應(yīng)用于 Hybrid App 的 Vue 多頁面構(gòu)建

本文介紹一款基于 Vue 的使 App 支持離線緩存 Web 資源的混合開發(fā)框架。本人小白一枚,請將它視作一份我的學(xué)習(xí)總結(jié),歡迎大神們賜教。本文多闡述思路,實現(xiàn)細(xì)節(jié)請閱讀源碼。

為何選擇混合開發(fā)?

  • 高效率界面開發(fā):HTML + CSS + JavaScript 被證實具備極高的界面開發(fā)效率。

  • 跨平臺:較統(tǒng)一的瀏覽器內(nèi)核標(biāo)準(zhǔn),使 H5 頁面在 IOS、Android 共享同套代碼。使用 Native 開發(fā)一功能需 IOS、Android 研發(fā)各一枚,而使用 H5 一枚前端工程師足矣。但混合 App 并非 Native 越少越佳,性能要求較高的仍需勞 Native 大駕...分工需明確,不可厚此薄彼。

  • 熱更新:不依賴于發(fā)布渠道自主更新應(yīng)用。Native 修復(fù)線上 Bug 需發(fā)布新版本,用戶未升級 App 該 Bug 將一直呈現(xiàn)。而修復(fù) H5 只需將 Fixbug 的代碼推至服務(wù)器,任一版本 App 便可同步更新對應(yīng)功能無需升級。

為何離線緩存 Web 資源?

相比于從遠(yuǎn)程服務(wù)器請求加載 Web 資源,App 優(yōu)先加載本地預(yù)置資源,可提升頁面響應(yīng)速度,節(jié)省用戶流量。

問題來了...本地預(yù)置的 Web 資源也隨 App 安裝包一起成為潑出去的水,修復(fù) H5 線上 Bug 也需發(fā)版了?丟西瓜撿芝麻的事定不可做!請注意“優(yōu)先加載本地預(yù)置資源”,但檢測到更新時加載遠(yuǎn)程最新資源,如何檢測更新我稍后闡明。

對我司前端團(tuán)隊的意義

  • 技術(shù)棧由 Jinja + jQuery + Require + Gulp 遷移至 Vue + Webpack + Gulp + Sass,擁抱 Vue!
  • 實現(xiàn)前后端分離:原 Jinja 為 Python 模板引擎,前端代碼的運作依賴于服務(wù)端,服務(wù)端異常等待環(huán)境維修嚴(yán)重影響前端工作進(jìn)度。分離后,服務(wù)器掛了我們愉快的開啟 Mock Server 繼續(xù)搬磚便是。

  • App 優(yōu)先加載本地預(yù)置 Web 資源,可提升 H5 頁面加載速度。

弊端

  • 技術(shù)重構(gòu)本身具備風(fēng)險性。

  • 增加團(tuán)隊學(xué)習(xí)成本。

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


進(jìn)入正題~

混合開發(fā)框架運作機制

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

App 內(nèi)預(yù)置 dist/ 下全部資源(發(fā)版時僅下載 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)"
}

欠你一個回答~

請注意“優(yōu)先加載本地預(yù)置資源”,但檢測到更新時加載遠(yuǎn)程最新資源,如何檢測更新我稍后闡明。

檢測 .html 文件更新的橋梁便是 routes.json。每啟動 App 從 CDN 靜默更新 routes.json 一次(CDN 緩存會導(dǎo)致 routes.json 無法及時更新,下載路由表請?zhí)砑訒r間戳參數(shù)強制更新),任一資源更新均同步至 routes.json 并上傳 CDN。

標(biāo)記更新的方式則是為 .html 打 Hash(MD5)戳,于 App 而言不同 Hash 后綴的 .html 為不同文件。App 根據(jù)路由表 remote_file 查尋本地 .html,若該 .html 不存在則直接加載遠(yuǎn)程資源同時靜默下載更新。

注:由于 js、css 腳本均被內(nèi)聯(lián)至對應(yīng) .html,App 僅需監(jiān)聽 .html 文件的變化。其實我們可以提取公用腳本并為之打 Hash 戳,將該資源的變化記錄至一張表供 App 監(jiān)聽。常年不更新的公用腳本,緩存在 App 內(nèi)不隨 .html 一同加載也可提升頁面響應(yīng)速度。

綜上,Web 資源雖被預(yù)置于 App,但其 Fixbug 級別的更新不必走發(fā)版這條路。

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


Web 框架設(shè)計

Web 框架設(shè)計圍繞:

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

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

  • 開發(fā)環(huán)境模式盡量簡易

減少無用資源及冗余資源

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

假定我站含頁面 A、B、C,用戶僅訪問 A 但單頁應(yīng)用卻將 A、B、C 所依賴的全部資源加載。B、C 于用戶而言是無用的,我們偷偷吃用戶流量下載無用資源很不厚道。

拆分資源可減小 .html 體積自然提升頁面加載速度,且 App 優(yōu)先訪問本地 .html 免去遠(yuǎn)程請求更是快上加快。

無用資源需丟棄,公共資源也需提取。假定頁面 A、B 均引用資源 C,資源 C 便可單獨提取。可使用 CommonsChunkPlugin 達(dá)成對第三方庫,公用組件的抽離。一提取項目所應(yīng)用 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
        )
    }
})

項目中所應(yīng)用到的 node_module 將統(tǒng)一打包至 vendor.js。公用腳本也需預(yù)置,也需檢測更新,若認(rèn)為監(jiān)聽眾多資源較麻煩將腳本內(nèi)聯(lián)至 .html 也可,但我不提倡這樣做(失去了去冗余的意義)。預(yù)置的公用腳本拷貝到哪里?拷貝至手機內(nèi)存空間不夠怎么破,拷貝至存儲卡被用戶誤刪怎么破,客戶端同學(xué)為此很糾結(jié)...emmm

vendor.js 含所有頁面依賴到的 node_module。假定頁面 A 使用了 Swiper 而其它頁面未引用它,vendor.js 中的 Swiper 相關(guān)代碼便應(yīng)僅打包至頁面 A,如何實現(xiàn)?

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

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

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

使用 @mixin、% 定義的通用樣式未被繼承不會被解析產(chǎn)生相應(yīng)的 css。

想了解更多的同學(xué)請研讀 Sass: Syntactically Awesome Style Sheets。

減小依賴模塊對 Hash 的影響

由于 App 需監(jiān)聽眾 .html 變化并實時更新資源,應(yīng)格外注意 Hash 值的穩(wěn)定性,為此應(yīng)堅守代碼模塊化原則。假定全局引入 app.js、app.css,則不允許添加非全局性質(zhì)的代碼至上述兩個文件。

假如模塊 A 被注入 app.js,它的修改將影響所有 .html 的 Hash 值,未調(diào)用模塊 A 的頁面實際上未做修改卻被動更新 Hash。App 根據(jù) Hash 的變化判斷資源更新則認(rèn)為所有 .html 更新了,進(jìn)而重新下載所有 Web 資源。

總之 A 未調(diào)用 B,B 的修改不要影響 A 的 Hash,模塊如何拆分請自行依照此原則把握。

接下來討論 manifest 的注入時機。manifest 包含模塊處理邏輯,在 Webpack 編譯及映射應(yīng)用代碼時,模塊信息被記錄至 manifest,runtime 則根據(jù) manifest 加載模塊。

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

任一模塊更新均會引發(fā)它的細(xì)微變化(但可通過 minChunks 控制 manifest 影響范圍),且所有頁面加載依賴 manifest。可怕的現(xiàn)象發(fā)生了:manifest 更新所有 .html 的 Hash 更新 -> 所有 .html 被重新下載。我們可先為 .html 打 Hash 再將 manifest 內(nèi)聯(lián),因為未更新模塊調(diào)用舊 manifest 不會受影響。

開發(fā)環(huán)境模式盡量簡易

一個項目參與者眾多,開發(fā)環(huán)境模式復(fù)雜將提高學(xué)習(xí)成本與風(fēng)險。在簡化開發(fā)模式上我做了哪些:

開發(fā)環(huán)境單入口、生產(chǎn)環(huán)境多入口

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

核心思想:

  • 單頁:多 View 對應(yīng) 單 index.html + 單 entry.js。

  • 多頁:多 View 對應(yīng) 多 index.html + 多 entry.js

假定含 100 個 View 則需對應(yīng)創(chuàng)建 100 個 index.html、100 個 entry.js!但它們幾乎一模一樣,重復(fù)創(chuàng)建十分浪費,開發(fā)成本也被增加。

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

我們可形式上單入口,實際上多入口,如何做?定義一含占位符的 entry 模板,build 時將占位符替換為對應(yīng) 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 不重復(fù)構(gòu)造
                flag = false
            })
            .on('end', () => {
                if (flag) {
                    console.log('new entry: ', `/entries/${routes[key].view}.js`)
                    // 構(gòu)造新 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
            })
    }
})

僅生產(chǎn)環(huán)境執(zhí)行 gulp entries 構(gòu)造多入口,開發(fā)環(huán)境單入口即可,免去研發(fā)同學(xué)構(gòu)造 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
}
開發(fā)環(huán)境引用本地圖片、生產(chǎn)環(huán)境引用 CDN 圖片

由于 App 僅監(jiān)聽 .html 變化,圖片資源需從遠(yuǎn)程引用。研發(fā)自行上傳圖片至 CDN 似乎并不復(fù)雜,但我司 CDN 上傳權(quán)限泛濫是不被允許的。

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

開發(fā)階段將圖片上傳測試 CDN,生產(chǎn)階段再統(tǒng)一拷貝至線上環(huán)境?轉(zhuǎn)化成本不小,遺漏上傳還會引發(fā)線上事故。

開發(fā)階段書寫相對路徑引用本地資源,免去研發(fā)自行上傳圖片的煩惱且模式與傳統(tǒng) Web 開發(fā)保持一致。生產(chǎn)環(huán)境直接轉(zhuǎn)化圖片鏈接為 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 緩存導(dǎo)致圖片無法及時更新,build 后圖片名稱添加 Hash 后綴。在此我設(shè)置 Base64 轉(zhuǎn)化 limit 為 1,防止 HTML 穿插過多 Base64 格式圖片阻塞加載。

生產(chǎn)環(huán)境圖片鏈接轉(zhuǎn)化 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-plugin、gulp-inline-source:JS、CSS 資源內(nèi)聯(lián)工具。

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

gulp-rev、hashed-module-ids-plugin:MD5 簽名生成工具。

gulp-zip:壓縮工具。

其它常用 Gulp 工具:gulp-rename、gulp-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)"
}

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

因兩大環(huán)境參數(shù)解析方式不同,需自行封裝 $router,例如 this.$router.query 的定義:

const App = {
    $router: {
        query: (key) => {
            var search = window.location.search
            var value = ''
            var tmp = []
            if (search) {
                // 生產(chǎn)環(huán)境解析 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 執(zhí)行:

Vue.use(App)

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

缺陷:自制 router 僅支持 query 參數(shù)不支持 param 參數(shù)。

Cookie 同步問題

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

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

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

  • Native 維護(hù) Cookie 并提供獲取接口,H5 拼接 Cookie 自行寫入 Request Header。

  • Native 代發(fā)請求回傳返回值,但無法實現(xiàn)大數(shù)據(jù)量 POST 請求(例 POST File)。

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

未完待續(xù)~


作者:呆戀小喵

我的后花園:https://sunmengyuan.github.io/garden/

我的 github:https://github.com/sunmengyuan

原文鏈接:https://sunmengyuan.github.io/garden/2018/03/05/ballade.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在現(xiàn)在的前端開發(fā)中,前后端分離、模塊化開發(fā)、版本控制、文件合并與壓縮、mock數(shù)據(jù)等等一些原本后端的思想開始...
    Charlot閱讀 5,690評論 1 32
  • 當(dāng)你處于單身時期,其實也是自我增值的最好時期,也是磨練自己心靈最好的時期。單身不必要感覺到慌張,要學(xué)會享受其中, ...
    P尐c閱讀 338評論 0 0
  • 抱著試試看的心態(tài),接受的了司儀的建議,從來都不知道什么叫婚禮直播,在我的印象中是不是和花椒那種又唱又跳...
    司儀工會閱讀 721評論 5 3
  • 有一段時間,當(dāng)我聽到“只要你過得比我好”時,心都是酸酸的??v使這樣對感情失望的我,依然希望別人過得比我好,這也是我...
    邊緣人羅凌閱讀 279評論 0 0
  • 1. AndroidManifest.xml 在 application 標(biāo)簽下配置,與 activity 標(biāo)簽同...
    plusend閱讀 950評論 0 0

友情鏈接更多精彩內(nèi)容