23.6.14 【新機能】フレームワーク2

Buddyではアプリの構成要素のうちBuddy側で用意しているもの(例えばスクリーンモジュールなど)を総称してフレームワークと呼んでいます。フレームワークにユーザーが作成したデータベースやスクリーンなどの設計情報が組み合わされて、実際に動作するアプリとなります。2023年6月のアップデートで、フレームワークの新しいバージョンをリリースしました。従来のフレームワークをフレームワーク1、新しいフレームワークをフレームワーク2と呼んでいます。

フレームワーク2はフレームワーク1との互換性は考慮せずに一から設計し直して、より使いやすくなるようにしました。その基本的な考え方は、従来のフレームワーク1ではどうしても一定量のJavascriptのスクリプトを書く必要があった機能について、その必要をなくしたり、簡潔でわかりやすい書き方にすることです。具体的には次のような特徴があります。

・スクリーンスクリプトの書き方がシンプルになりました。Buddyが提供する様々な機能は全てbuddyというオブジェクトから利用できます。
・データベース操作などの非同期処理が最近の主流であるプロミスを使用した方式になり、簡潔でわかりやすい書き方ができるようになりました。
・データベースとモジュールの結びつきを扱うDataLinkerという仕組みが導入され、非常に簡潔に書けるようになりました。
・スクリーンテンプレートが一新され、作成後のスクリーンにモジュールの追加・削除を行った際にも、スクリプトはできるだけそのままで動作するようになりました。
・新しいスクリーンテンプレート「一覧」「閲覧・入力」が追加され、よりシンプルな画面構成が可能となりました。これを利用したアプリをExcelデータから簡単な操作で作成できるようになりました。
・スクリーンモジュールはすべてフレームワーク2用に一新されました。使用頻度の高い基本的なモジュールのみが標準モジュールとなり、それ以外はプラグインとして必要な時にリポジトリから取り込んで使用するようになりました。今後、特別な機能を持ったモジュールもプラグインとして提供し、作成できるアプリの幅を広げていく予定です。

詳しくは開発ガイドやプログラミングガイドなどのマニュアルに記載していますが、この記事ではプロミスによる非同期処理、DataLinker、プラグインについて簡単に紹介します。

○プロミスによる非同期処理

プロミスは非同期処理を扱うための比較的新しい仕組みです。従来はJavascriptでの非同期処理はコールバック方式で、フレームワーク1でもコールバック方式でした。フレームワーク2ではプロミス方式に変更されています。

例えばフレームワーク1でデータベーステーブルtable1からID1のレコードを読み出してnameカラムの値を表示するには次のようにします。(エラー処理については省略しています。)

const table = this.tables["table1"];
table.readData({where: {ID: 1}}, (function(error, data) {
	this.items.TEXTBOX1.setvalue(data[0].name);
}).bind(this));

フレームワーク2では次のようになります。

const table = buddy.app.findModel("table1");
const reulst = await table.select().where({ID: 1});
buddyscreen.items.TEXTBOX1.value = result.rows[0].name;

フレームワーク1ではreadData()のコールバック関数の中で、読み出した結果を得て、それを表示する処理をしています。それに対してフレームワーク2ではselect()はプロミスを返し、プロミスの処理終了を待ってその結果を得るキーワード「await」を利用することで、「result = await …」と結果を得ています。

上記の例のように単独の処理であればどちらの書き方でも大差ないように見えますが、いくつかの非同期処理を順次行いたい場合には大きな違いがあります。例えばtable1のnameカラムに「サンプルA」「サンプルB」「サンプルC」という値をこの順に入れるようにレコード追加したいとします。フレームワーク1では次のようになります。

const table = this.tables["table1"];
table.insertRecord({data: {name: "サンプルA"}}, (function(error, result){
	table.insertRecord({data: {name: "サンプルB"}}, (function(error, result){
		table.insertRecord({data: {name: "サンプルC"}}, (function(error, result){
		}).bind(this));
	}).bind(this));
}).bind(this));

コールバック関数の中で次のinsertRecord()を実行する必要があるので、このようにコールバックの入れ子になります。省略しているエラー処理もおこなうようにすると、とても複雑なスクリプトになります。

フレームワーク2では次のようになります。

const table = buddy.app.findModel("table1");
await table.insert({name: "サンプルA"});
await table.insert({name: "サンプルB"});
await table.insert({name: "サンプルC"});

awaitで処理終了まで待つことができるので、上記のように単純に書き並べることで順次処理ができ、非常にわかりやすいスクリプトになります。

○DataLinker

DataLinkerはスクリーンモジュールとデータベースとの仲立ちをしてくれる仕組みです。スクリーンに配置したテキストボックスなどのモジュールの「データリンク」という属性で、そのモジュールとどのデターベーステーブルやビューとそのカラムが対応するかを指定しておきます。すると、そのテキストボックスなどの値をデータベースから読み出した値にセットしたり、逆に入力された値をデータベースに保存したりする処理は、DataLinkerが面倒を見てくれます。

例えばデータベーステーブルtable1のID1のレコードを読み出して、その値をスクリーンモジュールの「データリンク」でtable1が指定されているものにセットするには、次のようにします。

const tableName = "table1";
const table = buddy.app.findModel(tableName);
const data = await table.select().where({ID: 1});
const linker = new buddy.lib.DataLinker(buddy, tableName);
linker.set(data);

逆にテキストボックスなどの入力用のスクリーンモジュールに入力された値を、データベーステーブルにレコードとして追加するには、次のようにします。

const tableName = "table1";
const linker = new buddy.lib.DataLinker(buddy, tableName);
await linker.insert();

「linker.insert()」だけで、スクリーンモジュールからの値の収集と、それをデータベーステーブルにinsert()する処理をまとめて行ってくれます。

○プラグイン

フレームワーク1では、テキストボックスやボタンなどの基本的なものから、地図やカレンダーなどの複合的なものまで、様々なスクリーンモジュールが用意されています。Buddyの応用範囲も広がるにつれて、Buddyのアップデートによって少しずつ新たなモジュールが加わったり、モジュールの機能が強化されたりしてきました。これにより、次第にモジュールが肥大化し、アプリが重くなる要因となる恐れが出てきました。

そこでフレームワーク2では、標準で用意するモジュールは基本的なものに絞り、使用頻度が低いと思われるものはプラグインとして必要なときにアプリに取り込む仕組みとしました。フレームワーク1にあったモジュールがプラグインに移ったものもありますが、新しく作られたプラグインモジュールもあります。今後さらに充実させて、Buddyでのアプリ制作の幅を広げていく予定です。

以上、かいつまんでフレームワーク2をご紹介しました。現時点ではベータバージョンであり、未実装の機能もありますが、今後完成度を高めていきます。ぜひ試していただければと思います。

(2023/6/14 中島)

22.9.13 【補完テクニック】画像をクライアント側で処理する

扱うデータに画像を含むようなアプリを作る場合、クライアント側にある画像ファイルあるいはスマホなどであればその場で撮影した画像をサーバーにアップロードして保存することになります。最近のスマホのカメラは大変解像度が高くなり、そのままだとサイズが大きすぎるので、縮小処理したい場合も多いと思います。

Buddyではサーバーに保存された画像ファイルを縮小処理する方法も用意されていますが、クライアント側で縮小してからサーバーにアップロードすることができれば、通信量が減るのでよりよい方法となります。

これを実現するには、
1)選択した画像の内容を読み取る。
2)画像を縮小する。
3)縮小した画像をアップロードする。
という処理をJavascriptでおこなう方法を知る必要があります。
具体絵的には、1)はFileReaderクラスの使い方、2)はcanvasの機能、3)は画像データをアップロードに必要なFileオブジェクトにする方法、そして共通するテクニックとしてデータURLの扱い、ということになります。これらをすべて学んで適切に組み立てるのは結構面倒です。そこで、必要な機能をまとめたライブラリファイルとしてimagefiletool.jsを作成しました。

imagefiletool.js です。← このファイル名部分をクリックしてダウンロードしてください。

imagefiletool.jsを、アプリのfiles/javascriptsフォルダに入れることによって、スクリーンのスクリプトでImageFileToolというクラスと、ImageUtilというオブジェクトが使えるようになります。これらには次の機能があります。

new ImageFileTool() で得たオブジェクトで次のメソッドを使う
  readFile() 画像ファイルのFileオブジェクトから読み取り、縮小して内部のキャンバスに描画
  getSrc() その画像を表示するsrc,width,heightを得る
  fileForUpload() その画像をアップロードするFileオブジェクトを得る
次のユーティリティもある。これはImageFileToolとは独立して使える。
  ImageUtil.getSize() 画像の幅と高さを得てコールバックで返す
  ImageUtil.calcShrinkSize() 幅と高さを指定の最大長辺値内になるように計算する

スクリーンで、ファイル選択モジュールFILE1、画像モジュールIMAGE1、ボタンBUTTON1、が配置されているとして、FILE1で画像を選択したら縮小処理するとともに画像をIMAGE1に表示し、BUTTON1をクリックしたらアップロードする、というスクリプトは次のようになります。

	// 画像ファイル選択時
	FILE1_onChange: function(evt){
		var files = this.items.FILE1.getFiles();
		if(files.length != 1) return;
		this.ift.readFile(files[0], (function(err){
			if(err) return console.log(err);
			var src = this.ift.getSrc();
			this.items.IMAGE1.setSrc(src.src);
			this.items.IMAGE1.setStyle({width: src.width, height: src.height});
		}).bind(this));
	},
	// ボタンクリック時
	BUTTON1_onClick: function(evt){
		var file = this.ift.fileForUpload();
		var dir = 'files/…';  // アップロード先のフォルダ
		api.request.upload(api.constants.appName, dir, [file], (function(error, result){
			// アップロード結果にもとづく処理
			…
		}).bind(this));
	},
	// スクリーンロード時
	onLoad: function(){
		// maxはアップロードする縮小画像の長辺サイズ、showmaxは表示する縮小画像の長辺サイズ
		this.ift = new ImageFileTool({max: 800, showmax: 150});
	},

このように、縮小処理とアップロードを簡単に行うことができます。

なお、カメラで撮影した画像を読み取った場合、右や左に回転した状態の画像となる場合があります。imagefiletool.jsではこれを補正する処理も行っています。興味のある方はソースコードをご覧ください。

(2022/9/13 中島)

22.2.2 【プログラミングの罠】ES6とBabelとIE

Javascript言語は、もともとは各Webブラウザでの違いが大きかったのですが、現在では標準化が進んでいます。標準化された名称がECMAScriptです。その第6版が通称としてES6と呼ばれ、規格としての正式名称はECMAScript2015です。その後のさらに新しい規格としてECMAScript2017があります。これらの最近の規格では、Javascript言語の弱点を補う改良が施されているので、ぜひ活用したいものです。ここでは主な機能だけ紹介しますが、ECMAScript2015やECMAScript2017と検索していただくと解説しているサイトがいろいろ見つかると思います。

Buddyアプリのスクリーンスクリプトはブラウザ上で実行されるので、使える機能はそのブラウザのJavascript言語の実装に依存することになります。Edge、Chrome、Firefoxといったブラウザはみな最近のJavascriptの規格に対応していますが、IE(Internet Explorer)は基本的に対応していません。

しかし、IEでもES6の新しい機能を使えるようにするBabelという仕組みがあります。これはES6の文法で書かれたスクリプトを古いES5で実行できるスクリプトに変換してくれるものです。Buddyではアプリ生成の際のオプションでBabelを使用するように指定することができます。ただし、変換されたスクリプトが実行されることになるのでデバッグの際にブラウザでソースコードを見ると元のスクリプトとは異なることになって、わかりにくくなるという難点はあります。

ES6になれた方だとBuddyアプリの開発でも下記でご紹介するようなES6の機能を使ったスクリプトを自然に書いてしまう場合もあるでしょう。それをBabelを使用しないでアプリとして生成すると、Edge、Chrome、Firefoxでは問題ないが、IEでは動かないということになりますので注意が必要です。アプリ開発の一般的な注意事項ですが、そのアプリはどのブラウザで利用するのかをあらかじめきちんと決めておくこと、そして対象のブラウザでテストすることが重要です。IEを対象外とできる場合は、ES6をどんどん使うことができ、Babelを使用する必要もなくなります。

以上が今回の「プログラミングの罠」ですが、ES6の新しい機能が具体的にわからないとピンとこないかもしれません。Buddyのアプリ開発でもすぐに役立ちそうな点に絞っていくつかご紹介します。

letとconst

変数の定義は従来はvarで行いますが、varには、何度も同じ変数を定義できる、スクリプト全体から見える、という大きな弱点があります。

var a = 1;
if(true) {
	var a = 2;
}
console.log(a);

この場合、最初の var a と、ifの中の var a は同じ変数になり、最後のconsole.logは 2 と表示されることになります。変数の定義は、本来は一度だけ適切な場所で行われるべきもので、そうでない状況はバグの原因になります。しかしコピーアンドペーストで編集したりすると同じ名前の変数を二箇所以上で定義している状況が生まれることがあり、var ではこれがエラーにならないため、気づきにくいのです。

letはvarと同様に変数を定義しますが、指定の変数名が既存だとエラーになります。また、{ } の間であればその中に有効範囲が限定されます。

let a = 1;
if(true) {
	let a = 2;
}
console.log(a);

このように先ほどの var を let に返ると、このスクリプトはifの中の let a のところで「a はすでにあるよ」という意味のエラー二なります。

let a = 1;
if(true) {
	let b = 2;
}
a = b;
console.log(a);

こうすればletでのエラーは起こりませんが、a = b; のところで「bは未定義」という意味のエラーになります。let b はifの{ }の中にあるため、その中だけで有効な変数となり、外側で使おうとするとこうなるわけです。
var だとあるところで定義して使った変数が、無関係なところでも見えてしまってバグの原因になることがありますが、let ではそれが防げます。

const は let と同様ですが、定義した変数の値を変更することができないという違いがあります。

const c = 1;
c = 2;

これは c = 2; のところでエラーになります。変更されるべきでないものは const にしておくと安心です。

アロー関数

Javascriptでは無名関数をよく使います。代表的なものが非同期処理でのコールバックです。例えばBuddyではDBテーブルからデータを読み出すreadDataは次のようになります。

this.tables.table1.readData(options, function(err, data){
	…
});

この function(err, data){ … } が無名関数です(通常の関数定義であれば function の後の括弧の前にある関数名がありません)。readDataの処理が終わるとこの無名関数が呼ばれ、引数のerrやdataで結果を得ることができます。
これをES6のアロー関数で書くと次のようになります。function を省略して ) と { の間に => を書きます。

this.tables.table1.readData(options, (err, data) => {
	…
});

これだけだと、書き方が簡便になったくらいのことですが、従来の無名関数とはthisの扱いが異なります。従来の無名関数ではthisは関数を呼び出したオブジェクトですが、アロー関数では定義時のthisになります。
例えば上記の例で、得られたレコード数をLABEL1にセットして表示したい場合、従来の無名関数では、

this.tables.table1.readData(options, (function(err, data){
	this.items.LABEL1.setValue(data.length);
}).bind(this));

のように、bind(this) を付ける必要があります。しかしアロー関数では、

this.tables.table1.readData(options, (err, data) => {
	this.items.LABEL1.setValue(data.length);
});

と、bind(this)無しでOKで、非常にすっきりします。

テンプレート文字列

Javascriptでは文字列はシングルクォート「’」またはダブルクォート「”」で囲みます。ES6ではこれに加えてバッククォート「`」で囲むことができ、その場合は ${変数名} で変数の値を埋め込むことができます。また途中に改行を入れることもできます。

従来であれば、

const name = '名前';
const age = 25;
const s = "名前: " + name + "\n年齢: " + age + "\n";

と書くところを、

const name = '名前';
const age = 25;
const s = `名前: ${name}
年齢: ${age}
`;

と書くことができます。

いくつか限定してご紹介しましたが、ES6の新しい機能はこのほかにもたくさんありますので、検索して調べてみてください。

(2022/2/2 中島)