前言
H5 基礎(chǔ)腳手架:極速構(gòu)建項(xiàng)目
上一篇講到了快速構(gòu)建項(xiàng)目的通用 webpack 構(gòu)建鞭达,此篇將結(jié)合業(yè)務(wù)修改 H5 的腳手架
小聲 BB司忱,不是一定適合你的項(xiàng)目皇忿,具體項(xiàng)目具體對(duì)待,符合自身業(yè)務(wù)的才是最好的
資源添加版本號(hào)
看過之前博客的同學(xué)坦仍,應(yīng)該知道在創(chuàng)建版本的時(shí)候引入了版本號(hào)的概念鳍烁,在創(chuàng)建分支版本的時(shí)候,帶上版本號(hào)繁扎,創(chuàng)建的分支名為 feat/0.0.01幔荒,而我們發(fā)布的靜態(tài)資源也是帶了版本
之前有讓同學(xué)關(guān)注過 url 上面的版本號(hào)哈
改造 Webpack 路徑
const branch = childProcess.execSync('git rev-parse --abbrev-ref HEAD').toString().replace(/\s+/, '')
const version = branch.split('/')[1] // 獲取分支版本號(hào)
output: {
publicPath: `./${version}`,
}
如上,我們先將分支版本號(hào)獲取梳玫,再修改資源引用路徑爹梁,即可完成資源版本的處理,如下圖所示提澎,h5 鏈接被修改成常規(guī) url姚垃,引用資源帶上了版本號(hào)
版本號(hào)的優(yōu)勢(shì)
- 可以快速定位 code 版本,針對(duì)性的修復(fù)
- 每個(gè)版本資源保存上在 cdn 上盼忌,快速回滾只需要刷新 html积糯,不必重新構(gòu)建發(fā)布
Webpack Plugin 開發(fā)
直接添加版本的方法是不是蠢出天際,so 我們隨便寫個(gè)插件玩玩好了
const HtmlWebpackPlugin = require('html-webpack-plugin');
const childProcess = require('child_process')
const branch = childProcess.execSync('git rev-parse --abbrev-ref HEAD').toString().replace(/\s+/, '')
const version = branch.split('/')[1]
class HotLoad {
apply(compiler) {
compiler.hooks.beforeRun.tap('UpdateVersion', (compilation) => {
compilation.options.output.publicPath = `./${version}/`
})
}
}
module.exports = HotLoad;
module.exports = {
plugins: [
new HotLoad()
]}
如上谦纱,我們創(chuàng)建一個(gè) webpack plugin看成,通過監(jiān)聽 webpack hooks 在任務(wù)執(zhí)行之前修改對(duì)應(yīng)的資源路徑,通用性上升跨嘉。
高級(jí)定制化
CDN 資源引入
此外之前的博客我們還引入了 cdn 的概念川慌,我們可以將上述插件升級(jí),構(gòu)建的時(shí)候引入通用的 cdn 資源祠乃,減少構(gòu)建與加載時(shí)間窘游。
const scripts = [
'https://cdn.bootcss.com/react-dom/16.9.0-rc.0/umd/react-dom.production.min.js',
'https://cdn.bootcss.com/react/16.9.0/umd/react.production.min.js'
]
class HotLoad {
apply(compiler) {
compiler.hooks.beforeRun.tap('UpdateVersion', (compilation) => {
compilation.options.output.publicPath = `./${version}/`
})
compiler.hooks.compilation.tap('HotLoadPlugin', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('HotLoadPlugin', (data, cb) => {
scripts.forEach(src => [
data.assetTags.scripts.unshift({
tagName: 'script',
voidTag: false,
attributes: { src }
})
])
cb(null, data)
})
})
}
}
上述我們借助了 HtmlWebpackPlugin 提供的 alterAssetTags hooks,主動(dòng)添加了 react 相關(guān)的第三方 cdn 鏈接跳纳,這樣在生產(chǎn)環(huán)境中忍饰,同域名下面的項(xiàng)目,可以復(fù)用資源寺庄。
通過緩存解決加載 js
對(duì)于長(zhǎng)期不會(huì)改變的靜態(tài)資源艾蓝,可以直接將資源緩存在本地,下次項(xiàng)目打開的時(shí)候可以直接從本地加載資源斗塘,提高二次開啟效率赢织。
首先,我們選擇 indexDB 來進(jìn)行緩存馍盟,因?yàn)?indexDB 較 stroage 來說于置,容量會(huì)更大,我們本身就需要緩存比較大的靜態(tài)資源所以需要更大容量的 indexDB 來支持
import scripts from './script.json';
import styles from './css.json';
import xhr from './utils/xhr'
import load from './utils/load'
import storage from './stroage/indexedDb'
const _storage = new storage()
const _load = new load()
const _xhr = new xhr()
class hotLoad {
constructor(props) {
this.loadScript(props)
this.issafariBrowser = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
}
async loadScript(props = []) {
const status = await _storage.init()
let _scripts = scripts
const expandScripts = props
if (status) {
for (let script of _scripts) {
const { version, type = 'js', name, url } = script
if (this.issafariBrowser) {
await _load.loadJs({ url })
} else {
const value = await _storage.getCode({ name, version, type });
if (!value) {
const scriptCode = await _xhr.getCode(url || `${host}/${name}/${version}.js`)
if (scriptCode) {
await _load.loadJs({ code: scriptCode })
await _storage.setCode({ scriptName: `${name}_${version}_${type}`, scriptCode: scriptCode })
}
} else {
await _load.loadJs({ code: value })
}
}
}
for (let style of styles) {
const { url, name, version, type = 'css' } = style
if (this.issafariBrowser) {
await _load.loadCSS({ url })
} else {
const value = await _storage.getCode({ name, version, type })
if (!value) {
const cssCode = await _xhr.getCode(url || `${host}/${name}/${version}.css`)
_storage.setCode({ scriptName: `${name}_${version}_${type}`, scriptCode: cssCode })
_load.loadCSS({ code: cssCode })
} else {
_load.loadCSS({ code: value })
}
}
}
} else {
for (let script of _scripts) {
const { version, name } = script
const scriptCode = await _xhr.getCode(script.url || `${host}/${name}/${version}.js`)
if (scriptCode) {
await _load.loadJs({ code: scriptCode })
}
}
for (let style of styles) {
const { url, name, version } = style
const cssCode = await _xhr.getCode(url || `${host}/${name}/${version}.css`)
_load.loadCSS({ code: cssCode })
}
}
for (let script of expandScripts) {
const { url } = script
await _load.loadJs({ url })
}
}
}
window.hotLoad = hotLoad
上述代碼是將第三方資源贞岭,通過 xhr 獲取之后八毯,使用 Blob + URL.createObjectURL 制造本地鏈接搓侄,使用 js 動(dòng)態(tài)添加到頁(yè)面中去。
class load {
constructor() { }
// 加載js
loadJs({ url, code, callback }) {
let oHead = document
.getElementsByTagName('HEAD')
.item(0);
let script = document.createElement('script');
script.type = "text/javascript";
return new Promise(resolve => {
if (url) {
script.src = url
} else {
let blob = new Blob([code], { type: "application/javascript; charset=utf-8" });
script.src = URL.createObjectURL(blob);
}
oHead.appendChild(script)
if (script.readyState) {
script.onreadystatechange = () => {
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
callback && callback();
resolve(true)
}
}
} else {
script.onload = () => {
callback && callback();
resolve(true)
}
}
})
}
// 加載css
loadCSS({ url, code }) {
let oHead = document
.getElementsByTagName('HEAD')
.item(0);
let cssLink = document.createElement("link");
cssLink.rel = "stylesheet"
return new Promise(resolve => {
if (url) {
cssLink.href = url
} else {
let blob = new Blob([code], { type: "text/css; charset=utf-8" });
cssLink.type = "text/css";
cssLink.rel = "stylesheet";
cssLink.rev = "stylesheet";
cssLink.media = "screen";
cssLink.href = URL.createObjectURL(blob);
}
oHead.appendChild(cssLink);
resolve(true)
})
}
}
// 通過 xhr 拉取靜態(tài)資源
class xhr {
constructor() {
this.xhr;
if (window.XMLHttpRequest) {
this.xhr = new XMLHttpRequest();
} else {
this.xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
}
// 同步請(qǐng)求js
getCode(url) {
return new Promise(resolve => {
this.xhr.open('get', url, true);
this.xhr.send(null);
this.xhr.onreadystatechange = () => {
if (this.xhr.readyState == 4) {
if (this.xhr.status >= 200 && this.xhr.status < 300 || this.xhr.status == 304) {
resolve(this.xhr.responseText)
}
}
}
})
}
}
弱網(wǎng)環(huán)境下的直接加載
弱網(wǎng)環(huán)境下的緩存加載
對(duì)比上圖话速,可以明顯看出讶踪,在網(wǎng)絡(luò)環(huán)境波動(dòng)的情況下,有緩存加持的網(wǎng)頁(yè)二次開啟的速度會(huì)明顯提效泊交,當(dāng)然在性能上乳讥,由于需要判斷第三方靜態(tài)資源版本以及從本地讀取資源,會(huì)消耗部分時(shí)間廓俭,可以針對(duì)業(yè)務(wù)自行取舍
優(yōu)劣勢(shì)對(duì)比
優(yōu)勢(shì)
- 統(tǒng)一接管項(xiàng)目的依賴云石,可以針對(duì)性的升級(jí)通用資源
- 資源有版本依賴概念,緩存在本地的時(shí)候研乒,可以快速切換版本
- 二次加載速度會(huì)上升
- 配合 Service Worker 有奇效
劣勢(shì)
- 統(tǒng)一升級(jí)的過程留晚,可能有引用項(xiàng)目存在不匹配造成程序崩潰的情況
- 其實(shí)強(qiáng)緩存所有共用靜態(tài) cdn 資源也是 ok 的,干嘛那么費(fèi)勁呢
上述的插件有沒有同學(xué)想要用的告嘲,需要的留言错维,我放到 github 上去
全系列博文目錄
后端模塊
- DevOps - Gitlab Api使用(已完成,點(diǎn)擊跳轉(zhuǎn))
- DevOps - 搭建 DevOps 基礎(chǔ)平臺(tái) 基礎(chǔ)平臺(tái)搭建上篇 | 基礎(chǔ)平臺(tái)搭建中篇 | 基礎(chǔ)平臺(tái)搭建下篇
- DevOps - Gitlab CI 流水線構(gòu)建
- DevOps - Jenkins 流水線構(gòu)建
- DevOps - Docker 使用
- DevOps - 發(fā)布任務(wù)流程設(shè)計(jì)
- DevOps - 代碼審查卡點(diǎn)
- DevOps - Node 服務(wù)質(zhì)量監(jiān)控
前端模塊
- DevOps - H5 基礎(chǔ)腳手架
- DevOps - React 項(xiàng)目開發(fā)
尾聲
此項(xiàng)目是從零開發(fā)橄唬,后續(xù)此系列博客會(huì)根據(jù)實(shí)際開發(fā)進(jìn)度推出(真 TMD 累)赋焕,項(xiàng)目完成之后,會(huì)開放部分源碼供各位同學(xué)參考仰楚。
為什么是開放部分源碼隆判,因?yàn)橛行I(yè)務(wù)是需要貼合實(shí)際項(xiàng)目針對(duì)性開發(fā)的,開放出去的公共模塊我寫的認(rèn)真點(diǎn)
為了寫個(gè)系列博客僧界,居然真擼完整個(gè)系統(tǒng)(不是一般的累)侨嘀,覺得不錯(cuò)的同學(xué)麻煩順手三連(點(diǎn)贊,關(guān)注捂襟,轉(zhuǎn)發(fā))咬腕。