Vue3 JSX和template哪个好?哪个更适合我现在的项目?
很多前端开发者刚接触Vue3的时候,都会在template和JSX之间纠结:template是Vue的“亲儿子”写法,写了这么多年都习惯了;但Vue3官方却给了JSX完整的支持,甚至连生态里的组件库比如Element Plus、Ant Design Vue 3都有不少地方用JSX重构或者实现,还有人说JSX更“自由”、更好复用,那这俩到底该怎么选?今天咱们就掰开揉碎了聊,从写法、能力、性能、生态、适配场景这几个维度说清楚。
Vue3 JSX和template的本质是什么?
要选对工具,先得知道它俩“肚子里装的是什么”,不管你写Vue3的template还是JSX,最终浏览器都看不懂,都得经过编译变成同一个东西——渲染函数(render function),渲染函数的作用就是生成虚拟DOM(VNode),然后Vue3的响应式系统和渲染引擎会接手后面的工作:对比新旧VNode,只更新变化的部分,最后挂载到真实DOM上。
那为什么官方要搞两种写法呢?其实核心是给开发者不同的“自由度”选择:template是“约束型自由”,JSX是“无上限自由”——就像你开车,template是走有导航、有车道线的高速路,省心省力,但不能随便变道、不能在应急车道停车;JSX是走没有明确边界的荒野小路,想去哪就去哪,甚至可以自己开一条路,但需要你自己认路、自己选路线,容错率低一点,但遇到特殊路况会更灵活。
从template的编译过程看约束性
Vue3的template编译器(@vue/compiler-sfc里的template部分),会把你写的模板代码,经过词法分析、语法分析、代码生成这三步,变成渲染函数,词法分析和语法分析咱们不用细抠,但可以看看它做了哪些“有用的约束”:
- 会帮你做静态标记:把不会变化的节点(比如纯文本、没有绑定动态属性的img标签)标记出来,Vue3的更新过程中会直接跳过这些节点,性能会有提升;
- 会帮你做静态提升:把纯静态的VNode或者VNode片段提到渲染函数外面,每次重新渲染的时候直接复用,不用再重新生成;
- 会帮你做事件缓存:如果你在组件里写了没有依赖当前this上下文的事件处理函数(lt;button @click="handleClick">,handleClick不需要访问this.data),编译器会把这个函数缓存起来,每次重新渲染不会重新创建;
- 会自动绑定this:在模板里写的表达式,不管是插值{{ }}还是v-bind、v-on,都会自动绑定到当前组件的实例上,你不用特意写this.handleClick或者this.message;
- 会对指令语法做限制但优化:比如v-for必须加key(开发模式警告,生产模式如果你忘了可能会有问题,但编译器会尽量帮你处理),v-model可以同时绑定多个属性(比如组件的v-model:title和v-model:content),这些语法虽然看起来“死板”,但都是经过大量实践验证的最佳实践,能帮你少踩坑。
从JSX的编译过程看灵活性
Vue3的JSX编译器(@vue/babel-plugin-jsx),会把你写的JSX代码,变成和template编译后结构几乎一样但需要手动写一部分优化逻辑的渲染函数,这里要注意,Vue3的JSX不是React的JSX,它有自己的一套语法糖,和React的区别不小,咱们后面会说。
JSX的本质是JavaScript的语法扩展,所以你可以在JSX里写任何JavaScript代码:循环、条件判断、函数调用、变量赋值、甚至闭包,这些都不用像template那样依赖指令,直接用原生的JS语法就能搞定,比如你要遍历一个数组生成列表,template里得写v-for,JSX里直接用Array.prototype.map就行;比如你要做复杂的条件渲染,template里得嵌套v-if、v-else-if、v-else,JSX里直接用三元运算符、或者逻辑与/或,甚至可以写一个专门的函数来返回不同的VNode。
Vue3 JSX和template的写法对比
光说本质可能有点虚,咱们直接拿几个常见的场景来对比写法,看看哪个更顺手。
场景1:简单的展示型组件
展示型组件的特点是:数据都是从父组件传过来的props,没有或者很少有自己的状态,逻辑很简单。
template写法
<template>
<div class="user-card">
<img :src="user.avatar" :alt="user.name" class="user-avatar" />
<h3 class="user-name">{{ user.name }}</h3>
<p class="user-bio">{{ user.bio || '暂无简介' }}</p>
<div class="user-tags">
<span v-for="tag in user.tags" :key="tag.id" class="user-tag">
{{ tag.content }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
interface User {
id: string
avatar: string
name: string
bio?: string
tags: { id: string; content: string }[]
}
defineProps<{ user: User }>()
</script>
<style scoped>
/* 样式代码省略 */
</style>
JSX写法
// UserCard.tsx
import { defineComponent } from 'vue'
interface User {
id: string
avatar: string
name: string
bio?: string
tags: { id: string; content: string }[]
}
export default defineComponent({
name: 'UserCard',
props: {
user: {
type: Object as () => User,
required: true
}
},
setup(props) {
return () => (
<div class="user-card">
<img src={props.user.avatar} alt={props.user.name} class="user-avatar" />
<h3 class="user-name">{props.user.name}</h3>
<p class="user-bio">{props.user.bio || '暂无简介'}</p>
<div class="user-tags">
{props.user.tags.map(tag => (
<span key={tag.id} class="user-tag">
{tag.content}
</span>
))}
</div>
</div>
)
}
})
这个场景下,两种写法的代码量差不多,template的写法更“直观”——因为它和HTML几乎一模一样,新手甚至不用学JavaScript就能看懂大概的结构;而JSX的写法需要写setup函数,需要用箭头函数返回VNode,需要注意props不能省略(因为没有自动绑定this),但TypeScript的支持会更“丝滑”——因为defineProps的类型定义是直接写在props选项里的(或者用setup的参数解构,但不管哪种,都是纯TypeScript语法,不需要像template那样单独定义interface再用泛型)。
场景2:复杂的条件渲染+动态内容
比如我们要做一个“内容卡片”,根据内容的类型(文字、图片、视频、文章链接)显示不同的布局:如果是文字,显示全部内容;如果是图片,显示缩略图,点击弹出大图;如果是视频,显示封面和播放按钮;如果是文章链接,显示标题、描述和来源网站的logo。
template写法
<template>
<div class="content-card">
<!-- 文字类型 -->
<template v-if="content.type === 'text'">
<p class="text-content">{{ content.data.text }}</p>
</template>
<!-- 图片类型 -->
<template v-else-if="content.type === 'image'">
<div class="image-wrapper" @click="showImageModal = true">
<img :src="content.data.thumbnail" :alt="content.data.alt" class="image-thumbnail" />
<div class="image-count">共{{ content.data.images.length }}张</div>
</div>
<!-- 图片弹窗,这里用了Vue3的Teleport组件,假设弹窗组件是全局注册的 -->
<Teleport to="body">
<ImageModal
v-if="showImageModal"
:images="content.data.images"
:initial-index="0"
@close="showImageModal = false"
/>
</Teleport>
</template>
<!-- 视频类型 -->
<template v-else-if="content.type === 'video'">
<div class="video-wrapper" @click="playVideo">
<img :src="content.data.cover" :alt="content.data.title" class="video-cover" />
<div class="play-button">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z"></path>
</svg>
</div>
<div class="video-duration">{{ formatDuration(content.data.duration) }}</div>
</div>
</template>
<!-- 文章链接类型 -->
<template v-else-if="content.type === 'link'">
<a :href="content.data.url" target="_blank" rel="noopener noreferrer" class="link-wrapper">
<div class="link-info">
<h4 class="link-title">{{ content.data.title }}</h4>
<p class="link-desc">{{ content.data.desc }}</p>
<div class="link-source">
<img :src="content.data.sourceLogo" :alt="content.data.sourceName" class="source-logo" />
<span class="source-name">{{ content.data.sourceName }}</span>
</div>
</div>
<img v-if="content.data.thumbnail" :src="content.data.thumbnail" :alt="content.data.title" class="link-thumbnail" />
</a>
</template>
<!-- 未知类型 -->
<template v-else>
<p class="unknown-content">不支持的内容类型</p>
</template>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ImageModal from './ImageModal.vue'
type ContentType = 'text' | 'image' | 'video' | 'link'
interface TextData {
text: string
}
interface ImageData {
thumbnail: string
alt: string
images: string[]
}
interface VideoData {
cover: string string
duration: number
}
interface LinkData {
url: string string
desc: string
sourceLogo: string
sourceName: string
thumbnail?: string
}
interface Content {
type: ContentType
data: TextData | ImageData | VideoData | LinkData
}
defineProps<{ content: Content }>()
const showImageModal = ref(false)
const playVideo = () => {
// 这里是播放视频的逻辑,比如调用视频播放器API
console.log('播放视频')
}
const formatDuration = (seconds: number) => {
const m = Math.floor(seconds / 60)
const s = seconds % 60
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
}
</script>
<style scoped>
/* 样式代码省略 */
</style>
这个场景下,template的写法已经开始有点“乱”了——嵌套了很多template标签,代码的缩进层次很深,读的时候需要不断往上翻找对应的v-if/v-else-if;而且很多逻辑需要写在script setup里,比如formatDuration、playVideo,和模板代码是分离的,修改的时候需要在两个文件块之间切换(虽然现在的IDE可以拆分窗口,但还是不如在一起方便)。
JSX写法
// ContentCard.tsx
import { defineComponent, ref, Teleport } from 'vue'
import ImageModal from './ImageModal.vue'
type ContentType = 'text' | 'image' | 'video' | 'link'
interface TextData {
text: string
}
interface ImageData {
thumbnail: string
alt: string
images: string[]
}
interface VideoData {
cover: string string
duration: number
}
interface LinkData {
url: string string
desc: string
sourceLogo: string
sourceName: string
thumbnail?: string
}
interface Content {
type: ContentType
data: TextData | ImageData | VideoData | LinkData
}
export default defineComponent({
name: 'ContentCard',
props: {
content: {
type: Object as () => Content,
required: true
}
},
components: { ImageModal },
setup(props) {
const showImageModal = ref(false)
const playVideo = () => {
console.log('播放视频')
}
const formatDuration = (seconds: number) => {
const m = Math.floor(seconds / 60)
const s = seconds % 60
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
}
// 专门写一个渲染内容的函数,逻辑和UI放在一起
const renderContent = () => {
const { type, data } = props.content
switch (type) {
case 'text':
// 这里可以直接用类型断言,因为TypeScript会根据type缩小data的类型
const textData = data as TextData
return <p class="text-content">{textData.text}</p>
case 'image':
const imageData = data as ImageData
return (
<>
<div class="image-wrapper" onClick={() => showImageModal.value = true}>
<img src={imageData.thumbnail} alt={imageData.alt} class="image-thumbnail" />
<div class="image-count">共{imageData.images.length}张</div>
</div>
<Teleport to="body">
{showImageModal.value && (
<ImageModal
images={imageData.images}
initialIndex={0}
onClose={() => showImageModal.value = false}
/>
)}
</Teleport>
</>
)
case 'video':
const videoData = data as VideoData
return (
<div class="video-wrapper" onClick={playVideo}>
<img src={videoData.cover} alt={videoData.title} class="video-cover" />
<div class="play-button">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z"></path>
</svg>
</div>
<div class="video-duration">{formatDuration(videoData.duration)}</div>
</div>
)
case 'link':
const linkData = data as LinkData
return (
<a href={linkData.url} target="_blank" rel="noopener noreferrer" class="link-wrapper">
<div class="link-info">
<h4 class="link-title">{linkData.title}</h4>
<p class="link-desc">{linkData.desc}</p>
<div class="link-source">
<img src={linkData.sourceLogo} alt={linkData.sourceName} class="source-logo" />
<span class="source-name">{linkData.sourceName}</span>
</div>
</div>
{linkData.thumbnail && (
<img src={linkData.thumbnail} alt={linkData.title} class="link-thumbnail" />
)}
</a>
)
default:
return <p class="unknown-content">不支持的内容类型</p>
}
}
return () => (
<div class="content-card">
{renderContent()}
</div>
)
}
})
这个场景下,JSX的优势就体现出来了:
- 逻辑和UI完全放在一起:我们写了一个renderContent函数,里面用switch语句处理不同的内容类型,读代码的时候不用在script和template之间切换;
- TypeScript的类型缩小更自然:因为switch语句是JavaScript原生的,TypeScript可以根据type的值自动缩小data的类型,不需要像template那样(虽然template的TypeScript支持在Vue3.3之后已经好了很多,但还是不如JSX灵活);
- 没有多余的标签:比如template写法里的template标签,JSX里可以用Fragment(<>...</>)代替,不会生成额外的真实DOM节点;
- 事件绑定更符合JavaScript习惯:JSX里用onClick、onClose,而不是@click、@close,如果你之前写过React,会觉得非常顺手。
场景3:高阶组件(HOC)和渲染函数组件
高阶组件和渲染函数组件是Vue3里用来复用逻辑和UI的两种高级方式,template在这两种场景下几乎“无能为力”,而JSX则是“如鱼得水”。
渲染函数组件
渲染函数组件是指没有template,直接用setup函数返回VNode的组件(也就是我们刚才写的JSX组件的简化版,如果你不想用defineComponent,也可以直接写一个箭头函数),比如我们要做一个“可复用的加载状态容器”,不管里面包的是什么内容,当loading为true的时候显示加载动画,loading为false的时候显示内容。
用JSX写的渲染函数组件
// WithLoading.tsx
import { defineComponent } from 'vue'
export default defineComponent({
name: 'WithLoading',
props: {
loading: {
type: Boolean,
default: false
},
loadingText: {
type: String,
default: '加载中...'
}
},
setup(props, { slots }) {
return () => (
<div class="with-loading">
{props.loading ? (
<div class="loading-wrapper">
<div class="loading-spinner"></div>
<span class="loading-text">{props.loadingText}</span>
</div>
) : (
slots.default?.()
)}
</div>
)
}
})
使用这个组件
不管是用template还是JSX的组件,都可以直接用这个WithLoading组件:
<!-- UserList.vue -->
<template>
<WithLoading :loading="loading" loading-text="正在获取用户列表...">
<ul class="user-list">
<li v-for="user in users" :key="user.id" class="user-item">
{{ user.name }}
</li>
</ul>
</WithLoading>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import WithLoading from './WithLoading.tsx'
interface User {
id: string
name: string
}
const loading = ref(true)
const users = ref<User[]>([])
onMounted(async () => {
// 模拟获取用户列表的API请求
await new Promise(resolve => setTimeout(resolve, 2000))
users.value = [
{ id: '1', name: '张三' },
{ id: '2', name: '李四' },
{ id: '3', name: '王五' }
]
loading.value = false
})
</script>
高阶组件(HOC)
高阶组件是指接受一个组件作为参数,返回一个新组件的函数,它可以用来给原组件添加额外的props、状态、逻辑或者UI,比如我们要给刚才的UserList组件添加“懒加载”功能,当用户滚动到页面底部的时候自动加载更多用户。
如果用template的话,你需要把懒加载的逻辑(监听滚动事件、计算滚动位置、判断是否加载更多)复制到每个需要懒加载的组件里,或者用Vue3的Composition API封装成一个hook,然后在每个组件里引入使用,但如果你有很多组件需要添加完全一样的懒加载UI(比如底部的加载动画、没有更多数据的提示),用hook的话还是需要在每个组件里写重复的UI代码,而用HOC+JSX的话,就可以把逻辑和UI都封装在HOC里,原组件几乎不需要修改。
用JSX写的高阶组件
// withInfiniteScroll.tsx
import { defineComponent, ref, onMounted, onUnmounted, h } from 'vue'
// 定义HOC的参数类型
interface WithInfiniteScrollOptions {
threshold?: number // 滚动到底部的阈值(像素),默认100px
loadMore: () => Promise<void> // 加载更多的函数
hasMore: boolean // 是否还有更多数据
}
// 定义HOC
export function withInfiniteScroll<T extends new (...args: any[]) => any>(
Component: T,
options: WithInfiniteScrollOptions
) {
const { threshold = 100, loadMore, hasMore } = options
return defineComponent({
name: `WithInfiniteScroll${(Component as any).name || 'Component'}`,
setup(props, { attrs, slots }) {
const loading = ref(false)
const containerRef = ref<HTMLElement | null>(null)
// 监听滚动事件
const handleScroll = async () => {
if (loading.value || !hasMore) return
const container = containerRef.value
if (!container) return
const { scrollTop, scrollHeight, clientHeight } = container
if (scrollTop + clientHeight >= scrollHeight - threshold) {
loading.value = true
try {
await loadMore()
} finally {
loading.value = false
}
}
}
onMounted(() => {
const container = containerRef.value
if (container) {
container.addEventListener('scroll', handleScroll, { passive: true })
}
})
onUnmounted(() => {
const container = containerRef.value
if (container) {
container.removeEventListener('scroll', handleScroll)
}
})
return () => (
<div ref={containerRef} class="infinite-scroll-container" style={{ height: '100%', overflow: 'auto' }}>
{/* 渲染原组件,把所有的props、attrs、slots都传过去 */}
<Component {...props} {...attrs} v-slots={slots} />
{/* 渲染底部的加载状态 */}
<div class="infinite-scroll-footer">
{loading.value ? (
<div class="loading-wrapper">
<div class="loading-spinner"></div>
<span class="loading-text">加载更多...</span>
</div>
) : hasMore ? (
<div class="no-more-tip">上拉加载更多</div>
) : (
<div class="no-more-tip">没有更多数据了</div>
)}
</div>
</div>
)
}
}) as T
}
使用这个高阶组件
我们需要把刚才的UserList.vue改成一个纯展示型的组件(没有自己的loading和获取用户列表的逻辑,因为这些逻辑会被HOC封装):
<!-- PureUserList.vue -->
<template>
<ul class="user-list">
<li v-for="user in users" :key="user.id" class="user-item">
{{ user.name }}
</li>
</ul>
</template>
<script setup lang="ts">
interface User {
id: string
name: string
}
defineProps<{ users: User[] }>()
</script>
我们在另一个组件里使用withInfiniteScroll HOC:
// UserListContainer.tsx
import { defineComponent, ref } from 'vue'
import { withInfiniteScroll } from './withInfiniteScroll.tsx'
import PureUserList from './PureUserList.vue'
interface User {
id: string
name: string
}
// 用HOC包裹PureUserList
const UserListWithInfiniteScroll = withInfiniteScroll(PureUserList, {
threshold: 50,
loadMore: async () => {
// 模拟加载更多的API请求
await new Promise(resolve => setTimeout(resolve, 1500))
// 这里假设我们有一个全局的users状态,或者通过props传进来,为了简化示例,我们直接在外面定义
// 实际项目中,你可以把hasMore、loadMore、users都作为props传给UserListContainer,或者用Pinia/Vuex管理状态
console.log('加载更多用户')
},
hasMore: true // 这里暂时设为true
})
export default defineComponent({
name: 'UserListContainer',
setup() {
const users = ref<User[]>([
{ id: '1', name: '张三' },
{ id: '2', name: '李四' },
{ id: '3', name: '王五' },
// 初始加载20个用户
...Array.from({ length: 17 }, (_, i) => ({ id: `${i + 4}`, name: `用户${i + 4}` }))
])
return () => (
<div style={{ height: '100vh' }}>
<UserListWithInfiniteScroll users={users.value} />
</div>
)
}
})
这个场景下,JSX的优势是不可替代的——因为HOC需要动态创建一个新组件,并且在新组件的setup函数里返回VNode,template根本做不到这一点(除非你用Vue.compile API动态编译template,但那样的话性能会很差,而且不利于Tree Shaking)。
Vue3 JSX和template的性能对比
很多开发者关心的另一个问题是:JSX和template的性能哪个更好?
其实在Vue3里,两者的性能差距非常小,几乎可以忽略不计——因为它们最终都会变成渲染函数,而Vue3的响应式系统和渲染引擎是一样的,template编译器会自动做一些优化(静态标记、静态提升、事件缓存),而JSX编译器需要你手动开启这些优化(或者用Vue3.3之后的defineComponent+setup返回箭头函数的方式,编译器会自动做一部分优化)。
如何让JSX的性能和template一样好?
如果你想让JSX的性能达到template的水平,可以做以下几件事:
- 使用Vue3.3+版本:Vue3.3之后,@vue/babel-plugin-jsx会自动对setup返回的箭头函数里的静态内容做标记和提升;
- 使用
@vue/babel-plugin-jsx的优化选项:在babel.config.js或者.babelrc里配置transformOn: true(自动把v-on的数组语法转换成优化后的代码)、cacheHandlers: true(自动缓存事件处理函数)、optimize: true(开启所有优化选项); - 手动做静态提升:如果你有一些非常大的纯静态VNode片段,可以把它提到setup函数外面,这样每次重新渲染的时候直接复用;
- 避免在渲染函数里创建不必要的对象和数组:比如不要在return的VNode里写
style={{ color: 'red', fontSize: '14px' }},而是把这个对象提到setup函数外面;不要在return的VNode里写[1, 2, 3].map(...),而是如果数组不变的话,提到外面。
Vue3 JSX和template的生态对比
生态也是选择写法的一个重要因素——如果生态里的组件库、工具库、教程文档都偏向某一种写法,那你用这种写法会更省心。
组件库
目前主流的Vue3组件库,比如Element Plus、Ant Design Vue 3、Naive UI,都同时支持template和JSX——你可以用template调用它们的组件,也可以用JSX调用;而且这些组件库的内部实现,都有不少地方用JSX重构或者实现(比如Element Plus的表格组件、Ant Design Vue 3的表单组件),因为JSX更适合处理复杂的UI逻辑。
工具库
Vue3的官方工具库,比如Vue Router、Pinia、VueUse,都完全支持两种写法——不管你用template还是JSX,都可以正常使用它们的API。
教程文档
目前Vue3的官方教程文档,还是以template为主——官方文档里的大部分示例都是用template写的,只有在“渲染函数”和“JSX”这两个章节里才会介绍JSX,随着越来越多的开发者开始使用JSX,第三方的JSX教程文档也越来越多,比如Vue Mastery、B站上的很多Vue3教程,都会专门花一节课讲JSX。
到底该选Vue3 JSX还是template?
说了这么多,终于到了最关键的问题:到底该选哪个?
其实没有“绝对的正确答案”,只有“适合你的项目和团队的答案”——你可以根据以下几个因素来选择:
团队的技术栈和习惯
如果你的团队之前都是写Vue2或者Vue3 template的,而且没有React的经验,那继续用template是更好的选择——因为学习成本低,上手快,团队协作也更顺畅;如果你的团队之前有React的经验,或者有部分成员有React的经验,那可以尝试用JSX——因为React的开发者转到Vue3 JSX几乎没有学习成本,而且可以把React的一些最佳实践(比如HOC、渲染属性、自定义Hook)带到Vue3里。
项目的类型和复杂度
- 如果是简单的展示型项目(比如企业官网、博客、电商的商品详情页):用template——因为这类项目的逻辑很简单,template的写法更直观,开发效率更高;
- 如果是复杂的中后台项目(比如CRM、ERP、OA):可以混合使用——展示型的页面用template,复杂的表单、表格、图表组件用JSX;
- 如果是组件库或者工具库:尽量用JSX——因为JSX更灵活,更容易复用,而且TypeScript的支持更好;
- 如果是需要大量高阶组件或者渲染函数组件的项目:必须用JSX——因为template在这两种场景下几乎“无能为力”。
你个人的喜好
也是最重要的一点:你个人的喜好——如果你喜欢“约束型自由”,喜欢写和HTML几乎一模一样的代码,喜欢少踩坑,那选template;如果你喜欢“无上限自由”,喜欢把逻辑和UI放在一起,喜欢用原生的JavaScript语法,那选JSX。
写在最后
不管你选template还是JSX,都是Vue3的“合法公民”——官方不会强制你用某一种写法,而是给你足够的选择权,你也不用一开始就完全切换到某一种写法,可以先在一个小的组件或者页面里尝试一下,看看哪种写法更适合你,然后再慢慢推广到整个项目。
给大家一个小建议:如果你是Vue3的新手,先把template学好——因为template是Vue的核心,学好template之后,你再学JSX会非常容易,因为JSX的很多概念(比如VNode、响应式、插槽)和template是一样的;如果你已经是Vue3的老手,而且有一定的React经验,那可以尝试用JSX——它会给你带来不一样的开发体验。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


