Midqiu

Midqiu 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

Midqiu 关注了用户 · 4月24日

尹光耀 @yinguangyao

前端交流群:468273103

关注 57

Midqiu 关注了用户 · 3月5日

皮小蛋 @scaukk

The Best Way to Improve Yourself:

  1. Build Stuffs
  2. Help Others
  3. Teach

PS: Shopee 招人, 薪酬福利待遇好

感兴趣的话, 可以联系我内推。

关注 712

Midqiu 关注了用户 · 1月10日

凤晴铃玉 @fengqinglingyu

关注 1

Midqiu 赞了文章 · 1月6日

Vue with typescript

最近尝试了一下 TypeScript,试着把一个 Vue 项目改成了 TypeScript 的,感觉还不错

目前 Vue 和 TypeScript 的配合还不算很完美,Vuex 和 TypeScript 的配合挺糟糕的,尝试需要谨慎

如果想体验一下的话,强烈建议你用 vue-cli 3 直接生成项目目录,这样会少挺多配置,比如配 tsconfig 什么的,在用 vue-cli 的时候选择 TypeScript 就好

如果想自己体验从 0 开始配置请参考这个文档 TypeScript-Vue-Starter

初始配置

这里不提初始配置,因为 vue-cli 已经默认配置好了,在根目录下,有个 tsconfig.json 的文件,在 src 的文件夹下面,有 shims-tsx.d.ts, shims-vue.d.ts 文件

shims-vue.d.ts 这个文件,主要用于 TypeScript 识别.vue 文件,Ts 默认并不支持导入 vue 文件,这个文件告诉 ts 导入.vue 文件都按VueConstructor<Vue>处理,因此导入 vue 文件必须写.vue 后缀,但是这样同样的也会造成,就算你写的导入的 .vue 文件的路径就算是错的,静态检测也不会检测到错误,如果你把鼠标放上面你会看到错误的路径就是指向这个文件,因为你定义了这个模块是所有 .vue 后缀的导入都会指向到这个文件,但是如果你的路径是对的,ts 能读出正确的 module。
说那么多可能没用,看一下下面两张图就懂了

导入地址是正确的导入地址是正确的
导入地址是错误的导入地址是错误的

shims-tsx.d.ts 文件,这个文件主要是方便你使用在 ts 中使用 jsx 语法的,如果不使用 jsx 语法,可以无视这个,但是强烈建议使用 jsx 语法,毕竟模板是没法获得静态类型提示的,当然,如果你境界高的话,直接用 vue render function。想要使用 jsx 语法的话,配合 babel-plugin-jsx-v-model,这个插件,体验更佳,这个插件在 jsx 语法中实现了 v-model。

编辑器支持

推荐使用 Visual Studio Code 和 Vetur 插件,如果用的是 IDE,推荐使用 WebStorm

使用 TypeScript 编写 Vue 的时候,主要有两种方法Vue.extend()vue-class-component

  • Vue.extend():使用基础 Vue 构造器,创建一个“子类”。 这种方式最接近 Vue 的单文件组件的写法,如果一个完善 Vue 项目从 JS 改成 TS,用这种方法很快,只要加上 lang=ts和一些必要的变量类型就好了,然后用Vue.extend()包裹就好。请看下面的例子:
    JavaScript
    JavaScript

    TypeScript
    TypeScript

  • vue-class-component:通常和vue-property-decorator一起搭配使用,实际使用只使用vue-property-decorator就好了,vue-property-decorator是在vue-class-component上扩展来的,并且提供了很多修饰器比如 @Prop@Watch等等,使用这个可以编写类式组件,但是如果你是完善的项目 JS 改 TS 的话,需要改的地方很多。看一下下面的例子:
    JavaScript
    JavaScript

    TypeScript
    TypeScript

    可以看出变化真的很大

这两种编写方式风格有很大的不同,下面具体说一下两种方式的具体实现

  • 组件 props

    • Vue.extend()实现 props 其实和 JavaScript 没有任何差别,但是如果你需要有 Object 变量类型提示,那就有点不一样了
    • vue-class-component实现 props, 需要从 vue-property-decorator 引入 Prop 这个修饰符,使用起来也非常方便,还是用上面的例子
      // JavaScript
      props: ['isVisible', 'title', 'item', 'count', 'items']
      // 如果加入了prop验证,这样写
      props: {
        isVisible: {
          type: Boolean,
          required: true
        },
        title: {
          type: [String, Number]
        },
        item: {
          type: Object
        },
        items: {
          type: Array,
        }
        count: {
          count: Number
        }
      }

      // TypeScript
      /* 这种写法没有任何改变,但是这样没有任何类型提示,这些变量能被TS识别,但是会全部被识别成any,和没类型检查一样 */
      props: ['isVisible', 'title', 'item', 'count', 'items']
      /* 当加入prop验证之后,TS就会提示prop类型了,如果是对象的话,还能有对象的成员提示,写法和JS写法差不多,只是对象类型(包括对象,数组和函数)的有点差别,这样写的话。*/
      // 假设item对象的结构是
      interface Item {
        key: string
        val: string
        num: number
      }
      props: {
        isVisible: {
          type: Boolean,
          required: true
        },
        title: {
          type: [String, Number]
        },
        item: {
          // 注意这里不是
          // Object as Item
          type: Object as () => Item
        },
        itmes: {
          // 注意这里不是
          // Array as Array<Item>
          type: Array as () => Array<Item>
        }
        count: {
          count: Number
        }
      }

      // vue-class-component方式
      import { Vue, Component, Prop } from 'vue-property-decorator'

      // 注意要加非空断言符 ! 不然会报,当然,你定义成any类型当我没说
      /* [non-null-assertion-operator](https://github.com/Microsoft/TypeScript/wiki
      /What's-new-in-TypeScript#non-null-assertion-operator) 关于非空断言可以参考这个 */
      @Prop({
        required: true
      }) isVisible!: boolean
      @Prop() title!: string | number
      @Prop() item!: Item
      @Prop() items!: Array<Item>
      @Prop() count!: number
  • 组件 data computed methods watch

    • Vue.extend()实现 data 其实和 JavaScript 没有任何差别,computed 的话,也没什么大的改变,但是有 this 参与运算的必须标明返回值类型,不然会报错, methods 的处理方式和 computed 的一样,有 this 参与运算的必须标明返回值类型,watch 也是一样
    • vue-class-component实现 data 的话,直接在类里面写变量就好,computed 的话,写法类似 getter 和 setter,methods 处理方式就是直接在里面写方法,watch 需要从 vue-property-decorator 引入 Watch 这个修饰符
      //一个简单的例子
      // Vue.extend()
      import Vue from 'vue'
      export default Vue.extend({
        data() {
          return {
            count: 1,
            item: {
              c: '',
              n: ''
            }
          }
        },
        computed: {
          // 需要标注有 `this` 参与运算的返回值类型
          num(): number {
            return this.count
          },
          name: {
            // 需要标注有 `this` 参与运算的返回值类型
            get(): string {
              return this.item.n
            },
            set(val: string) {
              this.item.n = val
            }
          }
        },
        watch: {
          count(newVal: number, oldVal: number): void {
            console.log(newVal)
          },
          'item.n'(newVal: string, oldVal: string): void {
            console.log(newVal)
          },
          item: {
            handler(newV, oldVal) {
              console.log(oldVal)
            },
            deep: true
          }
        },
        methods: {
          reset(): void {
            this.$emit('reset')
          },
          getKey(): string {
            return this.item.c
          }
        }
      })
    // vue-class-component
    import { Vue, Component, Watch } from 'vue-property-decorator'

    interface KeyValue {
      c: string
      n: string
    }

    @Component
    export default class Test extends Vue {
      // data
      count: number = 1
      item: KeyValue = {
        c: '',
        n: ''
      }

      // computed
      get num(): number {
        return this.count
      }
      get name(): string {
        return this.item.n
      }
      // 注意,这里不能标返回值类型,就算写void也不行
      set name(val: string) {
        this.item.n = val
      }

      // watch
      @Watch('count')
      watchCount(newVal: number, oldVal: number): void {
        console.log(newVal)
      }
      @Watch('item.n')
      watchName(newVal: string, oldVal: string): void {
        console.log(newVal)
      }
      @Watch('item', { deep: true })
      watchItem(newVal: KeyValue, oldVal: KeyValue): void {
        console.log(newVal)
      }
      // methods
      reset(): void {
        this.$emit('reset')
      },
      getKey(): string {
        return this.item.c
      }
    }
  • 组件 components

    • Vue.extend() components 和 JavaScript 写法完全一致
    • vue-class-component 需要把导入的组件写在修饰器@Components({})里面
      // Vue.extend
      import Vue from 'vue'
      import MainHeader from './header.vue'
      import MainContent from './content.vue'

      export default Vue.extend({
        components: {
          MainHeader,
          MainContent
        }
      })

      // vue-class-component
      import { Vue, Component } from 'vue-property-decorator'
      import MainHeader from './header.vue'
      import MainContent from './content.vue'

      @Component({
        components: {
          MainHeader,
          MainContent
        }
      })
      export default class extends Vue {}
  • 组件 mixins

    • Vue.extend() 并不能完全实现 mixins 多混入的效果,只能混入一个。不推荐混入用这种方式写,无法实现多继承。如果你非要尝试这种写法,可以看看这个Issue,我没有尝试过这种写法,不过有人写了个例子,可以作为参考,但是我尝试了没成功

      // ExampleMixin.vue
      export default Vue.extend({
        data () {
          return {
            testValue: 'test'
          }
        }
      })
      
      // other.vue
      export default Vue.extend({
        mixins: [ExampleMixin],
        created () {
          this.testValue // error, testValue 不存在!
        }
      })
      我们需要稍作修改:
      
      // other.vue
      export default ExampleMixin.extend({
        mixins: [ExampleMixin],
        created () {
          this.testValue // 编译通过
        }
      })
      
    • vue-class-component 能够实现多混入,写法类似类继承

        // mixin1.ts
        import Vue from 'vue'
      
        export default Vue.extend({
          data () {
            return {
              valFromMixin1: 'test'
            }
          }
        })
        // 不能是
        // 这种写法会报 Mixin1 is not a constructor function type
        export default {
          data () {
            return {
              valFromMixin1: 'test'
            }
          }
        }
      
        // mixin2.ts
        import { Component, Vue } from 'vue-property-decorator'
      
        @Component
        export default class Mixin2 extends Vue {
          methodFromMixin2() {}
        }
      
        // test.ts
        import Mixin1 from './mixin1'
        import Mixin2 from './mixin2'
        import { Component, Mixins } from 'vue-property-decorator'
      
        export default class Test extends Mixins(Mixin1, Mixin2) {
          test() {
            this.methodFromMixin2()
            console.log(this.valFromMixin1)
          }
        }
        // 如果只混入一个的话,可以这样写
        export default class Test extends Mixin1 {}
        export default class Test extends Mixin2 {}
      

      这样写不仅不会报错,而且编辑器还有提示
      Mixin

      这张图可以看出,setWatch 是 BookingWatcher 里面的方法,实际上,Test 这个类并没有自身的属性,都是从 Vue,BookingWatcher 还有 TestMixins 继承过来的

  • 函数式组件

    这个只能用 Vue.extends(),Vue-class-component 无能为力,可以看看这个 Issue
    这里提供一个函数式组件的例子
    函数式组件

这部分资料参考

在 Vue 中使用 TypeScript 的一些思考(实践)
vue 官网 TypeScript 支持

开发中遇到的一些问题总结

  • $refs 报错
    $refs 报错这个问题相信基本都遇到,除非你真没用到这个,如图:
    图片描述

    报错信息是
    Property 'blur' does not exist on type 'Vue | Element | Vue[] | Element[]'.
    Property 'blur' does not exist on type 'Vue'.

    解决方案:把上图报错部分改成

    // 把这个变量改成定义成HTMLInputElement就好,这里需要用到类型断言
    test() {
      let inputBox: HTMLInputElement = this.$refs.inputBox as HTMLInputElement
       inputBox.blur()
    }
    

    如果引用的比较多,并且引用里面有自己的组件的话,可以这样写:
    $refs: {}
    图片描述

    这样编辑器还会提示组件里面有什么方法,当你打出 this.$refs.header.时候,编辑器有提示 Header 这个组件里面的属性和方法

查看原文

赞 8 收藏 1 评论 0

Midqiu 发布了文章 · 2019-11-07

写给JS程序员的typescript入门教程(一)

为什么要用 typescript

typescript引入了类型,避免了诸如函数调用传错参数的低级错误:

clipboard.png

clipboard.png

提供了代码提示:
clipboard.png

如何使用

npm install typescript -g

这里需要说明一下的是,typescript是一个泛指,包括语言特性和语言相关的生态功能。要不然看官方的更新文档,就会有困惑,有很多明显不属于typescript语言的更新,比如重命名的时候对应的函数或文件跟着自动变了这种功能,很明显不属于语言的特性,应该是编辑器或者IDE负责的东西,咋就放到语言的更新日志里了呢?

从一个例子说起

来,我们看个例子:

function add(a:number, b:number):number {
    return a+b
}

上面的代码,我们声明了一个add函数,接收两个参数a和b,告诉编辑器和调用add函数的人,“这俩参数都是number类型,别传错参数啊,对了,我这个函数的返回值也是一个number类型,别指望我给你返回其他东西”。

我们可以看出,和JS最大的区别就是,增加了类型。

类型可以用在哪里

我们上面的例子里,类型用来标注函数的参数的类型、函数的返回值类型。此外也可以用来表示一个变量的类型。
比如:

var a:number=1

上面的代码的意思是,声明里一个变量a,这个变量a的类型是number类型。由于typescript的类型推断功能,我们上面的代码也可以写成这样:

var a=1

ts编译器看到后面的数字1之后,会智能的推断的出a是一个number类型,是不是发现ts还是很聪明的嘛

image.png

都有哪些类型

常用类型

除了我们上面说的number类型,还有这几个常用类型:string boolean undefined null 。
当然了复杂类型也是有的,

var list:string[]=[] 

表示的是变量list是一个数组,这个数组里面每个元素都是string。

对象

function f1(a: { name: string; age: number }) {

}

上面的代码,表示参数a是一个对象,这个对象上有name和age两个属性,并且这两个属性的类型分别是string和number,那么当我们调用这个函数的时候,就要出符合条件的参数,不符合的话,ts就提示代码写的有问题了。
image.png

我们也可以用interface来声明一个对象结构,

interface af {
    name: string
    age: number
}


function f1(a:af) {

}

和Java的interface有点像但不太一样,我们把它理解成用来声明一个结构,这个结构长成这个样子:有属性xxx,对应的值是string类型,有属性yyy,对应的值的类型是number……

有个注意的点是,当我们传入的参数多了一些的属性的话,ts也会提示报错:
image.png
那么要解决这种情况,一种方式是改造我们的interface成这样子:

interface af {
    name: string
    age: number
    [attr:string]:any
}

多出来的[attr:string]:any意思是说我这个af上,还可能有一些其他的属性,对应的属性值的类型是any。
同样的,当我们往a上面赋值一些新的属性的时候,也会提示,
image.png
我们也要加[attr:string]:any

另外一种方法参看文章后面的类型断言。

函数类型

image.png
现在,我们这里有个参数callback,它是一个函数类型,那怎么声明呢:
image.png
其中void表示这个函数没有返回值。那通常,我们的回调函数会需要一些参数来接受数据:
image.png

当然我们也可以把这个callback的类型给提取出来,用interface声明一个函数类型(是的,你没看错……就是刚才用来声明对象的interface,目前你知道了它有两个作用了)

interface ccc{
    (data:string):void
}

function getInfo(id:number,callback:ccc){

}

注意写法有点稍微不一样,一个是=>void 一个是:void

字面量类型

image.png

image.png
上面,a3和a4就只能是固定的字面量"name"或者1了。

还有个any类型,顾名思义,表示任意类型,是其他类型的子类型。
image.png
你看,本来f2是要传一个number进来的,我们传了一个any类型进去也不报错,一般是,当我们不指明变量(常量、参数、返回值)类型,ts又推断不出来的时候,ts会推断成any类型。(这个需要到tsconfig.json里面配置)

类型之间运算

|表示或的意思:
image.png
比如这里,我们告诉编译器,u1这个变量可能是1也可能是2,也可能是3。
再比如:

var u2:"aaaa"|1|{a:string}

可能是三种完全不同的类型。
&表示合并两个类型:
image.png
这样,u1就有a和b加一块的属性了。

声明一个类型

使用type来声明一个类型,比如说,上面的一长串"aaaa"|1|{a:string}我们再很多地方都要用到,我们可以提取出来,起个名字供大家调用:

type uu="aaaa"|1|{a:string}

var u3:uu
...
var u4:uu

interface a {
  aaa: string
}

interface fc {
  (data: string): void
}

type uu2 = a & fc

类型断言

啥是类型断言呢,顾名思义,就是对类型的判断。比如我们上面举的一个例子:当传进来的参数不符合要求时,
image.png

使用as语句,告诉编译器/编辑器,“你就把我传进来的参数当成符合af接口的数据吧”,相当于我人肉判断了,我传的参数类型我知道符合条件。
再比如:声明变量的时候:
image.png

非空断言

有一种情况,如下,当我们在tsconfig.json里面开启了严格的null检查的时候,
image.png
这时候,当然我们 使用as string就可以搞定,其实还有个小技巧,使用!告诉ts这个变量不是null也不是undefined
image.png

类型保护

说白了就是当我们使用一个变量的时候,当访问到可能不存在的属性或方法的时候,强制要求做类型判断:

使用typeof

比如这个例子:
image.png
参数a可能是number或者string,默认情况下,只能调用number和string都有的方法。当调用number独有的方法比如说toFixed的时候就报错了。这时候你就应该判断一下,如果是number类型的再调用toFixed
image.png
上面的例子,你会发现,ts还是挺智能的,自动推断出else的情况就是string类型。

使用 instanceof

image.png
instanceof 差不多,主要用来判断是不是一个类的对象。

使用 in

你可能会机智的想到了,那假如类型是interface呢,js里面可没有这东西
image.png

ts也做了非常聪明的推断,就不详细说了。

泛型

有时候,我们的几个类型非常相似,只有一两个参数不一样,比如常在CMS开发中定义接口返回值类型的情况,如下面的UserResponse和RoleResponse

interface User{
    name: string
    age:number
}

interface Role{
    name: string
    id:string
}

interface UserResponse{
    ret: number
    message:string
    data:User
}

interface RoleResponse{
    ret: number
    message:string
    data:Role
}

这时候我们就在想能不能把公共的部分提前出来作为一个通用的类型,然后这个通用的类型接收不同的参数并生成新的类型?当然是可以的:

interface CommonResponse<T>{
    ret: number
    message: string
    data: T
}
type UserResponse = CommonResponse<User>
type RoleResponse=CommonResponse<Role>

你会发现泛型有点类似于函数参数。

泛型默认值

和函数参数一样,我们也可以给泛型设置个默认值,当不传的时候就使用默认值。

interface T1<T = any>{
    aaa: string
    t:T
}

多个泛型

也支持多个泛型,

interface T1<T ,K> {
    aaa: string
    t: T
    k:K
}

泛型约束 extends

有时候,我们想要约束一下传进来的泛型的类型:
使用extends来约束
image.png

泛型除了可以用在interface上,还可以用在type、class、function上:

type t1<T> = {
    aaa: T
}

type t2<T> = string | T

class C<T> {
    aaa: T
    constructor() {}
}


function get<T>(a:T) :T{
    return a
}

var g = get<string>("")

这里说一个特别常用的例子:Promise,
image.png
不加泛型的时候,ts不知道await get()的返回值是什么类型,加了泛型之后就知道了,并且能智能提示了:
image.png

tsconfig.json

本质上,我们写的ts代码最终会编译js代码,比如删除掉ts代码里面的类型接口等。那么这个编译行为有许多参数可以控制,最常用的方法是在项目根目录创建一个tsconfig.json的文件,ts编译代码的时候会根据里面的配置进行编译。

通过命令行执行tsc将ts代码编译成js代码。tsc -w监听文件变化自动编译代码。

tsc -init可以生成tsconfig.json文件。

查看原文

赞 13 收藏 8 评论 2

Midqiu 赞了文章 · 2019-11-05

我们为什么要做接口管理平台 YApi

前言

随着 Web 技术的发展,前后端分离构架变的越来越流行。前后端分离使后端专注于数据处理和定义前端所需要的接口,前端负责数据的展现和交互,大大细化了开发者的职责,提高了开发效率,但与此同时也带来了一些问题:

  • 对于前端工程师,后端提供的接口文档,大多是不规范的,有使用 wiki 的,有 word 文档的,甚至还有用即时聊天软件沟通的,后端接口对于前端就像一个黑盒子,经常遇到问题是接口因未知原因增加参数了,参数名变了,参数被删除了。对于后端工程师,接口对接时总是需要写冗杂繁琐的文档,需要大量时间去维护接口文档。
  • 前端开发的功能在后端功能还没完成前,因为前端的功能依赖于后端的数据,导致工作无法顺利展开。为了解决这个问题,有些前端工程师在代码注入 json,还有后端工程师临时搭建一套测试数据服务器,这种情况下势必会影响工作效率和代码质量,也不能及时进行字段的更新。
  • 接口数据正确性无法得到保证。前端调用后端的接口数据渲染到 视图,数据一旦出错,将会导致视图和交互也出现问题,保证后端接口数据正确性变的愈来愈重要。接口自动化测试就是用来解决这个问题,但传统的接口测试框架使用成本很高,很多团队采用肉眼比对方式,效率很低。

相关产品调研

我们迫切希望有一款产品能够满足我们的诉求,于是开始寻找市面上类似产品,经过一段时间的分析,最终我们找到了几个比较有代表性的产品 Rap,Nei,Easy-Mock。同时我们按照自己的诉求列出了一些关键的特征:

clipboard.png

Nei 是网易前端事业部的产品,在这些产品中算是做得比较好的, nei 是专注做 saas 服务这块,没有开源版本。对于去哪儿内部,肯定不会把公司机密的接口数据放到第三方平台。

Rap 是阿里妈妈 MUX 团队2013年出的一款产品,从时间上看是同类产品中最早的。Rap 是后端工程师基于 java 开发的,如果想定制部分功能,还需要学习 java,而我们部门大家对 java 都不熟悉。另一方面 Rap 没有接口测试功能,而后端使用其他工具(postman, restlet)测试接口,将导致不能及时更新接口文档。

Easy-mock 是大搜车无线团队出的一款产品,Easy-mock 定位是接口数据的模拟,解决前端依赖后端接口数据的问题,在同类产品中 mock 服务做得比较好。Easy-mock 专注于前端数据的模拟,但无法解决去哪儿现有的问题。

Nei,Rap 接口管理平台共同存在的问题是不易维护接口返回数据。笔者曾跟一个使用过 Rap 的后端工程师聊过,他说每次定义后端接口返回数据字段,好几个百个字段需要更新很长时间。Nei,Rap 是基于维护一个 json-schema 方式定义后端返回数据结构,我们假设某个接口有100个字段,如果基于 json-shema 那么就要维护差不多 600 多左右字段的更新。这么大工作量的,很可能导致后端工程师根本没有动力去维护。

比较遗憾的是,这几款优秀的产品,都缺失了一些我们在意的关键特征。我们可能需要做比较大的改动才能够基本满足自己的需求,这个工作量很有可能会超过重新开发一次。所以我们开始自主研发一个全新的接口管理平台,我们希望它能够提供接口文档管理,接口数据模拟(Mock),接口调试,自动化测试等功能,让前后端接口相关的工作进行的更加高效。这就是 YApi 接口管理平台斐然由来,下面简要聊聊 YApi 是如何实现上述这些特征的。

YApi 解决方案

1. 共同维护一份接口定义,连接前后端

大家看下图,在后端开发接口过程中,接口开发和测试接口这是必不可少的环节,但文档因为没有跟接口开发和测试联系到一起,被孤立。后端要维护对于他们冗杂繁琐的文档,是件收益很低的事情。没有人喜欢做收益低的事情,所以最终的解决办法就是要提高收益。下面详细说明解决方案。

在接口开发过程中,后端通常都会使用 postman 等类似的工具测试接口,而测试接口是在开发过程中一个必要的过程。假如参数有改动,大家肯定会在 postman 等工具上更新字段和测试接口。由此可以联想到,
如果能有一款工具既可用来做测试接口,又能作为接口文档工具,将接口文档和接口测试连接到一起,不就解决了此问题。YApi 解决方案是将接口文档和测试通过单一数据源连接到一起,如果有改动,因为改的是单一的数据源,就不会出现更新滞后和不及时问题。

2. 前端 Mock Server 方案

数据 Mock 服务在开发前期是非常头疼的一个问题。大多数情况下,接口请求参数和返回数据都是后端规定的,在后端接口没有完成之前,接口对于前端就是一个黑洞,可能最初对接口的定义跟实际后端做出的接口会有非常大的不同。这个时候就需要有一个工具,不仅能模拟真实接口的情况,还能关联接口文档,在后端开发过程中,可以随时调整接口定义,并通知给前端开发者改动信息。

在 YApi 平台,前后端只要维护接口定义的响应数据,就可以生成需要的模拟数据,下面这段代码定义了生成数据模板:

{
    "errcode": 0,
    "errmsg": "@string",
    "data": {
        "type":"@pick(1,2,3)",
        "list|1-10": [{
            "uid": "@id",
            "username": "@name"
        }]
    }
}

可生成如下的模拟数据:

{
  "errcode": 0,
  "errmsg": "^*!SF)R",
  "data": {
    "type": 2,
    "list": [
      {
        "uid": "370000200707276255",
        "username": "Ruth Clark"
      },
      {
        "uid": "650000200211185728",
        "username": "Anthony Martin"
      },
      {
        "uid": "370000199201143855",
        "username": "Laura Rodriguez"
      },
      {
        "uid": "610000198704072775",
        "username": "Anthony Perez"
      }
    ]
  }
}

以往的数据 mock 方案难免会影响项目源码,yapi 使用了服务器代理的方案,只需要在你的开发机做下服务器反向代理配置,不用修改项目一行源代码,即可获取到所有的 mock 数据。

基础的 Mock 工具已经能满足大部分的需求了,但有些复杂场景是无法实现的。例如:当我做一个数据列表页面,需要测试某个字段在各种长度下的 ui 表现,还有当数据为空时的 ui 表现。YApi 提供了期望和自定义脚本的功能。
本文主要介绍自定义脚本功能,期望功能可参考 yapi 平台文档。

自定义脚本可根据请求的参数,cookie 信息,使用 js 脚本自定义返回的数据。我们假设有个场景,我希望通过 cookie "_type" 控制列表页面数据显示,假设 _type 是 error,那么列表显示异常错误信息;假设 _type 是 empty ,列表显示为空。可使用下面代码实现:

if(cookie._type == 'error'){
    mockJson.errcode = 400;
}

if(cookie._type == 'empty'){
    mockJson.data.list = [];
}

3.自动化测试

接口开发完成后,后续的迭代是非常多的,每次对源码的修改,都需要大量的测试才能确保接口是否正确。人工判断肯定是不好的,最好的办法是做成自动化,但自动化测试又是一件成本非常高的事情,需要后端人员和QA人员学习相关的框架,和写大量的代码。YApi 简化了这一个过程,基于一个可视化界面,就算不懂程序开发,只需配置相关的参数和断言语句,就能实现自动化测试,非常的易用。

除了基本的功能外,YApi 还提供了强大的 pre-script 和可视化表达式功能,pre-script 包括请求参数处理脚本和响应数据处理脚本两部分。通过自定义 js 脚本方式改变请求的参数和返回的 response 数据。他的使用场景如下:

  • 接口请求参数需要加密及返回 response 解密
  • 接口请求参数需要添加计算 token

可视化表达主要是为了方便用户生成自动化测试所用到的参数,通过一个树形选择性,快速引用所依赖的参数值。
在所有的需要测试的接口配置完成后,点击开始测试,就会按照指定的顺序依次测试所有接口,测试完成后,可查看测试报告。

4.插件机制

YApi 最强大的一点莫过于他的插件机制,我们去哪儿各个业务线有不同的需求,通过 YApi 预留的钩子,开发不同的插件解决,比如我们现有的 qsso 登录,swagger 数据导入就是通过插件机制实现的,我们团队最近还在跟业务部门讨论使用插件实现压力测试功能等。总得来说,YApi基于插件机制,既满足了产品需求的多样性,又保证了内核足够易用和简洁。

5. 开源和易部署

为了帮助更多开发者和提升大家的工作效率,YApi 不仅开源到 github,还提供了一个 cli 工具方便广大开发者部署。使用 yapi-cli 提供的可视化部署方案,即便你不懂任何 nodejs、mongodb 的知识,也能轻松一键部署。

后记

YApi 已在去哪儿大面积使用,对 200+ 项目接口进行管理,每周有上万次 mock 请求。在开源以后,越来越多的公司和团队使用 YApi, github star 数已经上升到 1.1k了。YApi 在未来还将继续专注于接口管理方面的功能,让 YApi 成为各位开发者的好帮手。

访问地址

查看原文

赞 21 收藏 13 评论 7

Midqiu 赞了文章 · 2019-09-03

CDN工作原理

传统的网络访问形式为:

传统的网络访问形式

由上图可见,用户访问未使用CDN缓存网站的过程为:

  1. 用户向浏览器提供要访问的域名;
  2. 浏览器调用域名解析函数库对域名进行解析,以得到此域名对应的IP地址;
  3. 浏览器使用所得到的IP地址,域名的服务主机发出数据访问请求;
  4. 浏览器根据域名主机返回的数据显示网页的内容。

通过以上四个步骤,浏览器完成从用户处接收用户要访问的域名到从域名服务主机处获取数据的整个过程。CDN网络是在用户和服务器之间增加Cache层,如何将用户的请求引导到Cache上获得源服务器的数据,主要是通过接管DNS实现,下面让我们看看访问使用CDN缓存后的网站的过程:

CDN工作原理

CDN客户使用CDN的方法:
对于CDN客户来说,不需要改动网站架构,只需要修改自己的DNS解析,设置一个CNAME指向CDN服务商即可。原理在下面会解释

通过上图,我们可以了解到,使用了CDN缓存后的网站的访问过程变为:

  1. 用户向浏览器提供要访问的域名;
  2. 浏览器调用域名解析库对域名进行解析,由于CDN对域名解析过程进行了调整,所以解析函数库得到的是该域名对应的CNAME记录(由于现在已经是使用了CDN服务,CNAME为CDN服务商域名),为了得到实际IP地址,浏览器需要再次对获得的CNAME域名进行解析以得到实际的IP地址;在此过程中,使用的全局负载均衡DNS解析,如根据地理位置信息解析对应的IP地址,使得用户能就近访问。(CDN服务来提供最近的机器)
  3. 此次解析得到CDN缓存服务器的IP地址,浏览器在得到实际的IP地址以后,向缓存服务器发出访问请求;
  4. 缓存服务器根据浏览器提供的要访问的域名,通过Cache内部专用DNS解析得到此域名的实际IP地址,再由缓存服务器向此实际IP地址提交访问请求;
  5. 缓存服务器从实际IP地址得得到内容以后,一方面在本地进行保存,以备以后使用,二方面把获取的数据返回给客户端,完成数据服务过程;
  6. 客户端得到由缓存服务器返回的数据以后显示出来并完成整个浏览的数据请求过程。

概念解析:

CNAME(Canonical Name)指别名记录也被称为规范名字,CNAME可以理解为对域名设置别名。比如一个域名www.domain.com,设置一个CNAME指向它,由于www.domain.com与一个ip进行绑定,如果设置多个CNAME指向它,以后修改CNAME指向的服务器时,只需要修改一个www.domain.com对应的ip即可。详细可以参考dns一些名词解释,

查看原文

赞 28 收藏 57 评论 1

Midqiu 回答了问题 · 2019-08-29

typescript 如何定义全局变量?

创建一个UUU.d.ts文件(当然了,理论上名字可以随便起):

declare var  UUU:any

这样就创建了一个全局变量UUU。

clipboard.png

关注 3 回答 3

Midqiu 赞了文章 · 2019-05-21

React项目从Javascript到Typescript的迁移经验总结

抛转引用

现在越来越多的项目放弃了javascript,而选择拥抱了typescript,就比如我们熟知的ant-design就是其中之一。面对越来越火的typescript,我们公司今年也逐渐开始拥抱typescript。至于为什么要使用typescript?本文不做深入探讨,对这方面有兴趣的小伙伴们可以去看一下这篇文章:

TypeScript体系调研报告

这篇文章比较全面地介绍了TypeScript,并且和Javascript做了一个对比。看完上面这篇文章,你会对TypeScript有一个比较深入的认识,另外在TypeScript和Javascript的取舍上,可以拿捏得更好。

开始迁移

在开始迁移之前,我要说点题外话,本篇文章仅是记录我在迁移过程中遇到的问题以及我是如何解决的,并不会涉及typescript的教学。所以大家在阅读本篇文章之前,一定要对typescript有一个基础的认识,不然你读起来会非常费力。

环境调整

由于Typescript是Javascript的超集,它的很多语法浏览器是不能识别的,因此它不能直接运行在浏览器上,需要将其编译成JavaScript才能运行在浏览器上,这点跟ES6需要经过babel编译才能支持更多低版本的浏览器是一个道理。

tsconfig.json

首先我们得装一个typescript,这就跟我们在用babel前需要先装一个babel-core是一个道理。

yarn global add typescript 
yarn add typescript 

有些人会选择将typescript安装在全局环境上,但是我个人建议是装在项目目录下的,因为每个项目的typescript版本是不完全一样的,装在全局容易因为版本不同而出现问题。如果需要用tsc命令的话,可以借助npx去实现。接下来我们执行如下命令生成tsconfig.json,这玩意就跟.babelrc是一个性质的。

npx tsc --init

执行完之后,你的项目根目录下便会有一个tsconfig.json这么一个东西,但是里面会有很多注释,我们先不用管他的。

webpack

安装ts-loader用于处理ts和tsx文件,类似于babel-loader。

yarn add ts-loader -D

相应的webpack需要加上ts的loader规则:

module.exports = {
    //省略部分代码...
    module: {
        rules: [
            {
                test:/\.tsx?$/,
                loader:'ts-loader'
            }
            //省略部分代码...
        ]
    }
    //...省略部分代码
}

之前用javascript的时候,可能有人不使用.jsx文件,整个项目都是用的.js文件,webapck里面甚至都不配.jsx的规则。但是在typescript项目中想要全部使用.ts文件这就行不通了,会报错,所以当用到了jsx的用法的时候,还是得乖乖用.tsx文件,因此这里我加入了.tsx的规则。

删除babel

关于babel这块,网上有不少人是选择留着的,理由很简单,说是为了防止以后会使用到JavaScript,但是我个人觉得是没有必要留着babel。因为我们整个项目里面基本上只有使用第三方包的时候才会用到javascript,而这些第三方包基本上都是已经编译成了es5的代码了,不需要babel再去处理一下。而业务逻辑里面用javascript更是不太可能了,因为这便失去了使用typescript的意义。综上所述,我个人觉得是要删除babel相关的东西,降低项目复杂度。但是有一个例外情况:。

当你用了某些babel插件,而这些插件的功能恰巧是typescript无法提供的,那你可以保留babel,并且与typescript结合。

文件名调整

整个src目下所有的.js结尾的文件都要修改文件名,使用到jsx语法的就改成.tsx文件,未使用的就改成.ts文件,这块工作量比较大,会比较头疼。另外改完之后文件肯定会有很多标红的地方,不要急着去改它,后面我们分类统一去改。

解决报错

webpack入口文件找不到


由于我们在做文件名调整的时候,把main.js改成main.tsx,因此webpack的入口文件要改成main.tsx。

module.exports = {
    //省略部分代码...
    entry: {
        app: './src/main.tsx'
    },
    //省略部分代码...
}

提示不能使用jsx的语法


这个解决很简单,去tsconfig配置一下即可。

{
   "compilerOptions":{
        "jsx": "react"
   }
}

jsx这个配置项有三个值可选择,分别是"preserve","react-native"和"react"。在preservereact-native模式下生成代码中会保留JSX以供后续的转换操作使用(比如:Babel)。另外,preserve输出文件会带有.jsx扩展名,而react-native是.js拓展名。react模式会生成React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.js。

模式输入输出输出文件扩展名
preserve<div /><div />.jsx
react<div />React.createElement("div").js
react-native<div /><div />.js

webpack里面配置的alias无法解析

module.exports = {
    //省略部分代码...
    resolve: {
        alias:{
          '@':path.join(__dirname,'../src')
        }
        //省略部分代码...    
    },
    //省略部分代码...   
}


这里需要我们额外在tsconfig.json配置一下。

{
    "compilerOptions":{
        "baseUrl": ".",
        "paths": {
          "@/*":["./src/*"]
        } 
    }
}

具体如何配置,请看typescript的文档,我就不展开介绍了,但是要注意的是baseUrl和paths一定要配合使用。

https://www.tslang.cn/docs/ha...

无法自动添加拓展名而导致找不到对应的模块


原先我们在webpack里是这么配置的:

module.exports = {
    //省略部分代码... 
    resolve: {
        //省略部分代码... 
        extensions: ['.js', '.jsx', '.json']
    },
    //省略部分代码... 
}

但是我们项目里所有.js和.jsx的文件都改成了.ts和.tsx文件,因此配置需要调整。

{
    //省略部分代码... 
    resolve: {
        //省略部分代码... 
        extensions: ['.ts','.tsx','.js', '.jsx', '.json']
    },
    //省略部分代码... 
}

Could not find a declaration file for module '**'

这个比较简单,它提示找不到哪个模块的声明文件,你就装个哪个模块的就好了,安装格式如下:

yarn add @types/**

举个🌰,如果提示Could not find a declaration file for module 'react',那你应该执行如下命令:

yarn add @types/react

这个仅限于第三方包,如果是项目自己的模块提示缺少声明文件,那就需要你自己写对应的声明文件了。比如你在window这个全局对象上挂载了一个对象,如果需要使用它的话,就需要做一下声明,否则就会报错。至于具体怎么写,这得看typescript的文档,这里就不展开说明了。

https://www.tslang.cn/docs/ha...

Cannot find type definition file for '**'


这些并没有在我们的业务代码里直接用到,而是第三方包用到的,遇到这种情况,需要检查一下tsconfig.json中的typeRoots这个配置项有没有配置错误。一般来说是不用配置typeRoots,但是如果需要加入额外的声明文件路径,就需要对其进行修改。typeRoots是有一个默认值,有人会误以为这个默认值是“["node_modules"]”,因此会有人这样配置:

{
    "compilerOptions":{
        "typeRoots":["node_modules",...,"./src/types"]
    }
}

实际上typeRoots的默认值“["@types"]”,所有可见的"@types"包都会在编辑过程中被加载进来,比如“./node_modules/@types/”,“../node_modules/@types/”和“../../node_modules/@types/”等等都会被加载进来。所以遇到这种问题,你的配置应该改成:

{
    "compilerOptions":{
        "typeRoots":["@types",...,"./src/types"]
    }
}

在实际项目中,@types基本上存在于根目录下的node_modules下,因此这里你可以改成这样:

{
    "compilerOptions":{
        "typeRoots":["node_modules/@types",...,"./src/types"]
    }
}

不支持decorators(装饰器)


typescript默认是关闭实验性的ES装饰器,所以需要在tsconfig.json中开启。

{
    "compilerOptions":{
        "experimentalDecorators":true
    }
}

Module '**' has no default export


提示模块代码里没有“export
default”,而你却用“import from ”这种默认导入的形式。对于这个问题,我们需要把tsconfig.json配置项“allowSyntheticDefaultImports”设置为true。允许从没有设置默认导出的模块中默认导入。不过不必担心会对代码产生什么影响,这个仅仅为了类型检查。

{
    "compilerOptions":{
        "allowSyntheticDefaultImports":true
    }
}

当然你也可以使用“esModuleInterop”这个配置项,将其设置为true,根据“allowSyntheticDefaultImports”的默认值,如下:

module === "system" or --esModuleInterop

对于“esModuleInterop”这个配置项的作用主要有两点:

  • 提供__importStar和__importDefault两个helper来兼容babel生态
  • 开启allowSyntheticDefaultImports

对于“esModuleInterop”和“allowSyntheticDefaultImports”选用上,如果需要typescript结合babel,毫无疑问选“esModuleInterop”,否则的话,个人习惯选用“allowSyntheticDefaultImports”,比较喜欢需要啥用啥。当然“esModuleInterop”是最保险的选项,如果对此拿捏不准的话,那就乖乖地用“esModuleInterop”。

无法识别document和window这种全局对象


遇到这种情况,需要我们在tsconfig.json中lib这个配置项加入一个dom库,如下:

{
    "compilerOptions":{
        "lib":[
            "dom",
            ...,
            "esNext"
        ]
    }
}

文件中的标红问题

关于这个问题,我们需要分两种情况来考虑,第一种是.ts的文件,第二种是.tsx文件。下面来看一下具体是哪些注意的点(Ps:以下提到的注意的点并不能完全解决文件中标红的问题,但是可以解决大部分标红的问题):

第一种:.ts文件

这种文件在你的项目比较少,比较容易处理,根据实际情况去加一下类型限制,没有特别需要讲的。

第二种:.tsx文件

这种情况都是react组件了,而react组件又分为无状态组件和有状态组件组件,所以我们分开来看。

无状态组件

对于无状态组件,首先得限制他是一个FunctionComponent(函数组件),其次限制其props类型。举个🌰:

import React, { FunctionComponent, ReactElement } from 'react';
import {LoadingComponentProps} from 'react-loadable';
import './style.scss';

interface LoadingProps extends LoadingComponentProps{
  loading:boolean,
  children?:ReactElement
}

const Loading:FunctionComponent<LoadingProps> = ({loading=true,children})=>{
  return (
    loading?<div className="comp-loading">
      <div className="item-1"></div>
      <div className="item-2"></div>
      <div className="item-3"></div>
      <div className="item-4"></div>
      <div className="item-5"></div>
    </div>:children
  )  
}
export default Loading;

其中你要是觉得FunctionComponent这个名字比较长,你可以选择用类型别名“SFC”或者“FC”。

有状态组件

对于有状态组件,主要注意三点:

  1. props和state都要做类型限制
  2. state用readonly限制“this.state=**”的操作
  3. 对event对象做类型限制
import React,{MouseEvent} from "react";
interface TeachersProps{
  user:User
}
interface TeachersState{
  pageNo:number,
  pageSize:number,
  total:number,
  teacherList:{
    id: number,
    name: string,
    age: number,
    sex: number,
    tel: string,
    email: string
  }[]
}
export default class Teachers extends React.PureComponent<TeachersProps,TeachersState> {
    readonly state = {
        pageNo:1,
        pageSize:20,
        total:0,
        userList:[]
    }
    handleClick=(e:MouseEvent<HTMLDivElement>)=>{
        console.log(e.target);
    }
    //...省略部分代码
    render(){
        return <div onClick={this.handleClick}>点击我</div>
    }
}

实际项目里,组件的state可能会有很多值,如果按照我们上面这种方式去写会比较麻烦,所以可以考虑一下下面这个简便写法:

import React,{MouseEvent} from "react";
interface TeachersProps{
  user:User
}
const initialState = {
  pageNo:1,
  pageSize:20,
  total:0,
  teacherList:[]
}
type TeachersState = Readonly<typeof initialState>
export default class Teachers extends React.PureComponent<TeachersProps,TeachersState> {
    readonly state = initialState
    handleClick=(e:MouseEvent<HTMLDivElement>)=>{
        console.log(e.target);
    }
    //...省略部分代码
    render(){
        return <div onClick={this.handleClick}>点击我</div>
    }
}

这种写法会简便很多代码,但是类型限制效果上明显不如第一种,所以这种方法仅仅作为参考,可根据实际情况去选择。

Ant Design丢失样式文件

当我们把项目启动起来之后,某些同学的页面可能会出现样式丢失的情况,如下:

打开控制台,我们发现Ant Design的类名都找不到对应的样式:


出现这种情况是因为我们把babel删除之后,用来按需加载组件样式文件的babel插件babel-plugin-import也随着丢失了。不过typescript社区有一个babel-plugin-import的Typescript版本,叫做“ts-import-plugin”,我们先来安装一下:

yarn add ts-import-plugin -D

这个插件需要结合ts-loader使用,所以webpack配置中需要做如下调整:

const tsImportPluginFactory = require('ts-import-plugin')
module.exports = {
    //省略部分代码...
    module:{
        rules:[{
            test: /\.tsx?$/,
            loader: "ts-loader",
            options: {
                transpileOnly: true,//(可选)
                getCustomTransformers: () => ({
                  before: [
                    tsImportPluginFactory({
                        libraryDirectory: 'es',
                        libraryName: 'antd',
                        style: true
                    })
                  ]
                })
            }
        }]
    }
    //省略部分代码...
}

这里要注意一下transpileOnly: true这个配置,这是个可选配置,我建议是只有大项目中才加这个配置,小项目就没有必要了。由于typescript的语义检查器会在每次编译的时候检查所有文件,因此当项目很大的时候,编译时间会很长。解决这个问题的最简单的方法就是用transpileOnly: true这个配置去关闭typescript的语义检查,但是这样做的代价就是失去了类型检查以及声明文件的导出,所以除非在大项目中为了提升编译效率,否则不建议加这个配置。

配置完成之后,你的浏览器控制台可能会报出类似下面这个错误:

出现这个原因是因为你的typescript配置文件tsconfig.json中的module参数设置不对,两种情况会导致这个问题:

  • module设置成了“commonjs”
  • target设置"ES5"但是并未设置module(当target不为“ES6”时,module默认为“commonjs”)

解决这个办法就是把module设置为“esNext”便可解决这个问题。

{
    "compilerOptions":{
        "module":"esNext"
    }
}

可能会有小伙们说设置成“ES6”或者“ES2015”也是可以的,至于我为什么选择“esNext”而不是“ES6”或者“ES2015”,主要原因是设置成“ES6”或者“ES2015”之后,就不能动态导入了,因为项目使用了react-loadable这个包,要是设置成“ES6”或者“ES2015”的话,会报如下这个错误:

typescript提示我们需要设置成“commonjs”或者“ESNext”才可动态导入,所以保险起见,我是建议大家设置成ESNext。完成之后我们的页面就可以正常显示了。

说到module参数,这里要再多提一嘴说一下moduleResolution这个参数,它决定着typescript如何处理模块。当我们把module设置成“esNext”时,是可以不用管moduleResolution这个参数,但是大家项目里要是设置成“ES6”的话,那就要设置一下了。先看一下moduleResolution默认规则:

module === "AMD" or "System" or "ES6" ? "Classic" : "Node"

当我们module设置为“ES6”时,此时moduleResolution默认是“Classic”,而我们需要的是“Node”。为什么要选择“node”,主要是因为node的模块解析规则更符合我们要求,解析速度会更快,至于详情的介绍,可以参考Typescript的文档。

https://www.tslang.cn/docs/ha...

同样为了保险起见,我是建议大家强行将moduleResolution设置为“node”。

总结

以上就是我自己在迁移过程中遇到的问题,可能无法覆盖大家在迁移过程中所遇到的问题,如果出现我上面没有涉及的报错,欢迎大家在评论区告诉我,我会尽可能地完善这篇文章。最后再强调一下,本篇文章仅仅只是介绍了我个人在迁移至typescript的经验总结,并未完全覆盖tsconfig.json的所有配置项,文章未涉及到的配置项,还需大家多花点时间看看typescript的文档。最后附上我已迁移到typescript的项目的地址:

项目地址:https://github.com/ruichengpi...
查看原文

赞 65 收藏 41 评论 4

Midqiu 收藏了文章 · 2019-05-21

React项目从Javascript到Typescript的迁移经验总结

抛转引用

现在越来越多的项目放弃了javascript,而选择拥抱了typescript,就比如我们熟知的ant-design就是其中之一。面对越来越火的typescript,我们公司今年也逐渐开始拥抱typescript。至于为什么要使用typescript?本文不做深入探讨,对这方面有兴趣的小伙伴们可以去看一下这篇文章:

TypeScript体系调研报告

这篇文章比较全面地介绍了TypeScript,并且和Javascript做了一个对比。看完上面这篇文章,你会对TypeScript有一个比较深入的认识,另外在TypeScript和Javascript的取舍上,可以拿捏得更好。

开始迁移

在开始迁移之前,我要说点题外话,本篇文章仅是记录我在迁移过程中遇到的问题以及我是如何解决的,并不会涉及typescript的教学。所以大家在阅读本篇文章之前,一定要对typescript有一个基础的认识,不然你读起来会非常费力。

环境调整

由于Typescript是Javascript的超集,它的很多语法浏览器是不能识别的,因此它不能直接运行在浏览器上,需要将其编译成JavaScript才能运行在浏览器上,这点跟ES6需要经过babel编译才能支持更多低版本的浏览器是一个道理。

tsconfig.json

首先我们得装一个typescript,这就跟我们在用babel前需要先装一个babel-core是一个道理。

yarn global add typescript 
yarn add typescript 

有些人会选择将typescript安装在全局环境上,但是我个人建议是装在项目目录下的,因为每个项目的typescript版本是不完全一样的,装在全局容易因为版本不同而出现问题。如果需要用tsc命令的话,可以借助npx去实现。接下来我们执行如下命令生成tsconfig.json,这玩意就跟.babelrc是一个性质的。

npx tsc --init

执行完之后,你的项目根目录下便会有一个tsconfig.json这么一个东西,但是里面会有很多注释,我们先不用管他的。

webpack

安装ts-loader用于处理ts和tsx文件,类似于babel-loader。

yarn add ts-loader -D

相应的webpack需要加上ts的loader规则:

module.exports = {
    //省略部分代码...
    module: {
        rules: [
            {
                test:/\.tsx?$/,
                loader:'ts-loader'
            }
            //省略部分代码...
        ]
    }
    //...省略部分代码
}

之前用javascript的时候,可能有人不使用.jsx文件,整个项目都是用的.js文件,webapck里面甚至都不配.jsx的规则。但是在typescript项目中想要全部使用.ts文件这就行不通了,会报错,所以当用到了jsx的用法的时候,还是得乖乖用.tsx文件,因此这里我加入了.tsx的规则。

删除babel

关于babel这块,网上有不少人是选择留着的,理由很简单,说是为了防止以后会使用到JavaScript,但是我个人觉得是没有必要留着babel。因为我们整个项目里面基本上只有使用第三方包的时候才会用到javascript,而这些第三方包基本上都是已经编译成了es5的代码了,不需要babel再去处理一下。而业务逻辑里面用javascript更是不太可能了,因为这便失去了使用typescript的意义。综上所述,我个人觉得是要删除babel相关的东西,降低项目复杂度。但是有一个例外情况:。

当你用了某些babel插件,而这些插件的功能恰巧是typescript无法提供的,那你可以保留babel,并且与typescript结合。

文件名调整

整个src目下所有的.js结尾的文件都要修改文件名,使用到jsx语法的就改成.tsx文件,未使用的就改成.ts文件,这块工作量比较大,会比较头疼。另外改完之后文件肯定会有很多标红的地方,不要急着去改它,后面我们分类统一去改。

解决报错

webpack入口文件找不到


由于我们在做文件名调整的时候,把main.js改成main.tsx,因此webpack的入口文件要改成main.tsx。

module.exports = {
    //省略部分代码...
    entry: {
        app: './src/main.tsx'
    },
    //省略部分代码...
}

提示不能使用jsx的语法


这个解决很简单,去tsconfig配置一下即可。

{
   "compilerOptions":{
        "jsx": "react"
   }
}

jsx这个配置项有三个值可选择,分别是"preserve","react-native"和"react"。在preservereact-native模式下生成代码中会保留JSX以供后续的转换操作使用(比如:Babel)。另外,preserve输出文件会带有.jsx扩展名,而react-native是.js拓展名。react模式会生成React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.js。

模式输入输出输出文件扩展名
preserve<div /><div />.jsx
react<div />React.createElement("div").js
react-native<div /><div />.js

webpack里面配置的alias无法解析

module.exports = {
    //省略部分代码...
    resolve: {
        alias:{
          '@':path.join(__dirname,'../src')
        }
        //省略部分代码...    
    },
    //省略部分代码...   
}


这里需要我们额外在tsconfig.json配置一下。

{
    "compilerOptions":{
        "baseUrl": ".",
        "paths": {
          "@/*":["./src/*"]
        } 
    }
}

具体如何配置,请看typescript的文档,我就不展开介绍了,但是要注意的是baseUrl和paths一定要配合使用。

https://www.tslang.cn/docs/ha...

无法自动添加拓展名而导致找不到对应的模块


原先我们在webpack里是这么配置的:

module.exports = {
    //省略部分代码... 
    resolve: {
        //省略部分代码... 
        extensions: ['.js', '.jsx', '.json']
    },
    //省略部分代码... 
}

但是我们项目里所有.js和.jsx的文件都改成了.ts和.tsx文件,因此配置需要调整。

{
    //省略部分代码... 
    resolve: {
        //省略部分代码... 
        extensions: ['.ts','.tsx','.js', '.jsx', '.json']
    },
    //省略部分代码... 
}

Could not find a declaration file for module '**'

这个比较简单,它提示找不到哪个模块的声明文件,你就装个哪个模块的就好了,安装格式如下:

yarn add @types/**

举个🌰,如果提示Could not find a declaration file for module 'react',那你应该执行如下命令:

yarn add @types/react

这个仅限于第三方包,如果是项目自己的模块提示缺少声明文件,那就需要你自己写对应的声明文件了。比如你在window这个全局对象上挂载了一个对象,如果需要使用它的话,就需要做一下声明,否则就会报错。至于具体怎么写,这得看typescript的文档,这里就不展开说明了。

https://www.tslang.cn/docs/ha...

Cannot find type definition file for '**'


这些并没有在我们的业务代码里直接用到,而是第三方包用到的,遇到这种情况,需要检查一下tsconfig.json中的typeRoots这个配置项有没有配置错误。一般来说是不用配置typeRoots,但是如果需要加入额外的声明文件路径,就需要对其进行修改。typeRoots是有一个默认值,有人会误以为这个默认值是“["node_modules"]”,因此会有人这样配置:

{
    "compilerOptions":{
        "typeRoots":["node_modules",...,"./src/types"]
    }
}

实际上typeRoots的默认值“["@types"]”,所有可见的"@types"包都会在编辑过程中被加载进来,比如“./node_modules/@types/”,“../node_modules/@types/”和“../../node_modules/@types/”等等都会被加载进来。所以遇到这种问题,你的配置应该改成:

{
    "compilerOptions":{
        "typeRoots":["@types",...,"./src/types"]
    }
}

在实际项目中,@types基本上存在于根目录下的node_modules下,因此这里你可以改成这样:

{
    "compilerOptions":{
        "typeRoots":["node_modules/@types",...,"./src/types"]
    }
}

不支持decorators(装饰器)


typescript默认是关闭实验性的ES装饰器,所以需要在tsconfig.json中开启。

{
    "compilerOptions":{
        "experimentalDecorators":true
    }
}

Module '**' has no default export


提示模块代码里没有“export
default”,而你却用“import from ”这种默认导入的形式。对于这个问题,我们需要把tsconfig.json配置项“allowSyntheticDefaultImports”设置为true。允许从没有设置默认导出的模块中默认导入。不过不必担心会对代码产生什么影响,这个仅仅为了类型检查。

{
    "compilerOptions":{
        "allowSyntheticDefaultImports":true
    }
}

当然你也可以使用“esModuleInterop”这个配置项,将其设置为true,根据“allowSyntheticDefaultImports”的默认值,如下:

module === "system" or --esModuleInterop

对于“esModuleInterop”这个配置项的作用主要有两点:

  • 提供__importStar和__importDefault两个helper来兼容babel生态
  • 开启allowSyntheticDefaultImports

对于“esModuleInterop”和“allowSyntheticDefaultImports”选用上,如果需要typescript结合babel,毫无疑问选“esModuleInterop”,否则的话,个人习惯选用“allowSyntheticDefaultImports”,比较喜欢需要啥用啥。当然“esModuleInterop”是最保险的选项,如果对此拿捏不准的话,那就乖乖地用“esModuleInterop”。

无法识别document和window这种全局对象


遇到这种情况,需要我们在tsconfig.json中lib这个配置项加入一个dom库,如下:

{
    "compilerOptions":{
        "lib":[
            "dom",
            ...,
            "esNext"
        ]
    }
}

文件中的标红问题

关于这个问题,我们需要分两种情况来考虑,第一种是.ts的文件,第二种是.tsx文件。下面来看一下具体是哪些注意的点(Ps:以下提到的注意的点并不能完全解决文件中标红的问题,但是可以解决大部分标红的问题):

第一种:.ts文件

这种文件在你的项目比较少,比较容易处理,根据实际情况去加一下类型限制,没有特别需要讲的。

第二种:.tsx文件

这种情况都是react组件了,而react组件又分为无状态组件和有状态组件组件,所以我们分开来看。

无状态组件

对于无状态组件,首先得限制他是一个FunctionComponent(函数组件),其次限制其props类型。举个🌰:

import React, { FunctionComponent, ReactElement } from 'react';
import {LoadingComponentProps} from 'react-loadable';
import './style.scss';

interface LoadingProps extends LoadingComponentProps{
  loading:boolean,
  children?:ReactElement
}

const Loading:FunctionComponent<LoadingProps> = ({loading=true,children})=>{
  return (
    loading?<div className="comp-loading">
      <div className="item-1"></div>
      <div className="item-2"></div>
      <div className="item-3"></div>
      <div className="item-4"></div>
      <div className="item-5"></div>
    </div>:children
  )  
}
export default Loading;

其中你要是觉得FunctionComponent这个名字比较长,你可以选择用类型别名“SFC”或者“FC”。

有状态组件

对于有状态组件,主要注意三点:

  1. props和state都要做类型限制
  2. state用readonly限制“this.state=**”的操作
  3. 对event对象做类型限制
import React,{MouseEvent} from "react";
interface TeachersProps{
  user:User
}
interface TeachersState{
  pageNo:number,
  pageSize:number,
  total:number,
  teacherList:{
    id: number,
    name: string,
    age: number,
    sex: number,
    tel: string,
    email: string
  }[]
}
export default class Teachers extends React.PureComponent<TeachersProps,TeachersState> {
    readonly state = {
        pageNo:1,
        pageSize:20,
        total:0,
        userList:[]
    }
    handleClick=(e:MouseEvent<HTMLDivElement>)=>{
        console.log(e.target);
    }
    //...省略部分代码
    render(){
        return <div onClick={this.handleClick}>点击我</div>
    }
}

实际项目里,组件的state可能会有很多值,如果按照我们上面这种方式去写会比较麻烦,所以可以考虑一下下面这个简便写法:

import React,{MouseEvent} from "react";
interface TeachersProps{
  user:User
}
const initialState = {
  pageNo:1,
  pageSize:20,
  total:0,
  teacherList:[]
}
type TeachersState = Readonly<typeof initialState>
export default class Teachers extends React.PureComponent<TeachersProps,TeachersState> {
    readonly state = initialState
    handleClick=(e:MouseEvent<HTMLDivElement>)=>{
        console.log(e.target);
    }
    //...省略部分代码
    render(){
        return <div onClick={this.handleClick}>点击我</div>
    }
}

这种写法会简便很多代码,但是类型限制效果上明显不如第一种,所以这种方法仅仅作为参考,可根据实际情况去选择。

Ant Design丢失样式文件

当我们把项目启动起来之后,某些同学的页面可能会出现样式丢失的情况,如下:

打开控制台,我们发现Ant Design的类名都找不到对应的样式:


出现这种情况是因为我们把babel删除之后,用来按需加载组件样式文件的babel插件babel-plugin-import也随着丢失了。不过typescript社区有一个babel-plugin-import的Typescript版本,叫做“ts-import-plugin”,我们先来安装一下:

yarn add ts-import-plugin -D

这个插件需要结合ts-loader使用,所以webpack配置中需要做如下调整:

const tsImportPluginFactory = require('ts-import-plugin')
module.exports = {
    //省略部分代码...
    module:{
        rules:[{
            test: /\.tsx?$/,
            loader: "ts-loader",
            options: {
                transpileOnly: true,//(可选)
                getCustomTransformers: () => ({
                  before: [
                    tsImportPluginFactory({
                        libraryDirectory: 'es',
                        libraryName: 'antd',
                        style: true
                    })
                  ]
                })
            }
        }]
    }
    //省略部分代码...
}

这里要注意一下transpileOnly: true这个配置,这是个可选配置,我建议是只有大项目中才加这个配置,小项目就没有必要了。由于typescript的语义检查器会在每次编译的时候检查所有文件,因此当项目很大的时候,编译时间会很长。解决这个问题的最简单的方法就是用transpileOnly: true这个配置去关闭typescript的语义检查,但是这样做的代价就是失去了类型检查以及声明文件的导出,所以除非在大项目中为了提升编译效率,否则不建议加这个配置。

配置完成之后,你的浏览器控制台可能会报出类似下面这个错误:

出现这个原因是因为你的typescript配置文件tsconfig.json中的module参数设置不对,两种情况会导致这个问题:

  • module设置成了“commonjs”
  • target设置"ES5"但是并未设置module(当target不为“ES6”时,module默认为“commonjs”)

解决这个办法就是把module设置为“esNext”便可解决这个问题。

{
    "compilerOptions":{
        "module":"esNext"
    }
}

可能会有小伙们说设置成“ES6”或者“ES2015”也是可以的,至于我为什么选择“esNext”而不是“ES6”或者“ES2015”,主要原因是设置成“ES6”或者“ES2015”之后,就不能动态导入了,因为项目使用了react-loadable这个包,要是设置成“ES6”或者“ES2015”的话,会报如下这个错误:

typescript提示我们需要设置成“commonjs”或者“ESNext”才可动态导入,所以保险起见,我是建议大家设置成ESNext。完成之后我们的页面就可以正常显示了。

说到module参数,这里要再多提一嘴说一下moduleResolution这个参数,它决定着typescript如何处理模块。当我们把module设置成“esNext”时,是可以不用管moduleResolution这个参数,但是大家项目里要是设置成“ES6”的话,那就要设置一下了。先看一下moduleResolution默认规则:

module === "AMD" or "System" or "ES6" ? "Classic" : "Node"

当我们module设置为“ES6”时,此时moduleResolution默认是“Classic”,而我们需要的是“Node”。为什么要选择“node”,主要是因为node的模块解析规则更符合我们要求,解析速度会更快,至于详情的介绍,可以参考Typescript的文档。

https://www.tslang.cn/docs/ha...

同样为了保险起见,我是建议大家强行将moduleResolution设置为“node”。

总结

以上就是我自己在迁移过程中遇到的问题,可能无法覆盖大家在迁移过程中所遇到的问题,如果出现我上面没有涉及的报错,欢迎大家在评论区告诉我,我会尽可能地完善这篇文章。最后再强调一下,本篇文章仅仅只是介绍了我个人在迁移至typescript的经验总结,并未完全覆盖tsconfig.json的所有配置项,文章未涉及到的配置项,还需大家多花点时间看看typescript的文档。最后附上我已迁移到typescript的项目的地址:

项目地址:https://github.com/ruichengpi...
查看原文

认证与成就

  • 获得 318 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-07-28
个人主页被 1.5k 人浏览