随着前端技术的飞速发展,前端开发也从静态页面发展到了web应用,简单的静态页面已经不能满足前端开发的需求。这时涌现了大量的工程化工具,例如早期的gulp,grunt等任务流工具,他们类似于使用javascript完成了shell的一些功能。这些工具虽然解决了大量静态文件的批处理问题,但本质上还是对静态文件的操作。
而webpack的出现将前端工程化提升到了一个新的高度,它可谓是将前端的模块化功能发挥到了极致,进而也成为了目前前端市场最火爆的打包工具。在这篇文章里,我们会简单了解一下webpack的打包原理以及进行一些简单的配置。相信看完成篇文章后,您一定会对webpack有着更深的了解。
webpack是一个现代javascript模块打包器,由于commonJS提出了js的模块化规范,webpack很好的利用了这一规范。奉行一切文件皆模块的原则,会依据入口文件递归的编译并打包所有依赖到的文件,并将这些文件打包成一个或多个bundle。
您也许会想webpack到底把文件打包成了什么?在实际工作中我们并不需要去关心这个问题,因为webpack已经帮我们做好了一切,并且webpack打包后的代码非常难以阅读。其实打包后的代码看起来复杂,但原理都是一样的,接下来通过一个最基础的小例子来分析一个webpack是如何打包我们的源文件的。
首先先初始化我们的项目:
mkdir webpack-bundle-analysis cd webpack-bundle-analysis npm init 或者 yarn init
接下来安装webpack依赖:
npm install --save-dev webpack webpack-cli 或者 yarn add --dev webpack webpack-cli
然后在根目录创建我们的源文件:
#index.js const { add } = require('./add'); add(1, 1); #add.js export function add (a, b) { return a + b; }
接下来就到了配置webpack的阶段了,在这里我们只需要最简单的webpack配置即可:
#webpack.config.js module.exports = { mode: 'development', entry: { index: './index.js', }, }
注意:这里一定要将mode设置为development,否则webpack会默认使用生产模式打包代码,使得代码难以阅读。
接下来在我们的package.json文件内填写如下几行:
"scripts": { "build": "webpack --config ./webpack.config.js", },
最后我们只需要执行 npm run build 或者yarn run build我们就会看见webpack已经帮我们把代码打包到了根目录的/dist/index.js文件内。
现在让我们来看一下webpack打包後的代码:
/******/ (function(modules) { // webpackBootstrap /******/ // webpack会在此处缓存已经加载过的模块,以防止模块的重复加载 /******/ var installedModules = {}; /******/ /******/ // webpack自定义的模块加载函数 /******/ function __webpack_require__(moduleId) { /******/ /******/ // 如果模块已经被缓存则使用缓存中的模块 /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // 创建一个模块并将该模块放入缓存中,注意:这里的exports属性为空对 象,webpack会在下面的执行模块方法是将mokuai导出的对象挂载到module.exports /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // 执行模块方法,并将模块导出对象挂载在module.exports /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // 标记模块已加载过 /******/ module.l = true; /******/ /******/ // 返回模块导出对象 /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // 这个方法很关键,我们可以在下面的./add.js模块中看到此方法,此方法便起到了挂载导出对象的作用 /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); /******/ } /******/ }; /******/ /******/ /******/ __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 = ""; /******/ /******/ /******/ // 加载入口模块并将导出模块返回 /******/ return __webpack_require__(__webpack_require__.s = "./index.js"); /******/ }) /************************************************************************/ /******/ ({ /***/ "./add.js": /*!****************!*\ !*** ./add.js ***! \****************/ /*! exports provided: add */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"add\", function() { return add; });\nfunction add (a, b) {\n return a + b;\n}\n\n//# sourceURL=webpack:///./add.js?"); /***/ }), /***/ "./index.js": /*!******************!*\ !*** ./index.js ***! \******************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { eval("const { add } = __webpack_require__(/*! ./add */ \"./add.js\");\n\nadd(1, 1);\n\n//# sourceURL=webpack:///./index.js?"); /***/ })
从上面代码我们可以看出webpack的打包结果实际上就是一个立即执行函数表达式(IIFE),参数是一个模块对象,对象的键值即module_id,实际上是模块相对根目录的相对路径,对象的值则是一个方法。我们可以看到这个IIFE最后执行了__webpack_require__(__webpack_require__.s = "./index.js")这个方法,并且传入根模块的module_id,这样便会调用IIFE内部定义的__webpack_require__方法,首先会从缓存变量installedModules中去取出这个模块exports出的对象,如果缓存中没有这个模块,便将模块加入缓存并执行。执行当前模块方法时会不断的加载重复以上步骤,即缓存中有就读缓存并执行,没有便执行后加入缓存。
前文我们简单分析了webpack打包後的代码,虽然看起来比较晦涩难懂,但当我们提取出代码的关键部分之后,一切困难也就瞬间迎刃而解了。但是,知其然也要知其所以然,我们也要了解webpack的如何编译代码的。接下来,我们将通过webpack的源码来逐步解析webpack的代码编译过程。
Webpack的编译过程采用的是一种事件流的模式,其内部使用Tapable来进行事件定义,事件注册与事件执行。Webpack内部同时也定义了大量的插件,这些插件都会注册在Tapable定义的事件上,在Webpack编译过程中会有序的触发这些事件,即执行不同的插件以满足webpack的编译需求。
Webpack的核心对象有两个:
Compiler:compiler可谓是webpack的核心对象,在webpack的编译开始会创建一个唯一的compiler,这个对象继承自Tapable类,以便定义事件,调用插件。
Compilation:compilation对象是在监听模式下启动时由compiler对象创建的,其内部也定义了大量的事件钩子以便插件调用。compilation对象可以监听文件的变化来进行重新编译并加载进内存,因此其可以访问所有的模块及其依赖。每次模块内容变动时都会创建一个新的compilation对象。在编译阶段,模块会被加载,封存,优化,分块,哈希和重新创建。
上面我们了解了webpack的核心对象,现在让我们来分析webpack的源码。通常阅读源码时我们需要从主模块来开始阅读,其主模块为/node_modules/webpack/lib/webpack.js文件。主模块的webpack方法内首先会检查我们的配置文件格式是否有误。当配置文件格式准确无误时,接下来便开始创建compiler对象
compiler = new Compiler(options.context);
在webpack方法的底部会判断是否由监听模式启动(通常我们会使用webpack-dev-server来启动监听模式),如果为监听模式,便会调用compiler对象的watch方法,反之便会调用run方法。
if ( options.watch === true || (Array.isArray(options) && options.some(o => o.watch)) ) { const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : options.watchOptions || {}; return compiler.watch(watchOptions, callback); } compiler.run(callback);
我们先不要急着离开这里。文件的下面导出了大量webpack内置的插件,这些插件内部会监听不同的事件。即上面所说的,webpack编译过程中会触发不同的事件,那是便是这些插件大显神通的时候了。
#compiler.js exportPlugins(exports, { AutomaticPrefetchPlugin: () => require("./AutomaticPrefetchPlugin"), BannerPlugin: () => require("./BannerPlugin"), ...
接下来让我们走进webpack的编译过程,这里以compiler.watch为例。执行compiler.watch方法,经过一系列的方法调用,会走到compiler对象的compile方法内。在这个方法里面或创建一个compilaton实例。
#compiler.js const compilation = this.newCompilation(params);
与此同时会触发compiler的compilation钩子,会触发loaderPlugin的执行,这时便会开始模块的编译过程
#compiler.js this.hooks.compilation.call(compilation, params); #LoaderPlugin.js compiler.hooks.compilation.tap("LoaderPlugin", compilation => {...
在这里我们要稍微暂停一下,因为模块的编译过程需要着重说一下。webpack会从配置文件的入口配置开始根据其依赖收集模块,收集模块的过程中会对不同文件类型使用不同的loader进行解析编译。在实际工作中我们通常会碰到各种类型的文件,如.css, .png等等,由于node只能解析.js(非es6),.json,.node类型的文件,所以我们通常需要向babel-loader,css-loader,file-loader等协助webpack完成文件的模块编译。
接下来,会触发compilation实例的normalModuleLoader钩子,最后会在normalModule实例的doBuild方法内完成模块的编译工作。
#NormalModule.js doBuild(options, compilation, resolver, fs, callback) {...
模块编译加载完成后会触发compilation实例的finishModules钩子,代表模块加载完毕。
最后调用seal方法,seal方法内部做的工作比较多,依次进行了模块的封存,优化,分块和哈希等工作。
上述过程全部结束之后会根据开发者的配置文件出口配置将输出文件打包到相应的输出目录。
webpack是一个及其庞大的项目,源码相对来说也非常复杂,这里只列出关键流程,帮助大家能够对webpack打包过程有一个比较清晰的认识。