不同的需求与不同的运行环境,衍生出了不同的模块化标准,比如commonjs/esmodule/amd/cmd等,当然还有兼容大部分标准的umd。
commonjs由node.js的具体实现。
esmodule是js/es的标准实现,将是未来的主流。
amd/cmd是异步的模块化规范,有requirejs,sea.js,systemjs等实现。
在实际项目中,commonjs/esmodule是用的更多的。在我们的项目中,通常是commonjs/esmodule写代码,最后转成了webpack实现的浏览器端commonjs。
在试验webpack前,我们先来试想下commonjs是如何运行呢?module,exports是个怎么回事?
接下来,我们暂且用commonjs机制来瞧瞧打包的前后结果。
b.js:
module.exports = 2;
a.js:
const b = require('./b.js');
module.exports = function (num) {
return num+b
};
入口文件index.js:
const a = require('./a.js');
console.log(a(2));
实际上三个文件,逻辑很简单,即入口index.js引入了a,a是个函数,接受一个参数,并引入了b.js导出的常量,返回参数与常量相加的结果。
众所周知,浏览器不支持commonjs规范,想要让这样的代码生效,就要做模块化打包。
我们接下来会用webpack尝试打包,但尝试之前我们想来设想下,commonjs是怎么回事,requre,module和exports是怎么回事,为什么通过require可以引用到其他的模块,require是怎么来的?
站在node.js设计者的角度上来想,如何实现一个模块加载机制呢?如何管理这些依赖和文件呢?
首先我们要把所有用到的文件加载出来,这一步按照node.js加载规则递归逐个分析并读取就好,重点在于读取后怎么办呢?
比如上面的代码,我们的运行文件是index.js,我们调用了require(‘./a.js’),那么就去找到a.js文件,把文件内容读取出来,缓存起来。接下来,在a里又发现了require(‘./b.js’),那么就去找到b.js,把文件内容读取出来,缓存起来,直到没有require为止,当然了,这里深入下去还会涉及到循环引用的问题,但百度上有一堆解读的文章,有兴趣的同学可以去了解一下,我们这里先不跑题。
回到之前的问题,加载文件就是一个寻找require然后加载文件内容并缓存的过程,那么要存在哪呢?肯定是内存里了,以什么形式去存呢?数组肯定是无法满足了,对象(map)是可以的。首先,我们在加载文件的过程中,是可以获取到当前目录的,比如a.js在c盘目录下,那全局路径就是c:/a.js。c:/a.js是一个唯一的标识,那么就可以作为该文件的key。value的话,我们可以是用一个函数,来包裹住文本内容。
const sourceModules = {
"c:\\index.js":function(module,exports,require) {
const a = require('./a.js');
console.log(a(2));
},
"c:\\a.js":function(module,exports,require) {
const b = require('./b.js');
module.exports = function (num) {
return num+b
};
},
"c:\\b.js":function(module,exports,require) {
module.exports = 2;
},
}
运行文件是index.js,第一步肯定要先执行他。后面每个引用的模块,第一次被引用时,都会被调用一次。但是require方法,还有modules和exports等对象从哪来呢?当然要自己实现了。
function run(execFile,modules) {
const installedModules = {};
const require = function require(id) {
// 假设currentPath是当前文件的cwd目录 并且加上id 就是完整的路径
const currentPath = "c:\\";
const fullModuleName = currentPath+id.slice(2);
// 如果已经引用过这个模块了,直接返回结果,不重新执行
if(installedModules[fullModuleName]) {
return depModules[fullModuleName].exports;
}
const module = {
id:fullModuleName,
exports:{},
}
installedModules[fullModuleName] = module;
sourceModules[fullModuleName](module,module.exports,require);
return module.exports;
}
require(execFile);
}
run("c:\index.js",sourceModules);
我们现在把这两段代码运行到浏览器里去运行一下,运行成功,并如期望般输出了a(2)的结果,即2+2等于4。真的没有什么黑魔法,只不过是map的与缓存模式结合的简单实践。我们仅仅用了几十行代码就实现了一个简易的commonjs模块加载器。
下面我们用webpack来打包下看看:
/******/
(function (modules) { // webpackBootstrap
/******/ // The module cache
/******/
var installedModules = {};
/******/
/******/ // The require function
/******/
function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/
if (installedModules[moduleId]) {
/******/
return installedModules[moduleId].exports;
/******/
}
/******/ // Create a new module (and put it into the cache)
/******/
var module = installedModules[moduleId] = {
/******/ i: moduleId ,
/******/ l: false ,
/******/ exports: {}
/******/
};
/******/
/******/ // Execute the module function
/******/
modules[moduleId].call(module.exports , module , module.exports , __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/
module.l = true;
/******/
/******/ // Return the exports of the module
/******/
return module.exports;
/******/
}
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/
__webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/
__webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/
__webpack_require__.d = function (exports , name , getter) {
/******/
if (!__webpack_require__.o(exports , name)) {
/******/
Object.defineProperty(exports , name , {
enumerable: true ,
get: getter
});
/******/
}
/******/
};
/******/
/******/ // define __esModule on exports
/******/
__webpack_require__.r = function (exports) {
/******/
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/
Object.defineProperty(exports , Symbol.toStringTag , { value: 'Module' });
/******/
}
/******/
Object.defineProperty(exports , '__esModule' , { value: true });
/******/
};
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/
__webpack_require__.t = function (value , mode) {
/******/
if (mode & 1) value = __webpack_require__(value);
/******/
if (mode & 8) return value;
/******/
if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/
var ns = Object.create(null);
/******/
__webpack_require__.r(ns);
/******/
Object.defineProperty(ns , 'default' , {
enumerable: true ,
value: value
});
/******/
if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns , key , function (key) { return value[key]; }.bind(null , key));
/******/
return ns;
/******/
};
/******/
/******/ // getDefaultExport function for compatibility with non-harmony
// modules
/******/
__webpack_require__.n = function (module) {
/******/
var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/
__webpack_require__.d(getter , 'a' , getter);
/******/
return getter;
/******/
};
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/
__webpack_require__.o = function (object , property) { return Object.prototype.hasOwnProperty.call(object , property); };
/******/
/******/ // __webpack_public_path__
/******/
__webpack_require__.p = "/";
/******/
/******/
/******/ // Load entry module and return exports
/******/
return __webpack_require__(__webpack_require__.s = "./index.js");
/******/
})
/************************************************************************/
/******/({
/***/ "./a.js":
/*!**************!*\
!*** ./a.js ***!
\**************/
/*! no static exports found */
/***/ (function (module , exports , __webpack_require__) {
var b = __webpack_require__(/*! ./b.js */ "./b.js");
module.exports = function (num) {
return num + b;
};
/***/
}) ,
/***/ "./b.js":
/*!**************!*\
!*** ./b.js ***!
\**************/
/*! no static exports found */
/***/ (function (module , exports) {
module.exports = 2;
/***/
}) ,
/***/ "./index.js":
/*!******************!*\
!*** ./index.js ***!
\******************/
/*! no static exports found */
/***/ (function (module , exports , __webpack_require__) {
var a = __webpack_require__(/*! ./a.js */ "./a.js");
console.log(a(2));
/***/
})
/******/
});
上面有一些无用的代码和注释,我们精简一下,看看实际产生的代码。
(function (modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId ,
l: false ,
exports: {}
};
modules[moduleId].call(module.exports , module , module.exports , __webpack_require__);
module.l = true;
return module.exports;
}
return __webpack_require__("./index.js");
})({
"./a.js": (function (module , exports , __webpack_require__) {
var b = __webpack_require__("./b.js");
module.exports = function (num) {
return num + b;
};
}) ,
"./b.js": (function (module , exports) {
module.exports = 2;
}) ,
"./index.js": (function (module , exports , __webpack_require__) {
var a = __webpack_require__("./a.js");
console.log(a(2));
})
});
除了用了立即执行函数,和改了require方法名外,基本和我们的实现是一致的,为什么webpack把require改成了webpackrequire__而不是require?这个问题大家可以想一想。
顺着生成的文件和代码,我们来逆向推理一下,webpack是如何做到这一点的?
从webpack的设计者角度出发,我们首先要读取配置,通常也就是webpack.config.js,在这个文件里我们会读取到入口文件的配置,打包结果的配置,entry即是我们的index.js。我们必须要有一个入口文件,否则代码无法执行,这就是为什么entry是必填项的原因。
接下来,通过entry文件加载,先分析调用了require方法的地方,分解出require的调用参数也就是模块id,然后拼接路径,去给文件内容读取出来,建立sourceModules,来确保后面代码runtime的依赖关系,其实一切依赖都是被摊平的一层维度。
抽象函数的过程就是找出不变,和变化的地方,我们这里也应当遵循这个原则,例如require方法的实现和installedModules的初始化,以及是不会产生变化的。
唯一变动的,就是初始调用的入口id还有modules,那我们就可以基于这个不变的模板来动态生成文件。
