webpack入门-2

这次,研究一些稍微复杂一点的。

之前的操作,我们都是用ES5语法写的,打包直接进行合并/压缩就可以了。仅仅是让webpack负责处理代码require依赖而已。

代码合并例子可以体现,但是代码压缩之前的例子里面是没有的。

我们平时接触的项目,会远远比这复杂。

比如你写angular2,那么它使用typescript,React呢,它用JSX,或者,你更喜欢原生的代码,使用ES6来书写。

不仅如此,CSS也有很多“变种”,比如说LESSSASS等。

再或者,你希望仅仅书写

1
transition: all 2s;

但是最终代码希望是

1
2
3
-webkit-transition: all 2s;
-moz-transition: all 2s;
transition: all 2s;

这样子,自动帮你补全兼容处理。

这时候,就需要对源代码进行加工,加工完之后,变成普通ES5代码或者CSS之后,在进行之前的打包工作。

这就是webpackloaders

所有例子的代码位置

可以访问 github 进行查阅。

DEMO4 体验一次loaders

首先创建项目工程,建立demo4目录

建立一个./src/mian.js文件:

1
2
3
var hello = () => {
console.log('hello loaders!');
};

我们写一句最简单的ES6语法——箭头函数。这样,打包之后,我们可以直接查看生成的JS就可以了,都不用镶嵌到html中在浏览器中查看效果~怎么样?岂不是很简单?

下面,我们要做的事情是,配置webpack.config.js,越简单越好!

我们在原有的基础上,进行精简。之后,再试试我们的loaders功能。

等等!我们要把我们写的./src/main.js先转换为普通的ES5代码,才能使用我们之前的配置方案。

这时候,需要引入其他组件了,根据经验,将ES6转换为ES5,大家用的比较多的是 babel 。我们需要用 npm 安装它。如果你不会用npm,请先查阅最基础的文章。

首先在我们的demo4项目里,

  • 创建npm配置文件:npm init,使用默认参数即可
  • 安装babel插件:npm install -save-dev babel-core babel-preset-es2015
  • 安装webpack的babel适配器:npm install -save-dev babel-loader

其中,babel-core是babel的核心文件,它可以对JS进行转码。但是转成什么呢,我们需要babel-preset-es2015,即ES5代码。

为什么还要安装babel-loader呢?因为babel自己是一个独立的工具,可以直接运行的。我们想在 webpack 中应用它,就需要对它做兼容处理,babel-loader 就是这个兼容器,相当于连接桥。webpack下基本上所有的loaders(可以理解为工具/连接桥),都是xxx-loader这种形式的。

恩,就这些,搞定了。检查下你的package.json文件,是不是和下面的很类似,应该不缺少任何一行!当然,版本号可以和我的有出入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "demo4",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.21.0",
"babel-loader": "^6.2.10",
"babel-preset-es2015": "^6.18.0"
}
}

接下来,我们来写webpack配置文件webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
// 入口文件配置
entry: './src/main.js',
// 文件导出的配置
output: {
path: './build',
filename: 'app.js'
},
module: {
loaders: [{
test: /\.js$/,
loader: 'babel-loader'
}]
},
babel: {
presets: ['es2015']
}
}

loaders是一个数组,因为它可以配置多种处理器。每一种处理器,是一个对象。

处理器对象要包括test字段,用正则或者路径进行匹配要处理的文件,比如上面这个,匹配所有.js结尾的文件。匹配成功后,会用loader内的处理器进行处理。我们这里用babel-loader

loader需要说明的是:

  • 它可以是字符串,也可以直接是数组。如果是字符串,使用!进行分割。
  • 它的插件默认都是xxx-loader形式,可以简写为xxx。比如上面的例子简写是loader: 'babel'
  • 如果是多个loader,那么处理顺序是从右往左。例如 loader: 'aaa!bbb!ccc' ,那么相当于先进行 ccc-loader 处理,之后是 bbb-loader ,最后是 aaa-loader

babel需要有自己的配置文件。我们可以在根目录下创建一个.babelrc的json文件,或者在上面的文件内配置babel字段。甚至还有其他的配置方法(比如在当前处理器对象中,加入query字段),反正条条大路通罗马。

具体的插件如何配置,需要参考插件的文档。

好了,不需要在进行其他配置了,我们直接当前目录下执行webpack命令就可以了:webpack

程序自动生成了./build/app.js。我们只需要打开查看最后一点点代码:

1
2
3
4
5
6
7
8
9
10
11
12
/******/ ([
/* 0 */
/***/ function(module, exports) {

'use strict';

var hello = function hello() {
console.log('hello loaders!');
};

/***/ }
/******/ ]);

你看,babel已经帮我们把ES6转化为ES5了!多么神奇!

loaders的include和exclude参数

这个参数我也查过一些文档,就是说在处理时候包含/排除那些规则的文件,但是目前没有测试成功具体的用法。网络上也没查到有什么特殊的用意。

网上都是这么用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
module: {
loaders: [{
test: /\.js$/,
loader: 'babel-loader',
include: [
path.resolve(__dirname, "app/src"),
path.resolve(__dirname, "app/test")
],
exclude: /node_modules/
}]
}
}

曾经测试过,加入exclude后确实管用,可以让打出来的包更小(但是不会差别很大),但是原因还不详。

DEMO5 编写自己的loader

这次,我们自己简单的实现一个loader,来熟悉下loader的处理流程。

这里,我不做过多的扩展字段说明,可以参考例子代码中的注释。

新建一个目录,叫做demo5。入口文件 ./src/main.js 如下:由于我们不打算加入babel,所以处理不了ES6语法,我们这次试用ES5的语法。

1
2
3
4
5
6
var someComponent = require('./some-component');
var foo = function () {
console.log('hi');
};
foo();
someComponent();

引用的文件./src/some-component.js 如下:

1
2
3
module.exports = function(){
console.log('this is some-component');
};

现在,我们做两个loader,这样可以更清楚的看到它的处理流程。第一个是针对当前处理的文件,通过注释的形式,给当前代码段加入文件名和版本号,第二个更简单,直接在当前代码段最前面加上 'ususe strict' 标志。

先要写好 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
var path = require('path');

module.exports = {
entry: './src/main.js',
output: {
path: './build',
filename: 'app.js'
},
resolve: {
extentions: ['js']
},
module: {
loaders: [{
test: /\.js$/,
loader: [path.resolve(__dirname, './strict-loader'), path.resolve(__dirname, './comment-loader')].join('!'),
//自有loader需要使用绝对路径,否则代码中的require部分后的代码,将无法找到!
}]
},
commentLoader: {
str: 'v1.0'
}
}

这里要注意的是:

  • 引用的每个loader,需要使用绝对路径,这里我用 path.resolve 来解决。使用相对路径,require会找不到的!
  • loader如果是字符串或者数组,一定是从右往左写的。
  • commnetLoader字段,我们稍后学习在loader里面进行读取使用。相当于是loader的参数。

给代码段加入文件名和版本号注释

我们就在根目录建立./comment-loader.js文件:

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

module.exports = function (source) {
console.log('====进入comment模块====');
console.log(source);
console.log('======================');

var param = this.options['commentLoader'] || {};
var str = param.str || '';
var file = path.parse(this.resourcePath);
var ret = '\n//--------' + file.base + ' ' + str + '--------\n' + source + '\n//------------------------';

//return ret;//同步模式,单参数返回(只能返回处理后的代码)
//this.callback(null, ret, null); //同步模式,多参数返回,第一个参数不详,第二个为处理后的代码,第三个参数为map
var cb = this.async();//异步模式,调用async函数

setTimeout(() => {
cb(null, ret, null);
}, 2000);

};

解释说明

所有的loader可以接收两个参数,分别为 source map,第一个是当前的源代码,第二个是sourceMap。第二个一般用不到,这里我们省略。

当前环境内的 this ,比较复杂,是webpack提供的一些方法。

通过打印 source 参数,我们可以看到内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
====进入comment模块====
var someComponent = require('./some-component');
var foo = function () {
console.log('hi');
};
foo();
someComponent();
======================

====进入comment模块====
module.exports = function(){
console.log('this is some-component');
};
======================

可以看到,访问了几个符合条件的文件,就要被执行几次。而参数 source 就是单纯的文件内容而已。

之后,我发现 this.options 内包含了整个webpack.config.js内容,故直接采用 var param = this.options['commentLoader'] || {}; 方式获取 commentLoader 字段内容,当然做了兼容处理,如果不存在则默认配置一个对象。

下文也是, var str = param.str || '' ,读取 param.str字段,不存在则为空。

通过测试发现, this.resourcePath 可以获得当前正在处理的文件路径。我使用NodeJS的 path.parse 进行解析。下文就可以直接用 file.base 来获得文件名了。

最后,进行返回数据。

loader返回数据的三种方案:

  • 代码是同步模式,可以最后直接返回,比如例子可以写成: return ret;。这个很明显有个缺陷,那就是:只能返回一个代码字符串!最上面说了,可以传入2个参数,那么第二个map参数呢?这个方法解决不了。
  • 代码是同步模式,可以解决直接 return 的单参数问题: this.callback(null, ret, null) 。第一个参数不详,第二个为处理后的代码,第三个参数为map。
  • 代码是异步模式,首先需要在同步代码中,调用 var cb = this.async() 。之后就可以在异步中调用 cb() 了。参数和同步模式一样的。

上面的例子,为异步模式,意思为延时两秒后,进行回调。

这个看懂之后,就好办了。我们来写./strict-loader.js文件:

1
2
3
4
5
6
module.exports = function (source) {
console.log('====进入strict模块====');
console.log( source);
console.log('======================');
return '\'use strict\';\n' + source;
};

这个就不解释了。

最终,我们执行webpack,首先看看终端中的显示内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
====进入comment模块====
var someComponent = require('./some-component');
var foo = function () {
console.log('hi');
};
foo();
someComponent();
======================
====进入strict模块====

//--------main.js v1.0--------
var someComponent = require('./some-component');
var foo = function () {
console.log('hi');
};
foo();
someComponent();
//------------------------
======================


====进入comment模块====
module.exports = function(){
console.log('this is some-component');
};
======================
====进入strict模块====

//--------some-component.js v1.0--------
module.exports = function(){
console.log('this is some-component');
};
//------------------------
======================

可以很清晰的看到,从入口开始,遇到一个符合条件的文件,开始进入loader处理。首先将源码带入 comment-loader ,之后将输出的代码带入 strict-loader。这样完成一个文件之后,在进行下一个文件。

再看看最终生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

'use strict';

//--------main.js v1.0--------
var someComponent = __webpack_require__(1);
var foo = function () {
console.log('hi');
};
foo();
someComponent();
//------------------------

/***/ },
/* 1 */
/***/ function(module, exports) {

'use strict';

//--------some-component.js v1.0--------
module.exports = function(){
console.log('this is some-component');
};
//------------------------

/***/ }
/******/ ]);

也是非常符合预期的。每个代码模块标有文件名和版本号,最上面还有严格模式标签。

完成。