这是vue提出的一个独有的概念,组件化编程,组件是什么呢,就是一个包含了代码和资源的集合。
里面有html,css,js,mp3,ttf等文件
一个页面里面只会有一个vue实例,其它的都是用组件,比如header组件,body组件,footer组件,而header组件里可能还有nav组件,search组件,这样的话,你那里需要再用到这个结构,就直接使用这个组件即可。注意这不仅是其它页面可以用这个页面的组件,就是一个页面里面有多个相同结构的也可以直接使用,这就使得代码复用性提高
const school = Vue.extend({
template: `
{{name}}
{{address}}
`,
data(){
return {
name: "xxx",
address: "中国"
}
}
});
这个template就是模板的意思,组件是一个集合,当然有最基本的html,这个html就是写在template里面,而且只能有一个祖宗元素,就比如把这个template的div去掉是不行的
Vue.extend就是一个创建组件的方法,里面的配置项和之前写的vue实例是基本一致的,但是不能写el,因为一个组件如果和一个元素绑定在一起了,那还怎么复用,在哪里使用是要看你在哪里调用
还有一个需要注意的是,data只能写成函数式,如果不写函数式会报错,而不让写函数式的原因就是如果写成对象式,那么你复用这个组件的时候,data里的数据是在同一片内存里的,一改全改,这显然不符合我们的需求,所以要用函数式,每用一次,就给你一个全新的对象,而不是别人在用的对象
有两种注册方式,第一种局部
new Vue({
components:{
school,
}
})
全局
Vue.component("student", student);
这个称为组件标签
<div id = "root">
<school>school>
<student>student>
div>
注册组件的名字在开发者工具里面是大驼峰写法,无论你写的是大写还是小写
多个字母的名字,vue推荐使用大驼峰(需要在脚手架环境下),或者用a-b的方式
使用组件的时候可以使用单标签,需要在脚手架环境下
创建脚手架的时候可以自定义开发者工具里显示的名字,用name配置项,而且这个创建可以简写
const a ={
name: “fdsj”,
template:``
}
要实现一个这样的结果
下面是html结构,一般的标准里会有一个组件是一人之下,万人之上,vm下面只有一个组件,就是app,而app控制着所有的组件
<div id = "root">
<app>app>
div>
vue实例里的组件配置只写了儿子辈,而没有更下一层的
new Vue({
el: "#root",
components:{
app,
}
});
app的下一层里面有testa和testb,所以我们要与vue实例一样直接使用components配置好,然后在template里面使用
const app = {
template: `
`,
components: {
testa,
testb
}
}
以此类推,testa里面的components里写的就是testc
组件的本质其实就是一个构造函数,而我们在使用一个组件的时候,vue帮我实现了new的实例化操作
而我们在使用vue.extend的时候,这个函数就会返回一个构造构造函数,而这两个构造函数也不是一个,因为在extend的内部,实现了一个先用let a = function, return a的结构,这就使得每一次获取的构造函数都不是一个函数
正因为是一个构造函数,所以用同一个组件多次,这些组件都互不干扰,因为他们都是一个个的vueComponent的实例对象
组件的实例对象和vm的属性是一样的
在控制台输出vm
组件以数组的形式存在 c h i l d r e n 这个属性里面,而组件的 children这个属性里面,而组件的 children这个属性里面,而组件的children则是这个组件底下的组件
组件里的this指向的是VueComponent实例对象(vc)
VueComponent.prototype._proto_ === Vue.prototype
需要先了解原型对象知识才能学会,忘了先去复习一下 js遗漏知识的补充
通过原型链的知识,我们可以知道,vc的_proto_指向的就是VueComponent.prototype,vc的prototype的_proto_指向的应该是object,但是vue内部做了一个处理,就是将vc._proto_指向了vm._proto_就是说在vc上找一个东西,没有在vc上找到,就去VueComponent上找,再找不到就去vm_proto_上去找,而vm的原型对象上放了很多东西,data里的数据, m o u n t , mount, mount,watch都在这。
而这种设计也不会破坏原有的Object的方法,因为可以通过Vue.prototype._proto_去找到
前置知识是es6的模块化 es新特性学习笔记
首先创建一个文件,用大驼峰的命名方式,比如我这个文件就叫School.vue
然后就可以在里面放组件的东西了
结构就是这样的,对应的就是html js和css
加上内容,就是这样的
<template>
<div id="demo">
<p>学校名称:{{name}}p>
<p>学校地址:{{address}}p>
<button @click = "showMsg">点我提示信息button>
div>
template>
<script>
const School = {
name: "School",
data() {
return {
name: "xuexiao",
address: "dizhi"
}
},
methods: {
showMsg() {
alert("快来加入我们吧");
}
},
};
export default School;
script>
这个时候我们就用上了es模块化的内容,用暴露,然后我们才能够注册一个组件
我们还可以使用简写形式
export default {
name: "School",
data() {
//...
我们写好了组件之后,就可以写一个Vue实例,这个文件叫main.js
import App from "./App.vue";
new Vue({
el: "#root",
template: ` `,
components:{App},
})
然后再在html的最后引入这个main.js就可以使用了,但是现在还不能使用,需要在特定的环境下面运行这个文件。这也就是下面要学的脚手架
全名: Command Line Interface(CLI)命令行接口工具
脚手架版本:5,尽量用最新的。
全局配置脚手架: npm install -g @vue/cli
有警告不管它
下载慢就使用淘宝镜像: npm config set registry https://registry.npm.taobao.org
在cmd中输入vue没报错则成功
cmd中打开到对应的文件目录, 在此目录下创建项目 vue create NAME
这里是让你选择一个vue的版本,label是es6转es5的工具,eslint是检测语法的
启动项目npm run serve
在创建的时候,就会有一个预设好的项目,当我们启动项目的时候,vue会启动一个服务器,
这个页面就是访问的地址,第一个是本地的,第二个是别人可以访问的,打开后的页面就是下面这样
如需要停止这个服务,只需要使用ctrl+c,然后输入y即可
安装完毕的结构是这样的
src下面的不用在意,是一些npm,git等一些配置文件
点开src和public
public里面是一个图标文件和index.html网页,而src里面则是组件和main.js这个入口文件,还有assets里面放的是用到的图片视频等资源
可以看到App.vue是在components的外面的,而一般的组件就放在components里,main.js
main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
第一行是引入vue
第二行引入App.vue
后面用到了一个render,暂时不用理会,这里使用mount将vm挂载在#app上
在index.html里就有一个这样的东西,那么是如何将App里面的内容放到容器里的呢,没有注册组件和使用template,在html也没有使用App标签。就是靠这个render
<div id="app">div>
打开App.vue,基本结构是一样的,就是那三样,这里没有使用render,而是使用template
回到之前的问题,为什么使用render,而不是使用template的方式,在这里使用template是行不通的
注意到main.js里的import Vue from 'vue'
这个导入第三方库的时候,我们还没有指明文件类型,而vue也不是一个.vue的文件,在node_modules里找到vue的package.json,有这么一项,"module": "dist/vue.runtime.esm.js",
这个就是说我们在使用模块化导入的时候具体的地址是什么,所以我们在导入vue的时候,就相当于导入了vue.runtime.esm.js。
其实还有很多个版本的vue
这些都是基于vue.js的简化版本。而我们导入的那个vue.runtime.esm.js的意思,带有runtime就是没有模板解析器,就是不能使用template,所以我们在main.js 里面会报错,说我们的vue没有模板解析器。
而在.vue的文件里面,脚手架本身就有一个模板解析器,所以在.vue的文件里面我们可以使用template
用多个版本的原因,如果用的是完整版的vue,那么在我们完成项目的时候,这个模板解析器会依旧保存下来, 但是这个时候已经解析完了,不需要模板解析器了,所以会浪费空间,虽然也只是100多kb,但既然这个东西不应该出现在这里,那么就可以进行优化。
render可以理解成渲染,没有模板解析器,也就代表没有第三方帮助我们渲染,所以我们要使用这个内置的函数自己进行渲染。
render(createElement){
return
}
返回值是需要呈现在页面上的元素,而参数就是一个创建元素的函数,里面的参数可以是组件,也可以是标签和内容
比如
render(createElement){
return createElement(App);
//return createElement("h1", "hello world");//标签和内容
}
因为render一般也用不到this,所以我们就直接使用箭头函数进行精简,并且将这个比较长的参数名,换一个短的。这样就出现了默认的那种形式
render: h=>h(App);
默认配置就比如脚手架默认将main.js作为入口文件,如果没有这个文件,我们就不可以成功运行项目,还有就是当我们写了一个没有使用的变量的时候,eslint会进行语法检查,然后报错,也不能运行成功,如果项目还在开发的过程中,这个东西就会很烦,所以我们也可以通过修改默认配置暂时关闭语法检查。
vue修改默认配置的方法比较巧妙,不允许开发人员动核心的配置,不会破坏整体的结构,需要写一个vue.config.js的文件,然后把你需要修改的东西写在里面,当vue发现你写了这个文件的时候,就会把同样的东西给替换,这样做就可以保留原本,如果出错删掉这个文件就可以恢复默认。
而且不是所有的配置项都是能够修改的,只有在vue的CLI官网列出来的那些才能够被修改。
比如之前说的修改代码检查
module.exports = defineConfig({
lintOnSave: false
})
下面是一个App.vue的样例,实现的功能很简单,就是点击按钮输出dom
<template>
<div>
<p id="test">testp>
<button @click = "showDom">点击获取上方DOMbutton>
<student/>
div>
template>
<script>
import Student from "./components/Student.vue";
export default {
name: "App",
components: {
Student,
},
methods: {
showDom() {
console.log(document.getElementById("test"));
}
}
}
script>
使用的是DOM操作,而在vue里提供了ref属性来定位元素
<p ref="test">testp>
console.log(this.$refs.test);
组件里的this指向的是组件实例对象,带有ref属性的元素,都会被组件实例对象收录,存在this.$refs里面,通过值我们可以定位到这些元素
而ref还能做到一个dom比较难做到的事情,就是定位到子组件的实例对象
比如
```
``
这两个写法,然后第一个用dom定位得到的是stundent里的dom元素,而得不到组件实例对象
prop的作用就是父组件向子组件传参,组件本身就是为了复用,有的时候只是数据有点不一样,所以我们可以通过传参的方式,把之前的组件拿来用
父组件使用的时候,就像是写属性一样用键值的方式传参
<student name = "iceylia" age = "50"/>
下面有多种形式的方法引入
这个是最常见的方式,props是与data同级的配置项,这样写就相当于我们在data里面写了一个属性,props在这里的意思也就是属性
data() {
return {
}
},
props:["name", "age"],
引入之后的使用方法与之前无异
因为我们在写父组件的时候,使用的是属性传入的方式,而属性的值只能是字符串,所以传入的也只能是字符串,这就不利于我们操作,当然,我们可以在子组件用强转的方式进行转换,但是最优解决方案应该是在传入的时候就是一个数字类型
<student name = "iceylia" :age = "50"/>
只需要像这样在age前加一个:,这是数据绑定,数据绑定会把里面的内容变为一个表达式,也就是说现在的age就是一个数字类型的数据了
类型限定也就是如果你传入的不是一个指定的类型,虽然依旧能够执行,但是会报错。
写法如下
data() {
return {
}
},
props:{
name: String,
age: Number,
}
data() {
return {
}
},
props:{
name{
type: String,
required: true,
}
age{
type: Number,
required: false,
default: 15
}
}
type 类型
required 是否必须
default 默认值
props一般是不允许修改的,可能产生莫名其妙的错误,如果非要修改,就就用一个类似中间变量修改就可以了
props的优先级高于data,props先被弄到vc里,data后,所以就可以
data() {
return {
tempAge: this.age,
}
},
props:["name", "age"],
修改中间变量,而不修改传进来的值
也可以叫混合,官网上叫混入
作用是如果你在多个组件里用到了同一个配置项,那么我们可以用另一个文件把这些配置项放在一起,起到复用的作用
下面是一个与入口文件同级的mixin.js的文件
export const mixin1 = {
data() {
return {
x: 1,
y: 2,
}
},
}
export const mixin2 = {
mounted() {
console.log("混入成功");
}
}
用分别暴露的方式定义了两个对象,里面写的就是vc的配置项
使用的时候先引入再在配置项里写mixin
import {mixin1} from "../mixin.js";
export default {
//data..
//props//
mixins:[mixin1],
}
如果想要在所有的vc和vm里都有这个配置项,可以直接在入口文件main.js里面写上全局混入
import {mixin2} from "./mixin.js";
Vue.mixin(mixin2);
插件的作用就是把之前写的全局过滤器,自定义指令,全局混入和定义的Vue原型对象的方法都放到插件里,然后在入口文件引入和引入就可以使用了
比如我在main.js 同级下写了一个plugins.js的文件,插件的本质是一个对象,有install函数的对象就是插件,参数是Vue构造函数
export default {
install(Vue){
Vue.prototype.hello = ()=>{console.log("hello")};
Vue.minin({
console.log("混入");
});
}
}
使用
import plugins from "./plugins.js";
Vue.use(plugins);
然后在组件里尝试使用hello函数
<button @click = "hello">hellobutton>
组件里的style最终都会汇总,这就很可能命名冲突,所以我们需要一个局部的样式,
<style scoped>
div{
background-color: grey;
}
style>
当然在App.vue里面写的一般是全局的,不需要加scoped
scoped局部样式实现原理
自动生成了一个data-v的属性,然后在你的局部样式里面都加了一个属性选择器,只有拥有这个属性的元素才会被应用到这一个样式
TodoHeader
TodoList
TodoItem
TodoFooter
其中item是list的子组件
多个数据js中只有两种方式存储,一种是数组一种是对象,这里使用的是数组,然后数组里的元素是对象。里面有id,name和completed
可以暂时存放在list组件中
todos: [
{id: "001", name: "艾希", completed: true},
{id: "002", name: "去月球", completed: false},
{id: "003", name: "王国", completed: true},
]
<MyItem v-for="todo in todos" :key="todo.id" :todo="todo"/>
v-for的指令也可以这样写,:todo是props,向子组件item传参
而在item里,单选框的默认值可以根据completed完成,由于checked是一个动态的属性,所以我们在写的时候需要使用v-bind,如果不写:,那这个todo.completed就是字符串,而不是表达式
<input type="checkbox" :checked="todo.completed"/>
实现输入框功能
想要添加数据,就需要做到接收,处理和传输
数据有id,name和completed,是一个对象,completed可以默认为false
数据有id,name和completed,id我们可以借助一个uuid库的简化版nanoid,uuid是可以生成全球唯一的id,但是体积太大,nanoid则十分轻量
npm i nanoid
nanoid使用的是分别暴露,nanoid是一个函数,直接使用就可以生成一个id,nanoid();
import {nanoid} from "nanoid";
console.log(nanoid());
使用事件监听,keyup.enter
<input type="text" v-model="title" @keyup.enter = "add" placeholder="请输入你的任务名称,按回车键确认"/>
获取内容有两种方式,一种是使用dom操作
add(event) {
console.log(event.target.value);
}
第二种是使用v-model
data中有title这个数据,v-model绑定input,然后读取title就可以读取input.value
这样就获取了全部的数据,组成一个对象即可
兄弟组件直接如何传递数据还没有学,现在的只是做不到,只能使用最基础的去实现
就是将todos的数据放在app组件中,向app传递数据,app再向list和item传
app向list和item传可以使用props,父传子
但是header收集的数据如何传到app是一个问题
这里使用的是一个方法,在app定义一个方法,然后传到header中去使用,header中使用这个方法传的参数,可以在app中接收到
app
data() {
return {
todos: [
{id: "001", name: "艾希", completed: true},
{id: "002", name: "去月球", completed: false},
{id: "003", name: "王国", completed: true},
]
}
},
methods: {
addTodo(todo){
this.todos.unshift(todo);
}
},
<MyHeader :addTodo = "addTodo"/>
header
add() {
if(this.title.trim() === ""){
alert("请输入正确的格式");
return;
}
this.addTodo({id: nanoid(), name: this.title, completed: false})
this.title = "";
}
实现勾选时改变数据
item中这样写也能实现效果,v-model绑定checked,双向数据绑定,勾选则改变todo,completed
<input type="checkbox" v-model="todo.completed"/>
不推荐,改变了props值,之前修改props值会报错,这里不会,因为vue只能检测表层。修改的是对象中的数据,没有修改对象地址,算是漏洞吧。虽然简便,但不推荐使用。
删除的实现方法差不多一样,先在app定义一个deteleTodo的方法
传给item,点击就调用deleteTodo,传id参数,删除参数
deleteTodo具体方法如下
this.todos = this.todos.filter(item => item.id !== id);
把todos传到footer,用插值语法计算全部,用计算属性统计完成
<span>已完成{{completedSum}}span> / 全部{{todos.length}}
computed: {
completedSum(){
return this.todos.reduce((pre, curr)=>pre + (curr.completed ? 1 : 0), 0);
}
}
三个部分
当todos全部勾选的时候,左下角会被自动选上,少了一个就没了。
选上左下角的按钮的时候也能全选
当没有代办事项的时候底部消失
一键清除已完成
判断已完成和全部的数量是否相等即可
上一节已经获取两个数据
<input type="checkbox" :checked="isAll"/>
isAll是计算属性,判断是否相等
想要全选就要在app组件加方法来影响数据
checkAllTodo(completedStatus){
this.todos.forEach(item => {
item.completed = completedStatus;
})
}
传到footer,此时有两种解决方案
第一种用事件监听,监听这个按键,用event.target.checked获取checked属性
第二种巧妙一些,用v-model绑定isAll,当页面修改的时候,就会修改isAll,isAll是计算属性,需要写setter
<input type="checkbox" v-mode="isAll"/>
isAll: {
get(){
return this.totalNum === this.completedSum;
},
set(value){
this.checkAllTodo(value);
}
},
在footer最外面的div加上v-show="todos.length“
app定义事件,footer事件监听,调用事件
clearCompletedTodos(){
this.todos = this.todos.filter(item => {
return !item.completed;
})
}
app里的todos要先本地里读取
todos: JSON.parse(localStorage.getItem("todos")) || [],
JSON.parse是因为对象在存储中都是json形式的字符串,所以我们存储的时候也要是json形式,如果是不用json直接存储的形式
[object Object]
|| []的原因,当初次使用这个网站,还没有todos这个数据的时候,直接读取的结果为null,之前用到的todos.length就会报错,所以在没有的时候就读取空数组
然后是数据的存储
在删除,勾选,添加的时候都需要修改数据,但没必要在每个情况下都写一个函数,只需要使用watch监视todos,在todos修改的时候重新读取即可
watch:{
todos:{
deep: true,
handler(value){
localStorage.setItem("todos", JSON.stringfy(value));
}
}
}
这里用到了深度监视,在勾选的时候,只修改了completed,所以需要深度监视来记录勾选的状态
自定义事件是一种组件间通信方式,适用子传父
父组件使用子组件当标签,那如果要实现子组件触发一个绑定事件的时候,调用的不是子组件自身的方法,而是父组件的方法
有一种方法当然是用props实现,把方法传过去。
第二种就是用自定义事件,在子组件标签写个自定义的事件如
cutom就是一个自定义的事件,那么如何触发这个自定义事件,就是在子组件触发指定的绑定事件的时候加一个触发某事件的api
定义了一个app组件下面有一个Student组件
<Student @click1="demo1"/>
student有一个click1的事件监听,demo1是定义在app中的事件
而在Student组件中
<button @click="function1">按键1button>
有一个按键,绑定了内置事件,当这个按键被按下去的时候就能触发app的click1事件
function1(){
this.$emit("click1");
},
this. e m i t ( " c l i c k 1 " ) 的意思就是触发这个组件实例对象上的 c l i c k 1 , e m i t 本身是发射的意思,在这里可以理解成触发如果要向父组件的事件传递参数,只需要写在 e m i t 里面就可以了 ‘ ‘ t h i s . emit("click1")的意思就是触发这个组件实例对象上的click1,emit本身是发射的意思,在这里可以理解成触发 如果要向父组件的事件传递参数,只需要写在emit里面就可以了``this. emit("click1")的意思就是触发这个组件实例对象上的click1,emit本身是发射的意思,在这里可以理解成触发如果要向父组件的事件传递参数,只需要写在emit里面就可以了‘‘this.emit(“click1”, this.name)``
还有一种绑定自定义事件的方法,第一种是直接在标签上用vue指令绑定自定义事件,第二种是通过拿到组件实例,在组件实例上添加自定义事件
<Student @click1="demo1" ref="student"/>
methods: {
demo1(){
console.log("success");
},
demo2(name){
console.log(name);
}
},
mounted() {
this.$refs.student.$on("click2", this.demo2);
},
主要的思路就是通过refs属性得到student的实例对象,然后在mounted生命周期函数中用$on添加事件,最后在student组件中用emit触发就可以
这种方法虽然麻烦,但是胜在灵活
自定义事件用不上了就需要解绑
和emit一样,放在子组件里面,放在emit下面一行也行,在某个你想要的时机触发解绑
解绑某一个
this.$off("click1");
解绑多个
this.$off(["click2", "click1"];
解绑所有
this.$off();
在组件被销毁的时候。所有的自定义事件,子组件都没了,当然,原生的事件还是会存在
@click.native="demo”
this,$refs.student.$once()
实现任意组件间的通信
window.bus = data
window上的东西所有组件都能获取,直接window.bus就能得到这个数据,当然不推荐在window上放东西。这里的window指代一个任意组件都能访问到的地方,然后我添加了一个属性bus
如果这个bus还能使用$on,$off,$emit
等和自定义事件相关的api,那么就能通过这个实现全局事件总线
比如有两个组件,A和B
B想给A传
A里面可以写
window.bue.$on("test1", function1);
function1(data){
console.log(data);
}
B里面可以写
window.bus.$emit("test2", testData);
就是A给window.bus绑定一个事件,B来触发,带参数的那种。而这个事件的回调本身是在A身上的,A就接收到了参数
如何找到合适的地方放这个bus
组件身上的emit方法实际上是vue的原型对象上的,而根据vue的重要关系,组件的原型对象能够访问到vue的原型对象的数据
所以可以放在vue的原型对象上
为什么不能直接在组件的原型对象上放,因为不同的组件有不同的原型对象
放在上面什么才能调用emit,当然可以放一个组件的实例对象,标准的写法是放vm,vm当然可以使用vue原型的东西,不过也就是简便了一些
上面是组件a,下面是组件B,按B,A能够获取B发送来的姓名,显示在后面
组件实例对象的总线
const temp = Vue.extend({});
Vue.prototype.$bus = new temp();
new Vue()//...
标准做法,使用vm
new Vue({
el: "#app",
render: h=>h(App),
beforeCreate(){
Vue.prototype.$bus = this;
}
})
this就是vm,相当于复制了一份vm到vue原型上作为总线使用
<template>
<div>
<p>姓名:{{userName}}p>
div>
template>
<script>
export default {
name: 'A',
data() {
return {
userName:"",
}
},
methods: {
function1(value){
this.userName = value;
}
},
mounted() {
this.$bus.$on("test", this.function1);
},
beforeDestroy() {
this.$bus.$off("test");
},
}
script>
思路是首先在data中定义到name,然后等到b传来了新数据就更新name
通过生命周期函数,在mounted的时候给总线绑定一个自定义事件,等到组件销毁的时候给总线解绑,否则总线上的自定义事件越来越多
B组件就是一个点击事件,给自定义事件激发并传数据
methods: {
sendMsg(){
this.$bus.$emit("test", this.userName);
},
},
消息订阅是一个所有框架通用的一个概念,同样是用来实现全局的通信,所以消息订阅在vue中使用的少。
消息订阅需要借用第三方库,这里使用pubsub-js
安装
npm i pubsub-js
引入
import pubsub from "pubsub-js";
订阅与取消订阅
mounted(){
this.pid = pubsub.subscribe(msgName, callback(msgName, data));
},
beforeDestroyed(){
pubsub.unsubscribe(this.pid);//类似定时器,需要通过订阅api返回的id取消订阅
}
发送消息
pubsub.publish(msgName, "我是发送的数据data");
this.$nextTick(callback);
在下次更新,以及循环结束之后,延时执行函数。如果在获取数据后立即使用,就会等到dom更新完了,才会执行函数。
这个循环,指的是v-for。有的时候,我们已经获取了数据,然后需要页面更新完毕进行下一步操作,但是v-for还没有更新完,就需要使用nextTick.
axios请求和操作更新后的dom的指令是在同一个函数里面,不会在axios得到数据后就更新,而是会在函数全执行完了才会更新。
如果你没有更新dom就想要操作是不会有结果的。
$nextTick()的作用就是将里面的函数等到页面更新再执行,就相当于写了一个异步任务,加了一个定时器
就比如你想要设计一个编辑模式,需要操作dom,获取焦点,这个时候,而输入框是在点击后出现的,这个时候你需要等到输入框出现后才能获取焦点
在使用swiper的时候,就需要使用nextTick的技巧
进入离开的意思就是一个元素使用v-show或者v-if控制条件渲染,那么在我们
下面是一段动画内容的css,与vue暂时无关,定义关键帧,然后有一个进入和离开的类名
.test-enter-active{
animation: testAnimation 1s;
}
.test-leave-active{
animation: testAnimation 1s reverse;
}
@keyframes testAnimation{
from{
transform: translateX(-100%);
}
to{
transform: translateX(0);
}
}
如果将类名轮换着给同一个元素,就能实现一个元素的出现和离开的动画
vue优化的就是这一段的js代码
<transition name="test">
<h1 v-show="isShow">iceyliah1>
transition>
vue定义了一个新的标签,transition,在解析的时候不会解析这个标签
当标签内容出现,标签会加上test-enter-active,播放其中的动画。在标签内容消失,加上test-leave-active
transition的name属性是与css中的类名的第一个单词对应的,类名的后面部分不能随意更改
<transition name="test" appear>
appear属性能让transition在初始的时候使用出现动画
过度就是不用keyframes实现动画,需要定义四个css类名
/* 进入的起点 */
.test-enter{
transform: translateX(-100%);
}
/* 进入的终点 */
.test-enter-to{
transform: translateX(0);
}
/* 离开的起点 */
.test-leave{
transform: translateX(0);
}
/* 离开的终点 */
.test-leave-to{
transform: translateX(-100%);
}
/* 动画的播放时长和方式 */
.test-enter-active,.test-leave-active{
transition: 1s linear;
}
还需要给这个过度的动画设定一个transition属性,用来定义播放时长和播放方式
transition里不能有多个子元素
需要改为transition-group,每个元素都加上key值
<transition-group name="test2">
<h1 key="1">1h1>
<h1 key="2">2h1>
transition-group>
解决的是一个跨域的问题
数据请求有xhr和对xhr封装的jquery和axios,还有与xhr同级的fetch,但是一般使用的是axios
安装axios
npm i axios
跨域是什么
跨域
浏览器的协议,主机,端口和服务器的协议,服务器,端口有一个不一致就会形成跨域问题,跨域是服务器的数据放在浏览器那里,页面获取不到
解决跨域的方法
代理服务器通常使用nginx开启,还有一种方式是用vue的脚手架
在vue.config.js中写
devServer: {
proxy: "http://localhost:5000"
}
proxy是服务器的地址,在使用的时候,就需要直接向代理服务器发送请求,而不是向服务器,地址改为主机地址
缺点:
其实就是把devServer写完整
devServer: {
proxy: {
"api1":{
target: "http://localhost:5000",//代理目标的基础路径
changeOrigin: true,
pathRewrite: {"^/api1", ""}//将所有/api1在路径中去除
},
"api2": {
target: "...",
}
}
}
将proxy写成对象的形式
用法,请求路径写成http://localhost:8000/api1/data
配置解析
页面上如果有结构相似,只有部分地方不一样的,就可以用插槽,将相同的地方用一个组件,不同的地方放一个占位的插槽,等到使用组件的时候,决定在插槽里面放什么东西
Test组件
<div class="list">
<h1>数据列表h1>
<slot>没填东西的时候,默认显示这行字slot>
div>
slot标签就是一个插槽
填充
<div id="root">
<Test>
<h2>可以是图片h2>
Test>
<Test>
<p>可以是视频p>
Test>
<Test>
Test>
div>
如果想要写多个slot,需要使用name和slot属性进行匹配
<slot name="test">没填东西的时候,默认显示这行字slot>
<Test>
<h2 slot="test">可以是图片h2>
Test>
这样写就可以了,注意的是slot里的名字是写在某个元素的,那如果一个插槽有多个元素,就需要写多个相同的slot吗,可以使用template将所有的元素包裹起来,一起放在插槽的位置即可
如果想要使用插槽原本所在组件的数据,除了使用组件间通信将子组件的数据传给父组件之外,插槽本身就具有这一个功能
出现在slot标签上的属性,都可以出现在填充插槽的元素上
使用方法
<slot name="test" :userName="user">没填东西的时候,默认显示这行字slot>
这里传了一个userName,由于user是一个表达式,所以用:数据绑定
<Test>
<template scope="data" slot="test">
<p>可以是视频{{data.userName}}p>
template>
Test>
必须使用template将所有的填充元素包裹起来,用scope属性接收数据,data这个名字可以随便起,data是一个对象,里面都是传进来的数据,可以当成是这一个组件的数据来使用
vuex也是一个组件间通信的技术,可以称为一个共享的数据库,如果多个组件都需要一个数据,那么就可以放在vuex里面
下面是一个用vue之前的知识写的一个案例,用多个部分控制一个数据
<template>
<div>
<h1>当前求和为{{sum}}h1>
<select v-model.number="n">
<option value="1">1option>
<option value="2">2option>
<option value="3">3option>
select>
<button @click="increment">+button>
<button @click="decrement">-button>
<button @click="incrementOdd">当前求和为奇数再加button>
<button @click="increamentWait">等一等再加button>
div>
template>
<script>
export default {
name: "Count",
data() {
return {
sum: 0,
n: 1,
}
},
methods: {
increment(){
this.sum += this.n;
},
decrement(){
this.sum -= this.n;
},
incrementOdd(){
if(this.sum & 1){
this.sum += this.n;
}
},
increamentWait(){
setTimeout(() => this.sum += this.n, 1000);
}
},
}
script>
<style scoped>
button{
margin-left: 10px;
}
style>
vuex由三部分组成
npm i vuex@3
import Vuex from "vuex";
Vue.use(Vuex);
store与事件总线有些类似,vm和所有的组件都可以访问
而且store是写在vm配置项里面的,但是如果如下所示直接写在配置项中,那vm无法识别store,vm中不会保存store,需要写上面的
new Vue({
el: "#app",
store: "fsf",
render: h=>h(App),
})
这样就能够让所有的组件访问到store,应该叫$store
接下来是配置store
store是写在与main.js同级的store文件夹下的index.js中
import Vuex from "vuex";
const actions = {
};
const mutations = {
};
const state = {
};
export default new Vuex.Store({
actions,
mutations,
state,
});
然后再在main.js中引入store,将之前那个假的store改为真的
import Vue from "vue";
import App from "./App.vue";
import Vuex from "vuex";
import store from "./store";
Vue.config.productionTip = false;
Vue.use(Vuex);
new Vue({
el: "#app",
store,
render: h=>h(App),
})
这个时候会有一个错误,就是说必须在创建store实例之前使用vuex
import的时候,被引入的文件应该是执行完了,但是vue.use(Vuex)放在import store的后面,但是调换位置其实也不行
Vue cli 有一个特性,就是会将import提升,就是无论你写在哪,都会把它们放在最前面,所以这个use只能写在store里面
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const actions = {
};
const mutations = {
};
const state = {
};
export default new Vuex.Store({
actions,
mutations,
state,
});
import Vue from "vue";
import App from "./App.vue";
import store from "./store/index.js";
Vue.config.productionTip = false;
new Vue({
el: "#app",
store,
render: h=>h(App),
})
先将数据sum放在state里面
const state = {
sum: 0,
};
然后把所有更改数据的地方交由vuex管理
methods: {
increment(){
this.$store.dispatch("increment", this.n);
},
decrement(){
this.$store.dispatch("decrement", this.n);
},
incrementOdd(){
this.$store.dispatch("incrementOdd", this.n);
},
increamentWait(){
this.$store.dispatch("incrementWait", this.n);
}
},
actiions是逻辑层,mutations的意思为修改,只修改数据,不涉及逻辑,什么偶数再加,异步任务,全交由actions管理
所以第一个和第二个可以直接用commit交给mutatins修改
increment(){
this.$store.commit("INCREMENT", this.n);
},
然脏在mutations和actions都有对应的函数,为了区分两个函数,mutations里的函数使用大写
mutations里的函数有两个参数,第一个context是一个mini的store,一般需要的数据都在里面,可以当成一个store使用,而value就是传进来的值
const actions = {
increment(context, value){
context.commit("INCREMENT", value);
},
decrement(context, value){
context.commit("DECREMENT", value);
},
incrementOdd(context, value){
if(context.state.sum & 1){
context.commit("INCREMENT", value);
}
},
incrementWait(context, value){
setTimeout(()=>{
context.commit("INCREMENT", value);
}, 1000)
}
};
getters与state平级,也是store里的一个配置项,getters的作用是将state里的数据进行加工,state与getters的关系就像是data与computers的关系一样。
const getters = {
test(state){
return state.sum * 10;
}
};
export default new Vuex.Store({
actions,
mutations,
state,
getters,
});
使用的时候通过this.$store.getter.test访问
我们在写state里的数据的时候,都需要使用$store.state.sum
或者$store.getter.test
,如果想要只写sum和test读取到这些数据,也是有办法的,使用计算属性,一个个来,但是这样也很麻烦,除非你需要写大量的,不然还不如一个个写。
所以我们可以使用mapState和mapGetters来帮助我们生成这些计算属性
本质是代码帮助你写好
引入
import {mapState, mapGetters} from "vuex";
如果你想给这些数据起个别名的话,比如sum在这里叫sum2
computed: {
...mapState({sum2:"sum"}),
}
。。。是扩展运算符,mapState函数返回的是一个对象,将对象里的键值拆分出来
这样的效果相当于
computed: {
sum2(){
return this.$store.state.sum;
}
}
这里第一个的意思就是计算属性的名字,第二个是state中的变量名,如果不加引号,那就是一个变量,当然不对
如果不需要器别名,原生的js是没有这种简写的,毕竟是是字符串作为值
computed: {
...mapState(["sum"]),
数组形式
mapState与mapGetters是简化computed里的代码,而mapActions与mapMutations简化的是methods的方法
还是需要引入,方法如上,还是有两种写法,对象写法和数组写法,写法如上
原版写法
methods: {
increment(){
this.$store.commit("INCREMENT", this.n);
},
decrement(){
this.$store.commit("DECREMENT", this.n);
},
incrementOdd(){
this.$store.dispatch("incrementOdd", this.n);
},
increamentWait(){
this.$store.dispatch("incrementWait", this.n);
}
},
简化版本
methods: {
...mapMutations(["INCREMENT", "DECREMENT"]),
...mapActions(["incrementOdd", "incrementWait"]),
},
这里就有点不一样了,就是简化版本的this.n从哪里来,下面的代码其实不是上面代码的简写,而是需要传参的。
increment(value){
this.$store.commit("INCREMENT", value);
},
传参就在事件绑定的时候传
<button @click="INCREMENT(n)">+button>
比如目前有商品的数据和个人信息的数据存在vuex中,模块化就是将这两类数据分开存放,分开处理,以便于维护
const items = {
state: {},
actions: {},
mutations: {},
getters: {}
}
const person = {
//...
}
export default new Vuex.Store({
modules:{
items,
person,
}
});
将state,actions,mutations和getters分别存放在对应的对象中,items管理的东西也变成了对象
但是数据的读取又有了问题,需要通过this.$stote.items.state.a来获取
如果用mapState,也只能写成..mapState(["items", "person"])
然后就可以使用items.a ,person.a获取数据
如果还要进一步简化
就可以使用命名空间
export default new Vuex.Store({
namespaced: true,
modules:{
items,
person,
}
});
加上命名空间之后,就可以这样写
methods: {
...mapState("person", {a1: "a", b1: "b"});
...mapState("items",{a2: "a", b2: "b"});
}
同理,对于mapActions和mapMutations也需要做同样的设置
不用mapState等
模块化的意义: 能够解决命名冲突,简洁,在store文件夹下,可以将模块化的部分从index.html中分离出来,形成一个新的文件夹,然后导出,在index中引入再使用。
路由与路由器的区别,路由的英文名是route,而路由器的名字为router,路由是一组对应关系。路由器是管理路由的设备,路由器的接口就是一个路由
前端的路由是用来实现spa(single page website application)单页面应用
只有一个index.html,这个页面中的跳转和导航中的跳转都是由路由实现的
比如https://blog.csdn.net/nav/back-end和https://blog.csdn.net/nav/web
这两个网址的区别就在于域名后面的东西不一样,其实这是一个网站,后面的东西不能说是域名,就是通过/back-end关联一个组件,当出现/back-end就呈现对应的组件
前端的路由有些类似插槽
后端的就不一样了
实现这样一个功能,能够通过点击a和b切换文字
注意路径中有一个/#/,这是路由的两种规则导致的
路由是通过一个插件库vue-router 实现的
与vuex一样,需要使用3版本,而不能是4
npm i vue-router@3
需要专门一个文件来写路由规则
router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import A from "../components/A.vue"
import B from "../components/B.vue"
Vue.use(VueRouter);
export default new VueRouter({
routes: [
{
path: "/a",
component:A,
},
{
path: "/b",
component: B,
}
]
})
main.js
import router from "./router";
new Vue({
el: "#app",
router,
render: h=>h(App),
})
这样就配置好路由器了
接下来是按键A和B,这两个按键的实质是a标签,但是router对有路由规则的a有新的标签,router-link
<router-link id="btn1" class="ctr" active-class="active" to="/a">Arouter-link>
<router-link id="btn2" class="ctr" active-class="active" to="/b">Brouter-link>
a变成router-link,href变成to,to的内容就是相对应的路由规则,写在router里的路径
其中active-class也是路由独有的属性,就是链接激活时的样式类名,只需要将写好的类名放上去,就能自动切换样式
最后将组件放在对应的位置,与插值相似,有个占位的
<div>
<router-view>router-view>
div>
注意点:
this.$route
和this.$router
其中route是每个组件的内容都不一样,而router是每个组件的其实都是一个东西,可以用三等得到true导航的内容里还有导航的情况会用到
路由规则
{
path: "/a",
component:A,
children: [
{
path: "c",
component: C,
}
]
},
写在children里,path不用加斜线了
接下来就是link和view的配置,一样的,注意link的to属性需要写全,写成/a/c
向路由组件传参,有的时候不同导航内容的结构一致,只是部分数据不同,需要从父组件获取数据
<router-link to="/a/c?a=test&b=我想吃火锅">Crouter-link>
<h1>我是A组件下的路由组件C{{$route.query.a}}h1>
通过this.$route.query访问传过来的参数,是一个对象类型
但是如果/a/c?a=test&b=我想吃火锅
test是一个变量,放在data里的变量,那么就会在这里被当成是字符串
第一种写法
<router-link :to="`/a/c?a=${test}&b=我想吃火锅`">Crouter-link>
先用:to数据绑定,将内容按js解析,再使用模板字符串,把它们变回字符串,将变量用{}包裹
第二种写法
<router-link
:to="{
path: '/a/c',
query: {
a: test,
},
}">Crouter-link>
就是对象写法
params与query参数作用一样,都是用来传参的
<router-link :to="`/a/c/${test}`">Crouter-link>
/a/c的路径后面/test就是传的参数
需要在命名规则里使用一个占位符,使其成为一个参数,而不是路径的一部分
{
path: "c/:test",
component: C,
}
/:变量名 的形式,网址变成了/#/a/c/我是测试文字的形式
读取方式与query相似,this.$route.params.a
<h1>我是A组件下的路由组件C{{$route.params.a}}h1>
当使用to的对象形式携带params参数时,需要使用name,而不能使用path
路由的props与组件的props差不多是一个东西,路由的props是在路由规则里面传递参数,组件的props配置项接收参数
比如
name: "b",
path: "/b",
component: B,
props: {a: 1, b: 2},
B.vue
export default {
name: "B",
props: ["a", "b"]
}
这样就能在b组件里使用这些数据,但是很没必要,因为这个是写死的
第二种写法
name: "b",
path: "/b",
component: B,
props: true,
会将所有的params参数以props的形式传给组件,简化写法,可以不用this.$route.params.a,this.a就能读取到
但是这种写法不适用于query参数,
第三种写法
函数写法
props($route){
return {a: $route.query.a, b: $route.query.b, }
}
也可以用解构赋值简化写法
props({query}){
return {a: query.a, b: query.b}
}
这样在使用query参数的时候,就很简便,只需要使用a和b而不是this.$route.query.a
路由对历史记录的影响有两种模式,push和replace,
默认就是push,每一次点击导航,都会形成一个历史记录,可以回退到上一条记录里面
replace,替换当前记录
<router-link replace>Arouter-link>
替换当前的历史记录
比如你现在在/#
然后/#/b,/#/a,/#/a/c,可以回退三次到/#
但是我对a开启了replace模式,只能/#/a的时候,/#/b这条记录没了,你在/#/a再回退,就会是/#
当有的时候path特别长,可以使用name参数
{
name: "b",
path: "/b",
component: B,
}
先要在路由规则里配置好name参数
然后再使用的时候,需要搭配对象式的to来使用
<router-link :to="{name:'b'}">Brouter-link>
不借用route-link,也能实现导航,只需要使用一个按钮,再绑定事件即可,这样更为灵活
5个函数,都是$router
上的
methods: {
test1() {
this.$router.push({
path: "/a",
})
},
test2(){
this.$router.replace({
name: "b",
})
},
back(){
this.$router.back();
},
forward(){
this.$router.forward();
},
goto(){
this.$router.go(2);
}
},
路由组件会在切换时被销毁,但是有些时候有些用户输入的东西需要你缓存一下
将router-view包裹在keep-alive里即可
<keep-alive>
<router-view>router-view>
keep-alive>
因为view是导航区的展示区,有多个组件,有的组件我们不需要它缓存
用include属性,将要缓存的组件写在里面,里面的名字就是在写组件的时候的name配置项
<keep-alive include="B">
<router-view>router-view>
keep-alive>
当有多个组件需要缓存的时候,使用数组即可
<keep-alive :include="["A", "B"]">
<router-view>router-view>
keep-alive>
router特有的生命周期钩子
当缓存的组件里有一个定时器的时候,在切换的时候就不会停下,因为没有beforeDestroyed这个函数了,要等到父组件消失的时候才会被销毁
所以有两个新的函数
export default {
name: "B",
props: ["a", "b"],
activated() {
console.log("ji huo");
},
deactivated() {
console.log("2");
},
}
activated是这个页面在眼前的时候
可以将定时器之类放在里面
前置路由守卫就是每次组件切换之前,做一个逻辑,在一定情况下才能访问一个路由组件,比如你是会员,你是管理员
router/index.js
const router = new VueRouter({});
router.beforeEach((to, from, next)=>{
})
export default router;
router.beforeEach在初始化与每次组件切换之前调用,有三个参数
to与from是此次切换的起点与终点
拥有属性如下
next是一个函数,直接调用,表示放行此次切换,能够切换成功
可以据此写一个类似的逻辑
if(to.path === "/a/c"){
if(sss === "fwif"){
next();
}
else{
console.log("缺失访问权限");
}
}
但是当需要判断的地方多,一直绑定path有些麻烦
to与from中有一个meta是路由元信息,可以在里面自定义内容
{
path: "c/:test",
component: C,
meta: {isAuth: false}
}
在里面定义一个bool值的变量,用来标识这个路由组件是否需要判断
后置路由守卫与前置的作用不一样,前置的是在切换前判断你的权限,后置已经切换完了。
router.afterEach()只有两个参数,没有next函数
用来实现一些切换后的逻辑,比如你想在每次切换之后输出一个字,改一下title,如果将这个逻辑放在前置守卫中,需要在有权限的时候才能执行,也就是说需要在每个next前写一遍这个逻辑。
在一个单独的路由规则里面的守卫
{
path: "c/:test",
component: C,
meta: {isAuth: true},
beforeEnter: (to, from, next) => {
// ...
}
}
里面的写法还是和全局的守卫一样,不过我不理解的是为什么要有to这个,写在这里不是都知道是enter那个组件里吗
export default {
name: "C",
beforeRouteEnter (to, from, next) {
// ...
},
beforeRouteLeave (to, from, next) {
// ...
}
}
通过路由规则,切换到这个组件之前和切换到其它组件之前
路由有两种模式,一种是/#/是hash模式/#及其之后的内容都是hash值
hash的作用在于标识后面的内容不是网址,当给服务器发请求时,就不会有后面的内容
另一种模式是history,默认是hash模式,
mode: "history",
routes: []
history就是没有办法分辨出来你哪些是网址,哪些是路由,而且兼容性较差,但是好处在与美观,所以很多地方都会用到history模式
history模式无法分辨的话,就会把后面的内容也发给服务器,所以后端就要设置好对应的服务,当然后端是有办法自动设置好这个的,不需要对应好路径
下面待完善,有点问题
移动端常用UI组件库
PC端常用UI组件库
element ui的使用方式
安装
npm i element-ui -S
//完整引入
//引入element-ui组件库
// import ElementUI from 'element-ui';
//引入element全部样式
// import 'element-ui/lib/theme-chalk/index.css';
//使用element ui插件库
// Vue.use(ElementUI);
//按需引入
import { Button, Input, Row, DatePicker } from 'element-ui';
Vue.use(Button);
Vue.use(Input);
Vue.use(Row);
Vue.use(DatePicker);
按需引入减少打包后的项目体积,使用下面的插件
npm install babel-plugin-component -D
修改babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
官网中的修改方式估计没有更新,不太对,我发现值用加上pulgins的配置即可
不用修改第一个,修改第一个会提示es2015没有找到
做轮播图的时候我喜欢用swiper库,挺好用的