发布于 2026-01-06 4 阅读
0

实时构建待办事项

实时构建待办事项

我想先问一个问题:你有没有想过像 Messenger、WhatsApp 这样的应用是如何在不刷新页面的情况下更新新消息的?在本文中,我们将开发一个具有实时通信功能的待办事项应用,以便你更好地理解它的工作原理。

预览

本教程结束时,您将得到以下结果。

预览功能

先决条件。

  1. 安装Node.js。
  2. 安装一个代码编辑器(我用的是VSCode)。

创建项目。

在我的桌面上创建一个项目,并使用我们想要分配的名称。

mkdir todo-realtime

cd todo-realtime

code .

初始化项目。

执行以下命令。

npm init -y

tsc --init

npm init -y此命令会在项目根目录生成一个名为package.json 的文件,该文件包含应用程序正常运行所需的所有设置。更多信息请参阅https://docs.npmjs.com/cli/v7/configuring-npm/package-json 。此命令会生成一个名为tsconfig.json 的
tsc --init文件。该文件与 package.json 类似,区别在于它用于配置 TypeScript 项目。更多信息请参阅https://www.typescriptlang.org/docs/handbook/tsconfig-json.html。

很好,上述解释完毕后,我们就可以下载一些软件包了。

npm i @feathersjs/feathers @feathersjs/socketio @feathersjs/express

npm i nodemon -D

设置服务器。

现在,我们将配置我们的项目。

创建一个名为 nodemon.json 的文件。每当我们对以 .json 结尾的文件进行更改时,此文件将负责更新您的应用程序。.ts

> nodemon.json

{
  "watch": ["src"],
  "ext": "ts,json",
  "ignore": ["src/**/*.spec.ts", "node_modules"],
  "exec": "ts-node ./src/index.ts"
}
Enter fullscreen mode Exit fullscreen mode

我们更新 package.json 文件并添加以下内容。

> package.json

{
  // ...
  "scripts": {
    "serve": "nodemon",
    "start": "node ./src/index.ts"
  },
  //  ...
}
Enter fullscreen mode Exit fullscreen mode

现在我们创建目录src/index.ts。为了验证一切是否正确,请添加以下内容并执行。npm run serve

> src > index.ts

console.log("Hello world developers ♥");
Enter fullscreen mode Exit fullscreen mode

如果一切正常,我们会在控制台中看到这个信息。

在控制台中打印消息

完美,剩下的就是配置了。

开发开发服务器。

我们将创建一个简单的开发服务器,用于添加笔记,之后我们会为其添加实时支持。复制以下内容。

> src > index.ts

import feathers from "@feathersjs/feathers";
import express, { Application } from "@feathersjs/express";

const app: Application = express(feathers());

// Allows interpreting json requests.
app.use(express.json());
// Allows interpreting urlencoded requests.
app.use(express.urlencoded({ extended: true }));
// Add support REST-API.
app.configure(express.rest());

// Use error not found.
app.use(express.notFound());
// We configure the errors to send a json.
app.use(express.errorHandler({ html: false }));

app.listen(3030, () => {
  console.log("App execute in http://localhost:3030");
});

Enter fullscreen mode Exit fullscreen mode

设置我们的服务。

根据 Feathers 官方文档,Service是每个 Feathers 应用的核心。Service 是实现了特定方法的 JavaScript 对象(或 ES6 类的实例)。Feathers 本身也会为其 Services 添加一些额外的方法和功能。

导入模块和已定义的接口。

src > services > note.service.ts

import { Id, Params, ServiceMethods } from "@feathersjs/feathers";
import { NotFound } from "@feathersjs/errors";

export enum Status {
  COMPLETED = "completed",
  PENDING = "pending"
}

export interface Note {
  id: Id;
  name: string;
  status: Status;
  createdAt: string;
  updatedAt: string;
}
Enter fullscreen mode Exit fullscreen mode

定义类。


export class NoteService implements ServiceMethods<Note> {
  private notes: Note[] = [];
  /**
   * Get list of note.
   */
  find(params?: Params): Promise<Note[]> {
    throw new Error("Method not implemented.");
  }
  /**
   * Get on note.
   */
  get(id: Id, params?: Params): Promise<Note> {
    throw new Error("Method not implemented.");
  }
  /**
   * Create a new note.
   */
  create(
    data: Partial<Note> | Partial<Note>[],
    params?: Params
  ): Promise<Note> {
    throw new Error("Method not implemented.");
  }
  /**
   * Udate note.
   */
  update(
    id: NullableId,
    data: Note,
    params?: Params
  ): Promise<Note> {
    throw new Error("Method not implemented.");
  }
  /**
   * Partially update a note.
   */
  patch(
    id: NullableId,
    data: Partial<Note>,
    params?: Params
  ): Promise<Note> {
    throw new Error("Method not implemented.");
  }
  /**
   * Delete a note.
   */
  remove(id: NullableId, params?: Params): Promise<Note> {
    throw new Error("Method not implemented.");
  }
}

Enter fullscreen mode Exit fullscreen mode

我们为这些方法添加了功能。

NoteService.创建


  async create(
    data: Pick<Note, "name">,
    _?: Params
  ): Promise<Note> {
    const note: Note = {
      id: this.notes.length + 1,
      name: data.name,
      status: Status.PENDING,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };

    this.notes.unshift(note);
    return note;
  }

Enter fullscreen mode Exit fullscreen mode

NoteService.find


  async find(_?: Params): Promise<Note[]> {
    return this.notes;
  }
Enter fullscreen mode Exit fullscreen mode

NoteService.get

  async get(id: Id, _?: Params) {
    const note: Note | undefined = this.notes.find(
      note => Number(note.id) === Number(id)
    );
    if (!note) throw new NotFound("The note does not exist.");
    return note;
  }
Enter fullscreen mode Exit fullscreen mode

NoteService.update

  async update(id: Id, data: Note, _?: Params): Promise<Note> {
    const index: number = this.notes.findIndex(
      note => Number(note.id) === Number(id)
    );
    if (index < 0) throw new NotFound("The note does not exist");

    const { createdAt }: Note = this.notes[index];
    const note: Note = {
      id,
      name: data.name,
      status: data.status,
      createdAt,
      updatedAt: new Date().toISOString(),
    };
    this.notes.splice(index, 1, note);
    return note;
  }
Enter fullscreen mode Exit fullscreen mode

NoteService.patch

  async patch(id: Id, data: Partial<Note>, _?: Params): Promise<Note> {
    const index: number = this.notes.findIndex(
      note => Number(note.id) === Number(id)
    );
    if (index < 0) throw new NotFound("The note does not exist");

    const note: Note = this.notes[index];
    data = Object.assign({ updatedAt: new Date().toISOString() }, data);
    const values = Object.keys(data).reduce((prev, curr) => {
      return { ...prev, [curr]: { value: data[curr as keyof Note] } };
    }, {});
    const notePatched: Note = Object.defineProperties(note, values);
    this.notes.splice(index, 1, notePatched);
    return note;
  }

Enter fullscreen mode Exit fullscreen mode

NoteService.remove

  async remove(id: Id, _?: Params): Promise<Note> {
    const index: number = this.notes.findIndex(
      note => Number(note.id) === Number(id)
    );
    if (index < 0) throw new NotFound("The note does not exist");

    const note: Note = this.notes[index];
    this.notes.splice(index, 1);
    return note;
  }
Enter fullscreen mode Exit fullscreen mode

最终结果。

src > note.service.ts

import { Id, Params, ServiceMethods } from "@feathersjs/feathers";
import { NotFound } from "@feathersjs/errors";

export enum Status {
  COMPLETED = "completed",
  PENDING = "pending"
}

export interface Note {
  id: Id;
  name: string;
  status: Status;
  createdAt: string;
  updatedAt: string;
}

export class NoteService implements Partial<ServiceMethods<Note>> {
  private notes: Note[] = [
    {
      id: 1,
      name: "Guns N' Roses",
      status: Status.COMPLETED,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    },
    {
      id: 2,
      name: "Motionless In White",
      status: Status.PENDING,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    },
  ];

  async create(
    data: Pick<Note, "name">,
    _?: Params
  ): Promise<Note> {
    const note: Note = {
      id: this.notes.length + 1,
      name: data.name,
      status: Status.PENDING,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };

    this.notes.unshift(note);
    return note;
  }

  async find(_?: Params): Promise<Note[]> {
    return this.notes;
  }

  async get(id: Id, _?: Params) {
    const note: Note | undefined = this.notes.find(
      note => Number(note.id) === Number(id)
    );
    if (!note) throw new NotFound("The note does not exist.");
    return note;
  }

  async update(id: Id, data: Note, _?: Params): Promise<Note> {
    const index: number = this.notes.findIndex(
      note => Number(note.id) === Number(id)
    );
    if (index < 0) throw new NotFound("The note does not exist");

    const { createdAt }: Note = this.notes[index];
    const note: Note = {
      id,
      name: data.name,
      status: data.status,
      createdAt,
      updatedAt: new Date().toISOString(),
    };
    this.notes.splice(index, 1, note);
    return note;
  }

  async patch(id: Id, data: Partial<Note>, _?: Params): Promise<Note> {
    const index: number = this.notes.findIndex(
      note => Number(note.id) === Number(id)
    );
    if (index < 0) throw new NotFound("The note does not exist");

    const note: Note = this.notes[index];
    data = Object.assign({ updatedAt: new Date().toISOString() }, data);

    const values = Object.keys(data).reduce((prev, curr) => {
      return { ...prev, [curr]: { value: data[curr as keyof Note] } };
    }, {});
    const notePatched: Note = Object.defineProperties(note, values);

    this.notes.splice(index, 1, notePatched);
    return note;
  }

  async remove(id: Id, _?: Params): Promise<Note> {
    const index: number = this.notes.findIndex(
      note => Number(note.id) === Number(id)
    );
    if (index < 0) throw new NotFound("The note does not exist");

    const note: Note = this.notes[index];
    this.notes.splice(index, 1);
    return note;
  }
}


Enter fullscreen mode Exit fullscreen mode

服务配置完成后,就可以使用了。

src > index.ts

import { NoteService } from "./services/note.service";

// Define my service.
app.use("/notes", new NoteService());
Enter fullscreen mode Exit fullscreen mode

注意:务必在错误处理程序之前定义服务。

现在,我们来测试一下这个应用。请输入http://localhost:3030/notes

资源列表。

我们设置了对实时性的支持

此时此刻,我们将为服务器提供实时支持。

src > index.ts

import socketio from "@feathersjs/socketio";
import "@feathersjs/transport-commons";

// Add support Real-Time
app.configure(socketio());

// My services...

// We listen connection event and join the channel.
app.on("connection", connection =>
  app.channel("everyone").join(connection)
);

// Publish all events to channel <everyone>
app.publish(() => app.channel("everyone"));
Enter fullscreen mode Exit fullscreen mode

客户开发。

现在需要提供静态文件。我们使用以下内容来实现这一点。

src > index.ts

import { resolve } from "path";

// Server static files.
app.use(express.static(resolve("public")));
Enter fullscreen mode Exit fullscreen mode

注意:为了使此功能生效,必须在设置错误处理程序之前添加,否则始终显示 NotFound。

该目录具有以下结构。

结构目录

设置前端。

在此步骤中,我们将添加样式和脚本。

我们在样式文件中添加了以下内容。

@import url("https://fonts.googleapis.com/css2?family=Poppins&display=swap");
@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css");
@import url("https://unpkg.com/boxicons@2.0.9/css/boxicons.min.css");

* {
  font-family: 'Poppins', sans-serif;
}
i {
  font-size: 30px;
}
.spacer {
  flex: 1 1 auto;
}
.card-body {
  max-height: 50vh;
  overflow: auto;
}

Enter fullscreen mode Exit fullscreen mode

我们添加了项目的样式和脚本。

<head>
  <!-- Other tags -->
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <!-- My scripts -->
  <script src="//unpkg.com/@feathersjs/client@^4.3.0/dist/feathers.js"></script>
  <script src="/socket.io/socket.io.js"></script>
  <script src="/js/app.js"></script>
</body>

Enter fullscreen mode Exit fullscreen mode

我们正在搭建应用程序的所有视觉部分。请复制以下内容。

  <div class="container-fluid">
    <div
      class="row justify-content-center align-items-center"
      style="min-height: 100vh;"
    >
      <div class="col-12 col-sm-8 col-md-6 col-xl-4 p-3">
        <div class="card border-0 shadow" style="max-height: 80vh;">
          <div class="card-header border-0 bg-white">
            <div class="d-flex align-items-center text-muted">
              <small class="mx-1" id="box-completed"></small>
              <small class="mx-1" id="box-pending"></small>
              <small class="mx-1" id="box-total"></small>
              <span class="spacer"></span>
              <button class="btn btn-remove rounded-pill border-0">
                <i class='bx bx-trash'></i>
              </button>
            </div>
          </div>
          <div class="card-body">
            <ul class="list-group" id="container"></ul>
          </div>
          <div class="card-footer border-0 bg-white">
            <form id="form">
              <div class="form-group py-2">
                <input
                  placeholder="Example: Learning Docker"
                  class="form-control"
                  autocomplete="off"
                  id="input"
                  name="title"
                  autofocus
                >
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
Enter fullscreen mode Exit fullscreen mode

现在,是时候添加项目的所有逻辑了。

我们捕获 DOM 元素。

const form = document.getElementById("form");
const input = document.getElementById("input");

const container = document.getElementById("container");
const boxCompleted = document.getElementById("box-completed");
const boxPending = document.getElementById("box-pending");
const boxTotal = document.getElementById("box-total");

const btnRemove = document.querySelector(".btn-remove");
Enter fullscreen mode Exit fullscreen mode

我们在客户端配置 Feathers.js。

// Instance my app.
const socket = io();
const app = feathers(socket);

// Configure transport with SocketIO.
app.configure(feathers.socketio(socket));

// Get note service.
const NoteService = app.service("notes");
Enter fullscreen mode Exit fullscreen mode

设置某些变量的值。

// The id of the notes are stored.
let noteIds = [];
// All notes.
let notes = [];
Enter fullscreen mode Exit fullscreen mode

我们添加了一些功能,可以修改卡片的标题、备注等内容。

/**
 * Insert id of the notes selected.
 */
async function selectNotes(noteId) {
  const index = noteIds.findIndex(id => id === noteId);
  index < 0 ? noteIds.push(noteId) : noteIds.splice(index, 1);
  btnRemove.disabled = !noteIds.length;
}

/**
 * Update stadistic of the notes.
 */
function updateHeader(items) {
  const completed = items.filter(note => note.status).length;
  const pending = items.length - completed;

  boxCompleted.textContent = `Completed: ${ completed }`;
  boxPending.textContent = `Pending: ${ pending }`;
  boxTotal.textContent = `Total: ${ items.length }`;
}

/**
 * Update note by Id
 */
function updateElement(noteId) {
  const note = notes.find(note => note.id === noteId);
  NoteService.patch(note.id, { status: !note.status });
}

Enter fullscreen mode Exit fullscreen mode

我们创建一个类,该类将负责创建元素。

/**
 * This class is responsible for the creation,
 * removal and rendering of the component interfaces.
 */
class NoteUI {
  /**
   * Create element of the note.
   */
  createElement(note) {
    const element = document.createElement("li");
    element.className = "list-group-item border-0";
    element.id = note.id;
    element.innerHTML = `
    <div class="d-flex align-items-center">
      <div>
        <h6>
          <strong>${ note.name }</strong>
        </h6>
        <small class="m-0 text-muted">${ note.createdAt }</small>
      </div>
      <span class="spacer"></span>
      <div onclick="updateElement(${note.id})" class="mx-2 text-center text-${ note.status ? 'success' : 'danger' }">
        <i class='bx bx-${ note.status ? 'check-circle' : 'error' }'></i>
      </div>
      <div class="ms-2">
        <div class="form-check">
          <input
            class="form-check-input"
            type="checkbox"
            value=""
            id="flexCheckDefault"
            onclick="selectNotes(${ note.id })"
          >
        </div>
      </div>
    </div>
    `;
    return element;
  }

  /**
   * Insert the element at the beginning of the container.
   * @param {HTMLElement} container 
   * @param {HTMLElement} element 
   */
  insertElement(container, element) {
    container.insertAdjacentElement("afterbegin", element);
  }

  /**
   * Remove element by tag id.
   */
  removeElement(id) {
    const element = document.getElementById(id);
    element.remove();
  }
}

// Instance UI
const ui = new NoteUI();

Enter fullscreen mode Exit fullscreen mode

我们监听 CRUD 操作事件。

// Listening events CRUD.
NoteService.on("created", note => {
  const element = ui.createElement(note);
  ui.insertElement(container, element);

  notes.push(note);
  updateHeader(notes);
});

NoteService.on("updated", note => {
  // I leave this method for you as homework.
  console.log("Updated: ",  note);
  updateHeader(notes);
});

NoteService.on("patched", note => {
  // Remove old element.
  ui.removeElement(note.id);
  // Create element updated.
  const element = ui.createElement(note);
  ui.insertElement(container, element);
  // Update header.
  const index = notes.findIndex(item => item.id === note.id);
  notes.splice(index, 1, note);
  updateHeader(notes);
});

NoteService.on("removed", note => {
  ui.removeElement(note.id);

  const index = notes.findIndex(note => note.id === note.id);
  notes.splice(index, 1);
  updateHeader(notes);
});

Enter fullscreen mode Exit fullscreen mode

初始化一些值并获取笔记列表。

// Initialize values.
(async () => {
  // Get lits of note.
  notes = await NoteService.find();
  notes.forEach(note => {
    const element = ui.createElement(note);
    ui.insertElement(container, element);
  });
  // Update header.
  updateHeader(notes);
  // Button for remove is disable.
  btnRemove.disabled = true;
})();
Enter fullscreen mode Exit fullscreen mode

我们监听 DOM 元素的事件。

// Listen event of the DOM elements.
btnRemove.addEventListener("click", () => {
  if (confirm(`Se eliminaran ${ noteIds.length } notas ¿estas seguro?`)) {
    noteIds.forEach(id => NoteService.remove(id));
    btnRemove.disabled = true;
    noteIds = [];
  }
});

form.addEventListener("submit", e => {
  e.preventDefault();

  const formdata = new FormData(form);
  const title = formdata.get("title");
  if (!title) return false;

  NoteService.create({ name: title });
  form.reset();
});

Enter fullscreen mode Exit fullscreen mode

最终结果。

// Get elements DOM.
const form = document.getElementById("form");
const input = document.getElementById("input");

const container = document.getElementById("container");
const boxCompleted = document.getElementById("box-completed");
const boxPending = document.getElementById("box-pending");
const boxTotal = document.getElementById("box-total");

const btnRemove = document.querySelector(".btn-remove");

// Instance my app.
const socket = io();
const app = feathers(socket);

// Configure transport with SocketIO.
app.configure(feathers.socketio(socket));

// Get note service.
const NoteService = app.service("notes");

// Sets values.
let noteIds = [];
let notes = [];

/**
 * Insert id of the notes selected.
 */
async function selectNotes(noteId) {
  const index = noteIds.findIndex(id => id === noteId);
  index < 0 ? noteIds.push(noteId) : noteIds.splice(index, 1);
  btnRemove.disabled = !noteIds.length;
}

/**
 * Update stadistic of the notes.
 */
function updateHeader(items) {
  const completed = items.filter(note => note.status).length;
  const pending = items.length - completed;

  boxCompleted.textContent = `Completed: ${ completed }`;
  boxPending.textContent = `Pending: ${ pending }`;
  boxTotal.textContent = `Total: ${ items.length }`;
}

/**
 * Update note by Id
 */
function updateElement(noteId) {
  const note = notes.find(note => note.id === noteId);
  NoteService.patch(note.id, { status: !note.status });
}

/**
 * This class is responsible for the creation,
 * removal and rendering of the component interfaces.
 */
class NoteUI {
  /**
   * Create element of the note.
   */
  createElement(note) {
    const element = document.createElement("li");
    element.className = "list-group-item border-0";
    element.id = note.id;
    element.innerHTML = `
    <div class="d-flex align-items-center">
      <div>
        <h6>
          <strong>${ note.name }</strong>
        </h6>
        <small class="m-0 text-muted">${ note.createdAt }</small>
      </div>
      <span class="spacer"></span>
      <div onclick="updateElement(${note.id})" class="mx-2 text-center text-${ note.status ? 'success' : 'danger' }">
        <i class='bx bx-${ note.status ? 'check-circle' : 'error' }'></i>
      </div>
      <div class="ms-2">
        <div class="form-check">
          <input
            class="form-check-input"
            type="checkbox"
            value=""
            id="flexCheckDefault"
            onclick="selectNotes(${ note.id })"
          >
        </div>
      </div>
    </div>
    `;
    return element;
  }

  /**
   * Insert the element at the beginning of the container.
   * @param {HTMLElement} container 
   * @param {HTMLElement} element 
   */
  insertElement(container, element) {
    container.insertAdjacentElement("afterbegin", element);
  }

  /**
   * Remove element by tag id.
   */
  removeElement(id) {
    const element = document.getElementById(id);
    element.remove();
  }
}

// Instance UI
const ui = new NoteUI();

// Listening events CRUD.
NoteService.on("created", note => {
  const element = ui.createElement(note);
  ui.insertElement(container, element);

  notes.push(note);
  updateHeader(notes);
});

NoteService.on("updated", note => {
  // I leave this method for you as homework.
  console.log("Updated: ",  note);
  updateHeader(notes);
});

NoteService.on("patched", note => {
  // Remove old element.
  ui.removeElement(note.id);
  // Create element updated.
  const element = ui.createElement(note);
  ui.insertElement(container, element);
  // Update header.
  const index = notes.findIndex(item => item.id === note.id);
  notes.splice(index, 1, note);
  updateHeader(notes);
});

NoteService.on("removed", note => {
  ui.removeElement(note.id);

  const index = notes.findIndex(note => note.id === note.id);
  notes.splice(index, 1);
  updateHeader(notes);
});

// Initialize values.
(async () => {
  // Get lits of note.
  notes = await NoteService.find();
  notes.forEach(note => {
    const element = ui.createElement(note);
    ui.insertElement(container, element);
  });
  // Update header.
  updateHeader(notes);
  // Button for remove is disable.
  btnRemove.disabled = true;
})();

// Listen event of the DOM elements.
btnRemove.addEventListener("click", () => {
  if (confirm(`Se eliminaran ${ noteIds.length } notas ¿estas seguro?`)) {
    noteIds.forEach(id => NoteService.remove(id));
    btnRemove.disabled = true;
    noteIds = [];
  }
});

form.addEventListener("submit", e => {
  e.preventDefault();

  const formdata = new FormData(form);
  const title = formdata.get("title");
  if (!title) return false;

  NoteService.create({ name: title });
  form.reset();
});

Enter fullscreen mode Exit fullscreen mode

预览

预览功能

完美,这样我们就完成了待办事项实时系统的构建。嗯,差不多完成了,因为你还有作业要做,那就是更新笔记。

请记住,如果您有任何疑问,可以阅读官方文档:https://docs.feathersjs.com/guides

优秀的开发者们,如有任何问题、代码简化建议或改进意见,请随时留言。下次再见!

仓库地址:https://github.com/IvanZM123/todo-realtime

请在社交网络上关注我。

文章来源:https://dev.to/ivanzm123/building-todo-in-real-time-3pbl