微信小程序全面實戰,架構設計 && 躲坑攻略
最近集中开发了两款微信小程序,分别是好奇心日历(每天一条辞典+一个小投票)和好奇心日报(轻量版),直接上图:
Paste_Image.png
本文将结合具体的实战经验,主要介绍微信小程序的基础知识、开发中遇到的难点、项目的架构设计、最佳实践以及踩过的坑。文章内容较多,如果想看架构设计和躲坑技巧,请直接浏览后面的正文,简书没有目录,也挺伤感的。
文末有好奇心日报小程序的二维码,欢迎围观。
值得再次声明的是:微信小程序的内容部分是hybrid模式,并非原生,所以性能并不好,绑定的tap事件也有明显的延迟。
每一个由边框围起来的部分,都是一个最小粒度的原生view
如上图所示,每一个由边框围起来的部分,都是一个最小粒度的原生view,可以看出,整个微信小程序的内容部分,就是一个原生view。
小程序有哪些基础知识?
一个完整的微信小程序是由一个App实例和多个Page实例构成,其中App实例表示该小程序应用,多个Page表示该小程序的多个页面。
此外,微信小程序并没有提供自定义组件的方式,这就导致微信小程序在开发较复杂应用时,可能会比较艰难。
微信小程序本身很简单,和一个模板语言的难度几乎相当,翻翻官方教程就可以开始动手搞。
我也建议大家先动起来,然后再细致啃啃官方文档。由于微信官方文档仍在不断大幅更新中,所以务必查看最新官方文档。
微信小程序的基础知识主要分为以下几个部分:
两种配置文件 && 两个核心函数
WXML模板语法,页面渲染
页面间的跳转
交互事件
官方组件和官方API
后文会就每个部分简单介绍介绍...
两种配置文件 && 两个核心函数
app.json
应用的全局配置文件
app.json
是针对微信小程序的全局配置,主要包含以下几个配置:
pages:页面路径的数组,表示小程序要加载的所有页面,其中数组第一项代表小程序的初始页面。
window:微信原生功能,定制化不强。可设置小程序的状态栏、导航条、标题以及窗口背景色。
tabBar:微信原生功能,定制化不强。适用于常规的Tab应用,Tab栏可置于顶部或底部;tabBar是一个数组,仅支持2-5个tab。
networkTimeout:配置小程序网络请求的超时时间。
debug:调试模式开关,开发模式下建议开启,正式发布别忘了关闭。
app.json配置的主要区域
page.json
页面的全局配置文件
除了上面提到的全局配置,每个页面还可以单独配置page.json
,page.json
会覆盖app.json
中的配置,并只对当前页面生效。page.json
只能对window配置,有两个比较有用的配置项分别是:
enablePullDownRefresh:是否开启下拉刷新
disableScroll:只能在page.json配置,禁止页面上下滚动,猜测可以实现完美滑屏滑动(未验证)
App()
小程序注册入口,全局唯一
App()用来注册一个小程序,全局只有一个,全局的数据也可以放到这里面来操作。
// 注册微信小程序,全局只有一个let appConfig = { // 小程序生命周期的各个阶段 onLaunch: function(){}, onShow: function(){}, onHide: function(){}, onError: function(){}, // 自定义函数或者属性 ... }; App(appConfig);// 在别的地方可以获取这个全局唯一的小程序实例const app = getApp();
小程序并没有提供销毁的方式,所以只有当小程序进入后台一定时间、或者系统资源占用过高的时候,才会被真正的销毁。
Page()
页面注册入口
Page()用来注册一个页面,维护该页面的生命周期以及数据。
// 注册微信小程序,全局只有一个let pageConfig = { data: {}, // 页面生命周期的各个阶段 onLoad: function(){}, onShow: function(){}, onReady: function(){}, onHide: function(){}, onUnload: function(){}, onPullDownRefresh: function(){}, onReachBottom: function(){}, onShareAppMessage: function(){}, // 自定义函数或者属性 ... }; Page(pageConfig);// 获取页面堆栈,表示历史访问过的页面,最后一个元素为当前页面const page = getCurrentPages();
关于各个生命周期的细节以及流程,参考下图,可以细细品味:
Paste_Image.png
app.json
和 page.json
维护了应用和页面的配置属性。App()
和 Page()
维护了应用和页面的各个生命周期以及数据。
那么,APP
和 Page
如何将数据传递到页面呢?页面又是如何渲染呢?
WXML模板语法,页面渲染
小程序虽然是hybrid模式,但并不使用HTML渲染,而是全部通过自定义标签来渲染页面。这样做的好处我不清楚,但问题却不少:不能跨浏览器、富文本解析困难,iframe视频不支持,没办法外链跳转。
和所有的模板语言一样,WXML支持数据绑定、条件渲染、循环、模块化等功能。
数据绑定
在 WXML 中可以使用{{}}
将 Page
的变量或者表达式包裹起来,实现数据绑定,举个例子:
// 将message的值渲染到view中<view> {{ message }} </view>// 将id的值渲染到view的id属性中<view id="item-{{id}}"> </view>// 根据isSelected的值,输出不同的class<view class="{{isSelected ? 'selected' : ''}}"> HelloWorld </view>// 结合template,可以传入更复杂的数据<template is="objectCombine" data="{{...article, categoty, tag: '埃隆马斯克'}}"></template>
条件渲染
条件渲染,适合根据数据输出不同状态的 WXML,举个例子:
<view wx:if="{{length > 5}}"> 1 </view><view wx:elif="{{length > 2}}"> 2 </view><view wx:else> 3 </view>
循环渲染
循环渲染,适合遍历数据输出多段 WXML,举个例子:
// wx:for 表示需要遍历的数据// wx:key 使用唯一的字段来标识,有利于提升性能// wx:for-index 表示数组的下标// wx:for-item 表示数组的元素<view wx:for="{{[{id:1, message: 'HelloWorld1'}, {id:2, message: 'HelloWorld2'}]}}" wx:key="id" wx:for-index="idx" wx:for-item="item"> {{idx}}: {{item.message}} </view>
wx:key
有利于提升重新渲染时的效率,建议添加
模块化
WXML的模块化,可以让我们复用一些wxml片段,还挺重要的,举个例子:
// 引入wxml模块<import class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="../../components/grid-article/index"></import> <block wx:for="{{posts}}" wx:for-item="post" wx:key="id"> // 调用wxml模块,同时可传入数据 <template is="grid-article" data="{{post}}"></template> </block>
数据和页面的状态是一一对应的,微信小程序中,设计一份好的数据结构,对于Page和页面的代码都有很大的帮助。
微信小程序并不支持a标签,那么多个页面之间如何跳转呢?
页面间的跳转
小程序以栈的形式维护了历史访问的所有页面,并提供了多种页面间的跳转方式;结合前文提到的App()和Page()的各个生命周期,不同的跳转方式和不同的生命周期关联,如下图:
Paste_Image.png
举个例子,Tab 切换对应的生命周期(以 A、B 页面为 Tabbar 页面,C 是从 A 页面打开的页面,D 页面是从 C 页面打开的页面为例)
Paste_Image.png
好了,APP和Page负责维护小程序的生命周期和数据,模板负责接受数据完成页面渲染,页面间的跳转负责将多个页面贯穿起来,那么,如何发生交互呢?接下来我们简单说一下事件。
交互事件
事件绑定
// bindtap 和 catchtap的区别在于// bindtap 不会阻止事件冒泡// catchtap会冒泡事件冒泡<view id="tapTest" data-hi="WeChat" bindtap="tapName"> Click me! </view><view id="tapTest" data-hi="WeChat" catchtap="tapName"> Click me! </view>// 绑定的函数tapName只是一个函数名称,默认接受一个event对象作为参数Page({ tapName: function(event) { console.log(event) } })
接下来,另一个问题是:tapName()
如何接受自定义参数呢?
事件传参
传递自定义参数主要有两种方式:
第一种:将参数绑定到wxml标签上,然后通过event.target.dataset
获取
第二种:直接使用Page.data或其他数据
到目前为止,一个完整的小程序框架已经实现
小程序只有逻辑和视图两部分,而且不提供组件化解决方案
逻辑主要包含四个东西:两个配置文件 && 两个核心函数
视图很简单,模板语法稍微有点不完善
逻辑层的数据绑定到视图层是由小程序框架自动支持,数据变化,视图自动变化
视图层到逻辑层的,主要通过事件的方式来实现
视图之间的跳转,小程序也提供了它自己的方式,并不支持a标签
框架有了,小程序还提供了官方组件以便快速开发,提供了API以增强应用能力。
这块就不具体介绍了,也需要注意小程序的官方文档还在大规模的更新中,务必查看最新版
官方组件:https://mp.weixin.qq.com/debug/wxadoc/dev/component/?t=20161222
官方API:https://mp.weixin.qq.com/debug/wxadoc/dev/api/?t=20161222
微信小程序的基础知识就是以上的内容,并不复杂,边查边写都可以。
接下来会介绍更进阶一些的内容,内容主要结合好奇心日报这个小程序项目,先看效果:
好奇心日报实际效果图
如何设计微信小程序?
构建系统 && 目录结构
构建系统
由于微信小程序本身对工程化几乎没有任何的支持,所以动手搭建一份:wxapp-redux-starter。
使用gulp进行编译构建,主要功能包括:
集成了Redux
,数据管理更方便
开发过程中,使用xml
取代wxml
,对开发工具更友好
开发过程中,使用less
取代wxss
,功能更强大
引入es-promise
,以便可以创建并使用Promise
添加promisify
工具函数,可以便捷的将官方Api
转换成Pormise
模式
引入normalizr
,可以将数据扁平化,更方便进行数据管理
引入babel
自动进行ES2015
特性转换
对wxml/wxss/js/img
压缩,相对开发者工具提供的压缩,会减小一丢丢体积。
目录结构设计
按照pages、components、redux、vendors/libs、images几个核心部分拆分,直接上目录。
小程序工程目录
dist目录
:构建输出的文件存放到这个目录。
src目录
:开发模式的文件,包括app、页面、组件、图片等静态资源、辅助函数库、Redux数据管理器、第三方工具库。
gulpfile.js
:不用多说,gulp构建任务的入口文件。
package.json
:不用多说,管理者构建任务的依赖。
thirdPlugins
:由于小程序并不支持直接使用npm,我们可以自主拉取构建,然后拷贝到vendors里,有时候需要简单修改。
构建系统会将src目录下的代码,编译处理后输出到dist目录,小程序开发工具只需要引入dist目录即可。
有了构建工具,代码开发起来更舒心,但很快就遇到另外一个糟心的问题,那就是如何管理散布在各处的数据?
要知道,微信小程序并没有提供组件化方案,所以把组件写成无状态组件特别适合,但是页面管理太多数据很凌乱,有什么办法可以将数据集中管理呢?
引入Redux进行数据集中管理
关于Redux相关的内容,之前有三篇博客详细介绍,有兴趣的建议先移步:
Redux整体介绍:Redux 入门教程,应用的状态管理器
对State进行横向和纵向拆分设计:State设计,Redux 开发第一步
Reducer的最佳实践:Reducer 最佳实践,Redux 开发最重要的部分
这儿就简单介绍一下,如何在微信小程序中引入Redux 以及 如何将微信小程序和Redux连接起来。
引入Redux
直接在 thirdPlugins目录
运行 yarn add redux / npm install redux
,等redux安装好了之后,将 dist目录
的 redux.js/redux.min.js
拷贝到vendors目录中。
需要进行简单的修改才能使用,将压缩版208行代码 (i)
改成 (i || {})
即可。
简单修改,Redux就可以正常使用
连接微信小程序和Redux
将Redux和微信小程序连接起来才会真的有用处。找了个现成的方案charleyw/wechat-weapp-redux,可以直接使用。
一个完整的Redux方案如下,包括:将Store注入到App中、将state的数据和reducer的方法映射到Page中。一旦state发生变化,Page.data也会更新,进而触发页面的重新渲染。
// APP的逻辑import { createStore, applyMiddleware, combineReducers } from './vendors/redux.js';import thunk from './vendors/redux-thunk.js';import { Provider } from './vendors/weapp-redux.js';// import reducersimport { rootReducer } from './redux/reducer.js';// 从Storage读取数据let entities = wx.getStorageSync('entities') || {};const store = createStore( rootReducer, { // 将读取的数据注入到store中 entities: entities }, applyMiddleware( thunk ) );let appConfig = { onLaunch: function() {}, onHide: function() { let state = store.getState(), cacheEntities = {}; // 体积大于2M,直接清空,防止缓存占用过大体积 if (sizeof(state.entities) <= 2 * 1024 * 1024) { cacheEntities = state.entities; } // 退出时将entities缓存下来 wx.setStorageSync('entities', cacheEntities); } }; App(Provider(store)(appConfig));// Page的逻辑import { connect } from '../../vendors/weapp-redux.js';import { fetchArticleDetail } from '../../redux/models/articles.js';let pageConfig = { data: { id: 0, postsHash: {} }, onLoad: function(params) { var me = this, { id, postsHash } = me.data; me.fetchArticleDetail(id, function() {}, function() {}); } }// 考虑到列表页已经获取到部分数据// 为了在详情页第一时间利用这些数据,我们将params传入// 防止以后需要用data的数据,我们将data也一并传入let mapStateToData = (state, params, data) => { return { id: params.id, postsHash: state.entities.posts } };let mapDispatchToPage = dispatch => ({ fetchArticleDetail: (id, callback, errorCallback) => dispatch(fetchArticleDetail(id, callback, errorCallback)), }); pageConfig = connect(mapStateToData, mapDispatchToPage)(pageConfig) Page(pageConfig);
需要注意的是,为了保证第一时间能拿到数据,我们对wechat-weapp-redux/src/connect.js
做了优化调整,修改的地方如下
// 修改了以下两个函数// 可以对照原项目修改,也可以直接拿我的模板项目使用function handleChange(options) { if (!this.unsubscribe) { return } const state = this.store.getState(); // 将data也一并传入 const mappedState = mapState(state, options, this.data); if (!this.data || shallowEqual(this.data, mappedState)) { return; } this.setData(mappedState) }function onLoad(options) { this.store = app.store; if (!this.store) { warning("Store对象不存在!") } if (shouldSubscribe) { this.unsubscribe = this.store.subscribe(handleChange.bind(this, options)) // 第一次处理的时候也传入options handleChange.apply(this, [options]) } if (typeof _onLoad === 'function') { _onLoad.call(this, options) } }
引入Redux的优势
引入Redux的好处在于可以集中管理数据,并且让Page的代码保持绝对简单,让所有的组件都变成简单可复用的无状态组件。
此外,Redux还让离线缓存更方便,数据复用更简单。
引入Redux解决了数据散布各处的问题,参考Redux的管理思路,我们构思了一套简单组件化解决方案:假设我们把所有的组件都设计成无状态组件,而组件的数据来源是Page.data,那么我们是否也可以将组件数据的管理抽离到一个单独的文件中呢?接下来讲讲我们使用的简单的组件化解决方案。
简单的组件化解决方案
这份组件化解决方案的核心就在于把组件的关联数据集中起来管理,只暴露出默认数据和数据的操作函数。
比如好奇心日报的详情页有个Toolbar,该Toolbar并不复杂,主要提供返回和点赞功能,其中点赞功能只对文章详情有效,研究所详情页会将点赞功能隐藏。
Toolbar组件
// components/toolbar/index.js 文件// 仅提供默认值,不需要和page中的数据保持同步let defaultData = { isPraised: false, praiseCount: 0, showPraiseIcon: false, };// 切换点赞状态function togglePraise() { // 本质上是修改Page.data中的toolbarData}// 返回上一级function navigateToBack(wx) { wx.navigateBack({ delta: 1 }); }module.exports = { defaultData, togglePraise, navigateToBack }// pages/articles/show.js 文件import Toolbar from '../../components/toolbar/index.js';let pageConfig = { data: { // 其他数据 id: 0, // Toolbar数据,单独的一份数据,便于维护 toolbarData: Toolbar.defaultData }, // 点赞或者取消赞 togglePraise: function() { let me = this; Toolbar.togglePraise.call(me); } }// 这儿的组件化并不是真正的组件化// 而是将组件相关的逻辑和函数抽离到单独的文件中,保证Page代码清晰。// 同时也为这部分组件逻辑复用提供了可能。// 本质上来说,抽离出的组件都是“操作Page.data的工具函数”,他们也是纯函数,和“操作state的reducer”类似。
这种Redux的组件化解决方案既简单又好用,保持一定的代码规范即可。这样设计当然是为了复用,同时也让Page的逻辑保持极度精简。
开发中遇到了哪些难点 && 微信小程序有多少坑?
微信小程序目前的确算不上公测的版本,开发者工具不完善、真机表现和开发环境差异很大、部分组件性能较差、部分功能有缺陷,只有经历了这些大坑,才会真的知道你有多“爱”微信小程序。这儿总结了开发中的难点以及微信小程序中遇到的比较明显的坑。
富文本解析
微信小程序并不支持HTML标签,所以对于富文本解析来说,难度较大,而且有些功能还没有办法实现,比如:iframe视频、连接跳转等
这块功能建议由后台统一转换,如果非得前端转换,建议参考下面的思路。
非常感谢 wxParse 这款组件,替我省了不少时间。推荐大家使用,期间遇到一些问题,也分享给大家。
wxParse 默认层级只支持10层html嵌套,如果想要支持更深的层级,在wxParse.xml复制几份template即可。
wxParse 提供了图片加载成功的回调wxParseImgLoad
,很好用。但如果富文本中的图片已经预设宽高比,那么可以不用依赖该回调,在html2jons.js中根据屏幕宽度直接计算出图片高度,先占位,可以避免页面频繁抖动的问题。
如果你的富文本中有自定义模块,对wxParse.xml中的template进行改造即可。
自定义模块样式
数据扁平化
具体如何扁平化,请移步上一篇博客 State设计,Redux 开发第一步。
这儿只简单介绍下扁平化应用场景:
好奇心日报的研究所是三级表结构:papers > questions > options
,后台返回的数据是三级嵌套数据,如果想要修改option.selected字段,需要三级嵌套循环!如果想要获取所有选中的option,需要三级嵌套循环!
页面展现速度优化
数据复用,比如复用列表页的数据,可以让详情页的标题等字段第一时间呈现出来。
离线缓存,同样可以让列表页和详情页第一时间呈现出来,甚至有可能减少请求数量。
无论是数据复用还是离线缓存,配合数据扁平化,都非常好用。
小程序默认设置代理,会和Shadowsocks等VPN冲突(最新版又坏了)
解决方法很简单,设置微信小程序不使用代理;或者临时关闭VPN即可。
上一版开发者工具已经解决该问题,最新版又坏了。
最新版微信小程序移除了对Promise的支持。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章