从零开始搭建小程序架构

·  阅读 1265
从零开始搭建小程序架构

未经允许,请勿随意转载,谢谢。

笔者接触小程序开发已3年有余,如果问我从零开始开发一个小程序改怎么做,可以瞧瞧这篇文章。

一切的开端

要开发小程序,第一件事当然是先注册一个小程序啦。

小程序注册

iShot2021-08-12 23.11.08.png

微信小程序注册地址

点击箭头的注册,按界面提示就可以完成注册啦。

注册完成之后就可以获得我们的第一个重要道具 -- APPID

iShot2021-08-12 23.14.01.png

iShot2021-08-12 23.17.36.png

开发环境

和H5开发一样,开发小程序也是需要稍微搭建一下开发环境的。对微信小程序开发来说,最重要的环境就是微信官方提供的微信开发者工具。

微信开发者工具下载地址

iShot2021-08-12 23.21.54.png

点击界面中间最大的+号就可以进入小程序的新建界面。

iShot2021-08-12 23.22.27.png

在上图的箭头处填上我们刚刚申请的小程序APPID即可,然后会自动生成一个内置官方模板的小程序。

至此,我们算是迈出开发小程序的第一步了。但是这个时候一定不能急着撸代码,其实有很多事情比直接上手撸代码重要。

共同认知

一般来说开发都避不开协作,而协作最重要的是与小伙伴们的默契,这个默契就来源于大家的共同认知

只有大家对项目的认知是在同一水平面上,整个开发协作才会是正向发展的。

目录结构规范

不要小看我们每个文件、文件夹的命名与分布,随着项目的膨胀,有目录结构规范的项目和没有规范的项目,给人的感受是截然不同的。

在聊目录结构之前,我们先来简单的了解一下小程序的包体概念。

  • 小程序目前有这么三种包 -- 主包、分包以及独立分包

  • 对包体大小还有限制 -- 单个分包/主包大小不能超过 2M整个小程序所有分包大小不超过 20M

因为微信对主包的大小是有限制的,所以我们必然不该将所有逻辑都放到主包中。

主包和分包有一个十分明显的区别,分包可以引用主包内的所有内容,但是分包间是无法相互引用的,这就注定了,我们的公用逻辑必然是在主包中的,同时也能得出另一个结论 -- 业务逻辑都可以放到不同的分包中,也就有了下图。(tabBar页是什么?)

目录结构.png

图里还有一个比较奇异的说法,什么是分包内公用组件库?公用组件难道不应该都放到主包么?

其实在实际开发中,因各个业务的差异性,导致不是每个我们认为能公用的组件,它就一定是会为大家都会用。所以,笔者认为如果是本业务的公用组件就先放到分包内,如果确实出现了其他业务的需求,我们就可以对其进行组件提升,将其移动至主包,并统一更改原来分包中的引用。

独立分包在开发中比较少用到,故在此不做过多描述。

现在大家应该对小程序的包体有了一定的了解,在实际开发中我们应该怎么进行目录结构的规划呢?这里笔者仅抛砖引玉,举一个很浅显的例子,大概表述一下笔者的思路。

小程序根目录 (1).png

首先看看第一层,也就是我们的根目录,为什么这里要用app文件夹把真正的小程序内容包裹起来呢?这个主要是为我们日后引入工程化做铺垫,如果根目录就是小程序的根目录,后面会出现package.json混用的情况,届时再进行分离就比较麻烦了。

然后到了app内部,这里就是小程序的根目录了,和官方示例一致app.js/app.json/pages等文件/文件夹都放置于此,唯一不同的是新增了main文件夹,这里的设置是和前文互相照应的,main文件夹内放置的都是主包的内容,pages文件夹则放置各业务分包的内容,从文件目录上先对主、分包进行隔离。

紧接着是main文件夹的内部,上面也提及到了,这里面都是主包的内容,那么这里面的pages文件夹自然放置的是tabBar页面,然后外部则是存放各种公共资源。

最后就是与main同级的pages文件夹,这里就是存放各业务分包的地方,每个具体的业务都可以独立成包存放于此。

编码风格规范

上一段讲的是比较外层的结构规范,接下来就应该是比较细节的部分 -- 关于编码风格的规范。

这个其实没有太多好讲的,每个团队的编码风格肯定都是不一样的,这里就简单提及一下。

一个是变量、函数的命名,命名其实是一件很复杂的事情,我们需要明确这个变量或者函数的职责,在这个职责范围内思考贴合其实际用途的命名,一个明确且易懂的命名有时候比优秀的内部实现还重要。

然后就是lint、注释这类经典规范,就不在这里赘述了。

开发模式*可选

我们现在开发小程序大概有这三种方式

  • 多端开发框架
  • 基于原生小程序开发的二次封装
  • 原生小程序开发

多端开发框架指的是Taro、kbone这类框架,而后面两种开发模式的界定比较模糊,因为在团队的积累沉淀下,再纯粹的原生开发也会加上各式各样的二次封装以提高效率。所以这里指的二次封装模式是更高级别的,例如可以用vue3 composition API的模式来进行小程序页面的开发。

大家也许会问,这个二次封装和多端里面vue写法开发有区别吗?他们最大的区别是,前者的模板还是.wxml而后者是.vue。

这三种方式到底哪一个更优秀,笔者没有定论,每一种模式都有它的好坏,唯一能确定的是,模式的选定离不开项目形态,我们可以从这几个方向来进行方案的抉择:是否需要输出多端、团队主要技术栈或者技术积累是什么、是否需要时刻准备接入小程序最新的特性.....

基础设施搭建

做项目开发其实和建房子有点像,把整个项目看作一间屋子,我们建房子的第一要务自然就是打地基。回到我们的代码项目中,这一步笔者称之为基础设施搭建。我们首先要确保我们的根基是稳定的、高可用性的才有资格去谈我们屋子的内饰装修有多么的华丽,否则都是空中楼阁,一碰就倒。

登录态

首先我们需要聚焦的部分是登录态,登录态是用户在我们小程序的身份证,我们必须确保每位用户都能带着正确的登录态来操作我们的程序,否则将会出现不可预料的后果,轻则埋点上报错乱,重则影响数据库内数据稳定。

那么怎么确保我们的登录态能够稳定获取以及维持呢,笔者认为最基本的一点是需要维护统一的登录方法,收束路径之后就能维持稳定。接下来笔者将会分两个部分讲述登录态,因业务需求不同,一个小程序内可能会出现两种登录态 -- 小程序登录态以及自身账户体系登录态

小程序登录态

这里说的小程序登录态指通过调用wx.login获取code并使用该code换取该用户在本小程序openid的过程

理想情况下,我们应该能够对外提供一个通用方法miniappLogin,这个方法不需要关心当前是否已经获取有效登录态,只需要在每次调用的时候通过wx.checkSessionAPI进行登录态确认,如果已失效则重新登录。(在文章末尾会提供后端实现,以及整个流程的源码地址)

carbon.png

自身账户体系登录态

为什么这里还会多一种登录态呢?他们是两种不同的登录体系,其实用小程序提供的登录态也是可以满足我们对登录态的需求,它既有唯一的识别ID又有维护一套生效逻辑,对小型的业务来说确实可以直接套用。

但是当业务规模较大时,特别是如果同一个主体下拥有多个小程序的情况,在这种情况下我们需要拟定一个新的唯一识别字段,作为这个用户的标识,并且无论在哪个小程序内,用户的标识是一致的。

这个所谓的新字段其实就是我们H5开发时经常能见到的userid,uid等等,关于标识的生成就因人而异了,每个团队对其用户ID的要求不一样,有些可能就是普通的随机串,有些可能是加密串。除了用户ID,一般登录态里还会包含一个有有效期的token或者accessToken字段,就和微信的session_key一样,用于判断登录态的合法性。

具体怎么生成userid以及token基本没有一个通用的方案,都是需要结合业务需要去设计的,不过前提还是比较明确的 -- 必须要成功获取该用户的openid以及unionid后,才能进行用户的注册以及登录态的生成。

请求封装

自前后端分离成为共识之后,请求就成为了前端必须接触的事情。

正常的项目中,几乎每个页面都会有复数的请求,如果我们不尽早对请求作出统一且规范的封装,项目将会很快的陷入一个怪圈 -- 随着时间的推移,我们会发现项目的请求协议变得越来越繁重,到最后根本没人敢去整理协议内容,生怕因调整而影响了全局的请求。

因为项目间差异较大,我们当然没法整出一套大家都能通用的请求逻辑,只能稍微写点示例代码。

carbon (1).png

如上述伪代码,先是声明了一个通用的请求类Request,并以此为基础,拓展出业务真正需要用到的请求方法。

在示例中,笔者仅简单的预设了三类请求defaultRequestconfigRequestcfRequest。其实在实际开发中,我们需要用到的请求类型比这可能多得多,但是这三类请求是笔者认为是不可或缺的。

// 通用业务请求
const defaultRequest = (...args) => {
    const r = new Request({ baseURL: BASE_URL })
    return r.request(...args);
}
复制代码

首先是defaultRequest,顾名思义就是默认的请求模式,前端除了与用户产生交互之外,干的最多的事情就是与后端进行交互,这种请求就是用于这种情况的,它可能是一份表单的提交也可能是一次状态的查询......

// 页面配置请求
const configRequest = (someKey, data = {}) => {
    const r = new Request({ baseURL: `${BASE_URL}/config-api` });
    return r.request('getConfigBySomeKey', { someKey, ...data });
}
复制代码

然后就是configRequest,这个则是笔者构思的一种特别的请求范式。现在配置化已经是主流思维了,大家都想通过前端页面的配置化来达到快速验证、敏捷实现的目的,所以我们在构思框架时也应该将其考虑进去。我们应该有意识的将配置请求与业务请求分离开来,在原来的开发中,我们总是会将它们耦合在一起。但是仔细想想,它们差异明显,一种是界面配置的拉取,另一种则是后端操作的触发器,我们完全可以在项目构思之时将其解耦。

// 云函数请求
const cfRequest = (cfKey, data = {}) => {
    const r = new Request({ baseURL: BASE_CF_URL });
    return r.request(cfKey, { ...data });
}
复制代码

看到这里,可能有些实战经验丰富的童鞋会问了,有些情况,我们前端需要的并不是一份静态的配置,而是需要和后端数据进行一系列处理得出的配置。这个问题十分的有价值,这种情况是必然会出现的,但是也可以彻底的解决,借助我们最后的请求cfRequest即可。

云函数,说新颖倒也不是特别新颖,但是笔者感觉普及率还是不太高。稍微讲讲思路,在这里笔者认为cfRequest是一种高阶的configRequest,我们可以在云函数内完成defaultRequestconfigRequest的请求,并且根据返回的数据进行配置的处理,给前端输出开箱即用的数据。

另外,云函数不仅可以用于页面配置的拉取,它还可以根据后端提供的原子级接口,进行增删改查的操作,这里就不再拓展了,涉及团队对云函数职责边界的问题,如果有机会,可以新开一文进行讲述。

最后再啰嗦一句,封装请求的优点除了便捷、规范之外还有很重要的一点,它可以为后续的埋点上报提供极大的便利。

关于Request的完整代码可以点此(Github)

路由封装

小程序的跳转API是特殊的,与H5的路由跳转不太一样,但是我们封装的思路其实是一样的。

路由封装主要是为了解决下面几个问题:

  • 最大页面层级
  • 路由数据传递
  • 埋点上报

微信小程序的路由层级只支持到10层,在第十个页面想再跳转新页面则会报错并且无法正常跳转。所以我们需要在处理navigateTo类型的跳转时加上当前页面层级的判断,如果页面层级已经到达10层,可以将navigateTo的跳转变成redirectTo,从而确保本次跳转能够正常进行。

options.action = 'navigateTo';
const { _pathName, _query } = _navigateFormator(path, query);
if (getCurrentPages().length >= 10) {
    options.action = 'redirectTo';
}
复制代码

然后是数据传递的问题,我们在进行各种跳转的时候,总会想把原页面的某些数据传递到目标页面。虽然小程序现在提供了EventChannel的方式来进行页面间通信,但是用法还是比较麻烦。如果我们能够收束全项目的跳转方式,那么我们就可以在统一封装的路由方法中进行页面传递数据的维护。

constructor(options = {}) {
    this.routes = {};
}

router(path, query, options) {
    // ...
    const preloadData = typeof options.preloadData === 'function' ? options.preloadData() : options.preloadData;
    const routeID = _randomString(6);
    query.__routeID = routeID;
    this.routes[routeID] = {
        routeID,
        preloadData,
        // ...
    };
}
复制代码

最后就是埋点上报的问题,和上面讲到的请求一样,统一的操作,对埋点上报是百利而无一害。我们可以实现一个默认的跳转complete回调,在这个回调中实现上报相关的逻辑。下面是简单的封装router代码,还有很多细节可以根据业务需求进行优化,这里就不延伸了。

关于Router的完整代码可以点此(Github)

页面、组件初始化函数

本质是对小程序暴露出的Page、Component函数进行二次封装。

随着开发的深入,我们希望能够在页面加载之初就能够获取部分全局共享的参数或者方法,这种时候就需要开发页面、组件初始化函数。笔者总结了一下,当我们遇到下列情况的时候就可以考虑着手开发统一的初始化函数了。

  • 需要在页面或者组件初始化时统一执行某些操作,例如进行加载上报
  • 需要在多个页面或组件获取某些全局参数或者工具类

carbon (2).png

如上述代码块,在defaultPage对象中声明了onLoad函数,在里面进行一些页面初始化的操作,并约定将本阶段产生的数据都存放到this.data.$$define中。

在示例中,笔者完成了这几个初始动作:

  • 获取全局数据
  • 储存页面链接参数 大家在开发小程序页面的时候应该都会遇到这个麻烦的问题,在非onLoad周期时想要获取链接参数总是十分麻烦。
  • 处理约定数据 在强约束下,我们是可以对某些约定好的数据进行预处理,例如对参数中的url进行解码......

上面只不过是冰山一角,大家可以根据业务需要,将更多的逻辑添加进去,让开发更加便捷。

关于初始化函数的完整代码可以点此(Github)

公共组件库

小程序里面的公共组件库和我们平时H5内见到的不太一样,因为小程序有两个限制包体大小分包间不可互相引用,所以我们不能将组件库单独放置与某个分包中,只能都放在主包里,然后就延伸出另一个问题,包体体积大小的问题。

目录结构规范部分就有提及,笔者不提倡所有抽离出来的组件都当做全局公共组件放置在主包中,而且采用一种组件提升的方式,当组件确实需要被其他业务引用时,可以将其提升至主包,这样既确保了主包体积的平稳发展,也能对组件进行二次审核。

基础设施文档

!!一定不能忽略团队技术文档的建设和积累

引用不知道哪里传出来的经典语句 -- 铁打的辅助,流水的输出

用本文的情景简单的误解一下:

团队内的人员流动大概率是频繁的,无论是岗位调整亦或是人力增减,都是无法避免的。这种时候就需要有稳定的辅助来保障我们项目的可延续性,也就是我们的技术文档

那么技术文档到底有什么作用呢?大家平时可能很难察觉到它们的存在,甚至当自己要写的时候,还会很难受。

笔者以这些年在团队中的体会,总结了下面几点:

  • 技术文档是一本指南 技术文档初衷就是为新入职或是新接手的同学提供指南服务的,文档的详细程度直接影响大家融入项目的速度。

  • 编写技术文档是一种复盘 以一个组件文档为例,文档中必然会涉及组件用法的记录,像是接收参数或是事件传递,都会在此时重新展现在组件作者眼前。这是一次很好的复盘机会,我们可以重新审视一下组件细节,比如某个参数是否真的必须,又比如某个事件是否应该通知父组件......

  • 技术文档是团队技术沉淀的体现 笔者认为,一个技术团队除了保障项目需求的维护迭代,还应该有自身的技术沉淀,而技术文档则是技术沉淀的做法之一。短期来看,技术文档编辑可能是个吃力不讨好的工作,但是眼光放远来看,这就是一种类似种树的举动,随着时间的推移,我们终会在树荫底下回望这棵参天大树。

业务开发

每间公司都有属于自己的业务开发流程,这一块是不可能统一的,我们应该充分观察公司内部的工作流程结合公司业务形态,动态调整出最适合团队的开发模式。

测试与发布

这部分主要阐述的是黑盒测试流程,并且顺带引入了发布相关的内容。

进入主题前,我们需要思考一件事 -- 根据自身业务的规模判断是否需要申请两个小程序。

如果是比较大型的业务,建议准备两个小程序,一个用作测试,另一个则用于正式发布。

如果能够明确业务较小,或者说是个MVP项目,我们可以只准备一个小程序。

下面,笔者将从这两种项目出发,分别介绍两种项目的测试模式。

MVP项目

敏捷项目.png

小程序目前有三种版本开发版体验版以及正式版,开发版和体验版都有一定的访问限制,而正式版则是所有用户都可以访问的。根据每个版本的特性,我们可以作以下版本规划:

  • 开发版 -- 一般是开发人员使用,版本内是本地的小程序代码,仅用于开发阶段的自测。
  • 体验版 -- 功能特性相对稳定的版本,一般用于测试介入,后期可用于产品走查。
  • 正式版 -- 功能特性已经十分稳定并且通过测试、走查,此阶段小程序将会正式对外发布。

这是理想中的版本规划,但是在实际项目推进中,肯定会出现多个需求穿插开发到情况,这种时候体验版会被重复覆盖,十分不利于测试以及走查。那么我们为什么不能用开发版进行测试呢?

并不是完全不行,因为能够拥有开发权限的用户极其的少,如果团队人数不多,是可以都在开发版进行测试,然后用体验版进行走查。但是稍微大一点的团队就不能这么做了,只能将测试人员迁移到体验权限中。

如果在开发途中发现,项目比预期中的要大许多,并且团队也比较大,我们就应该考虑将项目转换成大型项目的测试方案。

大型项目

大型项目.png

如图,大型的项目将会有两个小程序,一个小程序专门用于测试,其每个版本的功能与MVP项目类似,唯一不同的是走查的时机变更了,在大型项目中我们将会选择更加稳定的版本进行产品、设计走查,也就是小程序的正式版。

另一个小程序则是会对外发布的小程序,代码来到这边就已经是绝对稳定的,所以在这里只留了一个体验版的位置,在这个体验版里进行预发布体验,基本上可以理解成是灰度阶段的正式小程序。

在上述测试部分中,出现了多种版本,那么这些版本是怎么发布、部署到微信的呢?

  • 利用微信开发者工具

  • 利用微信官方提供的CI工具

上述两种方式都可以将本地的开发版小程序变成体验版小程序,如果想让外部用户也能够访问到我们的小程序就需要登录微信公众平台,进入对应小程序的操作后台,进行版本提审以及发布,这方面没什么好说的,按照界面上的指引填写好小程序提审的资料即可。

小结

感谢大家看到最后:)

上述的内容如果都顺利搭建,我们的小程序就有了雏形,随着业务开发的深入,我们能够总结抽象出更多符合团队业务开发的“轮子”,让我们的小程序更加健壮。

下面笔者再给大家额外拓展一些偏业务的实现,下面的实现以及上文提及的代码实现都将会在本仓库中记录。

iShot2021-09-08 23.30.29.png

另外,在仓库中还有一个miniapp-startkit目录,这是个会持续迭代的项目,算是这篇文章内容的最终落地实现,有需要的同学可以star关注~