由 shims-vue.d.ts 引起的思考

By: Kazehaiyahtml

原文:kazehaiya.github.io/2019/07/07/…vue

前言

因爲項目近期進行 ts 遷移,做爲第一個吃螃蟹的人,踩過了很多坑。遷移過程當中遇到的大大小小的問題基本上都解決了,可是對於 shims-vue.d.ts 文件的命名以及其內的模塊聲明始終找不到比較貼切的解釋。沉下心來讀了些外網資料,總算是有點「豁開雲霧見青天」的感受了。此處就記錄我對於 ts 全局模塊聲明的一些思考以及一些 ts 項目遷移遇到的坑。webpack

Vue ts 聲明文件

在安裝 @vue/typescript 以後,項目會生成兩個新文件,分別是 shims-vue.d.tsshims-jsx.d.ts,其內容分別是:git

// shims-vue.d.ts

declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}
複製代碼

github

import Vue, { VNode } from 'vue';

declare global {
  namespace JSX {
    // tslint:disable no-empty-interface
    interface Element extends VNode { }
    // tslint:disable no-empty-interface
    interface ElementClass extends Vue { }
    interface IntrinsicElements {
      [elem: string]: any
    }
  }
}
複製代碼

那麼這兩個文檔有什麼做用呢?web

shims-vue.d.ts

前者爲 Ambient Declarations(通稱:外部模塊定義) ,主要爲項目內全部的 vue 文件作模塊聲明,畢竟 ts 默認只識別 .d.ts、.ts、.tsx 後綴的文件;(即便補充了 Vue 得模塊聲明,IDE 仍是無法識別 .vue 結尾的文件,這就是爲何引入 vue 文件時必須添加後綴的緣由,不添加編譯也不會報錯)vue-router

shims-jsx.d.ts

後者爲 JSX 語法的全局命名空間,這是由於基於值的元素會簡單的在它所在的做用域裏按標識符查找(此處使用的是**無狀態函數組件 (SFC)**的方法來定義),當在 tsconfig 內開啓了 jsx 語法支持後,其會自動識別對應的 .tsx 結尾的文件,可參考官網 jsxvue-cli

產生的問題

首先,官方文檔的上並無將 shims-xxx.d.ts 作爲通用的模板,其僅僅給咱們列舉了如下模板樣例:typescript

  • global-modifying-module.d.ts
  • global-plugin.d.ts
  • global.d.ts
  • module-class.d.ts
  • module-function.d.ts
  • module-plugin.d.ts
  • module.d.ts

那麼該如何理解這兩個文件?npm

是否可以更改在統一規範的文件內?

全局接口、命名空間、模塊等聲明又有那些寫法來定義?該如何寫?

... 對於產生的這麼些問題,下面依次分析。

解惑

理解並改造 shims-xxx.d.ts

咱們知道,xxx.d.ts 的文件代表,其內部的一些聲明都爲全局的聲明,可以在項目各組件內都能獲取到。所以 Vue 生成的兩個 shims-xxx.d.ts 實際上是爲了代表,該兩文件爲 Vue 相關的全局聲明文件。

可是從項目管理來講,隨着引入的 npm 模塊增多(好比公司內部 npm 源上的不帶 types 的包),那麼模仿 Vue 的聲明文件寫法,外部聲明的文件也會愈來愈多,文件夾看起來就不是很舒服了。所以有沒有一種比較好的方法來解決文件過多的問題呢?

對於我來講,我更偏向將這些簡單的聲明維護在一個 .d.ts 文件內,正好官網也推薦維護在一個大的 module 內,所以咱們能夠維護一個 module.d.ts 來整體聲明全部的外部模塊。基於官方的例子,我作了兩個文件來管理外部模塊的聲明,分別是 module.d.tsdeclarations.d.ts。前者主要維護須要寫的比較詳細的外部模塊,後者主要維護簡寫模式的模塊(包括內部須要聲明的 .js 文件,兼容歷史遺留問題)。例如:

改造後的 module/index.d.ts

// This `declare module` is called ambient module, which is used to describe modules written in JavaScript.

// 添加 vue-clipboard2 的 Vue 插件聲明
declare module 'vue-clipboard2' {
  import { PluginFunction } from 'vue';
  const clipboard: PluginFunction<any>;
  // 定義默認導出的類型
  export default clipboard;
}

// 添加 fe-monitor-sdk 的 Vue 插件聲明
declare module 'fe-monitor-sdk' {
  import { PluginObject } from 'vue';
  // 定義解構的變量類型
  export const monitorVue: PluginObject<any>;
}

// 添加全部 .vue 文件的聲明
declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}
複製代碼

改造後的 module/declarations.d.ts

// Shorthand ambient modules, All imports from this shorthand module will have the any type.

declare module '@/cookie-set';
複製代碼

附加:對於 global 聲明可視狀況分類,好比通用的放在 global.d.ts,其他可視狀況(若是該類型比較多的話)按照對應類型分類,好比 table 的可所有放在 global-table.d.ts

全局聲明的寫法

另外一個一直比較疑惑的問題是全局聲明的寫法,好比模塊的「單文件單模塊聲明」的寫法「單文件多模塊合併聲明」的寫法不太同樣,「無導入的全局聲明文件」和「帶導入聲明的全局聲明文件」的寫法又有些不一樣,這裏我一一列出其可行的寫法以及其不一樣的緣由。

注:這裏的一些定義都是我的總結的便於記憶的說法,爲非標準定義。

單文件單模塊聲明

該文件支持兩種寫法,分別以下:

// 寫法一
declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}

// 寫法二
import Vue from 'vue';

declare module '*.vue' {
  export default Vue;
}
複製代碼

注: 前者(寫法一)主要爲無 ts 聲明的模塊添加聲明,後者(寫法二)主要爲已有 types 聲明的模塊進行聲明擴展(能夠參考 vue-router 源碼部分

單文件多模塊合併聲明

僅有一種寫法(須要關閉對應的屢次引入重複模塊的 lint 規則或者忽略此 types 文件夾內的全部內容)

declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}
複製代碼
無導入的全局聲明文件

無導入即沒有 import 聲明,直接定義全局接口、函數等

interface TableRenderParam extends BasicObject {
  row: BasicObject,
  key: string,
  index?: number,
}
複製代碼
帶導入聲明的全局聲明文件

帶有 import 導入插件聲明的必須顯示定義 global,例如:

import { CreateElement } from 'vue';

// function 部分
declare global {
  interface TableRenderFunc {
    (h: CreateElement, { row, key, index }: TableRenderParam): JSX.Element,
  }
}

// namespace 部分
declare global {}
複製代碼
不一樣的緣由

若是在「單文件多模塊合併聲明」將 import 提出至最頂層時,會發現 ts 報錯,說模塊沒法進一步擴大,爲何將 import 提出後會報錯提示模塊沒法擴大?

我的研究得出的結論是,當將 import 提出至模塊外時,就已經代表該文件內的其它 declare 的模塊已是存在 ts 聲明的模塊,此時再對其進行 declare 聲明即對其本來的聲明上進行擴展(可參考 vue-router 對於 vue 的擴展),可是對於沒有 ts 聲明的模塊,咱們拿不到它的 ts 聲明,所以也就沒發進行模塊擴展,因此就會報錯。

而將 import 放至模塊內時,由於 module 原本就代表本身爲一個模塊,其就能夠做爲模塊的聲明,爲沒有對應聲明的模塊添加聲明瞭。

此外,對於多個 declare global 的寫法,此是採用了**聲明合併**的方式,使得全部的模塊聲明都合併至同一個 global 全局聲明中,所以,在對於將 import 提至外層的「帶導入聲明的全局聲明文件」來講,分文件全局維護或者單文件聲明合併式維護都是可行的。

注:TypeScript 與 ECMAScript 2015 同樣,任何包含頂級 import 或者 export 的文件都被當成一個模塊。相反地,若是一個文件不帶有頂級的 import 或者 export 聲明,那麼它的內容被視爲全局可見的(所以對模塊也是可見的)。

項目遷移的其他 ts 問題

固然,在項目遷移過程當中遇到的問題還有不少,做爲附帶項,以供你們參考。

動態引入無 ts 聲明的文件

由於動態設置的 cookie 會隨測試機不一樣而不一樣,且不一樣人開發,其 cookie 也會變,所以須要將此文件清除 git 跟蹤並動態導入(線上不到入),同時得支持 .js/ts 的聲明。

原寫法:

// 對應 cookie-set 文件內判斷當前環境
import '@/cookie-set';
複製代碼

改造一:清除 git 跟蹤並提出環境判斷

// git 部分
git rm --cache <cookie-set file path>

// 文件部分採用動態引入
if (process.env,NODE_ENV === 'development') {
  import('@/cookie-set');
}
複製代碼

改造二:支持 js 文件 由於動態 import 須要 ts 聲明,由於沒有跟蹤文件,爲了支持 .js 文件,可在 declarations.d.ts 內添加簡單聲明

declare module '@/cookie-set';
複製代碼

引入的自家插件無 vue 插件聲明

最初的改造例子裏面又貼到過,爲了方便你們理解,我就暖心的再貼一次代碼,注意看更改後的註釋~

// 此適用於 import vueClipboard from 'vue-clipboard2';
declare module 'vue-clipboard2' {
  import { PluginFunction } from 'vue';
  const clipboard: PluginFunction<any>;
  export default clipboard;
}

// 此適用於 import { monitorVue } from 'fe-monitor-sdk';
declare module 'fe-monitor-sdk' {
  import { PluginObject } from 'vue';
  export const monitorVue: PluginObject<any>;
}
複製代碼

export 和 export default 可參考模塊部分

vue-router 的引用路徑問題

雖然 webpack 內配置了 alias,但那僅僅只是 webpack 打包時用的,ts 並不認帳,它有本身的配置文件,所以,咱們須要再兩個地方配置來解決此問題。首先須要配置 tsconfig.json 的 path 路徑

// tsconfig.json
path: [
  "@/*": [
    "src/*"
  ],
  // ...
]
複製代碼

另外一個是 ts 對於 vue 文件的引用必須添加 .vue 後綴,由於編輯器的緣由使得沒法識別 .vue 後綴(尤大大也有說,參考文檔有連接附加,可本身查),所以全部的 vue 文件的引用都須要補上 .vue 後綴。

vue 的 mixins 文件寫法

參考 ts 的 vue 入門文檔,改造以下

// 原來的寫法
export default {/**/}

// 當前的寫法
import Vue form 'vue';
export default Vue.extend({/**/})
複製代碼

注意,此部分的 computed 須要添加返回值類型,不然會報錯

關於 data 部分的聲明

這個坑比較隱蔽,折騰了好久才發現由於 data 爲函數,其內的對象爲返回值,由於並無採用 Class 風格寫法(中途接入 TS 改動太大,原有的文件保持原有結構),所以此部分的聲明應該這麼寫(我的推薦不用斷言):

data(): Your Interface here {
  return {};
}

// 或者
data() {
  return <Your assertions here> {};
}
複製代碼

VS Code experimentalDecorators 問題

根據警告來作相應配置,即在 tsconfig.json 內添加屬性:

"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 引入便可。

DefinitelyTyped 的說明
  • 若是你的模塊須要將新的名稱引入全局命名空間,那麼就應該使用全局聲明。
  • 若是你的模塊無需將新的名稱引入全局命名空間,那麼就應該使用模塊導出聲明。

拓展內容

namespace

TS 裏的 namespace 主要是解決命名衝突的問題,會在全局生成一個對象,定義在 namespace 內部的類都要經過這個對象的屬性訪問。對於內部模塊來講,儘可能使用 namespace 替代 module,可參考官方文檔。例如:

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

module

模塊可理解成 Vue 中的單個 vue 文件,它是以功能爲單位進行劃分的,一個模塊負責一個功能。其與 namespace 的最大區別在於:namespace 是跨文件的,module 是以文件爲單位的,一個文件對應一個 module。類比 Java,namespace 就比如 Java 中的包,而 module 則至關於文件。

參考文檔

相關文章
相關標籤/搜索