在沒有框架的情況下設計和構建漸進式 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.