Bootstrap

umi命令行工具源码解读,umi build打包

umi build为例,查看umi命令行工具的逻辑

在这里插入图片描述

首先查看package.json文件的bin字段,找到umi可执行文件的位置:

 "bin": {
    "umi": "./bin/umi.js"
  },

查看umi/bin/umi.js文件,实际逻辑是在umi/src/cli.js文件中,执行umi build

// umi/src/cli.js
switch (script) {
  case 'build':
  case 'dev':
  case 'test':
  case 'inspect':
  case 'ui':
    // eslint-disable-next-line import/no-dynamic-require
    // build进来  require('./scripts/build')
    require(`./scripts/${script}`);
    break;
  default: {
    const Service = require('umi-build-dev/lib/Service').default;
    new Service(buildDevOpts(args)).run(aliasMap[script] || script, args);
    break;
  }
}

build命令会动态导入umi/src/scripts/build.js

umi/src/scripts/build.js

import yParser from 'yargs-parser';
import buildDevOpts from '../buildDevOpts';

process.env.NODE_ENV = 'production';
process.env.UMI_UI = 'none';
// 设置两个环境变量

const args = yParser(process.argv.slice(2));
const Service = require('umi-build-dev/lib/Service').default;
new Service(buildDevOpts(args)).run('build', args);
// 生成一个service实例,整个打包的逻辑都在里面(包括注册各个插件,生成webpack配置,webpack打包),然后运行实例的run函数

先看生成实例的参数是buildDevOpts(args)buildDevOpts来自umi/src/buildDevOpts.js

export default function(opts = {}) {
  loadDotEnv(); // 解析.env,.env.local文件里的环境变量到process.env中

  let cwd = opts.cwd || process.env.APP_ROOT || process.cwd();
  if (cwd) {
    if (!isAbsolute(cwd)) {
      cwd = join(process.cwd(), cwd);
    }
    cwd = winPath(cwd);
    // 原因:webpack 的 include 规则得是 \ 才能判断出是绝对路径
    if (isWindows()) {
      cwd = cwd.replace(/\//g, '\\');
    }
  }

  // 返回项目的目录路径,上面也有针对windows路径作不同处理
  return {
    cwd,
  };
}

function loadDotEnv() {
  const baseEnvPath = join(process.cwd(), '.env');
  const localEnvPath = `${baseEnvPath}.local`;

  const loadEnv = envPath => {
    if (existsSync(envPath)) {
      const parsed = parse(readFileSync(envPath, 'utf-8'));
      Object.keys(parsed).forEach(key => {
        // eslint-disable-next-line no-prototype-builtins
        if (!process.env.hasOwnProperty(key)) {
          process.env[key] = parsed[key];
        }
      });
    }
  };

  loadEnv(baseEnvPath);
  loadEnv(localEnvPath);
}

再进入umi-build-dev/src/Service.js

首先看下Service的构造函数:

constructor({ cwd }) {
    //  用户传入的 cmd 不可信任 转化一下
    this.cwd = cwd || process.cwd();

    try {
      // 首先将项目的package.json文件放到实例的`pkg`变量中
      this.pkg = require(join(this.cwd, 'package.json')); // eslint-disable-line
    } catch (e) {
      this.pkg = {};
    }

    // babel编译所有的配置文件,防止有不兼容的语法
    registerBabel({
      cwd: this.cwd,
    });

    this.commands = {}; // 存放build,dev等命令函数,具体函数来自内置插件,umi-build-dev/src/plugins/commands
    this.pluginHooks = {}; // 存放打包时的一些钩子函数
    this.pluginMethods = {};
    this.generators = {}; // 存放umi g的模板生成器,生成页面文件等
    this.UmiError = UmiError;
    this.printUmiError = printUmiError;

    // 解析用户配置
    this.config = UserConfig.getConfig({
      cwd: this.cwd,
      service: this,
    });
    debug(`user config: ${JSON.stringify(this.config)}`);

    // 解析插件
    this.plugins = this.resolvePlugins();
    this.extraPlugins = [];
    debug(`plugins: ${this.plugins.map(p => p.id).join(' | ')}`);

    // 存储相关文件的具体路径
    this.paths = getPaths(this);
  }

看下registerBabel函数:

// umi-build-dev/src/registerBabel.js
import { join, isAbsolute } from 'path';
import { existsSync } from 'fs';
import registerBabel from 'af-webpack/registerBabel';
import { winPath } from 'umi-utils';
import { getConfigPaths } from 'umi-core/lib/getUserConfig';
import { uniq } from 'lodash';

let files = [];

function initFiles(cwd) {
  files = uniq(files.concat(getConfigPaths(cwd)));
}

export function addBabelRegisterFiles(extraFiles, { cwd }) {
  initFiles(cwd);
  files = uniq(files.concat(extraFiles));
}

export default function({ cwd }) {
  initFiles(cwd); // 获取所有的配置文件路径,存到files变量中
  const only = files.map(f => {
    const fullPath = isAbsolute(f) ? f : join(cwd, f);
    return winPath(fullPath);
  });

  let absSrcPath = join(cwd, 'src');
  if (!existsSync(absSrcPath)) {
    absSrcPath = cwd;
  }

    // 运行`af-webpack/registerBabel`函数转义所有的配置文件,防止配置文件中有不兼容的语法。
  registerBabel({
    // only suport glob
    // ref: https://babeljs.io/docs/en/next/babel-core.html#configitem-type
    only,
    babelPreset: [
      require.resolve('babel-preset-umi'),
      {
        env: { targets: { node: 8 } },
        transformRuntime: false,
      },
    ],
    babelPlugins: [
      [
        require.resolve('babel-plugin-module-resolver'),
        {
          alias: {
            '@': absSrcPath,
          },
        },
      ],
    ],
  });
}

af-webpack/registerBabel.js中使用@babel/register编译:

export default function registerBabel(opts = {}) {
  const { only, ignore, babelPreset, babelPlugins, disablePreventTest } = opts;
  if (disablePreventTest || process.env.NODE_ENV !== 'test') {
    require('@babel/register')({
      presets: [require.resolve('@babel/preset-typescript'), babelPreset],
      plugins: babelPlugins || [],
      only,
      ignore,
      extensions: ['.es6', '.es', '.jsx', '.js', '.mjs', '.ts', '.tsx'],
      babelrc: false,
      cache: false,
    });
  }
}

配置文件有这些,umi文档中也有讲:

export function getConfigPaths(cwd): string[] {
  const env = process.env.UMI_ENV;
  return [
    join(cwd, 'config/'),
    join(cwd, '.umirc.js'),
    join(cwd, '.umirc.ts'),
    join(cwd, '.umirc.local.js'),
    join(cwd, '.umirc.local.ts'),
    ...(env ? [join(cwd, `.umirc.${env}.js`), join(cwd, `.umirc.${env}.ts`)] : []),
  ];
}

再看解析插件resolvePluginsumi-build-dev/src/getPlugins.js

export default function(opts = {}) {
  const { cwd, plugins = [] } = opts;

  // 内置插件
  const builtInPlugins = [
    './plugins/commands/dev',
    './plugins/commands/build',
    './plugins/commands/inspect',
    './plugins/commands/test',
    './plugins/commands/help',
    './plugins/commands/generate',
    './plugins/commands/rm',
    './plugins/commands/config',
    ...(process.env.UMI_UI === 'none' ? [] : [require.resolve('umi-plugin-ui')]),
    './plugins/commands/block',
    './plugins/commands/version',
    './plugins/global-js',
    './plugins/global-css',
    './plugins/mountElementId',
    './plugins/mock',
    './plugins/proxy',
    './plugins/history',
    './plugins/afwebpack-config',
    './plugins/404', // 404 must after mock
    './plugins/targets',
    './plugins/importFromUmi',
  ];

    // 将内置插件和用户给的插件合并后返回
  const pluginsObj = [
    // builtIn 的在最前面
    ...builtInPlugins.map(p => {
      let opts;
      if (Array.isArray(p)) {
        /* eslint-disable prefer-destructuring */
        opts = p[1];
        p = p[0];
        /* eslint-enable prefer-destructuring */
      }
      const apply = require(p); // eslint-disable-line
      return {
        id: p.replace(/^.\//, 'built-in:'),
        apply: apply.default || apply, // apply方法执行对应的内置插件
        opts,
      };
    }),
    ...getUserPlugins(process.env.UMI_PLUGINS ? process.env.UMI_PLUGINS.split(',') : [], { cwd }),
    ...getUserPlugins(plugins, { cwd }),
  ];

  debug(`plugins: \n${pluginsObj.map(p => `  ${p.id}`).join('\n')}`);
  return pluginsObj;
}

构造函数完毕后,就会执行service.run函数:

 run(name = 'help', args) {
    this.init();
    return this.runCommand(name, args);
  }

name是命令的名称,此时是build,可以看到默认会执行umi help命令

this.init()

init() {
    // 加载环境变量
    this.loadEnv();

    // 初始化插件 这里很重要,会填充this.commands、this.pluginHooks、this.pluginMethods等对象,方便调用

    this.initPlugins();

    // 重新加载用户配置
    const userConfig = new UserConfig(this);
    const config = userConfig.getConfig({ force: true });
    mergeConfig(this.config, config);
    this.userConfig = userConfig;
    if (config.browserslist) {
      deprecate('config.browserslist', 'use config.targets instead');
    }
    debug('got user config');
    debug(this.config);

    // assign user's outputPath config to paths object
    if (config.outputPath) {
      const { paths } = this;
      paths.outputPath = config.outputPath;
      paths.absOutputPath = join(paths.cwd, config.outputPath);
    }
    debug('got paths');
    debug(this.paths);
  }

this.initPlugins只需要看最重要的部分:

plugins.forEach(plugin => {
    // 每个插件都调用initPlugin
  this.initPlugin(plugin);
  this.plugins.push(plugin);
  // reset count
  count = 0;
  initExtraPlugins();
});

this.initPlugin

initPlugin(plugin) {
    const { id, apply, opts } = plugin;
    try {
      assert(
        typeof apply === 'function',
        `
plugin must export a function, e.g.

  export default function(api) {
    // Implement functions via api
  }
        `.trim(),
      );
      // 生成PluginAPI实例,包含插件会使用到的一些公用方法
      const api = new Proxy(new PluginAPI(id, this), {
        get: (target, prop) => {
          if (this.pluginMethods[prop]) {
            return this.pluginMethods[prop];
          }
          if (
            [
              // methods
              'changePluginOption',
              'applyPlugins',
              '_applyPluginsAsync',
              'writeTmpFile',
              'getRoutes',
              'getRouteComponents',
              // properties
              'cwd',
              'config',
              'webpackConfig',
              'pkg',
              'paths',
              'routes',
              // error handler
              'UmiError',
              'printUmiError',
              // dev methods
              'restart',
              'printError',
              'printWarn',
              'refreshBrowser',
              'rebuildTmpFiles',
              'rebuildHTML',
            ].includes(prop)
          ) {
            if (typeof this[prop] === 'function') {
              return this[prop].bind(this);
            } else {
              return this[prop];
            }
          } else {
            return target[prop];
          }
        },
      });
      api.onOptionChange = fn => {
        assert(
          typeof fn === 'function',
          `The first argument for api.onOptionChange should be function in ${id}.`,
        );
        plugin.onOptionChange = fn;
      };
      // 执行插件函数
      apply(api, opts);
      plugin._api = api;
    } catch (e) {
      if (process.env.UMI_TEST) {
        throw new Error(e);
      } else {
        signale.error(
          `
Plugin ${chalk.cyan.underline(id)} initialize failed

${getCodeFrame(e, { cwd: this.cwd })}
        `.trim(),
        );
        debug(e);
        process.exit(1);
      }
    }
  }

initPlugin的最后会执行apply函数,以内置插件为例,apply就是运行内置插件的函数(上面有提到),查看build内置插件:

// umi-build-dev/src/plugins/commands/build/index.js
export default function(api) {
// api是pluginAPI类的实例
  const { service, debug, config, UmiError, printUmiError } = api;
  const { cwd, paths } = service;

    // 调用pluginApi实例的registerCommand方法,将build命令需要执行的函数方法放置到service.commands中,也就是下面的第三个参数
  api.registerCommand(
    'build',
    {
      webpack: true,
      description: 'building for production',
    },
    args => {
      const watch = args.w || args.watch;
      notify.onBuildStart({ name: 'umi', version: 2 });

      const RoutesManager = getRouteManager(service);
      RoutesManager.fetchRoutes();

      return new Promise((resolve, reject) => {
        process.env.NODE_ENV = 'production';
        service.applyPlugins('onStart');
        service._applyPluginsAsync('onStartAsync').then(() => {
          service.rebuildTmpFiles = () => {
            filesGenerator.rebuild();
          };
          service.rebuildHTML = () => {
            service.applyPlugins('onHTMLRebuild');
          };

          const filesGenerator = getFilesGenerator(service, {
            RoutesManager,
            mountElementId: config.mountElementId,
          });
          filesGenerator.generate();

          function startWatch() {
            filesGenerator.watch();
            service.userConfig.setConfig(service.config);
            service.userConfig.watchWithDevServer();
          }

          if (process.env.HTML !== 'none') {
            const HtmlGeneratorPlugin = require('../getHtmlGeneratorPlugin').default(service);
            // move html-webpack-plugin to the head, so that
            // other plugins (like workbox-webpack-plugin)
            // which listen to `emit` event can detect assets
            service.webpackConfig.plugins.unshift(new HtmlGeneratorPlugin());
          }
          service._applyPluginsAsync('beforeBuildCompileAsync').then(() => {
            require('af-webpack/build').default({
              cwd,
              watch,
              // before: service.webpackConfig
              // now: [ service.webpackConfig, ... ] , for ssr or more configs
              webpackConfig: [
                service.webpackConfig,
                ...(service.ssrWebpackConfig ? [service.ssrWebpackConfig] : []),
              ],
              // stats now is Array MultiStats
              // [ clientStats, ...otherStats ]
              onSuccess({ stats }) {
                debug('Build success');
                if (watch) {
                  startWatch();
                }
                if (process.env.RM_TMPDIR !== 'none' && !watch) {
                  debug(`Clean tmp dir ${service.paths.tmpDirPath}`);
                  rimraf.sync(paths.absTmpDirPath);
                }
                if (service.ssrWebpackConfig) {
                  // replace using manifest
                  // __UMI_SERVER__.js/css => umi.${hash}.js/css
                  const clientStat = Array.isArray(stats.stats) ? stats.stats[0] : stats;
                  if (clientStat) {
                    replaceChunkMaps(service, clientStat);
                  }
                }
                service.applyPlugins('onBuildSuccess', {
                  args: {
                    stats,
                  },
                });
                service
                  ._applyPluginsAsync('onBuildSuccessAsync', {
                    args: {
                      stats,
                    },
                  })
                  .then(() => {
                    debug('Build success end');

                    notify.onBuildComplete({ name: 'umi', version: 2 }, { err: null });
                    resolve();
                  });
              },
              // stats now is Array MultiStats
              // [ clientStats, ...otherStats ]
              onFail({ err, stats }) {
                service.applyPlugins('onBuildFail', {
                  args: {
                    err,
                    stats,
                  },
                });
                notify.onBuildComplete({ name: 'umi', version: 2 }, { err });
                printUmiError(
                  new UmiError({
                    message: err && err.message,
                    context: {
                      err,
                      stats,
                    },
                  }),
                  { detailsOnly: true },
                );
                reject(err);
              },
            });
          });
        });
      });
    },
  );
}

上面的api.registerCommand最后实际是调用的service的registerCommand方法:

registerCommand(name, opts, fn) {
    if (typeof opts === 'function') {
      fn = opts;
      opts = null;
    }
    opts = opts || {};
    assert(!(name in this.commands), `Command ${name} exists, please select another one.`);
    // 以build内置插件为例,将build需要执行的函数和参数放置到this.commands中
    this.commands[name] = { fn, opts };
  }

然后执行build命令,this.runCommand(name, args)

runCommand(rawName, rawArgs = {}, remoteLog) {
    debug(`raw command name: ${rawName}, args: ${JSON.stringify(rawArgs)}`);
    const { name, args } = this.applyPlugins('_modifyCommand', {
      initialValue: {
        name: rawName,
        args: rawArgs,
      },
    });
    debug(`run ${name} with args ${JSON.stringify(args)}`);

    const command = this.commands[name];
    if (!command) {
      signale.error(`Command ${chalk.underline.cyan(name)} does not exists`);
      process.exit(1);
    }

    const { fn, opts } = command; // 取出build命令的执行函数和参数
    if (opts.webpack) { // opts.webpack也是在调用api.registerCommand时配置的
      // webpack config 获取webpack配置,打包时会用到
      this.webpackConfig = require('./getWebpackConfig').default(this, {
        watch: rawArgs.w || rawArgs.watch,
      });
      if (this.config.ssr) {
        // when use ssr, push client-manifest plugin into client webpackConfig
        this.webpackConfig.plugins.push(
          new (require('./plugins/commands/getChunkMapPlugin').default(this))(),
        );
        // server webpack config
        this.ssrWebpackConfig = require('./getWebpackConfig').default(this, {
          ssr: this.config.ssr,
        });
      }
    }

	// 执行
    return fn(args, {
      remoteLog,
    });
  }
}

可以看到最后运行的build命令就是存放到this.commands中的,来看看build命令的执行函数:

// umi-build-dev/src/plugins/commands/build/index.js中api.registerCommand的第三个参数
args => {
      const watch = args.w || args.watch;
      notify.onBuildStart({ name: 'umi', version: 2 });

      const RoutesManager = getRouteManager(service);
      RoutesManager.fetchRoutes();

      return new Promise((resolve, reject) => {
        process.env.NODE_ENV = 'production';
        // appluPlugins执行的是service.pluginHooks里的钩子函数,这也是在生产pluginAPI实例时初始化的
        service.applyPlugins('onStart');
        service._applyPluginsAsync('onStartAsync').then(() => {
          service.rebuildTmpFiles = () => {
            filesGenerator.rebuild();
          };
          service.rebuildHTML = () => {
            service.applyPlugins('onHTMLRebuild');
          };

          const filesGenerator = getFilesGenerator(service, {
            RoutesManager,
            mountElementId: config.mountElementId,
          });
          filesGenerator.generate();

          function startWatch() {
            filesGenerator.watch();
            service.userConfig.setConfig(service.config);
            service.userConfig.watchWithDevServer();
          }

          if (process.env.HTML !== 'none') {
            const HtmlGeneratorPlugin = require('../getHtmlGeneratorPlugin').default(service);
            // move html-webpack-plugin to the head, so that
            // other plugins (like workbox-webpack-plugin)
            // which listen to `emit` event can detect assets
            service.webpackConfig.plugins.unshift(new HtmlGeneratorPlugin());
          }
          service._applyPluginsAsync('beforeBuildCompileAsync').then(() => {
          // 最关键内容,调用af-webpack/build进行打包
            require('af-webpack/build').default({
              cwd,
              watch,
              // before: service.webpackConfig
              // now: [ service.webpackConfig, ... ] , for ssr or more configs
              webpackConfig: [
                service.webpackConfig,
                ...(service.ssrWebpackConfig ? [service.ssrWebpackConfig] : []),
              ],
              // stats now is Array MultiStats
              // [ clientStats, ...otherStats ]
              onSuccess({ stats }) {
                debug('Build success');
                if (watch) {
                  startWatch();
                }
                if (process.env.RM_TMPDIR !== 'none' && !watch) {
                  debug(`Clean tmp dir ${service.paths.tmpDirPath}`);
                  rimraf.sync(paths.absTmpDirPath);
                }
                if (service.ssrWebpackConfig) {
                  // replace using manifest
                  // __UMI_SERVER__.js/css => umi.${hash}.js/css
                  const clientStat = Array.isArray(stats.stats) ? stats.stats[0] : stats;
                  if (clientStat) {
                    replaceChunkMaps(service, clientStat);
                  }
                }
                service.applyPlugins('onBuildSuccess', {
                  args: {
                    stats,
                  },
                });
                service
                  ._applyPluginsAsync('onBuildSuccessAsync', {
                    args: {
                      stats,
                    },
                  })
                  .then(() => {
                    debug('Build success end');

                    notify.onBuildComplete({ name: 'umi', version: 2 }, { err: null });
                    resolve();
                  });
              },
              // stats now is Array MultiStats
              // [ clientStats, ...otherStats ]
              onFail({ err, stats }) {
                service.applyPlugins('onBuildFail', {
                  args: {
                    err,
                    stats,
                  },
                });
                notify.onBuildComplete({ name: 'umi', version: 2 }, { err });
                printUmiError(
                  new UmiError({
                    message: err && err.message,
                    context: {
                      err,
                      stats,
                    },
                  }),
                  { detailsOnly: true },
                );
                reject(err);
              },
            });
          });
        });
      });
    }

其中最重要的就是调用af-webpack/build进行webpack打包,并且传入对应的webpack配置,在上面的代码42行

总结

整个打包过程的核心就是注册多个插件,而我们平时执行的build dev g 等命令都是位于umi-build-dev/src/plugins/commands/目录下的内置插件。

;