iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
JavaScript

前端也可以搞微服務?!前端最複雜的一種架構系列 第 27

(二十七) 大型前端架構的共用管理

  • 分享至 

  • xImage
  •  

大型前端架構的共用管理

當你的專案隨著時間成長,你會越來越需要管理共用模組,這時候你就會需要一套共用管理機制。就像一個辦公室,隨著人擴張和成長,你沒辦法一直沿用原本的結構和安排,你換需要換伴公司,不可能瞬間進行橫向擴張。有的人說,那一開始就把架構開大不就好了?這叫過度設計,因為你無法確認未來是否會需要這些東西,會浪費一堆開發時間在處理系統架構的環節,你只要確保為來能持續拓張規模就好。

版本管理

這是所有前往大型架構的必經之路,你必須要有一套版本管理機制,確保版本升級不會影響到其他模組。經常有人會跟我說,使用 Monorepo 會有共用版本問題,使用微前端會有版本管理問題,使用共用的 npm package 會有版本管理問題。但這些都是謬誤,因為就算今天不使用這些技術,你在單體架構中依然有版本管理問題,只是你沒有發現而已。

其實你平常寫的模組就是一個個的共用模組,你每一次的修改都是一個讀一無二的版號,你每一個 git commit 都是在進行版本破壞,只是你沒有發現而已。所以往上思考,你去做這些複雜的拆分其實也是一種管理手段,它只是讓你對於版本會更加敏感,讓你更早發現模組管理議題問題,而不是等到專案規模已經大到變成巨獸時才害怕的無法進行改善。

抽象化

會經常需要變動某部分底層功能的程式碼,導致依賴的高階模組經常要被調整,那是因為你沒有做好抽象化,導致你無法單純的對於底層進行調整,你必須要跟著底層的調整而調整,這樣的架構是無法持續擴張的。你需要做的是,將底層的程式碼進行抽象化,讓底層的調整不會影響到高階模組,這樣你就可以單純的對於底層進行調整,而不需要跟著底層的調整而調整。說起來很繞口令,但這就是抽象化的目的。另外抽象化還可以帶來許多好處,像是降低耦合度、提高模組的可重用性、提高模組的可測試性、避免依賴循環。甚至你不會套件升級個版本需要大範圍進行改動。

我以 axios 為例子,這是前端常用的 HTTP 請求套件,它提供了許多方便的功能,像是攔截器、轉換器、取消請求、錯誤處理等等。但隨著時代演變,套件需要升級,也有可能你會想換掉套件。

import axios from "axios";

const axiosInstance = axios.create({
  baseURL: "https://example.com",
});

export const getUsers = () => axiosInstance.get("/apis/v1/users");

如果你想把 axios 換成 fetch,你會需要進行大量的修改,這樣的架構是無法持續擴張的,也難以抽換。

- import axios from "axios";

- const axiosInstance = axios.create({
-  baseURL: "https://example.com",
- });

+ const baseURL = "https://example.com";
+ const fetchInstance = {
+  get: (url) => {
+    return fetch(baseURL + url).then((res) => res.json());
+  }
+ }

- export const getUsers = () => axiosInstance.get("/apis/v1/users");
+ export const getUsers = () => fetchInstance.get("/apis/v1/users");

如果你換一個寫法,把 axiosInstance 抽象命名為 requestInstance

import axios from "axios";

export const requestInstance = axios.create({
  baseURL: "https://example.com",
});

export const getUsers = () => requestInstance.get("/apis/v1/users");

當你想要換掉 axios 時,你只需要換掉 requestInstance 的實作,業務邏輯的部分一行都不用修改。

- import axios from "axios";

- export const requestInstance = axios.create({
-  baseURL: "https://example.com",
- });

+ const baseURL = "https://example.com";
+ const requestInstance = {
+  get: (url) => {
+    return fetch(baseURL + url).then((res) => res.json());
+  }
+ }

export const getUsers = () => requestInstance.get("/apis/v1/users");

依賴反轉

大部分前端在寫程式時會很習慣去增加模組的依賴量,這其實對於模組來說會限制它的發展,也很容易因為需要用大量 map 的手段去管理無關模組,不但同時新增功能時會去大幅度改動個部位模組,很難同時進行協同開發。

當你在開發某個業務邏輯時,你會需要用到許多不同的 context,像是 user context、auth context、theme context、language context、notification context 等等。因為 App 需要依賴這些高階的 context,所以你會需要去 import 這些 context,這樣的架構會讓你的模組耦合度提高,也很難進行協同開發。

import { UserContext } from "./UserContext";
import { AuthContext } from "./AuthContext";
import { ThemeContext } from "./ThemeContext";
import { LanguageContext } from "./LanguageContext";
import { NotificationContext } from "./NotificationContext";

class App {
  contexts = [
    new UserContext(),
    new AuthContext(),
    new ThemeContext(),
    new LanguageContext(),
    new NotificationContext(),
  ];
}

const app = new App();

但如果改以依賴注入的方式,App 就不需要依賴這些 context,而是透過參數的方式傳入,他並不關心未來有哪些 Context 被傳入,App 的依賴性就減低了。他們只需要知道自己是什麼 Context 就好,這樣的架構可以讓各個 Context 更容易進行協同開發,也可以更容易進行單元測試,不會有開發衝突問題。

export interface Context {
  /* implement */
}

class App {
  contexts: Context[];

  useContext(ctx: Context) {
    this.contexts.push(ctx);
  }
}

export const app = new App();
import { app } from "./app";
import type { Context } from "./app";

// ==============================

class UserContext implements Context {
  /* implement */
}

app.useContext(new UserContext());

// ==============================

class AuthContext implements Context {
  /* implement */
}
app.useContext(new AuthContext());

// ==============================

class ThemeContext implements Context {
  /* implement */
}
app.useContext(new ThemeContext());

// ==============================

class LanguageContext implements Context {
  /* implement */
}
app.useContext(new LanguageContext());

// ==============================

class NotificationContext implements Context {
  /* implement */
}
app.useContext(new NotificationContext());

反思

在開發這樣的大型架構中需要應用各種複雜的技巧,這些技巧都是為了讓你的架構能夠持續擴張,不會因為規模擴張而導致開發困難,甚至無法進行開發。然而也提高了架構的複雜度,需要有熟悉軟體開發哲學的人來管理這些架構,以免失控或過度設計。


上一篇
(二十六) DDD 大型前端架構
下一篇
(二十八) 你可能不需要微前端
系列文
前端也可以搞微服務?!前端最複雜的一種架構30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言