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


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


前后端一体化开发优势


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


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


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


是否适合大型项目


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


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


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


团队组织方式


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


前后端一体化开发体验


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

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


代码目录结构


├── src
│   ├── browser                       前端代码目录
│   │   ├── module.ts                 前端模块入口文件
│   │   └── user.view.tsx             前端页面调用后端接口
│   ├── common                        公共代码目录
│   │   ├── index.ts
│   │   └── user-protocol.ts          接口定义
│   └── node                          后端代码目录
│       ├── entity                    数据库实体类定义文件夹
│       │   ├── index.ts
│       │   └── user.ts               用户实体类定义
│       ├── module.ts                 后端模块入口文件
│       └── user-service.ts           后端实现接口

定义接口


// 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 {}