Vue 3 Snippets https://marketplace.visualstudio.com/items?itemName=hollowtree.vue-snippets
Vetur https://marketplace.visualstudio.com/items?itemName=octref.vetur
vue优势:MVVM 在svue中,程序员不需要操作DOM。只需要把数据维护好即可
不建议在vue中安装jquery
在终端运行如下的命令,
安装webpack 相关的两个包:npm install [email protected] [email protected]
运行如下的命令,即可在项目中安装此插件:npm install [email protected]
运行如下的命令安装对应的依赖包:npm i [email protected]@babel/[email protected]@babel/[email protected]
数据驱动视图:
双向数据绑定:
在网页中,form 表单负责采集数据,Ajax 负责提交数据。
注意:数据驱动视图和双向数据绑定的底层原理是 MVVM(Mode 数据源、View 视图、ViewModel 就是 vue 的实例)
v-text
指令的缺点:会覆盖元素内部原有的内容!{{ }}
插值表达式:在实际开发中用的最多,只是内容的占位符,不会覆盖原有的内容!v-html
指令的作用:可以把带有标签的字符串,渲染成真正的 HTML 内容!注意:插值表达式只能用在元素的内容节点中,不能用在元素的属性节点中!
在 vue 中,可以使用 v-bind:
指令,为元素的属性动态绑定值;
简写是英文的 :
在使用 v-bind 属性绑定期间,如果绑定内容需要进行动态拼接,则字符串的外面应该包裹单引号,例如:
<div :title="'box' + index">这是一个 divdiv>
v-on:
简写是 @
语法格式为:
<button @click="add">button>
methods: {
add() {
// 如果在方法中要修改 data 中的数据,可以通过 this 访问到
this.count += 1
}
}
$event
的应用场景:如果默认的事件对象 e 被覆盖了,则可以手动传递一个 $event。例如:
<button @click="add(3, $event)">button>
methods: {
add(n, e) {
// 如果在方法中要修改 data 中的数据,可以通过 this 访问到
this.count += 1
}
}
.prevent阻止默认行为(例如:阻止a 连接的跳转、阻止表单的提交等)
.stop阻止事件冒泡.capture以捕获模式触发当前的事件处理函数.once绑定的事件只触发1次
.self只有在event.target是当前元素自身时触发事件处理函数
事件修饰符:
.prevent
<a @click.prevent="xxx">链接a>
.stop
<button @click.stop="xxx">按钮button>
.number自动将用户的输入值转为数值类型
<input v-model.number="age" />
.trim自动过滤用户输入的首尾空白字符
<input v-model.trim="msg" />
.lazy在“change”时而非“input”时更新
<input v-model.lazy="msg" />
v-show
的原理是:动态为元素添加或移除 display: none
样式,来实现元素的显示和隐藏
v-if
的原理是:每次动态创建或移除元素,实现元素的显示和隐藏
在实际开发中,绝大多数情况,不用考虑性能问题,直接使用 v-if 就好了!!!
v-if 指令在使用的时候,有两种方式:
直接给定一个布尔值 true 或 false
<p v-if="true">被 v-if 控制的元素p>
给 v-if 提供一个判断条件,根据判断的结果是 true 或 false,来控制元素的显示和隐藏
<p v-if="type === 'A'">良好p>
<div v-if="type === 'A'">优秀div>
<div v-else-if="type === 'B'">良好div>
<div v-else-if="type === 'C'">一般div>
<div v-else>差div>
items 是待循环的数组
item 是被循环的每一项
v-for 指令还支持一个可选的第二个参数,即当前项的索引。语法格式为(item,index)in items
<div id="app">
<table class="table table-bordered table-hover table-striped">
<thead>
<th>索引th>
<th>Idth>
<th>姓名th>
thead>
<tbody>
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ index }}td>
<td>{{ item.id }}td>
<td>{{ item.name }}td>
tr>
tbody>
table>
div>
lab for属性
<input type="checkbox" id="cb1">
<label for="cb1">男label>
<hr>
<input type="checkbox" id="cb2">
<label for="cb2">女label>
要定义到 filters 节点下,本质是一个函数
在过滤器函数中,一定要有 return 值
在过滤器的形参中,可以获取到“管道符”前面待处理的那个值
如果全局过滤器和私有过滤器名字一致,此时按照“就近原则”,调用的是”私有过滤器“
<body>
<div id="app">
<p>message 的值是:{{ message | capi }}p>
div>
<script src="./lib/vue-2.6.12.js">script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello vue.js'
},
// 过滤器函数,必须被定义到 filters 节点之下
// 过滤器本质上是函数
filters: {
// 注意:过滤器函数形参中的 val,永远都是“管道符”前面的那个值
capi(val) {
// 字符串有 charAt 方法,这个方法接收索引值,表示从字符串中把索引对应的字符,获取出来
// val.charAt(0)
const first = val.charAt(0).toUpperCase()
// 字符串的 slice 方法,可以截取字符串,从指定索引往后截取
const other = val.slice(1)
// 强调:过滤器中,一定要有一个返回值
return first + other
}
}
})
script>
body>
定义全局过滤器,不用定义到filter里面
// 使用 Vue.filter() 定义全局过滤器
Vue.filter('capi', function (str) {
const first = str.charAt(0).toUpperCase()
const other = str.slice(1)
return first + other + '~~~'
})
<body>
<div id="app">
<input type="text" v-model="username">
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script src="./lib/jquery-v3.6.0.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
username: 'admin'
},
// 所有的侦听器,都应该被定义到 watch 节点下
watch: {
// 侦听器本质上是一个函数,要监视哪个数据的变化,就把数据名作为方法名即可
// 新值在前,旧值在后
username(newVal) {
if (newVal === '') return
// 1. 调用 jQuery 中的 Ajax 发起请求,判断 newVal 是否被占用!!!
$.get('https://www.escook.cn/api/finduser/' + newVal, function (result) {
console.log(result)
})
}
}
})
</script>
</body>
默认情况下,组件在初次加载完毕后不会调用watch 侦听器。如果想让watch 侦听器立即被调用,则需要使用immediate选项。示例代码如下:
watch:{
username:{
//handler是固定写法,表示当username的值变化时,自动调用handler处理函数
handler:asyncfunction(newVal){
if(newVal==='')return
const{data:res}=awaitaxios.get('https://www.escook.cn/api/finduser/'+newVal)
console.log(res)
},
//表示页面初次渲染好之后,就立即触发当前的watch侦听器
immediate:true
}
}
watch: {
// 定义对象格式的侦听器
username: {
// 侦听器的处理函数
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
// immediate 选项的默认值是 false
// immediate 的作用是:控制侦听器是否自动触发一次!
immediate: true
}
}
watch: {
/* info: {
handler(newVal) {
console.log(newVal)
},
// 开启深度监听,只要对象中任何一个属性变化了,都会触发“对象的侦听器”
deep: true
}*/
// 如果要侦听的是对象的子属性的变化,则必须包裹一层单引号
'info.username'(newVal) {
console.log(newVal)
}
}
计算属性指的是通过一系列运算之后,最终得到一个属性值。这个动态计算出来的属性值可以被模板结构或methods 方法使用。
特点:
好处:
所有的计算属性,都要定义到 computed 节点之下
<body>
<div id="app">
<div>
<span>R:</span>
<input type="text" v-model.number="r">
</div>
<div>
<span>G:</span>
<input type="text" v-model.number="g">
</div>
<div>
<span>B:</span>
<input type="text" v-model.number="b">
</div>
<hr>
<!-- 专门用户呈现颜色的 div 盒子 -->
<!-- 在属性身上,: 代表 v-bind: 属性绑定 -->
<!-- :style 代表动态绑定一个样式对象,它的值是一个 { } 样式对象 -->
<!-- 当前的样式对象中,只包含 backgroundColor 背景颜色 -->
<div class="box" :style="{ backgroundColor: rgb }">
{{ rgb }}
</div>
<button @click="show">按钮</button>
</div>
<script>
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
// 红色
r: 0,
// 绿色
g: 0,
// 蓝色
b: 0
},
methods: {
// 点击按钮,在终端显示最新的颜色
show() {
console.log(this.rgb)
}
},
// 所有的计算属性,都要定义到 computed 节点之下
// 计算属性在定义的时候,要定义成“方法格式”
computed: {
// rgb 作为一个计算属性,被定义成了方法格式,
// 最终,在这个方法中,要返回一个生成好的 rgb(x,x,x) 的字符串
rgb() {
return `rgb(${this.r}, ${this.g}, ${this.b})`
}
}
});
console.log(vm)
</script>
</body>
axios 是一个专注于网络请求的库!
axios() axios.get() axios.post() axios.delete() axios.put()
axios({
// 请求方式
method: 'GET',
// 请求的地址
url: 'http://www.liulongbin.top:3006/api/getbooks',
// URL 中的查询参数
params: {
id: 1
}
}).then(function (result) {
console.log(result)
})
document.querySelector('#btnPost').addEventListener('click', async function () {
// 如果调用某个方法的返回值是 Promise 实例,则前面可以添加 await!
// await 只能用在被 async “修饰”的方法中
//{data}解构赋值,只要返回的data数据 {data : res}结构赋值重新赋值res=data
const { data: res } = await axios({
method: 'POST',
url: 'http://www.liulongbin.top:3006/api/post',
//请求体参数
data: {
name: 'zs',
age: 20
}
})
console.log(res)
})
document.querySelector('#btnGET').addEventListener('click', async function () {
/* axios.get('url地址', {
// GET 参数
params: {}
}) */
const { data: res } = await axios.get('http://www.liulongbin.top:3006/api/getbooks', {
params: { id: 1 }
})
console.log(res)
})
document.querySelector('#btnPOST').addEventListener('click', async function () {
// axios.post('url', { /* POST 请求体数据 */ })
const { data: res } = await axios.post('http://www.liulongbin.top:3006/api/post', { name: 'zs', gender: '女' })
console.log(res)
})
https://cli.vuejs.org/zh/
使用 npm install -g @vue/cli 命令,即可方便的把它安装到自己的电脑上,cd到项目文件下 npm run serve 运行项目
单页面应用程序(英文名:Single Page Application)简称SPA,顾名思义,指的是一个Web 网站中只有唯一的一个HTML 页面,所有的功能与交互都在这唯一的一个页面内完成
在终端下运行如下的命令,创建指定名称的项目:
vue cerate 项目的名称
vue 项目中 src 目录的构成:
assets 文件夹:存放项目中用到的静态资源文件,例如:css 样式表、图片资源
components 文件夹:程序员封装的、可复用的组件,都要放到 components 目录下
main.js 是项目的入口文件。整个项目的运行,要先执行 main.js
App.vue 是项目的根组件。
在工程化的项目中,vue 要做的事情很单纯:通过main.js 把App.vue 渲染到index.html 的指定区域中
①App.vue 用来编写待渲染的模板结构
②index.html 中需要预留一个el 区域
③main.js 把App.vue 渲染到了index.html 所预留的区域中
组件化开发指的是:根据封装的思想,把页面上可重用的UI 结构封装为组件,从而方便项目的开发和维护。
template-> 组件的模板结构
script-> 组件的JavaScript 行为
style-> 组件的样式
export default{
// 所有的变量,都应该被定义到 data 节点下
data() {
return{
}
}
// 所有的侦听器,都应该被定义到 watch 节点下
watch: {
}
// 所有的过滤器,都应该被定义到 filters 节点下
filters: {
}
// 所有的函数,都应该被定义到 methods 节点下
method: {
}
// 所有的计算属性,都应该被定义到 computed 节点下
computed: {
}
}
在根组件(一般App.vue作为根组件)里面导入其他子组件
使用import 语法导入需要的组件
import Left from '@/components/Left.vue'
import Right from '@/components/Right.vue'
import Test from '@/components/Test.vue' //组件目录
使用components节点注册组件
export default {
// 2. 注册组件
components: {
Left,
Right,
Test
}
}
以标签形式使用刚才注册的组件
<Left></Left>
<Right></Right>
通过components 注册的是私有子组件例如:
在组件A的components 节点下,注册了组件F。则组件F 只能用在组件A 中;不能被用在组件C 中。
在vue 项目的main.js 入口文件中,通过Vue.component() 方法
// 导入需要被全局注册的那个组件
import Count from '@/components/Count.vue'
//参数1:字符串格式,表示组件的“注册名称”
//参数2:需要被全局注册的那个组件
Vue.component('MyCount',Count)
vue 规定:组件中封装的自定义属性是只读的,程序员不能直接修改props 的值。否则会直接报错:要想修改props 的值,可以把props 的值转存到data 中,因为data 中的数据都是可读可写的!
export default {
// props 是"自定义属性",允许使用者通过自定义属性,为当前组件指定初始值
// 自定义属性的名字,是封装者自定义的(只要名称合法即可)
// props 中的数据,可以直接在模板结构中被使用
// 注意:props 是只读的,不要直接修改 props 的值,否则终端会报错!
// props: ['init'],
props: {
// 自定义属性A : { /* 配置选项 */ },
// 自定义属性B : { /* 配置选项 */ },
// 自定义属性C : { /* 配置选项 */ },
init: {
// 如果外界使用 Count 组件的时候,没有传递 init 属性,则默认值生效
default: 0,
// init 的值类型必须是 Number 数字
type: Number,
// 必填项校验
required: true
}
},
data() {
return {
// 把 props 中的 init 值,转存到 count 上
count: this.init
}
},
methods: {
show() {
console.log(this)
}
}
}
/* 在组件使用者传参时在init前加:传数字否则为字符串*/
<h3 data-v-001>Left 组件</h3> //data-v-编号
<style>
h3[data-v-001]{
color:red;
}//选中其时要加上自定义属性
</syle>
less样式的scoped属性会自动为每一个组件自动生成一个自定义属性
<style lang="less" scoped>
h3 {
color: red;
}
</style>
默认情况下,写在.vue 组件中的样式会全局生效,
导致组件之间样式冲突的根本原因是:
①单页面应用程序中,所有组件的DOM 结构,都是基于唯一的index.html 页面进行呈现的
②每个组件中的样式,都会影响整个index.html 页面中的DOM 元素
在父组件中修改子组件时要用到/deep/
// h5[data-v-3c83f0b7] 不使用/deeep/
// [data-v-3c83f0b7] h5 使用/deeep/
// 当使用第三方组件库的时候,如果有修改第三方组件默认样式的需求,需要用到 /deep/
/deep/ h5 {
color: pink;
}
生命周期(Life Cycle)是指一个组件从创建-> 运行-> 销毁的整个阶段,强调的是一个时间段。
生命周期函数:是由vue 框架提供的内置函数,会伴随着组件的生命周期,自动按次序执行。
注意:生命周期强调的是时间段,生命周期函数强调的是时间点。
常用生命周期函数: create(), mounted()
// 创建阶段的第1个生命周期函数
beforeCreate() {
// console.log(this.info)
// console.log(this.message)
// this.show()
},
created() {
// created 生命周期函数,非常常用。
// 经常在它里面,调用 methods 中的方法,请求服务器的数据。
// 并且,把请求到的数据,转存到 data 中,供 template 模板渲染的时候使用!
this.initBookList()
},
beforeMount() {
// console.log('beforeMount')
// const dom = document.querySelector('#myh3')
// console.log(dom)
},
// 如果要操作当前组件的 DOM,最早,只能在 mounted 阶段执行
mounted() {
// console.log(this.$el)
// const dom = document.querySelector('#myh3')
// console.log(dom)
},
beforeUpdate() {
// console.log('beforeUpdate')
// console.log(this.message)
// const dom = document.querySelector('#pppp')
// console.log(dom.innerHTML)
},
// 当数据变化之后,为了能够操作到最新的 DOM 结构,必须把代码写到 updated 生命周期函数中
updated() {
// console.log('updated')
// console.log(this.message)
// const dom = document.querySelector('#pppp')
// console.log(dom.innerHTML)
},
beforeDestroy() {
console.log('beforeDestroy')
this.message = 'aaa'
console.log(this.message)
},
destroyed() {
console.log('destroyed')
// this.message = 'aaa'
}
}
在项目开发中,组件之间的最常见的关系分为如下两种:
①父子关系
②兄弟关系
①创建eventBus.js 模块,并向外共享一个Vue 的实例对象
②在数据发送方,调用bus.$emit(‘事件名称’, 要发送的数据) 方法触发自定义事件
③在数据接收方,调用bus.$on(‘事件名称’, 事件处理函数) 方法注册一个自定义事件
//子组件
<script>
export default {
props: ['msg', 'user'],
data() {
},
methods: {
}
}
</script>
<Left :msg="message" :user="userinfo"></Left>
//父组件
export default {
data() {
return {
message: 'hello 132 的宝们!',
userinfo:{ name:'wsc',age:18},
}
},
methods: {
},
components: {
Left,
Right
}
}
</script>
this.$emit(‘numchange’, this.count) 发送数据
//子组件
<script>
export default {
data() {
return {
// 子组件自己的数据,将来希望把 count 值传给父组件
count: 0,
}
},
methods: {
add() {
// 让子组件的 count 值自增 +1
this.count += 1
// 把自增的结果,传给父组件
this.$emit('numchange', this.count) //发送数据
}
}
}
</script>
//父组件
<template>
<Right @numchange="getNewCount"></Right> //numchange自定义事件,事件发生触发getNewCount函数
</template>
<script>
import Left from '@/components/Left.vue'
import Right from '@/components/Right.vue'
export default {
data() {
return {
// 定义 countFromSon 来接收子组件传递过来的数据
countFromSon: 0
}
},
methods: {
// 获取子组件传递过来的数据
getNewCount(val) {
console.log('numchange 事件被触发了!', val)
this.countFromSon = val
}
},
components: {
Left,
Right
}
}
</script>
bus.$on():接收数据方法
在vue2.x中,兄弟组件之间数据共享的方案是EventBus。
//发送方
import bus from'./eventBus.js’
exportdefault{
data(){
return{
msg:'hellovue.js'
}
},
methods:{
sendMsg(){
bus.$emit('share',this.msg)
}
}
}
//eventBus.js
importVuefrom'vue'
//向外共享Vue的实例对象
exportdefaultnewVue()
//数据接收方
importbusfrom'./eventBus.js'
exportdefault{
data(){
return{
msgFromLeft:''
}
},
created(){
bus.$on('share',val=>{
this.msgFromLeft=val
})
}
}
ref 用来辅助开发者在不依赖于jQuery 的情况下,获取DOM 元素或组件的引用。
每个vue 的组件实例上,都包含一个 r e f s 对 象 , 里 面 存 储 着 对 应 的 D O M 元 素 或 组 件 的 引 用 。 默 认 情 况 下 , 组 件 的 refs 对象,里面存储着对应的DOM 元素或组件的引用。默认情况下,组件的 refs对象,里面存储着对应的DOM元素或组件的引用。默认情况下,组件的refs 指向一个空对象。
<h1 ref="myh12">App 根组件</h1>
<script>
export default {
methods:{
showThis(){
//当前app的组件实例对象
// console.log(this.$refs.myh12)
this.$refs.myh12.style.color ='pink'
}
}
}
</script>
TypeError: Cannot read property ‘focus’ of undefined 错误 'focus’之前的属性为undefined
组件的$nextTick(cb) 方法,会把cb 回调推迟到下一个DOM 更新周期之后执行。通俗的理解是:等组件的DOM 更新完成之后,再执行cb 回调函数。从而能保证cb 回调函数可以操作到最新的DOM 元素。
//获取DOM 让文本框自动获得焦点
// this.$refs.iptRef.focus()
this.$nextTick(()=>{
this.$refs.iptRef.focus()
})
arr.foreach循环
const arr = ['小红', '你大红', '苏大强', '宝']
// forEach 循环一旦开始,无法在中间被停止
arr.forEach((item, index) => {
console.log('object')
if (item === '苏大强') {
console.log(index)
}
})
arr.some循环
arr.some((item, index) => {
console.log('ok')
if (item === '苏大强') {
console.log(index)
// 在找到对应的项之后,可以通过 return true 固定的语法,来终止 some 循环
return true
}
})
arr.every
const arr = [
{ id: 1, name: '西瓜', state: true },
{ id: 2, name: '榴莲', state: false },
{ id: 3, name: '草莓', state: true },
]
// 需求:判断数组中,水果是否被全选了!true为选中
const result = arr.every(item => item.state)
console.log(result)
reduce函数
const arr = [
{ id: 1, name: '西瓜', state: true, price: 10, count: 1 },
{ id: 2, name: '榴莲', state: false, price: 80, count: 2 },
{ id: 3, name: '草莓', state: true, price: 20, count: 3 },
]
// 需求:把购物车数组中,已勾选的水果,总价累加起来!
let amt = 0 // 总价
// arr.filter(item => item.state) 选出已选中的水果并返回一个新的数组
arr.filter(item => item.state).forEach(item => {
amt += item.price * item.count
})
console.log(amt)
// arr.filter(item => item.state).reduce((累加的结果, 当前循环项) => { }, 初始值)
const result = arr.filter(item => item.state).reduce((amt, item) => amt += item.price * item.count, 0)
console.log(result)
<template>
<div class="app-container">
<!-- Header 头部区域 -->
<Header title="购物车案例"></Header>
<!-- 循环渲染每一个商品的信息 -->
<Goods
v-for="item in list"
:key="item.id"
:id="item.id"
:title="item.goods_name"
:pic="item.goods_img"
:price="item.goods_price"
:state="item.goods_state"
:count="item.goods_count"
@state-change="getNewState"
></Goods>
<!-- Footer 区域 -->
<Footer :isfull="fullState" :amount="amt" :all="total" @full-change="getFullState"></Footer>
</div>
</template>
<script>
// 导入 axios 请求库
import axios from 'axios'
// 导入需要的组件
import Header from '@/components/Header/Header.vue'
import Goods from '@/components/Goods/Goods.vue'
import Footer from '@/components/Footer/Footer.vue'
import bus from '@/components/eventBus.js'
export default {
data() {
return {
// 用来存储购物车的列表数据,默认为空数组
list: []
}
},
// 计算属性
computed: {
// 动态计算出全选的状态是 true 还是 false
fullState() {
return this.list.every(item => item.goods_state)
},
// 已勾选商品的总价格
amt() {
// 1. 先 filter 过滤
// 2. 再 reduce 累加
return this.list
.filter(item => item.goods_state)
.reduce((total, item) => (total += item.goods_price * item.goods_count), 0)
},
// 已勾选商品的总数量
total() {
return this.list.filter(item => item.goods_state).reduce((t, item) => (t += item.goods_count), 0)
}
},
created() {
// 调用请求数据的方法
this.initCartList()
bus.$on('share', val => {
this.list.some(item => {
if (item.id === val.id) {
item.goods_count = val.value
return true
}
})
})
},
methods: {
// 封装请求列表数据的方法
async initCartList() {
// 调用 axios 的 get 方法,请求列表数据
const { data: res } = await axios.get('https://www.escook.cn/api/cart')
// 只要请求回来的数据,在页面渲染期间要用到,则必须转存到 data 中
if (res.status === 200) {
this.list = res.list
}
},
// 接收子组件传递过来的数据
// e 的格式为 { id, value }
getNewState(e) {
this.list.some(item => {
if (item.id === e.id) {
item.goods_state = e.value
// 终止后续的循环
return true
}
})
},
// 接收 Footer 子组件传递过来的全选按钮的状态
getFullState(val) {
this.list.forEach(item => (item.goods_state = val))
}
},
components: {
Header,
Goods,
Footer
}
}
</script>
<style lang="less" scoped>
.app-container {
padding-top: 45px;
padding-bottom: 50px;
}
</style>
<!-- 渲染 Left 组件和 Right 组件 -->
<!-- 1. component 标签是 vue 内置的,作用:组件的占位符 -->
<!-- 2. is 属性的值,表示要渲染的组件的名字 -->
<!-- 3. is 属性的值,应该是组件在 components 节点下的注册名称 -->
<!-- keep-alive 会把内部的组件进行缓存,而不是销毁组件 -->
<!-- 在使用 keep-alive 的时候,可以通过 include 指定哪些组件需要被缓存; -->
<!-- 或者,通过 exclude 属性指定哪些组件不需要被缓存;但是:不要同时使用 include 和 exclude 这两个属性 -->
<keep-alive exclude="MyRight"> //myRight右面组件 参数组件名
<component :is="comName"></component>
</keep-alive>
//在Right组件里写
export default {
// 当提供了 name 属性之后,组件的名称,就是 name 属性的值
// 对比:
// 1. 组件的 “注册名称” 的主要应用场景是:以标签的形式,把注册好的组件,渲染和使用到页面结构之中
// 2. 组件声明时候的 “name” 名称的主要应用场景:结合 标签实现组件缓存功能;以及在调试工具中看到组件的 name 名称
name: 'MyRight'
}
当组件被缓存时,会自动触发组件的deactivated生命周期函数。当组件被激活时,会自动触发组件的activated生命周期函数
expor default{
deactivated(){
console.log('组件被缓存了')
},
activated(){
console.log('组件被激活了')
}
}
插槽(Slot)是vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。可以把插槽认为是组件封装期间,为用户预留的内容的占位符。
//left组件
<template>
<div class="left-container">//组件用div包裹,建议为div起名字为"组件名-container"
<h3>Left 组件</h3>
<hr />
<!-- 声明一个插槽区域 -->
<!-- vue 官方规定:每一个 slot 插槽,都要有一个 name 名称 -->
<!-- 如果省略了 slot 的 name 属性,则有一个默认名称叫做 default -->
<slot name="default"> //带名字的插槽叫具名插槽
<h6>这是 default 插槽的后备内容</h6>
</slot>
</div>
</template>
//主组件app
<Left>
<!-- 默认情况下,在使用组件的时候,提供的内容都会被填充到名字为 default 的插槽之中 -->
<!-- 1. 如果要把内容填充到指定名称的插槽中,需要使用 v-slot: 这个指令 -->
<!-- 2. v-slot: 后面要跟上插槽的名字 -->
<!-- 3. v-slot: 指令不能直接用在元素身上,必须用在 template 标签上 -->
<!-- 4. template 这个标签,它是一个虚拟的标签,只起到包裹性质的作用,但是,不会被渲染为任何实质性的 html 元素 -->
<!-- 5. v-slot: 指令的简写形式是 # -->
<template v-slot:default> //#default
<p>这是在 Left 组件的内容区域,声明的 p 标签</p>
</template>
</Left>
//APP中
<template #content="{ msg, user }">//解构用法,多个参数传过来的是一个对象msg user为里面的数据
<div>
<p>啊,大海,全是水。</p>
<p>啊,蜈蚣,全是腿。</p>
<p>啊,辣椒,净辣嘴。</p>
<p>{{ msg }}</p>
<p>{{ user.name }}</p>
</div>
</template>
//article中
<template>
<!-- 文章的内容 -->
<div class="content-box">
<!-- 在封装组件时,为预留的 <slot> 提供属性对应的值,这种用法,叫做 “作用域插槽” -->
<slot name="content" msg="hello vue.js" :user="userinfo"></slot>
</div>
</template>
/*在data中定义
data() {
return {
// 用户的信息对象
userinfo: {
name: 'zs',
age: 20
}
}
}*/
vue 中的自定义指令分为两类,分别是:
⚫私有自定义指令
⚫全局自定义指令
只能自己用
在每个vue 组件中,可以在directives节点下声明私有自定义指令。
注意bind()和update()
export default{
directives: {
// 定义名为 color 的指令,指向一个配置对象
/* color: {
// 当指令第一次被绑定到元素上的时候,会立即触发 bind 函数
// 形参中的 el 表示当前指令所绑定到的那个 DOM 对象
bind(el, binding) {
console.log('触发了 v-color 的 bind 函数')
el.style.color = binding.value
},
// 在 DOM 更新的时候,会触发 update 函数
update(el, binding) {
console.log('触发了 v-color 的 update 函数')
el.style.color = binding.value
}
} */
//当bind()和update()函数里的代码一样时可简写为
color(el, binding) {
el.style.color = binding.value
}
}
}
使用
<h1 v-color="color">App 根组件</h1>
<p v-color="'red'">测试</p>
<button @click="color = 'green'">改变 color 的颜色值</button>
全局共享的自定义指令需要通过“Vue.directive()”进行声明
//写在main.js中
// 全局自定义指令
/* Vue.directive('color', {
bind(el, binding) {
el.style.color = binding.value
},
update(el, binding) {
el.style.color = binding.value
}
}) */
Vue.directive('color', function(el, binding) {
el.style.color = binding.value
})
官网 https://eslint.bootcss.com/
//main.js里
import Vue from 'vue'
import App from './App.vue'
import axios from 'axios'
Vue.config.productionTip = false
// 全局配置 axios 的请求根路径
axios.defaults.baseURL = 'http://www.liulongbin.top:3006'
// 把 axios 挂载到 Vue.prototype 上,供每个 .vue 组件的实例直接使用
Vue.prototype.$http = axios
// 今后,在每个 .vue 组件中要发起请求,直接调用 this.$http.xxx
// 但是,把 axios 挂载到 Vue 原型上,有一个缺点:不利于 API 接口的复用!!!
new Vue({
render: h => h(App)
}).$mount('#app')
//left
<template>
<div class="left-container">
<h3>Left 组件</h3>
<button @click="getInfo">发起 GET 请求</button>
<button @click="btnGetBooks">获取图书列表的数据</button>
</div>
</template>
<script>
// import axios from 'axios'
export default {
methods: {
async getInfo() {
const { data: res } = await this.$http.get('/api/get')
console.log(res)
},
// 点击按钮,获取图书列表的数据
async btnGetBooks() {
const { data: res } = await this.$http.get('/api/getbooks')
console.log(res)
}
}
}
</script>
//right
<template>
<div class="right-container">
<h3>Right 组件</h3>
<button @click="postInfo">发起 POST 请求</button>
<button @click="btnGetBooks">获取图书列表的数据</button>
</div>
</template>
<script>
// import axios from 'axios'
export default {
methods: {
async postInfo() {
const { data: res } = await this.$http.post('/api/post', { name: 'zs', age: 20 })
console.log(res)
},
// 点击按钮,获取图书列表的数据
async btnGetBooks() {
const { data: res } = await this.$http.get('/api/getbooks')
console.log(res)
}
}
}
</script>
//app
<template>
<div>
<h1>App 根组件</h1>
<button @click="btnGetBooks">获取图书列表的数据</button>
<hr />
<div class="box">
<Left></Left>
<Right></Right>
</div>
</div>
</template>
<script>
import Left from '@/components/Left.vue'
import Right from '@/components/Right.vue'
export default {
methods: {
// 点击按钮,获取图书列表的数据
async btnGetBooks() {
const { data: res } = await this.$http.get('/api/getbooks')
console.log(res)
}
},
components: {
Left,
Right
}
}
</script>
路由(英文:router)就是对应关系。
①用户点击了页面上的路由链接
②导致了URL 地址栏中的Hash 值发生了变化
③前端路由监听了到Hash 地址的变化
④前端路由把当前Hash 地址对应的组件渲染都浏览器中
<template>
<div class="app-container">
<h1>App 根组件</h1>
<a href="#/home">首页</a>
<a href="#/movie">电影</a>
<a href="#/about">关于</a>
<hr />
<component :is="comName"></component>
</div>
</template>
<script>
// 导入组件
import Home from '@/components/Home.vue'
import Movie from '@/components/Movie.vue'
import About from '@/components/About.vue'
export default {
name: 'App',
data() {
return {
// 在动态组件的位置,要展示的组件的名字,值必须是字符串
comName: 'Home'
}
},
created() {
// 只要当前的 App 组件一被创建,就立即监听 window 对象的 onhashchange 事件
window.onhashchange = () => {
console.log('监听到了 hash 地址的变化', location.hash)
switch (location.hash) {
case '#/home':
this.comName = 'Home'
break
case '#/movie':
this.comName = 'Movie'
break
case '#/about':
this.comName = 'About'
break
}
}
},
// 注册组件
components: {
Home,
Movie,
About
}
}
</script>
vue-router是vue.js 官方给出的路由解决方案。它只能结合vue 项目进行使用,能够轻松的管理SPA 项目中组件的切换。
vue-router 的官方文档地址:https://router.vuejs.org/zh/
在src源代码目录下,新建router/index.js 路由模块,所有的声明规则都写在index.js里
路由的嵌套在rentes[]写里children[]来存储子路由的路由
// src/router/index.js 就是当前项目的路由模块
import Vue from 'vue'
import VueRouter from 'vue-router'
// 导入需要的组件
import Home from '@/components/Home.vue'
import Movie from '@/components/Movie.vue'
import About from '@/components/About.vue'
import Tab1 from '@/components/tabs/Tab1.vue'
import Tab2 from '@/components/tabs/Tab2.vue'
import Login from '@/components/Login.vue'
import Main from '@/components/Main.vue'
// 把 VueRouter 安装为 Vue 项目的插件
// Vue.use() 函数的作用,就是来安装插件的
Vue.use(VueRouter)
// 创建路由的实例对象
const router = new VueRouter({
// routes 是一个数组,作用:定义 “hash 地址” 与 “组件” 之间的对应关系 3.声明的规则
routes: [
// 重定向的路由规则
{ path: '/', redirect: '/home' },
// 路由规则
{ path: '/home', component: Home },
// 需求:在 Movie 组件中,希望根据 id 的值,展示对应电影的详情信息
// 可以为路由规则开启 props 传参,从而方便的拿到动态参数的值
{ path: '/movie/:mid', component: Movie, props: true },//加props: true可以在movie.vue里用props['mid']接收
{
path: '/about',
component: About,
// redirect: '/about/tab1', 重定向,进去about页面时展示ta1路由
children: [
// 子路由规则
// 默认子路由:如果 children 数组中,某个路由规则的 path 值为空字符串,则这条路由规则,叫做“默认子路由”
//用默认路由要把tab1 改为tab1
{ path: '', component: Tab1 },
{ path: 'tab2', component: Tab2 }
]
},
{ path: '/login', component: Login },
{ path: '/main', component: Main }
]
})
// 为 router 实例对象,声明全局前置导航守卫
// 只要发生了路由的跳转,必然会触发 beforeEach 指定的 function 回调函数
router.beforeEach(function(to, from, next) {
// to 表示将要访问的路由的信息对象
// from 表示将要离开的路由的信息对象
// next() 函数表示放行的意思
// 分析:
// 1. 要拿到用户将要访问的 hash 地址
// 2. 判断 hash 地址是否等于 /main。
// 2.1 如果等于 /main,证明需要登录之后,才能访问成功
// 2.2 如果不等于 /main,则不需要登录,直接放行 next()
// 3. 如果访问的地址是 /main。则需要读取 localStorage 中的 token 值
// 3.1 如果有 token,则放行
// 3.2 如果没有 token,则强制跳转到 /login 登录页
if (to.path === '/main') {
// 要访问后台主页,需要判断是否有 token
const token = localStorage.getItem('token')
if (token) {
next()
} else {
// 没有登录,强制跳转到登录页
next('/login')
}
} else {
next()
}
})
export default router
1.路由链接
<router-link to="/home">首页</router-link>
2.占位符
<!-- 只要在项目中安装和配置了 vue-router,就可以使用 router-view 这个组件了 -->
<!-- 它的作用很单纯:占位符 -->
<router-view></router-view>
{
path: '/about',
component: About,
// redirect: '/about/tab1', 重定向,进去about页面时展示ta1路由
children: [
// 子路由规则
// 默认子路由:如果 children 数组中,某个路由规则的 path 值为空字符串,则这条路由规则,叫做“默认子路由”
//用默认路由要把tab1 改为tab1
{ path: '', component: Tab1 },
{ path: 'tab2', component: Tab2 }
]
},
<!-- 注意1:在 hash 地址中, / 后面的参数项,叫做“路径参数” -->
<!-- 在路由“参数对象”中,需要使用 this.$route.params 来访问路径参数 -->
<!-- 注意2:在 hash 地址中,? 后面的参数项,叫做“查询参数” -->
<!-- 在路由“参数对象”中,需要使用 this.$route.query 来访问查询参数 -->
<!-- 注意3:在 this.$route 中,path 只是路径部分;fullPath 是完整的地址 -->
<!-- 例如: -->
<!-- /movie/2?name=zs&age=20 是 fullPath 的值 -->
<!-- /movie/2 是 path 的值 -->
<router-link to="/movie/1">洛基</router-link>
<router-link to="/movie/2?name=zs&age=20">雷神</router-link>
<router-link to="/movie/3">复联</router-link>
<router-link to="/about">关于</router-link>
在浏览器中,点击链接实现导航的方式,叫做声明式导航。
例如:⚫普通网页中点击 链接、vue 项目中点击 都属于声明式导航
在浏览器中,调用API 方法实现导航的方式,叫做编程式导航。
例如:⚫普通网页中调用location.href 跳转到新页面的方式,属于编程式导航
vue-router 提供了许多编程式导航的API,其中最常用的导航API 分别是:
①this.$router.push(‘hash 地址’)
⚫跳转到指定hash 地址,并增加一条历史记录
②this.$router.replace(‘hash 地址’)
⚫跳转到指定的hash 地址,并替换掉当前的历史记录
③this.$router.go(数值n)
⚫实现导航历史前进、后退
在实际开发中,一般只会前进和后退一层页面。因此vue-router 提供了如下两个便捷方法:
①$router.back()
⚫在历史记录中,后退到上一个页面
②$router.forward()
⚫在历史记录中,前进到下一个页面
<!-- 在行内使用编程式导航跳转的时候,this 必须要省略,否则会报错! -->
每次发生路由的导航跳转时,都会触发全局前置守卫。因此,在全局前置守卫中,程序员可以对每个路由进行访问权限的控制:
// 为 router 实例对象,声明全局前置导航守卫
// 只要发生了路由的跳转,必然会触发 beforeEach 指定的 function 回调函数
router.beforeEach(function(to, from, next) {
// to 表示将要访问的路由的信息对象
// from 表示将要离开的路由的信息对象
// next() 函数表示放行的意思
//当前用户拥有后台主页的访问权限,
//直接放行:next()当前用户没有后台主页的访问权限,
//强制其跳转到登录页面:next('/login')当前用户没有后台主页的访问权限,
//不允许跳转到后台主页:next(false)
// 分析:
// 1. 要拿到用户将要访问的 hash 地址
// 2. 判断 hash 地址是否等于 /main。
// 2.1 如果等于 /main,证明需要登录之后,才能访问成功
// 2.2 如果不等于 /main,则不需要登录,直接放行 next()
// 3. 如果访问的地址是 /main。则需要读取 localStorage 中的 token 值
// 3.1 如果有 token,则放行
// 3.2 如果没有 token,则强制跳转到 /login 登录页
if (to.path === '/main') {
// 要访问后台主页,需要判断是否有 token
const token = localStorage.getItem('token')
if (token) {
next()
} else {
// 没有登录,强制跳转到登录页
next('/login')
}
} else {
next()
}
})
vant http://vant-contrib.gitee.io/vant/#/zh-CN/theme
代码优化 http://doc.toutiao.liulongbin.top
less逛网 https://less.bootcss.com/
load https://www.lodashjs.com/
cmtCount 可以写为cmt-count
cmtCount: {
// 通过数组形式,为当前属性定义多个可能的类型,可以是数字也可以是字符串
type: [Number, String],
default: 0
},
const newArr = [...Arr1, ...Arr2] //数组拼接
//APP.vue
<template>
<div>
<!-- 路由占位符 -->
<router-view></router-view>
<!-- Tabbar 区域 -->
<van-tabbar route>
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
<van-tabbar-item icon="user-o" to="/user">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style lang="less" scoped></style>
//Home.vue
<template>
<div class="home-container">
<!-- 头部区域 -->
<van-nav-bar title="黑马头条" fixed />
<!-- 导入,注册,并使用 ArticleInfo 组件 -->
<!-- 在使用组件的时候,如果某个属性名是“小驼峰”形式,则绑定属性的时候,建议改写成“连字符”格式。例如: -->
<!-- cmtCount 建议写成 cmt-count -->
<van-pull-refresh v-model="isLoading" :disabled="finished" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<ArticleInfo
v-for="item in artlist"
:key="item.id"
:title="item.title"
:author="item.aut_name"
:cmt-count="item.comm_count"
:time="item.pubdate"
:cover="item.cover"
></ArticleInfo>
</van-list>
</van-pull-refresh>
</div>
</template>
<script>
// 按需导入 API 接口
import { getArticleListAPI } from '@/api/articleAPI.js'
// 导入需要的组件
import ArticleInfo from '@/components/Article/ArticleInfo.vue'
export default {
name: 'Home',
data() {
return {
// 页码值
page: 1,
// 每页显示多少条数据
limit: 10,
// 文章的数组
artlist: [],
// 是否正在加载下一页数据,如果 loading 为 true,则不会反复触发 load 事件
// 每当下一页数据请求回来之后,千万要记得,把 loading 从 true 改为 false
loading: true,
// 所有数据是否加载完毕了,如果没有更多数据了,一定要把 finished 改成 true
finished: false,
// 是否正在下拉刷新
isLoading: false
}
},
created() {
this.initArticleList()
},
methods: {
// 封装获取文章列表数据的方法
async initArticleList(isRefresh) {
// 发起 GET 请求,获取文章的列表数据
const { data: res } = await getArticleListAPI(this.page, this.limit)
if (isRefresh) {
// 证明是下拉刷新;新数据在前,旧数据在后
// this.artlist = [新数据在后, 旧数据在前]
this.artlist = [...res, ...this.artlist]
this.isLoading = false
} else {
// 证明是上拉加载更多;旧数据在前,新数据在后
// this.artlist = [旧数据在前, 新数据在后]
this.artlist = [...this.artlist, ...res]
this.loading = false
}
if (res.length === 0) {
// 证明没有下一页数据了,直接把 finished 改为 true,表示数据加载完了!
this.finished = true
}
},
// 只要 onLoad 被调用,就应该请求下一页数据
onLoad() {
console.log('触发了 load 事件!')
// 1. 让页码值 +1
this.page++
// 2. 重新请求接口获取数据
this.initArticleList()
},
// 下拉刷新的处理函数
onRefresh() {
console.log('触发了下拉刷新!')
// 1. 让页码值 +1
this.page++
// 2. 重新请求接口获取数据
this.initArticleList(true)
}
},
// 注册组件
components: {
ArticleInfo
}
}
</script>
<style lang="less" scoped>
.home-container {
padding: 46px 0 50px 0;
}
</style>
//User.vue
<template>
<div class="user-container">
<van-nav-bar title="黑马头条" fixed />
<!-- 用户基本信息面板 -->
<div class="user-card">
<!-- 用户头像、姓名 -->
<van-cell>
<!-- 使用 title 插槽来自定义标题 -->
<template #icon>
<img src="../../assets/logo.png" alt="" class="avatar" />
</template>
<template #title>
<span class="username">用户名</span>
</template>
<template #label>
<van-tag color="#fff" text-color="#007bff">申请认证</van-tag>
</template>
</van-cell>
<!-- 动态、关注、粉丝 -->
<div class="user-data">
<div class="user-data-item">
<span>0</span>
<span>动态</span>
</div>
<div class="user-data-item">
<span>0</span>
<span>关注</span>
</div>
<div class="user-data-item">
<span>0</span>
<span>粉丝</span>
</div>
</div>
</div>
<!-- 操作面板 -->
<van-cell-group class="action-card">
<van-cell icon="edit" title="编辑资料" is-link />
<van-cell icon="chat-o" title="小思同学" is-link />
<van-cell icon="warning-o" title="退出登录" is-link />
</van-cell-group>
</div>
</template>
<script>
// 按需导入 API 接口
import { getArticleListAPI } from '@/api/articleAPI.js'
// const result = getArticleListAPI(1, 5)
// console.log(result)
export default {
name: 'User',
data() {
return {
page: 1,
limit: 5
}
},
created() {
this.initArticleList()
},
methods: {
async initArticleList() {
const { data: res } = await getArticleListAPI(this.page, this.limit)
console.log(res)
}
}
}
</script>
<style lang="less" scoped>
.user-container {
.user-card {
background-color: #007bff;
color: white;
padding-top: 20px;
.van-cell {
background: #007bff;
color: white;
&::after {
display: none;
}
.avatar {
width: 60px;
height: 60px;
background-color: #fff;
border-radius: 50%;
margin-right: 10px;
}
.username {
font-size: 14px;
font-weight: bold;
}
}
}
.user-data {
display: flex;
justify-content: space-evenly;
align-items: center;
font-size: 14px;
padding: 30px 0;
.user-data-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 33.33%;
}
}
}
</style>
//Ariticleinfo.vue 数据请求的方法封装并导出
<template>
<div>
<van-cell>
<!-- 标题区域的插槽 -->
<template #title>
<div class="title-box">
<!-- 标题 -->
<span>{{ title }}</span>
<!-- 单张图片 -->
<img :src="cover.images[0]" alt="" class="thumb" v-if="cover.type === 1" />
</div>
<!-- 三张图片 -->
<div class="thumb-box" v-if="cover.type === 3">
<img :src="item" alt="" class="thumb" v-for="(item, i) in cover.images" :key="i" />
</div>
</template>
<!-- label 区域的插槽 -->
<template #label>
<div class="label-box">
<span>作者 {{ author }} {{ cmtCount }} 评论 发布日期 {{ time }}</span>
<!-- 关闭按钮 -->
<van-icon name="cross" />
</div>
</template>
</van-cell>
</div>
</template>
<script>
export default {
name: 'ArticleInfo',
// 自定义属性
props: {
// 文章的标题
title: {
type: String,
default: ''
},
// 作者名字
author: {
type: String,
default: ''
},
// 评论数
cmtCount: {
// 通过数组形式,为当前属性定义多个可能的类型,可以时数字也可以是字符串
type: [Number, String],
default: 0
},
// 发布日期
time: {
type: String,
default: ''
},
// 封面的信息对象
cover: {
type: Object,
// 通过 default 函数,返回 cover 属性的默认值
default: function() {
// 这个 return 的对象就是 cover 属性的默认值
return { type: 0 }
}
}
}
}
</script>
<style lang="less" scoped>
.label-box {
display: flex;
justify-content: space-between;
align-items: center;
}
.thumb {
// 矩形黄金比例:0.618
width: 113px;
height: 70px;
background-color: #f8f8f8;
object-fit: cover;
}
.title-box {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.thumb-box {
display: flex;
justify-content: space-between;
}
</style>
//request.js 请求地址
import axios from 'axios'
// 调用 axios.create() 函数,创建一个 axios 的实例对象,用 request 来接收
const request = axios.create({
// 指定请求的根路径
baseURL: 'https://www.escook.cn'
})
export default request
vue.config.js
// 这个文件是 vue-cli 创建出来的项目的配置文件
// 在 vue.config.js 这个配置文件中,可以对整个项目的打包、构建进行全局性的配置
// webpack 在进行打包的时候,底层用到了 node.js
// 因此,在 vue.config.js 配置文件中,可以导入并使用 node.js 中的核心模块
const path = require('path')
const themePath = path.join(__dirname, './src/theme.less')
module.exports = {
publicPath: './',
css: {
loaderOptions: {
less: {
modifyVars: {
// 直接覆盖变量
// 'nav-bar-background-color': 'orange'
// 或者可以通过 less 文件覆盖(文件路径为绝对路径)
// ../ ./ theme.less
// 从盘符开始的路径,叫做绝对路径 C:\\Users\liulongbin\\theme.less
hack: `true; @import "${themePath}";`
}
}
}
}
}module.exports = {
publicPath: './', //使打包后的文件可以在file运行不只是在http运行
css: {
loaderOptions: {
less: {
modifyVars: {
// 直接覆盖变量
// 'nav-bar-background-color': 'orange'
// 或者可以通过 less 文件覆盖(文件路径为绝对路径)
// ../ ./ theme.less
// 从盘符开始的路径,叫做绝对路径 C:\\Users\liulongbin\\theme.less
hack: `true; @import "${themePath}";`
}
}
}
}
}
// 在 theme.less 文件中,覆盖 Vant 官方的 less 变量值
@blue: #007bff;
// 覆盖 Navbar 的 less 样式
@nav-bar-background-color: @blue;
@nav-bar-title-text-color: #fff;