# 组件化
根据封装的思想,把页面上可复用的 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>
- 使用 http-vue-loader (opens new window)(.html 文件中引入 .vue 文件)
五星好评-案例 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)(里面介绍的非常详细)
官方介绍了,前面使用非单文件组件的缺点,并提供了文件扩展名为 .vue
的 SFC (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
# ② 开发应用
# 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.js把App.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 函数分析
在 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)
})