rollup.js 插件实现原理与自定义
Rollup.js 是一个JavaScript模块打包器,它主要用于将小块代码编译成大块复杂的库或应用程序。相较于Webpack,Rollup更专注于代码的ES模块转换和优化,特别适合构建库或者那些对代码体积、执行效率有严格要求的应用。Rollup的核心特性之一就是它的插件系统,这使得其高度可扩展,可以很容易地通过插件来支持各种编译转换、代码分析、资源处理等任务。
Rollup.js 插件实现原理
Rollup的插件主要基于JavaScript编写,每个插件都是一个对象,至少需要实现一个特定的函数(如transform
、load
、resolveId
等),这些函数会在Rollup构建的不同阶段被调用,以执行相应的任务。插件的工作流程大致可以分为以下几个阶段:
- 解析(Resolving): 在此阶段,插件可以帮助解析模块的导入语句,决定如何处理这些导入(比如别名、路径映射等)。常用的钩子函数有
resolveId
和load
。 - 转换(Transforming): 这是插件最常介入的阶段,用于将源代码转换为浏览器或其他环境可理解的格式。例如,将ES6+语法转换为ES5,或将TypeScript转换为JavaScript。主要使用的钩子函数是
transform
。 - 捆绑(Bundling): 在这个阶段,Rollup会根据模块之间的依赖关系生成最终的捆绑包。虽然这一阶段更多的是Rollup核心的功能,但插件可以通过提供模块信息(如
moduleInfo
钩子)来影响捆绑过程。 - 输出(Outputting): 最后,当所有模块被处理并捆绑后,插件可以通过修改或添加到输出中来进一步处理生成的代码或资源,如压缩代码、添加元数据等。常用钩子包括
generateBundle
和renderChunk
。
其他常用钩子函数
在 Rollup 中开发插件时,你可以通过使用一系列的钩子函数来控制构建过程的不同阶段。下面是一些常用的钩子函数及其简要说明:
options
- 这个钩子允许你在解析命令行选项后修改最终的配置对象。这对于基于环境动态调整配置非常有用。buildStart
- 当构建开始时触发,可用于执行一些初始化工作,比如清理输出目录。resolveId
- 在尝试加载模块前调用此钩子,可以用来重定向模块请求到其他路径,或者处理虚拟模块等场景。load
- 当 Rollup 需要从文件系统或其他来源加载源码时会调用这个钩子。你可以返回自定义内容代替实际文件的内容,适用于注入全局变量、模拟数据等情况。transform
- 该钩子允许你转换已加载的源代码。这是实现代码转译(如 Babel)、添加头部注释等操作的好地方。moduleParsed
- 每当一个模块被完全解析之后都会调用这个钩子,这使得可以在生成图表之前对模块进行额外处理。renderStart
- 开始渲染输出时触发,适合于需要根据输出格式做不同处理的情况。generateBundle
- 当所有文件和 chunk 已经生成完毕但还未写入磁盘时调用。可以通过此钩子访问并修改即将输出的所有资源。writeBundle
- 文件已经写入磁盘后触发。如果你需要在构建完成后执行某些任务(例如运行测试),那么这是一个很好的时机。closeBundle
- 整个构建过程结束时调用。对于清理临时文件或者其他收尾工作很有帮助。watchChange
- 当监听模式下检测到文件变动时触发,可用于决定是否应该重新构建整个项目还是只更新特定部分。
上下文环境
每个Rollup插件在执行过程中都可以访问到一些上下文信息(context),这些信息对于编写高效的插件非常有用。当你为Rollup创建一个插件时,可以通过配置对象来定义几个钩子函数(hooks)。这些钩子函数会在构建过程中的不同阶段被调用,并且会接收到不同的参数,其中包括了当前的插件上下文。以下是Rollup插件中可用的一些主要上下文属性和方法:
- options: 包含传递给Rollup的原始选项对象。这对于理解用户是如何配置Rollup以及可能需要调整你的插件行为是非常有用的。
- moduleIds: 一个映射表,用于存储模块ID与其对应的输出文件名之间的关系。这有助于追踪哪些模块被如何命名。
- getModuleInfo(id): 根据提供的ID获取模块的具体信息。
- emitFile(options): 允许向最终输出添加额外的文件。这对于生成除了JavaScript之外的其他资源文件很有帮助。
- setAsset(name, source, options): 设置一个非JavaScript资产,类似于
emitFile()
但更专注于处理非JS资源。 - error(message, code, loc, frame, pos, id, pluginCode, url, hint): 抛出错误的一种方式,支持丰富的错误信息格式化选项,便于调试。
如何自定义Rollup插件?
自定义Rollup插件通常涉及以下步骤:
- 创建插件对象: 首先,你需要定义一个对象,该对象包含你想要实现的钩子函数。每个钩子函数接收特定的参数,并返回处理结果或Promise。
const myPlugin = {name: 'my-plugin', // 插件名称,用于在日志中标识resolveId(source, importer) {// 解析模块ID的逻辑},load(id) {// 加载模块源码的逻辑},transform(code, id) {// 转换代码的逻辑return { code: transformedCode, map: sourcemap };},generateBundle(options, bundle) {// 输出阶段处理逻辑},
};
- 注册插件: 在Rollup配置文件中,通过
plugins
选项注册你的插件。
export default {input: 'src/index.js',output: {file: 'dist/bundle.js',format: 'iife',},plugins: [myPlugin],
};
- 测试和调试: 编写完插件后,运行Rollup构建命令,观察控制台输出,确保插件按预期工作,并根据需要进行调试。
编写插件时,应尽量遵循Rollup的最佳实践,比如异步操作使用Promise,保持钩子函数的幂等性等,以保证插件的稳定性和兼容性。此外,查看Rollup的官方文档和现有的开源插件源码,也是学习和理解插件开发的好方法。
自定义插件实现
通过上述介绍的插件自定义过程,这里给出一个通过实现读取.env、.env.development等环境配置文件生成对应dts文件,并注入到js的环境变量中,如下所示:
// It reads the environment variables from the .env file and generates a type definition file for them.
const { config } = require('dotenv') // 读取.env文件
const fs = require('fs');
const path = require('path');const { writeFileSync } = fs;
/*** @typedef {Object} Plugin* @property {string} name - Plugin name* @property {function} buildStart - Called when the build starts* @property {function} renderChunk - Called for each chunk during the build* @property {function} buildEnd - Called when the build ends*//*** @typedef {Object} ENV* @property {string} [key] - Environment variable key* @property {string|number|boolean} [value] - Environment variable value *//*** @typedef {Object} Options* @property {string} [mode='development'] - Build mode* @property {string} [path='.env.development'] - Path to the environment variables file* @property {string} [dto='env.d.ts'] - Path to the generated type definition file* @property {ENV} [env] - Environment variables object (optional)*//*** Rollup plugin to inject environment variables into the bundle and generate a type definition file for them.* @constructor* @param {Options} options - Plugin options object (optional)* @returns {Plugin}*/
function injectEnv(options) {let addWatched = false // 是否添加监听const transformEnv = (env) => {const transformed = {};Object.keys(env).forEach(key => {const value = env[key];if (/^\d+(\.\d+)?$/.test(value)) {// 可以转成数字类型transformed[key] = Number(value);} else if (value === 'true' || value === 'false') {// 可以转成布尔类型transformed[key] = Boolean(value);} else {// 其他类型都转成字符串类型transformed[key] = String(value);}});return transformed;}if (options === void 0) options = {};if (typeof options.mode === 'undefined') {options.mode = process.env.NODE_ENV || '';;}if (typeof options.path === 'undefined') {if (options.mode)options.path = `.env.${options.mode}`;else options.path = '.env';}if (typeof options.dto === 'undefined') {options.dto = 'env.d.ts';}if (typeof options.env === 'undefined') {options.env = config({ path: options.path }).parsed || {}; // read .env file}options.env = transformEnv(options.env); // transform env values to number or boolean or stringconst createEnvTypes = () => {let envTypes = `/* eslint-disable */
/**
* ${options.dto}
* This file is automatically generated by 'rollup-plugin-inject-env' plugin.
* Use 'dotenv' npm package to load your environment variables from .env file.
* You can also manually edit this file to add or remove environment variables.
* Global environment variables.
*//**
* Global environment variables.
*/
export interface GlobalEnv {
`;Object.keys(options.env).forEach(key => {const value = options.env[key];if (typeof value === 'number') {// 可以转成数字类型envTypes += ` ${key}: number;\n`;} else if (typeof value === 'boolean') {// 可以转成布尔类型envTypes += ` ${key}: boolean;\n`;} else {// 其他类型都转成字符串类型envTypes += ` ${key}: string;\n`;}});envTypes += `}\n`;envTypes += `declare global{\n const ENV: GlobalEnv;\n}\nexport {};`const dtoPath = path.dirname(options.dto);if (dtoPath && !path.isAbsolute(dtoPath)) {fs.mkdirSync(dtoPath, { recursive: true });}// 写入dist/env.d.tswriteFileSync(options.dto, envTypes);console.log('rollup-plugin-inject-env global env types:', options.dto, ' generated successfully.');}return {name: 'rollup-plugin-inject-env',buildStart() {if (!addWatched) {this.addWatchFile(options.path); // listen to .env file changesaddWatched = true}createEnvTypes();},renderChunk(code, chunk) {if (chunk.isEntry) {// entry file needs to be modified to inject environment variablesreturn `window.ENV = ${JSON.stringify(options.env)};${code}`;}},watchChange(id, change) {createEnvTypes();},}
}module.exports = injectEnv;
如何配置?
通过在rollup.config.js
中配置自定义插件,实现环境变量读取和注入:
// rollup.config.js
import injectEnv from 'rollup-plugin-inejct-dotenv';export default {input: 'index.js',output: {file: 'dist/bundle.js',format: 'cjs'},plugins: [ injectEnv({dto: 'typings/env.d.ts', // 输出的类型定义文件路径mode: 'development' // 环境模式,development或production,对应读取.env.development或.env.production文件, 不设置的时候会默认从process.env.NODE_ENV获取,不设置从.env文件读取path: '.env' // 环境变量文件路径,若设置path,则会读取该文件,否则会通过mode配置获取文件地址}),// other plugins]
};
通过上述配置,运行rollup -c
生成对应的dto
文件:
具体使用可参照:rollup-plugin-inject-dotenv