前端知识体系总结-前端工程化(Vite篇)

实现 Vite 核心功能(自测:Vite 核心功能和运行原理有哪些,由最简讲起,具体是怎么实现的)

Webpack 是先打包好文件再放到 dev server 运行

而 Vite 是先运行 dev server ,之后浏览器请求什么文件就在 dev server 中动态编译后再返回。核心是基于浏览器原生支持的 ES Modules (<script type="module">),当浏览器解析到 import 语句时,会向服务器发送 HTTP 请求,服务器拦截这些请求并实时编译文件与响应。

一、搭建服务返回index.html与编译js文件

1.1 搭建基础开发服务器

我们需要一个能拦截请求的 HTTP 服务器。这里使用 Koa (Vite 内部使用 connect,逻辑类似)。

目录结构:

text 复制代码
mini-vite/
  ├── src/
  │   ├── main.js
  │   └── App.vue
  ├── index.html
  ├── server.js  (我们将编写的代码)
  └── package.json

index.html: 关键在于 type="module",这告诉浏览器直接以 ES 模块方式加载 js。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<body>
  <div id="app"></div>
  <!-- 浏览器会发起 GET /src/main.js 请求 -->
  <script type="module" src="/src/main.js"></script>
</body>
</html>

server.js (第一步:静态文件服务): 浏览器请求 / 返回 HTML,请求 /src/main.js 返回 JS 内容。

js 复制代码
const Koa = require('koa');
const fs = require('fs');
const path = require('path');

const app = new Koa();

app.use(async (ctx) => {
  const url = ctx.request.url;
  
  // 1. 根路径返回 index.html
  if (url === '/') {
    ctx.type = 'text/html';
    ctx.body = fs.readFileSync('./index.html', 'utf-8');
    return;
  }
  
  // 2. JS文件请求处理 (如 /src/main.js)
  if (url.endsWith('.js')) {
    const p = path.join(__dirname, url);
    ctx.type = 'application/javascript';
    ctx.body = fs.readFileSync(p, 'utf-8');
    return;
  }
});

app.listen(3000, () => {
  console.log('Vite dev server running at http://localhost:3000');
});

二、实现第三方库导入处理

2.1 问题描述

src/main.js 中,我们通常这样写:

js 复制代码
import { createApp } from 'vue'; // ❌ 浏览器报错
import App from './App.vue';

浏览器遇到 import ... from 'vue' 时会报错,因为它不知道 'vue' 在哪里。浏览器只认识相对路径 (./, ../) 或绝对路径 (/)。

2.2 解决方案:路径重写

服务器需要在返回 JS 文件内容给浏览器之前 ,把内容里的 'vue' 替换成 '/@modules/vue',给它一个特殊标识。

修改 server.js:

js 复制代码
// 工具函数:把文件流转成字符串
function readStream(stream) {
  return new Promise((resolve, reject) => {
    let data = '';
    stream.on('data', chunk => data += chunk);
    stream.on('end', () => resolve(data));
  });
}

// 路径重写逻辑
function rewriteImport(content) {
  // 正则匹配: from 'vue' -> from '/@modules/vue'
  // s0: 匹配到的完整字符串
  // s1: 捕获组,即包名 'vue'
  return content.replace(/ from ['"](.*)['"]/g, (s0, s1) => {
    // 如果是相对路径 ./ 或 ../ 或 / 开头,不处理
    if (s1.startsWith('.') || s1.startsWith('/')) {
      return s0;
    }
    // 否则加上 /@modules/ 前缀
    return ` from '/@modules/${s1}'`;
  });
}

app.use(async (ctx) => {
  const url = ctx.request.url;

  if (url.endsWith('.js')) {
    const p = path.join(__dirname, url);
    const content = fs.readFileSync(p, 'utf-8');
    ctx.type = 'application/javascript';
    // 返回修改后的内容
    ctx.body = rewriteImport(content); 
    return;
  }
});

经过这一步,浏览器收到的代码变成了:

js 复制代码
import { createApp } from '/@modules/vue'; // ✅ 浏览器会发起新请求
import App from './App.vue';

2.3 获取真实文件路径

当浏览器请求 http://localhost:3000/@modules/vue 时,服务器需要去 node_modules 里找到 vue 的入口文件。

查找步骤:

  1. 找到 node_modules/vue 文件夹。
  2. 读取 package.jsonmodule 字段 (ESM 入口) 或 main 字段。
  3. 读取该入口文件的内容返回。

2.4 server.js 新增逻辑

js 复制代码
app.use(async (ctx) => {
  const url = ctx.request.url;

  // 3. 处理第三方模块请求
  if (url.startsWith('/@modules/')) {
    // 提取模块名,例如 'vue'
    const moduleName = url.replace('/@modules/', '');
    
    // 在 node_modules 中找到该模块文件夹
    const prefix = path.join(__dirname, './node_modules', moduleName);
    
    // 读取 package.json
    const packageJSON = require(path.join(prefix, 'package.json'));
    
    // 获取入口文件路径 (优先使用 module 字段,因为是 ESM)
    const entryPath = path.join(prefix, packageJSON.module);
    
    // 读取文件内容
    const content = fs.readFileSync(entryPath, 'utf-8');
    
    ctx.type = 'application/javascript';
    // 第三方库内部可能也引用了其他库,也需要重写路径
    ctx.body = rewriteImport(content);
    return;
  }
  
  // ... 其他逻辑
});

三、处理 .vue 单文件组件 (SFC)

3.1 浏览器不认识 .vue

浏览器请求 App.vue 时,服务器不能直接返回 Vue 源码,需要把 .vue 编译成 JS。

Vite 使用 vue 官方提供的 @vue/compiler-sfc 进行编译。

3.2 server.js 新增 Vue 处理逻辑

js 复制代码
const compilerSfc = require('@vue/compiler-sfc');

app.use(async (ctx) => {
  const url = ctx.request.url;
  
  // 4. 处理 .vue 文件
 if (ctx.url.endsWith(".vue")) {
	ctx.type = "application/javascript; utf-8";
	const content = fs.readFileSync(path.join(__dirname, ctx.url), "utf-8");
	const { descriptor } = compilerSfc.parse(content);

	// 使用 inlineTemplate 选项,让 compileScript 直接生成包含 render 的完整组件
	const compiled = compilerSfc.compileScript(descriptor, {
		id: ctx.url,
		inlineTemplate: true, // 关键:内联编译模板,setup 直接返回 render 函数
	});

	ctx.body = rewriteImport(compiled.content);
	return;
	}
});

四、Vite 核心功能总结

实现一个简易 Vite 只需要解决三个问题:

  1. 服务器:用 Koa 拦截浏览器发起的文件获取 HTTP 请求并实时编译与返回。
  2. JS 处理 :遇到 import 'vue' 这种裸模块导入,重写路径为 /@modules/vue,并去 node_modules 里找文件返回。
  3. Vue 处理 :遇到 .vue 文件,使用 compiler-sfc 编译。先把 Script 发给浏览器,再让浏览器回头取 Template 的编译结果,最后拼在一起。

这种模式下,开发环境启动速度与项目大小无关,因为只有当你点击了某个页面,浏览器发起了请求,服务器才开始编译那个页面用到的文件。

Vite HMR实现原理(自测:更新一个文件后,wbp和vite分别会经过什么流程进行网页的热更新)

一、先回顾 Webpack 热更新原理

假如你的项目有1000个JS模块,你修改了其中一个文件 src/components/Header.vue

Webpack的处理方式

  1. Wepack Compiler 监听工作区:Webpack监听到文件保存动作。
  2. 重新构建被修改的模块:loader 链转换文件为 JS 可执行代码 -> AST 解析代码并识别 import、export 代码进行依赖图的增加或删除 -> 对新发现的依赖进行递归处理
  3. 打包:生成 Manifest JSON 文件,告诉浏览器这次更新涉及哪些模块;生成 Update Chunk JS 文件,包含被修改那个模块的新代码。
  4. 推送:HMR Server通过WebSocket推送更新通知给浏览器。
  5. 替换:浏览器的 HMR Runtime 请求清单文件,并根据清单文件请求被更新的模块代码,接着找到模块是否有自己的 module.hot.accept ,否则冒泡沿着依赖图向上查找,在 accept 回调中 import 并执行新的JS代码进行视图的更新。

三、Vite HMR 具体工作流程

1. 建立连接

客户端(浏览器)连接 Vite 开发服务器的 WebSocket。

2. 文件修改与通知

当你保存 Header.vue 时:

  1. Vite 文件监听器检测到变化。
  2. 解析该文件导出内容,确定它是Vue组件。
  3. 通过 WebSocket 向客户端发送一段JSON消息。

消息内容示例:

json 复制代码
{
  "type": "update",
  "updates": [
    {
      "type": "js-update",
      "timestamp": 1678888888,
      "path": "/src/components/Header.vue",
      "acceptedPath": "/src/components/Header.vue"
    }
  ]
}

3. 浏览器重新请求

Vite 在浏览器端注入的客户端代码(vite/client)收到消息。它不会像 Webpack 那样去执行一段新推过来的 JS 代码块,而是利用浏览器动态导入功能

具体操作: 浏览器构造一个新的 import URL,带上时间戳以强制让浏览器认为这是一个新文件,从而避开缓存。

js 复制代码
// 浏览器端逻辑模拟
import('/src/components/Header.vue?t=1678888888')
  .then((newModule) => {
    // 获取到新的模块内容,进行替换
  });

4. 模块替换

对于Vue组件,Vite使用了 vue-loader 类似的逻辑(vite-plugin-vue)。

  • 旧的 Header.vue 组件实例还保留在内存中。
  • 新的模块加载后,框架(Vue/React)利用 HMR API 重新渲染该组件,保留组件内的 data/state 状态,仅更新 render 函数或样式。

四、Vite HMR API:import.meta.hot

Webpack使用 module.hot,而 Vite 使用 ESM 标准的 import.meta.hot

开发者的代码(通常由插件自动注入):

js 复制代码
// src/components/Header.vue 编译后的JS代码
// ... 组件代码 ...

export default _sfc_main;

// HMR 逻辑
if (import.meta.hot) {
  // 接受自身更新
  import.meta.hot.accept((newModule) => {
    if (newModule) {
      // 执行组件重渲染逻辑
      __VUE_HMR_RUNTIME__.reload('组件HashID', newModule.default);
    }
  });
}

实现逻辑:

  1. import.meta.hot.accept:告诉 Vite,如果这个文件变了,不需要刷新页面,我自己能处理。
  2. 回调函数 :当新文件被 import(...) 加载成功后,执行这个回调,传入新模块内容。

五、所以为什么 Vite HMR 速度快

  1. 无需重构依赖图:文件保存后无需重新分析依赖图的更改,本质是因为 Vite 不需要构建依赖图去生成 bundle,而是通过浏览器 ESM 能力提供所需文件即可。
  2. 无需打包:Vite 只需编译一次文件,而 Webpack 需要将受影响的模块及其相关依赖(修改模块本身、父节点可能更新对子模块的Module ID引用代码、所属Chunk)重新打包与合并,涉及 n 个文件的修改。
  3. 全量代码下发:Webpack 下发包含新代码的 HMR 更新包,而 Vite 只发送一个指向修改该文件的 HTTP 请求,由浏览器重新请求。

Vite Plugin 实现原理与实战(自测:实现一个vite-plugin-svg-icons)

在前文中,我们了解了 Webpack 的打包流程:读取入口 -> 分析 AST -> 递归依赖 -> 转换代码 -> 生成 Bundle

Vite 的工作方式完全不同。在开发环境下,Vite 不打包 。它利用浏览器对 ES Modules 的原生支持。当浏览器发起请求(如 GET /src/main.js)时,Vite 服务器拦截请求,进行必要的代码转换,然后直接返回 JS 内容。

Vite 插件 就是用来拦截处理这些请求的工具。

Vite 插件基于 Rollup 的插件接口设计,同时扩展了一些 Vite 独有的钩子(Hooks)。

一、Vite 插件的核心钩子 (Hooks)

Webpack 将功能分为 Loader(转换文件)和 Plugin(监听构建生命周期)。Vite 将这两者合并了。一个 Vite 插件本质上是一个返回配置对象的函数

处理一个文件请求时,主要经过以下三个核心钩子:

  1. resolveId(source, importer) : 找文件
    • 输入 : 代码中的导入路径(如 import x from './a' 中的 './a')。
    • 作用: 告诉 Vite 这个文件的绝对路径在哪里,或者标记这是一个"虚拟模块"。
    • 返回: 文件的绝对路径或 ID。
  2. load(id) : 读文件
    • 输入 : resolveId 返回的绝对路径或 ID。
    • 作用: 读取文件内容。通常用于加载磁盘文件或生成虚拟文件内容。
    • 返回: 文件内容的字符串。
  3. transform(code, id) : 改代码 (相当于 Webpack Loader)
    • 输入 : load 返回的代码字符串,以及文件 ID。
    • 作用: 将非 JS 代码(如 Vue, CSS, TS)转换为浏览器能识别的 JS 代码。
    • 返回: 转换后的 JS 代码。

二、实战:实现一个虚拟模块插件

场景:你需要在一个项目中引入一个并不存在于磁盘上的文件,比如构建时的环境变量信息。

目标代码

javascript 复制代码
// main.js
import env from 'virtual:env'; // 这个文件在磁盘上不存在
console.log(env); 

插件实现

javascript 复制代码
export default function myVirtualPlugin() {
  const virtualModuleId = 'virtual:env';
  const resolvedVirtualModuleId = '\0' + virtualModuleId; // \0 是 Rollup 的约定,表示这是一个虚拟模块,不要去磁盘找

  return {
    name: 'my-virtual-plugin', // 插件名称,必填

    // 1. 拦截 import
    resolveId(source) {
      if (source === virtualModuleId) {
        // 如果 import 的是 'virtual:env',返回我们自定义的 ID
        return resolvedVirtualModuleId;
      }
      return null; // 其他文件不管,交给 Vite 处理
    },

    // 2. 加载内容
    load(id) {
      if (id === resolvedVirtualModuleId) {
        // 匹配到自定义 ID,直接返回一段 JS 代码
        return `export default { 
            user: "admin", 
            buildTime: "${new Date().toISOString()}" 
        }`;
      }
      return null; // 其他文件不管,读取磁盘
    }
  };
}

配置 vite.config.js:

javascript 复制代码
import myVirtualPlugin from './plugins/myVirtualPlugin';

export default {
  plugins: [myVirtualPlugin()]
};

三、实战:实现一个 vite-plugin-svg-icons

首先明确插件功能:扫描 指定目录下的 SVG 文件 -> 转换<symbol> 标签并合并 -> 提供 虚拟模块 virtual:svg-register import 注入页面 -> 支持 HMR 热更新。

为了更好理解插件功能,我们看看在实际场景中它的作用:

你正在开发一个企业级后台管理系统,设计师提供了一套自定义 SVG 图标(如 nav-order.svg, action-edit.svg),要求图标颜色能随文字颜色变化(如菜单 Hover 时变蓝),且会有数十个图标散落在各个页面。

使用img标签,第一个是无法改变颜色需要重新提供另一版本svg,并且还需要根据hover事件动态切换src,非常麻烦;使用内联svg代码,代码很臃肿,可读性差;使用手动import,若一个页面需要的svg很多,会产生大量import语句

我们的插件目标:

  1. 零配置引用 :只需将 SVG 文件丢入 src/icons 文件夹,无需任何 import 语句,直接通过文件名即可使用。
  2. CSS 样式控制 :插件生成的 SVG Sprite 支持 currentColor,图标就像文字一样,可以用 CSS 随意控制颜色大小
  3. 高性能 :所有图标被合并成一段 JS 注入 HTML,零 HTTP 请求,且按需加载。

使用效果演示:

js 复制代码
// main.ts
import 'virtual:svg-register' // 一行代码,所有图标自动打包注入
html 复制代码
<!-- 无需 import,直接使用 -->
<svg class="icon" aria-hidden="true">
  <use xlink:href="#icon-nav-order" />
</svg>

<style>
.icon {
  color: grey;       /* 默认灰色 */
  font-size: 20px;   /* 控制大小 */
}
.icon:hover {
  color: blue;       /* 悬停自动变蓝,无需 JS */
}
</style>

可以封装为一个组件:

html 复制代码
<!-- src/components/SvgIcon.vue -->
<template>
  <svg class="svg-icon" aria-hidden="true">
    <use :xlink:href="symbolId" />
  </svg>
</template>

<script setup lang="ts">
import { computed } from 'vue'

const props = defineProps({
  name: { type: String, required: true }, // 传入图标文件名,如 'truck'
  prefix: { type: String, default: 'icon' }
})

const symbolId = computed(() => `#${props.prefix}-${props.name}`)
</script>

<style scoped>
.svg-icon {
  width: 1em; height: 1em; /* 默认跟随字体大小 */
  vertical-align: -0.15em;
  fill: currentColor; /* 关键:让图标颜色跟随文字颜色 */
  overflow: hidden;
}
</style>

现在我们来实现这个插件功能

首先理解"虚拟模块"

你可以在浏览器端 import 一个不存在于文件系统中的文件

目标:用户在代码里写 import 'virtual:svg-register',插件可以正确识别和拦截。

具体实现:使用 resolvedId 属性进行配置,当文件路径是我们的虚拟模块时,直接返回不需要解析,并且在 load 阶段返回我们自定义的代码交给程序执行

第二步:实战代码编写

新建一个 my-svg-plugin.js

我们可以安装一个依赖来方便找文件:npm install fast-glob

javascript 复制代码
// my-svg-plugin.js
import path from 'path'
import fs from 'fs'
import fg from 'fast-glob'

export default function mySvgPlugin(options) {
  // 1. 配置虚拟模块 ID
  const VIRTUAL_MODULE_ID = 'virtual:svg-register'
  const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID

  return {
    name: 'vite-plugin-my-svg-sprite', // 插件名称

    // 2. resolveId: 告诉 Vite 这个 import 归我管
    resolveId(id) {
      if (id === VIRTUAL_MODULE_ID) {
        return RESOLVED_VIRTUAL_MODULE_ID
      }
    },

    // 3. load: 返回这个虚拟模块的具体代码
    async load(id) {
      if (id === RESOLVED_VIRTUAL_MODULE_ID) {
        
        // --- 核心逻辑开始 ---
        
        // A. 找到所有 SVG 文件
        const { iconDir } = options
        const svgFiles = await fg('**/*.svg', { cwd: iconDir, absolute: true })

        // B. 遍历并读取内容,拼接成 Symbol 字符串
        let symbols = ''
        
        svgFiles.forEach((file) => {
	         if (file.endsWith(".svg")) {
		 const content = fs.readFileSync(path.join(iconDir, file), "utf-8");
		 const viewBox = content.match(/viewBox="([^"]+)"/)?.[1] || "0 0 24 24";
		 const pathContent = content.match(/<svg[^>]*>(.*)<\/svg>/s)?.[1] || "";
		 const iconName = file.replace(".svg", "");

		 symbols += `<symbol id="icon-${iconName}" viewBox="${viewBox}">${pathContent}</symbol>`;
		}
	});

        // C. 构造最终的 JS 代码
        // 返回在页面中注入 SVG sprite 的代码
		return `		
                const svgSprite = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
		     svgSprite.style.position = 'absolute';
		     svgSprite.style.width = '0';
		     svgSprite.style.height = '0';
		     svgSprite.innerHTML = \`${symbols}\`;
		     document.body.insertBefore(svgSprite, document.body.firstChild);
			`;
        // --- 核心逻辑结束 ---
      }
    }
  }
}
第三步:在项目中使用 (验证效果)
  1. 配置 vite.config.js:

    javascript 复制代码
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import path from 'path'
    import mySvgPlugin from './my-svg-plugin' // 引入你写的插件
    
    export default defineConfig({
      plugins: [
        vue(),
        mySvgPlugin({ 
            iconDir: path.resolve(__dirname, 'src/icons') // 假设你的图标都在这里
        })
      ]
    })
  2. 准备素材 : 在 src/icons 下放几个 svg 文件,比如 vue.svg, react.svg

  3. 引入注册 : 在 src/main.js (或 main.ts) 中引入虚拟模块:

    javascript 复制代码
    import { createApp } from 'vue'
    import App from './App.vue'
    
    // 这一行会触发你插件的 resolveId -> load,
    // 然后在浏览器执行那段插入 DOM 的 JS 代码
    import 'virtual:svg-register' 
    
    createApp(App).mount('#app')
  4. 组件使用: 在 Vue 组件里写:

    html 复制代码
    <template>
      <div>
        <!-- 使用图标 -->
        <svg style="width: 50px; height: 50px; fill: red;">
          <use xlink:href="#icon-vue"></use>
        </svg>
        <svg style="width: 50px; height: 50px; fill: blue;">
          <use xlink:href="#icon-react"></use>
        </svg>
      </div>
    </template>

四、Vite 独有的钩子:configureServer

Vite 插件不仅仅是构建工具,还是一个开发服务器。configureServer 钩子允许我们在 Vite 的 Node.js 服务器(基于 connect 库)中添加中间件。这在 Webpack Plugin 中很难直接做到。

场景 :实现一个简易的 API Mock 功能。当请求 /api/user 时,拦截请求并返回假数据,不经过后端。

插件实现

javascript 复制代码
export default function myMockPlugin() {
  return {
    name: 'my-mock-plugin',

    configureServer(server) {
      // server 是 Vite 开发服务器实例
      // server.middlewares 是一个 connect 实例,用法类似 Express
      
      server.middlewares.use((req, res, next) => {
        // 拦截 /api/user 请求
        if (req.url === '/api/user') {
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ id: 1, name: 'Mock User' }));
          return; // 结束请求
        }
        
        // 其他请求放行
        next();
      });
    }
  };
}

五、Vite 的热更新 (HMR) 钩子:handleHotUpdate

在 Webpack 中实现 HMR 需要修改打包逻辑。在 Vite 中,插件可以直接介入 HMR 流程。

场景 :当 .txt 文件修改时,不刷新页面,只通过自定义事件通知浏览器更新。

插件实现 (服务端)

javascript 复制代码
export default function myHmrPlugin() {
  return {
    name: 'my-hmr-plugin',

    handleHotUpdate({ file, server, modules }) {
      if (file.endsWith('.txt')) {
        // 1. 读取更新后的文件内容
        const content = require('fs').readFileSync(file, 'utf-8');

        // 2. 向浏览器发送自定义 Websocket 消息
        server.ws.send({
          type: 'custom',
          event: 'txt-update',
          data: { file, content } // 发送新内容
        });

        // 3. 返回空数组,告诉 Vite:这个文件我处理了,你不需要执行默认的 HMR 逻辑(默认逻辑通常是重新加载模块)
        return [];
      }
    }
  };
}

客户端代码 (Client)

javascript 复制代码
// 在 main.js 中接收消息
if (import.meta.hot) {
  import.meta.hot.on('txt-update', (data) => {
    console.log(`文件 ${data.file} 变了,新内容是: ${data.content}`);
    // 在这里手动更新 DOM
    document.querySelector('#app').innerText = data.content;
  });
}

六、总结:Webpack vs Vite 插件开发对比

功能点 Webpack 实现方式 Vite 实现方式
引入非 JS 文件 Loader (如 css-loader) Plugintransform 钩子
寻找模块路径 resolve.alias 配置或 Resolver 插件 PluginresolveId 钩子
读取文件内容 Loader 读取 Pluginload 钩子
开发服务器拦截 devServer.before 配置 PluginconfigureServer 钩子
热更新控制 注入 Runtime 代码,较复杂 PluginhandleHotUpdate + import.meta.hot

开发思维转变:

  • Webpack 插件像是在一条已经铺好的流水线(Compiler Hooks)上安装传感器和机械臂。
  • Vite 插件更像是拦截器。浏览器请求文件 -> 你的插件拦截 -> 告诉你 ID -> 你给它内容 -> 你转换内容 -> 返回给浏览器。
相关推荐
Neon12041 小时前
WKWebView 中 iframe 无法监听原生 JSBridge 回调的完整分析
前端
用户8168694747251 小时前
Chrome 插件开发入门
前端
_Eleven1 小时前
前端布局指南
前端·css
一枚前端小姐姐1 小时前
Vue3 + Vite 从零搭建项目,超详细入门指南
前端·vue.js
小李独爱秋2 小时前
模拟面试:简述一下MySQL数据库的备份方式。
数据库·mysql·面试·职场和发展·数据备份
一只叫煤球的猫2 小时前
别再把 Lambda 当匿名类:这 9 类坑你一定踩过
java·后端·面试
赵_叶紫2 小时前
Docker 从入门到部署实战
前端
PD我是你的真爱粉2 小时前
Vue 3 生命周期完全指南:从流程图到最佳实践
前端·vue.js·流程图
耀耀切克闹灬2 小时前
前端签章数据的模板处理
前端