30分钟搞懂Rollup+Typescript工程构建

7,660 阅读11分钟

背景

最近在研究一个ngptcommit命令行工具,然后想通过Rollup+Typescript去编译的时候,发现对RollupTypescript的编译配置有点陌生,所以希望通过本文能够对其有个系统的认知。

本文主要是项目编译基础知识,明白其为什么要这么配置,同时能够将项目完整跑起来。

参考项目地址为:node-gptcommit

为什么不用WebpackVite呢?因为这两个对于一个命令行工具来说有点过于重了,有点啥像杀鸡焉用宰牛刀的感觉。

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中配置entryFileNamesname的值
  • 最终会输出两个文件entry-a.jsentry-b/index.js

output

output相比较input会复杂很多,但是我们所关心的主要配置项如下:

  • output.dir, 构建好的代码文件放d到哪个文件夹中
  • output.file,针对单一入口,指定生成的文件名,如:index.esm.js index.cjs.js
  • output.format,按照哪个代码规范去生成,目前主要有:
    • cjs 为CommonJS规范,适用于 Node 环境和其他打包工具
    • es 为ES规范,适用于其他打包工具以及支持 <script type=module> 标签的浏览器
    • umd 通用模块定义,生成的包同时支持 amdcjsiife 三种格式
    • 其他有amd iife
  • output.globals,用来忽略打包(umd 或 iife 规范)后的代码的代码依赖,比如:代码中依赖jquery,且jquery在代码使用$标识,则可以配置:
{
...
output:{
    globals: {
        jquery: '$'
    }
}
...
}
  • output.name, 以umd 或 iife 规范打包后的代码,需要注册在全局对象中的名字
  • output.plugins,针对输出后的代码需要进行插件扩展,如:压缩代码
  • output.chunkFileNames,对代码分割中产生的 chunk 文件自定义命名,默认值是:[name]-[hash].js
  • output.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执行流程有一个完整的理解,如下图生命周期钩子函数所示:

image.png

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,就会解析成src
  • target 代码转义哪个ES标准下,如:ES2017,ES2018,ES2019等
  • module 代码模块化遵循哪个标准,如:ESNext,CommonJS等
  • strict 是否使用严格模式检测代码质量
  • lib 编译的有时候需要依赖一些全局变量,比如:Document对象,这个时候需要设置为DOM,或者使用Map对象,这个时候需要ESNext
  • declaration 是否给每个文件都生成 声明文件.d.ts
  • noImplicitOverride 设置后可以提醒继承类override同名方法时候,需要标注override关键字
  • noUnusedLocals 不允许有未使用的变量
  • esModuleInterop 可以修复由于ES规范和其他规范混合使用导致的引用错误
  • useUnknownInCatchVariables 支持catch中error设置为Unknown类型
  • resolveJsonModule 支持json文件引入为一个模块

RollupTypescript都有一定了解后,接下来我们就来实战Rollup+Typescript工程化项目。

实战

  1. 初始化项目 新建一个文件夹demo-rollup,后续命令如下:
mkdir demo-rollup

npm init
  1. 安装依赖
  • 安装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
  1. 配置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' }
        ]
    }
];
  1. 配置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"]
}

  1. 配置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"
  },
  ...
}
  1. 编写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
}
  1. 测试运行
pnpm run dev

pnpm run build

查看生成dist/index.jsdist/index.esm.js

完整项目源码地址:demo-rollup