最近遇到了一个不错的框架——umijs。现在前端开发的一大痛点就是,上手开发一个项目,上来就是一堆的配置,其中最重的可能就是webpack。umijs就是为了解决这个问题,希望能将开发者从无穷无尽的配置中解放出来,只关注业务代码。

优势在哪里?

umijs的优势如何体现呢?

➜  myapp tree 
.
└── pages
    ├── index.css
    ├── index.js
    ├── users.css
    └── users.js

只有核心的业务代码,没有任何配置信息,什么路由、什么webpack统统没有,但是这时我们直接执行

umi dev

就可以开始开发了。而这就是优势。当然这个并不是 umi 首创的,create-react-app 已经先行一步了,但是umi提供了一种比较通用的解决方案,值得期待,

架构图

umi架构图

总的来说,野心很大,希望一统江湖。

umijs的核心开发者云谦大佬说了,umijs的核心点有以下几点。

  • 路由
  • 插件
  • webpack
  • 约定优于配置

而核心中的重点,就是插件,而插件带来的最大的能力就是扩展性。而扩展性重要性是毋庸置疑的,这一点从koavscodewebpack等等知名的开源项目中都得到了体现。

插件

umi插件的核心原理就是深入到构建的整个过程中去,hook重要的时间节点,让插件能影响后续的构建流程,从而影响构建结果。

umi插件的标准写法如下:

export default (api, opts) => {
  // your plugin code here
};

插件初始化方法会收到两个参数,第一个参数 apiumi 提供给插件的接口都是通过它暴露出来的。第二个参数 opts 是用户在初始化插件的时候填写的。

API

其中 api 就是前面提到的整个构建过程中的重要时间节点。umi 将这些时间节点做了拆分,详细的可以参考这里 —— 插件开发。个人认为应该分为三个部分。

context

看下使用场景,简单就不多说了。

export default function(api, options = {}) {
  const { paths, findJS } = api; // 取出paths
  ......
  api.addEntryImport(() => {
    return {
      source: relative(
        paths.absTmpDirPath,   // 获取tmp目录绝对地址
        findJS(paths.absSrcPath, 'hd') ||  // 获取src绝对地址
          join(__dirname, '../template/index.js'),
      ),
    };
  });
}

功能函数

看下使用场景,实现都比较单一,不多说。

export default function(api, options = {}) {
  const { config, paths } = api;
  const { targets } = config;
  ......
  api.onOptionChange(newOpts => {
    options = newOpts;
    api.rebuildTmpFiles();  // 直接调用功能函数,重新创建临时文件
  });
}

生命周期hook

生命周期的 hook很好理解,在开启一个本地开发服务的时候,会有各个时机,就是要将这些时机能被插件 hook,进而做一些好玩的事情。

比如:

beforeDevServer:dev server 启动之前。

afterDevServer:dev server 启动之后。

onStart:umi dev 或者 umi build 开始时触发。

afterDevServer 这个看看,在 umi 中是怎么被触发的。

// https://github.com/umijs/umi/blob/master/packages/af-webpack/src/dev.js#L148
server.listen(port, HOST, err => {
  if (err) {
    console.log(err);
    return;
  }
  if (isInteractive) {
    clearConsole();
  }
  console.log(chalk.cyan('Starting the development server...\n'));
  send({ type: STARTING });
  if (afterServer) {
    afterServer(server); // server启动以后执行
  }
});

// https://github.com/umijs/umi/blob/master/packages/umi-build-dev/src/plugins/commands/dev/index.js#L119
afterServer(devServer) {
  service.applyPlugins('afterDevServer', { // 触发afterDevServer的hook,注意这里并不关心返回值
    args: { server: devServer },
  });
  startWatch();
}

除了以上这种事件类的 hook 之外,还有一种比较常见的 hookumi 称之为 应用类 api

事件类 hook 能被插件监听到事件发生,然后做一些处理,并不关心返回值。但是如何能让插件方便地将处理后的结果同步的返回给事件发出者,进而影响构建的结果呢?

应用类的 api 主要就是解决这个问题。

随便找一个 addHTMLMeta 看看具体是如何实现的。

// https://github.com/umijs/umi/blob/master/packages/umi-build-dev/src/html/HTMLGenerator.js#L300
getContent(route) {
  ......
  if (this.modifyMetas) metas = this.modifyMetas(metas, { route });  // 获取metas的时机
  ......
   // insert tags
    // insert tags
    html = html.replace(
      '<head>',
      `
<head>
${metas.length ? this.getMetasContent(metas) : ''}   // 将meta插入到页面中
${links.length ? this.getLinksContent(links) : ''}
${styles.length ? this.getStylesContent(styles) : ''}
    `.trim() + '\n',
    );
  ......
}



// https://github.com/umijs/umi/blob/master/packages/umi-build-dev/src/plugins/commands/getHtmlGenerator.js#L33
modifyMetas(memo, opts = {}) {
  const { route } = opts;
  // 在获取metas的时机调用 `addHTMLMeta api`,这个api不同于事件型api,是可以同步返回插件的处理结果的,进而会直接影响生成的html文件。
  return service.applyPlugins('addHTMLMeta', {
    initialValue: memo,
    args: { route },
  });
}

umi 提供了非常多类似的 api,这也是 umi 比较强大的地方,这些 api 保证了插件编写的便利性。

applyPlugins

上面提到了很多次applyPlugins函数,单独拿出来看看。自己编写的插件和系统内置的插件都会被 register,然后在 applyPlugins 的时候被集体按顺序执行。

applyPlugins(key, opts = {}) {
  debug(`apply plugins ${key}`);
  return (this.pluginHooks[key] || []).reduce((memo, { fn }) => {   // reduce函数,见下图
    try {
      return fn({
        memo,
        args: opts.args,
      });
    } catch (e) {
      console.error(chalk.red(`Plugin apply failed: ${e.message}`));
      throw e;
    }
  }, opts.initialValue);
}

reduce函数相信大家都不陌生。

reduce

如果 plugin 是基于事件类 api 写的,那就把所有的 plugin 都执行一遍就完事了。 如果 plugin 是基于应用类 api 写的,那也会把所有的 plugin,并将每次执行的结果 reduce 成最终结果返回给 applyPlugins 的点。

展望

个人还是比较好奇基于构建时期的 hook 做插件到底能 cover 多广的业务开发需求,如果遇到不能解决的,那岂不是有点头疼?

参考文档