該內(nèi)容由銀科控股融匯研發(fā)部曹俊及其團(tuán)隊(duì)授權(quán)提供。該團(tuán)隊(duì)擁有 10 多名小程序開發(fā),深耕小程序領(lǐng)域,總結(jié)出了本篇優(yōu)質(zhì)長文。同時本篇內(nèi)容也已經(jīng)合并入我的 開源項(xiàng)目 中,目前項(xiàng)目內(nèi)容包含了 JS、網(wǎng)絡(luò)、瀏覽器相關(guān)、性能優(yōu)化、安全、框架、Git、數(shù)據(jù)結(jié)構(gòu)、算法等內(nèi)容,無論是基礎(chǔ)還是進(jìn)階,亦或是源碼解讀,你都能在本圖譜中得到滿意的答案,希望這個面試圖譜能夠幫助到大家更好的準(zhǔn)備面試。
了解小程序登陸之前,我們寫了解下小程序/公眾號登錄涉及到兩個最關(guān)鍵的用戶標(biāo)識:
OpenId UnionId
wx.login 官方提供的登錄能力
wx.checkSession 校驗(yàn)用戶當(dāng)前的session_key是否有效
wx.authorize 提前向用戶發(fā)起授權(quán)請求
wx.getUserInfo 獲取用戶基本信息
以下從筆者接觸過的幾種登錄流程來做闡述:
直接復(fù)用現(xiàn)有系統(tǒng)的登錄體系,只需要在小程序端設(shè)計(jì)用戶名,密碼/驗(yàn)證碼輸入頁面,便可以簡便的實(shí)現(xiàn)登錄,只需要保持良好的用戶體驗(yàn)即可。
:point_up_2:提過, OpenId 是一個小程序?qū)τ谝粋€用戶的標(biāo)識,利用這一點(diǎn)我們可以輕松的實(shí)現(xiàn)一套基于小程序的用戶體系,值得一提的是這種用戶體系對用戶的打擾最低,可以實(shí)現(xiàn)靜默登錄。具體步驟如下:
小程序客戶端通過 wx.login 獲取 code
傳遞 code 向服務(wù)端,服務(wù)端拿到 code 調(diào)用微信登錄憑證校驗(yàn)接口,微信服務(wù)器返回 openid和會話密鑰 session_key ,此時開發(fā)者服務(wù)端便可以利用 openid 生成用戶入庫,再向小程序客戶端返回自定義登錄態(tài)
小程序客戶端緩存 (通過 storage )自定義登錄態(tài)(token),后續(xù)調(diào)用接口時攜帶該登錄態(tài)作為用戶身份標(biāo)識即可
如果想實(shí)現(xiàn)多個小程序,公眾號,已有登錄系統(tǒng)的數(shù)據(jù)互通,可以通過獲取到用戶 unionid 的方式建立用戶體系。因?yàn)?unionid 在同一開放平臺下的所所有應(yīng)用都是相同的,通過 unionid 建立的用戶體系即可實(shí)現(xiàn)全平臺數(shù)據(jù)的互通,更方便的接入原有的功能,那如何獲取 unionid 呢,有以下兩種方式:
如果戶關(guān)注了某個相同主體公眾號,或曾經(jīng)在某個相同主體App、公眾號上進(jìn)行過微信登錄授權(quán),通過 wx.login 可以直接獲取 到 unionid
結(jié)合 wx.getUserInfo 和 <button open-type="getUserInfo"><button/> 這兩種方式引導(dǎo)用戶主動授權(quán),主動授權(quán)后通過返回的信息和服務(wù)端交互 (這里有一步需要服務(wù)端解密數(shù)據(jù)的過程,很簡單,微信提供了示例代碼) 即可拿到 unionid 建立用戶體系, 然后由服務(wù)端返回登錄態(tài),本地記錄即可實(shí)現(xiàn)登錄,附上微信提供的最佳實(shí)踐:
調(diào)用 wx.login 獲取 code,然后從微信后端換取到 session_key,用于解密 getUserInfo返回的敏感數(shù)據(jù)。
使用 wx.getSetting 獲取用戶的授權(quán)情況
獲取到用戶數(shù)據(jù)后可以進(jìn)行展示或者發(fā)送給自己的后端。
wx.login(獲取code) ===> wx.getUserInfo(用戶授權(quán)) ===> 獲取 unionid 復(fù)制代碼
因?yàn)樾〕绦虿淮嬖?nbsp;cookie 的概念, 登錄態(tài)必須緩存在本地,因此強(qiáng)烈建議為登錄態(tài)設(shè)置過期時間
值得一提的是如果需要支持風(fēng)控安全校驗(yàn),多平臺登錄等功能,可能需要加入一些公共參數(shù),例如platform,channel,deviceParam等參數(shù)。在和服務(wù)端確定方案時,作為前端同學(xué)應(yīng)該及時提出這些合理的建議,設(shè)計(jì)合理的系統(tǒng)。
openid , unionid 不要在接口中明文傳輸,這是一種危險(xiǎn)的行為,同時也很不專業(yè)。
經(jīng)常開發(fā)和使用小程序的同學(xué)對這個功能一定不陌生,這是一種常見的引流方式,一般同時會在圖片中附加一個小程序二維碼。
借助 canvas 元素,將需要導(dǎo)出的樣式首先在 canvas 畫布上繪制出來 (api基本和h5保持一致,但有輕微差異,使用時注意即可)
借助微信提供的 canvasToTempFilePath 導(dǎo)出圖片,最后再使用 saveImageToPhotosAlbum(需要授權(quán))保存圖片到本地
根據(jù)上述的原理來看,實(shí)現(xiàn)是很簡單的,只不過就是設(shè)計(jì)稿的提取,繪制即可,但是作為一個常用功能,每次都這樣寫一坨代碼豈不是非常的難受。那小程序如何設(shè)計(jì)一個通用的方法來幫助我們導(dǎo)出圖片呢?思路如下:
繪制出需要的樣式這一步是省略不掉的。但是我們可以封裝一個繪制庫,包含常見圖形的繪制,例如矩形,圓角矩形,圓, 扇形, 三角形, 文字,圖片減少繪制代碼,只需要提煉出樣式信息,便可以輕松的繪制,最后導(dǎo)出圖片存入相冊。筆者覺得以下這種方式繪制更為優(yōu)雅清晰一些,其實(shí)也可以使用加入一個type參數(shù)來指定繪制類型,傳入的一個是樣式數(shù)組,實(shí)現(xiàn)繪制。
結(jié)合上一步的實(shí)現(xiàn),如果對于同一類型的卡片有多次導(dǎo)出需求的場景,也可以使用自定義組件的方式,封裝同一類型的卡片為一個通用組件,在需要導(dǎo)出圖片功能的地方,引入該組件即可。
class CanvasKit {
constructor() {
}
drawImg(option = {}) {
...
return this
}
drawRect(option = {}) {
return this
}
drawText(option = {}) {
...
return this
}
static exportImg(option = {}) {
...
}
}
let drawer = new CanvasKit('canvasId').drawImg(styleObj1).drawText(styleObj2)
drawer.exportImg()
|
數(shù)據(jù)統(tǒng)計(jì)作為目前一種常用的分析用戶行為的方式,小程序端也是必不可少的。小程序采取的曝光,點(diǎn)擊數(shù)據(jù)埋點(diǎn)其實(shí)和h5原理是一樣的。但是埋點(diǎn)作為一個和業(yè)務(wù)邏輯不相關(guān)的需求,我們?nèi)绻诿恳粋€點(diǎn)擊事件,每一個生命周期加入各種埋點(diǎn)代碼,則會干擾正常的業(yè)務(wù)邏輯,和使代碼變的臃腫,筆者提供以下幾種思路來解決數(shù)據(jù)埋點(diǎn):
小程序的代碼結(jié)構(gòu)是,每一個 Page 中都有一個 Page 方法,接受一個包含生命周期函數(shù),數(shù)據(jù)的 業(yè)務(wù)邏輯對象 包裝這層數(shù)據(jù),借助小程序的底層邏輯實(shí)現(xiàn)頁面的業(yè)務(wù)邏輯。通過這個我們可以想到思路,對Page進(jìn)行一次包裝,篡改它的生命周期和點(diǎn)擊事件,混入埋點(diǎn)代碼,不干擾業(yè)務(wù)邏輯,只要做一些簡單的配置即可埋點(diǎn),簡單的代碼實(shí)現(xiàn)如下:
page = function(params) {
let keys = params.keys()
keys.forEach(v => {
if (v === 'onLoad') {
params[v] = function(options) {
stat() //曝光埋點(diǎn)代碼
params[v].call(this, options)
}
}
else if (v.includes('click')) {
params[v] = funciton(event) {
let data = event.dataset.config
stat(data) // 點(diǎn)擊埋點(diǎn)
param[v].call(this)
}
}
})
}
|
這種思路不光適用于埋點(diǎn),也可以用來作全局異常處理,請求的統(tǒng)一處理等場景。
對于特殊的一些業(yè)務(wù),我們可以采取 接口埋點(diǎn) ,什么叫接口埋點(diǎn)呢?很多情況下,我們有的api并不是多處調(diào)用的,只會在某一個特定的頁面調(diào)用,通過這個思路我們可以分析出,該接口被請求,則這個行為被觸發(fā)了,則完全可以通過服務(wù)端日志得出埋點(diǎn)數(shù)據(jù),但是這種方式局限性較大,而且屬于分析結(jié)果得出過程,可能存在誤差,但可以作為一種思路了解一下。
微信本身提供的數(shù)據(jù)分析能力,微信本身提供了常規(guī)分析和自定義分析兩種數(shù)據(jù)分析方式,在小程序后臺配置即可。借助 小程序數(shù)據(jù)助手 這款小程序可以很方便的查看。
目前的前端開發(fā)過程,工程化是必不可少的一環(huán),那小程序工程化都需要做些什么呢,先看下目前小程序開發(fā)當(dāng)中存在哪些問題需要解決:
對于目前常用的工程化方案,webpack,rollup,parcel等來看,都常用與單頁應(yīng)用的打包和處理,而小程序天生是 “多頁應(yīng)用” 并且存在一些特定的配置。根據(jù)要解決的問題來看,無非是文件的編譯,修改,拷貝這些處理,對于這些需求,我們想到基于流的 gulp 非常的適合處理,并且相對于webpack配置多頁應(yīng)用更加簡單。所以小程序工程化方案推薦使用 gulp
通過 gulp 的 task 實(shí)現(xiàn):
上述實(shí)現(xiàn)起來其實(shí)并不是很難,但是這樣的話就是一份純粹的 gulp 構(gòu)建腳本和 約定好的目錄而已,每次都有一個新的小程序都來拷貝這份腳本來處理嗎?顯然不合適,那如何真正的實(shí)現(xiàn) 小程序工程化 呢? 我們可能需要一個簡單的腳手架,腳手架需要支持的功能:

微信小程序的框架包含兩部分 View 視圖層、App Service邏輯層。View 層用來渲染頁面結(jié)構(gòu),AppService 層用來邏輯處理、數(shù)據(jù)請求、接口調(diào)用。
它們在 兩個線程里 運(yùn)行。
它們在 兩個線程里 運(yùn)行。
它們在 兩個線程里 運(yùn)行。
視圖層和邏輯層通過系統(tǒng)層的 JSBridage 進(jìn)行通信,邏輯層把數(shù)據(jù)變化通知到視圖層,觸發(fā)視圖層頁面更新,視圖層把觸發(fā)的事件通知到邏輯層進(jìn)行業(yè)務(wù)處理。
補(bǔ)充

在 Mac下 使用 js-beautify 對微信開發(fā)工具 @v1.02.1808080代碼批量格式化:
cd /Applications/wechatwebdevtools.app/Contents/Resources/package.nw
find . -type f -name '*.js' -not -path "./node_modules/*" -not -path -exec js-beautify -r -s 2 -p -f '{}' \;
復(fù)制代碼
在 js/extensions/appservice/index.js 中找到:
267: function(a, b, c) {
const d = c(8),
e = c(227),
f = c(226),
g = c(228),
h = c(229),
i = c(230);
var j = window.__global.navigator.userAgent,
k = -1 !== j.indexOf('game');
k || i(), window.__global.getNewWeixinJSBridge = (a) => {
const {
invoke: b
} = f(a), {
publish: c
} = g(a), {
subscribe: d,
triggerSubscribeEvent: i
} = h(a), {
on: j,
triggerOnEvent: k
} = e(a);
return {
invoke: b,
publish: c,
subscribe: d,
on: j,
get __triggerOnEvent() {
return k
},
get __triggerSubscribeEvent() {
return i
}
}
}, window.WeixinJSBridge = window.__global.WeixinJSBridge = window.__global.getNewWeixinJSBridge('global'), window.__global.WeixinJSBridgeMap = {
__globalBridge: window.WeixinJSBridge
}, __devtoolsConfig.online && __devtoolsConfig.autoTest && setInterval(() => {
console.clear()
}, 1e4);
try {
var l = new window.__global.XMLHttpRequest;
l.responseType = 'text', l.open('GET', `http://${window.location.host}/calibration/${Date.now()}`, !0), l.send()
} catch (a) {}
}
復(fù)制代碼
|
在 js/extensions/gamenaitveview/index.js 中找到:
299: function(a, b, c) {
'use strict';
Object.defineProperty(b, '__esModule', {
value: !0
});
var d = c(242),
e = c(241),
f = c(243),
g = c(244);
window.WeixinJSBridge = {
on: d.a,
invoke: e.a,
publish: f.a,
subscribe: g.a
}
},
復(fù)制代碼
|
在 js/extensions/pageframe/index.js 中找到:
317: function(a, b, c) {
'use strict';
function d() {
window.WeixinJSBridge = {
on: e.a,
invoke: f.a,
publish: g.a,
subscribe: h.a
}, k.a.init();
let a = document.createEvent('UIEvent');
a.initEvent('WeixinJSBridgeReady', !1, !1), document.dispatchEvent(a), i.a.init()
}
Object.defineProperty(b, '__esModule', {
value: !0
});
var e = c(254),
f = c(253),
g = c(255),
h = c(256),
i = c(86),
j = c(257),
k = c.n(j);
'complete' === document.readyState ? d() : window.addEventListener('load', function() {
d()
})
},
復(fù)制代碼
我們都看到了 WeixinJSBridge 的定義。分別都有 on 、 invoke 、 publish 、 subscribe 這個幾個關(guān)鍵方法。
拿 invoke 舉例,在 js/extensions/appservice/index.js 中發(fā)現(xiàn)這段代碼:
f (!r) p[b] = s, f.send({
command: 'APPSERVICE_INVOKE',
data: {
api: c,
args: e,
callbackID: b
}
});
復(fù)制代碼
|
在 js/extensions/pageframe/index.js 中發(fā)現(xiàn)這段代碼:
g[d] = c, e.a.send({
command: 'WEBVIEW_INVOKE',
data: {
api: a,
args: b,
callbackID: d
}
})
復(fù)制代碼
|
簡單的分析得知:字段 command 用來區(qū)分行為, invoke 用來調(diào)用 Native 的 Api。在不同的來源要使用不同的前綴。 data 里面包含 Api 名,參數(shù)。另外 callbackID 指定接受回調(diào)的方法句柄。Appservice 和 Webview 使用的通信協(xié)議是一致的。
我們不能在代碼里使用 BOM 和 DOM 是因?yàn)楦緵]有,另一方面也不希望 JS 代碼直接操作視圖。
在開發(fā)工具中 remote-helper.js 中找到了這樣的代碼:
const vm = require("vm");
const vmGlobal = {
require: undefined,
eval: undefined,
process: undefined,
setTimeout(...args) {
//...省略代碼
return timerCount;
},
clearTimeout(id) {
const timer = timers[id];
if (timer) {
clearTimeout(timer);
delete timers[id];
}
},
setInterval(...args) {
//...省略代碼
return timerCount;
},
clearInterval(id) {
const timer = timers[id];
if (timer) {
clearInterval(timer);
delete timers[id];
}
},
console: (() => {
//...省略代碼
return consoleClone;
})()
};
const jsVm = vm.createContext(vmGlobal);
// 省略大量代碼...
function loadCode(filePath, sourceURL, content) {
let ret;
try {
const script = typeof content === 'string' ? content : fs.readFileSync(filePath, 'utf-8').toString();
ret = vm.runInContext(script, jsVm, {
filename: sourceURL,
});
}
catch (e) {
// something went wrong in user code
console.error(e);
}
return ret;
}
復(fù)制代碼
|
這樣的分層設(shè)計(jì)顯然是有意為之的,它的中間層完全控制了程序?qū)τ诮缑孢M(jìn)行的操作, 同時對于傳遞的數(shù)據(jù)和響應(yīng)時間也能做到監(jiān)控。一方面程序的行為受到了極大限制, 另一方面微信可以確保他們對于小程序內(nèi)容和體驗(yàn)有絕對的控制。
這樣的結(jié)構(gòu)也說明了小程序的動畫和繪圖 API 被設(shè)計(jì)成生成一個最終對象而不是一步一步執(zhí)行的樣子, 原因就是 Json 格式的數(shù)據(jù)傳遞和解析相比與原生 API 都是損耗不菲的,如果頻繁調(diào)用很可能損耗過多性能,進(jìn)而影響用戶體驗(yàn)。


var context = wx.createCanvasContext('firstCanvas')
context.setStrokeStyle("#00ff00")
context.setLineWidth(5)
context.rect(0, 0, 200, 200)
context.stroke()
context.setStrokeStyle("#ff0000")
context.setLineWidth(2)
context.moveTo(160, 100)
context.arc(100, 100, 60, 0, 2 * Math.PI, true)
context.moveTo(140, 100)
context.arc(100, 100, 40, 0, Math.PI, false)
context.moveTo(85, 80)
context.arc(80, 80, 5, 0, 2 * Math.PI, true)
context.moveTo(125, 80)
context.arc(120, 80, 5, 0, 2 * Math.PI, true)
context.stroke()
context.draw()
復(fù)制代碼
|
Page({
data: {
animationData: {}
},
onShow: function(){
var animation = wx.createAnimation({
duration: 1000,
timingFunction: 'ease',
})
this.animation = animation
animation.scale(2,2).rotate(45).step()
this.setData({
animationData:animation.export()
})
}
})
復(fù)制代碼
|
知識點(diǎn)考察
WXML(WeiXin Markup Language)

Wxml編譯器:Wcc 把 Wxml文件 轉(zhuǎn)為 JS
執(zhí)行方式:Wcc index.wxml
WXSS(WeiXin Style Sheets)

wxss編譯器:wcsc 把wxss文件轉(zhuǎn)化為 js
執(zhí)行方式: wcsc index.wxss
親測包含但不限于如下內(nèi)容:
建議 Css3 的特性都可以做一下嘗試。
rpx(responsive pixel): 可以根據(jù)屏幕寬度進(jìn)行自適應(yīng)。規(guī)定屏幕寬為 750rpx。公式:
const dsWidth = 750
export const screenHeightOfRpx = function () {
return 750 / env.screenWidth * env.screenHeight
}
export const rpxToPx = function (rpx) {
return env.screenWidth / 750 * rpx
}
export const pxToRpx = function (px) {
return 750 / env.screenWidth * px
}
|
復(fù)制代碼

可以了解一下 pr2rpx-loader 這個庫。
使用 @import 語句可以導(dǎo)入外聯(lián)樣式表, @import 后跟需要導(dǎo)入的外聯(lián)樣式表的相對路徑,用 ; 表示語句結(jié)束。
靜態(tài)的樣式統(tǒng)一寫到 class 中。style 接收動態(tài)的樣式,在運(yùn)行時會進(jìn)行解析, 請盡量避免將靜態(tài)的樣式寫進(jìn) style 中,以免影響渲染速度 。
定義在 app.wxss 中的樣式為全局樣式,作用于每一個頁面。在 page 的 wxss 文件中定義的樣式為局部樣式,只作用在對應(yīng)的頁面,并會覆蓋 app.wxss 中相同的選擇器。
小程序未來有計(jì)劃支持字體。參考微信公開課。
小程序開發(fā)與平時 Web開發(fā)類似,也可以使用字體圖標(biāo),但是 src:url() 無論本地還是遠(yuǎn)程地址都不行,base64 值則都是可以顯示的。
將 ttf 文件轉(zhuǎn)換成 base64。打開這個平臺 transfonter.org/。點(diǎn)擊 Add fonts 按鈕,加載ttf格式的那個文件。將下邊的 base64 encode 改為 on。點(diǎn)擊 Convert 按鈕進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換后點(diǎn)擊 download 下載。
復(fù)制下載的壓縮文件中的 stylesheet.css 的內(nèi)容到 font.wxss ,并且將 icomoon 中的 style.css 除了 @font-face 所有的代碼也復(fù)制到 font.wxss 并將i選擇器換成 .iconfont,最后:
<text class="iconfont icon-home" style="font-size:50px;color:red"></text> 復(fù)制代碼
小程序提供了一系列組件用于開發(fā)業(yè)務(wù)功能,按照功能與HTML5的標(biāo)簽進(jìn)行對比如下:

小程序的組件基于Web Component標(biāo)準(zhǔn)
使用Polymer框架實(shí)現(xiàn)Web Component
目前Native實(shí)現(xiàn)的組件有
cavnas
video
map
textarea

Native組件層在 WebView 層之上。這目前帶來了一些問題:
包含但不限于:
小程序仍然使用 WebView 渲染,并非原生渲染。(部分原生)
服務(wù)端接口返回的頭無法執(zhí)行,比如:Set-Cookie。
依賴瀏覽器環(huán)境的 JS 庫不能使用。
不能使用 npm,但是可以自搭構(gòu)建工具或者使用 mpvue。(未來官方有計(jì)劃支持)
不能使用 ES7,可以自己用babel+webpack自搭或者使用 mpvue。
不支持使用自己的字體(未來官方計(jì)劃支持)。
可以用 base64 的方式來使用 iconfont。
小程序不能發(fā)朋友圈(可以通過保存圖片到本地,發(fā)圖片到朋友前。二維碼可以使用B接口)。
獲取二維碼/小程序接口的限制。
小程序推送只能使用“服務(wù)通知” 而且需要用戶主動觸發(fā)提交 formId,formId 只有7天有效期。(現(xiàn)在的做法是在每個頁面都放入form并且隱藏以此獲取更多的 formId。后端使用原則為:優(yōu)先使用有效期最短的)
小程序大小限制 2M,分包總計(jì)不超過 8M
轉(zhuǎn)發(fā)(分享)小程序不能拿到成功結(jié)果,原來可以。鏈接(小游戲造的孽)
拿到相同的 unionId 必須綁在同一個開放平臺下。開放平臺綁定限制:
公眾號關(guān)聯(lián)小程序,鏈接
一個公眾號關(guān)聯(lián)的10個同主體小程序和3個非同主體小程序可以互相跳轉(zhuǎn)
品牌搜索不支持金融、醫(yī)療
小程序授權(quán)需要用戶主動點(diǎn)擊
小程序不提供測試 access_token
安卓系統(tǒng)下,小程序授權(quán)獲取用戶信息之后,刪除小程序再重新獲取,并重新授權(quán),得到舊簽名,導(dǎo)致第一次授權(quán)失敗
開發(fā)者工具上,授權(quán)獲取用戶信息之后,如果清緩存選擇全部清除,則即使使用了wx.checkSession,并且在session_key有效期內(nèi),授權(quán)獲取用戶信息也會得到新的session_key
為了驗(yàn)證小程序?qū)TTP的支持適配情況,我找了兩個服務(wù)器做測試,一個是網(wǎng)上搜索到支持HTTP2的服務(wù)器,一個是我本地起的一個HTTP2服務(wù)器。測試中所有請求方法均使用 wx.request。
網(wǎng)上支持HTTP2的服務(wù)器: HTTPs://www.snel.com:443
在Chrome上查看該服務(wù)器為 HTTP2

在模擬器上請求該接口, 請求頭 的HTTP版本為HTTP1.1,模擬器不支持HTTP2

由于小程序線上環(huán)境需要在項(xiàng)目管理里配置請求域名,而這個域名不是我們需要的請求域名,沒必要浪費(fèi)一個域名位置,所以打開不驗(yàn)證域名,TSL 等選項(xiàng)請求該接口,通過抓包工具表現(xiàn)與模擬器相同

由上可以看出,在真機(jī)與模擬器都不支持 HTTP2,但是都是成功請求的,并且 響應(yīng)頭 里的 HTTP 版本都變成了HTTP1.1 版本,說明服務(wù)器對 HTTP1.1 做了兼容性適配。
本地新啟一個 node 服務(wù)器,返回 JSON 為請求的 HTTP 版本

如果服務(wù)器只支持 HTTP2,在模擬器請求時發(fā)生了一個 ALPN 協(xié)議的錯誤。并且提醒使用適配 HTTP1

當(dāng)把服務(wù)器的 allowHTTP1 ,設(shè)置為 true ,并在請求時處理相關(guān)相關(guān)請求參數(shù)后,模擬器能正常訪問接口,并打印出對應(yīng)的 HTTP 請求版本





代碼包的大小是最直接影響小程序加載啟動速度的因素。代碼包越大不僅下載速度時間長,業(yè)務(wù)代碼注入時間也會變長。所以最好的優(yōu)化方式就是減少代碼包的大小。

小程序加載的三個階段的表示。

在構(gòu)建小程序分包項(xiàng)目時,構(gòu)建會輸出一個或多個功能的分包,其中每個分包小程序必定含有一個主包,所謂的主包,即放置默認(rèn)啟動頁面/TabBar 頁面,以及一些所有分包都需用到公共資源/JS 腳本,而分包則是根據(jù)開發(fā)者的配置進(jìn)行劃分。
在小程序啟動時,默認(rèn)會下載主包并啟動主包內(nèi)頁面,如果用戶需要打開分包內(nèi)某個頁面,客戶端會把對應(yīng)分包下載下來,下載完成后再進(jìn)行展示。
優(yōu)點(diǎn):
限制:
原生分包加載的配置假設(shè)支持分包的小程序目錄結(jié)構(gòu)如下:
├── app.js ├── app.json ├── app.wxss ├── packageA │ └── pages │ ├── cat │ └── dog ├── packageB │ └── pages │ ├── apple │ └── banana ├── pages │ ├── index │ └── logs └── utils 復(fù)制代碼
開發(fā)者通過在 app.json subPackages 字段聲明項(xiàng)目分包結(jié)構(gòu):
{
"pages":[
"pages/index",
"pages/logs"
],
"subPackages": [
{
"root": "packageA",
"pages": [
"pages/cat",
"pages/dog"
]
}, {
"root": "packageB",
"pages": [
"pages/apple",
"pages/banana"
]
}
]
}
|
官方即將推出分包預(yù)加載

獨(dú)立分包


每次 setData 的調(diào)用都是一次進(jìn)程間通信過程,通信開銷與 setData 的數(shù)據(jù)量正相關(guān)。
setData 會引發(fā)視圖層頁面內(nèi)容的更新,這一耗時操作一定時間中會阻塞用戶交互。

在需要頻繁更新的場景下,自定義組件的更新只在組件內(nèi)部進(jìn)行,不受頁面其他部分內(nèi)容復(fù)雜性影響。
小程序的幾個頁面間,存在一些相同或是類似的區(qū)域,這時候可以把這些區(qū)域邏輯封裝成一個自定義組件,代碼就可以重用,或者對于比較獨(dú)立邏輯,也可以把它封裝成一個自定義組件,也就是微信去年發(fā)布的自定義組件,它讓代碼得到復(fù)用、減少代碼量,更方便模塊化,優(yōu)化代碼架構(gòu)組織,也使得模塊清晰,后期更好地維護(hù),從而保證更好的性能。
但微信打算在原來的基礎(chǔ)上推出的自定義組件 2.0,它將擁有更高級的性能:
目前小程序開發(fā)的痛點(diǎn)是:開源組件要手動復(fù)制到項(xiàng)目,后續(xù)更新組件也需要手動操作。不久的將來,小程序?qū)⒅С謓pm包管理,有了這個之后,想要引入一些開源的項(xiàng)目就變得很簡單了,只要在項(xiàng)目里面聲明,然后用簡單的命令安裝,就可以使用了。
微信小程序團(tuán)隊(duì)表示,他們在考慮推出一些官方自定義組件,為什么不內(nèi)置到基礎(chǔ)庫里呢?因?yàn)閮?nèi)置組件要提供給開發(fā)者,這個組件一定是開發(fā)者很難實(shí)現(xiàn)或者是無法實(shí)現(xiàn)的一個能力。所以他們更傾向于封裝成自定義組件,想基于這些內(nèi)置組件里,封裝一些比較常見的、交互邏輯比較復(fù)雜的組件給大家使用,讓大家更容易開發(fā)。類似彈幕組件,開發(fā)者就不用關(guān)注彈幕怎么飄,可以節(jié)省開發(fā)者的開發(fā)成本。
同時,他們也想給開發(fā)者提供一些規(guī)范和一些模板,讓開發(fā)者設(shè)計(jì)出好用的自定義組件,更好地被大家去使用。
當(dāng)小程序加載太慢時,可能會導(dǎo)致用戶的流失,而小程序的開發(fā)者可能會面臨著不知道如何定位問題或不知如何解決問題的困境。
為此,小程序即將推出一個體驗(yàn)評分的功能,這是為了幫助開發(fā)者可以檢查出小程序有一些什么體驗(yàn)不好的地方,也會同時給出一份優(yōu)化的指引建議。
小程序在最初的技術(shù)選型時,引入了原生組件的概念,因?yàn)樵M件可以使小程序的能力更加豐富,比如地圖、音視頻的能力,但是原生組件是由客戶端原生渲染的,導(dǎo)致了原生組件的層級是最高的,開發(fā)者很容易遇到打開調(diào)試的問題,發(fā)現(xiàn)視頻組件擋在了 vConsole 上。
為了解決這個問題,當(dāng)時微信做了一個過渡的方案:cover-view。cover-view可以覆蓋在原生組件之上,這一套方案解決了大部分的需求場景。比如說視頻組件上很多的按鈕、標(biāo)題甚至還有動畫的彈幕,這些都是用 cover-view 去實(shí)現(xiàn)的,但它還是沒有完全解決原生組件的開發(fā)體驗(yàn)問題,因?yàn)?cover-view 有一些限制:
因此微信決定將用同層渲染取代 cover-view,它能像普通組件一樣使用,原生組件的層級不再是最高,而是和其他的非原生組件在同一層級渲染,可完全由 z-index 控制,可完全支持觸摸事件。
微信表示,同層渲染在 iOS 平臺小程序上已經(jīng)開始內(nèi)測,會很快開放給開發(fā)者,Android 平臺已經(jīng)取得突破性進(jìn)展,目前正在做一輪封裝的工作,開放指日可待。
相比傳統(tǒng)的小程序框架,這個一直是我們作為資深開發(fā)者比較期望去解決的,在 Web 開發(fā)中,隨著 Flux、Redux、Vuex 等多個數(shù)據(jù)流工具出現(xiàn),我們也期望在業(yè)務(wù)復(fù)雜的小程序中使用。
WePY 默認(rèn)支持 Redux,在腳手架生成項(xiàng)目的時候可以內(nèi)置
Mpvue 作為 Vue 的移植版本,當(dāng)然支持 Vuex,同樣在腳手架生成項(xiàng)目的時候可以內(nèi)置
如果你和我們一樣,經(jīng)歷了從無到有的小程序業(yè)務(wù)開發(fā),建議閱讀【小程序的組件化開發(fā)】章節(jié),進(jìn)行官方語法的組件庫開發(fā)(從基礎(chǔ)庫 1.6.3 開始,官方提供了組件化解決方案)。
export default class Index extends wepy.page {}
復(fù)制代碼
所有的小程序開發(fā)依賴官方提供的開發(fā)者工具。開發(fā)者工具簡單直觀,對調(diào)試小程序很有幫助,現(xiàn)在也支持騰訊云(目前我們還沒有使用,但是對新的一些開發(fā)者還是有幫助的),可以申請測試報(bào)告查看小程序在真實(shí)的移動設(shè)備上運(yùn)行性能和運(yùn)行效果,但是它本身沒有類似前端工程化中的概念和工具。

先說結(jié)論:選擇 mpvue。
wepy vs mpvue。
理由:
工程化原生開發(fā)因?yàn)椴粠Чこ袒?,諸如NPM包(未來會引入)、ES7、圖片壓縮、PostCss、pug、ESLint等等不能用。如果自己要搭工程化,不如直接使用wepy或mpvue。mpvue和wepy都可以和小程序原生開發(fā)混寫。, 參考wepy 。 而問題在于wepy沒有引入webpack(wepy@2.0.x依然沒有引入),以上說的這些東西都要造輪子(作者造或自己造)。沒有引入 Webpack 是一個重大的硬傷。社區(qū)維護(hù)的成熟 Webpack 顯然更穩(wěn)定,輪子更多。
維護(hù)wepy 也是社區(qū)維護(hù)的,是官方的?其實(shí) wepy 的主要開發(fā)者只有作者一人,附上一個 contrubutors 鏈接。另外被官方招安了也是后來的事,再說騰訊要有精力幫著一起維護(hù)好 wepy,為什么不花精力在小程序原生開發(fā)上呢?再來看看 mpvue,是美團(tuán)一個前端小組維護(hù)的。
學(xué)習(xí)成本Vue 的學(xué)習(xí)曲線比較平緩。mpvue 是 Vue的子集。所以 mpvue 的學(xué)習(xí)成本會低于 wepy。尤其是之前技術(shù)棧有學(xué)過用過 Vue 的。
未來規(guī)劃mpvue 已經(jīng)支持 web 和小程序。因?yàn)?mpvue 基于AST,所以未來可以支持支付寶小程序和快應(yīng)用。他們也是有這樣的規(guī)劃。
請?jiān)谛枨蟪叵旅孀约赫?/p>

坑兩者都有各自的坑。但是我覺得有一些wepy的坑是沒法容忍的。比如 repeat組建里面用computed得到的列表全是同一套數(shù)據(jù) 而且1.x是沒法解決的。 wepy和mpvue我都開發(fā)過完整小程序的體驗(yàn)下,我覺得wepy的坑更多,而且wepy有些坑礙于架構(gòu)設(shè)計(jì)沒辦法解決。
Vue.js 小程序版, fork 自 vuejs/vue@2.4.1,保留了 vue runtime 能力,添加了小程序平臺的支持。 mpvue 是一個使用 Vue.js 開發(fā)小程序的前端框架。框架基于 Vue.js 核心, mpvue 修改了 Vue.js 的 runtime 和 compiler 實(shí)現(xiàn),使其可以運(yùn)行在小程序環(huán)境中,從而為小程序開發(fā)引入了整套 Vue.js 開發(fā)體驗(yàn)。
mpvue mpvue-loader
要了解 mpvue 原理必然要了解 Vue 原理,這是大前提。但是要講清楚 Vue 原理需要花費(fèi)大量的篇幅,不如參考 learnVue 。
現(xiàn)在假設(shè)您對 Vue 原理有個大概的了解。
由于 Vue 使用了 Virtual DOM,所以 Virtual DOM 可以在任何支持 JavaScript 語言的平臺上操作,譬如說目前 Vue 支持瀏覽器平臺或 weex,也可以是 mp(小程序)。那么最后 Virtual DOM 如何映射到真實(shí)的 DOM 節(jié)點(diǎn)上呢?vue為平臺做了一層適配層,瀏覽器平臺見 runtime/node-ops.js 、weex平臺見 runtime/node-ops.js ,小程序見 runtime/node-ops.js 。不同平臺之間通過適配層對外提供相同的接口,Virtual DOM進(jìn)行操作Real DOM節(jié)點(diǎn)的時候,只需要調(diào)用這些適配層的接口即可,而內(nèi)部實(shí)現(xiàn)則不需要關(guān)心,它會根據(jù)平臺的改變而改變。
所以思路肯定是往增加一個 mp 平臺的 runtime 方向走。但問題是小程序不能操作 DOM,所以 mp 下的 node-ops.js 里面的實(shí)現(xiàn)都是直接 return obj 。
新 Virtual DOM 和舊 Virtual DOM 之間需要做一個 patch,找出 diff。patch完了之后的 diff 怎么更新視圖,也就是如何給這些 DOM 加入 attr、class、style 等 DOM 屬性呢? Vue 中有 nextTick 的概念用以更新視圖,mpvue這塊對于小程序的 setData 應(yīng)該怎么處理呢?
另外個問題在于小程序的 Virtual DOM 怎么生成?也就是怎么將 template 編譯成 render function 。這當(dāng)中還涉及到 運(yùn)行時-編譯器-vs-只包含運(yùn)行時 ,顯然如果要提高性能、減少包大小、輸出 wxml、mpvue 也要提供預(yù)編譯的能力。因?yàn)橐A(yù)輸出 wxml 且沒法動態(tài)改變 DOM,所以動態(tài)組件,自定義 render,和 <script type="text/x-template"> 字符串模版等都不支持(參考)。
另外還有一些其他問題,最后總結(jié)一下
render function
platform/mp的目錄結(jié)構(gòu)
. ├── compiler //解決問題1,mpvue-template-compiler源碼部分 ├── runtime //解決問題3 4 5 6 7 ├── util //工具方法 ├── entry-compiler.js //mpvue-template-compiler的入口。package.json相關(guān)命令會自動生成mpvue-template-compiler這個package。 ├── entry-runtime.js //對外提供Vue對象,當(dāng)然是mpvue └── join-code-in-build.js //編譯出SDK時的修復(fù) 復(fù)制代碼
mpvue-loader 是 vue-loader 的一個擴(kuò)展延伸版,類似于超集的關(guān)系,除了 vue-loader 本身所具備的能力之外,它還會利用 mpvue-template-compiler 生成 render function 。
它會從 webpack 的配置中的 entry 開始,分析依賴模塊,并分別打包。在entry 中 app 屬性及其內(nèi)容會被打包為微信小程序所需要的 app.js/app.json/app.wxss,其余的會生成對應(yīng)的頁面page.js/page.json/page.wxml/page.wxss,如示例的 entry 將會生成如下這些文件,文件內(nèi)容下文慢慢講來:
// webpack.config.js
{
// ...
entry: {
app: resolve('./src/main.js'), // app 字段被識別為 app 類型
index: resolve('./src/pages/index/main.js'), // 其余字段被識別為 page 類型
'news/home': resolve('./src/pages/news/home/index.js')
}
}
// 產(chǎn)出文件的結(jié)構(gòu)
.
├── app.js
├── app.json
├──· app.wxss
├── components
│ ├── card$74bfae61.wxml
│ ├── index$023eef02.wxml
│ └── news$0699930b.wxml
├── news
│ ├── home.js
│ ├── home.wxml
│ └── home.wxss
├── pages
│ └── index
│ ├── index.js
│ ├── index.wxml
│ └── index.wxss
└── static
├── css
│ ├── app.wxss
│ ├── index.wxss
│ └── news
│ └── home.wxss
└── js
├── app.js
├── index.js
├── manifest.js
├── news
│ └── home.js
└── vendor.js
復(fù)制代碼
<template>
<div class="my-component" @click="test">
<h1>{{msg}}</h1>
<other-component :msg="msg"></other-component>
</div>
</template>
<script>
import otherComponent from './otherComponent.vue'
export default {
components: { otherComponent },
data () {
return { msg: 'Hello Vue.js!' }
},
methods: {
test() {}
}
}
</script>
復(fù)制代碼
這樣一個 Vue 的組件的模版部分會生成相應(yīng)的 wxml
<import src="components/other-component$hash.wxml" />
<template name="component$hash">
<view class="my-component" bindtap="handleProxy">
<view class="_h1">{{msg}}</view>
<template is="other-component$hash" wx:if="{{ $c[0] }}" data="{{ ...$c[0] }}"></template>
</view>
</template>
復(fù)制代碼
可能已經(jīng)注意到了 other-component(:msg="msg") 被轉(zhuǎn)化成了 。mpvue 在運(yùn)行時會從根組件開始把所有的組件實(shí)例數(shù)據(jù)合并成一個樹形的數(shù)據(jù),然后通過 setData 到 appData, $c 是 $children 的縮寫。至于那個 0 則是我們的 compiler 處理過后的一個標(biāo)記,會為每一個子組件打一個特定的不重復(fù)的標(biāo)記。 樹形數(shù)據(jù)結(jié)構(gòu)如下:
// 這兒數(shù)據(jù)結(jié)構(gòu)是一個數(shù)組,index 是動態(tài)的
{
$child: {
'0'{
// ... root data
$child: {
'0': {
// ... data
msg: 'Hello Vue.js!',
$child: {
// ...data
}
}
}
}
}
}
復(fù)制代碼
這個部分的處理同 web 的處理差異不大,唯一不同在于通過配置生成 .css 為 .wxss ,其中的對于 css 的若干處理,在 postcss-mpvue-wxss 和 px2rpx-loader 這兩部分的文檔中又詳細(xì)的介紹。
app.json/page.json 1.1.1 以上
推薦和小程序一樣,將 app.json/page.json 放到頁面入口處,使用 copy-webpack-plugin copy 到對應(yīng)的生成位置。
1.1.1 以下
這部分內(nèi)容來源于 app 和 page 的 entry 文件,通常習(xí)慣是 main.js,你需要在你的入口文件中 export default { config: {} },這才能被我們的 loader 識別為這是一個配置,需要寫成 json 文件。
import Vue from 'vue';
import App from './app';
const vueApp = new Vue(App);
vueApp.$mount();
// 這個是我們約定的額外的配置
export default {
// 這個字段下的數(shù)據(jù)會被填充到 app.json / page.json
config: {
pages: ['static/calendar/calendar', '^pages/list/list'], // Will be filled in webpack
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#455A73',
navigationBarTitleText: '美團(tuán)汽車票',
navigationBarTextStyle: '#fff'
}
}
};
復(fù)制代碼
同時,這個時候,我們會根據(jù) entry 的頁面數(shù)據(jù),自動填充到 app.json 中的 pages 字段。 pages 字段也是可以自定義的,約定帶有 ^ 符號開頭的頁面,會放到數(shù)組的最前面。
style scoped 在 vue-loader 中對 style scoped 的處理方式是給每個樣式加一個 attr 來標(biāo)記 module-id,然后在 css 中也給每條 rule 后添加 [module-id],最終可以形成一個 css 的“作用域空間”。
在微信小程序中目前是不支持 attr 選擇器的,所以我們做了一點(diǎn)改動,把 attr 上的 [module-id] 直接寫到了 class 里,如下:
<!-- .vue -->
<template>
<div class="container">
// ...
</div>
</template>
<style scoped>
.container {
color: red;
}
</style>
<!-- vue-loader -->
<template>
<div class="container" data-v-23e58823>
// ...
</div>
</template>
<style scoped>
.container[data-v-23e58823] {
color: red;
}
</style>
<!-- mpvue-loader -->
<template>
<div class="container data-v-23e58823">
// ...
</div>
</template>
<style scoped>
.container.data-v-23e58823 {
color: red;
}
</style>
復(fù)制代碼
|
生產(chǎn)出的內(nèi)容是:
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// mpvue-template-compiler會利用AST預(yù)編譯生成一個render function用以生成Virtual DOM。
var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
// _c創(chuàng)建虛擬節(jié)點(diǎn),參考https://github.com/Meituan-Dianping/mpvue/blob/master/packages/mpvue/index.js#L3606
// 以及https://github.com/Meituan-Dianping/mpvue/blob/master/packages/mpvue/index.js#L3680
return _c('div', {
staticClass: "my-component"
}, [_c('h1', [_vm._v(_vm._s(_vm.msg))]), _vm._v(" "), _c('other-component', {
attrs: {
"msg": _vm.msg,
"mpcomid": '0'
}
})], 1)
}
// staticRenderFns的作用是靜態(tài)渲染,在更新時不會進(jìn)行patch,優(yōu)化性能。而staticRenderFns是個空數(shù)組。
var staticRenderFns = []
render._withStripped = true
var esExports = { render: render, staticRenderFns: staticRenderFns }
/* harmony default export */ __webpack_exports__["a"] = (esExports);
if (false) {
module.hot.accept()
if (module.hot.data) {
require("vue-hot-reload-api").rerender("data-v-54ad9125", esExports)
}
}
/***/ })
復(fù)制代碼
|
compiler相關(guān),也就是template預(yù)編譯這塊,可以參考《 聊聊Vue的template編譯 》來搞明白。原理是一樣的。
mpvue自己實(shí)現(xiàn)了 export { compile, compileToFunctions, compileToWxml } ( 鏈接 )其中 compileToWxml 是用來生成wxml,具體代碼 在這 。
另外mpvue是不需要提供運(yùn)行時-編譯器的,雖然理論上是能夠做到的。因?yàn)樾〕绦虿荒懿僮鱀OM,即便提供了運(yùn)行時-編譯器也產(chǎn)生不了界面。
詳細(xì)講解compile過程:
1.將vue文件解析成模板對象
// mpvue-loader/lib/loader.js var parts = parse(content, fileName, this.sourceMap) 復(fù)制代碼
假如vue文件源碼如下:
<template>
<view class="container-bg">
<view class="home-container">
<home-quotation-view v-for="(item, index) in lists" :key="index" :reason="item.reason" :stockList="item.list" @itemViewClicked="itemViewClicked" />
</view>
</view>
</template>
<script lang="js">
import homeQuotationView from '@/components/homeQuotationView'
import topListApi from '@/api/topListApi'
export default {
data () {
return {
lists: []
}
},
components: {
homeQuotationView
},
methods: {
async loadRankList () {
let {data} = await topListApi.rankList()
if (data) {
this.dateTime = data.dt
this.lists = data.rankList.filter((item) => {
return !!item
})
}
},
itemViewClicked (quotationItem) {
wx.navigateTo({
url: `/pages/topListDetail/main?item=${JSON.stringify(quotationItem)}`
})
}
},
onShow () {
this.loadRankList()
}
}
</script>
<style lang="stylus" scoped>
.container-bg
width 100%
height 100%
background-color #F2F4FA
.home-container
width 100%
height 100%
overflow-x hidden
</style>
復(fù)制代碼
|
調(diào)用 parse(content, fileName, this.sourceMap) 函數(shù)得到的結(jié)果大致如下:
{
template: {
type: 'template',
content: '\n<view class="container-bg">\n <view class="home-container">\n <home-quotation-view v-for="(item, index) in lists" :key="index" :reason="item.reason" :stockList="item.list" @itemViewClicked="itemViewClicked" />\n </view>\n</view>\n',
start: 10,
attrs: {},
end: 251
},
script: {
type: 'script',
content: '\n\n\n\n\n\n\n\n\nimport homeQuotationView from \'@/components/homeQuotationView\'\nimport topListApi from \'@/api/topListApi\'\n\nexport default {\n data () {\n return {\n lists: []\n }\n },\n components: {\n homeQuotationView\n },\n methods: {\n async loadRankList () {\n let {data} = await topListApi.rankList()\n if (data) {\n this.dateTime = data.dt\n this.lists = data.rankList.filter((item) => {\n return !!item\n })\n }\n },\n itemViewClicked (quotationItem) {\n wx.navigateTo({\n url: `/pages/topListDetail/main?item=${JSON.stringify(quotationItem)}`\n })\n }\n },\n onShow () {\n this.loadRankList()\n }\n}\n',
start: 282,
attrs: {
lang: 'js'
},
lang: 'js',
end: 946,
...
},
styles: [{
type: 'style',
content: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.container-bg\n width 100%\n height 100%\n background-color #F2F4FA\n\n.home-container\n width 100%\n height 100%\n overflow-x hidden\n\n',
start: 985,
attrs: [Object],
lang: 'stylus',
scoped: true,
end: 1135,
...
}],
customBlocks: []
}
復(fù)制代碼
|
2.調(diào)用mpvue-loader/lib/template-compiler/index.js導(dǎo)出的接口并傳入上面得到的html模板:
var templateCompilerPath = normalize.lib('template-compiler/index')
...
var defaultLoaders = {
html: templateCompilerPath + templateCompilerOptions,
css: options.extractCSS
? getCSSExtractLoader()
: styleLoaderPath + '!' + 'css-loader' + cssLoaderOptions,
js: hasBuble ? ('buble-loader' + bubleOptions) : hasBabel ? babelLoaderOptions : ''
}
// check if there are custom loaders specified via
// webpack config, otherwise use defaults
var loaders = Object.assign({}, defaultLoaders, options.loaders)
復(fù)制代碼
調(diào)用mpvue/packages/mpvue-template-compiler/build.js的compile接口:
// mpvue-loader/lib/template-compiler/index.js
var compiled = compile(html, compilerOptions)
復(fù)制代碼
compile方法生產(chǎn)下面的ast(Abstract Syntax Tree)模板,render函數(shù)和staticRenderFns
{
ast: {
type: 1,
tag: 'view',
attrsList: [],
attrsMap: {
class: 'container-bg'
},
parent: undefined,
children: [{
type: 1,
tag: 'view',
attrsList: [],
attrsMap: {
class: 'home-container'
},
parent: {
type: 1,
tag: 'view',
attrsList: [],
attrsMap: {
class: 'container-bg'
},
parent: undefined,
children: [
[Circular]
],
plain: false,
staticClass: '"container-bg"',
static: false,
staticRoot: false
},
children: [{
type: 1,
tag: 'home-quotation-view',
attrsList: [{
name: ':reason',
value: 'item.reason'
}, {
name: ':stockList',
value: 'item.list'
}, {
name: '@itemViewClicked',
value: 'itemViewClicked'
}],
attrsMap: {
'v-for': '(item, index) in lists',
':key': 'index',
':reason': 'item.reason',
':stockList': 'item.list',
'@itemViewClicked': 'itemViewClicked',
'data-eventid': '{{\'0-\'+index}}',
'data-comkey': '{{$k}}'
},
parent: [Circular],
children: [],
for: 'lists',
alias: 'item',
iterator1: 'index',
key: 'index',
plain: false,
hasBindings: true,
attrs: [{
name: 'reason',
value: 'item.reason'
}, {
name: 'stockList',
value: 'item.list'
}, {
name: 'eventid',
value: '\'0-\'+index'
}, {
name: 'mpcomid',
value: '\'0-\'+index'
}],
events: {
itemViewClicked: {
value: 'itemViewClicked',
modifiers: undefined
}
},
eventid: '\'0-\'+index',
mpcomid: '\'0-\'+index',
static: false,
staticRoot: false,
forProcessed: true
}],
plain: false,
staticClass: '"home-container"',
static: false,
staticRoot: false
}],
plain: false,
staticClass: '"container-bg"',
static: false,
staticRoot: false
},
render: 'with(this){return _c(\'view\',{staticClass:"container-bg"},[_c(\'view\',{staticClass:"home-container"},_l((lists),function(item,index){return _c(\'home-quotation-view\',{key:index,attrs:{"reason":item.reason,"stockList":item.list,"eventid":\'0-\'+index,"mpcomid":\'0-\'+index},on:{"itemViewClicked":itemViewClicked}})}))])}',
staticRenderFns: [],
errors: [],
tips: []
}
復(fù)制代碼
|
其中的render函數(shù)運(yùn)行的結(jié)果是返回 VNode 對象,其實(shí) render 函數(shù)應(yīng)該長下面這樣:
(function() {
with(this){
return _c('div',{ //創(chuàng)建一個 div 元素
attrs:{"id":"app"} //div 添加屬性 id
},[
_m(0), //靜態(tài)節(jié)點(diǎn) header,此處對應(yīng) staticRenderFns 數(shù)組索引為 0 的 render 函數(shù)
_v(" "), //空的文本節(jié)點(diǎn)
(message) //三元表達(dá)式,判斷 message 是否存在
//如果存在,創(chuàng)建 p 元素,元素里面有文本,值為 toString(message)
?_c('p',[_v("\n "+_s(message)+"\n ")])
//如果不存在,創(chuàng)建 p 元素,元素里面有文本,值為 No message.
:_c('p',[_v("\n No message.\n ")])
]
)
}
})
復(fù)制代碼
其中的 _c 就是vue對象的 createElement 方法 (創(chuàng)建元素), _m 是 renderStatic (渲染靜態(tài)節(jié)點(diǎn)), _v 是 createTextVNode (創(chuàng)建文本dom), _s 是 toString (轉(zhuǎn)換為字符串)
// src/core/instance/render.js
export function initRender (vm: Component) {
...
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
...
}
...
Vue.prototype._s = toString
...
Vue.prototype._m = renderStatic
...
Vue.prototype._v = createTextVNode
...
復(fù)制代碼
// mpvue-loader/lib/template-compiler/index.js compileToWxml.call(this, compiled, html) 復(fù)制代碼
目錄結(jié)構(gòu)
. ├── events.js //解答問題5 ├── index.js //入口提供Vue對象,以及$mount,和各種初始化 ├── liefcycle //解答問題6、7 ├── node-ops.js //操作真實(shí)DOM的相關(guān)實(shí)現(xiàn),因?yàn)樾〕绦虿荒懿僮鱀OM,所以這里都是直接返回 ├── patch.js //解答問題3 └── render.js //解答問題4 復(fù)制代碼
patch.js
和vue使用的 createPatchFunction 保持一致,任然是舊樹和新樹進(jìn)行patch產(chǎn)出diff,但是多了一行this.$updateDataToMP()用以更新。
render.js
兩個核心的方法 initDataToMP 、 updateDataToMP 。
initDataToMP 收集vm上的data,然后調(diào)用小程序Page示例的 setData 渲染。
updateDataToMP 在每次patch,也就是依賴收集發(fā)現(xiàn)數(shù)據(jù)改變時更新(參考patch.js代碼),這部分一樣會使用 nextTick 和隊(duì)列。最終使用了節(jié)流閥 throttleSetData 。50毫秒用來控制頻率以解決頻繁修改Data,會造成大量傳輸Data數(shù)據(jù)而導(dǎo)致的性能問題。
其中 collectVmData 最終也是用到了 formatVmData 。尤其要注意的是一句注釋:
getVmData 這兒獲取當(dāng)前組件內(nèi)的所有數(shù)據(jù),包含 props、computed 的數(shù)據(jù)
我們又知道,service到view是兩個線程間通信,如果Data含有大量數(shù)據(jù),增加了傳輸數(shù)據(jù)量,加大了傳輸成本,將會造成性能下降。
events.js
正如官網(wǎng)所說的,這里使用 eventTypeMap 做了各事件的隱射
import { getComKey, eventTypeMap } from '../util/index'
復(fù)制代碼
// 用于小程序的 event type 到 web 的 event
export const eventTypeMap = {
tap: ['tap', 'click'],
touchstart: ['touchstart'],
touchmove: ['touchmove'],
touchcancel: ['touchcancel'],
touchend: ['touchend'],
longtap: ['longtap'],
input: ['input'],
blur: ['change', 'blur'],
submit: ['submit'],
focus: ['focus'],
scrolltoupper: ['scrolltoupper'],
scrolltolower: ['scrolltolower'],
scroll: ['scroll']
}
復(fù)制代碼
|
使用了 handleProxyWithVue 方法來代理小程序事件到vue事件。
另外看下作者自己對這部分的思路
事件代理機(jī)制:用戶交互觸發(fā)的數(shù)據(jù)更新通過事件代理機(jī)制完成。在 Vue.js 代碼中,事件響應(yīng)函數(shù)對應(yīng)到組件的 method, Vue.js 自動維護(hù)了上下文環(huán)境。然而在小程序中并沒有類似的機(jī)制,又因?yàn)?Vue.js 執(zhí)行環(huán)境中維護(hù)著一份實(shí)時的虛擬 DOM,這與小程序的視圖層完全對應(yīng),我們思考,在小程序組件節(jié)點(diǎn)上觸發(fā)事件后,只要找到虛擬 DOM 上對應(yīng)的節(jié)點(diǎn),觸發(fā)對應(yīng)的事件不就完成了么;另一方面,Vue.js 事件響應(yīng)如果觸發(fā)了數(shù)據(jù)更新,其生命周期函數(shù)更新將自動觸發(fā),在此函數(shù)上同步更新小程序數(shù)據(jù),數(shù)據(jù)同步也就實(shí)現(xiàn)了。
getHandle 這個方法應(yīng)該就是作者思路當(dāng)中所說的:找到對應(yīng)節(jié)點(diǎn),然后找到handle。
lifecycle.js
在 initMP 方法中,自己創(chuàng)建小程序的App、Page。實(shí)現(xiàn)生命周期相關(guān)方法,使用 callHook代理兼容小程序App、Page的生命周期。
官方文檔生命周期中說到了:
同 vue,不同的是我們會在小程序 onReady 后,再去觸發(fā) vue mounted 生命周期
這部分查看, onReady 之后才會執(zhí)行 next ,這個 next 回調(diào)最終是vue的 mountComponent??梢栽?nbsp;index.js 中看到。這部分代碼也就是解決了"小程序生命周期中觸發(fā)vue生命周期"。
export function initMP (mpType, next) {
// ...
global.Page({
// 生命周期函數(shù)--監(jiān)聽頁面初次渲染完成
onReady () {
mp.status = 'ready'
callHook(rootVueVM, 'onReady')
next()
},
})
// ...
}
復(fù)制代碼
在小程序onShow時,使用$nextTick去第一次渲染數(shù)據(jù),參考上面提到的render.js。
export function initMP (mpType, next) {
// ...
global.Page({
// 生命周期函數(shù)--監(jiān)聽頁面顯示
onShow () {
mp.page = this
mp.status = 'show'
callHook(rootVueVM, 'onShow')
// 只有頁面需要 setData
rootVueVM.$nextTick(() => {
rootVueVM._initDataToMP()
})
},
})
// ...
}
復(fù)制代碼
|
在mpvue-loader生成template時,比如點(diǎn)擊事件 @click 會變成 bindtap="handleProxy" ,事件綁定全都會使用 handleProxy 這個方法。
可以查看上面回顧一下。
最終handleProxy調(diào)用的是event.js中的 handleProxyWithVue 。
export function initMP (mpType, next) {
// ...
global.Page({
handleProxy (e) {
return rootVueVM.$handleProxyWithVue(e)
},
})
// ...
}
復(fù)制代碼
|
index.js
最后index.js就負(fù)責(zé)各種初始化和mount。
原因:目前的組件是使用小程序的 template 標(biāo)簽實(shí)現(xiàn)的,給組件指定的class和style是掛載在template標(biāo)簽上,而template 標(biāo)簽不支持 class 及 style 屬性。
解決方案: 在自定義組件上綁定class或style到一個props屬性上。
// 組件ComponentA.vue
<template>
<div class="container" :class="pClass">
...
</div>
</template>
復(fù)制代碼
|
<script>
export default {
props: {
pClass: {
type: String,
default: ''
}
}
}
</script>
復(fù)制代碼
<!--PageB.vue-->
<template>
<component-a :pClass="cusComponentAClass" />
</template>
復(fù)制代碼
<script>
data () {
return {
cusComponentAClass: 'a-class b-class'
}
}
</script>
復(fù)制代碼
<style lang="stylus" scoped>
.a-class
border red solid 2rpx
.b-class
margin-right 20rpx
</style>
復(fù)制代碼
但是這樣會有問題就是style加上scoped之后,編譯模板生成的代碼是下面這樣的:
.a-class.data-v-8f1d914e {
border: #f00 solid 2rpx;
}
.b-class.data-v-8f1d914e {
margin-right 20rpx
}
復(fù)制代碼
所以想要這些組件的class生效就不能使用scoped的style,改成下面這樣,最好自己給a-class和b-class加前綴以防其他的文件引用這些樣式:
<style lang="stylus">
.a-class
border red solid 2rpx
.b-class
margin-right 20rpx
</style>
<style lang="stylus" scoped>
.other-class
border red solid 2rpx
...
</style>
復(fù)制代碼
<!--P組件ComponentA.vue-->
<template>
<div class="container" :style="pStyle">
...
</div>
</template>
復(fù)制代碼
<script>
export default {
props: {
pStyle: {
type: String,
default: ''
}
}
}
</script>
復(fù)制代碼
<!--PageB.vue-->
<template>
<component-a :pStyle="cusComponentAStyle" />
</template>
復(fù)制代碼
<script>
const cusComponentAStyle = 'border:red solid 2rpx; margin-right:20rpx;'
data () {
return {
cusComponentAStyle
}
}
</script>
復(fù)制代碼
<style lang="stylus" scoped> ... </style> 復(fù)制代碼
也可以通過定義styleObject,然后通過工具函數(shù)轉(zhuǎn)化為styleString,如下所示:
const bstyle = {
border: 'red solid 2rpx',
'margin-right': '20rpx'
}
let arr = []
for (let [key, value] of Object.entries(bstyle)) {
arr.push(`${key}: ${value}`)
}
const cusComponentAStyle = arr.join('; ')
復(fù)制代碼
<!--組件ComponentA.vue-->
<template>
<div class="container" :style="{'background-color': backgroundColor}">
...
</div>
</template>
復(fù)制代碼
<script>
export default {
props: {
backgroundColor: {
type: String,
default: 'yellow'
}
}
}
</script>
復(fù)制代碼
<!-- PageB.vue -->
<template>
<component-a backgroundColor="red" />
</template>
復(fù)制代碼
package.json修改
注意事項(xiàng)
移動src/main.js中config相關(guān)內(nèi)容到同級目錄下main.json(新建)中
export default {
// config: {...} 需要移動
}
復(fù)制代碼
to
{
"pages": [
"pages/index/main",
"pages/logs/main"
],
"subPackages": [
{
"root": "pages/packageA",
"pages": [
"counter/main"
]
}
],
"window": {...}
}
復(fù)制代碼
|
build/webpack.base.conf.js
+var CopyWebpackPlugin = require('copy-webpack-plugin')
+var relative = require('relative')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
-function getEntry (rootSrc, pattern) {
- var files = glob.sync(path.resolve(rootSrc, pattern))
- return files.reduce((res, file) => {
- var info = path.parse(file)
- var key = info.dir.slice(rootSrc.length + 1) + '/' + info.name
- res[key] = path.resolve(file)
- return res
- }, {})
+function getEntry (rootSrc) {
+ var map = {};
+ glob.sync(rootSrc + '/pages/**/main.js')
+ .forEach(file => {
+ var key = relative(rootSrc, file).replace('.js', '');
+ map[key] = file;
+ })
+ return map;
}
plugins: [
- new MpvuePlugin()
+ new MpvuePlugin(),
+ new CopyWebpackPlugin([{
+ from: '**/*.json',
+ to: 'app.json'
+ }], {
+ context: 'src/'
+ }),
+ new CopyWebpackPlugin([ // 處理 main.json 里面引用的圖片,不要放代碼中引用的圖片
+ {
+ from: path.resolve(__dirname, '../static'),
+ to: path.resolve(__dirname, '../dist/static'),
+ ignore: ['.*']
+ }
+ ])
]
}
復(fù)制代碼
build/webpack.dev.conf.js
module.exports = merge(baseWebpackConfig, {
devtool: '#source-map',
output: {
path: config.build.assetsRoot,
- filename: utils.assetsPath('js/[name].js'),
- chunkFilename: utils.assetsPath('js/[id].js')
+ filename: utils.assetsPath('[name].js'),
+ chunkFilename: utils.assetsPath('[id].js')
},
plugins: [
new webpack.DefinePlugin({
module.exports = merge(baseWebpackConfig, {
// copy from ./webpack.prod.conf.js
// extract css into its own file
new ExtractTextPlugin({
- filename: utils.assetsPath('css/[name].wxss')
+ filename: utils.assetsPath('[name].wxss')
}),
module.exports = merge(baseWebpackConfig, {
}
}),
new webpack.optimize.CommonsChunkPlugin({
- name: 'vendor',
+ name: 'common/vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.exports = merge(baseWebpackConfig, {
}
}),
new webpack.optimize.CommonsChunkPlugin({
- name: 'manifest',
- chunks: ['vendor']
+ name: 'common/manifest',
+ chunks: ['common/vendor']
}),
- // copy custom static assets
- new CopyWebpackPlugin([
- {
- from: path.resolve(__dirname, '../static'),
- to: config.build.assetsSubDirectory,
- ignore: ['.*']
- }
- ]),
復(fù)制代碼
build/webpack.prod.conf.js
var webpackConfig = merge(baseWebpackConfig, {
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {
path: config.build.assetsRoot,
- filename: utils.assetsPath('js/[name].js'),
- chunkFilename: utils.assetsPath('js/[id].js')
+ filename: utils.assetsPath('[name].js'),
+ chunkFilename: utils.assetsPath('[id].js')
},
plugins: [
var webpackConfig = merge(baseWebpackConfig, {
}),
// extract css into its own file
new ExtractTextPlugin({
- // filename: utils.assetsPath('css/[name].[contenthash].css')
- filename: utils.assetsPath('css/[name].wxss')
+ // filename: utils.assetsPath('[name].[contenthash].css')
+ filename: utils.assetsPath('[name].wxss')
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
var webpackConfig = merge(baseWebpackConfig, {
new webpack.HashedModuleIdsPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
- name: 'vendor',
+ name: 'common/vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
var webpackConfig = merge(baseWebpackConfig, {
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
- name: 'manifest',
- chunks: ['vendor']
- }),
+ name: 'common/manifest',
+ chunks: ['common/vendor']
+ })
- // copy custom static assets
- new CopyWebpackPlugin([
- {
- from: path.resolve(__dirname, '../static'),
- to: config.build.assetsSubDirectory,
- ignore: ['.*']
- }
- ])
]
})
復(fù)制代碼
config/index.js
module.exports = {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
- assetsSubDirectory: 'static', // 不將資源聚合放在 static 目錄下
+ assetsSubDirectory: '',
assetsPublicPath: '/',
productionSourceMap: false,
// Gzip off by default as many popular static hosts such as
@@ -26,7 +26,7 @@ module.exports = {
port: 8080,
// 在小程序開發(fā)者工具中不需要自動打開瀏覽器
autoOpenBrowser: false,
- assetsSubDirectory: 'static', // 不將資源聚合放在 static 目錄下
+ assetsSubDirectory: '',
assetsPublicPath: '/',
proxyTable: {},
// CSS Sourcemaps off by default because relative paths are "buggy"
復(fù)制代碼