前言
本篇文章将从一个实战案例的角度逐步解析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
都可以按照同样的思路去实现服务器端渲染.