js模块化

Posted by Qz on January 3, 2019

“Yeah It’s on. ”

正文

网页链接

JavaScript 模块

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' ] 
 }
 */
  1. 每个js文件一创建,都有一个var exports = module.exports = {};,使exports和module.exports都指向一个空对象。
  2. module是全局内置对象,exports是被var创建的局部对象。
  3. 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  
  1. module.exports像是exports的大哥,当module.exports以{}整体导出时会覆盖exports的属性和方法,
  2. 注意,若只是将属性/方法挂载在module.exports./exports.上时,exports.id=1和module.exports.id=100,module.exports.id=function(){}和exports.id=function(){},最后id的值取决于exports.id和module.exports.id的顺序,谁在后,就是最后的值
  3. 若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

What is commonjs2

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 模块采用了静态编译的方式,并且具有以下特点:

  1. 模块化:ES 模块将 JavaScript 代码分割成一系列独立的模块,每个模块可以导入(import)其他模块的功能,并且可以导出(export)自己的功能。这种模块化的方式使得代码更加模块化和易于维护。
  2. 文件级别导入和导出:ES 模块的导入和导出是基于文件级别的,每个模块都是一个独立的文件。通过导入和导出,可以明确地指定哪些功能对外部可见,从而实现模块的封装和隐藏内部实现。
  3. 静态编译:ES 模块在代码解析时就会静态地分析导入和导出语句,确定模块的依赖关系。这种方式使得编译器可以在运行前检查模块的有效性,并且可以进行优化,提高代码的执行效率。
  4. 默认导出和命名导出:ES 模块支持默认导出和命名导出。默认导出是每个模块只能有一个默认导出,而命名导出可以有多个。默认导出使用 export default 语法,命名导出使用 export 语法。
  5. 动态导入:ES 模块支持动态导入,允许在代码执行时根据需要动态加载模块。动态导入使用 import() 函数,返回一个 Promise 对象。

Pure ESM package

https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c

Pure ESM package 中文版

script 模块

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script

module

此值导致代码被视为 JavaScript 模块。其中的代码内容会延后处理。charsetdefer 属性不会生效。对于使用 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 用文件夹区分比较好。