什么是UmiJS?
UMI是基于一种微内核架构方式,核心是只保留架构的核心功能,其他需求及服务以插件的形式载入,“即插即用,不用即走”,因而又称为“插件化架构”。简单来说就是"如无必要,勿增实体",只保留最精简、最核心的部分。
如上图所示,umi首先会加载用户的配置和插件,然后基于配置或者目录生成一份路由配置,再基于此路由配置,把JS/CSS源码和HTML完整的串联起来。
UmiJS的核心竞争力是什么?
插件机制
UMI本身并不负责任何的业务,开发人员可通过安装不同的插件实现不同的功能,从而实现了框架和业务的解耦。
- 插件即社区,总不可能一个人把所有的功能都写了。
- 插件用于满足不同人、场景的不同需求。
- 插件用于封装复杂逻辑,简化使用。
如上图所示,首先有三个入口,node的编译时、浏览器的运行时、UMI UI。UMI负责读取配置文件,收集用户配置,然后挂载插件,再去执行具体的命令。每个命令再去执行自己的事情,把整个生命周期串联起来。但是每个生命周期里面具体做的事情都是由插件去实现的。
- 插件不仅用于"编译时",还作用于"运行时"。
基于路由
除了配置式路由,UMI也支持约定式路由。可以把路由想象成一个个的页面,文件系统即路由,减少冗余配置。基于路由还可以去做页面标题的切换、代码的分割、按需编译等等。
配置式是对于现实的低头,而约定式是Umi希望走去的方向,因为他简洁优雅。通过“约定大于配置”的核心理念,让开发者可以更加集中精力于业务开发。
需要注意的是,满足以下任意规则的文件不会被注册为路由。
- 以
.或_开头的文件或目录 - 以
d.ts结尾的类型定义文件 - 以
test.ts、spec.ts、e2e.ts结尾的测试文件(适用于.js、.jsx和.tsx文件) components和component目录utils和util目录- 不是
.js、.jsx、.ts或.tsx文件 - 文件内容不包含 JSX 元素
HTML是一等公民
首先在最下面的依赖层保证技术是不落伍的。在实现层除了有UMI之外,还有插件层,区块层。输出层是通过命令行进行输出。最后通过HTML去打通各种应用场景。
区块市场
- 可以从0搭建一个ant-design-pro。例如:先去选择一个整体的布局,再去选择一些页面,像列表页面、图表页面等等。
插件集
@umijs/preset-react
针对 react 应用的插件集。
包含:
- plugin-access,权限管理
- plugin-analytics,统计管理
- plugin-antd,整合 antd UI 组件
- plugin-crossorigin,通常用于 JS 出错统计
- plugin-dva,整合 dva
- plugin-helmet,整合 react-helmet,管理 HTML 文档标签(如标题、描述等)
- plugin-initial-state,初始化数据管理
- plugin-layout,配置启用 ant-design-pro 的布局
- plugin-locale,国际化能力
- plugin-model,基于 hooks 的简易数据流
- plugin-request,基于 umi-request 和 umi-hooks 的请求方案
UmiJS的工作原理
核心源码在core目录下的Config、Route及Service,而核心中的核心就是Service这个类,其他都是基于它进行的相关扩展与融合。
临时文件夹
.umi 临时目录是整个 Umi 项目的发动机,你的入口文件、路由等等都在这里,这些是由 umi 内部插件及三方插件生成的。
临时文件是 Umi 框架中非常重要的一部分,框架或插件会根据你的代码生成临时文件,这些原来需要放在项目里的脏乱差的部分都被藏在了这里。
├─ src
├─ .umi
├─ .cache
├─ babel-loader
├─ ...一些缓存文件
├─ core
├─ devScript.ts #开发脚本
├─ history.ts #历史对象
├─ plugin.ts #运行时插件
├─ pluginConfig.d.ts #配置插件
├─ pluginRegister.ts #注册插件
├─ polyfill.ts
├─ routes.ts #路由定义 umi会扫描pages目录,根据它的目录结构生成这样的文件
├─ umiExports.ts #导出
├─ umi.ts #入口文件
实现流程
Service核心代码
Service 代码位于 umi\packages\core\src\Service\Service.ts
Service 继承了 nodeJS events 模块提供的实例 EventEmitter,用于实现自定义事件。
export default class Service extends EventEmitter {
// 项目根路径
cwd: string;
// package.json的绝对路径
pkg: IPackage;
// 跳过的插件
skipPluginIds: Set<string> = new Set<string>();
// 生命周期执行阶段
stage: ServiceStage = ServiceStage.uninitialized;
// 注册命令
commands: {
[name: string]: ICommand | string;
} = {};
// 解析完的插件
plugins: {
[id: string]: IPlugin;
} = {};
// 插件方法
pluginMethods: {
[name: string]: Function;
} = {};
// 初始化插件预设
initialPresets: IPreset[];
initialPlugins: IPlugin[];
// 额外的插件预设
_extraPresets: IPreset[] = [];
_extraPlugins: IPlugin[] = [];
// 用户配置
userConfig: IConfig;
configInstance: Config;
config: IConfig | null = null;
// babel处理
babelRegister: BabelRegister;
// 钩子函数处理
hooksByPluginId: {
[id: string]: IHook[];
} = {};
hooks: {
[key: string]: IHook[];
} = {};
// 用户配置生成的路径信息
paths: {
// 项目根目录
cwd?: string;
// node_modules文件目录
absNodeModulesPath?: string;
// src目录
absSrcPath?: string;
// pages目录
absPagesPath?: string;
// dist导出目录
absOutputPath?: string;
// 生成的.umi目录
absTmpPath?: string;
} = {};
env: string | undefined;
ApplyPluginsType = ApplyPluginsType;
EnableBy = EnableBy;
ConfigChangeType = ConfigChangeType;
ServiceStage = ServiceStage;
args: any;
constructor(opts: IServiceOpts) {
super();
this.cwd = opts.cwd || process.cwd();
// 仓库根目录,antd pro构建的时候需要一个新的空文件夹
this.pkg = opts.pkg || this.resolvePackage();
this.env = opts.env || process.env.NODE_ENV;
// babel处理
this.babelRegister = new BabelRegister();
// 加载环境变量
this.loadEnv();
// 获取用户配置
this.configInstance = new Config({
cwd: this.cwd,
service: this,
localConfig: this.env === 'development',
});
// 从.umirc.ts中获取内容
this.userConfig = this.configInstance.getUserConfig();
// 获取导出的配置
this.paths = getPaths({
cwd: this.cwd,
config: this.userConfig!,
env: this.env,
});
// 初始化插件
const baseOpts = {
pkg: this.pkg,
cwd: this.cwd,
};
// 初始化预设
this.initialPresets = resolvePresets({
...baseOpts,
presets: opts.presets || [],
userConfigPresets: this.userConfig.presets || [],
});
// 初始化插件
this.initialPlugins = resolvePlugins({
...baseOpts,
plugins: opts.plugins || [],
userConfigPlugins: this.userConfig.plugins || [],
});
// 初始化配置及插件放入babel注册中
this.babelRegister.setOnlyMap({
key: 'initialPlugins',
value: lodash.uniq([
...this.initialPresets.map(({ path }) => path),
...this.initialPlugins.map(({ path }) => path),
]),
});
}
// 设置生命周期
setStage(stage: ServiceStage) {
this.stage = stage;
}
// 解析package.json的文件
resolvePackage() {
try {
return require(join(this.cwd, 'package.json'));
} catch (e) {
return {};
}
}
// 加载环境
loadEnv() {
const basePath = join(this.cwd, '.env');
const localPath = `${basePath}.local`;
loadDotEnv(basePath);
loadDotEnv(localPath);
}
// 真正的初始化
async init() {
this.setStage(ServiceStage.init);
await this.initPresetsAndPlugins();
// 状态:初始
this.setStage(ServiceStage.initHooks);
// 注册了plugin要执行的钩子方法
Object.keys(this.hooksByPluginId).forEach((id) => {
const hooks = this.hooksByPluginId[id];
hooks.forEach((hook) => {
const { key } = hook;
hook.pluginId = id;
this.hooks[key] = (this.hooks[key] || []).concat(hook);
});
});
// 状态:插件已注册
this.setStage(ServiceStage.pluginReady);
// 执行插件
await this.applyPlugins({
key: 'onPluginReady',
type: ApplyPluginsType.event,
});
// 状态:获取配置信息
this.setStage(ServiceStage.getConfig);
// 拿到对应插件的默认配置信息
const defaultConfig = await this.applyPlugins({
key: 'modifyDefaultConfig',
type: this.ApplyPluginsType.modify,
initialValue: await this.configInstance.getDefaultConfig(),
});
// 将实例中的配置信息对应修改的配置信息
this.config = await this.applyPlugins({
key: 'modifyConfig',
type: this.ApplyPluginsType.modify,
initialValue: this.configInstance.getConfig({
defaultConfig,
}) as any,
});
// 状态:合并路径
this.setStage(ServiceStage.getPaths);
if (this.config!.outputPath) {
this.paths.absOutputPath = join(this.cwd, this.config!.outputPath);
}
// 修改路径对象
const paths = (await this.applyPlugins({
key: 'modifyPaths',
type: ApplyPluginsType.modify,
initialValue: this.paths,
})) as object;
Object.keys(paths).forEach((key) => {
this.paths[key] = paths[key];
});
}
async initPresetsAndPlugins() {
this.setStage(ServiceStage.initPresets);
this._extraPlugins = [];
while (this.initialPresets.length) {
await this.initPreset(this.initialPresets.shift()!);
}
this.setStage(ServiceStage.initPlugins);
this._extraPlugins.push(...this.initialPlugins);
while (this._extraPlugins.length) {
await this.initPlugin(this._extraPlugins.shift()!);
}
}
getPluginAPI(opts: any) {
const pluginAPI = new PluginAPI(opts);
[
'onPluginReady',
'modifyPaths',
'onStart',
'modifyDefaultConfig',
'modifyConfig',
].forEach((name) => {
pluginAPI.registerMethod({ name, exitsError: false });
});
return new Proxy(pluginAPI, {
get: (target, prop: string) => {
// 由于 pluginMethods 需要在 register 阶段可用
// 必须通过 proxy 的方式动态获取最新,以实现边注册边使用的效果
if (this.pluginMethods[prop]) return this.pluginMethods[prop];
if (
[
'applyPlugins',
'ApplyPluginsType',
'EnableBy',
'ConfigChangeType',
'babelRegister',
'stage',
'ServiceStage',
'paths',
'cwd',
'pkg',
'userConfig',
'config',
'env',
'args',
'hasPlugins',
'hasPresets',
].includes(prop)
) {
return typeof this[prop] === 'function'
? this[prop].bind(this)
: this[prop];
}
return target[prop];
},
});
}
async applyAPI(opts: { apply: Function; api: PluginAPI }) {
let ret = opts.apply()(opts.api);
if (isPromise(ret)) {
ret = await ret;
}
return ret || {};
}
// 初始化配置
async initPreset(preset: IPreset) {
const { id, key, apply } = preset;
preset.isPreset = true;
const api = this.getPluginAPI({ id, key, service: this });
// register before apply
this.registerPlugin(preset);
// TODO: ...defaultConfigs 考虑要不要支持,可能这个需求可以通过其他渠道实现
const { presets, plugins, ...defaultConfigs } = await this.applyAPI({
api,
apply,
});
// register extra presets and plugins
if (presets) {
assert(
Array.isArray(presets),
`presets returned from preset ${id} must be Array.`,
);
// 插到最前面,下个 while 循环优先执行
this._extraPresets.splice(
0,
0,
...presets.map((path: string) => {
return pathToObj({
type: PluginType.preset,
path,
cwd: this.cwd,
});
}),
);
}
// 深度优先
const extraPresets = lodash.clone(this._extraPresets);
this._extraPresets = [];
while (extraPresets.length) {
await this.initPreset(extraPresets.shift()!);
}
if (plugins) {
assert(
Array.isArray(plugins),
`plugins returned from preset ${id} must be Array.`,
);
this._extraPlugins.push(
...plugins.map((path: string) => {
return pathToObj({
type: PluginType.plugin,
path,
cwd: this.cwd,
});
}),
);
}
}
// 初始化插件
async initPlugin(plugin: IPlugin) {
const { id, key, apply } = plugin;
const api = this.getPluginAPI({ id, key, service: this });
// register before apply
this.registerPlugin(plugin);
await this.applyAPI({ api, apply });
}
getPluginOptsWithKey(key: string) {
return getUserConfigWithKey({
key,
userConfig: this.userConfig,
});
}
// 注册插件
registerPlugin(plugin: IPlugin) {
// 考虑要不要去掉这里的校验逻辑
// 理论上不会走到这里,因为在 describe 的时候已经做了冲突校验
if (this.plugins[plugin.id]) {
const name = plugin.isPreset ? 'preset' : 'plugin';
throw new Error(`\
${name} ${plugin.id} is already registered by ${this.plugins[plugin.id].path}, \
${name} from ${plugin.path} register failed.`);
}
this.plugins[plugin.id] = plugin;
}
isPluginEnable(pluginId: string) {
// api.skipPlugins() 的插件
if (this.skipPluginIds.has(pluginId)) return false;
const { key, enableBy } = this.plugins[pluginId];
// 手动设置为 false
if (this.userConfig[key] === false) return false;
// 配置开启
if (enableBy === this.EnableBy.config && !(key in this.userConfig)) {
return false;
}
// 函数自定义开启
if (typeof enableBy === 'function') {
return enableBy();
}
// 注册开启
return true;
}
// 判断函数:是否有插件
hasPlugins(pluginIds: string[]) {
return pluginIds.every((pluginId) => {
const plugin = this.plugins[pluginId];
return plugin && !plugin.isPreset && this.isPluginEnable(pluginId);
});
}
// 判断函数:是否有预设
hasPresets(presetIds: string[]) {
return presetIds.every((presetId) => {
const preset = this.plugins[presetId];
return preset && preset.isPreset && this.isPluginEnable(presetId);
});
}
// 真正的插件执行函数,基于promise实现
async applyPlugins(opts: {
key: string;
type: ApplyPluginsType;
initialValue?: any;
args?: any;
}) {
const hooks = this.hooks[opts.key] || [];
switch (opts.type) {
case ApplyPluginsType.add:
if ('initialValue' in opts) {
assert(
Array.isArray(opts.initialValue),
`applyPlugins failed, opts.initialValue must be Array if opts.type is add.`,
);
}
const tAdd = new AsyncSeriesWaterfallHook(['memo']);
for (const hook of hooks) {
if (!this.isPluginEnable(hook.pluginId!)) {
continue;
}
tAdd.tapPromise(
{
name: hook.pluginId!,
stage: hook.stage || 0,
// @ts-ignore
before: hook.before,
},
async (memo: any[]) => {
const items = await hook.fn(opts.args);
return memo.concat(items);
},
);
}
return await tAdd.promise(opts.initialValue || []);
case ApplyPluginsType.modify:
const tModify = new AsyncSeriesWaterfallHook(['memo']);
for (const hook of hooks) {
if (!this.isPluginEnable(hook.pluginId!)) {
continue;
}
tModify.tapPromise(
{
name: hook.pluginId!,
stage: hook.stage || 0,
// @ts-ignore
before: hook.before,
},
async (memo: any) => {
return await hook.fn(memo, opts.args);
},
);
}
return await tModify.promise(opts.initialValue);
case ApplyPluginsType.event:
const tEvent = new AsyncSeriesWaterfallHook(['_']);
for (const hook of hooks) {
if (!this.isPluginEnable(hook.pluginId!)) {
continue;
}
tEvent.tapPromise(
{
name: hook.pluginId!,
stage: hook.stage || 0,
// @ts-ignore
before: hook.before,
},
async () => {
await hook.fn(opts.args);
},
);
}
return await tEvent.promise();
default:
throw new Error(
`applyPlugin failed, type is not defined or is not matched, got ${opts.type}.`,
);
}
}
// 运行方法
async run({ name, args = {} }: { name: string; args?: any }) {
args._ = args._ || [];
if (args._[0] === name) args._.shift();
this.args = args;
await this.init();
this.setStage(ServiceStage.run);
await this.applyPlugins({
key: 'onStart',
type: ApplyPluginsType.event,
args: {
args,
},
});
return this.runCommand({ name, args });
}
// 运行命令
async runCommand({ name, args = {} }: { name: string; args?: any }) {
assert(this.stage >= ServiceStage.init, `service is not initialized.`);
args._ = args._ || [];
if (args._[0] === name) args._.shift();
const command =
typeof this.commands[name] === 'string'
? this.commands[this.commands[name] as string]
: this.commands[name];
assert(command, `run command failed, command ${name} does not exists.`);
const { fn } = command as ICommand;
return fn({ args });
}
}