前言

在使用 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)

  1. 脚本模式 (Legacy)

    • 如果文件里没有 importexport,TS 认为它是脚本。
    • 脚本里定义的变量默认就是全局的。
    • 这是以前大家习惯直接写 declare const WeixinJSBridge 不需要套 global 的原因。
  2. 模块模式 (Modern)

    • 只要文件包含 任何 importexport(哪怕是 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 依然保持模块级作用域,不会重复污染全局

总结

在现代前端工程中,正确声明全局类型的口诀是:

  1. 文件要像模块:文件顶部加 export {}
  2. 类型要穿透:使用 declare global { ... } 包裹声明。
  3. 双重声明:既要在 interface Window 里加,也要单独 const 声明,这样才能同时支持 window.xxx 和直接调用 xxx

按照这个规范,你可以轻松声明 WeixinJSBridgeAlipayJSBridge 或任何第三方注入的全局对象,让代码既安全又有完善的类型提示。

始终使用模块化写法是保证类型系统健壮性的最佳实践。

分类: TypeScript 标签: 模块declare全局类型声明global

评论

暂无评论数据

暂无评论数据

目录