前言
习惯了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
经过踩坑解决后得到兼容的版本,这里放一张代码对比图吧,方便大家直观的看到依赖前后差别:
最终敲定并使用的依赖版本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用过的都说好,快来试试吧!