webpack已经成为编译构建前端项目的必备工具。今天就来了解一下webpack的打包机制,先从最简单的js编译打包开始。
webpack打包后的文件
先来看下webpack打包之后的文件是什么样,然后再反推它的打包机制。补充说明:本文使用的是webpack 4.x的版本。
代码示例:
入口文件index.js,依赖a.js和b.js,而a.js和b.js都依赖c.js
代码如下:
// index.js
import {getDate} from "./a";
import {getDay} from "./b";
console.log(getDate() + ' ' + getDay());
// a.js
import now from './c';
export function getDate() {
var date = now();
var year = date.getFullYear();
var month = date.getMonth() + 1;
month = month > 9 ? month : `0${month}`;
var day = date.getDate();
day = day > 9 ? day : `0${day}`;
return `${year}-${month}-${day}`;
}
// b.js
import now from './c';
export function getDay() {
var date = now();
var arr = ['日', '一', '二', '三', '四', '五', '六'];
return `周${arr[date.getDay()]}`;
}
// c.js
export default function now() {
var date = new Date();
return date;
}
webpack打包后的bundle.js如下:
/******/ (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 = "./example/src/index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./example/src/a.js":
/*!**************************!*\
!*** ./example/src/a.js ***!
\**************************/
/*! exports provided: getDate */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"getDate\", function() { return getDate; });\n/* harmony import */ var _c__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./c */ \"./example/src/c.js\");\n\nfunction getDate() {\n var date = Object(_c__WEBPACK_IMPORTED_MODULE_0__[\"default\"])();\n var year = date.getFullYear();\n var month = date.getMonth() + 1;\n month = month > 9 ? month : `0${month}`;\n var day = date.getDate();\n day = day > 9 ? day : `0${day}`;\n return `${year}-${month}-${day}`;\n}\n\n\n//# sourceURL=webpack:///./example/src/a.js?");
/***/ }),
/***/ "./example/src/b.js":
/*!**************************!*\
!*** ./example/src/b.js ***!
\**************************/
/*! exports provided: getDay */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"getDay\", function() { return getDay; });\n/* harmony import */ var _c__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./c */ \"./example/src/c.js\");\n\nfunction getDay() {\n var date = Object(_c__WEBPACK_IMPORTED_MODULE_0__[\"default\"])();\n var arr = ['日', '一', '二', '三', '四', '五', '六'];\n return `周${arr[date.getDay()]}`;\n}\n\n\n//# sourceURL=webpack:///./example/src/b.js?");
/***/ }),
/***/ "./example/src/c.js":
/*!**************************!*\
!*** ./example/src/c.js ***!
\**************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return now; });\nfunction now() {\n var date = new Date();\n return date;\n}\n\n\n//# sourceURL=webpack:///./example/src/c.js?");
/***/ }),
/***/ "./example/src/index.js":
/*!******************************!*\
!*** ./example/src/index.js ***!
\******************************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./example/src/a.js\");\n/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ \"./example/src/b.js\");\n\n\nconsole.log(Object(_a__WEBPACK_IMPORTED_MODULE_0__[\"getDate\"])() + ' ' + Object(_b__WEBPACK_IMPORTED_MODULE_1__[\"getDay\"])());\n\n\n//# sourceURL=webpack:///./example/src/index.js?");
/***/ })
/******/ });
简单点,打包后的形式是这样的:
(function(modules) {
// ...
})({
// ...
})
其实就是自执行函数,传入的moduels对象是下面的形式:
{
"./example/src/a.js": (function(module, __webpack_exports__, __webpack_require__) {
// 模块代码
}),
"./example/src/b.js": (function(module, __webpack_exports__, __webpack_require__) {
// 模块代码
}),
"./example/src/c.js": (function(module, __webpack_exports__, __webpack_require__) {
// 模块代码
}),
"./example/src/index.js": (function(module, __webpack_exports__, __webpack_require__) {
// 模块代码
})
}
模块代码被编译成es5格式的代码,在执行时,自执行函数从入口文件开始执行
__webpack_require__(__webpack_require__.s = "./example/src/index.js");
再来看看__webpack_require__
函数的定义:
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;
/******/ }
函数返回的是module.exports
,对于已经存在于installedModules
中的模块,再次require
时,直接返回module.exports
,而不会执行。
根据webpack打包后的文件,可以得出打包的思路:
- 定义require函数
- 分析入口文件的依赖,打包成moduels形式的代码
- 生成自执行的bundle.js,从入口文件开始执行
开发简单的打包程序
将es6语法转换成es5
这一步通过babel来转换
- 通过
babylon
生成AST - 通过
babel-core
将AST重新生成源码
/**
* 获取文件,生成AST
* @param filename
*/
function getAst(filename) {
const content = fs.readFileSync(filename, 'utf-8');
return babylon.parse(content, {
sourceType: 'module'
});
}
/**
* 编译
* @param ast
*/
function getTranslateCode(ast) {
const { code } = babel.transformFromAst(ast, null, {
presets: ['env']
});
return code;
}
处理模块的依赖关系
通过babel-traverse遍历AST,找到模块的依赖
function getDependencies(ast) {
let dependencies = [];
traverse(ast, {
ImportDeclaration: ({node}) => {
dependencies.push(node.source.value);
}
});
return dependencies;
}
解析模块
function parse(filename, entry) {
let absolutePath = path.join(entry, filename + '.js');
const ast = getAst(absolutePath);
return {
filename,
dependence: getDependencies(ast), // 解析依赖
code: getTranslateCode(ast) // 编译成es5
};
}
解析深度依赖
此时的getDependencies还只是解析一个模块的依赖,但依赖的依赖没有解析,所以需要深度遍历
const modules = {};
/**
* 深度队列依赖
* @param main
*/
function getQueue(main) {
if (modules[main.filename]) {
return;
}
modules[main.filename] = main;
main.dependence.forEach(dep => {
let child = parse(dep, 'example/src');
getQueue(child);
});
}
bundle函数
function bundle(queue) {
let modules = '';
queue.forEach(mod => {
modules += `'${mod.filename}': function(require, module, exports) {${mod.code}},`
});
const result = `
(function(modules){
var installedModules = {};
function require(moduleId){
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var fn = modules[moduleId];
var module = installedModules[moduleId] = {exports: {}};
fn(require, module, module.exports);
return module.exports;
}
require('index');
})({${modules}})
`;
return result;
}
执行bundle后的输出
(function (modules) {
var installedModules = {};
function require(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var fn = modules[moduleId];
var module = installedModules[moduleId] = {exports: {}};
fn(require, module, module.exports);
return module.exports;
}
require('index');
})({
'index': function (require, module, exports) {
"use strict";
var _a = require("./a");
var _b = require("./b");
console.log((0, _a.getDate)() + ' ' + (0, _b.getDay)());
}, './a': function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getDate = getDate;
var _c = require("./c");
var _c2 = _interopRequireDefault(_c);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {default: obj};
}
function getDate() {
var date = (0, _c2.default)();
var year = date.getFullYear();
var month = date.getMonth() + 1;
month = month > 9 ? month : "0" + month;
var day = date.getDate();
day = day > 9 ? day : "0" + day;
return year + "-" + month + "-" + day;
}
}, './c': function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = now;
function now() {
var date = new Date();
return date;
}
}, './b': function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getDay = getDay;
var _c = require("./c");
var _c2 = _interopRequireDefault(_c);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {default: obj};
}
function getDay() {
var date = (0, _c2.default)();
var arr = ['日', '一', '二', '三', '四', '五', '六'];
return "\u5468" + arr[date.getDay()];
}
}
})
总结
这样,基本上处理js依赖和编译的工作算是完成了
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。