20201016のNode.jsに関する記事は4件です。

VeeValidateの使い方(Vue + TypeScript + Vuetify)

実施内容

Vue(TypeScript)でVeeValidateを使ったバリデーションを実施したメモです。Vuetifyを使っています。Vueは、Vue.extend方式を使っています。あまり詳しい説明は記載できていません。

動作環境

  • Node.js v12.18.3
  • npm 6.14.8
  • vue-cli 4.5.6
  • vue 2.6.12
  • vuetify 2.2.11

使い方

インストール

npm install --save vee-validate

import

import Vue from 'vue';
import { ValidationProvider, ValidationObserver, extend } from 'vee-validate';
import { required, email } from 'vee-validate/dist/rules';
コンポーネント 用途
ValidationProvider 指定項目でバリデーションを行う
ValidationObserver 複数のバリデーションをグループ化する
extend ルールを編集する

ルールの指定

ルール一覧

sample.vue
// 編集なし(デフォルトのルール・メッセージを使用
extend('email', email);

// デフォルトのメッセージを上書き
extend('required', {
  ...required,
  message: '{_field_}は必須項目です。'
});

{_field_}には、ValidationProviderのname要素が割り当てられる。

メッセージは、上記のように使うルールだけ日本語に書き換える方法と、デフォルトで全て日本語にする方法がある。日本語化については、こちら
デフォルトのメッセージは、以下に記載されている。
英語→node_modules/vee-validate/dist/locale/en.json

componentsに登録

そのまま。

sample.vue
export default Vue.extend({
  name: 'Login',
  components: {
    ValidationProvider,
    ValidationObserver,
  },
  ...

v-text-fieldにバリデーションを追加

v-text-fieldvalidation-providerで囲む。

sample.vue
<validation-provider v-slot="{ errors }" name="User ID" rules="required">
  <v-text-field
    prepend-icon="mdi-account-circle"
    label="User ID"
    v-model="user.userId"
    :error-messages="errors"
  />
</validation-provider>

v-slotで、エラーメッセージなど様々な値を取得する。上記の例では、エラーメッセージを取得し、v-text-fieldのエラーメッセージに入力している。
ルールは複数指定可能。例 rules="required|email"


image.png

そのほかにv-slotで取得できる値はこちら

バリデーションのグループ化

この場合、v-formValidationObserverで囲む。

Sample.vue
          <ValidationObserver ref="observer" v-slot="{ invalid }">
            <v-form>
              <v-container>
                <validation-provider v-slot="{ errors }" name="User ID" rules="required">
                  <v-text-field
                    prepend-icon="mdi-account-circle"
                    label="User ID"
                    v-model="user.userId"
                    :error-messages="errors"
                    @input="input"
                  />
                </validation-provider>
                <validation-provider v-slot="{ errors }" name="Password" rules="required">
                  <v-text-field
                    prepend-icon="mdi-account-lock"
                    type="password"
                    label="Password"
                    v-model="user.password"
                    :error-messages="errors"
                  />
                </validation-provider>
              </v-container>
            </v-form>
            <v-btn type="submit" @click="doLogin" :loading="loading" :disabled="invalid">
              ログイン<v-icon>mdi-login</v-icon>
            </v-btn>
          </ValidationObserver>

この状態では、User IdとPassword二つのバリデーションをValidationObserverで確認する。ここでも、v-slotで、エラーメッセージなど様々な値を取得できる。上記の例では、invalidで二つのバリデーションが成功しているか確認し、ログインボタンの有効・無効を制御している(:disabled="invalid"の部分)。したがって、User IdとPassword両方を入力しないと、ボタンが有効化しない。

そのほかにv-slotで取得できる値はこちら

$refsを使ったValidationObserverの制御(引っかかったところ)

vueのmethod内でバリデーションをリセットしたい時などに、$refsを利用してValidationObserver Compornent APIにアクセスすることが必要になる。ここで、Typescriptを使っていると記載方法に癖がある。
公式ドキュメントに記載している方法では、Vue.extendのパターンには合わなかったので、以下の方法で実施した。

Sample.vue
    ...
    resetForm() {
      this.user.userId = '';
      this.user.password = '';
      (this.$refs.observer as InstanceType<typeof ValidationObserver>).reset();
    },

このように記載することで、reset()を使えるようになる。
今回の例だと、

<ValidationObserver ref="observer" v-slot="{ invalid }">

で検出したバリデーションのエラーを

(this.$refs.observer as InstanceType<typeof ValidationObserver>).reset();

でリセットすることになる。

これに時間かかった、、、

日本語化

最後に、メッセージを全て日本語化する。
公式ドキュメント

まず、tsconfig.jsonを編集する。

tsconfig.json
{
  "compilerOptions": {
    // ...
    "resolveJsonModule": true,
    "esModuleInterop": true
    // ...
  }
}

importその2+

localizeと、日本語メッセージが含まれるja.jsonをimportする。また、localizeで日本語を有効化する。

Sample.vue
// localize追加
import { ValidationProvider, ValidationObserver, localize, extend } from 'vee-validate';
// 追加
import ja from 'vee-validate/dist/locale/ja.json';

localize('ja', ja);

日本語のメッセージ一覧→vee-validate/dist/locale/ja.json

以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Electron上でVulkanを動かすプログラムを極限まで軽くしてみた結果

この話の続きです

https://qiita.com/nanikore55554/items/6da8a40dbc6258b819a2

要約すると「Electron上でVulkanを動かすプログラムを現実的な範疇まで軽くしてみた」という話です。

このプログラムをもっと軽くする事に挑戦しました。

1.そもそも何故重いのか

詳しい話は上のURLを見ていただくとして、この文が原因です。

pixels = new Uint8Array(bar.send_raw_image().buffer.slice(0, texWidth*texHeight*4));

このbar.send_raw_image()から呼び出されるBuffer型の配列からUint8Array型に変えている部分が重くなっている原因です。
Buffer型の配列はvulkanで生成されている画像の生データなのですが、20万程度の要素数があります。これだけ大量の要素数をコピーして新しい型の変数に変える処理をJavascript上でやればプログラムが遅くなるのは当然でしょう。

つまり、この部分を直接Uint8Array型に変えるプログラムを書けば軽くなるはずです。

2.どうやって生データの画像をを直接Uint8Arrayに変えられるのか

このbar.send_raw_image()は元々DLLにある関数で、Vulkanで生成された画像をスクリーンショットとしてCのconst char*型で返す関数です。
このbar.send_raw_image()をnode-ffi-napiというDLLから関数を呼ぶnode.jsのアドオンを経由して呼び出しています。
この際、const char*はBuffer型として返されており、Uint8ArrayにC++上で直接変換して呼び出すのはnode-ffi-napiでは不可能です。

ではどうするのかというと今回はnapiとnode-gypという物を用いてnode.jsのネイティブアドオンを作成する方法を取りました。

3.napiとnode-gypを経由してDLLを呼び出す

napiとは簡潔に言うとnode.jsのCやC++製のアドオンを作るためのプログラムであり、node-gypはそのアドオンをビルドするためのプログラムです。詳しいことは下記のURLを参考にすると良いと思います。

https://dev.classmethod.jp/articles/native-extension-using-n-api/

上にあるURLを元にDLLのネイティブアドオンを作りました。その際に使われるC++のソースコードを抜粋するとこうなります。(バグがあると思われる事とあくまで実験のため、Vulkanの終了処理を書いていませんので参考にする方はご注意ください)

sample.cpp
#include <napi.h>
#include <vector>
#include <DllHelloVulkan.h>

int width=800;
int height=600;

Napi::Value vulkanStart(const Napi::CallbackInfo& info ) 
{
    start();
    return info.Env().Null();
}

Napi::Value get(const Napi::CallbackInfo& info ) 
{
    //Vulkanで画像を生成する関数
    main_loop();
    //スクリーンショットを書く関数
    make_ss();
    //send_raw_imageは撮ったスクリーンショットを取り出す関数
    return Napi::TypedArrayOf<uint8_t>::New( info.Env(),
                                      4*width*height,
                                      Napi::ArrayBuffer::New(info.Env(), send_raw_image(), 4*width*height),
                                      0);
}

Napi::Object init( Napi::Env env, Napi::Object exports ) 
{
    exports.Set( Napi::String::New( env, "get" ), Napi::Function::New( env, get ) );
    exports.Set( Napi::String::New( env, "start" ), Napi::Function::New( env,vulkanStart) );
    //exports.Set( Napi::String::New( env, "main_loop" ), Napi::Function::New( env, main_loop ) );
    return exports;
}

NODE_API_MODULE( addon, init )

4.実際に作ったネイティブアドオンを使用した結果

実際にこのネイティブアドオンを使用するソースコードを書き、Electron上で動かしてみました。

すると、CPUの負担が約半分に減りました
大体30fpsで5~6%程度、60fpsで10~12%程度です。30fpsで8~10%、60fpsで20%以上CPUに負担がかかっていた前回と比べれば、かなりの進歩です。

おそらく、ElectronでVulkanを動かすプログラムでこれ以上速度を追求するのはほぼ不可能だと思います。

5.最後に分かった事

1.もうWebGPUいらなくね?
2.けどWebGLの力を借りているのでWebGPUの力を借りられればもっと高速になるのか?
3.うまくやればFlutterにも応用可能か?
4.作ったのはいいけど役に立つのかは微妙・・・なのか?(この辺の話は後ほど)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Electron上でVulkanを動かすプログラムを出来る限り軽くしてみた結果

この話の続きです

https://qiita.com/nanikore55554/items/6da8a40dbc6258b819a2

要約すると「Electron上でVulkanを動かすプログラムを現実的な範疇まで軽くしてみた」という話です。

このプログラムをもっと軽くする事に挑戦しました。

1.そもそも何故重いのか

詳しい話は上のURLを見ていただくとして、この文が原因です。

pixels = new Uint8Array(bar.send_raw_image().buffer.slice(0, texWidth*texHeight*4));

このbar.send_raw_image()から呼び出されるBuffer型の配列からUint8Array型に変えている部分が重くなっている原因です。
Buffer型の配列はvulkanで生成されている画像の生データなのですが、20万程度の要素数があります。これだけ大量の要素数をコピーして新しい型の変数に変える処理をJavascript上でやればプログラムが遅くなるのは当然でしょう。

つまり、この部分を直接Uint8Array型に変えるプログラムを書けば軽くなるはずです。

2.どうやって生データの画像をを直接Uint8Arrayに変えられるのか

このbar.send_raw_image()は元々DLLにある関数で、Vulkanで生成された画像をスクリーンショットとしてCのconst char*型で返す関数です。
このbar.send_raw_image()をnode-ffi-napiというDLLから関数を呼ぶnode.jsのアドオンを経由して呼び出しています。
この際、const char*はBuffer型として返されており、Uint8ArrayにC++上で直接変換して呼び出すのはnode-ffi-napiでは不可能です。

ではどうするのかというと今回はnapiとnode-gypという物を用いてnode.jsのネイティブアドオンを作成する方法を取りました。

3.napiとnode-gypを経由してDLLを呼び出す

napiとは簡潔に言うとnode.jsのCやC++製のアドオンを作るためのプログラムであり、node-gypはそのアドオンをビルドするためのプログラムです。詳しいことは下記のURLを参考にすると良いと思います。

https://dev.classmethod.jp/articles/native-extension-using-n-api/

上にあるURLを元にDLLのネイティブアドオンを作りました。その際に使われるC++のソースコードを抜粋するとこうなります。(バグがあると思われる事とあくまで実験のため、Vulkanの終了処理を書いていませんので参考にする方はご注意ください)

sample.cpp
#include <napi.h>
#include <vector>
#include <DllHelloVulkan.h>

int width=800;
int height=600;

Napi::Value vulkanStart(const Napi::CallbackInfo& info ) 
{
    start();
    return info.Env().Null();
}

Napi::Value get(const Napi::CallbackInfo& info ) 
{
    //Vulkanで画像を生成する関数
    main_loop();
    //スクリーンショットを書く関数
    make_ss();
    //send_raw_imageは撮ったスクリーンショットを取り出す関数
    return Napi::TypedArrayOf<uint8_t>::New( info.Env(),
                                      4*width*height,
                                      Napi::ArrayBuffer::New(info.Env(), send_raw_image(), 4*width*height),
                                      0);
}

Napi::Object init( Napi::Env env, Napi::Object exports ) 
{
    exports.Set( Napi::String::New( env, "get" ), Napi::Function::New( env, get ) );
    exports.Set( Napi::String::New( env, "start" ), Napi::Function::New( env,vulkanStart) );
    //exports.Set( Napi::String::New( env, "main_loop" ), Napi::Function::New( env, main_loop ) );
    return exports;
}

NODE_API_MODULE( addon, init )

4.実際に作ったネイティブアドオンを使用した結果

実際にこのネイティブアドオンを使用するソースコードを書き、Electron上で動かしてみました。

すると、CPUの負担が約半分に減りました
大体30fpsで5~6%程度、60fpsで10~12%程度です。30fpsで8~10%、60fpsで20%以上CPUに負担がかかっていた前回と比べれば、かなりの進歩です。

おそらく、ElectronでVulkanを動かすプログラムでこれ以上速度を追求するのはほぼ不可能だと思います。

5.最後に分かった事

1.もうWebGPUいらなくね?
2.けどWebGLの力を借りているのでWebGPUの力を借りられればもっと高速になるのか?
3.うまくやればFlutterにも応用可能か?
4.作ったのはいいけど役に立つのかは微妙・・・なのか?(この辺の話は後ほど)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsで「並列実行処理を一定間隔で繰り返す」

はじめに

自身の業務上の課題を解決するために、Node.jsのスクリプトを作りました。
そのメモです。

背景

ある会社にて、社員向けの業務システムがあります。
そのシステムでは、年に一度、社員情報の初期化を行う必要があります。
社員情報を初期化するには、社員情報更新APIを使用します。
1回のレスポンスタイムは1~2秒かかります。
そのAPIでは、引数に社員リストを指定できません。
1社員あたりにつき、1回APIを呼ぶ必要があります。
また、そのシステムで管理している社員は10,000人とします。

さて、10,000人分の社員情報初期化を考えると…

  • 10,000回のAPI呼び出しを同時実行 → サーバー側で処理を捌ききれない
  • 10,000回のAPI呼び出しを順次実行 → 時間がかかりすぎる(1回あたり2秒とすると、20,000秒 = 5時間)

つらい。

サーバーへの過剰負荷を避けつつも、効率的に全ての処理を完遂したい。

方針

APIの呼び出し方をクライアント側で工夫します。

10,000回のAPI呼び出しを同時に実行するのでもなく、順次に実行するのでもなく、中間をとります。

例えば、2,000回ずつのAPI呼び出し処理に分けて、2秒間隔で繰り返し実行する(10,000回に到達するまで)、というイメージ。

つまり、並列実行処理を、一定の間隔で繰り返します

これによって、サーバー側への過剰負荷を避けながら、効率的に処理を完遂することを目指します。

加えて、API実行エラーがあったら、その社員に対する処理はリトライしたいので、エラーが発生したAPI呼び出しは結果として出力できるようにします。

※1回のAPIでエラーが発生しても後続の処理に影響はないものとします。

サンプルプログラム(Node.js)

目的を実現するためのNode.jsスクリプトを準備しました。
Promise・await/async・resolveあたりを駆使して何とかできそうです。

尚、このスクリプトは並列実行処理を一定間隔で繰り返すためのサンプルプログラムであり、実際にはAPI呼び出しは行っていません。
最小単位の処理として、1秒待機するだけの処理をdoUnitTaskとして定義しています。
さらに、1%の確率でエラーが発生する処理として仮定しています。

example.js
// 最小単位のタスク(次の状況を想定したダミーの処理)を定義する。
// - 1秒の遅延が発生する
// - 1%の確率でエラーが発生する
function doUnitTask(num) {
    return new Promise(function (resolve, reject) {
        setTimeout(function(){ 
            if (Math.random() <= 0.01) {
                // ※Promise.all().catch()は最初のrejectのみキャッチされる。
                //  複数のエラーハンドリングに対応できるように、rejectは使わずにresolveで例外を発生させる。
                resolve(new Error(`Error occured! [id: ${num}]`));
                // reject();
            } else {
                resolve(); 
            }
        }, 1000);        
    });
}

async function main() {
    // 全タスク数
    let TASK_NUM = 1000;

    // いくつのタスクに分割するか
    let DEVIDE_LENGTH = 100;

    // タスク群を実行する間隔[ms]
    let DURATION = 2000;

    // 全てのタスクを実行するための配列を用意
    let allTasks = [];
    for (let i = 0; i < TASK_NUM; i++){
        allTasks.push(i)
    }

   // 指定した数毎にタスクを分割する
    let dividedAllTasks = []
    for(let i = 0; i < allTasks.length; i += DEVIDE_LENGTH){
        dividedAllTasks.push(allTasks.slice(i, i + DEVIDE_LENGTH));
    }

    // 「複数タスク」を「定期的」に「並列実行」する
    let successedTaskNum = 0
    let erroredTaskNum = 0
    for (let i = 0; i < dividedAllTasks.length; i++){
        let currentTasks = dividedAllTasks[i]

        const promises = [];
        for (let j = 0; j < currentTasks.length; j++){
            let unittask = currentTasks[j];
            promises.push(doUnitTask(unittask));
        }

        await new Promise(function(resolve, reject) {
            setTimeout(function() {
                Promise.all(promises)
                    .then(function(values) {
                        values.filter(function(item){
                            // エラーハンドリング
                            if (item instanceof Error) {
                                console.log(item.message)
                                erroredTaskNum++;
                            } else {
                                successedTaskNum++;
                            }
                        })
                        // 進捗率を表示
                        console.log(`Proccessing...(${Math.round((successedTaskNum + erroredTaskNum) / TASK_NUM * 100)}%)`)
                        resolve()
                    })
                    // ※Promise.all().catch()は最初のrejectのみキャッチする。
                    //  複数のrejectに対応できないため使用しない。
                    // .catch(function(reason){
                    //     console.log(`${reason}`)
                    // })
                }, DURATION)
        });
    }

    console.log(`all done.`)
    console.log(`SUCCESS TASK NUM : ${successedTaskNum}`)
    console.log(`ERROR TASK NUM   : ${erroredTaskNum}`)
}

main()

実行例

$ node example.js
start.
Error occured! [id: 168]
Error occured! [id: 183]
Error occured! [id: 319]
Error occured! [id: 551]
Error occured! [id: 580]
Error occured! [id: 838]
Error occured! [id: 943]
Proccessing...(10%)
...(略)...
Proccessing...(90%)
Error occured! [id: 9152]
Error occured! [id: 9281]
Error occured! [id: 9814]
Error occured! [id: 9860]
Error occured! [id: 9972]
Proccessing...(100%)
all done.
SUCCESS TASK NUM : 9899
ERROR TASK NUM   : 101

さいごに

はやく業務で使ってみたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む