一、需求背景
刚入职的第一个需求是工程化需求,mentor 也是希望我能从工程化的角度切入去熟悉项目,起初单纯的我以为很快就可以上手,在入职初狠狠表现一波,却没想到这个需求最后用了两个月的时间才完成!!😭
在讨论具体需求和解决方案之前,我们先聊聊从这篇文章中能得到什么:
- 在面对知识体系空白的需求时该如何有效面对。
- 相关工程化的概念(文中会有很多外链)。
monorepo中的源码引用方法。- 一些构建配置的细节知识点(package.json、tsconfig.json)。
ps:文章主要是分享需求的处理过程,不会对某个大知识点做详细说明。
接下来我们直接说需求:
我们的一个 monorepo 项目中,SDK 子项目通过 alias 被其他项目引入(从源码引入而不是从dist)时无法模块化(alias本身很冗长),与此同时,目前的项目打包速度也偏慢,因此考虑更换 monorepo 源码引用的方式,同时更换 SDK 子项目的构建工具。
在这里我们通过图来进一步 get 这个需求:

对于alias的模块化,我们通过下面的代码理解一下:
js
// 模块化前
const aliasA = {
"@monorepo/sdk/src/modules/ui/component-a/*": "./src/module/ui/componet-a/*",
"@monorepo/sdk/src/modules/ui/component-b/*": "./src/module/ui/componet-b/*",
};
// 模块化后
const aliasB = {
"$componet-a/*": "./src/module/ui/componet-a/*",
"$componet-b/*": "./src/module/ui/componet-b/*",
};
此时此刻,这种需求给我(工程化小白)带来的压力难以言喻,只能硬着头皮上了!
首先基于当下的认知分析项目现状:
- 基本架构为
monorepo - SDK 的打包工具为
Rollup - alias 的处理涉及到 构建工具配置、tsconfig配置、package配置
与此同时,mentor 提出可以参照司内主项目的源码引用方式,开干!
在主项目项目A对项目B-SDK的引用方式中 dev 和 prod 的构建方案有一定的区别,初步判断,其中运用到的知识点可能有 Babel、TypeScript Compiler、Webpack、gulp,同时还涉及到手写构建工具插件。
与此同时,组内的大哥还提了一嘴(司内的构建工具或者 tsup 也很不错啊!)------ 好好好,也调研一下!
综上,可以总结出现在需要去深入的 map:

这里计划先从 Babel、AST、tsc 深入,理解 Plugins 的开发后再去研究各类打包工具的细节。
大佬请移步第四点~

二、编译工具与抽象语法树
这一部分记录 Babel、AST、tsc 的总结。因为存在较大的知识体系,有些点会写的比较简略。
2.1 Babel
参考文章 :一口(很长的)气了解 babel
作用 :帮助开发者能够在低版本环境中(浏览器、node)使用最新的JavaScript 语言特性。通过将新版本的 JavaScript 代码转换为向后兼容的旧版本。
插件系统 :Babel 具有可扩展的插件系统,允许开发者根据项目需求选择和配置转换功能。插件可以用来添加、删除或修改转换规则 。
配置文件 :Babel使用一个名为 .babelrc 的配置文件,用于指定转换规则、插件和其他选项 。开发者可以根据项目的需要创建和配置自己的 .babelrc 文件。
preset :一组插件的集合,开发者可以通过使用预设来快速配置 Babel 的转换行为,而不需要一个个手动添加和配置各个插件。例如,@babel/preset-env 是一个常用的 Babel 预设,它根据目标环境的配置,自动选择需要的转换规则,以便将较新的 JavaScript 语法转换为目标环境兼容的代码。
2.2 AST
参考文章 :前端工程化基石 -- AST(抽象语法树)以及AST的广泛应用🔥
作用 :AST 是用于表示程序源代码的抽象语法结构 。其源代码的一个树状表示,其中每个节点代表源代码中的一个语法结构或表达式 。它捕捉了源代码的结构和语义,并且可以用于代码分析、优化、转换和生成等任务。
构建过程:
- 词法分析:将源代码拆分成一个个的标记(
tokens),例如关键字、标识符、运算符、常量等。 - 语法分析:根据语法规则,将标记组织成一个
抽象语法树。语法分析器会根据语法规则对标记序列进行解析,并构建出一个表示语法结构的树状结构。 - 构建抽象语法树:根据语法分析生成的
解析树,将其转换为一个更抽象、更简化的树状结构,即抽象语法树。
更多的操作可以看看参考文章,里面讲述了实现原理,以及各类实践操作。
2.3 tsc
参考文章 :TypeScript 想更深入一层?自定义 transformer 的 compiler api 试一下
作用 :TypeScript compiler(tsc)是 TypeScript 的官方编译器。它将 TypeScript 代码转换为 JavaScript 代码,以便能够在各种 JavaScript 运行时环境中执行。
其中,TypeScript transformer是一种用于自定义编译过程的工具,实现更多的定制和扩展。
整个编译过程涉及到的 api 较多,一般都是有需要再查阅文档。想要查看具体的例子请看参考文章。
在学会 Babel、AST、tsc 的过程中会发现三者是串联的关系,此时已经有编写一些 plugin 的能力了,那么下一步就是去了解构建工具。
三、构建工具
3.1 模块化原理
在学习 构建工具 前,需要先熟悉一下模块化的内容。
参考文章 :从构建产物洞悉模块化原理
模块化作用:
- 组织和管理代码:通过将代码拆分为独立的模块,可以使代码结构更清晰、可维护性更高。
- 代码复用和共享:开发者可以将常用的功能封装为模块,然后在不同的项目中重复使用。
解决命名冲突:每个模块都有自己的作用域,模块内部的变量和函数不会与其他模块发生冲突。
参考文章中具体介绍了 CommonJS 和 ESM 的作用、语法,以及实现原理,同时涉及到了许多相关的常考问题,可以仔细阅读一下。
3.2 构建工具
作用 :帮助开发者自动化和简化了许多繁琐的任务,提高了开发效率和代码质量。
可以直接参考以下文章,按照流程实践一遍即可。
Webpack参考文章 :🔥【万字】透过分析 webpack 面试题,构建 webpack5.x 知识体系
Gulp参考文章 :Gulp快速入门教程
Rollup参考文章 :一文带你快速上手Rollup
tsup参考文章 :使用 tsup 创建你的全新现代 TypeScript/JavaScript 库
以下是对这几类构建工具的表格总结:
| 工具 | 优点 | 缺点 |
|---|---|---|
| Webpack | - 插件生态系统丰富 - 支持热模块替换 (HMR)和代码分割 - 大型项目优先考虑 | - 配置复杂 - 构建速度较慢 - 需要额外的配置和插件来支持某些功能 - 生成的打包文件较大 |
| Gulp | - 简洁易用的API和链式操作 - 构建速度快,适用于小型项目 - 丰富的插件生态系统 - 支持增量构建 和并行执行任务 - 任务配置和自定义选项很灵活 | - 不适用于复杂的依赖关系处理和模块化打包 - 需要手动编写 gulpfile.js 配置文件来定义任务和处理文件 - 不支持代码分割 和热模块替换(HMR) |
| Rollup | - 专注于 ES模块 的打包和优化,生成更小更高效的打包文件 - 构建速度快 - 支持 Tree Shaking,消除未使用的代码 - 支持代码分割和动态导入 - 适用于库和组件的打包和发布 |
- 对非 ES模块 的资源处理较弱 - 插件生态系统相对较小 - 配置相对较少和简单 |
| tsup | - 极简的配置和使用 - 构建速度非常快 - 适用于构建纯 TypeScript 项目 - 支持代码分割 |
- 功能相对较少,仅适用于纯 TypeScript 项目 - 插件生态系统相对较小 |
这边对一些名词做一下解释:
- 热模块替换:在运行时替换模块的代码,而无需刷新整个应用程序。
- 代码分割:将应用程序的代码拆分成多个较小的块(chunks),而不是将整个应用程序打包成一个单独的文件。(减少初始加载时间)
- 增量构建:在每次构建时,只重新构建已更改的文件,而不是重新构建整个项目。
- Gulp的链式操作:在Gulp任务中使用连续的方法调用来定义任务的处理流程,举例如下:
js
const gulp = require('gulp');
const sass = require('gulp-sass');
const autoprefixer = require('gulp-autoprefixer');
const cleanCSS = require('gulp-clean-css');
gulp.task('styles', function() {
return gulp.src('src/scss/*.scss') // 指定要处理的scss文件
.pipe(sass()) // 编译scss为css
.pipe(autoprefixer()) // 添加浏览器前缀
.pipe(cleanCSS()) // 压缩CSS
.pipe(gulp.dest('dist/css')); // 输出到目标文件夹
});
四、技术方案形成
在过完一遍 map 之后,下一步则是带着目的性去阅读项目A和项目B-SDK的配置方式,去形成我们的技术方案。
4.1 司内主项目的构建方式和源码引用方案
我们先来看看两个项目的构建方式
- 项目A:司内的构建工具,内核基于
Webpack - 项目B-SDK:
Gulp+Bable
其中 项目B-SDK 通过在 package.json 中配置 export 字段导出路径,与此同时 项目A 中的 tsconfig.json 中的 references 配置字段指向了 项目B-SDK 依赖。
听起来是不是有点绕,我们先来解释一下这些字段的作用:
解释exports
通过在 package.json 文件中的 exports 字段中指定特定的属性,可以定义模块的导出路径。
json
{
"name": "my-package",
"exports": {
".": {
"import": "./src/index.js",
"require": "./dist/index.js",
"default": "./dist/index.js"
},
"./utils": {
"import": "./src/utils.js",
"require": "./dist/utils.js"
}
}
}
. 表示默认导出,而 ./utils 表示一个特定的导出路径
我们再结合 monorepo,给出案例:
json
项目结构
my-monorepo/
packages/
package-a/
package.json
src/
index.js
package-b/
package.json
src/
index.js
package-a/package.json
json
{
"name": "package-a",
"exports": {
"./count": "./src/count.js"
}
}
package-b/src/index.js
js
import { someFunction } from 'package-a/count';
注意,别名的引用要通过 tsconfig 配置(compilerOptions 中的 path)
什么是 references
tsconfig.json 文件中的 references 字段来管理项目的依赖关系和构建顺序。references 字段允许在一个 Monorepo 或多包项目中指定包之间的依赖关系,以确保正确的构建顺序和类型检查。
同时它还有这些功能:
- 增量编译:当使用项目引用时,TypeScript 编译器可以只编译那些自上次编译以来发生变化的项目。
- 编辑器性能:使用项目引用可以改善编辑器的性能,因为编辑器可以仅加载需要的项目,从而减少内存占用并提高响应速度。
配置示例:
json
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist",
// alias
"paths": {
"@/*": ["src/*"]
},
},
"references": [
{ "path": "./packages/package-a" },
{ "path": "./packages/package-b" }
]
}
注意事项:
- 所有被引用的项目都应该设置
"``composite``": true在它们的compilerOptions中,这样它们才能被其他项目引用。 - 引用的项目会生成
.d.ts声明文件,这些文件会被依赖它的项目使用,以了解类型信息。
在了解两个字段的作用后,我们再来通过一张图来搞清整个源码引用的流程:

我们可以看到还多了一个角色------转换路径的 webpack 插件,在阅读插件实现原理后,得知他的作用就是解析依赖路径为绝对路径。
至此,我们的实现方案可以初步的确定下来:
- 更换构建工具
rollup为司内的构建工具(简称 x) - 配置相关的
package.json、tsconfig.json配置,实现导出、依赖、路径别名配置 - 参照主项目的
webpack插件,实现引用方的alias引入
对于更换构建工具,要考虑到功能需求、学习曲线、生态支持和性能表现。
在经过调研后,x 在构建方面更为便捷,同时速度上有很大的优势,demo 测试的打包时间如下:
- Rollup:3m 39.7s
- Rollup + rollup-plugin-esbuild :1s
- Gulp:after 10 min
- EdenX:0.2s
- Tsup: 0.314s
在确定方案后,下一步则是准备技术评审 。
在评审后,我的需求获得了巨大的"进步",在处理 SDK 需求的基础上,还要将引用方A的构建工具迁移到最新版本,与此同时,还希望我构建一条线上发包的流水线,最后在群里通过机器人进行发包通知。

好家伙,干就干吧,这需求要没完了🫠。
评审结束后马上又进行了第二次评审准备,再把整体需求方案确定好后,接下来就要开工了,从调研到评审,就花掉了三周的时间,此时此刻生无可恋🫠 。
五、方案实施
依据整个需求,我拆分成了三个独立需求:
5.1 SDK 构建工具替换、monorepo源码引用
这个需求其实就是之前长篇大论的部分,其中对于构建工具的替换暂且忽略(没啥好说的,虽然挺坐牢),我们主要讲讲我是如何实现了 monorepo 的源码引用方案。
alias 配置
首先需要调整 alias 的配置,我们只需要在 SDK 的 tsconfig 中配置 path 字段,即可实现别名引用(这个是给编译工具读取的),与此同时,SDK 的构建工具 x 内置了自动读取 tsconfig 配置处理为自己的 alias 配置的功能,因此无需在 x 中额外配置 alias。
js
"@sdk/*": "./src/*",
"$componet-a/*": "./src/module/ui/componet-a/*",
"$componet-b/*": "./src/module/ui/componet-b/*",
与此同时,我们可以在 A 中的 tsconfig 配置 SDK 包的别名引用,这样可以让 lint 的 fix 支持从 SDK 引入(说白了就是让编译器读懂,虽然 export 也做到了这个功能,但是没法让 lint 获取)
js
"@/*": "./src/*",
"@sdk/*": "../sdk/src/*",
export 配置
为要在 SDK 中导出路径,我们需要在 export 中配置相关的路径转发,具体的可以参照前文的字段解释。
reference 配置(非必要配置)
在 A 中的 tsconfig 配置依赖 SDK,具体的效果参照前文解释
*alias处理的插件实现
参考主项目的插件,我自己手写实现了一个类似的 webpack 插件。
如何写一个 webpack 插件:干货!撸一个webpack插件(内含tapable详解+webpack流程) - 掘金 (juejin.cn)
具体的插件逻辑将通过代码结合注释说明:
js
class typescriptAliasPlugin {
// 获取构建配置中传入的数据
constructor(){
// 获取根目录路径
}
// 插件执行内容
apply(compiler:Compiler){
// 通过 hooks 约定执行的时机 afterResolve
// 获取 package.json 中 devDependencies 的包内容(依赖项)
// 遍历包名,在 node_module 中获取项目的 tsconfig (pnpm 有软连接)
// 提取 tsconfig 中的 path 字段,注入 compiler 的 alias 配置中(注册到全局 alias)
}
}
这个插件帮助 A 的构建工具得以读取到 SDK 的 alias,并注册到自己的 alias 中,使得在 A 中能通过 alias 直接引用到 SDK 的源码进行开发。
至此,第一个需求的开发就接近了尾声,后续则是一系列的 build、start 的测试(痛苦地开始)。
5.2 A 项目构建工具升级
这一块其实是最折磨的,在升级之后会存在一系列的配置差异、编译报错,需要逐步修改,同时打包策略的不同还要通过相关配置去对其,来来回回花了将近一个月的时间(哭死
方便起见,我把整个流程通过代码块展示一下:
js
graph LR
A[升级工具一键迁移] -->|对照配置映射表修改| B(尝试 build、start)
B -->|修改配置| C(获得匹配的打包产物、html模板,依据报错修改业务代码 type error)
C -->|配置 proxy,通过正则重定向请求| D(使得 start 后能通过代理访问)
D -->|依据打包策略变更| E(修改 pipeline 以及相关的脚本文件)
E -->|线上部署测试| F(分析 html 中 cdn 数量、包体积变化,做调整)
整个过程其实更为复杂,一坑埋好又一坑!!!
5.3 配置发包流水线 + 群机器人通知
这一块就相对轻松了,只需要依据司内提供的流水线平台搭建一个从打包到发布的模板,再通过群token,配置相关的机器人通知即可。
六、感悟
相对于技术文,这篇文章更注重分享解决需求的过程,当然对于工程化大佬而言就像一篇过家家的文章吧。

虽然整个需求开发过程充满了坎坷(其实还有些没完成,因为一些因素还要升级 lint,做自动包体积分析的玩意),但却是让我从一个工程化小白成功入门了企业级工程化配置。
与此同时,刚入职的童鞋们还是要谨记抱住师兄们的大腿!多多沟通!!!
当然对于刚入职的实习生千万不要被需求的完成速度所限制了,不同的部门有不同的要求,很多时候对于实习生的要求并没有多么严格,按照自己的节奏完成好需求其实已经很不错了(●'◡'●)