Scratchに代表されるブロックプログラミング形式でAWSにアクセスするものを作ってみました。
ブロックプログラミングとは
Bing AIに聞いてみました。
ブロックプログラミングとは、プログラミングの基本的な概念を視覚的に表現したブロックを組み合わせて、コンピュータに命令する方法です。ブロックプログラミングは、テキストでコードを書くよりも簡単で直感的なので、初心者や子どもにもおすすめです。ブロックプログラミングを学ぶには、Microsoft MakeCodeやBlockly Gamesなどのオンラインツールが便利です。
ブロックプログラムを実現するOSSライブラリー "Blockly"
Google製のOSSライブラリー「Blockly」を使うと、独自のブロックプログラミング環境を作成することができます。
使い方を紹介します。
プロジェクトの作成
pnpmとviteを使ってReactプロジェクトを作成します。
pnpmをインストール
shellnpm install -g pnpm
Reactプロジェクトを作成
shellpnpm create vite
✔ Project name: … blocky-s3-sample
✔ Select a framework: › React
✔ Select a variant: › TypeScriptReactに必要なライブラリーのインストール
shellcd blocky-s3-sample
pnpm installBlocklyをインストール
shellpnpm install blockly
Reactのstrictモードをオフにする
Blocklyの動作に影響があるので、strictモードをオフにします。
main.tsximport React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
- <React.StrictMode>
<App />
- </React.StrictMode>,
)
ここまでで必要なライブラリーの導入は完了です。
エディターの配置
idに
blocklyDiv
を指定したdivタグを追加します。App.tsx<div id="blocklyDiv" style={{ height: '50vh', width: '100vw' }}></div>
toolboxを定義
JSONやXMLでtoolboxを定義できるのですが、いちから作るのは大変なので、Blockly Developer Toolsを使って作成します。
Blockly Developer Toolsにアクセス
Workspace Factory
タブを選択 保存してない旨を確認されますがそのままOKで進みます。画面真ん中あたりの
+
をクリックし、Standard Toolbox
を選択Export
をクリックし、Toolboxに名前をつけて保存取得したXMLを文字列として定義します。
const toolboxXml = `<xml xmlns="https://developers.google.com/blockly/xml" id="toolbox" style="display: none">
...
...
...
</xml>
`注記自動生成される
id
に ` が入っている場合は - など別の文字に置換します。
Blocklyをセット
App.tsxuseEffect(() => {
const myWorkspace = Blockly.inject('blocklyDiv', { toolbox: toolboxXml })
}, [])
一度起動して画面を確認します。
pnpm run dev
ブロックエディタが表示されます。
独自のブロックを追加
まずは手始めにconsole.log()
を行うブロックを追加してみましょう。
独自ブロックを追加する手順は大きく以下のとおりです。
- ブロックを定義
- ツールバーにブロックを配置
- ブロックの処理を記述
console.log()
を行う処理はsrc/block/consoleLog.tsx
に作成します。
1. ブロックを定義
先程使用したBlockly Developer Toolsでブロック定義が作成できます。今回はこのようにしました。
export const define = {
"type": blockType,
"message0": "ログ出力 %1",
"args0": [
{
"type": "input_value",
"name": "message",
"check": "String",
},
],
"previousStatement": null,
"nextStatement": null,
"colour": "#888888",
"tooltip": "",
"helpUrl": ""
}
App.tsxで参照します。
+ import * as consoleLogBlock from './block/consoleLog'
...
useEffect(() => {
+ Blockly.defineBlocksWithJsonArray([
+ consoleLogBlock.define,
+ ])
const myWorkspace = Blockly.inject('blocklyDiv', { toolbox: toolboxXml })
}, [])
2. ツールバーにブロックを配置
toolboxXml
の末尾にツールバーのカテゴリを追加します。
<category name="Functions" colour="#995ba5" custom="PROCEDURE"></category>
+ <sep></sep>
+ <category name="Debug" custom="debug" colour="#888888"></category>
</xml>
追加したカテゴリにブロックを追加します。
const debugFlyoutCallback = function (workspace: Blockly.WorkspaceSvg) {
const blockList = [
{
"kind": "block",
"type": consoleLogBlock.blockType
},
];
return blockList;
};
myWorkspace.registerToolboxCategoryCallback('debug', debugFlyoutCallback);
registerToolboxCategoryCallbackの第一引数が、XMLで定義したcategory
のcustom
属性と対応します
3. ブロックの処理を記述
export const javascriptCode = function (block: Blockly.Block) {
const value_message = javascriptGenerator.valueToCode(block, 'message', javascriptGenerator.ORDER_ATOMIC);
const code = `console.log(${value_message})
`
return code
}
valueToCode
はブロックの引数を取得しています。code
が実行されるコードです。
App.tsxで参照します。
+ import { javascriptGenerator } from 'blockly/javascript';
useEffect(() => {
Blockly.defineBlocksWithJsonArray([
consoleLogBlock.define,
])
const myWorkspace = Blockly.inject('blocklyDiv', { toolbox: toolboxXml })
const debugFlyoutCallback = function (workspace: Blockly.WorkspaceSvg) {
const blockList = [
{
"kind": "block",
"type": consoleLogBlock.blockType
},
];
return blockList;
};
myWorkspace.registerToolboxCategoryCallback('debug', debugFlyoutCallback);
+ javascriptGenerator[consoleLogBlock.blockType] = consoleLogBlock.javascriptCode
}, [])
これでDebug
カテゴリにログ出力
ブロックが追加されます。
コード生成を行うようにボタンとテキストエリアを追加します。
function App() {
+ const [workspace, setWorkspace] = useState<Blockly.WorkspaceSvg>()
+ const [generatedCode, setGeneratedCode] = useState('')
useEffect(() => {
...
}, [])
+ const generateCode = (() => {
+ const jsCode = javascriptGenerator.workspaceToCode(workspace)
+
+ setGeneratedCode(jsCode)
+ })
return (
<>
<div id="blocklyDiv"
style={{ height: '600px', width: '800px' }}
></div>
+ <div>
+ <button onClick={generateCode}>generate code</button>
+ <br />
+ <textarea
+ readOnly
+ style={{ height: '40vh', width: '100vw' }}
+ value={generatedCode}
+ ></textarea>
+ </div>
</>
)
}
これでコード生成ができました。
S3を操作するためのブロックを作成
いよいよS3にアクセスする部分です。
ブロックを3つ作成しました。
- Import文を生成するブロック
- S3Clientを作成するブロック
- listObjectsを実行するブロック
1. Import文を生成するブロック
ブロックを定義
import.tsxexport const define = {
"type": blockType,
"message0": "Import宣言",
"nextStatement": null,
"colour": "#FF9900",
"tooltip": "",
"helpUrl": ""
}ツールバーにブロックを配置
toolboxXml
の末尾にツールバーのカテゴリを追加します。App.tsx<category name="Functions" colour="#995ba5" custom="PROCEDURE"></category>
<sep></sep>
<category name="Debug" custom="debug" colour="#888888"></category>
+ <sep></sep>
+ <category name="AWS" custom="AWS" colour="#FF9900"></category>
</xml>注記色はAWSカラーです
App.tsxBlockly.defineBlocksWithJsonArray([
consoleLogBlock.define,
+ importBlock.define,
])ブロックの処理を記述
import.tsxexport const javascriptCode = function (block: Blockly.Block) {
const code = `import {
ListObjectsV2Command,
S3Client,
} from '@aws-sdk/client-s3'
`
return code
}App.tsxjavascriptGenerator[importBlock.blockType] = importBlock.javascriptCode
2. S3Clientを作成するブロック
ブロックを定義
createS3Client.tsxexport const define = {
"type": blockType,
"message0": "S3Client生成: アクセスキー %1 シークレットアクセス %2 リージョン %3",
"args0": [
{
"type": "input_value",
"name": "accessKeyId",
"check": "String",
},
{
"type": "input_value",
"name": "secretAccessKey",
"check": "String",
},
{
"type": "field_dropdown",
"name": "region",
"options": [
["アジアパシフィック (東京)", "ap-northeast-1"],
["米国東部 (バージニア北部)", "us-east-1"]
],
"check": "String",
}
],
"output": null,
"inputsInline": false,
"colour": "#FF9900",
"tooltip": "",
"helpUrl": ""
}注記リージョンはドロップダウンで選択できるようにしました
ツールバーにブロックを配置
App.tsxBlockly.defineBlocksWithJsonArray([
consoleLogBlock.define,
importBlock.define,
+ createS3ClientBlock.define,
])ブロックの処理を記述
createS3Client.tsxexport const javascriptCode = function (block: Blockly.Block) {
const value_accesskeyid = javascriptGenerator.valueToCode(block, 'accessKeyId', javascriptGenerator.ORDER_ATOMIC);
const value_secretaccesskey = javascriptGenerator.valueToCode(block, 'secretAccessKey', javascriptGenerator.ORDER_ATOMIC);
const value_region = block.getFieldValue('region');
const code = `new S3Client({
credentials: {
accessKeyId: ${value_accesskeyid},
secretAccessKey: ${value_secretaccesskey},
},
region: '${value_region}',
})
`
return [code, javascriptGenerator.ORDER_ATOMIC]
}
3. listObjectsを実行するブロック
ブロックを定義
listObjects.tsxexport const define = {
"type": blockType,
"message0": "オブジェクト一覧取得: S3Client %1 バケット名 %2",
"args0": [
{
"type": "input_value",
"name": "client"
},
{
"type": "input_value",
"name": "bucket",
"check": "String"
},
],
"output": "Array",
"colour": "#FF9900",
"tooltip": "",
"helpUrl": ""
}ツールバーにブロックを配置
App.tsxBlockly.defineBlocksWithJsonArray([
consoleLogBlock.define,
importBlock.define,
createS3ClientBlock.define,
+ listObjectsBlock.define
])ブロックの処理を記述
listObjects.tsxexport const javascriptCode = function (block: Blockly.Block) {
const value_client = javascriptGenerator.valueToCode(block, 'client', javascriptGenerator.ORDER_ATOMIC);
const value_bucket = javascriptGenerator.valueToCode(block, 'bucket', javascriptGenerator.ORDER_ATOMIC);
const code = `await (async (client, bucket) => {
const command = new ListObjectsV2Command({
Bucket: bucket,
});
var keys = []
try {
let isTruncated = true;
while (isTruncated) {
const { Contents, IsTruncated, NextContinuationToken } = await client.send(command);
Contents?.forEach((content) => {
if (content.Key) keys.push(content.Key)
})
isTruncated = IsTruncated !== undefined ? IsTruncated : false;
command.input.ContinuationToken = NextContinuationToken;
}
} catch (err) {
console.error(err);
}
return keys
})(${value_client}, ${value_bucket})
`
return [code, javascriptGenerator.ORDER_ATOMIC]
}注記無名関数を作ってawaitして返します
App.tsxjavascriptGenerator[listObjectsBlock.blockType] = listObjectsBlock.javascriptCode
完成
生成されるコードはこちら
var s3Client, objectList, j;
import {
ListObjectsV2Command,
S3Client,
} from '@aws-sdk/client-s3'
s3Client = new S3Client({
credentials: {
accessKeyId: 'key',
secretAccessKey: 'secret',
},
region: 'ap-northeast-1',
})
;
objectList = await (async (client, bucket) => {
const command = new ListObjectsV2Command({
Bucket: bucket,
});
var keys = []
try {
let isTruncated = true;
while (isTruncated) {
const { Contents, IsTruncated, NextContinuationToken } = await client.send(command);
Contents?.forEach((content) => {
if (content.Key) keys.push(content.Key)
})
isTruncated = IsTruncated !== undefined ? IsTruncated : false;
command.input.ContinuationToken = NextContinuationToken;
}
} catch (err) {
console.error(err);
}
return keys
})(s3Client, 'my-bucket')
;
for (var j_index in objectList) {
j = objectList[j_index];
console.log(j)
}
generate.js
として保存し、AWS SDKをインストールし、実行します。
pnpm i @aws-sdk/client-s3
node generate.js
オブジェクト一覧が取得できました。