你 和一头骆驼准备穿过沙漠,前面是一眼望不到头的沙海,你的目的是要穿过 沙漠到达对面的绿洲。 现在你写的每一行代码就是往骆驼上负重。 当然,有些 负重是 必须 的,比如水和食物。
可能会由于负重太多,骆驼严重拖累行程,甚至可能永远走不出沙漠。
可能水和食物不够,都被饿死在中途。
本文目录:
-
有必要重申的交互原理
-
请慎重设置 data
-
请三思组件间如何通信
-
友好的使用 sass
-
不可或缺的 template & @import
-
理解多页应用的单页应用
-
第三方库
-
请善待主包
小程序有包大小限制,单个主包或者子包不能超过 2M,总包不能高于 12M。
有大小限制,那么对于超大型小程序,就要悠着点了,否则超包那就是家常便饭了,当然小程序这个远不及浏览器的内存和性能也提醒着我们要悠着点。
现在,请开始给骆驼负重!!!
有必要重申的交互原理
小程序的 Page 给我们提供了一个 data 对象。这个 data 对象的数据更新,则直接影响 wxml 的内容。有 vue 或者 react 开发经验的同学可能再熟悉不过了。当然小程序我们需要使用 setData 来更新数据。
Page({
data: {
text: "This is page data."
},
onLoad: function(options) {
// doSomething
}
});
这是官网上的描述:
小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。
简而言之,js 逻辑层的 data 的数据是需要转换成字符串,再使用 JS 脚本传输到 wxml 的视图层。文档也明确了传输不实时。实际上如果数据量大还很慢。
请慎重设置 data
data 对象是类似 vue 的 data,是 reactivity 的。vue 的 reactivity 的数据,是都需要 Object.defineProperty(或者 Proxy)的。
比如看到有代码,在 data 里面放大量的数据,而这些数据根本上在 wxml 里面用不上,仅仅是为了存储一个对象,在其他文件模块(同 this)的时候,能直接用。还看到在 data 里面设置 isLoading 对象,这个仅仅是为了在点击的时候做一个锁,等接口调用完成,再把 isLoading 改为 false。
以上情况,在小程序代码中比比皆是。这些都有一个通病,忽略了性能,随心使用 data,导致一个页面,在 data 下设置的变量可能占满一屏的高度。
那我们如何做呢?
原则:只在 data 对象设置在 wxml 需要使用的变量。
那么其他变量我们怎么做呢?有两种方案:
1、设置在实例作用域外,脱离 Page 或者 Component 对象。
let name = "is file scope data";
Page({});
2、设置在实例作用域内,但是脱离 data 对象,不需要 reactivity。
a) 如果是 Page ,可以直接新增一个 customData,
Page({
data:{
name:"reactivity data"
},
customData: {
name: 'not reactivity data'
}.
});
b) 如果是 Component,可以使用 pureData。
Component({
options: {
pureDataPattern: /^_/ // 指定所有 _ 开头的数据字段为纯数据字段
},
data: {
name: "is reactivity",
_name: "is pureData",
},
经过实验证明,对于逻辑复杂,数据量大的页面,很好的优化你的 data,对性能提升比较明显。
请三思组件间如何通信
组件间通信方式:
-
子组件向父组件:triggerEvent
-
父组件向子组件:props
-
父组件向子组件:selectComponent
-
订阅发布模式:emit、on
当前前三者,都是小程序直接提供的方式,第四种我们需要写一个公共的 emitter 组件。下面我们来分析下这种情况。
1.triggerEvent
子组件向父组件通信,这种在开发中非常常见,子组件响应了操作,如果需要同步给父组件,直接用 this.triggerEvent,然后在父组件中定义 bind 就行。
2. props
父组件向子组件传参,这种也非常常见,如果你需要传参给子组件,需要做如下两步:
1、父组件需要在 reactivity 的 data 中设置数据;
2、子组件需要在 props 属性中设置接收该数据对象。
发现没有,如果你需要传一个参数,那么你需要在父子组件当中都设置为 reactivity 的 data 对象。显然,对于大量的传输数据对象(比如一个大型数据列表),不适合直接这样传参。因为如果这样做的话,显然增大了父子组件成本。
3. selectComponent
父组件直接调用子组件实例对象,然后直接执行子组件的方法,比如:
子组件:
Component({
methods: {
updateStatus() {}
}
});
父组件:
this.selectComponent("#childComponent").updateStatus();
selectComponent,完美的解决了 props 传参的问题,微信给你另一条道路,也说明了 props 的问题。但是 props 传参方式更符合开发习惯和数据流思维。
个人建议,大数据传输,直接用 selectComponent,反之用 props。
4. $emit
发布订阅模式,大伙都很熟悉了。注册 on("key", () => {}),发布 emit("key", data)。这种方式的优点很明显,完全突破组件直接的关系。任何地方都可以监听,都可以发布。
缺点则是无状态的,和组件实例无关,且是全局的。
如果页面打开了两个 Page,比如 商祥 -> 店铺 -> 商祥
,这个时候,如果商详接受到一个消息,两个商祥 Page 都会收到。
发布订阅模式需要注意四点:
-
key 是全局的,好好命名。
-
接收消息的区分是不是该实例的。
-
销毁注册消息通常是全部的。A 监听 key,B 监听 key,如果执行 remove(key),则 key 都会清除。
-
如果不支持粘性事件则需要关注发布订阅时机。
(当然你可以选择不把 EventBus 放到全局)
友好的使用 sass
样式文件,我们一般使用预处理,比如 sass。我们看如下例子。
class="recommend">
class="recommend_header">
class="recommend_header_author">
class="recommend_header_author_img">
class="recommend_header_author_info">
class="recommend_header_author_info_name">
class="recommend_header_author_info_location">
class="recommend_header_follow">
class="recommend_footer">
.recommend { &_header { &_author { &_img { } &_info { &_name { } } } } }
上述结构和样式,看不出啥,我们经常在 H5 页面的时候都是如此做的,按照页面模块和层级定义结构,使用 sass 逐层写样式,这可以说是标准写法,语义和结构清晰,
但是在超大型小程序中,我要 say No!
我们知道,sass 是 css 预处理器,最终是需要转换成 css 被浏览器和微信小程序识别。
我们看下转换后的样式文件
.recommend {
}
.recommend .recommend_header {
}
.recommend .recommend_header .recommend_header_author {
}
.recommend .recommend_header .recommend_header_author {
}
.recommend
.recommend_header
.recommend_header_author
.recommend_header_author_info {
}
.recommend
.recommend_header
.recommend_header_author
.recommend_header_author_info
.recommend_header_author_info_name {
}
看了之后有没有觉得你哪里不对?重复的 class 太多了,看到这么多重复的,JS 开发人员第一反应:提炼抽取才是正道。想想我们大几十行研发人员开发的小程序,功能如此之多,体积如此之珍贵,岂能如此浪费。
回归到 sass,我们使用样式层级作用域的目的是啥呢?我认为无非是绝对标识该样式,类似 js 的作用域,不被其他模块的样式影响,然后能够清晰的定义样式。
试想,如果张三在页面 header 模块的用户昵称,定义成 .name,那么李四在 content 模块的用户昵称也可能会被定义成 .name,然而这两个样式完全不一样,但是样式就会相互影响,李四把 .name 样式写好了,然而 header 模块的 .name 样式又不对了,这显然不是我们想要的,所以推荐把样式的名称按层级定义,不会被影响。
理由很充分,sass 写起来也很舒服,但是在实际中极力不推荐严格按这种层级定义。
那么在大型小程序中,推荐: 最多三级,建议两级。
对应上述 wxml ,在结构不变的情况下,样式修正为如下:
class="recommend">
class="recommend_header">
class="recommend_author">
class="recommend_img">
class="recommend_info">
class="recommend_name">
class="recommend_location">
class="recommend_follow">
class="recommend_footer">
有人会说了,这个 header 模块用户昵称可能是 .recommend_name,但是 footer 模块也有一个用户昵称怎么办?
通常一个模块是一个人维护,就算多个人修改,那么样式也只在当前模块内影响,风险完全可控。那么 footer 模块的昵称可以定义成 .recommend_footer_name,增加到第三层,甚至于我推荐直接是 .recommend_footer-name,这样在解析成 css 的时候,仍然是两层。
都 21 世纪了,总想着再不断的出现爱因斯坦、牛顿等已经不太现实了。我们要 "微优化"!
总结:在可读性仍然很强大的情况下,保证模块直接样式不冲突,建议控制在两层 sass,最多不超过三层。减少文件大小。
在模块多的页面中,这种带来 wxml 和 css 体积的缩减其实是很可观的。
template & @import
根据页面划分组件,大伙都会做。然而实际上在模块中会存在很多共同的结构,但是有时候我们因为逻辑较少等原因没必要抽离成一个组件,这个时候 template 就派上大用场了。
单个文件中的共同结构
<template name="liveItem"> template>
<template is="liveItem" data="{{list:preLiveList}}" />
<template is="liveItem" data="{{list:liveList}}" />
多个模块中的共同结构
<import src="./template.wxml" />
is="goodsItem" data="{{item}}" />
is="goodsItem" data="{{item}}" />
当然,wxml 抽离成模块引入了,那么 css 自然也需要抽离成公共的文件。
@import "../../common/common.wxss";
按照经验,一个呈现稍微复杂一点的页面,如果没有存在 template 和 公共引入 css 文件,大概率重复代码不少。当然,如果你很乐意把组件拆的足够细的话(component 渲染性能慢的问题指南),那么也是可以规避这个问题,但是实际上可能有些代码量反而还增多了, 合理 template 才是正确姿势。
理解多页应用的单页应用
小程序是一个多页面应用,各个模块独立开发,整个系统是多个 Pages 组成的,但是和我们常规的 H5 不同,访问多个页面之后,这些页面是共存的。
访问 A 页面 -> 访问 B 页面,此时 A 页面是仍然在运行,只不过是 hide 的。
既然多个页面都存在,但是当前 active 的只有一个页面,因此我们仍然要做一些单页面应用需要做的事,比如:
1、停止更新数据 直播的点赞动画,如果当前页面不是 onShow 的,就不要去更新动画了,节省点 CPU 吧!
2、及时回收定时器 在 A 页面开启了定时器,显示倒计时,但是到 B 页面了,A 页面的倒计时就没有意义了,应该及时回收和清除。当然如果你需要在页面返回到 A 页面的时候,仍然能看到倒计时,那么请在 onShow 的时候唤醒倒计时,onHide 的时候清除倒计时。只管利用,不善后的可不是好学生哦!
3、及时清除不必要的资源。当前访问 A 页面,然后返回,再次访问 A 页面,那么页面的非实例对象数据是不会清除的,我们需要手动清除对象,释放资源,防止重复注册等操作。
第三方库
第三方库用起来爽呀,一个看起来不简单的三下五除二就搞定了,能不爽吗?
比如我们使用 vue 开发页面,有轮播图,引用第三方 swiper,我们需要做时间处理,引用 moment。
本人开发这么多页面和系统,这些组件很少引用,我的做法是明确我的需求,如果有类似的,然后不会写的,那么阅读下源码,自己写下就行。因为我知道,开发的组件都是支持最全面的,实际上你根本不需要这么多,比如格式化时间,你的需求可能就是实际十几行代码的事情,为啥要引入一个第三方组件呢?同理一个 swiper,别人支持的各种各种需求,你的可能只是需要普通的滑动切换就行了,为啥要引入强大的第三方组件呢?
当然如果第三方组件可以支持良好的 tree-shaking 就另当别论。
在 H5 时代,我一般不轻易引入第三方库,对于他人代码我也是持保留意见。
但是在大型小程序时代,我是坚决反对用啥直接引用啥。毕竟 【体积 & 内存 & 性能】三座大山摆在眼前。
请善待主包
如果直接打开子包链接,那么是需要下载 主包 + 当前子包,然后才能访问页面。那么主包的下载对页面首屏的打开,就显得至关重要了。
按照我们 H5 开发的逻辑,公共代码都需要抽离成独立的模块,方便共用。但是小程序场景有点不一样。
A 子包 require goodsModule
B 子包 require goodsModule
这个时候,将 goodsModule 抽离出来,放在主包(因为子包直接相互引用的问题)。看起来一切顺其自然。
然而,如果你遵循上面的方案,那么你会发现,在超大型的小程序中,会有大量的 Modules 被放到主包,导致主包严重不足。那如何解决这个问题?我们可以使用 NPM 功能。
思想仍然会是模块单独下载,访问了 A 页面,下载 A 子包,然后再访问 B 页面,下载 B 子包。尽管 A 和 B 子包有重复下载的代码。
那什么情况放在主包呢?
1、主包 Pages 已经有引用了
2、至少有三个及以上的子包引用同样的模块
善待主包,兼顾总包!