本文最后更新于:10 个月前
1 生命周期函数
生命周期钩子函数
实例处于阶段
描述
能否获取到 el (this.$el)
能否获取到 data (this. Xxx)
能否使用 methods 中的方法 (this. Xxx ())
beforeCreate
创建前
实例已初始化,但数据观测,watch/event 事件回调还未配置
获取不到
不能
不能
created
创建后
已完成如下配置,数据观测 (data observer),property 和方法的运算,watch/event 事件回调
获取不到
能
能
beforeMount
挂载前
dom 已初始化,但并未挂载和渲染
能
能
能
mounted
挂载后
dom 已完成挂载和渲染
能
能
能
beforeUpdate
更新前
数据已改变,但 dom 未更新
能
能
能
updated
更新后
dom 已更新
能
能
能
beforeDestroy
销毁前
实例销毁前,实例仍然可用
能
能
能
destroyed
销毁后
实例已销毁,所有指令被解绑,事件监听器被移除,子实例都被销毁
能
能
能
2 API 2.1 声明式 API
Vue 的核心功能是声明式渲染 :通过扩展于标准 HTML 的模板语法,我们可以根据 JavaScript 的状态来描述 HTML 应该是什么样子的。当状态改变时,HTML 会自动更新。
我们可以使用 data
组件选项来声明响应式状态,该选项应该是一个返回对象的函数:
1 2 3 4 5 6 7 export default { data ( ) { return { message : 'Hello World!' } } }
message
属性可以在模板中使用。下面展示了我们如何使用双花括号法,根据 message
的值来渲染动态文本:
在双花括号中的内容并不只限于标识符或路径 ——我们可以使用任何有效的 JavaScript 表达式 。
1 <h1 > {{ message.split('').reverse().join('') }}</h1 >
2.2 组合式 API 我们可以使用 Vue 的 reactive()
API 来声明响应式状态。由 reactive()
创建的对象都是 JavaScript Proxy ,其行为与普通对象一样:
1 2 3 4 5 6 import { reactive } from 'vue' const counter = reactive ({ count : 0 })console .log (counter.count ) counter.count ++
[!NOTE] Tipsreactive()
只适用于对象 (包括数组和内置类型,如 Map
和 Set
)。而另一个 API ref()
则可以接受任何值类型。ref
会返回一个包裹对象,并在 .value
属性下暴露内部值。
1 2 3 4 import { ref } from 'vue' const message = ref ('Hello World!' )console .log (message.value ) message.value = 'Changed'
在组件的 <script setup>
块中声明的响应式状态,可以直接在模板中使用
1 2 <h1 > {{ message }}</h1 > <p > count is: {{ counter.count }}</p >
使用任何有效的 JavaScript 表达式
1 <h1 > {{ message.split('').reverse().join('') }}</h1 >
3 基础语法 3.1 属性绑定
双大括号只能进行文本插值,为了给 attribute 绑定一个动态值,需要使用 v-bind
指令, 可以缩写只用 :
表示属性绑定
1 2 <div v-bind:id ="dynamicId" > </div > <div :id ="dynamicId" > </div >
3.1.1 动态绑定多个属性 3.1.1.1 对象 1 2 3 4 const objectOfAttrs = { id : 'container' , class : 'wrapper' }
通过不带参数的 v-bind
,你可以将它们绑定到单个元素上
1 <div v-bind ="objectOfAttrs" > </div >
3.1.1.2 字典 1 <div :class ="{info:info,danger:danger}" > 我叫:{{name}},年龄:{{age}}</div >
1 2 3 4 data : {info : true , danger : false , },
3.1.1.3 列表 1 <div :class ="[info,danger]" > 我叫:{{name}},年龄:{{age}}</div >
1 2 3 info : "c1" , danger : "c2" , },
3.1.2 调用函数
可以在绑定的表达式中使用一个组件暴露的方法
1 2 3 <span :title ="toTitleDate(date)" > {{ formatDate(date) }}</span >
3.2 事件监听
使用 v-on
指令监听 DOM 事件,可以简写为 @
,表示事件监听
3.2.1 不带参数 1 2 <button v-on:click ="increment" > {{ count }}</button > <button @click ="increment" > {{ count }}</button >
1 2 3 4 5 6 7 8 <script setup>import { ref } from 'vue' const count = ref (0 )function increment ( ) { count.value ++ } </script>
3.2.2 带传参 1 <h1 @mouseover ="dosomething('过来了')" @mouseout ="dosomething('离开了')" > 注册</h1 >
1 2 3 4 5 methods : { dosomething : function (msg ){ console .log (msg) } }
3.3 表单绑定
同时使用 v-bind
和 v-on
来在表单的输入元素上创建双向绑定
1 <input :value ="text" @input ="onInput" >
1 2 3 4 5 function onInput (e ) { text.value = e.target .value }
简化双向绑定,Vue 提供了一个 v-model
指令,它实际上是上述操作的语法糖
[!NOTE] Tips
v-model
会将被绑定的值与 <input>
的值自动同步
v-model
不仅支持文本输入框,也支持诸如多选框、单选框、下拉框之类的输入类型
完整 demo
1 2 3 4 5 6 7 8 <script setup> import { ref } from 'vue' const text = ref('') </script> <template> <input v-model="text" placeholder="Type here"> <p>{{ text }}</p> </template>
3.3.1 常用标签
demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > VueDemo</title > <script src ="https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js" > </script > <style > .info { color : aquamarine; } .danger { color : red; } </style > </head > <body > <div id ="app" > <h1 > 注册</h1 > <div > <input type ="text" v-model ="info.username" placeholder ="用户名" > <input type ="password" v-model ="info.pwd" placeholder ="密码" > </div > <div > 男:<input type ="radio" v-model ="info.gender" value ="1" > 女:<input type ="radio" v-model ="info.gender" value ="2" > </div > <div > 篮球:<input type ="checkbox" v-model ="info.hobby" value ="h1" > 足球:<input type ="checkbox" v-model ="info.hobby" value ="h2" > 排球:<input type ="checkbox" v-model ="info.hobby" value ="h3" > </div > <div > <select v-model ="info.city" > <option value ="c1" > 北京</option > <option value ="c2" > 上海</option > <option value ="c3" > 广州</option > <option value ="c4" > 深圳</option > </select > </div > <div > <select v-model ="info.field" multiple > <option value ="f1" > 前端</option > <option value ="f2" > 后端</option > <option value ="f3" > 运维</option > <option value ="f4" > 算法</option > </select > </div > <div > <textarea v-model ="info.other" > </textarea > </div > <input type ="button" value ="注册" @click ="clickme" > </div > <script > var app = new Vue ({ el : "#app" , data : { info : { username : "" , pwd : "" , gender : "1" , hobby : ["h1" ], city : "c1" , field : ["f1" ], other : "" , } }, methods : { clickme : function ( ) { console .log (this .info ) } } }) </script > </body > </html >
3.4 条件渲染 3.4.1 v-if
使用 v-if
指令来==有条件地渲染元素 ==(不一定渲染), 也可以使用 v-else
和 v-else-if
来表示其他的条件分支
1 2 <h1 v-if ="awesome" >Vue is awesome!</h1><h1 v-else > Oh no 😢</h1 >
3.4.2 v-show
v-show 根据**==变量的真假==决定是否显示该标签( 一定会渲染但不一定显示 **)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > VueDemo3</title > <script src ="https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js" > </script > </head > <body > <div id ="app" > <div > <button @click ="issms=false" > 用户名登录</button > <button @click ="issms=true" > 手机号登录</button > </div > <div v-show ="!issms" > <label > 用户名</label > <input placeholder ="用户名" type ="text" v-model ="username" > <label > 密码</label > <input placeholder ="密码" type ="password" v-model ="pwd" > </div > <div v-show ="issms" > <label > 手机号</label > <input placeholder ="手机号" type ="text" v-model ="phone" > <label > 密码</label > <input placeholder ="密码" type ="password" v-model ="pwd" > </div > </div > <script > var app = new Vue ({ el : "#app" , data : { issms : false , username : "" , phone : "" , pwd : "" , }, methods : {} }) </script > </body > </html >
3.5 列表渲染
使用 v-for
指令来渲染一个基于源数组的列表
3.5.1 无索引 1 2 3 4 5 <ul > <li v-for ="todo in todos" :key ="todo.id" > {{ todo.text }} </li > </ul >
todo
是一个局部变量,表示当前正在迭代的数组元素。它只能在 v-for
所绑定的元素上或是其内部访问,就像函数的作用域一样.
key 属性将它(唯一标识主键)作为特殊的 key
attribute 绑定到每个 <li>
更新列表有两种方式: 1. 在源数组上调用变更方法: todos.value.push(newTodo)
2. 使用新的数组替代原数组: todos.value = todos.value.filter(/* ... */)
完整 demo(实现列表动态增删)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <script setup > import { ref } from 'vue' let id = 0 const newTodo = ref ('' )const todos = ref ([ { id : id++, text : 'Learn HTML' }, { id : id++, text : 'Learn JavaScript' }, { id : id++, text : 'Learn Vue' } ]) function addTodo ( ) { todos.value .push ({id :id++,text :newTodo.value }) newTodo.value = '' } function removeTodo (todo ) { todos.value .pop (todo) todos.value = todos.value .filter ((t ) => t !== todo) } </script > <template > <form @submit.prevent ="addTodo" > <input v-model ="newTodo" > <button > Add Todo</button > </form > <ul > <li v-for ="todo in todos" :key ="todo.id" > {{ todo.text }} <button @click ="removeTodo(todo)" > X</button > </li > </ul > </template >
3.5.2 有索引 1 2 3 4 5 <ul > <li v-for ="(todo,index) in todos" :id ={{index}} > {{ todo }} </li > </ul >
3.5.3 有键值 1 2 3 4 5 <ul > <li v-for ="(value,key) in item" > {{ key }}:{{value}} </li > </ul >
3.6 计算属性
computed()
。它可以让我们创建一个计算属性 ref,这个 ref 会动态地根据其他响应式数据源来计算其 .value
computed (计算属性)
可用于快速计算视图(View)中显示的属性。这些计算将被缓存 ,并且只在需要时更新。computed 设置的初衷是能够解决复杂的计算 ,而不是直接在模板字符串里进行运算。
实现显示(隐藏)已完成的 todos
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <script setup> import { ref, computed } from 'vue' let id = 0 const newTodo = ref('') const hideCompleted = ref(false) const todos = ref([ { id: id++, text: 'Learn HTML', done: true }, { id: id++, text: 'Learn JavaScript', done: true }, { id: id++, text: 'Learn Vue', done: false } ]) const filteredTodos = computed(() => { return hideCompleted.value ? todos.value.filter((t) => !t.done) : todos.value }) function addTodo() { todos.value.push({ id: id++, text: newTodo.value, done: false }) newTodo.value = '' } function removeTodo(todo) { todos.value = todos.value.filter((t) => t !== todo) } </script> <template> <form @submit.prevent="addTodo"> <input v-model="newTodo"> <button>Add Todo</button> </form> <ul> <li v-for="todo in filteredTodos" :key="todo.id"> <input type="checkbox" v-model="todo.done"> <span :class="{ done: todo.done }">{{ todo.text }}</span> <button @click="removeTodo(todo)">X</button> </li> </ul> <button @click="hideCompleted = !hideCompleted"> {{ hideCompleted ? 'Show all' : 'Hide completed' }} </button> </template> <style> .done { text-decoration: line-through; } </style>
3.7 文本插值
最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号)
1 <span > Message: {{ msg }}</span >
双大括号标签会被替换为相应组件实例中 msg
属性的值。同时每次 msg
属性更改时它也会同步更新。
3.8 原始 HTML
双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,需要使用 v-html
指令
1 2 <p > Using text interpolation: {{ rawHtml }}</p > <p > Using v-html directive: <span v-html ="rawHtml" > </span > </p >
4 Demo 1 表格增删 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > vuedemo2</title > <script src ="https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js" > </script > </head > <body > <div id ="app" > <h1 > 用户</h1 > <div > <label > 用户名</label > <input type ="text" v-model ="username" placeholder ="用户名" > <label > 密码</label > <input type ="password" v-model ="pwd" placeholder ="密码" > </div > <div > <input type ="button" value ="添加" @click ="adduser" > </div > <div > <table > <thead > <tr > <th > 用户名</th > <th > 密码</th > <th > 操作</th > </tr > </thead > <tbody > <tr v-for ="(user,uid) in users" > <td > {{user.name}}</td > <td > {{user.pwd}}</td > <td > <button v-if ="user.name!='' && user.pwd!=''" @click ="deluser(uid)" > 删除</button > </td > </tr > </tbody > </table > </div > </div > <script > var app = new Vue ({ el : "#app" , data : { username : "" , pwd : "" , users : [ {name :"" ,pwd :"" }, ], }, methods : { adduser : function ( ) { let userinfo = {name :this .username ,pwd :this .pwd }; this .users .push (userinfo); this .username ="" ; this .pwd ="" ; console .log (userinfo); }, deluser : function (uid ) { this .users .splice (uid,1 ); } } }) </script > </body > </html >
5 Demo 2 登录(axios) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > VueDemo3</title > <script src ="https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js" > </script > <script src ="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.js" > </script > </head > <body > <div id ="app" > <div > <button @click ="issms=false" > 用户名登录</button > <button @click ="issms=true" > 手机号登录</button > </div > <div v-show ="!issms" > <div > <label > 用户名</label > <input placeholder ="用户名" type ="text" v-model ="info.username" > </div > <div > <label > 密码</label > <input placeholder ="密码" type ="password" v-model ="info.pwd" > </div > </div > <div v-show ="issms" > <div > <label > 手机号</label > <input placeholder ="手机号" type ="text" v-model ="sms.phone" > </div > <div > <label > 验证码</label > <input placeholder ="验证码" type ="text" v-model ="sms.code" > </div > </div > <input @click ="login" type ="button" value ="登录" > </div > <script > var app = new Vue ({ el : "#app" , data : { issms : false , info : { username : "" , pwd : "" , }, sms : { phone : "" , code : "" , }, }, methods : { login : function ( ) { let dataobj = this .issms ? this .sms : this .info ; axios ({ url : "http://localhost/login" , method : "post" , parameters : "" , data : dataobj, headers : { 'Content-Type' : 'application/json' }, }).then (function (res ) { console .log (res); }).catch (function (error ) { console .log (error); alert (error.message ) }) } } }) </script > </body > </html >
6 组件
提高相同代码的复用率。
6.1 局部组件
局部组件需要挂载到 Vue 根组件上,***components: { alias: component}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > vuedemo4</title > <script src ="https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js" > </script > </head > <body > <div id ="app" > <login > </login > </div > <script > const login = { data: function () { return { msg: "局部组件", username: "", pwd: "", } }, template: ` <div > <label > 用户名</label > <input type ="text" v-model ="username" placeholder ="用户名" > <label > 密码</label > <input type ="password" v-model ="pwd" placeholder ="密码" > </div > `, methods: {} }; var app = new Vue({ el: "#app", data: {}, methods: {}, components: { login: login, } }) </script > </body > </html >
6.2 全局组件
全局子组件不用挂载到 Vue 上,直接用***Vue. Component ('component_name',{})***
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > vuedemo5</title > <script src ="https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js" > </script > </head > <body > <div id ="app" > <login > </login > </div > <script > Vue.component('login', { data: function () { return { msg: "局部组件", username: "", pwd: "", } }, template: ` <div > <label > 用户名</label > <input type ="text" v-model ="username" placeholder ="用户名" > <label > 密码</label > <input type ="password" v-model ="pwd" placeholder ="密码" > </div > `, methods: {} }); var app = new Vue({ el: "#app", data: {}, methods: {}, }) </script > </body > </html >
7 路由
引入 vue-router: <script src="https://cdn.bootcdn.net/ajax/libs/vue-router/4.1.6/vue-router.global.min.js"></script>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > vuedemo6</title > <script src ="https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js" > </script > <script src ="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js" > </script > </head > <body > <div id ="app" > <div class ="menu" > <div class ="container" > <router-link to ="/" > Logo</router-link > <router-link to ="/home" > 首页</router-link > <router-link to ="/course" > 课程</router-link > </div > </div > <div class ="container" > <router-view > </router-view > </div > </div > <script > const Home = { data : function ( ) { return { msg : "首页组件" , } }, template : ` <h1>{{ msg }}</h1> ` , methods : {} }; const Course = { data : function ( ) { return { msg : "课程组件" , } }, template : ` <h1>{{ msg }}</h1> ` , methods : {} }; const router = new VueRouter ({ routes : [ {path : '/' , component : Home }, {path : '/home' , component : Home }, {path : '/course' , component : Course }, ], }); var app = new Vue ({ el : "#app" , data : {}, methods : {}, components : { Home : Home , Course : Course , }, router : router }) </script > </body > </html >
8 路由使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > vuedemo6</title > <script src ="https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js" > </script > <script src ="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.js" > </script > <script src ="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js" > </script > </head > <body > <div id ="app" > <div class ="menu" > <div class ="container" > <router-link to ="/" > Logo</router-link > <router-link to ="/home" > 首页</router-link > </div > </div > <div class ="container" > <router-view > </router-view > </div > </div > <script > const Home = { data : function ( ) { return { imgls : [], } }, created : function ( ) { axios ({ url : "https://picsum.photos/v2/list" , method : "get" , headers : { 'Content-Type' : 'application/json' }, }).then (res => { this .imgls = res.data ; console .log (this .imgls , res.data ); }).catch ((error ) => { alert (error.message ) }) }, mounted : function ( ) { console .log (this .imgls ) }, template : ` <div class="red"> <div v-for="img in imgls"> <a :href="img.download_url"> <img :src="img.url" style="align-content: center" alt="加载图片失败"> </a> </div> </div> ` , methods : {}, }; const router = new VueRouter ({ routes : [ {path : '/' , component : Home }, {path : '/home' , component : Home }, ], }); var app = new Vue ({ el : "#app" , data : {}, methods : {}, components : { Home : Home , }, router : router, }) </script > </body > </html >
9 Element-UI 使用
Element 是国内饿了么公司提供的一套开源前端框架,简洁优雅,提供了 Vue、React、Angular 等多个版本。 文档地址: 一个 Vue 3 UI 框架 | Element Plus 安装: npm i element-ui 引入 Element: main. js (vue 2):
1 2 3 4 5 6 7 8 9 import Vue from 'vue' ;import ElementUI from 'element-ui' ;import 'element-ui/lib/theme-chalk/index.css' ;import App from './App.vue' ;Vue .use (ElementUI );new Vue ({ el :#app, render : h => h (App ) );
main. js (vue 3):
1 2 3 4 5 6 7 8 import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' const app = createApp (App ) app.use (ElementPlus ) app.mount ('#app' )
9.1 第三方图标库 由于 Element UI 提供的字体图符较少,一般会采用其他图表库,如著名的 Font Awesome Font Awesome 提供了 675 个可缩放的矢量图标,可以使用 cSS 所提供的所有特 性对它们进行更改,包括大小、颜色、阴影或者其他任何支持的效果。 文档地址: http://fontawesome.dashgame.com/ 安装: npm i font-awesome
使用: import 'font-awesome/css/font-awesome.min.css'
10 Axios 10.1 使用方法
Axios 是一个基于 promise 网络请求库,作用于 node. js 和浏览器中。 Axios 在浏览器端使用 XMLHttpRequests 发送网络请求,并能自动完成 JSON 数据的转换。 安装: npm install axios 地址: https://www.axios-http.cn/
Get 请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 axios.get (/user?ID =12345 ') .then(function(response){ //处理成功情况 console.log(response); }) .catch(function (error)( //处理错误情况 console.log(error); }) .then(function (){ //总是会执行 });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //上述请求也可以按以下方式完成(可选) axios.get ("/user',{ params:{ ID:12345 } }) .then(function (response){ console.log(response); }) .catch(function (error){ console.log(error); }) .then(function (){ //总是会执行 });
POST 请求:
1 2 3 4 5 6 7 8 9 10 axios.post ('/user' , { firstName :'Fred' , lastName :'Flintstone' }) .then (function (response ){ console .log (response); }) .catch (function (error ){ console .log (error); });
异步回调问题(async/await):
1 2 3 4 5 6 7 async function getUser ( ) ( try ( const response = await axios.get ('/user?ID=12345' ); console .log (response) } catch (error){ console .error (error);
其他请求方式:
1 2 3 4 5 6 7 8 9 axios ({ method :'post' , url :"/user/12345', data: { firstName: " Fred ', lastName:"Flintstone' } });
axios.get (url, configl)
axios.delete (url[, config])
axios.head (uri[, config))
axios.options (url, config])
axios.post (url, datal, configl)
axios.put (url, datal, config]l)
axios.patch (url, datal, config]l)
10.2 与 Vue 整合 10.3 跨域
为了保证浏览器的安全,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源,称为同源策略,同源策略是浏览器安全的基石
同源策略 (Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能
所谓同源 (即指在同一个域) 就是两个页面具有相同的协议(protocol),主机 (host) 和端口号 (port)
当一个请求 url 的协议、域名、端口三者之间任意一个与当前页面 url 不同即为跨域,此时无法读取非同源网页的 Cookie,无法向非同源地址发送 AJAX 请求
解决方案:
CORS(Cross-Origin Resource Sharing)是由 W 3 C 制定的一种跨域资源共享技术标准,其目的就是为了解决前端的跨域请求。
CORS 可以在不破坏即有规则的情况下,通过后端服务器实现 CORS 接口,从而实现跨域通信。
CORS 将请求分为两类:简单请求和非简单请求,分别对跨域通信提供了支持。
10.3.1 Spring Boot 中配置 CORS 在传统的 Java EE 开发中,可以通过过滤器统一配置,而 Spring Boot 中对此则提供了更加简洁的解决方案方法 1:
1 2 3 4 5 6 7 8 9 10 @Configuration public class CorsConfig implements WebMvcConfigurer ( @override public void addCorsMappings (CorsRegistry registry) ( registry.addMapping("/**" ) .allowedorigins("*" ) .allowedMethods("POST" ,"GET" ,"PUT" ,"OPTIONS" ,"DELETE" ) .maxAge(168000 )//预检间隔时间 .allowedHeaders("*" ) .allowCredentials(true );
方法 2: 给控制器类前加 CrossOrigin 注解使用默认跨域配置@CrossOrigin
10.4 全局配置 Axios
在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题: 每个组件中都需要导入 axios 每次发请求都需要填写完整的请求路径 可以通过全局配置的方式解决上述问题:
1 2 3 4 5 6 axios.defaults .baseURL = 'http://api.com' app.config .globalProperties .$http = axiosVue .prototype .$http = axios
11 VueRouter 11.1 安装与使用
Vue 路由 vue-router 是官方的路由插件,能够轻松的管理 SPA 项目中组件的切换。
Vue 的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来
vue-router 目前有 3. x 的版本和 4. x 的版本,vue-router 3. x 只能结合 vue 2 进行使用,vue-router 4. x 只能结合 vue 3 进行使用
安装: **npm install vue-router@4 **
11.1.1 创建路由组件 在项目中定义 Discover. vue、Friends. vue、MyMusic. vue 三个组件,将来要使用 vue-router 来控制它们的展示与切换: Discover. vue:1 2 3 4 5 6 7 8 9 10 11 12 <template> <div > <h1 > 发现音乐</h1 > </div > </template> <script > export default { name : "Discover" } </script > <style scoped > </style >
发现音乐 Friends. vue :1 2 3 4 5 6 7 8 9 10 11 12 <template> <div > <h1 > 关注</h1 > </div > </template> <script > export default { name : "Friends" } </script > <style scoped > </style >
关注 MyMusic. vue:1 2 3 4 5 6 7 8 9 10 11 12 <template> <div > <h1 > 我的音乐</h1 > </div > </template> <script > export default { name : "MyMusic" } </script > <style scoped > </style >
声明路由链接和占位标签
可以使用 <router-link>
标签来声明路由链接,并使用 <router-view>
标签来声明路由占位符。示例代码如下:
App. vue:1 2 3 4 5 6 7 8 9 10 11 <template> <div > <h1 > APP 组件</h1 > <router-link to ="/discover" > 发现音乐</router-link > <router-link to ="/mymusic" > 我的音乐</router-link > <router-link to ="/friend" > 关注</router-link > <router-view > </router-view > </div > </template>
### 11.1.2 创建路由模块 在项目中创建 index. js 路由模块,加入以下代码: vue 2 路由的使用1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import VueRouter from 'vue-router' import vue from 'vue' import Discover from '@/components/Discover. vue' import Friends from '@/components/Friends. vue' import MyMusic from '@/components/MyMusic. vue' Vue . use (VueRouter )const router = new VueRouter ({ / / 指定 hash 属性与组件的对应关系 routes : [ { path : '/discover' , component : Discover }, { path : '/friends' , component : Friends }, { path : "/mymusic', component: MyMusic}, ] }) export default router
vue 3 路由的使用1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView. vue' const router = createRouter ({ history : createWebHistory (import . meta. env. BASE_URL ), routes : [ { path : '/' , name : 'home' , component : HomeView }, { path : '/about' , name : 'about' , component : () => import ('../views/AboutView. vue' ) } ] }) export default router
11.1.3 挂载路由模块 在 main. js 中导入并挂载 router1 2 3 4 5 6 7 8 import Vue from 'vue' import App from './App. vue' import router from './router' Vue . config. productionTip = false new Vue ({render : h => h (App ), router }).$mount (' #app ' )
### 11.1.4 路由重定向 > 路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C,从而展示特定的组件页面。 > > 通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:1 2 3 4 5 6 7 8 9 10 const router = new VueRouter (t / /指定 hash 属性与组件的对应关系 routes : [ {path : '/, redirect: ' /discover', {path: ' /discover', component: Discover}, {path: ' /friends', component: Friends}, {path: ' /my', component: MyMusic} ] })
11.2 子路由 11.2.1 嵌套路由
在 Discover. vue 组件中,声明 toplist 和 playlist 的子路由链接以及子路由占位符。示例代码如下:
1 2 3 4 5 6 7 8 9 10 <template> <div > <h1 > 发现音乐</h1 > <router-link to ="/discover/toplist" > 推荐</router-link > <router-link to ="/discover/playlist" > 歌单</router-link > <hr > <router-view > </router-view > </div > </template >
[!NOTE] Tips 在 src/router/index. js
路由模块中,导入需要的组件,并使用 children 属性声明子路由规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const router = new VueRouter (t / /指定 hash 属性与组件的对应关系 routes :[ { path : "/', redirect: " /discover'}, { path: ' /discover', component: Discover, //通过 chi 1 dren 属性,嵌套声明子路由 chiidren: [ { path: "toplist", component: TopList }, { path: "playlist", component: PlayList }, ] }, { path: ' /friends', component: Friends }, { path: ' /mymusic', component: MyMusic }, 1 3)
11.2.2 动态路由 思考:有如下 3 个路由链接:1 2 3 4 5 6 7 8 9 10 11 <router-link to="/product/1" >商品 1 </router-link> <router-link to ="/product/2" > 商品 2</router-link > <router-link to ="/product/3" > 商品 3</router-link > const router = new VueRouter (t / /指定 hash 属性与组件的对应关系 routes : [ path : '/product/1' , component : Product , path : '/product/2' , component : Product , path : '/product/3' , component : Product , ] })
> 上述方式复用性非常差。 > 动态路由指的是:把 Hash 地址中可变的部分定义为参数项,从而提高路由规则的复用性。在 vue-router 中使用英文的冒号(:)来定义路由的参数项。示例代码如下: > {path: "/product/: id', component: Product}
> 通过动态路由匹配的方式染出来的组件中,可以使用 $route. params
对象访问到动态匹配的参数值,比如在商品详情组件的内部,根据 id 值,请求不同的商品数据 。1 2 3 4 5 6 7 8 9 10 <template> <h1 > Product 组件</h1 > !--获取动态的 id 值--> <p > ($route. params. id)</p > </template><script > export default t name : 'Product' </script >
> 为了简化路由参数的获取形式,vue-router 允许在路由规则中开启 props 传参。示例代码如下: { path:/: id', component: Product, props: true}
1 2 3 4 5 6 7 8 9 10 11 12 <template> <h1 > Product 组件</h1 > <!--获取动态的id值--> <p > {{id}} </p > </template><script > export default { name : 'Product' , props : ["id" ] } </script >
11.2.3 编程式导航
声明式
编程式
<router-link :to="..." >
router. push (...)
除了使用 <router-link>
创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。
想要导航到不同的 URL,则使用 router. push
方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
当你点击 <router-link>
时,这个方法会在内部调用,所以说,点击router-link:to="..." 等同于调用 router. push(…)。
1 2 3 4 5 6 7 8 9 10 11 12 <template> <button@click="gotoProduct(2)">跳转到商品 2</button > </template> <script > export default { methods : { gotoProduct : function (id ){ this .$router . push ('/production/${id}' ) } } } </script >
11.3 导航守卫
导航守卫可以控制路由的访问权限。示意图如下: 全局导航守卫会拦截每个路由规则,从而对每个路由进行访问权限的控制。 你可以使用 router. beforeEach
注册一个全局前置守卫:
1 2 3 4 5 6 7 8 router. beforeEach ((to, from , next ) => { if (to. path ==='/main' && ! isAuthenticated) { next ('/login' ) } else { next () } })
to:即将要进入的目标
from:当前导航正要离开的路由
在守卫方法中如果声明了 next 形参,则必须调用 next () 函数,否则不允许用户访问任何一个路由!
直接放行:next ()
强制其停留在当前页面:next (false)
强制其跳转到登录页面:next ('/login')
12 VueX 12.1 VueX 介绍
对于组件化开发来说,大型应用的状态往往跨越多个组件。在多层嵌套的父子 组件之间传递状态已经十分麻烦,而 Vue 更是没有为兄弟组件提供直接共享数 据的办法。
基于这个问题,许多框架提供了解决方案使用全局的状态管理器,将所有 分散的共享数据交由状态管理器保管,Vue 也不例外。
Vuex 是一个专为 Vuejs 应用程序开发的状态管理库,采用集中式存储管理应 用的所有组件的状态。
简单的说,Vuex 用于管理分散在 Vue 各个组件中的数据。
安装: npm install vuex@next
12.1.1 状态管理
每一个 Vuex 应用的核心都是一个 store,与普通的全局对象不同的是,基于 Vue 数据与视图绑定的特点,当 store 中的状态发生变化时,与之绑定的视图也会被重新渲染。
store 中的状态不允许被直接修改,改变 store 中的状态的唯一途径就是显式地提交 (commit)mutation,这可以让我们方便地跟踪每一个状态的变化。
在大型复杂应用中,如果无法有效地跟踪到状态的变化,将会对理解和维护代> 码带来极大的困扰。
Vuex 中有 5 个重要的概念:State、Getter、Mutation、Action、Module 。
12.1.2 State
State 用于维护所有应用层的状态,并确保应用只有唯一的数据源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import {createStore} from 'vuex' const store = createstore ({ state () { return { count : 0 } }, mutations : { increment (state) { state.count ++ } } })export default store
在组件中,可以直接使用 this.$store.state.count
访问数据,也可以先用 mapState 辅助函数将其映射下来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { mapState } from 'vuex' export default { computed : mapState ({ count : state => state.count , countAlias : 'count' , countplusLocalState (state) { return state.count + this .localcount } }) }
12.1.3 Mutation
Mutation 提供修改 State 状态的方法。
1 2 3 4 5 6 7 8 9 10 11 12 const store = createstore ({ state return { count : 0 } }, mutations : { increment (state) { state.count ++ } } })
在组件中,可以直接使用 store.commit 来提交 mutation
1 2 3 4 5 6 methods : { increment () { this .$store .commit ('increment' ) console .log (this .$store .state .count ) } }
也可以先用 mapMutation 辅助函数将其映射下来
1 2 3 4 5 6 methods : { ...mapMutations ([ 'increment' , 'incrementBy' ]),
12.1.4 Action
Action 类似 Mutation,不同在于:
Action 不能直接修改状态,只能通过提交 mutation 来修改,Action 可以包含异步操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const store = createstore ({ state : { count : 0 }, mutations : { increment (state) { state. count++ } }, actions : { increment (context) context.commit ('increment' ) } } })
在组件中,可以直接使用 this.$store.dispatch (xxx')
分发 action,或者使用 mapActions
辅助函数先将其映射下来
1 2 3 4 5 6 7 methods : { ...mapActions ([ 'increment' , 'incrementBy' ]),
12.1.5 Getter
Getter 维护由 State 派生的一些状态,这些状态随着 State 状态的变化而变化
1 2 3 4 5 6 7 8 9 10 11 12 13 const store = createstore ({ state : { todos :[ { id : 1 , text : '...' , done : true }, { id : 2 , text : '...' , done : false } ] }, getters : { doneTodos : (state ) => { return state.todos .filter (todo => todo.done ) } } })
在组件中,可以直接使用 this.$store.getters.doneTodos
,也可以先用 mapGetters
辅助函数将其映射下来,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 import { mapGetters } from 'vuex' export default { computed : { ...mapGetters ([ 'doneTodosCount' "anotherGetter', // ... ]) } }
12.1.5.1 通过属性访问
Getter 会暴露为 store.getters
对象,你可以以属性的形式访问这些值:
Getter 也可以接受其他 getter 作为第二个参数:
1 2 3 4 5 6 getters : { doneTodosCount (state, getters) { return getters.doneTodos .length } }
1 store.getters .doneTodosCount
我们可以很容易地在任何组件中使用它:
1 2 3 4 5 computed : { doneTodosCount () { return this .$store .getters .doneTodosCount } }
注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。
12.1.5.2 通过方法访问
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
1 2 3 4 5 6 getters : { getTodoById : (state ) => (id ) => { return state.todos .find (todo => todo.id === id) } }
1 store.getters .getTodoById (2 )
注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
12.1.5.3 mapGetters
辅助函数 mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
1 2 3 4 5 6 7 8 9 10 11 12 import { mapGetters } from 'vuex' export default { computed : { ...mapGetters ([ 'doneTodosCount' , 'anotherGetter' , ]) } }
如果你想将一个 getter 属性另取一个名字,使用对象形式:
1 2 3 4 ...mapGetters ({ doneCount : 'doneTodosCount' })
12.2 Vuex 安装与使用
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性,让你少按几次键:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { mapState } from 'vuex' export default { computed : mapState ({ count : state => state.count , countAlias : 'count' , countPlusLocalState (state) { return state.count + this .localCount } }) }
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState
传一个字符串数组。
1 2 3 4 computed : mapState ([ 'count' ])
eg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <template> <div > <h1 > 我的音乐</h1 > <router-link :to ="murl" > 音乐{{ mid }}</router-link > <router-link :to ="nmurl" > 音乐{{ mid + 1 }}</router-link > <div > <router-view > </router-view > </div > <button @click ="nextMusic" > 下一首</button > </div > </template> <script > import {mapState} from "vuex" ; export default { name : "MyMusic" , computed : mapState ([ 'mid' , ]), data ( ) { return { murl : "/mymusic/0" , nmurl : "/mymusic/1" , } }, methods : { nextMusic ( ) { this .$store .commit ('increment' ) this .murl = "/mymusic/" + this .mid this .nmurl = "/mymusic/" + (this .mid + 1 ) console .log (this .$store .state .mid ) } } } </script > <style scoped > </style >
getter—eg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import {createStore} from 'vuex' const store = createStore ({ state : { mid : 0 , todos : [ {id : 1 , text : '学习' , done : true }, {id : 2 , text : '吃饭' , done : true }, {id : 3 , text : '睡觉' , done : false } ] }, getters : { doneTodos (state ) { return state.todos .filter (todo => todo.done ) }, doneTodosCount (state, getters ) { return getters.doneTodos .length }, getTodoById : (state ) => (id ) => { return state.todos .find (todo => todo.id === id) } }, mutations : { increment (state ) { state.mid ++ } } }) export default store
13 MockJS 13.1 MockJs 介绍
Mock.js 是一款前端开发中拦截 Ajax 请求再生成随机数据响应 的工具,可以用来模拟服务器响应
优点是非常简单方便,无侵入性,基本覆盖常用的接口数据类型.
支持生成随机的==文本、数字、布尔值、日期、邮箱、链接、图片、颜色==等。 安装: npm install mockjs
13.2 MockJs 使用
在项目中创建 mock 目录,新建 index.js 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import Mock from 'mockjs' Mock .mock ('/product/search' , { "ret" : 0 , "data" : { "mtime" : "@datetime" , "score|1-800" : 800 , "rank|1-100" : 100 , "stars|1-5" : 5 , "nickname" : "@cname" , "img" :"@image('200x100','#ffcc33', '#FFF','png','Fast Mock')" } });
在 main. js 中导入 mock 下的 index.js
1 2 3 4 5 6 7 8 9 10 import {createApp} from 'vue' import App from './App.vue' import router from './router' import store from './store' import './mock' import './assets/main.css' const app = createApp (App ) app.use (router) app.use (store) app.mount ('#app' )
组件中调用 mock. js 中模拟的数据接口,这时返回的 response 就是 mock.js 中用==Mock.mock (‘url’, data)中设置的 data==
1 2 3 4 5 6 7 8 import axios from 'axios' export default { mounted : function ( ){ axios.get ("/prduct/search" ).then (res => { console .log (res) }) } }
13.3 核心方法 Mock.mock(rurl?, rtype?, template|function ( options ) )
rurl,表示需要拦截的 URL,可以是 URL 字符串或 URL 正则 rtype,表示需要拦截的 Ajax 请求类型 。例如 GET、POST、PUT、DELETE 等。 template,表示数据模板,可以是对象或字符串 function,表示用于生成响应数据的函数 。
设置延时请求到数据
1 2 3 4 5 6 7 8 Mock .setup ({ timeout : 400 }) Mock .setup ({ timeout : '200-600' })
13.4 数据生成规则
mock 的语法规范包含两层规范:数据模板 (DTD)、数据占位符 (DPD)
数据模板中的每个属性由 3 部分构成:**属性名 name、生成规则 rule、属性值 value: ‘name|rule’: value
属性名和生成规则之间用竖线|
分隔,生成规则是可选的,有 7 种格式:
‘name|min-max’: value ‘namelcount’: value ‘name|min-max.dmin-dmax’: value ‘name|min-max.dcount’: value ‘name|count.dmin-dmax’: value ‘name|count.dcount’: value ‘name|+step’: value
13.5 生成规则与示例
属性值是字符串 String
1 2 3 4 5 6 7 8 'name|min-max' : string'name|count' : stringvar data = Mock .mock ({ 'name 1|1-3' : 'a' , 'name 2|2' : 'b' / /生成 bb })
属性值是数字 Number
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 / /属性值自动加 1 ,初始值为 number。'name|+1' : number'name|min-max' : number'name|min-max.dmin-dmax' : numberMock .mock ({ 'number1|1-100.1-10' : 1 , 'number2|123.1-10' : 1 , 'number3|123.3' : 1 , 'number4|123.10' : 1 123 }) { "number1" : 12.92 , "number2" : 123.51 , "number3" : 123.777 , "number4" :123.1231091814 }var data = Mock .mock ({ 'name1|+1' :4 , 'name2|1-7' :2 , 'name3|1-4.5-8' :1 })
属性值是布尔型 Boolean
1 2 3 4 5 6 7 8 'namel1' : bolean'name|min-max' : valuevar data = Mock .mock ({ 'name|1' : true , 'name1|1-3' : true })
属性值是对象 Object
1 2 3 4 5 6 7 8 9 10 11 12 13 14 'name|count' : object'name|min-max' : objectvar obj = { a : 1 , b : 2 , c : 3 , d : 4 }var data = Mock .mock ({ 'name|1-3' : obj, 'name|2' : obj })
属性值是数组 Array
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 'name|1' : array'name|+1' : array'name|min-max' : array'name|count' : arrayMock .mock ({ "favorite_games|1-3" : [3 ,5 ,4 ,6 ,23 ,28 ,42 ,45 ], }); var arr = [1 ,2 ,3 ];var data = Mock .mock ({ 'namel|1' : arr, 'name2|2' : arr, 'name3|1-3' : arr, })
属性值是函数 Function
执行函数 function,取其返回值作为最终的属性值,函数的上下文为属性’name’所在的对象。 ‘name’: function
1 2 3 4 5 6 var fun = function (x ) return x+10 ;1 var data = Mock .mock ({ 'name' : fun (10 ) })
属性值是正则表达式 RegExp
根据正则表达式 regexp 反向生成可以匹配它的字符串。用于生成自定义格式的字符串。 ‘name’: regexp
1 2 3 4 5 6 7 8 9 10 11 Mock .mock ({ 'regexp 1' : /[a-z][A-z][0-9]/ , 'regexp 2' : /\w\w\s|s\d\D/ , 'regexp 3' : /d[5,103/ }) // => { "regexp1": "pJ7", "regexp2": "F)\fp1G", "regexp3": "561659409" }
13.6 数据占位符 DPD 占位符只是在属性值字符串中占个位置,并不出现在最终的属性值中。 占位符的格式为:
@占位符 @占位符 (参数[,参数])
关于占位符需要知道以下几点
用@
标识符标识后面的字符串是占位符
占位符引用的是 Mock.Random
中的方法。
可以通过 Mock.Random.extend ()
来扩展自定义占位符。
占位符也可以引用数据模板中的属性 。
占位符会优先引用数据模板中的属性 。
占位符支持相对路径和绝对路径 。
1 2 3 4 5 6 7 8 9 10 11 12 13 import Mock from 'mockjs' Mock .mock ('/api/msdk/proxy/query_common_credit' , { "ret" :0 , "data" : { "mtime" :"@datetime" , "score" : "@natural(1,800)" , "rank" :"@natural(1,100)" , "stars" :"@natural(0,5)" , "nickname" :"@cname" , }) ;
13.6.1 基础随机内容的生成 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "string|1-10" :"=" , "string2|3" :"=" , "number|+1" :0 , "number 2|1-10.1-3" :1 , "boolean" :"@boolean(1,2,true)" , "name" :"@cname" , "firstname" :"@cfirst" , "int" :"@integer(1,10)" , "float" :"@float (1,2,3,4)" , "range" :"@range(1,100,10)" , "natural" :"@natural(60,100)" , "email" :"@email" , "ip" : "@ip" , "datatime" :"@date('yy-MM-ddhh:mm: ss')" }
13.6.2 列表数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "code" ;"0000" , "data" : { "pageNo" : "@integer (1, 100)" , "totalRecord" : "@integer (100, 1000)" , "pagesize" : 10 , "list|10" : [{ "id|+1" : 1 , "name" : "@cword(10)" , "title" : "@cword(20)" , "descript" : "@csentence(20,50)" , "price" : "@float(10,100,10,100)" , }] }, "desc" : "成功" }
13.6.3 图片 mockjs 可以生成任意大小,任意颜色块,且用文字填充内容的图片,使我们不用到处找图片资源就能轻松实现图片的模拟展示
1 2 3 4 5 6 7 8 9 10 11 12 13 { "code" : "0000" , "data" : { "pageNo" : "@integer(1, 100)" , "totalRecord" : "@integer(100, 1000)" , "pagesize" : 10 , "list|10" : [{ "image" : "@image ('200 x 100','#ffcc33 ','#FFF','png','Fast Mock')" }] }, "desc" :"成功" }
13.6.4 Mock. Random
Mock. Random 是一个工具类,用于生成各种随机数据。
Mock. Random 的方法在数据模板中称为『占位符』,书写格式为@占位符 (参数[, 参数)。 用法示例:
1 2 3 4 5 6 7 var Random = Mock .Random Random .email ()Mock .mock ('@email' )Mock .mock ({email : '@email' })
13.7 总结
[!NOTE] tips 如果前端请求的后端接口需要携带参数,那么前端 mock 的 index. js 里的拦截请求的写法应该如下所示(用正则表达式进行匹配接口):Mock.mock(RegExp('/product/search.*'),{...})
14 企业级集成方案 14.1 vue-element-admin 介绍
1 2 3 4 5 6 7 8 9 10 # 克隆项目 git clone https://github.com/PanJiaChen/vue-admin-template.git# 进入项目目录 cd vue-admin-template# 安装依赖 npm install# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 npm install --registry=https://registry.npm.taobao.org# 启动服务 npm run dev
15 JWT 跨域认证 15.1 Session 认证 互联网服务离不开用户认证。一般流程是下面这样。
用户向服务器发送用户名和密码 。
服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等。
服务器向用户返回一个 session_id,写入用户的 Cookie。
用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
session 认证流程: session 认证的方式应用非常普遍,但也存在一些问题,扩展性不好,如果是服务 器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能 够读取 session,针对此种问题一般有两种方案:
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。
一种方案是服务器不再保存 session 数据,所有数据都保存在客户端,每次请求都发回服务器。Token 认证就是这种方案的一个代表。
15.2 Token 认证 Token 是在服务端产生的一串字符串, 是客户端访问资源接口(API) 时所需要的资 源凭证,流程如下:
客户端使用用户名跟密码请求登录,服务端收到请求,去验证用户名与密码
验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
客户端每次向服务端请求资源的时候需要带着服务端签发的 token
服务端收到请求,然后去验证客户端请求里面带着的 token,如果验证成功,就向客户端返回请求的数据
基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。
用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力 减少频繁的查询数据库
token 完全由应用管理,所以它可以避开同源策略
15.3 JWT 的使用
JSON Web Token(简称 JWT)是一个 token 的具体实现方式,是目前最流行 的跨域认证解决方案。 JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,具体如下:
1 2 3 "姓名" : "张三" , "角色" : "管理员" , "到期时间" : "2018 年 7 月 1 日 0 点 0 分"
用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。 为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名 。
JWT 的由三个部分组成,依次如下: Header (头部) Payload (负载) Signature (签名) 三部分最终组合为完整的字符串,中间使用·分隔,如下: Header.Payload.Signature eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdwIi0iIxMjMoNTY30DkwIiwibmFtzsI6IkpvaG4 gRG91IiwiaXNTb2NpYWwiOnRydwV9. 4pcPyMD09o1PSyXnrXCjTwXyr4BsezdI1AVTmud2fU4
Header 部分是一个 JSON 对象,描述 JWT 的元数据
1 2 3 4 { "alg" : "H256" , "typ" : "JWT" }
alg 属性表示签名的算法(algorithm ),默认是 HMAC SHA 256 (写成HS256 )
typ 属性表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT
最后,将上面的 JSON 对象使用 Base 64 URL 算法转成字符串。
15.3.2 Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了 7 个官方字段,供选用。
iss (issuer):签发人
exp (expiration time): 过期时间
sub (subject): 主题
aud (audience): 受众
nbf (Not Before): 生效时间
iat (Issued At):签发时间
jti (WT ID): 编号
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在个部分。 这个 JSON 对象也要使用 Base 64 URL 算法转成字符串。
15.3.3 Signature
Signature 部分是对前两部分的签名,防止数据篡改。 首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户; 然后,使用 Header 里面指定的签名算法 (默认是 HMAC SHA 256),按照下面的公式产生签名。
1 2 3 4 HMACSHA 256 ( base64UrlEncode (header) + "." + base64UrlEncode (payload), secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(’.’)分隔,就可以返回给用户。
15.3.4 特点
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
客户端每次与服务器通信,都要带上这个 JWT,可以把它放在 Cookie 里面自动发送,但是这样不能跨域。
更好的做法是放在 HTTP 请求的头信息’Authorization’字段里面,单独发送。
15.3.5 请求认证
15.3.6 JWT验证拦截器
定义拦截器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.alleyf.config; import com.alibaba.fastjson2.JSON; import com.alleyf.sys.utils.Result; import com.alleyf.common.JwtUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component @Slf4j public class JwtValidateInterceptor implements HandlerInterceptor { @Autowired private JwtUtils jwtUtils; @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("X-Token" ); log.debug(request.getRequestURI() + "待验证:" + token); if (token != null ) { try { jwtUtils.getClaimsByToken(token); log.debug(request.getRequestURI() + " 验证通过" ); return true ; } catch (Exception e) { e.printStackTrace(); } } log.debug(request.getRequestURI() + " 禁止访问" ); response.setContentType("application/json;charset=utf-8" ); response.getWriter().write(JSON.toJSONString(Result.error().message("jwt令牌无效,请重新登录" ))); return false ; } }
配置使用拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.alleyf.config; import com.alleyf.config.JwtValidateInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class MyInterceptorConfig implements WebMvcConfigurer { @Autowired private JwtValidateInterceptor jwtValidateInterceptor; @Override public void addInterceptors (InterceptorRegistry registry) { InterceptorRegistration registration = registry.addInterceptor(jwtValidateInterceptor); registration.addPathPatterns("/**" ).excludePathPatterns( "/user/login" , "/user/register" , "/user/logout" , "/user/info" , "/error" ); } }
15.3.7 后端实现 15.3.7.1 加入依赖 1 2 3 4 5 <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.1</version > </dependency >
15.3.7.2 生成 Token 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private static Long expire = 604800 ;private static String secret = "abcdfghiabcdfghiabcdfghiabcdfghi" ;public static String generateToken (String username) { Date now = new Date (); Date expiration = new Date (now.getTime() + 1000 * expire); return Jwts.builder () .setHeaderParam("type" ,"JWT" ) .setSubject(username) .setIssuedAt(now) .setExpiration(expiration) .signWith(SignatureAlgorithm.HS512, secret) .compact(); }
15.3.7.3 解析 token 1 2 3 4 5 6 7 public static Claims getClaimsByToken (String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); }
15.3.7.4 后端完整部分 UserController. java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 package com.alleyf.airesume.controller; import com.alleyf.airesume.entity.User; import com.alleyf.airesume.mapper.UserMapper; import com.alleyf.airesume.utils.JwtUtils; import com.alleyf.airesume.utils.Result; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @Api(tags = "用户", value = "用户") @RestController @CrossOrigin @RequestMapping("/user") public class UserController { @Autowired UserMapper userMapper; @ApiOperation("用户登录") @PostMapping("/login") public Result login (@RequestBody User user) { String token = JwtUtils.generateToken(user.getUsername()); return Result.ok().data("token" , token); } @ApiOperation("获取用户信息") @GetMapping("/info") public Result info (String token) { String username = JwtUtils.getClaimsByToken(token).getSubject(); String url = "https://img2.baidu.com/it/u=1325995315,4158780794&fm=26&fmt=auto&gp=0.jpg" ; return Result.ok().data("name" , username).data("avatar" , url); } @ApiOperation("注销") @PostMapping("/logout") public Result logout () { return Result.ok(); } @ApiOperation("查询所有用户") @GetMapping("/queryAll") public List<User> queryAllUser () { return userMapper.queryAllUserAndTask(); } @ApiOperation("按照用户名查询用户") @GetMapping("/queryByMName") public User queryByMName (@RequestParam("username") String username) { return userMapper.selectByName(username); } @ApiOperation("按照用户名查询用户(MP)") @GetMapping("/queryByMPName") public User queryByMPName (@RequestParam("username") String username) { return userMapper.selectOne(new QueryWrapper <User>().eq("username" , username)); } @ApiOperation("按照用户名路径查询用户(MP)") @GetMapping("/queryByPMPName/{username}") public User queryByPMPName (@PathVariable("username") String username) { return userMapper.selectOne(new QueryWrapper <User>().eq("username" , username)); } @ApiOperation("按照页码查询用户(MP)") @GetMapping("/queryByPage/{page}") public IPage queryByPage (@PathVariable("page") int page) { Page<User> page1 = new Page <>(page, 5 ); IPage iPage = userMapper.selectPage(page1, null ); return iPage; } @ApiOperation("添加用户") @PostMapping("/add") public String addUser (User user) { return userMapper.insert(user) > 0 ? "添加成功" : "添加失败" ; } }
Result.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 package com.alleyf.airesume.utils; import java.util.HashMap; import java.util.Map; public class Result { private Boolean success; private Integer code; private String message; private Map<String, Object> data = new HashMap <>(); private Result () { } public static Result ok () { Result r = new Result (); r.setCode(ResultCode.Success); r.setSuccess(true ); r.setMessage("成功" ); return r; } public static Result error () { Result r = new Result (); r.setCode(ResultCode.Error); r.setSuccess(false ); r.setMessage("失败" ); return r; } public Result success (Boolean success) { this .setSuccess(success); return this ; } public Result message (String message) { this .setMessage(message); return this ; } public Result code (Integer code) { this .setCode(code); return this ; } public Result data (String key, Object value) { this .data.put(key, value); return this ; } public Result data (Map<String, Object> map) { this .setData(map); return this ; } public Boolean getSuccess () { return success; } public void setSuccess (Boolean success) { this .success = success; } public Integer getCode () { return code; } public void setCode (Integer code) { this .code = code; } public String getMessage () { return message; } public void setMessage (String message) { this .message = message; } public Map<String, Object> getData () { return data; } public void setData (Map<String, Object> data) { this .data = data; } }
JwtUtils.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.alleyf.airesume.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; public class JwtUtils { private static final Long expire = 604800L ; private static final String secret = "abcdfghiabcdfghiabcdfghiabcdfghi" ; public static String generateToken (String username) { Date now = new Date (); Date expiration = new Date (now.getTime() + 1000 * expire); return Jwts.builder() .setHeaderParam("type" , "JWT" ) .setSubject(username) .setIssuedAt(now) .setExpiration(expiration) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public static Claims getClaimsByToken (String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }