Vite 从入门到精通 -- 学习笔记(二)

169 阅读6分钟

Vite 中的热更新

HMR PAI

HMR 可能已经在特定于框架的启动器模板中处理过了

Vite 通过特殊的 import.meta.hot 对象暴露手动 HMR API。

  1. 必需的条件守卫
    if (import.meta.hot) {
      // HMR 代码
    }
    
  2. hot.accept(cb) 要接收模块自身,应使用 import.meta.hot.accept,参数为接收已更新模块的回调函数:
    export const count = 1
    
    if (import.meta.hot) {
      import.meta.hot.accept((newModule) => {
        if (newModule) {
          // newModule is undefined when SyntaxError happened
          console.log('updated: count is now ', newModule.count)
        }
      })
    }
    

HMR API

  1. hot.accept hot.accept(cb)

     要接收模块自身时使用, 参数为接收已更新模块的回调函数
     接受” 热更新的模块被认为是 HMR 边界。
    

    hot.accept(deps,cb)

     模块也可以接受直接依赖项的更新, 而无需重新加载自身
    
  2. hot.dispose(cb)

       一个接收自身的模块或者一个期望被其他模块接收的模式可以使用 *hot.dispose* 来清除任何由其更新副本产生的持久副作用
    
  3. hot.prune(cb)

       注册一个回调,当模块在页面上不再被导入时调用
    
  4. hot.data

       import.meta.hot.data 对象在同一个更新模块的不同实例之间持久化。它可以用于将信息从模块的前一个版本传递到下一个版本。
    
  5. hot.decline

      目前是一个空操作并暂留用于向后兼容; 指明某模块是不可热更新的,请使用 *hot.invalidate()*
    
  6. hot.invalidate(message?: string)

      调用该方法 HMR 服务将使调用方的导入失效; 建议在 accept 回调中调用 invalidate
    
  7. hot.on(event, cb)

    监听自定义 HMR 事件。 以下 HMR 事件由 Vite 自动触发:

'vite:beforeUpdate' 当更新即将被应用时(例如,一个模块将被替换)
'vite:afterUpdate' 当更新已经被应用时(例如,一个模块已被替换)
'vite:beforeFullReload' 当完整的重载即将发生时
'vite:beforePrune' 当不再需要的模块即将被剔除时
'vite:invalidate' 当使用 import.meta.hot.invalidate() 使一个模块失效时
'vite:error' 当发生错误时(例如,语法错误)
  1. hot.send(event, data)

      发送自定义事件到 Vite 开发服务器
    

例子:

  // renderA.js
  let timer;
export function render() {
      timer = setInterval(() => {
        index++;
        document.querySelector('#app').innerHTML = `
              <div>
                <h1>Hello Vite!</h1>
                <a href="https://cn.vitejs.dev/guide/api-hmr.html" target="_blank">Document${index</a>
              </div>
            `
      }, 1000);

}

let index = import.meta.hot.data?.cache?.getIndex?.() || 0;
// 帮助保存在同一个模块中热更新之前保存的状态, 直接用到更新之后的模块中

if (import.meta.hot) {
  import.meta.hot.data.cache = {
    getIndex() {
      return index;
    }
  }
  import.meta.hot.dispose(() => {
    // 清理副作用
    if (timer) {
      clearInterval(timer);
    }
  })
  // 表示此模块不可热更新
  // import.meta.hot.decline();
}

export { index };

// main.js
import { render } from './renderA';
import './style.css';

render();
if (import.meta.hot) {
  import.meta.hot.accept(['./renderA'], ([newA]) => {
    if (newA.index > 15) {
      import.meta.hot.invalidate()
    } else {
      newA?.render();
    }
  })

  import.meta.hot.accept();

}

预编译优化

CommonJS 和 UMD 兼容性

  CommonJS to ESM
   开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM

性能

Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能
在vite.config.js 中使用optimizeDeps选项进行 ¶依赖优化选项
 optimizeDeps: {
   entries: string | string[];
   exclude: string[]; // 在预构建中强制排除的依赖项
   include: string[]; // 强制预构建链接的包
   esbuildOptions:  EsbuildBuildOptions; // 在部署扫描和优化过程中传递给 esbuild 的选项  1. 忽略了 external 选项,请使用 Vite 的 optimizeDeps.exclude 选项 2. plugins 与 Vite 的 dep 插件合并
   force: boolean; // 设置为 true 可以强制依赖预构建, 而忽略之前已经缓存过的、已经优化过的依赖。
 }

注意: 依赖预构建仅会在开发模式下应用,并会使用 esbuild 将依赖转为 ESM 模块。在生产构建中则会使用 @rollup/plugin-commonjs

服务端集成

在非node环境中集成Vite

集成Vite项目:

  1. Vite 配置中配置入口文件和启动创建manifest:

    // vite.config.js
    export default defineConfig({
        build: {
            manifest: true
        }
    })
    
  2. 在开发环境中, 在服务器的HTML模版中注入以下内容:

      <!-- 如果是在开发环境中 -->
      <script type="module" src="http://localhost:3000/@vite/client"></script>
      <script type="module" src="http://localhost:3000/main.js"></script>
    

    ⚠️: 还要确保服务器配置为提供Vite工作目录中的静态资源, 否则图片等资源将无法正确加载

  3. 在生产环境中, 在运行yarn build之后, 会生成menifest。json文件将与静态文件一同生成, 在模版语言中将生成的文件引入进去

    ⚠️注意: 需要将引入文件目录映射到生成的静态文件目录下 例如: pug模版语言

    // pug 文件: index.pug(一种模版语言)
    html
       head
           title= title
           link(href=css rel="stylesheet")
       body
           h1= message
           div(id="app")
           script(src=vendor)
           script(src=index)
    
    // server.js
     const express = require('express')
     const app = express()
    
     const manifest = require('./dist/manifest.json')
    
    
     app.set("view engine", 'pug')
     app.use(express.static("dist")) // 将引入文件地址映射到dist静态文件目录中
    
     app.get('/', (req, res) => {
       res.render("index", {
         title: 'key',
         message: 'Hello there',
         index: manifest['index.html'].file,
         vendor: manifest['index.html'].dynamicImports.vendor,
         css: manifest['index.html'].css[0]
       })
     })
    

node环境中集成Vite

服务端SSR: 一个典型的SSR 应用应该由如下的源文件结构

  - index.html
      - src
        - main.js   # 导出环境无关的(通用的)应用代码
        - entry.client.js   // 将应用挂载到一个 DOM 元素上
        - entry.server.js    // 使用某框架的 SSR API 渲染该应用

index.html 将需要引用 entry-client.js 并包含一个占位标记供给服务端渲染时注入

  <div id="app"><!--ssr-outlet--></div>
  <script type="module" src="/src/entry-client.js"></script>

情景逻辑:

  1. 设置开发服务器
 // server.js
 import express from 'express';
 import fs from 'fs';
 import { createServer as createViteServer } from 'vite';

 const app = express();

 // 以中间件模式创建 Vite 应用,这将禁用 Vite 自身的 HTML 服务逻辑
 // 并让上级服务器接管控制
 createViteServer({
   server: {
     // middlewareMode: 'html'
     middlewareMode: 'ssr' // 对于请求的管理会转交给自己的服务
   }
 }).then(vite => {
   app.use(vite.middlewares)  // 使用 vite 的 Connect 实例作为中间件
   // 对于所有的请求通过以下方式渲染
   app.get('*', async (req, res) => {
     try {
       // 1. 读取 index.html
       let template = fs.readFileSync('index.html', 'utf-8') // 读取内容
       // 2. 应用 Vite HTML 转换。这将会注入 Vite HMR 客户端,
       //    同时也会从 Vite 插件应用 HTML 转换。
       template = await vite.transformIndexHtml(req.url, template)
       // 3. 加载服务器入口。vite.ssrLoadModule 将自动转换
       //    你的 ESM 源码使之可以在 Node.js 中运行!无需打包
       //    并提供类似 HMR 的根据情况随时失效。
       const { render } = await vite.ssrLoadModule('/src/server-entry.jsx')
       // 4. 渲染应用的 HTML。这假设 entry-server.js 导出的 `render`
       //    函数调用了适当的 SSR 框架 API。
       const html = await render(req.url);
       // 5. 注入渲染后的应用程序 HTML 到模板中。
       const responseHtml = template.replace("<!-- APP_HTML -->", html)
       // 6. 返回渲染后的 HTML。
       res.status(200).set('content-type', 'text/html').end(responseHtml)
     } catch (error) {
       // 如果捕获到了一个错误,让 Vite 来修复该堆栈,这样它就可以映射回
       // 你的实际源码中。
       vite.ssrFixStacktrace(error)
       res.status(500).end(error.message)
     }
   })

   app.listen(5001, () => {
     console.log('http://localhost:5001');
   }); // 监听端口
 });
  1. 生产环境构建

    • 生成一个客户端构建
    • 再生成一个SSR构建, 使其通过import()直接加载, 这样便无需再使用Vite的ssrLoadModule
    • package.json
    {
        "scripts": {
            "dev": "node server",
            "build:client": "vite build --outDir dist/client",
            "build:server": "vite build --outDir dist/server --ssr src/entry-server.js"
        }
    }
    

    Note:使用 --ssr 标志 表明这将会是一个SSR构建, 同时需要指定SSR 的入口

    import express from 'express';
        import fs from 'fs';
    
        const app = express();
    
        const template = fs.readFileSync('dist/client/index.html', 'utf-8')
    
        const isProd = process.env.NODE_ENV === 'production';
        app.get('*', async (req, res) => {
          const { render } = (await import('./dist/server/server-entry.js'))
    
    
          const context = {}
          const html = await render(req.url, context)
    
          if (context.url) {
            res.redirect(301, context.url)
          }
    
          const responseHtml = template.replace('<!-- APP_HTML -->', html)
          res.status(200).set({ 'Content-Type': 'text/html' }).end(responseHtml)
        })
    
        app.listen(4002, () => {
          console.log('http://localhost:4002')
        })
    

    注意:可以通过 process.env.NODE_ENV 条件分支,需要添加一些用于生产环境的特定逻辑

  2. 生产预加载构建

    vite build 支持使用 --ssrManifest 标志, 会生成一份ssr-manifest.json

    "build:client": "vite build --outDir dist/client --ssrManifest" // 生成dist/client/ssr-manifest.json