什么是前端工程化
在前端开发过程中,以降低成本、提高效率、保障质量为目的,通过一系列规范、工具、流程作为手段的实践体系,是处理代码的一系列工具链,他们并不关心代码的内容,只是把代码作为字符串来进行一系列处理。
- 规范:是完成软件开发的各项任务的技术方法,为软件开发提供“如何做”的技术。
- 工具:为运用方法而提供的自动的或半自动的软件工程的支撑环境。
- 流程:是为了获得高质量的软件所需要完成的一系列任务的框架。
工程化的几个阶段
- 编译构建
- 静态分析
- 代码规范
- 代码托管
- CI/CD(持续集成、持续部署)
编译构建
编译构建主要经过三个阶段的发展:task runner 任务运行器、bundle 打包工具、no bundle 不打包方案
1. task runnner 任务运行器
通过任务的形式组织编译过程,对不同的文件用不同的编译器编译、编译完的文件输出到哪个目录,可以指定编译的顺序、以及串行并行的方式。如 gulp。
缺点:每个任务都很独立,不好做全局的优化
2. bundle 打包工具
不组织任务了,通过分析模块之间的依赖关系,从入口文件起分析出一张依赖关系树,对节点用不同的编译器编译。如 webpack。
优点:
- tree shaking:未出现在依赖关系树中的节点无需编译。
- code splitting:允许开发者将代码分割成不同的包(chunk),可以按需加载或并行加载这些包。它可以用来实现更小的包,并控制资源加载优先级,如果使用正确,将对加载时间产生重大影响。
- Dynamic Imports:在代码运行时按需 js 加载模块的机制,是在模块级别进行操作,决定是否加载一个完整的模块。
- lazy load:懒加载的对象范围更广,不限于 js 模块,还涉及图片、样式等各种资源,并且更侧重于资源加载的时机优化。
缺点:每次都要构建依赖图,打包速度慢。
3. no bundle
no bundle 基于浏览器支持 es module 来实现的,浏览器会做 es module 的依赖分析,然后加载对应的模块,这样就不用自己做依赖分析了,只需要实现模块的编译即可。如 vite。
静态分析
静态分析是指在不运行代码的情况下,对前端代码进行分析的过程,包含语法分析、代码风格、潜在错误、安全漏洞检查等。
- 语法分析:可以检查 html 标签是否闭合、css 的属性是否正确、js 的书写是否合规
- 代码风格:代码缩进、变量命名规范
- 潜在错误:未使用的变量会标注、使用 ts 会标注变量类型错误
- 安全漏洞检查:XSS、CSRF
静态分析工具:ESLint、Stylelint、HTMLHint
代码规范
除了上述的 ESLint、Stylelint、HTMLHint 可提供代码格式化提示、还可使用 prettier 对整体进行格式化
代码托管
gitlab 托管代码
代码管理有好几种形式,可学习: 带你了解更全面的 Monorepo - 优劣、踩坑、选型
CI/CD
持续集成、持续部署,可以通过 jenkins 来组织构建流程,当 gitlab 代码有新的 push 的时候触发,进行构建,然后把产物部署到服务器,基于 git hook 的构建部署流程就叫做持续集成、持续部署。
什么是前端模块化
将一个复杂的程序拆分为好几个块,并组合起来,块的内部数据和实现是私有化的,仅通过向外暴露一些方法与其他模块通信。
模块化的几个阶段
- 没有标准的模块化规范时:使用全局 function、namespace、IIFE 模式
- 模块化规范:commonjs、AMD、ES6、CMD
没有标准模块化规范时
没有标准的模块化规范时,经过了如下几个阶段的发展:
1. 使用全局 function:将不同的功能分成不同的函数
缺点:污染全局命名空间,容易引起命名冲突和数据不安全,看不出不同模块之间的关系
function fn1 (){
}
function fn2 (){
}
2. 使用 namespace:对对象进行封装
优点:减少全局变量过多引起的命名冲突
缺点:数据不安全,可以访问对象的任意属性和方法
let myModule = {
data: 'www.baidu.com',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo() // foo() other data
3. IIFE 模式:使用闭包暴露方法
优点:闭包内的数据和方法以及内部实现外部无法获取,只有暴露出的方法外部可以调用(暴露方法的方式如下),保证了模块的独立性
缺点:如果不同模块的闭包有依赖关系,如何设置 <script> 的引入顺序,依赖关系越多,越不好维护
(function() {
function fn() {}
// 方式1
window.fn = fn
// 方式2
return {
fn,
}
})()
模块化规范
1. commonJS
概述:每个文件都是一个模块,模块内部的实现对外不可见,外部仅可通过暴露出的对象来调用模块内部。在服务器端模块是同步加载的,在浏览器端,需要编译打包后运行。
优点:实现模块化,不污染全局作用域
缺点:
- 模块会在第一次加载时缓存,后续调用模块内容会使用缓存的内容,后续只有刷新缓存后模块才会再次加载。
- 模块的加载是按照在代码出现的顺序加载的
- 引入模块的值是输出值的拷贝,而非值的引用,这就导致 module 在导出一个值后,后续不论 module 内这个值如何改变,其他文件最开始引用的数值并不会变
使用方法:
// module 代表当前模块,exports 是对外的接口
// 加载某个模块,其实是加载模块的 module.export 属性
暴露模块:`module.exports = value`或`exports.xxx = value`
// require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象,如果没有发现指定模块,会报错
引入模块:`let module1 = require(xxx)`
2. AMD
概述:相比 commonJS 支持异步加载模块,在浏览器端更适合选择 AMD,在服务器端,文件通常都存储在硬盘,加载较快,不需要考虑异步加载方式,commonJS 较为适用
优点:模块定义清晰、不污染全局环境,能够清楚地显示依赖关系
使用方法:
//定义没有依赖的模块
define(function(){
return 模块
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
return 模块
})
// 引入模块
require(['module1', 'module2'], function(m1, m2){
使用m1/m2
})
3. CMD
概述:CMD 专门用于浏览器端,模块加载是异步的,模块在使用时才会执行。
优点:
使用方法:
//定义没有依赖的模块
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})
// 引入使用模块
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
4. ES6 模块化语法
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
优点:写法简单、好理解,ES6 模块输出的是值的引用。
// 导出模块
export { basicNum, add };
export default fn;
// 引入模块
import { basicNum, add } from './math';