面试被问到低代码细节?听我这样吹(含架构和原理)

面试被问到低代码细节?听我这样吹(含架构和原理)

首页角色扮演代号jump更新时间:2024-05-01
写在前面

一直以来我总是会刷到一些关于低代码的文章,但都是零零散散的,根本记不住。现在终于有时间在这里做个系统性的总结了

关于低代码,想必大家都有所了解,就是用拖拉拽的方式来快速搭建某个页面 || 表单 || 海报 || 游戏等,但其实除了常见拖拉拽的方式以外,还可以用形如在线文档的方式来生成页面,这里分别贴个链接给大家体验一下:

当然了,本文主要讲解的是拖拽型的低代码,这里先简单截个图瞅瞅:

顺带罗列下低码平台的一些优缺点:

优点:✅

缺点:❌

确实,低代码自身限制还是很多的,随便提个改动,都可能牵一发动全身。不过这里我们并不会去评判低码平台的好坏,而只是单纯的分享一些低代码中的核心思路和整个流程:包括但不限于模块划分、如何解耦和扩展、画布的实现方式等等。

最重要的事最重要

如果让我说一个低代码中最重要的事情,那就是方向。我们要知道低代码是有它的适用场景的(交互简单 && 轻逻辑),比如:

单单只实现上面的任意一种场景就已经要兼容很多东西了,所以想覆盖所有情况几乎是不可能的,而且现在也没有一个统一的规范,都是各做各的,百花齐放。因此不要想着啥都做,得挑一个垂直方向发力,方向越具体,自动化程度越高(比如组件拿来即用,无需修改),生产效率也更高,平台也会更好用。

次重要

除了方向,次重要的事情就是简单(包括交互和逻辑)。为什么要简单呢?

那如何才能做到简单呢?还是得朝着垂直方向发力,也就是会有点定制,要固化一些操作、约束一些行为。目前我还是觉得低代码主要还是给非研发同学用的,所以要足够简单。

基本实现

扯了这么多,现在让我们赶紧步入正轨吧。纵观大部分低码平台,主要都是由以下四部分组成(画的有点简陋):

接下来我们会对每个部分都挑两三个重点来讲解一下。

1、协议

在讲解每个模块之前,我们先来说一个东西,就是协议(听起来很高大上,其实就是规范,更朴素点叫做格式),它主要包括物料协议、平台搭建协议和其他协议等等。为什么要先约定协议呢,因为这个东西贯穿低代码的整条链路:

协议是低码平台的基石,它的主要目的就是约束和扩展,约定的好,事半功倍;约定不好,版版重构。约定优于配置说的就是这个道理。如果维护到后期发现协议很难拓展了,那基本只能重来了,只不过你多了些经验。协议本质上就是一堆 interface(就是固定格式啦)。

2、物料区

先来看看最左侧的物料区吧,这是低代码的起点,协议也是从这边开始的,先约定个最简单的物料协议吧:

/** 单个物料约定 */ interface IComponent { /** 组件名 */ componentName: string; /** 组件中文名称 */ title: string; /** 缩略图 */ icon?: string; /** 包地址 */ npm: { /** 源码组件名称 */ componentName?: string; /** 源码组件库名 */ package: string; /** 源码组件版本号 */ version?: string; }; /** 分类:比如基础组件、容器组件、自定义组件 */ group?: string; /** 组件入参或者说是可配置参数 */ props?: { name: string, propType: string, description: string, defaultValue: any, }[]; /** 其他扩展协议 */ [key: string]: any; } // 举个例子 const componentList = [ { componentName: "Message", title: "Message", icon: "", group: "基础组件", npm: { // import { Message } from @alifd/next 的意思 exportName: "Message", package: "@alifd/next", version: "1.19.18", main: "src/index.js", destructuring: true, }, props: [{ name: "title", propType: "string", description: "标题", defaultValue: "标题" }] } ];

物料区其实没啥功能,我们会有一个 componentList,里面是各种组件的基本信息,直接循环渲染即可。同时还会顺便生成一个 componentMap,主要是方便后续我们通过组件名来快速获取是组件的元信息,比如下面这样:

const componentMap = { Message: { componentName: "Message", title: "Message", icon: "", group: "基础组件", // ... }, }; // 通常情况,低码平台平台还需要对外暴露出加载组件和注册组件的方法,比如这样: function createRegisterConfig() { const componentList = []; const componentMap = {}; return { componentList, componentMap, loadComponent: () => {}, register: (comp) => { componentList.push(comp); componentMap[comp.componentName] = comp; } } }

物料区本身并不复杂,这里就说三个注意点:

// 方法一:import const name = 'Button' // 组件名称 const component = await import('https://xxx.xxx/bundle.js') Vue.component(name, component) // 方法二:script function loadjs(url) { return new Promise((resolve, reject) => { const script = document.createElement('script') script.src = url script.onload = resolve script.onerror = reject }) } const name = 'Button' // 组件名称 await loadjs('https://xxx.xxx/bundle.js') // 这种方式加载组件,会直接将组件挂载在全局变量 window 下,所以 window[name] 取值后就是组件 Vue.component(name, window[name])

3、画布区

这里我们先说说画布区的几种实现方式吧:

那画布区是怎么渲染出来的呢?其实我们会有一个 componentTree 来递归渲染拖拽出来的元素,然后各自按照组件类型来渲染,先简单看下 componentTree 的大体结构吧:

import ReactRenderer from '@alilc/lowcode-react-renderer'; import reactDOM from 'react-dom'; import { Button } from "@alifd/next"; const componentTree = { // 画布区的所有元素都在这里维护 componentName: 'Page', // 因为我们是以页面为单位,所以顶层一定是 Page props: {}, children: [{ componentName: 'Button', props: { type: 'primary', size: "large", style: { color: '#2077ff' }, className: 'custom-button', }, children: '确定', }] }; const conponents = { Button, }; ReactDOM.render(( // 里面会递归渲染组件 <ReactRenderer componentTree={componentTree} components={components} /> ), document.getElementById('root')); // ================================================ // 如果树形结构不好理解的话,我们可以把数据换成普通的数组来理解,然后渲染的过程就是循环遍历数组即可,这样就容易多了,比如 H5 页面就很适合数组这种形式 const componentTree = [{ "componentName": "ElButton", "height": 100, "props": {}, "style": {} }, { "componentName": "ElInput", "height": 300, "props": {}, "style": {} }];

事实上,低码平台都秉持着数据驱动视图的思想(和我们现在用的 vue 和 react 框架如出一辙),也是通过递归解析 componentTree 这个全局组件树来动态生成页面,化简来说就是:UI = Transformer(componentTree)。Transformer 这一步我们可以称之为转换器、渲染器或者 render 函数,通常开发完成之后,这个渲染器是不用改的,我们只需要单纯的修改数据,渲染器自然会帮我们解析。

这个思想很重要,也是解耦的核心:就是我们所有的操作,不管拖拽也好,修改元素属性也好,还是调整元素位置,都是对 componentTree 这个数据进行修改,单纯的对数据进行操作,比如追加元素,就往 componentTree 的 children 里面 push 一个元素即可;如果要修改一个元素的属性值,只需要找到对应元素的数据修改其 props 值即可。画布编排的本质就是操作组件节点和属性。

另外为了让每个组件都能直接获取到这个 componentTree,我们可以把这个 componentTree 弄成全局的(全局数据能够让整体流程更加清晰),比如放在 window、vuex、redux 上,这样每个模块就能共享同一份数据,也能随时随地更改同一份数据(平台会暴露公共的修改方法),而这个渲染器只是单纯的根据这个数据来渲染,并不处理其他事情。

这里我们还要注意一个问题,就是画布区本身也是个组件,是个组件那它就会受到父元素和全局的影响,最简单的比如样式,可能受到外部样式作用,导致你这个画布区和最终呈现的页面可能有一丢丢的不同,所以要排除这些影响,那具体可以怎么做呢?就是把这个画布区搞成一个独立的 iframe,这样环境就比较纯了,完美隔离,只不过增加了通信的成本。现在,物料区只负责渲染组件列表,以及触发拖拽放下的事件,之后就是触发修改全局的 componentTree,再之后就是触发画布区的重新渲染。这样一来,你就会发现画布区和物料区就很好的解耦了。到目前为止画布区只负责单纯的渲染。

如果你还是想知道这个渲染器到底是怎么递归怎么渲染的,我这里也提供了段简单的代码帮助你理解:

function renderNode(node) { if (!node) return null; const component = components[node.componentName]; const props = compute(node.props); const children = node.children.map(c => renderNode(c)); return React.render(component, props, children); } renderNode( componentTree); 4、属性设置区

接下来我们简单讲下右侧的属性设置区,这个区域通常会支持三种基本的设置:props && 样式 && 事件。一般来说我们的操作是这样的:

上面的过程和我们平时开发中后台应用的表单项(FormRender)是一毛一样的,网上也有很多教程,这里就不细说了。

我们主要来讲一下属性设置区的几个注意点:

下面是简单的代码示例截图,有个印象就行:

那沙箱里面能拿到什么数据呢?其实主要看我们想暴露什么参数给组件了,比如全局数据、全局方法、父元素、当前组件的 state 等等,那其实我们就可以直接传个包含以上数据的大对象然后传给 with 即可。

5、顶部操作区

上面那几个组成部分其实已经构成了低代码中最核心的几个部分,我们可以称之为 Core(内核)。对于顶部操作区,通常可以有前进、后退、清空等操作,但是这些操作并不算是必须的,它们通常以插件的形式存在,也方便大家一起维护和扩展,这就是微内核架构:

1 * Core N * plugins

关于这个架构,有兴趣的可以参考这篇文章 微内核架构在前端的实现及其应用,这篇文章讲解的很清楚了。

然后我们这里就简单讲下清空和回退操作吧(当然其余其他操作也是一样的道理):

但其实插件不仅仅适用于顶部条:

6、代码生成

到目前为止,我们就初步搭建好了页面,它背后其实是一堆数据,但是有可能你还是一头雾水,没关系,为了能让大家有个更具象的认识,我把这份数据的最终形态简单汇总了下(放心,就一丢丢丢丢数据很好看懂的):

const json = { version: "1.0.0", // 当前协议版本号 componentList: [ { // 组件描述 componentName: "Button", package: "@alifd/next", version: "1.0.0", destructuring: true, }, { package: "@alifd/next", version: "1.3.2", componentName: "Page", destructuring: true, } ], state: { // 全局状态 name: "尤水就下" }, componentsTree: { // 画布内容 componentName: "Page", props: {}, children: [{ componentName: "Button", state: {}, props: { type: 'primary', size: "large", style: { color: 'red' }, className: 'custom-button', onClick: { // 事件绑定 type: "JSFunction", value: "function(e) { console.log(e.target.innerText) }" }, } }] } }

接下来就是要准备发布了,具体该怎么做呢?这里说下两种主要的方式:

import React, { memo } from 'react'; import ReactRenderer from '@alilc/lowcode-react-renderer'; const SamplePreview = memo(() => { return ( // 至于渲染器怎么实现的,网上有一堆文章,不理解的同样可以把数据当成简单的数组,for 循环渲染而已 <ReactRenderer className="lowcode-plugin-sample-preview-content" schema={schema} components={components} /> ); }); // ps: 这里简单说下 json 和 schema 的区别,以前我也很困惑,不知道现在理解对没有(但我感觉大部分情况下都是随便叫的) // json 是一个普通对象 // schema 是一个有固定格式的普通对象 // json schema 是一个描述 json 格式的普通对象

那两种方式有什么区别呢:

前者

后者

1

运行时编译(JIT)

预编译(AOT)

2

最终输出的是 json

最终输出的是项目

3

实时生效

得打包重新发版

4

一般这个够用

有性能要求用这个

为什么前者会有性能问题呢?因为渲染页面的时候,需要将 json 进行一层转换,而后者是已经打包之后的项目了。

其他问题

{ "version": "1.0.0", "packages": [{ // 依赖包 "title": "fusion 组件库", "package": "@alifd/next", "version": "1.20.0", "urls": [ "https://alifd.alicdn.com/npm/@alifd/next/1.20.0/next.min.js", "https://alifd.alicdn.com/npm/@alifd/next/1.20.0/next.min.css" ], "library": "Next" // 最终这样用:window.Next.componentName }], "components": [] }

小结

如果你能看到这里,那说..明...文章写的还行,哈哈哈嗝。这里我就简单的用几句话对本篇文章进行一个总结:

最后的最后,我又重新画了张图方便大家记忆:

好啦,本次分享就到这里,有什么问题欢迎点赞评论留言,我们下期再见,拜拜

作者:尤水就下
链接:https://juejin.cn/post/7276837017231835136

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved