umi 如何断点调试
clone 源代码到本地,修改.fatherrc.base.ts, 添加sourcemap: true,这样就可以愉快的debugger了;
umi 如何启动
umi dev
dev 也是一个插件;本质是实例化一个Server, 然后执行run方法,最后执行dev插件的command;(placeholder, 有时间后拓展)
umi 插件机制
核心:
packages/umi/src/service/service.ts
Service 继承自 CoreService(packages/core/src/service/service.ts);
实例化(引入内部预设插件集)
run(注册内部预设插件集, 依次触发modifyPaths,modifyTelemetryStorage,modifyAppData,onCheck,onStart, 执行dev命令)
那么触发的这些事件后续触发了什么操作呢?涉及到如下:
umi 插件如何订阅事件
例如onCheck,可以看到有很多预设插件有订阅该事件:
查看任意一个代码如下:
csharp
api.onCheck(() => {
// mako 仅支持 node 16+
// ref: https://github.com/umijs/mako/issues/300
checkVersion(16, `Node 16 is required when using mako.`);
});
可以看到是一个版本检查的逻辑;
那么这个api是如何注入的呢?
umi 插件如何注册与注入框架能力
- 通过Plugin.getPluginsAndPresets 实例化插件集为Plugin实例数组,过程中会在外层包裹apply
- 在service 的实例方法initPlugin中 实例化 PluginAPI 生成 pluginAPI, 并为pluginAPI 包裹一层代理,使其能够访问Service实例的部分方法与属性:
service/pluginAPI.ts
static proxyPluginAPI(opts: {
pluginAPI: PluginAPI;
service: Service;
serviceProps: string[];
staticProps: Record<string, any>;
}) {
return new Proxy(opts.pluginAPI, {
get: (target, prop: string) => {
if (opts.service.pluginMethods[prop]) {
return opts.service.pluginMethods[prop].fn;
}
if (opts.serviceProps.includes(prop)) {
// @ts-ignore
const serviceProp = opts.service[prop];
return typeof serviceProp === 'function'
? serviceProp.bind(opts.service)
: serviceProp;
}
if (prop in opts.staticProps) {
return opts.staticProps[prop];
}
// @ts-ignore
return target[prop];
},
});
}
- 将plugin API 注入 plugin;
ini
let ret = await opts.plugin.apply()(proxyPluginAPI);
可以看到PluginAPI 的实例和proxy 里 Service的实例并没有 onCheck, 那么它是哪来的呢?
其实是在servicePlugin 中注册的,它在所有内置插件实例化之前先实例化:
service/servicePlugin.ts
export default (api: PluginAPI) => {
[
'onCheck',
'onStart',
'modifyAppData',
'modifyConfig',
'modifyDefaultConfig',
'modifyPaths',
'modifyTelemetryStorage',
].forEach((name) => {
api.registerMethod({ name }); // PluginAPI 实例提供 registerMethod
});
};
typescript
registerMethod(opts: { name: string; fn?: Function }) {
assert(
!this.service.pluginMethods[opts.name],
`api.registerMethod() failed, method ${opts.name} is already exist.`,
);
this.service.pluginMethods[opts.name] = {
plugin: this.plugin,
fn:
opts.fn ||
// 这里不能用 arrow function,this 需指向执行此方法的 PluginAPI
// 否则 pluginId 会不会,导致不能正确 skip plugin
function (fn: Function | Object) {
// @ts-ignore
this.register({
key: opts.name,
...(lodash.isPlainObject(fn) ? (fn as any) : { fn }),
});
},
};
}
这样其他后面的插件就能使用api.onCheck来注册监听事件了;这种方式onChenck (也就是被代理出的this.service.pluginMethods.onCheck.fn) 底层是调用了内置key的regsiter 方法:
umi 如何触发事件
使用api.applyPlugins 触发事件:底层使用tapable来处理多个事件的触发顺序;
截取部分代码:(触发类型为event, 异步执行):
javascript
const tEvent = new AsyncSeriesWaterfallHook(['_']); // 创建一个异步串行流水钩子;虽是流水钩子,但是没有用到操作结果执行函数传递; 可以查看modify的处理,有串行参数,也就是第二个参数的入参
for (const hook of hooks) {
if (!this.isPluginEnable(hook)) continue;
tEvent.tapPromise(
{
name: hook.plugin.key,
stage: hook.stage || 0,
before: hook.before,
},
async () => {
const dateStart = new Date();
await hook.fn(opts.args);
hook.plugin.time.hooks[opts.key] ||= [];
hook.plugin.time.hooks[opts.key].push(
new Date().getTime() - dateStart.getTime(),
);
},
);
}
return tEvent.promise(1) as Promise<T>; // 开始执行,初始参数为1;
客户端插件机制
umi.ts 入口文件(默认没有修改的情况)的生成(如下代码揭示了入口文件的路径;初始值为umi.ts):
php
const entry = await api.applyPlugins({
key: 'modifyEntry',
initialValue: {
umi: join(api.paths.absTmpPath, 'umi.ts'),
},
})
packages/preset-umi/src/commands/dev/dev.ts 下发事件('onGenerateFiles')
packages/preset-umi/src/features/tmpFiles/tmpFiles.ts; 注册事件('onGenerateFiles'),触发时会调用多个applyPlugins来获取模板数据,并渲染模板(umi.tpl);
umi.ts 执行渲染的代码:
.umi/umi.ts
const render = () => {
// ....
return (pluginManager.applyPlugins({
key: 'render',
type: ApplyPluginsType.compose,
initialValue() {
const context = {
routes,
routeComponents,
pluginManager,
rootElement: contextOpts.rootElement || document.getElementById('root'),
publicPath,
runtimePublicPath,
history,
historyType,
basename,
callback: contextOpts.callback,
};
const modifiedContext = pluginManager.applyPlugins({
key: 'modifyClientRenderOpts',
type: ApplyPluginsType.modify,
initialValue: context,
});
return renderClient(modifiedContext);
},
}))()
}
render();
renderClient 里面有对应内置插件的调用,构成根元素的子组件:
renderer-react/src/browser.tsx
// 加载所有需要的插件
for (const key of UMI_CLIENT_RENDER_REACT_PLUGIN_LIST) {
rootContainer = opts.pluginManager.applyPlugins({
type: 'modify',
key: key,
initialValue: rootContainer,
args: {
routes: opts.routes,
history: opts.history,
plugin: opts.pluginManager,
},
});
}
modify相关的处理逻辑:
typescript
return hooks.reduce((memo: any, hook: Function | object) => {
assert(
typeof hook === 'function' || typeof hook === 'object',
`applyPlugins failed, all hooks for key ${key} must be function or plain object.`,
);
if (typeof hook === 'function') {
return hook(memo, args);
} else {
// TODO: deepmerge?
return { ...memo, ...hook };
}
}, initialValue);
内置插件:innerProvider -> i18nProvider -> accessProvider -> dataflowProvider -> outerProvider -> rootContainer;
嵌套顺序相反,最后一个在最外层;
其他
约定式路由如何生成:
获取routes在packages/preset-umi/src/features/appData/appData.ts 插件中;
------------------------------------------------------ 分割新 ------------------------------
后续待更;