管理 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 的坚实基础,这将使您能够更多地专注于开发而不是捆绑配置。