前端进阶(三)前端模块化

本文深入探讨了模块化编程的概念及其在不同JavaScript环境中的应用。包括CommonJS、AMD、CMD等模块化规范的特点与区别,以及ES6模块化的具体实现方式。文章还详细解释了这些规范在解决实际开发问题中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是模块化?

模块化开发是一种管理方式,是一种生产方式,一种解决问题的方案。他按照功能将一个软件切分成许多部分单独开发,然后再组装起来,每一个部分即为模块。当使用模块化开发的时候可以避免刚刚的问题,并且让开发的效率变高,以及方便后期的维护。

模块化主要解决:命名冲突(变量和函数命名可能相同),文件依赖(引入外部的文件数目、顺序问题)等。

一、ES5及ES5之前如何模块化?

要回答这个问题,首先先了解几个概念。

通行的JavaScript模块规范主要有三种:CommonJS、AMD和CMD。

(1)CommonJS

2009年,美国软件工程师Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。NodeJS是CommonJS规范的实现,webpack 也是以CommonJS的形式来书写。

Common.js优点在于:

1.CommonJS模块规范很好地解决变量污染问题,每个模块具有独立空间,互不干扰,命名空间等方案与之相比相形见绌。

2.CommonJS规范定义模块十分简单,接口十分简洁。导入是通过require,导出通过module.exports或者exports。

require一个模块一般会有两种情况:

require的模块第一次被加载。这时首先会执行县该模块,然后导出内容。

require的模块非首次加载。这时模块的代码不会再次执行,而是直接导出上次执行后得到的结果。

我们可以简单理解为commonjs在每个导出的模块首部添加了如下代码:

var module = {
    exports: {}
}

var exports = module.exports;

在使用exports时要注意,不要直接给exports赋值,负责会导致其失效。

但是CommonJS规范不适用于浏览器环境。原因是缺少四个Node.js环境的变量:module/exports/require/global,并且是同步的(意味着在浏览器中会阻塞),CommonJS是主要为了JS在后端的表现制定的,它是不适合前端的。

只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。Browserify 是目前最常用的 CommonJS 格式转换的工具,虽然 Browserify 很强大,但不能在浏览器里操作。

解决思路:

一是开发一个服务器端组件,对模块代码作静态分析,将模块与它的依赖列表一起返回给浏览器端。 但需要服务器安装额外的组件,并因此要调整一系列底层架构。

二是用一套标准模板来封装模块定义,但是对于模块应该怎么定义和怎么加载,又产生的分歧,分成了两个思路:AMD和CMD。

目前node主要编译工具统一为GYP工具,好处主要有两点:一是node的源码就是通过GYP编译的;二是可以跨平台编译。

(2)AMD 

AMD即Asynchronous Module Definition(AMD wiki中文版),中文名是异步模块定义的意思。它是一个在浏览器端模块化开发的规范。由于AMD不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎RequireJS,实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出。目前,主要有两个Javascript库实现了AMD规范:require.jscurl.js

本人只用过require.js,所以简单可以谈谈require.js。

它的优势主要在于异步模块化加载js文件,并且能很好的管理js文件的依赖关系。主要思想就是将页面用到的js全部异步加载。

1.实现js文件的异步加载,避免网页失去响应;

2.管理模块之间的依赖性,便于代码的编写和维护。

require.js要求,每个模块是一个单独的js文件。这样的话,如果加载多个模块,就会发出多次HTTP请求,会影响网页的加载速度。因此,require.js提供了一个优化工具,当模块部署完毕以后,可以用这个工具将多个模块合并在一个文件中,减少HTTP请求数。虽然require.js是基于AMD规范的,但是也可以加载非AMD规范的js文件。

(3)CMD

CMD 即Common Module Definition通用模块定义,CMD规范是国内发展出来的,就像AMD有个require.js,CMD有个浏览器的实现sea.js,sea.js要解决的问题和require.js一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同。

阿里大神玉伯写了sea.js,就是遵循他提出的CMD规范,与AMD非常相近,核心思想就是按需加载,不会等待较长时间。腾讯阅文集团的张鑫旭大神维护的seajs文档

SeaJS与RequireJS最大的区别

豆瓣上面这个答案说的比较好:SeaJS对模块的态度是懒执行, 而RequireJS对模块的态度是预执行。

二、ES6模块化

EcmaScript6 标准增加了JavaScript语言层面的模块体系定义。

在 ES6 中,我们使用export关键字来导出模块,使用import关键字引用模块。

1、导出

  • 命名导出
  • 默认导出

先看模块命名导出的两种写法

//写法1
export const name = 'name';
export const add = function(a, b) { return a + b; };
//写法2
const name = 'name';
const add = function(a, b) { return a + b; };
export {name, add};

模块的默认导出只能导出一个

export default {
    name: 'name',
    add: function(a, b){
        return a + b;
    }
}

2、导入

ES6 Module中使用import导入模块

整体导入

import * as name from <myModule>;

混合导入---导入一个默认导入和一个命名导入

目前很少JS引擎能直接支持 ES6 标准,因此 Babel 的做法实际上是将不被支持的import翻译成目前已被支持的require。

三、Commonjs和ES6 Module 区别

1、动态和静态

Commonjs和ES6 Module 最本质的区别是Commonjs对于模块依赖的解决是“动态的”,而ES6Module是“静态的”。

“动态”的含义是:模块依赖关系的建立发生在运行时。“静态”的含义是模块依赖关系的建立发生在代码编译阶段。

ES6Module相对于Commonjs来说具备以下优势:

(1)死代码检测和排除。使用静态工具检测那些代码未被调用,提前清除。

(2)模块变量检查。有利于模块间传递的接口或者变量是准确的。

(3)编译器优化。Commonjs等模块传出的都是一个对象,但是ES6传出的是一个变量,减少了引用层级,程序效率更高。

2、值拷贝和动态映射

在导入一个模块时,对于Commonjs来说获取的是一份导出值的拷贝,ES6中则是值得动态映射,并且这个映射是只读的。另一方面来说,Commonjs支持我们对于导出的值修改,但是ES6Module不支持修改,修改会抛出SyntaxError。

3、循环依赖

一般我们应该是要极力避免代码产生循环依赖,但有时实际工程项目中我们会遇到模块间循环依赖的问题。

下面是循环依赖的一个实例:

//b.js
const a = require('./a.js')
console.log('value of a:', a);
module.exports = 'This is b.js';

//a.js
const b = require('./b.js')
console.log('value of b:', b);
module.exports = ''This is a.js;

//index.js
require('./b.js')

这段代码是有循环依赖的,foo和bar两个模块相互依赖,我们预期输出的是:

value of a: This is a.js
value of b: This is b.js

实际输出:

value of b: {}
value of a: This is a.js

原因是当我们在执行了index.js之后,进入b.js后,执行完第一句就跳到a.js中,这时b.js就不会往下执行,而是进入a.js,但a.js中有循环依赖,

最硬核最关键的点的来了,由于我们知道require如果导出过一次,第二次就不会再导入,所以直接导出b.js的值,这时b.js的值为{},继续执行a.js,所以结果就是我们看到的实际输出。

我们用ES6Module改写来解决这个循环引用的问题:

//b.js
import a from './a.js';
console.log('value of a:', a);
exports default 'This is b.js';

//a.js
import b from './b.js';
console.log('value of b:', b);
exports default 'This is a.js;

//index.js
import b from './b.js';

实际输出:

value of b: undefined
value of a: This is a.js

我们如果想要实现想要的结果必须在内部重新指定导出值

//b.js
import a from './a.js';
function b(cur){
    console.log(cur + ' is b.js');
    a('b.js')
}
exports default b;

//a.js
import b from './b.js';
function a(cur){
    if(!invoked){
        cur = true;
        console.log(cur + ' is a.js');
        b('a.js')
    }
}
exports default a;

//index.js
import b from './b.js';
b('index.js');

因为ES6代码import发生在编译期,所以我们import时并没有代码运行,编译器将我们所有的函数都绑定。等到运行期后,直接运行b() -> a()->b()

执行结果如下:

index.js is b.js
b.js is a.js
a.js is b.js

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fullstack_lth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值