本文介绍: 大家构建工具中的 `external` 的属性一定不会陌生吧。在优化构建物体需求可能引入 `CDN` 来取代一些基础的模块工具包,如 `React`、`Vue`、`lodash` 等。最近没什么事情,闲余时间研究了下 `Vite` 和 `Rollup` 的内部实现,借此机会来探究下构建工具如何处理 `external` 这一类外部链接的,并对 `external` 的能力做一些扩展

前言

大家对构建工具中的 external属性一定不会陌生吧。在优化构建产物体需求可能引入 CDN 来取代一些基础的模块工具包,如 ReactVuelodash 等。
最近没什么事情,闲余时间研究了下 ViteRollup内部实现,借此机会来探究下构建工具如何处理 external 这一类外部链接的,并对 external 的能力做一些扩展

如何解析 External

因为 external 的能力主要体现在构建流程,那么我们就从构建的时机开始看起吧。构建的时候 Vite依赖于 Rollup 的能力,也就是说 external属性主要是在 Rollup 构建上体现出来的。从 Vite 传递Rollup参数上也可以看出来。

const userExternal = options.rollupOptions?.external
let external = userExternal
const rollupOptions: RollupOptions = {
    context: 'globalThis',
    preserveEntrySignatures: ssr
      ? 'allow-extension'
      : libOptions
      ? 'strict'
      : false,
    ...options.rollupOptions,
    input,
    plugins,
    external,
    onwarn(warning, warn) {
      onRollupWarning(warning, warn, config)
    }
}
const { rollup } = await import('rollup')
const bundle = await rollup(rollupOptions)

那么 Rollup如何处理 external路径信息呢?

在构建准备阶段我们可以看到一个初始化操作

function normalizeInputOptions(config) {
    const options = {
        // ...
        external: getIdMatcher(config.external)
    };
    return { options, unsetOptions };
}

getIdMatcher 函数中有对 config.external 进行初始化流程

const getIdMatcher = (option) => {
    if (option === true) {
        return () => true;
    }
    if (typeof option === 'function') {
        return (id, ...args) => (!id.startsWith('') && option(id, ...args)) || false;
    }
    if (option) {
        const ids = new Set();
        const matchers = [];
        for (const value of ensureArray(option)) {
            if (value instanceof RegExp) {
                matchers.push(value);
            }
            else {
                ids.add(value);
            }
        }
        return (id, ..._args) => ids.has(id) || matchers.some(matcher => matcher.test(id));
    }
    return () => false;
};

简单的说就是当用户vite.config.* 配置模块配置build.rollupOptions.external 的话,那么 rollup收集配置的路径用来进行判断解析路径是否为外部链接。

options.external = (id, ..._args) => ids.has(id) || matchers.some(matcher => matcher.test(id));

在此大家一定会好奇检测是在什么时机下发生的呢?

Rollup 检测时机

带着这个好奇我们继续往下看吧,Rollup 在初始化配置信息之后就会通过入口来生成 模块依赖

async generateModuleGraph() {
    (
        { entryModules: this.entryModules, implicitEntryModules: this.implicitEntryModules } =
            await this.moduleLoader.addEntryModules(normalizeEntryModules(this.options.input), true)
    );
}
async build() {
    timeStart('generate module graph', 2);
    await this.generateModuleGraph();
    // ...
}

大家对于模块依赖图一定不会陌生吧,说简单点就是确定模块与模块之间的关系我们就不一一照着源码解释生成模块依赖图的流程,那么多无聊呀!

我们可以静下来好好想想,如果让我们来构建项目的模块依赖图,我们应该怎么做呢?

首先,我们肯定需要从配置的 入口(默认 index.html) 开始分析起来,我们可以分析 index.html 模块依赖哪些模块吧,可能 依赖 这个词有些同学不是很懂,那么就用更通俗的话来说 “【我们可以分析 index.html 模块 importscriptlink哪些模块吧】”。分析index.html 模块后我们不就又可以分析它依赖的模块嘛,在分析依赖模块之前我们应该要先找一下依赖模块在哪,毕竟依赖模块通常也是文件资源嘛。找到依赖模块的位置后很轻易的就能联想到肯定是要读取依赖模块信息吧,那么熟悉 node 的同学就直接 fs 开干! 那我们读取完了嘞,这还要想嘛,不就也像 index.html 模块一样进行解析然后分析依赖情况嘛。细心的同学就可以发现其实就是一个递归检索依赖模块的过程,好像也没什么难度嘛。

真棒! Rollup 还真就是这么处理的,只不过处理流程时候引入插件来协助分析。

那么有同学可能就会说:好像讲离题了唉,这个文章不是要分析 external,怎么讲到了依赖构建的流程呢!

别急嘛,心急吃不了热豆腐。将上面的内容是为了让大家对于 Rollup 执行流程一个大体的认识,在后续会有关联的。

前面有提到 「找到依赖模块的位置Rollup 在处理依赖模块路径时候判断当前模块路径是否是 external

this.resolveId = 
async (source, importer, customOptions, isEntry, skip = null) => {
    return this.getResolvedIdWithDefaults(
       this.getNormalizedResolvedIdWithoutDefaults(
             this.options.external(source, importer, false)
                ? false
                : await resolveId(source, importer, this.options.preserveSymlinks, this.pluginDriver, this.resolveId, skip, customOptions, typeof isEntry === 'boolean' ? isEntry : !importer), 
            importer, 
            source
        )
    );
};

源码上可以了解到当路径被判定为外部链接的情况下是不会执行 await resolveId(...),也就意味着对于外部链接 Rollup 不会对其做处理。

external扩展目标与思路

好嘞,前文先介绍这里。我们接下来先确定下对 external 扩展目标

  1. ESMUMD 产物链接做支持自动引入 CDN 链接。
  2. 支持 Vite 2.0及其以上版本

好嘞,现在我们已经确认目标。那么接下来我们就要分析下如何需要如何进行实现

对 ESM 和 UMD 产物链接做支持且自动引入 CDN 链接 目标实现

  1. ESM规范产物链接的支持:

    由于高版本浏览器支持对 ESM 链接的支持,那么我们只需要将原先的包模块名称换成 CDN 链接就可以了。就好比将 import react from 'react' 替换import react from 'https://cdn.skypack.dev/react'。按照我们先前说的,当我们引入 react 模块,那么在构建阶段 Rollup 就会去找 react 模块究竟在哪里,也就是说寻找 react 模块的具体路径。那么我们只需要告诉 Rollupreact 的具体路径为 https://cdn.skypack.dev/react 不就好了嘛。的确就是这么处理的,不过这里需要提一嘴的是 Rollup获取路径后还会通过 external 配置项来确定下获取的链接是否外链,如果不是外链的话会进一步进行解析,继续解析按照我们先前说的就是去加载资源,我们引入 CDN,不就是想让包体积变小嘛,如果让 Rollup 加载资源,那打包体积不久没优化嘛,我们目标就想要替换 reacthttps://cdn.skypack.dev/react。因此我们需要告诉 Rolluphttps://cdn.skypack.dev/react一个外链,你不要继续解析了。

  2. UMD规范产物链接的支持:

    在早期 JQuery 盛行的年代,我们通常会使用 CDN方式来引入 JQuery然后 CDN 引入的产物执行后会在全局浏览器环境window 下)注入特定的属性开发者就可以通过这个特定属性获取 JQuery 的能力。大家可能会有想法,浏览器环境中我们只需要帮忙获取一下 JQuerywindow注入的属性然后将导入语句改一下就好了嘛,就也是将 import jQuery from 'jquery' 替换const jQuery = window['jQuery']。这样做也可以,不过需要对不同导入方式做特定的处理。在这里,我们可以使用更为简单方式来进行实现,借助 虚拟模块 来进行实现。可能有部分同学对于 虚拟模块定义有点陌生,其实顾名思义就是虚假的模块,是一个存在磁盘空间上的模块。那么我们可以在 虚拟模块 中将我们定义内容导出就好了,即将 import jQuery from 'jquery' 转换

    // virtual: jquery
        
    const jQuery = window['jQuery'];
    export default jQuery;
    

    那么执行流程就是 Rollup 加载 jquery 内容其实就转换为我们上述的代码了。

  3. 自动引入 CDN 链接:

    按照以往流程的话,最后一步我们肯定会手动创建 scriptlink 标签插入index.html 模块的 head 标签内。由于这个流程过于简单但有可能存在开发人员不用 CDN 后当没将 CDN 链接去除,那就存在一定的开销了,那么我们一并把这个能力做掉吧! 其实这个流程就是对于 html 模块做操作,按照正常想法实现的话肯定是先找到一个解析 html 模块的工具,将 html 文本转换ast 结构对象,然后递归分析 ast针对 head 节点插入处理。这里实现了相类似的能力,感兴趣的同学可以看看

    不过通过借助 Vite 内置 html 插件 的能力,我们可以用更简单方式来做到。感兴趣的同学可以点击了解一下具体的实现,这里我就简单说明一下作用吧。插件调用 transformIndexHtml返回值若携带特定的标识符会将信息注入html 特定的地方,使用方式可见 官方文章。那么我们只需要在 transformIndexHtml 钩子返回信息,Vite内置 插件 就会帮我们处理。

支持 Vite 2.0及其以上版本 目标的实现:

在上文 【ESM规范产物链接的支持】 中有提到我们需要告诉 Rollup 对于 ESM 规范CDN 链接不需要继续做解析,因此需要在 Rollup 配置上新增 external 属性。由于 ESM 外链支持异步化(配置外链的流程很大程度会接入平台),而对于在 2.02.2 版本之间 Vite 并不支持异步配置。只能在 vite.config.* 模块中手动添加 external

结束

好嘞,到此实现思路就已经聊完了。相信大部分的同学已经可以写出 external 这一类的插件了,感觉还是蛮有意思的。我已经将实现的思路代码化了,感兴趣的同学可以将 项目 clone 下来。在此我也很期待大家能仓库 带来好的 想法贡献,当然能留下您的 Star 再好不过了,感谢大家的阅读!

原文地址:https://blog.csdn.net/Ashen__/article/details/128025431

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_30750.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注