Dart sisi server

Dart sisi server: routing dengan shelf dan framework dart_frog.

Daftar isi

Bagian akhir jalur Dart bergerak ke luar aplikasi mobile, dan server adalah salah satu tujuannya. Kelas HttpServer dari dart:io sudah menyediakan pengikat port beserta aliran permintaan, sedangkan package shelf menyusun kemampuan itu menjadi kerangka server web yang teruji. Topik ini membangun satu proyek server kecil bernama server_demo dengan shelf dan shelf_router, mengujinya dengan curl, lalu menutupnya dengan dart_frog sebagai framework yang lebih lengkap. Server pada contoh sengaja dibuat melayani satu permintaan lalu menutup dirinya, sehingga setiap demo berakhir dengan sendirinya dan tidak menyisakan proses yang menggantung.

Konsep server pada shelf#

shelf adalah package resmi dari tim Dart yang mendefinisikan model middleware untuk server web, dan deskripsi resminya menekankan komposisi serta kemudahan penggunaan ulang. Unit terkecilnya adalah handler, yaitu fungsi yang menerima sebuah Request dan mengembalikan sebuah Response. Handler ditulis langsung dalam Dart, sehingga satu server utuh hanya berisi rangkaian fungsi yang saling terhubung tanpa berkas konfigurasi tambahan. Bentuk yang sederhana itu menjadi dasar bagi dart_frog dan framework server lain yang dibangun di atasnya.

Dua package pelengkap melengkapi gambar tersebut. Package shelf_router menambahkan kelas Router yang mengatur routing, yaitu pemetaan dari jalur permintaan ke handler, dan mengenali segmen parameter seperti <hari> pada jalur rute. Package shelf_io menyediakan penjembatan ke dart:io, yaitu fungsi handleRequest yang menerima HttpRequest hasil HttpServer.bind dan meneruskannya kepada handler. Middleware menyisipkan perilaku tambahan di sekitar handler, misalnya pencatatan atau penyaringan, tanpa mengubah kode handler itu sendiri.

Membangun proyek server#

Proyek contoh bernama server_demo dibangun dari nol pada folder ~/contoh/melampaui-mobile/dart-sisi-server. Strukturnya mengikuti tata letak proyek Dart standar, yaitu pubspec.yaml pada akar proyek dan program yang dieksekusi langsung pada folder bin. Berkas pubspec.yaml proyek ini memuat dua dependensi, sebagaimana ditampilkan berikut.

yaml pubspec.yaml
name: server_demo
description: Server kecil dengan shelf untuk latihan dart-sisi-server.
version: 1.0.0
environment:
  sdk: ^3.13.0
dependencies:
  shelf: ^1.4.0
  shelf_router: ^1.1.0

Batasan caret ^1.4.0 mengizinkan versi 1.4.0 ke atas sampai sebelum 2.0.0, dan pub memilih versi terbaru yang cocok ketika resolusi berlangsung. Nama package hanya boleh memuat huruf kecil, angka, dan garis bawah, sehingga nama server_demo ditulis dengan garis bawah. Blok bash berikut menulis pubspec.yaml beserta program bin/server.dart melalui heredoc, lalu mengunduh dependensinya dengan dart pub get.

bash
export PATH=/home/kal/dart-sdk/bin:$PATH
# bangun proyek server demo dari nol
rm -rf ~/contoh/melampaui-mobile/dart-sisi-server
mkdir -p ~/contoh/melampaui-mobile/dart-sisi-server/server_demo/bin
cd ~/contoh/melampaui-mobile/dart-sisi-server/server_demo
cat > pubspec.yaml <<'EOF'
name: server_demo
description: Server kecil dengan shelf untuk latihan dart-sisi-server.
version: 1.0.0
environment:
  sdk: ^3.13.0
dependencies:
  shelf: ^1.4.0
  shelf_router: ^1.1.0
EOF
cat > bin/server.dart <<'EOF'
import 'dart:convert';
import 'dart:io';

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart';

// jadwal piket sebagai data rute; kunci harinya menjadi parameter rute
const _piket = <String, String>{
  'senin': 'Ali',
  'rabu': 'Budi',
  'jumat': 'Citra',
};

Response _halo(Request permintaan) {
  return Response.ok('Halo dari server Dart dengan shelf\n');
}

Response _daftarPiket(Request permintaan) {
  final daftar = _piket.entries
      .map((entri) => {'hari': entri.key, 'nama': entri.value})
      .toList();
  return Response.ok(
    '${jsonEncode(daftar)}\n',
    headers: {'content-type': 'application/json'},
  );
}

Response _piketHari(Request permintaan, String hari) {
  final nama = _piket[hari];
  if (nama == null) {
    return Response.notFound('hari $hari tidak ada dalam jadwal piket\n');
  }
  return Response.ok(
    '${jsonEncode({'hari': hari, 'nama': nama})}\n',
    headers: {'content-type': 'application/json'},
  );
}

Future<void> main() async {
  final router = Router()
    ..get('/halo', _halo)
    ..get('/piket', _daftarPiket)
    ..get('/piket/<hari>', _piketHari);

  final server = await HttpServer.bind('127.0.0.1', 19081);
  await for (final permintaan in server) {
    await shelf_io.handleRequest(permintaan, router);
    await server.close(force: true);
  }
}
EOF
# unduh dependensi shelf dan shelf_router dari pub.dev
dart pub get
text Keluaran
Resolving dependencies...
Downloading packages...
+ async 2.13.1
+ collection 1.19.1
+ http_methods 1.1.1
+ http_parser 4.1.2
+ meta 1.19.0
+ path 1.9.1
+ shelf 1.4.3
+ shelf_router 1.1.5
+ source_span 1.10.2
+ stack_trace 1.12.2
+ stream_channel 2.1.4
+ string_scanner 1.4.1
+ term_glyph 1.2.2
+ typed_data 1.4.0
Changed 14 dependencies!

Baris berawalan + yang memuat shelf dan shelf_router adalah dependensi langsung, sedangkan dua belas lainnya adalah dependensi turunan yang dibutuhkan keduanya. Pesan Changed 14 dependencies! menghitung seluruh entri baru pada pubspec.lock, bukan hanya dua dependensi yang tertulis pada pubspec.yaml. Keluaran resolusi menampilkan versi yang terpilih saat itu, dan angkanya bergeser ketika pub.dev merilis versi yang lebih baru.

Router dan rute#

Program server utuh terdapat pada berkas bin/server.dart, yaitu berkas yang ditulis dengan heredoc pada blok bash sebelumnya. Isinya identik dengan aset server.dart di bawah ini, sehingga pembacaan dan eksekusi saling menguatkan. Bagian utama adalah Router pada fungsi main, sedangkan tiga fungsi di atasnya adalah handler tiap rute.

dart server.dart
import 'dart:convert';
import 'dart:io';

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart';

// jadwal piket sebagai data rute; kunci harinya menjadi parameter rute
const _piket = <String, String>{
  'senin': 'Ali',
  'rabu': 'Budi',
  'jumat': 'Citra',
};

Response _halo(Request permintaan) {
  return Response.ok('Halo dari server Dart dengan shelf\n');
}

Response _daftarPiket(Request permintaan) {
  final daftar = _piket.entries
      .map((entri) => {'hari': entri.key, 'nama': entri.value})
      .toList();
  return Response.ok(
    '${jsonEncode(daftar)}\n',
    headers: {'content-type': 'application/json'},
  );
}

Response _piketHari(Request permintaan, String hari) {
  final nama = _piket[hari];
  if (nama == null) {
    return Response.notFound('hari $hari tidak ada dalam jadwal piket\n');
  }
  return Response.ok(
    '${jsonEncode({'hari': hari, 'nama': nama})}\n',
    headers: {'content-type': 'application/json'},
  );
}

Future<void> main() async {
  final router = Router()
    ..get('/halo', _halo)
    ..get('/piket', _daftarPiket)
    ..get('/piket/<hari>', _piketHari);

  final server = await HttpServer.bind('127.0.0.1', 19081);
  await for (final permintaan in server) {
    await shelf_io.handleRequest(permintaan, router);
    await server.close(force: true);
  }
}

Konstruktor Router dipanggil sekali, lalu tiga pemanggilan get dengan cascade mendaftarkan rutenya. Rute /halo membalas dengan teks polos, dan Response.ok secara otomatis melengkapi responsnya dengan content-type text/plain. Rute /piket membalas dengan seluruh daftar jadwal dalam JSON yang disusun jsonEncode dari dart:convert, sedangkan rute /piket/<hari> memuat parameter segmen yang mengisi nilai hari pada handler-nya. Apabila hari yang diminta tidak ada pada map _piket, handler mengembalikan Response.notFound, sehingga kode statusnya 404.

Fungsi main mengikat HttpServer ke 127.0.0.1 pada port 19081, lalu setiap permintaan diteruskan ke router melalui shelf_io.handleRequest. Sesudah satu respons terkirim, server.close(force: true) melepaskan port dan fungsi main selesai, sehingga program berakhir dengan sendirinya. Pola satu-permintaan-lalu-berakhir ini sengaja dipilih untuk demo, karena server yang sungguhan melayani banyak permintaan dan berjalan terus sampai dihentikan.

Demo pertama menjalankan server di latar, mengirim satu permintaan ke /halo dengan curl, lalu menunggu server menutup dirinya. Opsi --retry-connrefused pada curl mengulang sambungan yang ditolak, sehingga permintaan tetap berhasil meskipun pemulai program memerlukan waktu lebih dari satu detik. Port 19081 dipilih dari rentang tinggi agar tidak bertabrakan dengan layanan yang biasa terpasang.

bash
export PATH=/home/kal/dart-sdk/bin:$PATH
cd ~/contoh/melampaui-mobile/dart-sisi-server/server_demo
# server dijalankan di latar, melayani satu permintaan, lalu menutup dirinya
dart run bin/server.dart & P=$!
sleep 1
curl -s --retry 5 --retry-connrefused http://localhost:19081/halo
wait $P
text Keluaran
Halo dari server Dart dengan shelf

Keluaran persis sama dengan teks yang dikembalikan handler _halo, ditambah baris baru di ujungnya. Tidak ada keluaran lain dari server karena handler hanya mengembalikan respons, dan prosesnya pun sudah berakhir sebelum wait selesai. Dari sisi pemanggil, server berperilaku seperti layanan web mana pun, karena perbedaannya hanya pada usia program yang sengaja singkat.

Rute dengan parameter diuji berikutnya. Permintaan ke /piket/rabu menangkap segmen rabu sebagai nilai parameter hari, sehingga handler _piketHari membalas dengan jadwal hari itu dalam satu baris JSON. Nilai parameter selalu bertipe String, dan pencocokannya hanya terjadi pada satu segmen jalur.

bash
export PATH=/home/kal/dart-sdk/bin:$PATH
cd ~/contoh/melampaui-mobile/dart-sisi-server/server_demo
# segmen rabu ditangkap parameter <hari> pada rute
dart run bin/server.dart & P=$!
sleep 1
curl -s --retry 5 --retry-connrefused http://localhost:19081/piket/rabu
wait $P
text Keluaran
{"hari":"rabu","nama":"Budi"}

Kasus terakhir menunjukkan respons gagal. Permintaan ke /piket/minggu tidak menemukan hari minggu pada map _piket, sehingga handler mengembalikan Response.notFound berisi teks penjelasan. Opsi -w pada curl menambahkan kode status di bawah isi respons, dan hasilnya 404 sesuai respons notFound tersebut.

bash
export PATH=/home/kal/dart-sdk/bin:$PATH
cd ~/contoh/melampaui-mobile/dart-sisi-server/server_demo
# kode status ditampilkan lewat format -w milik curl
dart run bin/server.dart & P=$!
sleep 1
curl -s --retry 5 --retry-connrefused -w "status: %{http_code}\n" http://localhost:19081/piket/minggu
wait $P
text Keluaran
hari minggu tidak ada dalam jadwal piket
status: 404

Middleware pencatat permintaan#

Middleware pada shelf adalah fungsi yang menerima sebuah Handler dan mengembalikan Handler baru, yaitu versi handler lama yang telah diberi perilaku tambahan. Pada server_catat.dart, middleware catat mencetak metode dan jalur permintaan sebelum handler berjalan, lalu mencetak kode status setelah handler selesai. Penyusunnya adalah kelas Pipeline, yaitu objek yang menempatkan middleware berurutan di depan handler akhir melalui addMiddleware.

dart server_catat.dart
import 'dart:io';

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart';

// middleware mencatat permintaan sebelum dan sesudah handler berjalan
Middleware catat() {
  return (Handler handler) {
    return (Request permintaan) async {
      print('permintaan: ${permintaan.method} ${permintaan.requestedUri.path}');
      final respons = await handler(permintaan);
      print('status: ${respons.statusCode}');
      return respons;
    };
  };
}

Response _halo(Request permintaan) {
  return Response.ok('Halo dari server Dart dengan shelf\n');
}

Future<void> main() async {
  final router = Router()..get('/halo', _halo);
  final handler = const Pipeline().addMiddleware(catat()).addHandler(router);

  final server = await HttpServer.bind('127.0.0.1', 19081);
  await for (final permintaan in server) {
    await shelf_io.handleRequest(permintaan, handler);
    await server.close(force: true);
  }
}

Demo berikut menuliskan berkas tersebut ke proyek yang sudah dibangun, lalu menjalankannya dengan mengarahkan keluaran server ke berkas terpisah. Pemisahan ini menjaga urutan tampilnya tetap pasti, karena keluaran curl dan keluaran server tidak bercampur pada satu aliran. Server kali ini hanya memuat rute /halo karena yang diuji adalah middlewarenya.

bash
export PATH=/home/kal/dart-sdk/bin:$PATH
cd ~/contoh/melampaui-mobile/dart-sisi-server/server_demo
cat > bin/server_catat.dart <<'EOF'
import 'dart:io';

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart';

// middleware mencatat permintaan sebelum dan sesudah handler berjalan
Middleware catat() {
  return (Handler handler) {
    return (Request permintaan) async {
      print('permintaan: ${permintaan.method} ${permintaan.requestedUri.path}');
      final respons = await handler(permintaan);
      print('status: ${respons.statusCode}');
      return respons;
    };
  };
}

Response _halo(Request permintaan) {
  return Response.ok('Halo dari server Dart dengan shelf\n');
}

Future<void> main() async {
  final router = Router()..get('/halo', _halo);
  final handler = const Pipeline().addMiddleware(catat()).addHandler(router);

  final server = await HttpServer.bind('127.0.0.1', 19081);
  await for (final permintaan in server) {
    await shelf_io.handleRequest(permintaan, handler);
    await server.close(force: true);
  }
}
EOF
# keluaran server diarahkan ke berkas agar urutan tampilnya tetap
dart run bin/server_catat.dart > keluaran-server.txt & P=$!
sleep 1
curl -s --retry 5 --retry-connrefused http://localhost:19081/halo
wait $P
# tampilkan catatan middleware setelah program berakhir
cat keluaran-server.txt
text Keluaran
Halo dari server Dart dengan shelf
permintaan: GET /halo
status: 200

Keluaran memperlihatkan tiga baris dalam urutan yang pasti. Baris pertama adalah isi respons yang diterima curl, sedangkan dua baris berikutnya berasal dari middleware yang mencatat permintaan beserta statusnya. Dengan pola ini, pencatatan akses dapat ditambahkan ke server mana pun tanpa menyentuh satu pun handler.

dart_frog sebagai framework lengkap#

shelf menyediakan bagian dasarnya, sedangkan dart_frog menyusun bagian itu menjadi framework yang lebih lengkap. Dokumentasi server Dart mendeskripsikan dart_frog sebagai framework dengan routing minimalis yang dibangun di atas shelf. Yang ditambahkan antara lain routing berbasis berkas pada folder routes, server pengembangan dengan hot reload, dan struktur proyek yang seragam, sehingga pengembang tidak merakit pengikat port dan router sendiri.

Alur kerjanya dimulai dari dart pub global activate dart_frog_cli, yaitu pemasangan perintah dart_frog ke daftar executable global pub. Setelah itu dart_frog create membuat proyek baru dari templat resminya, sedangkan dart_frog dev menjalankan server pengembangan pada port 8080 secara bawaan. Blok berikut menjalankan dua langkah pertama pada folder contoh. Keluaran create yang memuat lama proses tidak ditempel karena nilainya berubah antarjalanan, sehingga struktur berkasnya yang ditampilkan sebagai gantinya.

bash
export PATH=/home/kal/dart-sdk/bin:$PATH
# executable global pub berada pada folder ~/.pub-cache/bin
export PATH="$PATH":"$HOME/.pub-cache/bin"
# pasang dart_frog_cli sebagai perintah global
dart pub global activate dart_frog_cli
# buat proyek dart_frog dari templat resminya
cd ~/contoh/melampaui-mobile/dart-sisi-server
rm -rf jadwal_frog
dart_frog create jadwal_frog >/dev/null
# tampilkan berkas hasil templat beserta isi rute bawaannya
find jadwal_frog -type f -not -path '*/.dart_tool*' | sort
cat jadwal_frog/routes/index.dart
text Keluaran
Package dart_frog_cli is currently active at version 1.2.14.
Resolving dependencies...
Downloading packages...
The package dart_frog_cli is already activated at newest available version.
To recompile executables, first run `dart pub global deactivate dart_frog_cli`.
Installed executable dart_frog.
Activated dart_frog_cli 1.2.14.
jadwal_frog/.gitignore
jadwal_frog/.vscode/extensions.json
jadwal_frog/README.md
jadwal_frog/analysis_options.yaml
jadwal_frog/pubspec.lock
jadwal_frog/pubspec.yaml
jadwal_frog/routes/index.dart
jadwal_frog/test/routes/index_test.dart
import 'package:dart_frog/dart_frog.dart';

Response onRequest(RequestContext context) {
  return Response(body: 'Welcome to Dart Frog!');
}

Templat dart_frog create menghasilkan proyek dengan folder routes, dan berkas routes/index.dart menangani rute / sesuai aturan routing berbasis berkasnya. Handler-nya bernama onRequest, yaitu padanan handler shelf yang menerima RequestContext dan mengembalikan Response. Isi responsnya masih Welcome to Dart Frog! sesuai templat, dan teks itu tinggal diganti sesuai kebutuhan aplikasi. Dengan dependensi dart_frog pada pubspec.yaml proyeknya, kebutuhan server dari routing sampai pengujian sudah tersedia dalam satu framework.

Langkah terakhir dari alur ini adalah dart_frog dev, dan perintahnya tidak dijalankan pada demo ini karena server pengembangannya berjalan terus, sehingga eksekusi tidak pernah selesai dalam demo satu-permintaan.

bash
# tidak-dijalankan
# dart_frog dev menjalankan server pengembangan dengan hot reload
cd ~/contoh/melampaui-mobile/dart-sisi-server/jadwal_frog
dart_frog dev
# rute bawaan diuji dari terminal lain, misalnya curl http://localhost:8080

Ringkasan#

  • shelf menyediakan abstraksi handler, yaitu fungsi dari Request ke Response, dan shelf_router menambahkan Router dengan rute get serta parameter segmen seperti /piket/<hari>.
  • Proyek server_demo dideklarasikan pada pubspec.yaml dengan dua dependensi, dibangun dengan dart pub get, dan programnya mengikat HttpServer ke port 19081 sebelum permintaan diteruskan melalui shelf_io.handleRequest.
  • Pola satu-permintaan-lalu-berakhir menutup server dengan close(force: true) sesudah satu respons, sehingga setiap demo berakhir tanpa proses yang tersisa.
  • Middleware menyisipkan pencatatan melalui Pipeline tanpa mengubah handler, sedangkan dart_frog menyusun shelf menjadi framework dengan routing berbasis berkas dan server pengembangan bawaan.

Referensi#