工程化的范畴

广泛来说涵盖防范面面,具体说核心包括:模块化、组件化、规范化和自动化。
不过,其实任何方面都可以纳入工程化的范畴,毕竟前端工程也属于软件工程。在实际开发每一个环节中,都要用工程思维去解决问题,故而是一个很大的体系。
开发前的需求阶段,需求的评审,人员的划分和排期,日程和里程碑的规划和确认;
开发设计上,要做到高内聚低耦合,做到可复用,可扩展,高健壮,高性能,可测试;
在项目构建上,要合理选择打包工具,在项目性能、功能、兼容性,开发体验方面都要做好选择;
在开发编码上,要做到团队内代码规范(文件组织、命名、风格统一),版本控制工具和工作流统一;还有兼顾实现和兼容性的性能和问题,必要的模块还需要做好单元测试;
在开发完成时,先自测保证基本功能,再提测;另外,团队要做好Code Review,以进一步保证代码质量;
在部署方面,如今流行的都是自动化的持续集成和持续部署平台,这种几乎一劳永逸的平台在搭建好了之后能大大提升工程效率;
最后,在功能上线之后也不是没事了,需要日志来排错或分析,需要监控来性能分析;需要埋点来做关键数据的收集,以此来指导后续的产品走向;
这样在工程化方面,能持续形成闭环。所以说工程化是一个极为宽阔的领域。

JS模块化

“模块”指的是编程语言提供的一种组织代码的机制,可以将代码拆分为独立且通用的单元。
实现模块化的思路有:

  • 所有内容返回对象包裹:问题:可以被外部改写
  • IIFE立即执行函数:利用闭包特性,把需要使用的返回

    模块规范

  • CommonJS

CommonJS是一个规范集合,其初衷是为了标准化一套JS在浏览器之外的运行环境。所以,在CommonJS中规范化了File System、Stream、Buffer、Module等。

  • 引用模块用require函数;
  • 导出模块用exports;
  • exports是当前文件上下文的一个对象,用来对应挂上某个文件的导出内容,所以可以exports.xxx = ‘xxx’。这里还需要说明的一点是,每一个文件都存在一个module对象,用来指代文件自身。而上面说的exports本质是module对象的一个属性。
  • 输出的东西是值的拷贝,所以,当这个值被输出之后,就算模块内的值发生了变化,被输出的值也不会更新;
  • 多次引入同一个模块,第一引入之后会被缓存,后面的引入都会先在缓存中读取;
  • 同步按顺序加载多个模块;
    • AMD

AMD是Asynchronous Module Define 的缩写。
Async很明显的是异步的意思,也就是这是一种异步加载的模块化规范。
为什么要异步加载呢?CommonJS处理的Node端,是Server端,Server端同步加载无所谓,但是如果在浏览器端的话,同步加载多文件可能会导致白屏等待时间过长

  • CMD

CMD(Common Module Definition),和AMD的区别主要有两点:

  • CMD,不完全是异步加载,支持同步require和异步require.async;
  • AMD是依赖前置的,就是你在模块头部先声明出模块中要使用的依赖;但是在CMD中,随时需要随时引入;
  • UMD

UMD(Universal Module Definition)的出现最初是为了解决大家在代码混用CMD、ADM甚至CommonJS的问题,既然大家混用,那索性就都支持好了。
所以他做的最核心的事情就是,判断当前环境支持那种方式

  • ES Module

ESM是静态引入的,所谓的静态,就是编译时,就能确定模块的依赖关系。CommonJS就不是静态的,它必须到运行的时候才能确定关系。
export 用来导出模块接口,import 用来引入模块接口。export 可以直接导出也可以集中导出一个对象,只是写法不一样,实质是一样的。
ES2020 中,新引入了 import() 动态加载模块,该方法返回的是一个 Promise 对象。

package.json

现代前端项目中,都有 package.json 文件,它是项目的配置文件:

  • 基本信息:
    • name:名字,如要发布则要保证唯一;
    • version: 主版本号. 次版本号. 修订号, 内部版本(alpha)、公测版本(beta)和候选版本(rc,即 release candiate
    • description:描述信息
    • keywords:关键词
    • author、contributors :作者、贡献者
    • repository:代码的存放仓库地址
  • 依赖信息

    • 版本规则
      • 固定版本:安装时只安装这个指定的版本;
      • 波浪号:比如~ 4.0.3,表示安装 4.0.x 的最新版本(不低于 4.0.3),也就是说安装时不会改变主版本号和次版本号;
      • 插入号^:比如 react 的版本 ^17.0.2,表示安装 17.x.x 的最新版本(不低于 17.0.2),也就是说安装时不会改变主版本号;
      • latest:安装最新的版本
    • engines:对 npm 包的版本或者 Node 版本有特殊要求,比如 node:”>=10.0.0”

    • dependencies:项目的生产环境中所必须的依赖包;(默认安装或者 —save 参数都会写入此下)

    • devDependencies: 声明的是开发阶段需要的依赖包,如 Webpack、Eslint、Babel 等,用于辅助开发,无需在生产环境中运行代码(—save-dev)
    • peerDependencies : 用来供插件指定其所需要的主工具的版本,比如xVitePlugin针对的vite1.0就需要声明vite:^1.x.x
    • optionalDependencies:需要在找不到包或者安装包失败时,npm 仍然能够继续运行,则可以将该包放在 optionalDependencies 对象中;
    • bundledDependencies: 配置项是一个数组,数组里可以指定一些模块,这些模块将在这个包发布时被一起打包
  • 脚本scripts:配置 scripts 属性,可以定义一些常见的操作命令:通过调用 npm run XXX 或 yarn XXX 来运行它们
  • 文件以及目录
    • 入口
      • main:指定加载的入口文件(在 browser 和 Node 环境中都可以使用);
      • browser 字段可以定义 npm 包在 browser 环境下的入口文件;
      • module 字段可以定义 npm 包的 ESM 规范的入口文件
    • 可执行文件
      • bin:node_modules/.bin / 目录会在运行时加入系统的 PATH 变量,因此在运行 npm 时,就可以不带路径,直接通过命令来调用这些脚本
    • files:把 npm 包作为依赖包安装时需要说明的文件列表,是个数组。npm 包发布时,files 指定的文件会被推送到 npm 服务器中(. npmignore 文件可以忽略提交)
    • man:可以指定一个或多个文件, 当执行linux man 命令 时, 会展现给用户文档内容

此外还有第三方配置和发布相关的配置,比如license、os、typings、eslintConfig、gitHooks等等。

包管理器

npm:如何安装依赖

  • 首先会去查找 npm 的配置信息:查找是否有 .npmrc 文件,没有的话会再检查全局配置的 .npmrc ,还没有的话就会使用 npm 内置的 .npmrc
  • 构建依赖树:检查下项目中是否有 package-lock.json 🔐文件:存在 lock 文件的话,会判断 lock 文件和 package.json 中使用的依赖版本是否一致,如果一致的话就使用 lock 中的信息,反之就会使用 npm 中的信息;如果没有 lock 文件的话,就会直接使用 package.json 中的信息生成依赖树;
  • 根据依赖树下载完整的依赖资源:先检查下是否有缓存资源,有则放,无则下载,到node_modules;默认不会将依赖安装到全局,只会安装到当前的路径,除非-g安装;
  • 生成 package-lock.json 文件;

npm:依赖优化演进

  • npm 2.X :最简单的循环遍历方式,递归地下载所有的依赖包,只要有用到的依赖,都进行安装;存在问题:(node_modules 黑洞)项目之间难免有相同的依赖,然后就会有大量冗余的依赖;
  • npm 3.X:将原有的循环遍历的方式改成了更为扁平的层级结构,将依赖进行平铺(首先会遍历所有的依赖并将其放入树的第一层节点,然后再继续遍历每一个依赖。当有重复的模块时,如果依赖版本相同,就丢弃不放入依赖树中。如果依赖版本不一致,那么就将其放在该依赖下);存在新问题:生成的依赖树会因为依赖的顺序不同而不同;
  • npm 5.X:新增了锁文件 package-lock.json ;只要 lock 文件相同,那么每次安装依赖生成的 node_module 就会是相同的;package-lock.json 还可以减少安装时间。因为在 package-lock.json 锁文件中已经存放了每个包具体的版本信息和下载链接,这就不需要再去远程仓库进行查询,优先会使用缓存内容从而减少了大量的网络请求,并且对于弱网环境和离线环境更加友好

npm: run命令怎么执行命令

  • 运行 npm run xxx的时候,npm 会先在当前目录的 node_modules/.bin 查找要执行的程序,如果找到则运行;
  • 没有找到则从全局的 node_modules/.bin 中查找,npm i -g xxx就是安装到到全局目录;
  • 如果全局目录还是没找到,那么就从 path 环境变量中查找有没有其他同名的可执行程序。

npm:缓存

缓存是放在电脑中目录下(${user.home}.npm/_cacache),分为索引和内容;索引较index-v5,内容叫content-v2;根据 index-v5 中的索引去 content-v2 中查找具体的源文件

  • 在安装资源的时候,npm 会根据 lock 中的 integrity、version、name 信息生成一个唯一的 key;
  • 然后用这个 key 经过 SHA256 算法生成一个 hash,根据这个 hash 在 index-v5 中找到对应的缓存文件,该缓存文件中记录着该包的信息;
  • 根据该文件中的信息我们在 content-v2 中去找对应的压缩包,get缓存资源;
  • 最后再将该压缩包解压到 node_modules 中;

npm:资源完整性校验

通过计算shasum,通过SHA256计算得出的shasum一致就可认为具备完整性;

yarn和npm

yarn是另一种包管理器,是由facebook、google等开发的,在npm v3时代,npm还没有lock,缓存等功能,所以yarn在当时是比npm出色的。不过后来这些功能也都追加到了npm v5中了。
所以,当时yarn的特点有:

  • yarn.lock机制,即使是不同的安装顺序,相同的依赖关系在任何的环境,都可以以相同的方式安装;
  • 采用模块扁平化的安装模式: 将不同版本的依赖包,按照一定的策略,归结为单个版本;以避免创建多个版本造成工程的冗余(目前版本的npm也有相同的优化)
  • 网络性能更好: yarn采用了请求排队的理念,类似于并发池连接,能够更好的利用网络资源;同时也引入了一种安装失败的重试机制;
  • 采用缓存机制,实现了离线模式 (目前的npm也有类似的实现)

    pnpm

    特点:
  1. 更快,比yarn和npm都快;

image.png

  1. 磁盘利用高效:用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次;
  2. 支持了 monorepo,体现在各个子命令的功能上,比如在根目录下 pnpm add A -r, 那么所有的 package 中都会被添加 A 这个依赖

规范化:命名规范

一般按照团队要求,要统一,没有确定的一定正确的方案,但是要统一
我的文件命名:

  • 文件名不得含有空格hello-world.md
  • 文件名建议只使用小写字母,不使用大写字母。(为了醒目,某些说明文件的文件名,可以使用大写字母,比如 README、LICENSE。)反例:HelloWorld.txt
  • 文件名包含多个单词时,单词之间建议使用半角的连词线 ( - ) 分隔。eg: hello-world.js
  • 有复数结构式,要使用复数: services

我的代码变量命名:

  • 变量和函数,小驼峰方式
  • 常量:大写字母和下划线
  • 类:大驼峰

规范化:git

提交消息规范

  1. <type>(<scope>): <subject>
  2. // 空一行
  3. <body>
  4. // 空一行
  5. <footer>

Header 中又包括三个字段:type、scope、subject
其中:
type 是用于说明 commit 的类别,只允许使用下面 7 个标识。

  • feat:本次改动为新增功能;
  • fix:本次改动为修补 bug;
  • docs:本次改动为新增或修改文档(documentation)信息;
  • style:本地改动为修改样式文件;
  • refactor:本次改动为代码重构;
  • test:本次改动为新增或修改测试用例;
  • chore:构建过程或辅助工具的变动;

工作流

Git工作流可以理解为团队成员遵守的一种代码管理方案,在Git中有以下几种常见工作流:

  • 集中式工作流
  • 功能开发工作流
  • Gitflow工作流 (mine)
  • Forking工作流(open source)
  • gitflow:

适合用来管理大型项目的发布和维护。
贯穿整个开发周期,master和develop分支是一直存在的。流程规范如下:

  • master 分支可以被视为稳定的分支,一般不允许直接往master分支提交代码,只允许往这个分支发起merge request,只允许release分支和hotfix分支进行合流。
  • develop 分支是相对稳定的分支,用于日常开发,包括代码优化、功能性开发。
  • feature 分支从develop分支拉取,特性开发会在其上进行,开发完毕合后并到develop分支。
  • release 分支从develop分支拉取,用于回归测试,完成后打tag并合入master和develop。
  • hotfix 分支用于紧急修复上线版本的问题,修复后打tag并合入master和develop。

    changelog生成

  • Commitizen:这款插件可以根据交互式询问的方式更加友好的帮你生成 commit message

  • validate-commit-msg:这款插件可以在提交 commit 的时候检查你的 commit message 是否符合规范,不符合的不允许提交。
  • conventional-changelog:这款插件可以通过脚本自动根据你的 commit message 信息生成 CHANGELOG.md 文件

    规范化:eslint

    eslint是当前最流行的检查代码规范工具。
    安装完成之后在.eslintrc.js(.json .yml都可以)设置其规则。不过一般都是通过继承现有规则,简单的方式通过命令eslint —init初始化一份配置,在交互式命令选择中可以选择想要继承的规则。
    一般使用eslint后需要在package.json的script中配置两个命令:
    1. "lint": "eslint './**/*.js'", // 检查
    2. "lint:fix": "eslint './**/*.js' --fix", // 自动修复问题

更严格一点可以通过githooks配置提交代码时检查,以及通过CI工具在构建时检查;

babel

babel是一个js语法翻译器。因为前端环境多样性,导致js语法在开发环境和运行不一致,开发中用到的一些简便的高级语法需要向下兼容成早期版本js语法,所以需要借助babel这种工具。

babel的使用

  1. npm install --save-dev @babel/core @babel/cli @babel/preset-env
  • @babel/core是Babel的核心库
  • @babel/cli是Babel的命令行工具
  • @babel/preset-env是Babel默认的预设环境

配置文件.babelrc.json

  1. {
  2. "presets": [
  3. [
  4. "@babel/env", // 预设支持当前版本所能支持的所有JavaScript的正式标准
  5. {
  6. // 编译后的目标
  7. // 这里设置为目前市场占有率高于0.25%且依然在维护中(not dead)的浏览器
  8. "targets": "> 0.25%, not dead",
  9. // 根据我们实际用到的API以及编译目标的浏览器兼容性
  10. // core-js中按需添加需要的polyfill
  11. "useBuiltIns": "usage"
  12. }
  13. ]
  14. ]
  15. }

babel过程和原理

Babel 的转译过程主要可以分为三个步骤,解析(parse)、转换(transform)、生成(generate)
第一步就是将源代码解析为 AST

  • parse:在解析过程用到的工具包就是 @babel/parse。
    • @babel/parse 是 JS 语言解析的解析器,其作用是接收源码,然后进行词法分析、语法分析,最终生成AST
  • transform:使用 @babel/traverse的traverse方法AST其进行遍历。再进行添加、移除、更新等操作;
  • generate:转换后的 AST 再转换为可执行代码的过程被称为代码生成 (generate)。同时此过程也可以创建 source map,使用@babel/generator。

什么是@babel/polyfill

@babel/polyfill是babel官方推出的js api转换解决方案。
核心依赖是 core-js@2 和 regenerater-runtime/runtime;

  • core-js 是 JS 标准库的 polyfill,为其提供垫片能力;
  • regenerater-runtime/runtime 用来转译 generators 和 async 函数。

babel7.4.0 之前,我们可以直接安装 @babel/polyfill 来转换 API,但是在 7.4.0 之后的 Babel 版本,就会提示让我们分开引入 core-js/stable(默认安装3.x)和 regenerator-runtime/runtime。
所以,@babel/polyfill 库已经是时代的产物了。
@babel/polyfill一个要命的缺陷是,是全量进行引入,完整的 polyfills 文件非常大,及其不利于我们打包出来的体积和页面的性能。

我们当前如果使用preset-env,这里面其实集成了polyfill了,而且可以通过配置变更其引入方式。

presets

预设,是一个预先确定的插件集。避免一个一个的添加插件,使用 preset,方便用户对插件的使用和管理。
Babel 提供了一些常见的 preset,比如:

presets-env的一些设置:

  • targets:设置目标环境和兼容范围;
  • useBuiltIns:配置决定了 @babel/preset-env 该如何处理 polyfill
    • 默认的 false,polyfill 就不会被按需处理会被全部引入
    • 设置为 entry,需要手动导入 @babel/polyfill
    • 设置为 usage,则不需要手动导入 polyfill,babel 检测出此配置会自动进行 polyfill 的引入。usage 模式下,Babel 除了会针对目标环境引入 polyfill 的同时也会考虑项目代码代码中使用了哪些 ES6+ 的新特性,两者取一个最小的集合作为 polyfill 的导入
  • core-js:让你自己选择需要使用 2 还是 3,这个参数只有 useBuiltIn 设置为 usage 或者 entry 时才会生效。

    构建工具的发展和对比

    自动化:测试

    ci

    cd