Layr:前后端数据交互新思路

avatar
@智云健康

作者:Gavin,未经授权禁止转载。

前言

在前后端数据交互中,国内比较常见的数据交互模式是:

网关微服务通常采用RPC进行交互,本文不赘述。

前端网关的交互国内公司通常用Rest,国外有不少公司使用GraphQL,从技术实现上讲,这两种方式各有优缺点,也都不难。

对于开发人员来讲,如果不需要区分客户端与服务端,本来一套代码是可以相互调用的,但区分后,不得不通过url + GET/POST/DELETE + query/body/params 进行数据交互,前后端为此会产生大量的重复逻辑与代码,如:数据验证、工具函数、枚举值、ts的类型声明等,当然还不得不为此定义一套与JS/TS毫无关系的接口规范,如果使用React、Vue可能还会引入Redux、Vuex等状态管理库,对于小型项目而言稍显繁琐。

问题

有没有办法做到前后端代码、逻辑复用,类似于RPC调用一样,甚至把状态都管理起来呢?

答案当然是肯定的。

传统解决方式

服务端渲染

next举例(原生服务端渲染或nuxt也类似),可以把网关代码写到node端,node端做两件事:

  1. 网关层:与服务层进行RPC通信
  2. 路由区分:识别到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等类似状态管理库,不过也稍显繁琐:

  1. 首屏初始化store;
  2. store前后端同步

单页应用 + node

除了上方的服务端渲染外,纯单页应用+node也可以做到代码逻辑复用,这里面的原理与上方服务端渲染类似,只是next有个动态生成首屏的过程,而单页应用所有代码都是静态的,当然这里也不是真正意义上的代码逻辑共享,主要是通过webpack等工具将依赖打包到前端静态资源中,通过node启一个静态资源访问目录,或者传到三方静态资源管理中心,挂一个CDN。

更好的方式:Layr

简介

Layr是一个面向对象的JS/TS库,可以进行类RPC通信。

如何使用

  1. 后端由一个或多个类组成,每个类会将一些属性和方法显示的公开给前端;
  2. 前端会生成后端类的代理,通过代理进行通信。

原理

表面上看,Layr类似于Java RMI,但其原理大不相同:

  1. Layr不是一个分布式对象,其后端是无状态的,栈中没有共享对象;
  2. Layr不涉及任何模板代码、代码生成及配置文件等;
  3. 使用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的前后端是完全独立的,都可以进行独立部署。

相关链接