Vue
vue基础
前端框架发展史
- 最早期的网页是静态网页
- 后来出现了新的创建动态HTML的方式(数据嵌入到html文件中)如:ASP、JSP和PHP
这时候的互联网,兴起了数据嵌入模板,模板直接写样式的开发模式,例如MVC模式,在此时,前端只是后端 MVC中的 V,所以那时候的所谓 “前端工程师” 还没有对应的概念,前端开发人员都喜欢自称 “切图仔”。
- Model(模型层):提供/保存数据。
- Controller(控制层):数据处理,实现业务逻辑。
- View(视图层):展示数据,提供用户界面。
- 1995年网景公司推出了javascript,形成了前端的雏形:HTML为骨架,CSS为外貌,JavaScript为交互
- 1998年前后,ajax得到了相对的应用,从而促进了Web 从 1.0 的静态网页,纯内容展示向 Web 2.0(Web 2.0 时代:动态网页,富交互,前端数据处理) 模式迈进
这时候,前端不再是后端的模板,它可以独立得到各种数据
- 2006年jquery出现了,快速地风靡了全球
- MVVM模型框架出现
- 2009 年 AngularJS 诞生、Node诞生。
- 2011 年 React 诞生。
- 2014 年 Vue.js 诞生。
如今,后端负责数据,前端负责其余工作越发明显化。它们之间的通讯,只需要后端暴露API接口,前端通过 Ajax,以 HTTP 协议与后端通信即可。
什么是MVVM模式
- Model:提供/保存数据。
- View:视图(页面DOM)
- ViewModel:监控者(它就是负责监控两侧的数据,并相对应地通知另一侧进行修改)。
vue模板语法
vue模板
var vue = new Vue({
el:'#root', //el写html标签(css选择器)
// data定义的是当前vue应用的dom元素中要使用的数据
data:{
title:'hello vue',
total:100
},
methods:{
add(){
return this.x + this.y
}
}
})
插值
- {{变量名}}
变量名中含有html标签的内容是原样输出 不能解析html标签 <!-- v-html=“变量名”----将变量值作为当前标签的内容,是可以解析html标签,{{变量名}}}是不能解析html标签的 -->
- v-html=“变量名”
变量名中带有html标签的内容 可以解析html标签
- {{js表达式}}
{{price*number}}
{{‘btn-’+type}}
{{funcName()}}
{{sex==1?‘男’:‘女’}}
实例
<p>{{1+2}}</p>
<p>{{3>1?'red':'blue'}}</p>
<p>{{title.slice(1,3)}}</p>
<!-- 访问的是vue实例上定义的methods中定义的方法 -->
<p>{{add()}}</p> //add()是一个函数 是放在methods里面的
<p>{{'x+y='+add()}}</p>
<p>x+y={{+add()}}</p>
部分常用指令 v-
(是带有“v-”前缀的特殊属性)
- v-bind:属性名=“变量名” 绑定动态属性
<img v-bind:src="imgSrc" v-bind:alt="imgName"/> <-- 主要用于设置dom元素的自带的属性,如src,href,checked, 当设置所有属性名和属性值相等的属性的时候,设置false则舍去此属性,true则保留此属性-->
- v-bind缩写形式==>:属性名=“变量名”
<img :src="imgSrc" :alt="imgName"/>
- v-if,v-else,v-else-if
v-for与v-if一起使用(先执行v-for,再执行v-if)
<--当条件成立时则创建当前元素,不成立则删除-->
<div v-if="dataList.length">数据列表</div>
<p>
<span v-if="sex==1">男</span>
<span v-else-if="sex==2">女</span>
<span v-else>保密</span>
</p>
- v-show
v-show="条件" 当条件成立则显示元素,不成立则隐藏元素
当元素会频繁地进行显示与隐藏时则用v-show,反之v-if
<div v-show="isShow">
</div>
- v-for
// 遍历数组 v-for与v-if一起使用(先执行v-for,再执行v-if)
<ul>
<li v-for="(item,index) in dataList" :key="index">
{{item.name}}
</li>
</ul>
// 遍历普通对象{key:value}
<div v-for="(val,key) in config" :key="key">
<p><span>{{key}}:</span><span>{{val}}</span></p>
</div>
// 特殊用法
<p>
<span v-for="n in 10">*</span>
</p>
key属性的作用:跟踪每个节点的身份,从而重用和重新排序现有元素,理想的key值是每项都有的且它的值是唯一的。
- v-on:事件名=“函数名()”
<input type="button" v-on:click="add()" value="加1">
// add方法定义是写在Vue实例中的methods属性中
<!-- v-on:事件名="方法名" 这种方式不能传递额外的参数-->
- v-on缩写形式==>@事件名=“函数名()”
<input type="button" @click="add()" value="加1">
- v-model(双向绑定) //常用于表单元素
<!-- 双向绑定----数据变化视图自动更新,视图输入的内容也自动更新相应的数据 -->
<div>
<input type="text" v-model="money" placeholder="请输入捐献金额">
<p>你将捐献{{money}}金额</p>
</div>
数组更新检测
- 对数组操作会导致更新检测的方法
数组对象的方法:shift(),pop(),unshift(),push(),splice(),sort(),reverse()
数组变量名 = 新的数组
- 对数组操作不会导致更新检测
注意:数组名[下标] = newValue 并不会导致更新检测
以上解决方案:1、Vue.set(数组名,下标,newValue) 2、用数组对象的splice方法
this.colors[0] = 100 // 这样的方式是不能触发响应模式,因为没有使用Object.defineProperty来动态设置数组的元素
// Vue.set(this.onechecked,index,e.target.checked)
// this.onechecked.splice(index,1,e.target.checked)
对象更新检测 Vue.set
注意:要检测对象的属性是否变化了,一定要在声明此对象时将它的属性进行初始化,否则后期动态添加上的属性Vue是不能对它更新检测的,除非通过**Vue.set(对象名,属性名,值)**方法手动设置
Vue.set(对象名,属性名,值)
// avator对象起初没有src属性,以下设置并不会触发更新检测
this.avator.src = '1.jpg'
// 可以通过Vue实例的$set方法设置,触发视图更新
this.$set('avator','src','1.jpg')
Vue.set('avator','src','1.jpg')
object.definePorperty
声明响应式属性
let Person = {age:20}
Object.defineProperty(Person, 'name', {
get: function () {
// 当在获取当前对象的当前属性时自动调用该方法
console.log('get....')
},
set: function (val) {
// 当在设置当前对象的当前属性时自动调用该方法
console.log(Math.random())
document.getElementById('nickname').innerHTML = val
}
})
事件处理
事件处理几种写法
- 监听事件—直接触发代码
<input type="button" @click="count = count + 1">
- 方法事件处理器----写函数
<input type="button" @click="add">
// 这样方式不能传递参数,在定义add方法时可以写一个形参变量,用于接收当前事件对象
- 内联处理器方法----执行函数表达式
<input type="button" @click="increment(2)">
// 这样方式能传递参数
<input type="button" @click="increment2($event,2)">
// $event是当前事件对象,是Vue中的内置变量
修饰符
事件修饰符
- .stop 阻止事件冒泡(掌握)
- .prevent 阻止默认事件(掌握)
- .once 事件执行一次
- .self 事件源是本身才会触发
- 事件修饰符可以串写,如@click.stop.prevent
<div class="outer" @click="say()">
<!-- 阻止事件冒泡 -->
<div class="inner" @click.stop="out()"></div>
</div>
<!-- 阻止默认事件 -->
<form @submit.prevent>
<input type="submit" value="提交表单"/>
</form>
<!-- self点击的对象是本身,once事件绑定一次,修饰符可以连写 -->
<div class="outer" @click.self.once="say()">
<div class="inner" @click="out()"></div>
按键修饰符
- .enter 按下并释放回车键时
@keyup.enter<==>@keyup.13
- .left,.right,.top,.down 按下并释放左、右、上、下键时
- .esc 按下并释放esc键时
- .space 按下并释放空格键时
- .tab 按下并释放tab键时
- .delete 按下并释放删除键时
系统修饰键
- .alt
@keyup.alt.67 ===> 按alt+c键时
- .ctrl
@click.ctrl ===> 按住ctrl键同时再单击时
-
.shift
<form @submit.prevent> <!-- <input type="text" placeholder="输入品牌名称" @keyup.enter="send($event)"/> --> <input type="text" placeholder="输入品牌名称" @keyup.ctrl.enter="send($event)"/> </form>
表单绑定 v-model
单行文本框
v-model绑定的变量的值作为单行文本框的内容
<input type="text" v-model="message">
多行文本框
v-model绑定的变量的值作为多行文本框的内容
<textarea v-model="desc">
</textarea>
单选按钮
v-model绑定的变量的值是同一组单选按钮的value值之一
<input type="radio" v-model="sex" :value="1">男
<input type="radio" v-model="sex" :value="2">女
多选框
v-model绑定的变量的值是同一组复选框的value值中的一个或者多个,变量值是数组
<input type="checkbox" v-model="subject" value="java">java
<input type="checkbox" v-model="subject" value="C++">C++
<input type="checkbox" v-model="subject" value="H5">H5
<input type="checkbox" v-model="subject" value="phyon">phyon
下拉选择框(单选与多选)
单选,v-model绑定的变量的值是下拉列表所有选项的value值之一
<select v-model="brandId">
<option :value="1">华为</option>
<option :value="2">小米</option>
<option :value="3">苹果</option>
</select>
多选,v-model绑定的变量的值是下拉列表所有选项的value值中的一个或者多个,变量值是数组
<select v-model="cityIds" multiple>
<option :value="10">上海</option>
<option :value="11">广州</option>
<option :value="12">北京</option>
</select>
v-model的修饰符
.lazy–延迟更新
.number—强制将变量的值转换为数字整型
.trim—删除变量值两边空格
class与style属性绑定
绑定class
- 对象语法(:class="{类名1:boolean,类名n:boolean}")
<div :class="{ active: isActive}" class="box">可以与普通的class共存</div>
- 数组语法(:class=[‘类名1’,‘类名2’,{类名:boolean}])
<div :class="['box',{active:isActive}]" class="border">可以与普通的class共存</div>
绑定style属性
- 对象语法(:style="{样式名1:值,样式名n:值}")
<p :style="{color:'#ccc',fontSize:'18px'}">绑定内联样式</p>
- 数组语法(:style="[{样式名1:值,样式名n:值},{样式名1:值}]")
<p :style="[{color:'#ccc',fontSize:'20px'},{backgroundColor:'orange'}]">使用数组形式添加内联样式</p>
计算属性computed
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护;所以,对于任何复杂逻辑,你都应当使用计算属性
函数可以返回任意类型的数据
// 声明n个计算属性(某个属性的值是依赖其它变量计算得来的数据)
computed: {
total() { //total作为变量放入html标签中
// 函数的返回值作为计算属性的值,当参与计算的任意变量发生变化时则自动调用该方法
return this.lifeAccount + this.telAccount + this.transportAccount
},
sum: {
get() {
// 返回计算属性的值,当计算total的值运行的函数
return this.lifeAccount + this.telAccount + this.transportAccount
},
//当设置total的值的时候运行的函数 newValue是新的total的值
set(newValue) {
this.lifeAccount = this.telAccount = this.transportAccount = newValue / 3
}
},
},
<ul>
<li v-for="(item,index) in oddNums">{{item}}</li>
</ul>
new Vue({
computed:{
oddNums(){
// <!-- 当numbers变量的值发生变化此函数将会被执行 -->
return this.numbers.filter((item)=>item % 2 == 0)
}
}
})
监听属性watch
当需要在数据变化时执行异步或开销较大的操作时,watch这个方式是最有用的
watch:{
// 监听的是基本数据类型变化
total(newVal,oldVal){
// 当total变量的值发生变化时则执行当前方法
console.log(newVal,oldVal)
// 重新计算总页码,获取数据
this.pages = Math.ceil(newVal / 10)
this.getData()
},
// 监听对象的属性是否变化时
'pageInfo.total':function(newVal,oldVal){
console.log('监听对象上某个属性变化:',newVal)
this.pages = Math.ceil(newVal / 10)
this.getData()
},
// 监听对象变化(只要修改对象的任意一个属性则执行某个功能)---常用用于监听$route(路由变化)
filter:{
handler(newVal,oldVal){
console.log('filter',newVal)
},
deep:true, //递归的监听
immediate:true, //(页面初始化完毕)立即监听 加载页面的时候就执行一次
}
},
watch:{
// 监听基本类型变量
typeId1(newVal,oldVal){
// 当typeId1变化时将执行此方法数
// newVal是最新的值,oldVal是前一次的值
if(newVal){
this.getData()
}
},
typeId2:{
//如果我们需要在最初绑定值的时候也执行方法,则就需要用到immediate属性。
handler(newVal,oldVal){
if(newVal){
this.getData()
}
},
immediate:true
},
// 监听对象上某个属性
'filter.name'(newVal,oldVal){
this.search()
},
// 监听一个对象的改变时,需要将deep属性设置为true
filter:{
handler(newVal,oldVal){
// filter对象上的任何属性变化了将执行此方法
},
deep:true
}
},
computed和watch的区别
计算属性与监听属性的使用场景
当你在模板内使用了复杂逻辑的表达式时,你应当使用计算属性。
当需要在数据变化时执行异步或开销较大的操作时,使用watch。
调用计算computed属性与通过调用方法methods来获取到某数据的区别
- computed是属性调用,而methods是函数调用;
- computed带有缓存功能,而methods不具有缓存功能;
- 我们可以使用 methods 来替代 computed,效果上两个都是一样的,但是 computed是基于它的依赖缓存,只有相关依赖发生改变时才会重新取值。而使用 methods ,在重新渲染的时候,函数总会重新调用执行。可以说使用computed性能会更好
组件 component
每个组件的数据中心是的独立的
组件 :就是页面上的一部分。
组件的创建
// 组件名—首字母大写,例如HhPage
// 创建组件 Vue.component(‘组件名’,{组件的配置项})
// 创建一个按钮组件
Vue.component('HhButton',{
name : "hhbutton", //给组件创建一个名称
template:`<button>普通按钮组件</button>`,
data(){
// 定义组件自己的数据中心是用的data方法,该方法一定要返回一个对象
// 为什么要返回一个对象,目的是让当前组件的每一个实例的数据是独立的
return {
width:200
}
},
props:{ //也可以写成 props:["title","type"]
title:String,
type:{
type:String, //type属性的类型
default:'success', //设置当没有属性值的时候 默认值
validator(val){
// val接收的是当前属性值
let flag = ['info','warning','danger','success'].indexOf(val) !== -1
if(!flag){
console.log('********传递type属性值有误,当前传递的值是:'+val)
}
return flag
}
}
},
})
父组件向子组件传递数据
通过props进行数据的传递
在组件中是不能修改props属性值,(单向向下行绑定数据----单向数据流)----组件data上的数据由自己的行为处理
实例:
const ElChild = {
template : `<div><button>显示数据</button>
<p>{{numchild}}</p>
</div>`,
props:["numchild"], //子组件通过props对数据进行接收
}
Vue.component("ElParent",{
template :`
<div>
<el-child :numchild="num"></el-child>
</div>`, //父组件的模板通过绑定自定义属性:numchild把数据传入子组件
data() {
return {
num : 1,
}
},
components : {
ElChild,
}
})
子组件向父组件传递数据
通过自定义事件进行数据的传递
// 子组件向父组年传递数据(通过事件)
// 1.在父组件中使用子组件时绑定自定义事件
// 2.在子组件中触发在子组件上绑定的自定义事件emit()–>this.$emit(‘自定义事件名’,传递的数据)
// 3.在父组件中通过$event来获取子向父传递的数据
实例:
const ElChild = {
template : `<div><button @click="emit(money)">显示数据</button></div>`,
data() {
return {
money : 3,
}
},
methods: {
emit(e){
this.$emit("getdata",e) //向父组件自定义事件为getdata的事件传递数据。e可以是任意类型的数据
}
},
}
Vue.component("ElParent",{
template :`
<div>
<el-child @getdata="gete($event)"></el-child>
</div>`, //$event是获得的数据
methods: {
gete(e){
console.log(e); //e是子元素传递的数据
}
},
components : {
ElChild,
}
})
new Vue({
el:"#box",
})
兄弟组件传递数据
可以实现跨级传递数据
通过中央控制事件总线进行数据的传递
中央控制事件总线 就是一个空的Vue
var bus = new Vue()
bus.
o
n
(
′
事
件
名
′
,
(
)
=
>
)
−
−
−
−
写
在
组
件
的
c
r
e
a
t
e
d
勾
子
函
数
中
b
u
s
.
on('事件名',()=>{}) ----写在组件的created勾子函数中 bus.
on(′事件名′,()=>)−−−−写在组件的created勾子函数中bus.emit(‘事件名’,传递的数据)
//通过 中央控制事件总线
//bus.$on("事件名",()=>{}) 接收数据并进行相应的处理
//bus.$emit('事件名',传递的数据) 发送数据
实例
Vue.component('HhB', {
data() {
return {
bank: 2000,
}
},
template: `
<div>兄弟组件B{{bank}}
</div>`,
created(){
let _this = this
bus.$on('jq',function(val){
if(_this.bank > val){
_this.bank -= val
bus.$emit('answer',{code:1,msg:'同意',money:val})
}else{
bus.$emit('answer',{code:0,msg:'对不起,金额不足',money:0})
}
})
}
})
Vue.component('HhA', {
data() {
return {
money: 600,
num:0
}
},
template: `
<div>兄弟组件A{{money}}
<hr />
<input type="text" v-model.number="num" @keyup.enter="brrow()"/>
</div>`,
methods:{
brrow(){
bus.$emit('jq',this.num)
}
},
created(){
bus.$on('answer',(val)=>{
if(val.code == 1){
this.money += val.money
}else{
console.log(val.msg)
}
})
}
})
new Vue({
el: '#app'
})
组件绑定class和style
<App :style="{backgroundColor:'#ccc'}"></App>
<App :class="['bg','active']"></App>
<App :class="{active:index==2}"></App>
<!--有个App组件-->
slot 插槽
slot标签用于占位,接受当前组件中的子内容
//页面的<div slot="s"> <span></span> </div>
//template中 <slot name="s"></slot>
//页面中的内容等于<slot> </slot>
//<slot name="s"></slot> = <div> <span></span> </div>
当定义组件时,组件的内部结构是无法确定时则可以用slot占位
<hh-swiper>
<!-- 这一部分放在默认插槽处 -->
<div>
<img src="1.jpg" />
<img src="2.jpg" />
</div>
<!-- 这一部分放在slot的name属性为pagination处 -->
<div slot="pagination">
<span>1</span> <span>2</span>
</div>
</hh-swiper> //组件标签
// slot标签用于占位,接收的是当前组件中的子内容
// 定义组件中加了<slot name="插槽名称"></slot>,在使用组件时<组件名>插槽的内容</组件名>
template:`
<div class="swiper-container">
<div class='swiper-items'>
<slot>swiper要播放的项</slot>
</div>
<div class="swiper-pagination">
<slot name="pagination">swiper的分页器</slot>
</div>
</div>
`
this.$refs.refName
在vue.js中获取dom元素或者组件的实例
例如
<div ref="header" class="box"></div>
<!-- 想获取这个div
Vue的写法为 var box=this.$refs.header
这个box就是这个div DOM元素-->
动态组件
// is来对变量进行判断
template:`
<div>
<div>
<component :is="componentName"></component>
</div>
<hh-nav></hh-nav>
</div>
`,
data(){
return {
componentName:'HhCenter' //用来改变组件的变量
}
},
声明周期
生命周期钩子
创建和挂载内的函数在页面加载的时候会自动运行一次 创建created 挂载mounted
// created----初始化页面数据以及ajax请求
// mounted----用于获取dom元素的
vue一整个的生命周期中会有很多钩子函数提供给我们在vue生命周期不同的时刻进行操作,项目开发中常用的两个生命周期钩子函数是(created,mounted)
- beforeCreate
实例状态:实例初始化之后,this指向创建的实例,不能访问到data、computed、watch、methods上的方法和数据
使用场景:常用于初始化非响应式变量
- created
组件状态:实例创建完成,可访问data、computed、watch、methods上的方法和数据,未挂载到DOM,不能访问到 e l 属 性 , el属性, el属性,refs属性内容为空数组
使用场景:常用于简单的ajax请求,页面的初始化
- beforeMount
实例状态:在挂载开始之前被调用,beforeMount之前,会找到对应的template,并编译成render函数
- mounted
实例状态:实例挂载到DOM上,此时可以通过DOM API获取到DOM节点,$refs属性可以访问
使用场景:常用于获取dom元素以及对它的操作
- beforeUpdate
实例状态:响应式数据更新时调用,发生在虚拟DOM打补丁之前 js的一个对象来描述的html结构
使用场景:适合在更新之前访问现有的DOM,比如手动移除已添加的事件监听器
- updated
实例状态:虚拟DOM重新渲染和打补丁之后调用,组件DOM已经更新,可执行依赖于DOM的操作
注意:避免在这个钩子函数中操作数据,可能陷入死循环
- beforeDestroy
实例状态:实例销毁之前调用。这一步,实例仍然完全可用,this仍能获取到实例
使用场景:常用于销毁定时器、解绑全局事件、销毁插件对象等操作
- destroyed
实例状态:实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁
axios获取数据
axios的使用方式和jquery的AJAX请求没有什么区别
axios还相当于Promise所以回调函数是使用then
格式
axios({
url : "",
method :"",
.....
}).then((res)=>{})//res是请求端口响应的数据
// 设置ajax请求的基本路径
axios.defaults.baseURL = 'http://localhost:9000/';
// 设置ajax请求超时时间
axios.defaults.timeout = 2000
// 设置响应拦截器
axios.interceptors.response.use(result => {
return result.data
},error=>{
if(error.message.includes('timeout')){
alert('请求超时')
return 'timeout'
}
return Promise.reject(error);
});
// 设置请求拦截器
axios.interceptors.request.use((config)=>{
// 在此可以设置所有接口都需要用到的参数
config.headers['usertoken'] = '299429942428423191031031';
return config
})
// get请求
axios.get(url).then(data=>{
}).catch(err=>{
})
// post请求
axios.post(url).then(data=>{
}).catch(err=>{
})
axios({
url : "https://m.maizuo.com/gateway?cityId=510100&pageNum=1&pageSize=10&type=1&k=4834734",
method : "get",
headers:{
"X-Client-Info": '{"a":"3000","ch":"1002","v":"5.0.4","e":"156767148210033043604248"}',
"X-Host": "mall.film-ticket.film.list"
}
}).then(res=>{
if(res.data.status==0){
this.bannerlist=res.data.data.films
}
})
swipei组件的使用
//在不使用组件的时候 由于创建的时候拿到数据属于异步操作,所以当使用数据放入DOM元素中的时候会出现BUG,就是数据还没有拿到就进行了DOM操作。
//解决办法
//一. 在updated中调用对DOM进行操作的函数。但是由于操作只需要进行一次,所以设定判定条件
updated() {
if(this.bannerbool){this.setswiper()}//数据更新的时候只调用一次
},
//二.使用组件,在调用组件的时候设定if条件,判断数据拿到了,就在页面创建元素
<el-banner v-if="bannerlist.length" :bannerlist="bannerlist"></el-banner>
过滤器 Vue-filter
过滤器的作用就是在使一个函数变得可以多次重复调用,在页面的DOM元素上。
主要用于数据不是自己想要的数据的格式的时候对数据的处理
全局过滤器
全局过滤器,在创建Vue实例前定义,在实例所挂载的el元素范围内都能使用
在v-html中不能使用过滤器
Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示
过滤器只能用于插值表达式中
模板
Vue.filter('过滤器名称',function(val){
return //返回值 val为接收的参数
})
<!-- 在双花括号中 -->
{{ val | 过滤器名称 }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="val | 过滤器名称"></div>
实例:
Vue.filter('formatDate',function(val){
// val接收的是过滤器处理的变量值
return new Date(val).toLocaleDateString()
})
// 过滤器只能用于插值表达式的语法中
const filters = {
filterA(){
return '123'
},
filterB(){
return '456'
},
}
// 批量创建过滤器
for(var i in filters){
Vue.filter(i,filters[i])
}
局部过滤器
局部过滤器,只供当前组件使用
// 局部过滤器 在组件中配置项filters
模板
// 在组件的选项中定义
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
实例
template:`<div>局部过滤器:<p>主要演员:<span>{{actors|formatActors}}</span></p></div>`,
filters:{
formatActors(actors){
// 在过滤器不要返回html标签,应用在插值表达式不能解析的
let str = ''
actors.forEach(item=>{
str += item.name + ' '
})
return str
}
},
自定义指令Vue-directive
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令,有时候需要对普通DOM元素进行底层操作,这时候就会用到自定义指令
全局指令
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
内部钩子函数
Vue.directive('指令名称',{
bind(){//只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
console.log('bind')
},
inserted(){//被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
console.log('inserted')
},
update(){//更新的时候就进行调用
console.log('updated')
}
})
钩子函数简写方式:
// 当初始化的行为(bind或者inserted)与更新(update)时的行为相同时用以下简写的方式来实现
Vue.directive('hide1',function(el,binding,vode){
if(binding.value){ //binding.value为指令的绑定值
el.style.display = 'none'
}else{
el.style.display = ''
}
})
局部指令
可以在组件中使用 directives建立
// 在组件的选项中定义
directives: {
focus: {
inserted: function (el,binding,vnode) {
//el 指令所绑定的元素,可以用来直接操作 DOM 。
// binding 一个对象,所包含属性参考官网,其中有一个value属性获取到使用指令时传递的参数
el.focus()
}
}
}
// 在模板中使用方式:<input v-focus />
vue-cli
使用脚手架搭建项目
- 安装
npm install -g @vue/cli (全局安装)
- 创建项目
vue create 项目名称
- 命令的使用
npm run serve 开发环境构建 建立本地服务器
npm run build 将应用程序打包
npm run lint 代码检测工具
4.项目目录的介绍
public下的资源不会打包
assets下的资源会打包
@符号表示src文件夹
main.js
import Vue from 'vue'
// .vue----单文件组件(template(模板),script(数据+行为部分),style(样式部分))
// import App from './App.vue' // .vue文件是通过vue-loader工具转换为可识别的js文件
// import Hello from './Hello.vue'
import router from './router' //引入路由文件
import Todo from './Todo.vue'
Vue.config.productionTip = false
// main.js是入口文件----入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
new Vue({
router : router, //使用路由文件
render: h => h(Todo)
}).$mount('#app')
router.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router) //在vue上使用router
export default new Router({
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
}
]
})
单文件组件实例
<template>
<div>
<h1 @click="say('hello .vue')">{{title}}</h1>
</div>
</template>
<script>
export default {
data(){
return {
title:'第一个单文件组件'
}
},
methods:{
say(msg){
console.log(msg)
}
}
}
</script>
<style scoped lang="scss">
$color:blue;
h1{
color:$color
}
</style>
vue.config.js的配置
在项目根目录下创建vue.config.js,注意修改了此文件内容需要重新运行npm run serve命令
Vue.config
是一个对象,包含 Vue 的全局配置。
proxy代理配置
// 在vue.config.js中添加以下代码
devServer:{
port:3000,
'/api':{
target:'http://m.maoyan.com/',
changeOrigin: true,
pathRewrite: {
'/api': ''
}
}
}
alias别名配置
@ is an alias to /src
// 在vue.config.js中添加以下代码
chainWebpack:(config)=>{
console.log(config.resolve.alias.get('@')) // 获取别名
// 为src/components目录设置别名为components
config.resolve.alias.set('components',resolve('./src/components'))
}
publicPath配置
部署应用包时的基本URL,默认值是’/’,当程序不是部署在网站根目录是则需要配置此项
关闭eslint
去掉eslintrc.js文件中的@vue/standard
json-server实现mock数据
安装
npm install -g json-server
json-server命令的使用
json-server --watch db.json
利用vue-cli进行组件化开发
迁移todolist、swiper案例到vue-cli中
利用swiper模块封装一个Swiper组件
安装
npm install swiper --save
实现
<template>
<div class="swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item) in data" :key="item.bannerId">
<img :src="item.imgUrl" />
</div>
</div>
<div class="swiper-pagination"></div>
</div>
</template>
<script>
import Swiper from "swiper";
import "swiper/dist/css/swiper.css";
export default {
props: ["data"],
data() {
return {
title: "swiper模块的使用"
};
},
mounted() {
new Swiper(".swiper-container", {
pagination: {
el: ".swiper-pagination"
},
autoplay: {
delay: 3000,
stopOnLastSlide: false,
disableOnInteraction: true
}
});
}
};
</script>
<style scoped lang="scss">
</style>
vue-router
起步
- 安装
- 通过vue-router创建路由规则
动态路由
在定义路由时路径中带有冒号的即是动态路由,例如路径是 /film/:id 为动态路由,通过this.$route.params.id 获取到相应的值
动态路由(路径中包含了以:开头的部分,这一部分值是不确定的)
写法
path:'/article/:id',
嵌套路由
注意使用嵌套路由的时候 children里面的路由不要加/
const router = new VueRouter({
routes:[{
path:'/films',
component:FilmIndex,
children:[{
path:'',
// 当访问/films路由时直接跳转到/films/playing路径
redirect:'/films/playing'
},{
path:'playing',
//PlayingFilm 会被渲染在FilmIndex的<router-view>中
component:PlayingFilm
},{
path:'coming',
//ComingFilm 会被渲染在FilmIndex的<router-view>中
component:ComingFilm
}]
}]
})
实例:
{
path:'/films',
component:FilmsIndex,
children:[{
path:'',
// redirect重定向
redirect:'/films/nowPlaying'
},{
path:'nowPlaying',
component:NowPlaying
},{
path:'comingSoon',
component:ComingSoon
}]
}
编程式的导航
path 和params不能一起使用
this.$router.push('路径')
this.$router.push({path:'路径',query:{参数名:值}})
this.$router.push({name:'路由名称',params:{参数名:值}}) //通过name查找路由
// 注意:this.$router.push方法的参数是对象时,path不能与params搭配使用
// 获取动态路由中的参数使用this.$route.params
// 获取路径中?部分的参数使用this.$route.query
this.$router.go(-1)//-1表示返回上一页
实例:
goBack(){
// 转向某地址
// this.$router.push('/article')
// this.$router.push({path:'/article'})
this.$router.go(-1) // 上一页(history.back())
}
<li><router-link to="/article/199">react基础部分</router-link></li>
<li><router-link to="/article/100">react路由</router-link></li>
<li><input type="button" @click="$router.push({name:'detail',params:{id:800}})" value="查看文章id为109"/></li>
<li><input type="button" @click="$router.push({path:'/article/109'})" value="查看文章id为109"/></li>
<!-- <h1>当前文章id是:{{$route.query.id}}这是文章详情页面</h1> -->
<h1>当前文章id是:{{$route.params.id}}这是文章详情页面</h1>
路由守卫
全局前置守卫(全局拦截)
写在路由外部
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => { //进入路由前进行相应的运算
//next传参就跳到指定页面 不传参就跳到to的路径
})
router.afterEach(()=>{}) //进入路由后进行相应的运算
实例:
// 路由前置守卫----用于检测用户认证
let whitePathes = ['/login','/home'] //路由白名单
router.beforeEach((to,from,next)=>{
// to 进入的那个路由 from来自那个路由 next是个函数 决定是否展示to路由的组件
console.log('********',to)
if(whitePathes.indexOf(to.path) === -1 && !localStorage.getItem('username')){
// router.push('/login')
next('/login')
return
}
console.log('这是路由的全局守卫',to.path,from.path)
next()
})
路由的独享守卫(单个拦截)
写在路由内部
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
}
}
]
})
实例:
beforeEnter(to,from,next){
console.log('当前路由守卫....',to.params,from.path)
if(isNaN(to.params.nid)){
console.log('参数有误,新闻id必须有的')
next('/error')
return
}
next()
}
组件内的守卫(单个拦截)
写在组件内部
// 在同一个组件中切换时,组件不会重新挂载,而又要重新渲染页面的数据时,在路由发生变化时去更新相应的数据
/* 这里注意的是:::如果切换当前新闻id时还要重新获取相应的详情数据,
需要在beforeRouteUpdate中处理 */
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
next(vm => {
// 通过 `vm` 访问组件实例
})
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
监听路由变化
当路由发生改变的时候 运行handle的函数
watch:{
'$route':{
handle(newValue,oldValue){
// 当前路由发生变化时将调用该方法,如/user/1切换到/user/2时
},
deep:true,
immediate:true // 页面初始化完成就立即监听
}
}
路由元信息
定义路由的时候可以配置
meta
字段:
const router = new VueRouter({
routes: [
{
path: '/user',
component: User,
children: [
{
path: 'list',
component: UserList,
// 通过meta属性可以配置当前路由的说明信息
meta: { title: '用户列表' }
}
]
}
]
})
实例:
{
path:'/login',
component:Login,
// 是可以在当前路由上存储一些项目中需要的数据
meta:{
hideHeader:true
}
}
获取此路由上的meta
watch:{
$route:{
handler(val){
this.isShow = !val.meta.hideHeader
},
deep:true,
immediate:true
}
}
路由懒加载
按需要加载组件
const router = new VueRouter({
routes: [
{ path: '/user', component: ()=> import('./views/User.vue') }
] //就是把component写为函数形式 而不是一开始进入主页面就把全部的组件加载完成。当需要那个组件的时候就加载那个组件,使得进入主页面更快
})
HTML5 History模式
- 设置mode为history,默认是hash(router.js中)
- 后端配置(这里以node.js+express作为后端为例)
app.use(express.static('./')) // 设置静态资源目录第一次
const history = require('connect-history-api-fallback'); // 支持history模式的中间件
app.use(history());
app.use(express.static('./')) // 设置静态资源目录第二次
// 注意必须设置两次静态资源目录两次,否则没有办法实现history模式
vuex Store
状态管理:可追踪,共享数据,数据变化所有使用这个数据的组件也进行改变
工作原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u05BsdEE-1619682841247)(C:\Users\客户\Desktop\笔记\images\vuex.PNG)]
创建
store.js的内容
// vuex ---安装 npm install --save-dev vuex
// 导入
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex) //全局使用vuex
// 状态管理中心----一个应用程序只能创建一个状态管理中心
let store = new Vuex.Store({
//所有组件共享的数据中心
state:{
money:99, //班费
username:'admin'
},
// 定义的规则来改变state中的数据,同步事务
mutations:{
// 现金交班费,state是接收的是当前store中的state
addMoney(state,number){
state.money += number
},
},
// 处理异步事务的规则,是不能直接改变state中的数据,只能提交给mutaions中的规则来改变
actions:{
// context与store对象拥有相同的属性以及方法
ybAddMoney(context){
setTimeout(function(){
// 异步执行完毕时则提交mutations中的方法
context.commit('addMoney',450)
},2000)
},
},
getters:{
// isOk是state的扩展数据
isOk(state){
return state.money < 100 ? '班费不足,请交班费' : '班费充足'
}
}
})
export default store
组件的内容:
访问store中的state数据,html=>{{KaTeX parse error: Expected 'EOF', got '}' at position 18: …ore.state.money}̲} js=>this.store.state.money
import { mapState,mapMutations,mapActions } from 'vuex'
export default {
data(){
return {}
},
computed:{
free(){
return this.$store.state.money
},
...mapState(['money','username'])
},
methods:{
// 将this.addMoney(600)映射为this.$store.commit('addMoney',600)
...mapMutations(['addMoney','buyWater']),
// 将this.ybAddMoney()映射为this.$store.dispatch('ybAddMoney')
...mapActions(['ybAddMoney','ybReduceMoney']),
give(){
// this.$store.commit('规则名称即是mutaions中定义的方法名',传递的参数)
// this.$store.commit('addMoney',600)
this.addMoney(600)
} ,
ybGive(){
// 触发store中心定义的actions中的ybAddMoney方法
// this.$store.dispatch('规则名称即是actions中定义的方法名',传递的参数)
// this.$store.dispatch('ybAddMoney')
this.ybAddMoney()
},
}
}
state
存放所有的公共数据的地方 相当于组件中的computed
//在组件中获取数据 //在计算属性中
computed: {
count () {
return this.$store.state.count
}
}
//在html中可以使用 {{$store.state.count}}
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
})
}
//也可以使用数组的方式
export default {
computed: mapState(["count"])
}
mutations
实现同步操作的地方
每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
在组件中提交 Mutation
this.$store.commit( 规则名称即是actions中定义的方法名 , 参数)
import { mapMutations } from 'vuex'
export default {
methods: {
// 将this.addMoney(600)映射为this.$store.commit('addMoney',600)
...mapMutations(['addMoney','buyWater']),
give(){
// this.$store.commit('规则名称即是mutaions中定义的方法名',传递的参数)
// this.$store.commit('addMoney',600)
this.addMoney(600) //调用
} ,
}
}
store中的mutations
mutations:{
// 现金交班费,state是接收的是当前store中的state
addMoney(state,number){
state.money += number
},
},
actions
处理异步操作的地方
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
store中的actions
// 处理异步事务的规则,是不能直接改变state中的数据,只能提交给mutaions中的规则来改变
actions:{
// context与store对象拥有相同的属性以及方法
ybAddMoney(context){
setTimeout(function(){
// 异步执行完毕时则提交mutations中的方法
context.commit('addMoney',450)
},2000)
},
},
组件中使用actions
this.$store.dispatch( 规则名称即是actions中定义的方法名 )
// 将this.ybAddMoney()映射为this.$store.dispatch('ybAddMoney')
...mapActions(['ybAddMoney']),
ybGive(){
// 触发store中心定义的actions中的ybAddMoney方法
// this.$store.dispatch('规则名称即是actions中定义的方法名',传递的参数)
// this.$store.dispatch('ybAddMoney')
this.ybAddMoney()
},
getters
相当于是计算属性 只是是store的计算属性 和state中的数据相差不大
store中的getters
getters:{
havemoney(state){
if(state.money<0){
state.money = 0
}
return state.money>0? "还有钱":"没有钱啦"
}
}
组件中使用
//可以使用mapGetters
//也可以使用$store.getters.key
module
模块化
Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
写法
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
export default {
// 如果namespaced为true时,在调用相应的actions和mutaions需要在方法名前加模块名/
// 例如在组件中写:this.$store.commit('mas/changeBg')
// 例如在组件中写:this.$store.dispatch('mas/changeYbBg')
namespaced:true,
state:{
bgColor:'red'
},
mutations:{
changeBg(state){
console.log('模块a中的changeBg方法')
}
},
actions:{
changeYbBg(state){
console.log('模块a中的changeYbBg方法')
}
},
getters:{
}
}
项目
流程
产品对接客户 》需求文档》产品原型图(墨刀)
开发 UI 前端 后端 测试
熟悉产品业务–原型图规划结构(项目搭建)–静态页面–分模块开发
flexiable.js 动态设置根元素的font-size 设备宽度/设计稿的宽度
DPR 设备像素比
移动端页面自适应
方案一 : rem js实现 css用@media
方案二 :vw+rem
100vw 将视口平分为100单位的vw
大部分按iphone6分辨率设计
将html的font-size设为100px font-size:13.3333vw
1个逻辑像素 = 2个物理像素(设备像素) DPR : 2.0
在css中导入其他css的时候使用@符号要在前面加~符号,才会解析
下拉刷新使用Vant
代理 使用VUE-CLI
组件中不能修改porps属性值(单向向下行绑定数据—单向数据流)
Vue是双向绑定 — React是单向数据流
跨域 代理 proxy
在src同级别建立一个vue.config.js文件
vue.config.js 参考vue官网vue-cli的devServer 和devServer.proxy
module.exports = {
devServer: {
proxy: {
'/api': { //匹配所有以/api开头的请求接口
target: 'http://localhost:8089',
ws: true,
changeOrigin: true,
pathRewrite:{ //重定向
'/^api' :'' //把以/api开头的 去掉api
}
},
}
}
}
devServer: {
proxy: {
'/rng': { //这里最好有一个 /
target: 'http://45.105.124.130:8081', // 后台接口域名
ws: true, //如果要代理 websockets,配置这个参数
secure: false, // 如果是https接口,需要配置这个参数
changeOrigin: true, //是否跨域
pathRewrite:{
'^/rng':''
}
}
}
}
keep-alive
有缓存的作用第一次进入的时候进行缓存
<keep-alive>
<component :is="view"></component>
</keep-alive>
相关设置
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。max
- 数字。最多可以缓存多少组件实例。
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
include
和 exclude
属性允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
匹配首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值)。匿名组件不能被匹配。
**keep-alive有两个钩子函数activated
和 deactivated**
在一个组件第一次被创建的时候钩子函数执行顺序为 create=>mounted=>activated
退出的时候执行deactivated 再次进入此组件的时候只会执行activated (这就大大的优化了性能)
$nextTick
将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick
一样,不同的是回调的 this
自动绑定到调用它的实例上。
在DOM加载完成后在进行操作
异步操作改变DOM如果需要在改变DOM后对其进行操作就可以使用nextTick的回调
使用的两种方式 : Vue.nextTick() this.$nextTick()
this.$nextTick().then(()=>)//应为nextTick返回的是一个promise所以可以在后面.then()
this.$nextTick(()=>{})
实例
getFilmsBannerList().then(resp=>{
if(resp.status==0){
this.bannerList = resp.data
this.$nextTick(()=>{ //在获取数据后对DOM进行渲染之后才会执行
this.runBanner()
})
}
})
Vue API
全局API
Vue.compile(str)
var res = Vue.compile('<div><span>{{ msg }}</span></div>')
new Vue({
data: {
msg: 'hello'
},
render: res.render, //编译模板字符串
staticRenderFns: res.staticRenderFns //静态渲染
})//和下面的相等
new Vue({
el:"#app",
data: {
msg: 'hello'
},
template : `<div>{{msg}}</div>`,
}
Vue.observable(obj)
让一个对象可响应。Vue 内部会用它来处理 data
函数返回的对象。
选项 / 数据
data
props
propsData
只能用于new Vue()创建的实例中
创建实例时传递 props。主要作用是方便测试。
computed
计算属性
methods
方法
watch
监听
component>
相关设置
- `include` - 字符串或正则表达式。只有名称匹配的组件会被缓存。
- `exclude` - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
- `max` - 数字。最多可以缓存多少组件实例。
```js
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
include
和 exclude
属性允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
匹配首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值)。匿名组件不能被匹配。
**keep-alive有两个钩子函数activated
和 deactivated**
在一个组件第一次被创建的时候钩子函数执行顺序为 create=>mounted=>activated
退出的时候执行deactivated 再次进入此组件的时候只会执行activated (这就大大的优化了性能)
$nextTick
将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick
一样,不同的是回调的 this
自动绑定到调用它的实例上。
在DOM加载完成后在进行操作
异步操作改变DOM如果需要在改变DOM后对其进行操作就可以使用nextTick的回调
使用的两种方式 : Vue.nextTick() this.$nextTick()
this.$nextTick().then(()=>)//应为nextTick返回的是一个promise所以可以在后面.then()
this.$nextTick(()=>{})
实例
getFilmsBannerList().then(resp=>{
if(resp.status==0){
this.bannerList = resp.data
this.$nextTick(()=>{ //在获取数据后对DOM进行渲染之后才会执行
this.runBanner()
})
}
})
Vue API
全局API
Vue.compile(str)
var res = Vue.compile('<div><span>{{ msg }}</span></div>')
new Vue({
data: {
msg: 'hello'
},
render: res.render, //编译模板字符串
staticRenderFns: res.staticRenderFns //静态渲染
})//和下面的相等
new Vue({
el:"#app",
data: {
msg: 'hello'
},
template : `<div>{{msg}}</div>`,
}
Vue.observable(obj)
让一个对象可响应。Vue 内部会用它来处理 data
函数返回的对象。
选项 / 数据
data
props
propsData
只能用于new Vue()创建的实例中
创建实例时传递 props。主要作用是方便测试。
computed
计算属性
methods
方法
watch
监听