美文网首页
Vue2+typescript写法总结

Vue2+typescript写法总结

作者: 东扯葫芦西扯瓜 | 来源:发表于2022-07-08 21:33 被阅读0次

    老项目如何接入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版本如图


    image.png

    按住ctrl键点击版本号,进入真实版本依赖文件,显示为4.5.18


    image.png

    找到该文件所在的module包,查看包结构,如下,可以看到确实没有HomeView.vue,但是有Home.vue


    image.png

    这里顺便说下快速找包小诀窍(用webstorm的情况下),在当前文件打开时点开左边 风扇样的按钮即可


    image.png

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

    image.png image.png image.png

    现在我们知道了问题所在,那么只要仿造新创建的项目,将@vue/cli-plugin-router版本升级到5.0.0以上,也许就解决了。

    直接将我们要改造的项目的package.json里的@vue/cli-plugin-router改成如下, image.png

    然后 npm i 安装依赖,然后再运行 vue add typescript。顺利安装成功。
    注意,如果之前运行过一次 vue add typescript,再重新运行时,会提示是否继续,默认否,这里需要选 是(y)


    image.png

    安装成功后,我们可能发现报了一大堆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,


    image.png

    另外,会在views文件夹里面多个HomeView.vue,在components文件夹里面多个HelloWord.vue,删掉即可。

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

      "resolveJsonModule": true,
        "allowJs": true,
    
    image.png

    加上两个配置后,你会发现在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.message,就不是vue本身的。这里主要是this.route,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
    
    image.png

    这时候不报错了,而且还有提示(这里编辑器用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.refs调用时,有两种方式,一是将this设为any,如(this as any).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>
    
    
    
    

    运行结果


    image.png

    假设我们需要在组件里面覆盖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 // 混入高度
    

    运行结果


    image.png

    结果已经被改写

    除了使用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组件

    相关文章

      网友评论

          本文标题:Vue2+typescript写法总结

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