JavaScript

JavaScript製のStore管理フレームワークを作りました


作ったもの

javascript製の、Store管理フレームワークです。

ブラウザなどで、使うことができます。(古いブラウザでは、動作しないかもしれません。)





画像は、こちらのサービスを使いました。

※フレームワークと言っていいものか分からないですが、この記事ではフレームワークとしておきます。ご了承ください。🙇‍♂️


作った経緯

普段React.jsをよく使っていて、Stateの管理にReduxを使ったことがあるのですが、

データフローを理解するのに時間が掛かったのと、データをまとめて管理するため、必要なデータを分けるのがすごく難しく感じました。

どうにか簡単にデータを管理できないかと思っていた時に、Typescriptのinterfaceみたいに、

「あらかじめデータの型を定義して置いて、それを使ってデータ管理をしよう!」という発想になりこのフレームワークを作るに至りました。


解説

早速ですが、作ったフレームワークの解説をしてこうと思います。


Maker

Maker は、フロントと Store 部分を繋ぐ仲介役です。 Store の更新や、変更の検知などはこの Maker を介して行われます。

以下は、使用例です。YourStateClassChildStateなどが出てきますが、それらは、Stateクラスを継承したクラスです。


使い方


makerの使い方

import Golgua from "golgua";

const maker = Golgua.createMaker( YourStateClass ); // Maker インスタンスを取得

maker.listen( value => console.log(value) ); // { message: Hello Golgua, child_id: 10021 }

maker.listenWithState(ChildState, child_id => {
console.log(child_id); // 10021 <- update での更新
// 19819 <- updateWithState での更新
});

maker.update({ message: "Hello Golgua", child_id: 10021 });

maker.updateWithState( ChildState, 19819 );



createMaker

createMakerは、Stateクラスを継承したクラスを第一引数にもらって Makerインスタンスを返します。


createMaker

const maker = Golgua.createMaker( YourStateClass );



listen

listenは、データの更新が完了した時に引数に渡されたコールバックを実行します。コールバックの引数には更新した値が渡されます。


listen

maker.listen( listen_callback );



listenWithState

listenWithStateは、第一引数にStateクラスを継承したクラスのStoreが変更された時のみ、第二引数に渡されたコールバックを実行します。コールバックの引数には更新した値が渡されます。


listenWithState

maker.listenWithState( YourStateClass, listen_callback );



update

updateは、引数に渡された値で更新を試みます。この時点では、更新されるかはわからないので注意してください。


update

maker.update( update_value );



updateWithState

updateWithStateは、createMakerで渡したStateクラスの子のStateクラスのStoreのみを更新します。第一引数には、


updateWithState

maker.updateWithState( YourStateClass, update_value );



Types

Typesは、StateでのStoreのデータ型を決める関数群です。特定の型を実行することで、Storeの型を定義します。

また、デフォルト値の設定やバリデートなどの昨日もあります。

今の所は、以下の型に対応してます。( 今後増やす予定です )


  • string型

  • number型

  • boolean型

  • object型

  • array型


types定義の例

import { Types } from "golgua";

const types = {
string: Types.string({ default_value:"Hello Golgua!" }),
number: Types.number({ pattern: v => v < 10 }),
boolean: Types.boolean({ nullable: false, default_value: false }),
object: Types.object({
types: {
message: Types.string()
}
}),
array: Types.array({
types: Types.string(),
empty: false,
default_value: ["Default Message"]
})
}


上記のコードは、Typesが持つ関数をそれぞれ実行して関数名と同じ型のデータを定義してます。また、引数にdefault_valuepatternなど、オプションを指定して実行することでデフォルト値の設定や更新する時にバリデートをかけたりすることができます。

関数を実行するとTypesインスタンスを返します。

対応しているオプションは、以下のとおりです。


Types.stringTypes.numberTypes.booleanの場合



  • default_value : デフォルトの値を設定する時に使います。実行した関数と違う型の値を設定するとエラーになります。( 初期値は、nullです。)


  • pattern : データが更新される際に、このpatternに渡された関数が実行され、関数がfalseを返した時は、データを更新しないようにします。


  • nullable : nullを許容するかのフラグです。falseに設定すると、nullが値に設定された時にエラーになります。( 初期値は、trueです )


Types.objectTypes.arrayの場合



  • default_value : 上記と同じです。


  • pattern : 上記と同じです。


  • nullable : 上記と同じです。


  • types : 必須です。objectの場合は、Typesインスタンスのみを含むプレーンなobjectのみ指定可能で、arrayの場合はTypesインスタンスのみ指定可能です。


  • empty : 空の値を許容するかのフラグです。falseにすると、値が空の時にエラーになります。( 初期値はfalse )


State

Stateは、Typesによって定義されたデータ型を保持し、そのデータ型に沿ったStoreを保持します。また、ライフサイクルを定義することによって、データの更新にフックすることができます。

Stateは、Stateクラスを継承して定義できます。

import { State } from "golgua"

class SometingState extends State {}


types

typesは、Stateのインスタンス変数です。この変数にTypesで作ったデータ型を設定することで、Storeのデータ型を定義できます。

typesに設定できる値は、Typesインスタンスか、Typesインスタンス又はStateクラスを含むプレーンなオブジェクトだけです。

以下に例を示します。↓


Typesインスタンス

import { State, Types } from "golgua";

class SometingState extends State {
constructor(){
super();

this.types = Types.string()
}
}



Typesインスタンスを含むプレーンなobject

import { State, Types } from "golgua";

class SometingState extends State {
constructor(){
super();

this.types = {
message: Types.string()
};
}
}



TypesインスタンスとStateクラスを含むプレーンなobject

import { State, Types } from "golgua";

class UserState extends State {
constructor(){
super();
this.types = {
name: Types.string(),
age: Types.number()
};
}
}

class SometingState extends State {
constructor(){
super();

this.types = {
message: Types.string(),
user: UserState
};
}
}



state

stateは、Stateクラスのインスタンス変数です。Storeのデータを保持します。この変数は、参照用なので代入などはしないようにしてください。


this.stateの使用例

class SomethingState extends State {

/* - 省略 - */
constructor(){
super();

this.types = Types.string({ default_value: "Default Message" });

console.log(this.state); // null この時点では、まだnull

this.state = "message"; // NG ☠️
}

init(){
console.log( this.state ); // Default Message
}
}



defaultValue

this.typesで設定したdefault_valueの値を取得することができます。


defaultValueの使用例

import {State,Types} from "golgua";

class SomethingState extends State {
constructor(){
super();
this.types = Types.stirng({ default_value: "Hello Golgua!" });
}

init(){
console.log(this.defaultValue()); // Hello Goglua!
}
}



ライフサイクル

ライフサイクルは、以下の4つです。


  • init

  • willUpdate

  • didUpdate

  • updatedCatch


init

Stateクラスがnew(インスタンス化)された時に、一度実行されます。また、Promiseを返すとresolveで渡された値で、Storeのデータを更新します。


initの使用例

class SomethingState extends State {

/* - 省略 - */
async init(){
return await ajax_data(); // 通信結果で、Storeのデータを更新
}
}


willUpdate

Storeのデータが、更新される前に実行されます。また、この関数で返した値がthis.typesで設定した型で判定され、判定が通れば次のStoreのデータとして内部で設定されます。

第一引数には、maker.updateで渡された値が入ってきます。


willUpdateの使用例

import Golgua, { State, Types } from "golgua";

class SomethingState extends State {
constructor(){
this.types = Types.string();
}

willUpdate(props){
console.log(props); // Hello
return props + " Golgua!"; // この場合は、string型以外を返すとエラーになる
}
}

const maker = Golgua.createMaker(SomethingState);
maker.listen(message => console.log(message)); // Hello Golgua!
maker.update("Hello");



didUpdate

Storeのデータの更新が完了した時に呼ばれます。


didUpdateの使用例

import Golgua, { State, Types } from "golgua";

class SomethingState extends State {
constructor(){
this.types = Types.string();
}

init(){
console.log(this.state); // null
}

willUpdate(props){
console.log(props); // Hello
return props + " Golgua!"; // この場合は、string型以外を返すとエラーになる
}

didUpdate(){
console.log(this.state); // Hello Golgua!
}
}

const maker = Golgua.createMaker(SomethingState);
maker.update("Hello");



updatedCatch

このライフサイクルは、更新が失敗した時に呼ばれます。

第一引数には、失敗した時の値。第二引数には、objectkey名arrayindexが渡されます。無い場合は、nullが入ります。

第三引数には、更新データ全体が入っています。

以下は、Types.numberに設定したpatternが実行されfalseを返したために更新されなかった時の例です。


updatedCatchの使用例

import Golgua, { State, Types } from "golgua";

class SomethingState extends State {
constructor(){
this.types = {
id: Types.number({ pattern: v => v < 10 }),
text : Types.string()
};
}

updatedCatch(value, key, props){
const log = `value:${value}, key:${key}, props:${JSON.stringify(props)}`;
console.log( log ); // value: 100, key: id, props: { id: 100, text: "Hello" }
}
}

const maker = Golgua.createMaker(SomethingState);
maker.update({ id: 100, text: "Hello" });



まとめ&感想

今回 javascript でフレームワークを作ってみましたが、

発想から実装に至るまで体験してみて、フレームワーク(ライブラリ)の作成の難しさを身にしみて感じました。

「どうすれば実装しやすく、管理しやすくなるか?」、「どこまでをフレームワーク側で管理するか?」など、普段のプログラミングとはまた違った難しさなどがありました。

またそれと同時に、今あるフレームワークやライブラリの有り難みや凄さと言ったものも感じることができ、私としてはとても有意義な時間だったと思います。

今回作ったフレームワークは、これからもメンテナンスをして行き、もっと使いやすくしていこうと思います。(できれば、githubでスター100個ぐらいは取れるまでにはしたい。。。)

最後に、このフレームワークはOSS(オープンソースソフトウェア)で開発していこうと思います。なので、誰でもお気軽に開発に参加できます!

もし、この記事を見て 「参加してもいいよ!」 と言っていただける方がいましたら、参加してくれると嬉しいです😆

私自身、javascriptを初めて1年半くらいの若輩者なので、アドバイスなど色々教えてくれると助かりますし、

私と同じで、javascriptの経験が浅い人も一緒に参加して共に技術の向上を目指そうとも思っていますので、熟練者から初心者まで誰でも参加して大丈夫です!

ここまで読んで下さってありがとうございます。この記事を読んで質問などがあれば、お気軽にどうぞ。それでは👋