鸿蒙应用开发-知识:rollup打包

Hvigor

hvigor是一款基于TS实现的 构建任务编排工具,主要提供任务管理机制,包括任务注册编排、工程模型管理、配置管理等关键能力,更符合node生态开发者开发习惯

它不负责实际的打包,实际打包的工作是通过rollup来做的。

rollup

一个用于 JavaScript 的 模块打包工具,它将小的代码片段编译成更大、更复杂的代码

rollup打包工具在前端很早就有了,与它齐名的还有webpack,webpack主要用于打包web应用。

rollup基于ESM模块打包,能同时处理Nodejs和浏览器的JS打包工作,它还会自动对代码进行tree shaking减小包的体积,在对库和模块打包时非常有用。React、Vue等框架的构建就是使用的rollup

rollup 支持的打包文件的格式有 amd, cjs, esm/es, iife, umd。其中amd 为 AMD 标准,cjs 为 CommonJS 标准,esm/es为 ES 模块标准,iife 为立即调用函数, umd 同时支持 amd、cjs 和 iife

rollup.js 默认采用 ES 模块标准,ESM:ECMAScript 模块是未来的官方标准和主流

tree-shaking

tree-shaking 本质上是 消除无用的 JS 代码。 当引入一个模块时,并不引入整个模块的所有代码,而是只引入需要的代码,那些不需要的无用代码就会被"摇"掉

tree-shaking 虽然能够消除无用代码,但仅针对 ES6 模块语法,因为 ES6 模块采用的是静态分析,从字面量对代码进行分析

DCE(dead code elimination)

无用代码有一个专业术语 - dead code elimination(DCE)。编译器可以判断出哪些代码并不影响输出,然后消除这些代码。DCE主要包括以下几个方面

  • 代码不会被执行,不可到达
  • 代码执行的结果不会被用到
  • 代码只会影响死变量,只写不读

基于两个关键实现

  1. ES6 的模块引入是静态分析的,可以在编译时正确判断到底加载了什么代码。 ES6 Module一些特性如下

    1. 只能作为模块顶层的语句出现,不能出现在 function 或是 if等块级作用域中
    2. import 的模块名只能是字符串常量
    3. import binding 是 immutable 的,类似 const
    4. import hoisted,不管 import的语句出现的位置在哪里,在模块初始化的时候所有的import 都必须已经导入完成
  2. 分析程序流,判断哪些变量被使用、引用,打包这些代码

    1. 基于作用域,在 AST 过程中对函数或全局对象形成对象记录
    2. 在整个形成的作用域链对象中进行匹配 import 导入的标识,最后只打包匹配的代码,而删除那些未被匹配使用的代码

下面的截图中 index.js 是入口文件,打包生成的代码在 bundle.js 中,除此之外的 a.js、util.js 等文件均作为被引用的依赖模块

1)消除未使用的变量

a.js中定义的变量 b 和 c 没有使用到,它们不会出现在打包后的bundle.js文件中

2)消除未被调用的函数

仅引入但未使用到的 util3()和 util2()函数没有被打包进来

3)消除未被使用的类

只引用类文件 mixer.js 但实并未用 它的任何方法和变量,该类不会出现在bundle.js文件中

4)未消除的副作用-模块中类的方法未被引用

引用类文件 mixer.js并使用了其中的getName方法,虽然其他方法未被使用,但是整个类是被打包进去的

5)未消除的副作用-模块中定义的变量影响了全局变量

a.js和utils.js模块中都给window.c进行了重新赋值,他们的引入顺序会影响window上c这个属性的最终值

Rollup探索

AST 抽象语法树

树上定义了代码的结构,通过操作这棵树,可以精准的定位到声明语句、赋值语句、运算语句 等等。实现对代码的分析、优化、变更等操作

AST工作流

  • Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
  • Transform(转换) 对抽象语法树进行转换
  • Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码

打包流程

rollup的打包流程主要是 通过遍历输入的文件,生成抽象语法树AST并对AST进行剪枝做treeshaking功能,然后把最终用到的代码写入到输出文件中。有以下两个阶段

  • rollup()阶段,解析源码,生成 AST tree,对 AST tree 上的每个节点进行遍历,判断出是否 include(标记避免重复打包),是的话标记,然后生成 chunks,最后导出。

    • 通过 resolveId()方法解析文件地址,拿到文件绝对路径
    • 通过从入口文件的绝对路径出发找到它的模块定义,并获取这个入口模块所有的依赖语句并返回所有内容
    • 每个文件都是一个模块,每个模块都会有一个 Module 实例。在 Module 实例中,模块文件的代码通过 acorn 的 parse 方法遍历解析为 AST 语法树
    • 将 source 解析并设置到当前 module 上,完成从文件到模块的转换,并解析出 ES tree node 以及其内部包含的各类型的语法树
  • generate()/write()阶段,根据 rollup()阶段做的标记,进行代码收集,最后生成真正用到的代码

    • 将经处理生成后的代码写入文件,handleGenerateWrite()方法内部生成了 bundle 实例进行处理

具体细节可以参考,原理:无用代码去哪了?项目减重之 rollup 的 Tree-shaking

我看了下大体代码差不多。rollup最新版本为4.9.6 并且使用wasm技术,感兴趣的同学可以查看 github.com/rollup/roll...

为了简单的探索rollup的打包原理,我使用的版本为0.3.1

rollup中两个比较重要的库是 acorn 和 magic-string

acorn

一个JS语法解析器,用于将JS代码组成的字符串解析成抽象语法树AST。rollup 使用它来实现 AST 抽象语法树的遍历解析

比如这个代码

js 复制代码
export default function add(a, b) { return a + b }

通过 在线查看AST 之后,生成的AST如下

json 复制代码
{
  "type": "Program",
  "start": 0,
  "end": 50,
  "body": [
    {
      "type": "ExportDefaultDeclaration",
      "start": 0,
      "end": 50,
      "declaration": {
        "type": "FunctionDeclaration",
        "start": 15,
        "end": 50,
        "id": {
          "type": "Identifier",
          "start": 24,
          "end": 27,
          "name": "add"
        },
        "expression": false,
        "generator": false,
        "async": false,
        "params": [
          {
            "type": "Identifier",
            "start": 28,
            "end": 29,
            "name": "a"
          },
          {
            "type": "Identifier",
            "start": 31,
            "end": 32,
            "name": "b"
          }
        ],
        "body": {
          "type": "BlockStatement",
          "start": 34,
          "end": 50,
          "body": [
            {
              "type": "ReturnStatement",
              "start": 36,
              "end": 48,
              "argument": {
                "type": "BinaryExpression",
                "start": 43,
                "end": 48,
                "left": {
                  "type": "Identifier",
                  "start": 43,
                  "end": 44,
                  "name": "a"
                },
                "operator": "+",
                "right": {
                  "type": "Identifier",
                  "start": 47,
                  "end": 48,
                  "name": "b"
                }
              }
            }
          ]
        }
      }
    }
  ],
  "sourceType": "module"
}

AST是一棵树,由一个个的节点组成,每个节点都有一个 type 字段表示类型,例如 Identifier 表示一个标识符;BlockStatement 表示一个块语句;ReturnStatement 表示一个return语句等

树的根节点 type 是 Program 表示一个程序,这个程序内所有语句对应的代码的AST位于 body 字段下

magic-string

一个操作字符串的库,可以方便的替换、移除字符串中内容,并将字符串写入文件。rollup 使用它来操作字符串和生成 source-map 文件

下面是官方的一些示例用法

js 复制代码
import MagicString from 'magic-string';
import fs from 'fs'

const s = new MagicString('problems = 99');

s.update(0, 8, 'answer');
s.toString(); // 'answer = 99'

s.update(11, 13, '42'); // character indices always refer to the original string
s.toString(); // 'answer = 42'

s.prepend('var ').append(';'); // most methods are chainable
s.toString(); // 'var answer = 42;'

const map = s.generateMap({
  source: 'source.js',
  file: 'converted.js.map',
  includeContent: true
}); // generates a v3 sourcemap

fs.writeFileSync('converted.js', s.toString());
fs.writeFileSync('converted.js.map', map.toString());

目录结构简介

一些前置知识

  1. 在 rollup 中,一个文件就是一个模块

  2. 每一个模块都会根据文件中的代码生成一个 AST 抽象语法树,之后会对树上的每一个 AST 节点进行分析

  3. 分析 AST 节点,就是看看这个节点有没有调用函数或方法。如果有,就查看所调用的函数或方法是否在当前作用域,如果不在就往上找,直到找到模块顶级作用域为止。

  4. 如果本模块都没找到,说明这个函数、方法依赖于其他模块,需要从其他模块引入

js 复制代码
import { name, age, MyCls } from './modules/myModule' 
// 我们从myModule中引入了name、age、MyCls这几个变量,就需要从myModule文件查找 
// 在引入这几个变量的过程中,如果发现变量还依赖其他模块,就会递归读取其他模块,如此循环直到没有所引入的变量不再依赖的模块为止
  1. 最后将所有引入的代码打包在一起,写入最终的单个文件中。这个文件通过-o指定

举例看过程

  1. 按照如下的截图创建目录和文件,之后进入工程目录下执行 npm install,之后执行npm run build命令,生成的文件位于dist/bundle.js
  2. 可以看到,代码中没有用到的变量是不会被打进去的。一般开发库或者框架会选择rollup进行打包,可以减少代码的体积

大框架流程

读取入口文件

  1. rollup() 首先生成一个 Bundle 实例,也就是打包器。
  2. 然后根据入口文件路径去读取文件,最后根据文件内容生成一个 Module 实例

rollup->bundle.build->fetchModule

new Module()过程

在 new 一个 Module 实例时,会调用 acorn 库的 parse() 方法将代码解析成 AST

分析导入和导出的模块,将引入的模块和导出的模块填入对应的对象
  1. 每个 Module 实例都有一个 importsexports 对象,作用是将该模块引入和导出的对象填进去
  2. 填入的时候可以看到key是导入的名称,value是具有source、name、localName这些key的对象
分析每个 AST 节点的作用域,找出节点中定义的变量

每遍历到一个 AST 节点,都会为它生成一个 Scope 实例

Scope 的作用很简单,它有一个 names 属性数组,用于保存这个 AST 节点内的变量

分析标识符,并找出它们的依赖项

标识符:变量名,函数名,属性名等。

  1. 当解析到一个标识符时,rollup 会遍历它当前的作用域,看看有没这个标识符。
  2. 如果没有找到,就往它的父级作用域找。
  3. 如果一直找到模块顶级作用域都没找到,就说明这个函数、方法依赖于其它模块,需要从其他模块引入。
  4. 如果一个函数、方法需要被引入,就将它添加到 statement_dependsOn 对象里。生成代码时会根据 _dependsOn 里的值来引入文件
根据依赖项,读取对应的文件

rollup根据语句_dependsOn里面依赖的标识符名称,在模块的imports里面查找它对应的文件。然后读取这个文件生成一个新的 Module 实例

生成代码

到了这一步之后我们就已经引入了所有的函数,这是调用 Bundlegenerate() 方法生成代码。这一步还会做一些额外的操作

移除额外代码

例如从 foo.js 中引入的 foo1() 函数代码是这样的:export function foo1() {}

rollup 会移除掉 export,变成 function foo1() {}。因为最终会把所有的代码都写入到一个文件中,所以也就不存在export,所有的代码都在一个文件里

重命名

例如两个模块中都有一个同名函数 foo(),打包到一起时,会对其中一个函数重命名,变成 _foo(),以避免冲突

参考资料

  1. acorn
  2. magic-string
  3. rollup git仓库
  4. rollup npm package
  5. rollup官网
  6. rollup 在线体验 repl
  7. 在线查看JS的抽象语法树AST
  8. 工具:在线 ES6转ES5
  9. 工具:Google traceur 将ES转码成JS(适用于浏览器端)
  10. 工具:在线查看AST astexplorer
  11. 工具:在线查看AST语法树 esprima
  12. 工具: [揭秘 Rollup Tree Shaking](https://link.juejin.cn?target=https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000040009496 "https://segmentfault.com/a/1190000040009496")\]([segmentfault.com/a/119000004...](https://link.juejin.cn?target=https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000040009496 "https://segmentfault.com/a/1190000040009496"))

  13. 原理:从 rollup 初版源码学习打包原理 2.26.5版本
  14. 原理:浅析Rollup打包原理 0.3.1版本
  15. 原理:rollup打包原理 0.3.0版本
  16. 原理:rollup打包产物解析及原理(对比webpack)
  17. rollup - 构建原理及简易实现
  18. 原理:Rollup概念与运行原理
  19. 使用:rollup从入门到打包一个按需加载的组件库
  20. 使用:【实战篇】最详细的Rollup打包项目教程
  21. 使用:Rollup打包工具的使用(超详细,超基础,附代码截图超简单)
  22. 使用:一文带你快速上手Rollup
  23. 简单:关于Rollup那些事
  24. 使用Acorn解析JavaScript](juejin.cn/post/684490...)
  25. Roll打包系列文章
相关推荐
周胡杰29 分钟前
鸿蒙文件上传-从前端到后端详解,对比jq请求和鸿蒙arkts请求区别,对比new FormData()和鸿蒙arktsrequest.uploadFile
前端·华为·harmonyos·鸿蒙·鸿蒙系统
bestadc20 小时前
入门版 鸿蒙 组件导航 (Navigation)
harmonyos
HarmonyOS_SDK1 天前
几行代码配置高频按钮,保障用户体验一致
harmonyos
邹荣乐1 天前
鸿蒙HarmonyOS开发:多种内置弹窗及自定义弹窗的详细使用指南
harmonyos
别说我什么都不会1 天前
【仓颉三方库】图像处理库 —— gifdrawable4cj
harmonyos
彭不懂赶紧问1 天前
鸿蒙NEXT开发浅进阶到精通04:类似支付宝横向导航栏与list组件联动随航
前端·harmonyos
HarmonyOS小助手1 天前
如何用DevEco Studio的ArkUI Inspector轻松搞定鸿蒙应用UI布局
harmonyos·鸿蒙·deveco studio·harmonyos next·arkui inspector
hellojackjiang20111 天前
全平台开源即时通讯IM框架MobileIMSDK:7端+TCP/UDP/WebSocket协议,鸿蒙NEXT端已发布,5.7K Stars
网络·harmonyos·即时通讯·im开发
特立独行的猫a2 天前
HarmonyOS NEXT 诗词元服务项目开发上架全流程实战(一、项目介绍及实现效果)
华为·harmonyos·元服务·上架
云和数据.ChenGuang2 天前
鸿蒙版电影app设计开发
华为·harmonyos·鸿蒙·鸿蒙系统