仿知乎项目总结

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的优化, 从而减少应用体积.
上一篇下一篇

猜你喜欢

热点阅读