![927b1cd6a726dd488275e968ac57ed0f.png](https://i-blog.csdnimg.cn/blog_migrate/8a82ce08b452341884bf265a4e752cdd.jpeg)
前言
本文讲述如何在已有的 Nuxt 项目中支持 TypeScript,以及一些踩过的坑,希望能够帮助你快速接入。
如果是新项目,建议使用 @femessage/create-nuxt-app 脚手架生成,就无需再重复做本文的步骤啦。
另外,接入 TypeScript 其实是向下兼容的,也即只会针对声明 lang="ts"
的 .vue
或 .ts
文件进行类型检查,已有的代码不受影响。
环境说明
这里说明运行环境和本文所装依赖的具体版本,如果根据本文操作步骤的实际结果与预期结果不一致,请尝试升级或降级依赖版本。
运行环境:
- Mac OS 10.15.6
- Node v14.15.0
依赖版本:
- @nuxt/typescript-build@2.0.3
- @nuxt/types@2.14.7
- typescript@4.0.5
- nuxt@2.11.0
- vue@2.6.12
- eslint@7.12.1
- babel-preset-vca-jsx@0.3.6
- @babel/preset-env@7.12.1
操作
支持 TypeScript
1. 安装依赖
yarn add --dev @nuxt/typescript-build @nuxt/types
2. 配置 nuxt.config.js
module.exports = {
buildModules: [
+ '@nuxt/typescript-build',
]
}
3. 增加 tsconfig.json
{
"compilerOptions": {
"target": "es2018",
"module": "esnext",
"moduleResolution": "node",
"lib": ["esnext", "esnext.asynciterable", "dom"],
"esModuleInterop": true,
"allowJs": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"experimentalDecorators": true,
"baseUrl": ".", // 如果写 src 会出现 @ 无法正常指向
"jsx": "preserve",
"paths": {
"~/*": ["./src/*"], // 根据项目实际情况
"@/*": ["./src/*"]
},
"types": ["@types/node", "@nuxt/types", "@nuxtjs/axios"],
"skipLibCheck": true
},
"exclude": ["node_modules", ".nuxt", "dist"]
}
关于 tsconfig.json 的配置可以看这个 JSON(真的是一个 JSON),如果感觉有阅读障碍可以看这个不是官方的:Typescript tsconfig.json 全解析
4. 增加类型文件
// src/types/vue-shim.d.ts
declare module "*.vue" {
import Vue from 'vue'
export default Vue
}
5. 验证
新增一个 test
页面文件,增加以下内容:
<template>
<div>
{{ message }}
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
data () {
return {
message: 'TypeScript Test'
}
}
})
</script>
然后运行 yarn dev
,查看是否能够正常运行。
修改 ESLint
1. 升级 eslint
如果项目中的 eslint 版本较旧,可能会导致 lint 无法正常运行,安全起见,先升级到最新版本(目前是v7.12.1)
yarn upgrade eslint --latest
2. 安装 @nuxtjs/eslint-config-typescript
yarn add -D @nuxtjs/eslint-config-typescript
3. 检查是否有 eslint-plugin-vue
如果项目有 eslint-plugin-vue 这个依赖,那么就需要先移除该依赖,使用 eslint-plugin-nuxt 替换,如果已经有就可以省略这一步
yarn remove eslint-plugin-vue
yarn add -D eslint-plugin-nuxt
PS:这里踩过坑,如果不替换会导致运行 lint 时报错。
4. 替换 babel-eslint
因为这样做会使 ESlint 用 (@typescript-eslint/parser
) 作为TypeScript语法分析器,所以请确保parserOptions.parser
这个选项并没有被其他的扩展 (extends) 设置所覆盖。 如果你正在使用babel-eslint
当作你的语法分析器,请将其从.eslintrc.js
和你的依赖文件中移除。
参考: https:// typescript.nuxtjs.org/z h-Hans/guide/lint.html#%E8%AE%BE%E7%BD%AE
移除 babel-eslint 依赖
yarn remove babel-eslint
修改 .eslintrc.js
module.exports = {
- parserOptions: {
- parser: 'babel-eslint'
- },
extends: [
+ '@nuxtjs/eslint-config-typescript',
]
}
5. 验证
先在刚刚创建的 test
页面文件随便写一行会导致 lint 失败的代码,然后运行 yarn lint
看看是否按预期运行。
// src/pages/test.vue
+ const a = 2 // 'a' is assigned a value but never used.
6. 开发环境时检查 TS 错误
如果想要在开发环境时检查 TS 的相关错误,那么可以在 nuxt.config.js 中添加以下
module.exports = {
+ typescript: {
+ typeCheck: {
+ async: false, // 将 TS 错误信息显示在页面上
+ },
+ },
}
推荐开启,因为 TS 类型报错只会在 yarn dev 或 yarn build 时才能发现,eslint 检查不出来。
这里也可以通过设置 eslint 字段让它检查 eslint 规则,但这会影响开发体验,并不推荐。
库类型声明文件
FEMessage 组件库声明文件
相关文档在这里:《组件库类型声明文件组织方案》。
FEMessage 下的组件库都有类型声明文件,只需要这样引入:
// 也可以尝试直接输入 ElForm... ,说不定能直接自动引入哦
import { ElFormRendererType } from '@femessage/el-form-renderer'
// 这样就能得到友好的编辑器提示
(this.$refs.form as ElFormRendererType).validate(async (valid) => {
// doSomething
})
// 当然也可以使用 any 大法,不过不推荐使用
(this.$refs.form as any).validate(async (valid) => {
// doSomething
})
对于其他组件,导出的类型名字都是以组件名 + Type 命名。
由于部分组件类型依赖 @femessage/types,可能需要安装此依赖才能正常使用:
yarn add -D @femessage/types
外部库类型声明文件
如果是外部类库,且官方没有提供类型声明文件,同时在 @types/ 下也没有,这种情况就需要自己编写类型声明文件了。
引入报错
举个例子,如果引入一个没有提供类型声明文件的库/组件,就会看到类似这样的报错:
Could not find a declaration file for module 'xxx'. 'xxx/node_modules/xxx/dist/index.js' implicitly has an 'any' type...
虽然上面的报错不会影响运行,只是告诉我们它找不到对应的类型,隐式地转成 any,但用起来实在难受...
那么我们就需要在 types
目录下新增一个 .d.ts 文件,写如下代码:
// xxx => 引入库的包名 e.g. @femessage/excel-it
declare module 'xxx' {
// 如果这是一个通过解构导入的方法库,你还要声明属性类型,比如:
// export const importExcel: (ignore?: string[], callback: (any) => any) => any
}
Nuxt 上下文属性类型
如果某个类库提供了类型文件,但没有自动引入,那么需要在 types 下手动引入它,以 Sentry 为例:
import '@nuxtjs/sentry/types'
而如果不得不为了某些在 Vue 实例下通过 http://this.xxx 访问的属性编写类型,以 OneSignal 为例:
import Vue from 'vue'
// add type to Vue context
declare module 'vue/types/vue' {
interface Vue {
readonly $OneSignal: any
}
}
// App Context and NuxtAppOptions
declare module '@nuxt/types' {
interface Context {
readonly $OneSignal: any
}
interface NuxtAppOptions {
readonly $OneSignal: any
}
interface NuxtOptions {
OneSignal?: any
}
}
// add types for Vuex Store
declare module 'vuex/types' {
interface Store<S> {
readonly $OneSignal: any
}
}
支持 JSX/TSX
如果要在组件中使用 JSX,需要把 lang="ts" 改成 lang="tsx"(同理,也只能在 .tsx 文件里使用 JSX
- <script lang="ts">
+ <script lang="tsx">
另外还要增加类型文件,否则可能会报下面这个错:
JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.
// src/types/tsx-shim.d.ts
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
interface Element extends VNode {}
interface ElementClass extends Vue {}
interface IntrinsicElements {
[element: string]: any
}
}
}
这样就能正常处理 JSX 了。
但是!还是会有一个问题,举个例子:
formatter: (row: Feedback) => (
<StatusSelector
value={row.status}
// 下面这行代码会报错
onChange={(status: Status) => self.onChangeStatus(row, status)}
/>
)
报的错类似是这样的:
No overload matches this call. Overload 1 of 3, '(options?: ThisTypedComponentOptionsWithArrayProps<{... Property 'onChange' does not exist on type 'ThisTypedComponentOptionsWithArrayProps<{
原因
表面上好像只有当属性存在驼峰命名时才会报这个错,但其实终极原因是:在 JSX 中无法正常支持 Vue 组件,也即无法识别该组件所具有哪些属性,导致报这个属性不存在。 关于这个问题的讨论可以看:https://github.com/vuejs/vue-cli/issues/2417
解决方案
经过一番探索,该问题有三个解决方案:
- ts-ingore:直接让 ts 忽略检查
- 驼峰到中划线:让它不要报错
- vue-tsx-support:从根源上解决问题
1. ts-ingore
简单来说就是让 ts 忽略下来这行的类型检查,好处就是方便快捷。
formatter: (row: Feedback) => (
<StatusSelector
value={row.status}
+ // @ts-ignore
onChange={(status: Status) => self.onChangeStatus(row, status)}
/>
)
但是如果在 JSX 的第一层使用 ts-ingore 会失效,这里提供几个变种方案:
// 下面的 ts-ingore 会失效
return () => (
<div>
<p>{count.value}</p>
+ // @ts-ignore
<ElButton onClick={increase}>increase</ElButton>
</div>
)
// 以下方案可解决,参考链接:https://github.com/microsoft/TypeScript/issues/27552
// 方案 1
return () => (
<div>
<p>{count.value}</p>
+ {/* @ts-ignore */}
<ElButton onClick={increase}>increase</ElButton>
</div>
)
// 方案 2
return () => (
<div>
<p>{count.value}</p>
<ElButton
+ // @ts-ignore
onClick={increase}
>
increase
</ElButton>
</div>
)
// 方案 3
return () => (
<div>
<p>{count.value}</p>
+ {/*
+ // @ts-ignore */}
<ElButton onClick={increase}>increase</ElButton>
</div>
)
2. 驼峰改为中划线
这种方式也更符合 vue template 的风格,没有 @ts-ignore 这样粗暴,也不需要像 vue-tsx-support 这么麻烦,推荐使用这种方式。
formatter: (row: Feedback) => (
<StatusSelector
value={row.status}
- onChange={(status: Status) => self.onChangeStatus(row, status)}
+ on-change={(status: Status) => self.onChangeStatus(row, status)}
/>
)
注:如果这种方法不奏效,建议直接使用 ts-ingore
3. vue-tsx-support
如果想从根源解决,就用这种方式,在社区逛了一圈,发现大家用的都是 vue-tsx-support 这个插件(个人维护)。
我给它的一句话介绍就是:可以在 JSX 中为 Tag/Component 提供属性类型检查。
下面介绍一下如何在项目中使用,以及其中的坑。
1. 安装和引入
安装
yarn add -D vue-tsx-support
修改 tsconfig.json
{
"jsx": "preserve",
+ "jsxFactory": "VueTsxSupport",
}
引入类型
// src/types/tsx-shim.d.ts
import 'vue-tsx-support/enable-check'
2. 使用
像上面的例子,我们要做的就是为 StatusSelector
这个组件编写类型,以便可以在 JSX 中识别。
编写的方式分为两种:
- 不修改原组件(适用于第三方组件
- 直接修改原组件(适用于项目内的组件
不修改原组件,大概就是这样:
import * as tsx from 'vue-tsx-support'
import StatusSelectorOrig from '@/components/feedback/status-selector.vue'
import { Status } from '@/constant/feedback'
type StatusSelectorProps = {
value: Status
readonly?: boolean
}
type StatusSelectorEvent = {
onChange: (status: Status) => void
}
export default tsx
.ofType<StatusSelectorProps, StatusSelectorEvent>()
.convert(StatusSelectorOrig)
直接修改原组件,这里给一个完整例子:
<template>
<el-dropdown
:class="['status-selector', { 'status-selector__readonly': readonly }]"
trigger="click"
@command="onChange"
>
<span class="el-dropdown-link">
{{ status }}
<i
v-if="!readonly"
class="status-icon__bottom el-icon-caret-bottom el-icon--right"
/>
</span>
<el-dropdown-menu
v-if="!readonly"
slot="dropdown"
class="status-selecter__menu"
>
<el-dropdown-item
v-for="p in statusOptions"
:key="p.value"
:command="p.value"
>
{{ p.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script lang="ts">
import * as tsx from 'vue-tsx-support'
import { Status, statusNameMap, statusOptions } from '@/constant/feedback'
// 如果有自定义事件
type StatusSelectorEvents = {
onChange: (status: Status) => void
}
// 主要是这里,Vue.extend 替换为 tsx.componentFactoryOf().create
export default tsx.componentFactoryOf<StatusSelectorEvents>().create({
name: 'StatusSelector',
props: {
value: {
type: String as () => Status,
required: true,
},
readonly: {
type: Boolean,
default: false,
},
} as const,
data() {
return {
statusValue: '' as Status,
statusNameMap,
statusOptions,
}
},
computed: {
status(): string {
return statusNameMap[this.statusValue as Status]
},
},
watch: {
value: {
handler(v) {
this.statusValue = v
},
immediate: true,
},
},
methods: {
onChange(value: Status) {
this.statusValue = value
this.$emit('input:value', value)
// this.$emit('change', value)
// 相当于 $emit,但是可以静态检查
tsx.emitOn(this, 'onChange', value)
},
},
})
</script>
<style lang="less">
.status-selector {
.el-dropdown-link {
display: flex;
align-items: center;
cursor: pointer;
}
&.status-selector__readonly {
.el-dropdown-link {
cursor: unset;
}
}
.status-icon__bottom {
color: #c6c7ca;
}
}
.status-selecter__menu {
.el-dropdown-menu__item {
display: flex;
align-items: center;
}
}
</style>
好了,为组件编写完类型,就可以愉快地使用了,出了提供类型检查以外,还有编辑器提示哦!
FAQ
因在 JSX 使用自定义指令导致 ESLint 报错
在项目中有这么一段代码:
<v-img
v-img-preview:data-uncropped-src
src={imgUrl}
/>
结果 v-img-preview
指令这行在 eslint 中报错了: Parsing error: Identifier expected
但这个是正常的语法,而且只会在 JSX 的情况下才会报这个错,初步判断是 eslint 的问题,于是多番修改 eslint 配置后无果,于是换个思路:换个写法。
经网上一番搜寻,发现我们可以通过以下的方式代替这种写法,且 eslint 运行正常:
const directives = [
{
name: 'img-preview',
arg: 'data-uncropped-src'
}
]
<v-img {...{directives}} src={imgUrl} />
关于更多可以看这里:链接
无法正常识别 $store/$routers 等外部库的类型
如果编辑器出现以下报错:
Property '$xxx' does not exist on type 'CombinedVueInstance<Vue...
请检查是否 tsconfig.json
中的 baseUrl
是否填写正常。
this 类型推导问题
举个例子:
export default Vue.extend({
data() {
// 一个标配的 el-data-table 配置
tableConfig: {
extraButtons: [
{
text: '查看',
atClick: (row: Feedback) => {
this.visible = true // Property 'visible' does not exist on type 'Readonly<Record<never, any>> & Vue'.
return Promise.resolve(false)
},
},
],
}
}
})
这是因为 this 的类型推导错误的原因造成的,解决方案有 2 个:
- any 大法
- Composition API 真香!
最后
既然已经接入 TypeScript,那么推荐把 Composition API 也一并接入,可以得到更加的开发体验哦,有兴趣可以点击这篇文章《如何在 Nuxt 项目中引入 Composition API》。
另外,如果在接入过程中发现问题,不妨在评论区留言,大家一起讨论下哈。