本文概述
模块化的概念是大多数现代编程语言的固有组成部分。但是, 在最新版本的ECMAScript ES6到来之前, JavaScript缺乏任何正式的模块化方法。
在Node.js(当今最流行的JavaScript框架之一)中, 模块捆绑程序允许在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。我们可以通过使用入口点作为对象类型来生成多个包。以下配置生成两个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可以基于此拆分点生成单独的捆绑包, 并根据需要调用它。
在此示例中, 我们有两个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中, 诸如样式加载器和css-loader之类的加载器会对样式表进行预处理, 并将其嵌入到输出JavaScript包中, 但是在某些情况下, 它们会导致未样式化内容(FOUC)的泛滥。
我们可以使用ExtractTextWebpackPlugin避免FOUC, 该方法允许将所有样式生成为单独的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以便能够生成最终捆绑包。
ProvidePlugin
大多数第三方插件都依赖于特定的全局依赖项。对于jQuery, 插件依赖于定义的$或jQuery变量, 我们可以通过在代码中调用$(‘div.content’)。pluginFunc()来使用jQuery插件。
我们可以使用Webpack插件ProvidePlugin在每次遇到全局$标识符时在var $ = require(” jquery”)之前添加前缀。
webpack.config.js
webpack.ProvidePlugin({
‘$’: ‘jquery’, })
当Webpack处理代码时, 它将查找状态$, 并提供对全局依赖项的引用, 而无需导入require函数指定的模块。
进口装载机
一些jQuery插件在全局命名空间中采用$或依靠$作为窗口对象。为此, 我们可以使用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 Arrow函数混淆), 我们可以设置任意变量。最后一个值重新定义此全局变量以指向窗口对象。这与使用(function(){…})。call(window);包装文件的整个内容相同。并以window作为参数调用此函数。
我们还可以要求使用CommonJS或AMD模块格式的库:
// CommonJS
var $ = require("jquery");
// jquery is available
// AMD
define([‘jquery’], function($) {
// jquery is available
});
一些库和模块可以支持不同的模块格式。
在下一个示例中, 我们有一个jQuery插件, 该插件使用AMD和CommonJS模块格式并具有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, 则Webpack不会以CommonJS模块格式解析模块。
曝光装载机
如果需要将模块公开给全局上下文, 则可以使用Exposure-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尝试要求React时, 它将使用项目的node_modules中的版本。如果我们想找出我们使用的React版本, 可以在源代码中添加console.log(React.version)。
专注于开发, 而不是Webpack配置
这篇文章只是对Webpack功能和实用工具的了解。
还有许多其他的Webpack加载程序和插件可以帮助你优化和简化JavaScript捆绑。
即使你是初学者, 本指南也为你开始使用Webpack奠定了坚实的基础, 这将使你可以将更多的精力放在开发上, 而不必考虑捆绑配置。
相关:维护控制权:Pt Webpack and React指南。 1个
评论前必须登录!
注册