管理 Webpack 依賴項的指南
已發表: 2022-03-11模塊化的概念是大多數現代編程語言的固有部分。 然而,在最新版本的 ECMAScript ES6 到來之前,JavaScript 一直缺乏任何正式的模塊化方法。
在當今最流行的 JavaScript 框架之一 Node.js 中,模塊捆綁器允許在 Web 瀏覽器中加載 NPM 模塊,面向組件的庫(如 React)鼓勵和促進 JavaScript 代碼的模塊化。
Webpack 是可用的模塊捆綁器之一,可將 JavaScript 代碼以及所有靜態資產(例如樣式表、圖像和字體)處理到捆綁文件中。 處理可以包括管理和優化代碼依賴關係的所有必要任務,例如編譯、連接、縮小和壓縮。
但是,配置 Webpack 及其依賴項可能會帶來壓力,而且並不總是一個簡單的過程,尤其是對於初學者而言。
這篇博文提供瞭如何為不同場景配置 Webpack 的指南和示例,並指出了與使用 Webpack 捆綁項目依賴項相關的最常見缺陷。
這篇博文的第一部分解釋瞭如何簡化項目中依賴項的定義。 接下來,我們討論和演示多頁和單頁應用程序的代碼拆分配置。 最後,我們討論如何配置 Webpack,如果我們想在我們的項目中包含第三方庫。
配置別名和相對路徑
相對路徑與依賴關係不直接相關,但我們在定義依賴關係時會用到它們。 如果項目文件結構複雜,則可能很難解析相關的模塊路徑。 Webpack 配置最基本的好處之一是它有助於簡化項目中相對路徑的定義。
假設我們有以下項目結構:
- Project - node_modules - bower_modules - src - script - components - Modal.js - Navigation.js - containers - Home.js - Admin.js
我們可以通過我們需要的文件的相對路徑來引用依賴,如果我們想在我們的源代碼中將組件導入到容器中,它看起來像下面這樣:
Home.js
Import Modal from '../components/Modal'; Import Navigation from '../components/Navigation';
Modal.js
import {datepicker} from '../../../../bower_modules/datepicker/dist/js/datepicker';
每次我們想要導入腳本或模塊時,我們都需要知道當前目錄的位置,並找到我們想要導入的內容的相對路徑。 如果我們有一個具有嵌套文件結構的大項目,或者我們想要重構複雜項目結構的某些部分,我們可以想像這個問題會如何變得複雜。
我們可以使用 Webpack 的resolve.alias
選項輕鬆處理這個問題。 我們可以聲明所謂的別名——目錄或模塊的名稱及其位置,我們不依賴項目源代碼中的相對路徑。
webpack.config.js
resolve: { alias: { 'node_modules': path.join(__dirname, 'node_modules'), 'bower_modules': path.join(__dirname, 'bower_modules'), } }
在Modal.js
文件中,我們現在可以更簡單地導入 datepicker:
import {datepicker} from 'bower_modules/datepicker/dist/js/datepicker';
代碼拆分
我們可能會遇到需要將腳本附加到最終包中,或拆分最終包,或者我們希望按需加載單獨的包的場景。 為這些場景設置我們的項目和 Webpack 配置可能並不簡單。
在 Webpack 配置中, Entry
選項告訴 Webpack 最終包的起點在哪裡。 入口點可以具有三種不同的數據類型:字符串、數組或對象。
如果我們有一個單一的起點,我們可以使用這些格式中的任何一種並獲得相同的結果。
如果我們想追加多個文件,並且它們不相互依賴,我們可以使用數組格式。 例如,我們可以將analytics.js
附加到bundle.js
的末尾:
webpack.config.js
module.exports = { // creates a bundle out of index.js and then append analytics.js entry: ['./src/script/index.jsx', './src/script/analytics.js'], output: { path: './build', filename: bundle.js ' } };
管理多個入口點
假設我們有一個包含多個 HTML 文件的多頁面應用程序,例如index.html
和admin.html
。 我們可以通過將入口點用作 Object 類型來生成多個捆綁包。 下面的配置會生成兩個 JavaScript 包:
webpack.config.js
module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) } };
index.html
<script src=”build/index.js”></script>
admin.html
<script src=”build/admin.js”></script>
兩個 JavaScript 包都可以共享公共庫和組件。 為此,我們可以使用CommonsChunkPlugin
,它會查找出現在多個條目塊中的模塊,並創建一個可以在多個頁面之間緩存的共享包。
webpack.config.js
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js'); module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) }, plugins: [commonsPlugin] };
現在,我們一定不要忘記在捆綁腳本之前添加<script src="build/common.js"></script>
。
啟用延遲加載
Webpack 可以將靜態資源拆分成更小的塊,這種方法比標準串聯更靈活。 如果我們有一個大的單頁應用程序 (SPA),簡單地連接到一個包中並不是一個好方法,因為加載一個巨大的包可能會很慢,而且用戶通常不需要每個視圖上的所有依賴項。
我們之前解釋瞭如何將應用程序拆分為多個包、連接公共依賴項以及從瀏覽器緩存行為中受益。 這種方法非常適用於多頁應用程序,但不適用於單頁應用程序。
對於 SPA,我們應該只提供渲染當前視圖所需的那些靜態資產。 SPA 架構中的客戶端路由器是處理代碼拆分的理想場所。 當用戶輸入路由時,我們可以只加載結果視圖所需的依賴項。 或者,我們可以在用戶向下滾動頁面時加載依賴項。
為此,我們可以使用 webpack 可以靜態檢測的require.ensure
或System.import
函數。 Webpack 可以根據這個拆分點生成一個單獨的 bundle,按需調用。
在這個例子中,我們有兩個 React 容器; 管理視圖和儀表板視圖。
admin.jsx
import React, {Component} from 'react'; export default class Admin extends Component { render() { return <div > Admin < /div>; } }
dashboard.jsx
import React, {Component} from 'react'; export default class Dashboard extends Component { render() { return <div > Dashboard < /div>; } }
如果用戶輸入/dashboard
或/admin
URL,則僅加載相應的所需 JavaScript 包。 下面我們可以看到有和沒有客戶端路由器的例子。
index.jsx
if (window.location.pathname === '/dashboard') { require.ensure([], function() { require('./containers/dashboard').default; }); } else if (window.location.pathname === '/admin') { require.ensure([], function() { require('./containers/admin').default; }); }
index.jsx
ReactDOM.render( <Router> <Route path="/" component={props => <div>{props.children}</div>}> <IndexRoute component={Home} /> <Route path="dashboard" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/dashboard').default) }, "dashboard")}} /> <Route path="admin" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/admin').default) }, "admin")}} /> </Route> </Router> , document.getElementById('content') );
將樣式提取到單獨的包中
在 Webpack 中,加載器(如style-loader
和css-loader
)會預處理樣式表並將它們嵌入到輸出的 JavaScript 包中,但在某些情況下,它們會導致無樣式內容 (FOUC) 的 Flash 。

我們可以避免使用 ExtractTextWebpackPlugin 的ExtractTextWebpackPlugin
,它允許將所有樣式生成到單獨的 CSS 包中,而不是將它們嵌入到最終的 JavaScript 包中。
webpack.config.js
var ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { module: { loaders: [{ test: /\.css/, loader: ExtractTextPlugin.extract('style', 'css')' }], }, plugins: [ // output extracted CSS to a file new ExtractTextPlugin('[name].[chunkhash].css') ] }
處理第三方庫和插件
很多時候,我們需要使用第三方庫、各種插件或附加腳本,因為我們不想花時間從頭開始開發相同的組件。 有許多可用的遺留庫和插件沒有得到積極維護,不理解 JavaScript 模塊,並假設在預定義名稱下全局存在依賴項。
下面是一些 jQuery 插件的示例,解釋瞭如何正確配置 Webpack 以生成最終的包。
提供插件
大多數第三方插件依賴於特定全局依賴項的存在。 對於 jQuery,插件依賴於定義的$
或jQuery
變量,我們可以通過在代碼中調用$('div.content').pluginFunc()
來使用 jQuery 插件。
我們可以使用 Webpack 插件ProvidePlugin
在每次遇到全局$
標識符時添加var $ = require("jquery")
。
webpack.config.js
webpack.ProvidePlugin({ '$': 'jquery', })
當 Webpack 處理代碼時,它會查找存在$
,並提供對全局依賴項的引用,而無需導入require
函數指定的模塊。
進口裝載機
一些 jQuery 插件在全局命名空間中假設$
或依賴this
作為window
對象。 為此,我們可以使用imports-loader
將全局變量注入模塊。
example.js
$('div.content').pluginFunc();
然後,我們可以通過配置imports-loader
將$
變量注入到模塊中:
require("imports?$=jquery!./example.js");
這只是在前面加上var $ = require("jquery");
到example.js
。
在第二個用例中:
webpack.config.js
module: { loaders: [{ test: /jquery-plugin/, loader: 'imports?jQuery=jquery,$=jquery,this=>window' }] }
通過使用=>
符號(不要與 ES6 箭頭函數混淆),我們可以設置任意變量。 最後一個值將全局變量this
重新定義為指向window
對象。 它與用(function () { ... }).call(window);
包裝文件的全部內容相同。 並以window
作為參數調用this
函數。
我們還可以要求使用 CommonJS 或 AMD 模塊格式的庫:
// CommonJS var $ = require("jquery"); // jquery is available // AMD define(['jquery'], function($) { // jquery is available });
一些庫和模塊可以支持不同的模塊格式。
在下一個示例中,我們有一個使用 AMD 和 CommonJS 模塊格式並具有 jQuery 依賴項的 jQuery 插件:
jquery-plugin.js
(function(factory) { if (typeof define === 'function' && define.amd) { // AMD format is used define(['jquery'], factory); } else if (typeof exports === 'object') { // CommonJS format is used module.exports = factory(require('jquery')); } else { // Neither AMD nor CommonJS used. Use global variables. } });
webpack.config.js
module: { loaders: [{ test: /jquery-plugin/, loader: "imports?define=>false,exports=>false" }] }
我們可以選擇要用於特定庫的模塊格式。 如果我們聲明define
等於false
,Webpack 不會解析 AMD 模塊格式的模塊,如果我們聲明變量 export 等於false
, exports
不會解析 CommonJS 模塊格式的模塊。
暴露裝載機
如果我們需要將一個模塊暴露給全局上下文,我們可以使用expose-loader
。 這可能很有幫助,例如,如果我們有不屬於 Webpack 配置的外部腳本並且依賴於全局命名空間中的符號,或者我們使用需要訪問瀏覽器控制台中的符號的瀏覽器插件。
webpack.config.js
module: { loaders: [ test: require.resolve('jquery'), loader: 'expose-loader?jQuery!expose-loader?$' ] }
jQuery 庫現在可以在全局命名空間中用於網頁上的其他腳本。
window.$ window.jQuery
配置外部依賴
如果我們想包含來自外部託管腳本的模塊,我們需要在配置中定義它們。 否則,Webpack 無法生成最終的包。
我們可以使用 Webpack 配置中的externals
選項來配置外部腳本。 例如,我們可以通過單獨的<script>
標籤使用 CDN 中的庫,同時在我們的項目中仍顯式聲明它為模塊依賴項。
webpack.config.js
externals: { react: 'React', 'react-dom': 'ReactDOM' }
支持庫的多個實例
在前端開發中使用 NPM 包管理器來管理第三方庫和依賴項非常棒。 但是,有時我們可以擁有具有不同版本的同一個庫的多個實例,並且它們不能在一個環境中很好地協同工作。
例如,這可能發生在 React 庫中,我們可以在其中從 NPM 安裝 React,稍後可以通過一些額外的包或插件獲得不同版本的 React。 我們的項目結構可能如下所示:
project | |-- node_modules | |-- react |-- react-plugin | |--node_modules | |--react
來自react-plugin
的組件與項目中的其他組件具有不同的 React 實例。 現在我們有兩個獨立的 React 副本,它們可以是不同的版本。 在我們的應用程序中,這種情況可能會弄亂我們的全局可變 DOM,並且我們可以在 Web 控制台日誌中看到錯誤消息。 這個問題的解決方案是在整個項目中使用相同版本的 React。 我們可以通過 Webpack 別名來解決它。
webpack.config.js
module.exports = { resolve: { alias: { 'react': path.join(__dirname, './node_modules/react'), 'react/addons': path.join(__dirname, '/node_modules/react/addons'), } } }
當react-plugin
嘗試 require React 時,它使用項目的node_modules
中的版本。 如果我們想知道我們使用的是哪個版本的 React,我們可以在源代碼中添加console.log(React.version)
。
專注於開發,而不是 Webpack 配置
這篇文章只是觸及了 Webpack 的強大功能和實用性的表面。
還有許多其他 Webpack 加載器和插件可以幫助您優化和簡化 JavaScript 捆綁。
即使您是初學者,本指南也為您提供了開始使用 Webpack 的堅實基礎,這將使您能夠更多地專注於開發而不是捆綁配置。