Skip to content

Sandbox(沙箱环境)实现原理与关键代码

Sandbox 是 Studio 低代码平台的核心运行环境,负责组件渲染、拖拽交互和实时预览。它采用了类似小程序的架构,实现了安全隔离的运行时环境,同时提供了与 Studio 编辑器的双向通信能力。

1. 核心架构设计

1.1 分层设计

Sandbox 环境采用了经典的分层架构设计:

  • 服务层(Service Layer):处理业务逻辑和数据处理
  • 视图层(View Layer):负责 UI 渲染和用户交互
  • 通信层(Communication Layer):基于 WebSocket 的跨层通信机制

1.2 关键模块

  • WebSocket 通信模块:WsClient

    • 实现双向通信,支持消息分发和订阅
  • 组件渲染引擎

    • 基于 RayRunner(类似小程序运行时)的组件渲染系统
    • JSON-Renderer:将 JSON 配置转换为实际 UI 组件
  • 交互控制模块

    • MoveableManager: 处理绝对定位布局的拖拽、调整
    • SortableManager: 处理网格布局的排序和交互
  • 资源加载器:

    • loadResources: 动态加载组件资源和样式

2. 关键代码实现

2.1 沙箱服务启动

javascript
// serve.js
const app = express();
const PORT = process.env.PORT || 3000;

// 静态资源处理
app.use(express.static(path.join(__dirname, "static")));
app.use(express.static(path.join(__dirname, "dev-www")));

// HTML 资源处理中间件,支持代理模式
const htmlController = (req, res, next) => {
  const isProxy = "x-proxy-prefix" in req.headers;
  // 检测代理前缀并注入重定向脚本
  if (reqPath.endsWith(".html") && isProxy) {
    // 处理 HTML 资源
    const script = `<script>window.__redirectPath = '/studio-sandbox/'</script>`;
    html = html.replace("<head>", `<head>${script}`);
    res.send(html);
  } else {
    next();
  }
};

// 启动 HTTP 服务
httpServer.listen(PORT, () => {
  console.log(`Server is running on port http://localhost:${PORT}`);
});

2.2 页面服务实现

typescript
// pageService.ts
import { WsClient } from "./shared/WsClient";
import { getComponentFromRayMaterial } from "./shared/getComponentFromRayMaterial";

const ws = new WsClient({ side: "miniapp" });

Mini.registerEntry("pages/tab1/index", function (ty, Promise) {
  const Ray = RayRunner.Ray;
  const React = RayRunner.React;
  const Components = RayRunner.Components;
  const Material = RayMaterial;

  // 处理渲染数据,将 JSON 配置转换为 React 组件
  function handlerRenderData(data) {
    if (!data) return null;
    if (typeof data === "string") {
      return data;
    }

    // 获取组件实例
    let tagComponent = getComponentFromRayMaterial(data.tag);

    // 处理包装组件
    if (data.__wrapper__) {
      return {
        tag: Material[data.__wrapper__].default,
        props: {
          instanceId,
          isGroup: data.isGroup,
          pageType: data.pageType,
          containerStyle: data.__fixedStyle__ || data.__wrapperStyle__,
          dpCodes,
          expressions,
          actions,
          // 其他属性
        },
        children: () => {
          return {
            tag: tagComponent,
            props: pureProps,
            children: data.children && data.children.map(handlerRenderData),
          };
        },
      };
    } else {
      // 处理普通组件
      return {
        tag: tagComponent,
        props: data.props,
        children: data.children && data.children.map(handlerRenderData),
      };
    }
  }

  // 主页面组件实现
  const home_default = () => {
    // 使用 React 状态管理页面渲染
    const [renderKey, setRenderKey] = React.useState(0);
    const [renderData, setRenderData] = React.useState();

    // 监听页面配置变更消息
    React.useEffect(() => {
      ws.on("page_config_change", ({ payload }) => {
        setPageConfig(payload);
      });

      // 监听应用上下文变更
      ws.on("app_context_change", function ({ payload }) {
        const { currentLang, featureLocales, schema, staticPrefix } = payload;
        // 更新上下文数据
        ContextContainer.StudioObject.Strings.merge(featureLocales);
        ContextContainer.StudioObject.Strings.setLanguage(currentLang);
        ContextContainer.StudioObject.schema = JSON.parse(
          JSON.stringify(schema)
        );

        // 刷新渲染
        setRenderKey((pre: number) => pre + 1);
      });

      // 处理模板配置完成消息
      ws.on("template_configuration_complete", ({ payload, from }) => {
        if (from === "service") {
          let { tree, renderKey } = payload;
          if (renderKey) {
            setRenderKey(renderKey);
          }
          if (tree) {
            setRenderData(handlerRenderData(tree));
          }
        }
      });
    }, []);

    // 渲染组件
    return React.createElement(ContainerRenderContext, {
      key: renderKey,
      config: {
        renderData,
        // 其他配置
      },
    });
  };

  // 注册页面
  RayRunner.createPage(home_default, "pages/tab1/index");
});

2.3 页面视图与交互实现

typescript
// pageView.ts
import { MoveableManager } from "./MoveableManager";
import { SortableManager } from "./SortableManager";
import { WsClient } from "./shared/WsClient";
import { loadResources } from "./shared/loadResources";
import { viewport } from "./viewport";

// 初始化 WebSocket
const ws = new WsClient({ side: "template" });
viewport.bindWs(ws);

// 绝对定位拖拽管理器
const move = new MoveableManager(moveableClasses);

// 拖拽取消回调
move.onCancel(() => {
  if (viewport.isGridLayout()) return;
  if (viewport.getSelectTarget()) {
    viewport.clearSelectTarget();
  }
});

// 拖拽更新回调
move.onUpdate((res) => {
  if (viewport.isGridLayout()) return;
  if (res.type === "drag") {
    ws.emit({
      to: ["studio"],
      type: "template_drag_move",
      payload: { ...res },
    });
  } else if (res.type === "resize") {
    ws.emit({
      to: ["studio"],
      type: "template_on_resize",
      payload: { ...res },
    });
  }
});

// 网格布局排序管理器
const sortable = new SortableManager({
  ...sortableClasses,
  onCancel() {
    viewport.clearSelectTarget();
  },
  onSort(res) {
    ws.emit({
      to: ["studio"],
      type: "template_sort_complete",
      payload: { ...res },
    });
  },
});

// 监听渲染完成事件
ws.on("miniapp_render_complete", ({ payload, from }) => {
  if (from === "miniapp") {
    // 清除选择状态
    move.clearAllSelections();

    // 根据页面类型初始化不同的交互管理器
    if (viewport.isGridLayout()) {
      sortable.init();
    } else {
      move.init();
    }

    // 如果有自动选择目标,应用选择
    if (viewport.autoSelectTargetId) {
      viewport.clearSelectTarget({ emitEvent: false });
      viewport.applyAutoSelectTarget((elem) => {});
    }

    // 发送渲染完成事件
    window.requestAnimationFrame(() => {
      const rootStyle = viewport.isGridLayout()
        ? sortable.getRootStyle()
        : move.getRootStyle();
      if (rootStyle) {
        ws.emit({
          to: ["studio"],
          type: "template_render_complete",
          payload: { rootStyle },
        });
      }
    });
  }
});

// 处理 Studio 发起的渲染请求
ws.on("studio_render_start", ({ payload, from }) => {
  const { tree, highlightId } = payload;

  if (highlightId) {
    viewport.clearSelectTarget({ emitEvent: false });
    viewport.setAutoSelectTargetId(highlightId);
  }

  if (tree) {
    const { pageType = PAGE_TYPE.ABSOLUTE } = tree;
    viewport.setPageType(pageType);
    document.documentElement.setAttribute("data-page-layout", pageType);
  }

  // 处理资源加载
  if (payload.assetsResource) {
    let { usingComponents, viewAssets } = payload.assetsResource;
    ws.emit({
      to: ["webshell"],
      type: "load_assets",
      payload: { action: "start" },
    });

    loadResources(viewAssets, () => {
      Mini.updatePageConfig(
        "pages/tab1/index",
        (config) => {
          config.usingComponents = {
            ...config.usingComponents,
            ...usingComponents,
          };
          return config;
        },
        () => {
          ws.emit({
            to: ["webshell"],
            type: "load_assets",
            payload: { action: "done" },
          });
          ws.emit({
            to: ["service"],
            type: "template_view_configuration_complete",
            payload,
          });
        }
      );
    });
  }
});

2.4 视口管理与组件选择处理

typescript
// viewport.ts (大致实现)
export const viewport = {
  selectTarget: null,
  autoSelectTargetId: "",
  pageType: "absolute",
  ws: null,

  bindWs(wsClient) {
    this.ws = wsClient;
  },

  isGridLayout() {
    return this.pageType === "grid";
  },

  setPageType(type) {
    this.pageType = type;
  },

  getSelectTarget() {
    return this.selectTarget;
  },

  setSelectTarget(target) {
    this.selectTarget = target;

    if (this.ws) {
      this.ws.emit({
        to: ["studio"],
        type: "template_select_component",
        payload: {
          instanceId: target.getAttribute("instanceid"),
        },
      });
    }
  },

  clearSelectTarget({ emitEvent = true } = {}) {
    if (!this.selectTarget) return;

    this.selectTarget = null;

    if (emitEvent && this.ws) {
      this.ws.emit({
        to: ["studio"],
        type: "template_clear_select",
        payload: {},
      });
    }
  },

  setAutoSelectTargetId(id) {
    this.autoSelectTargetId = id;
  },

  applyAutoSelectTarget(callback) {
    if (!this.autoSelectTargetId) return;

    const target = document.querySelector(
      `[instanceid="${this.autoSelectTargetId}"]`
    );
    if (target) {
      this.setSelectTarget(target);
      if (callback) callback(target);
    }
  },
};

3. Sandbox 的核心技术原理

3.1 隔离沙箱机制

Sandbox 环境通过以下机制实现了安全隔离:

  • 独立运行时:基于小程序框架的独立运行时,避免全局污染
  • iframe 隔离:使用 iframe 实现 DOM 和 JavaScript 隔离
  • 资源隔离:通过独立的资源加载机制实现资源隔离

3.2 组件渲染机制

沙箱中的组件渲染采用了声明式语法:

  • JSON 描述:使用 JSON 对象描述组件的结构、属性和关系
  • 组件解析:将 JSON 描述转换为实际的组件对象
  • 动态渲染:使用 React 组件树进行 UI 渲染
typescript
// 组件树渲染示例
function handlerRenderData(data) {
  if (!data) return null;

  // 获取组件类型
  let tagComponent = getComponentFromRayMaterial(data.tag);

  return {
    tag: tagComponent,
    props: data.props,
    children: data.children && data.children.map(handlerRenderData),
  };
}

3.3 交互控制系统

沙箱环境实现了两种不同的布局交互控制系统:

  • 绝对定位(Moveable):
    • 支持组件自由拖拽、大小调整
    • 基于坐标系的精确定位
  • 网格布局(Sortable):
    • 支持组件排序和拖放
    • 基于网格的自适应布局

3.4 实时通信机制

基于 WebSocket 的实时通信机制支持以下功能:

  • 事件驱动:通过事件订阅和发布实现松耦合通信
  • 多端通信:支持编辑器、沙箱、逻辑层之间的多端通信
  • 消息路由:根据目标路由消息到正确的接收方
typescript
// WebSocket 通信示例
ws.emit({
  to: ["studio"], // 目标接收方
  type: "template_select_component", // 消息类型
  payload: {
    // 消息数据
    instanceId: target.getAttribute("instanceid"),
  },
});

// 监听消息
ws.on("page_config_change", ({ payload }) => {
  // 处理页面配置变更
  setPageConfig(payload);
});

4. 关键交互流程

4.1 组件拖拽流程

  1. 用户在编辑器中拖拽组件到画布
  2. 编辑器发送配置到沙箱
  3. 沙箱渲染并触发选中高亮
  4. 用户调整组件位置或大小
  5. 沙箱发送调整信息回编辑器
  6. 编辑器更新组件树配置

4.2 页面渲染流程

  1. 编辑器生成页面 JSON 描述
  2. 通过 WebSocket 发送到沙箱
  3. 沙箱加载必要资源(样式、组件)
  4. 沙箱解析 JSON 并渲染组件树
  5. 渲染完成后通知编辑器
  6. 编辑器更新 UI 状态

5. 总结

Sandbox 环境结合了现代 Web 技术和小程序的架构思想,实现了一个高性能、可扩展的低代码运行环境,为 IoT 设备控制应用的快速开发提供了坚实的技术基础。

基于 MIT 许可发布