在shopify站点上，每个section里，代码复用实例，该怎么优化？
在 Shopify 里，一个 .liquid Section 就是一块「彼此独立、可重复、可排序」的积木。 核心限制是：每个 Section 的代码最终会被 碎片式地 拼到同一个页面上；你 不能保证 两个 Section 谁在前谁在后，也 不能保证 只出现一次；Shopify 没有「公共 JS 打包器」——不能 在 Section 里直接写 import 然后让主题统一去 type="module" 合并；浏览器仍然 重复下载 同一个 CDN 地址，除非你用 nomodule/defer 做全局去重。因此，「单模块复用」那一套在 Section 级别 是行不通的；必须把 Vue 3 当全局库来用，并且保证：只加载一次；每个 Section 只负责「自己挂载自己」；即使两个相同 Section 被拖到同一页面，也能各自独立运行。下面给出 Shopify 官方推荐 的「Section-内自闭环 + 全局单例脚本」方案。一、theme.liquid（或 head 标签里）统一引入 Vue 3{%- comment -%} 仅当页面至少有一个需要 Vue 的 Section 时才加载 {%- endcomment -%} {%- if request.page_type contains 'index' or section_blocks_contains_vendor 'vue' -%} {%- endif -%}说明vue.global.prod.js 是 IIFE 版本，会把 Vue 暴露成全局 Vue;defer 保证执行顺序在 DOM 解析之后，且 只下载一次；用 section_blocks_contains_vendor 是 Shopify 2.0 的过滤器，可自己换成任意条件。二、写一个通用 Section：sections/vue-counter.liquid {{ section.settings.heading }} Counter 1: {{ count }} Counter 2: {{ count2 }} + Counter 1 + Counter 2 {% schema %} { "name": "Vue Counter", "settings": [ { "type": "text", "id": "heading", "label": "Heading", "default": "Counter Block" } ], "presets": [{ "name": "Vue Counter" }] } {% endschema %}三、同一个页面拖 N 个「Vue Counter」Section每个 Section 输出 独立 DOM + 独立脚本；由于 section.id 不同，变量名、挂载点互不影响；Vue 全局库只加载 一次；即使后续用「主题编辑器」再拖一个进来，也能立刻工作（document.currentScript 永远指向当前脚本块）。四、如果「多个不同 Section」都要用 Vue统一在 theme.liquid 里加一次 然后在每个 Section 里只写： 五、小结目标Shopify Section 内的正确姿势只加载一次 Vue在 theme.liquid 用 不重复下载浏览器缓存 + 全局变量每个 Section 独立用 section.id 做命名空间，自闭环脚本可重复拖放document.currentScript 保证永远拿到当前块不依赖打包器直接用 IIFE 版本，不写 type="module"按上面模板，无论多少个 Section、多少个实例，都能做到「零重复下载、零全局污染、零冲突」。
引入相关资源 sun-tab-video-css style liquid <el-tabs id="sun-video-tab-content" v-model="activeName" @tab-click="handleClick" type="card"> </el-tabs> html section {% if customer.id or true %} {% if section.settings.render_first %} {% render 'sun-tab-video-css' %} {% endif %} {% if section.settings.tab_title == blank %} {% endif %} {% 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 %} {% for block in section.blocks %} {{ block.settings.title }} {% endfor %} {% if section.settings.tab_title %} </el-tab-pane> {% endif %} {% 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
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这段代码中类型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 = ReadonlyPartial可以把每一个属性变成可选的。type A = { username: string age: number } /* type B = { username?: string|undefined; age?: number|undefined; } */ type B = PartialPick可以把某些指定的属性给筛选出来。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 = RequiredOmit是跟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 …等
为什么使用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等
章节介绍本章将学习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该如何展示对应的组件呢？可以通过这个组件来实现。除了进行展示外，还可以通过方式进行声明式的路由跳转，代码如下： <router-link to="/">首页</router-link> | <router-link to="/about">关于</router-link> <router-view></router-view> 嵌套路由模式往往我们的路由是比较复杂的，需要的层级也比较多，那么就会产生嵌套路由的模式，比如：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中进行跳转路由的方式叫做编程式路由，这样方式会更加的灵活。 编程式路由跳转 可以看到在动态路由中使用到了一个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 } ]; <router-link :to="{ name: 'bar' }">bar</router-link> <router-link :to="{ name: 'foo', params: {id: 123} }">foo 123</router-link> 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属性。 路由传递参数的多种方式及应用场景路由传参我们经常要在路由跳转的时候传递一些参数，这些参数可以帮助我们完成后续的一些开发和一些处理。路由的传递参数主要有以下三种方式：query方式（显示） -> $route.queryparams方式（显示） -> $route.paramsparams方式（隐式） -> $route.params两种显示传递数据的差异点主要为，query是携带辅助信息，而params是界面的差异化。 <router-link :to="{ name: 'bar', query: { username: 'xiaobai' } }">bar</router-link> <router-link :to="{ name: 'foo', params: { username: 'xiaoqiang' } }">foo</router-link> 前两种都是显示传递数据，那么第三种是隐式传递数据，这种方式并不会把数据暴露出来。 <router-link :to="{ name: 'bar', params: { username: 'xiaoqiang' } }">bar</router-link> 但是这里需要注意以下，隐式发送过来的数据，只是临时性获取的，一旦刷新页面，隐藏的数据就会消失，所以在使用的时候要额外注意以一下。详解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文件中进行路由守卫的设置，代码如下： 完整的导航解析流程导航被触发。在失活的组件里调用 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修改我们的共享数据，代码如下： 点击 hello home, {{ $store.state.count }} 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; } } }); 这样在vue devtools插件中就可以更好的观察到异步数据的变化。那么异步处理的应用场景有哪些呢？异步的获取后端的数据，这样可以利用状态管理来充当MVC架构中的C层，不仅衔接前后端，还能对数据进行共享，可以在切换路由的时候做到减少请求次数，而且状态管理配合本地存储进行数据持久化也是非常的方便。Vuex计算属性和辅助函数的使用Vuex中除了提供常见的异步处理和同步处理外，还提供了一些辅助的操作方式，比如：状态管理下的计算属性和简化操作的辅助函数。getters计算属性在Vuex中通过定义getters字段来实现计算属性，代码如下：const store = createStore({ state: { count: 0 } getters: { doubleCount(state){ return state.count * 2; } } }); {{ count }}, {{ doubleCount }} 当count数据发生改变的手，对应的计算属性doubleCount也会跟着发生改变。辅助函数在Vuex中为了让我们使用共享数据或调用方法的时候，更加简单易用，提供了对应的辅助函数，分别为：mapState、mapGetters、mapMutations、mapActions。 点击 hello home, {{ count }}, {{ doubleCount }} 辅助函数最大的优点就是可以处理大量共享数据的需求，这样写起来会非常的简便，因为只需要往数组里添加子项即可。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 } }); 点击 hello about, {{ msg }}, {{ upperMsg }} 在辅助函数的情况下，也可以进行很好的调用，辅助函数的第一个参数就是命名空间的名字。组合式API中使用Router和Vuex注意事项前面介绍的路由和状态管理都是在选项式API中进行使用的，那么路由和状态管理在组合式API中使用的时候，需要注意哪些问题呢？主要就是路由会提供两个use函数，分别为：useRoute和useRouter；同理状态管理页提供了一个use函数，useStore来操作的。先来看一下路由相关use函数的使用情况： 基本上跟选项式API中的用法是一样的，并没有太大的区别。再来看一下状态管理相关use函数的使用情况： 得到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;最后看一下三个页面的基本逻辑处理： {{ item.task }} {{ item.task }} {{ item.task }} 搭建 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使用起来要简单一些。下面看一下共享状态是如何进行渲染和方法调用的。 点击 {{ count }}, {{ doubleCount }} Pinia对于模块化的操作方式也比Vuex要简单一些，直接在/stores创建下新创建一个模块JS即可，如：message.js。message.js的代码跟counter.js的代码是一样的格式，使用的时候也是一样的操作行为。总结内容了解什么是前端路由，以及Vue3中如何使用vue-router路由掌握路由的基本操作，如：编程式路由、动态路由、路由守卫等了解什么是共享状态，以及Vue3中如何使用vuex共享状态掌握vuex的基本操作，如：同步异步方法、模块化、持久化等综合应用以及Vuex下一代版本，Pinia存储库
章节介绍本章将学习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来说会更加的简易。 setup方法 {{ count }} // Vue3.1 // Vue3.2 setup方法是需要把数据进行return后，才可以在template标签中进行使用，而setup属性方式定义好后就可以直接在template标签中进行使用。ref响应式如何在组合式API中来完成数据的响应式操作，通过的就是ref()方法，需要从vue模块中引入这个方法后才可以使用。 事件方法_计算属性 reactive_toRefs事件方法与计算属性下面看一下在组合式API中是如何实现事件方法和计算属性的。 点击 {{ count }}, {{ doubleCount }} 事件方法直接就定义成一个函数，计算属性需要引入computed方法，使用起来是非常简单的。reactive与toRefsreactive()方法是组合式API中另一种定义响应式数据的实现方式，它是对象的响应式副本。 reactive {{ state.count }} reactive()方法返回的本身就是一个state对象，那么在修改的时候就不需要.value操作了，直接可以通过state.count的方式进行数据的改变，从而影响到视图的变化。ref()和reactive()这两种方式都是可以使用的，一般ref()方法针对基本类型的响应式处理，而reactive()针对对象类型的响应式处理，当然还可以通过toRefs()方法把reactive的代码转换成ref形式。 reactive {{ state.count }}, {{ count }} 生命周期_watch_watchEffect生命周期钩子函数在学习选项式API的时候，我们学习了生命周期钩子函数，那么在组合式API中生命周期又是如何使用的呢？下面我们从图中先看一下对比的情况吧。 生命周期对比 那么具体的区别如下：组合式中是没有beforeCreate和created这两个生命周期，因为本身在组合式中默认就在created当中，直接定义完响应式数据后就可以直接拿到响应式数据，所以不需要再有beforeCreate和created这两个钩子组合式的钩子前面会有一个on，类似于事件的特性，那就是可以多次重复调用 watch与watchEffect这里先说一下watchEffect的用法，为了根据响应式状态自动应用和重新应用副作用，我们可以使用 watchEffect 函数。它立即执行传入的一个函数，同时响应式追踪其依赖，并在其依赖变更时重新运行该函数。watchEffect常见特性：一开始会初始触发一次，然后所依赖的数据发生改变的时候，才会再次触发触发的时机是数据响应后，DOM更新前，通过flush: 'post' 修改成DOM更新后进行触发返回结果是一个stop方法，可以停止watchEffect的监听提供一个形参，形参主要就是用于清除上一次的行为 watchEffect {{ count }} 再来看一下watch侦听器的使用方式，如下： 那么watch与watchEffect的区别是什么呢？懒执行副作用更具体地说明什么状态应该触发侦听器重新运行访问侦听状态变化前后的值跨组件通信方案provide_inject依赖注入在Vue中把跨组件通信方案provide_inject也叫做依赖注入的方式，前面我们在选项式中也学习了它的基本概念，下面看一下在组合式API中改如何使用。// provide.vue <my-inject></my-inject> //inject.vue {{ count }} 依赖注入使用的时候，需要注意的点：不要在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; use函数 {{ count }}, {{ doubleCount }} 通过useCounter函数的调用，就可以得到内部return出来的对象，这样就可以在.vue文件中进行功能的使用，从而实现功能的复用逻辑。利用defineProps与defineEmits进行组件通信在组合式API中，是通过defineProps与defineEmits来完成组件之间的通信。definePropsdefineProps是用来完成父子通信的，基本使用跟选项式中的props非常的像，代码如下：// parent.vue 父子通信 <my-child :count="0" message="hello world"></my-child> // child.vue hi child, {{ count }}, {{ message }} defineEmitsdefineEmits是用来完成子父通信的，基本使用跟选项式中的emits非常的像，代码如下：// parent.vue 子父通信 <my-child @custom-click="handleClick"></my-child> // child.vue 利用组合式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开发搜索提示列表本小节完成搜索页面的提示列表功能。 ... ... 搜索“ {{ searchWord }} ” {{ item.name }} 利用组合式API开发搜索结果列表本小节完成搜索页面的结果列表功能。 ... {{ item.name }} ... 利用组合式API开发搜索历史列表本小节完成搜索页面的历史列表功能。 历史记录 {{ item }} ... ... 总结内容了解了什么是组合式API，具备怎么的特性学习常见的组合式API语法，如：ref、reactive、watchEffect等组合式API之间的通信方案，如：父子组件、跨组件等通过实战案例，复杂的搜索应用，体验组合式的强大
-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，代码如下： ref属性 点击 aaaaaaaaa bbbbbbbbb 除了可以把ref属性添加给DOM元素外，还可以把ref属性添加给组件，这样可以获取到组件的实例对象，可以间接的实现组件之间的通信，代码如下： ref属性 <my-head ref="elem3"></my-head> 2_头部组件.vue文件： hello myhead 利用nextTick监听DOM更新后的情况nextTick方法，它的主要作用是将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。默认情况下，数据的更新会产生一个很小的异步延迟，所以直接再数据改变后取获取DOM是得不到DOM更新后的结果，而得到的是DOM更新前的结果。 hello nextTick {{ message }} 如何才能得到DOM更新后的结果呢，可以有两种方案，第一种就是利用生命周期updated这个钩子函数，第二种就是利用我们讲的nextTick方法，支持两种风格即回调和promise。 hello nextTick {{ message }} 自定义指令与自定义全局属性及应用场景除了核心功能默认内置的指令 (例如 v-model 和 v-show)，Vue 也允许注册自定义指令，来实现一些封装功能。自定义指令的实现首先来实现一个简单的v-color指令，用于给元素添加背景色，代码如下： 自定义指令 aaaaaaa 这里的回调函数是指令中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 编辑 自定义全局属性添加一个可以在应用的任何组件实例中访问的全局 property，这样在引入一些第三方模块的时候，就不用每一次进行import操作，而是直接通过this对象去访问全局属性即可，下面举一个例子，实现一个http的全局属性。// main.js app.config.globalProperties.$http = http; //demo.vue 复用组件功能之Mixin混入Mixin混入了解一下mixin混入，它是选项式API的一种复用代码的形式，可以非常灵活的复用功能。// mymixin.js const myMixin = { data(){ return { message: '复用的数据' } }, computed: { message2(){ return '复用的数据2' } } }; export { myMixin } // mymixin.vue mixin混入 {{ message }} {{ message2 }} 这样就可以直接在.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: ` hello myhead ` }) } } // 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中提供了非常多的常见组件，例如：按钮、评分、表单控件等等。 element plus <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> 除了常见的组件外，element plus中也提供了一些逻辑组件，这些逻辑组件是可以直接在JavaScript中进行使用，例如：ElMessage，ElNotification等方法。transition动画与过渡的实现在Vue中推荐使用CSS3来完成动画效果。当在插入、更新或从 DOM 中移除项时，Vue 提供了多种应用转换效果的方法。transition动画Vue中通过两个内置的组件来实现动画与过渡效果，分别是：<transition>和<transition-group>，代码如下： hello transition 点击 <transition name="slide" mode="out-in"> </transition> 其中<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属性的值，来决定使用哪个组件进行渲染。 动态组件 组件1 组件2 组件3 <component :is="nowCom"></component> keep-alive组件当我们点击的时候，就会进行组件的切换。在每次切换的过程中都会重新执行组件的渲染，这样组件操作的行为就会还原，而我们如何能够保证组件不变呢？可以利用<keep-alive>对组件进行缓存，这样不管如何切换，都会保持为初始的组件渲染，这样可以很好的保留之前组件的行为。组件的切换也可以配合<transition>完成动画的切换。 动态组件 组件1 组件2 组件3 <transition name="slide" mode="out-in"> <keep-alive> <component :is="nowCom"></component> </keep-alive> </transition> 异步组件与Suspense一起使用异步组件在大型应用中，我们可能需要将应用分割成小一些的代码块，并且只在需要的时候才从服务器加载一个模块。在上一个小节的动态组件的基础上，进行异步组件的演示。首先可以打开chrome浏览器的network网络，可以观察到在动态组件切换的时候，network网络中没有进行任何请求的加载，这证明了在初始的时候，相关的动态组件就已经加载好了。所以对于大型项目来说，如果能实现按需载入的话，那么势必会对性能有所提升，在Vue中主要就是利用defineAsyncComponent来实现异步组件的。 Suspense组件由于异步组件是点击切换的时候才去加载的，所以可能会造成等待的时间，那么这个时候可以配合一个loading效果，在Vue中提供了一个叫做<Suspense>的组件用来完成loading的处理。 <suspense> <component :is="nowCom"></component> loading... </suspense> 跨组件间通信方案 Provide_Inject跨组件通信方案正常情况下，我们的组件通信是需要一级一级的进行传递，通过父子通信的形式，那么如果有多层嵌套的情况下，从最外层把数据传递给最内层的组件就非常的不方便，需要一级一级的传递下来，那么如何才能方便的做到跨组件通信呢？可以采用Provide 和 inject 依赖注入的方式来完成需求，代码如下： 依赖注入 // provide.vue // inject.vue hello inject, {{ message }}, {{ count }} Provide与Inject注意点保证数据是单向流动的，从一个方向进行数据的修改如果要传递响应式数据，需要把provide改造成工厂模式发送数据Teleport实现传送门功能Teleport组件Teleport可以实现传送门功能，也就是说逻辑属于当前组件中，而结构需要在组件外进行渲染，例如：按钮模态框组件。// 模态框.vue 点击 <teleport to="body"> 模态框 </teleport> // 调用模态框.vue 传送门 <my-modal></my-modal> 逻辑组件但是往往我们需要的并不是普通组件的调用方式，而是逻辑组件的调用方式，那么如何实现逻辑组件呢？代码如下：// 定义逻辑组件，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;// 调用逻辑组件 传送门 点击 13-虚拟DOM与render函数及Diff算法虚拟DOMVue框架帮我们完成了大量的DOM操作，那么在底层Vue并没有直接操作真实的DOM，因为真实的DOM直接去操作是非常耗性能的，所以最好在JS环境下进行操作，然后再一次性进行真实DOM的操作。const vnode = { type: 'div', props: { id: 'hello' }, children: [ /* 更多 vnode */ ] }那么在Vue中是如何把 模板中的字符串编译成虚拟DOM的呢？需要用到内置的render函数，这个函数可以把字符串转换成虚拟DOM。 虚拟DOM Diff算法当更新的时候，一个依赖发生变化后，副作用会重新运行，这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树，将它与旧树进行比较，然后将必要的更新应用到真实 DOM 上去。而两个虚拟DOM进行对比的时候，需要加入一些算法提高对比的速度，这个就是Diff算法。 Diff算法 在脚手架下我们推荐使用来完成结构的编写，那么也可以直接通过render函数进行虚拟DOM的创建，代码如下： 总结内容ref属性、nextTick方法自定义指令、Mixin混入插件的概念、Element Plus框架的使用动画与过渡、动态组件、异步组件跨组件通信、传送门、虚拟DOM与render函数
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组件 组件可以拥有自己独立的结构，样式，逻辑。这样对于后期的维护是非常方便的。下面给出评分组件与按钮组件的抽离过程。 组件的划分 组件的命名方式与规范定义组件可通过驼峰、短线两种风格定义调用组件推荐短线方式 <my-head></my-head> 根组件app容器可以看成根组件，所以根组件跟普通组件都具备相同的配置信息，例如：data、computed、methods等等选项。 <my-head></my-head> 根组件与MyHead组件形成了父子组件。局部组件与全局组件局部组件只能在指定的组件内进行使用，而全局组件可以在容器app下的任意位置进行使用。组件之间是如何进行互相通信的上一个小节中，我们了解了组件是可以组合的，那么就形成了父子组件，父子组件之间是可以进行通信的， 那么为什么要通信呢？主要是为了让组件满足不同的需求。 组件之间差异化 父子通信最常见的通信方式就是父传子，或者子传父，那么父传子通过props实现，而子传父则通过emits自定义事件实现。 父子通信 <my-head :count="count" @custom-click="handleClick"></my-head> 父子通信需要注意的点组件通信的props是可以定义类型的，在运行期间会进行检测组件之间的数据是单向流动的，子组件不能直接修改传递过来的值但是有时候也需要数据的双向流动，可利用v-model来实现组件的属性与事件是如何进行处理的有时候组件上的属性或事件并不想进行组件通信，那么Vue是如何处理的呢？组件的属性与事件默认不通过props接收的话，属性会直接挂载到组件容器上，事件也是如此，会直接挂载到组件容器上。可通过 inheritAttrs 选项阻止这种行为，通过指定这个属性为false，可以避免组件属性和事件向容器传递。可通过 $attrs 内置语法，给指定元素传递属性和事件，代码如下： <my-head title="hello world" class="box" @click="handleClick"></my-head> $attrs也可以实现组件之间的间接通信。组件的内容是如何组合与分发处理的在前面的小节中，我们学习了组件之间的通信，让组件之间实现了不同的需求，我们通过给组件添加不同的属性来实现。那么在Vue中如何去传递不同的组件结构呢？这就涉及到了组件内容的分发处理。插槽slot在Vue中是通过插槽slot方式来进行分发处理的，Vue 实现了一套内容分发的 API，这套 API 的设计灵感源自 Web Components 规范草案，将 元素作为承载分发内容的出口。 <my-head> logo </my-head> 组件内的结构，即 logo 会被分发到<slot></slot>所在的区域。内容分发与插槽的注意点渲染作用域 -> 插槽只能获取当前组件的作用域具名插槽 -> 处理多个插槽的需求，通过v-slot指令实现，简写为#作用域插槽 -> 希望插槽能访问子组件的数据完整代码如下： <my-head> logo, {{ message }}, {{ count }} {{ item }} </my-head> 单文件组件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文件等。
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 的系统： Counter: {{ counter }} 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 用于修改某些操作的默认行为，等同于在语言层面做出修改，其实就是直接监控值的修改，当值发生改变的时候，可以监控到。 指令系统与事件方法及传参处理指令系统就是通过自定义属性实现的一套功能，也是声明式编程的体现。通常在标签上添加 v-* 字样，常见的指令有：v-bind -> 操作标签属性，可通过 : 简写v-on -> 操作事件，可通过 @ 简写 这是一个段落 点击 {{ message }} 如何添加事件方法，通过methods选项API实现，并且Vue框架已经帮我们帮事件传参机制处理好了。 点击 计算属性与侦听器区别与原理计算属性模板内的表达式非常便利，但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护，所以过于复杂的逻辑可以移植到计算属性中进行处理。 {{ reverseMessage }} 计算属性与方法比较像，如下所示： {{ reverseMessageMethod() }} {{ reverseMessageMethod() }} {{ reverseMessage }} {{ reverseMessage }} 计算属性跟方法相比，具备缓存的能力，而方法不具备缓存，所以上面代码执行完，会弹出两次1和一次2。注意：默认是只读的，一般不会直接更改计算属性，如果想更改也是可以做到的，通过Setter写法实现，官方地址。既然计算属性编写的是一个函数，而调用的时候以函数名的形式进行使用，其实实现起来也不是特别难的事情：let computed = { num(){ return 123; } } let vm = {} for(let attr in computed){ Object.defineProperty(vm, attr, { value: computed[attr]() }) }侦听器虽然计算属性在大多数情况下更合适，但有时也需要一个自定义的侦听器。侦听器的目的：侦听器用来观察和响应Vue实例上的数据变动,类似于临听机制+事件机制。当有一些数据需要随着其它数据变化而变化时,就可以使用侦听器。 {{ message }} 有时候，计算属性 和 侦听器 往往能实现同样的需求，那么他们有何区别呢？计算属性适合：多个值去影响一个值的应用；而侦听器适合：一个值去影响多个值的应用侦听器支持异步的程序，而计算属性不支持异步的程序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指令来操作表单的，可以非常灵活的实现响应式数据的处理。 {{ message }} 尽管有些神奇，但 v-model 本质上不过是语法糖。可通过value属性 + input事件来实现同样的效果。 {{ message }} v-model除了可以处理输入框以外，也可以用在单选框、复选框、以及下拉菜单中。 苹果 西瓜 哈密瓜 {{ fruits } 女 男 {{ gender }} 北京 上海 杭州 {{ city }} 生命周期钩子函数及原理分析每个组件在被创建时都要经过一系列的初始化过程——例如，需要设置数据监听、编译模板、将实例挂载到 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 } ]参考代码 全部 男 女 {{ item.name }}, {{ item.gender }}, {{ item.age }} 总结内容了解核心思想，例如：MVVM设计模式、选项式API优势了解Vue3各个选项的用法，例如：data、methods、computed等掌握常见的指令：v-bind、v-on、v-if、v-for等掌握样式操作、表单操作等行为了解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标签起到的作用 -> 形成一个整体容器并且不会被宣染 <!-- <input- type="text" - v-model="message">-->