devServer
自动编译
- 目前我们的代码,为了运行需要有两个操作:
- 操作一:
npm run build
,编译相关的代码; - 操作二:通过
live server
或者直接通过浏览器
,打开index.html代码,查看效果; - 频繁执行这个过程会影响我们的开发效率,其实可以做到,当文件发生变化时,可以自动的完成 编译和展示;
- 操作一:
为了完成自动编译,webpack提供了几种可选的方式:
webpack给我们提供了watch模式:
- 在该模式下,webpack依赖图中的所有文件,只要有一个发生了更新,代码将被重新编译;
- 不需要手动运行 npm run build指令了
如何开启watch呢?两种方式:
添加
watch
的方式虽然可以监听到文件的变化,但它本身没有自动刷新浏览器的功能:- 类似VSCode中使用
live-server
这样的插件可以完成自动刷新的功能; - 在不使用live-server的情况下,可以具备
live reloading
(实时重新加载)的功能,我们可以使用webpack-dev-server
- 类似VSCode中使用
npm install webpack-dev-server -D
"scripts": {
"build": "webpack",
"serve": "webpack serve"
},
contentBase,只能在webpack4中使用,在webpack5的版本中,用static属性来替换,默认是 ‘public’ 文件夹),将其设置为 false 以禁用:
static设置以后如果我们引入了文件中src中没有的内容,就回到默认的public文件夹中查找。
devServer: {
// contentBase: './test', // webpack4版本
// static: false, //禁用
static: ['public'],
},
webpack-dev-server 在编译之后不会写入到任何输出文件,而是将 bundle 文件保留在内存中:
- webpack-dev-server使用了一个库叫memfs(memory-fs webpack自己写的)
开启HMR
修改webpack的配置:
```javascript module.exports = { target: ‘web’,
devServer: … hot: true, }, } ```
- webpack-dev-server使用了一个库叫memfs(memory-fs webpack自己写的)
但是当修改了某一个模块的代码时,依然是刷新的整个页面:
- 需要手动去指定哪些模块发生更新时,进行HMR;
- 这个可以配置在入口文件中; ```javascript import ‘./js/element’; console.log(‘我是测试..’);
// webpack默认情况下不知道要对哪些模块做热替换,所以模块更新的时候都会刷新浏览器。 if (module.hot) { module.hot.accept(‘./js/element.js’, () => { console.log(‘热替换’); }); }
<a name="nJKFH"></a>
## 框架的HMR
开发Vue、React项目时,修改了组件,希望进行热更新,社区已经针对这些有很成熟的解决方案了;
- vue开发中,vue-loader支持vue组件的HMR,提供开箱即用的体验;
- 而react开发中,有React Hot Loader,实时调整react组件(目前React官方已经弃用了,改成使用reactrefresh);
<a name="cNvWx"></a>
## HMR的原理
<a name="EUOSv"></a>
### HMR的原理是什么?如何做到只更新一个模块中的内容?
<a name="coWhJ"></a>
#### 模块热替换(hot module replacement)
- webpack-dev-server会创建两个服务:
- 提供静态资源的服务(express)和 Socket服务(net.Socket);
- express server是负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析);
- HMR Socket Server,是一个socket的长链接:
- 长链接最大的好处是建立链接后双方可以通信(服务器可以直接发送文件到客户端);
- 当服务器监听到对应模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk);
- 通过长连接,可以直接将这两个文件主动发送给客户端(浏览器);
- 浏览器拿到两个新的文件后,通过HMR runtime机制加载这两个文件,并且针对修改的模块进行更新;
![image.png](https://cdn.nlark.com/yuque/0/2022/png/439030/1646899717649-4939afd1-1add-44fd-bb6c-ed622b4e9996.png#clientId=u454c9f0d-93a4-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=492&id=u3a96f1ed&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1568&originWidth=2552&originalType=binary&ratio=1&rotation=0&showTitle=false&size=2623743&status=done&style=none&taskId=udfdc161c-262d-44e0-8cc4-8876985fa62&title=&width=800)
<a name="PVFk0"></a>
## hotOnly、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地址是可以访问的;
<a name="OsSuy"></a>
## port、open、compress
- port设置监听的端口,默认情况下是8080
- open是否打开浏览器:
- 默认值是false,设置为true会打开浏览器;
- 也可以设置为类似于 Google Chrome等值;
- compress是否为静态文件开启gzip compression:
- 默认值是false,可以设置为true;
```javascript
module.export = {
devServer: {
// contentBase: './test',
static: false,
static: ['public'],
hot: true,
compress: false,
host: '0.0.0.0',
port: '5200',
open: true,
},
}
proxy
- proxy用于设置代理来解决跨域访问的问题:
- 比如api请求是 http://localhost:8888,但是本地启动服务器的域名是 http://localhost:8000,这个时候发送网络请求就会出现跨域的问题;
- 可以将请求先发送到一个代理服务器,代理服务器和API服务器没有跨域的问题,就可以解决我们的跨域问题了;
- 我们可以进行如下的设置:
- target:表示的是代理到的目标地址,
- 比如 /api/moment会被代理到:http://localhost:8888/api/moment;
- pathRewrite:默认情况下, /api也会被写入到URL中,可以使用pathRewrite删除;
- secure:默认情况下不接收转发到https的服务器上,如果希望支持,可以设置为false;
- changeOrigin:它表示是否更新代理后请求的headers中host地址;
- changeOrigin官方说的非常模糊,查看源码发现其实是修改代理请求中的headers中的host属性:
- 因为真实的请求是需要通过 http://localhost:8888来请求的;
- 但是因为使用了代码,默认情况下它的值是 http://localhost:8000;
- 如果我们需要修改,那么可以将changeOrigin设置为true即可;
- target:表示的是代理到的目标地址,
devServer: {
...
proxy: {
'/api': 'http://localhost: 8888',
},
},
devServer: {
...
proxy: {
'/api': {
target: 'http://localhost: 8888',
pathRewrite: {
'^/api': '',
},
secure: false,
changeOrigin: true,
},
},
},
historyApiFallback
- historyApiFallback是开发中一个常见的属性,它主要作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误。
- boolean值:默认是false
- 如果设置为true,则在刷新返回时404错误时,会自动返回 index.html 的内容;
- object类型的值,可以配置rewrites属性(了解):
- 可以配置from来匹配路径,决定要跳转到哪一个页面;
- 事实上devServer中实现historyApiFallback功能是通过connect-history-api-fallback库的:
- 可以查看connect-history-api-fallback 文档
resolve模块解析
resolve用于设置模块如何被解析
- 在开发中会有各种各样的模块依赖,这些模块可能来自于自己编写的代码,也可能来自第三方库;
- resolve可以帮助webpack从每个 require/import 语句中,找到需要引入的模块代码;
webpack 使用
enhanced-resolve
来解析文件路径;webpack能解析三种文件路径
绝对路径:
- 由于已经获得文件的绝对路径,因此不需要再做进一步解析。
- 相对路径:
- 在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录;
- 在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径;
模块路径
如果是一个文件:
- 如果文件具有扩展名,则直接打包文件;
- 否则,将使用
resolve.extensions
选项作为文件扩展名解析;
如果是一个文件夹:
extensions是解析到文件时自动添加扩展名:
- 默认值是 [‘.wasm’, ‘.mjs’, ‘.js’,’.json’];
- 所以如果代码中想要添加加载 .vue 或者 jsx 或者 ts 等文件时,必须自己写上扩展名;
另一个非常好用的功能是
配置别名alias
:目前webpack配置信息都是放到一个配置文件中的:
webpack.config.js
- 当配置越来越多时,这个文件会变得越来越不容易维护;
- 并且某些配置是在开发环境需要使用的,某些配置是在生成环境需要使用的,当然某些配置是在开发和生成环境都会使用的;
- 所以,我们最好对配置进行划分,方便我们维护和管理;
- 那么,在启动时如何可以区分不同的配置呢?
- 方案一:编写两个不同的配置文件,开发和生成时,分别加载不同的配置文件即可;
- 方式二:使用相同的一个入口配置文件,通过设置参数来区分它们;
"scripts": {
"serve": "webpack serve --config ./config/webpack.dev.config.js",
"build": "webpack --config ./config/webpack.prod.config.js"
},
入口文件解析
- 入口文件的配置文件所在的位置如果变成了 config 目录,依然要写成 ./src/index.js;
- 这是因为入口文件是和另一个属性是有关的 context;
- context的作用是用于解析入口(entry point)和加载器(loader):
- 官方说法:默认是当前路径(但是经过我测试,默认应该是webpack的启动目录)
- 另外推荐在配置中传入一个值;
这里我们创建三个文件:
pwebpack.comm.conf.js
pwebpack.dev.conf.js
pwebpack.prod.conf.js
我们通过这个插件用来合并不通的配置文件npm install webpack-merge -D
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { DefinePlugin } = require('webpack');
const { VueLoaderPlugin } = require('vue-loader/dist/index');
module.exports = {
target: 'web',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, '../build'),
filename: 'js/bundler.js',
},
resolve: {
extensions: ['.js', '.json', '.mjs', '.vue', '.ts', '.jsx', '.tsx'],
alias: {
'@': path.resolve(__dirname, '../src'),
js: path.resolve(__dirname, '../src/js'),
css: path.resolve(__dirname, '../src/css'),
},
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
{
test: /\.less$/i,
use: ['style-loader', 'css-loader', 'less-loader'],
},
{
test: /\.(jpe?g|png|svg|gif)$/,
type: 'asset',
generator: {
filename: 'img/[name]-[hash:6][ext]',
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024,
},
},
},
{
test: /\.(eot|ttf|woff2?)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name]-[hash:6][ext]',
},
},
{
test: /\.js$/,
loader: 'babel-loader',
},
{
test: /\.vue$/,
loader: 'vue-loader',
},
],
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hello cos..',
template: './public/index.html',
}),
new DefinePlugin({
BASE_URL: '"./"',
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
}),
new VueLoaderPlugin(),
],
};
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common.config');
module.exports = merge(commonConfig, {
mode: 'development',
devtool: 'source-map',
devServer: {
static: ['public'],
hot: true,
compress: false,
host: '0.0.0.0',
port: '5200',
open: true,
proxy: {
'/api': {
target: 'https://o2o.dailyyoga.com.cn',
pathRewrite: {
'^/api': '',
},
secure: false,
changeOrigin: true,
},
},
},
});
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common.config');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = merge(commonConfig, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: 'public',
to: './',
globOptions: {
ignore: ['**/index.html'],
},
},
],
}),
],
});