vue2 + Ts类型规范起来!Vue2 渐进式引入typescript

1,193 阅读6分钟

前言

习惯了typescript,再回过头来写裸跑的javascript代码,会发现各种不方便,最令我不习惯的是代码编辑器不智能了,代码检索和跳转呆滞了,有时会看着代码中的魔法数字猜想含义,非常担心自己的笔误会造成变量引用错误,这也让我在敲代码的时候也会呆滞,这也是我想在项目中加入typescript的原因。

关于一些如引入价值,学习成本、是否有额外负担之类的问题,可以参考:Vue2项目引入Typescript记录的回答,我的观点是学习成本较低,引入价值极高。不会吧不会吧,都2202年了,前端项目里不会还没有ts吧。

项目的正常运行是需要首要保证的,目前网上的文档整体项目重构的偏多,代入的我们的项目里短期内不现实,故我也在思索与实践vue2项目渐进式引入typescript的方案,最终也做到了script lang="ts"的vue文件可以使用ts规范,存量vue文件仍然按原有逻辑运行。

项目背景

项目由vue-cli@4搭建,vue2项目,某些文件使用的是jsx的语法,由于使用了node-sass,项目只有在node只有在低版本(v14.19.1)才能运行,项目最低需要支持到IE 10,package.json配置简化如下:

{
    "private": true,
    "dependencies": {
        "ace-builds": "^1.4.12",
        "axios": "^0.21.1",
        "babel-polyfill": "^6.26.0",
        "bpmn-js": "^8.2.0",
        "clipboard": "^2.0.6",
        "core-js": "^3.16.4",
        "countup.js": "^1.9.3",
        "crypto-js": "^4.0.0",
        "downloadjs": "^1.4.7",
        "echarts": "^4.1.0",
        "echarts-liquidfill": "^3.0.0",
        "el-tree-transfer": "^2.4.7",
        "element-ui": "2.13.0",
        "eslint": "^6.8.0",
        "fingerprintjs2": "^2.1.4",
        "html2canvas": "^1.0.0-rc.5",
        "jquery": "^3.5.1",
        "js-base64": "^3.6.0",
        "js-cookie": "2.2.0",
        "js-md5": "^0.7.3",
        "jspdf": "git+http://git.syf.com/nodepublic/jspdf.git#v1.5.3",
        "mockjs": "^1.0.1-beta3",
        "moment": "^2.22.2",
        "nprogress": "^0.2.0",
        "qrcodejs2": "0.0.2",
        "sortablejs": "^1.10.2",
        "vue": "^2.7.10",
        "vue-awesome-swiper": "^3.1.3",
        "vue-countup-v2": "^2.0.0",
        "vue-event-calendar": "^1.5.2",
        "vue-pdf": "4.0.0",
        "vue-quill-editor": "^3.0.6",
        "vue-router": "^3.0.1",
        "vue-ueditor-wrap": "^2.4.3",
        "vue2-editor": "^2.10.3",
        "vuescroll": "^4.17.3",
        "vuex": "^3.0.1",
        "@tinymce/tinymce-vue": "^3.2.0",
        "codemirror": "^5.58.2",
        "monaco-editor": "^0.20.0",
        "monaco-editor-webpack-plugin": "^1.9.0",
        "vcrontab": "^0.3.5",
        "vue-esign": "^1.0.5",
        "vue-simple-uploader": "^0.7.6",
        "spark-md5": "^3.0.2"
    },
    "devDependencies": {
        "@babel/plugin-transform-modules-commonjs": "^7.10.1",
        "@babel/plugin-transform-strict-mode": "^7.10.1",
        "@vue/cli-plugin-babel": "^4.4.0",
        "@vue/cli-plugin-eslint": "^4.4.0",
        "@vue/cli-plugin-router": "^4.4.0",
        "@vue/cli-service": "^4.4.0",
        "@vue/composition-api": "^1.6.1",
        "@vue/eslint-config-standard": "^4.0.0",
        "babel-eslint": "^10.1.0",
        "babel-plugin-transform-remove-strict-mode": "^0.0.2",
        "drag-tree-table": "^2.2.0",
        "eslint": "^6.8.0",
        "eslint-plugin-html": "^5.0.5",
        "eslint-plugin-vue": "^9.3.0",
        "less": "^2.7.3",
        "less-loader": "^4.0.5",
        "lint-staged": "^8.1.0",
        "mathjs": "^5.2.1",
        "node-sass": "^4.14.1",
        "pdfjs-dist": "2.5.207",
        "sass-loader": "^8.0.2",
        "sass-resources-loader": "^2.2.1",
        "script-loader": "^0.7.2",
        "speed-measure-webpack-plugin": "^1.5.0",
        "thread-loader": "^3.0.4",
        "vue-template-compiler": "^2.7.10",
        "vuedraggable": "^2.23.2",
        "webpack": "4.46.0",
        "xlsx": "^0.13.1"
    },
    "gitHooks": {
        "pre-commit": "lint-staged"
    },
    "lint-staged": {
        "*.{js,vue}": [
            "vue-cli-service lint",
            "git add"
        ]
    }
}

开始的时候想用魔法打败魔法,也尝试过使用vue cli的ts配置进行改造,但发现按照插件的思路,是将相关文件进行扫描之后代码替换,对于使用者来说比较黑盒(感觉vue-cli系列都比较黑盒),同时对于一些如jsx的写法该插件会报错并停止运行,因此还是选择一步步搭建ts相关配置。

为什么是渐进式的?

只有在vue文件的script标签设置lang="ts"才会引入ts的类型规范。

技术方案

基本思路

  • 安装ts相关依赖,并解决版本兼容问题
  • ts编译配置
  • 类型声明。项目公共类型声明,开源库相关@types依赖
  • eslint规则配置

依赖改造

在安装不恰当的依赖版本时,碰到的错误如(可能还有其他的):

  • Syntax Error: Thread Loader (Worker 0) eslint.CLIEngine is not a constructor
  • Parsing error: '>' expected at xxx
  • Parsing error: Unexpected token as operation eslint
  • npm ERR! code MODULE_NOT_FOUND
  • Eslint doesn't recognize "as" operator

经过踩坑解决后得到兼容的版本,这里放一张代码对比图吧,方便大家直观的看到依赖前后差别:

依赖对比.png

最终敲定并使用的依赖版本package.json的devDependencies如下:

{
    "devDependencies": {
        "@babel/plugin-transform-modules-commonjs": "^7.10.1",
        "@babel/plugin-transform-strict-mode": "^7.10.1",
        "@babel/preset-typescript": "^7.18.6",
        "@types/lodash": "^4.14.189",
        "@typescript-eslint/eslint-plugin": "^5.43.0",
        "@typescript-eslint/parser": "^5.43.0",
        "@vue/cli-plugin-babel": "^4.4.0",
        "@vue/cli-plugin-eslint": "^4.4.0",
        "@vue/cli-plugin-router": "^4.4.0",
        "@vue/cli-plugin-typescript": "^4.5.15",
        "@vue/cli-service": "^4.4.0",
        "@vue/composition-api": "^1.6.1",
        "@vue/eslint-config-standard": "^4.0.0",
        "babel-eslint": "^10.1.0",
        "babel-plugin-transform-remove-strict-mode": "^0.0.2",
        "drag-tree-table": "^2.2.0",
        "eslint": "^7.32.0",
        "eslint-plugin-html": "^5.0.5",
        "eslint-plugin-vue": "^9.3.0",
        "less": "^2.7.3",
        "less-loader": "^4.0.5",
        "lint-staged": "^8.1.0",
        "mathjs": "^5.2.1",
        "node-sass": "^4.14.1",
        "pdfjs-dist": "2.5.207",
        "sass-loader": "^8.0.2",
        "sass-resources-loader": "^2.2.1",
        "script-loader": "^0.7.2",
        "speed-measure-webpack-plugin": "^1.5.0",
        "thread-loader": "^3.0.4",
        "ts-loader": "^8.2.0",
        "typescript": "^4.5.5",
        "vue-template-compiler": "^2.7.10",
        "vuedraggable": "^2.23.2",
        "webpack": "4.46.0",
        "xlsx": "^0.13.1"
    }
}

tsconfig配置

我删除了项目中原有的jsconfig文件,并新建了tsconfig文件。有种解释是jsconfig是tsconfig加入"allowJs": true等某些配置的特例。

所有配置项可以看官网tsconfig,项目配置的考虑我写在了下面注释里。

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "target": "ES5", // IE 10可支持 ES5
        "lib": [
            "es5",
            "dom",
            "ScriptHost",
            "es2015.promise",
            "WebWorker"
        ], // 默认内置的类型库
        "allowJs": true, // jsconfig
        "noEmit": true,
        "typeRoots": [ // 存放自定义类型文件的地方
            "./typings"
        ],
        "skipLibCheck": true,
        "importHelpers": true,
        "esModuleInterop": true, // 配合importHelpers解决import和require兼容问题
        "forceConsistentCasingInFileNames": true,
        "useDefineForClassFields": true,
    },
    // vueCompilerOptions不属于tsconfig的配置项,属于Vue Language Features插件友好配置
    "vueCompilerOptions": { 
        "target": 2.7,
        "extensions": [
            ".vue"
        ]
    },
    "exclude": [
        "node_modules",
        "dist"
    ]
}

类型声明与安装

多数例子是在根目录下面创建vue-shim.d.ts进行声明,但是我觉得根目录下面文件已经太多了,同时这个文件的引用关系也不够明确,或许是脚手架里有存在与否的判断吧。所以我采用的方法是新建typings文件夹,在文件夹里定义类型,引用关系可以在tsconfig中直观的看到。

// ./typings/index.d.ts
import 'vue'
import VueRouter from 'vue-router'
import { Route } from 'vue-router'

declare module '*.vue' {
    import Vue from 'vue'
    export default Vue
}

declare module 'vue/types/vue' {
    interface Vue {
        $router: VueRouter
        $route: Route
    }
}

这里对vue和vue-router进行了定义,其实declare声明语句都是在某些源码里声明过的,这里再次声明是对eslint语法识别友好。

某些源码本身没有类型声明,就会在github上发布声明文件,仓库地址见types,大家按需下载到开发依赖中即可,安装方案也比较简单,如安装lodash的声明文件为:

npm i @types/lodash -D

eslint规则配置

eslint对ts的lint是友好的,历史背景是tslint发布后,发现原理和检查与eslint极为相似,所以tslint就废弃了,感兴趣可以自行查阅相关资料,废弃了好呀,统一起来规范些。

module.exports = {
    // 默认情况下,ESLint会在所有父级组件中寻找配置文件,一直到根目录。ESLint一旦发现配置文件中有 "root": true,它就会停止在父级目录中寻找。
    root: true,
    env: {
        // 环境定义了预定义的全局变量
        // 你的脚本将要运行在什么环境中;
        node: true,
        es6: true
    },
    parserOptions: {
        // 解析器更改为@typescript-eslint/parser,可兼容js的解析
        parser: '@typescript-eslint/parser', 
        ecmaFeatures: {
            legacyDecorators: true,
            jsx: true,
            tsx: true
        },
        extraFileExtensions: ['.vue'] // 拓展到.vue文件
    },
    globals: {
        // 额外的全局变量
        $: true,
        jquery: true,
        CountUp: true,
        Swiper: true,
        ActiveXObject: true
    },
    plugins: [
        'vue',
        '@typescript-eslint'
    ],
    extends: ['eslint:recommended', 'plugin:vue/recommended'], // 基础的lint检查
    overrides: [
        // 针对.ts文件,使用ts的相关拓展检查
        {
            files: ['*.ts'],
            parser: '@typescript-eslint/parser',
            plugins: ['@typescript-eslint'],
            extends: ['plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/eslint-recommended']
        }
    ]
}

eslint能够根据js、ts文件类别使用不同的解析器,关键在于overrides配置项的设置,如'plugin:@typescript-eslint/recommended'的eslint拓展项针对js文件也有效,但为了避免文件大片区域飙红,只在ts文件下使用。

有个问题,我们的代码不是.vue文件吗?为什么ts的规则也适配。其实,只要在.vue文件的script中设置lang="ts",在vue模版编译后也就成为了ts文件,所以ts的规则也可以生效。

结语

啦啦啦,配置完成啦,typescript用过的都说好,快来试试吧!