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 则至关于文件。