20210412のJavaScriptに関する記事は21件です。

webプログラミングで作る最初の題材は掲示板だよね、本気で作った結果

最終的にはレンタル掲示板になるよね 無料レンタル掲示板NIERU.NET http://nieru.net 掲示板サンプル http://nieru.net/sample/ PHP7.4 mysql 自分が利用してたサービスだからこそ作れるよね 携帯専用掲示板サイトを5年以上利用してそのサイトを10年以上運営して 掲示板サイト利用者からレンタル掲示板運営者になれて1つの夢が叶った感じ 最初は挫折するよね 2013年頃、PHPの勉強がてら掲示板を作ろうと努力したけど検索機能を実装する辺りで挫折した 当時は何がダメなのかも分からなかった フレームワークは便利らしいよね コードの書き方は全て自分ルールになっている 共同開発しないから問題はなし 何処かの会社で働くこともしないから問題なし 仕事を受注しないから問題なし ソースコードは見せられないよね if,swtich,includeのオンパレード 自分がわかれば問題ない 機能は沢山付けるよね いろんなレンタル掲示板サイトを参考にして取り込める機能は全て取り込んだ 一部の有料レンタル掲示板より機能が豊富になった バグはたくさん出るけど気付かないよね レンタル掲示板を開始して利用者が増えたおかげで見つかるバグがある バグを報告してくれる方もいるので、利用ユーザーと一緒に作り上げてる感が良いよね
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascript 備忘録5<関数>

■クラス定義 // ES6以降の書き方 class Person { // コンストラクタ constructor(name, age) { this.name = name; this.age = age; } // メソッドの定義 hello() { console.log('こんにちは'); } } // インスタンス化 // JSではオブジェクトとなることに注意 const me = new Person('Mike', 23); ■クラス継承 →上で定義したPersonクラスを継承して、newManクラスを作成する class newMan extends Person { constructor(name, age, gender) { // superによりPersonのクラスを継承する // 親のメソッドを先に初期化しないとエラーになる super(name, age); this.gender = gender; } hello() { console.log('こんにちは'); } // newManクラス特有のメソッドを定義する bye() { console.log('さようなら'); } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sveltekitに入門してみましたけれども

sveltekitに入門してみましたけれども 自分のサイトをsveltekitでつくってみました。 作成にあたって詰まった点などを書こうと思います。 sveltekitとは何か? まずsvelteを知らない人は、svelteを調べてください。sveltekitは、簡単に言えばルーティング機能がついたsvelteです。いい感じにページの移動みたいなことができます。僕のホームページでいえば、ナビゲーションにあるDiaryボタンを押せば、日記が表示される……みたいな感じですね。documentもあるので、ここ見れば大丈夫な気もします。 新規プロジェクトの作成 mkdir my-app cd my-app npm init svelte@next npm install npm run dev my-appのところは好きなフォルダ名で良いです。 フォルダつくって移動。 npm init svelte@nextで作成がはじまります。 Ok to proceed?は進めますか? なのでY Use TypeScriptはTypeScript使いますか? なので好きなほうで。 CSSとかSCSSとか使うかきかれるので好きなほうで Add ESLint for code linting?はESLintでlintしますか? みたいな感じなので、Yにしておきましょ。 Add Prettier for code formatting?はPrettierで良い感じにしましょうかってことなので、お好きにどうぞ。Yでいいのでは。 あとはnpm i でもyarnでもpnpm iでもお好きなコマンドをどうぞ。 npmは標準のやつ、yarnはnpmのちょっと進化したやつ、pnpmは同じモジュールがいろんなところに散らばらないやつみたいです。 sveltekitの出たはじめのうちは、Windows環境だとバグがありました。いまはWindowsでも動いてるみたいですけど、うまくCSSが当たらないとかのバグが残ってるような気がします(直ったかな?)WSL2とかでubuntuにしておくとバグを踏まない可能性が高くなるかと思います。僕はサブのノートパソコンでubuntuにしてますけど。 npm run devするとSvelteが起動します。こんな画面です。 良い感じですね。 ただ、素の状態でbuildすると、nodeで起動するタイプのものになります。index.jsとかapp.jsが生まれるので、それをpm2 startするとか、nodeで起動するみたいな感じです。 SSRに対応させてみよう。SSRってなんだか知らないけど。 nginxとかで動かしたいなら、SSRっていうんでしょうか、index.htmlがあって~みたいなやつでbuildしないといけないんです。そういうadapterという機能があるので、入れてみましょう。 @sveltejs/adapter-static こいつを入れます。 ただ、npm i @sveltejs/adapter-staticをすると古いバージョンのものが入ってbuildに失敗します。 npm i @sveltejs/adapter-static@1.0.0-next.4 こうすればbuild時にエラーを吐きません。たぶん。 それではsvelte.config.cjsもちょろっとadapterの部分を変更しましょう。 svelte.config.cjs const sveltePreprocess = require('svelte-preprocess'); //const node = require('@sveltejs/adapter-node'); const pkg = require('./package.json'); const static = require('@sveltejs/adapter-static'); /** @type {import('@sveltejs/kit').Config} */ module.exports = { preprocess: sveltePreprocess(), kit: { adapter: static(), // adapter: node(), target: '#svelte', vite: { ssr: { noExternal: Object.keys(pkg.dependencies || {}) } } } }; 邪魔なコメント取り除いてみました。 const nodeをconst staticに変える感じです。 これで一度rm -rfでbuildフォルダを綺麗にしてから、もう一度buildしてみましょう。 そうするとindex.htmlとそれに読み込まれるjsが出力されるので、これをnginxのvar/www/htmlとかに適当においてあげたら動きます。きっとね。htmlが表示できるサーバーならなんでも大丈夫なので、普通のレンタルサーバーとかでも動くはずです。たぶん。しらんけど。 Routingとか Svelteとの一番の違いはルーティングです。どういうことかというと、ナビゲーションバーを押したらページを遷移する、みたいな部分ですね。簡単に説明してみます。まずは僕のホームページを開いてみてください。 この上の部分のHomeとかDiaryとかArticleとかGALLERYとかSUPPORTがどうなっているかを簡単に説明します。 まず、routesフォルダの直下にindex.svelteがあります。ここにナビゲーションバーの下の部分を書きます。僕のサイトでいえば可愛い女の子の画像を出している部分からです。 ナビゲーションバーは、$layout.svelteという名前にします。これがルーティング機能を持つコンポーネントなんでしょう(たぶん)。ドキュメントを読まずに手探りでつくっているのであっているかどうかはしりません。 $layout.svelte <script type="ts"> import Nav from '../components/Nav.svelte'; import '../global.css'; </script> <div class="mx-auto container"> <Nav /> <div class="sticky top-10 h-auto grid grid-cols-5 text-center bg-white font-semibold w-full text-xs md:text-sm text-gray-700 z-50 md:justify-items-center md:w-2/4 md:mx-auto" > <a href="/" class="block col-span-1 tracking-widest h-full pt-3 pb-4">HOME</a> <a href="/diary" class="block col-span-1 tracking-widest h-full pt-3 pb-4">DIARY</a> <a href="/article" class="block col-span-1 h-full pt-3 pb-4">ARTICLE</a> <a href="/gallery" class="block col-span-1 h-full pt-3 pb-4">GALLERY</a> <a href="/support" class="block col-span-1 h-full pt-3 pb-4">SUPPORT</a> </div> </div> <slot /> こんな感じです。実はtailwindcssを使ってます。 こんだけ同じcss書くならコンポーネント化しろって話ですけど、まあコピペで済ませちゃいました。ダメですね。 aタグのhrefに相対リンクを書きます。フォルダ構造はこんな感じですね。 昔ホームページをつくったことがある人なら、なんとなくわかるでしょう。 /diaryのAタグを踏むと、diaryフォルダの$layout.svelteとindex.svelteが表示されます。$layoutで末尾の<slot />を忘れるとindex.svelteが表示されなくて3時間くらいはまるので気をつけましょう。 昔のインラインフレームが$layout.svelteになってみたいな感じでしょうか。 えっと、あとは画像のリンク先とかでしょうか。なんかこれもWindowsとLinuxで相対パス関連が怪しいような怪しくないような気がします。./にしないとダメだったり/でいけたり? よくわかんないですけど。 staticのなかにimagesフォルダをつくったら、リンクは/iamges/001.pngとかでいけます。たぶんね。 なんか流行ってるらしいtailwindcssを使ってみたい場合 npx svelte-add tailwindcss ↑ここにちょうどいいやつがあります。 npx svelte-add tailwindcss このコマンド入れるだけでよしなにやってくれます。 あとはまだ研究中ですけど、とりあえずSvelteでSSRを簡単につくりたいな~という人はこれで簡単につくれるんじゃないかと。まだWindowsだとバグ残ってる気がしなくもないです(devのときにsvelteに書いたCSSが反映されない?)。linuxでやったら大丈夫じゃないかな。Macは持ってないのでしりません。Dockerでつくってしまうのもいいかも。 たぶんsveltekitでできることっていろいろあると思うんですけど、それは必要になってからで良いんじゃないかな。暇なときにちゃんとdocumentを読もうと思って幾星霜。こういう機能便利だよっていうのがあれば知りたいですね。 そんなわけで、こつこつ頑張ってつくったサイトです。よろしくね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sveltekitに入門してみましたけれども(SSR+tailwindcss対応)

sveltekitに入門してみましたけれども 自分のサイトをsveltekitでつくってみました。 作成にあたって詰まった点などを書こうと思います。 sveltekitとは何か? まずsvelteを知らない人は、svelteを調べてください。sveltekitは、簡単に言えばルーティング機能がついたsvelteです。いい感じにページの移動みたいなことができます。僕のホームページでいえば、ナビゲーションにあるDiaryボタンを押せば、日記が表示される……みたいな感じですね。documentもあるので、ここ見れば大丈夫な気もします。 新規プロジェクトの作成 mkdir my-app cd my-app npm init svelte@next npm install npm run dev my-appのところは好きなフォルダ名で良いです。 フォルダつくって移動。 npm init svelte@nextで作成がはじまります。 Ok to proceed?は進めますか? なのでY Use TypeScriptはTypeScript使いますか? なので好きなほうで。 CSSとかSCSSとか使うかきかれるので好きなほうで Add ESLint for code linting?はESLintでlintしますか? みたいな感じなので、Yにしておきましょ。 Add Prettier for code formatting?はPrettierで良い感じにしましょうかってことなので、お好きにどうぞ。Yでいいのでは。 あとはnpm i でもyarnでもpnpm iでもお好きなコマンドをどうぞ。 npmは標準のやつ、yarnはnpmのちょっと進化したやつ、pnpmは同じモジュールがいろんなところに散らばらないやつみたいです。 sveltekitの出たはじめのうちは、Windows環境だとバグがありました。いまはWindowsでも動いてるみたいですけど、うまくCSSが当たらないとかのバグが残ってるような気がします(直ったかな?)WSL2とかでubuntuにしておくとバグを踏まない可能性が高くなるかと思います。僕はサブのノートパソコンでubuntuにしてますけど。 npm run devするとSvelteが起動します。こんな画面です。 良い感じですね。 ただ、素の状態でbuildすると、nodeで起動するタイプのものになります。index.jsとかapp.jsが生まれるので、それをpm2 startするとか、nodeで起動するみたいな感じです。 SSRに対応させてみよう。SSRってなんだか知らないけど。 nginxとかで動かしたいなら、SSRっていうんでしょうか、index.htmlがあって~みたいなやつでbuildしないといけないんです。そういうadapterという機能があるので、入れてみましょう。 @sveltejs/adapter-static こいつを入れます。 ただ、npm i @sveltejs/adapter-staticをすると古いバージョンのものが入ってbuildに失敗します。 npm i @sveltejs/adapter-static@1.0.0-next.4 こうすればbuild時にエラーを吐きません。たぶん。 それではsvelte.config.cjsもちょろっとadapterの部分を変更しましょう。 svelte.config.cjs const sveltePreprocess = require('svelte-preprocess'); //const node = require('@sveltejs/adapter-node'); const pkg = require('./package.json'); const static = require('@sveltejs/adapter-static'); /** @type {import('@sveltejs/kit').Config} */ module.exports = { preprocess: sveltePreprocess(), kit: { adapter: static(), // adapter: node(), target: '#svelte', vite: { ssr: { noExternal: Object.keys(pkg.dependencies || {}) } } } }; 邪魔なコメント取り除いてみました。 const nodeをconst staticに変える感じです。 これで一度rm -rfでbuildフォルダを綺麗にしてから、もう一度buildしてみましょう。 そうするとindex.htmlとそれに読み込まれるjsが出力されるので、これをnginxのvar/www/htmlとかに適当においてあげたら動きます。きっとね。htmlが表示できるサーバーならなんでも大丈夫なので、普通のレンタルサーバーとかでも動くはずです。たぶん。しらんけど。 Routingとか Svelteとの一番の違いはルーティングです。どういうことかというと、ナビゲーションバーを押したらページを遷移する、みたいな部分ですね。簡単に説明してみます。まずは僕のホームページを開いてみてください。 この上の部分のHomeとかDiaryとかArticleとかGALLERYとかSUPPORTがどうなっているかを簡単に説明します。 まず、routesフォルダの直下にindex.svelteがあります。ここにナビゲーションバーの下の部分を書きます。僕のサイトでいえば可愛い女の子の画像を出している部分からです。 ナビゲーションバーは、$layout.svelteという名前にします。これがルーティング機能を持つコンポーネントなんでしょう(たぶん)。ドキュメントを読まずに手探りでつくっているのであっているかどうかはしりません。 $layout.svelte <script type="ts"> import Nav from '../components/Nav.svelte'; import '../global.css'; </script> <div class="mx-auto container"> <Nav /> <div class="sticky top-10 h-auto grid grid-cols-5 text-center bg-white font-semibold w-full text-xs md:text-sm text-gray-700 z-50 md:justify-items-center md:w-2/4 md:mx-auto" > <a href="/" class="block col-span-1 tracking-widest h-full pt-3 pb-4">HOME</a> <a href="/diary" class="block col-span-1 tracking-widest h-full pt-3 pb-4">DIARY</a> <a href="/article" class="block col-span-1 h-full pt-3 pb-4">ARTICLE</a> <a href="/gallery" class="block col-span-1 h-full pt-3 pb-4">GALLERY</a> <a href="/support" class="block col-span-1 h-full pt-3 pb-4">SUPPORT</a> </div> </div> <slot /> こんな感じです。実はtailwindcssを使ってます。 こんだけ同じcss書くならコンポーネント化しろって話ですけど、まあコピペで済ませちゃいました。ダメですね。 aタグのhrefに相対リンクを書きます。フォルダ構造はこんな感じですね。 昔ホームページをつくったことがある人なら、なんとなくわかるでしょう。 /diaryのAタグを踏むと、diaryフォルダの$layout.svelteとindex.svelteが表示されます。$layoutで末尾の<slot />を忘れるとindex.svelteが表示されなくて3時間くらいはまるので気をつけましょう。 昔のインラインフレームが$layout.svelteになってみたいな感じでしょうか。 えっと、あとは画像のリンク先とかでしょうか。なんかこれもWindowsとLinuxで相対パス関連が怪しいような怪しくないような気がします。./にしないとダメだったり/でいけたり? よくわかんないですけど。 staticのなかにimagesフォルダをつくったら、リンクは/iamges/001.pngとかでいけます。たぶんね。 なんか流行ってるらしいtailwindcssを使ってみたい場合 npx svelte-add tailwindcss ↑ここにちょうどいいやつがあります。 npx svelte-add tailwindcss このコマンド入れるだけでよしなにやってくれます。 あとはまだ研究中ですけど、とりあえずSvelteでSSRを簡単につくりたいな~という人はこれで簡単につくれるんじゃないかと。まだWindowsだとバグ残ってる気がしなくもないです(devのときにsvelteに書いたCSSが反映されない?)。linuxでやったら大丈夫じゃないかな。Macは持ってないのでしりません。Dockerでつくってしまうのもいいかも。 たぶんsveltekitでできることっていろいろあると思うんですけど、それは必要になってからで良いんじゃないかな。暇なときにちゃんとdocumentを読もうと思って幾星霜。こういう機能便利だよっていうのがあれば知りたいですね。 そんなわけで、こつこつ頑張ってつくったサイトです。よろしくね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コールバック関数って使用頻度高いのに今までちゃんと理解できていなかったのでまとめます。。

コールバック関数とは コールバック関数は他の関数に引数として渡される関数で、外側の関数で何らかの処理やアクションを実行します。(MDN参照:https://developer.mozilla.org/ja/docs/Glossary/Callback_function) MDNの説明では、私には意味がさっぱり理解できませんでした。( ; ; ) いろいろ調べた結果、要は、 コールバック関数とは、高階関数に渡す関数のこと。 高階関数とは、関数に関数を渡すことが出来るやつ。 はい。ここまで調べて見ても私にはしっくりきません? 要の要は、 コールバック関数は、 ボタンクリックした時に〇〇を実行して! 〇秒後に〇〇を実行して! というように、今じゃなくて、任意のタイミングで何かを実行してほしい時に使える便利なものっていう理解に留まりました。(果たしてこの理解は合っているのかどうか...間違っていたらごめんなさい?) 構文 構文その1(関数宣言で書くとこれ) function 高階関数(コールバック関数){ //処理 コールバック関数(); }; 構文その2(無名関数/匿名関数で書くとこれ) const 高階関数 = function(コールバック関数){ //処理 コールバック関数(); }; 構文その3(アロー関数で書くとこれ) ※アロー関数は、...=(コールバック関数)=>{...の()括弧は、中のコールバック関数(引数)が1つのみであれば、()括弧の記載を省略出来る。 const 高階関数 = (コールバック関数)=>{ //処理 コールバック関数(); }; 使用例 「おにぎりの具は何が好き?」というダイアログが表示されて、「梅」と入力した場合のみ、「だと思った!」とコンソールに表示するというものを実装しました。(自分でも意味わからないのですが、思いつくままに作ったら、こんなものになりました?) 高階関数 -> onigiri コールバック関数 -> (fn) window.prompt -> テキストを入力することを促すメッセージを持つダイアログを表示(参照: https://developer.mozilla.org/ja/docs/Web/API/Window/prompt) function onigiri(fn) { const input = window.prompt("おにぎりの具は何が好き?"); if (input === "梅") { fn(); } } onigiri(function () { console.log("だと思った!"); }); setTimeoutメソッドという、「〇秒後に表示する」というメソッドを利用して、5秒後にコンソールに「Hello!」と表示するものを実装してみました。 setTimeout(function () { console.log("Hello!"); }, 5000); まとめ 高階関数、コールバック関数について、勉強してみましたが、まだまだ完全理解には届いていないような気がします。 コールバック関数について調べていると、「同期処理、非同期処理」や「promise」など初めて耳にする用語が次から次に現れて、コールバック関数の奥の深さを知らしめられました? 本投稿の内容に至らない点も多々あると思いますが、ご了承下さい。いつも皆さんが投稿して下さるQiitaの投稿を見て勉強させて頂いてます!プログラミングの勉強これからも頑張りましょう✨ 参考文献 初学者にもとても分かりやすく説明してくれている Youtuberさんのチャンネルです♪
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascript で PDFのページ数を取得するだけの処理(PDF.js)

Javascript で PDFのページ数を取得するだけの処理(PDF.js) function fToBase64Convert(file) { return new Promise((resolve, reject)=>{ const reader = new FileReader(); reader.onload = () => { resolve(reader.result); } reader.onerror = reject; reader.readAsArrayBuffer(file); }); } function getPdfPageNums(file){ return new Promise(async(resolve, reject)=>{ try{ pdfjsLib.GlobalWorkerOptions.workerSrc ="https://cdn.jsdelivr.net/npm/pdfjs-dist@2.7.570/build/pdf.worker.min.js"; var base64FObj = await fToBase64Convert(file); var pdfobj = pdfjsLib.getDocument({data: base64FObj }); pdfobj.promise.then(function(pdf) { resolve(pdf.numPages); }); }catch(e){ resolve(-1); } }); } var pdfPageNums = await getPdfPageNums(ファイルオブジェクト);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

YouTubeに3秒巻き戻し/早送りボタンを追加するブックマークレット

javascript:(()=>{const e=document.createElement("div");e.setAttribute("class","?"),e.textContent="<",document.body.appendChild(e);const t=document.createElement("div");t.setAttribute("class","?"),t.textContent=">",document.body.appendChild(t);let n=document.getElementsByClassName("?");n=[].slice.call(n);const o=e=>{const t=n.indexOf(e.target);document.getElementsByTagName("video")[0].currentTime+=3*(2*t-1)};for(let e=0;e<2;e++)n[e].style.cssText="z-index:99999;position:fixed;bottom:120px;"+`right:${20-60*(e-1)}px;`+"color:#222;font-size:10pt;text-align:center;height:50px;width:50px;line-height:50px;border-radius:50px;border:1px solid #aaa;background:#eee;opacity:.5;user-select:none;-webkit-user-select:none;",n[e].addEventListener("click",o);document.body.style.touchAction="manipulation",document.styleSheets.item(0).insertRule("c3-material-button {pointer-events: none}",0)})(); これは何? スマホで動画を視聴している時に3秒だけ戻したい、という願望を実現するものです。 使い方 youtube.comでこのブックマークレットを実行します。 ボタンが表示されるので、ここぞという時に押します。 簡潔な説明 上記コードは以下のコードを圧縮したものです。 (() => { // -------- // BTN_0 const btn_0 = document.createElement('div'); btn_0.setAttribute('class', '?'); btn_0.textContent = '<'; document.body.appendChild(btn_0); // BTN_1 const btn_1 = document.createElement('div'); btn_1.setAttribute('class', '?'); btn_1.textContent = '>'; document.body.appendChild(btn_1); // BTNs let BTNs = document.getElementsByClassName('?');// HTMLCollection BTNs = [].slice.call(BTNs);// => array // skip const skipp = (e) => { const idx = BTNs.indexOf(e.target); // ?[0] => back // ?[1] => forward document.getElementsByTagName('video')[0].currentTime += 3 * (2 * idx - 1); } // BTNs: style and EventListener for(let i=0; i<2; i++){ BTNs[i].style.cssText = 'z-index:99999;' + 'position:fixed;' + 'bottom:120px;' + `right:${20 - (i - 1) * 60}px;` + 'color:#222;' + 'font-size:10pt;' + 'text-align:center;' + 'height:50px;' + 'width:50px;' + 'line-height:50px;' + 'border-radius:50px;' + 'border:1px solid #aaa;' + 'background:#eee;' + 'opacity:.5;' + 'user-select:none;' + '-webkit-user-select:none;'; BTNs[i].addEventListener('click', skipp); } // body => double tap: off document.body.style.touchAction = 'manipulation'; // c3-material-button => touch: off document.styleSheets.item(0).insertRule('c3-material-button {pointer-events: none}', 0); // -------- })(); <div>を2つ作り、これをボタンとみなしてEventListenerを指定する。 ボタンがタップされてイベントが発生するたびに、document.getElementsByTagName('video')[0].currentTimeの値が増減する。秒数のところはお好みで変更して欲しい。 ボタンの大きさと色合いとかたち、表示位置についてstyleを記述。このあたりも微調整するといい。 また、ダブルタップの無効化、一部要素のタッチイベントの無効化を施している(が、使用環境によって機能しない場合がある)。 あとがき 外国語学習、特に動画を見ながらリスニングの訓練をする時に便利なんじゃないかと思います。 よかったら使ってください。 本日もありがとうございました。 参考 感謝します。 - https://qiita.com/takahasinaoki/items/60af70d47dee904caf27
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

日時系のinput要素に対し現在日時に基づく値を初期値として設定

日時系のtypeのinput要素に対して、現在日時に基づく各typeに合った値を初期値として設定するスクリプトです。 対応するtypeはtime, date, datetime-local, month, week 及び text です。 初期値を設定したい要素のタグにdata-nowDateを追記することで初期値が設定されます。 sample <input type='date' data-nowDate> 既に有効な値がvalueに設定済みの要素に対してはdata-nowDateを指定しても無視します。 ブラウザが対応していない日時系のtypeの場合は該当のtypeで本来設定されるvalue値を設定、その他の無効なtypeの場合はtextとして扱います。 typeがtextの場合は、data-nowDateに書式テンプレート文字を指定できます。 テンプレート文字一覧 %Y% 西暦 4桁 %Y2% 西暦 下2桁 %M% 月 2桁 %Ms% 月 0埋め無し %D% 日 2桁 %Ds% 日 0埋め無し %h% 時 2桁 %hs% 時 0埋め無し %m% 分 2桁 %ms% 分 0埋め無し %s% 秒 2桁 %ss% 秒 0埋め無し %MS% ミリ秒 3桁 %MSs% ミリ秒 0埋め無し sample <input type='text' data-nowDate='%h%:%m%:%s%'> スクリプト input_date_default_set.js 'use strict'; { window.addEventListener('DOMContentLoaded', function() { const d = new Date(); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); const isoLocal = d.toISOString(), pos = { time : [11, 16], date : [ 0, 10], month : [ 0, 7], 'datetime-local': [ 0, 16], }, elements = document.querySelectorAll('input[data-nowdate]'); for(let i = 0; i < elements.length; i++) { const e = elements[i]; if(e.value !== '') { continue; } const t = e.attributes.type !== undefined ? e.attributes.type.value.toLowerCase() : 'text', type = e.type === t ? e.type : t; if(Object.keys(pos).indexOf(type) >= 0) { const r = pos[type]; e.value = isoLocal.slice(r[0], r[1]); } else if(type === 'week') { const d_ = new Date(Math.ceil((d.getTime() - 2592e5) / 6048e5) * 6048e5), y = d_.getFullYear(), w = (1 + Math.floor((d_ - new Date(y, 0, 1)) / 6048e5)); e.value = y + '-W' + ('0' + w).slice(-2); } else if(type === 'text') { const p = isoLocal.match(/\d+/g); if(e.dataset.nowdate.trim() === '') { e.dataset.nowdate = '%Y%-%M%-%D% %h%:%m%:%s%'; } e.value = e.dataset.nowdate .replace(/%Y%/g, p[0]) // %Y% 西暦 4桁 .replace(/%Y2%/g, p[0].slice(-2)) // %Y2% 西暦 下2桁 .replace(/%M%/g, p[1]) // %M% 月 2桁 .replace(/%Ms%/g, +p[1]) // %Ms% 月 0埋め無し .replace(/%D%/g, p[2]) // %D% 日 2桁 .replace(/%Ds%/g, +p[2]) // %Ds% 日 0埋め無し .replace(/%h%/g, p[3]) // %h% 時 2桁 .replace(/%hs%/g, +p[3]) // %hs% 時 0埋め無し .replace(/%m%/g, p[4]) // %m% 分 2桁 .replace(/%ms%/g, +p[4]) // %ms% 分 0埋め無し .replace(/%s%/g, p[5]) // %s% 秒 2桁 .replace(/%ss%/g, +p[5]) // %ss% 秒 0埋め無し .replace(/%MS%/g, p[6]) // %MS% ミリ秒 3桁 .replace(/%MSs%/g, +p[6]); // %MSs% ミリ秒 0埋め無し } } }); } サンプル 動作デモ index.html <!DOCTYPE html> <html lang='ja'> <head> <meta name='viewport' content='width=device-width,initial-scale=1'> <meta charset='utf-8'> <title>日時系input要素に現在日時をデフォルト設定</title> <script src='input_date_default_set.js'></script> </head> <body> <!-- data-nowDateを追記で現在日時に基づく各typeに合った値を設定 --> time<br> <input type='time' data-nowDate><hr> date<br> <input type='date' data-nowDate><hr> datetime-local<br> <input type='datetime-local' data-nowDate><hr> month<br> <input type='month' data-nowDate><hr> week<br> <input type='week' data-nowDate><hr> text<br> <!-- type=textの場合に限り、data-nowDateに記入した書式テンプレートを反映 --> <input type='text' data-nowDate='%Y%-%M%-%D% %h%:%m%:%s%.%MS%'><br> &emsp;書式テンプレート<br>&emsp;%Y%-%M%-%D% %h%:%m%:%s%.%MS%<hr> <input type='text' data-nowDate='%Y2%-%Ms%-%Ds% %hs%:%ms%:%ss% %MSs%'><br> &emsp;書式テンプレート<br>&emsp;%Y2%-%Ms%-%Ds% %hs%:%ms%:%ss% %MSs%<hr> <!-- 書式テンプレート無指定時は %Y%-%M%-%D% %h%:%m%:%s% として解釈 --> <input type='text' data-nowDate><br> &emsp;書式テンプレートなし<hr> </body> </html> type='week'の要素については、私が確認できる範囲では2021年4月の時点でUIも表示されとくに問題なく使用できそうだったのはWindows/MacOS/Android版のChromeくらいでした。 非対応typeでもtextとして扱われるならまだいいのですが、iOS(14.4.2)/iPadOS(14.4.2)に至っては、typeはweekを保ったまま初期値も設定できるにも関わらず、要素をタップしても専用UIが表示されることもなく内容を変更することができない(通常のソフトウェアキーボードは表示されるが値を変更できない)状態だったので、やむなく使用する際には他のtype以上に注意が必要かな、と感じました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

社会人2年間の集大成!占いサイトを作成した話⑥

社会人2年間の集大成!占いサイトを作成した話⑤のつづきとなります! ついにこれがラストです!! 特集画面の紹介 機能の紹介 特集1 自分で考えるのはなかなか大変だったため、この文章は占いサイトからお借りしました。。 特集2 おやつ占いというのを他のサイトで見つけたので採用し、おやつの内容は自分で考えました。シンプルになりすぎないよう、各星座のところにイラストを追加してみました! 特集3 この開運術は自分で考えてみました!このページに関してもシンプルにならないよう各題名のところにイラストを追加しています。このイラストはFontAwesomeのものを使用しています。 特集ページは統一感が出るよう同じCSSを使用しています! 実装方法の紹介 特集画面のコンテンツ部分のHTML <section class="contents"> <ul> <li> <div class="exp"> <a href="contents3.html"> <img src="../../images/animal_usagi.png"> <span class="title">【2021年の運勢】生まれ年でわかる「最強の総合運」TOP3大公開!</span> </a> </div> </li> <li> <div class="exp"> <a href="contents2.html"> <img src="../../images/food_sweets.png"> <span class="title">星座別!2021年のラッキーおやつ占い</span> </a> </div> </li> <li> <div class="exp"> <a href="contents1.html"> <img src="../../images/oosouji_mado.png"> <span class="title">2021年の運勢をあげる「簡単開運術」4選</span> </a> </div> </li> </ul> </section> 画像と文字を押下した際にページ遷移するようにaタグでimageタグとspanタグを囲っています。 使用したCSS // 各特集ページの題名の下線をドットにする設定 .contents-title { border-bottom: 3px dotted rgba(220,220,220,.5); } .contents { text-align: left; } // 各特集ページの見出しの余白、位置、下線の設定 h2 { padding: 30px 30px 10px; text-align: left; border-bottom: 1px solid black; } // リスト項目の先頭に設定される「・」の削除 ul { list-style: none; } // 特集ページの見出しの下線の設定 .exp { border-bottom: 1px solid#e5e5e5; } // 特集ページの見出しの画像の余白、大きさの設定 .exp img { padding: 15px 10px 10px; width: 170px; height: 150px; } // 特集ページの見出しの文字の色、大きさ、余白、位置の設定 .title { color: black; font-size: larger; margin-top: 60px; position: absolute; } まとめ 特集サイトを作ろうと決めてはいましたが、自分で特集内容を考えるとなるとなかなか苦労しました。。今回は色々なサイトを参考にし、特集を作成していき無事完成しました!! また、全体を通して色々な占いを考え想像以上にサイトを完成するのには苦労しました。作っていく中で自分の思い通りにサイトを作りたいという気持ちと妥協したい気持ちが揺れ動きどちらを優先するかというのを味わえたのは個人開発だからこそだと思いました。今後もwebサイトやアプリを作っていきたいと思います
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】useStateで直前のデータを保存しつつ、新しいデータを挿入する方法

React学習で頭がパンクしていくのを防いでいくために今日もアウトプットします。 Reactの非常に便利なuseState Vueを使った後だとReactは本当に保守性に優れているなと日々感じます。 そんなuseStateで直前のデータを保存しつつ新たにデータを挿入する方法をお教えします。 やり方としてはスプレッド構文というものを使います。 とりあえず例を見ていきましょう! const [students, setStudents] = useState([]) setStudents(prevState => [...prevState, {name: 'Taro', gender: '男'}]); このようにprevStateを引数にとり、...prevState(スプレッド構文)を使って直前のデータを引き取りそのまま追加しています。 これを行わないとstudentsの中身が全て初期化されて上書きされてしまうのでとても役に立つっていう話です。 まぁこんな感じで今日のアウトプットは終わります。 Thank you for reading
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Typescriptの絶対パスImport利用時にESLint + Prettier + import sortのAuto Formatをいい感じにする

概要 Typescriptの絶対パスImportを利用しているときに、 ESLint + Prettier + import sortの構成でAuto Formatが思った通りに並ばなかったため、 その悪戦苦闘の結果を残します 絶対パスImportとESLint + Prettier + import sortの構成については 詳細を説明しませんが、実装の参考例を貼っておきます [React][TypeScript]絶対パスでimportできるようにする(@ozaki25様) ESLint + Prettier をセットアップするといつも苦しむからちゃんと理解する (2020年10月版) (@motch0214様) 事象 こうありたいという例.ts // [external, internal, sibling]のグループに分離する import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; // external import { UserEntity } from '@/entities/User'; // internal import { CorporationDbModel } from './Corporation'; // sibling 現実.ts // @から始まるinternalだと思うimportがexternalに含まれてしまう import { UserEntity } from '@/entities/User'; import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; // ここまでexternal import { CorporationDbModel } from './Corporation'; // sibling やったこと eslintの設定でimport/internal-regexを追加する。以上 https://github.com/benmosher/eslint-plugin-import/blob/master/README.md#importinternal-regex eslintrc.js module.exports = { settings: { 'import/internal-regex': '^@', // 以下割愛 }, // 以下割愛 }; おまけ import sortのgroup(外部パッケージ、内部パッケージ、同一ディレクトリなど)の順番を変えたい場合は以下を参照 https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/order.md#groups-array 個人的には以下の並びが外部->内部という並びの直感に合うと思っています eslintrc.js module.exports = { rules: { 'import/order': [ 'warn', { groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'], // 以下割愛 }, // 以下割愛 ], // 以下割愛 }, // 以下割愛 }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

交通費精算自動化スクリプト(outlookから予定抽出)

環境 Windows10 概要/背景 会社の交通費精算をするとき、毎月1回スケジュールを見直してEXCELにして処理担当の人に送っていました。抽出がめんどくさくなったので、抽出を自動化しました。 機能 サラリーマンやってる人がよく使ってる会社の支給PCでも制限なく使えるようにWindows標準のWindows Script Host(WSH)を使ってます。outlookから過去の予定を抽出して、csvに保存、処理担当の人に送るメールを作るまで自動化してます。 var FORREADING = 1; var FORWRITING = 2; var FORAPPENDING = 8; var TRISTATE_TRUE = -1; // Unicode var TRISTATE_FALSE = 0; // ASCII var TRISTATE_USEDEFAULT = -2; var WS_NOTVISIVLE = 0; var WS_ACT_NORMAL = 1; var WS_ACT_MIN = 2; var WS_ACT_MAX = 3; var WS_NOTACT_NORMAL = 4; var WS_ACT_DEF = 5; var WS_NOTACT_MIN = 7; var outlook = WScript.CreateObject("Outlook.Application"); var olFolderCalendar = 9; var name_space = outlook.GetNamespace("MAPI"); var folder = name_space.GetDefaultFolder(olFolderCalendar); var objNetWork = new ActiveXObject("WScript.Network"); var fs = new ActiveXObject("Scripting.FileSystemObject"); var sh = new ActiveXObject("WScript.Shell"); var homeDir = sh.SpecialFolders("Desktop") + "\\"; var schduleContents = homeDir + "ExtractedSchedule_" + objNetWork.UserName +".csv"; var previousChecktimeFile = homeDir + "LastSearchedIndex_" + objNetWork.UserName +".txt";//デフォルト値0 => folder.items.countを格納する var dd = new Date(); var shortCutName = "交通費精算_" + objNetWork.UserName; var previousCheckMonth = homeDir + "lastCheckedMonth_" + objNetWork.UserName +".txt"; var file_prev; var lastChecked; if(fs.FileExists(previousCheckMonth)){ file_prev = fs.OpenTextFile(previousCheckMonth, FORREADING, true, TRISTATE_FALSE); lastChecked = file_prev.ReadAll(); // file_prev = fs.OpenTextFile(previousCheckMonth, FORWRITING, true, TRISTATE_FALSE); // file_prev.Write(dd.getYear() + "/" + (dd.getMonth()+1) + "/" + dd.getDate() ); file_prev.Close(); }else{ //ファイルを作成 file_prev = fs.OpenTextFile(previousCheckMonth, FORWRITING, true, TRISTATE_FALSE); // file_prev.Write(dd.getYear() + "/" + (dd.getMonth()+1) + "/" + dd.getDate()); lastChecked = "Null(いつまで清算済みかは個別確認してください!)"; file_prev.Close(); } var mail = outlook.CreateItem(0); var mail_to = "担当者@メールアドレス "; var subject = "交通費精算依頼"; var body = "Auto-generated mail: "+ objNetWork.UserName + "です。\n"+lastChecked +"まで清算完了済みです。\n" + dd.getYear() + "/" + (dd.getMonth()+1) + "/" + dd.getDate() + "分までの交通費精算をお願いします。\n開始時間でソートしてください。\n 凡例(外出)・・・外出があった日\n(直行)・・・家から直行\n(直帰)・・・家に直帰\n外出が記載されず、直行/直帰のみ記載されている場合清算不要"; if (fs.FileExists(previousChecktimeFile)) { //存在した場合 var file = fs.OpenTextFile(previousChecktimeFile, FORREADING, true, TRISTATE_FALSE); var file2 = fs.OpenTextFile(schduleContents, FORAPPENDING, true, TRISTATE_TRUE); var str = file.ReadAll(); fileWrite(folder,file2,str); file = fs.OpenTextFile(previousChecktimeFile, FORWRITING, true, TRISTATE_FALSE); file.Write(folder.Items.Count);//調べたところまでを記録 // ファイルを閉じる file.Close(); file2.Close(); } else { //存在しない場合 //上書き新規保存 var file2 = fs.OpenTextFile(schduleContents, FORWRITING, true, TRISTATE_FALSE); fileWrite(folder, file2, 0); file2.Close(); } //ショートカットを同ディレクトリに作成 //var shortCut = sh.CreateShortcut(homeDir + shortCutName + ".lnk"); //shortCut.TargetPath = schduleContents; //shortCut.WindowStyle = WS_ACT_NORMAL; //shortCut.Save(); mail.To = mail_to; mail.Subject = subject; mail.Attachments.Add(schduleContents); mail.Body = body; //mail.Send(); mail.Display(); file_prev = fs.OpenTextFile(previousCheckMonth, FORWRITING, true, TRISTATE_FALSE); file_prev.Write(dd.getYear() + "/" + (dd.getMonth()+1) + "/" + dd.getDate() ); file_prev.Close(); // オブジェクトを解放 fs = null; sh = null; function fileWrite(folder, writingFile,startNum) { writingFile.WriteLine("要件,時間(分),場所,開始時間,終了時間");//要件,時間(分),場所,開始時間,終了時間 for (var i = folder.Items.Count; i > startNum; i--) { writingFile.Write(folder.Items(i).Subject.replace(/,/g, " ")); writingFile.Write(","); writingFile.Write(folder.Items(i).Duration); writingFile.Write(","); writingFile.Write(folder.Items(i).Location.replace(/,/g, " ")); writingFile.Write(","); writingFile.Write(folder.Items(i).Start); writingFile.Write(","); writingFile.Write(folder.Items(i).End); writingFile.Write(","); //writingFile.Write(folder.Items(i).Body.replace(/,/g, " ")); writingFile.Write(","); writingFile.WriteLine(); } //writingFile.Cose(); } 参考 WSH JScriptを使いこなそう https://jscript.zouri.jp/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

必要だと気付いてもいなかったフロントエンド用リポジトリ7選

本記事は、Anurag Kanoria氏による「7 Repos I Didn't Know I Needed For Front-End」(2021年3月8日公開)の和訳を、著者の許可を得て掲載しているものです。 必要だと気付いてもいなかったフロントエンド用リポジトリ7選 より良いものをより早く作るのに役立つ、あまり知られていないリポジトリ。 Photo by Juan Rumimpunu on Unsplash はじめに 私たちは、汎用ツールやリソースが数回タップするだけで手に入る時代に生きています。しかも幸いなことに、そのほとんどが無料です。 新しいツールやより簡単な方法が見つかり、面倒で複雑なタスクを実行できれば、誰にとっても、特に開発者には嬉しいことです。 でも、より良く賢い方法があり、予想外の方法で時間を節約できることを知らない場合もあります。 この2年間で見つけたGitHubの素晴らしいリソースは、私をかなり助けてくれました。これらのうちいくつかは、その必要性さえ知りませんでした。 そこで、あなたもおそらく必要になるGitHubリポジトリのリストを作りました。 1. 33 JS concepts ソース 私はプログラミングをJavaで始め、その後Dartに移行しました。 JavaScriptに移行した時は、プログラミングの概念はほぼ同じなのに、全く新しい言語のように感じました。 33 JS Conceptsは、JavaScript初心者や、スキルアップして新しいことを発見したい人はここを見るだけで事足ります。 その名の通り、これは知っておくべき33のJavaScript必須概念と基礎のリストです。 クリーンコード、部分適用、配列メソッド、データ構造、Promisesなどのトピックに関する詳細な記事やリソースが見つかります。 このリポジトリはJavaScriptを学ぶのにぴったりで、全くの初心者でも大丈夫です。 コンテンツは初心者向けにアレンジされており、変数や構文など、重要で最も基本的なトピックから、徐々に高度なトピックへと紹介されていきます。 2. Art of Command Line ソース このリポジトリは、Linuxのコマンドラインをマスターするためのものです。 コマンドラインを使いこなすスキルは軽視されがちですが、真の開発者だけが、その必要性を理解しています。 このリポジトリの内容の多くは、最初はQuoraに掲載されていましたが、GitHubに移されていくつかの改良が加えられました。 このリポジトリは、初心者と経験者の両方を対象としています。これはLinux用ですが、MacOSのみ、Windowsのみのセクションもあります。 Linuxを使っていなくても、このリポジトリには目を通すことを強くお勧めします。 3. RealWorld ソース ウェブ開発分野で数ヶ月以上働いているなら、より早く、より簡単にものを作れる新しいフレームワークが、次々と登場していることに気付くはずです。 これは素晴らしいことのようですが、ウェブ開発を続けていると、新しいフレームワークの出現が異常に早く、すべてのフレームワークに追いつくのはほぼ不可能だと、ある時点で悟ります。 さらに、新しいフレームワークを学ぶには独自の難点があります。というのは、ほとんどのチュートリアルは、FacebookやMediumのような実際のアプリケーションではなく、基本的なTodoアプリの作り方を教えているからです。 そこで、RealWorldの出番です。 このリポジトリでは、バックエンドとフロントエンドのフレームワークの一覧から好きなものを選んで、ほぼMediumのクローンであるConduitというアプリを作ることができます。 バックエンドとフロントエンドはAPIで接続され、各フレームワーク(バックエンドとフロントエンド)は同じAPI仕様で設計されているため、どの組み合わせでも問題ありません。 目的は、好みの技術スタックでMediumの実際のクローンを提供することです。 4. Front-end Developers Bookmarks ソース このリポジトリは、かなり前から私の頼みの綱です。 すべての開発者が時々必要とする重要なリソースを厳選して集めています。 このリストは、厳密にはフロントエンド開発者を対象としていますが、バックエンド開発者でも見る価値があります。 外観、ワークフロー、アーキテクチャ、ニュース、エコシステムなどに関するリソースを利用できます。 このリポジトリで最も興味深いものに、互換性セクションがあります。さまざまな入出力デバイスの連携に重点を置いています。 リポジトリが提供する情報は、この巨大なファイル1つにすべてまとめられています。 5. CSS Pro Tip ソース CSS Pro Tipは、CSSスキルの向上に役立ったリポジトリです。 最もシンプルでありながら最も効果的なヒントやガイドを紹介しています。 SVGやCSSリセットを使うメリットなど、基本的な情報が分かります。 ミュートされていない自動再生ビデオを非表示にする方法など、巧妙なトリックやハックを教えてくれます。 このガイドは、すべてのCSSユーザーを対象としています。初心者も経験者も、ぜひ見てみてください。 あまり知られていない面白いCSSプロパティを紹介した記事も、ぜひ読んでください。 誰も話題にしないCSSプロパティ6選 多くの人が聞いたことのない面白いプロパティ javascript.plainenglish.io 6. Awesome Design Patterns ソース このリポジトリは、大規模アプリケーションの構築を計画している場合に最適です。 フロントエンドデザインパターンと混同しないでください。これは完全にワークフローとアーキテクチャが対象です。 アーキテクチャは基本的に、ファイルを構造化する方法や、コードが他のコードファイルと通信する方法を指します。 例えば、MVCアーキテクチャは、アプリケーションをモデル、ビュー、コントローラの3つの主要な論理コンポーネントに分けます。 大規模アプリケーションと小規模アプリケーションの違いは、コーディングファイルの数だけでなく、アプリケーションの拡張性に重要な役割を果たすアーキテクチャにもあります。 Go、C#、JavaScriptなど、有名な言語のほとんどにパターンがあります。 クラウド、サーバーレス、マイクロサービスのアーキテクチャも、このリポジトリで提供されています。 7. Awesome Cheatsheets ソース その名の通り、このリポジトリには、データベース、フロントエンド、バックエンドなどに関する大量のチートシートがあります。 チートシートは、特にフレームワークと言語の間を行き来する時には、有益で重宝するツールになります。 また、数ヶ月ぶりにその言語に戻ってきた時にも、主要な概念を網羅したチートシートがあると便利です。 例えば、このリポジトリのJavaチートシートは、一般的な構文や規則だけでなく、比較演算子や数学ライブラリなど、主要なメソッドや概念をすべてカバーしています。 このライブラリを必要に応じて見直せば、既知のことを学び直すために膨大な時間を無駄にしないで済むでしょう。 おわりに 必要だと思っていなかったツールやリソースを見つけると、いつもワクワクします。 多くの場合、私たちは、より良い別の方法を探したり、より簡単な方法がないか調べたりせず、物事をありのままに受け入れてしまいがちです。 このリストには、厳選されたコンテンツも、RealWorldのようなユニークなリポジトリもあります。 この記事を楽しんでいただけたなら、プログラミングスキルを身につけられる楽しいゲームの記事も読んでください。 コーディングスキル向上のためにするべき楽しいゲーム7選 さまざまなウェブ技術をマスターする、ありきたりではない方法 javascript.plainenglish.io 記事を楽しんでいただけたなら嬉しいです。 翻訳協力 この記事は以下の方々のご協力により公開する事ができました。改めて感謝致します。 Original Author: Anurag Kanoria Original Article: 7 Repos I Didn't Know I Needed For Front-End Thank you for letting us share your knowledge! 選定担当: @gracen 翻訳担当: @gracen 監査担当: - 公開担当: @gracen ご意見・ご感想をお待ちしております 今回の記事はいかがでしたか? ・こういう記事が読みたい ・こういうところが良かった ・こうした方が良いのではないか などなど、率直なご意見を募集しております。 頂いたお声は、今後の記事の質向上に役立たせて頂きますので、お気軽に コメント欄にてご投稿ください。Twitterでもご意見を受け付けております。 皆様のメッセージをお待ちしております。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【javascript】配列のシャッフル2

前回の記事でsortが微妙だったのでreduceで書いてみた。 コード //要素の入れ替え関数 const swap = (a,i,j) => ( [a[i], a[j]] = [a[j], a[i]], a ); //reduceの内部処理 const reducer = (p,_,i,$,m=p.length-i) => swap(p, m-1, Math.floor(Math.random() * m)); //本体 const shuffleArray = arr => arr.reduce(reducer, arr.slice()); console.log(shuffleArray( [...Array(15).keys()]) ); 色んなものを犠牲にした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js: kolorist でConsole.log に色を付ける

10 Trending projects on GitHub for web developers - 9th April 2021 にて紹介されていたものをお試しした記録。 導入 npm install --save-dev kolorist お試し kolorist_test.js import { red, cyan, blue, yellow, white, magenta, green, bgYellow, bgWhite, bold } from 'kolorist'; console.log(red(`Error: カラリストで色付きエラー ${cyan('kolorist_test.js')}.`)); console.log(blue('あお')); console.log(magenta('マゼンタ')); console.log(yellow('イエロー')); console.log(green(bold('グリーン'))); console.log(bgYellow(white('背景イエロー'))); console.log(bgWhite(red('背景白'))); node kolorist_test.mjs でお試し。 出力結果 余談 本論と全く関係ないが mjs というものは何なのかは以下。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js+Vuetify on AWS Amplify環境のWebアプリbundleサイズ削減覚書

この記事は何 自作webアプリ開発中に直面した『bundleサイズ大きすぎる問題』解決に向けた覚書 最終的な結論は下記の通り 目下開発推進中のプロジェクト(今回だとaws-amplify)を使う場合は最新版が出ていないか、その中で自分が直面している問題が解決されていないか確認することが必要。 GithubのIssueを確認して、その中で解決策が議論されていないか探すことが必要。 何が問題だったか 以下の環境でbundleサイズがgzip後で680kB程度あり、Lighthouseのモバイルスコアが50点程度になる要因の1つであった。 package.json { "dependencies": { "@aws-amplify/ui-components": "^1.1.0", "@aws-amplify/ui-vue": "^1.0.6", "@nuxtjs/pwa": "^3.3.5", "aws-amplify": "^3.3.24", "core-js": "^3.8.3", "nuxt": "^2.14.12", "nuxt-webfontloader": "^1.1.0" }, "devDependencies": { "@nuxtjs/vuetify": "^1.11.3", etc..., }, } Nuxt.js開発時のbundleサイズ削減に向けてよく見かける方法として、下記はすでに導入済み。 Vuetify.jsのTreeShakeを有効化する extractCSSオプションを有効化する またAmplifyのTreeShakeを使ってbundleサイズ削減に向けた対策も織り込み済み。この辺、ちょっと古いネット情報を見てしまうと誤るので要注意。 https://github.com/aws-amplify/amplify-js/issues/488 ざっくり書くとこう OK: import { Auth } from '@aws-amplify/auth NG: import { Auth } from 'aws-amplify' その上で、js、cssそれぞれで下記の課題あり。 js: aws-amplify/authから参照しているelliptic, bn.jsなどのサイズが無駄に大きい(gzip後で100kB超) css: 全体のサイズが280kB程度ある。中身を見てみるとTreeShakeを有効にしているのに、使用していないVuetifyコンポーネントのcssが大量に含まれている 解決手段 aws-amplify/authから参照しているモジュール GithubのIssueを漁っていると全く同じ課題が議論されており、すでに解決策もmergeされていることがわかった(see) 多くの環境にとって不要なモジュールをすべてimportしていたっぽい(雰囲気) @aws-amplify/authが参照しているamazon-cognito-identity-jsが4.6.0以降であれば上記修正を織り込み済み aws-amplify/authを3.4.27から3.4.29に上げて効果を確認した。bundleサイズ▲150kB程度。 Vuetify.jsのCSSが読み込まれてしまう問題 こちらもnuxt-vuetifyのissueを読み漁って探した どうやらTreeShakeを有効にしていてもcssは適切に取捨選択されない模様 nuxt-purgecssを使えば良いという情報にたどり着くまでが長かった…。 ただしnuxt-purgecssのデフォルト設定だと、必要なcssまで削減してしまうことも分かった。適切な設定が必要。 nuxt-purgecssという単語がわかったので検索してみると、Qiitaでも投稿されている方を発見(see)。大変参考になりました。cssサイズ激減。 おわりに 新しいものに手を出すときは、最前線の情報を探しに行くことが大事ですね…。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsのLinkコンポーネントにpassHref属性を付与するのはどんな時なのか調べてみた

Next.jsを用いてクライアントサイドで画面遷移を行う場合、Linkコンポーネントを使用します。LinkコンポーネントはデフォルトでpropsとしてpassHref属性を受け取ることができるのですが、これはいったいどんな場面で使用するんだろう?と思って調べてみました。 使用技術 React Next.js styled-components 結論 Linkコンポーネントの子供がaタグをラップするカスタムコンポーネントの場合にLinkコンポーネントに「passHref」を付与する Next.js | 日本語ドキュメントに書いてありました。 公式ドキュメントやっぱり神! そもそも、passHrefとは何か? Linkコンポーネントがデフォルトで受け取ることができるpropsのうちのひとつ passHrefは子供にhrefプロパティを強制送信する デフォルト値は「false」 passHrefを付与しない場合 「passHref」を付与しないと、aタグにhref属性が付与されない。aタグにhref属性が付与されないと、検索エンジンにリンクとみなされないのでSEOに悪影響を及ぼす可能性がある。だから書き方的によろしくない。 サンプルコード import Link from 'next/link'; import styled from 'styled-components'; // これは、<a>タグをラップするカスタムコンポーネントを作成します const RedLink = styled.a` color: red; `; const NavLink = () => { return ( <Link href="/posts"> <RedLink>投稿リストページへ遷移します</RedLink> </Link> ); }; export default NavLink; 実際の画面 passHrefを付与したらどうなるか Linkの子であるaタグをラップしたカスタムコンポーネントにhref属性が付与されるようになります。 サンプルコード <Link href="/posts" passHref> <RedLink>投稿リストページへ遷移します</RedLink> </Link> 実際の画面 Next.jsの公式ドキュメントに記載されている内容ですし、理由も明白なため、CSS in JSライブラリとしてstyled-componentsを使用している場合は、必ずpassHref属性を付与するようにした方が良さそう 参考文献 Next.js | 日本語ドキュメント Next.js | 公式ドキュメント
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebエンジニアがパパっとCordova + ガワネイティブでスマホアプリを構築する

タイトルの通り ガワネイティブとはスマホアプリに近いUIの外部サイトをネイティブアプリ上のwebviewにて画面いっぱいに表示してアプリ化することでWebサイトをアプリストアに載せられるようにする技術のことを指します。 今回は正確にはネイティブアプリではなくjsやhtmlでハイブリッドアプリを構築するフレームワークであるCordovaを使うのでガワネイティブの正確な定義に当てはまるかは微妙ですが、まあ便宜的にガワネイティブという言葉を使います。 ちなみにPWAというブラウザから直接ホーム画面にWebサイトを登録してもらいアプリのように扱えるようになるPWAという技術はありますが一般的にはまだまだ知名度が低く、やはりアプリストアに載せることができるのは集客上結構なアドバンテージとなりそうです。 あとはWebエンジニアが1からスマホアプリ開発を習得するのは大変なのでまあなんとか楽したいという思想。 今回はAndroid+Windows10で開発行いました。 導入手順は下記URLなどを参考にしてます。 https://qiita.com/danishi/items/c7656d45bde73bf62feb 今回はとりあえずCordovaのwebviewで外部サイトを表示し、 その中でネイティブアプリのAPIで取得した情報などを外部サイトと双方向でデータ通信する方法を記述します。 スマホアプリ開発は今日始めて勉強したレベルなので間違ってるところはあるかもしれません。 iOSは持ってないのでまだ試してないですが多分大きくは変わらないと思います。 Cordova側 コマンドプロンプトでプロジェクトディレクトリに入りとりあえずInAppBrowserというプラグインを追加します。 これはCordova上でwebviewを表示するためのプラグインとなります。 cordova plugin add cordova-plugin-inappbrowser Cordovaで記述するソース index.js document.addEventListener('deviceready', onDeviceReady, false); function onDeviceReady() { function openInAppBrowser() { // ネイティブの機能を使いたい場合は_selfではなく_blankであることが必須 var target = "_blank"; // フルスクリーン指定することでアドレスバーとかを表示させなくする var options = "location=no,fullscreen=yes"; // 外部サイトを開く var inAppBrowserRef = cordova.InAppBrowser.open('_外部サイトのURL_', target, options); inAppBrowserRef.addEventListener('message', messageCallBack); // target="_blank"のタブが閉じたら同時にアプリを終了させる inAppBrowserRef.addEventListener('exit', function () { navigator.app.exitApp(); }); // 親画面から外部サイト上で特定のスクリプトを実行する 今回は単純に10秒後にアラートを出す setTimeout(function (){ inAppBrowserRef.executeScript({code: 'alert("test");'}) }, 10000); } // 外部サイトのイベントで受け取ったデータを処理する function messageCallBack(data){ alert(data.value); } openInAppBrowser(); } inAppBrowserRef.executeScript にて例えばネイティブの機能で取得したデバイス情報などをコードに埋め込み実行させることで外部サイトにデータを送ることができる感じですね。 外部サイト側 app.js // Cordova側に外部サイト上のデータを送信する function postMessage() { var messageObj = { "value": 'hogehoge', }; var stringifiedMessageObj = JSON.stringify(messageObj); webkit.messageHandlers.cordova_iab.postMessage(stringifiedMessageObj); } postMessage(); たったこれだけでデバイス、外部サイトの双方向のデータの受け渡しができるようです。 外部サイトのJSから直接Cordovaのプラグイン機能を呼び出すことは無理ですが、このようにデータを経由しあえばどうにでも行けそうです。 ちなみに親タブがあって、その親タブとtarget="_blank"で開いた外部サイトタブがデータを双方向に通信しあってるイメージで、target="_self"にした場合は親タブそのもののURLが切り替わるようで上記の方法は使えなくなります。 外部サイト側だけで機能が完結していてネイティブの機能連携など一切必要ない場合はtarget="_self"でもできそうですがこの場合マジでコードが2行とかで完結しそうなのでこんなんで審査通るのか疑問です。。。 あ、あとクッキー、ローカルストレージの保存とかはこの段階だとまだ課題としてありそうですね。 メリット・デメリットとか所管 メリットとしてはレスポンシブでうまく実装すればPC(Web)、iOSとAndroidアプリ、タブレットなどマルチデバイス対応を本当に最小工数で完結させることができそうです。 大きな工数がとれない個人開発などにおいてはかなり有用なテクニックになりそうです。 Webエンジニアがスマホアプリの開発を初めて行うというような際の学習コストも普通にハイブリッドアプリを構築するよりも小さく済みそうです。 デメリットとしてはやはり審査ですね。 ガワネイティブ自体はよく使われる技術ですが、本当にWebサイトをそのまま表示するだけのようなアプリは審査に落ちることも多々あるらしいです。 あと何気に致命的だと思ったのはネイティブ/外部サイト間でデータの受け渡しができるとは言っても 例えばAdMob(Googleが提供するアプリ向けの高単価広告サービス)など用のplugin導入には障壁があって(っていうか普通に導入無理じゃね?)、もしかすると広告収入モデルで収益化を目指してる場合はあまり向いてない可能性はありそうです。 こういうところまで考慮するなら固定オーバレイ広告を載せる前提でFlutterやガワネイティブという文字通りネイティブアプリとして開発した方が自由度は上がりそうです。 簡単そうで面倒なガワネイティブ、今だとどう実装するのが一番なんですかねぇ。。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

?React + TypeScript + Firebaseで認証付きの簡単な掲示板を作ろう

はじめに つぶやきしかできないWebアプリを作りました。ReactでWebサービスを作りたい人はこのチュートリアルを読み進め、足りない機能を作ってみると良いでしょう。 Demo: https://single-board-3c001.web.app/ Code: https://github.com/shuent/single-board Webアプリの機能 ログイン Googleアカウントかメールアドレスで認証。 つぶやき一覧 コメント投稿 認証したユーザーのみ投稿できる 使用する技術・ライブラリなど React Hooks useReducer ContextAPI TypeScript Create React App React Router Firebase Auth Firestore Hosting Chakra UI Hooks 今どきなので、関数コンポーネントとHooksを使います。筆者は関数コンポーネントが出てからReactを勉強したので、Class時代のReactを書いたことがない。 TypeScript コンパイル時エラーを出してくれる IDEがコード補完しやすい というメリットがあるので利用しています。初心者にとってコードを書く量が多くなるというデメリットを差し引いても、メリットが余りあります。 状態管理 状態管理にはHooksのuseReducerと Context APIを利用して、Fluxの思想を取り入れます。Reduxは使いませんが、実装の流れとしては一緒なので使い方は一度見ておくと良さそうです。 UIライブラリ: Chakra UI Chakra UI は TailwindCSS のようなユーティリティベースなComponentを提供するUIライブラリです。Reactコンポーネントになっているので、Tailwindより使いやすく、Material UIなどのUIフレームワークよりは自由度があるので好きです。 Firebase Firestore 今回はデータ構造が簡単なため、NoSQLであるFirestoreを利用します。NoSQLはDB設計に正解がないので難しく、複雑なリレーションを張るには向いてません。反面、バックエンドが要らず手軽に利用できるので、小規模で単純なデータ構造のアプリには使いやすいです。 Firebase Authentication 認証にはFirebase Authentication を利用します。tokenの管理などを裏でやってくれるので、とても楽です。さらにFirebase UIを使い、ログイン画面もほぼコード書かずに済みました。 Hosting コマンド一つでデプロイ、urlを発行してくれます。今回はこれを使います フロントエンドのホスティングサービスは他にもいろいろ出ています。Vercel, Netlify, Amplify. どれも簡単にデプロイできるので、試してみてください。 実装手順 画面設計・機能を書き出す データモデルの型を書き出す コンポーネント構造を考える 構造化して各フォルダ・ファイルを作る 画面を実装する。表示するのはダミーデータ ユーザー認証を実装 Flux(useReducer + Context)でつぶやきの状態管理を実装 firebaseから 読み取り、書き込みをできるようにする firestoreのルールを実装する デプロイする 次章から、ハンズオン形式でチュートリアルを書いていきます。コードを全て書いているわけではないので、説明が足りない部分はGithubリポジトリを参照してください。もしわからない部分があれば、質問していただけると記事の改善につながります。 画面設計・機能の書き出し どんなアプリを作るにしても設計が大事です。まず一言で何を作るかを決めます。 「ちょっと見た目に気を使ったシンプルなつぶやき投稿アプリ: Single Board」 機能一覧 次にアプリに欲しい機能を決めます。Single Boardは世に出す意識がなかったので、最低限と練習したい機能をつけることにしました。 投稿一覧が見れる 投稿にはユーザー情報と作成日時、つぶやき内容を載せる ログインしないと投稿できない googleとメールアドレスでログインできる 画面設計 紙でもUIツールでもパワポでも、ラフで良いので、画面を作ります。実際作ったのがこのくらいラフ。笑 トップページと、投稿コンポーネントを描いています。 どうせUIライブラリに依存することになるので、丁寧にSketchやFigmaを使ってデザインする必要はないです。画面数が多い場合、雑にプロトタイプとしてFigmaかなんかで作ってみるのはアリ。 実装 ここからは実装していきます。まず、create-react-app(CRA)でプロジェクトを作成します。 npx create-react-app single-board --template typescript CRAでは必要なライブラリが全部入っているので、設定なしにコードを書き始められます。eslintも入っています。 ただ、コードフォーマットツールのprettierはありません。自動でコードを綺麗に整形したい人はインストールしましょう。vscodeを使っている人は、prettierの拡張機能をインストールすればnpm installする必要がありません。.prettierrcで自分の設定でコードフォーマットしてくれます。 参考: https://create-react-app.dev/docs/setting-up-your-editor/#formatting-code-automatically https://www.digitalocean.com/community/tutorials/how-to-format-code-with-prettier-in-visual-studio-code-ja 手元でできた画面を確認してみてください。デフォルトの画面が立ち上がります。 npm start プロジェクトフォルダの中の、基本的にはsrc/の中にコードを書いていくことになります。 それぞれの初期ファイルの説明は公式ページを読んでみてください。 https://www.digitalocean.com/community/tutorials/how-to-format-code-with-prettier-in-visual-studio-code-ja データモデルの型を書き出す プロジェクトを作った時、何から書いていけば良いか迷いますよね。データモデルから書くことによって、データ中心にアプリを作っていけるのでおすすめです。今回は、 つぶやき一覧でユーザー名とつぶやきのデータを表示する。 つぶやきを投稿する という機能に必要なデータモデルを定義します。models.tsというファイルを作成します。 . └── src/ └── models.ts models.ts export type IUser = { displayName: string | null | undefined photoURL: string | null | undefined } export type IComment = { user: IUser content: string createdAt: Date id: string } export type ICommentAdd = { user: IUser content: string } 表示に使う属性だけ定義します。 コンポーネント構造を考える 次にView、見た目の部分を作っていきます。Reactで開発する上で大事な考え方が、コンポーネント志向です。画面を適切な役割ごとにコンポーネントで切り分けて実装することで可読性、保守性が上がります。 コンポーネントの種類には2種類あります。 APIと通信したり、状態管理コードを呼んだり、状態を持っていたり、という副作用を持った実体コンポーネント 受け取ったpropsを表示する純粋な関数コンポーネント (Hooksを使ってもそのコンポーネント内で閉じているものも含む) 私が今回アプリを作っていくときには、 まず画面をざっくり前者の実体コンポーネント(と名付けてみる)で分けてみる。 実体コンポーネントを実装する中で共通化できそうなものは関数コンポーネントに分けてみる という風に作っていきました。 component構造. - App - Home - Header - Editor - CommentList - Comment - UserAvatar - Content - Footer - Login - Header - Form 考え方としては、Atomic Designを参考に、簡易化しています。実体コンポーネント、関数コンポーネントはそれぞれ Organism, molecules に対応するかと思います。 大事なのは、難しく考えずだいたいで切り分けてあとで共通化する、ということです。最初からDRYでやるのは悪手です。 実装していきます。 フォルダ・ファイルを構造化して作成する。 先に必要になりそうなファイルを全部作っていきます。 . ├── package-lock.json ├── package.json ├── public ├── src │ ├── App.tsx # 各コンポーネントを呼び出す │ ├── api # firestoreのインターフェース │ │ └── commentsApi.ts │ ├── components │ │ ├── CommentList.tsx │ │ ├── Editor.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── Home.tsx │ │ ├── Login.tsx │ │ ├── MainVisual.tsx │ │ └── UserAvatar.tsx │ ├── contexts │ │ ├── authContext.tsx # ユーザー認証状態管理 │ │ └── commentsContext.tsx # つぶやきの状態管理 │ ├── reducers │ │ └── commentsReducer.ts # つぶやきのFlux (あとで解説) │ ├── firebase.ts │ ├── index.tsx # App.tsxを呼び出しているだけ │ ├── models.ts # データモデル │ └── theme.ts # 全体UIの設定 └── tsconfig.json Viewを作る UIライブラリのChakra UIをインストールします。 npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4 react-routerを使い、urlによって、トップ画面とログイン画面を出し分けます。公式ドキュメントではサンプルを動かせるので、めちゃわかりやすいです。 npm i react-router-dom App.tsx import { BrowserRouter as Router, Switch, Route } from 'react-router-dom' import { Header } from './components/Header' import { Login } from './components/Login' import { Home } from './components/Home' function App() { return ( <Router> <Header /> <Switch> <Route exact path='/'> <Home /> </Route> <Route path='/login'> <Login /> </Route> </Switch> </Router> ) } export default App Home.tsx import { CommentList } from './CommentList' import { MainVisual } from './MainVisual' import { Editor } from './Editor' import { Footer } from './Footer' export const Home = () => ( <> <MainVisual /> // 一番上のメインビジュアル <Editor /> // つぶやき編集フォーム <CommentList /> // つぶやきリスト <Footer /> // フッター </> ) ダミーデータを作り、とりあえず表示するの画面を作っていきます。 CommentList.tsx import { HStack, Box, Avatar, Heading, Text } from '@chakra-ui/react' import { IComment, IUser } from '../models' // ダミーデータ const user1: IUser = { displayName: 'testuser1', photoURL: 'sample.jpg' } const dcomments: IComment[] = [ { user: user1, content: 'first comment ss', createdAt: new Date(), id: 'comment1id', }, { user: user1, content: '元気ですか', createdAt: new Date(), id: 'comment2id', }, export const CommentList = () => { return ( <> <Heading> Posted Comments </Heading> <ul> {comments === [] ? ( <p>No Post</p> ) : ( // Comment 実装は省略 comments.map((comment) => ( <Comment key={comment.id} comment={comment} /> )) )} </ul> </> ) } 省略したコンポーネントはレポジトリをみてみてください。 認証画面・機能を作成 ログイン画面を作っていきます。Firebase AuthenticationとFirebaseUIを使うことで簡単に実装できます。 firebaseの設定 firebase consoleでプロジェクトを作成します。 https://console.firebase.google.com/u/0/?hl=ja 作成したら、プロジェクトの設定 > Firebase SDK snippet を取得します。 CRAは元々の設定で、REACT_APP_から始まる環境変数名を.envファイルからアプリに組み込んでくれます。そして、ビルド時に値を埋め込んでくれます。これで外に変数が漏れることはありません。 https://create-react-app.dev/docs/adding-custom-environment-variables/ 先ほど取得した値を変数として.local.envファイルに宣言し、プロジェクトのルートにおきます。 local.env REACT_APP_APIKEY=xxxxxx REACT_APP_AUTHDOMAIN=xxxxxx REACT_APP_PROJECTID=xxxxxx REACT_APP_STORAGEBUCKET=xxxxxx REACT_APP_MESSAGINGSENDERID=xxxxxx REACT_APP_APPID=xxxxxx REACT_APP_MEASUREMENTID=xxxxxx プロジェクト内では、firebaseを扱うファイルを作り、環境変数を埋めます。ついでにFirebaseの認証とデータベースにfirestoreを使うので、exportしておきます。 src/firebase.ts import firebase from 'firebase' const fireConfig = { apiKey: process.env.REACT_APP_APIKEY, authDomain: process.env.REACT_APP_AUTHDOMAIN, projectId: process.env.REACT_APP_PROJECTID, storageBucket: process.env.REACT_APP_STORAGEBUCKET, messagingSenderId: process.env.REACT_APP_MESSAGINGSENDERID, appId: process.env.REACT_APP_APPID, measurementId: process.env.REACT_APP_MEASUREMENTID, } firebase.initializeApp(fireConfig) const auth = firebase.auth() const firedb = firebase.firestore() export { firebase, auth, firedb } firebaseUIを導入 ログイン画面を作っていきます。 firebaseUIのReact用ライブラリがあるのでインストールします。 *公式の開発者がストップしているみたいなので、canary版を使います。 https://github.com/firebase/firebaseui-web-react/pull/122 npm install react-firebaseui@canary ログインコンポーネントを作ります。ログインフォームの挙動はuiConfig変数で設定します。 src/Login.tsx import { Center, Heading, VStack } from '@chakra-ui/layout' import { primaryTextColor } from '../theme' import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth' import { firebase, auth } from '../firebase' const uiConfig = { signInFlow: 'popup', signInSuccessUrl: '/', signInOptions: [ firebase.auth.GoogleAuthProvider.PROVIDER_ID, firebase.auth.EmailAuthProvider.PROVIDER_ID, ], } export const Login = () => { return ( <Center mt={8}> <VStack> <Heading size='md' color={primaryTextColor}> Sign In </Heading> <StyledFirebaseAuth uiConfig={uiConfig} firebaseAuth={auth} /> </VStack> </Center> ) <StyledFirebaseAuth firebaseAuth={auth} />でプロジェクトのfirebaseインスタンスとUIをつなげています。 AuthContextで認証状態管理 ログイン・登録ができるようになったので、セッション情報:(「ログインしているかどうか」と「ログインしているユーザー情報」)をアプリ内で使えるようにします ユーザー認証の状態管理には、Context APIを使用します。流れとしては、Contextを作成し、Providerで状態を保存し、useContextで使います。 公式: https://ja.reactjs.org/docs/context.html 認証用のContextを扱う、authContext.tsを作成します。 src/contexts/authContext.tsx import React, { createContext, useContext, useState, useEffect } from 'react' import { firebase, auth } from '../firebase' type AuthContextProps = { user: firebase.User | null } const AuthContext = createContext<AuthContextProps>({ user: null, }) export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const [user, setUser] = useState<firebase.User | any>(null) const [loading, setLoading] = useState(true) useEffect(() => { const unsubscribe = auth.onAuthStateChanged((user) => { setUser(user) setLoading(false) }) return unsubscribe }, []) return ( <AuthContext.Provider value={{ user }}> {!loading && children} </AuthContext.Provider> ) } export const useAuth = () => { return useContext(AuthContext) } auth.onAuthStateChangedではユーザーの認証状態を監視して、ログイン、ログアウト時、と認証情報が変わる度に引数に渡しているコールバック関数を実行します。 ProviderをUnmountする時に監視を捨てる必要があるので、useEffectの返り値に設定してます。 https://firebase.google.com/docs/auth/web/manage-users?hl=ja セッションを使用する 今定義した関数を使い、アプリ上でセッションを取得できるようにしましょう。アプリ全体をAuthProviderで囲みます。これで囲んだどのコンポーネント内でもuseAuth()が使えることになります。 App.tsx ... import { AuthProvider } from './contexts/authContext' function App() { return ( + <AuthProvider> <ChakraProvider theme={theme}> <Router> <Header /> <Switch> <Route exact path='/'> <Home /> </Route> <Route path='/login'> <Login /> </Route> </Switch> </Router> </ChakraProvider> + </AuthProvider> ) } ヘッダーでログインしている時はログアウトボタン、ログインしていないときはログインリンクを表示します。 Header.tsx import { Link } from 'react-router-dom' import { useAuth } from '../contexts/authContext' import { auth } from '../firebase' export const Header = () => { const { user } = useAuth() return ( <> // ...省略 {user ? ( <Text as='button' onClick={() => auth.signOut()}> Log Out </Text> ) : ( <Link to='/login'> <Text color='white'> Sign In</Text> </Link> )} // ... </> ) トップ画面のエディターでもログインしている時のみ投稿できるようにします。 src/Editor.tsx export const Editor = () => { const { user } = useAuth() const [content, setContent] = useState('') const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (content !== '' && user) { // post content to server } else if (!user) { alert('Sign in first') } setContent('') } const handleChange = (e: React.FormEvent<HTMLTextAreaElement>) => { setContent(e.currentTarget.value) } return ( <div> <VStack as='form' onSubmit={handleSubmit} > <Textarea name='content' value={content} onChange={handleChange} placeholder="What's on your mind?" /> <Button type='submit' colorScheme='orange'> post </Button> </VStack> </div> ) } これで認証状態管理は終わりです。 つぶやきの状態管理 つぶやきの状態管理でもContextAPIを使い、状態を保持できるようにします。加えて、useReducerというHookを使いFluxアーキテクチャでの状態管理を行います。Contextだけでも管理できないことはないですが、状態を変更する機能が多くなってきた時に分かりやすいです。 reducerから定義していきます。 src/reducers/commentsReducer.ts import { IComment } from '../models' export type CommentsAction = | { type: 'SET_COMMENTS'; comments: IComment[] } | { type: 'ADD_COMMENT'; comment: IComment } export type CommentsState = { comments: IComment[] } export const initialState: CommentsState = { comments: [], } export const commentsReducer = ( state: CommentsState, action: CommentsAction ): CommentsState => { switch (action.type) { case 'SET_COMMENTS': return { comments: action.comments } case 'ADD_COMMENT': return { comments: [action.comment, ...state.comments] } default: return state } 後々firestoreにつぶやきを投稿したタイミングでまたつぶやきリストを取得するかストリーミングすれば最新の状態になるので、ADD_COMMENTはあってもなくても良いのですが、毎回APIを呼ばなくても良いようにと、練習のために作っています。 Contextを作ります。 src/contexts/commentsContext.tsx import { createContext, Dispatch, ReactNode, useReducer, useContext, } from 'react' import { CommentsAction, commentsReducer, CommentsState, initialState, } from '../reducers/commentsReducer' type CommentsContextProps = { state: CommentsState dispatch: Dispatch<CommentsAction> } const CommentsContext = createContext<CommentsContextProps>({ state: initialState, dispatch: () => initialState, }) export const CommentsProvider = ({ children }: { children: ReactNode }) => { const [state, dispatch] = useReducer(commentsReducer, initialState) return ( <CommentsContext.Provider value={{ state, dispatch }}> {children} </CommentsContext.Provider> ) } export const useComments = () => useContext(CommentsContext) EditorとCommentListで使うので、それらを含むHomeコンポーネントで囲んでおきます。 src/Home.tsx export const Home = () => ( <> + <CommentsProvider> <MainVisual /> <Editor /> <CommentList /> <Footer /> + </CommentsProvider> <> ) フォーム送信時にDispatchします。 src/components/Editor.tsx export const Editor = () => { const { user } = useAuth() const { dispatch } = useComments() const [content, setContent] = useState('') const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (content !== '' && user) { const toPost: ICommentAdd = { user: { displayName: user.displayName, photoURL: user.photoURL }, content, } dispatch({ type: 'ADD_COMMENT', comment: { ...toPost, createdAt: new Date(), id: Date(), }, }) } else if (!user) { alert('Sign in first') } setContent('') } return (...) } useEffectでコンポーネントを読み込むタイミングでDispatchします。 src/components/CommentList.tsx export const CommentList = () => { const { state, dispatch } = useComments() const dcomments: IComment[] = [ { user: user1, content: 'first comment', createdAt: new Date(), id: 'comment1id', }, { user: user1, content: '元気ですか', createdAt: new Date(), id: 'comment2id', }, ] useEffect(() => { let unmount = false if (!unmount) { console.log('set comments called') dispatch({ type: 'SET_COMMENTS', comments: dcomments }) } return () => { unmount = true } }, [dispatch]) return (...) } これでつぶやき(コメント)の状態管理は終わりです。 firestore への read/write firestore上でデータを管理できるようにします。 firebaseコンソールで firestoreを有効にします。 firestoreへのインターフェースを実装します。ここで実装することで、将来別のAPIを使った時にも 関数名、引数、返り値を同じにすることでView側を変更しなくても良いように、疎結合に実装します。もっと厳密にやるならinterfaceを定義したり、Dipendency Injectionをすることになります。 src/api/commentsApi.ts import { firedb, firebase } from '../firebase' import { IComment, ICommentAdd } from '../models' export const getComments = async () => { const snapShot = await firedb .collection('comments') .orderBy('createdAt', 'desc') .get() const data = snapShot.docs.map<IComment>((doc) => ({ user: doc.data().user, content: doc.data().content, createdAt: doc.data().createdAt.toDate(), id: doc.id, })) return data } export const addComment = async (comment: ICommentAdd) => { return firedb.collection('comments').add({ user: comment.user, content: comment.content, createdAt: firebase.firestore.Timestamp.now(), }) 使用時には、主にDispatch呼び出し前におき、結果をDispatchに渡します。 コメントリスト src/components/CommentList.tsx export const CommentList = () => { const { state: { comments }, dispatch, } = useComments() useEffect(() => { + getComments().then((data) => { + dispatch({ type: 'SET_COMMENTS', comments: data }) + }) }, [dispatch]) return (...) } エディター src/components/Editor.tsx export const Editor = () => { const { user } = useAuth() const { dispatch } = useComments() const [content, setContent] = useState('') const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (content !== '' && user) { const toPost: ICommentAdd = { user: { displayName: user.displayName, photoURL: user.photoURL }, content, } + addComment({ ...toPost }) dispatch({ type: 'ADD_COMMENT', comment: { ...toPost, createdAt: new Date(), id: Date(), }, }) } else if (!user) { alert('Sign in first') } setContent('') } ブラウザでつぶやいてみると、firestoreにもデータが追加されているのが分かります firestore rule Editor Componentで、ユーザーではない場合投稿できないようにしましたが、直接APIを知られてしまった場合、投稿できてしまいます。さらに今のままだと投稿するユーザー名を偽装して、本人以外の名を騙り投稿できてしまいます。 そのようなことがないように、コンソールでruleを書くことで、セキュリティを守ります。ローカル環境で書いてデプロイすることも可能ですが、ここではコンソールに直接書いてます。 左下のルールプレイグラウンドでは、いろいろな条件でルールをテストできるので、試してみると良いです。 今回のルールはこちら 'comments'以外のリソースにアクセスできない (ログインしてなくても)誰でも読めるようにする ユーザー名と一致する投稿のみ受け付ける。 更新、削除は受け付けない firestore.rule rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { match /comments/{comment} { allow read: if true; allow create: if request.auth.token.name == request.resource.data.user.displayName } } } } 公式: https://firebase.google.com/docs/rules/basics?hl=ja Firebase Hosting へデプロイ ここまででアプリが完成したらFirebase Hostingサービスにデプロイします。 コンソールからHostingを有効にします。 # firebase cliをインストールして、deployコマンドを使えるようにします。 npm install -g firebase-tools # 認証してコンソールで作ったプロジェクトを選択します。 firebase login firebaseのファイルを作成します。 firebase init いろいろ聞かれます。Hostingだけ選択し、 What do you want to use as your public directory? には buildを指定します。 あとは好きなものを選んでください。 最終的にこんなファイルができていれば大丈夫です。 firebase.json { "hosting": { "public": "build", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ], "rewrites": [ { "source": "**", "destination": "/index.html" } ] } } Githubと連携してCICDやプルリクでデプロイしてくれるのですが、その設定は扱いません。調べてみてください。 プロジェクトをビルド後、デプロイします! npm run build firebase deploy --only hosting うまくいけば、ターミナルに出てくるurlがデプロイ先です!!! 終わりに 今回作ったアプリにはつぶやきの削除、ユーザー設定、ユーザーページなど機能が足りません。ここまで読んでくれた方はこれを発展させて、改造させて、面白いものを作ってみてください。もし作った時はコメントから報告してくれると嬉しいです。 初めて包括的な記事を書いたので足りないところはあると思いますが、楽しんでいただけたら何らかのアクションをしてくれると嬉しいです。ここまで読んでくれてありがとうございました。次回はもうちょっと高度なことか、コンポーネント設計に関することを書きたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueとPixi.JSでアクションゲーム『ネコメザシアタック』を開発して3年目なのでソースと解説

2/22に個人開発のブラウザゲーム『ネコメザシアタック21』をリリースしました。 特に理由はないのですが、3年前からこの時期には毎年同じテーマでゲームを作ってます。 ぶっちゃけ全然流行らないし、当然収益なんて1円もないのだけど、3年続けると見えてくることもあるので今年も記事書きます。せめて供養がわりにLGTM頂けると幸甚の極みです 作ったものの変遷 まあそんなわけで、まずはこの3年間での進歩をみて欲しい 1年目:その場でジャンプするだけの超シンプルゲーム プレイURL: https://mezashiattack.firebaseapp.com ソースコード: https://github.com/yuneco/mezashi 解説記事: VueとSVGを使ってシューティングゲーム『ネコ?メザシ?アタック?』を作ったのでソースと解説 その場でジャンプして弾(メザシ)を発射するだけの簡単仕様 Vue2(JavaScript)のオーソドックスな構成 Vueでdiv要素のCSSを操作してキャラクターを動かす方式 今コミットログ見返したら2週間(週末2回)で作ったらしい...まじか... 2年目:曲面(角丸)のステージ・ランキング・TS採用・レスポンシブ プレイURL: https://nekomzs2.web.app/ ソースコード: https://github.com/yuneco/mezashi2 解説記事: VueとCSSとTypeScriptでシューティングゲーム「ネコメザシアタック2020」を作ったのでソースと解説 時代に乗るためにTypeScriptとCompositionAPIを採用(Vue本体はまだv2) 前回位置固定だったキャラクター(たまさん)が画面内を動き回る仕様に進化 Firebaseで雑なランキングシステムを実装 3年目:複数のステージ・グラフィックの進化・マルチデバイス プレイURL: https://nekomzs21.web.app/ ソースコード: https://github.com/yuneco/mezashi21 解説記事: ここ Vue3採用 グラフィックスを超強化 画面描画をCSSからPixiJS(Canvas/WebGL)に変更 デバイス・画面サイズにかかわらず同じ画面を表示できるようになった(やっと!) 変えたこと変わらないこと 少しずつ採用技術を変えながら毎年新しく作りなおしているので基本的には別物なのですが、それでも「変わらないもの」って結構ある。「変えたもの」と「変えなかったもの」をいくつか紹介します。 3年間で変えたこと: JavaScript → TypeScriptへの変更 なんていうかTypeScriptって面倒そうじゃないですか? 型パズル大好きなつよつよエンジニアの人が使えばよくて、私みたいな弱小が趣味で作るならJSでいいじゃーん...そう思ってたのが最初の年 今だから言う。これ完全に間違ってた 別に型パズルはしなくてもいいし、それこそstrictなしのゆるゆるでもいいけど、とりあえずTypeScriptは入れとけ。きちんとした設計なしに走り出して、途中できちんとしたリファクタリングなんてしない趣味のゲーム開発なんかなら尚更。 ↓の図は1年目(js)と3年目(ts)の当たり判定ロジックの比較。 一見ほとんどおなじだけど、VSCodeでカーソル当てるとわかる。持ってる情報が段違い。 雑に作って雑に直していくためにもTypeScriptは必要。もう戻れない。戻っちゃいけない。 3年間で変えたこと:PixiJS + GSAPの採用 最初に紹介したように、1-2年目はDOMのCSSをVueで操作してキャラを動かしているんだけど、まあ正直言うとしんどいです。 1年目みたいな簡単な動きで5個10個の要素を動かすだけなら全然いいんだけど、入れ子になった何十何百の要素を管理するは結構きつい。色々最適化の工夫はあるけど、それでも動くdivの数が200〜300くらいがパフォーマンス的にも限界になることが多いので、装飾的な要素やパーティクルみたいなエフェクトもあんまり使えません。DOMでゲーム作るのは好きなんだけどね。。 そんなわけで今年は一念発起してDOMを捨て、2Dグラフィックスの王道的なライブラリであるPixiJS + GSAPを採用しました。PixiJSではWebGLを使って高速な描画ができる上、WebGLのフィルターを自作すれば表現の自由度が一気に上がるのも嬉しいポイント。 多分誰も気づいてないから自分で言うけど、上の絵の2枚目の水中面、ちゃんと画面全体に水中っぽいエフェクトかかってるのです。これもWebGLのフィルターのおかげ。 PixiJSでフィルターを自作して使う方法は●●WebGL(PIXI.js + glsl)と物理演算(matter.js)で可愛い絵本風タピオカ作ったので解説●●で解説してるので気になる方はみてみてください(今回物理演算はナシです)。 3年間変えなかったこと:Vueの利用 PixiJS + GSAPにするならVueいらなくね? みたいな意見もあると思うのですが、今年も迷わずVueを使ってます。個人的な主張ですが、ラフに個人開発をするときこそ、最初にVueなりReactなりは導入しておいた方が良いです。 だって、vue createなりcreate-react-appなりすれば、それだけで汎用的なweb開発環境にTypeScriptまでついてくるですよ?特別な思想信条がなければ使った方が早いです。 あと、最初はゲームのメイン画面だけ考えてても、リリースが近くなると「タイトル画面どうしよう?」「ランキングは?」みたいな悩みが出てきます。その手の付加的な画面をCanvasの中に作っていくのはしんどいので、さくっとVueなりReactなりで作れる準備をしておくのは有益です。 今回も、メインのゲーム画面の周辺に表示されるステータス部や上にオーバレイするランキング等はVueで作成・管理しています。Vue部分とPixi部分を連動させる方法は後ろの方でちょっと書きます。 3年間変えなかったこと:SVGの利用 PixiJSに限らず、Canvas/WebGL系のゲーム作成(特にチュートリアルや入門記事)では、画像のフォーマットとしてPNG画像をみっちり並べたスプライトシートを使うのが一般的です(多分)。たとえば『Pixi.js でゲームを作ってみる vol.1』という記事の後ろの方に実際の作例が載っているのですが、見ての通りまあ結構めんどくさいです(※この解説記事自体はとてもわかりやすくありがたい内容です。念のため)。 でもベクター画像のSVGよりもドット絵の方がまだ簡単なんじゃないの?って思うじゃん? とりあえず↓を読んで欲しい キャラや画面要素のサイズ・デザインが変わるたびに元絵を直す必要がある(=つまり主要なパーツのサイズが最初にちゃんと設計できないと辛い) Retina(高解像度ディスプレイ)対応がめんどい。あとでまとめて対応できるだろうって思うとほんと痛い目にあう ドット絵のフリー素材を集めてきてサイズやトンマナ合わせるのは至難の技。ベクターならわりとなんとでもなる(加工の可否とかの利用条件は確認してね) そもそもいい感じのドット絵を描くのはセンスも技術も超必要。あれはほんと職人芸 ...というわけで、「とりあえず適当に動けば見た目は気にしない」ならドット絵PNGでもいいけど、そうでければSVGを使うのが吉です。私は普段アナログタッチの絵を描くのでベクターイラストは得意ではないのだけど、それでもゲームの素材作るならイラレでSVG作る方が100倍楽(もちろん個人の見解ry) あと、SVGはPNGよりも圧倒的に軽いのです。今回(3年目)はそれなりに見た目にも拘ったけど、それでも実際に転送してる画像データは88ファイルで80KB! スマホの細い回線でも楽に動く上、ややこしいローディング待ちのロジックを作らなくても雑なロード処理で動いちゃうのも趣味開発にはありがたいポイント 3年間変えなかったこと:当たり判定・効果音再生等の基本ロジック これは手抜きって言えば手抜きなのですが、結果的に当たり判定とSE再生はほぼ3年間同じものを使い回しています。 当たり判定は結局3年間box-intersectという汎用の衝突検知ライブラリを使って実装しています。 - 1年目: CollisionDetector.js - 2年目: CollisionDetector.ts - 3年目: CollisionDetector.ts サウンド系はaudio-loaderとaudio-playの組み合わせです。 - 1年目: playSound.js - 2年目: playSound.ts - 3年目: playSound.ts 実装は毎年少しずつ変わってるけど、基本は全部同じなのがわかると思います。 当たり判定もサウンド再生も、よほど全部入りのゲームライブラリを使わない限り自分で組み込む必要があるので、上記に限らず一度何かを使えるようになっておくと、ものづくりのスピードが一気に上がります。ここらへんサクッとできるとつよつよ感がでて良いですね 技術的なポイント解説:VueとPixiJSとGSAPでいい感じにゲームを作る ここからはいくつか、Vue + PixiJS + GSAPでゲームを作る際のポイントや工夫を載せておきます。 ポイント:PixiJSでSVGをロードする + Retinaに対応する 上の方でSVGを超プッシュしておいてあれなのですが、PixiJSでSVGをテクスチャ画像として読み込むのはちょっと工夫が必要です。 基本的にはSVGであってもPixiのテクスチャーとして使うときにはPNGと同様のラスター画像に変換することになるのですが、そのまま読み込むとRetinaに対応できず画像が滲んで表示されます。。 対応方法はいくつかあるのですが、今回は汎用的に使える方法として、Retina環境ではSVGを一度表示サイズの2倍の<img>要素に表示して、その<img>からPixi.Textureを生成する方法をとっています。 https://github.com/yuneco/mezashi21/blob/master/src/logics/loadImgs.ts#L49 今回はかなり雑な実装しかできていないのですが、きちんと使えば画面サイズに合わせてぴったりのテクスチャーを生成することもできるはずです。 ポイント:PixiJSの世界でVueを使う 去年までVueで作ってたゲームをPixiJSベースにして最初に悩むのが状態の管理方法です。 簡単なチュートリアルだと、全部のソースが1ファイルに収まってていろんな変数がグローバルだったりするのだけど、それって現実的じゃないですよね。 Vueのprops/emitsのような仕組みもないので、ルートのPixiApplicationインスタンスからバケツリレーするのもしんどいです。 実はVueの状態管理で定番のVuexはVueの外側でも普通に使えます。 こんな感じで雑にストアを作って... /src/store/index.ts store/index.ts export default createStore<State>({ state: { system: { initialTapped: false }, stageSetting: { width: 0, height: 0, ... } ... } }) Pixiのスプライトから参照するだけ。もちろんwatchやcomputedも使えます。 /src/sprites/Tama.ts Tama.ts // ゲーム状態の監視 watch( () => store.state.game.play, (newVal, oldVal) => { if (newVal === 'over') { this.gameOverMotion() } if (oldVal === 'over') { this.stepMotion() } } ) まあこれも結局は「ちょっとおしゃれなグローバル変数」に過ぎないんだけど、Vue側と決まったルールで状態の共有ができるのは悪くないです。 ポイント:GSAPのtweenアニメーションをasync/awaitに使う GSAPといえばWebのTweenアニメーションライブラリとしてはおそらく最強で、まあ多分普通の用途でできないことはそうそうないはず。 なのでそのまま素直に使っても良いのですが、1年目にtweenクラスの実装でやったように、できることならアニメーションはasync/awaitで綺麗に書きたい・・・ということで、ちょっとだけラップしてasync/await中心でアニメーションを組み立てられるようにしています。 たまさんのジャンプモーション private async jumpMotion() { // 新しいモーションを開始する = 古いモーションはこの時点で中断 const mo = this.nextMotion('jump') // 予備動作 await all( mo.animate(cont, { scaleY: 0.75, angle: 15 }, 0.15, Sine.easeOut), // 本体 mo.animate(amFr, { angle: -40 }, 0.15), // 腕手前 mo.animate(amBk, { angle: -30 }, 0.15), // 腕奥 mo.animate(lgBk, { angle: 0 }, 0.15), // 脚奥 mo.animate(lgFr, { angle: 0 }, 0.15) // 脚手前 ) await all( // 本体ジャンプ run(async () => { await mo.animate(cont, { scaleY: 1.1, y: -1000 }, 1.6, Cubic.easeOut) await mo.animate(cont, { scaleY: 1.0, y: 0, angle: 0 }, 2.5, Bounce.easeOut) }), // 腕振り手前 run(async () => { await mo.animate(amFr, { angle: 50 }, 1.3) await mo.animate(amFr, { angle: 0 }, 1.0) }), // 腕振り奥 run(async () => { await mo.animate(amBk, { angle: 30 }, 1.3) await mo.animate(amBk, { angle: 0 }, 1.0) }), // 足振り run(async () => { await mo.animate(lgBk, { angle: -30 }, 1.2) await mo.animate(lgBk, { angle: 0 }, 0.9) }) ) store.dispatch('tamaJumpEnd') mo.alive && this.defaultMotion() } 上の例で「モーション」って呼んでるものの本体がこれ↓ /src/logics/animate.tsのAnimatorクラス Animatorクラス(抜粋) export class Animator { /** * キャンセル可能なアニメーションの管理インスタンスを作成します。 * @param canceller キャンセルすべきかどうかを返すcomputedプロパティ。一度でもtrueになるとその時点で実行中のアニメーションを中断し、以後のアニメーションを全て無視します。 */ constructor(canceller?: ComputedRef<boolean>) { ... } /** * アニメーションを実行します。すでにキャンセルされている場合には何も起こりません。 * また、実行を開始した後でキャンセルが成立した場合、アニメーションは途中で打ち切られます。 * 実行されなかった場合及び、実行が打ち切られた場合にもPromiseはresolveになります(rejectはされません)。 */ async animate(...params: Parameters<typeof animate>) { ... } } 基本的にGSAPのtweenをラップしているだけなんだけど、コンストラクターにVueのcomputedを指定することで、「所定の条件を満たさなくなったらアニメーションを打ち切る」動作ができるようにしています。Vuexストアの状態が変わったらアニメーションを切り替える...みたいなことができるわけです。便利!(自賛) こんな感じでVueの機能は結構Vueコンポーネントの外側でも便利に使えるものがあるので、活用するとPixiJSだけの開発に比べて辛さが緩和できる...はず。 おまけ:3年間でコード量はどれくらい増えた? なんか1年目より明らかにハードワークになってる気がするので確認してみた。 ↓でざっくり確認。余談だけどclocがnpxで使えることにこの間まで全然気づかなかった npx cloc mezashi/src npx cloc mezashi2/src npx cloc mezashi21/src ファイル数 Language 1年目 2年目 3年目 Vuejs Component 11 21 12 JavaScript 9 0 0 TypeScript 0 22 71 GLSL 0 0 3 合計 20 43 86 ステップ数 Language 1年目 2年目 3年目 Vuejs Component 985 2183 1127 JavaScript 234 0 0 TypeScript 0 442 3704 GLSL 0 0 110 合計 1219 2625 4941 見事に毎年2倍になってますね。。これは単に毎年前回以上のこだわりを入れようとして肥大化しただけって見方もあるにはあるのですが、それ以上に使う技術が変わったことで「ざっくり書けるコードの上限」が上がったと見ることもできそうです。 個人開発を長くやっている方ならなんとなくわかってもらえると思うのですが、同じスキルレベルの人が同じアーキテクチャで無計画にコードを書くと、大体同じくらいのボリュームで破綻が見えてきます。 私の場合、生のJS(バニラJS)だと大体1000-1500行くらい書くとヤバい空気が漂ってくるのですが、それがTSを使ったりPixiやGSAPの仕組みに乗っかることで数倍までは無理なく伸ばせている...とも言えそうです(もちろん3年でスキルアップした部分もあると信じたい...)。 まとめ そんなわけで今年もなんとか2/22にゲームをリリースすることができました。 来年は...あるかなぁ...。。来年のことはわからないけど、同じようなテーマでも何回も作っては崩しを繰り返しているとなかなか面白い結果が見えてくることがわかりました。ゆるゆる継続していけるといいですね 今年も同じこと言うけど、Vueでゲーム作るの面白いよ!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

正規表現クロスワードを遺伝的アルゴリズムで解こうとしてあきらめた話

はじめに 1週間ほど前、社内のチャットで、こんなクロスワードパズルがあるよ!とリンクが流れてきました。 なんじゃ、これは! おもしろそうすぎる! さっそく、午後を半休にして、というわけにはいかなかったので、夜にがんばってみました。 やってみるとわかると思いますが、解いているときの感覚は、通常のクロスワードパズルと違って、お絵かきロジック(ノノグラム)や、マインスイーパーをやってるような感じでした。 結局夜にあまり時間が取れず、2晩かけて解いたのですが、ふと、こんなの、遺伝的アルゴリズムとかで推測させれば楽勝じゃね? という考えが浮かび、試してみました。 設計方針 得意なPythonで書き直そうかとも思ったのですが、画面でうにょうにょ動く方が楽しそうなので、元のソースを改造してみることにします。 遺伝的アルゴリズムとは Wikipediaによると、 あらかじめ N 個の個体が入る集合を二つ用意する。以下、この二つの集合を「現世代」、「次世代」と呼ぶことにする。 現世代に N 個の個体をランダムに生成する。 評価関数により、現世代の各個体の適応度をそれぞれ計算する。 ある確率で次の3つの動作のどれかを行い、その結果を次世代に保存する。 個体を二つ選択(選択方法は後述)して交叉(後述)を行う。 個体を一つ選択して突然変異(後述)を行う。 個体を一つ選択してそのままコピーする。 次世代の個体数が N 個になるまで上記の動作を繰り返す。 次世代の個体数が N 個になったら次世代の内容を全て現世代に移す。 以降の動作を最大世代数 G 回まで繰り返し、最終的に「現世代」の中で最も適応度の高い個体を「解」として出力する。 ということだそうです。 仕様 まずは、この流れをベースに実装してみることにします。 途中経過が見えた方が楽しいので、「現世代」の中で最も適応度の高い個体を画面に表示することにします。 元ソースの研究 まず、キーボードの入力が前提になっているので、画面表示は自前で用意する必要がありそうです。 「評価関数」は成立している正規表現の数を数えればいいでしょう。 checkRules()という関数があるので、これを改造することにします。 実装 データの表示 こんな感じで実装してみました。 sizeはglobal変数のようでしたのでそのまま使います。 function draw(data) { var rows = data.rows var i, j, id, val; for (i = 0; i < size; i += 1) { for (j = 0; j < rowSize(i); j += 1) { id = '#cell_' + i + '_' + j val = (rows[i] && rows[i][j] || '') if (val === '?') { val = '' } $(id).val(val) } } } ランダムなデータの取得 こんかいは、アルファベット大文字だけのようなので、アルファベット大文字からなるランダムな盤面を返す関数を作成します。 function getRandomChar() { return String.fromCharCode('A'.charCodeAt(0) + Math.random() * 26) } function getRandomData() { var i, j var data = { rows: [] } for (i = 0; i < size; ++i) { var row = [] for (j = 0; j < rowSize(i); ++j) { row[j] = get_random_char() } data.rows[i] = row } return data } 評価関数 これは、もともとある、checkRules()を改造します。 user_dataというglobal変数しかチェック出来ない極悪仕様なので、引数にデータを取れるようにします。 また、常に、結果の画面表示を変更してしまうので、制御できるようにします。 function checkRules(data=user_data, show=true) { var ii; var debug = []; var match_num = 0; function check(str, axis, idx) { var rule = board_data[axis][idx]; var regex = new RegExp(rule); var match = str.match(regex); var ret; if (match && match[0] === str) { if (show) { $('#rule_' + axis + '_' + idx).removeClass('nomatch'); $('#rule_' + axis + '_' + idx).addClass('match'); } ret = 1; } else { if (show) { $('#rule_' + axis + '_' + idx).removeClass('match'); $('#rule_' + axis + '_' + idx).addClass('nomatch'); } ret = 0; } debug.push(axis + idx + ': ' + str + (match ? ' (match)' : '')); return ret } for (ii = 0; ii < size; ++ii) { var str = ''; for (jj = 0; jj < rowSize(ii); ++jj) { str += data.rows[ii][jj]; } match_num += check(str, 'y', ii); str = ''; for (jj = 0; jj < size; ++jj) { var i = jj; var j = ii; if (jj > mid) { j -= (jj - mid); } if (data.rows[i][j] !== undefined) { str += data.rows[i][j]; } } str = strReverse(str); match_num += check(str, 'x', ii); str = ''; for (jj = 0; jj < size; ++jj) { var i = jj; var j = ii; if (jj < mid) { j -= (mid - jj); } if (data.rows[i][j] !== undefined) { str += data.rows[i][j]; } } match_num += check(str, 'z', ii); } $('#debug').html(debug.join('<br/>')); return match_num } テスト 試しに動かしてみます。 data = getRandomData() draw(data) console.log(checkRules(data, true)) consoleには4が出力されました。 いい感じです。 N個のランダムな初期データを作成 現世代のデータN個を作成します。 データuser_dataとそれぞれの適応度matchを持つ配列を作成します。 それをsolveMainに渡して、そちらで判定、描画、次世代の計算をおこなうことにします。 const N = 200 function solve() { var user_datas = [] for (var i = 0; i < N; i++) { data = getRandomData() user_datas.push({ user_data: data, match: checkRules(data, false) }) } solveMain(user_datas) } 描画と判定、次世代への繰り返し ループで回したいところですが、ループで回してもブラウザが描画してくれませんので、setTimeout()を使ってG世代まで、または、解が見つかるまで繰り返します。 最も適応度の高いもののみを描画します。 次世代のデータはgetNextGen()で取得することにします。 function getBestData(user_datas) { var best_data var best_match = 0 for (var data of user_datas) { if (data.match > best_match) { best_match = data.match best_data = data.user_data } } return { user_data: best_data, match: best_match } } const G = 50 var g = 1 function solveMain(user_datas) { finish = false best = getBestData(user_datas) $("#gen").text(g) draw(best.user_data) checkRules(best.user_data, true) if (best.match == size * 3) { console.log("solved!") finish = true } if (g < G && !finish) { g += 1 next_user_datas = getNextGen(user_datas) window.setTimeout(() => solveMain(next_user_datas), 300); } else { console.log(g) $("#gen").val(g) } } 次世代のデータの作成 次世代の要素として、適応度の高いものを高い確率で使うために、「ルーレット選択」を利用します。 簡単に言えば、たわしの部分が大きく、パジェロの部分が小さくなっている、アレです。たわしの適応度が高い場合、そのようになります。 getNextGenでは、ルーレットを作成し、データがN個になるまで遺伝子組み換えなどを繰り返します。 遺伝子組み換えはgetNextDataでおこないます。 function getNextGen(user_datas) { var best_data var best_match = 0 var next_user_datas = [] // Make a roulette (The more match has the more win) var roulette = [] user_datas.forEach(arg => { for (var i = 1; i <= arg.match; i++) { roulette.push(arg.user_data) } }) for (var n = 0; n < N;) { for (var new_data of getNextData(roulette)) { match = checkRules(new_data, false) next_user_datas.push({ user_data: new_data, match: match }) n += 1 if (n >= N) break } } return next_user_datas } 遺伝子組み換え 確率により、二点交叉、一様交叉、突然変異、ただのコピーをおこないます。 二点交叉は、端の方が変わりにくいので、終端と先端をつなぐようにしてみました。 const mutation_rate = 0.3; const two_point_crossover_rate = 0.3; const uniform_crossover_rate = 0.3; const copy_rate = 1.0 - mutation_rate - two_point_crossover_rate - uniform_crossover_rate; function deep_copy(o) { return JSON.parse(JSON.stringify(o)) } function swap(data1, data2, i, j) { tmp = data1.rows[i][j] data1.rows[i][j] = data2.rows[i][j] data2.rows[i][j] = tmp } function getNextData(roulette) { var ret = [] var rand = Math.random() var new_data, new_data2 if (rand < copy_rate) { // Copy new_data = deep_copy(roulette[Math.floor(Math.random() * roulette.length)]) // do nothing ret.push(new_data) } else if (rand - copy_rate < mutation_rate) { // Mutation new_data = deep_copy(roulette[Math.floor(Math.random() * roulette.length)]) var swap_num = Math.floor(Math.random() * size) for (i = 1; i <= swap_num; i++) { i1 = Math.floor(Math.random() * size) j1 = Math.floor(Math.random() * rowSize(i1)) new_data.rows[i1][j1] = getRandomChar() } ret.push(new_data) } else { tmp = roulette[Math.floor(Math.random() * roulette.length)] new_data = deep_copy(tmp) do { tmp2 = roulette[Math.floor(Math.random() * roulette.length)] } while (tmp2 == tmp) new_data2 = deep_copy(tmp2) if (rand - copy_rate - mutation_rate < uniform_crossover_rate) { // Uniform Crossover for (i = 0; i < size; i++) { for (j = 0; j < rowSize(i); j++) { if (Math.random() < 0.5) { swap(new_data, new_data2, i, j) } } } } else { // Two Point Crossover i1 = Math.floor(Math.random() * size) j1 = Math.floor(Math.random() * rowSize(i1)) i2 = Math.floor(Math.random() * size) j2 = Math.floor(Math.random() * rowSize(i2)) if (i1 == i2) { if (j1 <= j2) { for (j = j1; j <= j2; j++) { swap(new_data, new_data2, i1, j) } } else { for (j = j1; j < rowSize(i1); j++) { swap(new_data, new_data2, i1, j) } for (i = i1 + 1; i < size; i++) { for (j = 0; j < rowSize(i); j++) { swap(new_data, new_data2, i, j) } } for (j = j2; j >= 0; j--) { swap(new_data, new_data2, i1, j) } for (i = i1 - 1; i >= 0; i--) { for (j = 0; j < rowSize(i); j++) { swap(new_data, new_data2, i, j) } } } } if (i1 <= i2) { for (j = j1; j < rowSize(i1); j++) { swap(new_data, new_data2, i1, j) } for (i = i1 + 1; i < i2; i++) { for (j = 0; j < rowSize(i); j++) { swap(new_data, new_data2, i, j) } } for (j = j2; j >= 0; j--) { swap(new_data, new_data2, i2, j) } } else { for (j = j1; j < rowSize(i1); j++) { swap(new_data, new_data2, i1, j) } for (i = i1 + 1; i < size; i++) { for (j = 0; j < rowSize(i); j++) { swap(new_data, new_data2, i, j) } } for (j = j2; j >= 0; j--) { swap(new_data, new_data2, i2, j) } for (i = i2 - 1; i >= 0; i--) { for (j = 0; j < rowSize(i); j++) { swap(new_data, new_data2, i, j) } } } } ret.push(new_data) ret.push(new_data2) } return ret } 動作確認 3x3で確認 元のソースに、3x3のデータも入ってたので、これで試してみます。 おーーー! なかなかいい感じです。 13x13で確認 おー!すげぇ!なんかかっこいいです! でも、あまり、解けている感じがしません。 世代が足りないのでしょうか。 50000世代目です。 だいぶ進みましたが、この後続けてもほとんど進展がありませんでした。 考察 なんとなく眺めていて感じた感想なのですが、どうも、.*や([^X]|XCC)*の何でもマッチする系とは相性が悪いようです。 今回作成した評価関数は、単純に成立する正規表現の数なので、例えば、([^X]|XCC)*の場合、AAAAAAAAAAAは適応度が1で、XCAAAAAAAAAの適応度は0になります。 せっかく、XCCができるかも知れないチャンスが生かされないということです。 また、[RC]*のような特定の文字のみ許す系も、特定の文字以外が1文字でもあれば評価は0です。つまり、この[RC]*が成立するのは、奇跡的にRかCの文字のみで構成されたときだけで、おそらく、この行が成立することはないでしょう。 正規表現の評価関数が、0/1ではなく、0.3くらい合ってる、というようなふうにできれば、いい線行くのではないかと思うのですが、それは簡単ではなさそうですし、そこまでできるなら、別の方法で答えが出せそうな気がします。 結論 頑張って、自力で解きましょう。 おまけ 今回のコードは においてあります。 feature/solverブランチです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む