在没有框架的情况下设计和构建渐进式 Web 应用程序(第 2 部分)

已发表: 2022-03-10
快速总结 ↬在本系列的第一篇文章中,您的作者,一个 JavaScript 新手,为自己设定了设计和编码基本 Web 应用程序的目标。 这个“应用程序”被称为“In/Out”——一个组织基于团队的游戏的应用程序。 在本文中,我们将集中讨论“In/Out”应用程序的实际制作过程。

这次冒险的存在理由是在视觉设计和 JavaScript 编码的学科中推动你谦逊的作者一点。 我决定构建的应用程序的功能与“待办事项”应用程序没有什么不同。 重要的是要强调这不是原始思维的练习。 目的地远没有旅程重要。

想知道应用程序是如何结束的吗? 将您的手机浏览器指向 https://io.benfrain.com。

以下是我们将在本文中介绍的内容的摘要:

  • 项目设置以及我选择 Gulp 作为构建工具的原因;
  • 应用程序设计模式及其在实践中的含义;
  • 如何存储和可视化应用程序状态;
  • CSS 是如何作用于组件的;
  • 使用了哪些 UI/UX 细节来使这些东西更像“应用程序”;
  • 职权范围如何通过迭代发生变化。

让我们从构建工具开始。

构建工具

为了让我的 TypeScipt 和 PostCSS 基本工具启动并运行并创造良好的开发体验,我需要一个构建系统。

在我的日常工作中,在过去五年左右的时间里,我一直在使用 HTML/CSS 以及在较小程度上使用 JavaScript 构建界面原型。 直到最近,我几乎完全使用 Gulp 和任意数量的插件来满足我相当简陋的构建需求。

通常,我需要处理 CSS,将 JavaScript 或 TypeScript 转换为更广泛支持的 JavaScript,偶尔还需要执行相关任务,例如缩小代码输出和优化资产。 使用 Gulp 总是让我能够沉着地解决这些问题。

对于那些不熟悉的人,Gulp 允许您编写 JavaScript 来对本地文件系统上的文件执行“某些操作”。 要使用 Gulp,您通常在项目的根目录中有一个文件(称为gulpfile.js )。 此 JavaScript 文件允许您将任务定义为函数。 您可以添加第三方“插件”,它们本质上是进一步的 JavaScript 函数,用于处理特定任务。

跳跃后更多! 继续往下看↓

Gulp 任务示例

当您更改创作样式表 (gulp-postcss) 时,一个示例 Gulp 任务可能是使用插件来利用 PostCSS 处理 CSS。 或者在保存它们时将 TypeScript 文件编译为 vanilla JavaScript (gulp-typescript)。 这是一个简单的例子,说明如何在 Gulp 中编写任务。 此任务使用“del”gulp 插件删除名为“build”的文件夹中的所有文件:

 var del = require("del"); gulp.task("clean", function() { return del(["build/**/*"]); });

requiredel插件分配给一个变量。 然后调用gulp.task方法。 我们用一个字符串作为第一个参数(“clean”)命名任务,然后运行一个函数,在这种情况下,该函数使用“del”方法删除作为参数传递给它的文件夹。 星号符号有“glob”模式,基本上表示构建文件夹的“任何文件夹中的任何文件”。

Gulp 任务可能会变得更复杂,但本质上,这就是处理事情的机制。 事实是,使用 Gulp,您无需成为 JavaScript 向导即可。 您只需要 3 级的复制和粘贴技能。

这些年来,我一直坚持使用 Gulp 作为我的默认构建工具/任务运行器,其政策是“如果它没有损坏; 不要尝试修复它'。

但是,我担心自己会陷入困境。 这是一个容易掉入的陷阱。 首先,您开始每年都在同一个地方度假,然后拒绝采用任何新的时尚潮流,最终并坚决拒绝尝试任何新的构建工具。

我在 Internet 上听到很多关于“Webpack”的讨论,并认为我有责任尝试使用前端开发人员酷孩子们的新奇吐司来尝试一个项目。

网页包

我清楚地记得我非常感兴趣地跳到了 webpack.js.org 网站。 关于 Webpack 是什么和确实是什么的第一个解释是这样开始的:

 import bar from './bar';

说什么? 用 Evil 博士的话来说,“Scott,给我扔一根该死的骨头”。

我知道这是我自己要处理的问题,但我对任何提到“foo”、“bar”或“baz”的编码解释都产生了反感。 再加上完全没有简洁地描述 Webpack 的实际用途,让我怀疑它可能不适合我。

进一步挖掘 Webpack 文档,提供了一个稍微不那么透明的解释,“在其核心,webpack 是现代 JavaScript 应用程序的静态模块捆绑器”。

嗯。 静态模块捆绑器。 那是我想要的吗? 我没有被说服。 我继续读下去,但我读得越多,我就越不清楚。 那时,依赖图、热模块重载和入口点等概念对我来说基本上是丢失的。

后来研究了几个晚上的 Webpack,我放弃了使用它的任何想法。

我确信在正确的情况和更有经验的手中,Webpack 非常强大和合适,但对于我卑微的需求来说,这似乎完全是矫枉过正。 模块捆绑、tree-shaking 和热模块重载听起来很棒; 我只是不相信我的小“应用程序”需要它们。

那么,回到 Gulp。

关于不为改变而改变的主题,我想评估的另一项技术是 Yarn over NPM,用于管理项目依赖关系。 在那之前,我一直使用 NPM,而 Yarn 被吹捧为更好、更快的替代品。 关于 Yarn,我没有太多要说的,只是如果您当前正在使用 NPM 并且一切正常,您无需费心尝试 Yarn。

Parceljs 是我无法评估此应用程序的一个工具。 零配置和支持浏览器重新加载之类的 BrowserSync 后,我在其中发现了很棒的实用程序! 此外,在 Webpack 的辩护中,有人告诉我,从 Webpack v4 开始,不需要配置文件。 有趣的是,在我最近在 Twitter 上进行的一项民意调查中,在 87 名受访者中,超过一半的人选择了 Webpack 而不是 Gulp、Parcel 或 Grunt。

我用启动和运行的基本功能启动了我的 Gulp 文件。

“默认”任务将监视样式表和 TypeScript 文件的“源”文件夹,并将它们与基本 HTML 和相关源映射一起编译到build文件夹中。

我也让 BrowserSync 与 Gulp 一起工作。 我可能不知道如何处理 Webpack 配置文件,但这并不意味着我是某种动物。 在使用 HTML/CSS 进行迭代时必须手动刷新浏览器是非常棒的 2010 年,BrowserSync 为您提供了对前端编码非常有用的简短反馈和迭代循环。

这是截至 11.6.2017 的基本 gulp 文件

你可以看到我是如何在接近发货结束时调整 Gulpfile 的,使用 ugilify 添加缩小:

项目结构

由于我的技术选择,应用程序的一些代码组织元素正在定义自己。 项目根目录中的gulpfile.jsnode_modules文件夹(Gulp 存储插件代码的地方)、用于创作样式表的preCSS文件夹、用于 TypeScript 文件的ts文件夹以及用于运行已编译代码的build文件夹。

这个想法是有一个index.html包含应用程序的“外壳”,包括任何非动态 HTML 结构,然后链接到样式和使应用程序工作的 JavaScript 文件。 在磁盘上,它看起来像这样:

 build/ node_modules/ preCSS/ img/ partials/ styles.css ts/ .gitignore gulpfile.js index.html package.json tsconfig.json

将 BrowserSync 配置为查看该build文件夹意味着我可以将浏览器指向localhost:3000 ,一切都很好。

有了一个基本的构建系统,文件组织和一些基本的设计开始,我已经用完了我可以合法地用来阻止我实际构建东西的拖延素材!

编写应用程序

应用程序如何工作的原理是这样的。 会有一个数据存储。 当 JavaScript 加载时,它将加载该数据,循环遍历数据中的每个播放器,创建将每个播放器表示为布局中的一行所需的 HTML,并将它们放置在适当的输入/输出部分中。 然后来自用户的交互会将玩家从一种状态转移到另一种状态。 简单的。

在实际编写应用程序时,需要理解的两大概念挑战是:

  1. 如何以易于扩展和操作的方式表示应用程序的数据;
  2. 当用户输入的数据发生变化时,如何让 UI 做出反应。

在 JavaScript 中表示数据结构的最简单方法之一是使用对象表示法。 这句话读起来有点计算机科学。 更简单地说,JavaScript 术语中的“对象”是一种存储数据的便捷方式。

考虑将这个 JavaScript 对象分配给一个名为ioState (用于 In/Out State)的变量:

 var ioState = { Count: 0, // Running total of how many players RosterCount: 0; // Total number of possible players ToolsExposed: false, // Whether the UI for the tools is showing Players: [], // A holder for the players }

如果您不太了解 JavaScript,那么您至少可以了解发生了什么:花括号内的每一行都是一个属性(或 JavaScript 用语中的“键”)和值对。 您可以将各种内容设置为 JavaScript 键。 例如,函数、其他数据的数组或嵌套对象。 这是一个例子:

 var testObject = { testFunction: function() { return "sausages"; }, testArray: [3,7,9], nestedtObject { key1: "value1", key2: 2, } }

最终结果是,使用这种数据结构,您可以获得和设置对象的任何键。 例如,如果我们要将 ioState 对象的计数设置为 7:

 ioState.Count = 7;

如果我们想将一段文本设置为该值,则符号的工作方式如下:

 aTextNode.textContent = ioState.Count;

您可以看到,在 JavaScript 方面,为该状态对象获取值和设置值很简单。 但是,在用户界面中反映这些变化的情况就不那么好了。 这是框架和库试图抽象出痛苦的主要领域。

一般来说,当涉及到根据状态更新用户界面时,最好避免查询 DOM,因为这通常被认为是次优方法。

考虑输入/输出接口。 它通常显示游戏的潜在玩家列表。 它们在页面下方垂直列出,一个在另一个之下。

也许每个玩家在 DOM 中都用一个包含复选框inputlabel来表示。 这样,由于标签使输入为“已选中”,单击播放器会将播放器切换到“进入”。

为了更新我们的界面,我们可能在 JavaScript 中的每个输入元素上都有一个“侦听器”。 在单击或更改时,该函数会查询 DOM 并计算检查了多少玩家输入。 根据该计数,我们将更新 DOM 中的其他内容,以向用户显示检查了多少玩家。

让我们考虑一下基本操作的成本。 我们在多个 DOM 节点上监听输入的点击/检查,然后查询 DOM 以查看有多少特定 DOM 类型被检查,然后在 DOM 中写入一些内容以向用户显示 UI 方面的玩家数量我们只是数了数。

另一种方法是将应用程序状态作为 JavaScript 对象保存在内存中。 DOM 中的按钮/输入单击可能仅更新 JavaScript 对象,然后基于 JavaScript 对象中的更改,对所需的所有界面更改进行单次更新。 我们可以跳过查询 DOM 来计算玩家数量,因为 JavaScript 对象已经保存了这些信息。

所以。 为状态使用 JavaScript 对象结构似乎很简单,但足够灵活,可以在任何给定时间封装应用程序状态。 如何管理这种情况的理论似乎也足够合理——这一定是“单向数据流”之类的短语的全部含义吗? 但是,第一个真正的技巧是创建一些代码,这些代码将根据对该数据的任何更改自动更新 UI。

好消息是,比我更聪明的人已经弄清楚了这些东西(谢天谢地! )。 自应用出现以来,人们一直在完善应对此类挑战的方法。 这类问题是“设计模式”的基础。 起初,“设计模式”这个绰号对我来说听起来很深奥,但在深入挖掘之后,一切都开始听起来不那么计算机科学,而更像常识了。

设计模式

在计算机科学词典中,设计模式是一种预定义且经过验证的解决常见技术挑战的方法。 将设计模式视为烹饪食谱的编码等价物。

也许最有名的设计模式文献是 1994 年的“设计模式:可重用面向对象软件的元素”。虽然它涉及 C++ 和 smalltalk,但概念是可以转移的。 对于 JavaScript,Addy Osmani 的“学习 JavaScript 设计模式”涵盖了类似的内容。 你也可以在这里免费在线阅读。

观察者模式

通常,设计模式分为三组:创建型、结构型和行为型。 我一直在寻找有助于处理围绕应用程序不同部分的通信更改的行为。

最近,我看到并阅读了 Gregg Pollack 对在应用程序中实现反应性的深入探讨。 这里有博客文章和视频供您欣赏。

在阅读《 Learning JavaScript Design Patterns 》中“观察者”模式的开篇描述时,我很确定它是适合我的模式。 是这样描述的:

观察者是一种设计模式,其中一个对象(称为主体)根据它(观察者)维护一个对象列表,自动通知它们状态的任何变化。

当一个主题需要通知观察者发生了有趣的事情时,它会向观察者广播一个通知(其中可以包括与通知主题相关的特定数据)。

我兴奋的关键是,这似乎提供了一些在需要时自我更新的方式。

假设用户点击了一个名为“Betty”的玩家来选择她在游戏中。 在 UI 中可能需要发生一些事情:

  1. 播放次数加 1
  2. 将贝蒂从“出局”玩家池中移除
  3. 将 Betty 添加到“In”玩家池中

该应用程序还需要更新代表 UI 的数据。 我非常想避免的是:

 playerName.addEventListener("click", playerToggle); function playerToggle() { if (inPlayers.includes(e.target.textContent)) { setPlayerOut(e.target.textContent); decrementPlayerCount(); } else { setPlayerIn(e.target.textContent); incrementPlayerCount(); } }

目的是拥有一个优雅的数据流,当中心数据发生变化时,它会更新 DOM 中所需的内容。

使用观察者模式,可以向状态发送更新,因此用户界面非常简洁。 这是一个示例,用于将新玩家添加到列表中的实际函数:

 function itemAdd(itemString: string) { let currentDataSet = getCurrentDataSet(); var newPerson = new makePerson(itemString); io.items[currentDataSet].EventData.splice(0, 0, newPerson); io.notify({ items: io.items }); }

与观察者模式相关的部分有io.notify方法。 这向我们展示了修改应用程序状态的items部分,让我向您展示监听“项目”更改的观察者:

 io.addObserver({ props: ["items"], callback: function renderItems() { // Code that updates anything to do with items... } });

我们有一个 notify 方法可以对数据进行更改,然后观察者会在他们感兴趣的属性更新时响应该数据。

使用这种方法,应用程序可以让 observables 监视数据的任何属性的变化,并在发生变化时运行一个函数。

如果您对我选择的观察者模式感兴趣,我会在这里更全面地描述它。

现在有一种基于状态有效更新 UI 的方法。 桃色。 然而,这仍然给我留下了两个明显的问题。

一个是如何跨页面重新加载/会话存储状态,以及尽管 UI 在视觉上正常工作,但它并不是很“像应用程序”。 例如,如果按下按钮,UI 会立即在屏幕上发生变化。 它只是不是特别引人注目。

让我们首先处理事物的存储方面。

保存状态

我在开发方面的主要兴趣集中在了解如何构建应用程序界面并使其与 JavaScript 交互。 如何从服务器存储和检索数据或处理用户身份验证和登录是“超出范围”的。

因此,我没有连接到 Web 服务来满足数据存储需求,而是选择将所有数据保留在客户端上。 有许多 Web 平台方法可以在客户端上存储数据。 我选择了localStorage

localStorage 的 API 非常简单。 您可以像这样设置和获取数据:

 // Set something localStorage.setItem("yourKey", "yourValue"); // Get something localStorage.getItem("yourKey");

LocalStorage 有一个setItem方法,您可以将两个字符串传递给该方法。 第一个是您要存储数据的键的名称,第二个字符串是您要存储的实际字符串。 getItem方法将字符串作为参数返回给您存储在 localStorage 中该键下的任何内容。 很好很简单。

但是,不使用 localStorage 的原因之一是所有内容都必须保存为“字符串”。 这意味着您不能直接存储诸如数组或对象之类的东西。 例如,尝试在浏览器控制台中运行这些命令:

 // Set something localStorage.setItem("myArray", [1, 2, 3, 4]); // Get something localStorage.getItem("myArray"); // Logs "1,2,3,4"

即使我们尝试将“myArray”的值设置为数组; 当我们检索它时,它已被存储为一个字符串(注意 '1,2,3,4' 周围的引号)。

您当然可以使用 localStorage 存储对象和数组,但您需要注意它们需要从字符串来回转换。

因此,为了将状态数据写入 localStorage,它使用JSON.stringify()方法写入字符串,如下所示:

 const storage = window.localStorage; storage.setItem("players", JSON.stringify(io.items));

当需要从 localStorage 检索数据时,使用JSON.parse()方法将字符串转换回可用数据,如下所示:

 const players = JSON.parse(storage.getItem("players"));

使用localStorage意味着一切都在客户端上,这意味着没有第三方服务或数据存储问题。

数据现在持续刷新和会话——耶! 坏消息是 localStorage 在用户清空浏览器数据时无法生存。 当有人这样做时,他们所有的输入/输出数据都会丢失。 这是一个严重的缺点。

不难理解,“localStorage”可能不是“正确”应用程序的最佳解决方案。 除了前面提到的字符串问题之外,由于它阻塞了“主线程”,因此对于严肃的工作来说也很慢。 替代方案即将出现,例如 KV 存储,但现在,请注意根据适用性对其使用进行警告。

尽管在用户设备上本地保存数据很脆弱,但连接到服务或数据库的做法仍受到抵制。 相反,通过提供“加载/保存”选项来回避这个问题。 这将允许 In/Out 的任何用户将他们的数据保存为 JSON 文件,如果需要,可以将其加载回应用程序。

这在 Android 上运行良好,但在 iOS 上就不太优雅了。 在 iPhone 上,它会导致屏幕上出现大量文本,如下所示:

在 iPhone 上,它导致屏幕上出现大量文字
(大预览)

正如你可以想象的那样,我并不是唯一一个通过 WebKit 批评苹果这个缺点的人。 相关的错误在这里。

在撰写本文时,这个错误已经有了解决方案和补丁,但尚未进入 iOS Safari。 据称,iOS13 修复了它,但在我写的时候它是 Beta 版。

因此,对于我的最小可行产品,这是存储解决方案。 现在是时候尝试让事情变得更像“应用程序”了!

App-I-Ness

经过与许多人的多次讨论后发现,准确定义“应用程序喜欢”的含义是相当困难的。

最终,我决定将“类似应用程序”作为网络上通常缺少的视觉流畅度的代名词。 当我想到使用起来感觉很好的应用程序时,它们都具有运动功能。 不是无缘无故的,而是增加你行动故事的动作。 它可能是屏幕之间的页面转换,菜单弹出的方式。 很难用语言来描述,但我们大多数人在看到它时就知道了。

需要的第一件视觉天赋是将玩家名称从“In”向上或向下移动到“Out”,反之亦然。 让玩家立即从一个部分移动到另一个部分很简单,但肯定不是“类似应用程序”。 单击播放器名称时的动画有望强调该交互的结果——玩家从一个类别移动到另一个类别。

与许多此类视觉交互一样,它们表面上的简单性掩盖了实际使其正常运行所涉及的复杂性。

它需要几次迭代才能使运动正确,但基本逻辑是这样的:

  • 单击“玩家”后,从几何上捕获该玩家在页面上的位置;
  • 测量玩家向上移动时需要移动到区域顶部的距离('In')以及向下移动时需要移动到底部的距离('Out');
  • 如果向上移动,则需要在玩家向上移动时留下与玩家行高相等的空间,并且上方的玩家应该以与玩家向上移动以降落在空间中的时间相同的速度向下塌陷由现有的“In”玩家(如果有的话)下场腾空;
  • 如果玩家要“出局”并向下移动,则其他所有内容都需要向上移动到左侧空间,并且玩家需要最终位于任何当前“出局”玩家的下方。

呸! 它比我用英语想象的要复杂——更别提 JavaScript 了!

还有其他复杂性需要考虑和试验,例如过渡速度。 一开始,并不清楚是恒定的移动速度(例如每 20 毫秒 20 像素)还是恒定的移动持续时间(例如 0.2 秒)看起来更好。 前者稍微复杂一些,因为需要根据玩家需要移动的距离“动态”计算速度——更大的距离需要更长的过渡持续时间。

然而,事实证明,恒定的过渡持续时间不仅在代码中更简单; 它实际上产生了更有利的效果。 差异是微妙的,但这些选择只有在您看到这两个选项后才能确定。

在试图确定这种效果时,经常会出现视觉故障,但不可能实时解构。 我发现最好的调试过程是创建动画的 QuickTime 记录,然后一次通过一帧。 这总是比任何基于代码的调试更快地揭示问题。

现在看代码,我可以理解,在我不起眼的应用程序之外,这个功能几乎肯定可以更有效地编写。 鉴于应用程序会知道玩家的数量并知道板条的固定高度,因此完全可以仅在 JavaScript 中进行所有距离计算,而无需读取任何 DOM。

并不是说交付的东西不起作用,只是它不是您会在 Internet 上展示的那种代码解决方案。 等一下。

其他“类似应用程序”的交互更容易实现。 与其通过切换显示属性之类的简单操作来简单地进出菜单,而是通过更巧妙地展示它们来获得很多里程。 它仍然被简单地触发,但 CSS 完成了所有繁重的工作:

 .io-EventLoader { position: absolute; top: 100%; margin-top: 5px; z-index: 100; width: 100%; opacity: 0; transition: all 0.2s; pointer-events: none; transform: translateY(-10px); [data-evswitcher-showing="true"] & { opacity: 1; pointer-events: auto; transform: none; } }

在那里,当在父元素上切换data-evswitcher-showing="true"属性时,菜单将淡入,转换回其默认位置,并且将重新启用指针事件,以便菜单可以接收点击。

ECSS 样式表方法论

您会注意到,在之前的代码中,从创作的角度来看,CSS 覆盖嵌套在父选择器中。 这就是我一直喜欢编写 UI 样式表的方式; 每个选择器的单一事实来源以及封装在一组大括号中的该选择器的任何覆盖。 这是一种需要使用 CSS 处理器(Sass、PostCSS、LESS、Stylus 等)的模式,但我认为这是利用嵌套功能的唯一积极方式。

我在我的书《持久的 CSS》中巩固了这种方法,尽管有许多更复杂的方法可用于为界面元素编写 CSS,但 ECSS 为我和与我合作的大型开发团队提供了良好的服务,因为该方法首次被记录在案回到2014年! 在这种情况下,它被证明同样有效。

对 TypeScript 进行分区

即使没有 CSS 处理器或像 Sass 这样的超集语言,CSS 也可以使用 import 指令将一个或多个 CSS 文件导入另一个:

 @import "other-file.css";

当我开始使用 JavaScript 时,我很惊讶没有类似的东西。 每当代码文件长于屏幕或如此之高时,总感觉将其拆分成更小的部分是有益的。

使用 TypeScript 的另一个好处是它有一种非常简单的方法,可以将代码拆分为文件并在需要时导入它们。

此功能早于原生 JavaScript 模块,是一个非常方便的功能。 编译 TypeScript 时,它会将所有内容拼接回单个 JavaScript 文件。 这意味着可以轻松地将应用程序代码分解为可管理的部分文件以进行创作并轻松导入到主文件中。 主inout.ts的顶部如下所示:

 /// <reference path="defaultData.ts" /> /// <reference path="splitTeams.ts" /> /// <reference path="deleteOrPaidClickMask.ts" /> /// <reference path="repositionSlat.ts" /> /// <reference path="createSlats.ts" /> /// <reference path="utils.ts" /> /// <reference path="countIn.ts" /> /// <reference path="loadFile.ts" /> /// <reference path="saveText.ts" /> /// <reference path="observerPattern.ts" /> /// <reference path="onBoard.ts" />

这个简单的家务和组织任务帮助很大。

多个事件

一开始,我觉得从功能的角度来看,一个单一的事件,比如“星期二晚上足球”就足够了。 在那种情况下,如果你加载了 In/Out ,你只是添加/删除或移动玩家进出,就是这样。 没有多重事件的概念。

我很快决定(即使是最小可行的产品)这将带来非常有限的体验。 如果有人在不同的日子组织了两场比赛,有不同的球员名单怎么办? In/Out 肯定可以/应该满足这种需求吗? 重新塑造数据以使其成为可能并修改加载不同集合所需的方法并没有花费太长时间。

一开始,默认数据集如下所示:

 var defaultData = [ { name: "Daz", paid: false, marked: false, team: "", in: false }, { name: "Carl", paid: false, marked: false, team: "", in: false }, { name: "Big Dave", paid: false, marked: false, team: "", in: false }, { name: "Nick", paid: false, marked: false, team: "", in: false } ];

包含每个玩家的对象的数组。

在考虑了多个事件后,它被修改为如下所示:

 var defaultDataV2 = [ { EventName: "Tuesday Night Footy", Selected: true, EventData: [ { name: "Jack", marked: false, team: "", in: false }, { name: "Carl", marked: false, team: "", in: false }, { name: "Big Dave", marked: false, team: "", in: false }, { name: "Nick", marked: false, team: "", in: false }, { name: "Red Boots", marked: false, team: "", in: false }, { name: "Gaz", marked: false, team: "", in: false }, { name: "Angry Martin", marked: false, team: "", in: false } ] }, { EventName: "Friday PM Bank Job", Selected: false, EventData: [ { name: "Mr Pink", marked: false, team: "", in: false }, { name: "Mr Blonde", marked: false, team: "", in: false }, { name: "Mr White", marked: false, team: "", in: false }, { name: "Mr Brown", marked: false, team: "", in: false } ] }, { EventName: "WWII Ladies Baseball", Selected: false, EventData: [ { name: "C Dottie Hinson", marked: false, team: "", in: false }, { name: "P Kit Keller", marked: false, team: "", in: false }, { name: "Mae Mordabito", marked: false, team: "", in: false } ] } ];

新数据是一个数组,每个事件都有一个对象。 然后在每个事件中都有一个EventData属性,该属性是一个数组,其中包含与以前一样的玩家对象。

重新考虑界面如何最好地处理这一新功能需要更长的时间。

从一开始,设计就一直很枯燥。 考虑到这也应该是一种设计练习,我觉得我不够勇敢。 因此,从标题开始,添加了更多的视觉效果。 这是我在 Sketch 中模拟的:

修改后的应用程序设计模型
修改后的设计模型。 (大预览)

它不会赢得奖项,但它肯定比它开始时更引人注目。

抛开美学不谈,直到有人指出,我才意识到标题中的大加号图标非常令人困惑。 大多数人认为这是添加另一个事件的一种方式。 实际上,它切换到了“添加玩家”模式,带有一个花哨的过渡,让您可以在当前事件名称所在的位置输入玩家的名称。

这是另一个例子,新鲜的眼睛是无价的。 这也是放手的重要一课。 老实说,我一直在标题中保留输入模式转换,因为我觉得它很酷而且很聪明。 然而,事实是它没有服务于设计,因此也没有服务于整个应用程序。

这在现场版本中已更改。 相反,标题只处理事件——一种更常见的场景。 同时,添加玩家是从子菜单中完成的。 这为应用程序提供了更易于理解的层次结构。

这里学到的另一个教训是,只要有可能,从同行那里获得坦率的反馈是非常有益的。 如果他们是善良和诚实的人,他们不会让你给自己通过!

摘要:我的代码很糟糕

正确的。 到目前为止,普通的科技冒险回顾展; 这些东西在 Medium 上是 10 美分! 公式是这样的:开发人员详细说明了他们如何克服所有障碍,将经过微调的软件发布到互联网上,然后在谷歌接受面试或在某个地方被录用。 然而,事情的真相是,我是这个应用程序构建问题的第一次参加者,所以代码最终作为“完成”的应用程序发送到天堂!

例如,使用的观察者模式实现效果很好。 一开始我很有条理,有条不紊,但随着我变得更加迫切地想要完成事情,这种方法“走向南方”。 就像一个连续节食者一样,熟悉的旧习惯又重新出现,代码质量随后下降。

现在看看发布的代码,它是一个不太理想的干净观察者模式和沼泽标准事件侦听器调用函数的大杂烩。 In the main inout.ts file there are over 20 querySelector method calls; hardly a poster child for modern application development!

I was pretty sore about this at the time, especially as at the outset I was aware this was a trap I didn't want to fall into. However, in the months that have since passed, I've become more philosophical about it.

The final post in this series reflects on finding the balance between silvery-towered code idealism and getting things shipped. It also covers the most important lessons learned during this process and my future aspirations for application development.