Vue.js 实例和组件

在使用 Vue 开发的每个 Web 应用中,大多数是一个单页应用(Single Page Application, SPA),就是只有一个 Web 页面的应用,它加载单个 HTML 页面,并在用户与应用程序交互时动态地更新该页面的 DOM 内容。下面只讨论单页应用的场合。

每一个单页 Vue 应用都需要从一个 Vue 实例开始。每一个 Vue 应用都由若干个 Vue 实例或组件组成。

创建 Vue.js 实例

首先新建 index.html,并通过 <script> 的方式来导入 Vue.js,然后创建一个 Vue 实例,如示例代码2-1-1所示。

示例代码2-1-1 创建Vue.js实例
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0,
maximum-scale=1.0, user-scalable=no" />
    <title>Vue实例</title>
    <script src="https://unpkg.com/vue@3.2.28/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
    {{msg}}
</div>
<script type="text/javascript">
    const app = Vue.createApp({
        data(){
            return {
                msg: "hello world",
            }
        }
    })
    app.mount('#app')
</script>
</body>
</html>

通过 createApp 方法可以创建一个 Vue 应用实例,一个 Vue 实例若想和页面上的 DOM 渲染进行挂载,就需要调用 mount 方法,参数传递 id 选择器,挂载之后这个 id 选择器对应的 DOM 会被 Vue 实例接管,当然也可以用 class 选择器。需要注意,如果是通过 class 选择器找到多个 DOM 元素,则只会选取第一个。

data 属性表示数据,用于接收一个对象。也就是说,如果 Vue 实例需要操作页面 DOM 里的数据,可以通过 data 来控制,需要在 HTML 代码中写差值表达式 {{}},然后获取 data 中的数据(如 {{msg}} 会显示成 hello world)。接着可以在 Vue 实例中通过 this.xxx 使用 data 中定义的值。需要注意在 Vue 3 中,data 需要是一个函数 function,并返回对象。在浏览器中打开 index.html 来查看,效果如图2-1所示。

image 2024 02 21 09 49 58 659
Figure 1. 图2-1 Vue.js实例,显示出hello world

createApp 方法传递的参数是根组件(也可以叫作根实例)的配置,返回的对象叫作 Vue 应用实例,当应用实例调用 mount 方法后返回的对象叫作根组件实例,一个 Vue 应用由若干个实例组成,准确地说是由一个根实例和若干个子实例(也叫子组件)组成。如果把一个 Vue 应用看作一棵大树,那么称根节点为 Vue 根实例,子节点为 Vue 子组件。当然,一个 Vue 实例还有很多其他的属性和方法,在后续章节中会讲到。

用 component() 方法创建组件

首先,Vue 中每个组件都可以定义自己的名字。下面就新建一个自定义组件,将其放在 Vue 根实例中使用,可使用上一步返回的 Vue 实例 app 调用 component() 方法新建一个组件,如示例代码2-1-2所示。

示例代码2-1-2 app.component() 注册组件
const app = Vue.createApp({})
// 定义一个名为 button-component 的新组件
app.component('button-component', {
    data() {
        return {
            str: 'btn'
        }
    },
    template: '<button>I am a {{str}}</button>'
})
app.mount('#app')

app.component() 方法的第一个参数是标识这个组件的名字,名为 button-component,第二个参数是一个对象,这里的 data 必须是一个函数 function,这个函数返回一个对象。template 定义了一个模板,表示这个组件将会使用这部分 HTML 代码作为其内容,如示例代码2-1-3所示。下面我们来看刚刚定义的 button-component 组件的使用。

<div id="app">
    <button-component></button-component>
    {{msg}}
</div>

<button-component> 表示用了一个自定义标签来使用组件,其内容保持和组件名一样,这是 Vue 中特有的使用组件的写法。

此外,也可以多次使用 <button-component> 组件,以达到简单的组件复用的效果,如示例代码2-1-4所示。

示例代码2-1-4 组件复用
<div id="app">
    <button-component></button-component>
    <button-component></button-component>
    {{msg}}
</div>

再次运行 index.html,效果如图2-2所示。

image 2024 02 21 09 57 50 063
Figure 2. 图2-2 组件的复用

Vue 组件、根组件、实例的区别

在一般情况下,我们使用 createApp 创建的叫作应用(根)实例,然后调用 mount 方法得到的叫作根组件,而使用 app.component() 方法创建的叫作子组件,组件也可以叫作组件实例,概念上它们的区别并不大。一个 Vue 应用由一个根组件和多个子组件组成,它们之间的关系和区别主要是:

  • 根组件也是组件,只是根组件需要应用实例挂载之后得到,可以看作是一个实例化的过程。

  • 创建子组件需要指定组件的名称,第二个对象参数和创建根组件时基本一致。

  • 子组件是可复用的。一个组件被创建好之后,就可以被用在任何地方。所以子组件的 data 属性需要一个函数 function,以保证组件无论被复用了多少次,组件中的 data 数据都是相互隔离、互不影响的

Vue 3 中,组件的 data 必须是一个函数,返回一个对象。这是为了确保每个组件实例拥有独立的状态,避免多个实例共享同一份数据。

在一般情况下,Vue 中的组件是相互嵌套的,可以看作是一个树结构,每个组件可以引用多个其他组件,而其他组件又可以引用另外一些组件,但是它们有一个共同的根组件,这就是组件树。

全局组件和局部组件

Vue 中,组件又可以分成 全局组件局部组件。之前在代码中直接使用 app.component() 创建的组件为全局组件,全局组件无须特意指定挂载到哪个实例上,可以在需要时直接在组件中使用,但是需要注意的是,全局组件必须在根应用实例挂载前定义才行,否则将无法被使用该根组件的应用找到,就像在 2.1.3 节的代码中,要写在 app.mount('#app') 之前,否则无法找到这个组件。

全局组件可以在任意的 Vue 组件中使用,也就意味着只要注册了全局组件,无论是否被引用,它在整个代码逻辑中都可见。而局部组件则表示指定它被某个组件所引用,或者说局部组件只在当前注册的这个组件中使用。创建局部组件如示例代码2-1-5所示。

// 局部组件
<div id="app">
    {{msg}}
    <inner-component></inner-component>
</div>
const app = Vue.createApp({
    data() {
        return {
            msg: "hello inner"
        }
    },
    components: {  // 可设置多个
        'inner-component': {
            template: '<h2>inner component</h2>'
        }
    }
}).mount('#app')

上面的代码中,inner-component 是一个局部组件,它只有一个简单的 template 属性,在使用者的组件中可以通过 components 将局部组件挂载进去,注意这里是 components(复数),而不是 component,因为可能有多个局部组件。这个局部组件只能被当前 app 的根组件使用。为了组件复用的效果,也可以将组件单独抽离出来,如示例代码2-1-6所示。

const myComponenta = {
    template: '<h2>{{str}}</h2>',
    data() {
        return {
            str: 'inner a'
        }
    }
}
const app = Vue.createApp({
    components: { // 根组件中复用组件
        'my-component-a': myComponenta
    }
})

app.component('button-component', {
    data() {
        return {
            str: 'btn'
        }
    },
    components: {
        'my-component-a': myComponenta
    },
    template: '<my-component-a></my-component-a>'
})

app.mount('#app')

在上面的代码中定义了局部组件的配置项 myComponenta,然后在根组件 app 和全局组件 <button-component> 中分别复用了使用配置项 myComponenta 的局部组件 <my-component-a>

组件方法和事件的交互操作

Vue 中可以使用 methods 为每个组件添加方法,然后可以通过 this.xxx() 来调用。下面通过一个单击事件的交互操作来演示如何使用 methods,如示例代码2-1-7所示。

示例代码2-1-7 组件方法 methods 的使用
<div id="app">
    <h2 @click="clickCallback">{{msg}}</h2>
</div>

Vue.createApp({
    data(){
        return {msg: "hello inner"}
    },
    methods:{
        clickCallback(){
            alert("click")
        }
    }
})

在组件或者实例中,methods 接收一个对象,对象内部可以设置方法,并且可以设置多个。在上面的代码段中,clickCallback 是方法名。

在模板中通过设置 @click="clickCallback" 表示为 <h2> 绑定了一个 click 事件,回调方法是 clickCallback,当单击发生时,会自动从 methods 中寻找 clickCallback 这个方法,并且触发它。

同理,可以设置另一个方法,同时在 clickCallback 中使用 this.xxx() 去调用,如示例代码2-1-8所示。

示例代码2-1-8 调用methods中的方法
Vue.createApp({
    data() {
        return {msg: "hello inner"}
    },
    methods:{
        clickCallback(){
            alert("click")
            this.foo()
        },
        foo(){
            alert("foo")
        }
    }
}).mount("#app")

在了解了组件方法 methods 的用法之后,下面借助 methods 通过一个计数器的例子来演示 Vue 中的事件和 DOM 交互操作的用法,如示例代码 2-1-9 所示。

<div id="counter">
    <my-component></my-component>
</div>

const myComponent = {
    template: '<h2 @click="clickCallback">点击{{num}}</h2>',
    data(){
        return {
            num: 0
        }
    },
    methods: {
        clickCallback(){
            this.num++
        }
    }
}
Vue.createApp({
    components:{
        myComponent: myComponent
    }
}).mount('#counter')

在这段代码中使用了局部组件进行演示,当单击 <h2> 时,会触发 clickCallback 回调方法,在回调方法内对当前 data 中的 num 值进行了加 1 自增,num 通过插值表达式 {{num}} 在页面中显示出来。我们会发现,每单击一次,页面上的 num 就增加 1,这就是 Vue 中响应式的体现,即当一个对象变化时,能够被实时检测到,并且实时修改结果。只有有了响应式,才能有双向绑定,这也是 Vue 中双向绑定时 Model 影响 DOM 的具体体现。在后续的章节中会讲解 DOM 影响 Model 的情况。

本小节讲解了 Vue 中基本的组件用法,使用了 createAppdatatemplatemethods 等属性和方法,这些基础内容对我们后续的学习有很大帮助,当然使用插值表达式 {{msg}}、指令 @click、生命周期等相关的用法还有很多,后面会进行深入详细的讲解。

单文件组件

Vue.js 的组件化是指每个组件控制一块用户界面的显示和用户的交互操作,每个组件都有自己的职能,代码在自己的模块内互相不影响,这是使用 Vue.js 的一大优势。

在一个有很多组件的项目中,如果想要达到组件复用,就可能需要使用 app.component() 来定义多个全局组件,或者定义多个局部组件,然后在组件中互相调用它们,但是前提是所有组件定义和引用的代码都必须在一个上下文对象中,或者说是写在一个 JavaScript 文件中,维护效率很低,这不符合前端工程化的思想。这样的写法有以下几点不足:

  • 全局定义(Global definitions):强制要求每个 component 中的命名不得重复。

  • 字符串模板(String templates):缺乏语法高亮显示功能,在 HTML 有多行时,需要用到丑陋的 “\” 或者 “+” 来拼接字符串。

  • 不支持 CSS(No CSS support):意味着当 HTML 和 JavaScript 组件化时,CSS 只能写在一个文件里,没法突出组件化的优点。

  • 没有构建步骤(No build step):在当前比较流行的前端工程化中,如果一个项目没有构建步骤,开发起来将会变得异常麻烦,简单地使用 app.component() 来定义组件是无法集成构建功能的。

文件扩展名为 .vue 的单文件组件(Single File Components, SFC)为以上所有问题提供了解决方法,并且还可以使用 WebpackRollup 等模块打包工具。该特性带来的好处是,对于项目所需要的众多组件进行文件化管理,再通过压缩工具和基本的封装工具处理之后,最终得到的可能只有一个文件,这极大地减少了对于网络请求多个文件带来的文件缓存或延时问题。

下面是一个单文件组件 index.vue 的例子,如示例代码2-1-10所示。

<template>
    <div class="box">
        {{msg}}
    </div>
</template>
<script>
module.exports = {
    name: 'single',
    data () {
        return {
            msg: 'Single File Components'
        }
    }
}
</script>
<style scoped>
    .box {
    color: #000;
}
</style>

在上面的代码中,组件的模板代码被抽离到一起,使用 <template> 标签包裹;组件的脚本代码被抽离到一起,使用 <script> 标签包裹;组件的样式代码被抽离到一起,使用 <style> 标签包裹。这使得组件 UI 样式和交互操作的代码可以写在一个文件内,方便了维护和管理。

当然,这个文件是无法被浏览器直接解析的,因而需要通过构建步骤把这些文件编译并打包成浏览器可以识别的 JavaScriptCSS,例如 Webpackvue-loader,对于 <template> 中的代码,会被解析成 Vuerender 方法中的虚拟 DOM 对应的 JavaScript 代码,<script> 中的代码会被解析成 Vue 组件的配置对应的 JavaScript 代码,<style> 中的内容会被单独抽离出来,在组件加载时插入 HTML 页面中。

当然,对于 <style> 标签,可以配置一些属性来提供比较实用的功能,scoped 属性表示当前 <style> 中的样式代码只会对当前的单文件组件生效,这样即使多个单文件组件被打包到一起,也不会互相影响。同时,<style> 标签也提供了 lang 属性,可以用来启用 scssless,代码如下:

<style scoped lang='less'></style>
<style scoped lang='scss'></style>

<style> 标签启用了 scoped 后,如果想要在样式代码中写一些样式来影响非当前组件所产生的 DOM 元素,可以采用深度选择器 :deep(),代码如下:

// 局部组件<aButton>
const aButton = {
    template: '<div class="a-button"></div>',
}
<template>
    <div class="content">
        <aButton />
        <a-button>
</template>
<script>
    module.exports = {
    name: 'single',
    components:{
        aButton:aButton
    }
}
</script>
<style scoped>
    .content :deep(.a-button) {
    /* ... */
}
</style>

上面的代码中,.a-button 这个 class 的样式可以通过父组件 single 中的 <style> 来设置。

<script> 标签可以标识当前使用的语言引擎,以便进行预处理,最常见的就是在 <script> 中使用 lang 属性来声明 TypeScript,代码如下:

<script lang="ts">
    // 使用 TypeScript
</script>

如果想将 *.vue 组件拆分为多个文件,<template><style><script> 都可以使用 src 属性来引入外部的文件作为语言块,代码如下:

<template src="./template.html"></template>
<style src="./style.css"></style>
<script src="./script.js"></script>

注意 src 引入所需遵循的路径解析规则与构建工具(例如 Webpack 模块)一致,即:

  • 相对路径需要以 ./ 开头。

  • 可以直接从 node_modules 依赖中引入资源。

直接引入 node_modules 的资源,代码如下:

<!-- 从已安装的 "todomvc-app-css" npm 包中引入文件 -->
<style src="todomvc-app-css/index.css">

最后,对于 <script> 标签,在 Vue 3 中引入了 setup 属性,当配置之后,就相当于可以在 <script> 标签内部直接写 Composition API 中的 setup() 方法中的代码,当然最终还是会在打包时被编译成对应的 JavaScript 代码,但是在开发阶段就显得简洁和便利了,我们会在后面的章节深入讲解 setup() 方法。

正因为 Vue.js 有了单文件组件,才能将其和构建工具(Webpack 等)结合起来,使得 Vue.js 项目不单单是简单的静态资源查看,而是可以集成更多文件预处理功能,这些功能改变了传统的前端开发模式,更能体现出前端工程化的特性,目前大部分 Vue.js 项目都会采用单文件组件。