本文主要讨论webpack4的基础配置信息。
webpack可识别ES6 module模块引入方式(import引入语句)、CommonJs模块或者AMD、CMD等,即是一个模块打包工具,可以识别任何模块引入的语法,是基于node.js开发的模块打包工具,本质上是由node实现的。
本质上,webpack是一个现代JavaScript应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。
webpack是一个静态模块打包器,静态模块包括脚本、样式表和图片等等;webpack打包时首先遍历所有的静态资源,根据资源的引用,构建出一个依赖关系图,然后再将模块划分,打包出一个或多个bundle。
npm install webpack webpack-cli –g
webpack [] -o
//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
//webpack.config.js
var path = require('path');
module.exports = {
mode: 'development',
//入口文件
entry: './index.js',
//输出目录
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
};
var path = require('path');
//env:环境对象
module.exports = function(env, argv){
return {
//其他配置
entry: './index.js',
output: {}
}
};
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会出现在我们的代码中,比如module.exports;而Chunk经常和entry一起出现,Bundle和output一起出现。
可以通过下面这张图看下:
简单总结下可以理解为module,chunk和bundle 是同一份逻辑代码在不同转换场景下取了三个名字:我们直接写出来的是module,webpack处理时是chunk,最后生成浏览器可以直接运行的是bundle。
在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');
}
现在是不是明白啦。
npm install --save-dev html-webpack-plugin
// 我们生成了三个不同的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']
}),
]
}
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后需要在模板文件中设置模板字符串:
<%= htmlWebpackPlugin.options.title %>
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:
npm i -D css-loader style-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会添加-moz、-ms、-webkit等浏览器私有前缀;提供了很多对样式的扩展功能。
npm i -D postcss-loader
所以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'
}]
}]
可以将高版本的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')]
}]
}
两者都是用来处理图片、字体图标等文件;
所以我们会优先使用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还是引用了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删除并且通过下面的方式来引用图片:
即处理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', // 开发环境
}
该插件主要用在打包前清理上一次项目生成的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',
})
]
}
之前的样式都是通过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",
})
]
}
可以发现虽然配置了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() // 引入插件
]
}
我们在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可以帮我们很快的加载想要引入的模块,而不用require。但是不能随意引入,建议引入常用的模块,比如jQuery、vue、lodash等
// 一般我们加载jQuery需要import,再使用
import $ from 'jquery'
$('.box').html('box')
// 使用ProvidePlugin之后,我们可以这么配置
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
}),
]
}
现在有没有对webpack有了基本的认识呢