Skip to main content

低代码平台的编程语言需求、路线和典型问题整理

· 12 min read
赵雨森

一、需求

从编程角度来说,低代码核心是为用户打造一款能够快速入门搭建、同时又能支持中等复杂度的企业级应用的编程系统(Programming System)。

编程系统分为两个方面来看:

  • 一方面,我们需要引入完善的编程表达能力(如泛型、模块、函数式等等),满足复杂应用的开发。
  • 另一方面,我们需要优化配套的编程环境,提升用户的编程体验(如 IDE、LanguageServer、Debugger、实时编译等等)

目标用户包含两类:

  • 企业业务人员,会使用一般的办公系统如 Excel 等,经过我们的培训可以独立使用低代码平台搭建系统;
  • 专业开发人员,如工商银行的专业前后端开发。

关于用户的需求复杂度和使用难度,目前我们产品中的使用曲线还是比较割裂:

我们希望通过对编程系统中的各个设施的优化,能够达到比较平滑的效果。

二、路线

在我们目前的规划中,编程系统的发展路线分为两个阶段:

第一个阶段(1-2 年)

借助 TS Language Server 能力,快速补齐和完善编程系统的表达能力。对标 OutSystems 等产品的语言能力。

举一个例子,现在用户在使用部分场景的时候,没有类型提示了。或者在使用的一些链路中,类型推断断掉了。

目前这个阶段的一些主要问题如下:

  • 前端组件类型体系不完善,如属性传递、事件回调等;
  • 数据查询出来,由于用户 SELECT 和 JOIN 等,会产生出来很多数据结构,如果按具名的话会爆炸;
  • 用户在操作列表数据的时候,目前只能使用 ForEach。无法用 find、filter、map 等方式;
  • 一个复杂的应用有划分模块的需要;
  • 目前逻辑块的空间利用率过低,想用一些更合适的交互形式代替;
  • 用户编程的实时反馈、互动、调试方面比较困难;
  • ...

第三节“典型问题整理”有详细描述。

第二个阶段

自研 Language Server,提供智能化的推荐能力。

三、典型问题整理

1. 支持前端组件的类型体系不完善

示例 1

比如下图是一个选择框组件的属性面板:

下面是选择框组件的一个简化版的 ts 声明和使用它的页面场景:

interface ChangeEvent<T> {
item: T;
value: string;
}
interface MultipleChangeEvent<T> {
item: T;
value: Array<string>;
}

export class Select<T> extends Component {
constructor(
public options?: {
// 属性
color?: 'default' | 'primary' | 'danger',
size?: 'mini' | 'small' | 'normal' | 'large',
multiple?: false,
dataSource?: Array<T> | ((params: DataSourceParams) => Promise<Array<T>>),
value?: string | Array<string>,
// 事件
onChange?: (e: ChangeEvent<T>) => void,
},
);
constructor(
public options?: {
// 属性
color?: 'default' | 'primary' | 'danger',
size?: 'mini' | 'small' | 'normal' | 'large',
multiple?: true,
dataSource?: Array<T> | ((params: DataSourceParams) => Promise<Array<T>>),
value?: string | Array<string>,
// 事件
onChange?: (e: ChangeEvent<T>) => void,
},
);
}
class View1 {
render(): VNode {
return new components.Select<Student>({
size: 'large',
multiple: true,
dataSource: this.loadStudents,
onChange: (e) => {
console.log(e.item.age);
},
});
}
}
class View2 {
render(): VNode {
return new components.Select<Student>({
size: 'large',
multiple: true,
dataSource: this.loadStudents,
onChange: (e) => {
showMessage(e.value.length);
},
});
}
}

它已经涉及到了以下的一些语言表达能力:

  • 属性都是可选类型
  • 选择框的数据源可以是数组、分页数组或函数(产品概念中的逻辑)
  • 有单选、多选的可能,多选时 value 为数组
  • onSelect 的事件入参是一个回调函数
  • 泛型 T 取决于数据源中的 T

示例 2

页面中会使用循环组件,甚至会有双循环的场景。类似如下 ts 代码表达。希望用户在编辑 Text 组件的文本时,可以自动补全一些 scope, scope2 的上下文提示。

class View {
render(): VNode {
return new components.ForComponent<Student>({
dataSource: this.loadStudents,
slotDefault: (scope) => [
new components.ForComponent<Course>({
dataSource: scope.item.courses,
slotDefault: (scope2) => [
new components.Text({
text: scope2.item.course.name,
}),
],
],
});
}
}

另外 scope 这种很生疏的字段又不想向用户暴露,我们可以用析构去除。但又会引入一些语言特性、如析构时 as。

class View {
render(): VNode {
return new components.ForComponent<Student>({
dataSource: this.loadStudents,
slotDefault: ({ item }) => [
new components.ForComponent<Course>({
dataSource: item.courses,
slotDefault: ({ item: item2 }) => [
new components.Text({
text: item2.course.name,
}),
],
],
});
}
}

2. 数据查询具名类型爆炸

比如用户了定义一个如下两个实体(对应两张数据库表):

class School {
id: Long;
name: String;
description: String;
address: String;

static get(id: Long): School;
static create(student: School): Long;
static update(student: School): void;
static delete(id: Long): void;
}

class Student {
id: Long;
name: String;
age: Integer;
gender: Gender;
number: String;
idCard: String;
phone: String;
schoolId: Long;

static get(id: Long): Student;
static create(student: Student): Long;
static update(student: Student): void;
static delete(id: Long): void;
}

首先,比如最简单的修改功能:

用户的编程意图大概如下:

let student = Student.get('123'); // 查询出一个学生
// 在表单中编辑 student
Student.update(student); // 再提交更新

那么复杂一点的情况是这样的:

示例 1

有时比如不需要查询出全部字段

用户的编程意图类似下面的方式:

interface PartialStudent {
id: Long;
name: String;
age: Long;
gender: Gender;
}

let partialStudent: PartialStudent = query('SELECT id, name, age, gender FROM Student'); // 查询出一个学生
// 在表单中编辑 partialStudent
Student.update(student); // 再提交更新。❌ 类型错误

这时用户如果直接使用我们自动生成的 update 接口会类型不匹配。需要用户额外做很多操作去处理这个事情。

另外也产生了一个 PartialStudent 这样一个临时的具名类型,用户如果使用了很多这类场景,这些具名类型就会爆炸。

示例 2

JOIN 的场景,用户想查询学生时附带查询关联的学校信息(schoolId)。

用户的编程意图类似下面的方式:

interface StudentRecord {
id: Long;
name: String;
age: Integer;
gender: Gender;
number: String;
idCard: String;
phone: String;
schooId: Long;
schoolName: String;
schoolDescription: String;
schoolAddress: String;
}

let record: StudentRecord = query('SELECT * FROM Student JOIN School ON Student.schoolId = School.id'); // 查询出一个学生及相关的学校信息
// 在表单中编辑 record
Student.update(record); // 再提交更新。❌ 类型错误

这种方式下,用户查询出来的字段比以前多,同样与 update 的类型不匹配。

后来我们参考 OutSystems,改进成以下方式:

interface StudentRecord {
student: Student;
school: School;
}

let record: StudentRecord = query('SELECT * FROM Student JOIN School ON Student.schoolId = School.id'); // 查询出一个学生及相关的学校信息
// 在表单中编辑 record
Student.update(record.student); // 再提交更新。

这种方式下,用户已经可以 update 了。但目前仍然会产生许多类似 StudentRecord 这样的具名类型,这是我们现在的一个主要问题。

3. 列表操作比较薄弱

目前只给用户提供了 ForEach。

ForEach(list, start, list.length - 1, (item, i) => {
//
});

用户需要类似下面的一些高级操作:

list.filter((student) => student.age > 20).map((student) => ({
name: student.name,
age: student.age,
number: 'HZ' + student.number,
}));

语言表达能力上可能需要 lambda 的支持。

4. 一个复杂的应用有划分模块的需要

这个上次会议上讨论过,我还是在下面列一下。

我们的模块粒度比较大,把一个大应用能拆解,支持 import/export 即可。

下面是一个伪代码:

import * as moduleB from './appB/moduleB.nasl';

export entity Student {
name: moduleB.School;
age: Integer;
}

export struct School {
name: String;
count: Integer;
}

export enum Color {
RED,
GREEN,
BLUE,
}

export logic logic1(param1: Student) {
return some;
}

后面这种定义比较繁琐:

import * as moduleB from './appB/moduleB.nasl';

namespace entities {
export entity Student {
name: moduleB.School;
age: Integer;
}
}

namespace structures {
export struct School {
name: String;
count: Integer;
}
}

namespace enums {
export enum Color {
RED,
GREEN,
BLUE,
}
}

export logic logic1(param1: entities.Student) {
return some;
}

5. 目前逻辑块的空间利用率过低

darklang 我们可以做一个参考。

但目标用户群体中包含了没有编程经验的业务人员,因此都是代码编程的方式可能对他们来说比较困难。还请老师能提供更多的一些思路。

6. 用户编程的实时反馈、互动、调试方面比较困难

作为低代码可视化为主的编程环境,想提升一些实时反馈、互动,调试方面的体验,希望老师能提供更多的思路。