走进开源项目|走进开源项目 - 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;
export type UrlCatConfiguration =
Partial & { objectFormat: Partial> }>
该项目是在 qs 项目的基础上并使用 typescript 进行开发,其中定义了 2 个类型,有几个不太了解知识点
type
、 Recode
、Partial
和 Pick
。interface 与 type 的区别
- 相同点:都可以描述对象或者函数,且可以使用
extends
进行拓展 - 不同点:
- type 可以声明基本类型别名,联合类型,和元组等类型,但 interface 不行
// 基本类型别名 type Name = string | number; // 联合类型 interface Common { name: string; } interface Person
extends Common { age: T; sex: string; }type People = { age: T; sex: string; } & Common; type P1 = Person | People ; // 元组 type P2 = [Person , People ];
- 跟 typeof 结合使用
const name = "小明"; type T= typeof name;
- type 可以声明基本类型别名,联合类型,和元组等类型,但 interface 不行
Reacord
是 TypeScript 的一种工具类。// 常规写法
interface Params {
[name: string]: any;
}// 高级写法
type Params = Recode
Partial 的用途
将传入的属性变为可选项
interface DataModel {
name: string
age: number
address: string
}let store: DataModel = {
name: '',
age: 0,
address: ''
}function updateStore (
store: DataModel,
payload: Partial
):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
第二段
/**
* 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;
}
总结 【走进开源项目|走进开源项目 - urlcat 源码分析】做开源并不一定要造个更好的轮子,但可以让这个轮子变得更好。通过该项目,也发现自己在 TypeScript 方面的不足,继续学习,再接再厉。
参考文章
- 玩转TypeScript工具类型(上)
- 你不知道的 TypeScript 高级类型
- 请别误用 TypeScript 重载函数类型
- 玩转TypeScript工具类型(中)
- 玩转TypeScript工具类型(下)
推荐阅读
- 简单实现一个快速传输电子书到kindle的小项目
- BBS项目(二)
- LuLu|LuLu UI - 腾讯阅文集团出品的“半封装” 开源 Web UI 组件库,特点是面向设计、简单灵活、支持 Vue
- 聊聊jenkins部署vue/react项目的问题
- BBS项目(一)
- 开源商城|微信商城小程序 带java后台源码
- Java毕业设计项目实战篇|Java项目:企业人事管理系统(java+SSM+jsp+mysql+maven)
- react|React Native 制作iOS静态库供其他原生项目使用
- sql|sql 查询字符串分组聚合(分组连接)
- 基于ESP8266+BY8301语音模块的与山地车捉迷藏的小项目