手写 Rollup 插件实现解析 Vue 文件

前言

通过上篇文章我们知道 Vue 文件一般由 template、script、style 三部分组成,编译的时候在读取了 Vue 文件的内容之后会通过 @vue/compiler-sfc 把上述 Vue 文件的三部分内容解析出来,然后再去解析各自部分的内容,比如,template 部分会通过模板编译器编译成 render 函数,script 部分会判断是否是 script setup 类型,因为 script setup 类型还需要编译 defineProps、defineEmits、defineModel 等语法糖代码,即对所谓编译宏的部分进行编译,以及将 TS 编译成 JavaScript,style 部分则需要编译 sass、less、stylus 的 CSS 代码。处理完成后,插件将结果传递给 Rollup 或 Vite 进行后续的打包或开发服务器启动等操作。

本文将详细讲解 Rollup 插件是如何编译 vue 文件的。

准备配置文件

首先我们在 rollup-plugin 目录下来创建一个 App.vue 文件,内容如下:

html 复制代码
<template>
    <div>i am Cobyte ~</div>
</template>
<script setup>
    console.log('我是掘金签约作者:Cobyte')
</script>
<style>
div {
    color: red;
}
</style>

我们现在的目标就是把上述的 App.vue 文件的代码通过 Rollup 插件编译成 JavaScript 代码。接着我们创建一个 Rollup 插件,根据我们前一篇文件所学的 Rollup 插件知识,一个基础的 Rollup 插件结构如下:

javascript 复制代码
// rollup-plugin-vue.js
export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue'
    }
}

接着我们修改 rollup.config.js 配置文件:

javascript 复制代码
import pluginVue from './rollup-plugin-vue.js'

export default [
  {
    input: './App.vue',
    output: {
      file: 'dist/output.js',
      format: 'esm',
    },
    plugins: [
      pluginVue()
    ],
  },
]

那么根据我们前一篇文章所学到的知识,可以知道 Rollup 插件对本地相对目录的文件是所有的钩子函数都会执行的,所以我们可以直接在 transform 钩子中直接编译 App.vue 文件,同时因为 Rollup 插件对本地目录的文件都会进行访问,所以我们需要判断一下只有 Vue 文件的时候才进行处理,Rollup 插件修改如下:

javascript 复制代码
import compiler from '@vue/compiler-sfc'
export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue',
        transform(code, id) {  
            // 因为 Rollup 插件对本地目录的文件都会进行访问,所以我们要判断一下只有 vue 文件的时候才进行处理
            if (id.endsWidth('.vue')) {
                const result = compiler.parse(code);
                console.log(result)
            }
        }
    }
}

同时我们需要安装 @vue/compiler-sfc

sql 复制代码
pnpm add @vue/compiler-sfc -D

Vue 文件的初步编译

接下来我们先进行调试一下,看一下经过 @vue/compiler-sfc 的 parse 方法编译结果是什么。

我们先打个断点:

然后启动调试模式运行命令:

接着我们进到 rollup-plugin 目录运行 pnpm build,最终可以看到 @vue/compiler-sfc 的 parse 方法编译结果如下:

我们可以看到 @vue/compiler-sfc 的 parse 方法把一个 Vue 文件分成了:

  • template 模块
  • script 模块
  • scriptSetup 模块 (script setup 模式)
  • styles 模块(是一个数组,因为可能有多个)

我们知道 Vue 文件一般由 template、script、style 三部分组成,@vue/compiler-sfc 的 parse 方法先把 Vue 文件进行拆分,处理成方便我们进行下一步编译的数据结构。这一步并没有对代码进行编译,可以看到每个模块中的 content 字段的内容还是原来 Vue 文件的内容。

编译 script 模块

我们知道 script setup 类型还需要编译 defineProps、defineEmits、defineModel 等语法糖代码,同时还是需要处理 CSS 变量注入等,所以我们需要编译 script 部分的代码,而这个能力 @vue/compiler-sfc 也提供了 compileScript 方法进行处理。compileScript 方法的使用如下:

diff 复制代码
import compiler from '@vue/compiler-sfc'
export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue',
        transform(code, id) {  
            if (id.endsWith('.vue')) {
                const result = compiler.parse(code);
+                // 创建组件的唯一标识符,用于样式作用域等
+                const scopeId = `${Math.random().toString(36).substr(2, 9)}`;
+                const scriptCode = compiler.compileScript(result.descriptor, { id: scopeId });
+                console.log(scriptCode)
            }
        }
    }
}

打印出来的结果如下:

也就是 content 字段中的内容就是编译后的 script 部分的代码,我们整理一下如下:

javascript 复制代码
export default {
    setup(__props, { expose: __expose }) {
         __expose();
        console.log('我是掘金签约作者:Cobyte')
        const __returned__ = {  }
        Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
        return __returned__
    }
}

接下来我们编译 template 部分的代码。

编译 template 模块

编译编译 template 部分的代码也很简单,只需要使用 compileTemplate 方法进行以下操作:

diff 复制代码
import compiler from '@vue/compiler-sfc'
export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue',
        transform(code, id) {  
            if (id.endsWith('.vue')) {
                const result = compiler.parse(code);
                // 创建组件的唯一标识符,用于样式作用域等
                const scopeId = `${Math.random().toString(36).substr(2, 9)}`;
                const scriptCode = compiler.compileScript(result.descriptor, { id: scopeId });
+                const templateCode = compiler.compileTemplate({ source: result.descriptor.template.content, id: scopeId })
+                console.log(templateCode)
            }
        }
    }
}

编译结果如下:

也就是 code 字段中的内容就是编译后的 template 部分的代码,也就是 render 函数,我们整理一下如下:

javascript 复制代码
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache) {
   return (_openBlock(), _createElementBlock("div", null, "i am Cobyte ~"))
}

那么我们现在得到了 script 、template 部分的编译后的代码,我们怎么整合它们呢?通过上一篇的文章我们知道可以通过虚拟模块的方式进行整合。

组装 script 和 render 函数

我们上面提到了可以通过虚拟模块的技术整合 script 、template 部分的编译后的代码,上一篇中代码是这样的:

现在我们依然可以这样:

diff 复制代码
import compiler from '@vue/compiler-sfc'
export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue',
        transform(code, id) {  
            if (id.endsWith('.vue')) {
                const result = compiler.parse(code);
                // 创建组件的唯一标识符,用于样式作用域等
                const scopeId = `${Math.random().toString(36).substr(2, 9)}`;
                const scriptCode = compiler.compileScript(result.descriptor, { id: scopeId });
                const templateCode = compiler.compileTemplate({ source: result.descriptor.template.content, id: scopeId })
+                // 脚本导入的虚拟模块
+                const scriptImport = `import script from '${id}?vue&type=script'\n`
+                // 模板导入的虚拟模块
+                const templateImport = `import { render } from '${id}?vue&type=template'\n`

+                // 组装
+                const renderReplace = `script.render = render\n`
+                const exportDefault = `export default script`
+                return {
+                    code: `
+                        ${scriptImport}
+                        ${templateImport}
+                        ${renderReplace}
+                        ${exportDefault}
+                    `
+                }
            }
        }
    }
}

通过我们前面对 Rollup 插件的了解,我们知道经过 transform 钩子处理过的代码如果还存在 import 语句那么还会继续调用 resolveId 钩子处理模块 ID,而且只对第三方模块的 ID 进行处理。而我们上述构建的模块地址是这样的:import script from '${id}?vue&type=script'\n,其中 id 是一个绝对路径地址,例如:'D:\workspace\github\mini-vue-vapor\rollup-plugin\App.vue',最终会变成:'D:\workspace\github\mini-vue-vapor\rollup-plugin\App.vue?vue&type=script'。那么这个导入地址很明显不是一个本地的相对路径,所以它将被当作一个第三方库的路径进行处理。所以我们需要在 resolveId 钩子处理在模块路径,让我们写的 Rollup 插件后续可以继续处理该文件。

我们的插件改动如下:

diff 复制代码
import compiler from '@vue/compiler-sfc'
+ import qs from 'querystring'
+ // 判断否是一个 Vue 单文件组件的请求
+ function parseVuePartRequest(id) {
+    const [filename, query] = id.split('?', 2)

+    if (!query) return { vue: false, filename }
+    // 使用 qs.parse 方法解析查询字符串,将其转换成一个对象  
+    const raw = qs.parse(query)
+    // 如果解析后的对象中包含 vue 键,说明这是一个 Vue 组件请求  
+    if ('vue' in raw) {
+      return {
+        ...raw,
+        filename,
+        vue: true
+      }
+    }
+    // 如果不包含 vue 键,返回表示这不是 Vue 组件请求的对象  
+    return { vue: false, filename }
+ }

export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue',
+        async resolveId(id) {
+            // 调用 parseVuePartRequest 函数解析模块 ID
+            const query = parseVuePartRequest(id);
+            // 解析结果如果 query.vue 为 true 表示这是一个 Vue 组件请求,则返回原始的模块 ID,这样该插件后续就会继续处理这个模块
+            if (query.vue) {
+              return id
+            }
+            // 如果不是 Vue 组件请求,返回 null,表示这个模块 ID 不应该由这个插件处理
+            return null
+        },
        // 省略...
    }
}

我们通过封装一个 parseVuePartRequest 函数来判断是否 Vue 组件请求,主要判断依据就是文件ID的 ? 后面部分的参数中是否存在 vue 键,这就是我们上述构建虚拟模块的时候为什么要设置 ?vue&type=script 部分的原因,就是为了后面可以进行区分。

同时我们需要安装 querystring 依赖模块: pnpm add querystring -D

这样我们再次运行测试的时候,结果如下:

当代码可以走到这一步的时候,说明我们的代码是没有问题的。

根据我们在前一篇文章中所学到的知识可以知道当在 resolveId 钩子中返回了一个虚拟模块的ID之后,那么接下来就轮到 load 钩子进行处理了,那么还是根据前面所学的知识知道可以在 load 钩子中返回虚拟模块的代码,所以我们就可以在 load 钩子中根据模块ID 中的 type 类型来判断是返回 script 部分的代码还是 template 部份的代码。

插件代码改动如下:

diff 复制代码
// 省略...
export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue',
        // 省略...
+        load(id) {
+            // 调用 parseVuePartRequest 函数解析模块 ID
+            const query = parseVuePartRequest(id);
+            // 解析结果如果 query.vue 为 true 表示这是一个 Vue 组件请求,则返回原始的模块 ID,这样该插件后续就会继续处理这个模块
+            if (query.vue) {
+              if (query.type === 'script') {
+                // 返回 script 部分的代码
+              } else if (query.type === 'template') {
+                // 返回 template 部分的代码
+              }
+            }
+        },
    // 省略...
    }
}

那么问题来了,我们在 load 钩子中返回 script 和 template 部分的代码呢?通过上文我们知道 script 和 template 部分的代码已经在 transform 钩子中进行了编译,怎么可以在 load 钩子中将 transform 钩子中编译好的代码获取到呢?我们可以在 transform 钩子中将 script 和 template 部分的编译结果缓存起来,然后在 load 钩子中就可以获取缓存中对应的编译结果了。

设置编译缓存

我们通过以下方式进行设置 script 和 template 部分的缓存。代码如下:

diff 复制代码
+ const cache = new Map();
// 省略...
export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue',
        // 省略...
        load(id) {
            // 调用 parseVuePartRequest 函数解析模块 ID
            const query = parseVuePartRequest(id);
            // 解析结果如果 query.vue 为 true 表示这是一个 Vue 组件请求,则返回原始的模块 ID,这样该插件后续就会继续处理这个模块
            if (query.vue) {
              if (query.type === 'script') {
                // 返回 script 部分的代码
+                const code = cache.get(id).content
+                return {
+                  code
+                }
              } else if (query.type === 'template') {
                // 返回 template 部分的代码
+                const code = cache.get(id).code
+                return {
+                  code
+                }
              }
            }
        },
        transform(code, id) {  
            if (/\.vue$/.test(id)) {
                // 省略 ...
-                const scriptImport = `import script from '${id}?vue&type=script'\n`
+                const scriptID = `${id}?vue&type=script`;
+                const scriptImport = `import script from '${scriptID}'\n`
-                const templateImport = `import { render } from '${id}?vue&type=template'\n`
+                const templateID = `${id}?vue&type=template`;
+                const templateImport = `import { render } from '${templateID}'\n`
+                // 缓存编译结果
+                cache.set(scriptID, scriptCode)
+                cache.set(templateID, templateCode)
                // 组装
                const renderReplace = `script.render = render\n`
                const exportDefault = `export default script`
                return {
                    // 省略...
                }
            }
        }
    }
}

在上述代码中我们通过 Map 对象的实例 cache 来缓存编译后的结果,这个缓存是以我们自己构建的虚拟模块 ID 为键,然后设置对应的 script 或 template 部分的编译结果为键值。

我们运行上述代码的时候却报错了:

这个报错是说我们在读取缓存结果的时候读取不到,那么我们在相关的地方打断点进行测试,最后发现了以下问题。

我们初始设置缓存的键是下面这样的:

在 load 钩子中却变成了:

我们发现路径没有被正确地转义。这是 Rollup 内部处理的,我们可以通过 JSON.stringify 确保任何特殊字符(如路径中的空格、查询参数中的特殊符号等)都被正确地转义,从而生成一个合法的模块请求路径。所以我们修改如下:

diff 复制代码
// 省略...
export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue',
        // 省略...
        transform(code, id) {  
            if (/\.vue$/.test(id)) {
                // 省略...
                // 脚本导入的虚拟模块
                const scriptID = `${id}?vue&type=script`;
+                const scriptRequest = JSON.stringify(scriptID);
-                const scriptImport = `import script from '${scriptID}'\n`
+                const scriptImport = `import script from ${scriptRequest}\n`
                // 模板导入的虚拟模块
                const templateID = `${id}?vue&type=template`;
+                const templateRequest = JSON.stringify(templateID);
-                const templateImport = `import { render } from '${templateID}'\n`
+                const templateImport = `import { render } from ${templateRequest}\n`
                // 缓存编译结果
                cache.set(scriptID, scriptCode)
                cache.set(templateID, templateCode)
                // 省略...
                return {
                  // 省略...
                }
            }     
        }
    }
}

我们再次运行代码,发现成功了。

编译结果如下:

运行编译结果代码

在前面一小节,我们实现了对 Vue 文件的编译,因为我们的打包输出格式是 ESM 的,所以我们可以通过以下的方式进行测试。

我们回到根目录的 src/main.js 中修改测试代码如下:

javascript 复制代码
import { createApp } from 'vue'
import App from '../rollup-plugin/dist/output'

const app = createApp(App)
app.mount('#app')

接着修改根目录的 vite.config.js 的文件内容:

javascript 复制代码
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    Vue(),
  ]
})

如果你还没要安装 Vue3 的依赖包,那么先安装一下 Vue3 的依赖包。

我们在根目录启动 Vite 开发模式,最后我们可以看到 Vite 项目正常启动了。结果如下:

在浏览器的网络窗口,我们也可以看到打包的 Vue 文件也被加载了,结果如下:

日志打印窗口也打印了组件中 setup 方法的日志输出:

编译 style 模块

对于 style 模块,跟 script 和 template 模块一样,需要通过 @vue/compiler-sfc 包将 Vue 本身赋予 style 模块的能力代码进行编译,比如是否使用了 scoped 属性,然后需要对 scoped 属性的处理,总的来说需要处理以下的问题:

  • 如果样式使用了 scoped 属性的,编译器会生成唯一的 CSS 类名,确保样式不会影响其他组件;
  • 如果 style 中使用了v-bind 指令来绑定 script 模块中的响应式变量,那么需要将 v-bind 指令转换为 CSS 变量;
  • 同时使用了 scoped 样式无法穿透到子组件,那么需要使用 :deep() 指令,所以当遇到 :deep() 指令时,编译器会特殊处理这些选择器,确保它们能够应用到子组件的 DOM 结构中。
  • 如果使用了 CSS 模块化 (module),也需要进行编译处理

经过 @vue/compiler-sfc 包编译之后,可能还需要通过 sass、less、stylus 编译器处理成普通的 CSS 代码。这样处理后 Vue 文件中 style 部分的样式就会被转换成标准的 CSS,并插入到最终打包后的 JavaScript 代码中,或者在单独的 CSS文件中引用。

而我们这里暂时只讲插入打包后的 JavaScript 代码中的情况,那么怎么将编译后的普通 CSS 代码插入打包后的 JavaScript 代码中呢?

比如我们上述的 App.vue 文件的 style 部分的 CSS 代码最终会编译成:

css 复制代码
div {
    color: red;
}

很明显这段代码是字符串,是不能直接放到 JavaScript 文件中的,那么我们可以通过一个变量把它存起来,这样它就可以放到 JavaScript 文件中了。

javascript 复制代码
const cssBlock = `
div {
    color: red;
}
`

那么我们再通过 JavaScript 代码动态地添加到HTML页面上即可,实现也很简单,直接用 createElement 创建 style 标签元素,然后插入到页面 head 元素内即可,代码如下:

javascript 复制代码
const cssBlock = `div {color: red;}`
function styleInject(css) {
    // 获取页面 head 标签元素
    const head = document.getElementsByTagName('head')[0]
    // 创建 style 元素
    const style = document.createElement('style')
    style.type = 'text/css'
    // 插入到页面 `head` 元素内
    head.appendChild(style)
    // 添加 style 元素的 CSS 内容
    style.appendChild(document.createTextNode(css))
}

styleInject(cssBlock)

这个就是实现编译后的 CSS 代码添加到页面上的实现原理。接下来我们来进行具体的实现。

跟 script 和 template 模块一样,style 部分我们也需要通过虚拟模块来处理,而 style 部分可能存在多个,所以我们的代码实现如下:

diff 复制代码
export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue',
        // 省略...
        transform(code, id) {  
          if (/\.vue$/.test(id)) {
              // 省略...
+              // 编译 style 模块
+              let stylesImport = ``
+              if (result.descriptor.styles.length) {
+                result.descriptor.styles.forEach((style, i) => {
+                  // 样式导入的虚拟模块
+                  const query = `?vue&type=style&index=${i}&lang.css`
+                  const styleRequest = id + query
+                  stylesImport += `\nimport ${JSON.stringify(styleRequest)}`
+                  // 编译 style 模块
+                  const styleCode = compiler.compileStyle({
+                    id: `data-v-${scopeId}`,
+                    source: style.content,
+                    scoped: style.scoped // 是否使用 scoped
+                  })
+                  // 缓存 style 模块
+                  cache.set(styleRequest, styleCode)
+                })
+              }
              // 省略...
              return {
                code: `

+                    ${stylesImport}
                `
              }
          }
        }
    }
}

值得注意的是我们在加载 style 文件路径参数后面加了 lang.css 的部分,以此来区分是哪种 style 预处理语言,如果是 scss 那么就以 lang.scss 结尾,以此类推,因为我们在 App.vue 文件中是只使用了普通的 CSS,所以我们是以 .css 结尾。

接着我们需要在 load 钩子里从缓存中加载 style 部分的代码,代码实现如下:

diff 复制代码
export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue',
        // 省略...
        load(id) {
            // 调用 parseVuePartRequest 函数解析模块 ID
            const query = parseVuePartRequest(id);
            // 解析结果如果 query.vue 为 true 表示这是一个 Vue 组件请求,则返回原始的模块 ID,这样该插件后续就会继续处理这个模块
            if (query.vue) {
              if (query.type === 'script') {
                // 省略...
+              } else if (query.type === 'style') {
+                  // 返回 style 部分的代码
+                  const code = cache.get(id).code
+                  return {
+                    code,
+                  }
+              }
            }
        },
        // 省略...
    }
}

我们知道目前在 load 中加载的 CSS 代码是不能直接放到 JavaScript 文件中的,所以我们需要在 transform 中进行处理,当然也可以在 load 中处理好再进行返回,我们现在的处理方案是放到 transform 中进行处理,而方案就是我们在上面已经提到的动态添加 CSS 代码到页面的方案。

diff 复制代码
export default function pluginVue() {
    return {
        name: 'rollup-plugin-vue',
        // 省略...
        transform(code, id) {  
          if (/\.vue$/.test(id)) {
            // 省略...
+          } else if (/\.css$/.test(id))  {
+            const styleCode = `
+ const cssBlock = '${JSON.stringify(code)}';\n
+ function styleInject(css) {\n
+  const head = document.getElementsByTagName('head')[0]\n
+  const style = document.createElement('style')\n
+  style.type = 'text/css'\n
+  head.appendChild(style)\n
+  style.appendChild(document.createTextNode(css))\n
+ }\n
+ styleInject(cssBlock)\n
+            `
+            return {
+              code: styleCode
+            }
+          }
        }
    }
}

这时我们在 ./rollup-plugin/ 目录下再执行 pnpm build,我们就可以看到 ./rollup-plugin/dist/output.js 文件,已经把我们的 CSS 代码编译进去了。

这时我们再回到根目录运行我们 ./src/main.js 中的代码,我们发现我们 CSS 代码已经成功运行了:

至此我们的 Vue 文件中的 CSS 代码也实现了编译。

总结

通过本文的逐步推导,我们完整实现了一个 Rollup 插件对 Vue 单文件组件的编译流程。从插件的基础结构出发,我们利用 @vue/compiler-sfc 提供的 parsecompileScriptcompileTemplatecompileStyle 等核心 API,将 Vue 文件中的 template、script 和 style 三部分分别提取并编译成独立的中间产物。其中 template 被编译为 render 函数,script(尤其是 <script setup>)中的编译宏和 TypeScript 被转换为标准 JavaScript,style 则经过作用域处理(scoped)和 CSS 变量绑定(v-bind)后生成普通 CSS 字符串。这个分步编译的过程,不仅展示了 Vue 官方编译工具链的威力,也揭示了现代前端构建工具处理组件化框架时的底层逻辑。

在整个实现中,虚拟模块技术和缓存机制的巧妙结合是保证 Rollup 插件正确工作的关键。我们为每个编译好的部分(script、template、style)构造了带有查询参数的模块 ID(如 App.vue?vue&type=script),并在 resolveIdload 钩子中拦截这些请求,返回缓存中的编译结果。这种设计让 Rollup 能够像处理普通 JavaScript 模块一样处理 Vue 的不同组成部分,同时避免了重复编译,提升了构建性能。特别是对于 style 模块,我们通过注入运行时的 styleInject 函数,将 CSS 字符串动态添加到页面中,实现了样式模块的按需加载,这一策略在开发环境和生产打包中都具有实用价值。

最终,我们成功将编译后的代码整合成一个完整的 Vue 组件对象,并能够在真实项目中运行验证。这个插件的实现不仅涵盖了 Vue 单文件组件编译的核心路径,也为后续扩展其他特性(如 CSS 预处理器支持、自定义块处理、热更新等)预留了清晰的接口。通过这篇实战,读者可以深入理解 Rollup 插件的钩子执行顺序、虚拟模块的用法以及 Vue 官方编译工具的内部工作原理,从而具备自主定制和优化 Vue 构建流程的能力,为复杂的工程化场景打下坚实基础。

相关推荐
IT_陈寒2 小时前
Vue的这个响应式陷阱,我调试了一整天才爬出来
前端·人工智能·后端
莪_幻尘2 小时前
手把手书写你的第一个 AI Skill:从原则到工程化
前端·agent·ai编程
lpfasd1232 小时前
2025-2026前端3D展示技术发展全景
前端·3d
JR空位投 李佃贵是2 小时前
如何编写轻量级 CSS 框架
前端·css·tensorflow
白猫不黑2 小时前
Web 应用中“敏感信息泄露“的常见位置?
前端·网络·web安全·网络安全·信息安全·渗透测试
张风捷特烈2 小时前
Flutter UI 解耦 - 天下大势,合久必分, 分久必合
android·前端·flutter
安冬的码畜日常2 小时前
【工欲善其事】深入理解 Node.js 带并发上限的异步任务批量执行逻辑
javascript·设计模式·node.js·ai编程·异步编程·并发执行
meilindehuzi_a3 小时前
Vue3组件样式隔离与组件通信实战:从 scoped、props 校验到记事本应用
前端·javascript·vue.js
LaughingZhu3 小时前
Product Hunt 每日热榜 | 2026-07-28
前端·神经网络·react.js·搜索引擎·前端框架