摘要:小程序的視圖層目前使用作為渲染載體,而邏輯層是由獨(dú)立的作為運(yùn)行環(huán)境。比如小程序的,通信一次就像是寫情書所以,嚴(yán)格來說,小程序是微信定制的混合開發(fā)模式。出棧入棧解決小程序接口不支持的問題小程序的所有接口,都是通過傳統(tǒng)的回調(diào)函數(shù)形式來調(diào)用的。
作者:張利濤,視頻課程《微信小程序教學(xué)》、《基于Koa2搭建Node.js實(shí)戰(zhàn)項(xiàng)目教學(xué)》主編,滬江前端架構(gòu)師本文原創(chuàng),轉(zhuǎn)載請(qǐng)注明作者及出處
小程序和 H5 區(qū)別
小程序的運(yùn)行過程
解決小程序接口不支持 Promise 的問題
小程序組件化開發(fā)及通信
小程序和 H5 區(qū)別我們不一樣,不一樣,不一樣。運(yùn)行環(huán)境 runtime
首先從官方文檔可以看到,小程序的運(yùn)行環(huán)境并不是瀏覽器環(huán)境:
小程序框架提供了自己的視圖層描述語言 WXML 和 WXSS,以及基于 JavaScript 的邏輯層框架,并在視圖層與邏輯層間提供了數(shù)據(jù)傳輸和事件系統(tǒng),可以讓開發(fā)者可以方便的聚焦于數(shù)據(jù)與邏輯上。 小程序的視圖層目前使用 WebView 作為渲染載體,而邏輯層是由獨(dú)立的 JavascriptCore 作為運(yùn)行環(huán)境。在架構(gòu)上,WebView 和 JavascriptCore 都是獨(dú)立的模塊,并不具備數(shù)據(jù)直接共享的通道。當(dāng)前,視圖層和邏輯層的數(shù)據(jù)傳輸,實(shí)際上通過兩邊提供的 evaluateJavascript 所實(shí)現(xiàn)。即用戶傳輸?shù)臄?shù)據(jù),需要將其轉(zhuǎn)換為字符串形式傳遞,同時(shí)把轉(zhuǎn)換后的數(shù)據(jù)內(nèi)容拼接成一份 JS 腳本,再通過執(zhí)行 JS 腳本的形式傳遞到兩邊獨(dú)立環(huán)境。 而 evaluateJavascript 的執(zhí)行會(huì)受很多方面的影響,數(shù)據(jù)到達(dá)視圖層并不是實(shí)時(shí)的。同一進(jìn)程內(nèi)的 WebView 實(shí)際上會(huì)共享一個(gè) JS VM,如果 WebView 內(nèi) JS 線程正在執(zhí)行渲染或其他邏輯,會(huì)影響 evaluateJavascript 腳本的實(shí)際執(zhí)行時(shí)間,另外多個(gè) WebView 也會(huì)搶占 JS VM 的執(zhí)行權(quán)限;另外還有 JS 本身的編譯執(zhí)行耗時(shí),都是影響數(shù)據(jù)傳輸速度的因素。
而所謂的運(yùn)行環(huán)境,對(duì)于任何語言的運(yùn)行,它們都需要有一個(gè)環(huán)境——runtime。瀏覽器和 Node.js 都能運(yùn)行 JavaScript,但它們都只是指定場(chǎng)景下的 runtime,所有各有不同。而小程序的運(yùn)行環(huán)境,是微信定制化的 runtime。
大家可以做一個(gè)小實(shí)驗(yàn),分別在瀏覽器環(huán)境和小程序環(huán)境打開各自的控制臺(tái),運(yùn)行下面的代碼來進(jìn)行一個(gè) 20 億次的循環(huán):
var k for (var i = 0; i < 2000000000; i++) { k = i }
瀏覽器控制臺(tái)下運(yùn)行時(shí),當(dāng)前頁面是完全不能動(dòng),因?yàn)?JS 和視圖共用一個(gè)線程,相互阻塞。
小程序控制臺(tái)下運(yùn)行時(shí),當(dāng)前視圖可以動(dòng),如果綁定有事件,也會(huì)一樣觸發(fā),只不過事件的回調(diào)需要在 『循環(huán)結(jié)束』 之后。
視圖層和邏輯層如果共用一個(gè)線程,優(yōu)點(diǎn)是通信速度快(離的近就是好),缺點(diǎn)是相互阻塞。比如瀏覽器。
視圖層和邏輯層如果分處兩個(gè)環(huán)境,優(yōu)點(diǎn)是相互不阻塞,缺點(diǎn)是通信成本高(異地戀)。比如小程序的 setData,通信一次就像是寫情書!
所以,嚴(yán)格來說,小程序是微信定制的混合開發(fā)模式。
在 JavaScript 的基礎(chǔ)上,小程序做了一些修改,以方便開發(fā)小程序。增加 App 和 Page 方法,進(jìn)行程序和頁面的注冊(cè)?!驹黾恿?Component】
增加 getApp 和 getCurrentPages 方法,分別用來獲取 App 實(shí)例和當(dāng)前頁面棧。
提供豐富的 API,如微信用戶數(shù)據(jù),掃一掃,支付等微信特有能力?!菊{(diào)用原生組件:Cordova、ReactNative、Weex 等】
每個(gè)頁面有獨(dú)立的作用域,并提供模塊化能力。
由于框架并非運(yùn)行在瀏覽器中,所以 JavaScript 在 web 中一些能力都無法使用,如 document,window 等?!拘〕绦虻?JsCore 環(huán)境】
開發(fā)者寫的所有代碼最終將會(huì)打包成一份 JavaScript,并在小程序啟動(dòng)的時(shí)候運(yùn)行,直到小程序銷毀。類似 ServiceWorker,所以邏輯層也稱之為 App Service。
與傳統(tǒng)的 HTML 相比,WXML 更像是一種模板式的標(biāo)簽語言從實(shí)踐體驗(yàn)上看,我們可以從小程序視圖上看到 Java FreeMarker 框架、Velocity、smarty 之類的影子。
小程序視圖支持如下
數(shù)據(jù)綁定 {{}} 列表渲染 wx:for 條件判斷 wx:if 模板 tempalte 事件 bindtap 引用 import include 可在視圖中應(yīng)用的腳本語言 wxs ...
Java FreeMarker 也同樣支持上述功能。
數(shù)據(jù)綁定 ${} 列表渲染 list指令 條件判斷 if指令 模板 FTL 事件 原生事件 引用 import include 指令 內(nèi)建函數(shù) 比如『時(shí)間格式化』 可在視圖中應(yīng)用的腳本語言 宏 marco ...小程序的運(yùn)行過程
我們?cè)谖⑿派洗蜷_一個(gè)小程序
微信客戶端在打開小程序之前,會(huì)把整個(gè)小程序的代碼包下載到本地。
微信 App 從微信服務(wù)器下載小程序的文件包
為了流暢的用戶體驗(yàn)和性能問題,小程序的文件包不能超過 2M。另外要注意,小程序目錄下的所有文件上傳時(shí)候都會(huì)打到一個(gè)包里面,所以盡量少用圖片和第三方的庫,特別是圖片。
解析 app.json 配置信息初始化導(dǎo)航欄,窗口樣式,包含的頁面列表
加載運(yùn)行 app.js
初始化小程序,創(chuàng)建 app 實(shí)例
根據(jù) app.json,加載運(yùn)行第一個(gè)頁面初始化第一個(gè) Page
路由切換
以棧的形式維護(hù)了當(dāng)前的所有頁面。最多 5 個(gè)頁面。出棧入棧
小程序的所有接口,都是通過傳統(tǒng)的回調(diào)函數(shù)形式來調(diào)用的?;卣{(diào)函數(shù)真正的問題在于他剝奪了我們使用 return 和 throw 這些關(guān)鍵字的能力。而 Promise 很好地解決了這一切。
那么,如何通過 Promise 的方式來調(diào)用小程序接口呢?
查看一下小程序的官方文檔,我們會(huì)發(fā)現(xiàn),幾乎所有的接口都是同一種書寫形式:
wx.request({ url: "test.php", //僅為示例,并非真實(shí)的接口地址 data: { x: "", y: "" }, header: { "content-type": "application/json" // 默認(rèn)值 }, success: function(res) { console.log(res.data) }, fail: function(res) { console.log(res) } })
所以,我們可以通過簡單的 Promise 寫法,把小程序接口裝飾一下。代碼如下:
wx.request2 = (option = {}) => { // 返回一個(gè) Promise 實(shí)例對(duì)象,這樣就可以使用 then 和 throw return new Promise((resolve, reject) => { option.success = res => { // 重寫 API 的 success 回調(diào)函數(shù) resolve(res) } option.fail = res => { // 重寫 API 的 fail 回調(diào)函數(shù) reject(res) } wx.request(option) // 裝飾后,進(jìn)行正常的接口請(qǐng)求 }) }
上述代碼簡單的展現(xiàn)了如何把一個(gè)請(qǐng)求接口包裝成 Promise 形式。但在實(shí)戰(zhàn)項(xiàng)目中,可能有多個(gè)接口需要我們?nèi)グb處理,每一個(gè)都多帶帶包裝是不現(xiàn)實(shí)的。這時(shí)候,我們就需要用一些技巧來處理了。
其實(shí)思路很簡單:我們把需要 Promise 化的『接口名字』存放在一個(gè)『數(shù)組』中,然后對(duì)這個(gè)數(shù)組進(jìn)行循環(huán)處理。
這里我們利用了 ECMAScript5 的特性 Object.defineProperty 來重寫接口的取值過程。
let wxKeys = [ // 存儲(chǔ)需要Promise化的接口名字 "showModal", "request" ] // 擴(kuò)展 Promise 的 finally 功能 Promise.prototype.finally = function(callback) { let P = this.constructor return this.then( value => P.resolve(callback()).then(() => value), reason => P.resolve(callback()).then(() => { throw reason }) ) } wxKeys.forEach(key => { const wxKeyFn = wx[key] // 將wx的原生函數(shù)臨時(shí)保存下來 if (wxKeyFn && typeof wxKeyFn === "function") { // 如果這個(gè)值存在并且是函數(shù)的話,進(jìn)行重寫 Object.defineProperty(wx, key, { get() { // 一旦目標(biāo)對(duì)象訪問該屬性,就會(huì)調(diào)用這個(gè)方法,并返回結(jié)果 // 調(diào)用 wx.request({}) 時(shí)候,就相當(dāng)于在調(diào)用此函數(shù) return (option = {}) => { // 函數(shù)運(yùn)行后,返回 Promise 實(shí)例對(duì)象 return new Promise((resolve, reject) => { option.success = res => { resolve(res) } option.fail = res => { reject(res) } wxKeyFn(option) }) } } }) } })
注: Object.defineProperty() 方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性,并返回這個(gè)對(duì)象。
用法也很簡單,我們把上述代碼保存在一個(gè) js 文件中,比如 utils/toPromise.js,然后在 app.js 中引入就可以了:
import "./util/toPromise" App({ onLoad() { wx .request({ url: "http://www.weather.com.cn/data/sk/101010100.html" }) .then(res => { console.log("come from Promised api, then:", res) }) .catch(err => { console.log("come from Promised api, catch:", err) }) .finally(res => { console.log("come from Promised api, finally:") }) } })小程序組件化開發(fā)
小程序從 1.6.3 版本開始,支持簡潔的組件化編程
官方支持組件化之前的做法// 組件內(nèi)部實(shí)現(xiàn) export default class TranslatePop { constructor(owner, deviceInfo = {}) { this.owner = owner; this.defaultOption = {} } init() { this.applyData({...}) } applyData(data) { let optData = Object.assign(this.defaultOption, data); this.owner && this.owner.setData({ translatePopData: optData }) } } // index.js 中調(diào)用 translatePop = new TranslatePop(this); translatePop.init();
實(shí)現(xiàn)方式比較簡單,就是在調(diào)用一個(gè)組件時(shí)候,把當(dāng)前環(huán)境的上下文 content 傳遞給組件,在組件內(nèi)部實(shí)現(xiàn) setData 調(diào)用。
應(yīng)用官方支持的方式來實(shí)現(xiàn)官方組件示例:
Component({ properties: { // 這里定義了innerText屬性,屬性值可以在組件使用時(shí)指定 innerText: { type: String, value: "default value" } }, data: { // 這里是一些組件內(nèi)部數(shù)據(jù) someData: {} }, methods: { // 這里是一個(gè)自定義方法 customMethod: function() {} } })結(jié)合 Redux 實(shí)現(xiàn)組件通信
在 React 項(xiàng)目中 Redux 是如何工作的
單一數(shù)據(jù)源
整個(gè)應(yīng)用的 state 被儲(chǔ)存在一棵 object tree 中,并且這個(gè) object tree 只存在于唯一一個(gè) store 中。
State 是只讀的
惟一改變 state 的方法就是觸發(fā) action,action 是一個(gè)用于描述已發(fā)生事件的普通對(duì)象
使用純函數(shù)來執(zhí)行修改
為了描述 action 如何改變 state tree ,你需要編寫 reducers。
Props 傳遞 —— Render 渲染
如果你有看過 Redux 的源碼就會(huì)發(fā)現(xiàn),上述的過程可以簡化描述如下:
訂閱:監(jiān)聽狀態(tài)————保存對(duì)應(yīng)的回調(diào)
發(fā)布:狀態(tài)變化————執(zhí)行回調(diào)函數(shù)
同步視圖:回調(diào)函數(shù)同步數(shù)據(jù)到視圖
第三步:同步視圖,在 React 中,State 發(fā)生變化后會(huì)觸發(fā) Render 來更新視圖。
而小程序中,如果我們通過 setData 改變 data,同樣可以更新視圖。
所以,我們實(shí)現(xiàn)小程序組件通信的思路如下:
觀察者模式/發(fā)布訂閱模式
裝飾者模式/Object.defineProperty (Vuejs 的設(shè)計(jì)路線)
在小程序中實(shí)現(xiàn)組件通信先預(yù)覽下我們的最終項(xiàng)目結(jié)構(gòu):
├── components/ │ ├── count/ │ ├── count.js │ ├── count.json │ ├── count.wxml │ ├── count.wxss │ ├── footer/ │ ├── footer.js │ ├── footer.json │ ├── footer.wxml │ ├── footer.wxss ├── pages/ │ ├── index/ │ ├── ... │ ├── log/ │ ├── ... ├── reducers/ │ ├── counter.js │ ├── index.js │ ├── redux.min.js ├── utils/ │ ├── connect.js │ ├── shallowEqual.js │ ├── toPromise.js ├── app.js ├── app.json ├── app.wxss1. 實(shí)現(xiàn)『發(fā)布訂閱』功能
首先,我們從 cdn 或官方網(wǎng)站獲取 redux.min.js,放在結(jié)構(gòu)里面
創(chuàng)建 reducers 目錄下的文件:
// /reducers/index.js import { createStore, combineReducers } from "./redux.min.js" import counter from "./counter" export default createStore(combineReducers({ counter: counter })) // /reducers/counter.js const INITIAL_STATE = { count: 0, rest: 0 } const Counter = (state = INITIAL_STATE, action) => { switch (action.type) { case "COUNTER_ADD_1": { let { count } = state return Object.assign({}, state, { count: count + 1 }) } case "COUNTER_CLEAR": { let { rest } = state return Object.assign({}, state, { count: 0, rest: rest+1 }) } default: { return state } } } export default Counter
我們定義了一個(gè)需要傳遞的場(chǎng)景值 count,用來代表例子中的『點(diǎn)擊次數(shù)』,rest 代表『重置次數(shù)』。
然后在 app.js 中引入,并植入到小程序全局中:
//app.js import Store from "./reducers/index" App({ Store, })2. 利用 『裝飾者模式』,對(duì)小程序的生命周期進(jìn)行包裝,狀態(tài)發(fā)生變化時(shí)候,如果狀態(tài)值不一樣,就同步 setData
// 引用了 react-redux 中的工具函數(shù),用來判斷兩個(gè)狀態(tài)是否相等 import shallowEqual from "./shallowEqual" // 獲取我們?cè)?app.js 中植入的全局變量 Store let __Store = getApp().Store // 函數(shù)變量,用來過濾出我們想要的 state,方便對(duì)比賦值 let mapStateToData // 用來補(bǔ)全配置項(xiàng)中的生命周期函數(shù) let baseObj = { __observer: null, onLoad() { }, onUnload() { }, onShow() { }, onHide() { } } let config = { __Store, __dispatch: __Store.dispatch, __destroy: null, __observer() { // 對(duì)象中的 super,指向其原型 prototype if (super.__observer) { super.__observer() return } const state = __Store.getState() const newData = mapStateToData(state) const oldData = mapStateToData(this.data || {}) if (shallowEqual(oldData, newData)) {// 狀態(tài)值沒有發(fā)生變化就返回 return } this.setData(newData) }, onLoad() { super.onLoad() this.__destroy = this.__Store.subscribe(this.__observer) this.__observer() }, onUnload() { super.onUnload() this.__destroy && this.__destroy() & delete this.__destroy }, onShow() { super.onShow() if (!this.__destroy) { this.__destroy = this.__Store.subscribe(this.__observer) this.__observer() } }, onHide() { super.onHide() this.__destroy && this.__destroy() & delete this.__destroy } } export default (mapState = () => { }) => { mapStateToData = mapState return (options = {}) => { // 補(bǔ)全生命周期 let opts = Object.assign({}, baseObj, options) // 把業(yè)務(wù)代碼中的 opts 配置對(duì)象,指定為 config 的原型,方便『裝飾者調(diào)用』 Object.setPrototypeOf(config, opts) return config } }
調(diào)用方法:
// pages/index/index.js import connect from "../../utils/connect" const mapStateToProps = (state) => { return { counter: state.counter } } Page(connect(mapStateToProps)({ data: { innerText: "Hello 點(diǎn)我加1哦" }, bindBtn() { this.__dispatch({ type: "COUNTER_ADD_1" }) } }))
最終效果展示:
項(xiàng)目源碼地址:
https://github.com/ikcamp/xcx-redux
直播視頻地址:
https://www.cctalk.com/v/15137361643293
iKcamp官網(wǎng):https://www.ikcamp.com
iKcamp新課程推出啦~~~~~開始免費(fèi)連載啦~每周2更共11堂iKcamp課|基于Koa2搭建Node.js實(shí)戰(zhàn)項(xiàng)目教學(xué)(含視頻)| 課程大綱介紹滬江iKcamp出品微信小程序教學(xué)共5章16小節(jié)匯總(含視頻)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/92306.html
摘要:阿里云是國內(nèi)云服務(wù)器市場(chǎng)的龍頭,性價(jià)比高,速度快又安全,是站長建站首選的云服務(wù)器之一。作為一個(gè)老司機(jī),福利吧也和大家分享一下我的阿里云推廣經(jīng)驗(yàn),教大家如何免費(fèi)推廣云大使。阿里云是國內(nèi)云服務(wù)器市場(chǎng)的龍頭,性價(jià)比高,速度快又安全,是站長建站首選的云服務(wù)器之一。福利吧使用的也是阿里云服務(wù)器,是折騰了很多次網(wǎng)站搬家后,才選擇了阿里云。身邊好幾個(gè)站長最后都殊途同歸,用了阿里云,可見阿里云服務(wù)器性能確實(shí)...
摘要:深入淺出容器云系列文章是由時(shí)速云出品,本文是第二篇,歡迎大家不吝賜教。容器服務(wù)是一種高度可擴(kuò)展的高性能容器管理服務(wù),服務(wù)于應(yīng)用的完整生命周期。存儲(chǔ)卷容器服務(wù)支持有狀態(tài)和無狀態(tài)服務(wù)。當(dāng)容器重新部署時(shí)也會(huì)隨著容器在不同主機(jī)之間遷移。 導(dǎo)語:隨著以Docker為代表的容器技術(shù)在國內(nèi)的迅速發(fā)展,容器云也逐漸被廣大開發(fā)者所熟知,但容器云(CaaS)相比傳統(tǒng)的云主機(jī)(IaaS)在實(shí)際應(yīng)用中還存在著...
摘要:說明處理方法被異步執(zhí)行了。意味著操作成功完成。狀態(tài)的對(duì)象可能觸發(fā)狀態(tài)并傳遞一個(gè)值給相應(yīng)的狀態(tài)處理方法,也可能觸發(fā)失敗狀態(tài)并傳遞失敗信息。注生命周期相關(guān)內(nèi)容引用自四使用和異步加載圖片這是官方給出的示例,部分的代碼如下 帶你玩轉(zhuǎn) JavaScript ES6 (七) - 異步 本文同步帶你玩轉(zhuǎn) JavaScript ES6 (七) - 異步,轉(zhuǎn)載請(qǐng)注明出處。 本章我們將學(xué)習(xí) ES6 中的 ...
摘要:透視即是以現(xiàn)實(shí)的視角來看屏幕上的事物,從而展現(xiàn)的效果。旋轉(zhuǎn)則不再是平面上的旋轉(zhuǎn),而是三維坐標(biāo)系的旋轉(zhuǎn),就包括軸,軸,軸旋轉(zhuǎn)。必須與屬性一同使用,而且只影響轉(zhuǎn)換元素。可自由轉(zhuǎn)載引用,但需署名作者且注明文章出處。 showImg(https://segmentfault.com/img/bVzJoZ); 話不多說,先上demo 酷炫css3走馬燈/正方體動(dòng)畫: https://bupt-...
閱讀 3389·2021-11-22 09:34
閱讀 663·2021-11-19 11:29
閱讀 1365·2019-08-30 15:43
閱讀 2245·2019-08-30 14:24
閱讀 1879·2019-08-29 17:31
閱讀 1237·2019-08-29 17:17
閱讀 2625·2019-08-29 15:38
閱讀 2743·2019-08-26 12:10