开发

前后端一体化应用

以全栈开发者视角重新审视前后端应用开发体验,发现传统开发体验的提升空间。

随着 Serverless 技术的出现,高可用、高并发和运维能力下沉到了 Serverless 平台底层,对开发者透明,开发者只需要关心自己的业务代码开发。原本门槛很高的后端业务开发变得触手可及,前端开发者更容易掌握后端业务开发。越来越多的开发者转型成为全栈开发者。当以一个全栈开发者视角重新审视前后端应用开发体验时,会发现传统开发体验有很大的提升空间。

Malagu 框架在设计之初就已经意识到了这一点:

  • 采用了对 Serverless、前后端一体化更为友好的语言 Typescript
  • 提供了前后端通用的 IoC 容器等基础设施
  • 抽象了前后端统一的渐进式解决方案:组件化方案
  • 前端像调用本地方法一样调用后端接口的 RPC 通信方式

前后端一体化开发优势

如果您不是全栈开发者,您仍然可以基于 Malagu 框架按照传统的前后端分离开发方式开发,与以前相比没有任何区别。如果您是一个全栈开发者,前后端一体化开发可能更加适合您。

Malagu 提供统一的开发语言、IoC 容器、工程化规范、命令行工具、渐进式方案,让您心智负担更小、项目更易维护、后端掌握的技能无缝应用到前端上。甚至前后端代码都可以在同一个项目中开发。

以前,我们只能针对单一的前端或者后端抽象通用代码;现在,我们可以基于前后端一起抽象通用代码,抽象的范围更大,进一步提升代码的复用能力。同时,也不存在前后端开发人员联调和沟通成本,而且更容易做全链路优化。

是否适合大型项目

对于小项目,采用前后端一体化开发,效率肯定是更高的,一个项目丢给一为全栈开发者开发,前后端联调、沟通成本都是省了。但是,对于大型项目,不可能让一个全栈开发者来开发,我们需要借助团队的力量。此时,您可能会觉得前后端一体化开发不适合大型项目。

但是,我们可以换一个角度来看,一个大型项目面对的问题往往是复杂的,我们应该从实现层面跳出来,从架构层面去寻找解决方案。我们可以把一个大型项目从业务视角拆解成许多个小项目,这些小项目再交由全栈开发者开发。

把一个复杂的问题分解成一个个简单的小问题,是一个更为可行的方案。让采用风险更低的渐进式地更新迭代应用的方案成为可能。

团队组织方式

在过去,前后端分工,按照技术类型纵向地组织团队;现在,前后端一体化开发,将一个大型应用从业务视角拆解成许多个微应用,按照业务功能横向地组织团队。

前后端一体化开发体验示例

以前端调用后端接口这样一个十分常见的场景为例,使用前后端一体化开发的流程是:

  1. 定义接口
  2. 后端实现接口
  3. 前端调用接口(像调用本地方法一样调用后端接口)

定义接口

src/common/user-protocol.ts
export const UserService = Symbol("UserService");

export interface UserService {
  list(): Promise<User[]>;
  get(id: number): Promise<User | undefined>;
  remove(id: number): Promise<void>;
  modify(user: User): Promise<void>;
  create(user: User): Promise<User>;
}

export interface User {
  id: number;
  name: string;
  age: number;
}

后端实现接口

src/node/user-service.ts
import { Transactional, OrmContext } from "@malagu/typeorm/lib/node";
import { Rpc } from "@malagu/rpc";
import { User } from "./entity";
import { UserService } from "../common";

@Rpc(UserService) // 相当于告诉框架,可以让前端通过 JSON RPC 的方式调用
export class UserServiceImpl implements UserService {
  @Transactional({ readOnly: true }) // 开启只读事务
  list(): Promise<User[]> {
    const repo = OrmContext.getRepository(User);
    return repo.find();
  }

  @Transactional({ readOnly: true })
  get(id: number): Promise<User | undefined> {
    const repo = OrmContext.getRepository(User);
    return repo.findOne(id);
  }

  @Transactional() // 开启读写事务
  async remove(id: number): Promise<void> {
    const repo = OrmContext.getRepository(User);
    await repo.delete(id);
  }

  @Transactional()
  async modify(user: User): Promise<void> {
    const repo = OrmContext.getRepository(User);
    await repo.update(user.id, user);
  }

  @Transactional()
  create(user: User): Promise<User> {
    const repo = OrmContext.getRepository(User);
    return repo.save(user);
  }
}

前端调用接口

src/browser/user.view.tsx
import * as React from "react";
import { View } from "@malagu/react";
import { RpcUtil } from "@malagu/rpc";
import { DataTable } from "grommet";
import { UserService, User } from "../common";

function Users() {
  const [data, setData] = React.useState<User[]>([]);

  React.useEffect(() => {
    // 通过 RpcUtil 获取接口 UserService 的远程调用代理对象
    const userService = RpcUtil.get<UserService>(UserService);
    // 像调用本地方法一样调用后端接口
    userService.list().then((users) => setData(users));
  }, []);

  return (
    <DataTable
      margin={{ vertical: "medium" }}
      columns={[
        {
          property: "name",
          header: "Name",
          primary: true,
        },
        {
          property: "age",
          header: "Age",
        },
      ]}
      data={data}
    ></DataTable>
  );
}

@View({ component: Users })
export default class {}

Copyright © 2024 Zero (github@groupguanfang) 粤ICP备2023102563号