来看一下官网对webpack的定义:
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。
首先webpack是一个静态模块打包器,所谓的静态模块,包括脚本、样式表和图片等等;webpack打包时首先遍历所有的静态资源,根据资源的引用,构建出一个依赖关系图,然后再将模块划分,打包出一个或多个bundle。再次白piao一下官网的图,生动的描述了这个过程:
提到webpack,就不得不提webpack的四个核心概念
我们首先在全局安装webpack:
npm install webpack webpack-cli –g
webpack可以不使用配置文件,直接通过命令行构建,用法如下:
webpack <entry> [<entry>] -o <output>
这里的entry和output就对应了上述概念中的入口和输入,我们来新建一个入口文件:
//demo1/index.js
var a = 1
console.log(a)
document.write('hello webpack')
有了入口文件我们还需要通过命令行定义一下输入路径dist/bundle.js:
webpack index.js -o dist/bundle.js
这样webpack就会在dist目录生成打包后的文件。
我们也可以在项目目录新建一个html引入打包后的bundle.js文件查看效果。
命令行的打包方式仅限于简单的项目,如果我们的项目较为复杂,有多个入口,我们不可能每次打包都把入口记下来;因此一般项目中都使用配置文件来进行打包;配置文件的命令方式如下:
webpack [--config webpack.config.js]
配置文件默认的名称就是webpack.config.js
,一个项目中经常会有多套配置文件,我们可以针对不同环境配置不同的文件,通过--config
来进行切换:
//生产环境配置
webpack --config webpack.prod.config.js
//开发环境配置
webpack --config webpack.dev.config.js
config配置文件通过module.exports
导出一个配置对象:
//webpack.config.js
var path = require('path');
module.exports = {
mode: 'development',
//入口文件
entry: './index.js',
//输出目录
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
};
除了导出为对象,还可以导出为一个函数,函数中会带入命令行中传入的环境变量等参数,这样可以更方便的对环境变量进行配置;比如我们在打包线上正式环境和线上开发环境可以通过env
进行区分:
var path = require('path');
//env:环境对象
module.exports = function(env, argv){
return {
//其他配置
entry: './index.js',
output: {}
}
};
另外还可以导出为一个Promise,用于异步加载配置,比如可以动态加载入口文件:
module.exports = () => {
return new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve({
entry: './index.js',
output: {}
})
}, 5000)
})
}
正如在上面提到的,入口是整个依赖关系的起点入口;我们常用的单入口配置是一个页面的入口:
module.exports = {
entry: './index.js',
}
它是下面的简写:
module.exports = {
entry: {
main: './index.js'
},
}
但是我们一个页面可能不止一个模块,因此需要将多个依赖文件一起注入,这时就需要用到数组了,代码在demo2中:
module.exports = {
entry: [
//轮播图模块
'./src/banner.js',
//主模块
'./src/index.js',
//底部模块
'./src/foot.js'
],
}
有时候我们一个项目可能有不止一个页面,需要将多个页面分开打包,entry支持传入对象的形式,代码在demo3中:
//demo3
module.exports = {
entry: {
home: './src/home.js',
list: './src/list.js',
detail: ['./src/detail.js', './src/common.js'],
},
}
这样webpack就会构建三个不同的依赖关系。
output
选项用来控制webpack如何输入编译后的文件模块;虽然可以有多个entry,但是只能配置一个output
:
module.exports = {
entry: './src/index.js', output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
//CDN地址
publicPath: '/',
},
}
这里我们配置了一个单入口,输出也就是bundle.js;但是如果存在多入口的模式就行不通了,webpack会提示Conflict: Multiple chunks emit assets to the same filename
,即多个文件资源有相同的文件名称;webpack提供了占位符
来确保每一个输出的文件都有唯一的名称:
module.exports = {
entry: {
home: './src/home.js',
list: './src/list.js',
detail: ['./src/detail.js', './src/common.js'],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
},
}
这样webpack打包出来的文件就会按照入口文件的名称来进行分别打包生成三个不同的bundle文件;还有以下不同的占位符字符串:
占位符 | 描述 |
---|---|
[hash] | 模块标识符(module identifier)的 hash |
[chunkhash] | chunk 内容的 hash |
[name] | 模块名称 |
[id] | 模块标识符 |
[query] | 模块的 query,例如,文件名 ? 后面的字符串 |
在这里引入Module、Chunk和Bundle的概念,上面代码中也经常会看到有这两个名词的出现,那么他们三者到底有什么区别呢?首先我们发现module是经常出现在我们的代码中,比如module.exports;而Chunk经常和entry一起出现,Bundle总是和output一起出现。
我们通过下面一张图更深入的理解这三个概念:
总结:
module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:我们直接写出来的是module,webpack处理时是chunk,最后生成浏览器可以直接运行的bundle。
理解了chunk的概念,相信上面表中chunkhash和hash的区别也很容易理解了;
在webpack2和webpack3中我们需要手动加入插件来进行代码的压缩、环境变量的定义,还需要注意环境的判断,十分的繁琐;在webpack4中直接提供了模式这一配置,开箱即可用;如果忽略配置,webpack还会发出警告。
module.exports = {
mode: 'development',
};
//相当于
module.exports = {
devtool:'eval',
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.NamedChunksPlugin(),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("development")
})
]
}
开发模式是告诉webpack,我现在是开发状态,也就是打包出来的内容要对开发友好,便于代码调试以及实现浏览器实时更新。
module.exports = {
mode: 'production',
};
//相当于
module.exports = {
plugins: [
new UglifyJsPlugin(/*...*/),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production")
}),
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.NoEmitOnErrorsPlugin()
]
}
生产模式不用对开发友好,只需要关注打包的性能和生成更小体积的bundle。看到这里用到了很多Plugin,不用慌,下面我们会一一解释他们的作用。
相信很多童鞋都曾有过疑问,为什么这边DefinePlugin定义环境变量的时候要用JSON.stringify("production")
,直接用"production"
不是更简单吗?
我们首先来看下JSON.stringify("production")
生成了什么;运行结果是""production""
,注意这里,并不是你眼睛花了或者屏幕上有小黑点,结果确实比"production"
多嵌套了一层引号。
我们可以简单的把DefinePlugin这个插件理解为将代码里的所有process.env.NODE_ENV
替换为字符串中的内容
。假如我们在代码中有如下判断环境的代码:
//webpack.config.js
module.exports = {
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": "production"
}),
]
}
//src/index.js
if (process.env.NODE_ENV === 'production') {
console.log('production');
}
这样生成出来的代码就会编译成这样:
//dist/bundle.js
//代码中并没有定义production变量
if (production === 'production') {
console.log('production');
}
但是我们代码中可能并没有定义production
变量,因此会导致代码直接报错,所以我们需要通过JSON.stringify来包裹一层:
//webpack.config.js
module.exports = {
plugins: [
new webpack.DefinePlugin({
//"process.env.NODE_ENV": JSON.stringify("production")
//相当于
"process.env.NODE_ENV": '"production"'
}),
]
}
//dist/bundle.js
if ("production" === 'production') {
console.log('production');
}
这样编译出来的代码就没有问题了。
在上面的代码中我们发现都是手动来生成index.html,然后引入打包后的bundle文件,但是这样太过繁琐,而且如果生成的bundle文件引入了hash值,每次生成的文件名称不一样,因此我们需要一个自动生成html的插件;首先我们需要安装这个插件:
npm install --save-dev html-webpack-plugin
在demo3中,我们生成了三个不同的bundle.js,我们希望在三个不同的页面能分别引入这三个文件,如下修改config文件:
module.exports = {
//省略其他代码
plugins: [
new HtmlWebpackPlugin({
//引用的模板文件
template: './index.html',
//生成的html名称
filename: 'home.html',
chunks: ['home']
}),
new HtmlWebpackPlugin({
template: './index.html',
filename: 'list.html',
chunks: ['list']
}),
new HtmlWebpackPlugin({
template: './index.html',
filename: 'detail.html',
chunks: ['detail']
}),
]
}
我们以index.html作为模板文件,生成home、list、detail三个不同的页面,并且通过chunks分别引入不同的bundle;如果这里不写chunks,每个页面就会引入所有生成出来的bundle。
html-webpack-plugin还支持以下字段:
new HtmlWebpackPlugin({
template: './index.html',
filename: 'all.html',
//页面注入title
title: 'html webpack plugin title',
//默认引入所有的chunks链接
chunks: 'all',
//注入页面位置
inject: true,
//启用hash
hash: true,
favicon: '',
//插入meta标签
meta: {
'viewport': 'width=device-width, initial-scale=1.0'
},
minify: {
//清除script标签引号
removeAttributeQuotes: true,
//清除html中的注释
removeComments: true,
//清除html中的空格、换行符
//将html压缩成一行
collapseWhitespace: false,
//压缩html的行内样式成一行
minifyCSS: true,
//清除内容为空的元素(慎用)
removeEmptyElements: false,
//清除style和link标签的type属性
removeStyleLinkTypeAttributes: false
}
}),
上面设置title后需要在模板文件中设置模板字符串:
<title><%= htmlWebpackPlugin.options.title %>title>
loader用于对模块module的源码进行转换,默认webpack只能识别commonjs代码,但是我们在代码中会引入比如vue、ts、less等文件,webpack就处理不过来了;loader拓展了webpack处理多种文件类型的能力,将这些文件转换成浏览器能够渲染的js、css。
module.rules
允许我们配置多个loader,能够很清晰的看出当前文件类型应用了哪些loader,loader的代码均在demo4中。
{
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {}
}
},
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
},
]
}
}
我们可以看到rules属性值是一个数组,每个数组对象表示了不同的匹配规则;test属性时一个正则表达式,匹配不同的文件后缀;use表示匹配了这个文件后调用什么loader来处理,当有多个loader的时候,use就需要用到数组。
多个loader支持链式传递,能够对资源进行流水线处理,上一个loader处理的返回值传递给下一个loader;loader处理有一个优先级,从右到左,从下到上;在上面demo中对css的处理就遵从了这个优先级,css-loader先处理,处理好了再给style-loader;因此我们写loader的时候也要注意前后顺序。
css-loader和style-loader从名称看起来功能很相似,然而两者的功能有着很大的区别,但是他们经常会成对使用;安装方法:
npm i -D css-loader style-loader
css-loader用来解释@import和url();style-loader用来将css-loader生成的样式表通过