作者:Gavin,未经授权禁止转载。
前言
在前后端数据交互中,国内比较常见的数据交互模式是:
网关到微服务通常采用RPC进行交互,本文不赘述。
前端与网关的交互国内公司通常用Rest,国外有不少公司使用GraphQL,从技术实现上讲,这两种方式各有优缺点,也都不难。
对于开发人员来讲,如果不需要区分客户端与服务端,本来一套代码是可以相互调用的,但区分后,不得不通过url + GET/POST/DELETE + query/body/params 进行数据交互,前后端为此会产生大量的重复逻辑与代码,如:数据验证、工具函数、枚举值、ts的类型声明等,当然还不得不为此定义一套与JS/TS毫无关系的接口规范,如果使用React、Vue可能还会引入Redux、Vuex等状态管理库,对于小型项目而言稍显繁琐。
问题
有没有办法做到前后端代码、逻辑复用,类似于RPC调用一样,甚至把状态都管理起来呢?
答案当然是肯定的。
传统解决方式
服务端渲染
拿next举例(原生服务端渲染或nuxt也类似),可以把网关代码写到node端,node端做两件事:
- 网关层:与服务层进行RPC通信
- 路由区分:识别到next的路由交给next渲染,识别到接口路由交由相应controller进行处理
示例伪代码
const Koa = require('koa');
const next = require('next');
// const config = require('xxx'); // 基础配置
// const log = require('xxx'); // 日志库
if (!module.parent) {
nextApp.prepare()
.then(() => {
nextApp.setAssetPrefix('/next');
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(config[env].port, config[env].host, () => {
log.info(`API server listening on ${config[env].host}:${config[env].port}, in ${env}`);
});
});
} else {
app.use(router.routes());
app.use(router.allowedMethods());
}
使用服务端渲染可以解决代码复用的问题,前后端代码可以调用公共函数、枚举等,但这里其实并不是真正意义上的复用,在首屏动态生成js过程中还是会抽出依赖代码动态编译到首屏动态生成的js中。当然在传统开发模式下,这里免不了还是得使用传统HTTP接口进行通信。
在类似next/nuxt这种服务端渲染库中,通过HTTP进行数据交互会比传统ajax更加繁琐,因为需要区分当前执行请求的环境。
示例伪代码
const request = method => async (path, data = {}, req = {}) => {
const keys = Object.keys(data);
const config = {
method,
headers: {
'Content-Type': 'application/json'
},
};
let baseUrl;
if (typeof window === void 0) {
// 服务端
config.headers['User-Agent'] = req.headers['user-agent'];
config.headers['Cookie'] = req.headers['cookie'];
baseUrl = 'http://127.0.0.1:3000/api'; // 当前node服务监听的端口为3000
} else {
// 客户端
config.credentials = 'include';
baseUrl = '/api';
}
const api = baseUrl + path;
const res = await (async () => {
if (method === 'GET') {
return !keys.length ? await fetch(api, config) : await fetch(`${api}?${ _.compact(keys.map(key => data[key] ? `${key}=${data[key]}` : '')).join('&') }`, config)
} else {
return await fetch(api, {
...config,
body: JSON.stringify(data),
})
}
})().catch(e => {
log.error(e);
});
const resJson = await res.json();
return resJson
};
对于状态管理,服务端渲染也可以引入redux、vuex等类似状态管理库,不过也稍显繁琐:
- 首屏初始化store;
- store前后端同步
单页应用 + node
除了上方的服务端渲染外,纯单页应用+node也可以做到代码逻辑复用,这里面的原理与上方服务端渲染类似,只是next有个动态生成首屏的过程,而单页应用所有代码都是静态的,当然这里也不是真正意义上的代码逻辑共享,主要是通过webpack等工具将依赖打包到前端静态资源中,通过node启一个静态资源访问目录,或者传到三方静态资源管理中心,挂一个CDN。
更好的方式:Layr
简介
Layr是一个面向对象的JS/TS库,可以进行类RPC通信。
如何使用
- 后端由一个或多个类组成,每个类会将一些属性和方法显示的公开给前端;
- 前端会生成后端类的代理,通过代理进行通信。
原理
表面上看,Layr类似于Java RMI,但其原理大不相同:
- Layr不是一个分布式对象,其后端是无状态的,栈中没有共享对象;
- Layr不涉及任何模板代码、代码生成及配置文件等;
- 使用Deepr协议
示例
后端
import {
Component,
primaryIdentifier,
attribute,
method,
expose
} from '@layr/component';
import {ComponentHTTPServer} from '@layr/component-http-server';
class Counter extends Component {
@expose({get: true, set: true}) @primaryIdentifier() id;
@expose({get: true, set: true}) @attribute() value = 0;
@expose({call: true}) @method() increment() {
this.value++;
}
}
const server = new ComponentHTTPServer(Counter, {port: 3210});
server.start();
前端
import {ComponentHTTPClient} from '@layr/component-http-client';
(async () => {
// 建立连接
const client = new ComponentHTTPClient('http://localhost:3210');
const Counter = await client.getComponent();
// 数据订阅示例
class ExtendedCounter extends Counter {
async increment() {
await super.increment();
if (this.value === 3)
console.log('状态改变了,触发xxx');
}
}
}
// 获取与修改数据
const counter = new Counter(); // new ExtendedCounter();
console.log(counter.value); // => 0
await counter.increment();
console.log(counter.value); // => 1
await counter.increment();
console.log(counter.value); // => 2
})();
总结
注意
本文只是提供一种新的前后端数据交互思路,在笔者看来Layr目前仅适合快速迭代的小型项目,对于大型项目无论是使用Layr还是GraphQL,对服务端(Node)的资源要求都会较高。
Layr采用面向对象的思想设计,这对于习惯了react函数式编程模式的同学来说有点不讲武德,相对来讲Layr对习惯Java开发模式的同学更加友好。
不同于传统的Node/PHP+模板的形式,Layr的前后端是完全独立的,都可以进行独立部署。