基础系列-模块化
模块化
模块化一般指的是可以被抽象封装的最小/最优代码集合,模块化解决的是功能耦合问题。
每个模块都能拥有小于完整程序的体积,使得验证、调试及测试变得轻而易举。
用require
语句(CommonJS模块)进行模块化开发了。同样,还有一个流行的JavaScript模块化标准,叫作异步模块定义(AMD
)。RequireJS
是AMD
最流行的实现。
https://exploringjs.com/es6/ch_modules.html 摘抄如下:
It is impressive how well ES5 module systems work without explicit support from the language. The two most important (and unfortunately incompatible) standards are:
- CommonJS Modules:
The dominant implementation of this standard is in Node.js (Node.js modules have a few features that go beyond CommonJS). Characteristics:
Compact syntax
Designed for synchronous loading and servers
Asynchronous Module Definition (AMD):
The most popular implementation of this standard is RequireJS. Characteristics:
- Slightly more complicated syntax, enabling AMD to work without eval() (or a compilation step)
- Designed for asynchronous loading and browsers
The above is but a simplified explanation of ES5 modules. If you want more in-depth material, take a look at “Writing Modular JavaScript With AMD, CommonJS & ES Harmony” by Addy Osmani.
CJS(CommonJS)
是CommonJs
的缩写。
CommonJS
模块: 是一种写法规范,导入用require
,导出用module.exports
。
同步加载。主要用于后端。
nodejs
中使用CJS
模块。
CJS
不能在浏览器中工作。它必须经过转换和打包。
关键词:
- require:引入一个模块
- export:导出模块内容
- module:模块本身
// importing
const doSomething = require('./doSomeing.js');
// exporting
module.exports = function doSomething(n) {
// do something
}
AMD(Asynchronous Module Definition)
AMD
是异步导入模块的。前端较为常用。
RequireJS
是AMD规范的具体实现。
解决问题:
- 多个JS文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器;
- JS加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长。
关键词:
- id:模块的id
- dependencies:模块依赖
- factory:模块的工厂函数,即模块的初始化操作函数
- require:引入模块
define(['dep1', 'dep2'], function(dep1, dep2) {
// Define the module value by returning a value
return function() {}
})
// 或
// RequireJS: simplified CommonJS wrapping
define(function(require) {
var dep1 = require('dep1')
var dep2 = require('dep2')
return function() {}
})
// 或
// 定义模块 myModule.js
define('myModule', ['dependency'], function(){
var name = 'Byron';
function printName(){
console.log(name);
}
return {
printName: printName
};
});
// 加载模块
require(['myModule'], function (my){
my.printName();
});
CMD(Common Module Definition)
SeaJS
是对CMD
的具体实现。
主要用于前端。
// 定义模块 myModule.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var foo = require('foo');
var out = foo.bar();
$('div').addClass('active');
module.exports = out;
});
// 加载模块
seajs.use(['myModule.js'], function(my){
});
SeaJS定义了一个全局函数define(id?, deps?, factory)
来创建一个模块, define接受一个需要三个参数的函数, 分别为:
- require: 一个方法, 接受模块标识 作为唯一参数,用来获取其他模块提供的接口:require(id)
- exports: 一个对象, 用来向外提供模块接口
- module: 一个对象, 上面存储了与当前模块相关联的一些属性和方法
CMD推崇依赖就近原则(也就是懒加载), 模块内部的依赖在需要引入的时候再引入, 如上例中的var $ = require('jquery.js')
, 这一点和通用的CommonJS
模块风格保持一致。
AMD与CMD的区别
AMD
推崇依赖前置,在定义模块的时候就要声明其依赖的模块,CMD
推崇就近依赖,只有在用到某个模块的时候再去require
- 两个都是定义的全局
define
函数来定义模块,define
接收函数function(require, exports, module)
保持一致 CMD
是懒加载, 仅在require
时才会加载模块;AMD
是预加载, 在定义模块时就提前加载好所有依赖CMD
保留了CommonJS
风格
UMD(Universal Module Definition)
UMD
表示通用模块定义。在前后端都有解决方案,UMD
仓库 中有多种解决方案。
当使用 Rollup/Webpack
之类的打包器时,UMD
通常用作备用模块。
// 其中一种写法
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['jquery', 'underscore'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('jquery'), require('underscore'))
} else {
root.Requester = factory(root.$, root._)
}
}(this, function($, _) {
// this is where I defined my module implementation
var Requester = {
// ....
}
return Requester
}))
// 或
;(function (global) {
function factory () {
var moduleName = {};
return moduleName;
}
if (typeof module !== 'undefined' && typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && (define.cmd || define.amd)) {
define(factory);
} else {
global.moduleName = factory();
}
})(typeof window !== 'undefined' ? window : global);
// 引入
// Node.js
var myModule = require('moduleName');
// SeaJs
define(function (require, exports, module) {
var myModule = require('moduleName');
});
// RequireJs
define(['moduleName'], function (moduleName) {
});
// Browse global
<script src="moduleName.js"></script>
ESM
ESM
代表ES
模块。这是 Javascript
提出的实现一个标准模块系统的方案。
使用import
和export
进行模块的导入和导出。浏览器不支持时需要使用babel
进行转义。
具有 CJS
的简单语法和 AMD
的异步,import
可以返回一个promise
。
ES6
的静态模块结构,可以进行 Tree Shaking(删除多余代码,参考掘金文章Tree shaking)。
ESM
允许像 Rollup
、Webpack
这样的打包器,删除不必要的代码,减少代码包可以获得更快的加载。
关键词:
- import:引入模块依赖
- export:模块导出
// 方式一
import moduleA from './moduleA'; // 导入
export default { // 默认导出
...
}
// 方式二
import { moduleB } from './moduleB'; // 导入 重定向命名 module语法,不是解构语法
export const moduleB = {...} // 导出 声明的同时命名导出
export { // 与变量名绑定命名导出
moduleB: '...'
}
// 方式三 -- wildcard(通配符)
import * as moduleC from './moduleC' // 导入 重定向命名
export const a = 1; // 导出, 会将a, b当作属性打包到moduleC对象中
export const b = 1;
// 方式一和方式二 合并使用
import moduleA, { moduleB } from './moduleD' // 导入
export default moduleA; // 导出
export const moduleB;
方式1和方式2可以同时使用,使用的方法依旧是一一对应的方式
总结
- 由于
ESM
具有简单的语法,异步特性和Tree Shaking
,因此它是最好的模块化方案。只有ESM
才支持Tree Shaking
。 UMD
随处可见,通常在ESM
不起作用的情况下用作备用CJS
是同步的,适合后端AMD
是异步的,适合前端CJS
是值的拷贝,ESM
的export default
也是值的拷贝,ESM
的export {}
是引用的绑定。CJS
模块是运行时加载,ESM
是编译时输出接口。
CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
参考链接
https://blog.risingstack.com/node-js-at-scale-module-system-commonjs-require/
https://exploringjs.com/es6/ch_modules.html#sec_design-goals-es6-modules
掘金:https://juejin.cn/post/6935973925004247077
简书:https://www.jianshu.com/p/5226bd9644b6
深入 CommonJs 与 ES6 Module – segmentfault
推荐阅读:前端模块化详解(完整版)
模块间的相互转换
ESM 转换为 CJS
浏览器中不能直接运行ESM
,所以要将ESM
转换为CJS
,需要使用到babel
的工具库对代码进行转义和分析。
@babel/parser 帮助进行源代码语义分析 https://www.babeljs.cn/docs/babel-parser
@babel/traverse 对解析出的抽象语法树进行遍历 https://www.babeljs.cn/docs/babel-traverse
@babel/core babel的核心模块,对代码进行转义,使得可以在浏览器中运行 https://babeljs.io/docs/en/babel-core
@babel/preset-env代码转义插件,将代码转换为目标环境可运行的代码 https://babeljs.io/docs/en/babel-preset-env
先使用@babel/parser
将源代码转换为抽象语法树,然后使用@babel/core
对抽象语法树使用@babel/preset-env
设置目标环境转义代码。
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default; // 默认ES6 module的导出,使用要用default
const babel = require('@babel/core')
const moduleAnalyser = (filename) => { // 读取文件,获取文件内容 进行解析抽象语法树
const content = fs.readFileSync(filename, 'utf-8');
// 获取抽象语法树
const ast = parser.parse(content, {
sourceType: 'module'
})
const dependencies = {} // 获取依赖文件信息, 存入的路径是绝对路径或者是相对于bundle的相对路径
// 遍历抽象语法树,对特定的部分进行处理
// 处理路径,如果只存一个相对路径会在打包的时候比较麻烦,所以存一个相对路径和绝对路径
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
const sourceValue = node.source.value
const newFile = './' + path.join(dirname, sourceValue)
dependencies[sourceValue] = newFile; // 键值对的方式进行存储
}
});
// 对抽象语法树进行转换, 将转换后的code取出
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
console.log(code)
return {
filename, // 文件
dependencies, // 依赖
code // 转移后的代码
}
}
使用export
,示例文件word.js
:
// EMD 源代码
export const word = 'hello'
// babel转义后的code
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.word = void 0;
var word = 'hello';
exports.word = word;
export导出的实际上是exports对象
使用export
和import
,示例文件message.js
:
// ESM源代码
import { word } from './word.js';
const message = `say ${word}`;
export default message;
// babel转义后的code
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _word = require("./word.js");
var message = "say ".concat(_word.word); // 转义 { word }
var _default = message;
exports["default"] = _default;
export default导出会生成对应的exports.default
使用import
,示例文件index.js
且为入口文件:
// ESM 源代码
import message from './message.js';
console.log(message);
// babel转义后的code
"use strict";
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) {
// 如果是ESM 则直接返回,否则返回带有default的新对象
return obj && obj.__esModule ? obj : { "default": obj };
}
console.log(_message["default"]);
import转换成require
使用通配符的方式import
word.js改写为
export const word = 'hello'
export const word2 = 'hello2'
message.js改写为
import * as word from './word.js';
const message = `say ${word.word} ${word.word2}`;
export default message;
message.js转义为
"use strict";
function _typeof(obj) {
"@babel/helpers - typeof";
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function _typeof(obj) { return typeof obj; };
} else {
_typeof = function _typeof(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof(obj);
}
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var word = _interopRequireWildcard(require("./word.js"));
function _getRequireWildcardCache() {
if (typeof WeakMap !== "function") return null;
var cache = new WeakMap();
_getRequireWildcardCache = function _getRequireWildcardCache() { return cache; };
return cache;
}
function _interopRequireWildcard(obj) {
if (obj && obj.__esModule) { return obj; }
if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") {
return { "default": obj };
}
var cache = _getRequireWildcardCache();
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj["default"] = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
var message = "say ".concat(word.word, " ").concat(word.word2);
var _default = message;
exports["default"] = _default;
CJS的话就创建一个新的空对象,并把obj内的所有自有属性都浅复制到新对象中,最后再把obj赋值给新对象的default属性
将ESM转义成浏览器中可以运行的代码(核心部分,简易的打包库):
// index.js为仅简单的模块间相互调用的示例
// 简易的打包工具
// 对项目的打包,首先要先读取入口文件,对项目代码进行分析
// cli-highlight插件,帮助高亮cli输出
// @babel/parser 帮助进行源代码语义分析 https://www.babeljs.cn/docs/babel-parser
// @babel/traverse 对解析出的抽象语法树进行遍历 https://www.babeljs.cn/docs/babel-traverse
// @babel/core babel的核心模块,对代码进行转义,使得可以在浏览器中运行 https://babeljs.io/docs/en/babel-core
// @babel/preset-env代码转义插件,将代码转换为目标环境可运行的代码 https://babeljs.io/docs/en/babel-preset-env
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default; // 默认ES6 module的导出,使用要用default
const babel = require('@babel/core')
const moduleAnalyser = (filename) => { // 读取文件,获取文件内容 进行解析抽象语法树
const content = fs.readFileSync(filename, 'utf-8');
// 获取抽象语法树
const ast = parser.parse(content, {
sourceType: 'module'
})
const dependencies = {} // 获取依赖文件信息, 存入的路径是绝对路径或者是相对于bundle的相对路径
// 如果只存一个相对路径会在打包的时候比较麻烦,所以存一个相对路径和绝对路径
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
const sourceValue = node.source.value
const newFile = './' + path.join(dirname, sourceValue)
dependencies[sourceValue] = newFile; // 键值对的方式进行存储
}
});
// 对抽象语法树进行转换, 将转换后的code取出
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return {
filename, // 文件
dependencies, // 依赖
code // 转移后的代码
}
}
// 依赖图谱,将入口文件中的依赖文件再进一步分析,一层一层递进
const makeDependenciedGraph = (entry) => {
// 分析入口文件
const entryModule = moduleAnalyser(entry);
const graphArray = [ entryModule ]; // 初始化图谱数组
for (let i = 0; i < graphArray.length; i++) {
const item = graphArray[i];
const { dependencies } = item; // 获取依赖文件
if (dependencies) {
// 如果存在依赖,则循环依赖
for (let j in dependencies) {
// 对依赖文件进行分析,将分析结果存入到图谱数组中
graphArray.push(
moduleAnalyser(dependencies[j]) // 依赖的对象文件
); // 推入graphArray中后,长度变长,下一个进行遍历分析的文件
}
}
}
// 关系图谱数据解构转化,将数组转化为对象
const graph = {};
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
});
return graph;
}
// 最后的结果返回代码字符串
const generateCode = (entry) => {
// 转化成字符串传入到闭包中
const graph = JSON.stringify(makeDependenciedGraph(entry));
// 代码在闭包中执行避免污染外部环境
// require无法在浏览器中直接运行,需要进行创建require方法
// export存储导出的内容
// 返回一个字符串
// localRequire转换文件的相对路径返回真实路径
// 递归调用到最内层的依赖文件,然后通过exports导出
return `
(function(graph) {
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;
};
require('${entry}');
})(${graph});
`;
}
const code = generateCode('./src/index.js')
console.log(code)
// 注意:示例文件中本来是有注释的,但是注释加上去了之后就会出现问题,所以删除注释后在chrome://test模式下运行成功
总结
ESM
的export
会被转义为exports
对象,根据写法的不同,属性命名方式不同。ESM
的import
会因文件的export
方式不同而不同。- 引入
export default
导出的文件时,会创建一个空对象exports
,并把导出对象赋值到exports
对象的default
属性上。 - 引入
export
导出的文件时,对象解构的方式引入,会创建一个空对象exports
,并把导出的属性赋值到exports
对象上,并且同名。 - 引入
export
导出的文件时,通配符(*)的方式引入,会创建一个空对象exports
,并把 CJS 的导出对象exports
中的自有属性浅复制到新的空对象中,最后再把导出对象exports
赋值到新对象的default
属性
- 引入
在node中使用ESM
https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
.mjs
文件总是以 ES6 模块加载,.cjs
文件总是以 CommonJS 模块加载,.js
文件的加载取决于package.json
里面type
字段的设置。
注意,ES6 模块与 CommonJS 模块尽量不要混用。require
命令不能加载.mjs
文件,会报错,只有import
命令才可以加载.mjs
文件。反过来,.mjs
文件里面也不能使用require
命令,必须使用import
。
从 node v13.2.0开始,node 才对ES6模块化提供了支持。
node v13.2.0之前需要进行项目配置,在 package.json中添加属性:”type”: “module”,在执行命令中添加
node --experimental-modules src/index.js
在 node v13.2.0之后只需要在 package.json 中添加属性:”type”: “module”
导入文件时,需要跟上.js后缀名
参考文章
CommonJS Module和ES6 Modules之间的引用与转换
https://github.com/Datura35422/study/blob/master/webpack-demos/bundler/bundler.js
其他概念
组件化
组件化更像是模块化的进一步封装,根据业务特点或者不同的场景封装出具有一定功能特性的独立整体。
CSS样式模块化
CSS本身也具有@import
的方式来引入依赖模块。
Less、Sass、stuly等css预处理器、CSS in JS扩展了css模块化的功能。
webpack使用 css modules 处理css模块问题