# 组件化

  根据封装的思想,把页面上可复用的 UI 结构封装为组件,从而方便项目的开发与维护。简单的说就是提高 UI 结构的模块化、复用性

一个 Vue 应用由一个通过 new Vue 创建的根 Vue 实例(根组件),以及可选的嵌套的、可复用的组件树组成。

  • 单页面应用(组件定义和注册

单文件组件的文件名建议采用小写 start-rate,与组件名称保持一致;若使用 StartRate :需要脚手架环境支持解析成 start-rate
组件标签尽量用双标签<comp-name></comp-name>,若使用单标签<comp-name/>:需要脚手架环境支持

提示

  全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生

  html 中是不区分大小写的

五星好评-案例 1
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script src="https://unpkg.com/vue@2/dist/vue.js"></script>
    <link rel="stylesheet" href="http://at.alicdn.com/t/c/font_3977208_ylcuq0nphol.css" />
    <style>
      i {
        color: red;
      }
    </style>
  </head>

  <body>
    <div id="app">
      <comp-name></comp-name>
    </div>

    <template id="startTemplate">
      <div class="star-rate">
        <span>{{ item }}:</span>
        <i class="iconfont icon-star-fill"></i>
        <i class="iconfont icon-star-fill"></i>
        <i class="iconfont icon-star-fill"></i>
        <i class="iconfont icon-star-fill"></i>
        <i class="iconfont icon-star"></i>
      </div>
    </template>

    <script type="text/JavaScript">
      // 一、组件定义
      // 完整写法
      // var StartRate = Vue.extend({ /* ... */ })
      // 简写
      // var StartRate = { /* ... */ }
      var StartRate = {
      //   template: `<div class="star-rate">
      //   <span>{{ item }}:</span>
      //   <i class="iconfont icon-star-fill"></i>
      //   <i class="iconfont icon-star-fill"></i>
      //   <i class="iconfont icon-star-fill"></i>
      //   <i class="iconfont icon-star-fill"></i>
      //   <i class="iconfont icon-star"></i>
      // </div>`,
        template: "#startTemplate",
        data: function () {
          return {
            item: '服务态度'
          }
        }
      }

      // 二、组件注册
      // 方式1:全局注册
      // Vue.component('comp-name', StartRate)

      new Vue({
        el: '#app',

        // 方式2:局部注册
        components: {
          'comp-name': StartRate
        }
      })
    </script>
  </body>
</html>
五星好评-案例 2
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
    <script src="https://unpkg.com/http-vue-loader"></script>
    <link rel="stylesheet" href="http://at.alicdn.com/t/c/font_3977208_ylcuq0nphol.css" />
  </head>

  <body>
    <div id="app">
      <comp-name></comp-name>
    </div>

    <script type="text/JavaScript">
      Vue.use(httpVueLoader);

      // 方式1:全局注册
      // Vue.component('comp-name', StartRate)

      new Vue({
        el: '#app',

        // 方式2:局部注册
        components: {
          'comp-name': 'url:./components/start-rate.vue'
        }
      })
    </script>
  </body>
</html>
<template>
  <div class="star-rate">
    <span>{{ item }}:</span>
    <i class="iconfont icon-star-fill"></i>
    <i class="iconfont icon-star-fill"></i>
    <i class="iconfont icon-star-fill"></i>
    <i class="iconfont icon-star-fill"></i>
    <i class="iconfont icon-star"></i>
  </div>
</template>

<script>
// 注意这里不要写成 export default
// 原因:浏览器不能直接支持ES6 Modules 语法
module.exports = {
  data: function () {
    return {
      item: '服务态度'
    }
  }
}
</script>

<style>
i {
  color: red;
}
</style>
  • vue 应用(组件树

开发者工具中的默认采用组件注册时的名字,如果想自定义可以在组件定义的时候使用 name 属性进行覆盖

vue-cli 脚手架案例(非单文件版)
<div id="root">
  <app></app>
</div>

<script type="text/JavaScript">
  // components文件夹
  const HelloWorld = Vue.extend({
    name: 'HelloWorld',
    template: `
      <div class="hello">
        <p>{{ msg }}</p>
      </div>
    `,
    data() {
      return {
        msg: "Welcome to Your Vue.js App"
      }
    },
  })

  // views文件夹
  const HomeView = Vue.extend({
    name: 'HomeView',
    template: `
      <div class="home">
        <h3>HomeView组件内容</h3>

        子组件内容如下:
        <hello-world></hello-world>
      </div>
    `,
    components: {
      "hello-world": HelloWorld
    }
  })

  const AboutWorld = Vue.extend({
    name: 'AboutWorld',
    template: `
      <div class="home">
        <h3>AboutWorld组件内容</h3>
      </div>
    `
  })

  // 根组件✍(根Vue实例)
  const app = Vue.extend({
    name: 'app',
    template: `
      <div>
        <home-view></home-view>
        <about-world></about-world>
      </div>
    `,
    components: {
      "home-view": HomeView,
      "about-world": AboutWorld
    }
  })

  new Vue({
    el: '#root',
    components: {
      app
    }
  });
</script>

# 一、vue 组件

提示

  通过上面的介绍,我们知道了在 vue 中只有一个根 Vue 实例,所以定义组件时是不能再使用 el属性的。

# 1、vue 实例对象(vm)

  在 Vue.js 框架中,我们可以使用它提供的构造函数Vue,new 了一个实例对象vm,通过打印我们可以发现:

  • $开头的公共方法和函数
  • _开头的私有方法和函数
  • 有浅颜色的 Vue 原型对象vm.__proto__ 的继承属性和方法

简单的说,vm 身上的我们可以用,vm 原型身上的我们也可以用

console.log(vm)

// 其中,我们要知道的是其中的`vm.__proto__.$mount`方法
  • data 和 el 的两种书写方式
在组件中为什么要写成函数式

  因为组件是块砖,哪里需要哪里搬,这样容易造成对象引用的发生,从而导致数据混乱

根组件就不需要担心这个问题,因为它只有一个

  • 问题
// 组件数据
let person = {
  age: 22,
  name: 'lencamo'
}

// 使用组件
const a = person
const b = person

a.age = 21
console.log(b.age) // 21
  • 解决办法

对象没有作用域,而函数是有作用域的

// 组件数据
function person() {
  return {
    age: 22,
    name: 'lencamo'
  }
}

// 使用数据的组件
const a = person()
const b = person()

a.age = 21
console.log(b.age) // 22
<div id="app">{{Msg}}</div>

<div id="app">{{$options}}</div>
<div id="app">{{$mount}}</div>

<script>
  // 写法1
  // new Vue({
  //   el: '#app',
  //   data: {
  //     Msg: 'lencamo'
  //   }
  // })

  // 写法2
  const vm = new Vue({
    // 在.vue文件组件中,必须写成函数式🤔(不然会报错)
    // data: function{}
    data() {
      return {
        Msg: 'lencamo'
      }
    }
  })
  vm.$mount('#app')
</script>
插件挂载

  通过 vue-cli 自动创建常规的 Vue 应用时,我们发现:

  • main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false

// 根 Vue 实例
new Vue({
  // 将vue插件挂载到vue实例上
  // 【这样我们就可以直接在组件✨中使用$router、$store对象】
  router,
  store,

  // App根组件(和其内部的子组件组成组件树)
  render: (h) => h(App)
}).$mount('#app')

注意事项:

所有的 Vue 组件都是 Vue 实例,并且接受相同的选项对象 (一些根实例特有的选项除外)

  所以,在非 vue 组件中使用 vue 插件时就需要手动引入:

import $store from '@/store'

# 2、组件实例对象(vc)

  VueComponent 是一个构造函数,是我们定义组件的返回值:

提示

  我们调用组件<about-world></about-world>时,Vue 会帮我们执行 new VueComponent(options)

// const AboutWorld = Vue.extend(options)
const AboutWorld = Vue.extend({
  name: 'AboutWorld',
  template: `
    <div class="home">
      <h3>AboutWorld组件内容</h3>
    </div>
  `,
})

console.log(AboutWorld)
// console.dir(AboutWorld)

// 结果:
ƒ VueComponent(options) {
              this._init(options);
          }

# ① 返回值角度

  其实我们通过查看源码也可以看到:

VueComponent 是我们定义组件时,使用 Vue.extend 是生成的。

  • vue.js
Vue.extend = function (extendOptions) {
  // ……

  var Sub = function VueComponent(options) {
    this._init(options)
  }

  // ……

  return Sub
}

# ② 构造函数角度

  通过打印,我们发现其 VueComponent 的实例对象vc,和我们前面学到 Vue 的实例对象vm是一样的结构。

也就是说:在 vue 中我们可以先分为 vue 实例对象 和 组件实例对象

const AboutWorld = Vue.extend({
  name: 'AboutWorld',
  template: `
    <div class="home">
      <button @click="showThis">打印this信息</button>
      <h3>{{msg}}</h3>
    </div>
  `,
  data() {
    return {
      msg: 'AboutWorld组件内容'
    }
  },
  methods: {
    showThis() {
      console.log('组件:', this) // 结果✍:VueComponent {}
    }
  }
})

 那 vue 实例对象 和 组件实例对象有什么关系呢?

VueComponent.prototype.__proto__ == Vue.prototype

const vm = new Vue({
  el: '#root',
  components: {
    app
  }
})

console.log('vue:', vm)

console.log(AboutWorld.prototype.__proto__ == Vue.prototype) // true

  所以,回顾 js 高级部分的原型链内容,我们就知道 VueComponent 的实例对象 vc 使用了 Vue 原型对象上的方法和属性。

# 3、路径识别插件

  • Path Autocomplete

@ 路径插件识别配置:

只有 vscode 仅单独 ✨ 打开一个 vue 项目时,该插件才会生效。

{
  // 导入文件时是否可以携带文件的扩展名
  "path-autocomplete.extensionOnImport": true,
  // 配置'@'的路径提示
  "path-autocomplete.pathMappings": {
    "@": "${folder}/src"
  }
}

回顾:

① 若使用 webpack,则需要手动配置

  • webpack-config.js
module.exports = {
  resolve: {
    alias: {
      '@': path.join(__dirname, './src/')
    }
  }
}

② 若使用 vue-cli,则已经自动生成

提示

  vue.config.js 中的 alias 配置是为了 webpack 打包是能够正确的解析,而 jsconfig.json 中 alias 配置是为了让 vscode 等编辑器有智能提示(代替上面 Path Autocomplete 的作用)

  • 原先:vue.config.js
const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
  // ……
  configureWebpack: {
    resolve: {
      alias: {
        '@/*': ['src/*']
      }
    }
  }
})
  • 现在:jsconfig.json

jsonfig.json 是给编辑器 vscode 等识别的(方便配置别名后 vscode 能够有路径提示)

{
  "compilerOptions": {
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
  }
}

# 二、单文件组件

官方

单文件组件 (opens new window)(里面介绍的非常详细)

  官方介绍了,前面使用非单文件组件的缺点,并提供了文件扩展名为 .vueSFC (single-file components)单文件组件 为以上所有问题提供了解决方法。

  并且还可以使用 webpack 或 Browserify 等构建工具构建项目,官方更是提供了 vue-cli 这个脚手架供我们使用

vue 是一个支持组件化开发的前端框架,其中 vue 规定其组件的后缀名为 .vue(单文件组件)。

npm install @vue/cli -g

vue create project-name

# 1、基础说明

# ① 单文件组件命名

  • school.vue 、 my-school.vue
  • School.vue 、 MySchool.vue

# ② 开发应用

简单的 todo 应用 (opens new window)

# 2、.vue 文件结构

  每个.vue 组件由三部分组成:

  • template —— 结构
  • script —— 数据、行为(可选)
  • style —— 样式(可选)
非单文件组件
<head>
  <meta charset="UTF-8" />
  <script src="https://unpkg.com/vue@2/dist/vue.js"></script>
  <link rel="stylesheet" href="http://at.alicdn.com/t/c/font_3977208_ylcuq0nphol.css" />
  <style>
    i {
      color: red;
    }
  </style>
</head>

<body>
  <div id="app">
    <comp-name></comp-name>
  </div>

  <script type="text/JavaScript">
    var StartRate = Vue.extend({
      template: `<div class="star-rate">
      <span>{{ item }}:</span>
      <i class="iconfont icon-star-fill"></i>
      <i class="iconfont icon-star-fill"></i>
      <i class="iconfont icon-star-fill"></i>
      <i class="iconfont icon-star-fill"></i>
      <i class="iconfont icon-star"></i>
    </div>`,
      data: function () {
        return {
          item: '服务态度'
        }
      }
    })

    new Vue({
      el: '#app',
      components: {
        'comp-name': StartRate
      }
    })
  </script>
</body>
单文件组件
  • StarRate.vue
<template>
  <div class="star-rate">
    <span>{{ item }}:</span>
    <i class="iconfont icon-star-fill"></i>
    <i class="iconfont icon-star-fill"></i>
    <i class="iconfont icon-star-fill"></i>
    <i class="iconfont icon-star-fill"></i>
    <i class="iconfont icon-star"></i>
  </div>
</template>

<script>
  // export default Vue.extend({
  export default {
    name: 'StarRate'
    data: function () {
      return {
        item: '服务态度'
      }
    }
  }
  // })
</script>

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

# 3、.vue 文件解析

  前面我们已经知道了,.vue 文件是不能直接使用的需要借助构造工具帮我们把它编译解析为.js、.css 文件。

# ① .vue 文件使用

  • 回顾前面:

使用 http-vue-loader (opens new window)(.html 文件中引入 .vue 文件)

  • 构造工具

常见的可以使用 vue-cli 脚手架 (opens new window)等启动 vue 项目

# ② vue-cli 脚手架

  在前面组件化的学习内容时,写了一个非单文件的 vue-cli 脚手架案例,现在我们可以真正意义上的使用 vue-cli 来完成 vue-cli 脚手架案例。

vue-cli 脚手架案例 (opens new window)(单文件 ✍ 版) ----- 后面所有的组件小案例都是基于它改造的。

  vue 脚手架创建的项目,通过入口文件main.jsApp.vue渲染到index.html的指定区域中。

其本质就是使用:

  • babel:用于解析 ES6+语法
  • webpack:处理各种不同类型的文件

  vue-cli 将 webpack 的默认配置隐藏了,我们在项目是无法找到 webpack.config.js 文件的,但我们可以通过命令输出该文件:

# 打印项目的webpack默认配置
vue inspect > cutput.js

提示

  综上,我们即可以直接在vue.config.js中进行使用vue-cli 配置项 (opens new window),也可以使用一些webpack 配置项 (opens new window)来覆盖默认配置。

# 4、render 选项 ✨

  在上面的案例中,虽然我们成功启动了项目(使用了默认 main.js),但该文件中的 render 是干什么的呢?

  • 传统的做法
// 引入完整版的vue 🤔
// runtime + compile(vue-loader)
import Vue from 'vue/dist/vue.js'

// import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  el: '#app',
  // template: `<App></App>`,
  template: `<h2>hello,world!</h2>`
  components: {
    App
  }
})
  • vue-cli 的做法
// 引入仅只包含运行时版的vue
// runtime
import Vue from 'vue'

import App from './App.vue'

Vue.config.productionTip = false

// vue-cli的做法
new Vue({
  render: (h) => h(App)
}).$mount('#app')

main.js 是如何把 App.vue 渲染到 index.html 的指定区域中?

# ① render 函数分析

官网:render (opens new window)

  在 vue 组件中,如果 render 选项存在,则 Vue 构造函数不会从 template 选项或通过 el 选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。

所以,按照 vue-cli 的做法,其 main.js 中并没有template 选项。其他组件用到的 template 标签,vue 也提供了vue-template-compiler包进行解决。

  render 渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode

import Vue from 'vue'
import App from './App.vue'

new Vue({
  render(createElement) {
    // 渲染DOM元素
    return createElement('h1', 'render函数创建并渲染了该vnode')
    // 渲染组件
    // return createElement(App)
  }
}).$mount('#app')

# ② vue 设计缘由

  我们用的是 Vue 核心内容,是无论开发还是生产环境存在;而模板编译仅仅是一个过程,需要是时候用,不需要的时候不用。所以引入仅只包含运行时版的 vue 就可以了。

# 5、组件分类

  使用 vue 组件,可以让他们之间产生父子关系兄弟关系

vue 组件被封装好后,各组件彼此之间是相互独立的。

  同时,我们还可以将子组件分为私有子组件 和 公有子组件:

# ① 私有子组件

  在前面单文件组件中使用的都是私有子组件。

# ② 全局子组件

  在 vue 项目的 main.js 入口文件中,可以通过 Vue.component()方法 注册的全局子组件

  • main.js
// 1、导入全局组件
import Comment from '@/components/Comment.vue'
// 2、注册全局组件
Vue.component('Comment', Comment)
  • 任意组件
<template>
  <div>
    <!-- 3、嵌入全局组件 -->
    <Comment></Comment>
  <div>
</template>

# 6、自动化注册

  可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。我们有时候会把它们称为基础组件,它们会在各个组件中被频繁的用到。

  为了避免很多组件注册组件是出现一个包含基础组件的长列表,我们可以使用 require.context 只全局注册这些非常通用的基础组件。

代码演示

基础组件 之 自动化注册 (opens new window)案例

// Globally register all base components for convenience, because they
// will be used very frequently. Components are registered using the
// PascalCased version of their file name.

import Vue from 'vue'

// https://webpack.js.org/guides/dependency-management/#require-context
const requireComponent = require.context(
  // Look for files in the current directory
  '.',
  // Do not look in subdirectories
  false,
  // Only include "_base-" prefixed .vue files
  /_base-[\w-]+\.vue$/
)

// For each matching file name...
requireComponent.keys().forEach((fileName) => {
  // Get the component config
  const componentConfig = requireComponent(fileName)
  // Get the PascalCase version of the component name
  const componentName = fileName
    // Remove the "./_" from the beginning
    .replace(/^\.\/_/, '')
    // Remove the file extension from the end
    .replace(/\.\w+$/, '')
    // Split up kebabs
    .split('-')
    // Upper case
    .map((kebab) => kebab.charAt(0).toUpperCase() + kebab.slice(1))
    // Concatenated
    .join('')

  // Globally register the component
  Vue.component(componentName, componentConfig.default || componentConfig)
})
更新于 : 8/7/2024, 2:16:31 PM