Rax 转小程序链路原理解析(一)

3,046 阅读8分钟

前言

随着 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.jsfoo.axmlfoo.json 等三个同名文件(如有样式内容,还会多一个 foo.acss 文件)。其中,foo.js 会包含源文件中的业务逻辑,foo.axml 会与源文件中的 JSX 有所对应,而 foo.json 则需要包含源文件引入自定义组件(该示例中只引入了 rax-view)的声明。Rax 转小程序链路做的就是以上的转换操作。当然,这只是简单的单个组件层面的转换,项目级别的构建会有很多复杂的问题需要考虑,下面我们将解析 Rax 转小程序链路在工程上的实现原理。

概览

先看一下整体的架构图:

Rax 转小程序链路架构图c.001
Rax 转小程序链路在实现上主要分为四个模块:CLIloadercompiler 以及 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 属性并作支付宝/微信两端的配置抹平
  • page-loader
    • 根据 jsx-compiler 中解析到的该组件所引用组件的信息,写入 json 文件的 usingComponents 属性中,并将这些组件加入 webpack 依赖分析链并交由 component-loader 处理
    • 处理用户定义在 app.json 中 routes 数组内每一个页面的配置(即 window 配置项)并输出至对应页面的 json 文件中
  • component-loader
    • 根据 jsx-compiler 中解析到的该组件所引用组件的信息,写入 json 文件的 usingComponents 属性中,并将这些组件加入 webpack 依赖分析链并交由 component-loader 处理

file loader

处理图片等静态文件资源,将其拷贝至指定目标文件夹。

script loader

所有 loader 中任务最繁重的非 script-loader 莫属。script-loader 负责处理所有 js 文件。而对于 js 文件,有一个非常重要的需要考虑的问题就是依赖路径处理。在 Rax 转小程序链路中,我们默认采用将 node_module 中使用到的文件提取并拷贝至目标文件夹,这样做的原因基于以下两点:

  1. 微信小程序项目不支持直接使用 npm 包(在我们设计方案时微信尚不支持,但是目前已经通过在 IDE 中点击『构建 npm』功能来使用 npm 包,其原理也是通过拷贝提取 npm 包文件至其指定的 miniprogram_npm 目录)
  2. 支付宝小程序虽然支持使用 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-clijsx2mp-loader 的基本原理,也梳理了 Rax 转小程序链路工程上的整体脉络。关于 jsx-compiler 怎么处理编译以及 jsx2mp-runtime 是如何作了运行时的支撑并抹平了原生小程序组件实例与 Rax 实例的差异,敬请期待后面的文章。

其它