说来惭愧,入职的第一个需求居然花了我两多个月?!😭

一、需求背景

刚入职的第一个需求是工程化需求,mentor 也是希望我能从工程化的角度切入去熟悉项目,起初单纯的我以为很快就可以上手,在入职初狠狠表现一波,却没想到这个需求最后用了两个月的时间才完成!!😭

在讨论具体需求和解决方案之前,我们先聊聊从这篇文章中能得到什么:

  1. 在面对知识体系空白的需求时该如何有效面对。
  2. 相关工程化的概念(文中会有很多外链)。
  3. monorepo 中的源码引用方法。
  4. 一些构建配置的细节知识点(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 是用于表示程序源代码的抽象语法结构 。其源代码的一个树状表示,其中每个节点代表源代码中的一个语法结构或表达式 。它捕捉了源代码的结构和语义,并且可以用于代码分析、优化、转换和生成等任务。
构建过程:

  1. 词法分析:将源代码拆分成一个个的标记(tokens),例如关键字、标识符、运算符、常量等。
  2. 语法分析:根据语法规则,将标记组织成一个抽象语法树。语法分析器会根据语法规则对标记序列进行解析,并构建出一个表示语法结构的树状结构。
  3. 构建抽象语法树:根据语法分析生成的解析树,将其转换为一个更抽象、更简化的树状结构,即抽象语法树。

更多的操作可以看看参考文章,里面讲述了实现原理,以及各类实践操作。

2.3 tsc

参考文章 :TypeScript 想更深入一层?自定义 transformer 的 compiler api 试一下
作用 :TypeScript compiler(tsc)是 TypeScript 的官方编译器。它将 TypeScript 代码转换为 JavaScript 代码,以便能够在各种 JavaScript 运行时环境中执行。

其中,TypeScript transformer是一种用于自定义编译过程的工具,实现更多的定制和扩展。

整个编译过程涉及到的 api 较多,一般都是有需要再查阅文档。想要查看具体的例子请看参考文章。

在学会 Babel、AST、tsc 的过程中会发现三者是串联的关系,此时已经有编写一些 plugin 的能力了,那么下一步就是去了解构建工具。

三、构建工具

3.1 模块化原理

在学习 构建工具 前,需要先熟悉一下模块化的内容。
参考文章 :从构建产物洞悉模块化原理
模块化作用:

  1. 组织和管理代码:通过将代码拆分为独立的模块,可以使代码结构更清晰、可维护性更高。
  2. 代码复用和共享:开发者可以将常用的功能封装为模块,然后在不同的项目中重复使用。
  3. 解决命名冲突:每个模块都有自己的作用域,模块内部的变量和函数不会与其他模块发生冲突。

参考文章中具体介绍了 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 或多包项目中指定包之间的依赖关系,以确保正确的构建顺序和类型检查。

同时它还有这些功能:

  1. 增量编译:当使用项目引用时,TypeScript 编译器可以只编译那些自上次编译以来发生变化的项目。
  2. 编辑器性能:使用项目引用可以改善编辑器的性能,因为编辑器可以仅加载需要的项目,从而减少内存占用并提高响应速度。

配置示例:

json 复制代码
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "dist",
    // alias
    "paths": {
      "@/*": ["src/*"]
    },
  },
  "references": [
    { "path": "./packages/package-a" },
    { "path": "./packages/package-b" }
  ]
}

注意事项:

  1. 所有被引用的项目都应该设置 "``composite``": true 在它们的 compilerOptions 中,这样它们才能被其他项目引用。
  2. 引用的项目会生成 .d.ts 声明文件,这些文件会被依赖它的项目使用,以了解类型信息。

在了解两个字段的作用后,我们再来通过一张图来搞清整个源码引用的流程:


我们可以看到还多了一个角色------转换路径的 webpack 插件,在阅读插件实现原理后,得知他的作用就是解析依赖路径为绝对路径。

至此,我们的实现方案可以初步的确定下来:

  1. 更换构建工具 rollup 为司内的构建工具(简称 x)
  2. 配置相关的 package.json、tsconfig.json 配置,实现导出、依赖、路径别名配置
  3. 参照主项目的 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,做自动包体积分析的玩意),但却是让我从一个工程化小白成功入门了企业级工程化配置。

与此同时,刚入职的童鞋们还是要谨记抱住师兄们的大腿!多多沟通!!!

当然对于刚入职的实习生千万不要被需求的完成速度所限制了,不同的部门有不同的要求,很多时候对于实习生的要求并没有多么严格,按照自己的节奏完成好需求其实已经很不错了(●'◡'●)

相关推荐
用户033307413956 小时前
TypeScript using 与 AsyncDisposableStack:在真实服务端代码中协调多资源清理
前端
daisychey6 小时前
用 Playwright 做多平台内容分发,我踩过的 8 个坑
前端
架构师那点事儿7 小时前
大模型如何私有化部署到生产环境
人工智能·架构·llm
IT_陈寒8 小时前
为什么你应该学习JavaScript?
前端·人工智能·后端
大龄秃头程序员8 小时前
iOS冷启动监控Demo
前端
lightning_bug8 小时前
Windows系统Docker+SpringBoot+H5+Mysql+Redis打包部署流程
后端·架构
光影少年8 小时前
为什么 JavaScript 中 0.1 + 0.2 !== 0.3,如何让其相等?
前端·javascript·算法
掘金酱8 小时前
[稀土掘金 × 火山引擎] AI用量周榜冲刺赛|获奖名单公示
前端·人工智能·后端
阿瑞IT9 小时前
企业知识库为什么越建越难用?从知识沉淀到权限检索的五种实现路径
程序员
泡泡oO9 小时前
“如果你还在用Superpowers,那我不要和你说话”
前端·后端·全栈