• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • 组件基础

    组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的编写。在实际应用中,组件常常被组织成层层嵌套的树状结构:

    这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以,在每个组件内,封装自定义内容与逻辑。Vue 同样也能很好地配合原生 Web Component。

    定义一个组件

    当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的.vue文件中,这被叫做单文件组件(Single-File Component,简称 SFC):

    <script setup>
    import { ref } from 'vue'
    
    const count = ref(0)
    </script>
    
    <template>
      <button @click="count++">点击了 {{ count }} 次</button>
    </template>
    

    当不使用构建步骤时,一个 Vue 组件,以一个包含 Vue 特定选项的 JavaScript 对象,来定义:

    import { ref } from 'vue'
    
    export default {
      setup() {
        const count = ref(0)
        return { count }
      },
      template: `
        <button @click="count++">
          You clicked me {{ count }} times.
        </button>`
      // 或者 `template: '#my-template-element'`
    }
    

    这里的模板 template 是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。你也可以使用ID选择器,来指向一个元素(通常是原生的<template>元素),Vue 将会使用其内容作为模板来源。

    上面的例子中定义了一个组件,并在一个.js文件里默认导出了它自己。但你也可以在一个指定名称的文件中,导出多个组件。


    使用组件

    要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做ButtonCounter.vue的文件中,这个组件将会以默认导出的形式被暴露给外部。

    <script setup>
    import ButtonCounter from './ButtonCounter.vue'
    </script>
    
    <template>
      <h1>这里是一个子组件!</h1>
      <ButtonCounter />
    </template>
    

    通过<script setup>,导入的组件,在模板中直接可用。

    当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入。关于组件的全局注册和局部注册两种方式利弊的讨论,我们放在了组件注册这一章节中讨论。

    组件可以被重用任意多次:

    <h1>这里有好多子组件!</h1>
    <ButtonCounter />
    <ButtonCounter />
    <ButtonCounter />
    

    你会注意到,每当点击这些按钮时,每一个组件都维护着自己的状态,是不同的count。这是因为每当你使用一个组件,就创建了一个新的实例


    • 在单文件组件中,即.vue文件。推荐为子组件使用PascalCase的标签名,以此来和原生的 HTML 元素作区分。虽然原生 HTML 标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的。使用/>来关闭一个标签。
    • 在 DOM 模板中,即.html文件。例如原生<template>元素的内容,模板的编译将服从于原生 HTML 的解析行为。在这种情况下,你应该需要使用kebab-case形式,并显式地关闭这些组件的标签。
    <!-- 如果模板时写在 DOM 中 -->
    <button-counter></button-counter>
    <button-counter></button-counter>
    
    <!DOCTYPE <html>
        <head>
            <meta charset="utf-8">
            <title>Vue Component</title>
        </head>
        <body>
            <div id="app">
                <!-- 在 HTML 中只能是 kebab-case (短横线命名) 的会被渲染 -->
                <my-component @component-click="myClick"></my-component>
            </div>
        </body>
        <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
        <script>
        </script>
    </html>
    


    传递 props

    如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到props

    props是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的props列表上,使用defineProps宏,声明它:

    <!-- BlogPost.vue -->
    <script setup>
    defineProps(['title'])
    </script>
    
    <template>
      <h4>{{ title }}</h4>
    </template>
    

    defineProps是一个仅<script setup>中可用的编译宏命令,并不需要显式地导入。声明的props会自动暴露给模板。defineProps会返回一个对象,其中包含了可以传递给组件的所有props,因此我们在 JavaScript 中使用:

    const props = defineProps(['title'])
    console.log(props.title)
    


    如果你没有使用<script setup>props必须以props 选项的方式声明,props对象会作为setup()函数的第一个参数

    export default {
      props: ['title'],
      setup(props) {
        console.log(props.title)
      }
    }
    

    一个组件可以有任意多的props,默认情况下,所有prop都接受任意类型的值。


    当一个prop被注册后,可以像这样以自定义 attribute 的形式传递数据给它:

    <BlogPost title="我的 Vue 学习之旅" />
    <BlogPost title="Vue 博客文章总集" />
    <BlogPost title="为什么 Vue 这么有趣" />
    

    然而在某些应用中,你也许在父组件中有如下的一个博客文章数组:

    const posts = ref([
      { id: 1, title: 'My journey with Vue' },
      { id: 2, title: 'Blogging with Vue' },
      { id: 3, title: 'Why Vue is so fun' }
    ])
    

    则可以使用v-for来渲染它们:

    <BlogPost
      v-for="post in posts"
      :key="post.id"
      :title="post.title"
     />
    

    请注意这里我们是怎么使用v-bind来传递动态props的。当事先不知道要渲染的确切内容时,这一点特别有用。


    已知 HTML attribute与 DOM property,具有映射关系,DOM property可以访问设置 HTML attribute的值。所以在引用组件的 HTML 上添加一些自定义的attribute,形成prop列表,此列表被子组件的defineProps()接收。(选项式 API 中,被props选项接收)。从而实现把父组件数据传值到子组件中
    若子组件没有defineProps()(或props选项)接收,那么attribute会被子组件继承,可以通过$attrs property 访问那些被继承的attribute
    组件实例提供了一个自定义事件系统。在父组件中,通过v-on@,来选择性地监听子组件中,抛出的事件,就像监听原生 DOM 事件那样。在子组件中,调用内置的$emit()方法,通过参数传入事件名称来抛出事件。子组件中,也可以用defineEmits()宏,选择性地声明需要抛出的事件,使用emit()函数,参数传入事件名称来抛出事件



    监听事件

    继续开发我们的<BlogPost>组件,我们会发现有时候需要与父组件进行交互。例如,要在此处实现 A11y 的需求,将博客文章的文字能够放大,而页面的其余部分仍使用默认字号。

    在父组件中,我们可以添加一个postFontSizeref来实现这个效果:

    js
    const posts = ref([
      /* ... */
    ])
    
    const postFontSize = ref(1)
    
    template
    <div :style="{ fontSize: postFontSize + 'em' }">
      <BlogPost
        v-for="post in posts"
        :key="post.id"
        :title="post.title"
        @enlarge-text="postFontSize += 0.1"
       />
    </div>
    


    现在,让我们给<BlogPost>组件添加一个按钮:

    <!-- BlogPost.vue, 省略了 <script> -->
    <template>
      <div class="blog-post">
        <h4>{{ title }}</h4>
        <button @click="$emit('enlarge-text')">放大文字</button>
      </div>
    </template>
    

    因为有了@enlarge-text="postFontSize += 0.1"的监听,父组件会接收这一事件,从而更新postFontSize的值。


    我们可以通过defineEmits()宏来选择性地声明需要抛出的事件:

    <!-- BlogPost.vue -->
    <script setup>
    defineProps(['title'])
    defineEmits(['enlarge-text'])
    </script>
    

    这记录了一个组件发出的所有事件,并可选择对其进行验证。这还使得 Vue 避免了将它们作为原生事件监听器隐式地应用于子组件的根元素。

    defineProps类似,defineEmits仅可用于<script setup>之中,并且不需要导入,返回的emit()函数可以被用于在 JavaScript 代码中抛出事件:

    const emit = defineEmits(['enlarge-text'])
    emit('enlarge-text')
    


    如果你没有在使用<script setup>,你可以通过emits 选项定义组件会抛出的事件。你可以从setup()函数的第二个参数(上下文对象)上访问到emit()函数:

    <script>
    export default {
      emits: ['enlarge-text'],
      setup(props, ctx) {
        ctx.emit('enlarge-text')
      }
    }
    </script>
    


    通过插槽来分配内容

    和 HTML 元素一样,像这样能够向组件中传递内容是非常有用的:

    <AlertBox>
      发生了一些错误。
    </AlertBox>
    

    这会渲染成这样:

    这可以通过 Vue 的自定义<slot>元素来实现:

    
    <template>
      <div class="alert-box">
        <strong>Error!</strong>
        <slot />
      </div>
    </template>
    
    <style scoped>
    .alert-box {
      /* ... */
    }
    </style>
    

    正如你上面所看到的,我们使用<slot>作为一个占位符,之后的内容就会放在这里。


    动态组件

    有的需求会想要在两个组件间来回切换,比如 Tab 界面:

    <script setup>
    import Home from './Home.vue'
    import Posts from './Posts.vue'
    import Archive from './Archive.vue'
    import { ref } from 'vue'
     
    const currentTab = ref('Home')
    
    const tabs = {
      Home,
      Posts,
      Archive
    }
    </script>
    
    <template>
      <div class="demo">
        <button
           v-for="(_, tab) in tabs"
           :key="tab"
           :class="['tab-button', { active: currentTab === tab }]"
           @click="currentTab = tab"
         >
          {{ tab }}
        </button>
        <component :is="tabs[currentTab]" class="tab"></component>
      </div>
    </template>
    
    <style></style>
    


    上面的例子是通过 Vue 的<component>元素和特殊的is attribute 实现的:

    <!-- currentTab 改变时组件也改变 -->
    <component :is="tabs[currentTab]"></component>
    

    在上面的例子中,被传给:is的值可以是以下几种:

    • 被注册的组件名
    • 导入的组件对象

    你也可以使用is attribute 来创建一般的 HTML 元素。

    当使用<component :is="...">来在多个组件间作切换时,组件会在被切换掉后卸载。我们可以通过<KeepAlive>组件强制不活跃的组件仍然保持“存活”的状态。

    <!-- 非活跃状态的组件将会被缓存! -->
    <KeepAlive>
      <component :is="tabs[currentTab]"></component>
    </KeepAlive>
    


    DOM 模板解析注意事项

    字符串模板来源:
    • 单文件组件。以.vue后缀结尾的文件。
    • 内联模板字符串,例如template:'...'。
    • <script type="text/x-template">
    DOM 模板来源:以.html后缀结尾的文件。

    如果你想在 DOM 中直接书写 Vue 模板,Vue 则必须从 DOM 中获取模板字符串。因为浏览器的原生 HTML 解析行为,因此有一些需要注意的事项。

    大小写区分

    HTML 标签和属性名称是不分大小写的,所以浏览器会把任何大写的字符解释为小写。这意味着当你使用 DOM 内的模板时,无论是PascalCase形式的组件名称、camelCase形式的prop名称还是v-on的事件名称,都需要转换为相应等价的kebab-case(短横线连字符)形式:

    // JavaScript 中的 camelCase
    const BlogPost = {
      props: ['postTitle'],
      emits: ['updatePost']
      template: `
        <h3>{{ postTitle }}</h3>
      `
    }
    
    <!-- HTML 中的 kebab-case -->
    <blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>
    


    闭合标签

    我们在上面的例子中已经使用过了闭合标签(self-closing tag):

    <MyComponent />
    

    这是因为 Vue 的模板解析器将/>作为标签关闭的标志,无关其类型。

    然而在 DOM 模板中,我们必须显式地写出关闭标签:

    <my-component></my-component>
    

    这是由于 HTML 只允许一小部分特殊的元素省略其关闭标签,最常见的就是<input><img>。对于其他的元素来说,如果你省略了关闭标签,原生的 HTML 解析器会认为开启的标签永远没有结束,用下面这个代码片段举个例子:

    <my-component /> <!-- 我们想要在这里关闭标签... -->
    <span>hello</span>
    

    将被解析为:

    <my-component>
      <span>hello</span>
    </my-component> <!-- 但浏览器会在这里关闭标签 -->
    


    元素位置限制

    某些 HTML 元素对于放在其中的元素类型有限制,例如<ul><ol><table><select>,相应的,某些元素仅在放置于特定元素中时才会显示,例如<li><tr><option>

    这将导致在使用带有此类限制元素的组件时出现问题。例如:

    <table>
      <blog-post-row></blog-post-row>
    </table>
    

    自定义的组件<blog-post-row>将作为无效的内容被忽略,因而在最终呈现的输出中造成错误。我们可以使用特殊的is attribute 作为一种解决方案:

    <table>
      <tr is="vue:blog-post-row"></tr>
    </table>
    
    当使用在原生 HTML 元素上时,is的值必须加上前缀vue:才可以被解析为一个 Vue 组件。这一点是必要的,为了避免和原生的自定义内置元素相混淆。



    HTML attribute(特性)和 DOM property(属性)

    HTML attributeDOM property
    举例标准 attributeidclasstypetitledata

    非标准 attribute:自定义的data-*
    取值字符串或 null任意合法 js 数据类型
    大小写对大小写不敏感对大小写敏感
    不存在时返回值如果是标准属性,返回"";如果是非标准属性,返回 nullundefined
    对于 href返回 html 设置的值返回解析后的完整 URL
    更新 valueproperty也更新attribute值不更新
    • 当浏览器解析网页时,将 HTML attribute(特性)映射为 DOM property(属性)。attribute → property。只映射标准 attribute;非标准 HTML attribute 并不会自动映射为 DOM property。当我们使用data-开头的attribute时,会映射到 DOM 的dataset property。中划线格式会变成驼峰格式。
    • DOM 对象也提供了操作特性的 API,可以获取非标准的 attributes。DOM 操作 HTML attribute 方法主要有三个,分别是getAttribute()setAttribute()removeAttribute()


    映射

    编写一段HTML代码的时候,你可以在 HTML 标签上面定义attribute,当浏览器编译完 HTML 代码,会生成与之对应的一个个DOM节点,每个DOM节点是一个对象,所以它拥有很多property。例如以下代码:

    <input type="text" value="Name:">
    

    input标签有两个attribute,即typevalue。当浏览器编译完这段代码,一个HTMLInputElement对象会被创建,这个对象包含一系列的property,例如acceptaccessKeyalignaltattributesautofocusbaseURIcheckedchildElementCountchildNodeschildrenclassListclassNameclientHeight等。


    例解

    • 对于一个 DOM 节点对象,property就是这个对象上的属性;attribute是该对象对应的HTML标签元素上的特性。
    • 当一个HTML标签元素解析成一个 DOM 节点对象时,这个节点对象上有很多property属性名和HTML标签元素上的attribute存在映射关系,或相同或相似,但不是全部都一一映射关系。

    比如:

    <input id="the-input" type="text" value="Name:">
    

    对于上述HTML标签,其对应的DOM节点对象的property包括idtypevalue(当然还有别的):

    • id:DOM节点对象上的id property和HTML标签上的id attribute是映射关系。当读取DOM节点对象的id属性值时就是读取HTML标签上的id特性值,当给DOM节点对象的id属性写入值时就是往HTML标签上的id特性写入值。id是一个纯映射特性,映射过程中不会产生副作用,比如修改或者限制其值。
    • type:DOM节点对象上的type property和HTML标签上的type attribute是映射关系。当读取DOM节点对象的type属性值时就是读取HTML标签上的tyep特性值,当给DOM节点对象的type属性写入值时就是往HTML标签上的type特性写入值。但是,type属性是一个非纯映射特性,因为在映射过程中,它的值会被限制在已知类型中,比如 input 支持的有效 type类型。举个例子,对于<input type="foo">这样一个HTML标签, Input.getAttribute('type')的返回值是foo,而Input.type的返回值为text
    • value:DOM节点对象上的value property和HTML标签上的value attribute不存在映射关系,它就是当前input输入框的值。当用户改变输入框中的值,DOM对象上的value属性会跟着变化。假如用户在输入框中输入'John',那么Input.value的返回值是John,而Input.getAttribute('value')的返回值是Name:。


    从上面可以看出,DOM节点对象上的value property是当前输入框内容的映射,而HTML标签上的value attribute是HTML标签元素上初始设置的值(或者代码设置)。

    如果你需要知道当前输入框的内容,就直接读取DOM节点对象的value property;如果你想知道输入框的初始值是什么,就直接读取 HTML 标签元素的 value attribute,或者使用DOM节点对象的 defaultValue 属性,它与HTML标签上的value attribute是纯映射关系:

    Input.value                 // returns "John"
    Input.getAttribute('value') // returns "Name:"
    Input.defaultValue          // returns "Name:"
    


    总结

    • 有些DOM节点对象上的property属性与 HTML 标签上的attribute特性一一映射(名字一样),比如relid
    • 有些名字稍微有些变化,比如 DOM节点对象htmlFor属性对应 HTML 标签上的for特性,DOM节点对象className属性对应 HTML标签上的class特性;
    • 更多的是两者虽然有映射关系,但是存在副作用,会限制或者修改属性值,比如srchrefdisabledmultiple等。

    下篇:组件注册