[譯] 編寫函式式的 JavaScript 實用指南

駕考寶典heyushuo發表於2019-01-22

[譯] 編寫函式式的 JavaScript 實用指南

一切皆為函式

函數語言程式設計很棒。隨著 React 的引入,越來越多的 JavaScript 前端程式碼正在考慮 FP 原則。但是我們如何在我們編寫的日常程式碼中開始使用 FP 思維模式?我將嘗試使用日常程式碼塊並逐步重構它。

我們的問題:使用者來到我們的登入頁面連結後會帶一個redirect_to 引數。就像/login?redirect_to =%2Fmy-page。請注意,當%2Fmy-page 被編碼為 URL 的一部分時,它實際上是/ my-page。我們需要提取此引數,並將其儲存在本地儲存中,以便在完成登入後,可以將使用者重定向到 my-page頁面。

第 0 步:必要的方法

如果我們以最簡單方式來呈現這個解決方案,我們將如何編寫它?我們需要如下幾個步驟

  1. 解析連結後引數。
  2. 獲取 redirect_to 值。
  3. 解碼該值。
  4. 將解碼後的值儲存在 localStorage 中。

我們還必須將不安全的函式放到try catch塊中。有了這些,我們的程式碼將如下所示:

function persistRedirectToParam() {
  let parsedQueryParam;
  try {
    //獲取連線後的引數{redirect_to:'/my-page'}
    parsedQueryParam = qs.parse(window.location.search); // https://www.npmjs.com/package/qs
  } catch (e) {
    console.log(e);
    return null;
  }
  //獲取到引數
  const redirectToParam = parsedQueryParam.redirect_to;
  if (redirectToParam) {
    const decodedPath = decodeURIComponent(redirectToParam);
    try {
      localStorage.setItem('REDIRECT_TO', decodedPath);
    } catch (e) {
      console.log(e);
      return null;
    }
    //返回  my-page
    return decodedPath;
  }
  return null;
}
複製程式碼

第 1 步:將每一步寫為函式

暫時,讓我們忘記 try catch 塊並嘗試將所有內容表達為函式。

// // 讓我們宣告所有我們需要的函式

const parseQueryParams = query => qs.parse(query);

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const decodeString = string => decodeURIComponent(string);

const storeRedirectToQuery = redirectTo => localStorage.setItem('REDIRECT_TO', redirectTo);

function persistRedirectToParam() {
 //使用它們

  const parsed = parseQueryParams(window.location.search);

  const redirectTo = getRedirectToParam(parsed);

  const decoded = decodeString(redirectTo);

  storeRedirectToQuery(decoded);

  return decoded;
}
複製程式碼

當我們開始將所有“結果”用函式的方式表示時,我們會看到我們可以從主函式體中重構的內容。這樣處理後,我們的函式變得更容易理解,並且更容易測試。

早些時候,我們將測試主要函式作為一個整體。但是現在,我們有 4 個較小的函式,其中一些只是代理其他函式,因此需要測試的足跡要小得多。

讓我們識別這些代理函式,並刪除代理,這樣我們就可以減少一些程式碼。

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const storeRedirectToQuery = redirectTo => localStorage.setItem('REDIRECT_TO', redirectTo);

function persistRedirectToParam() {
  const parsed = qs.parse(window.location.search);

  const redirectTo = getRedirectToParam(parsed);

  const decoded = decodeURIComponent(redirectTo);

  storeRedirectToQuery(decoded);

  return decoded;
}
複製程式碼

第 2 步 嘗試編寫函式式

好的。現在,似乎 persistRedirectToParam 函式是 4 個其他函式的“組合”讓我們看看我們是否可以將此函式編寫為合成,從而消除我們儲存為 const 的中間結果。

const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to;

// we have to re-write this a bit to return a result.
const storeRedirectToQuery = (redirectTo) => {
  localStorage.setItem("REDIRECT_TO", redirectTo)
  return redirectTo;
};

function persistRedirectToParam() {
  const decoded = storeRedirectToQuery(
    decodeURIComponent(
      getRedirectToParam(
        qs.parse(window.location.search)
      )
    )
  )

  return decoded;
}
複製程式碼

這很好。但是我同情讀取這個巢狀函式呼叫的人。如果有辦法解開這個混亂,那就太棒了。

第 3 步 更具可讀性的組合

如果你已經完成了以上的一些重構,那麼你就會遇到composeCompose 是一個實用函式,它接受多個函式,並返回一個逐個呼叫底層函式的函式。還有其他很好的資源來學習 composition,所以我不會在這裡詳細介紹。

使用 compose,我們的程式碼將如下所示:

const compose = require('lodash/fp/compose');
const qs = require('qs');

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const storeRedirectToQuery = redirectTo => {
  localStorage.setItem('REDIRECT_TO', redirectTo);
  return redirectTo;
};

function persistRedirectToParam() {
  const op = compose(
    storeRedirectToQuery,
    decodeURIComponent,
    getRedirectToParam,
    qs.parse
  );

  return op(window.location.search);
}
複製程式碼

compose 內的函式執行順序為從右向左,即最右邊的函式(最後一個引數)最先執行,執行完的結果作為引數傳遞給前一個函式。因此,在 compose 鏈中呼叫的第一個函式是最後一個函式。

如果你是一名數學家並且熟悉這個概念,這對你來說不是一個問題,所以你自然會從右到左閱讀。但對於熟悉命令式程式碼的其他人來說,我們想從左到右閱讀。

第 4 步 pipe(管道)和扁平化

幸運的是這裡有pipe(管道)compose 做了同樣的事情,但是執行順序和 compose 是相反的,因此鏈中的第一個函式最先執行,執行完的結果作為引數傳遞給下一個函式。

而且,似乎我們的 persistRedirectToParams 函式已經成為另一個我們稱之為 op 的函式的包裝器。換句話說,它所做的只是執行op。我們可以擺脫包裝並“扁平化”我們的函式。

const pipe = require('lodash/fp/pipe');
const qs = require('qs');

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const storeRedirectToQuery = redirectTo => {
  localStorage.setItem('REDIRECT_TO', redirectTo);
  return redirectTo;
};

const persistRedirectToParam = fp.pipe(
  qs.parse,
  getRedirectToParam,
  decodeURIComponent,
  storeRedirectToQuery
);
複製程式碼

差不多了。請記住,我們適當地將 try-catch 塊留在後面,以使其達到正確的狀態?好的接下來,我們需要一些方式來介紹它。qs.parse 和 storeRedirectToQuery 都是不安全。一種選擇是使它們成為包裝函式並將它們放在 try-catch 塊中。另一種函式式方式是將 try-catch 表示為一種函式。

第 5 步 作為函式的異常處理

有一些實用程式做到了這一點,但讓我們自己嘗試寫一些東西。

function tryCatch(opts) {
  return args => {
    try {
      return opts.tryer(args);
    } catch (e) {
      return opts.catcher(args, e);
    }
  };
}
複製程式碼

我們的函式在這裡需要一個包含 tryer 和 catcher 函式的 opts 物件。它將返回一個函式,當使用引數呼叫時,使用所述引數呼叫 tryer 並在失敗時呼叫 catcher。現在,當我們有不安全的操作時,我們可以將它們放入 tryer 部分,如果它們失敗,則從捕獲器部分進行救援並提供安全結果(甚至記錄錯誤)。

第 6 步 把所有東西放在一起

因此,考慮到這一點,我們的最終程式碼如下:

const pipe = require('lodash/fp/pipe');
const qs = require('qs');

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const storeRedirectToQuery = redirectTo => {
  localStorage.setItem('REDIRECT_TO', redirectTo);
  return redirectTo;
};

const persistRedirectToParam = fp.pipe(
  tryCatch({
    tryer: qs.parse,
    catcher: () => {
      return {
        redirect_to: null // we should always give back a consistent result to the subsequent function
      };
    }
  }),
  getRedirectToParam,
  decodeURIComponent,
  tryCatch({
    tryer: storeRedirectToQuery,
    catcher: () => null // if localstorage fails, we get null back
  })
);

// to invoke, persistRedirectToParam(window.location.search);
複製程式碼

這或多或少是我們想要的。但是為了確保程式碼的可讀性和可測試性得到改善,我們也可以將“安全”函式(tryCatch 函式)分解出來。

const pipe = require('lodash/fp/pipe');
const qs = require('qs');

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const storeRedirectToQuery = redirectTo => {
  localStorage.setItem('REDIRECT_TO', redirectTo);
  return redirectTo;
};

const safeParse = tryCatch({
  tryer: qs.parse,
  catcher: () => {
    return {
      redirect_to: null // we should always give back a consistent result to the subsequent function
    };
  }
});

const safeStore = tryCatch({
  tryer: storeRedirectToQuery,
  catcher: () => null // if localstorage fails, we get null back
});

const persistRedirectToParam = fp.pipe(
  safeParse,
  getRedirectToParam,
  decodeURIComponent,
  safeStore
);
複製程式碼

現在,我們得到的是一個更強大功能的函式,由 4 個獨立的函式組成,這些函式具有高度內聚性,鬆散耦合,可以獨立測試,可以獨立重用,考慮異常場景,並且具有高度宣告性。

有一些 FP 語法糖使這變得更好,但是這是以後的某一天。

如果發現譯文存在錯誤或其他需要改進的地方請指出。

相關文章