我們需要怎樣的 Service

ES2049發表於2022-01-11

14 世紀,英格蘭的邏輯學家奧卡姆在他的《箴言書注》中說「不要浪費過多的東西,去做那些用較少的東西同樣可以做好的事情」。後來這句話被簡化為「奧卡姆剃刀原理」,即:如無必要,勿增實體。奧卡姆剃刀在各個領域都有他的運用,他不是一個公理,沒有嚴謹的推導過程,但他卻是一個在實踐中被證明非常有效的解決問題的手段。

在程式設計世界裡,有太多我們習以為常的東西,我相信存在即合理,同時我也相信存在都有前提,而前提會隨著時間變化甚至消失。下面我想跟大家探討下,我們前端專案中那些應該被剃刀剃掉的東西。

前端專案裡的 service 層

在一個前端專案中,一般包含以下檔案目錄:

  • containers:頁面
  • components:元件
  • utils:工具方法
  • routes:路由
  • services:資料服務
  • index.js 入口檔案

我們的業務程式碼基本都在 containers components 裡,utils 和 routes 也是必不可少的,但仔細思考我們就會發現,這裡有個 services 資料夾,他被稱為資料服務層,是我們跟後端打交道的。這一層真的需要嗎?
我們來看看大家是怎麼使用 service 的。

// services  資料夾下的 accoutService.js

import { post } from '@/utils/request';

// 獲取賬號列表
export const getAccountsList = params => post('/api/accounts.json', params);
// 新增賬號
export const insertAccount = params => post('/api/insertAccount.json', params);
// 更新狀態
export const updateAccount = params => post('/api/updateAccount.json', params);
// 校驗賬號查詢
export const checkAccount = params => post('/api/checkAccount.json', params);

-------- 使用 ---------

import { getAccountsList } from '@/services/accountService'

const App = () => {
  const [name, setName] = useState('');
    useEffect(() => {
      getAccountsList().then((res) => {
       setName(res.name);
    });
  }, [])
  return <div>{name}</div>;
};

從上面的程式碼我們可以看出,services 檔案下基本是一些模板程式碼,偶爾有少見的一些資料轉換。這些內容對於我們的業務程式碼來說,都是非業務相關的,寫這些模板性的控制程式碼真的有必要嗎?

service 裡包含什麼?

  • 資料轉換邏輯 converHandler
  • 資料請求工具 request
  • 請求地址定義 url
  • 全域性攔截器 interceptor
  • 附加功能 openApi

資料轉換邏輯 converHandler:並不通用,有的一個請求在不同的頁面需要走不同的轉換邏輯,這些轉換邏輯一般會寫在呼叫位置的程式碼裡,我也建議這麼做,因為資料轉換也是這塊某個 container 的功能,而且為了方便測試,建議新增 handler.js 將轉換邏輯抽離出來。

資料請求工具 request:主要是封裝各種請求,這部分需要統一。非業務相關,可以提出來。

請求地址定義 url:這部分是強業務相關的,不應該放到 service 裡,而是作為 service 的一個配置,由外部輸入。

全域性攔截器 interceptor:處理一些通用的業務狀態碼,比如編輯成功 10001,這部分也是強業務相關的,而且相對比較複雜,但是可以通過配置 schame 來描述,後面再講。

附加功能 openAPI:如果你係統的介面想讓別的系統複用,比如 MTEE 基礎平臺的介面需要複用給運營平臺,那麼前端需要提供領域物料,領域物料裡會發請求,發請求要解決跨域、登陸、授權的問題,openAPI 應運而生。

綜上可以看出,service 層只需要一些統一的邏輯處理和配置檔案就能描述清楚,甚至我們可以把 Service 層簡化為

$$service = request + config$$

我的 service 包

由此,我希望能設計這樣一個 service 包,他需要包含下面的功能:

請求

支援常見的 get post jsonp 請求,以及對於這些請求的附加方法,比如 debounce、throttle、快取、loading 等功能。也可以提供大家比較喜歡的 hooks API。

介面配置

一個介面包含域名 domain,地址路徑 path,請求方法 method,引數 params,一些常見功能的開關,比如開啟防抖 { debounce:true } 。引數的配置裡,可以新增該引數的基本屬性,比如是否必選 { require: true } ,這樣包內可以對引數做必要的校驗,這樣可以保證非法資料傳入後臺。

環境切換

環境切換是一個非業務相關的功能,他不應該硬編碼到程式碼裡,帶到線上。他應該只是一個配置,儘量與程式碼脫離,因此是用瀏覽器外掛來切換,就是一個很好的方法。可以設計 service 包接收一個 domainMap,這個 domainMap 來自 window.GlobalConfig 下的某個變數,瀏覽器外掛可以動態改變這個變數,就可以做到環境的切換了。
image.png

閘道器轉發

我們寫程式碼追求複用,從程式碼塊的複用到元件複用,再到業務能力的複用,而業務能力複用的一個載體就是領域物料。一個領域物料裡很有多個介面請求,如果我們把原來在業務程式碼裡的元件拆出來作為領域物料的話,就不得不把專案裡的 service 層也要打包進去,這樣才能傳送請求和處理一些統一的異常。上面的我提到的把是service 層做成一個包,別人在使用的時候,只需要傳配置進來,也是出於領域物料這個場景。
這之後,我們還要解決一個問題:領域物料在不同站點使用帶來的介面跨域問題。我們現在的解決辦法是,前端搭建一套基於 node 的閘道器,用於做介面轉發和鑑權。service 包裡會整合這個過程,外部使用者只需要配置開不開啟閘道器就可以了。他完全不需要知道閘道器是如何轉發的,就像在自己的站點下寫元件一樣。

介面文件

我們在接手別的專案的時候,總是不容易找到他的介面文件,因為文件和程式碼是割裂的,文件的維護也有滯後性,甚至慢慢文件的連結也找不到了。因此,程式碼和文件應該在一起,最好是程式碼即文件。大家可能覺得用註釋就可以了,但程式設計師總是要求別人寫註釋,但自己卻不愛寫。寫註釋如果可以像寫程式碼一樣,或許能規範這部分的行為。例如:

{
    name: '獲取賬號',
    domain: DOMAIN.TAOBAO,
    url: '/api/getAccount.json',
    method: METHOD.GET,
    params: {
      userId: {
        name: '策略包id',
        type: PARAM_TYPE.STRING,
        required: true,
      },
    },
    response: {
        name: '賬戶名字'
    },
  },

這裡用配置檔案的方式規範了文件的形式,還可以與瀏覽器外掛相結合,通過外掛來檢視當前用的介面文件。

異常攔截

異常分為伺服器異常和業務異常,伺服器異常一般是用 http 狀態碼,400、500等;業務異常則需要是用 body 裡的 code 來表示。在真實的業務實踐中,我們發現對於伺服器異常我們是很容易寫出通用的攔截器做一些處理的,但是對於業務異常,就相對比較複雜了,這裡面存在幾個問題:

  • 很多後端不習慣使用 code 返回相應的業務編碼來表示不同的狀態。
  • 前端直接使用後端返回的 message 展示給使用者,這裡有兩個問題,① 後端的需要引入第三方庫對 message 做國際化 ② 後端定義的 message 不是使用者語言,使用者一般是看不懂的。因此這裡就需要一個第三方系統的參與,他提供業務 code 和前端動作的對映關係表,比如:後端返回 code:10000,前端應該彈窗並展示 message,定義的 json 如下:
{
  code: 10000,
  message: '編輯失敗',
  debug: '後端資料庫讀寫異常,堆疊資訊:',
  showType: 'openDialog'
}

這裡的 message 是可以根據不同語言環境返回不同語言文字的,showType 表示了前端的動作型別,這個是可列舉的,其中肯定有一種動作是,不做動作,直接透傳。這個第三方系統,就可以配置不同編碼的動作,有利於精細化的管理異常,給使用者更好的體驗。

落地

實踐是檢驗真理的唯一標準,基於上面的理想,我的 service 包也已經成型,使用他非常簡單。只需要兩步:
① 配置檔案
② 引入包
③ 業務程式碼裡呼叫

配置

// 配置檔案 account.js

import { METHOD, PARAM_TYPE } from '@ali/hulu-service';

export const DOMAIN = {
   TAOBAO: '//taobao.com',
   ALIPAY: '//alipay.com',
};

export default {
  getAccount: {
    name: '獲取賬號',
    domain: DOMAIN.TAOBAO,
    url: '/api/getAccount.json',
    method: METHOD.GET,
    params: {
      userId: {
        name: '策略包id',
        type: PARAM_TYPE.STRING,
        required: true,
      },
    },
    response: {
        name: '賬戶名字'
    },
  },
};

引入包

import HService from '@ali/hulu-service';
import account from './account';

// 初始化service
const service = HService.init({
  urls: [
      account,
  ]
});

export default service;

呼叫 API

import Service from './service';

const App = () => {
  const [name, setName] = useState('');
    useEffect(() => {
      Service.getAccount().then((res) => {
       setName(res.name);
    });
  }, []);
  return <div>{name}</div>;
};

export default App;

同時基於瀏覽器外掛,可以快速的切換環境,檢視介面文件等。

想想邊界

開頭,我們說到奧卡姆剃刀,如無必要,勿增實體,這個的前提是,有清晰獨立的實體,如果我們的實體之間相互勾連耦合,那又如何剃掉不必要的實體呢。
其實,無論做任何軟體構架,都要分清楚邊界,也就是一個模組他的定位是什麼,哪些功能是他該做的,哪些不是。這裡面一個非常重要的依據就是是否易於變更。哪些是業務的、常變化的,哪些是非業務的、一般不變的。我們的程式碼常常,壞就壞在邊界不清晰,或者是邊界原則沒有一以貫之。工程程式碼裡耦合了業務,業務程式碼裡摻雜著工程(比如環境判斷)。程式碼的壞味道是一點一點積累而成的,而這個壞的開始,就是初始的架構設計邊界不清晰,沒有用程式碼定義規範。
抵抗程式碼的腐敗,這是一個漫漫長路,沒有銀彈,但確實可以精進一個人的系統思維。

作者:ES2049 / 黑石
文章可隨意轉載,但請保留此原文連結。
非常歡迎有激情的你加入 ES2049 Studio,簡歷請傳送至 caijun.hcj@alibaba-inc.com

相關文章