前端模块化发展进程
没有模块化之前
在没有模块化的情况下,所声明的变量和函数都会挂载在全局环境当中,存在变量污染的问题。 多个js文件存在的情况下,还存在开发者需要对js文件进行顺序排列的问题(否则会报变量未定义)。
最早的模块化
最早的模块化是用立即执行函数(IIFC)包裹实现的,由于它自身可以实现封装变量(早期的块作用域)的功能,可以避免大部分变量暴露在全局当中。
但是!
1.还是会存在全局变量污染的问题,因为IIFE暴露出来的对象还是挂载在window对象上。
2.还是存在js文件的顺序排列问题。
服务端模块化开始快速发展
09年nodeJs的出现,给我们带来了模块化的思想--commonJS.
Commonjs是node服务端使用的模块化规范。
每一个文件都是一个模块,用require和module.exports引入和导出模块。
特点:用同步的方式去加载模块,并且模块引入是动态引入(在代码执行时才知道需要什么包,跟es module形成对比,后面讲)
如果想让程序运行在浏览器端,必须使用bundler(webpack等打包工具)将其转换成浏览器可以识别的样子。
因为浏览器不认识require(webpack自己实现了一个require函数)。
浏览器端模块化出现---AMD
为了实现前端浏览器模块化,出现了AMD。
AMD使用define和require定义和导入模块。
AMD规范采用异步的方式去加载模块。模块的加载不会影响到后面代码的实现。所有依赖这个模块的代码,都放在一个回调函数中,当模块加载完毕后才会执行回调。
不能直接应用在浏览器上,如果要应用在浏览器端,必须要引入requireJS这个bundler。
因为浏览器不认识require、define。
模块化兼容方式--UMD
将Commonjs、AMD、浏览器直接可用的方法进行了兼容,可以在三种环境中使用,也是唯一一种可以直接在HTML中引入script包使用的模块化工具(原理是向全局环境中注入全局变量)。
由于UMD支持在浏览器端使用,不需要bundler,可以直接在浏览器使用。
比如,jquery的UMD包会向全局对象中注入$对象。
但是如果想在项目当中使用,必须要先将包的依赖也通过script的形式引入(比如antd依赖react、reactDOM和moment)。
前端模块化大一统--ES Module
ES6 在语言标准的层面上,实现了模块化功能。
由于不同浏览器对于ESModule的支持程度不一样,必须要使用bundler(webpack、rollup等)、babel进行转换。
ES Module支持静态分析,也就是它只能从头部引入模块,不能像commonJS那样动态引入
// 动态引入
if(){
require('a')
} else {
require('b')
}
所以,ES Module可以实现tree shaking,按需加载等优化功能。
例如antd,就是通过ES Module方式实现按需加载的,用了哪个组件就只加载和打包哪个组件。
总结
1.UMD
优点:可直接在浏览器中使用。
缺点:无法实现按需加载和tree shaking等优化,会大大增加最终打包后的体积。并且需要考虑js包引入顺序问题。还需要把他的依赖也通过js包的形式引入才行。
2.AMD
由于使用方式太过复杂,还需要使用requireJS,现在已经很少使用。
3.CommonJS
用在服务器端,使用方便。
使用场景为webpack、node等服务端场景中。
但是现在node已经支持处理es modules模块,所以ES module也可以用在服务端。
4.ES module
是js原生支持的,使用方式简单,具有天生的优势。支持tree shaking和按需加载。
参考文章:
为什么只有ES Module模式可以实现tree shaking?
使用三句话概括以上三篇文章:
1. 可以在node中使用ES Module,通过创建mjs文件的方式。
2. ES Module是静态引用,CommonJS是动态引用,因此ES Module可以使用tree shaking
3. 使用tree shaking之后,代码包还是很大,很可能是因为side Effects
注:webpack和rollup配置文件中的"module": "dist/index.js",该配置项就是告诉webpack和rollup项目提供的es module代码入口,可以让webpack和rollup开启tree shaking等优化模式(之前的main字段是为node设计的,一般用来输出commonjs入口文件)。
- webpack是如何实现模块化的? 在webpack打包时,babel会将ES6模块转换成commonjs规范,但是我们前面不是说commonjs不能用在浏览器端吗?为什么这里可以?这是因为webpack自己实现了一个require函数,用来加载commonjs模块的内容,使得最后的打包代码可以在浏览器中使用。
附赠:webpack模块化原理
直接上demo:
// webpack.config.js
const path = require('path');
module.exports = {
entry: './a.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
}
};
// a.js
import a from './c';
export default 'a.js';
console.log(a);
// c.js
export default 333;
// 打包之后的代码
(function(modules) {
function __webpack_require__(moduleId) {
var module = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module, module.exports, __webpack_require__);
return module.exports;
}
return __webpack_require__(0);
})([
// 入参数组
(function (module, __webpack_exports__, __webpack_require__) {
// 引用 模块 1
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__c__ = __webpack_require__(1);
/* harmony default export */ __webpack_exports__["default"] = ('a.js');
console.log(__WEBPACK_IMPORTED_MODULE_0__c__["a" /* default */]);
}),
(function (module, __webpack_exports__, __webpack_require__) {
// 输出本模块的数据
"use strict";
/* harmony default export */ __webpack_exports__["a"] = (333);
})
]);
打包之后的代码就是webpack运行时的代码,包括webpack模块的实现。 我们打包之后的代码其实是一个自执行函数。
(function(modules) {
})([]);
自执行函数的入参是各个文件通过babel转码之后组成的code模块数组。 重点在于__webpack_require__函数的实现(webpack自己实现的require函数),这个函数就是webpack自己实现的require函数。该函数本质上就是执行一个模块的代码,然后将相应的导出变量挂载在module.exports对象上。这样就可以在其他模块通过__webpack_require__引入。
注:webpack在最后生成文件的时候会将代码中的require方法替换成__webpack_require__,否则会报错:require is not defined.
在自执行函数执行的最后,会将主文件的输出模块module.exports返回出来,便于日后我们可以require这个包的内容。
最后:这样打包之后的js文件,并不能被其他的文件引用,因为它只作用于当前作用域,这个js文件不能被其他模块通过import或者require的方式引用。
webpack编译后的文件如何被其他模块引用?
webpack通过output.libraryTarget属性来设置打包输出的包的引用方式。 打包输出方式可以参见我的另一篇博客 output.libraryTarget :commonjs2; 会将打包过的代码赋值给module.exports。
这样,就可以在别的模块利用require引用这个模块。
babel的作用
babel在webpack中用来转换es6语法,那它是怎么转换es6语法的呢?
1.处理导出模块
export default 123;
export const a = 123;
const b = 3;
const c = 4;
export { b, c };
babel会将这些都转换成commonjs的exports写法:
exports.default = 123;
exports.a = 123;
exports.b = 3;
exports.c = 4;
exports.__esModule = true;
exports.__esModule = true;表示是由ES6转换来的。
2.处理默认的导入模块
import a from './a.js';
function _interopRequireDefault(obj) {
return obj && obj.__esModule
? obj
: { 'default': obj };
}
var _a = require('assert');
var _a2 = _interopRequireDefault(_a);
var a = _a2['default'];
最后的 a 变量就是 require 的值的 default 属性。如果原先就是commonjs规范的模块,那么就是那个模块的导出对象。
3.引入 * 通配符
import * as a from './a.js'
function _interopRequireWildcard(obj) {
if (obj && obj.__esModule) {
return obj;
}
else {
var newObj = {}; // (A)
if (obj != null) {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key))
newObj[key] = obj[key];
}
}
newObj.default = obj;
return newObj;
}
}
4.import { a } from './a.js' 直接转换成 require('./a.js').a 即可。
总结:由于babel会帮我们将ES6的模块转换为commonjs,所以我们在引入的时候可以混合使用ES6和commonjs规范。
为什么有时候需要使用require('').default来引用模块?
babel在对模块进行转换的时候,会把es6的export default转换为exports.default属性,即使这个模块只有一个输出。所以引用的时候我们需要加上default属性来引用这个模块的默认导出。