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

一切皆爲函數javascript

函數式編程很棒。隨着 React 的引入,愈來愈多的 JavaScript 前端代碼正在考慮 FP 原則。可是咱們如何在咱們編寫的平常代碼中開始使用 FP 思惟模式?我將嘗試使用平常代碼塊並逐步重構它。前端

咱們的問題:用戶來到咱們的登陸頁面連接後會帶一個redirect_to 參數。就像/login?redirect_to =%2Fmy-page。請注意,當%2Fmy-page 被編碼爲 URL 的一部分時,它其實是/ my-page。咱們須要提取此參數,並將其存儲在本地存儲中,以便在完成登陸後,能夠將用戶重定向到 my-page頁面。java

第 0 步:必要的方法

若是咱們以最簡單方式來呈現這個解決方案,咱們將如何編寫它?咱們須要以下幾個步驟git

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

咱們還必須將不安全的函數放到try catch塊中。有了這些,咱們的代碼將以下所示:github

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 塊並嘗試將全部內容表達爲函數。npm

// // 讓咱們聲明全部咱們須要的函數

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 的中間結果。bash

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 語法糖使這變得更好,可是這是之後的某一天。

若是發現譯文存在錯誤或其餘須要改進的地方請指出。

相關文章
相關標籤/搜索