走進開源專案 - urlcat 原始碼分析

robin發表於2022-03-14

《走進開源專案 - urlcat》中,對專案整體進行了分析,對如何做開源也有了進一步的瞭解,該篇再深入研究下 urlcat 原始碼。

該專案到底做了什麼?

// 常規寫法一
const API_URL = 'https://api.example.com/';

function getUserPosts(id, blogId, limit, offset) {
  const requestUrl = `${API_URL}/users/${id}/blogs/${blogId}/posts?limit=${limit}&offset=${offset}`;
  // send HTTP request
}

// 常規寫法二
const API_URL = 'https://api.example.com/';

function getUserPosts(id, blogId, limit, offset) {
  const escapedId = encodeURIComponent(id);
  const escapedBlogId = encodeURIComponent(blogId);
  const path = `/users/${escapedId}/blogs/${escapedBlogId}`;
  const url = new URL(path, API_URL);
  url.search = new URLSearchParams({ limit, offset });
  const requestUrl = url.href;
  // send HTTP request
}

// 使用 urlcat 之後的寫法
const API_URL = 'https://api.example.com/';

function getUserPosts(id, limit, offset) {
  const requestUrl = urlcat(API_URL, '/users/:id/posts', { id, limit, offset });
  // send HTTP request
}

原始碼共 267 行,其中註釋佔了近 110,程式碼只有 157 行。註釋跟程式碼接近 1:1 ,接下來我們逐段分析。

第一段

import qs, { IStringifyOptions } from 'qs';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ParamMap = Record<string, any>;
export type UrlCatConfiguration =
  Partial<Pick<IStringifyOptions, 'arrayFormat'> & { objectFormat: Partial<Pick<IStringifyOptions, 'format'>> }>

該專案是在 qs 專案的基礎上並使用 typescript 進行開發,其中定義了 2 個型別,有幾個不太瞭解知識點 typeRecodePartialPick

interface 與 type 的區別

  • 相同點:都可以描述物件或者函式,且可以使用 extends 進行擴充
  • 不同點:

    • type 可以宣告基本型別別名,聯合型別,和元組等型別,但 interface 不行

      // 基本型別別名
      type Name = string | number;
      
      // 聯合型別
      interface Common {
          name: string;
      }
      interface Person<T> extends Common {
        age: T;
        sex: string;
      }
      
      type People<T> = {
        age: T;
        sex: string;
      } & Common;
      
      type P1 = Person<number> | People<number>;
      
      // 元組
      type P2 = [Person<number>, People<number>];
    • 跟 typeof 結合使用

      const name = "小明";
      
      type T= typeof name;

Record 的用途

Reacord 是 TypeScript 的一種工具類。

// 常規寫法
interface Params {
    [name: string]: any;
}

// 高階寫法
type Params = Recode<string, any>

Partial 的用途

將傳入的屬性變為可選項

interface DataModel {
  name: string
  age: number
  address: string
}

let store: DataModel = {
  name: '',
  age: 0,
  address: ''
}

function updateStore (
  store: DataModel,
  payload: Partial<DataModel>
):DataModel {
  return {
    ...store,
    ...payload
  }
}

store = updateStore(store, {
  name: 'lpp',
  age: 18
})

Pick 的用途

從型別 Type 中,挑選一組屬性組成一個新的型別返回。這組屬性由 Keys 限定, Keys 是字串或者字串並集。

interface Person {
  name: string
  age: number
  id: string
}

// 幼兒沒有id
type Toddler = Pick<Person, 'name' | 'age'>

第二段

/**
 * Builds a URL using the base template and specified parameters.
 *
 * @param {String} baseTemplate a URL template that contains zero or more :params
 * @param {Object} params an object with properties that correspond to the :params
 *   in the base template. Unused properties become query params.
 *
 * @returns {String} a URL with path params substituted and query params appended
 *
 * @example
 * ```ts
 * urlcat('http://api.example.com/users/:id', { id: 42, search: 'foo' })
 * // -> 'http://api.example.com/users/42?search=foo
 * ```
 */
export default function urlcat(baseTemplate: string, params: ParamMap): string;

/**
 * Concatenates the base URL and the path specified using '/' as a separator.
 * If a '/' occurs at the concatenation boundary in either parameter, it is removed.
 *
 * @param {String} baseUrl the first part of the URL
 * @param {String} path the second part of the URL
 *
 * @returns {String} the result of the concatenation
 *
 * @example
 * ```ts
 * urlcat('http://api.example.com/', '/users')
 * // -> 'http://api.example.com/users
 * ```
 */
export default function urlcat(baseUrl: string, path: string): string;

/**
 * Concatenates the base URL and the path specified using '/' as a separator.
 * If a '/' occurs at the concatenation boundary in either parameter, it is removed.
 * Substitutes path parameters with the properties of the @see params object and appends
 * unused properties in the path as query params.
 *
 * @param {String} baseUrl the first part of the URL
 * @param {String} path the second part of the URL
 * @param {Object} params Object with properties that correspond to the :params
 *   in the base template. Unused properties become query params.
 *
 * @returns {String} URL with path params substituted and query params appended
 *
 * @example
 * ```ts
 * urlcat('http://api.example.com/', '/users/:id', { id: 42, search: 'foo' })
 * // -> 'http://api.example.com/users/42?search=foo
 * ```
 */
export default function urlcat(
  baseUrl: string,
  pathTemplate: string,
  params: ParamMap
): string;

/**
 * Concatenates the base URL and the path specified using '/' as a separator.
 * If a '/' occurs at the concatenation boundary in either parameter, it is removed.
 * Substitutes path parameters with the properties of the @see params object and appends
 * unused properties in the path as query params.
 *
 * @param {String} baseUrl the first part of the URL
 * @param {String} path the second part of the URL
 * @param {Object} params Object with properties that correspond to the :params
 *   in the base template. Unused properties become query params.
 * @param {Object} config urlcat configuration object
 *
 * @returns {String} URL with path params substituted and query params appended
 *
 * @example
 * ```ts
 * urlcat('http://api.example.com/', '/users/:id', { id: 42, search: 'foo' }, {objectFormat: {format: 'RFC1738'}})
 * // -> 'http://api.example.com/users/42?search=foo
 * ```
 */
export default function urlcat(
  baseUrlOrTemplate: string,
  pathTemplateOrParams: string | ParamMap,
  maybeParams: ParamMap,
  config: UrlCatConfiguration
): string;

export default function urlcat(
  baseUrlOrTemplate: string,
  pathTemplateOrParams: string | ParamMap,
  maybeParams: ParamMap = {},
  config: UrlCatConfiguration = {}
): string {
  if (typeof pathTemplateOrParams === 'string') {
    const baseUrl = baseUrlOrTemplate;
    const pathTemplate = pathTemplateOrParams;
    const params = maybeParams;
    return urlcatImpl(pathTemplate, params, baseUrl, config);
  } else {
    const baseTemplate = baseUrlOrTemplate;
    const params = pathTemplateOrParams;
    return urlcatImpl(baseTemplate, params, undefined, config);
  }
}

這部分程式碼是利用 TypeScript 定義過載函式型別,採用連續多個過載宣告 + 一個函式實現的方式來實現,其作用是為了保證在呼叫該函式時,函式的引數及返回值都要相容所有的過載。

例如下圖,第三個引數型別在過載函式型別中並不存在。

Untitled.png

第三段

以下程式碼是核心,作者通過職責分離的方式,將核心方法程式碼簡化。

// 核心方法
function urlcatImpl(
  pathTemplate: string,
  params: ParamMap,
  baseUrl: string | undefined,
  config: UrlCatConfiguration
) {
    // 第一步 path('/users/:id/posts', { id: 1, limit: 30 }) 返回 "/users/1/posts" 和 limit: 30
  const { renderedPath, remainingParams } = path(pathTemplate, params);
    // 第二步 移除 Null 或者 Undefined 屬性
  const cleanParams = removeNullOrUndef(remainingParams);
    // 第三步 {limit: 30} 轉 limit=30
  const renderedQuery = query(cleanParams, config);
    // 第四步 拼接返回 /users/1/posts?limit=30
  const pathAndQuery = join(renderedPath, '?', renderedQuery);

    // 第五步 當 baseUrl 存在時,執行完整 url 拼接
  return baseUrl ? joinFullUrl(renderedPath, baseUrl, pathAndQuery) : pathAndQuery;
}

總結

做開源並不一定要造個更好的輪子,但可以讓這個輪子變得更好。通過該專案,也發現自己在 TypeScript 方面的不足,繼續學習,再接再厲。

參考文章

擴充閱讀

相關文章