跳到主要内容低代码引擎中 React 动态渲染器:JSON Schema 转 Fiber 树详解 | 极客日志TypeScriptSaaS大前端
低代码引擎中 React 动态渲染器:JSON Schema 转 Fiber 树详解
深入解析低代码引擎核心——React 动态渲染器。阐述了如何将复杂的 JSON Schema 描述转化为带状态、可交互的 React Fiber 树。内容涵盖宏观架构、Fiber 架构契合点、组件注册表、递归渲染机制、属性映射与数据绑定、表达式引擎及性能优化策略。通过实际案例展示了从配置蓝图到交互式界面的完整实现流程,为构建高效可扩展的低代码平台提供技术参考。
黑客2 浏览 本文将深入探讨低代码引擎中一个至关重要的核心技术:React 动态渲染器。具体来说,我们将聚焦于如何将复杂的 JSON Schema 描述,优雅而高效地转换为一个带状态的、可交互的 React Fiber 树。这是一个融合了前端架构、数据管理、编译原理和运行时优化的复杂系统工程。
一、引言:低代码的崛起与动态渲染的挑战
近年来,低代码开发平台如雨后春笋般涌现,极大地提升了应用开发的效率。其核心思想在于通过可视化界面、拖拽组件和配置属性,生成可运行的应用程序。在这一切的背后,一个强大且灵活的渲染引擎是不可或缺的。
React 作为当今最流行的前端框架之一,以其组件化、声明式和高效的虚拟 DOM/Fiber 调和机制,成为了构建低代码引擎运行时界面的理想选择。然而,低代码引擎中的界面并非由开发者直接编写 JSX 代码,而是由一套高度抽象的JSON Schema来描述。这个 Schema 不仅定义了组件的类型、属性,还包含了数据绑定、事件逻辑、布局信息甚至动态表达式。
- 解析复杂性:JSON Schema 可以非常复杂,包含嵌套、条件逻辑、数组等结构。
- 动态性:用户在设计器中修改配置,或者运行时数据变化,界面需要即时响应。
- 状态管理:渲染出的 UI 组件需要与底层数据模型进行双向绑定,管理自身状态并影响全局状态。
- 性能:面对大量组件和频繁更新,渲染过程必须高效。
- 扩展性:能够轻松集成自定义组件和业务逻辑。
这一切都指向一个核心问题:如何构建一个智能的'翻译器',将一份静态的 JSON Schema 蓝图,转换为一个既能响应数据变化、又能处理用户交互的、活生生的 React Fiber 树?
二、低代码引擎的宏观架构视角
在深入渲染细节之前,我们先从宏观层面审视一下低代码引擎的典型架构,这将有助于我们理解动态渲染器在整个系统中的定位。
- 设计器 (Designer):提供可视化拖拽、属性面板、大纲树等功能,供用户设计页面。
- 输入: 用户操作。
- 输出: 生成一份描述页面结构的 JSON Schema。
- Schema 生成器/解析器 (Schema Generator/Parser):将设计器的操作转换为标准化的 JSON Schema,或将现有 JSON Schema 加载到设计器中。
- 动态渲染器 (Dynamic Renderer):本文重点。它接收 JSON Schema 和运行时数据,将其转换为可交互的 React UI。
- 运行时数据引擎 (Runtime Data Engine):管理整个应用的数据状态,包括表单数据、全局变量、API 请求结果等。它与渲染器双向通信。
- 逻辑编排引擎 (Logic Orchestrator):处理复杂的业务逻辑,例如工作流、条件判断、数据转换等,通常由可视化流程图或 DSL (领域特定语言) 描述。
- 组件库 (Component Library):提供基础 UI 组件(按钮、输入框、表格等)和业务组件(用户选择器、商品列表等),它们是渲染器可用的'积木'。
我们的动态渲染器,就位于设计器生成 Schema 和运行时数据引擎之间,扮演着'图纸执行者'的角色。
三、React Fiber 架构的精髓与动态渲染的契合点
在理解如何将 JSON Schema 转换为 Fiber 树之前,我们有必要简要回顾一下 React 的 Fiber 架构。Fiber 是 React 16 引入的一种新的协调(Reconciliation)算法,它彻底改变了 React 内部的工作方式。
- 可中断的更新 (Interruptible Updates):Fiber 将渲染工作拆分成小单元(Fiber 节点),可以在每一帧之间暂停和恢复,从而避免长时间的阻塞,提升用户体验。
- 优先级调度 (Priority Scheduling):不同的更新可以有不同的优先级,高优先级的更新(如用户输入)可以打断低优先级的更新(如数据加载)。
- 链表结构 (Linked List):Fiber 树是一个单向链表,每个 Fiber 节点都有
child、sibling 和 return 指针,方便遍历。
- 双缓冲 (Double Buffering):React 维护两棵 Fiber 树:
current 树:代表当前在屏幕上渲染的 UI。
workInProgress 树:在后台构建的,代表即将渲染的 UI。
当 workInProgress 树构建完成后,它会替换掉 current 树,一次性提交到 DOM。
- 高效更新:当 JSON Schema 发生变化时(例如,设计器中拖拽了一个组件),我们的动态渲染器会生成新的 React 元素树。Fiber 算法能高效地比较新旧树,只更新实际发生变化的 DOM 部分,避免了不必要的重绘。
- 平滑的用户体验:即使 Schema 变化导致大量的组件需要重新渲染,Fiber 的可中断特性也能确保 UI 响应的流畅性,避免页面卡顿。
- 状态管理基础:每个 Fiber 节点都可以持有组件的本地状态(如
useState),这为我们管理动态生成组件的状态提供了天然的基础。当我们将 JSON Schema 映射到 React 组件时,这些组件自然就能利用 React 的状态管理能力。
- 错误边界 (Error Boundaries):Fiber 架构允许在组件树中定义错误边界,捕获子组件渲染过程中的错误,防止整个应用崩溃,这对于动态生成的复杂 UI 尤为重要。
简而言之,Fiber 架构为我们的动态渲染器提供了一个坚实而高效的底层支撑,使得我们能够将复杂的、不断变化的 JSON Schema 描述,可靠地转化为高性能、高响应的交互式用户界面。
四、JSON Schema:UI 描述的蓝图
在低代码语境下,JSON Schema 不仅仅用于数据验证,更被扩展为一种强大的 UI 描述语言。它以声明式的方式定义了页面的结构、组件类型、属性、数据绑定和交互逻辑。
让我们看一个简化的低代码 JSON Schema 示例,它描述了一个包含输入框和下拉选择器的表单:
{
"type": "object",
"name": "userForm",
"title": "用户信息",
"properties": {
"username": {
"type": "string",
"title": "用户名",
"description": "请输入用户的姓名",
"widget": "Input",
"placeholder": "张三",
"readOnly": false,
"rules": ["required", {"pattern": "^[a-zA-Z0-9]{3,16}$", "message": "3-16 位字母数字"}],
"layout": { "span": 12 }
},
"age": {
"type": "number",
"title": "年龄",
"widget": "InputNumber",
"min": 0,
"max": 120,
"default": 18,
"visible": "{{ formData.username !== '' }}"
},
"gender": {
"type": "string",
"title": "性别",
"widget": "Select",
"options": [
{"label": "男", "value": "male"},
{"label": "女", "value": "female"}
],
"default": "male"
},
"interests": {
"type": "array",
"title": "兴趣爱好",
"widget": "CheckboxGroup",
"items": { "type": "string" },
"options": [
{"label": "编程", "value": "coding"},
{"label": "阅读", "value": "reading"},
{"label": "运动", "value": "sport"}
]
}
},
"actions": [
{
"type": "button",
"text": "提交",
"onClick": "function() { console.log('表单数据:', formData); alert('提交成功!'); }",
"props": { "type": "primary" }
}
]
}
从这个 Schema 中,我们可以观察到几个关键点:
**type**: 标准 JSON Schema 类型,如 object, string, number, array。
**widget**: 这是低代码引擎特有的扩展,指定了应该渲染哪个具体的前端组件(例如 Input, Select, InputNumber)。这是 Schema 与 React 组件库之间的桥梁。
- 通用属性:
title, description, placeholder, default, rules 等,这些属性会直接映射到 React 组件的 props。
- 特定组件属性:
min, max (InputNumber), options (Select, CheckboxGroup)。
- 布局信息:
layout 属性,用于描述组件在页面中的布局方式(如 span for Ant Design Grid)。
- 动态表达式:
visible: "{{ formData.username !== '' }}",这是一个强大的特性,允许根据运行时数据动态控制组件的可见性、禁用状态等。
- 事件绑定:
onClick 属性可以绑定一段 JavaScript 代码或表达式,用于定义组件的交互行为。
- 嵌套结构:
properties 用于描述对象字段,items 用于描述数组元素。
我们的任务就是将这样一份富有表现力的蓝图,转化为一个功能完备的 React UI。
五、从 JSON Schema 到 React 元素的转换核心机制
现在,我们进入核心部分:如何将 JSON Schema 转换为 React 元素树并管理其状态。
A. 组件注册表 (Component Registry)
首先,渲染器需要知道当 Schema 中指定 widget 为 "Input" 时,应该使用哪个 React 组件。这通过一个组件注册表来实现。
import React from 'react';
import { Input, Select, InputNumber, Checkbox, CheckboxGroup, Button } from 'antd';
const FormItemWrapper: React.FC<{ title?: string; description?: string; children: React.ReactNode }> = ({ title, description, children }) => (
<div style={{ marginBottom: 16 }}>
{title && <label style={{ display: 'block', fontWeight: 'bold' }}>{title}</label>}
{description && <small style={{ display: 'block', color: '#888' }}>{description}</small>}
{children}
</div>
);
export const componentRegistry = new Map<string, React.ComponentType<any>>();
componentRegistry.set('Input', Input);
componentRegistry.set('InputNumber', InputNumber);
componentRegistry.set('Select', Select);
componentRegistry.set('Checkbox', Checkbox);
componentRegistry.set('CheckboxGroup', CheckboxGroup);
componentRegistry.set('Button', Button);
componentRegistry.set('FormItemWrapper', FormItemWrapper);
B. 递归渲染器 (Recursive Renderer)
渲染器的核心是一个递归函数,它遍历 JSON Schema 树,为每个节点生成对应的 React 元素。
import React from 'react';
import { componentRegistry } from '../components/registry';
import { evaluateExpression } from './expressionEngine';
export interface SchemaNode {
type: string;
widget?: string;
title?: string;
description?: string;
properties?: { [key: string]: SchemaNode };
items?: SchemaNode;
options?: Array<{ label: string; value: any }>;
default?: any;
visible?: string;
readOnly?: boolean | string;
[key: string]: any;
}
export interface RenderConfig {
formData: { [key: string]: any };
onDataChange: (path: string, value: any) => void;
pathPrefix: string;
}
export const renderSchemaNode = ( schemaNode: SchemaNode, config: RenderConfig ): React.ReactNode => {
const { formData, onDataChange, pathPrefix } = config;
const isVisible = schemaNode.visible ? evaluateExpression(schemaNode.visible, { formData }) : true;
if (!isVisible) { return null; }
const isReadOnly = schemaNode.readOnly ? evaluateExpression(schemaNode.readOnly, { formData }) : false;
let ComponentToRender: React.ComponentType<any> | undefined;
let componentProps: { [key: string]: any } = { ...schemaNode };
if (schemaNode.widget) {
ComponentToRender = componentRegistry.get(schemaNode.widget);
if (!ComponentToRender) {
console.warn(`Widget "${schemaNode.widget}" not found in registry.`);
ComponentToRender = () => <div>Unknown Widget: {schemaNode.widget}</div>;
}
} else {
switch (schemaNode.type) {
case 'string': ComponentToRender = componentRegistry.get('Input'); break;
case 'number': ComponentToRender = componentRegistry.get('InputNumber'); break;
case 'boolean': ComponentToRender = componentRegistry.get('Checkbox'); break;
case 'object': ComponentToRender = ({ children }) => <div style={{ border: '1px solid #eee', padding: 10 }}>{children}</div>; break;
case 'array': ComponentToRender = ({ children }) => <div>{children}</div>; break;
default: ComponentToRender = () => <div>Unsupported Type: {schemaNode.type}</div>;
}
}
if (!ComponentToRender) { return null; }
let children: React.ReactNode = null;
if (schemaNode.type === 'object' && schemaNode.properties) {
children = Object.keys(schemaNode.properties).map((key) => {
const propSchema = schemaNode.properties![key];
const currentPath = pathPrefix ? `${pathPrefix}.${key}` : key;
return (
<React.Fragment key={key}>
{renderSchemaNode(propSchema, { ...config, pathPrefix: currentPath })}
</React.Fragment>
);
});
} else if (schemaNode.type === 'array' && schemaNode.items) {
const arrayData = (formData[pathPrefix] || []) as any[];
children = arrayData.map((itemValue, index) => {
const currentPath = `${pathPrefix}[${index}]`;
return (
<React.Fragment key={index}>
{renderSchemaNode(schemaNode.items!, { ...config, pathPrefix: currentPath, formData: { ...formData, [pathPrefix]: itemValue } })}
</React.Fragment>
);
});
} else if (schemaNode.widget === 'Button') {
children = schemaNode.text || 'Button';
}
if (['Input', 'InputNumber', 'Select', 'Checkbox', 'CheckboxGroup'].includes(schemaNode.widget || schemaNode.type)) {
const currentPathValue = pathPrefix ? getPathValue(formData, pathPrefix) : undefined;
componentProps.value = currentPathValue === undefined ? schemaNode.default : currentPathValue;
componentProps.onChange = (e: any) => {
let newValue = e && typeof e === 'object' && 'target' in e ? e.target.value : e;
if (schemaNode.widget === 'CheckboxGroup' && Array.isArray(newValue)) { }
else if (schemaNode.type === 'number') { newValue = parseFloat(newValue); if (isNaN(newValue)) newValue = undefined; }
onDataChange(pathPrefix, newValue);
};
}
if (schemaNode.onClick) {
componentProps.onClick = () => {
try {
const func = new Function('formData', schemaNode.onClick as string);
func(formData);
} catch (error) {
console.error('Error executing onClick handler:', error);
}
};
}
if (isReadOnly) {
componentProps.readOnly = true;
componentProps.disabled = true;
}
const WrappedComponent = componentRegistry.get('FormItemWrapper');
if (WrappedComponent && (schemaNode.title || schemaNode.description) && schemaNode.type !== 'object' && schemaNode.type !== 'array' && schemaNode.widget !== 'Button') {
return (
<WrappedComponent title={schemaNode.title} description={schemaNode.description}>
<ComponentToRender {...componentProps}>{children}</ComponentToRender>
</WrappedComponent>
);
} else {
return <ComponentToRender {...componentProps}>{children}</ComponentToRender>;
}
};
function getPathValue(obj: any, path: string): any {
if (!obj || !path) return undefined;
return path.split('.').reduce((acc, part) => {
const match = part.match(/(.*)[(d+)]/);
if (match) {
const arrayKey = match[1];
const index = parseInt(match[2], 10);
return acc && acc[arrayKey] ? acc[arrayKey][index] : undefined;
}
return acc ? acc[part] : undefined;
}, obj);
}
C. 属性映射与数据绑定 (Property Mapping & Data Binding)
如上所示,renderSchemaNode 函数在生成 React 组件时,会执行以下关键的属性映射和数据绑定逻辑:
- 直接属性传递: Schema 节点中的大部分属性(如
placeholder, min, max, options 等)可以直接作为 props 传递给对应的 React 组件。
value 绑定: 对于表单输入组件,value prop 会绑定到 config.formData 中对应 pathPrefix 的值。如果 formData 中没有值,则使用 Schema 中定义的 default 值。
onChange 事件: onChange 事件处理器负责将组件的新值更新回 config.formData。这需要一个 onDataChange 回调函数,它接收 path 和 newValue,并负责更新根组件的状态。
- 路径处理:
pathPrefix 参数至关重要,它确保了在复杂的嵌套结构中,能够精确地定位到需要更新的数据字段。
- 类型转换: 在
onChange 中,需要根据 Schema 定义的 type 对值进行适当的转换。
D. 状态管理与数据流 (State Management & Data Flow)
整个动态渲染器需要一个顶层组件来持有和管理整个页面的数据状态。当子组件通过 onChange 触发 onDataChange 回调时,这个顶层组件会更新其状态,从而引发整个渲染器重新运行。
import React, { useState, useCallback } from 'react';
import { renderSchemaNode, SchemaNode } from './renderSchemaNode';
import { setPathValue } from './utils';
interface DynamicFormRendererProps {
schema: SchemaNode;
initialData?: { [key: string]: any };
onFormSubmit?: (data: { [key: string]: any }) => void;
}
export const DynamicFormRenderer: React.FC<DynamicFormRendererProps> = ({ schema, initialData = {}, onFormSubmit }) => {
const [formData, setFormData] = useState<{ [key: string]: any }>(initialData);
const handleDataChange = useCallback((path: string, value: any) => {
setFormData((prevData) => {
const newData = { ...prevData };
setPathValue(newData, path, value);
return newData;
});
}, []);
const handleSubmit = useCallback(() => {
if (onFormSubmit) { onFormSubmit(formData); }
}, [formData, onFormSubmit]);
const renderConfig = {
formData,
onDataChange: handleDataChange,
pathPrefix: '',
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
{renderSchemaNode(schema, renderConfig)}
{!schema.actions?.some(action => action.type === 'button' && action.text === '提交') && (
<button type="submit" style={{ marginTop: 20 }}>提交表单 (Fallback)</button>
)}
</form>
);
};
E. 事件处理与行为定义 (Event Handling & Behavior Definition)
低代码引擎不仅要渲染 UI,还要赋予 UI 交互能力。Schema 中的 onClick 或其他事件属性允许我们定义这些行为。
- 表达式或函数字符串: 如
schemaNode.onClick: "function() { console.log('Hello'); }"。
- 沙箱执行: 直接
eval() 或 new Function() 存在安全风险。生产环境中,我们需要一个沙箱环境来安全地执行这些代码。常见的方案包括 Web Worker、iframe 沙箱或自定义表达式解析器。
这里为了演示方便,我们暂时使用 new Function()。
export const evaluateExpression = (expression: string, context: { [key: string]: any }): any => {
try {
const cleanedExpression = expression.startsWith('{{') && expression.endsWith('}}') ? expression.slice(2, -2).trim() : expression;
const paramNames = Object.keys(context);
const paramValues = Object.values(context);
const dynamicFunction = new Function(...paramNames, `return ${cleanedExpression};`);
return dynamicFunction(...paramValues);
} catch (error) {
console.error('Error evaluating expression:', expression, error);
return undefined;
}
};
六、动态渲染器的进阶特性与挑战
上述核心机制已经能处理大部分基本情况,但在实际的低代码引擎中,我们还需要考虑更多进阶特性和挑战。
A. 表达式引擎 (Expression Engine)
一个健壮的表达式引擎是低代码动态性的基石。它不仅用于 visible 和 readOnly,还可以用于计算属性、默认值、禁用状态、校验规则等。
- 安全性: 必须防止任意代码执行,避免 XSS 攻击。
- 性能: 频繁的表达式计算不能影响 UI 性能。
- 语法: 支持哪些 JavaScript 语法?是否需要扩展自定义函数?
B. 布局系统集成 (Layout System Integration)
低代码页面通常需要灵活的布局。Schema 应该能够描述布局信息,如 Grid System (Ant Design Row/Col)、Flexbox 等。渲染器需要在 renderSchemaNode 内部或外部包裹层中,根据 Schema 的 layout 属性动态渲染布局组件。
C. 自定义组件与扩展机制 (Custom Components & Extension)
低代码引擎的强大之处在于其可扩展性。用户应该能够注册自己的 React 组件,并使其可在设计器和渲染器中使用。
export const registerCustomComponent = (name: string, Component: React.ComponentType<any>) => {
if (componentRegistry.has(name)) { console.warn(`Component "${name}" already exists.`); }
componentRegistry.set(name, Component);
};
D. 性能优化策略 (Performance Optimization Strategies)
React.memo / useCallback / useMemo: 广泛使用这些优化 Hook。
- 虚拟化 (Virtualization): 对于长列表或大型表格,使用
react-window 进行虚拟化渲染。
- 防抖/节流 (Debouncing/Throttling): 对频繁触发的事件进行处理。
E. 错误处理与调试 (Error Handling & Debugging)
- React Error Boundaries: 在渲染器根部添加错误边界,捕获组件渲染阶段的错误。
- Schema 校验: 在加载 Schema 时对其进行预校验。
F. 运行时与设计时的考量 (Runtime vs. Design-time Considerations)
渲染器通常需要同时服务于两个场景:运行时(追求性能和稳定性)和设计时(支持选中、拖拽、属性编辑)。同一个渲染器可以通过传递不同的 props 或 context 来适应这两种模式。
七、真实场景案例分析:构建一个动态表单
让我们把前面的概念综合起来,看一个更完整的 JSON Schema 和它如何被渲染的示意。
JSON Schema (formSchema.json)
{
"type": "object",
"name": "registrationForm",
"title": "用户注册",
"properties": {
"username": {
"type": "string",
"title": "用户名",
"widget": "Input",
"placeholder": "请输入用户名",
"rules": [{"required": true, "message": "用户名必填"}],
"layout": {"span": 12}
},
"password": {
"type": "string",
"title": "密码",
"widget": "Input.Password",
"placeholder": "请输入密码",
"rules": [{"required": true, "message": "密码必填"}],
"layout": {"span": 12}
},
"confirmPassword": {
"type": "string",
"title": "确认密码",
"widget": "Input.Password",
"placeholder": "请再次输入密码",
"rules": [{"required": true, "message": "请确认密码"}],
"visible": "{{ formData.password && formData.password.length > 0 }}"
},
"role": {
"type": "string",
"title": "角色",
"widget": "Select",
"options": [
{"label": "普通用户", "value": "user"},
{"label": "管理员", "value": "admin"}
],
"default": "user",
"layout": {"span": 24}
},
"isAdmin": {
"type": "boolean",
"title": "是否管理员",
"widget": "Checkbox",
"visible": "{{ formData.role === 'admin' }}"
},
"permissions": {
"type": "array",
"title": "权限",
"widget": "CheckboxGroup",
"items": {"type": "string"},
"options": [
{"label": "读", "value": "read"},
{"label": "写", "value": "write"}
],
"visible": "{{ formData.isAdmin === true }}"
}
},
"actions": [
{
"type": "button",
"text": "注册",
"props": {"type": "primary"},
"onClick": "function(formData) { console.log('注册数据:', formData); alert('注册成功!'); }"
}
]
}
| Schema 属性/结构 | 对应的 React 元素/逻辑 | 关键点 |
|---|
type: "object" | 一个 <div> 或 <form> 容器 | 递归渲染其 properties |
properties | 遍历每个属性,递归调用 renderSchemaNode | 构建子组件树 |
username, password | <FormItemWrapper><Input /></FormItemWrapper> | widget: "Input" 映射,value 和 onChange 双向绑定 |
confirmPassword | <FormItemWrapper><Input.Password /></FormItemWrapper> | visible 表达式动态计算,决定是否渲染 |
role | <FormItemWrapper><Select /></FormItemWrapper> | options 属性直接传递给 Select |
isAdmin | <FormItemWrapper><Checkbox /></FormItemWrapper> | visible 表达式动态控制渲染 |
permissions | <FormItemWrapper><CheckboxGroup /></FormItemWrapper> | visible 表达式动态控制渲染 |
actions (button) | <Button type="primary" onClick={...}>注册</Button> | onClick 属性执行沙箱中的 JavaScript 代码 |
通过这种方式,一份结构化的 JSON Schema 就能被我们的动态渲染器'编译'成一个功能完整的、带状态的 React 表单。
八、未来展望:智能化与标准化
低代码引擎的动态渲染器是一个持续演进的领域,未来可能会有以下几个发展方向:
- AI 赋能: 利用 AI 辅助 Schema 生成,例如通过自然语言描述自动生成表单结构。
- Web Components 兼容: 探索使用 Web Components 作为底层组件标准,实现跨框架复用。
- 可视化编程增强: 将渲染器与更高级的逻辑编排工具深度融合。
- 标准化: 推动低代码领域的 Schema 规范和组件 API 标准化。
- 性能极限: 针对超大型场景,探索 WebAssembly、Rust 等技术栈与 React 结合。
九、结语
低代码引擎的核心渲染机制,在于将声明式的 JSON Schema 通过递归映射、智能状态管理和事件绑定,转化为响应式的 React Fiber 树,从而实现高度可配置和动态的 UI 构建。我们通过组件注册、递归渲染、路径化数据绑定和表达式引擎等关键技术,成功地将静态的配置蓝图转化为生动的交互界面,为低代码的普及和发展奠定了坚实的基础。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown 转 HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
- HTML 转 Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online