[译] 使用 React 和 ImmutableJS 构建一个拖放布局构建器

Drag and Drop in React!

使用 React 和 ImmutableJS 构建一个拖放(DnD)布局构建器

拖放』这一类的行为存在着巨大的用户需求,例如构建网站(Wix)或交互式应用程序(Trello)。毫无疑问,这种类型的交互创造了很是酷的用户体验。若是再加上一些最新的 UI 技术,咱们能够建立一些很是好的软件。javascript

这篇文章的最终目标是什么?

我想构建一个能让用户使用一系列可定制 UI 组件来构建布局的拖放布局构建器,最终能构建出一个网站或者是 web 应用程序。  css

咱们会用到哪些库?

  1. React
  2. ImmutableJS

下面花一点时间来解释它们在构建这个项目时所起的做用。html

React

React 基于声明式编程,这意味着它根据状态来进行渲染。状态(State)实际上只是一个 JSON 对象,它具备告诉 React 应该怎么去渲染(样式和功能)的属性。与操做 DOM 的库(例如 jQuery)不一样,咱们不直接改变 DOM,而是经过修改状态(state)而后让 React 去负责 DOM(稍后会介绍 DOM)。前端

在这个项目中,假设有一个父组件来保存布局的状态(JSON 对象),而且这个状态将被传递给其余的组件,这些组件都是 React 中的无状态组件。java

这些组件的做用是从父组件中获取状态,而后根据其属性来渲染自己。react

如下是一个具备三个 link 对象的状态的简单示例:android

{
  links:  [{
    name: "Link 1",
    url: "http://link.one",
    selected: false
  }, {
    name: "Link 2",
    url: "http://link.two",
    selected: true
  }, {
    name: "Link 3",
    url: "http://link.three",
    selected: false
  }]
}
复制代码

经过上面的例子,咱们能够遍历 links 数组来为每一个元素建立一个无状态组件:ios

interface ILink {
  name: string;
  url: string;
  selected: boolean;
}

const LinkComponent = ({ name, url, selected }: ILink) =>
<a href={url} className={selected ? 'selected': ''}>{name}</a>;
复制代码

你能够看到咱们如何根据状态中保存的选定属性将 css 类『selected』应用到 links 数组组件。下面是呈现给浏览器的内容:git

<a href="http://link.two" class="selected">Link 2</a>
复制代码

ImmutableJS

咱们已经了解了状态在咱们项目中的重要性,它是使 React 组件如何渲染的惟一真实的数据来源。React 中的状态保存在不可变的数据结构中。github

简而言之,这意味着一旦建立了数据对象,就不能直接去修改它。除非咱们建立一个具备更改状态的新对象。

让咱们用另一个简单的例子来讲明不变性:

interface ILink {
  name: string;
  url: string;
  selected: boolean;
}

const link: ILink = {
    name: "Link 1",
    url: "http://link.one",
    selected: false
}
复制代码

在传统的 JavaScript 中,你能够经过下面的操做来更新 link 对象:

link.name = 'New name';
复制代码

若是咱们的状态是不可变的,那么上面操做不可能完成的,那么咱们必需要建立一个 name 属性已经被修改的新对象。

link = {...link, name: 'New name' };
复制代码

注意:为了支持不变性,React 为咱们提供了一个方法 this.setState(),咱们可使用它来告诉组件状态已经改变,而且组件还须要从新进行渲染若是状态发生任何改变。

上面只是基本示例,可是若是想要在复杂的 JSON 状态结构中更改嵌套了多层的属性应该怎么作?

ECMA Script 6 为咱们提供了一些方便的操做符和方法来改变对象,但它们并不适用于复杂的数据结构,这就是咱们须要 ImmutableJS 来简化任务的缘由。

稍后咱们会使用 ImmutableJS,可是如今你只须要知道它具备给咱们提供简便的方法用来改变复杂的状态方面的做用。

HTML5 拖放(DnD)

因此咱们知道咱们的状态是一个不可变的 JSON 对象,而 React 来负责处理组件,但咱们须要有趣的用户交互体验,对吧?

幸好有了 HTML5 使得这实际上很是简单,由于它提供了咱们能够用来检测拖动组件的时间和放置它们的位置的方法。因为 React 将原生 HTML 元素暴露给浏览器,所以咱们可使用原生的事件方法使咱们的实现更加简单。

注意:我得知使用 HTML5 实现的 DnD 可能存在一些问题但若是没有其它的问题,这多是一个探究课程,若是发现有问题的话,咱们以后能够换掉它。

在这个项目中,咱们拥有用户能够拖动的组件(HTML divs),我称他们为可拖动组件

同时咱们也拥有容许用户放置组件的区域, 我称他们为可放置组件

使用原生 HTML5 事件如 onDragStartonDragOveronDragDrop,咱们也应该拥有基于 DnD 交互更改应用程序状态所须要的东西。

如下是一个可拖动组件的实例:

export interface IDraggableComponent {
  name: string;
  type: string;
  draggable?: boolean;
  onDragStart: (ev: React.DragEvent<HTMLDivElement>, name: string, type: string) => void;
}

export const DraggableComponent = ({
  name,
  type,
  onDragStart,
  draggable = true
}: IDraggableComponent) =>
<div className='draggable-component' draggable={draggable} onDragStart={(ev) => onDragStart(ev, name, type)}>{name}</div>;
复制代码

在上面的代码片断中,咱们渲染了一个 React 组件,该组件使用 onDragStart 事件告诉父组件咱们正开始拖动组件。咱们还能够经过传递 draggable 属性来切换拖动它的能力。

如下是一个可放置组件的实例:

export interface IDroppableComponent {
  name: string;
  onDragOver: (ev: React.DragEvent<HTMLDivElement>) => void;
  onDrop: (ev: React.DragEvent<HTMLDivElement>, componentName: string) => void;
}

export const DroppableComponent = ({
  name,
  onDragOver,
  onDrop
}: IDroppableComponent) =>
<div className='droppable-component'
  onDragOver={(ev: React.DragEvent<HTMLDivElement>) => onDragOver(ev)}
  onDrop={(ev: React.DragEvent<HTMLDivElement>) => onDrop(ev, name)}>
  <span>Drop components here!</span>
</div>;
复制代码

在上面的组件中,咱们正在监听 onDrop 事件,所以咱们能够根据放进可放置组件的新组件来更新状态。

好的,是时候快速回顾一下,而后将他们所有放到一块儿:

咱们将使用 React 中基于状态对象的少许解耦无状态组件来渲染整个布局。用户交互将由 HTML5 DnD 事件来处理,而时间会使用 ImmutableJS 来触发对状态对象的更改。

拖动所有

如今咱们已经对要作的事情以及如何处理它们有了深入的了解,让咱们考虑一下这个难题中的一些最重要的部分:

  1. 布局状态
  2. 拖放构建器组件
  3. 渲染网格内的嵌套组件

1. 布局状态

为了使咱们的组件能表示无限的布局组合,状态须要灵活且可拓展。咱们还须要记住的是,若是想要表示任何给定布局的 DOM 树,意味着须要不少使人愉快的递归来支持嵌套结构!

咱们的状态须要存储大量组件,能够经过如下接口表示:

若是你不熟悉 JavaScript 中的接口,你应该看看 TypeScript — 你大概能看出我是它的粉丝。它很适用于 React。

export interface IComponent {
  name: string;
  type: string;
  renderProps?: {
    size?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
  };
  children?: IComponent[];
}
复制代码

我会使组件的定义最小化,可是你能够根据须要拓展它。我在 renderProps 这里定义一个对象,因此咱们能够为组件提供状态来告诉它如何渲染,children 的属性为咱们提供了递归。

对于更高层次,我会建立一个对象数组来保存组件,它们将出如今状态的根部。

为了说明这一点,咱们建议将如下内容做为 HTML 中标记的有效布局:

<div class="content-panel-1">
  <div class="component">
    Component 1
  </div>
  <div class="component">
    Component 2
  </div>
</div>
<div class="content-panel-2">
  <div class="component">
    Component 3
  </div>
</div>
复制代码

为了在状态中表示这一点,咱们能够为内容面板定义以下所示的接口:

export interface IContent {
  id: string;
  cssClass: string;
  components: IComponent[];
}
复制代码

而后咱们的状态将会成为一个像以下 IContent 数组:

const state: IContent[] = [
  {
    id: 'content-panel-1',
    cssClass: 'content-panel-1',
    components: [{
      type: 'component1',
      renderProps: {},
      children: []
    },
    {
      type: 'component2',
      renderProps: {},
      children: []
    }]
  },
  {
    id: 'content-panel-2',
    cssClass: 'content-panel-2',
    components: [{
      type: 'component3',
      renderProps: {},
      children: []
    }]
  }
];
复制代码

经过在 children 数组属性中推送其余组件,咱们能够定义其余组件来建立嵌套的相似 DOM 的树结构:

[0]
  components:
    [0]
      children:
        [0]
          children:
            [0]
               ...
复制代码

2. 拖放布局构建器

布局构建器组件将执行一系列功能,例如:

  • 保持并更新组件状态
  • 渲染 可拖动组件可放置组件
  • 渲染嵌套布局结构
  • 触发 DnD HTML5 事件

代码大概是这样的:

export class BuilderLayout extends React.Component {

  public state: IBuilderState = {
    dashboardState: []
  };

  constructor(props: {}) {
    super(props);

    this.onDragStart = this.onDragStart.bind(this);
    this.onDragDrop = this.onDragDrop.bind(this);
  }

  public render() {

  }

  private onDragStart(event: React.DragEvent <HTMLDivElement>, name: string, type: string): void {
    event.dataTransfer.setData('id', name);
    event.dataTransfer.setData('type', type);
  }

  private onDragOver(event: React.DragEvent<HTMLDivElement>): void {
    event.preventDefault();
  }

  private onDragDrop(event: React.DragEvent <HTMLDivElement>, containerId: string): void {

  }


}
复制代码

咱们先暂时不用管 render() 函数,后面很快会再见到它。

咱们有三个事件,咱们将绑定它们到咱们的『可拖动组件』和『可放置组件』上。

onDragStart() ——这个事件这里咱们设置一些关于 event 对象中组件的细节,即 nametype

onDragOver() ——咱们如今不会对这个事件作任何事情,事实上咱们经过 .preventDefault() 函数禁用浏览器的默认行为。

这就留下了 onDragDrop() 事件,这就是修改不可变状态的神奇之处。为了改变状态,咱们须要几条信息:

  • 要放置组件的名称 —— nameevent 对象中设置 onDragStart()
  • 要放置组件的类型 —— typeevent 对象中设置 onDragStart()
  • 组件被放置的位置 —— containerId 从可放置的组件中传入这个方法。

containerId 中必须告诉咱们,新的组件具体要放在状态里的什么位置。可能有一种更简洁的方法能够作到这一点,但为了描述这个位置,我将使用一个下划线分隔的索引列表。

回顾咱们的状态模型:

[index]
  components:
    [index]
      children:
        [index]
          children:
            [index]
               ...
复制代码

用字符串格式表示为 cb_index_index_index_index

此处的索引数描述了应该删除组件的嵌套结构中的深度级别。

如今咱们须要调用 immutableJS 中的强大功能来帮助咱们改变应用程序的状态。咱们将在 onDragDrop() 方法中执行此操做,改方法可能以下所示:

private onDragDrop(event: React.DragEvent <HTMLDivElement>, containerId: string) {
  const name = event.dataTransfer.getData('id');
  const type = event.dataTransfer.getData('type');

  const newComponent: IComponent =  this.generateComponent(name, type);

  const containerArray: string[] = containerId.split('_');
  containerArray.shift(); // 忽略第一个参数,它是字符串前缀

  const componentsPath: Array<number | string> = []   containerArray.forEach((id: string, index: number) => {
  componentsPath.push(parseInt(id, INT_LENGTH));
  componentsPath.push(index === 0 ? 'components' : 'children');
});

  const { dashboardState } = this.state;
  let componentState = fromJS(dashboardState);

  componentState = componentState.setIn(componentsPath,       componentState.getIn(componentsPath).push(newComponent));

  this.setState({ dashboardState: componentState.toJS() });

}
复制代码

这里的功能来自于 ImmutableJS 提供给咱们的 .setIn().getIn() 方法。

它们采用一组字符串/值以肯定要在嵌套状态模型中获取或设置值的位置。这与咱们生成可放置的 ids 方式很吻合。很酷吧?

fromJS()toJS() 方法转变 JSON 对象到 ImmutableJS 对象,而后再返回。

关于 ImmutableJS 有不少东西,我可能会在将来写一篇关于它的专门的帖子。很抱歉,此次只是一次简单的介绍!

3. 渲染网格内的嵌套组件

最后让咱们快速看一下前面提到的渲染方法。我想支持一个 CSS 网格系统相似于 Material responsive grid 来使咱们的布局更加灵活。它使用 12 列网格来规定 HTML 布局,以下所示:

<div class="mdc-layout-grid">
  <div class="mdc-layout-grid__inner">
    <div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-6">
      Left column
    </div>
    <div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-6">
      Right column
    </div>
  </div>
</div>
复制代码

将它与咱们的状态所表明的嵌套结构相组合,咱们能够获得一个很是强大的布局构建器。

如今,我只是将网格的大小固定为两列布局(即单个可放置组件中的两列具备的递归)。

为了实现这一点,咱们有一个可拖动组件的网格,它将包含两个可放置的(每列一个)。

这是我以前建立的一个:

上面我有一个Grid,第一列中有一个Card,第二列中有一个Heading

如今我在第一列中放置了另外一个Grid,第一列中有一个Heading,第二列中有一个Card

你明白了吗?

举个例子来讲明如何使用 React 伪代码实现这个目的:

  1. 循环遍历内容项(咱们状态的根)而且渲染一个 ContentBuilderDraggableComponent 和一个 DroppableComponent

  2. 肯定组件是否为 Grid 类型,而后渲染 ContentBuilderGridComponent,不然渲染一个常规的 DraggableComponent

  3. 渲染被 X 个子项目标记的 Grid 组件,每一个子项目中都有一个 ContentBuilderDraggableComponent 和一个 DroppableComponent

class ContentBuilderComponent... {
  render() {
    return (
      <ContentComponent>
        components.map(...) {
          <ContentBuilderDraggableComponent... />
        }
        <DroppableComponent... />
      </ContentComponent>
    )
  }
}

class ContentBuilderDraggableComponent... {
  render() {
    if (type === GRID) {
      return <ContentBuilderGridComponent... />
    } else {
      return <DraggableComponent ... />
    }
  }
}

class ContentBuilderGridComponent... {
  render() {
    <GridComponent...>
      children.map(...) {
        <GridItemComponent...>
          gridItemChildren.map(...) {
            <ContentBuilderDraggableComponent... />
            <DroppableComponent... />
          }
        </GridItemComponent>
      }
    </GridComponent>
  }
}
复制代码

下一步是什么?

咱们已经完成了这篇文章,但我未来会对此进行一些拓展。这是一些想法:

  • 配置组件的渲染道具
  • 使网格组件可配置
  • 使用服务端呈现从已保存的状态对象生成 HTML 布局

但愿你能 follow 我,若是你没有,这是我在 GitHub 上的一个工做示例,但愿你能欣赏它。 chriskitson/react拖放布局构建器 使用React和ImmutableJS拖放(DnD)UI布局构建器 - chriskitson/react拖放布局构建器github.com

感谢您抽出宝贵时间阅读个人文章。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索