webpack简介webpack是一款模块打包工具。它为不同的依赖创建模块,将其整体打包成可管理的输出文件。这一点对于单页面应用(如今web应用的事实标准)来说特别有用。
假设我们有一个可以执行两个简单数学任务(加法和乘法)的应用程序,为了方便维护,我们决定切分这些函数到不同的文件中去。
index.html
<html><head> <script src="src/sum.js"></script> <script src="src/multiply.js"></script> <script src="src/index.js"></script></head></html>
index.js
var totalmultiply = multiply(5, 3);var totalsum = sum(5, 3);console.log('product of 5 and 3 = ' + totalmultiply);console.log('sum of 5 and 3 = ' + totalsum);
multiply.js
var multiply = function (a, b) { var total = 0; for (var i = 0; i < b; i++) { total = sum(a, total); } return total;};
sum.js
var sum = function (a, b) { return a + b;};
这个应用程序的输出应该是:
product of 5 and 3 = 15sum of 5 and 3 = 8
webpack是如何帮助我们的?我们不能仅仅只是使用工具,而不知道这些工具能帮助我们做什么。那么,webpack帮我们做了什么呢?
用模块来拯救依赖
在上面的代码中,我们可以看到,multiply.js与index.js均依赖于sum.js。因此,如果index.html导入依赖时使用了错误的顺序,那么我们的应用就无法工作。举个例子,如果index.js最先被导入,或者sum.js在multiply.js之后被导入,都会得到错误。
基于上面的例子,让我们想象一下,一个真实的web应用往往可能会包含多达几十个依赖项,这些依赖项之间还可能存在依赖关系,维护这些依赖项之间的顺序想想就让人窒息。这里还可能存在变量被其它依赖覆盖的风险,而这将会导致难以发现的bug。
为了解决这个痛点,webpack会将依赖转换为作用域更小的模块,从而避免变量被覆盖。依赖转换为模块带来的额外好处是,webpack可以为我们管理这些依赖。具体做法是,webpack会在需要时,把依赖模块引入进来,并匹配对应的作用域。
通过打包来减少http请求次数
我们还是回看一下index.html,这个文件中我们需要下载三个独立的文件。当然这里文件比较少还能够应付,但还是之前提到的问题,真实的web应用中,依赖项可能会很多,而这将会导致用户不得不等待所有依赖项挨个下载完成后,主应用才能运行。
而这就引出了webpack的另一特性——打包。webpack可以将所有的依赖打包成一个文件,而这就意味着,用户只需要下载一个依赖项,主应用就可以运行。
综上所述,webpack的主要特性就是打包和模块化。通过使用插件和加载器,我们可以扩展webpack的这两大特性。
让依赖可用,并组合它们我们将使用commonjs模块语法,作为初始设置。当然,这里也有诸如amd,es2015等其它选择,但这里我们将先使用commonjs,稍后迁移到es2015。
commonjs将模块导出,使得其它代码可以使用导出模块中的函数或变量。我们可以通过require将导出模块中的值读出来。
index.html
<html><head> <script src="./dist/bundle.js""></script></head></html>
index.js
var multiply = require('./multiply');var sum = require('./sum');var totalmultiply = multiply(5, 3);var totalsum = sum(5, 3);console.log('product of 5 and 3 = ' + totalmultiply);console.log('sum of 5 and 3 = ' + totalsum);
multiply.js
var sum = require('./sum');var multiply = function (a, b) { var total = 0; for (var i = 0; i < b; i++) { total = sum(a, total); } return total;};module.exports = multiply;
sum.js
var sum = function (a, b) { return a + b;};module.exports = sum;
观察上面的代码,不难发现,为了让函数sum与函数multiply能够被使用,我们在sum.js与multiply.js脚本中,导出了这两个函数。这里有个细节,不知道大家是否注意到了,在index.html中我们现在仅需要导入一个bundle.js文件。
这可帮大忙了!我们现在不再需要关注依赖的顺序,可以暴露我们想暴露的内容,并使得其它内容仍然保持私有。同时,我们现在仅需要导入一个文件,而不是三个,这有助于提高应用的加载速度。
webpack的初始配置
为了实现我们上面要达到的效果,我们需要对webpack做一些初始的配置。
var path = require('path');module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, './dist/'), filename: 'bundle.js' }}
这里我们实现了一个最简单的配置。我们至少需要告诉webpack,我们应用的入口在哪,输出结果应该是什么。我们来详细看看每个属性所代表的含义。
entry: 这个属性表示应用的入口。入口就意味着,这是我们加载程序和程序逻辑的起点。webpack将从这个入口开始,遍历整棵依赖树。根据遍历结果建立一个依赖间关系图,并创建需要的模块。output.path: 这个属性表示存放打包结果的绝对路径。这里为了方便使用,我们采用了node.js自带的函数path,这个函数能够根据我们程序所处的位置,动态的创建绝对路径。其中,__dirname是node.js的一个工具变量,它表示当前文件的目录名。output.filename: 这个属性表示打包结果的文件名。它的名字可以是任意的,只不过我们习惯叫它bundle.js。来看看bundle.js
阅读生成的bundle.js代码,可以给我们带来一些启发。
// the webpack bootstrap(function (modules) { // the module cache var installedmodules = {}; // the require function function __webpack_require__(moduleid) { // check if module is in cache // create a new module (and put it into the cache) // execute the module function // flag the module as loaded // return the exports of the module } // expose the modules object (__webpack_modules__) // expose the module cache // load entry module and return exports return __webpack_require__(0);})/************************************************************************/([ // index.js - our application logic /* 0 */ function (module, exports, __webpack_require__) { var multiply = __webpack_require__(1); var sum = __webpack_require__(2); var totalmultiply = multiply(5, 3); var totalsum = sum(5, 3); console.log('product of 5 and 3 = ' + totalmultiply); console.log('sum of 5 and 3 = ' + totalsum); }, // multiply.js /* 1 */ function (module, exports, __webpack_require__) { var sum = __webpack_require__(2); var multiply = function (a, b) { var total = 0; for (var i = 0; i < b; i++) { total = sum(a, total); } return total; }; module.exports = multiply; }, // sum.js /* 2 */ function (module, exports) { var sum = function (a, b) { return a + b; }; module.exports = sum; }]);
从上面的代码可以看出,webpack把每一个js脚本都封装到一个模块中,并把这些模块放进数组中。模块数组被传入webpack的引导程序中,引导程序会把这些模块加入webpack并执行,使得模块可用。
这里bundle.js返回的是__webpack_require__(0),而这刚好对应了模块数组中的index.js部分。基于此我们同样可以得到正确的运行结果,而不需要处理依赖管理,下载依赖的次数也仅需要一次。
loader让webpack更好用
webpack仅能理解最基本的javascript es5代码,它自身仅支持创建模块并打包javascript es5。如果我们不仅仅局限于javascript es5,例如我们想使用es2015,这就需要告诉webpack如何处理es2015。这里我们的处理方式往往是,我们需要将其它语言(如typescript)或其它版本(如es2015)预处理成javascript es5,再让webpack做打包。这里就需要使用babel来做转换,把es2015转换为es5(当然babel能做的事情远不止如此)。
为了说明这个过程,我们使用es2015重写之前的功能。
index.js
import multiply from './multiply';import sum from './sum';const totalmultiply = multiply(5, 3);const totalsum = sum(5, 3);console.log(`product of 5 and 3 = ${totalmultiply}`);console.log(`sum of 5 and 3 = ${totalsum}`);
multiply.js
import sum from './sum';const multiply = (a, b) => { let total = 0; for(let i=0;i<b;i++) { total = sum(a, total); } return total;};export default multiply;
sum.js
const sum = (a, b) => a + b;export default sum;
这里我们使用了很多es2015的新特性,例如箭头函数、const关键字、模板字符串和es2015的导入导出。es2015的代码webpack无法处理,所以我们需要babel来进行转换。想要让babel配合webpack完成工作,我们就需要用到babel loader。事实上,loader就是webpack处理javascript es5以外内容的方式。有了加载器,我们就可以让webpack处理各式各样的文件。
想要在webpack中使用babel loader,我们还需要三个babel依赖:
babel-loader: 提供babel与webpack之间的接口;
babel-core: 提供读取和解析代码的功能,并生成对应的输出;
babel-preset-es2015: 提供将es2015转换为es5的babel规则;
在webpack中配置babel loader的代码,差不多是下面这样子:
const path = require('path');module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, './dist/'), filename: 'bundle.js' }, module: { loaders: [ { test: /.js$/, loader: 'babel-loader', exclude: /node_modules/, query: { presets: ['es2015'] } } ] }};
这段代码你可以在webpack.config.js中找到。值得注意的是,webpack中是支持同时存在多个loader的,所以提供的值是一个数组。接着,还是让我们来看看每个属性代表的含义。
test: 我们只希望loader处理javascript文件,这里通过一个正则表达式匹配.js文件;loader: 要使用的loader,这里使用了babel-loader;exclude: 哪些文件不需要被处理,这里我们不希望babel处理node_modules下的任何文件;query.presets: 我们需要使用哪个规则,这里我们使用babel转换es2015的规则;配置好这些内容后,再次查看打包生成的bundle.js,其中的内容看起来就像下面这样:
/* 2 */function(module, exports) { var sum = function sum(a, b) { return a + b; }; module.exports = sum;}
可以看到,babel loader已经把es2015的代码变成了es5的代码。
css与样式,让webpack看起来更出色接下来,让我们拓展上面的例子,输出计算结果。我们将在页面上创建一个body,然后把乘积与加和的结果添加到span中。
import multiply from './multiply';import sum from './sum';const totalmultiply = multiply(5, 3);const totalsum = sum(5, 3);// create the bodyconst body = document.createelement("body");document.documentelement.appendchild(body);// calculate the product and add it to a spanconst multiplyresultsspan = document.createelement('span');multiplyresultsspan.appendchild(document.createtextnode(`product of 5 and 3 = ${totalmultiply}`));// calculate the sum and add it to a spanconst sumresultspan = document.createelement('span');sumresultspan.appendchild(document.createtextnode(`sum of 5 and 3 = ${totalsum}`));// add the results to the pagedocument.body.appendchild(multiplyresultsspan);document.body.appendchild(sumresultspan);
这段代码的输出结果,应该与之前是一致的,区别仅在于显示在页面上。
product of 5 and 3 = 15 sum of 5 and 3 = 8
我们可以使用css来美化这个结果,比如,我们可以让每个结果都独占一行,并且给每个结果都加上边框。为了实现这一点,我们可以使用如下的css代码。
span { border: 5px solid brown; display: block;}
我们需要将这个css也导入应用中。这里最简单的解决方案是,在我们的html中添加一个link标签。但有了webpack提供的强大功能,我们可以先导入它,再用webpack来处理这个样式。
在代码中导入css带来的另一个好处是,开发者可以清晰的看到css与其使用之间的关联。这里需要注意的是,css的作用域并不局限于它所导入的模块本身,其作用域仍然是全局的,但从开发者的角度看,这样使用更加清晰。
import multiply from './multiply';import sum from './sum';// import the css we want to use hereimport './math_output.css';const totalmultiply = multiply(5, 3);const totalsum = sum(5, 3);// create the bodyconst body = document.createelement("body");document.documentelement.appendchild(body);// calculate the product and add it to a spanconst multiplyresultsspan = document.createelement('span');multiplyresultsspan.appendchild(document.createtextnode(`product of 5 and 3 = ${totalmultiply}`));// calculate the sum and add it to a spanconst sumresultspan = document.createelement('span');sumresultspan.appendchild(document.createtextnode(`sum of 5 and 3 = ${totalsum}`));// add the results to the pagedocument.body.appendchild(multiplyresultsspan);document.body.appendchild(sumresultspan);
这段代码中,与前面代码的唯一区别在于,我们导入了css。我们需要两个loader来处理我们的css:
css-loader: 用于处理css导入,具体来说,获取导入的css并加载css文件内容;
style-loader: 获取css数据,并添加它们到html文档中;
现在我们在webpack.config.js中的webpack配置看起来像这样:
const path = require('path');module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, './dist/'), filename: 'bundle.js' }, module: { loaders: [ { test: /.js$/, loader: 'babel-loader', exclude: /node_modules/, query: { presets: ['es2015'] } }, { test: /.css$/, loaders: ['style-loader', 'css-loader'] } ] }};
我们还是来看看新增的css配置属性所表示的内容。
test: 我们需要告诉loader,我们只需要它处理css文件。这里的正则表达式仅匹配css文件。loaders: 这里与前面不同的是,我们使用了多个loader。还有一个需要注意的细节是,webpack从右向左处理loader,因此css-loader处理的结果(读出css文件内容)会被传递给style-loader,最终得到的是style-loader的处理结果(将样式添加到html文档中)。假如我们现在需要提取css,并输出到一个文件中,再导入该文件。为了实现这一点,我们就要用到plugin。loader的作用在于,在数据被打包输出前进行预处理。而plugin则可以阻止预处理的内容直接出现在我们的打包结果中。
我们的webpack配置现在变成了这样:
const path = require('path');const extracttextplugin = require('extract-text-webpack-plugin');module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, './dist/'), filename: 'bundle.js' }, module: { loaders: [ { test: /.js$/, loader: 'babel-loader', exclude: /node_modules/, query: { presets: ['es2015'] } }, { test: /.css$/, loader: extracttextplugin.extract('css-loader') } ] }, plugins: [ new extracttextplugin('style.css') ]};
在这段代码的顶部,我们导入了extracttextplugin,并使用这个插件改造了之前的css loader。这里的作用是,css-loader的处理结果不再直接返回给webpack,而是传递给extracttextplugin。在底部我们配置了这个plugin。
这里配置的作用是,对于传递给extracttextplugin的css样式数据,将会被保存在名为style.css的文件中。这样做的好处与之前处理javascript时一样,我们可以将多个独立的css文件合并为一个文件,从而减少加载样式时的下载次数。
最终我们可以直接使用我们合并好的css,实现和之前一致的效果。
<html> <head> <link rel="stylesheet" href="dist/style.css"/> <script src="./dist/bundle.js""></script> </head></html>
使用webpack处理图片现在我们开始尝试向应用中添加图片,并让webpack来协助我们处理这些图片。这里我们添加了两张图片,一个用于求和,一个用于求积。为了让webpack有能力处理这些图片,我们使用这两个loader:
image-webpack-loader: 尝试帮助我们自动压缩图片体积;
url-loader: 如果image-webpack-loader的输出图片体积小,就内联使用这些图片,如果image-webpack-loader的输出图片体积大,就将图像包含在输出目录中;
我们准备了两张图片,用于求积的图片(multiply.png)相对较大,用于求和的图片(sum.png)相对较小。首先,我们添加一个图片工具方法,这个方法会为我们创建图片,并将图片添加到文档中。
image_util.js
const addimagetopage = (imagesrc) => { const image = document.createelement('img'); image.src = imagesrc; image.style.height = '100px'; image.style.width = '100px'; document.body.appendchild(image);};export default addimagetopage;
接着,让我们导入这个图片工具方法,以及我们想要添加到index.js中的图片。
import multiply from './multiply';import sum from './sum';// import our image utilityimport addimagetopage from './image_util';// import the images we want to useimport multiplyimg from '../images/multiply.png';import sumimg from '../images/sum.png';// import the css we want to use hereimport './math_output.css';const totalmultiply = multiply(5, 3);const totalsum = sum(5, 3);// create the bodyconst body = document.createelement("body");document.documentelement.appendchild(body);// calculate the product and add it to a spanconst multiplyresultsspan = document.createelement('span');multiplyresultsspan.appendchild(document.createtextnode(`product of 5 and 3 = ${totalmultiply}`));// calculate the sum and add it to a spanconst sumresultspan = document.createelement('span');sumresultspan.appendchild(document.createtextnode(`sum of 5 and 3 = ${totalsum}`));// add the results to the pageaddimagetopage(multiplyimg);document.body.appendchild(multiplyresultsspan);addimagetopage(sumimg);document.body.appendchild(sumresultspan);
最后,我们还是来修改webpack.config.js,配置两个新的loader来处理这些图片。
const path = require('path');const extracttextplugin = require('extract-text-webpack-plugin');module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, './dist/'), filename: 'bundle.js', publicpath: 'dist/' }, module: { loaders: [ { test: /.js$/, loader: 'babel-loader', exclude: /node_modules/, query: { presets: ['es2015'] } }, { test: /.css$/, loader: extracttextplugin.extract('css-loader') }, { test: /.png$/, loaders: [ 'url-loader?limit=5000', 'image-webpack-loader' ] } ] }, plugins: [ new extracttextplugin('style.css') ]};
还是老规矩,我们来看看新增的参数都表示什么含义。
output.publicpath: 让url-loader知道,保存到磁盘的文件需要添加指定的前缀。例如,我们需要保存一个output_file.png,那么最终保存的路径应该是dist/output_file.png;test: 还是和之前一样,通过正则表达式匹配图像文件,非图像文件不处理;loaders: 这里还是要再强调一下,webpack从右向左处理loader,因此image-webpack-loader的处理结果将会被传递给url-loader继续处理。现在我们执行webpack打包,会得到下面三个东西。
38ba485a2e2306d9ad96d479e36d2e7b.pngbundle.jsstyle.css
这里的38ba485a2e2306d9ad96d479e36d2e7b.png实际上就是我们的大图片multiply.png,较小的图片sum.png会被内联到bundle.js中,就像下面这样。
module.exports = "data:image/png;base64,ivborw0kggoaaaansuheugaaaoaaaahgcamaaaacdyzwaaac6fbmveuauv8agl...."
这其实相当于
img.src="data:image/png;base64,ivborw0kggoaaaansuheugaaaoaaaahgcamaaaacdyzwaaac6fbmveuauv8agl..."
更多编程相关知识,请访问:编程视频!!
以上就是webpack是什么?详解它是如何工作的?的详细内容。