1. 定义Vue组件
组件: 组件的出现,就是为了拆分Vue实例的代码量的,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功能,就可以去调用对应的组件即可;
组件化和模块化的不同:
- 模块化: 是从代码逻辑的角度进行划分的;方便代码分层开发,保证每个功能模块的职能单一;
- 组件化: 是从UI界面的角度进行划分的;前端的组件化,方便UI组件的重用;
1.1 全局组件定义的四种方式
- extend + component
// 使用Vue.extend创建全局的Vue组件
var con1 = Vue.extend({
template:'<h3>这是组件</h3>' //指定组件要展示的html结构
})
// Vue.component('组件名称',创建出来的组件模板对象)
Vue.component('myCon1',con1)
//简写
Vue.component('myCon1',Vue.extend({
template:'<h3>这是组件</h3>'
}))
html结构:
注意:驼峰命名规则
<!-- 若: myCon1 -->
<my-con1></my-con1>
<!-- 若: mycon-->
<mycon></mycon>
- Vue.component 方法
Vue.component('myCon2',{
template:'<div><h3>这是组件</h3><span>123</span></div>'
})
- 外部template标签
<div id="app">
<my-con3></my-con3>
</div>
<!-- 在被控制的app外边,使用template元素定义组件的html模板结构 -->
<template id='tmp'>
<div>
<h3>在外部定义组件结构</h3>
</div>
</template>
同时,需要使用 Vue.component 来定义组件:
Vue.component('myCon3',{
template:'#tmp'
})
注意: 组件中的DOM结构,有且只能有唯一的根元素(Root Element)来进行包裹!
template:'<h3>这是组件</h3><span>123</span>' // 报错:Component template should contain exactly one root element.
- 对象字面量
- 页面结构
<div id="app">
<!-- <my-con></my-con> -->
<login></login>
</div>
- vue实例定义
// 通过对象字面量的形式定义一个组件模板对象
var login = {
template:'<h3>组件</h3>'
}
// Vue.component('myCon',login) //全局
var vm = new Vue({
el:'#app',
data:{},
methods:{},
components:{
// 'myCon':login
// 简写
login //等同于'login':login
}
})
1.2 定义局部子组件
- 组件实例定义方式:
<template id='tmp2'>
<div>
<h3>这是tmp2</h3>
</div>
</template>
var vm = new Vue({
el:'#app2',
data:{
},
methods:{
},
components:{
myCon2:{
template:'#tmp2'
}
}
})
- 引用组件:
<div id="app2">
<my-con2></my-con2>
</div>
1.3 组件的数据和事件
- 组件中的 data 除了必须为一个方法之外,这个方法内部,还必须返回一个对象才行;
- 组件中 的data 数据,使用方式,和实例中的 data 使用方式完全一样
//<my-con4></my-con4>
Vue.component('myCon4',{
template:'<h3 @click="login">这是组件---{{msg}}</h3>',
data:function() {
return {
msg:'这是组件中的data定义的数据'
}
},
methods:{
login() {
alert(this.msg)
}
}
})
- 【重点】为什么组件中的data属性必须定义为一个方法并返回一个对象
方法 !== 方法 对象 === 对象 防止多个组件使用混乱
2. 组件切换
2.1 使用flag标识符
- 页面结构:
<div id="app">
<a href="javascript:;" @click.prevent="flag='login'">登录</a>
<a href="javascript:;" @click.prevent="flag='register'">注册</a>
<a href="javascript:;" @click.prevent="flag='about'">关于</a>
<login v-if="flag == 'login'"></login>
<register v-else-if="flag=='register'"></register>
<about v-else="flag"></about>
</div>
- Vue实例定义:
<script>
Vue.component('login',{
template:'<h3>登录组件</h3>'
})
Vue.component('register',{
template:'<h3>注册组件</h3>'
})
Vue.component('about',{
template:'<h3>关于组件</h3>'
})
var vm = new Vue({
el:'#app',
data:{
flag:'login'
},
methods:{},
})
2.2 使用:is
属性
Vue提供了 component ,来展示对应名称的组件
总结:当前学习了几个 Vue 提供的标签了???
component, template, transition, transitionGroup
- 页面结构
<div id="app">
<a href="" @click.prevent="comName='login'">登录</a>
<a href="" @click.prevent="comName='register'">注册</a>
<component :is="comName"></component>
<!-- component 是一个占位符, :is 属性,可以用来指定要展示的组件的名称 -->
</div>
- Vue实例定义
Vue.component('login',{
template:'<h3>登录</h3>'
})
Vue.component('register',{
template:'<h3>注册</h3>'
})
var vm = new Vue({
el:'#app',
data:{
comName:'login'
}
})
3. 父子组件相传
3.1 父向子传值
- 使用
v-bind
或简化指令,将数据传递到子组件中:
<div id="app">
<mycon v-bind:pmsg="msg"></mycon>
</div>
-
Vue实例定义
注意:
- 一定要使用
props
属性来定义父组件传递过来的数据 - 父组件的数据只读不可更改
- 一定要使用
var vm = new Vue({
el:'#app',
data:{
msg:'这是父组件的数据'
},
components:{
mycon:{
data(){ //子组件的data属性必须定义为一个方法,并返回一个对象
return {
child:'子组件数据'
}
},
template:'<h3 @click="change">这是子组件--{{pmsg}}--{{child}}</h3>',
props:['pmsg'],
methods:{
change(){
this.child = '子组件数据被修改了', //子组件可读可写
this.pmsg = '父的数据不能被修改' //父数据只读不可写
//报错:[Vue warn]: Avoid mutating a prop directly since the value will be overwritten
}
}
}
}
})
3.2 父向子传方法
- 自定义一个属性将父方法传递进去
<con @pclick="show"></con>
- 在子方法中拿到父方法
this.$emit('pclick')
<body>
<div id="app">
<con @pclick="show"></con>
<!--自定义一个属性将父方法传递进去-->
</div>
<template id="tmp">
<div>
<h3>子组件</h3>
<input type="button" value="子组件按钮,触发父组件方法" @click="myclick">
</div>
</template>
<script>
var con = {
template:'#tmp',
methods:{
myclick(){
console.log('调用了子组件的方法')
this.$emit('pclick') //拿到父组件传过来的pclick方法
// emit:发射 调用 触发
}
}
}
var vm = new Vue({
el:'#app',
data:{},
methods:{
show(){
console.log("调用了父组件的方法")
}
},
components:{
con
}
})
</script>
</body>
3.3 子向父传值
- 父组件将方法的引用,传递到子组件内部,子组件在内部调用父组件传递过来的方法,同时把要发送给父组件的数据,在调用方法的时候当作参数传递进去;
- 在父组件方法中接收子数据
show(data){this.parent=data}
var con = {
template:'#tmp',
data(){
return {
child:{name:'cc',age:16}
}
},
methods:{
myclick(){
this.$emit('pclick',this.child) //拿到父组件传过来的pclick方法
//把要发送给父组件的数据,在调用方法的时候当作参数传递进去;
}
}
}
var vm = new Vue({
el:'#app',
data:{
parent:null
},
methods:{
show(data){
//data为子组件传递过来的数据
this.parent=data
}
},
components:{
con
}
})
- 子组件通过父组件的方法修改父组件的属性
function A() {
this.name = "name1";
this.changeName = (name) => {
this.name = name;
}
}
function B(func) {
this.func = func;
this.click = () => {
this.func("name2");
};
}
var a = new A();
console.log(a);
// {
// changeName: (name) => { this.name = name; }
// name: "name1"
// }
var b = new B(a.changeName);
console.log(b);
// {
// func: (name) => { this.name = name; }
// click: () => { this.func("name2"); }
// }
b.click();
console.log(a);
// {
// changeName: (name) => { this.name = name; }
// name: "name2"
// }
评论列表案例
目标:主要练习父子组件之间传值
- html结构
<div id="app">
<cms-box @func="loadContent"></cms-box>
<ul class="list-group">
<li v-for="(item,i) in list" :key="i" class="list-group-item" @click="del(i)">
<span class="badge">评论人: {{ item.user }}</span>
{{ item.comment }}
</li>
</ul>
</div>
<template id='cmsTmp'>
<!-- 组件中的DOM结构有且仅有一个根目录 -->
<div>
<div class="form-group">
<label>评论人:</label>
<input type="text" class="form-control" v-model="user">
</div>
<div class="form-group">
<label>评论内容:</label>
<textarea class="form-control" v-model="comment"></textarea>
</div>
<div class="form-group">
<input type="button" value="发表评论" class="btn btn-primary" @click="postCmts">
</div>
</div>
</template>
- 父向子传方法
var cmsBox = {
template:'#cmsTmp',
data(){
return {
user:'',
comment:'',
}
},
methods:{
postCmts(){
var content = {id:Date.now(),user:this.user,comment:this.comment}
// 获取之前的数据转成数组形式
var list = JSON.parse(localStorage.getItem('content') || '[]')
// JSON.parse('[]') //[]
//将新数据插入到数组中
list.unshift(content)
//将数组转成字符串添加到localStorage中
localStorage.setItem('content',JSON.stringify(list))
// 清空输入框
this.user = this.comment = ''
this.$emit('func') //拿到父元素的方法
// <cms-box @func="loadContent"></cms-box>
}
}
}
var vm = new Vue({
el:"#app",
data:{
list:[
{id:Date.now(),user:'李白',comment:'不及汪伦送我情'},
{id:Date.now(),user:'杜甫',comment:'商女不知亡国恨'},
{id:Date.now(),user:'骆宾王',comment:'曲项向天歌'}
]
},
methods:{
loadContent(){
// 从localStorage中加载评论列表
var list = JSON.parse(localStorage.getItem('content') || '[]')
this.list = list
}
},
created(){
// 调用加载评论列表方法
this.loadContent()
},
components:{
cmsBox
}
})
- 使用钩子函数
var cmsBox = {
template:'#cmsTmp',
data(){
return {
user:'',
comment:'',
}
},
methods:{
postCmts(){
this.$emit('func',this.user,this.comment)
//<cms-box @func="loadContent"></cms-box>
this.user = this.comment = ''
}
}
}
var vm = new Vue({
el:"#app",
data:{
list:[
{id:Date.now(),user:'李白',comment:'不及汪伦送我情'},
{id:Date.now(),user:'杜甫',comment:'商女不知亡国恨'},
{id:Date.now(),user:'骆宾王',comment:'曲项向天歌'}
]
},
created(){
// 获取localStroage中的content
this.list = JSON.parse(localStorage.getItem('content') || '[]')
console.log(localStorage.getItem('content'))
},
beforeUpdate(){
// 将list保存到localStorage中
localStorage.setItem('content',JSON.stringify(this.list))
},
methods:{
loadContent(user,comment){
// 将数据添加到list中
this.list.unshift({id:Date.now(),user:user,comment:comment})
},
del(i) {
this.list.splice(i, 1);
}
},
components:{
cmsBox
}
})
4. this.$refs获取元素和组件
ref 是 英文单词 【reference】 值类型 和 引用类型 referenceError
<body>
<div id="app">
<input type="button" value="获取元素" @click="getElement">
<h3 ref="myh3">哈啊哈哈哈哈</h3>
<login ref="mylogin"></login>
</div>
<script>
var login = {
template:'<h3>登录组件</h3>',
data(){
return {
son:'子组件数据'
}
},
methods:{
show(){
console.log('调用了子组件方法')
}
}
}
var vm = new Vue({
el:'#app',
data:{ },
methods:{
getElement(){
console.log(this.$refs.myh3.innerText) //哈啊哈哈哈哈
console.log(this.$refs.mylogin.son) //子组件数据
this.$refs.mylogin.show() //子组件数据
}
},
components:{
login
}
})
</script>
</body>
5. 路由
- 对于普通的网站,所有的超链接都是URL地址,所有的URL地址都对应服务器上对应的资源;
- 对于单页面应用程序来说,主要通过URL中的hash(#号)来实现不同页面之间的切换,同时,hash有一个特点:HTTP请求中不会包含hash相关的内容;所以,单页面程序中的页面跳转主要用hash实现;
- 在单页面应用程序中,这种通过hash改变来切换页面的方式,称作前端路由(区别于后端路由);
5.1 在 vue 中使用 vue-router
-
导入 vue-router 组件类库:
vue-router-3.0.1.js
-
使用 router-link 组件来导航
-
使用 router-view 组件来显示匹配到的组件
-
创建组件
-
创建一个路由 router 实例,通过 routers 属性来定义路由匹配规则
-
使用 router 属性来使用路由规则
<body>
<div id="app">
<!-- 2. 使用router-link组件来导航 -->
<router-link to="/login">登录</router-link>
<router-link to="/register">注册</router-link>
<!-- 3. 使用router-view组件来显示匹配到的组件 -->
<router-view></router-view>
</div>
<script>
// 4. 创建组件
var login = {
template:'<h3>登录组件</h3>'
};
var register = {
template:'<h3>注册组件</h3>'
}
// 5. 创建一个路由router实例
var router = new VueRouter({
// 通过routes属性来定义路由匹配规则
routes:[
{path:'/',redirect:'/login'},
// redirect重定向,与node中的重定向不一回事
{path:'/login',component:login},
// path:监听哪个路由连接地址
// component:必须是一个组件模板对象,不能是组件的引用名称
{path:'/register',component:register},
],
linkActiveClass:'myclass' //修改默认激活类
})
var vm = new Vue({
el:'#app',
data:{},
// 6 使用router属性来使用路由规则
router, //将路由规则对象,注册到VM实例上,用来监听url地址的变化,然后展示对应的组件
})
</script>
</body>
5.2 在路由规则中定义参数
- query
若 to="/login?name=cc&age=16"
则 路由规则不需修改
{{this.$route.query.name}}
- params
若 to="/register/cc/16"
则 path:'/register/:name/:age'
{{this.$route.params.name}}
<body>
<div id="app">
<router-link to="/login?name=cc&age=16">登录</router-link>
<router-link to="/register/cc/16">注册</router-link>
<router-view></router-view>
</div>
<script>
var login = {
template:'<h3>登录组件 --{{this.$route.query.name}}:{{$route.query.age}}</h3>',
// this可省略
created(){
// console.log(this.$route)
console.log(this.$route.query) //{name: "cc", age: "16"}
}
}
var register = {
template:'<h3>注册组件--{{this.$route.params.name}}:{{$route.params.age}}</h3>',
created(){
console.log(this.$route.params) //{name: "cc", age: "16"}
}
}
var router = new VueRouter({
routes:[
{path:'/login',component:login},
//若 to="/login?name=cc&age=16" 则 路由规则不需修改
{path:'/register/:name/:age',component:register},
// 若 to="/register/cc/16" 则 path:'/register/:name/:age'
]
})
var vm = new Vue({
el:'#app',
router
})
</script>
</body>
5.2 路由嵌套child
<body>
<div id="app">
<router-link to="/account">Account</router-link>
<router-view></router-view> <!--展示account组件-->
</div>
<template id="tmp">
<div>
<h3>这是account组件</h3>
<router-link to="/account/login">登录</router-link>
<router-link to="/account/register">注册</router-link>
<router-view></router-view> <!--展示login或register组件-->
</div>
</template>
<script>
var account = {
template:'#tmp',
}
var login = {
template:'<h3>登录</h3>'
}
var register = {
template:'<h3>注册</h3>'
}
var router = new VueRouter({
routes:[
//1
// {path:'/account',component:account},
// {path:'/account/login',component:login},
// {path:'/account/register',component:register}
// 页面跳转了,不符合嵌套关系
//2
{
path:'/account',
component:account,
children:[
{path:'login',component:login},
{path:'register',component:register}
//子路由的 path 前面不要带 / ,否则永远以根路径开始请求
]
}
]
})
var vm = new Vue({
el:'#app',
data:{},
methods:{},
router
})
</script>
</body>
5.3 命名视图实现经典布局
- name属性指定要展示的组件 :
<router-view name="left"></router-view>
<body>
<div id="app">
<router-view></router-view>
<router-view name="left"></router-view>
<router-view name="main"></router-view>
<!-- name属性指定要展示的组件 -->
</div>
<script>
var header = {
template: '<h1 class="header">Header头部区域</h1>'
}
var leftBox = {
template: '<h1 class="left">Left侧边栏区域</h1>'
}
var mainBox = {
template: '<h1 class="main">mainBox主体区域</h1>'
}
// 创建路由对象
var router = new VueRouter({
routes: [
{path:'/',components:{
'default':header,
'left':leftBox,
'main':mainBox
// 属性可以不加引号
}}
]
})
var vm = new Vue({
el: '#app',
data: {},
methods: {},
router
});
</script>
</body>
- CSS 样式:
<style>
html,
body,
h1 {
margin: 0;
padding: 0;
}
.header {
background-color: skyblue;
width: 100%;
height: 100px;
line-height: 100px;
}
.container {
display:flex;
height: 800px;
}
.left {
flex: 1;
background-color: greenyellow;
}
.main {
flex:3;
background-color: yellow;
}
</style>
6. 名称案例
需求: 名
和 姓
两个文本框的内容改变,则全名的文本框中的值也跟着改变
6.1 method方法
<body>
<div id="app">
<input type="text" v-model="firstname" @keyup="getfullname">
<input type="text" v-model="lastname" @keyup="getfullname">
<input type="text" v-model="fullname">
</div>
<script>
var vm = new Vue({
el:'#app',
data:{
firstname:'',
lastname:'',
fullname:'',
},
methods:{
getfullname(){
this.fullname = this.firstname + '-' + this.lastname
}
}
})
</script>
</body>
6.2 watch属性
- 监听data
中属性的改变
<body>
<div id="app">
<input type="text" v-model="firstname">+
<input type="text" v-model="lastname">=
<input type="text" v-model="fullname">
</div>
<script>
var vm = new Vue({
el:'#app',
data:{
firstname:'',
lastname:'',
fullname:'',
},
methods:{},
watch:{
//watch:可以监视 data 中指定数据的变化,然后触发这个 watch 中对应function 处理函数
// 作为属性名: firt-name 必须加引号 first可不加引号
firstname:function(nVal,oVal){
// 第一个参数是新数据,第二个参数是旧数据
// console.log(oVal,nVal)
this.fullname = nVal + '-' + this.lastname
},
lastname:function(nVal) {
this.fullname = this.firstname + '-' + nVal
}
}
})
</script>
</body>
- 监听路由对象的改变
<body>
<div id="app">
<router-link to="/login">登录</router-link>
<router-link to="/register">注册</router-link>
<!-- <login></login>
<register></register> -->
<router-view></router-view>
</div>
<script>
var login ={
template:'<h3>登录组件</h3>'
}
var register ={
template:'<h3>注册组件</h3>'
}
// 创建一个路由对象
var router = new VueRouter({
routes: [ // 路由规则数组
{ path: '/', redirect: '/login' },
{ path: '/login', component: login },
{ path: '/register', component: register }
],
linkActiveClass: 'myactive' // 和激活相关的类
})
var vm = new Vue({
el:'#app',
data:{
},
methods:{
},
router,
watch:{
'$route.path':function(nVal,oVal){
if(nVal === '/login') {
console.log('登录页面')
} else if(nVal === '/register') {
console.log('注册页面')
}
}
}
})
</script>
</body>
6.3 computed计算属性
-
注意: 计算属性,在引用的时候不要加()去掉用,直接把他当做普通属性去使用
computed:{}
-
只要计算属性function内部中所用的任何data中的数据发生了变化,都会重新计算这个计算属性的值
-
计算属性的求值结果会被缓存,方便下次直接使用,若数据未发生变化,则不会重新计算
只打印一次ok
<body> <div id="app"> <input type="text" v-model="firstname">+ <input type="text" v-model="lastname">= <input type="text" v-model="fullname"> <p> {{fullname}} </p> <p> {{fullname}} </p> <p> {{fullname}} </p> <p> {{fullname}} </p> <!-- 只打印一次 ok --> </div> <script> var vm = new Vue({ el:'#app', data:{ firstname:'', lastname:'', }, methods:{}, computed:{ fullname:function(){ console.log('ok') return this.firstname + '-' + this.lastname } } }) </script> </body>
- 默认只有`getter`的计算属性:
```html
<div id="app">
<input type="text" v-model="firstName"> +
<input type="text" v-model="lastName"> =
<span>{{fullName}}</span>
</div>
<script>
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
firstName: 'jack',
lastName: 'chen'
},
methods: {},
computed: { // 计算属性; 特点:当计算属性中所以来的任何一个 data 属性改变之后,都会重新触发 本计算属性 的重新计算,从而更新 fullName 的值
fullName() {
return this.firstName + ' - ' + this.lastName;
}
}
});
</script>
- 定义有
getter
和setter
的计算属性:
<div id="app">
<input type="text" v-model="firstName">
<input type="text" v-model="lastName">
<!-- 点击按钮重新为 计算属性 fullName 赋值 -->
<input type="button" value="修改fullName" @click="changeName">
<span>{{fullName}}</span>
</div>
<script>
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
firstName: 'jack',
lastName: 'chen'
},
methods: {
changeName() {
this.fullName = 'TOM - chen2';
}
},
computed: {
fullName: {
get: function () {
return this.firstName + ' - ' + this.lastName;
},
set: function (newVal) {
var parts = newVal.split(' - ');
this.firstName = parts[0];
this.lastName = parts[1];
}
}
}
});
</script>
6.4 watch,computed,methods对比
computed
属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。主要当作属性来使用;需要returnmethods
方法表示一个具体的操作,主要书写业务逻辑;watch
一个对象,键是需要观察的表达式,值是对应回调函数。主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作;可以看作是computed
和methods
的结合体;