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 string
const 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.ts
writeFileSync(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 changes
addWatched = true
}
createEnvTypes();
},
renderChunk(code, chunk) {
if (chunk.isEntry) {
// entry file needs to be modified to inject environment variables
return `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