應用於 Hybrid App 的 Vue 多頁面構建

呆戀小喵_sunmy發表於2019-03-04

本文介紹一款基於 Vue 的使 App 支援離線快取 Web 資源的混合開發框架。本人小白一枚,請將它視作一份我的學習總結,歡迎大神們賜教。本文多闡述思路,實現細節請閱讀原始碼。

原始碼

為何選擇混合開發?

  • 高效率介面開發:HTML + CSS + JavaScript 被證實具備極高的介面開發效率。

  • 跨平臺:較統一的瀏覽器核心標準,使 H5 頁面在 IOS、Android 共享同套程式碼。使用 Native 開發一功能需 IOS、Android 研發各一枚,而使用 H5 一枚前端工程師足矣。但混合 App 並非 Native 越少越佳,效能要求較高的仍需勞 Native 大駕…分工需明確,不可厚此薄彼。

  • 熱更新:不依賴於釋出渠道自主更新應用。Native 修復線上 Bug 需釋出新版本,使用者未升級 App 該 Bug 將一直呈現。而修復 H5 只需將 Fixbug 的程式碼推至伺服器,任一版本 App 便可同步更新對應功能無需升級。

為何離線快取 Web 資源?

相比於從遠端伺服器請求載入 Web 資源,App 優先載入本地預置資源,可提升頁面響應速度,節省使用者流量。

問題來了…本地預置的 Web 資源也隨 App 安裝包一起成為潑出去的水,修復 H5 線上 Bug 也需發版了?丟西瓜撿芝麻的事定不可做!請注意“優先載入本地預置資源”,但檢測到更新時載入遠端最新資源,如何檢測更新我稍後闡明。

對我司前端團隊的意義

  • 技術棧由 Jinja + jQuery + Require + Gulp 遷移至 Vue + Webpack + Gulp + Sass,擁抱 Vue!
應用於 Hybrid App 的 Vue 多頁面構建
  • 實現前後端分離:原 Jinja 為 Python 模板引擎,前端程式碼的運作依賴於服務端,服務端異常等待環境維修嚴重影響前端工作進度。分離後,伺服器掛了我們愉快的開啟 Mock Server 繼續搬磚便是。

  • App 優先載入本地預置 Web 資源,可提升 H5 頁面載入速度。

弊端

  • 技術重構本身具備風險性。

  • 增加團隊學習成本。

  • 前端框架通過 JS 渲染 HTML 對 SEO 不友好。但你可選擇使用 Vue 2.2 的服務端渲染(SSR)。增添 Node 層除實現 SSR,能做的事還很多…


進入正題~

混合開發框架運作機制

應用於 Hybrid App 的 Vue 多頁面構建

將 Web 資原始檔打包至 dist/(含 routes.json 及 N 多 .html)並壓縮為 dist.zip,圖片資源單獨打包至 assets/,一同上傳至 CDN。

應用於 Hybrid App 的 Vue 多頁面構建

App 內預置 dist/ 下全部資源(發版時僅下載 dist.zip,安裝 App 時解壓),在攔截並解析 URL 後,通過 routes.json 查詢並載入本地 .html 頁面。

routes.json 如下:

{
    "items": [
        {
            "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Demo-13700fc663.html",
            "uri": "https://backend.igengmei.com/demo[/]?.*"
        },
        {
            "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Album-a757d93443.html",
            "uri": "https://backend.igengmei.com/album[/]?.*"
        },
        {
            "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/post/ArticleDetail-d5c43ffc46.html",
            "uri": "https://backend.igengmei.com/article/detail[/]?.*"
        }
    ],
    "deploy_time": "Fri Mar 16 2018 15:27:57 GMT+0800 (CST)"
}
複製程式碼

欠你一個回答~

請注意“優先載入本地預置資源”,但檢測到更新時載入遠端最新資源,如何檢測更新我稍後闡明。

檢測 .html 檔案更新的橋樑便是 routes.json。每啟動 App 從 CDN 靜默更新 routes.json 一次(CDN 快取會導致 routes.json 無法及時更新,下載路由表請新增時間戳引數強制更新),任一資源更新均同步至 routes.json 並上傳 CDN。

標記更新的方式則是為 .html 打 Hash(MD5)戳,於 App 而言不同 Hash 字尾的 .html 為不同檔案。App 根據路由表 remote_file 查尋本地 .html,若該 .html 不存在則直接載入遠端資源同時靜默下載更新。

注:由於 js、css 指令碼均被內聯至對應 .html,App 僅需監聽 .html 檔案的變化。其實我們可以提取公用指令碼併為之打 Hash 戳,將該資源的變化記錄至一張表供 App 監聽。常年不更新的公用指令碼,快取在 App 內不隨 .html 一同載入也可提升頁面響應速度。

綜上,Web 資源雖被預置於 App,但其 Fixbug 級別的更新不必走發版這條路。

為何圖片資源單獨打包至 assets/,先欠著~


Web 框架設計

Web 框架設計圍繞:

  • 減少無用資源及冗餘資源

  • 減小依賴模組對 Hash 的影響

  • 開發環境模式儘量簡易

減少無用資源及冗餘資源

機智的你發現使用 Vue 腳手架 build 後產生單 .html、單 .js、單 .css(所有頁面資源打包在一坨啦),而我所舉例的卻是多 .html。如何實現 Vue 多頁面拆分我會細講,先討論拆分多頁面的意義吧:“快” + “節約”!

假定我站含頁面 A、B、C,使用者僅訪問 A 但單頁應用卻將 A、B、C 所依賴的全部資源載入。B、C 於使用者而言是無用的,我們偷偷吃使用者流量下載無用資源很不厚道。

拆分資源可減小 .html 體積自然提升頁面載入速度,且 App 優先訪問本地 .html 免去遠端請求更是快上加快。

無用資源需丟棄,公共資源也需提取。假定頁面 A、B 均引用資源 C,資源 C 便可單獨提取。可使用 CommonsChunkPlugin 達成對第三方庫,公用元件的抽離。一提取專案所應用 node_module 指令碼示例:

new webpack.optimize.CommonsChunkPlugin({
    name: `vendor`,
    minChunks: function (module) {
        return (
            module.resource &&
            /.js$/.test(module.resource) &&
            module.resource.indexOf(
                path.join(__dirname, `../node_modules`)
            ) === 0
        )
    }
})
複製程式碼

專案中所應用到的 node_module 將統一打包至 vendor.js。公用指令碼也需預置,也需檢測更新,若認為監聽眾多資源較麻煩將指令碼內聯至 .html 也可,但我不提倡這樣做(失去了去冗餘的意義)。預置的公用指令碼拷貝到哪裡?拷貝至手機記憶體空間不夠怎麼破,拷貝至儲存卡被使用者誤刪怎麼破,客戶端同學為此很糾結…emmm

vendor.js 含所有頁面依賴到的 node_module。假定頁面 A 使用了 Swiper 而其它頁面未引用它,vendor.js 中的 Swiper 相關程式碼便應僅打包至頁面 A,如何實現?

  • 生成 vendor.js 時過濾 Swiper 並將其單獨打包,node_modules 仍含 Swiper。

  • 將 Swiper 從 node_modules 移動至其它路徑,引用時使用遷移後的路徑。

引入 Sass 也可一定程度的去除無用程式碼:

使用 @mixin、% 定義的通用樣式未被繼承不會被解析產生相應的 css。

想了解更多的同學請研讀 Sass: Syntactically Awesome Style Sheets

減小依賴模組對 Hash 的影響

由於 App 需監聽眾 .html 變化並實時更新資源,應格外注意 Hash 值的穩定性,為此應堅守程式碼模組化原則。假定全域性引入 app.js、app.css,則不允許新增非全域性性質的程式碼至上述兩個檔案。

假如模組 A 被注入 app.js,它的修改將影響所有 .html 的 Hash 值,未呼叫模組 A 的頁面實際上未做修改卻被動更新 Hash。App 根據 Hash 的變化判斷資源更新則認為所有 .html 更新了,進而重新下載所有 Web 資源。

總之 A 未呼叫 B,B 的修改不要影響 A 的 Hash,模組如何拆分請自行依照此原則把握。

接下來討論 manifest 的注入時機。manifest 包含模組處理邏輯,在 Webpack 編譯及對映應用程式碼時,模組資訊被記錄至 manifest,runtime 則根據 manifest 載入模組。

new webpack.optimize.CommonsChunkPlugin({
    name: `manifest`,
    minChunks: Infinity
})
複製程式碼

任一模組更新均會引發它的細微變化(但可通過 minChunks 控制 manifest 影響範圍),且所有頁面載入依賴 manifest。可怕的現象發生了:manifest 更新所有 .html 的 Hash 更新 -> 所有 .html 被重新下載。我們可先為 .html 打 Hash 再將 manifest 內聯,因為未更新模組呼叫舊 manifest 不會受影響。

開發環境模式儘量簡易

一個專案參與者眾多,開發環境模式複雜將提高學習成本與風險。在簡化開發模式上我做了哪些:

開發環境單入口、生產環境多入口

先講下 Vue 多頁面拆分如何做。相關文章很多在此推薦一篇,點我~

核心思想:

應用於 Hybrid App 的 Vue 多頁面構建
  • 單頁:多 View 對應 單 index.html + 單 entry.js

  • 多頁:多 View 對應 多 index.html + 多 entry.js

假定含 100 個 View 則需對應建立 100 個 index.html、100 個 entry.js!但它們幾乎一模一樣,重複建立十分浪費,開發成本也被增加。

index.html 可被多個 View 複用,entry.js 不可。共享 entry 需在其中 import 全部 View,則 build 生成的每一頁面含每一 View 的全部資源,即 100 個內容一模一樣的 .html。

我們可形式上單入口,實際上多入口,如何做?定義一含佔位符的 entry 模板,build 時將佔位符替換為對應 View 的引入,如此 import 資源將按需拆分。

<%=Page%> 佔位符的 entry.js:

import Vue from `vue`
import Page from `<%=Page%>`
/* eslint-disable no-new */
new Vue({
    el: `#app`,
    template: `<Page />`,
    components: {
        Page
    }
})
複製程式碼

生成多 entry 的 gulp task:

gulp.task(`entries`, () => {
    var flag = true
    for (let key in routes) {
        // 檢查 entry 是否已存在
        gulp.src(`./entry/entries/${routes[key].view}.js`)
            .on(`data`, () => {
                // 已存在 entry 不重複構造
                flag = false
            })
            .on(`end`, () => {
                if (flag) {
                    console.log(`new entry: `, `/entries/${routes[key].view}.js`)
                    // 構造新 entry
                    gulp.src(`./entry/entry.js`)
                        .pipe(replace({
                            patterns: [
                                {
                                    match: /<%=Page%>/g,
                                    replacement: `../../src/views/${routes[key].path}${routes[key].view}`
                                }
                            ]
                        }))
                        .pipe(rename(`entries/${routes[key].view}.js`))
                        .pipe(gulp.dest(`./entry/`))
                }
                flag = true
            })
    }
})
複製程式碼

僅生產環境執行 gulp entries 構造多入口,開發環境單入口即可,免去研發同學構造 entry 的成本。

function entries () {
    var entries = {}
    for (let key in routes) {
        entries[routes[key].view] = process.env.NODE_ENV === `production`
            ? `./entry/entries/${routes[key].view}.js`
            : `./entry/dev.js`
    }
    return entries
}
複製程式碼
開發環境引用本地圖片、生產環境引用 CDN 圖片

由於 App 僅監聽 .html 變化,圖片資源需從遠端引用。研發自行上傳圖片至 CDN 似乎並不複雜,但我司 CDN 上傳許可權氾濫是不被允許的。

圖片上傳交專人負責,方法原始溝通成本高,等待他人上傳也影響自身開發效率。

開發階段將圖片上傳測試 CDN,生產階段再統一拷貝至線上環境?轉化成本不小,遺漏上傳還會引發線上事故。

開發階段書寫相對路徑引用本地資源,免去研發自行上傳圖片的煩惱且模式與傳統 Web 開發保持一致。生產環境直接轉化圖片連結為 CDN 路徑。並將所有 image 單獨打包至 assets/ 一同上傳 CDN,此時 .html 對 CDN 圖片的引用生效了。

{
    test: /.(png|jpe?g|gif|svg)(?.*)?$/,
    loader: `url-loader`,
    options: {
        limit: 1,
        name: `assets/imgs/[name]-[hash:10].[ext]`
    }
}
複製程式碼

為防止 CDN 快取導致圖片無法及時更新,build 後圖片名稱新增 Hash 字尾。在此我設定 Base64 轉化 limit 為 1,防止 HTML 穿插過多 Base64 格式圖片阻塞載入。

生產環境圖片連結轉化 CDN 路徑程式碼如下:

const settings = require(`../settings`)
module.exports = {
    dev: {
        // code...
    },
    build: {
        assetsRoot: path.resolve(__dirname, `../../dist`),
        assetsSubDirectory: `static`,
        assetsPublicPath: `${settings.cdn}/`,
        // code...
    }
}
複製程式碼

工具一覽

html-webpack-inline-source-plugingulp-inline-source:JS、CSS 資源內聯工具。

commons-chunk-plugin:公共模組拆分工具。

gulp-revhashed-module-ids-plugin:MD5 簽名生成工具。

gulp-zip:壓縮工具。

其它常用 Gulp 工具:gulp-renamegulp-replace-taskdel


踩坑札記

路由解析問題

假定路由配置為:

{
    "/demo": {
        "view": "Demo",
        "path": "demo/",
        "query": [
            "topic_id",
            "service_id"
        ]
    },
    "/album": {
        "view": "Album",
        "path": "demo/"
    }
}
複製程式碼

生成 routes.json 為:

{
    "items": [
        {
            "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Demo-2392a800be.html",
            "uri": "https://backend.igengmei.com/demo[/]?.*"
        },
        {
            "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Album-1564b12a1c.html",
            "uri": "https://backend.igengmei.com/album[/]?.*"
        }
    ],
    "deploy_time": "Mon Mar 19 2018 19:41:22 GMT+0800 (CST)"
}
複製程式碼

開發環境通過 localhost:8080/demo?topic_id=&service_id= 訪問 Demo 頁面,形如 vue-router 為我們構建的路由。而生產環境訪問路徑為 file:////dist/demo/Demo-2392a800be.html?uri=https%3A%2F%2Fbackend.igengmei.com%2Fdemo%3Ftopic_id%3D%26service_id%3D,獲取引數需解析 uri。

因兩大環境引數解析方式不同,需自行封裝 $router,例如 this.$router.query 的定義:

const App = {
    $router: {
        query: (key) => {
            var search = window.location.search
            var value = ``
            var tmp = []
            if (search) {
                // 生產環境解析 uri
                tmp = (process.env.NODE_ENV === `production`)
                    ? decodeURIComponent(search.split(`uri=`)[1]).split(`?`)[1].split(`&`)
                    : search.slice(1).split(`&`)
            }
            for (let i in tmp) {
                if (key === tmp[i].split(`=`)[0]) {
                    value = tmp[i].split(`=`)[1]
                    break
                }
            }
            return value
        }
    }
}
複製程式碼

可將 $router 繫結至 Vue.prototype:

App.install = (Vue, options) => {
    Vue.prototype.$router = App.$router
}
export default App
複製程式碼

在 entry.js 執行:

Vue.use(App)
複製程式碼

此時任一 .vue 可直接呼叫 this.$router,無需 import。呼叫頻率較高的 method 均可 bind 至 Vue.prototype,例如對請求的封裝 this.$request。

缺陷:自制 router 僅支援 query 引數不支援 param 引數。

Cookie 同步問題

App 載入本地預置資源在 file:/// 域,無法直接將 Cookie 載入 Webview,對 file:/// 開放 Cookie 將導致安全問題。幾種解決思路:

  • 區分 file:/// 來源,判定來源安全則載入 Cookie,但 H5 依然無法將 Cookie 帶到請求中。

  • 偽造類似 http 請求形成假域。

  • Native 維護 Cookie 並提供獲取介面,H5 拼接 Cookie 自行寫入 Request Header。

  • Native 代發請求回傳返回值,但無法實現大資料量 POST 請求(例 POST File)。

通常在頁面 render 時伺服器會將 CSRFToken 寫入 Cookie,Request 時再將 CSRFToken 傳回伺服器防止跨域攻擊。但載入本地 HTML 缺少上述步驟,需額外注意 CSRFToken 的獲取問題。

未完待續~


作者:呆戀小喵

我的後花園:sunmengyuan.github.io/garden/

我的 github:github.com/sunmengyuan

原文連結:sunmengyuan.github.io/garden/2018…

應用於 Hybrid App 的 Vue 多頁面構建

相關文章