博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
带你玩转小程序开发实践|含直播回顾视频
阅读量:6414 次
发布时间:2019-06-23

本文共 9281 字,大约阅读时间需要 30 分钟。

作者:张利涛,视频课程《微信小程序教学》、《基于Koa2搭建Node.js实战项目教学》主编,沪江前端架构师

本文原创,转载请注明作者及出处

小程序和 H5 区别

我们不一样,不一样,不一样。

运行环境 runtime

首先从官方文档可以看到,小程序的运行环境并不是浏览器环境:

小程序框架提供了自己的视图层描述语言 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 亿次的循环:

var kfor (var i = 0; i < 2000000000; i++) {  k = i}复制代码

浏览器控制台下运行时,当前页面是完全不能动,因为 JS 和视图共用一个线程,相互阻塞。

小程序控制台下运行时,当前视图可以动,如果绑定有事件,也会一样触发,只不过事件的回调需要在 『循环结束』 之后。

视图层和逻辑层如果共用一个线程,优点是通信速度快(离的近就是好),缺点是相互阻塞。比如浏览器。

视图层和逻辑层如果分处两个环境,优点是相互不阻塞,缺点是通信成本高(异地恋)。比如小程序的 setData,通信一次就像是写情书!

所以,严格来说,小程序是微信定制的混合开发模式。

在 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.jsimport { createStore, combineReducers } from './redux.min.js'import counter from './counter'export default createStore(combineReducers({  counter: counter}))// /reducers/counter.jsconst 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.jsimport Store from './reducers/index'App({  Store,})复制代码

2. 利用 『装饰者模式』,对小程序的生命周期进行包装,状态发生变化时候,如果状态值不一样,就同步 setData

// 引用了 react-redux 中的工具函数,用来判断两个状态是否相等import shallowEqual from './shallowEqual'// 获取我们在 app.js 中植入的全局变量 Storelet __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.jsimport 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"    })  }}))复制代码

最终效果展示:

项目源码地址:

直播视频地址:

iKcamp官网:

iKcamp新课程推出啦~~~~~


2019年,iKcamp原创新书《Koa与Node.js开发实战》已在京东、天猫、亚马逊、当当开售啦!

你可能感兴趣的文章
HBase应用笔记:通过HBase Shell与HBase交互(转自:Taobao QA Team)
查看>>
SAP 图形页面
查看>>
Selenium2学习(十一)-- select下拉框
查看>>
echarts系列之动态修改柱状图颜色
查看>>
(4.1)LingPipe在Eclipse中的运行
查看>>
表格模版编辑器的一些思路
查看>>
ActiveMQ内存配置和密码设置
查看>>
Unity5 BakeGI(Mixed Lighting)小记
查看>>
十六、Mediator 仲载者设计模式
查看>>
jsonToxls jsonTocsv csvTojson xlstocsv 文件转换
查看>>
黑盒测试实践-华科软硕1706班1组 2017.11.30记录
查看>>
Matplotlib基础学习
查看>>
搭建GIT服务端
查看>>
Calendar时间操作
查看>>
iOS多线程_02_多线程的安全问题
查看>>
通过Ajax post Json类型的数据
查看>>
leetcode------Subsets II
查看>>
leetcode------3Sum
查看>>
搞懂分布式技术2:分布式一致性协议与Paxos,Raft算法
查看>>
delphi定义结构体
查看>>