0755-83066025

网站建设 APP开发 小程序

KNOWLEDGE/知识

分享你我感悟

您当前位置> 首页 > 知识 > 小程序

我是如何写超大型小程序代码的?

发表时间:2019-09-29 20:44:59

文章作者:腾云网络

浏览次数:

你和一头骆驼准备穿过沙漠,前面是一眼望不到头的沙海,你的目的是要穿过 沙漠到达对面的绿洲。 现在你写的每一行代码就是往骆驼上负重。 当然,有些 负重是 必须 的,比如水和食物。


可能会由于负重太多,骆驼严重拖累行程,甚至可能永远走不出沙漠。


可能水和食物不够,都被饿死在中途。


本文目录:


有必要重申的交互原理


请慎重设置 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。我们看如下例子。


<view class="recommend">

    <view class="recommend_header">

        <view class="recommend_header_author">

            <view class="recommend_header_author_img"></view>

            <view class="recommend_header_author_info">

                <view class="recommend_header_author_info_name"></view>

                <view class="recommend_header_author_info_location"></view>

            </view>

        </view>

        <view class="recommend_header_follow"></view>

    </view>

    <view class="recommend_footer">

    </view>

</view>

.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 ,在结构不变的情况下,样式修正为如下:


<view class="recommend">

    <view class="recommend_header">

        <view class="recommend_author">

            <view class="recommend_img"></view>

            <view class="recommend_info">

                <view class="recommend_name"></view>

                <view class="recommend_location"></view>

            </view>

        </view>

        <view class="recommend_follow"></view>

    </view>

    <view class="recommend_footer">

    </view>

</view>

有人会说了,这个 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" />

<template is="goodsItem" data="{{item}}" />

<template 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、至少有三个及以上的子包引用同样的模块


善待主包,兼顾总包!


相关案例查看更多