6
头图

In "Into the Open Source Project - urlcat" , the overall project was analyzed, and a further understanding of how to do open source was obtained. This article will further study the source code of urlcat .

What exactly does the project do?

// 常规写法一
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
}

The source code has a total of 267 lines, of which the comments account for nearly 110 , and the code is only 157 lines. The comments and the code are close to 1:1 , so let's analyze it piece by piece.

first paragraph

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'>> }>

This project is based on the qs project and developed using typescript, which defines 2 types, some of which are not very familiar with knowledge points type , Recode , Partial and Pick .

The difference between interface and type

  • The same point: both can describe objects or functions, and can be extended using extends
  • difference:

    • type can declare basic type aliases, union types, and tuples, but interfaces cannot

      // 基本类型别名
      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>];
    • Use with typeof

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

Purpose of Record

Reacord is a tool class for TypeScript.

// 常规写法
interface Params {
    [name: string]: any;
}

// 高级写法
type Params = Recode<string, any>

Purpose of Partial

Make incoming properties optional

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
})

Purpose of Pick

From the type Type, select a set of properties to form a new type to return. This set of properties is qualified by Keys, which are strings or a union of strings.

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

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

second paragraph

/**
 * 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);
  }
}

This part of the code uses TypeScript to define the type of overloaded function, and implements it by using multiple overloaded declarations + one function implementation. Its function is to ensure that when the function is called, the parameters and return values of the function must be compatible with all overloaded.

For example, in the figure below, the third parameter type does not exist in the overloaded function type.

Untitled.png

third paragraph

The following code is the core. The author simplifies the core method code by separating responsibilities.

// 核心方法
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;
}

Summarize

Doing open source doesn't have to make a better wheel, but it can make the wheel better. Through this project, I also found my shortcomings in TypeScript, continue to learn and make persistent efforts.

Reference article

Extended reading


robin
1.6k 声望3.2k 粉丝

折腾不止、夜游八方~