管理 Webpack 依赖项的指南

已发表: 2022-03-11

模块化的概念是大多数现代编程语言的固有部分。 然而,在最新版本的 ECMAScript ES6 到来之前,JavaScript 一直缺乏任何正式的模块化方法。

在当今最流行的 JavaScript 框架之一 Node.js 中,模块捆绑器允许在 Web 浏览器中加载 NPM 模块,面向组件的库(如 React)鼓励和促进 JavaScript 代码的模块化。

Webpack 是可用的模块捆绑器之一,可将 JavaScript 代码以及所有静态资产(例如样式表、图像和字体)处理到捆绑文件中。 处理可以包括管理和优化代码依赖关系的所有必要任务,例如编译、连接、缩小和压缩。

Webpack:初学者教程

但是,配置 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.htmladmin.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.ensureSystem.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-loadercss-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 等于falseexports不会解析 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 的坚实基础,这将使您能够更多地专注于开发而不是捆绑配置。

相关:维护控制:Webpack 和 React 指南,Pt。 1