webpack入门-1

花了很多时间,于研究webpack,结果发现很多项目开始转到webpack2或者rollup了,这叫一个尴尬。

不过学习下webpack1也没什么坏处,至少可以比较平稳的升级webpack2。

webpack配置参数太多了…真心太多了,还有多种配置方法,太灵活,这也导致它非常难,网上有的资料都不匹配,没办法四处借鉴来用。故此,还是自己摸索着来吧。

webpack是什么

我的简单理解就是,将多个分散的文件(模块),打包成为一个或者指定形式的文件。

比如说:有一个文件main.js,依赖A.js B.js,如果要想用main.js我们就需要对其进行打包(打包为bundle.js)。通过webpack打包,会自动把A.js B.js的代码打包到bundle.js

webpack自带有一些插件,也有第三方的组件,所以支持的功能比较多,也比较复杂。我通过一些配置文件,来介绍一些常用的用法写法。

wepack 准备说明

首先要安装webpack。安装有两种方案:

全局安装:npm install -g webpack

项目安装:npm install -save-dev wepack

启动终端/命令行,找到当前项目的路径,进入项目。

如果是全局安装,那么之后可以直接输入webpack ...这样来进行使用。

如果是项目安装,可以在package.json中的scripts字段中定义命令,之后执行npm run 命令来进行操作。

为了省事,本系列文章是按照全局安装来使用的。

还是那句话,也可以不用全局安装。可以package.json中的scripts中配置如下代码,来通过npm run webpack使用。

1
2
3
4
5
{
"scripts": {
"webpack": "webpack"
}
}

所有例子的代码位置

可以访问 github 进行查阅。

DEMO0 - 入门

第一个最简单的打包例子。

demo0下,建立src文件夹

新建一个say.js,有两个方法,但是仅仅导出了一个。

1
2
3
4
5
6
7
8
9
var fn = function (msg) {
console.log('hello, ' + msg + '!');
};

var otherFn = function () {
console.log('this is another function!');
};

module.exports = fn;

再新建一个main.js

1
2
3
var say = require('./say.js');

say('world');

例子很简单,公用方法文件say.js,被main.js调用。

请注意

引入的文件(模块),如果是npm安装的,那么代码执行过程中,会有路径处理,能找到这个依赖包,例如写require('react')即可;

但是是自己写的文件(模块)的话,要写成require('./say.js')(可以省略扩展名),注意当前目录要加入./。否则找不到文件。下文会有如何建立别名的方法。

原因是这样的

require默认的工作路径,拿本例来说,不是/webpack-demo/demo0,而是/webpack-demo/demo0/node_modules。在这个目录里面,存在结构react/index.js,或者存在react/package.json文件,此文件中,定义了main入口文件。

这样的话,你直接写require('react'),就是读取 /webpack-demo/demo0/node_modules/react/index.js (因为没有指定文件,所以读取默认文件index.js),所以是OK的。

所以,如果是我们自己的模块,并没有在node_modules下的,直接写require('say.js')就会找不到。要写成当前目录require('./say.js')

了解完路径问题,我们回到例子,接下来进行webpack打包。

webpack打包

启动终端/命令行,首先进入目录:cd demo0

用webpack进行编译:webpack src/main.js build/app.js,第一个参数是入口文件,第二个参数是打包后文件

之后可以发现,多了一个build目录,同时也有了我们要的app.js文件

剩下的事情,就是编写index.html,并查看效果了。

重要的说明

请仔细查看编译后的app.js文件。在源文件say.js中,我故意加入了没有用到的函数otherFn,在编译后的最终文件内,是存在这个函数的。

通过实验,不管用这种ES5风格,还是ES6风格,只要文件内有的东西,都会被编译到最终文件,不论是否被引用。这个是webpack1的问题,好在webpack2已经加入Tree-shaking技术来解决这个问题了。

DEMO1 - 使用配置文件webpack.config.js

这回,我们不再使用直接敲命令来控制webpack打包了,而是将要打包的东西,放到配置文件里。

我们复制一份src目录,到demo1目录下。这样,最终的打包后结果会是相同的。

我们在demo1下,建立一个webpack.config.js文件,这个是默认的配置文件名。

除此外,还有其他的一些名字也是默认名。但是webpack.config.js是最流行的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log('当前dirname:', __dirname);

module.exports = {
// 入口文件配置
entry: './src/main.js',
// 文件导出的配置
output: {
path: './build',
filename: 'app.js'
},
resolve: {
// 当require的模块找不到时,添加这些后缀再次查找
extentions: ['js']
}
}

这里面,我们第一句打印出当前的工作环境目录,这个以后会用得到,可以注意下。

之后,导出了一个配置对象。这个对象里面,有入口文件entry、打包文件output 和特殊查找处理resolve。其中,入口文件和打包文件是必须的,否则webpack不知道入口和出口啊。这两个配置太好理解了,我就不描述了。resolve下文再讲。

这里一样需要注意,当前目录请写上./

之后,我们进入工作目录cd demo1,并执行webpack即可。

如果你的配置文件不是默认名,那么需要这样执行webpack --config somename.js,就是指定配置文件。

执行后,会看到当前dirname路径,因为例子比较简单,不会报错。

如果写的比较复杂,报错了,怎么查看详情?请加参数执行打包:webpack --display-error-details,这样打包一旦遇到错误,会有错误信息。所有的命令参数,都是两条横线开头的。

resolve是什么

它相当于一个变通处理,基本上都是在解决代码中引用部分require import文件的问题

这里,用到了扩展名extentions参数,我们加入了['js']处理方案。意思是,针对引用的文件,自动尝试匹配扩展名。

这里的数组值,可以写不带扩展名的js,也可以写成带有扩展名的.js,当然,还可以写 .config.js 这样的双重扩展名

一般你见到的,都是这样写的:extensions: ['', '.jsx', '.js'],会用一个空串开头。我个人认为没什么意义。

加入扩展名参数后,在我们的代码文件里,一旦有require(或者ES6的import),默认会优先找当前文件。找不到,会自动加入我们配置的扩展名js

比如main.js中的require('./say.js'),默认去找当前目录下的say.js,结果找到了,OK。

如果我们写成require('./say'),默认去找当前目录下的say,找不到!只好按配置的extentions顺序增加扩展名,再去找say.js say/index.js等,直到找到为止。

所以,一旦我们在resolve中配置了extentions,就可以在代码引用环节省去对应的扩展名了。这个简写方案,非常常见。

DEMO2 - resolve的alias参数 官网API

这里,我们在提一个参数alias

接上一个DEMO1,复制一份为DEMO2。

这次项目不一样了,我们要用到很多自己写的模块,这个例子中,我新加入了take.js watch.js文件,并和say.js一起放到了modules目录下,而且是按照分类和版本号存放。

结构为:

1
2
3
modules/say/1.0/say.js
modules/take/1.2/take.js
modules/watch/2.0/watch.js

在引用的文件main.js中,我们希望这么写:

1
2
3
4
5
6
7
var say = require('say');
var take = require('take');
var watch = require('watch');

say('world');
take();
watch();

注意,我们希望这里把say take watch定义了为别名,没有路径概念。

这样好处是,就可以写模块的同事自己专心写模块,而写main.js的同事只需要关心引入对应模块就行了,不需要关心具体的版本和路径。至于别名的处理,只需要有一个人每次负责修改webpack.config.js就可以了。

配置文件,需要修改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
console.log('当前dirname:', __dirname);

module.exports = {
// 入口文件配置
entry: './src/main.js',
// 文件导出的配置
output: {
path: './build',
filename: 'app.js'
},
resolve: {
// 当require的模块找不到时,添加这些后缀再次查找
extentions: ['js'],
// 建立require别名
alias: {
'say': './modules/say/1.0/say',
'take': './modules/take/1.2/take',
'watch': './modules/watch/2.0/watch'
}
}
}

这样写之后,在main.js中,当遇到require('say')的时候,相当于变成了require('./modules/say/1.0/say')。这就是别名的作用。

DEMO3 - resolve的更多参数(root)官网API

resolve.root,用的人应该很少。这里仅做介绍。

现在来个特殊的例子。我们在demo2中,写过一些公共方法,也写了一个调用的main.js。另一个开发小组,知道我们的公共方法了,觉得很好,也想使用,但是他们不希望自己去维护公共方法。怎么办?

让require的默认路径设置为demo2的模块路径呗~

他们的main.js是这样的:

1
2
3
var say = require('say');

say('onather');

webpack.config.js需要这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var path = require('path');

var rootPath = path.resolve('../demo2/src/modules');
console.log('当前rootPath:',rootPath);

module.exports = {
// 入口文件配置
entry: './src/main.js',
// 文件导出的配置
output: {
path: './build',
filename: 'app.js'
},
resolve: {
// 当require的模块找不到时,添加这些后缀再次查找
extentions: ['js'],
// 修改require默认的路径,默认是在./node_modules下,现在修改到demo2/src/modules下
root: rootPath,
// 建立require别名
alias: {
'say': 'say/1.0/say'
}
}
}

要说明注意的点

  • 开头引入了path,这个是node自带模块。path.resolve可以将参数拼接,组成绝对路径。
  • resolve.root的值,可以是字符串,也可以是数组。数组相当于多个路径,一个一个查找
  • resolve.root只支持绝对路径,相对路径不行的
  • 由于我们使用root修改了require处理依赖的工作路径,即不在当前运行的路径了,修改成为了/webpack-demo/demo2/src/modules,所以在alias里,也不能是./开头了,而应该直接写成 say/1.0/say,和上文中的react例子相同。如果继续使用./开头会自动在demo3目录下查找

小结

这篇文章,介绍了如何webpack入门,以及最基础的配置。

配置里面,要有 entry output

处理代码中require的问题,可以配置resolve来解决。这里面有扩展名extentions和别名alias。常用的就这两个,其他方法我很少见别人用。

还提及到了,如果打包出错,想看错误信息,这样执行:webpack --display-error-details