作者|刘子健
编辑|贾亚宁
原文地址:前端之巅
本文由 InfoQ 整理自阿里巴巴淘系技术部前端技术专家刘子健(繁易)在 GMTC 全球大前端技术大会(深圳站)2021 的演讲《跨云 & 跨端——小程序云开发一体化方案》。
你好,我是刘子健,花名繁易,GitHub 的账号是 Lxxyx。目前在阿里巴巴的 Node.js 架构组担任前端技术专家一职。今天给大家带来的分享话题是:《跨云 & 跨端——小程序云开发一体化方案》。
目前我的个人工作主要专注于以下三方面:Node.js、Serverless 和 Web 框架。
同时我个人也在积极地参与开源社区共建:
阿里 Node.js 框架 Midway.js 团队核心成员,负责框架研发工作Node.js Core CollaboratorTC39 阿里巴巴代表,对部分提案提供反馈 / 意见
本次的分享我会从以下四个部分来展开:
小程序与云开发云开发一体化方案iFashion 落地实践总结与展望
小程序与云开发
小程序
自微信推出小程序开始,各大厂商都针对自己的平台推出了相应的小程序的方案,可以说是层出不穷,基本上每一个厂都有一个对应的小程序的一个方案(如下图所示)。
但是很多第三方开发者或者厂商开发的应用不只是投放至单一平台的。很多时候有一码多投或代码复用的需求,在这种情况下,开发者必须去使用跨端框架。
大家也知道微信传统的小程序开发模式,我认为是非常低效且痛苦的。于是很多开发者想着使用前端框架来解决开发体验的问题,所以也会去使用跨端框架,比如基于 Vue 或基于 React 的各种第三方跨端框架。
因此对于目前小程序跨端框架的现状,我的判断是:“跨端和效率的需求催生了大量的跨端框架,每次都有新方案,每年都有新东西。”
云开发
我们再来说说云开发。提到云开发,就不得不先提我们传统的服务开发。
在我们传统服务开发中,如果你要去开发一个应用的后端,其实是非常繁琐的。
大家可以看上图,在实际的开发过程中,开发者其实是需要关心应用扩容、安全、文件存储、容灾等各方各面的。开发者在服务端需要做非常多的运维工作,之后也需要花费很多时间在接口联调上,然后才能去实现业务逻辑,整体工作其实是非常繁琐的。
对一些第三方规模比较小的开发者,甚至说个人开发者,这个部分的成本其实高到难以接受。
面对上面说到的这些问题,云厂商推出了云开发功能。关于云开发,我把它拆解成以下两个部分:
平台特定的 API云开发基础能力
那么云开发到底节省了哪部分的时间呢?我认为有以下两方面:
在一些常用功能的开发上,你不再需要重新去做很多事情了。例如登录支付功能,做过对接的同学都知道这个过程非常痛苦。但是有云开发提供的 API 情况下,就可以直接调用它内置的 API 去实现功能,从而节约你的开发成本;云开发的基础能力(函数、文件、数据库等)。也就是说你不用再关心运维,不用再关心安全,不用再关心容灾,直接去调用这些就可以。然后它整个的目标是为了让你能专注到业务逻辑上。
关于云开发,我们的判断是:“Serverless BaaS,可以降低小程序的开发成本。”
但云开发不是一个新东西,在 2018~2019 年左右其实就已经有云厂商提出并落地了。大家在各种会议上听分享,其实可以看到社区重点提的都是跨端框架,例如我的框架可以跨几个端,但几乎没有人关注到云开发可以怎么做。
对于这一点,其实我们也有自己的一个判断:云开发和跨端之间存在“不可调和”的矛盾。我们可以做跨端,但很难说跨端框架上可以很好地去利用云开发的能力。
我们归纳总结之后认为,跨端与云开发间存在以下三个矛盾:云开发的语法分裂、开发繁琐以及云平台锁定。
第一个矛盾是:云开发的语法分裂。
在上图中我列举了几个云:阿里云、腾讯云、轻服务。这一点其实非常有趣,因为我在长期跟踪各个云的云函数功能,发现每个云为了让自己长得不太一样,它必须设计一个不太一样,但其实一样的语法。例如:
阿里云:context腾讯云:event轻服务:params
虽然功能大致是一样的,但不同厂商有着不同的一个语法,导致云开发的语法其实相当分裂。之前做跨端小程序,开发者可以通过同一套 DSL 去做,但在云开发上其实很难去做,每个云服务商你都要学习和接受全新的语法。
第二个矛盾是:开发繁琐。
目前所有云开发的目录结构或者是云开发所谓的架构都是上图的模式:
架构视角:端侧使用了云开发 BaaS 服务进行开发;代码视角:前端后端同一个仓库,通过 monorepo 的方式去开发,其中小程序的每一个函数都需要创建新文件夹。
这种情况下,其实我们认为它只是机械的组合资源,你的代码很多部分是雷同的,并没有发挥 JavaScript 通吃前后端的优势。
第三个矛盾是:云平台锁定。
云平台锁定其实是提的比较多的一个概念。简单来说,如果你选择了一家云服务厂商,你的数据、你的代码、你的服务器全都上云之后,如果这个云服务厂商的质量没有达到你的要求,或者说它的功能达不到你的要求,你想去切换云的时候,这种时候成本是非常高并且难以接受的。这种情况我们就称为云平台锁定。
很多厂商也会选择多云策略,同理做小程序也是一样,做小程序时,开发者可能说不止投一个平台,但很多时候你也希望去使用别的平台的功能,就容易遇见云平台锁定问题。
说完以上三个矛盾,接下来我给大家一起来看我们是如何去解决这些问题的。这边就要提到我们推出的云开发一体化方案。
云开发一体化方案
一体化应用开发方案
一体化应用开发方案它并不是一个非常新的概念。这个方案其实是我们团队 2020 年 4 月份在阿里内部发布的。因为涉及到非常多配套的云平台,因此也没有做大规模的宣传。
目前在阿里内部有 2500 一体化应用,它也是目前阿里前端主流开发模式之一。举个例子:目前我们在开发中后台应用的时候,已经很少在开发那种纯资源型的应用,而是选择一体化应用的研发模式,它可以更好地利用函数与 Serverless 的能力,让开发者可以去做一些后端的事情,我们认为这对应用来说是一个增强的能力。
一体化应用本来只是我们在开发 Web 应用时去使用的方案,比如说 C 端或者中后台应用。但是我们发现如果把它的方案和我们内部的小程序方案去做一个结合(例如淘宝小程序),其实也可以解决小程序所带来的一些痛点。
因此我们也将云开发与小程序的方案去进行了一个融合,接下来会具体分享一下我们是如何解决上面三个矛盾的。
语法分裂 — 函数式
第一个矛盾是语法分裂。
前面讲到语法分裂的时候,我提到不同厂商有不同语法,往往这种时候,我们需要设计一种 DSL,通过跨端的 DSL 来解决这个问题。而在后端情况下我们发现其实有更简单的方式,因此我们最终选择的 DSL 可能简单到,我相信在座的每位同学不用学习就会了。
函数式 – 函数即接口
在这里,我们使用的 DSL 是 JavaScript 函数,也称作为:JavaScript 函数即接口——统一 & 无协议。
我们发现,在实际开发的过程中,你去开发一个 JavaScript 函数,这个函数所携带的信息其实就已经足够描述 API 接口或者云函数需要的信息。我以 HTTP 接口为例:
HTTP 路径:函数名 文件名HTTP Method:无参数为 GET,有参数为 POSTHTTP Body:参考 RPC 设计,使用数组来映射函数实际入参返回值:TypeScript 自带类型分析
这一点就是我们所提出的一个理念,叫做基于函数元信息生成接口。同理如果你生成的是一个云函数,你有一个函数名,只要保证这个函数名不重名,你就能使用函数名作为函数 ID 去生成一个云函数。
大家可以看上图中的代码,这里用了一个 useContext 的 API,这也是我们方案的创新之处。
函数式 – 使用 Hooks 开发后端接口
这一点我们称之为: 使用 Hooks 开发后端接口。
为了给开发者提供更好的开发体验,解决语法问题和 DSL 问题。我们提出了一个全新的理念:Hooks 开发后端接口。在传统的 Web 开发中,大家其实需要传递 Context 的,此处 Context 你可以理解为 Express 的 req/res 或者是 Koa 的 ctx。也可以理解为云函数里面 Context 所带的一些函数信息。
上图有简单的示例,如果你的云函数支持 HTTP 请求的话,可以使用 useContext API 来获取到 Context,从而拿到请求的 IP。这种情况下你可以获取上下文,而且不需要手动传参数,使用方法也与前端完全保持一致。这一点也会引出我们后面的一个核心 feature ,后文会说到。
我们当时做的时候其实也遇到了一些难题,比如其实 JavaScript 是不支持线程级 Context 传递的,想要实现需要依赖 Node.js async_hooks 模块。
这背后其实也有一个小故事,就是 JavaScript 不支持线程上下文,我们想让它支持,要怎么办?我们团队在 2020 年的 6 月份向 ECMA 去提交了一份关于线程上下文的提案,但是因为各种语言语义等潜在的问题,并没有通过,所以后面我们才切换到 Node.js async_hooks 的实现。所以我们也在积极参与标准,只不过这个过程比较坎坷,不像 Error Cause 提案推进的比较顺利。
当我们在使用 async_hooks 时,其实也存在两个问题:
性能问题非常严重稳定性不足,Node.js Module Stability 1
因此,我们尝试做了两版方案来解决提到的这两个问题。
A. 编译时方案
首先是性能问题,我们通过编译器解决。我们自研了 TypeScript 的编译器 mwcc,这里面用到了非常多的 TypeScript Internal API。我们通过将所有函数的调用编译为带 bind 的调用方式,同时去生成正确的 source map ,从而得到一个可以正确被加载和调用的函数,也解决了性能问题(当然也存在一定的语法限制)。
但是最后我们还是放弃了这个方案。因为在 TypeScript 3.9 迭代到 4.0 的时候,TypeScript 4.0 出现了非常多的 breaking changes,然后内部的 API 改动也非常大,我们已经改不动了,所以我们决定投入到第二版方案,运行时方案。
B. 运行时方案
这个使用了 Node.js 中的 AsyncLocalStorage API。AsyncLocalStorage 是 Node.js 为了专门解决请求上下文追踪难题或者用来做监控的一个模块,它性能比较好,也没有语法限制,同时 API 也比较稳定。
在这之后我们也在积极参与社区。下图就是我们参与社区的例子,包含我们对 AsyncLocalStorage 的反馈等,来推进这个 API 达到稳定可用的状态。
在 Node.js 16.x 时,这个 API 正式落地了,目前是一个 Stability: 2 的稳定模块。整个过程略显繁琐,但其实也是我们在不断探索的一个过程,也是我个人加入 Node.js 社区的初衷。
开发繁琐 – 全栈架构 & 简化调用
关于开发繁琐:其实大家用传统的开发模式,去开发一个全栈应用或者云函数应用是非常繁琐的。每一个云函数都需要新建一个文件夹,需要拷代码,很难复用。如果抽象成一个 npm 模块,你需要来回发布,也非常繁琐,并且前后端其实也是不连通的。
全栈架构
在这里,我们也推出了一个全新的架构模式,叫全栈架构。
上图就是目前我们内部使用一体化去开发的一个目录结构。与传统的方式完全不一样的是,src 目录内包含了云函数、小程序页面等前后端的资源。
这么做的好处其实有很多:
统一依赖管理:不再需要重复安装依赖了,你也不需要再复制粘贴文件了统一工程配置:避免来回配置共享 src:这个其实带来了非常大的一个想象力空间,你可以共享代码,你也可以共享类型。之前前后端那种分割的模式下,需要每次手动生成类型的时代已经过去了,你们都在一起,为什么还要互相引用呢?为什么还要互相生成呢?直接引用就行,而且是最新的。
云函数依赖管理
针对这个目录结构,我们也做了非常多的优化,其中就包括依赖管理。
这张图是我们云开发目前的函数结构,我相信所有做过云开发用过函数的同学都知道,每个云开发函数都是一个单独的一个实例,它包含业务逻辑 / node_modules / package.json,每个函数都互相分隔开来。
这样做会带来以下问题:
跨函数逻辑复用:比如说你要去检查用户是否登录,utils 的抽象复用,都会带来逻辑复用的问题重复依赖:每个函数如果要用到 loadsh,就得重复安装重复文件:package.json 等
针对以上这些问题,我们提出了新方案:一体化函数。
我们将函数以入口文件的方式做了隔离。简单来说,我们不再是像左边一样一个函数包含所有文件,而是只包含一个入口文件。
大家可以看到我每个函数都有一个入口文件, A 函数 / B 函数 / C 函数其实是可以引用公用的一些逻辑的,比如说有 utils / model / service, 然后共享同一份 node_modules 和 package.json。在打包的时候,我们在工程上做优化,而不是让你手动去做优化。
在打包的时候,我们会根据每个函数入口文件去打包成一个函数,然后单独去做部署,相当于是说你在保留了应用开发的习惯之下,同时拥有了函数的一个优势,而且不需要去做切换,也不会遇到刚才的那些问题。
ncc -> Webpack for Node.js
在这一点,其实我们是使用了 vercel 的 ncc 。ncc 有个最简单的解释:“Webpack for Node.js”。它可以把一个函数的入口文件打包成单独的一个 bundle ,把这个函数所引用到的各种的后端的一些模块,不管是 lodash 还是相关的模块,它都会帮你打包在一起。这样做的话其实可以保证你每个函数入口文件都是一个单独的可以部署的 JS。
数据比对
这里其实我们也对新的方案进行了一个优化。在我们传统的打包方案过程中,打一次包其实可能会需要 20~30 秒,我们需要做非常多构建和优化的事情。同时,压缩包也很大,然后解压后的体积也很大,启动时间也很慢。
而在单文件的情况下,不仅打包时间快,压缩包大小也小,我们解压体积也小。因为 Webpack 大家都知道它有 Tree Shaking 的功能,可以把一些用不到的模块给去掉,因为体积小所以启动时间也快。
通过这种方案,我们解决了云开发工程中资源机械组合的问题,但是我们觉得还可以再往前进一步。
简化调用 – “零”API 实践
我们把后面进一步的实践叫做:“零”API 实践。
刚才我说过共享 src 的代码其实有非常广阔的想象空间,想象空间就在于此。因为你共享代码之后,你调用的函数或者接口,其实是不需要再去手动发请求,或者不需要再手动使用云函数的客户端。
你只需要两步就可以完成整个调用:
把函数从 API 目录里面导入进来,比如上图的 getUserProfile 函数调用函数,拿到返回值
在这个过程中,我们把原来传统的云函数的发布、调用、获取返回值的过程给精简化了。为什么我们上一章会提到用 JavaScript 函数去写接口,因为这一章我们就要用 JavaScript 去调用,把云函数开发或前后端调用的过程中的胶水层给抹到 0。
这个就是我们一个核心的设计理念,“零”API。
这样做的好处很多:
前后端调用的胶水层都被抹去了通过 useContext 获取到请求上下文,调用接口函数和普通函数是一样的前后端都使用 Hooks 的语法,前后端非常一致
简单且容易理解。
简化调用 – “零”API 原理
整个方案的原理其实也很简单。
大家都知道前端工程里,我们会依赖 Webpack / Vite 此类的构建工具。我们可以通过 Webpack Loader 或者 Vite 的 Plugin 来对导入的文件做处理。
如上图所示,一个导入的函数,我们可以在 Loader 中,通过 es-module-lexer 来解析导出的函数,最终构建出前端可用的 API Client。也就是说,前端在编辑器里面引入的是后端接口,但实际运行的时候其实已经被替换成了 API Client,从而可以正确地发起调用。
提一句题外话,es-module-lexer 实际上也是个非常有趣的库,这也是 Vite 使用的底层库,用来分析导入导出的,大家有兴趣可以上 GitHub 看看相关的文档和实现。
云平台锁定 – 跨云 & 多云适配
刚才我们提到的云平台锁定,是否能去做一些多云的适配工作呢?其实是可以的。
函数式 – 多云适配
多云适配的原理比较简单。
我们针对不同的云厂商,开发了不同的 adapter,每个 adapter 负责如下的工作:
解析参数调用函数错误处理
上图中有 adapter 简单的示例。一个 adapter 接收 fn 和 ctx 两个参数,其中 fn 就是我要调用的云函数,ctx 则是调用的上下文。
关于 ctx,因为不同云对应的入参不一样,因此获取参数的方式也不一样,可以看上图。当我们拿到入参的时候,因为之前提到过入参的格式是数组,其实是可以直接和后端参数映射上的。这种情况下,前端传入的参数就是后端实际收到的参数,就可以实现跨云的一体化调用了。
跨云原理 – 多云部署
同时多云其实还涉及到部署的问题。
上图是我们的部署产物的结构,包含:
User Code:用户代码entry.js:云平台入口文件,不同云平台要求入口文件格式和导出方法不一致Args Adapter:云平台适配
这些构建产物可以去部署到不同的云平台,比如说我生成的是阿里云的部署结构,我就可以部署到阿里云;生成到腾讯云的结构,我就可以部署到腾讯云;生成到自有服务的结构,就可以部署到自有的一个云平台,非常自由。
关于该方案其实后面还有很多可以做的事情,但基本原理就是根据不同云平台去生成适配代码,提交部署。而且适配代码是做的非常轻量的,你不需要去关心特别多的事情,只需要去拿到参数,调用函数并返回即可。
跨云 – 使用 Case
这里我会列举一些跨云的使用 Case。
例如小程序 云函数的组合,去落地跨云方案时,其实是可以根据不同云平台去使用不同云函数的一些服务的。例如:
支付宝:支付宝小程序 阿里云 FC微信:微信小程序 微信云函数头条:头条小程序 轻服务
如果你的代码不涉及到具体的后端服务,其实在云平台上就可以部署和调用。
最后还有一点是关于我们的内部场景,我们称为自建云服务,这个其实能做的事情就非常多了。我们开发的一体化应用,前端可以部署为 Web / 小程序 / Native 页面,后端也可以部署在自建的云服务 / FaaS / 裸机上。在这种架构模式下,我们可以在不同的前端源站及云服务间切换,不仅防止锁定,而且可以统一语法,降低学习成本。
这里其实也提到了一个很重要的概念,就是统一语法,降低学习成本。像之前社区有两个概念:
Learn once write everywhere. 这个是 React 的理念Write once run everywhere. 这个是国内部分开源产品推荐的理念
而我们实际上更认可前者,学习一次在哪儿都能写。
iFashion 落地实践
在解决了上述的问题后,我们尝试在内部找了一个小程序 iFashion 做落地尝试。
项目概况
iFashion 是淘宝服饰的应用,也是我们内部流量非常大的一个小程序应用。
它的技术栈如下:
Rax阿里云定制版构建工具 Web投放平台:手淘小程序 &H5 页面(我们开发的应用会投放至全网;投放在手淘是小程序,投放在外部我们是 H5 页面)
前两年淘系前端在推动一个前端研发模式升级战役,在一体化方案推出后,iFashion 也希望使用一体化方案去接入一个内部的云服务去提升效率。这也就是我刚才提到的那个使用的 case——如何去把这个服务部署到自建云平台上。因为有些公司不会用公有云,它自己就有云服务(比如阿里、腾讯等云服务商)。
Webpack 工程接入
实际工程接入一体化应用需要解决几个问题,第一个就是如何接入 Webpack 的工程体系。
这一点其实非常简单。我们开发了 Webpack Loader 和 Express(for webpack dev server) 的中间件,然后使用 Webpack chain 去快速接入原有的工程。
上图就是我们实际接入的代码,通过这简单的几行代码,我们就可以去接入一体化工程,成本很低。
这个就是我们实际的调用流程图。
一体化调试
第二部分是关于一体化调试的,也是目前我们做的觉得体验非常好的一个功能。
这里我写了一个简单的工程,前后端代码分别如上图,功能也很简单,就是展示 Rax 和 Midway 两个仓库的 Stars。
说一句题外话,大家看上图的后端代码,第四行其实有个注释,这个是给 GitHub 的 Copilot 用的。整段 Demo 基本上是 Copilot 写的,在小 Demo 的场景中非常好用。
下面是我们调试录制的视频。在一体化调试功能下,你可以在编辑器中,一次完成从前端到后端的全链路 Debug。
,时长00:48
一体化调试放大版
,时长00:17
一体化发布
关于应用的发布,我们也定制了内部的发布工作流。
如上图所示,当我们在点击发布之后,Workflow 会运行设定好的 steps ,去做不同的事情。
从源码检查开始,之后同时会进入一个并行的构建的过程。首先是前端进行构建,这时前端会将它的产物进行一个上传和部署,网页就到 CDN,小程序就到小程序平台。然后也会对函数做一个构建和部署,去部署到相应的 FaaS 平台上或者多个云平台上。
构建完成,有一体化灰度和切流的节点。简单的说,相较于之前的前后端分开切流、中间需要配合不一样,我们在一体化切流的 Workflow 中也做了非常多的定制,能保证整个应用现在按一体化的维度去做切流,新应用新接口、老应用老接口,新老接口不会串。我们认为这是一种更简单的一种开发方式。
灰度完成后即正式上线。
提效 30%
以下是我们上线数据的实测结果。
关于应用的发布,我们也定制了内部的发布工作流。
上图是我们之前在开发一个需求时,需要投入前端和后端两名同学,大概一起开发 13 个人日。
这张图则是使用一体化这种开发,只要投入一个人 10 个人日即可。
这个听起来有点不可思议。但是我们在内部大量应用实践的情况下发现,当你使用一体化这种开发模式去开发一些简单或者一般的需求时,你完全可以实现节约人力投入且节约时间的效果。
因为在开发过程中,其实大家有做过前后端联调,都知道联调是非常繁琐的一个过程。联调 / debug / 上线等待花费了很多时间,但可能你的改动就两行代码。因此在使用一体化的时候,一个人开发就可以大大加速整个流程。
总结与展望
最后来总结一下:
在跨端小程序方案中,我们去尝试引入云和一体化的开发方案来进一步为开发同学提效。
这一点其实也是我们架构团队一直在探索的,我们思考端侧红利及工程红利已经见顶的时候,有什么办法能进一步提高开发效率呢?
于是我们尝试了云和端的一个结合,这个也是我们未来非常重要的目标和方案。我们会尝试去更多地和云上的能力做结合,而不仅仅是在端上去寻找更多的红利。因为端上再怎么做,还是需要那么多人。而你在云上做,一个人做掉了反而可能效率更高,这是一个非常神奇的反应。
总结之上,再做一个展望:云端融合
在目前的场景中,我们也去加入了 TC39,参与提案的讨论。
上图中有两个提案,一个是 JS Module Blocks,另外一个是衍生的提案 JS Module Fragments。我们更关注的是后者,因为这能为一体化应用带来更好的研发体验。
这就是我们基于 JS Module Fragments 写的 Demo。这里面有一个叫 cloud 的模块,写一些后端的代码,也可以去引入微信的服务端的 SDK,去拿到微信的 context,去获取它的 OpenId。同时我可以有一个 client 的模块,来写前端的逻辑。其中后端接口的调用可以直接从云端这个 cloud 这个模块导入。
这个提案下,一个 JS 文件就包含了前端、后端两部分的代码,并且非常和谐。这个也是目前我们未来可能会去倡导的一个理念叫云端更加融合,更加一体化。
云端一体在前端框架的趋势已经非常明显了,无论是 Next.js/Nuxt/Svelte Kit 或者是新出的 Remix 框架,大家都在更多地去结合云函数的能力,去提供服务 OR 实现 SSR。
目前我们也在和前端委员会的标准化小组去共同推进提案,这个是我们的反馈链接:
https://github.com/tc39/proposal-module-fragments/issues/14。
欢迎关注
整个一体化框架是基于 Midway.js 开发的,也欢迎大家多多 Star 或者 Watch 我们的 Repo,看看我们最近在搞什么新东西。
一体化方案后端框架:
https://github.com/midwayjs/midway
最后分享我个人的 Slogan:让 Node.js Web 开发更简单 & 有趣。这也是我一直在努力的方向!
作者简介
刘子健(繁易):阿里巴巴淘系技术部 前端技术专家。
2018 年校招加入阿里。阿里 Node.js 框架 Midway 负责人,Node.js Core Collaborator,阿里巴巴 TC39 代表,目前就职于淘系前端 – Node.js 架构组,全职负责 Node.js 框架 Midway 的设计与研发工作。个人于 2019~2021 年,创新性的提出并推动云端一体的新研发模式大规模落地,目前已成为阿里前端主流的研发模式。目前专注于 Node.js 架构 / Serverless / 开发者体验 / 前端标准 / 工程化领域,具有较为丰富的实战经验与较好的技术视野。