本文抄自《深入淺出webpack》,建議想學習原理的手打一遍,操作一遍,給別人講一遍,然後就會了
在閱讀前希望您已有webpack相關的實務經驗,不然讀了也讀不懂
本文閱讀需要幾分鐘,理解需要自己動手操作蠻長時間
0 設定檔
首先簡單看一下webpack設定檔(webpack.config.js):
var path = require('path'); var node_modules = path.resolve(__dirname, 'node_modules'); var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js'); module.exports = { // 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。 entry: { bundle: [ 'webpack/hot/dev-server', 'webpack-dev-server/client?http://localhost:8080', path.resolve(__dirname, 'app/app.js') ] }, // 文件路径指向(可加快打包过程)。 resolve: { alias: { 'react': pathToReact } }, // 生成文件,是模块构建的终点,包括输出文件与输出路径。 output: { path: path.resolve(__dirname, 'build'), filename: '[name].js' }, // 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。 module: { loaders: [ { test: /\.js$/, loader: 'babel', query: { presets: ['es2015', 'react'] } } ], noParse: [pathToReact] }, // webpack 各插件对象,在 webpack 的事件流中执行对应的方法。 plugins: [ new webpack.HotModuleReplacementPlugin() ] };
1. 工作原理概述
1.1 基本概念
在了解webpack原理之前,需要掌握以下幾個核心概念
Entry: 入口,webpack建構第一步從entry開始
#module:模組,在webpack中一個模組對應一個檔案。 webpack會從entry開始,遞歸找出所有依賴的模組
Chunk:程式碼區塊,一個chunk由多個模組組合而成,用於程式碼合併與分割
Loader: 模組轉換器,用於將模組的原始內容按照需求轉換成新內容
Plugin:拓展插件,在webpack建置流程中的特定時機會廣播對應的事件,外掛可以監聽這些事件的發生,在特定的時機做對應的事情
webpack從啟動到結束依序執行以下操作:
graph TD 初始化参数 --> 开始编译 开始编译 -->确定入口 确定入口 --> 编译模块 编译模块 --> 完成编译模块 完成编译模块 --> 输出资源 输出资源 --> 输出完成
各個階段執行的操作如下:
#初始化參數:從設定檔(預設webpack.config.js)和shell語句中讀取與合併參數,得到最終的參數
開始編譯(compile):用上一步得到的參數初始化Comiler對象,載入所有配置的插件,透過執行對象的run方法開始執行編譯
確定入口:根據配置中的entry找出所有的入口檔案
編譯模組:從入口檔案出發,調用所有配置的Loader對模組進行翻譯,再找出該模組依賴的模組,再遞歸本步驟直到所有入口依賴的檔案都經過處理
完成編譯模組:經過第四步之後,得到了每個模組被翻譯之後的最終內容以及他們之間的依賴關係
#輸出資源:根據入口和模組之間的依賴關係,組裝成一個個包含多個模組的chunk,再將每個chunk轉換成一個單獨的檔案加入輸出清單中,這是可以修改輸出內容的最後機會
輸出完成:在確定好輸出內容後,根據配置(webpack.config.js && shell)決定輸出的路徑與檔名,將檔案的內容寫入檔案系統(fs)
在上述過程中,webpack會在特定的時間點廣播特定的事件,外掛程式監聽事件並執行對應的邏輯,而外掛程式可以呼叫webpack提供的api改變webpack的運行結果
#webpack建置流程可分為以下三大階段。
初始化:啟動構建,讀取與合併配置參數,載入plugin,實例化Compiler
編譯:從Entry出發,針對每個Module串列呼叫對應的Loader去翻譯檔案中的內容,再找到該Module依賴的Module,遞歸的進行編譯處理
輸出:將編譯後的Module組合成Chunk ,將Chunk轉換成文件,輸出到文件系統中
如果只執行一次,流程如上,但在開啟監聽模式下,流程如下圖
graph TD 初始化-->编译; 编译-->输出; 输出-->文本发生变化 文本发生变化-->编译
在初始化階段會發生的事件如下
#描述 | |
---|---|
初始化參數 | 從設定檔和shell語句中讀取與合併參數,得到最終的參數,這個過程也會執行設定檔中的插件實例化語句new Plugin() |
實例化Compiler | 實例化Compiler,傳入上一個步驟所得到的參數,Compiler負責檔案監聽和啟動編譯。在Compiler實例中包含了完整的webpack配置,全域只有一個Compiler實例。 |
載入外掛程式 | 依序呼叫外掛程式的apply方法,讓外掛程式可以監聽後續的所有事件節點。同時向插件中傳入compiler實例的引用,以方便插件透過compiler呼叫webpack的api |
environment | 開始應用Node.js風格的檔案系統到compiler對象,以方便後續的檔案尋找和讀取 |
Entry-option | 讀取配置的Entrys,為每個Entry實例化一個對應的EntryPlugin,為後面該Entry的遞歸解析工作做準備 |
#After-plugins | 調用完所有內建的和配置的插件的apply方法 |
After-resolvers | 根據配置初始化resolver,resolver負責在檔案系統中尋找指定路徑的檔案 |
事件 | 解釋 |
---|---|
run | 啟動一次編譯 |
#Watch-run | 在監聽模式下啟動編譯,檔案發生變更會重新編譯 |
compile | 告訴外掛程式一次新的編譯將要啟動,同時會給外掛程式帶上compiler物件 |
compilation | 當webpack以開發模式運作時,每當偵測到檔案的變化,就會有一次新的compilation被建立。一個Compilation物件包含了目前的模組資源、編譯產生資源、變化的檔案等。 compilation物件也提供了許多事件回調給外掛程式進行拓展 |
make | 一個新的compilation物件建立完畢,即將從entry開始讀取檔案,根據檔案類型和編譯的loader對檔案進行==編譯==,編譯完後再找出該檔案所依賴的檔案,遞歸地編譯和解析 |
after-compile | |
解釋 | |
build-module | 使用對應的Loader去轉換一個模組 |
#Normal-module-loader | 在使用loader轉換完一個模組後,使用acorn解析轉換後的內容,輸出對應的抽象語法樹(AST),以方便webpack對程式碼進行分析 |
program | 從設定的入口模組開始,分析其AST,當遇到require等導入其他模組的語句時,便將其加入依賴的模組列表中,同時對於新找出來的模組遞歸分析,最終弄清楚所有模組的依賴關係 |
解釋 | |
#should-emit | 所有需要輸出的檔案已經產生,詢問插件有哪些檔案需要輸出,有哪些不需要輸出 |
emit | 確定好要輸出哪些檔案後,執行檔輸出,==可以在這裡取得和修改輸出的內容== |
after-mit | 檔案輸出完畢 |
在输出阶段已经得到了各个模块经过转化后的结果和其依赖关系,并且将相应的模块组合在一起形成一个个chunk.在输出阶段根据chunk的类型,使用对应的模板生成最终要输出的文件内容. |
//以下代码用来包含webpack运行过程中的每个阶段 //file:webpack.config.js const path = require('path'); //插件监听事件并执行相应的逻辑 class TestPlugin { constructor() { console.log('@plugin constructor'); } apply(compiler) { console.log('@plugin apply'); compiler.plugin('environment', (options) => { console.log('@environment'); }); compiler.plugin('after-environment', (options) => { console.log('@after-environment'); }); compiler.plugin('entry-option', (options) => { console.log('@entry-option'); }); compiler.plugin('after-plugins', (options) => { console.log('@after-plugins'); }); compiler.plugin('after-resolvers', (options) => { console.log('@after-resolvers'); }); compiler.plugin('before-run', (options, callback) => { console.log('@before-run'); callback(); }); compiler.plugin('run', (options, callback) => { console.log('@run'); callback(); }); compiler.plugin('watch-run', (options, callback) => { console.log('@watch-run'); callback(); }); compiler.plugin('normal-module-factory', (options) => { console.log('@normal-module-factory'); }); compiler.plugin('context-module-factory', (options) => { console.log('@context-module-factory'); }); compiler.plugin('before-compile', (options, callback) => { console.log('@before-compile'); callback(); }); compiler.plugin('compile', (options) => { console.log('@compile'); }); compiler.plugin('this-compilation', (options) => { console.log('@this-compilation'); }); compiler.plugin('compilation', (options) => { console.log('@compilation'); }); compiler.plugin('make', (options, callback) => { console.log('@make'); callback(); }); compiler.plugin('compilation', (compilation) => { compilation.plugin('build-module', (options) => { console.log('@build-module'); }); compilation.plugin('normal-module-loader', (options) => { console.log('@normal-module-loader'); }); compilation.plugin('program', (options, callback) => { console.log('@program'); callback(); }); compilation.plugin('seal', (options) => { console.log('@seal'); }); }); compiler.plugin('after-compile', (options, callback) => { console.log('@after-compile'); callback(); }); compiler.plugin('should-emit', (options) => { console.log('@should-emit'); }); compiler.plugin('emit', (options, callback) => { console.log('@emit'); callback(); }); compiler.plugin('after-emit', (options, callback) => { console.log('@after-emit'); callback(); }); compiler.plugin('done', (options) => { console.log('@done'); }); compiler.plugin('failed', (options, callback) => { console.log('@failed'); callback(); }); compiler.plugin('invalid', (options) => { console.log('@invalid'); }); } }
#在目录下执行 webpack #输出以下内容 @plugin constructor @plugin apply @environment @after-environment @entry-option @after-plugins @after-resolvers @before-run @run @normal-module-factory @context-module-factory @before-compile @compile @this-compilation @compilation @make @build-module @normal-module-loader @build-module @normal-module-loader @seal @after-compile @should-emit @emit @after-emit @done Hash: 19ef3b418517e78b5286 Version: webpack 3.11.0 Time: 95ms Asset Size Chunks Chunk Names bundle.js 3.03 kB 0 [emitted] main [0] ./main.js 44 bytes {0} [built] [1] ./show.js 114 bytes {0} [built]
下面通过 Webpack 构建一个采用 CommonJS 模块化编写的项目,该项目有个网页会通过 JavaScript 在网页中显示 Hello,Webpack
。
运行构建前,先把要完成该功能的最基础的 JavaScript 文件和 HTML 建立好,需要如下文件:
页面入口文件 index.html
<meta> <p></p> <!--导入 Webpack 输出的 JavaScript 文件--> <script></script>
JS 工具函数文件 show.js
// 操作 DOM 元素,把 content 显示到网页上 function show(content) { window.document.getElementById('app').innerText = 'Hello,' + content; } // 通过 CommonJS 规范导出 show 函数 module.exports = show;
JS 执行入口文件 main.js
// 通过 CommonJS 规范导入 show 函数 const show = require('./show.js'); // 执行 show 函数 show('Webpack');
Webpack 在执行构建时默认会从项目根目录下的 webpack.config.js
文件读取配置,所以你还需要新建它,其内容如下:
const path = require('path'); module.exports = { // JavaScript 执行入口文件 entry: './main.js', output: { // 把所有依赖的模块合并输出到一个 bundle.js 文件 filename: 'bundle.js', // 输出文件都放到 dist 目录下 path: path.resolve(__dirname, './dist'), } };
由于 Webpack 构建运行在 Node.js 环境下,所以该文件最后需要通过 CommonJS 规范导出一个描述如何构建的 Object
对象。
|-- index.html |-- main.js |-- show.js |-- webpack.config.js
一切文件就绪,在项目根目录下执行 webpack
命令运行 Webpack 构建,你会发现目录下多出一个 dist
目录,里面有个 bundle.js
文件, bundle.js
文件是一个可执行的 JavaScript 文件,它包含页面所依赖的两个模块 main.js
和 show.js
及内置的 webpackBootstrap
启动函数。 这时你用浏览器打开 index.html
网页将会看到 Hello,Webpack
。
看之前记住:一个模块就是一个文件,
首先看下bundle.js长什么样子:
注意:序号1处是个自执行函数,序号2作为自执行函数的参数传入
具体代码如下:(建议把以下代码放入编辑器中查看,最好让index.html执行下,弄清楚执行的顺序)
(function(modules) { // webpackBootstrap // 1. 缓存模块 var installedModules = {}; // 2. 定义可以在浏览器使用的require函数 function __webpack_require__(moduleId) { // 2.1检查模块是否在缓存里,在的话直接返回 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // 2.2 模块不在缓存里,新建一个对象module=installModules[moduleId] {i:moduleId,l:模块是否加载,exports:模块返回值} var module = installedModules[moduleId] = { i: moduleId,//第一次执行为0 l: false, exports: {} };//第一次执行module:{i:0,l:false,exports:{}} // 2.3 执行传入的参数中对应id的模块 第一次执行数组中传入的第一个参数 //modules[0].call({},{i:0,l:false,exports:{}},{},__webpack_require__函数) modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 2.4 将这个模块标记为已加载 module.l = true; // 2.5 返回这个模块的导出值 return module.exports; } // 3. webpack暴露属性 m c d n o p __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 = ""; // 4. 执行reruire函数引入第一个模块(main.js对应的模块) return __webpack_require__(__webpack_require__.s = 0); }) ([ // 0. 传入参数,参数是个数组 /* 第0个参数 main.js对应的文件*/ (function(module, exports, __webpack_require__) { // 通过 CommonJS 规范导入 show 函数 const show = __webpack_require__(1);//__webpack_require__(1)返回show // 执行 show 函数 show('Webpack'); }), /* 第1个参数 show.js对应的文件 */ (function(module, exports) { // 操作 DOM 元素,把 content 显示到网页上 function show(content) { window.document.getElementById('app').innerText = 'Hello,' + content; } // 通过 CommonJS 规范导出 show 函数 module.exports = show; }) ]);
以上看上去复杂的代码其实是一个自执行函数(文件作为自执行函数的参数),可以简写如下:
(function(modules){ //模拟require语句 function __webpack_require__(){} //执行存放所有模块数组中的第0个模块(main.js) __webpack_require_[0] })([/*存放所有模块的数组*/])
bundles.js能直接在浏览器中运行的原因是,在输出的文件中通过__webpack_require__
函数,定义了一个可以在浏览器中执行的加载函数(加载文件使用ajax实现),来模拟Node.js中的require语句。
原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。
修改main.js,改成import引入模块
import show from './show'; show('Webpack');
在目录下执行webpack
,会发现:
生成的代码会有所不同,但是主要的区别是自执行函数的参数不同,也就是2.2代码的第二部分不同
([//自执行函数和上面相同,参数不同 /* 0 */ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__show__ = __webpack_require__(1); Object(__WEBPACK_IMPORTED_MODULE_0__show__["a" /* default */])('Webpack'); }), /* 1 */ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; /* harmony export (immutable) */ __webpack_exports__["a"] = show; function show(content) { window.document.getElementById('app').innerText = 'Hello,' + content; } }) ]);
参数不同的原因是es6的import和export模块被webpack编译处理过了,其实作用是一样的,接下来看一下在main.js中异步加载模块时,bundle.js是怎样的
main.js
修改如下
import('./show').then(show=>{ show('Webpack') })
构建成功后会生成两个文件
bundle.js 执行入口文件
0.bundle.js 异步加载文件
其中0.bundle.js文件的内容如下:
webpackJsonp(/*在其他文件中存放的模块的ID*/[0],[//本文件所包含的模块 /* 0 */, /* 1 show.js对应的模块 */ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony export (immutable) */ __webpack_exports__["default"] = show; function show(content) { window.document.getElementById('app').innerText = 'Hello,' + content; } }) ]);
bundle.js文件的内容如下:
注意:bundle.js比上面的bundle.js的区别在于:
多了一个__webpack_require__.e
,用于加载被分割出去的需要异步加载的chunk对应的文件
多了一个webpackJsonp函数,用于从异步加载的文件中安装模块
(function(modules) { // webpackBootstrap // install a JSONP callback for chunk loading var parentJsonpFunction = window["webpackJsonp"]; // webpackJsonp用于从异步加载的文件中安装模块 // 将webpackJsonp挂载到全局是为了方便在其他文件中调用 /** * @param chunkIds 异步加载的模块中需要安装的模块对应的id * @param moreModules 异步加载的模块中需要安装模块列表 * @param executeModules 异步加载的模块安装成功后需要执行的模块对应的index */ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { // add "moreModules" to the modules object, // then flag all "chunkIds" as loaded and fire callback var moduleId, chunkId, i = 0, resolves = [], result; for(;i { show('Webpack') }) /***/ }) ]);
以上是webpack原理的深入介紹(附範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!