模块化技术
创始人
2025-05-31 13:40:25

前端模块化

1.什么是前端模块化

  • 将复杂的程序根据规则或者规范拆分成若干模块,一个模块包括输入和输出

  • 模块化的内部数据和实现是私有的,对外暴露一些接口与其他模块进行通信

2.前端模块化的背景

  • 前端模块化是一种标准,不是实现

  • 理解模块化是理解前端工程化的前提

  • 前端模块化是前端项目规模化的必然结果

3.脚本和模块的区别

有很多同学会对脚本和模块之间产生一定的混淆,我这里通过一张图来帮助大家区分两者的不同。

4.前端模块化的进化过程

4.1 全局function模式:将不同功能封装成不同的全局函数

  • 缺陷:容易引发全局命名空间冲突,而且模块成员之间看不出直接关系

// 所有function都是挂在window下面的
funtion api(){return {xxx}
}functionhandle(data, key){return xxx
}functionsum(a, b){return a + b;
}const data = api();
const a = handle(data, 'a')

4.2 全局namespace模式

  • 作用:减少了全局变量,解决了命名冲突

  • 缺陷:存在数据安全的问题,外部可以直接修改模块内部数据

window.__Module = {x: 1,api(){xxx},handle(){xxx},sum(a,b){return a + b}
}constmodule = window.__Module
consr data = module.api()console.log(module.x) // 1module.x = 2

4.3 IIFE模式:匿名函数自调用 -- 闭包

  • 作用:通过自执行函数创建闭包,解决私有化的问题,外部只能通过暴露的方法操作

  • 缺陷:无法解决模块间相互依赖的问题

(function(window){var x = 1;functionapi(){xxx}functionsetX(v){x = v}functiongetX(){return x}window.__Module = {x,setX,getX,api,}
})(window)const m = window.__Module// 这里改的是函数作用域内变量的值
m.setX(10)
console.log(m.getX()) // 10// 这里改的是对象属性的值,不是修改的模块内部的data
m.x = 2console.log(m.getX()) // 10

4.4 IIFE模式增强,支持传入自定义依赖

  • 作用:通过模块间参数的传递,来实现解决模块间的依赖问题

  • 缺陷:

  • 多依赖传入时,代码阅读困难

  • 无法支持大规模的模块化的开发

  • 无特定语法支持,代码简陋

A: __Module_API模块

(function(global){var a = 1;functionapi(){return {code: 0,data: {a,b: 2}}}functionhandle(data, key){return data.data[key]}global.__Module_API = {api,handle}
})(window)

B:__Module模块

(function(global, moduleAPI){functionsum(a, b){return a + b;}global.__Module = {api: moduleAPI.api,handle: moduleAPI.handle,sum,}
})(window, window.__Module_API)constmodule = window.__Module
const data = module.api.api()
const a = module.api.handle(data, 'a')

通过将Module_API模块作为入参传入到Module模块中,实现在Module模块中引用依赖Module_API的一些模块方法

5.前端模块化的好处

  • 减少了全局变量,解决了命名冲突

  • 能够更好的分离,实现按需加载

  • 有更高复用性和更高可维护性

CommonJS模块化规范

1.CommonJs规范介绍

  • Node.js默认的模块化规范,每个文件就是一个模块,有自己的作用域

  • Node中CJS模块加载采用在服务器端运行时同步加载方式,在浏览器端提前编译打包处理方式

  • 通过require加载模块,通过exports或module.exports输出模块

2.CommonJS规范特点

  • 所有代码都运行在模块作用域,不会污染全局作用域

  • 模块可以多次加载,第一次加载时会运行模块,模块输出结果会被缓存,再次加载时,会从缓存结果中直接读取模块输出结果

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

  • CommonJS 规范的核心变量: exports、module.exports、require

  • CommonJS规范规定,每个模块内部,module变量代表当前模块。

  • 这个module变量是一个对象,它的exports属性(module.exports)是对外的接口, 负责对模块中的内容进行导出

// lib.jsvar counter = 3;
functionincCounter() {counter++;
}
module.exports = {counter: counter,incCounter: incCounter,
};
  • require 函数基本功能就是读入并执行一个JavaScript文件,然后返回该模块的exports对象。

const mod = require('./lib')
  • 模块输出的值是值的拷贝,类似IIFE方案中的内部变量

3.CommonJS加载机制

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

请看下面这个模块文件lib.js的例子。

// lib.jsvar counter = 3;
functionincCounter() {counter++;
}
module.exports = {counter: counter,incCounter: incCounter,
};

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js里面加载这个模块。

// main.jsvar mod = require('./lib');console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。比如改成下面的这种写法

// lib.jsvar counter = 3;
functionincCounter() {counter++;
}
module.exports = {getcounter() {return counter},incCounter: incCounter,
};

这样输出的counter属性就变成了一个取值器的函数,就可以正确读取到内部变量counter的变动。

ESModule模块化规范

1.简单了解AMD规范和CMD规范

1.1 AMD规范:

  • AMD规范采用非同步加载模块,允许指定回调函数(针对commonjs同步而诞生的规范)

  • Node模块主要用于服务器编程,模块文件通常都位于本地硬盘,加载起来速度比较快,所以适用于CommonJS的这种同步加载

  • 但是浏览器环境下,模块需要请求获取,要从服务端加载模块,所以适用于异步加载,一般采用AMD规范

  • require.js是AMD的一个具体实现库

AMD基本语法 -- 定义暴露模块
//定义没有依赖的模块
define(function(){return 模块
})//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){return 模块
})
AMD基本语法 -- 引入使用模块
require(['module1', 'module2'], function(m1, m2){使用m1/m2
})

1.2 CMD规范

  • CMD专门用于浏览器端,整合了CommonJS和AMD的优点,模块的加载是异步的,模块使用时才会加载执行

  • Sea.js是CMD规范的一个实现

CMD基本语法 -- 定义暴露模块
//定义没有依赖的模块
define(function(require, exports, module){exports.xxx = valuemodule.exports = value
})//定义有依赖的模块
define(function(require, exports, module){//引入依赖模块(同步)var module2 = require('./module2')//引入依赖模块(异步)require.async('./module3', function (m3) {console.log(m3)})//暴露模块exports.xxx = value
})
CMD基本语法 -- 引入使用模块
define(function (require) {var m1 = require('./module1')var m4 = require('./module4')m1.show()m4.show()
})
注意:AMD 和 CMD 规范现在已经不怎么去用了。AMD和CMD最大的问题是没有通过语法升级解决模块化(它们定义模块还是通过调用js的方式定义一个模块,它没有办法对模块进行规模化的引用)
所以我们现在主流的使用:node环境下用commonjs,浏览器环境下用ESModule

2.ESModule规范介绍

  • ESModule设计理念是希望在编译的时就确定模块的依赖关系及输入输出

  • CommonJS和AMD都只能在运行时才能确定依赖和输入、输出

举例说明:

// ES6模块import { stat, exists, readFile } from'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

3.ESModule模块化语法(结合日常实战开发)

3.1 export命令

工作编码的时候我们常用的一些公共导出方法(例如utils.js文件),经常会采用下面两种方式来书写

第一种写法:
// 获取url上指定参数的值exportconst getQueryString = (name) => {console.log('getQueryString...')
};
// 随机数生成exportconst getRandomString = (len) => {console.log('getRandomString...')
};
// 获取cookieexportconst getCookie = (objName) => {console.log("getCookie....")
};
第二种写法:(推荐)

优先推荐使用这种写法。因为这样我们就可以在脚本尾部,一眼看清楚输出了哪些变量。

而且方便通过as的关键字可以对输出的变量进行重命名。

// 获取url上指定参数的值const getQueryString = (name) => {console.log('getQueryString...')
};
// 随机数生成const getRandomString = (len) => {console.log('getRandomString...')
};
// 获取cookieconst getCookie = (objName) => {console.log("getCookie....")
};export { getQueryString as getQuery, getRandomString, getCookie }

3.2 export default命令

从前面的实战我们可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。

为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

1)实战用法举例1:

导出ace接口文件配置:

// api/ace文件import * as ace from'@/api/common/color.jd.com';// ace.jd.comconst getACEData = async (id: string) => {returnawait ace.get({url: `//api.m.jd.com/client.action?xxx`,});
};
const api = {getACEData,
};
exportdefault api;

引用ace文件:(import命令可以为该函数指定任意名字。)

import ACE_API from'@/api/ace';
const res = await ACE_API.getACEData(123);
注意:需要注意的是,这时import命令后面,不使用大括号。
2)实战用法举例2:

如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。

import _, { each, forEach } from'lodash';

对应上面代码的export语句如下

exportdefaultfunction (obj) {// ···
}exportfunctioneach(obj, iterator, context) {// ···
}export { each as forEach };

比如我们平时用react框架开发时,也会用到这种写法:

import React, { useEffect, useState } from'react';

3.3 import命令

使用import命令有一些重要的关键点,这里给大家列举阐述一下:

  • 可以使用as关键字将输入变量重命名

import { lastName as surname } from'./profile.js';
  • import命令是只读的,不允许在加载模块的脚本里面,改写接口。

import {a} from'./xxx.js'a = {}; // Syntax Error : 'a' is read-only;
  • import命令具有提升效果,会提升到整个模块的头部,首先执行

下面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

foo();import { foo } from'my_module';
  • 多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。

  • 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

// 报错import { 'f' + 'oo' } from'my_module';// 报错letmodule = 'my_module';
import { foo } frommodule;// 报错if (x === 1) {import { foo } from'module1';
} else {import { foo } from'module2';
}

3.4 模块的整体加载

我们可以用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

日常开发举例:

比如我们开发所用到的公共方法类文件(utils.js文件)

// utils.js// 获取url上指定参数的值const getQueryString = (name) => {console.log('getQueryString...')
};
// 随机数生成const getRandomString = (len) => {console.log('getRandomString...')
};
// 获取cookieconst getCookie = (objName) => {console.log("getCookie....")
};export { getQueryString as getQuery, getRandomString, getCookie }

那么我们整体加载的写法可以更改如下:

import * as utils from'@/utils/utils';
utils.getQueryString('shopId')

4.CommonJS和ESModule规范对比

这里主要对上面两种不同的规范进行一个对比总结:

4.1 CommonJS模块输出的是值的拷贝,ES6模块输出值的引用

ESModule举例:输出的a是一个地址,这个值变化后面是跟着变化的

// test.jsexportlet a = 1;
exportfunctionplus(){a++;
}// entry.jsimport { a , plus } from'./test.js'console.log(a); // 1
plus();
console.log(a); // 2

CommonJS是对值是进行拷贝的,例如这里是对值a的一个拷贝

// test.jslet a = 1;exports.a = a;
exports.plus = function(){a++;
}
exports.get = function(){return a;
}// entry.jsconst { a, plus, get } = require('./test.js')
console.log(a) // 1
plus();
console.log(a) // 1console.log(get()) // 2

4.2 CommonJS模块运行时加载,ES6模块是编译时输出接口

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

4.3 CommonJS模块为同步加载,ES6模块支持异步加载

// ESModule 可以通过promise的方式异步加载import('./test.js').then(mod =>{console.log('mod', mod)
})

4.4 CommonJS中this是当前模块,ES6模块的this是undefined

// commonjsconsole.log(this === module.exports)

5. 浏览器模块化的局限

缺乏模块管理能力,模块分散在各个项目中 ---- npm统一管理

性能加载慢,无法在大型项目中直接使用 --- webpack性能优化

npm + webpack原理

前端工程化之关键技术npm + webpack原理

1.npm包管理工具

1.1 npm诞生背景

  • npm由程序员Isaac发明

  • 初步思路

  • 集中管理所有模块,所有模块都上传到仓库(registry)

  • 模块内创建package.json标注模块的基本信息

  • 通过npm publish发布模块,上传到仓库(registry)

  • 通过npm install安装模块,模块安装到node_modules目录

1.2 npm介绍

  • npm解决的核心问题是模块管理问题

  • npm规范:package.json管理模块信息,node_modules保存依赖

1.3 npm原理分析

因此我们可以总结出如下常用的命令:

npm init创建模块,npm install 安装模块,npm publish发布模块

npm link本地开发,npm config 调整配置,npm run调用scripts

1.4 npm局限

  • npm只能解决模块的高效管理和获取问题

  • npm无法解决性能加载问题

  • 模块化发明后,制约其广泛应用的因素就是性能问题

2.webpack代码编译工具

2.1 webpack诞生背景

  • Webpack 2012年3月10号诞生,作者是Tobias

  • webpack的出现模糊了任务和构建的边界,使之融为一体

webpack诞生之前专门有一些工具是做任务的,例如gulp或者grunt。任务就是每一步要干什么东西,由这个任务的引擎来决定。构建是由其他工具来决定

2.2 webpack原理

  • 最初的webpack核心解决的是代码合并与拆分

  • webpack的核心理念是将资源都视为模块,统一进行打包和处理

  • webpack提供了loader和plugins完成功能扩展

总结

本节从前端模块化发展历史,衍生出了CommonJS规范、AMD规范、CMD规范到现在的ESModule,成为浏览器和服务器通用的模块解决方案。再加上npm管理工具和webpack打包编译工具的诞生,一举突破了前端工程化的关键技术。

相关内容

热门资讯

重大通报-"麻友圈2... 您好:麻友圈2贵阳捉鸡这款游戏可以开挂,确实是有挂的,需要软件加微信【5991307】,很多玩家在麻...
我来教大家“无为好运麻将有没有... 您好:无为好运麻将这款游戏可以开挂,确实是有挂的,需要了解加客服微信【3398215】很多玩家在这款...
独家实测.YY棋牌有没有挂.其... 独家实测.YY棋牌有没有挂.其实是有挂!亲,YY棋牌这个游戏其实有挂的,确实是有挂的,需要了解加客服...
今日了解" 麻友圈2... 有 亲,根据资深记者爆料麻友圈2是可以开挂的,确实有挂(咨询软件无需打开...
玩家必看“天天重庆麻将究竟有没... 您好:天天重庆麻将这款游戏可以开挂,确实是有挂的,需要软件加微信【4830828】很多玩家在这款游戏...