文章目录
引入问题:为什么要进行模块化?
1)模块化就是将系统分离成独立功能的模块,这样我们需要什么功能,就加载什么功能
2)模块化的好处:
- 避免命名空间的冲突(减少命名空间的污染)
- 更好的分离,实现按需加载
- 提高可代码的复用性
- 提高了代码的维护性
JS模块化大致发展过程
CommonJS(服务端)=》AMD(浏览器)=》CMD=》ES6 Module模块化
模块化规范的种类
模块化规范的发展趋势
终极:现代工具webpack
- webpack自己实现了一套模块机制,无论是CommonJs模块的require语法还是ES6模块的import语法,都能够被解析并转换成指定的环境的可运行代码。(随着webpack打包工具的流行,ES6语法广泛手中,后来开发者对于AMD CMD的感知越来越少)
1.CommonJS规范
1.1说明:
- 每个文件都可以作为一个模块(这里的文件指的是js文件)
- 在服务器端:模块的加载是运行时同步加载的
- 在浏览去端:模块需要提前编译打包处理,不然浏览器不能识别require语法
1.2 使用
主要分为定义模块和引入模块两个步骤
定义模块语法:
1)module.exports=value
2)export.xxx=value
引入模块语法
var xxx=require(url)
模块标识:
模块标识就是require()函数的参数,规范是这样的:
- 必须时字符串
- 可以时以./ …/开头的相对路径
- 可以时绝对路径
- 可以省略后缀名
其中,当引入的模块为自定义的模块时,那么url则是该模块所在的路径
当引入的模块是第三方模块时,则url为其具体包名
标准内容:
- 模块通过
exports
来向外暴露API,exports
只能是一个对象,暴露的API需作为此对象的属性 - 定义全局函数
require
,通过传入模块标识来引入其他模块,执行的结果即为别的模块暴露出来的API - 如果被reqiure函数引入的模块中也包含依赖,那么以此加载这些依赖
特点
- 模块加载是一项阻塞操作,也就是同步加载
1.2.1模块定义与使用
举例1:
- 方式①(module1.js)
module.exports={
var msg="i am msg"
foo(){
console.log("i am ①");
}
}
- 方式②(module2.js)
//这里暴露处的是一个函数
//当引入的方式为
//var module2=require('./module2')时,
//便可以直接使用 module2()调用即可
//当引入的方式为
//var fun=require('./module2')时,
//便可以直接使用 fun()调用即可
module.exports=function(){
return ...
}
如:
module.exports=function(){
console.log('i am ② ');
}
- 方式③(module3.js)
module.exports.xxx=function(){
....
}
如:
module.exports.foo=function(){
console.log('i am ③');
}
module.exports.bar=function(){
console.log('i am bar from ③');
}
- 引入(一般在主模块中如main.js)
/*说明:
./module1是该模块所在的路径
*/
const module1=require('./module1');
const module2=require('./module2');
const module3=require('./module3');
//引入第三方模块
var http = require('http');
//使用
module1.foo();
module2();
module3.foo();
module3.bar();
举例2:
// file greeting.js 定义一个模块
var helloInLang = {
en: 'Hello world!',
es: '¡Hola mundo!',
ru: 'Привет мир!'
};
var sayHello = function (lang) {
return helloInLang[lang];
}
// 对外输出
module.exports.sayHello = sayHello;
// file hello.js 引入一个模块
var sayHello = require('./lib/greeting').sayHello;
var phrase = sayHello('en');
console.log(phrase);
举例3:
// a.js
module.exports = {
moduleFunc: function() {
return true;
};
}
// 或
exports.moduleFunc = function() {
return true;
};
// 在 b.js 中引用
var moduleA = require('a.js');
// 或
var moduleFunc = require('a.js').moduleFunc;
console.log(moduleA.moduleFunc());
console.log(moduleFunc())
2.AMD规范
2.1说明
- CommonJS规范出现后,在Node开发中产生了非常好的效果,开发者希望借鉴这个经验来解决浏览器JS的模块化
- 但是大部分人认为浏览器和服务器的环境差别太大,毕竟浏览器JS时通过网络动态以此加载的,而服务器的JS是保存在本地磁盘中。因此浏览器需要实现异步加载,模块在定义的时候就必须先知名它所需要依赖的模块,然后把本模块的代码写在回调函数中执行,最终衍生出了AMD规范
- AMD的主要思想时异步模块,主逻辑在函数回调中执行
2.2 标准内容
1.定义没有依赖的模块
module1.js
define(function(require,exports.module){
return 模块
}
2.定义具有依赖的模块
module2.js
define(['module1','module2'],function(m1,m2){
return 模块
}
3.引入模块
main.js
require(['module1','module2'],function(m1,m2){
})
2.3 举例使用:
//module.js
define(function (require, exports, module) {
console.log('module.js')
exports.name = "jack" //暴露
})
//module2.js
define(function (require, exoprts, module) {
console.log('module2.js');
exports.desc = "hello world"
})
//main.js
require(['module1', 'module2'], function (m1, m2) {
console.log('main.js');
console.log(m1.name + ',' + m2.desc); //引入module1和module2之后,直接使用其暴露的属性
})
// 执行顺序:
// module1.js
// module2.js
// main.js
人无完人,AMD/RequireJS 也存在饱受诟病的缺点。按照 AMD 的规范,在定义模块的时候需要把所有依赖模块都罗列一遍(前置依赖)
,而且在使用时还需要在 factory 中作为形参传进去。
CMD/RequireJS模块化的顺序是这样的:模块化加载=》全部模块预执行=》主逻辑中调用模块
所以是依赖加载完成后会先预先将模块执行一遍,这种方式会使得程序效率低;
define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){
..... });
3.CMD规范
3.1 说明
- AMD/RequireJS的JS模块实现有很多不优雅的地方,主要原因不能以一种更好的管理模块的依赖加载和执行;
那么就出现了SeaJS,SeaJs遵循的是CMD规范,CMD规范在AMD的基础上改进的一种规范,解决了AMD对依赖模块的执行时机的问题; - SeaJS模块化的顺序是:
模块化预加载=》主逻辑调用模块时才执行模块中的代码
- SeaJS的用法和AMD基本相同,并且融合了
CommonJS的写法:
3.2 使用
(对于模块的引入,具有同步和异步两中方式)
//module1.js
define(function (require, exports, module) {
console.log('module1.js')
// module.exports = value;
// exports.xxx = value
exports.name="i am module1"
})
//main.js
define(function (require, exports, module) {
//引入依赖模块(同步)
var module2 = require('./module2');
console.log(module2.name)
//引入依赖模块(异步1)
require.async('./module3', function (m3) {
//这里m3对应module3
})
//引入依赖模块(异步2)
var module4=require.async('./module4');
console.log(module4.name)
})
总结:
SeaJS的出现,是CommonJS在浏览器的践行者,并吸收了RequireJS的优点
4. ES6中的Module模块
4.1 标准内容
- 模块功能主要由两个命令构成:
export
和import
export
用于暴露接口,import
用于引入模块
4.2 模块的定义
有如下3中方式
- 方式1-----分别暴露
//module1.js
export var m=1
export var arr=[1,2,4]
export function fun(){
console.log('i am a fun')
}
//引入与使用(结构引入):
import {
m,arr,fun} from './module1'
console.log(m);
fun()
- 方式2—统一暴露
//module2.js
var m=1;
var arr=[1,2,4]
function fun() {
console.log('i am a fun')
}
export {
m,arr,fun}
//引入与使用(结构引入):
import {
m,arr,fun} from '/module2.js'
console.log(arr);
fun()
- 方式3----默认暴露
//这里默认暴露对象
//module3.js
//export default其实是导出一个叫做default的变量,所以其后面不能跟变量声明语句。
//错误
export default var a = 1;
//正确
export default {
m:1,
fun(){
console.log('i am a fun from export defalut')
}
}
//person.js
export default function getName(){
...
}
//引入和使用(module为自定义任意名字)
import module from './module3.js'
module.fun()
4.4 模块的引入
// 解构引入
import {
firstName, lastName, year } from 'a-module';
// 为输入的变量重新命名
import {
lastName as surname } from 'a-module';
// 引出模块对象(引入所有)
import * as ModuleA from 'a-module';
- 在使用 ES Module 值得注意的是:import 和 export 命令只能在模块的顶层,在代码块中将会报错,这是因为 ES Module 需要在编译时期进行模块静态优化,import 和 export 命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行,这种设计有利于编译器提高效率,但也导致无法在运行时加载模块(动态加载)。
对于这个缺点,TC39 有了一个新的提案 – Dynamic Import,提案的内容是建议引入 import()
方法,实现模块动态加载。
// specifier: 指定所要加载的模块的位置
import(specifier)
- import()方法返回一个Promise对象
import('b-module')
.then(module => {
module.helloWorld();
})
.catch(err => {
console.log(err.message);
});
PS:
import()
函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。
它是运行时执行,也就是说,什么时候运行到这句话,就会加载到指定的模块。另外,import()
函数所加载的模块没有静态链接关系,这点也是与import
语法不同- 注意的时ES6 的Module语法有些浏览器是不支持的,因此需要Babel先进性转码,将import和export命令转成ES5语法才能被浏览器解析。
这里举例之前做过的一个小项目,就是使用了ES6语法中的module模块化思想结合axios库,将需要向服务端发送请求的操作封装到一个模块中,然后对于不同的请求数据操作,直接导入该模块调用即可
1)封装ajax请求函数(使用的时默认暴露)
//首先向外暴露一个函数
//默认为GET请求
import axios from 'axios'
export default function ajax(url,data={
},type='GET') {
return new Promise(function (resolve, reject) {
//这里的return主要时结果的回调函数,即成功了则回调sesolve函数,失败了则回调reject函数
//即异步返回的数据是response.data
let promise
if (type === 'GET') {
//这里的目的主要是想将参数拼接成url?username=xx&password=XX
let dataString = '';
Object.keys(data).forEach(key => {
dataString += key + '=' + data[key] + '&'
})
if (dataString != null) {
dataString = dataString.substring(0, dataString.length - 1);
url = url + '?' + dataString;
}
//使用axios发送get请求
promise = axios.get(url)
///console.log(promise)
} else {
//使用axios发送post请求
//post请求不用
promise = axios.post(url, data)
}
//response是axios发送请求后得到的promise对象中的response,
//最后因为响应体的数据杂多,这里只取response中的data
promise.then(function (response) {
//成功了则调用resolve
resolve(response.data);
}).catch(function (error) {
//失败了则调用reject
reject(error)
})
})
}
2)封装接口请求函数----使用的时分别暴露
(这个接口函数需要导入上面封装的ajax请求函数,调用,发送请求)
import ajax from './ajax'
const BASE_URL='/api'
//注册接口
//注册时即向后台传送username与password---->一个user对象---参数
//需要ajax为桥梁发送请求,ajax返回的结果就是现所需要的结果
//再ajax中需要指定参数
//url---只需要指定后面部分
//2.获取食品分类列表
export const reqFootCategory=()=>ajax(BASE_URL+'/index_category')
//3.商店数组对象
export const reqShops=(longitude,latitude)=>ajax(BASE_URL+'/shops',{
longitude,latitude})
//1.根据经纬度获取地址详情
export const reqAddress=(geohash)=>ajax(`${
BASE_URL}/position/${
geohash}`)
//4.根据经纬度和关键字搜索商铺列表
export const reqSearchShop = (geohash, keyword) => ajax(BASE_URL+'/search_shops', {
geohash, keyword})
//5.获取一次性验证码
export const reqGetcaptcha=()=>ajax(BASE_URL+'/captcha')
//6.用户名密码登录
export const reqPwdLogin=({
name,pwd,captcha})=>ajax(BASE_URL+'/login_pwd',{
name,pwd,captcha},'POST')
//7.发送短信验证码
export const reqSendCode=(phone)=>ajax(BASE_URL+'/sendcode',{
phone})
//8.手机号验证码登录
export const reqSmsLogin=(phone,code)=>ajax(BASE_URL+'/login_sms',{
phone,code},'POST')
//18813216310
//9.根据会话获取用户信息
export const reqUserInfo=()=>ajax(BASE_URL+'/userinfo')
//10.用户登出
export const reqLogout=()=>ajax(BASE_URL+'logout')
3)当其他模块需要调用发送请求时,直接使用import结构导入封装的接口函数即可
5.CommonJS、AMD、CMD、ES6 Module的区别
5.1 AMD与CMD区别
1)模块定义时对依赖的处理不同
AMD推崇迁至以来,在定义模块时就要声明其依赖的模块;而CMD推从就近依赖,只有在用到某个模块时再使用require导入;
AMD:
difine(['module1','module2'],function(m1,m2){
})
CMD:
define(function(require,exports,module){
const module1=require('./module1');
})
2)对依赖模块的处理机制不同
- 首先AMD和CMD对模块的加载方式都是异步的
- 不过区别在于AMD当加载了依赖模块之后立即执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致;
- 而CMD加载完依赖模块之后,并不会立即执行,等所有的依赖模块都加载好之后,进入回到函数逻辑,遇到require语句的时候,才执行对应的模块,这样模块的执行顺序就和我们书写的时候一致了
5.2 ES6模块与CommonJS模块加载的区别
- CommonJS时运行时加载,因为ComminJS加载是先加载整个模块,生成一个对象(这个对象包含了path这个模块的所有API),然后再从这个对象上面读取方法-----运行时加载
- ES6是编译时加载,ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态定义阶段就会生成-----编译时加载
//ES6模块
import {
basename, dirname, parse } from 'path';
//CommonJS模块
let {
basename, dirname, parse } = require('path');
以上这种写法与CommonJS的模块加载有什么不同?
- 当require path 时,CommonJS会将path模块运行一遍,并返回一个对象,这个对象包含path模块的所有API。
参考文章:
https://juejin.cn/post/6844903629447495687