CcbeanBlog CcbeanBlog
首页
  • 前端文章

    • JavaScript
    • HTML+CSS
    • Vue
    • React
  • 系列笔记

    • React使用学习
    • Vue2源码探究
  • Node文章

    • 基础
    • 问题
    • 框架
  • 系列笔记

    • 数据结构与算法
  • 构建工具文章

    • webpack
  • 系列笔记

    • Webpack5使用学习
  • MySQL
  • Linux
  • 网络
  • 小技巧
  • 杂记
  • 系列笔记

    • Protobuf Buffers
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Ccbean

靡不有初,鲜克有终
首页
  • 前端文章

    • JavaScript
    • HTML+CSS
    • Vue
    • React
  • 系列笔记

    • React使用学习
    • Vue2源码探究
  • Node文章

    • 基础
    • 问题
    • 框架
  • 系列笔记

    • 数据结构与算法
  • 构建工具文章

    • webpack
  • 系列笔记

    • Webpack5使用学习
  • MySQL
  • Linux
  • 网络
  • 小技巧
  • 杂记
  • 系列笔记

    • Protobuf Buffers
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 邂逅Webpack
  • Webpack的配置和处理CSS资源
  • Webpack加载和处理其它资源
  • Webpak模块化原理
  • Webpack中的source-map
  • Webpack中的babel
  • Webpack中的DevServer和HMR
    • webpack watch mode
    • webpack DevServer
    • webpack-dev-middleware
    • 模块热替换HMR
    • 框架中使用HMR
      • React的HMR
      • Vue的HMR
    • HMR的原理
    • 常见配置说明
      • output.publicPath
      • devServer一些配置
      • static.publicPath 或 publicPath
      • static.directory 或 contentBase
      • hotOnly
      • host
      • port
      • open
      • compress
      • proxy
      • historyApiFallback
      • resolve模块解析
      • resolve.extensions
      • resolve.alias
      • 常见配置说明完整配置
  • Webpack环境分离和代码分离
  • Webpack中的DLL、Terser和ScopeHoisting
  • Webpack中的TreeShaking以及其它优化
  • Webpack打包分析
  • Webpack自定义Loader
  • Webpack自定义Plugin
  • 分析React和Vue脚手架
  • Gulp的使用
  • Rollup的使用
  • Vite的使用
  • Webpack5使用学习
ccbean
2022-05-19
目录

Webpack中的DevServer和HMR

# Webpack中的DevServer和HMR

目前我们开发的代码,为了运行需要有两个操作:

  • 操作一:npm run build,编译相关的代码;
  • 操作二:通过live server或者直接通过浏览器,打开index.html代码,查看效果;

这个过程经常操作会影响我们的开发效率,我们希望可以做到,当文件发生变化时,可以自动的完成 编译和展示;

为了完成自动编译,webpack提供了几种可选的方式:

  • webpack watch mode;
  • webpack-dev-server;
  • webpack-dev-middleware

# webpack watch mode

webpack给我们提供了watch模式 (opens new window),在该模式下,webpack依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译,我们不需要手动去运行npm run build指令。

开启watch模式有两种方法:

方式一: 在webpack的配置中,添加 watch: true


 








module.exports = {
  watch: true,
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'build')
  },
  // ...
}

方式二: 在启动webpack的命令中,添加 --watch的标识

"scripts": {
  "build": "webpack",
  "watch": "webpack --watch"
},

执行命令后,当文件繁盛修改,不用再此执行,webpack会自动监听。

# webpack DevServer

上面的方式可以监听到文件的变化,但是事实上它本身是没有自动刷新浏览器的功能的,当然,目前我们可以在VSCode中使用live-server来完成这样的功能。

但是,我们希望在不适用live-server的情况下,可以具备live reloading(实时重新加载)的功能。

我们可以使用DevServer (opens new window)来完成。

安装插件:

npm install webpack-dev-server -D

在package.json中添加启动脚本:

"scripts": {
  "build": "webpack",
  "watch": "webpack --watch",
  "serve": "webpack serve"
},

运行npm run serve脚本就可以使用devServer了。

修改代码后,devServer会自动刷新浏览器页面。webpack-dev-server在编译之后不会将编译结果写入到任何输出文件。而是将 bundle文件保留在内存中,事实上是使用了一个叫memfs (opens new window)的库在内存中操作文件系统。

# webpack-dev-middleware

默认情况下,webpack-dev-server已经帮助我们做好了一切,比如通过express启动一个服务,比如HMR(热模块替换);

如果我们想要有更好的自由度,可以使用webpack-dev-middleware (opens new window)来定制自己的devServer。

webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server。

webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行更多自定义设置。

安装express和webpack-dev-middleware:

npm install express webpack-dev-middleware -D

然后我们创建server.js文件:

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();

const config = require("./webpack.config");

// 传入配置信息, webpack根据配置信息进行编译
const compiler =  webpack(config);

// 生成中间件并使用
const middleware = webpackDevMiddleware(compiler);
app.use(middleware);

app.listen(3000, () => {
  console.log("server has been started at http://localhost:3000");
});

启动服务node server.js,在浏览器中可以看到编译后的效果。

当然,也可以选择Koa来搭建此服务。

以上所有方式的,每次再修改文件自动编译时,都会刷新整个页面,也就是对所有的文件都进行重新编译。假如有成百上千个模块,修改一处,全部编译,显然这是不合理的。

我们可以使用HMR来解决此问题。

# 模块热替换HMR

模块热替换 (opens new window)(HMR - hot module replacement)功能会在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面。

HMR通过如下几种方式,来提高开发的速度:

  • 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失
  • 只更新需要变化的内容,节省开发的时间;
  • 修改了css、js源代码,会立即在浏览器更新,相当于直接在浏览器的devtools中直接修改样式;

默认情况下,webpack-dev-server已经支持HMR,从Webpack4开始,热模块替换是默认开启的;在不开启HMR的情况下,当我们修改了源代码之后,整个页面会自动刷新,使用的是live reloading;

修改webpack配置以开启HMR:

devServer: {
  hot: true
}

此时再执行npm run serve,此时可以看到浏览器控制台中有如下输出:

 

 
 

[HMR] Waiting for update signal from WDS...
main.js:1 hello webpack!
index.js:551 [webpack-dev-server] Hot Module Replacement enabled.
index.js:551 [webpack-dev-server] Live Reloading enabled.

但是此时再修改文件,依然是刷新的整个页面,这是因为我们需要去指定哪些模块发生更新时,进行HMR,修改入口文件main.js如下:





 
 
 
 
 

import './util'

console.log('hello webpack')

if(module.hot) {
  module.hot.accept('./util', () => {
    console.log('utils 更新了')
  })
}

使用accept函数 (opens new window)监听util.js文件,当这个文件发生修改时,Webpack就会进行热替换,不会再刷新整个页面。

# 框架中使用HMR

在开发其他项目时,我们是否需要经常手动去写入module.hot.accpet相关的API,比如开发Vue、React项目,我们修改了组件,希望进行热更新,这个时候应该如何去操作。

这个问题,社区已经有了响应的解决方案 (opens new window)。

  • vue开发中,使用vue-loader,此loader支持vue组件的HMR,提供开箱即用的体验;
  • react开发中,有React Hot Loader (opens new window),实时调整react组件(目前React官方已经弃用了,改成使用react-refresh (opens new window))

# React的HMR

在之前,React是借助于React Hot Loader来实现的HMR,目前已经改成使用react-refresh来实现了。

安装react相关依赖:

npm install -D react react-dom

安装react HRM相关依赖:

npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh

配置webpack.config.js

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.jsx?$/i,
        loader: 'babel-loader'
      }
    ]
  },
  plugins: [
	 // ...
    new ReactRefreshWebpackPlugin()
  ],
}

修改babel.config.js文件:

module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-react',
  ],
  plugins: [
    'react-refresh/babel'
  ]
}

创建App.jsx文件

import React, { Component } from "react";

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      message: "Hello React",
    };
  }

  render() {
    return (
      <div>
        <h2>{this.state.message}</h2>
      </div>
    );
  }
}

export default App;

在main.js中引用

import React from 'react';
import ReactDom from 'react-dom';
import ReactApp from './App.jsx'

ReactDom.render(<ReactApp/>, document.getElementById('reactRoot'))

npm run serve后,刷新页面可以看到渲染出Hello React

# Vue的HMR

Vue的加载需要使用vue-loader,而vue-loader加载的组件默认会帮助我们进行HMR的处理。

安装Vue相关依赖:

npm install -D vue

安装加载vue所需要的依赖:

npm install vue-loader vue-template-compiler -D

配置webpack.config.js

const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.vue$/i,
        loader: 'vue-loader'
      },
      {
        test: /\.css$/i,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
	 // ...
    new VueLoaderPlugin()
  ]
}

创建App.vue文件

<template>
  <div id="app">
    <h2 class="title">{{message}}</h2>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: "Hello Vue"
    }
  }
}
</script>

<style scoped>
  .title {
    color: red
  }
</style>

在main.js中引用

import { createApp } from 'vue'
import VueApp from './App.vue'


createApp(VueApp).mount('#vueRoot')

npm run serve后,刷新页面可以看到渲染出Hello Vue

# HMR的原理

HMR的原理是什么呢?如何可以做到只更新一个模块中的内容呢?

  • webpack-dev-server会创建两个服务:提供静态资源的服务(express)和Socket服务(net.Socket);
  • express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析);
  • HMR Socket Server,是一个通过websocket实现的socket的长连接,:
    • 长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端);
    • 当服务器监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk);
    • 通过长连接,可以直接将这两个文件主动发送给客户端(浏览器);
    • 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新;

webpack中的DevServer和HMR01

  1. 首次请求时,浏览器通过HTTP请求,获取express server中的所有资源。
  2. 当有文件发生变化时,HMR Server会生成一个json文件和做修改的js文件。
  3. 通过websocket连接将这两个文件发送到浏览器,浏览器中的HMR runtime机制,加载这两个文件
  4. 针对修改的模块进行更新。

# 常见配置说明

# output.publicPath

该属性用于指定打包后index.html要加载资源对应的一个基本路径:

  • 它的默认值是一个空字符串,所以打包后引入js文件时,路径就是bundle.js;

    <script defer src="bundle.js"></script>
    

    那么,开发中启动的本地服务,请求资源就类似于http://localhost:8080/bundle.js

  • 在开发中,我们也将其设置为 / ,路径是/bundle.js,

    <script defer src="/bundle.js"></script>
    

    那么浏览器会根据所在的域名+路径去请求对应的资源,也是http://localhost:8080/bundle.js;

  • 如果配置为/assets,那么生成如下:

    <script defer src="/assets/bundle.js"></script>
    

    请求资源就是:http://localhost:8080/assets/bundle.js

  • 如果我们希望在本地直接打开html文件来运行,会将其设置为 ./,路径时./bundle.js,可以根据相对路径去查找资源。

# devServer一些配置

# static.publicPath 或 publicPath

webpack5中已经移除devServer.publicPath配置,可用static.publicPath进行配置

devServer中也有一个publicPath的属性,该属性是指定本地服务所在的文件夹:

  • 它的默认值是 /,也就是我们直接访问端口即可访问其中的资源 http://localhost:8080;
  • 如果我们将其设置为了/abc,那么我们需要通过 http://localhost:8080/abc才能访问到对应的打包后的资源;
  • 并且这个时候,我们其中的bundle.js通过 http://localhost:8080/bundle.js也是无法访问的:
    • 必须将output.publicPath也设置为 /abc;
    • 官方Webpack4文档中有提到 (opens new window),建议 devServer.publicPath 与 output.publicPath相同;

# static.directory 或 contentBase

webpack5中已经移除contentBase配置,可用static.directory进行配置

devServer中contentBase对于我们直接访问打包后的资源其实并没有太大的作用,它的主要作用是如果我们打包后的资源,又依赖于其他的一些资源,那么就需要指定从哪里来查找这个内容:

  • 比如在index.html中,我们需要依赖一个 abc.js 文件,这个文件我们存放在 public文件中;

在index.html中,我们应该如何去引入这个文件呢?

  • 比如代码是这样的:

    <script src="./public/abc.js"></script>
    

    但是这样打包后浏览器是无法通过相对路径去找到这个文件夹的

  • 所以代码是这样的:

    <script src="./abc.js"></script>
    

    再设置contentBase: path.resolve(__dirname, './public')即可

在devServer中还有一个可以监听contentBase发生变化后重新编译的一个属性:watchContentBase,Webpack5中已移除,可使用static.watch配置。

  • 默认情况下当我们修改contentBase加载的文件,是不会触发重新编译的
  • 设置watchContentBase: true,修改文件后会webpack也会自动重新编译。

# hotOnly

webpack5中已经移除该配置

hotOnly是当代码编译失败时,是否刷新整个页面:

  • 默认情况下当代码编译失败修复后,会重新刷新整个页面;
  • 如果不希望重新刷新整个页面,可以设置hotOnly: true,此时仍会继续进行HMR,不会刷新整个页面;

# host

设置主机地址:

  • 默认值是localhost;
  • 如果希望其他地方也可以访问,可以设置为 0.0.0.0;

# port

设置监听的端口,默认情况下是8080

# open

设置是否打开浏览器:

  • 默认值是false,设置为true会打开浏览器;
  • 也可以设置打开指定页面,详见webpack5文档 (opens new window)

# compress

是否为静态文件开启gzip compression

  • 默认值是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地址;即是否修改代理请求中的headers中的host属性,默认是本地的host,改成true后是代理的host。

# historyApiFallback

historyApiFallback (opens new window)是开发中一个非常常见的属性,它主要的作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误。

boolean值:默认是false。如果设置为true,那么在刷新时,返回404错误时,会自动返回 index.html 的内容;

object类型的值,可以配置rewrites属性:可以配置from来匹配路径,to决定要跳转到哪一个页面;

devServer中实现historyApiFallback功能是通过connect-history-api-fallback (opens new window)库的。

# resolve模块解析

resolve用于设置模块如何被解析:

  • 在开发中我们会有各种各样的模块依赖,这些模块可能来自于自己编写的代码,也可能来自第三方库;
  • resolve可以帮助webpack从每个require/import 语句中,找到需要引入到合适的模块代码;
  • webpack 使用 enhanced-resolve (opens new window) 来解析文件路径;

webpack能解析三种文件路径:

  • 绝对路径:由于已经获得文件的绝对路径,因此不需要再做进一步解析。
  • 相对路径:
    • 在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录;
    • 在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径;
  • 模块路径
    • 在 resolve.modules (opens new window)中指定的所有目录检索模块;默认值是 ['node_modules'],所以默认会从node_modules中查找文件

# resolve.extensions

默认值是[string] = ['.js', '.json', '.wasm']

尝试按顺序解析这些后缀名。如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件,并跳过其余的后缀。

确定是文件还是文件夹:

  • 如果是一个文件:

    • 如果文件具有扩展名,则直接打包文件;
    • 否则,将使用resolve.extensions选项作为文件扩展名解析;
  • 如果是一个文件夹:

    • 会在文件夹中根据resolve.mainFiles (opens new window)配置选项中指定的文件顺序查找
      • resolve.mainFiles默认是['index']
      • 然后再根据 resolve.extensions来解析扩展名;

# resolve.alias

  • 当我们项目的目录结构比较深的时候,或者一个文件的路径可能需要 ../../../这种路径片段;
  • 可以给某些常见的路径起一个别名;

# 常见配置说明完整配置

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    clean: true,
    filename: "bundle.js",
    path: path.resolve(__dirname, "./build"),
    // publicPath: '/test'
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/i,
        use: "babel-loader"
      },
      {
        test: /\.vue$/i,
        use: "vue-loader"
      },
      {
        test: /\.css/i,
        use: [
          "style-loader",
          "css-loader"
        ]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html"
    }),
    new ReactRefreshWebpackPlugin(),
    new VueLoaderPlugin()
  ],
  resolve: {
    extensions: ['.js', '.json', '.jsx', '.ts', '.vue', '.wasm', '.mjs'],
    alias: {
      '@': path.resolve(__dirname, './src'),
      "@pages": path.resolve(__dirname, "./src/pages")
    }
  },
  devServer: {
    hot: true,
    host: 'localhost',
    port: 9090,
    open: true,
    compress: true,
    static: {
      directory: path.join(__dirname, './public'), // 同之前的devServer.contentBase
      // publicPath: '/test', // 同之前的devServer.publicPath
      watch: true // 同之前的devServer.watchContentBase
    },
    proxy: {
      // '/api': 'http://httpbin.org',
      '/api': {
        target: 'http://httpbin.org',
        pathRewrite: {
          '^/api': ''
        },
        changeOrigin: true
      },
      '/hello': {
        target: 'http://localhost:9999',
        pathRewrite: {
          '^/hello': ''
        },
        logLevel: 'debug',
        changeOrigin: true
      }
    },
    historyApiFallback: true,
    // historyApiFallback: {
    //   rewrites: [
    //     {from: /abc/, to: "/index.html"}
    //   ]
    // }
  }
}
编辑 (opens new window)
上次更新: 2022/05/31, 23:04:44
Webpack中的babel
Webpack环境分离和代码分离

← Webpack中的babel Webpack环境分离和代码分离→

最近更新
01
阅读精通正则表达式总结
09-29
02
项目搭建规范的配置
07-15
03
Vite的使用
07-03
更多文章>
Theme by Vdoing | Copyright © 2018-2023 Ccbeango
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式