• home > webfront > ECMAS > vue >

    vue2.x老项目typescript改造过程经验总结

    Author:zhoulujun Date:

    关于Vue2 x 的TS改造的解决方案对比。项目实施过程中需要注意哪些事项。做过一个笔记,给可能有用的童鞋做参考。

    前言:

    关于Vue2.x 的TS改造,其实没有啥好说的。

    对于vue-cli项目来说,从新跑一遍 vue create xxx-project ,选择Manually select features ,重新选择上typescript 选项即可。或者直接vue add typescript也可。

    网上太多的资料,这里也推荐一些我觉得还可的(我是自己搞的,个人感觉不难吧,哈哈)

    对于webpack,就是增加一下ts-loader,然后增加tsconfig.json,配置ts属性,再在eslint增加 ts代码规范。然后就去把老的项目文件改为ts文件,就好了。就这么一句话而已^_^,毕竟如今都2021了(毕竟TS已经流行多年了),教程实在太多了。

    项目配置注意事项:

    本篇讲的是需要的一些注意事项,以及一些实现方式的异同,以及本人的观点(不妥之前请留言,多谢)

    WebPack配置修改

    • 增加TS后缀:extensions: ['.js', '.vue', '.json', '.ts'], 

    • 增加ts-loader: {test: /\.ts$/, loader: 'ts-loader', exclude: /node_modules/, options: {appendTsSuffixTo: [/\.vue$/],}}

    TypeScript配置文件

    项目根目录创建tsconfig.json文件,需要注意下面几个配置:

    • "strictPropertyInitialization": false, // strict默认为true——必须要确保每个实例的属性都会初始值

    • "noImplicitAny": false, // false表示运行隐式的any类型,也就是允许不设置任何类型, 这个设置运行js文件直接改成ts文件

    •  "allowJs": true, // 初期改造,肯定是JS与TS并行跑

    • "strictFunctionTypes": false, // 启用 vuex-class 需要开启此选项

    • "target": "es5", // 编译输出目标 ES 版本

    • "skipLibCheck": true,

    •  "jsx": "preserve", // 在.tsx文件里支持JSX

    • "experimentalDecorators": true,  // 启用装饰器

    •  "strictNullChecks": true, // 当你声明一个变量时,它不会自动地包含null或undefined。

    其他的,按照官方的来就可。

    想noImplicitAny 就是比较鸡贼的玩法,但是你一个老项目的改造,可以边改变调整。不然,改着改着,就会失去重构信心。

    eslint解析规则增加TS配置项

    根目录下,.eslintrc.js,参考配置

    extends: [
      'plugin:vue/recommended',
      'eslint:recommended',
      '@vue/typescript/recommended',
      '@vue/prettier',
      '@vue/prettier/@typescript-eslint'
    ],
    parserOptions: {
      ecmaVersion: 2020,
    },

    其实这个配置,看大家随意。默认vue-cli 生成的文件就好,没有vue-cli生成一个demo项目,copy一份。我们当然得遵从鹅厂的内部代码规范,就不贴了。

    TypeScript的声明文件

    官方文档:https://www.tslang.cn/docs/handbook/declaration-files/introduction.html

    import Vue, { VNode } from 'vue';
    declare global {
      interface Window { // 全局变量
        i18n: any;
        eCharts: any;
      }
    }
    declare module 'vue/types/vue' {
      interface Vue {
        $bus: Vue;
        $route: any;
        $router: any;
        CancelToken: any;
        $message: any;
        $confirm: any;
      }
    }
    declare global {
      namespace JSX {
        interface Element extends VNode {}
        interface ElementClass extends Vue {}
        interface IntrinsicElements { [elem: string]: any; }
      }
    }

    项目改造到这里就基本结束了

    TS一些注意事项

    这部分对于刚刚改造,需要提醒成员的事项

    TS类型

    any

    any,这个东西好用,但是,如果完全放开的话,相信我,带最最后可能基本都是any

    但是项目改造初期,可以先用any 顶替,后面有有时间,在进一步细化。这个度量,其实不是很好衡量。对于新手,代码合并的时候,还是打回any。

    可选属性vs null undefined

    null 和 undefined 是 ts 中的基础类型,分别具有值 null 和 undefined,默认情况下它们是所有类型的子类型,即可以赋值给任意类型

    null与undefined是所有其它类型的一个有效值。 这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。

    null的发明者,Tony Hoare,称它为价值亿万美金的错误。

    tsconfig.js 文件中设置 strictNullChecks 为 true 时,就不能将 null 和 undefined 赋值给除它们自身和 void 之外的任意类型了。

    在这种严格检查的情况下,如果你确实在某个地方想要给一个其他类型的值设置初始值为空,然后再赋值,可以使用联合类型来实现。

    let test: string | null = 'hi'

    null 和 undefined 是区别的

    string|undefined、string|null 和 string|undefined|null 是三种不同的类型

    如果设置了 "strictNullChecks": true,可选参数会被自动加上 |undefined

    let test?: string = 'hi'

    interface/class/abstract class/type

    Declaration TypeNamespaceTypeValue
    NamespaceX
    X
    Class
    XX
    Enum
    XX
    Interface
    X
    Type Alias

    X
    Function

    X
    Variable

    X

    不算symbol,js中有6种基本类型,number,string,boolean,null, undefined, object。但是只依靠这几种类型,来描述某个函数需要传什么样的参数,是远远不够的,这也是interface的使命--描述一个值(value)的形状(type)。

    class首先也具有interface的能力,描述一个形状,或者说代表一种类型。此外class还提供了实现,也就是说可以被实例化;

    interface可以extends class,此时的class承担类型的角色。这里对于之前写java的我来说,有点WTF。其实对于undefined 有自己的类型叫做 undefined,java程序员也表示懵逼。

    TypeScript 通过采用结构化类型系统来体现 JavaScript 的动态特性,并且在类型推断方面做得非常出色,这意味着你不必像 C#或 Java 那样明确表达类型。

    TypeScript 的设计目标之一不是为了创建一个“正确的类型系统”,而是“在正确性和生产力之间取得平衡”。——TypeScript 编译器不会强制你声明类型,类型安全的程度由你自己来决定。你甚至可以决定在项目的不同区域应用不同级别的类型安全严格程度。这种灵活性不是传统的静态类型语言可以提供的。

    这里不像讲太多,觉得typescript手册就非常详细:https://www.tslang.cn/docs/handbook/basic-types.html

    Vue升级方案对比

    vue2升级到TS改造方案有很多种。

    传统方案:vue-property-decorator

    vue2对ts的支持主要是通过vue class component。这里主要依赖装饰器。顺手安利下《从java注解漫谈到typescript装饰器——注解与装饰器》。

    此外,可以拓展了解一下元编程。

    vue2比较令人诟病的地方还是对ts的支持,对ts支持不好是vue2不适合大型项目的一个重要原因。其根本原因是Vue依赖单个this上下文来公开属性,并且vue中的this比在普通的javascript更具魔力(如methods对象下的单个method中的this并不指向methods,而是指向vue实例)。换句话说,尤大大在设计Option API时并没有考虑对ts引用的支持)

    具体用法算是比较详细吧:https://github.com/kaorun343/vue-property-decorator

    <template>
      <div>
        <sidebar/>
      </div>
    </template>
    
    <script lang="ts">
    import { Component, Vue, Watch } from 'vue-property-decorator';
    import { Action, Mutation, State } from 'vuex-class';
    import sidebar from 'layout/sidebar';
    @Component({
      components: {
        sidebar,
      },
    })
    
    export default class App extends Vue {
      private loading = true
    
      @State(state => state.user.theme)
      private readonly theme
      @Mutation('user/setThemeModel')
      private setThemeModel
      @Action('user/getUserInfo')
      private getUserInfo
    
      @Watch('$route.params.id')
      private handler() {
        // TODO
      }
    
      private get testCount() {
        // TODO
      }
    
      private mounted(): void {
      }
    }
    </script>

    我个人不太喜欢这个风格,但是,但是周围都是这么样用,现在重构的项目就采用这个风格了

    typescript mixin

    我对mixin不太感冒。既然要用,一定要注意以下几点:

    mixins合并规律:
    • 覆盖的:data,props,methods,computed,inject

    • 直接替换:el,template,propData

    • 合并的:

      • methods,权重高的函数先执行

      • 生命周期函数,watch监听回调函数,权重小的 先执行

    mixins混合权重

    类似css权重规则(其实没有权重这个东西,但是结果是一样的,只是觉得这样好理解而已)

    • *、全局 选项

    • 1、......省略无数可能存在的嵌套 mixin

    • 10、组件 - mixin - mixin

    • 100、组件 - mixin

    • 1000、组件选项

    更多参看《vue mixins、Vue.extend() 、extends使用注意事项笔记

    用起来很简单

    import { Component, Mixins, Vue } from 'vue-property-decorator';
    // mixin
    @Component
    export default class PageLeave extends Vue {
      // TODO
    }
    // 组件
    @Component({
      components: {
        TitleBar,
      },
    })
    export default class CardPanel extends Mixins(PageLeave,OtherMixin) {
      //TODO
    }

    不过TS的vue项目,mixin基本被我排除在外。

    早在2016年中期,丹·阿布拉莫夫(Dan Abramov)就写了《mixin被认为是有害的》(mixin Considered Harmful),他在书中辩称,将 mixin 用于在 React 组件中重用逻辑是一种反模式,主张远离它们。

    他提到的关于 React mixins 的缺点同样适用于 Vue。

    OPP本来就可以解决一切呀,不香么!

    vue-property-decorator方案缺点

    • vue class component与js的vue组件差异太大,另外需要引入额外的库,学习成本大幅度增高。

    • 依赖于装饰器语法。而目前装饰器目前还处于stage2阶段(可查看tc39 decorators),在实现细节上还存在许多不确定性,这使其成为一个相当危险的基础。

    • 复杂性增高。采用Vue Class Component且需要使用额外的库,相比于简单的js vue组件,显然复杂化。

    个人更偏下一下方案。

    tsx组合方案:Vue Components + TypeScript

    我起初是写react的,后写vue,所以更喜这种风格

    import Vue, { VueConstructor, CreateElement, VNode } from 'vue';
    interface InputInstance extends Vue {
      composing: boolean;
    }
    export default (Vue as VueConstructor<InputInstance>).extend({
      name: 'demo-input',
      props: {},
      data() {},
      // TODO 更Vue其它组件一样
      render(h: CreateElement): VNode {
        // TODO 逻辑
        const classes = []
        const wrapperAttrs = {...this.$attrs};
        const wrapperEvents = {...this.$listeners};
        // JSX 语法
        return (
          <div class={classes} {...{ attrs: wrapperAttrs, on: wrapperEvents }}>
            <input/>
          </div>
          );
      }
    }

    这里的mixin还是和之前的一样

    // Count.mixin.ts
    import Vue from 'vue'
    import { mapGetters } from 'vuex'
    export default Vue.mixin({
      computed: {
        ...mapGetters(['count'])
      },
      methods: {}
    })
    // Count.vue
    export default Vue.extend<{}, Methods, Computed, {}>({
      mixins: [CountMixin],
      methods: {}
    })

    多个mixin混合

    import CountMixin, { Computed as CountComputed } from './Count.mixin'
    import LoadingMixin, { Computed as LoadingComputed } from './Loading.mixin'
    type Computed = CountComputed & LoadingComputed
    interface Methods {
      incrementCount: Function
      decrementCount: Function
    }
    export default Vue.extend<{}, Methods, Computed, {}>({
      mixins: [CountMixin, LoadingMixin],
      methods: {}
    })

    但是,上面mixin的data 类型 糊了……

    推荐实现
    interface CountBindings extends Vue {
      count: number
    }
    export default (Vue as VueConstructor<CountBindings>).extend({
      mixins: [CountMixin],
      methods: {}
    })

    具体可以参看,https://medium.com/glovo-engineering/vue-components-typescript-ff62db05829c

    composition-api

    这个首先需要npm i -S @vue/composition-api

    然后全局注入

    import VueCompositionApi from "@vue/composition-api";
    Vue.use(VueCompositionApi);

    其实,这个我也琢磨中。晚点在补充这方面的内容。

    直接升级Vue3.0

    我是没有怎么做,如果是重写性重构,我肯定会直接用Vue3.0。但是对于庞大的项目,重构直接用3.0,还是怕怕。

    虽然尤大大说vue2 与vue3,不会像angular2 与其后代版本差异那么大,但是,我还是缓缓先

    Vuex Store的痛

    在ts里面使用vuex非常的蛋疼。

    vuex ts版相关的vuex-classvuex-module-decorators两个库应该是目前用的最多的(个人认为)。

    https://www.npmtrends.com/vuex-aggregate-vs-vuex-class-vs-vuex-module-decorators



    starsissuesupdatedcreated
    vuex-class1,65318Oct 12, 2020Jan 14, 2017
    vuex-module-decorators1,595123May 8, 2021May 1, 2018

    如果是老旧项目,个人推荐先使用vuex-class过度。


    暂时先整理到这里,周末早点睡。后续再跟进……




    转载本站文章《vue2.x老项目typescript改造过程经验总结》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/vue/8637.html