学习资料:拉勾课程《大前端高薪训练营》
阅读建议:搭配文章的侧边栏目录进行食用,体验会更佳哦。
内容说明:本文不做知识点的搬运工,技术详情请查看官方文档。

一:认识rollup

rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。
它是一个小而美的JavaScript打包工具,与webpack适用于打包应用相比,rollup更加适用于类库的打包,其运行机制比较简单,如下图所示:

接下来我们先看一个简单示例来帮助我们更好地理解rollup这个打包工具的功能作用以及使用场景,而后学习一些rollup的一些常用配置和插件,最后尝试实现一个简单的JavaScript打包工具进而学习rollup原理。

二:简单示例

需求:通过rollup打包工具,把用ES6模块化规范编写的多个模块文件代码合并各种模块化规范下的一个模块文件中。

下面用函数式编程思想,从输入、打包、输出这三个方面来叙述这个简单示例的打包过程逻辑。

1. 打包输入(input)

  • module.js:被引入的模块,涉及模块的导出操作
const message = 'hello rollup';const noUsedVar = 'this is a no used variable';export const testFn = () => {console.log(message)
}
  • index.js:主入口,涉及模块的导入导出操作
import { testFn } from './module1'const noUsedFn = () => {console.log('this is a no used function')
}testFn()export default {}

2.打包(action)

  • 安装rollup
yarn global add rollup
  • 配置:rollup.config.js

这里打包成 iife 以及几种我们常见的模块化规范。

export default {input: 'src/index.js',output: [{file: 'dist/bundle.iife.js',name: 'indexBundle',format: 'iife'},{file: 'dist/bundle.cjs.js',format: 'cjs'},{file: 'dist/bundle.amd.js',format: 'amd'},{file: 'dist/bundle.es.js',format: 'es'},{file: 'dist/bundle.umd.js',name: 'indexBundle',format: 'umd'},],
}
  • 读取配置文件执行打包
rollup -c rollup.config.js

3.打包结果(output)

  • bundle.iife.js
var indexBundle = (function () {'use strict';const message = 'hello rollup';const testFn = () => {console.log(message);};testFn();var index = {};return index;}());
  • bundle.cjs.js
'use strict';const message = 'hello rollup';const testFn = () => {console.log(message);
};testFn();var index = {};module.exports = index;
  • 为了避免代码篇幅过长,其它三个规范的代码输出就不再粘贴了。

通过这个简单示例,我们就可以很好的理解rollup 是一个 JavaScript 模块打包器的概念,以及它可以实现将小块代码编译成大块复杂代码的功能。

除此之外很重要的几点是:

  • 它可以接收多种符合模块化规范的模块输入打包输出成符合各种模块化规范的模块输出
  • 在打包的过程中,会自动tree-shaking去掉没有引用的变量键及其值。
  • 打包输出结果更扁平,代码依然可读。

再通过上述叙述以及简单示例认识了rollup之外,接下来我们简单探讨一下rollup的打包原理而后总结一些它的常见需求及其配置实现。

三:rollup打包原理

从本质上来说,rollup只是一个文件内容的转换脚本。如果要实现将多个ES模块打包为一个ES模块的需求(最简单的ES6模块打包),那么个人觉得应该重点关注两件事情,一个是模块聚合,一个模块tree-shaking

对于模块聚合,个人刚开始的思路是通过正则的方式来实现,也即使用正则 /(?:import).+?from[\s]*([’"])[^\1]+?\1/g 来匹配模块中的import语句,用正则 /([’"])([^\1]+?)\1/ 来提取出import语句中的相对路径,这样可以提取出完整的import语句信息。然后读取被导入模块的内容,通过递归就可以读取解析依赖的所有模块。这样就可以实现模块聚合(字符串层面,非语法层面)。

但是上述把代码作为字符串来做正则匹配的方式会有很多问题,比如说聚合后,多个命名空间变为了一个,肯定会出现大片的程序错误。显而易见,简单的通过把代码视为字符串并通过正则匹配来实现模块聚合的方式不可取。

而对于模块的tree-shaking,正则的方式就更无从实现了,因为tree-shaking的实现必然要依赖于代码的上下文。

通过网上搜索资料以及查看rollup源码发现,rollup是通过使用acorn这个库把JavaScript代码解析为抽象语法树ast的方式来实现的模块聚合和tree-shaking。

那么在用ast和acorn的路线实现个人的rollup打包工具之前,有必要先简单探讨一下抽象语法树ast和JavaScript抽象语法树的解析库acorn。

1.ast(Abstract Syntax Tree):抽象语法树

行文参考资料:

  • 文档:我特么的居然没找到ast及其node的权威文档,有知道的观众老爷麻烦告知,谢谢
  • 博客:https://www.cnblogs.com/qinmengjiao123-123/p/8648488.html

(1): 认识抽象语法树

在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。

Javascript的语法是为了给开发者更好的编程而设计的,但是不适合程序的理解。所以需要转化为AST来更适合程序分析,浏览器编译器一般会把源码转化为AST来进行进一步的分析等其他操作。

比如说有一段JavaScript代码:

import module from './module'const a = module.a;function fn(arg) {console.log(arg)
}fn(a)export default {}

通过AST Explorer这个抽象语法树可视化网站解析上述代码后,可以得到其抽象语法树的结构是这样的(折叠后):

这里可以看到,我们的代码从上往下有序的被解析为了一个个node节点,这些node节点各有类型,比如:

  • import语句对应于ImportDeclaration类型的node
  • var、const、let等变量声明语句对应于VariableDeclaration类型的node
  • function函数声明语句对应于FunctionDeclaration类型的node
  • 函数调用等表达式对应于ExpressionStatement类型的node
  • export语句对应于ExportDefaultDeclaration类型的node

每个节点的内部内部也会被解析,直到解析到底为止。这样,整个代码在解析过后,会得到一颗与代码的编码顺序有关的抽象语法树以便于程序识别造轮子的利器啊)。

具体的细节,本文不再展开讨论。在了解了抽象语法树的概念及其可以解析代码以便程序理解的作用之后,我们接下来叙述一些抽象语法树的常用用途。

(2): 抽象语法树用途

用于代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
  • 如JSLint、JSHint对代码错误或风格的检查,发现一些潜在的错误
  • IDE的错误提示、格式化、高亮、自动补全等等
代码混淆压缩
  • UglifyJS2等
优化变更代码,改变代码结构使达到想要的结构
  • 代码打包工具webpack、rollup等等
  • CommonJS、AMD、CMD、UMD等代码规范之间的转化
  • CoffeeScript、TypeScript、JSX等转化为原生Javascript

2.acorn

在造轮子的过程中,如果我们希望得到一份JavaScript代码的抽象语法树,我们就会需要一个JavaScript语言的语法解析工具,比较成熟的JavaScript语法解析库有以下几种:

  • esprima
  • traceur
  • acorn
  • shift

由于rollup是使用的acorn做的语法解析,那么我们接下来就简单探讨一下acorn的使用。

在yarn add acorn 安装好acorn这个库后,以下是基本使用案例(生成的语法树结构与上面所述一致,此处就不贴出来了,想查看可以把代码粘贴到AST Explorer来查看语法树结构):

  • 案例1:JavaScript非模块代码解析
const acorn = require("acorn");
const util = require("util");    // 用于使console.log打印对象时子对象不折叠const code = `
function add (a, b) {return a + b
}
`;
const ast = acorn.parse(code, {ecmaVersion: 2020});
console.log(util.inspect(ast, {showHidden: false, depth: null}));
  • 案例2:JavaScript模块代码解析
const acorn = require("acorn");
const util = require("util");    // 用于替代console.log,打印对象不会折叠
const fs = require("fs");const code = fs.readFileSync(__dirname + '/test2.js', {encoding: 'utf8'});
const ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: "module" }); // 代码类型为模块
console.log(util.inspect(ast, {showHidden: false, depth: null}));

3.rollup打包原理

在认识了ast以及acorn之后,我们就可以以解析抽象语法树的方式来实现将多个ES模块打包为一个ES模块的需求了,实现过程如下(始终关注模块聚合与模块tree-shaking两点):

准备被导入的模块:module1.js
const usedData = "module1: this is a used data"
const noUsedData = "module1: this is a no used data"export default  {usedData
}
准备主(入口)模块:index.js
import moduleData from './module1'const indexUsedData = "index: this is a used data"
const indexNoUsedData = "index: this is a no used data"const testFn = () => {console.log(indexUsedData)
};testFn();export default {"xxx": 'ooo'
}
最简化、定制版的打包:myRollup.js
const acorn = require("acorn");
const fs = require("fs");// 1.读取入口
const indexCodeStr = fs.readFileSync(__dirname + '/index.js', {encoding: 'utf8'
})// 2.解析入口
const getModuleInfo = (codeStr) => {// 当前模块下的所有标识符,标识符作为键const importInfo = {}const moduleVarInfo = {}// 当前模块使用到的所有标识符const usedExpOrVarArr = []// 入口暴露的字符串需保留const exportInfo = {}let exportStr = ''const ast = acorn.parse(codeStr, {ecmaVersion: 2020,sourceType: "module" // 代码类型为模块});ast.body.forEach(node => {switch (node.type) {case "ImportDeclaration":// 解析模块导入:import语句const localName = node.specifiers[0].local.nameimportInfo[localName] = node.source.valuebreakcase "VariableDeclaration":const varInfo = node.declarations[0]moduleVarInfo[varInfo.id.name] = nodebreakcase "ExpressionStatement":const fnName = node.expression.callee.nameusedExpOrVarArr.push({type: 'varNode',name: fnName})const fnNode = moduleVarInfo[fnName]const fnIncludedKey = fnNode.declarations[0].init.body.body[0].expression.arguments[0].nameusedExpOrVarArr.unshift({type: 'varNode',name: fnIncludedKey})usedExpOrVarArr.push({type: 'express',data: codeStr.slice(node.start, node.end)})breakcase "ExportDefaultDeclaration":// 解析模块导出:Export语句const property = node.declaration.properties[0]const exportKey = property.key.nameconst exportTar = moduleVarInfo[property.value.name]exportInfo[exportKey] = exportTarusedExpOrVarArr.push({type: 'varNode',name: exportKey})exportStr = codeStr.slice(node.start, node.end)break}});return {importInfo,moduleVarInfo,exportInfo,exportStr,usedExpOrVarArr}
}const indexModuleInfo = getModuleInfo(indexCodeStr)// 3.准备输出
let writeStr = ''
const {moduleVarInfo: indexModuleVarInfo,importInfo: indexImportInfo,exportStr: indexExportStr
} = indexModuleInfoindexModuleInfo.usedExpOrVarArr.forEach((usedExpOrVar) => {if (usedExpOrVar.type === 'express') {const expressStr = usedExpOrVar.datawriteStr += expressStr + '\r\n'}  else if (usedExpOrVar.type === 'varNode') {const usedVarName = usedExpOrVar.nameif (usedVarName in indexModuleVarInfo) {const curUsedVarNode = (indexModuleVarInfo[usedVarName])writeStr += indexCodeStr.slice(curUsedVarNode.start, curUsedVarNode.end) + '\r\n'} else if (usedVarName in indexImportInfo) {const importModuleCodeStr = fs.readFileSync(__dirname + importInfo[usedVarName], {encoding: 'utf8'})const curUsedVarNode = getModuleInfo(importModuleCodeStr).indexImportInfo[usedVarName] // 假设它没有再importwriteStr += curUsedVarNode.slice(curUsedVarNode.start, curUsedVarNode.end) + '\r\n'}}})writeStr += '\r\n' + indexExportStr
fs.writeFileSync(__dirname + '/bundle.js', writeStr)

哈哈,已经精简我都不好意思分析了。总的来说,思路就是如下:

  • 一个文件就是一个模块,一个 AST 语法抽象树
  • 解析模块时,找到并按顺序注册该模块的所有标识符(导入的标识符、本模块内定义的标识符)放入importInfo和moduleVarInfo中。
  • 解析模块时,碰到表达式就直接把表达式字符串放入usedExpOrVarArr中,如果是函数表达式就进入解析,把表达式用到的所有标识符以及当前表达式按先后顺序放入usedExpOrVarArr中。
  • 解析usedExpOrVarArr,按顺序拼接表达式用到的所有标识符的定义字符串、表达式,最后再加入入口模块的暴露字符串exportStr。
  • 输出上述拼接后的字符串
打包后的结果
  • 打包结果:使用rollup.js
const indexUsedData = "index: this is a used data";const testFn = () => {console.log(indexUsedData);
};testFn();var index = {"xxx": 'ooo'
};export default index;
  • 打包结果:使用myRollup.js
const indexUsedData = "index: this is a used data"
const testFn = () => {console.log(indexUsedData)
};
testFn();export default {"xxx": 'ooo'
}

如此可以看到,模块聚合以及tree-shaking都已经实现了。这里再简单提一提关键的两个问题。一个是模块聚合的多个命名空间合并为一个的问题,简单起见,此处案例没有考虑。而对于rollup的tree-shaking原理,个人理解,它是因为rollup在解析代码的抽象语法树后,并不会把所有的标识符的定义字符串都打包输出,而是只把表达式中用到的标识符的定义字符串打包输出,没用到的自然而然就被过滤掉了。

在简单学习rollup打包原理之后,接下来我们了解一些我们使用rollup时的常见需求及其配置。

四:常见需求及其配置

1.入口与输出

在使用rollup打包的过程中,对于打包入口与打包输出,我们通常会有以下四种形式:

类型 配置方式
单输入单输出 export default: obj, output: obj
单输入多输出 export default: obj, output: arr
多输入单输出 export default: arr, output: obj
多输入多输出 export default: arr, output: arr

对于配置中的具体表现形式,可以看如下配置示例:

// 多输入多输出
export default [{input: 'src/index1.js',output: [{file: 'dist/bundle.iife.js',format: 'iife'}, {file: 'dist/bundle.cjs.js',format: 'cjs'}]
}, {input: 'src/index2.js',output: [{file: 'dist/bundle.iife.js',format: 'iife'}, {file: 'dist/bundle.cjs.js',format: 'cjs'}]
}, ]

2.常用Plugin

打包本质上是个转换过程,这个过程rollup通过插件机制(钩子思想)让我们能够访问并操作这个转换切面,官方插件列表rollup plugins,其中最常见的插件有如下几种:

需求 插件名
Babel转换 @rollup/plugin-babel
支持打包json模块 @rollup/plugin-json
支持打包cjs模块 @rollup/plugin-commonjs
支持打包node_module中的模块 @rollup/plugin-node-resolve
打包结果压缩 @rollup/plugin-terser

对于插件的使用,下文是一个插件使用示例:

  • 配置文件
import json from '@rollup/plugin-json'
import resolve from '@rollup/plugin-node-resolve'    // 帮助寻找node_modules里的包
import commonjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'
import {terser} from '@rollup/plugin-terser'export default {input: 'src/index.js',external: ['lodash']// 外部依赖,不打包output: {file: 'dist/bundle.js',format: 'iife',name: 'index',// plugins: [terser()]// 压缩},plugins: [json(),resolve(),commonjs(),babel({exclude: '**/node_modules/**'})],
}
  • 代码引用
import { name, version } from '../package.json'
import _ from 'lodash'
import cjs from './cjs-module'console.log(name, version)
console.log(_.add(1 + 1))
console.log(cjs.msg)
  • 打包结果(未压缩、lodash作为外部依赖)
(function (_) {'use strict';function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }var ___default = /*#__PURE__*/_interopDefaultLegacy(_);var name = "00-mine";var version = "1.0.0";var cjsModule = {msg: 'i am a cjs module'};console.log(name, version);console.log(___default['default'].add(1 + 1));console.log(cjsModule.msg);}(_));

3.代码分割

代码分割可以实现代码模块的按需加载和懒加载,提高构建以及文件加载速度。通常来说会有两种方式来实现代码分割,即:

  • 多入口打包
  • 动态导入

多入口打包通常是从大业务上进行代码分割,其配置实现通过上述第一点中所述,以多入口多输出方式配置即可。

而动态导入则通常可以用于在小业务以及代码逻辑上进行代码分割,其逻辑和实现也很简单,示例代码如下(index.js):

if (fileNotExist(filePath)) {import('fileUtils').then(({ createFile }) => {createFile(filePath)})
}

本文结束,谢谢观看。
如若认可,点赞收藏。

JavaScript模块打包器rollup相关推荐

  1. webpack 模块打包器

    webpack的入门概念 概念 webpack是现代Javascript应用程序的模块打包器.当webpack处理程序时,它会递归地构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将这些模块打 ...

  2. 前端组件/库打包利器rollup使用与配置实战

    目前主流的前端框架vue和react都采用rollup来打包,为了探索rollup的奥妙,接下来就让我们一步步来探索,并基于rollup搭建一个库打包脚手架,来发布自己的库和组件. 前言 写rollu ...

  3. 导出库的版本_了解 JavaScript 模块系统基础知识,搭建自己的库

    我想很多"前端工程师"都听过说过 "JavaScript 模块",那你们都知道如何处理它,以及它在日常工作中如何发挥作用吗? JS 模块系统到底是什么呢 随着 ...

  4. python模块捆绑器组件_让我们学习模块捆绑器如何工作,然后自己编写

    python模块捆绑器组件 by Adam Kelly 通过亚当凯利 让我们学习模块捆绑器如何工作,然后自己编写 (Let's learn how module bundlers work and t ...

  5. 指定模块打包命令_大前端进阶之Babel、模块化、webpack

    Babel 什么是Babel? 很多ES6高级语法浏览器是不支持的,Node.js也不一定能够运行,这时就需要使用转码器了. Babel是一个使用非常广泛的转码器,它可以将ES6语法代码转换为ES5语 ...

  6. 【模块打包工具】Webpack

    Webpack的出现 使得 前端模块化的范围扩大了许多 以前 只是 JS 模块化 而现在 是 前端模块化 包括 样式 图片 字体等所有的资源模块化 核心特性 1.可以通过webpack 模块打包器 b ...

  7. Javascript模块加载捆绑器Browserify Webpack和SystemJS用法

    Javascript模块加载捆绑器Browserify Webpack和SystemJS用法 转自 http://www.jdon.com/idea/js/javascript-module-load ...

  8. javascript模块_JavaScript模块第2部分:模块捆绑

    javascript模块 by Preethi Kasireddy 通过Preethi Kasireddy JavaScript模块第2部分:模块捆绑 (JavaScript Modules Part ...

  9. 为何webpack风靡全球?三大主流模块打包工具对比

    小编说:前端项目日益复杂,构建系统已经成为开发过程中不可或缺的一个部分,而模块打包(module bundler)正是前端构建系统的核心.Webpack能成为最流行的打包解决方案,并不是偶然.webp ...

最新文章

  1. 让英文版windows 8支持非Unicode程序的语言方法
  2. 算法----------字符串的排列(Java版本)
  3. MAT之GRNN/PNN:基于GRNN、PNN两神经网络实现并比较鸢尾花(iris数据集)种类识别正确率、各个模型运行时间对比
  4. 前端学HTTP之字符集
  5. java多状态机_一个java状态机样例的代码
  6. flask get和post
  7. 解决ssh登录过慢问题
  8. Python3入门机器学习经典算法与应用 第3章 Numpy中的arg运算
  9. vs2019 无法打开包括文件:“SDKDDKVer.h”: No such file or directory的另外一种解决思路
  10. 禁止用鼠标拖动窗口的大小 - 回复 合肥的石头 的问题
  11. C#中Action和=>用法(入门)
  12. 用jQuery实现9宫格抽奖
  13. Java篇,小米java校招面试
  14. javaSE简单介绍
  15. 计算机竞赛进省队可以保送吗,厉害!物理竞赛8名学子入选省队!信息学竞赛5人获清北保送资格,他们来自……...
  16. vSphere 6.7 U3部署win11
  17. facebook 照片存储系统haystack的学习
  18. 数据结构基础之图(上):图的基本概念
  19. Wing IDE 6.0 算号器注册机代码
  20. js混淆算法 java_JAVA动态混淆JS

热门文章

  1. python出现invalid argument什么意思_python程序运行后提示IOError: [Errno 22] Invalid argument 急啊!!!!...
  2. Win7+opencv3.30+vs2015提示无法打开XXX.lib
  3. 《Kettle构建Hadoop ETL系统实践》大数据ETL开发工具选择Kettle的理由
  4. 表单引擎之表单组件详细说明
  5. 【Linux/shell】bash命令和sh命令的区别(20210109)
  6. 免费数据上新 | CnOpenData中国上市公司信息披露评分数据
  7. 某医药公司北亚数据恢复报告书
  8. 采购原料的流程是什么  分享原料采购流程图模板
  9. 爬取安居客租房信息,主要是获取电话号码
  10. 一文解决中文在Eclipse中显示乱码的问题