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

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

爲什麼選擇混合開發?

  • 高效率界面開發:HTML + CSS + JavaScript 被證明具有極高的界面開發效率。
  • 跨平臺:較統一的瀏覽器內核標準,使 H5 頁面在 IOS、Android 共享同套代碼。使用 Native 開發一功能需 IOS、Android 研發各一枚,而使用 H5 一枚前端工程師足矣。但混合 App 並不是 Native 越少越佳,性能要求較高的仍需勞 Native 大駕...分工需明確,不可厚此薄彼。
  • 熱更新:不依賴於發佈渠道自主更新應用。Native 修復線上 Bug 需發佈新版本,用戶未升級 App 該 Bug 將一直呈現。而修復 H5 只需將 Fixbug 的代碼推至服務器,任一版本 App 即可同步更新對應功能無需升級。

爲什麼離線緩存 Web 資源?

相比於從遠程服務器請求加載 Web 資源,App 優先加載本地預置資源,可提高頁面響應速度,節省用戶流量。html

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

對我司前端團隊的意義

  • 技術棧由 Jinja + jQuery + Require + Gulp 遷移至 Vue + Webpack + Gulp + Sass,擁抱 Vue!

  • 實現先後端分離:原 Jinja 爲 Python 模板引擎,前端代碼的運做依賴於服務端,服務端異常等待環境維修嚴重影響前端工做進度。分離後,服務器掛了咱們愉快的開啓 Mock Server 繼續搬磚即是。
  • App 優先加載本地預置 Web 資源,可提高 H5 頁面加載速度。

弊端

  • 技術重構自己具有風險性。
  • 增長團隊學習成本。
  • 前端框架經過 JS 渲染 HTML 對 SEO 不友好。但你可選擇使用 Vue 2.2 的服務端渲染(SSR)。增添 Node 層除實現 SSR,能作的事還不少...

進入正題~vue

混合開發框架運做機制

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

App 內預置 dist/ 下所有資源(發版時僅下載 dist.zip,安裝 App 時解壓),在攔截並解析 URL 後,經過 routes.json 查找並加載本地 .html 頁面。webpack

routes.json 以下:git

{
    "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)"
}

欠你一個回答~github

請注意「優先加載本地預置資源」,但檢測到更新時加載遠程最新資源,如何檢測更新我稍後闡明。

檢測 .html 文件更新的橋樑即是 routes.json。每啓動 App 從 CDN 靜默更新 routes.json 一次(CDN 緩存會致使 routes.json 沒法及時更新,下載路由表請添加時間戳參數強制更新),任一資源更新均同步至 routes.json 並上傳 CDN。web

標記更新的方式則是爲 .html 打 Hash(MD5)戳,於 App 而言不一樣 Hash 後綴的 .html 爲不一樣文件。App 根據路由表 remote_file 查尋本地 .html,若該 .html 不存在則直接加載遠程資源同時靜默下載更新。vue-router

注:因爲 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 多頁面拆分如何作。相關文章不少在此推薦一篇,點我~

核心思想:

  • 單頁:多 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 的獲取問題。

未完待續~


做者:呆戀小喵

個人後花園:https://sunmengyuan.github.io...

個人 github:https://github.com/sunmengyuan

原文連接:https://sunmengyuan.github.io...

相關文章
相關標籤/搜索