帶你玩轉小程序開發實踐|含直播回顧視頻

做者:張利濤,視頻課程《微信小程序教學》、《基於Koa2搭建Node.js實戰項目教學》主編,滬江前端架構師php

本文原創,轉載請註明做者及出處html

小程序和 H5 區別

咱們不同,不同,不同。前端

運行環境 runtime

首先從官方文檔能夠看到,小程序的運行環境並非瀏覽器環境: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

因此,嚴格來講,小程序是微信定製的混合開發模式。

在 JavaScript 的基礎上,小程序作了一些修改,以方便開發小程序。

  • 增長 App 和 Page 方法,進行程序和頁面的註冊。【增長了 Component】
  • 增長 getApp 和 getCurrentPages 方法,分別用來獲取 App 實例和當前頁面棧。
  • 提供豐富的 API,如微信用戶數據,掃一掃,支付等微信特有能力。【調用原生組件:Cordova、ReactNative、Weex 等】
  • 每一個頁面有獨立的做用域,並提供模塊化能力。
  • 因爲框架並不是運行在瀏覽器中,因此 JavaScript 在 web 中一些能力都沒法使用,如 document,window 等。【小程序的 JsCore 環境】
  • 開發者寫的全部代碼最終將會打包成一份 JavaScript,並在小程序啓動的時候運行,直到小程序銷燬。相似 ServiceWorker,因此邏輯層也稱之爲 App Service。

與傳統的 HTML 相比,WXML 更像是一種模板式的標籤語言

從實踐體驗上看,咱們能夠從小程序視圖上看到 Java FreeMarker 框架、Velocity、smarty 之類的影子。

小程序視圖支持以下

數據綁定 {{}}
列表渲染 wx:for
條件判斷 wx:if
模板 tempalte
事件 bindtap
引用 import include
可在視圖中應用的腳本語言  wxs
...
複製代碼

Java FreeMarker 也一樣支持上述功能。

數據綁定 ${}
列表渲染 list指令
條件判斷 if指令
模板 FTL
事件 原生事件
引用 import include 指令
內建函數 好比『時間格式化』
可在視圖中應用的腳本語言 宏 marco
...
複製代碼

## 小程序的運行過程

  1. 咱們在微信上打開一個小程序
    微信客戶端在打開小程序以前,會把整個小程序的代碼包下載到本地。

  2. 微信 App 從微信服務器下載小程序的文件包
    爲了流暢的用戶體驗和性能問題,小程序的文件包不能超過 2M。另外要注意,小程序目錄下的全部文件上傳時候都會打到一個包裏面,因此儘可能少用圖片和第三方的庫,特別是圖片。

  3. 解析 app.json 配置信息初始化導航欄,窗口樣式,包含的頁面列表

  4. 加載運行 app.js 初始化小程序,建立 app 實例

  5. 根據 app.json,加載運行第一個頁面初始化第一個 Page

  6. 路由切換
    以棧的形式維護了當前的全部頁面。最多 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() {}
  }
})
複製代碼

結合 Redux 實現組件通訊

在 React 項目中 Redux 是如何工做的

  • 單一數據源

    整個應用的 state 被儲存在一棵 object tree 中,而且這個 object tree 只存在於惟一一個 store 中。

  • State 是隻讀的

    唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通對象

  • 使用純函數來執行修改

    爲了描述 action 如何改變 state tree ,你須要編寫 reducers。

  • Props 傳遞 —— Render 渲染

若是你有看過 Redux 的源碼就會發現,上述的過程能夠簡化描述以下:

  1. 訂閱:監聽狀態————保存對應的回調
  2. 發佈:狀態變化————執行回調函數
  3. 同步視圖:回調函數同步數據到視圖

第三步:同步視圖,在 React 中,State 發生變化後會觸發 Render 來更新視圖。

而小程序中,若是咱們經過 setData 改變 data,一樣能夠更新視圖。

因此,咱們實現小程序組件通訊的思路以下:

  1. 觀察者模式/發佈訂閱模式
  2. 裝飾者模式/Object.defineProperty (Vuejs 的設計路線)

在小程序中實現組件通訊

先預覽下咱們的最終項目結構:

├── 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
複製代碼

1. 實現『發佈訂閱』功能

首先,咱們從 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,
})
複製代碼

2. 利用 『裝飾者模式』,對小程序的生命週期進行包裝,狀態發生變化時候,若是狀態值不同,就同步 setData

// 引用了 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實戰項目教學(含視頻)| 課程大綱介紹

滬江iKcamp出品微信小程序教學共5章16小節彙總(含視頻)


2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章
相關標籤/搜索