美文网首页
仿知乎项目总结

仿知乎项目总结

作者: Amok校长 | 来源:发表于2021-04-20 16:28 被阅读0次

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

    相关文章

      网友评论

          本文标题:仿知乎项目总结

          本文链接:https://www.haomeiwen.com/subject/mvpnlltx.html