Umi4插件开发 - 什么是 umi,一文看懂插件的执行机制

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

希望你阅读这个专栏的时候,是已经看过了我前一个专栏 Umi@4 开发实战教程,因为一些 umi 常识性的内容,我在之前的专栏中已经讲解过了,如果我记得我讲过,那我会选择性的忽略,如果我不记得我讲过,我可能会再复述一遍,如果你看到有重复的内容,或者我跳过了一部分你无法理解的内容,欢迎你通过评论区和我交流。

首先你知道什么是 umi 吗,真的不是把 umi + preset@umijs/maxant-design-pro等这些概念都当作是 umi。这很重要,因为你首先要明白你要写的插件,是为写的插件,比如你完全基于 umi 来开发插件,你就仅仅需要考虑,你的插件如何被加载,具备什么功能。而如果你是编写一个 umi + preset 的插件,那你还需要考虑多个插件之间是否需要相互影响和互相成就。比如官方的 access 就基于 initialState 插件的能力实现,而 initialState 插件又基于 models 插件。

谁才是真正的 umi?

举个简单的例子吧,我们如何形容什么是车?四个轮子能跑用于交通运输的东西我们称它是车。贴膜镀金我们还是叫它是车。引擎升级之后我们也称它是车。类似的我们把这个能跑的框架叫做 umi,它配套了一些好用的插件之后,我们还是称它是 umi。把这些好用的插件搞成插件集合捆绑在一起它还是 umi。不讲道理的话,其实 alita 也可以称之为 umi。

这也是很多新手刚开始接触 umi,存在认知差异上的困惑的原因。没点耐心或者没用过 umi 的就可以大声吐槽这是什么乱七八糟的玩意了。

为了减少这个系列文章的认知差异,我们将最简的不包含任何插件的这部分框架,叫做 umi,后续这个系列的所有关于 umi 的称呼均指代这个内容。

现在我们来创建一个带四个轮子能跑的 umi 吧。它将作为我们接触 umi 插件开发的第一个平台。

1、新建空白文件夹

你可以在任意的文件路径下,执行

$mkdir learn-for-umi-plugin
复制代码

2、初始化 npm 项目

$cd learn-for-umi-plugin
$npm init -y
复制代码

3、安装依赖

$pnpm i umi react react-dom
复制代码

4、新建 src 目录

$mkdir src
复制代码

5、新建 umi 页面

$npx umi g page index

Write: src/pages/index.tsx
Write: src/pages/index.less
复制代码

6、启动 umi

$npx umi dev

info  - Umi v4.0.11
ready - App listening at http://localhost:8000
event - [Webpack] Compiled in 593 ms (510 modules)
info  - [MFSU] buildDeps since cacheDependency has changed
wait  - [Webpack] Compiling...
event - [Webpack] Compiled in 53 ms (496 modules)
event - [MFSU] Compiled in 1136 ms (1137 modules)
info  - [MFSU] write cache
info  - [MFSU] buildDepsAgain
info  - [MFSU] skip buildDeps
复制代码

7、访问页面

访问控制台打印的连接,即可看到当前的 umi 页面

Page index
复制代码

你现在这个项目就是一个最简单的 umi 项目了,还有另一种方式确认你手头上的项目使用了多少插件,就是执行 umi plugin list

$npx umi plugin list
复制代码

如果使用到的插件都是来自 @umijs/preset-umi@umijs/core 那就说明这是一个最简的 umi 项目了。

一般给复现demo的时候,umi 开发人员期望的也是在这个demo的基础上复现。

什么是插件

Umi 的核心就在于它的插件机制。那什么是插件机制呢?如果你有 webpack 基础,那你应该知道 webpack 整个架构很大程度上是基于事件的,每个 webpack 插件基本上都是一组在编译阶段挂钩不同事件的监听器。webpack 在底层使用了一个叫做 tapable 的库来封装“发布-订阅”的实现。

tapable 提供了不同的“钩子”类(SyncBailHook、AsyncParallelHook等)来“钩起”具有一些额外丰富功能(例如拦截或跨侦听器集成)的事件。umi 的插件机制也是基于 tapable 实现的。

如果你听不懂上面的表达,也没有关系,以下我用一些伪代码来让你明白什么是插件,插件是如何运行的。当然,这并不是真正的插件运行机制,我只是为了便于理解做了大量的简化。

class Umi {
  constructor(plugins) {
    this.plugins = plugins;
  }
  init() {
    console.log('umi core init');
    this.plugins.map(i => {
      i?.init?.()
    })
  }
  generateFile() {
    console.log('umi core generateFile');
    this.plugins.map(i => {
      i?.generateFile?.()
    })
  }
  build() {
    console.log('umi core build');
    this.plugins.map(i => {
      i?.build?.()
    })
  }
  run() {
    this.init();
    this.generateFile();
    this.build();
  }
}
const plugin1 = {
  init:()=>{
    console.log('[plugin1] init')
  },
  build:()=>{
    console.log('[plugin1] build')
  }
}
const plugin2 = {
  generateFile:()=>{
    console.log('[plugin2] generateFile')
  },
  build:()=>{
    console.log('[plugin2] build')
  }
}
const plugins = [plugin1,plugin2]
const umiCore = new Umi(plugins);
umiCore.run();
复制代码

run 里面的内容就是定义好的生命周期,当执行 run 的时候,我们会按顺序执行 initgenerateFilebuild,并且在内部生命周期函数中,使用 plugins.map 来触发每个插件中定义的钩子函数。

想一想,上面的代码执行之后,会打印什么呢?可以复制上面的代码,在控制台中运行查看结果。

umi core init
[plugin1] init
umi core generateFile
[plugin2] generateFile
umi core build
[plugin1] build
[plugin2] build
复制代码

如果你理解了上面的代码,那加入一个自定义的 EventEmitter 来模拟 tapable 库的“发布-订阅”逻辑。 我们就得到了下面的代码。

class EventEmitter {
    constructor() {
        this.subscriptions = new Set();
    }
    emit = (val) => {
        for (const subscription of this.subscriptions) {
            subscription(val);
        }
    };

    useSubscription = (callback) => {
        function subscription(val) {
            if (callback) {
                callback(val);
            }
        }
        this.subscriptions.add(subscription);
    };
}

const initEmitter = new EventEmitter();
const buildEmitter = new EventEmitter();

class Umi {
    constructor(plugins) {
        plugins.map(i => i());
    }
    init() {
        console.log('umi core init');
        initEmitter.emit();
    }
    build() {
        console.log('umi core build');
        buildEmitter.emit()
    }
    run() {
        this.init();
        this.build();
    }
}

const plugin1 = () => {
    initEmitter.useSubscription(() => {
        console.log('[plugin1] init')
    })
    buildEmitter.useSubscription(() => {
        console.log('[plugin1] build')
    })
}

const plugin2 = () => {
    buildEmitter.useSubscription(() => {
        console.log('[plugin2] build')
    })
}

const plugins = [plugin1, plugin2]
const umiCore = new Umi(plugins);
umiCore.run();
复制代码

同样的复制上面的代码,在控制台中运行查看结果。

umi core init
[plugin1] init
umi core build
[plugin1] build
[plugin2] build
复制代码

在这次的生命周期函数中,使用 emit 来“发布”事件。在每个插件中使用 useSubscription 来“订阅”事件。当执行“发布”时,所有的“订阅”都将被响应。

以上为了说明仅仅展示了两个生命周期,在 umi 生态中,有一套更加完整丰富的生命周期,并且全部通过 umi 的 api 暴露给插件。

我将在下一次的更新中和你一起共建一个真正的 umi 插件,如果你对这个内容感兴趣,可以关注这个专栏:Umi 插件开发。如果你觉得这个文章对你有帮助,请点赞评论收藏支持我,并将这个文章分享给更多的朋友,文章的数据是我持续更新的动力。感谢。

猜你喜欢

转载自juejin.im/post/7129869654587080711
今日推荐