Bootstrap

Part3-4-1 搭建自己的SSR

构建同构渲染

构建流程

源码结构
我们需要使用 webpack 来打包我们的 Vue 应用程序。事实上,我们可能需要在服务器上使用
webpack 打包 Vue 应用程序,因为:
  • 通常 Vue 应用程序是由 webpack vue-loader 构建,并且许多 webpack 特定功能不能直接在Node.js 中运行(例如通过 file-loader 导入文件,通过 css-loader 导入 CSS)。
  • 尽管 Node.js 最新版本能够完全支持 ES2015 特性,我们还是需要转译客户端代码以适应老版浏览器。这也会涉及到构建步骤。
所以基本看法是,对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要「服务器 bundle 」然后用于服务器端渲染 (SSR) ,而「客户端 bundle」会发送给浏览器,用于混合静态标记。
现在我们正在使用 webpack 来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用 webpack 支持的所有功能。同时,在编写通用代码时,有一些 事项 要牢记在心。

一个基本项目可能像是这样:

App.vue
<template> 
<!-- 客户端渲染的入口节点 --> 
<div id="app">
 <h1>拉勾教育</h1>
</div> 
</template> 

<script> 
export default { 
    name: 'App' 
}
</script> 

<style> </style>
app.js
app.js 是我们应用程序的「通用 entry 」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实 例,并直接挂载到 DOM 。但是,对于服务器端渲染 (SSR) ,责任转移到纯客户端 entry 文件。 app.js 简单地使用 export 导出一个 createApp 函数:
import Vue from 'vue' 
import App from './App.vue' 

// 导出一个工厂函数,用于创建新的 
// 应用程序、router 和 store 实例 
export function createApp () { 
    const app = new Vue({ 
        // 根实例简单的渲染应用程序组件。 
        render: h => h(App) 
    })
    return { app } 
}
entry-client.js
客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:
import { createApp } from './app' 

// 客户端特定引导逻辑…… 
const { app } = createApp() 

// 这里假定 App.vue 模板中根元素具有 `id="app"` 
app.$mount('#app')
entry-server.js
服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)
import { createApp } from './app' 

export default context => { 
    const { app } = createApp() 
    return app 
}

构建配置

安装生产依赖
npm i vue vue-server-renderer express cross-env

安装开发依赖
npm i -D webpack webpack-cli webpack-merge 
webpack-node-externals @babel/core 
@babel/plugin-transform-runtime 
@babel/preset-env babel-loader 
css-loader url- loader file-loader 
rimraf vue-loader 
vue-template-compiler 
friendly-errors- webpack-plugin

配置文件及打包命令
(1 )初始化 webpack 打包配置文件

webpack.base.config.js  

/*** 公共配置 */
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const path = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const resolve = file => path.resolve(__dirname, file)
const isProd = process.env.NODE_ENV === 'production'

module.exports = {
  mode: isProd ? 'production' : 'development',
  output: {
    path: resolve('../dist/'),
    publicPath: '/dist/',
    filename: '[name].[chunkhash].js'
  },
  resolve: {
    alias: {
      // 路径别名,@ 指向 src 
      '@': resolve('../src/')
    },
    // 可以省略的扩展名 
    // 当省略扩展名的时候,按照从前往后的顺序依次解析
    extensions: ['.js', '.vue', '.json']
  },
  devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map',
  module: {
    rules: [
      // 处理图片资源 
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
            },
          },
        ],
      },
      // 处理字体资源 
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: ['file-loader',],
      },
      // 处理 .vue 资源 
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      // 处理 CSS 资源 
      // 它会应用到普通的 `.css` 文件 
      // 以及 `.vue` 文件中的 `<style>` 块 
      {
        test: /\.css$/,
        use: ['vue-style-loader', 'css-loader']
      },
      // CSS 预处理器,参考:https://vue-loader.vuejs.org/zh/guide/pre- processors.html 
      // 例如处理 Less 资源 
      {
        test: /\.less$/,
        use: ['vue-style-loader', 'css-loader', 'less-loader']
      },
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new FriendlyErrorsWebpackPlugin()
  ]
}

webpack.client.config.js

/*** 客户端打包配置 */
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

module.exports = merge(baseConfig, {
  entry: {
    app: './src/entry-client.js'
  },
  module: {
    rules: [
      // ES6 转 ES5 
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            cacheDirectory: true,
            plugins: ['@babel/plugin-transform-runtime']
          }
        }
      },
    ]
  },
  // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中, 
  // 以便可以在之后正确注入异步 chunk。 
  optimization: {
    splitChunks: {
      name: "manifest",
      minChunks: Infinity
    }
  },
  plugins: [
    // 此插件在输出目录中生成 `vue-ssr-client-manifest.json`。 
    new VueSSRClientPlugin()
  ]
})

webpack.server.config.js
/*** 服务端打包配置 */
const { merge } = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(baseConfig, {
  // 将 entry 指向应用程序的 server entry 文件 
  entry: './src/entry-server.js',
  // 这允许 webpack 以 Node 适用方式处理模块加载 
  // 并且还会在编译 Vue 组件时, 
  // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
  target: 'node',
  output: {
    filename: 'server-bundle.js',
    // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) 
    libraryTarget: 'commonjs2'
  },
  // 不打包 node_modules 第三方包,而是保留 require 方式直接加载 
  externals: [
    nodeExternals(
      {
        // 白名单中的资源依然正常打包 
        allowlist: [/\.css$/]
      }
    )
  ],
  plugins: [
    // 这是将服务器的整个输出构建为单个 JSON 文件的插件。 
    // 默认文件名为 `vue-ssr-server-bundle.json`
    new VueSSRServerPlugin()
  ]
})
2 )在 npm scripts 中配置打包命令
"scripts": {
    "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js",
    "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js",
    "build": "rimraf dist && npm run build:client && npm run build:server"
  },
运行测试:
npm run build:client 
npm run build:server 
npm run build
启动应用
server.js
const Vue = require('vue')
const express = require('express')
const fs = require('fs')
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const template = fs.readFileSync('./index.template.html', 'utf-8')
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {
  template,
  clientManifest
})

const server = express()

server.use('/dist', express.static('./dist'))

server.get('/', (req, res) => {
  renderer.renderToString({
    title: '拉勾教育',
    meta: `
      <meta name="description" content="拉勾教育" />
    `
  }, (err, html) => {
    if (err) {
      return res.status(500).end('Internal Server Error')
    }
    res.setHeader('Content-Type', 'text/html;charset=utf8')
    res.end(html)
  })
})

server.listen(3000, () => {
  console.log('server running at port 3000')
})

解析渲染流程

(1)服务端渲染

renderer.renderToString 渲染了什么? renderer 是如何拿到 entry-server 模块的?

createBundleRenderer 中的 serverBundle
server Bundle 是 Vue SSR 构建的一个特殊的 JSON 文件

entry:入口 files:所有构建结果资源列表 maps:源代码 source map 信息

server-bundle.js 就是通过 server.entry.js 构建出来的结果文件 最终把渲染结果注入到模板中

(2)客户端渲染 vue-ssr-client-manifest.json

publicPath:访问静态资源的根相对路径,与 webpack 配置中的 publicPath 一致 all:打包后的所有静态资源文件路径 initial:页面初始化时需要加载的文件,会在页面加载时配置到 preload 中 async:页面跳转时需要加载的文件,会在页面加载时配置到 prefetch 中 modules:项目的各个模块包含的文件的序号,对应 all 中文件的顺序;moduleIdentifier和 和all数组中文件的映射关系(modules对象是我们查找文件引用的重要数据)

构建开发模式

基本思路

生产模式

  • npm run build 构建
  • node server.js 启动应用

开发模式

  • 监视代码变动自动构建,热更新等功能
  • node server.js 启动应用

所以我们设计了这样的启动脚本:

"scripts": {
  ...
// 启动开发服务
"dev": "node server.js",
// 启动生产服务
  "start": "cross-env NODE_ENV=production && node server.js"
}

服务端配置:

1

;