rollup探究

292 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第28天,点击查看活动详情

研究一下rollup的使用,配合文档,分析场景。这里讲一下编译和打包阶段的一些钩子。

简单的使用

参考:rollupjs.org/guide/en/#r…

# 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

rollupjs.org/guide/en/#b…

文档里的流程图很重要

钩子


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 静态的importresolveId,动态的走一下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

文档:rollupjs.org/guide/en/#o…

引入方式同上,和上面不同,这边有同步任务、并行串行任务

插件执行顺序是至上而下顺序的。

   plugins:[build(),build2()],
   options是串行的,会在第一个执行完后再执行第二个插件

流程

我的理解

1 输出配置

2 开始渲染

3 在代码里添加添加 banner footer 等必要的信息

4 给chunk添加hash后缀名

5 渲染输出chunk

6 打包 写入文件

结束

钩子

# 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]
	}