目录
- 1. 开发环境的搭建
- 1.1 Node.js管理
- 1.2 NPM管理
- 2. 基础概念及常见配置项
- 2.1 webpack.config.js 配置文件
- 2.2 webpack支持多类型配置
- 2.3 webpack中的常见名词
- 2.4 mode模式
- 2.5 webpack的入口和输出
- 2.6 output输出配置项
- 2.7 resolve
- 2.8 module
- 2.9 条件匹配
- 2.10 Loader配置
- 2.11 plugin插件
1. 开发环境的搭建
在说webpack的基础概念和常见配置项之前,先来看一下webpack的环境搭建,列举一写常见的问题。
1.1 Node.js管理
Node.js的流行离不开NPM的贡献,使用Node.js写的代码,可以打包发到JavaScript包管理平台npmjs.com,供其他开发者使用。当我们的项目需要某个包(模块)时,就可以通过包管理工具下载安装对应的包。
NPM 中使用了一个命名为package.json
的文件作为一个 NPM 包的描述文件,package.json
包含了包的基本信息(名称、版本号、描述、作者等)和依赖关系,例如:
{
"name": "demo", // 项目的名称
"version": "1.0.0",
"dependencies": {
"webpack": "^4.29.6"
}
}
其中,dependencies
是该项目的依赖,对应的还有devdependencies(开发依赖)。实际项目中,webpack是构建工具,代码不会直接用 webpack 的 API,而只在开发和打包的时候采用,所以正确做法是放在devdependencies中。
我们可以看到webpack版本号前面有一个^
,它就表示主版本是4的最新版本,每次执行安装命令的时候,会更新符合这个规则的最新包。
1.2 NPM管理
(1)NPM安装和删除
npm install packageName // 安装包
npm i packageName // 简写
npm i packageName@x.x.x // 安装指定版本的包
npm i packageName --save // 将安装的包写入package.age
npm i packageName -S // 简写
npm uninstall packageName // 删除包
(2)NPM包的模式
npm的包安装,分为本地模式和全局模式,默认为本地模式。
- 本地模式:就是在执行
npm install
命令时,会在当前目录创建node_modules
,然后下载安装包及其依赖到node_modules
目录。 - 全局模式:安装到全局路径的方式。
在Node的require依赖时,会优先查找当前文件的node_modules
,如果没有,就查找上层的node_modules
,如果遍历到根目录都没有找到,就会使用全局安装的模块。
全局安装的包可以使用指定全局命令,只需要在package.json
中增加bin字段,并且指向包内对应的文件即可。我们如果需要全局安装一个包,使用以下命令:
npm install packageName --global
npm install packageName -g // 简写
(3)NPM其他常用的命令
- npm set:设置环境变量,例如:
npm set init-author-name 'Your name'
,初始化的时候会使用默认环境变量; - npm info:查看某个包的信息,例如:
npm info lodash
; - npm search:查找 npm 仓库,后面可以跟字符串或者正则表达式,例如:
npm search webpack
; - npm list:树形的展现当前项目安装的所有模块,以及对应的依赖,例如:
npm list --global
查看全局安装的模块。
(3)NPM scripts
NPM不仅可以用于模块管理,还可以用于执行脚本。在package.json
文件中添加scripts
字段,用于指定脚本命令,供npm直接调用,如下:
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"start": "node src/scripts/dev.js"
},
根据上面代码中的配置,我们就可以执行以下命令进行对应的操作:
npm run build //打包项目
npm run serve //启动项目
npm run lint //代码检查
npm run start //执行dev.js文件
2. 基础概念及常见配置项
2.1 webpack.config.js 配置文件
webpack是可配置的模块化打包工具。我们可以通过修改webpack的配置文件webpack.config.js
来对其进行配置,webpack的配置文件遵循Node.js的模块化规范CommonJS:
- 通过
require()
语法导入其他文件或者使用Node.js内置的模块 - 使用JavaScript来编写语法
简单来说,webpack.config.js
就是Node.js的一个模块。
2.2 webpack支持多类型配置
webpack不仅支持JavaScript还支持typescript等语言。不同语言的核心配置都一样,只是语法不同。
除了配置文件的语法不同之外,配置的类型也是多样的,最常见的就是作为一个对象来使用,除了使用对象,webpack还支持函数、Promise和多配制数组。
(1)函数类型的webpack配置
如果我们只使用一个配置文件来区分开发环境和生产环境,就可以使用函数类型的webpack配置,函数类型的配置必须返回一个配置对象,具体方式如下:
module.exports = (env, argv) => {
return {
mode: env.production ? 'production' : 'development',
devtool: env.production ? 'source-maps' : 'eval',
plugins: [
new TerserPlugin({
terserOptions: {
compress: argv['optimize-minimize'] // 只有传入 -p 或 --optimize-minimize
}
})
]
};
};
ebpack 配置函数接受两个参数env
和argv
:分别对应着环境对象和 Webpack-cli的命令行选项,例如上面代码中的--optimize-minimize
。
(2)Promise 类型的 Webpack 配置
如果需要异步加载一些 Webpack 配置需要做的变量,那么可以使用 Promise 的方式来做 Webpack 的配置,具体方式如下:
module.exports = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
entry: './app.js'
/* ... */
});
}, 5000);
});
};
(3)多配置数组
在一些特定的场景,我们可能需要一次打包多次,而多次打包中有一些通用的配置,这时候可以使用配置数组的方式,将两次以上的 Webpack 配置以数组的形式导出:
module.exports = [
{
mode: 'production'
// 配置1
},
{
// 配置2
}
];
配置的使用
默认情况下,webpack会查找执行目录下面的webpack.config.js
,如果需要指定某个配置文件,可以使用以下命令:
webpack --config 配置文件名
2.3 webpack中的常见名词
参数 | 说明 |
entry | 项目入口 |
module | 开发中每一个文件都可以看做 module,模块不局限于 js,也包含 css、图片等 |
chunk | 代码块,一个 chunk 可以由多个模块组成 |
loader | 模块转化器,模块的处理器,对模块进行转换处理 |
plugin | 扩展插件,插件可以处理 chunk,也可以对最后的打包结果进行处理,可以完成 loader 完不成的任务 |
bundle | 最终打包完成的文件,一般就是和 chunk 一一对应的关系,bundle 就是对 chunk 进行便意压缩打包等处理后的产出 |
2.4 mode模式
Webpack4.0 开始引入了mode配置,通过配置mode=development
或者mode=production
来制定是开发环境打包,还是生产环境打包,比如生产环境代码需要压缩,图片需要优化。Webpack 默认mode是生产环境,即mode=production
。
module.exports = {
mode: 'development'
};
除了在配置文件中设置mode,还可以在命令行中设置mode:
npx webpack --config webpack.config.entry.js --mode development
2.5 webpack的入口和输出
webpack是一个模块化打包工具,它是从指定的入口文件(entry)开始,经过加工处理,最终按照output设定输出固定内容的 bundle;而这个加工处理的过程,就用到了loader和plugin两个工具;loader是源代码的处理器,plugin解决的是 loader处理不了的事情。
下面主要来介绍一下entry和output。
(1)context
context
指的是上下文,即项目打包的相对路径的上下文,如果执行了context
,那么entry
和output
的相对路径都是相对于context
值的,包括在 JavaScript 中引入模块也是从这个路径开始的。由于context
的作用,决定了context值必须是一个绝对路径。 不过一般情况下,context
不需要配置。
(2)entry
Webpack 的entry支持多种类型,包括字符串、对象、数组。从作用上来说,包括了单文件入口和多文件入口两种方式。
- 单文件入口
单入口文件可以创建一个只有单一文件入口的情况,但是单文件入口的方式比较简单,在扩展配置的时候灵活性较低。
module.exports = {
entry: 'path/to/my/entry/file.js'
};
// 或者使用对象方式
module.exports = {
entry: {
main: 'path/to/my/entry/file.js'
}
}
entry还可以传入包含文件路径的数组,当entry为数组的时候也会合并输出,例如下面的配置:
module.exports = {
mode: 'development',
entry: ['./src/app.js', './src/home.js'],
output: {
filename: 'array.js'
}
}
上面的代码虽然都是只有一个入口,但是在打包产出上会有些差异:
1)如果是string类型,webpack就会直接把该string指定的模块作为入口模块
2)如果是数组类型,webpack会生成另外一个入口模块,并将数组中每个元素指定的模块的内容加载进来,并将最后一个模块的 module.exports
作为入口模块的 module.exports
导出。
- 多文件入口
多文件入口是使用对象语法来通过支持多个entry
,多文件入口的对象语法相对于单文件入口,具有较高的灵活性,例如多页应用、页面模块分离优化。多文件入口的语法如下:
module.exports = {
entry: {
home: 'path/to/my/entry/home.js',
search: 'path/to/my/entry/search.js',
list: 'path/to/my/entry/list.js'
}
};
上面的语法将entry分成了 3 个独立的入口文件,这样会打包出来三个对应的 bundle。
(3)output
webpack 的output
是指定了entry
对应文件编译打包后的输出 bundle
。output
的常用属性是:
- path:此选项制定了输出的 bundle 存放的路径,比如dist、output等
- filename:这个是 bundle 的名称
- publicPath:指定了一个在浏览器中被引用的 URL 地址
需要注意的是,当不指定 output
的时候,默认输出到 dist/main.js
,即 output.path
是dist
,output.filename
是 main
。
output
只能有一个,对于不同的entry可以通过output.filename
占位符语法来区分,比如:
module.exports = {
entry: {
home: 'path/to/my/entry/home.js',
search: 'path/to/my/entry/search.js',
list: 'path/to/my/entry/list.js'
},
output: {
filename: '[name].js',
path: __dirname + '/dist'
}
};
其中[name]
就是占位符,它对应的是entry的key(home、search、list),所以最终输出结果是:
path/to/my/entry/home.js → dist/home.js
path/to/my/entry/search.js → dist/search.js
path/to/my/entry/list.js → dist/list.js
下面是webpack中支持的占位符:
占位符 | 含义 |
[hash] | 模块标识符的 hash |
[chunkhash] | chunk 内容的 hash |
[name] | 模块名称 |
[id] | 模块标识符 |
[query] | 模块的 query,例如,文件名 ? 后面的字符串 |
[function] | 一个 return 出一个 string 作为 filename 的函数 |
其中[hash]
和 [chunkhash]
的长度可以使用 [hash:16]
(默认为 20)来指定。或者,通过指定 output.hashDigestLength 在全局配置长度,他们的区别:
-
[hash]
:是整个项目的 hash 值,其根据每次编译内容计算得到,每次编译之后都会生成新的 hash,即修改任何文件都会导致所有文件的 hash 发生改变;在一个项目中虽然入口不同,但是 hash 是相同的;hash 无法实现前端静态资源在浏览器上长缓存,这时候应该使用 chunkhash; -
[chunkhash]
:根据不同的入口文件(entry)进行依赖文件解析,构建对应的 chunk,生成相应的 hash;只要组成 entry 的模块文件没有变化,则对应的 hash 也是不变的,所以一般项目优化时,会将公共库代码拆分到一起,因为公共库代码变动较少的,使用 chunkhash 可以发挥最长缓存的作用;
注意:占位符可以组合使用,例如[name]-[hash:16]
。
(4)output.publicPath
对于使用<script>
和 <link>
标签时,当文件路径不同于他们的本地磁盘路径(由output.path
指定)时,output.publicPath
被用来作为src或者link指向该文件。这种做法在需要将静态文件放在不同的域名或者 CDN 上面的时候是很有用。
module.exports = {
output: {
path: '/home/git/public/assets',
publicPath: '/assets/'
}
};
// 输出为:
<head>
<link href="/assets/logo.png" />
</head>
上面的/assets/logo.png
就是根据publicPath输出的,output.path
制定了输出到本地磁盘的路径,而output.publicPath
则作为实际上线到服务器之后的 url 地址。所以我们在上 CDN 的时候可以这样配置:
module.exports = {
output: {
path: '/home/git/public/assets',
publicPath: 'http://cdn.example.com/assets/'
}
};
// 输出为:
<head>
<link href="http://cdn.example.com/assets/logo.png" />
</head>
2.6 output输出配置项
下面再来详细看一下与output输出相关的三个配置项:externals
,target
和 devtool
(1)externals
externals配置项用于去除输出的打包文件中依赖的第三方JS模块(例如Vue,React),来减小打包的体积。
使用场景: 该功能通常在开发自定义的JS库的时候用到,用于去除自定义js库依赖的其他第三方js模块。这些依赖模块应该由使用者提供,而不应该包含在js库文件中。
(2)target
在开发过程中,我们不仅是开发web应用,还可能开发node应用等,这时因为对应的宿主环境不同,所以在构建的时候需要特殊处理。webpack中可以通过target来指定构建的目标。
module.exports = {
target: 'web' // 默认是 web,可以省略
};
target
的值有两种类型string和function:
string类型支持以下七种值:
- web:默认,编译为类浏览器环境里可用;
- node:编译为类 Node.js 环境可用(使用 Node.js require 加载 chunk);
- async-node:编译为类 Node.js 环境可用(使用 fs 和 vm 异步加载分块);
- electron-main:编译为 Electron 主进程;
- electron-renderer:编译为 Electron 渲染进程;
- node-webkit:编译为 Webkit 可用,并且使用 jsonp 去加载分块。支持 Node.js 内置模块和 nw.gui 导入(实验性质);
- webworker:编译成一个 WebWorker。
除了string类型,target 还支持 function 类型,这个函数接收一个compiler作为参数,如下面代码可以用来增加插件:
const webpack = require('webpack');
const options = {
target: compiler => {
compiler.apply(new webpack.JsonpTemplatePlugin(options.output), new webpack.LoaderTargetPlugin('web'));
}
};
(3)devtool
devtool是来控制怎么显示sourcemap,通过 sourcemap 我们可以快速还原代码的错误位置。
但是由于 sourcemap 包含的数据量较大,而且生成算法需要计算量支持,所以 sourcemap 的生成会消耗打包的时间。
一般在实际项目中,建议生产环境不使用或者使用 source-map(如果有 Sentry 这类错误跟踪系统),开发环境使用cheap-module-eval-source-map
。
2.7 resolve
Webpack进行构建的时候会从入口文件开始遍历各个模块的依赖,resolve配置就是为了帮助webpack查找依赖模块的,通过resolve的配置,可以帮助webpack快速查找依赖,也可以替代对应的依赖。配置形式如下:
module.exports = {
resolve: {
// resolve的配置
}
};
(1)resolve.extensions
extensions
是用来帮助webpack解析扩展名的配置,默认为.wasm
、.mjs
、.js
、.json
。所以,在引入这几种类型文件的时候可以不写扩展名,我们可以在里面添加需要解析的文件的扩展名,这样引入文件时就可以不写扩展名(前提是没有重名的已在extensions定义扩展名的文件):
module.exports = {
resolve: {
extensions: ['.js', '.json', '.css']
}
};
(2)resolve.alias
alias
是用来帮助webpack更快的查找模块依赖,而且可以让我们编写代码更加方便。
如有有一个很深层的层级的引入,我们写的很长的话就不是很好看,比如下面的例子:
src
├── lib
│ └── utils.js
└── pages
└── demo
└── index.js
如果index.js需要引入utils.js文件,就要写成:import utils from '../../lib/utils'
我们可以通过alias来缩短这种写法:
module.exports = {
resolve: {
alias: {
src: path.resolve(__dirname, 'src'),
'@lib': path.resolve(__dirname, 'src/lib')
}
}
};
这样,就可以直接使用require('@lib/utils')
或require('src/lib/utils')
来引入文件。这样看起来进很简洁,如果需要多次引入,这样写会大大的提高开发效率。
2.8 module
在webpack解析模块的同时,不同的模块需要使用不同类型的模块处理器来处理,这部分的设置就是在module中实现的。module有两个配置:noParse
和rules
。
(1)module.noParse
noParse配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析和处理,这样做的好处是能提高构建性能,接收的类型为正则表达式、正则表达式数组或者接收模块路径参数的一个函数:
module.exports = {
module: {
// 使用正则表达式
noParse: /jquery|lodash/
// 使用函数
noParse: (content) => {
// content 代表一个模块的文件路径
// 返回 true or false
return /jquery|lodash/.test(content);
}
}
}
注意,这里一定要确定被排除出去的模块代码中不能包含import、require、define等内容,以保证 webpack 的打包包含了所有的模块,不然会导致打包出来的 js 因为缺少模块而报错。
(2)module.rules
rules
是在处理模块时,将符合规则条件的模块,提交给对应的处理器来处理,通常用来配置 loader,其类型是一个数组,数组里每一项都描述了如何去处理部分文件。每一项 rule 大致可以由以下三部分组成:
- 条件匹配:通过test、include、exclude等配置来命中可以应用规则的模块文件;
- 应用规则:对匹配条件通过后的模块,使用use配置项来应用loader,可以应用一个 loader 或者按照从后往前的顺序应用一组 loader,当然我们还可以分别给对应 loader 传入不同参数;
- 重置顺序:一组 loader 的执行顺序默认是从后到前(或者从右到左)执行,通过enforce选项可以让其中一个 loader 的执行顺序放到最前(pre)或者是最后(post)。
rules中的parser
用来控制模块化语法,和上面noParse作用类似,因为webpack是以模块化的JavaScript文件为入口,所以内置了对模块化JavaScript的解析功能。支持AMD、Commonjs、SystemJs、ES6。parse属性可以更细粒度的配置哪些模块语法要解析,哪些不解析。
支持的选项如下:
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader'],
parser: {
amd: false, // 禁用 AMD
commonjs: false, // 禁用 CommonJS
system: false, // 禁用 SystemJS
harmony: false, // 禁用 ES6 import/export
requireInclude: false, // 禁用 require.include
requireEnsure: false, // 禁用 require.ensure
requireContext: false, // 禁用 require.context
browserify: false, // 禁用 browserify
requireJs: false, // 禁用 requirejs
}
}]
}
注意,parser是语法层面的限制,noParse只能控制哪些文件不进行解析。
2.9 条件匹配
条件匹配相关的配置有test
、include
、exclude
、resource
、resourceQuery
和issuer
。条件匹配的对象包括三类:resource
,resourceQuery
和issuer
。
- resource:请求文件的绝对路径。它已经根据 resolve 规则解析;
- issuer: 被请求资源的模块文件的绝对路径,即导入时的位置。
例如,从 app.js 导入 './style.css?inline'
:
- resource 是
/path/to/style.css
; - resourceQuery 是?之后的
inline
; - issuer 是
/path/to/app.js
。
注意,上面的test
、include
、exclude
、resource
匹配的对象都是resource。
看下面的例子,匹配的条件为:来自src和test文件夹,不包含node_modules
和bower_modules
子目录,模块的文件路径为.tsx
和.jsx
结尾的文件。
{
test: [/\.jsx?$/, /\.tsx?$/],
include: [
path.resolve(__dirname, 'src'),
path.resolve(__dirname, 'test')
],
exclude: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, 'bower_modules')
]
}
rules 最重要的内容有以下四个:
- test:正则匹配需要处理的模块文件;
- use:loader 数组配置,内部有loader和options;
- include:包含;
- exclude:排除;
2.10 Loader配置
Loader是解析处理器,通过loader可以将 ES6 语法的 js 转化成 ES5 的语法,可以将图片转成 base64 的dataURL,在 JavaScript 文件中直接import css 和 html 也是通过对应的loader来实现的。
(1)Loader基本配置
在使用之前,我们要先安装它,例如引入less:
npm i -D less-loader
然后指定所有的.less
文件都是用less-loader解析:
module.exports = {
module:{
rules:[
test: /\.less$/,
use:'less-loader'
]
}
}
上面的配置简单来说就是test来匹配.less 的文件,然后交给less-loader来处理。这样所有的less文件都会处理为css文件。
除了直接在webpack.config.js使用 loader 的方式之外,还可以在对应的 JavaScript 文件中使用 loader,来在js文件中引入html或者css:
// 先配置
module.exports = {
module: {
rules: [{test: /\.html$/,
use: ['html-loader']}]
}
};
// 再使用
const html = require('html-loader!./loader.html');
console.log(html);
上面的代码实际上是将loader.html转化为string变量直接输出了,等同于:
const html = require('./loader.html');
console.log(html);
(2)Loader的参数
给 loader 传参的方式有两种:
- 通过options传入
- 通过query的方式传入
// inline内联写法,通过 query 传入
const html = require("html-loader?attrs[]=img:src&attrs[]=img:data-src!./file.html");
// config内写法,通过 options 传入
module: {
rules: [{
test: /\.html$/,
use: [{
loader: 'html-loader',
options: {
minimize: true,
removeComments: false,
collapseWhitespace: false
}
}]
}]
}
// config内写法,通过 query 传入
module: {
rules: [{
test: /\.html$/,
use: [ {
loader: 'html-loader?minimize=true&removeComments=false&collapseWhitespace=false',
}]
}]
}
(3)Loader的解析顺序
需要注意的是,Loader的解析顺序是逆序的,也就是从后往前,有些Loader的的解析是需要顺序,所以不要搞错顺序,例如:
const styles = require('css-loader!less-loader!./src/index.less');
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader'
},
{
loader: 'less-loader'
}
]
}
]
}
};
如果需要调整 Loader 的执行顺序,可以使用enforce,enforce取值是pre|post
,pre表示把放到最前,post是放到最后:
use: [
{
loader: 'babel-loader',
enforce: 'post' // 将babel-loader放在最后执行
}
];
2.11 plugin插件
plugin是 Webpack 的重要组成部分。Webpack 本身就是由很多插件组成的,所以内置了很多插件,可以直接通过webpack对象的属性来直接使用,例如,压缩js文件到的插件:
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin();
]
}
除了一些内置的插件,还可以通过 NPM 包的方式来使用插件:
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
//....
plugins: [
// 导出css文件到单独的内容
new ExtractTextPlugin({
filename: 'style.css'
})
]
}
注意,loader面向的是解决某个或者某类模块的问题,而plugin面向的是项目整体,解决的是loader解决不了的问题。