- 投稿日:2020-03-25T23:39:58+09:00
【未完】楽天市場のAPIを使って買えるマスクを提案してくれるLINEBotを作ってみた
はじめに
ProtoOutStudioというイケイケなスクールの「LINE Bot+APIで表現してアウトプット」という課題で製作したものです。
こちらの1時間でLINE BOTを作るハンズオンの記事をベースにLINEBotを作成しました。
最近の新型ウィルスの影響で、ネットショッピングばかりしているのですが、ちょっと楽天市場のトップ画面を見るのに飽きてきたので、(マスクなどの)必要なものを、(売り切ればかりなので)在庫のある商品で、高額になりすぎて買う気がなくなってしまわない(金額の)範囲で、提案してもらえるLINEBotを考えました。
(Amazonは申請が大変そうに見えたのでお見送りしました)概要と作れなかったところ
概要
- LINEBotにほしいもの、「マスク」と入力したら
- 楽天市場のキーワード検索から「マスク」を検索
- 絞り込み検索で「購入可能」
- 「最安価でソート」
- 「最低金額○円以上」
- 「最高金額○円以下」
- 商品画像付き
- を5つくらい返してくれるBot #### 作れなかったところ
- ほしいもの 「マスク」と入力したら
- × → 楽天市場のキーワード検索から「マスク」を検索
- ○ → 絞り込み検索で「購入可能」
- ○ → 「最安価でソート」
- ○ → 「最低金額○円以上」
- ○ → 「最高金額○円以下」
- ○ → 商品画像付き
- × → を5つくらい返してくれるBot
入力したものをエンコードしてAPIのURLに入れて作成するところと、
(APIをベタでかくと動くBotはできたけど)これを複数表示するためにどこでFor文を回せばよいかわからなかった環境
Node.js v13.7.0
MacBook Pro macOS Mojave
Visual Studio Code v1.43.1できたもの
「マスクある?」と質問することで、楽天市場の中で在庫あり商品、最安価の商品名と商品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}`);参考サイト
- LINEBotでの画像の出しかた
- こういうとこまで行きたかった。。。
- 楽天ウェブサービス: API一覧
感想
深いAPIを操るの大変ですが自分好みの条件の商品をサクッと提案してもらえるBotはそれなりに便利そうなでちゃんと完成させねばです。
きっともっといい書き方や2回書かなくてもいいものとかたくさんある気がしますが、【未完】を取れるように早めにやっつけたいです。
- 投稿日:2020-03-25T23:27:02+09:00
自動更新
自動更新の機能
①何秒かおきに、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.rbclass Api::MessagesController < ApplicationController def index end endRubyのクラス名は、一行目のように::で繋げて装飾することができます。これを、名前空間または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.rbclass 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 ~ endroutes.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.jbuilderjson.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.jbuilderjson.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.jsvar 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メソッドが動くことはありません。
- 投稿日:2020-03-25T23:13:38+09:00
Chromeはwindow.openするときに修飾キーの影響を受ける
たまたま「⌘+クリック」で新規タブを開かせたい機能の実装をしていて見つけた挙動です。知らなかったのでメモ。
window.open の普通の挙動
const $button = document.querySelector('#button'); $button.addEventListener('click', (e) => { window.open('./hoge'); )};のような実装をした場合、通常はボタンクリックで新規タブが開き、新しいタブへ自動的に切り替わります。
修飾キーを押しながらクリックしたときの挙動
同じ実装のまま、ボタンをクリックするときに特定の修飾キーを押し続けてみます。
- macOSの場合はcommandキー
- Windowsの場合はctrlキー
すると新規タブは背面に開きます。
また、同様に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では発生しません。
【追記】 回避策
非常に怪しい実装ですが、
window.open
をsetTimeout
の中で実効すると再現しなくなります。
この記事の執筆時点では回避できましたが、将来的に使えなくなる可能性もあります。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.htmlWindowオブジェクトは怪しい
この挙動に限らず、Windowオブジェクトの挙動はブラウザ依存の部分が多く、リファレンスを読むだけでは動きを把握できない部分が多い印象です。
たとえば、そもそも
window.open
で新規ウィンドウになるのか新規タブになるのかも厳密には仕様化されていません。
(たまたまChromeは第2引数の有無でタブ/ウィンドウの挙動を変えてくれているだけで、将来この挙動が変わる可能性もあります。)要件にウィンドウを開く挙動が絡む場合は実現可能かどうかをよく検討しておいた方が良いかもしれませんね。
- 投稿日:2020-03-25T22:31:38+09:00
【学習記録】『スラスラ読める 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予備校 一章「いいところ診断」に登場する無名関数】
イベント、「onkeydown」「onclick」が実行された時に動く関数。
関数自体(=()=>)に名前はない。「変数.イベント」が実行された時に関数が動く。これが無名関数。【N予備校 三章「集計処理を行うプログラム」に登場する無名関数】
イベント、「'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予備校 一章「いいところ診断」に登場する正規表現】
{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)位置付け的には、公式ドキュメントを読めるようになるための足掛かりと思えば良さそう。これからの勉強に使います。
- 投稿日:2020-03-25T22:23:02+09:00
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>
- 投稿日:2020-03-25T21:58:34+09:00
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ひとまずここまで
- 投稿日:2020-03-25T21:36:35+09:00
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年は本来無いらしいですが…)
長いので最初と最後を。
これだけです。所感
全体的にもっと短くきれいに書けるはず。あまりにも無知。
下記の箇所はfor文をどうにかして、配列の箇所を
...Array(2020)
と簡潔に出来そうな気がするのですが、西暦2020の所が西暦undefinedとなってしまうので現状の記述にしています。const arr = [...Array((2020) + 1).keys()]for (let i = 0; i < arr.length; i++) {最後に
今後はもっと難しいのを書けるようになりたい。
- 投稿日:2020-03-25T20:08:35+09:00
【SpecTest GUI】MonacoEditor + Vue.js/Electron
SpecTest GUI ヘの道(1)
誰向け?
- VSCode で使われている Monaco Editor に興味ある人
- Monaco Editor で Markdown Editor を Electron ベースで 作りたい人
- SpecTest を 応援してくれる 人
尚、今回の結果は以下にコミットしてあります。
はじめに
SpecTest は私が欲しいと思っていた BDD を実現するための汎用フレームワーク。
今回はいつもと趣向を変えて、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-builderChoose 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おぉ。
この辺で 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おぉ、変わった。
splitpanes / vue-monaco / marked / highlight.js
そしてお待ちかね、Monaco Editor の出番です。
vue-monaco
というパッケージがあります。ついでに今回、マルチペインで作業できるようにするつもりなので、splitpanes
というライブラリを入れてしまいます。また、表示用にmarked
とhighlight.js
も入れます。さらにgithub-markdown-css
も入れておきましょう。$ npm install splitpanes $ npm install vue-monaco $ npm install marked $ npm install highlight.js $ npm install github-markdown-cssfontawesome
間違いなく 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.jsimport { 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
を配置します。全体を書くと以下の通り。基本、あったものをざっくり消して、HelloWorld
をMarkdownPane
に変える。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>これで一応動くようにはなる。まだエディタ組み込んでませんが、ペイン分割の動作を確認できます。
こんな感じ。真ん中のスプリッタでぐりぐりと動かせます。
Window の大きさ調整
Electron を使っているとだいたいそうなのだが、ウィンドウのリサイズに追随してくれないコンポーネントが結構ある。なので、ウィンドウ・サイズを Vuex のストアに確保しておき、各コンポーネントで参照できるようにしておく。具体的には、App.vue にハンドラを設置。その際、上部のアプリケーションバーのサイズを含めないようにあらかじめ引き算しておく。軽く 3 引いているのは、誤差でスクロールバーが出たり変な感じになることがあったので気持ち少なめに程度の意味。
まず、store でウィンドウサイズを保存するように修正。
src/store/index.jsexport 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">
methods
にhandleResize
を追加し、ハンドラとして呼ばれるようにmounted
とbeforeDestroy
でリスナーに登録し、ウィンドウサイズを 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 で管理。state
にcode
として追加し、upadteCode
でアップデートできるようにしておく。さっきのと合わせるとこんな感じsrc/store/index.jsexport 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.vue
のhandleResize
で通知するように修正。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> ...ここまでできると、エディタが使える。
やった!
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
に登録してリサイズされるようにメソッドを追加。splitpanes
もdefault-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>おぉー。エディタだ。
おわりに
ここまでで簡単な Markdown Editor ができました。保存とかできないけど。あと、やはりスクロールは連動してほしい。
ということで、次回は スクロール連動 を実現させます。
ここまでの結果は、以下にコミットしてあります。
SpecTest そのものに関しては以下を参照してください。
ではまた次回。
- 投稿日:2020-03-25T19:42:27+09:00
【学習記録】『スラスラ読める 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予備校 三章「集計処理を行うプログラム」】
ここでは、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-25T19:32:54+09:00
[Node.js勉強会]ExpressフレームワークでTODOアプリを作ろう
この記事について
Node.jsの概念や、Expressの使い方について詳しく書いてある記事ではありません。
ある程度の土台が用意してあるので、それを元に実際に手を動かして、TODOアプリを完成させることがこの記事の目的です。pugファイルと仮処理を記述 の部分までは、コピペで進めていただいても大丈夫です。
TODOの処理を記述 の部分から実際に、Node.jsの様々な書き方を試していただけたらと考えております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
で再度立ち上げると治ると思います。手順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.jsvar 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.pugextends 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.jsfunction 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.jsvar 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
コマンドでコンテナを立ち上げなおします。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.jsvar 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
- 投稿日:2020-03-25T18:09:19+09:00
プログラミングを技術目線ではなく、もたらす結果をモチベーションにコードを書いた、プログラミング初心者の末路(僕パターン)
前置き
まずこの記事の対象者は"非エンジニア"です。
非エンジニアである僕が、プログラミングスキルを身につけることを目指し、その過程でどんな取り組みをしたかを説明するしょーもない記事です。
ちなみに今はプログラマーとしてやっています、というオチはなく今も勉強中ですし、そもそも勉強の一貫でこうして記事を書いてあるので、突然カバディ部の顧問にさせられた教師が、そのスポーツのルールを覚えるのと並行して部員にカバディを教えるのと同じような図になると思います。
それでもよければ記事をご覧いただき、「あ、自分が思ってた不満って他の人も感じてたのか」みたいな気持ちになってもらえればいいなと思います。
第一回 『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 で基礎から学べと言われ、書籍を購入してちょっと読んだ
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/e211547438967827661fGAS(Google Apps Script) と LINE Notify を触るだけでよかったので、なんとか動かすところまで進めました。(コピぺだけど)
6.それは動いて少し嬉しかった。←イマココ
プログラムが動くとすごく嬉しいということがわかりました。たとえコピぺでも。
"自分の言うことを聞いてくれた"という感覚が確かにあり、なんとも癖になる感じでした。
無理やり話のオチ
今回、技術的に達成できたことはなかったのですが、やりたいことやろうとして消耗するより、小さい成功体験するほうが、結果的に課題に対して長く取り組める体質になれそうだな。という気づきがありました。
あとは、もっと人に質問できるようにならないと、何かを作るときは効率悪いなと思いました。
"人に質問できない人"の考え方として(僕もそのタイプ)、自分が何をやりたいのかを明確に把握していないことが実はよくあります。(僕)
自分が何をやりたいかを把握していないまま、手段だけを人に質問してしまうので、質問される方は、適切な回答に悩んでしまい怪訝な顔をされてしまう。ということがあると思います。(僕)
質問する方も、実は自分のハードルを上げていて、そもそもどんな手段があるかわからないのに、手段について聞く。という自分の首を締めるようなやり方を無意識的にやっていると思います。(僕)
そういった自分の経験から、人に相談するとき、もっとも力を入れるコミュニケーションは、"自分はこう言うことをしたい"ということだけに絞るようにしました。
本当はもう少し相手のことを考えたコミュニケーションがしたいのですが、まだまだそこまで手が回らないという状況です。
きっと周りには迷惑をかけているのでしょう。
スミマセン...と言うことで、非エンジニアのしょーもない記事でした。
ここまで読んでくださりありがとうございました。
- 投稿日:2020-03-25T17:42:03+09:00
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
- 投稿日:2020-03-25T16:40:34+09:00
なぜTypescriptを使うんですか?
はじめに
皆さん、TypeScriptはもう使っていますか?
既に使っている方でしたら、もう型のない世界に戻りたくない方も多いと思います。
"では、改めてなぜ我々は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を使って行こうとおもいます。
- 投稿日:2020-03-25T16:40:34+09:00
なぜTypeScriptを使うんですか?
はじめに
皆さん、TypeScriptはもう使っていますか?
既に使っている方でしたら、もう型のない世界に戻りたくない方も多いと思います。
"では、改めてなぜ我々は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を使って行こうとおもいます。
- 投稿日:2020-03-25T16:04:04+09:00
JavaScript で音声を順に再生する方法
音声が複数個あっても順に再生したい!!!
非同期処理って難しいですね…
テキストを音声化して再生するといった動作を実装したのですがそこでハマったことをメモとして書き出していきます。概要
- 音楽ファイルを用意する
- 音楽ファイルを DataURI Scheme 形式(base64) にエンコードする
- 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 を使おうかと考えていたのですが、こっちのほうがいい感じのコードになるかなと思われます。
まとめ
テキストチャットの音声化のような複数の音声データを順に再生するような状況になった場合、このように実装すればいいのではないでしょうか。
非同期処理、わかるようでわからず難しいなーと感じました。
もっと勉強頑張ります?
- 投稿日:2020-03-25T16:02:38+09:00
Webデザイナーの仕事内容や働き方について
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デザイナーの働き方
Webデザイナーといっても、様々な働き方があります。働き方にはインハウス、制作会社、SES会社、フリーランスなどがあり、雇用形態や仕事内容なども変わってきます。
インハウス
企業に所属し、自社のWebサイトやサービスを担当するWebデザイナーです。業務の幅が広く、パンフレット作成などいろいろな経験ができることが魅力です。企業によっては、Web担当者が他におらず、全てをやらなければいけない場合があります。デザインの幅は少ないので、いろいろなデザインを担当したい人には向きません。
制作会社
クライアントから依頼を受けてWebサイトを制作します。新規サイト制作や、既存サイトのリニューアルなどを担当します。新規のWebサイト制作の場合、クライアントや案件ごとにデザインをゼロから考えることも多く、デザインセンスが求められます。クライアントの都合で納期が決まるため、仕事に追われることもありますが、様々なデザインを経験したい人には向いています。
SES会社
SES(システムエンジニアリングサービス)会社は、自社で契約しているエンジニアを、外部の会社に客先常駐として派遣する会社のことで、最近ではWebデザイナーにも波及しています。常駐先は一般企業や制作会社などで、大手で働ける機会もあります。未経験でも採用されやすいことがメリットです。業務内容が単純作業になりがちで、スキルアップが難しいというデメリットもあります。
個人で独立して仕事を請負うWebデザイナーです。仕事の請負先は、企業や個人など多岐にわたります。フリーランスは自分で仕事を獲得する営業力が必要ですが、最近では営業代行サービスなども増えてきています。フリーランスは自分の裁量で仕事量などを決められることが魅力です。反面、トラブル対応などすべて自分でやらなければならないため、ある程度の実力と対応力が求められます。
Webデザイナーのしごと内容まとめ
今回はWebデザイナーの仕事内容や働き方について紹介しました。Webデザイナーは単にデザインするだけでなく、Webサイト制作のさまざまな業務に携わる仕事だということがわかっていただけたと思います。Webサイト制作は技術の移り変わりが早く、デザインのトレンドも変化します。Webデザイナーとして成長意欲があり、向上心をもって取り組める人が向ている職業といえるでしょう。
- 投稿日:2020-03-25T15:15:31+09:00
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 の使い方をよく理解できてません。今回みたいな使い方でも同じ動作できるように実装できるのかなあ?
今後改めて勉強したいと思います ?
- 投稿日:2020-03-25T15:02:32+09:00
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.tsimport { 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.tsconst config: Configuration = { // ... devServer: { contentBase: path.resolve(__dirname, 'dist'), }, // ... }これでエラーが消えたはずです。
HtmlWebpackPlugin
の設定詳しくは割愛しますが、このプラグインを使うとテンプレートのHTMLのbodyタグの末尾にscriptタグを勝手に挿入してくれるので便利です。
というわけでインストールします。npm i -D html-webpack-plugin @types/html-webpack-pluginwebpack.config.tsimport 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.tsximport React, { FC } from 'react' export const App: FC = () => <div>Hello World!</div>src/index.tsximport 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で行っているものは少なかったので記事にしました。型定義があるぶん補完も効くので書きやすいです。こんな記事でも誰かのお役に立てば幸いです。ご覧頂きありがとうございました。
- 投稿日:2020-03-25T14:26:20+09:00
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サーバですが、ちゃちゃっとつかうフロントのサーバとしては十分でしょう。おつかれさまでした!
関連リンク
- 投稿日:2020-03-25T13:10:30+09:00
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と素子を接続する
リモコンの赤外線信号を解析する
まずは、赤外線センサーを使ってリモコンの信号を受信するプログラムを作成します。
(確認してないですが、このプログラムを使ってOCR-05W以外のリモコンの信号も受信できると思います。)receive.tsimport 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(); }); }これをコンパイルして実行する。
と表示されるので、リモコンのボタンを押すと、めっちゃ長い信号が受信される。昨日から作っているobniz+赤外線LED+リモコンコンセントをTypeScriptで書き直し中。
— Takahiro Mitsuoka (@tmitsuoka0423) March 25, 2020
まずはリモコン受信。#obniz #iot pic.twitter.com/Uj8A0TqliA無事受信できた。
リモコンコンセントにobnizから信号を送信する
プログラムは以下の通り。
send.tsimport 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+赤外線LED+TypeScriptで信号送信もできた。#obniz #iot pic.twitter.com/6fFlRiVIyr
— Takahiro Mitsuoka (@tmitsuoka0423) March 25, 2020まとめ
obniz+TypeScriptは補完が効くようになるのでオススメ。
- 投稿日:2020-03-25T13:04:38+09:00
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}`);result34.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}`);result270.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のみで実験しましたがブラウザ上だとまた違った結果になりそうです。
初学者ですので間違い等あれば訂正いただけると嬉しいです。
- 投稿日:2020-03-25T11:50:22+09:00
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.jsconst 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と連動して。。。(笑)
- 投稿日:2020-03-25T04:28:58+09:00
N予備校の教材でAjaxでCORSを試してみる
やりたいこと
N予備校のAjaxのページに「同一生成ポリシーを満たさない場合CORSにひかかって通信できません」と書かれているが、そのあたりの回避策について特に紹介されていなかったので
Access-Control-Allow-Origin
を試してみる。前提知識
- CORS https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
- N予備校 https://www.nnn.ed.nico/courses/497/chapters/6891
- この教材をそのまま使います(非会員は見れません)
環境
- 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
- 投稿日:2020-03-25T03:33:07+09:00
【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.jsimport React from 'react'; import ReactDOM from 'react-dom'; import App from './src/App'; ReactDOM.render(<App />, document.getElementById('root'));
src/App.js
の作成src/App.jsimport React from 'react'; import Router from './routes/router'; function App() { return <Router />; } export default App;
src/routes/router.js
を作成src/routes/router.jsimport 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.jsimport 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.jsimport 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.jsimport 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;
- 以下のコマンドで起動すると http://localhost:1234 でアクセスできます
npx parcel index.html
- ここまでだとこんな感じです
useStateを使った状態管理
Counterアプリ
+
を押すとインクリメントされてー
を押すとデクリメントされるサンプルです
- 現在の値をメモリ上で保持しておく必要があるためそこで状態管理が発生するわけですね
src/components/Counter.js
を以下の内容に変更します
- 1行目で
useState
のimportが追加されているようにReactが標準で提供するuseState
を使いますsrc/components/Counter.jsimport 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アプリを作ってみます
- 現在のTodoのリストや現在の入力された値をuseStateを使って管理してみます
src/components/TodoList.jsimport 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
はコンポーネント単位で宣言しています- なのでコンポーネントが破棄されると管理していた状態もすべて破棄されてしまいます
- サンプルアプリでページ遷移して戻ってくると値がリセットされていることからも分かるかと思います
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.jsimport { configureStore } from '@reduxjs/toolkit'; export const store = configureStore({ reducer: { // この中は後で作る }, });
src/App.js
に適用しますsrc/App.jsimport 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.jsimport { 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.jsimport { configureStore } from '@reduxjs/toolkit'; // importを追加 import counterReducer from './counterSlice'; export const store = configureStore({ reducer: { // 定義を追加 counter: counterReducer, }, });
- 次に
counterSlice
に定義したものをコンポーネントが使いやすいようにワンクッション挟むファイルを作ります
- このファイルでやってることを直接コンポーネントで書いても動きます
src/hooks/useCounter.js
を作成しますsrc/hooks/useCounter.jsimport { 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.js
でuseCounter
を使うように修正しますsrc/components/Counter.jsimport 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
からの置き換え完了です- ページ遷移をしても状態が維持されることを確認しましょう
TodoListアプリ
- 同じ要領でTodoListもReactRedux化していきましょう
- まずは
src/store/todoSlice.js
を作成しますsrc/store/todoSlice.jsimport { 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.jsimport { 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.jsimport { 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.jsimport 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;
- これで完成です
- 動作確認してみましょう
ReactReduxの特徴
useState
の時は状態管理はコンポーネントに閉じていたためコンポーネントが破棄されると状態も破棄されていました- ReactReduxを使うとコンポーネントの外で状態を管理するためコンポーネントが破棄されても状態を維持することができます
- それにともなってソースコードも状態管理とコンポーネントと分離されてGoodですね
- 状態がコンポーネントの外で管理されているためどのコンポーネントからでも状態にアクセス可能にもなります
- 試しに
src/components/Home.js
を以下のようにしてみましょうsrc/components/Home.jsimport 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コンポーネントからでも管理している状態にアクセスできることが確認できます
- どのコンポーネントからでもアクセスはできますが、あくまでメモリ上で保持しているだけなのでリロードすると値は消えてしまいます
- 永続化したい場合はサーバに送信して保存しておく必要があります
まとめ
useState
を使ったコンポーネント内での状態管理と、ReactReduxを使ったアプリ全体での状態管理を紹介しました- ReactReduxを使うと登場人物が増えて複雑になってくるところもあるので規模や要件に応じて使い分けるとよいでしょう
蛇足
- 今回の例では問題は起きないが
useTodo
の中でuseSelector(selectInputText)
とuseSelector(selectTodoList)
を呼んでいるため、どちらか片方しか使わないコンポーネントがもう一方の値が更新された時にも再レンダリングされてしまうuseSelector
の存在意義からしてもコンポーネントで必要なものだけuseSelector
する方がいいのかもしれない
- 投稿日:2020-03-25T02:25:11+09:00
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.cssbody { 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の拡張をインストールして、以下からブラウザを起動します。
出来てました!
XとOを入力できるようにする
基本的な設計はチュートリアルを真似します。
index.js$(function() { let xIsNext = true; $(".square").click(function() { $(this).text(xIsNext ? "X" : "O"); xIsNext = !xIsNext; }); });履歴なしの完成までもっていく
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; } });一旦完成
ここまでだと、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
- 投稿日:2020-03-25T01:07:51+09:00
GraphQL Mesh は何を解決するのか? ~ Qiita API を GraphQL でラップして理解する GraphQL Mesh ~
GraphQL Mesh とは
The Guild から GraphQL Mesh が発表されました。
? GraphQL Mesh - Query Anything, Run Anywhere ?https://t.co/PlZpAC9b54
— Urigo (@UriGoldshtein) March 23, 2020
? I'm very proud to announce our new open source library - GraphQL Mesh!
Use #GraphQL to query:
? openapi/Swagger
? gRPC
? SOAP
? SQL
? GraphQL
? More!
Without changing the source!
Thread 1/5 pic.twitter.com/xo0G5smUwpGraphQL 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.yamlQiitaAPI の 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 } } }きちんと取得できているようです。
さらに OpenAPI のモデルの定義を正確に読み解き、GraphQL のスキーマ定義にもきちんと反映ができています。素晴らしい。
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 の業界に大きなインパクトがありそうだと感じました。
- 投稿日:2020-03-25T00:48:59+09:00
高階関数の記事のタイトルが長いから、高階関数で処理してみた
概要
- 高階関数についての記事タイトルが引用を繰り返してたので、高階関数的に処理してみました。
コード
[ '高階関数を書いたら、中級者になれた気がした。', '批判したら上級者になれた気がした。', '格下目線から批判してみた。', '高階関数でタイトル生成してみた。', ].reduce((stack, title) => stack + 'を' + title) // "高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。を格下目線から批判してみた。を高階関数でタイトルを作ってみた。"高階関数 reduce
高階関数 reduce は、第二引数を指定すると、最初の処理時に stack に指定されます。第二引数を省略すると、最初の要素が stack に指定されて、第二要素から処理がスタートします。
第二要素込みで書き換えると次のようになります。配列の要素が分散されるので、あまりよいコードではなくなると思います。
第二要素込み。[ '批判したら上級者になれた気がした。', '格下目線から批判してみた。', '高階関数でタイトル生成してみた。', ].reduce( (stack, title) => stack + 'を' + title, '高階関数を書いたら、中級者になれた気がした。' ) // "高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。を格下目線から批判してみた。を高階関数でタイトルを作ってみた。"結論
- このケースなら
Join
を使った方が簡単ですね。join[ '高階関数を書いたら、中級者になれた気がした。', '批判したら上級者になれた気がした。', '格下目線から批判してみた。', '高階関数でタイトル生成してみた。', ].join('を')Tips
- 高階関数は、英語で higher-order functions です。
参考記事
- 投稿日:2020-03-25T00:26:06+09:00
チーム開発 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 endaccepts_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
参考ページ
- 投稿日:2020-03-25T00:09:31+09:00
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つの記述方法もあります。
内容については、明日まとめていきたいと思います。本日はありがとうございました。
- 投稿日:2020-03-25T00:07:00+09:00
高階関数ってどんなときに使う?
高階関数(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
によって手を決めました。
hand1
とhand2
をplay
関数に渡してじゃんけんを行います。ここで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
関数が必要に応じてstrategyA
、strategyB
を呼び出せばよいので変更は容易です。
このように高階関数を使うと、じゃんけんの戦略のように複数の実装が考えられうる部分の変更を簡単にしてくれるのです。私の場合は自分で高階関数を実装するほとんどの場合はこの利点を得るためです。
処理の管理や制御のための用途
例えば、JavaScriptでは呼び出し元の処理をロックしないようにIO処理を伴うような重い処理を行うライブラリでは多くの場合はコールバック関数を受け取る高階関数を提供しています。
const fs = require('fs'); fs.readFile("path/to/file.txt", 'utf-8', (err, data) => { // 読み込んだファイルを処理 }) // fs.readFileを待たずに以降が処理される。JavaScriptの場合は特にことパターンが多いです(最近は
Promise
が使われますが)。また、コールバック関数は渡された高階関数側で実行や制御を制御できます。そのため、WebフレームワークのExpressやテストフレームワークのMochaなど多くのフレームワークでは利用者側はコールバック関数を渡す形式で使います。
こうすることでコールバック関数ではアプリケーションが着目すべき内容のみを記述し、面倒な副作用的な部分はフレームワークに任せることができます。さらにコレクション系の処理ではよく出てきます。コレクション系は複数の値を扱うのですが、その部分の処理を
map
やfilter
、reduce
などに任せることで、使う側は1つの値に着目して処理を記述することができるのです。[1, 2, 3].map(n => n * n)
map
でコレクションの制御は行ってくれているので、for
分など使って制御しなくとも良いわけですね。まとめ
高階関数は複数の実装の切り替えを容易にしてくれたり、コールバック関数の実行を管理したり制御するという使い方が便利だということがわかりました。