您现在的位置: 微信小程序 > 微信小程序运营 > 经验 >

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

来源:微信小程序 编辑:Yiyongtong.com 发布时间:2020-05-18 10:28热度:

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

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

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

本文目录:

  • 有必要重申的交互原理

  • 请慎重设置 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,对性能提升比较明显。

请三思组件间如何通信

组件间通信方式:

  1. 子组件向父组件:triggerEvent

  2. 父组件向子组件:props

  3. 父组件向子组件:selectComponent

  4. 订阅发布模式: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 都会收到。

发布订阅模式需要注意四点:

  1. key 是全局的,好好命名。

  2. 接收消息的区分是不是该实例的。

  3. 销毁注册消息通常是全部的。A 监听 key,B 监听 key,如果执行 remove(key),则 key 都会清除。

  4. 如果不支持粘性事件则需要关注发布订阅时机。

(当然你可以选择不把 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、至少有三个及以上的子包引用同样的模块

善待主包,兼顾总包!