一. 开启本地服务器
1.为什么要搭建本地服务器?
- 目前我们开发的代码,为了运行需要有两个操作:
- 操作一:
npm run build
,编译相关的代码 - 操作二:通过
live server
或者直接通过浏览器,打开index.html
代码,查看效果
- 操作一:
- 这个过程经常操作会影响我们的开发效率,我们希望可以做到,当文件发生变化时,可以自动的完成编译和展示
- 为了完成自动编译,
webpack
提供了几种可选的方式:webpack watch mode
webpack-dev-server
(常用)webpack-dev-middleware
2.webpack-dev-server
上面的方式可以监听到文件的变化,但是事实上它本身是没有自动刷新浏览器的功能的:
- 当然,目前我们可以在
VSCode
中使用live-server
来完成这样的功能 - 但是,我们希望在不适用
live-server
的情况下,可以具备live reloading
(实时重新加载)的功能
- 当然,目前我们可以在
安装
webpack-dev-server
webpacl-dev-server
可以自动对src
的文件进行打包,而且不会生成本地的打包文件- 会直接将打包的文件放到内存中,然后搭建一个服务器,从内存中对打包的文件进行读取,读取之后放到服务器中,浏览器只需对服务器做一个请求即可
jsnpm i webpack-dev-server -D
修改配置文件,启动时加上
serve
参数:js"scripts": { "serve": "webpack serve --config webpack.config.js" }
webpack-dev-server
在编译之后不会写入到任何输出文件,而是将bundle
文件保留在内存中:- 事实上
webpack-dev-server
使用了一个库叫memfs
(后来使用memory-fs webpack
,webpack
自己写的)
- 事实上
二. HMR
热模块替换
1.认识模块热替换(HMR
)
- 什么是
HMR
呢?HMR
的全称是Hot Module Replacement
,翻译为模块热替换(模块热更新)- 模块热替换是指在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面
HMR
通过如下几种方式,来提高开发的速度:- 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失
- 只更新需要变化的内容,节省开发的时间
- 修改了
css
、js
源代码,会立即在浏览器更新,相当于直接在浏览器的devtools
中直接修改样式
- 如何使用
HMR
呢?- 默认情况下,
webpack-dev-server
已经支持HMR
,我们只需要开启即可(默认已经开启) - 在不开启
HMR
的情况下,当我们修改了源代码之后,整个页面会自动刷新,使用的是live reloading
- 默认情况下,
2.开启HMR
修改
webpack
的配置:jsmodule.exports = { // ... devServer: { hot: true } }
浏览器可以看到如下效果:

但是你会发现,当我们修改了某一个模块的代码时,依然是刷新的整个页面:
- 这是因为我们需要去指定哪些模块发生更新时,进行
HMR
jsif (module.hot) { module.hot.accept('./utils/math.js', () => { console.log('math.js模块发生改变') }) }
- 这是因为我们需要去指定哪些模块发生更新时,进行
注意:
- 入口模块发生改变,会刷新页面,但是一般是不会在入口模块中做修改的
- 如果入口模块中引入着
a
,b
模块,而b
模块本身又引入a
模块,即使a
模块在入口模块中被指定了发生更新时进行HMR
,当a
模块改变时,仍然会刷新页面,因为b
模块也引入着a
模块,所以需要在b
模块中也需要指定a
模块发生更新时进行HMR
- 多个模块都引入某一个模块,当该模块发生改变时,多个模块中都需要指定该模块发生更新时进行
HMR
3.框架的HMR
- 有一个问题:在开发其他项目时,我们是否需要经常手动去写入
module.hot.accpet
相关的API
呢?- 比如开发
Vue
、React
项目,我们修改了组件,希望进行热更新,这个时候应该如何去操作呢?
- 比如开发
- 事实上社区已经针对这些有很成熟的解决方案了:
- 比如
vue
开发中,我们使用vue-loader
,此loader
支持vue
组件的HMR
,提供开箱即用的体验 - 比如
react
开发中,有React Hot Loader
,实时调整react
组件(目前React
官方已经弃用了,改成使用react-refresh
)
- 比如
三. devServe
配置
1.host
配置
host
设置主机地址:- 默认值是
localhost
- 如果希望其他地方也可以访问,可以设置为
0.0.0.0
- 默认值是
localhost
和0.0.0.0
的区别:localhost
:本质上是一个域名,通常情况下会被解析成127.0.0.1
127.0.0.1
:回环地址(Loop Back Address
),表达的意思其实是我们主机自己发出去的包,直接被自己接收- 正常的数据库包经常 应用层 - 传输层 - 网络层 - 数据链路层 - 物理层
- 而回环地址,是在网络层直接就被获取到了,是不会经常数据链路层和物理层的
- 比如我们监听
127.0.0.1
时,在同一个网段下的主机中,通过ip
地址是不能访问的
0.0.0.0
:监听IPV4
上所有的地址,再根据端口找到不同的应用程序- 比如我们监听
0.0.0.0
时,在同一个网段下的主机中,通过ip
地址是可以访问的 - 比如:这台电脑
ipv4
地址:192.168.31.252
,同网段下的设备,即可通过该地址搭配端口号进行访问项目
jsmodule.exports = { //... devServe: { host: '0.0.0.0', hot: true } }
- 比如我们监听
2.port、open、compress
port
设置监听的端口,默认情况下是8080
open
是否打开浏览器:- 默认值是
false
,设置为true
会打开浏览器 - 也可以设置为类似于
Google Chrome
等值
jsmodule.exports = { //... devServe: { // host: '0.0.0.0', port: '8888', open: true, compress: true, hot: true } }
- 默认值是
compress
是否为静态文件开启gzip compression
:- 默认值是
false
,可以设置为true
- 默认值是
3.Proxy(Vue项目学习)
proxy
是我们开发中非常常用的一个配置选项,它的目的设置代理来解决跨域访问的问题:- 比如我们的一个
api
请求是http://localhost:8888
,但是本地启动服务器的域名是http://localhost:8000
,这个时候发送网络请求就会出现跨域的问题 - 那么我们可以将请求先发送到一个代理服务器,代理服务器和
API
服务器没有跨域的问题,就可以解决我们的跨域问题了
- 比如我们的一个
- 我们可以进行如下的设置:
target
:表示的是代理到的目标地址,比如/api-hy/moment
会被代理到http://localhost:8888/api-hy/moment
pathRewrite
:默认情况下,我们的/api-hy
也会被写入到URL
中,如果希望删除,可以使用pathRewrite
secure
:默认情况下不接收转发到https
的服务器上,如果希望支持,可以设置为false
changeOrigin
:它表示是否更新代理后请求的headers
中host
地址
4.changeOrigin
的解析(Vue
项目学习)
这个
changeOrigin
官方说的非常模糊,通过查看源码我发现其实是要修改代理请求中的headers
中的host
属性:- 因为我们真实的请求,其实是需要通过
http://localhost:8888
来请求的 - 但是因为使用了代码,默认情况下它的值时
http://localhost:8000
- 如果我们需要修改,那么可以将
changeOrigin
设置为true
即可
- 因为我们真实的请求,其实是需要通过
5.historyApiFallback
(Vue
项目学习)
historyApiFallback
是开发中一个非常常见的属性,它主要的作用是解决SPA
页面在路由跳转之后,进行页面刷新时,返回404
的错误boolean
值:默认是false
- 如果设置为
true
,那么在刷新时,返回404
错误时,会自动返回index.html
的内容
- 如果设置为
object
类型的值,可以配置rewrites
属性:- 可以配置
from
来匹配路径,决定要跳转到哪一个页面
- 可以配置
- 事实上
devServer
中实现historyApiFallback
功能是通过connect-history-api-fallback
库的:- 可以查看
connect-history-api-fallback
文档
- 可以查看
四. 开发和生产环境
1.如何区分开发环境
目前我们所有的
webpack
配置信息都是放到一个配置文件中的:webpack.config.js
- 当配置越来越多时,这个文件会变得越来越不容易维护
- 并且某些配置是在开发环境需要使用的,某些配置是在生成环境需要使用的,当然某些配置是在开发和生成环境都会使用的
- 所以,我们最好对配置进行划分,方便我们维护和管理
那么,在启动时如何可以区分不同的配置呢?
方案一:编写两个不同的配置文件,开发和生成时,分别加载不同的配置文件即可
方式二:使用相同的一个入口配置文件,通过设置参数来区分它们
json"scripts": { "build": "webpack --config ./config/webpack.prod.config.js", "serve": "webpack serve --config ./config/webpack.dev.config.js" },
2.入口文件解析
我们之前编写入口文件的规则是这样的:
./src/index.js
,但是如果我们的配置文件所在的位置变成了config
目录,我们是否应该变成../src/index.js
呢?- 如果我们这样编写,会发现是报错的,依然要写成
./src/index.js
- 这是因为入口文件其实是和另一个属性时有关的
context
- 如果我们这样编写,会发现是报错的,依然要写成
context
的作用是用于解析入口(entry point
)和加载器(loader
):- 官方说法:默认是当前路径(但是经过我测试,默认应该是
webpack
的启动目录) - 另外推荐在配置中传入一个值
- 官方说法:默认是当前路径(但是经过我测试,默认应该是
注意:
context
属性默认是webpack
的启动路径,那启动路径是啥呢,根据package.json
配置文件中的scripts
属性中的命令如:"webpack serve --config ./config/webpack.dev.config.js"
中的./config/
可以推断出:webpack
的启动目录应该是在项目的根目录下,所以entry
中的路径相对的其实context
上下文属性的值,并不是相对当前配置文件所在的路径,相对的是
jsmodule.exports = { //... // context: '', // 默认是webpack的启动路径,那启动路径是啥呢,根据package.json配置文件中的scripts属性中的命令如:"webpack serve --config ./config/webpack.dev.config.js"中的`./config/`可以推断出:webpack的启动目录应该是在项目的根目录下,所以entry中的路径相对的其实context上下文属性的值(即默认是相对的项目的根目录) entry: './src/main.js', // 并不是相对当前配置文件所在的路径,相对的是其实context上下文属性的值 }
webpack5
之后,推出使用merge
命令还对文件进行合并,但vue\react
脚手架中仍然是用文件的方式
3.区分开发和生成环境配置
这里我们创建三个文件:
webpack.comm.conf.js
webpack.dev.conf.js
webpack.prod.conf.js
注意:
合并文件,需先下载
webpack-merge
插件,然后在dev
和prod
配置文件中分别引入使用jsnpm i webpack-merge -D
webpack.dev.conf.js
的代码jsconst { merge } = require('webpack-merge') const commonConfig = require('./webpack.comm.config') // 为什么配置文件用CommonJS的规范进行导入导出? // 因为webpack要跑在node的环境里,而node环境中实现了CommonJS的模块化 module.exports = merge(commonConfig, { // 为什么当前的模块中的配置放在后面,是为了如果有冲突的话,使用后面这个覆盖 mode: "development", devServer: { // host: '0.0.0.0', // port: '8888', open: true, // compress: true, hot: true } })
webpack.prod.conf.js
的代码jsconst { CleanWebpackPlugin } = require('clean-webpack-plugin') const { merge } = require('webpack-merge') const commonConfig = require('./webpack.comm.config') // 为什么配置文件用CommonJS的规范进行导入导出? // 因为webpack要跑在node的环境里,而node环境中实现了CommonJS的模块化 module.exports = merge(commonConfig, { // 为什么当前的模块中的配置放在后面,是为了如果有冲突的话,使用后面这个覆盖 mode: "production", // 设置为生成环境,会自动检查包的大小,如果包过大,会自动提出建议你限制包的大小,通过import()函数限制 output: { clean: true }, plugins: [ new CleanWebpackPlugin() ] })
webpack.comm.conf.js
的代码jsconst path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const { VueLoaderPlugin } = require("vue-loader/dist/index") const { DefinePlugin } = require('webpack') module.exports = { // context: '', // 默认是webpack的启动路径,那启动路径是啥呢,根据package.json配置文件中的scripts属性中的命令如:"webpack serve --config ./config/webpack.dev.config.js"中的`./config/`可以推断出:webpack的启动目录应该是在项目的根目录下,所以entry中的路径相对的其是项目的根目录 entry: './src/main.js', // 并不是相对当前配置文件所在的路径,相对的是context上下文 output: { filename: 'aaa.js', path: path.resolve(__dirname, '../bundle') }, resolve: { extensions: [".js", ".json", ".vue", ".jsx", ".ts", ".tsx"], alias: { utils: path.resolve(__dirname, "./src/utils") } }, module: { rules: [ { test: /\.css$/, // 告诉webpack匹配什么文件 use: [ // use多个loader使用顺序:从下往上 从右往左的 { loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'postcss-loader' } // { // loader: 'postcss-loader', // options: { // postcssOptions: { // plugins: [ // 'autoprefixer' // ] // } // } // } ], //简写一: 如果loader只有一个,可以省略use,直接写在外面 // loader: "css-loader", // 简写二: 多个loader不需要其他属性时, 可以直接写loader字符串形式 // use: [ // "style-loader", // "css-loader" // ] }, { test: /\.less$/, use: [ "style-loader", "css-loader", "less-loader", "postcss-loader" ] }, { test: /\.(png|jpe?g|svg|gif)$/, // 1.打包两张图片, 并且这两张图片有自己的地址, 将地址设置到img/bg中 // // 缺点: 多图片加载的两次网络请求,增加网络请求次数 // type: "asset/resource" // 2.将图片进行base64的编码, 并且直接编码后的源码放到打包的js文件中 // 缺点: 造成js文件非常大, 下载js文件本身消耗时间非常长, 造成js代码的下载和解析/执行时间过长,阻塞js文件后面代码的解析 // type: "asset/inline" // 3.合理的规范: // 3.1.对于小一点的图片, 可以进行base64编码 // 3.2.对于大一点的图片, 单独的图片打包, 形成url地址, 单独的请求这个url图片 type: "asset", parser: { dataUrlCondition: { maxSize: 60 * 1024 // 60kb } }, generator: { // 占位符 // name: 指向原来的图片名称 // ext: 扩展名 // hash: webpack生成的hash,保留下来hash值,防止同名图片冲突,截取哈希值位数 :n // filename: "abc.png" filename: "./img/[name]_[hash:8][ext]" } }, { test: /\.js$/, use: [ { loader: "babel-loader", // options: { // plugins: [ // "@babel/plugin-transform-block-scoping", // "@babel/plugin-transform-arrow-functions" // ] // } } ] }, { test: /\.vue$/, loader: "vue-loader" } ] }, plugins: [ new VueLoaderPlugin(), new HtmlWebpackPlugin({ title: '电商项目', template: "./src/index.html" }), new DefinePlugin({ // 默认情况下有注入一个变量(process.env.NODE_ENV),用来判断当前环境是生产环境还是开发环境 BASE_URL: '"./"', // 外层引号里面的值("./")会被当成js代码执行,相当于通过eval的方式执行,将执行的结果作为这里的值,所以需要在里面再添加引号,这样就会被解析为字符串了 later: "'later'" // later加引号不加引号都行,就是js里的对象的key // 这两个DefinePlugin注入的变量当前项目的其他位置都可以使用 }) ] }