zoukankan      html  css  js  c++  java
  • 原来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 -> 字符串

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

    拜了个拜~

  • 相关阅读:
    MS CRM 2011的自定义和开发(10)——CRM web服务介绍(第一部分)——IDiscoveryService
    MS CRM 2011的自定义和开发(7)——视图编辑器(第二部分)
    MS CRM 2011 SDK 5.06版本已经发布
    MS CRM 2011的自定义和开发(11)——插件(plugin)开发(一)
    近来遇到的MS CRM 2011方面的几个问题
    MS CRM 2011的自定义与开发(6)——表单编辑器(第二部分)
    Microsoft Dynamics CRM 2011中,Lookup字段的赋值
    MS CRM 2011的自定义和开发(6)——表单编辑器(第三部分)
    Visual Studio 目标框架造成 命名空间“Microsoft”中不存在类型或命名空间名称“Crm”。是否缺少程序集引用中错误的处理
    一步步学习Reporting Services(二) 在报表中使用简单的参数作为查询条件
  • 原文地址:https://www.cnblogs.com/xiaoyuxy/p/12694566.html
Copyright © 2011-2022 走看看