前端学习-前端工程化、模块化

309 阅读7分钟

什么是前端工程化

在前端开发过程中,以降低成本、提高效率、保障质量为目的,通过一系列规范、工具、流程作为手段的实践体系,是处理代码的一系列工具链,他们并不关心代码的内容,只是把代码作为字符串来进行一系列处理。

  • 规范:是完成软件开发的各项任务的技术方法,为软件开发提供“如何做”的技术。
  • 工具:为运用方法而提供的自动的或半自动的软件工程的支撑环境。
  • 流程:是为了获得高质量的软件所需要完成的一系列任务的框架。

工程化的几个阶段

  • 编译构建
  • 静态分析
  • 代码规范
  • 代码托管
  • 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。

静态分析

静态分析是指在不运行代码的情况下,对前端代码进行分析的过程,包含语法分析、代码风格、潜在错误、安全漏洞检查等。

  1. 语法分析:可以检查 html 标签是否闭合、css 的属性是否正确、js 的书写是否合规
  2. 代码风格:代码缩进、变量命名规范
  3. 潜在错误:未使用的变量会标注、使用 ts 会标注变量类型错误
  4. 安全漏洞检查: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';

参考文章

你能给前端工程化下个定义么?

我在美团三年的前端工程化实践回顾

前端领域的转译打包工具链(下):工程化闭环

前端模块化详解(完整版)