大家好,我是前端西瓜哥。今天来看看前端工程化是什么。
什么是前端工程化?
工程化,可以理解为使用一些方式,去改良然后提高行业中现有的步骤、设计、应用方式。前端工程化,就是指对前端进行一些流程的标准化,让开发变得更有效率,且更好地做产品交付。
一开始,网页页面并不复杂,只是提供一些简单的展示和交互的静态页面,甚至不需要后端。
后来需要根据不同用户返回不同的页面信息,此时我们会用后端读取数据,配合一些模板引擎,在后端拼接好内容再返回,这就是所谓的服务端渲染(SSR)。
再后来,页面变得非常复杂,于是出现了前后端分离,前端被单独拎了出来,专门写 html、css 和 js,变成了 单页面应用(SPA)。但复杂也带来了很多问题,比如多个脚本的执行时机不对、css 名冲突、文件过于臃肿、错误的缓存导致没能下载最新的资源,等前端复杂后出现的一系列问题。
随着 Nodejs 的诞生,我们可以用 JS 去写前端工具了。为了解决上面这些问题,前端界出现了一大堆的工具和框架:Gulp、Angular、babel、Sass、React、Vue、Webpack、Yarn、TypeScript、ESLint、Docker、k8s 等等。
一切都是为了让前端的开发更工程化,也就是 不停地改良前端项目的开发流程,让开发者能够更高效地开发、更好地进行团队协作、让代码的风格标准化、对资源做压缩以及懒加载、更好地交付部署等。
(当然也因为工具过多,让前端直呼 “学不动了”)
我们通过四个维度来谈谈前端工程化一些具体的细节,分别是:
- 模块化
- 组件化
- 规范化
- 自动化
模块化
模块化,指的是将代码功能做拆分,分成独立地单能相互依赖的片段。
首先是 JS 的模块化。
JS 一开始的职责是给网页提供一些简单的交互,所以语法相对简单且不支持模块化。随着网页的复杂,发现原来的组织方式带来了很多问题,变得难以维护。
于是 CommonJS、AMD、ES Module 等模块系统出现了。正统标准是 ES Module,通过 import 关键字引入模块,通过 export 导出模块。
JS 的模块化将代码做了拆分,解决了全局变量污染、依赖关系不清晰、多人协作不方便、脚本引入顺序、单元测试等问题。
CSS 的模块化。
CSS 的第一个问题是比较难写,比如不支持选择器嵌套,对此我们可以用 CSS 预编译器(比如 Less、Sass、Stylus)去写一些更高级的语法,然后编译成 CSS。
然后是就 命名冲突问题,一种旧的方案是 BEM,就是通过将 CSS 命名 在组件化的框架中,我们有很多方案,可以用 CSS in JS,也可以用 CSS Module,或者 Vue 特有的 CSS Scoped。
HTML 的模块化。
html 通常是动态的,在服务端我们会使用模板引擎(template),将得到的数据注入到占位符中。在后端 Nodejs,我们可以用 pug、handlebars、ejs 等。
前后端分离后,我们通常使用的是 Vue 的 template(类似 handlebars 语法)以及 React 的 JSX。
资源整合模块化
不同类型的资源无法组织在一起,比如 JS 引擎能识别引入的 js 文件,但无法识别 css 文件。如果我们希望所有的资源都能组织再一起进行管理,要分别管理一个个不同类型的资源要方便地多。
为了解决这个问题,webpack 诞生了。webpack 是一个模块打包器,能够将任何资源转换为 js 代码进行导入。比如图片,它可以先变成一个静态资源服务的一个资源,然后在 js 文件 import 的时候在转换为一个 url 字符串,或者直接就变成一个 base64 字符串。
这些需要使用到一些 loader(加载器)。webpack 是一个框架,使用者需要根据需求,添加一些 loader,去识别不同的文件,转化成 JS 代码导入。
此外还有 plugin(插件),在这整个流程中做一些处理,比如将导出的 JS 文件插入到 HTML 模板中,或是进行代码的压缩等等。
组件化
组件化是 UI 层面上的更细粒度的拆分,一种类似 div 等原生元素的 “自定义元素”。
组件有自己的 HTML、CSS 和 JS,同时有自己的状态,并支持嵌入到其他组件中并接受外部的数据,可以进行复用。组件化可以看作是 UI 层组织方式的一种模块化。
目前主流的 React 和 Vue 前端框架都是基于组件的。
原本的以资源类型为单位进行组织的管理(所有 JS 文件放一个文件夹、CSS 同理),其实维护起来比较困难,也不好复用,组件化的构想是以视觉为单位进行拆分,做了结构、样式、脚本的组装,抽象出一个 “新的元素”。
组件已经是前端开发的基石了,是一种比较合理的抽象。
规范化
然后就是前端代码的规范。规范是很重要的,能让代码能够写得更容易更正确,避免一些不必要的错误。
能想到的规范有:
- 目录结构规定。
- 代码风格(包括 JS、HTML、CSS)。
- 注释规范。
- commit message 规范。
- git 工作流规范。
- Code Review。
- 请求接口规范。
有些规范不太能用工具进行限制,比如目录结构。但有些规范是可以利用到工具的,下面来说说有哪些和规范相关的工具。
首先是重磅级的 TypeScript。
TS 是有类型的 JS,是 JS 的超集。通过类型,我们可以预测变量的行为,比如一个布尔值类型是不能被作为函数调用的,可能为 undefined 的值需要进行类型收窄后丢弃 undefined 的可能性才能使用。
TS 越来越流行,是因为在大型项目中,类型系统是非常重要的,能够避免大量的类型错误。TS 让代码即文档,降低程序员理解代码的成本。
如果工具层就能做规范,就不应该用文档去说明,人不是绝对正确的机器,但工具可以。
然后是 ESLint。
ESLint 能够检测 JS 代码中的错误,主要两个方面:
- 代码质量,比如你不能声明一个没有被使用的变量。
- 代码风格,比如字符串引号必须用单引号。
ESLint 有助于统一团队的风格,让代码看起来基本像是一个人写的,避免出现字符串一会用单引号,一会用双引号,变量命名一会用下划线风格,一会用驼峰风格,这种让强迫症抓狂的情况。
然后是 commit message。commit message 我们不希望看到像是 “修复了一些 bug” 这种不够具体的写法,希望具有一定的结构,比如 "fix(工作台): 修复了卡片不能滚动的问题"。
对此我们可以用 commitlint 的命令行工具去判断是否符合特定风格。
当然还需要确保团队成员是使用了这些工具的,我们可以用保存后自动格式化(需要配合编辑器和对应插件)。然后最重要的就是 git hook,可以在本地 commit 时先对 staged 中的文件做风格校验和格式化,然后再检查 commit messge 风格是。如果不对,本地 commit 会失败。对应的工具是 husky。
自动化
重复的可以自动化的流程化工作,应该尽量去自动化。让人去做,对人是一种折磨,然后也不能保证质量,因为通常流程也很复杂,即使是简单,做多了也容易错。
一个小概率事件只要做的次数足够多,它就会变成大概率事件。这也是为什么分布式系统中容错机制是非常重要的原因。
首先想到的自然是 CI/CD(持续集成和持续交付/部署)。我们将代码提交到远端仓库时,或者是给一个分支打了 tag 后,能够触发一些脚本,将我们的项目代码做打包编译,发布成制品,然后发布到生产环境。这些都是自动化的,流程化的。
CI/CD 工具有很多:Jenkins(比较古老了)、GitLab CI/CD、GitHub Action、Docker(发布制品) 和 k8s(容器编排)等。
前面说的 git hook,在本地 commit 时进行一些操作,也算是一种简单的自动化。
打包工具
前端工程化的核心是打包工具。
打包工具需要支持的 几种重要的能力:
- 代码分割:指的将代码划分为可以按需 / 同时加载的多个bundles 或组件的能力。比如动态 import、提取公共依赖模块代码、多个入口文件没有重复代码、支持 ESM 的值引用模拟等。
- 哈希:资源更新时做哈希,防止资源缓存。哈希分很多种,比如文件路径名哈希、内容哈希等。
- 包引入:ES Module、CommonJS 以及从 node_modules 目录引入包的支持。
- 非 JS 资源:导入非 JS 资源的支持,像是 webpack 需要使用各种 loader 来支持,有些打包工具是内置的。
- 输出的模块格式:支持导出为 ES Module、CommonJS 等模块。
- 转换处理:比如对图片压缩、代码压缩、JS 版本降低等,在 webpack 中是使用 plugins 来实现的。
其他
还有一些零散的可以提高效率的工具。
- babel:开发时使用高版本的 ES 语言特性,然后生产环境用 babel 转换为低版本的兼容性好的 ES5。打包工具内部其实使用了 babel。
- tsc:tsc(TS 编译工具) 在支持 TS 的前提下,也支持编译为 JS 低版本。
- polyfill:低版本的 ES5,想要使用一些新的 API,可以自己写函数去模拟,这就是 polyfill,通常我们会使用 core.js 库,但有些语言层面上的新特性就不能用 polyfill。
- monorepo:将多个项目放到一个 git 仓库下,方便包的依赖共用和维护。
- 异常监控:当前端报错时,将相关信息提交到异常监控服务,比如 sentry,通常配合 sourcemap 精确定位源码中的错误位置。
- 制品库:使用 Nexus 来部署自己的私有制品库,支持各种制品库。比如发布 docker 包、npm 包等,配合发版部署;
- VSCode Snippet:自定义 VSCode 编辑器的代码片段,可以快速生成一些预置好的代码模板,减少一些模板代码的书写。可以归为自动化。
- Mock:前端在后端确认返回数据结构后完成接口前,可以通过模拟虚假数据进行调试开发,比如 yapi 平台就是除了支持接口文档,还提供 mock 功能。
- 单元测试:以模块(比如组件)为单位进行测试,保证代码逻辑符合预期。单元测试通常比较耗时,会在提交到远端时或合并到主分支时进行。流行的单元测试库有 Jest。
- 热重载:因为每次改代码都要编译,如果整个项目都要重新编译开发体验很差,可以用热重载只编译被修改的模块。
- 组件库文档:可以用 stroybook。如果是 vue 组件,可以考虑用 Vue Press。
- Tree shaking:丢掉一些引入了但没有使用的模块。
结尾
简单来说,前端工程化是对前端开发流程的改良,是效率工具。