§Play WS を使用した REST API の呼び出し
Play アプリケーション内から他の HTTP サービスを呼び出したい場合があります。Play は、WS(「WebService」)ライブラリを介してこれをサポートしています。これは、WSClient インスタンスを通じて非同期 HTTP 呼び出しを行う方法を提供します。
WSClient の使用には、リクエストの作成とレスポンスの処理という 2 つの重要な部分があります。最初に GET と POST の HTTP リクエストを作成する方法について説明し、次に WSClient からのレスポンスを処理する方法を示します。最後に、いくつかの一般的なユースケースについて説明します。
注:Play 2.6 では、Play WS は 2 つに分割されました。Play に依存しない基盤となるスタンドアロン クライアントと、Play 固有のクラスを使用する上位のラッパーがあります。さらに、AsyncHttpClient と Netty のシェーディングされたバージョンが Play WS で使用されるようになり、ライブラリの競合が最小限に抑えられます。これは主に、Play の HTTP エンジンが異なるバージョンの Netty を使用できるようにするためです。詳細については、2.6 移行ガイドを参照してください。
§プロジェクトへの WS の追加
WSClient を使用するには、最初に build.sbt ファイルに ws を追加します
libraryDependencies += ws
§Play WS での HTTP キャッシュの有効化
Play WS は HTTP キャッシュをサポートしていますが、この機能を有効にするには JSR-107 キャッシュ実装が必要です。ehcache を追加できます
libraryDependencies += ehcache
または、Caffeine などの別の JSR-107 互換キャッシュを使用することもできます。
ライブラリの依存関係を取得したら、WS キャッシュ設定ページに示されているように HTTP キャッシュを有効にします。
HTTP キャッシュを使用すると、バックエンド REST サービスへの繰り返しのリクエストを節約できます。stale-on-error や stale-while-revalidate などの回復機能と組み合わせると特に役立ちます。
§リクエストの作成
WS を使用したいコンポーネントは、WSClient への依存関係を宣言する必要があります
import javax.inject.Inject
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.scaladsl._
import org.apache.pekko.stream.SystemMaterializer
import org.apache.pekko.util.ByteString
import play.api.http.HttpEntity
import play.api.libs.ws._
import play.api.mvc._
class Application @Inject() (ws: WSClient, val controllerComponents: ControllerComponents) extends BaseController {}
WSClient インスタンスを ws と呼びました。以下のすべての例では、この名前が使用されていると想定しています。
HTTP リクエストを作成するには、URL を指定するために ws.url() から始めます。
val request: WSRequest = ws.url(url)
これは、ヘッダーの設定など、さまざまな HTTP オプションを指定するために使用できる WSRequest を返します。複雑なリクエストを作成するために、呼び出しを連鎖させることができます。
val complexRequest: WSRequest =
request
.addHttpHeaders("Accept" -> "application/json")
.addQueryStringParameters("search" -> "play")
.withRequestTimeout(10000.millis)
最後に、使用したい HTTP メソッドに対応するメソッドを呼び出します。これによりチェーンが終了し、WSRequest で構築されたリクエストに定義されているすべてのオプションが使用されます。
val futureResponse: Future[WSResponse] = complexRequest.get()
これは Future[WSResponse] を返します。ここで、Response にはサーバーから返されたデータが含まれています。
java.util.URL.equals()の呼び出しなど、あらゆる種類の DNS 作業を含むブロッキング作業を行う場合は、スレッドプールで説明されているように、できればCustomExecutionContextを介してカスタム実行コンテキストを使用する必要があります。プールサイズを調整して、障害を考慮した十分な安全マージンを残す必要があります。信頼性の低いネットワークを呼び出す場合は、
Futures.timeoutと サーキット ブレーカー(Failsafe など)の使用を検討してください。
§認証付きリクエスト
HTTP 認証を使用する必要がある場合は、ユーザー名、パスワード、および AuthScheme を使用して、ビルダーで指定できます。AuthScheme の有効なケース オブジェクトは、BASIC、DIGEST、KERBEROS、NTLM、および SPNEGO です。
ws.url(url).withAuth(user, password, WSAuthScheme.BASIC).get()§リダイレクトをフォローするリクエスト
HTTP 呼び出しが 302 または 301 リダイレクトになった場合、別の呼び出しを行わなくてもリダイレクトを自動的にフォローできます。
ws.url(url).withFollowRedirects(true).get()§クエリ パラメーター付きリクエスト
パラメーターは、一連のキーと値のタプルとして指定できます。パラメーターを追加するには addQueryStringParameters を使用し、すべてのクエリ文字列パラメーターを上書きするには withQueryStringParameters を使用します。
ws.url(url).addQueryStringParameters("paramKey" -> "paramValue").get()§追加ヘッダー付きリクエスト
ヘッダーは、一連のキーと値のタプルとして指定できます。追加のヘッダーを追加するには addHttpHeaders を使用し、すべてのヘッダーを上書きするには withHttpHeaders を使用します。
ws.url(url).addHttpHeaders("headerKey" -> "headerValue").get()
特定の形式でプレーン テキストを送信する場合は、コンテンツ タイプを明示的に定義することをお勧めします。
ws.url(url)
.addHttpHeaders("Content-Type" -> "application/xml")
.post(xmlString)§Cookie 付きリクエスト
DefaultWSCookie を使用するか、play.api.mvc.Cookie を渡すことで、リクエストに Cookie を追加できます。Cookie を追加するには addCookies を使用し、すべての Cookie を上書きするには withCookies を使用します。
ws.url(url).addCookies(DefaultWSCookie("cookieName", "cookieValue")).get()§仮想ホスト付きリクエスト
仮想ホストは文字列として指定できます。
ws.url(url).withVirtualHost("192.168.1.1").get()§タイムアウト付きリクエスト
リクエスト タイムアウトを指定する場合は、withRequestTimeout を使用して値を設定できます。Duration.Inf を渡すことで、無限のタイムアウトを設定できます。
ws.url(url).withRequestTimeout(5000.millis).get()§フォーム データの送信
URL フォーム エンコードされたデータを送信するには、Map[String, Seq[String]] を post に渡す必要があります。
本文が空の場合、post メソッドに play.api.libs.ws.EmptyBody を渡す必要があります。
ws.url(url).post(Map("key" -> Seq("value")))§multipart/form-data の送信
multipart-form-encoded データを送信するには、Source[play.api.mvc.MultipartFormData.Part[Source[ByteString, Any]], Any] を post に渡す必要があります。
ws.url(url).post(Source.single(DataPart("key", "value")))
ファイルをアップロードするには、play.api.mvc.MultipartFormData.FilePart[Source[ByteString, Any]] を Source に渡す必要があります
ws.url(url)
.post(
Source(
FilePart("hello", "hello.txt", Option("text/plain"), FileIO.fromPath(tmpFile.toPath)) :: DataPart(
"key",
"value"
) :: List()
)
)§JSON データの送信
JSON データを送信する最も簡単な方法は、JSON ライブラリを使用することです。
import play.api.libs.json._
val data = Json.obj(
"key1" -> "value1",
"key2" -> "value2"
)
val futureResponse: Future[WSResponse] = ws.url(url).post(data)§XML データの送信
XML データを送信する最も簡単な方法は、XML リテラルを使用することです。XML リテラルは便利ですが、それほど高速ではありません。効率を上げるために、XML ビュー テンプレートまたは JAXB ライブラリの使用を検討してください。
val data = <person>
<name>Steve</name>
<age>23</age>
</person>
val futureResponse: Future[WSResponse] = ws.url(url).post(data)§ストリーミング データの送信
Akka ストリームを使用して、リクエスト本文でデータをストリーミングすることもできます。
たとえば、大きな画像を返すデータベース クエリを実行し、そのデータをさらに処理するために別のエンドポイントに転送したいとします。データベースからデータを受信するときにデータを送信できれば、待ち時間が短縮され、大量のデータをメモリにロードすることによる問題も回避できます。データベース アクセス ライブラリが リアクティブ ストリームをサポートしている場合(たとえば、Slick はサポートしています)、記述された動作を実装する方法の例を次に示します
val wsResponse: Future[WSResponse] = ws
.url(url)
.withBody(largeImageFromDB)
.execute("PUT")
上記のコード スニペットの largeImageFromDB は Source[ByteString, _] です。
§リクエスト フィルター
リクエスト フィルターを追加することで、WSRequest に対して追加の処理を実行できます。リクエスト フィルターは、play.api.libs.ws.WSRequestFilter 特性を拡張し、request.withRequestFilter(filter) を使用してリクエストに追加することで追加されます。
リクエストを cURL 形式で SLF4J に記録するサンプル リクエスト フィルターが play.api.libs.ws.ahc.AhcCurlRequestLogger に追加されました。
ws.url(s"https://:$serverPort")
.withRequestFilter(AhcCurlRequestLogger())
.put(Map("key" -> Seq("value")))
は出力します
curl \
--verbose \
--request PUT \
--header 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
--data 'key=value' \
https://:19001/
§レスポンスの処理
Response の操作は、Future 内でマッピングすることで簡単に行えます。
以下に示す例には、簡潔にするためにここで一度だけ示される共通の依存関係がいくつかあります。
Future に対して操作が行われるたびに、暗黙的な実行コンテキストが利用可能でなければなりません。これは、future へのコールバックを実行するスレッド プールを宣言します。クラスのコンストラクターで ExecutionContext に追加の依存関係を宣言することで、DI されたクラスにデフォルトの Play 実行コンテキストを注入できます
class PersonService @Inject() (ec: ExecutionContext) {
// ...
}
この例では、シリアル化と逆シリアル化に次のケース クラスも使用します
case class Person(name: String, age: Int)
WSResponse は play.api.libs.ws.WSBodyReadables トレイトを拡張しており、Play JSON と Scala XML 変換のための型クラスを含んでいます。レスポンスを独自の型に変換したり、異なる JSON や XML エンコーディングを使用したりする場合、独自の型クラスを作成することもできます。
§JSON としてのレスポンスの処理
response.json を呼び出すことで、レスポンスを JSON オブジェクト として処理できます。
val futureResult: Future[String] =
ws.url(url).get().map { response => (response.json \ "person" \ "name").as[String] }
JSON ライブラリには、暗黙の Reads[T] をクラスに直接マッピングする 便利な機能 があります。
import play.api.libs.json._
implicit val personReads: Reads[Person] = Json.reads[Person]
val futureResult: Future[JsResult[Person]] =
ws.url(url).get().map { response => (response.json \ "person").validate[Person] }§XML としてのレスポンスの処理
response.xml を呼び出すことで、レスポンスを XML リテラル として処理できます。
val futureResult: Future[scala.xml.NodeSeq] = ws.url(url).get().map { response => response.xml \ "message" }§大規模なレスポンスの処理
get()、post()、または execute() を呼び出すと、レスポンスが利用可能になる前にレスポンスのボディがメモリにロードされます。数ギガバイトの大規模なファイルをダウンロードする場合、これにより望ましくないガベージコレクションが発生したり、メモリ不足エラーが発生したりする可能性があります。
WS では、Pekko Streams の Sink を使用して、レスポンスのボディを段階的に消費できます。WSRequest の stream() メソッドは、Source[ByteString, _] を返す bodyAsSource メソッドを含むストリーミング WSResponse を返します。
注: 2.5.x では、
request.stream()呼び出しに対してStreamedResponseが返されていました。2.6.x では、標準のWSResponseが返され、bodyAsSource()メソッドを使用して Source を返す必要があります。
folding Sink を使用してレスポンスから返されるバイト数をカウントする簡単な例を次に示します。
// Make the request
val futureResponse: Future[WSResponse] =
ws.url(url).withMethod("GET").stream()
val bytesReturned: Future[Long] = futureResponse.flatMap { res =>
// Count the number of bytes returned
res.bodyAsSource.runWith(Sink.fold[Long, ByteString](0L) { (total, bytes) => total + bytes.length })
}
あるいは、ボディを別の場所にストリーミングすることもできます。たとえば、ファイルなどです。
// Make the request
val futureResponse: Future[WSResponse] =
ws.url(url).withMethod("GET").stream()
val downloadedFile: Future[File] = futureResponse.flatMap { res =>
val outputStream = java.nio.file.Files.newOutputStream(file.toPath)
// The sink that writes to the output stream
val sink = Sink.foreach[ByteString] { bytes => outputStream.write(bytes.toArray) }
// materialize and run the stream
res.bodyAsSource
.runWith(sink)
.andThen {
case result =>
// Close the output stream whether there was an error or not
outputStream.close()
// Get the result or rethrow the error
result.get
}
.map(_ => file)
}
レスポンスボディのもう1つの一般的な宛先は、コントローラーの Action からストリーミングして返すことです。
def downloadFile = Action.async {
// Make the request
ws.url(url).withMethod("GET").stream().map { response =>
// Check that the response was successful
if (response.status == 200) {
// Get the content type
val contentType = response.headers
.get("Content-Type")
.flatMap(_.headOption)
.getOrElse("application/octet-stream")
// If there's a content length, send that, otherwise return the body chunked
response.headers.get("Content-Length") match {
case Some(Seq(length: String)) =>
Ok.sendEntity(HttpEntity.Streamed(response.bodyAsSource, Some(length.toLong), Some(contentType)))
case _ =>
Ok.chunked(response.bodyAsSource).as(contentType)
}
} else {
BadGateway
}
}
}
stream() を呼び出す前に、リクエストで withMethod を呼び出して使用する HTTP メソッドを設定する必要があることに気付いたかもしれません。GET の代わりに PUT を使用する別の例を次に示します。
val futureResponse: Future[WSResponse] =
ws.url(url).withMethod("PUT").withBody("some body").stream()
もちろん、他の有効な HTTP 動詞も使用できます。
§一般的なパターンとユースケース
§WSClient 呼び出しの連結
for 内包表記を使用すると、信頼できる環境で WSClient 呼び出しを連結するのに適しています。発生する可能性のあるエラーを処理するために、for 内包表記と Future.recover を一緒に使用する必要があります。
val futureResponse: Future[WSResponse] = for {
responseOne <- ws.url(urlOne).get()
responseTwo <- ws.url(responseOne.body).get()
responseThree <- ws.url(responseTwo.body).get()
} yield responseThree
futureResponse.recover {
case e: Exception =>
val exceptionData = Map("error" -> Seq(e.getMessage))
ws.url(exceptionUrl).post(exceptionData)
}§コントローラーでの使用
コントローラーからリクエストを行う場合、レスポンスを Future[Result] にマッピングできます。非同期結果の処理 で説明されているように、これは Play の Action.async アクションビルダーと組み合わせて使用できます。
def wsAction = Action.async {
ws.url(url).get().map { response => Ok(response.body) }
}
status(wsAction(FakeRequest())) must_== OK§Future タイムアウトでの WSClient の使用
WS 呼び出しのチェーンが時間内に完了しない場合、結果をタイムアウトブロックでラップすると便利です。タイムアウトブロックは、チェーンが時間内に完了しない場合に失敗した Future を返します。これは、単一のリクエストにのみ適用される withRequestTimeout を使用するよりも汎用的です。これを行う最良の方法は、Play の ノンブロッキングタイムアウト機能 を使用して、play.api.libs.concurrent.Futures を使用することです。
// Adds withTimeout as type enrichment on Future[WSResponse]
import play.api.libs.concurrent.Futures._
val result: Future[Result] =
ws.url(url)
.get()
.withTimeout(1.second)
.flatMap { response =>
// val url2 = response.json \ "url"
ws.url(url2).get().map { response2 => Ok(response.body) }
}
.recover {
case e: scala.concurrent.TimeoutException =>
GatewayTimeout
}§コンパイル時依存性注入
コンパイル時依存性注入を使用している場合、アプリケーションのコンポーネント でトレイト AhcWSComponents を使用することで、WSClient インスタンスにアクセスできます。
§WSClient の直接作成
上記のように、依存性注入を使用して WSClient インスタンスを取得することをお勧めします。依存性注入によって作成された WSClient インスタンスは、アプリケーションの起動時に自動的に作成され、アプリケーションの停止時にクリーンアップされるため、使い方が簡単です。
ただし、必要に応じて、コードから WSClient を直接インスタンス化し、これを使用してリクエストを作成したり、基盤となる AsyncHttpClient オプションを設定したりできます。
WSClient を手動で作成する場合は、使い終わったら必ず
client.close()を呼び出してクリーンアップする必要があります。 各クライアントは独自の thread pool を作成します。クライアントを閉じなかった場合、またはクライアントを過剰に作成した場合、スレッドまたはファイルハンドルが不足し、「新しいネイティブスレッドを作成できません」または「開いているファイルが多すぎます」などのエラーが発生します。基盤となるリソースが消費されるためです。
play.api.libs.ws.ahc.AhcWSClient インスタンスを直接作成するには、pekko.stream.Materializer のインスタンスが必要です。 通常は、依存性注入を使用してこれをサービスに注入します。
import play.api.libs.ws.ahc._
// usually injected through @Inject()(implicit mat: Materializer)
implicit val materializer: Materializer = app.materializer
val wsClient = AhcWSClient()
クライアントを直接作成すると、AsyncHttpClient と Netty の設定レイヤーでも設定を変更できます。
import play.api._
import play.api.libs.ws._
import play.api.libs.ws.ahc._
val configuration = Configuration("ws.followRedirects" -> true).withFallback(Configuration.reference)
// If running in Play, environment should be injected
val environment = Environment(new File("."), this.getClass.getClassLoader, Mode.Prod)
val wsConfig = AhcWSClientConfigFactory.forConfig(configuration.underlying, environment.classLoader)
val mat = app.materializer
val wsClient: WSClient = AhcWSClient(wsConfig)(mat)
また、play.api.test.WsTestClient.withTestClient を使用して、関数テストで WSClient のインスタンスを作成することもできます。 詳細については、ScalaTestingWebServiceClients を参照してください。
または、実行中の Play アプリケーションをまったく使用せずに、WSClient を完全にスタンドアロンで実行することもできます。
import scala.concurrent.Future
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import org.apache.pekko.stream.SystemMaterializer
import play.api.libs.ws._
import play.api.libs.ws.ahc.AhcWSClient
import play.api.libs.ws.ahc.StandaloneAhcWSClient
import play.shaded.ahc.org.asynchttpclient.AsyncHttpClient
import play.shaded.ahc.org.asynchttpclient.AsyncHttpClientConfig
import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClient
import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClientConfig
object Main {
import scala.concurrent.ExecutionContext.Implicits._
def main(args: Array[String]): Unit = {
implicit val system = ActorSystem()
val asyncHttpClientConfig = new DefaultAsyncHttpClientConfig.Builder()
.setMaxRequestRetry(0)
.setShutdownQuietPeriod(0)
.setShutdownTimeout(0)
.build
val asyncHttpClient = new DefaultAsyncHttpClient(asyncHttpClientConfig)
implicit val materializer = SystemMaterializer(system).materializer
val wsClient: WSClient = new AhcWSClient(new StandaloneAhcWSClient(asyncHttpClient))
call(wsClient)
.andThen { case _ => wsClient.close() }
.andThen { case _ => system.terminate() }
}
def call(wsClient: WSClient): Future[Unit] = {
wsClient.url("https://www.google.com").get().map { response =>
val statusText: String = response.statusText
println(s"Got a response $statusText")
}
}
}
これは、設定からアクセスできない特定の HTTP クライアントオプションがある場合に役立ちます。
繰り返しますが、カスタムクライアントの作業が完了したら、クライアントを必ず閉じる必要があります。
wsClient.close()
理想的には、すべてのリクエストが完了した後にクライアントを閉じる必要があります。WSClient ロジックは非同期であり、多くの ARM ソリューションはシングルスレッドの同期ソリューション用に設計されている可能性があるため、自動リソース管理パターンを使用してクライアントを閉じる場合は注意が必要です。
§スタンドアロン WS
Play のコンテキスト外で WS を呼び出したい場合は、Play ライブラリに依存しないスタンドアロンバージョンの Play WS を使用できます。 これを行うには、プロジェクトに play-ahc-ws-standalone を追加します。
libraryDependencies += "org.playframework" %% "play-ahc-ws-standalone" % playWSStandalone
詳細については、https://github.com/playframework/play-ws および 2.6 移行ガイド を参照してください。
§カスタム BodyReadables と BodyWritables
Play WS には、play.api.libs.ws.WSBodyWritables の形式でボディの豊富な型サポートが付属しています。これには、WSRequest のボディで JsValue や XML などの入力を ByteString や Source[ByteString, _] に変換するための型クラスが含まれています。また、play.api.libs.ws.WSBodyReadables には、WSResponse のボディを ByteString や Source[ByteString, _] から読み取り、JsValue や XML などの適切な型を返す型クラスが集約されています。 これらの型クラスは、ws パッケージをインポートすると自動的にスコープ内に入りますが、カスタム型を作成することもできます。 これは、カスタムライブラリを使用する場合、つまり STaX API を介して XML をストリーミングしたり、Argonaut や Circe などの別の JSON ライブラリを使用したりする場合に特に役立ちます。
§カスタム Readable の作成
レスポンスボディにアクセスすることで、カスタムの readable を作成できます。
trait URLBodyReadables {
implicit val urlBodyReadable: BodyReadable[URL] = BodyReadable[java.net.URL] { response =>
import play.shaded.ahc.org.asynchttpclient.{ Response => AHCResponse }
val ahcResponse = response.underlying[AHCResponse]
val s = ahcResponse.getResponseBody
java.net.URI.create(s).toURL
}
}§カスタム BodyWritable の作成
BodyWritable と InMemoryBody を使用して、リクエストにカスタムの body writable を作成できます。ストリーミングでカスタムの body writable を指定するには、SourceBody を使用します。
trait URLBodyWritables {
implicit val urlBodyWritable: BodyWritable[URL] = BodyWritable[java.net.URL](
{ url =>
val s = url.toURI.toString
val byteString = ByteString.fromString(s)
InMemoryBody(byteString)
},
"text/plain"
)
}§AsyncHttpClient へのアクセス
WSClient から基盤となる AsyncHttpClient にアクセスできます。
import play.shaded.ahc.org.asynchttpclient.AsyncHttpClient
val client: AsyncHttpClient = ws.underlying§WSClient の設定
application.conf で次のプロパティを使用して、WSClient を設定します。
play.ws.followRedirects: 301 および 302 リダイレクトに従うようにクライアントを設定します *(デフォルトは **true** です)*。play.ws.useProxyProperties: JVM システムの HTTP プロキシ設定(http.proxyHost、http.proxyPort)を使用します *(デフォルトは **true** です)*。play.ws.useragent: User-Agent ヘッダーフィールドを設定します。play.ws.compressionEnabled: gzip/deflater エンコーディングを使用するには、true に設定します *(デフォルトは **false** です)*。
§SSL を使用した WSClient の設定
SSL/TLS(HTTPS)を介した HTTP での使用に WS を設定する方法については、WS SSL の設定 を参照してください。
§キャッシングを使用した WS の設定
HTTP キャッシングで使用するために WS を設定する方法については、WS キャッシュの設定 を参照してください。
§タイムアウトの設定
WSClient には 3 つの異なるタイムアウトがあります. タイムアウトに達すると、WSClient リクエストが中断されます。
play.ws.timeout.connection: リモートホストに接続するときの最大待機時間 *(デフォルトは **120 秒** です)*。play.ws.timeout.idle: リクエストがアイドル状態を維持できる最大時間(接続は確立されていますが、 المزيدのデータを待機しています) *(デフォルトは **120 秒** です)*。play.ws.timeout.request: リクエストの許容時間(リモートホストがまだデータを送信していても中断されます) *(デフォルトは **120 秒** です)*。
リクエストタイムアウトは、withRequestTimeout() を使用して特定の接続に対してオーバーライドできます(「リクエストの作成」セクションを参照)。
§AsyncHttpClientConfig の設定
基盤となる AsyncHttpClientConfig で、次の詳細設定を行うことができます。
詳細については、AsyncHttpClientConfig のドキュメント を参照してください。
play.ws.ahc.keepAliveplay.ws.ahc.maxConnectionsPerHostplay.ws.ahc.maxConnectionsTotalplay.ws.ahc.maxConnectionLifetimeplay.ws.ahc.idleConnectionInPoolTimeoutplay.ws.ahc.maxNumberOfRedirectsplay.ws.ahc.maxRequestRetryplay.ws.ahc.disableUrlEncoding
このドキュメントに誤りを見つけましたか? このページのソースコードは こちら にあります。ドキュメントガイドライン を読んだ後、プルリクエストを送信してください。 質問やアドバイスがありますか? コミュニティフォーラム にアクセスして、コミュニティとの会話を始めてください。