MVC、MVP、MVVM
mvc
-
Model - 模型,数据保存
-
View - 视图 用户界面
-
Controller - 控制器 业务逻辑
mvvm框架?
-
M - Model
-
V - View
-
VM - ViewModel
采用 双向绑定(data-binding):View的变动,自动反映在 ViewModel,ViewModel 会自动去更新 Model 数据,反之亦然(即当 Model 发生改变时也会自动反映到 ViewModel 上,触发 View 的自动更新渲染)
VUE
现行两大版本:vue2.x vue3.x
vue架构中包含了mvvm…
Vue2.x 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性(Object.defineProperty()
)
- Vue2.x 底层使用 Object.defineProperty() 来实现 数据劫持
- Vue3.x 底层使用 Proxy 来实现 数据代理。
- 用于构建用户界面的渐进式框架,核心库只关注视图层。
安装
-
使用
<script>
直接引入:- 使用
<script src="">
方式引入,会添加一个全局变量Vue
- 两种版本:
- 开发版本: 包含完整的警告和调试模式
- 生产版本: 压缩混淆,不包含调试模式信息 vue.min.js
- 使用
-
npm
$ npm install vue
-
vue-cli (常用)
使用
1.商品渲染
html
<!-- view视图界面 -->
<div id="app">
<h1>购物车</h1>
<h1>推荐商品:</h1>
<ul>
<!--for in 遍历数组 -->
<li v-for="curr in products">
{{ curr.id }}-{{ curr.title }}-{{curr.price}}-{{curr.num}}
<button>加入购物车</button></li>
</ul>
</div>
<script src="./libs/vue.js"></script>
<script>
// 定义数组,保存推荐商品(模拟数据)
const _products = new Array(8).fill(null).map((item, index, array) => {
return {
id: index + 1,
title: '商品标题:' + (index + 1),
price: (Math.random() * 100).toFixed(2),
num:0,
}
})
//vue 创建一个新的 Vue 实例
const vm = new Vue({
el: '#app', //view,element
data:{ //model
products:_products, //推荐商品
},
computed:{
属性一(){
return ...
},...
},
methods:{
方法一(){
},...
}
})
</script>
2.添加到购物车
<!-- view视图界面 -->
<div id="app">
<h1>推荐商品:</h1>
<ul>
<li v-for="curr in products">
{{ curr.id }}-{{ curr.title }}-{{curr.price}}-{{curr.num}}
<button v-on:click="addToCart(curr)">加入购物车</button></li>
</ul>
</div>
<script src="./libs/vue.js"></script>
<script>
//vue 创建一个新的 Vue 实例
const vm = new Vue({
el: '#app', //view,element
data: { //model
products: _products, //推荐商品
cart: [], //购物车数组
},
methods: { //方法
//加入购物车
addToCart: function (curr) {
// 如果当前商品已选购,则叠加数量
const product = this.cart.find(item => item.id == curr.id)
if (product) {
product.num++
} else {
this.cart.push({
...curr,
})
}
}
}
})
</script>
3.计算勾选商品总价
<!-- view视图界面 -->
<div id="app">
<h1>购物车</h1>
<ul>
<li v-for="curr in cart">
<input type="checkbox" v-model="curr.checked">
{{ curr.id }}-{{ curr.title }}-{{curr.price}}-{{curr.num}}
<button>删除商品</button>
</li>
</ul>
<div>
合计商品总价:{{ total }}
</div>
<script src="./libs/vue.js"></script>
<script>
//vue 创建一个新的 Vue 实例
const vm = new Vue({
el: '#app', //view,element
data: { //model
products: _products, //推荐商品
cart: [], //购物车数组
},
computed:{ //计算属性
total(){
return this.cart.reduce((resault,curr,index,arr)=>{
return curr.checked?resault + (curr.price * curr.num):resault
},0)
}
},
})
</script>
4.删除商品
<!-- view视图界面 -->
<div id="app">
<h1>购物车</h1>
<ul>
<li v-for="curr in cart">
<input type="checkbox" v-model="curr.checked">
{{ curr.id }}-{{ curr.title }}-{{curr.price}}-{{curr.num}}
<button v-on:click="removeCart(curr)">删除商品</button>
</li>
</ul>
<script src="./libs/vue.js"></script>
<script>
//vue 创建一个新的 Vue 实例
const vm = new Vue({
el: '#app', //view,element
data: { //model
},
computed:{ //计算属性
}
},
methods: { //方法
//从购物车移除
removeCart(curr){
this.cart = this.cart.filter(item=>item.id!==curr.id)
}
}
})
</script>
vue沿用了mvvm特性:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MPYLtcAU-1654654274388)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1652088957608.png)]
声明式渲染
Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统
<div id="app">
{{ message }}
</div>
<script src="./libs/vue.js"></script>
<script>
new Vue({
el: '#app', // View
data: { // Model
message: 'Hello Vue.js'
},
})
</script>
现在数据和 DOM 已经被建立了关联,所有东西都是响应式的,示例如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HoIHiUtR-1654654274389)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1652153464068.png)]
vue实例
const vm = new Vue({
el: '',
data: {},
methods: {},
})
注意:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mRMZgiHl-1654654274390)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1652173816092.png)]
- 在 data 中定义的数据会被挂载到 vm 这个 Vue 对象实例下(即可以直接通过
vm.
来调用 data 中的数据)。注意,在 data 中尽量不要定义以$
或_
开关的字段,因为可能和 Vue 对象自身的属性冲突(以$
或_
开头的数据,不会挂载到 Vue 实例下) - 虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用
vm
(ViewModel 的缩写) 这个变量名表示 Vue 实例。 - 当一个 Vue 实例被创建时,它将
data
对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。 - 但只有当实例被创建时就已经存在于
data
中的 property 才是响应式的。 - $el $data $options
- 在 methods 中定义的方法内部,this 通常指向的是当前创建出的 Vue 实例对象本身(不要使用箭头函数)
vue–methods
<div id="app">
{{message}}
<br>
{{nodata}}
<br>
{{ meth1() }}<br>
{{ meth2() }}<br>
{{ meth3() }}<br>
</div>
<script src="./libs/vue.js"></script>
<script>
const vm = new Vue({
//选项
el: '#app',
data: {
message: 'nihao'
},
methods: {
meth1:function(){
console.log("meth1",this);
return "method---1" //vue
},
meth2() {
console.log("meth2",this);
return "method---2" //vue
},
meth3:()=>{
console.log("meth3",this);
return "method---3" // window,通常不使用箭头函数来书写
},
},
created() { //data外,无响应式渲染
this.nodata = "data外数据--不响应式渲染"
}
})
</script>
模板语法
所有 Vue.js 的模板都是合法的 HTML,所以能被遵循规范的浏览器和 HTML 解析器解析。
在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。
插值语法
“Mustache”语法 (双大括号)
文本
{{ expression }}
<!-- 双大括号会将数据解释为普通文本,而非 HTML 代码 -->
- 注意,
{{ }}
中包含的是 JS 的表达式,会进行 html 文本转义,主要是为了避免XSS
攻击
<div v-text="expression"></div>
<!-- v-text == 双大括号 == innerText-->
- 可使用
v-text
来绑定渲染文本表达式的值
原始 HTML
<div v-html="expression"></div>
<!--v-html:不会解析html标签: => innerHtml-->
- 注意:请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。
属性绑定
<div v-text="message" v-bind:title="title"></div>
v-bind 可简写为 `:`
v-bind:属性名="属性值"
<div v-text="message" :title="title"></div>
<span v-html="message" title='title'>
v-html:会解析html标签: => innerHtml
</span><br>
<span v-text="message" v-bind:title="title">
v-text:不会解析html标签: =>innerText
</span><br>
<span v-once :title="title">
v-once指令:这个值不会响应变化 {{message}}
</span>
const vm = new Vue({
el: "#app",
data: {
message: "<input type='text'>输入框",
title:"提示符"
}
})
指令
指令,是在标签中添加的自定义属性,以 v-
作为前缀,有特殊的意义(在 Vue 中会对这些属性进行特殊处理)。
- 指令名称
v-
之后的部分,即为指令名称
- 参数
指令名称之后以冒号表示
- 修饰符
以半角句号 .
指明的特殊后缀,用于指出一个指令应该以特殊方式绑定
系统定义的指令
文本:
- v-text:
- v-html:
条件渲染:满足条件的节点即显示,否则隐藏
-
v-show:
-
v-if:
<div id="app"> <button @click="visible=!visible">显示/隐藏</button> <div v-if="visible">v-if-显示/隐藏</div> <div v-show="visible">show-显示/隐藏元素2</div> </div> <!--当visible:false时候,v-if从dom树中消失,v-show设置了display:none属性--> <script src="./libs/vue.js"></script> <script> const vm = new Vue({ el:"#app", data:{ visible:true, } }) </script>
-
v-else-if:
-
v-else:
<button v-if="visible">显示/隐藏</button> <button v-else-if="visible2">显示/隐藏2</button> <div v-else>show-显示/隐藏元素</div> <!--`v-else` 元素必须紧跟在带 `v-if` 或者 `v-else-if` 的元素的后面,否则它将不会被识别。 --> <script> const vm = new Vue({ el:"#app", data:{ visible:false, visible2:false, } }) </script>
v-if vs v-show
v-if
vsv-show
【面试】
v-if
是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。操作dom树的节点的消失和出现,条件满足创建节点,条件未false,销毁节点
v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。相比之下,
v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。一般来说,
v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show
较好;如果在运行时条件很少改变,则使用v-if
较好。
列表渲染:通常是对数组进行遍历
- v-for:
<div v-for="n in 10">{{n}}-重复渲染的内容</div>
<div v-for="(curr,index) in arr">当前{{curr}},索引{{index}}</div>
<div v-for="(val,key,index) in obj">属性值:{{val}},属性:{{key}},索引:{{index}}</div>
<div v-for="(val,key) of obj">属性值:{{val}},属性:{{key}}</div>
<div v-for="(curr,index) of arr">当前of:{{curr}},索引{{index}}</div>
<div v-for="n of 10">{{n}}-重复渲染的内容</div>
注意:在进行列表渲染时,尽量为渲染的每一项绑定 key
属性。建议尽可能在使用 v-for
时提供 key
attribute
v-if vs v-for
v-if 与 v-for 一起使用
当在同一个节点中同时使用 v-if 与 v-for 时:
vue2.x:v-for 的优先级高于 v-if
vue3.x:v-if 的优先级高于 v-for
不推荐 同时使用
v-if
和v-for
。
事件处理:
-
v-on:可简写为
@
-
可使用
v-on
去接收一个需要调用的方法名称(事件处理程序函数名称) -
在内联 JavaScript 语句中调用方法,当在事件处理程序调用时需要显式传递参数时,通常使用这种方式
<div id="app"> <button v-on:click="alertW">点击弹框</button> <button @click="alertM('hi')">点击弹框2</button> </div> <script src="./libs/vue.js"></script> <script> const vm = new Vue({ el:"#app", data:{ message:"你好哇" }, methods:{ alertM(message){ alert(message) }, alertW(){ alert('what') } } }) </script>
-
属性绑定:
- v-bind:可简写为
:
表单处理:
- v-model:双向绑定
你可以用 v-model
指令在表单 input
、textarea
及 select
元素上创建双向数据绑定。
它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model
本质上不过是语法糖。
语法糖解释:class => 并不是真正的类,只是原型链的简单写法,class为语法糖
v-model实现原理语法糖:
v-model
在内部为不同的输入元素使用不同的 property 并抛出不同的事件:v-bind:value v-on:input
- text 和 textarea 元素使用
value
property 和input
事件;- checkbox 和 radio 使用
checked
property 和change
事件;- select 字段将
value
作为 prop 并将change
作为事件。
插槽:
- v-slot:
其它:
-
v-pre:跳过这个元素和它的子元素的编译过程,可以用来显示原始 Mustache 标签
<span v-pre>{{这是不被解析的}}</span> //{{这是不被解析的}}
-
v-cloak:这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如
[v-cloak] { display: none }
一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。<style> [v-cloak]{ display: none; } </style> <div id="app" v-cloak> {{message}} </div> <script src="./libs/vue.js"></script> <script> setTimeout(() => { const vm = new Vue({ el: "#app", data: { message: "<input type='text'>" } }) }, 5000) </script>
未编译时候,渲染{{message}},避免客户不好体验,设置v-cloak属性不可见,编译完毕
-
v-once:一次
<span v-once>{{message}}</span> v-once指令:这个值不会响应变化 {{message}}
Todolist - 1.0
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
padding: 0;
margin: 0;
}
#app {
margin: 100px;
}
ul {
margin-top: 10px;
list-style: none;
}
.finished {
background-color: #ccc;
text-decoration: line-through;
width: fit-content;
}
</style>
</head>
<body>
<div id="app">
<!-- 标题 -->
<h3>待办事项</h3>
<!-- 输入 -->
<!-- 3. 键盘回车添加事件-->
<input type="text" placeholder="请输入新的待办事项" v-model.trim="inputValue" ref="input" @keydown.enter="addtodo">
<button v-on:click="addtodo">添加</button>
<!-- 列表事项 -->
<div v-if="todos.length === 0">
待办事项列表为空,请添加新待办事项
</div>
<ul v-else>
<!-- key:v-for要绑定一个key,将id作为key -->
<li v-for="curr in todos" :key="todo.id" :class="{finished: todo.status, abc: todo.status}"
:style="{marginTop: '2px', paddingBottom: '4px'}">
<!-- 4. 修改待办事项状态-->
<input type="checkbox" v-model="curr.status">
<span>{{curr.title}} -- {{curr.status?"已":"未"}} 完成</span>
<button v-on:click="removeTodo(curr.id,$event)">删除</button>
</li>
</ul>
<!-- 统计 -->
<!-- 5.全选状态 -->
<input type="checkbox" :checked="checkedAll" @click="chandleCheckAll">
<span>已完成{{completeCount}}/全部{{todos.length}}</span>
<button v-on:click="clearAll">清除所有</button>
</div>
<script src="./libs/vue.js"></script>
<script>
let index = 3;
const vm = new Vue({
el: "#app",
data: {
/* 1.动态渲染数组todos数据 */
todos: new Array(3).fill(null).map((_, index) => ({
id: index + 1,
title: "代办事项标题" + (index + 1),
status: Math.random() > 0.5
})),
inputValue: "", //输入框双向绑定
// todos:new Array(3).fill(null).map((curr,index)=>{
// return {
// id:index+1,
// title:"代办事项标题"+ (index+1),
// }
// })
},
computed: {
/*5.全选状态 */
checkedAll() { // 计算是否所有待办事项已完成
// return this.todos.every(item=>item.status==true)
return this.todos.every(item => item.status)
},
completeCount() { //已完成的数量
return this.todos.filter(item => item.status).length
}
},
methods: {
/* 2.点击添加按钮,获取输入框内数据,向数组todos添加一条数据 */
addtodo() {
/*2.1文本框自动获取焦点*/
// document.querySelector('.input').focus() //class="input"
this.$refs.input.focus() //ref="input"
/*2.2输入框空白判断*/
if (this.inputValue !== '') {
this.todos.push({
id: ++index,
title: this.inputValue,
})
/*清空输入框*/
this.inputValue = ''
}
},
/*6.处理全选--点击全选,数组中每一项都为选中*/
chandleCheckAll(event) {
let checked = event.target.checked
this.todos.forEach(item => item.status = checked)
},
/*7.删除当前事*/
removeTodo(id, event) {
console.log("id", id, event);
this.todos = this.todos.filter(item => item.id !== id)
},
/*8.清除所有待办=>3种方法*/
clearAll() {
this.todos = []
}
}
})
</script>
</body>
</html>
Ref
ref
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs
对象上。 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件
计算属性
可以使用计算属性来代替在表达式中进行的复杂运算,以便于能够更方便的维护与复用逻辑。
计算属性是在选项对象中使用 computed
字段来定义。
特点:计算属性是可被缓存的(计算属性是基于它的响应式依赖进行缓存的,只有当依赖项发生变化时,才会重新计算并缓存)
计算属性 VS 方法
-
计算属性有缓存,方法没有缓存
-
方法中可包含如网络请求类似的副作用操作,计算属性中没有
const vm = new Vue({ el: "#app", data: { message: "hello,sea" }, computed: { //计算属性控制台只打印一次... //1.计算属性的简写形式,相当于是计算属性的 getter 方法定义 reversedMessage() { console.log("计算属性..."); return this.message.split('').reverse().join('') }, //2.计算属性完整写法 reversedMsg: { get() { // getter,用于获取属性值,比如:console.log(this.reversedMsg) console.log('计算 reversedMsg...') return this.message.split('').reverse().join('') }, set(val) { // setter,用于设置(修改)属性值,this.reversedMsg = 'abc' console.log('为 reversedMsg 赋值:', val) }, } }, methods: { //方法请求n次,控制台打印n次... reversed() { console.log("方法..."); return this.message.split('').reverse().join('') } } })
侦听属性
一种更通用的方式来观察和响应 Vue 实例上的数据变动。
当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
侦听属性是在选项对象中使用 watch
字段来定义。
<div id="app">
{{ message }}
</div>
<script src="./libs/vue.js"></script>
<script>
const vm = new Vue({
el: '#app', // View
data: { // Model
message: 'Hello Vue.js',
obj: {name: 'infomation', address: { xx: {} }},
},
watch: {
// 1.简写,相当于 handler 处理器
//语法:监听对象(修改后值,修改前值){ }
message(newVal, oldVal) {
console.log('message 变化了...,修改后:', newVal, ',修改前:', oldVal)
// TODO.......
},
// 2.侦听器完整写法
obj: {
handler(newVal, oldVal) { // 处理器
console.log('info 变化了...', newVal, oldVal)
},
immediate: true, // 立即执行(初始渲染时,就执行一个侦听器)
deep: true, // 深度监听
}
},
})
计算属性 VS 侦听属性
- 缓存
- 计算属性是由一个或多个依赖项计算返回一个值,而侦听器是侦听一个数据的变化引起其它操作或一到多个数据变化
数组变更
变更方法
- push() / pop() push/unshift返回该数组的新长度
- unshift() / shift() pop/shify返回删除元素的值。
- splice() 返回值:数组–被删除的元素组成的一个数组
- sort() / reverse()
vue 对这七个方法进行了重写,当调用这些方法进行数组修改时,会触发响应式渲染
替换数组
当调用非变更方法时,不会触发响应式渲染,则可以使用替换数组的方式来进行数组更新。
filter()
、concat()
和slice()
。它们不会变更原始数组,而总是返回一个新数组。当使用非变更方法时,可以用新数组替换旧数组:
组件化应用构建
组件,即构建应用程序所使用到的部件。
组件系统,允许我们使用小型、独立和通常可复用的组件构建大型应用
在 Vue 里,一个组件本质上是一个拥有预定义选项的一个 Vue 实例。
定义组件选项
const options = {
template: '模板-视图的结构(布局)',
data() {
return {}
},
}
注意:
-
template 中定义的结构,必须使用单个根元素包裹
-
data 必须是函数结构,在函数体内部返回一个普通对象
-
为什么 data 要是函数?
组件是可被复用的,当创建同一个组件的不同实例时,如果 data 是普通对象,则不同的实例引用到的是同一个 data 对象,当任意一个实例中对 data 数据进行更新时,其它实例都会受影响,通常这与实际业务不符。
定义成函数,则创建各组件实例时,会调用 data 函数生成组件实例自身私有用到的对象数据,各组件实例间数据是独立的,互不受影响。
-
注册组件
全局注册
Vue.component(name, options)
全局注册的组件可在任意组件中使用到
局部注册
const options = {
components: {
//"name包括-短横线,引号必加" => 组件名字
name: {componentOptions}
}
}
局部注册的组件仅在其父组件内部可使用
注册实例
<div id="app">
{{message}} <br>
<hello-component></hello-component>
<hello></hello>
</div>
<script src="./libs/vue.js"></script>
<script>
/*定义组件的选项对象*/
const options = {
//view模板 字符串模板!需要单个根节点元素
template: `<div>
<h1>{{ title }}</h1>
<h2>副标题</h2>
<button v-on:click="add">按钮</button>
<div>{{reversed}}</div>
</div>
`,
// data语法:一个函数,return对象
data() { //数据
return {
title: "主标题"
}
},
methods:{
add(){
console.log('按钮事件');
this.title="改变成点击事件"
}
},
computed:{
reversed(){
return this.title.split('').reverse().join('')
}
}
}
//全局组件注册
Vue.component("helloComponent", options)
/*1.创建vue实例*/
const vm = new Vue({
el: "#app",
data: {
message: "hello,baobao"
},
components: {
//局部组件注册 hello标签 == template
hello: {
template: "<h3>局部注册的组件</h3>"
}
}
})
</script>
渲染组件
-
利用组件名称作为自定义标签名称使用,来渲染组件。
-
注意:在使用自定义组件名作为标签名称使用时,要完整书写独立的结束标签,标签名称应该使用
短横线命名
的规范。
组件通信
组件间进行数据的传递称为组件通信。
父子组件通信
-
父传子:利用属性(props)的方式传递数据。
子组件定义时,在选项中使用 props 定义组件可接收的属性。
父组件中使用到子组件(标签)时,在标签内部书写需要传递给子组件的数据(是 name=value 键值对格式的属性)。
<div id="app"> <father></father> </div> <script> /*1.全局父组件的name,sex,phone 传递 给子组件*/ Vue.component("father", { template: ` <div> <h2>我是父组件father</h2> <son name="哥哥" :phone="phone"></son> </div> `, //未传sex,为默认值 data(){ return { phone:"1008611" } } , components: { son: { template: ` <div> <h3>我是子组件son--{{name}}--{{sex}}--{{phone}}</h3> </div> `, // props:["name","sex","phone"] /// 声明组件可接收的属性名称 props:{ //声明组件可接收的属性名称及其校验规则 name:String, sex:{ type:String, //字符串类型 default:"女", //未传值默认项 }, phone: { type: String, required: true, //该项为必填项 } } } } }) /*! 初始化在后面位置*/ const vm = new Vue({ el: "#app", data: { message: "我是app" } }) </script>
-
子传父:利用事件方式传递数据。
父组件中使用到子组件标签时,利用
v-on
注册一个自定义的事件监听,通常引用在父组件中定义的 methods 方法去处理接收到的数据。在子组件中需要传递数据时,调用
this.$emit(eventName, data)
触发在父组件中绑定的自定义事件并传递数据即可($emit() 的第二个参数是需要传递给父组件的数据)
div id="app">
<father></father>
</div>
<script src="./libs/vue.js"></script>
<script>
/*1.父组件*/
Vue.component("father", {
template: `
<div>
<h2>我是父组件father</h2>
<son v-on:add="addFather"></son>
</div>
`,//addFather:父组件的方法 ;add:触发子传父时 this.$emit('add',"data")
methods:{
addFather(data){
console.log("这是子传父的数据",data);
}
},
components: {
son: {
template: `
<div>
<h3>我是子组件son</h3>
<button v-on:click="addSon">向父组件传递数据</button>
</div>
`,//addSon:子方法
/*子传父*/
methods:{
addSon(){
console.log("向父组件传递...");
//调用(触发)在父组件中绑定的事件,向父组件传递数据
this.$emit('add', {code: 200, data: {userInfo: {name: '张三', age: 18}}})}
}
}
}
})
/*! 初始化在后面位置*/
const vm = new Vue({
el: "#app",
data: {
message: "我是app"
}
})
</script>
跨组件层级组件通信
-
转换为父子关系
-
event-bus(事件总线):·面试
-
借助 Vue 实例中的
$on()
与$emit()
方法来实现:首先创建一个全局的 Vue 实例对象(bus
)//1.全局变量 const eventBus = new Vue() //2.原型链 new实例之前 Vue.prototype.$bus = new Vue()
-
在需要接收数据的组件中,利用
bus
来注册事件监听(绑定自定义事件)created() { //生命周期钩子函数 //1当前组件(APP)会接收数据,调用 eventBus.$on() 绑定事件 eventBus.$on("removeItem", this.removeTodo) // 2 this.$bus.$on("removeItem", this.removeTodo) }
-
在需要传递数据的组件中,利用
bus
来触发事件并传递数据。methods:{ // 3.1.2当前组件(APP)会传递数据id,调用 eventBus.$emit() 绑定事件 removeTodo(){ // 1. eventBus.$emit('removeItem',this.item.id) //2. this.$bus.$emit('removeItem',this.item.id) } }
-
-
vuex 插件
Todoliat - 2.0 组件
生命周期
问?
生命周期,指的是vue实例从开始创建,到最终销毁,所经历的整个过程。即指从创建,初始化数据,编译模板,挂载Dom到渲染,更新到渲染,销毁等一系列过程,主要分为8阶段…以及一些特殊场景的生命周期,说一下生命周期流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hIeRY5dK-1654654274391)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1653124318172.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IDLfbYep-1654654274391)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1653124247775.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VrR299MO-1654654274391)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1653124266958.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t2on8uRj-1654654274392)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1653124154081.png)]
这个过程中会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
图示
生命周期钩子 【面试】
create 阶段 - 创建
-
beforeCreate():在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用
-
created():在实例创建完成后被立即同步调用
beforeCreate() { console.log('before create:', this.message); }, created() { console.log("created", this.message); },
mount 阶段 - 挂载
-
beforeMount():在挂载开始之前被调用
-
mounted():实例被挂载后调用,这时
el
被新创建的vm.$el
替换了。beforeMount() { console.log('before mount:',this.message) }, mounted(){ console.log("mounted",this.message); },
update 阶段 - 更新
-
beforeUpdate():在数据发生改变后,DOM 被更新之前被调用
-
updated():在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用
beforeUpdated(){ console.log("before updated",this.message); }, updated(){ console.log("updated",this.message); },
destroy 阶段 - 销毁
通常会销毁:启动的定时器、未完成的网络请求、打开的网络socket连接等
-
beforeDestroy():实例销毁之前调用
-
destroyed():实例销毁后调用
boforeDestory(){ console.log("destoryed",this.message); }, destory(){ console.log("destoryed",this.message); }
插槽
作用:内容分发
在组件定义时,可使用 <slot>
内置组件来占位,定义插槽内容。
//todoHeader.vue文件
<template>
<div class="todo-header">
<slot>
<h2>待办事项列表</h2>
<h2>TODOLIST...</h2>
</slot>
</div>
</template>
//app.vue文件
<todo-header>
<h5>这是todolist</h5>
</todo-header>
//渲染的时候
<h5>这是todolist</h5>会代替原来全部的h2
在一个组件中,可以使用多个 <slot>
来定义插槽,要区分这些不同的插槽,就需要给 <slot>
添加 name
属性,这就是命名插槽(具名插槽)。
<slot name="title">
<h2>待办事项列表</h2>
</slot>
<slot name="subtilte">
<h2>TODOLIST</h2>
</slot>
渲染命名插槽是的语法:
-
2.6 之前:
<p slot="slot-name">
即在标签中使用slot
属性指定插槽名称<h5 slot="title">这是todolist</h5>
-
2.6 及之后:可使用
v-slot
指令:<template v-slot:subtitle> <h3>副标题</h3> </template> ex: <todo-header> <template v-slot:subtitle> <h5>这是todolist</h5> </template> </todo-header>
v-slot 可简写为
#
,即:<template #subtitle> <h3>副标题</h3> </template> ex: <todo-header> <template #title> <h5>这是todolist</h5> </template> </todo-header>
单文件组件 SFC
single-file components
文件后缀(扩展)名为 .vue
解决的问题:
- 全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复
- 字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的
\
- 不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
- 没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript,而不能使用预处理器,如 Pug (formerly Jade) 和 Babel
如何定义选项?
单文件组件语法:
<template>
模板语法
</template>
<script>
// 定义并导出组件的选项对象模块
</script>
<style lang="scss" scoped>
/* 样式 */
div { }
</style>
-
template
:视图结构 -
script
:交互逻辑 -
style
:样式- lang:使用 css 预处理器,如:scss、less
-
scoped:表示所书写的样式仅支持在当前组件中使用,其它组件不受影响
Vue loader
Vue Loader 是一个 webpack 的 loader,它允许你以一种名为单文件组件 (SFCs)的格式撰写 Vue 组件
VUE CLI
vue工具:vue Devtools || vue cli
基于 Vue.js 进行快速开发的完整系统。
- 脚手架:快速搭建项目结构
安装
$ npm install -g @vue/cli
# OR
$ yarn global add @vue/cli
安装完毕后,可以在 cmd 命令行中输入:
$ vue --version
如果能够查看到版本信息,则说明安装成功,否则安装失败或环境变量配置有问题。
创建项目
GUI - 图形化用户界面
$ vue ui
命令行
- 执行创建命令:
$ vue create project-name
显示创建项目的向导
- 选择手动选择项目新特性项:
? Please pick a preset:
Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
> Manually select features #手动配置项目
- 选择新特性:
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection, and
<enter> to proceed)
(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
(*) Router
(*) Vuex
>(*) CSS Pre-processors
(*) Linter / Formatter
( ) Unit Testing
( ) E2E Testing
- 选择 vue 版本:
? Choose a version of Vue.js that you want to start the project with
3.x
> 2.x
- 选择 CSS 预处理器:
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default):
Sass/SCSS (with dart-sass)
> Less
Stylus
- 选择 linter 规范:
? Pick a linter / formatter config:
ESLint with error prevention only
ESLint + Airbnb config
> ESLint + Standard config
ESLint + Prettier
- 选择保存时验证并格式化:
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to pr
oceed)
>(*) Lint on save
( ) Lint and fix on commit
- 选择配置文件存放位置:
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
> In dedicated config files
In package.json
- 是否将上述选择特性保存为预设项:
? Save this as a preset for future projects? (y/N)
如果是 yes,则还需要输入预设项名称
- 安装项目依赖
- 项目创建成功后,进入项目目录,运行任务:
$ cd project-name
$ npm run serve # 或 yarn serve
项目说明
-
public 目录中放置的是应用的 html 文件(通常只有一个 index.html)
-
src 目录中放置我们自己项目中所书写的源代码
-
src 下 main.js 是应用的入口 JS 文件,名字勿改
-
.eslintrc.js
是 ESLint 的配置文件 -
babel.config.js
是 Babel 的配置文件 -
package.json
是项目配置文件 -
vue.config.js
是 Vue CLI 的配置文件(在 VueCLI4.x 中这个文件需要自己手动创建)// 老项目维护: module.exports = { ... }
npm scripts
{
"scripts": {
"start": "npm run serve",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
}
- serve: 开发任务,会自动启动开发服务器(webpack-dev-server)
- build:生产任务,构建生产环境下的资源
- lint:验证并格式化代码
安装 VSCode 插件
ESLint :代码规范报错
Vetur :让代码具有高亮
代码规范配置
-
对于自己的代码习惯,可以添加在
eslintrc.js
文件中的rules
,根据提示跳转到对于配置页面,找到options
,将代码复制进文件中'comma-dangle': ['erro', 'always'],
’ erro ’ == 3 红色报错
’ warn ’ == 2 黄色警告
’ off ’ == 1 关闭提示
-
设置后,原代码不符用户习惯,出现很多报错,cmd输入命令
npm run lint //根据规则自动格式化
注意:vscode 只打开项目文件,多余文件出现Eslint可能不会识别
Todolist - 3.0 单文件组件
标准规则注意点:单文件组件要用多个单词命名
Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。---- 工具,用于管理应用中组件间共享的状态(数据)
vuex4 – vue 3
vuex3 – vue 2
采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
概念
- store:仓库,用于集中式管理
- state:状态,应用中组件要共享的数据
- mutation:同步更新状态数据的函数,在 vuex 中仅能提交 mutation 来更新状态数据
- action:和 mutation 类似,但可以包含异步操作,不能直接更新状态,而是要提交 mutation 更新
- getter:是 store 中的计算属性
- module:模块,每个模块可以有自己的 state、mutation、action、getter,甚至自己的 module
使用
安装
$ npm i vuex@3.6.2
# 或
$ yarn add vuex@3.6.2
创建 src/store/index.js
文件
import Vue from 'vue'
import Vuex from 'vuex'
// 使用核心插件 Vuex
Vue.use(Vuex)
// 创建 Store
const store = new Vuex.Store({
state: { // 组件间共享的数据
todos: [],
},
getters: { // store的计算属性
/* 7.是否所有都完成? */
allChecked (state) {
return state.todos.every(item => item.completed)
},
mutations: { // 唯一进行同步更新状态数据的方法
/**
* 添加新待办事项
*/
addTodoItem(state, payload) {
state.todos.push({
id: Math.random(),
title: payload.title,
completed: false,
})
},
},
})
export default store
向根实例中 main.js
注入 store
import Vue from 'vue'
// 引入 store
import store from './store' // import store from './store/index.js'
new Vue({
store, // 注入 store,这样,在所有后代组件中,都可以使用 this.$store 获取 vuex 的 Store 使用
// render: h => h(App),
}).$mount('#app')
使用store
中的mutations getters
//使用store中的state
this.$store.state.todos
//辅助函数
import { mapGetters, mapMutations, mapState, mapActions } from 'vuex'
methods: {
...mapMutations(['clear', 'checkAll'])
// clear () {
// this.$store.commit('clear')
// },
// checkAll (event) {
// this.$store.commit('checkAll', { checked: event.target.checked })
// }
},
computed: { // 获取仓库的getters 和 公共数据
...mapGetters(['allChecked', 'checkCount', 'todoCount'])
todos () {
return this.$store.state.todos
}
// allChecked () {
// return this.$store.getters.allChecked
// },
// checkCount () {
// return this.$store.getters.checkCount
// },
// todoCount () {
// return this.$store.getters.todoCount
// }
},
维护vuecli老任务注意?
- package.json文件
- scripts 任务打开指令是 dev || start || serve ? npm run …
- vue 版本 2.6 || 2.5 代码差异
- eslintrc.js 文件
- 配置自己的代码规范
Todolist - 3.1 单文件组件样式
样式css美化
字体图标库
如何使用Bulma?
-
安装
npm install bulma
-
src/main.js
/* 引入模块*/ import 'bulma/css/bulma.min.css'
-
官网copy喜欢的结构
components
-
让图标显示
<!-- 引入 font-awesome -->
<script src="https://kit.fontawesome.com/304fcfb799.js" crossorigin="anonymous"></script>
Vue Router
SPA
单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新
该页面的Web应用程序。
前端路由
利用 hash 或 history 改变不会向后端发起新的 html 请求特点
模式
-
hash:利用 URL 中
#hash
hash 值改变后,不会发送新的网络请求的特点,来实现的路由。使用如#/home
、#/login
类似的方式来表示所请求的前端 URL 资源。window.onhashchange = function () { console.log('hash changes', location.hash)}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-14OAV0SJ-1654654274393)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1652690682250.png)]
-
history:利用 h5 中 history 新增的 API
pushState()
(往历史记录添加)、replaceState()
( 替换当前历史记录 ) 来实现。其路由的格式与服务端路由 URL 格式一致(比 hash 模式来说,没有多余的如#
之类的符号),所以,这种路由模式要用好,还需要服务端配置(运维工作)(刷新当前页面会认为向服务端请求资源,会出现404所以需要服务端配置)。
Vue Router
是 Vue.js 官方的路由管理器,是一个核心插件,与 Vue.js 高度集成。
Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举
安装
$ npm i vue-router@3.5.3
# 或
$ yarn add vue-router@3.5.3
定义 VueRouter 对象
- 创建组件 Home | About
src/views/Home.vue | src/views/About.vue
- 创建index.js
src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home'
import Login from '@/views/Login'
// 使用路由插件
Vue.use(VueRouter)
// 创建 VueRouter 对象
const router = new VueRouter({
mode: 'hash', // 路由模式,可取 hash、history,默认为 hash
routes: [ // 静态路由的配置信息
{
path: '/home', // 当 URL 中访问地址为 /home 时
component: Home, // 拿 Home 组件渲染
},
{
path: '/login',
component: Login,
},
],
})
// 导出路由对象
export default router
在 Vue 组件中使用 VueRouter 时,可使用 VueRouter 提供的两个内置组件:
- router-link:链接,相当于
<a>
- router-view:视图,用于渲染访问路径所对应的组件
注入 router 到 vue 根实例
- main.js
import App from './routerApp.vue'
// 引入 router
import router from './router'
new Vue({
router,
// render: h => h(App),
}).$mount('#app')
当在 Vue 根实例中注入了 router 后,在 routerApp.vue 组件中会自动注入如下两个属性:
- $router:代表 VueRouter 对象实例
- $route:代表的是当前激活的路由对象
3.src/routerApp.vue
<template>
<div class="router-app">
<!-- 页面连接 -->
<router-link to="/home">home主页</router-link> |
<router-link to="/about">about关于我们</router-link>
<!-- 对应组件的视图 -->
<router-view></router-view>
</div>
</template>
npm run serve
重定向
/* 重定向:在访问根目录的时候直接访问当前地址 */
routes: [ // 静态路由的配置信息
{
path: '/',
redirect: '/home' // 重定向
},
{
path: '/home', // 当 URL 中访问地址为 /home 时
component: Home // 拿 Home 组件渲染
},
编程式导航
导航到不同的位置 注意:在 Vue 实例中,你可以通过 $router
访问路由实例。因此你可以调用 this.$router.push
导航方法:
handleClick () {
//push 当用户点击浏览器后退按钮时,会回到之前的 URL。
this.$router.push('./todo')
//replace 当用户点击浏览器后退按钮时,不会回到之前的 URL是取代。
this.$router.replace('./todo')
//path 属性
router.push({ path: '/home', replace: true })
// =====
router.replace({ path: '/home' })
//除了path之外,也可以通过name
this.$router.push({
name: 'todo'
})
}
back() {
this.$router.back()
this.$router.forward()
this.$router.go()
},
- 当你点击
时,内部会调用这个方法,所以点击
相当于调用router.push(...)
- 声明式
- 编程式 this.$router.push(‘./todo’)
路由传参:
/* 字符串 =>传参 */
//1.
this.$router.push('./todo?id=12345&title=你好')
location.href=> http://localhost:8080/#/todo?id=12345&title=%E4%BD%A0%E5%A5%BD
//2.
this.$router.push({
name: 'todo',
query: {
id: 345,
title: 'lanlan',
price: 212.123,
amount: 9
}
})
location.href=> http://localhost:8080/#/todo?id=345&title=lanlan&price=212.123&amount=9
=> app2.vue
created () {
//route 激活路由
console.log(this.$route)
}
log=> obj.query
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lEX7oFK0-1654654274394)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1652700930073.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nS8QPBLJ-1654654274394)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1652700830455.png)]
/* 动态路径传参 */
{
path: '/todo/:id', // /:id动态路径传参
name: 'todo',
component: todoList // 渲染App2 组件
}
routerApp.vue
this.$router.push({
name: 'todo',
params: {
id: 2000
}
})
//注意:path会忽略 params
//一般只传 1-2 个参数
location.href =>http://localhost:8080/#/todo/2000
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tZSbLNpF-1654654274395)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1652702078267.png)]
命名视图
<router-view></router-view>
<router-view name="home"></router-view>
<router-view name="about"></router-view>
- 如果 router-view 没有设置名字,那么默认为 default。
嵌套路由
//主页 /category
获取方法1:点击获取id,发起请求
获取方法2:路由嵌套
<div class="right">
<router-view></router-view>
</div>
//子路由 /category/sub
import SubCategory from '@/views/category/sub-category'
{
path: '/category',
name: 'category',
// component: Category,
components: {
footer: AppTabbar,
default: Category,
header: AppNavbar,
},
children: [ // 子路由,嵌套路由
{ /* 'sub/:id' => 完整路径:/category/sub,:id 是定义的动态路径参数,用于接收传递的分类id
* /sub,/ 代表根路径 => /sub
*/
path: 'sub/:id',
name: 'sub',
component: SubCategory,
},
],
},
UI组件库
通用的已封装好的基础组件,可在各种类型项目中使用。
印记中文–vue( mobile ) vue( pc )
移动端
- vant
- mint-ui
- cube-ui
- Mand mobile (金融场景)
PC端
即时通信
=>难 云信 音频视频,
前端后端配合
课堂案例taotao
电商 – 移动端
实现功能页面
- 首页
- 分类
- 购物车
- 我的
- 商品详情
- 登录
- …
创建并构建项目
-
利用 vue cli 创建项目:选择 vue、babel、vue-router、vuex、less、linter 待功能
-
配置 eslint 规则(特别是团队协作时,eslint 规范很重要)
.eslintrc/ /* 配置规则总有逗号 */ 'comma-dangle': ['error', 'always-multiline'], 'vue/multi-word-component-names': 0,
-
配置应用开发服务器,如:端口、主机、自动打开浏览器功能等…
vue.config.js/ 老版本如下内容: module.exports = { devServer: { port: 3000, host: 'localhost', // 0.0.0.0 别人用你的ip就能看到界面 open: true, }, }
-
创建页面级 .vue 文件
views/home/index.vue views/category/index.vue ...
- 每个页面要实现的功能很多,所以需要响应页面文件夹进行管理
-
定义路由
router/routes.js =>单独写所有页面的路由 /* 由于每个组件页面下的文件命名都为index.vue所以书写路径可省略 */ import Home from '@/views/home' import Detail from '@/views/detail' import Login from '@/views/login' import Mine from '@/views/mine' import Cart from '@/views/cart' import Category from '@/views/category' const routes = [ { /* 重定向 */ path: '/', redirect: 'home', }, { path: '/home', name: 'home', component: Home, }, { path: '/detail', name: 'detail', component: Detail, }, { path: '/login', name: 'login', component: Login, }, { path: '/mine', name: 'mine', component: Mine, }, { path: '/cart', name: 'cart', component: Cart, }, { path: '/category', name: 'category', component: Category, }, { /* 通配符路由 */ path: '*', component: Notfount, }, ] export default routes
- 当使用通配符路 由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。路由
{ path: '*' }
通常用于客户端 404 错误
- 当使用通配符路 由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。路由
-
引入项目使用的 UI 组件库
移动端–vant2–vue2
-
安装
npm i vant@2.12.47 #版本号去npm.js找对应版本下载量最多的
-
引入组件:
2.1 推荐使用自动按需引入组件
- 配置操作
# 安装插件 npm i babel-plugin-import -D # babel.config.js 中配置文件 || 在.babelrc 中添加配置(老版本) plugins: [ ['import', { libraryName: 'vant', libraryDirectory: 'es', style: true, }, 'vant'], ], # 接着你可以在代码中直接引入 Vant 组件 插件会自动将代码转化为方式二中的按需引入形式 import { Button } from 'vant';
- 组件注册](https://youzan.github.io/vant/v2/#/zh-CN/advanced-usage)
ex:全局注册 import Vue from 'vue' import { Button } from 'vant' // 方式一. 通过 Vue.use 注册 // 注册完成后,在模板中通过 <van-button> 或 <VanButton> 标签来使用按钮组件 Vue.use(Button) <template> <div class="Home"> 主页 <van-button>按钮</van-button> </div> </template>
- 单独文件模块封装
src/utils/vant-import.js import Vue from 'vue' import { Button, Cell, CellGroup, } from 'vant' // 方式一. 通过 Vue.use 注册 // 注册完成后,在模板中通过 <van-button> 或 <VanButton> 标签来使用按钮组件 Vue.use(Button) Vue.use(Cell) Vue.use(CellGroup) ... src文件入口/main.js /* 引入单独的vantjs文件 */ import './utils/vant-import'
- 注意每引入一次,use一次
- 可能cv的代码包含方法和属性,注意粘贴整合
- 可能cv的代码包含其他的van-开头标签,也需要其他组件引入并use才会展示效果
2.2 导入所有组件=>可能vant组件你全部都用到
src入口文件/main.js import Vue from 'vue'; import Vant from 'vant'; import 'vant/lib/index.css'; Vue.use(Vant);
- Tips: 配置按需引入后,将不允许直接导入所有组件。
- 与2.1互斥
-
-
axios 二次封装
-
接口文档使用
import 异步任务,实现懒加载技术
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue'),
定制主题
每个人页面主题不一致,颜色不一致,难看,需要统一
-
按需引入样式
babel.config.js配置 plugins: [ [ 'import', { libraryName: 'vant', libraryDirectory: 'es', // 指定样式路径 style: (name) => `${name}/style/less`, }, 'vant', ], ],
-
修改样式变量
// vue.config.js module.exports = { css: { loaderOptions: { less: { // 若 less-loader 版本小于 6.0,请移除 lessOptions 这一级,直接配置选项。 lessOptions: { // modifyvars 修改变量:里面自定义 modifyVars: { // 直接覆盖变量 'text-color': '#111', 'border-color': '#eee', // 或者可以通过 less 文件覆盖(文件路径为绝对路径) hack: `true; @import "your-less-file-path.less";`, }, }, }, }, }, };
vue-cli 搭建的项目,可以在
vue.config.js
中进行配置。
国际化 I18N
i18n(其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数)是“国际化”的简称
网络请求
1.数据请求
ajax( 步骤分散 ) => JQuery( 封装ajax $ajax) =>ES6 promise ( 避免回调地狱 )=> axios (基于 ajax 和 promise 进行封装的库,核心原理是 Ajax,封装使用 promise管理,解决异步并行问题 ) => fetch ( 内置的类,进行数据请求的,天生就是基于promise 进行管理的 )
Fetch API
优点:
-
减少对外部依赖项的加载,不用安装三方包
-
写法简单
-
兼容使用polyFill
fetch()
必须接受一个参数——资源的路径。无论请求成功与否,它都返回一个 Promise 对象,resolve 对应请求的Response
对象。你也可以传一个可选的第二个参数init
(参见Request
) -
IE
兼容问题 -
默认请求方式
get
,可以fetch(一参,二参) -
测试接口:get请求
home.vue组件实现:
created () {
/* 网络请求方法1:fetch API */
const result = fetch('https://jsonplaceholder.typicode.com/users')
console.log('result', result)
result
.then(res => res.json())
.then(data => {
console.log(data)
})
// 调用两次then
},
axios二次封装
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
特性
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
安装
使用 npm:
$ npm install axios qs
qs: 对象转化为ur url转化成对像
2.配置scripts字段
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NdQgVo9n-1654654274395)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1654246759625.png)]
- 定义 baseURL – 适应不同环境请求不同接口
src/utils/request.js
import axios from 'axios'
import { Toast } from 'vant'
/* 定义 baseURL -- 适应不同环境请求不同接口 */
// const baseURL = process.env.NODE_ENV === 'development' ? '开发测试接口' : '生产服务器接口'
// const baseURL = process.env.NODE_ENV === 'development' ? 'http://102.666.6.6' : 'http://www.test-site.com'
const baseURL = ''
/* 创建 axios 实例 */
const service = axios.create({
baseURL,
timeout: 10000,
})
// 拦截请求
service.interceptors.request.use(config => {
// 加载提示
Toast.loading({
message: '加载中...',
forbidClick: true,
duration: 0, // 展示时间,0为不消失
})
// 添加请求头中的token 认证数据
// ...
return config
})
// 拦截响应
service.interceptors.response.use(resData => {
// 关闭提示
Toast.clear()
// 响应数据处理:根据前后端接口规范来处理数据结果
if (resData.status === 200) {
return resData.data
}
return Promise.reject(new Error('接口请求异常...'))
})
export default service
调用方法:
- 每个组件调用–繁琐
home.vue组件实现:
import request from '@/utils/request'
created () {
/* 网络请求方法2:axios */
request({
url: 'https://jsonplaceholder.typicode.com/users',
method: 'GET',
}).then(data => console.log(data))
// request.get()
// request.post()
},
- 入口文件调用 【推荐】
/* 引入二次封装的axios文件 */
import request from './utils/request'
Vue.prototype.$http = request
每个组件中使用方便
created () {
/* 网络请求方法2:axios */
this.$http({
url: 'https://jsonplaceholder.typicode.com/users',
method: 'GET',
}).then(data => console.log('$http', data))
},
- 注意公司中文件的
this.$http
名字 是axios
封装的 还是 插件vue-resourse
- vue-resourse = > The plugin for Vue.js provides services for making web requests and handle responses using a XMLHttpRequest or JSONP.
接口文档
去公司会遇到各种格式…
txt | doc | swagger | 公司工具…
swagger文档理解
案例接口地址:http://quanzhan.site:3000/swagger/
使用
统一处理网络请求
src/api/constants.js
/* 很多页面需要调用当前接口,采取变量保存;
改版接口时,只需要改一次变量值,方便维护
*/
const API = {
/* api/tabs 获取所有分类信息 */
TABS_API: 'api/tabs',
}
export default API
封装Tabbar组件( 底部导航 )
面试刚需:你有自己写过组件吗?
意思是说自己封装使用组件的这个过程,每个公司业务不一样,需要你进行整理封装复用
vant – tabbar标签栏
不使用vant组件,自己如何封装定义vue插件tabbar呢?
插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
- 添加全局方法或者 property。如:vue-custom-element
- 添加全局资源:指令/过滤器/过渡等。如 vue-touch
- 通过全局混入来添加一些组件选项。如 vue-router
- 添加 Vue 实例方法,通过把它们添加到
Vue.prototype
上实现。- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router
src/components/tabbar/tab-bar.vue
定义插件
<template> // 底部导航的布局
<div class="tao-tab-bar">
<div
@click="jump(item.pagePath)"
class="tao-tab-bar-item" v-for="(item, index) in list" :key="index">
<span>{{item.icon}}</span>
<span>{{item.text}}</span>
</div>
</div>
</template>
<script>
export default {
name: 'Tabbar',
/* home/index.vue父传子list */
props: {
list: {
type: Array,
required: true,
/* 自定义匹配规则 */
validator: function (value) {
// list底部导航要求2-5个
return value.length >= 2 && value.length <= 5
},
},
},
/* 编程式导航 */
methods: {
/* 点击对应tabbar 跳转到对应页面 */
jump (path) {
this.$router.push(path)
},
},
}
</script>
<style lang="less" scoped>
</style>
- 当前页面home,点击home,重复跳转渲染当前页面的时候,控制台会报错
- 不用管,这是新版会提示
- 也可以在
this.$router.push(path).catch(()=>{})
静默处理(对于异常什么也不做)
src/components/tabbar/index.js
开发插件
import Tabbar from './tab-bar'
/* 开发插件 */
// 自定义 Vue 插件 暴露install方法 官网
const plugin = {
install (Vue) {
// 全局注册自定义的 tabbar 组件
Vue.component('tao-tabbar', Tabbar)
},
}
export default plugin
home/index.vue
渲染插件
<tao-tabbar :list="list"></tao-tabbar>
-
home/index.vue
定义tabbar 动态数组/* tabbar数组 */ const list = [ { pagePath: '/home', // 点击跳转路径 text: '首页', icon: 'index', // icon图标 }, { pagePath: '/category', text: '分类', icon: 'cat', }, { pagePath: '/cart', text: '购物车', icon: 'cart', }, { pagePath: '/mine', text: '我的', icon: 'mine', }, ]
-
多个页面跳转,当前
tabbar
要激活将之前定义的组件外部添加标签
<div class="tao-tab-bar"> // router-link自动转化成<a> <router-link v-for="(item, index) in list" :key="index" :to="item.pagePath" v-slot="{ navigate, isActive, isExactActive }" custom // 控制台黄色警告 v4/v3版本差别。对渲染无影响 > <div @click="navigate" // 激活样式使用2: :style="{color:isAcitve || isExactActive?selectedColor:color}" // 激活样式使用1:class可传数组对象字符串... :class="['tao-tab-bar-item',isActive && 'router-link-active',isExactActive && 'router-link-exact-active']" > <span>{{item.icon}}</span> <span>{{item.text}}</span> </div> </router-link> </div>
-
router-link
配置tag
属性生成别的标签 -
router-link
标签带来的class类router-link-exact-active
|router-link-active
-
v-slot
作用域插槽 是个对象,无slot
生成a
标签v-slot="{ href, route, navigate, isActive, isExactActive }"
href
:解析后的 URL。将会作为一个a
元素的href
attribute。route
:解析后的规范化的地址。navigate
:触发导航的函数。会在必要时自动阻止事件,和router-link
同理。isActive
:如果需要应用激活的 class 则为true
。允许应用一个任意的 class。isExactActive
:如果需要应用精确激活的 class 则为true
。允许应用一个任意的 class。
-
-
激活两种方法
<style lang="less" scoped> // 激活样式--1.直接固定激活样式 router-link标签带来的class类名 // .router-link-exact-active, .router-link-active{ // color:red, // } <style> props: { /* 激活--2.自定义激活样式,父传子,没有用到class类名 */ color: { type: String, default: '#666', //默认字体颜色 }, selectedColor: { type: String, default: 'pink', //未传参,激活默认颜色为粉色 }, },
-
引入
iconfont库
在线图标选好加入到项目
taotao
使用在线链接复制全部–字体图标任何页面都会用到
复制到
App.vue
单总页面style
中 补全https协议右侧 使用帮助 按步骤复制操作
<i class="iconfont">{{item.icon}}</i> // 不会生效 <i class="iconfont" v-html="item.icon"></i> // html转译 成功生效
-
将
tabbar-list
数组提取放到/router/routes.js
文件中// meta保存开发阶段用到的数据 router/routes.js { path: '/home', name: 'home', component: Home, meta: { title: '首页', icon: '𐃍', inTabbar: true, // 是否在tabbar中 }, }... home/index.vue /* tabbar数组 */ import routes from '@/router/routes.js' created () { /* routes中要过滤出tabbar中需要的四个图标条件:有meta,有intabbar */ // const list = routes.filter(router => router.meta && router.meta.inTabbar) this.list = routes.filter(router => router.meta?.inTabbar) .map(route => ({ text: route.meta.title, icon: route.meta.icon, pagePath: route.path, })) }
-
页面划分区域
header-main-footer
- 有时候想同时 (同级) 展示多个视图,
<router-view name='header'/> <router-view/> // 没有命名name:default <router-view name="footer"/>
-
route/routes.js
路由页面进行多个命名视图渲染{ path: '/home', name: 'home', // component: Home, components: { // 指定命名视图中渲染的组件 header: '', footer: '', default: '', },
-
不同页面中都需要tabbar,避免多次引入,再次封装一下tabbar
components/app-tab-bar/index.js <template> <div class="app-tab-bar"> <tao-tabbar :list="list" selectedColor="red"></tao-tabbar> </div> </template> <script> /* tabbar数组 */ /* 各个页面复用的tabbar */ import routes from '@/router/routes.js' export default { name: 'AppTabbar', computed: { /* routes中要过滤出tabbar中需要的四个图标 条件:有meta,有intabbar */ // const list = routes.filter(router => router.meta && router.meta.inTabbar) // ?. 可选链运算符 list () { return routes.filter(router => router.meta?.inTabbar) .map(route => ({ text: route.meta.title, icon: route.meta.icon, pagePath: route.path, })) }, }, }
-
路由视图渲染
routes.js { path: '/home', name: 'home', components: { // 指定命名视图中渲染的组件 footer: AppTabbar, default: Home, }...
-
app.vue
<!-- 头部视图 --> <header></header> <!-- 主体视图 --> <main> <router-view /> </main> <!-- 底部视图 --> <footer> <router-view name="footer"/> </footer> </div>
路径跳转自动渲染视图页面,底部导航成功
Tab标签页
-
引入van组件Tab
utils/vant-import.js import Vue from 'vue'; import { Tab, Tabs } from 'vant'; Vue.use(Tab); Vue.use(Tabs); views/home/index.vue渲染使用 <!-- 顶部导航tabs --> <van-tabs> <van-tab v-for="(category, id) in categories" :title="category.name" :key="id"> </van-tab>
-
调接口,引入分类数据
api/category.js /* 引入axios封装 */ import request from '@/utils/request' import API from './constants' /* 获取所有分类信息,返回的是promise对象 */ // 暴露函数 export default只可以暴露一个,export暴露很多 export const getCategories = () => { return request({ method: 'GET', url: API.TABS_API, // params: {}, // 传递的是路径中的参数 }) }
-
优化
request返回数据
/* * 响应数据处理:根据前后端接口规范来处理数据结构。 * resData.data 中保存的是后端 响应主体的内容。 * 后端与前端有交互规范,返回的是 JSON 格式的对象, * 只接收相应成功的数据:解构赋值 * 在对象中当 code 为 200 时,可以从 data 中获取实际 * 使用的数据,如果 code 不为 200,则可以从 message 中 * 获取操作错误相关信息。 */ if (resData.status === 200) { // return resData.data //后端 响应主体的内容 const { code, data } = resData.data if (code === 200) { return data } }
-
使用接口数据
home/index.vue import { getCategories } from '@/api/category' // 引入对象形式 data () { return { categories: [], } }, created () { /* 引入模块的方法 getCategories */ getCategories().then(data => { console.log('getCategories', data) this.categories = data.list }) }
知识点
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
// nextTick(callback) 作用:将回调函数的执行
// 延迟到下一次 DOM 更新结束后执行
this.$nextTick(() => {
// eslint-disable-next-line
new Swiper('.swiper-container', {
autoplay: true,
loop: true, // 循环模式选项
})
})
过滤器可以用在两个地方:双花括号插值和
v-bind
表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
防止冒泡事件
@click.stop()
购物车
addToCart(prod) {
// console.log('加入购物车:', prod)
// 调用到 store 中的 action 方法来实现加入购物车操作。
// 在组件中不能直接调用到 action 方法,需要使用
// store.dispatch(actionType, payload) 来触发 action 方法的调用。
// 这与 store.commit(mutationType, payload) 来提交 mutation 类似。
// 如果模块启用了命名空间,则在触发action方法时,需要将命名空间名称
// 作为前缀添加到 action 方法名称路径前,mutation类似。
this.$store.dispatch('shoppingCartModule/addToCart', prod)
// 提示
this.$toast({
message: '加入成功',
type: 'success',
})
},
现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
注:提示请用组件或手写框,避免使用alert等,会阻塞,生产环境下避免使用
- 修改bug:
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation .
对于浅拷贝actions改数据,和v-model同步更新数据,避免使用,因为在维护修改代码时,会造成困难,vuex就是唯一mutations管理数据更新的
- 添加商品成功提示
// this.$toast.success('加入成功')
this.$toast({
message: '加入成功',
type: 'success',
})
// 引入 Toast 组件后,会自动在 Vue 的 prototype 上挂载 $toast 方法,便于在组件内调用。
const store = new Vuex.Store({
// ...
strict: process.env.NODE_ENV !== 'production'
})
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
store/modules/shoppingCartModule
const shoppingCartModule = {
namespaced: true, // 命名空间(名字空间)
state: {
cart: [],
},
store/index.js
modules: {
shoppingCartModule, // shoppingCartModule 就是这个模块的命名空间名称
},
使用方法1:
// 如果模块启用了命名空间,则在触发action方法时,需要将命名空间名称
// 作为前缀添加到 action 方法名称路径前,mutation类似。
this.$store.dispatch('shoppingCartModule/addToCart', prod)
使用方法2:
// [vuex] unknown action type: removeFromCart
...mapActions('shoppingCartModule', ['removeFromCart']),
- 购物车界面,删除商品
v-for || v-if 避免共用问题
加一个父级标签,如果不想再
dom
渲染,可以用template
标签作为父级
<template v-if="cart.length">
<van-swipe-cell v-for="prod in cart" :key="prod.id">
<van-card
:num="prod.num"
:price="prod.price"
desc="描述信息"
:title="prod.title"
class="goods-card"
:thumb="prod.image"
/>
</template>
<van-empty v-else image="error" description="请先添加商品" />
- 报错
do not mutate vuex store state outside mutation handlers."
浅拷贝 || 深拷贝
// const cart = context.state.cart // !!!action模式下cart也会更改数据,严格模式开启,引用数据类型,公用一个数据
// const cart = [...context.state.cart] // 报错,浅拷贝 action不会更改啦 传到mutation
const cart = JSON.parse(JSON.stringify(context.state.cart)) // 深拷贝
-
删除当前商品
-
修改数量
-
选中 || 全选 || 计算总价
-
持久化存储
store
封装插件
// 方法1:自己封装
store/plugins/savePlugins.js
const savePlugin = (store) => {
// 当 store 初始化后调用
store.subscribe((mutation, state) => {
// 每次 mutation 之后调用
// mutation 的格式为 { type, payload }
localStorage.setItem('cart', JSON.stringify(state.shoppingCartModule.cart))
})
}
export default savePlugin
store/index.js
// 自己封装的本地存储插件
import savePlugin from './plugins/savePlugin'
plugins: [savePlugin],
初始,刷新后读取cart
cart: JSON.parse(localStorage.getItem('cart')) || [],
方法二:三方包下载引入
vuex-persistedstate(注意版本)
store.index.js
import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate";
const store = new Vuex.Store({
// ...
plugins: [createPersistedState({
storage: sessionStorage, // 默认为localStorage
})],
});
登录页面处理
页面静态书写
获取验证码接口
login.js
export const getCode = (phone) => {
return request({
method: 'get',
url: API.TAB_SESSIONS_code + '/' + phone,
params: {
phone,
},
})
}
index.vue
import { login, getCode } from '@/api/login'
async getCaptcha (phone) {
// 如果没有手机号
if (!phone) { return }
const data = await getCode(phone)
console.log('data', data) // {code: '8704'}
// 验证码反馈后直接填上
this.code = data.code
},
登录接口
login.js
export const login = (userInfo) => {
const { phone, code } = userInfo
return request({
url: API.TAB_API_SESSIONS,
method: 'post',
data: {
phone,
code,
},
})
}
index.vue
async onSubmit (values) { // values=>{phone: '1232435346', code: '8749'}
// console.log('submit', values)
/* try catch捕获异常 */
try {
const data = await login(values)
console.log(data)
} catch (err) {
console.log(err)
}
登录成功之后
怎么样让多个页面传参呢?
- vuex
- localStorage
- 动态路径||query
- …
登录成功=>个人页面=>点击业务(查看个人信息等)=>判断是否登录?登录才能访问私人数据
登录成功后,获取到用户数据同步到vuex中
const result = await this.$store.dispatch('userModule/loginAction', values)
console.log('userinfo', result)
if (result) { // 获得用户数据了
// 跳转到个人中心页面
this.$router.push('./mine')
}
权限处理
个人中心=>没有登录自动跳转到登录页面=>登录成功跳转到个人中心事件
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。
面试:导航守卫流程
router/index.js
/*
* 定义全局前置守卫,实现页面级的权限拦截。
* 在导航进入每个页面前,都会执行这个钩子函数
*/
router.beforeEach((to, from, next) => {
// to: 即将进入的路由对象
// from: 正在离开的路由对象
// next:函数,下一步,是导航解析流程中的下一步。
if (to.meta?.permission) { // 需要用户登录后才能访问
if (store.state.userModule.token) { // 有 token 则说明用户已登录
next()
} else { // 未登录,跳转到登录页面
next('/login')
}
} else {
next()
}
})
课选择只将token数据存储到本地,三方包插件配置
plugins: [createPersistedState({
// storage: sessionStorage, // 默认为localStorage
paths: [
'userModule.token', // 选择性的本地存储数据token
],
})],
个人中心根据是否有token值,有则请求用户数据渲染到页面
老师的
注意:加锁的接口是需要在请求头中携带认证字段:Authorization,字段值格式为
**Bearer **
// 接口数据
export const getUser = () => {
return request({
method: 'get',
url: API.TAB_TOKEN,
})
}
store方法:
/* 根据token获取用户 */
async getUSerImg (context, payload) {
const data = await getUser()
return data
},
index.vue
async created () {
const data = await this.$store.dispatch('userModule/getUSerImg')
console.log('mine', data)
},
运行环境出错
npm i # nodules包删掉重新下载,可能丢包
导航守卫
作用:守卫导航
导航守卫钩子:有机会在导航解析过程中植入开发者的业务逻辑
分类:全局、路由独享、组件内
全局导航守卫
-
beforeEach(callback): 全局前置守卫
-
任何页面都先要进入beforeEach函数中
-
callback: function(to, from, next)
-
next()一定要执行
-
-
beforeResolve():全局解析守卫
-
afterEach():全局后置钩子
路由独享
-
beforeEnter()
{ path: '/mine', name: 'mine', // component: Mine, components: { footer: AppTabbar, default: Mine, }, meta: { title: '我的', icon: '𐃎', inTabbar: true, permission: true, }, beforeEnter (to, from, next) { // 没有next() 路由跳转不过来 }, },
组件级导航守卫
-
beforeRouteEnter(to, from, next):
注意,在这个函数中,不能使用 this 调用到组件实例,因为这个钩子函数是在组件创建前就被调用到了。要在这个函数中使用到组件实例,则需要在 next(callback) 调用的时候,传递回调函数,当前组件实例对象会作为回调函数的参数传入,即 callback 结构为:vm => {}
-
beforeRouteUpdate():组件重用时导航解析过程中被调用到
-
beforeRouteLeave()
完整的导航解析流程#
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫(2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫(2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。(面试题:导航守卫中,不能用this
直接获取组件实例对象)
git clone -b 分支名字 地址
Axios二次封装
前端的重要性-前后端分离-
ajax–步骤分散–处理需要额外处理
jquery–有效封装ajax
es6崛起–promise 管理异步操作 (并行问题-需要promise, jq使用promise需要自己封装)
先要更多采用promise–axios诞生
axios是基于ajax和promise进行封装的库
fetchApi (浏览器内置的类,进行数据请求,天生就是基于Promise进行管理的)-- ajax 不是一个东西
网路发起请求两种方式:
axios,fetch
fetch
fetch([url],[options]).then(response=>{
}) // 异步请求
1.fetch是浏览器内置的类,内置的API,新的一种向服务器发请求的方式,天生就是基于Promise进行管理的
2.步骤简单
要在不支持的浏览器中使用fetch,使用polyfill
Vue-Element-Admin
项目拿到:
- gitee仓库下载
- 安装依赖
- 运行
- 出错去gitee仓库那里找 issue 答案
登录权限篇
利用导航守卫 (全局前置守卫-beforeEach() ) 来实现权限拦截处理
-
从本地读取保存的用户 token 数据
-
存在 token 时:
a.如果访问的是登陆页面,则跳转至主页面中;(已有token,用户无需再次登录 )
b.如果访问的是其他页面,则继续:
1.从 vuex 的 store 中获取用户的角色信息
2.如果已有保存用户的角色信息,则继续正常执行下一步流程(store 中有保存用户角色的信息,则说明用户角色权限相关的路由已经处理好了)
3.如果在 store 中还没有用户角色信息,则:根据token 发送网络请求获取用户基本信息后保存到store 中,从响应结果中获取到
4.有了用户角色信息后,根据用户角色信息,动态生成可访问的路由表(这些动态生成的路由表也会保存到store 中)
5.将动态生成的路由表 调用 router.addRoutes() 方法添加到路由中
6.进入正常的下一步导航流程
-
不存在 token 时:
继续判断:
如果是访问白名单页面,则继续进入下一步正常访问
如果不是白名单页面,则跳转登陆页面进行登录处理(白名单:不需要权限就能访问的页面)
// main.js
router.beforeEach((to, from, next) => {
if (store.getters.token) { // 判断是否有token
if (to.path === '/login') {
next({ path: '/' });
} else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => { // 拉取info
const roles = res.data.role;
store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch(err => {
console.log(err);
});
} else {
next() //当有用户权限的时候,说明所有可访问路由已生成 如访问没权限的全面会自动进入404页面
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next();
} else {
next('/login'); // 否则全部重定向到登录页
}
}
});
hack
这里还有一个小hack的地方,就是router.addRoutes
之后的next()
可能会失效,因为可能next()
的时候路由并没有完全add完成,好在查阅文档发现
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
这样我们就可以简单的通过next(to)
巧妙的避开之前的那个问题了。这行代码重新进入router.beforeEach
这个钩子,这时候再通过next()
来释放钩子,就能确保所有的路由都已经挂在完成了。
左侧面板
路由动态渲染-递归
vue 的render 函数实现
基于项目继续开发
添加用户管理【与complex-table结构类似】
router/module/user.js cv /table.js
自己封装)
先要更多采用promise–axios诞生
axios是基于ajax和promise进行封装的库
fetchApi (浏览器内置的类,进行数据请求,天生就是基于Promise进行管理的)-- ajax 不是一个东西
网路发起请求两种方式:
axios,fetch
fetch
fetch([url],[options]).then(response=>{
}) // 异步请求
1.fetch是浏览器内置的类,内置的API,新的一种向服务器发请求的方式,天生就是基于Promise进行管理的
2.步骤简单
要在不支持的浏览器中使用fetch,使用polyfill
Vue-Element-Admin
项目拿到:
- gitee仓库下载
- 安装依赖
- 运行
- 出错去gitee仓库那里找 issue 答案
登录权限篇
利用导航守卫 (全局前置守卫-beforeEach() ) 来实现权限拦截处理
-
从本地读取保存的用户 token 数据
-
存在 token 时:
a.如果访问的是登陆页面,则跳转至主页面中;(已有token,用户无需再次登录 )
b.如果访问的是其他页面,则继续:
1.从 vuex 的 store 中获取用户的角色信息
2.如果已有保存用户的角色信息,则继续正常执行下一步流程(store 中有保存用户角色的信息,则说明用户角色权限相关的路由已经处理好了)
3.如果在 store 中还没有用户角色信息,则:根据token 发送网络请求获取用户基本信息后保存到store 中,从响应结果中获取到
4.有了用户角色信息后,根据用户角色信息,动态生成可访问的路由表(这些动态生成的路由表也会保存到store 中)
5.将动态生成的路由表 调用 router.addRoutes() 方法添加到路由中
6.进入正常的下一步导航流程
-
不存在 token 时:
继续判断:
如果是访问白名单页面,则继续进入下一步正常访问
如果不是白名单页面,则跳转登陆页面进行登录处理(白名单:不需要权限就能访问的页面)
// main.js
router.beforeEach((to, from, next) => {
if (store.getters.token) { // 判断是否有token
if (to.path === '/login') {
next({ path: '/' });
} else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => { // 拉取info
const roles = res.data.role;
store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch(err => {
console.log(err);
});
} else {
next() //当有用户权限的时候,说明所有可访问路由已生成 如访问没权限的全面会自动进入404页面
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next();
} else {
next('/login'); // 否则全部重定向到登录页
}
}
});
hack
这里还有一个小hack的地方,就是router.addRoutes
之后的next()
可能会失效,因为可能next()
的时候路由并没有完全add完成,好在查阅文档发现
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
这样我们就可以简单的通过next(to)
巧妙的避开之前的那个问题了。这行代码重新进入router.beforeEach
这个钩子,这时候再通过next()
来释放钩子,就能确保所有的路由都已经挂在完成了。
左侧面板
路由动态渲染-递归
vue 的render 函数实现
基于项目继续开发
添加用户管理【与complex-table结构类似】
router/module/user.js cv /table.js
slot-scope:作用域插槽