引言
Rollup是一个JavaScript模块打包器,它可以将多个模块打包成一个单独的文件,以便在浏览器中使用。与其他打包工具相比,Rollup的主要优势在于它可以生成更小、更快的代码。在本文中,我们将深入了解Rollup的插件开发。
@rollup/pluginutils介绍
@rollup/pluginutils
是一个官方提供的Rollup插件开发工具库,它提供了一些实用的函数和工具,用于简化插件开发过程中的一些常见任务。
该工具库提供了以下常用的函数和工具方法:
createFilter(include?: string | RegExp | (string | RegExp)[], exclude?: string | RegExp | (string | RegExp)[]): FilterPattern
- 用于创建一个过滤器,可以根据指定的包含和排除规则来过滤文件。
- 可以传入字符串、正则表达式或字符串/正则表达式数组作为参数。
- 返回一个函数,该函数接受文件路径作为参数,并返回一个布尔值,表示该文件是否应该被处理。
makeLegalIdentifier(name: string): string
- 用于将给定的字符串转换为合法的JavaScript标识符。
- 主要用于处理可能包含非法字符或保留字的模块名称。
dataToEsm(data: any, options?: DataToEsmOptions): string
- 将给定的数据转换为ES模块导出语法的字符串。
- 可以传入选项对象来自定义导出语法。
attachScopes(ast: any, scope: Scope): void
- 将作用域信息附加到AST(抽象语法树)节点上。
- 可以帮助插件在处理代码时正确地处理变量作用域。
这些函数和工具可以帮助开发者更方便地处理文件过滤、标识符转换、数据转换和作用域处理等常见任务,提高插件开发的效率和可靠性。
插件上下文
这个其实也是插件中很常用的一些api,可以通过 this
从大多数钩子中访问一些实用函数和信息位。
自定义插件
rollup-plugin-custom
import { createFilter } from'@rollup/pluginutils'; importpathfrom"path"; exportdefaultfunctioncustomPlugin(options= {}) { constfilter=createFilter(options.include, options.exclude); return { name: "custom-plugin", transform(code, id) { if (!filter(id)) { returnnull; } constparsedCode=this.parse(code); constsource=`${code}\n\n ${JSON.stringify(parsedCode, null, 2)}`; constfileName=path.basename(id, path.extname(id)); console.log(fileName); if (options.emitFile) { this.emitFile({ type: "asset", fileName: fileName+".txt", source }) } } } }
首先,通过 createFilter
函数创建一个过滤器,用于确定哪些文件需要被处理。options.include
和 options.exclude
分别指定了需要包含和排除的文件。
然后,返回一个对象,其中包含了插件的名称和一个 transform
方法。transform
方法会在每个模块被转换时调用。 在 transform
方法中,首先使用过滤器检查当前模块是否需要处理。如果不需要处理,则返回 null
。
接下来,使用 this.parse(code)
方法解析代码,并将解析结果与原始代码拼接成一个新的字符串 source
。 然后,使用 path.basename(id, path.extname(id))
获取当前模块的文件名(不包含扩展名),并打印输出。 如果设置了 options.emitFile
为 true,则调用 this.emitFile()
方法将处理后的代码作为一个 asset 文件输出。输出的文件名为当前模块的文件名加上 .txt
扩展名。
最后,这个插件可以通过在 Rollup 配置文件中引入并添加到插件列表中来使用。
rollup.config.mjs
import { defineConfig } from"rollup"; importcustomPluginfrom"./plugins/rollup-plugin-custom.js"; exportdefaultdefineConfig({ input: "src/index.js", output: { dir: "dist", format: "esm", sourcemap: true, }, plugins: [ customPlugin({ emitFile:true }) ], });
JSON插件示例
rollup默认是不能直接读取json文件的内容的,我们自己写一个插件处理一下。
安装
npm install @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/pluginutils -D
rollup-plugin-json
import { createFilter,dataToEsm } from'@rollup/pluginutils'; importpathfrom'path'; exportdefaultfunctionmyJson(options= {}) { // rollup 推荐每一个 transform 类型的插件都需要提供 include 和 exclude 选项,生成过滤规则constfilter=createFilter(options.include, options.exclude); return { name: 'rollup-plugin-json', transform: { order: "pre", handler(code, id) { if (!filter(id) ||path.extname(id) !=='.json') returnnull; try { constparse=JSON.stringify(JSON.parse(code)); return { // dataToEsm 将数据转换成esm模块// 其实就是 export default "xxx"code: dataToEsm(parse), map: { mappings: '' } }; } catch (err) { constmessage='Could not parse JSON file'; this.error({ message, id, cause: err }); returnnull; } } } }; }
首先,通过 createFilter
方法创建一个过滤器,用于确定哪些文件需要被处理。options.include
和 options.exclude
分别指定了需要包含和排除的文件。
然后,返回一个对象,其中包含了插件的名称和一个 transform
对象。transform
对象中有两个属性:order
和 handler
。
order: "pre"
表示这个插件在转换过程中应该在其他插件之前执行。handler(code, id)
是一个处理函数,它会在每个模块被转换时调用。
在 handler
函数中,首先使用过滤器检查当前模块是否需要处理,并且判断当前模块是否是 JSON 文件。如果不需要处理或者不是 JSON 文件,则返回 null
。
接下来,尝试将代码解析为 JSON 对象,并使用 dataToEsm(parse)
方法将解析后的对象转换为 ES 模块格式的代码。然后返回一个对象,其中包含了转换后的代码和一个空的 Source Map。
如果解析过程中出现错误,则会捕获错误并通过调用 this.error()
方法抛出错误信息,并返回 null
。
最后,这个插件可以通过在 Rollup 配置文件中引入并添加到插件列表中来使用。它会在构建过程中将 JSON 文件转换为 ES 模块格式的代码。
页面使用
importpkgfrom"../package.json"; console.log(pkg.name)
图片读取
mini-svg-data-uri
是一个用于将SVG图像转换为mini data URI格式的JavaScript库。它可以将SVG图像的内容转换为base64编码,并生成一个data URI,以便在HTML或CSS中直接使用。
安装mini-svg-data-uri
npm install mini-svg-data-uri -D
rollup-plugin-image
import { createFilter,dataToEsm } from"@rollup/pluginutils"; import { extname,resolve,basename,relative,normalize,sep } from"path"; importfsfrom"fs"; importsvgToMiniDataURIfrom"mini-svg-data-uri"; constdefaults= { fileSize: 1024*4, target: "./dist", include: null, exclude: null, } constmimeTypes= { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".webp": "image/webp", ".avif": "image/avif"} constgetDataUri= ({ format, isSvg, mime, source }) =>isSvg?svgToMiniDataURI(source) : `data:${mime};${format},${source}`; constensureDirExists=async (dirPath) => { try { awaitfs.promises.access(dirPath); returntrue; } catch (err) { // 文件夹不存在就创建文件夹try { awaitfs.promises.mkdir(dirPath, { recursive: true }); returntrue; } catch (err) { console.error(err); returnfalse; } } } exportdefaultfunctionmyImage(opts= {}) { constoptions=Object.assign({}, defaults, opts); constfilter=createFilter(options.include, options.exclude); return { name: "my-image", asynctransform(code, id) { if (!filter(id)) returnnull; // 获取后缀constext=extname(id); // 判断是否是图片if(!mimeTypes.hasOwnProperty(ext)) { returnnull; } // 获取图片的mime类型constmime=mimeTypes[ext]; // 判断是否svgconstisSvg=mime===mimeTypes[".svg"]; // 图片format格式constformat=isSvg?"utf-8" : "base64"; // 目标路径constassetsPath=resolve(process.cwd(), options.target); //获取文件名constfileName=basename(id); // 最终文件路径constfilePath=resolve(assetsPath, fileName); letrelativePath=normalize(relative(process.cwd(), filePath)); relativePath=relativePath.substring(relativePath.indexOf(sep) +1); try { // 如果图片文件过大,就应该直接拷贝文件,返回文件路径// 读取图片文件大小与设置的大小进行比较conststat=awaitfs.promises.stat(id); if (stat.size>options.fileSize) { // 文件的拷贝,以及对象的返回// 文件拷贝,无非就是文件源路径,目标路径// copyFile 拷贝文件地址的文件夹必须存在// 如果文件夹不存在,那么就创建文件夹constdirExists=awaitensureDirExists(assetsPath); dirExists&&awaitfs.promises.copyFile(id, filePath); return { code: dataToEsm(relativePath), //返回拷贝之后处理的路径map: { mappings: "" } } } else { // 否则转换为base64格式// 读取文件constsource=awaitfs.promises.readFile(id, format); return { code: dataToEsm(getDataUri({ format, isSvg, mime, source })), map: { mappings: "" } } } } catch (err) { constmessage="图片转换失败:"+id; this.error({ message, id, cause: err }); returnnull; } } } }
createFilter(include, exclude)
: 这个函数来自于@rollup/pluginutils
包,用于创建一个过滤器函数,根据给定的include和exclude规则来判断文件是否需要被处理。extname(id)
: 这个函数来自于path
模块,用于获取文件路径的扩展名。resolve(...paths)
: 这个函数来自于path
模块,用于将多个路径片段解析为绝对路径。basename(path)
: 这个函数来自于path
模块,用于获取文件路径的基本名称(不包含目录部分)。relative(from, to)
: 这个函数来自于path
模块,用于获取从一个路径到另一个路径的相对路径。normalize(path)
: 这个函数来自于path
模块,用于规范化给定的路径字符串。sep
: 这是一个常量,表示操作系统特定的路径分隔符(例如,在Windows上是反斜杠``)。fs.promises.access(path)
: 这是一个Promise-based API,用于检查指定路径是否可访问。fs.promises.mkdir(path, options)
: 这是一个Promise-based API,用于创建指定路径的目录。options参数可以包含递归选项,以便创建多级目录。fs.promises.stat(path)
: 这是一个Promise-based API,用于获取指定路径的文件信息,例如文件大小。fs.promises.copyFile(src, dest)
: 这是一个Promise-based API,用于将源文件复制到目标文件。fs.promises.readFile(path, encoding)
: 这是一个Promise-based API,用于读取指定路径的文件内容。encoding参数用于指定读取的编码格式。dataToEsm(value)
: 这个函数来自于@rollup/pluginutils
包,用于将给定的值转换为ES模块导出语法。svgToMiniDataURI(svg)
: 这个函数来自于mini-svg-data-uri
包,用于将SVG图像转换为mini data URI格式。
在插件的transform
方法中,首先使用过滤器函数判断是否需要处理当前文件。然后根据文件扩展名判断是否为图片文件,并获取对应的MIME类型。接下来根据配置的目标路径和文件名构建最终的文件路径。如果图片文件大小超过了设置的阈值,则直接拷贝该文件到目标路径,并返回拷贝后的路径。否则,将图片内容转换为base64格式,并返回对应的data URI。
rollup.config.mjs
import { defineConfig } from"rollup"; importimagePluginfrom'./plugins/rollup-plugin-image.js'exportdefaultdefineConfig({ input: "src/index.js", output: { dir: "dist", format: "esm", sourcemap: true, }, plugins: [ imagePlugin({ fileSize: 1024*10, target: './dist/assets' }) ], });
总结
- Rollup插件机制允许开发者通过编写自定义插件来扩展Rollup的功能。
- 插件是由一个或多个钩子函数组成的,钩子函数定义了在打包过程中的不同阶段执行的操作。
- 常用的钩子函数有
options
、resolveId
、load
、transform
和generateBundle
,每个钩子函数都有特定的调用时机和参数。 - 插件可以通过返回一个Promise对象来处理异步操作。
- Rollup插件可以使用第三方库来辅助开发,例如
rollup-pluginutils
用于创建过滤器。 - 开发者可以根据自己的需求编写自定义插件,并将其添加到Rollup配置中,以实现各种功能扩展,例如压缩代码、处理CSS、加载和解析JSON等。
- 插件开发需要注意性能和代码质量,避免不必要的操作和副作用。
通过使用Rollup插件机制,开发者可以灵活地定制打包过程,并根据项目需求添加各种功能扩展。这使得Rollup成为一个强大而灵活的JavaScript模块打包工具。