资讯专栏INFORMATION COLUMN

带你玩转小程序开发实践|含直播回顾视频

fxp / 1524人阅读

摘要:小程序的视图层目前使用作为渲染载体,而逻辑层是由独立的作为运行环境。比如小程序的,通信一次就像是写情书所以,严格来说,小程序是微信定制的混合开发模式。出栈入栈解决小程序接口不支持的问题小程序的所有接口,都是通过传统的回调函数形式来调用的。

</>复制代码

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

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

小程序和 H5 区别

小程序的运行过程

解决小程序接口不支持 Promise 的问题

小程序组件化开发及通信

小程序和 H5 区别

</>复制代码

  1. 我们不一样,不一样,不一样。
运行环境 runtime

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

</>复制代码

  1. 小程序框架提供了自己的视图层描述语言 WXML 和 WXSS,以及基于 JavaScript 的逻辑层框架,并在视图层与逻辑层间提供了数据传输和事件系统,可以让开发者可以方便的聚焦于数据与逻辑上。
  2. 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。
  3. 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。同一进程内的 WebView 实际上会共享一个 JS VM,如果 WebView 内 JS 线程正在执行渲染或其他逻辑,会影响 evaluateJavascript 脚本的实际执行时间,另外多个 WebView 也会抢占 JS VM 的执行权限;另外还有 JS 本身的编译执行耗时,都是影响数据传输速度的因素。

而所谓的运行环境,对于任何语言的运行,它们都需要有一个环境——runtime。浏览器和 Node.js 都能运行 JavaScript,但它们都只是指定场景下的 runtime,所有各有不同。而小程序的运行环境,是微信定制化的 runtime。

大家可以做一个小实验,分别在浏览器环境和小程序环境打开各自的控制台,运行下面的代码来进行一个 20 亿次的循环:

</>复制代码

  1. var k
  2. for (var i = 0; i < 2000000000; i++) {
  3. k = i
  4. }

浏览器控制台下运行时,当前页面是完全不能动,因为 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 之类的影子。

小程序视图支持如下

</>复制代码

  1. 数据绑定 {{}}
  2. 列表渲染 wx:for
  3. 条件判断 wx:if
  4. 模板 tempalte
  5. 事件 bindtap
  6. 引用 import include
  7. 可在视图中应用的脚本语言 wxs
  8. ...

Java FreeMarker 也同样支持上述功能。

</>复制代码

  1. 数据绑定 ${}
  2. 列表渲染 list指令
  3. 条件判断 if指令
  4. 模板 FTL
  5. 事件 原生事件
  6. 引用 import include 指令
  7. 内建函数 比如『时间格式化』
  8. 可在视图中应用的脚本语言 宏 marco
  9. ...
 小程序的运行过程

我们在微信上打开一个小程序
微信客户端在打开小程序之前,会把整个小程序的代码包下载到本地。

微信 App 从微信服务器下载小程序的文件包
为了流畅的用户体验和性能问题,小程序的文件包不能超过 2M。另外要注意,小程序目录下的所有文件上传时候都会打到一个包里面,所以尽量少用图片和第三方的库,特别是图片。

解析 app.json 配置信息初始化导航栏,窗口样式,包含的页面列表

加载运行 app.js
初始化小程序,创建 app 实例

根据 app.json,加载运行第一个页面初始化第一个 Page

路由切换
以栈的形式维护了当前的所有页面。最多 5 个页面。出栈入栈

 解决小程序接口不支持 Promise 的问题

小程序的所有接口,都是通过传统的回调函数形式来调用的。回调函数真正的问题在于他剥夺了我们使用 return 和 throw 这些关键字的能力。而 Promise 很好地解决了这一切。

那么,如何通过 Promise 的方式来调用小程序接口呢?

查看一下小程序的官方文档,我们会发现,几乎所有的接口都是同一种书写形式:

</>复制代码

  1. wx.request({
  2. url: "test.php", //仅为示例,并非真实的接口地址
  3. data: {
  4. x: "",
  5. y: ""
  6. },
  7. header: {
  8. "content-type": "application/json" // 默认值
  9. },
  10. success: function(res) {
  11. console.log(res.data)
  12. },
  13. fail: function(res) {
  14. console.log(res)
  15. }
  16. })

所以,我们可以通过简单的 Promise 写法,把小程序接口装饰一下。代码如下:

</>复制代码

  1. wx.request2 = (option = {}) => {
  2. // 返回一个 Promise 实例对象,这样就可以使用 thenthrow
  3. return new Promise((resolve, reject) => {
  4. option.success = res => {
  5. // 重写 API 的 success 回调函数
  6. resolve(res)
  7. }
  8. option.fail = res => {
  9. // 重写 API 的 fail 回调函数
  10. reject(res)
  11. }
  12. wx.request(option) // 装饰后,进行正常的接口请求
  13. })
  14. }

上述代码简单的展现了如何把一个请求接口包装成 Promise 形式。但在实战项目中,可能有多个接口需要我们去包装处理,每一个都多带带包装是不现实的。这时候,我们就需要用一些技巧来处理了。

其实思路很简单:我们把需要 Promise 化的『接口名字』存放在一个『数组』中,然后对这个数组进行循环处理。

这里我们利用了 ECMAScript5 的特性 Object.defineProperty 来重写接口的取值过程。

</>复制代码

  1. let wxKeys = [
  2. // 存储需要Promise化的接口名字
  3. "showModal",
  4. "request"
  5. ]
  6. // 扩展 Promise 的 finally 功能
  7. Promise.prototype.finally = function(callback) {
  8. let P = this.constructor
  9. return this.then(
  10. value => P.resolve(callback()).then(() => value),
  11. reason =>
  12. P.resolve(callback()).then(() => {
  13. throw reason
  14. })
  15. )
  16. }
  17. wxKeys.forEach(key => {
  18. const wxKeyFn = wx[key] // 将wx的原生函数临时保存下来
  19. if (wxKeyFn && typeof wxKeyFn === "function") {
  20. // 如果这个值存在并且是函数的话,进行重写
  21. Object.defineProperty(wx, key, {
  22. get() {
  23. // 一旦目标对象访问该属性,就会调用这个方法,并返回结果
  24. // 调用 wx.request({}) 时候,就相当于在调用此函数
  25. return (option = {}) => {
  26. // 函数运行后,返回 Promise 实例对象
  27. return new Promise((resolve, reject) => {
  28. option.success = res => {
  29. resolve(res)
  30. }
  31. option.fail = res => {
  32. reject(res)
  33. }
  34. wxKeyFn(option)
  35. })
  36. }
  37. }
  38. })
  39. }
  40. })

注: Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

用法也很简单,我们把上述代码保存在一个 js 文件中,比如 utils/toPromise.js,然后在 app.js 中引入就可以了:

</>复制代码

  1. import "./util/toPromise"
  2. App({
  3. onLoad() {
  4. wx
  5. .request({
  6. url: "http://www.weather.com.cn/data/sk/101010100.html"
  7. })
  8. .then(res => {
  9. console.log("come from Promised api, then:", res)
  10. })
  11. .catch(err => {
  12. console.log("come from Promised api, catch:", err)
  13. })
  14. .finally(res => {
  15. console.log("come from Promised api, finally:")
  16. })
  17. }
  18. })
小程序组件化开发

小程序从 1.6.3 版本开始,支持简洁的组件化编程

官方支持组件化之前的做法

</>复制代码

  1. // 组件内部实现
  2. export default class TranslatePop {
  3. constructor(owner, deviceInfo = {}) {
  4. this.owner = owner;
  5. this.defaultOption = {}
  6. }
  7. init() {
  8. this.applyData({...})
  9. }
  10. applyData(data) {
  11. let optData = Object.assign(this.defaultOption, data);
  12. this.owner && this.owner.setData({
  13. translatePopData: optData
  14. })
  15. }
  16. }
  17. // index.js 中调用
  18. translatePop = new TranslatePop(this);
  19. translatePop.init();

实现方式比较简单,就是在调用一个组件时候,把当前环境的上下文 content 传递给组件,在组件内部实现 setData 调用。

应用官方支持的方式来实现

官方组件示例:

</>复制代码

  1. Component({
  2. properties: {
  3. // 这里定义了innerText属性,属性值可以在组件使用时指定
  4. innerText: {
  5. type: String,
  6. value: "default value"
  7. }
  8. },
  9. data: {
  10. // 这里是一些组件内部数据
  11. someData: {}
  12. },
  13. methods: {
  14. // 这里是一个自定义方法
  15. customMethod: function() {}
  16. }
  17. })
结合 Redux 实现组件通信

在 React 项目中 Redux 是如何工作的

单一数据源

</>复制代码

  1. 整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

State 是只读的

</>复制代码

  1. 惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象

使用纯函数来执行修改

</>复制代码

  1. 为了描述 action 如何改变 state tree ,你需要编写 reducers。

Props 传递 —— Render 渲染

如果你有看过 Redux 的源码就会发现,上述的过程可以简化描述如下:

订阅:监听状态————保存对应的回调

发布:状态变化————执行回调函数

同步视图:回调函数同步数据到视图

第三步:同步视图,在 React 中,State 发生变化后会触发 Render 来更新视图。

而小程序中,如果我们通过 setData 改变 data,同样可以更新视图。

所以,我们实现小程序组件通信的思路如下:

观察者模式/发布订阅模式

装饰者模式/Object.defineProperty (Vuejs 的设计路线)

在小程序中实现组件通信

先预览下我们的最终项目结构:

</>复制代码

  1. ├── components/
  2. │ ├── count/
  3. │ ├── count.js
  4. │ ├── count.json
  5. │ ├── count.wxml
  6. │ ├── count.wxss
  7. │ ├── footer/
  8. │ ├── footer.js
  9. │ ├── footer.json
  10. │ ├── footer.wxml
  11. │ ├── footer.wxss
  12. ├── pages/
  13. │ ├── index/
  14. │ ├── ...
  15. │ ├── log/
  16. │ ├── ...
  17. ├── reducers/
  18. │ ├── counter.js
  19. │ ├── index.js
  20. │ ├── redux.min.js
  21. ├── utils/
  22. │ ├── connect.js
  23. │ ├── shallowEqual.js
  24. │ ├── toPromise.js
  25. ├── app.js
  26. ├── app.json
  27. ├── app.wxss
1. 实现『发布订阅』功能

首先,我们从 cdn 或官方网站获取 redux.min.js,放在结构里面

创建 reducers 目录下的文件:

</>复制代码

  1. // /reducers/index.js
  2. import { createStore, combineReducers } from "./redux.min.js"
  3. import counter from "./counter"
  4. export default createStore(combineReducers({
  5. counter: counter
  6. }))
  7. // /reducers/counter.js
  8. const INITIAL_STATE = {
  9. count: 0,
  10. rest: 0
  11. }
  12. const Counter = (state = INITIAL_STATE, action) => {
  13. switch (action.type) {
  14. case "COUNTER_ADD_1": {
  15. let { count } = state
  16. return Object.assign({}, state, { count: count + 1 })
  17. }
  18. case "COUNTER_CLEAR": {
  19. let { rest } = state
  20. return Object.assign({}, state, { count: 0, rest: rest+1 })
  21. }
  22. default: {
  23. return state
  24. }
  25. }
  26. }
  27. export default Counter

我们定义了一个需要传递的场景值 count,用来代表例子中的『点击次数』,rest 代表『重置次数』。

然后在 app.js 中引入,并植入到小程序全局中:

</>复制代码

  1. //app.js
  2. import Store from "./reducers/index"
  3. App({
  4. Store,
  5. })
2. 利用 『装饰者模式』,对小程序的生命周期进行包装,状态发生变化时候,如果状态值不一样,就同步 setData

</>复制代码

  1. // 引用了 react-redux 中的工具函数,用来判断两个状态是否相等
  2. import shallowEqual from "./shallowEqual"
  3. // 获取我们在 app.js 中植入的全局变量 Store
  4. let __Store = getApp().Store
  5. // 函数变量,用来过滤出我们想要的 state,方便对比赋值
  6. let mapStateToData
  7. // 用来补全配置项中的生命周期函数
  8. let baseObj = {
  9. __observer: null,
  10. onLoad() { },
  11. onUnload() { },
  12. onShow() { },
  13. onHide() { }
  14. }
  15. let config = {
  16. __Store,
  17. __dispatch: __Store.dispatch,
  18. __destroy: null,
  19. __observer() {
  20. // 对象中的 super,指向其原型 prototype
  21. if (super.__observer) {
  22. super.__observer()
  23. return
  24. }
  25. const state = __Store.getState()
  26. const newData = mapStateToData(state)
  27. const oldData = mapStateToData(this.data || {})
  28. if (shallowEqual(oldData, newData)) {// 状态值没有发生变化就返回
  29. return
  30. }
  31. this.setData(newData)
  32. },
  33. onLoad() {
  34. super.onLoad()
  35. this.__destroy = this.__Store.subscribe(this.__observer)
  36. this.__observer()
  37. },
  38. onUnload() {
  39. super.onUnload()
  40. this.__destroy && this.__destroy() & delete this.__destroy
  41. },
  42. onShow() {
  43. super.onShow()
  44. if (!this.__destroy) {
  45. this.__destroy = this.__Store.subscribe(this.__observer)
  46. this.__observer()
  47. }
  48. },
  49. onHide() {
  50. super.onHide()
  51. this.__destroy && this.__destroy() & delete this.__destroy
  52. }
  53. }
  54. export default (mapState = () => { }) => {
  55. mapStateToData = mapState
  56. return (options = {}) => {
  57. // 补全生命周期
  58. let opts = Object.assign({}, baseObj, options)
  59. // 把业务代码中的 opts 配置对象,指定为 config 的原型,方便『装饰者调用』
  60. Object.setPrototypeOf(config, opts)
  61. return config
  62. }
  63. }

调用方法:

</>复制代码

  1. // pages/index/index.js
  2. import connect from "../../utils/connect"
  3. const mapStateToProps = (state) => {
  4. return {
  5. counter: state.counter
  6. }
  7. }
  8. Page(connect(mapStateToProps)({
  9. data: {
  10. innerText: "Hello 点我加1哦"
  11. },
  12. bindBtn() {
  13. this.__dispatch({
  14. type: "COUNTER_ADD_1"
  15. })
  16. }
  17. }))

最终效果展示:

项目源码地址:
https://github.com/ikcamp/xcx-redux

直播视频地址:
https://www.cctalk.com/v/15137361643293

</>复制代码

  1. iKcamp官网:https://www.ikcamp.com

</>复制代码

  1. iKcamp新课程推出啦~~~~~开始免费连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍

    沪江iKcamp出品微信小程序教学共5章16小节汇总(含视频)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92306.html

相关文章

  • 老司机【分享】带你玩转阿里云服务器

    摘要:阿里云是国内云服务器市场的龙头,性价比高,速度快又安全,是站长建站首选的云服务器之一。作为一个老司机,福利吧也和大家分享一下我的阿里云推广经验,教大家如何免费推广云大使。阿里云是国内云服务器市场的龙头,性价比高,速度快又安全,是站长建站首选的云服务器之一。福利吧使用的也是阿里云服务器,是折腾了很多次网站搬家后,才选择了阿里云。身边好几个站长最后都殊途同归,用了阿里云,可见阿里云服务器性能确实...

    banana_pi 评论0 收藏0
  • 【深入浅出容器云】五分钟带你玩转Docker容器服务

    摘要:深入浅出容器云系列文章是由时速云出品,本文是第二篇,欢迎大家不吝赐教。容器服务是一种高度可扩展的高性能容器管理服务,服务于应用的完整生命周期。存储卷容器服务支持有状态和无状态服务。当容器重新部署时也会随着容器在不同主机之间迁移。 导语:随着以Docker为代表的容器技术在国内的迅速发展,容器云也逐渐被广大开发者所熟知,但容器云(CaaS)相比传统的云主机(IaaS)在实际应用中还存在着...

    AlexTuan 评论0 收藏0
  • 带你玩转 JavaScript ES6 (七) - 异步

    摘要:说明处理方法被异步执行了。意味着操作成功完成。状态的对象可能触发状态并传递一个值给相应的状态处理方法,也可能触发失败状态并传递失败信息。注生命周期相关内容引用自四使用和异步加载图片这是官方给出的示例,部分的代码如下 带你玩转 JavaScript ES6 (七) - 异步 本文同步带你玩转 JavaScript ES6 (七) - 异步,转载请注明出处。 本章我们将学习 ES6 中的 ...

    he_xd 评论0 收藏0
  • 带你玩转css3的3D!

    摘要:透视即是以现实的视角来看屏幕上的事物,从而展现的效果。旋转则不再是平面上的旋转,而是三维坐标系的旋转,就包括轴,轴,轴旋转。必须与属性一同使用,而且只影响转换元素。可自由转载引用,但需署名作者且注明文章出处。 showImg(https://segmentfault.com/img/bVzJoZ); 话不多说,先上demo 酷炫css3走马灯/正方体动画: https://bupt-...

    Panda 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<