背景
最近在研究一个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构建项目主要有以下几个部分组成:
rollupnpm包,用于执行构建命令源头,可以安装本地项目,也可以安装全局命令,但是一般是跟着项目走rollup.config.jsroll的配置文件,是所有命令的入口,也是学习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.jsindex.cjs.js等output.format,按照哪个代码规范去生成,目前主要有:cjs为CommonJS规范,适用于 Node 环境和其他打包工具es为ES规范,适用于其他打包工具以及支持<script type=module>标签的浏览器umd通用模块定义,生成的包同时支持amd、cjs和iife三种格式- 其他有
amdiife等
output.globals,用来忽略打包(umd 或 iife 规范)后的代码的代码依赖,比如:代码中依赖jquery,且jquery在代码使用$标识,则可以配置:
{
...
output:{
globals: {
jquery: '$'
}
}
...
}
output.name, 以umd 或 iife 规范打包后的代码,需要注册在全局对象中的名字output.plugins,针对输出后的代码需要进行插件扩展,如:压缩代码output.chunkFileNames,对代码分割中产生的 chunk 文件自定义命名,默认值是:[name]-[hash].jsoutput.exports,指定导出模式,有几个值:default,等于最终导出等于export default xxx,这里适用于单个文件入口named,等于export default {xxx1, xxx2},适用于多个入口文件none,没有export,适用于打包web应用,不需要对外抛出对象
output.externalLiveBindings, 是否给外部依赖生成动态绑定代码,简单的说就是是否需要将外部依赖的npm包通过转义来引入output.freeze指定是否使用 Object.freeze() 冻结动态访问的引入对象的命名空间,禁止修改外部的依赖对象属性output.sourcemap是否生成sourcemap文件
Typescript主要知识点:
plugins
Rollup plugin有很多,这里我们分成两块去学习,一个是如何配置plugin,另外一个是如何开发一个plugin。
配置plugin
这块内容就相对比较简单了,主要在于如何找到适合的plugin,以及它们的配置项是怎么样的。
第一个问题,到哪里找插件,Rollup官网提供一个地方去找对应plugin,awesome Rollup插件
第二个文件,如何配置plugin,具体如下:
import typescript from 'rollup-plugin-typescript2';
export default [{
plugins:[
typescript()
]
}]
开发plugin
在官网有详细的教程,这里我们简单学会如何快速完成一个插件。
首先,我们需要对Rollup执行流程有一个完整的理解,如下图生命周期钩子函数所示:
Rollup对外提供的生命周期钩子函数:
- 读取配置项 options
- 开始构建 buildStart
- 解析代码 resolveId,这里可以自定义一个解析代码器
- 加载代码 load
- 加载缓存模块 shouldTransformCacheModule
- 转义代码中 transform
- 将代码解析ES模块化后 modulePared
- 解析异步加载,如:import(()=> xxx) resolveDynamicImport
- 构建结束 buildEnd
- 监听改变中 watchChange
- 关闭监听后 closeWatcher
接下来我们来完成一个插件,就是在代码构建前,将__helloworld__换成"hello qborfy!",避免代码解析出错,代码如下:
// replaceHelloWorld.js
export default function replaceHelloWorld(){
return {
name: 'replace-helloworld', // 插件名称
transform ( code, id ) { // 当进入转换的时候
if (id === 'replace-helloworld') {
//
code = code.replace(/__helloworld__/g, `"hello qborfy!"`)
return {
code,
map: null, // 这里不影响sourcemap生成 具体可以看https://rollupjs.org/plugin-development/#source-code-transformations
};
}
return null;
}
}
}
// 接下来在rollup.config.js中引用
import replaceHelloWorld from './replaceHelloWorld.js';
export default ({
input: 'virtual-module', // resolved by our plugin
plugins: [replaceHelloWorld()],
output: [{
file: 'bundle.js',
format: 'es'
}]
});
有两种方式去管理插件,一个是在项目直接管理维护,另外一种是通过发布npm包管理,这个取决插件是否有公用性即可。
Typescript
在本文中不讨论Typescript的具体用法,我们将学习如何将Typescript代码转为JavaScript。
如何将一个Typescript代码转义为JavaScript呢?Typescript本身提供了一个工具typescript,因此我们针对其来学习一番。
如何使用typescript呢? 可以到官网TypeScript使用说明文档。
我们这里简单总结一下,在项目中使用Typescript,构建工具一般有以下几个步骤:
npm install typescript -D安装Typescript工具npm install @babel/core -D结合babel对Typescript进行转义- 配置文件
tsconfig.json,配置一些Typescript编译功能 npm install eslint -D,结合eslint对代码语法做检测- 配置文件
eslint.json,配置代码检测标准
tsconfig.json
tsconfig.json一般是放到项目根目录,如果放到其他目录,需要修改对应地址, 配置文件主要几个部分:
compilerOptions编译时的一些配置内容watchOptions当监听文件变化时候需要配置一些内容include哪些文件需要编译,如:"include": ["src/**/*", "tests/**/*"]exclude对某些文件进行忽略,不做编译,如:"exclude": ["src/js/*"]extends继承其他配置文件,如:"extends": ["./base.json"]
目前我们主要使用的还是compilerOptions,里面主要的配置项有:
paths将部分路径进行缩写,比如:"@App/*": ["src/*"],后续使用@App,就会解析成srctarget代码转义哪个ES标准下,如:ES2017,ES2018,ES2019等module代码模块化遵循哪个标准,如:ESNext,CommonJS等strict是否使用严格模式检测代码质量lib编译的有时候需要依赖一些全局变量,比如:Document对象,这个时候需要设置为DOM,或者使用Map对象,这个时候需要ESNextdeclaration是否给每个文件都生成 声明文件.d.tsnoImplicitOverride设置后可以提醒继承类override同名方法时候,需要标注override关键字noUnusedLocals不允许有未使用的变量esModuleInterop可以修复由于ES规范和其他规范混合使用导致的引用错误useUnknownInCatchVariables支持catch中error设置为Unknown类型resolveJsonModule支持json文件引入为一个模块
对Rollup和Typescript都有一定了解后,接下来我们就来实战Rollup+Typescript工程化项目。
实战
- 初始化项目
新建一个文件夹
demo-rollup,后续命令如下:
mkdir demo-rollup
npm init
- 安装依赖
- 安装rollup相关依赖
pnpm add rollup -D
- 安装Typescript相关依赖
pnpm add typescript tslib -D
- 安装babel
pnpm add @babel/core @babel/preset-env @babel/plugin-proposal-class-properties -D
- 安装rollup相关插件
pnpm add @rollup/plugin-babel -D #babel插件
pnpm add @rollup/plugin-commonjs -D #转成commonjs的插件
pnpm add rollup-plugin-typescript2 -D #typescript插件
- 安装其他依赖
pnpm add rimraf -D
- 配置rollup.config.js
import typescript from 'rollup-plugin-typescript2'; // 处理typescript
import babel from '@rollup/plugin-babel';
export default [
{
input: 'src/index.ts',
plugins: [
typescript(), // typescript 转义
babel({
babelrc: false,
presets: [['@babel/preset-env', { modules: false, loose: true }]],
plugins: [['@babel/plugin-proposal-class-properties', { loose: true }]],
exclude: 'node_modules/**',
})
],
output: [
{ file: 'dist/index.js', format: 'cjs' },
{ file: 'dist/index.esm.js', format: 'es' }
]
}
];
- 配置tsconfig.json
{
"include": ["./src/*"],
"compilerOptions": {
"target": "ES2019",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
"resolveJsonModule": true,
"useUnknownInCatchVariables": false,
"typeRoots": ["./types", "./node_modules/@types"]
},
"types": ["jest"]
}
- 配置package.json
添加dev和build命令脚本,如下所示:
{
...
"main": "dist/index.js",
"type": "module", // rollup新增的配置项
"scripts": {
"dev": "rimraf dist && rollup -c rollup.config.ts -w",
"build": "rimraf dist && rollup -c rollup.config.ts"
},
...
}
- 编写demo代码
// sum.ts
export default function sum(a: number, b:number){
return a+b;
}
// index.ts
import sum from './sum';
console.log(sum(1, 2));
export default {
sum
}
- 测试运行
pnpm run dev
pnpm run build
查看生成dist/index.js 和 dist/index.esm.js。
完整项目源码地址:demo-rollup