Vue SSR 服务端渲染深度解析及实践

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_44781409/article/details/90720486

一 . SSR 的优缺点

  • 更利于SEO

不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本(Google除外,据说Googlebot可以运行javaScript)。使用了Vue或者其它MVVM框架之后,页面大多数DOM元素都是在客户端根据js动态生成,可供爬虫抓取分析的内容大大减少。另外,浏览器爬虫不会等待我们的数据完成之后再去抓取我们的页面数据。服务端渲染返回给客户端的是已经获取了异步数据并执行JavaScript脚本的最终HTML,网络爬中就可以抓取到完整页面的信息。

  • 更利于首屏渲染

首屏的渲染是node发送过来的html字符串,并不依赖于js文件了,这就会使用户更快的看到页面的内容。尤其是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载所有所需文件时间较长,首页就会有一个很长的白屏等待时间。

二 . SSR的局限

  • 服务端压力较大

    本来是通过客户端完成渲染,现在统一到服务端node服务去做。尤其是高并发访问的情况,会大量占用服务端CPU资源;

  • 开发条件受限

    在服务端渲染中,created和beforeCreate之外的生命周期钩子不可用,因此项 目引用的第三方的库也不可用其它生命周期钩子,这对引用库的选择产生了很大的限制;

  • 学习成本相对较高

    除了对webpack、Vue要熟悉,还需要掌握node、Express相关技术。相对于客户端渲染,项目构建、部署过程更加复杂。

了解完服务端渲染的有缺点后下面开始详细分析一波

三 . 解析构建流程
来先看一眼官方构建图
在这里插入图片描述

  • app.js / main.js入口文件

    app.js是我们的通用entry,它的作用就是构建一个Vue的实例以供服务端和客户端使用,注意一下,在纯客户端的程序中我们的app.js将会挂载实例到dom中,而在ssr中这一部分的功能放到了Client entry中去做了。

  • 两部分入口entry.js
    我们来看Client entry和Server entry,这两者分别是客户端的入口和服务端的入口。Client entry的功能很简单,就是挂载我们的Vue实例到指定的dom元素上;Server entry是一个使用export导出的函数。主要负责调用组件内定义的获取数据的方法,获取到SSR渲染所需数据,并存储到上下文环境中。这个函数会在每一次的渲染中重复的调用。

  • webpack打包构建
    我们的服务端代码和客户端代码通过webpack分别打包,生成Server Bundle和Client Bundle,前者会运行在服务器上通过node生成预渲染的HTML字符串,发送到我们的客户端以便完成初始化渲染;而客户端bundle就自由了,初始化渲染完全不依赖它了。客户端拿到服务端返回的HTML字符串后,会去“激活”这些静态HTML,是其变成由Vue动态管理的DOM,以便响应后续数据的变化。

  • 解析运行流程

    这里我们该谈谈ssr的程序是怎么跑起来的了。首先我们得去构建一个vue的实例,也就是我们前面构建流程中说到的app.js做的事情,但是这里不同于传统的客户端渲染的程序,我们需要用一个工厂函数去封装它,以便每一个用户的请求都能够返回一个新的实例,也就是官网说到的避免交叉污染了。

    然后我们可以暂时移步到服务端的entry中了,这里要做的就是拿到当前路由匹配的组件,调用组件里定义的一个方法(官网取名叫asyncData)拿到初始化渲染的数据,而这个方法要做的也很简单,就是去调用我们vuex store中的方法去异步获取数据。

    接下来node服务器如期启动了,跑的是我们刚写好的服务端entry里的函数。在这里还要做的就是将我们刚刚构建好的Vue实例渲染成HTML字符串,然后将拿到的数据混入我们的HTML字符串中,最后发送到我们客户端。

    打开浏览器的network,我们看到了初始化渲染的HTML,并且是我们想要初始化的结构,且完全不依赖于客户端的js文件了。再仔细研究研究,里面有初始化的dom结构,有css,还有一个script标签。script标签里把我们在服务端entry拿到的数据挂载了window上。原来只是一个纯静态的HTML页面啊,没有任何的交互逻辑,所以啊,现在知道为啥子需要服务端跑一个vue客户端再跑一个vue了,服务端的vue只是混入了个数据渲染了个静态页面,客户端的vue才是去实现交互的!

四 . SSR服务端渲染注意点

在SSR中,创建Vue实例、创建store和创建router都是套了一层工厂函数的,目的就是避免数据的交叉污染。

  • 注意点一

    服务端只能执行生命周期中的created和beforeCreate,原因是在服务端是无法操纵dom的,所以可想而知其他的周期也就是不能执行的了。

  • 注意点二
    服务端渲染和客户端渲染不同,需要创建两个entry分别跑在服务端和客户端,并且需要webpack对其分别打包;

  • 注意点三
    SSR服务端请求不带cookie,需要手动拿到浏览器的cookie传给服务端的请求。实现方式戳这里

  • 注意点四
    SSR要求dom结构规范,因为浏览器会自动给HTML添加一些结构比如tbody,但是客户端进行混淆服务端放回的HTML时,不会添加这些标签,导致混淆后的HTML和浏览器渲染的HTML不匹配。

  • 注意点五
    性能问题需要多加关注。
    vue.mixin、axios拦截请求使用不当,会内存泄漏。
    lru-cache向内存中缓存数据,需要合理缓存改动不频繁的资源。如何实现戳这里

五 .完整项目模板

利用 webpack 3 可以非常快速的搭建一个简单的 vue 开发环境,你也可以走捷径(git地址)
项目目录如下所示

├─.babelrc // babel 配置文件
├─index.template.html // html 模板文件
├─server.js // 提供服务端渲染及 api 服务
├─src // 前端代码
| ├─app.js // 主要用于创建 vue 实例
| ├─App.vue // 根组件
| ├─entry-client.js // 客户端渲染入口文件
| ├─entry-server.js // 服务端渲染入口文件
| ├─stores // vuex 相关
| ├─routes // vue-router 相关
| ├─components // 组件
├─dist // 代码编译目标路径
├─build // webpack 配置文件

六 . 正式开始构建项目

以下配置是根据在vue项目中改造完成

项目构建完成后的基本架构

在这里插入图片描述

在这里插入图片描述

打包运行在客户端文件的配置 , 在build中添加webpack.client.config.js文件

const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.conf.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const SWPrecachePlugin = require('sw-precache-webpack-plugin')
const uglify = require('uglifyjs-webpack-plugin');
//压缩html配置
const htmlPlugin = require('html-webpack-plugin')

const config = merge(base, {
  entry: './src/entry-client.js',
  plugins: [
    new uglify(),//打包压缩
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
      'process.env.VUE_ENV': '"client"'
    }),
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function (module) {
        // any required modules inside node_modules are extracted to vendor
        return (
          // it's inside node_modules
          /node_modules/.test(module.context) &&
          // and not a CSS file (due to extract-text-webpack-plugin limitation)
          !/\.css$/.test(module.request)
        )
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest'
    }),
    new VueSSRClientPlugin(),
    // 拷贝静态文件
    // new CopyWebpackPlugin([
    //     {
    //         from: path.resolve(__dirname, '../static'),
    //         to: conf.build.assetsSubDirectory,
    //         ignore: ['.*']
    //     }
    // ]),
    new htmlPlugin({
      /*压缩文件,removeAttributeQuotes指去掉属性的双引号,目前你随便不用也行*/
      minify: {
        removeAttributeQuotes: true
      },
      /*加入hash值,为了避免浏览器缓存js*/
      hash: true,
      /*要打包的html文件的路径及名称*/
      template: './index.template.html'
    }),
    new VueSSRClientPlugin()
  ]
})
if (process.env.NODE_ENV === 'production') {
  config.plugins.push(
    // auto generate service worker
    new SWPrecachePlugin({
      cacheId: 'vue-hn',
      filename: 'service-worker.js',
      minify: true,
      dontCacheBustUrlsMatching: /./,
      staticFileGlobsIgnorePatterns: [/\.map$/, /\.json$/],
      runtimeCaching: [
        {
          urlPattern: '/',
          handler: 'networkFirst'
        },
        {
          urlPattern: /\/(top|new|show|ask|jobs)/,
          handler: 'networkFirst'
        },
        {
          urlPattern: '/item/:id',
          handler: 'networkFirst'
        },
        {
          urlPattern: '/user/:id',
          handler: 'networkFirst'
        }
      ]
    })
  )
}

module.exports = config

打包运行在服务端文件的配置 , 在build中添加webpack.server.config.js文件

const webpack = require('webpack')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const base = require('./webpack.base.conf.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const uglify = require('uglifyjs-webpack-plugin');

module.exports = merge(base, {
  // 将 entry 指向应用程序的 server entry 文件
  entry: './src/entry-server.js',

  // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
  // 并且还会在编译 Vue 组件时,
  // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
  target: 'node',

  // 对 bundle renderer 提供 source map 支持
  devtool: 'source-map',

  // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
  output: {
    filename: 'server-bundle.js',
    libraryTarget: 'commonjs2'
  },

  // https://webpack.js.org/configuration/externals/#function
  // https://github.com/liady/webpack-node-externals
  // 外置化应用程序依赖模块。可以使服务器构建速度更快,
  // 并生成较小的 bundle 文件。
  externals: nodeExternals({
    // 不要外置化 webpack 需要处理的依赖模块。
    // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
    // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
    whitelist: /\.(scss|sass|css|less)$/
  }),

  // 这是将服务器的整个输出
  // 构建为单个 JSON 文件的插件。
  // 默认文件名为 `vue-ssr-server-bundle.json`
  plugins: [
    new uglify(),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
      'process.env.VUE_ENV': '"server"'
    }),
    new VueSSRServerPlugin()
  ]
})

修改公共配置文件webpack.base.conf.js

var path = require('path');
var utils = require('./utils');
var config = require('../config');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const webpack = require('webpack')

function resolve (dir) {
    return path.join(__dirname, '..', dir)
}
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
    devtool: isProd
        ? false
        : '#cheap-module-source-map',
    // 入口文件
    entry: {
        app: ["babel-polyfill", "./src/main.js"]
    },
    // 出口文件
    output: {
        path: config.build.assetsRoot,
        filename: '[name].js',
        publicPath: '/dist/',
    },
    resolve: {
        // 自动补全文件后缀
        extensions: ['.js', '.vue', '.json'],
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
            '@': resolve('src'),
            'src': path.resolve(__dirname, '../src'),
            'assets': path.resolve(__dirname, '../src/assets'),
            'components': path.resolve(__dirname, '../src/components'),
            'views': path.resolve(__dirname, '../src/views'),
            'styles': path.resolve(__dirname, '../src/styles'),
            'api': path.resolve(__dirname, '../src/api'),
            'utils': path.resolve(__dirname, '../src/utils'),
            'store': path.resolve(__dirname, '../src/store'),
            'router': path.resolve(__dirname, '../src/router'),
            'mock': path.resolve(__dirname, '../src/mock'),
            'vendor': path.resolve(__dirname, '../src/vendor'),
            'static': path.resolve(__dirname, '../static'),

        }
    },
    module: {
        noParse: /es6-promise\.js$/, // avoid webpack shimming process
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {
                    // // enable CSS extraction
                    // extractCSS: isProd,
                    compilerOptions: {
                        preserveWhitespace: false
                    }
                }
            },
            {
                test: /\.js$/,
                loader: 'babel-loader?cacheDirectory',
                include: [resolve('src'), resolve('test')]
            },
            // 解析图片配置
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                loader: 'url-loader',
                query: {
                    limit: 10000, // 小于10000会转成base64格式
                    name: utils.assetsPath('img/[name].[hash:7].[ext]')
                }
            },
            {
                test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
                loader: 'url-loader',
                query: {
                    limit: 10000,
                    name: utils.assetsPath('fonts/[name].[ext]')
                }
            },
            // 解析css文件
            {
                test: /\.css$/,
                // ��Ҫ��ʹ�� vue-style-loader ��� style-loader
                use: isProd
                    ? ExtractTextPlugin.extract({
                        use: 'css-loader',
                        fallback: 'vue-style-loader'
                    })
                    : ['vue-style-loader', 'css-loader']
            },
            // 解析less文件
            {
                test: /\.less$/,
                use: [{
                    loader: "vue-style-loader"
                }, {
                    loader: "css-loader"
                }, {
                    loader: "less-loader"
                }]
            }
        ],
        loaders: [{    // babel loader
            test: /\.js?$/,
            exclude: /node_modules/,
            loader: "babel-loader"
        }, {
            test: /\.(png|jpg|jpng)$/, // pack images
            loader: 'url-loader?limit=8192&name=resource/image/[name]-[hash:8].[ext]'
        },
        {
            test: /\.(woff|woff2|eot|ttf|svg)(\?[a-z0-9]+)?$/,
            loader: 'url-loader?limit=1000&name=resource/fonts/[name]-[hash:8].[ext]'
        },
        {
            test: /\.ejs$/,
            loader: 'ejs-loader',
        },
        {
            test: /\.(scss|sass|css|less)$/,  // pack sass and css files
            loader: ExtractTextPlugin.extract({ fallback: "style-loader", use: "css-loader!postcss-loader!sass-loader" })
        }
        ]
    },
    performance: {
        hints: false
    },
    plugins: isProd
        ? [
            new VueLoaderPlugin(),
            new webpack.optimize.UglifyJsPlugin({
                compress: { warnings: false }
            }),
            new webpack.optimize.ModuleConcatenationPlugin(),
            new ExtractTextPlugin({ filename: 'common.[chunkhash].css' })
        ]
        : [
            new VueLoaderPlugin(),
            new FriendlyErrorsPlugin()
        ]
}

在build中添加setup-dev-server.js文件

const fs = require('fs')
const path = require('path')
const MFS = require('memory-fs')
const webpack = require('webpack')
const chokidar = require('chokidar')
const clientConfig = require('./webpack.client.config')
const serverConfig = require('./webpack.server.config')

const readFile = (fs, file) => {
  try {
    return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
  } catch (e) {}
}

module.exports = function setupDevServer (app, templatePath, cb) {
  let bundle
  let template
  let clientManifest

  let ready
  const readyPromise = new Promise(r => { ready = r })
  const update = () => {
    if (bundle && clientManifest) {
      ready()
      cb(bundle, {
        template,
        clientManifest
      })
    }
  }

  // read template from disk and watch
  template = fs.readFileSync(templatePath, 'utf-8')
  chokidar.watch(templatePath).on('change', () => {
    template = fs.readFileSync(templatePath, 'utf-8')
    console.log('index.html template updated.')
    update()
  })

  // modify client config to work with hot middleware
  clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
  clientConfig.output.filename = '[name].js'
  clientConfig.plugins.push(
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  )

  // dev middleware
  const clientCompiler = webpack(clientConfig)
  const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
    publicPath: clientConfig.output.publicPath,
    noInfo: true
  })
  app.use(devMiddleware)
  clientCompiler.plugin('done', stats => {
    stats = stats.toJson()
    stats.errors.forEach(err => console.error(err))
    stats.warnings.forEach(err => console.warn(err))
    if (stats.errors.length) return
    clientManifest = JSON.parse(readFile(
      devMiddleware.fileSystem,
      'vue-ssr-client-manifest.json'
    ))
    update()
  })

  // hot middleware
  app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 }))

  // watch and update server renderer
  const serverCompiler = webpack(serverConfig)
  const mfs = new MFS()
  serverCompiler.outputFileSystem = mfs
  serverCompiler.watch({}, (err, stats) => {
    if (err) throw err
    stats = stats.toJson()
    if (stats.errors.length) return

    // read bundle generated by vue-ssr-webpack-plugin
    bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
    update()
  })

  return readyPromise
}

在src文件下添加打包运行在客户端入口文件 src / entry-client.js . 里面可能涉及原项目部分内容请酌情删改

import Vue from 'vue'
import 'es6-promise/auto'
import { createApp } from './main'
import {getToken} from 'utils/util';
import { Base64 } from 'js-base64';
import NProgress from 'nprogress'; // Progress 进度条
import 'nprogress/nprogress.css';// Progress 进度条 样式


Vue.mixin({
  beforeRouteUpdate (to, from, next) {
    const { asyncData } = this.$options
    if (asyncData) {
      asyncData({
        store: this.$store,
        route: to
      }).then(next).catch(next)
    } else {
      next()
    }
  }
})
const { app, router, store } = createApp()

// prime the store with server-initialized state.
// the state is determined during SSR and inlined in the page markup.
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}
if(store.getters.needLoginFlag){
  //递归获取菜单数据,并将其修改为符合路由规则的数据
  store.dispatch('GenerateRoutes').then((createdRouters) => {
  router.addRoutes(createdRouters) // 动态添加可访问路由表
  })
}else{
  store.dispatch('GenerateRoutesNoLogin').then((createdRouters) => { // 生成可访问的路由表
  router.addRoutes(createdRouters) // 动态添加可访问路由表
  })
}
router.onReady(() => {
// 添加路由钩子函数,用于处理 asyncData.
  // 在初始路由 resolve 后执行,
  // 以便我们不会二次预取(double-fetch)已有的数据。
  // 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
  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))
    })
    const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _)
    if (!asyncDataHooks.length) {
      return next()
    }
    NProgress.start() // 开启Progress
    // 这里如果有加载指示器 (loading indicator),就触发
    Promise.all(asyncDataHooks.map(hook => hook({ store, route: to })))
      .then(() => {
        NProgress.done() // 结束Progress
        next()
      })
      .catch(next)
  })
router.beforeEach((to, from, next) => { 
  //如果已经有token了,就直接登陆系统
  if(localStorage.getItem("TOKEN") || !store.getters.needLoginFlag){
      next();
    }else{
      //如果没有token,则获取token值
        const tgc = getToken('TGC');
        const u = getToken('U');
        const tokenNums = getToken('_TOKENUUMS');
        /**
          * 判断是否已登录
          * 未登录跳转到无访问权限页
          * 已登录获取工号,存到 localStorage 中
          */
        if (!tgc || !u || !tokenNums) {
          //TODO dispatch(routerRedux.push('/noAccess'));
        //FIXME 这个需要跳转到dpboot中的action中,
          store.dispatch('loginToServer');

        } else {
          const token = Base64.decode(tokenNums);
          localStorage.setItem("TOKEN",tokenNums);
          // store.dispatch('setUserToken',tokenNums);
          const tokenArr = token.split(',');
          //1,设置用户信息
          store.dispatch('setUserInfo',{userName:tokenArr[1],deptCode: tokenArr[3],deptName: tokenArr[4]});
          //2,产生动态路由,生成菜单树
          next();

          //FIXME 拿到 token后,可以访问
        }
    }
})
  // actually mount to DOM
  app.$mount('#app')
})

// service worker
if ('https:' === location.protocol && navigator.serviceWorker) {
  navigator.serviceWorker.register('/service-worker.js')
}

在src文件下添加打包运行在服务端入口文件 src / entry-server.js

// entry-server.js
import { createApp } from './main'

const isDev = process.env.NODE_ENV !== 'production'

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp()
    if (store.getters.needLoginFlag) {
      //递归获取菜单数据,并将其修改为符合路由规则的数据
      store.dispatch('GenerateRoutes').then((createdRouters) => {
        router.addRoutes(createdRouters) // 动态添加可访问路由表
        const s = isDev && Date.now()
        const { url } = context
        const { fullPath } = router.resolve(url).route
        if (fullPath !== url) {
          return reject({ url: fullPath })
        }
        // set router's location
        router.push(url)
        router.onReady(() => {
          const matchedComponents = router.getMatchedComponents()
          if (!matchedComponents.length) {// no matched routes
            return reject({ code: 404 })
          }

          // 对所有匹配的路由组件调用 `asyncData()`
          Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
            store,
            route: router.currentRoute
          }))).then(() => {
            // 在所有预取钩子(preFetch hook) resolve 后,
            // 我们的 store 现在已经填充入渲染应用程序所需的状态。
            // 当我们将状态附加到上下文,
            // 并且 `template` 选项用于 renderer 时,
            // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
            context.state = store.state
            resolve(app)
          }).catch(reject)
        }, reject)
      })
    } else {
      store.dispatch('GenerateRoutesNoLogin').then((createdRouters) => { // 生成可访问的路由表
        router.addRoutes(createdRouters) // 动态添加可访问路由表
        const s = isDev && Date.now()
        const { url } = context
        const { fullPath } = router.resolve(url).route
        if (fullPath !== url) {
          return reject({ url: fullPath })
        }
        // set router's location
        router.push(url)
        router.onReady(() => {
          const matchedComponents = router.getMatchedComponents()
          if (!matchedComponents.length) {// no matched routes
            return reject({ code: 404 })
          }

          // 对所有匹配的路由组件调用 `asyncData()`
          Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
            store,
            route: router.currentRoute
          }))).then(() => {
            // 在所有预取钩子(preFetch hook) resolve 后,
            // 我们的 store 现在已经填充入渲染应用程序所需的状态。
            // 当我们将状态附加到上下文,
            // 并且 `template` 选项用于 renderer 时,
            // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
            context.state = store.state
            resolve(app)
          }).catch(reject)
        }, reject)
      })
    }
  })
}

在根目录下创建server.js文件添加可运行的node代码

const fs = require('fs')
const path = require('path')
const LRU = require('lru-cache')
const resolve = file => path.resolve(__dirname, file)
const express = require('express')
const compression = require('compression')
const { createBundleRenderer } = require('vue-server-renderer')
const microcache = require('route-cache')
const isProd = process.env.NODE_ENV === 'production'
const useMicroCache = process.env.MICRO_CACHE !== 'false'
const serverInfo =
  `express/${require('express/package.json').version} ` +
  `vue-server-renderer/${require('vue-server-renderer/package.json').version}`


const app = express()

function createRenderer (bundle, options) {
  // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer
  return createBundleRenderer(bundle, Object.assign(options, {
    // this is only needed when vue-server-renderer is npm-linked
    cache: LRU({
      max: 1000,
      maxAge: 1000 * 60 * 5
    }),
    basedir: resolve('./dist'),
    // recommended for performance
    runInNewContext: false
  }))
}

let renderer
let readyPromise

const templatePath = resolve('./dist/index.html')
if (isProd) {
  // In production: create server renderer using template and built server bundle.
  // The server bundle is generated by vue-ssr-webpack-plugin.
  const template = fs.readFileSync(templatePath, 'utf-8')
  const bundle = require('./dist/vue-ssr-server-bundle.json')
  // The client manifests are optional, but it allows the renderer
  // to automatically infer preload/prefetch links and directly add <script>
  // tags for any async chunks used during render, avoiding waterfall requests.
  const clientManifest = require('./dist/vue-ssr-client-manifest.json')
  renderer = createRenderer(bundle, {
    template,
    clientManifest
  })
} else {
  // 开发环境使用webpack热更新服务
  // In development: setup the dev server with watch and hot-reload,
  // and create a new renderer on bundle / index template update.
  readyPromise = require('./build/setup-dev-server')(
    app,
    templatePath,
    (bundle, options) => {
      renderer = createRenderer(bundle, options)
    }
  )
}
const serve = (path, cache) => express.static(resolve(path), {
  maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
})

app.use(compression({ threshold: 0 }))
app.use('/', serve('./dist', true))
app.use('/static', serve('./static', true))
app.use('/favicon.ico', serve('./favicon.ico', true))
app.use('/dist', serve('./dist', true))
app.use('/manifest.json', serve('./manifest.json', true))
app.use('/service-worker.js', serve('./dist/service-worker.js'))


app.use(microcache.cacheSeconds(1, req => useMicroCache && req.originalUrl))

function render (req, res) {
  const s = Date.now()

  res.setHeader("Content-Type", "text/html")
  res.setHeader("Server", serverInfo)

  const handleError = err => {
    if (err.url) {
      res.redirect(err.url)
    } else if (err.code === 404) {
      res.status(404).send('404 | Page Not Found')
    } else {
      // Render Error Page or Redirect
      res.status(500).send('500 | Internal Server Error')
      console.error(`error during render : ${req.url}`)
      console.error(err.stack)
    }
  }

  const context = {
    title: 'dpboot前端框架', // default title
    url: req.url
  }
  renderer.renderToString(context, (err, html) => {
    if (err) {
      return handleError(err)
    }
    res.send(html)
    if (!isProd) {
      console.log(`whole request: ${Date.now() - s}ms`)
    }
  })
}


// // 组件缓存
// export default {
//   name: 'Home',
//   title () {
//     return {
//       title: 'vue-ssr',
//       keywords: 'vue-ssr服务端脚手架, home',
//       description: 'vue-ssr-template, vue-server-renderer, home'
//     }
//   },
//   asyncData ({ store }) { },
//   serverCacheKey: props => props.id
// }

app.get('*', isProd ? render : (req, res) => {
  readyPromise.then(() => render(req, res))
})




const port = process.env.PORT || 8080
app.listen(port, () => {
  console.log(`server started at localhost:${port}`)
})

修改package.json文件执行打包命令

 "scripts": {
    "build": "rimraf dist && npm run build:client && npm run build:server",
    "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules",
    "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules"
  },

配置好以上文件可尝试运行npm run build 命令, 若打包成功即可运行 node server.js 启动项目
以上配置皆由本人实践成功后所得经验 , 对于不同项目可能有不同差别 , 请灵活配置 .

猜你喜欢

转载自blog.csdn.net/weixin_44781409/article/details/90720486