Scratch 3.0 の拡張ブロックの作り方が分かってきたので、Web API の呼び出しを作ってみる。
基本的なところは、Scratch 3.0でオリジナルブロックをつくろう – Qiita と同じで、もうちょっと先に進んでみよう、というところです。Scratch 自体が学習用なので、その目的に沿っているかどうかは分からないのですが、まぁ、ちょっとばかし無茶をやって業務で使ってみるのもアリかなぁと。「Scratch はプログラム言語なのか?」という質問が多い中で、じゃあ、実用的な業務で使うためにはこんなパターンがあるという例です。
適当な MDA ツールとして使う
ブロックを組み合わせてロジックを作る言語(ビジュアル言語とかブロックプログラミングとか)は、Scratch の他にどんなものがあるのか?というと、
- micro:bit の MakeCode
- Microsoft Flow 改め Power Automate Power Automate
- Node-RED日本ユーザ会
なんてのがあります。この中に UML を含めてもいいかもしれません。ブロックを組み合わせて細かいロジック(加算や減算など)を組むものもあれば、ある程度コンポーネント化されたものを呼び出す MDA(モデル駆動型アーキテクチャ)的なものもあります。
Scratch の場合は、細かいロジックを組む所謂プログラム言語に属する訳ですが、手順を繋げたり条件分岐をうまく使えば MDA なツールとしても使える…と思ってみるわけです。
試してみるのは、
- 適当な Web API を複数回呼び出す
- Web API の戻り値で適当に分岐する
- Web API の戻り値で適当に猫が喋る
あたりを想定して作っていきます。
テストデータの投入を自動化するとか、サンプルデータの投入あたりを考えてみます。
下準備
まずは、ローカル環境で Scratch 3.0 が動く環境を整えます。Linux 環境でやるのが望ましいのですが、Windows の WSL を使っても大丈夫です。というか、VSCode を使うと便利なので、WSL がお薦めです。
github からコードをダウンロード
$ git clone --depth 1 https://github.com/llk/scratch-vm.git
$ git clone --depth 1 https://github.com/llk/scratch-gui.git
ビルド
$ cd scratch-vm
$ npm i
$ sudo npm link
$ cd ../scratch-gui
$ npm i
$ sudo npm link scratch-vm
scratch-vm に拡張機能を使って、scratch-gui で UI を動かします。scratch-gui は React が使われているので、scratch-vm 内のコードを修正すると自動的にブラウザのほうでリロードが掛かります。
試しに実行
ローカル環境で http://localhost:8601/ とすれば、ブラウザで Scratch 3.0 が動く状態になります。
$ cd scratch-gui
$ npm start
この状態で、ほぼ https://scratch.mit.edu/ と同じ状態になります。違いは、
- 共有ができない
- サーバーにコードが自動保存されない。
ことぐらいです。
ローカル環境では、「ファイル」メニューからローカルのコンピュータにコードを保存できます。この保存したファイルを本家の Scratch にアップロードすれば共有が可能になります。
ただし、今回のようなオリジナルな拡張ブロックを使っている場合は本家では動かないので、適宜ローカル環境で動かすか、社内で適当なサーバーを立てて(ラズパイ3とかでも十分です)そこにアクセスするか、という形になります。
多分、ラズパイ上に Scratch 3.0 のサーバーを立てて、そこから Arduino に接続させるとか、センサーのデータを読み取るとかするほうが PC よりも手軽かもしれません。そのあたりはおいおい模索していきます。
リモート環境で VSCode を動かす
WSL(Windows System Linux)にはリモート接続対応の VScode をインストールできます。
$ code .
WSL 上でこんな風に code コマンドを入れると、自動的に Windows 側で VSCode が立ち上がってくれます。
ドライブが共有になっているので、Windows 側で閲覧してもよいのですが、WSL 側とファイル競合が発生してまうので、リモート接続にします。ここで *.js のファイルを編集して保存すると自動的にブラウザのほうでリロードが掛かります。
scratch-vm の編集
拡張機能を「webapisample」にして作成していきます。
$ mkdir scratch-vm/src/extensions/scratch3_webapisample/
$ touch scratch-vm/src/extensions/scratch3_webapisample/index.js
scratch-vm/src/extensions フォルダーの中に、新しく「webapisample」フォルダーを作って index.js ファイルを作成します。
const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Cast = require('../../util/cast');
const log = require('../../util/log');
class Scratch3WebApiSample {
constructor (runtime) {
this.runtime = runtime;
this._result = "" ;
this._url = "";
this._result = "";
}
getInfo () {
return {
id: 'webapisample',
name: 'Web API Sample',
blocks: [
{
opcode: 'setUrl',
blockType: BlockType.COMMAND,
text: 'Set URL [TEXT]',
arguments: {
TEXT: {
type: ArgumentType.STRING,
defaultValue: "http://localhost:5000/api/Hello/"
}
}
},
{
opcode: 'getUrl',
blockType: BlockType.REPORTER,
text: 'URL'
},
{
opcode: 'getResult',
blockType: BlockType.REPORTER,
text: 'result'
},
{
opcode: 'callWebApiByGet',
blockType: BlockType.REPORTER,
text: 'Call Web API GET [TEXT]',
arguments: {
TEXT: {
type: ArgumentType.STRING,
defaultValue: "100"
}
}
}
],
menus: {
}
};
}
setUrl( args ) {
const text = Cast.toString(args.TEXT);
this._url = text ;
}
getUrl() {
return this._url ;
}
getResult() {
return this._result ;
}
callWebApiByGet(args) {
const text = Cast.toString(args.TEXT);
const path = this._url + text ;
var pr = fetch( path )
.then( res => res.text() )
.then( body => this._result = body ) ;
return pr ;
}
}
module.exports = Scratch3WebApiSample;
少し癖があるのですが、
- getInfo 関数で、ブロックの情報を返す
- opcode に対応する関数名を記述する
- それぞれの関数内で Javascript で記述する
ブロックの種類にはいくつかあるのですが、
- BlockType.COMMAND : コマンドブロック(非同期)
- BlockType.REPORTER : 値ブロック(同期処理)
になります。Web API呼び出しでは、fetch を使うのですが、ここをコマンドブロックにすると戻り値が取れません。なので、値ブロックにして同期的に処理を行います。戻り値は fetch が返す Promise 型を使えば ok です。
Scratch 3.0の拡張機能を作ってみよう/応用 – Japanese Scratch-Wiki を参考にしてください。
callWebApiByGet(args) {
const text = Cast.toString(args.TEXT);
const path = this._url + text ;
var pr = fetch( path )
.then( res => res.text() )
.then( body => this._result = body ) ;
return pr ;
}
ここでは4つのブロックを作っていて、こんな感じになります。
Web API を実行する http://localhost:5000/api/Hello/ は後で、ASP.NET Core MVC で作ります。ここの呼び先は、PHP で作ってもいいし、nodejs でもいいし、Azure Functions でも構いません。
クラス内の変数の扱いは、JS なのでそれに則ります。このあたり、コード自体は Javascript なので、結構いろいろなことができます。
scratch-vm/src/extension-support/extension-manager.js のファイルに、拡張機能をインクルードするための行を追加しておきます。以下の「★」の部分です。
const builtinExtensions = {
// This is an example that isn't loaded with the other core blocks,
// but serves as a reference for loading core blocks as extensions.
coreExample: () => require('../blocks/scratch3_core_example'),
// These are the non-core built-in extensions.
pen: () => require('../extensions/scratch3_pen'),
wedo2: () => require('../extensions/scratch3_wedo2'),
music: () => require('../extensions/scratch3_music'),
microbit: () => require('../extensions/scratch3_microbit'),
text2speech: () => require('../extensions/scratch3_text2speech'),
translate: () => require('../extensions/scratch3_translate'),
videoSensing: () => require('../extensions/scratch3_video_sensing'),
ev3: () => require('../extensions/scratch3_ev3'),
makeymakey: () => require('../extensions/scratch3_makeymakey'),
boost: () => require('../extensions/scratch3_boost'),
gdxfor: () => require('../extensions/scratch3_gdx_for'),
webapisample: () => require('../extensions/scratch3_webapisample') // ★
};
最終的には id を使ったりするのでしょうが、いまはこのままで。
vscode でファイル保存して特にエラーが出なければ ok です。
scratch-gui の編集
scratch-gui では拡張機能を読み取るところを追加します。
$ mkdir scratch-gui/src/lib/libraries/extensions/webapisample
webapisample このフォルダーに2つの画像ファイルを置きます。
- webapi.png
- webapi-small.png
拡張機能を読み込むときに使われる画像ですね。面倒ななので字だけです(苦笑)。
scratch-gui/src/lib/libraries/extensions/index.jsx 追加の設定を記述します。
import webapisampleIconURL from './webapisample/webapi.png';
import webapisampleInsetIconURL from './webapisample/webapi-small.png';
// 省略
{
name: "Web API Sample Blocks",
extensionId: 'webapisample',
iconURL: webapisampleIconURL,
insetIconURL: webapisampleInsetIconURL,
collaborator: 'moonmile',
description: "You can call Web API.",
featured: true,
internetConnectionRequired: true,
helpLink: 'http://moonmile.net/'
}
}
最初の import はアイコンを指定しているだけなので、直に iconURL や insetIconURL に指定してもよいでしょう。多言語化しないので、FormattedMessage を使わずに直書きしています。
ファイルを保存して、Scratch 3.0 側で拡張機能を読み込むと、オリジナルのブロックが現れます。Javascript のコードが間違っていたり、ブロックの opcode などの設定が間違っていると、拡張ブロックの部分が読み込めないので注意してください。
読み込めなかったときのデバッグ手段がないので、ちょっとずつブロックを増やしていくのがコツです。
Web API を作る
.NET Core を使ってテスト用の Web API サーバーを作ります。
$ dotnet new webapi -n hello
こんな風に hello プロジェクトを作った後に、Controllers/HelloController.cs を追加します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace hello.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class HelloController : ControllerBase
{
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
var text = "Hello api {id}";
System.Diagnostics.Debug.WriteLine(text);
return text;
}
}
}
数値の id を渡されたら「Hello api 100」のように返すだけの GET メソッドです。本来ならば、データベース検索をしてデータを返すところです。ASP.NET MVC の Web API は自動で JSON 形式で返すのですが、ひとまずこのままにしておきます。
もう一つ、XSS 対応をしておきます。Scratch 3.0 では http://localhost:8601/ で動くのですが、作成する Web API は http://localhost:5000/ で動くことになります。なので、クロスサイトスクリプトの実行状態になり、そのままでは次のようなエラーがでます。これは Chrome の F12 でエラー表示をしたところです。
この対策のために、Web API 側で CORS 対策をします。
Microsoft.AspNetCore.Cors パッケージを dotnet コマンドで入れた後に、
dotnet add package Microsoft.AspNetCore.Cors
Startup.cs に次の2行を追加します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace hello
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(); // CORS 追加
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseCors(o => o.AllowAnyOrigin()); // CORS 追加
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
これで先のエラーがなくなります。dotnet run で Web API サーバーを動かしたあとに、ブラウザで http://localhost:5000/api/Hello/100 のように確認してみてください。
実行してみる
Scratch 3.0 と Web API サーバーを起動した状態で、次のようにブロックを組んでみます。
猫スプライトをクリックすると、「カウンタ」が+1されて、Web API が呼び出されます。Web API は同期的に呼び出されるので、戻り値が「結果」に入って、猫が喋る、という簡単なものです。
内部的にはちょっと複雑な(とはいえ、Web APIを呼び出しているだけだけど)ことになっていますが、表面的にはカウンタを使って何かの「結果」を取ってきているだけです。
実際、猫をクリックすると結果がカウントアップされていくのが分かります。きちんとデータベース検索をしてやれば、IDなどを渡して Web API 経由でデータを引き出すこともできます。
今後は
fetch を使って Web API を順次呼び出せることが分かったので、
- CRUD を揃えて、RESTful な Web API にしておく。
- データベース検索をする
- やり取りを JSON 形式にして、プロパティの受け渡しができるようにする
を引き続き後日。