作为前端工程师,前端工程化是经常听到的概念,但虽然经常听到,很多人对它的认识依然很模糊。
比如,提到前端工程化,他并不能说出什么是前端工程化。给出一门具体的技术,他也不能确定是不是属于工程化范畴的技术。
这是因为他没有对前端工程化有一个概念上的认识。
那么,这篇文章我们就来给前端工程化下个定义吧。
什么是前端工程化
提到前端工程化,最容易想到的就是编译了。很多代码需要经过编译才能运行在目标环境:
- 高版本的语法需要用 babel 编译成低版本的。
- less、sass 要经过各自的编译器转换成 css 代码。
- TypeScript 代码需要经过 tsc 或者 babel 等编译器转成 JS 代码。
- ...
前端工程化首先要做的就是支持各种代码的编译。
最早的前端工程化是通过任务的形式组织这些编译过程的,指定对什么文件用什么编译器编译,然后输出到哪个目录。任务之间可以规定先后顺序、串行并行。
gulp 就是这一类工具,叫做任务运行器(task runner)。
这一类工具能够组织整个编译流程,对不同的文件分别做相应的处理,使之能运行在目标环境。但因为每个任务都比较独立,很难做一些全局的优化。
后来出现了另一种思路,不通过任务组织了,而是分析模块之间的依赖关系,从入口模块开始构建一棵依赖图,中间遇到的用到的 js、css、图片等都会作为他的依赖。然后对依赖图的每个节点分别用对应的编译器处理。
有的同学说,这个和 task runner 的方式有啥区别,不都是对不同的文件用不同的编译器处理么?
那肯定有区别呀,现在有了模块之间的依赖图了,那就可以做一些全局的优化:
比如通过分析依赖关系来去掉一些没有用到的代码,这叫做 tree shaking。
比如把这些模块拆分到不同的分组(chunk)里,然后生成不同的文件,这样把变动频繁的模块和不咋变动的模块分到不同的 chunk,进而生成到不同的文件里,就可以更好的利用缓存,这叫做 code splitting。
而且,因为生成的代码是自己控制的,有自己的 runtime 代码,那就可以配合 runtime 来实现一些功能,比如实现模块的 lazy load,也就是把 code splitting 分出来的 chunk,在运行时动态加载。
这叫做打包工具(bundler),典型的是 webpack。
任务运行器和打包工具的区别还是很明显的:
任务运行器只是把不同的编译任务组织起来,并不参与具体的代码处理,具体处理啥文件,怎么处理都是开发者指定的。
而打包工具则是分析模块依赖关系,构成依赖图,通过这种方式确定处理哪些文件,可以基于这种依赖关系实现 tree shking、code splitting 等优化,并且生成的代码会有自己的 runtime,可以配合 runtime 实现 lazy load 等功能。
因为打包工具这种明显的优势,慢慢的就取代了任务运行器,成为了构建的主流方式。
但是打包工具也不是完美的,因为每次都要构建整个依赖图,对不同文件分别做处理,之后才能生成代码,所以当项目的模块多了就会很慢,大项目打包几分钟也是很常见的事情。
有痛点问题,大家就会想办法去解决,所以出现了 no bundle 的方案,也就是不打包,比如 vite。
不打包也就不会进行依赖分析,那怎么确定处理哪些文件呢?
no bundle 是基于浏览器支持 es module 来实现的,浏览器会做 es module 的依赖分析,然后加载对应的模块,这样自然就不用自己做依赖分析了,只需要实现模块的编译即可。
所以,no bundle 工具会启动一个开发服务器,根据请求的模块路径来进行相应的编译,然后返回编译后的代码。
当然,生产环境还是需要打包的,会用打包工具来处理。no bundle 方案只是解决了开发环境下打包工具要构建整个依赖图导致比较慢的痛点问题。
我们回过头来综合看一下:
构建的核心是对不同的文件做不同的编译,最早的任务运行器的方案实现了编译流程的组织,但是并没有做全局的优化,也没有自己的 runtime 代码,所以出现了基于依赖分析的打包工具,打包工具可以基于依赖分析实现 treeshking、code splitting 等优化,可以配合 runtime 代码实现 lazy load。但成也依赖分析,败也依赖分析,这个太慢了,所以出现了 no bundle 的方案,配合浏览器对 es module 的支持,只要实现对应模块的编译服务即可,不过生产环境还是要打包的。
那我们马后炮一下,假如回到 gulp 当时的时代,能够实现打包工具和 no bundle 服务么?
还真不一定,因为打包工具的实现是基于模块规范的,很早的时候并没有,所以只能简单的对编译流程做下组织。更不用说 no bundle 还要浏览器支持 es module 了,这个也是近几年才可以的。
所以,不管是任务运行器、打包工具、no bundle 服务都是在当时的环境下的最优的解决方案,并不是说被淘汰的就是不好的。
上面我们只聊了构建,那前端工程化就等于构建么?
肯定不是呀,还有很多别的方面,比如代码的规范和静态分析:
- JS 代码会用 ESLint 来禁止掉一些写法,比如 concole、debugger 的使用,还可以修复格式问题,比如缩进方式,还能检查出一些逻辑错误,比如 if 中用了赋值。
- CSS 代码也同样会用 StyleLint 来禁用一些写法,修复格式问题,检查出一些逻辑错误
- ESLint、StyleLint 只是局部的格式修复,我们还可以用 prettier 来进行整体的格式化
- 如果我们用了 TypeScript,那就可以用 tsc 来进行类型检查,发现代码中潜在的类型不匹配的错误
静态分析工具、格式化工具并不影响构建,他们一般是单独来跑的,用来发现一些代码潜在的问题,规范代码格式等。
代码写完之后,会上传到代码仓库,比如 gitlab,代码托管也是工程化的一部分。
代码上线的话,需要进行构建和部署,我们可以通过 jenkins 来组织构建流程,当 gitlab 代码有新的 push 的时候触发,进行构建,然后把产物部署到服务器,基于 git hook 的构建部署流程就叫做持续集成、持续部署(CI/CD)。这也是前端工程化的一部分。
好像很多东西都属于前端工程化,那怎么给前端工程化下个定义呢?
前面聊了构建、静态分析、格式化、代码托管、CI/CD,不知道大家有没有发现这些工具的共同特点:
他们的处理对象都是代码。
他们只是把代码当作字符串来处理,并不管你用的是 vue、react 还是 angular,你用的啥状态管理库、动画库之类的。
所以说,前端工程化就是处理代码的一系列工具链,他们并不会运行代码,只是把代码作为字符串来进行一系列处理。编译构建、ci/cd、代码托管、静态分析、格式化等都是。
不知道大家是否理解了。我们来看两个例子:
我们项目用了 react,公共组件比较多,所以封装了 react 的组件库。这属于前端工程化么?
不属于。前端框架还有组件都是运行时才有的,工程化并不会运行代码,只会处理代码。所以组件库属于前端基建,但不属于前端工程化。
我们好几个项目之间公共代码比较多,所以改造成了 monorepo 的形式,也就是一个工程下保存了多个项目的代码,使用了 pnpm workspace 来作为 monorepo 的管理工具,可以自动的进行依赖的关联,统一的进行依赖安装、构建、发版等。这属于前端工程化么?
属于。monorepo 是组织代码的方式,pnpm workspace 是管理 monorepo 的工具,它也是处理代码的工具,不会运行代码,所以也属于前端工程化的范畴。
我们公司自研了 IDE,集成了很多内部工具,这属于前端工程化么?
属于。IDE 是围绕代码编辑的场景来打造一系列工具链,也是处理代码但不会运行代码,所以属于前端工程化的范畴。
经过这些例子,相信大家对什么是前端工程化,哪些技术属于前端工程化就比较清晰了。
我们是前端工程师,所以经常谈的是前端的工程化,其实别的语言也有工程化,比如 java 代码,同样需要构建、格式化、静态分析、CI/CD,所以也有工程化的概念。
其实大公司都会有一个工程效能部门,他们做的就是工程化的事情,不过一般是跨语言的工程化,并不局限于前端工程化、后端工程化等。
总结
前端工程化是指围绕代码处理的一系列工具链,他们把代码当作字符串处理,并不运行代码,包括编译构建、静态分析、格式化、CI/CD 等等。
我们详细了解了编译构建的历史,从任务运行器、打包工具到 no bundle 服务的演变历史,他们都是特定时代下的产物。
再就是静态分析和格式化用的 eslint、stylelint、prettier、tsc 等工具。
前端工程化的范围可以很大,可以囊括很多工具进来,比如 monorepo、IDE 等等,因为在不同的场景下对代码处理,也就是工程化有不同的需求。
当你对前端工程化有了清晰的定义之后,对于前端工程化要做哪些事情,哪些技术属于前端工程化、哪些不属于,就很容易理清了。