沐光

记录在前端之路的点点滴滴

项目 ts 迁移的踩坑记录

前言

先前发过一篇对于 shimes-vue-ts 文件的思考,其中有一节写到项目 ts 迁移所遇到的问题和解决方法。因为项目才刚迁移完一部分 ts(用得到的部分),踩坑的过程还在进行中,为了更好的记录 ts 迁移过程中所遇到的问题,还是单独写一篇来做记录吧。此篇便是改进后的问题记录,以及相对详细的解决方法。该篇会持续跟新哦~

问题集锦

动态引入无 ts 声明的文件

项目内有动态设置 cookie 的文件,因为不同测试环境需要配置不同的 cookie,每天都需要从服务器拿取一次 cookie,目前的做法为:

1
2
3
4
// cookie-set 文件夹已经移除 git 缓存跟踪
if (process.env.NODE_ENV === 'development') {
import('@/cookie-set');
}

由于需要迁移 ts,对于原有的 cookie-set.js 需要进行类型声明(或者都改成 cookie-set.ts),为了兼容两种情况,这里还是做了对应类型声明,如:

1
2
// 在 declarations.d.ts 文件内添加
declare module '@/cookie-set';

引入无 typings 的 npm 包

比较全的 npm 包会包含对应的 typings 文件来支持 ts 项目,但是对于没有 typings 文件的包来说,我们需要对齐进行外部文件声明。那么为了支持 vue.use 方法,我们可以这么写对应的声明:

针对默认导出的声明写法:

1
2
3
4
5
6
// 此适用于 import vueClipboard from 'vue-clipboard2';
declare module 'vue-clipboard2' {
import { PluginFunction } from 'vue';
const clipboard: PluginFunction<any>;
export default clipboard;
}

针对解构的写法:

1
2
3
4
5
// 此适用于 import { monitorVue } from 'fe-monitor-sdk';
declare module 'fe-monitor-sdk' {
import { PluginObject } from 'vue';
export const monitorVue: PluginObject<any>;
}

export 和 export default 可参考模块部分内容

参考文章: 模块TypeScript 支持

vue-router 的组建引用报错

虽然在 webpack 内我们配置了 alias,但那仅仅只是 webpack 打包时用的,ts 并不认账,它有自己的配置文件。因此,在升级至 ts 项目时,我们还得为 ts 配置一份模块路径,如:

1
2
3
4
5
6
7
//  tsconfig.json
path: [
"@/*": [
"src/*"
],
// ...
]

此外,因为编辑器的原因使得无法识别 .vue 后缀,所以对于 vue 文件的引用必须添加 .vue 后缀,如:

1
import myComponent from './my-component.vue';

vue 的 data 部分爆红

这个问题比较隐蔽,折腾了很久才发现因为 data 为函数(主要是写惯了才难以察觉,官网文档因为类型推断所以没有写),其内的对象为返回值,因此此部分的声明可以写(个人推荐不要用断言):

1
2
3
4
5
6
7
8
9
// 返回值声明写法
data(): Your Interface here {
return {};
}

// 断言写法
data() {
return <Your assertions here> {};
}

注: 主要是为 data 内的数组、对象声明对应类型,都为基础类型时不写后面使用也不会报错。

vue 的 mixins 文件写法

Vue 的 mixins 写法有两种,一种为普通的 ts 写法,另一种为装饰器的写法

1
2
3
4
5
6
7
8
9
10
11
12
// 原来的写法
export default {/**/}

// 普通 ts 写法
import Vue form 'vue';
export default Vue.extend({/**/})

// 装饰器写法
import { Vue, Component, Prop } from "vue-property-decorator";

@Component
export default class ComponentName extends Vue { }

注: 普通 ts 写法的 computed 部分需要添加范围值,可参考 vue 文档 TypeScript 支持部分。

VS Code experimentalDecorators 问题

因为 vue 装饰器写法为实验性特性,可能在未来的发行版中发生变化,因此需要配置此参数来删除警告。直接根据警告来做相应配置,即在 tsconfig.json 内添加属性:

1
"experimentalDecorators": true

类的静态方法

关于类一般会采用 abstruct 抽象类来规范方法和属性等类的细节,但是对于“类”中 static 部分无法进行抽象规范,需要在对应静态方法部分进行单独处理,对于此部分是否有更好的处理方法存在疑问 🤔(如:提取一个 interface 之类的声明)。目前想到的比较靠谱的写法有两个:

namespace 写法

官方文档中也有说过,对于业务内的模块来说,推荐使用 namespace 来做全局命名,因此对于业务内比较通用的公共方法来说,可以使用 namespace 来处理。

对于多层命名空间的写法,可用别名写法 import NS = FirstNameSpace.SecondNameSpace,然后直接通过 NS.xxx 来直接取对应属性即可。同时区别加载模块时使用的 import someModule = require('moduleName'),此处的别名仅仅只是创建一个别名而已,简化代码量。

module 文件

另一种可用 ES6 的思想,import + export ,因为类中只有 static 方法,因此可以认为该类为一个模块,而一个模块对应一个文件,因此作为一个 ts 文件来存储对应方法,需要时在 import 引入即可。

eslint 迁移至 tslint 时部分校验修复失效问题

semi 无自动补全

将原来配置的 eslint 的 semi 校验替换:

1
2
3
4
5
6
// 原来的配置
semi: ['error', 'always']

// 现在的配置
"semi": "off",
"@typescript-eslint/semi": ["error"],
import 引入没有使用的内容不报错

因为 @vue/eslint-config-typescript 的文件内将 @typescript-eslint/no-unused-vars 给注释掉了,同时 no-unused-vars 设置为 ‘off’,使得对没有使用的变量都不校验。其对应设置的解释为:
传送门

解决方法(跟项目 eslint 和 typescript 版本不同而不同):

1
2
3
4
5
6
7
8
'@typescript-eslint/no-unused-vars': [
'error',
{
vars: 'local',
args: 'none',
ignoreRestSiblings: true
}
]

$refs 引用报错问题

由于 Vue 对 refs 的声明为 type Vue | Element | Vue[] | Element[],在通过 $refs 调用对应组件方法时,因为 Vue 和 Element 上没有对应方法声明,因此我么需要对其进行断言处理,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface MyComponent extends Vue {
someMethod(): void
}

// 普通 ts 写法
(this.$refs.myComp as MyComponent).someMethod()

// 装饰器写法
@Component
export default class A extends Vue {
$refs: { myComp: MyComponent }

this.$refs.myComp.someMethod()
}

@Prop 的类型问题

官方对于 @Prop 装饰器的写法提供了两种思路,分别为:

1
2
3
4
// 不带 type
@Prop({ default: '' }) readonly propB!: string
// 带 type
@Prop({ type: String, default: '' }) readonly propB!: string

带 type 的和原来的写法可以说没什么区别,但是不带 type 的会出现两种问题:

问题一

当为 Boolean 类型时,会使得属性官方 boolean 的简写方式无法生效,如:

1
2
<!-- 不写 type 时此不生效,只能显示绑定 true 值 -->
<blog-post is-published></blog-post>

官方例子

问题二

此不会触发部分的相应的 ts 类型校验,例如:

1
2
3
<!-- 假如对应 props 为: @Prop() likes!: number; -->
<!-- 此时 likes 传入类型为 string,但是不报错 -->
<blog-post likes="42"></blog-post>

个人建议: 除非非常确定传入的 Prop 的类型,否则尽量都加上对应的 type 声明

exceljs 引入报错问题

引入 exceljs 后,ts 一直报错 import("stream") 行因为没有找到 stream 包,因此报错。

问题定位了很久,才发现 stream 是 Node 自带的包,而我们的 tsconfig.json 内却并没有将对应的 node 类型引入,因此会报错,解决方法:

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
// ...
"types": [
"webpack-env",
"node" // 将 node 类型加入即可
]
}
}

注意,引入 node 后,可能会引发新的 ts 问题,比如自定义的 process.env.XXX 可能为 undefined,这个需要自己做兼容了。

参考 issue

拓展内容

namespace

TS 里的 namespace 主要是解决命名冲突的问题,会在全局生成一个对象,定义在 namespace 内部的类都要通过这个对象的属性访问。对于内部模块来说,尽量使用 namespace 替代 module,可参考 namespace 一节。例如:

1
2
3
4
5
6
7
8
9
10
11
12
namespace Test {
export const USER_NAME = 'test name';

export namespace Polygons {
export class Triangle {}
export class Square {}
}
}

// 取别名
import polygons = Test.Polygons;
const username = Test.username;

注:import xx = require(‘xx’) 为加载模块的写法,不要与取别名的写法混淆。默认全局环境的 namespace 为 global

参考文档:namespace

module

模块可理解成 Vue 中的单个 vue 文件,它是以功能为单位进行划分的,一个模块负责一个功能。其与 namespace 的最大区别在于:namespace 是跨文件的,module 是以文件为单位的,一个文件对应一个 module。类比 Java,namespace 就好比 Java 中的包,而 module 则相当于文件。

如果你的模块需要将新的名称引入全局命名空间,那么就应该使用全局声明。如果你的模块无需将新的名称引入全局命名空间,那么就应该使用模块导出声明。

FromDefinitelyTyped