以
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`)] : []),
];
}
再看解析插件resolvePlugins
,umi-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/
目录下的内置插件。