1. rollup基本使用
import babel from "@rollup/plugin-babel";
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import {terser} from 'rollup-plugin-terser';
import postcss from 'rollup-plugin-postcss';
import serve from 'rollup-plugin-serve';
export default {
input: "src/main.js",
output: {
file: "dist/rollup_bundle_1.js",
format: "es", // 很多中模式 amd/es/iife/umd/cjs
// name: 'bundleName' // iife的时候需要提供
},
plugins: [
babel({
babelHelpers: "bundled",
exclude: "node_modules/**",
}),
resolve(),
commonjs(),
typescript(),
terser(),
postcss(),
serve({})
],
};
复制代码
2. 前置知识
- magic-string(操作字符串 生成sourcemap)
- acorn(JavaScript Parser ast语法树 transform generate)
- scope(作用域 作用域链)
3. rollup基本流程
function rollup(entry, filename) {
// 从入口出发 生成一个 bundle
const bundle = new Bundle({ entry });
// build生成code和sourceMap
const {code} = bundle.build(filename);
// 写入到文件中
fs.writeFileSync(filename, code);
}
复制代码
4. 合并模块代码
- 我们先写个demo
//index.js
import { name, age } from './msg'
let city = 'sz'
var sex = 'boy'
console.log(city, name)
// msg.js
export const name = 'name'
export const age = 28
复制代码
- 合并代码
// bundle.js 我们先简单的把代码合并到一起
// 将 import { name, age } from './msg' 替换掉
const name = 'name'
const age = 28
let city = 'sz'
var sex = 'boy'
console.log(city, name)
复制代码
- 简单实现模块合并
// rollup
function rollup() {
const bundle = new Bundle({ entry });
bundle.build(filename);
}
// bundle.js
// 最简单的想法就是 找到 import的节点 然后把代码copy过来
// https://astexplorer.net/ 查看ast节点
class Bundle {
constructor(options) {
// 入口文件数据
this.entryPath = path.resolve(options.entry.replace(/\.js$/, "") + ".js");
}
build(filename) {
let magicString = new MagicString.Bundle();
const code = fs.readFileSync(this.entryPath);
magicString.addSource({
content: code,
separator: "\n",
});
// 将code变成ast语法树 找到 import的 语句 然后将代码 copy过来
// 实际上每个文件都是一个module
this.ast = parse(code, {
ecmaVersion: 7,
sourceType: "module",
});
let sourceList = [];
// 分析语法树 遍历ast找到import的语句 把source中的代码搞过来
this.ast.body.forEach((node) => {
if (node.type === "ImportDeclaration") {
let source = node.source.value; // ./msg
sourceList.push(source);
}
});
// 把 sourceList 中的代码直接拷贝过来
for (let i = 0; i < sourceList.length; i++) {
// 这里是相对路径 要变成绝对路径
let pathName = sourceList[i];
if (!path.isAbsolute(pathName)) {
pathName = path.resolve(
path.dirname(this.entryPath),
pathName.replace(/\.js$/, "") + ".js"
);
}
const code = fs.readFileSync(pathName);
magicString.addSource({
content: code,
separator: "\n",
});
}
fs.writeFileSync(filename, magicString.toString());
}
}
复制代码
- result
// 通过上面的操作 整合了所有的代码
import { name, age } from "./msg";
let city = "sz";
var sex = "boy";
console.log(city, name);
export const name = "name";
export const age = 28;
复制代码
- rollup结果
// 1. 每个文件是一个模块 我们需要一个 module.js
// 2. 将 ast 语法树处理单独出去
// 3. 我们需要去掉 import的语句 export不需要
// 4. 我们要将没有使用到的变量去掉 tree-shaking
const name = "name";
let city = "sz";
console.log(city, name);
复制代码
- 优化下代码
// 基本目录结构
├── ast
│ ├── analyse.js // 分析ast
│ ├── Scope.js // 作用域
│ └── walk.js // 遍历ast语法树
├── bundle.js // Bundle 收集依赖模块,最后把所有代码打包在一起输出
│ └── index.js
├── module.js / /每个文件都是一个模块
├── rollup.js // 打包的入口模块
└── utils.js // 一些辅助函数
复制代码
- bundle.js
// 我们以语句为单位添加到结果中 不在以文件为单位
class Bundle {
constructor(options) {
// 入口文件数据
this.entryPath = path.resolve(options.entry.replace(/\.js$/, "") + ".js");
}
build(filename) {
// 获取入口模块
let entryModule = this.fetchModule(this.entryPath);
// 展开所有的语句 我们一条一条的语句添加 不再以文件的角度来添加了
this.statements = entryModule.expandAllStatements(true);
const { code } = this.generate({});
fs.writeFileSync(filename, code);
}
// 入口模块是index.js 当遇到import的时候有两个参数
fetchModule(importee, importer) {
let route;
if (!importer) route = importee;
else {
if (path.isAbsolute(importee)) route = importee;
// 相对路径 ./msg
else
route = path.resolve(
path.dirname(importer),
importee.replace(/\.js$/, "") + ".js"
);
}
if (route) {
let code = fs.readFileSync(route, "utf8");
const module = new Module({
code,
path: importee,
bundle: this, // bundle的实例
});
return module;
}
}
// 生成代码
generate() {
let magicString = new MagicString.Bundle();
// 遍历语句添加到结果中
this.statements.forEach((statement) => {
const source = statement._source.clone();
if (/^Export/.test(statement.type)) {
if (
statement.type === "ExportNamedDeclaration" &&
statement.declaration.type === "VariableDeclaration"
) {
// 2. 把export干掉
source.remove(statement.start, statement.declaration.start);
}
}
magicString.addSource({
content: source,
separator: "\n",
});
});
return { code: magicString.toString() };
}
}
复制代码
- module.js
class Module {
constructor({ code, path, bundle }) {
this.code = new MagicString(code, { filename: path });
this.path = path;
this.bundle = bundle;
// 得到ast语法树
this.ast = parse(code, {
ecmaVersion: 7,
sourceType: "module",
});
// 分析ast语法树
// 1. 添加一个 _source 属性 执行当前语句的代码
analyse(this.ast, this.code, this);
}
// 展开所有的语句
expandAllStatements() {
let allStatements = [];
// 我们现在是有4条语句的 遍历展开
this.ast.body.forEach((statement) => {
let statements = this.expandStatement(statement);
allStatements.push(...statements);
});
return allStatements;
}
expandStatement(statement) {
// 标记为已经处理了
statement._included = true;
let result = [];
// 我们需要将语句展开 import 我们就要生成新的module
if (statement.type === "ImportDeclaration") {
// 递归创建module 从index.js中导入的 可能是相对也可能是绝对路径
let module = this.bundle.fetchModule(statement.source.value, this.path);
const statements = module.expandAllStatements();
result.push(...statements);
} else {
// 1.我们不需要import的语句
result.push(statement);
}
return result;
}
}
复制代码
- analyse
function analyse(ast, magicString) {
ast.body.forEach((statement) => {
Object.defineProperties(statement, {
_source: { value: magicString.snip(statement.start, statement.end) },
});
});
}
复制代码
- output
// 我们得到模块合并的结果
const name = "name";
const age = 28;
let city = "sz";
var sex = "boy";
console.log(city, name);
复制代码
5. tree-shaking
// 怎么知道变量是否使用了? 有哪些可能是会使用到变量
// 1. 赋值语句 name = xxx name += xx AssignmentExpression
// 2. 函数调用 console 等 CallExpression
// 在我们展开每条语句的时候 判断这些语句中使用了哪些变量
// 而函数声明 FunctionDeclaration 变量的声明 VariableDeclaration 我们都不要
// 排除了之后 我们的定义就没有 需要解决几个问题
// 1. 怎么知道使用了哪些变量
// 分析可能使用到变量的语句 console.log(name, age) 使用的就是name age
// 使用 _dependsOn来保存依赖的变量 (本模块也可能是import的)
// 2. 怎么获取到使用的变量的定义的语句
// 我们使用definitions来保存所有的变量对应的语句
// 3. 变量是本模块定义的还是import引入的
// 我们需要通过作用域来判断 如果在本模块中存在就是本模块的 否则就是import的
// 同时我们使用 imports来保存所有import的变量
复制代码
- 展开语句的时候排除我们不需要的
// ImportDeclaration FunctionDeclaration VariableDeclaration的
// module.js中expandAllStatements展开的时候不处理这些
this.ast.body.forEach((statement) => {
if (statement.type === "ImportDeclaration") return;
if (statement.type === "VariableDeclaration") return;
if (statement.type === "FunctionDeclaration") return;
// 只有console.log(city, name) 会被展开 其他语句都跳过了
let statements = this.expandStatement(statement);
allStatements.push(...statements);
});
// 得到结果 我们的变量定义哪去了
复制代码
- 遍历ast语法树
// 1. 添加几个属性 收集定义的变量 依赖的变量
Object.defineProperties(statement, {
_defines: { value: {} }, // 定义的变量 区分imports
_dependsOn: { value: {} }, // 依赖的变量
_included: { value: false, writable: true }, // 是否已经包含在输出语句中
_source: { value: magicString.snip(statement.start, statement.end) }, // 对应的代码
});
// 2. 构建作用域链 定义的变量 _defines 要支持块级作用域
scope.add(name, isBlockDeclaration)
// 如果是 BlockStatement
new Scope()
// 3. 找到依赖的变量 _dependsOn
// 遍历添加
if (node.type === "Identifier") {
statement._dependsOn[node.name] = true;
}
// 4. 找到本模块中所有定义的变量 (这个是模块的纬度 就是将所有的statement语句加到一起)
// 这样就可以通过变量获取到对应的语句
Object.keys(statement._defines).forEach((name) => {
this.definitions[name] = statement;
});
复制代码
- 展开语句
function expandStatement(statement) {
statement._included = true; // 标记已经添加过了
let result = [];
// 1. 添加依赖的变量对应的语句
// console.log(city, name) 依赖是 [console, log, city, name]
const dependencies = Object.keys(statement._dependsOn);
dependencies.forEach((name) => {
// 找到定义的语句 添加 因为变量可能是import的(我们就需要递归的创建module)
let definition = this.define(name);
result.push(...definition);
});
// 2.将自己添加进入 console.log(city, name) 添加进入
result.push(statement);
return result
}
// 找到变量对应的语句
function define(name) {
if (hasOwnProperty(this.imports, name)) {
// 表明是import的
let module = this.bundle.fetchModule(this.imports[name].source, this.path);
// 到msg模块中去找对应的语句 找到name对应的语句
return module.define(exportDeclaration.localName);
} else { // 本模块中自己的变量
statement = this.definitions[name];
// 找到city对应的语句
return this.expandStatement(statement);
}
}
// 这样我们就简单的实现了 tree-shaking 得到结果
let city = "sz";
const name = "name";
console.log(city, name);
复制代码
6. 处理 AssignmentExpression
// 除了 CallExpression 会访问到变量
// AssignmentExpression 的语句也会访问到变量 name = 123 我们需要将这些语句也添加到结果中
// 修改下msg的代码
export let name = "name";
export const age = 28;
name = "name-";
name += "AssignmentExpression";
// 先分析下ast的结构
复制代码
// 我们需要将修改的语句添加到结果中
// 1. 在遍历语句的时候添加一个 _modifies 属性
Object.defineProperties(statement, {
_modifies:{ value: {} }, // 修改
})
// 2. 在收集 _dependsOn 的时候我们要同时收集 _modifies (分读取和写入)
statement._modifies[node.name] = true;
// 3. 定义一个 modifications 变量来保存模块修改的 this.modifications = {};
// 可能是有多条修改语句的
this.modifications[name].push(statement);
// 4. 在展开语句的时候将修改的语句添加到结果中
function expandStatement(statement) {
// 1. 处理_dependsOn
// 2. 将自己添加到结果中
// 3. 将修改的语句添加到结果中
const defines = Object.keys(statement._defines);
let statements = this.expandStatement(statement);
result.push(...statements);
}
// 得到结果 包含了我们修改的语句
let city = "sz";
city = "city";
let name = "name";
name = "name-";
name += "AssignmentExpression";
console.log(city, name);
复制代码
7. 支持块级作用域
// 修改demo 本身低版本的rollup就没有支持 那这样作用域是不是就没用?
import { name, age } from "./msg";
if (age > 10) {
let block = "block";
console.log(block);
} else if (age > 100) {
console.log(name);
} else {
console.log("test");
}
// 得到结果
{
let block = "block";
console.log(block);
}
// 先看下ast结构 会根据 test的结果 判断是生成哪个 BlockStatement
// 在判断 BlockStatement 中的逻辑
复制代码
// 我们简单点处理 直接修改为
{
let block = "block";
let name = 'name'
console.log(block);
}
// 期望得到结果
{
let block = "block";
console.log(block);
}
// 0.3.1版本实际上没有做处理 得到结果为
{
let block = "block";
let name = "name";
var test = "var";
console.log(block);
}
//# sourceMappingURL=bundle.js.map
复制代码
// 我们简单的处理下var变量的声明 添加到父作用域
// 在遍历节点的时候我们加一个判断
function addToScope(declarator, isBlockDeclaration = false) {
if (!scope.parent || !isBlockDeclaration) {
// 如果是var声明的变量 也添加到里面去
statement._defines[name] = true;
}
}
// 尝试去掉作用域的代码 我们的代码根本就没有用到这个作用域
复制代码
8. 处理变量重名的问题
// 找到变量 如果重复了我们就改一个名字
// 我们修改demo的代码
// index.js 拷贝进来的时候就重名了 我们需要重命名
import { name1 } from "./name1"; // const name = 'name1'
import { name2 } from "./name2"; // const name = 'name2'
import { name3 } from "./name3"; // const name = 'name3'
console.log(name1, name2, name3);
// names1
const name = 'name1'
export const name1 = name
// names2
const name = 'name2'
export const name2 = name
// names3
const name = 'name3'
export const name3 = name
// 得到结果
const name$2 = "name1";
const name1 = name$2;
const name$1 = "name2";
const name2 = name$1;
const name = "name3";
const name3 = name;
console.log(name1, name2, name3); // 原理就是重命名
// 1. 在bundle的build过程得到statements之后处理变量名
this.deConflict();
function deConflict() {
const conflicts = {}; // 命令冲突
conflicts[name] = true // 记录冲突的变量
// 记录对应的模块
defines[name].push(statement._module)
// 遍历 conflicts 重命名
const replacement = name + `$${index + 1}`;
module.rename(name, replacement);
}
// 2. 在statement遍历的时候增加一个 _module属性
Object.defineProperties(statement, {
_module: { value: module }, // 对应的模块
})
// 3. module定义rename方法
this.canonicalNames = {}; // 存放对应关系
function rename(name, replacement) {
this.canonicalNames[name] = replacement;
}
// 4. 在生成代码的过程中我们需要修改ast节点的内容
function generate() {
Object.keys(statement._dependsOn)
.concat(Object.keys(statement._defines))
.forEach((name) => {
const canonicalName = statement._module.getCanonicalName(name);
if (name !== canonicalName) replacements[name] = canonicalName;
});
// 用新名字替换老的名字
replaceIdentifiers(statement, source, replacements);
}
// 5. 替换ast的内容
function replaceIdentifiers(statement, source, replacements) {
walk(statement, {
enter(node) {
if (node.type === "Identifier") {
// 重命名
if (node.name && replacements[node.name]) {
source.overwrite(node.start, node.end, replacements[node.name]);
}
}
},
});
}
// 得到结果
const name = "name1";
const name1 = name;
const name$1 = "name2";
const name2 = name$1;
const name$2 = "name3";
const name3 = name$2;
console.log(name1, name2, name3);
复制代码
9. sourcemap文件
// 我们先还原demo的代码
import { name, age } from "./hello";
let msg = "123";
console.log(age);
function fn() {
console.log(msg);
}
fn();
// rollup打包生成的结果
const age = "age";
let msg = "123";
console.log(age);
function fn() {
console.log(msg);
}
fn();
//# sourceMappingURL=rollup_bundle_map.js.map
// map文件
{
"version": 3,
"file": "rollup_bundle_map.js",
"sources": ["../src/hello.js", "../src/map.js"],
"sourcesContent": ["export let name = \"name\";\nexport const age = \"age\";\nname = \"test\";\nname += 20;\n", "import { name, age } from \"./hello\";\nlet msg = \"123\";\nconsole.log(age);\nfunction fn() {\n console.log(msg);\n}\nfn();\n"],
"names": ["age", "msg", "console", "log", "fn"],
"mappings": "AACO,MAAMA,GAAG,GAAG,KAAZ;;ACAP,IAAIC,GAAG,GAAG,KAAV;AACAC,OAAO,CAACC,GAAR,CAAYH,GAAZ;;AACA,SAASI,EAAT,GAAc;AACZF,EAAAA,OAAO,CAACC,GAAR,CAAYF,GAAZ;AACD;;AACDG,EAAE"
}
// 根据rollup打包生成的结果 我们需要做两个操作
// 1.在源码后面添加上sourceMappingURL的地址 在写入文件写 给code拼接上字符串即可
let SOURCEMAPPING_URL = "sourceMa";
SOURCEMAPPING_URL += "ppingURL";
code += `\n//# ${SOURCEMAPPING_URL}=${path.basename(filename)}.map`;
// 2.生成sourcemap文件
// generate 要返回map的内容
const {code, map} = this.generate({})
fs.writeFileSync(filename + ".map", map.toString());
// 我们直接使用 magicString 的 generateMap 方法生成map文件
map: magicString.generateMap({
includeContent: true,
file: options.dest,
// TODO
}),
// 这里又有一个问题 0.3.1的文件和最新 rollup 生成文件不一致
{
"version": 3,
"file": "bundle_map.js",
"sources": ["../..//Users/xueshuai.liu/Desktop/rollup-study/rollup-0.3.1/main.js", "../..//Users/xueshuai.liu/Desktop/rollup-study/rollup-0.3.1/hello.js"],
"sourcesContent": ["import { name, age } from \"./hello\";\nlet msg = \"123\";\nconsole.log(age);\nfunction fn() {\n console.log(msg);\n}\nfn();\n", "export let name = \"name\";\nexport const age = \"age\";\nname = \"test\";\nname += 20;\n"],
"names": [],
"mappings": "AACA;AAEA;AACA;AACA;ACJO;ADCP;AAIA"
}
复制代码
10. 源码调试
// 参考 https://juejin.cn/post/6898865993289105415
// 版本 0.3.1
├── Bundle
│ └── index.js # 负责打包
├── Module
│ └── index.js # 负责解析模块
├── ast
│ ├── Scope.js # 作用域链
│ ├── analyse.js # 解析ast语法树
│ └── walk.js # 遍历ast语法树
├── finalisers # 输出类型
│ ├── amd.js
│ ├── cjs.js
│ ├── es6.js
│ ├── index.js
│ └── umd.js
├── rollup.js # 入口
└── utils # 工具函数
├── map-helpers.js
├── object.js
├── promise.js
└── replaceIdentifiers.js
复制代码
总结
分析0.3.1版本流程 主要是分析tree-shaking的过程
1. rollup 简单的合并模块的代码 通过分析ast找到import的语句然后将代码直接拷贝过来
2. 实际上文件都是一个模块 通过分析ast可以知道 模块中 import和export的变量 (模块纬度)
模块中每条语句的 依赖项和修改项 (statement纬度的)
3. 从入口文件出发 递归的展开每一条语句 添加到最好输出的结果中
4. 我们如何进行tree-shaking?
1. 我们不处理 import let function等声明语句 当使用的时候我们才加到到语句中
2. 展开的时候将 将依赖的变量对应的语句加入 可能是import的变量 那么就new Module
3. 将自身加入
4. 将修改的语句加入
5. 块级作用域暂时没有支持 我们可以先将var变量加到父级的作用域可以处理 作用域好像用处不大
6. sourcemap文件直接使用magicString的方法生成
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END