rollup build 过程详解

304 阅读5分钟

一些小的知识点

在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

image.png

断点调试

截屏2023-03-07 20.50.38.png

这里就是大概的框架了

由于没有设置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之中

截屏2023-03-07 20.58.24.png

首先会执行插件里面的resolveId hook

截屏2023-03-07 21.01.42.png

由于插件返回的是null,就走默认获取地址的逻辑

截屏2023-03-07 21.08.10.png

之后就是获取module了

创建module对象后,就会进入addModuleSource获取文件内容

截屏2023-03-07 21.11.56.png

读文件会进入一个队列,可以看到最大并发数为20

截屏2023-03-07 21.12.54.png

load

然后进入插件的load,由于返回null,走默认逻辑,直接自己读文件

await promises.readFile(id, 'utf8'))

截屏2023-03-07 21.19.51.png

transform

截屏2023-03-07 21.24.26.png

首先进入插件的transform hook

截屏2023-03-07 21.26.46.png

由于返回的是null,源代码没有做改动,不然的话,code就取transform处理过后的值

如果transform有处理过ast的话,module.ast也会被赋值

截屏2023-03-07 21.29.53.png

因为没有返回ast,需要rollup自己去创建ast

截屏2023-03-07 21.40.01.png

截屏2023-03-07 21.41.41.png

截屏2023-03-07 21.43.26.png

生成了两个节点,一个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就是模块内的作用域

截屏2023-03-08 20.57.15.png

namespace就是封装了一个带命名空间的模块

截屏2023-03-08 20.59.45.png

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之中

截屏2023-03-08 21.22.13.png

此时type变为ImportSpecifier

class ImportSpecifier extends NodeBase {
    applyDeoptimizations() { }
}

又进入到NodeBase

截屏2023-03-08 21.26.00.png

此时会处理Identifier

class Identifier extends NodeBase {
    constructor() {
        super(...arguments);
        this.variable = null;
        this.isTDZAccess = null;
    }
}

截屏2023-03-08 21.29.47.png

到这里ImportSpecifier的递归拷贝就结束了

开始处理ImportDeclaration.source的递归

source.Literal = 'Literal'

class Literal extends NodeBase {
    // ……
    initialise() {
        this.members = getLiteralMembersForValue(this.value);
    }
}

这里有我们要收集的依赖————module-a.js

截屏2023-03-08 21.40.49.png

这次终于进入到initialise中了

截屏2023-03-08 21.44.05.png

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

截屏2023-03-08 21.47.32.png

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
        });
    }
}

截屏2023-03-08 21.52.37.png

Program.sourcesWithAssertions收集了依赖 module-a.js

截屏2023-03-08 22.00.41.png

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

截屏2023-03-08 22.26.43.png

这个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原生不支持打包第三方包的原因