本文主要讨论webpack4的基础配置信息。
webpack是什么?
webpack可识别ES6 module模块引入方式(import引入语句)、CommonJs模块或者AMD、CMD等,即是一个模块打包工具,可以识别任何模块引入的语法,是基于node.js开发的模块打包工具,本质上是由node实现的。
本质上,webpack是一个现代JavaScript应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。
webpack是一个静态模块打包器,静态模块包括脚本、样式表和图片等等;webpack打包时首先遍历所有的静态资源,根据资源的引用,构建出一个依赖关系图,然后再将模块划分,打包出一个或多个bundle。
webpack可以做什么?
- Tree shaking(代码懒加载)
- 代码分割
…
webpack4的特点
- 速度更快,大型项目节约90%的构建时间
- 内置了更多默认配置,变更了许多API
webpack安装方式
- 全局安装npm install webpack webpack-cli –g,不建议使用,因为有时候项目里的webpack和全局安装的版本号可能不一致,会有一些问题。
- 在项目内安装webpack,npm install webpack webpack-cli --save -dev
webpack的核心概念
- 入口(entry):指示webpack应该使用哪个模块,来作为构建其内部依赖图的开始
- 输出(output):在哪里输出它所创建的bundles
- loader:让webpack能够去处理那些非JavaScript 文件
- 插件(plugins):用于执行范围更广的任务
实现一个webpack打包器
- 全局安装webpack:
npm install webpack webpack-cli –g
- webpack可以不使用配置文件,直接通过命令行构建,用法如下:
webpack <entry> [<entry>] -o <output>
// entry和output对应上述概念中的入口和输入
- 新建一个入口文件index.js
//demo1/index.js
var a = 1
console.log(a)
document.write('hello webpack')
- 命令行定义输入和输出路径
webpack index.js -o dist/bundle.js
// 执行完上述代码之后,webpack就会在dist目录生成打包后的文件。
注意:命令行的打包方式仅限于简单的项目,如果项目较为复杂,有多个入口,我们不可能每次打包都把入口记下来;因此一般项目中都使用配置文件来进行打包;配置文件的命令方式如下:
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'
},
}
一个页面可能不止一个模块,因此需要将多个依赖文件一起注入,这时就需要用到数组了:
module.exports = {
entry: [
'./src/header.js', // 头部模块
'./src/index.js', // 主模块
'./src/foot.js' // 底部模块
],
}
一个项目可能有不止一个页面,需要将多个页面分开打包,entry支持传入对象的形式:
//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:我们在平常项目中的写的源码,无论是commonjs还是amdjs,都可以理解为一个module;
- chunk:当module源文件传到webpack进行打包时,webpack会根据文件引用关系生成chunk文件,webpack 会对这些chunk文件进行一些操作;
- bundle:webpack处理好chunk文件后,最后输出bundle文件,bundle文件包含了经过加载和编译的最终源文件,所以它可以直接在浏览器中运行。
可以通过下面这张图看下:
简单总结下可以理解为module,chunk和bundle 是同一份逻辑代码在不同转换场景下取了三个名字:我们直接写出来的是module,webpack处理时是chunk,最后生成浏览器可以直接运行的是bundle。
hash、chunkhash、contenthash
- hash:和整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值。
- chunkhash:和入口文件的构建有关,根据入口文件构建对应的chunk,生成每个chunk对应的hash;入口文件更改,对应chunk的hash值会更改。
- contenthash:和文件内容本身相关,根据文件内容创建出唯一hash,也就是说文件内容更改,hash就更改。
模式
在webpack2、webpack3中需要手动加入插件来进行代码压缩、环境变量的定义,还需注意环境的判断,非常繁琐;webpack4中直接提供了模式这一配置,开箱即用;忽略配置的话,webpack会发出警告。
// 开发环境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 = {
// 生产模式不用对开发友好,只需要关注打包的性能和生成更小体积的bundle
mode: 'production',
};
// 相当于
module.exports = {
plugins: [
new UglifyJsPlugin(/*...*/),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production")
}),
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.NoEmitOnErrorsPlugin()
]
}
这块我们可能会有一个疑问是,在DefinePlugin插件里面,“production"为什么不能直接写,而要通过JSON.stringify(“production”) 转一下,下来我们通过分析来看一下:
JSON.stringify(“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变量,因此会导致代码直接报错,所以我们需要使用JSON.stringify包裹一层,这些是不是明白了
if (production === 'production') {
console.log('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');
}
现在是不是明白啦。
html-webpack-plugin(自动生成页面)
安装
npm install --save-dev html-webpack-plugin
demo
// 我们生成了三个不同的bundle.js,希望在三个不同的页面能分别引入这三个文件
module.exports = {
// 伪代码
plugins: [
new HtmlWebpackPlugin({
template: './index.html', // 模板文件
filename: 'home.html', // 生成的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。
具体字段信息如下
new HtmlWebpackPlugin({
template: './index.html', // 模板文件
filename: 'all.html', // 生成的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
loader用于对模块module的源码进行转换,默认webpack只能识别commonjs代码,但是代码中会引入比如vue、ts、less等文件,webpack就不能处理;loader拓展了webpack处理多种文件类型的能力,将这些文件转换成浏览器能够渲染的js、css。
我们可以用过module.rules配置多个loader,具体配置方法如下:
{
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处理有一个优先级,从右到左,从下到上;在上面代码中我们可以看到,css-loader先处理,处理好了再给style-loader;所以在写loader的时候要注意前后顺序。
下来我们来看一些常用的loader:
css-loader和style-loader
安装
npm i -D css-loader style-loader
区别
- css-loader:解释@import和url();
- style-loader:将css-loader生成的样式表通过style标签插入页面中
sass-loader和less-loader
安装
npm i -D sass-loader less-loader node-sass
使用
{
//其他配置
rules: {
test: /\.scss$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
},{
loader: 'sass-loader'
}]
},{
test: /\.less$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
},{
loader: 'less-loader'
}]
}
}
postcss-loader
安装
postcss-loader会添加-moz、-ms、-webkit等浏览器私有前缀;提供了很多对样式的扩展功能。
npm i -D postcss-loader
功能
- 第一把css解析成JS可以操作的抽象语法树AST
- 第二调用插件来处理AST并得到结果
所以postcss一般都是通过插件来处理css,并不会直接处理,所以我们一般在使用时需要先安装一些插件:
npm i -D autoprefixer postcss-plugins-px2rem cssnano
在项目根目录新建一个.browserslistrc文件:
> 0.25%
last 2 versions
将postcss的配置单独提取到项目根目录下的postcss.config.js:
module.exports = {
plugins: [
//自动添加前缀
require('autoprefixer'),
//px转为rem,应用于移动端
require('postcss-plugins-px2rem')({ remUnit: 75 }),
//优化合并css
require('cssnano'),
]
}
有了上述这些插件之后,就可以直接使用啦,打包后的css就自动加上了前缀了。
使用
rules: [{
test: /\.scss$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
}, {
loader: 'postcss-loader'
},{
loader: 'sass-loader'
}]
},{
test: /\.less$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
}, {
loader: 'postcss-loader'
},{
loader: 'less-loader'
}]
}]
babel-loader
可以将高版本的ES6甚至ES7转为ES5;我们来安装babel所需要的依赖:
安装
npm i -D babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime
npm i -S @babel/runtime
使用
{
rules: [{
test: /\.js/,
use: {
loader: 'babel-loader'
}
}]
}
我们把babel的配置提取到根目录,新建一个.babelrc文件:
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}
babel-loader的转译速度很慢,我们可以通过加入时间插件后看到每个loader的耗时,babel-loader是最耗时间;因此我们要尽可能少的使用babel来转译文件,我们对config进行改进:
// 1.正则上使用$来进行精确匹配
// 2.通过exclude将node_modules中的文件进行排除
// 3.include将只匹配src中的文件;可以看出include的范围比exclude更缩小更精确,推荐使用include。
{
rules: [{
test: /\.js$/,
use: {
loader: 'babel-loader'
},
// exclude: /node_modules/,
include: [path.resolve(__dirname, 'src')]
}]
}
file-loader和url-loader
两者都是用来处理图片、字体图标等文件;
- url-loader:当文件大小小于limit参数,将文件转为base-64编码,用于减少http请求
- file-loader:当文件大小大于limit参数时,调用file-loader进行处理
所以我们会优先使用url-loader。
安装
安装url-loader之前需要先安装file-loader
npm i file-loader url-loader -D
使用
{
//省略其他配置
rules: [{
test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
use: {
loader: 'url-loader',
options: {
//10k
limit: 10240,
//生成资源名称
name: '[name].[hash:8].[ext]',
//生成资源的路径
outputPath: 'imgs/'
},
exclude: /node_modules/,
}
}]
}
html-withimg-loader
我们在页面上引用一个图片,会发现打包后的html还是引用了src目录下的图片,这样明显是错误的,我们还需要一个插件对html引用的图片进行处理:
安装
npm i -D html-withimg-loader
使用
{
rules: [{
test: /\.(htm|html)$/,
use: {
loader: 'html-withimg-loader'
}
}]
}
// url-loader处理如下
use: {
loader: 'url-loader',
options: {
//10k
limit: 10240,
esModule: false
}
}
注意:
html-withimg-loader会导致html-webpack-plugin插件注入title的模板字符串<%= htmlWebpackPlugin.options.title %>失效,原封不动的展示在页面上;因此,如果我们想保留两者的功能需要在配置文件config中把html-withimg-loader删除并且通过下面的方式来引用图片:
<img src="<%=require('./src/bg1.png') %>" alt="" srcset="">
vue-loader
即处理vue文件的。
安装
npm i -D vue-loader vue-template-compiler
npm i -S vue
使用
vue-loader和其他loader不太一样,除了将它和.vue文件绑定之外,还需要引入它的一个插件:
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}]
},
plugins: [
new VueLoaderPlugin(),
]
}
这样我们就可以写vue代码啦。
搭建开发环境
上面我们是通过命令行打包生成dist文件,然后直接打开html或者通过static-server来查看页面的;但如果开发中我们写完代码每次都打包会很影响开发效率,我们希望写完代码立即看到页面的效果;
webpack-dev-server提供了一个简单的web服务器,能够实时重新加载。用法和wepack一样,但是他会额外启动一个express的服务器。
安装
npm i -D webpack webpack-dev-server
使用
// webpack.dev.config.js
module.exports = {
devServer: {
//启动服务器端口
port: 9000,
//默认是localhost,只能本地访问
host: "0.0.0.0",
//自动打开浏览器
open: false,
//启用模块热替换
hot: true,
//启用gzip压缩
compress: true
},
plugins: [
//热更新插件
new webpack.HotModuleReplacementPlugin({
})
]
}
通过命令行webpack-dev-server来启动服务器,启动后发现根目录并没有生成任何文件,因为webpack打包到了内存中,不生成文件的原因在于访问内存中的代码比访问文件中的代码更快。
在public/index.html的页面上有时候会引用一些本地的静态文件,直接打开页面会发现这些静态文件的引用失效了,可修改server的工作目录,同时指定多个静态资源的目录:
contentBase: [
path.join(__dirname, "public"),
path.join(__dirname, "assets")
]
热更新
热更新(Hot Module Replacemen简称HMR)是在对代码进行修改并保存之后,webpack对代码重新打包,并且将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样就能在不刷新浏览器的前提下实现页面的更新。
通过浏览器我们可以发现浏览器和webpack-dev-server之间是通过一个websock进行连接,初始化的时候client端保存了一个打包后的hash值;每次更新时server监听文件改动,生成一个最新的hash值再通过websocket推送给client端,client端对比两次hash值后向服务器发起请求返回更新后的模块文件进行替换。
如何映射源码
经过webpack模块封装后,很难理解原代码含义,因此,我们需要将编译后的代码映射回源码;devtool中不同的配置有不同的效果和速度,我们一般在开发环境使用cheap-module-eval-source-map,在生产环境使用source-map。
module.exports = {
devtool: 'cheap-module-eval-source-map', // 开发环境
}
通过一张表看下其他模式:
plugin
clean-webpack-plugin
该插件主要用在打包前清理上一次项目生成的bundle文件,它会根据output.path自动清理文件夹;这个插件在生产环境用的频率非常高,因为生产环境经常会通过hash生成很多bundle文件,如果不进行清理的话每次都会生成新的,导致文件夹非常庞大;这个插件安装使用非常方便:
安装
npm i -D clean-webpack-plugin
配置
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin(), // 清理上一次项目生成的bundle文件
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
})
]
}
mini-css-extract-plugin
之前的样式都是通过style-loader插入到页面中去,但生产环境需要单独抽离样式文件,mini-css-extract-plugin可帮我们从js中剥离样式,安装如下:
安装
npm i -D mini-css-extract-plugin
一般在开发环境使用style-loader,生产环境使用mini-css-extract-plugin,引入loader后,还需要配置plugin,提取的css同样支持output.filename中的占位符字符串。
配置
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.less/,
use: [{
loader: isDev ? 'style-loader' : MiniCssExtractPlugin.loader
},{
loader: 'css-loader'
},{
loader: 'less-loader'
}]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash:8].css",
})
]
}
optimize-css-assets-webpack-plugin
可以发现虽然配置了production模式,打包出的js压缩了,但是css确没有压缩;在生产环境我们需要对css压缩:
安装
npm i optimize-css-assets-webpack-plugin -D
配置
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
plugins: [
new OptimizeCSSAssetsPlugin() // 引入插件
]
}
copy-webpack-plugin
我们在public/index.html中引入了静态资源,但是打包的时候webpack并不会帮我们拷贝到dist目录,因此copy-webpack-plugin就可以很好地帮助我们做拷贝的工作。
安装
npm i -D copy-webpack-plugin
配置
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: 'public/js/*.js', // 拷贝的源路径
to: path.resolve(__dirname, 'dist', 'js'), // 拷贝的目标路径:
flatten: true,
}
]
}),
]
}
ProvidePlugin
ProvidePlugin可以帮我们很快的加载想要引入的模块,而不用require。但是不能随意引入,建议引入常用的模块,比如jQuery、vue、lodash等
// 一般我们加载jQuery需要import,再使用
import $ from 'jquery'
$('.box').html('box')
// 使用ProvidePlugin之后,我们可以这么配置
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
}),
]
}
plugin和loader的区别
- loader: webpack只能识别js,loader相当于翻译官,帮webpack对其他类型的资源进行转译的预处理工作
- plugins: 扩展了webpack的功能,在webpack运行时会广播很多事件,plugin可以监听这些事件,然后通过webpack提供的API来改变输出结果,即实现loader无法实现的工作。
现在有没有对webpack有了基本的认识呢