iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Modern Web

Angular 工程師的 React 陣痛期:30 天心智模型重建系列 第 7

Day 7|沒有 @angular/cli之後:結構要自己立規矩

  • 分享至 

  • xImage
  •  

框架給了骨架,然後就沒了

npx create-react-router@latest file-manager

跑完打開資料夾,大致上會有:

app/
  root.tsx
  routes.ts
  routes/
    home.tsx
react-router.config.ts
vite.config.ts

有進入點、有路由設定、有型別產生、有建置設定——至少不是一片空白。

然後開始放第一個元件。

FileList.tsx 要放哪?它不是路由,不屬於 routes/。放 app/ 底下跟 root.tsx 並排?那 hook 呢?API 呼叫呢?共用的 Button 呢?

框架告訴我路由住在哪裡,但沒告訴我其他東西住在哪裡。

骨架有了,骨架之外全是空白。React又開始給我這個該死的自由了,海的另一邊是自由嗎?


一、CLI 給的不是程式碼,是架構的決定

我原本以為 ng g co file-row 的價值是省下打字。

錯了。它真正做的是替我做完一連串決定

  • 檔名用 kebab-case格式後
  • 一個元件拆成 .ts.html.scss.spec.ts 四個檔
  • class 叫 FileRowComponent,selector 加 app- 前綴
  • 放在當前目錄下的同名資料夾裡

以前沒有為這些事情煩惱過一秒鐘,因為 CLI 早就決定好了。真懷念飯來張口當伸手牌的日子。

而且全世界的 Angular 專案長得都差不多——換一家公司、接手一個陌生專案,打開 src/app 就知道東西在哪。這不是因為 Angular 工程師有默契,是因為大家用同一份被提供的建議。

React Router 也有建議,但範圍很窄。它規定了三件事(當然我最近也有看過不同的設計,下面講廣義的):

  • 路由檔案放 routes/,並在 routes.ts 登記
  • 每個路由檔案要 default export 一個元件
  • 設定檔叫 react-router.config.ts

這三件事之外,一律可以自由發揮。但說實在Angular有時也可以發揮啦啦,但會覺得為啥這麼亂搞而已。

所以「沒有 CLI」不是少了一個產生器,是少了一份完整的建議。剩下的八成,得由你自己提供。


二、所以我把 Angular 的結構整包先搬過來

面對那八成的空白,我做了最自然的事:複製貼上我熟悉的東西。

app/
  core/
    services/
    interceptors/
    guards/
  shared/
    components/
    pipes/
    utils/
  features/
    files/
    settings/
  routes/

core 放只能有一份的東西、shared 放到處都在用的東西、features 放各個功能——這套結構大致就是這樣。

當下的感覺是:終於有點熟悉的味道了。


三、幾週後,開始思考哪樣才是最適合的

先講結論:不是這套結構不好,是它在 React 裡沒有東西可以依附。

core/ 失去了判準

core 在 Angular 存在的理由很明確:那些只能被根模組注入一次的東西。HttpInterceptorprovidedIn: 'root' 的 service、route guard——它們有一個共同的身分,由 NgModule 和 injector 定義。

React 沒有 module,沒有 injector。一個單例就是 export const apiClient = ...(Day 2 講過),任何檔案都能放,也沒有誰規定它非得在某一層註冊。

於是 core/ 沒有了判準。它從「單例的家」變成「我不知道該放哪的東西的家」,裡面躺著彼此無關的檔案。

shared/ 變成垃圾場

這件事在 Angular 也會發生,但那邊至少有 SharedModule 逼你顯式列出 exports——每加一個東西,得在 module 裡寫一行,那一行就是一次小小的自我審查。

React 沒有這道關卡。

features/routes/ 打架

這是最煩的一個。框架規定路由住 routes/,於是同一個功能被拆成兩半:

app/routes/files.tsx          ← 路由與 loader 在這
app/features/files/           ← 元件與邏輯在這

改一個頁面,兩個資料夾來回跳。而且 routes/files.tsx 裡常常只剩三行——export 一個 loader、export 一個元件、從 features 裡 import 進來。

根因

盯著這個結構看了一陣子,我才想通:

Angular 的資料夾結構,是 module 階層的投影。

coresharedfeatures 之所以是那三個,因為它們對應 CoreModuleSharedModuleFeatureModule——資料夾只是把那個階層畫在檔案系統上而已。真正立規矩的是 module,資料夾只是它的影子。

我把影子搬過來了,但投下影子的那個東西不存在。


四、重來之後長這樣

我全部打掉重排,換成一條原則:就近放置(colocation)。

東西放在用到它的地方旁邊,而不是按「型別」分類。

app/
  routes/
    files.tsx                 ← 路由與 loader
    files.$id.tsx

  features/
    files/
      FileList.tsx
      FileRow.tsx
      useFiles.ts             ← 這個功能的 hook
      api.ts                  ← 這個功能的 API 呼叫
      types.ts
    settings/
      ...

  components/                 ← 真的到處都在用的才進來
    Button.tsx
    Modal.tsx

  lib/
    apiClient.ts
    format.ts

差別在哪?舊的結構問「這是什麼型別類型的檔案」,新的結構問「這是誰的東西」。

useFiles.ts 是一個 hook,但它只服務檔案功能,所以它住在 features/files/ 而不是某個統一的 hooks/ 資料夾。改檔案功能的時候,相關的東西全在同一個資料夾裡,不用在四個地方跳。

routes/files.tsx 依然只有幾行,但心態變了——我不再覺得它是被切走的一半,而是功能對外的接口:路由在這裡宣告我需要什麼資料、要渲染誰,實作在 features 裡。

至於什麼時候該把東西往上搬,只有一條規則:

等到第三個地方用到它,再往上搬。 在那之前,留在原地。

兩個地方用,複製一份沒關係。過早抽出來的共用元件,通常會在第三個需求進來時被改成一個滿是 if 的四不像。

順帶一提,Angular 也在往同個方向走

Standalone components 普及之後,NgModule 逐漸淡出。而 NgModule 一走,core/shared/features 那套結構的必要性也跟著下降——它本來就是 module 階層的投影,投影源消失了,影子自然要重畫。

現在的 Angular 專案也愈來愈多採用按功能就近放置。這件事上,兩邊其實在靠攏。
只是大多專案因為公司常有固定規範,所以即使換了版本但內裡其實不換的,因為停留在舊時代的人不好維護。

我搬過來的那套結構,某種意義上不只是「Angular 的結構」,而是「我熟悉的那個版本的 Angular 的結構」。


五、那些決定,得自己寫下來

沒有 CLI 幫你決定,就得自己決定——而且要寫下來,不然第三個人進專案時會出現第三種寫法。

但記得把討論後的放進 README 裡面:

檔名。 FileRow.tsx 還是 file-row.tsx?Angular CLI 只給一個答案,React 社群兩派都很多。我們選了元件用 PascalCase、其他用 camelCase,理由只是「檔名跟它 export 的東西一致」。

barrel file。 每個資料夾要不要放 index.ts 當統一出口?看起來乾淨,但會拖累 tree-shaking、也容易製造循環引用。也是維持著兩派論點。

測試檔位置。 放元件旁邊(FileRow.test.tsx)還是集中到 __tests__/?既然走就近放置,測試當然也放旁邊。

Lint 設定。 ng lint 沒有了,ESLint 加 Prettier 要自己組。eslint-plugin-react-hooks 一定要裝——Day 4 那個依賴陣列的坑就靠它擋一半。

這些決定沒有標準答案,重點不在選哪個,在選了就統一

Angular 把這份統一內建在 CLI 裡,所以你不用開會討論。React 要你自己開那個會,目前待的專案就常討論結構問題,後來才發現之前被 Angular 保護太好。想一想我剛進 React 專案卻想套用 Angular 的分類方式,沒人說不行,但或許會有更適合的方式,畢竟方式是人討論出來的。


六、Week 1 結束了

回頭看這七天不見的東西:DI、模板語法、生命週期、變更偵測、雙向綁定、專案結構。

寫的時候我以為是六個獨立的主題。排在一起才發現,它們是同一件事的六個面貌

Angular 把大量決定內建成框架的一部分——誰負責建立實例、什麼時候重新渲染、資料能往哪流、檔案該放哪。你不需要有意見,因為框架已經有了。

React 幾乎什麼都不決定。它給你一個渲染函式,跟一句「其他的你自己看著辦」。

這週我一直在做同一件事:找對應寫法。ngOnInit 對應什麼、@if 對應什麼、providedIn: 'root' 對應什麼。而答案一次比一次更接近同一句——

沒有對應寫法。那是一個決定,現在輪到你來做。

大致接受了「東西真的不見了」這件事。但本次提到的專案結構啊,說到底每家公司做法不同,打一架吧,打完厲害的有說服力的就會被留下來。

但接受不等於服氣。下週要進入抗拒期:我還不打算放棄 Angular 的做法。 我要把 RxJS 那一整套思維原封不動搬過去——switchMapforkJoindebounceTime,全部自己手刻。

然後看它撞牆。

明天先從最根本的開始:Observable 跟 Promise,到底差在哪。


上一篇
Day 6|@Input/@Output → props 與 callback:歡迎來到單向資料流
下一篇
Day 8|Observable 與 Promise 的世界觀差異
系列文
Angular 工程師的 React 陣痛期:30 天心智模型重建9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言