hmr webpack 不编译_从0到1,带你彻底搞懂 vite 中的 HMR 原理

前言

距离尤大大在微博宣布「vite」的出现,不知不觉间已经过了 5 个月多。

bd0420d22b77615d048c3a2c14d06ba7.png

当时,「vite」只是支持对 .vue 文件的即时编译和 importrewrite,相应地「Plugin」也没有几个。并且,最初在「GitHub」上「vite」的 slogan 是这样的:

—— No-bundle Dev Server for Vue 3 Single-File Components.

可以看到,起初介绍「vite」是一个不需要打包的开发阶段的服务器。但是,现在再回首,这句 slogan 已经消失了,而「vite」也已经处于 「beta」 阶段。并且,不仅仅是一个开发阶段的服务器这么简单。相应地也实现了很多「Feature」,例如:Web Assembly、JSX 、CSS Pre-processors、Dev Server Proxy 等等。

有兴趣了解这些「Feature」的同学,可以移步GitHub自行阅读

这两个月的时间,「vite」发展的劲头是非(xue)常(bu)猛(dong)的。并且,也出现了很多关于「vite」的文章,可以说是:“ 如雨后春笋般,络绎不绝 ”。

那么,作为一名「Vue」爱好者,我同样对「vite」充满了好奇。所以,回到本次文章,我会先浅析 webpack-dev-server的「HMR」,然后再循序渐进地讲解「vite」在「HMR」这个过程做了什么。

Webpack 的 HMR 过程

提及「HMR」,不可避免地是会想起现在我们家喻户晓webpack-dev-server 中的「HMR」。所以,我们先来了解一番webpack-dev-server的「HMR」。

首先,我们先对「HMR」建立一个基础的认知。「HMR」 全称即 Hot Module Replacement。相比较「live load」,它具有以下优点:

  • 可以实现局部更新,避免多余的资源请求,提高开发效率
  • 在更新的时候可以保存应用原有状态
  • 在代码修改和页面更新方面,实现所见即所得

而在 webpack-dev-server 中实现「HMR」的核心就是 HotModuleReplacementPlugin ,它是「Webpack」内置的「Plugin」。在我们平常开发中,之所以改一个文件,例如 .vue 文件,会触发「HMR」,是因为在 vue-loader 中已经内置了使用 HotModuleReplacementPlugin 的逻辑。它看起来会是这样

  1. Helloworld.vue
<template>
  <div>hello worlddiv>
template>
<script lang="ts">import { Vue, Component } from 'vue-property-decorator'
  @Componentexport default class Helloworld extends Vue() {}script>
  1. main.js(手动实现「HMR」效果)
import Vue from 'vue'
import HelloWorld from '_c/HelloWorld'

if (module.hot) {
  module.hot.accept('_c/HelloWorld', ()=>{
    // 拉取更新过的 HelloWorld.vue 文件
  })
}

new Vue({
  el: '#app',
  template: ''
  component: { HelloWorld }
})

那么,这个就是 webpack-dev-server 实现「HMR」的本质吗?显然不是,上面说的只是,如果你要通过 webpack-dev-server 实现「HMR」,你可以这么写来实现。

如果究其底层实现,是有两个关键的点

1.与本地服务器建立「socket」连接,注册 hashok 两个事件,发生文件修改时,给客户端推送 hash 事件。客户端根据 hash 事件中返回的参数来拉取更新后的文件。

2.HotModuleReplacementPlugin 会在文件修改后,生成两个文件,用于被客户端拉取使用。例如:

hash.hot-update.json

{
 "c": {
  "chunkname": true
 },
 "h": "d69324ef62c3872485a2"
}

chunkname.d69324ef62c3872485a2.hot-update.js,这里的 chunkname 即上面 c 中对于 key

webpackHotUpdate("main",{
   "./src/test.js":
  (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    eval(....)
  })
})

当然,在这之前还会涉及到对原模块代码的注入,让它具备拉取文件的能力。而这其中实现的细节就不去扣了,要不然有点喧兵夺主的感觉。

有兴趣的同学可以去看看这篇文章一年前,我去面试,小姐姐问我webpack热更新原理,我跟她说了一小时

基于 native ES Module 的 devServer

基于 native ES Module 的 devServer 是「vite」实现「HMR」的重要一环。总体来说,它会做这么两件事:

  • 初始化本地服务器
  • 加载并执行对应的 Plugin,例如 sourceMapPluginmoduleRewritePluginhtmlRewritePlugin 等等。

所以,本质上,devServer 做的最重要的一件事就是加载执行 Plugin。目前,「vite」总共具备了 11Plugin

c1caecc40ae224e3815287c42990a7c0.png

这里大致列举几点 Plugin 会做:

  • 拦截请求,处理「ES Module」语法相关的代码,转化为浏览器可识别的「ES Module」语法,例如第三方模块的 import 转化为 /@module/vue.js
  • .ts.vue 进行即时的编译以及 sassless 的预编译
  • 建立模块间的导入导出关系,即 importeeMap和客户端建立 socket 连接,用于实现「HMR」

这里就列举 devServer 几个常见的 Plugin 需要做的事,至于其他像 wasmPluginwebWorkerPlugin 之类的 Plugin 会做些什么,有兴趣的同学可以自行去了解。

然后,我们再从代码地角度看看它是怎么实现我们上述所说的:

1.首先,我们执行 vite 命令.实际上是运行 cli.js 这个文件,这里我摘取了其中核心的逻辑:

(async () => {
  const { help, h, mode, m, version, v } = argv
  ...
  const envMode = mode || m || defaultMode
  const options = await resolveOptions(envMode)
  // 开发环境下,我们会命中 runServer
  if (!options.command || options.command === 'serve') {
    runServe(options)
  } else if (options.command === 'build') {
    runBuild(options)
  } else if (options.command === 'optimize') {
    runOptimize(options)
  } else {
    console.error(chalk.red(`unknown command: ${options.command}`))
    process.exit(1)
  }
})()
async function runServe(options: UserConfig) {
  // 在 createServer() 的时候会对 HRM、serverConfig 之类的进行初始化
  const server = require('./server').createServer(options)
  ...
}

可以看到,在自执行函数中,我们会命中 runServer() 的逻辑,而它的核心是调用 server.js 文件中的 createServer()

createServer 方法:

export function createServer(config: ServerConfig): Server {
  const {
    ...,
    enableEsbuild = true
  } = config

  const app = new Koa()const server = resolveServer(config, app.callback())const watcher = chokidar.watch(root, {ignored: [/\bnode_modules\b/, /\b\.git\b/]
  }) as HMRWatcherconst resolver = createResolver(root, resolvers, alias)const context: ServerPluginContext = {
    ...
    watcher
    ...
  }
  app.use((ctx, next) => {Object.assign(ctx, context)
    ctx.read = cachedRead.bind(null, ctx)return next()
  })const resolvedPlugins = [
    ...,
    moduleRewritePlugin,
    hmrPlugin,
    ...
  ]// 核心逻辑执行 hmrPlugin
  resolvedPlugins.forEach((m) => m && m(context))const listen = server.listen.bind(server)
  server.listen = (async (port: number, ...args: any[]) => {
    ...
    }) as anyreturn server
}

createServer 方法做了这么几件事:

  • 创建一个 koa 实例
  • 创建监听除了 node_modules 之外的文件的 watcher,并传入 context
  • context 上下文传入并调用每一个 Plugin

到这里,「vite」的 devServer 的创建过程就已经完成。那么,接下来我们去领略一番属于「vite」的「HMR」过程!

vite 的 HMR 过程

在「vite」中「HMR」的实现是以 serverPluginHmr 这个 Plugin 为核心实现。这里我们以 .vue 文件的修改触发的「HMR」为例,这个过程会涉及三个 PluginserverPluginHtmlserverPluginHmrserverPluginVue,这个过程看起来会是这样:

493bc6203ae31dda1ed6e946c948fcd0.png

serverPluginHtml

从前面的流程图可以看到,首先是 serverPluginHtml 这个 Plugin 向 index.html 中注入了获取 hmr 模块的代码:

export const htmlRewritePlugin: ServerPlugin = ({
  root,
  app,
  watcher,
  resolver,
  config
}) => {
  const devInjectionCode =
    `\n

  const scriptRE = /(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值