2020-08-05

vue框架介绍

框架,framework,是能够让程序开发人员更好的专注于业务逻辑的开发,而无需关心底层功能的实现。

vue是一个渐进式 JavaScript 框架,Vue (读音 /vjuː/,类似于 **view**) 是一套用于构建用户界面的**渐进式框架**。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。
国人自己的开发的框架,作者是:尤雨溪

vue有两大核心:数据驱动页面、组件化

vue框架学习内容

vue、vue-cli脚手架、vue-router路由、ui库、样式预处理器stylus、网络请求axios、状态管理vuex、服务器端渲染

vue优点-缺点

优点:易学、速度快、采用虚拟DOM、数据双向绑定、指令系统、生态圈活跃

缺点:兼容性,不支持ie8及以下的浏览器、语法报错提示不是特别的准确

vue基本使用

安装方式一:引入js文件使用
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
安装方式二:结合node环境,下载vue使用(进入一个指定的目录,然后执行命令进行下载)
    npm install vue
注:下载慢--设置淘宝镜像
npm config set registry "https://registry.npm.taobao.org"

vue常用配置选项
速查表

new Vue({
	el:'#app',		//设置挂载点 类似于querySelector
	data:{},		//初始数据
	menthods:{},	//自定义函数
	watch:{},		//监听
	computed:{},	//计算属性
	filters:{},		//过滤器
	components:{},	//自定义组件
	beforeCreate(){},	//创建之前
	created(){},		//创建完成
	beforeMount(){},	//挂载之前
	mounted(){},		//挂载完成
	beforeUpdate(){},	//更新之前
	updated(){},		//更新完成
	beforeDestroy(){},//销毁之前
	destroyed(){},	//销毁完成
})

el配置选项

指定vue的作用范围,相当于js中querySelector,只会配到满足条件的第一个标签,所以我们一般使用id选择器(不适用class或者标签选择器)。

data配置选项

初始化页面数据,初始化的数据会直接挂在vue实例上

methods 自定义函数

methods,用来存放用户自定义函数

常用指令
内容展示
mustache 语法

mustache 语法(文本插值) {{ 变量名或者单行JS语法 }}

v-text

可以解析data中设置的初始值,v-text把初始值设置到指定的标签内。
和mustache的区别
如果要展示的内容是固定的,可以使用v-text
如果要展示的内容中的一部分是固定的,可以是使用mustache

注意:所有v-xxx指令都要写标签的开始标签上

v-html

可以解析带有html标签的内容

<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 1 引入vue.js 核心文件 -->
    <script src="../node_modules/vue/dist/vue.js"></script>
</head>
 
<body>
    <!-- 2 设置挂载点 -->
    <div id="box">
        <!-- mustache语法 -->
        <!-- 文本插值 -->
        <h3>{{ `小豪:${txt}` }}</h3>
        <p>{{ nu + 10 }}</p>
        <!-- 布尔值 -->
        <p>{{ isshow ? 'true' : 'false' }}</p>
        <!-- 对象类型 -->
        <p>{{ `姓名:${user.name}` }}</p>
        <!-- 数组对象 -->
        <p>{{ `姓名:${users[1].name}` }}</p>
        <hr>
        <!-- v-text 替换源标签内所有内容-->
        <p v-text="user.name">我被v-text内容覆盖了</p>
        <hr>
        <!-- v-html -->
        <p v-text="ele">我不能识别html标签</p>
        <p v-html="ele"></p>
        <hr>
        <!-- 数据双向绑定 -->
        <input type="text" v-model="txt">
        <p>{{ txt }}</p>
    </div>
    <script>
        // 3 示例化vue
        new Vue({
            el: '#box',
            data: {
                txt: 'vue基础学习',
                nu: 20,
                isshow: false,
                ele:'<b>你好,我要加粗显示</b>',
                user: {
                    name: '小代',
                    age: 20
                },
                users: [
                    {
                        name: '小代',
                        age: 20
                    },
                    {
                        name: '小代2',
                        age: 22
                    }
                ]
            }
        })
    </script>
</body>
 
</html>

条件判断
v-if v-else-if v-else

根据条件表达式或者布尔值的结果来展示或者不展示指定标签
当表达式的结果为true时,在页面结构中才会存在指定的标

v-show

不论条件表达式或者布尔值的结果是什么,v-show指定的标签都会在页面结构中存在
​        当表达式的结果为true时,在页面结构中会显示指定的标签
​        当表达式的结果为false时,在指定的标签上会添加一个display:none属性
使用场景:
​    当页面中,要根据指定的内容来频繁的切换某些内容时,要使用v-show

列表渲染–循环
v-for语法格式

<标签 v-for="(每次遍历的变量名[,每次遍历的元素在数组的下标]) of/in 要遍历的数据源"></标签>

可以根据数组元素数量来生成指定数量的标签

v-for key属性

遍历的数据源类型是数组时:

第一个参数是数组中每一项元素,第二个参数是数组中每项元素的下标

<ul>
    <li v-for="(user,index) of users">
        <!-- 字符串拼接方式 -->
        <!-- <p>{{ '姓名:'+user.name+',年龄:'+${user.age} }}</p> -->
        <!-- 模板语法方式 -->
        <p>{{ index }}----{{ `姓名:${user.name},年龄:${user.age}` }}</p>
    </li>
</ul>

遍历的数据源类型是对象时:

第一个参数是对象中每一项元素的value属性,第二参数是每一项元素的key属性,第三个参数是每一项元素的下标

 <p v-for="(fruite,index,val) of fruites">{{ val }}---{{ index }}---{{ fruite }}</p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="../node_modules/vue/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <div v-if="islogin">已登录</div>
        <div v-else>未登录</div>
        <div v-show="islogin">显示</div>
        <div v-show="!islogin">隐藏</div>
        <!-- 
            注意:v-if有更高的切换开销
            v-show有更高的初始渲染开销。
            因此,如果要非常频繁的切换,则使用v-show较好;如果在运行时条件不太可能改变,则v-if较好
         -->
        <hr>
        <ul>
            <li v-for='(item,idx) of arr'>
                {{ idx }}--->{{ item }}
            </li>
        </ul>
        <ul>
            <!-- 数组 -->
            <li v-for="(item,idx) of arrObj">
                <p>{{ idx }}--{{ item }}</p>
                <!-- 对象 -->
                <p v-for="(item,key,idx) of item">{{ `${idx}-${key}-姓名:${item}` }}</p>
            </li>
        </ul>
    </div>
    <script>
        new Vue({
            el:'#app',
            data:{
                islogin:true,
                arr:[11,22,33,44,55,66],
                obj:{
                    name:'小豪',
                    age:22
                },
                arrObj:[
                    {
                        name:'dyh',
                        age:19
                    },
                    {
                        name:'dyh1',
                        age:14
                    },
                    {
                        name:'dyh2',
                        age:12
                    }
                ]
            }
        })
    </script>
</body>
</html>

事件绑定
v-on

语法格式:<标签 v-on:事件名=“自定义函数名”></标签>
简写:<标签 @事件名="自定义函数"></标签>

//=========事件绑定
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box{
            width: 300px;
            height: 300px;
            background-color: orange;
        }
    </style>
    <script src="../node_modules/vue/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <!-- <button v-on:click="toggle">{{ !isshow ? '显示' : '隐藏' }}</button> -->
        <!-- 简化版 -->
        <button @click="toggle">{{ !isshow ? '显示' : '隐藏' }}</button>
        <div class="box" v-show="isshow">使用行内样式控制显示隐藏</div>
    </div>
    <script>
        new Vue({
            el:"#app",
            data:{
                isshow:true
            },
            methods:{
                toggle(){
                    //Vue中的this 指向Vue实例
                    this.isshow = !this.isshow;
                }
            }
        })
    </script>
</body>
</html>
//=========选项卡练习
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>选项卡-vue</title>
    <script src="../node_modules/vue/dist/vue.js"></script>
</head>
<body>
    <div id="page">
        //根据按钮数组遍历出指定数量的按钮
        //给按钮绑定点击事件,并把当前遍历的元素下标赋值给指定的变量
        <button v-for="(btn,btnidx) of btns" @click="showidx = btnidx">{{ btn }}</button>
        //根据按钮数组遍历出指定数量的内容标签
        //遍历时,拿当前数组下标与指定遍历进行比较,如果相等则显示,否则不显示
        <div class="content" v-for="(btn,index) of btns" v-show="index == showidx">
            //根据数组下标来遍历对象内容
            <p v-for="con of news[index]">{{ con }}</p>
        </div>
    </div>
    <script>
        new Vue({
            el:"#page",
            data:{
                showidx:0,//默认显示哪个新闻内容
                btns:[
                    '北京新闻1','中国新闻2','国际新闻3'
                ],
                news:[
                    {
                        '新闻1':'北京新闻内容1',
                        '新闻2':'北京新闻内容2',
                        '新闻3':'北京新闻内容3'
                    },
                    {
                        '新闻1':'中国新闻内容1',
                        '新闻2':'中国新闻内容2',
                        '新闻3':'中国新闻内容3'
                    },
                    {
                        '新闻1':'国际新闻内容1',
                        '新闻2':'国际新闻内容2',
                        '新闻3':'国际新闻内容3'
                    }
                ]
            }
        })
    </script>
</body>
</html>

属性绑定
v-bind

语法格式:<标签 v-bind:属性名=“属性值”></标签>
可以简写:<标签 :属性名=“属性值”></标签>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>样式绑定</title>
    <style>
        #box div{
            width: 100px;
            height: 100px;
        }
        .red{
            color:red;
        }
        .blue{
            color:blue;
        }
    </style>
    <script src="../node_modules/vue/dist/vue.js"></script>
</head>
<body>
    <div id="box">
        <p v-bind:style="styleA">v-bind绑定样式</p>
        <button @click="green">绿色</button>
        <img v-bind:src="imgUrl">
        <!-- v-bing简写方式 : -->
        <p :class="className">简写</p>
        <button @click="toggle">切换</button>
    </div>
    <script>
        new Vue({
            el:"#box",
            data:{
                styleA:{
                    'background-color':'#f00'
                },
                className:'red',
                imgUrl:'https://cn.vuejs.org/images/logo.png'
            },
            methods:{
                green(){
                    this.styleA = {
                    'background-color':'green'
                    }
                },
                toggle(){
                    this.className = 'blue'
                }
            }
        })
    </script>
</body>
</html>

Vue常见错误解析

vue.js:634 [Vue warn]: Do not mount Vue to <html> or <body> - mount to normal elements instead.

vue实例只能挂在到普通的元素标签上,不能是html、body标签,而且推荐使用id选择器来获取标签

[Vue warn]: Property or method "XXX" is not defined on the instance but referenced during render.

vue框架是一个声明式的框架,所以在挂载点内要使用变量时,要定义才能使用。

属性绑定
style

第一种用法:直接使用变量
<标签 v-bind:sytle="styleA"></标签>
<script>
	new Vue({
		...
		data:{
			styleA:{
				'background-color':'#f00'
			}
		}
	
	})
</script>
 
第二种用法:使用对象
<标签 v-bind:style="{ 属性名:属性值,... }"
注意:如果属性名中包含"-“,把横杠去掉,并横杠后的字母变成驼峰法命名,或者把属性名用引号引起来
如果属性值是一个单词,也要用引号引起来
比如:font-size、background-color... 
​	fontSize、backgroundColor...
第三种用法:使用数组
<标签 v-bind:style="[变量1,变量2]"

class

第一种用法:直接使用变量
<标签 :class="变量名"></标签>
 
第二种用法:使用对象
<标签 :class="{ 类名:表达式或者布尔值 }"></标签>
当表达式或者布尔值的结果为true时,表示使用该类名,否则就不使用该类名
 
第三种用法:使用数组
<标签 :class="[‘类名1’,‘类名N’,...]"
class使用数组时,每一个类名都要加上引号才会被解析成class属性。

表单元素双向绑定

设计模式

MVC:
​    Model 数据模型层
​    View 视图层
​    Controller 控制器层
强制的把程序的输入、输出和处理分开

MVVM:
​    Model 数据模型层
​    View 视图层
​    ViewModel 视图模型层

内容展示
输入框

<div id="box">
    <!-- view -->
    <input type="text" v-model="msg">
    <p>{{ msg }}</p>
</div>
<script>
    new Vue({
    el:"#box",
    data:{ //可以理解为是model
        msg:""
    }
})
</script>

文本域

<!-- 文本域 -->
<textarea v-model="article"></textarea>
<p>{{ article }}</p>
<script>
    new Vue({
        el:"#box",
        data:{ //可以理解为是model
            msg:"",
            article:"这是一篇技术文章"
        }
    })
</script>

checkbox

数组
语法格式:<input type="checkbox" v-model="变量名" value="属性值" />内容

//数组
<input type="checkbox" v-model="hobbies" value="看电影">看电影
<input type="checkbox" v-model="hobbies" value="打游戏">打游戏
<input type="checkbox" v-model="hobbies" value="运动">运动
<p>{{ hobbies }}</p>
<script>
    new Vue({
        el:"#box",
        data:{ //可以理解为是model
            msg:"",
            article:"这是一篇技术文章",
            hobbies:["打游戏"]
        }
	})
</script>
 
//布尔值
<input type="checkbox" v-model="isagree">是否同意协议
<!-- 不使用v-model实现单选 -->
<input type="checkbox" :checked="isagree" @click="isagree = !isagree">是否同意协议
<p>{{ isagree }}</p>

radio

radio和checkbox使用v-model一定要加上value属性
<div>
    <span>性别:</span>
    <input type="radio" v-model="sex" value="男">男
    <input type="radio" v-model="sex" value="女">女
    </div>
<p>{{ sex }}</p>
<script>
new Vue({
    el:"#box",
    data:{ //可以理解为是model
        ...
        sex:'男'
    }
})
</script>

select

select双向绑定不需要添加到option标签中,只需要在select的开始标签里写v-model即可。
<select v-model="course">
    <option value="">请选择</option>
    <option value="0">web前端</option>
    <option value="1">java</option>
    <option value="2">ui</option>
</select>
<p>{{ course }}</p>
<script>
    new Vue({
        el:"#box",
        data:{ 
            ...,
            course:0
        }
    })
</script>

自定义指令

vue支持我们自定义一些指令来完成一定的操作

Vue.directive('指令名称',{
	inserted:function(el){
		el.focus();//让元素获得焦点
		//其他操作...
	}
})
new Vue({...})
 
注意,自定义指令,需要写在vue被实例化之前
 
//========示例
// 自定义命令需要在vue实例化前定义
Vue.directive('test',{
    //inserted 当指定插入到标签中时
    inserted:function(e){
        //e 是形参,名字可以自定义,此时e就代表添加了自定义指定v-test那个元素
        e.focus();
        e.value = '测试自定义指令'
        console.log(e)
    }
})
new Vue({
    el:'#app'
})

用户信息收集功能

<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
    <link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.css">
    <script src="../node_modules/vue/dist/vue.js"></script>
</head>
 
<body>
    <div id="box" class="container">
        <form class="form-horizontal" autocomplete="off">
            <h3 class="text-center">用户信息收集</h3>
            <div class="form-group">
                <label for="name" class="col-sm-2 control-label">姓名:</label>
                <div class="col-sm-6">
                    <input type="text" id="name" class="form-control" v-model.trim="info.name">
                </div>
            </div>
            <div class="form-group">
                <label for="age" class="col-sm-2 control-label">年龄:</label>
                <div class="col-sm-6">
                    <input type="text" id="age" class="form-control" v-model.trim="info.age">
                </div>
            </div>
            <div class="form-group">
                <label for="age" class="col-sm-2 control-label"></label>
                <div class="col-sm-6">
                    <input type="button" value="提交" class="btn btn-primary" @click="add()">
                    <input type="button" value="重置" class="btn" @click="clear()">
                    <input type="button" value="删除所有用户" class="btn btn-warning" @click="delAll">
                </div>
            </div>
        </form>
        <!-- 表格 -->
        <table class="table table-bordered table-hover">
            <thead>
                <tr>
                    <th>姓名</th>
                    <th>年龄</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                <tr v-for="(obj,idx) of users" :idx="idx">
                    <td>{{ obj.name }}</td>
                    <td>{{ obj.age }}</td>
                    <td class="col-sm-3  text-center">
                        <button class="btn btn-primary" @click="edit(idx)">编辑</button>
                        <button class="btn btn-danger" @click="del(idx)">删除</button>
                    </td>
                </tr>
                <tr v-show="users.length==0" class="text-center">
                    <td colspan="3">尚无用户信息显示!!</td>
                </tr>
            </tbody>
        </table>
 
    </div>
    <script>
        new Vue({
            el: '#box',
            data: {
                isidx:-1,//记录数组项
                info: {
                    name: '',
                    age: ''
                },
                users: JSON.parse(sessionStorage.getItem('users')) || [],
                // users: JSON.parse(localStorage.getItem('users')) || [],
            },
            methods: {
                add() {//添加 push 是把指定的内容追加到数组的末尾
                    //为空判断
                    if(this.info.name == '' || this.info.age == ''){
                        alert("请输入完整内容再提交表单!!!");
                        return false;
                    }
                    //判断是 提交 修改
                    if(this.isidx == -1){
                        //添加
                        this.users.push(JSON.parse(JSON.stringify(this.info)));
                    }else{
                        // 修改替换
                        this.users.splice(this.isidx,1,(JSON.parse(JSON.stringify(this.info))));
                        // this.users[this.isidx] = this.info;
                    }
                    //对象的浅拷贝问题
                    this.session();
                    this.clear();
                },
                session(){//本地临时存储
                    sessionStorage.setItem("users",JSON.stringify(this.users));
                },
                clear() {//重置
                    this.isidx = -1; //恢复提交状态
                    this.reset();
                },
                reset() {//清空
                    this.info = {
                        name: '',
                        age: ''
                    }
                },
                edit(id){//编辑信息
                    console.log(this.users[id])
                    // 深浅拷贝
                    this.info = JSON.parse(JSON.stringify(this.users[id]));
                    this.isidx = id; //赋值数组下标
 
                },
                del(id){//删除
                    this.users.splice(id,1);
                    this.session();
                },
                delAll(){
                    // 把数组赋值空
                    this.users = [];
                    // 1删除本地存储中指定key的内容
                    sessionStorage.removeItem('users');
                    // localStorage.removeItem('users');
                    // 2还可以清空本地存储所有数据
                    // sessionStorage.clear();
                    // localStorage.clear();
                }
            }
        })
    </script>
</body>
 
</html>

修饰符
事件修饰符

阻止默认事件
<标签 @事件名.prevent></标签>

阻止事件冒泡
<标签 @事件名.stop></标签>

捕获事件冒泡
<标签 @事件名.capture></标签>
注意:.capture修饰符不会阻止或者改变事件冒泡,但是会改变冒泡函数执行的顺序。

self修饰符
<标签 @事件名.self></标签>
注意:.self强调的是当前操作的元素只有是它自己本身时,才会触发指定的函数。

只执行一次
<标签 @事件名.once></标签>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>修饰符</title>
    <script src="../node_modules/vue/dist/vue.js"></script>
    <style>
        .box,.big{
            width: 300px;
            height: 300px;
            background-color: #f00;
        }
        .small{
            width: 100px;
            height: 100px;
            background-color: blue;
        }
    </style>
</head>
<body>
    <div id="app">
        <!-- 阻止默认事件 -->
        <div class="box" @contextmenu.prevent="menu"></div>
        <hr>
        <!-- 阻止事件冒泡 -->
        <div class="big" @click="bigClick">
            <div class="small" @click.stop="smallClick"></div>
        </div>
        <hr>
        <!-- 捕获事件冒泡 -->
        <div class="big" @click.capture="bigClick">
            <div class="small" @click="smallClick"></div>
        </div>
        <hr>
        <!-- self修饰符 -->
        <div class="big" @click.self="bigClick">
            <div class="small" @click="smallClick"></div>
        </div>
        <hr>
        <!-- once修饰符 -->
        <div class="big" @click.self.once="bigClick">
            <div class="small" @click="smallClick"></div>
        </div>
    </div>
    <script>
        new Vue({
            el:"#app",
            methods:{
                menu(){
                    // if(e){
                    //     e.preventDefault();
                    // }else{
 
                    // }
                    // return false;
                    console.log("鼠标右键执行了")
                },
                bigClick(){
                    console.log("大盒子被点击了")
                },
                smallClick(e){
                    // e.stopPropagation();
                    console.log("小盒子被点击了")
                }
            }
        })
    </script>
</body>
</html>

表单元素修饰符

.lazy
不再对数据进行实时双向绑定,而是在执行change事件时才进行双向绑定

.number
number修饰符不强制页面用户输入的内容,而是转变数据类型为number
如果写的内容是数字开头,字符串结尾,那么number修饰符会把字符串过滤掉
如果写的内容是字符串开通,数字结尾,那么number修饰符不做任何操作。

.trim
过滤输入内容左右两边的空格,不包含中间的空格。

<body>
    <div id="app">
        <!-- lazy修饰符 -->
        <input type="text" v-model.lazy="msg">
        <p>{{ msg }}</p>
        <hr>
        <!-- number修饰符 -->
        <input type="text" v-model.number="num">
        <button @click="getType">获取数据类型</button>
        <hr>
        <!-- trim修饰符 -->
        <input type="text" v-model.trim="str">
        <p>{{ str }}</p>
    </div>
    <script>
        new Vue({
            el:"#app",
            data:{
                msg:'',
                num:0,
                str:''
            },
            methods:{
                getType(){
                    console.log(typeof this.num)
                }
            }
        })
    </script>
</body>

其他修饰符

@事件.enter      回车键
@事件.down      下键
@事件.up           上键
@事件.left 
@事件.right
@事件.esc
@事件.tab

数据本地存储

localStorage
sessionStorage
方法
    数据添加:setItem('key',value)
            sessionStorage.setItem("users",JSON.stringify(this.users))

    数据读取:getItem('key')
            sessionStorage.getItem('users')

    删除数据:removeItem('key')
        根据指定的key来进行删除
            sessionStorage.removeItem('users');
            localStorage.removeItem('users');

    删除本地存储中所以数据:clear()
            sessionStorage.clear();
            localStorage.clear();

模拟跨域请求

基本步骤
第一步:创建一个script标签
var s = document.createElement("script");

第二步:设置script标签的src属性
s.src = "http://suggestion.baidu.com/su?cb=getwd&wd="+this.ipt;

第三步:把生成好的script标签追加到页面中
document.body.append(s)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .select{
            background-color: coral;
            color: #fff;
        }
    </style>
    <!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
    <script src="../node_modules/vue/dist/vue.js"></script>
</head>
<body>
    <div id="root">
        <!-- @input="search" -->
        <input type="text" v-model="ipt"  @keydown.down="down" @keydown.up.prevent="up" @keydown.enter="enter">
        <button @click="search">搜索</button>
        <ul>
            <li v-for="(item,idx) of arr" :class="{select:idx==isid}">{{ item }}</li>
        </ul>
    </div>
    <script>
        const vm = new Vue({
            el:'#root',
            data:{
                arr:[],
                ipt:'',
                isid:-1
            },
            methods:{
                search(){
                    if(this.ipt == '')return;
                    // cb 回调函数名称
                    // wd 要搜索的关键词
                    var s = document.createElement("script");
                    s.src="http://suggestion.baidu.com/su?cb=callback&wd="+this.ipt;
                    // s.src="https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?cb=callback&wd="+this.ipt;
                    document.body.append(s);
                },
                down(){
                    this.isid++;
                    if(this.isid>3){
                        this.isid=-1;
                    }
                },
                up(){
                    this.isid--;
                    if(this.isid<0){
                        this.isid=4;
                    }
                },
                enter(){
                    console.log(this.arr[this.isid],this.isid)
                    if(this.isid == -1 || this.isid == 4){
                        window.open("https://www.baidu.com/s?wd="+this.ipt);
                    }else{
                        window.open("https://www.baidu.com/s?wd="+this.arr[this.isid]);
                    }
                }
            },
            watch:{//监听
                // ipt:function() 简写
                ipt(newVal,oldVlal){
                    if(newVal == ''){
                        return false;
                    }
                    console.log(newVal);
                    console.log(oldVlal);
                    var s = document.createElement("script");
                    s.src="http://suggestion.baidu.com/su?cb=callback&wd="+newVal;
                    document.body.append(s);
                }
            }
        });
        function callback(res){
            //现在收索显示条数
            res.s.length=4;
            vm.arr = res.s;
        }
    </script>
</body>
</html>

侦听/监听器watch

可以对页面中已经定义好的变量进行监听,一旦变量值发生了改变,那么就可以执行一定操作。

普通监听

语法格式一:
new Vue({
	el
	data
	methods
	watch:{
		变量名(newVal[,oldVal]){
			业务逻辑代码...
		}
	}
})
 
语法格式二:
new Vue({
	el
	data
	watch:{
		要监听的变量名:{
			handler([newVal,oldVal]){
				业务逻辑...
			}
		}
	}
})

深度监听

如果要监听的变量类型为对象或者数组时,普通监听无法实现监听变化的效果,需要使用深度监听

new Vue({
	el
	data
	watch:{
		要监听的变量名:{
			handler([newVal,oldVal]){
				业务逻辑...
			},
			deep:true//显示的进行深度监听
		}
	}
})

<body>
    <div id="app">
        <p>普通监听</p>
        <input type="text" v-model="msg">
        <p v-show="newVal!=''">最新数据:{{ newVal }} -- 旧数据:{{ oldVla }}</p>
        <p>深度监听</p>
        <input type="text" v-model="userinfo.name">
        <p>{{ userinfo.name }}</p>
        <div v-for="user of users">
            姓名:<input type="text" v-model="user.name"><br>
            年龄:<input type="text" v-model="user.age"><br>
        </div>
    </div>
    <script>
        new Vue({
            el:'#app',
            data:{//数据
                msg:'',
                newVal:'',
                oldVla:'',
                userinfo:{
                    name:''
                },
                users:[
                    {
                        name:'小明',
                        age:18
                    },
                    {
                        name:'小芳',
                        age:19
                    }
                ]
            },
            methods:{//自定义方法
 
            },
            watch:{//侦听
                // 普通监听
                msg(newVal,oldVla){
                    this.newVal = newVal;
                    this.oldVla = oldVla;
                    console.log("数据发送了变化:"+newVal);
                },
                //深监听
                userinfo:{
                    handler(newVal){
                        console.log('用户信息发送改变:'+newVal.name);
                    },
                    deep:true //是否深度监听 默认false
                },
                users:{
                    handler(newVla){
                        console.log('用户信息发送改变');
                    },
                    deep:false
                }
            }
        })
    </script>
</body>

计算属性computed

作用:
如果页面上有需要频繁进行数学运算之后得到结果内容时,可以使用计算属性来实现。
​可以不用在data中定义计算结果的初始变量,只要在挂载点内使用了计算属性的结果后,计算属性对应的函数会自定触发。
​    计算属性依赖的数据,一旦发生改变,那么计算属性的逻辑函数会重新的执行

//语法格式
new Vue({
	el
	data
	methods
	watch
	computed:{
		要计算的结果(){
			业务逻辑...
			return 结果;
		}
	}
})
 
//=======示例
    <div id="app">
        <input type="text" v-model.number="num1"> + <input type="text" v-model.number="num2"> = {{ result }}
    </div>
    <script>
        new Vue({
            el:'#app',
            data:{
                num1:'',
                num2:''
            },
            computed:{
                result(){
                    return this.num1 + this.num2;
                }
            }
        })
    </script>

计算属性的get和set

在计算属性中内置了两个方法,一个是get,用来读取数据,一个是set用来设置数据。
在vue中默认使用的是get方法,只要在页面中使用了计算属性的变量或者计算属性变量依赖的数据发生了改变时,get方法会重新执行。

 <div id="app">
 {{ sum }}
 </div>
 <script>
     let vm = new Vue({
         el:"#app",
         data:{
             num1:10,
             num2:20
         },
         computed:{
             sum:{
                 get(){
                     console.log('get')
                     return this.num1 + this.num2
                 },
                 set(){
                 	console.log('set')
                 }
             }
         }
     })
</script>

只有给计算属性的变量直接赋值时,set函数才会执行

可以在谷歌浏览器的控制台中进行测试
vm.sum = 100 //此时自动执行set函数
vm.num1 = 20 //此时自动执行get,因为计算属性依赖于num1

计算属性和监听的区别

相同之处:
都可以根据依赖的数据变化,自动的触发相应的逻辑函数代码

不同之处:
计算属性的逻辑函数在页面使用了计算属性的变量后,就会自动的触发对应的逻辑函数
监听是只有依赖的数据发生了变化的时候,才会触发对应的逻辑函数

注意事项:
如果计算属性和监听,都对相同的数据进行操作,那么就会产生冲突互相影响。

计算属性computed和methods的区别

methods中定义的函数,只要在页面上调用,就会无条件的执行
计算属性computed中定义的函数,依赖的数据不发生变化时,只是读取,不会重新计算

计算属性特点:
依赖于数据,如果数据发生变化,那么计算属性会自动更新
计算属性的结果无需在data中定义,在页面中可以直接使用
会在vue实例上产生一个缓存,如果依赖的数据不发生变化,则会读取缓存,提高性能。

过滤器filters

对页面中要展示的数据进行处理,满足页面数据展示的需求

局部定义

new Vue({
	el
	data
	filters:{
		'过滤器名称':function(形参1[,形参N...]){
			业务逻辑
			return 结果
		}
	}
})

使用:
需要通过管道符来使用定义好的过滤器,要进行过滤的源数据 | 过滤器名称

传递额外参数:
管道符左边的是过滤器中的第一个参数,在管道符右边过滤器名称处,可以通过小括号来传递额外的参数。

<div id="app">
    <!-- 使用过滤器 -->
	总价格:{{ totalPrice | formatPrice(2) }}
</div>
<script>
    new Vue({
        el:"#app",
        data:{
            totalPrice:2999.9
        },
        filters:{
            //定义一个格式化价格的过滤器
            formatPrice(val,n=1){
            	return '¥'+val.toFixed(n)+'元';
            }
        }
    })
</script>

全局定义filter

//实例化vue之前
Vue.filter('过滤器名称',function(形参1[,形参N]){
	...
});
new Vue({})
 
全局定义的过滤器,可以在当前页面中的所有vue实例中来使用过滤器

过渡动画

<transition></transition>标签
当元素通过v-if、v-show或者动态组件的方式,进行展示或者不展示标签的情况下,才可以设置过渡动画

内置类名

匿名类名:
    进入状态
    ​    v-enter            设置进入开始状态的样式
    ​    v-enter-active 设置进入进行中状态的样式
    ​    v-enter-to    设置进入结束状态的样式
    离开状态
    ​    v-leave            设置离开开始状态的样式
    ​    v-leave-active 设置离开进行中状态的样式
    ​    v-leave-to    设置离开结束状态的样式
注意:一定要给需要过渡动画的标签用<transition>标签进行包裹,这样内容的类名才会起作用

具名类名:
如果页面上有多个元素需要设置不用的过渡动画效果,可以给transition标签设置一个name属性来进行区分,一旦设置了name属性,内置的类名就不能以.v-开头(.v-是所有匿名过渡动画的统一前缀)了,应该以.name属性的值-开头

<style>
    .content {
        width: 200px;
        height: 200px;
        background-color: coral;
    }
    .page {
        position: absolute;
        left: 300px;
        background-color: orange;
    }
 
    /* 离开动画 */
    .v-leave {
        /* 设置离开开始状态样式 */
        opacity: 1;
    }
    .v-leave-active {
        /* 设置离开进行中状态样式 */
        transition: opacity 2s;
    }
    .v-leave-to {
        /* 设置离开结束状态的样式 */
        opacity: 0;
    }
    /* 进入动画 */
    .v-enter {
        opacity: 0;
    }
    .v-enter-active {
        transition: opacity 4s;
    }
    .v-enter-to {
        opacity: 1;
    }
 
    /* 具名类名 */
    .page-leave{
        left: 300px;
    }
    .page-leave-active{
        transition: left 3s;
    }
    .page-leave-to{
        left: 0px;
    }
 
    .page-enter {
        left: 0px;
    }
    .page-enter-active {
        transition: left 2s;
    }
    .page-enter-to {
        left: 300px;
    }
</style>
</head>
<body>
<div id="app">
    <button @click="isshow = !isshow">toggle</button>
    <!-- 匿名方式 -->
    <transition>
        <div class="content" v-if="isshow"></div>
    </transition>
    <!-- 具名方式 -->
    <transition name="page">
        <div class="content page" v-show="isshow"></div>
    </transition>
</div>
<script>
    new Vue({
        el: '#app',
        data: {
            isshow: true
        }
    })
</script>

第三方工具(插件)
animate.css动画库插件

第一步:引入animate.css,可以用npm按照或者直接引入在线链接
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.0.0/animate.min.css" />
 
第二步:给需要设置过渡动画的标签添加transition标签
<transition>
      <div id="box" v-show="isshow"></div>
</transition>
 
第三步:给transition标签设置进入和离开的动画属性
<div id="app">
    <transition 
        leave-active-class="animate__animated animate__backOutDown" 
        enter-active-class="animate__animated animate__backInDown"
    >
    	<div id="box" v-show="isshow"></div>
    </transition>
    <button @click="isshow = !isshow">切换</button>
</div>
 
注意:设置动画属性的内容时,都要以animate__开头(是双下划线),具体用法见官网

vue的生命周期

vue实例从创建、挂载、更新、销毁的一个完整的过程叫生命周期

钩子函数

页面渲染期:
页面加载时自动触发的钩子函数:
beforeCreate    创建之前
created              创建完成
beforeMount    挂载之前
mounted            挂载完成    ***** 发起网络请求

页面更新期:
页面上的数据有变化时,会自动触发的钩子函数:
beforeUpdate     更新之前
updated                更新完成

页面销毁期:
vue实例被销毁时会自动触发的钩子函数:
beforeDestroy    销毁之前
destroyed            销毁完成

<div id="app">
    <input type="text" v-model="msg">
    <p>{{ msg }}</p>
</div>
<button class="destroy">销毁</button>
<button class="mount">挂载</button>
<script>
    let vm = new Vue({
        el:'#app',
        data:{
            msg:"vue生命周期"
        },
        // 1执行顺序和声明顺序无关
        //页面渲染期
        beforeCreate(){
            console.log('--------beforeCreate 创建之前--------');
            console.log(this.$el);//undefined
            console.log(this.$data);//undefined
        },
        created(){
            console.log('--------created 创建完成--------');
            console.log(this.$el);//undefined
            console.log(this.$data);//Object
        },
        beforeMount(){
            console.log('--------beforeMount 挂载之前--------');
            console.log(this.$el);//<div id="app">.值还没有解析.</div>
            console.log(this.$data);//Object
        },
        mounted(){
            console.log('--------mounted 挂载完成--------');
            console.log(this.$el);//<div id="app">.已经解析成data中对应值.</div>
            console.log(this.$data);//Object
        },
        // 2页面更新期
        beforeUpdate(){
            console.log('--------beforeUpdate 更新之前--------');
            console.log(this.$el);//<div id="app">.已经更新成data中对应值.</div>
            console.log(this.$data);//Object
        },
        updated(){
            console.log('--------updated 更新完成--------');
            console.log(this.$el);//<div id="app">.已经更新成data中对应值.</div>
            console.log(this.$data);//Object
        },
        // 3页面销毁
        beforeDestroy(){
            console.log('--------beforeDestroy 销毁之前--------');
            console.log(this.$el);//<div id="app">.data中对应值.</div>
            console.log(this.$data);//Object
        },
        destroyed(){
            console.log('--------destroyed 销毁完成--------');
            console.log(this.$el);//<div id="app">.data中对应值.</div>
            console.log(this.$data);//Object
        }
    });
    console.log(vm);
    const desBtn = document.querySelector(".destroy");
    const mouBtn = document.querySelector(".mount");
    desBtn.onclick = function(){
        vm.$destroy();//销毁vue实例
    }
    mouBtn.onclick = function(){
        vm.$mount();//挂载vue实例 但 更新周期 不会执行了
    }
</script>

组件components

组件是vuejs中最强大的功能之一,组件可以扩展html元素,封装可复用的代码,在大型项目中,为了合理分工、代码复用。每一个组件都是一个vue的实例,所以vue实例中的配置选项,在组件都可以使用。

组件的注册

局部注册
new Vue({
	el
	data
	...
	components:{
		组件名称:{
			template:"组件内容"
		}
	}
})
注意:template是组件的模板内容,必须要设置
 
全局注册
全局注册的组件,可以在所有的vue实例中使用
Vue.component('组件名称',{
	template:'组件内容'
})
new Vue({...})
使用:
在挂载点内,把组件名称当成一个普通的html标签去使用即可
//==========示例
<div id="root">
    <!-- 使用自定义组件 -->
    <mydiv></mydiv>
    <my-div></my-div>
</div>
<script>
    // 局部注册组件
    new Vue({
        el:"#root",
        components:{
            mydiv:{
                template:"<h1>这是第一个自定义组件1</h1>"
            },
            myDiv:{
                template:"<h1>这是第二个自定义组件</h1>"
            }
        }
    })
</script>

关于组件模板内容

Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.

组件的模板内容中,只能有一个根标签

常见错误

[Vue warn]: Unknown custom element: <mycom> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
组件的名字是否正确,或者检查有没有注册

注意事项关于组件名称:
vue.js:634 [Vue warn]: Do not use built-in or reserved HTML elements as component id: div
组件的名称不能是html内置的标签
组件的名称不是html内置的标签全大写
如果组件的名称包含大写字母(非首字母),在使用组件时,要把大写字母转换成-小写字母

template模板的使用
template标签的使用

template标签不会被浏览器解析,并且还可以在其中编写html代码
组件中的template属性和template标签是两个不同的东西,不要混为一谈
鉴于组件的template属性中直接编写页面内容不是很方便,所以我们可以结合template标签来设置组件的内容。
但是需要把template属性和template标签关联起来

<div id="app">
    <first></first>
</div>
<!-- 需要给template标签设置一个唯一的属性,用来区分不用的template标签 -->
<template id="first">
    <div>
        <h1>first组件</h1>
        <p>测试</p>
    </div>
</template>
<script>
    new Vue({
        el:"#app",
        components:{
            first:{
                // 关联指定的template标签
                template:"#first"
            }
        }
    })
</script>

template代码优化

由于组件的基本构成是一个对象,当页面中组件数量比较多时,注册组件时的代码量比较多,看上去非常混乱,所以我们可以把组件的构成提前定义好。

<script>
    var first = {
    	template:"#first"
    }
    var mysecond = {
    	template:"#second"
    }
    new Vue({
        el:"#app",
        components:{
        	first,mysecond
        }
    })
</script>

组件的嵌套
使用extend方法

<div id="app">
        <parent class="p"></parent>
    </div>
    <script>
        var child = Vue.extend({
            template:"<div>这是一个子组件</div>"
        })
        var parent = Vue.extend({
            template:"<div>这是一个父组件<child></child></div>",
            components:{
                child:child
            }
        })
        new Vue({
            el:"#app",
            components:{
                parent
            }
        });
    </script>

直接注册使用

使用template设置好组件的模板内容,然后用components来注册

<template id="main">
    <div>
        <my-left class="left"></my-left>
        <my-right class="right"></my-right>
    </div>
</template>
<script>
    var myLeft = { template:"#left" };
    var myRight = { template:"#right" }
    var myMain = { 
        template:"#main",
        components:{
        	myLeft,myRight
        }
    };
    new Vue({
        el:"#root",
        components:{
            myMain
        }
    })
</script>

组件中的data应该是一个函数
常见错误

vue.js:634 [Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.

因为对象类型的数据影响,组件复用时,数据会相互影响,所以为了保证数据的正确性和独立性,在组件中定义data时,要写成一个函数,并返回一个对象来定义组件的初始数据

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>组件中的data应该是一个函数</title>
    <script src="../node_modules/vue/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <!-- 直接使用对象方式定义数据 -->
        <div class="box">
            <h1>按钮被点击了{{ count }}次</h1>
            <button @click="count++">点击</button>
        </div>
        <div class="box">
            <h1>按钮被点击了{{ count }}次</h1>
            <button @click="count++">点击</button>
        </div>
        <div class="box">
            <h1>按钮被点击了{{ count }}次</h1>
            <button @click="count++">点击</button>
        </div>
        <hr>
        <!-- 组件中使用函数来定义数据 -->
        <v-box></v-box>
        <v-box></v-box>
        <v-box></v-box>
    </div>
    <template id="box">
        <div>
            <h1>按钮被点击了{{ count }}次</h1>
            <button @click="count++">点击</button>
        </div>
    </template>
    <script>
        var vBox = {
            template:"#box",
            data(){
                return{
                    count:1
                }
            }
        }
        new Vue({
            el:"#app",
            components:{
                vBox
            },
            data:{
                count:1
            }
        });
    </script>
</body>
</html>

脚手架
安装

版本的问题:稳定版 2.9.6  最新版 4.X
基本环境 :node
(1)webpack  全局安装
npm i webpack -g

(2)vue-cli  全局安装
npm i vue-cli -g

初始化项目

用命令行进入到一个非系统盘的目录中  初始化项目
vue init webpack projectname

注意:项目名称不能包含中文和空格
初始化项目步骤:
(1)先执行初始化命令,回车
(2)Project name (myapp) ?
确认项目名称,不需要改,则回车
(3)Project Description (A vue.js project)
项目描述,不需要改,则回车
(4)Author 
项目作者,不需要改,则回车
(5)Vue build (Use arrow keys)
项目运行模式
Runtime + Compiler 运行时编译 回车
(6)Install vue-router? (Y/n)
是否安装路由,暂时不安装,输入 n 回车
(7) Use ESLint to lint your code? (Y/n)
是否使用eslint验证代码格式,暂时不需要,输入n,回车
(8) Set up unit tests (Y/n) 
是否创建单元测试,暂时不需要,输入n,回车
(9)Setup e2e tests with Nightwatch? (Y/n)
是否用端对端测试,暂时不需要,输入n,回车
(10)Should we run `npm install` for you after the project has been created?
项目初始化方式,选择npm

运行项目

在命令行中进入到项目根目录下,然后执行
npm run dev

项目目录结构

project 项目根目录
​    build        项目打包依赖目录
​    config        项目配置文件目录
​    node_modules 项目依赖目录
​    src                项目源码目录(项目的主战场)
​        assets    项目静态资源目录
​        components    项目自定义组件
​        App.vue        项目根组件
​        main.js        项目启动文件
​    static        存放通过域名直接访问的静态资源
​    .babelrc        es6语法解析配置
​    .editorconfig    编辑器配置文件
​    .gitignore    git忽略文件配置
​    .postcssrc.js    postcss配置文件
​    index.html    项目首页
​    package.json    项目依赖配置文件

项目执行顺序

index.html
/src/main.js
/src/App.vue
/src/components/HelloWorld.vue

vue组件构成

(1)模板 template 【必须】
(2)js script
(3)css style

devtools

devtools是vue全家桶中的一个浏览器插件,作用是让我们开发人员能够非常清楚的了解项目的组件结构和数据及状态 
安装:访问谷歌商店,搜索vue关键词==>Vue.js devtools插件 添加扩展即可
使用:打开vue项目页面后,然后打开浏览器的调试工具,就会看到一个vue的选项点击vue选项后,看到当前vue项目的组件结构及每一个组件上的数据

scoped

为了防止样式污染,可以给组件的style标签设置一个scpoed属性,来限制样式仅在当前组件起作用
使用:<style scoped></style>

组件通信
父子组件-props验证

通过自定义属性和props来实现

只是在控制台中给出警告,但不影响程序的运行

(1)在父组件中使用子组件时,通过自定义属性来传递数据 
<v-child imgurl="./1.jpg" title="学习群" desc="组团学习高校"></v-child>
 
(2)在子组件中用过props来接收父组件传递的数据
<script>
export default {
    props:['imgurl','title','desc']
}
</script>

(1)验证数据类型

在子组件中,接收数据时,props写成一个对象
//写法一:(如果只验证数据类型)
props:{
	要接收的参数名:类型名称
}
//写法二:(不仅仅验证数据类型,还可以做其他的验证)
props:{
	要接收的参数名:{
		type:类型名称
	}
}
 
支持的数据类型:
    Boolean、String、Number、Function、Object、Array、Symbol

(2)验证必填 (不要和默认值同时使用)

props:{
	要接收的参数名:{
		type:类型名称,
		required:true//设置此属性为必填项
	}
}

(3)默认值 (不要和必填项同时使用)

如果父组件在使用子组件时,没有传递子组件需要的数据时,会使用默认值来代替
//写法1
props:{
	要接收的参数名:{
		type:'',
		default:'默认值'
	}
}
//写法2
props:{
	要接收的参数名:{
		type:'',
		default:function(){
			return '默认值'
		}
	}
}

(4)自定义验证规则

props:{
	要接收的参数名:{
		type:'',
		validator:function(形参){
			验证规则逻辑
			return 布尔值
		}
	}
}

只要父组件给子组件传递了要验证的参数时,validator中的验证规则函数会自动触发
如果规则函数返回true,表示验证成功
如果规则函数返回false,表示验证失败(就会在控制台中给出验证失败的警告)

子父组件

数据是由父组件传递给子组件,如果子组件想要改变数据,则要通过父组件传递一个自定义事件,然后在子组件中触发这个事件来实现数据的改变。
通过自定义事件和$emit实现

(1) 父组件在使用子组件时,传递一个自定义事件

<v-news 
    v-for="(item,index) of newsarr" 
    :key="index" 
    :title="item.title" 
    :count="item.count"
    :index="index"
    @addcount="add"
></v-news>
 
//addcount就是一个自定义事件,对应一个父组件级别的函数

(2)子组件通过自身的methods自定义函数通过this.$emit来触发父组件传递的这个自定义事件

<template>
	<div>
		<button @click="viewnews(index)">访问</button>
	</div>
</template>
<script>
export default {
    props:['title','count','index'],
    methods:{
        viewnews(i){
            //$emit 触发父组件传递过来的自定义事件
            // 第一个参数是要触发的事件名称
            // 第二个参数是要传递的参数
            this.$emit("addcount",i)
        }
    }
}
</script>

非父子组件

通过eventbus和$on实现

(1)创建一个公用的容器,用来进行数据的发送和接收

在src/main.js 中,vue实例化之前,给vue的原型上增加一个公用的容器
Vue.prototype.容器名称 = new Vue();
new Vue({...})
 
容器名称:可以为 $but 等自己定义

(2)$emit

在数据发送的组件中,通过公用容器中的$emit方法来触发一个函数,并发送数据
methods:{//自定义函数
	sendData(形参){
	this.容器名称.$emit("事件名称","要传递的数据")
	}
}

(3)$on

在其他任意组件,通过公用容器中的$on方法来监听事件名称,用来接收发送端传递的数据
mounted(){//钩子函数 挂载完成
	this.容器名称.$on('事件名称',(形参)=>{
		...
	})
}

组件进阶
is属性

(1)改变html标签的默认结构约束

<table>
    <tr is="h3">这是一个标题</tr>
    <tr is="my-child"></tr>
</table>

(2)动态组件

需要结合非父子组件通信和is属性,来实现页面上,展示不同的组件内容

①在左侧菜单组件中,进行数据的展示和点击事件的编写

<template>
  <div class="menu">
    <ul>
      <li 
        v-for="(menu, index) of menus" :key="index"
        @click="sendtag(menu.tagname)"
    >{{ menu.title }}</li>
    </ul>
  </div>
</template>
<script>
export default {
  data() {
    return {
      menus: [
        {
          title: "系统设置",
          tagname: "setting"  //需要和import 设置的值对应
        },
        {
          title: "用户管理",
          tagname: "user"
        },
        {
          title: "个人中心",
          tagname: "profile"
        }
      ]
    };
  },
  methods:{
      sendtag(t){
        this.$bus.$emit('changeTag',t)
      }
  }
};
</script>
<style lang="css">
.menu ul {
  text-align: center;
}
.menu ul li {
  padding: 10px;
}
</style>

②在content组件中,引入所有需要展示的页面组件,并注册,然后通过事件监听来接收数据

<template>
    <div class="content">
        <!-- <setting></setting>
        <user></user>
        <profile></profile> -->
        <!-- 动态组件 -->
        <table :is="tagname"></table>
    </div>
</template>
<script>
import setting from './Setting'
import user from './User'
import profile from './Profile'
export default {
    data(){
        return{
            tagname:'setting'
        }
    },
    components:{
        setting,user,profile
    },
    mounted(){//挂载完成
        this.$bus.$on('changeTag',(tag)=>{
            this.tagname = tag;
        })
    }
}
</script>

ref属性

vue中一般不直接操作DOM结构,如果必须要进行DOM结构操作,可以给标签或者组件添加ref属性

普通字符串

<h3 ref="myh3">XXX管理系统</h3>
<script>
	mounted(){//钩子函数 挂载完成 操作DOM
    	this.$refs.myh3.innerHTML = '小小后台管理系统'
    }
</script>

数组

如果在v-for的标签上,设置了ref属性,那么通过$refs获取到的DOM节点的数组
<li v-for ref="myli"></li>
<script>
	mounted(){
    	console.log(this.$refs.myli)
    }
</script>

自定义组件

ref更多的应用在自定义组件上,可以实现父子组件通信的效果

注意:
ref只能在组件挂载完成后使用

jquery组件

安装:
1、直接引入jquery.js文件-->在项目根目录的index.html文件中
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
2、npm安装
npm i jquery --save
在vue组件中使用jquery时进行引入
import $ from 'jquery'
使用:$(".box").slideUp(1000)
把jquery挂载到原型上(/src/main.js上)
    import jq from 'jquery'
    Vue.prototype.$ = jq
    new Vue({...})
    
把jquery挂到原型上之后,在任意的组件里都可以通过this.$来使用jquery

bootstrap组件

安装:
1、直接引入bootstrap.css文件-->在项目根目录的index.html文件中
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap-grid.css" rel="stylesheet">
2、npm安装
npm i bootstrap --save
在vue组件中使用bootstrap时进行引入 (/src/main.js上)
import 'bootstrap/dist/css/bootstrap.css'
使用:<button class="btn btn-danger">测试按钮</button>

引入成功后,就可以正常使用

插槽

如果需要在父组件使用子组件时,来分发一些不同的内容展示在子组件中,可以使用插槽来实现

slot

(1)匿名插槽

是在子组件中,写入一个<slot>标签,设置一个插槽的位置。
这样,在父组件使用子组件时,就可以在子组件内写入内容,并展示在子组件的插槽处。

父组件:
<v-index>
    <p>父组件的标题</p>
    <p>父组件的内容</p>
</v-index>
 
子组件:
<div class="index">
    <h1>index组件</h1>
    <!-- 插槽(匿名) -->
    <slot></slot>
</div>

(2)具名插槽

如果子组件中有多个插槽,来展示不同的内容,用匿名插槽就会出现内容重复的现象。
为了区分不同的插槽,可以给slot来设置一个name属性来区分它们。

子组件:
<div class="index">
    <!-- 插槽(具名) -->
    <slot name="top"></slot>
    <h1>index组件</h1>
    <slot name="bottom"></slot>
</div>
 
父组件:
<v-index>
    <p slot="top">父组件的标题</p>
    <p slot="bottom">父组件的内容</p>
</v-index>

(3)作用域插槽

在使用插槽的时候,希望父组件可以控制插槽的结构和内容,子组件只做遍历循环
子组件的一部分DOM结构由外部传

子组件:
<template>
    <div>
        <p>child组件</p>
        <ul>
            <slot name="default" v-for="item of arr" :childitem="item"></slot>
        </ul>
    </div>
</template>
<script>
export default {
    data(){
        return{
            arr:[
                'web大前端',
                'java开发',
                'ui设计',
                '软件测试'
            ]
        }
    }
}
</script>
 
父组件:
<template>
  <div class="app">
      <v-child>
            <template v-slot:default="props">
                <li>{{ props.childitem }}</li>
            </template>
      </v-child>
  </div>
</template>
<script>
import vChild from './components/Child'
export default {
    components:{
        vChild
    }
};
</script>
<style>
</style>

路由【重点】

让用户能够根据不同的浏览器地址来展示不同的页面组件

SPA:single page application 单页面应用    
概念:就是只有一个Web页面的应用,是加载单个HTML页面,并在用户与应用程序交互时动态更新该页面的Web应用程序。
与传统多页面程序的区别:
    传统多页面程序:每次请求服务器返回的都是一个完整的页面
    单页应用程序:只有第一次会加载页面, 以后的每次请求, 仅仅是获取必要的数据.然后, 由页面中js解析获取的数据, 展示在页面中
优势:
    1 减少了请求体积,加快页面响应速度,降低了对服务器的压力
    2 更好的用户体验,让用户在web app感受native app的流畅
概念:
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:
​    嵌套的路由/视图表
​    模块化的、基于组件的路由配置
​    路由参数、查询、通配符
​    基于 Vue.js 过渡系统的视图过渡效果
​    细粒度的导航控制
​    带有自动激活的 CSS class 的链接
​    HTML5 历史模式或 hash 模式,在 IE9 中自动降级
​    自定义的滚动条行为

基本使用

(1)在进行初始化项目时,选择安装路由

(2)手动安装

1安装vue-router插件
    npm i vue-router --save

2在vue项目中引入vue-router,并实例化后挂载到vue的配置选项上 /src/main.js
    import Router from 'vue-router'
    Vue.use(Router)
    //实例化vue-router 路由管理器
    let router = new Router({
        routes:[]//路由配置映射表
    })
    new Vue({
        el: '#app',
        components: { App },
        router,
        template: '<App/>'
    })

3创建几个页面组件
    .....

4配置路由映射表规则
    let router = new Router({
        //路由配置映射表
        routes:[
            {
                path:'/index',//path是浏览器地址中的关键词
                component:Index//与关键词匹配的页面组件
            },
            {
                path:'/login',
                component:Login
            }
        ], 

         mode:"history"//去除 #/
    })

5在App.vue中添加路由出口组件
    内置组件  router-view

作用:
​    根据浏览器地址中的关键词和路由映射表中的关键词匹配时
​    会把匹配到的组件模板内容展示到此处

重定向redirect

当某个路由规则没有匹配到时,我们可以设置一个默认的页面(首页/404页面) 

/src/router/index.js文件中
export default new Router({
    routes: [
        {
            path:'/index',component:Index
        },
        {
            path:'/login',component:Login
        },
        {
            path:'*',
            redirect:'/index'
            // component:Login//这个不是重定向,只是在没有匹配到路由规则时,默认展示的组件内容
        }
    ]
})
 
注意:如果需要让浏览器地址进行变化,那么必须使用redirect属性,不能使用component

路由导航

能够让访问网站的用户,快速的访问到指定的页面

内置标签 router-link

必要属性:to,具体的某一个路由规则path属性的值
​    选填项:
​    active-class,模糊匹配,可以设置激活状态router-link的样式
​    exact-active-class,精确匹配,可以设置激活状态router-link的样式

<router-link to="/index">首页</router-link>
<router-link to="/login">登录</router-link>
<!-- 路由出口/可变区 -->
<router-view />

编程式导航

在vue-router路由中,内置了一些方法也可以实现页面跳转的效果,可以自定义业务逻辑后再进行页面的跳转。

1、push
push方法,会在执行页面跳转之前,把当前访问的页面链接地址进行记录,然后再跳转。
this.$router.push('path属性的值')
 
2、replace
replace方法,会在执行页面跳转之前,把要跳转的链接地址覆盖当前访问的链接地址。
this.$router.replace('path属性的值')
 
3、go
用于返回已访问过的页面地址,一般写-1,表示回退到上一个页面
this.$router.go(-1)

路由嵌套

如果某个页面中还有不同的子级页面要展示,可以使用路由嵌套来实现。

(1)创建页面组件
    此处省略...
(2)定义路由规则
    在一级路由规则中,通过children属性,来设置它的子级路由规则

export default new Router({
    routes: [
        {
            path:'/index',
            component:Index,
            children:[
                {
                    path:'movie',
                    component:Movie
                },
                {
                    path:'music',
                    component:Music
                }
            ]
        }
        ...
    ]
});
 
注意:子级路由规则的path属性不需要加斜杠

(3)在一级路由规则对应的页面中放入路由出口,用来展示匹配条件的子级路由页面

<template>
    <div>
        <h1>首页</h1>
        <!-- <button @click="$router.replace('/user')">跳转到用户</button>
        <button @click="$router.go(-1)">返回</button> -->
        <router-link to="/index/movie">电影</router-link>
        <router-link to="/index/music">音乐</router-link>
        <router-view class="box"/>
    </div>
</template>
<style lang="css">
    .box{
        border:1px solid blue;
    }
</style>
 
注意:在子级路由的导航组件中,path属性必须包含一级路由规则/二级路由规则

路由传参
动态路由

不同的路由规则,匹配到同一个页面上,这种路由规则叫做动态路由。

设置路由规则
{ path:'user/:变量名',component:组件}
参数名前一定要有冒号,冒号后面的是一个可以变化的字符串、数字的组合。

跳转到动态路由地址
this.$router.push('/index/user/'+uid)
                    /index/user/123
                    
获取动态路由中的参数
$route.params.变量名

查询参数

在要传递的参数数量不确定时,使用动态路由的方式传递参数就不合适
我们可以使用查询参数方式进行数据的传递

定义路由规则

{ path:'article/info',component:ArticleInfo }

注意:不需要添加冒号来告知路由规则后续要跟动态参数

传递参数

在表格页面通过点击编辑按钮,实现路由跳转并传递参数
toInfo(obj){
    this.$router.push({
        path:'/index/article/info',
        query:{id:obj.id,title:obj.title}
    })
}
 
路由跳转后,链接地址会以下面的方式进行自动拼接:
query的数据数据类型是一个对象,对象的元素与元素之间
?key=value&key1=value...

获取路由参数

this.$route.query.id
this.$route.query.title

路由进阶
路由命名

(1)设置路由规则时,添加一个name属性,给这个路由规则设置一个名称(不能重复)
{ 
    path:'user/:uid',
    component:UserInfo,
    name:'user'
}
 
(2)在页面组件中跳转路由规则时,不用按照以前的手动补全一级路由、二级路由和参数的方式
toInfo(uid){
    // this.$router.push('/index/user/'+uid)
    this.$router.push({
        name:'user',
        params:{uid}
    })
}

路由别名

可以给已有的路由规则,设置一个其他的名称,此时,原有的路由规则和别名都可以正常的访问到指定的页面组件。
{ 
    path:'/login',
    component:Login,
    alias:'/denglu' 
}
此时访问/login和/denglu,看到的是同一个页面组件。

路由模式

vue-router中有两种路由模式:
一种是hash(默认)【浏览器地址中有#号】
​    hash虽然会出现在url地址,但是不会包含在http请求中,不会重新加载页面
另一种是history模式【浏览器地址中没有#号】
​    利用html5 histroy接口中的方法实现页面跳转,history部署到服务器上后,会出现404,需要后端配合进行配置。

在/src/router/index.js进行设置路由模式
export default new Router({
    // mode:'hash',
    mode:'history',
    routes:[...]
})

路由守卫【*】

路由守卫的作用是让指定的路由规则满足一定的条件时,才能够被访问,可以实现请求拦截的功能。
根据路由守卫的作用范围不同,分成以下三种:
- 全局守卫
- 组件守卫
- 路由独享守卫

全局守卫(/router/index.js中编写)

1、全局前置守卫钩子函数
是当前项目中所有的路由规则被访问之前,可以进行一定的验证和拦截操作
router.beforeEach((to,from,next)=>{
	//to	目标路由地址  对象
	//from  来源路由地址  对象
	//next 	函数,用来执行或者改变默认的路由规则  函数
})
 
2、全局后置守卫钩子函数
是当前项目中所有的路由规则被访问之后,没有验证和拦截的功能,一般就做记录
router.afterEach((to,from)=>{
    console.log(to,'to')
    console.log(from,'from')
})

组件守卫

(1)beforeRouteEnter  写在具体组件中
在此钩子函数中不可以获取到this
//当前组件的路由规则访问之前
beforeRouteEnter(to,from,next){
 let userinfo = localStorage.getItem('user')
 if(userinfo){
	next();//用户已登录,则执行默认的路由规则
 }else{
	next('/login')//用户未登录,则指向到登录的路由规则
 }
}
 
(2)beforeRouteLeave  写在具体组件中
在此钩子函数中可以获取到this
beforeRouteLeave(to, from, next) {
 console.log(this)
 console.log(to, 1111);
 console.log(from, 2222);
 console.log(next, 3333);
 next();
}
 
(3)beforeRouteUpdate  写在具体组件中
在此钩子函数中可以获取到this
// 动态路由的参数值发生变化时,会自动执行
beforeRouteUpdate(to, from, next) {
console.log(to, 1111);
console.log(from, 2222);
console.log(next, 3333);
}

路由独享守卫

写在路由映射表配置规则中
{ 
    path:'user/:uid',
    component:UserInfo,
    name:'yonghu',
    beforeEnter:function(to,from,next){
        console.log(to,1111)
        console.log(from,2222)
        console.log(next,3333)
        next()
    }
}

错误总结

路由模式
当路由模式是history时,组件守卫的beforeRouteUpdate不会执行,这个钩子函数只有在hash才会在满足条件的情况下自动执行。
router-link和编程式导航的区别
router-link会生成a标签,而编程式导航是我们自行编写的程序代码
所以,编程式导航中的代码会无条件的执行,当要跳转的路由地址就是当前访问的地址时,会出现一个错误产生。
Uncaught (in promise) Error: Avoided redundant navigation to current location: "/index/user".

项目目录结构优化

为了提升项目的可维护性和可扩展性,我们把src/components目录下的组件进行目录结构的优化。
把所有可以通过路由地址访问到的页面组件都放到一个文件夹中,比如这个文件夹叫做“pages”
把所有页面组成部分的组件(不通过路由地址访问的组件)都放到一个文件夹中,比如这个文件夹叫做“views”
以上文件夹的名称没有强制要求必须叫这些名字,可以自行设置。

调整之后的目录结构为:
project    项目名称
    -src
        -components
            -pages    存放页面组件
                ...    其他目录可以根据需求自行设置
            -views    存放页面组成部分的组件

注意:一旦组件目录结构调整了之后,在引用该组件的地方也要跟着把引入的路径地址进行改变才行。

stylus 样式预处理器

富于表现力、动态的、健壮的 CSS
特性:
冒号可有可无    分号可有可无    逗号可有可无    括号可有可无
变量    插值(Interpolation)   混合(Mixin)   数学计算
强制类型转换   动态引入   条件表达式    迭代     嵌套选择器
父级引用        Variable function calls      词法作用域
内置函数(超过 60 个)  语法内函数(In-language functions)
压缩可选    图像内联可选    Stylus 可执行程序    健壮的错误报告
单行和多行注释   CSS 字面量    字符转义   TextMate 捆绑

安装

npm i stylus stylus-loader --save

引入

是在页面组件中的style标签中添加一个lang属性(默认值是css),并把它的属性值明确的设置为stylus。
<style lang="stylus" ></style>

基本使用

<template>
    <div class="mask">
        <div class="content">
            <h1>登录页面</h1>
            <div class="item">
                <input type="text" placeholder="请输入用户名">
            </div>
            <div class="item">
                <input type="password" placeholder="请输入密码">
            </div>
            <div class="item">
                <button>登录</button>
            </div>
        </div>
        </div>
</template>
<script>
export default {}
</script>
<style lang="stylus" scoped>
    //vw view width 可视区域的宽度
    //vh view height 可视区域的高度
    .mask
        width 100vw
        height 100vh
        background rgba(0,0,0,0.5)
        position fixed
        left 0
        bottom 0
        .content
            width 400px
            height 300px
            background-color #fff
            transform translateY(50%)
            margin 0 auto
            border-radius 20px
            text-align center
            h1
                padding-top 10px
            .item
                padding:10px
                input
                    height 30px
                    width 300px
                    line-height 30px
                    font-size 20px
                button
                    width 300px
                    height 40px
                    background-color skyblue
                    border none
                    color #fff
                    font-size 20px
</style>

函数的使用

如果有不同的页面要使用相同的样式代码,我们可以在stylus中封装一个函数,把重复使用的样式代码放到函数中,然后在页面组件中引入函数即可使用。

1、src/common/css/fn.styl 
 
mask(){
    width 100vw
    height 100vh
    background rgba(0,0,0,0.5)
    position fixed
    left 0
    bottom 0
}
 
2、在页面组件中引入
<style lang="stylus" scoped>
@import '../../common/css/fn.styl'
	.mask
		mask()
</style>

变量的使用

可以预先设置好一些初始的样式信息,包括颜色、尺寸、字体、表格、表单

/src/common/color.styl
 
    $bgColor1 = #f47a55
    $bgColor2 = #f15a22
    $bgColor3 = #ae5039
 
在页面组件中只要引入相关的.styl文件既可以使用预先设置好的变量信息
<style lang="stylus" scoped>
@import "../../common/css/color.styl"
	.nav
        width $navWidth
        background-color $bgColor2
</style>

状态管理-vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用==集中式==存储管理应用的==所有组件==的状态,并以相应的规则保证状态以一种==可预测==的方式发生变化。

安装

npm i vuex --save

核心
state

state是vuex仓库中所有的状态,类似vue组件中的data

import Vuex from 'vuex'//引入
Vue.use(Vuex)
let store = new Vuex.Store({ //实例化
    state: {
        num: 100
    }
})
new Vue({
	...
	store:store//挂载到vue上
})
 
在任意组件中读取仓库中的数据
<p>当前仓库中的数据量是:{{ $store.state.num }}</p>

mutations

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation,类似vue组件中的methods

let store = new Vuex.Store({
    state: {
        num: 100
    },
    mutations:{
        addNumByOne(state){
            state.num++
        },
        addNumByNum(state,num){
            state.num+=num
        }
    }
})
 
在页面组件中使用mutations
<button @click="$store.commit('addNumByOne')">添加1</button>
<button @click="$store.commit('addNumByNum',5)">添加N</button>
 
注意:mutation必须是一个同步函数,不能执行异步操作

actions

Action 类似于 mutation,不同在于:
    Action 提交的是 mutation,而不是直接变更状态。
    Action 可以包含任意异步操作。
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作

let store = new Vuex.Store({
    ...
    actions:{
        addNumByOneSync(context){
            setTimeout(()=>{
                context.commit('addNumByOne')
            },1000)
        },
        addNumByNumSync(context,n){
            context.commit('addNumByNum',n)
        }
    }
})
 
在页面组件中使用:
<button @click="$store.dispatch('addNumByOneSync')">添加1--action</button>
<button @click="$store.dispatch('addNumByNumSync',5)">添加N--action</button>

getters

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

let store = new Vuex.Store({
	...
	getters:{
		showNum(state){
            //业务逻辑
            return `最新的数量是:${state.num}`;
        }
	}
});
 
在组件中使用计算属性
<p>{{ $store.getters.showNum }}</p>

module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)

1、设置模块中的state和mutation
export default new Vuex.Store({
    ...
    modules:{
        shop:{
            namespaced:true,//启用命名空间
            state:{
                num:1
            },
            mutations:{
                addNumByOne(state){
                    state.num++
                }
            }
        }
    }
});
shop是模块名称,可以自行设置。
	namespaced:true 启用命名空间
 
2、在页面中使用shop模块下的state和mutation
<template>
	<div>
		<h1>shop模块</h1>
        <p>shop模块中的num值为:{{ $store.state.shop.num }}</p>
        <button @click="handlerAdd">改变num</button>
    </div>
</template>
<script>
import {mapState} from 'vuex'
export default {
	computed:{
		...mapState('shop',['num']),//默认是更模块 可以增加模块名
	},
	data(){...},
    methods:{
        handlerAdd(){
            // this.$store.commit('addNumByOne')//调用根模块下的mutation
            this.$store.commit('shop/addNumByOne')//调用shop模块下的mutation
        }
    }
}

助手函数
mapState

当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余

在页面组件中:
import { mapState } from 'vuex' 
export default {
    computed:{
        ...mapState(['num'])
    }
}

mapGetters

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性

在页面组件中:
import { mapState,mapGetters } from 'vuex' 
export default {
    computed:{
        ...mapState(['num']),
        ...mapGetters(['showNum'])
    }
}

mapActions

使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store)

在页面组件中:
<template>
    <div>
        <h2>数量减少操作</h2>
        <p>当前仓库中的数据量是:{{ num }}</p>
        <p>{{ showNum }}</p>
        <button @click="addNumByNumSync(5)">action+N</button>
    </div>
</template>
<script>
import { mapState,mapGetters,mapActions } from 'vuex' 
export default {
    ...,
    methods:{
        ...mapActions(['addNumByNumSync']),
    }
}

mapMutations

你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)

在页面组件中:
<script>
import { mapState,mapGetters,mapActions,mapMutations } from 'vuex' 
export default {
    ...
    methods:{
        ...,
        ...mapMutations(['addNumByOne'])
    }
}
</script>

目录结构优化

把代码都写在main.js中是非常不明智且不合理的,因为一旦仓库中的状态非常多时,对应的代码量也会变的非常多。
 
(1)我们在src目录下创建一个store文件夹(名称可以自定义),在store目录下再创建一个index.js
 
(2)然后就可以把之前写在main.js中关于vuex的代码都可以放到/src/store/index.js中
 
但是,所有的代码都放在index.js中,也是不合适的,因为状态和改变状态的方法会有很多
 
所以在此基础上继续进行目录结构的细分,把state、mutations、actions、getters对应的代码分别拆分到对应的js文件中,然后在/src/store/index.js中引入这些文件即可。
优化之后的代码:
 
/src/main.js
import store from './store'
new Vue({
    el: '#app',
    router,
    store:store,//一定要把仓库挂到vue实例上
    components: { App },
    template: '<App/>'
})
 
/src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
//引入状态
import state from './state'
//引入修改状态的方法
import mutations from './mutation'
//引入异步操作mutation的方法
import actions from './action'
//引入计算属性
import getters from './getter'
//实例化vuex仓库
export default new Vuex.Store({
    state:state,//key和val相同时,state:state可以简写成state
    mutations,
    actions,
    getters
});
 
/src/store/state.js
export default{
    num: 100
}
 
/src/store/mutation.js
export default{
    addNumByOne(state){
        state.num++
    },
    addNumByNum(state,num){
        state.num+=num
    }
}
 
/src/store/action.js
export default{
    addNumByOneSync(context){
        setTimeout(()=>{
            context.commit('addNumByOne')
        },1000)
    },
    addNumByNumSync(context,n){
        context.commit('addNumByNum',n)
    }
}
 
/src/store/getter.js
export default {
    showNum(state){
        //业务逻辑
        return `最新的数量是:${state.num}`;
    }
}

购物车案例
业务流程

商品列表页–展示商品

<template>
    <div>
        <h1>商品列表页面</h1>
        <div class="list">
            <div
                class="item"
                v-for="good of goodsArr"
                :key="good.id"
                @click="toInfo(good.id)"
            >
                <div class="left">
                    <p>商品名称:{{ good.name }}</p>
                    <p>商品价格:{{ good.price }}</p>
                </div>
                <div class="right">
                    <img :src="good.img" :alt="good.name" />
                </div>
            </div>
        </div>
    </div>
</template>
<script>
export default {
	methods: {
        toInfo(id) {
            this.$router.push("/goods/" + id);
        }
    }
};
</script>

商品详情页–用来展示商品的具体信息,方便用户把商品加入购物车

<template>
    <div>
        <h1>商品详情页面</h1>
        <p>商品名称:{{ info.name }}</p>
        <p>商品价格:{{ info.price }}</p>
        <img :src="info.img" />
        <button @click="addCart">加入购物车</button>
    </div>
</template>
<script>
export default {
    mounted() {
        let id = this.$route.params.gid;
        this.info = this.goodsArr.find(item => item.id == id);
    },
    methods: {
    	//点击加入购物车按钮
        addCart() {
            let info = this.info;
            info.num = 1;
            info.ischeck = true;
            //触发vuex中的action
            this.$store.dispatch("addCartGoodsSync", info);
            this.$router.push("/cart");
        }
    }
}
</script>

初始化vuex状态,并定义好改变状态的方法

/src/store/state.js 定义初始状态
export default{
    num: 100,
    cartGoods:[]//定义一个购物车空数组
}
 
/src/store/mutation.js  定义直接改变状态的方法--只能同步操作
export default{
    addCartGoods(state,obj){
        let idx = state.cartGoods.findIndex((item)=>item.id == obj.id);
        if(idx >= 0){
            //在数组中已经存在某个商品
            state.cartGoods[idx].num++;
        }else{
            state.cartGoods.push(obj)
        }
    }
}
 
/src/store/action.js 定义触发mutations的action--可以执行异步操作
export default{
    addCartGoodsSync(context,obj){
        context.commit('addCartGoods',obj)
    }
}
 
/src/store/getter.js 定义计算属性,方便页面去获取
export default {
    getCartGoods(state){
        return state.cartGoods;
    }
}

购物车页面,通过计算属性来获取到已经加入到购物车中的商品信息

<script>
import {mapGetters} from 'vuex'
export default {
    computed:{
        ...mapGetters(['getCartGoods'])
    }
}
</script>

ui库
element-ui

Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库

类似的ui库还有iview、mint-ui

安装

npm i element-ui --save

引入

完整引入
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';//非常重要
Vue.use(ElementUI);
new Vue({
  ...
  render: h => h(App)
});

按需引入---需要哪个组件就引入哪个组件
import { Button, Select } from 'element-ui';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
或写为
Vue.use(Button)
Vue.use(Select)

new Vue({
  el: '#app',
  render: h => h(App)
});

vue中数据变化后页面不刷新

在vue项目,受对象数据类型的影响,有时直接通过下标操作数组,数组内容变化了,但是页面并没有跟着进行重新渲染

解决办法一

可以通过JSON序列化来实现
vuex的mutations
 
addCartNum(state,id){
	let idx = state.cartGoods.findIndex((item)=>item.id == id);
    let goodsArr = JSON.parse(JSON.stringify(state.cartGoods));
    goodsArr[idx].num++;
    state.cartGoods = goodsArr;
}

解决办法二

可以使用vue提供的$forceUpdate方法
1、vuex的actions
 
addCartNumSync(context,id){
	context.commit('addCartNum',id)
}
 
2、vuex的mutations
 
addCartNum(state,id){
    let idx = state.cartGoods.findIndex((item)=>item.id == id);
    state.cartGoods[idx].num++;
}
 
3、页面组件代码
 
methods:{
    ...mapActions(['addCartNumSync']),
    add(id){
        this.addCartNumSync(id);
        //调用完成vuex中的actions操作方法对数据进行改变后
        //强制重新渲染页面,触发update生命周期钩子函数
        this.$forceUpdate();
    }
}

项目页面准备
表单组件

element-ui中提供了表单中常用的组件,比如输入框、选择框、单选框、开关等

<el-form label-width="80px"  style="width:600px;" >
    <el-form-item label="菜单名称">
    	<el-input v-model="info.title"></el-input>
    </el-form-item>
    <el-form-item label="上级菜单">
    <el-select v-model="info.pid" placeholder="请选择">
        <!-- 
            value 	设置选中项的值
            label   设置选中项的名称
        -->
        <el-option value="">请选择</el-option>
        <el-option value="0" label="顶级菜单">顶级菜单</el-option>
        <el-option value="1" label="系统设置">系统设置</el-option>
    </el-select>
    </el-form-item>
    <el-form-item label="菜单图标">
    	<el-input v-model="info.icon"></el-input>
    </el-form-item>
    <el-form-item label="菜单地址">
    	<el-input v-model="info.address"></el-input>
    </el-form-item>
    <el-form-item label="状态">
    	<el-switch v-model="info.status"></el-switch>
    </el-form-item>
    <el-form-item>
    	<el-button type="primary">提交</el-button>
    </el-form-item>
</el-form>

其中,el-select组件中的el-option组件,如果不设置label和value属性的话,则默认把el-option中的内容当成值和label,但是实际项目中一般都不会直接把el-option中的内容进行传递,所以需要设置value属性和label属性,value属性用来控制传递的值,label属性用来控制匹配的值在option中显示的内容。

表单验证

element-ui的表单组件中内置了验证功能,可以防止数据的丢失

rules属性

需要给表单组件添加一个rules属性,用来告知表单具体的验证规则是什么

<el-form :rules="具体的验证规则名称">

验证规则

具体的验证规则需要写在data里来进行预定义 

required 设置元素必填
messeage设置元素不符合验证规则显示的文字内容
trigger 设置元素触发规则的机制
min 设置元素内容的最小长度
max设置元素内容的最大长度

<script>
export default {
    data(){
    	return{
    		验证规则名称:{
                要验证的字段名:[
                    { required:true,message:'菜单名称不能为空',trigger:'blur' },
                    { min:1,max:20,message:'菜单名称长度不符合要求' }
                ]
            }
    	}
    }
}
</script>

prop属性

给需要进行验证的表单元素设置一个prop属性,属性值要和在验证规则中设置的名称保持一致

<el-form-item label="展示的名称" prop="要验证的字段名">

model属性和ref属性

在进行表单验证时,需要给表单组件设置model属性,用来进行具体数据内容的验证

<el-form :model="要进行验证的数据对象" ref="表单自定义名称">

validate

在点击提交按钮时,需要执行表单组件内置的validate方法来实现表单内容的验证

<el-form-item>
	<el-button type="primary" @click="自定义方法('表单的ref属性值')">提交</el-button>
</el-form-item>
<script>
export default {
	...
	methods:{
        自定义方法(形参) {
            this.$refs[形参].validate((valid) => {
                if (valid) {
                    //验证规则满足时,才执行数据添加操作
                }
            });
        }
    }
}
</script>
 
//====示例代码
<template>
    <div>
        <h1>菜单信息页</h1>
        <!-- el-form验证时使用的属性
                rules   表单的验证规则
                model   表单验证时使用的数据
         -->
        <el-form 
            label-width="80px" 
            style="width:600px;"
            :rules="rules"
            :model="info"
            ref="menuForm"
        >
            <el-form-item label="菜单名称" prop="title">
                <el-input v-model="info.title"></el-input>
            </el-form-item>
            <el-form-item label="上级菜单" prop="pid">
                <el-select v-model="info.pid" placeholder="请选择">
                    <!-- 
                        value 
                        label   设置选中的选项名称
                     -->
                     <el-option value="">请选择</el-option>
                    <el-option value="0" label="顶级菜单">顶级菜单</el-option>
                    <el-option value="1" label="系统设置">系统设置</el-option>
                </el-select>
            </el-form-item>
            <el-form-item label="菜单图标">
                <el-input v-model="info.icon"></el-input>
            </el-form-item>
            <el-form-item label="菜单地址">
                <el-input v-model="info.address"></el-input>
            </el-form-item>
            <el-form-item label="状态">
                <el-switch v-model="info.status"></el-switch>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" @click="submitInfo('menuForm')">提交</el-button>
            </el-form-item>
        </el-form>
    </div>
</template>
 
<script>
export default {
    data(){
        return{
            info:{
                title:'',
                pid:'',
                icon:'',
                address:'',
                status:true
            },
            rules:{
                title:[
                    { required:true,message:'菜单名称不能为空',trigger:'blur' },
                    { min:1,max:20,message:'菜单名称长度不符合要求' }
                ],
                pid:[
                    { required:true,message:'请选择上级菜单' }
                ]
            }
        }
    },
    methods:{
        submitInfo(formName) {
            this.$refs[formName].validate((valid) => {
                if (valid) {
                    //验证规则满足时,才执行数据添加操作
                }
            });
        }
    }
}
</script>
 
<style scoped>
    .el-form{
        margin:20px;
    }
</style>

面包屑

显示当前页面的路径,快速返回之前的任意页面。
el-breadcrumb
el-breadcrumb-item

<el-breadcrumb separator=">">
    <el-breadcrumb-item :to="{path:'/home'}">首页</el-breadcrumb-item>
    <el-breadcrumb-item>
    	<a href="#/menu">菜单列表</a>
    </el-breadcrumb-item>
    <el-breadcrumb-item>菜单添加</el-breadcrumb-item>
</el-breadcrumb>

可以直接给el-breadcrumb-item组件设置to属性来进行页面的跳转,也可以在其中添加a标签/router-link标签来进行页面的跳转,如果不需要页面跳转,则直接写文字内容即可。

el-breadcrumb-item之间的分隔符默认是斜杠,可以通过separator属性来自行设置分隔符

NavMenu菜单导航

default-active 当前激活菜单的 index
router 是否使用 vue-router 的模式,启用该模式会在激活导航时以 index 作为 path 进行路由跳转
启用路由模式后,el-menu-item组件的index属性就是要跳转的路由地址,不需要再使用router-link标签也可以实现路由跳转。
但是dafault-active设置为固定值的话,再次刷新页面后,左侧菜单还是选中的固定值的菜单
所以需要把default-active的设置为一个变量

页面加载时

/src/components/views/Nav.vue

<script>
export default {
    data(){
        return{
            defaultActive:''
        }
    },
    mounted(){
        //页面加载时,控住左侧菜单选中效果
        //把当前路由中的meta的自定义属性赋值给默认选中变量
        this.defaultActive = this.$route.meta.select;
    }
}
</script>

由于在路由切换时,信息页面的路由地址并没有在左侧菜单中,可以通过路由的meta属性来自行设置选中哪个左侧菜单
/src/router/index.js

{
	path:'menu',  
	component:()=>import('../components/pages/Menu/Index'),  
	meta:{select:'/menu'}
},
{    
	path:'menu/add',    			
	component:()=>import('../components/pages/Menu/Info'), 
	meta:{select:'/menu'}
}

meta属性是路由信息中内置的一个属性,它的属性值类型为对象,在对象中自定义一个键值对用来告知左侧菜单应该选中哪个即可实现。

路由地址变化时

/src/components/views/Nav.vue

<el-menu
  :default-active="this.$route.meta.select" //第一种方式直接使用
  :default-active="defaultActive"  //第二种方式,使用data中数据监听
  router
  unique-opened
></el-menu>
 
<script>
export default {
  data(){
    return{
      // 第二种设置 当前激活菜单的 index 路由
      // defaultActive:'',
    }
  },
    mounted(){
        //页面加载时,控住左侧菜单选中效果
        //把当前路由中的meta的自定义属性赋值给默认选中变量
        console.log(this.$route)
        // this.defaultActive = this.$route.meta.select;
    },
    watch:{
        // 监听route的地址变化 并改变数据
        $route(newVal,oldVal){
            console.log('当前访问地址',newVal.meta.select,'\n','旧地址',oldVal.meta.select);
            // 监听路由地址的改变
            // this.defaultActive = newVal.meta.select;
        }
    }
}
</script>

网络请求

jquery-ajax、fetch、axios

axios

基于Promise的HTTP客户端,用于浏览器和node.js

安装

npm i axios --save

引用

import axios from 'axios'

基本使用
配置代理-跨域请求

Access to XMLHttpRequest at 'http://suggestion.baidu.com/su?cb=&wd=%E5%8C%97%E4%BA%AC' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

这个问题是因为没有设置代理,就进行了跨域请求

在vue项目中,可以配置webpack代理,实现跨域请求

项目根目录下/config/index.js

proxyTable: {
    '/关键词':{
        target:'http://suggestion.baidu.com',//目标域名地址
        changeOrigin:true,//允许跨域
        pathRewrite:{
        	'^/关键词':''
        }
    }
}
 
在proxyTable中	/关键词的作用是:当vue项目发起的请求中包含了指定的关键词时,就进行代理的转发
​	target	要转发的目标域名地址(只包含域名)
​	changeOrigin 允许跨域请求
​	pathRewrite 路径重写规则【选填】,当请求的接口地址中不包含指定的关键词时,需要设置
注意事项:	
①一旦配置好代理重写规则后,在具体的页面组件中发起网络请求时,不要写域名,只写/关键词/接口地址
②修改了配置文件后,一定要重启项目,才会重新加载配置文件

get请求

第一种写法:
axios.get('请求的地址').then(result=>{
	...
})
 
第二种写法:
	方式一
axios({
	url:'请求地址?参数名=参数值&参数名N=参数值N'
}).then(result=>{
	...
})
	方式二
axios({
	url:'请求地址',
	method:'get',//请求方式,默认就是get
	params:{key1:val1,keyN:valN}
}).then(result=>{
	...
})
 
post请求
	方式一
axios.post('请求的地址',要提交的数据).then(result=>{
	...
})
 
	方式二
axios({
	url:"请求地址",
	method:"post",//请求方式
	data:{key1:val1,keyN:valN}
}).then(result=>{
	...
})

接口项目
获取node_modules

拿到项目后,解压到一个文件夹中
在命令行中进入到这个解压后的文件夹里
执行命令
npm i

修改数据库配置文件

/接口项目根目录/config/global.js
exports.dbConfig = {
    host: '数据库地址', 
    user: '数据库用户名',
    password: '数据库用户名密码',
    port: 3306,
    database: '数据库名字'
}
 
通过navicat for mysql新建一个数据库,然后导入数据表结构即可

角色管理
角色添加

接口地址:/api/roleadd【post方式】

字段名 备注
rolename 角色名称
menus 角色权限,用逗号隔开的菜单编号
status 状态1正常2禁用

el-tree组件
<!-- 
    树形组件 
    data 要展示的数据
    props 默认属性
    show-checkbox 显示复选框
    default-expand-all 展开所有层级
    default-checked-keys 设置默认选中 数据类型是数组
-->
<el-tree 
    :data="menus" 
    :props="defaultProps"
    show-checkbox
    ref="tree"
    node-key="id"
    default-expand-all
    :default-checked-keys="defaultKeys"
></el-tree>

角色编辑和修改

获取角色信息
接口地址:
/api/roleinfo【get方式】
需要提供id参数

角色修改
接口地址:
/api/roleedit
提交提供id参数,和角色其他信息

<script>
	mounted(){
        if(this.$route.params.roleid){
            this.tip = '修改';
            //获取到当前路由地址编号对应的角色信息
            this.$axios({
                url:'/api/roleinfo',
                params:{id:this.$route.params.roleid}
            }).then(res=>{
                this.info = res.data.list;
                //处理和数据库中不一样的数据类型
                this.info.status = this.info.status == 1 ? true : false;
                //设置菜单默认选中--要把字符串变成数组
                this.defaultKeys = this.info.menus ? this.info.menus.split(',') : [];
            })
        }
        //获取已有的菜单信息
        this.$axios({
            url:'/api/menulist',
            params:{istree:1}
        }).then(result=>{
            this.menus = result.data.list;
        })
    },
    methods:{
        submitInfo(formName) {
            this.$refs[formName].validate((valid) => {
                if (valid) {
                    let data = JSON.parse(JSON.stringify(this.info));
                    let url = '/api/roleadd';
                    if(this.$route.params.roleid){
                        url = '/api/roleedit';
                        data.id = this.$route.params.roleid;//执行修改接口时的必要条件
                    }
                    //数据库中的status字段不是布尔值,所以要自行转换一下
                    data.status = data.status ? 1 : 2;
                    //getCheckedKeys el-tree组件获取勾选的菜单的node-key属性值
                    //join方法用来将数组元素用指定的内容分隔,转换成一个字符串
                    data.menus = this.$refs.tree.getCheckedKeys() ? this.$refs.tree.getCheckedKeys().join(',') : '';
                    //发起post请求,请求接口项目中的角色添加接口,完成数据的保存
                    this.$axios.post(url,data).then(res=>{
                        if(res.data.code == 200){
                            this.$router.push('/role')
                        }else{
                            alert(res.data.msg)
                        }
                    })
                }
            });
        }
    }
</script>

管理员管理

为了实现后台数据的安全,需要让指定有权限的用户访问系统后台并进行数据的管理,所以我们需要有管理员管理的功能模块。

管理员添加

接口地址:/api/useradd【post方式】

字段名 备注
roleid 角色编号
username 用户名
password 密码
status 状态1正常2禁用
管理员列表

接口地址:/api/userlist【get方式】

参数名 备注
size 每页展示的数据条数
page 页码数

分页功能:
element-ui组件
el-pagination
参数:
​        current-change 页码点击的事件
​        page-size 每页展示的数据条数
​        total  总数据条数
​        background 背景

<el-pagination
    background
    layout="prev, pager, next"
    :page-size="pagesize"
    :total="total"
    @current-change="(n)=>pageChange(n)"
>

接口代码调整,可以实现分页查询和排序

/show-api/routes/user.js

router.get("/userlist", async (req, res) => {
    const { size,page } = req['query'];
    let data = await Db.select(req, `SELECT a.*,b.rolename FROM ${tableName} a
        LEFT JOIN ${tableNameRole} b
        ON a.roleid = b.id
        ORDER BY a.id
        LIMIT ${(page-1)*size},${size}`);
    res.send(Success(data));
});

登录用户权限
左侧菜单

根据登录用户的角色,来动态的设置左侧菜单的内容

由于在登录成功后,接口会自动的返回用户信息、用户角色权限信息等,所以只需要从登录返回的数据中获取需要的菜单数据即可

本地存储版本:
/src/components/views/Nav.vue

methods:{
    getNavMenu(){
        // this.$axios({
        //     url:'/api/menulist',
        //     params:{istree:1}
        // }).then(result=>{
        //     this.navMenus = result.data.list;
        // })
        if(localStorage.getItem('htuser')){
            let info =  JSON.parse(localStorage.getItem('htuser'));
            this.navMenus = info.menus;
        }
    }
}

路由守卫验证

根据不同角色展示左侧菜单后,用户仍然可以通过路由地址进行访问没有分配的权限,所以为了进一步验证用户的权限,我们可以使用路由守卫来验证用户的权限

/src/router/index.js
 
let router = new Router({...})
router.beforeEach((to,from,next){
    //判断用户有没有登录
    let userinfo = localStorage.getItem('htuser') ? JSON.parse(localStorage.getItem('htuser')) : null;
    if(userinfo){
        //如果用户已登录,则执行默认路由规则
        // next();
        //根据用户角色已分配的菜单权限,来对用户当前访问的路由进行验证
        //menu ['/menu','/user']
        //把没有纳入到权限管理的路由地址允许用户访问
        userinfo.menus_url.push('/');
        userinfo.menus_url.push('/home');
        let menuarr = userinfo.menus_url;//允许访问的路由数组
        //遍历数组,找到匹配的元素
        let res = menuarr.find(d=>{
        	return d == to.path;
        });
        if(res){
        	next();
        }else{
        	next('/')
        }
    }else{
        //如果用户没有登录,跳转到登录页面
        if(to.path == '/login'){
            next();
        }else{
            next('/login')
        }
    }
})
export default router

商城管理

为了在前台能够展示商品相关的信息,所以要在后台先把商品相关的信息维护好

商品分类

商品分类添加
接口:/api/cateadd

参数 备注
pid 上级分类编号
catename 分类名称
img 分类图片
status 分类状态

elementui上传组件el-upload
参数:
​    action 上传的服务器地址
​    list-type  文件列表的类型
​    on-preview 图片预览时执行的函数
​    on-remove  图片删除时执行的函数
​    auto-upload 是否选择完文件后自动上传,默认是true
​    on-change  文件选择好之后或者上传完成、失败后执行的函数

<el-upload
    action="#"
    list-type="picture-card"
    :on-preview="handlePictureCardPreview"
    :on-remove="handleRemove"
    :auto-upload="false"
    :on-change="fileChange"
>
    <i class="el-icon-plus"></i>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
    <img width="100%" :src="dialogImageUrl" alt="">
</el-dialog>
<script>
export default {
    data(){
        return{
            ...
            dialogVisible:false,
            dialogImageUrl:'',
        }
    },
    methods:{
        handlePictureCardPreview(file){
            this.dialogImageUrl = file.url;
            this.dialogVisible = true;
        },
        handleRemove(){},
        fileChange(file){
            this.img = file.raw;
        }
    }
}
</script>

之前几个模块提交的数据都是对象类型,但是在分类添加中由于包含了文件类型的数据,所以不能再使用对象类型数据了,需要提交的是表单类型的数据,在JS中可以通过FormData方法来生成一个表单。

data(){
    return{
        info:{
            ...
        },
        img:''
    }
}
methods:{
    submitInfo(formName) {
        this.$refs[formName].validate((valid) => {
            if (valid) {
                //验证规则满足时,才执行数据添加操作
                let data = JSON.parse(JSON.stringify(this.info));
                //如果现在访问的是动态路由,则执行修改操作,否则执行添加操作
                let url = '/api/cateadd';
                if(this.$route.params.id){
                    url = '/api/cateedit';
                    data.id = this.$route.params.id;//执行修改接口时的必要条件
                }
                //数据库中的status字段不是布尔值,所以要自行转换一下
                data.status = data.status ? 1 : 2;
                //处理包含文件的表单
                let form = new FormData();
                for(let i in data){
                    form.append(i,data[i]);//遍历对象中的所有数据,并添加到表单中
                }
                if(this.img){
                   	form.append('img',this.img)
                }
                //发起post请求,请求接口项目中的商品分类添加接口,完成数据的保存
                this.$http.post(url,form).then(res=>{
                    if(res.data.code == 200){
                        this.$router.push('/category')
                    }else{
                        alert(res.data.msg)
                    }
                })
            }
        });
    }
}

商品分类列表
接口地址:/api/catelist
(3)商品分类删除
接口地址:/api/catedelete
(4)商品分类编辑和修改
①编辑
接口地址:/api/cateinfo
参数
id    分类编号
el-upload组件的file-list参数
file-list    用来设置图片列表信息,格式是一个数组[{ name:'图片名称',url:'图片地址' }]
⑤修改
接口地址:/api/cateedit
包括id和其他页面中的所有数据

接口权限验证

由于接口可以操作数据库中的所有数据,所以一般会给敏感操作的接口设置一定的权限,只有满足条件的用户才能够通过接口来操作数据。

修改接口项目代码

接口项目目录/app.js把第37行-47行的代码注释解开

app.use(async (req, res, next) => {
    if (!req.headers.authorization) {
        res.send(MError("请设置请求头,并携带验证字符串"));
    } else {
        if (!await checkToken(req)) { // 过期  
            res.send(Guest("登录已过期或访问权限受限"));
        } else {
            next();
        }
    }
});
 
此时,在请求接口时,如果不在请求的头信息中携带上authorization参数,则无法通过接口操作数据。

axios发起网络请求

axios发起网络请求时,可以自行设置头信息。
axios({
    url:'请求地址',
    headers:{
    	'Authorization':'令牌信息'
    }
})

网络请求代码优化

创建一个/src/common/js/http.js文件
在这个文件中封装了两个公用的方法
 
import axios from 'axios'
const token = JSON.parse(localStorage.getItem('htuser')).token;
/**
 * 携带令牌的get方法
 * @param {请求地址} url 
 * @param {提交的数据} data 
 */
function get(url,params = {}){
    return new Promise((resolve,reject)=>{
        axios({
            url,
            params,
            headers:{
                'Authorization':token
            }
        }).then(response=>{
            resolve(response)
        }).catch(err=>{
            reject(err)
        })
    })
} 
/**
 * 携带令牌的post请求
 * @param {请求地址} url 
 * @param {提交的数据} data 
 */
function post(url,data = {}){
    return new Promise((resolve,reject)=>{
        axios({
            url,
            method:'post',
            data,
            headers:{
                'Authorization':token
            }
        }).then(response=>{
            resolve(response)
        }).catch(err=>{
            reject(err)
        })
    })
} 
export default { get,post }

在/src/main.js中引入公用方法的文件
import http from './common/js/http'
Vue.prototype.$http = http;
 
页面组件使用
get请求
this.$http.get('请求地址').then(res=>{
	//处理逻辑
})
 
post请求
this.$http.post('请求地址',{提交的数据}).then(res=>{
	//处理逻辑
})

商品规格

商品规格添加
接口地址:/api/specsadd
商品规格和属性值之间的关系是一对多,所以在页面需要使用动态组件

<script>
	data(){
		return{
			specsAttrs:[
                { value:'' }
            ]
		}
	}
</script>
 
<!-- 页面结构: 根据属性值数组来动态生成页面结构 -->
<el-form-item label="规格属性" v-for="(item,index) of specsAttrs" :key="index">
    <div class="attritem">
        <el-input v-model="item.value"></el-input>
        <el-button @click="addAttr" v-if="index == 0" type="primary">新增规格属性</el-button>
        <el-button @click="removeAttr(index)" v-else type="danger">删除</el-button>
    </div>
</el-form-item>

新增规格属性:
通过给数组追加元素,来控制页面内容中的规格属性输入框
<script>
	methods:{
        addAttr(){
            if(this.specsAttrs.length<=5){
                this.specsAttrs.push({
                    value:''
                })
            }
        }
     }
</script>
提交数据:
submitInfo(formName) {
    this.$refs[formName].validate((valid) => {
        if (valid) {
            let data = JSON.parse(JSON.stringify(this.info));
            let url = '/api/specsadd';
            if(this.$route.params.id){
                url = '/api/specsedit';
                data.id = this.$route.params.id;//执行修改接口时的必要条件
            }
            //数据库中的status字段不是布尔值,所以要自行转换一下
            data.status = data.status ? 1 : 2;
            let arr = [];
            this.specsAttrs.map(item=>{
            	arr.push(item.value);
            })
            data.attrs = arr ? arr.join(',') : '';
            //发起post请求,请求接口项目中的规格添加接口,完成数据的保存
            this.$http.post(url,data).then(res=>{
                if(res.data.code == 200){
                	this.$router.push('/specs')
                }else{
                	alert(res.data.msg)
                }
            })
        }
    });
}

商品管理

商品添加
分类和规格属性联动
通过给select标签绑定change事件,可以获取当前分类和规格选中的value属性值,然后通过此属性值获取二级分类信息和规格属性信息

分类
<el-form-item label="一级分类" prop="first_cateid">
    <!-- 切换一级分类,让二级分类的内容进行联动 -->
    <el-select 
        v-model="info.first_cateid" 
        placeholder="请选择"
        @change="cateChange"
    >
        <el-option value="">请选择</el-option>
        <el-option 
            v-for="cateitem of cates" 
            :key="cateitem.id"
            :value="cateitem.id"
            :label="cateitem.catename"
        >{{ cateitem.catename }}</el-option>
    </el-select>
</el-form-item>
<el-form-item label="二级分类" prop="first_cateid">
    <el-select v-model="info.second_cateid" placeholder="请选择">
        <el-option value="">请选择</el-option>
        <el-option 
            v-for="seconditem of secondCates" 
            :key="seconditem.id"
            :value="seconditem.id"
            :label="seconditem.catename"
        >{{ seconditem.catename }}</el-option>
    </el-select>
</el-form-item>
 
规格
<el-form-item label="商品规格" prop="specsid">
    <el-select 
        placeholder="请选择"
        @change="specsChange"
        v-model="info.specsid"
    >
        <el-option value="">请选择</el-option>
        <el-option 
        	v-for="specsitem of specsarr" 
            :key="specsitem.id"
            :value="specsitem.id"
            :label="specsitem.specsname"
        >{{ specsitem.specsname }}</el-option>
    </el-select>
</el-form-item>
<el-form-item label="规格属性">
    <el-select v-model="info.specsattr" placeholder="请选择" multiple>
    <el-option value="">请选择</el-option>
    <el-option 
        v-for="(attritem,attrindex) of specsattrs" 
            :key="attrindex"
            :value="attritem"
            :label="attritem"
        >{{ attritem }}</el-option>
    </el-select>
</el-form-item>

js逻辑代码:

分类
//一级分类选择
cateChange(e){
    if(e==""){
    	return false;
    }
    this.$http.get('/api/catelist',{pid:e}).then(res=>{
    	this.secondCates = res.data.list;
    })
}
 
规格
specsChange(e){
    if(e==""){
    	return false;
    }
    this.$http.get('/api/specsinfo',{id:e}).then(res=>{
        if(res.data.code === 200){
        	this.specsattrs = res.data.list ? res.data.list[0].attrs : [];
        }
    })
}

富文本编辑器的使用

在编辑商品详情时,商品的描述有文字和图片内容,用普通的输入框和文本域操作不方便
我们可以使用富文本编辑器来实现图文混写

安装

npm i wangeditor --save

引入

在页面组件中引入
import E from 'wangeditor'

使用

设置一个容器用来生成富文本编辑器
<el-form-item label="商品描述">
	<div id="desc"></div>
</el-form-item>
 
在组件挂载时,实例化富文本编辑器
data(){
	return{
		...
		editor:null
	}
}
mounted(){
    this.editor = new E("#desc");
    this.editor.create();
}
 
获取内容
this.editor.txt.html()
 
设置内容
this.editor.txt.html(要展示的商品描述信息html代码)

用户信息使用vuex进行管理
安装vuex

npm i vuex --save

创建目录和相关文件

在src下创建一个store目录,然后在此目录下创建index.js、state.js、mutations.js、actions.js

//state.js
export default {
    adminUser:null
}
//mutations.js
export default{
	//定义直接改变数据的方法-同步操作
    setAdminUser(state,info){
        state.adminUser = info;
    }
}
//actions.js
export default {
    //异步方法,用来改变用户信息
    setAdminUserSync({ commit },info){
        commit('setAdminUser',info)
    }   
}
//getters.js
export default {
    userInfo : state => state.adminUser
}
//index.js
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex);
import state from './state'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'
 
export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters
})

路由守卫中从vuex仓库中读取数据,不再从本地存储中读取了。

import store from '../store'
router.beforeEach((to,from,next)=>{
    let userinfo = store.state.adminUser;//从vuex中读取用户信息
    if(userinfo){...}
});

Layout.vue中的头部组件内容也从vuex中读取用户信息

<script>
import { mapGetters } from 'vuex'
export default {
    computed:{
        ...mapGetters(['userInfo'])
    }
}
</script>

状态持久化

使用本地存储结合vuex
使用插件来实现数据持久化

安装

npm i vuex-persistedstate --save

使用

/src/store/index.js
 
import createPersistedState from 'vuex-persistedstate'
export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters,
    plugins:[createPersistedState()]
})

接口地址优化

为了防止后期接口地址变化,涉及到要修改的页面数量很多,所以我们可以预先把所有要访问的接口地址,定义在一个js文件中(接口地址配置文件),它的作用就是维护项目要请求的所有接口地址,一旦接口地址要调整,只需要改变这一个文件即可,页面组件中的代码不需要做任何调整。

在/src/common/目录创建了一个apis.js

let catelist = '/api/catelist',
    cateinfo = '/api/cateinfo',
    cateedit = '/api/cateedit',
    catedelete = '/api/catedelete'
	...
 
export default { 
    catelist,cateinfo,cateedit,catedelete
	...
}

在main.js中使用定义好的接口配置文件,并挂到原型链上,方便所有的页面组件使用

import apis from './common/js/apis'
Vue.prototype.$apis = apis;

数据可视化

highcharts官网 echarts官网
在vue项目中使用
安装

npm i echarts --save

引入

import echarts from 'echarts'

<template>
    <div>
        <div id="main" style="width:100%;height:400px;"></div>
    </div>
</template>
 
<script>
import echarts from 'echarts'
export default {
    data(){
        return{
            charts:'',
            orderData:["313","340","110","480","390","700","810"]
        }
    },
    methods:{
        drawLine(){
            // 基于准备好的dom,初始化echarts实例
            this.charts = echarts.init(document.getElementById('main'));
            // 指定图表的配置项和数据
            var option = {
                title: {
                    text: '近一周订单数量'
                },
                tooltip: {},
                legend: {
                    data:['近一周订单数量']
                },
                xAxis: {
                    type:'category',
                    data: ["周一","周二","周三","周四","周五","周六","周日"]
                },
                yAxis: {},
                series: [{
                    name: '近一周订单数量',
                    type: 'line',
                    data: this.orderData
                }]
            };
            // 使用刚指定的配置项和数据显示图表。
            this.charts.setOption(option);
        }
    },
    mounted(){
        this.drawLine();
    }
}
</script>
<style>
</style>

百度地图api

注册成为开发者
在控制台中,创建一个应用,就可以获取到秘钥了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>百度地图</title>
    <script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&type=webgl&ak=你的应用秘钥"></script>
    <style type="text/css">  
        html{height:100%}    
        body{height:100%;margin:0px;padding:0px}    
        #container{height:100%}    
    </style> 
</head>
<body>
    <div id="container"></div>
    <script type="text/javascript">
        var map = new BMapGL.Map("container");          // 创建地图实例 
        var point = new BMapGL.Point(116.566956,39.875176);  // 创建点坐标 
        map.centerAndZoom(point, 15);                 // 初始化地图,设置中心点坐标和地图级别
        map.enableScrollWheelZoom(true);     //开启鼠标滚轮缩放
        // map.setHeading(64.5);   //设置地图旋转角度
        // map.setTilt(73);       //设置地图的倾斜角度
        // map.setMapType(BMAP_EARTH_MAP);      // 设置地图类型为地球模式
    </script>
</body>
</html>

ui库-mintui
安装

npm i mint-ui --save

引入

在main.js中引入
import Mint from 'mint-ui';
Vue.use(Mint);
import "mint-ui/lib/style.css"

tabbar组件

创建前台基础页面布局、首页、购物车、我的四个组件,并定义好路由规则。

使用mint-tabbar组件实现底部菜单导航
 
<template>
    <mt-tabbar fixed>
        <mt-tab-item id="/home" @click.native="switchTab('/home')">
            <img slot="icon" src="../../assets/img/home_on.png" v-if="this.selected == '/home'"/>
            <img slot="icon" src="../../assets/img/home.png" v-else/>
            首页
        </mt-tab-item>
        <mt-tab-item id="/cart" @click.native="switchTab('/cart')">
            <img slot="icon" src="../../assets/img/cart_on.png" v-if="this.selected == '/cart'" />
            <img slot="icon" src="../../assets/img/cart.png" v-else />
            购物车
        </mt-tab-item>
        <mt-tab-item id="/me" @click.native="switchTab('/me')">
            <img slot="icon" src="../../assets/img/me_on.png" v-if="this.selected == '/me'"/>
            <img slot="icon" src="../../assets/img/me.png" v-else />
            我的
        </mt-tab-item>
    </mt-tabbar>
</template>
 
<script>
export default {
    data(){
        return{
            selected:'/home'
        }
    },
    methods:{
        switchTab(name){
            if(this.$route.path == name){
                return;
            }
            this.$router.push(name)
        }
    },
    mounted(){
        this.selected = this.$route.path;
    },
    watch:{
        $route(nowRoute){
            this.selected = nowRoute.path;
        }
    }
}
</script>
 
<style>
 
</style>

Typescript

Typescript是Javascript的超集,由微软开发和开源。
通俗的理解为Javascript的一个特殊版本,其语法规范严谨,适用于开发大型项目。

安装

npm i typescript -g

验证是否安装成功

tsc -v

编译
手动编译

创建一个index.ts,并在其中写一行普通的js代码
在命令行中,进入到指定的目录,执行命令tsc index.ts进行编译,在同级目录下会生成一个同名的js文件

自动编译(vscode)

生成ts配置文件

tsc --init
此时,就会在指定的目录下生成一个tsconfig.json的配置 

{
"compilerOptions": {
    /* 基本选项 */
    "target": "es5",// 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",// 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],// 指定要包含在编译中的库文件
    "allowJs": true,//允许编译 javascript 文件
    "checkJs": true,//报告javascript文件中的错误
    "jsx": "preserve",//指定jsx代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,//生成相应的 '.d.ts' 文件
    "sourceMap": true, //生成相应的 '.map' 文件
    "outFile": "./",//将输出文件合并为一个文件
    "outDir": "./",//指定输出目录
    "rootDir": "./",//用来控制输出目录结构 --outDir.
    "removeComments": true,//删除编译后的所有的注释
    "noEmit": true,//不生成输出文件
    "importHelpers": true,//从tslib导入辅助工具函数
    "isolatedModules": true,//将每个文件做为单独的模块(与 'ts.transpileModule' 类似).
    /* 严格的类型检查选项 */
    "strict": true,//启用所有严格类型检查选项
    "noImplicitAny": true,//在表达式和声明上有隐含的any类型时报错
    "strictNullChecks": true,//启用严格的null检查
    "noImplicitThis": true,//当this表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,//以严格模式检查每个模块,并在每个文件里加入 'use strict'
    /* 额外的检查 */
    "noUnusedLocals": true,//有未使用的变量时,抛出错误
    "noUnusedParameters": true,//有未使用的参数时,抛出错误
    "noImplicitReturns": true,//并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,//报告switch语句的fallthrough错误。
    /* 模块解析选项 */
    "moduleResolution": "node",//选择模块解析策略:'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",//用于解析非相对模块名称的基目录
    "paths": {},//模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],//根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],//包含类型声明的文件列表
    "types": [],//需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true, //允许从没有设置默认导出的模块中默认导入。
    /* Source Map Options */
    "sourceRoot": "./",//指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",//指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,//生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,//将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
    /* 其他选项 */
    "experimentalDecorators": true,//启用装饰器
    "emitDecoratorMetadata": true//为装饰器提供元数据的支持
  }
}
 
注意:需要什么配置,将其注释解开配置正确即可
修改配置文件,"outDir": "./"注释解开,具体目录地址可以自行设置

编写index.ts文件的内容
vscode 工具栏 > 终端 > 运行生成任务 > tsc:监视-tsconfig.json
这样,在index.ts文件,编写了内容,就会自动生成对应的js文件。
在根目录下创建一个index.html,并引入生成好的js文件

声明变量和数据类型

ts最大的特点就是进行严谨的数据类型验证,要求我们在声明一个变量时就定义好该变量的数据类型,且赋值的内容必须对应的数据类型保持一致。

声明变量 -格式

let 变量名 : 数据类型;
let 变量名 : 数据类型 = 内容;

错误用法:

let str : string;
str = 100 //此时会报错误,数据类型不一致

数据类型 -格式

字符串
let str : string = "初识ts";//值必须为字符串

数值
let num : number = 10;//值必须为数值

布尔值
let islogin : boolean = false;//值必须为布尔值

数组
原生js方式
let arr = []
let arr2 = new Array();

ts方式
//ts定义数组也有两种方式
//第一种方式:ts定义数组,需要指定数组内元素的数据类型
let arr3:string[];//定义了一个数组,并且数组元素的类型为字符串
//第二种方式
let arr4:Array<number> = [11,22,33]

ts新增数据类型
元组类型 -格式

元组类型是数组类型的子类型,表示一个已知元素数量和类型的数组,各元素类型可以不一样

let 变量名:[数据类型1,数据类型2...] = ['值1','值2'...]
let arr5:[string,number,boolean] = ["hello",100,true]

any -格式

any类型,表示任意类型,如果不确定我们的变量将来是什么类型,就可以使用any类型

let obj:any = {
    name : '刘飞',
    age:20
}
obj.age = '30'
let arr6:any = []
arr6[1] = 'hello'
arr6[2] = true

枚举类型 -格式

表示值时某些固定值中的一些
比如:性别,男、女
​            订单状态:已下单、已发货、已收货

enum 变量名 {
	值1,值2
}

void

一般应用于函数,表示函数无返回值

never

表示那些永远不存在的数据类型

function error(msg:string):never{
    throw new Error(msg)
}

object

object表示非原始类型
就是除了number、string、boolean、array、null、undefined之外的类型

function initbox(data:object):void{
    console.log(JSON.stringify(data))
}
initbox({name:'小明',age:22})

函数-定义

ts中的函数相关知识和es6有很多相同之处
在ts中,定义一个函数,需要定义函数的返回值类型,如果没有返回值则类型为void,如果函数有参数的话,需要给参数也指定数据类型

格式:
function 函数名([参数名:参数的数据类型]):函数返回值的数据类型{...}

function add(num:number):void{//函数无返回值
     console.log(num)
}
function add(num:number):number{//函数的返回值的类型是数字
    return num+=10;
    //return '100'//此时返回的数据类型为字符串,就会报错
}

函数参数

ts里的每个函数参数都是必须的

ts里的每个函数参数都是必须的
①默认参数
//默认参数
function add1( x:number,y:number=10 ) : number {
    return x + y;
}
// console.log(add1(100,50))
console.log(add1(100,30))
 
②可选参数
// 可选参数
function hello(str:string,txt?:string):string{
    return str + txt
}
console.log(hello('你好'))
可选的参数如果没有传递,在函数中仍然用到了这个参数,则它的值是undefined
 
③剩余参数
替代传统的函数的arguments的作用
剩余参数需要是所有参数中的最后一位,不然就做不到剩余。
function family(main:string,...child:string[]):string{
    console.log(main)
    console.log(child)
    return '户主:' + main + ",家庭成员:" + child.join(" ");
}
console.log(family("小王","王一","王二","王三"))

ts中相关类的知识,和es6中非常的相似。

(1)类的定义
使用class关键词
class Person{
    name : string;//属性
    constructor(username : string){
        this.name = username;
    }
    speak(language : string):void{
        console.log(`${ this.name }会说${ language }`)
    }
}
 
(2)类的继承
使用extends关键词
//类的定义
class Person{
    name : string;//属性
    constructor(username : string){
        this.name = username;
    }
    speak(language : string,langeuage2?:string):void{
        console.log(`${ this.name }会说${ language }`)
    }
}
 
//类的继承
class Chinese extends Person{
    constructor(username:string){
        super(username)
    }
    speak(language:string,language2:string){
        // super.speak(language);
        console.log(`${ this.name }会说${ language },而且还会说${language2}`)
    }
}
 
let p1 = new Chinese('张飞');
p1.speak("汉语","英语");
 
(3)类的修饰符
public,公有【默认】,在当前类里面、子类、类的外部都可以访问
protected,受保护,在当前类里面、子类可以访问,在类外部无法访问
private,私有,只能在当前类里面访问,不能在子类、类外部访问
①public
class Person{
    name:String;
    constructor(n:string){
        this.name = n;
    }
}
class Chinese extends Person{
    constructor(n:string){
        super(n)
        console.log(n)//在子类中获取
    }
    speak(){}
}
let p1 = new Chinese('小李111111')
p1.speak();
console.log(p1.name)//在类的外部调用属性
 
②protected受保护类型
class Teacher{
    protected name:string;//受保护属性
    constructor(name:string){
        this.name = name;
    }
    teach():string{
        return `${this.name}在讲课`;
    }
}
let t1 = new Teacher('李老师');
console.log(t1.teach());
// console.log(t1.name)//在类的外部无法直接访问一个受保护的属性
class WebTeacher extends Teacher{
    constructor(name:string){
        super(name)
    }
    teach():string{
        return `${this.name}在讲课----子类`;
    }
}
let t2 = new WebTeacher('范老师')
console.log(t2.teach())
 
③私有属性
class Student{
    private name:string;
    constructor(name:string){
        this.name = name;
    }
    study():string{
        return `${this.name}在学习web前端`;//在当前类中可以调用私有属性
    }
}
let s1 = new Student('小聪')
console.log(s1.study());
class MidStudent extends Student{
    constructor(name:string){
        super(name)
    }
    homework():void{
        // console.log(this.name+'在写作业')//无法调用父类中的私有属性name
    }
}

类的其他修饰符

①静态属性和方法
static
静态属性或者静态方法使用static修饰符,调用属性或者方法时,不需要实例化类
通过类名.静态属性或者静态方法直接使用
// 静态属性和方法
class Util{
    static desc:string = '工具类'
    static getTime(time:number):string{
        let d = new Date(time);
        return d.toLocaleTimeString();
    }
}
//调用类中的静态属性
console.log(Util.desc)
// 调用类中的静态方法
console.log(Util.getTime(new Date().getTime()))
 
②抽象类
抽象类,一般作为其派生类的基类使用,它们一般不会被直接实例化,一般用来定义标准。
abstract关键词,抽象类中的抽象方法,不实现具体的功能,它只做约束作用(约束它的子类)
在子类中,一旦继承了抽象类,那么抽象类中的抽象方法,就必须在子类去实现具体的函数功能。
//抽象类
abstract class Car{
    abstract drive():any;
    abstract stop():any;
    run(){
        console.log('汽车run')
    }
}
class BMW extends Car{
    drive():void{
        console.log("这辆宝马在路上驾驶...")
    }
    stop():void{
        console.log("这辆宝马已经停止驾驶了...")
    }
}
let c1 = new BMW();
c1.drive();

接口

function tt(str:string):void{
    console.log(str)
}
在上述代码中,只能校验一个变量的数据类型
如果传递的参数数量比较多或者是一个对象,那么这个函数的参数校验规则就无法实现校验
接口(interface)是类中的一个很重要的概念,它是对行为的抽象,而具体如何实现需要由类(class)去实现(implements)

(1)接口定义
interface Person {
    name:string,
    age:number
}
let p1 : Person;
p1 = {
    name : '小芳',
    age : 18
}
 
(2)类的接口
interface Animal{
    name:string;
    eat(str:string):void;
}
class Horse implements Animal{
    name:string;
    constructor(str:string){
        this.name = str;
    }
    eat():void{
        console.log(this.name + '在吃粮草')
    }   
}
let hxbm = new Horse('汗血宝马');
hxbm.eat();

装饰器

装饰器是一种特殊的类型声明,它可以被附加类、方法、属性、参数上面,可以修改类的行为。
装饰器就是一个方法或者说是一个特殊函数,可以被注入到类、方法、属性、参数上。
装饰器分类:类装饰器、属性装饰器、方法装饰器、参数装饰器
装饰器也是es7的标准特性之一。

类装饰器

类装饰器是在声明类之前(紧靠类的声明处),类装饰器应用于类的构造函数,可以监视、修改或者替换类定义。

①普通装饰器
定义装饰器
// 1.普通装饰器(无法传参数)
function logClass(target:any){
    target.prototype.url = "请求的链接地址"
    target.prototype.run = function(){
        console.log("这是run方法")
    }
}
使用装饰器
@logClass//使用装饰器
class HttpClient{
    name:string;
    constructor(n:string){
        this.name = n;
    }
}
let http : any = new HttpClient("test");
console.log(http.url)
http.run();
 
②装饰器工厂(可以传递参数)
定义装饰器
function logClass(params:string){
    return function(target:any){
        console.log(target,111)
        console.log(params,222)
        target.prototype.url = "接口地址是"+params;
    }
}
 
调用装饰器并传递参数
@logClass("www.ujiuye.com")
class HttpClient{
    name:string;
    constructor(n:string){
        this.name = n;
    }
}

属性装饰器

定义属性装饰器
function logProperty(params:any){
    return function(target:any,attr:any){
        console.log(target,3333)//target 类对象
        console.log(attr,4444)//attr 属性名
        console.log(params,5555)//params 调用属性装饰器时传递的参数
        target[attr] = params
    }
}
 
使用属性装饰器
@logClass("www.ujiuye.com")
class HttpClient{
    @logProperty("张辽")//调用属性装饰器并传递参数
    name:string | undefined;
    constructor(){}
}
let http : any = new HttpClient();

方法装饰器

它可以被运用到方法的属性描述上,可以用来监视、修改、替换方法定义
运行时,需要传递三个参数
(1)对于静态类成员来说是类的构造函数,对于实例成员来说是类的原型对象
(2)成员(属性)的名字
(3)成员的属性描述

定义方法装饰器
function logMethod(params:any){
    return function(target:any,methodName:any,desc:any){
        console.log(target,6666)//类的原型
        console.log(methodName,7777)//方法名称
        console.log(desc.value,8888)//方法的描述,desc.value就是具体的方法内容
    }
}
 
使用方法装饰器
class HttpClient{
    url:string | undefined;
    constructor(){}
    @logMethod('xxxxx')//调用方法装饰器
    getData(){
        console.log(this.url)
    }
}

参数装饰器

运行时会被当做函数调用,可以使用参数装饰器为类的原型增加一些元素数据,传入三个参数
(1)对于静态类成员来说是类的构造函数,对于实例成员来说是类的原型对象
(2)方法名字
(3)参数在函数参数列表中的索引

定义参数装饰器
function logParams(){
    return function(target:any,methodName:any,paramsIndex:any){
        console.log(target,9999)//类的原型
        console.log(methodName,11111)//方法名称
        console.log(paramsIndex,22222)//参数的索引
    }
}
 
使用参数装饰器
class HttpClient{
    constructor(){}
    @logMethod('xxxxx')
    getData(num:number,@logParams() str:any){//使用参数装饰器
        console.log(this.url)
    }
}

以上完整代码

//ts
let a = 2020;
console.log(a);
// 变量声明
let dyh:string;
dyh = "dyh";
 
let n:number;
n = 12;
 
let islogin : boolean = false;
 
// 数组定义
let arr = [];
let arr2 = new Array();
 
// ts的数组定义
//第一种方式:ts定义数组,需要指定数组内元素的数据类型
let arr3:string[];//定义了一个数组,并且数组元素的类型为字符串
//第二种方式
let arr4:Array<number> = [1,2,3];
 
// 元组 知道数据类型和数量
let arr5:[string,number,boolean] = ['dd',1,true];
 
// any 不能确定数据类型
let obj:any = {
    name:'dyh',
    age:20,
    sex:true
}
obj.age = '30';
 
let arr6:any = [];
arr6[1] = 'hello';
arr6[2] = true;
 
 
// 枚举 不赋值打印下标,赋值 打印值
enum Num {
    sex,
    true = '真',
    false = '假'
}
let mynum = Num.true;
let mynum1 = Num.sex;
console.log(mynum,mynum1,'mynum');
// void无返回值
 
// never类型 错误
function error(msg:string):never{
    throw new Error(msg)
}
// error('dyh');
 
// Object类型
function initbox(data:object):void{
    console.log(JSON.stringify(data))
}
initbox({nane:'小代',age:22})
 
// 函数
function add(num:number):void{//无返回值类型
    console.log(num,'无返回值void');
}
add(20)
 
function add1(str:string,str1:string):string{//返回字符型
    return str + str1;
}
console.log(add1('dyh:','string类型'));
 
// ts里的每个函数参数都是必须的
// 默认参数
function add2(x:number,y:number=10):number{
    return x + y;
}
console.log(add2(12,22));
console.log(add2(12));
// 可选参数
function hello(str:string,num?:number):string{
    return str + num;
}
console.log(hello('dyh:',22),'函数可选参数');
console.log(hello('dyh:'),'函数可选参数1');
 
// 剩余参数
// 替代传统的函数的arguments的作用
function family(main:string,...child:string[]):string{
    console.log('户主:'+main);
    console.log(child);
    return '户主:'+main+'家庭成员:'+child.join(' ');
}
console.log(family('啊豪','小一','小二','小三','小四'));
 
// 类 定义
// 使用class定义
class Person{
    name:string;//属性
    constructor(username:string){
        this.name = username;
    }
    // 方法
    speak(language:string,txt?:string):void{
        console.log(`${this.name} 和 ${language} ${txt}`)
    }
}
let p = new Person('啊豪')
p.speak('中国')
 
// 类的继承 extends
class chinese extends Person{
    constructor(name:string){
        super(name)//使用父级
    }
    speak(language:string,txt:string = '汉语'):void{
        super.speak(language,txt);
        // console.log(`${this.name} 和 ${language} ${txt}`)
    }
}
 
let p1 = new chinese('小代')
p1.speak('中国')
// console.log(p1);
 
// 类的修饰符
 
// 1 public 公共    默认
// 在当前类里面、子类、类的外部都可以访问
class Pub{
    public name:string;
    age:number;
    constructor(n:string,a:number){
        this.name = n;
        this.age = a;
    }
}
class Publ extends Pub{
    constructor(n:string,a:number){
        super(n,a)
        console.log(n,a);//子类中可以使用
    }
    speak(){}
}
let p2 = new Publ('小代',18)
console.log(p2.name);//外部也可以使用
 
// protected 受保护类型
// 在当前类里面、子类可以访问,在类外部无法访问
class Pro{
    protected name:string;
    constructor(name:string){
        this.name = name;
    }
    sk():string{
        return this.name+'666';
    }
}
let pr = new Pro('小山')
console.log(pr.sk);
// console.log(pr.name);受保护 外部无法访问
class Prot extends Pro{
    constructor(name:string){
        super(name)
    }
    sk():string{
        return this.name+'子类中使用'
    }
}
let pr2 = new Prot('小阿')
console.log(pr2.sk()); //子类中可以使用
 
// private 私有属性
// 只能在当前类里面访问,不能在子类、类外部访问
class Pri{
    private name:string;
    constructor(name:string){
        this.name = name;
    }
    su():string{
        return this.name + '父类中使用'
    }
}
let s = new Pri('小代')
console.log(s.su());
 
class Priv extends Pri{
    constructor(n:string){
        super(n)
    }
    k():void{
        // console.log(this.name+'子类') 无法调用父类的私有属性
    }
}
 
// 类的其他修饰符
// 静态属性和方法   static
// 静态属性或者静态方法使用static修饰符,调用属性或者方法时,不需要实例化类
// 通过类名.静态属性或者静态方法直接使用
class Sta{
    static desc:string = '静态方法'
    static getTim(time:number):string{
        let t = new Date(time);
        return t.toLocaleTimeString();
    }
}
// 调用静态方法
console.log(Sta.desc);
console.log(Sta.getTim(new Date().getTime()));
 
// 抽象类 abstract
// 抽象类,一般作为其派生类的基类使用,它们一般不会被直接实例化,一般用来定义标准。
// abstract关键词,抽象类中的抽象方法,不实现具体的功能,它只做约束作用(约束它的子类)
// 在子类中,一旦继承了抽象类,那么抽象类中的抽象方法,就必须在子类去实现具体的函数功能
abstract class Car{
    abstract drive():any;//定义基类 不实现方法
    abstract stop():any;
    run(){//实现具体方法
        console.log('开始了');
    }
}
class ww extends Car{
    drive():void{ //实现具体的方法
        console.log('启动了');
    }
    stop():void{
        console.log('停止');
    }
}
let cl = new ww();
cl.drive();
 
// 接口
// 验证变量的类型
function t(str:string):void{
    console.log(str);
}
 
// 如果传递的参数数量比较多或者是一个对象,那么这个函数的参数校验规则就无法实现校验
// 接口(interface)是类中的一个很重要的概念,它是对行为的抽象,而具体如何实现需要由类(class)去实现(implements)
 
// 1 接口定义 用于验证类型
interface Inter{
    name:string,
    age:number
}
let pe1:Inter;
// 同 str:string  只不过是自己写的验证接口
pe1 = {
    name:'小零',
    age:12
}
 
// 2 类的接口
interface Anim{
    name:string;
    eat(str:string):void;
}
class Hot implements Anim{
    name:string;
    constructor(str:string){
        this.name = str;
    }
    eat():void{
        console.log(this.name+'类接口');
    }
}
let hxbm = new Hot('使用');
hxbm.eat();
 
// 装饰器
// 装饰器是一种特殊的类型声明,它可以被附加类、方法、属性、参数上面,可以修改类的行为
// 是一个方法或者说是一个特殊函数,可以被注入到类、方法、属性、参数上。
// 装饰器分类:类装饰器、属性装饰器、方法装饰器、参数装饰器
 
// 普通装饰器
// 1.普通装饰器(无法传参数)
function logClass(target:any){
    target.prototype.url = '没有传参'
    target.prototype.run = function(){
        console.log('这个是run方法');
    }
}
 
// 使用装饰器
@logClass
class Client{
    name:string;
    constructor(n:string){
        this.name = n;
    }
}
let Cli:any = new Client('text');
console.log(Cli.url);
Cli.run();
 
//2 装饰器工厂(可以传递参数)
function lgClass(params:string){
    return function(target:any){
        console.log(target,111);
        console.log(params,222);
        target.prototype.url = 'www.'+params+'.com'
    }
}
// 调用装饰器并传递参数
@lgClass('baidu')
class http{
    name:string;
    constructor(n:string){
        this.name = n;
    }
}
let ht:any = new http('访问');
console.log(ht.url);
 
//3 属性装饰器
function logPro(params:any){
    return function(target:any,attr:any){
        console.log(target,333);//类对象
        console.log(attr,444);//属性名
        console.log(params,555);//调用属性修饰器时传递的参数
        target[attr] = params
    }
}
// 使用
class hp{
    @logPro('小代')
    name:string | undefined;
    constructor(){}
}
let hps:any = new hp();
 
// 4 方法装饰器
// 运用到方法的属性描述上,可以用来监视、修改、替换方法定义运行时,需要传递三个参数
// (1)对于静态类成员来说是类的构造函数,对于实例成员来说是类的原型对象
// (2)成员(属性)的名字
// (3)成员的属性描述
function logMethod(params:any){
    return function(target:any,methodName:any,desc:any){
        console.log(target,6666)//类的原型
        console.log(methodName,7777)//方法名称
        console.log(desc.value,8888)//方法的描述,desc.value就是具体的方法内容
    }
}
// 使用
class hh{
    url:string | undefined;
    constructor(){}
    @logMethod('方法装饰器')
    getData(){
        console.log(this.url);
    }
}
let gg:any = new hh();
 
//5 参数装饰器
// 运行时会被当做函数调用,可以使用参数装饰器为类的原型增加一些元素数据,传入三个参数
// (1)对于静态类成员来说是类的构造函数,对于实例成员来说是类的原型对象
// (2)方法名字
// (3)参数在函数参数列表中的索引
 
function logdyh(){
    return function(target:any,methodName:any,paramsIndex:any){
        console.log(target,999)//类的原型
        console.log(methodName,111)//方法名称
        console.log(paramsIndex,222)//方法的描述,desc.value就是具体的方法内容
    }
}
// 使用
class ld{
    constructor(){}
    getData(k:any,@logdyh() n:number,str:any,id:any){//使用参数装饰器
        console.log(111)
    }
}
let dd:any = new ld();

vue-cli脚手架升级
安装

先卸载已安装的脚手架,再安装新版本的脚手架(@vue/cli)
npm uninstall vue-cli -g

新的包名已改为@vue/cli
npm install @vue/cli -g

检查版本
vue --version

初始化项目

vue create 项目名称
或
vue ui     打开网页版可视化操作

运行项目

npm run serve

项目目录结构

新脚手架结合ts进行开发
定义class组件
创建一个页面组件,修改script代码

<script lang="ts">//lang=ts 表示使用ts语法
//引入类装饰器Component和Vue类
import { Component,Vue } from 'vue-property-decorator'
@Component({})
export default class Home extends Vue{
    
}
</script>

新语法

我们在定义data、生命周期、计算属性、事件方法、数据监听、组件引入都有一定的变化
在class组件中没有了data、methods

(1)定义初始变量

export default class Home extends Vue{
    public str : string = "超级天王"//定义初始变量
}

(2)定义方法

export default class Home extends Vue{
    public str : string = "超级天王"//定义初始变量
    changeStr():void{
        this.str = "中原一点红"
    }
}

(3)生命周期

mounted() {
	console.log("组件挂载完成")
}
 
生命周期直接在类中以函数的方式使用即可

(4)计算属性

没有了computed,直接写在前面并带上一个get
//计算属性
get newStr(){
	return '我是'+this.str;
}

组件通信
父子组件

新脚手架中父子组件通信,需要使用Prop装饰器

//父组件:
<template>
  <div class="about">
    <h1>This is an about page</h1>
	<!-- 使用子组件并传递数据 -->
    <v-child :gift="msg"></v-child>
  </div>
</template>
<script lang="ts">
import { Component,Vue } from 'vue-property-decorator'
//引入子组件
import vChild from './Child.vue';
//利用装饰器,注册子组件
@Component({
    components:{
        vChild
    }
})
export default class About extends Vue{
    msg:string = "父组件上的数据"
}
</script>
 
//子组件:
子组件中要通过Prop装饰器来接收数据
<template>
  <div class="about">
    <h1>这是子组件</h1>
    <p>gift:{{ gift }}</p>
  </div>
</template>
<script lang="ts">
import { Component,Vue,Prop,Emit } from 'vue-property-decorator'
@Component({})
export default class Child extends Vue{
    // @Prop(String) readonly gift : string | undefined
    @Prop({
        type:String,
        required:true
    })
    gift : string | undefined
}
</script>

子父组件

父组件:
在父组件中传递一个自定义事件
<v-child :gift="msg" @changeStr="changeMsg"></v-child>
 
并在父组件的类中定义对应的函数
export default class About extends Vue{
    msg:string = "父组件上的数据"
    changeMsg(str:string){
        console.log("父组件上的函数被子组件触发了..."+str)
    }
}
 
子组件:
在子组件中通过Emit装饰器来触发父组件的事件
<template>
  <div class="about">
    <h1>这是子组件</h1>
    <p>gift:{{ gift }}</p>
    <button @click="send">发送</button>
  </div>
</template>
<script lang="ts">
import { Component,Vue,Prop,Emit } from 'vue-property-decorator'
@Component({})
export default class Child extends Vue{
    // @Prop(String) readonly gift : string | undefined
    @Prop({
        type:String,
        required:true
    })
    gift : string | undefined
    @Emit("changeStr")//触发父组件的事件
    send(){
        return '子组件传递的数据';//传递数据给父组件
    }
}
</script>

vue项目打包

在项目根目录下执行命令
npm run build

注意事项:
每一个页面组件的样式,==一定==要写scoped,防止打包后,样式污染。
打包后的项目,代理转发失效,需要在对应的服务器上进行代理的设置(需要后端配置代理)
以nginx服务器为例,代理的配置
在网站域名列表处,点击设置按钮,再找到配置文件
//===============================================
location ^~/uploads{
	proxy_pass http://服务器域名或ip地址:端口;
}
location /api/{
	proxy_pass http://服务器域名或ip地址:端口; 
}
//===============================================
重启nginx让配置生效

@vue/cli新脚手架代理配置

在项目根目录下创建一个vue.config.js文件

module.exports = {
    outputDir: 'dist',  //build输出目录
    assetsDir: 'assets', //静态资源目录(js, css, img)
    lintOnSave: false, //是否开启eslint
    devServer: {
        open: true, //是否自动弹出浏览器页面
        https: false,  //是否使用https协议
        hotOnly: false, //是否开启热更新
        proxy: {
            '/关键词': {
                target: '目标域名地址', //API服务器的地址
                ws: true, //代理websockets
                changeOrigin: true, // 是否跨域,默认是true
				// pathRewrite:{ //替换为空 自己填写的
				//   '^/dyh':'' 
				// }
            }
        }
    }
}
 
配置完成后,==>重启项目生效

服务端渲染
什么是服务端渲染 SSR

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序

为什么要使用服务端渲染

优势:
​    更好的SEO优化
​    渲染速度快
缺陷:
​    开发条件受限,vue的某些生命周期钩子函数无法使用
​    部署和构建有要求,在node环境下运行
​    更多的服务器端负载
ssr本质就是将vue的语法功能延伸到服务器端,在服务器端渲染好节点内容,给客户端浏览器去编译。

代码实现

创建一个文件,并进入到命令行中
(1)初始化node环境
npm init

(2)安装vue和ssr
npm i vue vue-server-renderer --save

将vue模板内容编译成字符串

//引入vue
const Vue = require('vue')
//创建vue实例
const app = new Vue({
    data:{
        msg : 'vue-ssr学习'
    },
    template:'<div>你好,{{ msg }}</div>'
});
//创建一个vue服务端渲染对象
const renderer = require('vue-server-renderer').createRenderer()
//将vue实例变成字符串内容
renderer.renderToString(app,(err,html)=>{
    if(err) throw err;
    console.log(html)
})

启动一个服务器,并展示编译后的字符串

//引入vue
const Vue = require('vue')
//引入express
const express = require('express')
const server = express()
//创建vue实例
const app = new Vue({
    data:{
        msg : 'vue-ssr学习'
    },
    template:'<div>你好,{{ msg }}</div>'
});
//创建一个vue服务端渲染对象
const renderer = require('vue-server-renderer').createRenderer()
server.get('/index',(req,res)=>{
    //将vue实例变成字符串内容
    renderer.renderToString(app,(err,html)=>{
        if(err) throw err;
        // console.log(html)
        res.end(html)
    })
})
server.listen(3000,()=>{
    console.log('服务器运行在3000端口')
})

结合html文件和模板语法来实现ssr

//app.js
//引入vue
const Vue = require('vue')
//引入express
const express = require('express')
const server = express()
const fs = require('fs')
//创建vue实例
const app = new Vue({
    data:{
        // 这里无法传递变量 所有不用设置
        msg : 'vue-ssr学习'
    },
    template:'<div>你好,{{ msg }}</div>'
});
//创建一个vue服务端渲染对象
const renderer = require('vue-server-renderer').createRenderer({
    template:fs.readFileSync('./index.html','utf-8')
})
let htmldata = {
    title:'标题-vue-ssr服务端渲染',
    dyh:"我的渲染内容"
}
server.get('/',(req,res)=>{
    //将vue实例变成字符串内容
    renderer.renderToString(app,htmldata,(err,html)=>{
        if(err) throw err;
        //console.log(html)
        res.end(html)
    })
})
server.listen(3000,()=>{
    console.log('服务器运行在3000端口')
})

//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }}</title>
</head>
<body>
    <!-- 占位,用来展示编译之后的字符串,一定要设置 -->
    <!--vue-ssr-outlet-->
    <p>{{ dyh }}</p>
</body>
</html>

nuxt.js脚手架-后端渲染 *

(1)安装
npm i npx -g
npm create-nuxt-app -g

(2)初始化项目
npx create-nuxt-app 项目名称

(3)运行项目
npm run dev

nuxt初始化项目
react框架 *

react是facebook内部的一个javascript类库,用于构建用户界面的 JavaScript 库
react官网:https://react.docschina.org/
react不是一个完整的MVC框架,最多可以认为是MVC中的V(View)
react引入了虚拟DOM机制
react引入了组件化思想
react使用facebook专门为其开发的一套语法糖--jsx

使用注意事项-jsx语法

注意事项 jsx语法
* 标签内容内属性 例如
*         1 类名 class 和 js class冲突 ==> className
*         2 for ==>htmlFor
* 只能有一个根标签
* 语法要求必须是闭合标签 例如 单标签<imput /> 手动添加 / 闭合
* 注释 {/* 内容 */}
* 遇到 < 解析成 html代码  遇到 { 解析成js代码
* 使用js变量 用 { 变量 }
* 属性绑定 去除后面引号 src=变量

React常见错误

①Uncaught SyntaxError: Inline Babel script: Adjacent JSX elements must be wrapped in an enclosing tag
虚拟DOM中只能有一个根标签

②Warning: Invalid DOM property `class`. Did you mean `className`?
    in div
在react的jsx语法中,class是一个类的关键词,所以标签的class属性要改为className

③Uncaught SyntaxError: Inline Babel script: Unterminated JSX contents
在jsx中,所有的标签都要有闭合,比如input br img hr  要写成`<input /> <br />`

④Warning: Invalid DOM property `for`. Did you mean `htmlFor`?
    in label
    in div
在react的jsx语法中,for是循环的关键词,所以标签的for属性要改为htmlFor

<div id="box"></div>
<script type="text/babel">
    let el = <div className="container">
        <h1>标题1</h1><h2>标题2</h2>
        <label htmlFor="name">用户名:</label>
        <input id="name" type="text" placeholder="请输入用户名" />
    </div>
    ReactDOM.render(el,document.getElementById('box'))
</script>

React优缺点

优点:
    react运行速度快
    跨浏览器兼容
    一切皆组件
    单向数据流
    语法报错提示非常清晰
缺点:
    react不适合做一个完整的框架使用,react本身只是一个视图而已

使用-起步
单页面

第一步:引入react核心库
<!-- 注意: 部署时,将 "development.js" 替换为 "production.min.js"。-->
    <!-- react核心库 dom核心库 -->
    <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
 
第二步:设置一个挂载点
<body>
	 <div id="box"></div>
</body>
 
第三步:渲染虚拟DOM到挂载点
<script>
// 写法一 不方便
// let app = document.querySelector('#app');
// // 创建虚拟Dom
// let h1 = React.createElement('h1', { id: 'myh1' }, 'hhh')
// ReactDOM.render(h1, app)
 
ReactDOM.render(
    React.createElement('h1',{id:'myh1'},
    '这是一个虚拟dom-h1'),document.querySelector('#box')
)
</script>

React中的createElement方法用来创建一个虚拟DOM的函数
这个函数支持三个参数:
1. 要创建的标签名称(只能是系统内置的标签)
2. 要创建的标签的属性(支持内置属性和自定义属性)
3. 标签的内容

ReactDOM中的render方法用来渲染虚拟DOM到真实DOM中的函数
这个函数支持三个参数:
1. 要渲染的虚拟DOM元素
2. 要渲染的位置
3. 回调函数(一般不使用)

jsx的使用

如果要在页面结构中创建一个层级比较深的虚拟DOM结构的话,代码如下:
ReactDOM.render(
     React.createElement('div',null,
         React.createElement('h1',{id:'myh1'},'这是一个h1标题'),
         React.createElement('p',null,
             React.createElement('a',{},
                 React.createElement('span',{},'')
             )
         )
     ),
     document.getElementById('box')
 )

如果虚拟DOM层级比较深/复杂,使用createElement方法就不合适了,所以要使用jsx。
jsx是Javascript和XML的结合,是facebook为react框架开发的一套语法糖
语法糖又叫糖衣语法,是指计算机中添加的某种语法,这种语法对语言的功能并没有影响,而是更加方便程序员使用。
jsx是一种javascript语法的扩展,==>允许js和html进行混写。

jsx基本使用

1、引入babel核心库文件
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

2、修改script的type属性
<script type="text/babel"></script>

3、创建虚拟DOM
let el = <div id="smallbox">
    <h1>标题</h1>
    <p>段落</p>
    <a href="#">小豪</a>
</div>

4、渲染虚拟DOM到挂载点
ReactDOM.render(el,document.getElementById('box'))

数据类型解析

react的jsx语法中,当遇到<就会解析成html语法,当遇到{就会解析成js语法

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>jsx解析数据类型</title>
    <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
    <div id="app"></div>
    <script type="text/babel">
        let name = '小飞'
        let islogin = false//布尔值不要直接进行输出,可以做判断使用
        let obj = { name : '中公优就业' }//对象不要直接输出,可以读取对象中的某个key属性
        //数组元素如果是非对象类型,可以直接输出,但是对象不能直接输出
        let arr = [
            {
                name : '小飞',
                age:18
            },
            {
                name : '小李',
                age:19
            }
        ]
        function randomStr(){
            return Math.random()
        }
        let el = (
            <div>
                { /* 字符串 */ }
                <p>姓名:{ name }</p>
                { /* 数字 */ }
                <p>{ 100 * 90 }</p>
                {/* 布尔值 */}
                <p> { islogin ? '已登录' : '未登录' } </p>
                {/* 对象 */}
                <p>{ obj.name }</p>
                {/* 数组 */}
                <p>{ /*arr*/ }</p>
                { /* 函数 */ }
                <p> { randomStr() } </p>
            </div>
        )
        let app = document.querySelector('#app')
        ReactDOM.render(el,app)
 
    </script>
</body>
</html>

属性绑定

{ /* 属性绑定 */ }
<p style={{ color:'#f00',fontSize:30 }}> { randomStr() } </p>
<img src={img} />

条件判断

//条件判断
let btn = (function(){
    if(islogin){
    	return <a className="login1" href="#">已登录</a>
    }else{
    	return <a className="login2" href="http://www.ujiuye.com">请登录</a>
    }
})()
let el = (
   <div>
   		{btn}
   </div>
)

列表循环

let arr = [
    {
        name : '小飞',
        age:18
    },
    {
        name : '小李',
        age:19
    }
]
let el = (
   <div>
   		<ul>
        {
            arr.map((item,index)=>{
            return (
                <li key={ index }>
                    <p>姓名:{ item.name }</p>
                    <p>年龄:{ item.age }</p>
                </li>
            )
            })
        }
        </ul>
   </div>
)

React脚手架

1、安装
npm i create-react-app -g

2、初始化项目
进入一个指定的目录后执行命令
create-react-app 项目名称

    清空npm缓存
    npm cache clean --force

3、运行项目
进入项目录,执行命令
npm start
浏览器会自动打开,并且项目运行在3000端口上

项目目录结构

项目名称
​    node_modules            项目依赖目录
​    public                            项目执行根目录
​        index.html                项目首页
​        静态资源...
​    src                                项目源码目录
​        App.css                    项目根组件的样式文件
​        App.js                        项目根组件
​        App.test.js                项目根组件测试文件
​        index.css                 项目全局样式表文件
​        index.js                    项目启动文件
​    package.json                项目依赖配置文件

运行流程:
/public/index.html
/src/index.js
/src/App.js
自定义组件

组件
函数式组件

又叫做无状态组件/木偶组件

1格式:
import React from 'react'
let Home = ()=>{
    return(
        <div className="home">
            <h2>这是home组件</h2>
        </div>
    )
}
export default Home;
 
2在App.js中使用定义好的组件
import Home from './home'
 
3以标签的方式使用组件
<Home></Home>
或者
<Home />
 
注意:
在react中,引入组件时,组件的名称必须是==>首字母大写

类组件

又叫做状态组件/业务逻辑组件
在react中,更多的使用组件是类组件。因为类组件,可以有状态、生命周期和逻辑方法函数

格式1:
import React from 'react'
export default class 类名 extends React.Component{
	render(){
		return(
			<div>...</div>
		)
	}
}
 
格式2:
import React,{ Component } from 'react'
export default class 类名 extends Component{
	render(){
		return(
			<div>...</div>
		)
	}
}
 
类组件的类名一般都是首字母大写

状态机

状态是一个核心概念,允许构建可以存储数据的react组件,并根据数据变化自动更新视图页面

使用状态

(1)在构造函数中定义状态
constructor(){
     super();
     this.state = {
     	time : new Date().toLocaleTimeString()
     }
}
render(){
	return (
        <div>
            <h2>clock页面</h2>
            <p>当前时间:{ this.state.time }</p>
        </div>
    )
}
 
(2)直接在类中定义状态
state = {
	time : new Date().toLocaleTimeString()
}
render(){
	return (
        <div>
            <h2>clock页面</h2>
            <p>当前时间:{ this.state.time }</p>
        </div>
    )
}

更新状态

直接修改状态
利用setState方法,来对状态进行修改(不要使用直接赋值的方式,页面不会变化)
setState方法被调用时,react将数据和当前的state进行合并,然后调用render方法更新页面

import React,{ Component } from 'react'
export default class Clock extends Component{
    // constructor(){
    //     super();
    //     this.state = {
    //         time : new Date().toLocaleTimeString()
    //     }
    // }
    state = {
        time : new Date().toLocaleTimeString()
    }
    start(){
        setInterval(()=>{
            // this.state = new Date().toLocaleTimeString();
            // this.render();
            //setState是react中内容一个更改状态的方法,它会自动的重新调用render方法来重新渲染页面
            this.setState({
                time : new Date().toLocaleTimeString()
            })
        },1000)
    }
	//组件挂载完成
    componentDidMount(){
        this.start();
    }
    render(){
        console.log('页面渲染...')
        return (
            <div>
                <h2>clock页面</h2>
                <p>当前时间:{ this.state.time }</p>
            </div>
        )
    }
}

setState方法是一个=异步操作=,在没有执行数据的合并和页面重新渲染时,是无法直接获取到更新之后的数据,如果想要在调用setState之后获取到最新的数据,可以考虑使用setState的第二个参数---回调函数的方式来获取最新的数据

import React from 'react'
export default class Home extends React.Component{
    state = {num:1}
    changeNum(){
        let n = this.state.num;
        n++;
        /**
         * setState 参数
         *  第一个参数是要合并到原来状态的数据
         *  第二个参数[可选]是一个回调函数,用来获取最新的数据
         */ 
        this.setState({num:n},()=>{
            console.log(this.state.num,111111)//在数据合并之后且页面渲染完成后才执行
        });
        console.log(this.state.num,222222)//此时获取到的数据不是最新的
    }
    render(){
        return(
            <div className="home">
                <h2>这是home组件----{ this.state.num }</h2>
                <button onClick={ ()=>this.changeNum() }>点击增加数量</button>
            </div>
        )
    }
    
}

事件绑定
es5方式

<button onClick={ this.函数名称 }>点我</button>
 
不要在jsx语法中给函数添加小括号,否则就会自动执行指定的函数
无法获取到this指向,如果想要保持this指向,需要使用bind进行绑定
 
<button onClick={ this.函数名称.bind(this) }>点我-es5--this</button>
此时,按钮绑定的事件对应的函数中就可以获取到this指向

es6方式

<button onClick={ ()=>this.函数名称() }>点我-es6</button>
es6方式的事件绑定,可以在指定的函数中保持this指向

事件对象
es5方式

<button onClick={ this.handler2 }>点我-es5</button>
es5方式的事件绑定,可以在对应的函数中获取到事件对象
 
handler2(event){
    console.log('handler2')
    console.log(event)
}

es6方式

<button onClick={ ()=>this.handler2() }>点我-es6</button>
 
es6方式,默认在对应的函数中无法获取到事件对象,除非==显式==的传递事件对象
 
<button onClick={ (形参)=>this.handler2(形参) }>点我-es6</button>

参数传递
es5方式

函数代码:
handler3(num){
	console.log(num)
}
handler4(num,str,e){
    console.log(num,'num')
    console.log(str,'str')
    console.log(e)
}
 
<button onClick={ this.函数名称.bind(this,要传递的数据[,..]) }>点我-es5</button>
 
在es5事件绑定中,bind(this),this不是参数,要传递的参数放到this的后面
如果在传递参数时,还想要获取到事件对象,那么就要把事件对象的形参写到所有参数位置的后面(它是最后一个参数,不需要在事件触发时进行传递)
 
<button onClick={ this.handler3.bind(this,100) }>点我-es5-仅传递参数</button>
<button onClick={ this.handler4.bind(this,100,'小飞') }>点我-es5-传参和事件对象</button>

es6方式

<button onClick={ ()=>this.函数名称(数据) }>点我-es6-仅传递参数</button>
<button onClick={ (e)=>this.函数名称(100,'小飞',e) }>点我-es6-传参和事件对象</button>
 
函数代码同es5方式的函数

模拟表单元素双向绑定

由于react中没有vue中的指令系统,所以想要实现表单元素双向绑定,就要结合事件绑定和状态机来实现。
需要给指定的表单元素添加value属性来设置初始值,同时需要给表单元素再设置一个onChange事件(不设置的话它就是一个只读属性,无法进行修改),在onChange中结合事件绑定和setState方法来实现状态的变化并且页面上的内容也进行更新

import React,{ Component } from 'react'
export default class Form extends Component{
    state = {
        name:''
    }
    iptChange(e){
        // console.log(e.target.value)//获取事件对象,标签中的value
        this.setState({
            name:e.target.value
        })
    }
    submit(){
        //获取到用户输入的内容
        console.log(this.state)
    }
    render(){
        return(
            <div>
                <label htmlFor="name">用户名:</label>
                <input 
                    id="name" type="text" 
                    value={ this.state.name }
                    onChange={ (e)=>this.iptChange(e) }
                />
                <p>{ this.state.name }</p>
                <br/>
                <button onClick={ ()=>this.submit() }>提交</button>
            </div>
        )
    }
}

优化升级---页面多个表单元素公用一个实现来改变状态
import React,{ Component } from 'react'
export default class Form extends Component{
    state = {
        name:'',
        phone:''
    }
    iptChange(e,t){
        // console.log(e.target)//获取事件对象,标签中的value
        // this.setState({
        //     name:e.target.value
        // })
        let data = this.state;
        // data[e.target.id] = e.target.value
        data[t] = e.target.value
        this.setState(data)
    }
    submit(){
        //获取到用户输入的内容
        console.log(this.state)
    }
    render(){
        return(
            <div>
                <div>  
                    <label htmlFor="name">用户名:</label>
                    <input 
                        id="name" type="text" 
                        value={ this.state.name }
                        onChange={ (e)=>this.iptChange(e,'name') }
                        autoComplete="off"
                    />
 
                </div>
                <div>
                    <label htmlFor="phone">手机号:</label>
                    <input 
                        id="phone" type="text" 
                        value={ this.state.phone }
                        onChange={ (e)=>this.iptChange(e,'phone') }
                        autoComplete="off"
                    />
                </div>
                
                <br/>
                <button onClick={ ()=>this.submit() }>提交</button>
            </div>
        )
    }
}

React常见错误

1.Warning: The tag <home> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.

在react中,组件名称要以大写字母开头

2.Warning: Failed prop type: You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue. Otherwise, set either onChange or readOnly.

表单元素设置value属性之后,默认就是只读属性,如果想要让它变成可以修改的控件需要设置onChange事件,让这个控件变成受控组件。

React组件通信
父子组件

使用自定义属性和props
react中父子组件通信,和vue中的父子组件通信非常相似,都是在父组件使用子组件时通过自定义属性进行传递数据,然后在子组件中通过props来接收数据

父组件:
import React,{ Component } from 'react'
import Item from './Item'
export default class Home extends Component{
    arr = [111,222,333]
    render(){
        return(
            <div>
                <h1>这是home组件</h1>
                {
                    this.arr.map((d,i)=>{
                        return(
                            <Item key={ i } msg={ d }></Item>
                        )
                    })
                }
            </div>
        )
    }
}

子组件:
	类组件方式:
import React,{ Component } from 'react'
import './Item.css'
export default class Item extends Component{
    render(){
        return(
            <div className="box">
                <p>{ this.props.msg }</p>
            </div>
        )
    }
}
 
函数式组件方式:
import React from 'react'
import './Child.css'
export default (props)=>{
    return(
        <div className="item">
            <img className="img" src={ props.obj.img } alt={ props.obj.title }/>
            <p className="title">{ props.obj.title }</p>
            <p className="zan">{ props.obj.zanNum }</p>
        </div>
    )
}

函数式组件默认没有this,所以不能像类组件通过this.props来接收数据,但是它可以通过函数参数的方式来接收父组件传递的数据

子父组件

react中子父组件通信用自定义事件来实现

父组件:
<Child 
    key={ index } obj={ item } idx={ index }
    addParent={ (n)=>this.addZan(n) }
></Child>
 
addZan(num){
    this.arr[num].zanNum++;
    this.setState({})//调用setState,用来重新渲染页面
}
 
父组件使用子组件时,可以传递一个自定义事件
 
子组件:
import React,{ Component } from 'react'
import './Child.css'
export default class Child extends Component{
    zan(n){
        this.props.addParent(n)//通过props来触发父组件的自定义事件,同时传递参数
    }
    render(){
        return(
            <div className="item">
                <img className="img" src={ this.props.obj.img } alt={ this.props.obj.title }/>
                <p className="title">{ this.props.obj.title }</p>
                <p className="zan">
                    点赞数量:{ this.props.obj.zanNum }
                    <button onClick={ ()=>this.zan(this.props.idx) }>点赞</button>
                </p>
            </div>
        )
    }
}

非父子组件

公用容器和$emit、$on来实现非父子组件通信

创建一个公用的容器

/src/bus.js
/**
 * 创建一个公用的容器,用来实现非父子组件通信
 */
const EventEmitter = require('events')//引入events模块
const MyEvent = new EventEmitter();//实例化eventEmitter
export default MyEvent;

然后在/src/index.js中把公用容器引入并挂到Component原型上
import React,{ Component } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import bus from './bus'//引入公用容器
Component.prototype.$bus = bus;//将公用容器挂载到Component的原型上,这样只要继承了Component类的组件都可以使用此容器

1、在A组件的函数代码中发送数据
this.$bus.emit('事件名','要传递的数据')
2、在其他组件的构造函数中(推荐 组件挂载完成)通过$on来接收数据
constructor(){
    super()
    this.$bus.on('事件名',(要接收的数据)=>{
        ...
    })
}
-------------------------------------------
// 组件挂载完成执行  推荐
componentDidMount(){
     // 监听 自定义事件
     this.$bus.on('send', (str) => {
         this.setState({
             msg: str
         })
     })
 }

生命周期

只有类组件才有生命周期,函数组件没有生命周期

# React生命周期钩子函数
## 页面渲染期
* constructor                       构造函数 在所有函数执行之前它先执行(数据接收 实现继承super(props))
* UNSAFE_componentWillMount         组件将要挂载(数据挂载之前 可以操作数据 不可以操作dom)
* render                            页面渲染(渲染组件 和 html 标签)
* componentDidMount                 组件挂载完成(数据挂载之后 可以操作数据和dom)
 
## 页面更新期
* UNSAFE_componentWillReceiveProps   父组件上状态变化时 会自动触发钩子函数 子组件可以由此决定是否接收数据(接收组件传入输入数据)
 
* shouldComponentUpdate              组件是否要更新数据,需要返回布尔值 true 跟新数据 false 不更新数据(检测组件内的变化 可以用作页面性能的优化(默认值为true))
* UNSAFE_componentWillUpdate         组件更新数据(组件更新之前调用)
* render                             页面从新渲染(组件更新之后渲染组件)
* componentDidUpdate                 组件数据更新完成(组件更新之后调用)
 
## 页面销毁期
* componentWillUnmount               页面销毁(组件销毁时调用 可以做一些内存的优化 [全局变量,闭包,计时器,事件])

DOM节点操作
字符串

import React, { Component } from 'react'
export default class Index extends Component {
    render() {
        return (
            <div>
                <h1 ref='myh1'>这是index页面</h1>
            </div>
        )
    }
    componentDidMount(){
        this.refs.myh1.innerHTML ='refs直接赋值'
    }
}

回调函数

在标签或者组件上的ref属性,除了可以是字符串以外,还是可以写成一个回调函数
回调函数会自动触发,并且不会在this.refs中存在
如果需要让子组件在展示前就获取到父组件传递的最新数据,可以考虑使用回调函数

import React, { Component } from 'react'
import Child from './Child'
export default class Index extends Component {
    render() {
        return (
            <div>
                <Child ref={ (e)=>this.changeChild(e) } />
            </div>
        )
    }
    changeChild(el){
        console.log(el)//此时el就是子组件本身
        el.setState({
            num:500
        })
    }
}

createRef

createRef是react给提供的一个API方法,利用此方法也可以实现通过ref来获取DOM元素或者子组件,从而实现DOM操作或者组件传值操作

import React, { Component } from 'react'
import Child from './Child'
export default class Index extends Component {
    constructor(){
        super()
        this.childEl = React.createRef()//创建一个空的ref对象
    }
    render() {
        return (
            <div>
                <Child ref={ this.childEl }/>
            </div>
        )
    }
    componentDidMount(){
        console.log(this.childEl.current)//此时this.childEl.current就是子组件本身
    }
}

//index.js
import React,{Component} from 'react'
import Child from './child'
export default class Index extends Component{
    constructor(){
        super()
        this.childEl = React.createRef();//创建一个空的ref对象
    }
    
    render(){
        return(
            <div>
                {/* 1 字符串方式修改DOM节点 在组件的refs中存在 */}
                <h1 ref='myh1'>DOM节点操作</h1>
                {/* 2 回调函数方式 不依赖钩子函数 组件挂载完成 自己触发 组件的refs中不存在 */}
                {/* <Child ref={ (el)=>this.changeChild(el) }/> */}
                {/* 3 使用推荐的createRef */}
                <Child ref={ this.childEl }/>
            </div>
        )
    }
    changeChild(e){
        // e 是子组件本身
        console.log(this,e,e.state.num,'changeChild...')
        // e.state.num = 100; 不会从新渲染页面
        // 2 使用回调函数方式
        e.setState({num:100})
    }
    // 挂载完成
    componentDidMount(){
        // refs
        console.log(this,'挂载完成...')
        // 1 字符串方式修改DOM节点
        this.refs.myh1.innerHTML = 'refs直接赋值方式'
        // 3 使用createRef   this.childEl.current
        this.childEl.current.state.num = 500;
    }
}
 
//child.js
import React, { Component } from 'react'
 
export default class child extends Component {
    state = {
        num: 1
    }
    render() {
        return (
            <div>
                <h1>child子组件</h1>
                <p>num:{this.state.num}</p>
            </div>
        )
    }
}

state和props混用

state 一般是给组件本身设置的初始数据(组件可以数据进行任意操作)
props 一般是子组件接收父组件传递的数据(子组件无法修改props)
如果想要根据父组件传递的数据来改变子组件的本身状态时,可以使用props和state混用的方式。
如果子组件的数据依赖于父组件传递的数据,当父组件的数据方式变化时,子组件无法获取到父组件最新的数据

父组件:
import React, { Component } from 'react'
import Item from './Item'
export default class Home extends Component {
    state = {
        num:100
    }
    componentDidMount(){
        this.setState({
            num:50
        })
    }
    render() {
        return (
            <div>
                <p>home组件---{ this.state.num }</p>
                <Item num={ this.state.num }/>
            </div>
        )
    }
}
 
子组件:
import React, { Component } from 'react'
 
export default class Item extends Component {
    state = {
        sum : 200
    }
    componentDidMount(){
    	// 此时,子组件获取到的父组件传递的数据是100,而不是父组件更新之后的50
        this.setState({
            sum:this.props.num+this.state.sum
        })
    }
    render() {
        return (
            <div>
                <h2>结果是:{ this.state.sum }</h2>
            </div>
        )
    }
}

setState方法的参数,可以是一个对象+一个回调函数,还可以是一个函数
这个函数中的第一个参数是当前页面组件的初始状态,第二个参数父组件传递过来的最新数据

子组件:
import React, { Component } from 'react'
export default class Item extends Component {
    state = {
        sum : 200
    }
    componentDidMount(){
        // 此时,子组件获取到的父组件传递的数据是100,而不是父组件更新之后的50
        // this.setState({
        //     sum:this.props.num+this.state.sum
        // })
        this.setState(function(state,props){
            return {
                sum : state.sum + props.num
            }
        })
    }
    render() {
        return (
            <div>
                <h2>结果是:{ this.state.sum }</h2>
            </div>
        )
    }
}

//home.js
import React,{Component} from 'react'
import Item from './item'
export default class Home extends Component{
    state={
        num:100
    }
    componentDidMount(){
        // 挂载完成后,子组件没有收到新数据
        this.setState({
            num:66
        })
    }
    render(){
        return(
            <div>
                <h1>父组件--{ this.state.num }</h1>
                <Item num={ this.state.num }/>
            </div>
        )
    }
}
 
//item.js
import React, { Component } from 'react'
 
export default class Item extends Component {
    state = {
        sum: 300
    }
    // 挂载完成关闭状态
    componentDidMount(){
        // 子组件获取到父组件传递的数据是100 而不是父组件挂载完成后更新的数据66 会发生计算错误
        // this.setState({
        //     sum:this.props.num + this.state.sum
        // })
        // 第一个参数 自身状态 第二个参数 父组件传递的新数据
        this.setState((state,props)=>{
            console.log(state,props,11)
            // 返回一个对象 给setState
            return {
                sum:state.sum + props.num
            }
        })
    }
    render() {
        return (
            <div>
                <h1>item组件</h1>
                <h3>结构:{this.state.sum}</h3>
            </div>
        )
    }
}

react路由【重点】

react也是SPA应用

安装
npm i react-router-dom --save

引入
/src/index.js中,渲染虚拟DOM之前添加引入路由模块代码
//引入路由模块
import { BrowserRouter } from 'react-router-dom'
ReactDOM.render(
    <BrowserRouter><App /></BrowserRouter>
,document.getElementById('root'));

基本使用

创建几个页面组件然后在App.js中引入创建好的页面
引入路由中内置的组件Switch、Route,用来设置路由出口和具体的路由规则
//引入页面组件
...
//引入路由内置组件
import { Switch,Route } from 'react-router-dom'
export default ()=>{
    return(
        <div>
            {/* <Home />
            <Order />
            <Mine /> */}
            {/* 相当于vue中vue-router-view */}
            <Switch>
                {/* Route 是具体的路由规则,需要设置路由关键词和与之对应的组件 */}
                <Route path="/home" component={ Home }></Route>
                <Route path="/order" component={ Order }></Route>
                <Route path="/mine" component={ Mine }></Route>
            </Switch>
        </div>
    )
}
 
此时就可以通过浏览器地址输入不同的关键词来展示不同的页面组件内容了

路由导航
内置组件

Link、NavLink
相同:这两个内置组件都会在页面上生成超链接a标签,都需要设置to属性来告知跳转的链接地址
不同:Link仅在页面生成a标签和href属性,NavLink会在页面上生成a标签的同时会根据当前访问的路由地址来动态的设置激活状态。

编程式导航

如果某个组件不是路由规则的页面组件,而是某个路由规则页面组件的组成部分时,想要使用react-router-dom的路由相关信息时,无法直接使用,想要解决这个问题,需要使用withRouter
import { withRouter } from 'react-router-dom'
class 类名 extends Component {...}
export default withRouter(类名)

此时,被路由规则页面包含的组成部分组件就可以使用react-router-dom相关的信息了
在路由实例中的porps.histroy中有以下方法可以实现页面跳转
push 、replace 、 go(-1) 、 goBack
this.props.history.push('/login')
this.props.history.replace('/login')
this.props.history.go(-1)
this.props.history.goBack()

路由嵌套

需要在哪个页面中展示不同的子级页面,就继续在这个页面中引入Switch、Route路由的内置组件,并定义具体的路由规则

示例代码:
/src/components/views/right.js
import React, { Component } from 'react'
import Student from '../pages/student'
import Course from '../pages/course'
import { Switch,Route } from 'react-router-dom'
export default class right extends Component {
    render() {
        return (
            <div className="right">
                <Switch>
                    <Route path="/index/student" component={ Student }></Route>
                    <Route path="/index/course" component={ Course }></Route>
                </Switch>
            </div>
        )
    }
}

路由重定向

从react-router-dom包中引出Redirect组件
import { Switch,Route,Redirect } from 'react-router-dom'
 
在Switch中定义一个重定向的路由规则
<Switch>
    ...
    <Redirect path="*" to="/index"></Redirect>
</Switch>

路由传参
动态路由

/关键词/:参数名
(1)创建页面组件
(2)配置动态路由规则
<Route exact path="/index/student" component={ Student } />
<Route path="/index/student/:id" component={ StudentInfo } />
exact属性用来标记路由规则精确匹配
(3)在列表页面进行跳转并拼接参数
<button onClick={ ()=>this.props.history.push('/index/student/'+item.id) } className="btn btn-primary">编辑</button>
(4)获取动态路由参数
<p>学生编号:{ this.props.match.params.id }</p>

查询参数

1.定义一个固定的路由规则
<Route path="/index/student/info" component={ StudentInfo }/>
2.页面跳转
toInfo(obj){
    // this.props.history.push('/index/student/'+id)
    this.props.history.push({
        pathname:'/index/student/info',
        search:`id=${obj.id}&name=${obj.name}`
    })
}
3.获取查询参数 ->安装querystring插件
npm i querystring --save
使用
//引入querystring
import querystring from 'querystring'
//获取查询参数
let search = this.props.location.search.substr(1)//获取location中search参数,并去掉问号
let obj = querystring.parse(search)
console.log(obj)

路由模式

在react-router-dom中BrowserRouter是history模式,HashRouter是hash模式

/src/index.js
//引入路由模块
// import { BrowserRouter } from 'react-router-dom'
import { HashRouter } from 'react-router-dom'
ReactDOM.render(
    // <BrowserRouter><App /></BrowserRouter>
    <HashRouter><App /></HashRouter>
,document.getElementById('root'));
更换为hash模式后,就会在浏览器地址中看到一个#号,和vue的hash模式效果相同。

状态管理
flux - 官方

Flux是Facebook用户建立客户端Web应用的前端架构, 它通过利用一个单向的数据流补充了React的组合视图组件,这更是一种模式而非正式框架,你能够无需许多新代码情况下立即开始使用Flux。

流程:
view  视图 触发 action
action 通知 dispatcher(派发器)
dispatcher通知仓库改变状态
store 仓库改变完成后通知视图(view)

安装
npm i flux --save 

创建仓库
在src下创建一个store文件夹,在store下再创建一个index.js文件
/src/store/index.js
 
定义初始数据
let state = {
	name:'flux name',
	age:16
}
 
暴露数据
export default { state }

1普通页面组件使用仓库中的数据
import store from './store'
export default class Home extends Component {
	render() {
        return (
            <div>
                <h2>home页面</h2>
                <h4>仓库中的name为:{ store.state.name }</h4>
            </div>
        )
    }
}
此时,多个普通的页面组件,都可以引入仓库并使用仓库中的数据。
 
2在页面上发起一个动作,改变仓库中的数据
在仓库中通过派发器来注册改变仓库状态的具体方法
/src/store/index.js
//引入派发器
import { Dispatcher } from 'flux'
//引入事件监听器
import EventEmitter  from 'events'
class State extends EventEmitter{
    name = 'flux name'
    age = 16
}
let state = new State();
//实例化派发器
const dispatcher = new Dispatcher();
//通过派发器来派发具体的数据修改操作
dispatcher.register(action=>{
    switch(action.type){
        case 'changeName':
            state.name = action.params;
            break;
        case 'changeAge':
            state.age = action.params;
            break;
        default:
            break;
    }
    state.emit('change')//触发一个事件
})
export default { state,dispatcher }
 
在普通页面中通过仓库派发任务
store.dispatcher.dispatch({type:'changName',params:'你好'})
 
在普通页面组件中的挂载完成钩子函数中来监听仓库数据的变化
componentDidMount(){
    store.state.on('change',()=>{
    	this.setState({})//重新渲染页面
    })
}
 
此时仓库中的数据变化了,页面也会跟着进行重新渲染。

redux - 官方衍生版

Redux由Dan Abramov在2015年创建的科技术语。是受2014年Facebook的Flux架构以及函数式编程语言Elm启发。很快,Redux因其简单易学体积小在短时间内成为最热门的前端架构。

Redux最主要是用作应用状态的管理。简言之,Redux用一个单独的常量状态树(对象)保存这一整个应用的状态,这个对象不能直接被改变。当一些数据变化了,一个新的对象就会被创建(使用actions和reducers)

Redux核心概念有三个:
actions
store
reducers

Redux三个原则:
1. 单一数据源
2. state是只读的
3. 使用纯函数修改state

安装
npm i redux --save

基本语法
流程:
引入createStore
定义初始状态
定义纯函数
定义仓库

实例代码

①引入createStore
/src/store/index.js
import { createStore } from 'redux'
 
②定义初始状态
const initalState = {
    name:'小飞',
    age:18
}
 
③定义纯函数
state 上一次修改完成后的状态
action是组件dipatch的动作
reducer一定要返回一个新的state,否则就检测不到state的变化
function reducer( state = initalState,action ){
    switch(action.type){
        case 'changeName':
            return{
                ...state,
                name:action.params
            }
        case 'changeAge':
            return{
                ...state,
                age:action.params
            }
        default:
            return state;//一定要返回
    }
}
 
④创建仓库
const store = createStore(reducer);
export default store;
 
⑤页面组件使用状态--引入仓库
import store from './store'
 
使用仓库中的数据
<p>仓库中的name为:{ store.getState().name }</p>
 
⑥改变状态
changeName(){
    store.dispatch({
        type:'changeName',
        params:'小芳'
    });
}
 
⑦使用订阅者实现数据变化页面就变化
componentDidMount(){
    //仓库数据变化,想要重新渲染页面,就添加订阅者
    this.unsubscribe = store.subscribe(()=>{
    	this.setState({})
    })
}
componentWillUnmount(){
	this.unsubscribe();//在销毁之前,取消订阅
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

flybirding10011

谢谢支持啊999

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值