TypeScript 进阶:如何在 Vite + Vue 3 中正确声明全局对象(以 WeixinJSBridge 为例)
前言
在使用 Vite + Vue 3 + TypeScript 开发微信 H5 页面时,我们经常遇到一个痛点:TypeScript 报错找不到 WeixinJSBridge。
// TS Error: Cannot find name 'WeixinJSBridge'.
WeixinJSBridge.invoke('getNetworkType', {}, () => {});
// TS Error: Property 'WeixinJSBridge' does not exist on type 'Window & typeof globalThis'.
window.WeixinJSBridge.on(...);很多同学可能会发现,以前直接创建一个.d.ts文件,写一个 declare const 就能用的方法,在现在的模块化项目中似乎不灵了,或者行为变得奇怪了。
本文将深入讲解其背后的原理(Script vs Module),并提供一套符合官方标准的最佳实践方案。
核心原理:为什么写法变了?
在 TypeScript 中,文件的处理模式分为两种:脚本(Script) 和 模块(Module)。
脚本模式 (Legacy):
- 如果文件里没有
import或export,TS 认为它是脚本。 - 脚本里定义的变量默认就是全局的。
- 这是以前大家习惯直接写
declare const WeixinJSBridge不需要套global的原因。
- 如果文件里没有
模块模式 (Modern):
- 只要文件包含 任何
import或export(哪怕是export {}),TS 就认为它是模块。 - 模块里的变量是私有的(Local Scope),除非显式导出,否则外界看不见。
- Vite/Vue3 项目默认为模块化环境。
- 只要文件包含 任何
结论:为了在模块化项目中“穿透”局部作用域去扩展全局变量,我们需要使用 declare global 语法块。这是 TypeScript 官方文档 推荐的“全局增强(Global Augmentation)”方案。
最佳实践步骤
我们将创建一个类型声明文件,同时支持 window.WeixinJSBridge 和直接调用 WeixinJSBridge。
第一步:创建声明文件
在 src 目录下新建 types/global.d.ts(或者 src/global.d.ts)。文件命名随意,你也可以取 weixin-js-bridge.d.ts 这种。
第二步:编写类型定义
请直接复制以下标准代码模板:
// src/types/global.d.ts
// 1. 确保这是一个模块。
// 如果你的文件里已经有了 import 语句,这行可以省略;
// 如果没有,必须加上 export {},否则这只是一个普通脚本文件,容易造成冲突。
export {};
// 2. 定义 WeixinJSBridge 的具体结构
// 这里只列出了常用方法,你可以根据微信文档继续补充
interface WeixinJSBridge {
invoke(
action: string,
data: Record<string, any>,
callback: (response: any) => void
): void;
on(
eventName: string,
callback: (response: any) => void
): void;
call(
action: string
): void;
}
// 3. 全局增强(核心部分)
declare global {
// 扩展 Window 接口:支持 window.WeixinJSBridge
interface Window {
WeixinJSBridge: WeixinJSBridge;
}
// 声明全局变量:支持直接使用 WeixinJSBridge
// 由于已经模块化,我们声明的全局变量需要写在global块内
const WeixinJSBridge: WeixinJSBridge;
}第三步:检查 TSConfig 配置
确保你的 tsconfig.json(或 tsconfig.app.json)能够读取到这个 .d.ts 文件。通常在 include 字段中配置:
{
"compilerOptions": {
// ...
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts", // 👈 确保包含 .d.ts 文件
"src/**/*.tsx",
"src/**/*.vue"
]
}如何使用
配置完成后,不需要任何 import,你可以在 .vue 或 .ts 文件中直接使用,且拥有完整的代码提示。
示例代码 (src/views/Home.vue):
<script setup lang="ts">
import { onMounted } from 'vue';
onMounted(() => {
// 场景 1:直接调用(TypeScript 现在知道它是一个全局变量)
// 建议加上 typeof 判断,防止在非微信环境下报错
if (typeof WeixinJSBridge !== 'undefined') {
WeixinJSBridge.invoke(
'getNetworkType',
{},
(res) => {
console.log('网络状态:', res.err_msg);
}
);
}
// 场景 2:通过 window 调用
if (window.WeixinJSBridge) {
window.WeixinJSBridge.on('menu:share:appmessage', () => {
console.log('用户点击了发送给朋友');
});
}
});
</script>为什么要使用模块化写法?
这里我们可能会有这样的疑问:既然以前直接写 declare const WeixinJSBridge 就能用,为什么还要改成这么复杂的模块化写法?
事实上,模块化写法(Module)相比传统的脚本写法(Script)有两个决定性的优势,特别是在大型项目中:
优势一:防止全局类型污染 (Namespace Pollution)
在定义复杂的全局对象时,我们通常需要定义一些辅助接口(Helper Interfaces)。
- 传统脚本写法:文件内定义的所有接口都会自动变成全局的,极易与其他库或业务代码产生命名冲突。
- 模块化写法:只有在
declare global块内的内容才是全局的,块外部定义的接口是私有的,互不干扰。
❌ 传统脚本写法 (隐患)
假设你在 types.d.ts 中为了定义 WeixinJSBridge 定义了一个 Config 接口:
// types.d.ts (Script模式)
// 😱 糟糕:这个 Config 接口自动变成了全局接口!
// 如果你的项目中其他地方(或者引入的第三方库)也有一个叫 Config 的接口
// TypeScript 会尝试合并它们,导致不可预知的类型错误。
interface Config {
appId: string;
}
declare const WeixinJSBridge: {
invoke(action: string, conf: Config): void;
}✅ 模块化写法 (安全)
// types/global.d.ts (Module模式)
export {}; // 变成模块
// 😎 安全:这个 Config 接口只是当前文件的局部变量
// 外部文件根本看不见它,完全不用担心和别人的 Config 冲突
interface Config {
appId: string;
}
declare global {
const WeixinJSBridge: {
// 在这里使用局部的 Config 类型
invoke(action: string, conf: Config): void;
};
}优势二:支持引用外部类型 (Imports)
这是模块化写法最强大的地方。当你定义的全局对象需要引用第三方库的类型时,传统写法会直接失效。
因为在 TypeScript 中,一旦文件中出现了 import 语句,该文件就会自动被视为“模块”。如果你没有使用 declare global,原本声明的全局变量就会瞬间变成“模块内私有变量”,导致外部无法访问。
❌ 传统写法遇到的死胡同
假设我们需要用 Vue 的 Ref 类型或者某种复杂的业务类型来定义全局对象:
// types.d.ts
// 一旦写了这句话,这个文件就强制变成了 Module!
import { SomeType } from './my-business-types';
// 😱 崩溃:
// 因为文件变成了 Module,下面的声明不再是全局的了!
// 变成了当前文件的私有导出,其他文件根本找不到 WeixinJSBridge。
declare const WeixinJSBridge: {
customMethod(data: SomeType): void;
}✅ 模块化写法 (完美解决)
使用模块化写法,我们可以随意 import 任何需要的类型,然后通过 declare global 显式暴露结果。
// types/global.d.ts
// 1. 随意引入外部类型
import { ComponentPublicInstance } from 'vue';
import { UserInfo } from '@/api/types';
// 2. 只有这里面的内容会暴露给全局
declare global {
interface Window {
// 可以在全局定义中使用引入的类型
currentUser: UserInfo;
vm: ComponentPublicInstance;
}
const WeixinJSBridge: any;
}
// 3. 这里的 UserInfo 依然保持模块级作用域,不会重复污染全局总结
在现代前端工程中,正确声明全局类型的口诀是:
- 文件要像模块:文件顶部加
export {}。 - 类型要穿透:使用
declare global { ... }包裹声明。 - 双重声明:既要在
interface Window里加,也要单独const声明,这样才能同时支持window.xxx和直接调用xxx。
按照这个规范,你可以轻松声明 WeixinJSBridge、AlipayJSBridge 或任何第三方注入的全局对象,让代码既安全又有完善的类型提示。
始终使用模块化写法是保证类型系统健壮性的最佳实践。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据