Contents

如何避免程式組 URL 出錯

前正子專案因為設定檔有人網址會多 / 導致程式一些狀況,公司有人有特別寫組語法方法,這邊我思考正常內建方法可不可以解決這個問題,怎麼沒有人寫好 Library 分享給別人使用?探討有什麼更好方式去解這個問題。

心智圖

mindmap (程式組 Url 方法) 操作 Url 方法 .Net UriBuilder 物件 Java Uri 物件 JavaScript URL 物件 設定 QueryString 方法 .Net NameValueCollection Java MultiValuedMap 非原生 JavaScript URLSearchParams

.Net 實作方法

使用 UriBuilder

UriBuilder 是一個用於建立和操作 URI 的類別,適用於需要動態生成或修改 URI 的場景。它的用途包括構建 API 請求、處理用戶輸入的 URL、以及在應用程序中動態生成鏈接。相較於單純使用字串來紀錄 URI,它有以下的優點:

  1. 安全性:UriBuilder 會自動處理 URI 中的特殊字元和編碼問題,避免了手動操作可能產生的錯誤。
1
2
3
4
5
6
7
8
9
UriBuilder uriBuilder = new UriBuilder
{
    Scheme = "https",
    Host = "example.com",
    Path = "/api/values",
    Query = "name=" + Uri.EscapeDataString("John Doe")
};

Uri uri = uriBuilder.Uri;
  1. 易於操作:UriBuilder 提供了許多方法和屬性來操作 URI 的各個部分(例如 scheme、host、path、query 等),比起手動解析和組裝字串來得方便。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
UriBuilder uriBuilder = new UriBuilder
{
	Scheme = "https",
	Host = "example.com",
	Path = "/api/values",
	Query = "name=" + Uri.EscapeDataString("John Doe")
};

uriBuilder.Dump();
Uri uri = uriBuilder.Uri;
uri.ToString().Dump();

參考資料

可以看到 Query 的 ?name=John%20Doe,在 ToString() 會自動進行 URL 解碼,通常用於使用者輸入的 encodeUrl,不需要自己處理。
https://gist.github.com/assets/75846914/1e96f789-8a56-4b34-be65-b58406ee81b6
可以看到 Query 的?name=John%20Doe,在 ToString() 會自動做 urldecode 動作,通常用於使用者輸入encodeUrl 可以做這個動作,不需要自己做這個處理。

  1. 可讀性:使用 UriBuilder 可以讓程式碼更具可讀性,因為它明確地表達了你正在操作 URI,而不僅僅是操作字串。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
UriBuilder uriBuilder = new UriBuilder
{
    Scheme = "https",
    Host = "example.com",
    Path = "/api/values"
};

Uri uri1 = uriBuilder.Uri;

uriBuilder.Query = "id=123";
Uri uri2 = uriBuilder.Uri;

uriBuilder.Query = "id=456";
Uri uri3 = uriBuilder.Uri;
  1. 可重用性:UriBuilder 可以重複使用來建立具有相同基礎的多個 URI,這在需要建立許多相似 URI 的情況下非常有用。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
UriBuilder uriBuilder = new UriBuilder
{
    Scheme = "https",
    Host = "example.com",
    Path = "/api/values"
};

Uri uri1 = uriBuilder.Uri;

uriBuilder.Query = "id=123";
Uri uri2 = uriBuilder.Uri;

uriBuilder.Query = "id=456";
Uri uri3 = uriBuilder.Uri;
Info

UriBuilder 和 Uri 使用方是哪裡不一樣?

UriBuilder 和 Uri 兩者都是用於處理 URI 的,但它們的使用方式和目的有所不同。

Uri 是一個不可變的類別,代表一個 Uniform Resource Identifier (URI)。一旦 Uri 物件被建立,其內容就不能更改。Uri 物件通常用於存儲和傳遞 URI。

1
Uri uri = new Uri("https://example.com/api/values?id=123");

UriBuilder 則是一個可變的類別,用於建立和修改 URI。你可以使用 UriBuilder 的各種屬性(例如 Scheme、Host、Path、Query 等)來設定 URI 的各個部分,然後使用 Uri 屬性或 ToString 方法來獲取最終的 URI。

1
2
3
4
5
6
7
8
9
UriBuilder uriBuilder = new UriBuilder
{
    Scheme = "https",
    Host = "example.com",
    Path = "/api/values",
    Query = "id=123"
};

Uri uri = uriBuilder.Uri;  // 從 UriBuilder 獲取 Uri

總的來說,如果你需要建立或修改 URI,則應使用 UriBuilder。如果你只需要存儲或傳遞 URI,則應使用 Uri

Info

那我通常要改 Uri 資料要怎麼做?

由於 Uri 是不可變的,所以你不能直接修改 Uri 物件的內容。如果你需要修改 URI,你應該使用 UriBuilder

以下是一個範例,展示如何使用 UriBuilder 修改 URI:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Uri originalUri = new Uri("https://example.com/api/values?id=123");

// 使用原始的 Uri 建立 UriBuilder
UriBuilder uriBuilder = new UriBuilder(originalUri);

// 修改 UriBuilder 的內容
uriBuilder.Path = "/api/items";
uriBuilder.Query = "id=456";

// 從 UriBuilder 獲取新的 Uri
Uri newUri = uriBuilder.Uri;

在這個範例中,我們首先使用原始的 Uri 建立了一個 UriBuilder。然後,我們修改了 UriBuilder 的 Path 和 Query。最後,我們從 UriBuilder 獲取了新的 Uri。新的 Uri 是 “https://example.com/api/items?id=456"。

解決 URL 多餘的 / 問題 "

可以用 Path.Combine 解決這個問題。

組 URL 和 Query 使用 UriBuilderNameValueCollection 來解決這個問題。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
UriBuilder uriBuilder = new UriBuilder("http", "example.com");
NameValueCollection query = HttpUtility.ParseQueryString(string.Empty);

query["param1"] = "value1";
query["param2"] = "value2";

string path1 = "/path";
string path2 = "/to/resource";
uriBuilder.Path = Path.Combine(path1, path2);
uriBuilder.Query = query.ToString();

Console.WriteLine(uriBuilder.ToString());
// path2 前面根目錄會把 path1 路徑去掉,所以串接時候不要把 path2 前面加 /
//  http://localhost/to/resource

LinqPad 範例

Info

HttpUtility.ParseQueryString(string.Empty);NameValueCollection query = new NameValueCollection(); 差異?

NameValueCollection query = HttpUtility.ParseQueryString(string.Empty);NameValueCollection query = new NameValueCollection(); 兩者的主要差別在於他們的初始化方式。

HttpUtility.ParseQueryString(string.Empty) 會返回一個 NameValueCollection,並且這個集合的 ToString 方法被覆蓋以返回一個 URL 編碼的查詢字符串。這對於創建查詢字符串非常有用。
詳細可以參考 HttpQSCollection 這個類別。

1
2
3
4
NameValueCollection query = HttpUtility.ParseQueryString(string.Empty);
query["param1"] = "value1";
query["param2"] = "value2";
Console.WriteLine(query.ToString());  // 輸出 "param1=value1&param2=value2"

另一方面,創建一個新的 NameValueCollection,但是它的 ToString 方法並未被覆蓋,所以它只會返回類型名稱。

1
2
3
4
NameValueCollection query = new NameValueCollection();
query["param1"] = "value1";
query["param2"] = "value2";
Console.WriteLine(query.ToString());  // 輸出 "System.Collections.Specialized.NameValueCollection"

因此,如果你需要創建一個查詢字符串,使用 HttpUtility.ParseQueryString(string.Empty) 會更方便。
{{</admonition>}}

Javascript

1
2
3
4
5
const url = 'http://localhost//example/author/admin///';
const parts = url.split('/');
const cleanedParts = parts.filter((part, index) => index === 0 || part !== '');
const cleanUrl = cleanedParts.join('/');
console.log(cleanUrl); // 輸出: http://localhost/example/author/admin/

new URL()

1
2
3
const url = new URL('https://example.com');
url.pathname = '/newpath';
console.log(url.href); // 輸出: https://example.com/newpath

QueryString

1
2
3
4
const myUrl = new URL('https://example.com');
myUrl.pathname = '/mypage';
myUrl.searchParams.append('city', 'Rome');
console.log(myUrl.href); // 輸出: https://example.com/mypage?city=Rome

{{<admonition type=“info”>}}
在 Node.js 中,你可以使用 path 模組的 join 方法來組合路徑,並自動處理多餘的 /。以下是一個範例:

1
2
3
4
const path = require('path');

let combinedPath = path.join('/foo', 'bar', '/baz/asdf', 'quux', '..');
console.log(combinedPath);  // 輸出: '/foo/bar/baz/asdf'

在這個範例中,path.join 方法將多個路徑片段組合成一個路徑,並自動處理多餘的 /..

請注意,這個方法是 Node.js 特有的,並不適用於在瀏覽器中運行的 JavaScript。在瀏覽器中,你可能需要自己寫函數來處理路徑的組合和正規化。

Info

在瀏覽器中運行的 JavaScript 沒有內建的路徑組合函數,但你可以自己寫一個簡單的函數來實現這個功能。以下是一個範例:

1
2
3
4
5
6
7
8
9
function combinePaths(...paths) {
    return paths
        .join('/')
        .replace(/\/+/g, '/')  // 將多個連續的 '/' 替換為單個 '/'
        .replace(/\/+$/, '');  // 移除結尾的 '/'
}

let combinedPath = combinePaths('/foo/', '/bar', '/baz/asdf/', '/quux', '..');
console.log(combinedPath);  // 輸出: '/foo/bar/baz/asdf/quux/..'

這個 combinePaths 函數接受任意數量的路徑片段,將它們組合成一個路徑,並自動處理多餘的 /

Java

相對 .Net 的 Path.Combine 方法,Java 有 Paths.get 方法可以使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Paths;

class HelloWorld {
    public static void main(String[] args) {
        String path = Paths.get("/", "//path", "/to", "///resource").toString();
         System.out.println(path.toString());
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import org.apache.http.client.utils.URIBuilder;

public class Main {
    public static void main(String[] args) throws Exception {
        URIBuilder uriBuilder = new URIBuilder();
        uriBuilder.setScheme("http")
                .setHost("example.com")
                .setPath("/path/to/resource")
                .setParameter("param1", "value1")
                .setParameter("param2", "value2");

        System.out.println(uriBuilder.build());
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<NameValuePair> params = new ArrayList<>();
        params.add(new BasicNameValuePair("param1", "value1"));
        params.add(new BasicNameValuePair("param2", "value2"));

        String queryString = URLEncodedUtils.format(params, Charset.forName("UTF-8"));

        System.out.println(queryString);
    }
}

Java 類似 .Net 的 NameValueCollection 物件

在 Java 中,對應 .Net 的 HTTP QueryString 可以用 NameValueCollection 去組合字串,但 Java 沒有原生方法。

在 Java 中,您可以使用 Multimap 來存儲具有重複鍵的值。以下是一些選項:

Apache Commons Collections:
使用 Multimap,它支持重複的鍵和值對。
您可以使用 org.apache.commons.collections4.MultiMap 來實現。
Java 8+:
使用 Map<K, List<V>>,其中每個鍵都對應到一個值列表。
您可以使用 computeIfAbsent 方法來簡化操作。
總之,如果您需要一個類似於 NameValueCollection 的物件,您可以使用 Java 中的 Multimap 或 Map<K, List<V>>。🌟