該內容由銀科控股融匯研發部曹俊及其團隊受權提供。該團隊擁有 10 多名小程序開發,深耕小程序領域,總結出了本篇優質長文。同時本篇內容也已經合併入個人 開源項目 中,目前項目內容包含了 JS、網絡、瀏覽器相關、性能優化、安全、框架、Git、數據結構、算法等內容,不管是基礎仍是進階,亦或是源碼解讀,你都能在本圖譜中獲得滿意的答案,但願這個面試圖譜可以幫助到你們更好的準備面試。javascript
瞭解小程序登錄以前,咱們寫了解下小程序/公衆號登陸涉及到兩個最關鍵的用戶標識:css
OpenId
是一個用戶對於一個小程序/公衆號的標識,開發者能夠經過這個標識識別出用戶。UnionId
是一個用戶對於同主體微信小程序/公衆號/APP的標識,開發者須要在微信開放平臺下綁定相同帳號的主體。開發者可經過UnionId,實現多個小程序、公衆號、甚至APP 之間的數據互通了。wx.login
官方提供的登陸能力html
wx.checkSession
校驗用戶當前的session_key是否有效前端
wx.authorize
提早向用戶發起受權請求vue
wx.getUserInfo
獲取用戶基本信息java
如下從筆者接觸過的幾種登陸流程來作闡述:node
直接複用現有系統的登陸體系,只須要在小程序端設計用戶名,密碼/驗證碼輸入頁面,即可以簡便的實現登陸,只須要保持良好的用戶體驗便可。webpack
👆提過,OpenId
是一個小程序對於一個用戶的標識,利用這一點咱們能夠輕鬆的實現一套基於小程序的用戶體系,值得一提的是這種用戶體系對用戶的打擾最低,能夠實現靜默登陸。具體步驟以下:git
小程序客戶端經過 wx.login
獲取 codegithub
傳遞 code 向服務端,服務端拿到 code 調用微信登陸憑證校驗接口,微信服務器返回 openid
和會話密鑰 session_key
,此時開發者服務端即可以利用 openid
生成用戶入庫,再向小程序客戶端返回自定義登陸態
小程序客戶端緩存 (經過storage
)自定義登陸態(token),後續調用接口時攜帶該登陸態做爲用戶身份標識便可
若是想實現多個小程序,公衆號,已有登陸系統的數據互通,能夠經過獲取到用戶 unionid 的方式創建用戶體系。由於 unionid 在同一開放平臺下的所全部應用都是相同的,經過 unionid
創建的用戶體系便可實現全平臺數據的互通,更方便的接入原有的功能,那如何獲取 unionid
呢,有如下兩種方式:
若是戶關注了某個相同主體公衆號,或曾經在某個相同主體App、公衆號上進行過微信登陸受權,經過 wx.login
能夠直接獲取 到 unionid
結合 wx.getUserInfo
和 <button open-type="getUserInfo"><button/>
這兩種方式引導用戶主動受權,主動受權後經過返回的信息和服務端交互 (這裏有一步須要服務端解密數據的過程,很簡單,微信提供了示例代碼) 便可拿到 unionid
創建用戶體系, 而後由服務端返回登陸態,本地記錄便可實現登陸,附上微信提供的最佳實踐:
調用 wx.login 獲取 code,而後從微信後端換取到 session_key,用於解密 getUserInfo返回的敏感數據。
使用 wx.getSetting 獲取用戶的受權狀況
獲取到用戶數據後能夠進行展現或者發送給本身的後端。
unionid
形式的登陸體系,在之前(18年4月以前)是經過如下這種方式來實現,但後續微信作了調整(由於一進入小程序,主動彈起各類受權彈窗的這種形式,比較容易致使用戶流失),調整爲必須使用按鈕引導用戶主動受權的方式,此次調整對開發者影響較大,開發者須要注意遵照微信的規則,並及時和業務方溝通業務形式,不要存在僥倖心理,以防形成小程序不過審等狀況。wx.login(獲取code) ===> wx.getUserInfo(用戶受權) ===> 獲取 unionid
複製代碼
由於小程序不存在 cookie
的概念, 登陸態必須緩存在本地,所以強烈建議爲登陸態設置過時時間
值得一提的是若是須要支持風控安全校驗,多平臺登陸等功能,可能須要加入一些公共參數,例如platform,channel,deviceParam等參數。在和服務端肯定方案時,做爲前端同窗應該及時提出這些合理的建議,設計合理的系統。
openid
, unionid
不要在接口中明文傳輸,這是一種危險的行爲,同時也很不專業。
常常開發和使用小程序的同窗對這個功能必定不陌生,這是一種常見的引流方式,通常同時會在圖片中附加一個小程序二維碼。
藉助 canvas
元素,將須要導出的樣式首先在 canvas
畫布上繪製出來 (api基本和h5保持一致,但有輕微差別,使用時注意便可)
藉助微信提供的 canvasToTempFilePath
導出圖片,最後再使用 saveImageToPhotosAlbum
(須要受權)保存圖片到本地
根據上述的原理來看,實現是很簡單的,只不過就是設計稿的提取,繪製便可,可是做爲一個經常使用功能,每次都這樣寫一坨代碼豈不是很是的難受。那小程序如何設計一個通用的方法來幫助咱們導出圖片呢?思路以下:
繪製出須要的樣式這一步是省略不掉的。可是咱們能夠封裝一個繪製庫,包含常見圖形的繪製,例如矩形,圓角矩形,圓, 扇形, 三角形, 文字,圖片減小繪製代碼,只須要提煉出樣式信息,即可以輕鬆的繪製,最後導出圖片存入相冊。筆者以爲如下這種方式繪製更爲優雅清晰一些,其實也可使用加入一個type參數來指定繪製類型,傳入的一個是樣式數組,實現繪製。
結合上一步的實現,若是對於同一類型的卡片有屢次導出需求的場景,也可使用自定義組件的方式,封裝同一類型的卡片爲一個通用組件,在須要導出圖片功能的地方,引入該組件便可。
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()
複製代碼
短連接
的方式來解決數據統計做爲目前一種經常使用的分析用戶行爲的方式,小程序端也是必不可少的。小程序採起的曝光,點擊數據埋點其實和h5原理是同樣的。可是埋點做爲一個和業務邏輯不相關的需求,咱們若是在每個點擊事件,每個生命週期加入各類埋點代碼,則會干擾正常的業務邏輯,和使代碼變的臃腫,筆者提供如下幾種思路來解決數據埋點:
小程序的代碼結構是,每個 Page 中都有一個 Page 方法,接受一個包含生命週期函數,數據的 業務邏輯對象
包裝這層數據,藉助小程序的底層邏輯實現頁面的業務邏輯。經過這個咱們能夠想到思路,對Page進行一次包裝,篡改它的生命週期和點擊事件,混入埋點代碼,不干擾業務邏輯,只要作一些簡單的配置便可埋點,簡單的代碼實現以下:
代碼僅供理解思路
page = function(params) {
let keys = params.keys()
keys.forEach(v => {
if (v === 'onLoad') {
params[v] = function(options) {
stat() //曝光埋點代碼
params[v].call(this, options)
}
}
else if (v.includes('click')) {
params[v] = funciton(event) {
let data = event.dataset.config
stat(data) // 點擊埋點
param[v].call(this)
}
}
})
}
複製代碼
這種思路不光適用於埋點,也能夠用來做全局異常處理,請求的統一處理等場景。
對於特殊的一些業務,咱們能夠採起 接口埋點
,什麼叫接口埋點呢?不少狀況下,咱們有的api並非多處調用的,只會在某一個特定的頁面調用,經過這個思路咱們能夠分析出,該接口被請求,則這個行爲被觸發了,則徹底能夠經過服務端日誌得出埋點數據,可是這種方式侷限性較大,並且屬於分析結果得出過程,可能存在偏差,但能夠做爲一種思路瞭解一下。
微信自己提供的數據分析能力,微信自己提供了常規分析和自定義分析兩種數據分析方式,在小程序後臺配置便可。藉助小程序數據助手
這款小程序能夠很方便的查看。
目前的前端開發過程,工程化是必不可少的一環,那小程序工程化都須要作些什麼呢,先看下目前小程序開發當中存在哪些問題須要解決:
對於目前經常使用的工程化方案,webpack,rollup,parcel等來看,都經常使用與單頁應用的打包和處理,而小程序天生是 「多頁應用」 而且存在一些特定的配置。根據要解決的問題來看,無非是文件的編譯,修改,拷貝這些處理,對於這些需求,咱們想到基於流的 gulp
很是的適合處理,而且相對於webpack配置多頁應用更加簡單。因此小程序工程化方案推薦使用 gulp
經過 gulp 的 task 實現:
上述實現起來其實並非很難,可是這樣的話就是一份純粹的 gulp 構建腳本和 約定好的目錄而已,每次都有一個新的小程序都來拷貝這份腳原本處理嗎?顯然不合適,那如何真正的實現 小程序工程化
呢? 咱們可能須要一個簡單的腳手架,腳手架須要支持的功能:
微信小程序的框架包含兩部分 View 視圖層、App Service邏輯層。View 層用來渲染頁面結構,AppService 層用來邏輯處理、數據請求、接口調用。
它們在兩個線程裏運行。
它們在兩個線程裏運行。
它們在兩個線程裏運行。
視圖層和邏輯層經過系統層的 JSBridage 進行通訊,邏輯層把數據變化通知到視圖層,觸發視圖層頁面更新,視圖層把觸發的事件通知到邏輯層進行業務處理。
補充
視圖層使用 WebView 渲染,iOS 中使用自帶 WKWebView,在 Android 使用騰訊的 x5 內核(基於 Blink)運行。
邏輯層使用在 iOS 中使用自帶的 JSCore 運行,在 Android 中使用騰訊的 x5 內核(基於 Blink)運行。
開發工具使用 nw.js 同時提供了視圖層和邏輯層的運行環境。
在 Mac下 使用 js-beautify 對微信開發工具 @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 '{}' \;
複製代碼
在 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) {}
}
複製代碼
在 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
}
},
複製代碼
在 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()
})
},
複製代碼
咱們都看到了 WeixinJSBridge 的定義。分別都有 on
、invoke
、publish
、subscribe
這個幾個關鍵方法。
拿 invoke
舉例,在 js/extensions/appservice/index.js
中發現這段代碼:
f (!r) p[b] = s, f.send({
command: 'APPSERVICE_INVOKE',
data: {
api: c,
args: e,
callbackID: b
}
});
複製代碼
在 js/extensions/pageframe/index.js
中發現這段代碼:
g[d] = c, e.a.send({
command: 'WEBVIEW_INVOKE',
data: {
api: a,
args: b,
callbackID: d
}
})
複製代碼
簡單的分析得知:字段 command
用來區分行爲,invoke
用來調用 Native 的 Api。在不一樣的來源要使用不一樣的前綴。data
裏面包含 Api 名,參數。另外 callbackID
指定接受回調的方法句柄。Appservice 和 Webview 使用的通訊協議是一致的。
咱們不能在代碼裏使用 BOM 和 DOM 是由於根本沒有,另外一方面也不但願 JS 代碼直接操做視圖。
在開發工具中 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;
}
複製代碼
這樣的分層設計顯然是有意爲之的,它的中間層徹底控制了程序對於界面進行的操做, 同時對於傳遞的數據和響應時間也能作到監控。一方面程序的行爲受到了極大限制, 另外一方面微信能夠確保他們對於小程序內容和體驗有絕對的控制。
這樣的結構也說明了小程序的動畫和繪圖 API 被設計成生成一個最終對象而不是一步一步執行的樣子, 緣由就是 Json 格式的數據傳遞和解析相比與原生 API 都是損耗不菲的,若是頻繁調用極可能損耗過多性能,進而影響用戶體驗。
1.動畫須要綁定在 data 上,而繪圖卻不用。你以爲是爲何呢?
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()
複製代碼
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()
})
}
})
複製代碼
2.小程序的 Http Rquest 請求是否是用的瀏覽器 Fetch API?
知識點考察
wx.request
是否是遵循 fetch API 規範實現的呢?答案,顯然不是。由於沒有 Promise
WXML(WeiXin Markup Language)
Wxml編譯器:Wcc 把 Wxml文件 轉爲 JS
執行方式:Wcc index.wxml
使用 Virtual DOM,進行局部更新
WXSS(WeiXin Style Sheets)
wxss編譯器:wcsc 把wxss文件轉化爲 js
執行方式: wcsc index.wxss
親測包含但不限於以下內容:
建議 Css3 的特性均可以作一下嘗試。
rpx(responsive pixel): 能夠根據屏幕寬度進行自適應。規定屏幕寬爲 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
}
複製代碼
設備 | rpx換算px (屏幕寬度/750) | px換算rpx (750/屏幕寬度) |
---|---|---|
iPhone5 | 1rpx = 0.42px | 1px = 2.34rpx |
iPhone6 | 1rpx = 0.5px | 1px = 2rpx |
iPhone6 Plus | 1rpx = 0.552px | 1px = 1.81rpx |
能夠了解一下 pr2rpx-loader 這個庫。
使用 @import
語句能夠導入外聯樣式表,@import
後跟須要導入的外聯樣式表的相對路徑,用 ;
表示語句結束。
靜態的樣式統一寫到 class 中。style 接收動態的樣式,在運行時會進行解析,請儘可能避免將靜態的樣式寫進 style 中,以避免影響渲染速度。
定義在 app.wxss 中的樣式爲全局樣式,做用於每個頁面。在 page 的 wxss 文件中定義的樣式爲局部樣式,只做用在對應的頁面,並會覆蓋 app.wxss 中相同的選擇器。
截止20180810
小程序將來有計劃支持字體。參考微信公開課。
小程序開發與平時 Web開發相似,也可使用字體圖標,可是 src:url()
不管本地仍是遠程地址都不行,base64 值則都是能夠顯示的。
將 ttf 文件轉換成 base64。打開這個平臺 transfonter.org/。點擊 Add fonts 按鈕,加載ttf格式的那個文件。將下邊的 base64 encode 改成 on。點擊 Convert 按鈕進行轉換,轉換後點擊 download 下載。
複製下載的壓縮文件中的 stylesheet.css 的內容到 font.wxss ,而且將 icomoon 中的 style.css 除了 @font-face 全部的代碼也複製到 font.wxss 並將i選擇器換成 .iconfont,最後:
<text class="iconfont icon-home" style="font-size:50px;color:red"></text>
複製代碼
小程序提供了一系列組件用於開發業務功能,按照功能與HTML5的標籤進行對好比下:
小程序的組件基於Web Component標準
使用Polymer框架實現Web Component
目前Native實現的組件有
cavnas
video
map
textarea
Native組件層在 WebView 層之上。這目前帶來了一些問題:
cover-view
能夠覆蓋 cavnas video 等,可是也有一下弊端,好比在 cavnas 上覆蓋 cover-view
,就會發現座標系不統一處理麻煩截止20180810
包含但不限於:
小程序仍然使用 WebView 渲染,並不是原生渲染。(部分原生)
服務端接口返回的頭沒法執行,好比:Set-Cookie。
依賴瀏覽器環境的 JS 庫不能使用。
不能使用 npm,可是能夠自搭構建工具或者使用 mpvue。(將來官方有計劃支持)
不能使用 ES7,能夠本身用babel+webpack自搭或者使用 mpvue。
不支持使用本身的字體(將來官方計劃支持)。
能夠用 base64 的方式來使用 iconfont。
小程序不能發朋友圈(能夠經過保存圖片到本地,發圖片到朋友前。二維碼可使用B接口)。
獲取二維碼/小程序接口的限制。
小程序推送只能使用「服務通知」 並且須要用戶主動觸發提交 formId,formId 只有7天有效期。(如今的作法是在每一個頁面都放入form而且隱藏以此獲取更多的 formId。後端使用原則爲:優先使用有效期最短的)
小程序大小限制 2M,分包總計不超過 8M
轉發(分享)小程序不能拿到成功結果,原來能夠。連接(小遊戲造的孽)
拿到相同的 unionId 必須綁在同一個開放平臺下。開放平臺綁定限制:
公衆號關聯小程序,連接
一個公衆號關聯的10個同主體小程序和3個非同主體小程序能夠互相跳轉
品牌搜索不支持金融、醫療
小程序受權須要用戶主動點擊
小程序不提供測試 access_token
安卓系統下,小程序受權獲取用戶信息以後,刪除小程序再從新獲取,並從新受權,獲得舊簽名,致使第一次受權失敗
開發者工具上,受權獲取用戶信息以後,若是清緩存選擇所有清除,則即便使用了wx.checkSession,而且在session_key有效期內,受權獲取用戶信息也會獲得新的session_key
爲了驗證小程序對HTTP的支持適配狀況,我找了兩個服務器作測試,一個是網上搜索到支持HTTP2的服務器,一個是我本地起的一個HTTP2服務器。測試中全部請求方法均使用 wx.request
。
網上支持HTTP2的服務器:HTTPs://www.snel.com:443
在Chrome上查看該服務器爲 HTTP2
在模擬器上請求該接口,請求頭
的HTTP版本爲HTTP1.1,模擬器不支持HTTP2
因爲小程序線上環境須要在項目管理裏配置請求域名,而這個域名不是咱們須要的請求域名,不必浪費一個域名位置,因此打開不驗證域名,TSL 等選項請求該接口,經過抓包工具表現與模擬器相同
由上能夠看出,在真機與模擬器都不支持 HTTP2,可是都是成功請求的,而且 響應頭
裏的 HTTP 版本都變成了HTTP1.1 版本,說明服務器對 HTTP1.1 作了兼容性適配。
本地新啓一個 node 服務器,返回 JSON 爲請求的 HTTP 版本
若是服務器只支持 HTTP2,在模擬器請求時發生了一個 ALPN
協議的錯誤。而且提醒使用適配 HTTP1
當把服務器的 allowHTTP1
,設置爲 true
,並在請求時處理相關相關請求參數後,模擬器能正常訪問接口,並打印出對應的 HTTP 請求版本
面試題:先受權獲取用戶信息再 login 會發生什麼?
咱們知道view部分是運行在webview上的,因此前端領域的大多數優化方式都有用。
咱們知道view部分是運行在webview上的,因此前端領域的大多數優化方式都有用。
咱們知道view部分是運行在webview上的,因此前端領域的大多數優化方式都有用。
代碼包的大小是最直接影響小程序加載啓動速度的因素。代碼包越大不只下載速度時間長,業務代碼注入時間也會變長。因此最好的優化方式就是減小代碼包的大小。
小程序加載的三個階段的表示。
優化方式
首屏加載的體驗優化建議
在構建小程序分包項目時,構建會輸出一個或多個功能的分包,其中每一個分包小程序一定含有一個主包,所謂的主包,即放置默認啓動頁面/TabBar 頁面,以及一些全部分包都需用到公共資源/JS 腳本,而分包則是根據開發者的配置進行劃分。
在小程序啓動時,默認會下載主包並啓動主包內頁面,若是用戶須要打開分包內某個頁面,客戶端會把對應分包下載下來,下載完成後再進行展現。
優勢:
限制:
原生分包加載的配置 假設支持分包的小程序目錄結構以下:
├── app.js
├── app.json
├── app.wxss
├── packageA
│ └── pages
│ ├── cat
│ └── dog
├── packageB
│ └── pages
│ ├── apple
│ └── banana
├── pages
│ ├── index
│ └── logs
└── utils
複製代碼
開發者經過在 app.json subPackages 字段聲明項目分包結構:
{
"pages":[
"pages/index",
"pages/logs"
],
"subPackages": [
{
"root": "packageA",
"pages": [
"pages/cat",
"pages/dog"
]
}, {
"root": "packageB",
"pages": [
"pages/apple",
"pages/banana"
]
}
]
}
複製代碼
分包原則
引用原則
官方即將推出 分包預加載
獨立分包
每次 setData 的調用都是一次進程間通訊過程,通訊開銷與 setData 的數據量正相關。
setData 會引起視圖層頁面內容的更新,這一耗時操做必定時間中會阻塞用戶交互。
setData 是小程序開發使用最頻繁,也是最容易引起性能問題的。
避免不當使用 setData
避免不當使用onPageScroll
使用自定義組件
在須要頻繁更新的場景下,自定義組件的更新只在組件內部進行,不受頁面其餘部份內容複雜性影響。
小程序的幾個頁面間,存在一些相同或是相似的區域,這時候能夠把這些區域邏輯封裝成一個自定義組件,代碼就能夠重用,或者對於比較獨立邏輯,也能夠把它封裝成一個自定義組件,也就是微信去年發佈的自定義組件,它讓代碼獲得複用、減小代碼量,更方便模塊化,優化代碼架構組織,也使得模塊清晰,後期更好地維護,從而保證更好的性能。
但微信打算在原來的基礎上推出的自定義組件 2.0,它將擁有更高級的性能:
目前小程序開發的痛點是:開源組件要手動複製到項目,後續更新組件也須要手動操做。不久的未來,小程序將支持npm包管理,有了這個以後,想要引入一些開源的項目就變得很簡單了,只要在項目裏面聲明,而後用簡單的命令安裝,就可使用了。
微信小程序團隊表示,他們在考慮推出一些官方自定義組件,爲何不內置到基礎庫裏呢?由於內置組件要提供給開發者,這個組件必定是開發者很難實現或者是沒法實現的一個能力。因此他們更傾向於封裝成自定義組件,想基於這些內置組件裏,封裝一些比較常見的、交互邏輯比較複雜的組件給你們使用,讓你們更容易開發。相似彈幕組件,開發者就不用關注彈幕怎麼飄,能夠節省開發者的開發成本。
同時,他們也想給開發者提供一些規範和一些模板,讓開發者設計出好用的自定義組件,更好地被你們去使用。
當小程序加載太慢時,可能會致使用戶的流失,而小程序的開發者可能會面臨着不知道如何定位問題或不知如何解決問題的困境。
爲此,小程序即將推出一個體驗評分的功能,這是爲了幫助開發者能夠檢查出小程序有一些什麼體驗很差的地方,也會同時給出一份優化的指引建議。
小程序在最初的技術選型時,引入了原生組件的概念,由於原生組件可使小程序的能力更加豐富,好比地圖、音視頻的能力,可是原生組件是由客戶端原生渲染的,致使了原生組件的層級是最高的,開發者很容易遇到打開調試的問題,發現視頻組件擋在了 vConsole 上。
爲了解決這個問題,當時微信作了一個過渡的方案:cover-view。cover-view能夠覆蓋在原生組件之上,這一套方案解決了大部分的需求場景。好比說視頻組件上不少的按鈕、標題甚至還有動畫的彈幕,這些都是用 cover-view 去實現的,但它仍是沒有徹底解決原生組件的開發體驗問題,由於 cover-view 有一些限制:
所以微信決定將用同層渲染取代 cover-view,它能像普通組件同樣使用,原生組件的層級再也不是最高,而是和其餘的非原生組件在同一層級渲染,可徹底由 z-index 控制,可徹底支持觸摸事件。
微信表示,同層渲染在 iOS 平臺小程序上已經開始內測,會很快開放給開發者,Android 平臺已經取得突破性進展,目前正在作一輪封裝的工做,開放指日可待。
相比傳統的小程序框架,這個一直是咱們做爲資深開發者比較指望去解決的,在 Web 開發中,隨着 Flux、Redux、Vuex 等多個數據流工具出現,咱們也指望在業務複雜的小程序中使用。
WePY 默認支持 Redux,在腳手架生成項目的時候能夠內置
Mpvue 做爲 Vue 的移植版本,固然支持 Vuex,一樣在腳手架生成項目的時候能夠內置
若是你和咱們同樣,經歷了從無到有的小程序業務開發,建議閱讀【小程序的組件化開發】章節,進行官方語法的組件庫開發(從基礎庫 1.6.3 開始,官方提供了組件化解決方案)。
export default class Index extends wepy.page {}
複製代碼
全部的小程序開發依賴官方提供的開發者工具。開發者工具簡單直觀,對調試小程序頗有幫助,如今也支持騰訊雲(目前咱們尚未使用,可是對新的一些開發者仍是有幫助的),能夠申請測試報告查看小程序在真實的移動設備上運行性能和運行效果,可是它自己沒有相似前端工程化中的概念和工具。
對比\框架 | 微信小程序 | mpvue | wepy |
---|---|---|---|
語法規範 | 小程序開發規範 | vue.js | 類vue.js |
標籤集合 | 小程序 | htm l + 小程序 | 小程序 |
樣式規範 | wxss | sass,less,postcss | sass,less,styus |
組件化 | 無組件化機制 | vue規範 | 自定義組件規範 |
多段複用 | 不可複用 | 支持h5 | 支持h5 |
自動構建 | 無自動構建 | webpack | 框架內置 |
上手成本 | 全新學習 | vue 學習 | vue 和 wepy |
數據管理 | 不支持 | vuex | redux |
先說結論:選擇 mpvue。
wepy vs mpvue。
理由:
工程化 原生開發由於不帶工程化,諸如NPM包(將來會引入)、ES七、圖片壓縮、PostCss、pug、ESLint等等不能用。若是本身要搭工程化,不如直接使用wepy或mpvue。mpvue和wepy均可以和小程序原生開發混寫。參考mpvue-echart,參考wepy。 而問題在於wepy沒有引入webpack(wepy@2.0.x依然沒有引入),以上說的這些東西都要造輪子(做者造或本身造)。沒有引入 Webpack 是一個重大的硬傷。社區維護的成熟 Webpack 顯然更穩定,輪子更多。
維護 wepy 也是社區維護的,是官方的?其實 wepy 的主要開發者只有做者一人,附上一個contrubutors連接。另外被官方招安了也是後來的事,再說騰訊要有精力幫着一塊兒維護好 wepy,爲何不花精力在小程序原生開發上呢?再來看看 mpvue,是美團一個前端小組維護的。
學習成本 Vue 的學習曲線比較平緩。mpvue 是 Vue的子集。因此 mpvue 的學習成本會低於 wepy。尤爲是以前技術棧有學過用過 Vue 的。
將來規劃 mpvue 已經支持 web 和小程序。由於 mpvue 基於AST,因此將來能夠支持支付寶小程序和快應用。他們也是有這樣的規劃。
請在需求池下面本身找
坑 二者都有各自的坑。可是我以爲有一些wepy的坑是無法容忍的。好比repeat組建裏面用computed獲得的列表全是同一套數據並且1.x是無法解決的。 wepy和mpvue我都開發過完整小程序的體驗下,我以爲wepy的坑更多,並且wepy有些坑礙於架構設計沒辦法解決。
Vue.js 小程序版, fork 自 vuejs/vue@2.4.1,保留了 vue runtime 能力,添加了小程序平臺的支持。
mpvue
是一個使用Vue.js
開發小程序的前端框架。框架基於Vue.js
核心,mpvue
修改了Vue.js
的 runtime 和 compiler 實現,使其能夠運行在小程序環境中,從而爲小程序開發引入了整套Vue.js
開發體驗。
兩個大方向
mpvue
提供 mp 的 runtime 適配小程序mpvue-loader
產出微信小程序所須要的文件結構和模塊內容。七個具體問題
要了解 mpvue 原理必然要了解 Vue 原理,這是大前提。可是要講清楚 Vue 原理須要花費大量的篇幅,不如參考learnVue。
如今假設您對 Vue 原理有個大概的瞭解。
因爲 Vue 使用了 Virtual DOM,因此 Virtual DOM 能夠在任何支持 JavaScript 語言的平臺上操做,譬如說目前 Vue 支持瀏覽器平臺或 weex,也能夠是 mp(小程序)。那麼最後 Virtual DOM 如何映射到真實的 DOM 節點上呢?vue爲平臺作了一層適配層,瀏覽器平臺見 runtime/node-ops.js、weex平臺見runtime/node-ops.js,小程序見runtime/node-ops.js。不一樣平臺之間經過適配層對外提供相同的接口,Virtual DOM進行操做Real DOM節點的時候,只須要調用這些適配層的接口便可,而內部實現則不須要關心,它會根據平臺的改變而改變。
因此思路確定是往增長一個 mp 平臺的 runtime 方向走。但問題是小程序不能操做 DOM,因此 mp 下的node-ops.js
裏面的實現都是直接 return obj
。
新 Virtual DOM 和舊 Virtual DOM 之間須要作一個 patch,找出 diff。patch完了以後的 diff 怎麼更新視圖,也就是如何給這些 DOM 加入 attr、class、style 等 DOM 屬性呢? Vue 中有 nextTick 的概念用以更新視圖,mpvue這塊對於小程序的 setData
應該怎麼處理呢?
另外個問題在於小程序的 Virtual DOM 怎麼生成?也就是怎麼將 template 編譯成render function
。這當中還涉及到運行時-編譯器-vs-只包含運行時,顯然若是要提升性能、減小包大小、輸出 wxml、mpvue 也要提供預編譯的能力。由於要預輸出 wxml 且無法動態改變 DOM,因此動態組件,自定義 render,和<script type="text/x-template">
字符串模版等都不支持(參考)。
另外還有一些其餘問題,最後總結一下
render function
.
├── compiler //解決問題1,mpvue-template-compiler源碼部分
├── runtime //解決問題3 4 5 6 7
├── util //工具方法
├── entry-compiler.js //mpvue-template-compiler的入口。package.json相關命令會自動生成mpvue-template-compiler這個package。
├── entry-runtime.js //對外提供Vue對象,固然是mpvue
└── join-code-in-build.js //編譯出SDK時的修復
複製代碼
後面的內容逐步解答這幾個問題,也就弄明白了原理
mpvue-loader 是 vue-loader 的一個擴展延伸版,相似於超集的關係,除了vue-loader 自己所具有的能力以外,它還會利用mpvue-template-compiler生成render function
。
它會從 webpack
的配置中的 entry 開始,分析依賴模塊,並分別打包。在entry 中 app 屬性及其內容會被打包爲微信小程序所須要的 app.js/app.json/app.wxss,其他的會生成對應的頁面page.js/page.json/page.wxml/page.wxss,如示例的 entry 將會生成以下這些文件,文件內容下文慢慢講來:
// 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')
}
}
// 產出文件的結構
.
├── 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
複製代碼
.vue
的組件都會被生成爲一個 wxml 規範的 template,而後經過 wxml 規範的 import 語法來達到一個複用,同時組件若是涉及到 props 的 data 數據,咱們也會作相應的處理,舉個實際的例子:<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>
複製代碼
這樣一個 Vue 的組件的模版部分會生成相應的 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>
複製代碼
可能已經注意到了 other-component(:msg="msg") 被轉化成了
。mpvue 在運行時會從根組件開始把全部的組件實例數據合併成一個樹形的數據,而後經過 setData 到 appData,$c
是 $children 的縮寫。至於那個 0 則是咱們的 compiler 處理事後的一個標記,會爲每個子組件打一個特定的不重複的標記。 樹形數據結構以下:
// 這兒數據結構是一個數組,index 是動態的
{
$child: {
'0'{
// ... root data
$child: {
'0': {
// ... data
msg: 'Hello Vue.js!',
$child: {
// ...data
}
}
}
}
}
}
複製代碼
這個部分的處理同 web 的處理差別不大,惟一不一樣在於經過配置生成 .css 爲 .wxss ,其中的對於 css 的若干處理,在 postcss-mpvue-wxss 和 px2rpx-loader 這兩部分的文檔中又詳細的介紹。
app.json/page.json 1.1.1 以上
推薦和小程序同樣,將 app.json/page.json 放到頁面入口處,使用 copy-webpack-plugin copy 到對應的生成位置。
1.1.1 如下
這部份內容來源於 app 和 page 的 entry 文件,一般習慣是 main.js,你須要在你的入口文件中 export default { config: {} },這才能被咱們的 loader 識別爲這是一個配置,須要寫成 json 文件。
import Vue from 'vue';
import App from './app';
const vueApp = new Vue(App);
vueApp.$mount();
// 這個是咱們約定的額外的配置
export default {
// 這個字段下的數據會被填充到 app.json / page.json
config: {
pages: ['static/calendar/calendar', '^pages/list/list'], // Will be filled in webpack
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#455A73',
navigationBarTitleText: '美團汽車票',
navigationBarTextStyle: '#fff'
}
}
};
複製代碼
同時,這個時候,咱們會根據 entry 的頁面數據,自動填充到 app.json 中的 pages 字段。 pages 字段也是能夠自定義的,約定帶有 ^ 符號開頭的頁面,會放到數組的最前面。
style scoped 在 vue-loader 中對 style scoped 的處理方式是給每一個樣式加一個 attr 來標記 module-id,而後在 css 中也給每條 rule 後添加 [module-id],最終能夠造成一個 css 的「做用域空間」。
在微信小程序中目前是不支持 attr 選擇器的,因此咱們作了一點改動,把 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>
複製代碼
生產出的內容是:
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// mpvue-template-compiler會利用AST預編譯生成一個render function用以生成Virtual DOM。
var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
// _c建立虛擬節點,參考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的做用是靜態渲染,在更新時不會進行patch,優化性能。而staticRenderFns是個空數組。
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)
}
}
/***/ })
複製代碼
compiler相關,也就是template預編譯這塊,能夠參考《聊聊Vue的template編譯》來搞明白。原理是同樣的。
mpvue本身實現了export { compile, compileToFunctions, compileToWxml }
(連接)其中compileToWxml
是用來生成wxml,具體代碼在這。
另外mpvue是不須要提供運行時-編譯器的,雖然理論上是可以作到的。由於小程序不能操做DOM,即使提供了運行時-編譯器也產生不了界面。
詳細講解compile過程:
1.將vue文件解析成模板對象
// mpvue-loader/lib/loader.js
var parts = parse(content, fileName, this.sourceMap)
複製代碼
假如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>
複製代碼
調用parse(content, fileName, this.sourceMap)
函數獲得的結果大體以下:
{
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: []
}
複製代碼
2.調用mpvue-loader/lib/template-compiler/index.js導出的接口並傳入上面獲得的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)
複製代碼
// mpvue-loader/lib/template-compiler/index.js
var compiled = compile(html, compilerOptions)
複製代碼
compile方法生產下面的ast(Abstract Syntax Tree)模板,render函數和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: []
}
複製代碼
其中的render函數運行的結果是返回VNode
對象,其實render
函數應該長下面這樣:
(function() {
with(this){
return _c('div',{ //建立一個 div 元素
attrs:{"id":"app"} //div 添加屬性 id
},[
_m(0), //靜態節點 header,此處對應 staticRenderFns 數組索引爲 0 的 render 函數
_v(" "), //空的文本節點
(message) //三元表達式,判斷 message 是否存在
//若是存在,建立 p 元素,元素裏面有文本,值爲 toString(message)
?_c('p',[_v("\n "+_s(message)+"\n ")])
//若是不存在,建立 p 元素,元素裏面有文本,值爲 No message.
:_c('p',[_v("\n No message.\n ")])
]
)
}
})
複製代碼
其中的_c
就是vue對象的createElement
方法 (建立元素),_m
是renderStatic
(渲染靜態節點),_v
是 createTextVNode
(建立文本dom),_s
是 toString
(轉換爲字符串)
// 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
...
複製代碼
// mpvue-loader/lib/template-compiler/index.js
compileToWxml.call(this, compiled, html)
複製代碼
以上解答了問題一、2
.
├── events.js //解答問題5
├── index.js //入口提供Vue對象,以及$mount,和各類初始化
├── liefcycle //解答問題六、7
├── node-ops.js //操做真實DOM的相關實現,由於小程序不能操做DOM,因此這裏都是直接返回
├── patch.js //解答問題3
└── render.js //解答問題4
複製代碼
和vue使用的createPatchFunction
保持一致,任然是舊樹和新樹進行patch產出diff,可是多了一行this.$updateDataToMP()用以更新。
兩個核心的方法initDataToMP
、updateDataToMP
。
initDataToMP
收集vm上的data,而後調用小程序Page示例的setData
渲染。
updateDataToMP
在每次patch,也就是依賴收集發現數據改變時更新(參考patch.js代碼),這部分同樣會使用nextTick
和隊列。最終使用了節流閥throttleSetData
。50毫秒用來控制頻率以解決頻繁修改Data,會形成大量傳輸Data數據而致使的性能問題。
其中collectVmData
最終也是用到了formatVmData
。尤爲要注意的是一句註釋:
getVmData 這兒獲取當前組件內的全部數據,包含 props、computed 的數據
咱們又知道,service到view是兩個線程間通訊,若是Data含有大量數據,增長了傳輸數據量,加大了傳輸成本,將會形成性能降低。
正如官網所說的,這裏使用eventTypeMap
作了各事件的隱射
import { getComKey, eventTypeMap } from '../util/index'
複製代碼
// 用於小程序的 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']
}
複製代碼
使用了handleProxyWithVue
方法來代理小程序事件到vue事件。
另外看下做者本身對這部分的思路
事件代理機制:用戶交互觸發的數據更新經過事件代理機制完成。在 Vue.js 代碼中,事件響應函數對應到組件的 method, Vue.js 自動維護了上下文環境。然而在小程序中並無相似的機制,又由於 Vue.js 執行環境中維護着一份實時的虛擬 DOM,這與小程序的視圖層徹底對應,咱們思考,在小程序組件節點上觸發事件後,只要找到虛擬 DOM 上對應的節點,觸發對應的事件不就完成了麼;另外一方面,Vue.js 事件響應若是觸發了數據更新,其生命週期函數更新將自動觸發,在此函數上同步更新小程序數據,數據同步也就實現了。
getHandle
這個方法應該就是做者思路當中所說的:找到對應節點,而後找到handle。
在initMP
方法中,本身建立小程序的App、Page。實現生命週期相關方法,使用callHook
代理兼容小程序App、Page的生命週期。
官方文檔生命週期中說到了:
同 vue,不一樣的是咱們會在小程序 onReady 後,再去觸發 vue mounted 生命週期
這部分查看,onReady
以後纔會執行next
,這個next
回調最終是vue的mountComponent
。能夠在index.js中看到。這部分代碼也就是解決了"小程序生命週期中觸發vue生命週期"。
export function initMP (mpType, next) {
// ...
global.Page({
// 生命週期函數--監聽頁面初次渲染完成
onReady () {
mp.status = 'ready'
callHook(rootVueVM, 'onReady')
next()
},
})
// ...
}
複製代碼
在小程序onShow時,使用$nextTick去第一次渲染數據,參考上面提到的render.js。
export function initMP (mpType, next) {
// ...
global.Page({
// 生命週期函數--監聽頁面顯示
onShow () {
mp.page = this
mp.status = 'show'
callHook(rootVueVM, 'onShow')
// 只有頁面須要 setData
rootVueVM.$nextTick(() => {
rootVueVM._initDataToMP()
})
},
})
// ...
}
複製代碼
在mpvue-loader生成template時,好比點擊事件@click
會變成bindtap="handleProxy"
,事件綁定全都會使用handleProxy
這個方法。
能夠查看上面mpvue-loader回顧一下。
最終handleProxy調用的是event.js中的handleProxyWithVue
。
export function initMP (mpType, next) {
// ...
global.Page({
handleProxy (e) {
return rootVueVM.$handleProxyWithVue(e)
},
})
// ...
}
複製代碼
最後index.js就負責各類初始化和mount。
緣由:目前的組件是使用小程序的 template 標籤實現的,給組件指定的class和style是掛載在template標籤上,而template 標籤不支持 class 及 style 屬性。
解決方案: 在自定義組件上綁定class或style到一個props屬性上。
// 組件ComponentA.vue
<template>
<div class="container" :class="pClass">
...
</div>
</template>
複製代碼
<script>
export default {
props: {
pClass: {
type: String,
default: ''
}
}
}
</script>
複製代碼
<!--PageB.vue-->
<template>
<component-a :pClass="cusComponentAClass" />
</template>
複製代碼
<script>
data () {
return {
cusComponentAClass: 'a-class b-class'
}
}
</script>
複製代碼
<style lang="stylus" scoped>
.a-class
border red solid 2rpx
.b-class
margin-right 20rpx
</style>
複製代碼
可是這樣會有問題就是style加上scoped以後,編譯模板生成的代碼是下面這樣的:
.a-class.data-v-8f1d914e {
border: #f00 solid 2rpx;
}
.b-class.data-v-8f1d914e {
margin-right 20rpx
}
複製代碼
因此想要這些組件的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>
複製代碼
<!--P組件ComponentA.vue-->
<template>
<div class="container" :style="pStyle">
...
</div>
</template>
複製代碼
<script>
export default {
props: {
pStyle: {
type: String,
default: ''
}
}
}
</script>
複製代碼
<!--PageB.vue-->
<template>
<component-a :pStyle="cusComponentAStyle" />
</template>
複製代碼
<script>
const cusComponentAStyle = 'border:red solid 2rpx; margin-right:20rpx;'
data () {
return {
cusComponentAStyle
}
}
</script>
複製代碼
<style lang="stylus" scoped>
...
</style>
複製代碼
也能夠經過定義styleObject,而後經過工具函數轉化爲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('; ')
複製代碼
<!--組件ComponentA.vue-->
<template>
<div class="container" :style="{'background-color': backgroundColor}">
...
</div>
</template>
複製代碼
<script>
export default {
props: {
backgroundColor: {
type: String,
default: 'yellow'
}
}
}
</script>
複製代碼
<!-- PageB.vue -->
<template>
<component-a backgroundColor="red" />
</template>
複製代碼
package.json修改
注意事項
移動src/main.js中config相關內容到同級目錄下main.json(新建)中
export default {
// config: {...} 須要移動
}
複製代碼
to
{
"pages": [
"pages/index/main",
"pages/logs/main"
],
"subPackages": [
{
"root": "pages/packageA",
"pages": [
"counter/main"
]
}
],
"window": {...}
}
複製代碼
webpack 配置配合升級指南
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: ['.*']
+ }
+ ])
]
}
複製代碼
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: ['.*']
- }
- ]),
複製代碼
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: ['.*']
- }
- ])
]
})
複製代碼
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,
// 在小程序開發者工具中不須要自動打開瀏覽器
autoOpenBrowser: false,
- assetsSubDirectory: 'static', // 不將資源聚合放在 static 目錄下
+ assetsSubDirectory: '',
assetsPublicPath: '/',
proxyTable: {},
// CSS Sourcemaps off by default because relative paths are "buggy"
複製代碼
以上內容部分來自:
若是你想學習到更多的前端知識、面試技巧或者一些我我的的感悟,能夠關注個人公衆號一塊兒學習