前言
大多数 vue 项目要支持 SSR 应该是为了 SEO 考虑, 毕竟对于 web 应用来说, 搜索引擎是一个很大的流量入口. Vue SSR 现在已经比较成熟了, 但是如果是把一个 SPA 应用改造成 SSR 应用, 成本还是有些高的, 这工作量无异于重构前端. 另外对前端的技术要求也是挺高的, 需要对 Vue 比较熟悉, 还要有 Node.JS 和 webpack 的应用经验.
引入
Vue 是一个构建客户端应用的框架, 即 vue 组件是在浏览器中进行渲染的. 所谓服务端渲染, 指的是把 vue 组件在服务器端渲染为组装好的 html 字符串, 然后将它们直接发送到浏览器, 最后需要将这些静态标记 "激活" 为客户端上完全可交互的应用程序.
服务端渲染的优点
更好的 SEO, 搜索引擎爬虫可以抓取渲染好的页面
更快的内容到达时间(首屏加载更快), 因为服务端只需要返回渲染好的 HTML, 这部分代码量很小的, 所以用户体验更好
服务端渲染的缺点
首先就是开发成本比较高, 比如某些声明周期钩子函数 (如 beforeCreate,created) 能同时运行在服务端和客户端, 因此第三方库要做特殊处理, 才能在服务器渲染应用程序中运行.
由于服务端渲染要用 Node.JS 做中间层, 所以部署项目时, 需要处于 Node.JS server 运行环境. 在高流量环境下, 还要做好服务器负载和缓存策略
原理解析
先附上 demo 地址: https://github.com/wmui/vue-ssr-demo
第一步: 编写 entry-client.JS 和 entry-server.JS
entry-client.JS 只在浏览器环境下执行, 所以需要显示调用 $mount 方法, 挂载 DOM 节点
- import Vue from 'vue';
- import App from './App.vue';
- import createStore from './store/index.js';
- function createApp() {
- const store = createStore();
- const App = new Vue({
- store,
- render: h => h(App)
- });
- return {App, store}
- }
- const { App, store } = createApp();
- // 使用 Windows.__INITIAL_STATE__中的数据替换整个 state 中的数据, 这样服务端渲染结束后, 客户端也可以自由操作 state 中的数据
- if (Windows.__INITIAL_STATE__) {
- store.replaceState(Windows.__INITIAL_STATE__);
- }
- App.$mount('#app');
entry-server.JS 需要导出一个函数, 在服务端渲染期间会被调用
- import Vue from 'vue';
- import App from './App.vue';
- import createStore from './store/index.js';
- export default function(context) {
- // context 是上下文对象
- const store = createStore();
- let App = new Vue({
- store,
- render: h => h(App)
- });
- // 找到所有 asyncData 方法
- let components = App.components;
- let asyncDataArr = []; // promise 集合
- for (let key in components) {
- if (!components.hasOwnProperty(key)) continue;
- let component = components[key];
- if (component.asyncData) {
- asyncDataArr.push(component.asyncData({store})) // 把 store 传给 asyncData
- }
- }
- // 所有请求并行执行
- return Promise.all(asyncDataArr).then(() => {
- // context.state 赋值成什么, Windows.__INITIAL_STATE__ 就是什么
- // 这下你应该明白 entry-client.JS 中 Windows.__INITIAL_STATE__是哪来的了, 它是在服务端渲染期间被添加进上下文的
- context.state = store.state;
- return App;
- });
- };
上面的 asyncData 是干嘛用的? 其实, 这个函数是专门请求数据用的, 你可能会问请求数据为什么不在 beforeCreate 或者 created 中完成, 还要专门定义一个函数? 虽然 beforeCreate 和 created 在服务端也会被执行(其他周期函数只会在客户端执行), 但是我们都知道请求是异步的, 这就导致请求发出后, 数据还没返回, 渲染就已经结束了, 所以无法把 Ajax 返回的数据也一并渲染出来. 因此需要想个办法, 等到所有数据都返回后再渲染组件
asyncData 需要返回一个 promise, 这样就可以等到所有请求都完成后再渲染组件. 下面是在 foo 组价中使用 asyncData 的示例, 在这里完成数据的请求
- export default {
- asyncData: function({store}) {
- return store.dispatch('GET_ARTICLE') // 返回 promise
- },
- computed: {
- article() {
- return this.$store.state.article
- }
- }
- }
第二步: 配置 webpack
webpack 配置比较简单, 但是也需要针对 client 和 server 端单独配置
webpack.client.conf.JS 显然是用来打包客户端应用的
- module.exports = merge(base, {
- entry: {
- client: path.join(__dirname, '../entry-client.js')
- }
- });
webpack.server.conf.JS 用来打包服务端应用, 这里需要指定 node 环境
- module.exports = merge(base, {
- target: 'node', // 指定是 node 环境
- entry: {
- server: path.join(__dirname, '../entry-server.js')
- },
- output: {
- filename: '[name].js', // server.JS
- libraryTarget: 'commonjs2' // 必须按照 commonjs 规范打包才能被服务器调用.
- },
- plugins: [
- new HtmlWebpackPlugin({
- template: path.join(__dirname, '../index.ssr.html'),
- filename: 'index.ssr.html',
- files: {
- JS: 'client.js'
- }, // client.JS 需要在 HTML 中引入
- excludeChunks: ['server'] // server.JS 只在服务端执行, 所以不能打包到 HTML 中
- })
- ]
- });
第三步: 启动服务
打包完成后就可以启动服务了, 在 start.JS 中我们需要把 server.JS 加载进来, 然后通过 renderToString 方法把渲染好的 HTML 返回给浏览器
- 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('*', (req, res) => {
- renderer.renderToString((err, HTML) => {
- // console.log(HTML)
- if (err) {
- console.error(err);
- res.status(500).end('服务器内部错误');
- return;
- }
- res.end(HTML);
- })
- });
效果图
demo 已经上传到 GitHub: http://github.com/wmui/vue-ssr-demo
结语
个人实践 Vue SSR 已有一段时间, 发现要想搭建一套完整的 SSR 服务框架还是很有挑战的, 或许 Nuxt 是一个不错的选择, 对 Nuxt 感兴趣的朋友可以参考我的一个开源小作品 Essay https://github.com/wmui/essay
来源: https://juejin.im/post/5c1da3386fb9a049e93cb643