Webpack学习笔记(二)

623 阅读7分钟

提升开发体验

webpack-dev-server 是 Webpack 官方推出的一款开发工具,根据它的名字我们就应该知道,它提供了一个开发服务器,并且将自动编译和自动刷新浏览器等一系列对开发友好的功能全部集成在了一起。

可能大家都听说过'热更新'这个词,在使用Vue或React官方提供的脚手架时都无意中享受了热更新带给我们友好的开发体验。但是我在这里并不准备直接介绍webpack-dev-server插件的使用,而是手动去创建一个开发环境去达到webpack-dev-server的开发体验,看看webpack-dev-server到底帮我们做了哪些事情。

Webpack自动编译

  1. 首先,我们希望我们修改了代码之后,webpack能帮我们自动编译,而不是每次都需要在命令行重新运行打包命令

针对这个问题,Webpack cli提供了另外一种watch工作模式来解决。在这种模式下,Webpack完成初次构建之后,项目中的源代码会被见识,一旦发生任何改动,Webpack都会自动重新运行打包命令。

└─ 07-wepack-cli-watch
   ├── src
   │   ├── index.html
   │   └── index.js
   │   └── index.css
   └── webpack.config.js
// webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');


module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  }, 
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'My App',
      template: './src/index.html'
    }),
  ]
}
// index.js
console.log('hello world');

启动webpack的watch模式 npx webpack --watch

在用http-server启动一个本地服务 http-server -p 8888

然后我们修改我们的index.js文件,保存,发现我们webpack会自动运行打包命令,然后去刷新页面就能看到我们结果(和正常开发没有什么区别)

- console.log('hello world');
+ console.log('hello world1');

那此时我们的开发体验就是:修改代码 → Webpack 自动打包 → 手动刷新浏览器 → 预览运行结果。

自动刷新

  1. 现在我们已经实现了webpack自动编译,但是如果我们能省去手动刷新浏览器这一步骤,那我们的开发体验将会更好一些。

接下来我们用BrowserSync工具替换我们的http-server。启动http服务,并且监听我们dist目录下的文件变化

cnpm install -D  browser-sync
npx browser-sync dist --watch

这里注意下,由于我们的browser-sync需要额外引入js文件,并且,watch模式下CleanWebpackPlugin插件能正常执行,HtmlWebpackPlugin插件不能正常执行了。所以我们按照原来的方式在dist目录下新建一个index.html,然后引入bundle.js

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *;script-src * 'unsafe-inline'">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script id="__bs_script__">//<![CDATA[
    document.write("<script async src='http://HOST:3000/browser-sync/browser-sync-client.js?v=2.26.13'><\/script>".replace("HOST", location.hostname));
//]]></script>
<script src="./bundle.js"></script>
</body>
</html>
// webpack.config.js
const path = require('path');
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// const HtmlWebpackPlugin = require('html-webpack-plugin');
// const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: 
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  }, 
  plugins: [
  ]
}

然后,我们发现,每当我们修改index.js文件,webpack就会重新打包,并且browser-sync会帮助我们自动刷新浏览器。

现在我们的开发体验就是:修改代码 → Webpack 自动打包 → 自动刷新浏览器 → 预览运行结果。

但是这样的做法还存在一些缺陷:

  1. 我们同时使用了两个工具,增加学习和试错成本
  2. 效率低下,因为整个过程中我们需要把打包好的文件先写入磁盘,然后BrowserSync再进行读取。过程中涉及大量磁盘读写操作,必然会导致效率低下。

使用webpack-dev-server插件

webpack-dev-server是Webpack官方提出的一款开发工具,根据他的名字,我们就应该知道,他提供了一个开发服务器。不仅如此,他还将自动编译和自动刷新浏览器等一系列对开发友好的功能全部集成了在一起。

安装webpack-dev-server

cnpm install webpack-dev-server -D

然后就可以在项目的更目录,运行我们的webpack-dev-server,达到webpack --watch + browser-async的作用

npx webpack-dev-server

不过这里需要注意的是,webpack-dev-server 为了提高工作速率,它并没有将打包结果写入到磁盘中,而是暂时存放在内存中,内部的 HTTP Server 也是从内存中读取这些文件的。这样一来,就会减少很多不必要的磁盘读写操作(可以看到我们dist文件里是没有内容的),大大提高了整体的构建效率。

使用webpack-dev-server实现自动编译 + 刷新

└─ 08-webpack-dev-server-proxy
   ├── src
   │   ├── index.js
   │   └── index.js
   └── index.html
// webapck.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');


module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  }, 
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'My App',
      template: './src/index.html'
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
}
//index.js
import './index.css'
console.log('hello world1')
body {
  background-color: yellow;
}

运行 npx webpack-dev-server --open 可以看到,每次我们修改css文件中的颜色,webpack-dev-server都会帮我们自动打包,然后浏览器会自动刷新。

Proxy 代理

提到webpack-dev-server就不得提webpack-dev-server提供的代理功能。我们日常开发时,通常会启动一个locahost或者以本地IP的方式去启动一个项目,当我们去调用另外一个协议/域名/端口下的资源时,往往会跨域。

那么解决这种开发阶段的跨域请求问题最好的办法是,在开发服务器中配置一个后端API的代理服务(线上一般采用nginx做代理),也就是把后端接口服务代理到本地的开发服务地址(这句话其实我也没太理解)。

const path = require('path');


module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  }, 
  plugins: [
  ],
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.github.com',
        pathRewrite: {
          '^/api': '' // 替换掉代理地址中的 /api
        },
        changeOrigin: true // 确保请求 GitHub 的主机名就是:api.github.com
      }
    }
  }
}

运行 npx webpack-dev-server

然后在url中输入 http://localhost:8080/api/users, 可以看到已经能成功请求到不同域名下的数据了,可以开心的和后端联调了,就算后端没有做CROS

模块热替换(HRM)

之前我们说了,webpack-dev-server能帮助我们监听文件的变化,自动打包,并且刷新浏览器,很大程度上提升了我们的开发体验,但是,真正我们使用起来,还是会发现一些问题。

假设我们在开发过程中修改文件后自动刷新我们的页面,那么我们页面状态的状态会丢失,比如我们在表单中输入了很多内容,然后去修改css的样式,接着就是浏览器自动刷新。也许你觉得这并没有什么奇怪,我们之前开发web应用时都是这样干的。但是webpack提供了更好的功能 => Hot Module Replacement,翻译过来叫作“模块热替换”或“模块热更新”

热更新的“热”

计算机行业经常听到一个叫作热拔插的名词,指的就是我们可以在一个正在运行的机器上随时插拔设备,机器的运行状态不会受插拔的影响,而且插上去的设备可以立即工作,例如我们电脑上的 USB 端口就可以热拔插。

模块热替换中的“热”和这里提到的“热拔插”是相同的意思,都是指在运行过程中的即时变化。

Webpack 中的模块热替换,指的是我们可以在应用运行过程中,实时的去替换掉应用中的某个模块,而应用的运行状态不会因此而改变。

开启 HMR

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');


module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  }, 
  devServer: {
    hot: true,
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'My App',
      template: './src/index.html'
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
}
// 在index.js中导入一个css文件
import './index.css'
body {
  background-color: red;
}
// index.html 为了测试热更新不会刷新页面,我们在html中添加一个输入框
+ <input type="text" />

第一步: 运行 npx webpack-dev-server --open

第二步:在input框中输入内容

第三步: 修改css中的red属性为yellow

第四步: 可以看到我们的颜色变量,但是我们的input内容还存在,说明我们现在的页面并不是粗暴的刷新,而是根据有变化的模块实时的去替换掉应用中的某个模块,而不应用的整个运行状态并不会因此而改变。

脚本文件的模块热替换(HRM)

然后我们再来尝试一下修改 JS 文件。保存过后你会发现,这里的页面依然自动刷新了,好像并没有之前所说 HMR 的体验。

为了再次确认,你可以尝试先在页面中的input框里随意添加一些文字,然后修改代码,保存过后你就会看到页面自动刷新,页面中的状态也就丢失了。这是为什么呢?

Q1:可能你会问,为什么我们开启 HMR 过后,样式文件的修改就可以直接热更新呢?我们好像也没有手动处理样式模块的更新啊?

A1:这是因为样式文件是经过 Loader 处理的,在 style-loader 中就已经自动处理了样式文件的热更新,所以就不需要我们额外手动去处理了。

Q2:那你可能会想,凭什么样式就可以自动处理,而我们的脚本就需要自己手动处理呢?

A2:这个原因也很简单,因为样式模块更新过后,只需要把更新后的 CSS 及时替换到页面中,它就可以覆盖掉之前的样式,从而实现更新。而我们所编写的 JavaScript 模块是没有任何规律的,你可能导出的是一个对象,也可能导出的是一个字符串,还可能导出的是一个函数,使用时也各不相同。所以 Webpack 面对这些毫无规律的 JS 模块,根本不知道该怎么处理更新后的模块,也就无法直接实现一个可以通用所有情况的模块替换方案。

Q3:那可能还有一些平时使用 vue-cli 或者 create-react-app 这种框架脚手架工具的人会说,“我的项目就没有手动处理,JavaScript 代码照样可以热替换,也没你说的那么麻烦”。

A3:这是因为你使用的是框架,使用框架开发时,我们项目中的每个文件就有了规律,例如 React 中要求每个模块导出的必须是一个函数或者类,那这样就可以有通用的替换办法,所以这些工具内部都已经帮你实现了通用的替换操作,自然就不需要手动处理了。

React中的热更新(React-Hot-Loader)

当我们发现js文件修改并不能达到热更新的效果,根据官方提供的文档,我们可以在项目的入口文件中加入一段代码

if (module.hot) {
  module.hot.accept();
}

但是这样做的话相当于每一次都会重新加载所有的模块,毫无疑问,我们组件内部的state都会被清空。下面来介绍一下React框架中帮助我们实现热更新的工具--React-Hot-Loader。

React-Hot-Loader 使用了 Webpack HMR API,针对 React 框架实现了对单个 component 的热替换,并且能够保持组件的 state。 React-Hot-Loader 在编译时会在每一个 React component 外封装一层,每一个这样的封装都会注册自己的 module.hot.accept 回调,它们会监听每一个 component 的更新,在当前 component 代码更新时只替换自己的模块,而不是整个替换 root component。

同时,React-Hot-Loader 对 component 的封装还会代理 component 的 state,所以当 component 替换之后依然能够保持之前的 state。

具体使用方法,参考npm中给出的文档

另外,如果希望hooks中支持热更新的话,可以看一下文档中给到的@hot-loader/react-dom