记录一下前端学习笔记,坚持打卡记录!!
开始第五篇
什么是模块化?
- 内部、外部模块的合理使用和管理
- 模块源码到目标模板代码的编译
前端模块化发展史
- node.js 09
- npm 10
- AMD requirejs 10
- CMD seajs 11
- webpack 12
- grunt 12
- gulp 13
- react 13
- vue 14
- angular2 16
- vite 20
- snowpack 工程化 20
从以上可以知道先出现打包工具, 再出现前端框架。
前端模块化大致经历了以下几个阶段:
graph LR
无模块化 --> IIFE -->CommonJs --> AMD --> CMD -->UMD --> ESM
下面依次来介绍一下这几个阶段
无模块化
- 多种js为了维护和可读性,不同类型和功能的js被分配在不同文件中
- 不同文件在同一个模板中被使用
<script src="jquery.js"></script>
<script src="main.js"></script>
<script src="deps1.js"></script>
<script src="deps2.js"></script>
优点:比起之前所有的功能模块放在一个文件中,这种方式是最简单初步的模块化,相对是进步的。
缺点:各个js中定义的变量依然是全局的,存在全局变量为污染,且每个模块之前的变量名不能重复,不利于项目的开发与维护。
IIFE(Immediately-invoked function expression ):立即执行函数
利用函数的作用域限制定义模块
const iifeCounterModule = (() => {
let count = 0;
return {
increase: () => ++count;
reset: () => {
count = 0;
console.log('hahaha count is reset');
}
}
})();
iifeCounterModule.increase();
iifeCounterModule.reset();
如果函数依赖于其它模块呢?
const iifeCounterModule = ((dependencyModule1, dependencyModule2) => {
let count = 0;
// dependencyModule做处理
return {
increase: () => ++count;
reset: () => {
count = 0;
console.log('hahaha count is reset');
}
}
})(dependencyModule1, dependencyModule2);
问题:了解jquery的依赖处理以及模块加载方案吗?
答:IIFE 加 传参调配。通过将整个jQuery库包裹在一个自执行的函数中,可以避免全局命名空间的污染,同时确保模块之间的依赖关系正确加载。当jQuery库被加载到页面中时,这个自执行的函数会立即执行,从而完成模块的加载和初始化。
成熟期
CommonJs:CJS module
这个是node.js制定,主要使用特征:
- 通过module + exports来对外暴露接口
- require调用其它模块
使用举例:
// commonJSCounterModule.js
const dependencyModule1 = require('./dependencyModule1');
const dependencyModule2 = require('./dependencyModule2');
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log('hahaha count is reset');
};
exports.increase = increase;
exports.reset = reset;
module.exports = {
increase,
reset
}
// main.js
const { increase, reset } = require('./commonJSCounterModule')
increase();
const commonJSCounterModule = require('./commonJSCounterModule')
commonJSCounterModule.increase();
以上代码实际执行后的代码如下:
(function(exports, require, module, __filename, __dirname) {
const dependencyModule1 = require('./dependencyModule1');
const dependencyModule2 = require('./dependencyModule2');
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log('hahaha count is reset');
};
module.exports = {
increase,
reset
};
return module.exports;
}).call(thisValue, exports, require, module, filename, dirname);
(function (exports, require, module, __filename, __dirname) {
const commonJSCounterModule = require('./commonJSCounterModule')
commonJSCounterModule.increase();
}).call(thisValue, exports, require, module, filename, dirname);
由实际执行处理的代码可以看出,require,module这些js本身是这些方法的,是需要作为立即执行的参数依赖,由node.js传入的。 Commonjs总结: 优点: Commonjs规范在服务端完成了javascript的模块化,解决了依赖、全局变量污染的问题,这是js运行在服务端运行的必要条件。 缺点: 由于服务端以及commonjs是同步加载模块的,但是对于浏览器而言,异步加载是很重要的,所以需要可以支持异步的解决方案。
在看异步解决方案之前,先来看两个关于cjs的两个问题。
-
module.exports和exports的关系
Commonjs是没有module.exports概念的,但是为了实现模块的导出,node使用了Module类,每一个模块就是Moudule的一个实例,所以真正导出的不是exports而是moudule.exports。
因为module对象的exports属性是exports对象的一个引用;就是说
module.exports=exports=require(xx)。
注意: exports使用的时候如果是exports= xx,则导出是无效的,因为exports参数实际上传入的是module.exports,直接覆盖是不会改变module.exports的
-
如果出现循环引用会怎样?
即使出现循环引用,也不会出现无限循环的问题。
因为在使用require()加载一个模块时,由于存在缓存机制,加载模块时会先判断是否存在该模块的缓存,若存在,则会直接返回缓存中该模块的module.exports,而不会在重复执行。就是说只有在第一次加载时,加载的模块代码才会执行。但是如果如果某些代码块想要执行多次怎么办?可以导出一个函数,然后多次调用函数即可。
接下来看看异步加载的规范
AMD:Asynchronous Module Definition
(经典的实现框架:require.js)
定义的方式:
// 通过define来定义一个模块,然后require加载
define(id, [depends], callback)
require([module], callback)
模块定义方式举例:
define(
'amdCounterModule',
['dependencyModule1', 'dependencyModule2'],
(dependencyModule1, dependencyModule2) => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log('hahaha count is reset');
};
return {
increase,
reset
}
}
)
require(
['amdCounterModule'],
amdCounterModule => {
amdCounterModule.increase();
amdCounterModule.reset();
}
)
那如果想在AMD中使用require方式加载同步模块可以吗? AMD支持向前兼容的,使用require方法动态加载模块
define(require => {
const dependecyModule1 = require('dependecyModule1');
const dependecyModule2 = require('dependecyModule2');
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log('hahaha count is reset');
};
// return {
// increase,
// reset
// }
// revealing
exports.increase = increase;
exports.reset = reset;
})
Amd总结:
优点:可以在浏览器中异步加载模块,同时又采用了commonm模块
缺点:提高了开发成本,并且不能按需加载,必须提前加载所有依赖
那有没有什么方式可以统一AMD和Commonjs呢?
UMD:Universal Module Definition
其实UMD就是判断了是amd、node还是浏览器环境,根据不同规范按照不同方式加载,进行兼容,所以现在很多包打出来的都是umd格式。
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery', 'underscore'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory(require('jquery'), require('underscore'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.jQuery, root._);
}
}(this, function ($, _) {
// methods
function a(){}; // private because it's not returned (see below)
function b(){}; // public because it's returned
function c(){}; // public because it's returned
// exposed public methods
return {
b: b,
c: c
}
}));
前面的这些规范,都不是js官方提出的,直到ES6模块化出现ESM,解决了上述的一系列问题。
ES6模块化 - ESM
定义方式:import引入;export导出 举个常用例子:
import dependencyModule1 from './dependencyModule1';
import dependencyModule2 from './dependencyModule2';
let count = 0;
export const increase = () => ++count;
export const reset = () => {
count = 0;
console.log('hahaha count is reset');
};
export default {
increasem
reset
}
es6异步加载方式:
import('./esModule').then(({ increase, reset }) => {
increase();
reset();
});
import('./esModule').then((dynamicESModule) => {
dynamicESModule.increase();
dynamicESModule.reset();
});
总结
现在模块化大多使用最新的esm的方式,但是如果要考虑兼容性的话,直接打包出umd的格式基本可以满足大部分情况。