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

【未完】楽天市場のAPIを使って買えるマスクを提案してくれるLINEBotを作ってみた

はじめに

ProtoOutStudioというイケイケなスクールの「LINE Bot+APIで表現してアウトプット」という課題で製作したものです。

こちらの1時間でLINE BOTを作るハンズオンの記事をベースにLINEBotを作成しました。
最近の新型ウィルスの影響で、ネットショッピングばかりしているのですが、ちょっと楽天市場のトップ画面を見るのに飽きてきたので、(マスクなどの)必要なものを、(売り切ればかりなので)在庫のある商品で、高額になりすぎて買う気がなくなってしまわない(金額の)範囲で、提案してもらえるLINEBotを考えました。
(Amazonは申請が大変そうに見えたのでお見送りしました)

概要と作れなかったところ

概要

  • LINEBotにほしいもの、「マスク」と入力したら
    1. 楽天市場のキーワード検索から「マスク」を検索
    2. 絞り込み検索で「購入可能」
    3. 「最安価でソート」
    4. 「最低金額○円以上」
    5. 「最高金額○円以下」
    6. 商品画像付き
    7. を5つくらい返してくれるBot #### 作れなかったところ
  • ほしいもの 「マスク」と入力したら 
    1. × → 楽天市場のキーワード検索から「マスク」を検索
    2. ○ → 絞り込み検索で「購入可能」
    3. ○ → 「最安価でソート」
    4. ○ → 「最低金額○円以上」
    5. ○ → 「最高金額○円以下」
    6. ○ → 商品画像付き
    7. × → を5つくらい返してくれるBot

入力したものをエンコードしてAPIのURLに入れて作成するところと、
(APIをベタでかくと動くBotはできたけど)これを複数表示するためにどこでFor文を回せばよいかわからなかった

環境

Node.js v13.7.0
MacBook Pro macOS Mojave
Visual Studio Code v1.43.1

できたもの

Image from Gyazo
「マスクある?」と質問することで、楽天市場の中で在庫あり商品、最安価の商品名と商品URLと商品画像のあるマスクを提案してくれます。

コード

node.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
const PORT = process.env.PORT || 3000;

const guidecat = 'https://i.gyazo.com/97840790b257952c89c59e7c176e114c.png';

const config = {
  channelSecret: '',
  channelAccessToken: ''
};

const app = express();

app.post('/webhook', line.middleware(config), (req, res) => {
  console.log(req.body.events);
  Promise
    .all(req.body.events.map(handleEvent))
    .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let mes = event.message.text;
  if (mes.indexOf('') > -1) {
    getNodeVer(event.source.userId);
    return client.replyMessage(event.replyToken, [{
      type: 'image',
      originalContentUrl: guidecat,
      previewImageUrl: guidecat
    },
    { type: "text", text: '在庫があって安いのはこれだよ〜' }]);

  } else {
    mes = event.message.text;
    console.log(mes);
    return client.replyMessage(event.replyToken, [{
      type: 'image',
      originalContentUrl: guidecat,
      previewImageUrl: guidecat
    },
    { type: "text", text: '〇〇?って聞いてほしいな' }]);
  }
}
const getNodeVer = async (userId) => {
    //キーワード部分にevent.message.textをエンコードしたものを入れたい
  const res = await axios.get('https://app.rakuten.co.jp/services/api/IchibaItem/Search/20170706?format=json&keyword=' + '%E3%83%9E%E3%82%B9%E3%82%AF' + '&hits=3&applicationId=1003940711976508350');

  const item = res.data;
  await client.pushMessage(userId, {
    type: 'text',
    text: item.Items[0].Item.itemName +
      "\n" + item.Items[0].Item.itemUrl +
      "\n¥" +
      item.Items[0].Item.itemPrice
  });
  await client.pushMessage(userId, {
    type: 'image',
    originalContentUrl: item.Items[0].Item.mediumImageUrls[0].imageUrl,
    previewImageUrl: item.Items[0].Item.mediumImageUrls[0].imageUrl

  });
}
app.listen(PORT);
console.log(`Server running at ${PORT}`);

参考サイト

感想

深いAPIを操るの大変ですが自分好みの条件の商品をサクッと提案してもらえるBotはそれなりに便利そうなでちゃんと完成させねばです。
きっともっといい書き方や2回書かなくてもいいものとかたくさんある気がしますが、【未完】を取れるように早めにやっつけたいです。

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

自動更新

自動更新の機能

①何秒かおきに、JavaScriptを使ってブラウザに表示されているメッセージのうち最も新しいもののidをリクエストとして送る
②Railsのコントローラのアクションにてデータベースに保存されている最新のメッセージのidと①のidを比較し、①のidよりも大きいidを持つメッセージたちをレスポンスする
③JavaScriptを使って、レスポンスに含まれるメッセージたちをメッセージ一覧の最後に追加する

表示されているメッセージのidの確認

jQueryを使って表示されている最新メッセージのidを取得できるようにします。そのため今回はmessagesテーブルとし、messagesテーブルのidを、HTMLの中に埋め込みます。
その時に利用できるのがカスタムデータ属性です。

カスタムデータ属性

カスタムデータ属性とは、HTMLタグの属性の1種です。
【例】

xxxx.html
<p class="first-message">

例えば、上記の例はpタグにclass属性を設定しています。このように、あるタグを使う時に情報を付加するために使用するものです。
属性として設定できる項目はタグごとに決まっていますが、自由に追加することができる属性がカスタム属性です。
【例】

xxxx.html
<p class="first-message" data-messege-id=120>

カスタムデータ属性を使うときは、属性名を「data-」で始まる名称にします。
上記のように記述すれば、「message-id」という名前のカスタムデータ属性を設定できたことになります。
data-任意の名前=任意の値と書き、カスタムデータ属性を設定しておくことで、JavaScriptから簡単に値を取得できます。
jQueryでは、取得したDOMに対しdataというメソッドを利用することで、カスタムデータ属性の値を取得可能です。
【例】

xxxx.html
 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8" />
     <script src="http://code.jquery.com/jquery-1.4.3.min.js"></script>
   </head>
   <body>
     <!-- カスタムデータ属性 -->
     <section id="blog" data-author="Taro" data-create-date="2013-04-10">
       <h1>Hello World!</h1>
       <p>This is a sample text.</p>
     </section>
     <script>
       var blog = $("#blog");
      //jQueryでカスタムデータ属性の値を取得
       alert("author : " + blog.data('author'));
       alert("create date : " + blog.data('create-date'));
     </script>
   </body>
 </html>

メッセージのidをカスタムデータ属性として追加

hamlの場合、カスタムデータ属性をつけます。
【例】

example.haml
%div{data: {message: {id: '1'}}}
# 上記の記述で、以下のようにカスタムデータ属性が反映される
# → <div data-message-id='1'>

【例】_message.html.hamlの場合

_message.html.haml
.message{data: {message: {id: message.id}}}
# 以下省略

新規投稿を取得できるよう

次に、今表示されているメッセージよりも新しい投稿があるのか確認する機能を追加します。確認するためには、以下の2つの機能が必要です。
①コントローラーに、新規投稿を確認するアクションがあること
②①のアクションを呼び出す仕組みがあること
ここでは、先に①の機能を実装していきます。
新規メッセージがあるか確認し、追加されている場合はそのデータを返すアクションを作成します。
このような、リクエストに対してJSONなどのデータを返すアクションはWebAPIで実装します。

WebAPIとは

WebAPIはAPIの一種です。
まずAPIとは、アプリケーション開発者が外部に向けてアプリケーションの機能の一部を公開する仕組みです。
例えばTwitterのAPIを使用すれば、Twitterアプリを使うことなくつぶやきの情報を取得するなどの機能を使うことができます。
WebAPIは、HTTPやHTTPS通信を通じて利用するAPIのことです。例えば天気情報を公開しているAPIであれば、ブラウザのURL欄に必要なアドレス等を入力すればデータを取得することができます。

apiディレクトリおよびコントローラを作成

APIとして機能するコントローラーを作成していきます。
①controllersディレクトリ直下にapiディレクトリを作成します。
②そのフォルダの中に今回はmessages_controller.rbというファイルを新規作成します。既存で同じ名前があっても、別に作成する必要があります。
③新規作成したapi/messages_controller.rbの中身を以下のように編集します。
【例】

app/controllers/api/messages_controller.rb
class Api::MessagesController < ApplicationController
  def index
  end
end

Rubyのクラス名は、一行目のように::で繋げて装飾することができます。これを、名前空間またはnamespaceといいます。
###名前空間(namespace)
名前空間をつけることにより、同様のクラス名で名付けたクラスを作ってもそれらを区別することができます。今回の場合はcontrollers/messages_controller.rbとcontrollers/api/messages_controller.rbが存在するとします。ですが、ディレクトリを分けているおかげで区別できます。
ただし、プログラムがクラスを判別する際はどのディレクトリに入っているかでの判別はできないため、名前空間を利用するルールになっています。こうすることで、Railsは間違えることなく2つのコントローラを区別するようプログラムされています。
イメージとしては、同じ苗字の人がいたとしても、部署名などをつければ該当者が一人になるこという感じです。

indexアクションの完成

indexアクションの中には、新規で投稿されたメッセージのみをDBから取得する処理を書きます。
ビューに表示されている最新メッセージのidが送られてくる(後ほど実装します)ので、そのidより新しい投稿があるかをチェックします。whereメソッドを使ってidを検索条件にします。
【例】今回はMessageモデルとGroupモデルがあるとします

app/controllers/api/messages_controller.rb
class Api::MessagesController < ApplicationController
  def index
    # ルーティングでの設定によりparamsの中にgroup_idというキーでグループのidが入るので、これを元にDBからグループを取得する
    group = Group.find(params[:group_id])
    # ajaxで送られてくる最後のメッセージのid番号を変数に代入
    last_message_id = params[:id].to_i
    # 取得したグループでのメッセージ達から、idがlast_message_idよりも新しい(大きい)メッセージ達のみを取得
    @messages = group.messages.includes(:user).where("id > ?", last_message_id)
  end
end

次は、このアクションを呼び出すためのルーティングを設定します。
今回のようにnamespaceを使ったコントローラファイルをルーティングから指定する際は、以下のように書きます。

namespace :ディレクトリ名 do ~ end
routes.rb
〜省略〜
namespace :api do
 resources :messages, only: :index, defaults: { format: 'json' }
end
〜省略〜

namespace :ディレクトリ名 do ~ endと囲む形でルーティングを記述すると、そのディレクトリ内のコントローラのアクションを指定できます。
defaultsオプションを利用して、このルーティングが来たらjson形式でレスポンスするよう指定しています。
routes.rbの書き方については他にもオプションがあります。
参考記事
Railsのルーティングを極める (後編)
Railsのルーティング

投稿内容のレスポンス

json形式でレスポンスするためのファイルを作成します。内容としては
①viewsフォルダに「api」フォルダを作成します
②apiフォルダに「messages」フォルダを作成します
③messagesフォルダ内に「index.json.jbuilder」を作成します
④index.json.jbuilderファイルを編集します

app/views/api/messages/index.json.jbuilder
json.array! @messages do |message|
  json.content message.content
  json.image message.image.url
  json.created_at message.created_at.strftime("%Y年%m月%d日 %H時%M分")
  json.user_name message.user.name
  json.id message.id
end

メッセージは複数投稿されている可能性があるため、配列形式でarray!メソッドを使用してJSONを作成します。

jBuilderの設定

views/messages/create.json.jbuilder
json.content    @message.content
json.image      @message.image.url
json.created_at @message.created_at.strftime("%Y年%m月%d日 %H時%M分")
json.user_name @message.user.name
#idもデータとして渡す
json.id @message.id

取得した投稿データを表示

作成したアクションを動かすリクエストを実装します。まず、最新のメッセージのidを取得できていることを確認し、次にreloadMessagesという名前で関数を作成して、あとでこのメソッドを呼び出す想定で作成します。

最新メッセージのidを取得できることを確認

新規投稿だけを取得できるようにするには、今表示されている最新メッセージのidを取得する必要があります。
最初に、このidを取得できるか実験的にコードを記述します。コンソールで最新メッセージのidが取得できているかを確認します。

message.js
$(function(){
  var last_message_id = $('.message:last').data("message-id");
  console.log(last_message_id);

  〜省略〜
})

確認したら、このコードは一旦消します。

$('.message:last')

jQueryのオブジェクトの指定方法の1つに、:lastがあります。今回の場合は.messageというクラスがつけられた全てのノードのうち一番最後のノード、という意味になります。
1つ1つのメッセージが表示されているdivには.messageというクラスがついており、最新のメッセージは一番下、つまりページの中でも最後のノードということになります。これを利用して、一番最後のメッセージのidを取得しています。
ブラウザの検証ツールでデータベース上のidと同じidが表示されていることも確認します。

message.jsの編集

次にjQueryからAPIを呼び出せるようにします。このロジックはreloadMessagesという名前で関数を作成してその中に書いていくことにします。
APIを呼ぶには正しいURLにリクエストを送信する必要があります。まず「どのURLをリクエストしたいのか」を確認します。
今回リクエストしたいのは/groups/id番号/api/messagesとします。
ajax関数のurlに何も指定しなかった場合、リクエストのURLは現在ブラウザに表示されているパスと同様になります。つまり今回の場合は、groups/id番号となります。
対してurlに文字列で値を指定すると、パスを指定することができます。今回の場合は相対パスで書くことで、自動的に現在ブラウザに表示されているURLの後に繋がる形になります。例えば現在のURLがgroups/3/messagesとして、urlに"hoge"と指定すればリクエストのURLはgroups/3/hogeとなります。
この法則を考えつつ、文字列で相対パスとなるようURLを指定します。
【例】

message.js
$(function() {
〜省略〜

  var reloadMessages = function() {
    //カスタムデータ属性を利用し、ブラウザに表示されている最新メッセージのidを取得
    var last_message_id = $('.message:last').data("message-id");
    $.ajax({
      //ルーティングで設定した通り/groups/id番号/api/messagesとなるよう文字列を書く
      url: "api/messages",
      //ルーティングで設定した通りhttpメソッドをgetに指定
      type: 'get',
      dataType: 'json',
      //dataオプションでリクエストに値を含める
      data: {id: last_message_id}
    })
    .done(function(messages) {
      console.log('success');
    })
    .fail(function() {
      alert('error');
    });
  };
});

取得した最新のメッセージをブラウザのメッセージ一覧に追加します。
これまで作っているbuildHTMLメソッドを編集して、非同期で追加されるメッセージのHTMLにもdata-messege-idという名前のカスタムデータ属性をつけます。こうすることで、非同期で追加されるメッセージにもidを与えることができます。
【例】

message.js
〜省略〜
  var buildHTML = function(message) {
    if (message.content && message.image) {
      //data-idが反映されるようにしている
      var html = `<div class="message" data-message-id=` + message.id + `>` +
        `<div class="upper-message">` +
          `<div class="upper-message__user-name">` +
            message.user_name +
          `</div>` +
          `<div class="upper-message__date">` +
            message.created_at +
          `</div>` +
        `</div>` +
        `<div class="lower-message">` +
          `<p class="lower-message__content">` +
            message.content +
          `</p>` +
          `<img src="` + message.image + `" class="lower-message__image" >` +
        `</div>` +
      `</div>`
    } else if (message.content) {
      //同様に、data-idが反映されるようにしている
      var html = `<div class="message" data-message-id=` + message.id + `>` +
        `<div class="upper-message">` +
          `<div class="upper-message__user-name">` +
            message.user_name +
          `</div>` +
          `<div class="upper-message__date">` +
            message.created_at +
          `</div>` +
        `</div>` +
        `<div class="lower-message">` +
          `<p class="lower-message__content">` +
            message.content +
          `</p>` +
        `</div>` +
      `</div>`
    } else if (message.image) {
      //同様に、data-idが反映されるようにしている
      var html = `<div class="message" data-message-id=` + message.id + `>` +
        `<div class="upper-message">` +
          `<div class="upper-message__user-name">` +
            message.user_name +
          `</div>` +
          `<div class="upper-message__date">` +
            message.created_at +
          `</div>` +
        `</div>` +
        `<div class="lower-message">` +
          `<img src="` + message.image + `" class="lower-message__image" >` +
        `</div>` +
      `</div>`
    };
    return html;
  };
〜省略〜

reloadMessages関数からもHTMLを組み立てる関数を呼ぶようにします。

message.js
$(function() {
〜省略〜

  var reloadMessages = function() {
    //カスタムデータ属性を利用し、ブラウザに表示されている最新メッセージのidを取得
    var last_message_id = $('.message:last').data("message-id");
    $.ajax({
      //ルーティングで設定した通りのURLを指定
      url: "api/messages",
      //ルーティングで設定した通りhttpメソッドをgetに指定
      type: 'get',
      dataType: 'json',
      //dataオプションでリクエストに値を含める
      data: {id: last_message_id}
    })
    .done(function(messages) {
      //追加するHTMLの入れ物を作る
      var insertHTML = '';
      //配列messagesの中身一つ一つを取り出し、HTMLに変換したものを入れ物に足し合わせる
      $.each(messages, function(i, message) {
        insertHTML += buildHTML(message)
      });
      //メッセージが入ったHTMLに、入れ物ごと追加
      $('.messages').append(insertHTML);
    })
    .fail(function() {
      alert('error');
    });
  };
});

数秒ごとにリクエストするように実装

jQueryには、一定時間が経過するごとに処理を実行することができる関数があります。それがsetInterval()関数です。

setInterval()関数

第一引数に動かしたい関数名を、第二引数に動かす間隔をミリ秒単位で渡すことができます。
今回は、reloadMessages関数を数秒おきに呼び出します。
【例】

message.js
$(function() {
〜省略〜
//$(function(){});の閉じタグの直上(処理の最後)に以下のように追記
  setInterval(reloadMessages, 7000);
});

引数で渡している7000という数字は、7秒という意味になります。500にすると、0.5秒です。

メッセージを取得したら画面がスクロールできるように

スクロールを行うにはjQueryのanimate関数を利用します。
コードを追加する場所は、非同期通信が成功した場合行う処理の最後にします。
また更新するメッセージがなかった場合は.doneの後の処理が動かないよう、条件分岐を追加しています。さらに、フォームの中身を空にして、フォームを再度送信できるようにする処理も追記します。
【例】

message.js
$(function() {
〜省略〜

  var reloadMessages = function() {
    //カスタムデータ属性を利用し、ブラウザに表示されている最新メッセージのidを取得
    var last_message_id = $('.message:last').data("message-id");
    $.ajax({
      //ルーティングで設定した通りのURLを指定
      url: "api/messages",
      //ルーティングで設定した通りhttpメソッドをgetに指定
      type: 'get',
      dataType: 'json',
      //dataオプションでリクエストに値を含める
      data: {id: last_message_id}
    })
    .done(function(messages) {
      if (messages.length !== 0) {  #追加
        //追加するHTMLの入れ物を作る
        var insertHTML = '';
        //配列messagesの中身一つ一つを取り出し、HTMLに変換したものを入れ物に足し合わせる
        $.each(messages, function(i, message) {
          insertHTML += buildHTML(message)
        });
        //メッセージが入ったHTMLに、入れ物ごと追加
        $('.messages').append(insertHTML);
        $('.messages').animate({ scrollTop: $('.messages')[0].scrollHeight});   #追加
      }   #追加
    })
    .fail(function() {
      alert('error');
    });
  };
});

自動更新が必要ない画面では行わないように

jQueryは今のところ全てのページにて発火するため、どの画面を見ていても自動更新処理が行われます。このままでは、メッセージ更新を行わないページにおいてエラーが発生したり、無駄なトラフィックが発生してしまいます。
「グループのメッセージ一覧ページ」を表示している時だけ自動更新が行われるようにコードを追加します。jQueryの正規表現にまつわるメソッドである、.matchを利用します。

match

JavaScriptの文字列が利用できるメソッドです。引数に正規表現を取り、メソッドを利用した文字列にその正規表現とマッチする部分があれば、それを含む配列を返り値とします。
【例】

example.js
var str = "hogefuga"
str.match(/hoge/);
// → ["hoge", index: 1, input: "ghogefuga", groups: undefined]]

返り値の値に含まれる他の情報は、一旦無視してしまって大丈夫です。
もしもマッチする部分がない場合、返り値はnullになります。そのため、自動更新を行うべきURLである場合のみ、という条件分岐を作ることができます。
【例】

message.js
$(function() {
〜省略〜
//$(function(){});の閉じタグの直上(処理の最後)に以下のように追記
  if (document.location.href.match(/\/groups\/\d+\/messages/)) {
    setInterval(reloadMessages, 7000);
  }
});

matchメソッドの引数として書いている/\/groups\/\d+\/messages/の部分が正規表現です。正規表現は基本的には/と/で囲んだ部分で、/自体も正規表現に含めたい場合、直前に(バックスラッシュ)を付けます。
また、\d+の部分は、「桁無制限の数値」という意味になります。具体的には、\dが0 ~ 9までの数字のどれかを表し、+は+のついた文字が何文字でもマッチする、という特殊な意味を持ちます。
これで、URLにgroups/数字/messagesという部分があるページでない限り、reloadMessagesメソッドが動くことはありません。

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

Chromeはwindow.openするときに修飾キーの影響を受ける

たまたま「⌘+クリック」で新規タブを開かせたい機能の実装をしていて見つけた挙動です。知らなかったのでメモ。

window.open の普通の挙動

const $button = document.querySelector('#button');
$button.addEventListener('click', (e) => {
  window.open('./hoge');
)};

のような実装をした場合、通常はボタンクリックで新規タブが開き、新しいタブへ自動的に切り替わります。

window.openの通常の挙動を描いたGIF動画です。

修飾キーを押しながらクリックしたときの挙動

同じ実装のまま、ボタンをクリックするときに特定の修飾キーを押し続けてみます。

  • macOSの場合はcommandキー
  • Windowsの場合はctrlキー

すると新規タブは背面に開きます。

macOSのChromeでcommandキーを同時押ししてwindow.openしたときのGIF画像です。

また、同様にshiftキーを押したまま押下するとタブではなく新規ウィンドウで開かれます。

これらの挙動はイベントをpreventDefaultで殺しても発生します。

const $button = document.querySelector('#button');
$button.addEventListener('click', (e) => {
  e.preventDefault(); // 防げない
  window.open('./hoge');
)};

// これでも防げない
window.addEventListener('keydown', (e) => {
  e.preventDefault();
});

旧WebKit系以外のブラウザでは再現せず

これらの現象はSafariとmacOS版のEdgeでも再現しました。一方で、Firefoxや旧エンジンのEdgeでは発生しません。

macOSのFirefoxでcommandキーを同時押ししてwindow.openしたときのGIF画像です。

【追記】 回避策

非常に怪しい実装ですが、window.opensetTimeoutの中で実効すると再現しなくなります。
この記事の執筆時点では回避できましたが、将来的に使えなくなる可能性もあります。

const $button = document.querySelector('#button');
$button.addEventListener('click', (e) => {
  setTimeout(() => {
    window.open('https://codepen.io/hokkey/full/XWbxKOR');
  }, 100);
)};

検証用CodePen

See the Pen window.open test for Chrome on pushing a modifier key by y_hokkey (@hokkey) on CodePen.

なぜなのか

Chromeではリンクを⌘+クリック(Windowsの場合はctrl+クリック)したときに、タブを背面で開く機能があります。

リンクを新しいバックグラウンド タブで開く | ⌘+リンクをクリック
リンクを新しいウィンドウで開く | shift+リンクをクリック
Chrome のキーボード ショートカット - Google Chrome ヘルプ

最近のサイトはリンクに見える要素でも裏でJavaScriptが動いている場合があるため、DOMのAPIよりも深い部分で新規タブ作成時の挙動を制御しているのでしょう。WHATWGを読むと、そういうブラウザもあるよ〜という注釈が一応書かれていました。

If there is a user agent that supports control-clicking a link to open it in a new tab, and the user control-clicks on an element whose onclick handler uses the window.open() API to open a page in an iframe element, the user agent could override the selection of the target browsing context to instead target a new tab.
https://html.spec.whatwg.org/multipage/window-object.html

Windowオブジェクトは怪しい

この挙動に限らず、Windowオブジェクトの挙動はブラウザ依存の部分が多く、リファレンスを読むだけでは動きを把握できない部分が多い印象です。

たとえば、そもそもwindow.openで新規ウィンドウになるのか新規タブになるのかも厳密には仕様化されていません。
(たまたまChromeは第2引数の有無でタブ/ウィンドウの挙動を変えてくれているだけで、将来この挙動が変わる可能性もあります。)

要件にウィンドウを開く挙動が絡む場合は実現可能かどうかをよく検討しておいた方が良いかもしれませんね。

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

【学習記録】『スラスラ読める JavaScriptふりがなプログラミング』読書感想  5章編 (最終章) 〜N予備校の復習を添えて〜

【学習記録】『スラスラ読める JavaScriptふりがなプログラミング』読書感想  4章編(後半)( https://qiita.com/Molly95554907/items/e9587a8c1eccb9282f23) の続き

第五章

・HTMLで作成したページを、後からJavaScriptによって変える

・HTMLを操作するために対象となる要素を選び出す
→DocumentオブジェクトのquerySelectorメソッドを利用

let 変数 = document.querySelector('取得したい要素');

で、HTMLの'取得したい要素'セレクタを獲得して変数(Elementオブジェクトに自動変換される)に代入する。

変数(Elementオブジェクト).innetText

で、要素を操作することができる。つまり、innerTextプロパティを利用すると、要素の中のテキストを操作できる。

例)

let elem = document.querySelector('h1');
elem.innerText = 'こんにちは';

を実行すると、
HTML上のh1要素に「こんにちは」と表示させることができる。

・復習と疑問点の整理〜つまりオブジェクトって何〜

・consoleやdocumentのように、予め定められたオブジェクトがあり、logやquerySelectorのように予め定めたれたメソッドがある。(本書の1章、5章より)

document.querySelectot('h1')
ならば
documentがオブジェクト(機能と変数の集合体)
querySelectorがメソッド(機能)
'h1'がプロパティ(変数)

・一方で、オブジェクトとなる変数を自作して、その中にプロパティーと値を入れて複数のデータを一つにまとめるものがある。(本書の4章後半より)

let myfavoritebooks = {title:'関ヶ原'}
ならば
myfavoritebooksがオブジェクト(機能と変数の集合体)(だが「変数」でもある)
titleがメソッド(機能)(だが「プロパティ」でもある)
'関ヶ原'がプロパティ(変数)(だが「値」でもある)

・プログラムの中で果たしている役割に与えられる言葉が入れ替わってるような……!!ややこしい。そして結局のところ本質の理解に至ってない。いっぱい使って慣れるしかないのかな…!?

・HTMLのinput要素で入力させた文字を取得する

valueプロパティを利用する。

例)

let elem = document.querySelector('input');
console.log(elem.value);

を実行すると、inputに入力した文字をコンソールに表示させられる

・イベントに反応して、HTMLの文字をJavaScriptに変える

・clickすると何かが起きる→addEventListenerメソッドと関数オブジェクトを利用し表現する。

変数(イベントを起こす元となる変数).addEventListener('click', () => { //関数内の処理 });

変数.addEventListener('イベントタイプ', () => {});
ということ。

無名関数

addEventListenerメソッドの引数には、イベントタイプと関数が取られている。ここでの関数には名前がない。このような、関数名を持たない関数を「無名関数」と言う。
関数が呼び出されるのは、「関数名();」が実行される時ではなく、「変数.addeventListener();]が実行される時。

【N予備校 一章「いいところ診断」に登場する無名関数】

スクリーンショット 2020-03-25 22.09.37.png

イベント、「onkeydown」「onclick」が実行された時に動く関数。
関数自体(=()=>)に名前はない。「変数.イベント」が実行された時に関数が動く。これが無名関数。

【N予備校 三章「集計処理を行うプログラム」に登場する無名関数】

スクリーンショット 2020-03-25 22.14.01.png
イベント、「'line'」でcsvのstringの読み込みが実行された時に動く関数。

・ボタンが押されると、入力した文字がHTMLの要素pに表示されるプログラムを作る

『スラスラ読める JavaScriptふりがなプログラミング』P172のコードを利用

「関数定義」、「データ」、「関数呼び出し」を組み合わせてプログラムを作る

//あとで使う変数を定義していく
let ipt = document.querySelector('input');
let btn = document.querySelector('button');
let element = document.querySelector('p');


//イベントを実行
btn.addEventlistener('click', () => {
element.innerText = ipt.value;
});


無名関数の中で、取得したHTMLの要素の中身を、
'input'に入力した内容(ipt)をvalueプロパティで出力したものに
代入して(入れ替えて)いる。

・テキスト置き換えマシンを作る

→ボタンを押すと、「テキストエリア」に書かれた文章の中から、「検索欄」に入力されたテキストを探し出し、「置き換え欄」に入力されたテキストをかわりに代入する。

・textareaの書き方

<textarea clos = "(文字数)" rows = "(行数)">文章内容</textarea>
正規表現

正規表現オブジェクトを用いると、見つかったもの全てを置換してくれる。

・new RegExp('検索文字列','g')
→'g'は、出現する文字全てを検索せよ、という意味
※RegExpオブジェクトはJavaScriptで正規表現を表すオブジェクト
newは「オブジェクトを作れ」の呪術

・文字列.replace(/検索文字列/g, '置換文字列');
→文字列の中から、検索文字列を全て検索し、置換文字列に置き換える

【N予備校 一章「いいところ診断」に登場する正規表現】

スクリーンショット 2020-03-25 22.18.22.png
{userName}という文字列を全て探し出し、変数userNameに置き換える。
「/検索文字列/g」という正規表現にならって書くなら「/{userName}/」だが、()のの中で{}は使えないので、バックスラッシュを使っている。

プログラムを書こう

・HTMLを書く

「検索欄」を <input type = "text" id = "findtext">
「置き換え欄」を <input type = 'text' id = "replacetext">

にタグ付しておく

・JavaScriptを書く
『スラスラ読める JavaScriptふりがなプログラミング』P177のコードを利用

//HTMLの要素を取得したものを変数に入れていく
let textarea = document.querySelector('textarea');
let findinput = document.querySelector('#findtext');
let replacetext = document.querySelector('#replacetext');
let btn = document.querySelector('button');
let elem = document.querySelector('p');

//ボタンを押したあと起こるイベントを書く
btn.addEvebtListener('click', ()=>{

//置き換えに必要な要素をvalueで出力し変数に入れていく
let findtxt = findinput.value;
let reptxt = replacetext.value;
let tagtext = textarea.value;

//変数findtextに、全ての文字の中から検索した「変数findtext」を代入
findtext = new RegExp(findtxt, 'g');

//変数tagtext(元文を取得したもの)に、「変数tagtext」の中の「変数findtxt」を「変数reptxt」に置換したものを、代入する
tagtext = tagtext.replace(findtxt, reptxt);

//代入によって更新された変数tagtxtをelementとして獲得したHTMLのp要素に表示させる
elem.innerText = tagtxt;
});
【N予備校 三章「クロスサイトスプリンティング(XSS)脆弱性の対策」】

HTMLに埋め込まれたJavaScriptでWebサービスの内容が勝手に書き換えられるリスクの対策。

自作の掲示板に書き込む際に、以下のコードを埋め込むと、<\h2>を使って書いている項目の中で、一番最初のものが「どすこい」に書き換えられる

<script>window.onload=function(){document.getElementsByName('h2')[0].innerHTML= 'どすこい';}<\script>

・開発者向け情報サイトのおすすめ

Mozilla Developer Network
( https://developer.mozilla.org/ja/docs/Web)

位置付け的には、公式ドキュメントを読めるようになるための足掛かりと思えば良さそう。これからの勉強に使います。

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

JavaScriptに於ける闇の魔術と闇の魔術に対する防衛術

prototypeはね...死人がでるよね。

闇の魔術

自分のPCに閉じ込めておく分には、まぁ楽で良いんだけれども。

俗に言う黒魔術
const __BASE_PROTO = {
    $: {
        value (...child) {
            // 略
            return this;
        } 
    }
}
{
    Object.defineProperties(Node.prototype, __BASE_PROTO);
    Object.defineProperties(Element.prototype, __BASE_PROTO);

}
function GOD(v) {
    this.v = v;
    // 略
};
Object.assign(GOD.prototype, Object.prototype, Array.prototype, Number.prototype, String.prototype);

ただ、後で面倒になる。

闇の魔術に対する防衛術

参考にしたライブラリはある $(dsand).$()
で、自分はこの防衛術をつかって SVGをグリグリ書いて、うりうり動かすライブラリを書いている svg.js

俗に言う白魔術
const __BASE_PROTO__ = {
    // '@': elem,
    '@parse' : {
        value (arr) {return arr.filter(v => null != v).map(v => '@' in v ? v['@'] : v)}
    },
    _ : {
        value () {return this['@'];},
    },
    has: {
        value (prop) {return prop in this["@"];}
    },
    $: {
        value(...child) {
            // 略
            return this;
        },
    },
    // 略
}
const _ = function(el, custom = '') {
    return Object.create(
        { '@': el },
        custom instanceof Object
            ? Object.assign({}, __BASE_PROTO__, custom)
            : __BASE_PROTO__);
};
const _tag = tag => document.createElement(tag);
const div = () => _(_tag('div'));
const p = () => _(_tag('p'));
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>foo</title>
    <script src="_.js"></script>
    <script>
    const load = () => {
        _(document.body).$(
            div().$(
                p().$('hello')
            ),
            div().$(
                p().$('world')
            )
        );
    };
    document.addEventListener("DOMContentLoaded", load);
    </script>
</head>
<body>
</body>
</html>
bodyに追加される
<div><p>hello</p></div>
<div><p>world</p></div>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascript基本集

Javascript基本集

〜自分の学習用です〜

javascriptの基本を振り返ろうと思い作成しました。

javascriptとは

人間がプログラム言語で記述したソースコード(設計図)をインタープリタと呼ばれるソフトウェアによって、コンピューターが実行できる形式に変換しつつ逐次実行するタイプのプログラム言語(軽量なインタプリタ型)。
あるいは実行時コンパイルされる、第一級関数を備えたプログラミング言語
成熟した動的プログラミング言語であり、HTML文書に適用するとWebサイトに動的な対話操作を提供出来る。

  
インタプリタ言語とは、
コードを実行する際に1行ずつ機械語に翻訳していく言語
コンパイラ言語は始めに全コードを機械語に翻訳した後に一気に実行する言語。
  

第一級関数とは、
関数を第一級オブジェクトとして扱うことのできる性質またはその関数のこと。
  

第一級オブジェクトとは、
無名のリテラルとして表現可能な値で、
変数への格納や引数や戻り値として受け渡しが行えるようなオブジェクトのこと。

例)一般的な整数型のリテラル値(0, 99, -1)や論理型の値(true, false)
  文字列リテラルの値("abc")、配列リテラル([1, 2])、オブジェクトリテラル{key: 'val'}

関数を変数へ代入

JavaScriptの変数宣言方法は3種類。

スクリプトの中で変数を利用するにはまず最初に使用する変数を宣言しなくてはならない。 

var
変数を宣言し、ある値に初期化することもできる(繰り返し宣言可)。
let
ブロックスコープのローカル変数を宣言し、ある値に初期化することもできる。
const
読み取り専用の名前付き定数を宣言する(上書き不可)。

JavaScriptの関数や無名関数は第一級関数として扱うことができる。
行末のセミコロンは文が終わる場所を示す。

const foo = function() {
   console.log("aaa");
}

//変数を使用して呼び出し(aaaが出力される) 
foo();  
// 右側がコメントアウトされる。

/*
挟まれているすべてがコメントアウト。
*/

引数として関数を渡す場合

sayHello() 関数を greeting() 関数の引数として渡しており、関数を変数として扱っている

function sayHello() {
   return "Hello, ";
}
function greeting(helloMessage, name) {
  console.log(helloMessage() + name);
}
// `sayHello` を `greeting` 関数の引数として渡す
greeting(sayHello, "JavaScript!");

関数を返す場合

下記では、関数を他の関数から返す必要がある。
関数を返すことができるのは、 JavaScript では関数を値として扱っているためである。

function sayHello() {
   return function() {
      console.log("Hello!");
   }
}

関数の種類

ES6(ES2015)で追加されたアロー関数や無名関数などが存在する。

// 関数の実行方法

オブジェクト. 関数名();

従来の関数

function yesnameFunc() {
    alert( yesname.innerHTML );
}

/*
 関数名に引数のカッコを付けて書く。
 ただし、下記の様にイベントリスナから読み込んで実行する場合は引数のカッコを付けない
*/

yesname.addEventListener( 'click', yesnameFunc, false );

無名関数

const nonameFunc = function() {
    alert( noname.innerHTML );
}

/*
変数に関数を代入。これも同じ動き。
変数名=関数名になる。
その場限りで(再利用しない)で行われる処理に多用する。
*/

アロー関数(基本形)

// アロー関数1(Functionをアローに)

const arrow1Func = () => {
    alert( arrow1.innerHTML );
 }

/*
function() が ()=> になり、引数のカッコと順番が入れ替わる。
functionとアロー=>は順番が逆になる。
ブロック省略(処理が1行)
処理が1行の時はブロックが省略できる。
*/

アロー関数2(処理が1行:ブロックを省略)

const arrow2Func = () => alert( arrow2.innerHTML );

/*
引数のカッコ省略(引数が1つ)
さらに引数が1つだと引数のカッコも省略できる。
つまり関数の中の処理が1行のみの場合はブロックの波カッコ{ }を省略可能。
*/

アロー関数3(引数が1つ:カッコも省略)

const arrow3Func = text => alert(text);

/*
引数が1つだけの時は引数を囲うカッコまで省略可能。
(引数が0個または複数個の場合はカッコを省略不可)
引数付きの関数をイベントリスナで読み込む時には引数無しの無名関数でラップすることも注意
引数ありの関数の実行は下記の様に記述。
*/

//arrow3に表示したいテキスト
const arrow3Text = arrow3.innerHTML;

//関数を実行
オブジェクト.arrow3Func(arrow3Text);

演算子

演算子 説明 シンボル
追加
連結
2つの数字を加えるか2つの文字列を結合する。 + 6 + 9; Hello + "world!";
減算
乗算
除算
基本的な数学の計算を実施する。 -
*
/
9 - 3
8 * 2
9 / 3;
代入 変数に値を割り当てる。 = var myVariable = 'Bob';
等価 2 つの値と型が互いに等しいかどうかを調べ、 true / false (真偽値)の結果を返す。 === var myVariable = 3; myVariable === 4;
否定
非等価
その後にあるものと論理的に反対値を返す。
例えばtrueをfalseに返す。
等価演算子と一緒に使用されると、否定演算子は2つの値が等しくないかどうかをテストする。
!
!==
Notの場合、基本式はtrue
それを否定しているので比較結果はfalse。

var myVariable = 3;
!(myVariable === 3)

「等しくない」は、基本的に同じ結果を異なる構文で与える。
ここでは「myVariableが3とは等しくないことをテストする。
myVariableは3と等しいのでfalseを返す。

var myVariable = 3
myVariable !== 3

但し、「+」は文字列として数字を連携出来るが(例: 10 + "1" = 101 )、
「-」は連結せずに計算式となる(例: 10 - "1" = 9 )。

条件文

条件文は、ある式が true を返すかどうかをテストし、その結果次第でそれぞれのコードを実行するコード構造。条件文のよくある形は if ... else 文。

var s = 5;
var z = 2;
if ( s + z === 7) {
  alert('ご名答!');    
} else {
  alert('ブッブー!');    
}

ループ文

①for文

for ([初期化式(変数の初期化)], [条件式(継続する条件)], [加算式(変数の増減)])
  ループ処理文(繰り返す処理の内容)

1.もしあれば、初期化式が実行される。
 この式は通常、1個またはそれ以上のループカウンタを初期化するが、
 この構文ではいかなるレベルの複雑な式を入れることが可能。
 初期化式で変数を宣言することも可能。
2.条件式が評価される。
 条件式の値がtrueの場合、ループ文が実行される。
 条件式の値がfalseの場合、forループは終了する。
 条件式がすべて省略されている場合、条件式は真であると仮定される。
3.処理が実行される。
 複数の文を実行するには、それらの文をグループ化するためにブロック文 ({ ... }) を使用する。
 もしあれば、更新式 加算式が実行される。
4.上記2に制御が戻る。

例)1回目〜1000回目までを繰り返して表示する処理
1001回目以降は継続する条件に当てはまらないのでfalseとなり処理が中断される。

for(var i = 1, i <= 1000, i++) {
  document.writeln(i + "回目")
}

break

10回繰り返す処理であるが、変数iが7であればループを抜ける。

for (var i = 0; i < 10; i ++) {
    if (i == 7) {
        // 変数『i』が『7』ならループを抜ける
        document.write(i + "finish!<br>");
        break; 
    }
    document.write(i + "<br>");
}
【結果】

0
1
2
3
4
5
6
7 //finish!

continue

変数iが7なら、『★2』を処理せず『★1』に戻る。

for (var i = 0; i < 10; i ++) {        // ★1
    if (i == 7) {

        document.write("skip!<br>");
        continue;
    }

    document.write(i + "<br>");// ★2
}
【結果】

0
1
2
3
4
5
6
skip!
8
9

②do...while 文

do
  ループ処理文(繰り返す処理の内容)
while (条件式);

例)変数iが5未満でなくなる直前まで繰り返しされる。

var i = 0;
do {
  i += 1;
  console.log(i);
} while (i < 5);

③while 文

while (条件式)
  ループ処理文(繰り返す処理の内容)

例)whileループは、nが3未満の場合繰り返される。

let n = 0;
let x = 0;
while (n < 3) {
  n++;
  x += n;
}

処理内容としては
1 回目の反復後 : n = 1, x = 1
2 回目の反復後 : n = 2, x = 3
3 回目の反復後 : n = 3, x = 6

ひとまずここまで:santa:

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

JavaScriptで西暦と平年orうるう年を出力する

初投稿です。最近、JavaScriptなどの学習を始めました。優しくしてください。
先日JavaScriptを使ったうるう年のプログラムを学習したところですが、最近覚えたfor文と組み合わせて、
西暦と一緒にうるう年かどうかをhtml上で西暦○○年:平年ですorうるう年ですとブラウザに出力するプログラムを書いてみようと考えました。
0年から2020年までずらーっと並べます。

コード内の下記の配列は連番の数字の配列を作成(es2015 ver)
を参考にさせていただきました。
0から2020までの数値を変数に入れるつもりでした。

const arr = [...Array((2020) + 1).keys()]

コード

  • htmlの<script>タグ内に記述し、 document.write();で出力しています。
  • uruu(year)がうるう年を判定する関数です。引数に応じてうるう年か平年かを判定してresultに渡します。
<!DOCTYPE html>
<html lang="ja" dir="ltr">
<head>
    <meta charset="UTF-8">
    <title>うるう年出力</title>
</head>
<body>
    <script>
        function uruu(year) {
            if (year % 4 == 0) {
                if (year % 100 != 0 || year % 400 == 0) {
                    result = 'うるう年です';
                }
            } else {
                result = '平年です';
            }
        }

        const arr = [...Array((2020) + 1).keys()]
        let result = "";

        for (let i = 0; i < arr.length; i++) {
            uruu(arr[i])
            document.write('西暦' + arr[i] + '年:' + result);
            document.write('<br>');
        }
    </script>
</body>
</html>

結果

西暦0年から2020年までをブラウザに出力出来ました。(0年は本来無いらしいですが…)
長いので最初と最後を。
FireShot Capture 006 - うるう年出力 - .png
FireShot Capture 005 - うるう年出力 - .png
これだけです。

所感

  • 全体的にもっと短くきれいに書けるはず。あまりにも無知。

  • 下記の箇所はfor文をどうにかして、配列の箇所を...Array(2020)と簡潔に出来そうな気がするのですが、西暦2020の所が西暦undefinedとなってしまうので現状の記述にしています。

const arr = [...Array((2020) + 1).keys()]
for (let i = 0; i < arr.length; i++) {

最後に

今後はもっと難しいのを書けるようになりたい。

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

【SpecTest GUI】MonacoEditor + Vue.js/Electron

SpecTest GUI ヘの道(1)

誰向け?

  • VSCode で使われている Monaco Editor に興味ある人
  • Monaco Editor で Markdown Editor を Electron ベースで 作りたい人
  • SpecTest を 応援してくれる

尚、今回の結果は以下にコミットしてあります。

はじめに

SpecTest は私が欲しいと思っていた BDD を実現するための汎用フレームワーク。

  • SpecTest そのものについては ここ を参照してください。
  • リポジトリは ここ です。

今回はいつもと趣向を変えて、SpecTest GUI への道 と題して GUI 作っていきます。Kinx と両方並行して進めます。どっちかと言うとこっちがサイド・プロジェクト的な。

Markdown Editor ベース

私は普段 Virtual Studio Code にお世話になっているのだが、Markdown メモには Joplin を使っている。なので、Markdown エディタが別にあること自体に苦はないし、便利だと思う。色々 VSCode だけで行けるほうがいいとも思うが、ニッチに特化したツールはそれなりに存在意義があるし。

SpecTest も基本はマークダウンなので、Markdown Editor 的な何かがベースになると良いかなー。ということで、ちょっとした Markdown Editor ベースでいきます。結構長くなりそうなので、記事は分割します。今回は、簡易 Markdown Editor を作るところまで。最初は MavonEditor が良さそうだと思ったが、エディタ部分のフォントが変えられず、融通の利かなさでパスすることに。

簡単な Markdown Editor のサンプルにはなると思う。とは言っても今回の目玉、前々から使ってみたかった Monaco Editor を使うので意味はある。

そう、アレです。Virtual Studio Code で使われているアレです。実は、当初エディタは Atom を使おうと思っていたのだが、動作が重い...。そこで Visual Studio Code にしてみたところすこぶる軽快。しかし、どちらも Electron ベースだというではないか。 この違いは一体何だ?、と行きついたのが Monaco Editor。これは期待できる。

Monaco Editor の使い方や、Monaco Editor を使いたい人に参考になれば。

準備

node.js インストール

node.js は大体の人がインストール済みですかね。ここ からインストール。

vuecli インストール、プロジェクトの作成

以前は electron-vue を使っていたのですが、最近は vuecli + electron-builder らしいですね。今回はそれでいきます。

vuecli をインストール。既にインストール済みなら飛ばしてください。

$ npm install -g @vue/cli

プロジェクトの作成。プロジェクト名は spectest-gui にします。

$ vue create spectest-gui

Manually select features を選んで、Router と Vuex を選択します。私はいつもこれです。Router は使わないか...。あとは、TypeScript を使うかどうか。今時は使うべきかもしれないが、時間もないので慣れている JavaScript にしてしまう。

Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project:
 (*) Babel
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support
 (*) Router
>(*) Vuex
 ( ) CSS Pre-processors
 (*) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing

あとはデフォルト。

?  Successfully created project spectest-gui.
?  Get started with the following commands:

 $ cd spectest-gui
 $ npm run serve

上記が出れば成功。

electron-builder

プロジェクトが無事作成されたら、早速 electron-builder 入れてみましょう。

$ cd spectest-gui
$ vue add electron-builder

Choose Electron Version とか聞かれるので迷わず新しいのを。

? Choose Electron Version (Use arrow keys)
  ^4.0.0
  ^5.0.0
> ^6.0.0
✔  Successfully invoked generator for plugin: vue-cli-plugin-electron-builder

成功。

起動してみましょう。

$ npm run electron:serve

Vue

おぉ。

この辺で VSCode とかを立ち上げると、既に git リポジトリができていて、初版がコミットされており、既に変更があることが確認できます。必要に応じて git にコミットしておきましょう(たぶん git コマンドがないとできないと思うけど、git コマンドが無いと無視されるのかどうかとかは既に入っていたのでわかんない)。

$ git add .
$ git commit -m "added electron builder"

Vuetify

やはり流行に乗ってマテリアル・デザインで Vuetify を使います。好きなので。色々揃ってて良いですねえ。

$ vue add vuetify

以下が聞かれますが、とりあえずデフォルトで進めます。

? Choose a preset: (Use arrow keys)
> Default (recommended)
  Prototype (rapid development)
  Configure (advanced)
✔  Successfully invoked generator for plugin: vue-cli-plugin-vuetify
 vuetify  Discord community: https://community.vuetifyjs.com
 vuetify  Github: https://github.com/vuetifyjs/vuetify
 vuetify  Support Vuetify: https://github.com/sponsors/johnleider

成功したようですね。起動してみます。

$ npm run electron:serve

Vuetify

おぉ、変わった。

splitpanes / vue-monaco / marked / highlight.js

そしてお待ちかね、Monaco Editor の出番です。vue-monaco というパッケージがあります。ついでに今回、マルチペインで作業できるようにするつもりなので、splitpanes というライブラリを入れてしまいます。また、表示用に markedhighlight.js も入れます。さらに github-markdown-css も入れておきましょう。

$ npm install splitpanes
$ npm install vue-monaco
$ npm install marked
$ npm install highlight.js
$ npm install github-markdown-css

fontawesome

間違いなく fontawesome のアイコンは使います。入れておきます。

$ npm install @fortawesome/fontawesome-svg-core
$ npm install @fortawesome/free-solid-svg-icons
$ npm install @fortawesome/vue-fontawesome
$ npm install @fortawesome/free-brands-svg-icons
$ npm install @fortawesome/free-regular-svg-icons

色々入ったので、また変更が通知されているはずです。コミットしてしまいましょう。

$ git add .
$ git commit -m "added vuetify and some modules"

簡易 Markdown Editor

さて、Markdown Editor つくりますよ。

アイコンの設定

まず、fontawesome アイコンを使えるようにしてしまいましょう。src/main.js を開いて、import 文の最後の行の次あたりに以下の行を追加。

main.js
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { fab } from '@fortawesome/free-brands-svg-icons'
import { far } from '@fortawesome/free-regular-svg-icons'
library.add(fas, far, fab)

初期画面の整理

ひとまず以下のような感じで構成してきます。

src/App.vue
   /components/MarkdownPane.vue
              /markdown/Editor.vue
              /markdown/VIewer.vue

トップレベル(src/App.vue

Vuetify の <v-app><v-app-bar><v-content> を配置し、<v-content>MarkdownPane を配置します。全体を書くと以下の通り。基本、あったものをざっくり消して、HelloWorldMarkdownPane に変える。

App.vue
<template>
  <v-app>
    <v-app-bar app ref="appbar">
      <v-app-bar-nav-icon></v-app-bar-nav-icon>
      <v-toolbar-title>SpecTest GUI</v-toolbar-title>
    </v-app-bar>

    <v-content>
      <MarkdownPane />
    </v-content>
  </v-app>
</template>

<script>
import MarkdownPane from './components/MarkdownPane';

export default {
  name: 'App',

  components: {
    MarkdownPane,
  },

  data: () => ({
    //
  }),
};
</script>

src/components/MarkdownPane.vue が無いので、まだ起動できない。

ペイン分割(src/components/MarkdownPane.vue

中身のない src/components/MarkdownPane.vue を作ります。splitpanes で左右にペイン分割する。

MarkdownPane.vue
<template>
  <splitpanes class="default-theme" :style="{ height: '100%', overflow: 'hidden' }">   
    <pane class="pane-editor" ref="epane" size="55">
    </pane>
    <pane class="pane-view" ref="vpane">
    </pane>
  </splitpanes>
</template>

<script>
import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'

export default {
  name: 'MarkdownPane',

  components: {
    Splitpanes, Pane,
  },
};
</script>

これで一応動くようにはなる。まだエディタ組み込んでませんが、ペイン分割の動作を確認できます。

Panes

こんな感じ。真ん中のスプリッタでぐりぐりと動かせます。

Window の大きさ調整

Electron を使っているとだいたいそうなのだが、ウィンドウのリサイズに追随してくれないコンポーネントが結構ある。なので、ウィンドウ・サイズを Vuex のストアに確保しておき、各コンポーネントで参照できるようにしておく。具体的には、App.vue にハンドラを設置。その際、上部のアプリケーションバーのサイズを含めないようにあらかじめ引き算しておく。軽く 3 引いているのは、誤差でスクロールバーが出たり変な感じになることがあったので気持ち少なめに程度の意味。

まず、store でウィンドウサイズを保存するように修正。

src/store/index.js
export default new Vuex.Store({
  state: {
    windowSize: { width: 0, height: 0 },
  },
  mutations: {
    setWindowSize (state, appbar) {
      state.windowSize.width = window.innerWidth
      state.windowSize.height = window.innerHeight - appbar.clientHeight - 3
    },
  },
  ...

次に App.vue の <v-app-bar> タグに ref をつけ、バーの高さを渡せるようにした上で、

App.vue
    <v-app-bar app ref="appbar">

methodshandleResize を追加し、ハンドラとして呼ばれるように mountedbeforeDestroy でリスナーに登録し、ウィンドウサイズを store にセットをする。

  methods: {
    handleResize: function() {
      this.$store.commit('setWindowSize', this.$refs.appbar.$el)
    },
  },

  mounted: function () {
    window.addEventListener('resize', this.handleResize)
    this.$store.commit('setWindowSize', this.$refs.appbar.$el)
  },

  beforeDestroy: function () {
    window.removeEventListener('resize', this.handleResize)
  },

テキストデータ共有

テキストエディタで編集したデータは、marked で変換されて表示される。なので、編集ドキュメント自体も store で管理。statecode として追加し、upadteCode でアップデートできるようにしておく。さっきのと合わせるとこんな感じ

src/store/index.js
export default new Vuex.Store({
  state: {
    windowSize: { width: 0, height: 0 },
    code: "",
  },
  mutations: {
    setWindowSize (state, appbar) {
      state.windowSize.width = window.innerWidth
      state.windowSize.height = window.innerHeight - appbar.clientHeight - 3
    },
    updateCode (state, code) {
      state.code = code;
    }
  },
  ...

Markdown エディタの配置

お待ちかねの MonacoEditor を組み込む時間です。

まず、src/components/markdown/Editor.vue を作ります。あらかじめ設定しているのは以下の点。

  • MonacoEditor は自分でレイアウトに追随してくれないので、リサイズで再レイアウトさせる必要がある。そのための仕組みを入れておく。
  • resize 自体は親コンポーネントから受け取る。
  • code はローカルに編集するので this に持たせておき、変更をウォッチして store にコピーする。
  • 縦横サイズはウィンドウサイズをもとに設定してやらないとうまくレイアウトできない。Pane で各ペインの高さをそろえて、その高さに Editor を合わせる。
  • @editorWillMount イベントを受け取って、monaco インスタンスを確保しておく。これは後で使う。
  • fontSize は一応設定値として持っておく。
  • エディタが見やすいように、上下に少しマージンを入れておく。

全部入れると次のようになる。

src/components/markdown/Editor.vue
<template>
  <MonacoEditor ref="editor" v-model="code" language="markdown" class="mdeditor" :style="{ width: width, height: height }"
    :options="{ scrollBeyondLastLine: false, wordWrap: 'on', fontSize: fontSize }"
    @editorWillMount="onEditorWillMount"
  />
</template>

<script>
import MonacoEditor from 'vue-monaco'

export default {
  name: 'MarkdownEditor',
  components: { MonacoEditor },

  data: () => ({
    code: '',
    monaco: null,
    fontSize: 12,
    clientWidth: 1,
    clientHeight: 1,
  }),

  methods: {
    onEditorWillMount: function(monaco) {
      this.monaco = monaco
    },
    resize: function(el) {
      this.clientWidth = el.clientWidth - 1;
      this.clientHeight = el.clientHeight - 3;
      this.$nextTick(() => {
        this.$refs.editor.getEditor().layout()
      })
    },
  },

  computed: {
    width() {
      return this.clientWidth + 'px'
    },
    height() {
      return this.clientHeight + 'px'
    },
  },

  watch: {
    code() {
      this.$store.commit('updateCode', this.code)
    },
  },
};
</script>

<style scoped>
.mdeditor {
  margin-top: 6px;
  margin-bottom: 8px;
}
</style>

また、splitpanes でペインサイズが変わったときも追随してくれない。なので、その仕組みを入れておく。修正すると次のようになる。MarkdownEditor のメソッドを呼ぶので ref をつけておく。

src/components/MarkdownPane.vue
<template>
  <splitpanes class="default-theme" :style="{ height: height, overflow: 'hidden' }" @resized="resizedPane($event)">
    <pane class="pane-editor" ref="epane" size="55">
      <MarkdownEditor ref="editor" />
    </pane>
    <pane class="pane-view" ref="vpane">
    </pane>
  </splitpanes>
</template>

<script>
import MarkdownEditor from './markdown/Editor'
import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'

export default {
  name: 'MarkdownPane',

  components: {
    MarkdownEditor, Splitpanes, Pane,
  },

  methods: {
    resizedPane: function() {
      this.$refs.editor.resize(this.$refs.epane.$el)
    }
  },

  computed: {
    height () {
      return (this.$store.state.windowSize.height - 1) + "px"
    },
  }
};
</script>

リサイズも通知されるように、App.vuehandleResize で通知するように修正。mounted のときも通知しておく。App.vue も全部載せると以下のようになる。

src/App.vue
<template>
  <v-app>
    <v-app-bar app ref="appbar" height="56">
      <v-app-bar-nav-icon></v-app-bar-nav-icon>
      <v-toolbar-title>SpecTest GUI</v-toolbar-title>
    </v-app-bar>

    <v-content>
      <MarkdownPane ref="pane" />
    </v-content>
  </v-app>
</template>

<script>
import MarkdownPane from './components/MarkdownPane';

export default {
  name: 'App',

  components: {
    MarkdownPane,
  },

  data: () => ({
    //
  }),

  methods: {
    handleResize: function() {
      this.$store.commit('setWindowSize', this.$refs.appbar.$el)
      this.$refs.pane.resizedPane()
    },
  },

  mounted: function () {
    window.addEventListener('resize', this.handleResize)
    this.$store.commit('setWindowSize', this.$refs.appbar.$el)
    this.$nextTick(() => {
      this.$refs.pane.resizedPane()
    })
  },

  beforeDestroy: function () {
    window.removeEventListener('resize', this.handleResize)
  },
};
</script>
...

ここまでできると、エディタが使える。

Editor

やった!

VSCode と同じだ。検索も置換もできる。Minimap も付いてる。なんて素晴らしい。

Markdown ビューワの配置

編集できるだけではイマイチですね。ちゃんと HTML に変換して表示しましょう。src/components/markdown/Viewer.vue を作ります。ここでは試行錯誤した結果のみ先に見せます。

src/components/markdown/Viewer.vue
<template>
  <div id="viewer" class="mdviewer" :style="{ width: width, height: height, overflow: 'auto' }" ref="viewer">
    <div v-html="markdown" class="mdviewer-body markdown-body" />
  </div>
</template>

<script>
import marked from 'marked';
import hljs from 'highlight.js';
import 'highlight.js/styles/github-gist.css'
import 'github-markdown-css/github-markdown.css'

export default {
  name: 'MarkdownViewer',

  created: function () {
    marked.setOptions({
      langPrefix: '',
      highlight: function(code, lang) {
        var l = lang.split(':');
        return hljs.highlightAuto(code, [l[0]]).value
      }
    });
  },

  data: () => ({
    clientWidth: 1,
    clientHeight: 1,
  }),

  methods: {
    resize: function(el) {
      this.clientWidth = el.clientWidth - 1;
      this.clientHeight = el.clientHeight - 3;
    },
  },

  computed: {
    markdown: function () {
      return marked(this.$store.state.code) + '<br />'
    },
    width() {
      return this.clientWidth + 'px'
    },
    height() {
      return this.clientHeight + 'px'
    },
  }
}
</script>

<style>
.mdviewer {
  margin-top: 6px;
  margin-bottom: 8px;
}
.mdviewer-body {
  padding: 8px;
}

code {
  font-size: 85% !important;
  padding: 0px !important;
}
pre>code {
  width: 100%;
  padding: 0.5em;
  color: inherit !important;
  -webkit-box-shadow: inset 0 0px 0 #ffffff !important;
  box-shadow: inset 0 0px 0 #ffffff !important;
}
pre>code:after, pre>code:before {
  content: "" !important;
  letter-spacing: 0px !important;
}
</style>

marked で変換する際の highlight.js の設定を created で行い、エディタと同じようにサイズを調整できるようにして、どうも Vuetify が悪さしているっぽいスタイルを調整(グローバルスコープで !important とか使っているが、こうしないと回避できなかった...他に影響あったら考える)。

文書自体は store に変更があると自動的に computed して変換後の文書が表示される。賢いねー。

また、src/component/MarkdownPane.vue に登録してリサイズされるようにメソッドを追加。splitpanesdefault-theme が邪魔な感じなので消してしまう。全体は以下の通り。

src/component/MarkdownPane.vue
<template>
  <splitpanes :style="{ height: height, overflow: 'hidden' }" @resized="resizedPane($event)">
    <pane class="pane-editor" ref="epane" size="55">
      <MarkdownEditor ref="editor" />
    </pane>
    <pane class="pane-view" ref="vpane">
      <MarkdownViewer ref="viewer" />
    </pane>
  </splitpanes>
</template>

<script>
import MarkdownEditor from './markdown/Editor'
import MarkdownViewer from './markdown/Viewer'
import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'

export default {
  name: 'MarkdownPane',

  components: {
    MarkdownEditor, MarkdownViewer, Splitpanes, Pane,
  },

  methods: {
    resizedPane: function() {
      this.$nextTick(() => {
        this.$refs.editor.resize(this.$refs.epane.$el)
        this.$refs.viewer.resize(this.$refs.vpane.$el)
      })
    }
  },

  computed: {
    height () {
      return (this.$store.state.windowSize.height - 1) + "px"
    },
  }
};
</script>

Viewer

おぉー。エディタだ。

おわりに

ここまでで簡単な Markdown Editor ができました。保存とかできないけど。あと、やはりスクロールは連動してほしい。

ということで、次回は スクロール連動 を実現させます。

ここまでの結果は、以下にコミットしてあります。

SpecTest そのものに関しては以下を参照してください。

ではまた次回。

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

【学習記録】『スラスラ読める JavaScriptふりがなプログラミング』読書感想  4章編 (後半) 〜N予備校の復習を添えて〜

【学習記録】『スラスラ読める JavaScriptふりがなプログラミング』読書感想  4章編(前半)(https://qiita.com/Molly95554907/items/fbadbec14c4b02c4ee3a ) の続き

・戻り値を返す関数

つまり、ただ処理を実行したり、引数を表示したりするのではなく、何か計算をしてその答えを返す関数。
戻り値=関数の答え

let 関数名 = () => {
//関数内で実行する文
return 結果の値;
};


関数名();
を実行すると、 

結果の値 

が出力される。

例)

let square = (number) =>{
return number * number;
};
console.log(square( 3 ));


9が出力される(戻り値)

・オブジェクトを用いて複数のデータを一つの変数にまとめる

重要項目。つまるところオブジェクトって何なんだろ………

オブジェクトを、複数のデータを記録する入れ物として使う。

変数 = { プロパティ1 : 値a, プロパティ2 : 値b }

オブジェクトからプロパティを取り出す際は、
「変数.プロパティ1」と書くと、値a が取り出せる。
変数['プロパティ1']と書いても、値a が取り出せる。

例)

let myfavoritebooks = { title:'1984', author: 'George Orwell', publicationYear: 1949, pages: 328}; 

console.log( myfavoritebooks.title );   //1984と出力(値が出る)
console.log( myfavoritebooks.pages );   //328と出力(値が出る)

※オブジェクトを表のタイトル、プロパティを表の項目、値を表の中身と思うと理解しやすい!!

・配列の中にオブジェクトを入れる

先ほどのオブジェクトは、
「好きな本」オブジェクトの中に{「タイトル」プロパティ、「著者」プロパティ、「出版年」プロパティ、「ページ数」プロパティ}が入っていた。

→好きな本を複数「好きな本」オブジェクトに入れるには…
オブジェクトを入れた配列を作る!

let myfavoritebooks = [
{ title:'1984', author: 'George Orwell', publicationYear: 1949, pages: 328},
{ title:'コンビニ人間', author: '村田沙耶香', publicationYear: 2016, pages: 160}
];

console.log( myfovoritebooks[0].author )  //George Orwell が出力
console.log( myfovoritebooks[1].title )   //コンビニ人間 が出力
【N予備校 三章「集計処理を行うプログラム」】

スクリーンショット 2020-03-24 0.35.02.png

ここでは、4つのプロパティ(集計年,都道府県名,10〜14歳の人口,15〜19歳の人口)を持つCSVのデータストリングを「,」で区切り、
columnsという変数の箱の中に、配列としてデータを格納していく。

CSVの最初のプロパティの値(columns[0])を整数化し、変数yearに代入する。
CSVの二つ目のプロパティの値(columns[1])を変数prefectureに代入する。
CSVの四つ目のプロパティの値(column[3])を整数化し、変数popuに代入する。

・関数を組み合わせる

「関数定義」、「データ」、「関数呼び出し」を組み合わせてプログラムを作る。
一つのファイルに書くとプログラムが長くなるので、複数のファイルに分割すると良い。

例)
『スラスラ読める JavaScriptふりがなプログラミング』P149のコードを利用

関数定義

//メールを作る関数
let createMail = (recv, bill) = >{
let mag = `${recv}様
PT企画の斎藤です。
今月の請求額は${bil}円です。`
console.log(msg)
};


引数を二つ持つ関数

//手数料を追加する関数
let addCharge = (bill) => {
return bill * 1.07;
};



返り値を持つ関数

<h6>データを貼り付ける</h6>
//送付先データ
let data = [
{name:'山本', bill:40000, crg:true } ,
{name:'吉田', bill:25000, crg:false }
];


配列の中にオブジェクトを入れる。オブジェクトの中のプロパティは、本文に貼り付けたい順に入れている。

//メール作成実行
for(let rec of data){
let bill = rec['bill']
if(rec(['crg']){
bill = addCharge(bill);
}
createMail(rec['name'],bill);
}


data配列の中のオブジェクトをひとつづつ、変数recに入れていく。
変数recの中のbillプロパティを持った値を、変数billとする。
もし変数recの中のcrgプロパティーを持った値がtrueなら、
変数billに、関数addCharge(bill)の戻り値を代入する。
createMail関数を実行する。引数には変数recの中のnameプロパティを持った値と、変数billをとる。
疑問点

・疑問①
javaScriptは関数が上から下に実行される性質を持つんだよね?
なのに、プログラムの最初に登場するbillは、
プログラムの最後にlet billで定義されるってどういうことなんだろう。

・疑問②
関数内で作った変数は「ローカル変数」
関数外では呼び出せないんだよね?
なのに。for(let rec of data){}
の繰り返し文の中で作ったbill変数が、プログラムの最初の
let createMail = (recv, bill) =>{}
で呼び出されているのは何故だろう。

・関数の呼び出し

作った関数は、

module.export = {
関数名,
};

を使うと、同じフォルダ(ディレクトリ)の別ファイルからもこの関数が参照できるようになる。

【N予備校では、三章「秘密の掲示板作り」などで登場】

スクリーンショット 2020-03-22 12.12.39.png

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

[Node.js勉強会]ExpressフレームワークでTODOアプリを作ろう

この記事について

Node.jsの概念や、Expressの使い方について詳しく書いてある記事ではありません。
ある程度の土台が用意してあるので、それを元に実際に手を動かして、TODOアプリを完成させることがこの記事の目的です。

pugファイルと仮処理を記述 の部分までは、コピペで進めていただいても大丈夫です。
TODOの処理を記述 の部分から実際に、Node.jsの様々な書き方を試していただけたらと考えております

今回作成する、TODOアプリの動作は以下のような形です。
express-comp.mov.gif

Node.jsとは

Node.js はスケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動の JavaScript 環境です。
Node.js公式

非同期で処理が実行されるので、DBとの通信のような時間がかかる処理を実行する場合は async, await の記述が必要です。

Expressとは

Node.js のための高速で、革新的な、最小限のWebフレームワーク
Express公式

最小限の機能を持ったフレームワークです。基本的に利用するのは、ルーティングの機能かと思います。
LaravelやRailsのようなフレームワークの場合、プロジェクトを作成した時点でログイン機能、メール機能、など様々な機能が簡単に実装出来るように用意されていますが、Expressは全て自分でライブラリを選定して実装しないといけません。
そのため、フレームワーク自体の容量がとても小さいです。

事前準備

・ Git
・ Docker

手順1 dockerを利用して、Expressの実行環境を整える

スムーズにNode.jsの学習を進めるために、開発環境の構築をDockerで行います。

$ git clone https://github.com/KazukiSadasue/express-mysql-sample.git
$ cd express-mysql-sample
$ docker-compose up -d

上記のコマンドで、dockerコンテナが起動して、サーバーも立ち上がります。
http://localhost
に接続して、以下の画像のように表示されたら実行環境は整いました。
*もしかしたら、最初の1回目にサーバーの立ち上げ失敗するかもしれません。
その場合は、docker-compose restart で再度立ち上げると治ると思います。

express-first-step.png

手順2 作成するアプリケーションについて

簡単なTODOアプリケーションを作成します。
ルーティングは以下のようにしようと思います。

パス メソッド 内容
/ GET TODOトップ
/create POST TODO作成
/done POST TODO完了
/delete POST TODO削除

TODOテーブルは以下のようにします。

カラム 説明
id INTEGER PRIMARYキー
content STRING TODO内容(必須)
isDone Boolean TODOの状態(必須、デフォルト0)
deletedAt DATETIME 削除日時
createdAt DATETIME 作成日時
updatedAt DATETIME 更新日時

モデルの作成

テーブル設計から、TODOモデルを作成します。
modelsフォルダにtodo.jsを作成して、中身は以下のようにします。


todo.jsのコード
todo.js
'use strict';
const loader = require('./sequelize-loader');
const Sequelize = loader.Sequelize;

const Todo = loader.database.define(
  'todos',
  {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true,
      autoIncrement: true,
      allowNull: false,
    },
    task: {
      type: Sequelize.STRING,
      allowNull: false,
    },
    isDone: {
      type: Sequelize.BOOLEAN,
      allowNull: false,
      defaultValue: 0,
    },
  }, {
    paranoid: true,
    freezeTableName: true,
  }
);

module.exports = Todo;


次にapp.jsでモデルを読み込みます。

app.js
var logger = require('morgan');
var helmet = require('helmet');

// モデルの読み込み
+var Todo = require('./models/todo');
+Todo.sync();

モデルの作成が終わったら、Dockerコンテナを再度立ち上げます。

$ docker-compose restart

サーバーが再起動して、モデルに対応するテーブルが作成されます。
テーブルを確認するには、以下のコマンドを実行

$ docker exec -it mysql bash
# mysql -u root -p
Enter Password: パスワードは入力せずにEnter

mysql> use express-sample
mysql> show tables;
+--------------------------+
| Tables_in_express-sample |
+--------------------------+
| todos                    |
+--------------------------+

todosテーブルが作成されているのが確認出来ました!

pugファイルと仮処理を記述

TODOアプリのデザインはBootstrapを用いて作成しました。
以下のコードをコピペして配置してください。


views/index.pugのコード
views/index.pug
extends layout

block content
  h1 TODOアプリ
  div
  form(action="/create", method="post").form-inline.mb-3
    div.form-group
      input(type="text" name="task" placeholder="例: 晩御飯を作る").form-control
    button(type="submit").btn.btn-primary 追加

  div.mb-3
    h2 TODO
    form(name="todoform" method="post").mb-2
      ul.list-group
      each todo in todoTasks
        li.list-group-item.px-5
          input(name="todos", value=todo.id, type="checkbox", id=`todo-${todo.id}`).form-check-input
          label(for=`todo-${todo.id}`).form-check-label #{todo.task}
    if todoTasks.length
      button.btn.btn-success(onclick="done()").mr-2 達成
      button.btn.btn-danger(onclick="deleteTodo()") 削除
    else
      div TODOタスクはありません


  div
    h2 DONE
    form(name="doneform" method="post").mb-2
      ul.list-group
      each done in doneTasks
        li.list-group-item.px-5
          input(name="todos", value=done.id, type="checkbox", id=`todo-${done.id}`).form-check-input
          label(for=`todo-${done.id}`).form-check-label #{done.task}
    if doneTasks.length
      button.btn.btn-danger(onclick="deleteDone()") 削除
    else
      div DONEタスクはありません

  script(src="javascripts/todo.js")



puglic/javascripts/todo.jsのコード
public/javascripts/todo.js
function done() {
  document.todoform.action="/done";
  document.todoform.submit();
}

function deleteTodo() {
  document.todoform.action="/delete";
  document.todoform.submit();
}

function deleteDone() {
  document.doneform.action="/delete";
  document.doneform.submit();
}



routes/index.jsのコード
routes/index.js
var express = require('express');
var router = express.Router();

// TODOトップページ
router.get('/', async (req, res, next) => {
  const todoTasks = [];
  const doneTasks = []

  res.render('index', {
    todoTasks, doneTasks,
  });
});

module.exports = router;


コードの記述が終わったら、 docker-compose restart コマンドでコンテナを立ち上げなおします。

すると、以下のような状態になるかと思います。
express-second-step.png

TODOの処理を記述

ここまで見た目の部分と、モデルの定義まで終わりました。
あとは、routes/index.js の部分に処理を書き加えるだけでTODOアプリが完成します。
ここから先は、色々と手を動かしてNode.jsの書き方に触れていただけたらと思います。

基本的な使い方

// ライブラリや、モデルの読み込みには、requireを利用する
const { Op } = require('sequelize');

// getのルーティング
router.get('/', () => {});
// postのルーティング
router.post('/', () => {});

Sequelizeの使い方

モデルの使い方は、 Sequelize公式ドキュメント を参考にします。
使い方の一部を抜粋してこちらに記述します。

// async を記述することで、非同期関数を定義する。
router.get('/', async (req, res, next) => {
  // isDoneがtrueのデータを全件取得
  // awaitを記述することで、DB処理の結果が返ってくるまで待つ
  const todos = await Todo.findAll();
})

Sequelizeの使う上で、Node.jsは非同期通信であることに注意しないといけません。
async, awaitを利用して、DB処理が終わってから次の処理を行うようにしないと、不具合の原因となってしまいます。

上記の内容を参考に、TODOアプリを完成させていただけたらと思います。

最後に

TODOアプリ完成しましたでしょうか?
最終的な私の実装例を記述致します。


routes/index.jsの実装例
routes/index.js
var express = require('express');
var router = express.Router();
const Todo = require('../models/todo');
const { Op } = require('sequelize');

// TODOトップページ
router.get('/', async (req, res, next) => {
  const todos = await Todo.findAll();
  const todoTasks = todos.filter(todo => todo.isDone === false);
  const doneTasks = todos.filter(todo => todo.isDone === true);

  res.render('index', {
    todoTasks, doneTasks,
  });
});

// TODO作成
router.post('/create', async (req, res, next) => {
  await Todo.create({
    task: req.body.task,
  });
  res.redirect('/');
});

// TODO完了
router.post('/done', async (req, res, next) => {
  const todoModels = await getModelsByIds(req.body.todos);
  for(const todo of todoModels) {
    await todo.update({
      isDone: true
    });
  }
  res.redirect('/');
});

// TODO削除
router.post('/delete', async (req, res, next) => {
  const todoModels = await getModelsByIds(req.body.todos);
  for(const todo of todoModels) {
    await todo.destroy();
  }
  res.redirect('/');
});

// リクエストのIDからTODOモデルを取得する
async function getModelsByIds(ids) {
  if (typeof ids === 'string') {
    ids = ids.split();
  }
  return await Todo.findAll({
    where: {
      id: {
        [Op.in]: ids,
      }
    }
  });
}

module.exports = router;


もっと説明の記述が必要、xxが分からない等ありましたらコメントお願い致します!

今回のコードの配置場所
https://github.com/KazukiSadasue/express-mysql-sample

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

プログラミングを技術目線ではなく、もたらす結果をモチベーションにコードを書いた、プログラミング初心者の末路(僕パターン)

前置き

まずこの記事の対象者は"非エンジニア"です。

非エンジニアである僕が、プログラミングスキルを身につけることを目指し、その過程でどんな取り組みをしたかを説明するしょーもない記事です。

ちなみに今はプログラマーとしてやっています、というオチはなく今も勉強中ですし、そもそも勉強の一貫でこうして記事を書いてあるので、突然カバディ部の顧問にさせられた教師が、そのスポーツのルールを覚えるのと並行して部員にカバディを教えるのと同じような図になると思います。

それでもよければ記事をご覧いただき、「あ、自分が思ってた不満って他の人も感じてたのか」みたいな気持ちになってもらえればいいなと思います。

第一回 『Googleカレンダーの内容を LINE に飛ばしてくれるBOT作りたい』

僕はフリーのカメラマンとしてお仕事をすることがあるのですが、よく「空いているスケジュールはいつでしょうか?」とクライアント様からご相談いただくことがあります。

僕としては、いくつか候補日を用意していただけると嬉しいなと思いつつ「この日この時間と、この日この時間なら空いていますよ!」と回答しています。

が、とうとう面倒くさくなってしまいました、、、
というのも僕は勤めている会社の文化で「自分のスケジュールは公私関係なく、移動時間も含めて全てGoogleカレンダーに記載しているからです」

変わった会社ですよね〜。でもおかげで会議のスケジュール決めたりとかはスムーズに話せます。みんなのGoogleカレンダー見て、空いてる時間帯にスケジュールを飛すだけなので。

さて、この文化をフリーのお仕事でも生かしたいと思ったのがタイトルのきっかけです!

「Googleカレンダー共有すればいいだけじゃん?」とか

「Googleカレンダースクショしたの送ればいいじゃん?」って

思った方おられるかもしれませんが、それだとイマイチなんですよ。

だって「スケジュール教えてください!」って聞いた時、カレンダーだけ渡されるとイラッとしませんか?何様だよと。
なんというか、需要と供給の輪郭がはっきり出過ぎるんですよね。(意味不)

LINE Bot であれば「今週のスケジュール」みたいなラフな聞き方ができていいと思うんですよね。気を使わなくていいし。

だから僕が求めたのは
クライアントになりそうな方に、"僕のスケジュールを言うBot"を LINE に友達登録してもらい、お仕事が発生しそうになったら、その Bot にスケジュールを聞いてもらう
という一連のプロセスを確立することでした。

ちょうど通っているプロトアウトスタジオ (https://protoout.studio/) というプログラミングスクール?で、 API についての授業を受けたので、うまいことできないかなと手を動かしたわけです。


やったこと

 1.有料記事を購入した。

 2.記事通りやっているが動作せず、職場のプログラマーに聞いた

 3.結局動かず teratail で質問してみた

 4.teratail で基礎から学べと言われ、書籍を購入してちょっと読んだ

 5.しかし有料記事進めれず、結局簡単にできそうな無料の記事みてプログラム作った

 6.それは動いて少し嬉しかった。←イマココ

1.有料記事を購入した。
URL は貼りませんが、note で公開されていた記事で『LINE Botを使ってGoogleカレンダーの予定を確認したり、予定を追加できるようにしましょう』という内容のものでした。

「これだよ!欲しかったのは!!」

と有料でしたが割とあっさり購入し、早速手を動かし始めました。
が、基本を押さえていない僕の"ソースコードコピぺ術"ではすぐに限界がきてしまいました。

ちなみに、基礎を押さえていない僕がなんとかコードを理解しようと試した手法は、似た結果をもたらす(今回で言うと LINE Bot でGoogleカレンダーを操作すること)、他の記事のソースコードを探し、メインのコードとの類似点を見つけて、他の記事のコメントアウトの内容をメインのコードにも置き換えて見てみるというものでした。

意味がわからないって?

要は、”中国語わからなくても、「なんか日本語にも同じ漢字があるぞ」って気づけば、そこから中国語の内容を推測できる"みたいなのに近いです。

結果...
2.記事通りやっているが動作せず、職場のプログラマーに聞いた

動かせませんでした。やはり意味がわかりませんでした。
なので、めちゃくちゃ忙しい職場のプログラマーを捕まえて聞いてみたところ、部分的にではありますが、つまずいていたところを教えてくれました。
本当はもっと聞きたかったのですが、彼は何処かへ去っていってしまいました。

そして、部分的に教えてもらった内容は記事の内容と異なっており、記事の内容を進めていくには、記事通りのコードを書いて先に進まないと結局動かなくなることが判明したため、いよいよ万策尽きてきました。ここまでで6時間くらい使ってます。(どうでもいい)

3.結局動かず teratail で質問してみた

teratail という、"プログラマー向けYahoo知恵袋"のようなサービスがあることを知り、淡い期待で投稿してみました。
大体、投稿して30分くらいで反応をもらえたのはよかったですね。

実際の投稿記事

https://teratail.com/questions/248595?modal=q-comp

質問のやりとりから僕の初心者度合が伺えたかと思います。

さて、親切に対応してもらえましたが、教えていただいた内容を実行することができない僕の未熟さがバレてしまい、早々に基礎の学習を勧めていただきました。

4.teratail で基礎から学べと言われ、書籍を購入してちょっと読んだ

download.jpg
https://www.amazon.co.jp/dp/4295002089/ref=cm_sw_em_r_mt_dp_U_5RWEEbE1TKTNM

買って読みました。前半のみ。内容はわかりやすかったです。
(なぜ Python の本買ってんだ、と思われるかもしれませんが、僕が有料記事で困っていた内容は Python の記述だったからです)

この辺りでなんとなく「有料記事、コードの記述足りてないところあるかも??」と思いました。

というのも、これまでの過程で「全く理解できない」という訳ではなくなったからです。記述の意味もなんとなくわかるようになり、そこで気づいたのが実行に必要な関数の指定が記事で書かれていないということです。(知ったような口を聞いています。よくわかってません)

まぁでもそんな状況になり、いよいよ疲れてきました。

5.しかし有料記事進めれず、結局簡単にできそうな無料の記事みてプログラム作った

原因はわかりつつありましたが、これまでに時間をかけすぎて、心も体も消耗していました。

「このままでは忙しさにかまけて手を動かさなくなるな」と思ったので、自分がやりたいと思っていた機能をガッツリ削り"LINE Bot にGoogleカレンダーのスケジュールが飛んでくる"というプログラムがかければ良しとし、改めて記事探しの旅に出ました。

そしていい感じの記事を見つけました。↓
https://qiita.com/imajoriri/items/e211547438967827661f

GAS(Google Apps Script) と LINE Notify を触るだけでよかったので、なんとか動かすところまで進めました。(コピぺだけど)

6.それは動いて少し嬉しかった。←イマココ

プログラムが動くとすごく嬉しいということがわかりました。たとえコピぺでも。

"自分の言うことを聞いてくれた"という感覚が確かにあり、なんとも癖になる感じでした。


無理やり話のオチ

今回、技術的に達成できたことはなかったのですが、やりたいことやろうとして消耗するより、小さい成功体験するほうが、結果的に課題に対して長く取り組める体質になれそうだな。という気づきがありました。

あとは、もっと人に質問できるようにならないと、何かを作るときは効率悪いなと思いました。

"人に質問できない人"の考え方として(僕もそのタイプ)、自分が何をやりたいのかを明確に把握していないことが実はよくあります。(僕)

自分が何をやりたいかを把握していないまま、手段だけを人に質問してしまうので、質問される方は、適切な回答に悩んでしまい怪訝な顔をされてしまう。ということがあると思います。(僕)

質問する方も、実は自分のハードルを上げていて、そもそもどんな手段があるかわからないのに、手段について聞く。という自分の首を締めるようなやり方を無意識的にやっていると思います。(僕)

そういった自分の経験から、人に相談するとき、もっとも力を入れるコミュニケーションは、"自分はこう言うことをしたい"ということだけに絞るようにしました。

本当はもう少し相手のことを考えたコミュニケーションがしたいのですが、まだまだそこまで手が回らないという状況です。

きっと周りには迷惑をかけているのでしょう。
スミマセン...

と言うことで、非エンジニアのしょーもない記事でした。

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

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

TypeScriptで数値の空判定を行う

経緯

TypeScriptで数値型のinput入力値の空判定をする必要がありました。
簡易的に再現すると下記のような感じで
valはnumberなので''との比較を行うことでエラーが発生しました。

const val: number = 0;
if (val === '') { // エラーになる
  // 処理
}

解決方法

数値型の値を文字列化して別の変数に代入することで
空判定が可能になりました。

const val: number = 0;
const valStr = String(val); // 空判定のため文字列化
if (valStr === '') { // エラーにならない
  // 処理
}

参考

JavaScript のデータ型とデータ構造
https://developer.mozilla.org/ja/docs/Web/JavaScript/Data_structures

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

なぜTypescriptを使うんですか?

はじめに

皆さん、TypeScriptはもう使っていますか?
既に使っている方でしたら、もう型のない世界に戻りたくない方も多いと思います。
sick_alcohol_chudoku.png

"では、改めてなぜ我々はTypeScriptを使うのか? "
"TypeScriptってなんだっけ?"

それを今一度振り返ってみます。

本投稿のゴール

型システムに関するメリットの言語化・理解の向上をし、引き続き気持ちよくTypeScriptを使っていけたらと思います。
まだ使ってない方は使う際の動機の1つでも増やせたらいいなと思います。

TypeScriptとは

TypeScript extends JavaScript by adding types to the language.

日本語: TypeScriptは、JavaScriptに型を追加することでJavaScriptを拡張します。

TypeScript speeds up your development experience by catching errors and providing fixes before you even run your code.

日本語: TypeScript は、コードを実行する前にエラーをキャッチして修正を提供することで、開発経験をスピードアップします。

つまり、TypeScriptは、JavaScriptに型システムを追加したものであり、型システムの恩恵を受けた開発が可能になる。

イメージ図

引用元: typescript公式サイト
和訳: DeepL: DeepLはニュアンスまで汲み取ってくれて本当に凄すぎる

型システム

型システムとはなんですか?
どんな性質のもの?その歴史は?立ち位置は?型システムとは、プログラミングだけのもの?
それを追っていってみます。

型システムとは

今回はWikiを覗いてみます。

型システム(英: type system)とは、プログラミング言語において、その式などの部分が持つ値を、その種類(型(type)、データ型も参照)に沿って分類し、プログラムが正しく振る舞うこと、といった性質について保証する手法である。

プログラミング言語ごとにある型システムは型付けされたプログラムがどのようにふるまい得るかを規定し、その規則から外れたふるまいを不正であると判定する。

型システムの規定に沿ったプログラムを書いているか、判定するシステムという事ですね。
この型システムは形式手法を簡略化した、軽量形式手法の1つに分類されます。

型システムをより理解するためにwikiの特に関連が強そうな部分を主観的に判断し、紹介していきます。
まずは、用語の意味、定義が書かれているので、抑えていきます。

用語

「値の種類」が型である。

型検査

プログラムにおけるエラーはさまざまだが、型に基づく一連のエラーがある。単純な例としては、浮動小数点数を表現しているデータを整数型として扱ってしまう、といったようなものである。この例では 0 と +0.0 のような特別な場合を除いてたいていの場合は得られる結果は無意味であり、より複雑な構造を持った値の場合は構造を壊して不正にしてしまうかもしれない。このような異常をプログラムが起こさないことを検査するのが型検査である。

検査

安全性

型にまつわるものに限らず一般に、プログラムが言語で定義していない状態や、言語仕様で「未定義」とされている状態にならない、という性質。プログラムのエラーをランタイムやインタプリタが検出して異常終了するような場合も「安全」の側に含まれる。型にまつわる安全性が型安全性である。

型安全性が実現できる内容は言語に依存する。なぜならば、安全性とは「言語仕様で未定義とされている状態にならない」という性質である、つまり言語仕様によって決定されているからである。

性質

可読性

表現力の高い型システムでは型はプログラマの意図を説明することができるので、ドキュメントの役割を果たすこともある。例としてタイムスタンプが整数の派生型である環境において、プログラマが単なる整数ではなくタイムスタンプを返す関数を宣言すると、その型情報が関数の意味を記述していることになる。

抽象化またはモジュール化

型によってプログラマは低レベルでの実装に煩わされずにより高レベルで考えることができるようになる。例えば文字列型によってプログラマは文字列を文字列として、単なるバイトの列ではないものとして考えることができる。また型によってプログラマは2つのサブシステム間のインタフェースを表現することができる。これはサブシステムの相互運用性に必要な定義を局所化し、それらのサブシステムが通信する際に起きる矛盾を防止する。

まとめ

型システムを利用する事により

  • 可読性
  • 抽象化・モジュール性

が向上し、新たに

  • 型検査
  • 安全性

を手に入れる事ができるということですね。

型システムは軽量形式手法なのですが、この国立情報学研究所の形式手法の資料に書かれている資料の以下の項目のメリットはある程度得られると考えます。

  • 厳密な言語を用いることで仕様を明確化
  • 記述中に隠れている不具合を開発早期に発見
    • 証明駆動開発をすれば、アルゴリズムがただしく動作するかを一時的な入力値を元に検証ではなく、証明ができますが、軽量形式手法である型システムの場合だと、そこまではできない
  • テスティング(手動テスト含む)まで不具合を放っておかなくて済む

最後に

先日TypeScript3.8がリリースされました。
現在のTypeScriptの型システムはチューリング完全のようです。
TypeScriptの型システムはかなり柔軟でかなり深く、色々できます。

私の場合はこの柔軟な型システムに、発達したエコシステムを利用した型安全のある開発がしたいので、これからもJavaScriptのスーパーセットであるTypeScriptを使って行こうとおもいます。

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

なぜTypeScriptを使うんですか?

はじめに

皆さん、TypeScriptはもう使っていますか?
既に使っている方でしたら、もう型のない世界に戻りたくない方も多いと思います。
sick_alcohol_chudoku.png

"では、改めてなぜ我々はTypeScriptを使うのか? "
"TypeScriptってなんだっけ?"

それを今一度振り返ってみます。

本投稿のゴール

型システムに関するメリットの言語化・理解の向上をし、引き続き気持ちよくTypeScriptを使っていけたらと思います。
まだ使ってない方は使う際の動機の1つでも増やせたらいいなと思います。

TypeScriptとは

TypeScript extends JavaScript by adding types to the language.

日本語: TypeScriptは、JavaScriptに型を追加することでJavaScriptを拡張します。

TypeScript speeds up your development experience by catching errors and providing fixes before you even run your code.

日本語: TypeScript は、コードを実行する前にエラーをキャッチして修正を提供することで、開発経験をスピードアップします。

つまり、TypeScriptは、JavaScriptに型システムを追加したものであり、型システムの恩恵を受けた開発が可能になる。

イメージ図

引用元: typescript公式サイト
和訳: DeepL: DeepLはニュアンスまで汲み取ってくれて本当に凄すぎる

型システム

型システムとはなんですか?
どんな性質のもの?その歴史は?立ち位置は?型システムとは、プログラミングだけのもの?
それを追っていってみます。

型システムとは

今回はWikiを覗いてみます。

型システム(英: type system)とは、プログラミング言語において、その式などの部分が持つ値を、その種類(型(type)、データ型も参照)に沿って分類し、プログラムが正しく振る舞うこと、といった性質について保証する手法である。

プログラミング言語ごとにある型システムは型付けされたプログラムがどのようにふるまい得るかを規定し、その規則から外れたふるまいを不正であると判定する。

型システムの規定に沿ったプログラムを書いているか、判定するシステムという事ですね。
この型システムは形式手法を簡略化した、軽量形式手法の1つに分類されます。

型システムをより理解するためにwikiの特に関連が強そうな部分を主観的に判断し、紹介していきます。
まずは、用語の意味、定義が書かれているので、押さえていきます。

用語

「値の種類」が型である。

型検査

プログラムにおけるエラーはさまざまだが、型に基づく一連のエラーがある。単純な例としては、浮動小数点数を表現しているデータを整数型として扱ってしまう、といったようなものである。この例では 0 と +0.0 のような特別な場合を除いてたいていの場合は得られる結果は無意味であり、より複雑な構造を持った値の場合は構造を壊して不正にしてしまうかもしれない。このような異常をプログラムが起こさないことを検査するのが型検査である。

検査

安全性

型にまつわるものに限らず一般に、プログラムが言語で定義していない状態や、言語仕様で「未定義」とされている状態にならない、という性質。プログラムのエラーをランタイムやインタプリタが検出して異常終了するような場合も「安全」の側に含まれる。型にまつわる安全性が型安全性である。

型安全性が実現できる内容は言語に依存する。なぜならば、安全性とは「言語仕様で未定義とされている状態にならない」という性質である、つまり言語仕様によって決定されているからである。

性質

可読性

表現力の高い型システムでは型はプログラマの意図を説明することができるので、ドキュメントの役割を果たすこともある。例としてタイムスタンプが整数の派生型である環境において、プログラマが単なる整数ではなくタイムスタンプを返す関数を宣言すると、その型情報が関数の意味を記述していることになる。

抽象化またはモジュール化

型によってプログラマは低レベルでの実装に煩わされずにより高レベルで考えることができるようになる。例えば文字列型によってプログラマは文字列を文字列として、単なるバイトの列ではないものとして考えることができる。また型によってプログラマは2つのサブシステム間のインタフェースを表現することができる。これはサブシステムの相互運用性に必要な定義を局所化し、それらのサブシステムが通信する際に起きる矛盾を防止する。

まとめ

型システムを利用する事により

  • 可読性
  • 抽象化・モジュール性

が向上し、新たに

  • 型検査
  • 安全性

を手に入れる事ができるということですね。

型システムは軽量形式手法なのですが、この国立情報学研究所の形式手法の資料に書かれている資料の以下の項目のメリットはある程度得られると考えます。

  • 厳密な言語を用いることで仕様を明確化
  • 記述中に隠れている不具合を開発早期に発見
    • 証明駆動開発をすれば、アルゴリズムがただしく動作するかを一時的な入力値を元に検証ではなく、証明ができますが、軽量形式手法である型システムの場合だと、そこまではできない
  • テスティング(手動テスト含む)まで不具合を放っておかなくて済む

最後に

先日TypeScript3.8がリリースされました。
現在のTypeScriptの型システムはチューリング完全のようです。
TypeScriptの型システムはかなり柔軟でかなり深く、色々できます。

私の場合はこの柔軟な型システムに、発達したエコシステムを利用した型安全のある開発がしたいので、これからもJavaScriptのスーパーセットであるTypeScriptを使って行こうとおもいます。

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

JavaScript で音声を順に再生する方法

音声が複数個あっても順に再生したい!!!

非同期処理って難しいですね…
テキストを音声化して再生するといった動作を実装したのですがそこでハマったことをメモとして書き出していきます。

概要

  1. 音楽ファイルを用意する
  2. 音楽ファイルを DataURI Scheme 形式(base64) にエンコードする
  3. await を使って順に再生するように実装する

1. 音楽ファイルを用意する

適当な音楽ファイルを用意して読み込みます。
File API とか使えば読み込めるかと思います。
今回はサーバーにテキストを送ると音声データが返ってくる仕様だったので読み込む動作は割愛します。

2. 音楽ファイルを DataURI Scheme 形式(base64) にエンコードする

ローカルから読み込む場合は、File API の URL.createObjectURL() 使えばエンコードできると思います。
今回は送られてきた音声データを base64 にエンコードして返してくれるように自作 API を実装しました。

3. await を使って順に再生するように実装する

ここでループ回して await 使って音声再生… で終わると思っていたのですが、音声がすべて同時に再生されてしまい、うまくいきませんでした。
そこで Promise オブジェクトを作成し、音声の再生終了後までループが止まるように実装したところ、うまく動かすことができました。

for (const d of data) {
    const targetSoundBase64 = d.base64;

    // 音声の再生が終わるまでループを回さないように止めておく
    await new Promise((resolve) => {
        const sound = new Audio("data:audio/wav;base64," + targetSoundBase64);
        sound.play();
        sound.addEventListener('ended', async () => {
            // 音声終了後にいきなり次の音声が再生されてると違和感がすごいのでちょっとスリープかける
            await sleep(100);
            resolve();
        }, {once: true});
    });
}

最初は音声の秒数を取得してその曲が終わるタイミングで次の曲が再生されるように setTimeout を使おうかと考えていたのですが、こっちのほうがいい感じのコードになるかなと思われます。

まとめ

テキストチャットの音声化のような複数の音声データを順に再生するような状況になった場合、このように実装すればいいのではないでしょうか。

非同期処理、わかるようでわからず難しいなーと感じました。
もっと勉強頑張ります?

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

Webデザイナーの仕事内容や働き方について

※本稿は弊社ホームページに記載のものを転載しています。
図1b.png

Webデザイナーは、クリエイティブな職種として近年注目を集めています。そんなWebデザイナーの仕事内容は広く言うとWebサイトの制作に携わることです。

通常Webサイトを制作するにはチームで行うことが一般的です。制作に関わる職種は、Webサイトの仕様を決めるWebディレクター、サイトを構築するコーダー、文章を作成するライターなどさまざまです。

その中でWebデザイナーは、デザインソフトを駆使してWebサイトの見た目の部分を作ります。制作チームの一端を担うWebデザイナーですが、最近では在宅やインハウス等、働き方も多様化してきています。

Webデザイナーはパソコンとインターネット環境があれば仕事ができる性質上、ある程度のスキルがあれば場所を選ばずに働けることも人気の理由の一つです。

Webデザイナーの仕事内容

Webデザイナーの主な仕事内容はPhotoshopやillustratorなどのデザインソフトを使って、Webサイトの全体像やボタンなどの見た目の部分を作ることです。会社によってWebディレクターやコーダーと兼務している場合もあり、どこまでが業務範囲かという明確な決まりはありません。WebデザイナーからWebディレクターになり、管理業務のみ行う人もいれば、企画からサイト作成まで全てを一人で行う人もいます。そこで一般的にコーダーを兼務している場合のWebデザイナーの仕事内容をそれぞれの項目ごとに確認していきましょう。

Webサイトの要件整理

まずはじめに、Webサイトを作りたいクライアントの目的をヒアリングし要件を整理します。クライアントの目的やターゲットによってWebサイトの仕様やデザインが変わるため、しっかりとヒアリングを行う必要があります。サイトの目的、要件をコンセプトに落とし込みます。サイトコンセプトを企画書にまとめ、クライアントの合意を得ます。

Webサイトの構成とレイアウト(ワイヤーフレーム)

コンセプトが決まったら、Webサイトの設計図となるワイヤーフレームを作ります。ワイヤーフレームとは骨組みという意味です。このときにサイト内のページ数、コンテンツの位置や数、大きさなどを決めます。構成とレイアウトはサイトの使いやすさや、集客にも影響する重要な作業です。

Webサイトのデザインを作る

Photoshopやillustratorを使って、Webサイトの実際の見た目を作っていきます。このときにサイト全体のカラーや、ボタンのデザインなどを決めていきます。まずデザインカンプを作ります。デザインカンプとは、サイト制作に取り掛かる前のデザイン案のことです。デザインカンプでOKが出れば、実際にサイト制作に取り掛かります。デザイン作成はミリ単位の調節が求められる繊細な作業です。

Webサイトのコーディング

Webサイトの見た目ができたらコーディングを行います。Webサイトは見た目を作るだけではインターネット上に表示されません。コーディングによってインターネット上に認識させることでWebサイトを観覧できるようになります。コーディングにはHTMLとCSSを使います。HTMLはマークアップ言語と呼ばれ、文字情報を表示するために使います。CSSはサイト内の色や画像、見た目を定義します。Webサイトに動きや、システムを組み込むにはJavaScriptなどのプログラミング言語が必要です。

Webデザイナーの働き方

図2.png

Webデザイナーといっても、様々な働き方があります。働き方にはインハウス、制作会社、SES会社、フリーランスなどがあり、雇用形態や仕事内容なども変わってきます。

インハウス

企業に所属し、自社のWebサイトやサービスを担当するWebデザイナーです。業務の幅が広く、パンフレット作成などいろいろな経験ができることが魅力です。企業によっては、Web担当者が他におらず、全てをやらなければいけない場合があります。デザインの幅は少ないので、いろいろなデザインを担当したい人には向きません。

制作会社

クライアントから依頼を受けてWebサイトを制作します。新規サイト制作や、既存サイトのリニューアルなどを担当します。新規のWebサイト制作の場合、クライアントや案件ごとにデザインをゼロから考えることも多く、デザインセンスが求められます。クライアントの都合で納期が決まるため、仕事に追われることもありますが、様々なデザインを経験したい人には向いています。

SES会社

SES(システムエンジニアリングサービス)会社は、自社で契約しているエンジニアを、外部の会社に客先常駐として派遣する会社のことで、最近ではWebデザイナーにも波及しています。常駐先は一般企業や制作会社などで、大手で働ける機会もあります。未経験でも採用されやすいことがメリットです。業務内容が単純作業になりがちで、スキルアップが難しいというデメリットもあります。

個人で独立して仕事を請負うWebデザイナーです。仕事の請負先は、企業や個人など多岐にわたります。フリーランスは自分で仕事を獲得する営業力が必要ですが、最近では営業代行サービスなども増えてきています。フリーランスは自分の裁量で仕事量などを決められることが魅力です。反面、トラブル対応などすべて自分でやらなければならないため、ある程度の実力と対応力が求められます。

Webデザイナーのしごと内容まとめ

図3.png

今回はWebデザイナーの仕事内容や働き方について紹介しました。Webデザイナーは単にデザインするだけでなく、Webサイト制作のさまざまな業務に携わる仕事だということがわかっていただけたと思います。Webサイト制作は技術の移り変わりが早く、デザインのトレンドも変化します。Webデザイナーとして成長意欲があり、向上心をもって取り組める人が向ている職業といえるでしょう。

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

forEach だと await が効かない

はじめに

以前制作したアプリケーションの実装中にハマってしまった解決策のメモです。
いつか使うだろうと言うことで書いておきます。

結論

for of を使って実装しましょう

const texts = [...];
for (const text of texts) {
    const fetchData = await fetch(...);
    fetchDataArray.push(fetchData);
}

データを取得して直列的に待ちながら処理をしようとしたら…

いつもの感覚で forEach を使って実装したらうまく動きませんでした…

const texts = [...];
texts.forEach((e) => {
    const fetchData = await fetch(...);
    fetchDataArray.push(fetchData);
});

どうやら forEach は Promise を無視して動作するようです。
なので for of を使ってループしてあげる必要があるみたいですね。

const texts = [...];
for (const text of texts) {
    const fetchData = await fetch(...);
    fetchDataArray.push(fetchData);
}

このように書けば問題なく動作すると思われます。

余談

実は、Promise.all の使い方をよく理解できてません。今回みたいな使い方でも同じ動作できるように実装できるのかなあ?
今後改めて勉強したいと思います ?

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

create-react-appを卒業して自分でReact + TypeScriptの開発環境を作れるようになるということ

これまでReactの環境構築をする時はcreate-react-appに頼りっきりでしたが、いい加減自分で作れないとまずいなと思い忘備録も兼ねて残しておきます。
また、せっかくTypeScriptも使うので webpack.config.js もTypeScriptで書けるようにしたいと思います。


最終的なディレクトリ構成は次のようになります。

.
┃━━ public
┃  ┗━ index.html
┃━━ src
┃  ┃━ index.tsx
┃  ┗━ App.tsx
┃━━ package.json
┃━━ package-lock.json
┃━━ tsconfig.json
┗━━ webpack.config.ts

それではやっていきましょう。

1. ディレクトリの初期化

適当なディレクトリを作って npm init するだけです。

mkdir hogehoge
cd hogehoge
npm init -y

-y オプションはお好みで(つけると全部初期値で自動的に初期化されます)。
初期化が終了すると package.json ができているはずです。

2. モジュールのインストール

React + TypeScriptの環境構築なので、とりあえず必要そうなモジュールをインストールしていきます。
バンドラにはWebpackを利用します。

npm i -d react react-dom
npm i -D typescript ts-loader webpack webpacl-cli webpack-dev-server @types/node @types/react @types/react-dom

@types/node の中に webpack.Configuration が定義されており、これがWebpackの設定情報の型です。
これを使って webpack.config.ts を書いていきます。

webpack.config.ts
import { Configuration } from 'webpack'
import path from 'path'

const config: Configuration = {
  mode: 'development',
  entry: './src/index.tsx',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [
      '.tsx',
      '.ts',
      '.js',
    ],
  },
  devtool: 'inline-source-map',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
}

export default config

感のいい人はお気づきかもしれませんが、現段階だと開発用サーバーの設定である devServer プロパティがありません(書いてみるとエラーになるはずです)。
そこで @types/webpack-dev-server をインストールします。

npm i -D @types/webpack-dev-server

これで webpack-dev-server の設定ができるようになりました。 webpack.config.ts に設定を追記します。

webpack.config.ts
const config: Configuration = {
  // ...
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
  },
  // ...
}

これでエラーが消えたはずです。

HtmlWebpackPlugin の設定

詳しくは割愛しますが、このプラグインを使うとテンプレートのHTMLのbodyタグの末尾にscriptタグを勝手に挿入してくれるので便利です。
というわけでインストールします。

npm i -D html-webpack-plugin @types/html-webpack-plugin
webpack.config.ts
import HtmlWebpackPlugin from 'html-webpack-plugin'

const config: Configuration = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
  // ...
}

テンプレートとなるHTMLファイルを public/index.html に作成します。

public/index.html
<html lang="ja">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>your app title</title>
</head>

<body>
  <div id="root"></div>
</body>

</html>

ここではテンプレートHTMLのパスしか設定しませんが、 title など他にもいくつかオプションがあるので気になる方は調べてみてください。

3. TypeScriptの設定

tsconfig.json を作成・編集します。

tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "allowSyntheticDefaultImports": true
  }
}

src ディレクトリ内に、エントリポイントとなる index.tsx を作っていきます。とりあえず簡単なものだけ。

src/App.tsx
import React, { FC } from 'react'

export const App: FC = () => <div>Hello World!</div>
src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import { App } from './App'

ReactDOM.render(<App />, document.getElementById("root"))

4. npm scriptsを設定する

package.json を編集します。
開発用サーバーを立ち上げるコマンドと、ビルド用のコマンドを用意します。

package.json
{
  // ...
  "scripts": {
    "dev": "webpack-dev-server --open",
    "build": "webpack"
  },
  // ...
  }
}

ここまででほぼ完成みたいなものですが、このままだとコンパイルできません。

5. 設定ファイルを読み込めるようにする

今のままだと設定ファイルが .ts ファイルなので読み込めません。そこで ts-node というモジュールを使用します。
モジュール名の通りですが、Node.jsがTypeScriptを直接読めるようにするものです。

npm i -D ts-node

これでコンパイルが通る・・・ようにはなりません。もう1ステップ必要です。
作成した webpack.config.ts ですが、import/export文を使用しているためこのままだと使えません。
これが使えるように tsconfig.json を編集します。

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

詳細な説明は省きますが、「CommonJSモジュールだよ~」と教えてあげることで使えるようになります。

とりあえずはこれにて終了です。npm run dev なり npm run build して開発に励みましょう。

6. おわりに

ところどころ端折りましたが、これでReact + TypeScriptの開発環境は作れたはずです。
似たような記事は結構あるのですが、同じReact + TypeScriptでもWebpackの設定までTypeScriptで行っているものは少なかったので記事にしました。型定義があるぶん補完も効くので書きやすいです。

こんな記事でも誰かのお役に立てば幸いです。ご覧頂きありがとうございました。

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

Node.js を持ち歩けるようにして、どこでも簡易WEBサーバを起動出来るようにする

Node.jsで便利なライブラリをつくったとしても、お客さまのPCやサーバの本番環境に Node.jsの実行環境がないケース もあります。「Node.jsの実行環境を持ち歩けたらなぁ、、」ということでググってみたらこの記事が。。

インストーラなどの実行が不要な 解凍すればすぐに使えるNode.js実行環境の構築手順です。コレを使えば、実行環境ごとZipでアーカイブしたファイルを作成し作業環境でそれを解凍すればNode.js環境の構築完了、なんて事ができそうです。

ついでに、そのNode.js上でWebサーバを立ち上げることで、どこでも簡易WEBサーバが起動出来るようにしてみます。

Node.js実行環境の構築

公式からバイナリをダウンロード。Windows Binary (.zip) の64-bitを選択します。2020/03/15時点「node-v12.16.1-win-x64.zip」が最新版のようです。
ダウンロード後、任意の場所に解凍しましょう。たとえば T:\Tools\nodejs_portableなどなど。

さて上記ディレクトリには、node.exeなどが入っていると思いますが、同じディレクトリに起動コマンドrun.batを置いておきます。中身はテキストでこんな感じ。

@echo off
set PATH=%cd%;%PATH%
set NODE_PATH=%cd%\node_modules\npm\node_modules;%cd%\node_modules\npm
cmd

いわゆる、このディレクトリにパスを通しているだけです。
基本的な環境構築は以上です。

実行してみる

さてエクスプローラ上で run.bat をダブルクリックして、node.js 実行環境を起動してみましょう。

T:\Tools\nodejs_portable>

こんな感じで起動すればOKです。

T:\Tools\nodejs_portable> node --version
v12.16.1

T:\Tools\nodejs_portable> npm --version
6.13.4

T:\Tools\nodejs_portable>

一応 Hello Worldを。

T:\Tools\nodejs_portable> mkdir app
T:\Tools\nodejs_portable> cd app
T:\Tools\nodejs_portable\app> type index.js  ← あらかじめメモ帳とかで作成しておきましょう
console.log('Hello World.')

T:\Tools\nodejs_portable\app> node index.js  ←実行
Hello World.

T:\Tools\nodejs_portable\app>

実行OKですね。つづいて npm なども動作確認してみます。

(上でつくったappは一旦削除したとして)
T:\Tools\nodejs_portable> mkdir app
T:\Tools\nodejs_portable> cd app
T:\Tools\nodejs_portable\app> npm init -y
Wrote to T:\Tools\nodejs_portable\app\package.json:

{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

下記のような index.js をつくってみました。

T:\Tools\nodejs_portable\app> type index.js
const request = require('request')

request.get('http://www.yahoo.co.jp', (err, res, body) => {
  console.log(body)
})

npm iでインストールします。

T:\Tools\nodejs_portable\app> > npm i --save request
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN app@1.0.0 No description
npm WARN app@1.0.0 No repository field.

+ request@2.88.2
added 47 packages from 58 contributors and audited 63 packages in 9.683s
found 0 vulnerabilities

T:\Tools\nodejs_portable\app> node index.js

....  ばばっってYahooのサイトがなんか返ってくればOK

T:\Tools\nodejs_portable\app>

OKそうですね!

-- 注意 --
いわゆるグローバルインストールnpm install -g xxについて。
グローバルインストールした際は、T:\Tools\nodejs_portable\node_modules にインストールされる想定だったのですが、すでに Node.jsがインストールされている環境のばあいそちらのディレクトリにインストールされてしまいました。

すでにNode.jsがインストールされている環境では、グローバルインストールは実施しない方がよさそうです。
ご注意ください。

簡易WEBサーバの構築

さて、さいごにWEBサーバです。Node.jsを用いたWEBサーバの構築は
[Node.js] 簡単なWebサーバとして静的ファイル配信/ディレクトリ一覧機能のサンプルコード
ココをまるまる参考にさせていただきました!感謝です。

さてやってみます。

(上でつくったappは一旦削除したとして)
T:\Tools\nodejs_portable> mkdir app
T:\Tools\nodejs_portable> cd app
T:\Tools\nodejs_portable\app> npm init -y
Wrote to T:\Tools\nodejs_portable\app\package.json:

{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

T:\Tools\nodejs_portable\app> npm i --save express
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN app@1.0.0 No description
npm WARN app@1.0.0 No repository field.

+ express@4.17.1
added 50 packages from 37 contributors and audited 126 packages in 10.259s
found 0 vulnerabilities

さて index.js(WEBサーバのプログラム)は以下でOKです。単純です。

T:\Tools\nodejs_portable\app>type index.js
'use strict';

const express = require('express');

const app = express();
app.use(express.static('./dist'));
app.listen(process.env.PORT || 3000);

ポート番号:3000 で、ドキュメントルートを ./dist としたWEBサーバを起動するためのJavaScriptコードが完成です。

疎通のため単純なhtmlを./dist配下に配置します。

T:\Tools\nodejs_portable\app> mkdir dist
T:\Tools\nodejs_portable\app> type dist\index.html
<html>
<body>

<h1>テスト</h1>
</body>
</html>
T:\Tools\nodejs_portable\app>

実行してみる

さてWEBサーバを起動してみます。

T:\Tools\nodejs_portable\app> node index.js
...

起動したようです。

別のコマンドプロンプトからアクセスしてみます。(あもちろんWEBブラウザでもよいです)

C:\Users\xx> curl http://localhost:3000/index.html
<html>
<body>

<h1>テスト</h1>
</body>
</html>

OKそうですね!

例によって、インストールディレクトリ(例だとT:\Tools\nodejs_portableですね) をZip化して持ち運べば、任意の場所で解凍すればOKの、簡易WEBサーバの完成です。静的なWEBサーバですが、ちゃちゃっとつかうフロントのサーバとしては十分でしょう。

おつかれさまでした!

関連リンク

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

obniz+赤外線LED+TypeScriptでリモコンコンセント(OCR-05W)を動かす

はじめに

こんにちは。電気毛布エンジニアの@tmitsuoka0423です。

昨日書いた「obniz+赤外線LEDでリモコンコンセント(OCR-05W)を動かす」では、obnizのパーツライブラリページ上で動作確認しましたが、今回はTypeScriptで実装していきます。
ソースコードはhttps://github.com/tmitsuoka0423/obniz-ocr-05wで公開しています。

【クラウドファンディング実施中!】mouful(モウフル)

電気毛布につけるだけ!スマホで布団の温度調節できるmouful(モウフル)
というクラウドファンディングを実施しています。

電気毛布の
  △にして寝る→暑くて体がカラカラになってしまう
  △にして寝る→温まらなくて眠れない
  △にして寝る→面倒臭くてやらなくなる
という課題を解決します。

  ○1週間レンタルでフィードバックしてくれる方
  ○開発メンバー
を募集しています!

クラウドファンディングページはこちら→https://camp-fire.jp/projects/view/220261

今回使うもの

品名 画像 価格
obniz Board 5,500円くらい
赤外線センサー OSRB38C9AA 50円くらい
赤外線LED OSI5FU5111C-40 20円くらい
リモコンコンセント OCR-05W 1,200円くらい

obnizと素子を接続する

以下図のように接続します。
Screenshot from Gyazo

リモコンの赤外線信号を解析する

まずは、赤外線センサーを使ってリモコンの信号を受信するプログラムを作成します。
(確認してないですが、このプログラムを使ってOCR-05W以外のリモコンの信号も受信できると思います。)

receive.ts
import Obniz from 'obniz';

const obniz = new Obniz('OBNIZ_ID_HERE');

obniz.onconnect = async () => {
  const sensor = obniz.wired('IRSensor', { vcc: 0, gnd: 1, output: 2 });
  console.log('リモコンのボタンを短く押してください…');

  sensor.start(function (arr) {
    console.log('受信したシグナル:');
    console.log(JSON.stringify(arr));

    obniz.close();
  });
}

これをコンパイルして実行する。
Screenshot from Gyazo
と表示されるので、リモコンのボタンを押すと、めっちゃ長い信号が受信される。

無事受信できた。

リモコンコンセントにobnizから信号を送信する

プログラムは以下の通り。

send.ts
import Obniz from 'obniz';

const on: (0 | 1)[] = [1, 1, 1, 1, 1, ()]; // 受信したリモコンの信号をコピペする。
const off: (0 | 1)[] = [1, 1, 1, 1, 1, ()]; // GitHubに全量を載せています。

const obniz = new Obniz('OBNIZ_ID_HERE');

obniz.onconnect = async () => {
  const led = obniz.wired('InfraredLED', { anode: 3, cathode: 4 });
  led.send(on);
  // led.send(off);
  console.log('信号を送信しました。');

  obniz.close();
}

動かしてみる。

まとめ

obniz+TypeScriptは補完が効くようになるのでオススメ。

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

node.jsのテンプレートリテラルと+による文字列結合の速度差

はじめに

文字列を取り扱う際、毎回テンプレートリテラルと文字列連結だとどちらの方が早いのだろう。という疑問が毎回頭を過ぎってました。
ということで軽くですがテストしました。

環境

  • Windows 10 pro
  • node.js v13.0.1

試す

計測にはperformanceを使用しました。
10^7回文字列連結する処理を10回繰り返し、平均実行時間を出しました。
なお実行時間の単位はmsです。

まずは単純な文字列同士の結合です。

const { performance } = require("perf_hooks");
const a = "hogehoge";
const b = "fugafuga";

/** concat_text */
let concat_sum = 0;
for (let i = 0; i < 10; i++) {
  const start = performance.now();
  for (let j = 0; j < 10 ** 7; j++) {
    const hoge = a + b;
  }
  const time = performance.now() - start;
  console.log(time);
  concat_sum += time;
}
console.log("concat_text average: " + concat_sum / 10);

/** template_literal */
let template_sum = 0;
for (let i = 0; i < 10; i++) {
  const start = performance.now();
  for (let j = 0; j < 10 ** 7; j++) {
    const hoge = `${a}${b}`;
  }
  const time = performance.now() - start;
  console.log(time);
  template_sum += time;
}
console.log(`template_literal average: ${template_sum / 10}`);
result
34.08500099973753
39.77130000013858
11.797801000066102
11.937899000011384
11.332999000325799
11.727699999697506
11.424000999890268
12.096899999771267
11.576799999922514
11.971499999985099
concat_text average: 16.772190099954607
116.54179899999872
163.70389899984002
121.10849899984896
71.12280000001192
71.46999999973923
71.70690099988133
74.72199900029227
72.5004999996163
71.70279900031164
71.67179999966174
template_literal average: 90.62509959992022

単純な文字列の結合だと+で結合した方が若干早いようです。
連結する数を4個(a + b + c + d)に増やした場合以下のようになりました。

  • +は実行時間に然程影響は無かった
  • テンプレートリテラルは実行時間が倍になった。

次に連結の際にnumberの計算もするよう変更し、前テストと同じ回数行いました。

const { performance } = require("perf_hooks");
const a = "hogehoge";

/** concat_text */
let concat_sum = 0;
for (let i = 0; i < 10; i++) {
  const start = performance.now();

  for (let j = 0; j < 10 ** 7; j++) {
    const hoge = a + (8 + 2);
  }
  const time = performance.now() - start;
  console.log(time);
  concat_sum += time;
}
console.log("concat_text average: " + concat_sum / 10);

/** template_literal */
let template_sum = 0;
for (let i = 0; i < 10; i++) {
  const start = performance.now();

  for (let j = 0; j < 10 ** 7; j++) {
    const hoge = `${a}${8 + 2}`;
  }
  const time = performance.now() - start;
  console.log(time);
  template_sum += time;
}
console.log(`template_literal average: ${template_sum / 10}`);
result
270.05879899999127
226.13540100026876
226.2756000002846
230.80309899989516
245.48059999989346
221.4089999999851
226.89270000020042
227.69470000034198
225.0682989996858
220.0896009998396
concat_text average: 231.9907799000386
41.350199999753386
114.77740000002086
38.32340000011027
109.85499900020659
48.483599999919534
102.05140100000426
43.74809999996796
92.99629899999127
40.28349999990314
42.15730100031942
template_literal average: 67.40262000001967

テンプレートリテラルの方が早いですね。
連結する数を4個(a + (8 + 2) + (9 + 1) + (10 - 3))に増やした場合以下のようになりました。

  • テンプレートリテラルは実行時間に然程影響は無かった
  • +は実行時間が倍になった。

まとめ

  • +を用いた連結は複雑な処理を入れるかどうかでタイムに差が出た
  • テンプレートリテラルは複雑な処理をする際にタイムにブレが出にくい
  • 連結数を増やした場合、早い方のタイムはブレが出にくい

といってもここまで大量に処理しない限り誤差の範囲ですので個人の好みになって来ると思います。
私は見やすさ的にもタグ関数的にもテンプレートリテラルを使いたいと思います。

また、今回はnodeのみで実験しましたがブラウザ上だとまた違った結果になりそうです。

初学者ですので間違い等あれば訂正いただけると嬉しいです。

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

Ongaqを使ってみた!

背景

こんにちは
音楽が下手だけど、音楽やりたい人間です。
もともと音楽とプログラミングの融合に興味があって、それをとある知り合いの方に話したらLispって言語がいいよって言われたんですけど、かっこ多すぎてやめました。
そこで、もともと知識のあるJavaScriptでOngaqというAPIを叩けるものを見つけたのでやってみた!という記事になっています、

Ongaqとはなんぞや?

Ongaqとは、JavaScriptで音楽が作成できるというAPIです。
Keyの取得はこちらから
有料枠と無料枠があり、使える楽器の種類などが異なる模様(有料会員になろうかな)

ぽちぽちしていけば簡単に登録でき、誰でもKeyを取得できます!

やっていく

細かいことはドキュメントにだいたい書いてあります。

てことで僕からはすごい簡単に

  • Ongaq

    • 楽曲を再生する環境にあたるオブジェクト
    • APIKeyの指定やテンポなどの大まかな機能を指示する部分
  • Partクラス

    • 楽器の指定
    • measureで何小節分構成するか決める(ここ大事)
  • Filterクラス

    • 音の指定
    • Partクラス.add(new Filter~~~ で追加
    • keyでビートを刻める(ドラムとその他で入る文字が違うので注意)
    • activeで音のタイミングを指定
    • lengthで1音の長さを指定

だいぶ省略してるけど、だいたい使うのはここらへんかと思います。

適当に歌を作ってみる

みんな大好きなドレミの歌を作成しようと思ったのですが、この記事書いてる途中にカエルの歌を作ってる方を見つけたので僕は後ろ側のドラムの部分を簡単に実装してみました。

index.js
const ongaq = new Ongaq ({
            api_key: "取得したAPIKey",
            bpm: 70,
            volume: 90,
            onReady: () => {
                const button = document.getElementById("button");
                button.onclick = () => {
                    if (ongaq.params.isPlaying) {
                        ongaq.pause()
                    } else {
                        ongaq.start()
                    }
                }
            }
        })

const drum1 = new Part({
            sound: 'my_band_drums', //無料で使用できるドラム
            measure: 4, //4小節分構成
            beatsInMeasure: 8
        })

        drum1.add(new Filter({
            type: 'note', //typeがnoteだとkey,length, volumeがオプションとして指定可能
            key: beat => {
                switch(beat) {
                    case 0: return ['kick', 'snare2', 'side', 'hihat']
                    //case 1: return ['kick']
                    //case 2: return ['kick']
                    //case 3: return ['kick']
                    case 4: return ["kick"]
                    //case 5: return ["kick"]
                    //case 6: return ['kick']
                    //case 7: return ['kick']
                    case 8: return ['kick', 'snare2', 'side', 'hihat']
                    //case 9: return ['kick']
                    case 10: return ['kick', 'hihat']
                    //case 11: return ['kick']
                    case 12: return ['kick', 'tom']
                    //case 13: return ['tom']
                    //case 14: return ['tom']
                    //case 15: return ['kick']


                }
            },
            volume: 100,
            active: beat => beat % 1 === 0
        }))
        drum1.attach({
            taste: "kick"
        })

        const drum2 = new Part({
            sound: 'small_cube_drums', //これも無料
            measure: 4, //4小節分構成
        })


        drum2.add(new Filter({
            type: 'note', //typeがnoteだとkey, length, volumeがオプションとして指定可能
            key: beat => {
                switch(beat) {
                    case 0: return ['kick', 'snare2', 'side', 'hihat']
                    //case 1: return ['kick']
                    case 2: return ['kick']
                    //case 3: return ['kick']
                    case 4: return ['kick']
                    //case 5: return ['kick']
                    case 6: return ['kick', 'snare']
                    //case 7: return ['kick']
                    case 8: return ['kick', 'snare2', 'side', 'hihat']
                    //case 9: return ['kick']
                    case 10: return ['kick', 'hihat']
                    //case 11: return ['kick']
                    case 12: return ['kick', 'tom2']
                    //case 13: return ['tom']
                    case 14: return ['kick']
                    //case 15: return ['kick']

                }
            },
            volume: 100,
            active: beat => beat % 1 === 0
        }))

とまあこんな感じに簡単にですが実装してみました。
ドレミの歌含め全部のソースコードはこちらに公開してあります。
(Hue Lightsとも連動してるので少しコードが多いですが、、、、)

再生した感じはここにあります

感想

Lispよりも簡単に書けるし、なんだかんだで楽しいし、ドキュメントもシンプルだし、とっても楽しかったです!

先述したカエルさんも言ってましたが、使い道がよくわかりませんけど、自分はめざましで使おうと思っています。Hueと連動して。。。(笑)

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

N予備校の教材でAjaxでCORSを試してみる

やりたいこと

N予備校のAjaxのページに「同一生成ポリシーを満たさない場合CORSにひかかって通信できません」と書かれているが、そのあたりの回避策について特に紹介されていなかったのでAccess-Control-Allow-Originを試してみる。

前提知識

環境

  • Vagrant ubuntu(Node.jsにて上記のリンクのサーバが稼働している)
  • 手元のマシン(Mac)

実験

  • 手元のマシンのブラウザから、Vagrant上のNode.jsに対してAjax通信を行い失敗することを確認する
  • CORSを回避するためにサーバー側にヘッダーを挿入する

修正前

  • LoadAvgが表示されない
  • コンソールログをみると以下のエラーが発生している
Access to XMLHttpRequest at 'http://localhost:8000/server-status' 
from origin 'http://localhost' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

コード修正

server-status.js
'use strict';
const express = require('express');
const router = express.Router();
const os = require('os');

router.get('/', (req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost')  ?追加
  res.json({ loadavg: os.loadavg() });
});

module.exports = router;

Access-Control-Allow-Originを追加することで特定のOriginからの通信を許容する

修正後

  • LoadAvgが表示された
Access-Control-Allow-Origin: http://localhost ? 追加されている
Connection: keep-alive
Content-Length: 19
Content-Type: application/json; charset=utf-8
Date: Tue, 24 Mar 2020 19:25:17 GMT
ETag: W/"13-78iQH47CLcJ0F+4s06WtpVC9PNc"
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【useState/ReactRedux】Reactにおける状態管理

概要

  • Reactにおける状態管理のしかたをReact標準で備えるuseStateを使ったものとReactにおけるReduxライブラリであるReactReduxを使ったものを紹介します

サンプルアプリの準備

  • この後の説明をするためのサンプルを作っておきます
  • 必要なライブラリを追加
yarn add react react-dom react-router-dom parcel-bundler

index.htmlを作成
index.html
<div id="root"></div>
<script src="index.js"></script>


index.jsを作成
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './src/App';

ReactDOM.render(<App />, document.getElementById('root'));


src/App.jsの作成
src/App.js
import React from 'react';
import Router from './routes/router';

function App() {
  return <Router />;
}

export default App;


src/routes/router.jsを作成
src/routes/router.js
import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';

import Home from '../components/Home';
import Counter from '../components/Counter';
import TodoList from '../components/TodoList';

function Router() {
  return (
    <BrowserRouter>
      <div>
        <Route path="/" exact>
          <Home />
        </Route>
        <Route path="/counter" exact>
          <Counter />
        </Route>
        <Route path="/todo-list" exact>
          <TodoList />
        </Route>
      </div>
    </BrowserRouter>
  );
}

export default Router;


src/components/Home.jsを作成
src/components/Home.js
import React from 'react';
import { Link } from 'react-router-dom';

function Home() {
  return (
    <div>
      <h1>Home</h1>
      <p>
        <Link to="/counter">Counterへ</Link>
        <Link to="/todo-list">TodoListへ</Link>
      </p>
    </div>
  );
}

export default Home;


src/components/Couner.jsを作成
src/components/Counter.js
import React from 'react';
import { Link } from 'react-router-dom';

function Counter() {
  return (
    <div>
      <h1>Counter</h1>
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/todo-list">TodoListへ</Link>
      </p>
    </div>
  );
}

export default Counter;


src/components/TodoList.jsを作成
src/components/TodoList.js
import React from 'react';
import { Link } from 'react-router-dom';

function TodoList() {
  return (
    <div>
      <h1>TodoList</h1>
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/counter">Counterへ</Link>
      </p>
    </div>
  );
}

export default TodoList;

npx parcel index.html
  • ここまでだとこんな感じです

init.gif

useStateを使った状態管理

Counterアプリ

counter.gif

  • を押すとインクリメントされてを押すとデクリメントされるサンプルです
    • 現在の値をメモリ上で保持しておく必要があるためそこで状態管理が発生するわけですね
  • src/components/Counter.jsを以下の内容に変更します
    • 1行目でuseStateのimportが追加されているようにReactが標準で提供するuseStateを使います
src/components/Counter.js
import React, { useState } from 'react';
import { Link } from 'react-router-dom';

function Counter() {
  // countは現在のカウントの値、setCountはcountを更新する関数、useStateの引数は初期値
  const [count, setCount] = useState(0);
  // countを+1する関数
  const increment = () => setCount(count + 1);
  // countを-1する関数
  const decrement = () => setCount(count - 1);
  return (
    <div>
      <h1>Counter</h1>
      <p>{count}</p>
      <button onClick={increment}></button>
      <button onClick={decrement}></button>
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/todo-list">TodoListへ</Link>
      </p>
    </div>
  );
}

export default Counter;
  • ボタンを押すとそれぞれincrement/decrement関数を呼び出しています
    • increment/decrementの中ではsetCountを実行しています
    • useStateで生成したsetXxxを実行し値が更新されると、最新の値で自動的に再描画が走るというのが特徴です
  • useStateの動きをなんとなくつかんでもらえたのではないでしょうか?

TodoListアプリ

todolist.gif

  • TodoListアプリを作ってみます
  • 現在のTodoのリストや現在の入力された値をuseStateを使って管理してみます
src/components/TodoList.js
import React, { useState } from 'react';
import { Link } from 'react-router-dom';

function TodoList() {
  // todoListを管理するstateを宣言
  const [todoList, setTodoList] = useState([]);
  // 入力内容を管理するstateを宣言
  const [inputText, setInputText] = useState('');

  // 入力内容が変化をstateにsetする関数
  const onChangeText = event => setInputText(event.target.value);
  // 入力内容をTodoListに追加して入力域を空にする関数
  const onClickAdd = () => {
    setTodoList([...todoList, { text: inputText }]);
    setInputText('');
  };
  return (
    <div>
      <h1>TodoList</h1>
      <input onChange={onChangeText} value={inputText} />
      <button onClick={onClickAdd}>追加</button>
      {todoList.map((todo, i) => (
        <p key={i}>{todo.text}</p>
      ))}
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/counter">Counterへ</Link>
      </p>
    </div>
  );
}

export default TodoList;
  • Stateを複数管理したい場合はこのようにuseStateを必要なだけ宣言すればよいということがわかるかと思います
  • useStateの使い方に慣れてきましたか?

useStateの特徴

  • useStateはコンポーネント単位で宣言しています
  • なのでコンポーネントが破棄されると管理していた状態もすべて破棄されてしまいます
  • サンプルアプリでページ遷移して戻ってくると値がリセットされていることからも分かるかと思います

reset-state.gif

ReactReduxを使った状態管理

  • useStateはコンポーネント単位で状態を管理していましたが、ReactReduxはアプリ全体で状態の管理をします
    • single source of truthと言われるように状態はアプリ全体で一箇所で管理するという考え方です
  • ReactReduxを使うと、状態の管理や状態を更新するための処理がコンポーネントから切り離され役割が分離されるのが特徴です

ReactReduxのセットアップ

  • 必要なライブラリを追加します
    • 今回はredux-toolkitも使います
    • Reduxチームが作っているライブラリでこれを使うと実装量が大きく減少します
yarn add react-redux @reduxjs/toolkit
  • ReactReduxの設定周りのファイルを作成します
  • src/store/index.jsを作成します
src/store/index.js
import { configureStore } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: {
    // この中は後で作る
  },
});
  • src/App.jsに適用します
src/App.js
import React from 'react';
import { Provider } from 'react-redux';
import Router from './routes/router';
import { store } from './store';

function App() {
  return (
    <Provider store={store}>
      <Router />
    </Provider>
  );
}

export default App;
  • これで準備OKです

Counterアプリ

  • useStateを使ったCounterアプリをReactRedux化していきます
  • まずはCounter周りの状態管理と状態更新を定義するファイルを作成します
  • src/store/counterSlice.jsを作成します
src/store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

export const counterSlice = createSlice({
  name: 'counter',
  // Stateの初期値を設定
  initialState: {
    count: 0,
  },
  // 状態を更新する関数を定義する場所
  reducers: {
    // countを+1する処理
    increment(state) {
      return { count: state.count + 1 };
    },
    // countを-1する処理
    decrement(state) {
      return { count: state.count - 1 };
    },
  },
});

// reducersに定義した処理を呼び出すActionをexportする
export const { increment, decrement } = counterSlice.actions;

// 現在のcountの値を取得するためのSelectorをexportする
export const selectCount = ({ counter }) => counter.count;

// お作法としてdefault exportでreducerをexport
export default counterSlice.reducer;
  • useStateのパターンでCounter.jsに定義されていた状態管理と状態更新の処理がこちらに移動してきました
    • 書き方は独特ですがひとつひとつの処理は見れば分かると思います
  • 次に今作ったファイルをsrc/store/index.jsに反映させます
src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
// importを追加
import counterReducer from './counterSlice';

export const store = configureStore({
  reducer: {
    // 定義を追加
    counter: counterReducer,
  },
});
  • 次にcounterSliceに定義したものをコンポーネントが使いやすいようにワンクッション挟むファイルを作ります
    • このファイルでやってることを直接コンポーネントで書いても動きます
  • src/hooks/useCounter.jsを作成します
src/hooks/useCounter.js
import { useSelector, useDispatch } from 'react-redux';
// 現在のcountを取得するためのselectCount、countを更新するためのincrementとdecrementをimport
import { selectCount, increment, decrement } from '../store/counterSlice';

function useCounter() {
  const dispatch = useDispatch();
  // count,increment, decrementをすぐに使える状態で返す
  return {
    count: useSelector(selectCount),
    increment: () => dispatch(increment()),
    decrement: () => dispatch(decrement()),
  };
}

export default useCounter;
  • counterSliceでexportしたものはSelect系はuseSelectorを、Action系はuseDispatchを通さないと使うことができません
    • コンポーネント側にReactReduxを意識したコードを増やしたくないのでそれを隠蔽するためにuseCounter.jsを作りました
  • 最後にsrc/components/Counter.jsuseCounterを使うように修正します
src/components/Counter.js
import React from 'react';
import { Link } from 'react-router-dom';
// importを追加
import useCounter from '../hooks/useCounter';

function Counter() {
  // 以下の3行が不要になった
  // const [count, setCount] = useState(0);
  // const increment = () => setCount(count + 1);
  // const decrement = () => setCount(count - 1);

  // useCounterから値を取得
  const { count, increment, decrement } = useCounter();

  // 以下修正なし
  return (
    <div>
      <h1>Counter</h1>
      <p>{count}</p>
      <button onClick={increment}></button>
      <button onClick={decrement}></button>
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/todo-list">TodoListへ</Link>
      </p>
    </div>
  );
}

export default Counter;
  • 状態管理や状態更新をしていたコードが全て不要になりuseCounterから取得するものに置き換わりました
  • これでuseStateからの置き換え完了です
  • ページ遷移をしても状態が維持されることを確認しましょう

counter-redux.gif

TodoListアプリ

  • 同じ要領でTodoListもReactRedux化していきましょう
  • まずはsrc/store/todoSlice.jsを作成します
src/store/todoSlice.js
import { createSlice } from '@reduxjs/toolkit';

// Slice
export const todoSlice = createSlice({
  name: 'todo',
  // Stateの初期値を設定
  initialState: {
    inputText: '',
    todoList: [],
  },
  // 状態を更新する関数を定義する場所
  reducers: {
    changeText(state, action) {
      return {
        // returnしたものがStateにセットされるため変更がないものはそのままになるように`...state`をセットしている
        ...state,
        // 引数で渡された値はaction.payloadで取得できる
        inputText: action.payload.inputText,
      };
    },
    add(state, action) {
      return {
        ...state,
        todoList: [...state.todoList, { text: action.payload.text }],
      };
    },
  },
});

// reducersに定義した処理を呼び出すActionをexportする
export const { changeText, add } = todoSlice.actions;

// 現在のcountの値を取得するためのSelectorをexportする
export const selectInputText = ({ todo }) => todo.inputText;
export const selectTodoList = ({ todo }) => todo.todoList;

// お作法としてdefault exportでreducerをexport
export default todoSlice.reducer;
  • Counterの時との違いは管理する状態が複数あることと、Actionの実行時に引数を受け取ることです
    • reducersの中でreturnする時に変更しない値も返す必要があるので...stateを一番上に入れておく
    • reducersの中の関数で第2引数で受け取るactionからaction.payloadでAction実行時に渡された引数を取得できる
  • src/store/index.jsに反映させます
src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
// importを追加
import todoReducer from './todoSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    // 定義を追加
    todo: todoReducer,
  },
});
  • 続いてコンポーネントが使いやすいようにワンクッション挟むuseTodoを作成します
  • src/hooks/useTodo.jsを作成します
src/hooks/useTodo.js
import { useSelector, useDispatch } from 'react-redux';
import { selectTodoList, selectInputText, changeText, add } from '../store/todoSlice';

function useTodo() {
  const dispatch = useDispatch();
  return {
    inputText: useSelector(selectInputText),
    todoList: useSelector(selectTodoList),
    changeText: (inputText) => dispatch(changeText({ inputText })),
    add: (text) => dispatch(add({ text })),
  };
}

export default useTodo;
  • 最後にuseTodoをコンポーネントに適用します
  • src/components/TodoList.jsを修正します
src/components/TodoList.js
import React from 'react';
import { Link } from 'react-router-dom';
// importを追加
import useTodo from '../hooks/useTodo';

function TodoList() {
  // useTodoから値を取得
  const { inputText, todoList, changeText, add } = useTodo();

  // 呼び出す関数をchangeTextに変更
  const onChangeText = event => changeText(event.target.value);

  const onClickAdd = () => {
    // 呼び出す関数をaddに変更
    add(inputText);
    // 呼び出す関数をchangeTextに変更
    changeText('');
  };

  // 以下修正なし
  return (
    <div>
      <h1>TodoList</h1>
      <input onChange={onChangeText} value={inputText} />
      <button onClick={onClickAdd}>追加</button>
      {todoList.map((todo, i) => (
        <p key={i}>{todo.text}</p>
      ))}
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/counter">Counterへ</Link>
      </p>
    </div>
  );
}

export default TodoList;
  • これで完成です
  • 動作確認してみましょう

todolist-redux.gif

ReactReduxの特徴

  • useStateの時は状態管理はコンポーネントに閉じていたためコンポーネントが破棄されると状態も破棄されていました
  • ReactReduxを使うとコンポーネントの外で状態を管理するためコンポーネントが破棄されても状態を維持することができます
    • それにともなってソースコードも状態管理とコンポーネントと分離されてGoodですね
  • 状態がコンポーネントの外で管理されているためどのコンポーネントからでも状態にアクセス可能にもなります
    • 試しにsrc/components/Home.jsを以下のようにしてみましょう
src/components/Home.js
import React from 'react';
import { Link } from 'react-router-dom';
// importを追加
import useCounter from '../hooks/useCounter';
import useTodo from '../hooks/useTodo';

function Home() {
  // 値を取得
  const { count } = useCounter();
  const { todoList } = useTodo();
  return (
    <div>
      <h1>Home</h1>
      <p>現在のCounterの値: {count}</p>
      <p>現在のTodoListの件数: {todoList.length}</p>
      <p>
        <Link to="/counter">Counterへ</Link>
        <Link to="/todo-list">TodoListへ</Link>
      </p>
    </div>
  );
}

export default Home;
  • Homeコンポーネントからでも管理している状態にアクセスできることが確認できます

home-redux.gif

  • どのコンポーネントからでもアクセスはできますが、あくまでメモリ上で保持しているだけなのでリロードすると値は消えてしまいます
    • 永続化したい場合はサーバに送信して保存しておく必要があります

まとめ

  • useStateを使ったコンポーネント内での状態管理と、ReactReduxを使ったアプリ全体での状態管理を紹介しました
  • ReactReduxを使うと登場人物が増えて複雑になってくるところもあるので規模や要件に応じて使い分けるとよいでしょう

蛇足

  • 今回の例では問題は起きないがuseTodoの中でuseSelector(selectInputText)useSelector(selectTodoList)を呼んでいるため、どちらか片方しか使わないコンポーネントがもう一方の値が更新された時にも再レンダリングされてしまう
  • useSelectorの存在意義からしてもコンポーネントで必要なものだけuseSelectorする方がいいのかもしれない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactのチュートリアルの三目並べをjQueryでやる

前回作った三目並べをjQueryで書いてみようと思います。
どれくらい煩雑になるのだろう。

数値の表示のみ

index.html
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="css/index.css" />
    <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.4.1.min.js"></script>
    <script src="js/index.js"></script>
  </head>
  <body>
    <div id="root">
      <div class="game">
        <div class="gmae-board">
          <div>
            <div class="board-row">
              <button class="square"></button><button class="square"></button
              ><button class="square"></button>
            </div>
            <div class="board-row">
              <button class="square"></button><button class="square"></button
              ><button class="square"></button>
            </div>
            <div class="board-row">
              <button class="square"></button><button class="square"></button
              ><button class="square"></button>
            </div>
          </div>
        </div>
        <div class="game-info">
          <div>次の手番: X</div>
          <div>
            <!--<li><button>Go to game start</button></li>-->
            <!-- <li><button>Go to move #1</button></li> -->
          </div>
        </div>
      </div>
    </div>
  </body>
</html>
index.css
body {
  font: 14px 'Century Gothic', Futura, sans-serif;
  margin: 20px;
}

ol,
ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: '';
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

以下処理だけで行けそうです。

index.js
$(function() {
  $(".square").each((index, elem) => {
    $(elem).text(index);
  });
});

Live Serverの拡張をインストールして、以下からブラウザを起動します。
image.png
出来てました!
image.png

XとOを入力できるようにする

基本的な設計はチュートリアルを真似します。

index.js
$(function() {
  let xIsNext = true;

  $(".square").click(function() {
    $(this).text(xIsNext ? "X" : "O");
    xIsNext = !xIsNext;
  });
});

image.png

履歴なしの完成までもっていく

index.js
//import $ from "jQuery";

$(function() {
  const squares = new Array(9).fill(null);
  let xIsNext = true;
  const status = $(".game-info div:first-child");

  $(".square").click(function() {
    // 押されたsquareのインデックス
    const index = $(".square").index(this);

    if (calculateWinner(squares) || squares[index]) {
      return;
    }

    squares[index] = xIsNext ? "X" : "O";
    $(this).text(squares[index]);
    status.text("次の手番: " + (xIsNext ? "O" : "X"));
    xIsNext = !xIsNext;
  });

  // 勝敗判定関数(公式チュートリアルから拝借)
  function calculateWinner(squares) {
    const lines = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      [0, 4, 8],
      [2, 4, 6]
    ];
    for (let i = 0; i < lines.length; i++) {
      const [a, b, c] = lines[i];
      if (
        squares[a] &&
        squares[a] === squares[b] &&
        squares[a] === squares[c]
      ) {
        return squares[a];
      }
    }
    return null;
  }
});

ダウンロード (2).gif

一旦完成

ここまでだと、Reactよりソースコード短いかな?

履歴機能を持たせる

履歴機能を持たせて、最後まで作ります。

index.html
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="css/index.css" />
    <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.4.1.min.js"></script>
    <script src="js/index.js"></script>
  </head>
  <body>
    <div id="root">
      <div class="game">
        <div class="gmae-board">
          <div>
            <div class="board-row">
              <button class="square"></button><button class="square"></button
              ><button class="square"></button>
            </div>
            <div class="board-row">
              <button class="square"></button><button class="square"></button
              ><button class="square"></button>
            </div>
            <div class="board-row">
              <button class="square"></button><button class="square"></button
              ><button class="square"></button>
            </div>
          </div>
        </div>
        <div class="game-info">
          <div>次の手番: X</div>
          <div>
            <li><button>Go to game start</button></li>
            <!-- <li><button>Go to move #1</button></li> -->
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

jsは信じられないくらいワケわかめになりました。
もっとスマートにしたかった・・・。
(行数長かったんでコメント右に書いてます)

index.js
//import $ from "jQuery";

$(function() {
  // 履歴
  const history = [{ squares: new Array(9).fill(null) }];

  let stepNumber = 0; // 現在表示している履歴のインデックスを表す
  const status = $(".game-info div:first-child"); // 手番
  const allJqSquare = $(".square"); // 全square jQueryオブジェクト

  // 最初の履歴ボタン
  const startButton = $(".game-info li")
    .find("button:first")
    .click(historyButtonClick.bind(null, 0)); // bindで引数を縛る

  // square押下処理
  allJqSquare.click(function() {
    const index = allJqSquare.index(this); // 押されたsquareのインデックス
    const current = history[stepNumber]; // 現在のsquares
    const squares = current.squares;

    if (calculateWinner(squares) || squares[index]) {
      return;
    }

    const newSquares = squares.concat(); // squaresのコピー
    newSquares[index] = stepNumber % 2 === 0 ? "X" : "O";
    $(this).text(newSquares[index]);
    status.text("次の手番: " + (stepNumber % 2 === 0 ? "O" : "X"));

    // カレントのstepNumberより後ろのボタンを削除する
    $(".game-info li").each((i, element) => {
      if (i > stepNumber) {
        $(element).remove();
      }
    });

    // 現在のステップまで履歴も削除
    for (; stepNumber + 1 < history.length; ) {
      history.pop();
    }

    history.push({ squares: newSquares }); // 新しい要素追加
    stepNumber++;

    // 新たに履歴ボタンを作る
    // 親のliごとクローンする
    startButton
      .parent()
      .clone()
      .appendTo(startButton.parent().parent()) // .game-infoに追加
      .children("button")
      .text("Go to move #" + stepNumber) // ボタン名変更
      .click(historyButtonClick.bind(null, stepNumber)); // bindで引数を縛る
  });

  // 履歴ボタン押下処理
  function historyButtonClick(sNumber) {
    stepNumber = sNumber;
    status.text("次の手番: " + (stepNumber % 2 === 0 ? "X" : "O")); // ここは上とは反対になる

    // 全squareのテキストを書き直す
    history[sNumber].squares.forEach((value, i) => {
      allJqSquare.eq(i).text(value);
    });
  }

  // 勝敗判定関数(公式チュートリアルから拝借)
  function calculateWinner(squares) {
    const lines = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      [0, 4, 8],
      [2, 4, 6]
    ];
    for (let i = 0; i < lines.length; i++) {
      const [a, b, c] = lines[i];
      if (
        squares[a] &&
        squares[a] === squares[b] &&
        squares[a] === squares[c]
      ) {
        return squares[a];
      }
    }
    return null;
  }
});

感想

履歴がないバージョンまではスラスラできたんですが、履歴のとこで死にました。

私のやり方が悪いのもあるかもしれませんが、Reactのメリットを実感しました!tbn

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

GraphQL Mesh は何を解決するのか? ~ Qiita API を GraphQL でラップして理解する GraphQL Mesh ~

image.png

GraphQL Mesh とは

The Guild から GraphQL Mesh が発表されました。

GraphQL Mesh は REST API や gRPC などの既存のバックエンド API サービスと接続するプロキシとして機能します。
GraphQL Mesh は、開発者が他の API 仕様(gRPC、OpenAPI、Swagger、oData、SOAP、GraphQL など)で記述されたサービスに対して、GraphQL のクエリを通じて簡単にアクセス可能にすることを目的として作られました。

従来、GraphQL プロキシを実装するためには、バックエンド API サービスに対して以下の作業を行う必要がありました。

  • その API 仕様を読み解き、
  • GraphQL サーバを構築し、
  • スキーマ、リゾルバ、バックエンド API との通信処理を実装する

複数のバックエンド API をラップする GraphQL サーバを実装するためだけに多大な労力を割いていたのです。

もちろん、openapi-to-graphql のように、OpenAPI 定義を GraphQL のスキーマに読み換えるツールや、スキーマ定義からモックサーバを構築する graphql-tools などは登場していました。
今回登場した GraphQL Mesh は革新的です。バックエンド API の API 仕様さえあれば、そのバックエンド API に対して GraphQL クエリが即座に実行できる GraphQL プロキシが手に入ります。

本記事では GraphQL Mesh の簡単な使用方法とアーキテクチャの構成パターンについて解説します。

※ 本記事は こちら の記事を参照しています。

使用方法

バックエンドに OpenAPI で記述された REST API サービスがあることを想定して、GraphQL Mesh によるプロキシサーバを構築します。今回バックエンドの API は Qiita API を使用します。

1. インストール

GraphQL Mesh はいくつかのコアライブラリを組み合わせてインストールします。

$ yarn add graphql \
           @graphql-mesh/runtime \
           @graphql-mesh/cli \
           @graphql-mesh/openapi

使用可能な API(と実装予定の API)は 3/25 現在、以下の通りです。

Package Status Supported Spec
@graphql-mesh/graphql Available GraphQL endpoint (schema-stitching, based on graphql-tools-fork)
@graphql-mesh/federation WIP Apollo Federation services
@graphql-mesh/openapi Available Swagger, OpenAPI 2/3 (based on openapi-to-graphql)
@graphql-mesh/json-schema Available JSON schema structure for request/response
@graphql-mesh/postgraphile Available Postgres database schema
@graphql-mesh/grpc Available gRPC and protobuf schemas
@graphql-mesh/soap Available SOAP specification
@graphql-mesh/mongoose Available Mongoose schema wrapper based on graphql-compose-mongoose
@graphql-mesh/odata WIP OData specification

2. 設定ファイルにバックエンド API の API 仕様を記述する

次に、.meshrc.yaml というファイルを作成し、バックエンド API の API 仕様を記述しましょう。今回は OpenAPI を使用します。他にも gRPC、oData、SOAP、GraphQL などをサポートしています。.meshrc.yaml はプロジェクトのルートディレクトリに配置します。

sources:
  - name: Qiita
    handler:
      openapi:
        source: ./qiita.openapi.yaml

QiitaAPI の OpenAPI 定義 .qiita.openapi.yaml は以下のように記述しています。

Qiita APIの仕様 (OpenAPI)
swagger: "2.0"

info:
  version: 0.0.1
  title: Qiita API

host: "qiita.com"

basePath: "/api/v2"

schemes:
  - https

consumes:
  - application/json

produces:
  - application/json

paths:
  "/tags/{tagId}/items":
    get:
      parameters:
        - in: path
          name: tagId
          type: string
          required: true
        - $ref: "#/parameters/pageParam"
        - $ref: "#/parameters/perPageParam"
      responses:
        "200":
          description: 指定されたタグが付けられた投稿一覧を、タグを付けた日時の降順で返します。
          schema:
            title: タグ記事一覧
            type: array
            items:
              $ref: "#/definitions/Item"
  "/users/{userId}":
    get:
      parameters:
        - in: path
          name: userId
          type: string
          required: true
      responses:
        "200":
          description: ユーザを取得します。
          schema:
            $ref: "#/definitions/User"
  "/users/{userId}/items":
    get:
      parameters:
        - in: path
          name: userId
          type: string
          required: true
        - $ref: "#/parameters/pageParam"
        - $ref: "#/parameters/perPageParam"
      responses:
        "200":
          description: ユーザの投稿の一覧を作成日時の降順で返します。
          schema:
            title: ユーザー記事一覧
            type: array
            items:
              $ref: "#/definitions/Item"
  "/items":
    get:
      parameters:
        - $ref: "#/parameters/pageParam"
        - $ref: "#/parameters/perPageParam"
        - name: query
          in: query
          description: 検索クエリ
          required: false
          type: string
      responses:
        "200":
          description: 投稿の一覧を作成日時の降順で返します。
          schema:
            title: 記事一覧
            type: array
            items:
              $ref: "#/definitions/Item"
parameters:
  pageParam:
    in: query
    name: page
    description: ページ番号 (1から100まで)
    type: number
  perPageParam:
    in: query
    name: per_page
    description: 1ページあたりに含まれる要素数 (1から100まで)
    type: number
definitions:
  ErrorMessage:
    description: エラーの内容を説明するmessageプロパティと、エラーの種類を表すtypeプロパティで構成されます
    type: object
    properties:
      message:
        type: string
      type:
        type: string
  Group:
    description: "Qiita:Teamのグループを表します。"
    type: object
    properties:
      created_at:
        type: string
      id:
        type: integer
      name:
        type: string
      private:
        type: boolean
      updated_at:
        type: string
      url_name:
        type: string
  Tag:
    description: タグ
    properties:
      name:
        type: string
        example: Ruby
      versions:
        type: array
        items:
          type: string
          example: 0.0.1
  User:
    properties:
      description:
        description: 自己紹介文
        type: string
      facebook_id:
        type: string
      followees_count:
        description: このユーザがフォローしているユーザの数
        type: integer
      followers_count:
        description: このユーザをフォローしているユーザの数
        type: integer
      github_login_name:
        type: string
      id:
        type: string
      items_count:
        description: "このユーザが qiita.com 上で公開している投稿の数 (Qiita:Teamでの投稿数は含まれません)"
        type: integer
      linkedin_id:
        type: string
      location:
        type: string
      name:
        type: string
      organization:
        type: string
      permanent_id:
        description: ユーザごとに割り当てられる整数のID
        type: integer
      profile_image_url:
        description: 設定しているプロフィール画像のURL
        type: string
      twitter_screen_name:
        type: string
      website_url:
        type: string
  Item:
    type: object
    properties:
      rendered_body:
        type: string
      body:
        type: string
      coediting:
        type: boolean
      comments_count:
        type: integer
      created_at:
        type: string
      id:
        type: string
      likes_count:
        type: string
      private:
        type: boolean
      reactions_count:
        type: integer
      title:
        type: string
      updated_at:
        type: string
      url:
        type: string
      page_views_count:
        type: integer
      tags:
        type: array
        items:
          $ref: "#/definitions/Tag"
      user:
        $ref: "#/definitions/User"
      group:
        $ref: "#/definitions/Group"

3. GraphQL Mesh サーバを起動する

GraphQL Mesh サーバを起動します。以下コマンドは npm scripts に設定しておくと良いでしょう。

$ yarn graphql-mesh serve
yarn run v1.22.4
info: ?️ => Serving GraphQL Mesh GraphiQL: http://localhost:4000/

http://localhost:4000/GrapiQL が起動します。ブラウザを開いて確認しましょう。

4. GraphQL クエリを実行する

Qiita 記事の情報と、記事に紐づくユーザ情報も合わせて取得します。複数の REST API で取得できる情報をネストして記述し、1回のクエリで取得できることこそが GraphQL の真骨頂です。

query getItems {
  getItems{
    title
    likesCount
    user {
      name
      itemsCount
      organization
      description
    }
  }
}

きちんと取得できているようです。

image

さらに OpenAPI のモデルの定義を正確に読み解き、GraphQL のスキーマ定義にもきちんと反映ができています。素晴らしい。

image

GraphQL Mesh の活用方法

GraphQL Mesh はバックエンド API のプロキシとして機能します。この性質から、クライアントに対する GATEWAY としてふるまい、複数のバックエンドを束ねた構成をとっても良いでしょう。

また、複数のマイクロサービスが内部で相互通信する際に、HUB とする構成を取ることもできます。

まだ開発初期段階らしく、GitHub の README には以下のように記されています。

Note: this project is early and there will be breaking changes along the way

今後大きく変更されることがあるかもしれません。ただ、このツールのコアコンセプトには非常に感銘を受けます。AWS の AppSync などの GraphQL マネージドサービス系がこの考え方を取り入れたら、Web API の業界に大きなインパクトがありそうだと感じました。

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

高階関数の記事のタイトルが長いから、高階関数で処理してみた

概要

  • 高階関数についての記事タイトルが引用を繰り返してたので、高階関数的に処理してみました。

コード

[
  '高階関数を書いたら、中級者になれた気がした。',
  '批判したら上級者になれた気がした。',
  '格下目線から批判してみた。',
  '高階関数でタイトル生成してみた。',
].reduce((stack, title) => stack + '' + title)

// "高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。を格下目線から批判してみた。を高階関数でタイトルを作ってみた。"

高階関数 reduce

  • 高階関数 reduce は、第二引数を指定すると、最初の処理時に stack に指定されます。第二引数を省略すると、最初の要素が stack に指定されて、第二要素から処理がスタートします。

  • 第二要素込みで書き換えると次のようになります。配列の要素が分散されるので、あまりよいコードではなくなると思います。

第二要素込み。
[
  '批判したら上級者になれた気がした。',
  '格下目線から批判してみた。',
  '高階関数でタイトル生成してみた。',
].reduce(
  (stack, title) => stack + '' + title,
  '高階関数を書いたら、中級者になれた気がした。'
)

// "高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。を格下目線から批判してみた。を高階関数でタイトルを作ってみた。"

結論

  • このケースなら Join を使った方が簡単ですね。
join
[
  '高階関数を書いたら、中級者になれた気がした。',
  '批判したら上級者になれた気がした。',
  '格下目線から批判してみた。',
  '高階関数でタイトル生成してみた。',
].join('')

Tips

  • 高階関数は、英語で higher-order functions です。

参考記事

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

チーム開発 3/24

メモです

商品出品機能のサーバーサイドを作っていく

画像を貼る際にすぐに画像が
プレビューできるようにする
複数画像を送信できるようにする

使うメソッド
fields_forは,file_fieldや collection_check_boxesなどと
同じくフォームのインプット要素を生成するフォームヘルパーです。
1つのリソースに対し紐づいた、
複数の別のリソースを同時に保存したい際に利用します。
例えば今回のような1投稿に対して複数の画像をつけるパターンや、
1投稿に対して複数のタグをつける時などにも利用できます。

<%= form_for @product do |f| %>
  商品名<%= f.text_field :name %><br>
  価格<%= f.number_field :price %><br>
  <%= f.fields_for :images do |i| %>
    <%= i.file_field :src %><br>
  <% end %>
  <%= f.submit %>
<% end %>

accepts_nested_attributes_for
fields_forメソッドを利用する際に、
親モデルの中に書く必要があるメソッドです。
以下の例のように、引数として子モデルの名前を書きます。

hoge.rb
class hoge
  has_many :images
  accepts_nested_attributes_for :images
end

accepts_nested_attributes_forメソッドのオプションとして、
引数に書くことができる記述です。このオプションをつけることで、
親のレコードが削除された場合に、関連付いている子のレコードも一緒に削除してくれます。
accepts_nested_attributes_for :images, allow_destroy: true

まとめ
1つのフォームで商品と商品画像を保存
①productモデルに子モデルをまとめて追加・更新するための記述を加える
②newアクションでproductモデルと、それにひも付くimageモデルのインスタンスを作成する
③ビューでfields_forを使うことで、@productに関連付いているimageの数(newなら1つ、editなら登録した枚数分)だけ入力欄が生成される
④最後に、コントローラでレコードを保存する処理を加える。ストロングパラメータの形が独特なので注意する。

画像を複数枚投稿できるようにjsでフォームを生成する
①画像投稿用のinputボタンに、フォームで画像を選択したタイミングのイベントをセットする。
②①のイベントが発生したら、新しい画像投稿フォームを生成する、というJavaScriptのメソッドを動かす
③生成した画像投稿フォームを画面に追加する

スプリントレビューのため一時停止
デプロイした後
トップページ背景画像が消えていたので修正
Background:url
が使えないため
image_tag
を利用さらに
Position:abusolute
で修正
____________

調べたこと

imgタグにおけるalt
Altタグには、この画像はなんの画像かを
わかりやすくするのに記述するタグ
https://www.kanzaki.com/docs/html/htminfo-alt.html#formula
参考ページ

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

for文

forとは

ループ処理を行う為の、
方法の1つです。

記述方法から見ていきましょう↓↓

for (初期値; 条件式; ステップ) {
  繰り返し処理
}

上記のように記述していきます。

続いて、+1をループ処理していきましょう↓↓

let num=9;//変数「num」を定義

//繰り返し処理
for(num=1; num < 10; num++){
    document.write(num);
}//1,2,3,4,5,6,7,8,9

上記では、
9までの処理が、実行されます。

初期値

今回でいうと、「1」になります。
最初に変数で、「9」と定義されていますが、
for文が開始してから、初期値で上書きされてしまいます。

条件式

今回を見てみると、
10未満の処理まで、表記されることになります。

もし、最初から条件式で、
false(条件式が成り立たない)になる時は、
処理は行われません。(表記なし)

ステップ

増減式と呼ばれます。
初期値を増減していく計算方法になります。

途中で抜け出す方法

記述方法を見ていきましょう↓↓

for ( let num = 0; num<10; num++ ) {

    // 偶数の時(2で割り切れる時)だけ処理を行わない
    if( num % 2 === 0 ) continue;

    // カウンタ変数「num」を表示
    console.log( num );

}//1,3,5,7,9

上記を見て頂くと分かりますが、
if文に「continue」が付け加えられています。

「continue」を使うと、
ループ処理の最中に特定のタイミングだけ処理がされません。

上記では、+1をループしていき、
答えが9になるまで、処理されますが、
「continue」により、
偶数の時だけ、処理がされません。

最後に

・for in文
・for of文

上記2つの記述方法もあります。
内容については、明日まとめていきたいと思います。

本日はありがとうございました。

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

高階関数ってどんなときに使う?

高階関数(higher-order function)が流行っているので、高階関数をどんなときに使うのかを書いてみます。
高階関数というと「関数を関数に渡す」ということをイメージできますが、実際にどういう風に使うものかと聞かれると「関数とかサブルーチン」を渡したいときに使うものだよくらいにしか答えられないのでよくよく考えてみました。

私はよく使うと思うのは次の用途が多いと思います。

  • 実装の切り替えを柔軟にする用途 (関数を受け取る関数)
  • 処理の管理や制御のための用途 (関数を受け取る関数)
  • (省略)関数の構築を簡単にする用途 (関数を返す関数)

特に「関数を返す関数」も高階関数と呼ぶようですが、今回はややこしくなるので高階関数のうちでも「関数を受け取る関数」についてみていきたいと思います。

実装の切り替えを柔軟にする用途

高階関数を使わないと

もし仮に言語に関数に関数を渡すことができなかったとしましょう。
(大抵の言語は関数を関数に渡せるかそれに準ずる機能を提供してます。)

じゃんけんをするプログラムを書いた場合は以下のようになるでしょう。

高階関数を使わないパターン
// じゃんけんする関数
const play = (handA, handB) => {
  // TODO 勝敗の判定処理
  console.log(`handAの勝ち or 引き分け or 負け`)
}

const randomStrategy = () => { /* TODO ランダムで手を出す */ },
const unixTimeModStrategy = () => { /* TODO UNIX時間を3で割った余りによって手を出す */ }

const handA = randomStrategy()
const handB = dayModStrategy()

play(handA, handB)

hand1はランダムに手を出すというrandomStrategyによって手を決め、hadn2はUNIX時間を3で割った余りによって手を出すというunixTimeModStrategyによって手を決めました。
hand1hand2play関数に渡してじゃんけんを行います。

ここで3回勝負に書き換えます。三回勝負するのでplayで手をそれぞれ配列でもらうことにしましょう。

高階関数を使わないパターン(三回勝負)
// じゃんけんする関数
const play = (handsA, handsB) => { // 配列で受け取る
  // TODO 勝敗の判定処理
  console.log(`handsAの勝ち or 引き分け or 負け`)
}

const randomStrategy = () => { /* TODO ランダムで手を出す */ },
const unixTimeModStrategy = () => { /* TODO UNIX時間を3で割った余りによって手を出す */ }

// 3回分の手をあらかじめ作る
const handsA = [randomStrategy(), randomStrategy(), randomStrategy()]
const handsB = [dayModStrategy(), dayModStrategy(), dayModStrategy()]

play(handsA, handsB)

少し怪しいコードになってきました。さらにrandomStrategyを別の戦略に変えます。

高階関数を使わないパターン(三回勝負+戦略変更)
// じゃんけんする関数
const play = (handsA, handsB) => {
  // TODO 勝敗の判定処理
  console.log(`handsAの勝ち or 引き分け or 負け`)
}

const newStrategy = () => { /* TODO 新しい別の戦略で手を出す */ },
const unixTimeModStrategy = () => { /* TODO UNIX時間を3で割った余りによって手を出す */ }

const handsA = [newStrategy(), newStrategy(), newStrategy()] // 新しい戦略で手を生成
const handsB = [dayModStrategy(), dayModStrategy(), dayModStrategy()]

play(handsA, handsB)

ここで戦略を変更しましたが、書き換える箇所が多く改修コストが高いことに気づきます。
さらに三本先取の場合に変更したくなった場合はどうするでしょうか?play関数もplay関数の呼び出し側も書き換えなくてはいけません。じゃんけんプログラムの変更されうる可能性について柔軟性が低いと言えます。

高階関数を使うと

では高階関数を使うとどのように書けるでしょうか?

高階関数を使うパターン
// じゃんけんする関数
const play = (strategyA, strategyB) => { // 関数を渡す
  const handA = strategyA()
  const handB = strategyB()
  // TODO 勝敗の判定処理
  console.log(`strategyAの勝ち or 引き分け or 負け`)
}

const randomStrategy = () => { /* TODO ランダムで手を出す */ },
const dayModStrategy () => { /* TODO 日数を3で割った余りによって手を出す */ }

play(randomStrategy, dayModStrategy)

これを三本勝負に書き換えます。

高階関数を使うパターン(三回勝負)
// じゃんけんする関数
const play = (strategyA, strategyB) => {
  const handA1 = strategyA()
  const handB1 = strategyB()
  const handA2 = strategyA()
  const handB2 = strategyB()
  const handA3 = strategyA()
  const handB3 = strategyB()
  // TODO 勝敗の判定処理
  console.log(`strategyAの勝ち or 引き分け or 負け`)
}

const randomStrategy = () => { /* TODO ランダムで手を出す */ },
const dayModStrategy () => { /* TODO 日数を3で割った余りによって手を出す */ }

play(randomStrategy, dayModStrategy)

playの中を三回勝負に変更すればよく、playの呼び出し元は特に変更する必要はありません。
次に戦略を書き換えてみます。

高階関数を使うパターン(三回勝負+戦略変更)
// じゃんけんする関数
const play = (strategyA, strategyB) => {
  const handA1 = strategyA()
  const handB1 = strategyB()
  const handA2 = strategyA()
  const handB2 = strategyB()
  const handA3 = strategyA()
  const handB3 = strategyB()
  // TODO 勝敗の判定処理
  console.log(`strategyAの勝ち or 引き分け or 負け`)
}

const newStrategy = () => { /* TODO 新しい別の戦略で手を出す */ },
const dayModStrategy () => { /* TODO 日数を3で割った余りによって手を出す */ }

play(newStrategy, dayModStrategy)

変更箇所は非常に少なくて済みました。仮に三本先取に書き換えるとしても呼び出し元の変更の必要はなく、play関数が必要に応じてstrategyAstrategyBを呼び出せばよいので変更は容易です。
このように高階関数を使うと、じゃんけんの戦略のように複数の実装が考えられうる部分の変更を簡単にしてくれるのです。

私の場合は自分で高階関数を実装するほとんどの場合はこの利点を得るためです。

処理の管理や制御のための用途

例えば、JavaScriptでは呼び出し元の処理をロックしないようにIO処理を伴うような重い処理を行うライブラリでは多くの場合はコールバック関数を受け取る高階関数を提供しています。

const fs = require('fs');

fs.readFile("path/to/file.txt", 'utf-8', (err, data) => {
  // 読み込んだファイルを処理
})

// fs.readFileを待たずに以降が処理される。

JavaScriptの場合は特にことパターンが多いです(最近はPromiseが使われますが)。

また、コールバック関数は渡された高階関数側で実行や制御を制御できます。そのため、WebフレームワークのExpressやテストフレームワークのMochaなど多くのフレームワークでは利用者側はコールバック関数を渡す形式で使います。
こうすることでコールバック関数ではアプリケーションが着目すべき内容のみを記述し、面倒な副作用的な部分はフレームワークに任せることができます。

さらにコレクション系の処理ではよく出てきます。コレクション系は複数の値を扱うのですが、その部分の処理をmapfilterreduceなどに任せることで、使う側は1つの値に着目して処理を記述することができるのです。

[1, 2, 3].map(n => n * n)

mapでコレクションの制御は行ってくれているので、for分など使って制御しなくとも良いわけですね。

まとめ

高階関数は複数の実装の切り替えを容易にしてくれたり、コールバック関数の実行を管理したり制御するという使い方が便利だということがわかりました。

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