“Yeah It’s on. ”
正文
AMD
Asynchromous Module Definition - 异步模块定义
AMD是RequireJS在推广过程中对模块定义的规范化产出,AMD是异步加载模块,推崇依赖前置。
define('module1', ['jquery'], ($) => {
//do something...
});
CMD
Common Module Definition - 公共模块定义
CMD是SeaJS在推广过程中对模块定义的规范化产出,对于模块的依赖,CMD是延迟执行,推崇依赖就近。
define((require, exports, module) => {
module.exports = {
fun1: () => {
var $ = require('jquery');
return $('#test');
}
};
});
UMD
https://webpack.toobug.net/zh-cn/chapter2/umd.html
UMD又是个什么玩意呢?UMD是AMD和CommonJS的一个糅合。AMD是浏览器优先,异步加载;CommonJS是服务器优先,同步加载。
UMD模块是指那些既可以作为模块使用(通过导入)又可以作为全局(在没有模块加载器的环境里)
按理说,分别讲完非模块化、AMD、CommonJS的打包后,并没有必要再专门引入一篇讲UMD打包的。但因为UMD的实现中依赖一些特殊的变量,因此还是提一下。
首先回顾一下UMD的模块定义:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['b'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require('b'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.b);
}
}(this, function (b) {
//use b in some fashion.
// Just return a value to define the module export.
// This example returns an object, but the module
// can return a function as the exported value.
return {};
}));
CommonJS
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
/**
Module {
id: '.',
exports: {},
parent: null,
filename: '/1.js',
loaded: false,
children: [],
paths:
[
'/node_modules' ]
}
*/
- 每个js文件一创建,都有一个var exports = module.exports = {};,使exports和module.exports都指向一个空对象。
- module是全局内置对象,exports是被var创建的局部对象。
- module.exports和exports所指向的内存地址相同 (exports 是指向的 module.exports 的引用)
我们来看一个例子:
// 2.js
exports.id = 'exports的id';
exports.id2 = 'exports的id2';
exports.func = function(){
console.log('exports的函数');
};
exports.func2 = function() {
console.log('exports的函数2');
};
module.exports = {
id: 'module.exports的id',
func:function(){
console.log('module.exports的函数');
}
};
// 3.js
var a = require('./2.js');
// 当属性和函数在module.exports都有定义时:
console.log(a.id); // module.exports的id
console.log(a.func()); // module.exports的函数
// 当属性在module.exports没有定义,函数在module.exports有定义
console.log(a.id2); // undefined
console.log(a.func()); // module.exports的函数
// 当函数在module.exports没有定义,属性在module.exports有定义
console.log(a.id); // module.exports的id
console.log(a.func2()); // 报错了 TypeError: a.func2 is not a function
- module.exports像是exports的大哥,当module.exports以{}整体导出时会覆盖exports的属性和方法,
- 注意,若只是将属性/方法挂载在module.exports./exports.上时,exports.id=1和module.exports.id=100,module.exports.id=function(){}和exports.id=function(){},最后id的值取决于exports.id和module.exports.id的顺序,谁在后,就是最后的值
- 若exports和module.exports同时赋值时,exports所使用的属性和方法必须出现在module.exports,若属性没有在module.exports中定义的话,出现undefined,若方法没有在module.exports中定义,会抛出TypeError错误。
require.context
require.context
https://juejin.im/post/6844903583113019405#heading-0
可以使用 require.context() 方法来创建自己的(模块)上下文,这个方法有 3 个参数:
- 要搜索的文件夹目录
- 是否还应该搜索它的子目录,
- 以及一个匹配文件的正则表达式。
require.context(directory, useSubdirectories = false, regExp = /^\.\//)
require.context("./test", false, /\.test\.js$/);
//(创建了)一个包含了 test 文件夹(不包含子目录)下面的、所有文件名以 `.test.js` 结尾的、能被 require 请求到的文件的上下文。
require.context("../", true, /\.stories\.js$/);
//(创建了)一个包含了父级文件夹(包含子目录)下面,所有文件名以 `.stories.js` 结尾的文件的上下文。
require.context模块导出(返回)一个(require)函数,这个函数可以接收一个参数:request 函数–这里的 request 应该是指在 require() 语句中的表达式
导出的方法有 3 个属性: resolve, keys, id。
- resolve 是一个函数,它返回请求被解析后得到的模块 id。
- keys 也是一个函数,它返回一个数组,由所有可能被上下文模块处理的请求组成。
- id 是上下文模块里面所包含的模块 id. 它可能在你使用 module.hot.accept 的时候被用到
require源码粗读
https://github.com/CommanderXL/biu-blog/issues/24
在我们的node.js程序当中,我们使用require这个看起来是全局(后面会解释为什么看起来是全局的)的方法去加载其他模块。
const util = require('./util')
首先我们来看下关于这个方法,node.js内部是如何定义的:
Module.prototype.require = function () {
assert(path, 'missing path');
assert(typeof path === 'string', 'path must be a string');
// 实际上是调用Module._load方法
return Module._load(path, this, /* isMain */ false);
}
Module._load = function (request, parent, isMain) {
.....
// 获取文件名
var filename = Module._resolveFilename(request, parent, isMain);
// _cache缓存的模块
var cachedModule = Module._cache[filename];
if (cachedModule) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
// 如果是nativeModule模块
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
// Don't call updateChildren(), Module constructor already does.
// 初始化一个新的module
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
// 加载模块前,就将这个模块缓存起来。注意node.js的模块加载系统是如何避免循环依赖的
Module._cache[filename] = module;
// 加载module
tryModuleLoad(module, filename);
// 将module.exports导出的内容返回
return module.exports;
}
Module._load方法是一个内部的方法,主要是:
- 根据你传入的代表模块路径的字符串来查找相应的模块路径;
- 根据找到的模块路径来做缓存;
- 进而去加载对应的模块。
#### 模块的加载机制
CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。 这点与ES6模块化有重大差异
例子:
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
上面代码输出内部变量counter和改写这个变量的内部方法incCounter。
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3
上面代码说明,counter输出以后,lib.js模块内部的变化就影响不到counter了。这是因为counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
commonjs vs commonjs2
https://www.css3.io/commonjs-vs-commonjs2.html
CommonJs spec defines only exports
. But module.exports
is used by node.js and many other CommonJs implementations.
commonjs
mean pure CommonJs
commonjs2
also includes the module.exports
stuff.
commonjs 规范只定义了exports,而 module.exports是nodejs对commonjs的实现,实现往往会在满足规范前提下作些扩展,我们这里把这种实现称为了commonjs2.
ESM
块(ES Modules)是 ECMAScript 2015(ES6)中引入的一种模块化系统,用于组织和管理 JavaScript 代码。
ES 模块采用了静态编译的方式,并且具有以下特点:
- 模块化:ES 模块将 JavaScript 代码分割成一系列独立的模块,每个模块可以导入(import)其他模块的功能,并且可以导出(export)自己的功能。这种模块化的方式使得代码更加模块化和易于维护。
- 文件级别导入和导出:ES 模块的导入和导出是基于文件级别的,每个模块都是一个独立的文件。通过导入和导出,可以明确地指定哪些功能对外部可见,从而实现模块的封装和隐藏内部实现。
- 静态编译:ES 模块在代码解析时就会静态地分析导入和导出语句,确定模块的依赖关系。这种方式使得编译器可以在运行前检查模块的有效性,并且可以进行优化,提高代码的执行效率。
- 默认导出和命名导出:ES 模块支持默认导出和命名导出。默认导出是每个模块只能有一个默认导出,而命名导出可以有多个。默认导出使用
export default
语法,命名导出使用export
语法。 - 动态导入:ES 模块支持动态导入,允许在代码执行时根据需要动态加载模块。动态导入使用
import()
函数,返回一个 Promise 对象。
Pure ESM package
https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
script 模块
https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script
此值导致代码被视为 JavaScript 模块。其中的代码内容会延后处理。charset
和 defer
属性不会生效。对于使用 module
的更多信息,请参见 JavaScript 模块指南。与传统代码不同的是,模块代码需要使用 CORS 协议来跨源获取。
举个例子:
<script type="module">
import { h, Component, render } from 'https://esm.sh/preact';
// Create your app
const app = h('h1', null, 'Hello World!');
render(app, document.body);
</script>
Dual CommonJS/ES module packages
https://nodejs.org/api/packages.html#dual-commonjses-module-packages
"exports": {
"require":"./lib/index.js",
"import":"./lib/esm/index.js
}
AIl Mode package
再进一步考虑前面提到的 ModernjavaScript,如果一个包同时要面向 Node.js 和浏览器,同时要兼顾现代和传统浏览器,其实可以把包目录结构和 package.json设计成这样:
{
"name": "my-pkg"
"type": "module",
"exports": {
"types":"legacy/lib/index.d.ts",
"require":"legacy/lib/index.js",
"import":"legacy/esm/index.js",
"modern":"modern/index.js",
}
"main": "legacy/lib/index.js"
"types":"legacy/lib/index.d.ts"
"module": "legacy/esm/index.js"
"browser":"./dist/index.umd.js"
}
总结
CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
esm与cjs差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
下面重点解释第一个差异,我们还是举上面那个CommonJS模块的加载机制例子:
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
esm和cjs互转
https://juejin.cn/post/7205897684624474168
https://www.zhihu.com/question/288322186
道理上,ES module转成CommonJS,其default导出只能映射到module.exports.default上不能映射到CommonJS传统的module.exports上,因为ES module可以同时有命名导出,你让module.exports既是一个导出值,又是一个namespace object。
esm 转 cjs
module.exports.default
为默认导出module.exports.xxx
其他为命名导出
还要新增一个标记__esModule
例子:
export default 666
export const a = 123
export const b = 234
转为:
Object.defineProperty(exports, '__esModule', { value: true })
module.exports.default = 666
module.exports.a = 123
module.exports.b = 234
在这种情况下,必须要用 .default
访问默认导出
我们一般无感知访问默认导出
_interopDefault
函数会自动根据 __esModule
,将导出对象标准化,使 .default
一定为默认导出
// foo.js
import lib from 'lib'
import {a, b} from 'lib'
console.log(lib, a, b)
'use strict';
var lib = require('lib');
function _interopDefault (e) {
return e && e.__esModule ? e : { default: e };
}
var lib__default = /*#__PURE__*/_interopDefault(lib);
console.log(lib__default.default, lib.a, lib.b);
- 如果有
__esModule
,那就不用处理 - 没有
__esModule
,就将其放到default
属性中,作为默认导出
cjs 转 esm
一般不会用 CJS 写 npm 库然后输出 ESM;用 CJS 写的库,当时不会输出 ESM。新写的 npm 库,一般来说也是用 ESM 写。
CJS 转 ESM,没有一种统一的转换标准(相对来说,ESM 转 CJS 有 __esModule
约定),不同的工具和库,可能转换出来的结果是不一样的,可能会导致代码不兼容。
export-default-thing vs thing-as-default
https://jakearchibald.com/2021/export-default-thing-vs-thing-as-default/
// These give you a live reference to the exported thing(s):
import { thing } from './module.js';
import { thing as otherName } from './module.js';
import * as module from './module.js';
const module = await import('./module.js');
// This assigns the current value of the export to a new identifier:
let { thing } = await import('./module.js');
// These export a live reference: (是链接 thing 改变 import thing 的值也会变)
export { thing };
export { thing as otherName };
export { thing as default };
export default function thing() {}
// These export the current value: (是当前值 thing 改变 import thing 的值不会变)
export default thing;
export default 'hello!';
To sum up:
规则仅仅适用于 export string (function 等不适用)
script type importmap
What’s script type importmap used for?
import-maps
使用 Json 的形式来定义浏览器中的全局模块:
<script type="importmap">
{
"imports": {
"moment": "/node_modules/moment/src/moment.js",
"lodash": "/node_modules/lodash-es/lodash.js"
}
}
</script>
有了上面的 importmap
定义, 可以在浏览器环境中这样使用全局模块:
import * as _loadash from 'loadash'; // 自动加载 /libs/loadash/index.js
banned default exports
https://blog.neufund.org/why-we-have-banned-default-exports-and-you-should-do-the-same-d51fdc2cf2ad
尽量不要用 export default
// do not try this at home
export default {
propertyA: "A",
propertyB: "B",
}
// do this instead
export const propertyA = "A";
export const propertyB = "B";
Using named exports can reduce your bundle size when you don’t use all exported values (especially useful while building libs).
export { default as }
假设我们在 src/index.ts 中需要导出 src/xxx.ts 里面的东西,而 src/xxx.ts是以 export default 导出的
这个时候我们在src/index.ts
不能直接
export * from "./xxx.ts"
正确写法
export { default as xxx } from './xxx.ts';
__esModule 的作用
https://toyobayashi.github.io/2020/06/29/ESModule/
简要说明 Webpack 和 TypeScript 编译器对__esModule的处理方式
__esModule
是用来兼容 ES 模块导入 CommonJS 模块默认导出方案
CommonJS 的 module.exports
没法对应 ES 模块
然后为了解决这个问题,不知道是 JS 圈子里的谁最先提出了 __esModule
这个解决方案,现在市面上的打包器都非常默契地遵守了这个约定。
表面上看就是把一个导出对象标识为一个 ES 模块:
exports.__esModule = true
或
Object.defineProperty(exports, '__esModule', { value: true })
Webpack 会根据 __esModule 标识来自动处理 CommonJS 的模块导出对象,兼容 ES 模块中的导入。
import相同文件多次
假设:
// index.js
console.log(111)
我们在 a.js/b.js/c.js 中 import 上面文件 index.js
一个页面中同时 import a.js/b.js/c.js
console.log 只会打印一次
因为: 第一次加载 index.js 后,之后在import index.js 就会从cache缓存中获取,不会走再次加载index.js的逻辑了
ERR_MODULE_NOT_FOUND
在 node 中
常见的报错 ErrOr: ERR_MODULE_NOT_FOUNDCannot find module “../src/foo” 改 import { bar } from ‘./src/foo 成 import { bar } from./src/foo.js’即可。
当然在 Node.js 中.cjs 会被识别为Commonjs
.mjs 会被识别为 ESM,所以也可以通过后缀.cts(以 .cjs引入)、.mts(以 .mjs 引入)
但感觉看起来更乱,还是统一保持 .js 用文件夹区分比较好。