初识UmiJS 插件系统

172 阅读7分钟

1. 插件系统概述

UmiJS 的插件系统是框架的核心扩展机制,通过插件可以实现:

  • 编译时扩展:修改构建配置、添加文件、注册命令
  • 运行时扩展:增强应用功能、添加全局逻辑
  • 生态集成:无缝集成第三方库和工具

核心概念

image.png

2. 插件核心 API

2.1 插件注册 API

api.register - 注册全局钩子

api.register({
  key: 'modifyTitle',  // 全局钩子名称
  fn: (memo) => `${memo} - Umi Site`,  // 处理函数
  stage: 10,  // 执行顺序(数值小优先)
  before: 'specificHook' // 指定在特定钩子前执行
});

api.applyPlugins - 触发钩子

const result = api.applyPlugins({
  key: 'modifyTitle',
  type: api.ApplyPluginsType.modify, // 执行类型
  initialValue: '首页', // 初始值
  args: { /* 额外参数 */ } 
});

2.2 运行时扩展 API

api.addRuntimePluginKey - 声明运行时配置键

// 允许在 app.ts 中实现同名方法
api.addRuntimePluginKey('onThemeChange');

// src/app.ts
export function onThemeChange(theme) {
  console.log(`主题已切换: ${theme}`);
}

api.addRuntimePlugin - 添加运行时文件

// 添加运行时逻辑文件
api.addRuntimePlugin(() => './runtime.js');

// ./runtime.js
export function render(oldRender) {
  console.log('自定义渲染逻辑');
  oldRender();
}

2.3 元信息声明 API

api.describe - 声明插件元信息

api.describe({
  key: 'themePlugin',
  config: {
    schema(Joi) { // 配置校验
      return Joi.object({
        defaultTheme: Joi.string().default('light')
      });
    }
  },
  enableBy: api.EnableBy.config // 启用方式
});

3. 钩子系统的全局性与跨插件调用机制

3.1 钩子的全局性

所有通过 api.register() 注册的钩子都存储在全局钩子池中,与注册它的插件没有绑定关系。umi内置的钩子函数由umi本身控制在特定的生命周期执行。开发自定义的钩子方法不会自动执行,需要通过调用api.applyPlugins方法触发执行。

// 插件A
api.register({
  key: 'onDataReady',
  fn: (data) => { console.log('PluginA处理数据', data); }
});

// 插件B
api.register({
  key: 'onDataReady',
  fn: (data) => { console.log('PluginB处理数据', data); }
});

// 触发时两个函数都会执行
api.applyPlugins({ key: 'onDataReady', args: { value: 42 } });

3.2 钩子命名规则

  • 不强制唯一:同名钩子会形成执行链

  • 推荐命名空间plugin-name:hook-name 格式避免冲突

  • 执行顺序控制

    api.register({ key: 'hook', fn: fn1, stage: -10 }); // 先执行
    api.register({ key: 'hook', fn: fn2, stage: 0 });   // 后执行
    

3.3 跨插件调用机制

任何插件都可以调用全局钩子池中的钩子,包括Umi内置插件的钩子:

// 调用DVA插件的状态变更钩子
api.applyPlugins({
  key: 'dva:onStateChange',
  args: { type: 'CUSTOM_EVENT' }
});

3.4 注册内置钩子

因为UmiJS的钩子是全局的,并且允许同名钩子存在,它们会按照注册顺序(以及stage和before等控制)依次执行。所以我们可以通过注册内置钩子用于在特定的生命周期执行一些自定义逻辑。需要注意的是:

  1. 内置钩子通常由Umi核心或其他插件触发,自定义插件可以注册到这些钩子上以添加额外的逻辑。

  2. 注册到内置钩子时,需要了解该钩子的执行时机、参数和预期行为,避免破坏原有功能。

  3. 多个插件注册同一个钩子时,可以通过stagebefore/after来控制执行顺序。

// plugins/custom-render-plugin.js
export default (api) => {
  // 注册到 render 钩子
  api.register({
    key: 'render',
    fn: (oldRender) => {
      console.log('🔄 自定义插件开始处理渲染流程');
      
      // 自定义渲染前逻辑
      const startTime = Date.now();
      
      // 执行原始渲染方法
      // `render`钩子是一个特殊的钩子,它接收一个`oldRender`函数作为参数,并且必须在适当的时候调用这个函数,否则应用将无法渲染。
      oldRender();
      
      // 自定义渲染后逻辑
      const duration = Date.now() - startTime;
      console.log(`⏱️ 渲染完成,耗时 ${duration}ms`);
      
      // 返回新的渲染方法(可选)
      return function customRender() {
        console.log('✨ 执行自定义渲染');
        // 这里可以完全覆盖默认渲染逻辑
      };
    },
    stage: 100, // 确保在其他 render 钩子之后执行
    before: 'finalRender' // 可指定在特定钩子前执行
  });

  // 注册到其他内置钩子
  api.register({
    key: 'onRouteChange',
    fn: ({ location, routes }) => {
      console.log(`📍 路由变化: ${location.pathname}`);
      // 添加自定义路由处理逻辑
    }
  });
};

3.5 钩子类型与执行方式

类型执行方式特点适用场景
event并行执行无返回值传递通知类事件(如日志记录)
modify瀑布流传递修改结果配置修改链
compose串行组合组合结果路由收集、中间件组合

4. 运行时配置文件app.ts定义方法的执行时机

4.1 内置方法

UmiJS内置的运行时配置方法(如getInitialStatelayoutrequest等)是在Umi框架的特定生命周期阶段被调用的,每个方法都有其固定的执行时机。

方法名执行时机执行次数是否异步典型用途
getInitialState应用初始化时最早执行仅1次支持获取初始数据
layout在 getInitialState 之后仅1次同步修改布局配置
patchRoutes路由生成后、渲染前每次路由变化同步动态修改路由
render在路由匹配后、组件渲染前仅1次支持自定义渲染流程
onRouteChange每次路由变化时每次路由变化同步路由切换处理
request调用 useRequest 或 request 时按需异步统一请求处理

4.2 自定义方法

介绍api.addRuntimePluginKey时我们提到过,在app.ts文件中,除了能定义内置的生命周期方法供umi在特定生命周期调用,还允许定义通过api.addRuntimePluginKe注册的运行时钩子方法的具体实现。

  1. 注册为插件钩子: // 插件声明 api.addRuntimePluginKey('customMethod');

    // app.ts
    export function customMethod(param) {
      console.log('Custom method called:', param);
    }
    
  2. 在内置方法中调用: // app.ts export function onRouteChange() { myCustomHelper(); }

    function myCustomHelper() { /* ... */ }
    
  3. 通过initialState暴露: // app.ts export function getInitialState() { return { customMethod: (param) => { /* ... */ } }; }

    // 组件中调用
    const { initialState } = useModel('@@initialState');
    initialState.customMethod('test');
    
  4. api.applyPlugins直接调用: // 自定义插件 export default (api) => { // 声明运行时钩子 api.addRuntimePluginKey('customHook');

        // 插件中触发
        api.applyPlugins({
          key: 'customHook',
          args: { data: 'test' }
        });
    };
    
    // src/app.ts
    export function customHook(param) {
      console.log('Custom hook executed with:', param);
    }
    

4.3 执行顺序示例

// src/app.ts
export async function getInitialState() {
  console.log('1. getInitialState');
  return { user: null };
}

export function layout() {
  console.log('2. layout');
  return { title: 'App' };
}

// 自定义方法(不会自动执行)
export function customHelper() {
  console.log('Custom helper');
}

// 输出顺序:
// 1. getInitialState
// 2. layout

5. UmiJS 运行时插件 vs app.ts 运行时配置

5.1 核心相似点:功能一致性,两者都支持相同的生命周期方法

无论是 app.ts 还是通过 api.addRuntimePlugin 添加的文件,导出的生命周期方法都会在相同的时机被 Umi 调用。

// 以下方法在运行时插件文件和 app.ts 中都可以使用
export function getInitialState() {}
export function layout() {}
export function patchRoutes() {}
export function render() {}
export function onRouteChange() {}
export function request() {}
export function rootContainer() {}

5.2 两者都可以访问相同的运行时 API

// 两者都可以使用这些全局对象
history;          // 路由历史对象
plugin;           // 插件运行时 API
initialState;     // 全局初始状态

5.3关键区别:定位与使用场景

维度运行时插件文件 (api.addRuntimePlugin)app.ts 运行时配置
定位插件提供的运行时能力业务应用级的运行时配置
数量一个插件可添加多个,多个插件可添加多个每个项目只有一个
执行顺序按注册顺序执行(先于 app.ts)最后执行
编写者插件开发者业务开发者
主要用途封装插件核心运行时逻辑实现业务特定的运行时逻辑
可维护性随插件版本更新业务代码维护
访问业务代码有限访问(独立模块)完全访问业务模块

5.4 执行顺序图解

image.png

重要提示:所有运行时文件的导出方法会按注册顺序执行,最后执行 app.ts 中的方法

5.5 对比分析

5.5.1 功能范围:完全一致但上下文不同

运行时插件文件

// plugins/theme/runtime.js
import { history } from 'umi';

export function render(oldRender) {
  // 插件核心逻辑:主题初始化
  const theme = localStorage.getItem('theme') || 'light';
  applyTheme(theme);
  oldRender();
}

export function onRouteChange({ location }) {
  // 插件逻辑:路由监听
  if (location.query.theme) {
    applyTheme(location.query.theme);
  }
}

app.ts

// src/app.ts
import { useModel } from 'umi';

export function render(oldRender) {
  // 业务逻辑:用户认证检查
  if (!isAuthenticated()) {
    redirectToLogin();
  } else {
    oldRender();
  }
}

export function onRouteChange({ location }) {
  // 业务逻辑:页面访问统计
  trackPageView(location.pathname);
}

5.5.2 ### 执行顺序:插件先于业务

典型执行流程

image.png

5.5.3 访问权限:作用域差异

资源运行时插件文件app.ts
插件配置✅ 通过 api.config
业务组件❌ 无法直接导入✅ 可导入任何组件
业务工具函数
项目环境变量
Umi 运行时 API

5.6 如何选择

使用运行时插件文件 (api.addRuntimePlugin) 当:

  1. 需要封装可复用的运行时逻辑
  2. 逻辑是插件核心功能的一部分
  3. 需要确保逻辑在业务代码之前执行
  4. 需要与插件配置系统集成
  5. 逻辑应该与业务代码解耦

使用 app.ts 当:

  1. 实现业务特定的运行时逻辑
  2. 需要访问业务组件或工具函数
  3. 需要最后控制执行流程
  4. 需要快速原型开发而不想创建插件
  5. 需要完全控制运行时行为

6. 完整插件示例:主题切换插件

6.1 插件实现

// plugins/theme-plugin.js
export default (api) => {
  // 声明元信息
  api.describe({
    key: 'themePlugin',
    config: {
      schema(Joi) {
        return Joi.object({
          defaultTheme: Joi.string().default('light'),
          storageKey: Joi.string().default('app-theme')
        });
      }
    }
  });
  
  // 注册运行时钩子
  api.addRuntimePluginKey('onThemeChange');
  
  // 添加运行时文件
  api.addRuntimePlugin(() => './runtime.js');
  
  // 注册主题变更处理
  api.register({
    key: 'onThemeChange',
    fn: (theme) => {
      const { storageKey } = api.config.themePlugin;
      localStorage.setItem(storageKey, theme);
      document.documentElement.setAttribute('data-theme', theme);
    }
  });
};

6.2 运行时文件

// plugins/theme-plugin/runtime.js
import { history } from 'umi';

export function render(oldRender) {
  // 初始化主题
  const savedTheme = localStorage.getItem('theme') || 'light';
  applyTheme(savedTheme);
  oldRender();
}

export function onRouteChange({ location }) {
  // 路由参数切换主题
  const theme = new URLSearchParams(location.search).get('theme');
  if (theme) {
    window.g_plugin.applyPlugins({
      key: 'onThemeChange',
      args: theme
    });
  }
}

export function getInitialState() {
  return {
    changeTheme: (theme) => {
      window.g_plugin.applyPlugins({
        key: 'onThemeChange',
        args: theme
      });
    }
  };
}

6.3 业务集成

// src/app.ts
export function onThemeChange(theme) {
  console.log(`[App] 主题已切换: ${theme}`);
  // 发送统计事件
  analytics.track('theme_change', { theme });
}

// 组件中使用
function ThemeToggle() {
  const { initialState } = useModel('@@initialState');
  
  return (
    <button onClick={() => initialState?.changeTheme('dark')}>
      切换到深色模式
    </button>
  );
}

7. 总结

UmiJS 插件系统的强大之处在于其分层设计:

  1. 全局钩子池:解耦插件功能,支持跨插件协作
  2. 双运行时模式:插件文件提供基础能力,app.ts 实现业务定制
  3. 生命周期管理:内置方法在固定节点执行,自定义方法灵活触发
  4. 执行顺序控制:通过 stage 和 before 精细控制流程

image.png