Bryntum gantt的实时项目管理
#javascript #网络开发人员 #教程 #生产率

顾名思义的实时应用程序是一个应用程序,该应用程序已在实时或接近实时更新。实时应用程序的一个很好的例子是Google文档。当它于2006年启动时,Google Docs改变了多人在文档上合作的方式,使用户可以收到更新。

游戏,电子商务和社交媒体等行业采用实时网络应用程序来促进无缝协作并改善对远程变化的反应。

在本文中,您将学习如何使用ReactSocket.IOBryntum Gantt构建实时项目管理应用程序,这是一个强大的工具,可以帮助您轻松地设置项目任务,跟踪他们的进度并与团队进行协作会员。

什么是实时的Web应用程序?

实时Web应用程序是Web应用程序,可以实时在服务器和客户端之间传递信息。一旦收到新数据,它们就会不断改变。

实时Web应用程序使用WebSocket protocol,该应用程序在客户端和服务器之间提供全双工(或双向)通信。与Websocket相比,传统Web应用中使用的HTTP具有单向(或半双链)通信。下图显示了如何在服务器和客户端之间进行WebSocket传输数据(例如Websocket传输数据)等半教导通信协议:

Half-duplex vs. full-duplex courtesy of Gideon Idoko

WebSockets使客户端和服务器可以通过单个传输控制协议(TCP)套接字连接实时发送和接收数据。 Websocket连接持续使用,并保持打开状态,直到服务器或客户端终止它为止。相比之下,HTTP连接是暂时的,因为接收到服务器的响应后,它已关闭。如果您需要提出新请求,则必须创建新的连接。

实时Web应用程序比传统的Web应用程序具有多个优点。它们更响应迅速,可靠,可扩展,并且能够处理需要实时接收更新的更多并发用户。

构建实时项目管理应用程序

现在您知道了什么实时Web应用程序和Websockets,现在该学习如何使用React,Socket.io和Bryntum Gantt来创建一个简单但实​​时的项目任务管理工具。该应用程序将在本文结尾处这样工作:

Click to view gif

您可以在此GitHub repo上找到本教程的完整代码。

先决条件

在开始之前,您需要安装Node.js,因为需要在本地计算机上运行React React。 Node.js还使用Node Package Manager (npm)发货,它可以安装一些必需的软件包,例如Br​​yntum gantt。如果您还没有下载LTS version of Node.js并安装。

bryntum gantt是最可靠,易于使用且功能齐全的JavaScript gantt图表组件。它与所有主要浏览器兼容,并提供各种可以完全定制的功能以适应您的特定需求。 Bryntum Gantt的渲染引擎很快,它可以处理大型数据集而不会停止性能。此外,其内置调度引擎还提供了各种任务的异步重新安排。

bryntum gantt将提供甘特图和其他必要组件(例如按钮),这些组件将被配置为满足应用程序的需求。现在该建造它了。

基本设置

该应用程序将使用Create React App (CRA)引导。值得庆幸的是,Bryntum有一个CRA模板。这将使用户能够快速使用Bryntum Gantt软件包。黄褐色包装是商业化的,但是它们具有本教程足够的试用版本。

Bryntum的私人注册表可提供商业和试用版。打开终端并运行以下命令以找到注册表:

npm config set "@bryntum:registry=https://npm.bryntum.com"

然后登录:

npm login --registry=https://npm.bryntum.com

您将需要输入您的用户名,密码和电子邮件地址。将您的电子邮件与“@”一起替换为“ ..”作为您的用户名和“试用”作为密码:

Username: user..yourdomain.com
Password: trial
Email: (this IS public) user@yourdomain.com

这将使您可以访问Bryntum的私人注册表和软件包。

现在您可以访问注册表,现在该完成设置了。为此,为该项目创建一个新目录:

mkdir realtime-proman

您可以用首选的应用程序名称替换realtime-proman

然后用bryntum gantt模板引导一个新的反应客户端:

npx create-react-app client --template @bryntum/cra-template-javascript-gantt

由于将使用Websocket,因此需要简单的服务器。在项目的根目录中为服务器端代码创建一个新目录:

mkdir server

更改为服务器目录并初始化一个新的npm项目:

cd server && npm init -y

最后,创建一个将包含服务器端代码的index.js文件:

touch index.js

创建WebSocket服务器

现在,您需要创建一个简单的Websocket服务器,您的React客户端可以与之交互。在server目录中,运行以下命令以安装服务器所需的依赖关系,socket.io:

npm install socket.io

然后在您之前创建的index.js文件中,粘贴以下代码:

const 
    { Server } = require('socket.io'),
    io = new Server({
        cors: { origin: '*' }
    });
// data will be persisted here
let persistedData = null;
io.on('connection', (socket) => {
    console.log(`New connection ID: ${socket.id}`);
    socket.emit('just-joined', persistedData ?? {});
    socket.on('data-change', (data) => {
        persistedData = data;
        socket.broadcast.emit('new-data-change', data);
    })
});
io.listen(5000);
console.log(`🚀 socket server started on port 5000...`);

在这里,您有三个事件:data-changenew-data-changejust-joined。当射击data-change事件时,传递的数据将持续到persistData,并发出了new-data-change事件。 just-joined事件均在每个新连接上都具有持久数据。

最后,启动Websocket服务器:

node index

在客户上工作

现在,WebSocket服务器正在启动并运行,是时候完成客户端了。 client目录中的窥视将揭示其结构,看起来像这样:

client                                                          
├─ public                                                                        
│  ├─ data                                                                       
│  │  └─ gantt-data.json                                                         
│  ├─ favicon.png                                                                
│  ├─ index.html                                                                 
│  └─ manifest.json                                                              
├─ src                                                                           
│  ├─ App.js                                                                     
│  ├─ App.scss                                                                   
│  ├─ GanttConfig.js                                                             
│  └─ index.js                                                                   
├─ package-lock.json                                                             
├─ package.json                                                                  
└─ README.md

gantt-data.json文件包含一些虚拟数据,这些数据一旦App组件呈现。然后,GanttConfig.js文件包含gantt图表的配置。

client目录中创建一个新的终端窗口,然后旋转您的React开发服务器:

npm start

当React开发服务器启动时,您应该看到以下内容:

Initial app

您在此处看到的数据是从gantt-data.json文件中,当应用首次加载时,通过the project transport method绑定到BryntumGantt。这是在GanttConfig.js文件中的gantt图表配置中定义的:

// ...
project : {
        transport : {
            load : {
                url : 'data/gantt-data.json'
            }
        },
        autoLoad  : true,
    }
// ...

传递了gantt-data.json文件的路径作为url选项的值。当App组件最初呈现时,提出了GET请求,以获取其JSON数据。这就是为什么gantt-data.json文件的内容类似于服务器响应。它甚至具有具有true值的success键。除了success键,您还会找到七个根键:

  1. project保留您的ProjectModel的配置,这是您项目中所有数据的中央商店。

请注意: model只是可以添加到(或加载到)store的记录的定义。商店是一个具有平坦数据或树结构的数据容器。记录是数据容器和商店中的项目。

  1. calendars保留所有传递给CalendarModel的日历。

  2. tasks将所有任务传递给TaskModel

  3. dependencies保持任务之间的所有单个依赖关系。它传递给DependencyModel

  4. resources持有传递给ResourceModel的所有资源或资产。

  5. assignments将资源的所有单个任务保存到传递给AssignmentModel的任务。

  6. timeRanges在ProjectModel的计时型中保留所有时间范围。每个范围代表TimeSpan模型。

现在是时候使用加载到gantt图表的默认JSON数据了。

添加工具栏

要开始,您需要在甘特图中添加一个工具栏,该图表将分别具有两个用于创建和编辑任务的按钮组件。

在您的React客户端的src文件夹中创建一个GanttToolbar.js文件,然后向其添加以下代码:

import {
    Toolbar,
    Toast,
} from '@bryntum/gantt';

export default class GanttToolbar extends Toolbar {
    // Factoryable type name
    static get type() {
        return 'gantttoolbar';
    }

    static get $name() {
        return 'GanttToolbar';
    }

    // Called when toolbar is added to the Gantt panel
    set parent(parent) {
        super.parent = parent;

        const me = this;

        me.gantt = parent;

        me.styleNode = document.createElement('style');
        document.head.appendChild(me.styleNode);
    }

    get parent() {
        return super.parent;
    }

    static get configurable() {
        return {
            items: [
                {
                    type  : 'buttonGroup',
                    items : [
                        {
                            color    : 'b-green',
                            ref      : 'addTaskButton',
                            icon     : 'b-fa b-fa-plus',
                            text     : 'Create',
                            tooltip  : 'Create new task',
                            onAction : 'up.onAddTaskClick'
                        }
                    ]
                },
                {
                    type  : 'buttonGroup',
                    items : [
                        {
                            ref      : 'editTaskButton',
                            icon     : 'b-fa b-fa-pen',
                            text     : 'Edit',
                            tooltip  : 'Edit selected task',
                            onAction : 'up.onEditTaskClick'
                        }
                    ]
                },
            ]
        };
    }

    async onAddTaskClick() {
        const { gantt } = this,
              added     = gantt.taskStore.rootNode.appendChild({
                name     : 'New task',
                duration : 1
            });

        // wait for immediate commit to calculate new task fields
        await gantt.project.commitAsync();

        // scroll to the added task
        await gantt.scrollRowIntoView(added);

        gantt.features.cellEdit.startEditing({
            record : added,
            field  : 'name'
        });
    }

    onEditTaskClick() {
        const { gantt } = this;

        if (gantt.selectedRecord) {
            gantt.editTask(gantt.selectedRecord);
        } else {
            Toast.show('First select the task you want to edit');
        }
    }
}

// Register this widget type with its Factory
GanttToolbar.initClass();

此注册一个带有两个按钮的容器小部件。

使用以下内容更新您的GanttConfig.js

import './GanttToolbar';
/**
 * Application configuration
 */

const ganttConfig = {
    columns    : [
        { type : 'name', field : 'name', width : 250 }
    ],
    viewPreset : 'weekAndDayLetter',
    barMargin  : 10,

    project : {
        transport : {
            load : {
                url : 'data/gantt-data.json'
            }
        },
        autoLoad  : true,
    },
    tbar : { type: 'gantttoolbar' }, // toolbar
};

export { ganttConfig };

这将导入GanttToolbar.js文件,并将注册的工具栏类型添加到gantt Chart Configuration的tbar选项中。

保存所有文件,然后重新加载浏览器以查看新工具栏:

App with toolbar

此外,您应该能够分别单击 create 编辑按钮来创建新任务或编辑现有任务。

在这一点上,您已经创建了一个项目任务管理工具,但还没有实时。您的React客户端必须与Websocket服务器交换数据以实时。

在客户端上实施WebSocket

要在客户端上实现WebSocket,请先安装socket.io客户端库以及普遍唯一的标识符(UUID)软件包,该软件包将有助于识别套接字连接中的客户端。

client目录中打开一个终端并运行以下命令:

npm install socket.io-client uuid

安装完成后,请从已安装的软件包中导入iov4方法,以及react中的useEffectuseRef钩子中的App.js文件中的useRef挂钩:

import { useRef, useEffect } from 'react';
import { io } from 'socket.io-client';
import { v4 as uuid } from 'uuid';

App组件定义之前初始化套接字客户端:

const socket = io('ws://localhost:5000', { transports: ['websocket'] });

使用以下代码更新App组件:

function App() {
    const 
        ganttRef = useRef(null), // reference to the gantt chart
        remote = useRef(false), // set to true when changes are made to the store
        clientId = useRef(uuid()), // a unique ID for the client
        persistedData = useRef(), // store persisted data from the server
        canSendData = useRef(false); // set to true to allow data to be sent to the server

    useEffect(() => {
        const gantt = ganttRef.current?.instance;
        if (gantt) {
            // access the ProjectModel
            const project = gantt.project;
            // listen for change events
            project?.addListener('change', () => {
                // don't send data when the same client is updating the project store
                if (!remote.current) {
                    // only send data when allowed
                    if (canSendData.current) {
                        // get project store
                        const store = gantt.store.toJSON();
                        // send data to the store with the data-change event
                        socket.emit('data-change', { senderId: clientId.current, store });
                    }
                }
            });
            // trigger when project data is loaded
            project?.addListener('load', () => {
                if (persistedData.current) {
                    const { store } = persistedData.current;
                    remote.current = true;
                    if (store) {
                        // update project store if persisted store exists
                        gantt.store.data = store
                    }
                    remote.current = false;
                    // allow data to be sent 2s after data is loaded
                    setTimeout(() => canSendData.current = true, 2000);
                    // stop listening since this data from this event is needed once
                    socket.off('just-joined');
                }
            });
            socket.on('just-joined', (data) => {
                // update with the persisted data from the server
                if (data) persistedData.current = data;
            });

            socket.on('new-data-change', ({ senderId, store }) => {
                // should not update store if received data was sent by same client
                if (clientId.current !== senderId) {
                    // disable sending sending data to server as change to the store is to be made
                    remote.current = true;
                    // don't update store if previous data was sent by same client
                    if (JSON.stringify(gantt.store.toJSON()) !== JSON.stringify(store)) {
                        // update store with store from server
                        gantt.store.data = store;
                    }
                    // data can now be sent to the server again
                    remote.current = false;
                }
            });
        }
        return () => {
            // disconnect socket when this component unmounts
            socket.disconnect();
        };
    }, []);

    return (
        <BryntumGantt
            ref = {ganttRef}
            {...ganttConfig}
        />
    );
}

代码上的评论说明了每行的作用,但是项目存储的状态是通过Websocket发送和接收的。从WebSocket服务器接收新状态后,将更新项目商店。

保存并重新加载您的应用程序中的应用程序。然后在另一个浏览器窗口中打开应用程序,然后在其中一个窗口中的应用中创建或编辑任务。您应该在另一个窗口中的应用程序中实时看到更改,如下所示:

Click to view gif

结论

实时Web应用程序提供引人入胜的用户体验,使多个人可以实时与该应用程序进行交互。在本文中,向您介绍了什么是实时的Web应用程序以及什么使它们实时。您还学习了如何使用Bryntum Gantt chart,React和Websocket来构建简单的实时项目任务管理应用程序。在此过程中,您学会了如何创建WebSocket服务器以及如何从React客户端与它进行通信。

Bryntum在世界一流的网络组件方面是行业领导者。它们提供了强大而成熟的组件,例如Br​​yntum gantt,在这里使用。 Bryntum的其他Web组件包括Bryntum SchedulerBryntum GridBryntum CalendarBryntum Task Board,甚至测试Siesta等测试工具。这些组件非常可自定义,易于集成,您可以try them for free today

您是否已经在使用Bryntum Web组件?让我们知道您的想法!