持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第28天,点击查看活动详情
研究一下rollup的使用,配合文档,分析场景。这里讲一下编译和打包阶段的一些钩子。
简单的使用
# package.json
type:'module'
scripts:{
build:'rollup -c'
}
// 当我们用esm引用时,引入js文件需要加后缀
# rollup.config.js
export default {
input: 'src/main.js',
output: {
dir:'dist',
file: 'bundle.js',
format: 'cjs'
}
};
# build.js
import {rollup} from 'rollup'
import options from './rollup.config.js'
(async function(){
// 编译 build hooks
const bundle = await rollup(options)
// 生成文件
await bundle.generate(options.output)
// 生成的文件 写入 硬盘
await bundle.write(options.output)
// 关闭
await bundle.close()
})()
插件
具有一个或多个属性、构建钩子、输出生成钩子的对象。
规范
-
有名称
-
package.json带关键字keywords:[...] -
有测试
-
尽可能用异步方法
-
英文文档
-
能输出
sourcemap尽量正确 -
使用 虚拟模块 ,在模块id前面加
\0,会阻止其他插件尝试处理它
引用
# rollup.config.js
import build from './plugins/build.js'
export default {
input: 'src/main.js',
output: {
dir:'dist',
},
plugins:[build()]
};
build hooks
文档里的流程图很重要
钩子
function build(pluginOptions) {
return {
name: 'build',
async options(inputOptions) {
console.log('引入的配置文件')
// 这里可以修改配置文件 inputOptions.input = 'xxx
},
// (options: InputOptions) => void
// 修改后的完整的配置
async buildStart(inputOptions) {
},
// import { foo } from '../bar.js';
// 找引入模块的绝对路径
// 如果是第三方的,会去node_module里找,默认不支持。
async resolveId(importee, importer) {
// importee 被引用方 ../bar.js, importer 引用方 ..index.js
console.log(importee, importer)
// if(importee=='vite')return vite
// 如果return 值,那么这个值会作为文件引用路径,不走默认解析流程
},
// 如果上面的引用不是第三方的,走下面的
// 根据路径,查找文件内容 ,fs.readFile()
async load(id) {
// if(id=='vite')return 'xx'
},
// 文件内容,源代码,转化代码,最重要的地方
async transform(code, fileName) {
// 匹配文件名,不符合就返回,走默认逻辑
if (!filter(fileName)) return
return await transformAsync(code)
},
// 观察改变,有文件改变的时候,得到文件名。
async watchChange(id) {
},
// 监听下,判断改变的模块是否需要走转化还是用缓存,返回true就走transform,false跳过
async shouldTransformCachedModule({ id }) {
},
async moduleParsed(moduleInfo) {
// 模块解析,moduleInfo包含模块的信息,code,id等。。
},
async resolveDynamicImport(specifier, importer) {
// 动态引入 import('./abc.js').then(res=>,,,)
// specifier : ./abc.js
// importer 所属文件 ...index.js
// 他可以改变引入的文件,return './xx.js' 更改 import('./abc.js')
},
async buildEnd() {
// 结束
}
}
}
流程
装载配置项
options开始编译
buildStart解析入口文件
resolveId加载入口文件内容
load对内容进行转换
transform(会缓存)(webpack多了个loader的概念)对引入模块解析
moduleParsed静态的import走resolveId,动态的走一下resolveDynamicImport再走resolveId在监视模式下,
load加载内容后走shouldTransformCachedModule,加载的代码与缓存副本的代码相同,则Rollup将跳过模块的转换钩子transform结束
例子:bebel编译
npm i @babel/core @babel/preset-env -D
#babel.config.js
export default{presets:[@babel/preset-env]}
# rollup.config.js
import babel from './plugins/babel.js'
export default {
input: 'src/main.js',
output: {
dir:'dist',
},
plugins:[
babel({
include:'./src/**',
exclude:'/node_modules',
estensions:['js','jsx']
})
]
};
# babel.js
import {transformAsync} from '@babel/core'
import {createFilter} from 'rollup-pluginutils'
function babel(pluginOptions) {
const {include,exclude,extensions=['.js']} =pluginOptions
// 正则匹配 js结尾的文件
const extensionsRegexp = new RegExp(`(${extensions.json('|')})$`)
// 判断当前文件是否在include里,在exclude外
const userFilter = createFilter(include,exclude)
// 判断文件是否匹配
const filter = id=>extensionsRegexp.test(id)&&userFilter(id)
return {
name: 'babel',
async transform(code,fileName){
// 匹配文件名,不符合就返回,走默认逻辑
if(!filter(fileName))return
return await transformAsync(code)
}
Output Generation Hooks
引入方式同上,和上面不同,这边有同步任务、并行串行任务
插件执行顺序是至上而下顺序的。
plugins:[build(),build2()],
options是串行的,会在第一个执行完后再执行第二个插件
流程
我的理解
1 输出配置
2 开始渲染
3 在代码里添加添加
banner footer等必要的信息4 给
chunk添加hash后缀名5 渲染输出
chunk6 打包 写入文件
结束
钩子
# generation.js
function generation() {
return {
name: 'generation',
outputOptions(outputOptions) {
// 输出配置
},
renderStart() {
// 渲染开始,当调用 bundle.generate() 时 触发
// Called initially each time bundle.generate() or bundle.write() is called.
},
banner() {
// 往输出文件头部加文本
},
footer() {
// 往输出文件尾部加文本
},
intro() {
// 往输出文件头部下加文本
},
outro() {
// 往输出文件尾部上方加文本
},
renderDynamicImport() {
// 动态import,提供对动态导入的细粒度控制
// import.meta.url 模块文件路径
// 比如自己实现动态引入,
// return {
// left: 'dynamicImportPolyfill(',
// right: ', import.meta.url)'
// };
// // output
// import('./lib.js') =>
// dynamicImportPolyfill('./lib.js', import.meta.url);
},
// 由于treeshake,dynamicImportPolyfill没使用就不会被打包进去,所以加个
// console.log(dynamicImportPolyfill)
// 或者 moduleparse 钩子中判断模块信息里是否有dynamicImportPolyfill,有就不要treeshake
// function dynamicImportPolyfill(filename,url){
// return new Promise(resolve=>{
// window.__dynamicImportPolyfillResolve__ = resolve
// new URL:以右边为基准(自动去子域名),左边为相对路径,拼。
// const resourceUrl = new URL(filename,url).href
// let script = document.createElement('script')
// script.type = 'module'
// script.innerHTML = `
// import * as resource from ${JSON.stringify(resourceUrl)}
// window.__dynamicImportPolyfillResolve__(resource)
// `
// document.head.appendChild(script)
// })
// }
augmentChunkHash(chunkInfo) {
// 改名 引入模块 hash
// import('./lib.js') =》 打包时输出 lib-hash.js
// chunkInfo:模块名
if (chunkInfo.name === 'msg') {
// 反复 输出 时 ,导出文件 名字固定,msg-hash1
return 'msg'
// 每次 输出,都不一样
// return Date.now().toString();,msg-hash随机
}
},
// 输出文件后调用
async renderChunk(code, chunk) {
},
// 生成 打包
generateBundle() { },
// 调用 bundle.write()之后触发,类似`generateBundle`
async writeBudle(options, bundle) {
// 输出 文件选项,输出文件列表
},
async renderError() {
// 报错的时候调用
},
async closeBundle() {
// bundle.close触发
}
}
}
export default generation
例子:resolveFileUrl hook
# 原文件
import logger from 'logger'
console.log(logger)
# 插件
function resolveToDocumentPlugin() {
return {
name: 'resolve-to-document',
resolveId(i){
// 引入时 阻止默认行为
if(i=='logger'){
return i
}
},
load(i){
if(i=='logger'){
//https://rollupjs.org/guide/en/#thisemitfile
// 生成 logger.js 文件,返回 引入路径,通过import.meta.ROLLUP_FILE_URL触发resolveFileUrl钩子
let referenceId = this.emitFile({type:'asset',source:'文件内容',fileName:'logger.js'})
return `export default import.meta.ROLLUP_FILE_URL_${referenceId}`
}
},
//import.meta.ROLLUP_FILE_URL = `new URL('${fileName}', document.baseURI).href`
resolveFileUrl({ fileName }) {
// document.baseURI 可以让 引入都从根目录去引入
return `new URL('${fileName}', document.baseURI).href`;
}
};
}
# 输出
var logger = new URL('logger.js', import.meta.url).href
console.log(logger)
还有个logger.js文件输出
例子:generateBundle生成入口html
generateBundle(options,bundle){
let entry
for(let filename in bundle){
if(bundle[filename].isEntry){
entry = filename
}
}
this.emitFile({type:'asset',fileName:'index.html',source:`
html模板,
<script type='module' src="${entry}"></script>
`})
}
修改输出路径
generateBundle(options,bundle){
let entry
for(let filename in bundle){
// 这样所有打包文件都会到js目录下
bundle[filename].fileName = `js/${filename}`
bundle[`js/${filename}`] = bundle[filename]
}