住手!你们不要再打拉!我给你们写个检测项目更新的Webpack插件不就好了吗?

369 阅读6分钟

前言

Hello~大家好。我是秋天的一阵风

在一个阳光明媚的午后,我的同事 前端英雄小帅 正沉浸在代码的世界里,突然,他的钉钉响起了急促的铃声——是 测试女神小美 发来的紧急bug修复请求。小帅没有丝毫犹豫就开始了bug修复的征程。提交、部署、更新状态,一气呵成,仿佛在跳一支优雅的代码华尔兹。终于,小帅伸了个大大的懒腰,准备享受片刻的宁静。

然而,宁静被打破了。小美不知怎的,又将bug状态改回了 “待修复”

小帅瞪大了眼睛,心想:“我不是刚搞定了吗?我的开发环境里一切都完美无瑕啊!”

他立刻在微信群里发出了质疑:“小美,你刷新页面了吗?或者清一下浏览器缓存?”

小美回怼:“我怎么知道你刚部署了?你也没说啊,页面缓存又不是我的问题。”

小帅不甘示弱:“这不是常识吗?”

气氛一下子紧张起来,两人你来我往,仿佛一场没有硝烟的战争即将爆发,看得我目瞪口呆。

ceeb653ely8h0u34v8s8kg207e07ex24.gif

这时,作为曾荣获本市优秀市民奖的我,挺身而出,我说:“停!别争了,我来给你们写个插件。每次部署后,它会在页面上温馨提示用户:‘更新已完成,请刷新页面’。这样,是否刷新就交给用户自己决定吧。”

一、思路

  1. 在项目根目录创建一个VersionFilePlugin.js文件

  2. webpack中提供了一系列 hooks生命周期,在项目编译的时候我们创建一个version.json文件

  3. 每次编译,我们都往version.json写入新的版本号,这里我为了方便直接使用了当前时间的时间戳

  4. App.vue中我们通过fetch请求这个version.json文件,取出版本号进行判断

  5. 如果新的版本号与旧的版本号不一样,则抛出提示,让用户决定是否需要刷新页面。

二、webpakc4 插件

1. 插件是什么

Webpack插件是一个具有apply方法的类,该方法在编译过程中被Webpack编译器(compiler)调用。

apply方法接收一个编译器(compiler)对象作为参数,你可以通过这个对象访问Webpack的钩子(hooks)

2. 插件的生命周期

在Webpack中,生命周期钩子(也称为事件钩子或插件钩子)是编译过程中的不同阶段,Webpack会在这些阶段触发事件,允许插件介入并执行自定义操作。这些钩子是Webpack插件系统的核心,使得开发者可以在Webpack构建流程的不同阶段插入自己的逻辑。

以下是Webpack中一些重要的生命周期钩子:

  1. compile

    • 这个钩子在每次开始新的编译时触发。它提供了一个Compilation对象,你可以用它来访问和修改编译过程中的各种信息。这个钩子常用于初始化任务,比如设置环境变量或者清理之前的构建结果。
  2. seal

    • seal钩子在Compilation对象被封闭(seal)之前触发,这意味着所有的模块和chunk都已经被处理,但是还没有开始生成最终的输出文件。这个钩子适合用于最后的优化和修改Compilation对象,比如删除不必要的模块或者修改chunk的顺序。
  3. afterEmit

    • afterEmit钩子在emit阶段之后触发,即所有的文件已经被写入到输出目录。这个阶段是处理最终输出结果的好时机,比如生成额外的资源文件、记录构建信息或者执行清理工作。
  4. done

    • done钩子在编译完成时触发,无论是成功还是失败。这个钩子可以用来执行清理工作,比如关闭数据库连接,或者发送构建完成的通知。

这些钩子是Webpack插件系统中非常核心的部分,它们允许开发者在构建流程的关键点插入自定义逻辑,从而实现高度定制化的构建过程。

3. 关键对象:compilation

我们再重点了解一下afterEmit钩子以及它的 compilation 对象。

在Webpack 4中,afterEmit是一个非常重要的生命周期钩子,它在资源生成并输出到输出目录之后被触发。这个钩子允许开发者在Webpack的构建过程结束之后执行自定义逻辑,例如清理旧文件、记录构建信息或者执行其他后续处理。

(1) afterEmit钩子的参数

afterEmit钩子接收两个参数:

  • compilation:这是一个包含了当前编译上下文详细信息的对象。它提供了对编译过程中生成的资源和模块的访问。通过compilation对象,你可以获取构建过程中的各种数据,包括编译生成的资源(assets)、模块(modules)和警告(warnings)等。

  • callback:这是一个必须被调用的回调函数,用于指示异步操作的完成。如果你的钩子处理逻辑是异步的,你需要在操作完成后调用这个回调函数。

Tips: 关于webpack4的生命周期hooks

你可以参考我的这篇文章:自定义Webpack插件:五步清除项目中的“僵尸”文件!

或者是官方文档:Compilation Hooks - webpack 4 Documentation

三、具体代码

1. vue.config.js

//...
const VersionFilePlugin = require('./VersionFilePlugin');

module.exports = {
 // ...
 configureWebpack: {
    //...
    plugins: [new VersionFilePlugin()],
  },
}

2. VersionFilePlugin.js

class VersionFilePlugin {
  apply(compiler) {
    // Webpack编译过程 compiler.hooks.afterEmit异步钩子
    compiler.hooks.afterEmit.tapAsync('versionPlugin', (compilation, callback) => {
      const versionInfo = {
        version: Date.now(),
        buildTime: Date.now(),
      };
      const fileContent = JSON.stringify(versionInfo, null, 2);
      // 将此列表作为新的文件资产插入到webpack生成中:
      compilation.assets['version.json'] = {
        source: () => {
          return fileContent;
        },
        size: () => {
          return fileContent.length;
        },
      };
      callback();
    });
  }
}

module.exports = VersionFilePlugin;

3. App.vue

<template>
  <div id="app">
    <router-view />
  </div>
</template>
<script>

export default {
  name: 'App',
  data() {
    return {};
  },
  created() {
    this.checkVersionUpdate();
  },
  methods: {
    checkVersionUpdate() {
      setInterval(() => {
        fetch(`${window.location.origin}/version.json`)
          .then((response) => response.json())
          .then((data) => {
            const currentVersion = localStorage.getItem('currentVersion');
            if (currentVersion && currentVersion !== data.version.toString()) {
              this.showMessageBox();
            }
            localStorage.setItem('currentVersion', data.version.toString());
          });
      }, 30000); // 每30秒检查一次
    },
    showMessageBox() {
      this.$alert('系统更新,是否要刷新页面?', '系统提示', {
        confirmButtonText: '需要',
        cancelButtonText: '不需要',
        showCancelButton: true,
        type: 'info',
        center: true,
        dangerouslyUseHTMLString: true,
        callback: (action) => {
          if (action === 'confirm') {
            window.location.reload();
          }
        },
      });
    },
  },
};
</script>


4. 效果展示

image.png

三、可优化的地方

上面的代码例子中,我们是采用了最简单的轮询做法定时去获取版本号,如果仅仅是在我们自己的开发环境使用这种做法那么确实问题不大,但是如果要真正投入到生产环境中肯定是有不小问题的。

因为轮询存在着资源浪费、延迟、可维护性不足等等不足之处。所以我提供几种优化方案供大家选择和参考

1. 使用 Web Workers

将轮询逻辑放入 Web Workers 中运行,避免阻塞主线程。

// **`versionWorker.js`**
self.addEventListener('message', (event) => {
  const { currentVersion, delay } = event.data;

  const checkForUpdate = () => {
    fetch(`${window.location.origin}/version.json`)
      .then((response) => response.json())
      .then((data) => {
        if (currentVersion && currentVersion !== data.version.toString()) {
          // 发送消息到主线程
          self.postMessage({ action: 'updateAvailable', version: data.version });
        }
        // 设置下一次检查
        setTimeout(checkForUpdate, delay);
      })
      .catch((error) => {
        console.error('Error checking for update:', error);
        // 重试
        setTimeout(checkForUpdate, delay);
      });
  };

  // 启动轮询
  checkForUpdate();
});
// App.vue

<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {};
  },
  created() {
    this.initVersionWorker();
  },
  methods: {
    initVersionWorker() {
      // 检查浏览器是否支持 Web Workers
      if (window.Worker) {
        const worker = new Worker('versionWorker.js'); // 确保路径正确
        const currentVersion = localStorage.getItem('currentVersion');
        const delay = 30000; // 每30秒检查一次

        // 向 Web Worker 发送初始数据
        worker.postMessage({ currentVersion, delay });

        // 监听 Web Worker 发送的消息
        worker.onmessage = (event) => {
          if (event.data.action === 'updateAvailable') {
            this.showMessageBox();
            localStorage.setItem('currentVersion', event.data.version.toString());
          }
        };
      } else {
        console.error('Web Workers are not supported in this browser.');
      }
    },
    showMessageBox() {
      this.$alert('系统更新,是否要刷新页面?', '系统提示', {
        confirmButtonText: '需要',
        cancelButtonText: '不需要',
        showCancelButton: true,
        type: 'info',
        center: true,
        dangerouslyUseHTMLString: true,
        callback: (action) => {
          if (action === 'confirm') {
            window.location.reload();
          }
        },
      });
    },
  },
};
</script>

2. 根据页面活跃状态动态调整轮询

当用户离开页面时暂停轮询,回到页面时恢复轮询

let pollingInterval = null;

const startPolling = () => {
  pollingInterval = setInterval(() => {
    // 检测逻辑
  }, 600000); // 每10分钟检查一次
};

const stopPolling = () => {
  clearInterval(pollingInterval);
  pollingInterval = null;
};

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    stopPolling();
  } else {
    startPolling();
  }
});

startPolling();

3. 使用 WebSocket 或 Server-Sent Events

如果服务器支持,可以使用 WebSocketSSE 实现实时推送,服务器在版本更新时主动通知客户端

const eventSource = new EventSource('/updates');

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  const currentVersion = localStorage.getItem('currentVersion');
  if (currentVersion && currentVersion !== data.version.toString()) {
    this.showMessageBox();
  }
  localStorage.setItem('currentVersion', data.version.toString());
};

Tips: 关于websocket和Server-Sent Events的介绍与对比,你可以参考我这篇文章: WebSocket太笨重?试试SSE的轻量级魅力!