做者:張利濤,視頻課程《微信小程序教學》、《基於Koa2搭建Node.js實戰項目教學》主編,滬江前端架構師本文原創,轉載請註明做者及出處php
咱們不同,不同,不同。
首先從官方文檔能夠看到,小程序的運行環境並非瀏覽器環境:html
小程序框架提供了本身的視圖層描述語言 WXML 和 WXSS,以及基於 JavaScript 的邏輯層框架,並在視圖層與邏輯層間提供了數據傳輸和事件系統,可讓開發者能夠方便的聚焦於數據與邏輯上。 小程序的視圖層目前使用 WebView 做爲渲染載體,而邏輯層是由獨立的 JavascriptCore 做爲運行環境。在架構上,WebView 和 JavascriptCore 都是獨立的模塊,並不具有數據直接共享的通道。當前,視圖層和邏輯層的數據傳輸,實際上經過兩邊提供的 evaluateJavascript 所實現。即用戶傳輸的數據,須要將其轉換爲字符串形式傳遞,同時把轉換後的數據內容拼接成一份 JS 腳本,再經過執行 JS 腳本的形式傳遞到兩邊獨立環境。 而 evaluateJavascript 的執行會受不少方面的影響,數據到達視圖層並非實時的。同一進程內的 WebView 實際上會共享一個 JS VM,若是 WebView 內 JS 線程正在執行渲染或其餘邏輯,會影響 evaluateJavascript 腳本的實際執行時間,另外多個 WebView 也會搶佔 JS VM 的執行權限;另外還有 JS 自己的編譯執行耗時,都是影響數據傳輸速度的因素。
而所謂的運行環境,對於任何語言的運行,它們都須要有一個環境——runtime。瀏覽器和 Node.js 都能運行 JavaScript,但它們都只是指定場景下的 runtime,全部各有不一樣。而小程序的運行環境,是微信定製化的 runtime。前端
你們能夠作一個小實驗,分別在瀏覽器環境和小程序環境打開各自的控制檯,運行下面的代碼來進行一個 20 億次的循環:react
var k for (var i = 0; i < 2000000000; i++) { k = i }
瀏覽器控制檯下運行時,當前頁面是徹底不能動,由於 JS 和視圖共用一個線程,相互阻塞。git
小程序控制臺下運行時,當前視圖能夠動,若是綁定有事件,也會同樣觸發,只不過事件的回調須要在 『循環結束』 以後。github
視圖層和邏輯層若是共用一個線程,優勢是通訊速度快(離的近就是好),缺點是相互阻塞。好比瀏覽器。web
視圖層和邏輯層若是分處兩個環境,優勢是相互不阻塞,缺點是通訊成本高(異地戀)。好比小程序的 setData
,通訊一次就像是寫情書!編程
因此,嚴格來講,小程序是微信定製的混合開發模式。json
從實踐體驗上看,咱們能夠從小程序視圖上看到 Java FreeMarker 框架、Velocity、smarty 之類的影子。redux
小程序視圖支持以下
數據綁定 {{}} 列表渲染 wx:for 條件判斷 wx:if 模板 tempalte 事件 bindtap 引用 import include 可在視圖中應用的腳本語言 wxs ...
Java FreeMarker 也一樣支持上述功能。
數據綁定 ${} 列表渲染 list指令 條件判斷 if指令 模板 FTL 事件 原生事件 引用 import include 指令 內建函數 好比『時間格式化』 可在視圖中應用的腳本語言 宏 marco ...
小程序的全部接口,都是經過傳統的回調函數形式來調用的。回調函數真正的問題在於他剝奪了咱們使用 return 和 throw 這些關鍵字的能力。而 Promise 很好地解決了這一切。
那麼,如何經過 Promise 的方式來調用小程序接口呢?
查看一下小程序的官方文檔,咱們會發現,幾乎全部的接口都是同一種書寫形式:
wx.request({ url: "test.php", //僅爲示例,並不是真實的接口地址 data: { x: "", y: "" }, header: { "content-type": "application/json" // 默認值 }, success: function(res) { console.log(res.data) }, fail: function(res) { console.log(res) } })
因此,咱們能夠經過簡單的 Promise 寫法,把小程序接口裝飾一下。代碼以下:
wx.request2 = (option = {}) => { // 返回一個 Promise 實例對象,這樣就可使用 then 和 throw return new Promise((resolve, reject) => { option.success = res => { // 重寫 API 的 success 回調函數 resolve(res) } option.fail = res => { // 重寫 API 的 fail 回調函數 reject(res) } wx.request(option) // 裝飾後,進行正常的接口請求 }) }
上述代碼簡單的展示瞭如何把一個請求接口包裝成 Promise 形式。但在實戰項目中,可能有多個接口須要咱們去包裝處理,每個都單獨包裝是不現實的。這時候,咱們就須要用一些技巧來處理了。
其實思路很簡單:咱們把須要 Promise 化的『接口名字』存放在一個『數組』中,而後對這個數組進行循環處理。
這裏咱們利用了 ECMAScript5 的特性 Object.defineProperty 來重寫接口的取值過程。
let wxKeys = [ // 存儲須要Promise化的接口名字 "showModal", "request" ] // 擴展 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的原生函數臨時保存下來 if (wxKeyFn && typeof wxKeyFn === "function") { // 若是這個值存在而且是函數的話,進行重寫 Object.defineProperty(wx, key, { get() { // 一旦目標對象訪問該屬性,就會調用這個方法,並返回結果 // 調用 wx.request({}) 時候,就至關於在調用此函數 return (option = {}) => { // 函數運行後,返回 Promise 實例對象 return new Promise((resolve, reject) => { option.success = res => { resolve(res) } option.fail = res => { reject(res) } wxKeyFn(option) }) } } }) } })
注: Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回這個對象。
用法也很簡單,咱們把上述代碼保存在一個 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:") }) } })
小程序從 1.6.3 版本開始,支持簡潔的組件化編程
// 組件內部實現 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 中調用 translatePop = new TranslatePop(this); translatePop.init();
實現方式比較簡單,就是在調用一個組件時候,把當前環境的上下文 content 傳遞給組件,在組件內部實現 setData 調用。
官方組件示例:
Component({ properties: { // 這裏定義了innerText屬性,屬性值能夠在組件使用時指定 innerText: { type: String, value: "default value" } }, data: { // 這裏是一些組件內部數據 someData: {} }, methods: { // 這裏是一個自定義方法 customMethod: function() {} } })
在 React 項目中 Redux 是如何工做的
單一數據源
整個應用的 state 被儲存在一棵 object tree 中,而且這個 object tree 只存在於惟一一個 store 中。
State 是隻讀的
唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通對象
使用純函數來執行修改
爲了描述 action 如何改變 state tree ,你須要編寫 reducers。
若是你有看過 Redux 的源碼就會發現,上述的過程能夠簡化描述以下:
第三步:同步視圖,在 React 中,State 發生變化後會觸發 Render 來更新視圖。
而小程序中,若是咱們經過 setData 改變 data,一樣能夠更新視圖。
因此,咱們實現小程序組件通訊的思路以下:
先預覽下咱們的最終項目結構:
├── 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.wxss
首先,咱們從 cdn 或官方網站獲取 redux.min.js,放在結構裏面
建立 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
咱們定義了一個須要傳遞的場景值 count
,用來表明例子中的『點擊次數』,rest
表明『重置次數』。
而後在 app.js 中引入,並植入到小程序全局中:
//app.js import Store from './reducers/index' App({ Store, })
// 引用了 react-redux 中的工具函數,用來判斷兩個狀態是否相等 import shallowEqual from './shallowEqual' // 獲取咱們在 app.js 中植入的全局變量 Store let __Store = getApp().Store // 函數變量,用來過濾出咱們想要的 state,方便對比賦值 let mapStateToData // 用來補全配置項中的生命週期函數 let baseObj = { __observer: null, onLoad() { }, onUnload() { }, onShow() { }, onHide() { } } let config = { __Store, __dispatch: __Store.dispatch, __destroy: null, __observer() { // 對象中的 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)) {// 狀態值沒有發生變化就返回 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 = {}) => { // 補全生命週期 let opts = Object.assign({}, baseObj, options) // 把業務代碼中的 opts 配置對象,指定爲 config 的原型,方便『裝飾者調用』 Object.setPrototypeOf(config, opts) return config } }
調用方法:
// pages/index/index.js import connect from "../../utils/connect" const mapStateToProps = (state) => { return { counter: state.counter } } Page(connect(mapStateToProps)({ data: { innerText: "Hello 點我加1哦" }, bindBtn() { this.__dispatch({ type: "COUNTER_ADD_1" }) } }))
最終效果展現:
項目源碼地址:
https://github.com/ikcamp/xcx-redux
直播視頻地址:
https://www.cctalk.com/v/15137361643293
iKcamp官網: https://www.ikcamp.com
iKcamp新課程推出啦~~~~~ 開始免費連載啦~每週2更共11堂iKcamp課|基於Koa2搭建Node.js實戰項目教學(含視頻)| 課程大綱介紹