Webpack5 系列(三):开发环境的设置

1,668 阅读8分钟

一、前言

上一篇讲到如何处理静态资源,本篇将更进一步,介绍如何打造一个基础而不失效率的开发环境。

关键词:HtmlWebpackPlugin、Source Map、Dev Server、Hot Module Replacement

二、Plugins - 快捷打包

如果说,Loader 的作用是将不同的资源进行转换,那么 Plugin 则是在打包的过程中帮我们做一些事情,使打包过程更好管理。

在之前的打包流程中,实际上存在两个问题。

第一,我们是不可以随意删除输出文件夹(我设置的是 dist)下的 index.html 的,打包后的文件以此为 html 模板。

第二,当我们改变输出文件名称时,打包后的新文件与之前没有改名前的旧文件并存。

为了快捷打包,我们需要解决这两个问题。

a) HtmlWebpackPlugin

第一个问题的解决办法就是让 index.html 自动生成。而 HtmlWebpackPlugin 这个插件就是干这个的,它会在打包完成后,在输出目录中自动生成一个 index.html 文件。

在安装插件前,需要在 src 下编写一个 index.html ,以此作为后续打包的模板。

src/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>my webpack demo</title>
  </head>

  <body>
    <div id="root"></div>
  </body>
</html>

安装插件:

npm install --save-dev html-webpack-plugin

webpack.config.js

const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html", // 这里设置自己模板文件
    }),
  ],
};

b) CleanWebpackPlugin

第二个问题的解决办法是在打包之前清除输出目录中的内容,然后让它重新生成。CleanWebpackPlugin 插件虽然不是官方的,但是在 5.20.0 之前的版本中仍然值得推荐。

它的 github 地址如下:github.com/johnagan/cl…

安装插件:

npm install --save-dev clean-webpack-plugin

webpack.config.js

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
  // ...
  plugins: [
    new CleanWebpackPlugin(), // 在打包之前,清除输入目录下的文件
    new HtmlWebpackPlugin({
      template: "./src/index.html", // 这里设置自己模板文件
    }),
  ],
};

c) output.clean

webpack.js.org/configurati…

还有一个更加方便的方法:在 webpack 5.20.0+ 的版本中,内置了清除输出目录内容的功能,只要在 output 选项中配置一个参数即可。

webpack.config.js

module.exports = {
  //...
  output: {
    clean: true, // Clean the output directory before emit.
  },
};

三、Devtool

现在尝试写一个错误的语法,然后打包。

consele.log(123)

虽然写了错误的语法,但是打包仍然会成功,接着在浏览器打开打包好的 index.html,控制台中就会出现报错。

image-20210810213451388.png

当你点击右侧的index.js:79时,就会跳转到出错的位置。

但这个位置会和我们的正常逻辑不同,它会指引你到打包文件中的出错位置,而不是你的业务代码出错的位置

显然,我们作为开发者希望看到的是业务代码中的位置。

这时,就需要用到 devtool 这个选项了。它可以在代码出错时,映射到业务代码出错的位置上。

在不同的环境中,配置是不同的。

1. 开发环境中的 source map

webpack.config.js

module.exports = {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map' // development
}

2. 生产环境中的 source map

module.exports = {
  mode: 'production',
  devtool: 'nosources-source-map', // production
}

参考:webpack.js.org/configurati…

四、DevServer

在通过 Loaders 处理完静态资源以及 Plugins 快捷打包后,我们基本就能愉快地打包文件了。

这时,又遇到了新的问题,什么问题呢?那就是,我们只有在打包完之后,运行代码才能看到打包的结果。

在开发过程中,我们希望的是自动打包,让我们边写代码边看到修改代码后的效果,而不是每次都手动打包。

官方提供了三种方式:

  1. webpack's Watch Mode (监听文件变动,变动则重新打包,输出目录中可以看到新的打包文件。)
  2. webpack-dev-server (开启本地开发服务器,默认端口 8080,编译后的文件存在内存中,而非本地。)
  3. webpack-dev-middleware (将 webpack 作为 Node.js 的中间件)

这些方式都是不错的开发工具,大部分情况下,用第二种就好。

注意:这些工具仅对开发环境有益,在生产环境中请避免这样的使用!!!

1. watch (监听模式)

You can instruct webpack to "watch" all files within your dependency graph for changes. If one of these files is updated, the code will be recompiled so you don't have to run the full build manually.

在 package.json 中设置脚本。

package.json

{
  "scripts": {
    "watch": "webpack --watch", // 监听打包
    "bundle": "webpack" // 普通打包
  },
}

在终端中运行npm run watch,你会发现 webpack 是如何编译你的代码的,在打包完毕后它并不会消失,脚本会持续观察你的文件变化。当你对文件作出修改后,它会自动重新编译发生变化的模块。(也就是说你改了文件内容,它就帮你自动打包。)

2. webpack-dev-server (本地开发服务器)

The webpack-dev-server provides you with a rudimentary web server and the ability to use live reloading.

安装:

npm install --save-dev webpack-dev-server

package.json

{
  "scripts": {
    "start": "webpack serve", // 开启本地服务器
    "watch": "webpack --watch", // 监听打包
    "bundle": "webpack" // 普通打包
  },
}

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // ...
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  devServer: {
    // contentBase: path.join(__dirname, 'dist'), // 指定被访问html页面所在目录的路径
    static: path.join(__dirname, 'dist'), // 注意:Webpack5 中已用 static 替代 contentBase
    open: true, // 开启服务器时,自动打开页面
    compress: true, // 开启 gzip 压缩
    port: 9000, // 自定义端口号
    publicPath: '/' // 服务器访问静态资源的默认路径,优先级高于 output.publicPath
  },
  // ...
}

注意:

  1. 在开发环境中,mode、devtool、devServer这三个配置是非常重要的!
  2. webpack-dev-server 在编译后不会在输出目录写入任何文件。相反,它会将打包的文件存在内存中,就好像它们被安装在服务器根路径上的真实文件一样。如果希望在其他路径上找到打包的文件,可以通过使用 devServer 中的 publicPath 选项更改此设置。

3. webpack-dev-middleware (中间件)— 此部分可略过

webpack-dev-middleware is a wrapper that will emit files processed by webpack to a server. This is used in webpack-dev-server internally, however it's available as a separate package to allow more custom setups if desired.

安装:

npm install --save-dev express webpack-dev-middleware

package.json

{
  "scripts": {
    "server": "node server.js", // 运行 node 服务器
    "start": "webpack serve", // 开启本地服务器
    "watch": "webpack --watch", // 监听打包
    "bundle": "webpack" // 普通打包
  },
}

webpack.config.js

module.exports = {
  // ...
  output: {
    // ...
    publicPath: '/'
  }
}

在根目录下添加一个 server.js

const express = require('express');
const webpack = require('webpack');
const webpackMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config')
const compiler = webpack(config); // 打包编译器

// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
const app = express();
app.use(webpackMiddleware(compiler, {
  publicPath: config.output.publicPath,
}));
app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

运行 npm run server,即可看到效果,服务器在 3000 端口运行:

image-20210814102310053.png

以上,我们实现了自动打包。

五、Hot Module Replacement(HMR) - 热模块替换(热更新)

运行 npm run start,此时,我们尝试对文件进行修改,然后回到页面,你会发现终端内 webpack 帮我们重新编译了代码,然后它会自动刷新,刷新后的页面被重置,之前在页面上的操作不见了,又要重新开始。

我们想要的效果是,当文件修改重新编译后,页面不要全部刷新,只是响应发生变化的那一部分。这时候就要用到 HMR,热模块替换。

注意:HMR 相当于 dev Server 的辅助,同样只用在开发环境,不要用在生产环境中!!!

1. HMR 之前

现在看一下在设置 HMR 之前的情况

index.js

import './assets/styles/reset.css'
import './assets/styles/global.scss'
import { log } from './assets/js/log.js'

const root = document.getElementById('root');

// 1.生成一个按钮
const btn = document.createElement('button');
btn.textContent = 'Add Item';
btn.classList.add('btn');
root.appendChild(btn);
// 2.给按钮添加事件,向 root 上追加 div 元素
btn.addEventListener('click', () => {
  const item = document.createElement('div');
  item.textContent = 'Item ' + (root.children.length);
  item.classList.add('item');
  root.appendChild(item);
});

log('hello', 'world!');

./assets/styles/global.scss

// 自定义变量
$color: #ff4200;
$fs: 14px;
$ls: 1.2;

// 自定义mixin
@mixin size ($w, $h: $w) {
  width: $w;
  height: $h;
}

body {
  font-size: $fs;
  background-color: #eaeaea;

  .btn {
    @include size(100px, 50px);
    background-color: $color;
    border: 1px solid #000;
    color: #fff;
    text-align: center;
    padding: 10px;
    margin: 10px;
    &:hover {
      background-color: #ff4200;
    }
  }
  .item {
    @include size(100px, 50px);
    background-color: #ff4200;
    border: 1px solid #000;
    color: #fff;
    text-align: center;
    padding: 10px;
    margin: 10px;
    &:hover {
      background-color: #ff4200;
    }
  }
  .item:nth-of-type(2n) {
    background-color: blueviolet
  }
}

./assets/js/log.js

const log = (...args) => {
  console.log(...args);
}

export { log };

效果:点击 Add Item 按钮时,下方出现 item。

image-20210815152018031.png

现在,改变样式文件,让偶数次创建的 item 换成黄绿色。

global.scss

// ...
.item:nth-of-type(2n) {
  // background-color: blueviolet;
  background-color: yellowgreen;
}

注意,当我们改完,按下保存的那一刻,页面刷新了,之前的 item 自然也都不见了,我们需要重新点 btn 生成出 item,才能看到修改完的样式效果。

之后,让我们改一下 log.js,

const log = (...args) => {
  console.log(...args, 1);
}

export { log };

同上,保存后页面全局刷新。

2. HMR 之后

  • 在 devServer 配置后,添加 hot 和 hotOnly,意思是开启 HMR
  • 在 plugins 配置后,添加 HMR 插件。(它是 webpack 内置的,记得要在最上面引入一下 webpack)

webpack.config.js

const webpack = require('webpack');

module.exports = {
  // ...
  devServer: {
    contentBase: path.join(__dirname, 'dist'), // 指定被访问html页面所在目录的路径
    // ...
    hot: true, // 开启热更新
    hotOnly: true, // 强制热更新,不会刷新页面
  },
  plugins: [
    // ...
    new webpack.HotModuleReplacementPlugin()
  ],
}

在设置好后,重新执行npm run start

重复上面的样式修改,你会发现,页面并不会全部刷新,但修改的样式已经作用上去了。

然而,对 log.js 尝试修改后,并没有任何的变化。

对于 js 文件来说,需要设置一些东西。

在 index.js 中添加如下代码:

if (module.hot) {
  module.hot.accept('./assets/js/log.js', (arr) => {
    log('hello', 'world!');
  })
}

意思是,如果 webpack 开启了热更新(也就是热替代),那么,第一个参数是接受的发生更新的文件,第二个是当文件更新后触发的回调函数。

如上,我们接收了 log.js 的变化,当变化时,就会执行log('hello', 'world!')

此时,重新执行npm run start,就可以看到效果了。

小结

以上,是本篇的所有内容。

  • 为了以模板为支撑更有效率地输出打包文件,我们需要 HtmlWebpackPlugin
  • 为了快速定位代码错误的位置,我们需要 source map
  • 为了更好地模拟真实环境进行开发,我们需要 devServer(WDS);
  • 为了实时局部更新修改的内容而非全局更新,我们需要 Hot Module Replacement(HMR)!

添加我的微信:with_his_x,共同成长,卷卷群里等你 🤪。

以上,感谢您的阅读~