Skip to content

组件化开发

现在可以说整个的大前端开发都是组件化的天下

无论从三大框架(Vue、React、Angular),还是跨平台方案的Flutter,甚至是移动端都在转向组件化开发,包括小程序的开发也是采用组件化开发的思想。

所以,学习组件化最重要的是它的思想,每个框架或者平台可能实现方法不同,但是思想都是一样的。

我们需要通过组件化的思想来思考整个应用程序:

我们将一个完整的页面分成很多个组件;

每个组件都用于实现页面的一个功能块;

而每一个组件又可以进行细分;

而组件本身又可以在多个地方进行复用;

Vue的组件化

组件化是Vue、React、Angular的核心思想,也是我们后续课程的重点(包括以后实战项目):

前面我们的createApp函数传入了一个对象App,这个对象其实本质上就是一个组件,也是我们应用程序的根组件;

html
<body>

  <div id="app">
    <h2>{{message}}</h2>
  </div>
  
  <script src="../lib/vue.js"></script>
  <script>
    // 1.组件: App组件(根组件) App对象就是根组件
    const App = {
      data() {
        return {
          message: "Hello Vue"
        }
      }
    }

    // 1.创建app
    const app = Vue.createApp(App)

    // 2.挂载app
    app.mount("#app")
  </script>
</body>

组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用;

任何的应用都会被抽象成一颗组件树;

image-20230506195341916

接下来,我们来学习一下在Vue中如何注册一个组件,以及之后如何使用这个注册后的组件

注册组件的方式

如果我们现在有一部分内容(模板、逻辑等),我们希望将这部分内容抽取到一个独立的组件中去维护,这个时候如何注册一个

组件呢?

注册组件分成两种:

全局组件:在任何其他的组件中都可以使用的组件;

局部组件:只有在注册的组件中才能使用的组件;

注册全局组件

我们先来学习一下全局组件的注册:

全局组件需要使用我们全局创建的app来注册组件;

通过component方法传入组件名称、组件对象即可注册一个全局组件了;

之后,我们可以在App组件的template中直接使用这个全局组件:

html
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .product {
      background-color: orange;
    }
  </style>
</head>
<body>

  <div id="app">
    <!-- 1.内容一: -->
    <product-item></product-item>
    
    <!-- 2.内容二: -->
    <product-item></product-item>

    <!-- 3.内容三: -->
    <product-item></product-item>
  </div>


  <!-- 组件product-item的模板 -->
  <template id="item">
    <div class="product">
      <h2>我是商品</h2>
      <div>商品图片</div>
      <div>商品价格: <span>¥9.9</span></div>
      <p>商品描述信息, 9.9秒杀</p>
    </div>
  </template>
  
  <script src="../lib/vue.js"></script>
  <script>
    /*
      1.通过app.component(组件名称, 组件的对象)
      2.在App组件的模板中, 可以直接使用product-item的组件
    */

    // 1.组件: App组件(根组件)
    const App = {}

    // 2.创建app
    const app = Vue.createApp(App)

    // 3.注册一个全局组件
    // product-item全局组件

    // app.component("product-item", {
    //   template: `
    //   <div class="product">
    //     <h2>我是商品</h2>
    //     <div>商品图片</div>
    //     <div>商品价格: <span>¥9.9</span></div>
    //     <p>商品描述信息, 9.9秒杀</p>
    //   </div>
    //   `
    // })
    app.component("product-item", {
      template: "#item"
    })

    // 2.挂载app
    app.mount("#app")
  </script>
</body>
</html>

全局组件的逻辑

当然,我们组件本身也可以有自己的代码逻辑:

比如自己的data、computed、methods等等

html
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .product {
      background-color: orange;
    }
  </style>
</head>
<body>

  <div id="app">
    <!-- <home-nav></home-nav> -->
    <HomeNav></HomeNav>
    <home-nav></home-nav>

    <product-item></product-item>
    <product-item></product-item>
    <product-item></product-item>
  </div>

  <template id="nav">
    <h2>我是应用程序的导航</h2>
  </template>

  <template id="product">
    <div class="product">
      <h2>{{title}}</h2>
      <p>商品描述, 限时折扣, 赶紧抢购</p>
      <p>价格: {{price}}</p>
      <button @click="favarItem">收藏</button>
    </div>
  </template>
  
  <script src="../lib/vue.js"></script>
  <script>
    // 1.创建app
    const app = Vue.createApp({
      // data: option api
      data() {
        return {
          message: "Hello Vue"
        }
      },
    })

    // 2.注册全局组件
    app.component("product-item", {
      template: "#product",
      data() {
        return {
          title: "我是商品Item",
          price: 9.9
        }
      },
      methods: {
        favarItem() {
          console.log("收藏了当前的item")
        }
      }
    })

    app.component("HomeNav", {
      template: "#nav"
    })

    // 2.挂载app
    app.mount("#app")
  </script>
</body>
</html>

组件的名称

在通过app.component注册一个组件的时候,第一个参数是组件的名称,定义组件名的方式有两种:

方式一:使用kebab-case(短横线分割符)

当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my

component-name>;

方式二:使用PascalCase(驼峰标识符)

当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。

也就是说 <my-component-name><MyComponentName> 都是可接受的;

html
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .product {
      background-color: orange;
    }
  </style>
</head>
<body>

  <div id="app">
    <!-- <home-nav></home-nav> -->
    <HomeNav></HomeNav> // 这种写法在html中不生效,但是在.vue文件中生效
    <home-nav></home-nav>

    <product-item></product-item>
    <product-item></product-item>
    <product-item></product-item>
  </div>

  <template id="nav">
    <div>---------------------nav start ------------------------</div>
    <h2>我是应用程序的导航</h2>
    <product-item></product-item>
    <div>---------------------nav end ------------------------</div>
  </template>

  <template id="product">
    <div class="product">
      <h2>{{title}}</h2>
      <p>商品描述, 限时折扣, 赶紧抢购</p>
      <p>价格: {{price}}</p>
      <button @click="favarItem">收藏</button>
    </div>
  </template>
  
  <script src="../lib/vue.js"></script>
  <script>
    // 1.创建app
    const app = Vue.createApp({
      // data: option api
      data() {
        return {
          message: "Hello Vue"
        }
      },
    })

    // 2.注册全局组件
    // 全局组件的特点: 一旦注册成功后, 可以在任意其他组件的template中使用
    app.component("product-item", {
      template: "#product",
      data() {
        return {
          title: "我是商品Item",
          price: 9.9
        }
      },
      methods: {
        favarItem() {
          console.log("收藏了当前的item")
        }
      }
    })

    app.component("HomeNav", {
      template: "#nav"
    })

    // 2.挂载app
    app.mount("#app")
  </script>
</body>
</html>

注册局部组件

全局组件往往是在应用程序一开始就会全局组件完成,那么就意味着如果某些组件我们并没有用到也会一起被注册:

比如我们注册了三个全局组件:ComponentA、ComponentB、ComponentC;

在开发中我们只使用了ComponentA、ComponentB,如果ComponentC没有用到但是我们依然在全局进行了注册,那么

就意味着类似于webpack这种打包工具在打包我们的项目时,我们依然会对其进行打包;

这样最终打包出的JavaScript包就会有关于ComponentC的内容,用户在下载对应的JavaScript时也会增加包的大小;

所以在开发中我们通常使用组件的时候采用的都是局部注册:

局部注册是在我们需要使用到的组件中,通过components属性选项来进行注册;

比如之前的App组件中,我们有data、computed、methods等选项了,事实上还可以有一个components选项;

该components选项对应的是一个对象,对象中的键值对是 组件的名称: 组件对象;

html
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .product {
      background-color: orange;
    }
  </style>
</head>
<body>

  <div id="app">
    <home-nav></home-nav>

    <product-item></product-item>
    <product-item></product-item>
    <product-item></product-item>
  </div>
  
  <template id="product">
    <div class="product">
      <h2>{{title}}</h2>
      <p>商品描述, 限时折扣, 赶紧抢购</p>
      <p>价格: {{price}}</p>
      <button>收藏</button>
    </div>
  </template>

  <template id="nav">
    <div>-------------------- nav start ---------------</div>
    <h1>我是home-nav的组件</h1>
    <product-item></product-item>
    <div>-------------------- nav end ---------------</div>
  </template>

  <script src="../lib/vue.js"></script>
  <script>
    // 1.创建app
    const ProductItem = {
      template: "#product",
      data() {
        return {
          title: "我是product的title",
          price: 9.9
        }
      }
    }

    // 1.1.组件打算在哪里被使用
    const app = Vue.createApp({
      // components: option api
      components: {
        ProductItem,
        HomeNav: {
          template: "#nav",
          components: {
            ProductItem
          }
        }
      },
      // data: option api
      data() {
        return {
          message: "Hello Vue"
        }
      }
    })

    // 2.挂载app
    app.mount("#app")
  </script>
</body>
</html>

Vue的开发模式

目前我们使用vue的过程都是在html文件中,通过template编写自己的模板、脚本逻辑、样式等。

但是随着项目越来越复杂,我们会采用组件化的方式来进行开发:

这就意味着每个组件都会有自己的模板、脚本逻辑、样式等;

当然我们依然可以把它们抽离到单独的js、css文件中,但是它们还是会分离开来;

也包括我们的script是在一个全局的作用域下,很容易出现命名冲突的问题;

并且我们的代码为了适配一些浏览器,必须使用ES5的语法;

在我们编写代码完成之后,依然需要通过工具对代码进行构建、优化;

所以在真实开发中,我们可以通过一个后缀名为 .vue的single-file components (单文件组件)解决,并且可以使用

webpack或者vite或者rollup等构建工具来对其进行处理。

单文件的特点

在这个组件中我们可以获得非常多的特性:

代码的高亮;

ES6、CommonJS的模块化能力;

组件作用域的CSS;

可以使用预处理器来构建更加丰富的组件,比如

TypeScript、Babel、Less、Sass等;

如何支持SFC

如果我们想要使用这一的SFC的.vue文件,比较常见的是两种方式

方式一:使用Vue CLI来创建项目,项目会默认帮助我们配置好所有的配置选项,可以在其中直接使用.vue文件;

方式二:自己使用webpack或rollup或vite这类打包工具,对其进行打包处理;

我们最终,无论是后期我们做项目,还是在公司进行开发,通常都会采用Vue CLI的方式来完成。

VSCode对SFC文件的支持

在前面我们提到过,真实开发中多数情况下我们都是使用SFC( single-file components (单文件组件) )。

我们先说一下VSCode对SFC的支持:

插件一:Vetur,从Vue2开发就一直在使用的VSCode支持Vue的插件;

插件二:Volar,官方推荐的插件;

Vue CLI脚手架

什么是Vue脚手架?

我们前面学习了如何通过webpack配置Vue的开发环境,但是在真实开发中我们不可能每一个项目从头来完成所有的

webpack配置,这样显示开发的效率会大大的降低;

所以在真实开发中,我们通常会使用脚手架来创建一个项目,Vue的项目我们使用的就是Vue的脚手架;

脚手架其实是建筑工程中的一个概念,在我们软件工程中也会将一些帮助我们搭建项目的工具称之为脚手架;

Vue的脚手架就是Vue CLI:

CLI是Command-Line Interface, 翻译为命令行界面;

我们可以通过CLI选择项目的配置和创建出我们的项目;

Vue CLI已经内置了webpack相关的配置,我们不需要从零来配置;

Vue CLI 安装和使用

安装Vue CLI(目前最新的版本是v5.0.8)

我们是进行全局安装,这样在任何时候都可以通过vue的命令来创建项目;

javascript
npm install @vue/cli -g

升级Vue CLI:

如果是比较旧的版本,可以通过下面的命令来升级

javascript
npm update @vue/cli -g

通过Vue的命令来创建项目

javascript
vue create 项目的名称

Vue创建项目的方式一:vue create

使用vue create创建出来的项目使用的打包工具是webpack

image-20230507145520500

Vue创建项目的方式二:vite工具

执行下面的命令

javascript
npm init vue@latest

执行上面这个命令会做两件事情

1.安装一个本地工具create-vue

2.使用create-vue创建一个vue的项目

上面这种方式创建出来的项目,底层是基于vite进行打包

image-20230507175858516

项目的目录结构

image-20230507151242690

到目前为止,创建出来的项目有2个文件我们是不认识的,一个是.browserslistrc,另外一个是jsconfig.json,

.browserslistrc是用来设置目标浏览器,jsconfig.json是给vscode用的,可以给我们更好的代码提示。

src/main.js

javascript
import { createApp } from 'vue/dist/vue.esm-bundler' // 这里要使用这个库来解析template模板

const App = {
  template: `<h2>{{title}}</h2>`,
  data() {
    return {
      title: "我也是标题"
    }
  }
}

const app = createApp(App)

app.mount('#app')

上面这种写法又回到了之前的html中的写法

那么我们可以使用.vue文件,进行分离

src/main.js

javascript
import { createApp } from 'vue/dist/vue.esm-bundler'

import App from './components/App.vue' // 这里引入
import ProductItem from "./components/ProductItem.vue"

const app = createApp(App)

// 全局注册
app.component("product-item", ProductItem)

app.mount('#app')

components/App.vue

vue
<template>
  <h2>{{title}}</h2>
  <h2>当前计数: {{counter}}</h2>
  <button @click="increment">+1</button>
  <button @click="decrement">-1</button>

  <product-item></product-item>
</template>

<script>
import ProductItem from './ProductItem.vue'

export default { // 这里要导出对象,因为在main.js中导入了,一个组件就是一个对象
  components: {
    ProductItem // 局部注册,先导入,再注册,然后在template中即可使用
  },
  data() {
    return {
      title: "我还是标题",
      counter: 0
    }
  },
  methods: {
    increment() {
      this.counter++
    },
    decrement() {
      this.counter--
    }
  }
}
</script>

<style>
  h2 {
    color: red;
  }
</style>

components/ProductItem.vue

vue
<template>
  <div class="product">
    <h2>我是商品标题</h2>
    <p>我是商品描述, 9.9秒杀</p>
    <div>价格: {{price}}</div>
  </div>
</template>

<script>

export default {
  data() {
    return {
      price: 9.9
    }
  }
}

</script>

<style>
</style>

jsconfig.json文件的作用

作用:给VSCode来进行读取, VSCode在读取到其中的内容时, 给我们的代码更加友好的提示

开发中最常用的其中一个就是工具函数utils,项目一复杂层级就会比较深,其他js文件进行引用的时候路径不太好找

比如src/utils/math.js路径下有个工具函数想在src/utils/abc/cba/nba/index.js中使用,如果没有配置别名需要根据相对路径进行查找,不太方便,那么可以进行别名的配置。

如何配置?

在vue.config.js文件中进行配置,增加一个configureWebpack属性,里面配置和之前学的webpack一样

javascript
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  configureWebpack: {
    resolve: {
      // 配置路径别名
      // @是已经配置好的路径别名: 对应的是src路径
      alias: {
        "utils": "@/utils" 
      }
    }
  }
})

配置好之后就可以在index.js文件使用了,引入的时候就可以下面这样写

image-20230507165624734

但是,到这里会发现没有代码提示,需要自己手动敲utils/math,这个时候jsconfig.json就派上用场了

json
{
  "compilerOptions": {
    "target": "es5", // 最终打包出来ES5代码
    "module": "esnext", // 使用ES模块化规范
    "baseUrl": "./", // 这样配置,src是相对于当前目录的src
    "moduleResolution": "node", // 模块查找规则根据node进行查找
    "paths": {
      "@/*": [
        "src/*"
      ],
      // 增加utils的路径配置,引入的时候vscode会有代码提示
      "utils/*": [
        "src/utils/*"
      ]
    },
    "lib": [ // 使用下面这些库会有的对应代码提示
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  }
}

上面配置完utils的路径之后,vscode就会有代码提示

Vue不同版本对template的处理

在src/main.js中我们有两种方式编写Vue的代码,

第一种:不使用.vue文件

javascript
import { createApp } from 'vue/dist/vue.esm-bundler' // compile代码

const App = {
  template: `<h2>Hello Vue3 App</h2>`,
  data() {
    return {}
  }
}

createApp(App).mount('#app')

第二种:使用.vue文件

javascript
import { createApp } from 'vue' // 不支持template选项

import App from './App.vue'
createApp(App).mount('#app')

但是不管使用哪种方式,要将我们编写的代码交给浏览器解析并最终渲染都需要经过下面的过程

DOM元素,比如div元素,经过compile编译转换成createVNode函数,再调用createVNode函数就会变成vnode,一个个vnode就会组成虚拟DOM,再将虚拟DOM转换成真实的DOM

我们了解了整个过程,就知道为什么不同版本(这里不同版本指的是上面两种不同的写法)的Vue引入不同的库了

将模板转换成createVNode函数都需要经过compile编译,默认是vue的源码来完成,使用第一种方法,因为vue中的源码,没有进行compile编译,所以需要指定使用vue/dist/vue.esm-bundler进行compile编译;而第二种方法使用了vue-loader,它所做的事情就包含了将模板转换成createVNode函数

Vue文件style自己的作用域

vue
<template>
  <div class="app">
    <h2 class="title">我是App.vue中的h2元素</h2>

    <!-- AppHeader -->
    <AppHeader></AppHeader>
  </div>
</template>

<script>

import AppHeader from "./components/AppHeader"

export default {
  name: 'App',
  components: {
    AppHeader
  }
}
</script>
// 加scoped就会形成作用域,只在当前.vue文件中生效
<style scoped>
  .title {
    color: red;
  }
</style>

Vue CLI的运行原理

image-20230507151712831