Inside unjs/ofetch baseURL
參考版本:
紀錄 ofetch 如何處理 options.baseURL
ofetch('/movie', {
baseURL: 'https://api.example.com',
});
// url: "https://api.example.com/movie"
上方 usage,對照到 ofetch 實作會調用 withBase(context.request, context.options.baseURL)
import { withBase } from "ufo";
// skip
const $fetchRaw: $Fetch["raw"] = async function $fetchRaw(
_request,
_options = {}
) {
const context: FetchContext = {
request: _request,
options: mergeFetchOptions(_options, globalOptions.defaults, Headers),
response: undefined,
error: undefined,
};
// skip
if (typeof context.request === "string") {
if (context.options.baseURL) {
context.request = withBase(context.request, context.options.baseURL);
}
if (context.options.query) {
context.request = withQuery(context.request, context.options.query);
}
}
// skip
}
繼續看到 withBase(input, base)
,base
只需要加在相對路徑之前,這裡做了以下驗證:
- 如果
base
為空,直接回傳input
- 如果
input
包含 protocol 格式,代表不是相對路徑,直接回傳input
- 如果
input
開頭與base
重複,直接回傳input
function withBase(input: string, base: string) {
if (isEmptyURL(base) || hasProtocol(input)) {
return input;
}
const _base = withoutTrailingSlash(base);
if (input.startsWith(_base)) {
return input;
}
return joinURL(_base, input);
}
isEmpty
function isEmptyURL(url: string) {
return !url || url === "/";
}
hasProtocol
hasProtocol(input)
,有三組正規表達式支援不同的情境,這裡會使用預設的
const PROTOCOL_REGEX = /^[\s\w\0+.-]{2,}:([/\\]{2})?/
[\s\w\0+.-]{2,}:
literal colon 前至少兩個[]
集合內的任意 character([/\\]{2})?
出現兩次/
或\
為 optional
const PROTOCOL_STRICT_REGEX = /^[\s\w\0+.-]{2,}:([/\\]{1,2})/;
// examples of matches
// "tel:/"
// "http:\"
// "https://"
// "file:///home/user"
const PROTOCOL_REGEX = /^[\s\w\0+.-]{2,}:([/\\]{2})?/;
// examples of matches
// "tel:"
// "http:/"
// "https://"
// "mailto:foo@bar.com"
const PROTOCOL_RELATIVE_REGEX = /^([/\\]\s*){2,}[^/\\]/;
// examples of matches
// "//test.com"
// "///test.com"
// "/\\localhost//"
interface HasProtocolOptions {
acceptRelative?: boolean;
strict?: boolean;
}
function hasProtocol(
inputString: string,
opts: HasProtocolOptions = {},
): boolean {
if (opts.strict) {
return PROTOCOL_STRICT_REGEX.test(inputString);
}
return (
PROTOCOL_REGEX.test(inputString) ||
(opts.acceptRelative ? PROTOCOL_RELATIVE_REGEX.test(inputString) : false)
);
}
withoutTrailingSlash
這裡的目的是:清除 baseURL
結尾的 /
,避免兩個路徑進行字串連結後,出現非預期的重複
因為是 baseURL
,這裡不考慮包含 query, fragment(hash) 的情境
function withoutTrailingSlash(
input = "",
respectQueryAndFragment?: boolean,
): string {
if (!respectQueryAndFragment) {
return (hasTrailingSlash(input) ? input.slice(0, -1) : input) || "/";
}
if (!hasTrailingSlash(input, true)) {
return input || "/";
}
let path = input;
let fragment = "";
const fragmentIndex = input.indexOf("#");
if (fragmentIndex >= 0) {
path = input.slice(0, fragmentIndex);
fragment = input.slice(fragmentIndex);
}
const [s0, ...s] = path.split("?");
const cleanPath = s0.endsWith("/") ? s0.slice(0, -1) : s0;
return (
(cleanPath || "/") + (s.length > 0 ? `?${s.join("?")}` : "") + fragment
);
}
function hasTrailingSlash(
input = "",
respectQueryAndFragment?: boolean,
): boolean {
if (!respectQueryAndFragment) {
return input.endsWith("/");
}
const TRAILING_SLASH_RE = /\/$|\/\?|\/#/;
return TRAILING_SLASH_RE.test(input);
}
joinURL
目的是:確保不要有多餘空格或重複 /
先過濾空字串或 /
的參數
function joinURL(base: string, ...input: string[]): string {
let url = base || "";
const segments = input.filter(isNonEmptyURL);
for (const segment of segments) {
// skip
}
return url;
}
function isNonEmptyURL(url: string) {
return url && url !== "/";
}
進行字串連結前:
- 統一清除所有參數開頭的
./
或/
- 確保
url
結尾帶有/
function joinURL(base: string, ...input: string[]): string {
let url = base || "";
const segments = input.filter(isNonEmptyURL);
for (const segment of segments) {
if (url) {
const JOIN_LEADING_SLASH_RE = /^\.?\//;
const _segment = segment.replace(JOIN_LEADING_SLASH_RE, "");
url = withTrailingSlash(url) + _segment;
} else {
url = segment;
}
}
return url;
}
最後附上 withTrailingSlash
的程式碼
這裡不考慮包含 query, fragment(hash) 的情境,所以會進到 5-7 行的 block
function withTrailingSlash(
input = "",
respectQueryAndFragment?: boolean,
): string {
if (!respectQueryAndFragment) {
return input.endsWith("/") ? input : input + "/";
}
if (hasTrailingSlash(input, true)) {
return input || "/";
}
let path = input;
let fragment = "";
const fragmentIndex = input.indexOf("#");
if (fragmentIndex >= 0) {
path = input.slice(0, fragmentIndex);
fragment = input.slice(fragmentIndex);
if (!path) {
return fragment;
}
}
const [s0, ...s] = path.split("?");
return s0 + "/" + (s.length > 0 ? `?${s.join("?")}` : "") + fragment;
}