• 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",
    

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

    其他配置

    • 服务器路由缓存,加快编译
  • 相关阅读:
    入手了一个南京电表厂MF16袖珍万用电表,哥测的不是电,是情怀
    40元淘来的香橙派 orange pi one 全志h3竟然可以安装桌面使用浏览器firefox上网
    https://blog.csdn.net/daaikuaichuan/article/details/83862311
    Arthas
    elect、poll、epoll优缺点
    jvm调优神器——arthas
    Mysql事物与二阶段提交
    全网最全一篇数据库MVCC详解,不全你打我
    冒泡排序和选择排序
    快速排序(过程图解)
  • 原文地址:https://www.cnblogs.com/pengdt/p/12304026.html
Copyright © 2020-2023  润新知