Vue-Server-Renderer

文章来自
带你五步学会Vue-SSR
Vue服务器端渲染
构建一个SSR应用程序
SSR热更新
github项目

Vue-SSR优缺点

  • 请求到的首屏页面是服务器渲染好的了,SEO很好
  • 但是对服务器的压力很大

image.png

image.png

安装插件

# 安装 vue-server-renderer
# 安装 lodash.merge
# 安装 webpack-node-externals
# 安装 cross-env
npm install vue-server-renderer lodash.merge webpack-node-externals cross-env --save-dev

# 安装 koa
# 安装 koa-static
npm install koa koa-static --save

改造vuex

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export function createStore() {
  return new Vuex.Store({
    state: {
       test: ''
    },
    mutations: {
      SET_TEST(state, data) {
        state.test = data;
      }
    },
    actions: {
      test({ commit },opt) { 
         //  异步查询
         commit('SET_TEST', data); 
      }
    }
  });
}

改造vue-route

import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);
export function createRouter() {
  return new Router({
    mode: 'history', // 注意这里要使用history模式,因为hash不会发送到服务端
    routes: []
  });
}

改造main.js

import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router';
import { createStore } from './store';

import './assets/css/style.scss';
import './assets/iconfont/iconfont.css';

Vue.config.productionTip = false;

export function createApp() {
  const router = createRouter();
  const store = createStore();
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  });
  return { app, router, store };
}

改造所有的vue文件

<template>
  <div>{{ test }}</div>
</template>

<script>
export default {
  // 这个方法需要主动调用,等于是自定义生命周期函数,而且必须是这个名字
  asyncData ({ store, route }) {
    return store.dispatch('test', route.params.id)
  },
  computed: {
    // 当asyncData被执行,vuex数据改变,导致computed发生改变
    test() {
      return this.$store.state.test
    }
  }
}
</script>

创建entry-client.js

import { createApp } from './main';

// 客户端特定引导逻辑……
const { app, router, store } = createApp();

// 如果有__INITIAL_STATE__变量,则将store的状态用它替换
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__);
}

router.onReady(() => {
  // 添加路由钩子函数,用于处理 asyncData方法
  router.beforeResolve((to, from, next) => {
    const matched = router.getMatchedComponents(to);
    const prevMatched = router.getMatchedComponents(from);

    // 我们只关心非预渲染的组件
    // 所以我们对比它们,找出两个匹配列表的差异组件
    let diffed = false;
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = prevMatched[i] !== c);
    });

    if (!activated.length) {
      return next();
    }

    Promise.all( activated.map(c => {
        // 把所有的vue里的asyncData方法一起执行了
        if (c.asyncData) {
          return c.asyncData({ store, route: to });
        }
      })
    ).then(() => {
        next();
    }).catch(next);
  });

  // 将Vue实例挂载到dom中,完成浏览器端应用启动
  app.$mount('#app');
});

创建entry-server.js

import { createApp } from './main';

export default context => {
  // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise
  // 以便服务器能够等待所有的内容在渲染前,就已经准备就绪。
  return new Promise((resolve, reject) => {
    const { app, store, router } = createApp();
    // 设置服务器端 router 的位置
    router.push(context.url);

    // 等到 router 将可能的异步组件和钩子函数解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();
      // 匹配不到的路由,执行 reject 函数,并返回 404
      if (!matchedComponents.length) {
        return reject({ code: 404 });
      }
      Promise.all( matchedComponents.map(c => {
          if (c.asyncData) {
            return c.asyncData({ store, route: router.currentRoute});
          }
        })
      ).then(() => {
          // 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中
          context.state = store.state;
          // 返回根组件
          resolve(app);
        })
        .catch(reject);
    }, reject);
  });
};

修改vue.config.js
有些教程是把这个分成三个配置文件,效果也一样

const path = require('path');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const nodeExternals = require('webpack-node-externals'); // 忽略node_modules文件夹中的所有模块
// const merge = require('lodash.merge');
// const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

const TARGET_NODE = process.env.WEBPACK_TARGET === 'node';
const target = TARGET_NODE ? 'server' : 'client'; //根据环境变量来指向入口


function resolve(dir) {
  return path.join(__dirname, dir);
}

module.exports = {
  //基本路径
  publicPath: process.env.NODE_ENV !== 'production' ? 'http://127.0.0.1:8080' : './',
  // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
  // productionSourceMap: false,
  // 输出文件目录
  outputDir: 'dist',
  css: {
    extract: process.env.NODE_ENV === 'production',
    sourceMap: true
    //向 CSS 相关的 loader 传递选项(支持 css-loader postcss-loader sass-loader less-loader stylus-loader)
  },
  configureWebpack: () => ({
    // 将 entry 指向应用程序的 server / client 文件
    entry: `./src/entry-${target}.js`,
    // 需要开启source-map文件映射,因为服务器端在渲染时,
    // 会通过Bundle中的map文件映射关系进行文件的查询
    devtool: 'source-map',
    // 服务器端在Node环境中运行,需要打包为类Node.js环境可用包(使用Node.js require加载chunk)
    // 客户端在浏览器中运行,需要打包为类浏览器环境里可用包
    target: TARGET_NODE ? 'node' : 'web',
    // 关闭对node变量、模块的polyfill
    node: TARGET_NODE ? undefined : false,
    output: {
      // 配置模块的暴露方式,服务器端采用module.exports的方式,客户端采用默认的var变量方式
      libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
    },
    // 外置化应用程序依赖模块。可以使服务器构建速度更快
    externals: TARGET_NODE
      ? nodeExternals({
          // 不要外置化 webpack 需要处理的依赖模块。
          // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
          // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
          whitelist: [/.css$/]
        })
      : undefined,
    optimization: {
      splitChunks: TARGET_NODE ? false : undefined
    },
    // 根据之前配置的环境变量判断打包为客户端/服务器端Bundle
    plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
  }),
  chainWebpack: config => {
    // 关闭vue-loader中默认的服务器端渲染函数
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        // merge(options, {
        //   optimizeSSR: false
        // });
        options.optimizeSSR = false;
        return options;
      });
    config.resolve.alias
      .set('@src', resolve('src'))
      .set('@api', resolve('src/api'))
      .set('@assets', resolve('src/assets'))
      .set('@comp', resolve('src/components'))
      .set('@views', resolve('src/views'));
  },
  devServer: {
    historyApiFallback: true,
    headers: { 'Access-Control-Allow-Origin': '*' }
    // port: 8088
    // proxy: { ... }
    // }
  },
  lintOnSave: false
};

创建index.temp.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>{{ title }}</title>
</head>
<body>
  <div id="app">
    <!--vue-ssr-outlet-->
  </div>
</body>
</html>

修改package.json

// 在原本的dev和build基础上加上
"build:server": "cross-env NODE_ENV=production WEBPACK_TARGET=node vue-cli-service build",

运行npm run build:server会生成两个json

创建service.js

const fs = require("fs");
const Koa = require("koa");
const path = require("path");
const koaStatic = require('koa-static')
const app = new Koa();

const resolve = file => path.resolve(__dirname, file);
// 开放dist目录
app.use(koaStatic(resolve('./dist')))

// 第 2 步:获得一个createBundleRenderer
const template = fs.readFileSync(resolve("./public/index.temp.html"), "utf-8");
const { createBundleRenderer } = require("vue-server-renderer");
const bundle = require("./dist/vue-ssr-server-bundle.json");
const clientManifest = require("./dist/vue-ssr-client-manifest.json");

const renderer = createBundleRenderer(bundle, {
  runInNewContext: false,
  template: template,
  clientManifest: clientManifest
});

function renderToString(context) {
  return new Promise((resolve, reject) => {
    renderer.renderToString(context, (err, html) => {
      err ? reject(err) : resolve(html);
    });
  });
}
// 第 3 步:添加一个中间件来处理所有请求
app.use(async (ctx, next) => {
  const context = {
    title: "ssr-test",
  };
  // 将 context 数据渲染为 HTML
  const html = await renderToString(context);
  ctx.body = html;
});

const port = 3000;
app.listen(port, function() {
  console.log(`server started at localhost:${port}`);
});

修改package.json

// 再加上
"dev:serve": "node server.js",

全部配置完成,先执行打包再启动服务

其他配置

  • 服务器路由缓存,加快编译
原文地址:https://www.cnblogs.com/pengdt/p/12304026.html