前言
本篇文章将从一个实战案例的角度逐步解析vue服务器端渲染的具体实现方式,整个过程不使用第三方服务器端渲染框架,以讲解底层实现原理为主.
服务器端渲染(ssr)主要解决了以下两个问题.
- 提升了首页的加载速度,这对于单页应用而言有明显的优势.
- 服务端渲染优化了
seo.使用服务器端渲染的页面更容易被搜索引擎捕获从而提升网站排名,这一点非常重要.在一些To c的项目中,如果用户在搜索引擎里面输入关键字后,发现网站搜都搜不出来,那就更谈不上盈利了.只要是跟经济效益挂钩的技术,对于每一个技术人而言都应该重点关注.
在前后端还没分离的那个时代,像JAVA,PHP这些老牌编程语言.它们一直都在使用服务器渲染页面,并且多年的沉淀已经发展出了很多成熟的方案.
如今前后端分离已经覆盖了整个行业,前端程序员惯常使用三大框架vue,react和angular开发页面.一旦前端使用这些先进的框架开发出了页面,后台编程语言是JAVA或PHP,它们做ssr就有点束手无力了.老牌编程语言的ssr只能在自己的生态下做,所以这部分工作就落到了前端同学的头上.
前端一旦接手了ssr,可以让页面的开发模式和之前保持一致.之前是怎么开发单页面应用的现在依旧怎么开发,只不过是在原来的基础上增加了一些额外的配置.这样在成本花费很低的情况下既让前端程序员保留了过往的开发习惯又让应用支持了srr.
ssr到底在做什么事
服务器端渲染(srr),顾名思义,页面在后台渲染好后再发给前端展示.这要和客户端渲染对照来讲,看如下代码.
//index.js
import Vue from 'vue';
import App from '../App.vue';
new Vue({
render: (h) => h(App),
}).$mount('#app');
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<script src="http://www.xxx.com/main.js"></script>
</body>
</html>
前端在开发单页应用的时候会经常碰到上述代码.客户端渲染最明显的特征就是后端发送过来的index.html里面的app节点里面内容是空的.那整个客户端渲染流程很容易打通.
- 浏览器输入网址请求服务器
- 后端将一个不含有页面内容的
html发送给浏览器 - 浏览器接收到
html开始加载,当读到后面script处就开始向服务器请求js资源.此时html是不含有内容的空模板 - 后端收到请求便把
js发送给浏览器,浏览器收到后开始加载执行js代码. - 这个时候
vue开始接管了整个应用,它便开始加载App组件,但发现App组件里面有个异步请求的代码.浏览器便开始向后台发起ajax请求获取数据,数据得到后便开始渲染App组件的模板. App组件所有工作都做完后,vue便把App组件的内容插入到index.html里id为app的dom元素.
从上面客户端渲染的流程来看,后端发送给前台index.html是不包含页面内容的空模板,页面内容的渲染过程都是浏览器这边完成的,所以这种方式称为客户端渲染.
srr和客户端渲染最大区别就是上面第二步,后端直接将一个把内容都填充好的html发给浏览器渲染.
如此浏览器收到了html直接渲染就可以了,不需要自己再额外发送请求获取数据渲染模板,正因为这部分工作给省掉了,所以页面的加载速度会变得很流畅.其次由于发送过来的html本身就是有内容的,搜索引擎就能通过这些内容判端网站的类型和用处,这样便优化了seo.
小试牛刀
下面来通过一个非常简单的案例从宏观上感受一下服务器端渲染的过程.
import Koa2 from 'koa';
import { createRenderer } from 'vue-server-renderer';
import Vue from 'vue';
const renderer = createRenderer();
const app = new Koa2();
/**
* 应用接管路由
*/
app.use(async function(ctx) {
const vm = new Vue({
template:"<div>hello world</div>"
});
ctx.set('Content-Type', 'text/html;charset=utf-8');
const htmlString = await renderer.renderToString(vm);
ctx.body = `<html>
<head>
</head>
<body>
${htmlString}
</body>
</html>`;
});
app.listen(3000);
上面代码的逻辑十分简单,使用koa2搭建了一个web服务端,监听3000端口.
浏览器输入网址localhost:3000发起请求,请求便会进入到app.use里面的函数方法.在该函数里,首先定义了一个非常简单vue实例,随后设置一下响应的数据格式,告诉浏览器返回的数据是一段html.
重点来了,我们现在在服务器端创建了一个vue实例vm.
vm是什么?它是一个数据对象.
熟悉前后台交互的同学应该都清楚,前后端通信都是通过发送字符串作为数据格式的,比如API服务器通常采用的json字符串.vm它是一个对象,对象是不能直接发送给浏览器的,发送前必须要把vm转化成字符串.
怎么把一个vue实例转化成字符串呢?这个过程不能乱转化,因为在创建vue的实例过程中,可不光只有template这一个属性,我们还可以给它添加响应式数据data,我们还可以给它添加事件方法method.
十分庆幸vue官网提供了一个插件vue-server-renderer,它的作用就是把一个vue实例转化成字符串,使用这个包要先用npm安装.
通过renderer.renderToString这个方法,将vm作为参数传递进去运行,便很轻松的返回了vm转化后的字符串,如下.
<div data-server-rendered="true">hello world</div>
得到了内容字符串后,把它插入到html字符串中,最后发送给前端就大功告成了.此时页面上就会显示hello world.
从上面的案例,可以从宏观上把握服务器端渲染的整个脉络.
- 首先是要获取到当前这个请求路径是想请求哪个
vue组件 - 将组件数据内容填充好转化成字符串
- 最后把字符串拼接成
html发送给前端.
上面的vm是一个非常简单的vue实例,它只有一个template属性.现实业务中的vm要复杂一些,因为随着业务的增长会给vm集成路由和vuex.接下来一一讲解.
路由集成
一般而言项目不可能只有一个页面,集成路由的目的就是为了让一个请求路径匹配一个vue页面组件,方便项目管理.
在实现srr的任务里,主要工作是为了在客户端发送请求后能找出当前的请求路径是匹配哪个vue组件.
创建一个route.js,填写以下代码.
import Vue from 'vue';
import Router from 'vue-router';
import List from './pages/List';
import Search from './pages/Search';
//route.js
Vue.use(Router);
export const createRouter = () => {
return new Router({
mode: 'history',
routes: [
{
path: '/list',
component: List,
},
{
path: '/search',
component: Search,
},
{
path: '/',
component: List,
},
],
});
};
在route.js中定义好路由和页面组件,这和之前前端定义路由的方式一样.如果前端访问根路径,默认加载List组件.
App组件也和之前一样,里面只放了一个视口<router-view></router-view>展现内容.
回到服务器端的入口文件index.js中,引入上面定义的createRouter方法.
import Koa2 from 'koa';
import { createRenderer } from 'vue-server-renderer';
import Vue from 'vue';
import App from './App.vue';
import { createRouter, routerReady } from './route';
const renderer = createRenderer();
const app = new Koa2();
/**
* 应用接管路由
*/
app.use(async function(ctx) {
const req = ctx.request;
const router = createRouter(); //创建路由
const vm = new Vue({
router,
render: (h) => h(App),
});
router.push(req.url);
// 等到 router 钩子函数解析完
await routerReady(router);
const matchedComponents = router.getMatchedComponents();//获取匹配的页面组件
if (!matchedComponents.length) {
ctx.body = '没有找到该网页,404';
return;
}
ctx.set('Content-Type', 'text/html;charset=utf-8');
const htmlString = await renderer.renderToString(vm);
ctx.body = `<html>
<head>
</head>
<body>
${htmlString}
</body>
</html>`;
});
app.listen(3000);
- 使用
createRouter()方法创建一个路由实例对象router,把它注入到Vue实例中. - 随后执行
router.push(req.url),这一步非常关键.相当于告诉Vue实例,当前的请求路径已经传给你了,你快点根据路径寻找要渲染的页面组件. await routerReady(router);执行完毕后,就已经可以得到当前请求路径匹配的页面组件了.matchedComponents.length如果等于0,说明当前的请求路径和我们定义的路由没有一个匹配上,那么这里应该要定制一个精美的404页面返回给浏览器.matchedComponents.length不等于0,说明当前的vm已经根据请求路径让匹配的页面组件占据了视口.接下来只需要将vm转化成字符串发送给浏览器就可以了.
浏览器输入localhost:3000,经过上面的流程就能将List页面内容渲染出来.
vuex集成
同构
路由集成后虽然能够根据路径渲染指定的页面组件,但是服务器渲染也存在局限性.
比如你在页面组件模板上加一个v-click事件,结果会发现页面在浏览器上渲染完毕后事件无法响应,这样肯定会违背我们的初衷.
怎么解决这样的棘手的问题呢?我们还是要回到服务器端渲染的本质上来,它做的主要的事情就是返回一个填充满页面内容html给客户端,至于后面怎么样它就不管了.
事件绑定,点击链接跳转这些都是浏览器赋予的能力.因此可以借助客户端渲染来帮助我们走出困境.
整个流程可以设计如下.
- 浏览器输入链接请求服务器,服务器端将包含页面内容的
html返回,但是在html文件下要加上客户端渲染的js脚本. html开始在浏览器上加载,页面上已经呈现出静态内容了.当线程走到html文件下的script标签,开始请求客户端渲染的脚本并执行.- 此时客户端脚本里面的
vue实例开始接管了整个应用,它开始赋予原本后端返回的静态html各种能力,比如让标签上的事件绑定开始生效.
这样就将客户端渲染和ssr联合了起来.ssr只负责返回静态的html文件内容,目的是为了让页面快点展现出来.而客户端的vue实例在静态页面渲染后开始接管整个应用,赋予页面各种各样的能力,这种协作的方式就称为同构.
下面通过代码演示一遍上述流程加深理解.
为了实现同构,需要增加客户端渲染的代码.新建client/index.js作为webpack构建客户端脚本的入口.
import Vue from 'vue';
import App from '../App.vue';
import { createRouter } from '../route';
const router = createRouter(); //创建路由
new Vue({
router,
render: (h) => h(App),
}).$mount('#app', true);
webpack跑完上面的客户端代码,会把它们打包生成一个bundle.js.这里的代码和之前唯一有点区别的就是$mount('#app', true)后面多了一个true参数.
加上这个true的原因也好理解,由于ssr把渲染好的静态html发给浏览器渲染后,客户端开始接管应用.
但是当前这个路径所访问的页面已经被后台渲染好了,不需要客户端vue实例再渲染一遍.加个true参数就让客户端的vue实例只对当前的模板内容添加一些事件绑定和功能支持就行了.
在ssr的入口文件index.js里,需要添加如下代码.
import Koa2 from 'koa';
import { createRenderer } from 'vue-server-renderer';
import Vue from 'vue';
import staticFiles from 'koa-static';
import App from './App.vue';
import { createRouter, routerReady } from './route';
const renderer = createRenderer();
const app = new Koa2();
/**
* 静态资源直接返回
*/
app.use(staticFiles('public'));
/**
* 应用接管路由
*/
app.use(async function(ctx) {
... //省略
ctx.body = `<html>
<head>
</head>
<body>
${htmlString}
<script src="/bundle.js"></script>
</body>
</html>`;
});
app.listen(3000);
从上面修改的内容可以看出来仅仅只是在原来的基础上做了一点小修改,在ctx.body返回的html加了一个script标签让浏览器执行客户端渲染的配置.
为了让浏览器能够顺利请求到这个bundle.js,需要执行app.use(staticFiles('public')).这句代码的含义就是如果请求路径是静态资源,直接将public文件夹下的资源返回给客户端.
通过上面这一轮配置,就能使被ssr渲染的静态页面在客户端赋予各种能力.一旦html文件在浏览器上加载完毕后,ssr的使命就完成了,后面的所有事情比如页面跳转,交互操作都是客户端js脚本的vue实例在接管,到了此时就和前端之前熟悉的场景没有区别了.
vuex的配置
现在假设List.vue的模板内容如下.
<template>
<div class="list">
<p>当前页:列表页</p>
<a @click="jumpSearch()">go搜索页</a>
<ul>
<li v-for="item in list" :key="item.id">
<p>城市: {{item.name}}</p>
</li>
</ul>
</div>
</template>
从上可以看出模板并不全都是静态的标签内容,它下面要渲染一个城市列表.而城市列表数据list是放在远程一个JAVA服务器上.
这时就出现了问题.首先是客户端输入链接localhost:3000/list请求node服务器,node拦截请求后根据/list路径找到了当前要渲染的页面是List.vue,于是就开始加载组件的内容.
结果在这个组件内部发现它需要渲染的数据在远程服务器上,那么当前的node服务器必须要先去请求远程服务器把数据取回来,取回来后才能渲染List.vue,最后再把生成的字符串返回给浏览器.
为了顺利实现上面的流程,需要借助vuex的能力.
- 在项目根目录下创建
vuex/store.js.
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export function createStore() {
return new Vuex.Store({
state: {
list: [],
name: 'kay',
},
actions: {
getList({ commit }, params) {
return new Promise((resolve)=>{
commit("setList",[{
name:"广州"
},{
name:"深圳"
}]);
resolve();
},2000)
},
},
mutations: {
setList(state, data) {
state.list = data || [];
},
},
});
}
这份vuex的配置和前端之前的做法没区别.定义了一个action方法getList获取城市列表.在getList方法里使用定时器模拟远程请求延时返回数据.
- 客户端集成
vuex
import Vue from 'vue';
import App from '../App.vue';
import { createRouter } from '../route';
import { createStore } from '../vuex/store';
const router = createRouter(); //创建路由
const store = createStore();
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app', true);
List.vue文件增加异步获取数据的方法.
<template>
<div class="list">
<p>当前页:列表页</p>
<a @click="jumpSearch()">go搜索页</a>
<ul>
<li v-for="item in list" :key="item.name">
<p>城市: {{item.name}}</p>
</li>
</ul>
</div>
</template>
<script>
export default {
asyncData({ store, route }) {
return store.dispatch("getList");
},
computed: {
list() {
return this.$store.state.list;
},
},
methods: {
jumpSearch() {
this.$router.push({
path: "search",
});
},
},
};
</script>
在组件上增加一个asyncData方法,获取远程数据.
ssr集成vuex.在服务器端渲染入口文件index.js添加store.
import { sync } from 'vuex-router-sync';
...省略
/**
* 应用接管路由
*/
app.use(async function(ctx) {
const req = ctx.request;
const router = createRouter(); //创建路由
const store = createStore(); //创建数据仓库
// 同步路由状态(route state)到 store
sync(store, router);
const vm = new Vue({
router,
store,
render: (h) => h(App),
});
router.push(req.url);
...省略
const matchedComponents = router.getMatchedComponents();//获取当前路由匹配的页面组件
await Promise.all(
matchedComponents.map((Component) => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute,
});
}
})
);
const htmlString = await renderer.renderToString(vm);
...省略
})
首先先创建一个store仓库,然后要使用sync将路由状态同步一下,注入到vue实例中,最后还需要使用Promise.all将页面组件的asyncData执行一遍.
将上面这几个步骤再梳理一遍.现在页面组件模板List.vue的数据放在远程服务器,需要加载了数据才能渲染.
首先给List.vue增加一个asyncData函数,这个函数一旦触发就会启动vuex里面的action请求远程数据.
现在浏览器打开链接localhost:3000/list.服务器端渲染入口文件index.js接管了这个请求后,发现要渲染的页面的组件是List.vue.
于是node服务器开始执行Promise.all后面代码检查一下List.vue下有没有定义asyncData函数,如果定义了就赶紧执行这个函数去请求远程数据.数据返回后同步到store仓库中,紧接着整个vue实例会因为vuex的数据变化重新渲染,List.vue将远程数据填充在模板上,最后将vue实例转化成html字符串返回给浏览器.
脱水
现在ssr和客户端都配置了vuex,但区别是服务端的store里面放着List.vue需要的远程请求的数据,而客户端的store是空的.
这样就会造成一个问题.页面本来很好的在浏览器展现,突然闪烁一下,List.vue页面模板的城市列表的数据消失了.
为什么会这样呢?srr返回的静态html是带着城市列表的,一旦客户端的vue接管了整个应用就会展开各种各样的初始化操作.客户端也要配置vuex,由于它的数据仓库是空的所以重新引发了页面渲染.致使原本来含有城市列表的页面部分消失了.
为了解决这个问题,就要想办法让ssr远程请求来的数据也给客户端的store发一份.这样客户端即使接管了应用,但发现此时store存储的城市列表数据和页面保持一致也不会造成闪烁问题.
在ssr的入口文件加上如下代码.
ctx.body = `<html>
<head>
</head>
<body>
${htmlString}
<script>
var context = {
state: ${JSON.stringify(store.state)}
}
</script>
<script src="/index.js"></script>
</body>
</html>`;
其实就是将服务器端store里面的数据转化成字符串放到js的变量里再一起返回给浏览器.
这样的好处就是客户端的脚本就可以访问context.state拿到远程请求的数据.
将数据从服务器端注入到客户端js的过程就称之为脱水.
注水
服务器端将数据放入了js脚本里,客户端此时就可以轻松拿到这份数据.
import Vue from 'vue';
import App from '../App.vue';
import { createRouter } from '../route';
import { createStore } from '../vuex/store';
const router = createRouter(); //创建路由
const store = createStore();
if (window.context && window.context.state) {
store.replaceState(window.context.state);
}
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app', true);
在客户端入口文件里加上store.replaceState(window.context.state);.如果发现window.context.state存在,就把这部分数据作为vuex的初始数据,这个过程称之为注水.
装载真实数据
上面在vuex里是使用定时器模拟的请求数据,接下来利用网上的一些开放API接入真实的数据.
对vuex里的action方法做如下修改.
actions: {
getList({ commit }, params) {
const url = '/api/v2/city/lookup?location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0';
return axios.get(url).then((res)=>{
commit("setList",res.data.location);
})
}
}
现在重新把流程捋一捋,服务端根据请求路径要加载List.vue,发现了里面有异步调用的方法asyncData,便开始运行这个方法.
asyncData一运行就会走到上面actions里面的getList,它就会对上面那个url地址发起请求.但仔细观察发现这个url是没有写域名的,这样访问肯定会报错.
那把远程域名给它加上去行不行呢?如果这样硬加是会出现问题的.有一种场景就是客户端接管应用它也可以调用getList方法,我们写的这部分vuex代码可是服务端和客户端共用的.那如果客户端直接访问带有远程域名的路径就会引起跨域.
那如何解决这一问题呢?这里的url最好不要加域名,以/开头.那样客户端访问这个路径就会引向node服务器.此时只要加一个接口代理转发就搞定了.
import proxy from 'koa-server-http-proxy';
export const proxyHanlder = (app)=>{
app.use(proxy('/api', {
target: 'https://geoapi.qweather.com', //网上寻找的开放API接口,支持返回地理数据.
pathRewrite: { '^/api': '' },
changeOrigin: true
}));
}
定义一个中间件函数,在执行服务器端渲染前添加到koa2上.
这样node服务器只要看到以/api开头的请求路径就会转发到远程地址上获取数据,不会再走后面服务器端渲染的逻辑.
服务器端路径请求的问题
使用上面的代理转发之后又会带来新的问题,设想一种场景.如果浏览器输入localhost:3000/list后,node解析请求发现要加载List.vue这个页面组件,而这个组件又有一个asyncData异步方法,因此就运行异步方法获取数据.
actions: {
getList({ commit }, params) {
const url = '/api/v2/city/lookup?location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0';
return axios.get(url).then((res)=>{
commit("setList",res.data.location);
})
}
}
这个异步方法就是getList,注意此时执行这段脚本的是node服务器,不是客户端的浏览器.
浏览器如果请求以/开头的url,请求会发给node服务器.node服务器现在需要自己请求自己,只要请求了自己设置的代理就能把请求转发给远程服务器,而如今node服务器请求以/开头的路径是绝对无法请求到自己的,这个时候只能用绝对路径.
我们上面提到这部分的vuex代码是客户端和服务端共用的,最好不用绝对路径写死.还有一个更优雅的方法,就是对axios的baseURL进行配置生成带有域名的axios实例来请求.那这部分代码就可以改成如下.
export function createStore(_axios) {
return new Vuex.Store({
state: {
list: [],
name: 'kay',
},
actions: {
getList({ commit }, params) {
const url = '/api/v2/city/lookup?location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0';
return _axios.get(url).then((res)=>{
commit("setList",res.data.location);
})
},
},
mutations: {
setList(state, data) {
state.list = data || [];
},
},
});
}
_axios是配置基础域名后的实例对象,客户端会生成一个_axios,服务端也会生成一个,只不过客户端是不用配置baseURL的.
import axios from "axios";
//util/getAxios.js
/**
* 获取客户端axios实例
*/
export const getClientAxios = ()=>{
const instance = axios.create({
timeout: 3000,
});
return instance;
}
/**
* 获取服务器端axios实例
*/
export const getServerAxios = (ctx)=>{
const instance = axios.create({
timeout: 3000,
baseURL: 'http://localhost:3000'
});
return instance;
}
通过生成两份axios实例既保持了vuex代码的统一性,另外还解决了node服务器自己访问不了自己的问题.
cookie如何处理
使用了接口代理之后,怎么确保每次接口转发都能把cookie也一并传给远程的服务器.可以按如下配置.
在ssr的入口文件里.
***省略
**
* 应用接管路由,服务器端渲染代码
*/
app.use(async function(ctx) {
const req = ctx.request;
//图标直接返回
if (req.path === '/favicon.ico') {
ctx.body = '';
return false;
}
const router = createRouter(); //创建路由
const store = createStore(getServerAxios(ctx)); //创建数据仓库
***省略
})
在创建ctx和axios实例的时候将ctx传递进去.
/**
* 获取服务器端axios实例
*/
export const getServerAxios = (ctx)=>{
const instance = axios.create({
timeout: 3000,
headers:{
cookie:ctx.req.headers.cookie || ""
},
baseURL: 'http://localhost:3000'
});
return instance;
}
将ctx中的cookie取出来赋值给axios的headers,这样就确保cookie被携带上了.
样式处理
.vue页面的文件通常把代码分成三个标签<template>,<script>和<style>.
<style scoped lang="scss"></style>上还可以添加一些属性.
和客户端渲染相比,实现ssr的过程要多处理一步.即将<style>里面的样式内容提取出来,再渲染到html的<head>里面.
在ssr入口文件index.js添加如下代码.
...省略
const context = {}; //创建一个上下文对象
htmlString = await renderer.renderToString(vm, context);
ctx.body = `<html>
<head>
${context.styles ? context.styles : ''}
</head>
<body>
${htmlString}
<script>
var context = {
state: ${JSON.stringify(store.state)}
}
</script>
<script src="./bundle.js"></script>
</body>
</html>`;
服务端提取样式的过程非常简单,定义一个上下文对象context.
renderer.renderToString函数的第二个参数里传入context,该函数执行完毕后,context对象的styles属性就会拥有页面组件的样式.最后将这份样式拼接到html的head头部里即可.
Head信息处理
常规的html文件的head里面不仅包含样式,它可能还需要设置<title>和<meta />.如何针对每个页面设置个性化的头部信息,可以利用vue-meta插件.
现在需要给List.vue页面组件添加一些头信息,可以按如下设置.
<script>
export default {
metaInfo: {
title: "列表页",
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
],
},
asyncData({ store, route }) {
return store.dispatch("getList");
}
...省略
}
在导出的对象上添加一个属性metaInfo,在其中分别设置title和meta;
在ssr的入口文件处加入如下代码.
import Koa2 from 'koa';
import Vue from 'vue';
import App from './App.vue';
import VueMeta from 'vue-meta';
Vue.use(VueMeta);
/**
* 应用接管路由
*/
app.use(async function(ctx) {
...省略
const vm = new Vue({
router,
store,
render: (h) => h(App),
});
const meta_obj = vm.$meta(); // 生成的头信息
router.push(req.url);
...省略
htmlString = await renderer.renderToString(vm, context);
const result = meta_obj.inject();
const { title, meta } = result;
ctx.body = `<html>
<head>
${title ? title.text() : ''}
${meta ? meta.text() : ''}
${context.styles ? context.styles : ''}
</head>
<body>
${htmlString}
<script>
var context = {
state: ${JSON.stringify(store.state)}
}
</script>
<script src="./index.js"></script>
</body>
</html>`;
});
app.listen(3000);
通过 vm.$meta()生成头信息meta_obj,待到vue实例加载完毕后,执行meta_obj.inject()获取被渲染页面组件的meta和title数据,再将它们填充到html字符串即可.
这样一来浏览器访问localhost:3000/list,返回的html文件的头部就会包含上面定义的title和meta信息.
源码
结尾
上面这一整套流程走下来还是挺复杂的,服务器端渲染的难点不是在于本身技术存在难度.而是整个流程有些复杂,要处理的细节非常多.但如果真的将这些原理都吃透,那么不光是vue框架,像react和angular都可以按照同样的思路去实现服务器端渲染.