webpack是一个强大的模块打包工具,之所以强大的原因在于它拥有灵活、丰富的插件机制。webpack本质上是一个静态模块打包器,当 webpack 处理应用程序时,会根据代码的内容解析模块依赖,会递归构建一个依赖关系图,帮助我们把多个模块的代码打包成一个或多个 bundle。webpack`会把我们项目中使用到的多个代码模块(可以是不同文件类型),打包构建成项目运行仅需要的几个静态文件。

webpack优势在于:

  1. webpack是以CommonJS的形式来书写脚本的,但对AMD/CMD的支持也很全面,方便旧项目进行代码迁移;
  2. 不仅仅zhichi JS支持模块化打包,还支持样式(less/sass)、图片等其它静态资源;
  3. 开发便捷,能替代部分grunt/gulp的工作,比如打包、压缩混淆、图片转base64等;
  4. 扩展性强,插件机制完善。

理解模块化

模块化是指将一个复杂的系统分解为多个模块以方便编码。

CommonJS

CommonJS是一种被广泛使用的javascript模块化规范,其核心思想是:通过require方法来同步加载依赖的其他模块,通过module.exports导出需要暴露的接口。CommonJS规范的流行得益于Node.js采用了这种方式,后来这种方式被引入到了网页开发中。

// 导入
const moduleA = require('./moduleA');
// 导出
module.exports = moduleA.someFunc;
1
2
3
4

CommonJS的优点:

  • 代码可复用于Node.js环境下并运行,例如做同构应用;
  • 通过Npm发布的很多第三方模块都采用了CommonJS规范。

CommonJS的缺点:这样的代码无法直接运行在浏览器环境下,必须通过工具转换成标准的ES5。

AMD

AMD也是一种javascript模块化规范,与CommonJS最大的不同在于:它采用了异步的方式去加载依赖的模块。AMD规范主要用于解决针对浏览器环境的模块化问题,最具代表性的实现是requirejs

// 定义一个模块
define('module', ['dep'], function(dep) {
    return exports;
});
// 导入和使用
require(['module'], function(module) {
});
1
2
3
4
5
6
7

AMD的优点:

  1. 可在不转换代码的情况下直接在浏览器中运行;
  2. 可异步加载依赖;
  3. 可并行加载多个依赖;
  4. 代码可运行在浏览器环境和Node.js环境下。

AMD的缺点:在javascript运行环境没有原生支持AMD,需要先导入实现了AMD的库后才能正常使用。

ES6模块化

ES6模块化是国际标准化组织ECMA提出的javascript模块化规范,它在语言层面上实现了模块化。浏览器厂商和Node.js都宣布要原生支持该规范,它将逐渐取代CommonJS和AMD规范,成为浏览器和服务器通用的模块化解决方案。

// 导入
improt React, {Component} from 'react';
// 导出
export function hello() {};
export default {
 // ...
}
1
2
3
4
5
6
7

ES6模块化虽然是终极模块化方案,但是它的缺点在于:目前无法直接运行在大部分javascript运行环境下,必须通过工具转换成标准的ES5后才能正常运行。

Webpack 2版本开始,Webpack已经内置了对ES6、CommonJS、AMD模块化语句的支持。

webpack的核心概念

  1. Entry: 入口文件配置,Webpack执行构建的第一步将从Entry开始,可抽象成输入。
  2. Module: 模块,在Webpack里一切皆模块,一个模块对应一个文件。Webpack会从配置的Entry开始递归找出所有依赖的模块。最常用的是rules配置项,功能是匹配对应的后缀,从而针对代码文件完成格式转换和压缩合并等指定的操作。
  3. Chunk: 代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。
  4. Loader: 模块转换器,用于把模块原内容按照需求转换成新内容,配合Module模块中的rules中的配置项来使用。
  5. Plugin: 扩展插件,在Webpack构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。
  6. Output: 输出结果,在Webpack经过一系列处理并得出最终想要的代码后输出结果。

特别注意:webpack 4不是必须要有配置文件。它将查找./src/index.js作为默认入口点。 而且,它会在./dist/main.js中输出模块包。

webpack执行流程

webpack启动后,会根据entry里配置的入口文件开始递归解析entry所依赖的所有module,每找到一个module, 就会根据配置的loader去找相应的转换规则,对module进行转换后,再解析当前module所依赖的module,这些模块会以entry为分组,一个entry和所有相依赖的module也就是一个chunk。最后,webpack会把所有chunk转换成文件输出,在整个流程中,webpack会在恰当的时机执行plugin的逻辑。

安装和使用webpack

需要注意:webpack4把webpack命令行工具抽离成了独立包webpack-cli,因此在使用时需要单独安装。

// npm全局安装
npm install webpack webpack-cli -g

// yarn全局安装
yarn global add webpack webpack-cli

// 全局执行webpack命令
webpack --help

// 在项目目录中安装
npm install webpack webpack-cli -D
// 安装最新的稳定版本
npm i -D webpack webpack-cli
// 安装指定版本
npm i -D webpack@<version> webpack-cli@<version>
// 安装最新的体验版本
npm i -D webpack@beta webpack-cli@beta
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

特别注意

  1. 一般不推荐全局安装webpack,原因是可防止不同的项目因依赖不同版本的webpack而导致冲突。
  2. webpack-cli 是使用 webpack 的命令行工具,在 webpack4.x 版本之后不再作为 webpack 的依赖了,我们使用时需要单独安装这个工具。

webpack4的零配置

项目中没有webpack.config.js情况下,命令行直接运行webpack,webpack4不再像webpack3一样,提示未找到配置文件:

而是提示:

修改后可以发现零配置下系统的默认配置为:

  1. 入口文件为:/src/index.js,打包输出文件为:/dist/main.js
  2. 未传--mode参数时,默认是--mode production,会进行压缩混淆。传入--mode development指定为开发环境打包。

webpack cli执行

如果命令行直接webpack会运行全局安装webpack,如果想要运行当前目录下的webpack,可以执行./node_modules/.bin/webpack。或者直接使用:npx webpack

npxnpm 5.2.0及以上内置的包执行器,npx webpack --mode development会直接找项目的/node_modules/.bin/里面的命令执行,方便快捷。

npm scripts

使用npm脚本,配置好之后直接npm run xxx

// package.json
"scripts": {
    "dev": "webpack --mode development" // 启动开发环境
}
1
2
3
4

配置结构

webpack.config.js:

module.exports = {
    mode: 'development', // development|production
	entry: '', // 入口配置
	output: {}, // 输出配置
	module: {}, // 放置loader加载器,webpack本身只能打包commonjs规范的js文件,用于处理其他文件或语法
	plugins: [], // 插件,扩展功能
	// 以下内容进阶篇再涉及
	resolve: {}, // 为引入的模块起别名
	devServer: {} // webpack-dev-server
};
1
2
3
4
5
6
7
8
9
10

更多配置详见

引入了mode配置项,开发者可在none,development(开发 ) 以及 production(产品)三种模式间选择。该配置项缺省情况下默认使用production 模式。

webpack4有两种模式:development和production,默认为production。

// 生产环境
webpack --mode production

// 开发环境
webpack --mode development
1
2
3
4
5

package.json文件中的scripts字段中进行如下配置:

"scripts": {
    "build": "webpack --mode production --config webpack.production.config.js",
    "dev": "webpack-dev-server --mode development --open"
  }
1
2
3
4

入口(entry)

entry是配置模块的入口,可抽象成输入,Webpack执行构建的第一步将从入口开始,搜寻及递归解析出所有入口依赖的模块。

webpack 在构建时需要有入口文件。webpack 会读取这个文件,并从它开始解析依赖,然后进行打包。webpack4 默认从项目根目录下的 ./src/index.js 中加载入口模块。默认的入口文件就是 ./src/index.js

我们常见的项目中,如果是单页面应用,那么可能入口只有一个;如果是多个页面的项目,那么经常是一个页面会对应一个构建入口。

入口可以使用 entry 字段来进行配置,webpack 支持配置多个入口来进行构建:

单一入口

module.exports = {
  entry: './src/index.js'
}

// 上述配置等同于
module.exports = {
  entry: {
    main: './src/index.js'
  }
}
1
2
3
4
5
6
7
8
9
10

多入一出

entry传入数组相当于将数组内所有文件都打包到bundle.js中。

const path = require('path');

module.exports = {
    entry: ['./src/index.js', './src/index2.js'], // 使用数组来对多个文件进行打包
    // 可以理解为多个文件作为一个入口,webpack会解析两个文件的依赖后进行打包
	output: {
		filename: 'bundle.js', // 打包输出文件名
		path: path.join(__dirname, './dist') // 打包输出路径(必须绝对路径,否则报错)
	}
};
1
2
3
4
5
6
7
8
9
10

多入多出

  1. entry传入对象,key称之为chunk,将不同入口文件分别打包到不同的js;
  2. output.filename改为用中括号占位来命名,从而生成多个文件,nameentry中各个chunk
const path = require('path');

module.exports = {
    entry: { // 入口文件,传入对象,定义不同的chunk(如app, utils)
        app: './src/index.js',
        utils: './src/utils.js'
    },
    output: {
        // filename: 'bundle.js', // 此时因为有多个chunk,因此不能只定义一个输出文件,否则报错
        filename: '[name].[hash].js',
        path: path.join(__dirname, './dist')
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13

Chunk的名称

Webpack会为每个生成的Chunk取一个名称,Chunk的名称和Entry的配置有关:

  1. 如果entry是一个string或array,就只会生成一个Chunk,这时Chunk的名称是main
  2. 如果entry是一个object,就可能会出现多个Chunk,这时Chunk的名称是object键值对中键的名称。

输出(output)

filename

output.filename配置输出文件的名称,为string类型。如果只有一个输出文件,则可以将它写成静态不变的:

filename: 'bundle.js'
1

webpack的输出即指webpack最终构建出来的静态文件,可以看看上面webpack官方图片右侧的那些文件。当然,构建结果的文件名、路径等都是可以配置的,使用 output 字段:

module.exports = {
  // ...
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: 'http://cdn.eaxmple.com/assets/'
  },
}

// 或者多个入口生成不同文件
module.exports = {
  entry: {
    foo: './src/foo.js',
    bar: './src/bar.js',
  },
  output: {
    filename: '[name].js', //[name]是entry里的key
    path: __dirname + '/dist',
  },
}

// 路径中使用 hash,每次构建时会有一个不同 hash 值,避免发布新版本时线上使用浏览器缓存
module.exports = {
  // ...
  output: {
    filename: '[name].js',
    path: __dirname + '/dist/[hash]',
  },
}
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

publicPath属性

publicPath属性: 指定了在浏览器中用什么地址来引用静态文件,包括图片、js脚本以及css样式加载的地址,一般用于线上发布以及CDN部署的时候使用。具体例子如下:

<link href="http://cdn.eaxmple.com/assets/main.css" rel="stylesheet"></head>
<body>
    <div id="root"></div>
	<script type="text/javascript" src="http://cdn.eaxmple.com/assets/bundle.7e74c10f3f0fabe41a65.js">
	</script>
</body>
1
2
3
4
5
6

之所以会自动使用publicPath属性中设置的值,主要在于使用了html-webpack-plugin插件来自动生成项目首页文件,这样一来,link中的href属性和script中的src属性都会被自动替换。

Resolve

Webpack在启动后会从配置的入口模块出发找出所有依赖的模块,Resolve配置Webpack如何寻找模块对应的文件。

alias

resolve.alias配置项通过别名来将原来导入路径映射成一个新的导入路径。

module.exports = {
  //...
  resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
      Templates: path.resolve(__dirname, 'src/templates/')
    }
  }
};
1
2
3
4
5
6
7
8
9
// 原来这样访问
import Utility from '../../utilities/utility';
// 现在可以这样访问了
import Utility from 'Utilities/utility';
1
2
3
4

mainFields

有一些第三方模块会针对不同的环境提供几份代码。例如分别提供采用了ES5和ES6的两份代码,这两份代码的位置写在了package.json文件里,代码如下:

{
    "jsnext:main": "es/index.js", // 采用ES6语法的代码入口文件
    "main": "src/index.js" // 采用ES5语法的代码入口文件
}
1
2
3
4

Webpack会根据mainFields的配置去决定优先采用哪份代码,mainFields默认如下:

module.exports = {
  //...
  resolve: {
    mainFields: ['browser', 'main']
  }
};
1
2
3
4
5
6

Webpack会按照数组里的顺序在package.json文件里寻找,只会使用找到的第一个文件。假如我们想优先采用ES6的代码,则可以如下配置:

mainFields: ['jsnext:main', 'browser', 'main']
1

extensions

在导入语句没带文件后缀时,Webpack会自动带上后缀去尝试访问文件是否存在。resolve.extensions用于配置在尝试过程中用到的后缀列表,默认是:

module.exports = {
  //...
  resolve: {
    extensions: ['.ts', '.js', '.json']
  }
};
1
2
3
4
5
6

也就是说,当遇到require('./data')这样的导入语句时,Webpack会先寻找./data.ts文件,如果该文件不存在,就去寻找./data.js文件,如果该文件不存在,就去寻找./data.json文件,如果还是找不到,就报错。

modules

resolve.modules配置Webpack去哪些目录下寻找第三方模块,默认只会去node_modules目录下寻找。有时项目里会有一些模块被其他模块大量依赖和导入,由于其他模块的位置不定,针对不同的文件都要计算被导入的模块文件的相对路径,这个路径有时会很长,就像import '../../../components/button',这就可以利用resolve.modules配置项进行优化。假设那些大量被导入的模块都在./src/components目录下,则进行如下配置即可:

resolve: {
    modules: ['./src/components', 'node_modules']
  }
1
2
3

之后可以简单的通过import 'button'导入。

descriptionFiles

resolve.descriptionFiles配置描述第三方模块的文件名称,默认为:package.json

module.exports = {
  //...
  resolve: {
    descriptionFiles: ['package.json']
  }
};
1
2
3
4
5
6

enforceExtension

resolve.enforceExtension默认值为false,如果被配置为true,则所有导入语句都必须带文件后缀,例如设置之前import './foo'能正常工作,开启后就必须写成import './foo.js'

module.exports = {
  //...
  resolve: {
    enforceExtension: false
  }
};
1
2
3
4
5
6

enforceModuleExtension

resolve.enforceModuleExtensionresolve.enforceExtension的作用类似,但是enforceModuleExtension只对node_modules下的模块生效。enforceModuleExtension通常搭配enforceExtension使用,在enforceExtension: true时,因为安装的第三方模块中大多数导入语句都没有带文件的后缀,所以这时候通过设置enforceModuleExtension: false来兼容第三方模块。

其他配置

Target

Target配置项可以让Webpack构建出针对不同运行环境的代码。 6dce3a03d834d0b06de15680b1221279.png

在设置target: 'node'时,在源代码中导入Node.js原生模块的语句require('fs')将会保留,fs模块的内容不会被打包到Chunk中。

Devtool

devtool配置Webpack如何生成Source Map,默认值是false,即不生成Source Map,若想为构建出的代码生成Source Map以方便调试,可以这样配置:

module.export = {
    devtool: 'source-map'
}
1
2
3

Watch and WatchOptions

watch配置项用来配置Webpack的监听模式,支持监听文件更新,在文件发生变化时重新编译。在使用Webpack时,监听模式默认是关闭的,打开需进行如下配置:

module.exports = {
  //...
  watch: true
};
1
2
3
4

在使用DevServer时,监听模式默认开启。除此之外,还提供了watchOptions配置项来更灵活地控制监听模式,具体配置如下:

module.exports = {
  // 只有在开启监听模式时,watchOptions才有意义
  // 默认为false,也就是不开启
  watch: true,
  // 监听模式运行时的参数
  // 在开启监听模式时才有意义
  watchOptions: {
    // 不监听的文件或文件夹,支持正则匹配
    // 默认为空
    ignored: /node_modules/,
    // 监听到变化发生后等300ms再去执行动作,截流
    // 防止文件更新太快而导致重新编译频率太快。默认为300ms
    aggregateTimeout: 300,
    // 判断文件是否发生变化是通过不停地询问系统指定文件有没有变化实现的
    // 默认每秒询问1000次
    poll: 1000
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

externals

Externals用来告诉Webpack要构建的代码中使用了哪些不用被打包的模块,也就是说这些模板是外部环境提供的,Webpack在打包时可以忽略它们。

有些Javascript运行环境可能内置了一些全局变量或者模块,例如:在HTML的HEAD标签通过以下代码引入jQuery:

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous">
</script>
1
2
3
4
5

这时,全局变量jQuery就会被注入网页的Javascript运行环境里。

如果想在使用模块化的源代码里导入和使用jQuery,可能需要这样:

import $ from 'jquery';

$('.my-element').animate(/* ... */);
1
2
3

构建后我们会发现输出的Chunk里包含的jQuery库的内容,这导致jQuery库出现了两次,浪费加载流量,最好是Chunk里不会包含jQuery库的内容。

Externals配置项就是用于解决这个问题的。通过Externals可以告诉Webpack在Javascript运行环境中已经内置了哪些全局变量,不用将这些全局变量打包到代码中而是直接使用它们。具体配置如下:

module.exports = {
  //...
  externals: {
    jquery: 'jQuery'
  }
};
1
2
3
4
5
6

resolveLoader

resolveLoader配置项用来告诉Webpack如何去寻找Loader,因为在使用Loader时是通过其包名称去引用的,Webpack需要根据配置的Loader包名去找到Loader的实际代码,以调用Loader去处理源文件。默认配置如下:

module.exports = {
  //...
  resolveLoader: {
    // 去哪个目录下寻找Loader
    modules: [ 'node_modules' ],
    // 入口文件的后缀
    extensions: [ '.js', '.json' ],
    // 指明入口文件位置的字段
    mainFields: [ 'loader', 'main' ]
  }
};
1
2
3
4
5
6
7
8
9
10
11

使用loader

webpack 中提供一种处理多种文件格式的机制,便是使用 loader。我们可以把 loader 理解为是一个转换器,负责把某种文件格式的内容转换成 webpack 可以支持打包的模块。

在没有添加额外插件的情况下,webpack 会默认把所有依赖打包成 js 文件,如果入口文件依赖一个 .hbs 的模板文件以及一个 .css 的样式文件,那么我们需要 handlebars-loader 来处理 .hbs 文件,需要 css-loaderstyle-loader来处理 .css 文件,最终把不同格式的文件都解析成 js 代码,以便打包后在浏览器中运行。

当我们需要使用不同的 loader 来解析处理不同类型的文件时,我们可以在 module.rules 字段下来配置相关的规则。

编译ES6

安装相关依赖包

// 安装bable相关依赖包
npm install babel-loader babel-core babel-preset-env babel-preset-react babel-preset-stage-0 -D
// 安装react相关工具包
npm install --save-dev react react-dom
1
2
3
4
  • babel-core:babel核心包
  • babel-loader
  • babel-preset-env:定案内语法编译(babel-preset-es2015已废弃)
  • babel-preset-stage-0:预案内语法编译

在项目根目录新建.babelrc文件

进行如下配置:

{
  //【重要】顺序右到左,先处理高级或特殊语法
  "presets": ["env", "stage-0", "react"]
}
1
2
3
4
// webpack.config.js
module: {
	rules: [
		{
	        test: /\.js$/,
	        exclude: /node_modules/, //忽略node_modules目录
	        include: /src/, //只处理src目录
	        use: {
	            loader: 'babel-loader'
            }
        }
	]
}
1
2
3
4
5
6
7
8
9
10
11
12
13

css相关loader

// 安装loader
npm i -D style-loader css-loader
1
2
module: {
    rules: [
        {
            test: /\.css$/,
            use: ['style-loader', 'css-loader?minimize']
        }
    ]
}
1
2
3
4
5
6
7
8
  • use属性的值是一个由loader名称组成的数组,loader的执行顺序是由后到前的;
  • 每个loader都可以通过URL querystring的方式传入参数,例如上述代码中'css-loader?minimize'中的minimize就是告诉css-loader要开启css压缩。

向loader传入属性的方式除了可以通过URL querystring实现,还可以通过Object实现,配置如下:

module: {
    rules: [
        {
            test: /\.css$/,
            use: ['style-loader',
                {
                    loader: 'css-loader',
                    options: {
                        minimize: true
                    }
                }
            ]
        }
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

style-loader的工作原理是:将css的内容用javascript里的字符串存储起来,在网页执行javascript时通过DOM操作,动态地向HTML head标签里插入HTML style标签。

处理css(内联)

处理less和css等非js资源,需要安装相对应的loader

npm install -D style-loader css-loader less less-loader
1
  1. style-loader:处理其中的@import和url()
  2. css-loader:将css内联到页面中
  3. less-loader:less编译,处理less文件
// index.js
import './style/index.css';
import './style/test.less';

// webpack.config.js
module.exports = {
	...
	module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader', 'less-loader'] // 从右到左,loader安装后无需引入可直接使用
            }
        ]
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

最终cssstyle的形式内联进页面。

postcss-loader

postcss-loader用来自动给css属性加浏览器兼容性前缀。需要注意的是webpack4.x版本后,postcss-loader需要结合postcss-cssnext来使用,而不是autoprefixer。在此之前,需要先在根目录下创建一个postcss.config.js文件(类似于.babelrc文件)。

// postcss.config.js相关配置
module.exports = {
    plugins: [
        require('postcss-cssnext')
    ]
}
1
2
3
4
5
6
// 处理scss文件
// 特别注意除了安装sass-loader之外,还需要安装node-sass
npm install sass-loader node-sass postcss-loader postcss-cssnext -D
1
2
3
// 相关配置
rules:[
    {
        test: /\.css$/,
        use: [
            'style-loader',
            {
                loader:'css-loader',
                options:{
                    modules:true, //css模块化
                    minimize: true //在开发环境下压缩css
                }
            }
        ],
        include: path.resolve(__dirname, 'src'), //限制范围,提高打包速度
        exclude: /node_modules/ //排除打包目录
    }, {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', {
            loader:'less-loader',
            options:{
                modifyVars:{
                    "color":"#ccc"  //设置变量
                }
            }
        }]
    },
    {
        test: /\.scss$/,
        use: ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: ['css-loader', {
                loader: 'postcss-loader',
                options: {
                    config: {
                      path: './postcss.config.js'//得在项目根目录创建此文件
                    }
                }
            }, 'sass-loader']
        }),
        include: path.resolve(__dirname, 'src'), //限制范围,提高打包速度
        exclude: /node_modules/ //排除打包目录
    }
]
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
35
36
37
38
39
40
41
42
43
44

图片(字体/svg)处理

对于图片、字体这些资源的处理。我们希望做到:

  1. 图片能正确被webpack打包,小于一定大小的图片直接base64内联;
  2. 打包之后各个入口(css/js/html)还能正常访问到图片,图片引用路径不乱。
// 安装对应的loader
npm install -D url-loader file-loader
1
2

url-loader: 小于limit值时,直接base64内联,大于limit不处理,直接扔给file-loader处理,不装直接报错,之前还以为会自动调用,所以这两者都得装上。

html引用图片

html模板文件通过html-wepback-plugin生成的,如果希望webpack能够正确处理打包之后图片的引用路径,需要在模板文件中这样引用图片。

// 正确:会交给url-loader 或 file-loader
// require让图片和html产生依赖引用关系
<img src="<%= require('./images/sett1.png') %>" alt="">

// 错误:原样输出,不做任何处理
<img src="./images/sett1.png" alt="">
1
2
3
4
5
6

css引用图片

/* 图片作为背景图 */
#main {
    background: url("../images/test.jpg") #999;
    color: #fff
}
1
2
3
4
5

js引用图片

// app.js
import sett1 from './images/test.png';
const img = document.createElement('img');
img.src = sett1;
document.body.appendChild(img);
1
2
3
4
5

配置

// webpack.config.js
module.exports = {
	...
	modules: {
		rules: [
			{
				test: /\.(png|jpe?g|gif)$/,
				use: {
					loader: 'url-loader',
					options: {
						limit: 1024 * 10, // 10k以下的base64内联,不产生图片文件
						fallback: 'file-loader', // 10k以上,用file-loader抽离(非必须,默认就是file-loader)
						name: '[name].[ext]?[hash]', // 文件名规则,默认是[hash].[ext]
						outputPath: 'images/', // 输出路径
						publicPath: ''  // 可访问到图片的引用路径(相对/绝对)
					}
				}
			}
		]
	}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

上述配置除了limitfallbackurl-loader的参数以外,其他配置项如name, outputPath都会透传给file-loader

关于name, outputPath, publicPath

  1. 图片最终的输出路径:path.join(outputPath, name)
  2. 图片的引用路径:指定了publicPathpath.join(publicPath, name),这里会忽略掉outputPath; 否则用默认的output.publicPathpath.join(__webpack_public_path__, outputPath, name)

打包

735a036354c8eb870a18b2e7daad647b.png 1ed05ab6f0f4e21b4a740cd0bfa23452.png

由上图可以发现,打包之后的css放在了style目录下(style.css),与images不同级。导致无法正确访问到images目录下的图片。

要解决上面的问题,可以在抽离css时设定publicPath:

extractCSS.extract({
	fallback: 'style-loader',
	use: 'css-loader',
	publicPath: '../' // 默认取output.publicPath
})
1
2
3
4
5

output.publicPath

publicPath的值会作为前缀附加在loaders生成的所有URL前面。

output: {
     path: path.resolve(__dirname, 'dist'),
     filename: '[name].[chunkhash].js',
     publicPath: '../'
 }
1
2
3
4
5
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>webpack配置指南</title>
<link href="../style/style.css?cebba7d6ffb2308426a7" rel="stylesheet"></head>
<body>
<div class="box">
    hello webpack
</div>
<div class="container">
</div>
<script type="text/javascript" src="../app.e526cd961fd59aec06c2.js?cebba7d6ffb2308426a7"></script></body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

比如上面的images/mvvm.png,如果设置了output.publicPath:'../',那最终打包之后就会变成../images/mvvm.png

output.publicPath指定了output目录的访问路径,也就是浏览器怎样找到output目录。比如设置了output.publicPath:'../',就说明output目录在html所在目录的上一级。

如果这样设置的话,css和html的目录层级关系并不符合要求,所以单独在extractCSS.extract中设置publicPath起到了覆盖output.publicPath的作用。

file-loader和url-loader的区别

其实url-loader封装了file-loaderurl-loader不依赖于file-loader。我们在使用url-loader的时候,只需要安装url-loader,因为url-loader内置了file-loader

url-loader在处理图片资源时分两种情况:

  1. 图片大小小于limit参数:url-loader将会把文件转为base64编码字符串DataURL
  2. 图片大小大于limit参数:url-loader会调用file-loader进行处理。
// file-loader:在输出目录生成对应的图片,解决css等文件中引入图片路径的问题
{
    module:{
        rules:[
            {
                test: /\.(png|jpg|gif)$/,
                use: ['file-loader']
            }
        ]
    }
}

// url-loader
{
    module:{
        rules:[
            {
                test:/\.(jpg|gif|jpeg|gif|png)$/,
	            use:[
                    {
                        loader: 'url-loader',
                        options: {
                            outputPath: 'images/', // 图片会被打包在 dist/images 目录下
                            limit: 1024 * 10, //小于10kb进行base64转码引用
                            name: '[hash:8].[name].[ext]'//打包后图片的名称,在原图片名前加上8位hash值
                        }
                    }
                ]
            }
        ]
    }
}
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

plugin

在 webpack 的构建流程中,plugin 用于处理更多其他的一些构建任务。可以这么理解,模块代码转换的工作由 loader 来处理,除此之外的其他任何工作都可以交由 plugin 来完成。

Plugin是用来扩展webpack功能的,通过在构建流程里注入钩子实现,为webpack带来了很大的灵活性。

mini-css-extract-plugin

npm install -D mini-css-extract-plugin
1
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      // 指定提取出来的css文件的名称
      filename: "[name]_[contenthash:8].css"
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader"
        ]
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

[name]代表文件的名称,[contenthash:8]代表根据文件内容算出的8位Hash值。

extract-text-webpack-plugin(废弃)

该插件的主要是为了抽离css样式,防止将样式打包在js中引起页面样式加载错乱的现象。

// 安装
npm install extract-text-webpack-plugin --save-dev 或 -D

// 特别注意:webpack4.x,现在要安装一下版本
npm install extract-text-webpack-plugin@next -D
1
2
3
4
5
// 引入插件
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: ['css-loader', {
                loader: 'postcss-loader',
                options: {
                    config: {
                      path: './postcss.config.js'//需要在项目根目录创建此文件
                    }
                }
            }, 'sass-loader']
        }),
        include: path.resolve(__dirname, 'src'), //限制范围,提高打包速度
        exclude: /node_modules/ //排除打包目录
      }
    ]
  },
  plugins: [
    new ExtractTextWebpackPlugin({
      filename: 'css/[name].css' //放到dist/css/下
    })
  ]
}
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

该插件有三个参数:

  • use: 指需要什么样的loader去编译文件,这里由于源文件是.css所以选择css-loader;
  • fallback: 编译后用什么loader来提取css文件;
  • publicfile: 用来覆盖项目路径,生成该css文件的文件路径

uglifyjs-webpack-plugin(压缩js)

// 如果是生产模式下,会自动压缩,不需要使用该插件进行压缩
webpack --mode production

// 开发环境下
// 安装该插件
npm install uglifyjs-webpack-plugin -D
// 引入插件
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
// 调用插件
plugins: [
    new UglifyjsWebpackPlugin()
]
1
2
3
4
5
6
7
8
9
10
11
12

清空打包输出目录

// 安装
npm install clean-webpack-plugin -D

// 引入插件
const CleanWebpackPlugin = require('clean-webpack-plugin');

// 调用插件
new CleanWebpackPlugin('./dist/bundle.*.js')
1
2
3
4
5
6
7
8

DefinePlugin

webpack.DefinePlugin相当于是给配置环境定义了一组全局变量,业务代码可以直接使用定义在里面的变量。

ProvidePlugin(自动加载模块,而不必到处import或require)

new webpack.ProvidePlugin({
  identifier: 'module1',
  // ...
})
1
2
3
4
// 自动加载lodash和jquery,可以将两个变量($和_)都指向对应的 node 模块:

new webpack.ProvidePlugin({
  $: 'jquery',
  _: 'lodash'
})
1
2
3
4
5
6

copy-webpack-plugin(复制静态资源)

// 安装
npm install copy-webpack-plugin

// 引入插件
const CopyWebpackPlugin = require('copy-webpack-plugin');

// 调用
new CopyWebpackPlugin([
  {
    from: path.resolve(__dirname, 'static'),
    to: path.resolve(__dirname, 'pages/static'),
    ignore: ['.*']
  }
])
1
2
3
4
5
6
7
8
9
10
11
12
13
14

webpack-dev-server配置

DevServer会启动一个HTTP服务器用于服务网页请求,同时会帮助启动Webpack,并接收Webpack发出的文件变更信号,通过WebSocket协议自动刷新网页做到实时预览。

安装相应的依赖包

// react-hot-loader用来支持react热加载
npm install webpack-dev-server react-hot-loader -D
1
2

需要注意的是:DevServer会将Webpack构建出的文件保存在内存中,在要访问输出的文件时,必须通过HTTP服务访问。

实时预览

Webpack在启动时可以开启监听模式,之后Webpack会监听本地文件系统的变化,在发生变化时重新构建出新的结果。Webpack默认关闭监听模式,我们可以在启动Webpack时通过webpack --watch来开启监听模式。

通过DevServer启动的Webpack会开启监听模式,当发生变化时重新执行构建,然后通知DevServerDevServer会让Webpack在构建出的javascript代码里注入一个代理客户端用于控制网页,网页和DevServer之间通过WebSocket协议通信,以方便DevServer主动向客户端发送命令。DevServer在收到来自Webpack的文件变化通知时,通过注入的客户端控制网页刷新。

如果尝试修改index.html文件并保存,则我们会发现这并不会触发以上机制,导致这个问题的原因是:Webpack在启动时会以配置里的entry为入口去递归解析出entry所依赖的文件,只有entry本身和依赖的文件才会被Webpack添加到监听列表里。而index.html文件是脱离了javascript模块化系统的,所以Webpack不知道它的存在。

配置文件中进行相关配置

// 在webpack配置文件中添加devServer相应的配置
devServer: {
      contentBase: './dist',//本地服务器所加载的页面所在的目录
      historyApiFallback: true, // 不跳转
      inline: true, // 实时刷新,
      compress: true,
      port: 8088,
      hot: true // 热加载
}
1
2
3
4
5
6
7
8
9

还需要在.babelrc文件中进行插件项配置:

{
    "presets": ["env", "react"],
    "plugins": ["react-hot-loader/babel"] //新增加
}
1
2
3
4

配置文件

开发环境配置文件webpack.config.js

// rules是一个规则数组,每一项是一个对象,配置loader
rules:[
    {
        test:'匹配文件正则',
        include:'在哪个目录匹配',
        exclude:'排除哪个目录',
        use:[
            //配置多个loader,从右往左依次执行
            {
                loader:"需要的loader",
                options:{
                    //loader的相关配置项
                }
            }
        ]
    }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const path = require('path');
const webpack = require('webpack'); // 用于访问内置插件
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: 'babel-loader'
            }, {
                test: /\.html$/,
                use: {
                    loader: 'html-loader',
                    options: {
                        minimize: true
                    }
                }
            }, {
                test: /\.scss$/,
                use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
            }
        ]
    },
    devServer: {
        contentBase: './dist',//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true, //实时刷新,
        compress: true,
        port: 8088,
        hot: true //热加载
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            filename: 'index.html'
        }),
        //每次打包都会先清除当前目录中dist目录下的文件
        new CleanWebpackPlugin('./dist/bundle.*.js'),
        new webpack.HotModuleReplacementPlugin(),//热加载插件
    ],
    //由于压缩后的代码不易于定位错误, 配置该项后发生错误时即可采用source-map的形式直接显示你出错代码的位置
    devtool: 'eval-source-map',
    resolve: {
        //配置简写, 配置过后, 书写该文件路径的时候可以省略文件后缀。
        extensions: ['.js', '.jsx', '.coffee', '.css', './scss']
    }
};
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

生产环境配置文件webpack.production.config.js

const path = require('path');
const webpack = require('webpack'); // 用于访问内置插件
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.[hash].js'
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: ['env', 'react']
                        }
                    }
                ]
            }, {
                test: /\.scss$/,
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: ['css-loader', {
                        loader: 'postcss-loader',
                        options: {
                            config: {
                              path: './postcss.config.js'  // 得在项目根目录创建此文件
                            }
                        }
                    }, 'sass-loader']
                }),
                include: path.resolve(__dirname, 'src'), //限制范围,提高打包速度
                exclude: /node_modules/ //排除打包目录
            }, {
                test: /\.html$/,
                use: {
                        loader: 'html-loader',
                        options: {
                            minimize: true
                        }
                }
            }, {
                test:/\.(jpg|gif|jpeg|gif|png)$/,
	            use:[
                    {
                        loader: 'url-loader',
                        options: {
                            outputPath: 'images/', // 图片会被打包在 dist/images 目录下
                            limit: 10240, //小于10kb进行base64转码引用
                            name: '[hash:8].[name].[ext]'//打包后图片的名称,在原图片名前加上8位hash值
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            title: 'webpack实战练习',
            template: './src/index.html'
        }),
        //每次打包都会先清除当前目录中dist目录下的文件
        new CleanWebpackPlugin('./dist/bundle.*.js'),
        new ExtractTextPlugin({
            filename: '[name].css'
        })
    ],
    //由于压缩后的代码不易于定位错误, 配置该项后发生错误时即可采用source-map的形式直接显示你出错代码的位置
    devtool: 'eval-source-map',
    resolve: {
        //配置简写, 配置过后, 书写该文件路径的时候可以省略文件后缀。
        extensions: ['.js', '.jsx', '.coffee', '.css', '.scss']
    }
};
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

webpack 的配置其实是一个Node.js的脚本,这个脚本对外暴露一个配置对象,webpack 通过这个对象来读取相关的一些配置。因为是 Node.js 脚本,所以可玩性非常高,你可以使用任何的 Node.js 模块,如上述用到的 path 模块,当然第三方的模块也可以。

创建了 webpack.config.js 后再执行 webpack 命令,webpack 就会使用这个配置文件的配置了。

脚手架中的 webpack 配置

现今,大多数前端框架都提供了简单的工具来协助快速生成项目基础文件,一般都会包含项目使用的 webpack 的配置,如:

create-react-app 的 webpack 配置在这个项目下:react-scripts

vue-cli 使用 webpack 模板生成的项目文件中,webpack 相关配置存放在 build 目录下。

通常 angular 的项目开发和生产的构建任务都是使用 angular-cli 来运行的,但 angular-cli 只是命令的使用接口,基础功能是由 angular/devkit来实现的,webpack 的构建相关只是其中一部分,详细的配置可以参考webpack-configs

// webpack.config.js
const path = require('path');  //引入node的path模块
const webpack = require('webpack'); //引入的webpack,使用lodash
const HtmlWebpackPlugin = require('html-webpack-plugin')  //将html打包
const ExtractTextPlugin = require('extract-text-webpack-plugin')     //打包的css拆分,将一部分抽离出来
const CopyWebpackPlugin = require('copy-webpack-plugin')
// console.log(path.resolve(__dirname,'dist')); //物理地址拼接
module.exports = {
    entry: './src/index.js', //入口文件  在vue-cli main.js
    output: {       //webpack如何输出
        path: path.resolve(__dirname, 'dist'), //定位,输出文件的目标路径
        filename: '[name].js'
    },
    module: {       //模块的相关配置
        rules: [     //根据文件的后缀提供一个loader,解析规则
            {
                test: /\.js$/,  //es6 => es5
                include: [
                    path.resolve(__dirname, 'src')
                ],
                // exclude:[], 不匹配选项(优先级高于test和include)
                use: 'babel-loader'
            },
            {
                test: /\.less$/,
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: [
                    'css-loader',
                    'less-loader'
                    ]
                })
            },
            {       //图片loader
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'file-loader' //根据文件地址加载文件
                    }
                ]
            }
        ]
    },
    resolve: { //解析模块的可选项
        // modules: []//模块的查找目录 配置其他的css等文件
        extensions: ['.js', '.json', '.jsx','.less', '.css'], //用到文件的扩展名
        alias: { // 模快别名列表
            utils: path.resolve(__dirname,'src/utils')
        }
    },
    plugins: [  //插进的引用, 压缩,分离美化
        new ExtractTextPlugin('[name].css'),  //[name] 默认  也可以自定义name  声明使用
        new HtmlWebpackPlugin({  //将模板的头部和尾部添加css和js模板,dist 目录发布到服务器上,项目包。可以直接上线
            file: 'index.html', //打造单页面运用 最后运行的不是这个
            template: 'src/index.html'  //vue-cli放在跟目录下
        }),
        new CopyWebpackPlugin([  //src下其他的文件直接复制到dist目录下
            { from:'src/assets/favicon.ico',to: 'favicon.ico' }
        ]),
        new webpack.ProvidePlugin({  //引用框架 jquery  lodash工具库是很多组件会复用的,省去了import
            '_': 'lodash'  //引用webpack
        })
    ],
    devServer: {  //服务于webpack-dev-server  内部封装了一个express
        port: '8080',
        before(app) {
            app.get('/api/test.json', (req, res) => {
                res.json({
                    code: 200,
                    message: 'Hello World'
                })
            })
        }
    }

}
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

cross-env(跨平台设置环境变量)

npm install --save-dev cross-env
1
"scripts": {
    "build": "cross-env NODE_ENV=production webpack --config webpack.production.config.js --mode production",
    "dev": "cross-env NODE_ENV=development webpack-dev-server --mode development --open",
    "dll": "webpack --config webpack_dll.config.js --mode development"
  }
1
2
3
4
5

参考文档

  1. webpack 4 教程
  2. 精读《webpack4.0 升级指南》
  3. 手写一个webpack4.0配置
  4. webpack详解
  5. webpack中文文档
  6. Webpack 实用技巧高效实战
  7. 玩转webpack(一)上篇:webpack的基本架构和构建流程
  8. 玩转webpack(二):webpack的核心对象
  9. Webpack 持久化缓存实践
  10. cross-env
  11. webpack4 中文文档