头图

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

《走进开源项目 - 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 方面的不足,继续学习,再接再厉。

参考文章

拓展阅读


白话前端
前端学习,总结精选。
1 篇内容引用
avatar
robin
SegmentFault 前端工程师

折腾不止、夜游八方

1.5k 声望
3.2k 粉丝
0 条评论
推荐阅读
2022 年终总结
插图来源:[链接]今年可以用四个词概括,「寻找」、「坚持」、「有始有终」、「突破」寻找喜欢什么想要成为什么样的人未来做什么事喜欢的事,才能有源源不断的动力坚持下去,编码依旧是我最喜欢的事,乐此不疲。...

robin5阅读 2k评论 3

封面图
手把手教你写一份优质的前端技术简历
不知不觉一年一度的秋招又来了,你收获了哪些大厂的面试邀约,又拿了多少offer呢?你身边是不是有挺多人技术比你差,但是却拿到了很多大厂的offer呢?其实,要想面试拿offer,首先要过得了简历那一关。如果一份简...

tonychen152阅读 17.7k评论 5

封面图
正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青56阅读 8.4k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy48阅读 6.9k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木75阅读 7k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs42阅读 6.8k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木45阅读 8.5k评论 6

avatar
robin
SegmentFault 前端工程师

折腾不止、夜游八方

1.5k 声望
3.2k 粉丝
宣传栏