一些小的知识点
在package.json中需要"type": "module" 不然就需要给文件后缀做区分 .mjs是ESM .cjs是cmj
只import 不使用 是不会three-shaking掉的
输入只能是ESM,需要plugin-commonjs将cmj转为ESM
// 源文件
import { merge } from "lodash";
// cmj生成产物
require('lodash');
// es生成产物
import 'lodash';
不支持打包第三方包,和配置external效果相同,需要plugin-node-resolve,这个插件会加载包的内容
由于lodash是cmj,导入前需要转为esm,要plugin-commonjs
插件体系
和webpack有很大的不同,webpack的插件体系是基于tapable,生成compiler的时候,调用插件对象的apply,在各个hook注册方法,然后等webpack调用hook的时候就执行了这些回调
rollup的插件是直接返回带有钩子函数的一个对象
钩子大部分都类似
Async & Sync 对应tapable Async & Sync
Parallel 对应tapable Parallel 并行
Sequential 应tapable waterfall 串行,下一个函数参数是上一个函数的处理结果
First 应tapable SeriesBail 串行熔断
比如rollup的load就是 async first,和webpack相比简单了很多,入参就是这个文件的路径,有返回结果的话就熔断,不用像webpack一样还有loader排序
环境配置
// build.js
const rollup = require("rollup");
// 常用 inputOptions 配置
const inputOptions = {
input: "./src/index.js",
external: [],
plugins:[
{
namne: 'my-plugin',
resolveId (importee, importer, resolveOptions) {
return null
},
load (id) {
debugger
return null
},
transform (code, id) {
debugger
return null
},
renderChunk(code, chunk) {
debugger
return null
},
generateBundle(output, bundle) {
debugger
return null
},
}
]
};
bundle = rollup.rollup(inputOptions);
// index.js
import { a } from './module-a.js'
console.log(a)
// module-a.js
export const a = 1
断点调试
这里就是大概的框架了
由于没有设置buildStart hook
直接进入graph.build
async build() {
timeStart('generate module graph', 2);
await this.generateModuleGraph();
timeEnd('generate module graph', 2);
timeStart('sort and bind modules', 2);
this.phase = BuildPhase.ANALYSE;
this.sortModules();
timeEnd('sort and bind modules', 2);
timeStart('mark included statements', 2);
this.includeStatements();
timeEnd('mark included statements', 2);
this.phase = BuildPhase.GENERATE;
}
resolveId
首先是解析入口,进入到resolveId之中
首先会执行插件里面的resolveId hook
由于插件返回的是null,就走默认获取地址的逻辑
之后就是获取module了
创建module对象后,就会进入addModuleSource获取文件内容
读文件会进入一个队列,可以看到最大并发数为20
load
然后进入插件的load,由于返回null,走默认逻辑,直接自己读文件
await promises.readFile(id, 'utf8'))
transform
首先进入插件的transform hook
由于返回的是null,源代码没有做改动,不然的话,code就取transform处理过后的值
如果transform有处理过ast的话,module.ast也会被赋值
因为没有返回ast,需要rollup自己去创建ast
生成了两个节点,一个import导入,一个表达式
不管是自己生成的ast还是transform生成的,都会进行下面这三步
this.scope = new ModuleScope(this.graph.scope, this.astContext);
this.namespace = new NamespaceVariable(this.astContext);
this.ast = new Program(moduleAst, { context: this.astContext, type: 'Module' }, this.scope);
Scope就是模块内的作用域
namespace就是封装了一个带命名空间的模块
new Program会构造一份新的ast
Program依赖收集
在处理ast生成Program的时候
Program extends NodeBase
class NodeBase extends ExpressionEntity {
constructor(esTreeNode, parent, parentScope, keepEsTreeNode = false) {
super();
/**
* Nodes can apply custom deoptimizations once they become part of the
* executed code. To do this, they must initialize this as false, implement
* applyDeoptimizations and call this from include and hasEffects if they have
* custom handlers
*/
this.deoptimized = false;
// Nodes can opt-in to keep the AST if needed during the build pipeline.
// Avoid true when possible as large AST takes up memory.
this.esTreeNode = keepEsTreeNode ? esTreeNode : null;
this.keys = keys[esTreeNode.type] || getAndCreateKeys(esTreeNode);
this.parent = parent;
this.context = parent.context;
this.createScope(parentScope);
this.parseNode(esTreeNode);
this.initialise();
this.context.magicString.addSourcemapLocation(this.start);
this.context.magicString.addSourcemapLocation(this.end);
}
}
在this.parseNode(esTreeNode)
这里进入赋值
第一个node节点为ImportDeclaration
就进到入ImportDeclaration的构造类之中
class ImportDeclaration extends NodeBase {
// Do not bind specifiers or assertions
bind() { }
hasEffects() {
return false;
}
initialise() {
this.context.addImport(this);
}
render(code, _options, nodeRenderOptions) {
code.remove(nodeRenderOptions.start, nodeRenderOptions.end);
}
applyDeoptimizations() { }
}
ImportDeclaration.prototype.needsBoundaries = true;
注意看,ImportDeclaration extends NodeBase
,说明又回到了NodeBase之中
此时type变为ImportSpecifier
class ImportSpecifier extends NodeBase {
applyDeoptimizations() { }
}
又进入到NodeBase
此时会处理Identifier
class Identifier extends NodeBase {
constructor() {
super(...arguments);
this.variable = null;
this.isTDZAccess = null;
}
}
到这里ImportSpecifier
的递归拷贝就结束了
开始处理ImportDeclaration.source的递归
source.Literal = 'Literal'
class Literal extends NodeBase {
// ……
initialise() {
this.members = getLiteralMembersForValue(this.value);
}
}
这里有我们要收集的依赖————module-a.js
这次终于进入到initialise
中了
function getLiteralMembersForValue(value) {
if (value instanceof RegExp) {
return literalRegExpMembers;
}
switch (typeof value) {
case 'boolean': {
return literalBooleanMembers;
}
case 'number': {
return literalNumberMembers;
}
case 'string': {
return literalStringMembers;
}
}
return Object.create(null);
}
返回的是字符串的预设literalStringMembers
source也拷贝完毕后,进入到ImportDeclaration.initialise
initialise() {
this.context.addImport(this);// 注意这里绑定了this,是Program
}
addImport(node) {
const source = node.source.value;
this.addSource(source, node);
for (const specifier of node.specifiers) {
const name = specifier instanceof ImportDefaultSpecifier
? 'default'
: specifier instanceof ImportNamespaceSpecifier
? '*'
: specifier.imported instanceof Identifier
? specifier.imported.name
: specifier.imported.value;
this.importDescriptions.set(specifier.local.name, {
module: null,
name,
source,
start: specifier.start
});
}
}
给Program.sourcesWithAssertions
收集了依赖 module-a.js
Program.importDescriptions
也收集了依赖
ImportDeclaration
收集完后轮到第二个node ExpressionStatement
和前面也差不多
const loadPromise = this.addModuleSource(id, importer, module).then(() => [
this.getResolveStaticDependencyPromises(module),
this.getResolveDynamicImportPromises(module),
loadAndResolveDependenciesPromise
]);
依赖收集完后就需要加载依赖的module
进入到getResolveStaticDependencyPromises
getResolveStaticDependencyPromises(module) {
// eslint-disable-next-line unicorn/prefer-spread
return Array.from(module.sourcesWithAssertions, async ([source, assertions]) => [
source,
(module.resolvedIds[source] =
module.resolvedIds[source] ||
this.handleInvalidResolvedId(await this.resolveId(source, module.id, EMPTY_OBJECT, false, assertions), source, module.id, assertions))
]);
}
这里会过滤掉配置里external设置过的module
fetchResolvedDependency(source, importer, resolvedId) {
if (resolvedId.external) {
const { assertions, external, id, moduleSideEffects, meta } = resolvedId;
let externalModule = this.modulesById.get(id);
if (!externalModule) {
externalModule = new ExternalModule(this.options, id, moduleSideEffects, meta, external !== 'absolute' && isAbsolute(id), assertions);
this.modulesById.set(id, externalModule);
}
else if (!(externalModule instanceof ExternalModule)) {
return error(errorInternalIdCannotBeExternal(source, importer));
}
else if (doAssertionsDiffer(externalModule.info.assertions, assertions)) {
this.options.onwarn(errorInconsistentImportAssertions(externalModule.info.assertions, assertions, source, importer));
}
return Promise.resolve(externalModule);
}
return this.fetchModule(resolvedId, importer, false, false);
}
又会递归地执行fetchModule
异步的import这里就不展开了
async function waitForDependencyResolution(loadPromise) {
const [resolveStaticDependencyPromises, resolveDynamicImportPromises] = await loadPromise;
return Promise.all([...resolveStaticDependencyPromises, ...resolveDynamicImportPromises]);
}
等index.js解析好后,会执行moduleParsed
hook
这个hook触发的时机是上一个module处理完成的时候
async fetchStaticDependencies(module, resolveStaticDependencyPromises) {
for (const dependency of await Promise.all(resolveStaticDependencyPromises.map(resolveStaticDependencyPromise => resolveStaticDependencyPromise.then(([source, resolvedId]) => this.fetchResolvedDependency(source, module.id, resolvedId))))) {
module.dependencies.add(dependency);
dependency.importers.push(module.id);
}
if (!this.options.treeshake || module.info.moduleSideEffects === 'no-treeshake') {
for (const dependency of module.dependencies) {
if (dependency instanceof Module) {
dependency.importedFromNotTreeshaken = true;
}
}
}
}
这里就是收集的地方了
index.js.dependencies.add(module-a.js)
module-a.js.importers.push(index.js)
生成Graph
回到初始的generateModuleGraph
async generateModuleGraph() {
({ entryModules: this.entryModules, implicitEntryModules: this.implicitEntryModules } =
await this.moduleLoader.addEntryModules(normalizeEntryModules(this.options.input), true));
if (this.entryModules.length === 0) {
throw new Error('You must supply options.input to rollup');
}
for (const module of this.modulesById.values()) {
if (module instanceof Module) {
this.modules.push(module);
}
else {
this.externalModules.push(module);
}
}
}
这里有个判断,只有rollup生成的Module才能装入this.modules
这就是rollup原生不支持打包第三方包的原因