vue ssr实战1(从无到有搭建ssr)

Posted by Start Bootstrap on March 03, 2018
  1. 前期准备工作
    vue init webpack vue-ssr-demo
    cd vue-ssr-demo
    npm i -D vue-server-renderer
    
  2. 增加路由test页面
<template>
  <div>
    Just a test page.
    <div>
      <router-link to="/">Home</router-link>
    </div>
  </div>
</template>
<script>
  export default {
  }
</script>
  1. 在 src 目录创建entry-client.js和entry-server.js
// entry-client.js
import { createApp } from './main'
const { app, router } = createApp()
// 因为可能存在异步组件,所以等待router将所有异步组件加载完毕,服务器端配置也需要此操作
router.onReady(() => {
  app.$mount('#app')
})
// entry-server.js
import { createApp } from './main'
export default context => {
  // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
  // 以便服务器能够等待所有的内容在渲染前,
  // 就已经准备就绪。
  return new Promise((resolve, reject) => {
    const { app, router } = createApp()
    // 设置服务器端 router 的位置
    router.push(context.url)
    // 等到 router 将可能的异步组件和钩子函数解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      // 匹配不到的路由,执行 reject 函数,并返回 404
      if (!matchedComponents.length) {
        // eslint-disable-next-line
        return reject({ code: 404 })
      }
      // Promise 应该 resolve 应用程序实例,以便它可以渲染
      resolve(app)
    }, reject)
  })
}
  1. 修改router配置 无论什么系统路由总是最重要的,服务器端渲染自然也要公用一套路由系统,并且为了避免产生单例的影响,这里主要只为每一个请求都导出一个新的router实例:
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'

Vue.use(Router)

export function createRouter () {
  return new Router({
    mode: 'history', // 注意这里也是为history模式
    routes: [
      {
        path: '/',
        name: 'Hello',
        component: HelloWorld
      }, {
        path: '/test',
        name: 'Test',
        component: () => import('@/components/Test') // 异步组件
      }
    ]
  })
}
  1. 修改main.js main.js初始化的只适合在浏览器的运行,所以要改造两端都可以使用的文件,同样为了避免产生单例的影响,这里将导出一个createApp的工厂函数:
import Vue from 'vue'
import App from './App'
import { createRouter } from './router'

export function createApp () {
  // 创建 router 实例
  const router = new createRouter()
  const app = new Vue({
    // 注入 router 到根 Vue 实例
    router,
    render: h => h(App)
  })
  // 返回 app 和 router
  return { app, router }
}
  1. 修改webpack客户端配置 修改webpack.base.conf.js的entry入口配置为: ./src/entry-client.js。这样原 dev 配置与 prod 配置都不会受到影响。在prod配置中引入一个插件,并配置到plugin中即可:
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
	// ...
	// ...
	plugins: [
      new webpack.DefinePlugin({
        'process.env': env,
        'process.env.VUE_ENV': '"client"' // 增加process.env.VUE_ENV
      }),
      //...
      // 另外需要将 prod 的HtmlWebpackPlugin 去除,因为我们有了vue-ssr-client-manifest.json之后,服务器端会帮我们做好这个工作。
      // new HtmlWebpackPlugin({
      //   filename: config.build.index,
      //   template: 'index.html',
      //   inject: true,
      //   minify: {
      //     removeComments: true,
      //     collapseWhitespace: true,
      //     removeAttributeQuotes: true
      //     // more options:
      //     // https://github.com/kangax/html-minifier#options-quick-reference
      //   },
      //   // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      //   chunksSortMode: 'dependency'
      // }),

      // 此插件在输出目录中
      // 生成 `vue-ssr-client-manifest.json`。
      new VueSSRClientPlugin()
	]
// ...
  1. 修改webpack客户端配置 server的配置有用到新插件运行安装: npm i -D webpack-node-externals,新建一个webpack.server.conf.js文件,配置如下:
const webpack = require('webpack')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.conf.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
// 去除打包css的配置
baseConfig.module.rules[1].options = ''

module.exports = merge(baseConfig, {
  // 将 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: {
    libraryTarget: 'commonjs2'
  },
  // https://webpack.js.org/configuration/externals/#function
  // https://github.com/liady/webpack-node-externals
  // 外置化应用程序依赖模块。可以使服务器构建速度更快,
  // 并生成较小的 bundle 文件。
  externals: nodeExternals({
    // 不要外置化 webpack 需要处理的依赖模块。
    // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
    // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
    whitelist: /\.css$/
  }),
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
      'process.env.VUE_ENV': '"server"'
    }),
    // 这是将服务器的整个输出
    // 构建为单个 JSON 文件的插件。
    // 默认文件名为 `vue-ssr-server-bundle.json`
    new VueSSRServerPlugin()
  ]
})
  1. 修改package.json的打包命令
"scripts": {
	//...
	"build:client": "node build/build.js",
    "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules",
    "build": "rimraf dist && npm run build:client && npm run build:server"
}
  1. 修改index.html
<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8">
    <title>vue-ssr-demo</title>
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

原来的<div id="app">删掉,只在 body 中保留一个标记即可:。 服务器端会在这个标记的位置自动生成一个<div id="app" data-server-rendered="true">,客户端会通过app.$mount(‘#app’)挂载到服务端生成的元素上,并变为响应式的。注意一下,此处将模板 html 修改为服务端渲染适用的模板了,但项目中的 dev 模式也适用的这个模板,但会因为找不到#app到报错,可以为dev模式单独建立一个 html 模板,如下:

// index.dev.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>vue-ssr-demo</title>
  </head>
  <body>
    <div id="app">
  </body>
</html>

  1. 编写服务器端代码(express),创建server.js
const express = require('express')
const app = new express()
const fs = require('fs')
const path = require('path')
const {
  createBundleRenderer
} = require('vue-server-renderer')
const resolve = file => path.resolve(__dirname, file)

const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {
  // 推荐
  runInNewContext: false,
  // 模板html文件
  template: fs.readFileSync(resolve('./index.html'), 'utf-8'),
  // client manifest
  clientManifest: require('./dist/vue-ssr-client-manifest.json')
})

app.use(express.static(resolve('./dist'), {
  maxAge: 1000 * 60 * 60
}))

app.get('*', (req, res) => {
  if (!renderer) {
    return res.end('waiting for compilation... refresh in a moment.')
  }
  const s = Date.now()
  res.setHeader('Content-Type', 'text/html')
  const errorHandler = err => {
    if (err && err.code === 404) {
      res.status(404).end('404 | Page Not Found')
    } else {
      // Render Error Page or Redirect
      res.status(500).end('500 | Internal Server Error')
      console.error(`error during render : ${req.url}`)
      console.error(err)
    }
  }
  renderer.renderToStream({
      url: req.url
    })
    .on('error', errorHandler)
    .on('end', () => console.log(`whole request: ${Date.now() - s}ms`))
    .pipe(res)
})

app.listen(3001)

  1. 运行命令,大功告成
npm run build
node server.js

image.png image.png

代码呢?在这里