前面对比了前端模块化规范的优缺点前端模块化:commonJS、AMD、CMD、UMD、ES module,接下来深入探讨下 CommonJs 如何在浏览器运行
概述
目前 npm 包大部分是按照 Node.js 环境设计的,遵循 CommonJs
规范,浏览器没办法直接运行。要在浏览器上运行,需要进行格式的转化
那我们依赖根据CommonJs开发的页面是怎么在浏览器运行的呢?
原理
浏览器不兼容 CommonJS 主要有两大原因
同步加载
CommonJS
模块是同步加载的,也就是说,当遇到require
语句时,系统会停下来等待模块加载完成并执行完整个文件,然后才会继续执行后面的操作。
-
在服务器端,资源文件(模块)通常都是存放在本地磁盘,IO 操作速度相对较快,这样的设计是可以接受的。
-
但是对于浏览器来说,模块文件需要从服务器获取,等待的时间取决于网络速度,如果直接采用这种模式,可能会导致页面的加载和执行被阻塞,从而影响用户体验。
环境变量
Node.js 环境主要提供一下环境变量:
- module
- exports
- require
- global
实现思路
- 对于同步加载,我们只需要把依赖的文件全部打包进来,避免同步加载阻塞的问题;(这里也体现了 ES Module 静态化的优势,tree-shaking 只会引入文件中使用到的模块)
- 对于全局变量,我们只要能提供对应变量,浏览器就能处理 CommonJS 模块 简单示例:
javascript
const module = {
exports: {},
};
const require = () => {};
(function (module, exports, require) {
exports.add = function (a, b) {
return a + b;
};
})(module, module.exports);
const sum = module.exports.add;
sum(1, 2); // 3
目前 Webpack,Browserify 等工具都能很好将 CommonJS 语法转换为浏览器能理解的语法
下面将通过一个简单的 webpack 构建产物来深入理解处理后的 CommonJs
webpack5 构建 CommonJs 代码解析
javascript
// 将js封装打包成一个立即执行函数(IIFE)
(() => {})();
- 问题一:解决同步加载问题,将所有模块都打包进来
javascript
(() => {
// __webpack_modules__ 是一个包含所有模块的对象。
var __webpack_modules__ = {
// "./main.js" 是一个模块。
"./main.js": (module, exports, __webpack_require__) => {
// 使用 eval 执行模块内部的代码。
eval(
'const { add } = __webpack_require__(/*! ./add.js */ "./add.js");\n\nconst sum = add(1, 2);\n\nconsole.log(sum);\n\n\n//# sourceURL=webpack://webpack-demo/./main.js?'
);
},
// 同样 "./add.js" 也是一个模块。
"./add.js": (module, exports, __webpack_require__) => {
// 使用 eval 执行模块内部的代码。
eval(
"function add(a, b) {\n return a + b;\n}\nmodule.exports = {\n add: add,\n};\n\n\n//# sourceURL=webpack://webpack-demo/./add.js?"
);
},
};
})();
- 问题二:解决全局变量问题,核心实现是通过 require 方法,注入变量
javascript
// 缓存已加载的模块,减少多次加载 和 循环引用问题
var __webpack_module_cache__ = {};
// 核心方法:require导入
function __webpack_require__(moduleId) {
// 检查是否有缓存,有缓存则直接导出
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 如果没有缓存过,对应key创建一个缓存对象
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
// 执行模块函数,并传入module, module.exports, __webpack_require__等变量
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// 返回模块的exports,所以返回是module内的一个对象模块
return module.exports;
}
下面是完整的构建后代码
javascript
(() => {
// __webpack_modules__ 是一个包含所有模块的对象。
var __webpack_modules__ = {
// "./main.js" 是一个模块。
"./main.js": (module, exports, __webpack_require__) => {
// 使用 eval 执行模块内部的代码。
eval(
'const { add } = __webpack_require__(/*! ./add.js */ "./add.js");\n\nconst sum = add(1, 2);\n\nconsole.log(sum);\n\n\n//# sourceURL=webpack://webpack-demo/./main.js?'
);
},
// 同样 "./add.js" 也是一个模块。
"./add.js": (module, exports, __webpack_require__) => {
// 使用 eval 执行模块内部的代码。
eval(
"function add(a, b) {\n return a + b;\n}\nmodule.exports = {\n add: add,\n};\n\n\n//# sourceURL=webpack://webpack-demo/./add.js?"
);
},
};
// 缓存已加载的模块,减少多次加载 和 循环引用问题
var __webpack_module_cache__ = {};
// 核心方法:require导入
function __webpack_require__(moduleId) {
// 检查是否有缓存,有缓存则直接导出
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 如果没有缓存过,对应key创建一个缓存对象
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
// 执行模块函数,并传入module, module.exports, __webpack_require__等变量
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// 返回模块的exports,所以返回是module内的一个对象模块
return module.exports;
}
// 加载模块并导出
var __webpack_exports__ = __webpack_require__("./main.js");
})();
至此,通过以上代码,我们也能更好理解 module, exports, require 方法之间的关系
实际返回的是module.exports
:所以直接赋值exports = xxx
或者module = xxx
,都没办法拿到值,类似如下操作:
javascript
const module = {
exports: 0,
};
function test(module, exports) {
module = {
exports: 1,
};
exports = 1;
}
test(module, module.exports);
console.log(module, module.exports); // {exports: 0} 0
所以应该通过 module.exports 修改
javascript
const module = {
exports: 0,
};
function test(module, exports) {
module.exports = 1;
}
test(module, module.exports);
console.log(module, module.exports); // {exports: 1} 1