【手把手带你配 webpack】第二步, 面试官-谈谈你对模块化的理解
尴尬的面试题
问: 你知道的 js 模块化方案有哪些? 答: js 的模块化一般来说有 Commonjs AMD(CMD) ESModule 等, Commonjs 使用的同步加载一般用于 node, AMD(CMD) 由于是异步加载所以普遍用在前端. ESModule 旨在实现前后端模块化的统一. 言简意骇, 点到为止. 简直优秀...

问: 那么为什么同步的 Commonjs 要用在 node, 异步的 AMD 要用在前端呢? 答: node 应用程序运行在服务器上, 程序通过文件系统可以直接读取到各个模块的文件, 特点是响应快速不会因为同步而阻塞了程序的运行. 前端项目运行在浏览器中, 每个模块都要通过 http 请求加载 js 文件, 受到网络等因素的影响如果同步的话会使得浏览器出现"假死"的情况 --- 也就是卡住了. 影响用户体验. 整场邂逅已经完全 cover 住了有没有 ^_^.


问: 好的, 既然你说 Commonjs 的同步方式前端不适用, 为什么 webpack 转码的 Commonjs 形式以后可以前端运行呢? 答: emmmmm
面试官: 那要不今天的面试就先到这儿吧, 你先回去等通知... 我: hmmmmm

webpack 到底做了什么
一遍又一遍的执行了 webpack 命令, 一次又一次的研究生成的 bundle.js. 嗯哼? 所有的 js 模块都打包到了一个 bundle.js 里了, 根本没有分模块加载, 那还管他丫的同步还是异步. js 文件直接读取到内存里一步到位, 比 nodejs 需要读取文件还要方便...
那么, webpack 到底是怎样把这么多文件组合成一个 bundle.js 文件的呢? 仔细想一下, 还是把天书和大家一起学习一下吧, 要不感觉这个系列的文章会变成表情包分享教程! 2019-01-25-16-16-47
接下来我会分享 Commonjs 和 es6 Module 两种模块化方式 webpack 的处理, 如果有小伙伴对这两种方式有疑问, 请 google 一下. 其实所谓的模块化无非就是给一段代码添加了两把钥匙实现和其他代码的通信, 相互调用, 通过 require 获取引入其他模块的能力, 通过 export 实现向其他模块输出方法的功能.
commonjs 模块化的处理
首先添加一个 npm script
修改 package.json 如下图:

添加完成后代码, 就可以抛弃 ./node_modules/.bin/webpack
这个命令了, 打包时只需要 npm run build
即可.

搭建项目代码, 代码地址, 其中, index.js 内容:
const foo = require('./foo');
console.log(foo);
console.log('我是高级前端工程师~');
foo.js 内容:
module.exports = {
name: 'quanquan',
job: 'fe',
};
纯纯粹粹的 Commonjs 代码, 执行 npm run build
后, 我们再看看打包后的 bundle.js. (ps: 添加了部分注释)
(function(modules) {
// 缓存模块对象
var installedModules = {};
// 模拟 commonjs 实现的 require
function __webpack_require__(moduleId) {
// require 模块时先判断是否已经缓存, 已经缓存的模块直接返回
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// (模拟)创建一个模块, 并把新模块的引用保存到缓存中
var module = installedModules[moduleId] = {
// 模块 id
i: moduleId,
// 模块是否已加载
l: false,
// 模块主体内容, 会被重写
exports: {}
};
// 执行以下模块的包装函数, 并把模块内部的 this 志向模块主体
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 将模块标记为已加载
module.l = true;
// 返回模块主体内容
return module.exports;
}
// 向外暴露所有的模块
__webpack_require__.m = modules;
// 向外暴露已缓存的模块
__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 这个就没啥好解释的啦
// js 权威指南上有说
__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
// 准备工作做完了, require 一下入口模块, 让项目跑起来
return __webpack_require__(__webpack_require__.s = 0);
})
/******** 华丽的分割线 上边时 webpack 初始化代码, 下边是我们写的模块代码 ***************/
([
/* 模块 0 对应 index.js */
/***/ (function(module, exports, __webpack_require__) {
const foo = __webpack_require__(1);
console.log(foo);
console.log('我是高级前端工程师~');
/***/ }),
/* 模块 1 对应 foo.js */
/***/ (function(module, exports) {
module.exports = {
name: 'quanquan',
job: 'fe',
};
/***/ })
]);
原来 webpack 就是把我们写的代码用一个一个的包装函数包装了起来, 再执行__webpack_require__的时候调用一下包装函数. 通过包装函数内部的代码重写了参数中参数 module 的 exports 属性, 获取到我们编写的模块的主体代码.
所以我们看到了 index.js
包装后的代码为:
function(module, exports, __webpack_require__) {
const foo = __webpack_require__(1);
console.log(foo);
console.log('我是高级前端工程师~');
}
foo.js
包装后的代码为:
function(module, exports) {
module.exports = {
name: 'quanquan',
job: 'fe',
};
}
由于 index.js 中有 require('./foo')
所以 index.js 生成的包装函数参数中多了__webpack_require__用于导入 foo 模块, 从头到尾看下来, 之前的天书似乎不再晦涩难懂了. 当然这一块内容需要大家稍微动动脑筋思考一下. 也希望大家把自己的想法和问题放到评论区, 我们一起讨论.

es6 Module 模块化的处理
改动我们的项目代码为 es6 模块化代码, 首先修改 index.js
:
import foo from './foo';
console.log(foo);
console.log('我是高级前端工程师~');
其次修改 foo.js
export default {
name: 'quanquan',
job: 'fe',
};
最后就是命令行执行 npm run build
进行打包啦. 打包完成后 bundle.js
内容如下(经过优化):
(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;
}
__webpack_require__.m = modules;
__webpack_require__.c = installedModules;
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
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;
};
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
__webpack_require__.p = "";
return __webpack_require__(__webpack_require__.s = 0);
})([
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__foo__ = __webpack_require__(1);
console.log(__WEBPACK_IMPORTED_MODULE_0__foo__["a"]);
console.log('我是高级前端工程师~');
},
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_exports__["a"] = ({
name: 'quanquan',
job: 'fe',
});
}
]);
打包的结果仍然是天书式的代码, 还好和 commonjs 模块化方式大同小异. webpack 初始化代码完成相同.
index.js
生成的包裹代码为:
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__foo__ = __webpack_require__(1);
console.log(__WEBPACK_IMPORTED_MODULE_0__foo__["a"]);
console.log('我是高级前端工程师~');
}
foo.js
生成的包裹代码为:
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_exports__["a"] = ({
name: 'quanquan',
job: 'fe',
});
}
不难发现, 和 commonjs 不同的地方:
- 首先, 包装函数的参数之前的
module.exports
变成了__webpack_exports__
- 其次, 在使用了 es6 模块导入语法(import)的地方, 给
__webpack_exports__
添加了属性__esModule
- 其余的部分和 commonjs 类似
我们发现, commonjs 中咩有用到的 __webpack_require__.n
这个方法还是没有用到, 这难道是废方法? 那岂不是可以给 webpack 提个 issue, 参与过得开源项目不再是空白. 美滋滋...

要是这样都能搭上这种国际开源项目的班车, 岂不是太过容易了呢?

es6 Module CommonJs 混合使用
修改 foo.js
内容如下:
module.exports = {
name: 'quanquan',
job: 'fe',
};
打包的结果为:
/******/ (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, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__foo__ = __webpack_require__(1);
// 这里用到了 __webpack_require__.n
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__foo___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__foo__);
console.log(__WEBPACK_IMPORTED_MODULE_0__foo___default.a);
console.log('我是高级前端工程师~');
/* harmony default export */ __webpack_exports__["default"] = ({});
/***/ }),
/* 1 */
/***/ (function(module, exports) {
module.exports = {
name: 'quanquan',
job: 'fe',
};
/***/ })
/******/ ]);
index.js
没有变化, 也就是说当导出模块导出的语法为 commonjs 而导入模块的导入语法为 es6 时, 导入模块就会用到了 __webpack_require__.n
这个方法.
下集预告: 到这里一个纯 js 文件的打包结果就分析完了, webpack 巧妙的把所有的模块都打包到了一个 bundle.js 文件中从而实现同步加载模块, 实在高明. 但是, 如果你的项目很大, 动辄成百上千的 js 模块, 此时运用 webpack 又能怎样实现代码拆分呢? 我们下一步一起讨论.

转载自:https://juejin.cn/post/6844903768564334605