UMI源码学习【1】——从umi dev到启动流程

10 分钟
UMI

现在工作用的框架以及脚手架主要都是基于umi开发的,学习一下umi源码还是很有必要的,空闲之余学习了一下umi源码

以下分析完全基于自己理解, 基于umi 3.0版本,如有错误,还请斧正

UMI源码学习系列
UMI源码学习【1】——从umi dev到启动流程
UMI源码学习【2】——插件机制

个人认为,学习源码要从入口开始一步一步向下摸索,就像自己平时用umi工程,启动的时候运行下面的命令,我就会很好奇 umi dev之后的流程,然后顺着这个流程往下,其实整个umi的流程就大致熟悉了,然后再对每一个细小的点深入研究一下。

cross-env UMI_ENV=ronghe_dev WATCH_IGNORED=none umi dev

加载cli文件

umi入口就是加载了umi.js,其中加载了cli.js

new Service

cli中很操作简单,核心就是new Service();根据环境变量,开发模式以及生产模式有点区别,生产模式直接new Service();开发模式加载forkedDev,在forkedDev中依旧是new Service(),然后加了一些退出时触发插件中的onExit事件。

(async () => {
  try {
    switch (args._[0]) {
      case 'dev':
        const child = fork({
          scriptPath: require.resolve('./forkedDev'),
        });
        // ref:
        // http://nodejs.cn/api/process/signal_events.html
        process.on('SIGINT', () => {
          child.kill('SIGINT');
        });
        process.on('SIGTERM', () => {
          child.kill('SIGTERM');
        });
        break;
      default:
        const name = args._[0];
        if (name === 'build') {
          process.env.NODE_ENV = 'production';
        }
        await new Service({
          cwd: getCwd(),
        }).run({
          name,
          args,
        });
        break;
    }
  } catch (e) {
    console.error(chalk.red(e.message));
    console.error(e.stack);
    process.exit(1);
  }
})();

其中Service类来自这个文件

import { Service } from './ServiceWithBuiltIn';

在ServiceWithBuiltIn中,集成了了核心类@umijs/core中的service,且引入了一些预设以及插件,我们的dev命令就是在@umijs/preset-built-in中进行注册的。
@umijs/preset-built-in中准备了许多的插件这个后面细讲,很重要。

import { dirname, join } from 'path';
import { IServiceOpts, Service as CoreService } from '@umijs/core';

class Service extends CoreService {
  constructor(opts: IServiceOpts) {
    process.env.UMI_VERSION = require('../package').version;
    process.env.UMI_DIR = dirname(require.resolve('../package'));

    super({
      ...opts,
      presets: [
        require.resolve('@umijs/preset-built-in'),
        ...(opts.presets || []),
      ],
      plugins: [require.resolve('./plugins/umiAlias'), ...(opts.plugins || [])],
    });
  }
}

export { Service };

Service

service.js是整个umi源码的核心这个文件要反复多看几遍

上面可以看到主要就是new Service(),看一下service对象主要有那些功能

源码位置:core/src/Service/Service.js中

service主要操作这里不细说,介绍一下大概,后面细说每一个模块。
service加载环境变量、加载预设、加载插件核心就是加载插件。

dev命令

找到源码中preset-built-in

dev命令其实就是注册了一个插件,主要操作就是启动开发服务器,产生一些文件(.umi中文件),监听文件变化重启服务器。

export default (api: IApi) => {
  const {
    env,
    paths,
    utils: { chalk, portfinder },
  } = api;

  api.registerCommand({
    name: 'dev',
    description: 'start a dev server for development',
    fn: async function({ args }) {
      const defaultPort =
        process.env.PORT || args?.port || api.config.devServer?.port;
      port = await portfinder.getPortPromise({
        port: defaultPort ? parseInt(String(defaultPort), 10) : 8000,
      });
      console.log(chalk.cyan('Starting the development server...'));
      process.send?.({ type: 'UPDATE_PORT', port });

      cleanTmpPathExceptCache({
        absTmpPath: paths.absTmpPath!,
      });
      const watch = process.env.WATCH !== 'none';

      // generate files
      const unwatchGenerateFiles = await generateFiles({ api, watch });
      if (unwatchGenerateFiles) unwatchs.push(unwatchGenerateFiles);

      // watch config change
      if (watch) {
        const unwatchConfig = api.service.configInstance.watch({
          userConfig: api.service.userConfig,
          onChange: async ({ pluginChanged, userConfig, valueChanged }) => {
            if (pluginChanged.length) {
              api.restartServer();
            }
            if (valueChanged.length) {
              let reload = false;
              let regenerateTmpFiles = false;
              const fns: Function[] = [];
              valueChanged.forEach(({ key, pluginId }) => {
                const { onChange } = api.service.plugins[pluginId].config || {};
                if (onChange === api.ConfigChangeType.regenerateTmpFiles) {
                  regenerateTmpFiles = true;
                }
                if (!onChange || onChange === api.ConfigChangeType.reload) {
                  reload = true;
                }
                if (typeof onChange === 'function') {
                  fns.push(onChange);
                }
              });

              if (reload) {
                api.restartServer();
              } else {
                api.service.userConfig = api.service.configInstance.getUserConfig();

                // TODO: simplify, 和 Service 里的逻辑重复了
                // 需要 Service 露出方法
                const defaultConfig = await api.applyPlugins({
                  key: 'modifyDefaultConfig',
                  type: api.ApplyPluginsType.modify,
                  initialValue: await api.service.configInstance.getDefaultConfig(),
                });
                api.service.config = await api.applyPlugins({
                  key: 'modifyConfig',
                  type: api.ApplyPluginsType.modify,
                  initialValue: api.service.configInstance.getConfig({
                    defaultConfig,
                  }) as any,
                });

                if (regenerateTmpFiles) {
                  await generateFiles({ api });
                } else {
                  fns.forEach(fn => fn());
                }
              }
            }
          },
        });
        unwatchs.push(unwatchConfig);
      }

      // delay dev server 启动,避免重复 compile
      // https://github.com/webpack/watchpack/issues/25
      // https://github.com/yessky/webpack-mild-compile
      await delay(500);

      // dev
      const {
        bundler,
        bundleConfigs,
        bundleImplementor,
      } = await getBundleAndConfigs({ api, port });
      const opts: IServerOpts = bundler.setupDevServerOpts({
        bundleConfigs: bundleConfigs,
        bundleImplementor,
      });

      const beforeMiddlewares = await api.applyPlugins({
        key: 'addBeforeMiddewares',
        type: api.ApplyPluginsType.add,
        initialValue: [],
        args: {},
      });
      const middlewares = await api.applyPlugins({
        key: 'addMiddewares',
        type: api.ApplyPluginsType.add,
        initialValue: [],
        args: {},
      });

      const server = new Server({
        ...opts,
        compress: true,
        headers: {
          'access-control-allow-origin': '*',
        },
        proxy: api.config.proxy,
        beforeMiddlewares,
        afterMiddlewares: [
          ...middlewares,
          createRouteMiddleware({ api, sharedMap }),
        ],
        ...(api.config.devServer || {}),
      });
      const hostname =
        process.env.HOST || api.config.devServer?.host || '0.0.0.0';
      const listenRet = await server.listen({
        port,
        hostname,
      });
      return {
        ...listenRet,
        destroy,
      };
    },
  });
};

总结

至此通过顺着umi dev命令,我们大致了解了umi的整个启动流程

cross-env UMI_ENV=ronghe_dev WATCH_IGNORED=none umi dev

可以发现主要就是插件的注册, 核心文件就是service.ts,dev build help等命令就是注册了一个插件。后面会说的的路由、内置方法等都是以插件方式注册进来的。

service中插件是如何注册的?
serice中命令是如何注册的?
不同插件都可以使用的内置命令是如何累计执行的?

接下来直接进入umi 核心——插件机制


此文自动发布于:github issues