20191218のJavaScriptに関する記事は30件です。

Javascript復習①

Javascriptの復習もかねて記事を書いていきます。
今日は関数と配列について。

関数の書き方

①関数宣言

function wanko(){
         }

wankoという関数名をつけて関数宣言しています。
おなじみですね。

②関数式

const wanko = function(){
      }

const wanko = () => {
      }

無名関数というやつです。
関数はデータの方として存在しているのでこのような書き方が可能になります。

Functionオブジェクトのオンストラクタを使った書き方もあるのですが、ほぼ間違いなく使わないので省略します。

書き方

const p = document.createElement('p');
p.textContent = 'wanko';
documen.body.appendChild(p);

これを関数にすると

function toypoo(){
    const p = document.createElement('p');
    p.textContent = 'wanko';
    documen.body.appendChild(p);
}

コード自体はdocument.createElment(p)でp要素を作成
p要素のtextContentをwankoに
bodyにp要素を付与しています。

toypooという関数が出来ましたね、toypoo();で呼び出せます。

ボタン
このように書くとボタンを押したらtoypoo関数が呼び出されます。

HTMLファイルが読み込まれたらwankoと表示します。
window.onload = function(){
    console.log('wanko');
}

window.addEventListner('load', function(){
    console.log('wanko');
})

引数

Javascriptでは引数を複数指定できます。

function wanko(pome,toypoo){

    }

引数にpomeとtoypooを指定することで関数内でpomeとtoypooが使えるようになります。

wanko(1,2);

それぞれpomeとtoypooに対応しています。

引数にコールバック関数

function wanko(callback){
    setTimeout(function(){
        console.log('wanko');
        callback();
    }, 2000)
}

引数にcallback関数を指定しているので2秒経過後に引数に指定した関数が実行されます。

returnで返り値

function wanko(toypoo, pome){

    return toypoo+pome;
}

if(wanko(7,5) > 3){

 console.log('wan');
}  else {
       console.log(wanwan);
}

returnで返り値を戻すことで普通の値として関数を使えるようになりました。

続く

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

JavaScriptで配列内の文字列から最も文字数の小さい要素を取得する方法。

はじめに

短く書くにはどうすればいいか調べたのでまとめたよ。

ゴール

index.js
const string = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.";

function findShortWord(s){
    return Math.min(...s.split(" ").map (s => s.length));
}

console.log(findShortWord(string));

概要

Math.min()

スプレッド構文(...)

String.prototype.split()

Array.prototype.map()

さいごに

  • スプレッド構文(...)をちゃんと勉強したけど、こんなに便利だと思わなかった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsのバージョンを上げた際のnode-sassのビルドエラー

ほんとにどうってこと無いメモです。

久々に開発しようとしたNuxtJSプロジェクトで、node-sassがコケました。
node-sassはv4.13.0です。

$ yarn dev

・
・
・

● Client █████████████████████████ building (61%) 431/466 modules 35 active
 node_modules/markdown-it/lib/rules_core/state_core.js

✖ Server
  Compiled with some errors in 9.34s


✖ Client
  Compiled with some errors in 13.93s

✖ Server
  Compiled with some errors in 9.34s


 ERROR  Failed to compile with 1 errors                                            friendly-errors 23:04:34


 ERROR  in ./layouts/blog.vue?vue&type=style&index=0&lang=scss&                    friendly-errors 23:04:34

Module build failed (from ./node_modules/sass-loader/dist/cjs.js):                 friendly-errors 23:04:34
Error: Missing binding /Users/n0bisuke/dotstudio/1_protooutstudio/node_modules/node-sass/vendor/darwin-x64-79/binding.node
Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 13.x

Found bindings for the following environments:
  - OS X 64-bit with Node.js 12.x

・
・
・

みたいなコケかたをしました。

Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 13.x

この64-bitをみると一瞬カタリナにmacOSをアップデートしたから怒られるやつかと思いましたが、

Found bindings for the following environments:
  - OS X 64-bit with Node.js 12.x

こちらを見ると既に64bit Node.js v12でnode-sassが紐付けされてたみたいですね。

それにしてもエラー表示がすごい。

これで解決

yarn add node-sass

npmの人も入れ直せばOK。

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

expoのAppAuthでOAuthを実装してみる

What

expoのAppAuthの登場で、イジェクトなしでOAuth実装が非常に簡単になりました。
マイナーなSNSとの連携が必要な場合に非常に重宝しますね。

実装手順は以下
グーグルログインの場合

import * as AppAuth from "expo-app-auth";

 const config = {
    issuer: "https://accounts.google.com",
    clientId: "クライアント識別子"      
    scopes: ["profile"]
  };
 const tokenResponse = async () => {
    const authState = await AppAuth.authAsync(config);
    console.log(authState);
    return authState;
  };

これだけの手順でOauth認証が実装できます。非常に簡単ですね。
AppAuth.authAsync()はtoken情報のみを返します。

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

[ReactNative] expoのAppAuthでOAuthを実装してみる

What

expoのAppAuthの登場で、イジェクトなしでOAuth実装が非常に簡単になりました。
マイナーなSNSとの連携が必要な場合に非常に重宝しますね。

実装手順は以下
グーグルログインの場合

import * as AppAuth from "expo-app-auth";

 const config = {
    issuer: "https://accounts.google.com",
    clientId: "クライアント識別子"      
    scopes: ["profile"]
  };
 const tokenResponse = async () => {
   try {
     const authState = await AppAuth.authAsync(config);
     console.log(authState);
   } catch ({ message }) {
     alert(`${message}`); 
   } 
 };

これだけの手順でOAuthが実装できます。非常に簡単ですね。
AppAuth.authAsync()はtoken情報のみを返します。

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

Vue初級者がPropsとv-Slotをどう使い分けるかを考えた話

はじめに

Vue.jsをある程度やり、propsで子コンポーネントにデータの受け渡しをできるようになった…。
ところでSlotてあるけど、どういうシチュエーションで使っていくのだろうか、ってのを考えていきましょう。
ついでにPropsとどう使い分けていくのかも。

おさらい

まずはPropsSlotは何なのかのおさらいをしていきましょう。

Propsとは

Props - Vue.js

Vue.jsでコンポーネント間のデータの受け渡しをする上で最も基本的な機能ですね。
親側でv-bindや子側のProps名でデータを紐付けて、子側はProps内にプロパティで型とか状態を指定してって使い方。

parent.vue
<div>
  <child title="Qiita!"></child>
  <child :title="Vue"></child>
<div>
//*省略
data() {
  return {
    Vue: "Vue.js!"
  };
},
child.vue
props:{
  title:{
    type:String,
    required: true
  }
}

上のtitle="Qiita!"は文字列を受け渡し、下の:title="Vue"Vueというインスタンス変数などをv-bindで受け渡ししています。

Slotとは

Slot - Vue.js

コンポーネントの開始タグと終了タグの間に、何かしらの要素があった時に、それを子側で<slot></slot>と置換する機能ですね。
子側に<slot></slot>がないと、親側でタグの間に要素があってもそれは表示されないので、表示させたい場合は子側に<slot></slot>を指定してあげる必要があります。

parent.vue
<template>
  <child>
    Qiita!
  </child>
</template>
child.vue
<template>
  <slot></slot>
</template>

<child>に囲まれたQiita!の文字列が、子の<slot></slot>と置換され、Qiita!が表示されます。
なお、これは文字列だけではなく他のタグ要素に囲まれた物も置換されます。

parent.vue
<template>
  <child>
    <h1>Vue.js!</h1>
    <span>YES!Vue!</span>
  </child>
</template>
child.vue
<template>
  <slot></slot>
</template>

<h1>タグと<span>タグも、子の<slot></slot>と置換されます。
主に親コンポーネントの要素を子要素に差し込みたいってときに使うと思います。

使い分けを考える

値を渡すだけならProps

文字列なり数値なり、単にデータを受け渡すのならslotでやるよりもPropsでやるべきです。

状態で中の要素が変わるならslot

v-ifなどで中の要素の構成が動的に変わる事があり、それを子側で挟み込んでやりたい。
という場合にはslotを使ったほうが、子側にpropsを使い状態を渡してやるよりも依存関係が薄くなり、
子側のコンポーネントの再利用性が上がるので、このような場合はslotを使いましょう。

parent.vue
<template>
  <child>
    <template v-if="flag">
      <h1>Vue.js!</h1>
      <span>YES!Vue!</span>
    </template>
    <template v-else>
      <p>Error!!!</p>
    </template>
  </child>
</template>
child.vue
<template>
  <slot></slot>
</template>

flagの値がtrueの時は上の要素が、falseの時は下のErrorとかかれた要素が表示されます。
また、置換したい親側の要素をVueコンポーネントにしても、それを子側のslotで置換することができます。

AtomicDesignで使ってみよう

AtomicDesignを使ってコンポーネント設計を行っていた場合にでも、AtomMoleculesなど、コンポーネントの粒度が細かい箇所でよく使う機能になります。
特にAtomレベルのコンポーネントは、それ以上機能として分けれない、親との疎結合性を保つ必要がある、
などの必要があるのと、なによりもAtomコンポーネントの中に他の要素を入れる事はAtomicDesignの考えから離れてしまいます。
なので、AtomicDesignでも要素の子に別の要素を入れたい…!という時には、slotを使って置換させる、などの手法が特に有効と思います。

おわり

作成中のプロジェクトでslotを使う機会が出てきたので、今回は記事にしてみました。
propsslotもどちらも子に何かしらの値や要素を受け渡す便利な機能ですが、
どちらも使い方によってはアンチパターンとなるので、しっかりとコンポーネントの設計を考えていく必要がありますね。

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

ReactNative AndroidのBrige解説

機能

  • Native Modules
    出来ること
    ・関数の作成
    ・イベントの通知
    ・コールバック

  • Native UI Components
    出来ること
    ・Viewの作成
    ・Viewのイベントの通知

Native Modules

ダイアログを表示するModuleを例にします。

[機能]
1, Androidのダイアログを表示する。(関数の作成)
2, ダイアログの"OK"ボタンがクリックされたらコンソールに"true"が表示される。(イベントの通知)
3, "DATE"ボタンがクリックされたら現在時刻が表示される。(コールバック)

DialogModule.java
public class DialogModule extends ReactContextBaseJavaModule {
    // コンストラクター
    public DialogModule(@NonNull ReactApplicationContext reactContext) {
        super(reactContext);
    }
   // このモジュールを呼び出すためのタグのようなもの
   @Override
    public String getName() {
        return "DialogModule";
    }

    // RNで使用出来るクラス定数を定義出来る
    public Map<String, Object> getConstants() {
        final Map<String, Object> constants = new HashMap<>();
        constants.put("SINGLE_BUTTON", "SINGLE");
        constants.put("DOUBLE_BUTTON", "DOUBLE");
        return constants;
    }

    // ダイアログを表示する関数
    @ReactMethod
    public void showDialog(String message, String type) {
        if (type.equals("SINGLE")) {
            new AlertDialog.Builder(getCurrentActivity())
                    .setTitle("DialogModule")
                    .setMessage(message)
                    .setPositiveButton("CLOSE", (dialog, which) -> {
                        dialog.dismiss();
                    })
                    .show();
        } else {
            new AlertDialog.Builder(getCurrentActivity())
                    .setTitle("DialogModule")
                    .setMessage(message)
                    .setPositiveButton("OK", (dialog, which) -> {
                        sendEvent();
                    })
                    .setNegativeButton("CLOSE", (dialog, which) -> {
                        dialog.dismiss();
                    })
                    .show();
        }
    }

    // 現在時刻を表示してコールバックする関数
    @ReactMethod
    private void getCurrentTime(Callback callback) {
        Calendar calendar = Calendar.getInstance();
        callback.invoke(calendar.getTime().toString());
    }

    // ボタンがクリックされたことを通知するイベント
    private void sendEvent() {
        WritableMap params = Arguments.createMap();
        params.putBoolean("click", true);
        getReactApplicationContext()
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit("onClick", params);
    }
}

解説

getName

RNから呼び出す際の文字列を"DialogModule"のように記述する必要があります。

@Override
public String getName() {
    return "DialogModule";
}

getConstants

RNから使用できるMolueのクラス定数を設定できます。
※実装は必須ではありません。
constants.put(定数名, 値);

public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put("SINGLE_BUTTON", "SINGLE");
    constants.put("DOUBLE_BUTTON", "DOUBLE");
    return constants;
}

@ReactMethod

RNから使用できるメソッドを設定できます。
コールバックを実行する場合は.invoke()で実行します。

@ReactMethod
private void getCurrentTime(Callback callback) {
    Calendar calendar = Calendar.getInstance();
    callback.invoke(calendar.getTime().toString());
}

イベント(リスナー)
RNに登録できるイベントを設定できます。

// コールバックの値を設定
WritableMap params = Arguments.createMap();
params.putBoolean("click", true);
// イベント名とコールバックの値を設定
getReactApplicationContext()
    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
    .emit("onClick", params);

Native UI Components

動画playerを表示するViewを例にします。

[機能]
1, 動画playerを表示する。(Viewの作成)
2, urlをRN側からpropsで受け取り動画を再生する。
3, 再生終了時にログを表示する。(Viewのイベントの通知)

VideoViewManager.java
public class VideoViewManager extends SimpleViewManager<VideoView> {
    private Context context;

    // このモジュールを呼び出すためのタグのようなもの
    @Override
    public String getName() {
        return "VideoView";
    }

    // 使用するViewのインスタンを返すコンストラクターのようなもの
    @Override
    protected VideoView createViewInstance(ThemedReactContext reactContext) {
        this.context = reactContext;
        return new VideoView(reactContext);
    }

    // Propsで受け取った値で処理をする関数
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
    @ReactProp(name="url")
    public void setVideoPath(VideoView videoView, String urlPath) {
        Uri uri = Uri.parse(urlPath);
        videoView.setMediaController(new MediaController(context));
        videoView.setVideoURI(uri);
        // 再生準備出来次第再生する
        videoView.setOnPreparedListener(mp -> {
            videoView.start();
        });
        // 再生終了時にpropsの『onFinish』にコールバックする。(通知)
        videoView.setOnCompletionListener(mp -> {
            ReactContext reactContext = (ReactContext)context;

            WritableMap event = Arguments.createMap();
            event.putString("message", "onDirectEvent");
            reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onDirectEvent",event);

            WritableMap event2 = Arguments.createMap();
            event2.putString("message", "onBubblingEvent");
            reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onBubblingEvent",event2);
        });
        videoView.getDuration();
    }

    @Override
    public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
        return MapBuilder.<String, Object>builder()
                .put("onDirectEvent", MapBuilder.of("registrationName", "onDirectEvent"))
                .build();
    }

    @Override
    public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
        return MapBuilder.<String, Object>builder()
                .put("onBubblingEvent", MapBuilder.of("phasedRegistrationNames",
                                    MapBuilder.of(
                                            "bubbled", "onBubble",
                                            "captured", "onCapture")))
                .build();
    }
}

解説

getName

RNから呼び出す際の文字列を"VideoView"のように記述する必要があります。

@Override
public String getName() {
    return "VideoView";
}

@ReactProp

ViewのPropsを受け取るセッターメソッド
メソッド名自体はなんでもいいです
@ReactProp(name="props名")
public void setProp(Viewの型 view, 型 Propsの値)

@ReactProp(name="url")
public void setVideoPath(VideoView videoView, String urlPath) {
        // ...
}

createViewInstance

Viewのインスタンスを返す関数
※必須です

@Override
protected VideoView createViewInstance(ThemedReactContext reactContext) {
    return new VideoView(reactContext);
}

イベントの通知
Propsのコールバックを設定できます。

イベントの登録

イベントを登録するメソッドは2つあります。
・getExportedCustomDirectEventTypeConstants
・getExportedCustomBubblingEventTypeConstants

getExportedCustomDirectEventTypeConstants

1つのイベントで1つのpropsに通知する。
登録方法
MapBuilder.builder().put("イベント名", MapBuilder.of("registrationName", "プロップス名")).build();

getExportedCustomBubblingEventTypeConstants

1つのイベントで2つのpropsに通知することができる。
登録方法
MapBuilder.builder().put("イベント名", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "プロップス名1","captured", "プロップス名2"))).build();

// イベントを登録する関数1
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
    return MapBuilder.<String, Object>builder()
            .put("onDirectEvent", MapBuilder.of("registrationName", "onDirectEvent"))
            .build();
}

// イベントを登録する関数2
@Override
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
    return MapBuilder.<String, Object>builder()
            .put("onBubblingEvent", MapBuilder.of("phasedRegistrationNames",
                                MapBuilder.of(
                                        "bubbled", "onBubble",
                                        "captured", "onCapture")))
            .build();
}

通知したい箇所でreceiveEventを呼び出す
receiveEvent("viewのID", "イベント名", コールバックの値)

// ...
ReactContext reactContext = (ReactContext)context;
// イベントを通知する処理1 "onDirectEvent"イベント
WritableMap event = Arguments.createMap();
event.putString("message", "onDirectEvent");
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onDirectEvent",event);

// イベントを通知する処理2 "onBubblingEvent"イベント
WritableMap event2 = Arguments.createMap();
event2.putString("message", "onBubblingEvent");
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onBubblingEvent",event2);
// ...

RN側では以下のようにイベントが通知される

<VideoView
      onDirectEvent={({ nativeEvent }) => console.log(nativeEvent.message)} // onDirectEvent
      onCapture={({ nativeEvent }) => console.log(nativeEvent.message)} // onBubblingEvent
      onBubble={({ nativeEvent }) => console.log(nativeEvent.message)} // onBubblingEvent
/>

ModuleとUI Componentの登録

作成したModuleとUI Componentを以下のように登録します

ExamplePackage.java
public class ExamplePackage implements ReactPackage {
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.<ViewManager>singletonList(
                // UI Componentが増えるたびに追加していく
                new VideoViewManager()
        );
    }

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext
            return Collections.<ViewManager>singletonList(
                // Moduleが増えるたびに追加していく
                new DialogModule(reactContext)
        );
    }
}

作成したpackageをMainApplication.java内のgetPackagesに登録します

MainApplication.java
@Override
protected List<ReactPackage> getPackages() {
      List<ReactPackage> packages = new PackageList(this).getPackages();
           // packageが増えるたびに追加していく
           packages.add(new ExamplePackage());
      return packages;
}

RN側

Native Modules

Dialog.jsx
import React from 'react';
import { NativeModules } from 'react-native';
// getName関数の返り値("DialogModule")を指定
DialogModule = NativeModules.DialogModule;

const Dialog = () => {
  const [date, setDate] = React.useState("");

  React.useEffect(() => {
    const eventEmitter = new NativeEventEmitter(DialogModule);
    eventEmitter.addListener('onClick', (event) => {
       console.log(event.change) // "true"
    });
  }, [])

  return (
    <>
      <TouchableOpacity onPress={() => DialogModule.showDialog('SINGLE BUTTON', DialogModule.SINGLE_BUTTON)}>
        <Text>SINGLE BUTTON Dialog</Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => DialogModule.showDialog('DOUBLE BUTTON', DialogModule.DOUBLE_BUTTON)}>
        <Text>DOUBLE BUTTON Dialog</Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => DialogModule.getCurrentTime(time => setDate(time))}>
        <Text>DATE</Text>
      </TouchableOpacity>
        <Text>CURRENT DATE:</Text>
      <Text>[ {date} ]</Text>
    </>
  )
}

export default Dialog

Native UI Components

VideoView.jsx
import React from 'react';
import { requireNativeComponent } from 'react-native';
VideoView = requireNativeComponent('VideoView');

const VideoView = () => {

  return (
    <>
      <VideoView 
        style={{ width: '100%', height: '100%' }}
        url="https://www.radiantmediaplayer.com/media/bbb-360p.mp4"
        onDirectEvent={({ nativeEvent }) => console.log(nativeEvent.message)}
        onCapture={({ nativeEvent }) => console.log(nativeEvent.message)}
        onBubble={({ nativeEvent }) => console.log(nativeEvent.message)} />
    </>
  )
}

export default VideoView

おまけ

UI Componentに実装してある関数をModuleから呼びたい場合

ExamplePackage.java
public class ExamplePackage implements ReactPackage {
    private ExampleViewManager instance;
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.<ViewManager>singletonList(
                instance
        );
    }

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext
            instance = new ExampleViewManager();
            return Collections.<ViewManager>singletonList(
                // モジュールにViewManagerのインスタンスを渡す
                // あとはモジュール内ので定義した関数からインスタンスのメソッドを呼べば良い
                new ExampleModule(reactContext, instance)
        );
    }
}

最後に

Androidのブリッジ部分については英語の記事ばかりかつ動かないサンプルコードが多く苦労しました。
記事書く時間が少なかったので、抜けてるところや間違った箇所があるかもしれないので記事の内容は参考程度にしてください。

続きまして、React Native Advent Calendar 20日目の記事は @duka さんの「AB test とかの話」です :tada:

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

ReactNative AndroidのBridge解説

機能

  • Native Modules
    出来ること
    ・関数の作成
    ・イベントの通知
    ・コールバック

  • Native UI Components
    出来ること
    ・Viewの作成
    ・Viewのイベントの通知

Native Modules

ダイアログを表示するModuleを例にします。

[機能]
1, Androidのダイアログを表示する。(関数の作成)
2, ダイアログの"OK"ボタンがクリックされたらコンソールに"true"が表示される。(イベントの通知)
3, "DATE"ボタンがクリックされたら現在時刻が表示される。(コールバック)

DialogModule.java
public class DialogModule extends ReactContextBaseJavaModule {
    // コンストラクター
    public DialogModule(@NonNull ReactApplicationContext reactContext) {
        super(reactContext);
    }
   // このモジュールを呼び出すためのタグのようなもの
   @Override
    public String getName() {
        return "DialogModule";
    }

    // RNで使用出来るクラス定数を定義出来る
    public Map<String, Object> getConstants() {
        final Map<String, Object> constants = new HashMap<>();
        constants.put("SINGLE_BUTTON", "SINGLE");
        constants.put("DOUBLE_BUTTON", "DOUBLE");
        return constants;
    }

    // ダイアログを表示する関数
    @ReactMethod
    public void showDialog(String message, String type) {
        if (type.equals("SINGLE")) {
            new AlertDialog.Builder(getCurrentActivity())
                    .setTitle("DialogModule")
                    .setMessage(message)
                    .setPositiveButton("CLOSE", (dialog, which) -> {
                        dialog.dismiss();
                    })
                    .show();
        } else {
            new AlertDialog.Builder(getCurrentActivity())
                    .setTitle("DialogModule")
                    .setMessage(message)
                    .setPositiveButton("OK", (dialog, which) -> {
                        sendEvent();
                    })
                    .setNegativeButton("CLOSE", (dialog, which) -> {
                        dialog.dismiss();
                    })
                    .show();
        }
    }

    // 現在時刻を表示してコールバックする関数
    @ReactMethod
    private void getCurrentTime(Callback callback) {
        Calendar calendar = Calendar.getInstance();
        callback.invoke(calendar.getTime().toString());
    }

    // ボタンがクリックされたことを通知するイベント
    private void sendEvent() {
        WritableMap params = Arguments.createMap();
        params.putBoolean("click", true);
        getReactApplicationContext()
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit("onClick", params);
    }
}

解説

getName

RNから呼び出す際の文字列を"DialogModule"のように記述する必要があります。

@Override
public String getName() {
    return "DialogModule";
}

getConstants

RNから使用できるMolueのクラス定数を設定できます。
※実装は必須ではありません。
constants.put(定数名, 値);

public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put("SINGLE_BUTTON", "SINGLE");
    constants.put("DOUBLE_BUTTON", "DOUBLE");
    return constants;
}

@ReactMethod

RNから使用できるメソッドを設定できます。
コールバックを実行する場合は.invoke()で実行します。

@ReactMethod
private void getCurrentTime(Callback callback) {
    Calendar calendar = Calendar.getInstance();
    callback.invoke(calendar.getTime().toString());
}

イベント(リスナー)
RNに登録できるイベントを設定できます。

// コールバックの値を設定
WritableMap params = Arguments.createMap();
params.putBoolean("click", true);
// イベント名とコールバックの値を設定
getReactApplicationContext()
    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
    .emit("onClick", params);

Native UI Components

動画playerを表示するViewを例にします。

[機能]
1, 動画playerを表示する。(Viewの作成)
2, urlをRN側からpropsで受け取り動画を再生する。
3, 再生終了時にログを表示する。(Viewのイベントの通知)

VideoViewManager.java
public class VideoViewManager extends SimpleViewManager<VideoView> {
    private Context context;

    // このモジュールを呼び出すためのタグのようなもの
    @Override
    public String getName() {
        return "VideoView";
    }

    // 使用するViewのインスタンを返すコンストラクターのようなもの
    @Override
    protected VideoView createViewInstance(ThemedReactContext reactContext) {
        this.context = reactContext;
        return new VideoView(reactContext);
    }

    // Propsで受け取った値で処理をする関数
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
    @ReactProp(name="url")
    public void setVideoPath(VideoView videoView, String urlPath) {
        Uri uri = Uri.parse(urlPath);
        videoView.setMediaController(new MediaController(context));
        videoView.setVideoURI(uri);
        // 再生準備出来次第再生する
        videoView.setOnPreparedListener(mp -> {
            videoView.start();
        });
        // 再生終了時にpropsの『onFinish』にコールバックする。(通知)
        videoView.setOnCompletionListener(mp -> {
            ReactContext reactContext = (ReactContext)context;

            WritableMap event = Arguments.createMap();
            event.putString("message", "onDirectEvent");
            reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onDirectEvent",event);

            WritableMap event2 = Arguments.createMap();
            event2.putString("message", "onBubblingEvent");
            reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onBubblingEvent",event2);
        });
        videoView.getDuration();
    }

    @Override
    public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
        return MapBuilder.<String, Object>builder()
                .put("onDirectEvent", MapBuilder.of("registrationName", "onDirectEvent"))
                .build();
    }

    @Override
    public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
        return MapBuilder.<String, Object>builder()
                .put("onBubblingEvent", MapBuilder.of("phasedRegistrationNames",
                                    MapBuilder.of(
                                            "bubbled", "onBubble",
                                            "captured", "onCapture")))
                .build();
    }
}

解説

getName

RNから呼び出す際の文字列を"VideoView"のように記述する必要があります。

@Override
public String getName() {
    return "VideoView";
}

@ReactProp

ViewのPropsを受け取るセッターメソッド
メソッド名自体はなんでもいいです
@ReactProp(name="props名")
public void setProp(Viewの型 view, 型 Propsの値)

@ReactProp(name="url")
public void setVideoPath(VideoView videoView, String urlPath) {
        // ...
}

createViewInstance

Viewのインスタンスを返す関数
※必須です

@Override
protected VideoView createViewInstance(ThemedReactContext reactContext) {
    return new VideoView(reactContext);
}

イベントの通知
Propsのコールバックを設定できます。

イベントの登録

イベントを登録するメソッドは2つあります。
・getExportedCustomDirectEventTypeConstants
・getExportedCustomBubblingEventTypeConstants

getExportedCustomDirectEventTypeConstants

1つのイベントで1つのpropsに通知する。
登録方法
MapBuilder.builder().put("イベント名", MapBuilder.of("registrationName", "プロップス名")).build();

getExportedCustomBubblingEventTypeConstants

1つのイベントで2つのpropsに通知することができる。
登録方法
MapBuilder.builder().put("イベント名", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "プロップス名1","captured", "プロップス名2"))).build();

// イベントを登録する関数1
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
    return MapBuilder.<String, Object>builder()
            .put("onDirectEvent", MapBuilder.of("registrationName", "onDirectEvent"))
            .build();
}

// イベントを登録する関数2
@Override
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
    return MapBuilder.<String, Object>builder()
            .put("onBubblingEvent", MapBuilder.of("phasedRegistrationNames",
                                MapBuilder.of(
                                        "bubbled", "onBubble",
                                        "captured", "onCapture")))
            .build();
}

通知したい箇所でreceiveEventを呼び出す
receiveEvent("viewのID", "イベント名", コールバックの値)

// ...
ReactContext reactContext = (ReactContext)context;
// イベントを通知する処理1 "onDirectEvent"イベント
WritableMap event = Arguments.createMap();
event.putString("message", "onDirectEvent");
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onDirectEvent",event);

// イベントを通知する処理2 "onBubblingEvent"イベント
WritableMap event2 = Arguments.createMap();
event2.putString("message", "onBubblingEvent");
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onBubblingEvent",event2);
// ...

RN側では以下のようにイベントが通知される

<VideoView
      onDirectEvent={({ nativeEvent }) => console.log(nativeEvent.message)} // onDirectEvent
      onCapture={({ nativeEvent }) => console.log(nativeEvent.message)} // onBubblingEvent
      onBubble={({ nativeEvent }) => console.log(nativeEvent.message)} // onBubblingEvent
/>

ModuleとUI Componentの登録

作成したModuleとUI Componentを以下のように登録します

ExamplePackage.java
public class ExamplePackage implements ReactPackage {
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.<ViewManager>singletonList(
                // UI Componentが増えるたびに追加していく
                new VideoViewManager()
        );
    }

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext
            return Collections.<ViewManager>singletonList(
                // Moduleが増えるたびに追加していく
                new DialogModule(reactContext)
        );
    }
}

作成したpackageをMainApplication.java内のgetPackagesに登録します

MainApplication.java
@Override
protected List<ReactPackage> getPackages() {
      List<ReactPackage> packages = new PackageList(this).getPackages();
           // packageが増えるたびに追加していく
           packages.add(new ExamplePackage());
      return packages;
}

RN側

Native Modules

Dialog.jsx
import React from 'react';
import { NativeModules } from 'react-native';
// getName関数の返り値("DialogModule")を指定
DialogModule = NativeModules.DialogModule;

const Dialog = () => {
  const [date, setDate] = React.useState("");

  React.useEffect(() => {
    const eventEmitter = new NativeEventEmitter(DialogModule);
    eventEmitter.addListener('onClick', (event) => {
       console.log(event.change) // "true"
    });
  }, [])

  return (
    <>
      <TouchableOpacity onPress={() => DialogModule.showDialog('SINGLE BUTTON', DialogModule.SINGLE_BUTTON)}>
        <Text>SINGLE BUTTON Dialog</Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => DialogModule.showDialog('DOUBLE BUTTON', DialogModule.DOUBLE_BUTTON)}>
        <Text>DOUBLE BUTTON Dialog</Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => DialogModule.getCurrentTime(time => setDate(time))}>
        <Text>DATE</Text>
      </TouchableOpacity>
        <Text>CURRENT DATE:</Text>
      <Text>[ {date} ]</Text>
    </>
  )
}

export default Dialog

Native UI Components

VideoView.jsx
import React from 'react';
import { requireNativeComponent } from 'react-native';
VideoView = requireNativeComponent('VideoView');

const VideoView = () => {

  return (
    <>
      <VideoView 
        style={{ width: '100%', height: '100%' }}
        url="https://www.radiantmediaplayer.com/media/bbb-360p.mp4"
        onDirectEvent={({ nativeEvent }) => console.log(nativeEvent.message)}
        onCapture={({ nativeEvent }) => console.log(nativeEvent.message)}
        onBubble={({ nativeEvent }) => console.log(nativeEvent.message)} />
    </>
  )
}

export default VideoView

おまけ

UI Componentに実装してある関数をModuleから呼びたい場合

ExamplePackage.java
public class ExamplePackage implements ReactPackage {
    private ExampleViewManager instance;
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.<ViewManager>singletonList(
                instance
        );
    }

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext
            instance = new ExampleViewManager();
            return Collections.<ViewManager>singletonList(
                // モジュールにViewManagerのインスタンスを渡す
                // あとはモジュール内ので定義した関数からインスタンスのメソッドを呼べば良い
                new ExampleModule(reactContext, instance)
        );
    }
}

最後に

Androidのブリッジ部分については英語の記事ばかりかつ動かないサンプルコードが多く苦労しました。
記事書く時間が少なかったので、抜けてるところや間違った箇所があるかもしれないので記事の内容は参考程度にしてください。

続きまして、React Native Advent Calendar 20日目の記事は @duka さんの「AB test とかの話」です :tada:

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

Amazonの注文履歴ページから商品のASINコードを取得する雑JavaScript

これを参考にした

下記スクリプトを、注文履歴ページ上で対象期間を絞った上で実行すると、商品名・ASINコード・商品ページURLが記載されたcsvが吐き出される。

スクリーンショット 2019-12-18 20.54.16.png

スクリーンショット 2019-12-18 20.55.04.png

スクリーンショット 2019-12-18 20.56.13.png

スクリプト

// メッセージ用オーバーレイ初期化
var overlayId = 'overlay';
function initMsgOverlay() {
  var overlay = document.createElement('div');
  overlay.id = overlayId;
  overlay.style.position = 'fixed';
  overlay.style.left = 0;
  overlay.style.top = 0;
  overlay.style.width = '100%';
  overlay.style.height = '100%';
  overlay.style.zIndex = 100;
  overlay.style.backgroundColor = 'rgba(0,0,0,0.7)';
  overlay.style.color = '#fff';
  overlay.style.textAlign = 'center';
  overlay.style.paddingTop = '15em';
  document.body.appendChild(overlay);
}
function displayMsg(msg) {
  document.getElementById(overlayId).innerHTML = '<h1>' + msg + '</h1>';
}
function appendMsg(msg) {
  document.getElementById(overlayId).innerHTML += '<h1>' + msg + '</h1>';
}
function appendMsgSmall(msg) {
  document.getElementById(overlayId).innerHTML += '<br><h3>' + msg + '</h3>';
}

function getTargetPeriod() {
  try {
    var period = document.querySelector('#timePeriodForm .a-dropdown-prompt').textContent.trim();
    return period
  } catch(error) {
    displayMsg('対象期間を取得できません')
    throw Error
  }
}

function generateCsv(itemDetails) {
  var header = '"商品名","ASIN","商品ページURL"\n';
  var body = '';
  for (var i=0; i<itemDetails.length; i++) {
    body += '"' + itemDetails[i]['name'].replace(/"/g, '""') + '",';
    body += '"' + itemDetails[i]['asin'].replace(/"/g, '""') + '",';
    body += '"' + itemDetails[i]['url'].replace(/"/g, '""') + '"\n'
  }
  return header + body;
}


function sleep(milliSec) {
  return new Promise(resolve => {
    setTimeout(resolve, milliSec);
  });
}

async function getItemDetails(orderHistoryHtml, itemDetails) {
  return new Promise(async (resolve, reject) => {
    var itemNameSelector = '.a-box-group.a-spacing-base.order .a-fixed-left-grid-col.a-col-right div.a-row > a.a-link-normal';
    var itemNameElements = Array.from(orderHistoryHtml.querySelectorAll(itemNameSelector));
    var itemNames = itemNameElements.map(elem => elem.textContent.trim());
    var itemUrls = itemNameElements.map(elem => elem.href);
    var asinPattern = /gp\/product\/(.*)\/ref=ppx_yo_dt/;
    var itemAsins = itemUrls.map(url => url.match(asinPattern)[1]);

    for (var i=0; i<itemNames.length; i++) {
      var itemDetail = {};
      itemDetail['name'] = itemNames[i];
      itemDetail['asin'] = itemAsins[i];
      itemDetail['url'] = itemUrls[i];
      itemDetails.push(itemDetail);
    }

    var nextPageLinkSelector = '.a-last a';
    var nextPageLinkElement = orderHistoryHtml.querySelector(nextPageLinkSelector);
    var nextPageUrl;
    if (nextPageLinkElement === null) {
      displayMsg('全ページ取得完了');
      console.log('全ページ取得完了');
      resolve();
      return;
    } else {
      nextPageUrl = nextPageLinkElement.href;
      displayMsg('以下のページの情報を取得');
      appendMsg(nextPageUrl);
      console.log('以下のページの情報を取得');
      console.log(nextPageUrl);
      console.log(new Date());
    }

    var request = new XMLHttpRequest();
    request.open('GET', nextPageUrl, false);
    request.send(null);

    if (request.status === 200) {
      var nextPageHtmlText = request.responseText;
      nextPageHtml = document.createElement('div')
      nextPageHtml.innerHTML = nextPageHtmlText;
    }
    await sleep(1500);
    await getItemDetails(nextPageHtml, itemDetails);
    resolve();
  });
}

async function main(itemDetails) {
  initMsgOverlay();
  var period = getTargetPeriod();
  displayMsg(period+'注文のASINコードを取得します');
  await sleep(2000);
  displayMsg('以下のページの情報を取得');
  appendMsg(location.href);
  await getItemDetails(document, itemDetails);
  var csv = generateCsv(itemDetails);
  // csvの内容に"#"があるとdata urlにcsvの内容を設定した際に途中で途切れてしまう
  csv = csv.replace(/#/, '');
  var csvWindow = window.open('', 'csv', 'width=640,height=480');

  var downloadLink = csvWindow.document.createElement('a');
  downloadLink.setAttribute('download', 'asin.csv');
  downloadLink.setAttribute('href', 'data:text/html;charset=utf-8,'+csv);
  downloadLink.innerHTML = 'CSVダウンロード';

  csvWindow.document.write('<title>取得結果CSV</title>');
  csvWindow.document.write('<p>'+downloadLink.outerHTML+'</p>');
  csvWindow.document.write('<code>');
  csvWindow.document.write(csv);
  csvWindow.document.write('</code>');
}

var itemDetails = [];
main(itemDetails);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのArrayとLuaのtableの比較

はじめに

Qiitaに下書きが多すぎて新しい記事作れませんと言われたので途中まで書いて放っておいた記事をとりあえず投稿して下書きを減らします、すみません。


JavaScript の Array は「0以上の整数のキーを特別扱いするコレクションオブジェクト」と捉えると
Lua の テーブルに結構似ています。(テーブルは1以上ですが)
(実際にはArrayオブジェクトはObjectを継承しているものの、独自にプロパティを
 設定することは推奨されていない気がします)

  • JavaScriptのArrayとLua(5.3)のテーブルの比較
  • Arrayにあってテーブルにないものの中で便利そうなのはLuaでも実装してみる

をやっていこうと思います。

  • JavaScriptは下記参照先の表記優先(その他、ブラウザやVScodeのデバッグ)
  • Luaは lua.exe (lua-5.3.5_Win64_bin)にて確認
  • Luaはやっとメタテーブルをなんとなく理解してきたレベル
  • JavaScriptは調べながらじゃないとFizzBuzzも怪しいレベル

Array - Javascript|MDN
Lua 5.3 リファレンスマニュアル

作成と(簡易)文字列出力

JavaScript
var fruits = ["りんご","バナナ"];
console.log(fruits);
// ["りんご","バナナ"] <- 実際には先頭に Array(2) とか [Object Array](2) とかが入る
Lua5.3
fruits = {"りんご","バナナ"}
print(fruits)
--> table: 0000000000d3a390

Luaのテーブル型は参照なので識別子を型名とともに出力しています。(環境によっては4byte表記)
これはまあそうなのですが、ちょっと中身を確認したい時がありますので、
文字列変換を実装(とりえあず多次元テーブルはスルー)
後、テーブル用標準ライブラリ(table テーブルに格納されている)をメソッドっぽく
使用できるようにメタテーブルを利用します。

Lua5.3
table.of=function(...)
  return setmetatable({...},{
    __index=table,
    __tostring=function(t)
      return ("{%s}"):format(t:concat(", "))
    end
  })
end

fruits=table.of("りんご","バナナ")
print(fruits)
--> {りんご, バナナ}

デフォルトの参照先を設定する、prototype と メタテーブルは似ているけど、

長さ(要素数)を取得する

JavaScript
console.log(fruits.length);
// 2
Lua5.3
print(#fruits)
--> 2

プロパティで持っているJavaScript、長さ演算子#があるLua。どちらもめずらしい部類でしょうか?
添え字に抜けがある場合に返す値に関しては異なりますが、とりあえずスルーで。
個人的にarray.lenght=0とするとArray部分をクリアできるのはうらやましいのでclearとして追加。

Lua
table.clear=function(t)
  for n=1,#t do t[n]=nil end
end

要素を取得する

JavaScript
var first = fruits[0]
// りんご

var last = fruits[fruits.length - 1];
// バナナ
Lua5.3
first = fruits[1]
--> りんご

last = fruits[#fruits]
--> バナナ

自動で割り振ったり、要素数算出のために利用する順番号(添え字)が
javascriptでは0から開始なのに対し、Luaでは1から開始

番号割り振り(numbering)を数え上げ(counting)に寄せた Lua(1オリジン)と、
基準とそこからの相対値(offset)に寄せた javascript(0オリジン) の違い。

プログラミング初心者にとっては1オリジンの方が自然に感じますが、
他のプログラミング言語では0オリジンの方が多数派のため混乱の元となっているよう。
(C言語の配列がアドレス算出に都合がよいために 0 オリジンを採用し、
 それに倣った言語が多かったという説が有力のようです)

ループ処理

JavaScript
fruits.forEach(function(item, index, array) {
  console.log(item, index);
});
// りんご 0
// バナナ 1
Lua5.3
for index,item in ipairs(fruits) do
  print(item,index)
end
--> りんご 1
--> バナナ 2

末尾に要素を追加する

JavaScript
var newLength = fruits.push("みかん");
// ["りんご", "バナナ", "みかん"]
Lua5.3
fruits:insert("みかん")
--> {りんご, バナナ, みかん}

pushメソッドは末尾に要素を追加し、追加後の要素数を返す

table.insert 関数は何も返さない

末尾の要素を削除する

JavaScript
var last = fruits.pop(); // 配列の末尾の要素 "みかん" を削除
// ["りんご", "バナナ"];
console.log(last);
// みかん
Lua5.3
last = fruits:remove()
--> {りんご, バナナ}
print(last)
--> みかん

先頭の要素を削除する

JavaScript
var first = fruits.shift(); // 配列の末尾の要素 "みかん" を削除
// ["バナナ"];
console.log(first);
// りんご
Lua5.3
first = fruits:remove(1)
--> {バナナ}
print(first)
--> りんご

先頭に要素を追加する

JavaScript
var newLength = fruits.unshift("いちご") // 配列の先頭に追加
// ["いちご", "バナナ"];
Lua5.3
fruits:insert(1,"いちご")
print(fruits)
--> {いちご, バナナ}

fruits:insert("いちご",1)ではないことに注意

要素のインデックスを取得する

JavaScript
fruits.push("マンゴー");
// ["いちご", "バナナ", "マンゴー"]

var pos = fruits.indexOf("バナナ");
// 1
Lua5.3
table.indexOf=function(tbl,val)
  for n,v in ipairs(tbl)do
    if v==val then return n end
  end
  return false
end

fruits:insert("マンゴー")
--> {いちご, バナナ, マンゴー}
pos = fruits:indexOf("バナナ")
--> 2

事前に{ [val1]={key1,key2,..},[val2]={key}..}みたいなテーブルを作成しておくのも有り(?)

インデックスから複数の要素を削除する

JavaScript
var vegetables = ['Cabbage', 'Turnip', 'Radish', 'Carrot'];
console.log(vegetables); 
// ["Cabbage", "Turnip", "Radish", "Carrot"]

var pos = 1, n = 2;

var removedItems = vegetables.splice(pos, n); 
// 複数の要素を削除するには、n で削除する要素数を定義します
// 指定位置(pos)以降から n 個分の要素が削除されます

console.log(vegetables); 
// ["Cabbage", "Carrot"] (元の配列が変化)

console.log(removedItems); 
// ["Turnip", "Radish"]
Lua5.3
table.splice=function(t,pos,n)
  local rmvd=table.of()
  for nums=1,n do
    rmvd:insert(t:remove(pos))
  end
  return rmvd
end

vegetables = table.of('Cabbage', 'Turnip', 'Radish', 'Carrot')
print(vegetables)
--> {Cabbage, Turnip, Radish, Carrot}

pos = 2; n = 2 -- posは 2番目(Turnip)を指定

removedItems = vegetables:splice(pos, n)
print(vegetables)
--> {Cabbage, Carrot}

print(removedItems);
--> {Turnip, Radish}

こんなメソッドがあるんだなあ。
何個取り出すというより、同じ場所から指定回数取り出す、という実装にしたけど遅そうですね。

いつか続きを書くかも知れません。

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

ゲームにおけるFirebase 活用例

PONOS Advent Calendar 2019の24日目の記事です。
?メリークリスマス!!クリスマス・イブにFirebaseの記事をお届けします!!!?
???????????????????????????

はじめに:eyeglasses:

現在運用中のゲームでFirebaseを導入しました。
この記事では活用事例とノウハウを紹介していきます。
Firebase利用の一例になれば幸いです。

組み込み方法は別記事をご参照ください
公式ドキュメント
iOS
Android
Unity
C++
Web

この記事の対象者:eyeglasses:

・Firebase初心者、中級者
・Firebaseを検討中の方
・Firebaseの実装イメージを掴みたい方
・サーバーレスを検討中の方
・Googleのサービスが好きな方、興味のある方

Firebaseの各機能をざっくり紹介

機能がたくさんあります

名称 機能 活用
Firebase Authentication 認証機能
Firebase Realtime Database NoSQL クラウド データベースでデータの保管と同期を行うことができる
Cloud Firestore Realtime Database からさらにパワーアップした NoSQL データベース。
Cloud Storage for Firebase 写真や動画などのコンテンツを保管、取得することができる。
Firebase Hosting webサイトを構築
Cloud Functions for Firebase 関数ごとにサーバー処理を簡単に用意できる
ML Kit for Firebase 機械学習 ×
Firebase Crashlytics クラッシュ レポート管理
Firebase Performance Monitoring パフォーマンスの問題を診断
Firebase Test Lab 様々なデバイスでアプリの自動テストおよびカスタマイズされたテストを実行
Firebase Cloud Messaging サーバから通知機能
Firebase In-App Messaging ターゲットを絞り込んでアプリ内で通知 ×
Firebase Predictions 機械学習を用いた分析ツール
Firebase Remote Config Firebase consoleから、外観の変更、機能の段階的な展開などをカスタマイズ ×
Google Analytics for Firebase 総合的な分析ツール
Firebase A/B Testing ユーザーごとに出し分けしてA/Bテストを行う
Firebase Dynamic Links 複数のプラットフォームで機能するURLを生成 ×
Firebase App Indexing アプリを Google 検索結果に表示することができる ×

活用1:ログイン時のユーザー認証

Firebase Authenticationをゲーム内での認証機能として使っています。
ログイン時に認証して、Firebase各種機能と連携して認証済みのアカウントしかセキュリティー的に許可しないようにしています。
以下の4種類の認証を使っています。
・Google
・Facebook
・メールアドレス
・匿名認証

匿名認証で作成したデータと各種連携したデータを紐づける処理はこちらで実装する必要がありますが、
公式ドキュメントにもサンプルコードが用意されているので低コストで実装できます。

匿名認証のコード

Auth.cpp
#include "firebase/app.h"
#include "firebase/auth.h"

void Auth::signInAnonymously(){
    firebase::App* app = App::GetInstance();
    firebase::auth::Auth* auth = Auth::GetAuth(app);

    auth->SignInAnonymously().OnCompletion([this](const firebase::Future<firebase::auth::User*>& result){
        if (result.status() == firebase::kFutureStatusComplete &&
         result.error() == firebase::auth::kAuthErrorNone &&
         result.result()) {
            //成功処理
        }else{
           //失敗処理
        }
    });
}


活用2:通信時の不正対策

通信時の不正対策として
Firebase Authentication

Cloud Functions
を組み合わせてサーバーサイドでも認証チェックしております。

Cloud Functionsを使用するとクライアント側の通信処理もセキュアでシンプルな作りになります。
詳細はCloud Functionsのドキュメントを一読するのもありです。

クライアント側の通信処理
Cloud Functionsで定義したsampleにリクエストを投げます。

Request.cpp
void Request::callSample(){
    firebase::Variant data = firebase::Variant::EmptyMap();

    firebase::functions::HttpsCallableReference doSomething = functions->GetHttpsCallable("sample");
    doSomething.Call(data).OnCompletion([this]
    (firebase::Future<firebase::functions::HttpsCallableResult> result){
        if(result.status() == firebase::kFutureStatusComplete &&
        result.error() == firebase::functions::kErrorNone && result.result()){
        //成功処理
        }else{
        //失敗処理
        }
    });
}


サーバー側の認証チェック
exports.sampleの部分がレスポンスを返します。

Server.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
 
exports.sample = functions.https.onCall((data, context) => {
    if(!checkAuth (data, context) ){
        throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +'while authenticated.');
    }
    //以降でサーバー処理を記入していく 
    return {
            text:"success",
    };
});


//認証チェック
async function checkAuth (data, context) {
    if( !(context.auth && context.auth.uid && context.auth.token)){
        console.log('checkAuth error');
        return false;
    }
    const user = await admin.auth().getUser(context.auth.uid);
    if (user.customClaims && user.customClaims.admin !== true) {
        console.log('checkAuth error');
        return false;
    }
    return true;
}

活用3:サーバーからのPush通知

Cloud Messagingを活用して特定条件を満たしたユーザーにPush通知を送るようにしています。
このサンプルソースではRealTimeDBにユーザーのpushTokenが保存されており、pushTokenを取り出して
使用しているパターンです。

Server.js
exports.pushNotification = functions.https.onCall((data, context) => {
    if(!checkAuth (data, context) ){
        throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
        'while authenticated.');
    }


    const userId = data.userId;
    var pass = "user_table/" + userId + "/" + "pushToken";
    return admin.database().ref(pass).once('value').then(function(snapshot){
        var pushToken = snapshot.val();
        const payload = {
            notification: {
              title: 'プッシュ通知タイトル',
              body: `プッシュ通知本文`
            }
          };
          return admin.messaging().sendToDevice(pushToken, payload);
    }).catch(error => {
          console.error(error);
          throw new functions.https.HttpsError(error);
    });
});

シンプルに書くとこれだけで通知を送れます。

          return admin.messaging().sendToDevice(pushToken, payload);

活用4:バトルログの保存(アップロード)

バトル時のログや詳細情報はCloud Storageに保存しています。
DBに保存すると量と値段の面で、デメリットがあったので分析の必要がなく、他のユーザーから参照されないデータはCloud StorageにGZip化して保存しています。

CloudStorage.cpp
#include "firebase/app.h"
#include "firebase/storage.h"

void CloudStorage::storageUpLoad(){
    firebase::App* app = App::GetInstance();
    firebase::storage::Storage* storage = Storage::GetInstance(app);

    //アップロード先のパス
    std::string serverPath = "/";
    //アップロードするファイル保存先
    std::string localPath = "file://" + "ローカルパス";

    storage->GetReference().Child(serverPath).PutFile(localPath.c_str()).OnCompletion([](const firebase::FutureBase& result) {
                if (result.error() == firebase::storage::kErrorNone){
                  //成功処理
                }
                else {
                  //失敗処理
                }
    });

}


活用5:リソースダウンロード

これもCloud Storageを活用しています。
リソース数、容量などによって状況は変わると思いますが、問題ない速度でダウンロードできています。
何よりもCloud Storageはコストパフォーマンスがいいです。
現在、ほぼ無料枠で済んでいます。

CloudStorage.cpp
#include "firebase/app.h"
#include "firebase/storage.h"

void CloudStorage::storageDownload(){
    firebase::App* app = App::GetInstance();
    firebase::storage::Storage* storage = Storage::GetInstance(app);

    //対象リソースのパス
    std::string serverPath = "/";
    //ダウンロード後の保存先
    std::string localPath = "file://" + "ローカルパス";
    storage->GetReference().Child(serverPath).GetFile(localPath.c_str()).OnCompletion([](const firebase::FutureBase& result) {
        if (result.error() == firebase::storage::kErrorNone) {
         //成功処理
        }
        else {
         //失敗処理
        }
    });
}


活用6:ゲーム内お知らせ

お知らせ機能はFirebase Hostingを活用しています。
環境構築などの導入は非常に簡単です。
5分くらいでできると思います。
環境構築

環境構築が終わったら、HTML,CSS,画像などのWEB素材を用意して

firebase deploy

で、デプロイして完了です。

固定のお知らせやヘルプページを見せるだけでしたらFirebase Hostingだけで十分なのですが
ユーザーごとに見せるページを変えたい、期間で出し分けをしたい
などの仕様によってはサーバー側でHTMLを生成する必要があるので
うちのゲームではCloud Function、RealTimeDatabaseなども絡めて使っている箇所があります。

注意点としては
過去にデプロイしたWEBページがCDNに溜まっていき、容量が無駄になるので
Firebaseコンソール上からときどき消去しないといけません。
WEBページに問題があったときに、とっさにロールバックできるのでそれとトレードオフという感じですね

活用7:KPI 分析

ゲーム運営にKPI 分析は必須ですよね
Google Analytics for Firebaseを使っています。
for Firebaseと書いていますが、バックエンドの仕組みはGoogle AnalyticsでKPI分析するうえでは違いはありません。違いは組み込み方法が若干違うくらいです。

スクリーンショット 2019-12-19 18.55.43.png

こちらのFirebase デモプロジェクトを触ってみるのが理解が早いです。

1.デモプロジェクトログイン
デモプロジェクトが見れるようになったらAnalyticsページへ
2.デモプロジェクト Analyticsページ
※リンクから飛べない場合は、左側のメニューにてアナリティクス配下のDashboardをクリック

Analyticsは組み込むだけでセッション数、DAU、課金額、ARPPUなど
デフォルトでいくつかの値を集計してくれます。
ゲーム内独自の集計をしたい場合は、イベントを実装すればできます。

イベント実装例

FirebaseAnalytics.cpp
#include "firebase/app.h"
#include "firebase/analytics.h"

void FirebaseAnalytics::init(){
    firebase::App* app = App::GetInstance();
    firebase::analytics::Initialize(*app);
}

//レベルアップ時、イベント登録
void FirebaseAnalytics::levelUp(){
    firebase::analytics::LogEvent(firebase::analytics::kEventLevelUp);
}

活用8:予測データの運営活用

Firebase PredictionsではGoogleの機械学習の予測結果を簡単に知ることができます。

スクリーンショット 2019-12-19 19.03.49.png

デモプロジェクト

以下項目の一週間後の予測結果を知ることができます。
churn離脱ユーザーの数

not_churn継続ユーザーの数

spend課金するユーザーの数

not_spend課金しないユーザーの数

Firebase Predictionsの優れている点は
これらの予測結果に対して
・A/Bテストを行なって動向を分析できる
・Remote ConfigによってUIやパラメーターなどを瞬時に変えることができる
・Cloud Messagingを使ってお得な情報などを送ることができる
ことにあると思います。

また、Google Analyticsと連携してコンバージョンに設定されたイベントを予測項目として追加することもできます。
ゲームごとに有効なイベントを設定すれば有効活用できると思います。

活用9:バナー広告、動画広告

Google AdMobを使って、バナー広告、動画広告に対応することができます。
Firebaseの機能としてあまり注目されてないですが、実はFirebase SDKではAdMobの機能もサポートされてます。

バナー広告表示

FirebaseAdmob.cpp
#include "firebase/app.h"
#include "firebase/admob.h"
#include "firebase/admob/banner_view.h"


void FirebaseAdmob::createBanner(){
    //firebase::admob::BannerView* banner_view
    banner_view = new firebase::admob::BannerView();

    firebase::admob::AdSize ad_size;
    ad_size.ad_size_type = firebase::admob::kAdSizeStandard;
    ad_size.width = 320;
    ad_size.height = 50;
    // my_ad_parent is a reference to an iOS UIView or an Android Activity.
    // This is the parent UIView or Activity of the banner view.
    // kBannerAdUnitはAdMobのサイトで発行されるId
    banner_view->Initialize(getAdParent(), kBannerAdUnit, ad_size);
}

void FirebaseAdmob::showBanner(){
    banner_view->MoveTo(banner_view->kPositionBottomLeft);
    banner_view->Show();
    firebase::admob::AdRequest my_ad_request = {};
    banner_view->LoadAd(my_ad_request);
}

活用10:多端末テスト

スマホゲーム開発において多端末検証って大変ですよね
特にAndroidとかAndroidとかAndroidとか・・・
そこで活躍するのが
Firebase Test Lab
Google データセンターでホストされているデバイス上でアプリをテストします。
1 回のオペレーションで、さまざまなデバイス、
さまざまなデバイス構成で Android アプリや iOS アプリをテストし、
Firebase consoleで結果(ログ、動画、スクリーンショットなど)
を確認できます。

・Android端末が足りない場合
・テスターが足りない場合
・QA開始前のシステム的なテストをしたい場合
など使いどころはたくさんあります。

スクリーンショット 2019-12-19 19.04.49.png

1.デモプロジェクトログイン
デモプロジェクトが見れるようになったらTest Labページへ
2.デモプロジェクト Test Labページ
※リンクから飛べない場合は、左側のメニューにて品質、配下のTest Labをクリック

おわりに

Firebaseを導入してみての感想としては・・・

非常に簡単、便利で素晴らしいです!
ゲーム仕様や料金面での検討をする必要はありますが
まずは無料プランで使ってみてから他のサービスと比較検討する方法がオススメです!

料金面にて実装で創意工夫した箇所もありますが、結果として今は超・低コストで運営できています。

以下の機能は無料で使い続けることができるので、組み込んでおいて損はないです
・Google Analytics for Firebase
・Firebase Authentication
・Firebase Crashlytics
・Firebase Performance Monitoring
・Firebase Cloud Messaging
・Firebase Predictions
・Firebase Dynamic Links
・Firebase App Indexing
・Firebase A/B Testing

本プロジェクトではGCPも併用して活用しています。
次回はGCPのゲーム活用事例を書きたい思います。

明日は@honeniqさんの記事です
最終日の記事も楽しみですね!

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

[javascript]2つのarrayで完全一致と部分一致

たまに使いたくなるけど、書き方忘れるので備忘録。

変数
const baseArray = [1,2,3,4,5,6,7,8,9,10]
const target1   = [10,9,8,7,6,5,4,3,2,1]
const target2   = [1,2,3]
const target3   = [11]
完全一致(every)
baseArray.every((num) => target1.includes(num))
> true

baseArray.every((num) => target2.includes(num))
> false

baseArray.every((num) => target3.includes(num))
> false
部分一致(some)
baseArray.some((num) => target1.includes(num))
> true

baseArray.some((num) => target2.includes(num))
> true

baseArray.some((num) => target3.includes(num))
> false

参考

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

PlayCanvasで作成したゲームをPWAにしてAndroid端末へインストールする

PlayCanvasで作成したプログラムをPWAにしてリソースをキャッシュする

はがです。

PlayCanvasアドベントカレンダーの記事となります

今回のデモはこちらになります。

コインドーザー
https://yushimatenjin.github.io/

PlayCanvasからダウンロードします

まずPlayCanvasで作成したプロジェクトをダウンロードします。

ダウンロードされたファイルについて

PlayCanvasからダウンロードされたファイルは、index.htmlを始めとする静的なファイルが入っているものになります。

PWA対応のウェブサイトを作る

PlayCanvasで作ったゲームをPWAに対応させるために2つのファイルを追加します。

  • serviceWorker.js
  • manifest.json

manifest.jsonはゲームの基本的な情報を記載します

manifest.json
{
  "name": "コインドーザー",
  "short_name": "コインドーザー",
  "icons": [
    {
      "src": "https://yushimatenjin.github.io/logo192.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ],
  "start_url": "https://yushimatenjin.github.io/",
  "display": "standalone",
  "background_color": "#f16625",
  "theme_color": "#f16625"
}

https://yushimatenjin.github.io/manifest.json

serviceWorker.jsにはキャッシュについてを記述します。

https://yushimatenjin.github.io/serviceWorker.js

serviceWorker.js
    var CACHE_NAME = 'coin-dozer-version-1';
    var urlsToCache = [
    キャッシュするファイル....
    ];

    self.addEventListener("install", function(event) {
      event.waitUntil(
        caches.open(CACHE_NAME).then(function(cache) {
          return cache.addAll(urlsToCache);
        })
      );
    });

    self.addEventListener("fetch", function(event) {
      event.respondWith(
        caches.match(event.request, {
            ignoreSearch: true
        }).then(function(response) {
          if (response) {
            return response;
          }
          return fetch(event.request);
        })
      );
    });


serviceWorker.jsを一瞬で作る

playcanvas-toolsというnpmのパッケージを作りましたのでこちらを使用すると、1つのコマンドでserviceWorker.jsを作ることができます。

ソース
https://github.com/yushimatenjin/playcanvas-cli

npm

https://www.npmjs.com/package/playcanvas-tools

npx

cd PlayCanvasのプロジェクトを解凍したフォルダ
npx playcanvas-tools sw --name キャッシュ名

PlayCanvas特有の問題

PlayCanvasはロードするたびにクエリーストリングを追加します。そのURLについてもキャッシュを適用させるためにcaches.match無視するオプションを追加します。

https://developer.mozilla.org/en-US/docs/Web/API/Cache/match

serviceWorker.js
caches.match(event.request, {
        ignoreSearch: true
})
serviceWorker.js
self.addEventListener("fetch", function(event) {
  event.respondWith(
    caches.match(event.request, {
        ignoreSearch: true
    }).then(function(response) {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});

このオプションを追加することでキャッシュから呼び出されるようになります。

index.htmlからServiceWorkerを読み込むコードを追記する

index.htmlのHead内にこちらのコードを追加します。

index.html
<script>
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker
        .register("./serviceWorker.js")
        .then(function(registration) {
          if (typeof registration.update == "function") {
            registration.update();
          }
        })
        .catch(function(error) {
          console.log("Error Log: " + error);
        });
    }
  </script>

https://github.com/yushimatenjin/yushimatenjin.github.io

キャッシュ無し

キャッシュ有り

キャッシュをしないと28.1MBのダウンロードをしていたファイルをキャッシュすることで、2回目以降の読み込みを24.2KBまでにすることにできます。

インストールが可能になる

さらに下記の要件を満たすことでAndroidの端末へインストールすることができます。

  1. manifest.jsonに必要な要件を入れる
    • short_nameかname
    • icons 192pxか512px
    • start_url
    • display,fullscreen,standalone,minimal-uiのうちどれか
  2. HTTPSの設定
  3. fetchイベントハンドラをserviceWokerに追加する

Androidの表示

iOSの表示

Storeにも出せる

TWAと言われる形でビルドをすることでストアに出すこともできました。

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

jsで配列の特定keyのだけ取り出してカンマ区切りの文字列にする

jsで配列の特定keyのだけ取り出してカンマ区切りの文字列にする

言葉だけだと何言ってるの??ってなるので

表示だけ考えて文字列でカンマ区切りで最後は必要ないってやつ

// これを
const array = [
  {id: 1, category: 'ねこ', name: 'ポチ'},
  {id: 1, category: 'ねこ', name: 'ねこねこ'},
  {id: 1, category: 'いぬ', name: 'にゃん'},
];

// ↓↓↓ こうしたい ↓↓↓
const string = 'ポチ, ねこねこ, にゃん';

結論

function(array) {
   // 配列からnameだけ取り出して「,」区切りの文字列にする
   return array.map( (row) => {return [row['name']]} ).join(',')
},

分解

function(array) {
   // 配列からkeyのnameだけ取り出した配列を作る
   const namesArray = array.map((row) => {
       return [row['name']]
   });

   // 配列を「,」区切りの文字列にする
   const namesString = nameArray.join(',');
},

Vue.jsで実際に使った時の用途

<template>
  <div>
    <span>
       <!-- 「ねこ, にゃん, ぽち」 のように表示する --!>
      {{ array | convertArrayToString }}
    </span>
  </div>
</template>

<script>
  data: {
    array: array
  },
  filters: {
    convertArrayToString(array) {
      // 配列からnameだけ取り出して「,」区切りの文字列にする
      return array.map( (row) => {return [row['name']]} ).join(',')
    },
  }
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jsで配列の特定keyだけ取り出してカンマ区切りの文字列にする

jsで配列の特定keyだけ取り出してカンマ区切りの文字列にする

言葉だけだと何言ってるの??ってなるので

表示だけ考えて文字列でカンマ区切りで最後は必要ないってやつ

// これを
const array = [
  {id: 1, category: 'ねこ', name: 'ポチ'},
  {id: 1, category: 'ねこ', name: 'ねこねこ'},
  {id: 1, category: 'いぬ', name: 'にゃん'},
];

// ↓↓↓ こうしたい ↓↓↓
const string = 'ポチ, ねこねこ, にゃん';

結論

function(array) {
   // 配列からnameだけ取り出して「,」区切りの文字列にする
   return array.map( (row) => {return [row['name']]} ).join(',')
},

分解

function(array) {
   // 配列からkeyのnameだけ取り出した配列を作る
   const namesArray = array.map((row) => {
       return [row['name']]
   });

   // 配列を「,」区切りの文字列にする
   const namesString = nameArray.join(',');
},

Vue.jsで実際に使った時の用途

<template>
  <div>
    <span>
       <!-- 「ねこ, にゃん, ぽち」 のように表示する --!>
      {{ array | convertArrayToString }}
    </span>
  </div>
</template>

<script>
  data: {
    array: array
  },
  filters: {
    convertArrayToString(array) {
      // 配列からnameだけ取り出して「,」区切りの文字列にする
      return array.map( (row) => {return [row['name']]} ).join(',')
    },
  }
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js boilerplate / Authentication from scratch - (express, graphql, mongodb)

1*htOb7MGk4cXDpp4md_iHSQ.png

A boilerplate for Node.js apps / API server / Authentication from scratch - express, graphql - (graphql compose), mongodb - (mongoose).

https://github.com/watscho/express-graphql-mongodb-boilerplate :snowman2:

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

asyncを使用したコードをjestでテストすると「regeneratorRuntime is not defined」エラーが発生

確認環境

  • Node.js: 10.16.0
  • Jest: 24.9.0
  • @babel/core: 7.7.5

エラー内容

jestでテスト実行すると、asyncの記述の部分で、以下のようなエラーが発生しました。

    ReferenceError: regeneratorRuntime is not defined

       9 | }
      10 | 
    > 11 | const main = async () => {
         |              ^

対応

.babelrcにて、babelのターゲットを指定することで解決しました。
変更前:

.babelrc
{
  "presets": [
    "@babel/preset-env"
  ]
}

変更後:

.babelrc
{
  "presets": [
    [
      "@babel/preset-env", {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

以上。

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

JavaScriptで日本語DSLを作る

概要

普段のプログラムを逆ポーランド記法(後置記法)で書きたいとは思いませんが…
プログラミングに明るくない方に簡単なスクリプティングのようなことをしてもらう必要がある場合、日本語の識別子、日本語の語順(≒逆ポーランド記法)のDSLを用意できると便利なこともあるかもしれません。
というわけで何個目の車輪か見当も付きませんが作ってみました。


See the Pen
rpn
by shinji (@shinji709)
on CodePen.


逆ポーランド記法(後置記法)について少し

2つの引数をとり加算結果を返す加算演算子+を考えたとき、呼び出しの際+ 1 2などと関数を引数より前に置く記法を前置記法、1 + 2などと引数と引数の間に置く記法を中置記法、1 2 +などと引数の後ろに置く記法を後置記法といいます。
後置記法では1と2を足すと自然と読み下すことができ、日本語と相性がいいです。中置記法と違い計算順序が一意に定まりカッコが要らないなど他の利点もあるようですが、本トピックで逆ポーランド記法を採用するのはこの理由だけです。

コード主要部の説明

主要な部分は以下7行だけです。(続けて四則演算の単純な使用例も付記しています)

const createRpn = dictionary => words => words.reduce((acc, word) => {
  if (word in dictionary) {
    return acc.concat(dictionary[word](...acc.splice(-dictionary[word].length || Infinity)));
  } else {
    return [...acc, word];
  }
}, []);
// 以下使用例
// 辞書の定義
const dictionary = {
  足す: (a, b) => Number(a) + Number(b),
  引く: (a, b) => Number(a) - Number(b),
  掛ける: (a, b) => Number(a) * Number(b),
  割る: (a, b) => Number(a) / Number(b),
};
// DSL処理関数の生成
const rpn = createRpn(dictionary);
// サンプルDSLコード
const code = `
  3 4 足す
  8 5 引く
  割る
`;
const codeArr = code.trim().split(/\s+/);
console.log(codeArr);
//=> ["3", "4", "足す", "8", "5", "引く", "割る"]
// 中置記法で書くと(3 + 4) / (8 - 5)ということ
// 処理
const result = rpn(codeArr);
console.log(result);
//=> [2.3333333333333335]

DSL内の識別子をキー、その関数実装を値として持つオブジェクト、dictionaryを取り、コードを配列として取って結果を返す関数を返します。

処理時は、受け取ったコード(文字列の配列)をreduceにより先頭から順に処理していき処理結果としての新しい配列を作ります。
文字列が辞書に定義されていなければ、文字列をそのまま結果配列に入れていきます。
文字列が辞書に定義されている単語であれば、その実装関数の仮引数の個数(Function.prototype.lengthにより取得)分の要素を結果配列からpopし(popは使ってませんが…)、それを引数として実装関数を呼び出し、戻り値を結果配列に入れます。

実装にあたってはプログラミング言語Forthをほんの少し参考にしています。
「ほんの少し」というのは、「ほとんどオレが考えたんだぜ」というのではもちろんなく、せっかく参考にさせていただいたが本家の足元にも及ばないという意味です。念の為。(ちなみにwordとか、dictionaryというのはforthの用語です)

UIコード部の説明

主要コード部以外はほぼUIコードです。
コード入力エリアのonchangeで実行しようと思ってましたが、サンプルコードにユーザー入力を入れた関係でいちいち実行ボタンを押す形式に変更しました。
コード入力エリアの右にはdictionaryオブジェクトに定義されている単語を列挙し、ボタンを押すことでカーソル位置に単語を挿入できるようにしています。もちろん直接入力でもいいです。またPCであればボタンホバーで各単語の実装がツールチップで表示されます。(横着してtitle属性を使いました)

まとめ・感想

思っていたより簡単にできました。JavaScriptのコードで言う、foo();bar();baz()とか、foo(bar(baz()))などの単純な呼び出しのみの処理だからですね。
制御構造を入れだすととたんに難しくなりそうな気がしますが、追々挑戦していきたいです。
JavaScriptはライブラリが充実していてけっこう何でもあるので今回のコード利用して色々日本語DSL作ってみたいですね。まあ、自分では使わないんですけどね。
(今回のサンプルでは、"D"SLっていうけど一体どんなドメインなんだ?というようなしょうもないdictionaryですみません。思いつかなかったんです…)
以上です。
質問、追記・修正要請あればコメント欄にお願いします。

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

【Nuxt.js】slot基礎編:コンポーネントに自由にデータを渡そう

前置き

Nuxt.jsと言えばコンポーネント。
componentの内容を、ページによって少しで良いから変えたい…
そんな時、slotに随分と助けられたので記録に残しました。
Vue.jsでも同様の使い方が可能です。
※サンプルテキストはHello Nuxt.js!ではなくHello Vue.js!になっていますが、ご了承ください。

間違い等がございましたら、優しくご指摘いただけると嬉しいです!

基礎編:超簡単な解説から実践編まで
応用編:全体のまとめ、propsとの違い
を書いていきます。

slotって?メリットは?

ウェブサイトを作ってて、
「同じコードをコピペしまくってめんどいなぁ〜
 あ!コンポーネント使えばいいじゃん!!
 あれ?でもページによって○○だけ変えたいんだよなぁ」
って時、ありますよね?

コンポーネントを使いまわしたいけど、
ここの文字だけ変えたい!って時に使います。
(文字以外も変更は利きますが、今回はわかりやすく文字で例えます)

コンポーネントが使えないから、
基本のコードコピペして中身だけ編集する…
なんて面倒なことをしなくていいんです!
コンポーネントにしちゃって、一部だけ変えればいいんです!
楽ちん!!

slotを使わない場合どうなるの?

じゃあコンポーネントを使って…親コンポーネントで中身を変えよう!
slotなくてもの中身に書けばよくない?
と思ったそこのアナタ!
こちらをご覧ください。

Component.vue // 子コンポーネント
<div class="ItemTitle">
 <h1>ここは親ページによってタイトルを変えたい</h1>
</div>
index.vue // 親ページ
<Component>
 Hello Vue.js!
</Component>
表示結果
<div class="ItemTitle">

</div>

そうなんです。。。
子のコンポーネントの中身は無視されちゃいます。
それも完全なシカトです。ツライ。。。

<Component>ここは無視される</Component>

slotの使い方(超簡単ver.)

そこでslot君の登場です!!
使い方は超〜〜〜簡単。
子コンポーネント内に変えたい部分をslotにするだけ。
上の説明に使ったコードでやってみます。

【表示結果】
picture_pc_96a0996a6f584fe201bfcad0b23a9059.png

<div class="itemTitle">
 <h1>Hello Vue.js!</h1>
</div>

【コード】

子コンポーネントだけslotを加えます。

Component.vue // 子コンポーネント
<div class="itemTitle">
 <h1>
  <slot /> // ここは親ページによってタイトルを変えたい
 </h1> 
</div>

<style scoped>
.itemTitle {
 margin: 10px;
}
h1 {
 color: rgb(65, 193, 222);
}
</style>

親は一緒。

index.vue // 親ページ
<Component>
 Hello Vue.js!
</Component>

これだけで表示結果が変わる!!
slotくんに呼びかけて、ちゃんとデータを表示してくれます。
偉いぞslotくん!!!

slotの使い方(複数ver.)

便利なslotくん
「じゃあいっぱいslot使おう!
 h1とh2にも使おう〜」
って場合は
各slotに名前をつけてあげる必要があります。

「これ表示して?」って言われても、
名前で呼んでくれないと、
どのslotくんが表示すればいいのか分からなくなっちゃいますよね。

そういう先輩いません?
「これやっといて〜」って、
今誰に言ったの?私やるの???え、誰???
みたいな。

なので、
もし名前をつけないでslotを2つ用意すると
親に入れたものが2回表示されます。
「はいはい、私やりますね」って2人反応しちゃいます。

名前で呼ばなかった先輩はきっと
同じ仕事を無駄に2個やらせてしまったことを後悔するでしょう。
こんなにいらんわ。。。って。

【表示結果】
picture_pc_cd512416e11b9b6b113ed8fa8676b625.png

<div class="ItemTitle">
 <h1>
  Hellot Vue.js!
  about slot
 </h1>
 <h2>
  Hellot Vue.js!
  about slot
 </h2>
</div>

【コード】

Component.vue // 子コンポーネント
<div class="ItemTitle">
 <h1>
  <slot /> // ここは親ページによってタイトルを変えたい
 </h1>
 <h2>
  <slot /> // ここは親ページによってサブタイトルを変えたい
 </h2>
</div>

<style scoped>
.itemTitle {
 margin: 10px;
}
h1 {
 color: rgb(65, 193, 222);
}
h2 {
 color: gray;
}
</style>
index.vue // 親ページ
<Component>
 Hellot Vue.js!
 about slot
</Component>
表示結果
<div class="ItemTitle">
 <h1>
  Hellot Vue.js!
  about slot
 </h1>
 <h2>
  Hellot Vue.js!
  about slot
 </h2>
</div>

slotの使い方(実践ver.)

ということで各slotくんに名前をつけてあげましょう。
「◯◯くん、これ表示しといてよ」って言ってくれたら、
はい、私ですね!表示しますね!ってなりますよね。

ここで1つ注意。
名前をつける場合は、
親でタグで囲う必要があります。
これさえできればもう完璧。

【表示結果】
picture_pc_912dbfccaa95a0ce68d99cf19a889283.png

<div class="itemTitle">
 <h1 class="title">Hello Vue.js!</h1>
 <p>slotを学ぼう</p>
 <div class="catchCopy"># slotって?メリットは?</div>
</div>

【コード】

ItemTitle.vue // 子コンポーネント
<div class="itemTitle">
 <h1>
  <slot name="title" /> // 置き換えたい部分のslot nameを決める
 </h1>
 <p>slotを学ぼう</p> 
 <h2>
  <slot name="catchCopy" /> // 置き換えたい部分のslot nameを決める
 </h2>
</div>

<style scoped>
.itemTitle {
 margin: 10px;
}
h1 {
 color: rgb(65, 193, 222);
}
p {
 color: gray;
h2 {
 color: rgb(76, 212, 227);
}
</style>
index.vue // 親ページ
<ItemTitle>
 <template v-slot:title>
  Hello Vue.js! // slot name="title"を置き換える
 </template>
 <template v-slot:catchCopy>
  # slotって?メリットは? // slot name="catchCopy"を置き換える
 </template>
</ItemTitle>

おめでとうございます!
これでVue.js、Nuxt.jsのslot、今日から使えますね!わーい!
次は更なる応用も記載していきます★

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

jsで配列の特定keyを指定してgroupByをしたい(lodashすげ〜)

jsで配列の特定keyを指定してgroupByをしたい(lodashすげ〜)

結論

lodashすげ〜!!

const array = [
  {id: 1, category: 'ねこ', name: 'ポチ'},
  {id: 1, category: 'ねこ', name: 'ねこねこ'},
  {id: 1, category: 'いぬ', name: 'にゃん'},
];
const key = 'category';

const res = _.groupBy(array, key);

まとめ

lodashはすごいからとりあえずドキュメント読んでおこう!!

Lodash → https://lodash.com/docs/4.17.15
LodashのgroupBy → https://lodash.com/docs/4.17.15#groupBy

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

ラーメンで理解するasync/await

JavaScript 2 Advent Calendar 2019 の19日目の記事です。

対象

  • async/awaitがなんだかはある程度知ってる人
  • async/awaitをなんとなくで使ってる人

そもそもasync/awaitって?

async/awaitは、Promiseによる非同期処理をより簡潔に効率よく記述できる手法。
普通にPromiseを使うとネストが深くて辛くなるのを救ってくれる。
「async/await Promise」で検索すれば比較についてはたくさん出るので今回は書かない。

便利だから全部async/awaitにしちゃおう!

って思うんですけど、実は罠があって。
ちゃんと気をつけないと非効率な感じになっちゃうよっていうのが今回のお話。
ただ、コードを並べて説明してもよくわからない気がしたので、みんな大好きラーメンで例えてみようと思います。

image.png

ラーメンを作ろう!

突然ですが、あなたはラーメン屋店主です。
あなたのラーメン屋には腕のいい三人の店員がいます。

それぞれ、

  • 麺担当
  • スープ担当
  • 具担当

とします。

(なんかどっかで見た)

ラーメンを作るためには、

  1. 麺担当に麺をゆでてもらう
  2. スープ担当にスープを作ってもらう
  3. 具担当にチャーシューを切ってもらう
  4. 1~3までが出来たらそれらを組み合わせてあなたがラーメンを完成させる

という工程が必要になります。

APIを準備しよう!

こんなAPIが用意されているとしましょう。

エンドポイント メソッド 概要
/noodle GET 麺を作成し取得する
/soup GET スープを作成し取得する
/pork GET 具を作成し取得する
/ramen POST ラーメンを作る

実装してみよう!

なんのこっちゃない、かんたんかんたん

async () => {

  // 麺担当に麺をゆでてもらう
  const noodles = await axios.get('/noodle');

  // スープ担当にスープを作ってもらう
  const soup = await axios.get('/soup');

  // 具担当にチャーシューを切ってもらう
  const pork = await axios.get('/pork');

  // ラーメンを完成させる。
  const ramen = await axios.post('/ramen', {
    noodles,
    soup,
    pork
  });

  // 提供
  return ramen;
}

ちょちょいのちょい!
すっきりしてて良さそうですが、これはNGです。

awaitの罠

awaitは、指定した関数のPromiseの結果が返されるまで、async function内の処理を一時停止します。
つまりどういうことかというと、

await axios.get('/noodle');

の結果が帰ってくるまで処理を待ってしまいます。
その間 /soup /pork へのリクエストはされません。

つまり、
麺を茹で上がるのを待って、スープを作り始め
スープが出来上がるのを待って、チャーシューを切り始める
などといったものすごく非効率なことになってしまうのです!

そんなことしてたら出来上がる頃には麺がのびのびになってしまう...

一人ずつ作業が終わるのを待つのではなく、三人店員がいるんだから同時に指示をしたいですよね?

どうすればいいの?

awaitをするタイミングをずらします。

async () => {

  // 麺担当に麺をゆでてもらう
  const noodlesPromise = axios.get('/noodle');

  // スープ担当にスープを作ってもらう
  const soupPromise = axios.get('/soup');

  // 具担当にチャーシューを切ってもらう
  const porkPromise = axios.get('/pork');

  // 全員が出来上がるのを待つ
  const noodles = await noodlesPromise;
  const soup = await soupPromise;
  const pork = await porkPromise;

  // ラーメンを完成させる。
  const ramen = await axios.post('/ramen', {
    noodles,
    soup,
    pork
  });

  // 提供
  return ramen;
}

awaitはPromiseを返す処理とセットにしがちですが、別にバラバラに書いても良いのです。

これを行うことで、
麺担当・スープ担当・具担当に同時に調理指示をする。
その後全員が調理終了し、完成したらそれらを使ってラーメンを完成させる。
ということが実現できるわけです。

image.png

完成!やったね!

まとめ

ラーメンでふわふわっとした感じになっちゃいましたが、伝えたかったことは
待つ必要がある場合は待つ、待つ必要がない場合は待たない
ということをちゃんと意識して書くと良さそうっていうあれです。

最後に

書いててラーメン食べたくなってしまったのでラーメン食べに行ってきます。

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

Nuxt.jsとmysqlを連携してデータを表示してみた

はじめに

前から気になっていたNuxt.jsを触ってみました。これ使って何かやってみようと考えた結果、
mysqlと連携させてみることにしました。Nuxt.jsに関して入門的な情報は既にいろいろと存在するので、
ここでは、そのつなぎの部分にフォーカスして記載したいと思います。

内容

下記の項目に沿って記載します。

  • Nuxt.jsについて
  • Nuxt.jsのインストール
  • データベース準備
  • 必要なモジュール
  • APIの実装
  • フロントエンドの実装
  • 結果
  • おわりに

Nuxt.jsについて

ご存知の方には恐縮ですが、Nuxt.jsはナクストと読むようであり、
Webアプリケーションを開発するのに必要なものが既にある程度整った状態で
スタートできるVue.jsベースのJavaScriptフレームワークです。
UIではVue.jsを使いますが、UI以外の機能についてもいろいろ組み込まれているようです。
フレームワークなので効率的にアプリケーションの開発ができるようですね。

Nuxt.jsのインストール

早速、自分のPCにインストールしてみます。
事前にnpm と Node.jsがインストールされていることが前提です。

npx create-nuxt-app プロジェクト名

この形式でインストールするようなので、

npx create-nuxt-app prj

プロジェクト名をprjとして実行してみました。
いくつか聞かれるので、

  • server framework を Express
  • Nuxt.js module をAxios
  • rendering mode をUniversal (SSR)

として設定しました。
何度も繰り返し試した結果、上記の設定に落ち着きました。
Nuxt v2.10.2 がインストールされたようです。

次に、アプリを起動する必要があるので、

npm run dev

を打ち込むと、サーバーが起動します。
nuxtserver.png

npm run スクリプト名

ここで、上記の形式で入力すると、package.json(下記)で記述されているscripts内のスクリプト名
(この場合は"dev")で定義されたコマンドが実行されます。

{
  "name": "prj",
  "version": "1.0.0",
  "description": "My swell Nuxt.js project",
  "author": "**** ****",
  "private": true,
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
    "build": "nuxt build",
    "start": "cross-env NODE_ENV=production node server/index.js",
    "generate": "nuxt generate"
  },
  "dependencies": {
    "@nuxtjs/axios": "^5.3.6",
    "cross-env": "^5.2.0",
    "express": "^4.16.4",
    "fs": "0.0.1-security",
    "mysql": "^2.17.1",
    "net": "^1.0.2",
    "nuxt": "^2.0.0",
    "tls": "0.0.1"
  },
  "devDependencies": {
    "nodemon": "^1.18.9"
  }
}

下記URLをブラウザで表示すると、

http://localhost:3000

nuxtinit.png

こんな感じに初期表示されました。
Nuxt.jsのデフォルトポートは3000のようです。
インストールされたあとのディレクトリ構造は下記のようになってました。

prj----assets
    |--components
    |--layouts
    |--middleware
    |--node_modules
    |--pages
    |--plugins
    |--server
    |--static
    |--store
    |nuxt.config.js
    |package.json

調べたところ、最初に表示されたページは、

prj--
    |--pages
         |index.vue

pagesディレクトリ内にあるindex.vueに記載された内容でした。

<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        prj
      </h1>
      <h2 class="subtitle">
        My laudable Nuxt.js project
      </h2>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

<script>
import Logo from '~/components/Logo.vue'

export default {
  components: {
    Logo
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

ごちゃごちゃ書かれているので、余計な記述をバッサリ削除してコードを見やすくします。

<template>
  <div class="container">
   あいうえお
  </div>
</template>

<script>
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
</style>

表示は、下記のようになりました。

nuxtinit2.png

ここで気づいたのが、このindex.vueファイルを更新した場合、
ブラウザで再描画しなくても(F5押さなくても)自動でブラウザに反映されることです。
index.vueファイルを更新するたびに、自動でコンパイルが走るようで、
それが終わり次第ブラウザの表示が変更されるようです。

データベース準備

mysqlと連携させて何かしたいな?と考えたところ、
一番簡単なお試し内容として、DBに登録されているデータを取り出して、
それをブラウザに表示させてみるのがよさそうかなと思い、それを実装
してみることにしました。

なので、まずデータベースの準備をします。ローカル環境のmysqlにテスト用のデータベースを作成し
その中に都道府県情報のテーブルを作成しました。
データベース名:testdb
テーブル名:prefectures
prefdb.png
この情報を取り出してブラウザに表示させてみようと思います。

必要なモジュール

mysqlを使うには、どうやらmysql用のモジュールをインストールする必要が
あることがわかりましたので下記を実行します。

npm install --save mysql

APIの実装

index.vueの中のscritpt要素の中に、データベースアクセスの為の記述をしても
うまくいかずエラーを解消することができなかったので、
別途APIを作成し、そこにアクセスすることでDBの内容をとってくる、
という方法にしました。

prj--
    |--server
         |api.js

serverディレクトの直下にapi.jsというファイルを作成し、
dbに接続してデータを取ってくるコードを記述します。

const express = require('express')
const router = express.Router()
router.get('/prefectures', (req, res, next) => {
  const mysql = require('mysql');
  const connection = mysql.createConnection({
    host : 'localhost',
    user : 'testuser',
    database: 'testdb',
    password: 'testuser'
  });
  var ret=[];
  connection.connect();
  connection.query('SELECT * from prefectures;', function(error, row, fields){
    if (error) {
      console.log(error);
    } 
    var dat = [];
    for (var i = 0;i < row.length; i++) {
      dat.push({id: row[i].id, name: row[i].name});
    }
    ret = JSON.stringify(dat);
    res.header('Content-Type', 'application/json; charset=utf-8')
    res.send(ret)
  });
  connection.end();
})
module.exports = router

実はこれだけではだめなようで、同じディレクトリに存在するindex.jsというファイルを
一部編集しないといけないようです(★印の箇所)

const express = require('express')
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')
const app = express()

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = process.env.NODE_ENV !== 'production'

//↓↓↓★この行の追加
const apiRouter = require('./api')
async function start () {
//↓↓↓★この行の追加
app.use('/api', apiRouter)

  // Init Nuxt.js
  const nuxt = new Nuxt(config)

  const { host, port } = nuxt.options.server

  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  } else {
    await nuxt.ready()
  }

  // Give nuxt middleware to express
  app.use(nuxt.render)

  // Listen the server
  app.listen(port, host)
  consola.ready({
    message: `Server listening on http://${host}:${port}`,
    badge: true
  })
}
start()

下記でアクセスするとDBからのデータをJSON形式でとってこれるようになりました。

http://localhost:3000/api/prefectures

nuxt_api.png

参考サイト
https://dev.classmethod.jp/etc/node-js-module-mysq/
(他にもいくつか参考サイトがあったのですが忘れてしまった。。)

フロントエンドの実装

axiosというライブラリを使ってhttp通信を行います。
Node.jsで動作するhttpクライアントであり、通信するためのAPIが提供されているので
これを利用します(Ajaxのようなものですね。多分)

上記でばっさり削除したindex.vueの中を下記のように変更します。
先ほどのAPIにアクセスして取ってきたデータをitemsの中に格納すると、
Vue.jsによってフロントエンドに反映されます。

<template>
  <div class="container">
    <table border="1">
      <tbody>
      <tr>
        <th>ID</th>
        <th>都道府県名</th>
      </tr>
      <tr v-for="item in items" :key="item.id">
        <td>{{ item.id }}</td>
        <td>{{ item.name }}</td>
      </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: []
    }
  },
  mounted: function() {
    this.$axios
      .$get('/api/prefectures')
      .then(response => {
        this.items = response
      })
      .catch(error => {
        console.log(error)
      })
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
</style>

結果

表示は、下記のようになりました。

nuxtresult.png

おわりに

今回は、mysqlとの連携に注目しましたが、Nuxt.jsに関する知識が浅いこともあり、
もっといろんなこともできると思われますので、今後も引き続きいろいろ試してみたいと思います。
私はPHPを使っての作業が多いので、JavaScriptだけで当たり前のようにWebアプリケーションを
作れるようなったことに関心を寄せています。
試した内容は大したものではないですが、それでも途中いくつもの壁にぶち当たりました。
が、なんとか乗り越えました。
今後はよりスムーズに実装できるよう一層進化していくことと思います。

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

kintone Spreadsheetプラグインを作り直している話

Spreadsheetプラグインについて

kintoneの一覧画面をExcelのようなスプレッドシートの見た目で閲覧、編集をするためのプラグインを作っています。
https://github.com/mura-/kintone-spreadsheet

正確には、もともと下記リポジトリで作っていたのを書き直している状況です。

https://github.com/mura-/kintone-spreadsheet-no-longer-maintained
image.gif

なぜ書き換えたか、変えたこと

最初書き始めたのが4年前で古く、コードの品質もよくない、タスクランナーなどの周辺ツールも古いという点と、そのためにここからグレードアップしようにもやる気がしないという点が大きいです。

今回の新開発からは、下記のようにHandsontable使っている以外は、色々変えて作り直しています。

今回 前回
言語 TypeScript JavaScript
ライブラリ React, kintone UI Component Vue
グリッド表示ライブラリ Handsontable Handsontable
バンドラ Webpack + plugin-uploader Gulp + browserify

今回vueからreactに変えたのは、kintone UI Componentとも親和性が高いのもありますし、Vueよりは記述量が増えてしまいますがプリミティブに書きやすいという僕の好みによります。
kintone plugin-uploaderも今回利用しており、Build後コマンドラインでアップロードできるようにもしています。Watchモードにおいては、再Build後自動でアップロードするよう、WebpackのPlugin設定を変更して開発・デバッグしやすくしています。

現在の進捗

現時点では設定画面から任意のフィールドを表示する、というところまではできています。
no longer maintainedの方は編集までできますが、まだ追いつけていません。

今後どうするか

ひとまずno longer maintainedで実装できていた, データ編集までは実装を進めます。
その後はまだ検討中ですが、少しずつグレードアップしていく予定です。

ちなみに、krewSheet など、すでに展開されているサービスと違うのは、機能はだいぶ少ないですが、無料、あるいは低価格で使えることを目指しています。(半分趣味)

もしくはIssueに要望をいれていただければそこから対応するかもしれません!要望あればぜひご意見ください。

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

intra-martにコード整形ツールを実装してみた

JSONとSQL整形問題

業務中結構使われているJSONとSQL文が
毎回毎回変換を掛けてから、ようやく中身を確認することができる。
それが面倒だ!!

intra-mart環境でも使えるツール

FormaDesignerが簡単に画面が作れるし、
それに、JSもすぐ試せるので、作ってみた。
QS_20191218-165620.png
QS_20191218-165559.png

JSONの整形コードは以下の通り
js
var jsonBefore = JSON.parse(text);
var json = JSON.stringify(jsonBefore, null, " ")

二行で今までの悩みを紹介した。

SQLの整形はこのリンクを参照した。

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

認可Cookie持ってなかったらリダイレクトさせる

認可Cookie持ってなかったらリダイレクトさせる

目的

index.htmlに,「18歳以上ですか」というお決まりの案内を出しているが,URLベタが記されると目的のエロページに辿り着いてしまう.
そこで18歳ボタンを押した人だけが,エロページに行けるようにしたかった.
(という建前で,本音は会社での成果発表で,意図せずエロページ出さないようにするため)

やってみる

1.index.htmlの「18歳以上ですか」に対する「はい」ボタンクリック時に,認可Cookie(ていうほどのもんじゃないけど)をセット.

index.html
<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
   <!-- onClickに認可Cookie払い出し関数を紐付ける -->
   <a href="eropage.html"><button type="button" onClick="yesBtn();">はい</button></a>
   <script src="index.js"></script>
  </body>
</html>
index.js
function yesBtn() {
    //認可Cookieのセット
    document.cookie = "auth=over18; max-age=3600";//有効期限は3600秒=1時間
}

2.認可Cookieを持ってエロページに遷移.ここで認可Cookieがなければ,index.htmlにリダイレクト

eropage.js
// 認可Cookieを持っていなければ,リダイレクト
var cookies = document.cookie;
if (!cookies) {
    location.href="index.html";
}

このドメインでは,Cookieはこの1つのみ扱うため,cookieを持っているか否かで判定を行なっている.
持っていないければ,index.htmlにリダイレクトさせる.

終わりに

これで,直打ちされても表示は回避でき,意図しないエロサイト表示は避けられた.

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

漢文風プログラミング言語「文言」の基本文法を読み解いてみた

導入

以下のツイートを見かけまして。



すごい。そしてGitHubで公開されているみたいなので早速触ってみました。

概要

  • 言語名:文言
    • Wenyan-lang
  • 拡張子:.wy
  • 実行方法:JavaScriptかPythonに変換
  • ライセンス:MIT License
  • その他:src/render.jsでソースコードを原稿用紙にレンダリングできる

基礎文法

READMEに掲載されている例に基づき、取り急ぎ変数・関数・条件分岐・繰り返しのみ読解し、意味を読み解いてみました。これである程度のことは書けるはず。
原文READMEの文言例とJavaScript例は変数名や文字列を適宜翻訳して対応付けていますが、今回はわかりやすいように両者ともアルファベットにしました。アルファベットでも動きます。文法を覚えたらぜひ漢字で変数名や関数名などを付けてみてください。

変数宣言

文言 JavaScript
吾有一數。曰三。名之曰「a」。 var a = 3;
吾有一言。曰「「text_text_text」」。名之曰「str」。 var str = "text_text_text";
吾有一爻。曰陰。名之曰「flag」。 var flag = false;
吾有一列。名之曰「array」。 var array = [];
有數五十。名之曰「b」。 var b = 50;

対応表

文言 意味
漢数字(一二三……) 英数字(123...)
0
数値型
文字列型
真偽値型
配列
「」で囲まれた部分 変数名
「「」」で囲まれた部分 文字列
半角スペース

解説

  • 吾有一數。曰num。名之曰「var_name」。
  • 有數num。名之曰「var_name」。

で変数var_nameに数字numを代入して変数宣言できます。変数名は「」で囲む。初期値なしの変数宣言はできない。
前者の書き方の場合は、冒頭の吾有一數で宣言する変数の数を表しているため、ここの数字の分だけ後ろに曰num。名之曰「var_name」。を書き連ねる必要があります。
面倒なので有【型】~名之曰「【変数名】」だけ使えば良いと思う。

補足

実はは全部varになるので間違っても動くには動きます。

変数代入

文言 JavaScript
昔之「a」者。今「b」是也。 a = b;

対応表

文言 意味
昔之~者。今~是也。 =(代入)

条件分岐

文言 JavaScript
若三大於二者。乃得「「true_text」」也。 if (3>2){ return "true_text"; }
若三不大於五者。乃得「「true_text」」。若非。乃得「「false_text」」也。 if(3<=5){return "true_text"}else{return "false_text"}

対応表(条件分岐)

文言 意味
if
乃得 then
若非 else
end(条件分岐用)

対応表(真偽値)

文言 意味
a等於b a == b
a不等於b a != b
a大於b者 a > b
a不大於b者 a <= b
a小於b者 a < b
a不小於b者 a >= b
中無陰乎 and
中有陽乎 or
not

解説

スペースはいくら入れてもよいのでインデントも可能です。インデント例は下記の通り。

若三不大於五者。乃得
    「「true_text」」。
若非。乃得
    「「false_text」」
也。

繰り返し

文法的な近さの関係上この項目のみJavaScriptではなくRubyと対応付けます。(注意:2019年12月18日現在、文言をRubyにコンパイルすることはできない。)

文言 Ruby
為是n遍。⋯⋯ 云云。 n.times do ... end
恆為是。⋯⋯ 云云。 loop do ... end
凡「array」中之「elem」。⋯⋯ 云云。 for elem in array do ... end
乃止。 break;

対応表

文言 意味
為是n遍 云云(end)までの処理をn回繰り返す
恆為是 云云(end)までの処理を無限に繰り返す
云云 end(繰り返し用)
乃止 break

補足

実は云云はどちらもendに変換されるためどちらでもよい。

おわりに

文言の基礎文法だけ読み解いてみました。さらに発展的な書き方については、公式のREADMEまたはソースコードをお読みください。
とにかくsrc/render.jsでソースコードを原稿用紙に出力できるのが楽しみすぎるので何か書きたい。皆さんも漢字文化圏の特典を享受してプログラミング言語「文言」を楽しんでください!

参考文献

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

【vue】Vue Styleguidistの使い方を説明① 〜Laravel + vue環境でVue Styleguidistを動かす〜

※元々ブログに書いていたのですが、qiitaに転載しました
https://www.moyashidaisuke.com/entry/vue-styleguidist-install

概要

vueのstyleguild「Vue Styleguidist」をLaravel + vue環境で使い始めてみました。
思ったより手間取ってしまったので、設定ファイルや、私の環境で発生したエラーの対応等を残しておきます。

↑のカメレオンはVue Styleguidistのロゴ。きもかわいい。

※Laravel環境じゃなくても多分参考になると思います。

Vue Styleguidistとは

とりあえず動作サンプル見た方がわかりやすいのでいきなりですがリンクを。

https://vue-styleguidist.github.io/basic/

こういうコンポーネントの仕様と、サンプルのドキュメントを生成してくれるツールです。 こういうの。

20191005220952.png

GitHubのStar数はこの記事を書いてる時点で1419なので、デファクトになってる感はまだ無いですが、競合のvueseよりはstar数多いのでこちらを採用しました。
https://github.com/vue-contrib/vuese

導入手順(理想形

特にはまらないですんなりいくパターン。

https://vue-styleguidist.github.io/docs/GettingStarted.html#_1-install

前提

  • Laravel + vue環境導入済み
  • npmじゃなくてyarn
  • Vue CLIは使ってない

公式手順だとnpmですが、私の環境ではyarnを使っているのでyarnの手順を紹介します。

インストール

$ yarn add -D vue-styleguidist

〜色々インストールされる。省略〜

Done in 296.61s.

style guildの設定

公式手順だとリンクが2つ貼ってあります。

https://vue-styleguidist.github.io/docs/Components.html#finding-components
https://vue-styleguidist.github.io/docs/Webpack.html

いきなりぶん投げられてわかんないですが、プロジェクトルートに

styleguide.config.jsというファイルを作成してください。

中身は

const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
  webpackConfig: {
    module: {
      rules: [
        // Vue loader
        {
          test: /\.vue$/,
          exclude: /node_modules/,
          loader: 'vue-loader'
        },
      ]
    },
    plugins: [
      new VueLoaderPlugin()
    ]
  },
  // vueファイルへのpathを指定
  components: 'resources/js/components/**/[A-Z]*.vue',

}

で私の環境だといけました。

ポイントとしては、webpackConfiglaravel mixの設定を流用しても全く動かないです。(あれはmixで動的にconfigを生成したりしてるので)

また、vue-loaderの設定もちゃんと書いてあげないと動かないです。デフォルトで呼んでくれたりはしないようです。

あと、vueのファイルはLaravelだと普通resources/js以下で作成してる事が多いと思いますが、適時調整してください。

package.jsonにコマンド追加

これは公式そのままで大丈夫です。

{
  "scripts": {
+    "styleguide": "vue-styleguidist server",
+    "styleguide:build": "vue-styleguidist build"
  }
}

実行

hot reload版

yarn run styleguide

サーバが立ち上がってlocalhost:6060でつなげるようになります。vagrantやDocker等の仮想環境を使っている方はポートの設定をしてください。

私はdocker-composeを使っていたので

      ports:
        - 6060:6060 # styleguide

を追加しました。

htmlとjs吐き出す版

yarn run styleguide:build

styleguideというディレクトリにhtmlとjsが吐き出されますので、htmlを開けばOKです。

エラー色々

styleguide.config.jsの設定系

componentsへのpathがおかしい

画面を開くとこれが表示されるパターン。

Welcome to Vue Styleguidist!
We couldn’t find any components using these patterns:

src/{components,Components}/**/*.vue
Create styleguide.config.js file in your project root directory like this:

module.exports = {
  components: 'src/components/**/*.vue'
};
Read more in the locating components guide.

componentsの設定を自分の環境に合わせればOK。

Failed to compile

Failed to compile
./resources/js/components/XXXXX.vue 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type.

vue-loaderの設定をいれてあげればOK。

vueとvue-template-compilerのバージョン違い

Failed to compile
./resources/js/components/XXXXXX.vue (./node_modules/vue-styleguidist/loaders/vuedoc-loader.js!./resources/js/components/XXXXXX.vue)
Error: 

Vue packages version mismatch:

- vue@2.6.8 (/var/www/node_modules/vue/dist/vue.runtime.common.js)
- vue-template-compiler@2.6.10 (/var/www/node_modules/vue-docgen-api/node_modules/vue-template-compiler/package.json)

This may cause things to work incorrectly. Make sure to use the same version for both.
If you are using vue-loader@>=10.0, simply update vue-template-compiler.
If you are using vue-loader@<10.0 or vueify, re-installing vue-loader/vueify should bump vue-template-compiler to the latest.

バージョン合わせないとダメらしい。
yarn.lockを確認すると

vue-template-compiler@^2.0.0:
  version "2.6.10"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc"
  integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

となっており、^2.0.0 2以上を使ってね、の指定で2.6.10をinstallしちゃってる。

というわけで、無理やり2.6.8を入れてみる。

$ yarn add vue-template-compiler@2.6.8
yarn add v1.13.0

info Direct dependencies
└─ vue-template-compiler@2.6.8
info All dependencies
└─ vue-template-compiler@2.6.8
Done in 223.61s.

これで2.6.8入ったと思いきや、2.6.10も入ったままなのでダメ。

yarn.lockはこんな状態

vue-template-compiler@2.6.8:
  version "2.6.8"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c"
  integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

vue-template-compiler@^2.0.0:
  version "2.6.10"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc"
  integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

installしたりremoveしてもダメだったので、最終手段でyarn.lockを直接書き換える。

vue-template-compiler@2.6.8:
  version "2.6.8"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c"
  integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

vue-template-compiler@^2.0.0:
  version "2.6.8"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c"
  integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"
$ yarn install

これで動きました。

issueはちょいちょい見かけるのだけど、ちゃんとした対応方法は不明。誰か知ってたら教えて下さい。

https://github.com/vuejs/vue/issues/3941

実際のドキュメントを生成したり、パラメータの解説したりの予定。

第2段

https://www.moyashidaisuke.com/entry/vue-styleguild-sections

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

フルスタックエンジニアへの道(CakePHP/React)

はじめに

こんにちは、 @IZUMIRU0313 です。
ランサーズ Advent Calendar 2019 23日目の記事です。

法人向けの社外人材活用サービス「Lancers Enterprise」のフルスタックエンジニアです。
まだよわよわなので恐縮ですが、api blueprintでAPI仕様書、CakePHPでAPI、ReactでUIを実装しています?

想定する読者は、サーバーサイドエンジニアでフロントエンド(React)も学習していこうとしている方です。

Lancers Enterprise

エンジニア経歴

学生時代は、主にRails、Swift、AWS(EC2、S3)、Heroku、WordPressを利用して、サービス開発やインターンに取り組んでいました。特に以下2つのサービスは、すべての設計および開発をやっていたため、努力は報われると今日でも思える貴重な経験になっています。
フロントは、SassとjQueryが多少書けるレベルでした?

学生時代

ランサーズには、SREとしてジョインしました。当時、ターミナルはgitと多少のコマンドを知っているレベルであり、@yakitori009さんに、何から何まで教えていただきながら取り組んでいました?‍♂️
LPICでインプットしながら取り組んでいたため、座学と実務の両輪が上手く回せていました。

  • 踏み台サーバーの移行
  • Let's Encryptワイルドカード証明書の導入
  • AutoScaling
  • AutoScaling中ではデプロイ不可
  • docker-compose対応
  • MySQLコンテナ、WordPressコンテナの構築
  • MySQLのバージョンアップ5.6->5.7
  • LambdaでGitHubとChatworkの連携
  • LambdaでAthenaのload partitionを自動実行

その後、サーバーサイドエンジニアとしてCakePHPでプラットフォームの開発をすることにしました。まともにチーム開発とCakePHPを書くのは初めてだったため、@waldo0515さんや@numanomanuさん、井上さんにシステム設計からプロジェクトマネジメント、コーディングに渡るまで大変お世話になりました?‍♂️
インプットは、オブジェクト指向やドメイン駆動設計、クリーンアーキテクチャ、リーダブルコード等に努めました。

JavaScriptの習得

正直まだまだ未熟であり器用貧乏になる可能性も大いにあるのですが、自分が目指したいエンジニア像のために本格的にJavaScriptに力を入れることにしました?
まずは、半年後業務でReactを書けるレベルになることを目標に、GASでの個人開発から始めました。SREの際にLambdaでnode.jsを書いていたこともあり、特に詰まることなく開発できました。
インプットは、改訂新版JavaScript本格入門を読んでいました。

Reactの習得

ES6のお作法や非同期通信の変遷等も一通り理解することができたので、本格的にReactの学習を始めました。
元々副業や個人開発でVueやNuxtを触る機会があったのですが、個人的にはReactの方が学習ハードルが高かった印象です。
特にJSX(TSX)、TypeScript、Redux、redux-sagaは業務で開発するまで理解できませんでした。

半年ほど、@intrudercl14さんと@takepo0928さんにキャリアやJavaScript、Reactのアドバイスをいただき、なんとか「Lancers Enterprise」の開発にジョインすることができました?‍♂️

特にりあクト!は、対話形式で先輩エンジニアが後輩エンジニアに教えるというストーリーなので、非常に読みやすくオススメです。

りあクト!

またVue、React、React(Redux)で同じアプリケーションを実装することは、共通点と相違点を把握でき学習促進に繋がったのでオススメです。

comparison-vue-react-redux

Reactの学習と合わせて、APIの学習にも努めました。APIは学生時代のサービスでRailsでAPIを生やし、Swiftでキャッチするという経験等はありましたが、なんちゃってAPIレベルだったので1から学習しました。

展望

ReactやTypeScriptの学習は継続していますが、ReactNativeやFlutterの学習もし始めたため、@sayanetさんと@terukuraさんとともに「Lancers Enterprise」をより良くした後は、アプリの改善にコミットできたらと考えています。

またモチベーション高く学習するには、自分の性格を理解することが大事だなと非常に思いました。家だと怠惰なので仕事終わり必ずカフェに行く、まずは簡単なアプリケーションを開発した後に体系だった書籍で質を上げていく等。長くなりそうなので、個人開発のすゝめ的な記事は別途書けたら良いなと思います。

QiitaいいねやTwitterフォローは励みになります?

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

webpackを使ってobjectFitをIEに対応させる

webpackでobjectFitImagesを使えるようにするのははじめてだったので手順を残す。

最低限必要なのはライブラリだけ。
https://github.com/fregante/object-fit-images

object-fit-imagesをインストールする

yarn add object-fit-images

読み込ませるためのjsを書く

objectFitImages.js
/*
 * This file is use object-fit-images
 * IEをobjectFitに対応させるためのライブラリ
 * https://www.npmjs.com/package/object-fit-images
 *
*/
import objectFitImages from 'object-fit-images'

objectFitImages()

後はapp.tsなりにimportすればおk。

IEにも対応する

全てのCSSにfont-familiyを書いていくのは手間なので、postCssを使う。
https://github.com/ronik-design/postcss-object-fit-images

yarn add -D postcss-object-fit-images

postcss.configに追記

const objectFitImages = require('postcss-object-fit-images')
const autoprefixer = require('autoprefixer')

module.exports = {
  plugins: [
    autoprefixer,
    objectFitImages,
  ],
}

終わり。

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

v-date-pickerをカスタマイズしてbirthday-pickerを作る

TL;DR

  • VuejsのUIフレームワークであるVuetifyを使っていい感じの誕生日入力フォームを作成する
  • 以下を満たすものとする
    • 日本語対応
    • 日付の「日」は取る
    • 年から選択
    • 最初に年を選択するとき、1995年からになるようにする
    • 日付範囲制限(未来や古すぎる日付を誕生日に設定できない)
    • 日付を選択と同時にpickerを閉じる

動作環境

Vue.js v2.x
Vuetify v2.1.14

できたもの

https://codepen.io/kmr_0811/pen/oNgBzxR

解説

  • 日本語対応
    • v-date-pickerのlocaleプロパティを設定
  • 日付の「日」は取る
    • v-date-pickerのday-formatプロパティを設定
  • 日付範囲制限(未来や古すぎる日付を誕生日に設定できない)
    • max, minプロパティを設定
  • 最初に年を選択するとき、1995年からになるようにする
    • picker-dateプロパティを制御する必要がある
    • picker-dateではpickerの値をコントロールできる
    • そのため、v-modelでbindされたdateオブジェクトを触らずpickerについてのみ触ることができる
      <v-date-picker
        ref="picker"
        locale="jp-ja"
        v-model="date"
        :day-format="date => new Date(date).getDate()"
        :max="new Date().toISOString().substr(0, 10)"
        :picker-date="pickerDate"
        min="1950-01-01"
        @change="save"
      ></v-date-picker>
  • 年から選択
    • v-menuにバインドしたmenuをwatchし、v-date-pickerのactivePickerを制御する。
    • menuを選ばれたタイミングのみでactivePickerをYEAR指定にしないと、ずっと年選択になったりする
    • 選び始めたらpickerDateを初期化しないと、ユーザが入力してもpickerDateの値になってしまうので注意
  watch: {
    menu (val) {
      val && setTimeout(() => (
        // 年から選ぶようにする
        this.$refs.picker.activePicker = 'YEAR',
        // 選び始めたら初期化
        this.pickerDate = null
      ))
    },
  },
  • 日付を選択と同時にpickerを閉じる
    • 日付を選択したとき = v-date-pickerの@changeが発火するタイミング
    • その際、pickerDateと選択したdateを同期させないと、もう一度誕生日を選びなおした時に前回の日付が初期値として機能してくれないので注意
  methods: {
    save (date) {
      this.$refs.menu.save(date)
      // 再入力に備えて、入力が終わったら同期する
      this.pickerDate = date;
    },
  },

感想

v-date-pickerは抽象的な作りをしていて、開発者が用意されたプロパティやコンポーネントをよく調べる必要があります。
そのため、必然的にVueコンポーネントの学びを多く得られると思いました。

ぜひ、v-date-pickerもといVuetifyを触ってみてください!

付録


今回のコード

template

<div id="app">
  <v-app id="inspire">
    <v-menu
      ref="menu"
      v-model="menu"
      :close-on-content-click="false"
      transition="scale-transition"
      offset-y
      full-width
      min-width="290px"
    >
      <template v-slot:activator="{ on }">
        <v-text-field
          v-model="date"
          label="誕生日を入力"
          prepend-icon="event"
          readonly
          v-on="on"
        ></v-text-field>
      </template>
      <v-date-picker
        ref="picker"
        locale="jp-ja"
        v-model="date"
        :day-format="date => new Date(date).getDate()"
        :max="new Date().toISOString().substr(0, 10)"
        :picker-date="pickerDate"
        min="1950-01-01"
        @change="save"
      ></v-date-picker>
    </v-menu>
  </v-app>
</div>

script

new Vue({
  el: '#app',
  vuetify: new Vuetify(),
  data: () => ({
    date: null,
    menu: false,
    // pickerの初期値(95年から年が選べるようになる)
    pickerDate: '1995-1-1',
  }),
  watch: {
    menu (val) {
      val && setTimeout(() => (
        // 年から選ぶようにする
        this.$refs.picker.activePicker = 'YEAR',
        // 選び始めたら初期化
        this.pickerDate = null
      ))
    },
  },
  methods: {
    save (date) {
      this.$refs.menu.save(date)
      // 再入力に備えて、入力が終わったら同期する
      this.pickerDate = date;
    },
  },
})


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