记录一次vue-cli搭建vue2项目改造ssr全过程,附代码注释。
通过 vue-cli 创建 vue 项目 可参考我之前写的一篇文章 vue-cli4.0搭建vue项目
安装 vue-server-renderer 1 2 npm install vue vue-server-renderer --save
注意
推荐使用 Node.js 版本 6+。
vue-server-renderer 和 vue 必须匹配版本。
vue-server-renderer 依赖一些 Node.js 原生模块,因此只能在 Node.js 中使用。我们可能会提供一个更简单的构建,可以在将来在其他「JavaScript 运行时(runtime)」运行。
修改 src/router/index.js 文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const originalPush = VueRouter.prototype.pushVueRouter.prototype.push = function push (location ) { return originalPush.call(this , location).catch(err => err) } const routes = [ { path : '/' , name : 'Home' , component : () => import ( '../views/Home.vue' ) }, { path : '/about' , name : 'About' , component : () => import ( '../views/About.vue' ) } ] export function createRouter ( ) { return new VueRouter({ routes }) }
修改 src/store/index.js 文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state : { }, mutations : { }, actions : { }, modules : { } }) export function createStore ( ) { return store }
创建 src/app.js 入口文件 安装 vuex-router-sync
工具,该工具主要是将 store
跟 router
连接起来。详细了解可参考 Vuex Router Sync
1 2 npm install vuex-router-sync
代码修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import Vue from "vue" ;import App from './App.vue' import { createRouter } from "./router" ;import { createStore } from "./store" ;import { sync } from "vuex-router-sync" export function createApp ( ) { const router = createRouter() const store = createStore() sync(store, router) const app = new Vue({ router, store, render : h => h(App) }) return { app, router, store } }
创建 src/entry-server.js 文件 服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import { createApp } from "./app" export default context => { return new Promise ((resolve, reject ) => { const { app, router, store } = createApp() router.push(context.url) router.onReady(() => { const matchedComponents = router.getMatchedComponents() if (!matchedComponents.length) { return reject({ code : 404 }) } Promise .all(matchedComponents.map(Component => { if (Component.asyncData) { return Component.asyncData({ store, router : router.currentRoute }) } })).then(() => { context.state = store.state resolve(app) }) }, reject) }) }
创建 src/entry-client.js 文件 客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import { createApp } from './app' const { app, router, store } = createApp();if (window .__INITIAL_STATE__) { store.replaceState(window .__INITIAL_STATE__) } router.onReady(() => { 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() } Promise .all(activated.map(c => { if (c.asyncData) { return c.asyncData({ store, route : to }) } })).then(() => { next() }).catch(next) }) app.$mount('#app' ) })
创建页面模板 public/index.template.html 当你在渲染 Vue 应用程序时,renderer 只从应用程序生成 HTML 标记 (markup)。在这个示例中,我们必须用一个额外的 HTML 页面包裹容器,来包裹生成的 HTML 标记。注意 <!--vue-ssr-outlet-->
注释 – 这里将是应用程序 HTML 标记注入的地方,中间不能有空格。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="utf-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width,initial-scale=1.0" > <title > {{ title }}</title > </head > <body > </body > </html >
创建一个 Node.js web服务器 与服务器集成,在 Node.js 服务器中使用时相当简单直接,例如 Express
1 2 npm install express --save
项目根目录下创建 server.js 文件,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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/server/vue-ssr-server-bundle.json' );const clientManifest = require ('./dist/client/vue-ssr-client-manifest.json' );const template = fs.readFileSync(path.resolve('./public/index.template.html' ), 'utf-8' );const render = createBundleRenderer(serverBundle, { runInNewContext : false , template, clientManifest }); app.use(express.static('./dist/client' , { index : false })) app.get('*' , (req, res ) => { const context = { title : 'vue2-ssr-template' , 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} ` ); });
配置 vue.config.js
vue-cli4 脚手架搭建完成后,项目目录中没有 vue.config.js
文件,需要手动创建。vue.config.js
是一个可选的配置文件,如果项目的 (和 package.json
同级的) 根目录中存在这个文件,那么它会被 @vue/cli-service
自动加载。
根目录下创建 vue.config.js
,已创建可忽略。
注意: 配置文件中用到了 lodash.merge
合并对象,安装命令 npm install lodash.merge --save
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 const VueSSRServerPlugin = require ('vue-server-renderer/server-plugin' )const VueSSRClientPlugin = require ('vue-server-renderer/client-plugin' )const nodeExternals = require ('webpack-node-externals' )const merge = require ('lodash.merge' )const TARGET_NODE = process.env.WEBPACK_TARGET === 'node' const target = TARGET_NODE ? 'server' : 'client' module .exports = { css : { extract : false }, outputDir : "./dist/" + target, configureWebpack : () => ({ entry : `./src/entry-${target} .js` , devtool : 'source-map' , target : TARGET_NODE ? 'node' : 'web' , node : TARGET_NODE ? undefined : false , output : { libraryTarget : TARGET_NODE ? 'commonjs2' : undefined }, externals : TARGET_NODE ? nodeExternals({ allowlist : [/\.css$/ ] }) : undefined , optimization : { splitChunks : TARGET_NODE ? false : undefined }, plugins : [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()] }), chainWebpack : config => { config.module .rule('vue' ) .use('vue-loader' ) .tap(options => { return merge(options, { optimizeSSR : false }) }) } }
配置项目打包脚本 配置脚本需要用到 cross-env
插件,主要是用来运行跨平台设置和使用环境变量的脚本。
安装
1 npm install cross-env --save
使用
在 package.json 文件中,scripts选项下,添加以下脚本:
1 2 3 4 5 6 7 8 "scripts" : { "serve" : "vue-cli-service serve" , "build" : "npm run build:server && npm run build:client" , "build:client" : "vue-cli-service build" , "build:server" : "cross-env WEBPACK_TARGET=node vue-cli-service build" , "server" : "node server" , "start" : "npm run build:server && npm run build:client && npm run server" }
至此整个ssr改造完成,可通过 npm run start
在本地环境编译打包启动node服务,访问 http://localhost:3003
查看运行效果。
注意: 端口号可根据自己配置进行访问
查看网页源代码发现根元素上添加了一个特殊的属性:data-server-rendered
,该属性让客户端 Vue 知道这部分 HTML 是由 Vue 在服务端渲染的,并且应该以激活模式进行挂载
1 <div id ="app" data-server-rendered ="true" > ......</div >
最后 项目目录结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public ├── favicon.ico ├── index.html └── index.template.html src ├── components ├── router │ └── index.js ├── store │ └── index.js ├── app.js ├── App.vue ├── entry-client.js └── entry-server.js vue.config.js
打包后 dist
目录结构 1 2 3 4 5 6 7 8 9 client ├── img ├── js ├── favicon.ico ├── index.html ├── index.template.html └── vue-ssr-client-manifest.json server └── vue-ssr-server-bundle.json
参考资料 Vue CLI Vue.js Vue Router Vuex Vue SSR 指南