likes
comments
collection
share

使用 TypeScript 从零搭建自己的 Web 框架: Electron 环境运行

作者站长头像
站长
· 阅读数 2

使用 TypeScript 从零搭建自己的 Web 框架: Electron 环境运行

得益于我们框架中引入了依赖注入模式,并通过接口将核心类与组件优雅地解耦,我们仅需稍作调整,便能够轻松地在 Electron 环境中运行它。

创建 Electron 开发环境

通过使用 Electron Forge CLI 项目创建一个 Electron 项目

npm init electron-app@latest my-new-app -- --template=webpack-typescript

然后我们依然需要安装 reflect-metadata,以及修改 tsconfig 配置。

// tsconfig.josn
{
  "compilerOptions": {
    ...
    "experimentalDecorators": true /* 启用装饰器 */,
    "emitDecoratorMetadata": true /* 发射装饰器元数据 */
  }
  ...
}

优化核心类

在 index.ts 入口文件中引入 reflect-metadata,同时为了简单起见我们修改框架的核心类,将之前自动扫描文件和导入的逻辑提取到外部实现,修改之前的 Application 的 listen 方法名为 start,以显得更加通用,紧接着我们实现一个 TRPCServer 组件用于替换之前的 WebServer 组件,最后在框架初始化成功后再创建 Electron 窗口。

// core/Application.ts

// ...
  async initialize(resolvers: Constructor[] = []): Promise<this> {
    for (let resolver of resolvers) {
      const options = Reflect.getMetadata(INJECTABLE_METADATA, resolver);
      if (options) {
        this.container.register(resolver, { resolver, options });
        const isController = Reflect.getMetadata(CONTROLLER_METADATA, resolver);
        if (isController) {
          this.mapRoutes(resolver);
        }
      }
    }

    return this;
  }
// ...

// index.ts

// ...
const createWindow = async () => {
  const framework = await new Application({
    server: new TRPCServer(),
  }).initialize([HomeService, HomeController]);
  framework.start(async () => {
    const mainWindow = new BrowserWindow({
      height: 600,
      width: 800,
      webPreferences: {
        preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
      },
    });
    mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
    mainWindow.webContents.openDevTools();
  });
};
// ...

控制器和服务

// controller/HomeController.ts
import { Controller, Get } from '@/core';
import { HomeService } from '@/service/HomeService';

@Controller()
export class HomeController {
  constructor(private readonly homeService: HomeService) {}

  @Get('hello')
  async index() {
    return await this.homeService.sayHello();
  }
}

// service/HomeService.ts
import { Injectable } from '@/core';

@Injectable()
export class HomeService {
  constructor() {}

  async sayHello(): Promise<string> {
    return 'Hello from HomeService';
  }
}

实现 TRPCServer

简单实现一个 TRPCServer, 并将控制器方法与 tRPC 路由绑定。

// core/component/TRPCServer.ts
import { initTRPC } from '@trpc/server';
import { HTTPRequest, resolveHTTPResponse } from '@trpc/server/http';
import { ipcMain } from 'electron';
import SuperJSON from 'superjson';
import { IServer } from '../interface';
import { Route } from '../type';

export class TRPCServer implements IServer {
  private readonly instance;
  private routes: any = {};
  constructor() {
    this.instance = initTRPC.context<any>().create({
      transformer: SuperJSON,
    });
  }

  route(route: Route<any>) {
    this.routes = {
      ...this.routes,
      [route.path]: this.instance.procedure.query(route.handler),
    };
    return this;
  }

  async start(callback: any): Promise<any> {
    ipcMain.handle('trpc', (_, req: IpcRequest) => {
      return this.ipcRequestHandler({
        endpoint: '/trpc',
        req,
        router: this.instance.router(this.routes),
      });
    });

    callback();
  }

  private async ipcRequestHandler<TRouter extends any>(opts: {
    req: IpcRequest;
    router: TRouter;
    batching?: { enabled: boolean };
    onError?: (o: { error: Error; req: IpcRequest }) => void;
    endpoint: string;
  }): Promise<IpcResponse> {
    // adding a fake "https://electron" to the URL so it can be parsed
    const url = new URL('https://electron' + opts.req.url);
    const path = url.pathname.slice(opts.endpoint.length + 1);
    const req: HTTPRequest = {
      query: url.searchParams,
      method: opts.req.method,
      headers: opts.req.headers,
      body: opts.req.body,
    };

    const result = await resolveHTTPResponse({
      req,
      createContext: async () => {},
      path,
      // @ts-ignore
      router: opts.router,
      batching: opts.batching,
      onError(o) {
        opts?.onError?.({ ...o, req: opts.req });
      },
    });

    return {
      body: result.body,
      headers: result.headers,
      status: result.status,
    };
  }
}

在 preload.ts 中将 trpc 方法暴露给渲染进程

// preload.ts
import { contextBridge, ipcRenderer } from 'electron';

process.once('loaded', async () => {
  contextBridge.exposeInMainWorld('API', {
    trpc: (req: IpcRequest) => ipcRenderer.invoke('trpc', req),
  });
});

在渲染进程中调用方法

// renderer.ts
import { createTRPCProxyClient, httpLink } from '@trpc/client';
import SuperJSON from 'superjson';

import './index.css';

console.log('👋 This message is being logged by "renderer.js", included via webpack');

const trpc = createTRPCProxyClient<any>({
  links: [
    httpLink({
      url: '/trpc',
      fetch: async (input, init) => {
        const req = {
          url: input instanceof URL ? input.toString() : typeof input === 'string' ? input : input.url,
          method: input instanceof Request ? input.method : init?.method!,
          headers: input instanceof Request ? input.headers : init?.headers!,
          body: input instanceof Request ? input.body : init?.body!,
        };

        const resp = await window.API.trpc(req);

        return new Response(resp.body, {
          status: resp.status,
          headers: resp.headers,
        });
      },
    }),
  ],
  transformer: SuperJSON,
});

// 通过 trpc 对象直接调用 HomeController 的 index 方法
trpc.hello.query().then(res => console.log(res)); // 输出: "Hello from HomeService"

使用 TypeScript 从零搭建自己的 Web 框架: Electron 环境运行

总结

我们实现了一个全新的 TRPCServer 组件,虽然这个组件可能稍显简陋,但它成功助力我们的框架在 Electron 环境中运行,这标志着我们在技术探索道路上迈出了坚实的一步。在实际使用过程中,不断涌现的需求推动着我们不断对框架代码进行细致的修改和完善。这是一个持续进化的过程,未来我们还将倾注更多心血,不断完善框架的每一个细节,以提供更加出色、稳定的技术支持。