1、第一步没啥好说的,利用vue-cli脚手架创建一个demo项目:
vue init webpack vue-ssr-demo
cd vue-ssr-demo
npm install
2、安装SSR依赖的模块:
npm i -D vue-server-renderer
3、随便加一个components/test.vue
Just a test page.
export default {
data () {
return {
}
}
}
4、src目录下创建俩js文件:
src
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
5、修改router/index.js
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') // 异步组件
}
]
})
}
6、修改main.js里路由的引入:
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
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 }
}
7、在entry_client.js里面写上如下内容:
import { createApp } from './main';
const {app, store, router} = createApp();
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))
})
if (!activated.length) {
return next()
}
// 这里如果有加载指示器 (loading indicator),就触发
Promise.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
// 停止加载指示器(loading indicator)
next()
}).catch(next)
})
app.$mount('#app')
})
8、在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)
})
}
9、在build文件夹下新增一个webpack.server.conf.js,写上:
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.conf.js')
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(base, {
target: 'node',
devtool: '#source-map',
entry: './src/entry-server.js',
output: {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
},
resolve: {
alias: {
'create-api': './create-api-server.js'
}
},
// https://webpack.js.org/configuration/externals/#externals
// https://github.com/liady/webpack-node-externals
externals: nodeExternals({
// do not externalize CSS files in case we need to import it from a dep
allowlist: /\.css$/
}),
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
new VueSSRServerPlugin()
]
})
10、在webpack.prod.conf.js中加上:
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') // 开头引入这个
....
new webpack.DefinePlugin({
'process.env': env,
'process.env.VUE_ENV': '"client"' // 这里加一行process.env.VUE_ENV
}),
....
new VueSSRClientPlugin() // plugins数组里最后加一个这个
11、修改webpack.base.conf.js的entry:
entry: {
app: './src/entry-client.js' // 改为这个
},
12、配置package.json增加打包服务器端构建命令并修改原打包命令:
"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"
13、根目录新建一个index.template.html模板页面(或者直接修改原index.html,只是这样要区分生产环境和开发环境,否则run dev的时候就不ok):
14、根目录下新建一个server.js(没安装express的先npm install express):
const express = require('express');
const fs = require('fs');
const path = require('path');
const { createBundleRenderer } = require('vue-server-renderer');
const app = express();
const serverBundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');
const template = fs.readFileSync(path.resolve('./index.template.html'), 'utf-8'); // 这里就看13步骤里你是哪个文件
const render = createBundleRenderer(serverBundle, {
runInNewContext: false, // 推荐
template, // (可选)页面模板
clientManifest
});
app.use(express.static('./dist',{index:false}))
app.get('*', (req, res) => {
const context = { url: req.url }
render.renderToString(context, (err, html) => {
console.log(html)
// 处理异常……
res.end(html)
})
})
const port = 3003;
app.listen(port, function() {
console.log(`server started at localhost:${port}`);
});
15、准备就绪了兄弟们,因为是本地运行,偷偷把"build:server"里的NODE_ENV先等于development,运行:
npm run build
之后可以看到生成了dist目录:
16、然后,运行:
node server
成功启动server started at localhost:3003
打开localhost:3003,在控制台NetWork里看到如下,SSR服务端渲染成功。