老项目如何接入typescript

之前不是ts写的老项目,想接入ts,首先使用vue命令安装typescript

vue add typescript

下面依次对安装过程中出现的选项进行解释

E:\test>vue add typescript

📦  Installing @vue/cli-plugin-typescript...

+ @vue/cli-plugin-typescript@5.0.6
added 123 packages from 57 contributors and removed 11 packages in 15.57s

119 packages are looking for funding
  run `npm fund` for details

✔  Successfully installed plugin: @vue/cli-plugin-typescript

// 是否使用class类风格编码,默认是yes,这里直接回车使用默认
? Use class-style component syntax? Yes

// 是否同时使用ts和babel编译代码。这是因为ts编译后的代码是es6代码,如果需要最终代码编译成es5的js代码,需要使用bable。也就是选择是否将ts和bable编译器结合使用
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes

//将js文件转为ts文件,默认yes ,这里同样选默认
? Convert all .js files to .ts? Yes

//允许编辑js代码,默认no,这里选择yes,允许编译
? Allow .js files to be compiled? Yes

// 是否跳过所有类型文件的检查,默认yes,这里使用默认。不建议检查所有类型,因为很多类型文件是外部库提供的
? Skip type checking of all declaration files (recommended for apps)? Yes

最后这里回车过后就可以继续安装ts了。

安装过程中,可能出现如下错误:
ERROR Error: Cannot find module ‘@vue/cli-plugin-router/generator/template/src/views/HomeView.vue’ from ’ xxx 或者 ERROR Error: Cannot find module ‘@vue/cli-plugin-router/generator/template/src/views/HellowWord.vue’ from ’ xxx 类似的错误。
出现这种错误,是版本问题造成。


🚀  Invoking generator for @vue/cli-plugin-typescript...
 ERROR  Error: Cannot find module '@vue/cli-plugin-router/generator/template/src/views/HomeView.vue' from 'E:\test\node_modules\@vue\cli-plugin-typescript\generator\template\src\views'
Error: Cannot find module '@vue/cli-plugin-router/generator/template/src/views/HomeView.vue' from 'E:\lecent\lecent-web-pedestal\node_modules\@vue\cli-plugin-typescript\generator\template\src\views'
    at Function.resolveSync [as sync] (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\resolve\lib\sync.js:111:15)
    at renderFile (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\GeneratorAPI.js:518:17)
    at C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\GeneratorAPI.js:303:27
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at async Generator.resolveFiles (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\Generator.js:310:7)
    at async Generator.generate (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\Generator.js:204:5)
    at async runGenerator (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\invoke.js:111:3)
    at async invoke (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\invoke.js:92:3)

就是在 module依赖包@vue/cli-plugin-router 里面,找不到HomeView.vue,本测试项目的@vue/cli-plugin-router版本如图
在这里插入图片描述
按住ctrl键点击版本号,进入真实版本依赖文件,显示为4.5.18
在这里插入图片描述
找到该文件所在的module包,查看包结构,如下,可以看到确实没有HomeView.vue,但是有Home.vue
在这里插入图片描述
这里顺便说下快速找包小诀窍(用webstorm的情况下),在当前文件打开时点开左边 风扇样的按钮即可
在这里插入图片描述

这问题估计出现的比较少,至少百度很难查到(没试过google)。那我们可以建一个新项目,注意不选ts,然后当做老项目来试试。建新项目的步骤省略,建好后,在新项目根目录执行vue add typescript,发现运行畅通,然后我们查看新项目的@vue/cli-plugin-router 版本,是5.0.0以上,实际版本是5.0.6,打开module文件夹,确实是有HomeView.vue

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
现在我们知道了问题所在,那么只要仿造新创建的项目,将@vue/cli-plugin-router版本升级到5.0.0以上,也许就解决了。
直接将我们要改造的项目的package.json里的@vue/cli-plugin-router改成如下,在这里插入图片描述
然后 npm i 安装依赖,然后再运行 vue add typescript。顺利安装成功。
注意,如果之前运行过一次 vue add typescript,再重新运行时,会提示是否继续,默认否,这里需要选 是(y)
在这里插入图片描述

安装成功后,我们可能发现报了一大堆eslint的错,无妨。只要根据报错逐一改掉即可。

由于上面在安装的时候,我们在Convert all .js files to .ts? (Y/n) 这行选择了yes,所以安装完后,我们发现项目里面所有的js文件都被改成了s文件,仅仅改了文件后缀,里面的内容都得我们自己去改造,但这可能不是我们想要的,因为老项目改造可能牵扯过多,不宜一下子全部改造。因此我们可以在Convert all .js files to .ts? (Y/n)这里选择no。
安装后,main.js会改变成main.ts,
在这里插入图片描述
另外,会在views文件夹里面多个HomeView.vue,在components文件夹里面多个HelloWord.vue,删掉即可。

对于老项目,一下子改不完,我们需要允许js和TS同时存在,需要在tsconfig.json配置允许引入和编译js。

  "resolveJsonModule": true,
    "allowJs": true,

在这里插入图片描述
加上两个配置后,你会发现在main.ts里面引入的js模块文件,之前报的类似错误
TS7016: Could not find a declaration file for module ‘./router’. ‘E:/lecent/lecent-web-pedestal/src/router/index.js’ implicitly has an ‘any’ type.
就消失了

如何声明js模块

shims-vue.d.ts

shims-vue.d.ts 是模块声明文件,如果我们依赖的第三方包js写的,那就需要在这里声明模块,否则会报错。
js模块的声明示例

// vue 的模块,默认就有
declare module "*.vue" {
  import Vue from "vue"
  export default Vue
}
// 其他的根据需要自己加
declare module 'vue-cropper'
declare module "vue-clipboard2"
declare module "lodash/debounce"
declare module "file-saver"
declare module "*.less"
declare module "ant-design-vue" {
  const Ant: any
  export default Ant
}

在 ts 里面,.d.ts 文件声明的模块是全局的,因此不需要使用 declare global,在.d.ts里面使用declare.global也不起作用,如果在其他模块文件里面声明变量,想声明成全局变量,就要使用declare global,否则声明的变量就具有模块作用域,其他模块无法使用

下面我们先让项目跑起来:运行 npm run serve

结果发现有如下报错:
Syntax Error: TypeError: loaderContext.getOptions is not a function
@ multi (webpack)-dev-server/client?http://192.168.1.8:7590&sockPath=/sockjs-node (webpack)/hot/dev-server.js ./src/main.ts

关于这错误,开始比较蒙蔽,估计也是版本问题,但是却不知道是哪个包的问题。百度了一下,查出来的都说了less less-loader问题,但其实他们的问题不一样,没有loaderContext。
然后尝试将未安装ts的版本跑起来,却没问题。这说明这个问题很可能是ts插件的问题
无奈继续百度,终于看到有篇文章 https://blog.csdn.net/weixin_41610178/article/details/123292109 有类似问题,确实就是@vue/cli-plugin-typescript 插件版本的问题。按照文章版本改成4.5.15后,问题解决。

import 结构引入

现在我们在main.ts里面写一行代码

import { message } from "ant-design-vue"

首先因为 vue2 版本的ant-design-vue是js模块
这时候会报错,TS2614: Module ‘“ant-design-vue”’ has no exported member ‘message’. Did you mean to use ‘import message from “ant-design-vue”’ instead?
也就是 ant-design-vue 里面找不到 message ,这时候我们只需要对模块声明进行改写,添加 export const message:any 如下

declare module "ant-design-vue" {
  const Ant: any
  export const message:any
  export default Ant
}

vue自带挂载到vue 原型上的模块数据声明

这里强调vue自带的,因为不是所有的挂载到vue原型链上的都出自vue本身,例如ant的this. m e s s a g e , 就 不 是 v u e 本 身 的 。 这 里 主 要 是 t h i s . message,就不是vue本身的。这里主要是this. messagevuethis.route,this. r o u t e r , t h i s . router,this. router,this.store等。另外,this.$refs不适用。
示例如下

import VueRouter, { Route } from "vue-router"
import Store from "store"
declare module 'vue/types/vue' {
  interface Vue {
    $route: Route
    $router: VueRouter
    $store: Store 
  }
}

需要单独建.d.ts文件的

有些模块需要单独建.d.ts模块(具体原因待研究),比如video.js
我们直接在 shims-vue.d.ts中如下声明 declare module “video.js”,会发现一直报找不到,没有生效。但是我们建一个新的文件,video.d.ts,写上declare module “video.js”,ok,这样没问题。
关于d.ts文件的作用。这里还是不了解其机制,需要进一步学习了解。有知道的大神麻烦告知一下,谢谢。

全局变量声明

比如windows对象使用。当我们尝试在代码中使用window.xxx时,会报类似错误:TS2339: Property ‘xxx’ does not exist on type ‘Window & typeof globalThis’.
window全局变量和ant-design-vue不同,我们不能用声明模块方式,但是可以用声明接口方式。那么在shims-vue.d.ts中,增加window的声明

interface Window {
 xxx:string,
}
declare let window: Window

在这里插入图片描述
这时候不报错了,而且还有提示(这里编辑器用webstorm)。如果有多个,我们都加上就行,比如

interface Window {
  xxx:string,
  // eslint-disable-next-line camelcase
  umi_plugin_ant_themeVar:any,
  showMessage:any
}

如果我们想让所有用window.xxx的都不报错,那就改写成如下形式

interface AnyWindow {
  [k: string]: any
}
declare let window: AnyWindow 

但是这样就没有提示了
如果我们想让写在Window类型里面的有提示,其他不知道的也不报错
使用interface,写在一起就好

interface Window {
  [k: string]: any,
  xxx:any,
  // eslint-disable-next-line camelcase
  umi_plugin_ant_themeVar:any,
  showMessage:any,
}

当然我们也可以使用 interface的声明合并

interface Window {
  [k: string]: any,
  xxx:any,
  // eslint-disable-next-line camelcase
  umi_plugin_ant_themeVar:any,
  showMessage:any,
}
interface Window {
  [k: string]: any,
}
declare let window: Window 

值得一提的是这里不能使用type,type不具有继承性

vue文件中使用ts

vue2 使用ts有两种形式,一是使用vue.extend,一是使用vue-class-component。vue.extend写法和js写法比较接近,这里先不做讨论。重点讲下vue-class-component的使用。

装饰器使用

首先,使用vue-class-component ,依赖与vue-property-decorator 提供的装饰器。该包提供的装饰器有 @Prop 、@PropSync、@Model、@ModelSync、@Watch、@Provide、@Inject、@ProvideReactive、@InjectReactive、@Emit、@Ref、@VModel
这里讲下实践过的几个:@Prop 、@Watch、@Provide、@Inject、@Ref、@Component
统一的引用方式:

import { Prop, Component, Vue, Watch, Provide, Inject , Ref} from "vue-property-decorator"
@Component

这个基本时vue组件的必选,否则编译报错。就算我们什么操作都不做,也要使用

import {  Component, Vue,} from "vue-property-decorator"
@Component
export default class App extends Vue {
}

如果我们这个页面有子组件和使用了vuex

import {  Component, Vue,} from "vue-property-decorator"
import {mapState} from 'vuex'
import Child from "../components/home/child"
@Component({
	components:{
		Child
	},
	 computed:{
   	 ...mapState(["userInfo"])
  }
})
export default class App extends Vue {
}

也就是这里面其实可以实现部分和不使用ts相同的功能。

@Prop

父子组件传递数据必备,用于非ts时和props一样的功能

<template>
  <div class="hello">
    prop测试
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"

@Component
export default class HelloWorld extends Vue {
  @Prop() readonly height?:number|string // 高度
  @Prop({ type: Boolean, default: true }) readonly isEdit?:boolean // 是否是编辑状态
  @Prop({ type: Object, default: () => { return {} } }) readonly spanStyle?:object // 查看状态下样式
  @Prop() readonly value:string | number | any[] | undefined// 值
  @Prop() readonly mode?:string // 多选单选
  @Prop({ default: "100%" }) readonly width?:string // 宽度
  @Prop() readonly placeholder?:string //  占位符
  @Prop() readonly disabled?:boolean //  是否可见
  @Prop() readonly isUp?:boolean //  是否需要展示无上级
  @Prop() readonly string?:boolean //  使用字符串
}
</script>

@Prop函数里面,可以传递和平时使用prop时候一样的数据教育,比如

@Prop({ type: Boolean, default: true }) readonly isEdit?:boolean // 是否是编辑状态
 @Prop({ type: Object, default: () => { return {} } }) readonly spanStyle?:object // 查看状态下样式

至于 函数后面,就是ts相关的数据定义和类型判断了
另外如果是布尔值的,一定要在Prop函数里面写上{type:Boolean},否则接收到的默认值就是undefind

@Watch

使用watch监听需要watch监视器,示例如下

  @Watch("height")
  onHeightChange(v: string | number):void {
    console.log("heightChange", v)
  }

这里需要说明的是,@Watch装饰器对紧挨着自己后面的函数起作用,函数名可以任意例如:

  @Watch("height")
  abc(v: string | number):void {
    console.log("heightChange", v)
  }

如果紧挨着@watch装饰器下面的不是函数,或者是生命周期函数,就会报错,例如如下代码会报错:[Vue warn]: Error in callback for watcher “height”: “TypeError: handler.apply is not a function”

  @Watch("height")


  text = "ddd"
  abc(v: string | number):void {
    console.log("heightChange", v)
  }

@Watch装饰器第二个参数是watch的相关配置,如下:

  @Watch("height", { immediate: true, deep: true })
  onHeightChange(v: string | number):void {
    console.log("heightChange", v)
  }
@Provide、@Inject

parent.vue

<template>
  <child/>
</template>

<script lang='ts'>
import Child from "./child.vue"
import { Vue, Component, Prop, Provide } from "vue-property-decorator"

@Component({
  components: {
    Child
  }
})

export default class Parent extends Vue {
  @Provide() testProvide="testProvide"
  @Provide() testProvide2="testProvide2"
}
</script>

child.vue

<template>
  <div>
    <p>{{ testProvide }}</p>
    <p>{{ testProvide2 }}</p>
  </div>
</template>

<script lang='ts'>
import { Vue, Component, Prop, Inject } from "vue-property-decorator"

@Component

export default class Child extends Vue {
  @Inject() readonly testProvide!: string
  @Inject() readonly testProvide2!: string
  mounted() {
    console.log("this.testProvide", this.testProvide)
  }
}
</script>
@Ref

使用在vue2+ts里面,想实现this. r e f s 调 用 时 , 有 两 种 方 式 , 一 是 将 t h i s 设 为 a n y , 如 ( t h i s a s a n y ) . refs调用时,有两种方式,一是将this设为any,如(this as any). refsthisany,(thisasany).refs.footer.close(),第二种就是使用@Ref装饰器,写法为:
@Ref() readonly refName!: RefComponent
其中 RefComponent 代表组件类型,如果是自定义组件,可以直接使用组件,如果是原生标签,例如span,为HTMLSpanElement,div 为HTMLDivElement,其他类似。自定义组件时RefComponent也可以用一个对象,里面写上想调用的方法。另外如果ref名和我们声明的ref变量(refName)不一致,可以在Ref()函数中指明。现在假设我们有三个组件,parent.vue,HelloWord.vue,并且在HelloWord.vue中有两个方法,show和show2示例如下


<template>
 <div>
   <hello-world is-edit ref='helloWord'/>
   <parent ref="parent"/>
   <span ref="customRefKey"></span>
 </div>
</template>

<script lang="ts">
import { Component, Vue, Ref } from "vue-property-decorator"
import HelloWorld from "@/components/HelloWorld.vue"
import Parent from "@/components/parent.vue"
@Component({
 components: {
   HelloWorld,
   Parent
 },
})
export default class App extends Vue {
 @Ref() readonly parent!: Parent
 @Ref("customRefKey") readonly spanEl!: HTMLSpanElement
 @Ref() readonly helloWord!: { show: (arg0: any) => void }
 mounted() {
   console.log("this.parent", this.parent)
   console.log("this.spanEl", this.spanEl)
   console.log("this.helloWord", this.helloWord)
   this.helloWord.show(110)
   this.helloWord.show2(120)
 }
}
</script>

调用后我们发现,this.helloWord.show2会报错,TS2551: Property ‘show2’ does not exist on type ‘{ show: (arg0: any) => void; }’. Did you mean ‘show’,这是因为我们声明ref时只单独指明show函数,如果改成如下,不指明函数,就不会报错

@Ref() readonly helloWord!: HelloWorld

另外,声明spanEl时,我们用了和变量spanEl不一样的ref值,customRefKey,如果我们将customRefKey改成customRefKey2,那将拿不到span节点。

@Ref("customRefKey2") readonly spanEl!: HTMLSpanElement

然后就是调用,我们可以直接通过如this.helloWord访问组件里面的数据方法,可以直接通过如this.spanEl拿到dom节点

data 的使用与替代

在vue2+ts里面,可以像使用js时声明data,html里面也可以直接使用,不同的是,我们在生命周期、方法里面,不能直接通过this读取数据,需要使用(this as any)来调用

  data() {
    return {
      test: "xxx"
    }
  }
   mounted() {
    console.log((this as any).test)
  }
}
data的替代写法

由于我们使用class类书写方式,data完全可以如下替代,而且可以很方便的用this访问

  test:string = "xxx"
  count:number = 0
  list:Array<string> = []
  mounted() {
    console.log(this.test)
    console.log(this.count)
    console.log(this.list)
  }

methods

不需要methods对象了,直接写方法

<template>
  <div class="hello">
    测试
  </div>
</template>

<script lang="ts">
import { Component,, Vue,  } from "vue-property-decorator"

@Component
export default class HelloWorld extends Vue {
  mounted() {
    this.$emit("init", "init")
  }

/**
*显示1函数
**/
  show(index:number) {
    console.log(index)
  }
  /**
*显示2函数
**/
  show2(index:number) {
    console.log(index)
  }
}
</script>

mixins 混入

使用class模式,混入可以用Mixins处理。
如下,我们新建mixins-test.ts

import { Vue, Component, Prop } from "vue-property-decorator"

@Component

export default class MixinsTest extends Vue {
  @Prop({ default: "1000px" }) readonly mixinHeight?:number|string // 混入高度

  mixinText = "mixinText" // 测试文字
  mixinMethodValue = ""

  /**
   * 混入测试方法
   */
  mixinMethod() {
    console.log("mixinMethod执行")
    this.mixinMethodValue = "mixinMethod执行 后 mixinMethodValue"
  }
}


然后在 HelloWord.vue里面使用Mixins

<template>
  <div style="padding:40px;">
    <p>混入高度测试:{{mixinHeight}}</p>
    <p>混入文字测试:{{mixinText}}</p>
    <p>混入方法调用测试:{{mixinMethodValue}}</p>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Mixins } from "vue-property-decorator"
import MixinsTest from "@/mixins/mixins-test"

@Component
export default class HelloWorld extends Mixins(MixinsTest) {
  mounted() {
    this.mixinMethod()
  }
}
</script>



运行结果
在这里插入图片描述

假设我们需要在组件里面覆盖mixins-test.ts的内容,将HelloWord.vue修改如下

<template>
  <div style="padding:40px;">
    <p>混入高度测试:{{mixinHeight}}</p>
    <p>混入文字测试:{{mixinText}}</p>
    <p>混入方法调用测试:{{mixinMethodValue}}</p>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Mixins } from "vue-property-decorator"
import MixinsTest from "@/mixins/mixins-test"

@Component
export default class HelloWorld extends Mixins(MixinsTest) {
  @Prop({ default: 200 }) readonly mixinHeight?:number|string // 混入高度

  mixinText = "helloWordText" // 测试文字
  mixinMethodValue = ""

  /**
   * 混入测试方法
   */
  mixinMethod() {
    console.log("helloWordMethod执行")
    this.mixinMethodValue = "mixinMethod执行 后 helloWordValue"
  }
  mounted() {
    this.mixinMethod()
  }
}
</script>

这时候发现HelloWord.vue 里 prop 的 mixinHeight报错
TS2612: Property ‘mixinHeight’ will overwrite the base property in 'MixinsTest & Vue & object & Record '. If this is intentional, add an initializer. Otherwise, add a ‘declare’ modifier or remove the redundant declaration.
报错为mixinHeight将被改写,需要使用declare声明或者删除。这里我们加上declare

@Prop({ default: 200 }) declare readonly mixinHeight?:number|string // 混入高度

运行结果
在这里插入图片描述
结果已经被改写

除了使用Mixin实现混入效果,我们也可以直接使用继承,例如

<template>
  <div style="padding:40px;">
    <p>混入高度测试:{{mixinHeight}}</p>
    <p>混入文字测试:{{mixinText}}</p>
    <p>混入方法调用测试:{{mixinMethodValue}}</p>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Mixins } from "vue-property-decorator"
import MixinsTest from "@/mixins/mixins-test"

@Component
export default class HelloWorld extends MixinsTest {
  mounted() {
    this.mixinMethod()
  }
}
</script>

运行效果一致。当然,如果同时有多个混入,那还是用Mixins,使用为Mixins(MixinA,MixinB,…)

全局组件注册,name的坑

ts组件在生产环境下找不到name:全局注册多个组件时,我们为了方便,都会使用类似如下方式注册

for(let i=0;i<components.length;i++){
	Vue.use(components[i].name)
}

如果没有使用ts class模式,这么写,完全没问题
使用ts class 模式,开发环境下,这么写,也没问题。但是当我们打包到生产环境后,就会发现一个问题,组件注册没成功。控制台报注解未注册错误。这时候如果点开页面html,我们会发现组件没有被编译成原生标签,页面上完整的显示了我们定义的组件标签名。这时候如果我们在注册组件时打印组件name,开发环境下是正常的的,生产环境下,name被编译成a、b、c类似的变量。
使用ts编写的组件,在生产环境下,找不到name,需要使用 component.option.name来注册。

封装组件库并发布到npm使用tsx的坑

在使用比如基于ant-design-vue封装组件时,若不使用ts,比如我们在组件中使用了a-button标签,那么只要项目中引用了a-button,组件就可以正常使用。

如果组件使用tsx,则必须在组件库中引入a-button,否则报找不到a-button组件

Logo

前往低代码交流专区

更多推荐