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

Next2D NoCode Toolで絵を描く

描画ツール 本日は、描画ツールを紹介したいと思います。 Tool Areaに雛形として、矩形、楕円、角丸矩形を準備してます。 ツールを選択後、Screen Area上で描画範囲をマウスで選択する事で追加できます。 塗りツール 色設定もTool Areaに設置されてます。 画像の上から、塗りの色、線の色、線の幅となっています。 線の幅が0の時は線の描画は行われず、塗りだけが実行されます。 Screen AreaにShapeを追加した後、Controller Areaから以下の色設定変更が可能です。 - 色変更 - 画像の塗り(繰り返し) - 線形グラデーション - 円形グラデーション ペンツール ペンツールはマウスで自由に線を描く事ができます。 塗りツールの線の幅とカラー設定すると、Screen Areaに直接線を描く事ができます。 変形ツール Tool AreaのShape変形ツールからShapeのパスのカスタマイズが可能です。 四角のポインターがパスのxy座標で、円のポインターがカーブのxy座標になっています。 Shapeの外側もしくは内側をダブルクリックすると新しいパスのポインターが追加されます。 四角のポインターをダブルクリックすると円のポインターが追加され、Deleteボタンで削除する事ができます。 中抜き 中抜きを行いたい場合は新たに雛形などでShapeを追加し、右クリックで表示されるモーダルメニューのIntegrating pathsでShapeを結合させる事ができます。 動作サンプル動画 (僕自身、絵が描けないので下手なサンプル動画となり、すみません。。。) Shape機能は、まだまだ機能が不足していると思いますので、これから順次機能を拡張できればと思っています。 もし、この記事を見て、どんな絵が描けるか試してたい方はこちらから動作確認が可能です。 NoCode Tool 明日はテキストツールを紹介できればと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firestoreでサーバーのタイムスタンプを設定する時はフィールド名をtimestampにする

起きたこと Firestoreのドキュメント追加処理を下記の様に書きました。 ※"firebase": "^9.5.0" ※usersはコレクションの名前です。 ※省略部分で分からない事があれば聞いてください。 import { doc, setDoc, serverTimestamp } from "firebase/firestore"; // 省略 const userDocRef = doc(firestore, "users", "userId"); await setDoc(userDocRef, { ...userDocData, updatedAt: serverTimestamp(), }); すると、こんなエラーが出ました。 Unhandled Rejection (FirebaseError): Function setDoc() called with invalid data Unsupported field value: a function (found in field updatedAt in document users/xxx...) 解決方法 フィールド名をtimestampにする。 import { doc, setDoc, serverTimestamp } from "firebase/firestore"; // 省略 const userDocRef = doc(firestore, "users", "userId"); await setDoc(userDocRef, { ...userDocData, timestamp: serverTimestamp(), }); 以上、経験が浅く迷ってしまったため書き留めました。公式に素直に従うのは大事ですね。 参考 Firestore入門ガイド
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ご注文はNintendo Switchですか??

ご注文はNintendo Switchですか?? Nintendo Switchが欲しい今日この頃、ふと思いました。 HTML、CSSで「Nintendo Switchのロゴ作れるんじゃないか??」と。 ということでJavaScriptで動きを入れつつ、HTMLとCSSでNintendo Switchのロゴを作ってみました。 完成品 完成品は以下となります。(少しバウンドがずれているのはご愛嬌ください) フォントは特に合わせていません。 ソースコード HTMLは以下です。 前提 リセットCSSとしてdestyle.cssを当てています。 CDN: https://cdn.jsdelivr.net/npm/destyle.css@3.0.0/destyle.css バウンドのアニメーションはanime.jsライブラリを使用しています。 CDN: https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js CSSはSassを使用しています。 左のコントローラーは<div id="left-logo"></div>です。 右のコントローラーは<div id="right-logo"></div>です。 HTMLは至ってシンプルな構造です。 index.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="shortcut icon" href="../../images/favicon.png"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/destyle.css@3.0.0/destyle.css"> <link rel="stylesheet" href="css/style.css" type="text/css"> <title>Nintendo Switch</title> </head> <body> <main id="main" class="wrapper"> <div id="logo-container"> <div id="left-logo"></div> <div id="right-logo"></div> </div> <div id="name-container"> <p id="nintendo">NINTENDO</p> <p id="switch">SWITCH<span>TM</span></p> </div> </main> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js" integrity="sha512-z4OUqw38qNLpn1libAN9BsoDx6nbNFio5lA6CuTp9NlK83b89hgyCVq+N5FdBJptINztxn1Z3SaKSKUS5UP60Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="js/script.js"></script> </body> </html> SCSSは以下です。 各コントローラーはdisplay: flexで横並びにしてjustify-content: center;を当てています。 コントローラー自体のスタイルもborder-radiusやborderくらいをいじるくらいでかなりシンプルです。 各コントローラーにある「円」は擬似要素のafterを使用し、position: absolute;で位置を固定しています。あとはborder-radiusをいじって円にするだけです。特に難しくないでしょう。 style.scss @charset "utf-8"; $base-color: #E7000B; $main-color: #fff; html { font-size: 100%; } body { background-color: $base-color; color: $main-color; } .wrapper { max-width: 800px; margin: 0 auto; padding: 0 4%; } #main { display: flex; flex-direction: column; margin-top: 240px; #logo-container { display: flex; justify-content: center; #left-logo { position: relative; width: 132px; height: 270px; margin-right: 14px; border-radius: 70px 0 0 70px; border: 22px solid $main-color; &:after { position: absolute; top: 36px; left: 22px; width: 50px; height: 50px; border-radius: 50%; background-color: $main-color; content: ""; } } #right-logo { position: relative; width: 112px; height: 270px; margin-left: 14px; border-radius: 0 70px 70px 0; background-color: $main-color; &:after { position: absolute; top: 122px; left: 28px; width: 50px; height: 50px; border-radius: 50%; background-color: $base-color; content: ""; } } } #name-container { margin-top: 40px; text-align: center; #nintendo { font-size: 2.6rem; font-weight: bold; letter-spacing: 22px; } #switch { font-size: 4.8rem; font-weight: bold; letter-spacing: 10px; span { font-size: 0.8rem; font-weight: normal; letter-spacing: 0; } } } } ロゴ自体は本当にシンプルな要素やスタイルのみで作成できました! JavaScriptは以下です。 anime()はanime.jsライブラリが用意しているもので、簡単にアニメーションをしてくらる軽量のライブラリです。 moveRightLogo()は右のコントローラーのアニメーションをする関数で、moveLeftLogo()左コントローラのアニメーションをする関数を定義しました。 targetsはアニメーションさせたいセレクターを指定します。 translateYは垂直方向のアニメーションを配列で定義できます。 valueはアニメーションさせる移動距離です。 durationはアニメーションの長さ(ms)です。 delayはアニメーションの開始遅延時間(ms)です。 easingは時間の経過に伴うパラメーターの変化率を指定します。 上記の値はいい感じにアニメーションしてくれるものを指定しました。 script.js $(function () { moveRightLogo(); moveLeftLogo(); }); function moveLeftLogo() { anime({ targets: '#left-logo', translateY: [ { value: 32, duration: 40, delay: 460, easing: "easeInQuint" }, { value: 0, easing: 'easeOutCubic' } ] }); } function moveRightLogo() { // ロゴのボーダーを含む高さを取得する const height = $('#right-logo').outerHeight(); anime({ targets: '#right-logo', translateY: [ // 初期は(高さ/2)から下に落ちるようなアニメーション。 // バウンドした後は高さ0に戻す。 { value: -(height / 2), duration: 0, easing: 'linear' }, { value: 32, duration: 500, easing: 'easeInQuint' }, { value: 0, easing: 'easeOutSine' } ] }); } 参考文献 まとめ この記事では、JavaScriptで動きを入れつつ、HTMLとCSSでNintendo Switchのロゴの作成方法を紹介しました。意外と簡単にできました。 他にも何かロゴを作ってみたいと思います。(リクエストなどあればお願いします!)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebTransportでも4K配信がしたい! 〜データグラムでレイテンシー250ms編〜

WebRTCに変わる新しい技術 WebTransportとは? WebTransportとは、WebRTCがP2Pで使いにくく柔軟性が低いことからそれを補うため、 サーバー・クライアントモデルでリアルタイムに通信できることを目指した技術です。いよいよ年明けのChrome M97でリリースされます。 クラウドゲーミング界からの熱い要求がありますが、低レベルゆえ難しく、クラウドゲーミングへの道のりは遠いのです。(別に目指してないけど) (フルHDなら!)リアルタイムに配信できました! 前回ストリームで配信を行い、 残念ながらChromeがエラーを吐いたりリアルタイムで処理をしてくれなかったりと言う結果になりました。 今回はよりリアルタイムに通信できるデータグラムを使い、さらにロスしたパケットはゼロ埋めすることでエラーが少なくなりました。 4Kは結局、CPU処理能力が足りずカックカクです。。 ただしエンコーダとデコーダは4K対応していて(ダミーデータなら)30Mbpsくらい転送できるので、 H264のハードウェア支援がサポートされれば理屈の上で十分実現可能と思われます。 作ったもの ソースコードはgithubにアップしています。 (CPUが足りていない4Kの図。5K iMac 2020モデル / Intel i5 3GHz / メモリ8GB) こちらはフルHD動画を配信したところです。WebRTCくらいには違和感がありませんでした。(意外にも音声の方が遅延が500msくらいあり、映像は体感250ms程度遅延でした) Chromeさん起きて! 仕事して! 前回、何が問題だったかといいますと、サーバーからデータが飛んできてもChromeがすぐにデータを処理してくれないという現象にありました。 リアルタイム通信処理をするのに、数秒の遅延があるのはリアルタイムとは言えません。 そこで気になったのが公式にもあるechoサンプルです。 あれはすぐにechoが帰ってくるのです。 もしかして、一定時間通信していないとタスクの優先度が後回しにされる? と考えkeepaliveかはたまたheartbeatか、定期的にダミーパケットを送り続けることにしました。 viewer_worker.js // WebWorkerでWebTransportを接続したらダミーデータを定期的に送り続ける let stopped = false; let wt_video = null, frameWriter = null; let wt_audio = null, audioWriter = null; self.addEventListener('message', async (e) => { if (type === "connect") { wt_video = new WebTransport(url + '/video/view'); wt_audio = new WebTransport(url + '/audio/view'); ... // WebTransportを接続したら、1msごとにダミーデータを送り続ける let videoDatagramWriter = wt_video.datagrams.writable.getWriter(); let audioDatagramWriter = wt_audio.datagrams.writable.getWriter(); setInterval(() => { if (!stopped) { const dummy = new ArrayBuffer(1); videoDatagramWriter.write(dummy); audioDatagramWriter.write(dummy); } }, 1); ... // エンコードやら配信処理やら } } こうすることで、Chromeがリアルタイムに受信したデータを処理してくれるようになります! まあ、そのうちアクティブないタブやWebWorkerのパフォーマンスも改善されるでしょう。 WebCodecsやエンコード・デコード処理について WebCodecsはChrome M94でリリース済みであり、エンコーダー・デコーダーと圧縮データ・生データのインターフェース定義を含みます。 メディアからの入出力はまた別のAPIセットになっているようです。 前回の記事をご覧ください。 ストリームとデータグラムの違い 詳しい話は別記事で書きましたが、ざっくりいうとUDPのようにデータの信頼性や到達が保証されないけどリアルタイムで処理できるのがdatagramです。 TCPほど幻覚にやらなくても良いけどデータ保証と順番はいい感じにやって欲しい、というのがstreamです。 _ ストリーム データグラム 方向性 TCPとUDPの良いとこどり UDP 再送 QUICがやってくれる 必要なら自分でやる データ分割 QUICがやってくれる 自分で1パケットに治める データ結合 自分でやる 自分でやる データの長さと終端処理 UICがやってくれる 自分で長さを送る 用途 ストリーミング配信など リアルタイム通信など プログラム解説 それではストリームからデータグラムになって変わったところを見ていきましょう! データ構造にヘッダーをつける ストリームでは1フレーム1ストリームとして総重心しましたが、データグラムはWebTransport一コネクションにつき一つのオブジェクトでやりとりします。 動画か音声かはWebTransportのコネクションが話かけているので混ざることはないですが、1フレーム目と2フレーム目のデータが混ざってしまったら大変です。 データグラムは順番保証もしていないので、いつ何のデータが来るかわかりません。 そこで、フレームごとにストリーム番号をふりそれぞれパケット番号をふって並べ替えるようにしましょう。 パケット番号0にはこれから送信するデータ全体の長さを入れます。 パケット番号1にはヘッダとデータを、以降のパケットも1000バイトずつデータを送るようにします。 パケット番号0 パケット番号1 それ以降のはパケット ストリーム番号(4) ストリーム番号(4) ストリーム番号(4) パケット番号(4) パケット番号(4) 984 - 1984byte データの長さ(4) frame type(1) (終わるまで1000byteずつ) [End Of Packet] timestamp(8) [End Of Packet] _ duration(8) _ _ データ0 - 983byte _ _ [End Of Packet] _ (必ず12byte) (1008byteより少ない) (1008byteより少ない) 送信するときは自分で1000byteくらいに分割する ストリームの時はQUIC側でいい感じにやってくれますが、データグラムだと自分でサイズを決めないといけないようです。 その代わりRDC9000によると、データグラムのQUICパケットはできるだけ分割されないように優先して扱われるようです。 stream_worker.js async function sendBinaryData(datagramWriter, stream_number, data) { // データフォーマット // stream_number(4) // packet_number(4) // data(n) const size = data.byteLength; // 最初にパケット番号0としてデータの長さを送る let header = new ArrayBuffer(4 + 4 + 4); const view = new DataView(header); view.setUint32(0, stream_number); view.setUint32(4, 0); // パケット番号0はデータ全体の長さとする view.setUint32(8, size); datagramWriter.write(header); // データを1000byteずつ送る let count = 0; for (let i = 0; i < size; ) { const len = (size > i + 1000) ? 1000 : size - i; let payload = new Uint8Array(8 + len); const view = new DataView(payload.buffer); view.setUint32(0, stream_number); view.setUint32(4, ++count); payload.set(new Uint8Array(data, i, len), 8); datagramWriter.write(payload.buffer); i += len; } } 受け取る時はストリーム番号とパケット番号で整理する データグラムではデータの順番がバラバラになります。 受け取ったデータの最初の4byteにはストリーム番号があり、次の4byteにはパケット番号を入れています。 パケット番号0には全体の長さを入れてあるのでそれをもとにデータが揃ったか知ることができます。 (今回はパケット番号0がロストすると致命的ですが、再送要求は実装していません) 配列にデータを貯めておく形にします。 viewer_worker.js let size = new Array(); let count = new Array(); let buffer = new Array(); // ストリームとパケット番号ごとにデータをいれる // こんな感じでストリームごとに貯めておきます。 size[2] = 2000; // 全部で2000バイトある count[2] - 1000; // 1000バイト分読み込んだ buffer[2][0] = new Array(1000); // パケット番号1のデータ buffer[2][1] = undefined; // パケット番号2のデータが来るのを待っている状態 データグラム受信 まずはデータグラムを受け取る処理です。 初めてのストリーム番号のデータならバッファを初期化し、 パケット番号0なら全体の長さを格納します。 それ以外のパケットであればデータをパケット番号とともに配列に入れておきます。 viewer_worker.js async function readDatagram(transport, onstream) { let size = new Array(), count = new Array(); let buffer = new Array(); // ストリームとパケット番号ごとにデータをいれる let reader = transport.datagrams.readable.getReader(); while (true) { const { value, done } = await reader.read(); if (done) { self.postMessage('Done read datagram.'); return; } const data = value; // 最初の8バイトを取得して、ストリーム番号とパケット番号を取得する let view = new DataView(data.buffer); const stream_number = view.getUint32(0); const packet_number = view.getUint32(4); // 初めての来たストリームデータなら、バッファを初期化する if (!(stream_number in buffer)) { buffer[stream_number] = new Array(); size[stream_number] = Number.MAX_SAFE_INTEGER; // 最初はストリームの全体の長さがわからない count[stream_number] = 0 } // 最初のパケットにはデータの長さを入れてある if (packet_number === 0) { size[stream_number] = view.getUint32(8); // console.log(`${stream_number} ${packet_number} ${size[stream_number]}`); continue; } buffer[stream_number][packet_number-1] = data.slice(8); // ストリーム番号とパケット番号を除いた残りのデータ全てコピーする count[stream_number] += data.byteLength - 8 // ストリームのデータが全部揃ったら処理をする ... let payload = new Uint8Array(); } データ結合とパケロス対策 次にあるストリームのデータが揃った時の処理を書きます。 つまり動画なら1フレーム分のデータを受信したことになります。 データを結合してデコーダーに渡します。 ただし一つの前のフレームがまだデータとして残ってる場合、結合時に足りないパケットは0埋めして先にそちらを処理しておきます。 フレームをスキップしたりデータの長さが足りないフレームをデコーダーに食わせるとエラーを吐き出して再生が止まってしまいます。 // console.log(`${stream_number} ${packet_number} ${count[stream_number]}/${size[stream_number]}`); if (size[stream_number] === count[stream_number]) { // console.log(new Date(Date.now()).toISOString() + "stream readed! " + stream_number); // 前のフレームがまだ残っている場合は先に処理してしまう。 if (stream_number-1 in buffer) { self.postMessage(`stream ${stream_number - 1} skipped!`); concatFrame(buffer, size, count, stream_number - 1, onstream); } concatFrame(buffer, size, count, stream_number, onstream); } } 結合とパケロスの時に0でデータを埋める処理です。 フレームをスキップしてしまうとデコーダーがエラーを吐く確率が高いです。 0で埋めることでだいぷエラー率を下げることができますが、連続したパケロスにはやはり弱いようです。 (というかローカル環境なのでChromeがポロポロこぼしているだけっぽいです) viewer_worker.js function concatFrame(buffer, size, count, stream_number, onstream) { const buf = buffer[stream_number]; const length = size[stream_number]; // データを結合する let payload = new Uint8Array(length); let pos = 0; for (let i = 0; pos < length; i++) { // データがあればそれを使う。なければ0で埋める if (i in buf) { payload.set(new Uint8Array(buf[i]), pos); pos += buf[i].byteLength; } else { console.log(i); // このパケット番号が来てない! let dummy = new ArrayBuffer(pos + 1000 < length ? 1000 : length - 1000); payload.set(new Uint8Array(dummy), pos); // とりあえずダミーデータで長さを合わせる pos += dummy.byteLength; } } // データを処理する onstream(payload.buffer); // 使い終わったバッファは削除する delete buffer[stream_number]; delete size[stream_number]; delete count[stream_number]; } まとめ ひとまず、リアルタイム通信に必要な基本的なところは大方検証できたように思います。 WebRTCでは制限がかかっていてできなかった数十Mbps帯域の通信や、4K画質のエンコードとデコード それに、4KではないにしろフルHDで違和感のないレベルの画質とフレームレートとレイテンシーを実現できました。 今後はWasmをフル活用してエラー訂正や様々な改良が加えられて色々なサービスが出てくるのではないでしょうか? クラウドゲーミングも良いですが、リモートデスクトップで違和感なく高画質動画を見たりできるのではないでしょうか。 高画質な画面共有も気になるところです。 次はいよいよRustでWebTransportをやるかもしれません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

人々は一年のどのタイミングで時の流れの速さを感じるのか

この記事はTSG Advent Calendar 2021の1日目の記事です。 時が経つのははやい 気付けばもう12月ですね。もう2021年の91%が終わったそうですよ。 2021 is 91% complete. pic.twitter.com/yX1LBkYJo0— Progress Bar 2021 (@ProgressBar202_) November 29, 2021 ところでこのProgress Barボット、たまにTLで流れてくるのを見かけますが、パーセンテージによって反応の大きさに差があるようです。こいつをリツイートするときというのは基本的に「もうそんなに経ったの!?」と感じた時だと思うのですが、人々は一年の何%が過ぎ去った時にこのボットをリツイートしたがるのか調べてみました。 データを収集する Twitter APIは良く知らないので手元のDevToolsを使い気合いでスクレイピングします。 2021/12/01現在でのツイッター画面は、スクロールしていくと画面外に出たツイートのDOMは削除され、新しく表示するべきツイートのDOMが生成されるようです。そこでそのイベントを検知するスクリプトを実行し、手でスクロールしながら情報を収集していくことにします。 // <div area-label="タイムライン: Progress Bar 2021さんのツイート" class="css-1dbjc4n"> // この子ノードにツイートが詰められている const elm = document.querySelector("#react-root > div > div > div.css-1dbjc4n.r-18u37iz.r-13qz1uu.r-417010 > main > div > div > div > div > div > div:nth-child(2) > div > div > div:nth-child(3) > section > div"); const tweets = new Set(); const observer = new MutationObserver((mutationList, _) => { for (let record of mutationList) { for (let node of record.addedNodes) { if (node.innerText.includes("@ProgressBar202_")) { // 例:"Progress Bar 2021\n@ProgressBar202_\n·\n11月18日\n2021 is 88% complete.\n115\n4,668\n2.8万" const text = node.innerText; tweets.add(text); } } } }); observer.observe(elm.children[0], {childList: true}); これをDevToolsで実行した後、気の済むまでスクロールします。今回は2018年の12月まで集めました。 そして const tweetsArr = Array.from(tweets); を実行して現れた配列を右クリックでコピーします。 可視化 ここからはPythonを使って可視化します。先ほどコピーした配列をPythonに持ってきます。 tweets = [ "Progress Bar 2021\n@ProgressBar202_\n·\n11月14日\n2021 is 87% complete.\n77\n4,427\n2.6万", "Progress Bar 2021\n@ProgressBar202_\n·\n11月11日\n2021 is 86% complete.\n114\n4,444\n2.7万", "Progress Bar 2021\n@ProgressBar202_\n·\n11月7日\n2021 is 85% complete.\n137\n7,648\n3.9万", "Progress Bar 2021\n@ProgressBar202_\n·\n11月3日\n2021 is 84% complete.\n84\n4,693\n2.7万", ... これをいい感じのregexで成形し、pandasで表にします。今回日本語環境で収集したので数に「万」が付いてしまっているのでこれにも気を付けます。 import re import pandas as pd table = {"year": [], "percent": [], "reply": [], "RT": [], "fab": []} def parse(x): if x[-1] == "万": return int(float(x[:-1]) * 10000) elif "," in x: return int(x.replace(",","")) else: return int(x) for tw in tweets: m = re.search(r"(\d+).* is (\d+)%.*\n(.*)\n(.*)\n(.*)", tw) table["year"].append(int(m[1])) table["percent"].append(int(m[2])) table["reply"].append(parse(m[3])) table["RT"].append(parse(m[4])) table["fab"].append(parse(m[5])) df = pd.DataFrame(table) 実は2019年4月付近にツイートに誤植がある時期がありました。これを手で修正します。 """ 'Progress Bar 2021\n@ProgressBar202_\n·\n2019年4月2日\nApologies for my previous post. 2019 (Two thousand and *nineteen*) is 25% complete.\n114\n3,858\n1.2万', 'Progress Bar 2021\n@ProgressBar202_\n·\n2019年3月29日\n2018 is 24% complete.\n286\n3,160\n1.3万' """ df.iloc[269].year = 2019 結果を可視化して見ます plt.figure(figsize=(15, 10)) plt.subplot(2,1,1) for year in [2018, 2019, 2020, 2021]: data = df[df.year == year].sort_values("percent") plt.plot(data.percent, data.RT, ".-", label="%d"%year) plt.legend() plt.xlabel("%") plt.xticks(range(0, 101, 10)) plt.title("# of RTs") plt.subplot(2,1,2) for year in [2018, 2019, 2020, 2021]: data = df[df.year == year].sort_values("percent") plt.plot(data.percent, data.fab, ".-", label="%d"%year) plt.legend() plt.xlabel("%") plt.xticks(range(0, 101, 10)) plt.title("# of fabs") plt.show() ピークは 0%, 100% (元日) 10% 20% 25% 50% 69% 75% 80% 90% 99% (年末) であり、50%は新年とおなじくらい盛り上がっていることが分かりました。 不思議ポイントとしてはなぜか70%ではなく69%(9月10日付近)でRT/fabが増えている点でしょうか。「1年の7割が過ぎた」ではなく「1年の7割が過ぎそう」であることの方が人々にとっては感慨深いようです。 まとめ TwitterのProgress Barボット(ProgressBar202_)のRT/fab数をスクレイピングして人々がいつこれに反応しているのかを調べました。基本的にはキリのいい数字になった時か新年に盛り上がっているようですが、7割時点の場合のみ69%で反応が大きくなっているようでした。タイトルの結論は「1年の69%が経つと人々は時の流れの速さを感じる」ということにしようと思います。 --追記-- どうも海外にそういうミーム(下ネタ)があるっぽいです。的外れな考察をしてしまった...結論は50%に変えておきます。 解説:https://www.dailydot.com/unclick/69-nice-meme-twitter/ やたら反応の大きいChrome新バージョン告知:https://twitter.com/ChromiumDev/status/1037022478927912961
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kintone における IE11 サポート終了による JavaScript 開発への影響

ようやく、2022/6/15 で IE11 のサポート終了となります。 これで、kintone で IE11 を意識することなく、JavaScript 開発が出来るようになります。 どのような影響があるかまとめてみます。 概要 JavaScript 開発において、IE11 がサポート対象かどうかで、利用できる構文やライブラリが変わってきます。 kintone のプラグイン開発では、IE11 の動作テストも必須になっていたので、そのテスト工数が無くなるだけでもうれしいことです。 また JavaScript 開発で、アロー関数を使って、より簡潔に記述できます。 とりあえず、IE11 サポート終了で、どのような影響があるかまとめてみます。 MS IE11 のサポート終了 2022年6月15日 2021/5/20 にMSからIE11のサポート終了が公開されました。 特定のオペレーティング システムでの Internet Explorer 11 デスクトップ アプリケーションのサポート終了 kintone の IE11 サポート終了 2022年6月12日 2021/6/15 にサイボウズからのお知らせで、IE11のサポート終了が公開されました。 当社製品・サービスでのInternet Explorer 11サポート終了について(2021/6/17更新) IE11 のシェア 2021/07/10 日本で、4.58%。世界で、1.26%。 日本では、まだ少し使われているようです。 IE11 でしか動作しないサイトがあったりするので残しているとか、サポートの切れた古いPCをそのまま使っているところなどがある気がします。 WRブログ WebブラウザシェアランキングTOP10(日本国内・世界) IE11 の JavaScript 対応状況 Can I use で、各ブラウザーのJavaScript 対応状況を比較できます。 IE11 は、Partial(部分的サポート)がいくつかありますので、IE11のサポート終了後はそれを開発で使えるようになります。 Can I use "JavaScript、JavaScript APIを各ブラウザーで比較" IE11 サポート終了後に、利用できるJavaScript機能 よく使いそうなJavaScript機能です。 let for of文の中で使用できませんでした。 あと IE11 では、変数の定義が他と異なるようです。 for(let a of [1,2,3]){ console.log(a) } MDN, let In Internet Explorer, let within a for loop initializer does not create a separate variable for each loop iteration as defined by ES2015. Instead, it behaves as though the loop were wrapped in a scoping block with the let immediately before the loop. Can I use, let let variables are not bound separately to each iteration of for loops const for in文の中で使用できませんでした。 for(const a in [1,2,3]){ console.log(a) } Can I use, const Not supported in for-in and for-of loops 関数のデフォルト引数 関数の引数省略時の値を指定 MDN, デフォルト引数 function test(x, y = 1) { return x+y; } test(4); アロー関数式 関数定義で、"function" の記述が不要 MDN, アロー関数式 // 伝統的な関数 function (a){ return a + 100; } // アロー関数に分解 // 1. "function" という語を削除し、引数と本体の開始中括弧の間に矢印を配置する (a) => { return a + 100; } // 2. 本体の中括弧を削除と "return" という語を削除 -- return は既に含まれています。 (a) => a + 100; // 3. 引数の括弧を削除 a => a + 100; 名前付きアロー関数.js let test1 = (x, y = 5) => {return x+y}; test1(4); 非同期関数 async/await kintone REST API などの非同期処理に、async/await が使えます。 MDN 非同期関数 目指せ!JavaScriptカスタマイズ中級者(2) 〜Promiseのかわりにasync/await編〜 kintone UI Component v1 ※kintone ライクな UI パーツを簡単に作ることができるライブラリ kintone UI Component v1は、IE11 未サポート なお、kintone UI Component v1 では DateTime がまだ無いので、 V0 から v1 の切り替えは要検討 IE11 を動作対象にする場合は、kintone UI Component v0を利用 ※kintone UI Component v0 は、メンテナンスモードになっている
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

緯度・経度からWeb APIを利用して様々な情報を取得する

はじめに 本記事はMAヒーローズ・リーグ Advent Calendar 2021の12/4の記事となります。 今年も親子でヒーローズリーグ2021に参加しました。作成した作品は「toio地球儀」というもので、toioを2台用いた地球儀型の緯度経度入出力デバイスです。toio地球儀自体に興味がある方は、上の「toio地球儀」からリンクを辿って頂ければと思います。 toio地球儀 本記事では、この「toio地球儀」を利用した1アプリケーションである「Scope」に着目し、どのようにWeb APIを利用し、緯度・経度から有益な情報を取得したかを記載します。ただし、本親子はWeb技術を勉強し始めてからまだ1年足らずですので、記載しているコード上は技術的に稚拙な点があるかと思います。その点はご容赦ください。 地球ブラウザ「Scope」とは 地球ブラウザ「Scope」とは、toio地球儀と連携し、ユーザが入力した位置情報から、その位置に関連する情報をWebブラウザに表示するアプリケーションです。全部で8つのWeb APIを利用しており、緯度・経度から得られる(親子が思いつく限りの)情報を1画面にまとめて提示します。開発時のコンセプトは「神様が人の暮らしを覗き込むときに利用する道具」でした。 神様が人の暮らしを覗き込むときに利用する道具(イメージ) 「Scope」は、気になる場所をクリックすると、その場所に関する今の情報を手軽に確認することができ、暇つぶしにはちょうどよいアプリです。本来はtoio地球儀で緯度・経度を入力して利用するものではありますが、マウス操作にも対応し、一般利用できるようにしたものが↓です。画面左下にある地図をクリックすると、その場所に虫眼鏡マークが付き、その場所に関連する情報が表示されると思います。また、虫眼鏡のそばに表示されるカメラマークは、Webカメラ画像を取得した場所を表しています。 See the Pen sample2 by sunagimo (@sunagimo2) on CodePen. このScopeで表示している情報と、そのときに利用しているWeb APIは次の通りです。 地名:Google Geocoding 現在の気象情報:Azure Maps Weather 街並み画像:Google Street View 周辺地図:Google Maps 近隣のWebカメラ画像:Windy Webcams その地域の急上昇動画:YouTube Data API 地域のニュース:Bing News Search 地域のヒットソング:Spotify Web API 以後、この緯度・経度情報から各種情報への変換に関し、実例を用いて説明します。 緯度・経度から各種情報への変換 Scopeでは、toio地球儀や地図上でのマウスクリックによって取得した位置情報(緯度・経度)を、各種情報に変換して画面に表示します。ここでは、どのようなJavaScriptコードによって情報を取得しているかについて、実例を用いて説明します。 Google Geocodingによる地名情報の取得 Google Geocodingを用いると、緯度経度情報から、その場所の地名情報を取得することが可能です。下の例は、(36.8, 137.9)の緯度経度情報から取得した地名情報です。 本機能を利用するには、Google Cloud Platform で Geocoding API を有効にし、本APIを利用するためのAPI Keyを取得しておく必要があります。この手順に関しては、各種記事がありますので参考にするとよいと思います。 Geocoding API の API Key が取得できましたら、次のようなコードにより緯度経度(変数ichi)から地名情報を取得します。 <!DOCTYPE html> <html lang="ja"> <head> <script src="https://unpkg.com/jquery/dist/jquery.min.js"></script> <script src="https://maps.googleapis.com/maps/api/js?key=[取得したAPIキー]&libraries=&v=weekly" defer></script> </head> <body> <script> // 取得する緯度経度情報 let ichi = { lat: 36.8, lng: 137.9 }; let geocoder; // ページの最初 $(document).ready(function () { // Geocoder の生成 geocoder = new google.maps.Geocoder(); // 緯度経度情報から地名の取得 geocoder.geocode({ 'location': ichi }, function (results, status) { if (status == 'OK') { console.log(results); } }); }); </script> </body> </html> resultsの中に地名情報が格納されており、基本的にはresults[0]の中のaddress_componentsを辿っていけば、住所がすべて取得できます。また、address_componentsのtypesにcontryが含まれるものは国名を示しており、その時のshort_nameが「Country codes」となります。このContry codesは他のWeb API の入力値として必要になりますので、取得できた場合は変数に保存しましょう。 例えば、上のコードを実行した場合は、次のような結果が取得できます。この場合、results[0].address_components[4].short_name がその緯度経度のCountry codesです。 Azure Maps Weatherによる現在の気象情報の取得 Azure Maps Weather を利用すると、緯度経度情報から、その場所の気象情報を取得することが可能です。下の例は、(36.8, 137.9)の緯度経度情報から取得した気象情報です。 本機能を利用するには、Microsoft Azure で Azure Maps を有効にし、本APIを利用するためのAPI Keyを取得しておく必要があります。 API Key が取得できましたら、次のようなコードにより気象情報を取得します。関数引数のlatとlngには、それぞれ緯度、経度を数値で入力します。 <!DOCTYPE html> <html lang="ja"> <head> <script src="https://unpkg.com/jquery/dist/jquery.min.js"></script> </head> <body> <script> // Microsoft Azure で取得した API Key let AzureMapKey = [取得したAPIキー]; // 取得する緯度経度情報 let ichi = { lat: 36.8, lng: 137.9 }; // ページの最初 $(document).ready(function () { update_weather(ichi.lat, ichi.lng); }); // 現在位置の天気情報を取得 function update_weather(lat, lng) { $.ajax({ url: 'https://atlas.microsoft.com/weather/currentConditions/json?api-version=1.0&language=ja-JP&subscription-key=' + AzureMapKey + '&query=' + lat.toString() + ',' + lng.toString(), type: 'GET', }).then( function (results) { console.log(results); }, function () { console.log("error"); } ); } </script> </body> </html> 成功すると、次のような結果が取得できます。results[0] の中を辿っていけば、ほしい情報が得られるはずです。なお、海の上など、気象情報が提供されていない地域に関しては、本関数が失敗を返すことが結構あります。エラー対策はしっかり行っておきましょう。 Google Street Viewによる街並み画像の取得 Google Street View を利用すると、緯度経度の街並み画像(Street view)を取得することができ、マウス操作で街並み画像の拡大縮小、向きの変更などができるようになります。下の例は、(36.8, 137.9)の緯度経度情報から取得した街並み画像です。 本機能を利用するには、Google Cloud Platform で Maps JavaScript API を有効にし、本APIを利用するためのAPI Keyを取得しておく必要があります。上の Geocoding API ですでにAPI Key を生成している場合、そのKeyの利用範囲に、本APIも含めておくと、一つのKeyで利用法とも利用することが可能です。 なお、Street Visa Static API という、もっとぴったりの名前のAPIもありますが、そちらはマウス操作に対応していません。 API Key が取得できましたら、次のようなコードで街並み画像を取得します。なお、街並み画像を表示する領域用に、panorama_areaをidとして持つ領域が定義されているものとします。 <!DOCTYPE html> <html lang="ja"> <head> <script src="https://unpkg.com/jquery/dist/jquery.min.js"></script> <script src="https://maps.googleapis.com/maps/api/js?key=[取得したAPIキー]&libraries=&v=weekly" defer></script> </head> <body> <div id="panorama_area" style="width:200pt; height:200pt;"></div> <script> // 取得する緯度経度情報 let ichi = { lat: 36.8, lng: 137.9 }; let panorama; let StreetViewService; let search_radius = 5000; // 5km // ページの最初 $(document).ready(function () { // Street View の定義 panorama = new google.maps.StreetViewPanorama( document.getElementById("panorama_area"), { linksControl: false, panControl: false, zoomControl: false, addressControl: false, clickToGo: false, imageDateControl: false, showRoadLabels: false, panControl: false, zoom: 0, } ); // StreetViewService StreetViewService = new google.maps.StreetViewService(); update_panorama(ichi.lat, ichi.lng); }); // 指定した緯度経度の半径5km内にある街並み画像を取得 function update_panorama(lat, lng) { search_lating = new google.maps.LatLng(lat, lng); StreetViewService.getPanorama({ location: search_lating, preference: 'nearest', radius: search_radius }, check_street_view_pos).catch(errObj => { //例外を捕まえる必要がある。 }); } // 結果取得時のコールバック関数 function check_street_view_pos(data, status) { if (status === "OK") { // 街並み画像が見つかったので、その画像を表示 panorama.setPosition(data.location.latLng); } else { // 街並み画像が見つからなかった } } </script> </body> </html> 緯度経度からの街並み画像の表示は、街並み画像の取得と、表示に分けて実行します。街並み画像の取得には、StreetViewService を利用し、指定した緯度経度に最も近い街並み画像のある地点を検索します。このとき、検索範囲をm単位で取得することが可能です。日本では、数kmの検索範囲を設定すれば、まず街並み画像は見つかりますが、海外ではかなり範囲を広くしないと画像が取得できな場合があります。 街並み画像が見つかった場合、StreetViewPanoramaを利用し、その位置の街並み画像を画面に表示します。 Google Mapsによる周辺地図の取得 有名なGoogle Maps を利用すると、緯度経度の周辺地図を表示することができます。下の例は、(36.8, 137.9)の緯度経度情報から取得した周辺地図画像です。 この機能も、「Google Street Viewによる街並み画像の取得」同様、Maps JavaScript API を有効にすることで利用可能です。次のようなコードで街並み画像を取得します。なお、周辺地図画像を表示する領域用に、map_areaをidとして持つ領域が定義されているものとします。 <!DOCTYPE html> <html lang="ja"> <head> <script src="https://unpkg.com/jquery/dist/jquery.min.js"></script> <script src="https://maps.googleapis.com/maps/api/js?key=[取得したAPIキー]&libraries=&v=weekly" defer></script> </head> <body> <div id="map_area" style="width:200pt; height:200pt;"></div> <script> // 取得する緯度経度情報 let ichi = { lat: 36.8, lng: 137.9 }; let map; // ページの最初 $(document).ready(function () { // Map の定義 map = new google.maps.Map( document.getElementById("map_area"), { // 周辺地図表示時のオプション zoom: 2, clickableIcons: false, fullscreenControl: false, mapTypeControl: false, streetViewControl: false, zoomControl: false, draggable: true, }); update_map(ichi.lat, ichi.lng); }); // 指定した緯度経度を中心とする地図を表示する function update_map(lat, lng) { lating = new google.maps.LatLng(lat, lng); map.setCenter(lating); } </script> </body> </html> 周辺地図には、フキダシアイコンや画像などを配置することも可能です。 Windy Webcamsによる近隣のWebカメラ画像の取得 Windy Webcams を利用すると、特定の緯度経度の近くに配置されたWebカメラから画像を取得することができます。下の例は、(36.8, 137.9)の緯度経度情報から取得したWebカメラ画像です。 本機能を利用するには、Windy のサイトで Webcams API を有効にし、本APIを利用するためのAPI Keyを取得しておく必要があります。趣味で利用する範囲であれば、無料枠で行けると思います。 API Keyが取得できたら、次のコードでWebカメラ画像を取得できます。 <!DOCTYPE html> <html lang="ja"> <head> <script src="https://unpkg.com/jquery/dist/jquery.min.js"></script> </head> <body> <img id="windy" style="width:400pt; height:200pt;"></img> <script> // 取得する緯度経度情報 let ichi = { lat: 36.8, lng: 137.9 }; // ページの最初 $(document).ready(function () { update_windy(ichi.lat, ichi.lng); }); // 緯度経度(lat, lng)に最も近いWebカメラ画像の取得 function update_windy(lat, lng) { $.ajax({ url: 'https://api.windy.com/api/webcams/v2/list/orderby=distance/nearby=' + lat.toString() + ',' + lng.toString() + ',10000/limit=1?show=webcams:image,location', type: 'GET', headers: { 'x-windy-key': [取得したAPIキー], }, }).then( function (result) { if (result.result.webcams.length != 0) { console.log(result); // カメラ画像 document.getElementById("windy").src = result.result.webcams[0].image.current.preview; } else { console.log("no camera"); } }, function () { } ); } </script> </body> </html> 上のコードを実行すると、次のような結果が帰って来ます。result.result.webcams[0].image.current.preview を img のsrcに設定すれば、その画像を表示することが可能です。 YouTube Data APIによるその地域の急上昇動画の取得 YouTube Data API を利用すると、その国のYouTube 急上昇動画を取得することができます。下の例は、2021/11/28時点の日本国の急上昇動画です。Google がどのようなアルゴリズムで急上昇動画を選定しているかはわかりませんが、一応、その時点で最も着目すべきYouTube動画ではあるようです。 本機能を利用するには、Google Cloud Platform で YouTube Data API V3 を有効にし、本APIを利用するためのAPI Keyを取得しておく必要があります。上の Geocoding API ですでにAPI Key を生成している場合、そのKeyの利用範囲に、本APIも含めておくと、一つのKeyで利用法とも利用することが可能です。 API Keyが取得できたら、次のコードでその国の急上昇動画を取得できます。なお、引数として入力する国名は上記Google Geocoding API で取得したアルファベット2文字の「Country codes」が必要です。例えば日本では "JP"、アメリカでは "US"です。 <!DOCTYPE html> <html lang="ja"> <head> <script src="https://unpkg.com/jquery/dist/jquery.min.js"></script> </head> <body> <script> let GoogleKey = [APIキー情報]; // Google から取得した API Key // ページの最初 $(document).ready(function () { update_youtube("JP"); }); // 引数で指定した国の急上昇動画を取得 function update_youtube(CountryCodes) { $.ajax({ url: 'https://www.googleapis.com/youtube/v3/videos?key=' + GoogleKey + '&chart=mostPopular&maxResults=1&regionCode=' + CountryCodes, dataType: 'json', }).then( function (result) { // この国の現時点での急上昇動画のID console.log(result.items[0].id); }, function () { console.log("err"); } ); } </script> </body> </html> 動画のIDが取得できてしまえば、iframe埋め込みでの動画プレーヤーでの動画再生が可能となります。当然、YouTubeが浸透していない国では、上記スクリプトはエラーとなりますのでご注意ください。 Bing News Searchによる地域のニュースの取得 Bing News Searchを用いると、指定した国のトップニュースを取得することが可能です。下の例は、2021/11/28 17:30時点での日本国のトップニュースです。例のコロナウィルスに「オミクロン」なんて可愛い名前が付いたのですね。 本機能利用には、Microsoft Azure で Bing News Search を有効にし、本APIを利用するためのAPI Keyを取得しておく必要があります。 API Keyが取得できたら、次のコードでその国のトップニュース情報が取得できます。なお、引数として入力する国名は上記Google Geocoding API で取得したアルファベット2文字の「Country codes」が必要です。例えば日本では "JP"、アメリカでは "US"です。このコードでは"news1_img" "news2_img"をIDとして持つニュースのサムネイル画像配置用のimgタグと、"news1_text" "news2_text" をIDとして持つニュースタイトル配置用のdivタグがあることを想定しています。 <!DOCTYPE html> <html lang="ja"> <head> <script src="https://unpkg.com/jquery/dist/jquery.min.js"></script> </head> <body> <img id="news1_img" style="width:100pt; height:100pt;"></img> <p id="news1_text" style="width:100pt; height:100pt;"></p> <script> let api_key = [APIキー情報]; // ページの最初 $(document).ready(function () { update_news("JP"); }); // 引数で指定した国のニューストピックを取得し、Viewに反映 function update_news(CountryCodes) { $.ajax({ url: 'https://api.bing.microsoft.com/v7.0/news/search?count=2&cc=' + CountryCodes, type: 'GET', headers: { 'Ocp-Apim-Subscription-Key': api_key, 'Accept-Language': 'Jp', }, }).then( function (result) { if (result.value[0].image) { // サムネネイルがない場合があるので注意 document.getElementById("news1_img").src = result.value[0].image.thumbnail.contentUrl; } document.getElementById("news1_text").innerText = result.value[0].name; }, function () { } ); } </script> </body> </html> Spotify Web APIによる地域のヒットソングの再生 地域のヒットソングを再生するには、「地域のヒットソングの取得」と「取得した楽曲の再生」を行う必要があります。まず、地域のヒットソングを取得するには、Spotify Charts にアクセスし、楽曲のURLを取得する必要がありました。当初は、これもWeb APIで取得したかったのですが、どうしてもその手法がわかりませんでした。そのため、上記サイトを1日に一度巡回し、各国のヒットソングのURLを取得するようにしました。 楽曲の再生には、Spotify Web APIを用いることで実現できます。まず、Spotify for Developersにアクセスし、Client ID と Client Secret を取得します。そして、Authorization Code Flow に従い、access_token と refresh_token を取得します。 下記のコードは、CountryCodesで国情報を指定すると、その国のヒットソングを再生するコードとなります。ちょっとややこしいですが、定期的にrefresh_spotifyを呼び出し、access_token をリフレッシュする必要があります。そして、そのaccess_token を用い、楽曲の再生を行います。再生されるデバイスは、現在のSpotify楽曲を再生しているデバイスですので、再生したいデバイスで事前に何らかの楽曲を再生しておくとよいでしょう。 let access_token = [access_token]; let refresh_token = [refresh_token]; let authorization_basic = ["Client ID:Client Secret"をbase64化したもの]; // 一定時間ごとに呼び出し、spotify_token を更新する function refresh_spotify() { $.ajax({ url: 'https://accounts.spotify.com/api/token', type: 'POST', headers: { 'Authorization': 'Basic ' + authorization_basic, }, data: { grant_type: 'refresh_token', refresh_token: refresh_token, } }).then( function (result) { access_token = result.access_token; }, ); } // 1日に一度巡回し、更新された各国のヒットソングの情報 let ranking = [ ["AE", "by Adele", "Easy On Me", "46IZ0fSY2mpAiktS3KOqds", "https://spotifycharts.com/regional/ae/daily/latest", "https://i.scdn.co/image/ab67616d00004851c6b577e4c4a6d326354a89f7"], ["AR", "by TINI, L-Gante", "Bar", "0lJE8f0lx8mUSfMyxeYpiC", "https://spotifycharts.com/regional/ar/daily/latest", "https://i.scdn.co/image/ab67616d000048517b1a8b1a92561bb5d16d6b4c"], // 以後略 ]; // 特定の国のヒットソングを再生する function play_sound(CountryCodes) { for (let i = 0; i < ranking.length; i++) { if (ranking[i][0] == CountryCodes) { var spotify_data = { uris: ["spotify:track:2MvIexkUblP1QdpBzKot3N"], }; potify_data.uris[0] = 'spotify:track:' + ranking[i][3]; // 曲を再生 $.ajax({ url: 'https://api.spotify.com/v1/me/player/play', type: 'PUT', headers: { 'Authorization': 'Bearer ' + access_token, }, data: JSON.stringify(spotify_data), }).then( function (result) { }, function () { } ); break; } } } まとめ 本記事では、緯度・経度情報を入力とし、各種Web APIで様々な情報を取得する例を示しました。どれも簡単に有益な情報が取得できるAPIばかりですので、是非使って見てください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

大学生が0から始めたTypescript勉強

大学生が0から始めたTS勉強 こんにちは。どこにでもいる大学三年生をやらせてもらってます。 Typescriptをjavascriptを軽く触ったくらいの自分が勉強してインプット・アウトプットする過程でまとめたものです。誰かの役に立ったら幸いです。 変数定義 未定義の変数に代入はダメ 型の複数定義 let birthYear: number | string | “北極”; 値も定義可能 変数の巻き上げ(js特有) varで定義されたものの左辺は関数の先頭で定義。 var A=Bという記述がはじめて出てくるところでAにBが代入される。 varは関数のスコープ外からも読み取れる const,letはブロックスコープ内 プリミティブ型(最小単位) リテラル:専用の文法、データ TSのデータ型種類 ・boolean 型 →ture/false ・number 型 →演算子 条件(三項)演算子 condition ? exprIfTrue : exprIfFalse if文などで使われる。conditionがtrueのとき左の値を返す、falseだと右の値を返す。  Mathオブジェクト ・string 型 →基本シングルorダブルクオーテーション、ただしバッククオートだと改行が反映される ・配列 ・オブジェクト ・関数 ・undefined ・null →nullは気楽に使ってはいけない、もし使うなら「AもしくはBのデータが格納できる」という合併型(Union Type)の型宣言ができるので、これをつかって null を代入します //stringかnullを入れられるという宣言をしてnullを入れる let favaoriteGame: string | null = null; 文字コードの正規化 "ABCアイウエオ㍻".normalize("NFKC") 'ABCアイウエオ平成' 複合型 複合型:大きなデータを定義できるデータ型 配列 同じような目的の変数を使う時に全ての要素を代表する変数名をつける。ここの要素を「インデックス」という。 // 変数に代入。型名を付けるときは配列に入れる要素の型名の後ろに[]を付与する const years: number[] = [2019, 2020, 2021]; // 後ろの型が明確であれば型名は省略可能 const divs = ['tig', 'sig', 'saig', 'scig']; // 配列に要素を追加。複数個も追加可能 years.push(2022); years.push(2023, 2024); years[10] = 2029; // 要素から取り出し const first = years[0]; タプル 配列の要素ごと型が違う ただしタプル型は各位置の情報が不明瞭であるため、インターフェースを用いてプロパティを定義すしたほうがよい。 インタフェース:概念的に共通した部分、機能、操作の集まり ex) ノード(Node) └ 要素(Element) └ HTML要素(HTMLElement) └ p要素(HTMLParagraphElement) 例えばp要素やli要素にも共通して「コンテントを持つ(textcontentプロパティ)」という機能が備わっているがそれはHTMLParagraphElementの上位概念としてHTMLElementインターフェースとして登録しているから。 インターフェースは「概念」、オブジェクトは「実態」 const movie:[number,string] = [2019,"ゴジラ"] //インターフェース定義する時 interface momvie{ showingYear:number; title:string; } function getResult():movie{ return{ showingYear:2019 title:"ゴジラ" }; } 配列からデータを取り出す TSはJSと異なり、分割代入を用いて複数の値を取り出すことができる。 slice()を使わずに新しい残余(Rest)構文を使って複数取り出すことも可能。 →(...)をつかって複数の要素をまとめて取り出す。 const drinks =[ "latte", "coffee", "water" ]; //一つ取り出す(旧) var clear = drinks[2]; //二番目以降取り出す(slice()を使う) var delisiousDrinks = drinks.slice(1); //まとめて取り出す(新) const [drink1,drink2,drink3] = drinks; //二つ目以降要素まとめて取り出す const[,...other] = drinks; 配列の加工 スプレッド構文を使って配列の加工をする スプレッド構文:先程の(...)を用いた構文 const pencase:string[] = [ "pen", "eraser", "namepen" ]; const wantedItem = [ "ruler", "redpen", "bluepen" ]; //ペンを捨てて定規と赤青ペンを入れる const [pencase(1),pencase(2)] = tidiedPencase; const newPencilCase = [...tydiedPencase, ...wanterItem]; //スプレッド構文で配列のコピー const copy = [...pencase]; 配列のソート ソート:配列のインデックスを並び替える const numbers = [30, 1, 200]; numbers.sort(); // 1, 200, 30 ループ文はfor...ofを使う ループの書き方3つ ・for文 ・forEach() ・for...of var iterable = ["小金井", "小淵沢", "小矢部"]; // 旧: C言語由来のループ for (var i = 0; i < iterable.length; i++) { var value = iterable[i]; console.log(value); } // 中: forEach()ループ iterable.forEach(value => { console.log(value); }); // 新: for ofループで配列のインデックスが欲しい for (const [i, value] of iterable.entries()) { console.log(i, value); } // 要素のみ欲しいときは for (const value of iterable) イテレータ イテレータ:「イテラブル」なオブジェクトから生成されるオブジェクトで、「イテレータリザルト」を返します。またnext()という関数を持つオブジェクトです type Hoge = { value: string; done: boolean; } type HogeIterator = { next: () => Hoge; } // hogeIteratorはイテレータ const hogeIterator: HogeIterator = { next : () => { return { value: 'hogehoge', done: false } } イテレータリザルト:イテレータリザルトとは、値を表すvalueとイテレータが完了したかどうかを表すdoneという2種類のプロパティを持つオブジェクトのこと type Hoge = { value: string; done: boolean; } // hogeはイテレータリザルト const hoge: Hoge = { value: 'hogehoge', done: false, } 読み込み専用配列 TypeScriptの「const」は変数の再代入をさせないが、「変更不可」にはできません。 TypeScriptにはこれには別のキーワード、readonlyが提供されています。型の定義の前にreadonlyを付与するか、リテラルの後ろにas constをつけると読み込み専用になります。 // 型につける場合はreadonly const a: readonly number[] = [1, 2, 3]; // 値やリテラルに付ける場合はas const const b = [1, 2, 3] as const; a[0] = 1; // Index signature in type 'readonly number[]' only permits reading. オブジェクト オブジェクト:JavaScriptのコアとなるデータですが、クラスなどを定義しないで、気軽にまとまったデータを扱うときに使う。 配列は要素へのアクセス方法がインデックス(数値)だが、オブジェクトの場合は文字列。 JSON オブジェクトがよく出てくる。データ交換用フォーマットで文字列。 JSONはJS,TSに比べてルールが厳密。たとえばキーは必ずダブルクォーテーションで括るなど。 // 最初の引数にオブジェクトや配列、文字列などを入れる // 2つめの引数はデータ変換をしたいときの変換関数(ログ出力からパスワードをマスクしたいなど) // 省略可能。通常はnull // 3つめは配列やオブジェクトでインデントするときのインデント幅 // 省略可能。省略すると改行なしの1行で出力される const json = JSON.stringfy(smallAnimal, null, 2); // これは複製されて出てくるので、元のsmallAnimalとは別物 const smallAnimal2 = JSON.parse(json); 基本的な構文 switch switch (task) { case "休憩中": console.log("サーフィンに行く"); break; case "デスマ中": console.log("睡眠時間を確保する"); break; default: console.log("出勤する"); } ()とcaseが===で結ばれる時breakまでの処理を行う 基本的な型付け 一番手抜き型付け:any anyと書けば、TypeScriptのコンパイラは、その変数のチェックをすべて放棄する。 使う場面 ①ユーザー定義の型ガードの引数 ②メインの引数ではなくて、挙動をコントロールするオプションの項目がかなり複雑で、型定義が複雑な場合 ユーティリティ型 ユーティリティ型(utility type)は、型から別の型を導き出してくれる型 Required 全プロパティを必須にする Readonly 全プロパティを読み取り専用にする Partial 全プロパティをオプショナルにする Record キー・バリューからオブジェクト型を作る Pick 任意のプロパティだけを持つオブジェクト型を作る Omit 任意のプロパティを除いたオブジェクト型を作る などなど タグ付き合併型 分岐して細かく型を設定できる感じ type NumberStyle = { color: string; } type NumberColumn = { columnType: 'number'; caption: string; field: string; style: NumberStyle; } タグですぐに判断できる type CheckStyle = { uncheckBgColor: string; checkBgColor: string; } type CheckColumn = { columnType: 'check'; caption: string; field: string; style: CheckStyle; } type NumberStyle = { color: string; } type NumberColumn = { columnType: 'number'; caption: string; field: string; style: NumberStyle; } 型ガード 型ガード:複数の型付けがされている時にこっちのパターンの型に対するロジックを書く時につかうもの // userNameOrIdは文字列か数値 let userNameOrId: string|number = getUser(); if (typeof userNameOrId === "string") { // このif文の中では、userNameOrIdは文字列型として扱われる this.setState({ userName: userNameOrId.toUpperCase() }); } else { // このif文の中では、userNameOrIdは数値型として扱われる const user = this.repository.findUserByID(userNameOrId); this.setState({ userName: user.getName() }); } 組み込み型ガード ・typeof ・instanceof ・in ・比較 主にこの4つ ①typeof 変数 →変数の型名を文字列で返してくれる undefined: "undefined" bool型: "boolean" 数値: "number" 文字列: "string" シンボル: "symbol" 関数: "function" この他のものは"object"として返す ②変数 instanceof クラス名 →自作のクラスで使える ③"キー" in オブジェクト →オブジェクトに特定の属性が含まれているかどうかの判定 型アサーション キャスト=型アサーション 型アサーション:コンパイラのもつ型情報を上書きすること const page: any = { name: "profile page" }; // any型からはasでどんな型にも変換できる const name: string = page as string; ただし基本的にasは使わないし、anyも使わない。今回もpageはstringでないからコンパイラでエラーは吐かないが、ページでエラーをはく 関数 関数:一連の処理に名前をつけたもの ネスト:あるものの中に、それと同じ形や種類の(一回り小さい)ものが入っている状態や構造(ex:if( 条件A ){ ... if( 条件B ){ ... } ... } ) function 関数名(引数リスト): 返り値の型 { return 返り値 } 引数はカンマで区切る、引数の型付けもする。 typescriptの関数の特徴 関数そのものを変数に入れて名前をつけられるし、データ構造にも組み込める 他の関数の引数に関数を渡せる 関数の返り値として返すことができる 関数のバリエーション 無名関数(アノニマス関数):名前がない クロージャ:関数内に作られた関数 メソッド:何かオブジェクトに属する関数 非同期関数:処理全てが終わる前に何か他の処理を行うことのできる関数 関数の引数と返り値の型定義 // 昔からあるfunctionの引数に型付け。書く引数の後ろに型を書く。 // 返り値は引数リストの () の後に書く。 function checkFlag(flag: boolean): string { console.log(flag); return "check done"; } // アロー関数も同様 const normalize = (input: string): string => { return input.toLowerCase(); } 具体的には function 関数名(仮引数:データ型):返り値のデータ型 { 処理; } アロー関数の場合 const 変数 =(引数:データ型):返り値の型 =>{ 処理; } 型が明確な場合は書かなくてもよい 関数が何も帰さない場合:voidをつける 複数の型決めはあまりよろしくない 関数を扱う変数の型定義 ex)文字列と数値を受け取ってbooleanを返す関数 let check:(arg1:string,arg2:number) =>boolean; ここでは返り値は=>の右側につけて{}ははずす。 ex)arg2がもし関数であったとき let check (arg1:string arg2:(arg3:string)=>number) =>boolean; sortに関する注意 jsとTSで違いがあるので注意 SortSample.js const array = [1,5,2,4,3]; array.sort(); console.log(array); /* * 結果 * 「1,2,3,4,5」 * (期待通りに並べ替えが発生する) */ jsは戻り値がソートされた配列、対象の配列上で直接行われる。array自体の配列の中身が入れ替わる。 それに対してtypescriptは SortSample.ts let array = [1,5,2,4,3]; array = array.sort(); console.log(array); /* * 結果 * 「1,2,3,4,5」 * (期待通りに並べ替えが発生する) */ sortされた結果を戻り値として返却するのでarrayの並びを直接入れ替えることはないので太字のようにarrayを新しく定義する必要がある sortの比較 ・compareFunction(a,b)が未満の時aをbよりも小さいインデックスにソートする(=配列でaが先に来る) ・compareFunction(a, b) が 0 より大きい場合、b を a より小さいインデックスにソートします。(=b が先に来るようにします) compareFunction(a, b) が 0 を返した場合、a と b は互いに変更せず、他のすべての要素に対してソートします。 function compare(a, b) { if (ある順序の基準において a が b より小) { return -1; } if (その順序の基準において a が b より大) { return 1; } // a は b と等しいはず return 0; } こんな感じで比較、並び替えを行う function sort(a: string[], conv: (value: string) => string) { const entries = a.map((value) => [value, conv(value)]) entries.sort((a, b) => { if (a[1] > b[1]) { return 1; } else if (a[1] < b[1]) { return -1; } return 0; }); return entries.map(entry => entry[0]); } const a: string[] = ["a", "B", "D", "c"]; console.log(sort(a, s => s.toLowerCase())) // ["a", "B", "c", "D"] アロー関数のバリエーション // 基本形 (arg1, arg2) => { /* 式 */ }; // 引数が1つの場合は引数のカッコを省略できる // ただし型を書くとエラーになる arg1 => { /* 式 */ }; // 引数が0の場合はカッコが必要 () => { /* 式 */ }; // 式の { } を省略すると、式の結果が return される arg => arg * 2; // { } をつける場合は、値を返すときは return を書かなければならない arg => { return arg * 2; }; 引数が一つの場合や式の結果をそのままreturnする場合に括弧を省略できる。 非同期処理 コールバック関数:時間のかかる処理を行った後に実行する関数 非同期処理の歴史 コールバック地獄→ Promiseの登場(1段のネストで済む)、then()節の利用 →「await」の使用 // 新: 非同期処理をawaitで待つ(ただし、awaitはasync関数の中でのみ有効) const resp = await fetch(url); const json = await resp.json(); console.log(json); await を扱うにはasyncをつけて定義された関数でなければなりません。 TypeScriptでは、 async を返す関数の返り値は必ず Promise になります。 ジェネリクスのパラメータとして、返り値の型を設定します。 async function(): Promise { await 時間のかかる処理(); return 10; } 注意 比較的新しく作られたライブラリではpromiseを返す仕様になっているがそうでないコールバック関数方式のコードを使う場合「new Promise」を使う // setTimeoutは最初がコールバックという変態仕様なので仕方なくnew Promise const sleep = async (time: number): Promise => { return new Promise(resolve => { setTimeout(()=> { resolve(time); }, time); }); }; await sleep(100); 非同期と制御構文 「Promise」ってなんだっけ 一連の流れ ex:電気屋へ修理 修理の依頼(Promise)→修理できた(resolve)→次の処理(支払い)へ(Promise) 修理の依頼(Promise)→修理できない(reject)→エラー処理へ(catch) .js const repairPromise = new Promise((resolve, reject) => { const repair = true; if(!repair) reject('修理してください'); return setTimeout(() => resolve('修理しました'), 3000); }); const paymentPromise = new Promise((resolve, reject) => { const payment = true; if(!payment) reject('送金まだですか?'); return setTimeout(() => resolve('送金しました'), 500); }); console.log('進捗どうですか?') repairPromise .then(res => { console.log(res); return paymentPromise.then(res => console.log(res)).catch(err => console.log(err)); }) .catch(err => console.log('errorめっせー', err)); 宣言した処理が完遂するとthenで実行する 通常のasync/awaitの場合 .js const asyncFunc = async() => { console.log('進捗どうですか'); try{ const msg1 = await repairAsync(3000); console.log(msg1); const msg2 = await paymentAsync(500); console.log(msg2); } catch(e) { console.log(e); } }; const repairAsync = (ms) => new Promise(resolve => { setTimeout(() => { resolve('修理しました') },ms); }); const paymentAsync = (ms) => new Promise(resolve => { setTimeout(() => { resolve('送金しました') },ms); }); asyncFunc(); 一つ目のawaitが終わるまでに二つ目のawait処理を進められない ts. const asyncFunc = async(): Promise => { console.log("進捗どうですか"); try { const msg1 = await repairAsync(3000); console.log(msg1); const msg2 = await paymentAsync(500); console.log(msg2); } catch (e) { console.log(e); } }; const repairAsync = (ms: number): Promise => new Promise((resolve) => { setTimeout(() => { resolve("修理しました"); }, ms); }); const paymentAsync = (ms: number): Promise => new Promise((resolve) => { setTimeout(() => { resolve("送金しました"); }, ms); }); asyncFunc(); イメージ 大本asyncFunc(promise):非同期関数 内容はtryとcatch 大本のpromiseの中でawaitによってnew promiseの関数をそとで定義されてるところから呼び出す ex:味噌汁を温めながらご飯を電子レンジで温めて、温め終わったら両方食べる async function 味噌汁温め(): Promise<味噌汁> { await ガスレンジ(); return new 味噌汁(); } async function ご飯温め(): Promise<ご飯> { await 電子レンジ(); return new ご飯(); } const [a味噌汁, aご飯] = await Promise.all([味噌汁温め(), ご飯温め()]); いただきます(a味噌汁, aご飯); 非同期で繰り返し呼ばれる処理 async/awaitは便利なものですが、ワンショットで終わるイベント向けです。繰り返し行われるイベント(addEventListener()を使うようなスクロールイベントとか、画面のリサイズsetInterval()の繰り返しタイマー)に対しては引き続きコールバック関数を登録して使います addEventListener:イベントの実行、処理を行うためのメソッド ex) 対象要素.addEventListener( 種類, 関数, false ) 種類はネットで検索する!いっぱい載ってる 関数は無名関数or外部で定義した関数orアロー関数 無名関数の場合 対象要素.addEventListener(種類, function() { //ここに処理を記述する }, false); アロー関数の場合 対象要素.addEventListener(種類, () => { //ここに処理を記述する }); setInterval():処理をxミリ秒後に繰り返し行うためのメソッド let count = 0; const countUp = () => { console.log(count++); } setInterval(countUp, 1000); 関数のcountUpを1000ミリ秒(1秒)後に再び行う 例外処理 例外処理とは A→B→Cと順番にタスクをこなす際に例えばデータの取得がうまくいかなかったり、データの送信がうまくいかない際に処理を中断する(例外を投げる),中断したことを察知して何かしらの対処( 回復処理を行うこと。 throwを使って例外処理を投げる。throwするとその行でその関数の処理が中断し、呼び出し元へ巻き戻る。巻き戻った先にあるtry節/catch節のペアに当たるまで戻る。 ex) throw new Error("ネットワークアクセス失敗"); console.log("この行は実行されない"); 例外を投げる処理の周りはtry節で囲む catchは例外が飛んできたときに呼ばれるコードブロック finallyは例外が発生してもしなくても必ず通る ex) try { const data = await getData(); const modified = modify(data); await sendData(modified); } catch (e) { console.log(エラー発生 ${e}); throw e; // 再度投げる } finally { // 最後に必ず呼ばれる } もし、回復処理で回復し切れない場合は、再度例外を投げることもできます。 Errorクラス 問題が発生したときに情報伝達に使うのがErrorクラスでふつうのオブジェクト Errorクラスは作成時にメッセージの文字列を受け取れる。name属性にはクラス、massageにはコンストラクタに渡した文字列が格納される。 処理系独自の機能でstackプロパティに格納されるスタックトレースはthrowした関数が今までどのように呼ばれてきたかのりれきである。デバックのどに使う。 try節はなるべく狭くする ex)before try { logicA(); logicB(); logicC(); logicD(); logicE(); } catch (e) { // エラー処理 } ex)after logicA(); logicB(); try { logicC(); } catch (e) { // エラー処理 } logicD(); logicE(); tryの範囲を狭めることでどこで何が起きているのか分かりやすくなる 原因の違う例外が投げられることで混ざってしまう可能性もある。 Error以外をthrowしない 下のコードではcatch節の中でeのデータ型がError前提で書かれておりコードの補完が効くのでthrowではErrorしか投げない try { : } catch (e) { // e.とタイプすると、name, messageなどがサジェストされる console.log(e.name); } リカバリー処理の分岐のためにユーザー定義の例外クラスを作る コンストラクタ:クラスをnewした瞬間に実行される関数 クラス:設計図 インスタンス:設計図を基に作った実際のもの オブジェクト:モノ(クラスとかインスタンスとかふんわり表現したもの) 例外処理のためにクラスを作っておく。さまざまなシチュエーションでクラスの継承を行い対応する。 // 共通エラークラス class BaseError extends Error { constructor(e?: string) { super(e); this.name = new.target.name; // 下記の行はTypeScriptの出力ターゲットがES2015より古い場合(ES3, ES5)のみ必要 Object.setPrototypeOf(this, new.target.prototype); } } // BaseErrorを継承して、新しいエラーを作る // statusCode属性にHTTPのステータスコードが格納できるように class NetworkAccessError extends BaseError { constructor(public statusCode: number, e?: string) { super(e); } } // 追加の属性がなければ、コンストラクタも定義不要 class NoNetworkError extends BaseError {} 例外を受け取ったcatch節でリカバリー方法を選ぶことができる。その際に「instanceof」を使用する。instanceofは型ガードになっているのでコードの補完が行われる。 try { await getUser(); } catch (e) { if (e instanceof NoNetworkError) { alert("ネットワークがありません"); } else if (e instanceof NetworkAccessError) { // この節では、eはNetworkAccessErrorのインスタンスなので、 // ↓のe.をタイプすると、statusCodeがサジェストされる if (e.statusCode < 500) { alert("プログラムにバグがあります"); } else { alert("サーバーエラー"); } } } 非同期と例外処理 promise→ ①then()の二つ目のコールバック関数でエラー処理を行う ②catch()節内にエラー処理を書く ex)promiseのエラーの書き方 fetch(url).then(resp => { return resp.json(); }).then(json => { console.log(json); }).catch(e => { console.log("エラー発生!"); console.log(e); }); async関数→ ①try節内でawaitしてエラーを捕まえる try { const resp = await fetch(url); const json = await resp.json(); console.log(json); } catch (e) { console.log("エラー発生!"); console.log(e); } ②promise作成時のコールバック関数の二つ目の引数のreject()コールバック関数にオブジェクトを渡す。 async function heavyTask() { return new Promise((resolve, reject) => { // 何かしらの処理 reject(error); // こちらでもPromiseのエラーを発生可能 throw new Error(); }); }; モジュール パッケージとは Node.jsを核とするJS,TS共栄圏の言葉。 Node.jsが配布するソフトウェアの塊の単位。 nmpやyarnなどのツールを使ってnpmjs.orgなどのリポジトリからダウンロードすることができる。 ブラウザ向けのフレームワークなどもいまはnpmjs.orgで配布される。 パッケージのダウンロード $ npm install @vue/cli vue vueを作成するCLIコマンドとライブラリの二つをダウンロード モジュールとは JS,TS界隈でモジュールというとTS/JSの一つのソースファイルのことを指す。簡単に言えば.ts,.jsのファイルがモジュール。パッケージ内に大量のモジュールが入るイメージ。パッケージ作成時に1つ代表となるモジュールを決める(main属性) TSでモジュールになるもの ・TypeScriptの1ファイル ・TypeScript用の型定義ファイル付きのnpmパッケージ ・TypeScript用の型定義ファイルなしのnpmパッケージ+TypeScript用の型定義ファイルパッケージ エクスポート エクスポートとは ファイル内の変数、関数、クラスを送り出す // 変数、関数、クラスのエクスポート export const favorite = "小籠包"; export function fortune() { const i = Math.floor(Math.random() * 2); return ["小吉", "大凶"][i]; } export class SmallAnimal { } インポートとは エクスポートしたものはimportを使って取り組む。名前がぶつかりそうなときはasを使って別名をつけることができる。 // 名前を指定してimport import { favorite, fortune, SmallAnimal } from "./smallanimal"; // リネーム import { favorite as favoriteFood } from "./smallanimal"; defaultエクスポートとインポート エクスポートする要素の1つを default の要素として設定できます。 // defaultをつけて好きな要素をexport export default address = "小岩井"; // defaultつきの要素を利用する時は好きな変数名を設定してimport // ここではlocationという名前でaddressを利用する import location from "./smallanimal"; default のエクスポートと、 default 以外のエクスポートは両立できます // defaultつきと、それ以外を同時にimport import location, { SmallAnimal } from "./smallanimal"; パスの書き方-相対パスと絶対パス 相対パス:ピリオドから始まる、import文がかかれたファイルのフォルダを起点に探す 絶対パス:ピリオド以外から始まる。検索アルゴリズムを使って探す ひとつがtsconfig.jsonの compilerOptions.baseDir です。 プロジェクトのフォルダのトップを設定しておけば、絶対パスで記述できます。 プロジェクトのファイルは相対パスでも指定できるので、どちらを使うかは好みの問題ですが、Visual Studio Codeは絶対パスで補完を行うようです。 動的インポート 初期ロード時間を減らしたいときに使うのは動的インポート。Promiseを返すimport()関数となっている。fileアクセスやネットワークアクセスをしてファイルを読み込みロードが完了すると解決する。 ジェネリクス ジェネリクスとは ジェネリクスとは日本語で総称型と呼ばれる、使われるまで型が決まらないようないろいろな方を受ける機能を作るときに使う。 ジェネリクスの書き方 ジェネリクスは関数、インターフェース、クラスなどと一緒に利用できる ジェネリクスの場合は名前の直後、関数の場合は引数リストの直前にジェネリクス型パラメータを関数の引数のように記述する 下のコードでは「T」がそれにあたる function multiply(value: T, n: number): Array { const result: Array = []; result.length = n; result.fill(value); return result; } T には string など、利用時に自由に型を入れることができます。宣言文と同じように < > で括られている中に型名を明示的に書くことで指定できます // -1が10個入った配列を作る const values = multiply(-1, 10); // ジェネリクスの型も推論ができるので、引数から明示的にわかる場合は省略可能 const values = multiply("すごい!", 10); ジェネリクスの引数名にはT,U,Vなどがつかわれる。 preventDefault()について ①イベント DOM内の各要素では頻繁に「イベント」が発生 A,ユーザーが発生させるイベント、B,ブラウザが勝手に発生させるイベント A,ユーザーが発動させるイベントの例 ・click PCでは頻繁に発生させています ・keypress キー入力の際に発生するイベントです ・touchmove スマホを使っている人は日常的に大量に発動させています B.ブラウザが勝手に発動させるイベントの例 ・DOMContentLoaded ブラウザによりHTMLの解析が完了した場合に勝手に発動します ・ended 見慣れないかもしれませんが、audioやvideoが再生終了したときに勝手に発動します。 ・error よく見ますよね。通信エラーやJavaScriptエラーが発生した際に勝手に発動します。 ②イベントのデフォルト動作例 イベントのデフォルト動作例 イベント デフォルト動作例 「touchmove」 スマホの画面がスクロールされる aタグ」での「click」 aタグのherfで指定されたURLへ遷移する 「formのチェックボックス」での「click」 チェックボックスのオン/オフが切り替わる テキストタイプの「form」での「keypress」 文字が入力される inputの「submit」での「click」 actionで指定されたURLへ遷移+データ送信
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsをモダンなJSで実装する JavaScriptの歴史も理解しながら

はじめに Node.jsではrequire/module.exportを使うが、フロントエンドだとimport/exportを使うのがほぼであるのに、なんでそうなるの?と思う事があるかもしれない。 その辺りについてきちんと理解してみたのでそれの備忘録を残す。 JavaScriptの歴史からNodeとは何か?とNode.jsでimpoert/exportを使うにはwebpack/babelが必要になる事を理解する 最近(2015年以降)にフロントエンドをやり始めると、モダンな書き方でES6(ECMAScript2015)でJavaScriptを書くと思うが、その状態でNode.jsも書き始めると、なんか書き方が違うな~と感じると思う。 分かりやすいのがモジュール管理(他のファイルに実装した関数を使う、他のライブラリを使う、をする時の呼び込み方法)で、 import lodash from 'lodash'; という書き方と、 const lodash = require('lodash'); という書き方の2つを見た事があると思う。 何でこういうことになるのか?について少し理解してみる事にする。そしてNode.jsでimport/exportのモジュール管理方法(モダンな書き方)をするために何が必要か?も理解していく。 理解する上ではJavaScriptの歴史から入らないと分からないので、その歴史を見ていき、Node.jsがいつ誕生してその後何が起きたか?を見ていく事にする。 JavaScriptの歴史 歴史の全体感を見るには以下のサイトが分かりやすい。 この歴史の中で今回特に関係するのは、 2009年 Node.js の誕生 2015年 ES2015:大規模な ECMAScript のアップデート の2つ。 また、同時にJavaScriptのversionについても抑えておく必要があるが、version一覧は以下が分かりやすい。 Node.jsの誕生 2009年にNode.jsが誕生する。発端は、Chromeに搭載されたV8Engineと呼ばれるものがとても良いという事で、これをJavaとかRuby、Pythonと同じようにサーバーサイドの言語として使おうとして生まれたもの。 ここで前項のJavaScriptの歴史を見返すと、 年代 version名 1997-1999 ES1 ES2 ES3 2009- ES5 2015- ES6 2016- ES2016 ... ... のようになっており、Node.jsが生まれた時のJavaScriptのversionはES5(ECMAScript 5)と呼ばれるものである事が分かる。となると必然的にその当時のJavaScriptのversionでNode.jsが実装される事になるわけで、実際にそうなっている。 そして、モジュール管理の方法はCommonJSという仕様で行われており、それがまさしく const lodash = require('lodash'); という書き方のものになる。 参考:Node.js とは何か importとrequireという2つの書き方がある理由 上記で見てきたように、JavaScriptには色々な歴史があり、その過程で流派の違いのようなものが生まれた。そのため例えばモジュール管理の方法1つをとっても、その実装方法が違うものが世の中に存在しているという状態になっている。 もっともな疑問として新しいversionに統一すればいいのに、という事があるがそれについては次の項で見ていく。 JavaScriptの世界のルール webpack/babelが登場するわけ JavaScriptの歴史をみると新しいJavaScriptのversionが次々に生まれているが、後方互換(古いブラウザ(JavaScriptエンジン)でも動くようにしないといけない)というようなルールがある。これは昔からあるブラウザを使っているユーザを切り捨てないためにそういうルールになっている。実際にブラウザによってはES5の書き方でないとダメというブラウザ1もある。 そういった事情から、いくら新しいversionのJavaScriptの書き方があると言っても、昔からあるブラウザなどでもきちんと動くようにしてあげる必要が生じるのである。 ただ、折角新しいversionになったのに古くからある書き方をするのも…という事で、JavaScriptのversionを変えるツールがちゃんとある。それがbabelと呼ばれるオープンソースのライブラリで、これを使うと実装されたコードのJavaScriptのバージョンを変える事ができる。そしてそのbabelは、最近ではwebpackに組み込んで使われ、HTMLとかCSSとかその他色々なものをバンドルする際に同時にJavaScriptのversionを変えるという事が良く行われる。 これにより例えば、ES6(2015年のJavaScript version)で書いたコードをES5(2009年のJavaScript version)の書き方に変換する、という事ができる。そのため、フロントエンドでES6以降のモダンな書き方をする場合、必ずwebpack/babelがそのプロジェクトには組み込まれている事になる(React/Vue/Angularなどのフレームワークを使うと意識する事はないかもしれないが)。 では、バックエンド(サーバサイド)のJavaScriptであるNode.jsではどうなるのか?というと、それは次の項で見ていく。 Node.jsでimport/exportを使うには? 前項『Node.jsの誕生』で見たように、Node.jsはES5の書き方で書く必要があり、ES6以降のJavaScriptとはversionが違うものになっている(Node.jsもJavaScriptではあるのの、それはES5というルールで実装する必要があり、ES6とかES2016とかモダンと呼ばれるJavaScriptとは違う)。 ただ、前項のwebpack/babelでJavaScriptの書き方のversionを変える事ができるという話を踏まえると、単純に、Node.jsの方でもそのwebpack/babelの仕組みを使えばよいだけじゃんという事に気づく。 つまり、ES6以降のモダンな書き方をしても、それをwebpack/babelで変換してあげればES5の書き方をしたコードを生成できるので、Node.jsでもES6以降のモダンなJavaScriptの書き方で実装ができるという事。 これで何がうれしいかというと、 フロントエンドとバックエンドで書き方が違うという事をなくせるので分かりやすくなる(フロントエンドを新たに実装する場合、その大多数はES6以降の書き方であるので) versionが変わるのには理由(前のversionの良くない点などの改良)があるはずで、その改良を取り込んで最新の実装しやすい書き方ができる などのメリットがあるといった感じ。 実際にどんな設定・実装になるのか?については次の章『実際にNode.jsをモダンな書き方(ES6以降)で実装する』で扱う。 補足 webpackとbabel At its core, webpack is a static module bundler for modern JavaScript applications. webpackは基本的にはモジュールのバンドラーでしかないが、この中にbabelという機能を取り込むと、バンドルの過程でJavaScriptの書き方のバージョンを変えてくれる。 というわけで世の中的によくwebpack×babelの組み合わせが見られる。 実際にNode.jsをモダンな書き方(ES6以降)で実装する ES6以降の書き方でExpressのサーバを実装し、それを起動できるようにするまでをこの章では見ていく。 結論:どうすればいいか? webpack.config.js module.exports = { target: 'node', entry: './src/index.js', module: { rules: [ { test: /\.m?js$/, exclude: /node_modules/, use: { loader: 'babel-loader', }, }, ], }, }; .babelrc { "presets": [ "@babel/preset-env" ] } webpackでbuildするコードには以下を追記(npm install または yarn addも)。 import 'core-js/stable'; import 'regenerator-runtime/runtime'; npm installl --save-dev core-js@3 npm installl --save-dev regenerator-runtime/runtime buildするコードの全体としては以下のようになる。 index.js import 'core-js/stable'; import 'regenerator-runtime/runtime'; import express from 'express'; const app = express(); app.use(express.json()); app.get('/', async (req, res) => { const reqTime = Date.now(); console.log(Array.from('foo')); await new Promise((resolve) => { setTimeout(() => { resolve('sleep'); }, 500); }); res.status(200).send({ msg: 'hello world!', elaptime: Date.now() - reqTime, }); }); app.listen(3000, () => console.log('listening on port 3000!')); ※JavaScript configuration filesに書かれているように、BabelのConfigurationはJavaScriptでも書ける。その場合は以下のようになる。 babel.config.js const presets = [['@babel/preset-env']]; module.exports = { presets }; ※buildするコード内でDate.now(), Array.from, async/awaitなどを書いているがこれは意図的にそうしており、 Date.now(), Array.from標準組み込みオブジェクトのES5への変換を確認するため(polyfill ECMAScript features) async/awaitgenerator関数やasync/awaitのES5への変換を確認するため(use transpiled generator functions) の意味合いで実装している。 以降の項では、webpack.config.jsや.babelrc(babel.config.js)が上記のようになる理由について順にみていく。 何もせずに実装し実行するとどうなるか? まず、何もせずに単純にES6以降の書き方で書いたコードを実行してみるとどうなるか?を確認してみる。 import express from 'express'; const app = express(); // 省略 app.listen(3000, () => console.log('listening on port 3000!')); [root@localhost node-express]# node src/index.js (node:9816) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension. (Use `node --trace-warnings ...` to show where the warning was created) /root/workspace/node-express/src/index.js:1 import express from 'express'; ^^^^^^ SyntaxError: Cannot use import statement outside a module at ... 一部省略しているが、import文の所(モジュール管理の書き方)でエラーが出てしまう・・・。 webpack × babelの設定を追加し、webpackコマンドを実行する 次に、webpackとbabelを使ってJavaScriptのversionを下げる(ES6以降の書き方をES5に変換=トランスパイル)設定を追加する。 公式のbabel-loaderに沿って実装する。 webpack.config.js module.exports = { target: 'node', entry: './src/index.js', module: { rules: [ { test: /\.m?js$/, exclude: /node_modules/, use: { loader: 'babel-loader', }, }, ], }, }; .babelrc { 'presets': ['@babel/preset-env'] } ※target: 'node',については、webpackの公式ドキュメント(Targets)を読んでみると、 In the example above, using node webpack will compile for usage in a Node.js-like environment (uses Node.js require to load chunks and not touch any built in modules like fs or path). と書かれているように、Node.jsの場合はtarget: 'node'の設定が必要なので、webpack.config.jsにその設定を追加する。 上記の実装後にnpx webpack --mode=developmentを実行すると・・・2 $ webpack --mode=development asset main.js 949 KiB [emitted] (name: main) runtime modules 1.04 KiB 5 modules cacheable modules 716 KiB javascript modules 455 KiB 56 modules json modules 261 KiB ... 15 modules Done in 3.77s. のようになり、buildはうまくいっているように見える。さらにnode dist/main.jsを実行すると・・・ [root@localhost node-express]# node dist/main.js webpack://my-webpack-project/./src/index.js?:13 var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(req, res) { ^ ReferenceError: regeneratorRuntime is not defined at eval (webpack://my-webpack-project/./src/index.js?:13:46) ... のようにエラーが出てしまう。 core-js/regenerator-runtimeを使う regeneratorRuntime is not definedというエラーはES6以降の書き方でaysnc/awaitやgenerator関数というものをES5に変換するために必要な翻訳を担うものがないために起こっているエラー。 まずそもそもJavaScriptには標準組み込みオブジェクトと呼ばれるものが存在し、ライブラリを依存させたりせずに使える。例えば、Array.from('foo')とかが使えるのはこの標準組み込みオブジェクトのおかげ。 そしてこの標準組み込みオブジェクトもJavaScriptのversionが変わるごとに変わっており、ES6にはあるがES5にはないものがある。そうなるとES5で動く事を前提にしてるブラウザ(例えばIE11)ではES6の標準組み込みオブジェクトは使えないという事になる。 そこでpolyfillが登場する。これはJavaScriptの標準組み込みオブジェクトのversion間の差を埋めてくれるもので、例えば、ES6にある標準組み込みオブジェクトで実装したコードをES5のコードに変換する役割を担ってくれる。 このpolyfillだが、昔は@babel/polyfillというものがあり、これを利用していた。ただ、公式のサイトの注意書きの通りこのライブラリは非推奨になり、代わりにcore-js/stableとregenerator-runtime/runtimeを使い、ES5への変換を行う。それぞれ、 core-jspolyfill ECMAScript features regenerator-runtime/runtimetranspiled generator functions の役割がある。 実装としてはBabelのサイトにあるように、index.jsに index.js import "core-js/stable"; import "regenerator-runtime/runtime"; のようにimportを追加するだけでよい。 参考:@babel/polyfill おまけ 今回、Node.jsをモダン化する方法を見てきたが、上記のようにimport 'core-js/stable';, import 'regenerator-runtime/runtime';と書かないでも動く場合もあり混乱した。ちょっとどのパターンならbuildして動くのか?を調査してみたので以下にまとめてみた。 ※注意 今回検証に使ったコードだから動いただけの可能性もあり、この辺りはまだ理解が完全ではないため参考情報程度に見て頂ければと思います ※buildはnpx webpack --mode=developmentの事で、server起動はnode dist/main.jsの事 # パターン概要 build・server起動結果 package.json .babelrc(babel.config.js) index.js 1 webpackの設定のためのライブラリのみ ・build:成功・server起動:エラー 2 core-jsとregenerator-runtimeを依存に追加のみnpm install -D core-js regenerator-runtime ・build:成功・server起動:エラー(上と同じ) (上と同じ) (上と同じ) 3 babelの設定にuseBuiltInsを追加(usage)※必要なpolyfillだけ読み込むように設定 ・build:成功・server起動:成功 (上と同じ) (上と同じ) 4 babelの設定にuseBuiltInsを追加(entry)※全polyfillを読み込む ・build:成功・server起動:エラー (上と同じ) (上と同じ) 5 index.jsにimport 'core-js/stable';, import 'regenerator-runtime/runtime';を追記 ・build:成功・server起動:成功 (上と同じ) (上と同じ) ※5のパターンは、上記の結論どうすればいいかの設定に加えてbabelのconfigurationの設定も追加したものになる。 ※やってみて分かったが、パターン5のようにindex.jsにimport 'core-js/stable';, import 'regenerator-runtime/runtime';を書いた状態ではファイルサイズが大きくなるので、パターン3のようにuseBuiltInsの設定をusageにしてindex.jsでのimportはなしのやり方がいいかもしれないが、今までの@babel/polyfillでは全てのpolyfillを読み込んでいたので、素直にimportの書き方だけ(結論どうすればいいかの設定)をするでもいい気もする パターン main.jsのファイルサイズ 3 1.11 MiB 5(結論どうすればいいかのやり方) 1.72 MiB ※ちなみ、非推奨になった@babel/polyfillを使ってbuildをすると、サイズは1.42 MiBだった。 webpack.config.js module.exports = { target: 'node', entry: ["@babel/polyfill", "./app/js"], module: { rules: [ { test: /\.m?js$/, exclude: /node_modules/, use: { loader: 'babel-loader', }, }, ], }, }; 参考文献 Babel7.4で非推奨になったbabel/polyfillの代替手段と設定方法 IE11 ↩ --mode=developmentはトランスパイル後のソースコードを人が読めるものにするために設定 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】ディレクティブ v-bind

ディレクティブ ディレクティブとは、DOM要素に対して何かを実行することをライブラリに伝達する、マークアップ中の特別なトークンです。 v-bind htmlタグに属性を付ける場合などに使用する。 case1 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> </head> <body> <div id="app"> {{ message }} <!-- href属性にdataで定義したgoogleプロパティを追加することができる。 --> <a v-bind:href="google">googleへのリンク</a> <!-- 省略記法 --> <a :href="google">googleへのリンク</a> </div> <script> let app = new Vue({ el: '#app', data(){ return{ message:'Hello Vue!', google: 'https://google.com' } } }) </script> </body> </html> case2 オブジェクト形式で書くことも可能。 DOMの属性に対してプロパティ名が同じの場合は省略して書くことができる。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> </head> <body> <div id="app"> {{ message }} <a v-bind:href="google">googleへのリンク</a> <br> <a :href="book.url"></a>{{book.title}}</a> <br> <input v-bind="formInput"> DOMの属性に対してプロパティ名が同じの場合は省略して書/くことができる。 <br> <input v-bind="{name:formInput.name,placeholder:formInput.placeholder }"> </div> <script> let app = new Vue({ el: '#app', data(){ return{ message:'Hello Vue!', google: 'https://google.com', book:{ title: '宇宙はなぜこんなに上手くできているのか', url:'https://www.amazon.co.jp/%E5%AE%87%E5%AE%99%E3%81%AF%E3%81%AA%E3%81%9C%E3%81%93%E3%82%93%E3%81%AA%E3%81%AB%E3%81%86%E3%81%BE%E3%81%8F%E3%81%A7%E3%81%8D%E3%81%A6%E3%81%84%E3%82%8B%E3%81%AE%E3%81%8B-%E7%9F%A5%E3%81%AE%E3%83%88%E3%83%AC%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0%E5%8F%A2%E6%9B%B8-%E6%9D%91%E5%B1%B1%E6%96%89-ebook/dp/B00MTUI0LA' }, formInput:{ name:'your_name', placeholder:'お名前を入力してください。' } } } }) </script> </body> </html> case3 syleやclassの設定ができる。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> <style> .active { border:10px, solid red; } </style> </head> <body> <div id="app"> //cssプロパティなどでスネークケースプロパティがあるが使用できない。 //変わってキャメルケースで記述する必要がある。`font-size` > fontSize <h1 :style="{fontSize:fontSize, color:color}">いろはす</h1> //isAcriveに真偽値を設定することで表示非表示ができる。 <div :class="{active: isActive}">classテスト</div> </div> <script> const app = new Vue({ el: '#app', data(){ return { fontSize:"50px", color:"green", isActive: true } } }) </script> </body> </html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webの勉強はじめてみた その6 ~JavaScript編 ループと配列~

「N予備校のプログラミング入門 Webアプリ」のアーカイブ動画と一緒に勉強を進めています。 今日は第一章の9節と10節を受講しました。 ループ処理 まずはFizzBuzzをfor文で。 for(var i = 1; i <= 100; i+=1){ if(i % 15 === 0){ document.write('FizzBuzz '); }else if(i % 3 === 0){ document.write('Fizz '); }else if(i % 5 === 0){ document.write('Buzz '); }else{ document.write(i + ' '); } } 授業では扱わないっぽいので、while と do while を思い出しながらやってみた。 whileって何か応答を待つ間ずっと回す時に使うイメージ。 whileの場合 var i = 1; while(i <= 100){ if(i % 15 === 0){ document.write('FizzBuzz' + ' '); }else if(i % 5 === 0){ document.write('Buzz' + ' '); }else if(i % 3 === 0){ document.write('Fizz' + ' '); }else{ document.write(i + ' '); } i++; } do whileの場合 var i = 1; do{ if(i % 15 === 0){ document.write('FizzBuzz' + ' '); }else if(i % 5 === 0){ document.write('Buzz' + ' '); }else if(i % 3 === 0){ document.write('Fizz' + ' '); }else{ document.write(i + ' '); } i++; }while(i <= 100); 配列 配列って書き方がわりとあって混乱する。 JavaScriptの場合は[] for文を使って配列を書き出す //1年A組~3年D組を書き出すプログラム let classes=['A組', 'B組', 'C組', 'D組']; for(var grade =1; grade < 4; grade++){ for(var i = 0; i < classes.length; i++){ var rslt = '<p>' + grade + '年' + classes[i] + '<p>' document.write(rslt); } } //あ行とか行を使った2文字の名前の組み合わせを書き出すプログラム let name1 =['あ', 'い', 'う', 'え', 'お','か', 'き', 'く', 'け', 'こ']; for(var i = 0; i < name1.length; i++){ for(var x = 0; x < name1.length; x++){ var rslt = name1[i] + name1[x] + '<br>' document.write(rslt); } } ForEachを使う 配列とえいばforeachだと勝手に思ってます。 でも書き方が覚えにくい。VB.NETはシンプルだったなぁ(遠い目)。 //1年A組~3年D組を書き出すプログラム let classes =['A組', 'B組', 'C組', 'D組']; //学年も配列にしてみる let grade =['1年','2年','3年']; grade.forEach(gr => { classes.forEach(cls => { var rslt ='<p>' + gr + cls + '</p>'; document.write(rslt); }); }); //あ行とか行を使った2文字の名前の組み合わせを書き出すプログラム let name1 =['あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ']; name1.forEach(chars => { name1.forEach(chars2 => { var rslt = '<p>' + chars + chars2 +'</p>' document.write(rslt); }); }); For Of を使う これは初めて知りました。 //あ行とか行を使った2文字の名前の組み合わせを書き出すプログラム let name1 =['あ', 'い', 'う', 'え', 'お','か', 'き', 'く', 'け', 'こ']; for(var chr1 of name1){ for(var chr2 of name1){ var rslt = '<p>' + chr1 + chr2 + '</p>'; document.write(rslt); } } こっちのほうがわかりやすいかも。 ここを参考にさせてもらいました。 まとめ ループ処理や配列が絡んでくるといよいよプログラミングって感じがします。 今回授業ではやってなかったんですが、for ofなど、初めて知ることもあったので もっと興味のアンテナを広げたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フォームコンポーネント作りながら理解するCompositionApi

はじめに おはようございます。こんにちは。こんばんは。 Watatakuです。 今回はフォームコンポーネントを作りながらVue3で登場したcompositionApiについて解説していく記事です。 またcompositionApiを使うためにVue3にアップグレード(しなくても良かった)したのでVue2とVue3の違いについてもお話しできればと思います。 今回作ったやつ。⬇︎ コード 環境(Vueのバージョン) optionsApi: ver.2.6.11 compositionApi: ver.3.0.0 書かないこと optionApiの書き方、解説。 Vue Composition APIとは 以下の2点のために策定された関数ベースのAPIです。 Vue Composition APIのメリット 1.コードの可読性・再利用性の改善 2.型インターフェースの改善 今までのVue2でのOptions APIでは1つのコンポーネントが複数の役割を持った際にコードが肥大化し、可読性が著しく低下するという問題がありました。 そこでComposition APIでは関心事によってコードを分割し、分割したコードを簡単にコンポーネントに注入できるようになっています。 詳しくはこちら テキストボックス optionsApi App.vue <template> <div class="contents"> <TextInput v-model="text" type="text" name="text" placeholder="テキストボックス" :value="text" /> </div> </template> <script> import TextInput from "./components/TextInput.vue"; export default { components: { TextInput, }, data() { return { text: "", }; }, }; </script> TextInput.vue <template> <input :type="this.type" :placeholder="this.placeholder" :name="this.name" :value="this.value" @input="updateValue" /> </template> <script> export default { props: { type: { type: String }, placeholder: { type: String }, name: { type: String }, value: { type: String }, }, methods: { updateValue(e) { this.$emit("input", e.target.value); }, }, }; </script> compositionApi App.vue <template> <TextInput v-model:modalValue="form.text" type="text" name="text" placeholder="テキストボックス" :value="form.text" /> </template> <script> import { reactive } from "vue"; import TextInput from "./components/TextInput.vue"; export default { name: "App", components: { TextInput, }, setup() { const form = reactive({ text: "", }); return { form }; }, }; </script> TextInput <template> <input :type="type" :placeholder="placeholder" :name="name" :value="value" @input="updateValue" /> </template> <script> export default { props: { type: { type: String, required: true }, placeholder: { type: String, required: true }, name: { type: String, required: true }, value: { type: String, required: true }, }, emits: ["update:modalValue"], setup(_, context) { const updateValue = (e) => { context.emit("update:modalValue", e.target.value); }; return { updateValue }; }, }; </script> optionApiとcompositionApiを見比べて大きく違うのはsetup(){}の有無ではないでしょうか? このsetup(){}がCompositionApiを扱うためのキモになります。 このsetup(){}の中に、optionApiで言う「data, mounted, methods, ・・・・」を描くイメージです。 data() まずはじめにdata()をみていきます。 結論から申し上げますと, "vue"からref or reactiveで宣言すればいいです。 そして、その変数をテンプレートで扱うためにreturnします。 // --------------- optionsApi ---------------------- data() { return { text: "", }; }, // アクセス this.text // -------------- compositionApi -------------- // reactiveで書く場合 setup() { const form = reactive({ text: "", }); return { form }; }, // アクセス form.text // refで書く場合 setup() { const text = ref(""); return { text }; }, // アクセス text.value refとreactiveの違いについては下記記事がご参考になると思います。 参照 methods optionsApiでは、メソッドはmethodsに書かなくてはなりませんでしたが、 compositionApiではこれもsetup(){}に記述します。 書き方は、setup(){}で普通にjavascript(typescriptを使うのであれば、typescript)でメソッド宣言するだけです。 あとはdata()同様、returnするだけ。 //optionApi methods: { updateValue(e) { this.$emit("input", e.target.value); }, }, //compositionApi setup(_, context) { const updateValue = (e) => { context.emit("update:modalValue", e.target.value); }; return { updateValue }; }, propsとemit props propsの渡し方、受け取りかたは大きく変わってないです。 ただ、受け取ったpropsを用いてデータを加工したりする時(setup関数に渡さなければいけない時)に工夫が必要です。 props: { title: { type: String, required: true }, count: { type: Number, default: 0 } }, setup (props) { const doubleCount = computed(() => props.count * 2) return { doubleCount } } setup(){}の第一引数がpropsになります。ちなみに後述しますが第ニ引数がコンテキスト。 コンテキストを用いてemitします。 emit まずはじめに、Vue2とVue3で仕様が若干変わっていたので、作者は苦戦しました。ちなみにv-modelも若干仕様が変わっています。 詳しくはこちら。 v-model emit emits: ["update:modalValue"], setup(_, context) { const updateValue = (e) => { context.emit("update:modalValue", e.target.value); }; return { updateValue }; } Vue2との違いは事前にemitのイベントをemitsプロパティに指定します。 その後、propsの時にも少し触れたのですが、setup(){}の第二引数でコンテキストを指定し、 コンテキストのemitメソッドを使います。 ちなみにpropsが必要なく、emitだけを使いたい場合は上記のように(_, context)のように書きます。 他のcompositionApiの書き方 以上が、このアプリでのcompositionApiの説明でした。 おい、Watataku!!残りのライフサイクルとかwatchはどないすんねん!! と言うことで、ここからはこのアプリとは関係ないですがライフサイクルとかwatch*を説明していきましょう。 結論、importしろ。これだけです。 ライフサイクル // optionsApi export default { created() { console.log("created"); }, mounted() { console.log("mounted"); }, }; // compositionApi import { onMounted } from "vue"; export default { setup() { console.log("created"); onMounted(() => { console.log("mounted"); }); }, }; それぞれのライフサイクルに対応したonXXX関数を使用する beforeCreate・createdがsetupにまとめられている computed //optionsApi export default { data() { return { count: 1, }; }, computed: { doubleCount() { return this.count * 2; }, }, }; //compositionApi import { ref, computed } from "vue"; export default { setup() { const countRef = ref(1); // computed関数を使用して計算プロパティを定義します const doubleCount = computed(() => { return countRef.value * 2; }); return { countRef, doubleCount, }; }, }; watch //optionsApi export default { data() { return { count: 1, }; }, watch: { count(count, prevCount) { console.log(count); console.log(prevCount); }, }, }; //compositionApi import { ref, watch } from "vue"; export default { setup() { const countRef = ref(1); // watch関数でリアクティブな変数を監視します watch(countRef, (count, prevCount) => { console.log(count); console.log(prevCount); }); return { countRef, }; }, }; watchの第1引数には監視対象、第2引数にはcallback関数を渡します。 callback関数の第1引数には変更後の値、第2引数には変更前の値が渡されます。 画像アップローダー ここからはフォームコンポーネントの続きをやっていきます。 パスワードとテキストエリアに関してはテキストボックスと同じような作りなので省略します。 optionsApi App.vue <template> <div class="contents"> <div class="imgContent"> <ImagePreview :imageUrl="imageUrl" /> <div class="module--spacing--largeSmall"></div> <UploadFile @fileList="setFileList" /> </div> </div> </template> <script> import ImagePreview from "./components/ImagePreview.vue"; import UploadFile from "./components/UploadFile.vue"; export default { components: { ImagePreview, UploadFile, }, data() { return { imageUrl: "", fileList: null, }; }, methods: { setFileList(fileList) { this.fileList = fileList; const imageUrl = URL.createObjectURL(fileList[0]); this.imageUrl = imageUrl; }, } }; </script> <style> @media screen and (min-width: 1026px) { .imgContent { width: 90%; max-width: 700px; height: 35vh; margin: auto; margin-bottom: 10px; background-color: #ccc; padding-top: 5%; } } @media screen and (min-width: 482px) and (max-width: 1025px) { .imgContent { width: 90%; max-width: 700px; height: 20vh; margin: auto; margin-bottom: 10px; background-color: #ccc; padding-top: 5%; } } </style> UploadFile.vue <template> <label for="corporation_file" class="btn btn-success"> 画像を設定する <input type="file" class="file_input" style="display:none;" id="corporation_file" mulitple="multiple" @change="onDrop" /> </label> </template> <script> export default { methods: { onDrop(e) { const imageFile = e.target.files; if(imageFile) { this.$emit("fileList", imageFile); } }, }, }; </script> <style scoped> label { background-color: #fff; padding: 1%; width: 40%; margin: 0 auto; box-shadow: 1px 1px 8px 0px #000; display: block; } </style> ImagePreview.vue <template> <div class="imagePreview"> <img :src="this.imageUrl" width="50" height="50" alt /> </div> </template> <script> export default { props: ["imageUrl"], }; </script> <style scoped> @media screen and (min-width: 1026px) { .imagePreview { height: 200px; width: 200px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 200px; width: 200px; } } @media screen and (min-width: 482px) and (max-width: 1025px) { .imagePreview { height: 100px; width: 100px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 100px; width: 100px; } } @media screen and (max-width: 481px) { .imagePreview { height: 50px; width: 50px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 50px; width: 50px; } } </style> compositionApi App.vue <template> <div class="imgContent"> <ImagePreview :imageUrl="form.imageUrl" /> <div class="module--spacing--largeSmall"></div> <UploadFile @fileList="setFileList" /> </div> </template> <script> import { reactive } from "vue"; import ImagePreview from "./components/ImagePreview.vue"; import UploadFile from "./components/UploadFile.vue"; export default { name: "App", ImagePreview, UploadFile, }, setup() { const form = reactive({ imageUrl: "", fileList: null, }); const setFileList = (fileList) => { form.fileList = fileList; const imgUrl = URL.createObjectURL(fileList[0]); form.imageUrl = imgUrl; }; return { form, setFileList, }; }, }; </script> <style> @media screen and (min-width: 1026px) { .imgContent { width: 90%; max-width: 700px; height: 35vh; margin: auto; margin-bottom: 10px; background-color: #ccc; padding-top: 5%; } } @media screen and (min-width: 482px) and (max-width: 1025px) { .imgContent { width: 90%; max-width: 700px; height: 20vh; margin: auto; margin-bottom: 10px; background-color: #ccc; padding-top: 5%; } } </style> UploadFile.vue <template> <label for="corporation_file" class="btn btn-success"> 画像を設定する <input type="file" class="file_input" style="display: none" id="corporation_file" mulitple="multiple" @change="onDrop" /> </label> </template> <script> export default { emits: ["fileList"], setup(_, context) { const onDrop = (e) => { const imageFile = e.target.files; if (imageFile) { context.emit("fileList", imageFile); } }; return { onDrop }; }, }; </script> <style scoped> label { background-color: #fff; padding: 1%; width: 40%; margin: 0 auto; box-shadow: 1px 1px 8px 0px #000; display: block; } </style> ImagePreview.vue <template> <div class="imagePreview"> <img :src="imageUrl" alt /> </div> </template> <script> export default { props: { imageUrl: { type: String, required: true, }, }, }; </script> <style scoped> @media screen and (min-width: 1026px) { .imagePreview { height: 200px; width: 200px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 200px; width: 200px; } } @media screen and (min-width: 482px) and (max-width: 1025px) { .imagePreview { height: 100px; width: 100px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 100px; width: 100px; } } @media screen and (max-width: 481px) { .imagePreview { height: 50px; width: 50px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 50px; width: 50px; } } </style> セレクトボックス optionsApi App.vue <template> <div class="contents"> <SelextBox v-model="select" :options="optionsSelect" /> <span v-if="this.select == ''">選択オプション:選択してください。</span> <span v-else>選択オプション: {{ select }}</span> </div> </template> <script> import SelextBox from "./components/SelectBox.vue"; export default { components: { SelextBox, }, data() { return { select: 0, selectValue: "", optionsSelect: [ { label: "Vue.js", value: "Vue.js" }, { label: "React", value: "React" }, { label: "Angular", value: "Angular" }, ], }; }, }; </script> SelectBox.vue <template> <select name="select-box" @input="updateValue"> <option value="0">選択してください</option> <option v-for="(option, index) in options" :key="index" :value="option.value" >{{ option.label }}</option > </select> </template> <script> export default { props: { options: { type: Array, required: true }, }, methods: { updateValue(e) { this.$emit("input", e.target.value); }, }, }; </script> compositionApi App.vue <template> <SelectBox v-model:select="form.select" name="selectbox" :options="form.optionsSelect" /> <div class="module--spacing--verySmall"></div> <span v-if="form.select == ''">選択オプション:選択してください。</span> <span v-else>選択オプション: {{ form.select }}</span> </template> <script> import { reactive } from "vue"; import SelectBox from "./components/SelectBox.vue"; export default { name: "App", components: { SelectBox, }, setup() { const form = reactive({ select: 0, selectValue: "", optionsSelect: [ { label: "Vue.js", value: "Vue.js" }, { label: "React", value: "React" }, { label: "Angular", value: "Angular" }, ], }); return { form, }; }, }; </script> SelectBox.vue <template> <select :name="name" @input="updateValue"> <option value="0">選択してください</option> <option v-for="(option, index) in options" :key="index" :value="option.value" > {{ option.label }} </option> </select> </template> <script> export default { props: { name: { type: String, required: true }, options: { type: Array, required: true }, }, emits: ["update:select"], setup(_, context) { const updateValue = (e) => { context.emit("update:select", e.target.value); }; return { updateValue }; }, }; </script> ラジオボタン optionsApi App.vue <template> <div class="contents"> <RadioButton v-model="checkName" :options="optionsRadio" /> <span>選択オプション: {{ checkName }}</span> </div> </template> <script> import RadioButton from "./components/RadioButton.vue"; export default { components: { RadioButton, }, data() { return { checkName: "選択してね", optionsRadio: [ { label: "hoge", value: "hoge" }, { label: "bow", value: "bow" }, { label: "fuga", value: "fuga" }, ], }; }, }; </script> RadioButton <template> <div> <label v-for="(option, index) in options" :key="index"> <!-- ラジオボタンにはname属性必須 --> <input type="radio" :value="option.value" @change="updateValue" name="radio-button" />{{ option.label }}</label > </div> </template> <script> export default { props: { options: { type: Array, required: true }, }, methods: { updateValue(e) { this.$emit("input", e.target.value); }, }, }; </script> compositionApi App.vue <template> <RadioButton v-model:modalValue="form.checkName" :options="form.optionsRadio" /> <span>選択オプション: {{ form.checkName }}</span> </template> <script> import { reactive } from "vue"; import RadioButton from "./components/RadioButton.vue"; export default { name: "App", components: { RadioButton, }, setup() { const form = reactive({ checkName: "選択してね", optionsRadio: [ { label: "hoge", value: "hoge" }, { label: "bow", value: "bow" }, { label: "fuga", value: "fuga" }, ], }); return { form, }; }, }; </script> RadioButton.vue <template> <div> <label v-for="(option, index) in options" :key="index"> <!-- ラジオボタンにはname属性必須 --> <input type="radio" :value="option.value" @change="updateValue" name="radio-button" />{{ option.label }}</label > </div> </template> <script> export default { props: { options: { type: Array, required: true }, }, emits: ["update:modalValue"], setup(_, context) { const updateValue = (e) => { context.emit("update:modalValue", e.target.value); }; return { updateValue }; }, }; </script> ボタン チェックボックスにチェックを入れないとボタンがクリックできなくて、 そのボタンをクリックしたらモーダルが出現するものを実装します。(説明下手でごめんなさい。) optionsApi App.vue <template> <div class="contents"> <CheckBox v-model="checked" :checked="checked" /> <label>同意</label><br /> <Button :disabled="!checked" msg="モーダルが出ます" @push="click" /> <Modal title="モーダルタイトル" detail="モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。" v-if="open" @close="open = false" @modal-click="modalClick" /> </div> </template> <script> import CheckBox from "./components/CheckBox.vue"; import Button from "./components/Button.vue"; import Modal from "./components/Modal.vue"; export default { components: { CheckBox, Button, Modal, }, data() { return { checked: false, open: false, }; }, methods: { click() { this.open = true; }, modalClick() { console.log("テキストボックスの入力内容:", this.text); console.log("パスワードの入力内容:", this.pass); console.log("テキストエリアの入力内容:", this.textarea); console.log("画像の名前:", this.fileList[0].name); alert("コンソールを見ろ!!"); this.open = false; this.checked = false; // this.uploadImage(); }, }; </script> CheckBox.vue <template> <input type="checkbox" @change="updateValue" :checked="this.checked" /> </template> <script> export default { props: ["checked"], methods: { updateValue(e) { this.$emit("input", e.target.checked); }, }, }; </script> Button.vue <template> <button class="button" @click="push"> {{ this.msg }} </button> </template> <script> export default { props: ["msg"], methods: { push() { this.$emit("push"); }, }, }; </script> Modal.vue <template> <transition name="modal"> <div class="overlay" @click="$emit('close')"> <div class="panel" @click.stop> <h2>{{ this.title }}</h2> <div class="module--spacing--small"></div> <div class="modal-contents"> <p>{{ this.detail }}</p> </div> <Button :disabled="false" msg="ボタン" @push="click" /> </div> </div> </transition> </template> <script> import Button from "./Button.vue"; export default { props: ["title", "detail"], components: { Button, }, methods: { click() { this.$emit("modal-click"); }, }, }; </script> <style scoped> .overlay { background: rgba(0, 0, 0, 0.8); position: fixed; width: 100%; height: 100%; left: 0; top: 0; right: 0; bottom: 0; z-index: 900; transition: all 0.5s ease; } .panel { width: 40%; text-align: center; background: #fff; padding: 40px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .modal-contents { text-align: left; } .modal-enter, .modal-leave-active { opacity: 0; } .modal-enter .panel, .modal-leave-active .panel { top: -200px; } </style> compositionApi App.vue <template> <CheckBox v-model:change="form.checked" :checked="form.checked" /> <label>同意</label><br /> <Button :disabled="!form.checked" msg="モーダルが出ます" @push="handleOpen" /> <Modal title="モーダルタイトル" detail="モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。" v-if="form.open" @close="form.open = false" @modal-click="modalClick" /> </template> <script> import { reactive } from "vue"; import CheckBox from "./components/CheckBox.vue"; import Button from "./components/Button.vue"; import Modal from "./components/Modal.vue"; export default { name: "App", components: { CheckBox, Button, Modal, }, setup() { const form = reactive({ checked: false, open: false, }); const handleOpen = () => { form.open = true; }; const modalClick = () => { aleat("モーダルの中のボタンをクリックしました") }; return { form, handleOpen, modalClick }; }, }; </script> CheckBox.vue <template> <input type="checkbox" @change="updateValue" :checked="checked" /> </template> <script> export default { model: { prop: "checked", event: "change", }, props: { checked: { type: Boolean, required: true, }, }, emits: ["update:change"], setup(_, context) { const updateValue = (e) => { context.emit("update:change", e.target.checked); }; return { updateValue }; }, }; </script> Button.vue <template> <button class="button" @click="push"> {{ msg }} </button> </template> <script> export default { props: { msg: { type: String, required: true, }, }, emits: ["push"], setup(_, context) { const push = () => { context.emit("push"); }; return { push }; }, }; </script> Modal.vue <template> <transition name="modal"> <div class="overlay" @click="handleClose"> <div class="panel" @click.stop> <h2>{{ title }}</h2> <div class="module--spacing--small"></div> <div class="modal-contents"> <p>{{ detail }}</p> </div> <Button :disabled="false" msg="ボタン" @push="click" /> </div> </div> </transition> </template> <script> import Button from "./Button.vue"; export default { props: { title: { type: String, required: true, }, detail: { type: String, required: true, }, }, components: { Button, }, emits: ["close", "modal-click"], setup(_, context) { const handleClose = () => { context.emit("close"); }; const click = () => { context.emit("modal-click"); }; return { handleClose, click }; }, }; </script> <style scoped> .overlay { background: rgba(0, 0, 0, 0.8); position: fixed; width: 100%; height: 100%; left: 0; top: 0; right: 0; bottom: 0; z-index: 900; transition: all 0.5s ease; } .panel { width: 40%; text-align: center; background: #fff; padding: 40px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .modal-contents { text-align: left; } .modal-enter, .modal-leave-active { opacity: 0; } .modal-enter .panel, .modal-leave-active .panel { top: -200px; } </style> まとめ 以上。 フォームコンポーネントを作りながらcompositionApiについて書かせていただきました。 ご参考になれば幸いです。 もし間違いなどがあれば、ご教授お願いします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フォームコンポーネントを作りながら理解するCompositionApi

はじめに おはようございます。こんにちは。こんばんは。 Watatakuです。 今回はフォームコンポーネントを作りながらVue3で登場したcompositionApiについて解説していく記事です。 またcompositionApiを使うためにVue3にアップグレード(しなくても良かった)したのでVue2とVue3の違いについてもお話しできればと思います。 今回作ったやつ。⬇︎ コード 環境(Vueのバージョン) optionsApi: ver.2.6.11 compositionApi: ver.3.0.0 書かないこと optionApiの書き方、解説。 Vue Composition APIとは 以下の2点のために策定された関数ベースのAPIです。 Vue Composition APIのメリット 1.コードの可読性・再利用性の改善 2.型インターフェースの改善 今までのVue2でのOptions APIでは1つのコンポーネントが複数の役割を持った際にコードが肥大化し、可読性が著しく低下するという問題がありました。 そこでComposition APIでは関心事によってコードを分割し、分割したコードを簡単にコンポーネントに注入できるようになっています。 詳しくはこちら テキストボックス optionsApi App.vue <template> <div class="contents"> <TextInput v-model="text" type="text" name="text" placeholder="テキストボックス" :value="text" /> </div> </template> <script> import TextInput from "./components/TextInput.vue"; export default { components: { TextInput, }, data() { return { text: "", }; }, }; </script> TextInput.vue <template> <input :type="this.type" :placeholder="this.placeholder" :name="this.name" :value="this.value" @input="updateValue" /> </template> <script> export default { props: { type: { type: String }, placeholder: { type: String }, name: { type: String }, value: { type: String }, }, methods: { updateValue(e) { this.$emit("input", e.target.value); }, }, }; </script> compositionApi App.vue <template> <TextInput v-model:modalValue="form.text" type="text" name="text" placeholder="テキストボックス" :value="form.text" /> </template> <script> import { reactive } from "vue"; import TextInput from "./components/TextInput.vue"; export default { name: "App", components: { TextInput, }, setup() { const form = reactive({ text: "", }); return { form }; }, }; </script> TextInput <template> <input :type="type" :placeholder="placeholder" :name="name" :value="value" @input="updateValue" /> </template> <script> export default { props: { type: { type: String, required: true }, placeholder: { type: String, required: true }, name: { type: String, required: true }, value: { type: String, required: true }, }, emits: ["update:modalValue"], setup(_, context) { const updateValue = (e) => { context.emit("update:modalValue", e.target.value); }; return { updateValue }; }, }; </script> optionApiとcompositionApiを見比べて大きく違うのはsetup(){}の有無ではないでしょうか? このsetup(){}がCompositionApiを扱うためのキモになります。 このsetup(){}の中に、optionApiで言う「data, mounted, methods, ・・・・」を描くイメージです。 data() まずはじめにdata()をみていきます。 結論から申し上げますと, "vue"からref or reactiveで宣言すればいいです。 そして、その変数をテンプレートで扱うためにreturnします。 // --------------- optionsApi ---------------------- data() { return { text: "", }; }, // アクセス this.text // -------------- compositionApi -------------- // reactiveで書く場合 setup() { const form = reactive({ text: "", }); return { form }; }, // アクセス form.text // refで書く場合 setup() { const text = ref(""); return { text }; }, // アクセス text.value refとreactiveの違いについては下記記事がご参考になると思います。 参照 methods optionsApiでは、メソッドはmethodsに書かなくてはなりませんでしたが、 compositionApiではこれもsetup(){}に記述します。 書き方は、setup(){}で普通にjavascript(typescriptを使うのであれば、typescript)でメソッド宣言するだけです。 あとはdata()同様、returnするだけ。 //optionApi methods: { updateValue(e) { this.$emit("input", e.target.value); }, }, //compositionApi setup(_, context) { const updateValue = (e) => { context.emit("update:modalValue", e.target.value); }; return { updateValue }; }, propsとemit props propsの渡し方、受け取りかたは大きく変わってないです。 ただ、受け取ったpropsを用いてデータを加工したりする時(setup関数に渡さなければいけない時)に工夫が必要です。 props: { title: { type: String, required: true }, count: { type: Number, default: 0 } }, setup (props) { const doubleCount = computed(() => props.count * 2) return { doubleCount } } setup(){}の第一引数がpropsになります。ちなみに後述しますが第ニ引数がコンテキスト。 コンテキストを用いてemitします。 emit まずはじめに、Vue2とVue3で仕様が若干変わっていたので、作者は苦戦しました。ちなみにv-modelも若干仕様が変わっています。 詳しくはこちら。 v-model emit emits: ["update:modalValue"], setup(_, context) { const updateValue = (e) => { context.emit("update:modalValue", e.target.value); }; return { updateValue }; } Vue2との違いは事前にemitのイベントをemitsプロパティに指定します。 その後、propsの時にも少し触れたのですが、setup(){}の第二引数でコンテキストを指定し、 コンテキストのemitメソッドを使います。 ちなみにpropsが必要なく、emitだけを使いたい場合は上記のように(_, context)のように書きます。 他のcompositionApiの書き方 以上が、このアプリでのcompositionApiの説明でした。 おい、Watataku!!残りのライフサイクルとかwatchはどないすんねん!! と言うことで、ここからはこのアプリとは関係ないですがライフサイクルとかwatch*を説明していきましょう。 結論、importしろ。これだけです。 ライフサイクル // optionsApi export default { created() { console.log("created"); }, mounted() { console.log("mounted"); }, }; // compositionApi import { onMounted } from "vue"; export default { setup() { console.log("created"); onMounted(() => { console.log("mounted"); }); }, }; それぞれのライフサイクルに対応したonXXX関数を使用する beforeCreate・createdがsetupにまとめられている computed //optionsApi export default { data() { return { count: 1, }; }, computed: { doubleCount() { return this.count * 2; }, }, }; //compositionApi import { ref, computed } from "vue"; export default { setup() { const countRef = ref(1); // computed関数を使用して計算プロパティを定義します const doubleCount = computed(() => { return countRef.value * 2; }); return { countRef, doubleCount, }; }, }; watch //optionsApi export default { data() { return { count: 1, }; }, watch: { count(count, prevCount) { console.log(count); console.log(prevCount); }, }, }; //compositionApi import { ref, watch } from "vue"; export default { setup() { const countRef = ref(1); // watch関数でリアクティブな変数を監視します watch(countRef, (count, prevCount) => { console.log(count); console.log(prevCount); }); return { countRef, }; }, }; watchの第1引数には監視対象、第2引数にはcallback関数を渡します。 callback関数の第1引数には変更後の値、第2引数には変更前の値が渡されます。 画像アップローダー ここからはフォームコンポーネントの続きをやっていきます。 パスワードとテキストエリアに関してはテキストボックスと同じような作りなので省略します。 optionsApi App.vue <template> <div class="contents"> <div class="imgContent"> <ImagePreview :imageUrl="imageUrl" /> <div class="module--spacing--largeSmall"></div> <UploadFile @fileList="setFileList" /> </div> </div> </template> <script> import ImagePreview from "./components/ImagePreview.vue"; import UploadFile from "./components/UploadFile.vue"; export default { components: { ImagePreview, UploadFile, }, data() { return { imageUrl: "", fileList: null, }; }, methods: { setFileList(fileList) { this.fileList = fileList; const imageUrl = URL.createObjectURL(fileList[0]); this.imageUrl = imageUrl; }, } }; </script> <style> @media screen and (min-width: 1026px) { .imgContent { width: 90%; max-width: 700px; height: 35vh; margin: auto; margin-bottom: 10px; background-color: #ccc; padding-top: 5%; } } @media screen and (min-width: 482px) and (max-width: 1025px) { .imgContent { width: 90%; max-width: 700px; height: 20vh; margin: auto; margin-bottom: 10px; background-color: #ccc; padding-top: 5%; } } </style> UploadFile.vue <template> <label for="corporation_file" class="btn btn-success"> 画像を設定する <input type="file" class="file_input" style="display:none;" id="corporation_file" mulitple="multiple" @change="onDrop" /> </label> </template> <script> export default { methods: { onDrop(e) { const imageFile = e.target.files; if(imageFile) { this.$emit("fileList", imageFile); } }, }, }; </script> <style scoped> label { background-color: #fff; padding: 1%; width: 40%; margin: 0 auto; box-shadow: 1px 1px 8px 0px #000; display: block; } </style> ImagePreview.vue <template> <div class="imagePreview"> <img :src="this.imageUrl" width="50" height="50" alt /> </div> </template> <script> export default { props: ["imageUrl"], }; </script> <style scoped> @media screen and (min-width: 1026px) { .imagePreview { height: 200px; width: 200px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 200px; width: 200px; } } @media screen and (min-width: 482px) and (max-width: 1025px) { .imagePreview { height: 100px; width: 100px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 100px; width: 100px; } } @media screen and (max-width: 481px) { .imagePreview { height: 50px; width: 50px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 50px; width: 50px; } } </style> compositionApi App.vue <template> <div class="imgContent"> <ImagePreview :imageUrl="form.imageUrl" /> <div class="module--spacing--largeSmall"></div> <UploadFile @fileList="setFileList" /> </div> </template> <script> import { reactive } from "vue"; import ImagePreview from "./components/ImagePreview.vue"; import UploadFile from "./components/UploadFile.vue"; export default { name: "App", ImagePreview, UploadFile, }, setup() { const form = reactive({ imageUrl: "", fileList: null, }); const setFileList = (fileList) => { form.fileList = fileList; const imgUrl = URL.createObjectURL(fileList[0]); form.imageUrl = imgUrl; }; return { form, setFileList, }; }, }; </script> <style> @media screen and (min-width: 1026px) { .imgContent { width: 90%; max-width: 700px; height: 35vh; margin: auto; margin-bottom: 10px; background-color: #ccc; padding-top: 5%; } } @media screen and (min-width: 482px) and (max-width: 1025px) { .imgContent { width: 90%; max-width: 700px; height: 20vh; margin: auto; margin-bottom: 10px; background-color: #ccc; padding-top: 5%; } } </style> UploadFile.vue <template> <label for="corporation_file" class="btn btn-success"> 画像を設定する <input type="file" class="file_input" style="display: none" id="corporation_file" mulitple="multiple" @change="onDrop" /> </label> </template> <script> export default { emits: ["fileList"], setup(_, context) { const onDrop = (e) => { const imageFile = e.target.files; if (imageFile) { context.emit("fileList", imageFile); } }; return { onDrop }; }, }; </script> <style scoped> label { background-color: #fff; padding: 1%; width: 40%; margin: 0 auto; box-shadow: 1px 1px 8px 0px #000; display: block; } </style> ImagePreview.vue <template> <div class="imagePreview"> <img :src="imageUrl" alt /> </div> </template> <script> export default { props: { imageUrl: { type: String, required: true, }, }, }; </script> <style scoped> @media screen and (min-width: 1026px) { .imagePreview { height: 200px; width: 200px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 200px; width: 200px; } } @media screen and (min-width: 482px) and (max-width: 1025px) { .imagePreview { height: 100px; width: 100px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 100px; width: 100px; } } @media screen and (max-width: 481px) { .imagePreview { height: 50px; width: 50px; background: rgb(240, 240, 240); overflow: hidden; border-radius: 50%; background-position: center center; background-size: cover; margin-left: auto; margin-right: auto; margin-bottom: 20px; position: relative; } .imagePreview img { height: 50px; width: 50px; } } </style> セレクトボックス optionsApi App.vue <template> <div class="contents"> <SelextBox v-model="select" :options="optionsSelect" /> <span v-if="this.select == ''">選択オプション:選択してください。</span> <span v-else>選択オプション: {{ select }}</span> </div> </template> <script> import SelextBox from "./components/SelectBox.vue"; export default { components: { SelextBox, }, data() { return { select: 0, selectValue: "", optionsSelect: [ { label: "Vue.js", value: "Vue.js" }, { label: "React", value: "React" }, { label: "Angular", value: "Angular" }, ], }; }, }; </script> SelectBox.vue <template> <select name="select-box" @input="updateValue"> <option value="0">選択してください</option> <option v-for="(option, index) in options" :key="index" :value="option.value" >{{ option.label }}</option > </select> </template> <script> export default { props: { options: { type: Array, required: true }, }, methods: { updateValue(e) { this.$emit("input", e.target.value); }, }, }; </script> compositionApi App.vue <template> <SelectBox v-model:select="form.select" name="selectbox" :options="form.optionsSelect" /> <div class="module--spacing--verySmall"></div> <span v-if="form.select == ''">選択オプション:選択してください。</span> <span v-else>選択オプション: {{ form.select }}</span> </template> <script> import { reactive } from "vue"; import SelectBox from "./components/SelectBox.vue"; export default { name: "App", components: { SelectBox, }, setup() { const form = reactive({ select: 0, selectValue: "", optionsSelect: [ { label: "Vue.js", value: "Vue.js" }, { label: "React", value: "React" }, { label: "Angular", value: "Angular" }, ], }); return { form, }; }, }; </script> SelectBox.vue <template> <select :name="name" @input="updateValue"> <option value="0">選択してください</option> <option v-for="(option, index) in options" :key="index" :value="option.value" > {{ option.label }} </option> </select> </template> <script> export default { props: { name: { type: String, required: true }, options: { type: Array, required: true }, }, emits: ["update:select"], setup(_, context) { const updateValue = (e) => { context.emit("update:select", e.target.value); }; return { updateValue }; }, }; </script> ラジオボタン optionsApi App.vue <template> <div class="contents"> <RadioButton v-model="checkName" :options="optionsRadio" /> <span>選択オプション: {{ checkName }}</span> </div> </template> <script> import RadioButton from "./components/RadioButton.vue"; export default { components: { RadioButton, }, data() { return { checkName: "選択してね", optionsRadio: [ { label: "hoge", value: "hoge" }, { label: "bow", value: "bow" }, { label: "fuga", value: "fuga" }, ], }; }, }; </script> RadioButton <template> <div> <label v-for="(option, index) in options" :key="index"> <!-- ラジオボタンにはname属性必須 --> <input type="radio" :value="option.value" @change="updateValue" name="radio-button" />{{ option.label }}</label > </div> </template> <script> export default { props: { options: { type: Array, required: true }, }, methods: { updateValue(e) { this.$emit("input", e.target.value); }, }, }; </script> compositionApi App.vue <template> <RadioButton v-model:modalValue="form.checkName" :options="form.optionsRadio" /> <span>選択オプション: {{ form.checkName }}</span> </template> <script> import { reactive } from "vue"; import RadioButton from "./components/RadioButton.vue"; export default { name: "App", components: { RadioButton, }, setup() { const form = reactive({ checkName: "選択してね", optionsRadio: [ { label: "hoge", value: "hoge" }, { label: "bow", value: "bow" }, { label: "fuga", value: "fuga" }, ], }); return { form, }; }, }; </script> RadioButton.vue <template> <div> <label v-for="(option, index) in options" :key="index"> <!-- ラジオボタンにはname属性必須 --> <input type="radio" :value="option.value" @change="updateValue" name="radio-button" />{{ option.label }}</label > </div> </template> <script> export default { props: { options: { type: Array, required: true }, }, emits: ["update:modalValue"], setup(_, context) { const updateValue = (e) => { context.emit("update:modalValue", e.target.value); }; return { updateValue }; }, }; </script> ボタン チェックボックスにチェックを入れないとボタンがクリックできなくて、 そのボタンをクリックしたらモーダルが出現するものを実装します。(説明下手でごめんなさい。) optionsApi App.vue <template> <div class="contents"> <CheckBox v-model="checked" :checked="checked" /> <label>同意</label><br /> <Button :disabled="!checked" msg="モーダルが出ます" @push="click" /> <Modal title="モーダルタイトル" detail="モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。" v-if="open" @close="open = false" @modal-click="modalClick" /> </div> </template> <script> import CheckBox from "./components/CheckBox.vue"; import Button from "./components/Button.vue"; import Modal from "./components/Modal.vue"; export default { components: { CheckBox, Button, Modal, }, data() { return { checked: false, open: false, }; }, methods: { click() { this.open = true; }, modalClick() { console.log("テキストボックスの入力内容:", this.text); console.log("パスワードの入力内容:", this.pass); console.log("テキストエリアの入力内容:", this.textarea); console.log("画像の名前:", this.fileList[0].name); alert("コンソールを見ろ!!"); this.open = false; this.checked = false; // this.uploadImage(); }, }; </script> CheckBox.vue <template> <input type="checkbox" @change="updateValue" :checked="this.checked" /> </template> <script> export default { props: ["checked"], methods: { updateValue(e) { this.$emit("input", e.target.checked); }, }, }; </script> Button.vue <template> <button class="button" @click="push"> {{ this.msg }} </button> </template> <script> export default { props: ["msg"], methods: { push() { this.$emit("push"); }, }, }; </script> Modal.vue <template> <transition name="modal"> <div class="overlay" @click="$emit('close')"> <div class="panel" @click.stop> <h2>{{ this.title }}</h2> <div class="module--spacing--small"></div> <div class="modal-contents"> <p>{{ this.detail }}</p> </div> <Button :disabled="false" msg="ボタン" @push="click" /> </div> </div> </transition> </template> <script> import Button from "./Button.vue"; export default { props: ["title", "detail"], components: { Button, }, methods: { click() { this.$emit("modal-click"); }, }, }; </script> <style scoped> .overlay { background: rgba(0, 0, 0, 0.8); position: fixed; width: 100%; height: 100%; left: 0; top: 0; right: 0; bottom: 0; z-index: 900; transition: all 0.5s ease; } .panel { width: 40%; text-align: center; background: #fff; padding: 40px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .modal-contents { text-align: left; } .modal-enter, .modal-leave-active { opacity: 0; } .modal-enter .panel, .modal-leave-active .panel { top: -200px; } </style> compositionApi App.vue <template> <CheckBox v-model:change="form.checked" :checked="form.checked" /> <label>同意</label><br /> <Button :disabled="!form.checked" msg="モーダルが出ます" @push="handleOpen" /> <Modal title="モーダルタイトル" detail="モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。" v-if="form.open" @close="form.open = false" @modal-click="modalClick" /> </template> <script> import { reactive } from "vue"; import CheckBox from "./components/CheckBox.vue"; import Button from "./components/Button.vue"; import Modal from "./components/Modal.vue"; export default { name: "App", components: { CheckBox, Button, Modal, }, setup() { const form = reactive({ checked: false, open: false, }); const handleOpen = () => { form.open = true; }; const modalClick = () => { aleat("モーダルの中のボタンをクリックしました") }; return { form, handleOpen, modalClick }; }, }; </script> CheckBox.vue <template> <input type="checkbox" @change="updateValue" :checked="checked" /> </template> <script> export default { model: { prop: "checked", event: "change", }, props: { checked: { type: Boolean, required: true, }, }, emits: ["update:change"], setup(_, context) { const updateValue = (e) => { context.emit("update:change", e.target.checked); }; return { updateValue }; }, }; </script> Button.vue <template> <button class="button" @click="push"> {{ msg }} </button> </template> <script> export default { props: { msg: { type: String, required: true, }, }, emits: ["push"], setup(_, context) { const push = () => { context.emit("push"); }; return { push }; }, }; </script> Modal.vue <template> <transition name="modal"> <div class="overlay" @click="handleClose"> <div class="panel" @click.stop> <h2>{{ title }}</h2> <div class="module--spacing--small"></div> <div class="modal-contents"> <p>{{ detail }}</p> </div> <Button :disabled="false" msg="ボタン" @push="click" /> </div> </div> </transition> </template> <script> import Button from "./Button.vue"; export default { props: { title: { type: String, required: true, }, detail: { type: String, required: true, }, }, components: { Button, }, emits: ["close", "modal-click"], setup(_, context) { const handleClose = () => { context.emit("close"); }; const click = () => { context.emit("modal-click"); }; return { handleClose, click }; }, }; </script> <style scoped> .overlay { background: rgba(0, 0, 0, 0.8); position: fixed; width: 100%; height: 100%; left: 0; top: 0; right: 0; bottom: 0; z-index: 900; transition: all 0.5s ease; } .panel { width: 40%; text-align: center; background: #fff; padding: 40px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .modal-contents { text-align: left; } .modal-enter, .modal-leave-active { opacity: 0; } .modal-enter .panel, .modal-leave-active .panel { top: -200px; } </style> まとめ 以上。 フォームコンポーネントを作りながらcompositionApiについて書かせていただきました。 ご参考になれば幸いです。 もし間違いなどがあれば、ご教授お願いします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JavaScript]forEachの使い方

JavaScriptでforEach()を使用したので使い方を簡単にまとめておく。 forEach()の使い方  forEach()は、配列の各要素に対して操作を行いたいときに使用する。forEach()のラムダ式を使った書き方は以下となる。 // arrayは配列 array.forEach((value, index, array) => { // 繰り返し処理 }); 上記のように、forEach()は配列に対する処理をコールバック関数で指定することができる。この時のコールバック関数は、以下の3つの引数を受け取ることができる。 ■ value : 配列の要素 ■ index : 配列のインデックス ■ array : 処理対象の配列 ここで、indexとarrayについては省略することができる。また、for文などと違いbreakやcontinueが使用することができない。 まとめ 今回forEach()の使い方について簡単にまとめてみた。 forEach()は、繰り返し処理をラムダ式で記述できるため、再利用性や拡張性を確保することができる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

禁断の速読術の習得方法!速読英単語のアプリを個人開発してリリースした話

禁断の速読術の習得方法 速読のアプリというのは、大体高額のセミナーに参加したり、速読をするための訓練が必要な事がほとんどだと思います。 そんな中無料でアプリをリリースしたので、皆さんに使って貰いたいので、記事を書く事にしました。 まずリリースしたアプリはこちら 現在は、google playストアにしかリリースしていません。 androidユーザーが登録してくれたら、iOS版版も出そうと思っています。 開発環境はMonacaです Monacaとは、「ハイブリッドアプリ」を作成することができるWEBサービスです。ネット上にアプリの開発環境を用意してくれるので、面倒な環境構築は一切必要ありません。 また、サンプルのアプリもいくつか用意されているので、簡単にアプリ開発を体験することができます。 私はここの最小限のアプリケーションを作るから、JQueryとFont Awesomeをダウンロードして www配下に設定してアプリケーションを作成しました。 ハイブリッドアプリとは「iOS」「Android」「WEB」のどのデバイスでも動作するWEBアプリのことです。「HTML5」「CSS」「JavaScript」の3つのWEBプログラミング言語を使ってアプリを作ります。 興味がある方は、Monacaおススメです。 使い方 使い方は簡単で、再生ボタンを押したら、速読が始まります。 実際に操作している動画はこちら 読み上げる間隔を変更する場合は、読み上げる間隔左ほど早くなりますのしたのバーを一番左にすると、 速読する速度が変わります。 速度を変更するには、バーを動かして、再生ボタンを押すことで、速度の調整が出来ます。 このバーがとても便利で、HTMLのタグ< input type="range" > と入力するだけで、速度などの値を変更することが出来ます。 詳細な使い方は、下記のようになります。 下に使い方のリンクも張っているので、気になる方は要チェックです。 <div> <input type="range" id="volume" name="volume" min="0" max="11"> <label for="volume">Volume</label> </div> 要望受け付けます 文字の色も変更出来ます。もっと色の種類が必要だという声があったら、追加しますので、こちらにコメントして下さい。 速読する文字の大きさも変更出来ます。 これは、文字の大きさのバーを右にする程大きくなります。 速読の再生をしている場合でも色の変更と文字の大きさの変更は出来ます。 色の種類は現在Black、Red、Blue、Greenの4種類があります。 文字の大きさを変えるコードの説明 文字の大きさは最大100PXまで大きく出来ます。 コード的には、スライダーが変更される毎にmain_nowのIDの文字サイズを変更しています。 <input type="range" id="text-slider" value="30" min="1" max="100" step="1" oninput="onTextSliderInput(this.value)"> <!-- 変更する文字の出力先 --> <div id="main_now" class="main_now"></div> function onTextSliderInput(size) { textSize = size; document.getElementById("text-slider").value = textSize; document.getElementById("main_now").style.fontSize = textSize + "px"; } HTMLの参考例 一番簡単な文字のサイズを変えるコードはこちら 下のHTMLをコピーして、HTMLファイルにすると実際に文字の大きさを変える事が出来ます。 <html> <head> </head> <body> <script> function onTextSliderInput(size) { textSize = size; document.getElementById("text-slider").value = textSize; document.getElementById("main_now").style.fontSize = textSize + "px"; } </script> <input type="range" id="text-slider" value="30" min="1" max="100" step="1" oninput="onTextSliderInput(this.value)"> <!-- 変更する文字の出力先 --> <div id="main_now" class="main_now">大きくするテキストの例</div> </body> </html> 使ってみて、こんな機能があったら良いなどの要望がありましたら、コメントしてくれると嬉しいです。 Androidユーザーは是非とも速読体験をして下さいね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

連想配列をひとつのオブジェクトに変換する

忘れがちコード。 今年2回調べた気がするので書くことで忘れないようにする(忘れないとは言っていない let modelArray = [ { key1 : "value1" }, { key2 : "value2" }, { key3 : "value3" } ] let modelObject = Object.assign( {}, ...modelArray); console.log(modelObject) // { key1 : "value1", key2 : "value2" , key3 : "value3" } なんだ、簡単じゃん!(忘却
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2021年やったこと振り返り | マネジメント と 停滞

はじめに 会社に入って1年が経ち、任される仕事量も大きく増えた1年でした。一方で、自分の中に積み上がったものという観点で考えると、少し停滞を感じています。 要件整理と設計 年明けから春頃までは大きなシステムの設計を進めていましたが、失敗で終わりました。今振り返ると「顧客の求めていることがわかってない」状態になのですが、まさか自分がその状態になるとは思っていませんでした。 顧客が本当に必要だったもの という有名な(?)画像がありますが、まさにあんな感じです。 まだないものを作る 元々私は食品の営業として働いていました。出来上がった商品を売るので、納品物に対して認識齟齬はあり得ませんでした。ところがシステム開発においては、まだ存在しないものを作成します。したがって、顧客と私たちの脳内イメージを一致させなければなりません。 ウォーターフォールにしろ、アジャイルにしろ、この「認識の一致」はとても難しいです。まず顧客の業務を理解しなければならないと改めて感じた1年でした。 業務要件の理解 業務要件を理解するためには、できれば顧客の業務全体を理解する必要があるように思います。全体の中でどのようにシステムが役立つのかを知らなければ、局所的なシステムを作成してしまう恐れがあるからです。加えて、顧客が話す業務の中にも正確でないものや、本来連携して然るべき業務同士の繋がりが網羅できてない場合もあります。 その業務を見たこともやったこともない状態で、いきなり話だけ聞いて「はい完全に理解しました」となるはずがないのです。 ではどうしたらいいかですが、例えば 業務アプリケーションであれば、実際に使用される現場を見たり、 to C のアプリケーションであれば段ボールでもいいのでモックアップにして、実際にロールプレイしたり XDなどデザインツールをもっと活用して、想像を膨らませる こういった時間が十分に(顧客と)取れると良いと思いました。 ちなみに現実で起きたことは、上記の逆です。 口頭でしか業務について聞く機会がない。しかもリモート。 すぐに作り始める。さっさとプロトタイプしたほうが話が早いと考える。 XDの使い方を学ぶより、MVPを実装したほうが早いと考える。 では、どうしてそうなってしまうのか 「システムとして考える」発想と能力が足りていないのでは...と仮説を立てています。 顧客の要望をシステムに落とし込み、まだ見えていない要件を見出すためには、やはり技術力が必要ではないかと思います。ここでいう技術力とは「要件を満たすためのシステムを想像する」能力であり、「ExpressでWebアプリケーションが書ける」などとは異なります。 加えて、この「システム」とは、デジタルの世界で作るアプリケーションだけではなく、それを使用する人々を含む全体的なまとまりを指します。このシステム全体を想像するための「技術力」が自分には不足していると考えています。 具体的には以下の2つがあるといいなと思っています。 1. アプリケーションのライフサイクル全般にわたっての運用経験(システムの一生を知る) 開発 テスト デプロイ 障害対応 スケーリング ... サービスの終了 受託開発の現場で働いていると、「開発〜リリース」までで止まってしまう案件が少なくないと感じています。つまり実際にユーザーが使用し、それをサポートするところまで想像が及ばないわけです。もちろん努力はしますが、どうしてもリリースが目標になってしまう現実があります。(納期もあるし) そもそも使われないもの、使いにくいものを作ってもしょうがないですし、運用する中で直面するであろう様々な困難を想像できないと、どうしても「その場しのぎのシステム」しか描けないかな、と思います。 2. 低レイヤーに関する知識(現状動いているシステムの理解に必要) OS ネットワーク データ構造 ファイルシステム ...etc これから作るアプリケーションがそれ単体で運用される状況は、よほどのイノベーションでない限り滅多にないのではと思います。多くは現状動いているシステムの一部を引き継いだり、 toCのアプリケーションであればユーザーの生活システムの一部を担当するでしょう。 したがって、「現状どうなっているのか」という前提を理解するための技術力として、所謂低レイヤーの知識が役立つのではないかと考えています。これはひょっとすると、情報系の学校出身の方には当てはまらないかもしれません。私個人は表層の部分だけを見て生きてきましたので、上記のような理解を今後は意識的にする必要があると思っています。 開発のその前に システム開発が仕事かと思いきや、その前段階にもっともっとやるべき仕事がある、と身にしみた1年だったかなと思います。しかも、2021年の前半からわかっていたはずなのに結局年末までその反省を活かせず仕舞いでした。正直いって得意ではないのですが、だからといって諦めていいものではないので、来年以降もこの戦いの継続を予想しつつ、少しづつ上手くやれるようになれればと思います。 チームマネジメントとプロジェクトマネジメント また、2021年はチーム開発をリードする、いわゆるPL的な役割を全うした一年でした。メンバーの方々が本当に素晴らしい方ばかりでしたので、なんとか助かった...という心境です。 一方でプロジェクトの管理に関しては、まだまだ課題山積です。 チームの皆様: やっていただく チームとの関わりに関しては、「お仕事をしていただく」意識を持って取り組んだ1年でした。 前職では指示出しするシーンが多かったです。自分でやったほうが早い。質も高い....。驕りです。なんでも自分でやって、見せて、覚えてもらう、というスタイルでした。その結果としては自分が潰れてしまったのですが。 そういう経験もあって、なるべく「指示しない」という方針で、2021年を過ごしました。ちなみに、10名行かないくらいの小さいチームの話です。 マネージャはメンバーに仕事を「やっていただいて」、その分け前をもらうようなものかなと思います。したがって私のすべきことは指示出しではなく環境づくりです。メンバーの方がなるべく自分で判断してトライできる環境こそ一番大事。 ...と、頭では思っていても実際は自分が割と動いてしまうシーンも多々ありました。自分で動けるときに動くのは全く悪い行動ではありませんが、無意識的にそういう状態になっている状況が多いことには問題意識を持っています。もっともこれは、チーム管理ではなくて自己管理の問題なのですが。 顧客の皆様: こちらでやる チームマネジメントと両輪で考えなければならないのが、プロジェクトマネジメントです。こちらは反対に、もっと自分が主導権を握ってコントロールすべきだったと感じています。 「まだないものを作る」という特性ゆえに、どうしても不確実な部分を孕んで進むのがシステム開発なのかなと、この1年で思うようになりました。重要なのはその不確実な部分を、当然のものとして自分ごとに捉える必要があります。 不確実な部分をいかに確実なものにしていけるか、そのためにはどれくらいのスケジュールが必要で、決まった内容はいつまで変更できるのか、...など決まっていないものを決めていくプロセスがなかなかうまくできない期間を過ごしました。 お客様あっての受託開発なので、どうしてもそういう不確実な部分は顧客側で決めていただく心境になりやすいです。しかしそれでは上手く進まないのではと考えるようになりました。 使用したフレームワークなど モバイルアプリ関連 React Native Recoil SWR メインの業務で使用。Recoilがついに案件でも採用できるようになり、構成として安定した感じがあります。Expoも大型のアップデートがあり、機能がさらに充実しています。Bare React Nativeで立ち上げて、必要なモジュールだけExpoから使用するケースが多いです。 AWS関連 サーバレスの構成について、学習しアプリケーションを作成しました。SAMのおかげでリソース管理がしやすいのでかなり敷居が下がったように思います。 SAM Lambda Cognito DynamoDB APIGateway 特にDynamoDB特有のテーブル設計については新しい概念として楽しく学べたと思っています。今後他の案件でもどんどん使用できると嬉しいです。 Rust関連 Bracket-lib Actix-web Rocket 相変わらずちまちま学習しています。ようやくなんとなく掴めてきた。(掴めてきたとは言ってない) stack overflowでの回答 回答者として多少活動してコード書きたい欲を発散していました。しかしながら、なかなかどうしてバシッと解決できたことは少ないですね。ましてやベストアンサーなど。 あと世界中にはありえないくらい初心者がいっぱいいることを実感しました。別にマウント取ってるわけではないのですが、自分以外にも悩む人がたくさんいるのだなあと思うと少し楽になります。 ランクが上がって、upvoteできるようになったのは嬉しかったです。 Github Copilot 今年一番の驚き、感動みたいなものを感じました。劇的に作業効率が上がったわけではないのですが、確実に手札は増えました。 ちゃんと変数名とか関数名とか名付けをできればタイピングしなくていい場面が増えるので、英語得意な人はどんどん使えると思います。 それと、Rustとの相性もすこぶるいいです。Rustは厳格なコンパイラが知られており、学習元のデータが綺麗です。元データが綺麗なので必然的にAIが生成するコードも品質が高いです。 AIがコードを書く 優秀なコンパイラによるチェック Push それをもとにAIがコードを... という正のサイクル。(違ったらすみません) まとめ やはり去年よりインプットが少ないなあという印象です。性格的に流されるままにお仕事をすることが多くなってしまうので、来年は自分のコントロールできる範囲をもう少し増やして新鮮な体験を増やしたいです。 過去の振り返り
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DOMについて

JavaScriptはWebページから情報を得て、何らかの処理を行い、Webページに結果の表示を返す。 また、Webページの状態変化やユーザーの操作によって何らかの処理を行う。 JavaScript単体だとこの処理をHTMLで表現できないのでブラウザのDOMという仕組み(実装)を使って実現させる。 DOMはHTML文書をノードとオブジェクトでDOMのメモリー内に表現する。 そうすることによってwebページとスクリプトやプログラミング言語を接続できるので、JavaScriptのプログラムがHTMLに適用される。 <body> | L<section> | L<p> オブジェクト  | L<p> オブジェクト | L<p> オブジェクト | L<ul> L<li> L<li> こういうHTMLの階層があったとして、sectionを基準ノードだとすると Bodyは親ノード pは子ノード ulは兄弟ノード という。 ノードの中にオブジェクトを持つ。 //WindowでDOMが読み込まれたら window.addEventListener('DOMContentLoaded', function(){ //ブラウザに読み込まれたページのid=“calc”の部分を選択 let elCalc = document.querySelector('#calc'); . . このようにDOMが読み込まれた時の処理をプログラムすることがある。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オンライン会議での発言率を表示する

はじめに 会議をしていても「いつも同じ人が喋ってるなぁ」と思うことありませんか? 話すべき役割の人であれば問題ありませんが,誰かの発言機会を奪ってしまっていたり,他の人が発言する気がないのなら改善する必要がありそうです.そんな時に,発言率のデータを取れる機能を持ったオンライン会議アプリを開発してみましょう! 本記事ではあくまでチームの状況把握を目的としているので,この機能を評価に使うのが本当に良いのか気をつけてください. 本題 こんな感じで会議での発言率(正確には発言秒数)を円グラフで表します. 今回の実装はGithubで確認できます. 概要 ビデオ通話機能には,Twilio を使います. 準備 1. Twilioアカウントを作成します. 2. ベースとなるコードをFork & Cloneする. 基本的なビデオ機能は既に実装したものを使いましょう. 3. 必要となる認証情報を準備する ACCOUNT SIDを取得する ACCOUNT_SIDは Consoleから取得できます. API KEYを作成する API_KEY_SIDとAPI_KEY_SECRETは API Keysから「Create API Key」を選択します. API KEYに分かりやすい名前をつけます.今回は「Speak Rate」にします. するとSIDとSecretが表示されるので,次項の.envへコピペします. .envに書き込む .env.templateがあるので,これを.envという名前に変更し,中身を以下のように書き換えます. .env TWILIO_ACCOUNT_SID=ACCOUNT_SIDをコピペする TWILIO_API_KEY_SID=API_KEY_SIDをコピペする TWILIO_API_KEY_SECRET=API_KEY_SECRETをコピペする 4. 依存関係をインストールする 今回はPythonを使うのでvenvを使って仮想環境を準備し,pip installします. $ python -m venv venv $ source /venv/bin/activate (windowsの人は $ source venv\Scripts\activate) (venv) $ pip install -r requirements.txt 5. 起動してみる (venv) $ FLASK_ENV=development flask run ブラウザでlocalhost:5000にアクセスすし,別タブからも同様に接続すると以下のような画面が表示されると思います.ビデオと音声の使用は許可してください. 実装していく ここからがこの記事のメインです.「ボタンを押すとその時点での会話率を円グラフで表示する」機能を作っていきます. 1. UI(ボタン・表示領域を用意する) まずは必要となるボタンと,円グラフの表示領域を作成します.(空のcanvas領域が表示されるので少し汚いですが,今回は許してください) templates/index.html <button id="join_leave">Join call</button> +<button id="toggle_audio" disabled>Mute Audio</button> +<button id="display_speak_rate_button" disabled>Display Speak Rate</button> </form> <p id="count"></p> + <div class="chart-container" style="position: relative; height:300px; width:300px"> + <canvas id="display_speak_rate" height="300" width="300"></canvas> + </div> 以下の画像のように「MuteAudioボタン」と「Display Speak Rateボタン」が追加され,「自分のカメラ映像との間に空白」ができていればOKです. 2. ミュート機能を実装する 「ボタンを押すとその時点での会話率を円グラフで表示する」機能を1人で試すためにはミュートを使って擬似的に話者を切り替える必要があるので,まずはミュート機能を実装します. 通話中にはミュートボタンを押せるようにし,退室すると押せないようにします.また,現在ミュート中の時にはボタンの表示を「UnMute Audio」に,現在マイクがオンの時には「Mute Audio」にします.(発言率表示ボタン(display_speak_rate_button)も同様なので一緒に記述します) static/app.js const connectButtonHandler = (event) => { ... 省略 ... connect(username) .then(() => { joinLeaveButton.innerText = "Leave call"; joinLeaveButton.disabled = false; + document.getElementById("toggle_audio").disabled = false; + document.getElementById("display_speak_rate_button").disabled = false; }) ... 省略... }; const disconnect = () => { ... 省略 ... document.getElementById("join_leave").setAttribute("innerHTML", "Join call"); + document .getElementById("toggle_audio") .setAttribute("innerHTML", "Mute Audio"); connected = false; + document.getElementById("toggle_audio").disabled = true; + document.getElementById("display_speak_rate_button").disabled = true; updateParticipantCount(); }; +const toggleAudioHandler = (event) => { + event.preventDefault(); + room.localParticipant.audioTracks.forEach((publication) => { + if (publication.track.isEnabled) { + publication.track.disable(); + document.getElementById("toggle_audio").innerHTML = "Unmute Audio"; + } else { + publication.track.enable(); + document.getElementById("toggle_audio").innerHTML = "Mute Audio"; + } + }); +}; +document.getElementById("toggle_audio").addEventListener("click", toggleAudioHandler); これで通話中に自由にミュートできるようになったはずです. 3. 話者の変更を検知する 話している人が変わったことを検知するにはTwilioのDominant Speaker Detection APIを使用します.なお,この機能は2人以上のグループルームでないと有効になりません. static/app.js const connect = (username) => ... 省略 ... -.then((data) => { - return Twilio.Video.connect(data.token); -}) +return Twilio.Video.connect(data.token, { + dominantSpeaker: true, + }); ... 省略... }); サーバー側で参加者と発言時間を保持します.発言者が変わったことを検知しているので,そのタイミングで1つ前の参加者の発言時間を計算・追加する形です. app.py +from datetime import date, datetime, timedelta +speakMap = dict() +lastDominantSpeakerChanged = None +lastSpeaker = None @app.route('/') def index(): return render_template('index.html') +@app.route('/speaks', methods=['POST']) +def saveSpeak(): + global lastDominantSpeakerChanged + global lastSpeaker + username = request.get_json(force=True).get('username') + if not username: + abort(400) + # lastSpeakerがいない = 初めての発言者なので,登録だけして終わる + if not lastSpeaker: + lastSpeaker = username + lastDominantSpeakerChanged = datetime.now() + return {'status': 'ok'} + + now = datetime.now() + duration = now - lastDominantSpeakerChanged + print(duration) + + if lastSpeaker in speakMap: + speakMap[lastSpeaker] += duration + else: + speakMap[lastSpeaker] = duration + lastDominantSpeakerChanged = now + lastSpeaker = username + return {'status': 'ok'} dominantSpeakerChangedイベントを検知して,POST /speaksへ次の発言者名を送信します. static/app.js const connect = (username) => ... 省略 ... .then((_room) => { room = _room; room.participants.forEach(participantConnected); room.on("participantConnected", participantConnected); room.on("participantDisconnected", participantDisconnected); + room.on("dominantSpeakerChanged", (participant) => + dominantSpeaker(participant) + ); connected = true; ... 省略... }); +const dominantSpeaker = (participant) => { + fetch("/speaks", { + method: "POST", + body: JSON.stringify({ + username: participant.identity, + }), + }).catch((err) => { + console.log(err); + reject(); + }); +}; 4. 発言率を表示します. やっと最後です.display_speak_rate_buttonを押すと発言率を表す円グラフを表示します.グラフ表示にはChart.jsを用います. app.py +from flask.json import JSONEncoder +# timedeltaをJSONで出力できるようにする +class CustomJSONEncoder(JSONEncoder): + def default(self, obj): + try: + if isinstance(obj, + timedelta): + return obj.seconds + iterable = iter(obj) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, obj) +app = Flask(__name__) +app.json_encoder = CustomJSONEncoder speakMap = dict() lastDominantSpeakerChanged = None lastSpeaker = None +@app.route('/speaks', methods=['GET']) +def getSpeaks(): + return {'speaks': speakMap} templates/index.html + <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.6.0/chart.min.js" + integrity="sha512-GMGzUEevhWh8Tc/njS0bDpwgxdCJLQBWG3Z2Ct+JGOpVnEmjvNx6ts4v6A2XJf1HOrtOsfhv3hBKpK9kE5z8AQ==" + crossorigin="anonymous" referrerpolicy="no-referrer"></script> static/app.js +const displaySpeakRateHandler = (event) => { + event.preventDefault(); + fetch("/speaks") + .then((res) => res.json()) + .then((data) => { + console.log(data.speaks); + + const ctx = document + .getElementById("display_speak_rate") + .getContext("2d"); + const myChart = new Chart(ctx, { + type: "pie", + data: { + labels: Object.keys(data.speaks), + datasets: [ + { + label: "発言時間(s)", + data: Object.values(data.speaks), + backgroundColor: [ + "rgba(255, 99, 132, 0.2)", + "rgba(54, 162, 235, 0.2)", + "rgba(255, 206, 86, 0.2)", + "rgba(75, 192, 192, 0.2)", + "rgba(153, 102, 255, 0.2)", + "rgba(255, 159, 64, 0.2)", + ], + borderColor: [ + "rgba(255,99,132,1)", + "rgba(54, 162, 235, 1)", + "rgba(255, 206, 86, 1)", + "rgba(75, 192, 192, 1)", + "rgba(153, 102, 255, 1)", + "rgba(255, 159, 64, 1)", + ], + }, + ], + }, + }); + }) + .catch((err) => { + console.log(err); + }); +}; +document.getElementById("display_speak_rate_button").addEventListener("click", displaySpeakRateHandler); ここまで実装すれば2つのタブでlocalhost:5000を開き,ルームに入った後は交互にmuteして適当に何か喋ります.そして「Display Speak Rate」ボタンを押せば以下のように円グラフが表示されるはずです. 最後に Twilioを用いることでビデオ通話機能や発言者の検出などをとても簡単に実装できました. 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【javascript】Storage

Storageブラウザの保存領域にデータを格納するためのオブジェクト 保存と取得 データは同期的に保存される。 localStorage.setItem('key', 'value'); //保存 const result = localStorage.getItem('key'); //取得 console.log(result) //json形式で保存 const obj = {"a": 1, "b":2}; const json = JSON.stringify(obj) localStorage.setItem('json', json) const json_result = localStorage.getItem('json') console.log(json_result) >>> {"a":1,"b":2} データの確認 ブラウザの検証ツールからapplication > localstrageでkey,valueで保存されている。 その他メソッドは、__proto__に格納されているので随時確認してみる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JSONとは

JSON 表記はjavascriptのオブジェクトに似ているが、JSONはオブジェクトではなく文字列になる。 JSONの変換メソッド JSON.parse JSON => Objectへ変換 JSON.stringify Object => JSONへ変換 const obj = {a: 0, b: 1, c: 2}; const json = JSON.stringify(obj) console.log(typeof json) >>> string const obj2 = JSON.parse(json) console.log(typeof obj2) >>> object ストレージなどにデータを保存する場合はJSON形式になるので変換メソッドはよく使う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Twitterに誰でもドット絵を投下できるサービスを作ったよ

どうも今年もやってきました クソアプリアドベントカレンダー 1の2日目を担当させていただきます @nabettu です。 今年で参加5年目らしい。まじかよ。 過去のクソアプリ達 作ったもの 今年作ったのはこちら! 100 Pixel Artというドット絵が書けるWebサービスです。 どんなサービスか 絵文字の ⬜ ? ? ? ? ? ? ? ⬛ の9色の四角のみを使ってドット絵が描けるサービスになります。 10かける10で100ドットのみ9色限定で、9^100パターンのみ作れるエディターになっております。 キャンバスサイズが100ドットのみで、使える色も9色しかないというポイントがクソアプリ的制限です! サービスの特徴 このサービスの特徴として「Finish」ボタンを押すと、編集が完了すると絵文字なのでそのままテキストとしてコピーできます。 つまり完成したらそのままTwitterでテキストとしてつぶやくことができるんです! 画像としてどこかに保存しなくていいし、そのままシェアできて便利ですね〜。。。 (あれ、、、思ったより全然クソアプリじゃないかも、、、) 技術的な話 React(ルーティングとかないけどNext.jsを使うだけ使っています) Cloudflare Pages Cloudflare Pagesめちゃめちゃ便利なのでみんな使うと良いと思います!Netlifyがアジアリージョンなくなって悲しんでいた方におすすめです。 苦労したポイント 表示はCanvasを使わずにDomでやっています。 ドットをクリックして絵文字が切り替わるのはonClickで設定するだけなのですが、ドラッグしてもちゃんと絵文字が反映されるようにするのがちょっと工夫が必要でした。 PCの場合:マウスでの動作について キャンバス上でクリックダウンされたらフラグをON マウスがクリックアップされたらフラグをOFF キャンバスからマウスカーソルが外に出たらOFF キャンバス上でフラグがONの状態でドラッグされていたら文字を変える という形で実装できました。 スマホの場合:タッチでの動作について 基本のタップはPCと同じ形にしておりますが、タッチしながらの移動(タッチムーブ)は同じ実装では動きません。 タッチムーブではイベントがタッチし始めのdomから発火してしまうので、 タッチムーブで別な要素への移動を検知 移動後の座標を確認 キャンバス全体からの相対座標で10*10の中のどこにいるかをチェック その座標の位置の文字を変える というちょっとした計算を追加して実装しました。 (・・・素のJSで作ってしまったのでライブラリ探してもよかったな!笑) Androidでは四角のカラー絵文字が無い これは困ったのでNoto Color Emojiを入れました。 ※ Apache 2.0ライセンス です。 以上です! クソアプリアドベントカレンダー2021 二日目、100 Pixel Artでした! 明日は@alclimbさん「2021年×おバカアプリ✨」なクソアプリをよろしくお願いします!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google検索と仲良くなる!プログラマー必見ググラビリティについて

どうも初めまして。エンジニアのあーみーです。 私自身教育を担当している立場という事もあって、新人コーダーや新人エンジニアさんの質問に答える機会が割と多いのですが、 新人さんが躓きやすいGoogle検索についての記事を書かせて頂きました。 是非最後までお読み頂けますと嬉しいです ? 目次 1.ググラビリティとは 2.検索の前にすること 3.検索してみる 4.プログラミング側の気持ちを掴む 5.さいごに 1. ググラビリティとは 俗に言う「検索力」です。 今回は目的のページに辿り着くというのもそうですが、Googleないしインターネットを用いたトラブルシューティングについて解説したいと思います。 2. 検索の前にすること まず大前提としてエラーが発生した際はすぐにGoogle検索を利用してはいけません。 基本的にエラー構文を読む事が大事です。 これをやっていないエンジニアさんが大半です。 ※今回は簡単な例題にしています。 また言語はjsを利用します。 const hogehoge = 1; hogehoge = 'text'; こんな感じの誰でも分かるレベルの不具合を書いてみましょう。 その際のエラーはこんな感じです。 Uncaught TypeError: Assignment to constant variable. at index.js:2 プログラマーだとしたら行数を見たらすぐに間違いに気づきますが、経験が少ない方だとそうはいきません。 まずはエラーの内容を読む事が大事です。 英語が苦手な人はGoogle翻訳やDeepLを使いましょう。 Uncaught TypeError:定数変数への割り当て。 index.js:2で 翻訳を掛けると嘘みたいに答えを教えてくれます。 これを読んで理解できなかったら基礎が出来てない証拠です。 その場合は振り返って学習しましょう。上記の例だと定数変数に割り当てをした結果エラーが発生したというのが原因でしょうか。 翻訳したら出てくる答えのパターンでも理解できない・躓いてる人は以下のパターンに該当すると思います。 答えが出ているのに気付かない。(読み込めてない) 答えの意味が理解できない。(基礎知識が低い) Google検索すれば出ると思い込んでる(思考停止) まずはエラー構文を読みましょう 3. 検索してみる エラー構文を読んでも理解が出来なかったら、そのままの文章で検索しましょう。大事な単語が欠けてたりするとGoogleは意図した言葉を出しません。(まれに出しますが) なのでUncaught SyntaxError: Invalid or unexpected token等のエラーが発生した場合はそのままの文言で検索しましょう。 4. プログラミング側の気持ちを掴む 大前提として〇〇を作りたいと思うエンジニアの思考とソースコードをただ実行しているプログラミング言語の思考は全然異なります。 恐らく「エラー読んでも分からない!」という人はここのコツが掴めていないのかなと思います。 説明してもアレなので例文を作りました。 実用的なソースを用いて説明します。 const requestApi = async () => { const res = fetch('/data.json'); const body = res.json(); console.log(body.user); } requestApi(); 特定のデータの中身をリクエストして、ログを出力するというものです。 このソースを実行すると下記のエラーが表示されます。 Uncaught (in promise) TypeError: res.json is not a function at requestApi (index.js:5) at index.js:9 先程挙げたエラー構文をそのまま読むとres.json is not a functionがヒントになってくれそうな気がしていますが、fetchAPIのドキュメントには戻りのオブジェクトに関数:jsonが存在しています。 じゃあ何故エラーがres.json is not a function(res.jsonは関数ではありません。)と表示されるのでしょうか。 考えられるエラー解決の思考の流れは下記のようになるのが最適解かなと思います。 res.jsonが関数ではない別の何かになっている resはそもそも何だろう? そもそもfetchの使い方は正しいのか? この流れでいけばどんな言葉で検索するのか分かりますか? そうです、「fetch javascript」で検索してみましょう。 出てきた最初のURLのmozillaのサイトに記載がありました。 ※以下引用 // POST メソッドの実装の例 async function postData(url = '', data = {}) { // 既定のオプションには * が付いています const response = await fetch(url, { method: 'POST', // *GET, POST, PUT, DELETE, etc. mode: 'cors', // no-cors, *cors, same-origin 使い方の例を見るとawait fetchとawaitするのが正しかったようです。 これも基礎が抜けていたという結論で終わるのですが、初心者の方自身は基礎が出来てるか否かはやってみないと分かりません。 その為の解決方法で上記のような思考回路が必要になってきます。 とどのつまり、エラー文章を読んでも参考にならないという人はこのような検索までに至る思考が形成されていないケースが大半かなと思います。 なのでエラー文そのままで検索しても答えに辿り付けない場合、エラーの結果だけ見るのではなくて、エラーに至った過程を想像してみましょう。 結果だけ見てその場しのぎの対応を繰り返すとあなたのコードがいとも簡単にクソコードと化します!!ご注意あれ! 5. さいごに 簡単に説明させて頂きましたが、何かありましたらご意見いただければ幸いです。 それではみなさんHappy Hacking!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】モジュール⑥ ダイナミックインポート

はじめに Udemyの【JS】ガチで学びたい人のためのJavaScriptメカニズムの講座の振り返りです。 前回の記事 目的 モジュールについての理解を深める 本題 1.ダイナミックインポート exportで宣言された変数や関数を非同期で読み込める 前提 moduleAの変数や関数をmoduleBで使用したい moduleA.js export let publicVal = 0; export function publicFn() { console.log('publicFn called') } export default 1; 例1 通常のモジュールの読み込みは以下の通り moduleB.js // importを使用した場合 import { publicFn, publicVal } from './moduleA.js'; // moduleAで定義した通りpublicFn called'と出力される publicFn(); 上記をダイナミックインポートにすると下記の通りとなる moduleB.js // importという関数を宣言する // 引数にはファイル名を記述し、Promiseが帰ってくるので、thenメソッドで繋げる // thenの中に関数を定義し、中身を確認する import('./moduleA.js').then(function (modules) { // publicFnやpublicValが入ったオブジェクトが以下のように返ってくる console.log(modules); // それを使って出力結果を表示する modules.publicFn(); // public calledと出力される }) // // Module {Symbol(Symbol.toStringTag): 'Module'} // default: (...) // publicFn: (...) // publicVal: (...) 例2 awaitとasyncを使用することも可能 moduleB.js async function fn() { // importの実行部分をawaitで受けて変数に格納 const modules = await import('./moduleA.js') // 使いたい関数を下記のように実行 modules.publicFn(); } fn(); 今日はここまで! 参考にさせて頂いた記事 【JS】ガチで学びたい人のためのJavaScriptメカニズム Let'sプログラミング JavaScript入門
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UXデザイナーとして4ヶ月で学んだこと

初めまして、NSSOLにてUXデザイナーをしている1年目の濱岡と申します。 今回縁あってQiita Advent Calendarに投稿させて頂くことになりました。 本記事の要件は以下になります。 目的 新卒配属からUXデザイナーとして、約4ヶ月で取り組んできたことと所感の共有 想定している読者 Qiita Advent Calendarの参加メンバー、Qiitaの閲覧者。あわよくばUXデザイナー/NSSOL/サービスデザイン部に興味がある就活生も。 読者の方々に伝えたいこと UXデザイナーがどんなことをしているのかを理解し、面白い!と思ってもらう。また、自分がこれまでの取り組みから得た学びを実践してもらう。 目次 1.自己紹介 2.11月までにやったこと 3.これまでに得た学び 4.おわりに それでは記事の内容に入ります。 1. 自己紹介 まずお主誰じゃ、というツッコミが入るかと思うのでサラッと自己紹介します。自分は現在NSSOLのDXIC(Digital Transformation & Innovation Center)という事業部の、サービスデザイン部に所属しています(新卒1年目)。趣味は旅行、食べログランキング上位のお店巡り、読書、サウナ、韓ドラの視聴です。大学では材料科学の分野を専攻していました。入社時のITスキルはというと、Progateを触ったり基本情報技術者試験の勉強をしたぐらい。デザインの事もノンデザイナーズ・デザインブックを、興味本位で読んだことがあるぐらいのまっさらな状態でした。 じゃあ何でUXデザイナーを志望したかという話ですが、理由は2つあります。 1つ目は、好奇心が強く色々なことをしてみたいと思ったからです。かなりのミーハーなので、新しいもの・面白そうなものがあったら飛びついてしまうんですね。SI業界を選んだのもこれに理由が近いです。 2つ目は、人の体験に関わるような事をしてみたかったから。これまでの人生を振り返ると、自分の人生を豊かにしてきたのは「モノ」よりも「体験」だと感じていました。外車に乗るとか、良い時計を付けたいなどの野心的な物欲があまりなく、それよりも旅行やスポーツなどを通じ、自分の記憶に残るような体験を毎日したいと考えていました。それが転じて、自分のみならず人の体験を豊かにできるような仕事をしたいと思うようになったのだと思います。 2. 11月までにやったこと 次に自分が4月から現在(11月末)までにやってきたことをざっくり紹介します。 4~6月:全体研修(ビジネススキル、WF開発の進め方、Java・基盤系の基礎) 7月上旬〜7月中旬:デザイン&アジャイル研修(デザイン思考とアジャイルのマインドを身につける) 7月下旬〜9月上旬:サービスデザイン部に本配属され、社内サービスをスクラムで開発していく模擬PJに参加。UXデザインで具体的にやったことは ユーザインタビュー ペルソナの作成 OOUI(オブジェクト指向UI)の概念の学習 Material Designの読み込み スタイルガイドの作成 Adobe XDを使って画面をデザイン 9月中旬〜9月末:内定者向けの事業部紹介資料をリデザイン 資料の用途や目的、ターゲット及びターゲットに起こして欲しいアクションの整理 DXICの所長や、各部署の部長にインタビュー(緊張しました!) スタイルガイドの作成 インタビュー内容を元に、各スライドの中身をメッセージが強調されるよう修正 10月〜11月末(現在):模擬PJに復帰 Reactを用いて実装(HTML/CSS/JavaScript) 開発しているサービスのブランディング 途中で色彩検定/基本情報技術者試験/応用情報技術者試験/認定スクラムマスター研修などの資格も取得したり、10月末にDesignshipというイベントに参加したりもしましたが、やってきたことは大体こんな感じです。 3. これまでに得た学び さて、ここからの内容は自分がこれまでに得た学びになります。 チームワークがめちゃ大事 これは自分がスクラムチームに参加してから強く感じ、かなり衝撃でした。なにせ自分の中ではデザイン/エンジニア/リモートワークという言葉からは、黙々と個人作業を進めていくイメージが想起されていたからです。ところが現在行っているスクラム開発では、スクラム/アジャイル的にこの考え方・振る舞いはどうか?チームとして問題解決に取り組めているか?と、チームやメンバーの動きに関する議論が非常に多く交わされています。 スクラムやアジャイルに関するドキュメントは下記を参照。 就活時に「チームで働くのが好きな人はSIerに向いている」と聞いていましたが、こういうことなのかなと実感しています。実案件でスクラムに取り組んでいる先輩方の話を聞いていても、技術的な問題よりもスクラムとして成立しているかどうかがPJの成否を分けると耳にするので、スクラムやアジャイルの価値観を大事にしてチームとして動いていこうと思います。 使いやすいものの裏にはデザインの営みがある Googleの公開している、「Material Design」をご存知でしょうか?これはGoogleが提唱しているデザインのガイドラインのことで、「見やすく、直感的に操作できるWebページ・サービス」を作ることを目的としています。ユーザビリティの高いアプリやサイトは、このMaterial Design、ないしはAppleの公開している「Human Interface Guidelines」に準拠して作られています。 世の中の使いやすいWebサイトはこれらのガイドラインに沿って作られているという事を知り、自分が画面のデザインをする際にはMaterial Designを辞書の如く参照するようになりました。使いやすさの裏にはこのような共通した法則というか、ガイドラインがあるのですね。 皆が惹かれるモノの裏にもデザインの営みがある これはDesignShipでマツダのデザイナー、前田 育男さんがお話されていた事になります。前田さんは車のデザインをアートに昇華すべく、美しいクルマづくりに挑戦されています。マツダの車といえば高いデザイン性が特徴として挙げられますが、その裏にはこんな取り組みがありました。 マツダでは車を、ヒトと心を通わせ一体となれるような生き物のようにしたいと考えていました。そこで、「クルマに命を吹き込む」ために野生動物の躍動感ある写真のスケッチを行い、それを抽象化した立体である「御神体」を1年がかりで作成。そしてそれを作成してからそれをクルマのフォルムに落とし込む段階では、全てをCADのようなデジタルツールで行うのではなく、必ずクレイ(粘土)モデルを作り、感触を確かめながら手で削って形を作っていくそう。こうした取り組みから生まれた車体には独特のフォルムに絶妙な陰影が付いており、その美しさは世界的に高い評価を受けるようになりました。 1台のクルマが生まれるまでにここまで徹底したデザインのプロセスがあることに感服し、世の中のモノを見る視点が変わりました。美しいと感じるもの、何故か心惹かれるものの裏にはデザインという仕掛けがある。自分もデザインというプロセスを通じて、そういうものを作っていきたいですね。 デザインと実装の二刀流がシナジーを生む えっ、デザイナーってプログラミングするの...?と思われた方もいらっしゃるかと思います(自分もそうでした)。スクラムチームでは、スクラムマスター/プロダクトオーナー/開発者という3つの役割があるのですが、「開発者」にはエンジニアもデザイナーも関係なくひとくくりにして含まれています。機能横断的なチームがあるべき姿なので、自分はデザインしかやらない!基盤しかやらない!なんてことはありません。なので自分も自ずと実装をするようになりました。 ちなみにスクラムに限らず、今後はデザイナーとエンジニアの境目がどんどん曖昧になると言われています(下記の記事を参照)。 とはいえ自分はデザインも初心者だったので、まずはデザインを学ぼうということで9月まではAdobe XDを用いて画面を描いていましたが、実装はほとんどしていませんでした。先輩から「実装をした方がUIも描けるようになる」と言われていましたが、その意味をこの時点では分かっていませんでした。しかし10、11月と実装をしてみて、デザインと実装を経験する以下のメリットに気づきました。 挙動をイメージできるようになる 当たり前ですが、デザインツールを用いて画面を描く際は画面は動きません(画面遷移などをプレビューできるプロトタイプ機能ぐらいはありますが)。しかし実際に作るものは「動くソフトウェア」なので、画面の各要素の挙動を想定してデザインをする必要があります。実装を経験したことで、必須項目が入力されていない時のエラーメッセージはどのように表示されるか、画面幅を縮めた際にはレイアウトがどう変わるか、文章が長い場合は「...」で表示するかなどを想定してデザインするようになりました。 divが見える これは実装をしてみてのアハ体験でした。実装の際には各セクションをdiv(ないしは他のHTMLタグ)で区切って要素を配置しますが、その作業を一度経験すると画面上のdivの区切りが見えてくるようになります。例えば左下の図を見ただけでも、脳内には右下のようにdivがうっすらと浮かびます。 divを意識するようになったことで、デザインをする際に親要素のdiv幅内に子要素を配置するなど実装上無理のないUIをデザインできるようになりました。 ユーザ目線でのレビューができる 前述のMaterial Designを一読した上で自分で一度デザインをすると、要素同士の間隔は4or8の倍数になっているか/ユーザが入力する箇所のフォントサイズは16pxで指定されているか、などCSSの指定をかなり細かくチェックするようになりました。実装だけをしていると実際に書かれたコードを見て、コードの可読性や処理の書き方に注目するあまりユーザ目線が抜けがちですが、ユーザにとって価値があるのは「動くソフトウェア」なのでこの視点は忘れないようにしたいです。 複雑性を担う覚悟が生まれる UXデザインをする上で意識すべき1つの法則として、「テスラーの法則」というものがあります。これは、「どんなシステムであってもそれ以上シンプルにすることのできない固有の複雑さがあり、その複雑さはユーザか開発側のどちらかが担わなければならない」というものです。 例えばメールアプリでは、差出人と宛先の2つの情報が必要になり、これらはいずれも必要不可欠な複雑性になります。この複雑性をユーザに担わせないために、最近のメールアプリでは差出人(自分)を予め入力しておくこと、また過去のメールや連絡帳から宛先を予想し候補を表示してくれます。つまり、差出人と宛先の入力という複雑性を、ユーザに代わって引き受けるようなメールアプリをデザイン・実装することで、メールを書くことを楽にしてくれているのです。 このようなことを開発側として実際にやろうとなると、なかなか面倒に感じました。ユーザにとってあったら嬉しいけど必須じゃないし、の割に実装やデザインがややこしかったりするし...イチ開発者としては、できればやりたくない!と思うこともありました。デザイナーもエンジニアも、自分たちで複雑性を担うのってかなりの覚悟が必要なのだなと。 なのでGoogleやAppleの提供するようなシンプルでUI/UXの優れたサービスは、エンジニアとデザイナーがユーザの代わりに複雑性を担ってくれている賜物で、サービス提供側の本気度の表れなんだと尊敬の念を抱くようになりました。と同時に、デザイナーがエンジニアに対して何でこのデザインにするのか、そのこだわりについて語れなければUI/UXの優れたサービスは実現できないと感じました。開発者に納得してもらえるよう、誰よりもユーザを中心に考えこだわっていきたいです。 予防線を張らない これはDesignshipで、HI(NY) design というデザイン会社の渡邊デルーカ 瞳さんがお話されていた内容になります。 渡邉さんは過去にNYのSVAという美大に留学した際に、アメリカの学生よりも日本の学生の方がアウトプットの質は高いが、ストーリーテリングが上手いのはアメリカの方だと感じたそうです。正直大した作品ではないのに、自信満々に自分の作品をプレゼンする。そんなアメリカの学生を見て、「予防線を張るような発言をしてしまうと作品の良さが半減する。批判されるのは当たり前と思って、その作品の良さを最大限伝えよう!」と考えるようになったとお話されていました。 自分も「あんまり自信がないですが」「ちょっと初めてだったので」のように、成果物を見せる際に予防線を張ってしまう事は多々ありました。謙遜の意味合いの場合もありますが、自信があるからこそ批判されて傷つきたくないがために予防線を張ってしまう事がほとんどだったので、このお話を聞いてハッとしました。作品の価値を落とさないために、予防線は極力貼らないように気を付けています。 4. おわりに 4ヶ月で多くの学びを得ましたが、スキル的にはまだまだ見習いの域を出ません。本職のデザインの力を伸ばすにもまずは実装の基礎体力をつけるべく、年内はReactの勉強に注力します(今は下記の本で勉強中)。 モダンJavaScriptの基本から始める React実践の教科書 そんなにロジカルな人間ではないので実装に向いてるとは思いませんが、新しい知識を習得したときや、あれこれ試行錯誤して動くソフトウェアができた時の喜びを大切にしていきたいです。年明けごろにはデザインの勉強にシフトして、Google UX Design Certificateの習得を始めたいですね。 最後になりますが、まさか自分がこんな分野の事を勉強しているとは学生の頃には想像がつきませんでした(DesignshipもReactも、名前すら聞いたことがありませんでした)。 自分の好きなUXデザイナーの安藤 剛さんが、以前Youtubeで「技術的なキャッチアップは組織に所属していなくても可能だが、文化的なキャッチアップは組織に所属していないと難しい」とお話されていましたが、まさしくそれを実感しています(動画は下記リンク)。 今の時代はネットや本から技術は学べるものの、アジャイルやデザインの文化・トレンドを学べるのは組織に所属している人の特権ですよね。自分は組織に所属しているメリットを最大限に活かし、技術と文化をこれからもアップデートしていきたいと思います! ここまで読んで頂き、ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最もLGTMを獲得したのはあの記事!プロトアウトスタジオ2021年のアウトプットを振り返る

今年も始まりました!アドベントカレンダー! この記事はプロトアウトスタジオのカレンダー | Advent Calendar 2021 - Qiitaの1日目の記事です。 みなさんこんにちは。プロトアウトスタジオ講師の光岡(@mitsuoka0423)です。 今年も残すところ1ヶ月ですね。今年はどのような1年でしたでしょうか?? 毎年恒例のアドベントカレンダーもスタートしました。プロトアウトスタジオも毎日投稿していきますので、ぜひご覧ください。 プロトアウトスタジオのQiita記事が1000件に到達しました! 2021年もプロトアウトスタジオ学生によるたくさんのアウトプットが行われ、Qiita記事の投稿数が1000件に到達しました!祝 (10000LGTMも目の前ですね!) 本記事では、Qiita APIを利用して、2021年に作成されたQiita記事の中からLGTMを多く獲得した記事をピックアップして紹介していこうと思います。 2021年最もLGTMを獲得したのは... 1位: 227LGTM 疑似彼氏、作りました。 @mnana 1位は227LGTMを獲得した「疑似彼氏、作りました。」でした!888888888 "彼氏を作ろう"という見出しから始まり、"タケシ(仮)"というポケモンのジムリーダーを連想させる名前の彼氏が出来上がる様子が見事に描かれています。 またネタ記事と見せかけて、いろんな返信パターンを持っていたりや天気APIのデータから傘が必要か教えてくれるなど、細かいところまで実装されている点も評価につながったのではないでしょうか。 2位: 142LGTM たかしくんに時速10kmで日本全国を最短距離で走らせました @canonno 2位は142LGTMで「たかしくんに時速10kmで日本全国を最短距離で走らせました」でした。 数年後の中学校くらいの教科書に載ってきそうな内容ですね。 こちらもネタ記事と見せかけて、離散最適化問題を解くアルゴリズムが実装されています。 たかしくんは18日と4時間で日本を全国を走り切ることができるそうです。以外と早いですね。 3位: 138LGTM 「ひらがな化API」を使って、給食メニュー変換Botを作ったら、わが子に大好評だった話 @kokano23 3位は138LGTMで「「ひらがな化API」を使って、給食メニュー変換Botを作ったら、わが子に大好評だった話」でした。 ひらがなが読めるようになったお子さんに向けてLINE Botを作成するファミリーテックな記事です。 リッチメニューにポケモンのイラストを使うなど、お子さんが使いたくなる工夫も施されています。 また、@kokano23さんはのびすけラジオにも登場されているので、興味がある方はぜひお聞きください。 事務職勤めのワーママがテクノロジーを身に付けたら仕事でもプライベートでも驚きの変化が #身の丈DX #事務職DX 4位〜10位 4位〜10位は以下の記事でした。 4位: 90LGTM @n0bisuke 【初見にオススメ】Raspberry Pi PicoをブラウザだけでLチカする入門 (Web Serial API) 5位: 90LGTM @shoito66 AIは増えすぎたガンダムを見分けることができるのか?Teachable Machineで作った分類モデルで検証してみた 6位: 68LGTM @canonno 強化学習で酔っ払いの挙動を見る 7位: 54LGTM @naokiuc 顔からラグビー部かサッカー部かを判断する 8位: 43LGTM @tkyko13 YouTubeの動画をQiita記事に埋め込んでみた 9位: 40LGTM @mitsuoka0423 【ハンズオン】写真を送るとAIが分析してくれるLINE Botを1時間で作ってみよう 10位: 37LGTM @chihirokubota これで若い子とのカラオケも安心?!COBOLしか経験なし!文系出身SE(23歳女)が若者の音楽を伝授します! 11位以降の記事はこちらから確認できます。→プロトアウトスタジオ2021アウトプット集計 Qiita APIを使って記事を取得する 以降は技術な解説です。長いので流し読み推奨です。 さっと読んで、他のアドベントカレンダーの記事を見にいきましょう。 Qiita API v2を利用してデータを取得しています。 取得対象 取得対象は以下のとおりとします。 ①プロトアウトスタジオOrganizationに属しているメンバー ②2021/01/01〜2021/12/01の期間に作成された記事 ②の条件は、検索クエリでcreated:>2021-01-01を指定すれば良さそうです。 しかし、検索時に利用できるオプション - Qiita:Supportを見てみても、①を絞り込む方法は今のところ公式に提供されていないようです。 プロトアウトスタジオOrganizationを眺めていると、メンバーアイコンのリンクからQiitaのユーザーIDが取得できそうです。 今回は、Organizationに属する全メンバーのユーザーIDを取得して、それぞれのユーザーが投稿した記事の情報からLGTM数を取得していきます。 DevToolsを使って、OrganizationメンバーのユーザーIDを取得する 今回は手軽に、DevToolsを活用してユーザーIDを取得します。 まずは取得したい要素を特定できる属性を調べます。 メンバーアイコンは、class="op-Members_member"を持つliタグの要素として実装されているようです。 ユーザーIDは、liタグの子要素のaタグのhref属性に含まれているので、それを取得していきます。 DevToolsのコンソールタブで、以下のコードを実行します。 Array.from(document.getElementsByClassName('op-Members_member')).forEach(element => console.log(element.children[0].href.split('/')[3])); これで、プロトアウトスタジオメンバーのユーザーIDを取得できました。 Qiita APIを叩くプログラムを書く axiosを使ってQiita APIを叩くプログラムを書いていきます。 クリックしてプログラム全量を表示 'use strict'; const axios = require('axios'); const main = async () => { const users = [ 'cog1t0', 'n0bisuke', 'tseigo', 'tkyko13', 'mitsuoka0423', 'ukkz', 'shima-07', 'takeaship', 'UhRhythm', 'RatchoTetsugaku', 'kmaepu', 'Toshiki0324', 'banboo', 'sksk_go', 'MikH', 'suo-takefumi', '3yaka4', 'tatsuya1970', 'zerozeronineking', 'grayhamchan', 'iizuka2019', 'karu', 'mihoko-funatsu', 'taichi0128', 'dashgo5go', 'doikatsuyuki', 'takeatakea', '13sayu', 'marumaruchan', 'harach19k', 'kyanchi', 'khoahv', 'Teru_3', 'hiromae0213', 'cazzz', 'Naru0607', 'kaiser355', 'canonno', 'twtjudy1128', 'PmanRabbit', 'heihei15408697', 'Sugizo50073508', 'm3do', 'sawakoshi_yy', 'misa_m', 'ranchi1977', 'shoito66', 'kkyosuke17', 'kokano23', 'ShinsukeSutou', 'yuta-proto-biz', 'boriko', 'okinakamasayoshi', 'dsvvpxgcseldie', 'aya2648', 'Ziyasumin01', 'Junno', 'yudiramaruyama', 'kensyu20210726', 'komona', 'tanaka_LV5', 'NagaharaHitomi', 'tanakahiroki', 'naokiuc', 'Izumi0711', 'watanabe-tsubasa', 'TAKA_xedge', 'kk_puruzera', 'yui-kouy', 'mnana', 'ikumi623', 'chihirokubota', 'tishiyama', ]; for (const user of users) { const response = await axios.get( `https://qiita.com/api/v2/items?query=created:>2021-01-01+user:${user}&per_page=100`, { headers: { Authorization: 'Bearer [アクセストークン]', }, } ); response.data.forEach((content) => console.log( `${content.created_at},${content.user.id},${content.title},${content.likes_count},${response.data.length}` ) ); } }; main(); (オプション)StackBlitzを使って実行する 今回、オンラインIDEであるStackBlitzを利用して実行してみました。(ローカルで実行しても全然問題ありません。) 無料プランでもコードを書くだけでなく、ターミナルでコードを実行できました。 Pricingを見ても、現時点(2021/12/01)では実行時間の上限もなさそうです。 (これはgitpodの代替サービスとなりうるかも。引き続き調査します。) 結果は以下のように出力されます。 CSV形式で出力されるので、Googleスプレッドシートに読み込んでLGTM数でソートしてランキングを作成しました。 まとめ 2021年もたくさんのアイデアが形になり、Qiita記事として投稿されました。 疑似彼氏のような自分が作りたいから作ったというタイプのアウトプットや、ひらがな変換Botのように誰かのために作ったタイプのアウトプットのどちらもQiita上でLGTMを集められているというのは面白いですね。 残り1ヶ月で、疑似彼氏を超えるLGTMを獲得する記事は生まれてくるのでしょうか...? 12/25までアドベントカレンダーは続きますので、今後の記事もぜひご覧ください! 明日の担当は @suo-takefumi さんです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

three.js を使って簡単な 3D ルーレットを作った

概要 ブラウザ上で JavaScript を用いて 3D 表現が出来る、three.js というライブラリがあります。今回は、こちらの three.js を用いて簡単なアプリを作ってみた、という話をしようと思います。特に大きなものを作ったわけではないので、実際の仕様やコードについても説明を加えながら進めたいと思います。 参考: threejs.org (今回は NSSOL Advent Calendar 2021 に参加するということで、新規開発して記事まで書くぞと意気込んでおりましたが、気づけば投稿日まで短いということで、過去に作ったものの紹介をさせていただきます。) リンク まず、作ったものの紹介です。GitHub Pages で公開しております。 ↓実際のアプリ↓ ↓リポジトリ↓ 3D表現のイメージ アプリ概要とユースケース 簡単に言えば、ルーレットです。 幾つかの選択肢があるものからある1つを、(ほぼ)ランダムで選びたいときに使えます。 想定する具体的なユースケースとしては、「お昼にランチに行きたいけれど、どこの店に行こうか悩んでしまったとき」に使えます! あらかじめ、このアプリには、NSSOL シス研の所在するみなとみらいにあるお店が3個登録されております。プラスで選択肢の中に入れたい店があればそれを入力して、ルーレットを回します。そして、止まったお店でランチします。特に「これが食べたい!」という強い気持ちがない場合に使えるアプリとなっています。 (とはいえ、そもそもシス研に所属している多くの方が迷うことなく「陳麻婆豆腐」に行くので、あまり必要のないアプリですが……) ですので、このアプリのタイトルを「ランチの候補がルーレット」と命名しております。ふむ、何やら意味深長な語感をしていますね。皆様は特に気にせず、適当にルーレットとお呼びください。 他の利用実績としては、何か会話をしなければならないときに「最近ハマってること」「悲しかった話」「大学生の時の話」のような話題を候補の中に突っ込んで回します。初対面の相手と会った時とかに使うと、話のネタになります。 参考: 【陳麻婆豆腐】 補足: 同じくみなとみらいの「陳建一麻婆豆腐店」も美味しいですよ。名前が非常に似ていますが別の店です。僕はどちらの麻婆豆腐も大好きです。 アイデンティティ とはいえ、「いや、このアプリって普通のルーレットじゃん!(関東弁)」とおっしゃる方と、「普通のルーレットやん!」とおっしゃる方が居ると思いますが、まさにもっともなご指摘です。今私が説明した内容だけでは、世にごまんとあるルーレットアプリと同じでしょう。というわけで、私のほうで一応それらとの差別化要素についても考えておりまして、それが以下の3つとなります。 3D なのでルーレットが回ってる途中もカメラで遊べる。 仕様として、陳麻婆豆腐に止まりやすい。 URL に候補情報が含められるため、ブックマークで候補を保存したり、他人と共有することが出来る。 レイアウトについて軽く紹介 はじめに、全体レイアウトについて紹介する。大きく分けて 4 つのコンポーネント(?)がある。 ルーレット 抽選リスト START/STOP ボタン リスト保存用 URL three.js を使った実装 それでは実装の説明に入りたいと思う。three.js を用いた JavaScript のコードを交えながら説明したいと思う。(が、正直調べれば何でも説明が出てくるぐらいなので説明はあまり無くても良いだろう。three.js は世界でも利用が多いライブラリで、非常に使いやすい。) 今回説明するのは、以下の内容だ。 three.js の導入 各オブジェクトの配置 URLについて three.js の導入 同じリポジトリに描画デモ用のページを作ってみた。これがルーレットを表示する機能以外を省いたものとなる。本体の方にも同じようなコードが書かれている。 myCanvas という id を持つ canvas を取得し、WebGLRenderer を作成。 レンダラーは画面に描画するためのオブジェクト ピクセル比、サイズ、影を有効にする、などの設定を行う シーンを作成。 シーンはカメラで撮影する世界のようなもの。そこにオブジェクトを配置していく。 カメラを作成。 視野角、アスペクト比、初期位置を設定。 カメラ操作をマウスで行うために、OrbitControls を作成。 マウス左でカメラ角度移動、右で場所移動、中でズーム。 タッチ 1 本で角度移動、2 本で場所移動&ズーム シーン上にオブジェクトを配置 光源、地面・壁、針、円盤 毎フレーム行う処理を書く 画面ロード時に以上の初期設定を行う設定をする。 // キャンバスの初期設定 function setThree() { // サイズを指定 const width = $("#myCanvas").parent().width() - 20; const height = 700; const ereaSize = 400; // レンダラーを作成 renderer = new THREE.WebGLRenderer({ canvas: document.querySelector('#myCanvas'), antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, height); renderer.shadowMap.enabled = true; // シーンを作成 scene = new THREE.Scene(); // カメラを作成 camera = new THREE.PerspectiveCamera(45, width / height); camera.position.set(0, ereaSize * 0.4, ereaSize * 0.5); camera.lookAt(ORIGIN); // カメラコントロールの設定 var controls = new THREE.OrbitControls(camera, renderer.domElement); // 光源を作成 addLight(scene, ereaSize); // 地面を作成 addFloor(scene, ereaSize); // 針を作成 addCone(scene, ereaSize); // サイズ3のルーレット作成 addCylinders(); // 毎フレーム実行 tick(); function tick() { renderer.render(scene, camera); requestAnimationFrame(tick); } } // 画面ロード時にsetThreeを実行する window.addEventListener('load', setThree); この辺りは1に少し詳細な説明がある。 各オブジェクトの配置 シーンにオブジェクトを配置していくことになるが、今回置いているものは4種類だ。 ライト、環境光 床・壁(水色のメッシュ) 針(コーン) ルーレット本体(円柱) ライト・環境光 PointLight と AmbientLight というものを使っている。どちらも必須だと思っている。点光源と環境光源というものを使っている。 点光源 ある位置から全方向に発せられる光である。 環境光源 世界全体を均一に照らすような光である。 参考情報は2に載っている。 // 照明の追加 function addLight(scene, ereaSize) { const spotLight = new THREE.PointLight(0xFFFFFF, 3.4, 6000, 2.0); spotLight.position.set(ereaSize * 0.2, ereaSize * 0.2, ereaSize * 0.2); spotLight.castShadow = true; // 影を落とす設定 scene.add(spotLight); // 環境光 const ambientLight = new THREE.AmbientLight(0xFFFFFF, 2); scene.add(ambientLight); } 例として、それぞれ片方だけのキャプチャを載せる。まず SpotLight のみ。 次にAmbientLightのみ。 そして両方。これがベストだと思う。点光源は立体感を出すために、環境光源は世界全体の明るさを底上げするために使える。 床と壁 ここでは表面からだとしっかり色づいて見えるが、反対からだと透明に見える素材を使っている。内方向からだけ見えるように内方向に表面を設定し、外からは見えるけど中からは外が見えない状況を作り出す。 // 床壁を追加 function addFloor(scene, ereaSize) { const floorGeometry = new THREE.PlaneGeometry(ereaSize, ereaSize); const floorMaterial = new THREE.MeshStandardMaterial({ color: 0x66BBDD, roughness: 0.03, metalness: 0.75 }); const floorXZ = new THREE.Mesh(floorGeometry, floorMaterial); floorXZ.rotation.x = -Math.PI / 2; floorXZ.position.set(0, -ereaSize / 2, 0); scene.add(floorXZ); // 同様の記述が、立方体の面を作るためで全部で6つ } こんな感じ。 針 床壁のところで説明しなかったが、Mesh には geometry と material という2つの要素が必要となる。geometry は形を決定付けるもので、material はその質感や色を決めるものだ。roughness は表面の粗さ、metalness は金属感を表すものだ。それぞれベストな質感になるように、数値をいじる必要がある。(0~1 の間の値) 今回は ConeBufferGeometry を使用して、円錐を作成している。 // ルーレットの針を追加 function addCone(scene) { var geometry = new THREE.ConeBufferGeometry(3, 40, 30); var material = new THREE.MeshStandardMaterial({ color: 0xffff00, roughness: 0.03, metalness: 0.6 }); var cone = new THREE.Mesh(geometry, material); cone.position.set(0, 65, -80); cone.rotation.set(-Math.PI * 1.2, 0, 0); scene.add(cone); } 実は絶妙に浮いている。 ルーレット本体 少しだけ複雑なので処理概要を説明する。 基本的には CylinderGeometry で円柱を追加している。 ルーレットは1つの円を色んな色で分けるため、色ごとにMeshを分けている。 全体の角度を等分し扇形の円柱になっている。 各 Mesh の初期位置を少しずつずらしている。 Material で色を決定する際に、ランダムで振り分けられた色を使用する。 色はhsl3を利用し、彩度と輝度を揃えて、色相だけを変化させる。 基本的に色相はランダムに、かつ隣り合う色は遠くなるように決定する。 それぞれの Mesh に対し、さらに数字の Mesh を乗せるように配置する。 TextGeometry を利用する。 ルーレット全体をグループ化することで回転しやすくする。 最大の 10 個を設置した場合だ。色は本当にランダムなので、めちゃくちゃダサい色になることもある。下の画像は比較的うまくいったときのものだ。 コード全体が大きいので折りたたみに ```js // 円盤作り function addCylinders() { // データ作り var lunch = ["陳麻婆豆腐", "陳麻婆豆腐", "陳麻婆豆腐"]; // 色のランダム生成 var color = []; var memo = Math.random() * 360; for (var j in lunch) { if (lunch.length > 2 && j == lunch.length - 1) { var avg = (color[0] + color[j - 1]) / 2; const h = Math.abs(color[0] - color[j - 1]) > 180 ? avg : avg + 180; color.push(h); } else { const h = (memo + 90 + Math.random() * 180) % 360; color.push(h); memo = h; } } scene.remove(rouletteGroup); rouletteGroup = new THREE.Group(); // ケーキの追加 for (var i in lunch) { (function(i, color, rouletteGroup) { // 円の追加 var han = 60; var rad = Math.PI * 2 * i / lunch.length + Math.PI * (1 + 2 / lunch.length); var geometryc = new THREE.CylinderGeometry( 80, 80, 30, 300, 10, false, -rad, Math.PI * 2 / lunch.length); var materialc = new THREE.MeshStandardMaterial({ color: `hsl(${color[i]}, 90%, 70%)`, roughness: 0.02, metalness: 0.7 }); materialc.side = THREE.DoubleSide; var circle = new THREE.Mesh(geometryc, materialc); circle.position.set(0, 30, 0); circle.receiveShadow = true; rouletteGroup.add(circle); // 数字の追加 var loader = new THREE.FontLoader(); loader.load('json/helvetiker_bold.typeface.json', function(font) { var text = Number(i) + 1; /* lunch[i].toString() */ var textGeometry = new THREE.TextGeometry(String(text), { font: font, size: 15, height: 10, curveSegments: 12 }); var materials = [ new THREE.MeshBasicMaterial({ color: 0xFFFFFF }), new THREE.MeshBasicMaterial({ color: 0xAAAAAA }) ]; var textMesh = new THREE.Mesh(textGeometry, materials); // 文字オブジェクトのサイズ取得と微調整 textMesh.geometry.computeBoundingBox(); var box = textMesh.geometry.boundingBox.clone(); var center = box.getCenter(new THREE.Vector3()); textMesh.position.set(-center.x, 0, center.z * 2); textMesh.rotation.set(-Math.PI * 0.5, 0, 0); textMesh.castShadow = true; // 文字オブジェクトのグループ作成 var rouletteGroupchild = new THREE.Group(); rouletteGroupchild.add(textMesh); // 文字オブジェクトの回転的位置 var txtrad = -(Math.PI * 2 * i / lunch.length + Math.PI * 1.5 + Math.PI * 1 / lunch.length); rouletteGroupchild.position.set(han * Math.cos(txtrad), 40, han * Math.sin(-txtrad)); // 文字オブジェクト自体の回転 var txtrad2 = -(Math.PI * 2 * i / lunch.length + Math.PI * 1 / lunch.length); rouletteGroupchild.rotation.set(0, txtrad2, 0); // 追加 rouletteGroup.add(rouletteGroupchild); }); })(i, color, rouletteGroup); } scene.add(rouletteGroup); } ``` 画面サイズ変更時処理 正直こう書いておけば良い! みたいな感じだ。このへんは4を参考にしている。 // 画面サイズ変更時の処理 function onResize() { // サイズを取得 const width = $("#myCanvas").parent().width() - 20; const height = $("#myCanvas").height(); // レンダラーのサイズを調整する renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, height); // カメラのアスペクト比を正す camera.aspect = width / height; camera.updateProjectionMatrix(); } // リサイズイベント発生時に実行 window.addEventListener('resize', onResize); ルーレットの回転 three.js の導入の章にあるとおり、tick() には毎フレーム行う処理が書かれている。 demo.html では特に表示しなかったが、本体ではルーレットの回転処理を行っている。 function tick() { renderer.render(scene, camera); if (gameStatus === 1) { speedCurve = Math.max(0, Math.min(speedCurve + speedCurveCurvePlus, speedPlus)); speed = Math.max(0, Math.min(speed + speedCurve, speedMax)); rouletteGroup.rotation.y = rouletteGroup.rotation.y + Math.PI * speed } else if (gameStatus === 2) { speedCurve = Math.max(speedPlus / 80, Math.min(speedCurve - speedCurveCurveMinus, speedPlus)); speed = Math.max(0, Math.min(speed - speedCurve, speedMax)); rouletteGroup.rotation.y = rouletteGroup.rotation.y + Math.PI * speed if (speed === 0) { gameEnd(); } } requestAnimationFrame(tick); } ここでは、60fps の環境ではそれなりにいい感じにルーレットが回るようになっている。ここで言う「いい感じ」というのは「最大速度は速いけど、STOP 押した後の減速もなかなか速い。でも止まりそうで止まらないし、あ~~一体どこに止まるんだろう!?!?」という演出が出来る状態のことを言う。 ルーレット回転に関わる変数はこんな感じである。(自分でも久しぶりに見るので詳細は覚えていない。) rouletteGroupというのは、ルーレット全体をグループ化したもので、これの rotation を変化させることでルーレットが回転する。この rotation を変化させるために、speed や speedMax などの数値を設定している。まあいい感じになっていると思っているので、一度試してみてほしい。ちょっと焦らされるはず。(もちろん手っ取り早く決めて欲しい人からしたら、いい感じではない。) var rouletteGroup; // ゲームの状態 0=待機状態 1=加速状態 2=減速状態 var gameStatus = 0; // ルーレットの速度 var speed = 0; // ルーレットの最高速度 var speedMax = 0.196; // ルーレットの加速度 var speedPlus = 0.001; // ルーレットの加速度の微分 var speedCurve = 0; // ルーレットの加速度の微分の加速時の微分 var speedCurveCurvePlus = 0.00003; // ルーレットの加速度の微分の減速時の微分 var speedCurveCurveMinus = 0.0000027; URLについて 一応説明すべきことは終わったが一つだけ補足。 ver1.1 の追加機能である URL 機能についてだ。これのやりたいことは「URLのクエリパラメータを読むことでリストを再現出来る。」というものだ。これを実現するには、以下のようなことが必要だ。 クエリパラメータを入力として候補を復元出来る。 候補の状況を監視する。変更時に URL を出力する。ついでに現在の URL も変える。 入力 クエリパラメータを取得して、パースして、要素を作ってぶちこんでいるだけだ。 出力 history.replaceState()を用いることで、現在のクエリパラメータを変更することが出来る。 基本的にURLを候補の要素から読み取り、クエリパラメータっぽくしてからURLボックスに書いたり、history.replaceState()を呼び出したりしているだけだ。 // URLボックスの設定、URLの書き換え function setUrlBox() { new_query = ""; for (c of document.getElementById("input_lunchbox").children) { new_query += encodeURIComponent(String($(c).find(".form-control")[0].value)) + ","; } new_query = new_query.substring(0, new_query.length - 1); document.getElementById("url_box").value = location.origin + location.pathname + "?list=" + new_query; new URL(location).searchParams.set("list", new_query); // URLの書き換え history.replaceState(null, document.title, location.pathname + "?list=" + new_query); } ちなみに、この setUrlBox() を初回ロード時に行っているため、アクセス後はすぐに↓のような URL に変更される。 https://yuichisemura.github.io/lunchNoCohoGaRoulette/?list=陳麻婆豆腐,McDonald,五右衛門 まとめ 今回は、three.js を用いた簡単なアプリの説明を行った。2 年ほど前に作ったアプリでしたが、今回記事を書くにあたって色々と機能追加やリファクタリングを行いました。他にも作りたいアプリがあるので、完成したら Qiita もセットで書こうと思います。今後ともよろしくおねがいします。では、失礼しま~~す(退室ボタン)。 今後の課題 スマホでの利用を考え、ルーレットのみをズームアップする機能を追加する。 飲み会などの利用可能性を考え、接待モードを追加する。 白背景と鮮やかな色たちで目がチカチカするため、ダークモードに対応する。(おもちゃ機能) 現在、リフレッシュレートが 60 の端末でベストなパフォーマンスになる設定をしているため、端末依存性を減らす。 three.js超入門 第1回 レンダリングまでの流れ ↩ Three.js ライト機能まとめ - ICS MEDIA ↩ 配色を考えるのが面倒ならhslを使おう ↩ Three.jsでの最適なリサイズ処理  ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのコールバック関数

コールバック関数 JavaScriptでは頻繁に使われるみたいです。  違う関数を呼び出してもらい、で引数として渡される関数です。 function threetimes(fn) { //3回関数をする処理 fn();  //引数の処理を実行 fn(); fn(); } threetimes(function () { console.log('コールバック関数'); }); 「コールバック関数」が3つdevtoolsのconsoleの中に表示されます。 書き方を変えてみましょう function threetimes(fn) { //3回関数をする処理 fn(); //引数の処理を実行 fn(); fn(); } const callfunction = function () { //constでcallfunction を定義 console.log('コールバック関数'); }; threetimes(callfunction); //コールバック関数で呼びだす      //threetimesの引数は関数 これでも同じように「コールバック関数」が3つdevtoolsのconsoleの中に表示されます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

非同期通信をするなら絶対にやったほうがいいこと

TL; DR ユーザーを待たせるとき、「どのように待たせるか」によって印象は結構変わります。 非同期処理などでユーザーを待たせるときは、適切なローディングを表示してUXを改善しましょう。 違いを体感していただけるよう、いくつかの例を元に書いてみました。 ボタン押下の例 送信ボタンを押してから、通信に1秒かかるお問い合わせフォームを作ってみました。 2つの例を比べてみて下さい。 実際には何も通信しないダミーのFORMなので、気軽に試して下さい。 Bad See the Pen ダミーFORM by laineus (@laineus) on CodePen. Good See the Pen ダミーFORM by laineus (@laineus) on CodePen. 改善されたこと1 ユーザーの操作に対し、画面が即時応答するようになりました。 結果: 体感速度が向上しました。 どちらの例も、目的が達成されるのは1秒後であるにも関わらず、 「押して即何かが起きる」というステップを踏むだけで、サクサク動いている印象を受けます。 ローディングが無い場合は、ボタン押下してから通信が完了するまで、 画面に何も変化がないため、フリーズしているような、もっさりした印象を受けます。 例えば、頑張ってこの1秒かかる通信を0.8秒くらいに高速化するより、体感できる差は大きいと思われます。 改善されたこと2 ユーザーは通信中であるということが分かるようになりました。 結果: 1秒という待ち時間に対する印象が良くなりました。 ローディング 印象 無し 「通信中か?」「まだかな?」「もしかしてちゃんと送信ボタン押せなかった?」「もう一回押すか」 有り 「ああ、通信中ね。待とう。」 やはり同じ秒数であっても、ユーザーが感じるストレスは大きく異なると思います。 (多重送信の防止もセットで見直しましょう) SPAにおける画面遷移の例 もう1つ、画面遷移における例も用意したので、これも試しみていただきたいです。 一覧画面と詳細画面を持つブログのようなサイトです。 一覧と詳細を行ったり来たりして、どちらが快適かどうか比べてみて下さい。 今回はどちらの例でもローディングが表示されます。 違いは、遷移元の画面と遷移先の画面のどちらでローディングを表示するかです。 詳細画面の通信にかかる時間は、どちらも同じ1秒です。 元画面でローディング See the Pen 画面遷移 by laineus (@laineus) on CodePen. 遷移先でローディング See the Pen 画面遷移 by laineus (@laineus) on CodePen. 比較 みなさんは、どちらが快適に感じたでしょうか? 僕的には、どちらかと言うと後者のほうがサクサク動く感じがします。 どちらも遅いのは間違いないですが、感覚的に言うと、 前者: 全体的に重い 後者: サイトのUIは快適だけど通信が遅い みたいな…? 何故か ユーザーが僕だとすると、 ユーザーの欲求は「記事の本文を見たい」ですが、 記事押下に対して画面に期待する動作は「ページが遷移する」です。 ユーザーは「記事を押すと詳細ページに遷移するだろう」と期待しているわけです。 後者の例では、画面がユーザーの期待に即時応えるため、 たとえそれが要求を満たさない不完全なものであっても、 サイトのUI自体は自分の操作にサクサク応じてくれる印象を受けます。 前者の例では、ローディングのバーは即時表示されるものの、それは期待動作ではありません。 期待動作が満たされるのは1秒後です。 → → → → 元画面でロード 記事押下 Loading... UI期待動作 & 要求達成 遷移先でロード 記事押下 UI期待動作 Loading... 要求達成 ちなみに前者の例は、Nuxtのデフォルトのローディング(?)を真似たものです。 Nuxt製のサイトって、SPAなのに操作がもっさりした印象を持っていたんですが、それってスピードの問題よりも、UIの問題が大きいんじゃないかな?と思ったり。 非SPAサイトにおけるブラウザの基本的な挙動も前者に近いですね。 Chromeならタブにローディングを表示しつつ、HTMLのレスポンスがあった段階で遷移します。 サーバーサイドやネットワークのスピードが、UIの操作感に直に悪影響を与えてしまいます。 (後者は後者で、画面が未完成のまま表示されるので、嫌がる人も居るかもしれません) いいねボタン等の例 いいねボタンのようなケースでも、いいねボタン押下時にローディングを表示して、通信完了後に、いいねボタンを押下済みデザインに変更すればよいでしょうか? いいねボタンのようなケースでは、わざわざ通信完了を待つ意味が薄いので、実際にPOSTが完了していなかろうと、即時いいねボタンを押下済みデザインに差し替えてしまうのがいいですね。 ローディングを表示しないほうがいい(=そもそも何も待たせる必要がない)例の一つでした。 おわりに 最後に、タイトルには目を引くべく絶対にやったほうがいいとは書いたものの、 全てのケースで例で紹介したような対策を入れるべきと言いたいわけではありません。 同じ時間ユーザーを待たせるとき、どのように待たせるかで大分印象変わりますよね、と伝えたかったのがメインですので、待機時間のUIに少しでも意識を向けてもらえればいいかなと思います。 そのうえで、やるかどうかや、どうやるかはケースに応じて変えていただければと思います。 もしUI/UXデザイナーが居て、ここらへんを相談できるのであれば、恵まれた環境と思いますが、 各画面の静的なデザインデータだけで開発しなければいけないケースも珍しくありませんので、 そういったとき、エンジニアからも拾い上げたり、問題提起できれば、より良いプロダクトに近づけるんじゃないかと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next2D NoCode Tool入門

NoCode Toolとは Webブラウザ上で動作するオーサリングツールで、インストール不要、会員登録も不要、アクセスすれば誰でも利用できるサービスです。 サイトはこちら:NoCode Tool 昨日の記事(Next2D入門)では、SWFファイルを利用した過去の資産の再利用について書きました。 ですが、NoCode Tool本来の機能はSWFを読み込むところではありません。 今日はどのよう機能があるかをご紹介させて頂ければと思います。 前半は前日の記事と重複していますので、スクロールしてもらえればと ワークスペース Tool Area 描画ツール、テキストツールや言語設定や書き出し設定など各種ツールと設定項目が設置されているエリアになります。 Screen Area 描画エリア、表示するDisplayObjectの配置や重ね順などを操作するエリア Timeline Area レイヤーやアニメーションのキーフレームなどを操作するエリア Controller Area 拡大縮小/回転/カラー操作/BlendMode/Filterなど、指定のDisplayObjectを操作するエリア 操作方法 英語表記しかないので、初見は「うっ・・・」っとなるかと思います。 (言語追加頑張ります!!) (モーダルの説明文だけは日本語対応してます) 今日は全体の簡単な操作方法とデータの保存・書き出しに関して記載できればと思います。 アクセスすると、自動で新規のタブが画面の左上に「Untitled-1」と追加されます。 +マークを押下すると新しいタブを追加でき、メニューボタンでタブの一覧が表示されます。 タブの名前がデータ保存と書き出しを行った時のファイル名になります。 外部データの読み込み Controller AreaのLibraryタブから外部データを読み込めます(直接ドロップも可能です。) 利用可能なフォーマットは以下となります。 - 画像データ(jpeg,png,gif,svg) - 音声データ(mp3) - 動画データ(mp4) - SWF プロジェクトデータの保存・読み込み Tool Areaの雲ボタンの下矢印がプロジェクトデータの保存で、上矢印が読み込みです。 データのフォーマットはタブの名前.n2dとして保管され、同じく.n2dデーターであればいつでもプロジェクトデータの読み込みが可能です。 書き出し 作り上げたプロジェクトはTool Areaのoutputボタンから書き出しが可能です。 書き出しは以下のフォーマットで書き出しが可能です。 - JSON(圧縮) - JSON(無圧縮) - webm - GIF(Loop Animation) - GIF Animation JSONデータはNext2D Playerで読み込む事が可能です。 データが重い場合は、圧縮を選択する事で軽量化されます。 CDNなどでgzip圧縮が可能な環境であれば無圧縮で書き出す事を推奨しています。 理由としてはNext2D Playerでの圧縮データの解凍オーバーヘッドを回避できるからです。 Next2D Playerでの読み込みサンプルコード STEP1 next2d.jsをHTMLに追加 (Next2D Playerはこちらダウンロードできます。) <script src="next2d.js"></script> STEP2 表示させたいエレメントにscriptタグを追加 <script type="text/javascript"> next2d.load("path/to/output.json"); </script> 書き出し設定 書き出すフォーマットの設定は、Tool Areaの歯車ボタンの設定から行えます。 Hidden LayersをNot Includingにした場合、非表示レイヤーを含めず書き出します。 逆にIncludeに設定した場合は、非表示レイヤーも含めて書き出します。 動作サンプル動画 この動画の中では、MP4、サウンド、画像、SWFを配置してます。 MP4に馴染むようにSWFと画像はBlendModeで加工し、MP4の角を丸くするのにマスク機能を利用しました。 マスクはロックをOffにすると無効になり対象のDisplayObjectを移動することでマスク位置を調整できます。 逆にOnにすると有効になり、マスクの中のDisplayObjectを移動することで位置を調整できます。 サウンドにはloop機能があり、BGMはloopして利用し、ジングルは1回の利用で停止するよう設定してます。 また、MP4のボリュームとloop再生もコントールが可能です。 このように複数の異なったフォーマットのデータを編集し、任意のデータに書き出す事が可能です。 明日からは機能を絞って紹介させていただければと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む