浅析前端工程化中的一部曲——模块化

在日益复杂和多元的 Web 业务背景下,前端工程化经常会被提及。工程化的目的是高性能、稳定性、可用性、可维护性、高效协同,只要是以这几个角度为目标所做的操作,都可成为工程化的一部分。工程化是软件工程中的一种思想,当下的工程化可以分为四个方面:模块化、组件化、规范化、自动化。接下来,此文会浅析其中的一个部分——模块化。

一、什么是模块化

模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程,每个模块完成特定的子功能,所有的模块按某种方法组装起来,成为一个整体,从而完成整个系统所要求的的功能。

块的内部数据与实现是私有的,只是向外部暴露了一些接口和方法与外部其他模块通信。

二、为什么需要模块化

在早期的网页中,一些页面和样式都很简单,有极少交互行为,一个页面也不会依赖很多文件,所以逻辑代码会很少。但随着 Web 技术的发展,各种交互以及新技术使网页越来越丰富,逐渐地我们前端工程师的任务也越来越重,前端代码急速上涨、复杂度也在逐步升高,越来越多的业务逻辑和交互都放在 Web 层实现,代码一多,各种命名冲突、代码冗余、文件间依赖变大等等一系列问题都会浮现,而且也会导致后期难以维护。

在这些问题上,有很多语言已经有了很多实践经验,那就是模块化。因为小的、组织良好的代码远比庞大的代码更易理解和维护,于是前端也开启了模块化历程。

三、JS 模块化规范

CommonJS规范

2009年,美国程序员 Ryan Dahl 以 CommonJS 规范为基础创造了 node.js 项目,将 JS 语言用于服务器端编程,为前端奠基,此后,nodejs 就成为了 CommonJS 的代名词。

1. 概述

CommonJS 规范中规定每个文件就是一个独立的模块,有自己的作用域,模块的变量、函数、类都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。

2. 特点

1)所有代码都运行在模块作用域。不会污染全局作用域。

2)模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存,以后再加载,就直接读取缓存结果,要想让模块再次运行,必须清除缓存。

3)模块加载的顺序,按照其在代码中出现的顺序。

3. 基本语法

外部想要调用,必须使用 module.exports 主动暴露,而在另一个文件中引用则需要使用 require(),如下:

// moduleA.js
var a = 1;
var b = 2;
var add = function() {
    return a + b
}

module.exports.a = a;
module.exports.b = b;
module.exports.add = add;

// 引用
var moduleA = require('./moduleA.js');

console.log(moduleA.a)  // 1
console.log(moduleA.b)  // 2
console.log(moduleA.add(a,b))  // 3

4. 加载机制

CommonJS 模块的加载机制是:输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值了。

5. 总结

CommonJS 是模块化的社区标准,而 Nodejs 就是 CommonJS 模块化规范的实现,它对模块的加载时同步的,也就是说,只有引入的模块加载完成,才会执行后面的操作,在 Node 服务端应用当中,模块一般存在本地,加载较快,同步问题不大,在浏览器中就不太合适了,如果一个很大的项目,所有的模块都要同步加载,那可想而知,体验感是极差的,所以还需要异步模块化方案。

AMD规范

AMD(异步模块定义)是专门为浏览器环境设计的,是非同步加载模块,允许指定回调函数。

1. 基本语法

define(id?: String, dependencies?: String[], factory: Function|Object)

  • id:模块的名字,字符串类型,可选参数;
  • dependencies:依赖的模块列表,一个字符串数组,可选参数。每个依赖的模块的输出将作为参数一次传入 factory 中,如果没有指定 dependencies,那它的默认值是 ["require", "exports", "module"];
  • factory:包裹了模块的具体实现,可以为函数或对象,如果是函数,返回值就是模块的输出接口或者值;

使用 define() 定义暴露模块,引入模块需要使用 require(),如下:

// 定义没有依赖模块
define(function() {
    return 模块
})

// 定义有依赖模块
define(['module1', 'module2'], function(m1, m2) {
    return 模块
})


// 引入使用模块
require(['module1', 'module2'], function(m1, m2) {
    使用m1/m2
})

2. RequireJS

RequireJS 是一个遵守 AMD 规范的工具库,用于客户端的模块管理。它就是通过 define 方法将代码定义为模块,通过 require 方法,实现代码的模块加载,使用时需要下载和导入,也就是说在浏览器中想要使用 AMD 规范时先在页面中引入 require.js 就可以了。

3. 总结

AMD 模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系。AMD 模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块,RequireJS 就是 AMD 的标准化实现。

CMD规范

CMD 是 SeaJS 在推广过程中对模块定义的规范化产出,而 CMD 规范以及 SeaJS 在国内曾经十分被推崇,原因不只是因为它足够简单方便,更是因为 SeaJS 的作者是阿里的玉伯大佬所写。

1. 概述

CMD 规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD 规范整合了CommonJS 和 AMD 规范的特点。在Sea.js 中,所有 JavaScript 模块都遵循 CMD 模块定义规范。

2. 基本语法

define(factory: Function|Object|String)

在 CMD 规范中,一个模块就是一个文件,define 是一个全局函数,用来定义模块。define 接收 factory 参数,它可以是一个函数,也可以是一个对象或字符串。

1)参数为函数时,如下:

// 定义没有依赖的模块
define(function(require, exports, module) {
    exports.xxx = value;
    module.exports = value;
})

// 定义有依赖的模块
define(function(require, exports, module) {
    // 引入依赖模块(同步)
    var module1 = require('./module1')
    // 引入依赖模块(异步)
    require.async('./module2', function(m2) {
        ...
    })
    // 引入依赖模块(条件)
    if(status) {
        var m3 = require('./module3')
    }

    // 暴露模块
    exports.xxx = value
})


// 引入模块
define(function(require) {
    var m1 = require('./module1')
    var m2 = require('./module2')
    m1.show()
    m2.show()
})
  • require :是一个方法,接收模块标识作为唯一参数,用来获取其他模块提供的接口;
  • exports:是一个对象,用来向外提供模块接口;
  • module:是一个对象,上面存储了与当前模块相关联的一些属性和方法;

2)参数为对象和字符串时,表示模块的接口就是该对象和字符串,如下:

// factory为JSON数据对象
define({"name": "bao"});

// factory为字符串模板
define('my name is {{name}}');

3. 核心实现

对于 CMD 规范下的 SeaJS,同 AMD 规范下的 RequireJS 一样,都是浏览器端模块加载器,两者很相似,但又有明显不同,CMD 与 AMD 的区别是 AMD 推崇依赖前置,而 CMD 推崇依赖就近。

CMD 推崇尽可能的懒加载,也称为延迟加载,即在需要的时候才加载。对于依赖模块,AMD 是提前执行,CMD 是延迟加载,两者执行的方式不同,AMD 执行过程中会将所有依赖前置执行,也就是在自己的代码逻辑开始前全部执行,而 CMD 如果 require 引入了整个逻辑并未使用这个依赖或未执行到逻辑使用它的地方前是不会执行的。

ES6模块化

2015年6月,ESMAScript2015 也就是我们所说的 ES6 发布了,JS 终于在语言标准的层面上,实现了模块功能,设计思想是尽量的静态化,使得在编译时就能确定模块的依赖关系,以及其输入和输出的变量,不像 CommonJS、AMD 之类的需要在运行时才能确定,成为浏览器和服务器通用的模块解决方案。

所以说在 ES6 之前 JS 是没有官方的模块机制的,ES6 在语言标准的层面上,实现了模块化功能,而且实现的相当简单,旨在成为浏览器和服务器通用的模块化解决方案。

1. 基本语法

export 命令用于规定模块的对外接口,import 命令可以输入其他模块提供的功能。export 可以导出的是对象中包含多个属性、方法,提供的 export default 命令只能导出一个可以不具名的函数。

1)使用 export 导出,import 导入,如下:

// 定义模块module1
var count = 0;
var add = function(a, b) {
    return a + b;
}

export {count, add};


// 引用模块
import {count, add} from './module1';

function test() {
    return add(1, count);
}

2)使用 import 命令导入时,需要知道所要加载的变量名或函数名,否则无法加载。export default 命令可以为模块默认输出,如下:

// 导出module2
export default function() {
    console.log('this is a msg');
}

// 引入
import msgFun from './module2';

msgFun(); // 'this is a msg'

2. ES6 模块与 CommonJS 模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用;
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口;

第二个差异是因为 CommonJS 加载的是一个对象,该对象只有在脚本运行完才会生成;而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

ES6 模块运行机制与 CommonJS 运行机制不一样。js 引擎对脚本静态分析的时候,遇到模块加载指令后会生成一个只读引用,等到脚本真正执行的时候,才会通过引用模块中获取值,在引用到执行的过程中,模块中的值发生变化,导入到这里也会跟着发生变化。ES6 模块是动态引入的,并不会缓存值,模块里总是绑定其所在模块。

四、总结

对于 JS 模块化,上述方案都在解决几个同样的问题:

  • 全局变量的污染
  • 命名冲突
  • 繁琐的文件依赖

不同的模块化手段都在致力于解决这些问题。前两个问题其实很好解决,使用闭包配合立即执行函数就可以实现,难点在于文件依赖关系的梳理以及加载。CommonJS 在服务端使用 fs 模块同步读取文件,而在浏览器中,不管是使用 AMD 规范的 RequireJS 还是 CMD 规范的 SeaJS,其实都是使用动态创建 script 标签方式加载,在依赖加载完毕之后再执行。

ESM 作为语言标准层面的模块化方案,不需要我们额外引入用于模块化的三方包,完全可以取代 CommonJS 和 AMD 等规范,成为浏览器和服务器通用的模块解决方案。抛开兼容问题,绝对是最好的选择,也是未来趋势,这点在 Vite 上就足以证明。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值