本文介紹一款基於 Vue 的使 App 支持離線緩存 Web 資源的混合開發框架。本人小白一枚,請將它視做一份個人學習總結,歡迎大神們賜教。本文多闡述思路,實現細節請閱讀源碼。css
源碼html
高效率界面開發:HTML + CSS + JavaScript 被證明具有極高的界面開發效率。前端
跨平臺:較統一的瀏覽器內核標準,使 H5 頁面在 IOS、Android 共享同套代碼。使用 Native 開發一功能需 IOS、Android 研發各一枚,而使用 H5 一枚前端工程師足矣。但混合 App 並不是 Native 越少越佳,性能要求較高的仍需勞 Native 大駕...分工需明確,不可厚此薄彼。vue
熱更新:不依賴於發佈渠道自主更新應用。Native 修復線上 Bug 需發佈新版本,用戶未升級 App 該 Bug 將一直呈現。而修復 H5 只需將 Fixbug 的代碼推至服務器,任一版本 App 即可同步更新對應功能無需升級。node
相比於從遠程服務器請求加載 Web 資源,App 優先加載本地預置資源,可提高頁面響應速度,節省用戶流量。webpack
問題來了...本地預置的 Web 資源也隨 App 安裝包一塊兒成爲潑出去的水,修復 H5 線上 Bug 也需發版了?丟西瓜撿芝麻的事定不可作!請注意「優先加載本地預置資源」,但檢測到更新時加載遠程最新資源,如何檢測更新我稍後闡明。git
實現先後端分離:原 Jinja 爲 Python 模板引擎,前端代碼的運做依賴於服務端,服務端異常等待環境維修嚴重影響前端工做進度。分離後,服務器掛了咱們愉快的開啓 Mock Server 繼續搬磚即是。github
App 優先加載本地預置 Web 資源,可提高 H5 頁面加載速度。web
技術重構自己具有風險性。vue-router
增長團隊學習成本。
前端框架經過 JS 渲染 HTML 對 SEO 不友好。但你可選擇使用 Vue 2.2 的服務端渲染(SSR)。增添 Node 層除實現 SSR,能作的事還不少...
進入正題~
將 Web 資源文件打包至 dist/(含 routes.json 及 N 多 .html)並壓縮爲 dist.zip,圖片資源單獨打包至 assets/,一同上傳至 CDN。
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 框架設計圍繞:
減小無用資源及冗餘資源
減少依賴模塊對 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。
因爲 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
}
複製代碼
因爲 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-plugin、gulp-inline-source:JS、CSS 資源內聯工具。
commons-chunk-plugin:公共模塊拆分工具。
gulp-rev、hashed-module-ids-plugin:MD5 簽名生成工具。
gulp-zip:壓縮工具。
其它經常使用 Gulp 工具:gulp-rename、gulp-replace-task、del
假定路由配置爲:
{
"/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 參數。
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…