用 JSX 写一个简单的无任何依赖的甘特图组件
发布于 7 个月前 作者 helloyou2012 958 次浏览 来自 分享

最近项目中要用到甘特图组件,简单找了下没找到比较合适的,于是打算自己动手写一个简单的,且支持 SVG 和 Canvas 两种,基于这种前提用 JSX 最适合不过了,但是既然是简单的就不想有任何依赖包括 React,所以简单的借助 Babel 写了这个组件:https://github.com/d-band/gantt

配置 Babel

npm i babel-preset-env babel-preset-stage-2
npm i babel-plugin-transform-react-jsx

既然不用 React 所以需要配置 transform-react-jsxpragma 参数为自己实现的 h 函数,详细配置如下:

{
  "presets": ["env", "stage-2"],
  "plugins": [
    ["transform-react-jsx", {
      "pragma": "h"
    }]
  ]
}

简单的 h 函数

为了简单这里没有使用常用的 virtual-dom 定义 Node 的方式,而是用简单的 object 来表示树形结构。

/** File: h.js **/
function h(tag, props, ...children) {
  if (typeof tag === 'function') {
    return tag({ ...props, children });
  }
  return { tag, props, children };
}

用 JSX 写一个简单的组件

/** File: Gantt.js **/
import h from './h';

function Gantt({ width, height, rowHeight, data }) {
  const box = `0 0 ${width} ${height}`;
  return (
    <svg width={width} height={height} viewBox={box}>
      {data.map((v, i) => {
        return <text x={0} y={(i + 0.5) * rowHeight}>{v.name}</text>
      })}
      {data.map((v, i) => {
        const y = i * rowHeight;
        return <line x1={0} x2={width} y1={y} y2={y} />
      })}
      {data.map((v, i) => {
        const y = (i + 0.5) * rowHeight - 8;
        return <rect x={100 + v.from} y={y} width={v.to - v.from} height={16} />
      })}
    </svg>
  );
}

简单的 render 函数

接下来就需要实现相应的 render 了,这里简单的实现了三种:renderSVG、renderCanvas、renderString。

首先写一个 SVG 的 render

const NS = 'http://www.w3.org/2000/svg';
const doc = document;

function renderSVG(vnode, ctx) {
  const { tag, props, children } = vnode;
  const node = doc.createElementNS(NS, tag);

  if (props) {
    applyProperties(node, props);
  }

  children.forEach((v) => {
    node.appendChild(
      typeof v === 'string' ? doc.createTextNode(v) : render(v, ctx)
    );
  });
  return node;
}

再写一个 Canvas 的 render

function renderCanvas(vnode, ctx) {
  const { tag, props, children } = vnode;
  if (tag === 'svg') {
    const { width, height } = props;
    ctx.width = width;
    ctx.height = height;
  }
  if (tag === 'line') {
    const { x1, x2, y1, y2 } = props;
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.stroke();
  }
  if (tag === 'rect') {
    const { x, y, width, height } = props;
    ctx.fillRect(x, y, width, height);
  }
  if (tag === 'text') {
    const { x, y } = props;
    ctx.fillText(children, x, y);
  }
  children.forEach((v) => {
    if (typeof v !== 'string') {
      render(v, ctx);
    }
  });
}

最后再写一个 string 的 render 用于 SSR

function renderString(vnode, ctx) {
  const { tag, props, children } = vnode;
  const tokens = [];
  tokens.push(`<${tag}`);

  Object.keys(props || {}).forEach((k) => {
    const v = props[k];
    tokens.push(` ${k}="${attrEscape(v)}"`);
  });

  if (!children || !children.length) {
    tokens.push(' />');
    return tokens.join('');
  }

  tokens.push('>');

  children.forEach((v) => {
    if (typeof v === 'string') {
      tokens.push(escape(v));
    } else {
      tokens.push(render(v, ctx));
    }
  });

  tokens.push(`</${tag}>`);
  return tokens.join('');
}

看看如何使用

const data = [{
  name: 'hello',
  from: 3,
  to: 30
}, {
  name: 'world',
  from: 20,
  to: 50
}];

// SVG
const dom = document.querySelector('#svg-root');
const tree = renderSVG(<Gantt
  data={data}
  width={200}
  height={200}
  rowHeight={30}
/>);
dom.appendChild(tree);

// Canvas
const dom = document.querySelector('#canvas-root');
const ctx = dom.getContext('2d');
renderCanvas(<Gantt
  data={data}
  width={200}
  height={200}
  rowHeight={30}
/>, ctx);

最后再附一下项目的 Github 地址:https://github.com/d-band/gantt,顺便来个截图:

image.png

回到顶部