构建同构渲染
构建流程
源码结构
我们需要使用
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