首页
留言
友情链接
Search
1
如何使用JavaScript获取和设置CSS root变量值
830 阅读
2
春和 《江海共余生》
325 阅读
3
清除浮动,单行多行超出用...
295 阅读
4
Centos7 下编译安装php8.2
285 阅读
5
中国历史朝代顺序图
228 阅读
分享
Web前端
html&css
javascript
Vue
shopify
shoplazza
后端
ThinkPHP
YII2
服务器端
软件安装
问题合集
历史
故事
诗词
生活
学习
其他
抖音
快手
小视频
随笔
易经
书摘
登录
/
注册
Search
标签搜索
诗词
sunshine
累计撰写
142
篇文章
累计收到
14
条评论
首页
栏目
分享
Web前端
html&css
javascript
Vue
shopify
shoplazza
后端
ThinkPHP
YII2
服务器端
软件安装
问题合集
历史
故事
诗词
生活
学习
其他
抖音
快手
小视频
随笔
易经
书摘
页面
留言
友情链接
搜索到
10
篇与
的结果
2023-07-18
shopify 增加youtube视频和选项卡
引入相关资源<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.13/lib/theme-chalk/index.css"> <script src="https://cdn.shopify.com/s/files/1/0607/1861/2695/files/vue.min.js"></script> <script src="https://cdn.shopifycdn.net/s/files/1/0136/3119/3188/files/elementui-index.js"></script>sun-tab-video-css style liquid<style> * { margin: 0; padding: 0; box-sizing: border-box; } [v-cloak] { display: none; } @media (min-width: 1200px) { #shopify-section-header-08 .container, #shopify-section-navigation-08 .container { width: 1400px; } } .sun-video { width: 80%; margin: 50px auto; } .sun-video .video-group { display: flex; justify-content: space-between; margin: 10px 0; flex-wrap: wrap; } .sun-video .video-item { width: calc(100% / 3 - 10px); margin-top: 20px; } .sun-video .video-group:after { content: ""; display: block; width: calc(100% / 3 - 10px); height: 0; } .sun-video .video-title { display: -webkit-box; font-size: 20px; font-weight: bold; margin: 30px 0; text-align: center; min-height: 50px; overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; } .sun-video .video-content { width: 100%; text-align: center; } .sun-video .video-content:hover .play-button { background: #f00; } .sun-video .youtube { background-color: #000; margin-bottom: 30px; position: relative; padding-top: 56.25%; overflow: hidden; cursor: pointer; } .sun-video .youtube img { width: 100%; top: 0; left: 0; opacity: 0.8; } .sun-video .youtube img[data-class='default'] { top: -40px; } .sun-video .youtube .play-button { width: 90px; height: 60px; background-color: #000; box-shadow: 0 0 30px rgba(0, 0, 0, 0.6); z-index: 1; opacity: 0.8; border-radius: 6px; } .sun-video .youtube .play-button:before { display:block; content: ""; border-style: solid; border-width: 15px 0 15px 26.0px; border-color: transparent transparent transparent #fff; } .sun-video .youtube img, .sun-video .youtube .play-button { cursor: pointer; } .sun-video .youtube img, .sun-video .youtube iframe, .sun-video .youtube .play-button, .sun-video .youtube .play-button:before { position: absolute; } .sun-video .youtube .play-button, .sun-video .youtube .play-button:before { top: 50%; left: 50%; transform: translate3d(-50%, -50%, 0); display:block; } .sun-video .youtube iframe { height: 100%; width: 100%; top: 0; left: 0; } .sun-video .main-container-img img { width: 100%; } @media (max-width: 1000px) { .sun-video { width: 95%; } .sun-video .video-item { width: 100%; margin: 10px auto; } } @media (min-width: 1600px) { } </style> <style> .el-tabs__nav { width: 100%; } .el-tabs__item { padding: 0; width: calc(100% / {{ section.settings.tab_count | default: 2 }}); text-align: center; /*border-left: 1px solid #ccc;*/ /*border-top: 1px solid #ccc;*/ /*border-right: 1px solid #ccc;*/ /*border-bottom: 0;*/ } .el-tabs__item.is-active { color: #e71c20; } .el-tabs__active-bar { background-color: #e71c20; /*bottom: auto;*/ } .el-tabs__item:hover { color: #e71c20; } .el-tabs--card > .el-tabs__header .el-tabs__item.is-active { border-top: 2px solid #e71c20; } </style> <div class="sun-video" id="sun-video-app" v-cloak> <el-tabs id="sun-video-tab-content" v-model="activeName" @tab-click="handleClick" type="card"> </el-tabs> </div>html section {% if customer.id or true %} {% if section.settings.render_first %} {% render 'sun-tab-video-css' %} {% endif %} {% if section.settings.tab_title == blank %} <style> .el-tabs--card>.el-tabs__header{display:none;} </style> {% endif %} <div id="{{ section.id }}" style="display:none;"> {% if section.settings.tab_title %} <el-tab-pane label="{{ section.settings.tab_title }}" name="{% if section.settings.render_first %}first{% else %} {{ section.id }}{% endif %}"> {% endif %} <div class="video-group"> {% for block in section.blocks %} <div class="video-item"> <p class="video-title">{{ block.settings.title }}</p> <div class="video-content"> <div class="youtube" data-embed="{{ block.settings.embed_id }}" data-max="{{ block.settings.max }}"> <div class="play-button"></div> </div> </div> </div> {% endfor %} </div> {% if section.settings.tab_title %} </el-tab-pane> {% endif %} </div> <script> window.sunshine.tab_html = $('#{{ section.id }}').html(); $("#sun-video-tab-content").append(window.sunshine.tab_html) </script> {% if section.settings.render_last %} {% render 'sun-tab-video-js' %} {% endif %} {% endif %} {% schema %} { "name": "Sun Tab Video", "settings": [ { "type":"checkbox", "label":"Render First", "id":"render_first", "default": false }, { "type":"checkbox", "label":"Render Last", "id":"render_last", "default": false }, { "type":"number", "label":"Tab Count", "id":"tab_count" }, { "type":"text", "label":"Tab Title", "id":"tab_title" } ], "blocks":[ { "type":"text", "name":"Tab Item", "settings": [ { "type":"text", "id":"title", "label":"Item Title" }, { "type":"text", "id":"embed_id", "label":"Youtube Embed Id" }, { "type":"checkbox", "id":"max", "label":"Thumb Image Max?", "default":true } ] } ], "presets":[ { "name":"Sun Tab Video" } ] } {% endschema %}sun-tab-video-js <script> new Vue({ el: "#sun-video-app", delimiters: ['${', '}'], data: function () { return { activeName: 'first' } }, methods: { handleChange(val) { // console.log(val); }, handleClick(tab, event) { // console.log(tab, event); }, btn_submit() { // console.log('submit') } }, created() { }, computed: {} }) </script> <script> var youtube = document.querySelectorAll(".youtube"); // loop for (var i = 0; i < youtube.length; i++) { var source = ''; var img_class = ''; // thumbnail image source. if (youtube[i].dataset.max == 'true') { source = "https://img.youtube.com/vi/" + youtube[i].dataset.embed + "/maxresdefault.jpg"; img_class = 'max'; } else { source = "https://img.youtube.com/vi/" + youtube[i].dataset.embed + "/0.jpg"; img_class = 'default'; } var image = new Image(); image.src = source; image.setAttribute('data-class', img_class); image.addEventListener("load", function () { youtube[i].appendChild(image); }(i)); youtube[i].addEventListener("click", function () { var iframe = document.createElement("iframe"); iframe.setAttribute("frameborder", "0"); iframe.setAttribute("allowfullscreen", ""); iframe.setAttribute("src", "https://www.youtube.com/embed/" + this.dataset.embed + "?rel=0&autoplay=1&showinfo=1"); this.innerHTML = ""; this.appendChild(iframe); }); } </script>
2023年07月18日
33 阅读
0 评论
2 点赞
2022-12-24
Vue3 学习笔记(七)
02-详解接口与类型别名之间区别03-字面量类型和keyof关键字04-类型保护与自定义类型保护05-定义泛型和泛型常见操作06-类型兼容性详解07-映射类型与内置工具类型08-条件类型和infer关键字09-类中如何使用类型详解接口与类型别名之间区别接口接口是一系列抽象方法的声明,是一些方法特征的集合。简单来说,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。接口跟类型别名类似都是用来定义类型注解的,接口是用interface关键字来实现的,如下:interface A { username: string; age: number; } let a: A = { username: 'xiaoming', age: 20 }因为接口跟类型别名功能类似,所以接口也具备像索引签名,可调用注解等功能。interface A { [index: number]: number; } let a: A = [1, 2, 3]; interface A { (): void; } let a: A = () => {}接口与别名的区别那么接口跟类型别名除了很多功能相似外,他们之间也是具备某些区别的。对象类型接口合并接口继承映射类型第一个区别,类型别名可以操作任意类型,而接口只能操作对象类型。第二个区别,接口可以进行合并操作。interface A { username: string; } interface A { age: number; } let a: A = { username: 'xiaoming', age: 20 }第三个区别,接口具备继承能力。interface A { username: string } interface B extends A { age: number } let b: B = { username: 'xiaoming', age: 20 }B这个接口继承了A接口,所以B类型就有了username这个属性。在指定类型的时候,b变量要求同时具备A类型和B类型。第四个区别,接口不具备定义成接口的映射类型,而别名是可以做成映射类型的,关于映射类型的用法后面小节中会详细的进行讲解,这里先看一下效果。type A = { // success [P in 'username'|'age']: string; } interface A { // error [P in 'username'|'age']: string; }字面量类型和keyof关键字字面量类型在TS中可以把字面量作为具体的类型来使用,当使用字面量作为具体类型时, 该类型的取值就必须是该字面量的值。type A = 1; let a: A = 1;这里的A对应一个1这样的值,所以A类型就是字面量类型,那么a变量就只能选择1作为可选的值,除了1作为值以外,那么其他值都不能赋值给a变量。那么字面量类型到底有什么作用呢?实际上字面量类型可以把类型进行缩小,只在指定的范围内生效,这样可以保证值不易写错。type A = 'linear'|'swing'; let a: A = 'ease' // error比如a变量,只有两个选择,要么是linear要么是swing,不能是其他的第三个值作为选项存在。keyof关键字在一个定义好的接口中,想把接口中的每一个属性提取出来,形成一个联合的字面量类型,那么就可以利用keyof关键字来实现。interface A { username: string; age: number; } //keyof A -> 'username'|'age' let a: keyof A = 'username';如果我们利用typeof语法去引用一个变量,可以得到这个变量所对应的类型,如下:let a = 'hello'; type A = typeof a; // string 那么利用这样一个特性,可以通过一个对象得到对应的字面量类型,把typeof和keyof两个关键字结合使用。let obj: { username: 'xiaoming', age: 20 } let a: keyof typeof obj = 'username'类型保护与自定义类型保护类型保护类型保护允许你使用更小范围下的对象类型。这样可以缩小类型的范围保证类型的正确性,防止TS报错。这段代码在没有类型保护的情况下就会报错,如下:function foo(n: string|number){ n.length // error }因为n有可能是number,所以TS会进行错误提示,可以利用类型断言来解决,但是这种方式只是欺骗TS,如果在运行阶段还是可能报错的,所以并不是最好的方式。利用类型保护可以更好的解决这个问题。类型保护的方式有很多种,主要是四种方式:typeof关键字instanceof关键字in关键字字面量类型typeof关键字实现类型保护:function foo(n: string|number){ if(typeof n === 'string'){ n.length // success } }instanceof关键字实现类型保护,主要是针对类进行保护的:class Foo { username = 'xiaoming' } class Bar { age = 20 } function baz(n: Foo|Bar){ if( n instanceof Foo ){ n.username } }in关键字实现类型保护,主要是针对对象的属性保护的:function foo(n: { username: string } | { age: number }){ if( 'username' in n ){ n.username } }字面量类型保护,如下:function foo(n: 'username'|123){ if( n === 'username' ){ n.length } }自定义类型保护除了以上四种方式可以做类型保护外,如果我们想自己去实现类型保护可行吗?答案是可以的,只需要利用is关键字即可, is为类型谓词,它可以做到类型保护。function isString(n: any): n is string{ return typeof n === 'string'; } function foo(n: string|number){ if( isString(n) ){ n.length } }定义泛型和泛型常见操作定义泛型泛型是指在定义函数、接口或者类时,未指定其参数类型,只有在运行时传入才能确定。泛型简单来说就是对类型进行传参处理。type A<T = string> = T //泛型默认值 let a: A = 'hello' let b: A<number> = 123 let c: A<boolean> = true这里可以看到通过<T>来定义泛型,还可以给泛型添加默认值<T=string>,这样当我们不传递类型的时候,就会已string作为默认的类型进行使用。泛型还可以传递多个,实现多泛型的写法。type A<T, U> = T|U; //多泛型在前面我们学习数组的时候,讲过数组有两种定义方式,除了基本定义外,还有一种泛型的写法,如下:let arr: Array<number> = [1, 2, 3]; //自定义MyArray实现 type MyArray<T> = T[]; let arr2: MyArray<number> = [1, 2, 3];泛型在函数中的使用:function foo<T>(n: T){ } foo<string>('hello'); foo(123); // 泛型会自动类型推断泛型跟接口结合的用法:interface A<T> { (n?: T): void default?: T } let foo: A<string> = (n) => {} let foo2: A<number> = (n) => {} foo('hello') foo.default = 'hi' foo2(123) foo2.default = 123泛型与类结合的用法:class Foo<T> { username!: T; } let f = new Foo<string>(); f.username = 'hello'; class Foo<T> { username!: T } class Baz extends Foo<string> {} let f = new Baz() f.username = 'hello'有时候也会对泛型进行约束,可以指定哪些类型才能进行传递:type A = { length: number } function foo<T extends A>(n: T) {} foo(123) // error foo('hello')通过extends关键字可以完成泛型约束处理。类型兼容性详解类型兼容性类型兼容性用于确定一个类型是否能赋值给其他类型。如果是相同的类型是可以进行赋值的,如果是不同的类型就不能进行赋值操作。let a: number = 123; let b: number = 456; b = a; // success let a: number = 123; let b: string = 'hello'; b = a; // error当有类型包含的情况下,又是如何处理的呢?let a: number = 123; let b: string | number = 'hello'; //b = a; // success a = b; // error变量a是可以赋值给变量b的,但是变量b是不能赋值给变量a的,因为b的类型包含a的类型,所以a赋值给b是可以的。在对象类型中也是一样的处理方式,代码如下:let a: {username: string} = { username: 'xiaoming' }; let b: {username: string; age: number} = { username: 'xiaoming', age: 20 }; a = b; // success b = a; // errorb的类型满足a的类型,所以b是可以赋值给a的,但是a的类型不能满足b的类型,所以a不能赋值给b。所以看下面的例子就明白为什么这样操作是可以的。function foo(n: { username: string }) {} foo({ username: 'xiaoming' }) // success foo({ username: 'xiaoming', age: 20 }) // error let a = { username: 'xiaoming', age: 20 } foo(a) // success这里把值存成一个变量a,再去进行传参就是利用了类型兼容性做到的。映射类型与内置工具类型映射类型可以将已知类型的每个属性都变为可选的或者只读的。简单来说就是可以从一种类型映射出另一种类型。这里我们先要明确一点,映射类型只能用类型别名去实现,不能使用接口的方式来实现。先看一下在TS中是如何定义一个映射类型的。type A = { username: string age: number } type B<T> = { [P in keyof T]: T[P] } type C = B<A>这段代码中类型C与类型A是完全一样的,其中in关键字就类似于一个for in循环,可以处理A类型中的所有属性记做p,然后就可以得到对应的类型T[p]。那么我们就可以通过添加一些其他语法来实现不同的类型出来,例如让每一个属性都是只读的,可以给每一项前面添加readonly关键字。type B<T> = { readonly [P in keyof T]: T[P] }内置工具类型每次我们去实现这种映射类型的功能是非常麻烦的,所以TS中给我们提供了很多常见的映射类型,这些内置的映射类型被叫做,内置工具类型。Readonly就是跟我们上边实现的映射类型是一样的功能,给每一个属性做成只读的。type A = { username: string age: number } /* type B = { readonly username: string; readonly age: number; } */ type B = Readonly<A>Partial可以把每一个属性变成可选的。type A = { username: string age: number } /* type B = { username?: string|undefined; age?: number|undefined; } */ type B = Partial<A>Pick可以把某些指定的属性给筛选出来。type A = { username: string age: number gender: string } /* type D = { username: string; age: number; } */ type D = Pick<A, 'username'|'age'>Record可以把字面量类型指定为统一的类型。/* type E = { username: string; age: string; } */ type E = Record<'username'|'age', string>Required可以把对象的每一个属性变成必选项。type A = { username?: string age?: number } /* type B = { username: string; age: number; } */ type B = Required<A>Omit是跟Pick工具类相反的操作,把指定的属性进行排除。type A = { username: string age: number gender: string } /* type D = { gender: string } */ type D = Omit<A, 'username'|'age'>Exclude可以排除某些类型,得到剩余的类型。// type A = number type A = Exclude<string | number | boolean, string | boolean>我们的内置工具类型还有一些,如:Extract、NonNullable、Parameters、ReturnType等,下一个小节中将继续学习剩余的工具类型。条件类型和infer关键字在上一个小节中,学习了Exclude这个工具类型,那么它的底层实现原理是怎样的呢?type Exclude<T, U> = T extends U ? never : T;这里可以看到Exclude利用了 ? : 的写法来实现的,这种写法在TS类型中表示条件类型,让我们一起来了解下吧。条件类型条件类型就是在初始状态并不直接确定具体类型,而是通过一定的类型运算得到最终的变量类型。type A = string type B = number | string type C = A extends B ? {} : []条件类型需要使用extends关键字,如果A类型继承B类型,那么C类型得到问号后面的类型,如果A类型没有继承B类型,那么C类型得到冒号后面的类型,当无法确定A是否继承B的时候,则返回两个类型的联合类型。那么大多数情况下,条件类型还是在内置工具类型中用的比较多,就像上面的Exclude方法,下面就让我们一起看一下其他内置工具类型该如何去用吧。Extract跟Exclude正好相反,得到需要筛选的类型。// type Extract<T, U> = T extends U ? T : never -> 实现原理 // type A = string type A = Extract<string | number | boolean, string>NonNullable用于排除null和undefined这些类型。//type NonNullable<T> = T extends null | undefined ? never : T; -> 实现原理 //type A = string type A = NonNullable<string|null|undefined>Parameters可以把函数的参数转成对应的元组类型。type Foo = (n: number, m: string) => string //type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never; -> 实现原理 // type A = [n: number, m: string] type A = Parameters<Foo> 在Parameters方法的实现原理中,出现了一个infer关键字,它主要是用于在程序中对类型进行定义,通过得到定义的p类型来决定最终要的结果。ReturnType可以把函数的返回值提取出类型。type Foo = (n: number, m: string) => string //type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; -> 实现原理 //type A = string type A = ReturnType<Foo>这里也通过infer关键字定义了一个R类型,对应的就是函数返回值的类型。通过infer关键字可以在泛型之外也可以定义类型出来。下面再利用infer来实现一个小功能,定义一个类型方法,当传递一个数组的时候返回子项的类型,当传递一个基本类型的时候就返回这个基本类型。type A<T> = T extends Array<infer U> ? U : T // type B = number type B = A<Array<number>> // type C = string type C = A<string>这里的U就是自动推断出的数组里的子元素类型,那么就可以完成我们的需求。类中如何使用类型本小节主要讲解在类中如何使用TS的类型,对于类的一些功能使用方式,例如:类的修饰符、混入、装饰器、抽象类等等并不做过多的介绍。类中定义类型属性必须给初始值,如果不给初始值可通过非空断言来解决。class Foo { username!: string; }给初始值的写法如下:class Foo { //第一种写法 //username: string = 'xiaoming'; //第二种写法 // username: string; // constructor(){ // this.username = 'xiaoming'; // } //第三种写法 username: string; constructor(username: string){ this.username = username; } }类中定义方法及添加类型也是非常简单的。class Foo { ... showAge = (n: number): number => { return n; } }类使用接口类中使用接口,是需要使用implements关键字。interface A { username: string age: number showName(n: string): string } class Foo implements A { username: string = 'xiaoming' age: number = 20 gender: string = 'male' showName = (n: string): string => { return n } }在类中使用接口的时候,是一种类型兼容性的方式,对于少的字段是不行的,但是对于多出来的字段是没有问题的,比如说gender字段。类使用泛型class Foo<T> { username: T; constructor(username: T){ this.username = username; } } new Foo<string>('xiaoming');继承中用的也比较多。class Foo<T> { username: T; constructor(username: T){ this.username = username; } } class Bar extends Foo<string> { }最后来看一下,类中去结合接口与泛型的方式。interface A<T> { username: T age: number showName(n: T): T } class Foo implements A<string> { username: string = 'xiaoming' age: number = 20 gender: string = 'male' showName = (n: string): string => { return n } }了解类型中接口与类型别名之间的区别,及各种使用方式掌握什么是泛型与泛型的应用场景,以及与其他语法的结合使用了解了类型的一些高级用法,如:类型保护、类型兼容性了解了类型的一些高级语法,如:映射类型、条件类型全面学习TS中内置的工具类型,如: Partial、Readonly …等
2022年12月24日
31 阅读
0 评论
0 点赞
2022-12-24
Vue3 学习笔记(六)
为什么使用TS和TS运行环境搭建TypeScript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成。TypeScript通过TypeScript编译器或Babel转译为JavaScript代码,可运行在任何浏览器,任何操作系统。TS文件需要编译成JS文件首先我们的浏览器是不认识TS文件的,所以需要把TS编译成JS文件才可以,TS官网提供了一种方式,就是去全局安装typescript这个模块,命令如下:npm install -g typescript安装好后,就可以使用 tsc xxx.ts命名把TS文件自动转化成对应的JS文件了,在同级目录下就会生成一个xxx.js的文件,这个文件就是编译后的文件,浏览器是可以认识的。在TS编译JS文件的时候,会有一些细节,我们一起来看一下:第一,在tsc命令进行转换操作的时候,是不能实时进行转换的,那么可以通过添加一个-w的参数来完成实时转换的操作tsc xxx.ts -w第二,在编译后,我们会发现TS文件中定义的变量会产生错误的波浪线,这主要是因为TS默认是全局环境下的,所以跟其他文件中的同名变量冲突了,那么该如何解决呢?可以把全局环境改成局部的环境,只需要把TS文件变成一个模块化的文件,那么变量就只在模块内起作用,这样就不会产生冲突,从而引发错误的波浪线。let foo = 123; export {};第三,如果不想采用默认的编译方式,那么可以通过修改配置文件来改变一些默认的行为。配置文件需要叫做,tsconfig.json。可以通过tsc --init命令自动化的创建tsconfig.json这个文件,这个文件中的配置选项非常的多,我们会在后面小节中给大家进行详解的讲解,这里先修改其中的一两个配置,来感受一下配置文件的一个作用。{ “compilerOptions”: { "outDir": "./dist", //编译后文件输出的位置 "module": "ES6", //转换后模块的风格 "target": "ES5" //转换成JS不同语法版本 }, "include": ["xxx.ts"] //只对那些文件进行编译 }通过命令行输入tsc命令就可以自动去调用tsconfig.json这个文件了,非常的方便。为什么要使用TS去写程序TS编写代码的好处诸多,下面我们列出几点:友好的IDE提示强制类型,防止报错语言编写更加严谨快速查找到拼写错误JS的超集,扩展新功能下面通过几个小例子来感受一下TS比JS要优秀的点,let a = 123; a.map(()=>{}) // error这段代码在TS中是会报错的,因为a是一个数字,不会有map方法,而JS是不会给我们进行提示的。let a = 1; if(a === 1 && a === 2) { // error }这段代码在TS中是会报错的,因为从逻辑上来看这段代码并没有意义,因为a同一时间不可能既是1又是2。let a = { username: 'xiaoming', age: 20 }这段代码TS提示会非常的好,不会夹杂一些其他的提示信息,而且一旦单词写错了,TS会有非常好的提示效果。类型声明空间与变量声明空间变量声明空间在JS中我们只有变量声明空间,比如let a = 123。但是在TS中我们不仅存在变量声明空间,而且还存在类型声明空间。下面就让我们一起去探索一下类型声明空间的特点吧。类型声明空间可以通过type关键字来定义类型声明空间,type在TS中叫做类型别名。为了能够更好的区分两个空间,所以人为规定类型空间定义的名字首字母要大写,例如:type A = number。这里要注意,不要把两个空间进行互相赋值,会产生错误的,如下: let a = 'hello'; type B = a; // error type A = string; let a = A; // error但是在TS中,有的语法它既是变量声明空间也是类型声明空间,比如:类语法。class Foo {} // 类在TS中既是变量声明空间,也是类型声明空间 let a = Foo; // success type A = Foo; // success那么两个空间之间的关系有是什么呢?下一小节来进行学习。类型注解与类型推断上一个小节中,给大家留了一个思考题,那就是如何把两个空间联系到一起?主要采用类型注解来实现的。类型注解通过把变量空间与类型空间结合在一起的操作,就叫做类型注解,具体语法是通过冒号连接在一起的。let a: string = 'hello' //也可以通过类型别名进行连接 type A = string let a: A = 'hello'这样定义的a变量就具备了字符串类型,那么我们尝试给a修改成其他类型就会报错,现在就强制a的类型为字符串了,这也是TS强制类型的特点。那么我们不去进行类型注解,是不是就不会强制类型了呢?其实也会强制类型的,因为TS可以进行自动类型推断的。类型推断TS中会对不进行类型注解的变量,进行自动类型推断的,如下:// let a: string -> 类型推断 let a = 'hello' a = 123; // error所以在TS中不管是类型注解还是自动类型推断,对于类型都不能轻易的做修改,这样可以保证变量的类型稳定,从而减少BUG的产生。类型分类与联合类型与交叉类型类型分类在TS中会把类型大致划分为三部分:基本类型:string number boolean null undefined symbol bigint对象类型:[] {} function(){}TS新增类型:any never void unknown enum对于基本类型,我们就稍微演示一下,毕竟都是JS已有的内容:let a: string = 'hello' let b: bigint = 1n;对象类型也稍微演示一下,更具体的对象类型会在后面的小节中进行讲解:let c: object = []; c = function(){}; // success联合类型在TS中如何能够让一个变量支持多种不同类型呢?那么就可以采用联合类型来实现。let a: string|number|boolean = 'hello'; a = 123;在类型中除了有或的操作外,就一定有与的操作,那么在TS中实现类型与的行为叫做,交叉类型。交叉类型类型之间进行与的操作,不过一般在对象中进行使用,基本类型很少采用交叉类型。type A = { username: string } type B = { age: number } let a: A&B = { username: 'xiaoming', age: 20 }这里a变量,必须具备A、B两个共同指定的类型才可以。never类型与any类型与unknown类型在本小节中将学习一些TS中提供的新的类型,比如:never类型,any类型,unknown类型。never类型never类型表示永不存在的值的类型,当一个值不存在的时候就会被自动类型推断成never类型。// let a: never -> 不能将类型“number”分配给类型“never” let a: number & string = 123在这段代码中a变量要求类型既是数字又是字符串,而值是一个123无法满足类型的需求,所以a会被自动推断成never类型。所以never类型并不常用,只是在出现问题的时候会被自动转成never。有时候也可以利用never类型的特点,实现一些小技巧应用,例如可以实现判断参数是否都已被使用,代码如下:function foo(n: 1 | 2 | 3) { switch (n) { case 1: break case 2: break case 3: break default: let m: never = n; // 检测n是否可以走到这里,看所有值是否全部被使用到 break } }any类型和unknown类型any类型表示任意类型,而unknown类型表示为未知类型,是any类型对应的安全类型。既然any表示任意类型,那么定义的变量可以随意修改其类型,这样带来的问题就是TS不再进行类型强制,整个使用方式根JS没有任何区别。let a: any = 'hello'; a = 123; a = true; a.map(()=>{}) // success所以说any类型是TS中的后门,不到万不得已的时候尽量要少用,如果真的有这种需求的话,可以采用any对应的安全类型unknown来进行定义。let a: unknown = 'hello'; a = 123; // any不进行检测了,unknown使用的时候,TS默认会进行检测 a.map(()=>{}) // errorunknown类型让程序使用的时候更加严谨,我们必须主动告诉TS,这里是一个什么类型,防止我们产生误操作。那么怎样让unknown类型不产生错误呢?就需要配合类型断言去使用,下一个小节我们一起来学习吧。类型断言与非空断言类型断言类型断言主要用于当 TypeScript 推断出来类型并不满足你的需求,你需要手动指定一个类型。在上一个小节中使用unknown类型的时候,需要手动指定当前是一个什么类型,来满足TS类型的需求检测,那么就可以采用类型断言来实现。let a: unknown = 'hello'; a = 123; (a as []).map(()=>{}) // success这里就不会再报错了,当然类型断言只是告诉TS不要管我们了,这个只是在编译前的处理,就是欺骗TS。而在编译后,a确实不是一个数组,所以运行到浏览器后就会报错,那么我们在使用断言的时候还是要格外的小心,不要滥用断言操作。非空断言在类型断言中,也有一些特殊情况,如下:let b: string|undefined = undefined; b.length // error因为b可能是字符串也可能是undefined,所以b.length的时候就会报错,这样我们可以采用非空断言来告诉TS,这个b肯定不是undefined,所以b只能是字符串,那么b.length就不会报错了。let b: string|undefined = undefined; b!.length // success总结:类型断言是一种欺骗TS的手段,在编译阶段解决类型问题的,但是最终运行的结果需要开发人员自己负责,所以使用类型断言要严谨,否则最终还会产生报错。数组类型与元组类型在前面小节中介绍过对象类型,但是对象类型的范围太广了,本小节将具体讲解对象类型中的数组类型及元组类型。数组类型定义数组类型通常用两种写法:类型[]Array<类型>先看第一种方式,如下:let arr1: (number|string)[] = [1, 2, 3, 'hello'];再看第二种方式,如下:let arr2: Array<number|string> = [1, 2, 3, 'hello'];第二种写法是一种泛型的写法,这个会在后面章节中进行详解的。这里数组子项可以是数字也可以是字符串。对于数组子项的顺序以及数组子项的个数并没有限制。那么如何能够对数组的个数以及顺序都做限定类型呢?就可以采用元组类型。元组类型元组类型实际上就是数组类型的一种特殊形式,代码如下:let arr3: [number, string] = [1, 'hello'];这里会限定数组的每一项的值的类型和值的个数,对于多添加一些子项都是会报错的,属于比较严格的数组形式。对象类型与索引签名对象类型在TS中可以直接对对象字面量进行类型限定,可以精确到具体的字段都具备哪些类型,如下:type A = { username: string age: number } let a: A = { username: 'xiaoming', age: 20 }对于对象类型来说,多出来的字段还是缺少的字段都会产生错误,如下:type A = { username: string age: number } let a: A = { // error username: 'xiaoming' }那么可以给age添加一个可选标识符?来表示age为可选项,写与不写都是可以的。type A = { username: string //age是可选项 age?: number } let a: A = { // success username: 'xiaoming' }索引签名那么对于多出来的字段,可以通过索引签名方式来解决,如下:type A = { username: string //索引签名 [index: string]: any } let a: A = { username: 'xiaoming' gender: 'male', job: 'it' }索引签名中的属性也可以指定number类型,不过往往只有数组中会采用这种数字类型的索引签名方式,如下:type A = { [index: number]: any } let a: A = [1, 2, 3, true, 'hello'];对象类型如果想定义初始值为空值的话,可以采用类型断言来改造,如下:type Obj = {username: string} let obj = {} as Obj; // success最后来看一下对象和数组组合在一起定义的类型,如下:let json: {username: string, age: number}[] = [];函数类型与void类型函数类型在TS中对于函数的类型使用方式有很多种限定,先来看一下函数的参数:function foo(n: number, m?: string): number{ return 123; } foo(123, 'hello'); foo(123); // successTS中要求,实参的个数跟形参的个数必须相同,所以可以添加可选链?来实现参数可选操作。函数还可以通过在函数中定义返回值的类型:number,可以参考上面代码。下面看一下函数表达式的写法,及类型注解的方式。let foo: (n: number, m: string) => number = function(n, m){ return 123; }如果在前面进行了类型注解,那么就不用在函数体内进行类型的添加了。void类型表示函数没有任何返回值的时候得到的类型。let foo = function(){ // void }当函数没有return的时候返回void类型,当return undefined的时候也可以返回void类型。那么函数定义void类型跟undefined类型也是有区别的,因为undefined 类型是不能不写return的。let foo = function(): undefined{ // undefined 不能不写return的 } // error函数重载与可调用注解函数重载函数重载是指函数约束传入不同的参数,返回不同类型的数据,而且可以清晰的知道传入不同的参数得到不同的结果。假如我们实现这样一个函数,如下:function foo(n1: number, n2?: number, n3?: number, n4?: number){ } foo(1); foo(1, 2); foo(1, 2, 3); foo(1, 2, 3, 4);这样传递几个参数都是可以的,那么我们能不能限制传递几个参数呢?比如只允许传递一个参数,两个参数,四个参数,不允许传递三个参数,这时就可以通过函数重载还实现了。function foo(n1: number): any function foo(n1: number, n2: number): any function foo(n1: number, n2: number, n3: number, n4: number): any function foo(n1: number, n2?: number, n3?: number, n4?: number){ } foo(1); foo(1, 2); foo(1, 2, 3); // error foo(1, 2, 3, 4);下面再来实现一个需求,要求参数的类型必须保持一致,如下:function foo(n: number, m: number): any function foo(n: string, m: string): any function foo(n: number|string, m: number|string){ } foo(1, 2); foo('a', 'b'); foo(3, 'c'); // error可调用注解可调用注解提供多种调用签名,用以特殊的函数重载。首先可调用注解跟普通函数的类型注解可以起到同样的作用。type A = () => void; type A = { // 可调用注解,可以针对函数重载进行类型注解的 (): void } let a: A = () => {};那么可调用注解比普通类型注解优势在哪里呢?就是可以对函数重载做类型注解,代码如下:type A = { (n: number, m: number): any (n: string, m: string): any } function foo(n: number, m: number): any function foo(n: string, m: string): any function foo(n: number|string, m: number|string){ }枚举类型与const枚举枚举类型枚举是组织收集有关联集合的一种方式,使代码更加易于阅读。其实简单来说枚举就是定义一组常量。enum Roles { SUPER_ADMIN, ADMIN = 3, USER } console.log( Roles.SUPER_ADMIN ); // 0 console.log( Roles.ADMIN ); // 3 console.log( Roles.USER ); // 4枚举默认不给值的情况下,就是一个从0开始的数字,是可以自动进行累加的,当然也可以自己指定数值,后面的数值也是可以累加的。枚举也支持反向枚举操作,通过数值来找到对应的key属性,这样操作起来会非常的灵活。enum Roles { SUPER_ADMIN, ADMIN = 3, USER } console.log( Roles[0] ); // SUPER_ADMIN console.log( Roles[3] ); // ADMIN console.log( Roles[4] ); // USER枚举给我们的编程带来的好处就是更容易阅读代码,举例如下:if(role === Roles.SUPER_ADMIN){ // 更容易阅读 }下面来看一下,如果定义成字符串的话,需要注意一些什么?enum Roles { SUPER_ADMIN = 'super_admin', ADMIN = 'admin', USER = 'user' }字符串形式是没有默认值的,而且不能做反向映射的。const枚举在枚举的前面可以添加一个const关键字。const enum Roles { SUPER_ADMIN = 'super_admin', ADMIN = 'admin', USER = 'user' }那么没有const关键字和有const关键字的区别是什么呢?主要区别在于编译的最终结果,const方式最终编译出来的就是一个普通字符串,并不会产生一个对象,更有助于性能的体现。总结内容了解了什么是TypeScript,在编程中的优势如何对TS文件进行编译,以及tsconfig.json的使用全面掌握TS中的各种概念,如:类型注解、类型断言等全面掌握TS中常见类型,如:数组、对象、函数等全面掌握TS中的新类型,如:枚举、元组、any、never等
2022年12月24日
5 阅读
0 评论
0 点赞
2022-12-24
Vue3 学习笔记(五)
章节介绍本章将学习VueRouter路由与Vuex状态管理 - 组织与架构应用。本章学习目标本章将学习Vue3中的路由与状态管理,随着前后端分离式开发的兴起,单页面应用开发(即SPA页面)也越来越流行,所以前端路由与共享状态也越来越重要!安排-什么是前端路由以及路由两种模式实现原理-路由的基本搭建与嵌套路由模式-动态路由模式与编程式路由模式-命名路由与命名视图与路由元信息-路由传递参数的多种方式及应用场景-详解route对象与router对象-路由守卫详解及应用场景-Vuex状态管理的概念及组件通信的总结-Vuex共享状态的基本开发流程-Vuex处理异步状态及应用场景-Vuex计算属性和辅助函数的使用-Vuex-persist对数据进行持久化处理-Vuex分割模块及多状态管理-组合式API中使用Router和Vuex注意事项-Router_Vuex的任务列表综合案例-Vue状态管理之Pinia存储库-搭建 Vite3 + Pinia2 组合模式路由的基本搭建与嵌套路由模式在前面的小节中,已经介绍了什么是前端路由以及前端路由所具备的特点。本小节就来对路由进行实际搭建吧。vue路由的搭建路由在vue中属于第三方插件,需要下载安装后进行使用。版本说明一下,Vue3搭配的是Vue Router4,目前正常安装的话,就是路由4的版本。如下:npm install vue-router安装好后,需要对路由进行配置,并且与Vue进行结合,让路由插件生效。在/src/router/index.js创建配置文件。 配置路由信息 可以通过 createWebHistory()来创建history模式的路由,也可以通过createWebHashHistory()来创建hash模式的路由。那么在浏览器中输入的URL该如何展示对应的组件呢?可以通过这个组件来实现。除了进行展示外,还可以通过方式进行声明式的路由跳转,代码如下:<template> <div> <router-link to="/">首页</router-link> | <router-link to="/about">关于</router-link> <router-view></router-view> </div> </template>嵌套路由模式往往我们的路由是比较复杂的,需要的层级也比较多,那么就会产生嵌套路由的模式,比如:localhost:8080/about/foo和localhost:8080/about/barimport Foo from '@/views/Foo.vue' import Bar from '@/views/Bar.vue' const routes = [ { path: '/about', component: About, children: [ { path: 'foo', component: Foo }, { path: 'bar', component: Bar } ] } ]可以看到嵌套路由是通过children属性来完成的,那么对于这种嵌套路由写法,我们对应的也要在一级路由对应页面中添加一个,这样才能切换显示二级路由所对应的页面。动态路由模式与编程式路由模式动态路由模式所谓的动态路由其实就是不同的URL可以对应同一个页面,这种动态路由一般URL还是有规律可循的,所以非常适合做类似于详情页的功能。 动态路由 // router/index.js,定义动态路由 const routes = [ { path: 'foo/:id', component: Foo } ] // views/Foo.vue,获取动态路由的id export default { mounted(){ console.log( this.$route.params.id ); } }编程式路由前面介绍过如何在页面中对路由进行切换,可以采用声明式写法来完成,但是往往这种方式不够灵活,首先不能更灵活的控制结构和样式,其次不能自动进行路由的跳转,必须点击操作才行。那么编程式路由就会在JavaScript中通过逻辑的方式进行路由跳转,我们把这种写在JS中进行跳转路由的方式叫做编程式路由,这样方式会更加的灵活。<template> <button @click="handleToBar">编程式路由跳转</button> </template> <script> export default { methods: { handleToBar(){ this.$router.push('/about/bar'); } } } </script>可以看到在动态路由中使用到了一个route对象,而在编程式路由中使用了一个router对象,那么这两个比较重要的路由对象,会在后面的小节中给大家进行详细的讲解。命名路由与命名视图与路由元信息命名路由在路由跳转中,除了path 之外,你还可以为任何路由提供 name 的形式进行路由跳转。那么name方式的好处如下:没有硬编码的 URLparams 的自动编码/解码防止你在 url 中出现打字错误绕过路径排序(如显示一个)// router/index.js,定义命名路由 const routes = [ { path: '/about/bar', name: 'bar', component: Bar }, { path: '/about/foo/:id', name: 'foo', component: Foo } ];<!-- About.vue,使用命名路由跳转 --> <template> <div> <router-link :to="{ name: 'bar' }">bar</router-link> <router-link :to="{ name: 'foo', params: {id: 123} }">foo 123</router-link> </div> </template> name的方式也支持动态路由形式。命名视图有时候想同时 (同级) 展示多个视图,而不是嵌套展示,这个时候就非常适合使用命名视图。 命名视图 通过components字段配置多个组件,根据不同的router-view去渲染不同的组件。路由元信息有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta属性来实现。const routes = [ { path: '/about/bar', name: 'bar', component: Bar, meta: { auth: false } }, { path: '/about/foo/:id', name: 'foo', component: Foo, meta: { auth: true } } ];定义好meta元信息后,可通过route对象去访问meta属性。<!-- Foo.vue --> <script> export default { mounted(){ this.$route.meta.auth // true } } </script>路由传递参数的多种方式及应用场景路由传参我们经常要在路由跳转的时候传递一些参数,这些参数可以帮助我们完成后续的一些开发和一些处理。路由的传递参数主要有以下三种方式:query方式(显示) -> $route.queryparams方式(显示) -> $route.paramsparams方式(隐式) -> $route.params两种显示传递数据的差异点主要为,query是携带辅助信息,而params是界面的差异化。<!-- About.vue --> <template> <div> <router-link :to="{ name: 'bar', query: { username: 'xiaobai' } }">bar</router-link> <router-link :to="{ name: 'foo', params: { username: 'xiaoqiang' } }">foo</router-link> </div> </template> <!-- Bar.vue --> <script> export default { mounted(){ console.log(this.$route.query); } } </script> <!-- foo.vue --> <script> export default { mounted(){ console.log(this.$route.params); } } </script>前两种都是显示传递数据,那么第三种是隐式传递数据,这种方式并不会把数据暴露出来。<!-- About.vue --> <template> <div> <router-link :to="{ name: 'bar', params: { username: 'xiaoqiang' } }">bar</router-link> </div> </template> <!-- Bar.vue --> <script> export default { mounted(){ console.log(this.$route.params); } } </script>但是这里需要注意以下,隐式发送过来的数据,只是临时性获取的,一旦刷新页面,隐藏的数据就会消失,所以在使用的时候要额外注意以一下。详解route对象与router对象在前面小节中,我们频繁的使用过route对象和router对象,这两个对象在路由中非常的重要,下面我们来详细的学习一下。route对象与router对象首先route对象用来获取路由信息,而router对象用来调用路由方法的。具体区别在于,route对象是针对获取操作的,主要是操作当前路由的,而router对象是针对设置操作的,主要是操作整个路由系统对应的功能。route对象具体功能如下:fullPathhashhrefmatchedmetanameparamspathquery主要就是一些路由信息,像常见的动态路由参数,query数据,meta元信息,url路径等等。router对象具体功能如下:addRouteafterEachbackbeforeEachbeforeResolvecurrentRouteforwardgetRoutesgohasRoutepushremoveRoute主要就是一些方法,动态改变路由表,路由守卫, 历史记录的前进后退,编程式路由等等。路由守卫详解及应用场景正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。守卫主要的作用就是在进入到指定路由前做一个拦截,看一下我们是否具备权限,如果有权限就直接进入,如果没权限就跳转到其他页面。路由守卫分类一般可以分为三种路由守卫使用的方式:全局环境的守卫路由独享的守卫组件内的守卫先来看一下如何设置全局的路由守卫,一般在路由配置文件中进行设置。router.beforeEach((to, from, next)=>{ if(to.meta.auth){ next('/'); } else{ next(); } })其中to表示需要进入到哪个路由,from表示从哪个路由离开的,那么next表示跳转到指定的页面。有时候我们只是想给某一个指定的路由添加守卫,那么可以选择设置路由独享的守卫方式。const routes = [ { name: 'bar', component: Bar, beforeEnter(to, from, next){ if(to.meta.auth){ next('/'); } else{ next(); } } } ];还可以通过在.vue文件中进行路由守卫的设置,代码如下:<script> export default { name: 'FooView', beforeRouteEnter(to, from, next){ if(to.meta.auth){ next('/'); } else{ next(); } } } </script>完整的导航解析流程导航被触发。在失活的组件里调用 beforeRouteLeave 守卫。调用全局的 beforeEach 守卫。在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。在路由配置里调用 beforeEnter。解析异步路由组件。在被激活的组件里调用 beforeRouteEnter。调用全局的 beforeResolve 守卫(2.5+)。导航被确认。调用全局的 afterEach 钩子。触发 DOM 更新。调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。Vuex共享状态的基本开发流程在上一个小节中,我们介绍了什么是Vuex,本小节我们一起来看一下Vuex共享状态的基本开发流程。首先我们来看一下Vuex的经典流程图。 Vuex状态管理 我们可以看到,基本上就是先准备好一个共享数据state,然后渲染我组件中,通过组件调用dispatch -> actions -> commit -> mutations的方式来进行state修改。那么这里我们先不考虑dispatch -> actions,因为这两个环节是处理异步程序的,那么我们直接组件去调用commit就可以触发mutations中定义的方法,这样在这个方法中进行state的修改。首先在/src/store/index.js创建一个状态管理的配置文件,然后在main.js中让vuex于vue进行结合,就像我们路由的使用一样。// store/index.js const store = createStore({ state: { count: 0 }, mutations: { change(state, payload){ state.count += payload; } } }); // main.js import store from './store' app.use(store)下面看一下如何在页面中显示state数据和如何通过commit修改我们的共享数据,代码如下:<template> <div> <button @click="change(5)">点击</button> hello home, {{ $store.state.count }} </div> </template> <script> export default { name: 'HomeView', methods: { handleClick(){ this.$store.commit('change', 5); } } } </script>Vuex处理异步状态及应用场景本小节中将讲解一下Vuex中如何处理异步程序,因为在上一个小节中提到过,mutations中只能处理同步,不能处理异步,所以异步的工作就交给了 actions 来完成。那么如何触发actions中定义的方法呢,就需要通过dispatch进行触发,具体代码如下:const store = createStore({ state: { count: 0 }, actions: { change(context, payload){ setTimeout(()=>{ context.commit('change', payload) }, 1000) } }, mutations: { change(state, payload){ state.count += payload; } } });<script> export default { name: 'HomeView', methods: { handleClick(){ this.$store.dispatch('change', 5); } } } </script>这样在vue devtools插件中就可以更好的观察到异步数据的变化。那么异步处理的应用场景有哪些呢?异步的获取后端的数据,这样可以利用状态管理来充当MVC架构中的C层,不仅衔接前后端,还能对数据进行共享,可以在切换路由的时候做到减少请求次数,而且状态管理配合本地存储进行数据持久化也是非常的方便。Vuex计算属性和辅助函数的使用Vuex中除了提供常见的异步处理和同步处理外,还提供了一些辅助的操作方式,比如:状态管理下的计算属性和简化操作的辅助函数。getters计算属性在Vuex中通过定义getters字段来实现计算属性,代码如下:const store = createStore({ state: { count: 0 } getters: { doubleCount(state){ return state.count * 2; } } });<template> <div> {{ count }}, {{ doubleCount }} </div> </template>当count数据发生改变的手,对应的计算属性doubleCount也会跟着发生改变。辅助函数在Vuex中为了让我们使用共享数据或调用方法的时候,更加简单易用,提供了对应的辅助函数,分别为:mapState、mapGetters、mapMutations、mapActions。<template> <div> <button @click="change(5)">点击</button> hello home, {{ count }}, {{ doubleCount }} </div> </template> <script> import { mapState, mapGetters, mapActions } from 'vuex'; export default { name: 'HomeView', methods: { ...mapActions(['change']) }, computed: { ...mapState(['count']), ...mapGetters(['doubleCount']) } } </script>辅助函数最大的优点就是可以处理大量共享数据的需求,这样写起来会非常的简便,因为只需要往数组里添加子项即可。Vuex-persist对数据进行持久化处理默认情况下Vuex状态管理是不会对数据进行持久化存储的,也就是说当我们刷新浏览器后,共享状态就会被还原。那么我们想要在刷新的时候能够保持成修改后的数据就需要进行持久化存储,比较常见的持久化存储方案就是利用本地存储来完成,不过自己去实现还是比较不方便的,下面我们通过第三方模块来实现其功能。模块github地址:https://github.com/championswimmer/vuex-persist。根据地址要求的配置操作如下:// npm install vuex-persist import VuexPersistence from 'vuex-persist'; const vuexLocal = new VuexPersistence({ storage: window.localStorage, reducer: (state) => ({count: state.count}) }) const store = createStore({ state: { count: 0, msg: 'hello' } plugins: [vuexLocal.plugin] });默认情况下,vuex-persist会对所有共享状态进行持久化,那么如果我们只需要对指定的属性进行持久化就需要配置 reducer字段,这个字段可以指定需要持久化的状态。这样当我们修改了state下的count,那么刷新的时候会不会还原了,并且通过chrome浏览器中Application下的Local Storage进行查看。Vuex分割模块及多状态管理由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。那么这个时候,所有共享状态的值都在一起,所有的方法也都放在了一起,维护起来非常的不方便。那么Vuex中可利用modules属性来配置模块化的共享状态,那么对于后期维护起来是非常方便的,也利于大型项目的架构。在/store下创建一个modules文件夹,然后编写一个message.js,代码如下:const state = { msg: 'hello' }; const getters = { upperMsg(state){ return state.msg.toUpperCase() } }; const actions = {}; const mutations = { change(state, payload){ state.msg = payload; } }; export default { namespaced: true, state, getters, actions, mutations }模块中的选项跟index.js中的选项是一样的,对外提供接口的时候,可以看到一个namespaced字段,这表示当前模块的一个命名空间,主要是为了防止跟其他模块之间产生冲突,在调用的时候需要携带对应的命名空间标识符才行。再来看一下index.js如何去收集我们的模块,并如何去使用我们的模块。// store/index.js import message from '@/store/modules/message' const store = createStore({ modules: { message } });<!-- About.vue --> <template> <div> <button @click="change('hi')">点击</button> hello about, {{ msg }}, {{ upperMsg }} </div> </template> <script> import { mapState, mapGetters, mapMutations } from 'vuex'; export default { name: 'AboutView', methods: { // handleClick(){ // this.$store.commit('message/change', 'hi'); // } ...mapMutations('message', ['change']) }, computed: { // msg(){ // return this.$store.message.msg; // }, // upperMsg(){ // return this.$store.getters['message/upperMsg'] // } ...mapState('message', ['msg']), ...mapGetters('message', ['upperMsg']) } } </script>在辅助函数的情况下,也可以进行很好的调用,辅助函数的第一个参数就是命名空间的名字。组合式API中使用Router和Vuex注意事项前面介绍的路由和状态管理都是在选项式API中进行使用的,那么路由和状态管理在组合式API中使用的时候,需要注意哪些问题呢?主要就是路由会提供两个use函数,分别为:useRoute和useRouter;同理状态管理页提供了一个use函数,useStore来操作的。先来看一下路由相关use函数的使用情况:<script setup> import { useRoute, useRouter } from 'vue-router' const route = useRoute(); const router = useRouter(); console.log( route.meta ); console.log( router.getRoutes() ); </script>基本上跟选项式API中的用法是一样的,并没有太大的区别。再来看一下状态管理相关use函数的使用情况:<script setup> import { useStore } from 'vuex' const store = useStore(); console.log( store.state.count ); console.log( store.state.message.msg ); </script>得到store对象,接下来的操作也是跟选项式API中使用的是一样的。Router_Vuex的任务列表综合案例本小节将对本章学习的路由加状态管理做一个综合案例,通过案例来巩固本章所学知识点。 综合案例 首先先来配置案例中的路由,主要有三个页面,分别对应所有任务,已完成任务,未完成任务。import { createRouter, createWebHistory } from 'vue-router' import Todo from '@/views/Todo.vue' import Complete from '@/views/Complete.vue' import Incomplete from '@/views/Incomplete.vue' const routes = [ { path: '/', redirect: '/todo' }, { path: '/todo', component: Todo }, { path: '/complete', component: Complete }, { path: '/incomplete', component: Incomplete, } ]; const router = createRouter({ history: createWebHistory(), routes }) export default router;在来配置一下Vuex状态管理,主要对任务列表进行共享状态处理:import { createStore } from "vuex"; const store = createStore({ state: { todoList: [ { isChecked: true, id: 1, task: '第一个任务' }, { isChecked: false, id: 2, task: '第二个任务' } ] }, actions: { }, mutations: { add(state, payload){ state.todoList.unshift({ isChecked: false, id: state.todoList.length, task: payload }); } }, getters: { complete(state){ return state.todoList.filter((v)=> v.isChecked) }, incomplete(state){ return state.todoList.filter((v)=> !v.isChecked) } } }); export default store;最后看一下三个页面的基本逻辑处理:<!-- Todo.vue --> <template> <div> <ul> <li v-for="item in todoList" :key="item.id" :class="{ through: item.isChecked }"> <input type="checkbox" v-model="item.isChecked"> {{ item.task }} </li> </ul> </div> </template> <script setup> import { computed, defineComponent } from 'vue'; import { useStore } from 'vuex'; defineComponent({ name: 'TodoView' }); const store = useStore(); const todoList = computed(()=> store.state.todoList) </script> <!-- Complete.vue --> <template> <div> <ul> <li v-for="item in todoList" :key="item.id" :class="{ through: item.isChecked }"> <input type="checkbox" v-model="item.isChecked"> {{ item.task }} </li> </ul> </div> </template> <script setup> import { computed, defineComponent } from 'vue'; import { useStore } from 'vuex'; defineComponent({ name: 'CompleteView' }); const store = useStore(); const todoList = computed(()=> store.getters.complete) </script> <!-- Incomplete.vue --> <template> <div> <ul> <li v-for="item in todoList" :key="item.id" :class="{ through: item.isChecked }"> <input type="checkbox" v-model="item.isChecked"> {{ item.task }} </li> </ul> </div> </template> <script setup> import { computed, defineComponent } from 'vue'; import { useStore } from 'vuex'; defineComponent({ name: 'IncompleteView' }); const store = useStore(); const todoList = computed(()=> store.getters.incomplete) </script>搭建 Vite3 + Pinia2 组合模式前面我们介绍过Vite和Pinia,其中Vite是最新的脚手架,基于原生ESM方式;而Pinia则是最新的状态管理,比Vuex使用起来更加简单。本小节我们将演示Vite如何去搭配Pinia来完成项目的开发。首先对Vite3脚手架进行初始化的安装。安装脚手架# npm 6.x npm create vite@latest vite-study # yarn yarn create vite vite-study # pnpm pnpm create vite vite-study选择框架:因为Vite可以和很多框架配合使用,所以这里我们选择:Vite + Vue? Select a framework: » - Use arrow-keys. Return to submit. Vanilla > Vue React Preact Lit Svelte选择变体:这里先选择自定义形式? Select a variant: » - Use arrow-keys. Return to submit. JavaScript TypeScript > Customize with create-vue Nuxt选择安装Pinia? Add Pinia for state management? >> No / Yes Yes进入项目:安装第三方模块,并启动项目cd vite-study npm install npm run dev VITE v3.1.0 ready in 408 ms ➜ Local: http://127.0.0.1:5173/ ➜ Network: use --host to expose在安装好Vite后,打开/src/stores可以看到自动安装好了一个示例模块counter.js,代码如下:import { ref, computed } from 'vue' import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', () => { const count = ref(0) const doubleCount = computed(() => count.value * 2) function increment() { setTimeout(()=>{ count.value++ }, 2000) } return { count, doubleCount, increment } })这里的风格可以是上一个小节中介绍的配置写法,也可以利用组合式API风格来编程Pinia。这里做了一个共享状态count,又做了一个计算属性doubleCount,还有一个方法increment。在共享方法中是不分同步还是异步的,对于vue devtools都是可以很好的进行监控的,所以比Vuex使用起来要简单一些。下面看一下共享状态是如何进行渲染和方法调用的。<!-- App.vue --> <script setup> import { useCounterStore } from './stores/counter'; import { storeToRefs } from 'pinia'; const counterStore = useCounterStore(); const { count, doubleCount } = storeToRefs(counterStore); const handleClick = () => { counterStore.increment(); }; </script> <template> <header> <button @click="handleClick">点击</button> {{ count }}, {{ doubleCount }} </header> </template>Pinia对于模块化的操作方式也比Vuex要简单一些,直接在/stores创建下新创建一个模块JS即可,如:message.js。message.js的代码跟counter.js的代码是一样的格式,使用的时候也是一样的操作行为。总结内容了解什么是前端路由,以及Vue3中如何使用vue-router路由掌握路由的基本操作,如:编程式路由、动态路由、路由守卫等了解什么是共享状态,以及Vue3中如何使用vuex共享状态掌握vuex的基本操作,如:同步异步方法、模块化、持久化等综合应用以及Vuex下一代版本,Pinia存储库
2022年12月24日
6 阅读
0 评论
1 点赞
2022-12-24
Vue3 学习笔记(四)
章节介绍本章将学习Vue3组合式API详解 - 大型应用的高端写法。本章学习目标本章将学习Vue3组合式API(即:Composition API),组合式API是Vue3组织代码的一种新型写法,有别于选项式的API。学会使用这种风格进行编程,并应用在项目之中。什么是组合式API组合式API是有别于选项式API的另一种新型写法,它更适合编写复杂的应用,假如我们要完成一个搜索功能,可能会有如下功能需要实现:搜索提示列表搜索结果列表搜索记录列表 搜索案例 那么分别通过选项式和组合式是如何组织和实现代码的呢?可以发现选项式组织代码的时候,代码的跳跃性特别的强,对于后期维护特别的不方便;而组合式则是把相同功能的代码放到一起,功能独立,易于维护。 选项式VS组合式 课程安排-setup方法与script_setup及ref响应式-事件方法_计算属性_reactive_toRefs-生命周期_watch_watchEffect-跨组件通信方案provide_inject-复用组件功能之use函数-利用defineProps与defineEmits进行组件通信-利用组合式API开发复杂的搜索功能-利用组合式API开发搜索提示列表-利用组合式API开发搜索结果列表-利用组合式API开发搜索历史列表setup方法与script_setup及ref响应式setup方法与script_setup在Vue3.1版本的时候,提供了setup方法;而在Vue3.2版本的时候,提供了script_setup属性。那么setup属性比setup方法对于操作组合式API来说会更加的简易。 <template> <div> <h2>setup方法</h2> {{ count }} </div> </template> // Vue3.1 <script> export default { setup () { let count = 0; return { count } } } </script> // Vue3.2 <script setup> let count = 0; </script>setup方法是需要把数据进行return后,才可以在template标签中进行使用,而setup属性方式定义好后就可以直接在template标签中进行使用。ref响应式如何在组合式API中来完成数据的响应式操作,通过的就是ref()方法,需要从vue模块中引入这个方法后才可以使用。<script setup> import { ref } from 'vue'; let count = ref(0); // count -> { value: 0 } //count += 1; //✖ count.value += 1; // ✔ </scirpt>count数据的修改,需要通过count.value的方式,因为vue底层对响应式数据的监控必须是对象的形式,所以我们的count返回的并不是一个基本类型,而是一个对象类型,所以需要通过count.value进行后续的操作,那么这种使用方式可能会添加我们的心智负担,还好可以通过Volar插件可以自动完成.value的生成,大大提升了使用方式。那么现在count就具备了响应式变化,从而完成视图的更新操作。那么ref()方法还可以关联原生DOM,通过标签的ref属性结合到一起,也可以关联到组件上。<template> <div> <h2 ref="elem">setup属性方式</h2> </div> </template> <script setup> import { ref } from 'vue'; let elem = ref(); setTimeout(()=>{ console.log( elem.value ); //拿到对应的原生DOM元素 }, 1000) </script>事件方法_计算属性 reactive_toRefs事件方法与计算属性下面看一下在组合式API中是如何实现事件方法和计算属性的。<template> <div> <button @click="handleChange">点击</button> {{ count }}, {{ doubleCount }} </div> </template> <script setup> import { computed, ref } from 'vue'; let count = ref(0); let doubleCount = computed(()=> count.value * 2) let handleChange = () => { count.value += 1; }; </script>事件方法直接就定义成一个函数,计算属性需要引入computed方法,使用起来是非常简单的。reactive与toRefsreactive()方法是组合式API中另一种定义响应式数据的实现方式,它是对象的响应式副本。<template> <div> <h2>reactive</h2> {{ state.count }} </div> </template> <script setup> import { reactiv} from 'vue'; let state = reactive({ count: 0, message: 'hi vue' }) state.count += 1; </script>reactive()方法返回的本身就是一个state对象,那么在修改的时候就不需要.value操作了,直接可以通过state.count的方式进行数据的改变,从而影响到视图的变化。ref()和reactive()这两种方式都是可以使用的,一般ref()方法针对基本类型的响应式处理,而reactive()针对对象类型的响应式处理,当然还可以通过toRefs()方法把reactive的代码转换成ref形式。<template> <div> <h2>reactive</h2> {{ state.count }}, {{ count }} </div> </template> <script setup> import { reactive, toRefs } from 'vue'; let state = reactive({ count: 0 }) let { count } = toRefs(state); // let count = ref(0) setTimeout(() => { //state.count += 1; count.value += 1; }, 1000) </script>生命周期_watch_watchEffect生命周期钩子函数在学习选项式API的时候,我们学习了生命周期钩子函数,那么在组合式API中生命周期又是如何使用的呢?下面我们从图中先看一下对比的情况吧。 生命周期对比 那么具体的区别如下:组合式中是没有beforeCreate和created这两个生命周期,因为本身在组合式中默认就在created当中,直接定义完响应式数据后就可以直接拿到响应式数据,所以不需要再有beforeCreate和created这两个钩子组合式的钩子前面会有一个on,类似于事件的特性,那就是可以多次重复调用<script> import { onMounted, ref } from 'vue'; let count = ref(0); onMounted(()=>{ console.log( count.value ); }); onMounted(()=>{ console.log( count.value ); }); onMounted(()=>{ console.log( count.value ); }); </script>watch与watchEffect这里先说一下watchEffect的用法,为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 函数。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。watchEffect常见特性:一开始会初始触发一次,然后所依赖的数据发生改变的时候,才会再次触发触发的时机是数据响应后,DOM更新前,通过flush: 'post' 修改成DOM更新后进行触发返回结果是一个stop方法,可以停止watchEffect的监听提供一个形参,形参主要就是用于清除上一次的行为<template> <div> <h2>watchEffect</h2> <div>{{ count }}</div> </div> </template> <script setup> import { ref, watchEffect } from 'vue'; let count = ref(0); // const stop = watchEffect(()=>{ // console.log(count.value); // }, { // flush: 'post' // }) // setTimeout(()=>{ // stop(); // }, 1000) // setTimeout(()=>{ // count.value += 1; // }, 2000) watchEffect((cb)=>{ console.log(count.value); cb(()=>{ //更新前触发和卸载前触发,目的:清除上一次的行为(停止上一次的ajax,清除上一次的定时器) console.log('before update'); }) }) setTimeout(()=>{ count.value += 1; }, 2000) </script>再来看一下watch侦听器的使用方式,如下:<script setup> import { ref, watch } from 'vue'; let count = ref(0); watch(count, (newVal, oldVal) => { console.log(newVal, oldVal); }) setTimeout(()=>{ count.value = 1; }, 2000) </script>那么watch与watchEffect的区别是什么呢?懒执行副作用更具体地说明什么状态应该触发侦听器重新运行访问侦听状态变化前后的值跨组件通信方案provide_inject依赖注入在Vue中把跨组件通信方案provide_inject也叫做依赖注入的方式,前面我们在选项式中也学习了它的基本概念,下面看一下在组合式API中改如何使用。// provide.vue <template> <div> <my-inject></my-inject> </div> </template> <script setup> import MyInject from '@/inject.vue' import { provide, ref, readonly } from 'vue' //传递响应式数据 let count = ref(0); let changeCount = () => { count.value = 1; } provide('count', readonly(count)) provide('changeCount', changeCount) setTimeout(()=>{ count.value = 1; }, 2000) </script> //inject.vue <template> <div> <div>{{ count }}</div> </div> </template> <script setup> import { inject } from 'vue' let count = inject('count') let changeCount = inject('changeCount') setTimeout(()=>{ changeCount(); }, 2000); </script>依赖注入使用的时候,需要注意的点:不要在inject中修改响应式数据,可利用回调函数修改为了防止可设置成 readonly复用组件功能之use函数为了更好的组合代码,可以创建统一规范的use函数,从而抽象可复用的代码逻辑。利用use函数可以达到跟mixin混入一样的需求,并且比mixin更加强大。// useCounter.js import { computed, ref } from 'vue'; function useCounter(num){ let count = ref(num); let doubleCount = computed(()=> count.value * 2 ); return { count, doubleCount } } export default useCounter;<template> <div> <h2>use函数</h2> <div>{{ count }}, {{ doubleCount }}</div> </div> </template> <script setup> import useCounter from '@/compotables/useCounter.js' let { count, doubleCount } = useCounter(123); setTimeout(()=>{ count.value += 1; }, 2000); </script>通过useCounter函数的调用,就可以得到内部return出来的对象,这样就可以在.vue文件中进行功能的使用,从而实现功能的复用逻辑。利用defineProps与defineEmits进行组件通信在组合式API中,是通过defineProps与defineEmits来完成组件之间的通信。definePropsdefineProps是用来完成父子通信的,基本使用跟选项式中的props非常的像,代码如下:// parent.vue <template> <div> <h2>父子通信</h2> <my-child :count="0" message="hello world"></my-child> </div> </template> <script setup> import MyChild from '@/child.vue' </script> // child.vue <template> <div> <h2>hi child, {{ count }}, {{ message }}</h2> </div> </template> <script setup> import { defineProps } from 'vue' const state = defineProps({ // defineProps -> 底层 -> reactive响应式处理的 count: { type: Number }, message: { type: String } }); console.log( state.count, state.message ); </script>defineEmitsdefineEmits是用来完成子父通信的,基本使用跟选项式中的emits非常的像,代码如下:// parent.vue <template> <div> <h2>子父通信</h2> <my-child @custom-click="handleClick"></my-child> </div> </template> <script setup> import MyChild from '@/child.vue' let handleClick = (data) => { console.log(data); } </script> // child.vue <script setup> import { defineEmits } from 'vue' const emit = defineEmits(['custom-click']); setTimeout(()=>{ emit('custom-click', '子组件的数据'); }, 2000) </script>利用组合式API开发复杂的搜索功能从本小节我们要完成章节介绍中出现的搜索页面,利用组合式API去实现。先进行开发前的准备工作。数据接口后端地址:https://github.com/Binaryify/NeteaseCloudMusicApi后端接口: 搜索建议:/search/suggest?keywords=海阔天空 搜索结果:/search?keywords=海阔天空反向代理由于我们的前端是localhost:8080,而后端是localhost:3000,这样会存在跨域问题,所以可以通过脚手架下的vue.config.js进行反向代理的配置。// vue.config.js const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, runtimeCompiler: true, devServer: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, pathRewrite: { '^/api': '' } } } } })布局与样式我们提前把布局和样式做好了,直接在上面进行逻辑的开发,可直接查看 13_search.vue 这个文件,到此我们已经准备好了开发前的准备工作。利用组合式API开发搜索提示列表本小节完成搜索页面的提示列表功能。<template> <div class="search-input"> <i class="iconfont iconsearch"></i> <input type="text" placeholder="搜索歌曲" v-model="searchWord" @input="handleToSuggest"> <i v-if="searchWord" @click="handleToClose" class="iconfont iconguanbi"></i> </div> <template v-if="searchType == 1"> ... </template> <template v-else-if="searchType == 2"> ... </template> <template v-else-if="searchType == 3"> <div class="search-suggest"> <div class="search-suggest-head">搜索“ {{ searchWord }} ”</div> <div class="search-suggest-item" v-for="item in suggestList" :key="item.id" @click="handleItemResult(item.name), handleAddHistory(item.name)"> <i class="iconfont iconsearch"></i>{{ item.name }} </div> </div> </template> </template> <script setup> import { ref } from 'vue'; import axios from 'axios'; import '@/assets/iconfont/iconfont.css'; function useSearch(){ let searchType = ref(1); let searchWord = ref(''); let handleToClose = () => { searchWord.value = ''; searchType.value = 1; }; return { searchType, searchWord, handleToClose } } function useSuggest(){ let suggestList = ref([]); let handleToSuggest = () => { if(searchWord.value){ searchType.value = 3; axios.get(`/api/search/suggest?keywords=${searchWord.value}`).then((res)=>{ let result = res.data.result; if(!result.order){ return; } let tmp = []; for(let i=0;i<result.order.length;i++){ tmp.push(...result[result.order[i]]); } suggestList.value = tmp; }) } else{ searchType.value = 1; } }; return { suggestList, handleToSuggest } } let { searchType, searchWord, handleToClose } = useSearch(); let { suggestList, handleToSuggest } = useSuggest(); </script>利用组合式API开发搜索结果列表本小节完成搜索页面的结果列表功能。<template> <div class="search-input"> <i class="iconfont iconsearch"></i> <input type="text" placeholder="搜索歌曲" v-model="searchWord" @input="handleToSuggest" @keydown.enter="handleToResult($event)"> <i v-if="searchWord" @click="handleToClose" class="iconfont iconguanbi"></i> </div> <template v-if="searchType == 1"> ... </template> <template v-else-if="searchType == 2"> <div class="search-result"> <div class="search-result-item" v-for="item in resultList" :key="item.id"> <div class="search-result-word"> <div>{{ item.name }}</div> </div> <i class="iconfont iconbofang"></i> </div> </div> </template> <template v-else-if="searchType == 3"> ... </template> </template> <script setup> import { ref } from 'vue'; import axios from 'axios'; import '@/assets/iconfont/iconfont.css'; function useSearch(){ ... } function useSuggest(){ ... } function useResult(){ let resultList = ref([]); let handleToResult = () => { if(!searchWord.value){ return; } axios.get(`/api/search?keywords=${searchWord.value}`).then((res)=>{ let result = res.data.result; if(!result.songs){ return; } searchType.value = 2; resultList.value = result.songs; }) }; let handleItemResult = (name) => { searchWord.value = name; handleToResult(); }; return { resultList, handleToResult, handleItemResult } } let { searchType, searchWord, handleToClose } = useSearch(); let { suggestList, handleToSuggest } = useSuggest(); let { resultList, handleToResult, handleItemResult } = useResult(); </script>利用组合式API开发搜索历史列表本小节完成搜索页面的历史列表功能。<template> <div class="search-input"> <i class="iconfont iconsearch"></i> <input type="text" placeholder="搜索歌曲" v-model="searchWord" @input="handleToSuggest" @keydown.enter="handleToResult($event), handleAddHistory($event)"> <i v-if="searchWord" @click="handleToClose" class="iconfont iconguanbi"></i> </div> <template v-if="searchType == 1"> <div class="search-history"> <div class="search-history-head"> <span>历史记录</span> <i class="iconfont iconlajitong" @click="handleToClear"></i> </div> <div class="search-history-list"> <div v-for="item in historyList" :key="item" @click="handleItemResult(item)">{{ item }}</div> </div> </div> </template> <template v-else-if="searchType == 2"> ... </template> <template v-else-if="searchType == 3"> ... </template> </template> <script setup> import { ref } from 'vue'; import axios from 'axios'; import '@/assets/iconfont/iconfont.css'; function useSearch(){ ... } function useSuggest(){ ... } function useResult(){ ... } function useHistory(){ let key = 'searchHistory'; let getSearchHistory = () => { return JSON.parse(localStorage.getItem(key) || '[]'); }; let setSearchHistory = (list) => { localStorage.setItem(key, JSON.stringify(list)); }; let clearSearchHistory = () => { localStorage.removeItem(key); }; let historyList = ref(getSearchHistory()); let handleAddHistory = (arg) => { if(!searchWord.value){ return; } if(typeof arg === 'string'){ searchWord.value = arg; } historyList.value.unshift(searchWord.value); historyList.value = [...new Set(historyList.value)]; setSearchHistory(historyList.value); }; let handleToClear = () => { clearSearchHistory(); historyList.value = []; }; return { historyList, handleAddHistory, handleToClear }; } let { searchType, searchWord, handleToClose } = useSearch(); let { suggestList, handleToSuggest } = useSuggest(); let { resultList, handleToResult, handleItemResult } = useResult(); let { historyList, handleAddHistory, handleToClear } = useHistory(); </script>总结内容了解了什么是组合式API,具备怎么的特性学习常见的组合式API语法,如:ref、reactive、watchEffect等组合式API之间的通信方案,如:父子组件、跨组件等通过实战案例,复杂的搜索应用,体验组合式的强大
2022年12月24日
56 阅读
0 评论
1 点赞
2022-12-23
Vue3 学习笔记(三)
-ref属性在元素和组件上的分别使用-利用nextTick监听DOM更新后的情况-自定义指令与自定义全局属性及应用场景-复用组件功能之Mixin混入-插件的概念及插件的实现-Element Plus框架的安装与使用-transition动画与过渡的实现-动态组件与keep-alive组件缓存-异步组件与Suspense一起使用-跨组件间通信方案 Provide_Inject-Teleport实现传送门功能-虚拟DOM与render函数及Diff算法ref属性在元素和组件上的分别使用ref属性Vue是基于MVVM设计模式进行实现的,视图与数据不直接进行通信,但是Vue并没有完全遵循这一原则,而是允许开发者直接进行原生DOM操作。在Vue中可通过ref属性来完成这一行为,通过给标签添加ref属性,然后再通过vm.$refs来获取DOM,代码如下:<template> <div> <h2>ref属性</h2> <button @click="handleClick">点击</button> <div ref="elem">aaaaaaaaa</div> <div ref="elem2">bbbbbbbbb</div> </div> </template> <script> export default { methods: { handleClick(){ // ref属性添加到元素身上,可以获取到当前元素的原生DOM console.log( this.$refs.elem ); console.log( this.$refs.elem2 ); } } } </script>除了可以把ref属性添加给DOM元素外,还可以把ref属性添加给组件,这样可以获取到组件的实例对象,可以间接的实现组件之间的通信,代码如下:<template> <div> <h2>ref属性</h2> <my-head ref="elem3"></my-head> </div> </template> <script> import MyHead from '@/2_头部组件.vue' export default { methods: { handleClick(){ // ref属性添加到组件身上,可以获取到当前组件的vm对象(实例对象) console.log( this.$refs.elem3 ); console.log( this.$refs.elem3.message ); this.$refs.elem3.handleMessage('根组件的数据'); //$refs 也可以实现间接的父子通信 } } } </script>2_头部组件.vue文件:<template> <div> hello myhead </div> </template> <script> export default { data(){ return { message: '头部组件的消息' } }, methods: { handleMessage(data){ console.log(data); } } } </script>利用nextTick监听DOM更新后的情况nextTick方法,它的主要作用是将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。默认情况下,数据的更新会产生一个很小的异步延迟,所以直接再数据改变后取获取DOM是得不到DOM更新后的结果,而得到的是DOM更新前的结果。<template> <div> <h2>hello nextTick</h2> <div ref="elem">{{ message }}</div> </div> </template> <script> export default { data(){ return { message: 'hello world' } }, mounted(){ setTimeout(()=>{ this.message = 'hi vue'; console.log( this.$refs.elem.innerHTML ); // 'hello world' }, 2000) } } </script>如何才能得到DOM更新后的结果呢,可以有两种方案,第一种就是利用生命周期updated这个钩子函数,第二种就是利用我们讲的nextTick方法,支持两种风格即回调和promise。<template> <div> <h2>hello nextTick</h2> <div ref="elem">{{ message }}</div> </div> </template> <script> export default { data(){ return { message: 'hello world' } }, mounted(){ setTimeout(()=>{ this.message = 'hi vue'; /* this.$nextTick(()=>{ console.log( this.$refs.elem.innerHTML ); // 'hi vue' }) */ this.$nextTick().then(()=>{ console.log( this.$refs.elem.innerHTML ); // 'hi vue' }) }, 2000) }, updated(){ console.log( this.$refs.elem.innerHTML ); // 'hi vue' } } </script>自定义指令与自定义全局属性及应用场景除了核心功能默认内置的指令 (例如 v-model 和 v-show),Vue 也允许注册自定义指令,来实现一些封装功能。自定义指令的实现首先来实现一个简单的v-color指令,用于给元素添加背景色,代码如下:<template> <div> <h2>自定义指令</h2> <div @click="handleClick" v-color="color">aaaaaaa</div> </div> </template> <script> export default { data(){ return { color: 'red' } }, //创建局部的自定义指令 directives: { /* color: { mounted(el, binding){ el.style.background = binding.value }, updated(el, binding){ el.style.background = binding.value } } */ color: (el, binding) => { el.style.background = binding.value } } } </script>这里的回调函数是指令中mounted生命周期和updated生命周期的简写方式。下面我们来完成一个实际可以应用的指令,按钮权限指令,一般情况下这种指令不会局部使用,而是全局使用,所以可以通过vue来实现一个全局的按钮权限指令,代码如下:// main.js app.directive('auth', (el, binding) => { let auths = ['edit', 'delete']; let ret = auths.includes(binding.value); if(!ret){ el.style.display = 'none'; } }); // demo.vue <template> <button v-auth="'edit'">编辑</button> </template>自定义全局属性添加一个可以在应用的任何组件实例中访问的全局 property,这样在引入一些第三方模块的时候,就不用每一次进行import操作,而是直接通过this对象去访问全局属性即可,下面举一个例子,实现一个http的全局属性。// main.js app.config.globalProperties.$http = http; //demo.vue <script> export default { created(){ this.$http.get(); } } </script>复用组件功能之Mixin混入Mixin混入了解一下mixin混入,它是选项式API的一种复用代码的形式,可以非常灵活的复用功能。// mymixin.js const myMixin = { data(){ return { message: '复用的数据' } }, computed: { message2(){ return '复用的数据2' } } }; export { myMixin } // mymixin.vue <template> <div> <h2>mixin混入</h2> <div>{{ message }}</div> <div>{{ message2 }}</div> </div> </template> <script> import { myMixin } from '@/mymixin.js' export default { mixins: [myMixin] } </script>这样就可以直接在.vue中使用这些混入的功能。当然这种方式是局部混入写法,也可以进行全局混入的写法,代码如下:// main.js import { myMixin } from '@/mymixin.js' app.mixin(myMixin)mixin存在的问题也是有的,那就是不够灵活,不支持传递参数,这样无法做到差异化的处理,所以目前比较推荐的复用操作还是选择使用组合式API中的use函数来完成复用的逻辑处理。插件的概念及插件的实现插件是自包含的代码,通常向 Vue 添加全局级功能。例如:全局方法、全局组件、全局指令、全局mixin等等。基于Vue的第三方模块都是需要通过插件的方式在Vue中进行生效的,比如:Element Plus、Vue Router、Vuex等等。// myplugin.js import * as http from '@/http.js' export default { install(app, options){ console.log(options); app.config.globalProperties.$http = http; app.directive('auth', (el, binding) => { let auths = ['edit', 'delete']; let ret = auths.includes(binding.value); if(!ret){ el.style.display = 'none'; } }); app.component('my-head', { template: `<div>hello myhead</div>` }) } } // main.js 让插件生效 import myplugin from './myplugin.js' app.use(myplugin, { info: '配置信息' })可以看到,让插件生效的语法为app.use,这样就可以跟Vue结合到一起,所以插件就可以独立出去,成为第三方模块。Element Plus框架的安装与使用前面小节中介绍了自定义插件的实现,那么本小节来看下一比较出名的第三方插件Element Plus如何安装与使用。Element Plus框架Element Plus是一套基于PC端的组件库,可以直接应用到很多管理系统的后台开发中,使用前需要先下载安装,除了下载组件库以外,最好也下载组件库对应的icon图标模块,如下:npm install element-plus @element-plus/icons-vue接下来把element plus完整引入到Vue中,包装全局组件,全局样式。import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import * as ElementPlusIconsVue from '@element-plus/icons-vue' app.use(ElementPlus) for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) }基本使用方式element plus中提供了非常多的常见组件,例如:按钮、评分、表单控件等等。<template> <div> <h2>element plus</h2> <el-button @click="handleClick" type="primary" icon="Clock">Primary</el-button> <el-button @click="handleClick2" type="success">Success</el-button> <el-rate v-model="value1" /> <el-icon><Clock /></el-icon> </div> </template> <script> import { ElMessage, ElNotification } from 'element-plus'; export default { data(){ return { value1: 3 } }, mounted(){ setTimeout(()=>{ this.value1 = 5; }, 2000) }, methods: { handleClick(){ ElMessage.success('提示成功的消息'); }, handleClick2(){ ElNotification({ title: '邮件', message: '您今日的消费记录' }); } } } </script>除了常见的组件外,element plus中也提供了一些逻辑组件,这些逻辑组件是可以直接在JavaScript中进行使用,例如:ElMessage,ElNotification等方法。transition动画与过渡的实现在Vue中推荐使用CSS3来完成动画效果。当在插入、更新或从 DOM 中移除项时,Vue 提供了多种应用转换效果的方法。transition动画Vue中通过两个内置的组件来实现动画与过渡效果,分别是:<transition>和<transition-group>,代码如下:<template> <div> <h2>hello transition</h2> <button @click=" isShow = !isShow ">点击</button> <transition name="slide" mode="out-in"> <div v-if="isShow" class="box"></div> <div v-else class="box2"></div> </transition> </div> </template> <script> export default { data(){ return { isShow: true } } } </script> <style scoped> .box{ width: 200px; height: 200px; background: skyblue; } .box2{ width: 200px; height: 200px; background: pink; } .slide-enter-from{ opacity: 0; transform: translateX(200px); } .slide-enter-to{ opacity: 1; transform: translateX(0); } .slide-enter-active{ transition: 1s; } .slide-leave-from{ opacity: 1; transform: translateX(0); } .slide-leave-to{ opacity: 0; transform: translateX(200px); } .slide-leave-active{ transition: 1s; } </style>其中<transition>组件通过name属性去关联CSS中的选择器,CSS中的选择器主要有6种,分别:v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。 动画与过渡 默认情况下,进入和离开在两个元素身上是同时执行的,如果想改变其顺序,需要用到mode属性,其中out-in表示先离开再进入,而in-out表示先进入再离开。动态组件与keep-alive组件缓存动态组件动态组件可以实现在同一个容器内动态渲染不同的组件,依一个内置组件<component>的is属性的值,来决定使用哪个组件进行渲染。<template> <div> <h2>动态组件</h2> <button @click=" nowCom = 'my-com1' ">组件1</button> <button @click=" nowCom = 'my-com2' ">组件2</button> <button @click=" nowCom = 'my-com3' ">组件3</button> <component :is="nowCom"></component> </div> </template> <script> import MyCom1 from '@/13_MyCom1.vue' import MyCom2 from '@/14_MyCom2.vue' import MyCom3 from '@/15_MyCom3.vue' export default { data(){ return { nowCom: 'my-com1' } }, components: { 'my-com1': MyCom1, 'my-com2': MyCom2, 'my-com3': MyCom3 } } </script>keep-alive组件当我们点击的时候,就会进行组件的切换。在每次切换的过程中都会重新执行组件的渲染,这样组件操作的行为就会还原,而我们如何能够保证组件不变呢?可以利用<keep-alive>对组件进行缓存,这样不管如何切换,都会保持为初始的组件渲染,这样可以很好的保留之前组件的行为。组件的切换也可以配合<transition>完成动画的切换。<template> <div> <h2>动态组件</h2> <button @click=" nowCom = 'my-com1' ">组件1</button> <button @click=" nowCom = 'my-com2' ">组件2</button> <button @click=" nowCom = 'my-com3' ">组件3</button> <transition name="slide" mode="out-in"> <keep-alive> <component :is="nowCom"></component> </keep-alive> </transition> </div> </template>异步组件与Suspense一起使用异步组件在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。在上一个小节的动态组件的基础上,进行异步组件的演示。首先可以打开chrome浏览器的network网络,可以观察到在动态组件切换的时候,network网络中没有进行任何请求的加载,这证明了在初始的时候,相关的动态组件就已经加载好了。所以对于大型项目来说,如果能实现按需载入的话,那么势必会对性能有所提升,在Vue中主要就是利用defineAsyncComponent来实现异步组件的。<script> import { defineAsyncComponent } from 'vue' export default { data(){ return { nowCom: 'my-com1' } }, components: { 'my-com1': defineAsyncComponent(() => import('@/MyCom1.vue')), 'my-com2': defineAsyncComponent(() => import('@/MyCom2.vue')), 'my-com3': defineAsyncComponent(() => import('@/MyCom3.vue')) } } </script>Suspense组件由于异步组件是点击切换的时候才去加载的,所以可能会造成等待的时间,那么这个时候可以配合一个loading效果,在Vue中提供了一个叫做<Suspense>的组件用来完成loading的处理。<template> <suspense> <component :is="nowCom"></component> <template #fallback> <div>loading...</div> </template> </suspense> </template>跨组件间通信方案 Provide_Inject跨组件通信方案正常情况下,我们的组件通信是需要一级一级的进行传递,通过父子通信的形式,那么如果有多层嵌套的情况下,从最外层把数据传递给最内层的组件就非常的不方便,需要一级一级的传递下来,那么如何才能方便的做到跨组件通信呢?可以采用Provide 和 inject 依赖注入的方式来完成需求,代码如下: 依赖注入 // provide.vue <script> export default { provide(){ return { message: 'hello provide', count: this.count, getInfo(data){ console.log(data); } } } } </script> // inject.vue <template> <div> hello inject, {{ message }}, {{ count }} </div> </template> <script> export default { inject: ['message', 'getInfo', 'count'], mounted(){ this.getInfo('hello inject'); } } </script>Provide与Inject注意点保证数据是单向流动的,从一个方向进行数据的修改如果要传递响应式数据,需要把provide改造成工厂模式发送数据Teleport实现传送门功能Teleport组件Teleport可以实现传送门功能,也就是说逻辑属于当前组件中,而结构需要在组件外进行渲染,例如:按钮模态框组件。// 模态框.vue <template> <div> <button @click=" isShow = true ">点击</button> <teleport to="body"> <div v-if="isShow">模态框</div> </teleport> </div> </template> <script> export default { data(){ return { isShow: false } } } </script> // 调用模态框.vue <template> <div> <h2>传送门</h2> <my-modal></my-modal> </div> </template> <script> import MyModal from '@/模态框.vue' export default { components: { 'my-modal': MyModal } } </script>逻辑组件但是往往我们需要的并不是普通组件的调用方式,而是逻辑组件的调用方式,那么如何实现逻辑组件呢?代码如下:// 定义逻辑组件,modal.js import { createApp } from 'vue'; import ModalVue from '@/模态框.vue'; function modal(){ let div = document.createElement('div'); createApp(ModalVue).mount(div); document.body.append(div); } export default modal;// 调用逻辑组件 <template> <div> <h2>传送门</h2> <button @click="handleClick">点击</button> </div> </template> <script> import modal from '@/modal.js' export default { methods: { handleClick(){ modal(); } } } </script>13-虚拟DOM与render函数及Diff算法虚拟DOMVue框架帮我们完成了大量的DOM操作,那么在底层Vue并没有直接操作真实的DOM,因为真实的DOM直接去操作是非常耗性能的,所以最好在JS环境下进行操作,然后再一次性进行真实DOM的操作。const vnode = { type: 'div', props: { id: 'hello' }, children: [ /* 更多 vnode */ ] }那么在Vue中是如何把<template>模板中的字符串编译成虚拟DOM的呢?需要用到内置的render函数,这个函数可以把字符串转换成虚拟DOM。 虚拟DOM Diff算法当更新的时候,一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。而两个虚拟DOM进行对比的时候,需要加入一些算法提高对比的速度,这个就是Diff算法。 Diff算法 在脚手架下我们推荐使用来完成结构的编写,那么也可以直接通过render函数进行虚拟DOM的创建,代码如下:<!-- <template> <div> <h2>render</h2> </div> </template> --> <script> import { h } from 'vue'; export default { render(){ return h('div', h('h2', 'render2')) } } </script> <style scoped> </style>总结内容ref属性、nextTick方法自定义指令、Mixin混入插件的概念、Element Plus框架的使用动画与过渡、动态组件、异步组件跨组件通信、传送门、虚拟DOM与render函数
2022年12月23日
56 阅读
0 评论
0 点赞
2022-12-23
Vue3 学习笔记(二)
02-组件的概念及组件的基本使用方式03-组件之间是如何进行互相通信的04-组件的属性与事件是如何进行处理的05-组件的内容是如何组合与分发处理的06-仿Element Plus框架的el-button按钮组件实现07-单文件组件SFC及Vue CLI脚手架的安装使用08-脚手架原理之webpack处理html文件和模块打包09-脚手架原理之webpack启动服务器和处理sourcemap10-脚手架原理之webpack处理样式模块和图片模块11-脚手架原理之webpack处理单文件组件及loader转换12-Vite3介绍及基本使用13-仿Element Plus的el-rate评分组件实现(单文件组件)官网地址:https://cn.vuejs.org/Vue.js文件下载地址下载地址:https://unpkg.com/vue@3.2.36/dist/vue.global.js组件的概念及组件的基本使用方式组件的概念组件是带有名称的可复用实例,通常一个应用会以一棵嵌套的组件树的形式来组织,比如:页头、侧边栏、内容区等组件。 vue组件 组件可以拥有自己独立的结构,样式,逻辑。这样对于后期的维护是非常方便的。下面给出评分组件与按钮组件的抽离过程。 组件的划分 组件的命名方式与规范定义组件可通过驼峰、短线两种风格定义调用组件推荐短线方式<div id="app"> <my-head></my-head> </div> <script> let app = Vue.createApp({ data(){ return { } } }) app.component('my-head', { template: ` <header> <div>{{ message }}</div> <h2>logo</h2> <ul> <li>首页</li> <li>视频</li> <li>音乐</li> </ul> </header>`, data(){ return { message: 'hello world' } } }); let vm = app.mount('#app'); </script>根组件app容器可以看成根组件,所以根组件跟普通组件都具备相同的配置信息,例如:data、computed、methods等等选项。<div id="app"> <my-head></my-head> </div> <script> // 根组件 let RootApp = { data(){ return { } } }; // MyHead组件 let MyHead = { template: ` <header> <div>{{ message }}</div> <h2>logo</h2> <ul> <li>首页</li> <li>视频</li> <li>音乐</li> </ul> </header> ` }; let app = Vue.createApp(RootApp) app.component('MyHead', MyHead); let vm = app.mount('#app'); </script>根组件与MyHead组件形成了父子组件。局部组件与全局组件局部组件只能在指定的组件内进行使用,而全局组件可以在容器app下的任意位置进行使用。组件之间是如何进行互相通信的上一个小节中,我们了解了组件是可以组合的,那么就形成了父子组件,父子组件之间是可以进行通信的, 那么为什么要通信呢?主要是为了让组件满足不同的需求。 组件之间差异化 父子通信最常见的通信方式就是父传子,或者子传父,那么父传子通过props实现,而子传父则通过emits自定义事件实现。 父子通信 <div id="app"> <my-head :count="count" @custom-click="handleClick"></my-head> </div> <script> let app = Vue.createApp({ data(){ return { count: 10 } }, methods: { handleClick(data){ console.log(data); } } }) app.component('MyHead', { props: ['count'], emits: ['custom-click'], template: ` <header> <div>{{ count }}</div> <h2>logo</h2> <ul> <li>首页</li> <li>视频</li> <li>音乐</li> </ul> </header>`, mouted(){ this.$emit('custom-click', 'MyHead Data') } }); let vm = app.mount('#app'); </script>父子通信需要注意的点组件通信的props是可以定义类型的,在运行期间会进行检测组件之间的数据是单向流动的,子组件不能直接修改传递过来的值但是有时候也需要数据的双向流动,可利用v-model来实现组件的属性与事件是如何进行处理的有时候组件上的属性或事件并不想进行组件通信,那么Vue是如何处理的呢?组件的属性与事件默认不通过props接收的话,属性会直接挂载到组件容器上,事件也是如此,会直接挂载到组件容器上。可通过 inheritAttrs 选项阻止这种行为,通过指定这个属性为false,可以避免组件属性和事件向容器传递。可通过 $attrs 内置语法,给指定元素传递属性和事件,代码如下:<div id="app"> <my-head title="hello world" class="box" @click="handleClick"></my-head> </div> <script> let app = Vue.createApp({ data(){ return { } }, methods: { handleClick(ev){ console.log(ev.currentTarget); } } }) app.component('MyHead', { template: ` <header> <h2 v-bind:title="$attrs.title">logo</h2> <ul v-bind:class="$attrs.class"> <li>首页</li> <li>视频</li> <li>音乐</li> </ul> </header> `, mounted(){ console.log( this.$attrs ); // 也可以完成父子通信操作 }, inheritAttrs: false // 阻止默认的属性传递到容器的操作 }); let vm = app.mount('#app'); </script>$attrs也可以实现组件之间的间接通信。组件的内容是如何组合与分发处理的在前面的小节中,我们学习了组件之间的通信,让组件之间实现了不同的需求,我们通过给组件添加不同的属性来实现。那么在Vue中如何去传递不同的组件结构呢?这就涉及到了组件内容的分发处理。插槽slot在Vue中是通过插槽slot方式来进行分发处理的,Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 元素作为承载分发内容的出口。<div id="app"> <my-head> <p>logo</p> </my-head> </div> <script> let app = Vue.createApp({ data(){ return { message: 'hello' } } }) app.component('MyHead', { data(){ return { }; }, template: ` <header> <slot></slot> </header>`, }); let vm = app.mount('#app'); </script>组件内的结构,即<p>logo</p>会被分发到<slot></slot>所在的区域。内容分发与插槽的注意点渲染作用域 -> 插槽只能获取当前组件的作用域具名插槽 -> 处理多个插槽的需求,通过v-slot指令实现,简写为#作用域插槽 -> 希望插槽能访问子组件的数据完整代码如下:<div id="app"> <my-head> <template #title> <p>logo, {{ message }}, {{ count }}</p> </template> <template #list="{ list }"> <ul> <li v-for="item in list">{{ item }}</li> </ul> </template> </my-head> </div> <script> let app = Vue.createApp({ data(){ return { message: 'hello' } } }) app.component('MyHead', { data(){ return { count: 123, list: ['首页', '视频', '音乐'] }; }, template: ` <header> <slot name="title"></slot> <hr> <slot name="list" :list="list"></slot> </header> `, }); let vm = app.mount('#app'); </script>单文件组件SFC及Vue CLI脚手架的安装使用Vue 单文件组件(又名 *.vue 文件,缩写为 SFC)是一种特殊的文件格式,它允许将 Vue 组件的模板、逻辑 与 样式封装在单个文件中。为什么要使用 SFC使用 SFC 必须使用构建工具,但作为回报带来了以下优点:使用熟悉的 HTML、CSS 和 JavaScript 语法编写模块化的组件让本来就强相关的关注点自然内聚预编译模板,避免运行时的编译开销组件作用域的 CSS在使用组合式 API 时语法更简单通过交叉分析模板和逻辑代码能进行更多编译时优化更好的 IDE 支持,提供自动补全和对模板中表达式的类型检查开箱即用的模块热更新 (HMR) 支持如何支持SFC可通过项目脚手架来进行支持,Vue支持Vite脚手架和Vue CLI脚手架。这里我们先来介绍Vue CLI的基本使用方式。# 安装 npm install -g @vue/cli # 创建项目 vue create vue-study # 选择default default (babel, eslint) # 启动脚手架 npm run serve通过localhost:8080进行访问。脚手架文件的组成src/main.js -> 主入口模块src/App.vue -> 根组件src/components -> 组件集合src/assets -> 静态资源单文件的代码组成template -> 编写结构script -> 编写逻辑style -> 编写样式 单文件组件 其中style中的scoped属性,可以让样式成为局部的,不会影响到其他组件,只会作用于当前组件生效,同时在脚手架下支持常见的文件进行模块化操作,例如:图片、样式、.vue文件等。
2022年12月23日
113 阅读
0 评论
0 点赞
2022-12-23
Vue3学习笔记(一)
03-选项式API的编程风格与优势04-声明式渲染及响应式数据实现原理05-指令系统与事件方法及传参处理06-计算属性与侦听器区别与原理(一)06-计算属性与侦听器区别与原理(二)07-条件渲染与列表渲染及注意点08-class样式与style样式的三种形态09-表单处理与双向数据绑定原理10-生命周期钩子函数及原理分析11-搜索关键词加筛选条件的综合案例选项式API的编程风格与优势选项式API,即:options APIlet vm = createApp({ methods: {}, computed: {}, watch: {}, data(){}, mounted(){} })这种写法的优势:只有一个参数,不会出现参数顺序的问题,随意调整配置的位置非常清晰,语法化特别强非常适合添加默认值的声明式渲染及响应式数据实现原理Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统:<div id="counter"> Counter: {{ counter }} </div>const Counter = { data() { return { counter: 0 } } } Vue.createApp(Counter).mount('#counter')声明式编程:不需要编写具体是如何实现的,直接调用声明就可以实现功能。SQL就是比较经典的声明式语言:SELECT * from user WHERE username = xiaomingfor(var i=0;i<user.length;i++) { if(user[i].username == "xiaoming") { print("find"); break; } }注意:数据是通过 {{ }} 模板语法来完成的,模板语法支持编写JS表达式响应式数据实现的原理:利用JS的Proxy对象。Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,其实就是直接监控值的修改,当值发生改变的时候,可以监控到。<div id="app"></div> <script> let data = new Proxy( { message: "hello", }, { get(target) { console.log("get") return target.message }, set(target, key, value) { console.log("set") app.innerHTML = value; }, } ) app.innerHTML = data.message; setTimeout(() => { data.message = "hi" }, 2000) </script>指令系统与事件方法及传参处理指令系统就是通过自定义属性实现的一套功能,也是声明式编程的体现。通常在标签上添加 v-* 字样,常见的指令有:v-bind -> 操作标签属性,可通过 : 简写v-on -> 操作事件,可通过 @ 简写<div id="app"> <p :title="message">这是一个段落</p> <button @click=" message = 'hi' ">点击</button> </div> {{ message }} <script> let vm = Vue.createApp({ data(){ return { message: 'hello' } } }).mount('#app') </script>如何添加事件方法,通过methods选项API实现,并且Vue框架已经帮我们帮事件传参机制处理好了。<div id="app"> <button @click="toClick($event, 123)">点击</button> </div> <script> let vm = Vue.createApp({ methods: { toClick(ev, num){ } } }).mount('#app') </script>计算属性与侦听器区别与原理计算属性模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护,所以过于复杂的逻辑可以移植到计算属性中进行处理。<div id="app"> {{ reverseMessage }} </div> <script> let vm = Vue.createApp({ data(){ return { message: 'hello' } }, computed: { reverseMessage(){ return this.message.split('').reverse().join('') } } }).mount('#app') </script>计算属性与方法比较像,如下所示:<div id="app"> {{ reverseMessageMethod() }}<br> {{ reverseMessageMethod() }}<br> {{ reverseMessage }}<br> {{ reverseMessage }}<br> </div> <script> let vm = Vue.createApp({ data(){ return { message: 'hello world' } }, methods: { reverseMessageMethod(){ console.log(1); return this.message.split(' ').reverse().join(' '); } }, computed: { reverseMessage(){ console.log(2); return this.message.split(' ').reverse().join(' '); } } }).mount('#app'); </script>计算属性跟方法相比,具备缓存的能力,而方法不具备缓存,所以上面代码执行完,会弹出两次1和一次2。注意:默认是只读的,一般不会直接更改计算属性,如果想更改也是可以做到的,通过Setter写法实现,官方地址。既然计算属性编写的是一个函数,而调用的时候以函数名的形式进行使用,其实实现起来也不是特别难的事情:let computed = { num(){ return 123; } } let vm = {} for(let attr in computed){ Object.defineProperty(vm, attr, { value: computed[attr]() }) }侦听器虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。侦听器的目的:侦听器用来观察和响应Vue实例上的数据变动,类似于临听机制+事件机制。当有一些数据需要随着其它数据变化而变化时,就可以使用侦听器。<div id="app"> {{ message }} </div> <script> let vm = Vue.createApp({ data(){ return { message: 'hello' } }, watch: { message(newVal, oldVal){ } } }).mount('#app') </script>有时候,计算属性 和 侦听器 往往能实现同样的需求,那么他们有何区别呢?计算属性适合:多个值去影响一个值的应用;而侦听器适合:一个值去影响多个值的应用侦听器支持异步的程序,而计算属性不支持异步的程序class样式与style样式的三种形态操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind 处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind 用于 class 和 style 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。字符串数组对象let vm = Vue.createApp({ data() { return { myClass1: 'box box2', myClass2: ['box', 'box2'], myClass3: { box: true, box2: true }, myStyle1: 'background: red; color: white;', myStyle2: ['background: red', 'color: white'], myStyle3: { background: 'red', color: 'white' }, } }, }).mount("#app")数组和对象的形式要比字符串形式更加的灵活,也更容易控制变化。表单处理与双向数据绑定原理表单是开发过程中经常要进行操作的,一般需要收集表单数据,发送给后端,或者把后端的数据进行回显等。在Vue中是通过v-model指令来操作表单的,可以非常灵活的实现响应式数据的处理。<div id="app"> <input type="text" v-model="message"> {{ message }} </div> <script> let vm = Vue.createApp({ data() { return { message: 'hello' } } }).mount("#app") </script>尽管有些神奇,但 v-model 本质上不过是语法糖。可通过value属性 + input事件来实现同样的效果。<div id="app"> <input type="text" :value="message" @input="message = $event.target.value"> {{ message }} </div> <script> let vm = Vue.createApp({ data() { return { message: 'hello' } } }).mount("#app") </script>v-model除了可以处理输入框以外,也可以用在单选框、复选框、以及下拉菜单中。<div id="app"> <input type="checkbox" v-model="fruits" value="苹果">苹果<br> <input type="checkbox" v-model="fruits" value="西瓜">西瓜<br> <input type="checkbox" v-model="fruits" value="哈密瓜">哈密瓜<br> {{ fruits } <input type="radio" v-model="gender" value="女">女<br> <input type="radio" v-model="gender" value="男">男<br> {{ gender }} <select v-model="city"> <option value="北京">北京</option> <option value="上海">上海</option> <option value="杭州">杭州</option> </select> {{ city }} </div> <script> let vm = Vue.createApp({ data(){ return { fruits: ['西瓜', '哈密瓜'], gender: '男', city: '杭州' } } }).mount('#app'); </script>生命周期钩子函数及原理分析每个组件在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。就是工厂的流水线,每个工人站在各自的岗位,当任务流转到工人身边的时候,工人就开始工作。简单来说生命周期钩子函数就是回调函数,在Vue的某个时机去调用对应的回调函数。就像定时器一样,谁调用的定时器的回调函数呢?其实就是定时器内部在调用的。setTimeout(()=>{ console.log('2秒后被执行了'); }, 2000)官方提供的生命周期图示生命周期可划分为三个部分:初始阶段:beforeCreate、created、beforeMount、mounted更新阶段:beforeUpdate、updated销毁阶段:beforeUnmout、unmounted注:一般在,created,mounted中都可以发送数据请求,但是,大部分时候,会在created发送请求。因为这样可以更短的时间去响应数据。搜索关键词加筛选条件的综合案例准备好案例的JSON数据[ { "id": 1, "name": "小明", "gender": "女", "age": 20 }, { "id": 2, "name": "小强", "gender": "男", "age": 18 }, { "id": 3, "name": "大白", "gender": "女", "age": 25 }, { "id": 4, "name": "大红", "gender": "男", "age": 22 } ]参考代码<style> .active-gender{ background: red; } </style> <div id="app"> <input type="text" v-model="message"> <button :class="activeGender('全部')" @click="handleGender('全部')">全部</button> <button :class="activeGender('男')" @click="handleGender('男')">男</button> <button :class="activeGender('女')" @click="handleGender('女')">女</button> <ul> <li v-for="item in filterList" :key="item.id">{{ item.name }}, {{ item.gender }}, {{ item.age }}</li> </ul> </div> <script> let vm = Vue.createApp({ data() { return { list: [], message: '', gender: '全部' } }, created(){ fetch('./02-data.json').then((res)=> res.json()).then((res)=>{ this.list = res; }) }, computed: { filterList(){ return this.list .filter((v)=> v.name.includes(this.message)) .filter((v)=> v.gender === this.gender || '全部' === this.gender); } }, methods: { activeGender(gender){ return { 'active-gender': this.gender === gender }; }, handleGender(gender){ this.gender = gender; } } }).mount('#app'); </script>总结内容了解核心思想,例如:MVVM设计模式、选项式API优势了解Vue3各个选项的用法,例如:data、methods、computed等掌握常见的指令:v-bind、v-on、v-if、v-for等掌握样式操作、表单操作等行为了解Vue3的生命周期钩子函数,及如何使用
2022年12月23日
31 阅读
0 评论
1 点赞
2022-11-10
Vue3学习笔记
Vue3全家桶:Vue3,Composition API,Options API,Element Plus,Vue Router,Vue CLI,Vuex,Pinia,ViteVue3核心知识点:MVVM设计模式、响应式数据实现原理、指令系统、计算属性与侦听器、条件渲染与列表渲染、class样式与style样式的三种形态、表单处理、生命周期钩子函数、组件通信、组件属性与事件、组件内容分发、脚手架使用及原理实现、ref属性在元素和组件上、nextTick监听DOM、自定义指令及应用场景、组件功能之Mixin混入、插件的概念及插件的实现、动态组件与keep-alive组件缓存、transition动画与过渡的实现、异步组件与Suspense, Provide_Inject、Teleport、虚拟DOM与render函数及Diff算法、setup方法与script_setup及ref响应式、reactive,toRefs, watchEffect, defineProps...TypeScript与框架结合Vue组合式+TS,状态管理+TSVue选项式+TS,组件库+TSReact类组件+TS,Axios+TSReact函数组件+TS,路由+TSTypeScript核心知识点类型声明空间与变量声明空间、类型注解与类型推断、类型分类与联合类型与交叉类型、never类型与any类型与unknown类型、类型断言与非空断言、数组类型与元祖类型、对象类型与索引签名、函数类型与void类型、函数重载与可调用注解、枚举类型与const枚举、详解接口与类型别名之间区别、字面量类型和keyof关键字、类型保护与自定义类型保护、定义泛型和泛型常见操作、类型兼容性详解、映射类型与内置工具类型、条件类型和infer关键字、类中如何使用类型、模块系统与命名空间、d.ts声明文件和declare关键字、@types和DefinitelyTyped仓库、lib.d.ts和global.d.ts、tsconfig.json...MVC设计模式与MVVM设计模式选项式API的编程风格与优势指令系统与事件方法及传参处理声明式宣染及响应式数据实现原理计算属性与侦听器区别与原理条件渲染与列表宣染及注意点class样式与style样式的三种形态表单处理与双向数据绑定原理生命周期钩子函数及原理分析MVC设计模式与MVVM设计模式为什么使用Vue框架?◆最大的优点:就是不用自己完成复杂的DOM操作了,而由框架帮我们去完成选项式API的编程风格与优势:let vm = createApp( /* 选项*/ methods: {}, computed: {}, watch:{}, data(){}, mounted(){} }).mount(' #app');💠只有一个参数,不会出现参数顺序的问题,随意调整配置的位置 💠 非常清晰,语法化特别强 💠 非常适合添加默认值的声明式宣染及响应式数据实现原理💠利用ES6的Proxy对象对底层进行了监控计算属性与侦听器区别与原理🔸计算属性适合: 多个值去影响一个值的应用;而侦听器适合一个值去影响多个值的应用🔸侦听器支持异步的程序,而计算属性不支持异步的程序🔷 条件渲染:使用v-if指令条件性地宣染一块内容。这块内容只会在指令的表达式返回truthy真值的时候被渲染🔷 falsy假值 (即 false,0,-0, On, "“,null, undefined和NaN)条件宣染与列表渲染及注意点:列表宣染需要添加key属性 -> 用来跟踪列表的身份v-if和v-for尽量不要一起使用 -> 利用计算属性来完成这类功能template标签起到的作用 -> 形成一个整体容器并且不会被宣染<div id="app"> <!-- <input- type="text" - v-model="message">--> <input type="text" :value="message" @input="message = $event.target.value"> </div>
2022年11月10日
7 阅读
0 评论
1 点赞
2022-10-13
javascript框架-Vue
Vue.js西岭老湿第0章 Vue介绍0.0 开发工程发展历史通过前面的介绍,我们对目前的项目工程化有了大体了了解,那么其中,在第二阶段的工程化演进中,有一个重要的工程设计理念诞生,他就是著名的 MVC 设计模式,简单点,MVC 其实就是为了项目工程化的一种分工模式;MVC 中的最大缺点就是单项输入输出,所有的 M 的变化及 V 层的变化,必须通过 C 层调用才能展示;随着前端技术及前端工程化体系的发展成熟,参考MVC的设计理念,前端出现了 MVVM 的设计思想,简单理解就是在前端实现数据层与展示层的相互调用,降低业务层面的交互逻辑;后面再进行详细介绍;0.1 Vue 介绍Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的 渐进式框架。注意:Vue是一个框架,相对于 jq 库来说,是由本质区别的;https://cn.vuejs.org/Vue 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性。但它支持所有兼容 ECMAScript 5 的浏览器。0.2 Vue 初体验直接下载引入:https://cn.vuejs.org/v2/guide/installation.htmlCDN 引入:<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>CDN 加速: https://www.bootcdn.cn/<body> <div id="div"> {{user_name}} </div> </body> // 两种引入方式,任意选择 <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> <script src="./vue.js"></script> <script> var app = new Vue({ el:'#div', // 设置要操作的元素 // 要替换的额数据 data:{ user_name:'我是一个div' } }) </script>第1章 Vue 实例对象每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例 开始的:var vm = new Vue({ // 选项 })<body> <div id="div"> {{user_name}} </div> </body> <script src="./vue.js"></script> <script> var app = new Vue({ el:'#div', // 设置要操作的元素 // 要替换的额数据 data:{ user_name:'我是一个div' } }) // 打印Vue实例对象 console.log(app); </script>通过打印实例对象发现,其中 el 被Vue 放入了公有属性中,而data 则被放入了 私有属性中,而 data 中的数据,需要被外部使用,于是 Vue 直接将data 中的属性及属性值,直接挂载到 Vue 实例中,也就是说,data中的数据,我们可以直接使用 app.user_name 直接调用;var app = new Vue({ el:'#div', // 设置要操作的元素 // 要替换的额数据 data:{ user_name:'我是一个div', user:222222 } }) console.log(app.user_name);第 2 章 模板语法-插值我们在前面的代码中,使用 {{}} 的形式在 html 中获取实例对象对象中 data 的属性值;这种使用 {{}} 获取值得方式,叫做 插值 或 插值表达式 ;2.1 文本数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值:<span>Message: {{ msg }}</span>Mustache 标签将会被替代为对应数据对象上 msg 属性的值。无论何时,绑定的数据对象上 msg 属性发生了改变,插值处的内容都会更新。即便数据内容为一段 html 代码,仍然以文本内容展示<body> <div id="div"> 文本插值 {{html_str}} </div> </body> <script> var app = new Vue({ el:'#div', data:{ html_str:'<h2>Vue<h2>' } }) </script>浏览器渲染结果:<div id="div">文本插值 <h2>Vue<h2></div>打开浏览器的 REPL 环境 输入 app.html_str = '<s>vue</s>'浏览器渲染结果就会立刻发生改变: <div id="div">文本插值 <s>vue</s></div>2.2 使用 JavaScript 表达式迄今为止,在我们的模板中,我们一直都只绑定简单的属性键值。但实际上,对于所有的数据绑定,Vue.js 都提供了完全的 JavaScript 表达式支持,但是不能使用 JS 语句;(表达式是运算,有结果;语句就是代码,可以没有结果)<body> <div id="div" > {{ un > 3 ? '大' : '小'}} {{ fun() }} </div> </body> <script> var app = new Vue({ el:'#div', data:{ un:2, fun:()=> {return 1+2} } }) </script>第3章 模板语法-指令指令 (Directives) 是带有 v- 前缀的特殊 特性 相当于自定义html属性。指令特性的值预期是单个 JavaScript 表达式 (v-for 是例外情况,稍后我们再讨论)。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM;参考 手册 、 API<body> <div id="div" > <p v-if="seen">现在你看到我了</p> </div> </body> <script> var app = new Vue({ el:'#div', data:{ seen:false } }) </script>这里,v-if 指令将根据表达式 seen 的值的真假来插入/移除 <p> 元素。3.1 v-text / v-html 文本https://cn.vuejs.org/v2/api/#v-texthttps://cn.vuejs.org/v2/api/#v-html<body> <div id="div" {{class}}> <p v-text="seen"></p> <p v-html="str_html"></p> </div> </body> <script> var app = new Vue({ el:'#div', data:{ seen:'<h1>Vue</h1>', str_html:'<h1>Vue</h1>', class:'dd', } }) </script>注意:v-textv-text和差值表达式的区别v-text 标签的指令更新整个标签中的内容(替换整个标签包括标签自身)差值表达式,可以更新标签中局部的内容v-html可以渲染内容中的HTML标签尽量避免使用,否则会带来危险(XSS攻击 跨站脚本攻击)HTML 属性不能用 {{}} 语法3.2 v-bind 属性绑定https://cn.vuejs.org/v2/api/#v-bind可以绑定标签上的任何属性。动态绑定图片的路径<img id=“app” v-bind:src="src" /> <script> var vm = new Vue({ el: '#app', data: { src: '1.jpg' } }); </script>绑定a标签上的id<a id="app" v-bind:href="'del.php?id=' + id">删除</a> <script> var vm = new Vue({ el: '#app', data: { id: 11 } }); </script>绑定class对象语法和数组语法对象语法如果isActive为true,则返回的结果为 <div id="app" class="active"></div><div id="app" v-bind:class="{active: isActive}"> hei </div> <script> var vm = new Vue({ el: '#app', data: { isActive: true } }); </script>数组语法渲染的结果: <div id="app" class="active text-danger"></div><div id="app" v-bind:class="[activeClass, dangerClass]"> hei </div> <script> var vm = new Vue({ el: '#app', data: { activeClass: 'active', dangerClass: 'text-danger' } }); </script>绑定style对象语法和数组语法对象语法渲染的结果: <div id="app" style="color: red; font-size: 40px;">hei</div><div id="app" v-bind:style="{color: redColor, fontSize: font + 'px'}"> hei </div> <script> var vm = new Vue({ el: '#app', data: { redColor: 'red', font: 40 } }); </script>数组语法渲染结果:<div id="app" style="color: red; font-size: 18px;">abc</div><div id="app" v-bind:style="[color, fontSize]">abc</div> <script> var vm = new Vue({ el: '#app', data: { color: { color: 'red' }, fontSize: { 'font-size': '18px' } } }); </script>v-bind 简化语法<div id="app"> <img v-bind:src="imageSrc"> <!-- 缩写 --> <img :src="imageSrc"> </div> <script> var vm = new Vue({ el: '#app', data: { imageSrc: '1.jpg', } }); </script>3.3 数据绑定3.3.1 单向数据绑定<div id="div"> <input type="text" :value="input_val"> </div> <script> var app = new Vue({ el: '#div', data: { input_val: 'hello world ' } }) </script>浏览器渲染结果: <div id="div"><input type="text" value="hello world"></div>通过浏览器 REPL 环境可以进行修改 app.input_val = 'Vue'浏览器渲染结果: <div id="div"><input type="text" value="Vue"></div>我们通过 vue 对象修改数据可以直接影响到 DOM 元素,但是,如果直接修改 DOM 元素,却不会影响到 vue 对象的数据;我们把这种现象称为 单向数据绑定 ;3.3.2 双向数据绑定 v-modelhttps://cn.vuejs.org/v2/api/#v-model<div id="div"> <input type="text" v-model="input_val" > </div> <script> var app = new Vue({ el: '#div', data: { input_val: 'hello world ' } }) </script>通过 v-model 指令展示表单数据,此时就完成了 双向数据绑定 ;不管 DOM 元素还是 vue 对象,数据的改变都会影响到另一个;注意:数据绑定是目前所有MVVM前端框架的核心特性;甚至可以说,没有数据绑定就不能算是框架;3.3.3 双向数据绑定的应用范围文本框 & 文本域<div id="div"> <textarea v-model="inp_val"></textarea> <div>{{ inp_val }}</div> </div> <script> var app = new Vue({ el: '#div', data: { inp_val: '' } }) </script>绑定复选框<div id="div"> 吃饭:<input type="checkbox" value="eat" v-model="checklist"><br> 睡觉:<input type="checkbox" value="sleep" v-model="checklist"><br> 打豆豆:<input type="checkbox" value="ddd" v-model="checklist"><br> {{ checklist }} </div> <script> var vm = new Vue({ el: '#div', data: { checklist: '' // checklist: [] } }); </script>绑定单选框<div id="app"> 男<input type="radio" name="sex" value="男" v-model="sex"> 女<input type="radio" name="sex" value="女" v-model="sex"> <br> {{sex}} </div> <script> var vm = new Vue({ el: '#app', data: { sex: '' } }); </script>修饰符.lazy - 取代 input 监听 change 事件.number - 输入字符串转为有效的数字.trim - 输入首尾空格过滤<div id="div"> <input type="text" v-model.lazy="input_val"> {{input_val}} </div> <script> var app = new Vue({ el: '#div', data: { input_val: 'hello world ' } }) </script>3.4 v-on 绑定事件监听https://cn.vuejs.org/v2/api/#v-onhttps://cn.vuejs.org/v2/guide/events.html3.4.1 基本使用<div id="app"> <input type="button" value="按钮" v-on:click="cli"> </div> <script> var vm = new Vue({ el: '#app', data: { cli:function(){ alert('123'); } } }); </script>上面的代码运行是没有问题的,但是,我们不建议这样做,因为 data 是专门提供数据的对象,事件触发需要执行的是一段代码,需要的是一个方法 (事件处理程序) ;修改代码如下:<div id="app"> <!-- 使用事件绑定的简写形式 --> <input type="button" value="按钮" @click="cli"> </div> <script> var vm = new Vue({ el: '#app', data: {}, // 将事件处理程序写入methods对象 methods: { cli: function () { alert('123'); } } }); </script>向事件处理器中传参<div id="app"> <!-- 直接调用传参即可 --> <input type="button" value="按钮" @click="cli(1,3)"> </div> <script> var vm = new Vue({ el: '#app', data: {}, methods: { // 接受参数 cli: function (a,b) { alert(a+b); } } }); </script>而此时,如果在处理器中需要使用事件对象,则无法获取,我们可以用特殊变量 $event 把它传入方法<input type="button" value="按钮" @click="cli(1,3,$event)">methods: { // 接受参数 cli: function (a,b,ev) { alert(a+b); console.log(ev); } }3.4.2 事件修饰符原生 JS 代码,想要阻止浏览器的默认行为(a标签跳转、submit提交),我们要使用事件对象的 preventDefault() 方法<div id="app"> <a href="http://www.qq.com" id="a">腾百万</a> </div> <script> document.getElementById('a').onclick = (ev)=>{ // 组织浏览器的默认行为 ev.preventDefault(); } </script>使用修饰符 阻止浏览器的默认行为<div id="app"> <a href="http://www.qq.com" @click.prevent="cli">腾百万</a> </div> <script> var vm = new Vue({ el: '#app', data: {}, // 将事件处理程序写入methods对象 methods: { cli: function () { alert('123'); } } }); </script>使用修饰符绑定一次性事件<div id="app"> <a href="http://www.qq.com" @click.once="cli($event)">腾百万</a> </div> <script> var vm = new Vue({ el: '#app', data: {}, // 将事件处理程序写入methods对象 methods: { cli: function (ev) { ev.preventDefault(); alert('123'); } } }); </script>3.4.3 按键修饰符绑定键盘抬起事件,但是只有enter 键能触发此事件<div id="app"> <input type="text" @keyup.enter="keyup"> </div> <script> var vm = new Vue({ el: '#app', data: {}, methods: { keyup:()=>{ console.log('111') } } }); </script>3.4.4 系统修饰符按住 shift 后才能触发点击事件<div id="app"> <input type="button" value="按钮" @click.shift="cli"> </div> <script> var vm = new Vue({ el: '#app', data: {}, methods: { cli:()=>{ console.log('111') } } }); </script>3.4.5 鼠标修饰符鼠标中键触发事件<div id="app"> <input type="button" value="按钮" @click.middle="cli"> </div> <script> var vm = new Vue({ el: '#app', data: {}, methods: { cli:()=>{ console.log('111') } } }); </script>3.4.6 为什么在 HTML 中监听事件?你可能注意到这种事件监听的方式违背了关注点分离 (separation of concern) 这个长期以来的优良传统。但不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。实际上,使用 v-on 有几个好处:扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何清理它们。3.5 v-show 显示隐藏https://cn.vuejs.org/v2/api/#v-show根据表达式之真假值,切换元素的 display CSS 属性。<div id="app"> <p v-show="is_show">Vue</p> </div> <script> var vm = new Vue({ el:'#app', data:{ is_show:false }, methods:{}, }) </script>案例:点击按钮切换隐藏显示<div id="app"> <input type="button" value="按钮" @click="isshow"> <p v-show="is_show">Vue</p> </div> <script> var vm = new Vue({ el:'#app', data:{ is_show:false }, methods:{ isshow:function(){ this.is_show = !this.is_show; } }, }) </script>3.6 v-if / v-else / v-else-if 条件判断https://cn.vuejs.org/v2/api/#v-if<div id="app"> <div v-if="type === 'A'"> A </div> <div v-else-if="type === 'B'"> B </div> <div v-else-if="type === 'C'"> C </div> <div v-else> Not A/B/C </div> </div> <script> var vm = new Vue({ el: '#app', data: { type: 'F' }, }) </script>3.7 v-for 循环https://cn.vuejs.org/v2/api/#v-for<div id="app"> <ul> <li v-for="(val,key) in arr">{{val}}---{{key}}</li> </ul> <ul> <li v-for="(val,key) in obj">{{val}}---{{key}}</li> </ul> </div> <script> var vm = new Vue({ el: '#app', data: { arr: ['a', 'b', 'c'], obj: { id: 1, name: '李四' } }, }) </script>3.8 v-cloakhttps://cn.vuejs.org/v2/api/#v-cloak和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。<div id="app"> <p>{{obj.id}}</p> </div> <script src="./vue.js"></script> <script> setTimeout(() => { var vm = new Vue({ el: '#app', data: { arr: ['a', 'b', 'c'], obj: { id: 1, name: '李四' } }, }) }, 2000); </script>当我们的网络受阻时,或者页面加载完毕而没有初始化得到 vue 实例时,DOM中的 {{}} 则会展示出来;为了防止现象,我们可以使用 CSS 配合 v-cloak 实现获取 VUE 实例前的隐藏;<style> [v-cloak] { display: none; } </style> <div id="app"> <p v-cloak>{{obj.id}}</p> </div> <script src="./vue.js"></script> <script> setTimeout(() => { var vm = new Vue({ el: '#app', data: { obj: { id: 1, name: '李四' } }, }) }, 2000); </script>3.9 v-oncehttps://cn.vuejs.org/v2/api/#v-once只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过<div id="app"> <p v-once>{{msg}}</p> </div> <script> var vm = new Vue({ el: '#app', data: { msg:'kkk' }, }) </script>第4章 TodoList 案例上市产品: ToDoList 、奇妙清单 、滴答清单学习练手项目 : TodoMVC 、 Vue官方示例为什么选择这样的案例:产品功能简洁,需求明确,所需知识点丰富;实现基本功能容易,涵盖所学基础知识;而可扩展性强,完善所有功能比较复杂,所需技术众多;在学习中,可以灵活取舍;4.1 项目初始化在项目目录中执行 npm install 命令,下载所需静态资源 ; 将Vue.js框架代码,复制到 js 目录,在index.html中引入 vue : <script src="./js/vue.js"></script>同时 在 index.html 最下方,项目引入了app.js ; 而我们要写的 vuejs 代码,都放在这个文件中;4.2 数据遍历const list_data = [ {id:1,title:'吃饭',stat:true}, {id:2,title:'睡觉',stat:false}, {id:3,title:'打豆豆',stat:true}, ] new Vue({ el:'#todoapp', data:{ // list_data:list_data, list_data,// es6属性简写 } })<ul class="todo-list"> <li v-for="(val,key) in list_data"> <div class="view"> <input class="toggle" type="checkbox" v-model="val.stat"> <label>{{val.title}}</label> <button class="destroy"></button> </div> <input class="edit" value="Rule the web"> </li> </ul>4.3 展示无数据状态标签及内容都是在 section footer 两个标签中的,当 list_data 中没有数据时,我们只需要隐藏这个两个标签即可:<section v-if="list_data.length" class="main"> …… </section> <footer v-if="list_data.length" class="footer"> …… </footer>两个标签都有 v-if 判断 ,因此我们可以使用一个 div 包裹两个标签,使 div 隐藏即可:<div v-if="list_data.length"> <section class="main"> …… </section> <footer class="footer"> …… </footer> </div>如果有内容,那么 DOM 书中就会多出一个 div 标签,那么我们可以选择使用 template (vue中的模板标识),有内容时,浏览器渲染不会有此节点;<template v-if="list_data.length"> <section class="main"> …… </section> <footer class="footer"> …… </footer> </template>4.3 添加任务绑定 enter 键盘事件:<input @keyup.enter="addTodo" class="new-todo" placeholder="请输入" autofocus>new Vue({ el:'#todoapp', data:{ // list_data:list_data, list_data,// es6属性简写 }, //添加事件处理器 methods:{ // addTodo:function(){} // 简写形式 addTodo(){ console.log(123); } } })修改代码完成任务添加:methods: { // 添加任务 // addTodo:function(){} // 简写形式 addTodo(ev) { // 获取当前触发事件的元素 var inputs = ev.target; // 获取value值,去除空白后判断,如果为空,则不添加任务 if (inputs.value.trim() == '') { return; } // 组装任务数据 var todo_data = { id: this.list_data.length + 1 + 1, title: inputs.value, stat: false }; // 将数据添加进数组 this.list_data.push(todo_data); // 清空文本框内容 inputs.value = ''; } }4.4 任务的全选与反选点击文本框左边的下箭头,实现全选和反选操作为元素绑定点击事件:<input @click="toggleAll" id="toggle-all" class="toggle-all" type="checkbox">添加处理程序:toggleAll(ev){ // 获取点击的元素 var inputs = ev.target; // console.log(inputs.checked); // 循环所有数据为状态重新赋值 // 因为每个元素的选中状态都是使用 v-model 的双向数据绑定, // 因此 数据发生改变,状态即改变,状态改变,数据也会改变 for(let i=0;i<this.list_data.length;i++){ this.list_data[i].stat = inputs.checked; } }4.5 完成任务如果任务完成,状态改为选中, li 的 class 属性为 completed 时文字有中划线;<li v-for="(val,key) in list_data" v-bind:class="{completed:val.stat}">4.6 删除任务绑定点击事件,将当前索引值传入事件处理程序:<button @click="removeTodo(key)" class="destroy"></button>按照索引,删除相应的数据:removeTodo(key){ this.list_data.splice(key,1); },4.7 删除已完成的任务绑定事件<button @click="removeAllDone" class="clear-completed">Clear completed</button>循环遍历所有数据,删除已被标记为完成的任务:removeAllDone(){ for(let i=0;i<list_data.length;i++){ if(list_data[i].stat == true){ this.list_data.splice(i,1); } } }循环的代码看起来很不舒服, Array.prototype.filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。var arr = [1,4,6,2,78,23,7,3,8]; // 原始写法 // var new_arr = arr.filter(function(v){ // // if(v>8){ // // return true; // // } // return v>8; // }) // 箭头函数写法 // var new_arr = arr.filter((v)=>{ // return v>8; // }) // 精简写法 var new_arr = arr.filter((v)=> v>8); console.log(new_arr);修改项目代码:removeAllDone(){ // 原始循环判断用法 // for(let i=0;i<list_data.length;i++){ // if(list_data[i].stat == true){ // this.list_data.splice(i,1); // } // } // 上面是循环删除符合条件的数据 // 下面是保留不符合条件的数据 // 原始标准库对象方法 // this.list_data = this.list_data.filter(function(v){ // if(v.stat == false){ // return true; // } // }) // 箭头函数方法 // this.list_data = this.list_data.filter(function(v){ // return !v.stat; // }) // 精简方法 this.list_data = this.list_data.filter((v)=>!v.stat); },TodoList案例暂时告一段落,我们并没有将产品做完,因为我们需要用到其他知识了;第5章 MVVM设计思想MVC 设计思想:M: model 数据模型层 提供数据V: Views 视图层 渲染数据C: controller 控制层 调用数据渲染视图MVVM 设计思想:M: model 数据模型层 提供数据V: Views 视图层 渲染数据VM:ViewsModel 视图模型层 调用数据渲染视图 由数据来驱动视图(不需要过多考虑dom操作,把重心放在VM)第6章 其他知识点汇总6.1 计算属性与侦听器6.1.1 计算属性<div id="div"> <input type="text" v-model="xing"> <input type="text" v-model="ming"> {{xing + ming}} </div> <script> var app = new Vue({ el: '#div', data: { xing:'', ming:'', } }) </script>模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。因此我们可以使用方法,来进行运算并返回数据:<div id="div"> <input type="text" v-model="xing"> <input type="text" v-model="ming"> {{ fullname() }} <!-- 一百次调用,观察时间结果--> {{ fullname() }} </div> <script> var app = new Vue({ el: '#div', data: { xing:'', ming:'', }, methods:{ fullname(){ return this.xing+this.ming+Date.now(); } } }) </script>注意,每次在模板中使用 {{ fullname() }} fullname方法就会被调用执行一次;所以,对于任何复杂逻辑,你都应当使用计算属性 ,因为计算属性,会自动缓存数据:<div id="div"> <input type="text" v-model="xing"> <input type="text" v-model="ming"> <br> {{fulln}} <!-- 一百次调用 --> {{fulln}} </div> <script> var app = new Vue({ el: '#div', data: { xing:'', ming:'', }, computed:{ fulln(){ return this.xing+this.ming+Date.now(); } } }) </script>我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。只在相关依赖发生改变时它们才会重新求值;多次调用,计算属性会立即返回之前的计算结果,而不必再次执行函数。6.1.2 利用计算属性获取未完成任务个数<span class="todo-count"><strong>{{getNu}}</strong> item left</span>computed: { // 未完成任务个数 getNu() { return (this.list_data.filter((v) => !v.stat)).length; } }6.1.3 使用侦听器<div id="div"> <input type="text" v-model="xing"> <input type="text" v-model="ming"> {{ fullname }} </div> <script> var app = new Vue({ el: '#div', data: { xing: '', ming: '', fullname:'' }, // 设置侦听器 watch: { // 侦听器中的方法名和要真挺的数据属性名必须一致 // xing 发生变化,侦听器就会被执行,且将变化后的值和变化前的值传入 xing:function(newVal,old_val){ this.fullname = newVal+this.ming; }, ming:function(newVal,oldVal){ this.fullname = this.xing+newVal; } } }) </script>通过上面的案例,我们基本掌握了侦听器的使用,但是我们也发现,与计算属性相比,侦听器并没有优势;也不见得好用,直观上反而比计算属性的使用更繁琐;虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。<div id="div"> <input type="text" v-model="xing"> <input type="text" v-model="ming"> {{ fullname }} </div> <script src="./jq.js"></script> <script> var app = new Vue({ el: '#div', data: { xing: '', ming: '', fullname:'' }, // 设置侦听器 watch: { // 侦听器中的方法名和要真挺的数据属性名必须一致 // xing 发生变化,侦听器就会被执行,且将变化后的值和变化前的值传入 xing:function(newVal,old_val){ // this.fullname = newVal+this.ming; var t = this; // 在侦听器中执行异步网络请求 $.get('./xx.php',(d)=>{ t.fullname = d; }) }, } }) </script>6.2 使用ref操作DOM在学习 jq 时,我们首要任务就是学习选择的使用,因为选择可以极其方便帮助我们获取节点查找dom,因为我们要通过dom展示处理数据。而在Vue中,我们的编程理念发生了变化,变为了数据驱动dom;但有时我们因为某些情况不得不脱离数据操作dom,因此vue为我们提供了 ref 属性获取dom节点;<div id="app"> <input type="button" @click='click' value="按钮"> <br> <p ref="pv">123</p> </div> <script> var app = new Vue({ el: '#app', methods: { click: function () { // 使用原生JS获取dom数据 // var p = document.getElementsByTagName('p')[0].innerHTML; // console.log(p); // 使用vue ref 属性获取dom数据 var d = this.$refs.pv.innerHTML; console.log(d); } } }) console.log(app.$refs); </script>但是在项目开发中,尽可能不要这样做,因为从一定程度上,ref 违背的mvvm设计原则;6.3 过滤器的使用6.3.1 私有(局部)过滤器定义过滤器var app = new Vue({ el: '#app', data:{msg:'UP'}, //定义过滤器 filters:{ // 过滤器的名称及方法 myFilters:function(val){ return val.toLowerCase(); } } })过滤器的使用:Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化转义等操作。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器要被添加到操作值得后面,使用 管道符 | 分割;vue会自动将操作值,以实参的形式传入过滤器的方法中;{{msg|myFilters}}过滤敏感词汇<div id="app"> <input type="text" v-model="msg"> <br> {{msg|myFilters|get3}} </div> <script> var app = new Vue({ el: '#app', data:{ msg:'' }, //定义过滤器 filters:{ // 过滤器的名称及方法 myFilters:function(val){ return val.toLowerCase(); }, get3:function(val){ // 遇到数字替换为 0 // var reg = /\d/g; // return val.replace(reg,0); return val.replace('苍井空','***'); } } }) </script>6.3.2 全局过滤器上面的代码中,myFilters 及 get3 两个过滤器,仅在当前 vue 实例中可用;如果在代码 再次 var app2 = new Vue() 得到变量为 app2 的 vue 实例,则两个过滤器在 app2中都不可用;如果需要过滤器在所有实例对象中可用,我们需要声明 全局过滤器 Vue.filter(名称,处理器)<div id="app"> <input type="text" v-model="msg"> <br> {{msg|myFilters}} </div> <!-- 定义两个DOM节点 --> <div id="app2"> <input type="text" v-model="msg"> <br> {{msg|myFilters|get3}} </div> <script> Vue.filter('myFilters', function (val) { return val.toLowerCase(); }) // 定义两个全局过滤器 Vue.filter('get3', function (val) { return val.replace('苍井空','***'); }) // 两个Vue 实例 var app = new Vue({ el: '#app', data: { msg: '' } }) var app2 = new Vue({ el: '#app2', data: { msg: '' } }) </script>6.4 自定义指令前面我们学过 v-on 、v-model、v-show 等指令,在操作 dom 时使用了 ref 属性,其实之前学过的指令也是操作dom 的一种方式,但有时,这些指令并不能满足我们的需求,因此 vue 允许我们自定义指令来操作 dom6.4.1 全局自定义指令<div id="app"> <p v-setcolor>自定义指令的使用</p> </div> <script> // 注册一个全局自定义指令 `v-focus` Vue.directive('setcolor', { // 当被绑定的元素插入到 DOM 中时…… inserted: function (el) { // 聚焦元素 el.style.color = 'red'; } }) var app = new Vue({ el: '#app', }) </script>6.4.2 私有(局部)自定义指令<div id="app"> <p v-setcolor>自定义指令的使用</p> </div> <script> var app = new Vue({ el: '#app', // 注册 局部(私有)指令 directives: { // 定义指令名称 setcolor: { // 当被绑定的元素插入到 DOM 中时…… inserted: function (el) { // 聚焦元素 el.style.color = 'red'; } } } }) </script>6.4.3 利用自定义指令使TodoList获取焦点<input @keyup.enter="addTodo" v-getfocus class="new-todo" placeholder="请输入" >// 注册 局部(私有)指令 directives: { // 定义指令名称 getfocus: { // 当被绑定的元素插入到 DOM 中时…… inserted: function (el) { // 聚焦元素 el.focus() } } },6.4.4 为自定义指令传值之前学习的指令中,有的指令可以传值,有的则没有,而我们自定的指令中是没有值的,如果想为自定义指令赋值,如下即可:<div id="app"> <p v-setcolor='colors'>自定义指令的使用</p> </div> <script> var app = new Vue({ el: '#app', data:{ colors:'yellow' }, // 注册 局部(私有)指令 directives: { // 定义指令名称 setcolor: { // 自定义指令可以接受第二个参数 inserted: function (el,val) { // 第二个参数中包含了指令名称、挂载名称及数据键值 console.log(val); // 聚焦元素 el.style.color = val.value; } } } }) </script>6.5 过度及动画我们可以使用v-if或者v-show控制dom元素的显示和隐藏<div id="app"> <button @click="go">显示/隐藏</button> <p v-show="is">pppppp1111</p> </div> <script> var app = new Vue({ el: '#app', data: { isShow: true, }, methods: { go() { this.isShow = !this.isShow; } } }) </script>而在显示和隐藏的过程中,我们加入一些动画效果:在进入/离开的过渡中,会有 6 个 class 切换。v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。v-enter-to: 2.1.8版及以上 定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。v-leave: 定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。v-leave-to: 2.1.8版及以上 定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 <transition>,则 v- 是这些类名的默认前缀。如果你使用了 <transition name="my-transition">,那么 v-enter 会替换为 my-transition-enter。<style> .fade-enter-active, .fade-leave-active { transition: opacity 1s; } .fade-enter, .fade-leave-to { opacity: 0; } .man-enter-active, .man-leave-active { transition: opacity 4s; } .man-enter, .man-leave-to { opacity: 0; } </style> <div id="app"> <button @click="go">显示/隐藏</button> <transition name="fade"> <p v-show="isShow">pppppp1111</p> </transition> <transition name="man"> <p v-show="isShow">pppppp222</p> </transition> </div> <script> var app = new Vue({ el: '#app', data: { isShow: true, }, methods: { go() { this.isShow = !this.isShow; } } }) </script>这就是Vue中动画及过渡的基本使用方式,因为这些动画效果都需要我们自己写CSS样式,相对比较麻烦,在项目中,大多情况下,我们会借助第三方 CSS 动画库来实现,如:Animate.css ;后面项目中具体使用时,我们在进一步学习第三方 CSS 动画库的使用;第7章 json-server与axios一个项目从立项开始,一般都是前后端同时进行编码工作的,而此时前端需要的接口和数据后台都是无法提供的;7.1 json-server 使用使用全局安装 :npm install json-server -gjson-server 会将一个json文件作为数据库来存储数据,对json数据的格式是有要求的,如data.json的内容:{ "tb1": [ { "id": 1, "title": "标题1", "author": "描述信息1" }, { "id": 2, "title": "标题2", "author": "描述信息2" } ], "tb2": [ { "id": 1, "body": "some comment", "postId": 1 } ], "tb3": { "name": "typicode" } }启动服务: json-server --watch data.json启动成功后,提示信息如下:$ json-server --watch data.json \{^_^}/ hi! Loading data.json Done Resources http://localhost:3000/tb1 http://localhost:3000/tb2 http://localhost:3000/tb3 Home http://localhost:3000 Type s + enter at any time to create a snapshot of the database Watching...得到tb1所有的数据 GET: http://localhost:3000/tb1根据id得到数据 GET : http://localhost:3000/tb1/2添加一条数据 POST: http://localhost:3000/tb1删除一条数据 DELETE: http://localhost:3000/tb1/2模糊查找 GET : http://localhost:3000/tb1?title_like=标题根据id修改数据 PUT: http://localhost:3000/tb1/1注意:json-server 严格遵循 HTTP 请求语义进行数据处理7.2 axios我们在构建应用时需要访问一个 API 并展示其数据。做这件事的方法有好几种,而使用基于 Promise 的 HTTP 客户端 axios 则是其中非常流行的一种。<script src="./axios.js"></script> <script> // 获取全部数据 axios.get('http://localhost:3000/list_data') .then((data)=>{ console.log(data); }); // 获取一条数据 axios.get('http://localhost:3000/list_data/2') .then((data)=>{ console.log(data); }) // 添加一条数据 axios.post('http://localhost:3000/list_data',{stat:false,title:'喝水'}) .then((d)=>{ console.log(d); }).catch(error => console.log(error)) // 删除一条数据 axios.delete('http://localhost:3000/list_data/4') .then((d)=>{ console.log(d); }).catch(error => console.log(error)) // 修改一条数据 axios.put('http://localhost:3000/list_data/6',{title:'hhhhhh'}) .then((d)=>{ console.log(d); }).catch(error => console.log(error)) </script>第8章 重构TodoList案例8.1 启动API接口及数据db.json:{ "list_data": [ { "id": 1, "title": "吃饭", "stat": true }, { "id": 2, "title": "睡觉", "stat": false }, { "id": 3, "title": "打豆豆", "stat": true } ] }启动服务: json-server --watch db.json8.2 获取全部任务el: '#todoapp', data: { // list_data:list_data, list_data:[]// es6属性简写 }, // 当vue实例获取到 el:'#todoapp' 自动调用执行 mounted 方法 mounted:function(){ let url = 'http://localhost:3000/list_data'; axios.get(url).then((backdata)=>{ // console.log(backdata.data); this.list_data = backdata.data; }) },8.3 添加任务…… methods: { // 添加任务事件处理器 // addTodo:function(){} // 简写形式 addTodo(ev) { // 获取当前触发事件的元素 var inputs = ev.target; // 获取value值,去除空白后判断,如果为空,则不添加任务 if (inputs.value.trim() == '') { return; } // 组装任务数据 var todo_data = { // 通过服务器添加数据时,不需要id值 // id: this.list_data.length + 1 + 1, title: inputs.value, stat: false }; let url = 'http://localhost:3000/list_data'; // 将数据提交保存到服务器 axios.post(url,todo_data).then((back_data)=>{ let {data,status} = back_data; if(status == 201){ // console.log(this.list_data); // 数据保存成功后,将数据添加到任务列表展示 this.list_data.push(data); } }) // 清空文本框 inputs.value = ''; }, …… 8.4 删除任务<button @click="removeTodo(key,val.id)" class="destroy"></button>// 删除操作 removeTodo(key,id) { let url = 'http://localhost:3000/list_data/'+id; axios.delete(url).then((back_data)=>{ // 结构对象 let {data,status} = back_data; // console.log(back_data); if(status == 200){ this.list_data.splice(key, 1); } }) },8.5 完成任务<li v-for="(val,key) in list_data" @click="todoDone(key,val.id)" v-bind:class="{completed:val.stat}">// 完成任务 事件处理器(新添加,原案例中没有) todoDone(key,id){ let url = 'http://localhost:3000/list_data/'+id; // 组装数据准备修改服务器数据 setdata = {}; // 注意:事件优先于浏览器渲染执行,获取当前状态 var chestat = this.list_data[key].stat; // 状态取反 setdata.stat = !chestat; setdata.title = this.list_data[key].title; // console.log(setdata); axios.put(url,setdata).then((backdata)=>{ var {data,status} = backdata; // 如果服务器修改失败,则重新渲染DOM节点样式,改回原始状态 // 服务器返回状态有误 if(status != 200){ this.list_data[key].stat = chestat; } // 如果异步执行失败失败,则重新渲染DOM节点样式,改回原始状态 }).catch((err)=>{ if(err){ this.list_data[key].stat = chestat; } }) },8.6 案例中的Bug修改:<button @click.stop="removeTodo(key,val.id)" class="destroy"></button>第9章 组件https://cn.vuejs.org/v2/guide/components.htmlhttps://cn.vuejs.org/v2/guide/components-registration.html9.1 认识组件组件系统是 Vue 的一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。通常一个应用会以一棵嵌套的组件树的形式来组织:例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。9.2 基本使用组件是可复用的 Vue 实例,且带有一个名字。把这个组件作为自定义元素来使用。组件的好处是写一次可以进行任意次数的复用。<div id="app"> <!-- 使用组件 --> <!-- 将组件名直接当做标签名在html代码中使用即可 --> <mytemp></mytemp> <!-- 组件可以进行任意次数的复用 --> <mytemp></mytemp> </div> <script> // 定义一个名为 mytemp 的新组件 Vue.component('mytemp',{ // template属性的值,作为组件的内容 // vue 会把这个值替换到html中并会被浏览器渲染 template:"<h2>我是一个组件</h2>" }) var app = new Vue({ el: '#app', }) </script>上面代码中我们直接使用 Vue.component() 方法定义了组件,而这个 mytemp 组件可以用在所有 vue 实例中,这种组件被称为 全局组件在具体的某个vue实例中,也可以定义组件,但是组件仅会在具体的 vue 实例中起作用,这种组件被称为 局部(私有)组件<div id="app"> <!-- 使用组件 --> <!-- 将组件名直接当做标签名在html代码中使用即可 --> <mytemp></mytemp> </div> <div id="app2"> <!-- 不可用 --> <mytemp></mytemp> </div> <script> var app = new Vue({ el: '#app', // app 的私有组件,其他实例对象不可用 components: { mytemp: { template: "<h2>我是一个组件</h2>", } } }) var app2 = new Vue({ el: '#app2', }) </script>9.3 使用注意组件名如果是驼峰法命名,使用组件时要将大写字母改为小写,并且在前面加上 -组件中的tamplate属性必须有一个唯一的根元素,否则会报错<div id="app"> <!-- 使用组件 --> <!-- 将组件名直接当做标签名在html代码中使用即可 --> <my-temp></my-temp> <!-- 单标签方式使用 --> <my-temp/> </div> <div id="app2"> <!-- 不可用 --> <mytemp></mytemp> </div> <script> var app = new Vue({ el: '#app', // app 的私有组件,其他实例对象不可用 components: { // 驼峰法命名 myTemp: { // 必须有唯一的根标签,多标签报错 template: "<div><h2>我是一个组件</h2><h3>df</h3></div>", } } }) var app2 = new Vue({ el: '#app2', }) </script>9.4 组件的使用CSS 代码* { margin: 0; padding: 0; } .top { width: 100%; height: 80px; background-color: #ccc; } .left { margin-top: 20px; width: 800px; height: 600px; background-color: #ccc; float: left; } .right { margin-top: 20px; width: 400px; height: 600px; background-color: #ccc; float: right; }原始HTML代码<div id="app"> <div class="top">我是顶</div> <div class="left">我是左</div> <div class="right">我是右</div> </div>组件化代码<div id="app"> <tops></tops> <lefts></lefts> <rights></rights> </div> <script> var app = new Vue({ el: '#app', components:{ tops:{ template:'<div class="top">我是顶</div>' }, lefts:{ template:'<div class="left">我是左</div>', }, rights:{ template:'<div class="right">我是右</div>' } } }) </script>9.5 组件中的数据及方法组件是带有名字的可复用的 Vue 实例 ,所以它们与 new Vue 实例对象接收相同的参数选项 data、computed、watch、methods , 但 el例外;虽然组件和实例对象可以接收相同的参数选项,但在具体使用中,vue实例对象的 data 与组件中的 data 还是有差异的, 在我们自己写的组件中,data 必须是一个函数 一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回的对象;<div id="app"> <my-temp></my-temp> </div> <script> var app = new Vue({ el: '#app', components: { myTemp: { // 一个组件的 data 选项必须是一个函数 data:function(){ // 将 数据 装入 对象 返回 return {msg:'我是data选项'} }, // 其他选项的使用不受影响 methods:{ cli(){ alert(123); } }, template: "<div @click='cli'>{{msg}}</div>", } } }) </script>除 data 选项外,其他选项的使用都是一样的;9.6 vue实例也是组件通过new Vue() 可以得到一个实例对象,其实这个实例对象就是一个特殊的组件,也有 template 参数,也可以当做组件来使用;<div id="app"> {{msg}} </div> <script> var app = new Vue({ el: '#app', data:{msg:'数据'}, template:'<h2>组件</h2>' }) </script>上面的代码中直接为Vue 实例对象传入了 template 参数,那么 vue 会使用template中的数据替换 el 选中的整个 DOM 节点 , 因此 data 选项中的的数据也不会绑定,因为在绑定数据之前,整个 DOM 节点包括节点中 {{msg}} 都会被替换;如果想让数据正常绑定,我们可以在 template 数据中加入 {{msg}}<div id="app"> {{msg}} </div> <script> var app = new Vue({ el: '#app', data:{msg:'数据'}, template:'<h2>组件{{msg}}</h2>' }) </script>9.7 通过 Prop 向子组件传递数据 *https://cn.vuejs.org/v2/guide/components.html<div id="app"> <mytemp></mytemp> </div> <script> var app = new Vue({ el: '#app', data:{msg:'数据'}, components:{ mytemp:{ template:'<h2>data:{{msg}}</h2>' } } }) </script>运行上面的代码,我们发现,组件 mytemp 并不能获取实例中 data 的数据,这是因为组件与组件之间都拥有各自独立的作用域;vue 在组件中提供了props 选项,props 接受一个在组件中自定义属性的值;<div id="app"> <mytemp cc="我是cc"></mytemp> </div> <script> var app = new Vue({ el: '#app', data:{msg:'数据'}, components:{ mytemp:{ template:'<h2>data:{{cc}}</h2>', props:['cc'], } } }) </script>我们知道了props的用法后,怎么才能将vue实例对象中的数据传入组件中呢?我们可以借助 v-bind 指令来进行传值;<div id="app"> <mytemp v-bind:cc="msg" v-bind:kk="msg2"></mytemp> </div> <script> var app = new Vue({ el: '#app', data:{ msg:'数据', msg2:'数据二' }, components:{ mytemp:{ template:'<h2>data:{{cc}}<br>{{kk}}</h2>', props:['cc','kk'], } } }) </script>vue实例对象也是一个组件,而 mytemp 组件就是运行在 实例对象下面的,这时我们也会将 实例对象称为 父组件,将 mytemp 组件称为 子组件; 而我们上面的代码,实际上已经实现了 父组件向子组件传递数据的 功能;第10章 Vue的生命周期每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。比如 created 钩子可以用来在一个实例被创建之后执行代码:new Vue({ data: { a: 1 }, created: function () { // `this` 指向 vm 实例 console.log('a is: ' + this.a) } }) // => "a is: 1"也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 mounted、updated 和 destroyed。生命周期钩子的 this 上下文指向调用它的 Vue 实例。下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。<div id="app"> {{ msg }} <input type="text" ref="txt" v-model="msg"> </div> <script> var vm = new Vue({ el: "#app", data: { msg: 'hello vue', dataList: [] }, // 在vue对象初始化过程中执行 beforeCreate(){ console.log('beforeCreate'); console.log(this.msg);// undefined }, // 在vue对象初始化完成后执行 created() { console.log('created'); console.log(this.msg);//hello vue } // …… }); </script>第11章 单页应用11.1 单页应用什么是单页应用单页应用(single page web application,SPA),是在一个页面完成所有的业务功能,浏览器一开始会加载必需的HTML、CSS和JavaScript,之后所有的操作都在这张页面完成,这一切都由JavaScript来控制。单页应用优缺点优点操作体验流畅完全的前端组件化缺点首次加载大量资源(可以只加载所需部分)对搜索引擎不友好学习难度相对较高优缺点都很明显,但是我们都还没尝试过就来评价,就会显得空口无凭;接下来我们先来学习制作单页应用,然后再来进行点评;11.2 vue路由插件vue-routerhttps://cn.vuejs.org/v2/guide/routing.htmlhttps://router.vuejs.org/zh/<!-- 引入路由 --> <script src="./vue.js"></script> <script src="./vue-router.js"></script> <div id="app"> <ul> <li><a href="#/login">登录</a></li> <li><a href="#/register">注册</a></li> </ul> <!-- 路由中设置的组件会替换router-view标签 --> <router-view></router-view> </div> <script> // 1:定义路由组件 var login = { template: '<h2>我是登录页面</h2>' } var register = { template: '<h2>注册有好礼</h2>' } // 2:获取路由对象 var router = new VueRouter({ // 定义路由规则 routes: [ // {请求的路径,componet是模板} { path: "/register", component: register }, { path: "/login", component: login }, ] }) var app = new Vue({ el: '#app', // ES6 属性简写 // 3:将router对象传入Vue router }) </script>上例中,在HTML中我们直接使用了 a 标签,但是这样并不好,因为官方为我们提供了 router-link 标签<div id="app"> <ul> <li><router-link to="/login">登录</router-link></li> <li><router-link to="/register">注册</router-link></li> <!-- <li><a href="#/login">登录</a></li> <li><a href="#/register">注册</a></li> --> <!-- router-link 会被解析为a标签 --> <!-- 不同的是,router-link在解析为a标签后, 会自动为点击的 a 标签添加class属性 --> </ul> <!-- 路由中设置的组件会替换router-view标签 --> <router-view></router-view> </div>使用 router-link 的一大好处就是,每当我们点击时,在标签内就会自动帮我们添加 class 属性,而此时,我们就可以利用 class 属性,来定义样式:<style> .router-link-active { color: red; } </style>11.3 动态路由匹配假设有一个用户列表,想要删除某一个用户,需要获取用户的id传入组件内,如何实现呢?此时可以通过路由传参来实现,具体步骤如下:通过 传参,在路径上传入具体的值<router-link to="/users/120">用户管理</router-link>路由规则中增加参数,在path最后增加 :id{ name: 'users', path: '/users/:id', component: Users },在组件内部可以使用,this.$route 获取当前路由对象var Users = { template: '<div>这是用户管理内容 {{ $route.params.id }}</div>', mounted() { console.log(this.$route.params.id); } };第12章 构建一个项目12.0 命令行工具 (CLI)https://cn.vuejs.org/v2/guide/installation.html#%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7-CLIVue 提供了一个官方的 CLI,为单页面应用 (SPA) 快速搭建繁杂的脚手架。它为现代前端工作流提供了 batteries-included 的构建设置。只需要几分钟的时间就可以运行起来并带有热重载、保存时 lint 校验,以及生产环境可用的构建版本。更多详情可查阅 Vue CLI 的文档。12.1 初始化项目安装 cli 命令工具:npm install -g @vue/cli @vue/cli-init安装成功后,使用 vue -V 命令,查看版本号;使用 vue init webpack myapp 构建一个名为 myapp 的项目:Vue 依然使用询问的方式,让我们对项目有一个初始化的信息Project name:项目名Project description: 项目描述Author: 作者Vue build:第一种:配合大部分的开发人员第二种:仅仅中有runtimeInstall vue-router? 是否安装vue-routerUse ESLint to lint your code?是否使用ESLint来验证我们的语法。Pick an ESLint preser:使用哪种语法规范来检查我们的代码:Standard: 标准规范Airbnb: 爱彼迎规范Set up unit test: 设置单元测试Setup e2e tests: 设置端对端测试Should we run 'npm install':要不要帮忙你下载这个项目需要的第三方包使用npm来下载使用yarn来下载To get started: cd myapps npm run dev // 使用命令启动项目 ----- Your application is running here: http://localhost:8080 打开浏览器,访问 http://localhost:8080 看到浏览器的欢迎界面,表示项目运行成功12.2 项目结构介绍├── build webpack打包相关配置文件目录 ├── config webpack打包相关配置文件目录 ├── node_modules 第三方包 ├── src 项目源码(主战场) │ ├── assets 存储静态资源,例如 css、img、fonts │ ├── components 存储所有公共组件 │ ├── router 路由 │ ├── App.vue 单页面应用程序的根组件 │ └── main.js 程序入口,负责把根组件替换到根节点 ├── static 可以放一些静态资源 │ └── .gitkeep git提交的时候空文件夹不会提交,这个文件可以让空文件夹可以提交 ├── .babelrc 配置文件,es6转es5配置文件,给 babel 编译器用的 ├── .editorconfig 给编辑器看的 ├── .eslintignore 给eslint代码风格校验工具使用的,用来配置忽略代码风格校验的文件或是目录 ├── .eslintrc.js 给eslint代码风格校验工具使用的,用来配置代码风格校验规则 ├── .gitignore 给git使用的,用来配置忽略上传的文件 ├── index.html 单页面应用程序的单页 ├── package.json 项目说明,用来保存依赖项等信息 ├── package-lock.json 锁定第三方包的版本,以及保存包的下载地址 ├── .postcssrc.js 给postcss用的,postcss类似于 less、sass 预处理器 └── README.md 项目说明文档12.3 语法检查注意 :如果我们在 构建项目时 选择了 Use ESLint to lint your code 那么我们在写代码时必须严格遵守 JavaScript Standard Style 代码风格的语法规则:使用两个空格 – 进行缩进字符串使用单引号 – 需要转义的地方除外不再有冗余的变量 – 这是导致 大量 bug 的源头!无分号 – 这没什么不好。不骗你!行首不要以 (, [, or ` 开头这是省略分号时唯一会造成问题的地方 – 工具里已加了自动检测!详情关键字后加空格 if (condition) { ... }函数名后加空格 function name (arg) { ... }坚持使用全等 === 摒弃 == 一但在需要检查 null || undefined 时可以使用 obj == null。一定要处理 Node.js 中错误回调传递进来的 err 参数。使用浏览器全局变量时加上 window 前缀 – document 和 navigator 除外避免无意中使用到了这些命名看上去很普通的全局变量, open, length, event 还有 name。说了那么多,看看这个遵循了 Standard 规范的示例文件 中的代码吧。或者,这里还有一大波使用了此规范的项目 代码可供参考。注意: 如果你不适应这些语法规则,可以在构建项目时不使用 ESLint 的语法检查12.4 项目代码预览12.4.1 知识储备严格模式http://javascript.ruanyifeng.com/advanced/strict.html严格模式主要有以下限制。变量必须声明后再使用函数的参数不能有同名属性,否则报错不能使用with语句不能对只读属性赋值,否则报错不能使用前缀 0 表示八进制数,否则报错不能删除不可删除的属性,否则报错不能删除变量delete prop,会报错,只能删除属性delete global[prop]eval不会在它的外层作用域引入变量eval和arguments不能被重新赋值arguments不会自动反映函数参数的变化不能使用arguments.callee不能使用arguments.caller禁止this指向全局对象不能使用fn.caller和fn.arguments获取函数调用的堆栈增加了保留字(比如protected、static和interface)ES6模块化http://es6.ruanyifeng.com/#docs/module总结:CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用;CommonJS 模块是运行时加载,ES6 模块是编译时输出接口;ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";;ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块;12.4.2 代码加载执行main.js// 入口文件 // 以es6模块的方式引入 vue APP router 三个模块; import Vue from 'vue' import App from './App' import router from './router' Vue.config.productionTip = false; /* eslint-disable no-new */ new Vue({ // 获取节点对象 el: '#app', // 引入路由 router, // 本实例的私有组件 components: { App }, // el 与 template 在同一个实例中出现, // 根据生命周期的执行顺序可知,template中的内容会替换el选中的内容 template: '<App/>' }) roter/index.jsimport Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld' // Vue 中插件引入语法 // https://cn.vuejs.org/v2/guide/plugins.html Vue.use(Router) // ES6模块导出语法 export default new Router({ routes: [ // 定义一个路由规则 { path: '/', // 请求路径 name: 'HelloWorld', // 路由名称标识 component: HelloWorld //请求此路由时,使用的组件 } ] }) components/HelloWorld.vueexport default { // 模块名字 name: 'HelloWorld', // 组件中 data 数据必须是一个有返回值的方法 data () { return { msg: 'Welcome to Your Vue.js App' } } }(main.js->template: '<App/>')替换 (index.html->div#app); (index.html-><App/>) --> (components: { App }) ( components: { App }) --> (import App from './App' -> src/App.vue) (App.vue -> <router-view/> -> 路由组件) --> (main.js-> router) ========此项决定了页面展示那个组件内容 ======== ({path: '/',name: 'HelloWorld', component: HelloWorld }) --> (import HelloWorld from '@/components/HelloWorld') (src/components/HelloWorld.vue) --> <router-view/> 12.5 添加自己的路由组件修改 router/index.js ,添加自己的路由import Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld' // 引入(导入) 组件 import MyRouter from '@/components/MyRouter' Vue.use(Router) // ES6模块导出语法 export default new Router({ routes: [ {path: '/',name: 'HelloWorld', component: HelloWorld }, // 添加自己的路由及组件 { path:'/myrouter', name:'MyRouter', component:MyRouter } ] })在 components 文件夹中添加 MyRouter.vue 文件,写自己的组件代码:<template> <div class="mypage"> {{mydatas}} </div> </template> <script> // 模块化导出 export default { data(){ return {mydatas:'lksadjflks'} } } </script> <style> .mypage{ width: 200px; height: 50px; background: pink } </style> 浏览器渲染效果如下:
2022年10月13日
14 阅读
0 评论
1 点赞