做者:張利濤,視頻課程《微信小程序教學》、《基於Koa2搭建Node.js實戰項目教學》主編,滬江前端架構師php
本文原創,轉載請註明做者及出處html
咱們不同,不同,不同。前端
首先從官方文檔能夠看到,小程序的運行環境並非瀏覽器環境:react
小程序框架提供了本身的視圖層描述語言 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。git
你們能夠作一個小實驗,分別在瀏覽器環境和小程序環境打開各自的控制檯,運行下面的代碼來進行一個 20 億次的循環:github
var k
for (var i = 0; i < 2000000000; i++) {
k = i
}
複製代碼
瀏覽器控制檯下運行時,當前頁面是徹底不能動,由於 JS 和視圖共用一個線程,相互阻塞。web
小程序控制臺下運行時,當前視圖能夠動,若是綁定有事件,也會同樣觸發,只不過事件的回調須要在 『循環結束』 以後。編程
視圖層和邏輯層若是共用一個線程,優勢是通訊速度快(離的近就是好),缺點是相互阻塞。好比瀏覽器。json
視圖層和邏輯層若是分處兩個環境,優勢是相互不阻塞,缺點是通訊成本高(異地戀)。好比小程序的 setData
,通訊一次就像是寫情書!redux
因此,嚴格來講,小程序是微信定製的混合開發模式。
從實踐體驗上看,咱們能夠從小程序視圖上看到 Java FreeMarker 框架、Velocity、smarty 之類的影子。
小程序視圖支持以下
數據綁定 {{}}
列表渲染 wx:for
條件判斷 wx:if
模板 tempalte
事件 bindtap
引用 import include
可在視圖中應用的腳本語言 wxs
...
複製代碼
Java FreeMarker 也一樣支持上述功能。
數據綁定 ${}
列表渲染 list指令
條件判斷 if指令
模板 FTL
事件 原生事件
引用 import include 指令
內建函數 好比『時間格式化』
可在視圖中應用的腳本語言 宏 marco
...
複製代碼
## 小程序的運行過程
咱們在微信上打開一個小程序
微信客戶端在打開小程序以前,會把整個小程序的代碼包下載到本地。
微信 App 從微信服務器下載小程序的文件包
爲了流暢的用戶體驗和性能問題,小程序的文件包不能超過 2M。另外要注意,小程序目錄下的全部文件上傳時候都會打到一個包裏面,因此儘可能少用圖片和第三方的庫,特別是圖片。
解析 app.json 配置信息初始化導航欄,窗口樣式,包含的頁面列表
加載運行 app.js 初始化小程序,建立 app 實例
根據 app.json,加載運行第一個頁面初始化第一個 Page
路由切換
以棧的形式維護了當前的全部頁面。最多 5 個頁面。出棧入棧
## 解決小程序接口不支持 Promise 的問題
小程序的全部接口,都是經過傳統的回調函數形式來調用的。回調函數真正的問題在於他剝奪了咱們使用 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。
Props 傳遞 —— Render 渲染
若是你有看過 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"
})
}
}))
複製代碼
最終效果展現:
項目源碼地址: github.com/ikcamp/xcx-…
直播視頻地址: www.cctalk.com/v/151373616…
iKcamp官網:www.ikcamp.com
iKcamp新課程推出啦~~~~~開始免費連載啦~每週2更共11堂iKcamp課|基於Koa2搭建Node.js實戰項目教學(含視頻)| 課程大綱介紹
2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!