• 原来rollup这么简单之插件篇



    大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。




    内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。




    大家的支持是我创作的动力。



    计划


    rollup系列打算一章一章的放出,内容更精简更专一更易于理解


    这是rollup系列的最后一篇文章,以下是所有文章链接。



    TL;DR


    rollup的插件和其他大型框架大同小异,都是提供统一的标准接口,通过约定大于配置定义公共配置,注入当前构建结果相关的属性与方法,供开发者进行增删改查操作。为稳定可持续增长提供了强而有力的铺垫!


    但不想webpack区分loader和plugin,rollup的plugin既可以担任loader的角色,也可以胜任传统plugin的角色。rollup提供的钩子函数是核心,比如load、transform对chunk进行解析更改,resolveFileUrl可以对加载模块进行合法解析,options对配置进行动态更新等等~


    注意点



    所有的注释都在这里,可自行阅读




    !!!提示 => 标有TODO为具体实现细节,会视情况分析。




    !!!注意 => 每一个子标题都是父标题(函数)内部实现




    !!!强调 => rollup中模块(文件)的id就是文件地址,所以类似resolveID这种就是解析文件地址的意思,我们可以返回我们想返回的文件id(也就是地址,相对路径、决定路径)来让rollup加载




    rollup是一个核心,只做最基础的事情,比如提供默认模块(文件)加载机制, 比如打包成不同风格的内容,我们的插件中提供了加载文件路径,解析文件内容(处理ts,sass等)等操作,是一种插拔式的设计,和webpack类似
    插拔式是一种非常灵活且可长期迭代更新的设计,这也是一个中大型框架的核心,人多力量大嘛~



    主要通用模块以及含义



    1. Graph: 全局唯一的图,包含入口以及各种依赖的相互关系,操作方法,缓存等。是rollup的核心

    2. PathTracker: 引用(调用)追踪器

    3. PluginDriver: 插件驱动器,调用插件和提供插件环境上下文等

    4. FileEmitter: 资源操作器

    5. GlobalScope: 全局作用局,相对的还有局部的

    6. ModuleLoader: 模块加载器

    7. NodeBase: ast各语法(ArrayExpression、AwaitExpression等)的构造基类


    插件机制分析


    rollup的插件其实一个普通的函数,函数返回一个对象,该对象包含一些基础属性(如name),和不同阶段的钩子函数,像这个样子:


    function plugin(options = {}) {
    return {
    name: 'rollup-plugin',
    transform() {
    return {
    code: 'code',
    map: { mappings: '' }
    };
    }
    };
    }

    这里是官方建议遵守的约定.


    我们平常书写rollup插件的时候,最关注的就是钩子函数部分了,钩子函数的调用时机有三类:



    1. const chunks = rollup.rollup执行期间的Build Hooks

    2. chunks.generator(write)执行期间的Output Generation Hooks

    3. 监听文件变化并重新执行构建的rollup.watch执行期间的watchChange钩子函数


    除了类别不同,rollup也提供了几种钩子函数的执行方式,每种方式都又分为同步或异步,方便内部使用:



    1. async: 处理promise的异步钩子,也有同步版本

    2. first: 如果多个插件实现了相同的钩子函数,那么会串式执行,从头到尾,但是,如果其中某个的返回值不是null也不是undefined的话,会直接终止掉后续插件。

    3. sequential: 如果多个插件实现了相同的钩子函数,那么会串式执行,按照使用插件的顺序从头到尾执行,如果是异步的,会等待之前处理完毕,在执行下一个插件。

    4. parallel: 同上,不过如果某个插件是异步的,其后的插件不会等待,而是并行执行。


    文字表达比较苍白,咱们看几个实现:



    • 钩子函数: hookFirst
      使用场景:resolveId、resolveAssetUrl等


    function hookFirst<H extends keyof PluginHooks, R = ReturnType<PluginHooks[H]>>(
    hookName: H,
    args: Args<PluginHooks[H]>,
    replaceContext?: ReplaceContext | null,
    skip?: number | null
    ): EnsurePromise<R> {
    // 初始化promise
    let promise: Promise<any> = Promise.resolve();
    // this.plugins在初始化Graph的时候,进行了初始化
    for (let i = 0; i < this.plugins.length; i++) {
    if (skip === i) continue;
    // 覆盖之前的promise,换言之就是串行执行钩子函数
    promise = promise.then((result: any) => {
    // 返回非null或undefined的时候,停止运行,返回结果
    if (result != null) return result;
    // 执行钩子函数
    return this.runHook(hookName, args as any[], i, false, replaceContext);
    });
    }
    // 最后一个promise执行的结果
    return promise;
    }


    • 钩子函数: hookFirstSync
      使用场景:resolveFileUrl、resolveImportMeta等


    // hookFirst的同步版本,也就是并行执行
    function hookFirstSync<H extends keyof PluginHooks, R = ReturnType<PluginHooks[H]>>(
    hookName: H,
    args: Args<PluginHooks[H]>,
    replaceContext?: ReplaceContext
    ): R {
    for (let i = 0; i < this.plugins.length; i++) {
    // runHook的同步版本
    const result = this.runHookSync(hookName, args, i, replaceContext);
    // 返回非null或undefined的时候,停止运行,返回结果
    if (result != null) return result as any;
    }
    // 否则返回null
    return null as any;
    }


    • 钩子函数: hookSeq
      使用场景:onwrite、generateBundle等


    // 和hookFirst的区别就是不能中断
    async function hookSeq<H extends keyof PluginHooks>(
    hookName: H,
    args: Args<PluginHooks[H]>,
    replaceContext?: ReplaceContext
    ): Promise<void> {
    let promise: Promise<void> = Promise.resolve();
    for (let i = 0; i < this.plugins.length; i++)
    promise = promise.then(() =>
    this.runHook<void>(hookName, args as any[], i, false, replaceContext)
    );
    return promise;
    }


    • 钩子函数: hookParallel
      使用场景:buildStart、buildEnd、renderStart等


    // 同步进行,利用的Promise.all
    function hookParallel<H extends keyof PluginHooks>(
    hookName: H,
    args: Args<PluginHooks[H]>,
    replaceContext?: ReplaceContext
    ): Promise<void> {
    // 创建promise.all容器
    const promises: Promise<void>[] = [];
    // 遍历每一个plugin
    for (let i = 0; i < this.plugins.length; i++) {
    // 执行hook返回promise
    const hookPromise = this.runHook<void>(hookName, args as any[], i, false, replaceContext);
    // 如果没有那么不push
    if (!hookPromise) continue;
    promises.push(hookPromise);
    }
    // 返回promise
    return Promise.all(promises).then(() => {});
    }


    • 钩子函数: hookReduceArg0
      使用场景: outputOptions、renderChunk等


    // 对arg第一项进行reduce操作
    function hookReduceArg0<H extends keyof PluginHooks, V, R = ReturnType<PluginHooks[H]>>(
    hookName: H,
    [arg0, ...args]: any[], // 取出传入的数组的第一个参数,将剩余的置于一个数组中
    reduce: Reduce<V, R>,
    replaceContext?: ReplaceContext // 替换当前plugin调用时候的上下文环境
    ) {
    let promise = Promise.resolve(arg0); // 默认返回source.code
    for (let i = 0; i < this.plugins.length; i++) {
    // 第一个promise的时候只会接收到上面传递的arg0
    // 之后每一次promise接受的都是上一个插件处理过后的source.code值
    promise = promise.then(arg0 => {
    const hookPromise = this.runHook(hookName, [arg0, ...args], i, false, replaceContext);
    // 如果没有返回promise,那么直接返回arg0
    if (!hookPromise) return arg0;
    // result代表插件执行完成的返回值
    return hookPromise.then((result: any) =>
    reduce.call(this.pluginContexts[i], arg0, result, this.plugins[i])
    );
    });
    }
    return promise;
    }

    通过观察上面几种钩子函数的调用方式,我们可以发现,其内部有一个调用钩子函数的方法: runHook(Sync),该函数执行插件中提供的钩子函数。


    实现很简单:


    function runHook<T>(
    hookName: string,
    args: any[],
    pluginIndex: number,
    permitValues: boolean,
    hookContext?: ReplaceContext | null
    ): Promise<T> {
    this.previousHooks.add(hookName);
    // 找到当前plugin
    const plugin = this.plugins[pluginIndex];
    // 找到当前执行的在plugin中定义的hooks钩子函数
    const hook = (plugin as any)[hookName];
    if (!hook) return undefined as any;

    // pluginContexts在初始化plugin驱动器类的时候定义,是个数组,数组保存对应着每个插件的上下文环境
    let context = this.pluginContexts[pluginIndex];
    // 用于区分对待不同钩子函数的插件上下文
    if (hookContext) {
    context = hookContext(context, plugin);
    }
    return Promise.resolve()
    .then(() => {
    // permit values allows values to be returned instead of a functional hook
    if (typeof hook !== 'function') {
    if (permitValues) return hook;
    return error({
    code: 'INVALID_PLUGIN_HOOK',
    message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.`
    });
    }
    // 传入插件上下文和参数,返回插件执行结果
    return hook.apply(context, args);
    })
    .catch(err => throwPluginError(err, plugin.name, { hook: hookName }));
    }

    当然,并不是每个人刚开始都会使用插件,所以rollup本身也提供了几个必需的钩子函数供我们使用,在Graph实例化的时候与用户自定义插件进行concat操作:


    import { getRollupDefaultPlugin } from './defaultPlugin';

    this.plugins = userPlugins.concat(
    // 采用内置默认插件或者graph的插件驱动器的插件,不管怎么样,内置默认插件是肯定有的
    // basePluginDriver是上一个PluginDriver初始化的插件
    // preserveSymlinks: 软连标志
    basePluginDriver ? basePluginDriver.plugins : [getRollupDefaultPlugin(preserveSymlinks)]
    );

    那rollup提供了哪些必需的钩子函数呢:


    export function getRollupDefaultPlugin(preserveSymlinks: boolean): Plugin {
    return {
    // 插件名
    name: 'Rollup Core',
    // 默认的模块(文件)加载机制,内部主要使用path.resolve
    resolveId: createResolveId(preserveSymlinks) as ResolveIdHook,
    // this.pluginDriver.hookFirst('load', [id])为异步调用,readFile内部用promise包装了fs.readFile,并返回该promise
    load(id) {
    return readFile(id);
    },
    // 用来处理通过emitFile添加的urls或文件
    resolveFileUrl({ relativePath, format }) {
    // 不同format会返回不同的文件解析地址
    return relativeUrlMechanisms[format](relativePath);
    },
    // 处理import.meta.url,参考地址:https://nodejs.org/api/esm.html#esm_import_meta)
    resolveImportMeta(prop, { chunkId, format }) {
    // 改变 获取import.meta的信息 的行为
    const mechanism = importMetaMechanisms[format] && importMetaMechanisms[format](prop, chunkId);
    if (mechanism) {
    return mechanism;
    }
    }
    };
    }

    过一眼发现都是最基本处理路径解析内容的钩子函数。


    不仅如此,rollup给钩子函数注入了context,也就是上下文环境,用来方便对chunks和其他构建信息进行增删改查。


    文档中也写得很清楚,比如:



    • 使用this.parse,调用rollup内部中的acron实例解析出ast

    • 使用this.emitFile来增加产出的文件,看这个例子.


    我们通过transform操作来简单看下,之前对ast进行transform的时候,调用了transform钩子:



    graph.pluginDriver
    .hookReduceArg0<any, string>(
    'transform',
    [curSource, id], // source.code 和 模块id
    transformReducer,
    // 第四个参数是一个函数,用来声明某些钩子上下文中需要的方法
    (pluginContext, plugin) => {
    // 这一大堆是插件利用的,通过this.xxx调用
    curPlugin = plugin;
    if (curPlugin.cacheKey) customTransformCache = true;
    else trackedPluginCache = getTrackedPluginCache(pluginContext.cache);
    return {
    ...pluginContext,
    cache: trackedPluginCache ? trackedPluginCache.cache : pluginContext.cache,
    warn(warning: RollupWarning | string, pos?: number | { column: number; line: number }) {
    if (typeof warning === 'string') warning = { message: warning } as RollupWarning;
    if (pos) augmentCodeLocation(warning, pos, curSource, id);
    warning.id = id;
    warning.hook = 'transform';
    pluginContext.warn(warning);
    },
    error(err: RollupError | string, pos?: number | { column: number; line: number }): never {
    if (typeof err === 'string') err = { message: err };
    if (pos) augmentCodeLocation(err, pos, curSource, id);
    err.id = id;
    err.hook = 'transform';
    return pluginContext.error(err);
    },
    emitAsset(name: string, source?: string | Buffer) {
    const emittedFile = { type: 'asset' as const, name, source };
    emittedFiles.push({ ...emittedFile });
    return graph.pluginDriver.emitFile(emittedFile);
    },
    emitChunk(id, options) {
    const emittedFile = { type: 'chunk' as const, id, name: options && options.name };
    emittedFiles.push({ ...emittedFile });
    return graph.pluginDriver.emitFile(emittedFile);
    },
    emitFile(emittedFile: EmittedFile) {
    emittedFiles.push(emittedFile);
    return graph.pluginDriver.emitFile(emittedFile);
    },
    addWatchFile(id: string) {
    transformDependencies.push(id);
    pluginContext.addWatchFile(id);
    },
    setAssetSource(assetReferenceId, source) {
    pluginContext.setAssetSource(assetReferenceId, source);
    if (!customTransformCache && !setAssetSourceErr) {
    try {
    return this.error({
    code: 'INVALID_SETASSETSOURCE',
    message: `setAssetSource cannot be called in transform for caching reasons. Use emitFile with a source, or call setAssetSource in another hook.`
    });
    } catch (err) {
    setAssetSourceErr = err;
    }
    }
    },
    getCombinedSourcemap() {
    const combinedMap = collapseSourcemap(
    graph,
    id,
    originalCode,
    originalSourcemap,
    sourcemapChain
    );
    if (!combinedMap) {
    const magicString = new MagicString(originalCode);
    return magicString.generateMap({ includeContent: true, hires: true, source: id });
    }
    if (originalSourcemap !== combinedMap) {
    originalSourcemap = combinedMap;
    sourcemapChain.length = 0;
    }
    return new SourceMap({
    ...combinedMap,
    file: null as any,
    sourcesContent: combinedMap.sourcesContent!
    });
    }
    };
    }
    )

    runHook中有一句判断,就是对上下文环境的使用:


    function runHook<T>(
    hookName: string,
    args: any[],
    pluginIndex: number,
    permitValues: boolean,
    hookContext?: ReplaceContext | null
    ) {
    // ...
    const plugin = this.plugins[pluginIndex];
    // 获取默认的上下文环境
    let context = this.pluginContexts[pluginIndex];
    // 如果提供了,就替换
    if (hookContext) {
    context = hookContext(context, plugin);
    }
    // ...
    }

    至于rollup是什么时机调用插件提供的钩子函数的,这里就不啰嗦了,代码中分布很清晰,一看便知.


    还有 rollup 为了方便咱们变化插件,还提供了一个工具集,可以非常方便的进行模块的操作以及判断,有兴趣的自行查看。


    插件的缓存


    插件还提供缓存的能力,实现的非常巧妙:


    export function createPluginCache(cache: SerializablePluginCache): PluginCache {
    // 利用闭包将cache缓存
    return {
    has(id: string) {
    const item = cache[id];
    if (!item) return false;
    item[0] = 0; // 如果访问了,那么重置访问过期次数,猜测:就是说明用户有意向主动去使用
    return true;
    },
    get(id: string) {
    const item = cache[id];
    if (!item) return undefined;
    item[0] = 0; // 如果访问了,那么重置访问过期次数
    return item[1];
    },
    set(id: string, value: any) {
    // 存储单位是数组,第一项用来标记访问次数
    cache[id] = [0, value];
    },
    delete(id: string) {
    return delete cache[id];
    }
    };
    }

    然后创建缓存后,会添加在插件上下文中:


    import createPluginCache from 'createPluginCache';

    const cacheInstance = createPluginCache(pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null)));

    const context = {
    // ...
    cache: cacheInstance,
    // ...
    }

    之后我们就可以在插件中就可以使用cache进行插件环境下的缓存,进一步提升打包效率:


    function testPlugin() {
    return {
    name: 'test-plugin',
    buildStart() {
    if (!this.cache.has('prev')) {
    this.cache.set('prev', '上一次插件执行的结果');
    } else {
    // 第二次执行rollup的时候会执行
    console.log(this.cache.get('prev'));
    }
    },
    };
    }
    let cache;
    async function build() {
    const chunks = await rollup.rollup({
    input: 'src/main.js',
    plugins: [testPlugin()],
    // 需要传递上次的打包结果
    cache,
    });
    cache = chunks.cache;
    }

    build().then(() => {
    build();
    });

    不过需要注意的一点是options钩子函数是没有注入上下文环境的,它的调用方式也和其他钩子不一样:


    function applyOptionHook(inputOptions: InputOptions, plugin: Plugin) {
    if (plugin.options){
    // 指定this和经过处理的input配置,并未传入context
    return plugin.options.call({ meta: { rollupVersion } }, inputOptions) || inputOptions;
    }

    return inputOptions;
    }

    总结


    rollup系列到此也就告一段落了,从开始阅读时的一脸懵逼,到读到依赖收集、各工具类的十脸懵逼,到现在的轻车熟路,真是一段难忘的经历~


    学习大佬们的操作并取其精华,去其糟粕就像打怪升级一样,你品,你细品。哈哈


    在这期间也是误导一些东西,看得多了,就会发现,其实套路都一样,摸索出它们的核心框架,再对功能缝缝补补,不断更新迭代,或许我们也可以成为开源大作的作者。


    如果用几句话来描述rollup的话:


    读取并合并配置 -> 创建依赖图 -> 读取入口模块内容 -> 借用开源estree规范解析器进行源码分析,获取依赖,递归此操作 -> 生成模块,挂载模块对应文件相关信息 -> 分析ast,构建各node实例 -> 生成chunks -> 调用各node重写的render -> 利用magic-string进行字符串拼接和wrap操作 -> 写入


    精简一下就是:


    字符串 -> AST -> 字符串



    如果改系列能对你一丝丝帮忙,还请动动手指,鼓励一下~


    拜了个拜~

  • 相关阅读:
    移动应用专项测试
    MAC连接安卓手机通过adb指令安装apk
    Git GUI可视化操作教程
    nestjs中typeorm进行事物操作
    vue-element-admin 实现动态路由(从后台查询出菜单列表绑定侧边栏)
    el-form 表单校验
    vscode设置VUE eslint开发环境
    .netcore signalR 实时消息推送
    psexec局域网执行远程命令
    Asp.Net跨平台 Jexus 5.8.1 独立版
  • 原文地址:https://www.cnblogs.com/yangzhuxian/p/13371637.html
Copyright © 2020-2023  润新知