作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Andrej Gajdos的头像

Andrej Gajdos

拥有服务科学硕士学位, Management, and Engineering, Andrej为世界各地的客户从事各种规模的项目.

Expertise

Years of Experience

11

Share

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

In Node.作为当今最流行的JavaScript框架之一,模块绑定器允许加载 NPM modules in web browsers, 以及面向组件的库(如React)鼓励并促进JavaScript代码的模块化.

Webpack is 可用的模块捆绑器之一 that processes JavaScript 代码以及所有静态资产(如样式表、图像和字体)打包到一个捆绑文件中. 处理可以包括管理和优化Webpack包依赖的所有必要任务, such as compilation, concatenation, minification, and compression.

Webpack:初学者教程

However, 配置Webpack和它的依赖关系可能会有压力,而且并不总是一个简单的过程, 特别适合初学者.

这篇博文提供了指导方针, with examples, 如何为不同的场景配置Webpack, 并指出了与使用Webpack捆绑项目依赖相关的最常见的陷阱.

这篇博文的第一部分解释了如何简化项目对其Webpack依赖项的定义. Next, 我们讨论并演示了多页和单页应用程序的代码分割配置. Finally, 我们将讨论如何配置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’;
导入导航../组件/导航”;


Modal.js

导入{datepicker}../../../../ bower_modules / datepicker / dist / js / datepicker”;

每次我们想要导入脚本或模块时, 我们需要知道当前目录的位置,并找到我们想要导入的文件的相对路径. 我们可以想象,如果我们有一个嵌套文件结构的大项目,这个问题的复杂性会如何升级, 或者我们想重构一个复杂项目结构的某些部分.

我们可以很容易地处理这个问题与Webpack的 resolve.alias option. 我们可以声明所谓的别名——目录或模块的名称及其位置, 而且我们不依赖于项目源代码中的相对路径.


webpack.config.js

resolve: {
    alias: {
        “node_modules”:路径.加入(__dirname node_modules),
        “bower_modules”:路径.加入(__dirname bower_modules),
    }
}

In the Modal.js 文件,我们现在可以更简单地导入datepicker:

从'bower_modules/datepicker/dist/js/datepicker'中导入{datepicker};

Code Splitting

在某些情况下,我们需要将脚本附加到最终包中, 或者将最后的捆绑分成两部分, 或者我们想要按需加载单独的包. 为这些场景设置我们的项目和Webpack配置可能并不简单.

在Webpack配置中, Entry 选项告诉Webpack最终包的起始点在哪里. 入口点可以有三种不同的数据类型:字符串、数组或对象.

如果我们有一个单一的起点,我们可以使用这些格式中的任何一种,并得到相同的结果.

如果我们想要附加多个文件,并且它们不依赖于彼此,我们可以使用Array格式. 例如,我们可以追加 analytics.js to the end of the bundle.js:


webpack.config.js

module.exports = {
    //创建一个bundle.Js,然后追加分析.js
    entry: ['./src/script/index.jsx', './ src /脚本/分析.js'],
    output: {
        path: './build',
        filename: bundle.js '  
   }
};

管理多个入口点

假设我们有一个包含多个HTML文件的多页面应用程序,例如 index.html and admin.html. 通过使用入口点作为Object类型,我们可以生成多个bundle. 下面的配置生成了两个JavaScript包:


webpack.config.js

module.exports = {
   entry: {
       index: './src/script/index.jsx',
       admin: './src/script/admin.jsx'
   },
   output: {
       path: './build',
       filename: '[name].Js ' //基于上面条目中的键的模板(index . Js).js & admin.js)
   }
};


index.html



admin.html


两个JavaScript包都可以共享公共库和组件. 我们可以用 CommonsChunkPlugin, 哪个查找出现在多个条目块中的模块,并创建可以在多个页面之间缓存的共享包.


webpack.config.js

var commonplugin =新webpack.optimize.CommonsChunkPlugin(“共同.js');
 
module.exports = {
   entry: {
       index: './src/script/index.jsx',
       admin: './src/script/admin.jsx'
   },
   output: {
       path: './build',
       filename: '[name].Js ' //基于上面条目中的键的模板(index . Js).js & admin.js)
   },
   插件(commonsPlugin):
};

现在,我们一定不要忘记加 在捆绑脚本之前.

注意:Webpack共享依赖关系的最佳实践在较新的Webpack版本中有所发展. 这本书值得一读 SplitChunksPlugin and ModuleFederationPlugin 并比较它们的优点,除非你坚持使用Webpack的旧版本.

启用延迟加载

Webpack可以将静态资产分割成更小的块, 这种方法比标准连接更灵活. 如果我们有一个大型单页应用(SPA), 简单地连接到一个bundle中并不是一个好方法,因为加载一个巨大的bundle会很慢, 用户通常不需要每个视图上的所有依赖项.

我们在前面解释了如何将应用程序拆分为多个bundle, 连接公共依赖项, 并受益于浏览器缓存行为. 这种方法适用于多页面应用程序,但不适用于单页面应用程序.

对于SPA,我们应该只提供呈现当前视图所需的那些静态资产. SPA架构中的客户端路由器是处理代码分割的完美场所. 当用户输入路由时,我们只能为生成的视图加载那些需要的依赖项. 或者,我们可以在用户向下滚动页面时加载依赖项.

为此,我们可以使用 require.ensure or System.import 函数,Webpack可以静态地检测. Webpack可以基于这个分离点生成一个单独的bundle,并根据需要调用它.

In this example, we have two React containers; an admin view and a dashboard view.


admin.jsx

从' React '中导入React, {Component};
 
导出默认类Admin扩展组件{
   render() {
       return 
Admin < /div>; } }


dashboard.jsx

从' React '中导入React, {Component};
 
导出默认类Dashboard扩展组件{
   render() {
       return 
Dashboard < /div>; } }

如果用户输入 /dashboard or /admin URL,则只加载相应的所需JavaScript包. 下面我们可以看到有客户端路由器和没有客户端路由器的例子.


index.jsx

if (window.location.路径名=== '/dashboard') {
   require.确保([],function() {
       require('./集装箱/仪表盘”).default;
   });
} else if (window.location.路径名=== '/admin') {
   require.确保([],function() {
       require('./集装箱/管理”).default;
   });
}


index.jsx

ReactDOM.render(
   
        
{props.children}
}> { require.确保([],function (require) { cb(null, require('./集装箱/仪表盘”).default) }, "dashboard")}} /> { require.确保([],function (require) { cb(null, require('./集装箱/管理”).default) }, "admin")}} />
, document.getElementById(“内容”) );

将样式提取到单独的bundle中

In Webpack, loaders, like style-loader and css-loader, 预处理样式表并将它们嵌入到输出的JavaScript包中, but in some cases, they can cause the 无样式内容的Flash (FOUC).

我们可以用 ExtractTextWebpackPlugin 它允许在单独的CSS包中生成所有样式,而不是将它们嵌入到最终的JavaScript包中.


webpack.config.js

var ExtractTextPlugin = require('extract-text-webpack-plugin');
 
module.exports = {
   module: {
       loaders: [{
           test: /\.css/,
           装载机:ExtractTextPlugin.提取(“风格”,css) '
       }],
   },
   plugins: [
       //将提取的CSS输出到文件
       新ExtractTextPlugin(“[名称].[chunkhash].css')
   ]
}

处理第三方库和插件

Many times, 我们需要使用第三方库, various plugins, 或者其他脚本, 因为我们不想花时间从头开始开发相同的组件. 有许多遗留库和插件没有得到积极维护, 不懂JavaScript模块, 并假设在预定义的名称下存在全局依赖项.

下面是一些使用jQuery插件的例子, 并解释了如何正确配置Webpack以生成最终的包.

ProvidePlugin

大多数第三方插件依赖于特定的全局依赖项. 在jQuery中,插件依赖于 $ or jQuery 变量被定义,我们可以通过调用 $(‘div.content’).pluginFunc() in our code.

我们可以使用Webpack插件 ProvidePlugin to prepend Var $ = require("jquery") 每次它遇到全球 $ identifier.


webpack.config.js

webpack.ProvidePlugin({
   ‘$’: ‘jquery’,
})

当Webpack处理代码时,它会查找是否存在 $,并提供对全局依赖项的引用,而无需导入由 require function.

Imports-loader

一些jQuery插件假定 $ 在全局命名空间中或依赖 this being the window object. 为此,我们可以使用 imports-loader 将全局变量注入到模块中.


example.js

$(‘div.content’).pluginFunc();

然后,我们可以注入 $ 变量导入到模块中 imports-loader:

require("imports?$=jquery!./example.js");

这只是一个前缀 Var $ = require("jquery"); to example.js.

在第二个用例中:


webpack.config.js

module: {
   loaders: [{
       测试:/ jquery-plugin /,
       loader: 'imports?jQuery=jquery,$=jquery,this=>window'
   }]
}

By using the => 符号(不要与 ES6 Arrow functions),我们可以设置任意变量. 最后一个值重新定义了全局变量 this to point to the window object. 方法包装文件的整个内容是一样的 (function () { ... }).call(window); and calling this function with window as an argument.

我们也可以要求使用CommonJS或AMD模块格式的库:

// CommonJS
Var $ = require("jquery");  
//可以使用jquery

// AMD
定义([' jquery '],函数($){  
//可以使用jquery
});

一些库和模块可以支持不同的模块格式.

在下一个例子中, 我们有一个jQuery插件,它使用AMD和CommonJS模块格式,并有一个jQuery依赖:


jquery-plugin.js

(函数(工厂){
   If (typeof define === 'function') && define.amd) {
       //使用AMD格式
       定义((“jquery”)、工厂);
   } else if (typeof exports === 'object') {
       //使用CommonJS格式
       module.Exports = factory(require('jquery'));
   } else {
       // AMD和CommonJS都没有使用. 使用全局变量.
   }
});


webpack.config.js

module: {
   loaders: [{
       测试:/ jquery-plugin /,
       loader: "imports?define=>false,exports=>false"
   }]
}

我们可以为特定的库选择要使用的模块格式. If we declare define to equal false, Webpack不会解析AMD模块格式的模块,如果我们声明变量 exports to equal false, Webpack不会以CommonJS模块格式解析模块.

Expose-loader

如果我们需要将一个模块公开给全局上下文,我们可以使用 expose-loader. 这很有帮助, for example, 如果我们有不属于Webpack配置的外部脚本,并且依赖于全局命名空间中的符号, 或者我们使用需要访问浏览器控制台中符号的浏览器插件.


webpack.config.js

module: {
   loaders: [
       test: require.resolve('jquery'),
       装载机:“expose-loader?jQuery!expose-loader?$'
   ]
}

jQuery库现在可以在全局命名空间中用于网页上的其他脚本.

window.$
window.jQuery

配置外部Webpack依赖项

如果我们想包含来自外部托管脚本的模块, 我们需要在构型中定义它们. 否则,Webpack将无法生成最终的bundle.

我们可以使用Webpack来配置外部脚本 externals 配置选项. 例如,我们可以通过一个单独的

\n\n\n


\nadmin.html

\n\n
\n
\n\n

两个JavaScript包都可以共享公共库和组件. 我们可以用 CommonsChunkPlugin, 哪个查找出现在多个条目块中的模块,并创建可以在多个页面之间缓存的共享包.

\n\n


\nwebpack.config.js

\n\n
var commonplugin =新webpack.optimize.CommonsChunkPlugin(“共同.js');\n \nmodule.exports = {\n   entry: {\n       index: './src/script/index.jsx',\n       admin: './src/script/admin.jsx'\n   },\n   output: {\n       path: './build',\n       filename: '[name].Js ' //基于上面条目中的键的模板(index . Js).js & admin.js)\n   },\n   插件(commonsPlugin):\n};\n
\n\n

现在,我们一定不要忘记加 在捆绑脚本之前.

\n\n

注意:Webpack共享依赖关系的最佳实践在较新的Webpack版本中有所发展. 这本书值得一读 SplitChunksPlugin and ModuleFederationPlugin 并比较它们的优点,除非你坚持使用Webpack的旧版本.

\n\n

启用延迟加载

\n\n

Webpack可以将静态资产分割成更小的块, 这种方法比标准连接更灵活. 如果我们有一个大型单页应用(SPA), 简单地连接到一个bundle中并不是一个好方法,因为加载一个巨大的bundle会很慢, 用户通常不需要每个视图上的所有依赖项.

\n\n

我们在前面解释了如何将应用程序拆分为多个bundle, 连接公共依赖项, 并受益于浏览器缓存行为. 这种方法适用于多页面应用程序,但不适用于单页面应用程序.

\n\n

对于SPA,我们应该只提供呈现当前视图所需的那些静态资产. SPA架构中的客户端路由器是处理代码分割的完美场所. 当用户输入路由时,我们只能为生成的视图加载那些需要的依赖项. 或者,我们可以在用户向下滚动页面时加载依赖项.

\n\n

为此,我们可以使用 require.ensure or System.import 函数,Webpack可以静态地检测. Webpack可以基于这个分离点生成一个单独的bundle,并根据需要调用它.

\n\n

In this example, we have two React containers; an admin view and a dashboard view.

\n\n


\nadmin.jsx

\n\n
从' React '中导入React, {Component};\n \n导出默认类Admin扩展组件{\n   render() {\n       return 
Admin < /div>;\n }\n}\n
\n\n


\ndashboard.jsx

\n\n
从' React '中导入React, {Component};\n \n导出默认类Dashboard扩展组件{\n   render() {\n       return 
Dashboard < /div>;\n }\n}\n
\n\n

如果用户输入 /dashboard or /admin URL,则只加载相应的所需JavaScript包. 下面我们可以看到有客户端路由器和没有客户端路由器的例子.

\n\n


\nindex.jsx

\n\n
if (window.location.路径名=== '/dashboard') {\n   require.确保([],function() {\n       require('./集装箱/仪表盘”).default;\n   });\n} else if (window.location.路径名=== '/admin') {\n   require.确保([],function() {\n       require('./集装箱/管理”).default;\n   });\n}\n
\n\n


\nindex.jsx

\n\n
ReactDOM.render(\n   \n        
{props.children}
}>\n \n {\n require.确保([],function (require) {\n cb(null, require('./集装箱/仪表盘”).default)\n }, \"dashboard\")}}\n />\n {\n require.确保([],function (require) {\n cb(null, require('./集装箱/管理”).default)\n }, \"admin\")}}\n />\n \n
\n , document.getElementById(“内容”)\n);\n
\n\n

将样式提取到单独的bundle中

\n\n

In Webpack, loaders, like style-loader and css-loader, 预处理样式表并将它们嵌入到输出的JavaScript包中, but in some cases, they can cause the 无样式内容的Flash (FOUC).

\n\n

我们可以用 ExtractTextWebpackPlugin 它允许在单独的CSS包中生成所有样式,而不是将它们嵌入到最终的JavaScript包中.

\n\n


\nwebpack.config.js

\n\n
var ExtractTextPlugin = require('extract-text-webpack-plugin');\n \nmodule.exports = {\n   module: {\n       loaders: [{\n           test: /\\.css/,\n           装载机:ExtractTextPlugin.提取(“风格”,css) '\n       }],\n   },\n   plugins: [\n       //将提取的CSS输出到文件\n       新ExtractTextPlugin(“[名称].[chunkhash].css')\n   ]\n}\n
\n\n

处理第三方库和插件

\n\n

Many times, 我们需要使用第三方库, various plugins, 或者其他脚本, 因为我们不想花时间从头开始开发相同的组件. 有许多遗留库和插件没有得到积极维护, 不懂JavaScript模块, 并假设在预定义的名称下存在全局依赖项.

\n\n

下面是一些使用jQuery插件的例子, 并解释了如何正确配置Webpack以生成最终的包.

\n\n

ProvidePlugin

\n\n

大多数第三方插件依赖于特定的全局依赖项. 在jQuery中,插件依赖于 $ or jQuery 变量被定义,我们可以通过调用 $(‘div.content’).pluginFunc() in our code.

\n\n

我们可以使用Webpack插件 ProvidePlugin to prepend var $ = require(\"jquery\") 每次它遇到全球 $ identifier.

\n\n


\nwebpack.config.js

\n\n
webpack.ProvidePlugin({\n   ‘$’: ‘jquery’,\n})\n
\n\n

当Webpack处理代码时,它会查找是否存在 $,并提供对全局依赖项的引用,而无需导入由 require function.

\n\n

Imports-loader

\n\n

一些jQuery插件假定 $ 在全局命名空间中或依赖 this being the window object. 为此,我们可以使用 imports-loader 将全局变量注入到模块中.

\n\n


\nexample.js

\n\n
$(‘div.content’).pluginFunc();\n
\n\n

然后,我们可以注入 $ 变量导入到模块中 imports-loader:

\n\n
require(\"imports?$=jquery!./example.js\");\n
\n\n

这只是一个前缀 var $ = require(\"jquery\"); to example.js.

\n\n

在第二个用例中:

\n\n


\nwebpack.config.js

\n\n
module: {\n   loaders: [{\n       测试:/ jquery-plugin /,\n       loader: 'imports?jQuery=jquery,$=jquery,this=>window'\n   }]\n}\n
\n\n

By using the => 符号(不要与 ES6 Arrow functions),我们可以设置任意变量. 最后一个值重新定义了全局变量 this to point to the window object. 方法包装文件的整个内容是一样的 (function () { ... }).call(window); and calling this function with window as an argument.

\n\n

我们也可以要求使用CommonJS或AMD模块格式的库:

\n\n
// CommonJS\nvar $ = require(\"jquery\");  \n//可以使用jquery\n\n// AMD\n定义([' jquery '],函数($){  \n//可以使用jquery\n});\n
\n\n

一些库和模块可以支持不同的模块格式.

\n\n

在下一个例子中, 我们有一个jQuery插件,它使用AMD和CommonJS模块格式,并有一个jQuery依赖:

\n\n


\njquery-plugin.js

\n\n
(函数(工厂){\n   If (typeof define === 'function') && define.amd) {\n       //使用AMD格式\n       定义((“jquery”)、工厂);\n   } else if (typeof exports === 'object') {\n       //使用CommonJS格式\n       module.Exports = factory(require('jquery'));\n   } else {\n       // AMD和CommonJS都没有使用. 使用全局变量.\n   }\n});\n
\n\n


\nwebpack.config.js

\n\n
module: {\n   loaders: [{\n       测试:/ jquery-plugin /,\n       loader: \"imports?define=>false,exports=>false\"\n   }]\n}\n
\n\n

我们可以为特定的库选择要使用的模块格式. If we declare define to equal false, Webpack不会解析AMD模块格式的模块,如果我们声明变量 exports to equal false, Webpack不会以CommonJS模块格式解析模块.

\n\n

Expose-loader

\n\n

如果我们需要将一个模块公开给全局上下文,我们可以使用 expose-loader. 这很有帮助, for example, 如果我们有不属于Webpack配置的外部脚本,并且依赖于全局命名空间中的符号, 或者我们使用需要访问浏览器控制台中符号的浏览器插件.

\n\n


\nwebpack.config.js

\n\n
module: {\n   loaders: [\n       test: require.resolve('jquery'),\n       装载机:“expose-loader?jQuery!expose-loader?$'\n   ]\n}\n
\n\n

jQuery库现在可以在全局命名空间中用于网页上的其他脚本.

\n\n
window.$\nwindow.jQuery\n
\n\n

配置外部Webpack依赖项

\n\n

如果我们想包含来自外部托管脚本的模块, 我们需要在构型中定义它们. 否则,Webpack将无法生成最终的bundle.

\n\n

我们可以使用Webpack来配置外部脚本 externals 配置选项. 例如,我们可以通过一个单独的