我们都知道,webpack 作为一个构建工具,解决了前端代码缺少模块化能力的问题。我们写的代码,经过 webpack 构建和包装之后,能够在浏览器以模块化的方式运行。这些能力,都是因为 webpack 对我们的代码进行了一层包装,本文就以 webpack 生成的代码入手,分析 webpack 是如何实现模块化的。
PS: webpack 的模块不仅指 js,包括 CSS、图片等资源都可以以模块看待,但本文只关注 js。
首先我们创建一个简单入口模块 index.js 和一个依赖模块 bar.js:
- //index.js
- 'use strict';
- var bar = require('./bar');
- function foo() {
- return bar();
- }
- //bar.js
- 'use strict';
- exports.bar = function() {
- return 1;
- }
webpack 配置如下:
- var path = require("path");
- module.exports = {
- entry: path.join(__dirname, 'index.js'),
- output: {
- path: path.join(__dirname, 'outs'),
- filename: 'index.js'
- },
- };
这是一个最简单的配置,只指定了模块入口和输出路径,但已经满足了我们的要求。
在根目录下执行
,得到经过 webpack 打包的代码如下(去掉了不必要的注释):
- 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, {
- configurable: false,
- enumerable: true,
- get: getter
- });
- }
- };
- // 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 = 0);
- })
- /************************************************************************/
- ([
- /* 0 */
- (function(module, exports, __webpack_require__) {
- "use strict";
- var bar = __webpack_require__(1);
- bar();
- }),
- /* 1 */
- (function(module, exports, __webpack_require__) {
- "use strict";
- exports.bar = function() {
- return 1;
- }
- })]);
上面 webpack 打包的代码,整体可以简化成下面的结构:
- (function(modules) {
- /* 省略函数内容 */
- })([function(module, exports, __webpack_require__) {
- /* 模块index.js的代码 */
- },
- function(module, exports, __webpack_require__) {
- /* 模块bar.js的代码 */
- }]);
可以看到,整个打包生成的代码是一个 IIFE(立即执行函数),函数内容我们待会看,我们先来分析函数的参数。
函数参数是我们写的各个模块组成的数组,只不过我们的代码,被 webpack 包装在了一个函数的内部,也就是说我们的模块,在这里就是一个函数。为什么要这样做,是因为浏览器本身不支持模块化,那么 webpack 就用函数作用域来 hack 模块化的效果。
如果你 debug 过 node 代码,你会发现一样的 hack 方式,node 中的模块也是函数,跟模块相关的参数
、
- exports
,或者其他参数
- require
和
- __filename
等都是通过函数传值作为模块中的变量,模块与外部模块的访问就是通过这些参数进行的,当然这对开发者来说是透明的。
- __dirname
同样的方式,webpack 也控制了模块的
、
- module
和
- exports
,那么我们就看看 webpack 是如何实现这些功能的。
- require
下面是摘取的函数内容,并添加了一些注释:
- // 1、模块缓存对象
- var installedModules = {};
- // 2、webpack实现的require
- function __webpack_require__(moduleId) {
- // 3、判断是否已缓存模块
- if (installedModules[moduleId]) {
- return installedModules[moduleId].exports;
- }
- // 4、缓存模块
- var module = installedModules[moduleId] = {
- i: moduleId,
- l: false,
- exports: {}
- };
- // 5、调用模块函数
- modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
- // 6、标记模块为已加载
- module.l = true;
- // 7、返回module.exports
- return module.exports;
- }
- // 8、require第一个模块
- return __webpack_require__(__webpack_require__.s = 0);
模块数组作为参数传入 IIFE 函数后,IIFE 做了一些初始化工作:
,这个变量被用来缓存已加载的模块。
- installedModules
这个函数,函数参数为模块的 id。这个函数用来实现模块的 require。
- __webpack_require__
函数首先会检查是否缓存了已加载的模块,如果有则直接返回缓存模块的
- __webpack_require__
。
- exports
、
- module
和
- module.exports
作为参数传入。注意这里做了一个动态绑定,将模块函数的调用对象绑定为
- __webpack_require__
,这是为了保证在模块中的 this 指向当前模块。
- module.exports
的内容。
- exports
函数,require 第 0 个模块,也就是入口模块。
- __webpack_require__
require 入口模块时,入口模块会收到收到三个参数,下面是入口模块代码:
- function(module, exports, __webpack_require__) {
- "use strict";
- var bar = __webpack_require__(1);
- bar();
- }
webpack 传入的第一个参数
是当前缓存的模块,包含当前模块的信息和
- module
;第二个参数
- exports
是
- exports
的引用,这也符合 commonjs 的规范;第三个
- module.exports
则是
- __webpack_require__
的实现。
- require
在我们的模块中,就可以对外使用
或
- module.exports
进行导出,使用
- exports
导入需要的模块,代码跟 commonjs 完全一样。
- __webpack_require__
这样,就完成了对第一个模块的 require,然后第一个模块会根据自己对其他模块的 require,依次加载其他模块,最终形成一个依赖网状结构。webpack 管理着这些模块的缓存,如果一个模块被 require 多次,那么只会有一次加载过程,而返回的是缓存的内容,这也是 commonjs 的规范。
到这里,webpack 就 hack 了 commonjs 代码。
原理还是很简单的,其实就是实现
和
- exports
,然后自动加载入口模块,控制缓存模块,that's all。
- require
细心的你一定会发现,文章到这里只介绍了 webpack 对 commonjs 的实现,那么 ES6 module 是如何实现的呢?
敬请期待本系列第二篇《webpack 模块化原理 - ES6 module》。
来源: http://www.tuicool.com/articles/bEJjEjY