Skip to content

Commit

Permalink
feat(loader): custom fetch (#521)
Browse files Browse the repository at this point in the history
* feat(loader): custom fetch (#495 #518)

* test(loader): custom fetch

* feat(loader): loader plugin走同一plugin入口注册

* test(loader): 增加loader.fetch单测

* feat(loader): loader fetch响应response、loader plugin外层业务调用

* feat(loader): loader plugin name、lint

Co-authored-by: duanyuanping <duanyuanping@bytedance.com>
  • Loading branch information
duanyuanping and duanyuanping committed Jul 12, 2022
1 parent e457952 commit cff894e
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 26 deletions.
26 changes: 24 additions & 2 deletions dev/app-main/src/config.ts
@@ -1,4 +1,4 @@
import GarfishInstance from 'garfish';
import GarfishInstance, { interfaces } from 'garfish';
import { GarfishEsModule } from '@garfish/es-module';
import { store } from './store';
import { basename, localApps } from './constant';
Expand All @@ -8,6 +8,21 @@ type RunInfo = NonNullable<Parameters<typeof GarfishInstance.run>[0]>;

(window as any).__GARFISH_PARENT__ = true;

// 业务自定义garfish loader
export function GarfishLoader() {
return function (Garfish: interfaces.Garfish): interfaces.Plugin {
return {
name: 'garfish-loader',
bootstrap(options: interfaces.Options = {}) {
Garfish.loader.usePlugin(Object.assign(
{name: 'garfish-loader' },
options.loader
));
},
};
};
}

let defaultConfig: RunInfo = {
// 子应用的基础路径,默认值为 /,整个微前端应用的 basename。
// 设置后该值为所有子应用的默认值,若子应用 AppInfo 中也提供了该值会替换全局的 basename 值
Expand Down Expand Up @@ -57,7 +72,7 @@ let defaultConfig: RunInfo = {
},

// 插件列表
plugins: [GarfishEsModule()],
plugins: [GarfishEsModule(), GarfishLoader()],

// 开始加载子应用前触发该函数,支持异步函数,可以在该函数中执行异步操作,
// 当返回 false 时表示中断子应用的加载以及后续流程,所有子应用加载都会触发该函数的调用
Expand Down Expand Up @@ -118,6 +133,13 @@ let defaultConfig: RunInfo = {
onNotMatchRouter(path) {
// console.log('子应用路由未匹配', path);
},

// 业务自定义fetch
loader: {
async fetch(url) {
// return Response
}
},
};

// The test environment into
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/config.ts
Expand Up @@ -27,6 +27,7 @@ const filterAppConfigKeys: Record<
plugins: true,
autoRefreshApp: true,
onNotMatchRouter: true,
loader: true,
};

// `props` may be responsive data
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/interface.ts
Expand Up @@ -100,6 +100,7 @@ export namespace interfaces {

export interface GlobalLifecycle extends Partial<PluginLifecycle> {
customLoader?: CustomerLoader;
loader?: LoaderInterface.LoaderLifecycle;
}

export type AppLifecycle = Pick<
Expand Down
45 changes: 43 additions & 2 deletions packages/garfish/__tests__/run.spec.ts
@@ -1,4 +1,4 @@
import Garfish from '@garfish/core';
import Garfish, { interfaces } from '@garfish/core';
import { GarfishRouter } from '@garfish/router';
import { GarfishBrowserVm } from '@garfish/browser-vm';
import { mockStaticServer } from '@garfish/test-suite';
Expand Down Expand Up @@ -48,6 +48,21 @@ async function noRenderApp(container: Element) {
expect(appContainer).toHaveLength(0);
}

// 业务自定义garfish loader
export function GarfishLoader() {
return function (Garfish: interfaces.Garfish): interfaces.Plugin {
return {
name: 'garfish-loader',
bootstrap(options: interfaces.Options = {}) {
Garfish.loader.usePlugin(Object.assign(
{name: 'garfish-loader' },
options.loader
));
},
};
};
}

const mockBeforeLoad = jest.fn();
const mockAfterLoad = jest.fn();
const mockBeforeMount = jest.fn();
Expand All @@ -57,6 +72,11 @@ const mockAfterUnmount = jest.fn();
const mockBeforeEval = jest.fn();
const mockAfterEval = jest.fn();

const loaderFetchReturn = {
code: 'function() {}',
type: 'aplication/javascript',
}

describe('Core: run methods', () => {
let GarfishInstance: Garfish;

Expand All @@ -70,7 +90,7 @@ describe('Core: run methods', () => {
GarfishInstance.run({
basename: '/',
domGetter: '#container',
plugins: [GarfishRouter(), GarfishBrowserVm()],
plugins: [GarfishRouter(), GarfishBrowserVm(), GarfishLoader()],
apps: [
{
name: 'vue-app',
Expand All @@ -91,6 +111,18 @@ describe('Core: run methods', () => {
afterMount: mockAfterMount,
beforeUnmount: mockBeforeUnmount,
afterUnmount: mockAfterUnmount,
loader: {
fetch: async url => {
if (url === 'http://jest') {
return new Response(loaderFetchReturn.code, {
status: 200,
headers: {
'Content-Type': loaderFetchReturn.type,
},
})
}
},
},
});
});

Expand Down Expand Up @@ -152,4 +184,13 @@ describe('Core: run methods', () => {
GarfishInstance.router.push({ path: '/vue-app' });
await vueAppInDocument(container);
});

it('custom fetch', async () => {
const fetchResult = await GarfishInstance.loader.load({
scope: 'jest-loader-fetch',
url: 'http://jest',
});

expect(fetchResult.code).toBe(loaderFetchReturn.code);
});
});
8 changes: 8 additions & 0 deletions packages/hooks/__tests__/hooks.spec.ts
Expand Up @@ -108,6 +108,14 @@ describe('hooks', () => {
expect(i).toBe(3);
await hook1.emit();
expect(i).toBe(5);

// AsyncHook emit返回内容
const hook2 = new AsyncHook<unknown, number>();

hook2.on(() => Promise.resolve(1));

const returnValue = await hook2.emit();
expect(returnValue).toBe(1);
});

it('SyncWaterfallHook', () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/hooks/src/asyncHook.ts
Expand Up @@ -2,8 +2,8 @@ import { ArgsType, SyncHook } from './syncHook';

type CallbackReturnType = void | false | Promise<void | false>;

export class AsyncHook<T> extends SyncHook<T, CallbackReturnType> {
emit(...data: ArgsType<T>): Promise<void | false> {
export class AsyncHook<T, ExternalEmitReturnType = CallbackReturnType> extends SyncHook<T, CallbackReturnType | Promise<ExternalEmitReturnType>> {
emit(...data: ArgsType<T>): Promise<void | false | ExternalEmitReturnType> {
let result;
const ls = Array.from(this.listeners);
if (ls.length > 0) {
Expand All @@ -13,6 +13,8 @@ export class AsyncHook<T> extends SyncHook<T, CallbackReturnType> {
return false; // Abort process
} else if (i < ls.length) {
return Promise.resolve(ls[i++].apply(null, data)).then(call);
} else {
return prev;
}
};
result = call();
Expand Down
21 changes: 19 additions & 2 deletions packages/loader/src/index.ts
@@ -1,4 +1,4 @@
import { SyncHook, SyncWaterfallHook, PluginSystem } from '@garfish/hooks';
import { SyncHook, SyncWaterfallHook, PluginSystem, AsyncHook } from '@garfish/hooks';
import {
error,
__LOADER_FLAG__,
Expand All @@ -10,7 +10,7 @@ import { StyleManager } from './managers/style';
import { ModuleManager } from './managers/module';
import { TemplateManager } from './managers/template';
import { JavaScriptManager } from './managers/javascript';
import { request, copyResult, mergeConfig } from './utils';
import { getRequest, copyResult, mergeConfig } from './utils';
import { FileTypes, cachedDataSet, AppCacheContainer } from './appCache';

// Export types and manager constructor
Expand Down Expand Up @@ -47,6 +47,12 @@ export enum CrossOriginCredentials {
'use-credentials' = 'include',
}

type CustomFetchReturnType = Response | void | false;

export interface LoaderLifecycle {
fetch(name: string, config: RequestInit): void | false | Promise<CustomFetchReturnType>;
}

export class Loader {
public personalId = __LOADER_FLAG__;
public StyleManager = StyleManager;
Expand All @@ -67,6 +73,10 @@ export class Loader {
url: string;
requestConfig: ResponseInit;
}>('beforeLoad'),
fetch: new AsyncHook<
[string, RequestInit],
CustomFetchReturnType
>('fetch'),
});

private options: LoaderOptions; // The unit is "b"
Expand All @@ -93,6 +103,12 @@ export class Loader {
}
}

usePlugin(options?: LoaderLifecycle & { name: string; }) {
if (options) {
this.hooks.usePlugin(options);
}
}

loadModule(url: string) {
return this.load<ModuleManager>({
scope: 'modules',
Expand Down Expand Up @@ -154,6 +170,7 @@ export class Loader {
requestConfig,
});

const request = getRequest(this.hooks.lifecycle.fetch);
const loadRes = request(resOpts.url, resOpts.requestConfig)
.then(({ code, size, result, type }) => {
let managerCtor,
Expand Down
40 changes: 23 additions & 17 deletions packages/loader/src/utils.ts
@@ -1,24 +1,30 @@
import { error, parseContentType } from '@garfish/utils';
import { Manager, Loader } from './index';

export async function request(url: string, config: RequestInit) {
const result = await fetch(url, config || {});
// Response codes greater than "400" are regarded as errors
if (result.status >= 400) {
error(`"${url}" load failed with status "${result.status}"`);
}
const code = await result.text();
const type = result.headers.get('content-type') || '';
const size = Number(result.headers.get('content-size'));
const mimeType = parseContentType(type || '');
export function getRequest(customFetch: Loader['hooks']['lifecycle']['fetch']) {
return async function request(url: string, config: RequestInit) {
let result = await customFetch.emit(url, config || {});
if (!result || !(result instanceof Response)) {
result = await fetch(url, config || {});
}

return {
code,
result,
mimeType,
type,
size: Number.isNaN(size) ? null : size,
};
// Response codes greater than "400" are regarded as errors
if (result.status >= 400) {
error(`"${url}" load failed with status "${result.status}"`);
}
const code = await result.text();
const type = result.headers.get('content-type') || '';
const size = Number(result.headers.get('content-size'));
const mimeType = parseContentType(type || '');

return {
code,
result,
mimeType,
type,
size: Number.isNaN(size) ? null : size,
};
}
}

export function copyResult(result) {
Expand Down
2 changes: 1 addition & 1 deletion website/docs/api/run.md
Expand Up @@ -251,4 +251,4 @@ Garfish.run({
<ErrorUnmountApp />
### onNotMatchRouter
<OnNotMatchRouter />
<OnNotMatchRouter />

1 comment on commit cff894e

@vercel
Copy link

@vercel vercel bot commented on cff894e Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.