01服务端渲染简介
服务端渲染不是一个新的技能 ;在 Web 最初的时间 ,页面就是通过服务端渲染来返回的,用 PHP 来说,通常是利用 Smarty 等模板写模板文件 ,然后 PHP 服务端框架将数据和模板渲染为页面返回,如许 的服务端渲染有个缺点就是一旦要查察 新的页面,就必要 哀求 服务端 ,革新 页面 。
但如今 的前端,为了寻求 一些体验上的优化,通常整个渲染在欣赏 器端利用 JS 来完成 ,共同 history.pushState 等方式来做单页应用(SPA: Single-Page Application),也收到不错的结果 ,但是如许 还是 有一些缺点:第一次加载过慢,用户必要 等待 较长时间来等待 欣赏 器端渲染完成;对搜刮 引擎爬虫等不友爱 。这时间 就出现了雷同 于 React ,Vue 2.0 等前端框架来做服务端渲染。
利用 这些框架来做服务端渲染的分身 了上面的几个长处 ,而且写一份代码就可以跑在服务端和欣赏 器端 。Vue 2.0 发布了也有一段时间了,新版本比力 大的更新就是支持服务端渲染 ,近来 有空折腾了下 Vue 的服务端渲染,记录 下来。
02在 Vue 2.0 中利用 服务端渲染
官方文档给了一个简单 的例子来做服务端渲染:
// 步调 1:创建一个Vue实例var Vue = require('vue')var app = new Vue({ render: function (h) { return h('p', 'hello world') }})// 步调 2: 创建一个渲染器var renderer = require('vue-server-renderer').createRenderer()// 步调 3: 将 Vue实例 渲染成 HTMLrenderer.renderToString(app, function (error, html) { if (error) throw error console.log(html) // = p server-rendered="true"hello world/p})
如许 子,共同 通常的 Node 服务端框架就可以简单 来实现服务端渲染了 ,但是 ,在真实场景中,我们一样平常 采取 .vue 文件的模块构造 方式 ,如许 的话,服务端渲染就必要 利用 webpack 来将 Vue 组件举行 打包为单个文件。
03共同 Webpack 渲染 .vue 文件
先创建 一个服务端的入口文件 server.js
importVue from'vue';importApp from'./vue/App';export default function (options) { const VueApp = Vue.extend(App); const app = new VueApp(Object.assign({}, options)); returnnew Promise(resolve = { resolve(app); });}
这里和欣赏 器端的入口文件大同小异,只是默认导出了一个函数 ,这个函数吸取 一个服务端渲染时服务端传入的一些设置 ,返回一个包罗 了 app 实例的 Promise;
简单 写一个 App.vue 的文件
template h1{{ title }}/h1/templatemodule.exports = { props: ['title']/
这里将会读取服务端入口文件传入 options 的 data 属性,取到 title 值,渲染到对应 DOM 中;
再看看 Webpack 的设置 ,和客户端渲染同样是大同小异:
const webpack = require('webpack');const path = require('path');const projectRoot = __dirname;const env = process.env.NODE_ENV || 'development';module.exports = { target: 'node', // 告诉 Webpack 是 node 代码的打包 devtool: null, // 既然是 node 就不消 devtool 了 entry: { app: path.join(projectRoot, 'src/server.js') }, output: Object.assign({}, base.output, { path: path.join(projectRoot, 'src'), filename: 'bundle.server.js', libraryTarget: 'commonjs2'// 和客户端差别 }), plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(env), 'process.env.VUE_ENV': '"server"'// 设置 vue 的环境 变量,告诉 vue 是服务端渲染,就不会做耗性能的 dom-diff 操纵 了 }) ], resolve: { extensions: ['', '.js', '.vue'], fallback: [path.join(projectRoot, 'node_modules')] }, resolveLoader: { root: path.join(projectRoot, 'node_modules') }, module: { loaders: [ { test: /.vue$/, loader: 'vue'}, { test: /.js$/, loader: 'babel', include: projectRoot, exclude: /node_modules/ } ] }};
此中 重要 就是三处差别 :声明 node 模块打包;修改打包后模块加载方式为 commonjs(commonjs2 具体 可以看 Webpack 官方文档);再就是 vue 的服务端打包优化了 ,这部分 假如 不传的话背面 vue 服务端渲染会慢至几十秒,一度以为服务端代码挂了 。
末了 就是服务端载入天生 的 bundle.server.js 文件:
const fs = require('fs');const path = require('path');const vueServerRenderer = require('vue-server-renderer');const filePath = path.join(__dirname, 'src/bundle.server.js');// 读取 bundle 文件,并创建渲染器const code = fs.readFileSync(filePath, 'utf8');const bundleRenderer = vueServerRenderer.createBundleRenderer(code);// 渲染 Vue 应用为一个字符串bundleRenderer.renderToString(options, (err, html) = { if(err) { console.error(err); } content.replace('div id="app"/div', html);});
这里 options 可以传入 vue 组件所必要 的 data 等信息;下面还是 以官方实例中的 express 来做服务端示例下:
const fs = require('fs');const path = require('path');const vueServerRenderer = require('vue-server-renderer');const filePath = path.join(think.ROOT_PATH, 'view/bundle.server.js');global.Vue = require('vue')// 读取 bundle 文件 ,并创建渲染器const code = fs.readFileSync(filePath, 'utf8');const bundleRenderer = vueServerRenderer.createBundleRenderer(code);// 创建一个Express服务器var express = require('express');var server = express();// 摆设 静态文件夹为 "assets"文件夹server.use('/assets', express.static( path.resolve(__dirname, 'assets');));// 处理 惩罚 全部 的 Get 哀求 server.get('*', function (request, response) { // 设置一些数据,可以是数据库读取等等 const options = { data: { title: 'hello world'} }; // 渲染 Vue 应用为一个字符串 bundleRenderer.renderToString(options, (err, html) = { // 假如 渲染时发生了错误 if(err) { // 打印错误到控制台 console.error(err); // 告诉客户端错误 returnresponse.status(500).send('Server Error'); } // 发送布局 和HTML文件 response.send(layout.replace('div id="app"/div', html)); });// 监听5000端口server.listen(5000, function (error) { if(error) throw error console.log('Server is running at localhost:5000')});
如许 子根本 就是 Vue 服务端渲染的整个流程了,如许 子和利用 平凡 的模板渲染并没有什么其他的上风 ,但是 当渲染完成后再由客户端担当 渲染后就可以做到无缝切换了,下面我们来看看和客户端连合 渲染;
04和欣赏 器渲染无缝聚集
为了和客户端渲染连合 ,我们将 webpack 设置 文件分为三部分 ,base 共用的设置 ,server 设置 ,client 欣赏 器端设置 ,如下:
webpack.base.js
const path = require('path');const projectRoot = path.resolve(__dirname, '../');module.exports = { devtool: '#source-map', entry: { app: path.join(projectRoot, 'src/client.js') }, output: { path: path.join(projectRoot, 'www/static'), filename: 'index.js'}, resolve: { extensions: ['', '.js', '.vue'], fallback: [path.join(projectRoot, 'node_modules')], alias: { 'Common': path.join(projectRoot, 'src/vue/Common'), 'Components': path.join(projectRoot, 'src/vue/Components') } }, resolveLoader: { root: path.join(projectRoot, 'node_modules') }, module: { loaders: [ { test: /.vue$/, loader: 'vue'}, { test: /.js$/, loader: 'babel', include: projectRoot, exclude: /node_modules/ } ] }};
webpack.server.js
const webpack = require('webpack');const base = require('./webpack.base');const path = require('path');const projectRoot = path.resolve(__dirname, '../');const env = process.env.NODE_ENV || 'development';module.exports = Object.assign({}, base, { target: 'node', devtool: null, entry: { app: path.join(projectRoot, 'view/server.js') }, output: Object.assign({}, base.output, { path: path.join(projectRoot, 'view'), filename: 'bundle.server.js', libraryTarget: 'commonjs2'}), plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(env), 'process.env.VUE_ENV': '"server"', 'isBrowser': false }) ]});
服务端的设置 ,和之前多了一个 isBrowser 的全局变量,用于在 Vue 模块中做一些差别 处理 惩罚 ;
webpack.client.js
const webpack = require('webpack');const ExtractTextPlugin = require('extract-text-webpack-plugin');const base = require('./webpack.base');const env = process.env.NODE_ENV || 'development';const config = Object.assign({}, base, { plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(env), 'isBrowser': true }) ]});config.vue = { loaders: { css: ExtractTextPlugin.extract({ loader: 'css-loader', fallbackLoader: 'vue-style-loader'}), sass: ExtractTextPlugin.extract('vue-style-loader', 'css!sass?indentedSyntax'), scss: ExtractTextPlugin.extract('vue-style-loader', 'css!sass') }};config.plugins.push(new ExtractTextPlugin('style.css'));if(env === 'production') { config.plugins.push( new webpack.LoaderOptionsPlugin({ minimize: true }), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) );}module.exports = config;
服务端打包时间 会忽略 css 等样式文件,欣赏 器端打包时间 就将样式文件单独打包到一个 css 文件。如许 在实行 webpack 时间 就必要 指定 --config 参数来编译差别 的版本了 ,如下:
# 编译 客户端webpack --config webpack.client.js# 编译 服务器端webpack --config webpack.server.js
同样,入口文件也提出三个文件,index.js, server.js, client.js
index.js
importVue from'vue';importApp from'./vue/App';importClipButton from'Components/ClipButton';importToast from'Components/Toast';Vue.filter('byte-format', value = { const unit = ['Byte', 'KB', 'MB', 'GB', 'TB']; let index = 0; let size = parseInt(value, 10); while(size = 1024 index unit.length) { size /= 1024; index++; } return[size.toString().substr(0, 5), unit[index]].join(' ');});Vue.use(Toast);Vue.component('maple-clip-button', ClipButton);const createApp = function createApp(options) { const VueApp = Vue.extend(App); returnnew VueApp(Object.assign({}, options));};export {Vue, createApp};
index.js 中做一些通用的组件、插件加载 ,一些全局的设置,末了 返回一个可以天生 app 实例的函数供差别 环境 来调用;
server.js
import{createApp} from'./index';export default function (options) { const app = createApp(options); returnnew Promise(resolve = { resolve(app); });}
大部分 逻辑已经抽为共用了,以是 服务端这里就是简单 将 app 实例通过 promise 返回;
client.js
importVueResource from'vue-resource';import{createApp, Vue} from'./index';Vue.use(VueResource);const title = 'Test';const app = createApp({ data: { title }, el: '#app'});export default app;
客户端也雷同 ,这里在客户端加载 VueResource 这个插件,用于客户端的 ajax 哀求 ;通常通过 ajax 哀求 服务端返回数据再初始化 app,如许 根本 就是一个单页的服务端渲染框架了 ,一样平常 环境 下,我们做单页应用还会共同 history.pushState 等通过 URL 做路由分发;如许 ,我们服务端也最好利用 同一套路由来渲染差别 的页面。
05服务端和欣赏 器路由共用
路由这里利用 vue-router 就可以了,欣赏 器端还是 通过正常的方式载入路由设置 ,服务端同样载入路由设置 ,并在渲染之前利用 router.push 渲染必要 显现 的页面,以是 ,在通用的入口文件参加 路由设置 :
importVue from'vue';importrouter from'./router';importApp from'./vue/App';const createApp = function createApp(options) { const VueApp = Vue.extend(App); returnnew VueApp(Object.assign({ router }, options));};export {Vue, router, createApp};
路由文件是如许 子的:
importVue from'vue';importVueRouter from'vue-router';importViewUpload from'../vue/ViewUpload';importViewHistory from'../vue/ViewHistory';importViewLibs from'../vue/ViewLibs';Vue.use(VueRouter);const routes = [ { path: '/', component: ViewUpload }, { path: '/history', component: ViewHistory }, { path: '/libs', component: ViewLibs }];const router = new VueRouter({mode: 'history', routes, base: __dirname});export default router;
这里路由的利用 HTML5 的 history 模式;
服务端入口文件如许 设置 :
import{createApp, router} from'./index';export default function (options) { const app = createApp({ data: options.data }); router.push(options.url); returnnew Promise(resolve = { resolve(app); });}
这里在初始化 app 实例后,调用 router.push(options.url) 将服务端取到的 url push 到路由之中;
06利用 中碰到 的坑
整个过程还算顺遂 ,此中 碰到 最多的题目 就是有些模块只能在服务端大概 欣赏 器来利用 ,而利用 ES6 模块加载是静态的,以是 必要 将静态加载的模块改为动态加载,以是 就有了上面设置 isBrowser 这个全局属性 ,通过这个属性来判定 模块加载了,比如 我项目中用到的 clipboard 模块,之前是直接利用 ES6 加载的;
template a @click.prevent :href="text"slot/slot/a/templateimport Clipboard from 'clipboard';export default { props: ['text'], mounted() { return this.$nextTick(() = { this.clipboard = new Clipboard(this.$el, { text: () = { return this.text; } }); this.clipboard.on('success', () = { this.$emit('copied', this.text); }); }); }};/
如许 就会在服务端渲染时间 报错 ,将加载改为动态加载就可以了:
let Clipboard = null;if(isBrowser) { Clipboard = require('clipboard');}
假如 这个模块在服务端并不会渲染,那背面 的代码并不必要 更改;
尚有 VueResource 插件也是必要 欣赏 器环境 的,以是 必要 将它单独设置 在 client.js 中;
07总结
同样的路由通过 Vue 服务端渲染后的 HTML 总是一样的,这和 React 渲染后会加上哈希差别 ,以是 可以做渲染后结果 的缓存优化,这部分 可以参考官方文档的做法,总的来说 ,Vue 服务端渲染相沿 了 Vue 客户端的轻量做法,也显得比力 轻量,唯一不敷 之处大概 就是服务端也同样必要 webpack 来完成 。
? ? ? ? ? ? ? ?
原文:
https://blog.alphatr.com/how-to-use-ssr-in-vue-2.0.html
点击“阅读原文 ” ,看更多
精选文章
↓↓↓