如何多个项目公用.eslintrc_如何在 Nuxt 项目中引入 TypeScript

927b1cd6a726dd488275e968ac57ed0f.png

前言

本文讲述如何在已有的 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

解决方案

经过一番探索,该问题有三个解决方案:

  1. ts-ingore:直接让 ts 忽略检查
  2. 驼峰到中划线:让它不要报错
  3. 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 中识别。

编写的方式分为两种:

  1. 不修改原组件(适用于第三方组件
  2. 直接修改原组件(适用于项目内的组件

不修改原组件,大概就是这样:

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 个:

  1. any 大法
  2. Composition API 真香!

最后

既然已经接入 TypeScript,那么推荐把 Composition API 也一并接入,可以得到更加的开发体验哦,有兴趣可以点击这篇文章《如何在 Nuxt 项目中引入 Composition API》。

另外,如果在接入过程中发现问题,不妨在评论区留言,大家一起讨论下哈。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值