20191130のJavaScriptに関する記事は24件です。

【 初心者向け】ReactNativeで電卓アプリを作ろう【 チュートリアル】

こんにちは!DMM WEBCAMPでメンターをしている人です!
アドベントカレンダー16日目は、(僕の中で)最近話題のReact Nativeを使って電卓を作ってみます。

チュートリアル形式でReact未経験の方でも分かるように専門用語を出来るだけ削って書いていきます。

是非トライしてみてください!

1.React Nativeとは??

Facebookが開発した「React」というJavaScriptのフレームワークの亜種で、iOSやAndroidのアプリを作ることが出来る代物です。Reactは仮装DOMが実装されているので表示が早いというメリットがあります。

・jsなら任せて!
・Reactならわかるぞ!
・さくっとアプリ作りたい!
・スマホのアプリが作りたいけどSwift・Kotlinやる時間がない・・・
なんて方にオススメのアプリケーションフレームワークです。

さらに踏み込んだ解説が知りたい方はこちらの記事がオススメです。
「React Nativeとは何なのか」

2.今回やること

この記事では、四則演算が出来る電卓アプリを作っていきます。

ezgif.com-video-to-gif (2).gif

3.開発環境&構築

・Androidの開発を行う場合は、「Android studio」
・iOSの開発を行う場合は、「Xcode」が必要になります。

今回はiOSのアプリを作るのでAndroidの環境構築は省かせていただきます。ごめんなさい:rolling_eyes:
(Androidの開発が行いたい方はこちらの記事が分かりやすかったです!)

まずはnodeとwatchmanというものをインストールします。

$ brew install node
$ brew install watchman

次にexpoのダウンロードです。
nodeをインストールしたときにnpmが入っているのでnpmコマンドが使えます。

$ npm install expo-cli --g

Expoを深く知りたい方はこちらの記事がGoodです。

これで完了!といいたいところですがExpoはXcodeがないと開けないため、Xcodeのダウンロードをしましょう。
Xcodeで開いたiOSシュミレーター(PC上で開かれるiPhone)の中にExpoアプリがインストールされるのでXcode必須です。

Xcodeとは

XcodeはAppleが開発したソフトウェア開発者向けの主にMacやiPhone、iPadのアプリケーションを作るのに向いています。XcodeのおかげでMac上でiOSのシュミレーターが起動できます。

ダウンロードしたことがない方はApple Storeからダウンロードしてください!

ただ、めちゃくちゃサイズがデカいのでインストールに時間がかかります。

待ち時間にReactNativeの公式ドキュメント でも読んで時間を潰しちゃいましょう。

ここまできたら準備完了です!

プロジェクトの新規作成

以下のコマンドを実行してReactNativeアプリを新規作成しましょう!

$ expo init お好きな名前

プロジェクトのテンプレート選択

expo init ○○○後に、画像のようにテンプレートをどうするか聞かれますがここは「blank」を指定しましょう。
blankのスクリーンショット

名前の設定をしよう

スクリーンショット 2019-12-11 12.09.42.png
テンプレート選択した後はこのような画面になると思います。

ここでは実際のアプリ名を設定します。(アプリアイコンの下に表示される名前)

リリースを考えているアプリの場合変な名前にしてはいけません!!!

Yarnは使わなくてOK!

Yarn v1.19.1 found. Use Yarn to install dependencies? (Y/n) 

このような質問がされる場合があります。
これは「Yarnが見つかったけどYarnは使う?どうする?」と聞かれています。
今回はnpmを使用しているので「n」を押してEnterです。

最終行にこのような記述があればアプリの作成完了です!

:(省略)
:
Your project is ready at 〜〜略〜〜〜
To get started, you can type:

  cd アプリ名
  npm start

npm start を実行してこの画面が表示されれば下ごしらえも完了です!
start.jpg

Railsに比べてシックで落ち着いたスタート画面ですね、おしゃれ。

補足:自分のスマホで開いてみよう

なんと自分の携帯にも開発しているアプリを開くこともできます。

まず。ご自身のスマホにExpoのアプリ(AppleStoreもしくはGooglePlayにて「Expo」で検索です!)をインストールします。

次に、アプリを読み取るQRコードをための作業をPCでします。

アプリが立ち上がった後のターミナルで

Press ? to show a list of all available commands

と出たら、「?」を入力します。次に「d」を押すとブラウザでデベロッパツールが開かれます。

デベロッパーツールにあるQRコードを読み取ると作っているアプリのURLが出るのでタッチしましょう!これでExpoアプリが開かれます!!

4.実装!

まず初めに、開発するアプリケーションの階層から見ていきましょう。

.expo
 ├ packager-info.json
 ├ setting.json
.expo-shared
 ├ assets.json
.assets
 ├ icon.png
 ├ splash.png
.node_module
 ├ 色々入ってます
.gitignore
App.js ← 主にこれを使います
babel.config.js
package-lock.json
package.json
style.js ← こちらを新規作成します

5.アプリ作成後に書かれている記述の意味は?

アプリの新規作成後にデフォルトで開かれるのは「App.js」というファイルです。そこにいろいろな記述を書いていき、最終的にはかっこいい電卓を作ります。

では、App.jsに最初から書かれている記述を見ていきましょう。

App.js
import React from 'react'; // ①
import { StyleSheet, Text, View } from 'react-native';  // ②

export default function App() {  // ⑤
  return (
    <View style={styles.container}> // ③
      <Text>Open up App.js to start working on your app!</Text>
    </View>
  );
};

const styles = StyleSheet.create({ // ④
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

importってなに?

①と②はReact Nativeを使って開発するときのおまじないのようなものです。

①は、"react"モジュールからimport(=輸入する)して、Reactの記述を使用できるようにしています。

アプリケーションの中にnode-moduleというフォルダがあり、更にその中のreactというフォルダからimportしてきて色々読み込んでいます。

②は、"react-native"モジュールからimportして、StyleSheetやTextクラスを使えるようにしています。

こちらもnode-moduleの中のreact-nativeフォルダからimportしてきています。

「なんで全部importしておかないの?」と思った方いませんか?僕は思いました。

実は、全ファイルやコンポーネントをimportするより、各々が使いたいファイルやコンポーネントを選んでimportする方が読み込み速度が上がるらしいです。言われてみれば納得です:writing_hand_tone1:

Reactの描画速度が早いと言われる理由はこんなところにもあるんです!

コンポーネントについて

React Nativeでは描画するために独自のコンポーネントを使用する必要があります。
理由としては、divやspanタグが使えないので、Viewなどのコンポーネントを使う必要があるというわけです。
(この概念が難しいと言うか、コードを理解不能にしている要因の1つですが、慣れると余裕です:beer:

現状のコードの中だと、VIewTextStyleSheetがReact Nativeのコンポーネントです。

importせずにこれらのコンポーネントを使用するとエラーを吐いてしまいます。

だから自分が使用したいコンポーネントに合わせて②に追記していく感じです。

例えば入力フォームを作成したい場合は、以下のように追記します。

App.js
import React from 'react'; 
import { StyleSheet, Text, View, TextInput } from 'react-native';  //←追加!!!

export default function App() {  
  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
        <TextInput style={styles.input} />  //←追加!!! 
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  input: {     //←追加!!!
    width: 100,
    borderColor: "gray",
    borderWidth: 1,
  },
});

 <TextInput />  <TextInput> ~~ </TextInput>の省略系です、便利!

コンポーネントはたくさん種類があり公式ドキュメントにたくさん載っています。

StyleSheet.createってなに?

続いて、③と④についてです。この2つはスタイル(見た目)を担っている部分です

何をしているかというとStyleSheetというコンポーネントを作成し、その中にスタイルの記述をたくさん書いています。

では、文字を赤くさせたい!なんて時はどうすればよいでしょうか。

3ステップです。
1. stylesの中に、新しく「redColor」を定義します。(ここの命名は自由です)
2.「redColor」を反映させたいコンポーネントにstyle={styles.redColor}を足します
3. 完成

App.js
import React from 'react'; 
import { StyleSheet, Text, View, TextInput } from 'react-native'; 

export default function App() {  
  return (
    <View style={styles.container}>
      <Text style={styles.redColor}>Open up App.js to start working on your app!</Text>   //← 追記!!
        <TextInput style={styles.input} /> 
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  input: {     
    width: 100,
    borderColor: "gray",
    borderWidth: 1,
  },
   redColor: {        //← 追記!!!
    color: "red" 
  },
});

いちいちこんなことするの面倒臭い!って方には、コンポーネントに直接スタイルを書くのがオススメです。こんな風に直書きができます。

   <TextInput
     style={{width:100, borderColor: 'gray', borderWidth: 1 }} 
   />

ハマりポイント スタイルはキャメルケースで!

注意点としてはReact Nativeではプロパティーの部分はキャメルケースのみ使えます。

キャメルケースとは、「backgroundColor」のような、単語と単語の間にスペースや"-"を置かずに、文字の始まりを大文字にする形式です。
(ラクダのこぶに似てることからキャメルケースと呼ばれます。他にもスネークケース、ケバブケースがあります)

普段「background-color」としていたところを「backgroungColor」としなければなりません。

ようやくコンポーネントが描画される!

export default function App() {  // ⑤
  return (
    <View style={styles.container}> 
      <Text>Open up App.js to start working on your app!</Text>
    </View>
  );
};

ラストです。
ここではimportしたViewとTextコンポーネントを使用しています。

HTML/CSS経験者であればこの書き方に違和感を感じますよね。

意訳ですが、HTML/CSSに変換して記述してみるとこんな感じです。
コンポーネントの使い方とイメージがつかめば全然怖くありません。

意訳.html
<div style="省略">
  <p>Open up App.js to start working on your app!</p>
</div>

続いて、以下の部分です。

export default function App() {  // ⑤

};

exportとは先ほど出てきたimportの反対の意味です。

export内で定義した記述を他の場所で使えるようにするものです。

例えばsample.jsを新規作成し、そこにexport~~の記述をしたコンポーネントを2つ書きます。

これをApp.jsで受け取りたいとしましょう。

sample.js
import React, {Component} from 'react';
import {Text, View} from 'react-native';

export class Sample1 extends Component {
    render(){
        return(
            <View>
              <Text>サンプルの1です</Text>
            </View>
        );
    };
};

export class Sample2 extends Component {
    render(){
        return(
            <View>
              <Text>サンプルの2です</Text>
            </View>
        );
    };
};
App.js
import React from 'react'; 
import { StyleSheet,  View,  } from 'react-native';
import {Sample1 , Sample2} from "./sample"; //←ここでimportしています


export default function App() {  
  return (
    <View style={styles.container}>
      <Sample1 />  //←ここでimportしたSample1を
      <Sample2 />  //←ここでimportしたSample2を使っています
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

defaultオプション付きのexportは、そのファイルの中においてデフォルトでexportされるコンポーネントを決めることができます。default付きexportはimportする時の名前はなんでもよく、ついていないものはclass名をimportする側で同じにしなければなりません。(面倒くさい)

例えば先ほどのSample1クラスにdefaultを設定します

sample.js
export default class Sample1 extends Component {
//略

次にApp.jsでimportします。

App.js
import hogehoge ,{Sample2} from "./sample"; //←import名を適当にしてます

//略

    <View style={styles.container}>
      <hogehoge />  //←ここでimportしたhogehogeを使える!
      <Sample2 />  
    </View>

defaultが付いていないものは{ }で囲う必要があるので要注意です!

export defaultについて詳しくはこちらです。

6.スタイルを適応させよう

次にレイアウトを整えるためにスタイル(≒CSS)を作っています。

ここはそれほど重要ではないので、何をしているか理解したらコピペしちゃって大丈夫です。

また、App.jsにスタイルの記述をしてもいいのですが個人的に見辛いのでファイルを分けました。

2ステップです。

①App.jsと同じ階層にstyle.jsを新規作成する

②以下のコードを貼り付ける

style.js
import { StyleSheet } from "react-native";

const styles = StyleSheet.create({
    container: {
      backgroundColor: '#FFF',
    },
    countArea: {
      marginTop: 140,
      justifyContent: "center",
      alignItems: "center", 
      marginBottom:10
    },
    buttonArea: {
      marginTop: 5,
      justifyContent: 'center',
      alignItems: 'center'  
    },
    value: {
      fontSize: 30
    },
    topbutton: {
      marginTop: 50
    },
    button: {
      marginTop: 5,
      justifyContent: 'center',
      alignItems: 'center',
      width: 50,
      height: 40,
      borderRadius: 5,
      borderWidth: 1,
      borderColor: '#fff'
    },
    buttonText: {
      fontSize: 25,
      color: '#fff'
    },
    buttonUp: {
      backgroundColor: '#A53434',
      fontWeight: "bold"
    },
    buttonUpText: {
      backgroundColor: 'red'
    },
    buttonMinus: {
      backgroundColor: '#343EA5'
    },
    buttonTimes: {
      backgroundColor: 'purple'
    },
    buttonDivide: {
      backgroundColor: "green"
    },
    buttonReset: {
      borderColor: '#AFADAD',
      marginBottom: 200
    },
    buttonResetText: {
      color: '#000',
      fontSize:11
    },
    buttonNum: {
      backgroundColor: 'grey',
      fontWeight: "bold",
      justifyContent: 'center',
      alignItems: 'center',
      flexDirection: 'row',
      width: 60,
      height: 40,
      borderRadius: 5,
      borderWidth: 1,
      borderColor: '#fff'
    },
    buttonEqual: {
      backgroundColor: "black"
    }
  });

  export default (styles);

"react-native”から”StyleSheet”コンポーネントをimportしています。

こうすることによって

3行目でStyleSheet.createが使えるようになります。

この記述をすることによって、このコンポーネントはスタイルの記述をしますよ〜!と明示することが出来ます。
(StyleSheet.createしなくても動作はしますが、一応書いたほうがいいです。詳しくはこちら の記事です。)

そして、最終行では、このstyle.jsがどこでも使えるようにexportしています。

これをApp.jsでimportすればスタイルの土台は完成ですね。

7.電卓のレイアウトを作ろう

スタイルを作成したので、次はそれを適応させるためのコンポーネントを作っていきます。
App.jsに書いている記述は1度全て消してから以下のコードを書いていきましょう!ここからスタートです!!

まずは先ほどのstyle.jsをApp.jsでimportします。

ついでに、この後使う「TouchableOpactity」というコンポーネントもimportしておきます。

App.js
import React, {Component} from 'react';
import { Text, View, TouchableOpacity } from 'react-native'; //←StyleSheetとInputTextがある方は削除でOK!
import styles from "./style"  //←追記!

次にコンポーネントの作成を行っていきます。

App.js
//↓import ~~~の下に↓

export default class App extends Component {
    render() {
      return (
        <View>
          <View style={styles.countArea}>
            <Text style={styles.value}>0  ?  0  =  0</Text>
          </View>
        </View>
      );
    };
};

 画像のようなものが画面に写ればOKです!
スクリーンショット 2019-11-29 15.38.39.png

計算用ボタンの設置

次に電卓のボタンを作っていきましょう。

先ほどimportした「TouchableOpacity」がここで出番です。

TouchableOpacityとは、簡単に言うとクリックできる要素のことです。
これを用いることでボタンが簡単に作れます。

App.js
//↓import ~~~の下に↓

export default class App extends Component {
    render() {
      return (
        <View>
          <View style={styles.countArea}>
            <Text style={styles.value}>0  ?  0  =  0</Text>
          </View>

          <View style={styles.buttonArea}>
            <View style={{ flexDirection: 'row'}}>
               <TouchableOpacity style={[styles.button, styles.buttonUpText]}>
                 <Text style={styles.buttonText}>+</Text>
               </TouchableOpacity>
            </View>
           </View>
        </View>
      );
    };
};

画像のようなボタンが表示されたでしょうか?

スクリーンショット 2019-11-29 16.26.34.png

あとは、数字のボタンや四則演算のボタンを設置していきます!

一気に書いていきましょ〜!

App.js
export default class App extends Component {
    render() {
      return (
        <View>
          <View style={styles.countArea}>
            <Text style={styles.value}>0  ?  0  =  0</Text>
          </View>

          <View style={styles.buttonArea}>
//足し算ボタン
            <View style={{ flexDirection: 'row'}}>
               <TouchableOpacity style={[styles.button, styles.buttonUpText]}>
                 <Text style={styles.buttonText}>+</Text>
               </TouchableOpacity>
//引き算ボタン
               <TouchableOpacity style={[styles.button, styles.buttonMinus]} >
                 <Text style={styles.buttonText}>-</Text>
               </TouchableOpacity>
//掛け算ボタン
               <TouchableOpacity style={[styles.button, styles.buttonTimes]} >
                 <Text style={styles.buttonText}>x</Text>
               </TouchableOpacity>
//割り算ボタン
               <TouchableOpacity style={[styles.button, styles.buttonDivide]} >
                 <Text style={styles.buttonText}>÷</Text>
               </TouchableOpacity>
//=ボタン
               <TouchableOpacity style={[styles.button, styles.buttonEqual]} >
                 <Text style={styles.buttonText}>=</Text>
               </TouchableOpacity>
//リセットボタン
               <TouchableOpacity style={[styles.button, styles.buttonReset]} >
                 <Text style={[styles.buttonResetText]}>リセット</Text>
               </TouchableOpacity>
             </View>
           </View>
        </View>
      );
    };
};

スクリーンショット 2019-11-30 16.40.46.png
画像のようにボタンが6つ表示さえれば計算ボタンは完成です。

数字ボタンの設置

次は0〜9の数字ボタンを作っていきます。

先ほどの計算ボタンはボタンごとに色や文言が変わるので個別に作成しました。

しかし、0〜9の数字ボタンは値やボタンの文字が変わるだけです。なので繰り返し処理を使って簡潔に記述しましょう!

まずは、以下のように配列を定義してあげます。

App.js
//〜〜〜略
exporet ~~
  render() {
    return (
      <View>
      //略
      </View>
    );
  };
};
//↓ここの下に記述

const ZeroToFour = [];
for (let i = 0; i< 5; i++) {
  ZeroToFour[i] = i
};

const FiveToNine = [];
for (let i = 5; i< 10; i++) {
  FiveToNine[i] = i
};

まずは繰り返し処理に使う配列を2つ用意します。ZeroToFourは0〜4、FiveToNineは5〜9のボタンを作ります。
(レイアウト調整のためここは敢えて配列2つ使っています、10個まとめて作るとレイアウトが綺麗に整えられませんでした。甘えです、ごめんなさい)

App.js
//略
export default class App extends Component {
    render() {
      return (
        <View>
          <View style={styles.countArea}>
            <Text style={styles.value}>0  ?  0  =  0</Text>
          </View>

          <View style={styles.buttonArea}>

//↓追加↓
            <View style={{flexDirection: 'row'}}>
              {ZeroToFour.map(i => (
                  <TouchableOpacity style={styles.topbutton,styles.buttonNum} key={i} >
                    <Text style={styles.buttonText}> {i} </Text>
                  </TouchableOpacity>
              ))};
            </View> 

            <View style={{ flexDirection: 'row'}}>
              {FiveToNine.map(i => (
                  <TouchableOpacity style={styles.topbutton, styles.buttonNum} key={i}>
                    <Text style={styles.buttonText}> {i} </Text>
                  </TouchableOpacity>
              ))};
            </View>

//ここより上を追加
//足し算ボタン
            //<View style={{ flexDirection: 'row'}}>
              //<TouchableOpacity style={[styles.button, styles.buttonUpText]}>
                 //<Text style={styles.buttonText}>+</Text>
               //</TouchableOpacity>

少し複雑な記述です。

ここはZeroToFourとFiveToNineの値を繰り返し処理(map)で出力させています。

実際は見本のように描画されていますが10個分の記述を書くとなると非常に長いので繰り返し処理を用いてボタンを作っています。

styleのflexDirection: "row"は、要素を横並びにするスタイルです。
このおかげでかっこいい電卓のレイアウトが出来ましたね。

見本
        <View style={{flexDirection: 'row'}}>
              <TouchableOpacity style={styles.topbutton,styles.buttonNum}>
                <Text style={styles.buttonText}> 0 </Text>
              </TouchableOpacity>
                           .
                           . 1~8省略)
                           .
              <TouchableOpacity style={styles.topbutton,styles.buttonNum}>
                <Text style={styles.buttonText}> 9 </Text>
              </TouchableOpacity>
        </View>

完成版ボタンのスクショ

ようやくここまででレイアウトが整いました。お疲れ様です:pensive::relaxed:

後半では実際にデータをやりとりしたり計算する処理を作っていきます。

前半戦の完全版コードをこちらに置いておきます。うまくいかない方は確認してみましょう!

後編はこちらです!!!

8.数字ボタンを押してその値を描画しよう!

レイアウトが完成したので次は機能を作っていきます。
複雑な部分が多いので頑張っていきましょう:angel:

まずは、数字ボタンが押されたらその数字が描画されるところまでやっていきましょう。

①「ボタンが押されたら値(= 押したボタンの数字)が送信される」という記述を作る

②送信された値を受け取る

③その値を描画する

という流れで作っていきます。

ezgif.com-video-to-gif.gif

①「ボタンが押されたら値が送信される」という記述を作る

早速作っていきましょう。ボタンを作っているコードに追加していきます。

App.js
        <View style={styles.buttonArea}>
          <View style={{flexDirection: 'row'}}>
            {ZeroToFour.map(i => (
                <TouchableOpacity style={styles.topbutton,styles.buttonNum} key={i} onPress={() => this.onNum({i})}>
                  <Text style={styles.buttonText}> {i} </Text>
                </TouchableOpacity>
            ))}
          </View> 

          <View style={{ flexDirection: 'row'}}>
            {FiveToNine.map(i => (
                <TouchableOpacity style={styles.topbutton, styles.buttonNum} key={i} onPress={() => this.onNum({i})} >
                  <Text style={styles.buttonText}> {i} </Text>
                </TouchableOpacity>
            ))}
          </View>

onPress={() => this.onNum({i})}という記述を追加しています。

onPressはjsやJQueryの「click関数」とほぼ同じ意味です。

{i}には繰り返し処理中のindexが入るので、0〜9の値が入りますよね。
これを利用して押されたボタンの数字を取得して送信しているという仕組みです。

②送信された値を受け取る

値の送信は完了したので、その値を処理して受け取る作業をしていきます。

①に追記したonNumは未定義のものなので、定義してあげましょう。

App.js

//import 略

export default class App extends Component {
//ここから
  constructor(props){
    super(props);
    this.state = {
      leftNum: 0
    }
  }
  onNum(input){
    console.log(input); //←デバック用
    this.setState({ leftNum:  input["i"] })
  } 
//ここまで


//render(){
  //return(

onPressイベントから送信された値は、onNumイベントで受け取っています。引数(input)に値が送られてきています。

console.log(input)でどんなものが送られてきたか確認してみましょう!

「7」のボタンを押すと、こんなデータが送信されていることが分かりました。

7のボタン
//inputの中身
Object {
  "i": 7,
}

オブジェクトとして送信されていて、"i"がkey、7がvalueですね。

では、ここから「7」を取得したい場合は以下のようにすれば良いですよね。

input["i"] 
//valueの7が取れる

そして、最後に取得した値を保存します。

保存
this.setState({ leftNum:  input["i"] })

この記述を行うことでstateにあるlaftNumにinput["i"]の値を保存しています。

setStateはstateを変化させたい時に使用するもので「stateにsetする」的なイメージを持ってくれたらOKです。

こうすることで、

    this.state = {
      leftNum: 0
    }

0だったleftNumが

    this.state = {
      leftNum: 7
    }

7に変化します!

③保存した値を描画する

ラストは保存した値を描画する作業です。これは秒です、2秒です。

先ほどまで、「0 ? 0 = 0」としていたところを少し変更します!

1番左の0の部分を{this.state.leftNum}にするだけです。

これで現在のleftNumの値を描画してくれるというわけです。(すごい!)

App.js
//略

  render() {
    return (
      <View>
        <View style={styles.countArea}>
          <Text style={styles.value}>{this.state.leftNum}  ?  0  =  0</Text>
        </View>

9. 2桁以上の入力をできるようにしよう!

ここまででようやく押したボタンの数値が適応されるようになりました。でも、現状だと数字ボタンを押しても値が変更されるだけで1桁しか表示されません。

1桁しか計算できない電卓なんで全然かっこよくないですよね。むしろダサいです。

しかし、僕は2桁以上の入力を出来るようにするやり方に悩みに悩みました、何日も悩みました。1度考えることを辞めた僕はなぜか閃きました。

「1回文字型に変換して足して、数値型に戻せば良いんだ!!!!」

(僕はこのやり方しか思いつかなかったので他にもあれば教えていただきたいです。)

数値型のまま、既存のstateの値と入力された値を足すと普通に足し算されてしまいます。

悪い例
//デフォルトでleftNumに2、input["i"]に4があるとする
this.state.leftNum + input["i"]

2 + 4 = 6になってしまう
成功例
//デフォルトでleftNumに2、input["i"]に4があるとする
String(this.state.leftNum) + String(input["i"])

(文字列の)"2" + (文字列の)"4" = "24"

既存の値と入力された値の両者を文字列に変換して、+してあげると文字列同士の足し算が行われるのでこれを利用しました。

これを使って記述を変更しましょう!

App.js
  onNum(input){
    //console.log(input);
    //this.setState({ leftNum:  input["i"] })
    const num = Number(String(this.state.leftNum) + String(input["i"])) //文字列同士の足し算を行い、最後に数値型に変換
    this.setState({ leftNum:  num }) //leftNumに計算した値をset!
  } 

だんだん完成に近付いてきました。現在70%くらいの完成度です。ラストスパートです!

10.四則演算の選択を可能に!

左側の数値の入力は可能になったので、計算方法の選択が出来るようにしましょう。

数字ボタンの時とやることは同じなのでサクッといきましょう!

App.js
export default class App extends Component {
  constructor(props){
    super(props);
    this.state = {
      leftNum: 0,
      mark: "?",
    }
  }

//~~~略

  render() {
    return (
      <View>
        <View style={styles.countArea}>
          <Text style={styles.value}>{this.state.leftNum}  {this.state.mark}  0  =  0</Text>
        </View>

//~~~略
//~~~略
        <View style={{ flexDirection: 'row'}}>
        //以下4つにonPressを追記
          <TouchableOpacity style={[styles.button, styles.buttonUpText]} onPress={() => this.onMark("plus")}>
            <Text style={styles.buttonText}>+</Text>
          </TouchableOpacity>

          <TouchableOpacity style={[styles.button, styles.buttonMinus]} onPress={() => this.onMark("minus")}>
            <Text style={styles.buttonText}>-</Text>
          </TouchableOpacity>

          <TouchableOpacity style={[styles.button, styles.buttonTimes]} onPress={() => this.onMark("times")}>
            <Text style={styles.buttonText}>x</Text>
          </TouchableOpacity>

          <TouchableOpacity style={[styles.button, styles.buttonDivide]} onPress={() => this.onMark("divide")}>
            <Text style={styles.buttonText}>÷</Text>
          </TouchableOpacity>

やっていることは3つです。

①stateに[+ ー ➗X]を表すための「mark」を追加

②常に今のmarkの値を描画するために{ this.state.mark }を追加

③四則演算のボタンにonPressイベントを追加

この3つは数字ボタンと流れは同じですよね。

この状態でボタンを押してもエラーが出るので、入力したボタンの値を受け取って正常に描画させましょう。

入力してきた値が"plus"ならmarkを「+」に変更というようなイメージです
たくさん条件分岐させていきます。

App.js
onMark(input){
    if (input == "plus"){
      this.setState({ mark: "+"  })
    } else if (input == "minus") {
      this.setState({ mark: "-" })
    } else if (input == "times") {
      this.setState({ mark: "x"} )
    } else if (input == "divide") {
      this.setState({ mark: ""})
    }
  }
}

(↑の➗マークが見えづらくなってます。)

11.右側の数字を入力可能に使用!

現状は左の数値と四則演算のマークの入力は可能になっています。

次は右側の数値の入力ができるようにしましょう!ここも今までと同じ流れで完了です。

APp.js
export default class App extends Component {
  constructor(props){
    super(props);
    this.state = {
      leftNum: 0,
      mark: "?",
      rightNum: 0, //←追記
    }
  }
  onNum(input){ //以下追記
    if (this.state.mark == "?"){
      let num = Number(String(this.state.leftNum) + String(input["i"]))
      this.setState({ leftNum: num })
    } else {
      let num = Number(String(this.state.rightNum) + String(input["i"]))
      this.setState({ rightNum: num })
    } 
  }

//~~~~~略

  render() {
    return (
      <View>
        <View style={styles.countArea}> //以下追記
          <Text style={styles.value}>{this.state.leftNum}  {this.state.mark}  {this.state.rightNum}  =  0</Text>
        </View>

ここまでのコードはこちらです。困ったら確認してください:relaxed:

12. 「=」を押したら計算を実行する!

ようやく計算式の入力が出来るようになりました。

「=」が押されたら四則演算のマークに応じて計算が行われるようにしましょう。

まずは「=」ボタンの所に追記しましょう。

App.js
 <TouchableOpacity style={[styles.button, styles.buttonEqual]} onPress={() => this.onEqual()} >
   <Text style={styles.buttonText}> = </Text>
 </TouchableOpacity>

onEqualを定義して、markの値によって条件分岐を行います。

そして、結果の値をsetすれば完了です。

App.js
export default class App extends Component {
  constructor(props){
    super(props);
    this.state = {
      leftNum: 0,
      mark: "?",
      rightNum: 0,
      result: 0 //追記
    }
  }
      :
      :
      :
  onEqual(){
    if (this.state.mark == "+") {
      this.setState( { result: this.state.leftNum + this.state.rightNum })
    } else if (this.state.mark == "-") {
      this.setState( { result: this.state.leftNum - this.state.rightNum})
    } else if (this.state.mark == "x") {
      this.setState( { result: this.state.leftNum * this.state.rightNum})
    } else if (this.state.mark == "") {
      this.setState( { result: this.state.leftNum / this.state.rightNum})
    }
  }

  render() {
    return (
      <View>
        <View style={styles.countArea}>
          <Text style={styles.value}>{this.state.leftNum}  {this.state.mark}  {this.state.rightNum}  =  {this.state.result}</Text> //追記
        </View>

「=」ボタンを押したらonEqualが発火します。

onEqualイベントは、markの値によって条件分岐しています。

例えばmarkが「➗」だったら、leftNumとrightNumを割り算して、resultにsetしています。

これでようやく計算ができるようになりました!!!!!

次でラストです!!!

13.リセットを反映させよう!

最後はリセットボタンです。iPhoneアプリの電卓なら「AC」にあたるところです。

リセットボタンを押したらstateにあるleftNumとrightNumを「0」に、markを「?」に変更します。

App.js
:
:略(onEqual)の下に記述
:
  onReset(){
    this.setState({ leftNum: 0, mark: "?", rightNum:0, result:0 })
  }


:略 リセットボタンに追記

   <TouchableOpacity style={[styles.button, styles.buttonReset]} onPress={() => this.onReset()}>
      <Text style={[styles.buttonResetText]}>リセット</Text>
   </TouchableOpacity>

これで値のリセットが完了しました!

まとめ

ようやく電卓が完成しました。お疲れ様でした。
Reactが初めての方はとても大変でしたよね。

今回のアドベントカレンダーでアウトプットの重要性が身に染みてわかりました。
この記事を書くために調べて、実践して、改善してと非常に良い勉強になりました。

とりあえず本当にお疲れ様でした!
完成版コードの確認をしたい方はこちらです。

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

Google Apps ScriptでHappyou Final Scraperを中継してInoreaderで読む

あらゆるウェブページをRSSフィードに変換するHappyou Final Scraperという便利なものがあり愛用していたのですが、
かなり昔のあるときからInoreaderからのアクセスが非常に遅くなり、タイムアウトになりRSSを取得できない状態が続いていました。

failed.JPG

新規追加のときはタイムアウトが長いのか、かなり待てば追加されるのですが、普段の巡回はエラーに書いてあるように3秒以内に応答がなければ失敗扱いとなります。

原因はわからず、私の環境かHappyou Final ScraperとInoreaderの経路間の問題なのか不明で放置していたのですが、
一念発起して迂回スクリプトを書くことに。
普通のアクセスは高速なので、別のところで取得してそのままInoreaderに返せばよかろうとなりました。
ラムダとかでいいのですが簡単なGASで書きます。

function doGet(e) {
  var URL = ''
  if (e.parameter.url) {
    URL = 'https://' + decodeURIComponent(e.parameter.url)
  } else {
    URL = 'https://www.happyou.info/fs/gen.php?u=1525577309&p=-453397430'
  }
  const res = UrlFetchApp.fetch(URL)
  return ContentService.createTextOutput(res.getContentText())
  .setMimeType(ContentService.MimeType.XML);
}

GETパラメーターで取得先のRSSURLを渡してあげて、fetchはGASで行いInoreaderにレスポンスを返す単純なアプリを公開します。

こうするとフェッチを加味してもInoreaderとGAS間の応答は3秒以内に収まるので、

succsess.JPG

このようにInoreaderでHappyou Final ScraperのRSSが再び取得できるようになりましたとさ。めでたしめでたし。

URLを指定すると

https://script.google.com/macros/s/fugafuga/exec?url=www.happyou.info%2Ffs%2Fgen.phphogehoge

こんなかんじですね。

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

JavaScriptのみで構築するディープラーニング with Jupyter and TensorFlow.js

はじめに

間も無く終了する2010年代はJavaScriptが大きく成長した十年でした。Reactなどの革新的なフレームワークが登場し、Node.js、React Naitive などブラウザ以外でのエコシステムも大きく成長しました。また言語自体もES2015以降日々進歩しており、JavaScriptは2010年代でもっとも多くの人に使われたプログラミング言語となりました。さらにJavaScriptに静的型付を導入したTypeScriptとそのエコシステムもここ数年で大きく成長しており、JavaScriptは大規模なソフトウェアの開発にも耐えうるようになってきています。

top-languages
出典: https://octoverse.github.com/#top-languages

一方で2010年代にJavaScript以上に大きく人気を伸ばしたプログラミング言語がありました。Pythonです。2010年代に大きく伸びた分野であるデータサイエンスと機械学習(AI)の分野ではPythonはその利便性で他のメジャーなプログラミング言語を圧倒しています。2010年代はPython2から下位互換性のないPython3への移行という混乱と困難があったにも関わらず、Pythonはデータ分析・機械学習分野の成長と歩みをともにして大きく人気を伸ばしました。2019年のGitHubの報告ではJavaを抜いて遂にユーザー数が2位となっています。JavaScriptはまだPythonにユーザー数では優っているもののデータ分析・機械学習の分野では大きく遅れをとっており、この分野の今後の伸び次第では近いうちにランキングが逆転するかもしれません。逆にユーザー数で優位にあるJavaScriptがこの分野でも人気を獲得し、その地位を不動のものとするかもしれません。

2020年代がJavaScript, Pythonにとってどのような時代になるのか楽しみです。この記事では2020年代におけるJavaScriptの機械学習、特にディープラーニングにおける成長の可能性を探るべく、JavaScriptのみを使ってディープニューラルネットワークモデルを構築することに挑戦してみます。

TensorFlow

TensorFlowはGoogleが公開しているディープラーニングのフレームワークです。FacebookのPyTorchと人気を二分するディープラーニングの二大フレームワークの1つです。学術分野では最近PyTorchに追い上げられているとの指摘もありますが、産業分野では一日の長があり2019年現在もっとも広く使用されているディープラーニングのフレームワークです。

TensorFlowのメインのターゲットはPythonですが、Python以外にもJavaScript, C++, Java, Go, Swiftなどが(実験的に)サポートされています。

TensorFlow.js

TensorFlow.js is a JavaScript Library for training and deploying machine learning models in the browser and in Node.js.

TensorFlow.jsはTensorFlowのJavaScript版のAPIです。TensorFlow.jsのもっとも重要な用途は他の環境で作成したTensorFlowのトレーニング済みモデルをウェブブラウザ上のアプリケーションに組み込めるようにすることです。ただしTensorFlow.jsはブラウザ上でのトレーニング済みモデルの実行だけでなく、Node.js上での実行やJavaScriptを使ったディープラーニングモデルのトレーニングもサポートしています。そのためTensorFlow.jsを使えばPythonを使うことなく、JavaScriptのみでディープラーニングモデルの構築からトレーニング、実際のアプリケーションとしての利用までエンドツーエンドで行うことが可能になっています。

Jupyter

Pythonをブラウザからインタラクティブに実行するための環境です。コードや実行結果が「ノート」として保存されるので非常に便利です。JavaScriptユーザーにはあまり知られてないツールかもしれませんが、Pythonを用いたデータ処理や機械学習、あるいは教育分野で非常に人気のあるツールであり、ここ数年で急激にユーザを増やしています。2019年のGitHub OctoverseでもJupyterの急激な成長が言及されています。Jupyter Notebook自体についてもっと知りたい人はGoogleでJupyterを検索して下さい。

Jupyterは「カーネル」を追加することでPython以外の言語もサポートすることが可能です。さまざまな言語のカーネルが存在しています。今回は僕が作っているJavaScript/TypeScript用のカーネルを使用してJupyter上で機械学習を行います。


出典: https://octoverse.github.com/#industry-spotlight-data-science

tslab

Jupyter上でJavaScriptおよびTypeScriptをインタラクティブに実行するためのJupyterカーネルです。

yunabe/tslab - Interactive JavaScript and TypeScript programming with Jupyter (GitHub)

(気に入ったらぜひStarもして下さい:star:)

tslabはTypeScriptをベースに作られており、以下のような特徴があります:

  • Jupyter上でのJavaScriptおよびTypeScriptのインタラクティブな実行
  • TypeScript由来の静的型情報の活用
    • JavaScriptでも静的型チェックが行われる。
    • 静的型情報を利用した、コード補完やtipsの表示
  • TypeScript 3.7のサポート
  • Top-level await のサポート

またtslabはJavaScript/TypeScriptのリッチなREPLとして使用することもできます。

この記事ではtslabを使って、Jupyter上でインタラクティブにディープラーニングモデルを構築していきます。

complete.jpg
complete.jpg

環境構築

Node.js

Node.jsがインストールされていない場合はNode.jsを導入しましょう。現在の最新のLTSであるv12を前提に話を進めますが、v10, v13でも問題はないと思います。

Jupyter

Jupyterを使ってインタラクティブにディープラーニングモデルの構築を行いたいので、Jupyterもインストールしましょう。Pythonがよくわからない場合はAnacondaを使って導入するのが一番簡単だと思います。最新版のJupyterはPython2のサポートをすでに停止しています。特に理由がなければPython3版を使用して下さい。

tslab

tslabのREADME.mdに従ってインストールしてください。

npm install -g tslab

インストールに成功したら、tslabをJupyterに登録します

tslab install

tslab installが成功したら、jupyter kernelspec listでカーネルがJupyterに登録されていることを確認しましょう。

$ jupyter kernelspec list
Available kernels:
  jslab      /usr/local/google/home/yunabe/.local/share/jupyter/kernels/jslab
  tslab      /usr/local/google/home/yunabe/.local/share/jupyter/kernels/tslab

これでJupyter上でJavaScript/TypeScriptを実行できる環境が整いました。

jupyter notebook [--port=8888]

を実行して、JavaScript, TypeScriptの「ノートブック」がJupyterで作成でき、JavaScriptをインタラクティブにノートブック上で実行できることを確かめてください。

npmプロジェクトの作成

Node.js, Jupyter, tslab のセットアップができたら環境のセットアップは完了です。npmのプロジェクトを作ってTensorFlowをインストールし、ディープニューラルネットーワークをJavaScriptで行っていきましょう。今回は僕が作成したnpmプロジェクトとJupyterのノートがGitHubに置いてあるのでそれを取得してください。

git clone https://github.com/yunabe/qiita-20191202-jsml.git
cd qiita-20191202-jsml
npm install # or yarn

一からプロジェクトを構築する場合は通常通り、npm inityarn initでプロジェクトを作ってください。

TensorFlow.js のインストール

チュートリアルTensorFlow.js in Nodeに従って、TensorFlow.jsをプロジェクトにインストールしてください。

npm install @tensorflow/tfjs-node

TensorFlow.jsはGPUによる高速化にも対応しています。
LinuxでGPU/CUDAが使用可能な環境であれば、tsjs-nodeではなく、@tensorflow/tfjs-node-gpuをインストールしてください。

これでJupyter上でTensorFlow.jsを使ってディープラーニングを行う準備は完了です。Jupyterを起動してJavaScriptあるいはTypeScriptのノートブックを作成してください。ここから先のコードはJupyter上で実行していきます。

jupyter notebook

ここで使ったノートブックはGitHubにコミットしてあります。ここから先はGitHub上のノートブックを直接参照してもらっても構いません。この記事ではJavaScriptを使用していますが、ノートブックはTypeScript版も用意しています。TypeScriptが分かる人にはTypeScript版をお勧めします。

GitHub

nbviewer (GitHubよりノートブックがみやすいです)

JavaScriptと静的型チェック

今回使用するJavaScript/TypeScriptの実行環境 tslabはTypeScriptをベースに実装されており、JavaScriptに対してもある程度の静的型チェックを行います。

let x = 123;
x += ' hello';
console.log(x);

例えば上のコードは1行目ではxの型はnumberであることを意図しているようにみえるのに、2行目でstringを代入しています。tslabはこのようなケースに対してエラーを表示します。

2:1 - Type 'string' is not assignable to type 'number'.

ただし、上記のコードはstringの代入が本当に意図的なのであれば正当なJavaScriptのコードです。そのような場合にはJSDocを使ってxが任意の型(any)をとりうることを明示する必要があります。

/** @type {any} */
let x = 123;
x += ' hello';
console.log(x);

このノートブックでは

を利用しているので留意して下さい。型チェックの詳細についてはType Checking JavaScript Files
を参照して下さい。

またtslabでは静的型情報を使っているので、強力なコードの補完(Tab)と変数・関数定義情報の表示(Shift-Tab)が使用可能です。活用しながらコーディングして下さい。

TensorFlow.jsがインストールされていることの確認

まず初めに、TensorFlow.jsが正しくimportでき実行できるかバージョンを表示して確かめてみましょう。ちなみにtslabでHTMLや画像を表示するにはtslab.displayを使用します。無事バージョン番号が表示されたでしょうか。

ちなみにJupyter上ではTabでコード補完、Shift-Tabで関数定義などの表示が行えます。活用しながらコードを書いてください。

// const tf = require('@tensorflow/tfjs-node') も可能。
import * as tf from '@tensorflow/tfjs-node'
import * as tslab from "tslab";

tslab.display.html('<h2>tf.version</h2>')
console.log(tf.version);
tslab.display.html('<h2>tslab.versions</h2>')
console.log('tslab.versions:', tslab.versions);

MNIST をダウンロードする

TensorFlowが無事にNodeにインストール出来たので、ニューラルネットワークモデルを実際に構築してJavaScriptでトレーニングを行ってみましょう。ここでは機械学習のチュートリアルで常に使わる手書き文字認識のデータセットMNIST databaseを使って、数字の文字認識の機械学習を行ってみましょう。

TensorFlow.jsのサンプルコードの中にMNISTをダウンロードしてTensorFlowの内部表現に変換するコードの例が存在するので今回はそれを利用します。このコードはすでに例のレポジトリにコピーしてあるのでここではそれをimportします。興味があればどのような実装になっているかのぞいてみてください。

loadData()Promiseを返すのでタスクの終了まで待機するのを忘れないでください。tslabはtop-level awaitをサポートしているのでawaitをつけるだけでOKです。

import mnist from '../lib/mnist';
await mnist.loadData();

データの確認と可視化

WikipediaのMNISTの記事にも書かれているように、MNISTのデータは60,000の訓練用データ(training data)と10,000の評価用データ(test data)に事前に分けられています。実際にダウンロードされたデータの大きさを確認してみましょう。

訓練データに60,000個、評価データに10,000個の数字の画像(images)と文字認識の正解データ(labels)が存在することが確認できたと思います。数字の画像データは28x28の白黒1チャンネル(グレースケール)であることも分かります。

ちなみにデータの内部表現として使われているTensorのAPIはここにドキュメントがあります

tslab.display.html('<h2>訓練データのサイズ</h2>')
console.log(mnist.getTrainData());
tslab.display.html('<h2>評価データのサイズ</h2>')
console.log(mnist.getTestData());
// 訓練データのサイズ
{
  images: Tensor {
    kept: false,
    isDisposedInternal: false,
    shape: [ 60000, 28, 28, 1 ],
    dtype: 'float32',
    size: 47040000,
    strides: [ 784, 28, 1 ],
    dataId: {},
    id: 0,
    rankType: '4'
  },
  labels: Tensor {
    kept: false,
    isDisposedInternal: false,
    shape: [ 60000, 10 ],
    dtype: 'float32',
    size: 600000,
    strides: [ 10 ],
    dataId: {},
    id: 10,
    rankType: '2',
    scopeId: 6
  }
}
// 評価データのサイズ
{
  images: Tensor {
    kept: false,
    isDisposedInternal: false,
    shape: [ 10000, 28, 28, 1 ],
    dtype: 'float32',
    size: 7840000,
    strides: [ 784, 28, 1 ],
    dataId: {},
    id: 11,
    rankType: '4'
  },
  labels: Tensor {
    kept: false,
    isDisposedInternal: false,
    shape: [ 10000, 10 ],
    dtype: 'float32',
    size: 100000,
    strides: [ 10 ],
    dataId: {},
    id: 21,
    rankType: '2',
    scopeId: 14
  }
}

次にMNISTのデータの可視化もしてみましょう。MNISTの画像データは28x28の0から1.0のグレースケールのデータの配列です。画像ライブラリjimpを使用してPNGに変換して可視化してみます。

import Jimp from 'jimp';
import {promisify} from 'util';

/**
 * @param {tf.Tensor4D} images
 * @param {number} start
 * @param {number} size
 * @return {Promise<Buffer[]>}
 */
async function toPng(images, start, size) {
    // Note: mnist.getTrainData().images.slice([index], [1]) is slow.
    let arry = images.slice([start], [size]).flatten().arraySync();
    let ret = [];
    for (let i = 0; i < size; i++) {
        let raw = [];
        for (const v of arry.slice(i * 28 * 28, (i+1)*28*28)) {
            raw.push(...[v*255, v*255, v*255, 255])
        }
        let img = await promisify(cb => {
            new Jimp({ data: Buffer.from(raw), width: 28, height: 28 }, cb);
        })();
        ret.push(await img.getBufferAsync(Jimp.MIME_PNG));
    }
    return ret;
}

{
    const size = 8;
    const labels = await mnist.getTestData().labels.slice([0], [size]).argMax(1).array();
    const pngs = await toPng(mnist.getTestData().images, 0, size);
    for (let i = 0; i < size; i++) {
        tslab.display.html(`<h3>label: ${labels[i]}</h3>`)
        tslab.display.png(pngs[i]);
    }
}

ディープラーニングで文字認識を行う

MNISTのデータの素性が一通り分かったので、TensorFlow.jsを使って「ディープニューラルネットワーク」機械学習のモデルを設計、訓練し文字認識を行ってみます。
初めはPythonのTensorFlowチュートリアルでも利用されている、128ノードの中間層を一つ持つ単純なニューラルネットワークモデルを使って文字認識を行います。

TensorFlow.jsは、PythonのTensorFlowでも使われているkerasをベースにしたAPIを提供しています。そのためこの程度のシンプルなディープニューラルネットワークはTensorFlow.jsでも非常に簡単に実装できます。Layer APIの詳細はTensorFlow.jsのドキュメントを参照してください。

const model = tf.sequential();
model.add(tf.layers.flatten({inputShape: [28, 28, 1]}));
model.add(tf.layers.dense({units: 128, activation: 'relu'}));
model.add(tf.layers.dropout({rate: 0.2}));
model.add(tf.layers.dense({units: 10, activation: 'softmax'}));

model.compile({
  optimizer: 'adam',
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
});

/**
 * @param {tf.Sequential} model
 * @param {number} epochs
 * @param {number} batchSize
 * @param {string} modelSavePath
 */
async function train(model, epochs, batchSize, modelSavePath) {
  // Hack to suppress the progress bar by TensorFlow.js
  process.stderr.isTTY = false;
  const {images: trainImages, labels: trainLabels} = mnist.getTrainData();
  model.summary();

  let epochBeginTime;
  let millisPerStep;
  const validationSplit = 0.15;
  const numTrainExamplesPerEpoch =
      trainImages.shape[0] * (1 - validationSplit);
  const numTrainBatchesPerEpoch =
      Math.ceil(numTrainExamplesPerEpoch / batchSize);
  const batchesPerEpoch = Math.floor(trainImages.shape[0]*(1-validationSplit)/batchSize);
  /** @type {tslab.Display} */
  let display = null;
  await model.fit(trainImages, trainLabels, {
    callbacks: {
      onEpochBegin: (epoch) => {
        display = tslab.newDisplay();
      },
      onBatchBegin: (batch) => {
        display.text(`Progress: ${(100*batch/batchesPerEpoch).toFixed(1)}%`)
      },
    },
    epochs,
    batchSize,
    validationSplit,
  });

  const {images: testImages, labels: testLabels} = mnist.getTestData();
  const evalOutput = model.evaluate(testImages, testLabels);

  console.log(
      `\nEvaluation result:\n` +
      `  Loss = ${evalOutput[0].dataSync()[0].toFixed(3)}; `+
      `Accuracy = ${evalOutput[1].dataSync()[0].toFixed(3)}`);

  if (modelSavePath != null) {
    await model.save(`file://${modelSavePath}`);
    console.log(`Saved model to path: ${modelSavePath}`);
  }
}

const epochs = 5;
const batchSize = 32;
const modelSavePath = 'mnist'
await train(model, epochs, batchSize, modelSavePath);
_________________________________________________________________
Layer (type)                 Output shape              Param #   
=================================================================
flatten_Flatten1 (Flatten)   [null,784]                0         
_________________________________________________________________
dense_Dense1 (Dense)         [null,128]                100480    
_________________________________________________________________
dropout_Dropout1 (Dropout)   [null,128]                0         
_________________________________________________________________
dense_Dense2 (Dense)         [null,10]                 1290      
=================================================================
Total params: 101770
Trainable params: 101770
Non-trainable params: 0
_________________________________________________________________
Epoch 1 / 5
Epoch 2 / 5
Epoch 3 / 5
Epoch 4 / 5
Epoch 5 / 5
14906ms 292us/step - acc=0.974 loss=0.0845 val_acc=0.975 val_loss=0.0796 

Evaluation result:
  Loss = 0.083; Accuracy = 0.980
Saved model to path: mnist

トレーニングしたモデルを実行する

98%とそこそこ精度のよい文字認識のモデルができたので実際にテストデータを使って文字認識を行ってみましょう。

const predicted = /** @type {number[]} */(tf.argMax(/** @type {tf.Tensor} */ (model.predict(mnist.getTestData().images)), 1).arraySync());
const labels = /** @type {number[]} */(tf.argMax(mnist.getTestData().labels, 1).arraySync());
console.log('predictions:', predicted.slice(0, 10));
console.log('labels:', labels.slice(0, 10));
predictions: [
  7, 2, 1, 0, 4,
  1, 4, 9, 5, 9
]
labels: [
  7, 2, 1, 0, 4,
  1, 4, 9, 5, 9
]

正しく文字認識ができていますね。せっかくなので文字認識に失敗する例を可視化してみましょう。精度が98%程度あるとはいえ、人間であれば間違えないようなものが多いですね。

const predicted = /** @type {number[]} */(tf.argMax(/** @type {tf.Tensor} */ (model.predict(mnist.getTestData().images)), 1).arraySync());
const labels = /** @type {number[]} */(tf.argMax(mnist.getTestData().labels, 1).arraySync());

const numSamples = 32;
let count = 0;
for (let i = 0; i < predicted.length && labels.length; i++) {
    const pred = predicted[i];
    const label = labels[i];
    if (pred === label) {
        continue;
    }
    tslab.display.html(`<h3>予測: ${pred}, 正解: ${label}</h3>`)
    const pngs = await toPng(mnist.getTestData().images, i, 1);
    tslab.display.png(pngs[0]);
    count++;
    if (count >= numSamples) {
        break;
    }
}

CNN (convolutional neural network) による画像認識

MNISTの文字列認識は典型的な画像を対象としたディープラーニングなので、CNNによるディープラーニングも試してみましょう。モデルの構造はTensorFlow.jsの例から拝借してきます

const cnnModel = tf.sequential();
cnnModel.add(tf.layers.conv2d({
  inputShape: [28, 28, 1],
  filters: 32,
  kernelSize: 3,
  activation: 'relu',
}));
cnnModel.add(tf.layers.conv2d({
  filters: 32,
  kernelSize: 3,
  activation: 'relu',
}));
cnnModel.add(tf.layers.maxPooling2d({poolSize: [2, 2]}));
cnnModel.add(tf.layers.conv2d({
  filters: 64,
  kernelSize: 3,
  activation: 'relu',
}));
cnnModel.add(tf.layers.conv2d({
  filters: 64,
  kernelSize: 3,
  activation: 'relu',
}));
cnnModel.add(tf.layers.maxPooling2d({poolSize: [2, 2]}));
cnnModel.add(tf.layers.flatten());
cnnModel.add(tf.layers.dropout({rate: 0.25}));
cnnModel.add(tf.layers.dense({units: 512, activation: 'relu'}));
cnnModel.add(tf.layers.dropout({rate: 0.5}));
cnnModel.add(tf.layers.dense({units: 10, activation: 'softmax'}));

const optimizer = 'rmsprop';
cnnModel.compile({
  optimizer: optimizer,
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
});

最初のモデルに比べると構造が複雑なのでトレーニングには時間がかかります。GPUが使える場合はGPUによる高速化の威力がよく実感できると思います。

const epochs = 20;
const batchSize = 128;
const modelSavePath = 'cnn_mnist'
await train(cnnModel, epochs, batchSize, modelSavePath);
Layer (type)                 Output shape              Param #   
=================================================================
conv2d_Conv2D5 (Conv2D)      [null,26,26,32]           320       
_________________________________________________________________
conv2d_Conv2D6 (Conv2D)      [null,24,24,32]           9248      
_________________________________________________________________
max_pooling2d_MaxPooling2D3  [null,12,12,32]           0         
_________________________________________________________________
conv2d_Conv2D7 (Conv2D)      [null,10,10,64]           18496     
_________________________________________________________________
conv2d_Conv2D8 (Conv2D)      [null,8,8,64]             36928     
_________________________________________________________________
max_pooling2d_MaxPooling2D4  [null,4,4,64]             0         
_________________________________________________________________
flatten_Flatten3 (Flatten)   [null,1024]               0         
_________________________________________________________________
dropout_Dropout4 (Dropout)   [null,1024]               0         
_________________________________________________________________
dense_Dense5 (Dense)         [null,512]                524800    
_________________________________________________________________
dropout_Dropout5 (Dropout)   [null,512]                0         
_________________________________________________________________
dense_Dense6 (Dense)         [null,10]                 5130      
=================================================================
Total params: 594922
Trainable params: 594922
Non-trainable params: 0
_________________________________________________________________
Epoch 1 / 20
  ...
Epoch 20 / 20

Evaluation result:
  Loss = 0.022; Accuracy = 0.994
Saved model to path: cnn_mnist

テストデータに対する精度が99.4%まで上昇しました。先の精度98%の単純なモデルでやったように、CNNでも認識に失敗している画像を表示してみましょう。
最初の単純なモデルの失敗例に比べると、人間でも認識に失敗しそうな、あるいはラベルが間違っていると言いたくなるような例が多くなっており、文字認識の精度が大きく向上していることが体感できると思います。

const predicted = /** @type {number[]} */(tf.argMax(/** @type {tf.Tensor} */ (cnnModel.predict(mnist.getTestData().images)), 1).arraySync());
const labels = /** @type {number[]} */(tf.argMax(mnist.getTestData().labels, 1).arraySync());

const numSamples = 32;
let count = 0;
for (let i = 0; i < predicted.length && labels.length; i++) {
    const pred = predicted[i];
    const label = labels[i];
    if (pred === label) {
        continue;
    }
    tslab.display.html(`<h3>予測: ${pred}, 正解: ${label}</h3>`)
    const pngs = await toPng(mnist.getTestData().images, i, 1);
    tslab.display.png(pngs[0]);
    count++;
    if (count >= numSamples) {
        break;
    }
}

以上でJavaScriptのみで複雑なディープラーニングモデルを構築・訓練から実際にアプリケーション上で推論を行うところまでエンドツーエンドで行うことができました。TensorFlow.jsを使っているので、完成したモデルをブラウザ上で実行することも最小限の追加コードで実現できます。詳しくはTensorFlow.jsのドキュメントや他の人による解説記事を参照してください。

最後に

JavaScript (TypeScript) でもTensorFlowとJupyterを使用してディープラーニングのトレーニングが行えることを示しました。TensorFlow.jsを使えばPythonを使用しなくてもJavaScriptだけで最新のディープラーニングをモデルのトレーニングから実際のプロダクションでの利用までエンドツーエンドで行うことが可能です。

もちろん現状では機械学習の分野でのPythonの地位は圧倒的なものであり、TensorFlowがJavaScriptをサポートしているとはいえ、APIやドキュメントの充実度には同じTensorFlowの中でも天地ほどの差があります。また周辺のデータ分析関係のライブラリの充実度やエコシステムの規模も考えると機械学習のプロジェクトにJavaScript(というよりPython以外の言語)をメインで採用する合理的な理由は現時点ではほとんど存在しないと思います。

一方でPythonは2010年代に大きく成長したもう一つの分野であるモバイルアプリ開発やWebの分野ではそれほど成功していない現状や、Webフロントエンド開発でのJavaScriptの地位は当分の間は揺らぎそうにないこと、TypeScriptとそのエコシステムが大きく成長しているおり大規模開発にも耐えうるようになってきていることなどを踏まえるとデータ分析分野でのPythonの不動の地位と、それによるPythonの人気の上昇も必ずしも向こう10年間安泰ではないかもしれません。いずれにしてもプログラミング言語のある分野での人気と人気を裏付ける利便性は鶏と卵の関係にあるので誰かが初期投資をする必要があります。2019年末現在、この分野におけるJavaScriptのライブラリ、フレームワークは非常に貧弱であり、Pythonにおけるnumpy, pandas, sklearnなどのようなデファクトスタンダードは存在しません。JavaScriptユーザのみなさん、ブルー・オーシャンであるJavaScriptの機械学習分野での成長に賭けてみるのも面白いかもしれませんよ。

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

javascript 基本その3

本日もjavascriptについて書き込みこみます。
今日はHTMLを指定して画面上の表示を帰る方法です。

DOM

DOMとはDocument Object Model(ドキュメントオブジェクトモデル)の略です。
HTMLを解析し、データを作成する仕組みです。

ユーザーが画面をみる仕組みは

1,HTMLを解析、DOM(WEBページ)に変換
2,CSS,javascriptを読み込み装飾
3,ユーザーが画面をみる。

という工程です。
要はCSSのように装飾を担当するのがjavascriptの役目です。

HTMLは階層構造になっていることが特徴です。
DOMによって解析されたHTMLは、階層構造のあるデータとなります。
これを、DOMツリーやドキュメントツリーと呼びます。

DOMツリー

基本的に各タグがネスト(入れ子構造になっていて)
ツリーのようになっている感覚です。

body
|
--header--p
|
--main--p
|
--footer--p

簡単に書くとこのようになります。

ノード

HTML1つ1つのタグが、DOMツリーの中ではノードと呼ばれます。

例として
body,header,fooder,main,など

下記にHTMLの取得方法について紹介します。

document

documentは、開いているWebページのDOMツリーが入っているオブジェクトです。
documentに対していくつかのメソッドを利用することで
DOMツリーに含まれる要素を抽出して取得することができます。

大文字小文字
単数形、複数形に注意してください。

document.getElementById("id名");

.getElementById("id名")はDOMツリーから特定の要素を取得するためのメソッドの1つです。
引数に渡したidを持つ要素を取得します。

IDの場合
document.getElementById("id名");

document.getElementsByClassName("class名");

classを指定して取得する際はこちらを利用します。
ここで気をつけたいのは
getElementsと複数形になっていることです。

id名はhtml上に必ず一つしか存在しないのに対して
class名を指定するgetElementsByClassName("class名")の場合は
同じclassを持つ要素を全て取得することが可能です。

クラスの場合
document.getElementsByClassName("クラス名");

document.querySelector("セレクタ名");

セレクタ名とは、CSSでスタイルを適用するために指定している要素です。
セレクタ名を指定してDOMを取得する場合
querySelectorメソッドを使用します。

HTML上から、引数で指定したセレクタに合致するもののうち一番最初に見つかった要素1つを取得します。

セレクタの場合
document.querySelector("セレクタ名");
セレクタの例
p.menu{  /*p.menuがセレクタにあたります*/
color:red
}

querySelectorメソッドでは、複雑なセレクタも指定できます。

複雑な例
document.querySelector("button#Button2"); // idがButton2のbuttonタグ要素
document.querySelector("footer a.next"); // footerタグ要素の中の、クラスがnextのaタグ要素

イベント

JavaScriptにおけるイベントとは、HTMLの要素に対して行われた処理要求のことをいいます。
例えば

  • 「ユーザーがブラウザ上のボタンをクリックした」
  • 「テキストフィールドでキー入力をした」
  • 「要素の上にマウスカーソルを乗せた」

などがいずれもイベントです。

また、イベントを起こすのはユーザーだけでなく

  • 「ドキュメントの読み込みが終わった」

などブラウザが発生させるものもあります。

イベント駆動

JavaScriptはイベント駆動という概念に基づいて設計されています。
これは、「イベント」が発生したら、それをきっかけにコードが実行される仕組みです。

イベントを取得するためには、先に取得したノードに対して処理を書きます。

addEventListener

addEventListenerメソッドはノードオブジェクトのメソッドです。

この記述の読み込み以降で「ノードオブジェクト」に「イベント」が起きた時
「関数」を実行するようになります。

一つのイベントと一つの関数を紐付ける仕組みのことを

イベントリスナと呼びます。

一つのイベントに複数の関数を紐付ける場合は、関数の数だけイベントリスナが存在します。

addEventListenerメソッド
(ノードオブジェクト).addEventListener("イベント名", 関数);

上記よりコードを書いていくと下記のようになりますが。
これ単体だとnullというエラーがおきます。

基本使用方法の間違い
let btn = document.querySelector("button");
// ボタンをノードオブジェクトとして取得し、変数btnに代入する

function printHello() {
  console.log("Hello world");
}
// printHello関数を定義

btn.addEventListener("click", printHello);
// ボタンのノードオブジェクトであるbtnに対して、
// clickイベントとprintHello関数を紐付ける仕組みであるイベントリスナを追加する

ブラウザは上から順に実行をするので、このJavaScriptのコードを読み込む時
まだhtmlファイルのheadタグ内までしか読み込まれておらず

bodyタグ内にあるbuttonタグは読み込まれていなかったのです。

基本HTMLコード
<head>

<script ~ ></script>
<!--ここまでしか読み混まれないのでbuttonタグまで到達せずエラーが起きます。-->

</head>

<body>

<button ~ >< /button >
<!--ここまで読み込みたい-->

<body>

これを解決するため全て読み込んでから実行する
下記のような実行が必要です。

ページの読み込みをするwindow.onloadを使おう。

ページの読み込みは、以下2つの記述方法があります。

その1,onload
window.onload = function() { 処理 };
その2,addEventListener
window.addEventListener('load', function() { 処理 });

その2を踏まえてコードを書いていくと

基本使用方法の正解
function printHelloWithButton() {
  let btn = document.querySelector("button");

  function printHello() {
    console.log("Hello world");
  }
  // 関数内で定義された関数は、関数の中でしか呼び出せない性質があるだけで、
  // 通常の関数同様に呼び出せる

  btn.addEventListener("click", printHello);
}
// 一連の処理をまとめた関数を作る

window.addEventListener("load", printHelloWithButton);

コードの改善

今まで書いたことは三つの改善方法があるので書いて行きます。

先に結論だけ行って仕舞えば
addEventListenerの第二引数にfunctionを持ってくるポイントと
考えてください。

第二引数に置換

改善前
function func() {}
// 何もしないfunc関数

btn.addEventListener("click", func);

第二引数に内容をそのまま写して

改善後1
btn.addEventListener("click", function func() {});

関数名の省略

第二引数に置かれたため、関数名を省略することができます。

改善後2
btn.addEventListener("click", function() {});

コードを複数行書く

処理がある場合(殆どの場合)は一般に改行を用いて以下のように書きます。

改善後3
btn.addEventListener("click", function() {
  // 処理
});

上三つを踏まえた改善例

sample改善前
window.addEventListener("load", function() {
  let btn = document.querySelector("button"); //処理1 ノード取り出し変数宣言

  function printHello() {      //処理2 関数宣言
    console.log("Hello world");
  }

  btn.addEventListener("click", printHello); //処理3 イベントリスナ
});
sampleの改善後
window.addEventListener("load", function() {
  let btn = document.querySelector("button"); //処理1 ノード取り出し変数宣言

  btn.addEventListener("click", function() { //処理2 + 処理3
    console.log("Hello world");
  });
});

格HTMLの処理方法

innerHTML

innerHTMLを使用するとHTML要素の中身を書き換えることができます。

書換例
// テキストの要素を取得し、変数で定義
let btn = document.querySelector("#Button");
let changeText = document.querySelector("p");

// ボタン2をクリックしたらテキストが置換される
btn.addEventListener("click", function() {
  changeText.innerHTML = '変更されました';
});

classList.add

クラス追加するメソッドです。

クラス追加
// Button3を取得して、変数で定義
let btn3 = document.querySelector("#Button3");

// クラス追加を押したらredクラスが追加される
btn3.addEventListener("click", function() {
  changeText.classList.add("red");
  console.log(changeText.classList); // ここに追加
});

classList.remove

クラス削除するメソッドです。

クラス削除
// Button4を取得して、変数で定義
let btn4 = document.querySelector("#Button4");

// div要素を取得して、変数で定義
let obj = document.querySelector("div");

// クラス削除を押したらblueクラスが削除される
btn4.addEventListener("click", function() {
  obj.classList.remove("blue");
});

小まとめ

まとめ
window.addEventListener("load", function() {
  let btn = document.querySelector("button#Button");

  btn.addEventListener("click", function() {
    console.log("Hello world");
  });

  // テキストの要素を取得し、変数で定義
  let btn2 = document.querySelector("button#Button2");
  let changeText = document.querySelector("p");

  // ボタン2をクリックしたらテキストが置換される
  btn2.addEventListener("click", function() {
    changeText.innerHTML = '変更されました';
  });

  // Button3を取得して、変数で定義
  let btn3 = document.querySelector("#Button3");

  // クラス追加を押したらredクラスが追加される
  btn3.addEventListener("click", function() {
    changeText.classList.add("red");
  });

  // Button4を取得して、変数で定義
  let btn4 = document.querySelector("#Button4");
  // div要素を取得して、変数で定義
  let obj = document.querySelector("div");

  // クラス削除を押したらblueクラスが削除される
  btn4.addEventListener("click", function() {
    obj.classList.remove("blue");
  });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

冬空の中でReactの世界観をちょっとだけ広げたかった

それはただ美しかった。

コンポーネントという呪縛がある一方で、開発者の自由さを守りながら宣言的UIによる美しい構成が可能となった。

数千行に渡るDOMイベント管理の苦行から開放された。

見上げればそこにはReactがあった。

まるでオリオン座のように。

だがしかしBUT

仮想DOMというバーチャルな世界を実現するために、「Hello World」を表示するには現実的にWebpackやRollup等のバンドルが必要になった。

これを私は「コンポーネントの呪縛」と勝手に思っている。(悪気はない。)

初学者だった頃のjQuery使いの私にはしばらく難しかった。

とりあえずこれのようなminimalなスターターを書いて、そっと公式ドキュメントを閉じた。

数年の月日を経て

複雑なステート管理にはReduxが有名に、事実上のデファクトとなった。

やがてHooksが誕生した。

Hooksの登場により再度公式ドキュメントを開き、そしてそれをそっと閉じた。

どうしてもReduxのような管理方法が必要になった

JSerとしての経験値が上がるにつれて、jQuery思想の自分にはやがて限界が訪れた。

コンポーネントである以上基本は親が子を教育するのである。

したがってグローバルなstoreへどのコンポーネントからもアクセスできる機構が必要になったのだ。

dispatchめ。(悪気はない。)

おや...

結局グローバルstoreは親から子へ移譲しているだけではないか。

親から子へデータを渡している?

それは究極の発想ではview template engineに近しいのではなかろうか。

JSならejsやhandlebars、PHPあたりではbladeとかそういった、あの{{ data }}ではなかろうか。

あの古き良き、サーバーから受け取ったデータをビューへ渡す方法ではなかろうか。

Reactを view template engine にできないだろうか

調べたらあった。

https://github.com/reactjs/express-react-views

サーバーからデータを渡す:

// views/index.jsxをレンダリング
res.render('index', { message: 'Hello World' });

するとviews/index.jsxのpropsにデータが伝搬される:

var React = require('react');

function IndexPage(props) {
  return <div>{props.message}</div>;
}

module.exports = IndexPage;

素晴らしいではないか。

と思ったら

It renders static markup and does not support mounting those views on the client.

静的マークアップをレンダリングするだけでReactはマウントされないだと。。。

Reactにする意味はあるのか?(ビューの数千行が数百行くらいにはなるかもしれないが。。。)

JSのイベントはどうする?

CSS-in-JSはどうなる?

どうしてもReactをマウントしたかった

https://github.com/saltyshiomix/react-ssr

$ npm install --save @react-ssr/express
package.json
{
  "scripts": {
    "start": "node server.js"
  }
}
server.js
const express = require('express');
const register = require('@react-ssr/express/register');

const app = express();

(async () => {
  // Reactビューエンジンとして`.jsx`を登録
  await register(app);

  app.get('/', (req, res) => {
    const message = 'Hello World!';
    res.render('index', { message });
  });

  app.listen(3000, () => {
    console.log('> Ready on http://localhost:3000');
  });
})();
views/index.jsx
export default function Index({ message }) {
  return <p>{message}</p>;
}

そう、Reactをそのまま利用できるビューエンジンにしたかった

views/index.jsx
import React from 'react';

const IndexPage = (props) => {
  const [message, setMessage] = React.useState('waiting...');

  const onClick = () => setMessage('This is a react-ssr!');

  return (
    <React.Fragment>
      <button onClick={onClick}>Click Me</button>
      <p>Message from state: {message}</p>
    </React.Fragment>
  );
};

export default IndexPage;

スクリーンショット 2019-11-30 22.12.58.png

↓↓↓

スクリーンショット 2019-11-30 22.13.27.png

想像してみてほしい

server.js
const posts = [
  { id: 1, body: 'This is a first post.' },
  { id: 2, body: 'This is a second post.' },
  { id: 3, body: 'This is a last post.' },
];

app.get('/', (req, res) => {
  res.render('index', { posts });
});

app.get('/posts/:postId', (req, res) => {
  const { postId } = req.params;
  const post = findById(postId);
  res.render('post', { post });
});

この古き良きデータ流しを、

views/index.jsx
import React from 'react';

const IndexPage = ({ posts }) => {
  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

export default IndexPage;

↓↓↓

http://localhost:3000

スクリーンショット 2019-11-30 22.23.13.png

views/post.jsx
import React from 'react';

const PostPage = ({ post }) => {
  return (
    <React.Fragment>
      <p>{post.body}</p>
    </React.Fragment>
  );
};

export default PostPage;

↓↓↓

http://localhost:3000/posts/1

スクリーンショット 2019-11-30 22.23.55.png

Reactで扱える喜びを...!!!

NEXT.jsとの比較

データの非同期取得

クライアント側のデータ非同期取得は、どちらもReact.useEffect()で非同期関数を処理する必要がある。

NEXT.jsの場合

pages/index.jsx
import React, { useState, useEffect } from 'react';

const HomePage = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    // componentDidMount like
    fn() {
      const res = await fetch('http://localhost:4000/api/posts');
      const posts = await res.json();
      setPosts(posts);
    }
    fn();

    return () => {
      // componentWillUnmount like
    };
  }, []);

  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

export default HomePage;

しかしこれではSSRされないのでSEO対策にならない。

仕方なく以下のようなNEXT.js wayに従って書き直す。

pages/index.jsx
import React from 'react';

const HomePage = ({ posts }) => {
  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

HomePage.getInitialProps = async () => {
  const res = await fetch('http://localhost:4000/api/posts');
  const posts = await res.json();
  return { posts };
};

export default HomePage;

だいぶすっきりしたものの、非同期データをSSRさせたい場合、このgetInitialPropsという魔法の呪文を唱える必要がある。

react-ssrの場合

すでに紹介したように、view template engineとして機能するため、APIサーバーから取得したデータをそのままビューへ渡せば良い。

そもそも同一サーバーならAPIサーバーを建てる必要もなく、直接DBから取得したデータをビューへ渡せばpropsから取得データへアクセスできる。

server.js
(async () => {
  const posts = await fetchFromDbOrAPIServer();

  app.get('/posts', (req, res) => {
    res.render('posts', { posts });
  });
})();
views/index.jsx
import React from 'react';

const HomePage = ({ posts }) => {
  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

export default HomePage;

ユーザー認証

NEXT.jsの場合

iaincollins/next-authというライブラリがあるが、これもクライアント側ではgetInitialPropsを利用する。

しかしサーバー側の使い方が、NEXT.js wayなサーバーの書き方をする必要があったり、そもそもドキュメントが充分とは思えないのであまり使う気にはなれなかった。

server.js
const next = require('next')
const nextAuth = require('next-auth')
const nextAuthConfig = require('./next-auth.config')

require('dotenv').load()

const nextApp = next({
  dir: '.',
  dev: (process.env.NODE_ENV === 'development')
})

nextApp.prepare()
.then(async () => {
  const nextAuthOptions = await nextAuthConfig()
  const nextAuthApp = await nextAuth(nextApp, nextAuthOptions)  
  console.log(`Ready on http://localhost:${process.env.PORT || 3000}`)
})
.catch(err => {
  console.log('An error occurred, unable to start the server')
  console.log(err)
})

↑を読んでこれから何が起こるのかわかるエスパーはいるのだろうか。

react-ssrの場合

react-ssrはExpressのview template engineとして振る舞うので、古き良きExpress wayなユーザー認証を利用できる。

例えばメールアドレス・パスワードによる認証であればpassportpassport-localを利用する。

ソースコードは長くなるため割愛するが、その分サーバー側で柔軟な調整ができる。

  • ユーザーデータのシリアライズ・デシリアライズ方法の調整
  • sessionの保存方法の調整(たとえばDBにsession情報を保存する等)
  • リダイレクトURLの柔軟な調整(条件分岐等)

サーバー側のバグはクリティカルであることが多い。
「なんでも屋」すぎるライブラリには頼らず、小さくて優秀なライブラリを複数利用しつつ細かい調整は自前で実装するのがベストプラクティスだと筆者は考える。

react-ssrの実装サンプル

examples/with-jsx-emotion

スクリーンショット 2019-12-01 5.25.35.png

examples/with-jsx-material-ui

スクリーンショット 2019-12-01 5.27.23.png

examples/with-jsx-semantic-ui

スクリーンショット 2019-12-01 5.29.42.png

examples/styled-components

スクリーンショット 2019-12-01 5.31.16.png

さいごに

最終的にreact-ssrの宣伝っぽくなってしまってすみません。

ですが古き良きビューエンジンの形でReactを利用したいと思ったことはありませんか?

小規模開発でもわざわざAPIサーバーとクライアントを分けなければならない煩雑さを感じたことはありませんか?

そういった疑問を感じたことがある方にとって、少しでも可能性が広がる記事になれば幸いです。

筆者の稚拙な技術記事であるため、間違い等はコメントにてご指摘ください。

ここまで読んでくださりありがとうございました。

ちなみに私はベテルギウスが好きです。(一番高いところにあるから。)

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

確かな力が身につくJavaScript超入門

この記事は、技術書「確かな力が身につくJavaScript超入門」を読みつつ、JSの基礎を学んだ学習記録です。

確かな力が身につくJavaScript超入門

confirm

if(window.confirm('ゲームスタート')) {
  console.log('ゲームを開始します');
} else {
  console.log('ゲームを終了します');
}

confirmはユーザーの選択によって返す内容が変わる
OK:true
キャンセル:false

prompt

let answer = window.prompt('ヘルプを見ますか?');
console.log(answer);

promptに入力された値をそのまま返す

「===」と「==」

const answer = window.prompt('ヘルプを見ますか?');
if(answer === 'yes') {
  window.alert('タップしてジャンプします');
}

if(answer === 'yes')この時にtrue返し、ifの内容が実行される。
==と===の違い
== 文字列が同じであればtrue
=== 文字列、データ型まで全て一致すればtrue

const number = Math.floor(Math.random() * 6);
const answer = parseInt(window.prompt('数あてゲーム 0~5'));

let message;
if(answer === number) {
  message = 'あたり';
} else if(answer < number) {
  message = '残念でしたもっと小さい';
} else {
  message = '0-5の数字を入力してください。';
}

Math.random()でランダムな整数を生成する

parseInt()文字列を整数に変換する

時間帯によって異なる処理をする

const hour = new Date().getHours();

if(hour >= 19 && hour < 21) {
// 19以上「かつ」21未満
  window.alert('お弁当30%OFF');
} else if(hour === 9 || hour === 15) {
// 9「もしくは」15
  window.alert('お弁当買ったら1個おまけ') ;
} else {
  window.alert('お弁当はいかがですか');
}

ページを開いた時の時刻を取得する
.getHours()で、24時間時計で取得し、hourに代入
hourには、0~23までの整数が代入されている

繰り返し処理(while)

let i = 1;
while(i <= 10) {
  console.log(i);
  i +=1 ;
  // i = 1 +1;と同じ意味
}

回数が決まってない繰り返し

let enemy = 100;
let count = 0;
//while処理の中で、countを1ずつ足していく

window.alert('戦闘スタート');

while(enemy > 0 ) {
  const attack = Math.floor(Math.random() * 30) + 1;
  console.log('モンスターに' + attack + 'のダメージ!');
  enemy -= attack;
  count += 1;
}
console.log( count + '回でモンスターを倒した');
/* 
結果
モンスターに17のダメージ!
モンスターに3のダメージ!
モンスターに27のダメージ!
モンスターに8のダメージ!
モンスターに30のダメージ!
モンスターに26のダメージ!
モンスターを倒した
8回でモンスターを倒した
*/

while(enemy > 0 )
変数enemyが0より大きい時trueになる
変数attackの値がランダムに決まることから、繰り返し処理がいつ終わるかわからない

while文の中のconstについて

繰り返し処理の度に値が変わるのに、なぜconstでもいけるの?

変数attackは、whileの中で定義している
そのため、whileの中でのみ値を参照できる

whileの繰り返しの場合、
1回の繰り返しが終わると、変数や定数の値は消去される
そのため、繰り返すたびに定義し直すことができる

変数の有効範囲を狭くすることを意識すれば、エラーを防ぐことができる

function(関数)

function total(price) {
  const tax = 0.1;
  return price + price * tax;
}
console.log('コーヒーメーカーの値段は' + total(8000) + '円です。');

functionとは

  • パラメータを受けとり
  • 加工をして
  • 結果を呼び出して値を返す

関数名();で呼び出し
()内には、ファンクションに要求するパラメータを入力する

基本の形
function 関数名(パラメータ) {
  処理内容
}

アロー関数で書いた場合

//アロー関数の場合
const price = 300;
const tax = 0.1;
const coffee = ({price,tax}) => {
  return (
    'コーヒーの値段は' +
    (price + price * tax) +
    '円です。'
    );
}
  const res = coffee({price,tax});
  // console.log(res);
document.getElementById('output').textContent = res;
//結果:
//<div id="output">コーヒーの値段は330円です。</div>


//アロー関数の場合
const price = 300;
const tax = 0.1;
const coffee = ({price,tax}) => {
  return (
    'コーヒーの値段は' +
    (price + price * tax) +
    '円です。'
    );
}
  const res = coffee({price,tax});
  // console.log(res);
document.getElementById('output').textContent = res;
//結果:
//<div id="output">コーヒーの値段は330円です。</div>

FizzBuzz

/*
3でも5でも割り切れる場合は、「FizzBuzz」をリターンとして返す
それ以外で3で割り切れる場合は「Fizz」をリターンとして返す
それ以外で、5で割り切れる場合は「Buzz」をリターンとして返す
それ以外で(3でも5でも割り切れない場合)は渡された数値をそのままリターンとして返す
*/

function fizzbuzz(num) {
  if(num % 3 === 0 && num % 5 === 0) {
    return 'fizzbuzz';
  } else if(num % 3 === 0) {
    return 'fizz';
  } else if(num % 5 === 0 ) {
    return 'buzz';
  } else {
    return num;
  }
}
console.log(fizzbuzz(15));

繰り返し処理で30までFizzBuzzする

let i = 1;
while(i <= 30) {
  console.log(fizzbuzz(i));
  i += 1;
}

配列

let todo = [
  'デザインカンプ作成',
  'JavaScript学習',
  'Webサイト作成',
  'ブログ執筆'
];
console.log(todo);
// 結果:
// ["デザインカンプ作成", "JavaScript学習", "Webサイト作成", "ブログ執筆"]

配列の作り方

let 変数名 = [];

配列からデータを読み取る

配列名[インデックス番号];

インデックス番号は0から始まる

配列の各項目を全て読み取る

let todo = [
  'デザインカンプ作成',
  'JavaScript学習',
  'Webサイト作成',
  'ブログ執筆'
];
for(let item of todo) {
console.log(item);
}
// 結果:
// デザインカンプ作成
// JavaScript学習
// Webサイト作成
// ブログ執筆

例で定義している変数名itemに、
繰り返し処理が行われる度に配列のデータが一つ代入されていく。

基本の形
for(let 変数名 of 読み取りたい配列の変数名) {
  処理内容(consoleとか)
}

配列で使うメソッド

  • .pop() - 配列の最後のデータを削除する
  • .push(データ) -配列の最後にデータを追加する
  • .shift() -  配列の最初のデータを削除する
  • .unshift(データ1,データ2,...) - 配列の最初にデータを追加する

配列をHTMLタグで囲む

for(let item of todo) {
  const li = `<li>${item}</li>`;
  console.log(li);
}
// 結果:
// <li>デザインカンプ作成</li>
// <li>JavaScript学習</li>
// <li>Webサイト作成</li>
// <li>ブログ執筆</li>

for ...of の中で、配列todoの項目を、<li></li>で囲んだ文字列を作成して,定数liに代入しています。

``(バックティック)で囲んだ文字列のことを、テンプレート文字列という

テンプレート文字列は、「`」で囲んだ部分のテキストを文字列データにするが、変数を埋め込めるという特徴がある

変数を埋め込むには、${変数名}というように入力する。

const li = '<li>' + item + '</li>';
//これでもいいが、

const li = `<li>${item}</li>`;
//こっちの方が入力しやすいし、パッと見てもわかりやすい。

const li = `<li>
             ${item}
            </li>`;
//こっちだと改行もOK

配列をHTMLに出力する

<h1>やることリスト</h1>
<ul id="list">

</ul>
//配列の各項目を全て読み取る
//変数itemに格納
for(let item of todo) {
  //配列の内容を<li>タグで囲み、変数liに格納
  const li = `<li>${item}</li>`;
  //htmlに出力する
  document.getElementById('list').insertAdjacentHTML('beforeend',li);
}

ElementオブジェクトのtextContentプロパティでは、テキストを挿入するか書き換えるしかできず、
HTMLタグを挿入することはできない。

そこで使用するのが、insertAdjacentHTMLメソッド

getElementByIdで挿入先の要素を指定し、
insertAdjacentHTMLメソッドを使ってHTMLを挿入する。

insertAdjacentHTMLメソッドの書き方

取得した要素.insertAdjacentHTML('挿入する場所の指定', '挿入する要素')

挿入する場所の指定

  • beforebegin - 取得した要素の前
  • afterbegin - 取得した要素の後
  • beforeend - 取得した要素の子要素として挿入、すでに子要素があればその前に挿入
  • afterend - 取得した要素の子要素として挿入、すでに子要素があればその後に挿入

今回の例で表すとこんな感じ

beforebegin

<ul id="list">
afterbegin

<li>リスト</li>

beforeend

</ul>
afterend

オブジェクト

let jsbook = {title: 'JavaScript入門', price:2500, stock:3};
console.log(jsbook);
// 結果:
// {title: "JavaScript入門", price: 2500, stock: 3}
// price: 2500
// stock: 3
// title: "JavaScript入門"
// __proto__: Object

複数のプロパティを持つデータの集まりで、
各種データを一まとまりにして一つの変数として扱えるデータのこと。

オブジェクトの作成

let 変数名 = {};

プロパティが複数ある場合
let 変数名 = {プロパティ:データ, プロパティ:データ, プロパティ:データ,...};

プロパティは、データのセットを差す

例えば、title:'JavaScript入門'であれば

  • title:'JavaScript入門' - プロパティ
  • title - プロパティ名
  • JavaScript入門 - データ(値)

プロパティを読み取る

オブジェクト名.プロパティ名
オブジェクト名.['プロパティ名']

プロパティのデータを書き換える

オブジェクト名.プロパティ名 = 新しいデータ
オブジェクト名.['プロパティ名'] = 新しいデータ

オブジェクトのプロパティを全て読み取る

for(let p in jsbook) {
  console.log(p + '=' + jsbook[p]);
}
// 結果:
// title=JavaScript入門
// price=2500
// stock=3
基本の形
for(let 変数名 in オブジェクト名){
  処理内容
}

変数名 にはプロパティ名が挿入される
プロパティ名を読み取るには変数名を指定すればいい

プロパティのデータを読み取るには、
オブジェクト名[変数名]とすることで読み取れる

オブジェクトをHTMLに出力する

<table>
  <tr>
    <td id="title"></td>
    <td id="price"></td>
    <td id="stock"></td>
  </tr>
</table>
let jsbook = {title: 'JavaScript入門', price:2500, stock:3};
for(let p in jsbook) {
  document.getElementById('title').textContent = jsbook.title;
  document.getElementById('price').textContent = jsbook.price + '';
  document.getElementById('stock').textContent = jsbook.stock;
}

配列とオブジェクト

どちらも複数のデータを扱う。
使い分けはどうするのか?

Excelの表に置き換えてイメージしてみる。

  • 縦に並べた方が管理しやすそうなデータは配列
  • 横に広げた方が管理しやすそうならオブジェクト

フォームの内容を読み取る

<form action="#" id="form">
  <input type="text" name="word">
  <input type="submit" value="検索">
  <p id="output"></p>
</form>

/*
formを取得、
onsubmitイベントプロパティにファンクションを代入する

*/
document.getElementById('form').onsubmit = function(event) {
  event.preventDefault();
//preventDefaultでHTMLの基本動作をキャンセル
// (ページ自動読み込み)
  const search = document.getElementById('form').word.value;
  // テキストフィールドの内容を取得して、searchに代入する
  // <form>要素を取得
  // 読み取りたいフォーム部品を、nam属性で指定する。
  // フォーム部品に入力された内容は、valueプロパティに保存されているため、valueをつける。

  document.getElementById('output').textContent = `${search}の検索中...`;
};

formは、submitボタンがクリックされたら、action属性のURLに入力内容が送信される。

日時を表示させる(Date)

const now = new Date();
/*
Dateオブジェクトの初期化
new はオブジェクトを初期化する時のキーワード
Dateオブジェクトは、現在日時を記憶した状態で初期化される
*/
const year = now.getFullYear();
const month = now.getMonth();
const date = now.getDate();
const hour = now.getHours();
const min = now.getMinutes();
const sec = now.getSeconds();

const output = `${year}/${month + 1}/${date} ${hour}:${min}:${sec}`;
document.getElementById('time').textContent = output;

それぞれ取得して変数に代入、出力する。

注意点としては、
getMonthは、「実際の月-1」の数字が取得される。
つまり、現在が1月なら、12月、2月なら1月が取得されるため、数字に1を足している。

Mathオブジェクト

document.getElementById('pi').textContent = Math.PI;
//結果:
//3.141592653589793

document.getElementById('floor').textContent = Math.floor(Math.PI);
//結果:
//3

Math.PI円周率を出力

Math.floor(数値)小数点以下を切り捨てる

参考リンク

確かな力が身につくJavaScript超入門

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

CodeMirror 6 を触ってみよう

APIが充実しててカスタマイズしやすいCodeMirrorというWysiwygエディタの開発中のバージョン6が、スマホでもいい感じに動くことを目指してるし、共同編集の機能を実装しやすそうな気がしたのでちょっと試してみました!

出来上がるもの

image.png

(エディタのとこを3回くらいクリックすると編集できるよ!)

Hello World!

✍️ まずはエディタを設置する。CodeMirror6はコアには最低限の機能だけを備えていて、必要な昨日はExtensionを追加することで実現する。
✍️ そのため、ソースコードが小さくなるし(今は全部が一つのリポジトリに入ってるから小さくはない?)、必要な処理だけするから速い

このままだと文字が書けるだけ。

import { EditorView } from "@codemirror/next/view";
import { EditorState } from "@codemirror/next/state";

let myView = new EditorView({
  state: EditorState.create({
    doc: "hello world!",
    extensions: []
  })
});

document.body.appendChild(myView.dom);

履歴機能を追加

✍️ 拡張機能を追加する時はこう書く
✍️ 履歴機能とキーマップのExtensionを追加
✍️ 実装方法を学ぶ時はデモを見るのがわかりやすい
✍️ baseKeymapは日本語の変換時の挙動がちょっとおかしい

これで、元に戻す・やり直すができるようになった。

import {
  history,
  redo,
  redoSelection,
  undo,
  undoSelection
} from "@codemirror/next/history";
import { keymap } from "@codemirror/next/keymap";
import { baseKeymap } from "@codemirror/next/commands";
import { EditorView } from "@codemirror/next/view";
import { EditorState } from "@codemirror/next/state";

let myView = new EditorView({
  state: EditorState.create({
    doc: "hello world!",
    extensions: [
      history(),
      keymap({
        "Mod-z": undo,
        "Mod-Shift-z": redo,
        "Mod-u": view => undoSelection(view) || true,
        "Mod-Shift-u": redoSelection,
        "Ctrl-y": undefined
      }),
      keymap(baseKeymap),
    ]
  })
});

document.body.appendChild(myView.dom);

行番号を設置

✍️ 簡単(gutterのlineNumbersを追加するだけ)

行番号を設置したら見た目が変わって嬉しい。

import {
  history,
  redo,
  redoSelection,
  undo,
  undoSelection
} from "@codemirror/next/history";
import { keymap } from "@codemirror/next/keymap";
import { baseKeymap } from "@codemirror/next/commands";
import { lineNumbers} from '@codemirror/next/gutter'
import { EditorView } from "@codemirror/next/view";
import { EditorState } from "@codemirror/next/state";

let myView = new EditorView({
  state: EditorState.create({
    doc: "hello world!",
    extensions: [
      history(),
      keymap({
        "Mod-z": undo,
        "Mod-Shift-z": redo,
        "Mod-u": view => undoSelection(view) || true,
        "Mod-Shift-u": redoSelection,
        "Ctrl-y": undefined
      }),
      keymap(baseKeymap),
      lineNumbers()
    ]
  })
});

document.body.appendChild(myView.dom);

image.png

シンタックスハイライトを追加

✍️ jsとhtmlとcssは初めから用意されてる
✍️ typescriptを使うとモジュールを見つけやすい

シンタックスハイライトが入るとエディタ感が増して良いよね。

import {
  history,
  redo,
  redoSelection,
  undo,
  undoSelection
} from "@codemirror/next/history";
import { html } from "@codemirror/next/lang-html";
import { defaultHighlighter } from "@codemirror/next/highlight";
import { keymap } from "@codemirror/next/keymap";
import { baseKeymap } from "@codemirror/next/commands";
import { lineNumbers } from "@codemirror/next/gutter";
import { EditorView } from "@codemirror/next/view";
import { EditorState } from "@codemirror/next/state";

let myView = new EditorView({
  state: EditorState.create({
    doc: "hello world!",
    extensions: [
      history(),
      keymap({
        "Mod-z": undo,
        "Mod-Shift-z": redo,
        "Mod-u": view => undoSelection(view) || true,
        "Mod-Shift-u": redoSelection,
        "Ctrl-y": undefined
      }),
      keymap(baseKeymap),
      lineNumbers(),
      html(),
      defaultHighlighter
    ]
  })
});

document.body.appendChild(myView.dom);

終わり

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

JavaScriptのビルトインオブジェクトを拡張する

例えば、配列を拡張してrubyのようにfirstで1件目のエントリーを取得できるようにするにはどうすればよいか、
といったことを紹介します。

const arr = [1, 10, 100, 200]
arr.first // -> 1

拡張する方法を紹介する記事ですので、拡張している内容は特に重要ではありません。

注意

グローバルなものを変更してしまう内容を紹介しています。

一見便利に思えても、名前が衝突する可能性があったりしますので、実用するべきかどうかは慎重に検討してください。

(僕は個人の実装物でしかやりません)

そして、実装する内容も、本当に汎用的に使えるものに絞ったほうがよいです。

Mathオブジェクトに関数を拡張

この場合、単に好きなキー名に関数をアサインしてしまって大丈夫です。

Math.sum = (...numbers) => {
  return numbers.reduce((prev, current) => prev + current)
}

Math.sum(1, 10, 100) // -> 111

↑与えられた可変長引数の数値を全て合計して返す関数です。可変長引数やreduceの解説は省略。

Math.average = (...numbers) => {
  return Math.sum(...numbers) / numbers.length
}

Math.average(1, 10, 100) // -> 37

↑与えられた可変長引数の数値の平均を返す関数。さっきのsumを再利用。

Math.randomInt = (min, max) => {
  return Math.floor(Math.random() * (max + 1 - min)) + min
}

Math.randomInt(1, 100) // -> 55

↑第一引数〜第二引数の範囲で、ランダムに整数を返す関数。

Arrayのprototypeにgetter/setterを定義

prototypeを拡張する際は、definePropertyを使ってください。

Object.defineProperty(Array.prototype, 'first', {
  get () {
    return this[0]
  },
  set (value) {
    this[0] = value
  }
})

const arr = [1, 10, 100, 200]
arr.first // -> 1
arr.first = 5
arr.first // -> 5

配列の1件目にアクセスできるプロパティを実装してみました。

Arrayのprototypeに関数を拡張

同じくdefinePropertyを使い、valueに定義します。

Object.defineProperty(Array.prototype, 'random', {
  value () {
    return this[Math.randomInt(0, this.length - 1)]
  }
})

const arr = [1, 10, 100, 200]
arr.random() // -> 10
arr.random() // -> 200

↑配列内から一つのエントリーをランダムに取得します。さっきのrandomIntを再利用。

Object.defineProperty(Array.prototype, 'clear', {
  value () {
    return this.length = 0
  }
})

const arr = [1, 10, 100, 200]
arr.clear()
arr // -> []

↑配列を空にする関数です。

(配列はarr.length = 0で空にできますが、仕様を知っていないと何をしているか意味がわかりません。arr = []なら分かりやすいけど、別のオブジェクトになってしまうのでやっていることが全然変わってきます。なので明確な名前をつけて関数化しました。)

プリミティブ型のラッパーオブジェクトも拡張

手順は同じです。

Object.defineProperty(Number.prototype, 'half', {
  get () {
    return this / 2
  }
})
const num = 1280
console.log(num) // -> 1280
console.log(num.half) // -> 640
console.log(num.half.half) // -> 320
console.log(num.half.half.half) // -> 160

数値を半分にします。

補足

defineProperty

さきほどのdefinePropertyの第三引数で渡した記述子には、get, set, value以外にもありますので、詳しくは以下をご覧ください。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Objectのprototype

もちろんMathArray以外でも同じ方法で拡張できますが、Objectのprototypeは変更するとあらゆるものに影響してしまうのでやめたほうが良いです。

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

GoogleカレンダーとSlackステータスを連携する【GAS】

この記事は『Slack Advent Calendar 2019』の15日目の記事です。

作りたいもの

Googleカレンダーに予め予定を入れておき、予定の時間になると、Slackのステータスが切り替わるもの。

は、もう既にあるようですね...

ただ、これを使用するには、Googleカレンダーから出席依頼を「承諾」することが必要で、ステータスも「In a meeeting」のみ。もっと他のこと(例えば有給休暇中とか)でステータスを切り替えたくなったり、会社がGoogleカレンダーを使っていないとなると、どうしても自分で作る必要が出てくる。

そこで自分でGoogleカレンダーとSlackステータスを連携させるものを作成して、実際の稼働までを書いてみる。

構成

今回はSlack Web APIとGoogle Calendarを、Google App Script(GAS)を使って連携させる。図にすると理想はこんな感じ。
fig1.png
このようなものを作っている方は結構いらっしゃったが、POSTで投げている方はあまりいなかったので、今回はPOSTで投げてみる。

GASコード

SlackStatusWriter.gs
/**
 Googleカレンダーから取得した現在の予定をSlackステータス用に整形して返却する。
**/
function createStatusText(event) {
  // 整形した開始時刻・終了時刻
  var start = event.getStartTime().getHours() + ":" + ("00" + event.getStartTime().getMinutes()).slice(-2);
  var end = event.getEndTime().getHours() + ":" + ("00" + event.getEndTime().getMinutes()).slice(-2);
  // ステータステキスト
  var text = event.getTitle() + "(" + start + "" + end + ")";

  // イベントがある時のステータス
  var event_status = {
    "profile": JSON.stringify({
      "status_text": text,
      "status_emoji": ":spiral_calendar_pad:"
    })
  };

  return event_status;

}

/**
 作成したステータスをSlack Web API経由でプロフィールに反映させる。
**/
function postSlackStatus(status) {
  // アクセス情報
  const TOKEN = "(取得したSlack API トークン)";
  const URL = "https://slack.com/api/users.profile.set";

  // HTTPヘッダー
  const headers = {
    "Authorization" : "Bearer " + TOKEN
  };

  //POSTデータ
  var option = {
    "Content-Type": "application/json",
    "headers": headers,
    "method": "POST",
    "payload": status
  };

  var fetch = UrlFetchApp.fetch(URL, option);

}

/**
 カレンダーから今日の予定を取得し、必要であればSlackステータスを更新する。
**/
function main() {
  // カレンダーID
  const ID = "(Slackステータスに使うGoogleカレンダーID)";
  // 今日の日付
  var date = new Date();
  // カレンダーから今日の予定を取得
  var calendar = CalendarApp.getCalendarById(ID);
  var events = calendar.getEventsForDay(date);

  // 今日のイベントがない場合は何もしない
  if (events.length !== 0) {

    // イベントがないときのステータス
    var set_status = {
      "profile": JSON.stringify({
        "status_text": "暇暇の暇",
        "status_emoji": ":dancer:"
      })
    };

    // 今日の予定をすべて調査
    for (var i in events){
      // 今が予定の開始時刻以降で終了時刻以前なら今はその予定の最中 -> ステータス変更
      if (events[i].getStartTime() <= date && events[i].getEndTime() >= date) {
        set_status = createStatusText(events[i]);
        break;
      }
    }

    postSlackStatus(set_status);

  }
}

コード内の

  • (取得した取得したSlack API トークン)
  • (Slackステータスに使うGoogleカレンダーID)

は、別途自分で取得する必要がある。

また、時間を取得する際、分を

("00" + getMinutes()).slice(-2);

としておいたほうがいい。でないと、例えば17:00とかだと、「17:0」になってしまう。間違ってはないけど違和感がある…。

アクセス情報の取得方法

Slack API トークン

  1. Legacy tokensへアクセスする。
    • なぜか見つけるのが大変なので、よく使うならブックマークしてもいいかも
  2. Slackワークスペースにサインインしていれば、自分のワークスペースが表示される。まだトークンを作っていなければ、「Create Token」をクリックすれば作成できるので、これをコピーする。 fig2.png

GoogleカレンダーID

  1. Googleカレンダーにアクセスし、Slackのステータスに使いたいカレンダーの「設定と共有」を開く。
    fig3.png

  2. 「カレンダーの統合」を開き(もしくは下にスクロール)、カレンダーIDをコピーする。
    fig4.png

GASコードを動かす

  1. G Suite Developer Hubにアクセスし、「Start Scripting」をクリックする。
  2. Googleアカウントでログインすると、管理画面に飛ばされるので、「新しいプロジェクト」で新規作成する。
  3. コードを実装し、保存する。

時間周期で稼働させる前に、一度実行ボタンから実行しておく。というのも、初回のみGoogleカレンダーへのアクセスの確認が必要なため。

実行時は、最初にどのメソッドを動かすかを指定して、実行する。
fig5.png

時間周期で稼働させる

  1. エディタ画面から「編集→現在のプロジェクトのトリガー」をクリックする。
  2. 「トリガーの追加」をクリックする。
  3. 以下のように設定する。

    項目 設定値
    1. 実行する関数を選択 main
    2. 実行するデプロイを選択 Head(しかないはず)
    3. イベントのソースを選択 時間主導型
    4. 時間ベースのトリガーのタイプを選択 分ベースのタイマー
    5. 時間の間隔を選択(分) 1分おき

    4.と5.は「稼働させる時間の間隔」なので、ここはお好みで。横の「エラー通知設定」も好きなタイミングで受け取るようにすれば良い。

  4. 「保存」をクリックする。

これで指定した時間周期でGASが実行される。

お手並み拝見

こんな予定を立ててみる。
fig6.png
(この予定はフィクションです。)

予定開始前

fig7.png

予定開始時刻

さぁ、嘘の課会議が始まりました。
fig8.png
うん、いい感じ。

予定終了時刻

fig9.png
完璧ですな…

GASの注意点

GASも無料で使えるだけあって色々制限がある。例えば今回のものだと、URL fetchの呼び出しは、1日で20,000回まで。また、トリガーの1日の総実行時間は90分以内でなければならない。今回のスクリプトは1分に1回動かしているから、1回の実行時間は3.75秒以内でないといけない。

詳しい制限内容はこちら

とはいえ、1分に1回URL Fetchしたところで1日合計1,440回だし、今回のスクリプトは1回あたりの実行時間が長くても0.8秒ほどなので、割と制限はゆるゆるなのかもしれない。もう少し複雑なものになると実行時間だけ気をつける必要があるかも…。

ちなみに実行時間は、エディタ画面から「表示→実行トランスクリプト」で確認できる。

最後に

普通に楽しかった。もっと早くからGASを始めておけばよかったと思った。最後のほうは少しGASの話になってしまったけど、Slackへのリクエストがすごく簡単だったので、Slackを使っている方は合わせて覚えたほうがいいものだと思った。

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

【未完成】ドットインストールでHTML/CSS,JavaScriptを学習してみた所感と学習内容

はじめに

フロントエンドに興味がある新人技術者がプログラミング学習サービス「ドットインストール」で学習してみた感想と、そこで得た知識をアウトプットしてみました。
実施したレッスンは以下。
 ・HTML/CSSの学習環境を整えよう macOS編 (全5回)
 ・はじめてのHTML (全14回)
 ・はじめてのCSS (全15回)
 ・はじめてのJavaScript (全11回)

活用してみた所感

今回「ドットインストール」を初めて活用して感じた所感をあげていきます。

①動画なので操作がわかりやすい

書籍で学習する際との決定的な違いです。
書籍だと省略されがちな操作方法を動画で見ながら確認できるので、「説明書いてあるけど難しすぎてどう操作していいか分からない」といった詰み状態にならないのが最大のメリットだと思いました。

②手軽に学習できるため、学習がストップしない

動画1本の時間が1〜3分くらいと短めなので、集中力が切れることなくサクサク進められるのが特徴です。
ゲームのステージをこなしていく感覚で実際に手を使いながらどんどん学習できます。

③OSごとの解説がある

環境設定やショートカットキーなど、Mac環境とWindows環境で異なる部分の解説をしっかりとしてくれているため、誰でもスムーズに学習に取り組めます。
個人的に、覚えておいて役立つショートカットキーを動画内で説明してくれるのはありがたかったです。

④自分に必要なレッスンが分かりやすい

目的別でやるべきレッスンがまとめられているため、初心者にありがちな「何から手をつけていいか分からない問題」が起こりづらいのではないでしょうか。
動画内でも事前に受けておいた方が良いレッスンを教えてくれるので、迷いなく学習に入れます。

実際に学習した内容

以下、実施したレッスンのアウトプットです。
なぜこのレッスンを選んだのかも記載していきます。

① HTML/CSSの学習環境を整えよう [macOS編]

もともとJavaScriptを勉強しよう!と思い、後に出てくる「はじめてのJavaScript (全11回)」を始めたのですが、動画冒頭で「HTML/CSS」の学習が大前提と説明があったため、HTML/CSSの学習から始めることに。
エディター環境が整っていなかったため、このレッスンをまず実施しました。
MacOS編とWindows編があり、とても親切。
レッスン内容は以下。
 ・Chromeブラウザの導入方法
 ・VS Codeの導入と設定

②はじめてのHTML

①の経緯で学習を開始。
実際に手を動かしながら簡単なプロフィールサイトを作成していきます。
以下、レッスンのアウトプット。

■タグでテキストを意味付けしていくことを”マークアップする”という

index.html
<h1>ダイエット</h1>
<p>健康的なダイエット法を紹介します。</p>

■タグの種類

<h1></h1> : 見出しタグ。数字が小さいほど大きな見出しになる。
<p></p> : 段落タグ。
<img src="画像のファイル名"> : 画像を表示するためのタグ。マークアップする文章がないため、終了タグは不要。

参考になったタグ入れの子ルール

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

写経のすヽめ

創作は真似事から始まる

わりとよく聞く言葉だとは思いますが、世の中に存在する全ての作られたものは模倣から始まっていると思います。完全オリジナルなんてないのです。多分。
幼年期に絵を書いていた人は恐らくドラえもんやアンパンマンの落書きから始まって、そこから色んな絵を描き、成長と共に色んなアレンジや技法を知り、自分のものにしたり。
小さい頃ピアノを習ってた人だって、いきなりオリジナル曲を作れる人は稀有なんではないでしょうか。恐らく。

Let's 写経

例えばjavascriptを学習するとき、分厚い教本を買って小難しいカタカナなどを乗り越えながら手を動かしつつ学習する人もいると思うのですが(私がそうです)、個人的には自分に向いてないと思いました。

まず構文がわからん。
「え、なにこの書き方?」
みたいなのを見つけても説明がなかったりします。
ネットで調べようにも調べ方がわからなかったり。

知識を吸収しようにも教本だけではもどかしく、「え、なにこれ」が発生した場合解決するまで次のステップへ進めない(それができる人もいるのでしょうけども)。

というわけで、私みたいにじゃあ教本に頼りっぱなしではなく自分でコードを書きまくって覚えちまおうって人に向けたい話が今回伝えたいことです。Let's 写経。

どうするの?

GitHubでもなんでもいいので、オープンソースのコードを拾ってきます。
難易度はばらつきがあると思いますが、自分ならこれを解読しながら模写できそうと思えるのがいいかも。
思えるものがなかったらとりあえず片っ端からやっていきましょう。

例えば

var utils = {
    wrap: function(el, className) {
        if (!el) {
            return;
        }

        var wrapper = document.createElement('div');
        wrapper.className = className;
        el.parentNode.insertBefore(wrapper, el);
        el.parentNode.removeChild(el);
        wrapper.appendChild(el);
    },

    addClass: function(el, className) {
        if (!el) {
            return;
        }

        if (el.classList) {
            el.classList.add(className);
        } else {
            el.className += ' ' + className;
        }
    },

    removeClass: function(el, className) {
        if (!el) {
            return;
        }

        if (el.classList) {
            el.classList.remove(className);
        } else {
            el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
        }
    },

    hasClass: function(el, className) {
        if (el.classList) {
            return el.classList.contains(className);
        } else {
            return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
        }

        return false;
    },

    // ex Transform
    // ex TransitionTimingFunction
    setVendor: function(el, property, value) {
        if (!el) {
            return;
        }

        el.style[property.charAt(0).toLowerCase() + property.slice(1)] = value;
        el.style['webkit' + property] = value;
        el.style['moz' + property] = value;
        el.style['ms' + property] = value;
        el.style['o' + property] = value;
    },

    trigger: function(el, event, detail = null) {
        if (!el) {
            return;
        }

        let customEvent = new CustomEvent(event, {
            detail: detail
        });
        el.dispatchEvent(customEvent);
    },

    Listener: {
        uid: 0
    },
    on: function(el, events, fn) {
        if (!el) {
            return;
        }

        events.split(' ').forEach(event => {
            var _id = el.getAttribute('lg-event-uid') || '';
            utils.Listener.uid++;
            _id += '&' + utils.Listener.uid;
            el.setAttribute('lg-event-uid', _id);
            utils.Listener[event + utils.Listener.uid] = fn;
            el.addEventListener(event.split('.')[0], fn, false);
        });
    },

    off: function(el, event) {
        if (!el) {
            return;
        }

        var _id = el.getAttribute('lg-event-uid');
        if (_id) {
            _id = _id.split('&');
            for (var i = 0; i < _id.length; i++) {
                if (_id[i]) {
                    var _event = event + _id[i];
                    if (_event.substring(0, 1) === '.') {
                        for (var key in utils.Listener) {
                            if (utils.Listener.hasOwnProperty(key)) {
                                if (key.split('.').indexOf(_event.split('.')[1]) > -1) {
                                    el.removeEventListener(key.split('.')[0], utils.Listener[key]);
                                    el.setAttribute('lg-event-uid', el.getAttribute('lg-event-uid').replace('&' + _id[i], ''));
                                    delete utils.Listener[key];
                                }
                            }
                        }
                    } else {
                        el.removeEventListener(_event.split('.')[0], utils.Listener[_event]);
                        el.setAttribute('lg-event-uid', el.getAttribute('lg-event-uid').replace('&' + _id[i], ''));
                        delete utils.Listener[_event];
                    }
                }
            }
        }
    },
};

↑こういうコードを

var utils = {//utilsオブジェクト作成
  wrap: function(el,className) { //wrap作成
    if (!el) {//elがtrueでなかった場合
      return; //undefinedを返す
    }

    var wrapper = document.createElement('div'); //divタグを生成
    wrapper.className = className; //生成されたdivタグのclassNameが引数のclassNameとなる
    el.parentNode.insertBefore(wrapper,el);//wrapper(div)をelの前に挿入
    el.parentNOde.removeChild(el);//elを削除
    wrapper.appendChild(el);//wrapperの子要素としてelを追加
  },

  addClass: function(el, className) { //class追加
    if (!el) {//elがtrueでなかった場合
      return;//undefinedを返す
    }

    if (el.classList) {// el.classListがtrueならば
      el.classList.add(className);//elのclassにclassNameを追加
    } else {
      el.className += ' ' + className;//それ以外は「"elのクラス名"&nbsp;className」という文字列を生成
    }
  },

  removeClass: function(el, className) { //class削除
    if (!el) {//elがtrueでなかった場合
      return;//undefinedを返す
    }

    if (el.classList) {//el.classListがtrueならば
      el.classList.remove(className); //elのclassからclassNameを削除
    } else {//それ以外のときはマッチする全てのクラス名を空白に
      el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');//それ以外のときはマッチする全てのクラス名を空白に
    }
  },

  hasClass: function(el, className) {//class判定
    if (el.classList) {//el.classListがtrueならば
      return el.classList.contains(className); //classListの中身をマッチさせた結果を返す
    } else { //それ以外の時はclassNameをマッチさせた結果を返す
      return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
    }

    return false;//falseを返す
  },

// 動きを与える
// transitionでのタイミング操作の関数
  setVendor: function(el,property,value) {//ベンダープレフィックス付与
    if(!el) {//elがtrueでなかった場合
      return;//undefinedを返す
    }

    el.style[property.charAt(0).toLowerCase() + property.slice(1)] = value;//頭文字が大文字のものを小文字に
    el.style['webkit' + property] = value; //webkit付与
    el.style['moz' + property] = value; //moz付与
    el.style['ms' + property] = value; //ms付与
    el.style['o' + property] = value; //o付与
  },

  trigger: function(el, event ,detail = null) {//event発火
    if(!el) {
      return;
    }

    let customEvent = new CustomEvent(event, {
      detail: detail
    });
    el.dispatchEvent(customEvent);//CustomEventを発生させる
  },

  Listener: {
    uid: 0
  },
  on: function(el, events, fn) {
    if(!el) {
      return;
    }

    events.split(' ').forEach(event => {//eventsを配列に置換し、foreachで繰り返し処理
      var _id = el.getAttribure('lg-event-uid') || '';//elの属性にlg-event-uidが含まれていれば_idに代入、それ以外は空の値を代入
      utils.Listenr.uid++; //uidに1を追加していく
      _id += utils.Listener.uid;// _idにuidを追加していく
      el.setAttribute('lg-event-uid',_id);// elのlg-event-uid属性に_idの値を付与
      utils.Listener[event + utils.Listener.uid] = fn; //utils.Listenerオブジェクトにevent+uidの数値というプロパティを追加し、値をfnにする
      el.addEventListener(event.split('.')[0],fn,false);//eventsの1番目のイベントがハンドラとなり、fnを実行。そのイベントは伝播する。
    });
  },

  off: function(el, event) {
    if (!el) {
      return;
    }

     var _id = el.getAttribure('lg-event-uid');
     if (_id) {//_idがtureだったら
       _id = _id.split('&');//_idを&単位で分割し配列にする
       for (var i = 0; i < _id.length; i++) { //_idの個数分だけiの数値を増加
         if (_id[i]) {//_id配列がtrueだったら
           var _event = event + _id[i];//_eventにeventと_id配列の数値を足したものを代入
           if (_event.substring(0, 1) === '.') {//_event文字列の頭文字が.だったら
             for (var key in utils.Listener) { //utils.Listenerの値を反復
               if (utils.Listener.hasOwnProperty(key)) { //Listenerがkeyをもっていれば
                 if (key.split('.').indexOf(_event.split('.')[1]) > -1) {//keyを.で分割した配列の中に、_eventを.で分割した配列の2番目の値が一致したものが-1より
                   el.removeEventListener(key.split('.')[0], utils.Listener[key]); //keyを.で分割した配列の最初の値のイベントで、utils.Listenerのプロパティを削除していく
                   el.setAttribute('lg-event-uid', el.getAttribure('lg-event-uid').replace('&' + _id[i], ''));//elのlg-event-uid属性にlg-event-uidの&とidを削除した文字列の値をセット
                   delete utils.Listener[key]; //utilsのListennerの中のkeyプロパティを削除
                 }
               }
             }

           } else {//_id配列がfalseだったら
             el.removeEventListener(_event.split('.')[0], utils.Listener[_event]);//eventsの1番目のイベントがハンドラとなり、utils.Listener[_event]を削除。
             el.setAttribute('lg-event-uid', el.getAttribute('lg-event-uid').replace('&' + _id[i], '')); //elのlg-event-uid属性にlg-event-uidの&とidを削除した文字列の値をセット
             delete utils.Listener[_event]; //utilsのListennerの中の_eventプロパティを削除
           }
         }

       }
     }
  }
};

↑こう書いていきます。
もちろんコピペはNGで、元のソースを見ながら全て手書きで書いていきます。

スクリーンショット 2019-11-30 14.17.07.png

エディター上で両ソースを見れる状態にしておき、オリジナルを見ながら模写していくってのがやりやすいかもしれないです。

結局写経したらどうなるの?

例えば見慣れない構文を見つけたとき、
この書き方はどういう機能を意味してるのか?
この処理はなにを意味しているのか?
そもそもこの記号何?
とか色々疑問が湧いてきます。
自分の手で書き写して、見慣れない構文はその都度調べて、既にある機能を自分の手で作り上げて行くのです。関数の書き方、効率的な処理の書き方、よくわからない記号の意味がじわじわわかったりしてきます。

楽器で例えるなら、ギターの練習の際基礎練はほどほどに既成曲ばっかり弾きまくって上達していくといった感じでしょうか。当然基礎は大事ですが、人によってはガシガシ手を動かした方が吸収できる人もいるようです。自分にあった学習の仕方があるとは思いますが、教本は補助教材としてわからないことがあったときに確認するようにして、あとはひたすら書く、というのが性に合ってる人はハマると思います。

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

JS thisの使い方について

thisについて
thisはイベントの中でそのイベントが起こった要素を取得する事ができる。
よく聞く文言だがよく分からなかったので調べてみた

例を出すとわかりやすく

<div>
 <ul>
  <li>text1</li>
  <li>text2</li>
  <li>text3</li>
 </ul>
</div>
$function(){
 $('li').click(function(){
  $(this).css('color','red);
  });
});

このように指定した場合、仮にtext1をクリックした際にthisの中にはli要素のtext1が取得されtext1の文字の色が赤に変化する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ES6で使えるアロー関数

ES6から関すにアロー関数というものが使えるようになりました。
functionとかくよりシンプルです。

今まで

const hoge = function() {
  console.log("foobar");
}

ES6ではアロー関数で記述できる

const hoge = () => {
  console.log("foobar");
}

内容はいままでとかわりありません。

hoge();

呼び出し方も今までと同じです。

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

Adobe ExtendScript Debuggerのセットアップ

はじめに

この記事は、Visual Studio Codeに、Adobe ExtendScript Debuggerプラグインをセットアップする手順を共有するためのものです。

前提とする条件

この記事は以下の環境を想定しています。

  • macOS 10.14.6
  • Visual Studio Code 1.38.1
  • ExtendScript Debugger 1.1.2

記事を読む前に、お手元の環境をご確認ください。

想定する読者

この記事は以下の読者を想定しています。

  • Visual Studio Codeを利用したことがある
  • JavaScriptの知識がある
  • ExtendScriptを利用、開発したことがある

この記事ではVisual Studio CodeおよびExtendScript開発の解説はしません。

ExtendScriptとは

ExtendScriptとは、Adobe社のCreativeCloudアプリケーションを制御するためのスクリプトです。ES3相当のJavaScriptに、各アプリケーション用のAPIを追加したものです。

ExtendScript ToolKitのサポート終了

ExtendScriptの開発には、いままではAdobe ExtendScript Toolkit CCが利用されてきました。しかしこのソフトはすでに開発とサポートの終了が宣言されています。Adobeの公式リリースでmacOS Catalina(10.15)では起動せず、また修正も行わないと告知されました。

リリース内で移行先として指定されているのがExtendScript Debuggerです。

ExtendScript Debuggerのセットアップ

ExtendScript DebuggerはMicrosoft社のVisual Studio Code用プラグインです。動作させるにはまず、このコードエディターをインストールする必要があります。

インストール

次に、ExtendScript Debuggerをインストールします。Visual Studio Codeを起動し、Extensionパネルを開くと検索ボックスがあります。そこに「ExtendScript」と入力するとプラグインが表示されます。

「インストール」ボタンを押せばExtendScript Debuggerのインストールは完了です。

アプリケーションとの接続

ExtendScript DebuggerはAdobe CCアプリケーションと接続し、デバッグ用の情報を収集します。この接続先アプリケーションを指定します。

画面下の青い帯に「ターゲットアプリケーションを指定」という表示が出ている場合、接続先のアプリケーションが指定されていません。ここをクリックするとコマンドパレットが開き、接続先の候補が表示されます。

候補のアプリケーションが起動していない場合は、起動するかどうかを聞かれますので「はい」を選択します。

画面右下の文字が、アプリケーション名になれば接続は成功です。

デバッグ構成を追加する

次にデバッグ構成を追加します。デバッグ構成はデバッガーの動作を設定します。一度設定してしまえば何度も繰り返し呼び出せます。

デバッグ構成はlaunch.jsonという設定ファイルに保存されます。launch.jsonはいま開いているフォルダーの.vscodeという隠しフォルダーに保存されます。まだフォルダーを開いていないなら「ようこそ」画面からフォルダーを選択してください。

つぎにデバッガーパネルを開き、デバッグ構成のプルダウンメニューを開きます。

プルダウンメニューの最下部の「構成の追加…」を選びます。

すると新規にlaunch.jsonファイルが作成されます。複数のテンプレートが用意されているので、「ExtendScriptデバッグ起動設定」を選びます。

launch.json
{
    "type": "extendscript-debug",
    "request": "launch",
    "name": "スクリプト名を求める",
    "program": "${workspaceFolder}/${command:AskForScriptName}",
    "stopOnEntry": false
}

このようなテンプレートが挿入されるので、いったんlaunch.jsonを保存します。

デバッガーパネルに戻り、プルダウンから「スクリプト名を求める」を選択し、緑の矢印アイコンを押します。コマンドパレットでデバッグしたいスクリプトファイルまでのパスを求められます。ここでファイルパスを入力すれば、AdobeCCアプリケーションでスクリプトが実行されます。

デバッグ構成を編集する

launch.jsonの冒頭コメントには、Microsoftのガイド記事へのリンクが張られています。
このリンク先の記事には、デバッグ構成の編集方法が解説されています。

例として、以下のように構成を変更するとコマンドパレットを開かず現在開いているファイルを実行します。
参考 : Debugging current file in VS Code
ExtendScript Debuggerをスクリプトのランチャーとして利用する場合は、こちらの構成がオススメです。

launch.json
{
    "type": "extendscript-debug",
    "request": "launch",
    "name": "現在のファイルをExtendScriptとして実行",
    "program": "${file}",
    "stopOnEntry": false
},

参考記事

Adobe製品をVSCodeで制御(Javascript)

以上、ありがとうございました。

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

jQueryを使わないAjax共通ロジック

最近はjQuery使いたくない時もあるので、これもメモ

commonajax.js
//-----------------------------------------------
// jquery使わない式のajax
// @param url   リクエスト先
// @param requestJson リクエストデータ(json形式)
// @param afterFunction 取得後実行処理
// @param afterFuncParams 取得後実行処理に対する引数
//-----------------------------------------------
const commonajax = function(url, requestJson, afterFunction, afterFuncParams){

    fetch(url,{
        method : "POST",
        body : JSON.stringify(requestJson), // 文字列で指定する
        headers: {"Content-Type": "application/json; charset=utf-8"},
    })
    .then(function(res){
        return res.json(); 
    })
    .then(function(data){
        // 返されたデータ(json) 
        if(afterFunction) afterFunction(data, afterFuncParams);
    })
    .catch(function(err){
        // エラー処理
        console.error(err);
    });
}

使い方

temp.js
window.addEventListener("load", function(){
    //イベントトリガー
    var getbtn = document.getElementById("getbtn");
    if(getbtn){
        //クリックしたとき
        getbtn.addEventListener("click", function(){
            commonajax("hoge/ajax", {"id":"hoge1"}, testfunc, "setelm");
        });

        //事後処理
        var testfunc = function(data, param){
            var setelm = document.getElementById(param);
            if(setelm) setelm.value = data["colom"];
        }            
    }    
});

使い方の動作確認はまだしてない(^^;

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

ES6で使えるようになった変数のまとめ

今までjqueryとか古いjavascriptの文法しかつかったことがなかったので
もう一度新しくなったJSを勉強しなおしています。

そのメモ代わりとして

変数

var

ずっとこれを使ってきました。

変数宣言はもちろんのこと
再宣言、再代入も可能

var hogehoge = "Variable";
console.log(hogehoge);

let

ES6からはこちらがvarより主流になってきました。
再代入は可能ですが、再定義はできません。

let hogehoge = "Let";
console.log(hogehoge);

const

定数として利用
再宣言、再代入が不可

const hogehoge = "Constant";
console.log(hogehoge);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jqueryでのajax共通ロジック

都度都度作るハメになってしまっているので、書き残しメモ

ajaxcommon.js
//-----------------------------------------------
// ajax共通
//  requrl:リクエスト先
//  reqparam:リクエストパラメータ(json形式)
//  donefunc:ajax成功時における関数処理
//  doneparam:ajax成功時における関数処理の引数
//            (複数必要な場合は、json等を使用)
//-----------------------------------------------
const ajaxcommon = function(requrl,reqparam,donefunc,doneparam){
  $.ajax({
    type: "post",
    url: requrl,
    data: reqparam,
    dataType : 'json'
  }).done(function(data) {
    if(donefunc) donefunc(data, doneparam);
  }).fail(function(e) {
    //エラー処理
    console.log(e);
  });
}

type("post" or "get")の指定とエラー処理はお好みで
※エラーは引数増やして、ちゃんとやったほうが良いかも

使い方

{"reqname1":"name1","reqname2":"name2"} 

みたいなjsonがリクエスト先から返ってくると仮定するとして、

temp.js
$(document).ready(function(){
  //取得ボタン押下
  $("#getbtn").on("click", function(){
    ajaxcommon(
              "/hoge/ajax/",
              {reqcode:"001"},
              setdata,
              {setid1:"field1",setid2:"field2"} //設定id
    );
  });
});

//設定処理
var setdata = function(data, params){
  console.log(data);
  $("#"+params["setid1"]).val(data["reqname1"]);
  $("#"+params["setid2"]).val(data["reqname2"]);
}

とかで動かすイメージ

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

jQueryでのAjax共通ロジック

都度都度作るハメになってしまっているので、書き残しメモ

ajaxcommon.js
//-----------------------------------------------
// ajax共通
//  requrl:リクエスト先
//  reqparam:リクエストパラメータ(json形式)
//  donefunc:ajax成功時における関数処理
//  doneparam:ajax成功時における関数処理の引数
//            (複数必要な場合は、json等を使用)
//-----------------------------------------------
const ajaxcommon = function(requrl,reqparam,donefunc,doneparam){
  $.ajax({
    type: "post",
    url: requrl,
    data: reqparam,
    dataType : 'json'
  }).done(function(data) {
    if(donefunc) donefunc(data, doneparam);
  }).fail(function(e) {
    //エラー処理
    console.log(e);
  });
}

type("post" or "get")の指定とエラー処理はお好みで
※エラーは引数増やして、ちゃんとやったほうが良いかも

使い方

{"reqname1":"name1","reqname2":"name2"} 

みたいなjsonがリクエスト先から返ってくると仮定するとして、

temp.js
$(document).ready(function(){
  //取得ボタン押下
  $("#getbtn").on("click", function(){
    ajaxcommon(
              "/hoge/ajax/",
              {reqcode:"001"},
              setdata,
              {setid1:"field1",setid2:"field2"} //設定id
    );
  });
});

//設定処理
var setdata = function(data, params){
  console.log(data);
  $("#"+params["setid1"]).val(data["reqname1"]);
  $("#"+params["setid2"]).val(data["reqname2"]);
}

とかで動かすイメージ

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

【ESLint】eslint-plugin-lodash-templateがEJSを(部分的に)サポートしたよ

かなり前に、【ESLint】eslint-plugin-lodash-template作ってみたよ という記事を書き、ちょっと前に 【ESLint】eslint-plugin-lodash-templateがJavaScriptテンプレートをサポートしたよ という記事を書きましたが、eslint-plugin-lodash-templateをJavaScriptテンプレートサポートさせたときついでに、部分的にEJSをサポートをしたのでその思い出を残します。

eslint-plugin-lodash-templateを基本的な使い方で使えば、EJSESLintが動くはずなので、
EJSでの使い方の説明は無いです。仕組みをどう変えたかの思い出の話がメインです。

eslint-plugin-lodash-templateとは

Lodashの_.templateに渡して使えるテンプレートタグ内のJavaScriptに対してESLintで検証できるようにするESLintのプラグインです。

今回の変更でEJS構文でもESLintでエラー検出ができるようになりました!

image.png

WEBで試す

EJSとは

JavaScriptを使ったテンプレート構文です。

<% if (user) { %>
  <h2><%= user.name %></h2>
<% } %>

公式はこちら https://ejs.co/

Lodashの_.templateに似た構文ですが、
HTMLエスケープする(EJS<%=)、しない(EJS<%-)、の意味が逆だったり、
<%_ ... _%>のような無駄な空白を除去するタグ記法があったり、
<%# ... %>というコメントタグ記法があったり、
<%%というリテラルを表現する記法があったりします。

EJSを部分的にサポートしました

以前から、受け入れるタグ構文をカスタマイズする機能(parserOptionsを利用)がありましたが、
これを拡張してEJSの記法を設定できるようにしました。
(拡張子.ejsに自動的に適用する共有設定も提供するようにしました。)

EJSのタグを処理できるように変更しました。

EJSはタグ記法はLodashの_.templateに比べてタグの記法が豊富です。
開始タグ・終了タグに様々な種類があり、組み合わせることができます。
以前までは、開始タグ・終了タグを1種類ずつしか受け入れられなかったのですが、
EJSをサポートするために、開始タグ・終了タグの種類を配列で受け入れ、構文解析できるようにしました。

// script部分
evaluate:    [ ["<%", "<%_"] /*開始タグ2種類*/, ["%>", "-%>", "_%>"] /*終了タグ3種類*/ ],
// 補完部分(HTMLエスケープなし)
interpolate: [  "<%-"        /*開始タグ*/    , ["%>", "-%>", "_%>"] /*終了タグ3種類*/ ],
// 補完部分(HTMLエスケープあり)
escape:      [  "<%="        /*開始タグ*/    , ["%>", "-%>", "_%>"] /*終了タグ3種類*/ ],

また、以前までは、コメントやリテラルを処理する機能はなかったのですが、(Lodashの_.templateにはコメントやリテラルという概念はなかったので。)
コメントタグ記法、リテラルタグ記法も受け入れ、構文解析できるようにしました。

// コメント部分
comment:    [ "<%#" /*開始タグ*/, ["%>", "-%>", "_%>"] /*終了タグ3種類*/ ],
// リテラル
literal:    [ "<%%" /*タグ (リテラルには終了タグの概念はない様子)*/ ],

共有設定で.ejs用の設定を適用

eslint-plugin-lodash-templateの提供する共有設定で、以下ようにoverridesを利用して、拡張子.ejsのファイルにEJSで動作する用のparserOptionsを設定しています。
これによって.ejsのファイルでは自動的にEJSのタグで構文解析するようになりました。

module.exports = {
    // ...
    overrides: [
        {
            files: ["*.ejs"], // `.ejs`に適用
            parserOptions: {
                // eslint-plugin-lodash-template用の設定
                templateSettings: {
                    // 上記で書いたやつ
                    evaluate: [["<%", "<%_"], ["%>", "-%>", "_%>"]],
                    interpolate: ["<%-", ["%>", "-%>", "_%>"]],
                    escape: ["<%=", ["%>", "-%>", "_%>"]],
                    comment: ["<%#", ["%>", "-%>", "_%>"]],
                    literal: ["<%%"],
                },
            },
        },
    ],
}

結果

これらの変更でEJS構文でもESLintでエラー検出ができるようになりました!(再掲)

image.png

なぜ(部分的)と言っているのか?

EJSには古い構文で<% include _header %>というようなincludeディレクティブというのがまだ息してるらしく、eslint-plugin-lodash-templateはこれを解析できません。なので(部分的)サポートということにしています。

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

Ionic赤本の疑問がすごく鋭かったのでまとめて返事する記事

https://hackmd.io/0ephzNFASSCOVKpjfcr6xA で、おそらく他にも疑問に持たれる方がいる疑問を書いていただいてるので、記事にして私見やコメントをしておきます。この記事はIonic Advent Calendar 2019の1日目の記事にします。

ionic serveng serve の違い

Ionic CLIでは、内部的にAngular CLIのコマンドを実行しておりますので、以下のようにコマンド以下に表示されるものが実行されるコマンドです。

sakakibara app % ionic serve
> ng run app:serve --host=localhost --port=8100  # これです

ちなみに、Ionic/Reactだったら npm start が実行されます。フレームワークやIonicのバージョン間の差分を吸収しながら実行することが目的ですので、Angular CLIのコマンドを打ち慣れていたら、そちらを実行して問題ありません。

Ionicのライフサイクルフック用のinterfaceは無いのかな?

そういうプルリクエストはでています: https://github.com/ionic-team/ionic/pull/18044

個人的にはいずれマージされるとは思うのですが、Angularのライフサイクルのようにimplementsしないとlintエラーがでるようなことは起きないだろうと予測しています。

Ionicではinterfaceの型にprefixとしてIをつける。例:interface IPost {}

完全に著者の好みです。IDEで I を打つと一覧で候補だしてくれるのが心地よくて・・・。ただ再利用しないinterfaceはserviceの中にそのまま挿入することもありがちなので、prefixつける方がとりまわしいい気がしています。ただ、筆者の好みです。

Ionicのライフサイクルフックが動く詳細な条件は?

Pageコンポーネント、もう少し厳密にいうと ion-app > ion-router-outlet 以下のRouterで定義されているコンポーネントです。ただ、ライフサイクルをinjectableで挿入するプルリクエストがコアチームによってつくられているので、将来的にはもう少し自由がきくようになる可能性が高いです。

https://github.com/ionic-team/ionic/pull/19391

Cordovaのスピリチュアルな後継ライブラリ > スピリチュアルとは?

開発意図としては後継ライブラリとして考えてるけど、Cordovaもまだ現行で、またCordovaチームがそう言ったわけでもないので「そういう考え方で開発しています」というニュアンスです。CapacitorはIonic teamによって開発が行われています。

Capacitor NativeとかCapacitor Native for Webとかの謎プロダクトが作られて欲しい

NativeプロジェクトのWeb ViewにCapacitorを使うことができるという噂を聞いたことがあります(未検証)(でもできそう)

クリアテキストのパーミッションとは?

HTTPで通信していると「HTTTPSにしなさい」と怒られるやつです。
https://qiita.com/b_a_a_d_o/items/afa0d83bbffdb5d4f6be

.toPromise(Promise) このPromiseはなんだろう

Promiseのインスタンス化に使用するconstructor関数なのです。といっても、確か不要だったと思います(明示するためだけに書いてます)
https://rxjs-dev.firebaseapp.com/api/index/class/Observable#toPromise

それでは、また。

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

Ionic赤本への疑問がすごく鋭かったのでまとめて返事する記事

https://hackmd.io/0ephzNFASSCOVKpjfcr6xA で、おそらく他にも疑問に持たれる方がいる疑問を書いていただいてるので、記事にして私見やコメントをしておきます。この記事はIonic Advent Calendar 2019の1日目の記事にします。

ionic serveng serve の違い

Ionic CLIでは、内部的にAngular CLIのコマンドを実行しておりますので、以下のようにコマンド以下に表示されるものが実行されるコマンドです。

sakakibara app % ionic serve
> ng run app:serve --host=localhost --port=8100  # これです

ちなみに、Ionic/Reactだったら npm start が実行されます。フレームワークやIonicのバージョン間の差分を吸収しながら実行することが目的ですので、Angular CLIのコマンドを打ち慣れていたら、そちらを実行して問題ありません。

Ionicのライフサイクルフック用のinterfaceは無いのかな?

そういうプルリクエストはでています: https://github.com/ionic-team/ionic/pull/18044

個人的にはいずれマージされるとは思うのですが、Angularのライフサイクルのようにimplementsしないとlintエラーがでるようなことは起きないだろうと予測しています。

Ionicではinterfaceの型にprefixとしてIをつける。例:interface IPost {}

完全に著者の好みです。IDEで I を打つと一覧で候補だしてくれるのが心地よくて・・・。ただ再利用しないinterfaceはserviceの中にそのまま挿入することもありがちなので、prefixつける方がとりまわしいい気がしています。ただ、筆者の好みです。

Ionicのライフサイクルフックが動く詳細な条件は?

Pageコンポーネント、もう少し厳密にいうと ion-app > ion-router-outlet 以下のRouterで定義されているコンポーネントです。ただ、ライフサイクルをinjectableで挿入するプルリクエストがコアチームによってつくられているので、将来的にはもう少し自由がきくようになる可能性が高いです。

https://github.com/ionic-team/ionic/pull/19391

Cordovaのスピリチュアルな後継ライブラリ > スピリチュアルとは?

開発意図としては後継ライブラリとして考えてるけど、Cordovaもまだ現行で、またCordovaチームがそう言ったわけでもないので「そういう考え方で開発しています」というニュアンスです。CapacitorはIonic teamによって開発が行われています。

Capacitor NativeとかCapacitor Native for Webとかの謎プロダクトが作られて欲しい

NativeプロジェクトのWeb ViewにCapacitorを使うことができるという噂を聞いたことがあります(未検証)(でもできそう)

クリアテキストのパーミッションとは?

HTTPで通信していると「HTTTPSにしなさい」と怒られるやつです。
https://qiita.com/b_a_a_d_o/items/afa0d83bbffdb5d4f6be

AndroidでCapacitorのライブリロードを使うと、Android的にはローカルファイルではなく、外部の http://localhost:8100 というURLを内部に表示しますのでクリアテキストを有効にしないとエラーがでます。

.toPromise(Promise) このPromiseはなんだろう

Promiseのインスタンス化に使用するconstructor関数なのです。といっても、確か不要だったと思います(明示するためだけに書いてます)
https://rxjs-dev.firebaseapp.com/api/index/class/Observable#toPromise

それでは、また。

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

vue + electron で windowsデスクトップアプリを作成する

確認した環境

  • windows 10 pro 64bit
  • node v10.16.0

環境のセットアップ

Vue CLI + electron-builderプラグイン で環境をセットアップします。
electron-vueボイラープレートは更新が止まっているみたいなので使いません。

Vue CLI

以下のコマンドで、Vue CLI をインストールします。

npm i -g @vue/cli

vueプロジェクトを作成します。プロジェクト名はmy-projectにしました。

vue create my-project

以下のコマンドで、Welcomeページが表示されるのを確認します。webブラウザにhttp://localhost:8080/を指定することで確認できます。

cd my-project
npm run surve

electron-builderプラグイン

続けて、プロジェクトフォルダの直下で以下のコマンドを実行し、electron-builderプラグインをインストールして呼び出します。これにより、electronやその依存パッケージ、スクリプトが追加されます。

vue add electron-builder

途中でインストールするelectronのバージョンの選択を求められますが、6.0.0を選択しました。

最後に以下のエラーが表示されますが、使用に影響はないようです。

---(省略)---

16 packages are looking for funding.
Run "npm fund" to find out more.
-  Running completion hooks...
error: 'installVueDevtools' is defined but never used (no-unused-vars) at src\background.js:6:3:
  4 | import {
  5 |   createProtocol,
> 6 |   installVueDevtools
    |   ^
  7 | } from 'vue-cli-plugin-electron-builder/lib'
  8 | const isDevelopment = process.env.NODE_ENV !== 'production'
  9 |


1 error found.

実行とアプリケーションのビルド

webアプリとして実行

npm run serve

デスクトップアプリとして実行

npm run electron:serve

ビルドしてデスクトップアプリを作成

プロジェクトフォルダ直下のdist_electronフォルダ内に実行ファイルが作成されます。

npm run electron:build

ビルドオプション

プロジェクトのルートにvue.config.jsファイルを新規作成し、そのファイル内容を記述します。

記述例:

vue.config.js
module.exports = {
  pluginOptions: {
    electronBuilder: {
      builderOptions: {
        productName: "my-application",
        appId: "com.sample.myapplication",
        win: {
          icon: 'src/assets/app.ico',
          target: [
            {
              target: 'zip', // 'zip', 'nsis', 'portable'
              arch: ['x64'] // 'x64', 'ia32'
            }
          ]
        }
      }
    }
  }
}

builderOptions:

  • productName : アプリケーションタイトル
  • appID : アプリケーションユーザーモデル ID (AUMID)として使用される。
  • win : windowsアプリの設定
    • icon : アプリケーションアイコンのパス
    • target :
      • target : 配布形態
      • 'zip' : zip圧縮
      • 'nsis' : Nullsoft Scriptable Install System
      • 'portable' : インストーラーのないポータブルアプリケーション
      • 'msi' : Microsoft Installer
      • 'appx' : UWPアプリパッケージ
      • arch: プラットフォーム
      • 'x64' : 64bitアプリケーション
      • 'ia32' : 32bitアプリケーション

参考:Any Windows Target

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

年末まで毎日webサイトを作り続ける大学生 〜43日目 HTML5のcanvasを触る〜

はじめに

こんにちは!@70days_jsです。

HTML5のcanvasを触ってみました。
とりあえず図形を描いて動かしてみました。

今日は43日目。(2019/11/30)
よろしくお願いします。

サイトURL

https://sin2cos21.github.io/day43.html

やったこと

前提
- Canvasとは?・・・HTML5・JavaScriptを使ってブラウザ上で図を描くための仕様
- canvas単体ではアニメーションできないので、js使う
- canvasでレイアウトを決めた部分はcssを適用できない

円を書いて動かしました。↓
test2.gif

canvasタグを使って描画範囲を確保します。↓

<body>
    <canvas id="canvas">
    </canvas>
</body>

cssで描画サイズを書きます。↓

#canvas {
    width: 100vw;
    height: 50vh;
}

JavaScriptで動きをつけます。

window.onload = function () {
    setInterval(draw, 10);
}

let canvas = document.getElementById('canvas');
let cvs = canvas.getContext('2d');
var x = 0;
let test = 0;
let test2 = 3;
let aa = 0.1;

function draw() {
    cvs.beginPath();
    cvs.arc(x, canvas.height / 2, 10, 0, Math.PI * test / 2, false);
    cvs.fillStyle = 'rgba(0,0,255,.1)';
    cvs.fill();
    cvs.closePath();

    cvs.beginPath();
    cvs.arc(x, canvas.height / 4, 10, 0, Math.PI * test2 / 2, true);
    cvs.fillStyle = 'rgba(255,0,0,.1)';
    cvs.fill();
    cvs.closePath();

    if (4 <= test) {
        test = 0;
        test2 = 0;
        x += 10;
    }

    test += aa;
    test2 -= aa;

}

円の書き方がいまいち分からず調べているとteratailさんに分かりやすい回答があったので参考にしました。↓
スクリーンショット 2019-11-30 6.51.28.png
引用: JavaScript - CANVASのarcメソッドの引数が分からない|teratail

gifに乗せた赤色の方が第六引数をtrueにした方です。
青色の方がfalseです。
デフォルトではfalseみたいですね。

testとtest2の値を0.1ずつ減らしたり増やしたりすることで、円が徐々に描かれているように見せています。

感想

canvas面白かったです。今後も何かに使えたらなーと思いました。

最後までお読みいただきありがとうございます。明日も投稿しますのでよろしくお願い致します。

参考

  1. JavaScript - CANVASのarcメソッドの引数が分からない|teratail (https://teratail.com/questions/94435#reply-149622)

とても分かりやすかったです。ありがとうございます!

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

都道府県を地方ごとに出力をした(改善版)

はじめに

前回こちらの記事都道府県を地方ごとに配列を組みかえてみたを投稿させていただいたのですがあまりにもコードが長く単調なものだったので会社の上司にリファクタリングしてもらい、ちょっと自分でもいじってリファクタリングしてみた。

area.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="/css/style.css">
  <title>地方ごとにタイトルを出力</title>
</head>
<body>

  <div class="container">

  </div>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
  <script src="/js/area.js"></script>
</body>
</html>
area.json
[
  {
    "hoge": "田中",
    "title": "",
    "image": "",
    "parent_category": "architecture",
    "category_slug": "production",
    "address": "北海道",
    "link": "",
    "topics": "0"
  },
  {
    "hoge": "山田",
    "title": "",
    "image": "",
    "parent_category": "civil",
    "category_slug": "train",
    "address": "北海道",
    "link": "",
    "topics": "0"
  },
  {
    "hoge": "池田",
    "title": "",
    "image": "",
    "parent_category": "civil",
    "category_slug": "architecture",
    "address": "北海道",
    "link": "",
    "topics": "0"
  },
{},
{},
{},
{}
]

area.js
$(function(){
  const region_prefs = {
    北海道地方: ["北海道"],
    東北地方: ["青森県", "岩手県"],
    関東地方: ["茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "神奈川県", "東京都"],
    中部地方: ["新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県", "静岡県", "愛知県"],
    近畿地方: ["三重県", "滋賀県", "京都府", "大阪府", "兵庫県", "奈良県", "和歌山県"],
    中国地方: ["鳥取県", "島根県", "岡山県", "広島県", "山口県"],
    四国地方: ["徳島県", "香川県", "愛媛県", "高知県"],
    九州地方: ["福岡県", "佐賀県", "長崎県", "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県"]
  };

  Object.keys(region_prefs).forEach(function (region) {
    $(".container").append(
      `<section data-region="${region}"><h3>${region}</h3><ul class="entries" /></section>`
    );
  });

  $.ajax({
    type: "GET",
    url: "/json/area.json",
    dataType: "json",
  })
  .done(function (data) {
    $.each(data, function (index, value) {
      Object.keys(region_prefs).some(function (region) {
        if (region_prefs[region].includes(value.address)) {
          $(`[data-region='${region}'] ul`).append(`<li>${value.title}</li>`);
        }
      });
    });
  }).
  fail(function () {
    window.alert("通信に失敗しました")
  });
})


改善ポイント

  • 前回都道府県の配列の初期化で長文で書いていたものを連想配列でまとめた。
  • 連想配列のキーを使い
  • 連想配列にまとめたことで前回の長いif文まとめることが出来た。
  • htmlも長かったのでsectionに地方ごとのdataを持たせてこちらも動的に生成するようにした。
  • some関数を使った。

結果

jsのコード量も半分以下になった。section以下を動的に生成できるようになった。

まとめ

まとめられるところを探す癖をつけるのが大事なんだと再確認出来ました。d

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