Vue SSR

294 阅读4分钟

SSR概念: 将一个vue组件在服务器端渲染为HTML字符串并发送到浏览器,最后再将这些静态标记“激活”为可交换的应用程序的过程称为服务端渲染。

传统asp.net php jsp web渲染技术:c向服务端发请求,服务端进行查库拼接HTML等操作,然后返回给客户端。

spa: 用户发送请求到后台,返回的是一个普通的HTML的空结构,没有内容;空结构有了后再执行js,才会有ajax等操作发送到后端,才能把数据内容拿回来显示;至少要两次请求才能拿到;

首屏渲染速度慢(两次);空结构内容对SEO不友好;

SSR:输入URL首次请求服务端,在web服务端用vue模板拼接,解析HTML,这里会进行查库等异步操作,完成之后返回到客户端;

与传统不同点: 除了首屏渲染所需HTML字符串外,还有与客户端相关代码,比如spa相关的代码;是在首屏渲染完成之后才开始下载的;静态页面过来后会激活转换成spa,这时操作不在刷新页面就是一个spa页面了;

缺点:

开发条件受限: 有些代码只能在客户端执行,Mounted不在服务端执行(beforeCreate 和 created)在服务端执行;只有在先关代码转换成spa挂载的时候才能用;

构建部署要求多,服务端渲染器用node去渲染,了解服务端的渲染koa,express;如果后端用Java、PHP则需要写中间层的node服务器,构建部署要了解;

服务端压力变大,每次请求都是创建一个新的vue的实例;负载能承受;

 // router.js
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
//导出工厂函数
export function createRouter() {
  return new Router({
    mode: 'history',
    routes: [
        // 客户端没有编译器,这里要写成渲染函数
        { path: "/", component: { render: h => h('div', 'index page') } },
        { path: "/detail", component: { render: h => h('div', 'detail page') } }
    ] 
});
}

// 主文件
import Vue from "vue";
import App from "./App.vue";
import { createRouter } from "./router";
// 导出Vue实例工厂函数,为每次请求创建独立实例 // 上下文用于给vue实例传递参数
export function createApp(context) {
  const router = createRouter();
  const app = new Vue({
    router,
    context,
    render: h => h(App)
  });
  return { app, router };
}

// app.server.js
import { createApp } from "./main";
// 返回一个函数,接收请求上下文,返回创建的vue实例 export default context => {
// 这里返回一个Promise,确保路由或组件准备就绪 return new Promise((resolve, reject) => {
const { app, router } = createApp(context); // 跳转到首屏的地址
router.push(context.url);
// 路由就绪,返回结果
    router.onReady(() => {
      resolve(app);
    }, reject);
  });
};

// app.client.js
import { createApp } from "./main";
// 创建vue、router实例
const { app, router } = createApp(); // 路由就绪,执行挂载
router.onReady(() => {
  app.$mount("#app");
});

// vue.config.js
npm install webpack-node-externals lodash.merge -D
module.exports = {
  css: {
    extract: false
  },
  outputDir: './dist/'+target,
  configureWebpack: () => ({
    // 将 entry 指向应用程序的 server / client 文件
    entry:  `./src/entry-${target}.js`,
    // 对 bundle renderer 提供 source map 支持
    devtool: 'source-map',
    // target设置为node使webpack以Node适用的方式处理动态导入,
    // 并且还会在编译Vue组件时告知`vue-loader`输出面向服务器代码。
    target: TARGET_NODE ? "node" : "web",
    // 是否模拟node全局变量
    node: TARGET_NODE ? undefined : false,
    output: {
      // 此处使用Node⻛格导出模块
      libraryTarget: TARGET_NODE ? "commonjs2" : undefined
    },
    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 外置化应用程序依赖模块。可以使服务器构建速度更快,并生成较小的打包文件。
    externals: TARGET_NODE ? nodeExternals({
      // 不要外置化webpack需要处理的依赖模块。
      // 可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
      // 还应该将修改`global`(例如polyfill)的依赖模块列入白名单
      whitelist: [/\.css$/]
    }): undefined,
    optimization: {
      splitChunks: undefined
    },
    // 这是将服务器的整个输出构建为单个 JSON 文件的插件。
    // 服务端默认文件名为 `vue-ssr-server-bundle.json`
    // 客户端默认文件名为 `vue-ssr-client-manifest.json`。
    plugins: [TARGET_NODE ? new VueSSRServerPlugin() : newVueSSRClientPlugin()]
  }),
  chainWebpack: config => { // cli4项目添加
    if (TARGET_NODE) {
      config.optimization.delete('splitChunks')
    }
    config.module
      .rule("vue")
      .use("vue-loader")
      .tap(options => {
        merge(options, {
          optimizeSSR: false
        });
      });
  }
};


// entry-server.js
 
import { createApp } from "./app";
export default context => {
  return new Promise((resolve, reject) => {
  // 拿出store和router实例
  const { app, router, store } = createApp(context); router.push(context.url);
  router.onReady(() => {
    // 获取匹配的路由组件数组
    const matchedComponents = router.getMatchedComponents();
    // 若无匹配则抛出异常
    if (!matchedComponents.length) {
      return reject({ code: 404 });
    }
    // 对所有匹配的路由组件调用可能存在的`asyncData()`
    Promise.all(
        matchedComponents.map(Component => {
          if (Component.asyncData) {
            return Component.asyncData({
              store,
              route: router.currentRoute,
            });
          }
        }),
     ).then(() => {
        // 所有预取钩子 resolve 后,
        // store 已经填充入渲染应用所需状态
        // 将状态附加到上下文,且 `template` 选项用于 renderer 时,
        // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
 
        context.state = store.state;
          resolve(app);
        })
        .catch(reject);
      }, reject);
    });
};



 
// 导出store
const { app, router, store } = createApp();
// 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态自动嵌入到 最终的 HTML // 在客户端挂载到应用程序之前,store 就应该获取到状态:
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__);
}