前言
随着 1.0 版本的发布,Rax 在小程序端的转换能力也得到了补齐。现在,你可以像过去开发 Web/Weex 端的应用一样使用 Rax 来进行小程序的开发。本系列文章将介绍 Rax 转小程序链路的实现原理。这是本系列的第一篇。
首先,我们需要对小程序(如无特别交代,文中的小程序均指支付宝小程序)的开发方式有一定的了解。你可以查看这里对整个小程序体系框架有一个大概的认知。本文假定读者已对上述内容有所了解。为了明确转换链路要做的事情,我们以最简单的情况举例:在使用 Rax 开发项目时,单个组件的内容如下所示:
// foo.js
import { createElement } from 'rax';
import View from 'rax-view';
export default (props) => {
return (
<View>Hello World!</View>
);
};
经过处理后,该组件对应的小程序代码应当包含 foo.js
、foo.axml
和 foo.json
等三个同名文件(如有样式内容,还会多一个 foo.acss
文件)。其中,foo.js
会包含源文件中的业务逻辑,foo.axml
会与源文件中的 JSX 有所对应,而 foo.json
则需要包含源文件引入自定义组件(该示例中只引入了 rax-view
)的声明。Rax 转小程序链路做的就是以上的转换操作。当然,这只是简单的单个组件层面的转换,项目级别的构建会有很多复杂的问题需要考虑,下面我们将解析 Rax 转小程序链路在工程上的实现原理。
概览
先看一下整体的架构图:
Rax 转小程序链路在实现上主要分为四个模块:CLI、loader、compiler 以及 runtime。CLI 即命令行工具,是整个链路的入口,用户编写的所有业务代码都经由 CLI 读取、处理和输出。loader 即 webpack loader,用于处理各种类型的文件,包括 app、page、component、script 以及静态资源等。compiler 用于进行 AST 转换并生成对应的小程序代码。最后,runtime 为生成的 js 代码提供了运行时的支持,确保其在小程序环境下能正常运行。而在更上层,我们有多端统一的 universal 组件以及 API 的基础服务支持,使得小程序的开发体验和原先的 Web/Weex 端几无区别。本文将介绍 CLI 和 loader 两个工程模块。
CLI
把 CLI 和 loader 放在一起介绍是因为二者在功能上联系非常紧密。CLI 本身底层依赖 webpack 对项目进行依赖分析,然后调用 loader 层提供的各种 loader 对对应类型的文件进行处理。CLI 对外提供 watch 和 build 两个指令。前者用于监听代码变动并实时编译,后者相比前者会剔除部分调试用的代码(如 source map)并压缩代码,完成编译打包。
具体到实现上,CLI 本身并不复杂,用一句话概括的话就是从命令行读取各种必要参数,然后传入 webpack 执行。利用 webpack 的依赖分析能力,我们能够遍历到所有有效代码并交由对应的 loader 进行处理。这里可能会有同学提出疑问,入口文件是什么样的,它又是如何声明依赖的?因为在小程序原生开发框架中,入口文件 app.js
并没有声明依赖,而 pages 是在 app.json
中注册的。下面就需要先介绍一下 Rax 小程序的工程目录。
为了保持多端统一,Rax 采用同一套工程目录,当我们使用 rax-scripts 创建一个小程序项目时,其目录结构如下所示:
.
├── README.md # 项目说明
├── build.json # 项目构建配置
├── package.json
└── src # 源码目录
├── app.js # 应用入口文件
├── app.json # 应用配置,包括路由配置,小程序 window 配置等
├── components # 应用的公共组件
│ └── Logo # 组件
│ ├── index.css # Logo 组件的样式文件
│ └── index.jsx # Logo 组件 JSX 源码
├── document # 页面的 HTML 模板
│ └── index.jsx
└── pages # 页面
└── Home # home 页面
└── index.jsx
app.json
中包含路由配置。其默认内容如下:
{
"routes": [
{
"path": "/",
"source": "pages/Home/index"
}
],
"window": {
"defaultTitle": "Rax App 1.0"
}
}
CLI 将读取其中的 routes
内容并将所有引用到的 pages 文件以及 app.js
作为 entry,类似于多页应用程序的配置。至此,以 pages 文件为入口,所有依赖文件将依次被遍历并交由对应 loader 进行处理。loader 处理完毕后最终的编译代码将生成到目的目录,而 webpack 默认生成的 bundle 对我们来说并不需要,我们将 outputFileSystem
设置为内存文件系统使其不产出至磁盘上即可。
loader
jsx2mp-loader 中一共存在以下五个角色的 loader,分别用来处理对应类型的文件。下面将分别介绍其功能。
app && page && component loader
一句话概括三种 loader 的主要功能就是读取对应类型的文件内容(app-loader 处理 Rax 源码中的 app.js,page-loader 处理定义在 app.json 中 routes
属性内的 page 类型组件,component-loader 处理 component 类型组件)并交由 jsx-compiler 处理然后产出编译后代码,并写入至指定目标文件夹位置。除此之外,每个loader 还有一些自己特定的职责:
- app-loader
- 处理 app.json 中 的
window
属性并作支付宝/微信两端的配置抹平
- 处理 app.json 中 的
- page-loader
- 根据 jsx-compiler 中解析到的该组件所引用组件的信息,写入 json 文件的
usingComponents
属性中,并将这些组件加入 webpack 依赖分析链并交由 component-loader 处理 - 处理用户定义在 app.json 中
routes
数组内每一个页面的配置(即window
配置项)并输出至对应页面的 json 文件中
- 根据 jsx-compiler 中解析到的该组件所引用组件的信息,写入 json 文件的
- component-loader
- 根据 jsx-compiler 中解析到的该组件所引用组件的信息,写入 json 文件的
usingComponents
属性中,并将这些组件加入 webpack 依赖分析链并交由 component-loader 处理
- 根据 jsx-compiler 中解析到的该组件所引用组件的信息,写入 json 文件的
file loader
处理图片等静态文件资源,将其拷贝至指定目标文件夹。
script loader
所有 loader 中任务最繁重的非 script-loader 莫属。script-loader 负责处理所有 js 文件。而对于 js 文件,有一个非常重要的需要考虑的问题就是依赖路径处理。在 Rax 转小程序链路中,我们默认采用将 node_module 中使用到的文件提取并拷贝至目标文件夹,这样做的原因基于以下两点:
- 微信小程序项目不支持直接使用 npm 包(在我们设计方案时微信尚不支持,但是目前已经通过在 IDE 中点击『构建 npm』功能来使用 npm 包,其原理也是通过拷贝提取 npm 包文件至其指定的 miniprogram_npm 目录)
- 支付宝小程序虽然支持使用 npm 包,但是如果该 npm 包没有按照标准发布 ES5 语法的文件,则无法正常使用。支付宝小程序默认不会编译 node_modules 中的文件,理由是会拖慢编译速度(不过在最新版本中,支付宝已经支持对 npm 包作最基本的 ES6 转 ES5 的处理)
所以,依赖路径处理是 script-loader 最核心的功能之一,其基本工作流程是:搜集代码中使用到的 npm 依赖,获取 npm 包的真实地址 => 路径处理 => babel 编译 => 输出代码至目标文件夹。
在 Rax 小程序项目中,对于来自 npm 包的纯 js 文件(比如 loadsh)或者用户自己编写的本地 js 文件(比如 utils 文件),执行以上流程即可。对于以下两种类型,除上述基本流程外,script-loader 还需要一些额外操作:
来自 npm 包的第三方原生小程序库
用户使用绝对路径去使用第三方原生小程序库时,script-loader 需要读取 js 文件同目录下同名的 json 文件中的 usingComponents
字段并将其加入 webpack 的依赖分析链
来自 npm 包的依据多态组件(库)协议开发并发布的小程序组件(库)
关于多态组件(库)协议可以点击这里查看官网文档。 Rax 基础组件均基于该协议支持小程序端。实际上,通过使用绝对路径引入原生小程序组件使用完全可以满足需求,但是 Rax 定位于多端统一开发框架,这样做会使开发小程序端时产生平台特定的代码,无法真正做到一套代码多端运行,因此我们设计了多态组件(库)协议。用户在小程序端引入组件时可以正常如 Web/Weex 端一样边写代码如 import View from 'rax-view'
,但实际上由 script-loader 去读取 package.json 中 miniappConfig
字段的值以获取真实使用的原生小程序组件地址。基于多态组件(库)协议,用户可以方便地进行 Rax 小程序组件的构建与发布,然后在 Rax 项目中引入使用。
总结
本文阐述了 jsx2mp-cli 与 jsx2mp-loader 的基本原理,也梳理了 Rax 转小程序链路工程上的整体脉络。关于 jsx-compiler 怎么处理编译以及 jsx2mp-runtime 是如何作了运行时的支撑并抹平了原生小程序组件实例与 Rax 实例的差异,敬请期待后面的文章。