webpack深入浅出

使用

使用webpack打包 – 初窥篇

1
2
3
4
5
6
7
8
const path = require('path')
module.exports = {
entry: './src/index.js', // 入口文件
output: { // 出口
path: path.resolve(__dirname, 'dist'), // 导出文件的目录
filename: 'bundle.js' // 导出的文件名
}
}
1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html>
<head>
...
</head>
<body>
...
<script src="dist/bundle.js"></script>
</body>
</html>

webpack 概念

webpack 是一个现代Javascript应用程序的静态模块打包器(module bundler)当webpack处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。

从webpack v4.0.0开始,可以不用引入一个配置文件。然而,webpack仍然是高度可配置的。

  • 入口(entry)
  • 输出 (output)
  • loader
  • 插件 (plugins)

入口 (entry)

简单的说就是构建的入口文件,webpack会根据这个文件去寻找它的直接或间接依赖项,然后逐一去处理

webpack.config.js

1
2
3
4
5
6
7
8
9
10
// 单文件入口
module.exports = {
entry: './src/main.js'
}
// 多文件入口
module.exports = {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
}

出口 (output)

output属性告诉webpack在哪里输出它创建的bundles,以及如何命名这些文件,默认值为./dist。基本整个应用程序结构,都会被编译到指定的输出路径的文件夹中。你可以通过在配置中指定一个output字段,来配置这些处理过程

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
25
26
27
const path = require('path')
module.exports = {
entry: './src/main.js', // 入口文件
output: { // 出口
path: path.resolve(__dirname, 'dist'), // 导出文件的目录
filename: 'bundle.js' // 导出的文件名
}
}

// 多出口
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name].js',
path: __dirname + '/dist'
}
}
// 写入到硬盘:./dist/app.js, ./dist/search.js

// 以下是使用 CDN 和资源 hash 的复杂示例:
output: {
path: "/home/proj/cdn/assets/[hash]",
publicPath: "http://cdn.example.com/assets/[hash]/"
}

loader

loader让webpack能够去处理那些非Javascript文件(webpack自身只能解析javascript)loader可以将所有类型的文件转为webpack能够处理的有效模块。然后就so good了

在更高层面,webpack的配置中loader有两个目标:
1、test 属性,用于标识出应该被对应的loader进行转换的某个或某些文件。
2、use属性,表示进行转换时,应该使用哪个loader。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const path = require('path')
const config = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{test: /\.txt$/, use: 'raw-loader'}
]
}
}

以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(compiler) 如下信息:

“嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 ‘.txt’ 的路径」时,在你对它打包之前,先使用 raw-loader 转换一下。”

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或”加载”模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

示例

例如,你可以使用 loader 告诉 webpack 加载 CSS 文件,或者将 TypeScript 转为 JavaScript。为此,首先安装相对应的 loader:
npm install –save-dev css-loader
npm install –save-dev ts-loader
然后指示 webpack 对每个 .css 使用 css-loader,以及对所有 .ts 文件使用 ts-loader:

1
2
3
4
5
6
7
8
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: 'css-loader' },
{ test: /\.ts$/, use: 'ts-loader' }
]
}
};

使用loader

在你的应用程序中,有三种使用 loader 的方式:

  • 配置(推荐):在 webpack.config.js 文件中指定 loader。
  • 内联:在每个 import 语句中显式指定 loader。
  • CLI:在 shell 命令中指定它们。
配置[Configuration]

module.rules 允许你在 webpack 配置中指定多个 loader。 这是展示 loader 的一种简明方式,并且有助于使代码变得简洁。同时让你对各个 loader 有个全局概览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 解析css先用css-loader再用style-loader。所以要用下面的顺序引入。(嗯,也可以说是倒叙)

module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true
}
}
]
}
]
}

内联

可以在 import 语句或任何等效于 “import” 的方式中指定 loader。使用 ! 将资源中的 loader 分开。分开的每个部分都相对于当前目录解析。

import Styles from 'style-loader!css-loader?modules!./styles.css';

通过前置所有规则及使用 !,可以对应覆盖到配置中的任意 loader。

选项可以传递查询参数,例如 ?key=value&foo=bar,或者一个 JSON 对象,例如 ?{“key”:”value”,”foo”:”bar”}。

CLI

你也可以通过 CLI 使用 loader:

webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'
这会对 .jade 文件使用 jade-loader,对 .css 文件使用 style-loader 和 css-loader。

loader 特性

  • loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript。
  • loader 可以是同步的,也可以是异步的。
  • loader 运行在 Node.js 中,并且能够执行任何可能的操作。
  • loader 接收查询参数。用于对 loader 传递配置。
  • loader 也能够使用 options 对象进行配置。
  • 除了使用 package.json 常见的 main 属性,还可以将普通的 npm 模块导出为 loader,做法是在 package.json 里定义一个 loader 字段。
  • 插件(plugin)可以为 loader 带来更多特性。
  • loader 能够产生额外的任意文件。
  • loader 通过(loader)预处理函数,为 JavaScript 生态系统提供了更多能力。 用户现在可以更加灵活地引入细粒度逻辑,例如压缩、打包、语言翻译和其他更多。

翻译成人话

众所周知,webpack是运行在node.js之上的,一个loader其实就是node.js模块,这个模块需要导出一个函数。
这个导出的函数的工作就是获得处理前的内容,对原内容执行处理后,返回处理后的内容。
下面一个简单的loader

1
2
3
4
5
module.exports = function (source) {
// source 为 compiler 传递给Loader的一个文件的原内容
// 该函数需要返回处理后的内容,这里不做处理,直接返回
return source
}

由于 Loader 运行在 Node.js 中,你可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用:

1
2
3
4
const sass = require('node-sass');
module.exports = function(source) {
return sass(source);
}

loader进阶

总说loader可以链式操作,那么loader怎么获取上一个loader传入的数据呢?
很简单,因为webpack给我们内置了一个方法:‘loader-utils’ 惊喜不?意外不?

1
2
3
4
5
const loaderUtils = require('loader-utils')
module.exports = function (source) {
const options = loaderUtils.getOptions(this) // 这样就获得了^_^
return source
}

有时loader还需要返回内容之外的东西。举个例子:用babel-loader转换es6代码,除了输出内容,还要输出转换后的es5代码对应的Source Map,以方便调试源码。由于多返回了一个,
webpack提供了callback回调函数。

1
2
3
4
5
6
7
module.exports = function (source) {
// 通过this.callback告诉webpack返回的结果
this.callback(null, source, sourceMaps)
// 当使用this.callback返回内容时,该Loader必须返回undefined。
// 用以告知webpack该loader返回的结果在this.callback中,而不是return中
return
}

那么,this.callback的使用方法是什么呢?

1
2
3
4
5
6
7
8
9
10
11
this.callback(
// 当无法转换原内容时,给webpack返回一个Error
err: Error | null,
// 原内容转换后的内容
content: string | Buffer,
// 用于把转换后的内容得出原内容的Source Map, 方便调试
sourceMap?: SourceMap,
// 如果本次转换为原生内容生成了AST语法树,可以把这个AST返回,
// 以方便之后需要AST的Loader复用该AST,以避免重复生成AST,提升性能
abstractSyntaxTree?: AST
)

Source Map 的生成很耗时,通常在开发环境下才会生成 Source Map,其它环境下不用生成,以加速构建。
为此 Webpack 为 Loader 提供了 this.sourceMap API 去告诉 Loader 当前构建环境下用户是否需要 Source Map。
如果你编写的 Loader 会生成 Source Map,请考虑到这点。

如果loader中需要异步请求怎么办?this.async()

1
2
3
4
5
6
7
8
module.exports = function (source) {
// 告诉webpack本次转换时异步的,Loader会在callback中回调结果
var callback = this.async()
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通过callback返回异步执行后的结果
callback(err, result, sourceMaps, ast)
})
}

默认情况下,webpack传给Loader的原内容都是UTF-8格式编码的字符串。
那么问题来了,有时候我们要处理的并不是文本文件,比如文件上传。
这时候webpack又给Loader传二进制格式的数据
为此,loader要加点东西了

1
2
3
4
5
6
7
8
9
10
module.exports = function (source) {
// 在exports.raw===true时webpack传给Loader的source时BUffer类型的
source instanceof Buffer === true
// Loader 返回的类型也可以时Buffer类型的
// 在exports.raw !== true时,Loader也可以返回Buffer类型的结果
return source
}
// 这句话很关键!!!
// 通过exports.raw属性告诉webpack该Loader是否需要二进制数据
module.exports.raw = true

webpack(保姆)
为什么叫webpack保姆呢?因为他真的做了很多事情,反正我是很喜欢,嘎嘎
loader操作很多都需要大量计算,所以非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变的非擦缓慢。
这时候保姆上场了,他缓存了所有的loader的处理结果。除非你要处理的文件或者依赖的文件发生改变。

这时候有人就说了,我就是要每次构建都执行,怎么办?你不服啊,我就要。可以,配置一下咯

1
2
3
4
5
module.exports = function (source) {
// 关闭该loader缓存功能
this.cacheable(false)
return source
}

说了这么多。好想亲自写一写。但是项目里的loader都是通过npm安装的。我总不能上传到npm仓库把,那不是扯鸡鸡了。
别怕。有Npm link和ResolveLoader
Npm link: 专门用于开发和调试本地 Npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 Npm 模块。
由于是通过软链接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。
完成 Npm link 的步骤如下:

  1. 确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
  2. 在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
  3. 在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 Npm 模块链接到项目的 node_moduels 下,其中的 loader-name 是指在第1步中的 package.json 文件中配置的模块名称。
    链接好 Loader 到项目后你就可以像使用一个真正的 Npm 模块一样使用本地的 Loader 了。

我不喜欢Npm link,别问为什么,就是不喜欢

ResolveLoader: ResolveLoader 用于配置 Webpack 如何寻找 Loader
默认情况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 需要修改 resolveLoader.modules。

假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:

1
2
3
4
5
6
module.exports = {
resolveLoader:{
// 去哪些目录下寻找 Loader,有先后顺序之分
modules: ['node_modules','./loaders/'],
}
}

加上以上配置后, Webpack 会先去 node_modules 项目下寻找 Loader,如果找不到,会再去 ./loaders/ 目录下寻找。

彩蛋
使用绝对路径的方式

1
2
3
4
5
6
7
8
9
module: {
rules: [
{
test: /\.js$/,
use: path.resolve(__dirname, 'loaders/a.js'),
exclude: /node_modules/
}
]
},

插件(plugins)

loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务

想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建它的一个实例。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 用于访问内置插件
const config = {
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
};

module.exports = config;

模式

通过选择 development 或 production 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化

1
2
3
module.exports = {
mode: 'production'
};
文章目录
  1. 1. 使用
  2. 2. 使用webpack打包 – 初窥篇
  3. 3. webpack 概念
    1. 3.1. 入口 (entry)
    2. 3.2. 出口 (output)
    3. 3.3. loader
      1. 3.3.1. 示例
      2. 3.3.2. 使用loader
        1. 3.3.2.1. 配置[Configuration]
        2. 3.3.2.2. 内联
        3. 3.3.2.3. CLI
      3. 3.3.3. loader 特性
      4. 3.3.4. 翻译成人话
    4. 3.4. 插件(plugins)
    5. 3.5. 模式
|