前端工程化配置,工程能力学习
前端工程化配置,工程能力学习
月晕工程治理
工程管理
GIT
ssh key config
ssh(secure shell)是一种加密网络传输协议,用于在不安全的网络环境中提供安全的方式来访问网络服务,SSH 协议主要是用来远程登录、管理系统、安全传输文件等,在数据传输的过程中采用加密保证数据的安全性和完整性
ssh 协议通信的基本流程:
- SSH 版本协商阶段:服务器监听 22 端口信息,客户端发送 TCP 连接,建立请求,服务端返回一个 SSH 信息给客户端判断
- 密钥和算法协商阶段
- 客户端认证
- 会话请求
- 交互会话
ssh:SSH 客户端实现 & scp, sftp:rcp 的替代方案,传输文件 & sshd:SSH 服务端的视线 & ssh-keygen:产生 RSA 或 ECDSA 密钥,用来认证 & ssh-agent, ssh-add:帮助用户不需要每次输入密钥或密码的工具 & ssh-keysacn:收集大量主机的 ssh 主机公钥
Ssh-keygen 参数:-C (comment) - 提供一个注释 -t (type) - 提供生成的密钥类型 -b (bits) - 指定要生成的密钥长度 -f (filename) - 生成密钥文件名
git 中配置 ssh 的步骤
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
生成密钥对在~/ssh
目录下面,在 github 或者 gitlab 上面去增加 ssh key 去配置公钥即可完成配对
测试 SSH 连接:ssh -T git@github.com
GIT 版本管理中的指令操作
详细的查看我的另一篇博客
依赖管理
npm
嵌套的 node_modules 结构:
npm 早期是采用嵌套的 node_modules 结构,会将依赖直接平铺在 node_modules 文件夹下,子依赖会直接嵌套在 node_modules 下面如下
1 | node_modules |
如果此时 D 模块也依赖与 B 则会在添加一次,在真实的开发下会导致 node_modules 臃肿不堪,依赖地狱
扁平化 node_modules 结构:
为了将嵌套的依赖尽量打平,避免过深的依赖树和包冗余,npm v3 将子依赖提升(hoist),采用扁平化的 node_modules 结构,子依赖会尽量平铺安装在主依赖所在的目录中
1 | node_modules |
将 B 依赖提升至相同的层级,不同版本则单独安装,这样既不会导致大量包的安装,也可以解决依赖地狱的问题,但是其中也存在很多问题
幽灵依赖 Phantom dependencies
幽灵依赖是指在 package.json 中未定义的依赖,但是依然可以正常的被引入使用
就比如我们只依赖 A、C 但是 C 中依赖了 B,B 被提升到了相同的层级,依然可以使用,如果某天 C 中改版不依赖 B 了,那么使用到 B 的地方可能导致报错
1 | { |
不确定性 Non-Determinism
不确定性指的是:同样的 pkg.json 文件,install 依赖后可能会得到不同样的 node_modules 目录结构
比如 A 依赖B@1.0.0,但是 C 依赖B@2.0.0 ,那么 install 后提升目录的是B@1.0.0还是@B2.0.0? 取决于用户安装的顺序,很可能导致生产环境和开发环境不一样
依赖分身 doppelgangers
假设此时的情况是:A 和 D 依赖B@1.0 ,C 和 E 依赖B@2.0
下面是提升B@1.0的情况
1 | node_modules |
这样 B 会安装两次,无论提升那个都会导致被安装两此,且引入的 B 不是相同的 B,这样可能对 B 做一些缓存和副作用的时候,会导致出错
yarn
提升安装速度
npm 中安装依赖时,安装任务是串行的,会按顺序包逐个下载,为了解决安装速度问题,yarn 采用了并行安装,且存在缓存机制,存在磁盘中
lockfile 引入
引入 yarn.lock 文件,记录依赖,子依赖,获取地址等一些关键的元信息,即使不同的安装顺序,相同的依赖关系在任何容器和环境中,都能得到稳定的 node_modules 目录结构,保证安装依赖的确定性
依然没能解决幽灵依赖和依赖分身的问题
pnpm
定义为快速的、节省磁盘空间的包管理工具,建立了一套新的依赖管理机制
内容寻址存储 CAS
与依赖提升和扁平化的 node_modules 不同,pnpm 引入了另一套依赖管理策略:内容寻址存储
该策略会将包安装在全局的 store 中,依赖的每个版本只会在系统中安装一次
在引用项目 node_modules 的依赖时,会通过硬链接与符号链接在全局的 store 中找到这个文件,为了实现此过程,node_modules 下会多出 .pnpm
目录,而且是非扁平化结构。
- 硬链接 Hard Link: 硬链接可以理解为源文件的副本,项目里安装的其实是副本,它使得用户可以通过路径引用查找到源文件,而且这个副本根本不占任何空间。同时,pnpm 会在全局 store 里存储硬链接,不同的项目可以从全局 store 寻找到同一个依赖,大大地节省了磁盘空间
- 符号链接 Symbolic link :也叫软连接,可以理解为快捷方式,pnpm 可以通过它找到对应磁盘目录下的依赖地址。
这样的机制十分巧妙可以解决大部分问题了
缺点:在不支持软链的环境中无法使用,比如 Electron,因为依赖源是在 store 中,调试依赖,修整依赖会有比较大的影响
monorepo
代码规范
TypeScript 代码规范
命名
- 标识符必须只能使用 ASCII 字母、数字、下划线组成
- 类、枚举、类型命名使用大写驼峰(UpperCamelCase)
- 变量和函数命令使用小写驼峰(lowerCamelCase)
- 全局常量使用
CONSTANT_CASE
- 枚举值命令使用大驼峰或
CONSTANT_CASE
- 名称中不要出现对应的类型标识和 my 之类的(比如 nameString myType … 等)
- 使用描述性名称,名称应该是描述性和清晰的,不要使用缩写或者删除单词中某些字母等的方式
语言特性
字符串
- 使用
String#startWith
和String#endWith
和includes
和slice
等现代方法
数组
- 禁止使用 Array()构造函数,直接使用字面量或者
Array.from
方法 - 建议使用
for....of
方法来遍历数组,更快,更好的可读性,break
和return
可以提前退出
枚举
- 所有的枚举成员必须是字面量,不能是表达式之类的
- 禁止枚举成员的值重复
函数
- 函数重载签名必须是连续的
- 通过联合类型或可选/剩余参数解决的函数不能使用函数重载
- 禁止使用 this 别名,可以通过箭头函数去解决问题
接口和类
- 类成员:禁止使用
#
声明私有字段,使用private
字段, 建议使用readonly
,限制属性,方法,整个类型的可见性,方便更清晰的代码结构,可维护的设计,默认情况是public
不用添加,对私有的添加private
和protected
- 接口和类声明使用 ; 来分隔,单个成员声明,内联对象等都使用 **;**来声明
- 接口和对象中的方法必须使用属性签名而不是方法签名
- 禁止在类中定义 new 方法,接口中声明 constructor 方法
类型断言和非空断言
类型断言和非空断言是不安全的,使用之后会让 TypeScript 编译器静默,不会插入任何运行时检查来匹配这些断言,可能导致程序运行时崩溃,因此没有必要不要使用,使用判断和 instanceof 去完成替代上面的逻辑
类型断言使用
as
语法必须使用类型注释而不是类型断言来指定对象字面量的类型
不要使用非空断言
可以在
tsconfig
中去启用strictNullChecks
选项禁止使用多余的非空断言
禁止在可选链之后写非空断言
禁止在空值运算合并的左侧操作数中使用非空断言
注释指令
禁止使用 _
@ts-ignore
、@ts-nocheck
、@ts-check
_,可以使用@ts-expect-error
代替特殊情况下可以使用
@ts-expect-error
,用注释指令抑制编译错误,会掩盖背后更大的问题。最佳实践是使用正确的代码类型。但有些情况下,也有无法解决的类型问题,比如第三方依赖没有完备的类型声明或者错误的类型。
禁止类型始终为真或者始终为假的变量作为条件
使用可选链式表达,不要使用&& 或者 || 去替代
类型系统
- 禁止使用大写的内置对象作为类型:某些内置对象用作类型时被认为是危险的或有害的,通常的做法是禁止这些类型的使用以确保一致性和安全性
- 能简单推断类型的代码,禁止声明类型,其余必须声明类型:能推断出的类型是声明的代码初始化的
string
、number
、boolean
、RegExp
字面量 - 使用返回类型
- 在导出函数和类的公共方法上必须定义返回和参数类型
- 禁止使用字面量做为用户的类型
- 为对象声明类型的时候必须使用
interface
而不是type
- 禁止使用 any,可以采用更具体的类型,unknown,抑制 linter 警告并记录原因
- 禁止在泛型上使用无效的类型约束
- 禁用声明冗余的类型
- 禁止在泛型和返回类型之外使用 void 类型
文件结构
- 禁止使用命名空间
namespaces
在 ts 文件中 - 禁止在导入模块的时使用require语句
React 代码规范
命名
- 单文件的组件名应该使用
UpperCamelCase
或kebab-case
- 使用 JSX 语法的文件扩展名应该是
.jsx
或者.tsx
- JSX 组件命名的方式应该采用
UpperCamelCase
- JSX 组件名由多个单词组成
代码格式化
- 非函数式组件的创建采用 ES6 class
- 禁止不必要的 JSX 表达式
- 禁止给没有子组件的组件添加额外的结束标签
- JSX 组件中的函数不应超过 300 行:遵循单一职责,分离业务状态和 UI 显示
特性
- 禁止将注释作为文本节点插入
- 禁止在 JSX 中直接使用未转义的 HTML 实体
- 禁止出现定义了但未使用的状态
- 禁止在组件内创建不稳定的组件,单独开成一个组件引入,不然每次渲染都会导致引用发生变化,不必要的性能消耗
- 在使用
useEffect
、useMemo
或useCallback
等 Hook 函数时应该提供正确的依赖项数组
组件
- 迭代使用组件的时候必须执行 key 属性
- 不要使用数组索引作为组件的
key
- 禁止将子节点通过组件
children
属性传递 - 组件的
style
属性值必须是一个对象
Eslint
Prettier
Husky
编译构建
编译构建工具
Webpack
基本概念
Webpack 是一个现代的 JavaScript 应用程序的静态模块打包器(module bundler),通过分析目标结构,找到 JavaScript 模块以及其项目中一些高级的扩展语言(SCSS,TypeScript),将其转换和打包为合适的格式提供给浏览器使用
即整个过程完成了内容转换 + 资源合并 两种功能
基本术语介绍
- Entry: 构建的起点指定从那个路径开始构建其内部依赖关系图,可以存在多个入口
- Output:指定 bundles 命名和输入路径
- Loaders:用于 Webpack 只能理解 JavaScript 和 JSON 文件,帮组 webpack 处理其他类型的文件并转换
- Plugin:用于 webpack 的拓展,在 webpack 流程中存在很多的 hooks,能够执行更广泛的任务,例如 bundle 优化,资源管理,环境变量注入等
- Mode:环境,用于不同的优化策略
- Module:构建应用单元,在 webpack 中,一切文件皆被视为一个模块,webpack 从 Entry 出发,去递归构建一个包含所有依赖文件的模块网络
- Chunk:代码的集合体,Chunk 由模块合并而成,被用来优化输出文件的结构,Chunk 使得 Webpack 能够灵活的组织和分割代码
- initial 是入口点的主要 chunk,包含为入口指定的所有 module 及其依赖
- non-intial 是一个可能被延迟加载的 chunk,使用动态导入或 SplitChunksPlugin 时可能出现
编译过程
- 初始化阶段
- 初始化参数:从配置文件、配置对象、Shell 参数中读取,与默认配置结合得出最终的参数
- 创建编译器对象:用上一步得到的参数创建 Compiler 对象
- 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等
- 开始编译:执行 compiler 对象的 run 方法,创建 Complition 对象
- 确定入口:根据配置中的 entry 找出所有的入口文件,调用
compilition.addEntry
将入口文件转换为dependence
对象
- 构建阶段
- 编译模块(make):从
Entry
文件开始,调用loader
将模块转译为标准 JS 内容,调用 JS 解析器将内容转换为 AST 对象,从中找出该模块依赖的模块,在递归本步骤直到所有入口依赖的文件都经过本步骤的处理 - 完成模块编译:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的依赖关系图
- 编译模块(make):从
- 生成阶段
- 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk
,在把每个Chunk
转换成一个单独的文件加入到输出列表 - 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
- 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
编译的开始
Webpack 的执行入口在/lib/webpack.js 中
1 | const webpack = (options, callback) => { |
webpack.create() 核心逻辑:
- validateSchema 校验 webpack.config.js 的参数
- 调用 createCompiler(options)方法创建 compiler 对象
- 返回 compiler、watch、watchOptions
其中,compiler.run(callback)
的执行即开启了 webpack 的编译过程
compiler 的创建过程
createCompiler
函数实现如下
1 | const createCompiler = (rawOptions, compilerIndex) => { |
crateCompiler 核心流程:
- 调用 getNormalizedWepackOptions + applyWebpackOptionsBaseDefaults 对 webpack.config.js 对 options 进行规范化和默认值的操作
- new compiler() 初始化 compiler 实例
- new NodeEnviromentPlugin 调用内置插件,把 node 环境变量绑到 compiler 实例上
- 遍历 options.plugins,注册所有自定义的 plugin
- 调用 enviroment,afterEnvironment 生命周期回调
- new WebpackOptionsApply().process(options, compiler),进行各种 options 解析
- 调用 initialize 生命周期回调方法,说明初始化已经执行完成
- 返回 compiler 实例
WebpackOptionsApply()
现在了解一下WebpackOptionsApply().process(options, compiler)
方法,其主要作用是解析 Webpack.config.js 配置,数十个插件在该方法中完成注册,在合适的时机运行 plugin 插件,注册,运行
1 | class WebpackOptionsApply extends OptionsApply { |
WebpackOptionsApply() 核心流程:
new xxxPlugin().apply(compiler)
的写法注册内置插件,用以解析 webpack.config.js 配置,同时传入 compiler 实例- 对入口、运行时等进行处理,例如
new EntryOptionPlugin().apply(compiler)
在执行compiler.run()
之前,做了充足的准备,接下来才会进入编译阶段
Compiler.run
1 | run(callback) { |
整个函数实现,触发了很多的 hooks 比如 beforeRun、run、afterDone 等,其中比较重要的就是 this.compile(onCompiled)函数
调用 compiler.compile()
对核心方法Compiler.compile(onCompiled)
方法继续查看
1 | // 启动编译流程 |
钩子的执行顺序beforeCompile - compile - make - finishMake - afterCompile
,整个编译是编译前 - 编译后
其中比较最核心的就是 compile 和 make 阶段,其中compile
在函数中首次实现了compilation
实例的创建
compile 和 compilation 的区别:
Compiler: webpack 的核心,贯穿于整个构建周期,Compiler 封装了 Webpack 环境的全局配置,包括但不限于配置信息、输出路径等
Compilation: 表示单次的构建过程及其产出,与 compiler 不同的是,compilation 对象在每次构建中都是新创建的,描述了构建的具体过程,包括模块资源,编译后的产出资源,文件变化,依赖关系状态,每当文件变化触发重写构建时,都会生成新的
Complation
实例
Compiler
是一个长期存在的环境描述,贯穿整个 Webpack 运行周期;而Compilation
是对单次构建的具体描述,每一次构建过程都可能有所不同
重点关注 make 阶段,但是我们并没有在 make 中看到关键的函数执行,即相关的逻辑应该是通过 hooks 去触发的,在全局中搜索compiler.hooks.make.tapAsync
去查看 make 编译的逻辑
1 | compiler.hooks.make.tapAsync('EntryPlugin', (compilation, callback) => { |
通过查找我们发现它在 EntryOptionPlugin 中被实例化,再搜索 EntryOptionPlugin,发现在 WebpackOptionsApply 中被引入,很显然,这个插件在 compiler.run()
之前就被注册好了
添加 Entry
看看 EntryPlugin 中的compilation.addEntry
完成了什么
1 | function addEntry(context, entry, optionsOrName, callback) { |
可以了解到这里的工作就是在处理 Entry,Entry 的添加过程中会调用addModuleTree()
,根据代码依赖递归构建模块树(Module Tree)
添加到 Module Tree
Rollup
Vite
babel
其它工具
esbuild swc bun