By: Kazehaiyahtml
因爲項目近期進行 ts 遷移,做爲第一個吃螃蟹的人,踩過了很多坑。遷移過程當中遇到的大大小小的問題基本上都解決了,可是對於 shims-vue.d.ts 文件的命名以及其內的模塊聲明始終找不到比較貼切的解釋。沉下心來讀了些外網資料,總算是有點「豁開雲霧見青天」的感受了。此處就記錄我對於 ts 全局模塊聲明的一些思考以及一些 ts 項目遷移遇到的坑。webpack
在安裝 @vue/typescript 以後,項目會生成兩個新文件,分別是 shims-vue.d.ts
和 shims-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
前者爲 Ambient Declarations(通稱:外部模塊定義) ,主要爲項目內全部的 vue 文件作模塊聲明,畢竟 ts 默認只識別 .d.ts、.ts、.tsx 後綴的文件;(即便補充了 Vue 得模塊聲明,IDE 仍是無法識別 .vue 結尾的文件,這就是爲何引入 vue 文件時必須添加後綴的緣由,不添加編譯也不會報錯)vue-router
後者爲 JSX 語法的全局命名空間,這是由於基於值的元素會簡單的在它所在的做用域裏按標識符查找(此處使用的是**無狀態函數組件 (SFC)**的方法來定義),當在 tsconfig 內開啓了 jsx 語法支持後,其會自動識別對應的 .tsx 結尾的文件,可參考官網 jsx。vue-cli
首先,官方文檔的上並無將 shims-xxx.d.ts 作爲通用的模板,其僅僅給咱們列舉了如下模板樣例:typescript
那麼該如何理解這兩個文件?npm
是否可以更改在統一規範的文件內?
全局接口、命名空間、模塊等聲明又有那些寫法來定義?該如何寫?
... 對於產生的這麼些問題,下面依次分析。
咱們知道,xxx.d.ts 的文件代表,其內部的一些聲明都爲全局的聲明,可以在項目各組件內都能獲取到。所以 Vue 生成的兩個 shims-xxx.d.ts 實際上是爲了代表,該兩文件爲 Vue 相關的全局聲明文件。
可是從項目管理來講,隨着引入的 npm 模塊增多(好比公司內部 npm 源上的不帶 types 的包),那麼模仿 Vue 的聲明文件寫法,外部聲明的文件也會愈來愈多,文件夾看起來就不是很舒服了。所以有沒有一種比較好的方法來解決文件過多的問題呢?
對於我來講,我更偏向將這些簡單的聲明維護在一個 .d.ts 文件內,正好官網也推薦維護在一個大的 module 內,所以咱們能夠維護一個 module.d.ts 來整體聲明全部的外部模塊。基於官方的例子,我作了兩個文件來管理外部模塊的聲明,分別是 module.d.ts
和 declarations.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 聲明,那麼它的內容被視爲全局可見的(所以對模塊也是可見的)。
固然,在項目遷移過程當中遇到的問題還有不少,做爲附帶項,以供你們參考。
由於動態設置的 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';
複製代碼
最初的改造例子裏面又貼到過,爲了方便你們理解,我就暖心的再貼一次代碼,注意看更改後的註釋~
// 此適用於 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 可參考模塊部分
雖然 webpack 內配置了 alias,但那僅僅只是 webpack 打包時用的,ts 並不認帳,它有本身的配置文件,所以,咱們須要再兩個地方配置來解決此問題。首先須要配置 tsconfig.json 的 path 路徑
// tsconfig.json
path: [
"@/*": [
"src/*"
],
// ...
]
複製代碼
另外一個是 ts 對於 vue 文件的引用必須添加 .vue 後綴,由於編輯器的緣由使得沒法識別 .vue 後綴(尤大大也有說,參考文檔有連接附加,可本身查),所以全部的 vue 文件的引用都須要補上 .vue
後綴。
參考 ts 的 vue 入門文檔,改造以下
// 原來的寫法
export default {/**/}
// 當前的寫法
import Vue form 'vue';
export default Vue.extend({/**/})
複製代碼
注意,此部分的 computed 須要添加返回值類型,不然會報錯
這個坑比較隱蔽,折騰了好久才發現由於 data 爲函數,其內的對象爲返回值,由於並無採用 Class 風格寫法(中途接入 TS 改動太大,原有的文件保持原有結構),所以此部分的聲明應該這麼寫(我的推薦不用斷言):
data(): Your Interface here {
return {};
}
// 或者
data() {
return <Your assertions here> {};
}
複製代碼
根據警告來作相應配置,即在 tsconfig.json 內添加屬性:
"experimentalDecorators": true
複製代碼
由於是裝飾器目前版本爲實驗性特性,可能在將來的發行版中發生變化,所以須要配置此參數來刪除警告。
關於類通常會採用 abstruct 抽象類來規範方法和屬性等類的細節,可是對於「類」中 static 部分沒法進行抽象規範,須要在對應靜態方法部分進行單獨處理,對於此部分有沒有比較好的處理方法(即能提取一個 interface 之類的聲明)存在疑問🤔。剛開始開發時留的此問題目前想到的比較靠譜的寫法有兩個。
官方文檔中也有說過,對於業務內的模塊來講,推薦使用 namespace 來作全局命名,所以對於業務內比較通用的公共方法來講,可使用 namespace 來處理。
對於多層命名空間的寫法,可用別名寫法
import NS = FirstNameSpace.SecondNameSpace
,而後直接經過NS.xxx
來直接取對應屬性便可。同時區別加載模塊時使用的import someModule = require('moduleName')
,此處的別名僅僅只是建立一個別名而已,簡化代碼量。
另外一種可用 ES6 的思想,import + export ,由於類中只有 static 方法,所以能夠認爲該類爲一個模塊,而一個模塊對應一個文件,所以做爲一個 ts 文件來存儲對應方法,須要時在 import 引入便可。
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
模塊可理解成 Vue 中的單個 vue 文件,它是以功能爲單位進行劃分的,一個模塊負責一個功能。其與 namespace 的最大區別在於:namespace 是跨文件的,module 是以文件爲單位的,一個文件對應一個 module。類比 Java,namespace 就比如 Java 中的包,而 module 則至關於文件。