本系列文章分为上下两篇:
小程序开放平台架构指南(上)中介绍了小程序开放平台的核心价值点与开发路线图;
小程序开放平台架构指南(下)里对小程序开放平台中两个关键问题: 跨端通信与跨端构建响应式 Dom 界面的解决思路进行了介绍。
进入 2021 年, 提供小程序开放能力的平台越来越多, 除了京东/钉钉/虎牙/快手/飞书, 甚至在以保守出名的金融行业, 招商银行和云闪付也推出了自己的小程序开放平台. 在这样的背景下, 要不要实现自己的小程序开放平台, 如何实现小程序平台自然成为每个公司前端开发者心里的问题. 贝壳基础平台中心对小程序平台的实现进行了一定的调研, 这系列的文章即是对这段时间的总结。
评估我们是否需要小程序, 首先得定义什么才是小程序。
按微信的定义, 小程序是一种特殊的网页应用. 传统模式下, 逻辑层(js)和渲染层(dom)都在同一页面上执行, 但在小程序里, 逻辑层运行在 jsCore, 渲染层运行在 webview, 互相不能接触. 不仅如此, jsCore 中只支持使用 ECMAScript 规范中定义的语法, ECMA 规范之外, 无论是浏览器环境中的 Dom/window, 还是 Node.js 中提供的 fs/path 模块, 都未予以支持. 这当然使开发变得困难重重, 但作为补偿, 微信通过 wx 对象向逻辑层暴露了大量原生能力. 借助 wx 对象和微信平台本身的支持, 小程序最终实现了以下效果:
快速的加载
更强大的能力
原生的体验
易用且安全的微信数据开放
高效和简单的开发
这些收益当然很好, 但技术选型时, 我们需要先确认以下几个问题:
所以, 第一个问题: 如果选择微信小程序方案, 我们需要付出那些成本?
在微信小程序的模型里, 由于逻辑层不能接触渲染层, 因此也就不能使用传统浏览器页面的方式开发小程序. 为了模拟这种环境, 我们需要开发专门的环境模拟器, 屏蔽几乎所有的全局函数, 禁止 js 接触 dom, 同时还要模拟小程序基础库提供的开放能力 API----简单来说, 我们需要开发一套 IDE 系统。
其次, 实际业务开发中必然有必须操作 Dom 的场景, 比如Picker
组件的 scroolTo
方法, 比如 Canvas
对象. 为了提供这些能力, 我们需要一个中间层将 Dom 操作封装为组件对象, 在逻辑层和渲染层之间转发操作指令和执行结果, 换句话说, 还要开发一套组件库。
然后, 由于逻辑层和渲染层的分离, 为了最终可以渲染出界面, 所以我们需要设计一套类似虚拟 dom/vue 模板/jsx 的系统, 将逻辑层的操作映射为渲染层的实际 Dom, 这是一个 webview-render.
除了 webview-render 之外, 微信还要向逻辑层暴露原生能力, 这样就需要一层中间层在逻辑层和原生应用之间转发操作指令和运行结果, 也就是一套小程序基础库。
Android/iOS 两端都要提供原生功能, 这样又至少需要一名 Android 开发和一名 iOS 开发, 如果公司有多条产品线(如微信/企业微信/QQ/...), 那么还需要开发一套小程序 SDK, 用于在多个平台间共享小程序能力。
以上这些是小程序的硬件需求, 除此之外, 小程序的创建/预览/上传/审核/发布都需要一个后台进行交互, 所以我们还需要一个小程序后台系统。
总结一下人力需求, IDE 一人, 组件库一人, 基础库+设计模板语法规范+编写 webview-render 一人, Android 一 iOS 一, 小程序后台一, 再加一名技术经理, 总计需要 7 名开发工程师. 按半年出 demo 算, 当第一版小程序平台上线时, 总成本大约是 7 *6 => 42 人月. 而且, 这只是一个 demo, 不包括后期的推广成本维护成本。
考虑到 IDE 工程师的薪资水准, 42 个人月的成本是相当可观的. 所以, 我们自然会想到一个问题: 除了小程序, 实现既定的期望目标, 我们还有其他方案吗?
当然有, 小程序方案成本高企原因其实只有一条: 逻辑层和渲染层相分离. 如果允许逻辑层直接操作 Dom, 那么:
js 调用原生能力的接口还是需要, Android/iOS 开发也需要, 后台开发可有可无----如果把调用原生能力的 js 传到 cdn 上对外开放, 允许开发者用自己的域名发布应用的话, 那后台开发也可以省掉。
唯一的问题是...
这不就是 js-bridge/hybrid 吗?
这当然是 js-bridge, 但没有什么. 作为一名成熟的技术人员, 根据任务目标选择合适的解决方案而不是最炫的解决方案是基本准则. 所以我们实际要解决的问题是, 是什么让微信团队放弃了简单明快的 js-bridge,而去选择了成本高昂前途未知的小程序方案?(微信 15 年推出小程序时看衰的人不在少数)
再回顾一下我们需要实现的任务目标和小程序
/js-bridge
/React-Native
方案间的对比:
需求目标&实现方法/方案名 | 小程序方案 | js-bridge + webview 缓存改造 | React-Native |
---|---|---|---|
首次快速加载 | ✅ 调用接口, 对特定小程序资源进行预缓存 | ✅ 调用接口, 对 url 资源进行预缓存 | ✅ 调用接口, 对特定应用资源进行预缓存 |
二次启动快速加载 | ✅ 在本地缓存使用过的小程序的静态资源 | ✅ 通过修改 webview, 对使用过的页面静态资源进行缓存.如果本地已有缓存则直接读取缓存, 跳过网络加载流程. 或者直接利用 E-tag 字段对静态资源进行缓存 | ✅ 在本地缓存使用过的应用的静态资源 |
更强大的能力 | ✅ 取决于原生向小程序应用开放多少能力 | ✅ 取决于原生向 js-bridge 开放多少能力 | ✅ 取决于原生向 React-Native 开放多少能力 |
原生的体验 | ❌ 最终界面渲染在 webview 上, 并非原生体验. 对特殊标签(map/canvas)才使用原生渲染 | ❌ 最终界面渲染在 webview 上, 并非原生体验. | ✅ 真正的原生应用----缺点是官方对很多原生能力欠奉, 例如视频播放功能, 到 2021 年 9 月仍没有官方支持 |
高效和简单的开发 | ❓ 类 vue 语法, 但由于不支持 Dom API, 仍有一些学习成本 | ✅ 原汁原味的 web 应用开发体验, 如假包换 | ❌ 理论上可以直接写 React, 实际使用时受制于平台具体实现, 限制很大. 例如到 2021 年 9 月仍没有完善的虚拟列表支持 |
易用且安全的微信数据开放 | ❓ 取决于原生能力开放度 | ❓ 取决于原生能力开放度 | ❓ 取决于原生能力开放度 |
从表格可以看出, 在实现宣传中的任务目标方面, 小程序对其他竞品方案并没有决定性的优势。
有观点认为小程序优势在于逻辑进程渲染进程分别进行, 所以加载速度会比网页快. 但实际上, 百度首页渲染时间分析显示, 网页渲染时间只有 58ms, 只占总渲染时长的 1.9%, js 运行时长(956ms)和静态资源加载时长(145ms)才是可控的占比大头。
如果考虑到逻辑进程和渲染进程之间通信所消耗的时间, 以及不管是小程序还是 js-bridge 方案, 页面最终运算结果总会呈现在 webview 上这一事实. 双进程方案由于多了启动逻辑进程和进程通信的步骤, 在同等优化层次的情况下, 其性能只会比 js-bridge 更差, 不会更好。
微信小程序启动流程 :
hybrid 应用启动流程 :
实际上, 微信小程序文档自己也提到, 微信是先提供了 js-bridge , 然后才提出了小程序方案。
所以, 问题来了: 什么才是小程序方案优于 js-bridge 等其他方案的关键因素?
2015 年, 微信首先推出了 js-bridge 方案向公司内部开发者开放原生能力, 但很快被平台上的其他开发者发现, 于是微信顺带推出了正式的 js-sdk 方案, 希望作为平台向开发者提供更多能力。
但是, 违规应用层出不穷, 由于使用的是 js-bridge 方案, 所以微信只能通过封禁域名的方式对页面进行限制. 但微信的运营团队很快发现, 由于预期收入很高而违规成本又极低(域名 35 元/个/年), 黑产灰产团队完全不在乎域名的损失. 也就是说, 在微信平台上, js-bridge 虽然可以开放能力, 但却不能限制谁去使用这些能力. 不开放能力则平台生态难于发展, 贸然开放则又是稚子怀千金于闹市. 在这种情况下, 安全可控成为了微信对技术方案的最高要求, 准确来说, 是这三点:
不允许开发者把页面跳转到其他在线网页----确保审核人员看到的页面就是最终展示的页面 不允许开发者直接访问 DOM----避免潜在 hack 点 不允许开发者随意使用 window 上的某些未知的可能有危险的 API----白名单是最好的防御
对应于这种诉求, 逻辑层和表现层完全分离的双进程方案, 对微信而言就是必然选择。
小程序平台的关键优势不在于性能/开发体验, 而在于为平台提供了一个安全可控的环境, 使之可以安心的向平台内的开发者暴露大量原来不可外露的原生能力, 除此之外, 快速发版, 页面载入时 loading 效果优化, 消除切换页面期间的白屏都属于小程序的附赠功能, 在技术选型中的权重可以忽略不提。
所以, 评估是否需要小程序平台的关键在于以下几点:
明确了这三个问题的答案后, 剩下的, 就只是技术/成本问题. 所以问题来了, 小程序的开发路线图, 应该是什么样的?
在制定开发路线图前, 我们要先梳理小程序的业务流程, 以及相关的技术点。
小程序的业务流程可以分为外围和内部两部分. 外围指的是业务方从创建小程序到在 App 上启动的一系列操作, 内部则是小程序在 App 上从启动到退出的全部过程, 这里我们分开讨论。
小程序外围流程主要分为两部分:
首先是注册发布流程:
需要实现以下模块&功能:
- 后台
- 小程序注册接口
- 上传接口
- 提审接口
- 发布接口
- IDE
- 创建小程序
- npx miniprogram-cli create
- 编辑
- 打开已有项目
- 预览小程序
- npx miniprogram-cli start
- 小程序打包
- npx miniprogram-cli build
- 获取小程序信息
- 向后台上传小程序
- 登录后台, 获取上传 token
- miniprogram-cli
- 整合在 IDE 中
- 提供 create/start/build 功能
其次是启动流程:
对应的, 是以下模块&功能:
- App(Android & iOS)
- 小程序广场页(一般是聊天列表页下拉)
- 接口调用(获取小程序具体配置)
- 静态资源下载 & 校验
- 启动小程序
- 后台
- 根据预设条件判断 detail 接口返回值
- 符合条件返回小程序静态资源地址 & md5 校验值
- 不符合条件走异常流程
- 小程序未上线
- 小程序已下线
- 小程序已被屏蔽
- 所在平台未开通小程序
- 所在城市未开通小程序
- 所在用户组没有访问小程序权限
- 基础库版本过低
- 基础库版本过低, 降级到 h5 地址
- 基础库版本为特定值, 需要返回指定静态资源内容(锁版本)
调研期间我们可以不考虑具体实现方案, 只整理完成小程序项目所需的前置技术点, 大致可以分为这么几类:
# IDE 选型
- [ ] 构建 IDE 可选方案集
- [ ] VS Code 插件
- [ ] Electron + 代码编辑器
- [ ] 代码编辑器方案
- [ ] [monaco-editor](https://microsoft.github.io/monaco-editor/)
- [ ] [vscode-web](https://github.com/microsoft/vscode/blob/main/remote/web/package.json)
- [ ] [code-server](https://github.com/cdr/code-server)(第三方公司实现的 web 版 vscode)
- [ ] CodeMirror
- [ ] ace.js
- [ ] 订制 VS Code
- [ ] [Theia](https://theia-ide.org/)
- [ ] [阿里-开天 IDE-未公开发布](https://developer.aliyun.com/article/762768)
- [ ] 编写界面
- [ ] 创建小程序
- [ ] 登录小程序后台(以获取上传用的 token)
- [ ] 启动预览
- [ ] 启动构建
- [ ] 上传小程序包
- [ ] 类 chrome 的 DevTools
# cli 工具
- [ ] 制定/维护小程序项目模板
- [ ] 基于模板创建小程序项目(npx miniprogram create)
- [ ] 启动小程序开发环境(npx miniprogram start)
- [ ] 构建小程序安装包(npx miniprogram start)
- [ ] [进阶]打包输出 source-map, 支持监控线上错误/查看报错详情
# App
- [ ] 小程序启动流程设计
- [ ] 逻辑进程渲染进程间通信方案设计
- [ ] 小程序实现方案设定
- [ ] 页面切换如何实现
- [ ] 前进/返回效果
- [ ] 打开新页面效果
- [ ] 从 App 进入小程序/从小程序跳转到 App/从小程序跳转到 App 再返回小程序的交互过程 如何实现 / [进阶] 如果跳转到外部 App, 如何实现(如微信小程序打开百度地图)
- [ ] schema 跳转方案
- [ ] 支付功能
- [ ] 本地静态缓存
- [ ] Native & js 通信方案实现
- [ ] js 如何调用 Native 中的接口
- [ ] Native 如何获取 js 中传入的参数
- [ ] Native 运行完成后, 如何通知 js. 期间控制流程切换的时序图如何设定
- [ ] js 如何获取 Native 中执行方法后的结果
- [ ] 实现非 ECMA 语法
- [ ] 实现 setTimeout
# 小程序后台
- 技术选型
- 项目方案(Express/koa)
- ORM 方案
- CDN 上传
- redis 库选择
- 日志记录
- 接口设计
- Mock 管理
- 文档管理
- 用户系统
- 注册/登录
- 项目权限管理
- root 用户
- 管理员
- 开发者
- 预览成员
- 小程序发布流程设计
- 上传->预览->提审->审核->发布
- 小程序项目配置
- 项目基础信息(logo/应用名/应用简介/etc)
- 降级策略
- 开城策略
- 注销应用
- [进阶]监控系统
- 数据清洗
- 数据存储(ES/mongodb)
- 错误查询
- source-map 解析
- 性能监控
- 订制数据项
- 上报/处理/分析数据项
# 小程序基础库(运行在逻辑进程中)
- [ ] API 设计
- [ ] 页面启动
- [ ] 页面切换
- [ ] 页面路由管理
- [ ] 路由参数读取
- [ ] 触发页面生命周期事件
# 小程序 webview-render(运行在渲染进程中)
- [ ] 订制渲染协议
- [ ] 向逻辑层转发 Dom 事件(支持冒泡)
- [ ] 单实例组件支持(例如地图组件)
# 组件库
- [ ] 技术方案选型
- [ ] React
- [ ] Vue
- [ ] [stencil](https://getstencil.com/)
- [ ] 特殊元素支持
- [ ] Canvas
# 其他问题
- [ ] 构建小程序预览环境
- [ ] 版本管理与同步
- [ ] alpha 版本与 正式版
- [ ] 管理小程序项目/小程序基础库/小程序 webview-render/小程序组件库/Native 小程序 sdk 之间版本对应关系
- [ ] bug 解决
- [ ] 逻辑进程引擎统一为 V8
- [ ] jsCore 中 setTimeout 的 bug
其中, 最为关键的, 是这两个问题:
惊险一跃
, 整个小程序方案将会无从谈起欲知这两个问题究竟如何解决, 且听下文分解。