对焦10年前iPhone的发布时间,产品之神张小龙显然是想让这一天具有十分重要的历史意义。小程序发布之后,它终于揭开了最终面目,我们不得不承认,这一天,必定是一个新时代的开端。
作为一个第一批小程序的开发者,从小程序内测之初通过开发工具破解版开始尝试小程序,见证了小程序官方文档的每一次更新,踩过小程序的大多数坑,也见证了好几个小程序社区的逐步发展,到最终把自己的小程序过审上线,这个过程对我而言,收获良多。
为了对这段时间的收获做一个总结,也希望能将自己的经验分享给大家,所以有了这篇文章,让想要开发小程序的小伙伴们对比一下自己掌握的知识,距离开发一个成熟的小程序,到底还差多少。也让想要学习小程序的朋友,有一个大的方向。
2016这一整年来,前端开发发生了很重要的变化,比如ES6的全面普及,react的持续火热,vue的爆发式增长,所有人都知道学习这些东西很重要,但是为什么很多很多的新手朋友们往往只能了解皮毛而难以真正掌握呢?
那就是因为构建工具的门槛太高。
很多人在学习别人的代码的时候,会很惊讶的发现,为什么别人的代码,和浏览器能识别的样子,差距那么大?其实全都是构建工具的功劳。无论是ES6,还是react,组件化,模块化,这些东西统统都会通过构建工具,最终变成浏览器能识别的样子,也就是我们最初学习时所知道的那个简单样子,一个页面,包括css,html,js,image等
对于大多数团队来说,只要公司中有大神能搭建一个成熟的开发环境,对于这些知识的学习其实是容易很多的,这也是为什么,很多人在进入某个团队的时候成长速度会变得更快的原因所在。但是对于一个刚入门的新手来说,想要搭建一个能用的构建工具是非常困难的。
而对于这个门槛,小程序的开发工具,则完美的帮大家解决了这个问题。我们不需要额外搭建任何开发环境,只需要下载《微信web开发工具》,就可以看着小程序的文档开始写demo了。
所以如果你想要低成本的学习ES6,体验组件化的开发模式,小程序应该算是一个不错的选择。
熟悉文档
由于小程序是中文文档,所以我相信对大多数人来说,学习成本非常低。我们需要实现什么效果,需要用到什么组件和api,这些基础的东西就在文档里,我们只需要熟悉他们即可,如果你连文档都不熟悉,真的谈不上开发小程序了,但是小程序文档确实足够简单以致于对大多数人来说,过一遍就知道应该怎么玩了。
需要对html,css的知识有足够的熟悉程度
小程序的组件,仍然是基于html与css的知识来完成,我们只需要对html与css足够熟悉,就能够很轻松的完成小程序的布局。诸如文档流,包含块,BFC,定位系统,怪异盒模型,弹性盒模型等等知识,都能够用得到。
需要对JavaScript足够熟悉
和网页应用一样,在小程序里,所有的功能都由js完成。而由于三方插件的匮乏,对于js的封装能力和对数据的处理能力就会要求高一点。并且需要你对模块化有一定的认知。
需要有控制数据就能改变UI的思维
没有接触过类MVC模式框架的朋友,往往对于这个思维转变有一定程度上的难以接受。所以经常看到有人在学习angular和react时到处询问如何把jQuery引入进来,其实在99%的场景下,我们都不再需要获取到DOM节点,只需要操作数据和方法就能完成所有的事情,我们需要有这样一个心理准备。
二、开发小程序,我们面临着什么样的挑战
如果你只是想要写一个demo,看着官方文档把一些组件,api体验一下,那是没有什么挑战的。但是如果我们想要开发一个成熟健全的小程序,那么面临的挑战就很多。
比如:
如何保存登录状态,UI状态等?
为了给用户节约流量,应该如何规划缓存机制?
不支持webview,我们如何展示html文章?
如何应对三方插件匮乏的状况?
没有提供明确的组件机制,我们应该如何处理组件?
不支持promise,我们如何处理异步?
小程序体积限制为1024k,我们应该如何优化代码与处理静态资源?
具体功能应该如何实现等等… …
当然,并不是每一个小程序都要考虑所有的问题,这里我从我自己开发的小程序的角度,跟大家分享一下,我遇到了哪些问题,如何解决,用了什么样的方案。
如何保存各种轻量缓存数据以及状态值
假如在一个简单的学习demo中,我们想要改变一个矩形的宽高,并把结果保存起来,只需要设定一个全局变量即可。这样我们就可以在任何地方知道这个矩形改变之后的宽高了,但是我们知道,为了防止变量污染,以及在多人开发中出现命名冲突等问题,我们的原则上是不能有全局变量的。那么如果不用全局变量,我们应该怎么做?
如果学习过react的同学,应该会对redux有所耳闻。redux就能够解决这个问题。但是redux对于小程序而言,由于功能过于强大,反而不太适合。因此,尝试自己造一个简单的轮子来解决这个问题。
小程序中,我们可以使用es6的模块化规范,来创建模块,引入模块等,如果对于模块化的开发思维还不太熟悉,建议先学习一下。
创建一个叫state.js的文件,该文件就是一个独立的模块,专门用来处理轻量的数据缓存
首先创建一个state对象,准备将数据以json的格式存起来
let states = {} // 存储变量
分别提供一个获取数据的方法,一个获取states全部数据的方法
function get (name) {
if(states[name]) { return states[name] }
return ''
}
function getStates () {
return states
}
再提供一个保存数据的方法,我们知道如果只保存一个key-value的数据很简单,但是我们想要实现react中,setState同样功能的话,则需要做一些特殊的处理。
function set (options, target) {
let keys = Object.keys(options)
let o = target ? target : states
keys.map( item => {
if(typeof o[item] == 'undefined') {
o[item] = options[item]
}
else {
if(utils.type(o[item]) == 'object') {
set(options[item], o[item])
} else {
o[item] = options[item]
}
}
return item
})
}
对外提供访问的接口,这个模块就算完成了。
module.exports = {
get: get,
getStates: getStates,
set: set
}
在别的模块中,使用方式如下
// 先引用
import state from './utils/state'
// 保存一个值
state.set('single', { c: 1, d: 2 })
// 查看一下single中的数据
state.get('single')
// 只修改single中的c值
state.set({
single: { c: 20 }
})
// 修改完成之后再查看一下结果,这也是set方法的厉害之处,这对于我们保存提供了极大的便利
虽然这个模块代码简单,但是state能够存储足够多的数据,它提供了全局变量给我们带来的便利,也避免了全局变量的弊端,甚至还给组件之间的交互提供了可能。如果你没有意识到这个模块的重要性,那么你还得多写几个demo细细的体会一下。
上面的state模块还需要什么重要的拓展吗?
我们试想一下,如果我们想要修改一个皮肤设置项,或者我们要切换白天/黑夜模式的外观,我们应该怎么做?为什么我的按钮一改变,全局的皮肤就能立即做出响应?
这个时候,我们需要掌握js设计模式中,一个极为重要的模式, 订阅-通知模式,或者叫做观察者模式,监听者模式都可以。在一个app中,如果我们想要做好缓存和用户体验,就会大量用到它。
在修改皮肤这个例子中,我们改变了按钮的状态值,这个状态改变值触发了一个事件,这个事件完成了对皮肤的修改。当然在此之前,我们还得将这个事件,与这个状态值绑定起来。
绑定事件 -> 点击按钮状态值改变 -> 触发事件 -> 皮肤修改完成
因此,该模式的原理也大概如下,我们首先需要将对应的事件保存起来与变量值的key,这个过程就叫做绑定,类似事件绑定。在状态值改变时,我们就发送一个通知,告诉我们的模块,状态值改变了,应该执行事件了,于是事件执行。
声明一个存储数据的数组
let events = [] // 存储事件
每一个绑定,都会以对象的形式,保存在数组中
events = [{
name: 'changeToNight',
handler: changeFn,
page: targetPage
}]
添加一个绑定事件
function bind (name, notification, targetPage) {
if (name && notification) {
if (!targetPage) {
console.error('bind error: 没有绑定页面对象')
return;
}
events.push({
name: name,
handler: notification,
page: targetPage
})
} else {
console.error('bind error: no name or handler')
}
}
添加一个通知事件
function dispatch (name, value) {
if(!value) {
value = get(name)
}
else {
set({
[name]: value
})
}
if(!events.length) { return }
events.map( (item, i) => {
if(item.name == name) {
item.handler(value)
}
})
}
简单的实现了一下,如果大家想要将该模式运用的更加自如,还需要专门花精力去研究他。我这里只是简单的暂时了一种实现方式。
如何解决html文章的展示
由于小程序中不支持html标签,它有自己的一套标签组件,因此我们在使用时,并不能直接把html文章显示出来。为了能够解决这个问题,我们需要将html标签转换为小程序支持的标签。
有一个叫做html2json.js的组件。它遍历html的标签结构,并根据便利结果将标签内容以json的数据形式保存起来,我们就可以通过该json数据生成对应的小程序标签。
当然如果是我们自己做的话还比较麻烦,好在有大神在第一时间提供了一个叫做wxParse的三方插件。大家可以去搜索使用一下。
由于该插件功能太齐全,很多我的小程序用不上,所以就根据上面我说的思路自己实现了一个轻量级的,刚好够自己用。
如何考虑缓存机制
上面的state组件,能够在一定程度上缓存一些轻量的数据。但是该组件的生命周期短暂,在小程序退出之后数据就会消失,而且对于数据量比较大的情况,也不适合用state组件来缓存。
每一个小程序有10M的本地缓存空间。而且小程序也提供了缓存和清除缓存的api,因此这不是我们的难点,我们的难点在于,如何分清哪些数据应该缓存,每一种数据应该缓存多久?如何更新?服务器数据如果更新了应该如何同步本地缓存?
缓存策略做得好不好,在很大程度上会决定你小程序的整体质量。当然这里我就不详细展开解读了,涉及到很多东西,一时半会儿感觉说不清楚了,大家只要考虑清楚了上面的几个问题,相信都能够结合自己的实际情况,弄出一个合理的方案。
如何处理组件以及组件的数据传递,组件的交互等
小程序并没有提供明确的组件机制。但是我们知道,在小程序里,一个页面可以由xx.wxml, xx.wxjs, xx.wxss, xx.json组成,这是一个基本结构。而且小程序提供了js的模块引入,wxml的模板引入,这就为我们自定义组件创造了可能。
+ component
+ rect
- rect.wxml
- rect.wxss
- rect.js
+ pages
+ index
- index.wxml
- index.wxss
- index.js
我们创建了一个rect组件,并希望我们自己能够自定义矩形的颜色,点击之后,还会再修改一次颜色
一切从简,rect组件中代码
// rect.wxml
"rect">
"single-rect" bindtap="changeColor" style="background-color: {{rectColor}}">
// rect.wxss
.single-rect {
width: 100px;
height: 100px;
background-color: red;
}
// rect.js
module.exports = {
changeColor: function (_this) {
_this.setData({
rectColor: 'orange'
})
}
}
index页面中引入该组件
// index.wxml
"../../component/rect/rect.wxml" />
"rect" data="{{rectColor}}">
// index.wxss
@import "../../component/rect/rect.wxss";
// index.js
import rect from '../../component/rect/rect'
Page({
data: {},
onLoad: function () {
console.log(rect)
},
changeColor: function () {
rect.changeColor(this)
}
})
这个例子演示了组件的创建与使用,其中包含了数据传递。当然看上去有点麻烦,因此我们可以通过构建工具来简化这个过程。这里就不多说了,在不增加额外构建的情况下,这样使用是完全没有问题的。至于不同组件之间的交互,就得通过上面的state组件来完成。
如何使用Promise
Promise的重要性不言而喻,这里就不再介绍promise了,如果不明白的同学可以去其他文章里学习一下。但是小程序经过几次改变,已经决定取消对Promise的支持,在实际应用中,promise几乎无处不在使用。因此,我们需要自己引入一个polyfill来支持promise。
小程序对于接口的返回结果进行了一层统一的封装,但是我们并不想使用这样的结果,这个结果会导致代码量的增加,因此我们可以通过promise的过滤设置,只返回我们想要的东西即可。
如果能够准确理解resolve与reject,那么我们就能够随意的定制过滤规则了。
function wxPromise (cb) {
return function (result = {}) {
return new Promise ((resolve, reject) => {
result.success = _res => {
if(_res.statusCode) {
/(2|3)\d+/.test(_res.statusCode) ? resolve(_res.data) : reject(_res.data)
} else {
resolve(_res)
}
}
result.fail = (...args) => {
reject(...args)
}
cb(result)
})
}
}
当然还有许多具体功能的具体实现方案,比如如何实现加载更多下拉刷新,如何实现图片懒加载,如何实现k线图的绘制,如果实现本地数据同步刷新等等等,由于时间关系,就不一一介绍了,以后有时间再跟大家分享吧。