在《走进开源我的项目 - 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-anyexport 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}// 幼儿没有idtype 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 定义重载函数类型,采纳间断多个重载申明 + 一个函数实现的形式来实现,其作用是为了保障在调用该函数时,函数的参数及返回值都要兼容所有的重载。

例如下图,第三个参数类型在重载函数类型中并不存在。

第三段

以下代码是外围,作者通过职责拆散的形式,将外围办法代码简化。

// 外围办法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 方面的有余,持续学习,再接再厉。

参考文章

  • 玩转TypeScript工具类型(上)
  • 你不晓得的 TypeScript 高级类型
  • 请别误用 TypeScript 重载函数类型

拓展浏览

  • 玩转TypeScript工具类型(中)
  • 玩转TypeScript工具类型(下)