发布于 2026-01-05 3 阅读
0

构建同一个应用程序 5 次 DEV 全球展示挑战赛,由 Mux 呈现:展示你的项目!

五次构建同一个应用程序

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

我做这件事的灵感来源于 YouTube 频道 Fireship,他们制作了很多关于 Web 开发的精彩视频,如果你感兴趣,我强烈推荐。
这是原视频的链接,其中包含了 10 个用于构建待办事项应用程序的框架:
https://youtu.be/cuHDQhDhvPE

我琢磨着不想在这上面耗费太多时间,而且我主要是想借此机会学习几个新框架,而不是六个,所以我只会重复开发同一个应用五次。我计划开发的应用是一个简单的笔记应用,用户可以随意记录,并保存为不同的笔记。其中一些框架我以前用过,也开发过类似的应用,但其他的框架我要么从未用过,要么根本就没用过,所以这些对我来说会更有挑战性。

构建应用程序

jQuery

我会用 jQuery 来简化无框架应用的开发,但考虑到我一开始就给自己找麻烦,我仍然不太期待这个项目。总之,我先创建了一个基本的文件结构,然后打开了它。index.html如果你好奇的话,文件结构是这样的: 基本上,我有一个 SCSS 样式表,我会把它编译成 CSS,目前就这些。HTML 代码现在看起来是这样的,之后我会进行扩展:
图像

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="./css/styles.css" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <title>Notes App</title>
</head>

<body>
    <div class="container">
        <header>
            <h1>Notes App</h1>
        </header>
        <main>
            <div class="note">
                <form>
                    <input required type="text" id="note-title" placeholder="Note Title" />
                    <textarea id="note-body" placeholder="Note Body"></textarea>
                    <input type="submit" id="note-submit" title="Add Note" />
                </form>
            </div>
        </main>
    </div>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

样式表如下所示:

body {
    height: 100%;
    width: 100%;
    margin: 0;
}

.container {
    width: 100%;
    height: auto;
    margin: 0;
    display: flex;
    flex-direction: column;

    header {
        display: flex;
        align-items: center;

        width: 100%;
        height: 56px;
        background-color: #4e78b8;
        color: white;

        h1 {
            margin-left: 6px;
        }
    }

    main {
        margin: 10px;
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
        grid-gap: 1rem;
        align-items: center;

        .note {
            display: flex;
            flex-direction: column;

            padding: 10px;
            background-color: #a15fbb;
            border-radius: 5px;

            form {
                display: flex;
                flex-direction: column;

                textarea {
                    resize: none;
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

然后我用编译命令编译代码sass scss/styles.scss:css/styles.css,就可以开始编写一些 JavaScript 代码了。基本上,我们只需要在表单提交时向 DOM 添加一个新的 div 元素,并添加几个子元素,然后保存到本地存储即可。这就是我最终得到的代码:

let notes = [];

$(document).ready(function () {
    if (localStorage.getItem("notes")) notes = JSON.parse(localStorage.getItem("notes"));
    setNotes();
});

$("#note-submit").click(function (e) { 
    let noteTitle = $("#note-title").val();
    let noteDesc = $("#note-body").val();
    let note = {
        title: noteTitle,
        desc: noteDesc
    }
    notes.push(note);
    console.log(notes);
    localStorage.setItem("notes", JSON.stringify(notes));
    setNotes();
});

function setNotes() {
    notes.forEach((note) => {
        $("main").prepend(`
            <div class="note">
                <h4>${note.title}</h4>
                <span>${note.desc}</span>
            </div>
        `);
    });
}
Enter fullscreen mode Exit fullscreen mode

这段代码可能不是最好的,但我觉得这样最合理,而且我觉得这次也不需要完美的代码。无论如何,考虑到我之前的经验,这比我想象的要容易得多,而且我其实还挺喜欢的。其他应用里可能不一样的地方是笔记的顺序,因为我实在懒得去设置让它们总是添加到表单之前、其他笔记之后。不过话说回来,现在想想,这应该也不难实现。

考虑到 Angular 的功能如此强大,而我们实际用到的功能却如此之少,这个例子看起来有点傻。但与我之前可能给人的印象相反,我其实很喜欢 Angular,只是不太喜欢它与 React 之类的框架相比模块化程度不够高。好了,现在开始生成项目:

$ ng new angular
Enter fullscreen mode Exit fullscreen mode

这就是我们启动项目所需的全部步骤,Angular 的 CLI 是不是很棒?总之,我将使用基本相同的代码来实现应用程序的基本结构:

<div class="container">
  <header>
    <h1>Notes App</h1>
  </header>
  <main>
    <div class="note" *ngFor="let note of [0, 1, 2, 3]">
      <h4>Note Title</h4>
      <span>Note Body</span>
    </div>
    <div class="note">
      <form>
        <input required type="text" #noteTitle placeholder="Note Title" ngModel />
        <textarea #noteBody placeholder="Note Body" ngModel></textarea>
        <input type="submit" #noteSubmit title="Add Note" />
      </form>
    </div>
  </main>
</div>
Enter fullscreen mode Exit fullscreen mode

这可能因人而异,但我打算把应用的所有逻辑都放在应用组件本身中,不使用任何子组件。这样做虽然并非必须,但整体上会更简洁一些。总之,我们基本沿用之前的样式:

.container {
  width: 100%;
  height: auto;
  margin: 0;
  display: flex;
  flex-direction: column;

  header {
      display: flex;
      align-items: center;

      width: 100%;
      height: 56px;
      background-color: #4e78b8;
      color: white;

      h1 {
          margin-left: 6px;
      }
  }

  main {
      margin: 10px;
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
      grid-gap: 1rem;
      align-items: center;

      .note {
          display: flex;
          flex-direction: column;

          padding: 10px;
          background-color: #a15fbb;
          border-radius: 5px;

          form {
              display: flex;
              flex-direction: column;

              textarea {
                  resize: none;
              }
          }
      }
  }
}
Enter fullscreen mode Exit fullscreen mode

总之,我们可以编写一些和之前类似的代码:

import { Component } from '@angular/core';

type Note = {
  title: string;
  desc: string;
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  notes: Array<Note> = [];
  title!: string;
  body?: string;

  constructor() {
    const data = localStorage.getItem("notes");
    if (data) this.notes = JSON.parse(data);
  }

  submitForm() {
    let note: Note = {
      title: this.title,
      desc: this.body || ""
    }
    this.notes.push(note);
    localStorage.setItem("notes", JSON.stringify(this.notes));
  }
}
Enter fullscreen mode Exit fullscreen mode

这样我们就可以回到模板中,修改备注的逻辑了:

<div class="container">
  <header>
    <h1>Notes App</h1>
  </header>
  <main>
    <div class="note" *ngFor="let note of notes">
      <h4>{{note.title}}</h4>
      <span>{{note.desc}}</span>
    </div>
    <div class="note">
      <form #addNoteForm="ngForm">
        <input required type="text" placeholder="Note Title" [(ngModel)]="title" name="Title" />
        <textarea placeholder="Note Body" [(ngModel)]="body" name="Body"></textarea>
        <input type="submit" #noteSubmit title="Add Note" (click)="submitForm()" />
      </form>
    </div>
  </main>
</div>
Enter fullscreen mode Exit fullscreen mode

就到这里啦!

React

我觉得由于 React 的特性,这个项目可能会比实际需要的更复杂。React 的设计理念是比其他框架更模块化、更轻量级,但由于其结构方式,对于小型应用来说,它在某些方面实际上反而更复杂。总之,我首先使用自定义模板生成了我的 React 应用sammy-libraries

$ yarn create react-app react-app --template sammy-libraries
Enter fullscreen mode Exit fullscreen mode

我遇到了一个偶尔会出现的 bug:Node.js Sass(我仍然使用它主要是因为根据我的经验,Dart Sass 在 React 上的编译速度很慢)拒绝编译我的 Sass 代码。所以我删除了 node_modules 和 yarn.lock 文件,然后yarn重新运行,问题就解决了。总之,以下是我的操作步骤。首先,我创建了与第一个应用index.scss相同的组件styles.scss,然后在我的 App 组件中重新创建了应用的基本结构:

import React, { useEffect, useState } from "react";
import NotesList from "components/NotesList";
import { NoteType } from "components/Note";
//import "scss/App.scss";

function App() {
    const [notesList, setNotesList] = useState<NoteType[]>([]);

    const [noteTitle, setNoteTitle] = useState<string>("");
    const [noteDesc, setNoteDesc] = useState<string>("");

    useEffect(() => {
        const data = localStorage.getItem("notes");
        if (data) {
            setNotesList(JSON.parse(data));
        }
    }, []);

    useEffect(() => {
        localStorage.setItem("notes", JSON.stringify(notesList));
    }, [notesList])

    const addNote = (event: React.FormEvent<HTMLFormElement>) => {
        let note: NoteType = {
            title: noteTitle,
            desc: noteDesc,
        };
        setNotesList([...notesList, note]);
        event.preventDefault();
    };

    const changeTitle = (event: React.ChangeEvent<HTMLInputElement>) => {
        setNoteTitle(event.currentTarget.value);
    };

    const changeDesc = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
        setNoteDesc(event.currentTarget.value);
    };

    return (
        <div className="container">
            <header>
                <h1>Notes App</h1>
            </header>
            <NotesList addNote={addNote} changeTitle={changeTitle} changeDesc={changeDesc} notes={notesList} />
        </div>
    );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

这目前还没有任何作用,所以我们来添加其他组件:
我在一个单独的组件文件夹中创建了 3 个组件,然后相应地填充了它们
NotesList.tsx

import React from "react";
import AddNote from "components/AddNote";
import Note, { NoteType } from "components/Note";

type NotesListProps = {
    notes: NoteType[];
    addNote: (event: React.FormEvent<HTMLFormElement>) => void;
    changeTitle: (event: React.ChangeEvent<HTMLInputElement>) => void;
    changeDesc: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
};

function NotesList({ notes, addNote, changeTitle, changeDesc }: NotesListProps) {
    return (
        <main>
            {notes.map((note) => {
                return (
                    <Note
                        note={{
                            title: note.title,
                            desc: note.desc,
                        }}
                    />
                );
            })}
            <AddNote addNote={addNote} changeTitle={changeTitle} changeDesc={changeDesc} />
        </main>
    );
}

export default NotesList;
Enter fullscreen mode Exit fullscreen mode

Note.tsx

import React from "react";

export type NoteType = {
    title: string;
    desc: string;
}

interface NoteProps {
    note: NoteType;
}

function Note(props: NoteProps) {
    return (
        <div className="note">
            <h4>{props.note.title}</h4>
            <span>{props.note.desc}</span>
        </div>
    );
}

export default Note;
Enter fullscreen mode Exit fullscreen mode

AddNote.tsx

import React from "react";

interface AddNoteProps {
    changeTitle: (event: React.ChangeEvent<HTMLInputElement>) => void;
    changeDesc: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
    addNote: (event: React.FormEvent<HTMLFormElement>) => void;
}

function AddNote(props: AddNoteProps) {
    return(
        <div className="note">
            <form onSubmit={props.addNote}>
                <input type="text" placeholder="Note Title" onChange={props.changeTitle} />
                <textarea placeholder="Note Body" onChange={props.changeDesc}></textarea>
                <input type="submit" value="Add Note" />
            </form>
        </div>
    );
}

export default AddNote;
Enter fullscreen mode Exit fullscreen mode

这虽然不是我做过的最复杂的项目,但感觉比直接用 jQuery 或 Angular 要复杂得多,至少对我来说是这样。我真的很喜欢 React,它是我最喜欢的框架,只是我不确定我是否喜欢把它用在这种类型的项目上。目前来看,如果非要选一个,我会说 Angular 最简洁,jQuery 最合理(至少对这个项目来说是这样),而 React 则有点别扭,用起来感觉很好,但似乎没什么实际意义。

观景台

我只用过一次这个框架,这或许会让一些人觉得不可思议,但说实话,我真的没觉得有必要用它。Angular 和 React 我都能用,感觉它们已经能满足我的大部分需求了(剩下的通常用库来弥补),所以 Vue 对我来说一直都没什么用。总之,咱们来创建一个 Vue 项目吧。

$ vue ui
Enter fullscreen mode Exit fullscreen mode

我基本上沿用了所有默认设置,但选择了 TypeScript 和 SCSS(主要是 Dart Sass,这样就不会出现依赖冲突),因为我真的很喜欢在我的项目中使用它们。第一个项目中我没用 TypeScript 的唯一原因是我懒得去搞清楚 jQuery 和 TypeScript 是否兼容,不过如果你感兴趣的话,这是可以实现的。
我是怎么开发这个应用的呢?首先,我删除了几乎所有自动生成的应用代码,然后用以下代码替换了原有的应用代码:

<template>
  <div class="container">
    <header>
      <h1>Notes App</h1>
    </header>
    <main>
      <Note
        v-for="(note, index) in notes"
        :key="index"
        :title="note.title"
        :body="note.body"
      />
      <div class="note">
        <form @submit="submitForm()">
          <input type="text" placeholder="Note Title" v-model="title" />
          <textarea placeholder="Note Body" v-model="body"></textarea>
          <input type="submit" value="Add Note" />
        </form>
      </div>
    </main>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import Note from "./components/Note.vue";

type NoteType = {
  title: string;
  body: string;
};

@Component({
  components: {
    Note,
  },
})
export default class App extends Vue {
  notes: Array<NoteType> = [];
  title!: string;
  body?: string;

  constructor() {
    super();
    const data = localStorage.getItem("notes");
    if (data) this.notes = JSON.parse(data);
  }

  submitForm(): void {
    let note: NoteType = {
      title: this.title,
      body: this.body || "",
    };
    this.notes.push(note);
    localStorage.setItem("notes", JSON.stringify(this.notes));
  }
}
</script>

<style lang="scss">
body {
  height: 100%;
  width: 100%;
  margin: 0;
}

.container {
  width: 100%;
  height: auto;
  margin: 0;
  display: flex;
  flex-direction: column;

  header {
    display: flex;
    align-items: center;

    width: 100%;
    height: 56px;
    background-color: #4e78b8;
    color: white;

    h1 {
      margin-left: 6px;
    }
  }

  main {
    margin: 10px;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    grid-gap: 1rem;
    align-items: center;

    .note {
      display: flex;
      flex-direction: column;

      padding: 10px;
      background-color: #a15fbb;
      border-radius: 5px;

      form {
        display: flex;
        flex-direction: column;

        textarea {
          resize: none;
        }
      }
    }
  }
}
</style>
Enter fullscreen mode Exit fullscreen mode

然后,Note 组件是这样的:

<template>
  <div class="note">
    <h4>{{ this.title }}</h4>
    <span>{{ this.body }}</span>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

@Component({
  components: {},
})
export default class App extends Vue {
  @Prop() title!: string;
  @Prop() body?: string;
}
</script>
Enter fullscreen mode Exit fullscreen mode

事情就是这样。

苗条

这就是我一直想学的框架,但直到想到要做这个项目之前,我都没想过要碰它。基本上,我对它一无所知,只知道 Svelte 很受 Web 开发者的欢迎。不过,我之后可能会继续用 Svelte 做项目,所以现在可能还不太熟练,但以后或许会进步。
总之,在花了大约 10 分钟试图找到一个适用于 Svelte 的 yarn create-* CLI 命令(结果根本不存在)之后,我决定直接按照他们的样板代码来搭建项目。我把项目转换成了 TypeScript,因为我有点迷恋强类型语言,然后就开始了:
至于样式,我一咬牙放弃了 SCSS,我的意思是,不管 SCSS 有多简单,我都懒得去配置它,所以我直接手动编译了,反正我也不会经常修改样式表。这就是我最终选择的组件:

<script lang="ts">
import Note from "./components/Note.svelte";

type NoteType = {
    title: string;
    body: string;
};

let notes: Array<NoteType> = [];

const data = localStorage.getItem("notes");
if (data) notes = JSON.parse(data);

let title: string = "";
let body: string = "";

function onSubmit() {
    let note: NoteType = {
        title: title,
        body: body
    };
    notes.push(note);
    localStorage.setItem("notes", JSON.stringify(notes));
}
</script>

<div class="container">
    <header>
        <h1>Notes App</h1>
    </header>
    <main>
        {#each notes as note}
            <Note title={note.title} body={note.body} />
        {/each}
        <div class="note">
            <form on:submit={onSubmit}>
                <input type="text" placeholder="Note Title" bind:value={title} />
                <textarea placeholder="Note Body" bind:value={body}></textarea>
                <input type="submit" value="Add Note" />
            </form>
        </div>
    </main>
</div>

<style>
body {
  height: 100%;
  width: 100%;
  margin: 0;
}

.container {
  width: 100%;
  height: auto;
  margin: 0;
  display: flex;
  flex-direction: column;
}
.container header {
  display: flex;
  align-items: center;
  width: 100%;
  height: 56px;
  background-color: #4e78b8;
  color: white;
}
.container header h1 {
  margin-left: 6px;
}
.container main {
  margin: 10px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  grid-gap: 1rem;
  align-items: center;
}
.container main .note {
  display: flex;
  flex-direction: column;
  padding: 10px;
  background-color: #a15fbb;
  border-radius: 5px;
}
.container main .note form {
  display: flex;
  flex-direction: column;
}
.container main .note form textarea {
  resize: none;
}
</style>
Enter fullscreen mode Exit fullscreen mode

以下是 Note 组件:

<script lang="ts">
    export var title: string;
    export var body: string;
</script>

<div class="note">
    <h4>{title}</h4>
    <span>{body}</span>
</div>
Enter fullscreen mode Exit fullscreen mode

问题在于,我不知道如何解决,而且目前也不想解决:样式只有在粘贴到 `<style>` 标签内才能生效bundle.css,但每次页面刷新后样式都会重置。这在正式版应用中不会有问题,但对测试来说非常烦人。我估计短期内不会修复这个问题,但也许以后会解决。

结论

还记得我说过要尝试用 Svelte 做更多项目吗?我不知道自己能坚持多久,因为虽然我很喜欢 Svelte 的很多方面,但它的问题实在太多,让我无法更频繁地使用它。我觉得 React 在我做的那个项目中被低估了,Angular 仍然是我心目中最简洁的框架,Vue 是最有趣的,而 jQuery 可能是最好的,这让我很意外。如果让我为未来的项目选择一个框架,那肯定要看具体项目,但我感觉以后我还会用到它们,即使 Svelte 确实存在一些问题。话虽如此,我可能还是会主要用 Angular 和 React 来完成大部分工作,jQuery 和 Vue 是我的下一个选择。我或许会再给 Svelte 一次机会,但无论我之前在这个项目中是否对它有所偏见,我都不想用它做太多项目。总之,我认为这些框架在很多使用场景下都是不错的选择,我也完全理解为什么现在人们喜欢 Vue,但我不能说我的观点发生了很大的改变。

代码

所有代码都可以在 GitHub 上找到:https://github.com/jackmaster110/five-apps

文章来源:https://dev.to/sammyshear/building-the-same-app-5-times-5d8l