用 JSX 写一个简单的无任何依赖的甘特图组件
最近项目中要用到甘特图组件,简单找了下没找到比较合适的,于是打算自己动手写一个简单的,且支持 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-jsx
的 pragma
参数为自己实现的 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,顺便来个截图: