webpack|透过webpack打包文件看模块化

537 阅读6分钟

前言

  • 文章结构采用【指出阶段目标,然后以需解决问题为入口,以解决思路为手段,小测试加深印象】达到本文目标,若使诸君稍有启发,不枉此文心力^-^

目标

  • 了解webpack编译之后的dist中的main.js(一般是)的运行原理;
  • 了解webpack的模块化是如何实现的,已经如何兼容多个模块化标准的; 如何打包在之后系列文章中会补上

关键点

模块化无非导入导出规则的制定,webpack是基于commonJs规范的,所以问题转为

  1. commonJS的导入导出实现
  2. es6等导入导出的兼容(hammary),即转为commonJS

commonJS的导入导出实现

先看使用
## title.js
module.export = {
    a: 'title'
}
export.b = 'b'

## index.js

let title = require('title');
console.log(title);

核心问题
  1. 如何实现模块作用域上挂载module对象、epxorts对象、require函数
  2. 根据commonJS规范实现module对象、epxorts对象、require函数
  3. 实现require方法的缓存效果,即已加载过一次的模块再次加载只需要取之前的计算值
解决思路
  1. 实现模块函数的赋值

    • 作用域自然想到函数, 那我们就可以用一个函数去包裹用户定义逻辑(根据require传进来的路径去获取文件内容,并用eval包裹作为函数体得到一个函数),

    • 然后在模块安装时调用次模块对应函数,传递三个参数:module , module.exports,__webpack_require__(也就是我们的require)就实现了挂载

  2. 实现commonJS规范

    • 规定module对象是一个Object类型,有三个属性

      • i : 模块id
      • l :模块是否已加载完成
      • exports :模块导出对象
      注意:传给webpack的模块(注意区分,上面讲的模块对象是指webpack内模块对象)在webpack不同版本有不同描述,在3.xx中是对象,其模块id是key,val是函数,4.xx中直接是函数,模块id是数组索引;本文采用对象版本进行描述,更好理解,思路是一致的;
      
    • epxorts其实是module.exports的一个引用,传值时直接传module.exports即可

    • require函数是核心的模块安装函数,核心是创建模块对象进行模块安装,还有缓存避免循环依赖问题等等,逻辑较为复杂下文补充;

  3. 实现模块缓存

    • 我们为模块取一个模块id即可,当模块id在缓存对象(var installedModules = {};)中已存在,则直接取出;未存在则进行安装;
      • webpack内中定义的模块对象是一个Object类型,有三个属性
        • i : 模块id
        • l :模块是否已加载完成
        • exports :模块导出对象
注意,exportsmodule.exports的引用,也就意味着当引用关系改变,exports上赋值可能出现“赋值不上”的情况,此处故意含糊了点,因为后面我会提问加深印象,关键词:引用传递
实现逻辑:

首先,进入入口文件,其自然会存在require函数的执行,这个require我们前置中提到的_webpack_require,也是我们的核心方法,核心是生成对应模块对象,

然后,将module , module.exports,__webpack__require__传递给包裹着用户自定义逻辑的函数中,这也是我们“全局”有module、exports、require的原因;同时,逻辑中会为导出值赋值;

最后,__webpack__require__函数返回module.exports,这样,导入方也就拿到了依赖模块的导出值。

show the code(简化版打包结果)

(function(modules) { // webpackBootstrap  webpack 启动脚本
	// The module cache   模块缓存
	var installedModules = {};
	// The require function   就是我们用的require
	function __webpack_require__(moduleId) {
    // 1. 是否已经加载过 已加载就将加载过的返回
		// Check if module is in cache
		if(installedModules[moduleId]) {
			return installedModules[moduleId].exports;
        }
    }
    // 2. 未加载
		var module = installedModules[moduleId] = {
			i: moduleId,  // 模块id
			l: false,  // 是否 已加载完成
			exports: {} // 导出对象
		};
		//  执行传入对象中 moduleId对应的函数
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		module.l = true;
		return module.exports;
	}
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
// 传参
({
 "./src/index.js":
(function(module, exports, __webpack_require__) {
  let title = __webpack_require__("./src/title.js");
  console.log(title);
}),

"./src/title.js":
(function(module, exports) {
    module.exports = {
          a: 'title'
    };
    exports.b = 'b';
    })
});
小测试
## index.js
let title =  require('./title');
let {b} = title;
console.log(title,b);

## title.js
module.exports = {
    a: '1'
}
exports.b = 2

控制台会打印什么?

hammary:es6

先看使用
## index.js
import title,{esB} from './title';
console.log(title,esB);

## title.js
export default {
    esA: 1
}
export const esB = 2;

核心问题
  1. 模块来源的记录,便于引入方去区分加载方式
  2. 的导入导出转为commonJS的导入导出
解决思路

关键点:对于es和commonJs而言,最大的区别在于,es中的define语法导出值与直接export导出值并无关联;而在commonJs中其实只会导出一个export对象,其exports和module.exports是共享同一个对象(即地址值)的,所以在兼容时我们可以做如下处理

  1. 通过判断模块中是否有import export define 等关键字判断当前是否为es6模块,对es6模块先记录(模块对象上加__esModule属性为true)
  2. 导出时,模块对象的exports属性承载export语法的导出值,再在模块对象上添加default属性指向export default的导出值;
  3. 导入时,对__esModule属性进行判断,如果是es6模块,则默认值取模块对象exports的default指向值;如果是commonJs,则直接是模块对象的exports本身
  4. 至此,我们就实现了模块化对es6的hammary(融合)
实现逻辑
前提
  • 定义记录es模块函数,功能,传参导出值,将导出值标为es模块
/**
 * 1. 改变exports在调用Object.prototype.toString.call时的值  表示这是一个模块对象
 * 2. 增加__esModule属性 且值为true   用于表示此模块对象是es模块
 * @param {*} exports
 */
__webpack_require__.r = function(exports) {
	if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        // 此语法是设置toString函数的返回值的
		Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
	}
	Object.defineProperty(exports, '__esModule', { value: true });
};
  • 定义取值函数n,功能,根据模块导出值类型去区分取值,commonJS则返回export本身,es模块则返回export的default。
    • 其中定义属性可以封装一个方法d,先判断有无此属性(函数o),无则添加;
// 判断对象上是否存在属性
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

// definePropetry定义属性
__webpack_require__.d = function(exports, name, getter) {
	if(!__webpack_require__.o(exports, name)) {
		Object.defineProperty(exports, name, { enumerable: true, get: getter });
	}
};

__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;
};
实现

当模块含有import export define 时,在模块函数中

导出模块

当发现export关键字时调用

  • 定义方法(webpack_require.r),两个作用
    • 改变exports在调用Object.prototype.toString.call时的值 表示这是一个模块对象
    • 增加__esModule属性 且值为true 用于表示此模块对象是es模块
  • 定义方法(webpack_require.d),作用
    • export const b = 1;类似语法转为 在模块对象的exports对象上定义属性名为b,值为 1;
    • export default 导出值挂载在 模块对象的exports对象的default属性上
导入模块

当发现import关键字时

  • 调用__webpack_require__.r标明模块来源是es6
  • import xxx from 解析为从exports对象的default属性上取值

show the code(简化版打包结果,函数体相同,只是做了传入值的兼容转化,所以只加这块代码)

(function(modules){...})({
  "./src/index.js":
  (function(module, __webpack_exports__, __webpack_require__) {
      "use strict";
      // 将此模块标为es模块
      __webpack_require__.r(__webpack_exports__);
      var _title__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./title */ "./src/title.js");
      // 从default属性上取值
      console.log(_title__WEBPACK_IMPORTED_MODULE_0__["default"],_title__WEBPACK_IMPORTED_MODULE_0__["esB"]);
}),

  "./src/title.js":
  (function(module, __webpack_exports__, __webpack_require__) {
      "use strict";
      // 将此模块标为es模块
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, "esB", function() { return esB; });
      // 将`export default`导出值定义在导出对象的default属性上
      __webpack_exports__["default"] = ({
            esA: 1
      });
      const esB = 2;
})})
小测试
// foo.js
const bar = require('./bar.js');
console.log('value of bar:', bar);
module.exports = 'This is foo.js';

// bar.js
const foo = require('./foo.js');
console.log('value of foo:', foo);
module.exports = 'This is bar.js';

// index.js
require('./foo.js');

思考:会打印出什么

答案:

value of foo: undefined
value of bar: This is bar.js