// packages/core/src/garfish.ts export class Garfish extends EventEmitter2 { public running = false; public version = __VERSION__; public flag = __GARFISH_FLAG__; // A unique identifier public loader = new Loader(); public hooks = globalLifecycle(); public channel = new EventEmitter2(); public options = createDefaultOptions(); public externals: Record<string, any> = {}; public activeApps: Array<interfaces.App> = []; public plugins: interfaces.Plugins = {} as any; public cacheApps: Record<string, interfaces.App> = {}; public appInfos: Record<string, interfaces.AppInfo> = {};
// packages/core/src/garfish.ts usePlugin( plugin: (context: Garfish) => interfaces.Plugin, ...args: Array<any> ) { // ... // this指向是Garfish类 args.unshift(this); // 执行传入的plugin const pluginConfig = plugin.apply(null, args) as interfaces.Plugin; assert(pluginConfig.name, 'The plugin must have a name.');
// 如果没有注册过,则进行注册 if (!this.plugins[pluginConfig.name]) { this.plugins[pluginConfig.name] = pluginConfig; // Register hooks, Compatible with the old api this.hooks.usePlugin(pluginConfig); } else if (__DEV__) { warn('Please do not register the plugin repeatedly.'); } return this; }
args数组首位是Garfish自己,然后获取插件配置,前面我们提到了,插件最后的返回是一个对象,里面包含name、version、生命周期钩子等。然后plugin.apply()就是返回的这些配置,并且赋值给了pluginConfig。接下来就是注册逻辑了,如果之前没有注册过该plugin,则进行注册,就是key为plugin的name,value为具体的配置形式,放在this.plugins对象中,这个好理解,接下来是进行this.hooks.usePlugin(pluginConfig)操作,这个其实是用来注册生命周期的,看一下this.hooks是啥,再构造函数中,是这么初始化hooks的: // packages/core/src/garfish.ts public hooks = globalLifecycle(); 通过函数名,也应该能猜得到,全局生命周期的hooks。接着看globalLifeCycle实现:
1 2 3 4 5 6 7 8 9 10 11 12
// packages/core/src/lifecycle.ts export function globalLifecycle() { return new PluginSystem({ beforeBootstrap: new SyncHook<[interfaces.Options], void>(), bootstrap: new SyncHook<[interfaces.Options], void>(), beforeRegisterApp: new SyncHook<[interfaces.AppInfo | Array<interfaces.AppInfo>], void>(), registerApp: new SyncHook<[Record<string, interfaces.AppInfo>], void>(), beforeLoad: new AsyncHook<[interfaces.AppInfo], Promise<boolean | void> | void | boolean>(), afterLoad: new AsyncHook<[interfaces.AppInfo, interfaces.App], void>(), errorLoadApp: new SyncHook<[Error, interfaces.AppInfo], void>(), }); }
constructor(lifecycle: T) { /* lifecycle: { beforeBootstrap: new SyncHook<[interfaces.Options], void>(), bootstrap: new SyncHook<[interfaces.Options], void>(), beforeRegisterApp: new SyncHook<[interfaces.AppInfo | Array<interfaces.AppInfo>], void>(), registerApp: new SyncHook<[Record<string, interfaces.AppInfo>], void>(), beforeLoad: new AsyncHook<[interfaces.AppInfo], Promise<boolean | void> | void | boolean>(), afterLoad: new AsyncHook<[interfaces.AppInfo, interfaces.App], void>(), errorLoadApp: new SyncHook<[Error, interfaces.AppInfo], void>(), } */ this.lifecycle = lifecycle; this.lifecycleKeys = Object.keys(lifecycle); }
usePlugin(plugin: Plugin<T>) { assert(isPlainObject(plugin), 'Invalid plugin configuration.'); // Plugin name is required and unique const pluginName = plugin.name; assert(pluginName, 'Plugin must provide a name.');
if (!this.registerPlugins[pluginName]) { this.registerPlugins[pluginName] = plugin;
for (const key in this.lifecycle) { const pluginLife = plugin[key as string]; if (pluginLife) { // Differentiate different types of hooks and adopt different registration strategies this.lifecycle[key].on(pluginLife); } } } else if (__DEV__) { warn(`Repeat to register plugin hooks "${pluginName}".`); } }
removePlugin(pluginName: string) { assert(pluginName, 'Must provide a name.'); const plugin = this.registerPlugins[pluginName]; assert(plugin, `plugin "${pluginName}" is not registered.`);
for (const key in plugin) { this.lifecycle[key].remove(plugin[key as string]); } } }
this.setOptions(options); // Register plugins this.usePlugin(GarfishHMRPlugin()); this.usePlugin(GarfishPerformance()); if (!this.options.disablePreloadApp) { this.usePlugin(GarfishPreloadPlugin()); } options.plugins?.forEach((plugin) => this.usePlugin(plugin)); // Put the lifecycle plugin at the end, so that you can get the changes of other plugins this.usePlugin(GarfishOptionsLife(this.options, 'global-lifecycle'));
// packages/router/src/agentRouter.ts export const normalAgent = () => { // By identifying whether have finished listening, if finished listening, listening to the routing changes do not need to hijack the original event // Support nested scene const addRouterListener = function () { window.addEventListener(__GARFISH_BEFORE_ROUTER_EVENT__, function (env) { RouterConfig.routerChange && RouterConfig.routerChange(location.pathname); linkTo((env as any).detail); }); };
if (!window[__GARFISH_ROUTER_FLAG__]) { // Listen for pushState and replaceState, call linkTo, processing, listen back // Rewrite the history API method, triggering events in the call const rewrite = function (type: keyof History) { const hapi = history[type]; return function () { const urlBefore = window.location.pathname + window.location.hash; const stateBefore = history?.state; const res = hapi.apply(this as any, arguments); const urlAfter = window.location.pathname + window.location.hash; const stateAfter = history?.state;
const e = createEvent(type); (e as any).arguments = arguments;
const from = { ...fromRouterInfo, matched: deactiveApps, };
await toMiddleWare(to, from, beforeEach!);
// Pause the current application of active state if (current!.matched.length > 0) { await asyncForEach( deactiveApps, async (appInfo) => await deactive(appInfo, getPath(appInfo.basename, location.pathname)), ); }
// Within the application routing jump, by collecting the routing function for processing. // Filtering gar-router popstate hijacking of the router // In the switch back and forth in the application is provided through routing push method would trigger application updates // application will refresh when autoRefresh configuration to true const curState = window.history.state || {}; if ( eventType !== 'popstate' && (curState[__GARFISH_ROUTER_UPDATE_FLAG__] || autoRefreshApp) ) { callCapturedEventListeners(eventType); }
await asyncForEach(needToActive, async (appInfo) => { // Function using matches character and routing using string matching characters const appRootPath = getAppRootPath(appInfo); await active(appInfo, appRootPath); });
if (activeApps.length === 0 && notMatch) notMatch(location.pathname);
// packages/core/src/module/app.ts export class App { public appId = appId++; public display = false; public mounted = false; public esModule = false; public strictIsolation = false; public name: string; public isHtmlMode: boolean; public global: any = window; public appContainer: HTMLElement; public cjsModules: Record<string, any>; public htmlNode: HTMLElement | ShadowRoot; public customExports: Record<string, any> = {}; // If you don't want to use the CJS export, can use this public sourceList: Array<{ tagName: string; url: string }> = []; public appInfo: AppInfo; public hooks: interfaces.AppHooks; public provider: interfaces.Provider; public entryManager: TemplateManager; public appPerformance: SubAppObserver; /** @deprecated */ public customLoader: CustomerLoader;
private active = false; private mounting = false; private unmounting = false; private context: Garfish; private resources: interfaces.ResourceModules; // Environment variables injected by garfish for linkage with child applications private globalEnvVariables: Record<string, any>; // es-module save lifeCycle to appGlobalId,appGlobalId in script attr private appGlobalId: string;
// Performs js resources provided by the module, finally get the content of the export async compileAndRenderContainer() { // ... }
private canMount() { // ... }
// If asynchronous task encountered in the rendering process, such as triggering the beforeEval before executing code, // after the asynchronous task, you need to determine whether the application has been destroyed or in the end state. // If in the end state will need to perform the side effects of removing rendering process, adding a mount point to a document, // for example, execute code of the environmental effects, and rendering the state in the end. private stopMountAndClearEffect() { // ... }
// Calls to render do compatible with two different sandbox private callRender(provider: interfaces.Provider, isMount: boolean) { // ... }
// Call to destroy do compatible with two different sandbox private callDestroy(provider: interfaces.Provider, isUnmount: boolean) { // ... }
// Create a container node and add in the document flow // domGetter Have been dealing with private async addContainer() { // ... }
// Good provider is set at compile time const provider = await this.getProvider(); // Existing asynchronous functions need to decide whether the application has been unloaded if (!this.stopMountAndClearEffect()) return false;