01服務(wù)端渲染簡(jiǎn)介
服務(wù)端渲染不是一個(gè)新深圳回收服務(wù)器設(shè)備的技術(shù)深圳回收服務(wù)器設(shè)備;在 Web 最初深圳回收服務(wù)器設(shè)備的時(shí)候深圳回收服務(wù)器設(shè)備,頁(yè)面就是通過(guò)服務(wù)端渲染來(lái)返回的,用 PHP 來(lái)說(shuō),通常是使用 Smarty 等模板寫(xiě)模板文件,然后 PHP 服務(wù)端框架將數(shù)據(jù)和模板渲染為頁(yè)面返回,這樣的服務(wù)端渲染有個(gè)缺點(diǎn)就是一旦要查看新的頁(yè)面,就需要請(qǐng)求服務(wù)端,刷新頁(yè)面。
但如今的前端,為了追求一些體驗(yàn)上的優(yōu)化,通常整個(gè)渲染在瀏覽器端使用 JS 來(lái)完成,配合 history.pushState 等方式來(lái)做單頁(yè)應(yīng)用(SPA: Single-Page Application),也收到不錯(cuò)的效果,但是這樣還是有一些缺點(diǎn)深圳回收服務(wù)器設(shè)備:第一次加載過(guò)慢,用戶(hù)需要等待較長(zhǎng)時(shí)間來(lái)等待瀏覽器端渲染完成;對(duì)搜索引擎爬蟲(chóng)等不友好。這時(shí)候就出現(xiàn)了類(lèi)似于 React,Vue 2.0 等前端框架來(lái)做服務(wù)端渲染。
使用這些框架來(lái)做服務(wù)端渲染的兼顧了上面的幾個(gè)優(yōu)點(diǎn),而且寫(xiě)一份代碼就可以跑在服務(wù)端和瀏覽器端。Vue 2.0 發(fā)布了也有一段時(shí)間了,新版本比較大的更新就是支持服務(wù)端渲染,最近有空折騰了下 Vue 的服務(wù)端渲染,記錄下來(lái)。
02在 Vue 2.0 中使用服務(wù)端渲染
官方文檔給了一個(gè)簡(jiǎn)單的例子來(lái)做服務(wù)端渲染:
// 步驟 1:創(chuàng)建一個(gè)Vue實(shí)例var Vue = require('vue')var app = new Vue({ render: function (h) { return h('p', 'hello world') }})// 步驟 2: 創(chuàng)建一個(gè)渲染器var renderer = require('vue-server-renderer').createRenderer()// 步驟 3: 將 Vue實(shí)例 渲染成 HTMLrenderer.renderToString(app, function (error, html) { if (error) throw error console.log(html) // = p server-rendered="true"hello world/p})
這樣子,配合通常的 Node 服務(wù)端框架就可以簡(jiǎn)單來(lái)實(shí)現(xiàn)服務(wù)端渲染了,可是,在真實(shí)場(chǎng)景中,我們一般采用 .vue 文件的模塊組織方式,這樣的話(huà),服務(wù)端渲染就需要使用 webpack 來(lái)將 Vue 組件進(jìn)行打包為單個(gè)文件。
03配合 Webpack 渲染 .vue 文件
先建立一個(gè)服務(wù)端的入口文件 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); });}
這里和瀏覽器端的入口文件大同小異,只是默認(rèn)導(dǎo)出了一個(gè)函數(shù),這個(gè)函數(shù)接收一個(gè)服務(wù)端渲染時(shí)服務(wù)端傳入的一些配置,返回一個(gè)包含了 app 實(shí)例的 Promise;
簡(jiǎn)單寫(xiě)一個(gè) App.vue 的文件
template h1{{ title }}/h1/templatemodule.exports = { props: ['title']/
這里將會(huì)讀取服務(wù)端入口文件傳入 options 的 data 屬性,取到 title 值,渲染到對(duì)應(yīng) DOM 中;
再看看 Webpack 的配置,和客戶(hù)端渲染同樣是大同小異:
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'// 和客戶(hù)端不同 }), plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(env), 'process.env.VUE_ENV': '"server"'// 配置 vue 的環(huán)境變量,告訴 vue 是服務(wù)端渲染,就不會(huì)做耗性能的 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 的服務(wù)端打包優(yōu)化了,這部分如果不傳的話(huà)后面 vue 服務(wù)端渲染會(huì)慢至幾十秒,一度以為服務(wù)端代碼掛了。
最后就是服務(wù)端載入生成的 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 文件,并創(chuàng)建渲染器const code = fs.readFileSync(filePath, 'utf8');const bundleRenderer = vueServerRenderer.createBundleRenderer(code);// 渲染 Vue 應(yīng)用為一個(gè)字符串bundleRenderer.renderToString(options, (err, html) = { if(err) { console.error(err); } content.replace('div id="app"/div', html);});
這里 options 可以傳入 vue 組件所需要的 data 等信息;下面還是以官方實(shí)例中的 express 來(lái)做服務(wù)端示例下:
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 文件,并創(chuàng)建渲染器const code = fs.readFileSync(filePath, 'utf8');const bundleRenderer = vueServerRenderer.createBundleRenderer(code);// 創(chuàng)建一個(gè)Express服務(wù)器var express = require('express');var server = express();// 部署靜態(tài)文件夾為 "assets"文件夾server.use('/assets', express.static( path.resolve(__dirname, 'assets');));// 處理所有的 Get 請(qǐng)求server.get('*', function (request, response) { // 設(shè)置一些數(shù)據(jù),可以是數(shù)據(jù)庫(kù)讀取等等 const options = { data: { title: 'hello world'} }; // 渲染 Vue 應(yīng)用為一個(gè)字符串 bundleRenderer.renderToString(options, (err, html) = { // 如果渲染時(shí)發(fā)生了錯(cuò)誤 if(err) { // 打印錯(cuò)誤到控制臺(tái) console.error(err); // 告訴客戶(hù)端錯(cuò)誤 returnresponse.status(500).send('Server Error'); } // 發(fā)送布局和HTML文件 response.send(layout.replace('div id="app"/div', html)); });// 監(jiān)聽(tīng)5000端口server.listen(5000, function (error) { if(error) throw error console.log('Server is running at localhost:5000')});
這樣子基本就是 Vue 服務(wù)端渲染的整個(gè)流程了,這樣子和使用普通的模板渲染并沒(méi)有什么其他的優(yōu)勢(shì),可是當(dāng)渲染完成后再由客戶(hù)端接管渲染后就可以做到無(wú)縫切換了,下面我們來(lái)看看和客戶(hù)端結(jié)合渲染;
04和瀏覽器渲染無(wú)縫集合
為了和客戶(hù)端渲染結(jié)合,我們將 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 }) ]});
服務(wù)端的配置,和之前多了一個(gè) 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;
服務(wù)端打包時(shí)候會(huì)忽略 css 等樣式文件,瀏覽器端打包時(shí)候就將樣式文件單獨(dú)打包到一個(gè) css 文件。這樣在執(zhí)行 webpack 時(shí)候就需要指定 --config 參數(shù)來(lái)編譯不同的版本了,如下:
# 編譯 客戶(hù)端webpack --config webpack.client.js# 編譯 服務(wù)器端webpack --config webpack.server.js
同樣,入口文件也提出三個(gè)文件,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 中做一些通用的組件、插件加載,一些全局的設(shè)置,最后返回一個(gè)可以生成 app 實(shí)例的函數(shù)供不同環(huán)境來(lái)調(diào)用;
server.js
import{createApp} from'./index';export default function (options) { const app = createApp(options); returnnew Promise(resolve = { resolve(app); });}
大部分邏輯已經(jīng)抽為共用了,所以服務(wù)端這里就是簡(jiǎn)單將 app 實(shí)例通過(guò) 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;
客戶(hù)端也類(lèi)似,這里在客戶(hù)端加載 VueResource 這個(gè)插件,用于客戶(hù)端的 ajax 請(qǐng)求;通常通過(guò) ajax 請(qǐng)求服務(wù)端返回?cái)?shù)據(jù)再初始化 app,這樣基本就是一個(gè)單頁(yè)的服務(wù)端渲染框架了,一般情況下,我們做單頁(yè)應(yīng)用還會(huì)配合 history.pushState 等通過(guò) URL 做路由分發(fā);這樣,我們服務(wù)端也最好使用同一套路由來(lái)渲染不同的頁(yè)面。
05服務(wù)端和瀏覽器路由共用
路由這里使用 vue-router 就可以了,瀏覽器端還是通過(guò)正常的方式載入路由配置,服務(wù)端同樣載入路由配置,并在渲染之前使用 router.push 渲染需要展現(xiàn)的頁(yè)面,所以,在通用的入口文件加入路由配置:
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 模式;
服務(wù)端入口文件這樣配置:
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 實(shí)例后,調(diào)用 router.push(options.url) 將服務(wù)端取到的 url push 到路由之中;
06使用中遇到的坑
整個(gè)過(guò)程還算順利,其中遇到最多的問(wèn)題就是有些模塊只能在服務(wù)端或者瀏覽器來(lái)使用,而使用 ES6 模塊加載是靜態(tài)的,所以需要將靜態(tài)加載的模塊改為動(dòng)態(tài)加載,所以就有了上面配置 isBrowser 這個(gè)全局屬性,通過(guò)這個(gè)屬性來(lái)判斷模塊加載了,比如我項(xiàng)目中用到的 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); }); }); }};/
這樣就會(huì)在服務(wù)端渲染時(shí)候報(bào)錯(cuò),將加載改為動(dòng)態(tài)加載就可以了:
let Clipboard = null;if(isBrowser) { Clipboard = require('clipboard');}
如果這個(gè)模塊在服務(wù)端并不會(huì)渲染,那后面的代碼并不需要更改;
還有 VueResource 插件也是需要瀏覽器環(huán)境的,所以需要將它單獨(dú)配置在 client.js 中;
07總結(jié)
同樣的路由通過(guò) Vue 服務(wù)端渲染后的 HTML 總是一樣的,這和 React 渲染后會(huì)加上哈希不同,所以可以做渲染后結(jié)果的緩存優(yōu)化,這部分可以參考官方文檔的做法,總的來(lái)說(shuō),Vue 服務(wù)端渲染沿襲了 Vue 客戶(hù)端的輕量做法,也顯得比較輕量,唯一不足之處可能就是服務(wù)端也同樣需要 webpack 來(lái)完成。
? ? ? ? ? ? ? ?
原文:
https://blog.alphatr.com/how-to-use-ssr-in-vue-2.0.html
點(diǎn)擊“閱讀原文”,看更多
精選文章
↓↓↓
我要評(píng)論