目录
11. 配置package.json增加打包服务器端构建命令并修改原打包命令
14. 构建服务器端(官方例子使用的express,所以此 demo 将采用koa2来作为服务器端,当然,无论是 koa 与 express 都不重要…)
-
准备工作
使用 vue-cli
再次初始化一个项目:
vue init webpack vue-ssr-demo
然后,
cd vue-ssr-demo
npm install
npm run dev
确保初始化的项目可正常运行,接下来开始慢慢折腾吧~~
npm i -D vue-server-renderer
重要的是 vue-server-renderer 与 vue 版本必须一致匹配
-
2. 增加路由
test
与页面
随便写了个计数器,以验证服务端渲染时,vue 的机制会正常工作。
<template>
<div>
Just a test page.
<div>
<router-link to="/">Home</router-link>
</div>
<div><h2>{{mode}}</h2></div>
<div><span>{{count}}</span></div>
<div><button @click="count++">+1</button></div>
</div>
</template>
<script>
export default {
data () {
return {
mode: process.env.VUE_ENV === 'server' ? 'server' : 'client',
count: 2
}
}
}
</script>
-
3. 在
src
目录下创建两个js:
src
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
-
4. 修改
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') // 异步组件
}
]
})
}
-
5. 改造
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 }
}
-
6.
entry-client.js
加入以下内容:
import { createApp } from './main'
const { app, router } = createApp()
// 因为可能存在异步组件,所以等待router将所有异步组件加载完毕,服务器端配置也需要此操作
router.onReady(() => {
app.$mount('#app')
})
-
7.
entry-server.js
// 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)
})
}
-
8.
webpack
配置
vue
相关代码已处理完毕,接下来就需要对webpack
打包配置进行修改了。
官网推荐下面这种配置:
build
├── webpack.base.conf.js # 基础通用配置
├── webpack.client.conf.js # 客户端打包配置
└── webpack.server.conf.js # 服务器端打包配置
但vue-cli
初始化的配置文件也有三个:base、dev、prod
,我们依然保留这三个配置文件,只需要增加webpack.server.conf.js
即可。
-
9. webpack 客户端的配置
修改webpack.base.conf.js
的entry
入口配置为: ./src/entry-client.js
。这样原 dev 配置与 prod 配置都不会受到影响。
服务器端的配置也会引用base配置,但会将
entry
通过merge
覆盖为 server-entry.js。
生成客户端构建清单client manifest
好处:
- 在生成的文件名中有哈希时,可以取代
html-webpack-plugin
来注入正确的资源 URL。- 在通过 webpack 的按需代码分割特性渲染 bundle 时,我们可以确保对 chunk 进行最优化的资源预加载/数据预取,并且还可以将所需的异步 chunk 智能地注入为
<script>
标签,以避免客户端的瀑布式请求(waterfall request),以及改善可交互时间(TTI - time-to-interactive)。
其实很简单,在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()
]
// ...
-
10. 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()
]
})
注意此处对baseConfig删除了一个属性
baseConfig.module.rules[1].options = '' // 去除分离css打包的插件
-
11. 配置
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"
}
如果出现
cross-env
找不到,请安装npm i -D cross-env
-
12. 修改
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 中保留一个标记即可:<!--vue-ssr-outlet-->
。 服务器端会在这个标记的位置自动生成一个<div id="app" data-server-rendered="true">
,客户端会通过app.$mount('#app')
挂载到服务端生成的元素上,并变为响应式的。
注意一下,此处将模板 html 修改为服务端渲染适用的模板了,但项目中的
dev
模式也适用的这个模板,但会因为找不到#app
到报错,可以这样处理一下:
- 最简单的办法,为
dev
模式单独建立一个 html 模板。。。- 为
dev
模式也集成服务端渲染模式,这样无论生产环境与开发环境共同处于服务端渲染模式下也是相当靠谱的一件事。(官方例子是这样操作的)
-
13. 运行构建命令
npm run build
然后在dist
目录下可见生成的两个 json 文件: vue-ssr-server-bundle.json
与vue-ssr-client-manifest.json
。
这两个文件都会应用在 node 端,进行服务器端渲染与注入静态资源文件。
-
14. 构建服务器端(官方例子使用的
express
,所以此 demo 将采用koa2
来作为服务器端,当然,无论是 koa 与 express 都不重要…)
npm i -S koa
在项目根目录创建server.js
,内容如下
const Koa = require('koa')
const app = new Koa()
// response
app.use(ctx => {
ctx.body = 'Hello Koa'
})
app.listen(3001)
运行node server.js
,访问localhost:3001
,确保浏览器得到了Hello Koa
。
-
15. 编写服务端代码
需要安装koa静态资源中间件:
npm i -D koa-static
server.js
代码如下:
const Koa = require('koa')
const app = new Koa()
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')
})
function renderToString (context) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html)
})
})
}
app.use(require('koa-static')(resolve('./dist')))
// response
app.use(async (ctx, next) => {
try {
const context = {
title: '服务端渲染测试', // {{title}}
url: ctx.url
}
// 将服务器端渲染好的html返回给客户端
ctx.body = await renderToString(context)
// 设置请求头
ctx.set('Content-Type', 'text/html')
ctx.set('Server', 'Koa2 server side render')
} catch (e) {
// 如果没找到,放过请求,继续运行后面的中间件
next()
}
})
app.listen(3001)
运行启动服务命令:
node server.js
-
16. 大功告成
浏览器访问: localhost:3001/test
,截图为服务器渲染成功的页面
在test.vue
中的 data 属性便已证明服务器端渲染工作是正常的(mode: process.env.VUE_ENV === 'server' ? 'server' : 'client',
),但在客户端数据混合的时候,mode 是等于 client 的。
可以做到真实数据实时渲染,完全可供SEO
小蜘蛛尽情的爬来爬去
完全前后端同构,路由配置共享,不再影响服务器404
请求
-
缺
依旧只支持h5 history
的路由模式,(没办法,哈希就是提交不到服务器能咋办呢。。。)
配置比较麻烦、处理流程比较复杂 (比对预渲染插件,复杂太多)
约束较多,不能随心所欲的乱放大招
对服务器会造成较大的压力,既然让浏览器更快的渲染了,那就得以占用服务器的性能来买单了