本文是基础教程,意在让大家快速了解wepback主要功能,同时也是个人学习webpack时的笔记整理。
倔强青铜——初识webpack
初始化项目
创建项目文件夹,执行以下命令,生成package.json文件,初始化项目。
npm init
package.json文件如下,它包含项目的一些基本信息。
{
"name": "aaa",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
安装webpack
//局部安装
npm install webpack webpack-cli -D
//在webpack4中,由于webpack内核和webpack-cli分离,因此要同时安装webpack和webpack-cli。
-D 是
npm install --save-dev
的简写,表示安装模块并保存到pageage.json中的devDependencies属性,它表示项目开发所需要的模块。-S 是
npm install --save
的简写,表示安装模块并保存到pageage.json中的dependencies属性,dependencies字段指定了项目运行所依赖的模块。
此时package.json中devDependencies属性如下:
"devDependencies": {
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0"
}
根目录创建src文件夹及main.js文件。
命令行输入.\node_modules\.bin\webpack .\src\main.js,提示编译成功,此时根目录下多出了dist文件夹及其子文件main.js,也就是我们打包出来的文件。
需要注意的是:
在命令行打包时需要将“/”改成“\”,否则会提示“不是内部或外部命令,也不是可运行的程序 或批处理文件”。node_modules\.bin\webpack src\main.js
命令行中 / 后面的是命令所使用的参数, \ 是文件的所在路径。
可以看到在打包结果的后面跟着个warning信息,提示我们没有设置打包模式mode(规定当前是开发环境还是生产环境),这个warning在webpack2中是不会有的,但是在webpack4中,mode变成了一个重要概念(webpack会根据我们设置的mode默认为我们开启一些配置)。可以通过在命令后面跟上--mode development指定为开发环境或是--mode production指定为生产环境解决。
node_modules\.bin\webpack src\main.js --mode development
如果你实在是不想多敲两个字,可以使用npx webpack代替node_modules\.bin\webpack,npx是什么呢?
为什么我们没有指定打包输出的路径和文件名,webpack却可以打包输出呢?从webpack4开始支持零配置打包(固定的入口src/index.js和固定的出口dist/main.js),让我们看看它长啥样:
const path = require('path')
module.exports = {
entry: path.resolve(__dirname, 'src/index.js'), //打包入口
output: {
filename: main.js,
path: path.resolve(__dirname, 'dist')
}
}
也就是说,webpack默认会以src下的index.js作为入口,如果存在,只需要执行.\node_modules\.bin\webpack就可以直接执行打包,不用添加文件及路径;如果不是src/index.js,就需要指定入口文件,比如我们的.\node_modules\.bin\webpack .\src\main.js;如果没有src/index.js文件并且在打包的时候没有指定入口文件,是会报错的。
context配置根目录
const path = require('path')
module.exports = {
context: __dirname, //默认为启动webpack时所在的当前工作目录
entry: path.resolve(__dirname, 'src/index.js'), //打包入口
}
如果将context设置成path.resolve(__dirname, 'src'),则寻找相对路径文件时,都是以相对于context的路径来描述。比如上面的entry可以写成entry: './index.js'。
entry配置
entry是打包的入口,它有多种写法:
1.字符串形式(对象形式缩写),一般用在单页应用: entry: path.resolve(__dirname, 'src/main.js')
2.1对象形式,一般用在多页应用: entry: { main: path.resolve(__dirname, 'src/main.js')}
2.2 entry: {
main: path.resolve(__dirname, 'src/main.js'),
search: path.resolve(__dirname, 'src/search.js')
}
3.1数组形式 entry,一般用在dll动态链接库: ['/src/main.js', './src/search.js']
3.2 entry: {
main: ['/src/main.js', './src/search.js']
search: ['/src/main2.js', './src/search2.js']
}
4.函数形式:函数返回上面三种任意一种
其中,字符串形式
entry: path.resolve(__dirname, 'src/main.js')
等同于
entry: {
main: path.resolve(__dirname, 'src/main.js')
}
默认是以main为键的对象形式。
数组形式最后的文件是按数组排列顺序来产出的。比如main: ['/src/a.js', './src/b.js'],a.js输出a,b.js输出b,那么最后输出的文件main.js输出a、b。一般用于单页面应用。有时用在js动态兼容上。
其中,entry代表打包的入口,webpack在打包的时候,会从入口指定的文件开始打包,而这些入口文件可能会通过import等形式又导入其它文件,而其它文件可能还导入了其它文件。。。这就形成了一个依赖图,它就像一棵树一样,把树根看作入口,那么对于树来说,它有很多的枝干,虽然多,但是从入口开始遍历,一定能找到所有枝干。找的这个过程,由webpack帮我们做了。
output配置
output表示打包的出口,可以发现在我们没有任何配置的时候进行打包,会在根目录创建dist文件夹,并输出main.js文件。
output有两个基本的属性,filename表示要输出时的(路径)文件名,path表示资源(所有)输出的公共目录。
我们可以通过-o命令指定打包的输出路径。
.\node_modules\.bin\webpack .\src\main.js -o .\disttemp
webpack5中新加入了--output-filename
指定输出的文件名。
.\node_modules\.bin\webpack .\src\main.js --output-filename main2.js
在之前的版本中,可以通过-o同时指定输出的文件路径和文件名。
.\node_modules\.bin\webpack .\src\main.js -o .\disttemp\main2.js
NPM script
每次打包都需要输入一串代码(.\node_modules\.bin\webpack .\src\main.js),那么有没有什么简短的方式呢?
npm run如果不加任何参数,直接运行,会列出package.json中所有可以执行的脚本命令。
npm内置了两个命令简写,npm test和npm start,它们是npm run xxx的简写形式。
npm run会创建一个shell,执行指定的命令,并**临时将node_modules/.bin加入环境变量PATH,**这就是我们能直接在npm script中访问模块的原因。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack ./src/main.js"
}
模块在安装的时候会在./node_modules/.bin下创建软链接,而npm script是可以直接访问到命令的。后面我们打包只需要npm run dev,就会执行打包命令。
秩序白银——webpack配置文件
我们在根目录下创建webpack配置文件,默认文件名为webpack.config.js:
将我们刚刚推测的默认配置写上:
const path = require('path')
module.exports = {
entry: path.resolve(__dirname, 'src/main.js'),
output: {
filename: main.js,
path: path.resolve(__dirname, 'dist')
}
}
你也可以这么写:
entry: {
main: path.resolve(__dirname, 'src/main.js')
}
在我们没有配置文件时,我们执行webpack命令打包需要显式指定一个入口,当我们配置了webpack.config.js后,在package.json中的webpack ./src/main.js
命令我们只需要保留webpack
就可以了。webpack会自动读取该配置文件进行打包。
如果我给配置文件换个名字呢?
嗯?耗子喂汁
也不是不行。
我们在vue-cli或其他框架中会看到它们会对生产环境配置和开发环境进行区分。比如公共配置webpack.base.conf.js、开发环境配置webpack.dev.conf.js以及生产环境配置webpack.prod.conf.js。
如果使用自定义的配置文件名,我们在打包的时候就需要使用--config显示指定我们的配置文件和它所在的路径:
package.json
webpack --config ./build/webpack.dev.conf.js
entry为字符串或数组形式的情况下,output为一个文件名好理解,那么当我们需要打包的是多页面的时候该怎么配置呢?
src文件夹下新添加一个search.js,而后修改webpack.config.js文件如下:
const path = require('path')
module.exports = {
entry: {
main: path.resolve(__dirname, 'src/main.js'),
search: path.resolve(__dirname, 'src/search.js')
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}
output中的[name]表示占位符,什么意思呢,它所对应的就是入口entry对象的键名。我们执行打包命令,最后dist目录下会生成main.js和search.js文件。
如果多个入口打包到一个出口会怎么样?
module.exports = {
entry: {
main: path.resolve(__dirname, 'src/main.js'),
search: path.resolve(__dirname, 'src/search.js')
},
output: {
filename: 'test.js',
path: path.resolve(__dirname, 'dist')
}
}
多个入口打包到一个出口是不被允许的。
倒腾了这么久,也该看看打包后该怎么用了。
我们同时给main.js和search.js加入一些“标记”(打印几句话),然后进行打包。
打包完成后,在dist目录下创建index.html文件并引入打包好后的js文件,打开页面后F12(ctrl+shift+i)进入开发者工具,可以看到我们的控制台打印了刚刚的结果。
webpack之loader
webpack自身只能解析js文件,但是前端应用中的资源多的很,比如less文件或者图片资源,webpack是解析不了的,如何让webpack识别并解析它们,就要靠loader帮我们解决了。loader的执行是从右往左,从下往上的(可以通过enforce属性对执行顺序进行改变)。
从webpack2开始,默认支持导入json文件,所以在有的教程上会看到webpack能解析js文件和json文件这句话。但是当你使用自定义文件扩展名时,仍需要添加json-loader。
file-loader
file-loader用来解析文件。官网说的老清楚了。我们首先安装它:
npm install -D file-loader
我们在main.js中引入一张图片,我将它放在了src文件夹下。此时的项目结构如下:
(对没错,我的大儿子)
(对没错,还有个小儿子)
loader是通过module下的rules进行配置的,rule是一个数组,每一项都是一个对象,代表对某个资源的解析方式。每个对象有四个基本的属性:
-
test:匹配的文件。
-
use:应用的loader列表,可以是一个字符串,如
use: 'file-loader'
表示对匹配到的资源应用一个loader,可以是一个数组,表示使用多个loader。如use: ['file-loader', {loader: 'xxxx-loader',options: { ... }}]。其中loader表示要使用的单个loader,options表示loader的配置,每个loader都有自己的配置属性。
-
include:只匹配include所包含的文件。
-
exclude:不匹配exclude所包含的文件。
-
enforce:'post' | 'pre',代表执行顺序,没有配置则为普通loader,执行顺序为pre->普通loader->post。
我们修改main.js
console.log('This is main.js');
import imgSrc from './2.jpg'
var imgTag = new Image();
imgTag.src = imgSrc;
document.body.appendChild(imgTag);
webpack.config.js
module.exports = {
entry: {
main: path.resolve(__dirname, 'src/main.js'),
search: path.resolve(__dirname, 'src/search.js')
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'file-loader',
//outputPath: 'img' //图片输出的目录,默认为打包输出路径下,此处是进行拼接
}
}
]
}
}
执行打包命令npm run dev
,打开dist文件夹下index.html,可以看到图片已经被追加到页面上了。
url-loader——解析文件资源
url-loader和file-loader很像,区别就是url-loader有一个指定大小的属性limit,大于limit的会交给file-loader处理,小于loader的转为dataUrl,内敛在html文件中。这么做可以减少网咯请求,虽然增加了一些文件体积。
导入file:///C:/Users/86133/Desktop/webpackTest/dist/2.jpg会在输出目录生成文件 file.png并返回 public URL,即file:///C:/Users/86133/Desktop/webpackTest/dist/2.jpg。
需要注意的是,url-loader必须和file-loader配合起来用,什么意思呢,你可以不配file-loader,但是你需要安装,否则在资源大于limit时,会提示你安装file-loader。来吧,展示!
安装url-loader
npm install -D url-loader
接着进行配置,有了url-loader就不需要配置file-loader了:
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 170 * 1024, //一般是使用8 * 1024,也就是8kb左右
//outputPath: 'img' //图片输出的目录,默认为打包输出路径下,此处是进行拼接
}
}
}
]
其中,limit的单位是byte。我的图片160k,我将它设为170,它最终应该转为dataUrl。
来打包瞄一眼,npm run dev
css-loader——解析css文件
css-loader是用来解析css文件的。
css会被解析成字符串:
\"* {\\r\\n background-color: red;\\r\\n}\"
首先进行安装:
npm install -D css-loader
webpack.config.js:
rules: [
{
test: /\.css$/,
use: ['css-loader']
}
]
然后在src文件夹下创建style.css,给body个背景色aqua,然后在main.js中引入。
import './style.css'
执行npm run dev进行打包。
打开index.html查看,发现样式没有生效。
为什么呢?
开发者工具审查元素看看网页结构,没有style标签,没有link标签,打开main.js看看。我们可以找到这么一段代码。
___CSS_LOADER_EXPORT___.push([module.i, "body {\n background-color: rgb(41, 85, 85);\n display: -webkit-box;\n display: -webkit-flex;\n display: -ms-flexbox;\n display: flex;\n -webkit-box-pack: center;\n -webkit-justify-content: center;\n -ms-flex-pack: center;\n justify-content: center;\n -webkit-box-align: center;\n -webkit-align-items: center;\n -ms-flex-align: center;\n align-items: center;\n}\", \"\"])"
css文件解析的结果被css-loader转换为一个js模块,push到一个数组里,这个数组是由 css-loader 内部的一个模块提供的,但是没有使用到这个数组。
我们说过,**loader只负责解析文件,**为了让css生效,我们需要将css-loader解析出来的结果通过style-loader处理,即通过style标签插入到head标签中,默认每个css文件都会生成一个style标签(可通过参数配置)。
style-loader——将解析的css挂载到style标签
npm install -D style-loader
更改webpack.config.js
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
loader的作用规则是从右往左依次进行的,我们需要先解析css文件然后再通过style标签挂载到页面。
打包后打开index.html可以发现页面背景色发生了改变。
sass-loader——解析sass/scss文件
sass-loader负责解析scss文件。
npm install -D sass-loader
webpack.config.js:
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
然后在src文件夹下创建style2.scss,给img个border,然后在main.js中引入。
import './style2.scss'
打包后打开网页查看效果。
需要注意的是sass-loader是依赖sass的,所以sass也需要进行安装。
postcss-loader——自动添加浏览器前缀
前端的部分css属性由于产商的不同需要进行兼容,而postcss结合autoprefixer能帮助我们自动添加浏览器前缀(注意autoprefixer需要指定添加前缀的范围)。
npm install -D postcss-loader autoprefixer
配置方式1:在根目录下添加配置文件postcss.config.js:
module.exports = {
plugins: [
require('autoprefixer')({
overrideBrowserslist: [
"Android 4.1",
"iOS 7.1",
"Chrome > 31",
"ff > 31",
"ie >= 8"
]
})
]
};
webpack.config.js:
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}
配置方式2:webpack.config.js配置文件中配置:
{
test: /\.css$/,
use: ['style-loader', 'css-loader', {
loader: 'postcss-loader',
options: {
plugins: [
require('autoprefixer')({
overrideBrowserslist: [
"Android 4.1",
"iOS 7.1",
"Chrome > 31",
"ff > 31",
"ie >= 8"
]
})
]
}
}]
}
注意:在webpack5中options下plugin属性已经没了,需要在options下的plugins外再包层postcssOptions(options->postcssOptions->plugins:[])。
配置方式3:package.json中设置browserslist字段:
"browserslist": [
"last 1 version",
"> 1%",
"IE 10"
]
webpack.base.config.js
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', {
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
"autoprefixer"
]
]
}
}
}]
}
配置方式4:配置方式3的webpack.base.config.js+根目录创建.browserslistrc文件:
last 2 version
> 1%
babel-loader——处理js语法
npm install -D babel-loader @babel/core @babel/preset-env
为什么安装个babel-loader还要安装@babel/core @babel/preset-env
babel 的 node API 已经被移到 babel-core 中,如果不安装@babel/core,webpack会提示你先进行安装。
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'] //这是babel-loader的写法
}
}
}
]
实际上除了在babel-loader的options选项中使用presets,还可以在项目根目录下创建.babelrc文件,它使用json格式。
.babelrc:
{ "presets": [["@babel/preset-env"]] }
webpack之plugins
plugin就是插件,但凡loader做不到的事,你可以找plugins。
html-webpack-plugin——自动创建index.html并引入文件
我们在看打包效果的时候,是在dist目录下创建index.html并手动引入打包的js文件和css文件,如果文件一多岂不凉凉——html-webpack-plugin帮助我们自动生成一个html文件并引入打包好的文件(如果你有多个 webpack 入口点, 他们都会在生成的HTML文件中的 script
标签内。如果有任何CSS assets 在webpack的输出中,例如, 利用ExtractTextPlugin提取CSS, 那么这些将被包含在HTML head中的<link>
标签内)。
npm install -D html-webpack-plugin
webpack.config.js:
var HtmlWebpackPlugin = require('html-webpack-plugin');
modules: {
...
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html')
})
]
其中,template指定一个项目中的html文件作为模板,不指定的话它也会在dist文件夹下自动生成一个html文件,那它有啥用呢,举个例子,比如你想用自己的网页title,而不是自动生成的网页的默认title,那么你就项目中创建个html文件,配置好你想配置的,让它作为模板。
删除dist目录下index.html后打包,发现dist目录下多出了index.html文件。
clean-webpack-plugin——清理构建输出目录
clean-webpack-plugin一般用在生产环境(因为开发环境使用的是devServer,一般不打包的,也就不存在构建目录了)。
如果我们修改了entry的键名,打包后输出的文件名也会变,那么dist目录会越来越多没有用的文件,因此我们引入clean-webpack-plugin帮助我们每次打包前自动清除打包目录。
npm install -D clean-webpack-plugin
webpack.base.conf.js
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
plugins: [
new CleanWebpackPlugin()
]
这样每次打包后我们都不需要自己删除没有用的文件了。
mini-css-extract-plugin——将style提取成单独的css文件
mini-css-extract-plugin一般用在生产环境。
npm i -D mini-css-extract-plugin
webpack.config.js:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module: {
rules: [
...
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}
]
},
plugins: [
...
new MiniCssExtractPlugin({ filename: '[name].css' //默认值是以chunk为文件名 })]
需要将'style-loader'替换成MiniCssExtractPlugin.loader,否则不生效。
打包后查看index.html,可以看到css通过link标签引入了。
mini-css-extract-plugin一般用在生产环境,开发环境一般使用style-loader,打包速度会更快(少引插件少做额外处理,而且开发环境也没有必要抽取成css文件)。但是如果实在想在开发使用该插件,建议这么使用(不知道contenthash是什么的建议学到文件指纹后再回来看这部分的内容):
new MiniCssExtractPlugin({ filename: devMode? '[name].css' : '[name]_[contenthash].css'})
即开发环境下使用固定的css文件名。如果使用的是contenthash作为文件名后缀,那么修改css后需要手动刷新才能得到修改的结果,这是因为热更新的缘故,在修改css后会重新构建,此时生成的css文件的contenthash会发生变化,而html-webpack-plugin自动生成的html文件对css的link引用还是之前那个名字。
修改前的文件名:
修改后的文件名:
刷新后的文件名:
terser-webpack-plugin——压缩js,多进程并行压缩(加快打包构建速度)
一般用在生产环境
npm install -D terser-webpack-plugin
webpack.prod.conf.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = merge(webpackBaseConfig, {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true //开启多进程打包
})
]
}
plugins: [
..
]
})
parallel:Type: Boolean|Number
Default: true
可以为它设置boolean或number值,为true时默认为CPU核心数-1,也可以设置具体数值。
optimize-css-assets-webpack-plugin——压缩css
一般用在生产环境。
npm install -D optimize-css-assets-webpack-plugin
webpack.prod.conf.js
var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = merge(webpackBaseConfig, {
mode: 'production',
plugins: [
...
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/g, //匹配的文件,默认为css文件
cssProcessor: require('cssnano') //默认的css处理器,可配置别的
})
]
})
压缩后的css文件大概长这样:
copy-webpack-plugin——拷贝资源
npm install -D copy-webpack-plugin
在src目录下新建image文件夹,放入一些图片,然后进行路径配置。
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
...
plugins: [
...
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, './src/image'), //需要拷贝的资源路径
to: path.resolve(__dirname, 'dist/static') //拷贝到哪儿
}
]
})
]
}
需要注意的是,现在的CopyWebpackPlugin需要传入配置对象的patterns属性来放置拷贝规则数组,之前的CopyWebpackPlugin是直接传入拷贝规则数组的。
荣耀黄金——webpack之devServer
在学习devServer之前,我们先插播一下文件监听:
文件监听
文件监听是在发现源码发生变化的时候,自动重新构建出输出文件(唯一缺陷是需要用户手动刷新浏览器)。
文件监听原理:
module.exports = {
watch: true,
watchOptions: { //只有在watch: true开启监听模式时才生效
//默认为空,不监听的文件或文件夹,支持正则匹配
ignored: /node_modules/,
//监听到变化发生后会等待300ms再执行,默认为300ms
aggregateTimeout: 300,
//每秒询问文件系统的次数,默认每秒1000次
poll: 1000
}
};
需要注意的是,我们需要先关闭CleanWebpackPlugin,否则监听到文件变化后会自动删除index.html文件。
devServer
devServer能开启一个简单的web服务器。简称WDS。devServer底层也是通过--watch对文件进行监听的。
npm install -D webpack-dev-server
webpack.config.js:
module.exports = {
...
devServer: {
contentBase: path.join(__dirname, "dist"), //将dist目录下的文件,作为可访问文件。
compress: true, //是否启用gzip压缩
port: 9000, //在该端口下打开服务
open: true, //是否自动打开浏览器
//hot: true //设置为true才能使热更新生效
},
plugins: []
}
其中compress表示开启gzip压缩,port表示开启服务的端口,open表示服务启动时自动打开浏览器。
接下来我们要使用WDS的专属命令了:
package.json
"dev": "webpack-dev-server"
如果是自定义的文件名,使用--config 文件路径文件名。
devServer的open参数可以在NPM script中使用--open代替。
"dev": "webpack-dev-server --config ./webpack.config.js --open"
open参数实际上除了Boolean值外还可以是String,用来指定开启的浏览器,设置为true则开启默认浏览器,可以指定为"Chrome"等。
"dev": "webpack-dev-server --config ./webpack.config.js --open Chrome"
注:使用webpack-dev-server打包后,是不会在项目文件中生成dist目录的,它会将文件打包到内存中。
模块热替换插件HotModuleReplacementPlugin
先前我们在修改项目文件时,如果修改了文件,需要重新打包,才能看到最新的效果。而模块热替换是在程序运行时,对修改部分的文件进行操作的,当你修改了一个地方,页面不会重新渲染。
HotModuleReplacementPlugin通常搭配derServer一起使用,它是webpack自带的。
webpack.config.js:
var webpack = require('webpack')
module.exports = {
devServer: {
...
hot: true //devServer默认是以刷新网页来更新的,设置为true才能使热更新生效
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
除了在webpack.config.js中配置hot:true之外,还可以在NPM script中使用--hot参数来开启热更新。
webpack5中配置devServer
"webpack": "^5.6.0",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.11.0"
在webpack-cli4中,cli文件结构发生了变化,从webpack-cli3迁移过来时,会出现
Error: Cannot find module 'webpack-cli/bin/config-yargs'的提示,是因为在webpack-cli4中,devServer的启动命令由webpack-dev-server变成webpack serve
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack serve"
}
然后就又可以开开心心装逼辣。
如果我不使用HMR插件,仅开启devServer{ hot:true }呢?
我就知道有人跟我一样好奇这个问题。
在安装了webpack-dev-server之后,我们可以在依赖包中看到这么一段代码:
node_modules/webpack-dev-server/utils/addEntries.js
if (options.hot || options.hotOnly) {
config.plugins = config.plugins || [];
if (
!config.plugins.find(
// Check for the name rather than the constructor reference in case
// there are multiple copies of webpack installed
(plugin) => plugin.constructor.name === 'HotModuleReplacementPlugin'
)
) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
}
什么意思呢,如果你配置了hot参数或hotOnly参数为true,那么webpack-dev-server就会检查你是否添加了HMR插件,如果没有添加的话,会自动帮你添加。嗯,果然还是逃不掉的吗。
尊贵铂金——webpack之webpack-merge
我们日常开发中使用的webpack配置以及打包上线的webpack配置有一定区分。
在webpack.config.js中,有个mode属性,它用来表示当前配置所应用的环境,默认是production生产环境。
webpack.config.js:
module.exports = {
mode: 'production', //不写等同于production
entry: {
...
}
}
它们有如下区别(截自webpack官网):
生产环境中会启用ModuleConcatenationPlugin插件。每个模块都会被webpack包装在单独的函数闭包中,这些包装函数使JavaScript在浏览器中的执行速度变慢。而ModuleConcatenationPlugin会将所有模块按引用顺序放在一个函数闭包中。可以通过new webpack.optimize.ModuleConcatenationPlugin()手动添加。
开发环境中会自动开启NamedModulesPlugins,webpack为了减小文件大小(所以不要在生产模式使用啊喂!),会对模块进行重命名,当程序报错的时候,是看不出来当前的错误来自哪个文件的。
开启前的报错:
开启后报错:
开启前的更新信息:
开启后的更新信息:
妙不妙?
生产模式会开启FlagDependencyUsagePlugin,会删除无用的代码,比如导入了某个模块之后未进行使用,那么这个模块是不会被打包的。
那么开发环境和生产环境有一些公共的配置,如果仅靠cv大法,那属实有点丑,也不好维护。怎么办呢?
webpack-merge
npm install -D webpack-merge
我们将配置文件重命名为webpack.base.conf.js作为我们的公共配置文件,同时创建一个生产环境使用的配置文件webpack.prod.conf.js以及开发环境下使用的配置文件webpack.dev.conf.js。
同时对配置做一些调整:
devServer官方建议不要放在生产环境,因此抽离devServer以及HotModuleReplacementPlugin这两兄弟到webpack.dev.conf.js中
webpack.base.conf.js:
const path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var {CleanWebpackPlugin} = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: {
main: path.resolve(__dirname, 'src/main.js'),
search: path.resolve(__dirname, 'src/search.js')
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 170 * 1024
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html')
}),
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename:'[name].css'
})
]
}
webpack.dev.conf.js:
const path = require('path');
const {merge} = require('webpack-merge')
var webpack = require('webpack')
const webpackBaseConfig = require('./webpack.base.conf.js')
module.exports = merge(webpackBaseConfig, {
mode: 'development',
devServer: {
contentBase: path.join(__dirname, "dist"),
compress: true,
port: 9000,
open: true,
hot: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
})
webpack.dev.conf.js:
const {merge} = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base.conf.js')
module.exports = merge(webpackBaseConfig, {
mode: 'production'
})
此时的项目目录结构如下:
由于我们的配置文件名改了,所以我们要修改package.json中的命令并增加生产模式的打包命令prod。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server --config ./webpack.dev.conf.js",
"dev2": "webpack --config ./webpack.dev.conf.js", //开发环境打包
"prod": "webpack --config ./webpack.prod.conf.js"
}
分别测试,功能正常。
特殊情况
有时候我们可能想在公共配置文件中进行”不同环境采取不同loader/plugin“的操作。通过刚刚的了解,我们知道当给mode参数设置为开发环境development(production)时,webpack会将process.env.NODE_ENV的值设为development(production),但是当我们在webpack配置文件中打印process.env.NODE_ENV会发现值为undefined,而不是所谓的development或production,哦!它设了个空气!
我们尝试在src/main.js中打印process.env.NODE_ENV,分别执行开发环境npm run dev2和生产环境npm run prod,打开控制台发现就是我们想要的值。
那怎么在配置文件中判断当前的环境呢?
在搜索引擎里连滚带爬了一阵子后,找了个“偏方”。
webpack.base.conf.js
const devMode = process.argv.indexOf('--mode=production') === -1;
需要配合NPM script使用:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server --config ./webpack.dev.conf.js",
"dev2": "webpack --mode=development --config ./webpack.dev.conf.js",
"prod": "webpack --mode=production --config ./webpack.prod.conf.js"
}
--mode用来指定当前环境。
如果配置文件中的mode与NPM script的--mode同时存在,是以--mode优先的。
然后就可以这么用了:
{
test: /\.css$/,
use: [ devMode? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
}
文件指纹策略
在学习文件指纹前,让我尝试用一张图勾起你们的回忆:
什么?没回忆起来?
我帮你回忆回忆:
文件指纹是打包输出的文件名后缀(注意后缀和扩展名的区别)
有什么用呢?
举个栗子:你们公司有个打包上线了段时间的项目,有天你对某个文件如index.js进行了改动,重新发布。此时你的更新对于用户是无感知的,在用户客户端本地,先前已经对index.js资源进行了缓存,如果此时客户端的缓存还处于可用状态,那么就会直接从缓存中读取index.js文件,而你的更新对于这部分用户来说是察觉不到的,所以由于缓存的存在,当你需要获取新的代码时,就会出现不符预期的行为。(简而言之就是部署新版本时如果不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本)
这显然不符合我们的预期:对于已更新的内容,我们期望客户端发起资源的请求,对于没有更新的内容,我们期望客户端能直接从浏览器缓存中读取。
对没错,就是它!
先来看看常见的文件指纹有哪几种:
- Hash:和整个项目文件的构建有关,只要项目文件有修改,整个项目构建的hash值就会改变。
- Chunkhash:和webpack打包的chunk有关,不同的entry会生成不同的chunkhash值。
- Contenthash:根据文件内容生成的hash,文件内容不变,则contenthash不变。
文件指纹怎么用呢?
webpack.base.conf.js:
module.exports = {
entry: {
main: path.resolve(__dirname, 'src/main.js'),
search: path.resolve(__dirname, 'src/search.js')
},
output: {
filename: devMode? '[name].js' : '[name]_[hash].js', //生产环境下使用文件指纹
path: path.resolve(__dirname, 'dist')
}
}
search.js
console.log('This is search page')
main.js
import './style.css'console.log('This is main.js');
此时我们执行npm run prod
,生成的部分内容如下:
我们发现文件名都加上了该次构建相关的hash。此时解决了“更新文件后,用户浏览器依然读取缓存文件”的问题。因为每次修改文件重新打包都会改变hash值,所以文件名也会进行相应的改变。
如果我仅仅修改main.js文件呢?
import './style.css'console.log('This is main.js2');
执行打包npm run prod
:
输出的js文件的文件名(hash)全部发生了改变。换句话说就是重新发布后,用户需要重新请求所有资源。
要不。。。
跑路是不可能跑路的(主要是跑不过),那就只能改了!
这不是还有chunkhash吗
module.exports = {
entry: {
...
},
output: {
filename: devMode? '[name].js' : '[name]_[chunkhash].js',
path: path.resolve(__dirname, 'dist')
}
}
webpack,启动!
可以看到每个bundle都生成了各自的chunkhash,互不影响。此时继续修改main.js(main.js:别扶我,我还能改!)。
webpack,启动!
可以看到,修改了main.js后重新打包,search的chunkhash没有发生变化。对于用户浏览器来说,既能读取到最新的main.js,又能读取缓存中的search.js,美滋滋。
。
。
。
等等!还有个Contenthash呢?
电脑左上角手机右下角点赞可解锁新内容——Contenthash
我们看看上面几张图,当然不是表情包了啊喂!抽出它的共同点,发现了啥?
对没错,是main.css
如果我是修改css文件然后打包发布,是不是也会出现浏览器直接读取缓存文件的情况?
那么css文件需要使用的文件指纹是hash?chunkhash?
结合刚才所讲,hash肯定是不用考虑了(不缓存),chunkhash如何?
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: devMode? '[name].css' : '[name]_[chunkhash].css'
})
...
]
}
如果是这样的话,那么修改main.js,main.css的文件名也会发生变化。这显然也不合适。我们试试contenthash:
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: devMode? '[name].css' : '[name]_[contenthash].css'
})
...
]
}
webpack,启动!
修改main.js后再打包查看:
可以发现css文件依然是原本的文件名。但是我们修改css文件后查看,发现main.js和main.css文件名都改变了,这是因为main这个bundle的chunkhash因css文件的修改而改变了。
hash一般用在文件上:
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
}
总结一下:
js的文件指纹设置:设置output的filename,使用chunkhash。
css的文件指纹设置:设置MiniCssExtractPlugin的filename,使用contenthash。
图片等资源的文件指纹设置:设置file-loader的name,使用hash。
不要在开发环境使用 [chunkhash]/[hash]/[contenthash],因为不需要在开发环境做持久缓存,而且这样会增加编译时间,开发环境用 [name] 就可以了。
卸载游戏——搭建vue开发环境
安装vue-loader和vue-template-compiler
npm install -D vue-loader vue-template-compiler
其中vue-template-compiler用来编译template模板。
配置vue-loader
webpack.base.conf.js:
const vueLoaderPlugin = require('vue-loader/lib/plugin')
module: {
rules: [
{
test: /\.vue$/,
use: [
{
loader: 'vue-loader'
}
]
}
]
},
plugins: [
...
new vueLoaderPlugin()
]
main.js
import Vue from 'vue';
import App from './app.vue';
new Vue({
render: h=>h(App)
}).$mount('#app')
在src文件夹下新建app.vue文件
<template>
<div>
{{title}}
</div>
</template>
<script>
export default {
data() {
return {
title: "Hello Vue!"
}
}}
</script>
index.html文件
<!DOCTYPE html><html lang="en">
<head>
...
<title>my first webpack</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
webpack,启动~!(在这之前要安装vue的啊,别忘了)
快乐的很。