简单的Vue SSR的示例代码
前言
最近接手一个老项目,典型的 vue 组件化前端渲染,后续业务优化可能会朝 ssr 方向走,因此,就先做些技术储备。如果对 vue ssr 完全不了解,请先阅读官方文档。
思路
vue 提供了一个官方 demo,该 demo 优点是功能大而全,缺点是对新手不友好,容易让人看蒙。因此,今天我们来写一个更加容易上手的 demo。总共分三步走,循序渐进。
- 写一个简单的前端渲染 demo(不包含 ajax 数据);
- 将前端渲染改成后端渲染(仍然不包含 ajax 数据);
- 在后端渲染的基础上,加上 ajax 数据的处理;
第一步:前端渲染 demo
这部分比较简单,就是一个页面中包含两个组件:foo 和 bar。
<!-- index.html --> <body> <div id="app"> <app></app> </div> <script src="./dist/web.js"></script> <!--这是 app.js 打包出来的 js 文件 --> </body> // app.js,也是 webpack 打包入口 import vue from 'vue'; import app from './app.vue'; var app = new vue({ el: '#app', components: { app } });
// app.vue <template> <div> <foo></foo> <bar></bar> </div> </template> <script> import foo from './components/foo.vue'; import bar from './components/bar.vue'; export default { components:{ foo, bar } } </script>
// foo.vue <template> <div class='foo'> <h1>foo</h1> <p>component </p> </div> </template> <style> .foo{ background: yellow; } </style>
// bar.vue <template> <div class='bar'> <h1>bar</h1> <p>component </p> </div> </template> <style> .bar{ background: blue; } </style>
最终渲染结果如下图所示,源码请参考这里。
第二步:后端渲染(不包含 ajax 数据)
第一步的 demo 虽不包含任何 ajax 数据,但即便如此,要把它改造成后端渲染,亦非易事。该从哪几个方面着手呢?
- 拆分 js 入口;
- 拆分 webpack 打包配置;
- 编写服务端渲染主体逻辑。
1. 拆分 js 入口
在前端渲染的时候,只需要一个入口 app.js。现在要做后端渲染,就得有两个 js 文件:entry-client.js 和 entry-server.js 分别作为浏览器和服务器的入口。
先看 entry-client.js,它跟第一步的 app.js 有什么区别吗? → 没有区别,只是换了个名字而已,内容都一样。
再看 entry-server.js,它只需返回 app.vue 的实例。
// entry-server.js export default function createapp() { const app = new vue({ render: h => h(app) }); return app; };
entry-server.js 与 entry-client.js 这两个入口主要区别如下:
- entry-client.js 在浏览器端执行,所以需要指定 el 并且显式调用 $mount 方法,以启动浏览器的渲染。
- entry-server.js 在服务端被调用,因此需要导出为一个函数。
2. 拆分 webpack 打包配置
在第一步中,由于只有 app.js 一个入口,只需要一份 webpack 配置文件。现在有两个入口了,自然就需要两份 webpack 配置文件:webpack.server.conf.js 和 webpack.client.conf.js,它们的公共部分抽象成 webpack.base.conf.js。
关于 webpack.server.conf.js,有两个注意点:
- librarytarget: 'commonjs2' → 因为服务器是 node,所以必须。
- target: 'node' → 指定 node 环境,避免非 node 环境特定 api 报错,如 document 等。
3. 编写服务端渲染主体逻辑
vue ssr 依赖于包 vue-server-render,它的调用支持:createrenderer 和 createbundlerenderer,前者以 vue 组件为入口,后者以打包后的 js 文件为入口,本文采取后者。
// server.js 服务端渲染主体逻辑 // dist/server.js 就是以 entry-server.js 为入口打包出来的 js const bundle = fs.readfilesync(path.resolve(__dirname, 'dist/server.js'), 'utf-8'); const renderer = require('vue-server-renderer').createbundlerenderer(bundle, { template: fs.readfilesync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf-8') }); server.get('/index', (req, res) => { renderer.rendertostring((err, html) => { if (err) { console.error(err); res.status(500).end('服务器内部错误'); return; } res.end(html); }) }); server.listen(8002, () => { console.log('后端渲染服务器启动,端口号为:8002'); });
这一步的最终渲染效果如下图所示,从图中我们可以看到,组件已经被后端成功渲染了。源码请参考。
第三步:后端渲染(预获取 ajax 数据)
这是关键的一步,也是最难的一步。
假如第二步的组件各自都需要请求 ajax 数据的话,该怎么处理呢?官方文档给我们指出了思路,我简要概括如下:
- 在开始渲染之前,预先获取所有需要的 ajax 数据(然后存在 vuex 的 store 中);
- 后端渲染的时候,通过 vuex 将获取到的 ajax 数据分别注入到各个组件中;
- 把全部 ajax 数据埋在 window.initial_state 中,通过 html 传递到浏览器端;
- 浏览器端通过 vuex 将 window.initial_state 里面的 ajax 数据分别注入到各个组件中。
下面谈几个重点。
我们知道,在常规的 vue 前端渲染中,组件请求 ajax 一般是这么写的:“在 mounted 中调用 this.fetchdata,然后在回调里面把返回数据写到实例的 data 中,这就 ok 了。”
在 ssr 中,这是不行的,因为服务器并不会执行 mounted 周期。那么我们是否可以把 this.fetchdata
提前到 created 或者 beforecreate 这两个生命周期中执行?同样不行。原因是:this.fetchdata 是异步请求,请求发出去之后,没等数据返回呢,后端就已经渲染完了,无法把 ajax 返回的数据也一并渲染出来。
所以,我们得提前知道都有哪些组件有 ajax 请求,等把这些 ajax 请求都返回了数据之后,才开始组件的渲染。
// store.js function fetchbar() { return new promise(function (resolve, reject) { resolve('bar ajax 返回数据'); }); } export default function createstore() { return new vuex.store({ state: { bar: '', }, actions: { fetchbar({commit}) { return fetchbar().then(msg => { commit('setbar', {msg}) }) } }, mutations:{ setbar(state, {msg}) { vue.set(state, 'bar', msg); } } }) }
// bar.uve asyncdata({store}) { return store.dispatch('fetchbar'); }, computed: { bar() { return this.$store.state.bar; } }
组件的 asyncdata 方法已经定义好了,但是怎么索引到这个 asyncdata 方法呢?先看我的根组件 app.vue 是怎么写的。
// app.vue <template> <div> <h1>app.vue</h1> <p>vue with vue </p> <hr> <foo1 ref="foo_ref"></foo1> <bar1 ref="bar_ref"></bar1> <bar2 ref="bar_ref2"></bar2> </div> </template> <script> import foo from './components/foo.vue'; import bar from './components/bar.vue'; export default { components: { foo1: foo, bar1: bar, bar2: bar } } </script>
从根组件 app.vue 我们可以看到,只需要解析其 components 字段,便能依次找到各个组件的 asyncdata 方法了。
// entry-server.js export default function (context) { // context 是 vue-server-render 注入的参数 const store = createstore(); let app = new vue({ store, render: h => h(app) }); // 找到所有 asyncdata 方法 let components = app.components; let prefetchfns = []; for (let key in components) { if (!components.hasownproperty(key)) continue; let component = components[key]; if(component.asyncdata) { prefetchfns.push(component.asyncdata({ store })) } } return promise.all(prefetchfns).then((res) => { // 在所有组件的 ajax 都返回之后,才最终返回 app 进行渲染 context.state = store.state; // context.state 赋值成什么,window.__initial_state__ 就是什么 return app; }); };
还有几个问题比较有意思:
1、是否必须使用 vue-router?→ 不是。虽然官方给出的 demo 里面用到了 vue-router,那只不过是因为官方 demo 是包含多个页面的 spa 罢了。一般情况下,是需要用 vue-router 的,因为不同路由对应不同的组件,并非每次都把所有组件的 asyncdata 都执行的。但是有例外,比如我的这个老项目,就只有一个页面(一个页面中包含很多的组件),所以根本不需要用到 vue-router,也照样能做 ssr。主要的区别就是如何找到那些该被执行的 asyncdata 方法:官方 demo 通过 vue-router,而我通过直接解析 components 字段,仅此而已。
2、是否必须使用 vuex? → 是,但也不是,请看。为什么必须要有类似 vuex 的存在?我们来分析一下。
2.1. 当预先获取到的 ajax 数据返回之后,vue 组件还没开始渲染。所以,我们得把 ajax 先存在某个地方。
2.2. 当 vue 组件开始渲染的时候,还得把 ajax 数据拿出来,正确地传递到各个组件中。
2.3. 在浏览器渲染的时候,需要正确解析 window.initial_state ,并传递给各个组件。
因此,我们得有这么一个独立于视图以外的地方,用来存储、管理和传递数据,这就是 vuex 存在的理由。
3、后端已经把 ajax 数据转化为 html 了,为什么还需要把 ajax 数据通过 window.initial_state 传递到前端? → 因为前端渲染的时候仍然需要知道这些数据。举个例子,你写了一个组件,给它绑定了一个点击事件,点击的时候打印出 this.msg 字段值。现在后端是把组件 html 渲染出来了,但是事件的绑定肯定得由浏览器来完成啊,如果浏览器拿不到跟服务器端同样的数据的话,在触发组件的点击事件的时候,又上哪儿去找 msg 字段呢?
至此,我们已经完成了带 ajax 数据的后端渲染了。这一步最为复杂,也最为关键,需要反复思考和尝试。具体渲染效果图如下所示,源码请参考。
效果
大功告成了吗?还没。人们都说 ssr 能提升首屏渲染速度,下面我们对比一下看看到底是不是真的。(同样在 fast 3g 网络条件下)。
官方思路的变形
行文至此,关于 vue ssr demo便已经结束了。后面是我结合自身项目特点的一些变形,不感兴趣的读者可以不看。
第三步官方思路有什么缺点吗?我认为是有的:对老项目来说,改造成本比较大。需要显式的引入 vuex,就得走 action、mutations 那一套,无论是代码改动量还是新人学习成本,都不低。
有什么办法能减少对旧有前端渲染项目的改动量的吗?我是这么做的。
// store.js // action,mutations 那些都不需要了,只定义一个空 state export default function createstore() { return new vuex.store({ state: {} }) } // bar.vue // tagname 是组件实例的名字,比如 bar1、bar2、foo1 等,由 entry-server.js 注入 export default { prefetchdata: function (tagname) { return new promise((resolve, reject) => { resolve({ tagname, data: 'bar ajax 数据' }); }) } }
// entry-server.js return promise.all(prefetchfns).then((res) => { // 拿到 ajax 数据之后,手动将数据写入 state,不通过 action,mutation 那一套 // state 内部区分的 key 值就是 tagname,比如 bar1、bar2、foo1 等 res.foreach((item, key) => { vue.set(store.state, `${item.tagname}`, item.data); }); context.state = store.state; return app; });
// ssrmixin.js // 将每个组件都需要的 computed 抽象成一个 mixin,然后注入 export default { computed: { prefetchdata () { let componenttag = this.$options._componenttag; // bar1、bar2、foo1 return this.$store.state[componenttag]; } } }
至此,我们就便得到了 vue ssr 的一种变形。对于组件开发者而言,只需要把原来的 this.fetchdata 方法抽象到 prefetchdata 方法,然后就可以在 dom 中使用 {{prefetchdata}} 拿到到数据了。这部分的代码请参考。
总结
vue ssr 确实是个有趣的东西,关键在于灵活运用。此 demo 还有一个遗留问题没有解决:当把 ajax 抽象到 prefetchdata,做成 ssr 之后,原先的前端渲染就失效了。能不能同一份代码同时支持前端渲染和后端渲染呢?这样当后端渲染出问题的时候,我就可以随时切回前端渲染,便有了兜底的方案。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。