走进开源项目 - urlcat 源码分析

date
Mar 14, 2022
slug
urlcat-source-code-interpretation
status
Published
tags
OpenSource
summary
《走进开源项目 - urlcat》中,对项目整体进行了分析,对如何做开源也有了进一步的了解,该篇再深入研究下 urlcat 源码。
type
Post
《走进开源项目 - 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 方面的不足,继续学习,再接再厉。

参考文章

拓展阅读

对于本文内容有任何疑问, 可与我联系.