前言
前端工程化是指将前端开发过程中的一系列流程和工具进行规范和自动化,从而提高开发效率、减少重复劳动、降低出错率。前端工程化的目标是让前端开发更高效、更优质。
前端工程化的核心概念包括模块化、打包构建、自动化部署、自动化测试和持续集成等。
第一章 前端工程化含义
1. 模块化
模块化是指将一个大的应用程序划分成多个小的模块,每个模块都有自己的功能和特点,可以独立开发、测试和维护。
常见的模块化方案有CommonJS、ES6模块、AMD、CMD等。
2. 打包构建
打包构建是指将多个模块组合起来,生成可以在浏览器中运行的代码。
打包构建的过程包括代码压缩、文件合并、资源管理等,常见的打包构建工具有 webpack、rollup等。
3. 自动化部署
自动化部署是指将打包构建后的代码部署到生产环境或测试环境中的自动化过程。
自动化部署可以减少手动部署的错误和工作量,同时也可以缩短部署的时间。
常见的自动化部署工具有Jenkins、Travis CI等。
4. 自动化测试
自动化测试是指使用自动化工具对代码进行测试,以确保它们在开发过程中不会出现问题,并且在部署到生产环境之前也不会出现问题。
自动化测试可以分为两类:单元测试和端到端测试。
- 单元测试:测试应用程序中最小的可测试单元,例如一个函数或一个类
- 端到端测试:测试应用程序的整个流程,包括用户界面和后端逻辑
自动化测试的优势在于它可以提高开发效率和代码质量。它可以帮助开发人员在更早的阶段发现问题,并且可以确保代码的正确性,减少代码中的错误和缺陷。
5. 持续集成
持续集成是指在应用程序开发过程中,将代码的改变频繁地集成到共享代码库中,并且每次集成都会进行自动化构建和自动化测试。这样可以确保代码的稳定性和质量,并且能够更快地检测和修复错误。
持续集成的优势在于它可以提高开发效率、加速代码部署和减少错误。它可以使团队更加协作,提高产品质量,并且可以更快地响应客户的需求。
第二章 前端工程化工具
1. 模块化
1.1 模块化
前端模块化是指将一个大型程序拆分成多个相互独立的小模块,每个模块负责一个特定的功能。
模块化可以提高代码的可读性、可维护性和可复用性。
ES6的模块语法通过使用import和export关键字,将代码分割成多个模块,并在不同的模块之间共享和重用代码。ES6模块具有以下特点:
-
支持异步加载:开发者可以在需要时才加载模块,而不是一开始就加载所有模块
-
支持循环依赖:开发者可以在两个模块之间建立双向引用关系,而不需要担心循环依赖导致的问题
-
支持Tree Shaking:开发者可以通过剔除未使用的代码来减小最终输出文件的大小
1.2 组件化
前端组件化是指将一个大型程序拆分成多个相互独立的小组件,每个组件负责一个特定的UI功能。
一个网页不再是由一个个独立的HTML、CSS和JavaScript文件组成的了,而是按照组件的思想将网页划分成一个个组件,如轮播图组件、列表组件、导航栏组件等。将这些组件拼装在一起,就形成了一个完整的网页。
通常前端页面可以借助某种框架(如Vue.js、Angular、React)来实现组件化开发,对项目进行自上而下的拆分,把通用的/可复用的功能涉及的模型(Model)、视图(View)和视图模型(ViewModel)以黑盒的形式封装到一个组件中,然后暴露一些开箱即用的函数和属性供外部组件调用,实现与业务逻辑的解耦,来达到代码间的高内聚、低耦合,实现功能模块的可配置、可复用、可扩展。
组件化可以提高代码的可读性、可维护性和可复用性。
- React组件具有以下特点
-
独立:React组件是独立的,它们之间没有直接的关系。这使得开发者可以轻松地重用和组合组件,以构建复杂的用户界面。
-
可重用:React组件是可重用的,它们可以在多个项目中使用。这使得开发者可以快速地构建和维护应用程序,而不需要重复编写相同的代码。
-
灵活:React组件是灵活的,它们可以根据需求进行扩展和修改。这使得开发者可以轻松地适应不断变化的需求和业务场景。
- Vue组件具有以下特点
-
声明式:Vue组件是声明式的,它们通过描述数据和状态之间的关系来定义用户界面。这使得开发者可以轻松地理解和修改组件的逻辑和结构。
-
响应式:Vue组件是响应式的,它们会自动更新用户界面以反映数据和状态的变化。这使得开发者可以轻松地实现数据的绑定和处理逻辑。
-
组合式:Vue组件是组合式的,它们可以通过组合其他组件来构建复杂的用户界面。这使得开发者可以轻松地实现代码的复用和扩展。
- 组件分类
一般常见的组件可以划分为这四种:基础组件、业务组件、区块、页面。
-
基础组件:不包含业务,具备独立具体的功能,比如button、input、select等组件。通过组件定义的props配置来实现不同的功能;
-
业务组件:由基础组件组合而成,含有业务逻辑的组件,不能随意改动;
-
区块:由基础组件和业务组件组合而成,项目中对于区块代码可以进行任何改动;
-
页面:呈现给用户的页面,也可以看作是业务组件
- 组件设计原则
-
单一性:一个组件只专注做一件事,且把这件事做好
-
可配置:明确组件的输入和输出
-
粒度适中:划分的粒度大小要根据实际情况权衡,太小会提升维护成本,太大又不够灵活
1.3 组件化和模块化的区别
-
模块化:从文件层面上,对代码或资源进行拆分
-
组件化:从设计层面上,对用户界面进行拆分
1.4 React组件优化
- 容器组件和可视化组件
-
可视化组件:根据接收到的数据进行视图渲染
-
容器组件:一般作为可视化组件的父组件,提供数据
- 优化方法
- 使用 Pure Components 或 Memo 组件
- 使用 shouldComponentUpdate 生命周期函数
- 使用 useCallback 和 useMemo
- 使用代码拆分和 lazy懒加载
2. 包管理工具
2.1 pnpm
pnpm全称为“performant npm”,它是一种替代npm和Yarn的包管理器,用于管理JavaScript项目中的依赖关系。与传统的npm和Yarn不同,pnpm提供了一种更为创新的方式来管理依赖项。
pnpm的官网对其性能有一定的介绍,汇总如下:
- 快速:pnpm比其他包管理器快2倍
- 高效:node_modules中的文件链接自特定的内容寻址存储库
- 支持Monorepo:pnpm内置支持单仓多包
- 严格:pnpm默认创建一个非平铺的node_modules,因此代码无法访问任意包
- 链接方式
-
硬链接(hard link):电脑文件系统中的多个文件平等地共享同一个文件存储单元,当删除一个文件名字后,还可以用其他名字继续访问该文件
-
软连接:又称符号链接(soft link)是一类特殊的文件,包含一条以绝对路径或者相对路径的形式指向其他文件或者目录的引用。
- 不同的文件链接
- 文件拷贝:会在硬盘中复制出一份文件资源
copy foo.js foo_copy.js
- 软连接:保存一个文件的引用,不能通过软连接修改源文件
mklink foo_soft.js foo.js
- 硬链接:多个文件指向同一个数据,相互影响
mklink /H foo_hard.js foo.js
pnpm特点
- 统一保存,节省磁盘空间
当使用npm或者yarn安装依赖时,如果有100个项目,并且所有的项目都有一个相同的依赖包,那么会在硬盘上保存100份相同依赖包的副本。
而使用pnpm安装依赖时,会将所有的依赖包存储在磁盘的某一个位置,简称pnpm store。如果新项目需要相同的依赖包时会,会检查pnpm store中是否存在这个包,存在的话就从pnpm store创建一个硬链接到node_modules/.pnpm下对应的依赖。这样即使多个项目都依赖一个包,也只会在本地存一份代码,不会占用额外的磁盘空间。
- 如果对同一依赖包使用的是相同的版本,磁盘只有一份依赖包文件
- 如果对同一依赖包使用不同的版本,磁盘只会保存多个版本的依赖包中不同的文件
- 所有文件都保存在一个统一的位置,共不同项目共享
- 非扁平化的node_modules管理
当使用npm或者yarn安装依赖时,所有的包都会被提升到node_module根目录下。导致项目可以访问不属于当前依赖的包。
而pnpm的node_module不是扁平化的,通过package.json安装的依赖通过软连接到node_module/.pnpm中,并且在安装时会判断pnpm store中是否有相对应的包,如果有就创建硬连接到node_module/.pnpm。
通过非扁平化管理方式可以有效的解决幽灵依赖的问题。
- 提升安装速度
由于依赖包都是统一管理在pnpm store中,所以相同依赖不需要重复下载,这样使得pnpm的安装和构建速度相对更快,特别是在项目中存在大量依赖项时。
常用指令
- 获取store目录
pnpm store path
- 删除store中当前未被引用的包
pnpm store prune
- 安装依赖包
pnpm add package-name
- 根据package.json文件安装所有依赖
pnpm install
官方文档链接:pnpm.io/zh/cli/add
幽灵依赖
出现的原因:安装某个依赖包时,该包会依赖其他的一些包...,以此类推,所有的包都会安装到node_modules下,对于非主动下载的包,也可以通过ESM语法导入使用,对于这些非主动安装的包,则称为幽灵依赖。
导致的问题:依赖丢失
解决幽灵依赖问题:pnpm
2.2 npm & yarn
npm和yarn同为包管理工具,其中yarn的出现解决了npm的一些缺陷问题。
-
npm
- 顺序安装:npm按照顺序安装依赖包,前一个包的安装会阻塞后一个包的安装
- 不会缓存:即使已经安装过某个包,npm依然会从网络再次下载
- 默认拉取最新包:npm 默认从网络下载最新的最稳定的
- 冗余输出:npm安装包的时候输出的信息冗余
-
yarn
- 并行安装:yarn 并行安装所有依赖包
- 离线缓存:对于安装过的包,yarn会从缓存中读取
- 锁定版本:yarn 默认有一个 yarn.lock 文件锁定版本,保证环境统一
- 简洁输出:yarn 安装包时输出的信息较少
yarn 采用本地缓存的方式来存储依赖项:
- 本地缓存:
yarn会在用户的主目录下创建一个.yarn目录,用于存储下载的依赖项。这样,当一个项目需要安装某个依赖项时,它会首先检查本地缓存,如果已经下载过,就直接复用,而不必重新下载。 - 离线模式:通过本地缓存,
yarn支持离线模式,即使没有互联网连接也可以使用本地缓存中的依赖项进行安装。
2.3 --save&--dev
package.json中的依赖关系如下所示:
- dependencies是运行时依赖
- devDependencies是开发时依赖
- peerDependencies是插件需要的依赖
npm install常用命令区别如下所示:
-
npm install moduleName 命令
- 安装模块到项目 node_modules 目录下
- 不会将模块依赖写入 devDependencies 或 dependencies 节点
- 运行 npm install 初始化项目时不会下载该模块
-
npm install -g moduleName 命令
- 安装模块到全局,不会在项目 node_modules 目录中保存模块包
- 不会将模块依赖写入 devDependencies 或 dependencies 节点
- 运行 npm install 初始化项目时不会下载模块
-
npm install --save moduleName 命令
- 安装模块到项目 node_modules 目录下
- 会将模块依赖写入 dependencies 节点
- 运行 npm install 初始化项目时,会将模块下载到项目目录下
- 运行 npm install --production 或者注明 NODE_ENV 变量值为 production 时,会自动下载模块到 node_modules 目录中
-
npm install --save-dev moduleName 命令
- 安装模块到项目 node_modules 目录下
- 会将模块依赖写入 devDependencies 节点
- 运行 npm install 初始化项目时,会将模块下载到项目目录下
- 运行npm install --production 或者注明 NODE_ENV 变量值为 production 时,不会自动下载模块到 node_modules 目录中
在 Node v5.0.0 之后 --save 已经成为了预设指令,即
npm install --save lodash可以写为npm install lodash即可,--save-dev可以简写为-D
2.4 git仓库管理
git是目前最流行的分布式版本控制软件。
文件状态
- 已修改:在工作目录修改Git文件
- 已暂存:对已修改的文件执行Git暂存操作,将文件存入暂存区
- 已提交:将已暂存的文件执行Git提交操作,将文件存入版本库
工作流程
基础指令
- git init:初始化本地git仓库
- git status:查看当前git仓库中所有文件的状态
- git add <文件名称>:将文件放入暂存区
- git commit -m '描述信息':将文件存入版本库
- git log:查看版本记录,不包括已回滚的记录
回滚代码
- git reflog:查看所有版本记录,包括已回滚的记录
- git checkout:取消对文件的修改
- git reset HEAD:将暂存区的文件放回工作区
- git reset --hard <commit版本号>:回滚代码,可以向前或者向后回滚
回滚方式
--hard:清空所有修改,删除本地数据--soft:将之前提交的内容恢复到暂存区,不会修改本地文件--mixed:将之前提交的内容恢复到未暂存状态,不会修改本地文件(默认值)
分支操作
- git branch:查看当前分支名称
- git branch <分支名>:基于当前分支创建新的分支
- git checkout <分支名>:切换当前分支
- git merge <分支名>:将其他分支的代码合并到当前分支,需要先切换到被合并的分支上
- git branch -d <分之名>:删除指定分支
远程仓库交互
- git push origin 分支:提交代码到远程仓库
- git fetch origin 分支:将远程仓库代码拉到本地仓库中
- git merge origin/分支:将本地仓库的代码拉到工作区
- git pull origin 分支:将远程仓库的代码拉到工作区,相当于
git fetch origin 分支+git merge origin/分支
git rebase
1)合并多次commit
git rebase -i <版本号>
git rebase -i HEAD~数字
2)在当前分支进行变基,整合merge过程中的分支出岔
git rebase <需要变基的分支>
git rebase与git merge区别
共同点:可以整合不同分支间的变更,执行结果相同
不同点:实现原理不同
- git merge:通过快照向前推进提交历史,不会影响中间的提交状态
- git rebase:通过重写提交历史生成新的节点,会产生新的提交状态
git merge master
git rebase master
执行git fetch拉取远程仓库代码后,会生成一个FETCH_HEAD文件,文件内容如下所示:
FETCH_HEAD文件记录了远程仓库中的分支名称、最新的commit id以及提交的作者等相关信息。
但它不是随着远程仓库实时变更的,是保存在我们本地,那就需要我们手动去更新。
当执行git fetch命令时,它会从远程仓库获取最新的提交信息,并将这些信息存储在本地FETCH_HEAD中。
在合并代码时,实际是根据FETCH_HEAD文件中记录的commit id去合并对应的版本,所以我们只需要更新FETCH_HEAD文件就行,那么我们的合并流程可以简化成以下步骤:gitpull origin master。
3. 构建工具
3.1 webpack
3.2 Vite
4. 前端自动化测试
4.1 Jest单元测试
5. 持续集成和持续部署
5.1 CI/CD
CI/CD的核心概念是持续集成、持续交付和持续部署。
- CI
持续集成是指多名开发者在开发不同功能代码的过程当中,可以频繁的将代码行合并到一起并且不会互相影响工作。
持续集成的目的是让产品可以快速迭代,同时还能保持高质量。
它的核心措施是代码集成到主干之前,必须通过自动化测试,只要有一个测试用例失败,就不能集成。
包括以下优点;
- 快速发现错误:每完成一点更新,就集成到主干,可以快速发现并定位错误
- 防止分支大幅偏离主干
- 快速的发布更新:持续集成可以帮助团队更快、更积极的发布程序和更新程序
- 节省人力,避免重复工作
- CD
持续部署是持续集成的下一步,指的是代码通过评审以后,自动部署到生产环境。
持续部署的目标是,代码在任何时刻都是可部署的,可以进入生产阶段。
持续部署集中依赖于部署流水线,团队通过流水线自动化测试和部署过程。可以决定每天,每周,每两周发布一次,这完全可以根据自己的业务进行设置。
- Gitlab
在项目根目录下配置 .gitlab-ci.yml文件,控制CI流程的不同阶段,例如install/检查/编译/部署服务器。
每次push/merge后都会触发CI流程,gitlab-ci都会检查项目下有没有.gitlab-ci.yml文件,如果有,它会执行你在里面编写的脚本,并完整地走一遍 intall => eslint检查 => 编译 => 部署服务器。
CI的所有流程都是可视化的,每个流程节点的状态可以在gitlab的交互界面上看到,不管成功还是失败。
不同push/merge所触发的CI流程互不影响。
6. 线上部署清除缓存
- 版本化文件名(css、js)
Webpack打包构建时,使用文件内容的哈希值作为文件名的一部分,这样每次代码发生变化时,文件名都会改变,浏览器就不会从缓存中获取旧版本的代码。
- HTTP缓存头(css、js)
在HTTP响应头中设置Cache-Control,Expires等缓存控制头,来告诉浏览器缓存策略。例如,可以将Cache-Control设置为max-age=0,这样浏览器每次都会请求最新的页面和资源。
- 文件名拼接动态符(css、js)
Webpack打包构建时,给 script和 link 里引入静态文件的地方加上一个动态参数,如在末尾加上?time=xxx 或者? v=1.0.1。
- 前端head添加设置(html)
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
- 登陆/退出后路由跳转(html)
在登录或者退出应用时,用 window.location.href = /a 来实现路由跳转,这样跳转会重新向后端获取最新的html,避免缓存,不要使用react router 实现的路由跳转。