前言
最开始的时候,js只是浏览器脚本语言;而前端,就是小小的切图仔.随着时代的进步,技术的发展,前端工程日益复杂化,于是就需要分工合作,需要将前端项目工程化.
其中一项就是模块化,将代码进行模块化拆分,这就需要js能够支持模块化.说到前端的模块化,我们肯定马上想到Commonjs、AMD、CMD、UMD、ESM,但是很多人还不清楚它们分别代表何种模块化方案.
本文将围绕模块化讲讲它们的演变过程,并简要模拟下它们的实现方法.
一,最初没有模块化时的处境
1.1,全局上堆叠变量
最开始的时候,网页的页面和样式都非常简单,极少有交互,也就是js也很少,于是人们写代码就轻松随意得很:
function foo(){
console.log("foo")
}
function bar(){
console.log("bar")
}
这样带来的问题,就是所有的变量都堆积在全局作用域上,代码混乱且容易命名冲突.作为一个程序员,起变量名困难已经是不争的事实了.
1.2,使用对象包裹变量
为了减少全局作用域上的变量数,人们想到使用一个对象进行包裹.于是人们写代码的方式变成:
var obj={
a:1,
foo:function(){
console.log(a)
},
bar:function(){
console.log('bar')
}
}
obj.foo()
但是这种方式还存在一个弊端,obj.a应该是它的私有变量,如果我们将其改成obj.a=2,那么obj.foo()打印出来的就会变成2.
1.3,采用匿名函数IIFE的形式
为了让上文的obj.a能够更安全,人们想到了利用函数的块级作用域,于是开始改写成匿名函数自调用的形式.
var myModule = (function() {
var name = '孙悟空'
function getName() {
console.log(name)
}
return { getName }
})()
myModule.getName()
console.log(myModule.name)//undefined
这样我们就可以通过 myModule.getName()
来获取 name
,并且实现 name
属性的私有化,即外部无法访问其内部的私有变量.
但是这样子做还存在一个问题:多模块js引入后,如何保证多模块之间依赖关系的维护?于是就有了更进一步的模块化.
二,早期模块化的演变
2,1,common.js
Commonjs
的提出,弥补 Javascript 对于模块化,没有统一标准的缺陷。
Commonjs有如下特点:
在 commonjs 中每一个 js 文件都是一个单独的模块,我们可以称之为 module;
该模块中,包含 CommonJS 规范的核心变量: exports、module.exports、require;
exports 和 module.exports 可以负责对模块中的内容进行导出;
require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
而浏览器原生是不支持 CommonJS 的,因为 CommonJS 适用于加载本地模块,是一个同步加载的过程,比如 Node.js 中加载模块其实是一个读取本地文件并执行的同步过程,而在浏览器中要获取资源通常是需要异步请求获取的;
2.1.1,commonjs的使用案例
导出案例一(使用exports):
//index.js
exports.test=()=>{
console.log("1")
}
exports.a="这是一个变量"
导入案例一(使用exports):
const lala=require('./index')
console.log("---",lala)//--- { test: f()], a: '这是一个变量' }
导出案例一(使用module.exports)
module.exports={
a:'这是一个变量',
test:()=>{
copnsole.log(1)
}
}
引入使用的效果和exports是一样的.
2.1.2,module是啥
我们先把module打印出来:
//./index.js
module.exports={
a:'这是一个变量',
test:()=>{
console.log(module)
}
}
//test.js
const lala=require('./index')
lala.test()
运行之后得到的结果:
可以看到,每一个文件,都是一个module实例,里面有以下几个属性:
module.id 模块的识别符,通常是带有绝对路径的模块文件名
module.path 模块的文件路径
module.exports 表示模块对外输出的值
module.filename 模块的文件名,带有绝对路径
module.loaded 返回一个布尔值,表示模块是否已经完成加载
module.parent 返回一个对象,表示调用该模块的模块
module.children 返回一个数组,表示该模块要用到的其他模块
module.paths 返回一个数组,查找node_modules的路径
2.1.3,commonjs为何能实现模块化
按照上文的说法,commonjs最重要的两个点是:能进行模块变量的隔离和引入导出的模块变量.
那么一定还是基于函数自调用的形式.于是可以写出最简单的commonjs方法:
//./index.js
module.exports={
a:'这是一个变量',
test:()=>{
console.log(module)
}
}
//common.js
let path = require('path');
let fs = require('fs');
let vm = require('vm');
// 构造函数Module
function Module(filename){
this.filename = filename; // 文件的绝对路径
this.exports = {}; // 模块对应的导出结果
}
// 拼凑成闭包的数组
Module.wrapper = ['(function(module){','\r\n})'];
// 加载模块本身
Module.prototype.load = function (module) {
// 读文件
let content = fs.readFileSync(module.filename, 'utf8');
// 形成闭包函数字符串
let script = Module.wrapper[0] + content + Module.wrapper[1];
// 创建沙箱环境,运行并返回结果(把模块代码处理成字符串-然后利用vm.runInThisContext处理成可执行代码,这里是个函数包裹着module)
let fn = vm.runInThisContext(script);
// 执行闭包函数,将被闭包函数包裹的加载内容
fn(module)
};
// 仿require方法, 实现加载模块
function req(p) {
// 根据输入的路径 转换绝对路径
let filename= path.join(__dirname, p);
// 通过文件名创建一个Module实例
let module = new Module(filename);
// 加载文件,执行对应加载方法
module.load(module);
return module.exports
}
let str = req('./index.js');
console.log(str);//{ a: '这是一个变量', test: [Function: test] }
实际上原理非常简单:
1,引入每个文件的时候,执行require,会创建一个module实例,其内有个exports属性(用来存储后续要导出的对象)
2,require文件的时候,会根据绝对路径读取这个文件内容,并且用一个函数包裹成(function(module){module.exports={a:'这是一个变量',test:()=>{console.log(module)}}})的形式,然后使用vm.runInThisContext(script)转化为可执行代码后执行.
3,在执行该闭包函数的时候,因为引入的文件内容有module.exports=...的形式,即给module实例的exports属性赋值存储了要导出的对象.
4,requie方法最后导出module.exports这个存储的对象即可.
最核心的一点就是require方法会把文件用函数包裹起来,并且传入实参mudule,这才能在模块文件中修改exports的值,并且require方法最后将这个修改后的exports值返回.
2.1.4,精简版的commonjs
上文的仅仅是最简陋的commonjs,如下代码,这才是个精简的,基本的功能实现如下:
let path = require('path');
let fs = require('fs');
let vm = require('vm');
let n = 0
// 构造函数Module
function Module(filename){
this.id = n++; // 唯一ID
this.filename = filename; // 文件的绝对路径
this.exports = {}; // 模块对应的导出结果
}
// 存放可解析的文件模块扩展名
Module._extensions = ['.js'];
// 缓存
Module._cache = {};
// 拼凑成闭包的数组
Module.wrapper = ['(function(exports,require,module){','\r\n})'];
// 没写扩展名,默认添加扩展名
Module._resolveFilename = function (p) {
p = path.join(__dirname, p);
if(!/.\w+$/.test(p)){
//如果没写扩展名,尝试添加扩展名
for(let i = 0; i < Module._extensions.length; i++){
//拼接出一个路径
let filePath = p + Module._extensions[i];
// 判断文件是否存在
try{
fs.accessSync(filePath);
return filePath;
}catch (e) {
throw new Error('module not found')
}
}
}else {
return p
}
}
// 加载模块本身
Module.prototype.load = function () {
// 解析文件后缀名 isboyjc.js -> .js
let extname = path.extname(this.filename);
// 调用对应后缀文件加载方法
Module._extensions[extname](this);
};
// 后缀名为js的加载方法
Module._extensions['.js'] = function (module) {
// 读文件
let content = fs.readFileSync(module.filename, 'utf8');
// 形成闭包函数字符串
let script = Module.wrapper[0] + content + Module.wrapper[1];
// 创建沙箱环境,运行并返回结果
let fn = vm.runInThisContext(script);
// 执行闭包函数,将被闭包函数包裹的加载内容
fn.call(module, module.exports, req, module)
};
// 仿require方法, 实现加载模块
function req(path) {
// 根据输入的路径 转换绝对路径
let filename = Module._resolveFilename(path);
// 查看缓存是否存在,存在直接返回缓存
if(Module._cache[filename]){
return Module._cache[filename].exports;
}
// 通过文件名创建一个Module实例
let module = new Module(filename);
// 加载文件,执行对应加载方法
module.load();
// 入缓存
Module._cache[filename] = module;
return module.exports
}
和之前最简陋版大差不差,只是增加了一些细节的处理.
值得注意的是,传入的参数变成了这三个:
fn.call(module, module.exports, req, module)
这里module.exports可以看到它对应的实参是exports,这就是模块中为啥既可以使用module.exports也可以使用exports的原因.因为他两实际上是都指向module实例的exports对象.
总结,简单点说,CommonJs 就是模块化的社区标准,而 Nodejs 就是 CommonJs 模块化规范的实现,它对模块的加载是同步的,也就是说,只有引入的模块加载完成,才会执行后面的操作,在 Node
服务端应用当中,模块一般存在本地,加载较快,同步问题不大,在浏览器中就不太合适了,太慢了,用户体验极,所以还需要异步模块化方案,所以 AMD规范
就此诞生。
这里我原本有个疑惑,为啥它实现的模块加载是同步的呢?直到我看到这个读取文件的代码:
// 读文件
let content = fs.readFileSync(module.filename, 'utf8');
它是用的fs.readFileSync而不是fs.readFile!
2.1.5,commonjs总结
commonjs模块化的特点如下:
1,使用require、exports和module作为模块化组织的关键字;
2,每个模块只加载一次,作为单例存在于内存中,每次require时使用的是它的接口;
3,require是同步的,通俗地讲,就是node运行A模块,发现需要B模块,会停止运行A模块,把B模块加载好,获取的B的接口,才继续运行A模块。如果B模块已经加载到内存中了,当然require B可以直接使用B的接口,否则会通过fs模块化同步地将B文件内存,开启新的上下文解析B模块,获取B的API。
2.2,AMD规范
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
它的重点是:等所有相关的依赖都加载并执行取得结果后,才执行这个回调函数.
使用案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./requireJS.js"></script>
<script>
require(['a', 'b'], function (a, b) {
console.log(b + a)
});
</script>
</body>
</html>
相关模块:
//a.js
define([], function () {
return 1
})
//b.js
define(['c'], function (c) {
console.log('3333')
return 2 + c
})
//c.js
define([], function () {
return 2
})
那么我们需要三个属性
moudle://存储一个模块的信息:依赖、回调函数、返回值
cache:该对象用来存储每个模块的返回值,如a被bcd引用的话,只要执行一次,得到结果暂存结果,后续cd模块就能直接使用结果了.
tasks:该数组用来存储每个模块的具体信息:名字、依赖项目、回调函数.之所以存储这个,是为了在依赖项都加载完毕后执行对应的回调函数.
(function () {
// 缓存
let moudle = null//存储一个模块的信息:依赖、回调函数、返回值
const cache = {}//存储每个模块的返回值
const tasks = []//收集所有具备依赖项的模块的数据
// 创建script标签,用来加载文件模块,浏览器中动态引入文件需要创建script标签来引入
const createNode = function (depend) {
let script = document.createElement("script");
script.src = `./${depend}.js`;
// 嵌入自定义 data-moduleName 属性,后可由dataset获取
script.setAttribute("data-moduleName", depend);
let fs = document.getElementsByTagName('script')[0];
fs.parentNode.insertBefore(script, fs);
return script;
}
// 校验所有依赖是否都已经解析完成-每个文件如果解析完成得到结果了,会存储在cache中,所以cache中有该模块的结果,则说明解析完成过
const hasAlldependencies = function (dependencies) {
let hasValue =dependencies.every(depd => {
return cache.hasOwnProperty(depd)
})
return hasValue
}
// 递归执行callback
const implementCallback = function (tasks) {
if (tasks.length) {
tasks.forEach((task, index) => {
// 某个模块所有依赖解析都已完成,就执行它的回调函数,取得回调函数的结果存储在cache中
if (hasAlldependencies(task.dependencies)) {
//从cache中取出该模块需要的依赖的返回结果,传入该模块的回调函数执行
const returnValue = task.callback(...task.dependencies.map(it => cache[it]))
if (task.name) {
cache[task.name] = returnValue
}
tasks.splice(index, 1)
implementCallback(tasks)
}
})
}
}
// 根据依赖项加载js文件
const require = function (dependencies, callback) {
if (!dependencies.length) { // 此require没有依赖项
moudle = {
value: callback() //直接执行回调函数,得到该模块的结果
}
} else { //此require有依赖项
moudle = {
dependencies,
callback
}
tasks.push(moudle)//对于存在依赖项的模块,需要收集到tasks中(因为后续需要每个依赖项加载完后,都判断下它的依赖项都加载完毕没)
dependencies.forEach(function (item) {
if (!cache[item]) {//还没有取得该模块的结果,则需要注册加载完毕后的回调.对于已取得结果的模块则无需处理
createNode(item).onload = function () {// script加载文件结束(该模块的js执行完毕后会执行)
// 获取嵌入属性值,即module名
let modulename = this.dataset.modulename
// 校验module中是否存在value属性,一个模块加载完成,如果执行完毕,则会有该模块的返回,存储在module.value中
if (moudle.hasOwnProperty('value')) {
// 例如a模块,它没有依赖项,a中代码require后,当前的module.value就存储着该模块的结果,因为该模块执行完毕,立马就会执行它的onload方法
cache[modulename] = moudle.value//这时就直接拿到了a模块的结果,存储在缓存中,下次遇到了,会覆盖
}else {
// 该模块还没有取得结果,
moudle.name = modulename
if (hasAlldependencies(moudle.dependencies)) {
// 所有依赖解析都已完成,执行回调,抛出依赖返回(导出)值(这里是用于依赖项在其他地方已经得到结果了,这里就可以直接执行回调,不需要再引入并执行依赖了),如b模块,因为依赖了c,执行完毕后,就因为c模块未加载完毕,而无法走到这里
cache[modulename] = callback(...moudle.dependencies.map(v => cache[v]))
}
}
implementCallback(tasks)//[i]
}
}
})
}
}
window.require = require
window.define = require
})(window)
对于浏览器中加载模块需要注意的是:
1,每个script是同步的代码先执行,也就是最外层的所有同步代码都执行完毕.
2,然后才执行到第一个引入的js内部的代码.
3,每个引入的js代码执行完毕后,才会执行该script的onload方法.
但又因为创建script引入代码是异步的,所以代码执行的顺序是:
1,index.html中的require,foreach两次createNode(引入a和b代码)和绑定对应onload
2,执行a中的代码,define方法直接取得a模块结果存储在module.value
3,a模块执行完毕,执行a模块的onload方法,将a模块的结果存储在cache.a中.[1]
4,开始执行b模块内的代码,define方法,foreach一次createNode(引入c代码)和绑定onload
5,b模块执行完毕,执行b模块的onload方法,该模块还没有取得结果(module.value无值)
6,b模块有依赖没有获得结果,执行implementCallback(tasks)//[2],但是因为c模块没取得结果,所以b取不到结果,导致最外层也取不到结果.
7,开始执行c模块的代码,define方法直接取得c模块结果存储在module.value
8,c模块执行完毕,执行c模块的onload方法,将c模块的结果存储在cache.c中.//[3]
9,这时候执行implementCallback(tasks),因为b的依赖项都取得结果了(c),于是就可以执行b模块的回调,得到b的结果.然后就能执行最外层的回调了.
如上文所说,AMD规范的重点是:每个模块执行完毕后,绑定的onload事件最后都要递归遍历保存起来的tasks,让准备好的模块(相关依赖文件已取到结果)执行回调函数并缓存该模块结果.
而判断该模块是否准备好的依据就是:它的依赖项是否都取到结果了(cache数组中有它的结果值)
2.3,CMD规范
2.3.1,sea.js的使用
说到CMD,就不得不说玉伯大佬的Sea.js 。它的诞生在 RequireJS 之后,因为AMD 规范是异步的,模块的组织形式不够自然和直观。
例如说,a模块需要依赖bc两个模块的结果,那么使用AMD规范写出来的代码会是如下:
//a.js
define(['b','c'], function (b,c) {
return b + c
})
而如果使用CMD规范,则会是:
define(function(require, exports,module) {
var b = require('./b');
var c = require('./c');
exports.result=b+c
});
就变成了同步的方式.看起来倒是和commonjs很像.
2.3.2,CMD在浏览器实现同步写法的原理
由于在浏览器端,采用与node同样的依赖加载方式是不可行的(commonjs是同步加载同步执行),因为依赖只有在执行期才能知道,但是此时在浏览器端,我们无法像node一样直接同步地读取一个依赖文件并执行!我们只能采用异步的方式。于是Sea.js的做法是,分成两个时期——加载期和执行期;
加载期:即在执行一个模块之前,将其直接或间接依赖的模块从服务器端同步到浏览器端;
执行期:在确认该模块直接或间接依赖的模块都加载完毕之后,执行该模块。
CMD全称Common Module Definition,和AMD最明显的差别时,CMD模块规范遵从就近原则,认为只有在需要的时候才进行模块加载。
接下来,我简单实现下一个CMD的代码(只考虑模块的引入,不考虑缓存模块之类的):
const modules = {};//缓存模块信息
sj = {};
//使用promise控制,resolve绑定在onload上,也就是引入的js执行完毕后执行后续的then回调
const __load = (mod) => {
return new Promise((resolve, reject) => {
const head = document.getElementsByTagName('head')[0];
const node = document.createElement('script');
node.type = 'text/javascript';
node.src = `./${mod}.js`;;
node.async = true;
node.onload = resolve;
node.onerror = reject;
head.appendChild(node)
})
}
//['b','c']所有加载的文件名加路径
const getDepsFromFn = (fn) => {
let matches = [];
let reg = /(?:require()(?:['"])([^'"]+)/g;
let r = null;
while((r = reg.exec(fn.toString())) !== null) {
matches.push(r[1])
}
return matches
}
//定义模块
define = (factory) => {
const deps = getDepsFromFn(factory);//获取到该模块的依赖列表
modules[modules.name] = { factory, deps ,exports:{}}//存储对应模块信息
}
// 这里面才是加载模块的地方
const __require = (id) => {
modules.name=id//暂存当前加载模块名,在define中使用
return new Promise((resolve)=>{
__load(id).then(async() => {
const { factory, deps } = modules[id];
await factory(__require, modules[id]['exports'], modules[id])
modules[id]['result']=modules[id]['exports']
resolve(modules[id]['result'])
})
})
}
//加载所有依赖项取得结果,存储在缓存中,并返回,执行回调函数
sj.use = (mods, callback) => {
mods = Array.isArray(mods) ? mods : [mods];
return new Promise((resolve, reject) => {
Promise.all(mods.map(mod => {
modules.name=mod//暂存当前加载模块名,在define中使用
return __load(mod).then(async () => {
const { factory } = modules[mod];//这时候,该模块的代码已经执行了define,modules中有东西了
await factory(__require, modules[mod]['exports'], modules[mod])
return modules[mod]['exports']
})
})).then(resolve, reject)//所有依赖加载完毕
}).then(instances => callback && callback(...instances))//结果透传给下一个promsie
}
使用的案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./requireJS.js"></script>
<script>
// 加载一个模块,在加载完成时,执行回调
sj.use('a', function(a) {
console.log("---+++---",a.foo)
});
</script>
</body>
</html>
//a.js
define(async function(require, exports,module) {
// 获取模块b的接口
var b = await require('b');
b.doSomething();
var c =await require('c');
c.doSomething();
exports.foo = c.name;
// 对外提供 doSomething 方法
exports.doSomething = function() {
console.log("do something aaaa")
};
});
//b.js
define(async function(require, exports, module) {
var d = await require('d');
console.log("大大啊大大啊啊",d)
// 对外提供接口
module.exports = {
name: d.foo,
doSomething: function() {
console.log("bbbbb",d)
}
};
});
//d.js
define(async function(require, exports, module) {
// 对外提供接口
module.exports = {
foo: "d模块啦",
doSomething: function() {
console.log("dddd")
}
};
});
//c.js
define(async function(require, exports, module) {
// 对外提供接口
module.exports = {
name: 'c',
doSomething: function() {
console.log("ccccc")
}
};
});
实际上,CMD模块,就是commonjs和AMD模块的结合.
如在上文a模块中:
var b = await require('b');
b.doSomething();
var c =await require('c');
c.doSomething();
就是同步执行.
对于sj.use方法:
因为可能存在多个依赖,所以需要单独的方法,使用promise.all来保证所有依赖已经取得结果后,才执行回调函数.
对于require方法:
当a模块运行到var b = await require('b')的时候,如果b模块还有依赖,依赖内部还有更深层的依赖,则会逐层打开,逐层进入,就像洋葱一样,直到获得结果,再一层层返回,继续执行代码.
其实也没啥东西,主要就是利用两次闭包:
第一次:将define传入的回调函数存储在全局的module[id].factory中,等load完对应js后调用执行.
第二次:将暂存起来的模块的api传入factory中使用:factory(__require, modules[id]['exports'], modules[id])
2.3.3,CMD规范和commonJS、AMD规范的差别
三者的差异:
1,commonJS是给node环境使用的,AMD和CMD是给浏览器使用的.
2,CommonJS 模块是同步加载和同步执行.它读取文件采用的是fs.readFileSync同步读取.
3,AMD 模块是异步加载和异步执行.它是依赖前置,先提前声明会用到的所有依赖,然后每当一个依赖执行完毕:script加载文件结束(该模块的js执行完毕后会执行),onload方法都会递归执行所有存在依赖的模块,直到该模块依赖结果收集完毕,才执行对应回调函数.也就是异步执行(代码不会卡在某个模块等它结束,而是会执行后续代码)
4,CMD(Sea.js)模块是异步加载和同步执行.它是在define中字符串化后再收集相关依赖,然后像洋葱一样一层层返回结果后继续执行代码,也就是同步执行.
三者的原理差别
Commons:
commonjs最简单,他就是同步读取文件,然后用个函数把该文件包裹起来执行一遍,又因为传入这几个参数fn.call(module, module.exports, req, module),所以内部将导出的结果赋值给了module.exports对象,然后在require方法最后导出这个对象即可.
AMD:
因为是用在浏览器的,所以它需要创建script标签,不同的是,它采用const tasks = []来收集所有具备依赖项的模块信息.先是依赖前置,然后每当一个依赖执行完毕:script加载文件结束(该模块的js执行完毕后会执行),onload方法都会递归执行所有存在依赖的模块,直到该模块依赖结果收集完毕,才执行对应回调函数.
CMD:
它则是两者的结合,让使用者能采用同步的方式来书写模块代码.
use方法是依赖前置,和AMD一样,等所有依赖都获得结果后执行回调函数.
define方法则是在运行过程中收集依赖,然后采用洋葱方法一层层取得依赖结果,再返回上一层继续执行代码,它和commonjs一样,是将module.exports传入回调函数中进行赋值,然后在require方法的最后进行返回.
其实都是闭包的使用啦.
2.4,UMD规范
严格意义上来说,UMD并不是一个规范.它的出现是因为人们觉得node端需要写commonjs,浏览器端又要写AMD、CMD.
历史的发展从来都是分久必合,所有就有人提出使用一种规范,作为大一统的写法,能够将三者统一成一个写法.
如下代码:
((root, factory) => {
if (typeof define === 'function' && define.amd) {
// AMD
define(factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory();
} else if (typeof define === 'function' && define.cmd){
// CMD
define(function(require, exports, module) {
module.exports = factory()
})
} else {
// 都不是
root.umdModule = factory();
}
})(this, () => {
console.log('UMD')
// todo...
});
如上代码,他就是做大一统的,其实也没啥东西.内核还是commonjs、AMD、AMD三个.
三,成熟的模块化方案ES modu le
如上文所说,到目前为止我们说的 CommonJS
、AMD
、 CMD
等都只是社区比较认可的统一模块化规范,并不是官方提供的,那接下来要说的就是 JS 的官方模块化规范了。
3.1,使用案例一:export的写法
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
只有这三种形式的写法,才能正确地在文件对象上增加对应的属性。
export的作用,就是给文件对象增加一个属性.
3.2,使用案例二: export default
既然有了 export ,为什么还要有个 export default 呢?我们用它作为文件的默认导出接口(所以一个文件只能有一个)。
//./a.js
export const a = 1;
export const b = 2;
export const c = () => console.log("第一个");
const d = 3;
export { d };
export default {
test: "aaa",
};
//使用
import * as all from "./a.js";//全量引入
console.log("----", all);
得到的结果:
---- [Module: null prototype] {
a: 1,
b: 2,
c: [Function: c],
d: 3,
default: { test: 'aaa' }
}
可以看到,打印出来的文件对象中多了个default属性。
所以说,export default 的作用,是给文件对象,添加一个 default属性,default属性的值也是一个对象,且和export default导出的内容完全一致。
export default的使用方法:
//写法一
export default {
test: "aaa",
};
//写法二
const test='aaaa'
export default {
test,
};
//写法三
export default function () {
console.log("aaaaa");
}
也就是export default仅支持对象和函数的导出。
3.3,使用案例三:import引入的使用
文件对象全量引入
这个引入的是整个文件对象,不仅包含export导出的内容(文件对象.属性名),还包含export default导出的内容(文件对象.defalut)
//./a.js
export const a = 1;
export const b = 2;
export const c = () => console.log("第一个");
const d = 3;
export { d };
export default {
test: "aaa",
};
//导入
import * as all from "./a.js";
console.log("----", all);
//结果
---- [Module: null prototype] {
a: 1,
b: 2,
c: [Function: c],
d: 3,
default: { test: 'aaa' }
}//可以看到文件对象包含export的属性,以及defaults属性存储着export default导出的对象
文件对象引入(仅包含default)
//./a.js
export const a = 1;
export const b = 2;
export const c = () => console.log("第一个");
const d = 3;
export { d };
export default {
test: "aaa",
};
//导入
import test from "./a.js";
console.log("----", test);//---- { test: 'aaa' }
//结果
---- { test: 'aaa' }
可以看到,直接引入的话,是仅包含default的,而不会引入export导出的内容.
部分引入(无法引入default)
//./a.js
export const a = 1;
export const b = 2;
export const c = () => console.log("第一个");
const d = 3;
export { d };
export default {
test: "aaa",
};
//导入
import {a,b,d} from "./a.js";
console.log("----", a,b,d);//---- 1 2 3
可以看到这种方式,仅限于获取文件对象的正常属性,default属性是获取不到的,原因有两个:
- 未解构的对象全部进行了丢弃
- default是关键字,不能再解构中当做变量进行使用
混合引入(引入default和export的内容)
结合上文几点,很明显,可以用如下方式导出想要的部分内容:
//./a.js
export const a = 1;
export const b = 2;
export const c = () => console.log("第一个");
const d = 3;
export { d };
export default {
test: "aaa",
};
//导入
import defaultObj,{a,b,d} from "./a.js";
console.log("----",defaultObj,a,b,d);//---- { test: 'aaa' } 1 2 3
四,总结
实际上,JS 模块化,上述这些方案都在解决这两个问题:
1,全局变量污染,命名冲突.
2,文件依赖问题
因为node环境衍生了commonJs方案,采用函数包裹模块的方式,同步引入同步执行.
因为浏览器环境衍生了AMD规范,采用动态创建script的方式引入模块,异步引入,异步执行.
而又嫌弃AMD写起来麻烦,看起来难看,于是就有了CMD规范,结合commonjs和AMD规范的优点,实现了异步加载同步执行的效果.
紧接着,因为各种模块方案的存在,你写你的,我写我的,太乱了.于是就有人想要把这些模块统一成一种写法,可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD
甚至是 AMD
的项目中运行,也就是说同一个 JavaScript 包运行在浏览器端、服务区端甚至是 APP 端都只需要遵守同一个写法就行了,这就是UMD的诞生背景.
而后,随着社区的发展,官方终于提供了模块化的解决方案,这就是ES module.
至此,js才真正有了模块化.
五,参考文章
前端科普系列-CommonJS:不是前端却革命了前端 - 知乎 (zhihu.com)
「前端工程四部曲」模块化的前世今生(上) - 掘金 (juejin.cn)
今天要讲的几个模块化规范CMJ、AMD、CMD、ESM,以及简单实现AMD、CMD - 掘金 (juejin.cn)
Sea.js是如何工作的? | Hello Sea.js (island205.github.io)
JS模块加载器加载原理是怎么样的? - 知乎 (zhihu.com)
CMD 模块定义规范 · Issue #242 · seajs/seajs · GitHub
「万字进阶」深入浅出 Commonjs 和 Es Module - 掘金 (juejin.cn)
六,系列文章
本文是我整理的js基础文章中的一篇,下面是已经完成的文章:
从promise到await - 掘金 (juejin.cn)