小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
- CommonJS
- AMD
- ESM
上一次我们介绍了常用,也是未来 JavaScript 包管理的一个方向,
- 暴露方式有
export
和export default
,命名导出和默认导出 - 导入方式有
import from
方式
这里我们就看到了矛盾。ES module本质上只有命名导出,默认导出只是名为default的命名导出的语法糖。
在 Node 14 中,现在支持旧式的 CommonJS(CJS)和 新式的 ESM 脚本(又名MJS)包管理。CJS 中暴露和导入使用 require()
和 module.exports
;ESM 脚本使用 import
和 export
。
ESM 和 CJS 是完全不同的动物。从表面上看,ESM 与 CJS 非常相似,但他们的实现却不能再不同了。一个是蜜蜂,另一个是杀人的大黄蜂。
从 ESM 调用 CJS,反之亦然是可以的,但很麻烦。
这里有一些规则,我将在下面详细解释。
-
不能
require()
方式导入 ESM 格式的脚本;只能导入 ESM 格式脚本,像这样import {foo} from 'foo'
。 -
CJS 脚本不能使用像上面这样的静态
import
语句。 -
CJS 脚本可以使用异步的动态
import()
来加载 ESM 模块文件,但与同步的require
相比,这很麻烦。 -
ESM 模块的文件可以
import
CJS 的文件,但只能使用默认导入语法import _ from 'lodash'
,不支持命名导入语法import {shuffle} from 'lodash'
,如果 CJS 文件在使用命名导出,这就很麻烦了。 -
ESM 可以同
require()
导入 CJS 文件,即使有命名的导出,也尽量避免麻烦,因为需要更多的模板,而且最糟糕的是,像Webpack
和Rollup
这样的打包工具也不知道如何与使用require()
的 ESM文件一起工作。 -
CJS 是默认的,如果需要选择加入 ESM 模式。可以通过将你的 JS 文件名称从
.js
重命名为.mjs
来选择加入 ESM 模式。还需要可以在 package.json 中,设置 "type"。"module",然后就可以通过将 JS 文件从 .js 改成 .cjs 来选择退出 ESM。(甚至可以通过在package.json中加入一行{"type": "module"} package.json文件来进行调整)。)
These rules are painful. Worse, for many users, especially newbies to Node, these rules are incomprehensible. (Fear not, I’ll explain them all here in this article.)
对于许多用户,特别是对那些 Node 的新手,上面这些规则是无法理解的,不过随后给出更多介绍
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="container">
<ul id="tuts">
</ul>
<div>
<input type="text" id="input">
<button id="submit">添加</button>
</div>
</div>
<script src="tut.js"></script>
<script src="dom.js"></script>
</body>
</html>
复制代码
tut.js
let tuts = ["machine learning","deep learning","meta learning"];
function getTuts(){
return tuts;
}
复制代码
dom.js
function addTutsToDom(title){
const node = document.createElement('li');
const text = document.createTextNode(title);
node.appendChild(text);
document.getElementById("tuts").appendChild(node);
}
document.getElementById("submit")
.addEventListener("click",function(){
const input = document.getElementById("input")
addTutsToDom(input.value)
})
let list = window.getTuts();
console.log(list)
for(let i = 0; i < list.length; i++){
addTutsToDom(list[i]);
}
复制代码
这样做定义函数 getTuts
都是全局对象 window
下的方法,所以我们程序都在一个作用域下,程序结构没有层次感。现在主要问题是这些定义变量和方法都是在 window
下,稍不留神就会出现命名冲突的问题。大家可能想我们可以事先将变量和方法定义好,设计好,看是可行但是这样做的隐患可想而知,而且我们协同开发不仅是在团队内部,公司内部,之前提到了 leverage ,这个范围可能是跨区域或者世界范围内的合作,所以我们从本质上来解决而是是否补救,这样成本会很高。
那么我们先定义对象,然后将我们数据都挂载到这个对象下。
let App = {}
复制代码
function domWrapper(){
function addTutsToDom(title){
const node = document.createElement('li');
const text = document.createTextNode(title);
node.appendChild(text);
document.getElementById("tuts").appendChild(node);
}
document.getElementById("submit")
.addEventListener("click",function(){
const input = document.getElementById("input")
addTutsToDom(input.value)
})
// let list = window.getTuts();
let list = App.getTuts();
console.log(list)
for(let i = 0; i < list.length; i++){
addTutsToDom(list[i]);
}
}
domWrapper();
复制代码
function tutsWrapper(){
let tuts = ["machine learning","deep learning","meta learning"];
function getTuts(){
return tuts;
}
App.getTuts = getTuts
}
tutsWrapper();
复制代码
不过这样做,tutsWrapper
和 domWrapper
都还是挂载在 window
这个全局对象,接下来我们来继续解决这个问题,引入立即执行函数,立即执行函数第一次接触就感觉很神奇,回想起当初自己确确实实是一个小白,虽然现在还是一个门外汉,但是回想起一路走来的确不容易。
(function(){
})()
复制代码
好处是我们不需要定义一个 tutsWrapper
函数,用这个函数包裹逻辑,然后调用这个函数。
(function(){
let tuts = ["machine learning","deep learning","meta learning"];
function getTuts(){
return tuts;
}
App.getTuts = getTuts
})();
复制代码
CommonJS
我们都知道 Nodejs 就是基于 CommonJs 模块化管理,每一个文件都可以当作一个模块,
- 服务器端,模块的加载时运行时间同步加载的
- 浏览器端,浏览器引擎并不支持
require
语法,所以模块需要提前编译打包处理,Browsrify 工具支持
方法
- 暴露方式
module.exports = value
或者exports.something = value
- 引入模块
require(somethingModule)