简体中文
切换主题
在 Vue 开发中,我们经常需要在模板中添加注释。这些注释可能是:
但是,你可能会发现 Vue 默认会移除模板中的所有 HTML 注释!今天我们就来深入探讨如何在 Vue 中保留这些有价值的注释。
// 简化版 Vue 编译器处理 function compile(template) { // 默认情况下,注释节点会被移除 const ast = parse(template, { comments: false // 默认不保留注释 }) // 生产环境优化:移除所有注释 if (process.env.NODE_ENV === 'production') { removeComments(ast) } }
Vue 移除注释的原因:
<template> <div> <!-- 这个注释在最终渲染中会被移除 --> <h1>Hello World</h1> <!-- 多行注释 也会被移除 --> <!-- TODO: 这里需要添加用户头像 --> <div class="user-info"> {{ userName }} </div> </div> </template>
编译结果:
<div> <h1>Hello World</h1> <div class="user-info"> John Doe </div> </div>
所有注释都不见了!
Vue 2 配置
module.exports = { chainWebpack: config => { config.module .rule('vue') .use('vue-loader') .tap(options => { return { ...options, compilerOptions: { comments: true // 保留注释 } } }) } }
module.exports = { module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { compilerOptions: { comments: true } } } ] } }
Vue 3 配置
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [ vue({ template: { compilerOptions: { comments: true // 保留注释 } } }) ] })
// (Vue CLI) module.exports = { configureWebpack: { module: { rules: [ { test: /\.vue$/, use: [ { loader: 'vue-loader', options: { compilerOptions: { comments: true } } } ] } ] } } }
<template> <!-- 这个注释会被保留 --> <div> <!-- 组件说明:用户信息展示 --> <UserProfile /> </div> </template> <script> export default { // Vue 3 可以在组件级别配置 compilerOptions: { comments: true } } </script>
// 使用完整版 Vue(包含编译器) import Vue from 'vue/dist/vue.esm.js' new Vue({ el: '#app', template: ` <div> <!-- 运行时编译会保留注释 --> <h1>Hello</h1> </div> `, compilerOptions: { comments: true } })
<script type="text/x-template">
<!DOCTYPE html> <html> <body> <div id="app"></div> <!-- 模板定义,注释会被保留 --> <script type="text/x-template" id="my-template"> <div> <!-- 用户信息区域 --> <div class="user-info"> {{ userName }} </div> <!-- TODO: 添加用户权限展示 --> </div> </script> <script> new Vue({ el: '#app', template: '#my-template', data: { userName: 'John' }, // 可能需要额外配置 compilerOptions: { comments: true } }) </script> </body> </html>
<template> <!-- UserCard 组件 @prop {Object} user - 用户对象 @prop {Boolean} showDetails - 是否显示详情 @slot default - 自定义内容 @slot avatar - 自定义头像 @event click - 点击事件 --> <div class="user-card" @click="$emit('click', user)"> <!-- 用户头像 --> <div class="avatar"> <slot name="avatar"> <img :src="user.avatar" alt="头像"> </slot> </div> <!-- 用户基本信息 --> <div class="info"> <h3>{{ user.name }}</h3> <p v-if="showDetails">{{ user.bio }}</p> <!-- 自定义内容区域 --> <slot /> </div> <!-- FIXME: 这里应该显示用户标签 --> </div> </template> <script> export default { name: 'UserCard', props: { user: { type: Object, required: true }, showDetails: { type: Boolean, default: false } } } </script>
<template> <!-- Design System: Button Component Type: Primary Button Color: Primary Blue (#1890ff) Spacing: 8px vertical, 16px horizontal Border Radius: 4px States: Default, Hover, Active, Disabled --> <button class="btn btn-primary" :disabled="disabled" @click="handleClick" > <!-- Button Content Guidelines: 1. 使用动词开头 2. 不超过4个汉字 3. 保持简洁明了 --> <slot>{{ label }}</slot> </button> <!-- Design Tokens Reference: --color-primary: #1890ff; --spacing-md: 8px; --radius-sm: 4px; --> </template>
<template> <div class="checkout-page"> <!-- TODO: @前端小王 - 添加优惠券选择功能 --> <div class="coupon-section"> 优惠券功能开发中... </div> <!-- FIXME: @前端小李 - 修复移动端支付按钮布局 --> <div class="payment-section"> <button class="pay-btn">立即支付</button> </div> <!-- OPTIMIZE: @性能优化小组 - 图片懒加载优化 --> <div class="recommendations"> <img v-for="img in productImages" :key="img.id" :src="img.thumbnail" :data-src="img.fullSize" class="lazy-image" > </div> <!-- HACK: @前端小张 - 临时解决Safari兼容性问题 --> <div v-if="isSafari" class="safari-fix"> <!-- Safari specific fixes --> </div> </div> </template> <script> export default { computed: { isSafari() { return /^((?!chrome|android).)*safari/i.test(navigator.userAgent) } } } </script>
module.exports = { chainWebpack: config => { config.module .rule('vue') .use('vue-loader') .tap(options => { const compilerOptions = { ...options.compilerOptions } // 只在开发环境保留注释 if (process.env.NODE_ENV === 'development') { compilerOptions.comments = true } else { compilerOptions.comments = false } return { ...options, compilerOptions } }) } }
// 自定义注释处理器 const commentPreserver = { // 只保留特定前缀的注释 shouldPreserveComment(comment) { const preservedPrefixes = [ 'TODO:', 'FIXME:', 'HACK:', 'OPTIMIZE:', '@design-system', '@api' ] return preservedPrefixes.some(prefix => comment.trim().startsWith(prefix) ) } } // 在配置中使用 module.exports = { chainWebpack: config => { config.module .rule('vue') .use('vue-loader') .tap(options => { return { ...options, compilerOptions: { whitespace: 'preserve', // 自定义注释处理 comments: (comment) => commentPreserver.shouldPreserveComment(comment) } } }) } }
<template> <!-- @component UserProfile @description 用户个人资料展示组件 @version 1.2.0 @author 开发团队 @prop {String} userId - 用户ID @prop {Boolean} editable - 是否可编辑 @event save - 保存事件 @event cancel - 取消事件 --> <div class="user-profile"> <!-- @section 基本信息 --> <div class="basic-info"> {{ user.name }} </div> <!-- @section 联系信息 --> <div class="contact-info"> {{ user.email }} </div> </div> </template>
// 注释提取脚本 const fs = require('fs') const path = require('path') const parser = require('@vue/compiler-sfc') function extractCommentsFromVue(filePath) { const content = fs.readFileSync(filePath, 'utf-8') const { descriptor } = parser.parse(content) const comments = [] const template = descriptor.template if (template) { // 解析模板中的注释 const ast = parser.compile(template.content, { comments: true }).ast traverseAST(ast, (node) => { if (node.type === 3 && node.isComment) { comments.push({ content: node.content, line: node.loc.start.line, file: path.basename(filePath) }) } }) } return comments } // 生成文档 const componentComments = extractCommentsFromVue('./UserProfile.vue') console.log(JSON.stringify(componentComments, null, 2))
// eslint-plugin-vue-comments module.exports = { rules: { 'require-todo-comment': { create(context) { return { 'VElement'(node) { const comments = context.getSourceCode() .getAllComments() .filter(comment => comment.type === 'HTML') // 检查是否有 TODO 注释 const hasTodo = comments.some(comment => comment.value.includes('TODO:') ) if (!hasTodo && node.rawName === 'div') { context.report({ node, message: '复杂 div 元素需要添加 TODO 注释说明' }) } } } } } } }
// Vue 模板(支持 HTML 注释) const template = ` <div> <!-- 这个注释会被处理 --> <h1>Title</h1> </div> ` // JSX(使用 JS 注释) const jsx = ( <div> {/* JSX 中的注释 */} <h1>Title</h1> { // 也可以使用单行注释 } </div> ) // Vue 渲染函数 export default { render(h) { // 渲染函数中无法添加 HTML 注释 // 只能使用 JS 注释,但不会出现在 DOM 中 return h('div', [ // 这是一个 JS 注释,不会出现在 DOM 中 h('h1', 'Title') ]) } }
// 自定义注释组件 const Comment = ({ text }) => ( <div style={{ display: 'none' }} data-comment={text} aria-hidden="true" /> ) // 使用 const Component = () => ( <div> <Comment text="TODO: 这里需要优化" /> <h1>内容</h1> </div> )
// 保留大量注释的性能测试 const testData = { withComments: ` <div> ${Array(1000).fill().map((_, i) => `<!-- 注释 ${i} -->\n<div>Item ${i}</div>` ).join('\n')} </div> `, withoutComments: ` <div> ${Array(1000).fill().map((_, i) => `<div>Item ${i}</div>` ).join('\n')} </div> ` } // 测试结果 // 有注释:虚拟DOM节点数 2000 // 无注释:虚拟DOM节点数 1000 // 内存占用增加约 30-50%
建议:只在开发环境保留注释,生产环境移除。
<template> <!-- 危险:可能泄露敏感信息 --> <!-- API密钥:sk_test_1234567890 --> <!-- 数据库连接:mysql://user:pass@localhost --> <!-- 内部接口:https://internal-api.company.com --> <!-- 安全:使用占位符 --> <!-- 使用环境变量:{{ apiEndpoint }} --> </template>
const Vue = require('vue') const renderer = require('@vue/server-renderer') const app = new Vue({ template: ` <div> <!-- SSR注释 --> <h1>服务端渲染</h1> </div> ` }) // SSR 渲染 const html = await renderer.renderToString(app, { // 需要显式启用注释 template: { compilerOptions: { comments: true } } }) console.log(html) // 输出:<div><!-- SSR注释 --><h1>服务端渲染</h1></div>
module.exports = { chainWebpack: config => { // Vue 文件处理 config.module .rule('vue') .use('vue-loader') .tap(options => { const isDevelopment = process.env.NODE_ENV === 'development' const isProduction = process.env.NODE_ENV === 'production' return { ...options, compilerOptions: { // 开发环境:保留所有注释 // 生产环境:移除注释,或只保留特定注释 comments: isDevelopment ? true : (comment) => { const importantPrefixes = [ 'TODO:', 'FIXME:', '@design-system', '@api-docs' ] return importantPrefixes.some(prefix => comment.trim().startsWith(prefix) ) }, // 其他编译选项 whitespace: isProduction ? 'condense' : 'preserve', delimiters: ['{{', '}}'] } } }) } }
<template> <!-- 良好的注释规范: 1. 使用清晰的标题 2. 使用标准标记(TODO, FIXME等) 3. @作者 和 @日期 4. 保持注释简洁 --> <!-- SECTION: 用户信息展示 TODO: 添加用户角色徽章 - @前端小李 - 2024-01 FIXME: 移动端头像大小需要调整 - @UI设计师 - 2024-01 @design-system: 使用 DS-Button 组件 @api: 用户数据来自 /api/user/:id --> <div class="user-profile"> <!-- 基本信息区域 --> <div class="basic-info"> <!-- 用户头像 --> <img :src="user.avatar" alt="头像"> </div> </div> </template>
comments: true
comments: false
comments: /@design-system/
在 Vue 中保留 HTML 注释需要明确的配置,但这对于开发效率、团队协作、文档维护都大有裨益。关键点:
记住:好的注释是代码的路标,而不仅仅是装饰。合理配置和使用注释,能让你的 Vue 项目更加可维护、可协作。
模板中保留 HTML 注释的完整指南
前言:注释的艺术
在 Vue 开发中,我们经常需要在模板中添加注释。这些注释可能是:
但是,你可能会发现 Vue 默认会移除模板中的所有 HTML 注释!今天我们就来深入探讨如何在 Vue 中保留这些有价值的注释。
一、Vue 默认行为:为什么移除注释?
源码视角
2
3
4
5
6
7
8
9
10
11
12
Vue 移除注释的原因:
默认行为演示
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
编译结果:
2
3
4
5
6
所有注释都不见了!
二、配置 Vue 保留注释的 4 种方法
Vue 编译器配置(全局)
Vue 2 配置
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vue 3 配置
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
单文件组件配置(Vue 3 特有)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
运行时编译(仅开发环境)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
使用
<script type="text/x-template">2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
三、注释的最佳实践与用例
组件文档生成
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
设计系统标注
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
协作开发标记
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
四、环境差异化配置
开发环境 vs 生产环境
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
按需保留特定类型注释
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
五、高级用法:注释数据处理
自动提取 API 文档
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
代码质量检查
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
六、与 JSX/渲染函数的对比
Vue 模板 vs JSX
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
在 JSX 中模拟 HTML 注释
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
七、注意事项与常见问题
性能影响
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
建议:只在开发环境保留注释,生产环境移除。
安全性考虑
2
3
4
5
6
7
8
9
SSR(服务端渲染)兼容性
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
八、最佳实践总结
配置文件模板
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
注释编写规范
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
各场景推荐方案
comments: truecomments: falsecomments: /@design-system/总结
在 Vue 中保留 HTML 注释需要明确的配置,但这对于开发效率、团队协作、文档维护都大有裨益。关键点: