我們如何打造 Web App

AppDev Ooops
AppDev Ooops
Published in
13 min readApr 17, 2023

這篇文章想跟你分享

在開發 Web 專案時,會有很多項目需要決定,在進行考量時又有許多面向需要進行分析。這篇文章將會從我們團隊的角度出發,說說我們是如何打造 Web 專案?如何考量、又會是因為哪些原因而選擇這些工具?

前言

在開始任何一個新的 Web App 時,為了之後工作的快樂程度,一定會好好的考量一番基礎選擇,例如:要使用什麼語言、框架、將如何 Bundle、是否需要進行效能分析、是否需要編寫文件等等繁雜的選擇,而感到頭痛。

這篇文章將以我們團隊的角度,說明我們將如何取捨、建立專案,以下將這些在開發初期常遇到的選擇題分為幾個大方向,將內容拆分方便讀者了解,分別為:

  • 初始化專案
  • 架構設計
  • 團隊風格
  • 持續交付/持續部署(CI / CD)與測試

為了讓每個大方向中的切入角度都能夠被點題,以及確保整份文章的一致性,以下大部分的舉例都將以 JavaScript 生態系為主。

初始化專案

大部分的框架都有提供 CLI ( Command Line Interface ) ,能快速建立初始專案,例如:

  • React : npx create-react-app my-app
  • Vue : npm create vue@3
  • Svelte : npm create svelte@latest my-app

雖說看起來方便又快速,但卻在一開始就強迫決定太多事情,如此一來就難以做出對團隊最合適得選擇。

因此接著我們將以各種角度來考量,團隊是如何挑選並建立初始專案的:

框架

在框架的挑選上,考量了以下幾點:

  • 使用情境:是否需要經常互動?→ CSR ; 是否需以瀏覽居多?→ SSG ; 是否需要良好的 SEO?→ SSR。
  • 社群活躍度:如果一個社群足夠活躍,就代表或許你遇到的問題,對別人來說早就討論過並有了解法,對於開發將會是一大助力。
  • 維護難度:如果使用的框架時常有更新,就代表隨著時間推進,這份專案維護難度並不一定會顯著的提升,當然,這很大一定程度取決於最初是如何設計架構的。
  • 生態系:框架是否有相對應的解決方案,例如:路由、資料流/狀態管理、打包工具、測試工具、等等等 …,如果框架有相關生態圈的話,在開發上會相對輕鬆很多。

對需要長期經營的專案來說,只要是成熟且熟悉的框架,都是好選擇!

綜合以上,我們選擇了 Vue 作為開發框架,除了團隊成員的熟悉度以外,身為前端前幾大框架社群活躍度足夠,有相關的生態系、也有專業團隊持續維護更新,更有 Nuxt 這種往 SSR(Server Side Rendering)兼容的升級方案。

總的來說,他是一個成熟的框架,當創建一個需要長期經營的專案,選擇一個成熟且熟悉的框架,不管是 Vue、React、Angular、Svelte,都是一個好的選擇。

ToolChain

ToolChain 就是泛指一個專案會應用到的工具,這些工具可以被用來完成軟體開發的不同階段,包含:

這裡的選擇有很大一部分是基於團隊成員的開發習慣,例如:TypeScriptSass ,因此就會需要相對應的編譯器。打包工具除了是因為熟悉相關的 Plug-ins 之外,再加上 Webpack 相關社群也還算活躍。

監控工具則是考量到後續維護,透過預先埋點,整合 log、error、event 並導流到 Sentry、Rollbar,方便查看整體專案的使用情況,也可以提早發現問題,討論出相對應的解決辦法。

當然還有一些部分尚未提及,包含 CI / CD、分析工具、測試等等,也都涵蓋在 ToolChain 的範圍內,後續將會有更詳細的描述。

不管是 ToolChain 或框架,視團隊需求來決定吧!

如何挑選框架和 ToolChain 沒有絕對,單看團隊的需求,所以如果團隊對於要建置初始專案有一些考量,那就嘗試看看不依賴 CLI 自己打造吧!

延伸閱讀: Create a React App …From Scratch

架構設計

專案會隨著開發不斷擴增,所以我們希望設計的架構,可以易於更新,同時更能保持穩定。

為了這樣的目標,我們團隊依照 The Clean Architecture On Frontend 的內容,實作了大致框架,整體架構看起來會像:

Clean Architecture
來源:The Clean Architecture

實際上的做法,就是將專案分為三個層級,由內而外分別為:

  1. 領域層(Domain Layer):描述物件(Entities)本身的層級,每個物件不會改變,外部只能使用它,但不能決定他會如何被使用。
  2. 應用層(Application Layer):描述物件被如何使用,他會開啟一個一個端口(ports),描述這些使用情境該如何被使用。
  3. 適配層(Adapters Layer):將外部與內部的交流層,負責將外部資訊轉換為端口適配的資訊。

為了能夠最大程度的降低耦合,使整個專案能夠更加靈活,因此,適配層又分為兩個角色:

  • 驅使者(Driving):傳送資料到應用層,就像提交表單的按鈕。
  • 被驅使者(Driven):接收從應用層來的資料,就像是某一種服務,例如:搜尋。

這樣不管第三方如何使用、需求如何變更,都可以快速適應。當然,這三個層級有著嚴謹的依賴規則,其中包含:

  1. 領域層必須完全獨立
  2. 只有外部層級可以依賴內部層級

尊循架構設計,讓專案更易於維護、擴展、測試

如此一來,那麼整個架構就能夠達成:

  • 容易維護:依賴清晰,不容易牽扯到不相關的程式。
  • 容易擴展:只需要更換/新增 適配層,就能夠快速達成需求,例如:UI 從 vue 換成 react。
  • 容易測試:每個物件的使用情況清晰,且無相關依賴,容易撰寫測試。

儲存庫選擇

當設計好了大致的架構,剩下就是專案要如何放置的問題。

如果只是考量到只開發單一專案,那只使用單一儲存庫是沒問題的,但問題是,隨著開發的專案越來越多,其中很多共通的商業邏輯就會被重複撰寫,一些 test 和 CI 流程也要再重新設置,反而會降低效率。

團隊在開發新專案時,已經考量到以後專案擴充的相關問題,因此我們選擇在初期就建設 Monorepo

Monorepo

Monorepo 就是就在在同一份儲存庫下,涵蓋多個不同的套件。在良好的關係定義下(掌握好核心依賴不被影響),就可以享有諸多好處,例如:易於維護、套件間共用相同商業邏輯。

假設我想建構一個購物網站,架構大致上如下:

monorepo 架構圖
  • /app : 下面放的就是每個不同的專案
  • /lib : 下面放的就是共通的商業邏輯

另外如果團隊有使用強型別的語言,例如 TypeScript ,就可以依樣放置在 lib 內,方便統一管理與取用。

Monorepo 具有以下特點:

  • 共享 lib(商業邏輯): lib 可以共享和重複使用,並且可以簡化 lib 彼此之間的依賴項管理。
  • 整體可視性:更輕鬆地查看所有 lib 的更改和進度,而不需要切換到其他儲存庫。
  • 一致性:全部專案都使用相同的架構和測試工具,並且可以減少維護不同儲存庫所需的時間和精力。
  • 更好的部署:在 Monorepo 中,可以更輕鬆地將多個專案部署到同一個環境中,從而簡化部署流程。

團隊內部曾經有花一些時間去考察市面上 Monorepo 框架之間不同的差異,最後選擇了 Nx 這一個框架。

原因在於 Nx 不只能達成上述優勢,還能夠提供文件生成依賴圖生成等等要素,如果有機會的話,有可能未來還會針對 Monorepo 的使用心得作分享!

現階段的團隊,真的有需要 Monorepo 嗎?

當然 Monorepo 也有其壞處:

  • 巨大的專案:Monorepo 大小可能隨著專案數量增加,從而增加了編譯和部署時間。
  • 複雜的依賴:lib 和專案之間的依賴管理可能會變得更加複雜,因為所有專案都需要相同的依賴項版本。
  • Side Effect:單一 lib 中的任何錯誤都可能影響所有專案。這可能會使專案變得不穩定,lib 也很難更動,因為錯誤可能會影響多個專案。

還有更重要的,就是建置成本與維護的複雜度,如果只是想開發一個獨立專案,那其實也不需要使用到 Monorepo。

團隊風格

再多人開發的情況下,就算一開始的架構設計得再好,如果每個人的撰寫風格都不一致,只會造成專案越來越難維護。

因此團隊會選擇一些工具來幫助統一撰寫風格,以及使用一些自動化文件生成的工具,來幫助成員之間消減彼此的隔閡,讓大家的程式都可以更融洽的共存在一起,維護更加輕鬆,且讓開發更有效率,這些工具大致上可以分文以下幾類:

排版與撰寫風格

上面列舉出的各項,都是團隊內在開發時會使用到的相關工具,linter 能夠檢查出不合規的撰寫方式 ; formater 可以讓開發者在儲存時就能自動排版,這兩者都能幫助開發者在編譯時期就能一定程度的保證程式會趨於團隊共識,例如:

  • 要不要加分號
  • 能不能放置 console
  • camelCase or snake_case

等等的條例,至於要有什麼內容也沒有一定的標準,取決於團隊共識,但要如何取得共識就是另一個問題了。

space vs tab meme

ESLint 和 Prettier 在這兩個領域都算是老大哥了,因此沒有其他理由不用他們,ESLint 更是有諸多像是 AirbnbGoogle 出的範本可供參考,可以說是相當方便。

文件化

文件化的部分則是有更多選擇,團隊內部目前有使用到 JSDocStorybook

JSDoc

算是一個元老級的工具,用來撰寫註解,幫助其他開發者可以快速了解自己撰寫的程式碼,以及如何使用它。

/**
* 加總兩個數
* @param {number} a
* @param {number} b
* @returns {number}
*/
function sum(a:number,b:number){
return a+b
}

如此一來,搭配特定 IDE(例如:vscode),從其他地方引用時,將滑鼠移動到該 function 上,也可以快速看到註解,可謂是相當方便, TypeDoc 更是以這位老前輩為基礎所做出的另一個註解工具,還能搭配 CLI 檢查哪個 export function 沒有加上 comment 喔!

Storybook

是一個將 UI 元件文件化的工具,他會根據你撰寫的 Meta,搭配 CLI yarn run storybook 生成一個可供玩耍的網頁,讓開發者能夠最大化的去測試、了解之前定義好的 UI 元件該如何使用。

import type { Meta } from '@storybook/react';

import { Button } from './Button';

const meta: Meta<typeof Button> = {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/.
title: 'Button',
component: Button,
};

export default meta;
來源:Story Book 演示

持續交付/持續部署(CI / CD)與測試

當程式被撰寫完成,我們會需要用更為嚴格的角度去把關我們即將交付的程式碼,這裡面的過程不外乎就是:測試、分析、打包、部署。

要達成這件事有很多種方法,包含 GitHubGitLab,都有各自的方式,要使用哪一個方法,就端看你將專案儲存在哪個平台,以下就來分別說說我們會在交付和部署時分別經過哪些處理吧!

持續交付

在交付時,會固定執行測試分析

測試

測試是為了確保程式有照當初設計的情況撰寫,至少確認核心功能沒有出問題,分為:

  • 單元測試:以最小單位進行測試,最常見的工具是 Jest
  • 整合測試:確保程式在彼此互動間的狀況運作正常,這時為了確保測試情況單一,通常會使用一些工具去模擬部分內容,基於 Clean Architecture,我們團隊的做法是針對特定物件撰寫 function 去生成假資料,例如:Facker
  • E2E 測試:以實際的使用情況去測試,可以使用像是 Playwright 這種會模擬使用行為的測試框架去代替完成。

分析

分析是確保程式的品質,包含維護成本、安全性、以及效能問題,其中也包含了追蹤使用者的使用情況:

持續部署

當需要釋出新的專案版本時,會經歷過預先設定好的打包部署流程。

打包流程會將上述的工具鏈工具們,透過撰寫完成的 config 編譯、打包起來,例如將 TypeScriptBabelPostCss 編完成之後,再由 Webpack 等 Bundler 的設定,透過 splitMinificationUglify 等等的方式,將程式碼打包之後,就可以藉由發布流程出去了。

可惜的是,以 web 來說實作策略部署的成本較高,除非針對使用者分群,否則很難達到像 App 那般策略性的部署,不過最近團隊內正在實作 electron 的特殊版本發佈流程,或許等之後有機會就可以和大家分享了。

總結

時間、人力成本、以及團隊熟悉度

綜觀以上,描述了很多我們團隊在各種情境上的選擇,整個過程不是一蹴而就,而是經歷了非常多的研究、比較、討論,最後才能達成共識,且因為時間和人力成本、其他 Feature 也要跟著進行等等原因,就導致了一部分都還是以團隊熟悉度為考量的選擇 XD。

但其實這也不是壞事,畢竟團隊開發還是要以能穩定輸出為主,反而是受到新工具影響而效率降低,那便不是一件好事了。

希望以上內容能給各位讀者一點小小的收穫,在思考如何打造新專案時更有想法!

Reference

Written By

Consulted From

--

--