vue的服務器端渲染

0. 服務端渲染簡介

服務端渲染不是一個新的技術;在 Web 最初的時候,頁面就是經過服務端渲染來返回的,用 PHP 來講,一般是使用 Smarty 等模板寫模板文件,而後 PHP 服務端框架將數據和模板渲染爲頁面返回,這樣的服務端渲染有個缺點就是一旦要查看新的頁面,就須要請求服務端,刷新頁面。javascript

但現在的前端,爲了追求一些體驗上的優化,一般整個渲染在瀏覽器端使用 JS 來完成,配合 history.pushState 等方式來作單頁應用(SPA: Single-Page Application),也收到不錯的效果,可是這樣仍是有一些缺點:第一次加載過慢,用戶須要等待較長時間來等待瀏覽器端渲染完成;對搜索引擎爬蟲等不友好。這時候就出現了相似於 React,Vue 2.0 等前端框架來作服務端渲染。css

使用這些框架來作服務端渲染的兼顧了上面的幾個優勢,並且寫一份代碼就能夠跑在服務端和瀏覽器端。Vue 2.0 發佈了也有一段時間了,新版本比較大的更新就是支持服務端渲染,最近有空折騰了下 Vue 的服務端渲染,記錄下來。html

1. 在 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實例 渲染成 HTML renderer.renderToString(app, function (error, html) {  if (error) throw error  console.log(html)  // => <p server-rendered="true">hello world</p> }) 

這樣子,配合一般的 Node 服務端框架就能夠簡單來實現服務端渲染了,但是,在真實場景中,咱們通常採用 .vue 文件的模塊組織方式,這樣的話,服務端渲染就須要使用 webpack 來將 Vue 組件進行打包爲單個文件。vue

2. 配合 Webpack 渲染 .vue 文件

先創建一個服務端的入口文件 server.jsjava

import Vue from 'vue';  import App from './vue/App';  export default function (options) {  const VueApp = Vue.extend(App);   const app = new VueApp(Object.assign({}, options));   return new Promise(resolve => {  resolve(app);  }); } 

這裏和瀏覽器端的入口文件大同小異,只是默認導出了一個函數,這個函數接收一個服務端渲染時服務端傳入的一些配置,返回一個包含了 app 實例的 Promise;node

簡單寫一個 App.vue 的文件webpack

<template>
    <h1>{{ title }}</h1>
</template>

<script>
module.exports = {
    props: ['title']
</script>

這裏將會讀取服務端入口文件傳入 options 的 data 屬性,取到 title 值,渲染到對應 DOM 中;git

再看看 Webpack 的配置,和客戶端渲染一樣是大同小異:github

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);  // 告訴客戶端錯誤  return response.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 服務端渲染的整個流程了,這樣子和使用普通的模板渲染並無什麼其餘的優點,但是當渲染完成後再由客戶端接管渲染後就能夠作到無縫切換了,下面咱們來看看和客戶端結合渲染;

3. 和瀏覽器渲染無縫集合

爲了和客戶端渲染結合,咱們將 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

import Vue from 'vue';  import App from './vue/App'; import ClipButton from 'Components/ClipButton'; import Toast 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);   return new VueApp(Object.assign({}, options)); };  export {Vue, createApp}; 

index.js 中作一些通用的組件、插件加載,一些全局的設置,最後返回一個能夠生成 app 實例的函數供不一樣環境來調用;

server.js

import {createApp} from './index';  export default function (options) {  const app = createApp(options);   return new Promise(resolve => {  resolve(app);  }); } 

大部分邏輯已經抽爲共用了,因此服務端這裏就是簡單將 app 實例經過 promise 返回;

client.js

import VueResource 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 作路由分發;這樣,咱們服務端也最好使用同一套路由來渲染不一樣的頁面。

4. 服務端和瀏覽器路由共用

路由這裏使用 vue-router 就能夠了,瀏覽器端仍是經過正常的方式載入路由配置,服務端一樣載入路由配置,並在渲染以前使用 router.push 渲染須要展示的頁面,因此,在通用的入口文件加入路由配置:

import Vue from 'vue'; import router from './router'; import App from './vue/App';  const createApp = function createApp(options) {  const VueApp = Vue.extend(App);   return new VueApp(Object.assign({  router  }, options)); };  export {Vue, router, createApp}; 

路由文件是這樣子的:

import Vue from 'vue'; import VueRouter from 'vue-router';  import ViewUpload from '../vue/ViewUpload'; import ViewHistory from '../vue/ViewHistory'; import ViewLibs 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);  return new Promise(resolve => {  resolve(app);  }); } 

這裏在初始化 app 實例後,調用 router.push(options.url) 將服務端取到的 url push 到路由之中;

5. 使用中遇到的坑

整個過程還算順利,其中遇到最多的問題就是有些模塊只能在服務端或者瀏覽器來使用,而使用 ES6 模塊加載是靜態的,因此須要將靜態加載的模塊改成動態加載,因此就有了上面配置 isBrowser 這個全局屬性,經過這個屬性來判斷模塊加載了,好比我項目中用到的 clipboard 模塊,以前是直接使用 ES6 加載的;

<template>  <a @click.prevent :href="text"><slot></slot></a> </template>  <script> import 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);  });  });  } }; </script> 

這樣就會在服務端渲染時候報錯,將加載改成動態加載就能夠了:

let Clipboard = null;  if (isBrowser) {  Clipboard = require('clipboard'); } 

若是這個模塊在服務端並不會渲染,那後面的代碼並不須要更改;

還有 VueResource 插件也是須要瀏覽器環境的,因此須要將它單獨配置在 client.js 中;

6. 總結

一樣的路由經過 Vue 服務端渲染後的 HTML 老是同樣的,這和 React 渲染後會加上哈希不一樣,因此能夠作渲染後結果的緩存優化,這部分能夠參考官方文檔的作法,總的來講,Vue 服務端渲染沿襲了 Vue 客戶端的輕量作法,也顯得比較輕量,惟一不足之處可能就是服務端也一樣須要 webpack 來完成。

 

原文:https://blog.alphatr.com/how-to-use-ssr-in-vue-2.0.html

相關文章
相關標籤/搜索