• Vue服务端渲染


    一、服务端渲染 - 简介

    所谓服务端渲染就是将代码的渲染交给服务器,服务器将渲染好的html字符串返回给客户端,再由客户端进行显示。

    服务器端渲染的优点

    有利于seo搜索引擎优化,因为服务端渲染是将渲染好的html字符串返回给了客户端,所以其可以被爬虫爬取到;
    加快首屏渲染时间,不会出现白屏;

    服务器端渲染的缺点

    SSR会占用更多的CPU和内存资源
    vue中一些常用的浏览器API可能无法使用,比如vue的生命周期在服务器端渲染只能使用beforeCreate()和created(),因为服务端呈现的仅仅是html字符串是没有所谓的mount的。
     

    二、服务端渲染 - 初体验

    使用Vue的服务端渲染功能,需要引入Vue提供的服务端渲染模块vue-server-renderer,其作用是创建一个渲染器,该渲染器可以将Vue实例渲染成html字符串

    用Koa来搭建一个web服务器来实现:

    ① 目录结构

    ② 创建一个server.js 文件

    const Koa = require("koa");
    const Router = require("koa-router");
    const fs = require("fs");
    
    const app = new Koa(); // 创建服务器端app实例
    const router = new Router(); // 创建服务器端路由
    
    const Vue = require("vue");
    const VueServerRender = require("vue-server-renderer"); // 引入服务端渲染模块
    const vm = new Vue({ // 创建Vue实例
        data() {
            return {msg: "hello vm"}
        },
        template: `<div>{{msg}}</div>` // 渲染器会将vue实例中的数据填入模板中并渲染成对应的html字符串
    });
    const template = fs.readFileSync("./server.template.html", "utf8"); // 读取基本的html结构
    const render = VueServerRender.createRenderer({
        template
    }); // 创建渲染器并以server.template.html作为html页面的基本结构
    
    router.get("/", async ctx => {
        // ctx.body = await render.renderToString(vm);
        ctx.body = await new Promise((resolve, reject) => {
                render.renderToString(vm, (err, html) => { // 将vm实例渲染成html并插入到server.template.html模板中
                console.log(`${html}`);
            });
        );
    });
    
    app.use(router.routes()); // 添加路由中间件
    
    app.listen(3000, () => {
        console.log("node server listening on port 3000.");
    }); // 监听3000端口
    

    注意:

    • server.template.html文件中必须有 <!--vue-ssr-outlet-->占位符,即将Vue实例vm渲染成的html字符串插入到占位符所在的位置;
    • render.renderToString(vm)方法不传回调函数的时候返回的是Promise对象,但是如果传入了回调函数,那么就返回void了, 推荐自己创建一个Promise函数;
    • Vue服务端渲染出来的字符串中会包含data-server-rendered="true"这样一个标识,标识这是由Vue服务端渲染的结果字符
    <div data-server-rendered="true">hello vm</div>
     

    三、服务端渲染 - 引入Vue项目

    上面初体验中,我们已经实现了一个简单的Vue服务端渲染,但是我们实际中Vue是一个很大的项目,里面是包含了很多组件的大型应用,而不是像初体验中的一个简单的Vue实例,所以我们必须引入一个Vue项目,包括Vue的入口文件main.js、App.vue、components、public/index.html等,如:

    通过webpack来打包我们的整个Vue项目,webpack将以Vue的根实例main.js作为入口文件,打包出一个合并的最终的bundle.js和一个页面入口index.html文件,该index.html文件引入bundle.js后就能加载整个Vue项目中的页面以及页面中的事件等等,这里我们的Vue项目是一个很简单的模板项目,关键在于webpack的配置

    // webpack.config.js

    const path = require("path");
    const resolve = (dir) => {
        return path.resolve(__dirname, dir);
    }
    const VueLoader = require("vue-loader/lib/plugin");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    module.exports = {
        entry: resolve("./src/main.js"), // webpack 入口, 即Vue的入口文件main.js
        output: {
            filename: "bundle.js", // 打包后输出的结果文件名
            path: resolve("./dist") // 打包后输出结果存放目录
        },
        resolve: {
            extensions: [".js", ".vue"] // 没有写扩展名的时候,解析顺序
        }, 
        module: {
            rules: [
                {
                    test: /.js$/, 
                    use: {
                        loader: "babel-loader", // 将所有的js文件通过babel-loader转换为ES5代码
                        options: {
                            presets: ["@babel/preset-env"]
                        }
                    },
                    exclude: /node_modules/
                },
                {
                    test: /.css$/, // 解析.vue文件中的css
                    use: [
                        "vue-style-loader", "css-loader"
                    ]
                },
                {
                    test: /.vue$/, // 解析.vue文件,需要配合其中的插件进行使用
                    use: "vue-loader"
                }
            ]
        },
        plugins: [
            new VueLoader(), // 解析.vue文件的插件
            new HtmlWebpackPlugin({
                filename: 'index.html', // 打包后输出的html文件名
                template: resolve("./public/index.html") // 该模板文件在哪
            })
        ]
    }
    打包输出后的dist目录中会出现两个文件: bundle.js和index.html, 直接在本地点击index.html文件即可执行并呈现整个Vue项目
     

    四、服务端渲染 - 将Vue项目分割为客户端和服务端

    ① 在非服务端渲染的时候,我们使用的打包入口文件是main.js,其主要就是创建了一个Vue实例,并且渲染App.vue,然后将渲染好的App.vue挂载到index.html文件#app元素中,但是我们的服务端渲染是无法mount的,也就是说无法将渲染结果渲染到#app元素上,所以需要改造main.js文件

    // 改造后的main.js文件

    import Vue from "vue";
    import App from "./App";
    /** 
     1. main.js在服务端渲染中的作用就是提供一个Vue项目的根实例,所以导出一个函数
     2. 让客户端和服务端都能获取到Vue项目的根实例,然后根据需要,
     3. 客户端通过手动调用$mount()进行挂载
     4. */
    export default () => {
        const app = new Vue({
            render: h => h(App)
        });
        return {app}; // 返回整个Vue根实例
    }

    ② 新建两个入口文件: client-entry.js 和 server-entry.js
    // client-entry.js

    import createApp from "./main";
    const {app} = createApp(); // 获取到Vue项目根实例
    app.$mount("#app"); // 将根实例挂载到#app上
    此时将webpack.config.js的入口文件改成client-entry.js应该和之前是一样的

    // server-entry.js

    import createApp from "./main";
    /** 
     * 服务端需要调用当前这个文件产生一个Vue项目的根实例
     * 由于服务端与客户端是1对多的关系,所以不能每个客户端访问都返回同一个Vue项目根实例
     * 所以需要返回一个函数,该函数返回一个新的Vue项目根实例
     * */ 
    export default () => {
        const {app} = createApp(); // 获取到Vue项目根实例
        return app;
    }
    为什么客户端入口文件就不需要暴露一个一个函数?因为客户端可以被访问多次,即多次执行,每次执行返回的都是一个新的Vue项目实例了。而服务器只会启动一次,但是却需要每次客户端访问都返回一个新的Vue项目实例,所以必须放到函数中

    ③ 拆分webapck.config.js, 将其分成两个配置文件,同样一个用于客户端,一个用于服务端打包
    由于客户端和服务端的webpack配置文件有很多是相同的,所以可以抽取出一个webpack.base.js

    // webpack.base.js

    const path = require("path");
    const resolve = (dir) => {
        return path.resolve(__dirname, dir);
    }
    const VueLoader = require("vue-loader/lib/plugin");
    module.exports = {
        output: {
            filename: "[name].bundle.js", // 打包后输出的结果文件名
            path: resolve("./../dist/") // 打包后输出结果存放目录
        },
        resolve: {
            extensions: [".js", ".vue"] // 没有写扩展名的时候,解析顺序
        }, 
        module: {
            rules: [
                {
                    test: /.js$/, 
                    use: {
                        loader: "babel-loader", // 将所有的js文件通过babel-loader转换为ES5代码
                        options: {
                            presets: ["@babel/preset-env"]
                        }
                    },
                    exclude: /node_modules/
                },
                {
                    test: /.css$/, // 解析.vue文件中的css
                    use: [
                        "vue-style-loader", "css-loader"
                    ]
                },
                {
                    test: /.vue$/, // 解析.vue文件,需要配合其中的插件进行使用
                    use: "vue-loader"
                }
            ]
        },
        plugins: [
            new VueLoader(), // 解析.vue文件的插件
        ]
    }

    // webpack-client.js

    const merge = require("webpack-merge");
    const base = require("./webpack.base");
    const path = require("path");
    const resolve = (dir) => {
        return path.resolve(__dirname, dir);
    }
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    
    module.exports = merge(base, {
        entry: {
            client: resolve("./../src/client-entry.js"), // 给客户端入口文件取名client,output的时候可以获取到该名字动态输出
        },
        plugins: [
            new HtmlWebpackPlugin({
                filename: 'index.html', // 打包后输出的html文件名
                template: resolve("./../public/index.html") // 该模板文件在哪
            })
        ]
    });

    // webpack-server.js

    const merge = require("webpack-merge");
    const base = require("./webpack.base");
    const path = require("path");
    const resolve = (dir) => {
        return path.resolve(__dirname, dir);
    }
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    
    module.exports = merge(base, {
        entry: {
            server: resolve("./../src/server-entry.js"), // 给客户端入口文件取名client,output的时候可以获取到该名字动态输出
        },
        target: "node", // 给node使用
        output: {
            libraryTarget: "commonjs2" // 把最终这个文件导出的结果放到module.exports上
        },
        plugins: [
            new HtmlWebpackPlugin({
                filename: 'index.server.html', // 打包后输出的html文件名
                template: resolve("./../public/index.server.html"), // 该模板文件在哪
                excludeChunks: ["server"] // 排除某个模块, 不让打包输出后的server.bundle.js文件引入到index.server.html文件中
            })
        ]
    });
    服务端webpack配置文件比较特殊,在output的时候需要配置一个libraryTarget,因为默认webpack输出的时候是将打包输出结果放到一个匿名自执行函数中的,通过将libraryTarget设置为commonjs2,就会将整个打包结果放到module.exports上;
    服务端webpack打包后输出的server.bundle.js文件不是直接引入到index.server.html文件中使用的,还需要经过处理渲染成html字符串才能插入到index.server.html文件中,所以打包输出后,要在html-webpack-plugin中排除对该模块的引用
    由于webpack配置文件被分割,所以启动webapck-dev-server的时候需要指定配置文件,在package.json文件中添加脚本
    "scripts": {
        "client:dev": "webpack-dev-server --config ./build/webpack.client.js --mode development",
        "client:build": "webpack --config ./build/webpack.client.js --mode development",
        "server:build": "webpack --config ./build/webpack.server.js --mode development"
      },
    此时分别指向npm run client:build 和 npm run server:build即可在dist目录下生成index.html、client.bundle.js, index.server.html、server.bundle.js,其中client.bundel.js被index.html引用,server.bundle.js没有被index.server.html引入,index.server.html仅仅是拷贝到了dist目录下,同时server.bundle.js的整个输出结果是挂在module.exports下的

    资源搜索网站大全 http://www.szhdn.com 广州VI设计公司https://www.houdianzi.com

    ④ 将打包好的server.bundle.js交给服务器进行渲染并生成html字符串返回给客户端,和之前初体验一样,创建一个web服务器,只不过,这次不是渲染一个简单的Vue实例,而是渲染整个打包好的server.bundle.js

    vue-server-renderer提供了两种渲染方式:

    • 和初体验中的一样,把server.bundle.js当作简单Vue实例进行渲染,我们打包后server.bundle.js的内容都是挂到了module.exports上,所以我们可以直接require,require返回的结果是一个对象,该对象上只有一个属性即default,属性值为一个函数,执行该函数即可获取整个Vue项目对应的Vue实例。
    // 获取server.bundle.js中的Vue实例进行渲染
    const VueServerRender = require("vue-server-renderer"); // 引入服务端渲染模块
    const vm = require("./dist/server.bundle").default(); // 执行server.budle的default方法获取Vue实例
    const template = fs.readFileSync("./server.template.html", "utf8"); // 读取基本的html结构
    const render = VueServerRender.createRenderer({
        template
    }); // 创建渲染器并以server.template.html作为html页面的基本结构
    router.get("/", async ctx => {
        ctx.body = await new Promise((resolve, reject) => {
            render.renderToString(vm, (err, html) => { // 将vm实例渲染成html并插入到server.template.html模板中
                if (err) reject(err);
                console.log(`${html}`);
                resolve(html);
            });
        });
    });
    • 通过vue-server-renderer提供的createBundleRenderer()方法进行渲染,该方法需要传入server.bundle.js中的文件内容字符串, 再传入模板html即可,所以需要读取server.bundle.js中的内容:
    // 直接渲染server.bundle.js
    const VueServerRender = require("vue-server-renderer"); // 引入服务端渲染模块
    // 读取server.bundle.js中的内容,即文件中的字符串
    const ServerBundle = fs.readFileSync("./dist/server.bundle.js", "utf8");
    const template = fs.readFileSync("./dist/index.server.html", "utf8"); // 读取基本的html结构
    const render = VueServerRender.createBundleRenderer(ServerBundle, { // 传入server.bundle.js字符串创建渲染器
        template
    });
    router.get("/", async ctx => {
        ctx.body = await new Promise((resolve, reject) => {
            render.renderToString((err, html) => { // 将server.bundle.js渲染成html字符串
                if (err) reject(err);
                resolve(html);
            });
        });
    });
    重启服务器,再次访问,查看源码,可以看到页面已经不是一个空的基础页面了,而是真实包含html内容的页面,但是仍然存在一个问题,那就是之前的事件并不起作用了,因为服务器将sever.bundle.js渲染成的是html字符串返回给客户端的,是不包含事件的,其中的事件执行函数在client.bundle.js中,所以我们可以在index.server.html文件中通过script标签显式地引入client.bundle.js,如:
    <body>
        <!--vue-ssr-outlet-->
        <script src="client.bundle.js"></script>
    </body>

    注意: 当访问页面的时候,就会向服务器请求client.bundle.js文件,所以服务器需要将client.bundle.js以静态资源的方式发布出去。

    刚才我们是手动在index.server.html中通过script标签引入client.bundle.js, 非常的不方便,vue-server-renderer给我们提供了两个插件,vue-server-renderer/client-plugin和vue-server-renderer/server-plugin,可以在webpack配置文件中引入,那么打包的时候,会分别生成两个json文件,vue-ssr-client-manifest.json和vue-ssr-server-bundle.json,这两个文件主要是生成客户端和服务端bundle的对应关系,这样就不需要我们收到引入client.bundle.js了。
    之前是通过读取server.bundle.js的内容来渲染的,现在可以直接requirevue-ssr-server-bundle.json文件即可,同时在渲染的时候再添加vue-ssr-client-manifest.json即可,如:
    // 直接渲染server.bundle.js
    const VueServerRender = require("vue-server-renderer"); // 引入服务端渲染模块
    // 读取server.bundle.js中的内容,即文件中的字符串
    // const ServerBundle = fs.readFileSync("./dist/server.bundle.js", "utf8");
    const ServerBundle = require("./dist/vue-ssr-server-bundle.json");
    const clientManifest = require("./dist/vue-ssr-client-manifest.json");
    const template = fs.readFileSync("./dist/index.server.html", "utf8"); // 读取基本的html结构
    const render = VueServerRender.createBundleRenderer(ServerBundle, { // 传入server.bundle.js字符串创建渲染器
        template,
        clientManifest
    });
  • 相关阅读:
    python ddt数据驱动框架
    yarn的安装和使用【转】
    辅助色选取思路
    echarts y轴起始坐标值设置
    mapbox 图层视角设置
    vue-cli2使用store存储全局变量
    vue 数组不响应解决办法
    echarts之dataZoom配置项【转】
    Vue中使用地图平台MapboxGL【转载】
    快速入门MapboxGL【转载】
  • 原文地址:https://www.cnblogs.com/xiaonian8/p/14010356.html
Copyright © 2020-2023  润新知