背景
最近在研究一个ngptcommit命令行工具,然后想通过Rollup
+Typescript
去编译的时候,发现对Rollup
和Typescript
的编译配置有点陌生,所以希望通过本文能够对其有个系统的认知。
本文主要是项目编译基础知识,明白其为什么要这么配置,同时能够将项目完整跑起来。
参考项目地址为:node-gptcommit。
为什么不用Webpack
和Vite
呢?因为这两个对于一个命令行工具来说有点过于重了,有点啥像杀鸡焉用宰牛刀的感觉。
Rollup
Rollup是JavaScript模块打包工具,可以将现代化代码编译成更加复杂的代码,如:库或应用。默认使用 JavaScript ES6 修订版中包含的代码模块的新标准化格式,而不是以前的特殊解决方案,如 CommonJS 和 AMD等。 —— Rollup 官网
了解Rollup的打包核心思想:主要是将代码编译成符合ES模块规范的代码包,当然也可以用其相关的插件实现CommonJS规范。
接下来我们将通过下面几个步骤去完整了解Rollup:
- Rollup组成部分
- Rollup执行构建原理流程
- 结合Typescript实战
组成部分
一般我们实际使用场景是不会通过命令行去编译某个文件,而是针对整个项目去编译构建,因此一个完整Rollup
构建项目主要有以下几个部分组成:
rollup
npm包,用于执行构建命令源头,可以安装本地项目,也可以安装全局命令,但是一般是跟着项目走rollup.config.js
roll的配置文件,是所有命令的入口,也是学习Rollup的核心基础之一- 插件部分,rollup有丰富的插件生态,如:Babel 编译代码,运行 JSON 文件等,可以让rollup完成更多复杂构建功能
- 输出插件,在rollup代码分析完成之后,才可以修改代码相关事项
这基本上就是Rollup
项目构建所组成的部分了,接下来我们进行一一学习。
Rollup命令
在项目中,我们常用的命令有以下几种:
rollup -c
使用配置文件(如果使用参数但是值没有 指定, 默认就是 rollup.config.js)执行构建rollup -c -w
监听入口文件并在文件改变时重新构建rollup -c --environment BUILD:production
可以设置环境变量,会设置process.env.BUILD = 'production'
通过我们会在package.json
中的scripts
设置如下命令:
{ "scripts": { "dev": "rollup -c -w", "build": "rollup -c --environment BUILD:production" } }
基本上就已经满足绝大部分项目需求了,如果有些项目需要更多命令配置,可以到官网查看命令行参数:Rollup命令行参数说明。
非命令行使用Rollup
还有一种Rollup
的使用,就是通过代码引用Rollup
实现,比如在scripts/build.js
,去引用rollup
,然后深度参与Rollup构建前后的一些事项,满足项目构建的自定义功能。
一般代码如下:
const rollup = require('rollup'); // 有关选项的详细信息,请参见下文 const inputOptions = {...}; const outputOptions = {...}; async function build() { // 创建一个 bundle const bundle = await rollup.rollup(inputOptions); console.log(bundle.watchFiles); // 该 bundle 依赖的文件名数组 // 在内存中生成输出特定的代码 // 您可以在同一个 bundle 对象上多次调用此函数 const { output } = await bundle.generate(outputOptions); for (const chunkOrAsset of output) { if (chunkOrAsset.type === 'asset') { // 对于assets,包含 // { // fileName: string, // asset 文件名 // source: string | Uint8Array // asset 资源 // type: 'asset' // 表示这是一个 asset // } console.log('Asset', chunkOrAsset); } else { // 对于chunks, 包含 // { // code: string, // 生成的JS代码 // dynamicImports: string[], // chunk 动态导入的外部模块 // exports: string[], // 导出的变量名 // facadeModuleId: string | null, // 该chunk对应的模块的ID // fileName: string, // chunk的文件名 // imports: string[], // chunk 静态导入的外部模块 // isDynamicEntry: boolean, // 该 chunk 是否是动态入口点 // isEntry: boolean, // 该 chunk 是否是静态入口点 // map: string | null, // sourcemaps(如果存在) // modules: { // 此 chunk 中模块的信息 // [id: string]: { // renderedExports: string[]; // 导出的已包含变量名 // removedExports: string[]; // 导出的已删除变量名 // renderedLength: number; // 模块中剩余代码的长度 // originalLength: number; // 模块中代码的原始长度 // }; // }, // name: string // 命名模式中使用的 chunk 的名称 // type: 'chunk', // 表示这是一个chunk // } console.log('Chunk', chunkOrAsset.modules); } } // 或者将bundle写入磁盘 await bundle.write(outputOptions); } build();
然后package.json
中的scripts
设置如下命令:
{ "scripts": { "build": "node scripts/build.js" } }
配置文件
rollup.config.js
一般会放在项目根目录下,如果放在其他目录下,需要在命令行上指定对应路径,如:rollup -c xxx/rollup.config.js
。
Rollup配置文件中,我们需要关心的配置项主要有以下几个:
input
入口文件,Rollup将从这里去扫描解析代码,生成代码依赖树,支持多个output
输出配置项,主要是指的Rollup编译输出什么格式的代码,这里涉及多个配置项plugins
依赖的插件列表,需要哪些插件去扩展Rollup构建能力,不同插件配置内容也不同,后面会讲述常用的几个插件external
忽略打包模块列表,如:有些公共库,我们不需要构建进来cache
构建缓存,是否开启构建缓存,提高构建速度,依据配置内容可以才去不同缓存策略
需要注意的一个点,Rollup
是支持多套配置,比如:用来生成umd
规范的代码文件用来支持浏览器,再就生成cjs
给node使用的js文件。
针对上面几个常用的配置项,我们来一一分析它们的用法,其他的配置项可以到官网查看:Rollup完整配置项。
input
input最常见的问题就是多个入口文件,毕竟单一入口文件就很简单解决了,那么遇到有多个入口文件的时候,input
该如何配置:
export default { ..., input: { a: 'src/main-a.js', 'b/index': 'src/main-b.js' }, output: { ..., entryFileNames: 'entry-[name].js' } };
结果:
input
配置的key值会作为output
中配置entryFileNames
中name
的值- 最终会输出两个文件
entry-a.js
和entry-b/index.js
output
output
相比较input
会复杂很多,但是我们所关心的主要配置项如下:
output.dir
, 构建好的代码文件放d到哪个文件夹中output.file
,针对单一入口,指定生成的文件名,如:index.esm.js
index.cjs.js
等output.format
,按照哪个代码规范去生成,目前主要有:
cjs
为CommonJS规范,适用于 Node 环境和其他打包工具es
为ES规范,适用于其他打包工具以及支持</code> 标签的浏览器</li><li><code>umd</code> 通用模块定义,生成的包同时支持 <code>amd</code>、<code>cjs</code> 和 <code>iife</code> 三种格式</li><li>其他有<code>amd</code> <code>iife</code>等</li></ul><ul><li><code>output.globals</code>,用来忽略打包(umd 或 iife 规范)后的代码的代码依赖,比如:代码中依赖<code>jquery</code>,且<code>jquery</code>在代码使用<code>$</code>标识,则可以配置:</li></ul><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%7B%5Cn...%5Cnoutput%3A%7B%5Cn%20%20%20%20globals%3A%20%7B%5Cn%20%20%20%20%20%20%20%20jquery%3A%20'%24'%5Cn%20%20%20%20%7D%5Cn%7D%5Cn...%5Cn%7D%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22jSOyo%22%2C%22autoWrap%22%3Atrue%7D"></div><ul><li><code>output.name</code>, 以umd 或 iife 规范打包后的代码,需要注册在全局对象中的名字</li><li><code>output.plugins</code>,针对输出后的代码需要进行插件扩展,如:压缩代码</li><li><code>output.chunkFileNames</code>,对代码分割中产生的 chunk 文件自定义命名,默认值是:<code>[name]-[hash].js</code></li><li><code>output.exports</code>,指定导出模式,有几个值:</li></ul><ul data-lake-indent="1"><li><code>default</code>,等于最终导出等于<code>export default xxx</code>,这里适用于单个文件入口</li><li><code>named</code>,等于<code>export default {xxx1, xxx2}</code>,适用于多个入口文件</li><li><code>none</code>,没有<code>export</code>,适用于打包web应用,不需要对外抛出对象</li></ul><ul><li><code>output.externalLiveBindings</code>, 是否给外部依赖生成动态绑定代码,简单的说就是是否需要将外部依赖的npm包通过转义来引入</li><li><code>output.freeze</code> 指定是否使用 Object.freeze() 冻结动态访问的引入对象的命名空间,禁止修改外部的依赖对象属性</li><li><code>output.sourcemap</code> 是否生成sourcemap文件</li></ul><div>Typescript主要知识点:</div><h3 id="ZKp5i">plugins</h3><div data-card-type="block" data-ready-card="hr"></div><div>Rollup plugin有很多,这里我们分成两块去学习,一个是如何配置plugin,另外一个是如何开发一个plugin。</div><h4 id="2198z">配置plugin</h4><div data-card-type="block" data-ready-card="hr"></div><div>这块内容就相对比较简单了,主要在于如何找到适合的plugin,以及它们的配置项是怎么样的。</div><div>第一个问题,到哪里找插件,Rollup官网提供一个地方去找对应plugin,<a href="https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Frollup%2Fawesome" target="_blank" ref="nofollow noopener noreferrer">awesome Rollup插件</a></div><div>第二个文件,如何配置plugin,具体如下:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22import%20typescript%20from%20'rollup-plugin-typescript2'%3B%5Cnexport%20default%20%5B%7B%5Cn%20%20%20%20plugins%3A%5B%5Cn%20%20%20%20%20%20%20%20typescript()%5Cn%20%20%20%20%5D%5Cn%7D%5D%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22qKLMw%22%2C%22autoWrap%22%3Atrue%7D"></div><h4 id="0G4Lk">开发plugin</h4><div data-card-type="block" data-ready-card="hr"></div><div>在官网有详细的教程,这里我们简单学会如何快速完成一个插件。</div><div>首先,我们需要对Rollup执行流程有一个完整的理解,如下图生命周期钩子函数所示:</div><div><span style="color: #181818;"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fm7fto6cqrsdsy_ad7379dee4c246219133e1191d895c96.png%22%2C%22originWidth%22%3A1224%2C%22originHeight%22%3A1524%2C%22name%22%3A%22f16f56c2b2f9bd6bc0d8e92ffe55407.png%22%2C%22size%22%3A313649%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Atrue%2C%22bottom%22%3Atrue%7D%2C%22width%22%3A1224%2C%22height%22%3A1524%7D"></span></span></div><div>Rollup对外提供的生命周期钩子函数:</div><ul><li>读取配置项 options</li><li>开始构建 buildStart</li><li>解析代码 resolveId,这里可以自定义一个解析代码器</li><li>加载代码 load</li><li>加载缓存模块 shouldTransformCacheModule</li><li>转义代码中 transform</li><li>将代码解析ES模块化后 modulePared</li><li>解析异步加载,如:import(()=> xxx) resolveDynamicImport</li><li>构建结束 buildEnd</li><li>监听改变中 watchChange</li><li>关闭监听后 closeWatcher</li></ul><div><br /></div><div>接下来我们来完成一个插件,就是在代码构建前,将<code>__helloworld__</code>换成<code>"hello qborfy!"</code>,避免代码解析出错,代码如下:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%5Cn%2F%2F%20replaceHelloWorld.js%5Cnexport%20default%20function%20replaceHelloWorld()%7B%5Cn%20%20%20%20return%20%7B%5Cn%20%20%20%20%20%20%20%20name%3A%20'replace-helloworld'%2C%20%2F%2F%20%E6%8F%92%E4%BB%B6%E5%90%8D%E7%A7%B0%5Cn%20%20%20%20%20%20%20%20transform%20(%20code%2C%20id%20)%20%7B%20%2F%2F%20%E5%BD%93%E8%BF%9B%E5%85%A5%E8%BD%AC%E6%8D%A2%E7%9A%84%E6%97%B6%E5%80%99%5Cn%20%20%20%20%20%20%20%20%20%20%20%20if%20(id%20%3D%3D%3D%20'replace-helloworld')%20%7B%5Cn%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20%5Cn%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20code%20%3D%20code.replace(%2F__helloworld__%2Fg%2C%20%60%5C%22hello%20qborfy!%5C%22%60)%5Cn%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20%7B%5Cn%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20code%2C%20%5Cn%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20map%3A%20null%2C%20%2F%2F%20%E8%BF%99%E9%87%8C%E4%B8%8D%E5%BD%B1%E5%93%8Dsourcemap%E7%94%9F%E6%88%90%20%E5%85%B7%E4%BD%93%E5%8F%AF%E4%BB%A5%E7%9C%8Bhttps%3A%2F%2Frollupjs.org%2Fplugin-development%2F%23source-code-transformations%5Cn%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%3B%5Cn%20%20%20%20%20%20%20%20%20%20%20%20%7D%5Cn%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%20%5Cn%20%20%20%20%20%20%20%20%7D%5Cn%20%20%20%20%7D%5Cn%7D%5Cn%5Cn%2F%2F%20%E6%8E%A5%E4%B8%8B%E6%9D%A5%E5%9C%A8rollup.config.js%E4%B8%AD%E5%BC%95%E7%94%A8%5Cn%5Cnimport%20replaceHelloWorld%20from%20'.%2FreplaceHelloWorld.js'%3B%5Cnexport%20default%20(%7B%5Cn%20%20input%3A%20'virtual-module'%2C%20%2F%2F%20resolved%20by%20our%20plugin%5Cn%20%20plugins%3A%20%5BreplaceHelloWorld()%5D%2C%5Cn%20%20output%3A%20%5B%7B%5Cn%20%20%20%20file%3A%20'bundle.js'%2C%5Cn%20%20%20%20format%3A%20'es'%5Cn%20%20%7D%5D%5Cn%7D)%3B%5Cn%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22p9VCi%22%2C%22autoWrap%22%3Atrue%7D"></div><div>有两种方式去管理插件,一个是在项目直接管理维护,另外一种是通过发布npm包管理,这个取决插件是否有公用性即可。</div>