二十一、babel-loader 使用
使用babel-loader
对js文件进行处理,在lg.Webpack.js
配置文件中配置js文件规则。
使用单独的插件进行转换
使用预设进行转换
使用babel.config.js
配置文件进行babel配置
const path = require('path')
const CopyWebpackPlugin = require('copy-Webpack-plugin')
const { DefinePlugin } = require('Webpack')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
const HtmlWebpackPlugin = require('html-Webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'js/main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader']
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'copyWebpackPlugin',
template: './public/index.html'
}),
new DefinePlugin({
BASE_URL: '"./"'
}),
new CopyWebpackPlugin({
patterns: [
{
from: 'public',
globOptions: {
ignore: ['**/index.html']
}
}
]
})
]
}
对于是否转换ES6+语法,此处还需要看.browserslistrc
文件的配置,如果在.browserslistrc
只配置了如:chrome 91
,由于chrome 91
版本已经支持const
以及箭头函数,所以此时babel并不会对箭头函数及块级作用域进行转换。
如果有.browserslistrc
文件配置,还有presets targets
配置,babel会优先以targets为主。
presets targets
配置
二十二、polyfill 配置
Webpack4
时不需要进行单独的polyfill
处理,因为Webpack4默认已经加入的polyfill,但是正因为默认加入了polyfill,导致打包后的产出内容特别大。到了Webpack5
之后,基于优化打包速度的考虑,默认情况下,polyfill就被移除掉了。如果需要用到,就需要自己进行安装配置。
什么是polyfill
Polyfill
是一块代码(通常是 Web 上的 JavaScript),用来为旧浏览器提供它没有原生支持的较新的功能。
为什么使用polyfill
首先在index.js中写入ES6新增的promise
语法,然后执行打包,查看打包结果。
可以发现,打包过后的main.js
中保留了Promise。但是有个问题,如果直接把main.js放入浏览器中运行,例如.browserslistrc
中包含了IE7、IE8、IE9等低版本浏览器,那些不支持Promise的浏览器就会报错。所以想要的是希望打包时,Webpack可以帮助我们定义一个Promise函数,用于支持低版本的浏览器。所以这时候,就需要一个Polyfill的存在,处理babel-preset-env不能处理的更新的语法(generator、symbol、promise
等),以适配低版本浏览器。
更早的时候会使用@babel/polyfill,根据安装提示,查看官方文档。目前已经不建议直接安装@babel/polyfill
,建议使用core-js/stable
以及regenerator-runtime/runtime
。
所以卸载@babel/polyfill
,重新安装core-js/stable
和regenerator-runtime/runtime
yarn add core-js regenerator-runtime
接下来配置babel-loader,之前我们的babel-loader放到了单独的配置文件babel.config.js
中。
index.js导入core-js/stable
、regenerator-runtime/runtime
import "core-js/stable";
import "regenerator-runtime/runtime"
const title = '前端'
const foo = () => {
console.log(title)
}
const p1 = new Promise((resolve, reject) => {
console.log(111)
})
console.log(p1)
foo()
babel-config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
// false: 不对当前的JS处理做 polyfill 的填充
// usage: 依据用户源代码当中所使用到的新语法进行填充
// entry: 依据我们当前筛选出来的浏览器.browserslistrc决定填充什么
useBuiltIns: 'usage',
corejs: 3
}
]
]
}
二十三、Webpack-dev-server 初始
在前端开发过程中,我们希望在一个项目的里程碑的时候对一些功能进行测试或者调试,在手动修改js代码后,希望Webpack重新打包,并自动刷新浏览器操作。之前可以使用–watch的模式进行监听。–watch有两种使用方法,第一种是package.json的scripts中添加命令行参数:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "Webpack --config lg.Webpack.js --watch",
},
第二种是在lg.Webpack.js中添加watch字段,并赋值为true(默认为false)
module.exports = {
watch: true,
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
filename: 'js/main.js',
path: path.resolve(__dirname, 'dist')
},
}
watch和live server的不足:
- 一个文件改动,所有源代码都会重新编译,耗时长
- 每次编译成功后都需要进行文件读写(dist目录)
- live server是node生态下的
- 模块化开发过程中,一个页面的由多个组件组成,我们希望一个组件改动后,只针对该部分组件进行刷新,而不是整个页面都刷新
Webpack Dev Server是Webpack官方推出的一个开发工具,根据名字,就应该知道它提供了一个开发服务器,并且,它将自动编译和自动刷新浏览器等一系列对开发非常友好的功能全部集成在了一起。这个工具可以直接解决我们之前的问题。
打开命令行,以开发依赖安装
yarn add Webpack-dev-server --dev
它提供了一个Webpack-dev-server的cli程序,那我们同样可以直接通过yarn去运行这个cli,或者,可以把它定义到npm script中。运行这个命令 yarn Webpack-dev-server,它内部会自动去使用Webpack去打包应用,并且会启动一个HTTP server去运行打包结果。在运行过后,它还会去监听我们的代码变化,一旦语言文件发生变化,它就会自动立即重新打包,这一点,与watch模式是一样的。不过这里也需要注意Webpack-dev-serverr为了提高工作效率,它并没有将打包结果写入到磁盘当中,它是将打包结果,暂时存放在内存当中,而内部的HTTP server从内存当中把这些文件读出来,然后发送给浏览器。这样一来的话它就会减少很多不必要的磁盘读写操作,从而大大提高我们的构建效率。
这里,我们还可以为这个命令传入一个–open
的参数,它可以用于去自动唤起浏览器,去打开我们的运行地址,打开浏览器过后(如果说你有两块屏幕的话),你就可以把浏览器放到另外一块屏幕当中,然后,我们去体验这种一边编码,一边即时预览的开发环境了。
yarn Webpack-dev-server --open
在package.json中配置server命令:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"serve": "webpack serve"
},
运行yarn serve
,这时动态的更改index.js
,发现浏览器会自动刷新并打印。
当当前端口8080被占用时,我们需要手动指定端口进行启动服务,命令如下:
"serve": "Webpack serve --config lg.Webpack.js --port 3000"
二十四、Webpack-dev-middleware 使用(主要为了解决定制化打包)
Webpack-dev-middleware
是一个容器(wrapper),它可以把 Webpack 处理后的文件传递给一个服务器(server)。 Webpack-dev-server
在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。
首先需要明确,在实际开发阶段很少使用Webpack-dev-middleware
,但是我们需要理解这样做的目的是什么,可以对打包过程做一个自由度非常高的定制。具体实现思路是:
- 在本地利用express开启一个服务
- 将Webpack打包的结果交给这个服务
- 浏览器进行访问
具体实施步骤:
安装express
和Webpack-dev-middleware
yarn add express Webpack-dev-middleware
利用express
自己实现一个server
Server.js
const express = require('express')
const WebpackDevMiddleware = require('Webpack-dev-middleware')
const Webpack = require('Webpack')
const app = express()
// 获取Webpack打包的配置文件
const config = require('./Webpack.config')
const compiler = Webpack(config)
app.use(WebpackDevMiddleware(compiler))
// 开启端口上的服务
app.listen(3000, () => {
console.log('Server 运行在3000端口上')
})
使用 node ./Server.js
启动之后,使用浏览器打开既可以看到我们打包过后的内容。
二十五、HMR 功能使用
HMR全称是Hot Module Replacement,叫做模块热替换或者叫做模块热更新。计算机行业经常听到一个叫做热拔插的名词,那指的就是可以在一个正在运行的机器上随时去插拔设备,而机器的运行状态是不会受插设备的影响,而且插上的设备可以立即开始工作,例如电脑上的USB端口就是可以热拔插的。
模块热替换当中的这个热,跟刚刚提到的热拔插实际上是一个道理,它们都是在运行过程中的即时变化,那Webpack中的模块热替换指的就是可以在应用程序运行的过程中实时的去替换掉应用中的某个模块,而应用的运行状态不会因此而改变。
例如在应用程序的运行过程中,修改了某个模块,通过自动刷新就会导致整个应用整体的刷新,页面中的状态信息都会丢失掉,而如果这个地方使用的是热替换的话,就可以实现只将刚刚修改的这个模块实时的去替换到应用当中,不必去完全刷新应用。
在src/index.js中打印文字,并且在Webpack.config.js中进行配置相应字段:target: 'web'
、devServer: {hot: true}
Webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
filename: 'js/main.js',
path: path.resolve(__dirname, 'dist')
},
target: 'web',
devServer: {
hot: true
},
}
这是启动yarn serve命令,发现我们手动修改index.js中的打印内容后,页面也会随之变化。
index.js配置上需要而更新的模块。
二十六、React 组件支持热更新
第一步创建App.jsx React组件,在其中书写title文字内容。其次在index.js中进行导入,并挂在到提前准备好的index.html模板中的id="app"
中。其次在Webpack.config.js
中引入针对jsx的loader。此时,启动yarn serve,发现每当修改title.js中的打印内容时,console会随之变化。但是当修改App.jsx中的title文字时,发现console中已经热更新的打印内容被清除了。这是因为目前还没有实现针对React组件模块的热更新效果。
第二步,需要实现针对React组件的热更新效果。在Webpack.config.js中引入@pmmmwh/react-refresh-Webpack-plugin
,并且在相应的plugins中进行创建new ReactRefreshWebpackPlugin()
。随后在babel.config.js
中进行单独的配置react-refresh/babel
,使其提供模块热更新的能力。
随后,再次修改title.js中内容后,我们发现对应的console中为打印结果已经被修改。再次修改React组件App.jsx中的title文字内容后,发现console中的打印内容被保留了下来,至此我们就实现了React组件支持热更新功能。
babel.config.js
module.exports = {
presets: [
['@babel/preset-env'],
['@babel/preset-react'],
],
plugins: [
['react-refresh/babel']
]
}
App.jsx
import React, {Component} from 'react'
import {BrowserRouter, Link, Route} from 'react-router-dom'
import Home from './components/Home.jsx'
import About from './components/About.jsx'
class App extends Component{
constructor(props){
super(props)
this.state = {
title: '前端'
}
}
render(){
return (
<div>
<h2>{this.state.title}</h2>
<BrowserRouter>
<Link to='/home'>首页</Link>
<Link to='/about'>关于</Link>
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
</BrowserRouter>
</div>
)
}
}
export default App
index.js
import './title'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.jsx'
if (module.hot) {
module.hot.accept(['./title.js'], () => {
console.log('title.js模块更新')
})
}
ReactDOM.render(<App />, document.getElementById('app'))
webpack.config.js
const path = require('path')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
module.exports = {
watch: true,
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
filename: 'js/main.js',
path: path.resolve(__dirname, 'dist')
},
target: 'web',
devServer: {
hot: true,
hotOnly: true,
port: 4000,
open: false,
compress: true,
historyApiFallback: true
},
module: {
{
test: /\.jsx?$/,
use: ['babel-loader']
}
]
},
plugins: [
new ReactRefreshWebpackPlugin()
]
}
二十七、Vue 组件支持热更新
提前准备App.vue
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello world!',
}
},
}
</script>
<style>
.example {
color: orange;
}
</style>
当前Webpack
并不能识别.vue
结尾的文件,所以需要在Webpack.config.js
中针对.vue结尾的文件进行处理.在Vue2
中使用vue-loader14
版本的时候只需要做如下配置。
{
test: /\.vue$/,
use: ['vue-loader']
}
但是在vue2中使用vue-loader15
版本时,需要单独引入vue-loader/lib/plugin
。
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
filename: 'js/main.js',
path: path.resolve(__dirname, 'dist')
},
target: 'web',
devServer: {
hot: true
},
module: {
rules: [
{
test: /\.jsx?$/,
use: ['babel-loader']
},
{
test: /\.vue$/,
use: ['vue-loader']
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
根据Webpack官方文档的提示,vue-loader是其默认提供的,不需要单独安装。
vue-loader的
16版本是专门提供给Vue3的,不能在Vue2中使用。其次Vue2中,如果使用vue-loader14版本的,可以做到开箱即用。但是如果升级到15版本后,需要单独的引入vue-loader/lib/plugin
,并且进行new操作才可以正常实现HMR功能。
下一步就是在index.js中引入App.vue文件,将其挂在到依赖图当中。
import './title'
import Vue from 'vue'
import App from './App.vue'
if (module.hot) {
module.hot.accept(['./title.js'], () => {
console.log('title.js模块更新')
})
}
new Vue({
render: h => h(App)
}).$mount('#root')
二十八、output 中的 path
- path:所有输出文件的目标路径;打包后文件在硬盘中的存储位置。
- publicPath:输出解析文件的目录,指定资源文件引用的目录 ,打包后浏览器访问服务时的 url 路径中通用的一部分。
区别:
path是Webpack所有文件的输出的路径,必须是绝对路径,比如:output输出的js,url-loader解析的图片,HtmlWebpackPlugin生成的html文件,都会存放在以path为基础的目录下。publicPath 并不会对生成文件的路径造成影响,主要是对你的页面里面引入的资源的路径做对应的补全,常见的就是css文件里面引入的图片。
output:“path”项和“publicPath”项
output项告诉Webpack怎样存储输出结果以及存储到哪里。output的两个配置项“path”和“publicPath”可能会造成困惑。“path”仅仅告诉Webpack结果存储在哪里,然而“publicPath”项则被许多Webpack的插件用于在生产模式下更新内嵌到css、html文件里的url值。
例如,在localhost(译者注:即本地开发模式)里的css文件中边你可能用“./test.png”这样的url来加载图片,但是在生产模式下“test.png”文件可能会定位到CDN上并且你的Node.js服务器可能是运行在HeroKu上边的。这就意味着在生产环境你必须手动更新所有文件里的url为CDN的路径。
然而你也可以使用Webpack的“publicPath”选项和一些插件来在生产模式下编译输出文件时自动更新这些url。
- output
- publicPath: index.html内部的引用路径
- 域名+ publicPath + filename
- devServer
- publicPath:指定本地服务所在的目录
- contentBase:我们打包之后的资源如果说依赖其它的资源,此时就告知去哪找。
{
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
filename: 'js/main.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/'
},
target: 'web',
devServer: {
hot: true,
publicPath: '/',
contentBase: path.resolve(__dirname, 'public'),
watchContentBase: true
},
}
三十、devServer 常用配置
使用Webpack-dev-server
是,有其他可用配置,可使得开发阶段拥有跟好的体验和性能。
-
hotOnly:true
- 当我们的某个组件发生语法性错误时,
Webpack
会自动帮我们抛出错误,但是当我们修改完错误后,Webpack-dev-server
会自动刷新整个页面,这就导致某些已经拥有数据的组件会重新刷新初始化。我们希望在修改完错误后,只针对修改错误的组件进行刷新,这时就可以开启hotOnly只针对当前错误组件进行刷新,保留其他组件的状态。
- 当我们的某个组件发生语法性错误时,
-
port:4000
Webpack-dev-server
默认的端口号是8080
,但是如果我们的8080端口号被其他服务占用时,可以开启port配置,并设置自己想要Webpack-dev-serve提供服务的端口号。
-
open:true
- Webpack-dev-server打包完成后,默认情况下不会帮我们打开浏览器,需要用户手动打开浏览器访问localhost:8080。这时可以设置open为true,当Webpack-dev-server打包完成后,自动打开浏览器。但是,当我们修改其中的文件后,Webpack-dev-server会帮我们再次打开浏览器,这就导致有多个浏览器进程存在,影响性能。所以一般情况下,我们保持关闭false状态。
-
compress:true
- Webpack-dev-server默认打包的资源不会进行压缩,可以开启compress选项,对文件进行gzip压缩,提高页面访问性能。
-
historyApiFallback:true
- 一般情况下,当我们使用Vue或React时,提供路由跳转功能,当前端路由跳转后,浏览器路径为前端控制。但是当我们手动对当前页面刷新(相当于重新向服务器索要about页面),会出现404状态。historyApiFallback开启后,会将404重定向到index.html。
三十一、proxy 代理设置
为什么开发阶段需要设置代理,在开发阶段,我们需要请求后端接口,但是一般后端接口地址和我们本地的不在同一个服务中提供,这时进行访问就会存在跨域的问题,所以我们需要对我们的请求进行转发操作。
在React demo中,index.js使用axios进行请求。
import './title'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.jsx'
import axios from 'axios'
if (module.hot) {
module.hot.accept(['./title.js'], () => {
console.log('title.js模块更新')
})
}
ReactDOM.render(<App />, document.getElementById('app'))
axios.get('/api/users').then((res) => {
console.log(res.data)
})
由于该接口不存在跨域问题,这里默认他会存在这个问题
...
proxy: {
// /api/users
// http://localhost:4000/api/users
// https://api.github.com/info/users
// /api/users---> 返回
'/api': {
target: 'https://api.github.com',
pathRewrite: { "^/api": "" },
changeOrigin: true
}
}
...
首先,在devServer
配置中添加proxy
配置,添加/api
标记,当我们本地服务进行接口请求时,通过/api标记会进入下面的配置中。
设置target属性,告诉Webpack-dev-server
检测到该标记后,去请求那个路径(target: 'https://api.github.com')
。配置过后,我们回到index.js中,修改请求的路径为/api/users
,这是发现依然无法请求成功,该接口依然抛出500服务端异常。
这是因为,github提供的接口下,并没有一个名为/api的服务,所以需要对/api接口 进行路径重写,添加pathRewrite配置,将其值配置为pathRewrite: { “^/api”: “” },告诉Webpack-dev-server遇到https://api.github.com/api
时,自动替换路径为https://api.github.com/
。
这时我们依然发现无法访问,这又是因为github对我们的请求来源进行校验,他拒绝了我们的请求。需要设置changeOrigin属性,更改host来源,changeOrigin: true
。
这时就可以正常访问该接口。
三十二、resolve 模块解析规则
配置模块如何解析。例如,当在 ES2015 中调用 import ‘lodash’,resolve 选项能够对 Webpack 查找 ‘lodash’ 的方式去做修改(查看模块)。
32.1 什么是 resolve 模块解析
在开发中我们会有各种各样的模块依赖,例如 js 文件、css 文件、vue 文件等,有自己编写的,也有第三方库。resolve 可以让 Webpack 在 require/import 语句中,找到需要解析的模块代码
32.2 配置自动寻找依赖的路径
模块路径:在 resolve.modules 中配置:到时导入下载好的依赖就会去 node_modules 文件夹里找
resolve: {
module: ["node_modules"], // 到时就会在 node_modules文件夹里面查找依赖包
}
拓展名配置:配置指定文件后就可以不写此文件的扩展名了
resolve: {
module: ["node_modules"], // 到时就会在 node_modules文件夹里面查找依赖包
extendsions: [".js", ".json", ".mjs", "vue"], // 添加了 vue 后导入 vue 文件就不需要加文件扩展名了
},
配置路径别名:为了简化相对路径的书写,我们直接配置路径给它一个别名:alias
我们需要在 index.js 导入 js 里面的 api.js 时,我们需要写成 ./js/api.js
我们希望写成 js/api ,配置如下:
resolve: {
modules: ["node_modules"], // 到时就会在 node_modules文件夹里面查找依赖包
extensions: [".js", ".json", ".mjs", "vue"], // 添加了 vue 后导入 vue 文件就不需要加文件扩展名了
alias: {
"js": path.resolve(__dirname, "./js"), // '以后可以使用 js 代替 ./js
"@": path.resolve(__dirname, "./src"), // @ 替换根目录
},
},
32.3 不同环境下的 Webpack 配置文件
我们在不同的环境下需要不同的配置,显然一个 Webpack.config.js 配置文件是不够的,在不同的环境使用不同的配置,比如我们在生产环境不需要 clearn-Webpack-plugin 的插件清理旧文件。
我们在根目录下新键文件夹:config,在里面新建 三个文件,一个是公共的配置、一个开发环境、一个生产环境。
配置:
先把 Webpack.config.js 文件内容复制一份去 comm.config.js 里, 然后按需提取至不同的配置文件。
把共同的文件留在 comm.config.js
里,使用插件再去各自合并
merge 插件:
安装:npm install Webpack-merge -D
使用:将公共配置和开发环境的配置结合在一起
const { merge } = require("Webpack-merge");
const commconfig = require("./Webpack.comm.config");
module.exports = merge(commconfig, {
开发环境的配置
});
注意相对路径的变化,除了有些路径的会默认根目录查找,其他正常路径需要修改。
三十三、source-map 作用
js变异之后生成的具体源码,然后再找回到编译之前的源代码的source-map操作。
模式(Mode)
提供 mode
配置选项,告知 Webpack 使用相应模式的内置优化。
string = 'production': 'none' | 'development' | 'production'
用法
只需在配置对象中提供 mode
选项:
module.exports = {
mode: 'development',
};
或者从CLI
参数中传递:
Webpack --mode=development
支持以下字符串值:
选项 | 描述 |
---|---|
development | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development. 为模块和 chunk 启用有效的名。 |
production | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin 和 TerserPlugin 。 |
none | 不使用任何默认优化选项 |
如果没有设置,Webpack 会给 mode
的默认值设置为 production
。
如果 mode 未通过配置或 CLI 赋值,CLI 将使用可能有效的 NODE_ENV 值作为 mode。
source-map工作流程
- 根据源文件中的源代码,生成source-map文件
- 浏览器开启source-map功能,浏览器基于生成的source-map来进行查找
三十四、devtool 详细说明
此选项控制是否生成,以及如何生成 source map。
使用 SourceMapDevToolPlugin
进行更细粒度的配置。查看 source-map-loader
来处理已有的 source map。
devtool
string = 'eval'` `false
选择一种 source map 风格来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。
Webpack 仓库中包含一个 显示所有 devtool 变体效果的示例。这些例子或许会有助于你理解这些差异之处。
你可以直接使用 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 来替代使用 devtool 选项,因为它有更多的选项。切勿同时使用 devtool 选项和 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 插件。devtool 选项在内部添加过这些插件,所以你最终将应用两次插件。
对于开发环境
以下选项非常适合开发环境:
eval
- 每个模块都使用 eval() 执行,并且都有 //# sourceURL。此选项会非常快地构建。主要缺点是,由于会映射到转换后的代码,而不是映射到原始代码(没有从 loader 中获取 source map),所以不能正确的显示行数。eval-source-map
- 每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。eval-cheap-source-map
- 类似 eval-source-map,每个模块使用 eval() 执行。这是 “cheap(低开销)” 的 source map,因为它没有生成列映射(column mapping),只是映射行数。它会忽略源自 loader 的 source map,并且仅显示转译后的代码,就像 eval devtool。eval-cheap-module-source-map
- 类似 eval-cheap-source-map,并且,在这种情况下,源自 loader 的 source map 会得到更好的处理结果。然而,loader source map 会被简化为每行一个映射(mapping)。
特定场景
以下选项对于开发环境和生产环境并不理想。他们是一些特定场景下需要的,例如,针对一些第三方工具。
inline-source-map
- source map 转换为 DataUrl 后添加到 bundle 中。cheap-source-map
- 没有列映射(column mapping)的 source map,忽略 loader source map。inline-cheap-source-map
- 类似 cheap-source-map,但是 source map 转换为 DataUrl 后添加到 bundle 中。cheap-module-source-map
- 没有列映射(column mapping)的 source map,将 loader source map 简化为每行一个映射(mapping)。inline-cheap-module-source-map
- 类似 cheap-module-source-map,但是 source mapp 转换为 DataUrl 添加到 bundle 中。
对于生产环境
这些选项通常用于生产环境中:
(none)
(省略 devtool 选项) - 不生成 source map。这是一个不错的选择。
source-map
- 整个 source map 作为一个单独的文件生成。它为 bundle 添加了一个引用注释,以便开发工具知道在哪里可以找到它。
你应该将你的服务器配置为,不允许普通用户访问 source map 文件!
hidden-source-map
- 与 source-map 相同,但不会为 bundle 添加引用注释。如果你只想 source map 映射那些源自错误报告的错误堆栈跟踪信息,但不想为浏览器开发工具暴露你的 source map,这个选项会很有用。
你不应将 source map 文件部署到 web 服务器。而是只将其用于错误报告工具。
nosources-source-map
- 创建的 source map 不包含 sourcesContent(源代码内容)。它可以用来映射客户端上的堆栈跟踪,而无须暴露所有的源代码。你可以将 source map 文件部署到 web 服务器。
这仍然会暴露反编译后的文件名和结构,但它不会暴露原始代码。
如果默认的 Webpack minimizer 被覆盖 (例如自定义 terser-Webpack-plugin 选项), 请确保将其替换配置为 sourceMap: true 以启用 SourceMap 支持。由上面了解到,设置为development后,默认的devtool为eval,这里我们将它更改为source-map
三十五、ts-loader 编译 TS
在项目开发中我们可能使用TypeScript进行编码开发,所以需要使用Webpack对TypeScript进行编译,编译为JavaScript文件。
首先对ts-loader
进行安装
yarn add ts-loader
Webpack.config.js配置
const path = require('path')
const { DefinePlugin } = require('Webpack')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
const HtmlWebpackPlugin = require('html-Webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/index.ts',
devtool: 'nosources-source-map',
output: {
filename: 'js/main.js',
path: path.resolve(__dirname, 'dist')
},
target: 'web',
devServer: {
hot: true,
port: 4000
},
module: {
rules: [
{
test: /\.jsx?$/,
use: ['babel-loader']
},
{
test: /\.ts$/,
use: ['ts-loader']
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'copyWebpackPlugin',
template: './public/index.html'
}),
new DefinePlugin({
BASE_URL: '"./"'
})
]
}
// source-map cheap-module-source-map
编译完成后,在浏览器中可以正常执行。
三十六、babel-loader 编译 TS
当TypeScript
中存在较新的JavaScript
代码,比如Promise,可以发现使用ts-loader
进行编译没有报错,但是在编译后的文件中,并没有对Promise进行特殊的兼容性处理。所以需要使用babel-loader
对TypeScript进行编译。
之前一直在使用@bebel/preset-env
,这里我们需要使用@bebel/preset-typescript
预设对TypeScript进行编译兼容性处理。
安装@bebel/preset-typescript
yarn add @babel/preset-typescript
在babel.config.js中进行相应配置:
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3
}],
['@babel/preset-typescript']
]
}
再次打包编译后,发现编译后的main.js代码量大了很多,对Promise等ES6+的语法也做了相应的兼容。
ts-loader
和@babel/preset-typescript
的区别:
ts-loader
虽然不能在编译阶段进行polyfill的填充,但是他可以在编译阶段,提前暴露出来语法错误问题。
反而babel-loader
可以进行polyfill填充,但是在编译阶段它不可以进行提前暴露错误,只有在运行阶段时才抛出错误。
如果既想要对TypeScript进行语法转换又想要实时的暴露出错误,TypeScript官网也给出了建议,分先后,在打包之前对语法先做校验,完事之后再做build操作。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npm run ck && webpack",
"serve": "webpack serve",
"ck": "tsc --noEmit"
},
三十七、加载 vue 文件
Webpack对.vue文件加载操作:
创建App.vue,并安装vue
、vue-loader
、vue-template-compiler
。
"vue": "^2.6.14",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14",
首先,在index.js中导入vue
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App)
}).$mount('#app')
其次在Webpack.config.js中配置相应的loader,由于vue文件中存在less模式的css样式,所以需要单独对css文件进行loader转换。
const path = require('path')
const { DefinePlugin } = require('Webpack')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
const HtmlWebpackPlugin = require('html-Webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
mode: 'development',
entry: './src/index.js',
devtool: false,
output: {
filename: 'js/main.js',
path: path.resolve(__dirname, 'dist')
},
target: 'web',
devServer: {
hot: true,
port: 4000
},
module: {
rules: [
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'postcss-loader',
'less-loader'
]
},
{
test: /\.jsx?$/,
use: ['babel-loader']
},
{
test: /\.ts$/,
use: ['babel-loader']
},
{
test: /\.vue$/,
use: ['vue-loader']
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'copyWebpackPlugin',
template: './public/index.html'
}),
new DefinePlugin({
BASE_URL: '"./"'
}),
new VueLoaderPlugin()
]
}
三十八、区分打包环境
尝试为不同的工作环境以创建不同的Webpack配置。创建不同的环境配置的方式主要有两种:
第一种是在配置文件中添加相应的配置判断条件,根据环境的判断条件的不同导出不同的配置。
Webpack配置文件支持导出函数,函数中返回所需要的的配置对象,函数接受两个参数,第一个是env(cli传递的环境名参数),第二个是argv(运行cli过程中传递的所有参数)。可以借助这样一个特点来去实现不同的开发环境和生产环境分别返回不同的配置。
const Webpack = require('Webpack')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
const HtmlWebpackPlugin = require('html-Webpack-plugin')
const CopyWebpackPlugin = require('copy-Webpack-plugin')
module.exports = (env, argv) => {
const config = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
}),
new Webpack.HotModuleReplacementPlugin()
]
}
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins, // ES6将几个数组组合起来,生产环境下需要clean-Webpack-plugin和copy-Webpack-plugin
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
命令行运行:yarn Webpack,当没有传递env参数时,Webpack会默认mode为开发阶段(development),对应的public下的文件不会被复制。
命令行运行:yarn Webpack --env production
,传递env参数后,Webpack以生产环境(production)进行打包,额外的插件会工作,public目录下的文件会被复制。
这就是通过在导出函数中对环境进行判断,从而去实现为不同的环境倒出不同的配置,当然也可以直接在全局去判断环境变量,然后直接导出不同的配置,这样也是可以的。
第二种是为不同的环境单独添加一个配置文件,确保每一个环境下面都会有一个对应的配置文件。
通过判断环境参数数据返回不同的配对象,这种方式只适用于中小型项目。因为一旦项目变得复杂,配置文件也会一起变得复杂起来,所以说对于大型的项目,还是建议大家使用不同环境去对应不同配置文件的方式来实现。一般在这种方式下面,项目当中至少会有三个Webpack配置文件,其中两个(Webpack.dev.js/Webpack.prod.js)是用来适配不同的环境的,那另外一个是一个公共的配置(Webpack.common.js)。因为开发环境和生产环境并不是所有的配置都完全不同,所以说需要一个公共的文件来去抽象两者之间相同的配置。
const HtmlWebpackPlugin = require('html-Webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
})
]
}
Webpack.dev.js
const Webpack = require('Webpack')
const merge = require('Webpack-merge')
const common = require('./Webpack.common')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
plugins: [
new Webpack.HotModuleReplacementPlugin()
]
})
Webpack.prod.js
const merge = require('Webpack-merge')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
const CopyWebpackPlugin = require('copy-Webpack-plugin')
const common = require('./Webpack.common')
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
})
Webpack-merge提供了更加智能的配置合并,使用yarn add Webpack-merge --dev安装到生产环境中。将common中的配置分别于dev和prod组合,生产新的配置。
命令行运行
yarn Webpack --config Webpack.prod.js # --config用于指定配置文件
# 或者 yarn Webpack --config Webpack.dev.js
如果觉得使用命令行太过麻烦,也可以在package.json进行配置
"scripts": {
"prod": "Webpack --config Webpack.prod.js",
"dev": "Webpack --config Webpack.dev.js"
},
随后命令行运行
yarn prod # 或者yarn dev
三十九、合并生产环境配置
代码目录如下:
通过package.json中的scripts命令,指定webpack打包的配置文件(build、build2)。
{
"name": "02_Webpack_config_start",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "Webpack",
"serve": "Webpack serve",
"build2": "Webpack --config ./config/Webpack.common.js --env production",
"serve2": "Webpack serve --config ./config/Webpack.common.js --env development"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.14.8",
"@babel/core": "^7.15.0",
"@babel/plugin-transform-arrow-functions": "^7.14.5",
"@babel/plugin-transform-block-scoping": "^7.14.5",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@pmmmwh/react-refresh-Webpack-plugin": "^0.4.3",
"autoprefixer": "^10.3.1",
"axios": "^0.21.1",
"babel-loader": "^8.2.2",
"clean-Webpack-plugin": "^4.0.0-alpha.0",
"copy-Webpack-plugin": "^9.0.1",
"css-loader": "^6.2.0",
"html-Webpack-plugin": "^5.3.2",
"less": "^4.1.1",
"less-loader": "^10.0.1",
"postcss": "^8.3.6",
"postcss-cli": "^8.3.1",
"postcss-loader": "^6.1.1",
"postcss-preset-env": "^6.7.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-refresh": "^0.10.0",
"react-router-dom": "^5.2.0",
"style-loader": "^3.2.1",
"Webpack": "^5.47.1",
"Webpack-cli": "^4.7.2",
"Webpack-dev-server": "^3.11.2",
"Webpack-merge": "^5.8.0"
},
"dependencies": {
"core-js": "^3.16.0",
"express": "^4.17.1",
"regenerator-runtime": "^0.13.9",
"Webpack-dev-middleware": "^5.0.0"
}
}
paths.js
const path = require('path')
const appDir = process.cwd()
const resolveApp = (relativePath) => {
return path.resolve(appDir, relativePath)
}
module.exports = resolveApp
Webpack.common.js
const resolveApp = require('./paths')
const HtmlWebpackPlugin = require('html-Webpack-plugin')
const { merge } = require('Webpack-merge')
// 导入其它的配置
const prodConfig = require('./Webpack.prod')
const devConfig = require('./Webpack.dev')
// 定义对象保存 base 配置信息
const commonConfig = {
entry: './src/index.js', // 反而没有报错( 相对路径 )
resolve: {
extensions: [".js", ".json", '.ts', '.jsx', '.vue'],
alias: {
'@': resolveApp('./src')
}
},
output: {
filename: 'js/main.js',
path: resolveApp('./dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
esModule: false
}
},
'postcss-loader'
]
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
'less-loader'
]
},
{
test: /\.(png|svg|gif|jpe?g)$/,
type: 'asset',
generator: {
filename: "img/[name].[hash:4][ext]"
},
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
}
},
{
test: /\.(ttf|woff2?)$/,
type: 'asset/resource',
generator: {
filename: 'font/[name].[hash:3][ext]'
}
},
{
test: /\.jsx?$/,
use: ['babel-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'copyWebpackPlugin',
template: './public/index.html'
})
]
}
module.exports = (env) => {
const isProduction = env.production
// 依据当前的打包模式来合并配置
const config = isProduction ? prodConfig : devConfig
const mergeConfig = merge(commonConfig, config)
return mergeConfig
}
Webpack.dev.js
const path = require('path')
const CopyWebpackPlugin = require('copy-Webpack-plugin')
const { DefinePlugin } = require('Webpack')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
const HtmlWebpackPlugin = require('html-Webpack-plugin')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-Webpack-plugin')
module.exports = (env) => {
const isProduction = env.production
return {
mode: 'development',
devtool: false,
entry: './src/index.js',
resolve: {
extensions: [".js", ".json", '.ts', '.jsx', '.vue'],
alias: {
'@': path.resolve(__dirname, 'src')
}
},
output: {
filename: 'js/main.js',
path: path.resolve(__dirname, 'dist')
},
target: 'web',
devServer: {
hot: true,
hotOnly: true,
port: 4000,
open: false,
compress: true,
historyApiFallback: true,
proxy: {
'/api': {
target: 'https://api.github.com',
pathRewrite: { "^/api": "" },
changeOrigin: true
}
}
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
esModule: false
}
},
'postcss-loader'
]
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
'less-loader'
]
},
{
test: /\.(png|svg|gif|jpe?g)$/,
type: 'asset',
generator: {
filename: "img/[name].[hash:4][ext]"
},
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
}
},
{
test: /\.(ttf|woff2?)$/,
type: 'asset/resource',
generator: {
filename: 'font/[name].[hash:3][ext]'
}
},
{
test: /\.jsx?$/,
use: ['babel-loader']
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'copyWebpackPlugin',
template: './public/index.html'
}),
new DefinePlugin({
BASE_URL: '"./"'
}),
new CopyWebpackPlugin({
patterns: [
{
from: 'public',
globOptions: {
ignore: ['**/index.html']
}
}
]
}),
new ReactRefreshWebpackPlugin()
]
}
}
Webpack.prod.js
const CopyWebpackPlugin = require('copy-Webpack-plugin')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
module.exports = {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: 'public',
globOptions: {
ignore: ['**/index.html']
}
}
]
})
]
}
四十、合并开发环境配置
同样通过package.json中的scripts指定Webpack配置文件
package.json
{
"name": "02_Webpack_config_start",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "Webpack",
"serve": "Webpack serve",
"build2": "Webpack --config ./config/Webpack.common.js --env production",
"serve2": "Webpack serve --config ./config/Webpack.common.js --env development"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.14.8",
"@babel/core": "^7.15.0",
"@babel/plugin-transform-arrow-functions": "^7.14.5",
"@babel/plugin-transform-block-scoping": "^7.14.5",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@pmmmwh/react-refresh-Webpack-plugin": "^0.4.3",
"autoprefixer": "^10.3.1",
"axios": "^0.21.1",
"babel-loader": "^8.2.2",
"clean-Webpack-plugin": "^4.0.0-alpha.0",
"copy-Webpack-plugin": "^9.0.1",
"css-loader": "^6.2.0",
"html-Webpack-plugin": "^5.3.2",
"less": "^4.1.1",
"less-loader": "^10.0.1",
"postcss": "^8.3.6",
"postcss-cli": "^8.3.1",
"postcss-loader": "^6.1.1",
"postcss-preset-env": "^6.7.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-refresh": "^0.10.0",
"react-router-dom": "^5.2.0",
"style-loader": "^3.2.1",
"Webpack": "^5.47.1",
"Webpack-cli": "^4.7.2",
"Webpack-dev-server": "^3.11.2",
"Webpack-merge": "^5.8.0"
},
"dependencies": {
"core-js": "^3.16.0",
"express": "^4.17.1",
"regenerator-runtime": "^0.13.9",
"Webpack-dev-middleware": "^5.0.0"
}
}
paths.js
const path = require('path')
const appDir = process.cwd()
const resolveApp = (relativePath) => {
return path.resolve(appDir, relativePath)
}
module.exports = resolveApp
Webpack.common.js
const resolveApp = require('./paths')
const HtmlWebpackPlugin = require('html-Webpack-plugin')
const { merge } = require('Webpack-merge')
// 导入其它的配置
const prodConfig = require('./Webpack.prod')
const devConfig = require('./Webpack.dev')
// 定义对象保存 base 配置信息
const commonConfig = {
entry: './src/index.js', // 反而没有报错( 相对路径 )
resolve: {
extensions: [".js", ".json", '.ts', '.jsx', '.vue'],
alias: {
'@': resolveApp('./src')
}
},
output: {
filename: 'js/main.js',
path: resolveApp('./dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
esModule: false
}
},
'postcss-loader'
]
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
'less-loader'
]
},
{
test: /\.(png|svg|gif|jpe?g)$/,
type: 'asset',
generator: {
filename: "img/[name].[hash:4][ext]"
},
parser: {
dataUrlCondition: {
maxSize: 30 * 1024
}
}
},
{
test: /\.(ttf|woff2?)$/,
type: 'asset/resource',
generator: {
filename: 'font/[name].[hash:3][ext]'
}
},
{
test: /\.jsx?$/,
use: ['babel-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'copyWebpackPlugin',
template: './public/index.html'
})
]
}
module.exports = (env) => {
const isProduction = env.production
process.env.NODE_ENV = isProduction ? 'production' : 'development'
// 依据当前的打包模式来合并配置
const config = isProduction ? prodConfig : devConfig
const mergeConfig = merge(commonConfig, config)
return mergeConfig
}
Webpack.dev.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-Webpack-plugin')
module.exports = {
mode: 'development',
devtool: 'cheap-module-source-map',
target: 'web',
devServer: {
hot: true,
hotOnly: true,
port: 4000,
open: false,
compress: true,
historyApiFallback: true,
proxy: {
'/api': {
target: 'https://api.github.com',
pathRewrite: { "^/api": "" },
changeOrigin: true
}
}
},
plugins: [
new ReactRefreshWebpackPlugin()
]
}
Webpack.prod.js
const CopyWebpackPlugin = require('copy-Webpack-plugin')
const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
module.exports = {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: 'public',
globOptions: {
ignore: ['**/index.html']
}
}
]
})
]
}
babel.config.js
const presets = [
['@babel/preset-env'],
['@babel/preset-react'],
]
const plugins = []
console.log(process.env.NODE_ENV, '<------')
// 依据当前的打包模式来决定plugins 的值
const isProduction = process.env.NODE_ENV === 'production'
if (!isProduction) {
plugins.push(['react-refresh/babel'])
}
module.exports = {
presets,
plugins
}