手写 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 构建流程的能力,为复杂的工程化场景打下坚实基础。

相关推荐
fangzhanpeng16827 分钟前
(前端)2.js变量作用域样例
开发语言·前端·javascript
赵广陆31 分钟前
企业实战:web服务集成
前端·pycharm·fastapi
demo007x44 分钟前
Hermes-Agent 技术架构
前端·后端·agent
Flynt1 小时前
我扒了Fantastic-admin 6.0的源码,聊聊"AI时代版本答案"这个称号
vue.js·ai编程·cursor
码艺-Alimjan3 小时前
Vue项目源码备份最佳实践:无视node_modules,打包体积仅2MB
前端·javascript·vue.js
隔窗听雨眠3 小时前
esbuild构建工具简介:重新定义前端构建速度的极速打包器
前端
蔓越莓3 小时前
Webpack 常用 Loader +手写Loader
前端·面试
潍坊老登4 小时前
写了一个某音自动刷视频,通过大模型分析视频主题的Android应用
前端