webpack学习笔记
由于上篇文章 在 angular6 中自定义 webpack 配置 中,需要在 angular6
中自定义 webpack
设置,所以在这里重新学习一下 webpack
,主要是写这个笔记,方便以后自己回顾。
本篇文章是我根据 B 站上的webpack 教学视频10天搞定webpack4,边看边写的,有兴趣的也可以看看。
注意:跟随视频敲代码的过程中,有很多包版本不对就会各种报错,我总结了一下,由于 webpack从4更新到 5 是两年前的事情,那么包就用三年前的问题应该不大
1. webpack的作用
webpack 可以实现代码转换,文件优化,代码分割,模块合并,自动刷新,代码校验,自动发布
2. webpack 安装
先全局安装 webpack
,由于 webpack4
以上的版本要使用相关命令的话,必须安装 webpack-cli
npm i webpack@4.28.2 -g
npm i webpack-cli@3.1.2 -g
上面的版本我们就按照视频中的来,保持一致
先创建文件夹,在文件夹中初始化一下(生成 package.json),再本地安装 webpack webpack-cli,还需要本地安装是为了方便使用 require
引入
npm init -y
npm intall webpack@4.28.2 webpack-cli@3.1.2 -D
安装成功后,webpack
可以进行 0 配置,但这没有意义,功能很弱。
在根目录下创建 src
文件夹,里面添加 index.js
文件,写一句 console.log('webpack4-notes')
之后可以运行 webpack
来打包,生成 dist
文件,结果如下:
其原理是,运行 webpack
命令时,会去找 node_modules/.bin/webpack.cmd
文件:
3. 自定义webpack
3.1 简单配置–入口和输出
默认配置文件是 webpack.config.js
,所以在根目录下创建 webpack.config.js
文件:
//webpack 是node 写出来的,是node 的写法
let path = require('path')
module.exports = {
mode:'development', //模式,默认两种:production--生产模式,development--开发模式
entry:'./src/index.js', //入口文件
output:{
filename:'bundle.js', //打包后的文件名
// path:'绝对路径', //打包后的文件位置:路径必须是一个绝对路径
path:path.resolve(__dirname,'dist'), //用到了内置的 path 模块,需要导入;path.resolve() 可以把相对路径解析成绝对路径,意思是 以当前目录解析出一个 dist 目录,将打包后的 bundle.js 放到 dist 里面去;__dirname 总是指向被执行 js 文件的绝对路径
}
}
完成后,就可以直接运行 webpack
进行打包了,此命令会去找 webpack.config.js
文件,打包结果如下:
此时想要查看打包后的结果是否能运行,就在 dist
目录下新建 index.html
,然后在浏览器中打开,如下图:
浏览器中的查看结果:
3.2 修改自定义 webpack 文件名称
如果你的配置文件不想叫 webpack.config.js
这个默认的名字,还可以自己改,比如 webpack.config.my.js
,如果还是运行 webpack
的话,就还是得到最开始的默认最弱的那个配置,结果是 main.js
,想要使用我们的 webpack.config.my.js
,就要指定配置文件,webpack --config webpack.config.my.js
,
3.3 本地查看打包后的效果(webpack-dev-server,html-webpack-plugin)
像我们上面这样查看打包的结果太麻烦了,每次改动后都要重新打包,还有手动添加 index.html
太麻烦了,所以我们可以使用 webpack
内置的服务 webpack-dev-server
,将打包后的结构丢到一个服务中,我们可以直接访问这个服务来查看结果
// 安装 webpack-dev-server 版本与视频一致
npm i webpack-dev-server@3.1.14 -D
视频中直接运行 webpack-dev-server
就可以了,但是我不行,就算重新安装包还是不行,在 package.json
中配置命令才行
之后运行 npm run dev
,就可以了,结果如下:
在浏览器中打开 http://localhost:8080/
:
这不是我们想要的结果,他是直接进到了当前目录,但是我们是想进入到 dist
文件夹中,可以直接访问里面的 index.html
,所以还需要在 webpack.config.js
中加上 devServer
配置
//webpack 是node 写出来的,是node 的写法
let path = require('path')
module.exports = {
devServer:{ //开发服务器的配置
port:3000, //指定端口
progress:true, //显示进度条
contentBase:"./dist", //以 dist 为静态服务,也就是访问 http://localhost:3000/时进入 dist 目录,就直接访问到了里面的 index.html 了
compress:true //启动压缩
},
mode:'development', //模式,默认两种:production--生产模式,development--开发模式
entry:'./src/index.js', //入口文件
output:{
filename:'bundle.js', //打包后的文件名
// path:'绝对路径', //打包后的文件位置:路径必须是一个绝对路径
path:path.resolve(__dirname,'dist'), //用到了内置的 path 模块,需要导入;path.resolve() 可以把相对路径解析成绝对路径,意思是 以当前目录解析出一个 dist 目录,将打包后的 bundle.js 放到 dist 里面去;__dirname 总是指向被执行 js 文件的绝对路径
}
}
再来运行一次 npm run dev
,结果如下:直接访问dist里面的 index.html
但是 dist
里面的 index.html
是我们手动加的,一般打包可能没有这个文件,不可能我们每次都手动加上这个文件然后再看效果吧,我们希望打包后能自动生成这个文件,此时我们需要一个插件(html-webpack-plugin
),作用是能帮我们把打包后的文件通过 script
标签的方式引入到我们项目自带的 index.html
中,并且把改造后的 index.html
直接放到打包(contentBase
)后文件目录下
安装插件:
npm i html-webpack-plugin -D
这里安装的是 “html-webpack-plugin”: “^5.3.2”,然后运行命令报错了:
将版本替换为 4.4.1的就好了
npm uninstall html-webpack-plugin -D
npm i html-webpack-plugin@4.4.1 -D
再添加插件配置:
//webpack 是node 写出来的,是node 的写法
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
devServer:{ //开发服务器的配置
port:3000, //指定端口
progress:true, //显示进度条
contentBase:"./dist", //以 dist 为静态服务
compress:true //启动压缩
},
mode:'development', //模式,默认两种:production--生产模式,development--开发模式
entry:'./src/index.js', //入口文件
output:{
filename:'bundle.js', //打包后的文件名
// path:'绝对路径', //打包后的文件位置:路径必须是一个绝对路径
path:path.resolve(__dirname,'dist'), //用到了内置的 path 模块,需要导入;path.resolve() 可以把相对路径解析成绝对路径,意思是 以当前目录解析出一个 dist 目录,将打包后的 bundle.js 放到 dist 里面去;__dirname 总是指向被执行 js 文件的绝对路径
},
plugins:[ //插件--数组形式,放着所有的 webpack 插件
new HtmlWebpackPlugin({
template:'./src/index.html', //以该文件为模板,所以要在 src 中新建 index.html 文件
filename:'index.html', //打包后要放在打包目录(dist)下的文件的名字
})
]
}
删除之前的 dist
文件夹后,再运行 npm run build
,就可以看到,dist
中自动就有 index.html
文件,运行npm run dev
后,内容区修改的话会自动更新的,但配置文件修改了就得重新运行了
3.4 生产环境打包–压缩代码,hash
接下来我们想如果在生产环境打包,代码要压缩,而且 打包后的目录下的 index.html
文件也要压缩
还可以添加其他的配置:压缩,hash
//webpack 是node 写出来的,是node 的写法
module.exports = {
plugins:[ //插件--数组形式,放着所有的 webpack 插件
new HtmlWebpackPlugin({
template:'./src/index.html', //以该文件为模板
filename:'index.html', //打包后要放在打包目录下的文件的名字
minify:{
removeAttributeQuotes:true, //删除 index.html 中属性的双引号(部分属性值带逗号的删不掉)
collapseWhitespace:true, //折叠,代码变成一行
},
hash:true, //index.html 中引入的 bundle.js 带 hash 戳,避免缓存(dist文件下的 bundle.js 名字不变,不带hash)
})
]
}
如果想所有打包后生成的文件都能带上 hash
,就要直接在 output.filename
中配置:
output:{
filename:'bundle.[hash].js', //打包后的bundle.js,带有 hash,比如 bundle.5c5c0e9b436ca27579xxx.js,每次有更新后的打包都可以生成不一样的名字,避免别覆盖
// filename:'bundle.[hash:8].js' //只显示 8 位的 hash 值,比如 bundle.1cfb6be1.js
},
3.5 支持 css,less,sass,及优化等
3.5.1 解析 css
我们现在 src 下创建一个 index.css,里面写一点样式,然后在 js 中引入此样式。有人问为什么不在 src\index.html 中引入样式呢,因为 index.html 是打包时的模板,这个模板是要原封不动的输出到 dist 中去的:
但是我们也可以看到,在 dist 文件里面是没有 index.css的,在 npm run dev
后,会报错:
所以不能在 html 中进行引入,而在 js 中通过 require 来,比如 require(’./index.css’):
但是由于 webpack 默认只能识别 javaScript,不识别css,所以又报错了:
提示需要一个合适的 loader 去解析这个模块,安装
npm i css-loader@2.1.0 style-loader@0.23.1 -D //照旧根据视频中的版本来,自己尝试随意安装就是报错
loader的作用:就是将我们的源代码进行转化,变成 webpack 能识别的代码
//webpack 是node 写出来的,是node 的写法
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'development', //模式,默认两种:production--生产模式,development--开发模式
entry:'./src/index.js', //入口文件
output:{
filename:'bundle.js', //打包后的文件名
path:path.resolve(__dirname,'dist'),
},
plugins:[ //省略,后面会有个配置的小结
],
module:{ //模块
rules:[ //规则
{
test:/\.css$/, //找到以 css 为后缀的文件
use:['style-loader','css-loader'] //先执行 css-loader,再执行 style-loader
}
]
}
}
- loader的特点:作用单一;
- loader的用法(也就是 use 的写法):
- 1.可以是字符串,那就只能用一个 loader ,比如
use:'css-loader'
- 2.使用多个loader时, 只能是一个数组,比如
use:['style-loader','css-loader']
- 3.如果有其他配置的话,loader还可以是个对象,比如
use:[{loader:'style-loader',options:{}},'css-loader']
- 1.可以是字符串,那就只能用一个 loader ,比如
- loader的顺序:从右向左执行,从下往上执行
为了体现上述部分特点,我们创建一个a.css
将之引入到index.css
中去:
npm run dev
运行结果:
上述我们打包后的样式时直接插在 head
标签的最底部,如果我们想要我们在 index.html
中自己写的样式生效,可以改变我们打包后 css 样式插入的位置
//webpack 是node 写出来的,是node 的写法
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
module:{ //模块
rules:[ //规则
{
test:/\.css$/, //找到以 css 为后缀的文件
use:[
{
loader:'style-loader',
options:{
insertAt:'top'
}
},
'css-loader'
] //先执行 css-loader,再执行 style-loader
}
]
}
}
npm run dev
运行结果:
3.5.2 解析 less,sass,stylus
我们可以在 src
中新建一个 index.less
文件,写一点样式:
然后也在 index.js
中引入:
require('./index.less')
安装处理 less
的 loader
:
npm i less@3.9.0 less-loader@4.1.0 -D
在 webpack.config.js
中进行配置
module:{ //模块
rules:[ //规则
{
test:/\.css$/, //找到以 css 为后缀的文件
use:[
{
loader:'style-loader',
options:{
insertAt:'top' //将解析后的 css 插入到 head 标签的最上方
}
},
'css-loader'
] //先执行 css-loader,再执行 style-loader
},
{ // 解析 less 文件
test:/\.less$/, //找到以 less 为后缀的文件
use:[
{
loader:'style-loader',
options:{
insertAt:'top' //将解析后的 css 插入到 head 标签的最上方
}
},
'css-loader', //解析 css 各种语法
'less-loader' //将 less 转化为 css
] //先执行 less-loader,再执行 css-loader,最后执行 style-loader
}
]
}
结果如下:
同理,sass 和 stylus 预处理器的解析都是一样的,需要 node-sass,sass-loader 和 stylus,stylus-loader
3.5.3 抽离 css (mini-css-extract-plugin)
我们上面解析后的样式文件全都放在了 head
标签里面,多了之后容易阻塞加载,所以想把解析后的样式文件单独处理出来,不放在 head
标签中了,为此有个插件可以帮助我们实现
安装抽离 css 的插件
npm i mini-css-extract-plugin@0.5.0 -D
let path = require('path') //绝对路径需要
let HtmlWebpackPlugin = require('html-webpack-plugin') //模板插件
let MiniCssExtractPlugin = require('mini-css-extract-plugin') //抽离 css 的插件
module.exports = {
plugins:[
new MiniCssExtractPlugin({ //作用:抽离 css
filename:'main.css' //抽离出来的 css 文件放在 这个文件中;
//那么我们还要决定哪些 css 文件,要放到这里来,
//所以 MiniCssExtractPlugin 提供了一个 loader,作用就跟 style-loader 是类似的
//可以在解析样式文件时,不使用 style-loader,而使用这个 MiniCssExtractPlugin.loader
})
],
module:{ //模块
rules:[ //规则
{
test:/\.css$/, //找到以 css 为后缀的文件
use:[
MiniCssExtractPlugin.loader, //解析css之后将文件放在 MiniCssExtractPlugin 配置的文件中去
/* { //这里我们不再把样式放在 head 标签中,而是放到 main.css 中
loader:'style-loader',
options:{
insertAt:'top' //将解析后的 css 插入到 head 标签的最上方
}
}, */
'css-loader'
] //先执行 css-loader,再执行 style-loader
},
{ // 解析 less 文件
test:/\.less$/, //找到以 less 为后缀的文件
use:[
{
loader:'style-loader', //将解析后的文件插入到打包后的 index.html 中的 head 标签中
options:{
insertAt:'top' //将解析后的 css 插入到 head 标签的最上方
}
},
'css-loader', //解析 css 各种语法
'less-loader' //将 less 转化为 css
] //先执行 less-loader,再执行 css-loader,最后执行 style-loader
}
]
}
}
从上面的配置可以看到,我们把 css 抽离到 main.css
中去了,但是,less
文件没有抽离,还是放在了 head
中,npm run dev
运行结果如下:
打包结果如下:
那么再扩展一下,我们想抽离到不同的文件中去,css文件抽离到 main.css 中,less文件抽离到 home.css 中,那么可以再加一个插件配置:
let MiniCssExtractPlugin = require('mini-css-extract-plugin') //抽离 css 的插件
let MiniCssExtractPlugin2 = require('mini-css-extract-plugin') //将 less 抽离到另一个文件中
module.exports = {
plugins:[
new MiniCssExtractPlugin({ //作用:抽离 css
filename:'main.css' //抽离出来的 css 文件放在 这个文件中;
}),
new MiniCssExtractPlugin2({ //作用:抽离 css
filename:'home.css' //抽离出来的 css 文件放在 这个文件中;
})
],
module:{ //模块
rules:[ //规则
{
test:/\.css$/, //找到以 css 为后缀的文件
use:[
MiniCssExtractPlugin.loader, //解析css之后将文件放在 main.css 中去
'css-loader'
]
},
{ // 解析 less 文件
test:/\.less$/, //找到以 less 为后缀的文件
use:[
MiniCssExtractPlugin2.loader, //将解析的 less 文件抽离到 home.css 中
'css-loader', //解析 css 各种语法
'less-loader' //将 less 转化为 css
]
}
]
}
}
效果如下:
3.5.4 解析后的样式自动加上浏览器前缀(postcss-loader,autoprefixer)
我们写一点有兼容性的样式
效果:
为此要安装插件:
npm i postcss-loader@3.0.0 autoprefixer@9.5.0 -D //这两个版本是我自己找的,不知道视频是啥
然后在解析 css 之前就上这个前缀就行了
rules:[ //规则
{
test:/\.css$/, //找到以 css 为后缀的文件
use:[
MiniCssExtractPlugin.loader, //解析css之后将文件放在 main.css 中去
'css-loader',
'postcss-loader' //给css代码加上各种浏览器前缀
]
},
{ // 解析 less 文件
test:/\.less$/, //找到以 less 为后缀的文件
use:[
MiniCssExtractPlugin2.loader, //将解析的 less 文件抽离到 home.css 中
'css-loader', //解析 css 各种语法
'postcss-loader', //给css代码加上各种浏览器前缀
'less-loader' //将 less 转化为 css
]
}
]
再打包还是报错:
缺少文件,那就创建一个 postcss.config.js
之后再运行,倒是不报错了,但是打包后的样式文件中没有生成前缀啊,视频中倒是没出现这个问题,在网上找到的解决方法:解决webpack4.x使用autoprefixer 无效
再试一次:
3.5.5 将解析出来的 css 文件进行压缩(optimize-css-assets-webpack-plugin)
上面我们打包出来的只有 js 文件压缩了,但是 css 文件没有,接下来,就将css文件也压缩一下
安装插件
npm i optimize-css-assets-webpack-plugin@3.2.1 -D
然后新增配置:
let OptimizeCss = require('optimize-css-assets-webpack-plugin') //压缩 css
module.exports = {
mode:'production', //模式,默认两种:production--生产模式,development--开发模式
optimization: { //优化--压缩 css 文件
minimizer:[
new OptimizeCss()
]
},
}
打包之后,css确实成一行了:
3.6 压缩 js
3.6.1 压缩 js(uglifyjs-webpack-plugin)
根据 3.5.5 的效果,css优化后, js 反而没有压缩了,变回去了:
所以说,如果压缩了 css ,那么就必须要写上压缩 js 的,插件是 uglifyjs-webpack-plugin
npm i uglifyjs-webpack-plugin@2.2.0 -D
let UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin') //压缩 js
module.exports = {
mode:'production', //模式,默认两种:production--生产模式,development--开发模式
optimization: { //优化
minimizer:[
new UglifyjsWebpackPlugin({ //压缩 js
cache:true, //有缓存
parallel:true, //是并发打包
sourceMap:true //
}),
new OptimizeCss() //压缩 css 文件
]
},
}
打包结果:
3.6.2 转化 js
3.6.2.1 babel-loader, @babel/core, @babel/preset-env
我们之前都是直接写的 es5
的代码,现在我们来写 es6
的语法,比如箭头函数:
开发模块下打包依旧是 es6的语法:
那么我们想把 es6
转化为 es5
,这个时候,我们需要用到 babel
安装:
babel-loader
:转化的加载器;@babel/core
:核心模块,可以调用方法实现转化;@babel/preset-env
:可以把高级的语法转化成低级的语法;
npm i babel-loader@8.0.4 @babel/core@7.2.2 @babel/preset-env -D
增加 rules:
rules:[ //规则
{ // 解析 js 文件
test:/\.js$/, //找到以 js 为后缀的文件
use:[
{
loader:'babel-loader',
options:{ //用 babel-loader 把 es6 转化为 es5
presets:[ //预设
'@babel/preset-env' //调用此模块来进行转化
]
}
}
]
}
]
执行打包操作:
3.6.2.1 @babel/plugin-proposal-decorators
我们还可以写个更高级的语法试试:(下面代码报错因为只要在vscode首选项中设置一下就行)
然后就报错了:
可以去babel
官网去找这个插件,看怎么用的,这还是提案中的语法,官网直接有代码:
npm i @babel/plugin-proposal-decorators@7.2.3 -D
rules:[ //规则
{ // 解析 js 文件
test:/\.js$/, //找到以 js 为后缀的文件
use:[
{
loader:'babel-loader',
options:{ //用 babel-loader 把 es6 转化为 es5
presets:[ //预设
'@babel/preset-env' //调用此模块来进行转化
],
plugins: [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties"]
]
}
}
]
}
]
之后就可以了:
3.6.2.3 @babel/plugin-transform-runtime,@babel/runtime
然后再高级一点:
打包及运行后:
这里我们还需要另一个包来进行转化
npm i @babel/plugin-transform-runtime@7.2.0 -D
npm i @babel/runtime@7.2.0 -S //这个包生产模式也需要
rules:[ //规则
{ // 解析 js 文件
test:/\.js$/, //找到以 js 为后缀的文件
use:[
{
loader:'babel-loader',
options:{ //用 babel-loader 把 es6 转化为 es5
presets:[ //预设
'@babel/preset-env' //调用此模块来进行转化
],
plugins: [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties"],
"@babel/plugin-transform-runtime"
]
}
}
],
include:path.resolve(__dirname,'src'), //只解析 src 中的 js 文件
exclude:/node_modules/ //排除掉 node_modules 中的js文件
}
]
3.6.2.4 @babel/polyfill
还有一个问题,实例上的方法都不会去解析,比如:
打包后没有解析成 es5
这里要用到 @babel/polyfill
npm i @babel/polyfill@7.2.5 -S //也需要到生产环境
打包后就解析了
另外,视频中没有报错的但是我报错的地方,就是 a.js
中的导出报错了:
Uncaught TypeError: Cannot assign to read only property’exports‘ of object’#[Object]
// module.exports = 'abcd' //不这样写,改成下面的
const a = 'abcd'
export default a
3.6.3 js 校验
写代码的时候加个校验器,来校验代码是否规范,使用 ESLint
,可以进入官网看 demo
官网下载的 eslintrc.json 文件,但是也说明
所以自己修改下文件名,加个 .
,然后复制到根目录下:
接下来再去改配置:
npm i eslint@5.12.0 eslint-loader -D
rules:[ //规则
//js 一般应该是先校验,再解析转化 js,所以这个 eslint-loader 应该写在 babel-loader 的下面,或者可以用 enforce 来配置
{ // 校验 js 语法
test:/\.js$/, //找到以 js 为后缀的文件,校验语法规范
use:[
{
loader:'eslint-loader',
options:{
enforce:'pre' //默认值: 'normal';还可以是 'post':在nornal之后;或者 'pre':执行强制在 normal 的loader 之前执行
}
}
]
},
{ // 解析 js 文件
test:/\.js$/, //找到以 js 为后缀的文件
use:[
{
loader:'babel-loader',
options:{ //用 babel-loader 把 es6 转化为 es5
presets:[ //预设
'@babel/preset-env' //调用此模块来进行转化
],
plugins: [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties",{"loose":true}],
"@babel/plugin-transform-runtime"
]
}
}
],
include:path.resolve(__dirname,'src'), //只解析 src 中的 js 文件
exclude:/node_modules/ //排除掉 node_modules 中的js文件
},
]
之后再打包的时候,就会看到各种报错:
当然了,写代码的时候我们不会去开这个校验,所以注释掉
就行,所以才会把这个 eslint-loader
单独写,而不是和 babel-loader
放在一起
4. 全局变量引入问题
4.1 内联 loader(expose-loader)暴露到 window 上
比如说,项目中要用到 jquery
等第三方包
npm i jquery -S
我们希望把 $
暴露给 window
,所以我们可以使用另一个 loader
:expose-loader
–暴露全局的loader,这个可以直接在代码中使用,是内联的loader
上一节我们提到过 enforce
参数中的三种 loader,分别是 前面执行的(pre
),普通的(normal
),后置的(post
),还有一种是内联
的,写法如下:
npm i expose-loader@0.7.5 -D
import $ from 'expose-loader?$!jquery';
//上面的意思是,将 jquery 作为 $ 暴露给全局
console.log('[ $ ] >', $)
console.log('[ window.$ ] >', window.$)
运行 npm run dev
之后结果如下:
4.2 不使用内联 loader
如果不想使用内联loader
,那就还是写到 webpack.config.js
中去:
代码中引入还是以前的写法:import $ from 'jquery';
rules:[ //规则
{ // 校验 js 语法,一般我们开发过程中不会去校验,注释掉
test:require.resolve('jquery'), //当代码中引入了 jquery
use:[
{
loader:'expose-loader?$',
}
]
},
]
打印结果如上图,我这就不贴了
4.3 ProvidePlugin 给每个模块提供 $
如果我还是嫌麻烦,不想要 import $ from 'jquery';
,那么可以在每个模块中注入 $
对象
// import $ from 'jquery';
// import $ from 'expose-loader?$!jquery';
console.log('[ $ ] >', $)
console.log('[ window.$ ] >', window.$)
let Webpack = require('webpack') //在每个模块中注入全局变量
plugins:[
new Webpack.ProvidePlugin({ //webpack 提供插件,作用是:在每个模块中都注入 $
$:'jquery'
})
],
rules:[ //规则
//注释掉 expose-loader ,直接使用 new Webpack.ProvidePlugin 实现
/* { // 校验 js 语法,一般我们开发过程中不会去校验,注释掉
test:require.resolve('jquery'), //当代码中引入了 jquery
use:[
{
loader:'expose-loader?$',
}
]
}, */
]
运行结果如下:
可以看到,每个模块都注入了 $
,但是 window
中是没有的
4.4 直接引入 CDN 链接,但是不打包
那就有 window.$
了:
如果你既在 index.html
中使用 CDN
引入了,又在 index.js
中引入了,打包之后:
那么再配置一下:
let Webpack = require('webpack') //在每个模块中注入全局变量
plugins:[
// new Webpack.ProvidePlugin({ //webpack 提供插件,作用是:在每个模块中都注入 $
// $:'jquery'
// })
],
externals:{
jquery:"$" //如果模块中引入了 jquery ,打包不要打进去
},
再打包看看:
5. 图片处理
5.1 在js中创建图片来引入
import logo from './1.png'; //把图片引入,返回的结果是一个新的图片地址
let image = new Image();
image.src = logo
document.body.appendChild(image)
npm run dev
之后报错:
这里要使用 file-loader
,作用是:默认会在内部生成一张图片到 dist
目录下,还会把生成的图片的名字返回回来
npm i file-loader@4.3.0 -D
{
test:/\.(png|jpg|gif|jpeg|bmp)/, //当遇到图片,就使用 file-loader 解析,作用是:默认会在内部生成一张图片到 `dist `目录下,还会把生成的图片的名字返回回来
use:[
{
loader:'file-loader',
}
]
},
5.2 在css 中引入
5.3 在 html 中直接写死
这里就看不到图片效果了,只有这个元素
这里也需要一个loader 来解析
npm i html-withimg-loader@0.1.16 -D
{
{
test:/\.html/, //当遇到图片,就使用 file-loader 解析,作用是:默认会在内部生成一张图片到 `dist `目录下,还会把生成的图片的名字返回回来
use:[
{
loader:'html-withimg-loader',
}
]
},
5.4 将小图转化为 base64
一般情况下,我们不会直接用 file-loader
,而是使用 url-loader
,作用是:当图片小于多少 kb
时,转化base64
,减少 http 请求;如果大于这个限制,就用 file-loader
将这个图片产出
npm i url-loader@1.1.2 -D
{
test:/\.(png|jpg|gif|jpeg|bmp)/, //当遇到图片
use:[
{
loader:'url-loader',
options:{
limit:200*1024, //当 图片小于 200K 时,转化为 base64 ,否则直接用 file-loader 解析产出
}
}
]
},
// {
// test:/\.(png|jpg|gif|jpeg|bmp)/, //当遇到图片,就使用 file-loader 解析,作用是:默认会在内部生成一张图片到 `dist `目录下,还会把生成的图片的名字返回回来
// use:[
// {
// loader:'file-loader',
// }
// ]
// },
打包后可以看到,小图片确实转化为 base64
了,但是要注意,base64
要比原图片大 1/3
:
url-loader
也有 file-loader
的功能,所以不需要用 file-loader
了,要验证可以将 limit
的值 改为 1
打包之后:
6 打包文件分类
6.1 图片打包分类
将打包后的图片单独放在一个文件夹里面
{
test:/\.(png|jpg|gif|jpeg|bmp)/, //当遇到图片
use:[
{
loader:'url-loader',
options:{
limit:1, //当 图片小于 200K 时,转化为 base64 ,否则直接用 file-loader 解析产出
outputPath:'img/'
}
}
]
},
打包后:
6.2 样式文件分类
在抽离 css 文件时,可以直接指定目录
new MiniCssExtractPlugin({ //作用:抽离 css
filename:'css/main.css' //抽离出来的 css 文件放在 这个文件中;
}),
打包后:
6.3 给打包后的引入资源加域名
我们还希望在引用图片的时候,加个域名在前面,我们可以在 output
中加个公共路径配置
output:{
filename:'bundle.js', //打包后的文件名
path:path.resolve(__dirname,'dist'),
publicPath:'http://www.baidu.com', //打包后引入的资源的路径都会加这个域名
},
打包看看:
但是可以看到,css
和 bundle.js
的路径都没问题,但是 图片的路径 img
前面少了个 /
所以在图片分类打包的配置里改一下:
{
test:/\.(png|jpg|gif|jpeg|bmp)/,
use:[
{
loader:'url-loader',
options:{
limit:1,
outputPath:'/img/' //这里的 img 前面加了个 /
}
}
]
},
如果我只想给图片加上这个域名,其他类型的资源不加,那要怎么办呢?好办,首先 output
里面的 publicPath
就不要了,然后在 解析图片时单独加上:
{
test:/\.(png|jpg|gif|jpeg|bmp)/,
use:[
{
loader:'url-loader',
options:{
limit:1,
outputPath:'/img/',
publicPath:'http://www.baidu.com', //打包后引入的图片的路径都会加这个域名
}
}
]
},
打包后:
7. 打包多页应用
构建多页应用,这里我们重新建一个文件夹,跟之前的区分开,还是之前的步骤:
npm init -y
npm intall webpack@4.28.2 webpack-cli@3.1.2 -D
npm i html-webpack-plugin@4.4.1 -D
可以看到我们的打包配置也是两个入口,但是只输出了一个 bundle.js
文件,看打包结果:
报错了,两个入口的输出文件名都叫 bundle.js
,这是不允许的;所以需要产出两个出口
修改配置文件:
打包成功后的结果:
仅仅是 js 还不能满足需求,肯定也有多个 html:
打包后:
但是上面两个 html 文件内的内容是一样的,这不是我们预期的效果,我们希望 home.html
中引入 home.js
;other.html
中引入 other.js
,那么再添加 chunks
配置:
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin') //模板插件
module.exports = {
//多入口
mode:"development",
entry:{
home:'./src/index.js',
other:'./src/other.js'
},
output:{
filename:'[name].js', //打包后多个出口, home.js other.js
path: path.resolve(__dirname,'dist')
},
plugins:[
new HtmlWebpackPlugin({ //作用:根据模板生成打包后的入口
template:'./index.html', //以该文件为模板
filename:'home.html', //打包后要放在打包目录下的 html 文件的名字,第1个
chunks:['home'] //代码块,引入 home.js
}),
new HtmlWebpackPlugin({ //作用:根据模板生成打包后的入口
template:'./index.html', //以该文件为模板
filename:'other.html', //打包后要放在打包目录下的 html 文件的名字,第2个
chunks:['other'] //代码块,引入 other.js
}),
]
}
重新打包:
8. 配置devtool,调试源代码
我们之前在优化项中用到了这个:
optimization: { //优化项--只有生产环境才会执行这个,开发环境不会执行这个
minimizer:[
new UglifyjsWebpackPlugin({ //压缩 js
cache:true, //有缓存
parallel:true, //是并发打包
sourceMap:true //
}),
new OptimizeCss() //压缩 css 文件
]
},
但是并没有详细说,这里我还是闲安装包:
npm i webpack-dev-server@3.1.14 -D
npm i babel-loader@8.0.4 @babel/core@7.2.2 @babel/preset-env -D
在 index.js
中写一点 es6
语法的代码,故意写错:
然后看一下配置,注意是生产环境:
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin') //模板插件
module.exports = {
mode:"production", //development || production
entry:{ //入口
home:'./src/index.js'
},
output:{ //输出
filename:'[name].js',
path: path.resolve(__dirname,'dist')
},
module:{ //规则
rules:[
{
test:/.js$/, //匹配以 js 结尾的文件
use:{
loader:'babel-loader', //使用 babel-loader 加载器
options:{
presets:['@babel/preset-env'] //将高级语法转化为es5
}
}
}
]
},
plugins:[ //插件
new HtmlWebpackPlugin({ //作用:根据模板生成打包后的入口
template:'./index.html', //以该文件为模板
filename:'index.html', //打包后要放在打包目录下的 html 文件的名字,第1个
}),
]
}
运行 npm run dev
之后,果然报错了:
这样就不好调试代码了,我希望点击错误的时候,出现的是源码,而不是打包之后的代码,所以需要一个源码映射
8.1 source-map
在配置文件中新增 devtool
配置:
然后打包,可以看到新增了 home.js.map
文件:
直接将打包后的 index.html
放在浏览器中,可以看到:
上面可以看到报错文件也不一样了,以前是 home.js:2
,这是打包后的文件,现在这个变成了 index.js:5
,这个是源文件,点击之后可以看到源代码了:
8.2 eval-source-map
devtool
还有另一种值:eval-source-map
,不会产生单独的文件,但是会显示行和列:
然后重新打包,果然没有了 home.js.map
:
还是将 打包后的 index.html
放到浏览器中:
依旧看到了源代码:
8.3 cheap-module-source-map
devtool
的第三种值:cheap-module-source-map
,不会产生列,但是会产生单独的映射文件,产生后不会跟我们的代码关联起来:
重新打包:
再来一次,查看 index.html
:
看不到源代码了:
8.4 cheap-module-eval-source-map
devtool
的第四种值:cheap-module-eval-source-map
,不会产生映射文件,也不会产生列,但是会集成在打包后的文件中:
打包后:
查看结果:
9. watch 监听代码变化打包
我们每次改完代码都需要手动再打包一遍,台麻烦了,虽然说 npm run dev
可以看到效果,但是它不会产生实体文件啊,所以我们可以使用 watch
配置,以及它对应的选项 watchOptions
:
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin') //模板插件
module.exports = {
mode:"production", //development || production
entry:{ //入口
home:'./src/index.js'
},
watch:true, //监控,作用是:监控代码的变化,代码一变化,就帮我们打包
watchOptions:{ //监控的选项
poll:1000, //每秒问1000次:需要更新吗,值越小精度越低
aggregateTimeout:500, //防抖: 我一直输入代码,但不能我写一个字母就打包一次吧,500表示:我一直输入内容,直到500毫秒内都没有输入了,就打包
ignored: /node_modules/ //忽略:不需要监控 node_modules 文件
},
output:{ //输出
filename:'[name].js',
path: path.resolve(__dirname,'dist')
},
}
当有以上设置后,每当代码变化或保存的时候,就会自动打包
10. webpack小插件
10.1 cleanWebpackPlugin
我们每次打包之前,都要手动把上一次的 dist 目录删掉,避免缓存的问题,那么这个插件会帮我们做这件事
npm i clean-webpack-plugin@3.0.0 -D
配置如下:
let CleanWebpackPlugin = require('clean-webpack-plugin') //打包后自动删除之前的 dist 文件
module.exports = {
plugins:[ //插件
new CleanWebpackPlugin('./dist'), //告诉它要清空的目录,或者是一个数组,要清空多个文件夹
]
}
验证过程如下:先打包一次,然后修改输出文件名称,从 index.html
改为 home.html
,再次打包,看 dist 中有没有 index.html
,没有就表示成功
修改之前:
修改之后再次打包,报错了,视频上是没报错的,可能是版本不一样,我这里是 @3.0.0,视频就不知道了:
然后在 这里 找到解决方法,修改之后:
let { CleanWebpackPlugin } = require('clean-webpack-plugin') //打包后自动删除之前的 dist 文件,由clean-webpack-plugin.d.ts文件的内容可知,导出的是以一个对象属性的形式,所以我们在引入的时候需要以解构的方式来获取
module.exports = {
plugins:[ //插件
new CleanWebpackPlugin(), //参数是可选的,如果什么都不配置默认删除未使用的资源,我们采用默认的即可
]
}
再次尝试打包,发现已经是 home.html
了,而且没有之前的 index.html
的缓存:
10.2 copyWebpackPlugin
我现在有些 src
之外的文件也想打包到 dist 文件里,比如:
还是先安装
npm i copy-webpack-plugin@4.6.0 -D
配置:
let CopyWebpackPlugin = require('copy-webpack-plugin') //将某些文件打包时直接复制到 dist 中去
module.exports = {
plugins:[ //插件
new CopyWebpackPlugin( //打包时复制文件
[
{from: './doc' , to: './doc'} //从相对于 src 的某个文件 copy 到 相对于dist中的某个路径下去,如果还有其他的文件需要copy,就再写一个对象
]
)
]
}
打包后:
10.3 bannerPlugin(内置的)
这个插件的作用是:在每个打包文件的头部都插入一些版权信息
let Webpack = require('webpack') //使用 webpack 内置的插件
module.exports = {
plugins:[ //插件
new Webpack.BannerPlugin('make 2021 by me') //在每个打包js文件的头部都有这样的版权信息
]
}
打包后:
11. webpack 跨域
11.1 使用 express 搭建简单的服务器
在根目录下创建 server.js
文件:
// 使用 express 来 启动一个 服务端
let express = require('express')
let app = express()
/**
* 当请求 /api/use 接口时 ,req:表示请求参数, res:表示响应
*/
app.get('/api/use',(req,res)=>{
res.json({name:'张三'})
})
app.listen(3000) //服务端监听 3000 的端口
然后修改 package.json
文件:
运行 npm run start
,然后浏览器访问:http://localhost:3000/api/use
,就能看到服务启动成功了:
11.2 解决跨域的3种方式
11.2.1 前端向 express 服务器请求数据
在 index.js
中进行请求:
//使用 ajax 请求刚刚我们写的接口
let xhr = new XMLHttpRequest()
//请求(请求类型,接口,是否异步),注意:这样默认访问的路径就是 http://localhost:8080/ ,是 webpack-dev-serve 的服务,但是 express 服务端默认端口是 3000
xhr.open('GET','/api/use',true)
xhr.onload = function(){
console.log(xhr.response) //打印请求结果
}
xhr.send() //将请求结果发送出去
然后运行 npm run dev
,可以看到报错了:
报错404了,我们刚刚请求,默认是请求 webpack-dev-server
的地址,所以找不到;
这里我们需要用到 反向代理 http-proxy
来解决跨域的问题,在 webpack 中可以直接配置一下就行:
module.exports = {
devServer:{
proxy:{
'/api' : 'http://localhost:3000/' //配置了一个代理,当访问了 http://localhost:8080/api 的时候,就直接转到 http://localhost:3000/ 去找里面的 api
}
},
]
再次 npm run dev
,就请求成功了:
上述是跨域成功了,但是一般后台在写 api 的时候,不会直接写 '/api/use'
,而是 '/use'
,这样的话,我们不可能写代理的时候,有一个接口就写一个代理吧,太麻烦了;
所以,当后台接口没有一个统一的路径时,比如:'/use1'
,'/use2'
,我们前端请求的时候还是可以自己加上一个统一的路径,比如:'/api/use1'
,'/api/use2'
,然后在代理的时候,将前端的路径中的 /api
删除 一下,再发给服务器就行了
在 server.js
中修改一下,然后重新启动一下服务器:npm run start
:
前端的 ajax
不变,然后在 webpack.config.js
中修改一下:
module.exports = {
devServer:{
proxy:{
'/api' : {
target:'http://localhost:3000/', //配置了一个代理,当访问了 http://localhost:8080/api 的时候,就直接转到 http://localhost:3000/ 去找里面的 api
pathRewrite:{ //代理的时候,先将 '/api' 重写,再发送到 target 去
'/api':'' //将 '/api' 重写成 ''
}
}
}
},
]
再 npm run dev
,看一下结果,还是请求成功了,这里就不截图了,还是和上面一样
11.2.2 前端不向服务器请求,只想 mock 一些数据
由于 devServer
内部本来就是 express
,所以我们可以在 devServer
里面写一些接口:
module.exports = {
devServer:{
before(app){ //提供的方法,钩子,启动之前调用此方法,这个参数 app,就跟我们刚刚在 express 中写的 app 是一样的
app.get('/use',(req,res)=>{
res.json({name:'张三-before'})
})
},
// proxy:{ //将请求重写的方式,把请求 代理 到 express 服务器
// '/api' : {
// target:'http://localhost:3000/', //配置了一个代理,当访问了 http://localhost:8080/api 的时候,就直接转到 http://localhost:3000/ 去找里面的 api
// pathRewrite:{ //代理的时候,先将 '/api' 重写,再发送到 target 去
// '/api':'' //将 '/api' 重写成 ''
// }
// }
// }
},
]
然后将前端请求的路径也直接改为 /use
:
这里就没有 express 服务器
什么事了,就是代码向 devServer
启动的服务请求了数据,npm run dev
一下:
11.2.3 前端和服务端用同一个端口
有服务端,但是不用代理来处理,在服务端中启动 webpack
的端口,前端和服务端用同一个端口,这样的话,也不会有跨域的问题了
npm i webpack-dev-middleware@3.4.0 -D //中间件,作用是可以在服务端启动 webpack
在 服务端 启动 webapck
,下面是 server.js
:
// 使用 express 来 启动一个 服务端
let express = require('express')
let app = express()
//在服务端启动 webpack:要使用 express 的中间件
// 大致流程: 先拿到 webpack.config.js 配置对象 --> 交给这里 服务端的 webpack 来处理,会产生一个编译对象 --> 将编译对象扔给中间件
let webpack = require('webpack')
//express的中间件
let middle = require('webpack-dev-middleware')
// 1. 拿到配置文件
let config = require('./webpack.config.js')
// 2. webpack 来处理这个 配置对象,返回的是一个编译的结果
let compiler = webpack(config)
// 3. 使用中间件,这样的话,就不需要 webpack-dev-server 了,直接启动服务端,也会自动启动 webpack 配置
app.use(middle(compiler))
/**
* 当请求 /api/use 接口时 ,req:表示请求参数, res:表示响应
*/
// app.get('/api/use',(req,res)=>{
// res.json({name:'张三'})
// })
app.get('/use',(req,res)=>{
res.json({name:'张三'})
})
app.listen(3000) //服务端监听 3000 的端口
然后直接启动服务端 npm run start
:
然后我们直接访问 http://localhost:3000/use
,也就是服务端,也可以看到服务端确实启动了:
我们再访问 http://localhost:3000
,也就是 webpack
运行的地址,也可以看到我们的请求成功了:
12 resolve 配置
12.2 resolve.modules
在 commonJS
规范中,我们知道,会从当前目录下的 node_modules
中去找,找不到就一层一层往上去找 node_modules
中的包
resolve
:解析 第三方包,这个配置可以限定我们查找包的路径:
module.exports = {
resolve:{ //解析 第三方包
modules:[ path.resolve('node_modules') ] //只在当前目录下的 node_modules 中去找包,不要往上找
},
]
12.2 resolve.alias
我们试试能不能引用 bootstrap 中的样式,先安装
npm i bootstrap -S
//这里直接安装 bootstrap@4的就行,4以上的安装时,没有 font 文件夹,webpack 就不用处理字体图标类型的文,不然会报错
上面的报错可以在 webpack 中加一个匹配规则,我实践过了,但应该有用:
npm i file-loader@4.3.0 -D
npm i url-loader@1.1.2 -D
{test:/\.(ttf|eot|svg|woff|woff2)$/,use:'url-loader'},
然后在 index.html
中写上按钮:
<button class="btn btn-danger"></button>
然后在 index.js
中引入样式:
// import 'bootstrap' //这个会先去 bootstrap 中找它的 package.json 中的 main: "main": "./dist/js/npm", 所以引用的是js 文件,而不是 css 文件,而且运行后还会报缺少 jquery 的错误
//所以我们直接引入 css 文件
import 'bootstrap/dist/css/bootstrap.css'
结果:
上面这样直接引入 css
文件写的也太长了,可以在 webpack
中配置别名:
resolve:{ //解析 第三方包
modules:[ path.resolve('node_modules') ], //只在当前目录下的 node_modules 中去找包,不要往上找
alias:{ //别名,比如你引入 vue 也是 别名,实际叫 vue.runtime
bootstrap: 'bootstrap/dist/css/bootstrap.css' //当你引入了 'bootstrap' 的时候,实际上是引入了 'bootstrap/dist/css/bootstrap.css'
}
},
这样配置了之后,也能够 直接在 index.js
中引入 bootstrap
了,这实际上就是引入了 css
文件
import 'bootstrap'
12.3 resolve.mainFields
或者另一个角度,我们在引入包的时候,会默认去找包里面的 package.json
里面的 main
,这样是会找到 js 文件的,那能不能设置去找 style
呢,这样就会找 css 文件了啊:
import 'bootstrap'
resolve:{ //解析 第三方包
modules:[ path.resolve('node_modules') ], //只在当前目录下的 node_modules 中去找包,不要往上找
// alias:{ //别名,比如你引入 vue 也是 别名,实际叫 vue.runtime
// bootstrap: 'bootstrap/dist/css/bootstrap.css' //当你引入了 'bootstrap' 的时候,实际上是引入了 'bootstrap/dist/css/bootstrap.css'
// },
mainFields:['style','main'] //先找包里面的 package.json 中的 style,style 找不到的话再去找 main
},
还有一种需求,当你写了样式文件,引入到 js 中去的时候,不想写 后缀:
引入样式:
结果:
12.4 resolve.extensions
但是我想引入样式文件的时候,不写 import './style.css'
,而写 import './style'
,这里可以配置扩展名:
resolve:{ //解析 第三方包
modules:[ path.resolve('node_modules') ], //只在当前目录下的 node_modules 中去找包,不要往上找
// alias:{ //别名,比如你引入 vue 也是 别名,实际叫 vue.runtime
// bootstrap: 'bootstrap/dist/css/bootstrap.css' //当你引入了 'bootstrap' 的时候,实际上是引入了 'bootstrap/dist/css/bootstrap.css'
// },
// mainFiles:[], //入口文件的名字 index.js
mainFields:['style','main'], //先找包里面的 package.json 中的 style,style 找不到的话再去找 main
extensions:['.js' , '.css' , '.json'] //引入的包如果没有写后缀,就先去找同名的 js 文件,没有就找 css 文件,再没有就找 json 文件
},
13. 区分生产环境和开发环境的配置
13.1 定义环境变量
区分开发环境和生产环境分别是什么配置,首先要用到 配置环境中的变量,先来看怎么使用吧:
我们在 index.js
中进行判断:
然后在 webpack.config.js
中定义该变量 `DEV``:
上图中要注意:变量的值要在 引号 内部,如果是字符串的话就要两层引号了,比如: DEV: "'development'"
,但是不推荐这样的写法,我们可以写成:DEV: JSON.stringify('development')
然后 npm run dev
的结果:
13.2 区分环境
我们可以建两个配置文件来对应两种环境,然后将我们之前写的 webpack.config.js
改名为 webpack.base.js
:
webpack.prod.js
:对应的是production
生产环境webpack.dev.js
:对应的是development
开发环境webpack.base.js
:对应的是基础的,公共的配置
通过上面三种配置文件,可以在生产环境下,是 webpack.base.js
+ webpack.prod.js
;
然后 开发环境下是: webpack.base.js
+ webpack.dev.js
这样合并不同文件中的配置的操作需要用到 一个插件 :webpack-merge
:
npm i webpack-merge@4.2.1 -D
然后是 webpack.prod.js
配置:
以及 webpack.dev.js
开发环境的配置:
然后将 webpack.base.js
中的 mode 直接删除就行了,然后将 package.json 中的 "build": "webpack --config webpack.config.js"
改为 "build": "webpack"
以后,要在生产环境或者开发环境下打包,就直接在打包指令后直接指定对应的配置文件就行:
npm run build -- --config webpack.prod.js //生产模式打包
npm run build -- --config webpack.dev.js //开发模式打包
生产环境打包:
开发环境打包:
发现确实生效后,我们之前的开发和生产的配置就可以分开了:
webpack.prod.js
中,现在可以直接把优化项放在这里:
let {smart} = require('webpack-merge') //合并 webpack 配置的插件
let base = require('./webpack.base.js') //基础,公共的 webpack 配置
//然后把两个配置变成一个:
module.exports = smart(base,{
mode:'production', //生产环境
optimization: { //优化项--只有生产环境才会执行这个,开发环境不会执行这个
minimizer:[
new UglifyjsWebpackPlugin({ //压缩 js
cache:true, //有缓存
parallel:true, //是并发打包
sourceMap:true
}),
new OptimizeCss() //压缩 css 文件
]
},
})
webpack.dev.js
中,现在可以直接把开发时的配置放在这里:
let {smart} = require('webpack-merge') //合并 webpack 配置的插件
let base = require('./webpack.base.js') //基础,公共的 webpack 配置
//然后把两个配置变成一个:
module.exports = smart(base,{
mode:'development', //生产环境
/**
* devtool 的值:
* 1. 'source-map':源码映射,会单独生成一个 sourcemap 文件,出错了会标识当前报错的列和行,大 和 全
* 2. 'eval-source-map':不会产生单独的文件,但是会显示行和列
* 3. 'cheap-module-source-map':不会产生列,但是会产生单独的映射文件,产生后不会跟我们的代码关联起来,但可以保留起来,用于调试
* 4. 'cheap-module-eval-source-map':不会产生映射文件,也不会产生列,但是会集成在打包后的文件中
*/
devtool:'cheap-module-eval-source-map', //增加映射文件,可以帮我们调试源代码;
devServer:{
before(app){ //提供的方法,钩子,启动之前调用此方法,这个参数 app,就跟我们刚刚在 express 中写的 app 是一样的
app.get('/use',(req,res)=>{
res.json({name:'张三-before'})
})
},
// proxy:{ //将请求重写的方式,把请求 代理 到 express 服务器
// '/api' : {
// target:'http://localhost:3000/', //配置了一个代理,当访问了 http://localhost:8080/api 的时候,就直接转到 http://localhost:3000/ 去找里面的 api
// pathRewrite:{ //代理的时候,先将 '/api' 重写,再发送到 target 去
// '/api':'' //将 '/api' 重写成 ''
// }
// }
// }
},
})
以上就是 webpack 的基础配置了,下面再讲到一些 插件,优化之类的
14. webpack 的优化
我们还是建立一个基础的 demo:
14.1 noParse
打包解析时忽略某些包的依赖关系
如上所示:在 index.js
中引入了 jquery
,那么再打包的时候,会自动去找这个包,解析其中的依赖关系,要是还有其他的依赖就一起打包,但是我们可以确定 jquery
中没有其他依赖,想跳过这一步解析,提高打包效率,这时可以使用 noParse
:
配置之前的打包:
配置后:
打包:
对比一下,配置后的打包时间明显缩短了
14.2 排除(exclude)和包含(include)
这一项之前我们用到过,这里就贴下代码:
module:{ //规则
noParse: /jquery/, //不去解析 jquery 中的依赖库
rules:[
{
test:/\.js$/, //匹配以 js 结尾的文件
use:{
loader:'babel-loader', //使用 babel-loader 加载器
options:{
presets:['@babel/preset-env'] //将高级语法转化为es5
}
},
include:path.resolve(__dirname,'src'), //只解析 src 中的 js 文件
exclude:/node_modules/ //排除掉 node_modules 中的js文件
}
]
},
14.3 IgnorePlugin 忽略包的某些无用引入
安装 moment
npm i moment@2.22.2 -S
使用 moment
:
运行 npm run dev
之后,打印成功了 :
上图中,虽然我们只用了这个包里的几个方法,但实际上是把包里面的所有内容都打包进来了,包括所有的语言:
方便的是,可以直接设置语言:
结果是:
但是,我只想用到中文包的情况下,把所有包都引进来就很冗余,打包体积很大,可以用一个插件实现:忽略包里面的引用的所有的本地文件
配置:
let Webpack = require('webpack')
module.exports = {
plugins:[
new Webpack.IgnorePlugin(/\.\/locale/,/moment/), //如果从 moment 中引入了 './locale',就忽略掉
],
}
再来运行:
配置前和配置后,减少了 500 多 KB,但是看看打印结果,又恢复到英文了,因为中文包没有被引入,所以我们需要手动引入所需要的语言:
再运行就是打印的中文了
14.4 动态链接库
由于视频中使用的是 react
,只能跟着来了,第一步还是安装包:
npm i react@16.7.0 react-dom@16.7.0 -S
npm i @babel/preset-react@7.0.0 -D //解析 angular 语法的包,只有一个版本 6.0.15
使用 react
写点代码:
配置文件中,要翻译 react
语法:
然后运行看看:
我们可以看到:体积也很大了,1.2M 左右,我们不会去更改 react 和 react-dom ,我们希望打包的时候,不要把这两个包打进去:
14.4.1 单独打包 react react-dom
新增 webpack.config.react.js
配置文件,里面是单独打包 react react-dom 的配置:
//单独去打包 react react-dom,在开发的时候,引用我们打包好的文件,这样的话 react react-dom 就不会重新打包了
let path = require('path')
let Webpack = require('webpack')
module.exports = {
mode:"development",
entry:{
react:['react','react-dom'],
},
output:{
filename:'_dll_[name].js', // 产生的文件名
path: path.resolve(__dirname,'dist'),
library:'_dll_[name]', //产生的文件导出的变量叫这个名字,_dll_react
libraryTarget:'var' //var 是默认值,还可以是 commonjs , var , this , umd ......
},
plugins:[
new Webpack.DllPlugin({
name:'_dll_[name]', // name == library 这是规定好的,容易去找对应关系
path:path.resolve(__dirname,'dist','mainfest.json') //一个路径,清单,让人能找到这个文件
})
]
}
然后 运行 webpack --config webpack.config.react.js
进行打包:
打包成功后,就改引入到我们的项目中去了:
然后修改我们的打包配置 wbpack.config.js
:
plugins:[
new Webpack.DllReferencePlugin({ //引用第三方动态链接库
manifest: path.resolve(__dirname,'dist','manifest.json') //先去 dist/manifest.json 中去找引用的模块,没有的话再按照顺序从 node_modules 中去找
}),
],
再打包看看:
打包的 index.js 只有 6k 了,因为我们已经把 react 相关的包都打包好了,放在了 _dll_react.js
中且在 index.html
中引用了,这样的动态链接库提升了我们的打包效率。
14.5 多线程打包
使用 模块 happypack
可以实现多线程打包
npm i happypack@5.0.1 -D
我们以前解析 js
的时候用的 babel-loader
,但是现在可以使用 happypack/loader
,通过指定的 id 去找 插件配置中对应的 id 来解析相关文件:
let Happypack = require('happypack') //多线程打包
module.exports = {
plugins:[
new Happypack({
id:'js',
use:[ // use 必须是 数组
{
loader:'babel-loader', //使用 babel-loader 加载器
options:{
presets:['@babel/preset-env','@babel/preset-react'] //将高级语法转化为es5
}
}
]
}),
],
module:{ //规则
noParse: /jquery/, //不去解析 jquery 中的依赖库
rules:[
{
test:/\.js$/, //匹配以 js 结尾的文件
// use:{
// loader:'babel-loader', //使用 babel-loader 加载器
// options:{
// presets:['@babel/preset-env','@babel/preset-react'] //将高级语法转化为es5
// }
// },
use:'Happypack/loader?id=js', //使用 Happypack 中的 loader 来解析,其中 id 是 js ,这时就会去 plugins 中找对应的插件(id:'js')来解析
include:path.resolve(__dirname,'src'), //只解析 src 中的 js 文件
exclude:/node_modules/ //排除掉 node_modules 中的js文件
}
]
},
}
上面是 解析 js 使用 多线程,同理也可以在解析 css 或其他类型文件时也可以使用多线程,差不多的配置
然后将配置前和配置后的 npm run build
打包来对比:
我们可以看到多线程怎么反而是 825ms 比单线程的 420ms 还多呢,这是因为我们现在的项目太小了,在分配线程的时候也会消耗一些性能
14.6 webpack 自带的优化
14.6.1 tree-shaking
先写点代码:
运行后是正常的:
在开发模式下打包:
开发模式下,我们可以看到, sum
和 minus
都有,但是我们使用的时候只用到了 sum
啊,所以打包的时候不想要 minus
,再看看生产模式下打包的结果:
生产环境下打包没有 minus
可以看到: 使用 import
引入时,会在生产环境下,自动去除没用的代码,这叫做 tree-shaking
:把没用到的代码自动删除掉
那么如果是使用 require
导入的呢:
结果运行就报错了:
直接打印 calc
看看:
可以看见 require
的是 es6 的模块,模块里面还有个 default 对象,所以使用的时候得去:
console.log(calc.default.sum(1,2))
:
解决报错后,再来对比生产和开发两种模式下打包的结果:
先是开发模式:
生产模块:
可见:在使用 require
导入的时候,生产模式下还是有 minus
这些没用到的代码
14.6.2 scope hosting 作用域提升
在 index.js
中:
let a = 1;
let b = 2;
let c = 3;
let d = a + b + c;
console.log(d);
//上面这些代码其实可以直接写成 console.log(1+2+3);
先在开发模式下打包:
生产模式下打包:
可以看见:开发模式下只是翻译成了 es5,但是变量都在,但在 生产模式下时, a,b,c,d 这四个变量都不见了,表达式也不见了,只有 console.log(6)
以上就是 scope hosting
--> 在 webpack
中自动省略一些可以简化的代码
15 抽离公共代码(splitChunks)
15.1 抽离公共文件
在多个页面中有公用的部分,我们需要抽离出来,我们来创建一个情景,多入口应用中,两个入口都需要用到 a.js
和 b.js
:
//a.js
console.log('a~~~~~~~~');
//b.js
console.log('b~~~~~~~~');
//index.js
import './a'
import './b'
console.log('index.js');
//other.js
import './a'
import './b'
console.log('other.js');
打包一下:
打包结果可以看到 ,打包后的 index.js
和 other.js
都引用了 a.js
和 b.js
,下面就开始抽离的操作了:
let path = require('path')
module.exports = {
optimization: { //优化项
splitChunks:{ //分割代码块
cacheGroups:{ //缓存组
common:{ //公共模块
chunks:'initial', //抽离的时机:刚开始就进行抽离
minSize:0, //只要大于 0 字节的就抽离出来
minChunks:1, //这个代码块需要引用多少次,才用抽离出来
}
}
}
},
devServer:{
port:3000,
open:true, //自动打开浏览器
contentBase:'./dist', //结果在 ./dist 目录中
},
mode:"production", // production || development
entry:{ //多入口应用
index:'./src/index.js',
other:'./src/other.js'
},
output:{
filename:'[name].js', //打包后多个出口, index.js other.js
path: path.resolve(__dirname,'dist')
},
}
打包:
15.2 抽离第三方包
在 other.js
里面和 index.js
中都加上以下代码:
import $ from 'jquery'
console.log($);
配置:
let path = require('path')
module.exports = {
optimization: { //优化项
splitChunks:{ //分割代码块
cacheGroups:{ //缓存组
common:{ //公共模块
chunks:'initial', //抽离的时机:刚开始就进行抽离
minSize:0, //只要大于 0 字节的就抽离出来
minChunks:1, //这个代码块需要引用多少次,才用抽离出来
},
vendor:{ //第三方包
priority:1, //优先级,因为是从上往下执行的,上面的 common 就直接把多次引用的 juqery 也一起抽离出来了,都不会到这里了,所以要设置优先级,先抽离第三方模块,再抽离公共文件
test:/node_modules/, //把你抽离出来
chunks:'initial', //抽离的时机:刚开始就进行抽离
minSize:0, //只要大于 0 字节的就抽离出来
minChunks:2, //这个代码块需要引用多少次,才用抽离出来
}
}
}
},
}
注意以上的 priority
在抽离第三方包的时候一定要设置,不然按照从上往下执行的顺序, jquery
在 common
中就直接被抽离到 common~index~other.js
中了
打包:
16 懒加载(es6 的 import)
我们在代码中引入其他资源的时候,之前都是直接导入的,就是资源跟代码一起加载出来的,现在懒加载就是实现,当我们需要资源的时候,才去加载资源文件,这里用到的是 es6 草案中的 动态导入文件 的做法:
index.js
中,创建按钮,点击按钮时加载 source.js
中的内容:
// index.js
let button = document.createElement('button')
button.innerHTML = 'hello'
button.addEventListener('click',function() {
console.log('click');
//加载资源:es6 草案中的语法:jsonp实现动态加载文件,返回的是一个 promise
//视频中这里的导入解析报错了,需要一个 @babel/plugin-syntax-dynamic-import@7.2.0 来解析语法
import ('./source').then(data=>{
console.log(data);
})
//实际上 vue懒加载,react 的懒加载都是根据上述的原理实现的
})
document.body.appendChild(button)
es6 草案中的 动态导入文件:import ('./source').then(data=>{console.log(data);})
,原理是 jsonp实现动态加载文件,返回的是一个 promise;
然后在 source.js
中导出一点内容:
// source.js
export default 'abcd'
然后 npm run dev
运行,这时,视频中出现报错,是需要插件来解析上面的 import
语法,但是我本地没有报错,不过还是按照视频中的安装了下:
npm i @babel/plugin-syntax-dynamic-import@7.2.0 -D
然后配置一下:
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin') //模板插件
let Webpack = require('webpack')
// let Happypack = require('happypack') //多线程打包
module.exports = {
devServer:{
port:3000,
open:true, //自动打开浏览器
contentBase:'./dist', //结果在 ./dist 目录中
},
mode:"production", // production || development
entry:{
index:'./src/index.js',
},
output:{
filename:'[name].js', //打包后多个出口, home.js other.js
path: path.resolve(__dirname,'dist')
},
plugins:[
new HtmlWebpackPlugin({ //作用:根据模板生成打包后的入口
template:'./public/index.html', //以该文件为模板
filename:'index.html', //打包后要放在打包目录下的 html 文件的名字,第1个
})
],
module:{ //规则
noParse: /jquery/, //不去解析 jquery 中的依赖库
rules:[
{
test:/\.js$/, //匹配以 js 结尾的文件
use:{
loader:'babel-loader', //使用 babel-loader 加载器
options:{
presets:['@babel/preset-env','@babel/preset-react'], //将高级语法转化为es5
plugins:['@babel/plugin-syntax-dynamic-import'] //用来解析 es6草案中 的动态导入文件的语法
}
},
include:path.resolve(__dirname,'src'), //只解析 src 中的 js 文件
exclude:/node_modules/ //排除掉 node_modules 中的js文件
}
]
},
}
运行后:
打包后:
17 热更新
我们以前每次更新代码,都会导致整个项目全都刷新,我们希望只更新某个部分,比如我只更改了某个组件,完成后希望也只更新这个组件,这就是热更新
这里就不使用懒加载了,直接导入 source.js
:
//index.js
import str from './source'
console.log(str);
然后其实我们每次改变 source.js
的时候,dev
` 运行都可以看到浏览器左上角的刷新图标实现了刷新,
下面配置热更新:
let Webpack = require('webpack')
module.exports = {
optimization: { //优化项
moduleIds:'named' // NamedModulesPlugin 弃用后,替换为 moduleIds 配置,在 webpack 官网可以查到
},
devServer:{
hot:true, //启用热更新:当我只修改了某个文件时,也只更新这个文件,而不是全部代码
port:3000,
open:true, //自动打开浏览器
contentBase:'./dist', //结果在 ./dist 目录中
},
plugins:[
// new Webpack.NamedModulesPlugin(), //已弃用--打印更新的模块路径:告诉我们那个模块更新了-- NamedModulesPlugin → optimization.moduleIds: 'named'
new Webpack.HotModuleReplacementPlugin, //热更新插件:用这个插件来支持热更新
],
}
然后每次修改 source.js
后再看浏览器的这个页面是否刷新,发现还是刷新了,结果还要在 index.js
中做修改:
//直接导入 source.js
import str from './source'
console.log(str);
if(module.hot){ //如果当前模块支持热更新
module.hot.accept('./source',()=>{ //当 './source' 模块更新后,就重新加载 source.js
console.log('文件更新了');
})
}
之后每次修改 source.js
之后,页面的刷新图标都没刷新过,但是打印内容一直在更新
18 Tapable
安装:
npm i tapable@1.1.1 -D