新規投稿のお知らせを受信されたい方は、サブスクリプションをご登録ください:

Cloudflare Workers Announces Broad Language Support

2020-07-28

6分で読了
この投稿はEnglishでも表示されます。

We initially launched Cloudflare Workers with support for JavaScript and languages that compile to WebAssembly, such as Rust, C, and C++. Since then, Cloudflare and the community have improved the usability of Typescript on Workers. But we haven't talked much about the many other popular languages that compile to JavaScript. Today, we’re excited to announce support for Python, Scala, Kotlin, Reason and Dart.

You can build applications on Cloudflare Workers using your favorite language starting today.

Getting Started

Getting started is as simple as installing Wrangler, then running generate for the template for your chosen language: Python, Scala, Kotlin, Dart, or Reason. For Python, this looks like:

wrangler generate my-python-project https://github.com/cloudflare/python-worker-hello-world

Follow the installation instructions in the README inside the generated project directory, then run wrangler publish. You can see the output of your Worker at your workers.dev subdomain, e.g. https://my-python-project.cody.workers.dev/. You can sign up for a free Workers account if you don't have one yet.

That’s it. It is really easy to write in your favorite languages. But, this wouldn’t be a very compelling blog post if we left it at that. Now, I’ll shift the focus to how we added support for these languages and how you can add support for others.

How it all works under the hood

Language features are important. For instance, it's hard to give up the safety and expressiveness of pattern matching once you've used it. Familiar syntax matters to us as programmers.

You may also have existing code in your preferred language that you'd like to reuse. Just keep in mind that the advantages of running on V8 come with the limitation that if you use libraries that depend on native code or language-specific VM features, they may not translate to JavaScript. WebAssembly may be an option in that case. But for memory-managed languages you're usually better off compiling to JavaScript, at least until the story around garbage collection for WebAssembly stabilizes.

I'll walk through how the Worker language templates are made using a representative example of a dynamically typed language, Python, and a statically typed language, Scala. If you want to follow along, you'll need to have Wrangler installed and configured with your Workers account. If it's your first time using Workers it's a good idea to go through the quickstart.

Dynamically typed languages: Python

You can generate a starter "hello world" Python project for Workers by running

wrangler generate my-python-project https://github.com/cloudflare/python-worker-hello-world

Wrangler will create a my-python-project directory and helpfully remind you to configure your account_id in the wrangler.toml file inside it.  The README.md file in the directory links to instructions on setting up Transcrypt, the Python to JavaScript compiler we're using. If you already have Python 3.7 and virtualenv installed, this just requires running

cd my-python-project
virtualenv env
source env/bin/activate
pip install transcrypt
wrangler publish

The main requirement for compiling to JavaScript on Workers is the ability to produce a single js file that fits in our bundle size limit of 1MB. Transcrypt adds about 70k for its Python runtime in this case, which is well within that limit. But by default running Transcrypt on a Python file will produce multiple JS and source map files in a __target__ directory. Thankfully Wrangler has built in support for webpack. There's a webpack loader for Transcrypt, making it easy to produce a single file. See the webpack.config.js file for the setup.

The point of all this is to run some Python code, so let's take a look at index.py:

def handleRequest(request):
   return __new__(Response('Python Worker hello world!', {
       'headers' : { 'content-type' : 'text/plain' }
   }))

addEventListener('fetch', (lambda event: event.respondWith(handleRequest(event.request))))

In most respects this is very similar to any other Worker hello world, just in Python syntax. Dictionary literals take the place of JavaScript objects, lambda is used instead of an anonymous arrow function, and so on. If using __new__ to create instances of JavaScript classes seems awkward, the Transcrypt docs discuss an alternative.

Clearly, addEventListener is not a built-in Python function, it's part of the Workers runtime. Because Python is dynamically typed, you don't have to worry about providing type signatures for JavaScript APIs. The downside is that mistakes will result in failures when your Worker runs, rather than when Transcrypt compiles your code. Transcrypt does have experimental support for some degree of static checking using mypy.

Statically typed languages: Scala

You can generate a starter "hello world" Scala project for Workers by running

wrangler generate my-scala-project https://github.com/cloudflare/scala-worker-hello-world

The Scala to JavaScript compiler we're using is Scala.js. It has a plugin for the Scala build tool, so installing sbt and a JDK is all you'll need.

Running sbt fullOptJS in the project directory will compile your Scala code to a single index.js file. The build configuration in build.sbt is set up to output to the root of the project, where Wrangler expects to find an index.js file. After that you can run wrangler publish as normal.

Scala.js uses the Google Closure Compiler to optimize for size when running fullOptJS. For the hello world, the file size is 14k. A more realistic project involving async fetch weighs in around 100k, still well within Workers limits.

In order to take advantage of static type checking, you're going to need type signatures for the JavaScript APIs you use. There are existing Scala signatures for fetch and service worker related APIs. You can see those being imported in the entry point for the Worker, Main.scala:

import org.scalajs.dom.experimental.serviceworkers.{FetchEvent}
import org.scalajs.dom.experimental.{Request, Response, ResponseInit}
import scala.scalajs.js

The import of scala.scalajs.js allows easy access to Scala equivalents of JavaScript types, such as js.Array or js.Dictionary. The remainder of Main looks fairly similar to a Typescript Worker hello world, with syntactic differences such as Unit instead of Void and square brackets instead of angle brackets for type parameters:

object Main {
  def main(args: Array[String]): Unit = {
    Globals.addEventListener("fetch", (event: FetchEvent) => {
      event.respondWith(handleRequest(event.request))
    })
  }

  def handleRequest(request: Request): Response = {
    new Response("Scala Worker hello world", ResponseInit(
        _headers = js.Dictionary("content-type" -> "text/plain")))
  }
}  

Request, Response and FetchEvent are defined by the previously mentioned imports. But what's this Globals object? There are some Worker-specific extensions to JavaScript APIs. You can handle these in a statically typed language by either automatically converting existing Typescript type definitions for Workers or by writing type signatures for the features you want to use. Writing the type signatures isn't hard, and it's good to know how to do it, so I included an example in Globals.scala:

import scalajs.js
import js.annotation._

@js.native
@JSGlobalScope
object Globals extends js.Object {
  def addEventListener(`type`: String, f: js.Function): Unit = js.native
}

The annotation @js.native indicates that the implementation is in existing JavaScript code, not in Scala. That's why the body of the addEventListener definition is just js.native. In a JavaScript Worker you'd call addEventListener as a top-level function in global scope. Here, the @JSGlobalScope annotation indicates that the function signatures we're defining are available in the JavaScript global scope.

You may notice that the type of the function passed to addEventListener is just js.Function, rather than specifying the argument and return types. If you want more type safety, this could be done as js.Function1[FetchEvent, Unit].  If you're trying to work quickly at the expense of safety, you could use def addEventListener(any: Any*): Any to allow anything.

For more information on defining types for JavaScript interfaces, see the Scala.js docs.

Using Workers KV and async Promises

Let's take a look at a more realistic example using Workers KV and asynchronous calls. The idea for the project is our own HTTP API to store and retrieve text values. For simplicity's sake I'm using the first slash-separated component of the path for the key, and the second for the value. Usage of the finished project will look like PUT /meaning of life/42 or GET /meaning of life/

The first thing I need is to add type signatures for the parts of the KV API that I'm using, in Globals.scala. My KV namespace binding in wrangler.toml is just going to be named KV, resulting in a corresponding global object:

object Globals extends js.Object {
  def addEventListener(`type`: String, f: js.Function): Unit = js.native
  
  val KV: KVNamespace = js.native
}
bash$ curl -w "\n" -X PUT 'https://scala-kv-example.cody.workers.dev/meaning of life/42'

bash$ curl -w "\n" -X GET 'https://scala-kv-example.cody.workers.dev/meaning of life/'
42

So what's the definition of the KVNamespace type? It's an interface, so it becomes a Scala trait with a @js.native annotation. The only methods I need to add right now are the simple versions of KV.get and KV.put that take and return strings. The return values are asynchronous, so they're wrapped in a js.Promise. I'll make that wrapped string a type alias, KVValue, just in case we want to deal with the array or stream return types in the future:

object KVNamespace {
  type KVValue = js.Promise[String]
}

@js.native
trait KVNamespace extends js.Object {
  import KVNamespace._
  
  def get(key: String): KVValue = js.native
  
  def put(key: String, value: String): js.Promise[Unit] = js.native
}

With type signatures complete, I'll move on to Main.scala and how to handle interaction with JavaScript Promises. It's possible to use js.Promise directly, but I'd prefer to use Scala semantics for asynchronous Futures. The methods toJSPromise and toFuture from js.JSConverters can be used to convert back and forth:

  def get(key: String): Future[Response] = {
    Globals.KV.get(key).toFuture.map { (value: String) =>
        new Response(value, okInit)
    } recover {
      case err =>
        new Response(s"error getting a value for '$key': $err", errInit)
    }
  }

The function for putting values makes similar use of toFuture to convert the return value from KV into a Future. I use map to transform the value into a Response, and recover to handle failures. If you prefer async / await syntax instead of using combinators, you can use scala-async.

Finally, the new definition for handleRequest is a good example of how pattern matching makes code more concise and less error-prone at the same time. We match on exactly the combinations of HTTP method and path components that we want, and default to an informative error for any other case:

  def handleRequest(request: Request): Future[Response] = {
    (request.method, request.url.split("/")) match {
      case (HttpMethod.GET, Array(_, _, _, key)) =>
        get(key)
      case (HttpMethod.PUT, Array(_, _, _, key, value)) =>
        put(key, value)
      case _ =>
        Future.successful(
          new Response("expected GET /key or PUT /key/value", errInit))
    }
  }

You can get the complete code for this example by running

wrangler generate projectname https://github.com/cloudflare/scala-worker-kv

How to contribute

I'm a fan of programming languages, and will continue to add more Workers templates. You probably know your favorite language better than I do, so pull requests are welcome for a simple hello world or more complex example.

And if you're into programming languages check out the latest language rankings from RedMonk where Python is the first non-Java or JavaScript language ever to place in the top two of these rankings.

Stay tuned for the rest of Serverless Week!

Cloudflareは企業ネットワーク全体を保護し、お客様がインターネット規模のアプリケーションを効率的に構築し、あらゆるWebサイトやインターネットアプリケーションを高速化し、DDoS攻撃を退けハッカーの侵入を防ぎゼロトラスト導入を推進できるようお手伝いしています。

ご使用のデバイスから1.1.1.1 にアクセスし、インターネットを高速化し安全性を高めるCloudflareの無料アプリをご利用ください。

より良いインターネットの構築支援という当社の使命について、詳しくはこちらをご覧ください。新たなキャリアの方向性を模索中の方は、当社の求人情報をご覧ください。
Cloudflare WorkersServerlessServerless WeekJavaScriptPythonWrangler

Xでフォロー

Cloudflare|@cloudflare

関連ブログ投稿