iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0

昨天我們看過了 Ktor 怎麼發送 POST 請求。不過,除了傳送各種不同 HTTP ACTION 以外,有時我們也會需要以其他形式傳送資料

下面我們來看看 Ktor 怎麼以 FORM DATA 的方式傳輸資料

Ktor 提供的函數是 submitForm

val client = HttpClient(CIO)
val response: HttpResponse = client.submitForm(
    url = "http://localhost:8080/signup",
    formParameters = parameters {
        append("username", "JetBrains")
        append("email", "example@jetbrains.com")
        append("password", "foobar")
        append("confirmation", "foobar")
    }

我們先看 formParameters 裡面的實作

/**
 * Builds a [Parameters] instance with the given [builder] function
 * @param builder specifies a function to build a map
 */
public fun parameters(builder: ParametersBuilder.() -> Unit): Parameters = Parameters.build(builder)
public interface Parameters : StringValues {
    public companion object {
        /**
         * Empty [Parameters] instance
         */
        public val Empty: Parameters = EmptyParameters

        /**
         * Builds a [Parameters] instance with the given [builder] function
         * @param builder specifies a function to build a map
         */
        public inline fun build(builder: ParametersBuilder.() -> Unit): Parameters =
            ParametersBuilder().apply(builder).build()
    }
}

ParametersBuilder 的實作則是

@Suppress("KDocMissingDocumentation")
public class ParametersImpl(
    values: Map<String, List<String>> = emptyMap()
) : Parameters, StringValuesImpl(true, values) {
    override fun toString(): String = "Parameters ${entries()}"
}

這邊繼承了 StringValuesImpl

@Suppress("KDocMissingDocumentation", "DEPRECATION")
public open class StringValuesBuilderImpl(
    final override val caseInsensitiveName: Boolean = false,
    size: Int = 8
) : StringValuesBuilder

這個物件功能比較多,今天我們只看有使用到的 append

override fun append(name: String, value: String) {
	validateValue(value)
	ensureListForKey(name).add(value)
}

通過 validateValue 檢查過後,就用 ensureListForKey 確認可以加上

@Suppress("DEPRECATION")
private fun ensureListForKey(name: String): MutableList<String> {
	return values[name] ?: mutableListOf<String>().also { validateName(name); values[name] = it }
}

然後利用 Kotlin Collection 的 add 加上對應的 value

我們最終就可以拿到 Parameters 物件了

看完 parameters 怎麼生成 Parameters 物件後

HttpClient.submitForm 實作如下

/**
 * Makes a request containing form parameters encoded using the `x-www-form-urlencoded` format.
 *
 * If [encodeInQuery] is set to `true`, form parameters are sent as URL parameters using the GET request.
 * Otherwise, form parameters are sent in a POST request body.
 *
 * Example: [Form parameters](https://ktor.io/docs/request.html#form_parameters).
 */
public suspend fun HttpClient.submitForm(
    url: String,
    formParameters: Parameters = Parameters.Empty,
    encodeInQuery: Boolean = false,
    block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse = submitForm(formParameters, encodeInQuery) {
    url(url)
    block()
}
/**
 * Makes a request containing form parameters encoded using the `x-www-form-urlencoded` format.
 *
 * If [encodeInQuery] is set to `true`, form parameters are sent as URL parameters using the GET request.
 * Otherwise, form parameters are sent in a POST request body.
 *
 * Example: [Form parameters](https://ktor.io/docs/request.html#form_parameters).
 */
public suspend inline fun HttpClient.submitForm(
    formParameters: Parameters = Parameters.Empty,
    encodeInQuery: Boolean = false,
    block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse = request {
    if (encodeInQuery) {
        method = HttpMethod.Get
        url.parameters.appendAll(formParameters)
    } else {
        method = HttpMethod.Post
        setBody(FormDataContent(formParameters))
    }

    block()
}

這邊我們又看到熟悉的 HttpClient.request

如果不是 encodeInQuery ,也就是都編在 Query String 裡面的話,那麼就需要設置 Body

這邊使用 FormDataContent

/**
 * [OutgoingContent] with for the `application/x-www-form-urlencoded` formatted request.
 *
 * Example: [Form parameters](https://ktor.io/docs/request.html#form_parameters).
 *
 * @param formData: data to send.
 */
public class FormDataContent(
    public val formData: Parameters
) : OutgoingContent.ByteArrayContent() {
    private val content = formData.formUrlEncode().toByteArray()

    override val contentLength: Long = content.size.toLong()
    override val contentType: ContentType = ContentType.Application.FormUrlEncoded.withCharset(Charsets.UTF_8)

    override fun bytes(): ByteArray = content
}

ContentType 的部分,這邊使用 FormUrlEncoded

public val FormUrlEncoded: ContentType =
	ContentType("application", "x-www-form-urlencoded")

加上 withCharset(Charsets.UTF_8)

/**
 * Creates a copy of `this` type with the added charset parameter with [charset] value.
 */
public fun ContentType.withCharset(charset: Charset): ContentType =
    withParameter("charset", charset.name)

這邊會將 formData 轉換變成 content

我們來看看 Parameters.formUrlEncode

/**
 * Encode form parameters
 */
public fun Parameters.formUrlEncode(): String = entries()
    .flatMap { e -> e.value.map { e.key to it } }
    .formUrlEncode()

formUrlEncode 則是

/**
 * Encode form parameters from a list of pairs
 */
public fun List<Pair<String, String?>>.formUrlEncode(): String = buildString { formUrlEncodeTo(this) }

formUrlEncodeTo

/**
 * Encode form parameters from a list of pairs to the specified [out] appendable
 */
public fun List<Pair<String, String?>>.formUrlEncodeTo(out: Appendable) {
    joinTo(out, "&") {
        val key = it.first.encodeURLParameter(spaceToPlus = true)
        if (it.second == null) {
            key
        } else {
            val value = it.second.toString().encodeURLParameterValue()
            "$key=$value"
        }
    }
}

到這邊,我們終於追到了實際的字串處理。

透過一連串的呼叫,formUrlEncodeTo 在最基礎負責將 Parameter 轉換成 Query String 的格式,我們就可以將 formParameters 轉換成 FormDataContent,並且放進 body 內,之後以 POST 的方式傳遞出去。

今天我們針對怎麼以 FORM DATA 的方式傳輸資料,這件事情我們先看到這邊,希望各位讀者都有收穫!


上一篇
Day 18:client.post 和 setBody
下一篇
Day 20:用 MultiPartFormDataContent 實作 multipart/form-data 請求
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言