使用Express实现webpack-dev-server

Webpack 是前端最流行的编译工具之一,而 webpack dev server 是 Webpack 的开发环境服务器,它的底层依赖的就是服务器框架 Express。下面使用 Express 的实现一个简单的 webpack dev s

Webpack 是前端最流行的编译工具之一,而webpack-dev-server是 Webpack 的开发环境服务器,它的底层依赖的就是服务器框架 Express。下面使用 Express 的实现一个简单的webpack-dev-server

webpack 官方和社区提供了两个非常好用的中间件库让我们可以在 Node 中很好的使用 Webpack,我们实现webpack-dev-server也需要借助这两个 库。它们就是webpack-dev-middlewarewebpack-hot-middleware

  • webpack-dev-middleware:开发环境中间件
  • webpack-hot-middleware:热重载中间件

首先安装所有的依赖

yarn add express webpack webpack-dev-middleware webpack-hot-middleware html-webpack-plugin -D

创建配置文件webpack.config.js(这个配置文件和普通配置文件没什么区别),编写下面代码

const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
 
module.exports = {
  mode: "development",
  devtool: "cheap-module-eval-source-map",
  entry: path.resolve(__dirname, "src/index.js"),
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "template.html"),
      filename: "index.html",
    }),
    new webpack.HotModuleReplacementPlugin(), // 注意:这里需要设置模块热重载插件。
  ],
};

在根目录的src文件夹下编写下面的文件

模块文件button.js

export function generateBtn(params) {
  const button = document.createElement("button");
  button.id = "btn";
  button.innerText = "click me";
  button.addEventListener("click", function () {
    alert(this.innerText);
  });
  document.body.appendChild(button);
}

入口文件index.js

import { generateBtn } from "./button";
generateBtn();

Express 服务器文件server.js

const express = require("express");
const webpack = require("webpack");
const webpackConfig = require("./webpack.config");
const webpackDevMiddleware = require("webpack-dev-middleware");
const webpackHotMiddleware = require("webpack-hot-middleware");
 
// 这里只处理 entry 选项是 string 和 array 的情况
if (typeof webpackConfig.entry === "string") {
  const entry = webpackConfig.entry;
  webpackConfig.entry = [entry];
}
webpackConfig.entry.push("webpack-hot-middleware/client"); // 引入 client
 
const app = express();
 
const compiler = webpack(webpackConfig); // 生成 webpack 编译器
 
// 使用中间件,都需要传入 webpack 编译器作为参数
app.use(
  webpackDevMiddleware(compiler, {
    noInfo: true,
  })
);
app.use(webpackHotMiddleware(compiler));
 
const PORT = process.env.PORT || 3000;
 
app.listen(PORT, (error) => {
  if (error) throw error;
  console.log("Server running at http://127.0.0.1:3000");
});

这里采用的是最简单的配置,只实现基本的功能。

首先通过 Express 创建一个服务器,然后分别调用webpack-dev-middlewarewebpack-hot-middleware这 两个中间件。这两个中间件都需要传入compiler参数,而这个参数是通过调用webpack并传入配置文件编译出来的。

另外,为了实现模块的热重载功能,还需要在入口entry中引入webpack-hot-middleware/client这个文件,这里只处理了入口entry的值是字符串和数组这两种类型,多个页面入口的情况暂时不考虑。

然后执行命令启动 Express 服务器

node server.js

发现 Webpack 编译打包成功。页面显示正常,模块文件button.js里面的点击事件监听器也能正常调用。

然后我们修改下模块button中元素的内容,比如把innerText的内容click me改成click me123,重新查看页面,发现页面并没有热重载,然后发现控制台中有警告信息,页面热重载实现失败,页面内容没有更新。不过尝试刷新下页面,浏览器重新渲染页面后,页面内容更新了。

webpack-hot-middleware/client添加参数,在热重载失败的时候自动刷新页面。

webpackConfig.entry.push(
  // 'webpack-hot-middleware/client'
  "webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true"
);

这种传参数的形式有点类似于 HTTP 请求时 URL 中的query字符串。其中reload=true参数表示热重载失败后会自动刷新页面。

重新启动服务器,刷新浏览器页面,再次修改innerText的值,发现页面热重载还是失败,不过浏览器自动刷新页面,重新渲染了页面的内容。其实到这一步,已经基本能满足一般的开发需求了。

如果还需要实现热重载的话,需要编写特定的热重载代码,在入口文件index.js中,增加下面代码

if (module.hot) {
  module.hot.accept("./button", () => {
    document.body.removeChild(document.getElementById("btn"));
    generateBtn();
  });
}

重新启动服务器,刷新浏览器页面,再修改innerText的值,发现页面热重载已经生效。可以通过查看控制台打印的信息确认页面热重载是否生效。

在上面的代码中,需要手动的编写模块module的热重载代码。这些代码表示在模块热重载时,需要先删除掉原来的元素,然后再执行函数generateBtn重新生成新的元素。如果不删除原来的原生,页面中就会出现两个button元素,这样就和预期不相符。

在实际生产环境中,我们一般是使用各种 Loader 去解析模块的,而在这些模块内部已经实现了这样类似的代码,所以不需要我们手动去编写这些代码。