Vue同構(二): 路由與代碼分割

Vue同構(二): 路由與代碼分割

前言

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

上一篇文章Vue同構(一)咱們介紹了若是使用Vue同構在服務端渲染一個簡單組件並在服務端對應激活。對應的代碼已經上傳到Github。本篇文章咱們介紹Vue同構中路由相關的知識。html

路由

寫到這裏咱們首先討論一下爲何會須要有前端路由,爲何咱們的程序中須要引入Vue-Router呢?其實最先的網站都是服務器渲染的,並不存在什麼瀏覽器渲染。每次在瀏覽器導航欄輸入對應的URL或者點擊當前的頁面的連接的時候,瀏覽器就會接收到對應的URL並渲染出HTML頁面。這就會存在一個問題,就是每次操做都意味着頁面刷新。異步請求的出現,解決了這一切,咱們能夠經過XMLHTTPRequest去動態請求數據而不是每次都刷新對應界面,實現了不須要後臺刷新實現頁面交互。後來單頁面應用(SPA: Single Page Web Application)的出現將這個概念更進一步,不只頁面交互不須要刷新頁面,連頁面跳轉都不須要刷新當前頁面。當頁面跳轉都不須要刷新當前頁面時,咱們必須就要解決的是不一樣URL下組件切換的問題,這也就是前端路由所作的工做。前端

路由(Router)概念實際上是來自於後臺,負責URL到函數的映射。好比:vue

/user         ->    getAllUsers()
/user/count   ->    getUserCount()

其中的每個URL到函數的映射規則咱們稱爲一個route,而router則至關於管理route的容器。前端路由的概念與此相似,只不過URL映射的是前端組件。好比:java

/user         ->    User組件

客戶端渲染路由

得益於Vue的優雅設計,Vue與Vue Router的結合使用很是簡單,其實就是首先配置好路由規則並生成路由實例,而後將路由實例傳遞給Vue根元素將其添加進來,最後使用router-view組件來告訴Vue Router在哪裏渲染。webpack

<div id="app">
  <!-- 路由匹配到的組件將渲染在這裏 -->
  <router-view></router-view>
</div>
//引入路由組件
import Home from '../components/Home.vue'
import About from '../components/About.vue'

//路由配置
const routes = [
  { path: '/', component: Home },
  { path: '/home', component: Home },
  { path: '/about', component: About }
]

//建立Vue Router實例
var router = new VueRouter({
    routes
})

//引入vue-router
const app = new Vue({
  router,
  //......
})

服務器渲染路由

上面咱們介紹了Vue Router在客戶端渲染的邏輯,固然這只是最簡單的邏輯,更高階的使用能夠參閱Vue Router官方文檔,並非本篇文章的重點內容,所以咱們就不在贅述。git

Vue Router其實有兩種模式: hash模式和history模式。hash模式是Vue Router默認的模式。要講清這兩種模式咱們不得不提到兩種模式所對應的不一樣的實現邏輯。github

hash模式

其實咱們能夠想到,做爲前端路由切換的過程當中是不能引發瀏覽器刷新的,不然就違反了SPA路由交互的規則。首先咱們就瞄上了URL中的片斷標識符(錨點),做爲一個完整的URL,格式以下web

http://user:pass@www.example.com:80/dir/index.html?uid=1#ch1

#ch1的部分就是咱們所說的片斷標識符,一般可用來標記出已獲取資源中的子資源,片斷標識符的改變並不會引發瀏覽器的刷新,所以hash模式就是使用的片斷標識符來做爲前端路由的依據。在前端路由中咱們把片斷標識符稱做hash部分,hash部分僅僅只是客戶端的狀態,hash部分並不會被服務器端所接收。咱們能夠經過window.onhashchagnge事件來監聽url中hash部分的變化,這也是基於hash路由的基礎。舉個例子:vue-router

window.addEventListener('hashchange', function(e){
    console.log('hashchange', e);
})

若是瀏覽器的hash部分變化了,監聽函數會馬上調用對應的事件。

history模式

HTML5 引入了新的API,能夠在不刷新當前頁面的狀況下,改變URL。分別對應的兩個方法:

  • pushState(state, title, url)
  • replaceState(state, title, url)

pushState用於向瀏覽器的歷史記錄中添加一條新記錄,同時改變地址欄的地址內容。replaceState則與pushState相似,是修改了當前的歷史記錄項而不是新建一個。兩個函數對應的參數分別是:

  • state(狀態對象): 狀態對象state是一個JavaScript對象,url改變後對應的事件狀態能夠讀取到該狀態對象。可用於還原頁面狀態。
  • title(標題): 目前忽略這個參數,但將來可能會用到。可傳空字符串
  • URL: 該參數定義了新的歷史URL記錄。

pushStatereplaceState配套使用的是onpopstate事件,須要注意的是調用history.pushState()history.replaceState()不會觸發popstate事件。只有在作出瀏覽器動做時,纔會觸發該事件,如用戶點擊瀏覽器的回退按鈕(或者在Javascript代碼中調用history.back()

例如:

window.addEventListener('popstate', function(){
    console.log("location: " + document.location + ", state: " +JSON.stringify(event.state));
})

history.pushState({page: 1}, "title 1", "?page=1");
history.back(); // alerts "location: http://example.com/example.html?page=1, state: {"page":1}"

兩種模式咱們說完了,history相比於hash來講url要美觀,可是須要後臺服務器的支持,由於history最怕瀏覽器刷新了,好比咱們前端的路由從/home改變爲/about,這個僅僅是前端url的改變,並不會刷新當前頁面,而且包括瀏覽器的後退和前進也不會刷新瀏覽器。可是若是一旦刷新,瀏覽器是真的會去請求當前的url,好比/about。這個時候,若是瀏覽器並不能識別這個url,就可能找不到當前頁面。

簡單的例子

說了這麼多,咱們服務器渲染須要採用哪一種模式呢?咱們採用的是history模式,這個是惟一的選擇,答案其實上面已經說過了,由於hash部分僅僅只是客戶端的狀態,並不會被服務器端所接收。如今咱們假設咱們當前的應用有兩個路由:

/             ->    Home
/about        ->    About

首先咱們建立咱們的路由實例,上一篇文章中咱們會爲每次的請求建立新的組件實例,其目的就是爲了方式不一樣的請求之間交叉影響,路由實例也是相同的道理:

// router/index.js
import Vue from 'vue'
import Router from 'vue-router'

import Home from '../components/Home.vue'
import About from '../components/About.vue'

Vue.use(Router)

export function createRouter() {
    return new Router({
        mode: "history",
        routes: [{
            path: '/', component: Home
        }, {
            path: "/about", component: About
        }]
    })
}

createRouter函數每次調用都建立一個路由實例,路由實例中配置的history模式,而且配置了路由規則。

接下來咱們看看Home組件:

<template>
    <div>
        <div>當前位置: About</div>
        <router-link to="/home">前往Home</router-link>
        <button @click="directHome">按鈕: 前往Home</button>
    </div>
</template>

<script>
    export default {
        name: "about",
        methods: {
            directHome: function () {
                this.$router.push('/');
            }
        }
    }
</script>

這個組件我之因此在使用了router-link的狀況下還使用了button,主要是爲了證實客戶端已經激活。About組件和Home組件除了名字和連接地址不一樣,其他徹底一致,再也不列出。

咱們在根組件App中渲染路由匹配的組件

//App.Vue
<template>
    <div id="app">
        <router-view></router-view>
    </div>
</template>

接下來咱們須要繼續改造app.js,上篇文章中咱們已經介紹過服務器中app.js主要任務是對外暴露一個工廠函數,具體客戶端和瀏覽器端的邏輯已經分別轉移到客戶端和瀏覽器端的入口文件entry-client.jsentry-server.js

import Vue from 'vue'
import App from './components/App.vue'
import {createRouter} from './router'

export function createApp() {
    const router = createRouter()

    const app =  new Vue({
        router,
        render: h => h(App)
    })

    return {
        app,
        router
    }
}

createApp與以前不一樣之處在於,每次建立的Vue實例中都注入了router。並返回了建立的Vue實例和Vue Router實例。

服務端渲染的邏輯集中在entry-server.js:

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

export default function (context) {
    return new Promise((resolve, reject) => {
        const {app, router} = createApp()
        router.push(context.url)
        router.onReady(() => {
            // Promise 應該 resolve 應用程序實例,以便它能夠渲染
            resolve(app)
        }, reject)

    })
}

entry-server.js做爲服務端渲染的入口打包爲對應的bundle傳入createBundleRenderer生成renderer,調用renderer.renderToString能夠傳入context,其中就能夠包含當前路由的url。而咱們在entry-server.js的函數中接受該context對象即可以獲取該路由信息。

與上面文章不一樣的,咱們並無直接返回Vue實例而是返回了一個Promise,在Promise中首先咱們調用createApp獲取到Vue實例app和Vue Router實例router,而後咱們調用push函數將當前的路由導航到目標url上。而後咱們調用在router.onReady函數,確保等待路由的全部異步鉤子函數異步組件加載完畢以後,resolve當前的Vue實例。

entry-server.js類似,客戶端的打包入口文件entry-client.js也須要在掛載 app 以前調用 router.onReady:

import { createApp } from './app'

const {app, router} = createApp();

router.onReady(() => {
    app.$mount('#app')
})

如今咱們繼續來看咱們的express服務器代碼,和上次的渲染基本徹底一致,只不過咱們須要給renderToString傳遞一個context對象,其中包含當前的url值便可。

//server.js
//省略......

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

//省略......

如今咱們打包好服務端和瀏覽器端的bundle,並啓動服務器:

如今咱們思考一個問題,若是咱們設置爲路由router中設置了守衛,是會在瀏覽器中執行仍是會爲服務端執行呢?爲了驗證這個問題,咱們給router增長全局守衛beforeEachafterEach:

export function createApp() {
    const router = createRouter()

    router.beforeEach((to, from, next) => {
        console.log("beforeEach---start");
        console.log('to: ', to.path, ' from: ', from.path);
        console.log("beforeEach---end");
        next();
    })

    router.afterEach((to, from) => {
        console.log("afterEach---start");
        console.log('to: ', to.path, ' from: ', from.path);
        console.log("afterEach---end");
    })
    // 省略......
}

咱們直接訪問/路由,咱們能夠看到服務端和客戶端的輸出結果以下:

服務端

客戶端

這說明守衛函數在服務器端和客戶端都同時執行了,兩端的路由都解析了調用組件中可能存在的路由鉤子。開發過程當中可能要留心這點。

代碼分割

首先能夠考慮一個問題,咱們當初引入Vue的同構的主要目的就是加快首屏的顯示速度,那麼咱們能夠考慮一下,若是咱們訪問/路由的時候,其實只須要加載Home組件就能夠了,並不須要加載About組件。等到須要的時候,咱們能夠再去加載About組件,這樣咱們就能夠減小初始渲染中下載的資源體積,加快可交互時間。在這裏咱們就能夠考慮對代碼進行分割。

代碼分割其實也是Webpack所支持的特性,能夠將不一樣的代碼打包到不一樣的bundle中,而後按需加載文件。

Webpack最簡單的代碼分割無非是手動操做,你能夠經過配置多個entry來實現,可是手動的模式存在諸多的問題,好比多個bundle都引用了相同的模塊,則每一個bundle中都存在重複代碼。這個問題卻是好解決,咱們可使用SplitChunksPlugin插件去解決這個問題。可是手動畢竟仍是不太方便,因此Webpack提供了更爲方便的動態導入

動態導入的功能推薦使用ECMAScript提案的import()語法,import()能夠指定所要加載的模塊的位置,而後執行時動態加載該模塊,並返回一個Promise。好比說咱們在一個模塊中想要動態加載lodash模塊,咱們首先能夠在Webpack的配置文件中添加:

output: {
    chunkFilename: '[name].bundle.js',
},

chunkFilename就是爲了配置決定非入口chunk的名稱,而後在代碼中:

import(/* webpackChunkName: "lodash" */ 'lodash').then(lodash => {
    //lodash即可以使用
})

打包代碼咱們能夠發現lodash被單獨打包,由於在註釋中咱們將webpackChunkName的值賦值爲lodash,所以將其命名爲 lodash.bundle.js。固然這種chunkFilename也並非必須的,默認會被命名成[id].bundle.js

異步組件

Vue提供異步組件的概念,容許咱們將代碼分割成代碼塊,而且按需加載。相比與普通的組件註冊,咱們能夠用工廠函數的方式定義組件,這個工廠函數會收到一個resolve回調,這個回調函數會在你從服務器獲得組件定義的時候被調用。或者直接在該工廠函數中返回一個Promise。咱們知道import()語法返回的就是一個Promise,所以咱們搭配改造以前的代碼:

// router.js
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export function createRouter() {
    return new Router({
        mode: "history",
        routes: [{
            path: '/',
            component: () => import('../components/Home.vue')
        }, {
            path: "/about",
            component: () => import('../components/About.vue')
        }]
    })
}

而後打包客戶端bundle:

> vue-ssr-demo@1.0.0 build:client /Users/mr_wang/WebstormProjects/vue-ssr-demo
> cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules

Hash: 16fbba9bf008ec7ef466                                                            
Version: webpack 3.12.0
Time: 1158ms
                           Asset     Size  Chunks                    Chunk Names
       0.8ac6ad83b93d774d3817.js  5.04 kB       0  [emitted]         
       1.5967060b78729a4577f9.js  5.04 kB       1  [emitted]         
     app.1c160fc3e08eec3aed0f.js  7.37 kB       2  [emitted]         app
  vendor.f32c57c9ee5145002da1.js   296 kB       3  [emitted]  [big]  vendor
manifest.4b057fd51087adaec1f3.js  5.85 kB       4  [emitted]         manifest
    vue-ssr-client-manifest.json  1.48 kB          [emitted]

咱們發現輸出文件多了0.[hash].js和1.[hash].js,其中分別對應的就是Home組件與About組件。固然若是你以爲這個模塊看起來不清晰,也能夠按照以前所說的傳入webpackChunkName參數,讓打包出來的問題更具備可識別性:

component: import(/* webpackChunkName: "home" */'../components/Home.vue')
component: import(/* webpackChunkName: "about" */'../components/About.vue')

這時Webpack打包出的文件:

Hash: aaf79995904c4786cadc                                                           
Version: webpack 3.12.0
Time: 976ms
                           Asset     Size  Chunks                    Chunk Names
                  home.bundle.js  5.04 kB       0  [emitted]         home
                 about.bundle.js  5.04 kB       1  [emitted]         about
     app.f22015420ff0db6ec4b0.js  7.37 kB       2  [emitted]         app
  vendor.f32c57c9ee5145002da1.js   296 kB       3  [emitted]  [big]  vendor
manifest.2a21c55e4a3e98ab252c.js  5.83 kB       4  [emitted]         manifest
    vue-ssr-client-manifest.json  1.44 kB          [emitted]

而後咱們啓動服務器,訪問'/'路由,咱們發現請求以下:

首先咱們看network,咱們發現,0.[hash].js首先被請求,而後再請求1.[hash].js,而且兩者加載的優先級是不一樣的,0.[hash].js的優先級高於1.[hash].js,這是爲何呢?咱們看對應的html。

咱們能夠看到0.[hash].js在注入的時候是preload而1.[hash].js注入的時候是prefetch,preload和prefetch之間有什麼區別嗎,其實但要說這兩個都能單寫一篇文章,可是在這邊咱們仍是簡單總結一下。

prefetch是一種告訴瀏覽器獲取一項可能被下一頁訪問所須要的資源方式。這意味着資源將以較低優先級地獲取,所以prefetch是用於獲取非當前頁面使用的資源。

preload是告訴瀏覽器提早加載較晚發現的資源。有些資源是隱藏在CSS和JavaScript中的,瀏覽器不知道頁面即將須要這些資源,而等到發現時加載又太晚了,所以聲明式的提早加載。

總結

這篇文章主要講了在Vue經過下若是使用路由而且如何經過代碼分割的方式進一步提升頁面首屏加載速度。具體的代碼能夠點這裏查看。最後但願能點個Star支持一下個人博客,感激涕零,若是有表述錯誤的地方,歡迎你們指正。

相關文章
相關標籤/搜索