仿知乎项目总结
2021-04-20 本文已影响0人
Amok校长
第1章 介绍
项目演示站点: http://zhihu.vikingship.xyz/
在线后端API查询和使用站点: http://api.vikingship.xyz/
项目在线文档: http://docs.vikingship.xyz/
完成的组件库展示: http://showcase.vikingship.xyz/
第2章 你好 Typescript: 进入类型的世界
2-1 什么是 Typescript
编程语言类型:
动态类型语言: 运行期间才会做类型检查的语言, 不用指定数据类型.
静态类型语言: 数据类型检查发生在编译阶段.
2-2 为什么要学习 typescript
1.程序更容易理解
问题: 函数或者方法输入输出的参数类型, 外部条件等
动态语言的约束: 需要手动调试等过程;
有了Typescript: 代码本身就可以回答上述问题
2.效率更高
在不同的代码块和定义中进行跳转
代码自动补全
丰富的接口提示
3.更少的错误
编译期间能够发现大部分错误
杜绝一些比较常见错误
4.非常好的包容性
完全兼容JavaScript
第三方库可以单独编写类型文件
大多数项目都支持Typescript
一点小缺点:
增加了一些学习成本
短期内增加了一些开发成本
2-3 安装 typescript
# 终端输入一下命令安装:
node -v
npm -v
sudo npm install -g typescript # 安装TS
tsc -v # 查看TS版本号
# 用VSCode创建一个test.ts文件
# ts文件编译成js文件
cd (test.ts所在文件夹)/
tsc test.ts # 编译ts文件, 将ts文件转换成js文件
ls
cat test.js # 查看js文件内容
2-4 原始数据类型和 Any 类型
let isDone: boolean = false
let age: number = 10
let firstName: string = 'viking'
let message: string = `Hello, ${firstName}` //模板字符串
let u: undefined = undefined
let n: null = null
// undefined 和 null 是所有类型的子类型
let num: number = undefined
// any 允许赋值给任意类型
let notSure: any = 4
notSure = 'maybe a string'
notSure = true
notSure.myName
notSure.getName()
2-5 数组和元组
// 数组将同一类型的数据, 聚集到一起
let arrOfNumbers: number[] = [1,2,3]
arrOfNumbers.push(3) //添加元素
//arrOfNumbers.push('123')//报错: 类型不对
function test() {
console.log(arguments)//arguments 属于类数组
let htmlCollection: HTMLCollection // 这个也属于类数组
}
// 元祖 : 限定数据类型的数组
let user: [string, number] = ['viking', 10]
//user.push(true)// 报错: 此时只能push string或number类型
2-6 Interface- 接口 初探
使用interface可以非常方便的定义对象的类.
/*
对对象的形状(shape)进行描述
Duck Typing(鸭子类型)
*/
/// 定义接口
interface Person {
name: string;
age?: number;// ?表示可选属性, 定义变量时可以不用这个属性.
readonly id: number;// readonly只读属性, 不能被赋值.
}
/// 定义一个变量
let viking: Person = {
name: 'viking',
age: 20,
// 必须与定义接口的属性的数量一致,否则报错
id: 1
}
// readonly 与 const的区别: readonly用在属性上; const用在变量上.
2-7 函数
/*
在JS中, 函数是一等公民
*/
// 多参数 + 返回值
function add(x: number, y: number): number {
return x + y
}
let result = add(1, 2)
// 可选参数
function add2(x: number, y: number, z?: number): number {
if (typeof z === 'number') {// 判断是否是number类型
return x + y + z
} else {
return x + y
}
}
// 函数表达式: 函数类型
const add3 = (x: number, y: number, z?: number): number => {
if (typeof z === 'number') {
return x + y + z
} else {
return x + y
}
}
// 赋值函数类型
let add4 : (x: number, y: number, z?: number) => number = add3
// interface 声明函数类型
interface ISum {
(x: number, y: number, z?: number): number
}
let add5: ISum = add3
2-8 类型推论 联合类型和 类型断言
// 类型推导
let str = 'str'
// 联合类型(union types)
let numberOrString: number | string
numberOrString = 'abc'
numberOrString = 123
numberOrString.length//报错: 不能访问私有类型的属性
numberOrString.toString()// 可以访问联合类型共有属性和方法
// 类型断言: 用来告诉编译器 你比它更了解这个类型, 并且它不应该再发生错误. 关键字as
// 注: 类型断言不是类型转换, as为一个不存在的类型是会报错的
function getLength(input: string | number): number{
const str = input as string
if(str.length){
return str.length
}else{
const number = input as number
return number.toString().length
}
}
//可以使用 type guard 完成上面👆功能, 遇到联合类型,使用条件语句缩小范围
function getLength2(input: string | number): number{
if (typeof input === 'string') {
return input.length
}else{
return input.toString().length
}
}
2-9 class - 类 初次见面
类(Class): 定义了一切事物的抽象特点
对象(Object): 类的实例
面向对象(OPP)三大特性: 封装、继承、多态
封装: 将数据操作细节隐藏起来, 只暴露对外的接口, 外界调用端不需要也不可能知道细节,只能通过对外的接口来访问该对象.
继承: 子类可以继承父类, 子类除了拥有父类所有特征外, 还有一些更具体的特性.
多态: 由继承产生了多个不同的类, 对同一个方法可以有不同的响应.比如猫和狗都继承自动物, 但他们都分别实现了自己吃的方法, 此时针对某一个实例, 我们无需了解它是猫还是狗, 我们可以直接调用eat方法, 程序会自动判断出来该如何执行这个方法.
/// 在test.js文件下:
// 类的创建
class Animal {
constructor(name) {//构造函数
this.name = name
}
run() {//实例方法
return `${this.name} is running`
}
}
const snake = new Animal('lily')
console.log(snake.run())
// 终端执行: node test.js # 运行test.js文件
// 类的继承
class Dog extends Animal {
bark(){
return `${this.name} is barking`
}
}
const xiaobao = new Dog('xiaobao')
console.log(xiaobao.run())
console.log(xiaobao.bark())
// 多态
class Cat extends Animal{
static categories = ['mammal']//静态属性或静态方法不需要实例化就可以访问
constructor(name){
super(name)//重写属性要添加super()
console.log(this.name)
}
run(){//重写run方法
return 'Meow, ' + super.run()
}
}
const maomao = new Cat('maomao')
console.log(maomao.run())
console.log(Cat.categories)
/// Typescript 中的类
Public: 修饰的属性或方法是共有的 (默认)
Private: 修饰的属性或方法是私有的, 不能在声明它的类外部调用, 包括子类也不能访问
Protected: 修饰的属性或方法是受保护的, 与Private类似, 在子类中是允许被访问的.
注: 类中的属性被readonly修饰后, 也是只能读, 不能修改
2-10 类和接口 - 完美搭档
//继承的困境: 一个类只能继承自另一个类才能用里面的方法
//类可以使用implements来实现接口
interface Radio {
switchRadio(trigger: boolean): void;
}
interface Battery {
checkBatteryStatus(): void;
}
class Car implements Radio{//implements 引入一个接口
switchRadio(trigger: boolean){
}
}
class Cellphone implements Radio, Battery {//implements导入多个接口
switchRadio(trigger: boolean){
}
checkBatteryStatus(){
}
}
//interface 接口继承
interface RadioWithBattery extends Radio{//接口继承自Radio
checkBatteryStatus(): void;
}
class Cellphone2 implements RadioWithBattery {
switchRadio(trigger: boolean){
}
checkBatteryStatus(){
}
}
2-11 枚举(Enum)
enum Direction {
Up = 10,
Down,
Left,
Right,
}//默认自增加
console.log(Direction.Up)// 10
console.log(Direction[0]) // Up
编译后的js代码:
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
console.log(Direction.Up);
// 普通枚举
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
console.log(Direction.Up)// UP
console.log(Direction[0])// undefined
const value = 'UP'
if (value === Direction.Up) {
console.log('go up!')
}
//常量枚举
const enum Direction2 {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
// 普通的运行ts的方式: 先通过tsc编译为js文件然后运行node xx.js
// 安装ts-node: sudo npm install -g ts-node
// 使用ts-node运行: ts-node xxx.ts #执行文件
2-12 泛型(Generics) 第一部分: 泛型的基本用法
// 泛型: 调用方法时再指定类型
function echo<T>(arg:T): T {
return arg
}
const result = echo(true)
// 泛型可以传入多个值
function swap<T, U>(tuple:[T, U]): [U, T] {
return [tuple[1], tuple[0]]
}
const result2 = swap(['string', 123])
2-13 泛型(Generics) 第二部分 - 约束泛型
// 可以把泛型看成一个占位符, 在使用的时候才动态的填入确定的类型值.
// 约束泛型
// 方式一:
function echoWithArr<T>(arg:T[]): T[] { //定义返回一个含有T类型的Array
console.log(arg.length)// 如果不知道它是什么类型, 不能随意的操作它的属性或方法
return arg
}
const arrs = echoWithArr([1, 2, 3])
// 方式二:
// 需求: 对泛型进行约束, 只允许这个函数只能传入包含length属性的变量. (这就是约束泛型)
interface IWithLength {
length: number
}
function echoWithLength<T extends IWithLength>(arg:T): T {
console.log(arg.length)
return arg
}
const str = echoWithLength('str')
const obj = echoWithLength({length: 10})//只要有length属性, 就符合要求
const arr2 = echoWithLength([1,2,3])
2-14 泛型第三部分 - 泛型在类和接口中的使用
//需求: (类)创建一个队列, 只要number队列才能添加到队列里
class Queue<T> {
private data = [];
push(item: T) {
return this.data.push(item)
}
pop(): T{
return this.data.shift()
}
}
const queue = new Queue<number>()
queue.push(1)
// queue.push('str')//报错, 因为类型不符
console.log(queue.pop().toFixed())
// 需求: (接口)通过泛型限制interface内部属性的传值类型
interface KeyPair<T, U> {
key: T
value: U
}
let kp1: KeyPair<number, string> = {key: 1, value:"string"}
let kp2: KeyPair<string, number> = {key: 'str', value:1}
// 需求: 定义数组类型, 也可以通过泛型的方式来表示
let arr: number[] = [1,2,3]
let arr2 : Array<number> = [1,2,3]
2-15 类型别名,字面量 和 交叉类型
let sum: (x: number, y: number) => number
const result = sum(1,2)
// 使用类型别名(type aliase) 简写:
type PlusType = (x: number, y: number) => number
let sum2: PlusType
const result2 = sum2(2,3)
// 类型别名 -- 使用联合类型
type StrOrNumber = string | number
let result3: StrOrNumber = '123'
result3 = 123
// 字面量
const str: 'name' = 'name'
const number: 1 = 1
type Directions = 'Up' | 'Down' | 'Left' | 'Right'
let toWhere: Directions = 'Left'
// 交叉类型
interface IName {
name: string
}
type IPerson = IName & {age: number}
let person: IPerson = {name: '123', age: 123}
/* 什么时候使用interface, 什么时候使用 类型别名?
type作为类型别名具有非常宽泛的概念, 它本身不是一种特殊的类型, 只是别的类型的别名. 有点像"快捷方式".
当使用交叉或者组合类型的时候可以考虑使用type;
interface是Duck Typing的实现方式, 是一种独特的类型.
当要实现extends或这个类的implements的时候可以考虑使用interface.
*/
2-16 声明文件
// 当使用第三方库的时候, 很多第三方库不是通过Typescript写的, 它们是通过原生的JavaScript或者是浏览器或者是nodejs提供的runtime对象, 直接使用的话ts会报错.
// 比如使用第三方库jQuery, 常见的做法是在HTML中, 通过script标签引入jQuery, 然后就可以全局使用jQuery.
// jQuery('#foo') // 由于TS并不知道jQuery是什么, 所以会报错
// 这个时候可以使用关键declare来告诉tic, 这个变量已经在其他地方定义了, 用就好了, 不要报错.
// 通常会把声明语句放到一个单独文件中, 是以.d.ts结尾, 这就是声明文件. d代表声明, 它说明该文件只有适配ts的类型声明.
// 创建一个jQuery.d.ts文件, 在文件中:
declare var jQuery: (selector: string) => any
// 在test.ts中:
jQuery('#foo') //现在发现有这个定义好的类型了
// 注意: declare 并没有真正的定义一个变量的实现, 只是定义了全局变量jQuery的类型, 仅仅用于编译时检查, 并不是实现功能的真正代码.
// 有了这个文件就可以享受ts带来的红利了, 其他地方使用都会获得d.ts里面jQuery的类型定义了.
// 官方: 使用第三方声明文件 @types/jquery
// 安装: npm install --save @types/jquery
// 一般常见库在@types组织下都能搜索到
// 在这个网站 https://microsoft.github.io/TypeSearch/ 可以搜索到三方库, 并提供下载方法
// 除了在@types中找到这些常用的库外,现在很多库源代码自带@types定义, 比如说用npm install安装了某个库, 它的类型定义也包含其中, 不需要跟jQuery一样,使用时先安装本体,再安装@types类型文件.
// 这种情况下, 我们可以一次安装, 双重搞定, 比如有个管理工具叫: redux, 安装方法: npm install --save redux . 它就是直接提供了定义文件和源代码.
import {Action} from 'redux' //导入redux文件
// 默认情况下所有可见的@types包, 都会在编译过程中被包含进来
// 当一个库没有声明文件的时候, 我们就需要自己定义声明文件了
2-17 内置类型
// 除了第三方库, JavaScript还有很多内置对象(built-in objects).
const a: Array<number> = [1,2,3]
const date = new Date()
date.getTime()
const reg = /abc/ //正则
reg.test('abc')
// build-in object
Math.pow(2, 2)
//DOM and BOM
let body = document.body
let allLis = document.querySelectorAll('li')
allLis.keys()
document.addEventListener('click', (e)=>{
e.preventDefault()
})
// Utility Types
interface IPerson {
name: string
age: number
}
let viking: IPerson = { name: 'viking', age: 20} //两个属性都必须传
type IPartial = Partial<IPerson>// Partial 可以把传入属性都变成可选
let viking2: IPartial = { name: 'viking'}
type IOmit = Omit<IPerson, 'name'>// Omit 可以忽略传入类型的某个属性
第3章 初识 Vue3.0: 新特性详解
3-1 vue3 新特性巡礼
/*Vue3新特性
Composition API :
ref 和 reactive
computed 和 watch
新的声明周期函数
自定义函数 - Hooks函数
其他新增特性:
Teleport - 瞬移组件的位置
Suspense - 异步加载组件的新福音
全局API的修改和优化
更好的Typescript支持:
Vue3的源代码都是用Typescript编写的
*/
3-2 为什么会有 vue3
Vue2遇到的难题:
随着功能的增长, 复杂组件的代码变得难以维护.
Vue2 对Typescript的支持非常有限
3-3 使用 vue-cli 配置 vue3 开发环境
node -v
# 脚手架工具: Vue CLI
# 安装步骤:
npm install -g @vue/cli #安装Vue CLI
vue --version #查看Vue CLI版本号
# 命令行创建vue项目
vue create 项目名称
Manually select features #手动选择一些特性
空格选择"Choose Vue version"、"Babel"、"Typescript"、"Linter / Formatter"(代码格式检查工具)
3.x(Preview) #选择3.x版本
Use class-style component syntax? (y/N) #是否需要class-style组件? 选择N
Use Babel alongside TypeScript(required for modern mode, auto-detected polyfills, transpiling JSX)?(Y/n) #是不是Babel结合Typescript一起使用? 选择n
ESLint with error prevention only #选择这个
Lint on save #Lint的特性
In dedicated config files #把配置文件放单独的文件还是package.json文件中
Save this as a Preset for future pfojects?(y/N)# N
# UI界面创建vue新项目:
vue ui #(打开生成网站, 选择步骤同上)
3-4 项目文件结构分析和推荐插件安装
#Vue项目结构:
node-modules #安装一些依赖
public #公共的文件
favicon.ico # 浏览器标签的小图标
index.html # 入口的html文件
src
assets #静态文件, 放图片之类的
components #
helloword.vue #子组件
App.vue #根组件
main.ts #入口文件
shims-vue.d.ts#专门为vue的文件创建的定义文件
package.json #配置文件
$ npm run serve #运行项目
推荐两个VSCode插件:
ESLint #代码规范规则
Vetur #官方推荐: 语法高亮、代码片段
3-5 vue3 - ref 的妙用
<template>
<h1>{{count}}</h1>
<h1>{{double}}</h1>
<button @click="increase">👍+1</button>
</template>
<script lang="ts">
import { ref, computed, defineComponent } from 'vue';
export default defineComponent({
name: 'App',
setup(){
const count = ref(0) //ref : 把js原始类型转换成为响应式对象
const double = computed(() => {//computed: 计算属性; 返回的也是响应对象
return count.value *2
})
const increase = () => {
count.value++
}
return {// 导出对象
count,
increase,
double
}
}
});
</script>
3-6 更近一步 - reactive
<template>
<img alt="Vue logo" src="./assets/logo.png">
<h1>{{count}}</h1>
<h1>{{double}}</h1>
<button @click="increase">👍👍 +1</button>
</template>
<script lang="ts">
import { ref, computed, reactive, toRefs, defineComponent } from 'vue';
interface DataProps{
count: number;
double: number;
increase: () => void;
}
export default defineComponent({
name: 'App',
setup(){
/*如果使用了Typescript, data会报错类型错误, 因为在computed回调中使用了data.count, 会造成类型推论的循环, 由于TS的局限性, 它会自动将data推断成any类型. 解决方法是: 我们需要显式的为data指定类型*/
const data: DataProps = reactive({ // reactive: 把多个变量, 包裹在一个对象中
count: 0,
increase: () => {data.count ++},
double: computed(()=> data.count * 2)
})
// toRefs接收一个普通对象, 会将这个对象变成响应对象
const refData = toRefs(data)
return {// 导出对象
...refData // ... 将refData内部属性展开
}
}
});
</script>
//确定模板中是否使用的是响应属性类型, 这样才会确定数据改变时模板变化.
// ref和reactive的区别: 像选择原始类型vs对象类型一样选择使用这两个; 单独使用reactive可能会丧失响应性, 配合toRefs来解决对象丧失响应的问题.
3-7 vue3 响应式对象的新花样
// vue2的实例:
Object.defineProperty(data, 'count',{
get(){},
set(){},
})
// vue3的实例:
new Proxy(data, {
get(key){ },
set(key, value){ },
})
vue3响应式对象的高明之处, 内部依赖了ES6的Proxy对象, 改变了原来的Object.defineProperty的弊端, 完美支持数组、对象的修改操作, 让$set成了过去时.
<template>
<img alt="Vue logo" src="./assets/logo.png">
<h1>{{count}}</h1>
<h1>{{double}}</h1>
<ul>
<li v-for="number in numbers" :key="number"><h1>{{number}}</h1></li>
</ul>
<h1>{{person.name}}</h1>
<button @click="increase">👍👍 +1</button>
</template>
<script lang="ts">
import { ref, computed, reactive, toRefs, defineComponent } from 'vue';
interface DataProps{
count: number;
double: number;
increase: () => void;
numbers: number[]; // 数组
person: { name?: string}; // Object对象
}
export default defineComponent({
name: 'App',
setup(){
const data: DataProps = reactive({
count: 0,
increase: () => {data.count ++},
double: computed(()=> data.count * 2),
numbers: [0, 1, 2],
person: {}
})
data.numbers[0] = 5
data.person.name = 'viking'
const refData = toRefs(data)
return {// 导出对象
...refData
}
}
});
</script>
3-8 老瓶新酒 - 生命周期
// mapping vue2 to vue3 (生命周期函数的变化:vue2->vue3)
beforeCreate -> use setup()
created -> use setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
activated -> onActivated
deactivated -> onDeactivated
errorCaptured -> onErrorCaptured
// added
onRenderTracked
onRenderTriggered
<script lang="ts">
import { ref, computed, reactive, toRefs, defineComponent, onMounted, onUpdated, onRenderTracked } from 'vue';
export default defineComponent({
name: 'App',
setup(){
onMounted(()=>{
console.log('mounted')
})
onUpdated(()=>{//组件更新会来到这个方法
console.log('updated')
})
onRenderTracked((event)=>{//调试作用: 观察数据的变化
console.log(event)
})
return {// 导出对象
}
}
});
</script>
3-9 侦测变化 - watch
<template>
<img alt="Vue logo" src="./assets/logo.png">
<h1>{{count}}</h1>
<h1>{{double}}</h1>
<ul>
<li v-for="number in numbers" :key="number"><h1>{{number}}</h1></li>
</ul>
<h1>{{person.name}}</h1>
<button @click="increase">👍👍 +1</button>
<button @click="updateGreeting">Update Title</button>
</template>
<script lang="ts">
import { ref, computed, reactive, toRefs, defineComponent, watch } from 'vue';
interface DataProps{
count: number;
double: number;
increase: () => void;
numbers: number[]; // 数组
person: { name?: string}; // 对象
}
export default defineComponent({
name: 'App',
setup(){
const data: DataProps = reactive({
count: 0,
increase: () => {data.count ++},
double: computed(()=> data.count * 2),
numbers: [0, 1, 2],
person: {}
})
data.numbers[0] = 5
data.person.name = 'viking'
const greetings = ref('')
const updateGreeting = () => {
greetings.value += 'Hello!'
}
// 监听一个值
// 第一个参数: 要监听的响应式对象/getter方法/数组, 第二个参数: 有变化时要执行的函数体
watch(greetings,(newValue, oldValue)=>{// 回调函数有两个参数: 新的值和旧的值
document.title = 'updated' + greetings.value
})
// 监听多个值
watch([greetings, () => data],(newValue, oldValue)=>{
console.log('old', oldValue)
console.log('new', newValue)
document.title = 'updated' + greetings.value + data.count
})
// 监听对象单个值
// 由于data是reactive对象, 对于debug来说不好用, 如何但单独拿出一个值进行监控呢?
// 监听对象某个属性值, 可以改为getter方法
watch([greetings, () => data.count],(newValue, oldValue)=>{
console.log('old', oldValue)
console.log('new', newValue)
document.title = 'updated' + greetings.value + data.count
})
const refData = toRefs(data)
return {// 导出对象
...refData,
greetings,
updateGreeting
}
}
});
</script>
3-10 vue3 模块化妙用- 鼠标追踪器
///需求: 跟踪鼠标位置坐标. (抽离逻辑模块)
// sp1.在"src"文件夹下创建"hooks"文件夹(放置所有抽离出来的逻辑模块 ), 在hooks下新建useMousePosition.ts文件, 在useMousePosition.ts中:
import { ref, onMounted, onUnmounted } from 'vue'
// 自定义函数
function useMousePosition() {
const x = ref(0)
const y = ref(0)
const updateMouse = (e: MouseEvent) => {//捕获鼠标点击的坐标
x.value = e.pageX
y.value = e.pageY
}
onMounted(() =>{
document.addEventListener('click',updateMouse)//添加事件
})
onUnmounted(() =>{
document.removeEventListener('click',updateMouse)// 移除事件
})
return {x , y}
}
export default useMousePosition
// sp2.在Vue中导入并使用useMousePosition.ts
<template>
<h1>X: {{x}}, Y:{{y}}</h1>
</template>
<script lang="ts">
import { ref, computed, reactive, toRefs, defineComponent, watch } from 'vue';
import useMousePosition from './hooks/useMousePosition'
export default defineComponent({
name: 'App',
setup(){
const { x, y } = useMousePosition()// 直接调用函数一样使用它
return {// 导出对象
x,
y,
}
}
});
</script>
3-11 模块化难度上升 - useURLLoader
// 发送异步请求工具: axios
// 安装: npm install axios --save
// 一个免费的狗狗图片API: https://dog.ceo/dog-api/
//需求: 异步加载图片时, 显示和隐藏loadding状态.
//sp1. 在hooks文件夹下创建文件useURLLoader.ts, 在useURLLoader.ts文件中:
import { ref } from "vue";
import axios from "axios";
function useURLLoader(url: string) {
const result = ref(null) // 返回结果
const loading = ref(true)
const loaded = ref(false) //视图加载完毕
const error = ref(null) // 返回错误信息
// 发送异步请求
axios.get(url).then((rawData)=>{
loading.value = false
loaded.value = true
result.value = rawData.data
}).catch(e =>{
error.value = e
loading.value = false
})
return {
result,
loading,
error,
loaded
}
}
export default useURLLoader
// sp2. 在APP.vue中使用它
<template>
<h1 v-if="loading">Loading!...</h1>
<img v-if="loaded" :src="result.message">
</template>
<script lang="ts">
import { ref, computed, reactive, toRefs, defineComponent, watch , onMounted, onUnmounted } from 'vue';
import useURLLoader from './hooks/useURLLoader'
export default defineComponent({
name: 'App',
setup(){
const {result, loading, loaded} = useURLLoader('https://dog.ceo/api/breeds/image/random')
return {// 导出对象
result,
loading,
loaded
}
}
});
</script>
3-12 模块化结合typescript - 泛型改造
// VSCode 代码格式化 快捷键: Shift + Option + F
// 一个免费的猫图片API: https://api.thecatapi.com/v1/images/search?limit=1
// 需求: 希望result获得对应的类型, 而不是类型推论推断出的ref<null>类型
// sp1. 在useURLLoader.ts中:
import { ref } from "vue";
import axios from "axios";
function useURLLoader<T>(url: string) {
const result = ref<T | null>(null) // 使用联合类型
const loading = ref(true)
const loaded = ref(false)
const error = ref(null)
axios.get(url).then((rawData)=>{
loading.value = false
loaded.value = true
result.value = rawData.data
}).catch(e =>{
error.value = e
loading.value = false
})
return {
result,
loading,
error,
loaded
}
}
export default useURLLoader
// sp2. 在App.vue中:
<template>
<h1 v-if="loading">Loading!...</h1>
<!-- <img v-if="loaded" :src="result.message" /> -->
<img v-if="loaded" :src="result[0].url" />
</template>
<script lang="ts">
import {
defineComponent,
watch,
} from "vue";
import useURLLoader from "./hooks/useURLLoader";
// interface DogResult {
// message: string;
// status: string;
// }
interface CatResult {
id: string;
url: string;
width: number;
height: number;
}
export default defineComponent({
name: "App",
setup() {
// const { result, loading, loaded } = useURLLoader<DogResult>(
// "https://dog.ceo/api/breeds/image/random"
// );
// watch(result, () => {
// if (result.value) {//判断为非空
// console.log('value', result.value.message)// 这样就可以是实现代码自动补全了
// }
// });
const { result, loading, loaded } = useURLLoader<CatResult[]>(
"https://api.thecatapi.com/v1/images/search?limit=1"
);
watch(result, () => {
if (result.value) {
console.log("value", result.value[0].url);
}
});
return {
result,
loading,
loaded,
};
},
});
</script>
3-13 Typescript 对 vue3 的加持
在components文件下的HelloWord.vue中:
<script lang="ts">
// 用新的方法定义Component, 这个方法就是defineComponent.
// 这个导入并没有实现任何逻辑, 把传入的Object直接返回, 它的存在完全为了让传入的对象获得对应的类型
// defineComponent它能让被包裹的对象获得非常多的类型 ,也就是说 完全是为服务Typescript而存在的
import { defineComponent } from 'vue';
// 普通的组件定义和导出都是一个Object, 这个Object没有任何代码提示; 被defineComponent包裹后, 就有代码提示了.
export default defineComponent({
name: 'HelloWorld',
props: {
msg: {
required: true,
type: String
},
},
// 参数一: 能访问组件传入的props属性, 而且会自动推论成在props里面定义的类型. props是一个响应式对象,当值放生变化时会做出响应
// 参数二: context提供了最常用的三个属性attrs、slots、emit. 这几个值都是同步到最新的值的.比如每次使用拿到的都是最新的值
setup(props, context){
// props.msg
// context.attrs //属性
// context.slots //插槽
// context.emit //发送事件
}
});
</script>
3-14 Teleport - 瞬间移动 第一部分
需求:在某个组件渲染的时候, 在某种条件下需要显示一个全局的对话框<Dialog/>, 让用户完成确定或取消的操作.
常规操作: 顶层DOM节点 <--挂载-- 顶层组件 --> 各种子组件 --> Dialog组件
<div class="foo">
<div class="foo">
<div> ... </div>
<Dialog v-if="dialogOpen"/>
</div>
</div>
遇到的问题:
Dialog被包裹在其他组件之中, 容易被干扰
样式在其他组件中, 容易变得非常混乱
希望的解决方案:
顶层DOM节点 <--挂载-- 顶层组件 --> 各种子组件
Modal组件--挂载-->顶层另一个DOM节点
//sp1. 在"components"文件夹下新建Modal.vue文件, 在Modal.vue中:
<template>
<!-- 使用teleport进行包裹 -->
<!-- to属性接收一个css 作为参数, 代表要把这个组件渲染到哪个DOM上去. 比如把它渲染到id为modal的DOM节点上去 -->
<teleport to="#modal">
<div id="center">
<h2>this is a modal</h2>
</div>
</teleport>
</template>
<script lang="ts">
export default {
}
</script>
<style>
#center {
width: 200px;
height: 200px;
border: 2px solid black;
background: white;
position: fixed;
left: 50%;
top: 50%;
margin-left: -100px;
margin-top: -100px;
}
</style>
//sp2. 在public文件夹下的index.html中, 创建一个"modal"节点:
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- 创建一个modal节点, 作为根元素, 跟app是同级的 -->
<div id="modal"></div>
</body>
</html>
//sp3. 在App.vue中使用该组件:
<template>
<modal/>
</template>
<script lang="ts">
import {
ref,
defineComponent,
watch,
} from "vue";
import Modal from "./components/Modal.vue";
export default defineComponent({
name: "App",
components: {
Modal,
},
setup() {
return {
};
},
});
</script>
Teleport成功的把应该在app内部渲染的"modal"组件, 传送到了另外一个DOM节点上去.
3-15 Teleport - 瞬间移动 第二部分
需求: 实现modal的打开和关闭
//sp1. 在Modal.vue中:
<template>
<teleport to="#modal">
<div id="center" v-if="isOpen">
<!-- 把slot放在自定义的地方, 这样就可以自定义内容, 默认内容是"this is a modal" -->
<h2><slot>this is a modal</slot></h2>
<button @click="buttonClock">Close</button>
</div>
</teleport>
</template>
<script lang="ts">
import { defineComponent } from "@vue/runtime-core";
export default defineComponent({
//定义属性
props: {
isOpen: Boolean,
},
//emits: 定义向外发射的自定义事件
emits: {
"close-modal": null//null表示不验证了
},
setup(props, context){
const buttonClock = () => {
context.emit('close-modal')
}
return {
buttonClock
}
}
/// 验证对外自定义方法:
// emits: {
// //自定义事件名称: 自定义验证函数
// "close-modal": (payload: any) => {
// //支持运行时检验
// return payload.type === "close";
// },
// },
// setup(props, context) {
// // 运行时验证"close-modal"方法
// context.emit("close-modal", {
// type: "hello",
// });
// },
});
</script>
//sp2. 在App.vue中:
<template>
<button @click="openMoal">open modal</button>
<modal :isOpen="modalIsOpen" @close-modal="onMoalClose">My Modal !!!</modal>
</template>
<script lang="ts">
import {
ref,
defineComponent,
watch,
} from "vue";
import Modal from "./components/Modal.vue";
export default defineComponent({
name: "App",
components: {
Modal,
},
setup() {
const modalIsOpen = ref(false);
const openMoal = () => {
modalIsOpen.value = true;
};
const onMoalClose = () => {
modalIsOpen.value = false;
};
return {
modalIsOpen,
openMoal,
onMoalClose,
};
},
});
</script>
// 总结: 用到了将组件渲染到另外一个DOM节点的方法, 也就是使用Vue3新推出的"传送门特性"Teleport; 同时还用到了vue3新推出的emits, 可以更明确的显示自定义事件有哪些.
3-16 Suspense - 异步请求好帮手第一部分
在发起异步请求时, 要判断请求状态, 根据请求是否完毕, 展示界面.
Suspense 是 Vue3 推出的一个内置的特殊异步组件. 它会有两个template标签, 刚开始的时候渲染一个fallback内容, 直到达到某个条件以后, 才会渲染正式的内容. 这样进行异步内容的渲染就会变得简单.
如果使用Suspense, 要在setup中返回一个promise, 而不是直接返回一个对象.
// 需求: 3秒后, 在setup返回一个数字
// sp1. 在components文件夹下, 新建AsyncShow.vue文件, 在AsyncShow.vue文件中:
<template>
<h1>{{ result }}</h1>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
setup() {
// 如果想用异步组件Suspanse, 需要包裹一层Promise
return new Promise((resolve) => {
//3秒钟后, 返回数字42
setTimeout(() => {
return resolve({
result: 42,
});
}, 3000);
});
},
});
</script>
// sp2. 在App.vue中:
<template>
<Suspense>
<!-- 这里面可以展示获得"result"之后的内容 -->
<template #default>
<asyncShow/>
</template>
<!-- 这里面可以展示获得"result"之前的内容 -->
<template #fallback>
<h1>Loading !...</h1>
</template>
</Suspense>
</template>
<script lang="ts">
import {
ref,
defineComponent,
} from "vue";
import AsyncShow from './components/AsyncShow.vue'
export default defineComponent({
name: "App",
components: {
AsyncShow,
},
setup() {
return {
};
},
});
</script>
总结: Suspanse可以非常方便的为我们针对异步请求界面进行个性化的定制
3-17 Suspense - 异步请求好帮手第二部分
// 之前在<template #default>里包裹了一个组件, 其实在等待"result"中, template可以添加多个异步组件, 它会等这些组件都result后, 再展示对应的内容.
// sp1. 在components文件夹下新建DogShow.vue文件, 在DogShow.vue中:
<template>
<img :src="result && result.message" />
</template>
<script lang="ts">
import axios from "axios";
import { defineComponent } from "vue";
export default defineComponent({
// 如果想用异步组件Suspanse, 需要包裹一层Promise
// 使用async await来代替Promise对函数进行包裹, 使用await进行异步请求, 返回结果自动用Promise进行包裹.
async setup() {
const rawData = await axios.get("https://dog.ceo/api/breeds/image/random");
return {
result: rawData.data,
};
},
});
</script>
//sp2. 在App.vue中:
<template>
<!-- 在界面上展示错误 -->
<p>🌺🌺{{ error }}</p>
<Suspense>
<!-- 这里面可以展示获得"result"之后的内容 -->
<template #default>
<div>
<async-show />
<dog-show />
</div>
</template>
<!-- 这里面可以展示获得"result"之前的内容 -->
<template #fallback>
<h1>Loading !...</h1>
</template>
</Suspense>
</template>
<script lang="ts">
import {
ref,
defineComponent,
onErrorCaptured,
} from "vue";
import AsyncShow from "./components/AsyncShow.vue";
import DogShow from "./components/DogShow.vue";
export default defineComponent({
name: "App",
components: {
AsyncShow,
DogShow,
},
setup() {
// 抓取"错误"信息, 比如网络请求产生的错误信息.
const error = ref(null);
onErrorCaptured((e: any) => {
error.value = e;
return true; //钩子函数要返回一个布尔值,表示这个错误是否向上传播,返回true就行
});
return {
error,
};
},
});
</script>
3-18 全局 API 修改
Vue2 全局API遇到的问题:
在单元测试中, 全局配置非常容易污染全局环境
在不同的apps中, 共享一份有不同配置的Vue对象, 也变的非常困难
Vue3 入口文件 main.ts.
全局配置: Vue.config --变成了--> app.config
config.productionTip 被删除
config.ignoredElements 改名为 config.isCustomElement
config.keyCodes 被删除
全局注册类API:
Vue.component --变成了--> app.component //注册全局的组件
Vue.directive --变成了--> app.directive //全局的指令
行为扩展类API:
Vue.mixin --变成了--> app.mixin
Vue.use --变成了--> app.use //安装全局的插件
Treeshaking (摇一棵树,让死掉的叶子掉下来): 由webpack提出的一个概念. 比如下面的例子:
// 在hello.js模块中:
export function hello(message) {
return 'hello' + message
}
export function foo(message){
return 'bar' + message
}
// 在main.js中使用这个模块:
import { hello, foo } from './hello'
alert(hello('viking'))
//此时把hello 和 foo函数都导入了进来, 而逻辑中只使用了hello方法, foo没有被使用, 所以如果你支持Treeshaking的打包工具呢, 比如webpack等, 它打包过程中, 就不会把foo这段具体实现给打包进去, 这样就做到了最后输出代码的精简过程.
// Vue3对于不修改行为的API采用了同样的ES6的Module的格式, 并且把全局的API改成具体的导出, 就是为了打包的代码可以享受Treeshaking的优化, 从而减少应用体积.