Vue同構(一): 快速上手

前言

  首先歡迎你們關注個人Github博客,也算是對個人一點鼓勵,畢竟寫東西無法得到變現,能堅持下去也是靠的是本身的熱情和你們的鼓勵。
  javascript

同構(服務器渲染)

  Vue同構也就是咱們常說的服務器渲染(Server Side Render),服務器渲染放在今天已經算不上是一個新鮮的東西了,從React到Vue都有各自的服務器渲染方案,不少小夥伴可能都有所接觸,首先咱們要了解一下爲何須要服務器渲染呢?Vue和React這類框架有一個特色,都屬於瀏覽器渲染,好比一個最簡單的例子:
  css

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

  咱們能夠看到,咱們收到服務器的模板中其實並無咱們所期待界面對應的html結構,而僅有一個用於掛載應用的根元素,在客戶端瀏覽器執行加載的JavaScript代碼時,纔會建立對應的DOM結構。然而瀏覽器渲染其實存在兩個明顯的缺點:html

  • 對搜索引擎優化(SEO:Search Engine Optimization)不友好,各個搜索引擎實際上都是對網頁的html結構和同步Javascript代碼進行索引,於是客戶端渲染可能會形成你的網頁沒法被搜索引擎正確索引。
  • TTC(內容到達時間:Time-To-Conten)過長,試想若是設備的網絡較差或者設備的代碼執行速度較慢,用戶須要等待較長的時間才能看到頁面的內容,等待期間看到的都是網頁的白屏或者其餘的加載狀態,這絕對是糟糕的用戶體驗。

  幸運的是,Node的到來爲這一切帶來了曙光,JavaScript不只僅能夠在瀏覽器中執行,並且也可能在後端環境中執行。所以咱們能夠將用戶的界面在服務器中渲染成HTML 字符串,而後再傳給瀏覽器,這樣用戶得到的就是可預覽的界面,最後將靜態標記"混合"爲客戶端上徹底交互的應用程序,整個渲染的過程就結束了。前端

最簡單的例子

  Vue服務器渲染使用官方提供的庫vue-server-renderer,因爲Express比較直觀,咱們採用Express做爲後端服務器,咱們首先給出一個最簡單的例子:vue

// server.js
const Vue = require('vue')
const server = require('express')()
// 建立一個 renderer
const renderer = require('vue-server-renderer').createRenderer()

server.get('*', (req, res) => {
  // 建立一個 Vue 實例
  const app = new Vue({
    data: {
      url: req.url
    },
    template: `<div>訪問的 URL 是: {{ url }}</div>`
  })

  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    // html就是Vue實例app渲染的html
    res.end(`
      <!DOCTYPE html>
      <html lang="en">
        <head><title>Hello</title></head>
        <body>${html}</body>
      </html>
    `)
  })
})

server.listen(8080)

  而後啓動node server.js,而且瀏覽器中訪問好比http://localhost:8080/app,瀏覽器界面中則會顯示出:java

訪問的 URL 是:/app

  這時候觀察該請求的返回值是:node

  咱們發現返回的html中已經渲染好DOM元素。所以咱們無需等待當即能夠看見頁面的內容。而上面的代碼邏輯也很是簡單,http服務器接收到get請求的時候,都會建立一個Vue實例,vue-server-renderer中的createRenderer用來建立一個Renderer實例,Renderer中的renderToString用來將Vue實例轉化對應的HTML字符串,須要注意的是,咱們須要將建立好的字符串包裹在html一併返回。固然你能夠採用頁面模板的形式,將二者相分離:webpack

<!DOCTYPE html>
<html lang="en">
  <head><title>Hello</title></head>
  <body>
    <!--vue-ssr-outlet-->
    <!--這裏將是應用程序 HTML 標記注入的地方>
  </body>
</html>
// renderer中包含了模板
const renderer = createRenderer({
  template: require('fs').readFileSync('./index.template.html', 'utf-8')
})

renderer.renderToString(app, (err, html) => {
  res.end(html)
})

  固然這只是最簡單的一個例子,瀏覽器收到的僅僅是對應Vue實例的html代碼,並無將其激活,所以是不可交互的。git

瀏覽器渲染的流程

  對於瀏覽器渲染中,咱們首選Webpack對代碼進行打包,總體流程能夠經過下面圖來釋義:github

  對於一個Vue應用,源碼層面其實主要包括三個方面: 組件、路由、狀態管理。這部分代碼咱們認爲是通用代碼,能夠同時在服務器端和瀏覽器端執行,Webpack有兩個入口: server entryclient entry,分別用來打包在服務器端執行的代碼與在瀏覽器端執行的代碼。Server Bundle做爲打包在服務器端執行的代碼,負責的生成對應的HTML,而Clinet Bundle做爲執行在瀏覽器端的代碼,主要負責的就是激活應用。

下面咱們給出對應的webpack配置,爲了方便上手咱們就僅僅只列出最簡單的配置,讓咱們能將代碼跑起來,配置包括三個部分: baseclientserver,其中base是兩者間能通用的部分,client則是對應瀏覽器的打包配置,server是服務器端的打包配置,經過webpack-merge(能夠簡單理解成 Object.assign)將其鏈接:

// webpack.base.config.js
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
    output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: '/dist/',
        filename: '[name].[chunkhash].js'
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
}

  上面是一個最簡單的webpack中通用的配置,規定了三部分:

  • output: 打包文件怎樣存儲輸出結果以及存儲到哪裏
  • module: 咱們對js文件和vue文件執行相應的loader
  • plugins: VueLoaderPlugin插件是必須的,做用是將你定義過的其它規則複製並應用到 .vue 文件裏相應語言的塊。好比vue文件中script標籤對應的JavaScript代碼和stype標籤對應的css代碼。
// webpack.server.config.js
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(base, {
    target: 'node',
    entry: './src/entry-server.js',
    output: {
        libraryTarget: 'commonjs2'
    },
    plugins: [
        new VueSSRServerPlugin()
    ]
})

  上面的配置用來打包服務器架bundle:

  • target: 用來指示構建目標,node表示webpack會編譯爲用於類 Node.js環境
  • entry: 服務器打包入口文件
  • libraryTarget: 由於是用於Node環境,所以咱們選擇commonjs2
  • VueSSRServerPlugin: 用來打包生成的服務器端的bundle,最終能夠將全部文件打包成一個json文件,最終傳給服務器renderer使用。
// webpack.client.config.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

module.exports = merge(base, {
    entry: {
        app: './src/entry-client.js'
    },
    plugins: [
        // extract vendor chunks for better caching
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: function (module) {
                // a module is extracted into the vendor chunk if...
                return (
                    // it's inside node_modules
                    /node_modules/.test(module.context)
                )
            }
        }),
        // extract webpack runtime & manifest to avoid vendor chunk hash changing
        // on every build.
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest'
        }),
        new VueSSRClientPlugin()
    ]
})
  • entry: 瀏覽器打包入口文件。
  • VueSSRClientPlugin:相似於VueSSRServerPlugin插件,主要的做用就是將前端的代碼打包成bundle.json,而後傳值給renderer,能夠自動推斷和注入preload/prefetch指令和script標籤到渲染的HTML中。

  關於CommonsChunkPlugin插件,其實對於一個最簡單的應用而言是能夠沒有的,可是由於其有助於性能提高仍是加了進來。在最開始學習Webpack的時候,每次打包的時候都會將全部的代碼打包到同一個文件,好比app.[hash].js中,其實在app.[hash].js中包含兩部分代碼,一部分是每次都在變化的業務邏輯代碼,另外一部分是幾乎不會變化的類庫代碼(例如Vue的源碼)。如今這種狀況其實很不利於瀏覽器的緩存,由於每次業務代碼改變後,app.[hash].js必定會發生改變,所以瀏覽器不得不從新請求,而app.[hash].js的代碼量可能都是數以兆計的。所以咱們能夠將業務代碼和類庫代碼相分離,在上面的例子中:

new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: function (module) {
    // a module is extracted into the vendor chunk if...
        return (
        // it's inside node_modules
        /node_modules/.test(module.context)
        )
    }
}),

  咱們將引用的node_modules中的代碼打包成vendor.[hash].js,其中就包含了引用的類庫,這是代碼中相對不變的部分。可是若是僅僅只有上面的部分的話,你會發現每次邏輯代碼改變後,vendor.[hash].jshash值也會發生改變,這是爲何呢?由於Webpack每次打包運行的時候,仍然是會產生一些和Webpack當前運行相關的代碼,會影響到運行的打包值,所以vendor.[hash].js每次打包仍然是會發生改變,這時候其實瀏覽器並不能正確的緩存。所以咱們使用:

new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest'
})

咱們須要將運行環境提取到一個單獨的manifest文件中,這樣vendorhash就不會變了,瀏覽器就能夠將vendor正確緩存,mainfesthash雖然每次在變,但很小,比起vendor變化帶來的影響能夠忽略不計。

咱們以前講過,Vue的應用其實能夠劃分紅三個部分: 組件、路由、狀態管理,做爲SSR系列的第一篇上手文章,咱們僅介紹如何在服務端渲染一個簡單組件並在客戶端激活該組件,使得其可交互。路由和狀態管理等其餘部分會在後序部分介紹。

組件

  首先咱們用Vue寫一個最簡單的可計數的組件,點擊"+"能夠增長計數,點擊"-"能夠減小計數。

// App.vue
<template>
    <div id="app">
        <span>times: {{times}}</span>
        <button @click="add">+</button>
        <button @click="sub">-</button>
    </div>
</template>

<script>
    export default {
        name: "app",
        data: function () {
            return {
                times: 0
            }
        },
        methods: {
            add: function () {
                this.times = this.times + 1;
            },
            sub: function () {
                this.times = this.times - 1;
            }
        }
    }
</script>
<style scoped>
</style>

  上面的部分是一個很是簡單的Vue組件,也是服務端和客戶端渲染的通用代碼。在單純的客戶端渲染的程序中,會存在一個app.js用來建立一個Vue實例並將其掛載到對應的dom上,例如:

// 客戶端渲染 app.js
import App from './App.vue'

new Vue({
  el: '#app',
  components: { App },
  template: '<App/>',
})

  在服務器渲染中,app.js僅會對外暴露一個工廠函數,用來每次都調用的都會返回一個新的組件實例用於渲染。具體的其餘邏輯都被各自轉移到客戶端和瀏覽器端的入口文件中。

import Vue from 'vue'
import App from './components/App.vue'

export function createApp() {
    return new Vue({
        render: h => h(App)
    })
}

  不一樣於客戶端渲染,值得注意的是咱們須要爲每一次請求都建立一個新的Vue實例,而不能共享同一個實例,由於若是咱們在多個請求之間使用一個共享的實例,可能會在各自的請求中形成狀態的污染,因此咱們爲每一次請求都建立獨立的的組件實例。

  接下來看瀏覽器端打包入口文件:

// entry-server.js
import { createApp } from './app'

export default context => {
    const app = createApp()
    return app
}

  entry-server.js對外提供一個函數,用於建立當前的組件實例。接着看客戶端打包入口文件:

// client-server.js
import { createApp } from './app'

var app = createApp();

app.$mount('#app')

  邏輯也是很是的簡單,咱們建立一個Vue實例,用將其掛載到idapp的DOM結構中。

  這時候咱們運行命令分別打包客戶端和服務器端的代碼,咱們發現dist,目錄下分別出現如下文件:

  咱們能夠看到app.[hash].js是打包的業務代碼,vendor.[hash].js則是相應的庫的代碼(好比Vue源碼),manifest.[hash].js則是CommonsChunkPlugin生成manifest文件。而vue-ssr-client-manifest.json則是VueSSRClientPlugin生成的對應客戶端的bundle,而vue-ssr-server-bundle.json則是VueSSRServerPlugin插件生成的服務器端的bundle。有了上面的打包文件,咱們就能夠處理請求:

//server.js
const fs = require("fs")
const express = require("express")
const { createBundleRenderer } = require('vue-server-renderer')

const template = fs.readFileSync("./src/index.template.html", "utf-8")
const bundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')


const app = express();
app.use("/dist", express.static("dist"))

const renderer = createBundleRenderer(bundle, {
    template,
    clientManifest
})


app.get('*', (req, res) => {
    renderer.renderToString({}, function (err, html) {
        res.end(html);
    });
})

app.listen(8080, function () {
    console.log("server start and listen port 8080")
})

  此次咱們並無使用一開始介紹的vue-server-renderer中的createRenderer函數,而是使用的createBundleRenderer函數,咱們在server.js中分別引入了server-bundle.jsonclient-manifest.json與模板template.html,而後將其傳給createBundleRenderer函數生成renderer,而後在每一次請求中,調用的rendererrenderToString方法,生成對應的html,而後返回客戶端。renderToString的第一個參數實質是上下文context對象,一方面context用於處理模板文件,好比模板文件中存在

<title>{{title}}</title>

  而context中存在title: 'SSR',模板中的文件則會被插值。另外一部分,客戶端的入口文件server-entry.js的中函數也會收到該context,可用於傳遞相關的參數。

  咱們之因此會使用express.staticdist文件夾下面的文件提供靜態的資源服務的緣由是客戶端的代碼中會注入相應的JavaScript文件(好比app.[hash].js),這樣才能保證對應的資源能夠被請求到。

  而後咱們運行命令:

node server.js

  並在瀏覽器中訪問http://localhost:8080。你就會發現一個簡單的計數器的程序已經運行,而且是可運行,點擊按鈕會觸發相應的事件。

  這是的對應接受的html結構爲:

  咱們發現返回的html代碼中就有咱們Vue實例對應的DOM結構,與普通的客戶端結構不一樣的,根元素中存在data-server-rendered屬性,表示該結構是由服務端對應渲染的節點,在開發模式中,Vue將渲染的虛擬DOM與當前的DOM結構相比較,若是相等的時候,則會複用當前結構,不然會放棄已經渲染好的結構,轉而從新在客戶端渲染。在生產模式下,則會略過檢測的步驟,直接複用,避免浪費性能。

  在服務器渲染中,一個組件僅僅會經歷beforeCreatecreated兩個生命週期,而其他的例如beforeMount等生命週期並不會在服務器端執行,所以應該注意的是避免在beforeCreatecreated 生命週期時產生全局反作用的代碼,例如在beforeCreatecreated中使用setInterval設置timer,而在beforeDestroydestroyed生命週期時將其銷燬,這會形成timer永遠不會被取消。

  至此咱們介紹了一個最簡單的Vue服務器渲染示例並在客戶端對應將其激活,服務器渲染的其餘部分好比路由、狀態管理等部分咱們將在接下來的文章一一介紹,有興趣的同窗記得在個人Github博客中點個Star,若是文章中有不正確的地方,歡迎指出,願一同進步。

相關文章
相關標籤/搜索