1.1 第一个组件
1.1.1 官方 demo
<div id="components-demo">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
<script>
// 定义一个名为 button-counter 的新组件
Vue.component("button-counter", {
data: function () {
return {
count: 0,
};
},
template:
'<button v-on:click="count++">You clicked me {{ count }} times.</button>',
});
</script>
<script>
new Vue({ el: "#components-demo" });
</script>
说明:
- 组件定义是由
component
完成,第一个参数为组件的名称,第二个参数为组件相关的数据、方法等; - 组件中只能包含一个根元素,如果有多个元素,则可使用一个元素包裹起来;
- 组件的使用方式和 html 标签相同,尖括号包裹组件名即可;
- 根实例中 el 指定 DOM 实例挂载位置,关于挂载和实例数据见后续笔记;
- 组件可以复用,但需要注意组件内的 data 必须是一个函数,这样每个实例的数据都是 data 返回对象的独立拷贝,否则实例数据会在组件内共享。
1.1.2 vue-cli 创建工程
下面笔记的代码使用 vue-cli 创建工程,不再使用单个 html,起步:
npm install -g @vue/cli # 安装 vue-cli 新版
vue ui # 启动 vue 项目 web 管理工具,ui 方式选择插件、并 install 到 node_modules 内。
npm run serve # 启动项目,实际脚本定义在 package.json 的 scripts 内。
下面使用的版本是:
- vue: 2.6.11 (package.json 中)
- vue-cli: 4.5.0 (命令 vue -V)
注意:对于这个空白项目,启动后会提示 Access to XMLHttpRequest at ‘http://10.20.19.9:8080/sockjs-node/info?t=1598950194338’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.,解决方案是在项目跟目录下新建 vue.config.js
写入:
module.exports = {
devServer: {
host: 'localhost',
port: 8080
}
}
关于 vue.config.js 的官方文档: 配置参考 – 全局 CLI 配置#devserver,webpack – DevServer#devserverhost
为了不每个这样的简单项目都创建一次,可以创建多个 App.vue,如: App-1.vue, App-2.vue,然后在 main.js 中
import App from './App-3.vue'
改为自己的需要运行的 App vue 文件即可。
在 vue-cli 创建的工程中改写上面官网的 demo:
- 在 components 下创建文件
ButtonClickCounter.vue
:<template> <button v-on:click="count++">You clicked me {{ count }} times.</button> </template> <script> export default { name: "button-click-cnt", // 有 name 选项,错误日志会更易读,且 vue-devtool 调试有此语义信息 data: function () { return { count: 0, }; }, }; </script>
- 在
App-2.vue
内通过 components 方式注册组件(局部注册):<template> <div id="app"> <!-- <ButtonClickCounter/> --> <button-click-counter/> </div> </template> <script> import ButtonClickCounter from "./components/ButtonClickCounter.vue"; export default { name: "App", components: { // ButtonClickCounter, // 不设置组件名,默认使用组件文件名的 PascalCase 以及 kebab-case "button-click-counter": ButtonClickCounter, // 设置组件名,设置后默认的不再有效 }, }; </script>
npm run serve
启动项目,可看到效果。
1.2 组件的组织与注册
实际中通常会有多个组件、以及组件之间相互依赖,形成组件树形式。
1.2.1 组件名
组件名相关规则,例子见上面 vue-cli 创建的工程:
- 组件文件名建议使用 PascalCase(单词首字母大写命名),import 后为文件名,from 为文件路径;
- components 中可以指定组件名,官方推荐使用 PascalCase 或 kebab-case。
- 组件名如果在 components 中未指定,则会使用组件文件名的 PascalCase 和 kebab-case(短横线分隔命名),二者均可;
1.2.2 组件注册
组件的注册有全局和局部两种方式:
- 通过
Vue.component
创建的组件是全局组件,实际中不推荐全局注册; export default { components: {}}
为局部注册:- 对于子组件中引入的组件,如果是局部注册,则还需要给子组件引入。
例,子组件中引入组件:
ChildChildComponent.vue
:<template> <div> {{ fullName }} </div> </template> <script> export default { name: "child-child-component", props: ["fullName"] } </script>
ChildComponent.vue
:<template> <child-child-component :fullName="fullName"></child-child-component> </template> <script> import ChildChildComponent from "./ChildChildComponent"; export default { props: { firstName: { type: String, default: "yourFirstName", }, lastName: String, }, components: { "child-child-component": ChildChildComponent, }, computed: { fullName: function() { return this.firstName + " " + this.lastName; }, }, }; </script>
- App-3.vue:
<template> <div id="app"> <child-component :lastName="lastName"></child-component> </div> </template> <script> import ChildComponent from "./components/ChildComponent"; export default { name: "App", components: { "child-component": ChildComponent, }, data: function() { return { lastName: "Rick555" }; }, }; </script>
1.3 向子组件传数据 – prop
1.3.1 基本使用
由于 html 中 attri 大小写不明感,浏览器会将大写转为小写,所以在使用 prop 时推荐使用等价的 kebab-case,在字符串模板中(template 标签的内容写在字符串内)不存在此限制。
- 新建组件,
SendDataToComponent1.vue
:<template> <div> <span>一个字符串:</span> {{ aString }} </div> </template> <script> export default { name: "send-data-to-component-1", props: ["aString"], }; </script>
- App-4.vue:
<template> <div id="app"> <span>aString</span> <send-data-to-component-1 :aString="stringFromApp" /> <span>astring</span> <send-data-to-component-1 :astring="stringFromApp" /> <span>a-string: 推荐</span> <send-data-to-component-1 :a-string="stringFromApp" /> </div> </template> <script> import SendDataToComponent1 from "./components/SendDataToComponent1.vue"; export default { name: "App", components: { "send-data-to-component-1": SendDataToComponent1, }, data: function() { return { stringFromApp: "stringFromApp", }; }, }; </script>
1.3.2 prop 类型
prop 即可以使用字符串数组也可以使用对象形式:
- 字符串数组:
props: ['a', 'b', 'c']
, 如上面例子使用的props: ["aString"]
; -
对象形式(推荐): 方便为每个属性指定数值类型。
- 如:
props: { aString: String, aNumber: Number, aArray: Array, aObject: Object, aFunction: Function, },
- 支持定制参数是否必须、默认值、自定义验证函数。更多见官方文档: prop#Prop-验证
例:
- 组件:
SendDataToComponent2.vue
:<template> <div> <div> <span>一个字符串:</span> {{ aString }} </div> <div> <span>一个数值:</span> {{ aNumber }} </div> <div> <span>一个数组:</span> {{ aArray }} </div> <div> <span>一个对象:</span> {{ aObject }} </div> <div> <span>一个函数:</span> {{ aFunction() }} </div> </div> </template> <script> export default { name: "send-data-to-component-1", props: { aString: { type: String, default: "aString-o_o" }, aNumber: { type: Number, required: true }, aArray: Array, aObject: { type: Object, default: function () { return {message: "object default property"} } }, aFunction: Function, }, }; </script>
- App-5.vue:
<template> <div id="app"> <send-data-to-component-2 :aNumber="55" :aArray="[111, 222, 33]" :aObject="objectFromApp" :aFunction="functionFromApp" /> </div> </template> <script> import SendDataToComponent2 from "./components/SendDataToComponent2.vue"; export default { name: "App", components: { "send-data-to-component-2": SendDataToComponent2, }, data: function() { return { stringFromApp: "stringFromApp", objectFromApp: { name: "Rick", habits: "pee", }, }; }, methods: { functionFromApp() { return "functionFromApp: called by component"; }, }, }; </script>
注意:prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data
、computed
等) 在 default
或 validator
函数中不可用。
1.3.3 prop 数据流动
prop 数据是单向下行流动,即父级向子级,在使用需要注意:
- 如果 prop 传递的是一个初始值,推荐在组件内 data 中定义此 property;
- 如果 prop 传递的原始数据需要计算,应该在 computed 或者 watch 中进行;
- 如果 prop 传递的是数组或对象,由于这俩类型在 js 中是引用传递,即传递的是地址,所以在子组件中更改,也会影响父级,见下例;
例:
- 组件
propTransmit.vue
:<template> <div> <span>子: a string value: {{ money }}</span> <br /> <span>子: object value: {{ creator }}</span> <br /> <button @click="changeMoneyInComponent">changeMoneyInComponent</button> <button @click="changeCreatorInComponent">changeCreatorInComponent</button> </div> </template> <script> export default { name: "prop-transmit", props: { money: Number, creator: Object, }, methods: { changeMoneyInComponent() { this.money += 100; }, changeCreatorInComponent() { this.creator.year += 5; }, }, }; </script>
- App-6.vue:
<template> <div id="app"> <hr /> <div> <p>测试 prop 传递</p> <span>父: a string value: {{ money }}</span> <br /> <span>父: object value: {{ creator }}</span> <br /> <button @click="changeMoneyInApp">changeMoneyInApp</button> <button @click="changeCreatorInApp">changeMoneyInApp</button> <prop-transmit :money="money" :creator="creator" /> </div> </div> </template> <script> import propTransmit from "./components/propTransmit.vue"; export default { name: "App", components: { "prop-transmit": propTransmit, }, data: function() { return { money: 100, creator: { name: "Rick", year: 1999, }, }; }, methods: { changeMoneyInApp() { this.money -= 100; }, changeCreatorInApp() { this.creator.year -= 5; }, }, }; </script>
这段代码现象及反映出的规则:
- 改变父组件的数据,子组件数据会重新被初始,如:在父中改变 money 或者 creator 后,子组件 prop 的值都会被重新传入;
- 改变子组件中 creator 中的数据,父组件中也会改变;
- 改变子组件 creator 的数据后,money 数据会被重置成父组件传入的值,因为子组件修改 creator 相当于改变父级的 creator,触发了规则 1;
故实际中不要随意在子组件改变 prop 的值,无论是数值、字符串还是对象、数值。推荐在子组件 data 中定义一个变量,将 prop 赋值过去,如:
props: ["initMoney"],
data: function () {
return {
money: this.initMoney
}
}
1.4 插槽
1.4.1 插槽基础
插槽使用的指令在 2.6.0 之后有更改,下面笔记均使用 2.6.0 的新指令。
在上面代码中,如果父级在子组件标签写任何内容,都会被直接删除(因为实例挂载就是用 vue 生成的 DOM 替换到挂载点上),如上面的:
<prop-transmit :money="money" :creator="creator">页面上是否显示子组件标签的内容?</prop-transmit>
标签内的文字是不会显示在页面的。
而对于实际中经常需要向组件传递内容的,此时就需要使用插槽来实现。
slot 标签提供一个插槽,组件起始标签和结束标签之间的任何内容都会被正确地渲染,需要注意的是作用域问题:
- 组件标签内插入的变量或对象等必须是当前实例中的,即在 App 中使用组件 CompA,并向 CompA 插入数据时,那这些数据也必须是 App 实例的。
- 组件 SlotContent.vue:
<template> <div> <h4>{{title}}</h4> <slot></slot> </div> </template> <script> export default { name: "slot-content", props: { title: String, }, } </script>
- App-7.vue
<template> <slot-content :title="'标题'"> 插槽插入内容<br> <strong>{{ creator }}</strong> <!-- 这个 creator 必须是当前实例的 --> </slot-content> </template> <script> import SlotContent from "./components/SlotContent.vue"; export default { name: "App", components: { "slot-content": SlotContent, }, data: function () { return { creator: "Rick" } }, }; </script>
1.4.2 具名插槽
- 作用:父级插入内容时插入到组件内的指定位置。
-
用法:在组件内 slot 标签的 name 属性设置插槽名,父级使用时在 template 标签的 v-slot 传递对应的 name,语法格式为:
<!-- 子组件内设置插槽名 --> <slot name="slotName"></slot> <!-- 父级插入到指定的插槽 --> <子组件> <template v-slot:slotName> <h4>header</h4> </template> </子组件>
- 如果组件 slot 插槽未指定名,则会分配默认名 default,在父级插入时如果未指定 name,则会插入到 default 插槽。
例:
- 组件
NamedSlot.vue
:<template> <div> <header> <slot name="header"></slot> </header> <main> <!-- slot 未指定 name,则默认名为 default --> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div> </template> <script> export default { name: "named-slot", data: function() { return {}; }, methods: {}, }; </script>
- App-8.vue:
<template> <div> <named-slot> <template v-slot:header> <h4>header</h4> </template> <!-- 未指定 v-slot:name,则默认插入到 slot 名为 default --> <h4>main</h4> <template v-slot:footer> <h4>footer</h4> </template> </named-slot> </div> </template> <script> import NamedSlot from "./components/NamedSlot.vue"; export default { name: "App", components: { "named-slot": NamedSlot, }, }; </script>
1.4.3 作用域插槽
- 作用:使父级能够访问子组件的数据;
-
用法:在组件内 slot 标签的上绑定属性,父级使用时在 template 标签的 v-slot 取绑定的属性,语法格式为:
<!-- 子组件内绑定数据到属性,name 具名可以不写,默认为 default --> <slot name="slotName" :attriName="data"></slot> <!-- 父级取子组件绑定的数据 --> <子组件> <!-- 可具名不写,则会默认指定 default 插槽 --> <template v-slot:slotName="scoped"> sloted data: {{scoped.attriName}} </template> </子组件>
例:
- 组件
ScopedSlot.vue
:<template> <div> creator: {{ creator }}<br /> <!-- 子组件内绑定数据到属性,name 具名可以不写,默认为 default --> <slot :creator1="creator"></slot> </div> </template> <script> export default { name: "scoped-slot", data: function() { return { creator: { firstName: "Rick", lastName: "Sanchez", }, }; }, }; </script>
- App.vue:
<template> <scoped-slot> <!-- 具名不写,则默认指定 default 插槽 --> <template v-slot="scoped"> sloted data: {{ scoped.creator1 }} </template> </scoped-slot> </template> <script> import ScopedSlot from "./components/ScopedSlot.vue"; export default { name: "App", components: { "scoped-slot": ScopedSlot, }, }; </script>
使用场景 – 取表格组件内数据
例如,在 element-ui 的表格中,对每行进行某项操作,如按钮或者弹窗显示。
下面示例弹窗显示表格当前行 echarts 折线图:
<template>
<div>
<el-table :data="customerInfo">
<el-table-column label="customerName" prop="name"/>
<el-table-column label="consumeRate">
<template v-slot="scope">
<el-progress
:percentage="parseFloat(scope.row.consumeRate)"
:stroke-width="15"
:text-inside="true"
:color="customBarColor"
></el-progress>
</template>
</el-table-column>
<el-table-column label="consumeHistory">
<template v-slot="scope">
<el-popover
placement="left-end"
title="消费趋势"
width="600"
trigger="click"
@after-enter="getConsumeHistory(scope.row.id)"
>
<el-button slot="reference">
<span>历史</span>
<img src="@/assets/image/history.png" alt="history"/>
</el-button>
<div class="chart-container">
<div class="chart" :ref="scope.row.id"></div>
</div>
</el-popover>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import Echarts from "echarts";
export default {
name: "ScopedTableRowData",
data: function () {
return {
customerInfo: [
{name: "c1", id: "001", consumeRate: "31"},
{name: "c2", id: "002", consumeRate: "80"},
{name: "c3", id: "003", consumeRate: "60"},
],
chart: null,
}
},
methods: {
customBarColor(percentage) {
if (percentage < 70) {
return '#85ea50';
} else {
return '#ff0000'
}
},
getConsumeHistory(customerId) {
let consumeHistory = [{
date: "2020-01-01",
consume: 50,
}, {
date: "2020-01-05",
consume: 19,
}];
this.renderChart(customerId, consumeHistory);
},
renderChart(customerId, consumeHistory) {
this.chart = Echarts.init(this.$refs[customerId]);
let datasource = {};
Array.prototype.forEach.call(consumeHistory, (ele) => {
if (datasource.time) {
datasource.time.push(ele.date)
} else {
datasource.time = [ele.date]
}
if (datasource.history) {
datasource.history.push(ele.consume)
} else {
datasource.history = [ele.consume]
}
});
this.chart.setOption({})
}
},
};
</script>
<style lang="scss" scoped>
.chart-container {
margin-left: 40px;
margin-top: 20px;
.chart {
height: 200px;
}
}
</style>
需要引入 依赖,这里使用 cnpm:
# scss 编译依赖
npm i sass-loader -D
npm i node-sass -D
# element-ui 依赖
npm i element-ui -S
# echarts 依赖
npm i element-ui -S
-S: –save,将包信息添加到 package.json 的 dependencies(生产阶段的依赖)
-D: –save-dev,将包信息添加到 package.json 的 devDependencies(开发阶段的依赖)
引入依赖后,还需要引入 element-ui 及其样式,通常在 main.js:
import ElementUI from 'element-ui';
import "element-ui/lib/theme-chalk/index.css"
Vue.use(ElementUI)
1.4.4 插槽新旧属性
上面例子使用的都是 2.6.0 的新属性 v-slot,而被遗弃的旧属性为:slot 与 slot-scope,旧属性的 slot 来指定具名插槽,slot-scope 表示为作用域插槽。可以看出新属性 v-slot 其实就是这俩旧属性的综合,例:
<!-- 新属性 v-slot -->
<template v-slot:default="scoped">
sloted data: {{ scoped.creator }}
</template>
<!-- 旧属性 slot, slot-scoped -->
<template slot="default" slot-scope="scoped">
sloted data: {{ scoped.creator }}
</template>
1.5 总结-命名规范
常见的四种:
- PascalCase:单词首字母大写,如 LastName
- camelCase: 首个单词之外的单词首字母大写,如 lastName
- kebab-case: 单词之间使用中划线间隔,如 last-name
- snake_case: 单词之间使用下划线间隔,如 last_name
Vue 中推荐的命名方式:
- 组件文件名:PascalCase;
- 组件名: PascalCase 或者 kebab-case;
- prop 数据: 定义时 camelCase,html 标签内使用时 kebab-case;
- 自定义事件名: kebab-case。
三和四是因为 DOM 中大小写不明感,会被全部替换为小写,导致数据或者自定义的事件无法被访问。
1.6 总结-父级与子组件的数据传递
父向子:prop,具名插槽
子向父:自定义事件(未学习),作用域插槽