美文网首页
SSR服务-nodejs+ express4 + webpack

SSR服务-nodejs+ express4 + webpack

作者: 摸着石头过河_崖边树 | 来源:发表于2020-07-22 16:57 被阅读0次

    we* 目录

    一、SSR与CSR对比
    二、各环境准备与插件安装
    三、express服务
    四、SSR服务渲染实现
    五、webpack与解析loader配置(本文重点)
    六、问题记录
    • 开始正文
    一、SSR与CSR对比
    渲染模式 原理 优点 缺点
    SSR-服务端渲染 客户端发送请求到服务端,服务端返回整个页面的HTML字符串给浏览器 SEO优化,首屏渲染,性能优化 性能全都依赖于服务器,前端界面开发可操作性不高
    CSR-客户端渲染 接口请求数据,前端动态处理和生成页面需要的结构和页面展示 用户交互多场景,vue的生命周期全 整体加载完速度慢,HTTP请求损耗严重等

    选型适用场景
    SSR适用于首页或者静态网页, CSR适用于交互页面

    二、各环境准备与插件安装

    我们使用的是nodejs,所以需要准备node 于 npm 一般开发都会有,本文使用

     node -v   // v14.5.0
     npm -v   // 6.14.5
    
    1. 初始化项目并安装所需要插件

      npm init       // 初始化项目
      

      创建package.json文件

    2. 安装插件
      express - 服务端框架
      vue
      vue-router - vue 框架
      vue-server-renderer - vue SSR渲染的核心框架

       "dependencies": {
       "express": "^4.17.1",
       "vue": "^2.6.11",
       "vue-router": "^3.3.4",
       "vue-server-renderer": "^2.6.11"
       }
      

    注意:

    • 推荐使用 Node.js 版本 6+。
    • vue-server-renderer 和 vue 必须匹配版本。
    • vue-server-renderer 依赖一些 Node.js 原生模块,因此只能在 Node.js 中使用
    三、express服务
    1. 在根目录创建一个server.js

      // 后台服务serve
       const express = require("express");
       const app = express();
      
       app.get('*',(request,response) => {
           response.end("start server ok");
         })
      
      //  3000 端口号   192.168.18.83 本机IP
       const server = app.listen(3000, "192.168.18.83",  () => {
            const host = server.address().address;
            const port = server.address().port;
            console.log("服务已启动,访问地址为 http://%s:%s", host, port)
       })
      
    2. 在package.json中配置启动服务脚本

        "scripts": {
           "serve": "node server.js"
         },
      
    3. 运行脚本

       npm  run  serve
      
    4. 结果 :浏览器输入http://192.168.18.83:3000/ 地址可以看到 “start server ok”
      说明我们的后台服务启动OK啦

    注意:过程中出现服务连接不上,请切换端口号,又可以你的端口被暂用

    四、SSR服务渲染实现

    SSR服务渲染分为简单版与加强版
    简单版: 修改serve.js 文件 简单实现渲染

     // 后台服务serve
    const express = require("express");
    const app = express();
    
    // 0. 导入vue 与 vue SSR渲染插件
    const Vue = require("vue");
    const vueServerRender = require("vue-server-renderer").createRenderer();
    
    
    app.get('*',(request,response) => {
    
    // 1. 创建vue
    const vueApp = new Vue({
        data:{
            message:"hello world,一切从hello world 开始"
        },
        template:`<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>Document</title>
        </head>
        <body>
              这是SSR页面
             <h2>{{message}}</h2>
        </body>
        </html>`
    });
    
    // 2.转化为html
    vueServerRender.renderToString(vueApp).then((html) => {
        response.end(html);
    }).catch(error => console.log(error));
    })
    
    const server = app.listen(3000, "192.168.18.83",  () => {
        const host = server.address().address;
        const port = server.address().port;
        console.log("服务已启动,访问地址为 http://%s:%s", host, port)
    })
    

    结果:浏览器输入http://192.168.18.83:3000/ 地址可以看到 页面渲染成功

    image.png

    加强版-最终实现(直接看代码)
    代码目录


    image.png

    serve.js 服务文件

     // 编译服务
     const compilerServer =  require("./build/compiler-server.js");
    
    // 后台服务serve
    const express = require("express");
    const app = express();
    
     // // vue --> html
     const vueRender = require("vue-server-renderer");
    
    app.get('*',(request,response) => {
    const {url} = request;
    response.status(200);
    response.header("Content-Type","text/html;charset-utf-8;");
    
    // 运行webpack 编译
    compilerServer((serverBundle,template) => {
        // console.log('serve ----',serverBundle);
        let render = vueRender.createBundleRenderer(serverBundle,{
            template,
            //  每次创建一个独立的上下文
            renInNewContext:false
        });
        render.renderToString({
            url:request.url
        }).then((html) => {
            response.end(html);
        }).catch(error => {
            if (error) {
                if (error.code === 404) {
                    response.status(404).end('Page not found')
                } else {
                    response.status(500).end('Internal Server Error')
                }
            } else {
                // response.end(html)
                response.end(JSON.stringify(error));
            }
    
        });
     });
    });
    
     // 端口号
    const config = require("./config/config.js");
    
    const server = app.listen(config.server.port, config.server.host, function () {
    const host = server.address().address;
    const port = server.address().port;
    console.log("服务已启动,访问地址为 http://%s:%s", host, port)
    })
    

    compiler-server.js 运行webpack 文件

      const webpackServeConfig = require("./webpack.server.conf.js");
      const webpack = require("webpack");
      const fs = require("fs");
      const path = require("path");
      //  读取内存中的.json文件
      const MFS = require("memory-fs");
    
      module.exports = (cb) =>{
      const webpackCompiler = webpack(webpackServeConfig);
      const mfs = new MFS();
       // 把文件保存到内存中
      webpackCompiler.outputFileSystem = mfs; 
    
    webpackCompiler.watch( {}, async (error, stats ) => {
        if(error) return console.log(error);
        stats = stats.toJson();
        stats.errors.forEach(error => console.log(error));
        stats.warnings.forEach(warning => console.log(warning));
    
        // //  获取server bundle的json文件 -  为什么要从从这里取文件,为什么是这个文件 原因参考https://ssr.vuejs.org/zh/guide/bundle-renderer.html
        const serverBundlePath = path.join(webpackServeConfig.output.path,'vue-ssr-server-bundle.json');
    
        const serverBundle = JSON.parse(mfs.readFileSync(serverBundlePath,"utf-8"));
        // 获取html模板路径读取文件
        const templateIndexPath =  path.join(__dirname,"../src/template/index.template.html");
        const template = fs.readFileSync(templateIndexPath,"utf-8");
        if(cb){
            cb(serverBundle,template);
        }
    });
     };
    

    entry-server.js 服务端入口文件

     import {createApp}  from "./app.js";
    
    export default (context) => {
        return  new Promise( (resolve, reject) => {
          const { url } = context;
          let { app, router } = createApp(context);
          router.push(url);
        //  router回调函数
        // 等到 router 将可能的异步组件和钩子函数解析完
        router.onReady(()=> {
            const matchedComponents = router.getMatchedComponents();
            if(!matchedComponents.length){
                return reject({
                    code:404,
                });
            }
            //   // Promise 应该 resolve 应用程序实例,以便它可以渲染
            resolve(app);
        },reject);
    });
    }
    

    index.template.html 模板文件

     <!DOCTYPE html>
      <html>
    <head>
      <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>模板</title>
     </head>
    <body>
    <div id="app">
          <!--vue-ssr-outlet-->
    </div>
    </body>
    </html>
    

    webpack.serve.conf.js 服务配置

    const webpack = require("webpack");
    const merge = require("webpack-merge").merge;
    const base = require("./webpack.base.conf.js");
    const utils = require('./utils');
    
      //  在服务端渲染中,所需要的文件都是使用require引入,不需要把node_modules文件打包
    const webpackNodeExternals = require("webpack-node-externals");
    const vueSSRServerPlugin = require("vue-server-renderer/server-plugin");
    
    module.exports = merge(base,{
    //  告知webpack,需要在node端运行
    target:"node",
    entry:"./src/entry-server.js",
    devtool:"source-map",
    output:{
        filename:'server-bundle.js',
        libraryTarget: "commonjs2"
    },
    module: {
        rules: utils.styleLoader({ sourceMap:true, usePostCSS: true })
    },
    externals:[
        webpackNodeExternals()
    ],
    plugins:[
        new vueSSRServerPlugin()
    ]
    });
    

    webpack.base.conf.js 基础配置

    'use strict'
    const path = require("path");
    const config = require("../config/config.js");
    const VueLoaderPlugin = require('vue-loader/lib/plugin');
    function resolve (dir) {
          return path.join(__dirname, '..', dir)
    }
    module.exports = {
        mode: 'development',
        entry: {
            app:"./src/entry-server.js",
        },
        output: {
            path: config.build.assetsRoot,
            publicPath: config.build.assetsPublicPath,
            filename: '[name].js',
    },
    resolve: {
        extensions: ['.js', '.vue', '.json'],
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
            '@': resolve('src'),
        }
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {
                    compilerOptions: {
                        preserveWhitespace: false
                    },
                    // 配置哪些引入路径按照模块方式查找
                    transformAssetUrls: {
                        video: ['src', 'poster'],
                        source: 'src',
                        img: 'src',
                        image: ['xlink:href', 'href'],
                        use: ['xlink:href', 'href']
                    }
                }
            },
            {
                // 它会应用到普通的 `.js` 文件
                // 以及 `.vue` 文件中的 `<script>` 块
                test: /\.js$/,
                loader: 'babel-loader',
            },
            {
              test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
              loader: 'url-loader',
              options: {
                esModule: false, // 必须配置 不让出现 src加载图片为object
              }
            },
    
        ]
    },
    plugins:[
        new VueLoaderPlugin(),
    ],
     }
    

    utils.js loader 加载工具类

    const path = require("path");
    const config = require("../config/config.js");
    
     // 加载路径
    exports.assetsPath = function (_path) {
        const assetsSubDirectory = process.env.NODE_ENV === 'production'
           ? config.build.assetsSubDirectory
            : config.dev.assetsSubDirectory
    
    const resultPath = path.posix.join(assetsSubDirectory, _path);
    console.log('------',resultPath);
    return resultPath;
     };
    
    // 加载所有的cssloader
    exports.cssLoaders= function (options) {
     options = options || { sourceMap : false};
    
    // 它会应用到普通的 `.css` 文件
    // // 以及 `.vue` 文件中的 `<style>` 块
     const cssLoader = {
         loader: 'css-loader',   // 解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码
         options: {
            sourceMap: options.sourceMap,
             // 开启 CSS Modules 设置之后会被生成唯一id
             modules: false,
         }
     };
    
    const postcssLoader = {
        loader: 'postcss-loader', // 使用 PostCSS 加载和转译 CSS/SSS 文件
        options: {
            sourceMap: options.sourceMap,
            config: {
                path: 'postcss.config.js'
            }
        }
    };
    
    function generalLoader(loaderName, generalOptions) {
       const loaderList = [cssLoader];
       if(loaderName){
           const item = {
               loader: loaderName + '-loader',
               options:Object.assign({}, generalOptions, {
                   //sourceMap: options.sourceMap
               })
           };
           loaderList.push(item);
       }
        if(options.usePostCSS){
            loaderList.push(postcssLoader);
        }
       // 是否分离css
        if(options.extract){
    
        }else {
            const item = {
                loader: "vue-style-loader"
            };
            return [item].concat(loaderList)
        }
    }
    
    const resultLoader = {
        css: generalLoader(),
        // 适配浏览器 增加前缀
        postcss: generalLoader('postcss',{
    
        }),
        less: generalLoader('less'),
        // 普通的 `.scss` 文件和 `*.vue` 文件中的 `<style lang="scss">` 块都应用它
        sass: generalLoader('sass', {
            indentedSyntax: true,
            sassOptions: {
                indentedSyntax: true
            }
        }),
        // 普通的 `.scss` 文件和 `*.vue` 文件中的 `<style lang="scss">` 块都应用它
        scss: generalLoader('sass'),
        // stylus: generalLoader('stylus'),
        // styl: generalLoader('stylus')
    };
    return resultLoader;
     };
    
    exports.styleLoader = function (options) {
    const outRules = [];
    const loaders = exports.cssLoaders(options);
    for (const extension in loaders) {
        const loader = loaders[extension];
        const  item = {
            test: new RegExp('\\.' + extension + '$'),
            use: loader
        };
        outRules.push(item);
        console.log('---------\n',item.test, item.use);
    }
    
    return outRules;
    }
    

    其次还有app.js App.vue 比较简单

    app.js

    import Vue from "vue";
    import createRouter from "./router/router.js";
    import App from "./App.vue";
    export function createApp(context) {
        const router = createRouter();
        const app = new Vue({
              router,
            // 注入 router 到根 Vue 实例
            render: h => h(App),
        });
        return {
            app,
            router
        };
      }
    

    App.vue

    <template>
    <div id="app">
        <div class="app-title">我就是一个页面</div>
        <div style="color: yellow">这个是测试style</div>
        <router-link to="/">首页</router-link>-->
        <router-link to="/about">关于</router-link>
        <router-view/>
    </div>
    </template>
    
    <script>
    export default {
        name: 'App'
    }
    </script>
    <style lang="scss" >
    #app{
     text-align: center;
     color: red;
     .app-title{
         color: purple;
     }
    }
    </style>
    

    基本所有的核心代码都已经贴出来,原理可有通过官网的一张图搞定,我没有复制

    五、webpack与解析loader配置(本文的核心)

    package.json文件说明:

     {
      "name": "ssr",
      "version": "1.0.0",
      "description": "ssr",
      "scripts": {
          "build-server": "webpack --config build/webpack.server.conf.js",
          "http": "node server.js"
       },
     "author": "ddd",
     "license": "ISC",
     "dependencies": {
          "express": "^4.17.1",
          "vue": "^2.6.11",
          "vue-router": "^3.3.4",
          "vue-server-renderer": "^2.6.11"
        },
      "devDependencies": {
          "@babel/core": "^7.10.5",
          "@babel/plugin-transform-runtime": "^7.10.5",
          "@babel/polyfill": "^7.10.4",
          "@babel/preset-env": "^7.10.4",
          "@babel/runtime": "^7.10.5",
          "autoprefixer": "^9.8.5",
          "babel-loader": "^8.1.0",
          "css-loader": "^3.6.0",
          "file-loader": "^6.0.0",
          "less": "^3.12.2",
          "less-loader": "^6.2.0",
          "memory-fs": "^0.5.0",
          "mini-css-extract-plugin": "^0.9.0",
          "node-sass": "^4.14.1",
          "postcss-loader": "^3.0.0",
          "postcss-scss": "^2.1.1",
          "sass-loader": "^9.0.2",
          "style-loader": "^1.2.1",
          "url-loader": "^4.1.0",
          "vue-loader": "^15.9.3",
          "vue-style-loader": "^4.1.2",
          "vue-template-compiler": "^2.6.11",
          "webpack": "^4.43.0",
          "webpack-cli": "^3.3.12",
          "webpack-dev-server": "^3.11.0",
          "webpack-merge": "^5.0.9",
           "webpack-node-externals": "^2.5.0"
     },
    }
    

    使用了这么多插件,这么多的loader 具体的作用是是什么,今天就来好好整整
    里面存在4类

      1. babel 编译
        babel是一个包含语法转换等诸多功能的工具链,通过这个工具链的使用可以使低版本的浏览器兼容最新的javascript语法

    "@babel/core": "^7.10.5",
    "@babel/plugin-transform-runtime": "^7.10.5",
    "@babel/runtime": "^7.10.5",
    "@babel/polyfill": "^7.10.4",
    "@babel/preset-env": "^7.10.4",
    "babel-loader": "^8.1.0",

    • @babel/core是babel的核心库,所有的核心Api都在这个库里,这些Api供babel-loader调用

    • @babel/plugin-transform-runtime: "^7.10.5", // 减少包的体积
      @babel/runtime: "^7.10.5",
      使用plugin-transform-runtime. transform-runtime的转换是非侵入性的,也就是它不会污染你的原有的方法。遇到需要转换的方法它会另起一个名字,否则会直接影响使用库的业务代码

    • @babel/preset-env这是一个预设的插件集合,包含了一组相关的插件,Bable中是通过各种插件来指导如何进行代码转换。该插件包含所有es6转化为es5的翻译规则, 比如箭头函数转换插件

    • @babel/polyfill: "^7.10.4", @babel/preset-env只是提供了语法转换的规则,但是它并不能弥补浏览器缺失的一些新的功能,如一些内置的方法和对象,如Promise,Array.from等,此时就需要polyfill来做js得垫片,弥补低版本浏览器缺失的这些新功能

    • babel-loader : "^8.1.0", // babel-loader了,它作为一个中间桥梁

    参考: https://www.tangshuang.net/7427.html

      1. loader 相关

    "css-loader": "^3.6.0", // 解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码
    "file-loader": "^6.0.0", // ,生成的文件的文件名就是文件内容的 MD5 哈希值并会保留所引用资源的原始扩展名。
    "less": "^3.12.2",
    "less-loader": "^6.2.0", // 加载和转译 LESS 文件 与less 一起出现
    "node-sass": "^4.14.1", // python 环境
    "postcss-loader": "^3.0.0", // 适配浏览器 增加前缀
    "postcss-scss": "^2.1.1",
    "autoprefixer": "^9.8.5", // 与postcss-loader 一起出现 增加前缀
    "sass-loader": "^9.0.2", // 加载和转译 SASS/SCSS 文件 与node-sass 一起
    "style-loader": "^1.2.1",
    "url-loader": "^4.1.0", // url-loader 功能类似于 file-loader,但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。
    比如加载图片
    "vue-loader": "^15.9.3", // 加载和转译 Vue 组件
    "vue-style-loader": "^4.1.2", // 加载和转译 Vue 组件 style 与style-loader 使用一个就可以

      1. webpack 相关

    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.12", // 脚手架
    "webpack-dev-server": "^3.11.0", // 开发环境
    "webpack-merge": "^5.0.9", // 合并webpack配置
    "webpack-node-externals": "^2.5.0" // 所需要的文件都是使用require引入,不需要把node_modules文件打包

      1. 其他

    "memory-fs": "^0.5.0", // 内存操作
    "mini-css-extract-plugin": "^0.9.0", // 分离css

    六、问题记录

    问题一: style 没有效果 class 变为唯一识别id


    image.png

    原因:css-loader 的 modules = true // 开启 CSS Modules 设置之后会被生成唯一id
    解决版本: 修改为false

      const cssLoader = {
         loader: 'css-loader',   // 解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码
         options: {
            sourceMap: options.sourceMap,
             // 开启 CSS Modules 设置之后会被生成唯一id
             modules: true,
         }
     };
    

    相关文章

      网友评论

          本文标题:SSR服务-nodejs+ express4 + webpack

          本文链接:https://www.haomeiwen.com/subject/xflzkktx.html