如何通过消除双重等待和DRY代码来重构?

How to refactor by eliminating double await and the DRY code?

我怀疑哪种策略是管理此 Web 应用程序中众多服务客户端的最佳策略。

就用户设备 RAM 和 Javascript 执行速度(主线程操作)之间的良好折衷而言,“最佳”。

这就是我现在正在做的,这是主文件:

import type { PlayerServiceClient } from './player.client';
import type { TeamServiceClient } from './team.client';
import type { RefereeServiceClient } from './referee.client';
import type { FriendServiceClient } from './friend.client';
import type { PrizeServiceClient } from './prize.client';
import type { WinnerServiceClient } from './winner.client';
import type { CalendarServiceClient } from './calendar.client';

let playerService: PlayerServiceClient;
export const player = async (): Promise<PlayerServiceClient> =>
    playerService ||
    ((playerService = new (await import('./player.client')).PlayerServiceClient()),
    playerService);

let teamService: TeamServiceClient;
export const getTeamService = (): TeamServiceClient =>
    teamService ||
    ((teamService = new (await import('./team.client')).TeamServiceClient()),
  teamService);

let refereeService: RefereeServiceClient;
export const getRefereeService = (): RefereeServiceClient =>
    refereeService ||
  ((refereeService = new (await import('./referee.client')).RefereeServiceClient()),
  refereeService);

let friendService: FriendServiceClient;
export const getFriendService = (): FriendServiceClient =>
    friendService ||
  ((friendService = new (await import('./friend.client')).FriendServiceClient()),
  friendService);

let prizeService: PrizeServiceClient;
export const getPrizeService = (): PrizeServiceClient =>
    prizeService ||
  ((prizeService = new (await import('./prize.client')).PrizeServiceClient()),
  prizeService);

let winnerService: WinnerServiceClient;
export const getWinnerService = (): WinnerServiceClient =>
    winnerService ||
  ((winnerService = new (await import('./winner.client')).WinnerServiceClient()),
  winnerService);

let calendarService: CalendarServiceClient;
export const getCalendarService = (): CalendarServiceClient =>
    calendarService ||
  ((calendarService = new (await import('./calendar.client')).CalendarServiceClient()),
  calendarService);

// and so on... a lot more...

如您所见,有许多个服务客户。

我正在使用此代码,因为我认为考虑到我的 Web 应用程序结构基于几乎与客户端服务重叠的路由,它更好:

我的意思是,如果播放器从 /home 转到 /players 页面,我可以这样使用它:

import { getPlayerService } from "main";

const playerService = await getPlayerService();
const players = await playerService.queryPlayers();

这样,如果PlayerService不存在,则此时导入并返回,否则为returns之前导入并实例化的

既然用户经常切换页面,这样我就可以避免那些客户端的突然创建和销毁,对吧?

但是通过这种方式,我使用了我不喜欢使用的 全局变量,并且我使用的是 冗长、枯燥和冗长的代码 在每个组件中。

有没有办法在组件中使用下面的代码?

import { playerService } from "main";

const players = await playerService.queryPlayers();

你建议我做什么?

您正在实施的模式是“延迟加载”和“单例”。

您可以拥有一个实现这些模式并将其用于每个服务的服务工厂:

文件serviceFactory.js

const serviceMap = {};

export function getService(serviceName) {
  return serviceMap[serviceName] ?? (serviceMap[serviceName] = import(serviceName).then(x => new x.default));
}

ECMAScript 模块标准将负责在应用程序中仅执行一次 serviceFactory.js 代码(无论您导入它多少次),因此您可以将单例保存在分配给私有对象的映射中serviceFactory.js 模块的顶级变量。

此服务工厂意味着每个服务都使用 default 关键字导出,如下所示:

export default class SomeService {
    constructor() {
        // ...
    }

    fetchSomething() {
        // ...
    }
}

然后,使用以下代码在您的应用程序中随处使用服务:

import { getService } from './serviceFactory.js';

const service = await getService('./services/some.service.js');
const something = await service.fetchSomething();

如果你真的想去掉doubleawait,你可以这样封装在服务工厂中:

const serviceMap = {};

export function getService(serviceName) {
  return serviceMap[serviceName] ?? (serviceMap[serviceName] = resolveService(serviceName));
}

function resolveService(name) {
  const futureInstance = import(name).then(x => new x.default);

  const handler = {
    get: function (target, prop) {
      return function (...args) {
        return target.then(instance => instance[prop](...args));
      }
    }
  }

  return new Proxy(futureInstance, handler);
}

允许您编写以下代码:

const something = await getService('./services/some.service.js').fetchSomething();

这允许在您需要的确切代码行加载服务。 如果因为需要 import { playerService } from "main"; 语法而使用静态 import 加载它不会打扰您,您可以在 每个服务一个文件 :

export const playerService = getService('./services/player.service.js');

我在这里发布了完整的工作演示:https://github.com/Guerric-P/lazy-singletons-demo

我认为来自@Guerric 的代码无法与构建工具(如 webpack)一起使用。 特别是不支持动态字符串导入 import(modulePath)

我的建议是将重复的代码位减少到它们的最小表示...希望它最终会感觉不那么嘈杂。

解决方案#1/2

下面是一个使用高阶 memoize 函数来帮助缓存的示例。

// Minimal definition of service loaders
export const getPlayerService = memoize<PlayerServiceClient>(async () => new (await import('./player.client')).PlayerServiceClient());
export const getTeamService = memoize<TeamServiceClient>(async () => new (await import('./team.client')).TeamServiceClient());
export const getRefereeService = memoize<RefereeServiceClient>(async () => new (await import('./referee.client')).RefereeServiceClient());
export const getFriendService = memoize<FriendServiceClient>(async () => new (await import('./friend.client')).FriendServiceClient());
export const getPrizeService = memoize<PrizeServiceClient>(async () => new (await import('./prize.client')).PrizeServiceClient());
export const getWinnerService = memoize<WinnerServiceClient>(async () => new (await import('./winner.client')).WinnerServiceClient());


// Mock hacked together memoize fn
// TODO: Replace with some npm library alternative
const fnCache = new WeakMap();
function memoize<TReturn>(fn): TReturn {
  let cachedValue = fnCache.get(fn);
  if (cachedValue) return cachedValue;
  cachedValue = fn();
  fnCache.set(fn, cachedValue);
  return cachedValue;
}

解决方案#2/2

根据 JS 引擎和转译器的版本,您可能会删除一些代码并使用模块的性质来缓存服务的单例。

(注意:我偶尔会 运行 陷入 ES 模块如何依赖确定性导出的陷阱。解决方法是将导出分配给 return 实例的未决承诺。)

了解 Promises 的重要特性:它们只被解析一次,并且可以用来有效地缓存它们的结果。 每个 await.then 将获得初始解析值。

// SUPER minimal definition of services
export const playerService = (async (): PlayerServiceClient => new (await import('./player.client')).PlayerServiceClient())();
export const teamService = (async (): TeamServiceClient => new (await import('./team.client')).TeamServiceClient())();
export const refereeService = (async (): RefereeServiceClient => new (await import('./referee.client')).RefereeServiceClient())();
export const friendService = (async (): FriendServiceClient => new (await import('./friend.client')).FriendServiceClient())();
export const prizeService = (async (): PrizeServiceClient => new (await import('./prize.client')).PrizeServiceClient())();
export const winnerService = (async (): WinnerServiceClient => new (await import('./winner.client')).WinnerServiceClient())();

调用服务包装器

import { playerService } from "./services";

// Example: Using async/await IIFE
const PlayerService = (async () => await playerService)();

function async App() {
  // Example: Function-scoped service instance:
  // const PlayerService = await playerService
  const players = await PlayerService.queryPlayers();
}