AntV G6新版源码浅析

简介: 本文旨在通过简要分析G6 5.x版本源码来对图可视领域的一些底层引擎进行一个大致了解,同时也为G6引擎的社区共建共享提供一些力量,可以更好的提供插件化功能的编写。

「AntV 」G6新版本源码浅析.png

前言

AntV是蚂蚁金服全新一代数据可视化解决方案,其中G6主要用于解决图可视领域相关的前端可视化问题,其是一个简单、易用、完备的图可视化引擎。本文旨在通过简要分析G6 5.x版本源码来对图可视领域的一些底层引擎进行一个大致了解,同时也为G6引擎的社区共建共享提供一些力量,可以更好的提供插件化功能的编写。

架构

g6.png

新版G6整体是基于“插件化”的架构进行设计的,对外整体暴露Graph类及StdLib标准库,将主题、数据处理、布局、视图、类型、交互等均作为插件来进行处理,提供更高层次的定制化需求,提升更好的开源能力。

目录

整体采用monorepo进行源码的仓库管理

  • packages
    • g6
      • docs
      • src
        • constant
          • index.ts
          • shape.ts
        • item
          • combo.ts
          • edge.ts
          • item.ts
          • node.ts
        • runtime
          • controller
            • data.ts
            • extensions.ts
            • index.ts
            • interaction.ts
            • item.ts
            • layout.ts
            • plugin.ts
            • theme.ts
            • viewport.ts
          • graph.ts
          • hooks.ts
        • stdlib
          • behavior
            • activate-relations.ts
            • brush-select.ts
            • click-select.ts
            • drag-canvas.ts
            • drag-node.ts
            • hover-activate.ts
            • lasso-select.ts
            • orbit-canvas-3d.ts
            • rotate-canvas-3d.ts
            • track-canvas-3d.ts
            • zoom-canvas-3d.ts
            • zoom-canvas.ts
          • data
            • comboFromNode.ts
          • item
            • edge
              • base.ts
              • index.ts
              • line.ts
            • node
              • base.ts
              • base3d.ts
              • circle.ts
              • index.ts
              • sphere.ts
          • plugin
            • grid
              • index.ts
            • legend
              • index.ts
            • minimap
              • index.ts
          • selector
            • lasso.ts
            • rect.ts
          • theme
            • dark.ts
            • light.ts
          • themeSolver
            • base.ts
            • spec.ts
            • subject.ts
          • index.ts
        • types
          • animate.ts
          • behavior.ts
          • combo.ts
          • common.ts
          • data.ts
          • edge.ts
          • event.ts
          • graph.ts
          • hook.ts
          • index.ts
          • item.ts
          • layout.ts
          • node.ts
          • plugin.ts
          • render.ts
          • spec.ts
          • stdlib.ts
          • theme.ts
          • view.ts
        • util
          • animate.ts
          • array.ts
          • canvas.ts
          • event.ts
          • extend.ts
          • extension.ts
          • index.ts
          • item.ts
          • mapper.ts
          • math.ts
          • point.ts
          • shape.ts
          • shape3d.ts
          • text.ts
          • type.ts
          • zoom.ts
        • index.ts
      • tests

源码

从架构层次可以看出,整体对外暴露的就是Graph的类以及stdLib的标准库,因而在分析源码调用过程中,我们抓住Graph进行逐步的往外拓展,从而把握整体的一个设计链路,避免陷入局部无法抽离。

Graph

对外暴露的Graph类是整个G6图的核心类

// https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts
export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
  extends EventEmitter
  implements IGraph<B, T>
{
   
   
  public hooks: Hooks;
  // for nodes and edges, which will be separate into groups
  public canvas: Canvas;
  // the container dom for the graph canvas
  public container: HTMLElement;
  // the tag to indicate whether the graph instance is destroyed
  public destroyed: boolean;
  // the renderer type of current graph
  public rendererType: RendererName;
  // for transient shapes for interactions, e.g. transient node and related edges while draging, delegates
  public transientCanvas: Canvas;
  // for background shapes, e.g. grid, pipe indices
  public backgroundCanvas: Canvas;
  // the tag indicates all the three canvases are all ready
  private canvasReady: boolean;
  private specification: Specification<B, T>;
  private dataController: DataController;
  private interactionController: InteractionController;
  private layoutController: LayoutController;
  private viewportController: ViewportController;
  private itemController: ItemController;
  private extensionController: ExtensionController;
  private themeController: ThemeController;
  private pluginController: PluginController;

  private defaultSpecification = {
   
   
    theme: {
   
   
      type: 'spec',
      base: 'light',
    },
  };

  constructor(spec: Specification<B, T>) {
   
   
    super();
    // TODO: analyse cfg

    this.specification = Object.assign({
   
   }, this.defaultSpecification, spec);
    this.initHooks();
    this.initCanvas();
    this.initControllers();

    this.hooks.init.emit({
   
   
      canvases: {
   
   
        background: this.backgroundCanvas,
        main: this.canvas,
        transient: this.transientCanvas,
      },
    });

    const {
   
    data } = spec;
    if (data) {
   
   
      // TODO: handle multiple type data configs
      this.read(data as GraphData);
    }
  }

  // 初始化控制器,用于各种类型插件的依赖注入
  private initControllers() {
   
   
    this.dataController = new DataController(this);
    this.interactionController = new InteractionController(this);
    this.layoutController = new LayoutController(this);
    this.themeController = new ThemeController(this);
    this.itemController = new ItemController(this);
    this.viewportController = new ViewportController(this);
    this.extensionController = new ExtensionController(this);
    this.pluginController = new PluginController(this);
  }

  // 初始化画布
  private initCanvas() {
   
   
    const {
   
    renderer, container, width, height } = this.specification;
    let pixelRatio;
    if (renderer && !isString(renderer)) {
   
   
      // @ts-ignore
      this.rendererType = renderer.type || 'canvas';
      // @ts-ignore
      pixelRatio = renderer.pixelRatio;
    } else {
   
   
      // @ts-ignore
      this.rendererType = renderer || 'canvas';
    }
    const containerDOM = isString(container)
      ? document.getElementById(container as string)
      : container;
    if (!containerDOM) {
   
   
      console.error(
        `Create graph failed. The container for graph ${
     
     containerDOM} is not exist.`,
      );
      this.destroy();
      return;
    }
    this.container = containerDOM;
    const size = [width, height];
    if (size[0] === undefined) {
   
   
      size[0] = containerDOM.scrollWidth;
    }
    if (size[1] === undefined) {
   
   
      size[1] = containerDOM.scrollHeight;
    }

    this.backgroundCanvas = createCanvas(
      this.rendererType,
      containerDOM,
      size[0],
      size[1],
      pixelRatio,
    );
    this.canvas = createCanvas(
      this.rendererType,
      containerDOM,
      size[0],
      size[1],
      pixelRatio,
    );
    this.transientCanvas = createCanvas(
      this.rendererType,
      containerDOM,
      size[0],
      size[1],
      pixelRatio,
      true,
      {
   
   
        pointerEvents: 'none',
      },
    );
    Promise.all(
      [this.backgroundCanvas, this.canvas, this.transientCanvas].map(
        (canvas) => canvas.ready,
      ),
    ).then(() => (this.canvasReady = true));
  }

  // 改变渲染类型,默认为Canvas
  public changeRenderer(type) {
   
   

  }

  // 初始化生命周期钩子函数
  private initHooks() {
   
   
    this.hooks = {
   
   
      init: new Hook<{
   
   
        canvases: {
   
   
          background: Canvas;
          main: Canvas;
          transient: Canvas;
        };
      }>({
   
    name: 'init' }),
      datachange: new Hook<{
   
    data: GraphData; type: DataChangeType }>({
   
   
        name: 'datachange',
      }),
      itemchange: new Hook<{
   
   
        type: ITEM_TYPE;
        changes: GraphChange<NodeModelData, EdgeModelData>[];
        graphCore: GraphCore;
        theme: ThemeSpecification;
      }>({
   
    name: 'itemchange' }),
      render: new Hook<{
   
   
        graphCore: GraphCore;
        theme: ThemeSpecification;
        transientCanvas: Canvas;
      }>({
   
   
        name: 'render',
      }),
      layout: new Hook<{
   
    graphCore: GraphCore }>({
   
    name: 'layout' }),
      viewportchange: new Hook<ViewportChangeHookParams>({
   
    name: 'viewport' }),
      modechange: new Hook<{
   
    mode: string }>({
   
    name: 'modechange' }),
      behaviorchange: new Hook<{
   
   
        action: 'update' | 'add' | 'remove';
        modes: string[];
        behaviors: (string | BehaviorOptionsOf<{
   
   }>)[];
      }>({
   
    name: 'behaviorchange' }),
      itemstatechange: new Hook<{
   
   
        ids: ID[];
        states?: string[];
        value?: boolean;
      }>({
   
   
        name: 'itemstatechange',
      }),
      itemvisibilitychange: new Hook<{
   
    ids: ID[]; value: boolean }>({
   
   
        name: 'itemvisibilitychange',
      }),
      transientupdate: new Hook<{
   
   
        type: ITEM_TYPE | SHAPE_TYPE;
        id: ID;
        config: {
   
   
          style: ShapeStyle;
          action: 'remove' | 'add' | 'update' | undefined;
        };
        canvas: Canvas;
      }>({
   
    name: 'transientupdate' }),
      pluginchange: new Hook<{
   
   
        action: 'update' | 'add' | 'remove';
        plugins: (
          | string
          | {
   
    key: string; type: string; [cfgName: string]: unknown }
        )[];
      }>({
   
    name: 'pluginchange' }),
      themechange: new Hook<{
   
   
        theme: ThemeSpecification;
        canvases: {
   
   
          background: Canvas;
          main: Canvas;
          transient: Canvas;
        };
      }>({
   
    name: 'init' }),
      destroy: new Hook<{
   
   }>({
   
    name: 'destroy' }),
    };
  }

  // 更改spec配置
  public updateSpecification(spec: Specification<B, T>): Specification<B, T> {
   
   

  }

  // 更改theme配置
  public updateTheme(theme: ThemeOptionsOf<T>) {
   
   

  }

  // 获取配置信息
  public getSpecification(): Specification<B, T> {
   
   

  }

  // 数据渲染,diff比对
  public async read(data: GraphData) {
   
   

  }

  // 更改图数据
  public async changeData(
    data: GraphData,
    type: 'replace' | 'mergeReplace' = 'mergeReplace',
  ) {
   
   

  }

  // 清空画布
  public clear() {
   
   

  }

  // 获取视图中心
  public getViewportCenter(): PointLike {
   
   

  }

  // 更给视图转换
  public async transform(
    options: GraphTransformOptions,
    effectTiming?: CameraAnimationOptions,
  ): Promise<void> {
   
   

  }

  // 立刻停止当前过渡变化
  public stopTransformTransition() {
   
   

  }

  // 画布位移
  public async translate(
    distance: Partial<{
   
   
      dx: number;
      dy: number;
      dz: number;
    }>,
    effectTiming?: CameraAnimationOptions,
  ) {
   
   

  }

  // 画布移动至视图坐标
  public async translateTo(
    {
   
    x, y }: PointLike,
    effectTiming?: CameraAnimationOptions,
  ) {
   
   

  }

  // 画布放大/缩小
  public async zoom(
    ratio: number,
    origin?: PointLike,
    effectTiming?: CameraAnimationOptions,
  ) {
   
   

  }

  // 画布放大/缩小至
  public async zoomTo(
    zoom: number,
    origin?: PointLike,
    effectTiming?: CameraAnimationOptions,
  ) {
   
   

  }

  // 获取画布放大/缩小比例
  public getZoom() {
   
   

  }

  // 旋转画布
  public async rotate(
    angle: number,
    origin?: PointLike,
    effectTiming?: CameraAnimationOptions,
  ) {
   
   

  }

  // 旋转画布至 
  public async rotateTo(
    angle: number,
    origin?: PointLike,
    effectTiming?: CameraAnimationOptions,
  ) {
   
   

  }

  // 自适应画布
  public async fitView(
    options?: {
   
   
      padding: Padding;
      rules: FitViewRules;
    },
    effectTiming?: CameraAnimationOptions,
  ) {
   
   

  }

  // 对齐画布中心与视图中心
  public async fitCenter(effectTiming?: CameraAnimationOptions) {
   
   

  }
  // 对齐元素
  public async focusItem(id: ID | ID[], effectTiming?: CameraAnimationOptions) {
   
   

  }

  // 获取画布大小
  public getSize(): number[] {
   
   

  }

  // 设置画布大小
  public setSize(size: number[]) {
   
   

  }

  // 获取视图下的渲染坐标
  public getCanvasByViewport(viewportPoint: Point): Point {
   
   

  }

  // 获取画布下的渲染视图
  public getViewportByCanvas(canvasPoint: Point): Point {
   
   

  }

  // 获取浏览器坐标
  public getClientByCanvas(canvasPoint: Point): Point {
   
   

  }

  // 获取画布坐标
  public getCanvasByClient(clientPoint: Point): Point {
   
   

  }

  // ===== item operations =====

  // 获取节点数据
  public getNodeData(condition: ID | Function): NodeModel | undefined {
   
   

  }

  // 获取边数据
  public getEdgeData(condition: ID | Function): EdgeModel | undefined {
   
   

  }

  // 获取combo数据
  public getComboData(condition: ID | Function): ComboModel | undefined {
   
   

  }

  // 获取所有节点数据
  public getAllNodesData(): NodeModel[] {
   
   

  }

  // 获取所有边数据
  public getAllEdgesData(): EdgeModel[] {
   
   

  }

  // 获取所有combo类型数据
  public getAllCombosData(): ComboModel[] {
   
   

  }

  // 获取相关边数据
  public getRelatedEdgesData(
    nodeId: ID,
    direction: 'in' | 'out' | 'both' = 'both',
  ): EdgeModel[] {
   
   

  }

  // 获取临近节点数据
  public getNeighborNodesData(
    nodeId: ID,
    direction: 'in' | 'out' | 'both' = 'both',
  ): NodeModel[] {
   
   

  }

  // 获取状态类型的id
  public findIdByState(
    itemType: ITEM_TYPE,
    state: string,
    value: string | boolean = true,
    additionalFilter?: (item: NodeModel | EdgeModel | ComboModel) => boolean,
  ): ID[] {
   
   

  }

  // 添加数据
  public addData(
    itemType: ITEM_TYPE,
    models:
      | NodeUserModel
      | EdgeUserModel
      | ComboUserModel
      | NodeUserModel[]
      | EdgeUserModel[]
      | ComboUserModel[],
    stack?: boolean,
  ):
    | NodeModel
    | EdgeModel
    | ComboModel
    | NodeModel[]
    | EdgeModel[]
    | ComboModel[] {
   
   

  }

  // 移除数据
  public removeData(itemType: ITEM_TYPE, ids: ID | ID[], stack?: boolean) {
   
   

  }

  // 更新数据
  public updateData(
    itemType: ITEM_TYPE,
    models:
      | Partial<NodeUserModel>
      | Partial<EdgeUserModel>
      | Partial<
          | ComboUserModel
          | Partial<NodeUserModel>[]
          | Partial<EdgeUserModel>[]
          | Partial<ComboUserModel>[]
        >,
    stack?: boolean,
  ):
    | NodeModel
    | EdgeModel
    | ComboModel
    | NodeModel[]
    | EdgeModel[]
    | ComboModel[] {
   
   

  }

  // 更新节点位置
  public updateNodePosition(
    models:
      | Partial<NodeUserModel>
      | Partial<
          ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
        >,
    stack?: boolean,
  ) {
   
   

  }

  // 显示类型元素
  public showItem(ids: ID | ID[], disableAniamte?: boolean) {
   
   

  }

  // 隐藏类型元素
  public hideItem(ids: ID | ID[], disableAniamte?: boolean) {
   
   

  }

  // 设置类型元素状态
  public setItemState(
    ids: ID | ID[],
    states: string | string[],
    value: boolean,
  ) {
   
   

  }

  // 获取类型元素状态
  public getItemState(id: ID, state: string) {
   
   

  }

  // 清空类型元素状态
  public clearItemState(ids: ID | ID[], states?: string[]) {
   
   

  }

  // 获取渲染器box
  public getRenderBBox(id: ID | undefined): AABB | false {
   
   

  }

  // 获取显示类型id
  public getItemVisible(id: ID) {
   
   

  }

  // ===== combo operations =====

  // 创建组
  public createCombo(
    combo: string | ComboUserModel,
    childrenIds: string[],
    stack?: boolean,
  ) {
   
   

  }

  // 取消组
  public uncombo(comboId: ID, stack?: boolean) {
   
   

  }

  // 释放组
  public collapseCombo(comboId: ID, stack?: boolean) {
   
   

  }

  // 扩展组
  public expandCombo(comboId: ID, stack?: boolean) {
   
   

  }

  // ===== layout =====

  // 设置布局参数
  public async layout(options?: LayoutOptions) {
   
   

  }

  // 取消布局算法
  public stopLayout() {
   
   

  }

  // 设置交互模式
  public setMode(mode: string) {
   
   

  }

  // 添加交互行为
  public addBehaviors(
    behaviors: BehaviorOptionsOf<B>[],
    modes: string | string[],
  ) {
   
   

  }

  // 移除交互行为
  public removeBehaviors(behaviorKeys: string[], modes: string | string[]) {
   
   

  }

  // 更新交互行为
  public updateBehavior(behavior: BehaviorOptionsOf<B>, mode?: string) {
   
   

  }

  // 添加插件
  public addPlugins(
    pluginCfgs: (
      | {
   
   
          key: string;
          type: string;
          [cfgName: string]: unknown; // TODO: configs from plugins
        }
      | string
    )[],
  ) {
   
   

  }

  // 移除插件
  public removePlugins(pluginKeys: string[]) {
   
   

  }

  // 更新插件
  public updatePlugin(plugin: {
   
   
    key: string;
    type: string;
    [cfg: string]: unknown;
  }) {
   
   

  }

  // 绘制过渡动效
  public drawTransient(
    type: ITEM_TYPE | SHAPE_TYPE,
    id: ID,
    config: {
   
   
      action: 'remove' | 'add' | 'update' | undefined;
      style: ShapeStyle;
      onlyDrawKeyShape?: boolean;
    },
  ): DisplayObject {
   
   

  }

  // 销毁画布
  public destroy(callback?: Function) {
   
   

  }
}

StdLib

标准库用于提供和社区开发者进行交互的标准构件,方便自定义开发及共建共享。其中,提供了dataextensionsinteractionitemlayoutplugintheme以及viewport的插件化能力。

这里,以plugin的控制器接入为例,代码如下:

// https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/controller/plugin.ts

export class PluginController {
   
   
  public extensions: any = [];
  public graph: IGraph;

  /**
   * Plugins on graph.
   * @example
   * { 'minimap': Minimap, 'tooltip': Tooltip }
   */
  private pluginMap: Map<string, {
   
    type: string; plugin: Plugin }> = new Map();

  /**
   * Listeners added by all current plugins.
   * @example
   * {
   *   'minimap': { 'afterlayout': function },
   * }
   */
  private listenersMap: Record<string, Record<string, Listener>> = {
   
   };

  constructor(graph: IGraph<any, any>) {
   
   
    this.graph = graph;
    this.tap();
  }

  /**
   * Subscribe the lifecycle of graph.
   */
  private tap() {
   
   
    this.graph.hooks.init.tap(this.onPluginInit.bind(this));
    this.graph.hooks.pluginchange.tap(this.onPluginChange.bind(this));
    this.graph.hooks.destroy.tap(this.onDestroy.bind(this));
  }

  private onPluginInit() {
   
   
    // 1. Initialize new behaviors.
    this.pluginMap.clear();
    const {
   
    graph } = this;
    const pluginConfigs = graph.getSpecification().plugins || [];
    pluginConfigs.forEach((config) => {
   
   
      this.initPlugin(config);
    });

    // 2. Add listeners for each behavior.
    this.listenersMap = {
   
   };
    this.pluginMap.forEach((item, key) => {
   
   
      const {
   
    plugin } = item;
      this.addListeners(key, plugin);
    });
  }

  private initPlugin(config) {
   
   
    const {
   
    graph } = this;
    const Plugin = getExtension(config, registry.useLib, 'plugin');
    const options = typeof config === 'string' ? {
   
   } : config;
    const type = typeof config === 'string' ? config : config.type;
    const key = typeof config === 'string' ? config : config.key || type;
    const plugin = new Plugin(options);
    plugin.init(graph);
    this.pluginMap.set(key, {
   
    type, plugin });
    return {
   
    key, type, plugin };
  }

  private onPluginChange(params: {
   
   
    action: 'update' | 'add' | 'remove';
    plugins: (string | {
   
    key: string; type: string; options: any })[];
  }) {
   
   
    const {
   
    action, plugins: pluginCfgs } = params;
    if (action === 'add') {
   
   
      pluginCfgs.forEach((config) => {
   
   
        const {
   
    key, plugin } = this.initPlugin(config);
        this.addListeners(key, plugin);
      });
      return;
    }

    if (action === 'remove') {
   
   
      pluginCfgs.forEach((config) => {
   
   
        const key =
          typeof config === 'string' ? config : config.key || config.type;
        const item = this.pluginMap.get(key);
        if (!item) return;
        const {
   
    plugin } = item;
        this.removeListeners(key);
        plugin.destroy();
        this.pluginMap.delete(key);
      });
      return;
    }

    if (action === 'update') {
   
   
      pluginCfgs.forEach((config) => {
   
   
        if (typeof config === 'string') return;
        const key = config.key || config.type;
        const item = this.pluginMap.get(key);
        if (!item) return;
        const {
   
    plugin } = item;
        plugin.updateCfgs(config);
        this.removeListeners(key);
        this.addListeners(key, plugin);
      });
      return;
    }
  }

  private addListeners = (key: string, plugin: Plugin) => {
   
   
    const events = plugin.getEvents();
    this.listenersMap[key] = {
   
   };
    Object.keys(events).forEach((eventName) => {
   
   
      // Wrap the listener with error logging.
      const listener = wrapListener(
        key,
        eventName,
        events[eventName].bind(plugin),
      );
      this.graph.on(eventName, listener);
      this.listenersMap[key][eventName] = listener;
    });
  };

  private removeListeners = (key: string) => {
   
   
    const listeners = this.listenersMap[key];
    Object.keys(listeners).forEach((eventName) => {
   
   
      const listener = listeners[eventName];
      if (listener) {
   
   
        this.graph.off(eventName, listener);
      }
    });
  };

  private onDestroy() {
   
   
    this.pluginMap.forEach((item) => {
   
   
      const {
   
    plugin } = item;
      plugin.destroy();
    });
  }

  destroy() {
   
   }
}

而其他的标准库控制器,则通过配置的方式,在Graph初始化时进行接入,代码如下:

// https://github.com/antvis/G6/blob/v5/packages/g6/src/stdlib/index.ts
const stdLib = {
   
   
  transforms: {
   
   
    comboFromNode,
  },
  themes: {
   
   
    light: LightTheme,
    dark: DarkTheme,
  },
  themeSolvers: {
   
   
    spec: SpecThemeSolver,
    subject: SubjectThemeSolver,
  },
  layouts: layoutRegistry,
  behaviors: {
   
   
    'activate-relations': ActivateRelations,
    'drag-canvas': DragCanvas,
    'hover-activate': HoverActivate,
    'zoom-canvas': ZoomCanvas,
    'drag-node': DragNode,
    'click-select': ClickSelect,
    'brush-select': BrushSelect,
    'lasso-select': LassoSelect,
    'zoom-canvas-3d': ZoomCanvas3D,
    'rotate-canvas-3d': RotateCanvas3D,
    'track-canvas-3d': TrackCanvas3D,
    'orbit-canvas-3d': OrbitCanvas3D,
  },
  plugins: {
   
   
    minimap: Minimap,
    legend: Legend,
  },
  nodes: {
   
   
    'circle-node': CircleNode,
    'sphere-node': SphereNode,
  },
  edges: {
   
   
    'line-edge': LineEdge,
  },
  combos: {
   
   },
};

export {
   
    stdLib }

总结

综上所述,AntV G6 5.0提供了更好的插件化机制,将功能进行解耦后提供暴露的通用标准制式方便开发者能够更好的开源与共建共享,从而利用开源社区的力量将G6建设成为更加通用且适配更多场景的图可视化库。开源的商业逻辑从来不在开源本身,用闭源的思路做开源就失去了开源的价值,共勉!!!

参考

相关文章
|
6月前
|
移动开发 数据可视化 小程序
Dooring3.0可视化搭建平台使用指南
Dooring3.0可视化搭建平台使用指南
101 1
|
1月前
|
移动开发 小程序 数据可视化
一招学会DIY官网可视化设计支持导出微擎、UNIAPP、H5、微信小程序源码
一招学会DIY官网可视化设计支持导出微擎、UNIAPP、H5、微信小程序源码
40 2
|
1月前
|
数据可视化 搜索推荐
重磅更新-UniApp自定义字体可视化设计
重磅更新-UniApp自定义字体可视化设计
39 0
|
6月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的星空游戏购买下载平台的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的星空游戏购买下载平台的详细设计和实现(源码+lw+部署文档+讲解等)
|
6月前
|
存储 JavaScript API
《VitePress 简易速速上手小册》第7章 高级功能与动态内容(2024 最新版)(上)
《VitePress 简易速速上手小册》第7章 高级功能与动态内容(2024 最新版)
285 2
|
6月前
|
存储 缓存 自然语言处理
《VitePress 简易速速上手小册》第7章 高级功能与动态内容(2024 最新版)(下)
《VitePress 简易速速上手小册》第7章 高级功能与动态内容(2024 最新版)
157 1
|
6月前
|
存储 JavaScript 搜索推荐
《VitePress 简易速速上手小册》第3章:主题定制与扩展(2024 最新版)
《VitePress 简易速速上手小册》第3章:主题定制与扩展(2024 最新版)
264 0
|
6月前
|
小程序 物联网 测试技术
【社区每周】小程序基础库更新2.9.9版本(1月第一期)
【社区每周】小程序基础库更新2.9.9版本(1月第一期)
62 6
|
6月前
|
JavaScript 算法 数据可视化
antv/g6使用教程及图配置
antv/g6使用教程及图配置
1506 0
|
6月前
|
移动开发 小程序 安全
【社区每周】导航栏支持设置字体颜色,基础库2.7.24版本上线(2022年8月第四期)
【社区每周】导航栏支持设置字体颜色,基础库2.7.24版本上线(2022年8月第四期)
58 0