20210720のJavaScriptに関する記事は16件です。

javascriptで自分のQiita記事一覧を得る(1)

Qiitaは人のためならず 自分は誰のためでもない、未来の自分のためにQiitaの記事を書いています。 最初のうちは結構いい感じに機能していましたが、記事が増えるにつれ、マイページから自分の記事を探すのがつらくなってきました。 で、自分の記事一覧を表示するスクリプトを用意しました。 ローカル環境でも動作します。 myindex.htmlという空のファイルを作成し、下記の内容をコピペしたらダブルクリックで動作します。 [myindex.html] <!DOCTYPE html> <html> <head><title>user</title> <script> const USER_ID='ELIXIR'; // 自分のuser_idに変えてください var resArr = []; function onload(){ document.title=USER_ID; getUser(); } function getUserCB(){ getData(Math.floor((this.response["items_count"]+99)/100)); } function getUser(){ resArr = []; var req = new XMLHttpRequest(); req.responseType="json"; req.addEventListener("load", getUserCB); req.open('GET', 'https://qiita.com/api/v2/users/'+USER_ID, true); req.send(); } function getDataCB(){ resArr=resArr.concat(this.response); if(this.page>1){ getData(this.page-1); }else{ dispData(resArr); } } function getData(_page=1){ var req = new XMLHttpRequest(); req.responseType="json"; req.page=_page; req.addEventListener("load", getDataCB); req.open('GET', 'https://qiita.com/api/v2/users/'+USER_ID+'/items?per_page=100&page='+_page, true); req.send(); } function dispData(_resArr){ var result = document.getElementById('iResult'); var str=""; for(i=0;i<_resArr.length;++i){ str += '<a href="'+_resArr[i]['url']+'" target="_blank">'+ _resArr[i]['title']+'</a><br/>'; } result.innerHTML += str; } </script> </head> <body onload="onload()"> <div id="iResult"> </div> </body> </html> 結果: 今回はとりあえず記事一覧を表示しています。 次回以降で、タグでフィルタリングしたりできるようにしたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptを基本からまとめてみた【ラジオボタンが選択か非選択かを取得】【随時更新】

はじめに 学習するに至った経緯 2020年より、未経験からエンジニアへの転職を目指し、某プログラミングスクールへ通う。 入学後、『Ruby』を未経験から学ぶ人が多いのと『Ruby』の求人が思っていた以上に少ないので、 卒業後、フロントエンドのエンジニアを目指す事に。 Javascriptの学習した事を言語化し、認識の深化による備忘録として記載。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GASでお手軽なAPIを作ってJSで叩いてみる

備忘録ついでに書いておきます。 無料で簡単にJSONを返してくれるAPIを作りたかったので、GAS(Google Apps Script)を利用してみました。全体的な流れとしては、weblioの英和辞書で、検索した単語の意味一覧をJSONで返すAPIを作り、それをローカルのJSから取得します。 GASでスクレイピング まずはGASへアクセスして新規プロジェクトを立ち上げましょう。そして、スクレイピングするために「Parser」というライブラリを導入します。「ライブラリの追加」から、スクリプトIDに「1Mc8BthYthXx6CoIz90-JiSzSafVnT6U3t0z_W3hLTAX5ek4w0G_EIrNw」で検索して追加することができます。 次に、スクレイピングする対象であるweblioの構造を見ていきます。 まず、URLに注目するとhttps://ejje.weblio.jp/content/(単語名)で、調べたい単語の意味一覧にに飛べるようです。 また、ページのソースを見ると、<div class=level0>のタグの中に意味が格納されていることが分かりました。 構造が大体わかったので、実際にスクレイピングしてみます。 function doGet(e) { // スクレイピング対象のurlを定義 let url = `https://ejje.weblio.jp/content/${e.parameter.word}`; // HTMLの取得 let html = UrlFetchApp.fetch(url).getContentText(); // 要素の抽出 let data = Parser.data(html) .from('<div class=level0>') .to('</div>') .iterate(); } ${e.parameter}でGETリクエスト時のパラメータを受け渡すことができます。渡す際はURLの末尾に?word=(検索したい単語)を追加してアクセスします。 html要素の抽出はUrlFetchApp.fetch(url).getContentText();で行い、同一のタグが複数ある場合はParser.data(html).from(タグ名開始).to(タグ名終了).iterate()で配列にまとめることができます。 スクレイピングしたデータをJSONにして返す 前項のdataではまだ、不要なタグや配列の形式のままになっているので、正規表現で不要部分を削除する作業と、データを後で扱いやすいようにするためにオブジェクト形式にしてキーを追加する作業を行いたいと思います。 まずは正規表現を使ってタグを消し、リストにまとめていきます。 const dataList = [] for (const content in data) { dataList.push(data[content].replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'')); } 次に、dataListをオブジェクトにして一つずつキーを追加していきます。 const newDataList = dataList.map((word, key) => ({key, word})); 最後に、JSON形式に変換して返します。 const json_data = JSON.stringify(newDataList); return ContentService.createTextOutput(json_data).setMimeType(ContentService.MimeType.JSON); 保存したら「新しいデプロイ」から「ウェブアプリ」を選択し、アクセスできるユーザーを「全員」にして保存してください。 すると、このようなURLが発行されます。 コピーして末尾に?word=englishを追加して飛んでみます。 無事、「english」の意味だけ抽出されました!(品詞名のノイズが入っていますが…) Javascriptから取得してみる 意味を抽出してJSONを返してくれるAPIは完成したので、今度はローカルのJSからフェッチしてみます。※下ではReactを使用していますが、関数部分は生のjsでも同じはずです const [answerList, setAnswerList] = useState([]); const [word, onChangeWord] = useState(''); const url = 'https://script.google.com/macros/s/xxxxx/exec'; // urlに検索したい単語の文字列を追加してjsonをフェッチする const fetchAnswer = async () => { const submitUrl = url + '?word=' + word; return fetch(submitUrl) .then(res => res.json()); } // 検索ボタンを押したときにfetchAnswer関数を実行し、リストに入れる const onPressFetch = () => { (async() => { setAnswerList(await fetchAnswer()); })(); } // 省略 <View> <Input onChangeText={onChangeWord} value={word} /> <Button title="検索" onPress={onPressFetch} /> </View> Inputに入力された文字をパラメータとしてGASに渡し、GASが返却したJSONをfetchAnswerでフェッチしています。フェッチしたものをリストに入れて良い感じに表示すると下のようになりました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

鶏(async)と卵(await)、どちらが先か?

「[JS]なぜawaitはasync関数の中にしか書けないのか」の記事の補足というか、おまけ記事です。 鶏と卵 多くの人は、「awaitを使いたい」がまず先にあると思います。 const data = await doAjax(); 上記の書き方に「これは便利そうだぞ?」とビビッとくるわけです。 しかしその後、次のルールを知って「ん? なんかめんどくさそうだな?」となります。 awaitの先は、Promiseを返す関数か、async関数でなくてはならない そして「なるほど、awaitは、戻ってくるPromiseの解決を待ってくれるわけか」と理解しますが、併せて「async関数ってなんだ?」となります。 そしてasync関数について調べると、次のことが分かります。 async関数は、関数の返り値をPromise.resolve()で包みます。 「どういうこと?」という感じです。 例えば、"hello"と返すだけの関数があったとします。 function hello(){ return "hello"; } const msg = hello(); console.log(msg); 出力 hello このhello関数の戻り値を、Promise.resolve()で包んでみます。 resolveされたPromiseが返るので、thenで受けなくてはなりません。 function test(){ return Promise.resolve("hello"); } test().then((msg)=>{ console.log(msg) }); 出力 hello これを自動的にやってくれるのがasync関数だ、ということです。 async function test(){ return "hello"; } test().then((msg)=>{ console.log(msg) }); 「なるほど。それで、それは何のためにそうするんだ? awaitからはこの関数しか呼び出せないというのはどういうことだ?」という感じですが、asyncの中でawaitを使うと意味がわかってきます。 async function test(){ const msg = await getMessageAsync(); return msg; } test().then((msg)=>{ console.log(msg) }); getMessageAsync()は、内部でサーバにメッセージを問い合わせる非同期関数です。呼び出すとPromiseを返し、サーバからの問い合わせが返った時にそのPromiseを完了状態にします。 return msgが実行されるのはawait getMessageAsync()のPromiseが完了になった後ですから、それまでは、test()が返すPromiseは待機状態のままです。そして、getMessageAsync()が完了になった後、return msgが、Promiseを完了状態にしてmsgを渡します。 こういった、本来ややこしいPromiseの管理を、asyncとawaitが肩代わりしてくれるわけですが、ここで重要なのは、「awaitがなければasyncにはほぼ意味がない」ということです。 言い換えると、awaitを中で呼び出す為のasync関数であって、async関数を呼び出す為のawaitではないということになるでしょう。 この理解の前に「awaitで呼び出せるのはasync関数」と聞くと、まるで「asyncが先にあって、それを扱う為のawait」のように錯覚してしまいますが、そうではなく、awaitはあくまでもPromiseを同期的な書き方で扱うためのものです。そしてawaitを適切に処理する為に、asyncが存在します。 async関数はPromiseを返すので、結果的にawaitはasync関数も扱える、というだけですね。非常に便利な仕組みだと思います。 ちなみに、何も返さない関数をasyncにしたらどうなるかというと、ちゃんとPromise.resolve()が返ります。 async function test(){ } test().then((msg)=>{ console.log("done!") }); なので、次のようにawaitを次々と呼び出すような場合も、単にそのまま処理を終わればOKです。 async function test(){ const data1 = await longProcAsync1(); const data2 = await longProcAsync2(data1); const data3 = await longProcAsync3(data2); await lastProcAsync(data3); } test().then((msg)=>{ console.log("done!") }); というわけで、長くなりましたが、結論は以下の通りです。 鶏(async)と卵(await)は、卵(await)が先である。 awaitがやりたいからのasync、ということでした。 おまけ:asyncとは結局何か 最初にasyncについてこう書きました。 async関数は、関数の返り値をPromise.resolve()で包みます。 しかし、前回の記事を読めばわかるように、実際はそれだけでは全然なくて、 asyncは、それを付けた関数をawaitからawaitまでのブロックで分割したジェネレータに変換し、各awaitが待つPromiseが完了状態になった時に次のブロックへと処理を進め、関数が終了した時に結果をPromise.resolve()で包んで返します。 ということでした。async、すごいやつです。 おまけ2:なぜawaitを最上位コードに書けないのか? (追記 2021/07/21)なんと、もうすぐ最上位にawaitを書けるようになるかもしれないそうです(一部ブラウザは実装済)。コメント欄で教えて頂きました。感謝! ちなみに、そろそろasync/awaitにも慣れてきて、次のように書きたくなってきたかもしれません。 async function test(){ const msg = await getMessageAsync(); return msg; } const msg = await test(); console.log("done") しかしこれはエラーになって実行できません。なぜならjavascriptコンパイラは、async関数に入っていないawait test()をジェネレータに変換できない為です(最上位のコード全体をまとめて1つのジェネレータに変換すれば可能かもしれませんが、さすがにそれはやりすぎということでしょう)。 これなら大丈夫です。 async function test(){ const msg = await getMessageAsync(); return msg; } async function main(){ const msg = await test(); console.log("done") } main();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

plunkerでvue その50

概要 plunkerでvueやってみた。 落ち物ゲーム、作ってみた。 参考にしたページ 写真 最初に、ランダムに色を決める。 function getBalls() { var balls = []; let sn = 0; for (let i = 0; i < 5; i++) { for (let j = 0; j < 5; j++) { let ballInfo = { iro: ballClass[Math.floor(Math.random() * ballClass.length)], x: j, y: i, sn: sn, df: 0, } balls.push(ballInfo); sn++; } } return balls }; クリックされたら、隣り合う石を上下左右1ずつ取得し同色か否かを判定 同色の場合、その同色を起点とし再度隣り合う石を1ずつ取得し同色か否かを判定し、消す。 breakCheckRecursive(startingBall, selectedClassName) { const leftBall = this.balls.find(b => b.x === startingBall.x - 1 && b.y === startingBall.y); this.targetDelBall(leftBall, selectedClassName); const rightBall = this.balls.find(b => b.x === startingBall.x + 1 && b.y === startingBall.y); this.targetDelBall(rightBall, selectedClassName); const topBall = this.balls.find(b => b.x === startingBall.x && b.y === startingBall.y - 1); this.targetDelBall(topBall, selectedClassName); const bottomBall = this.balls.find(b => b.x === startingBall.x && b.y === startingBall.y + 1); this.targetDelBall(bottomBall, selectedClassName); }, targetDelBall(delBall, selectedClassName) { if (delBall && delBall.iro === selectedClassName && delBall.df === 0) { let ball = this.balls.find(b => b.x === delBall.x && b.y === delBall.y) ball.df = 1; ball.iro = "item-0"; this.breakCheckRecursive(delBall, selectedClassName); } }, 石が消えた後、空中に浮いている石を下方向へ詰める処理を行わないといけません。 downArea() { var bballs; var j; var k; var b0; var b1; bballs = this.balls.filter(b => b.df === 1); while (bballs.length > 0) { if (bballs[0].y == 0) { k = 0; b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); //b1.sn = -1; b1.iro = "item-0"; b1.df = 0; } if (bballs[0].y == 1) { j = 1; k = 0; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; k = 0; b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); //b1.sn = -1; b1.iro = "item-0"; b1.df = 0; } if (bballs[0].y == 2) { j = 2; k = 1; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 1; k = 0; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; k = 0; b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); //b1.sn = -1; b1.iro = "item-0"; b1.df = 0; } if (bballs[0].y == 3) { j = 3; k = 2; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 2; k = 1; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 1; k = 0; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; k = 0; b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); //b1.sn = -1; b1.iro = "item-0"; b1.df = 0; } if (bballs[0].y == 4) { j = 4; k = 3; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 3; k = 2; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 2; k = 1; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 1; k = 0; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; k = 0; b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); //b1.sn = -1; b1.iro = "item-0"; b1.df = 0; } bballs = this.balls.filter(b => b.df === 1); } }, 縦列が全て消えた時に列ごと、右に詰めないといけない rightArea() { let xAxis = 4; while (xAxis >= 0) { let xBalls = this.balls.filter(b => b.x === xAxis && b.iro === "item-0"); if (xBalls.length > 4) { //alert("ok") if (xAxis == 1) { this.move(1) this.cls() } if (xAxis == 2) { this.move(2) this.move(1) this.cls() } if (xAxis == 3) { this.move(3) this.move(2) this.move(1) this.cls() } if (xAxis == 4) { this.move(4) this.move(3) this.move(2) this.move(1) this.cls() } } xAxis--; } }, move(l) { var i; for (i = 0; i < 5; i++) { var j = l; var k = l - 1; var b0 = this.balls.find(b => b.x === j && b.y === i); var b1 = this.balls.find(b => b.x === k && b.y === i); b0.df = b1.df; b0.iro = b1.iro; } }, cls() { var i; for (i = 0; i < 5; i++) { var k = 0; var b1 = this.balls.find(b => b.x === k && b.y === i); b1.iro = "item-0"; b1.df = 0; } }, 成果物 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptを基本からまとめてみた【ラジオボタンの値を取得・設定】【随時更新】

はじめに 学習するに至った経緯 2020年より、未経験からエンジニアへの転職を目指し、某プログラミングスクールへ通う。 入学後、『Ruby』を未経験から学ぶ人が多いのと『Ruby』の求人が思っていた以上に少ないので、 卒業後、フロントエンドのエンジニアを目指す事に。 Javascriptの学習した事を言語化し、認識の深化による備忘録として記載。 HTMLでラジオボタンを作成する HTML でラジオボタンを表示するには input 要素で type 属性に radio を指定する。 <input type="radio"> ⚪︎ ラジオボタンの後にラベルを表示する場合は label 要素と組み合わせる。 <label><input type="radio">オレンジ</label> ⚪︎ オレンジ ラジオボタンは複数のラジオボタンの中から一つを選択する使い方が一般的。複数のラジオボタンを同じグループにまとめるには、それぞれのラジオボタンの name 属性に同じ値を設定する。また、どのラジオボタンが選択されたのかを後で取得する為、それぞれのラジオボタンの value 属性に異なる値に設定する。 <label><input type="radio" name="fruit" value="orange">オレンジ</label> <label><input type="radio" name="fruit" value="lemon">レモン</label> <label><input type="radio" name="fruit" value="strawberry">ストロベリー</label> ⚪︎ オレンジ ⚪︎ レモン ⚪︎ ストロベリー 初期値としていずれかのラジオボタンを選択した状態にするには、対象のラジオボタンに checked 属性を設定する。 <label><input type="radio" name="fruit" value="orange">オレンジ</label> <label><input type="radio" name="fruit" value="lemon" checked>レモン</label> <label><input type="radio" name="fruit" value="strawberry">ストロベリー</label> ⚪︎ オレンジ  ◉ レモン ⚪︎ ストロベリー ラジオボタンの値の取得と設定 同じグループのラジオボタンの中で現在選択されているラジオボタンを調べるには、 getElementsByName メソッドを使い指定した name 属性が設定されている要素の一覧を取得したあと、各要素に対して input 要素を表す HTMLInputElement オブジェクトの checked プロパティの値を参照する。 element.checked checked プロパティの値はチェックされていれば true、チェックされていなければ false にする。 実際には次のように記述します。 checked プロパティが true の要素が見つかったら value プロパティの値を取得する。 let elements = document.getElementsByName('fruit'); let len = elements.length; let checkValue = ''; for (let i = 0; i < len; i++){ if (elements.item(i).checked){ checkValue = elements.item(i).value; } } 同じグループ内のラジオボタンのいずれかを選択するには、先ほどと同じ手順で要素の一覧を取得した後、インデックスを指定して checked プロパティに対して true を設定する。 let elements = document.getElementsByName('fruit'); elements[2].checked = true; form要素内にラジオボタンがある場合の値の取得と設定 ラジオボタンがフォーム要素内にある場合は別の方法で選択されているラジオボタンを取得することができる。 <form> <label><input type="radio" name="fruit" value="orange">オレンジ</label> <label><input type="radio" name="fruit" value="lemon" checked>レモン</label> <label><input type="radio" name="fruit" value="strawberry">ストロベリー</label> </form> ⚪︎ オレンジ  ◉ レモン ⚪︎ ストロベリー 最初に getElementById などのメソッドを使って form 要素を取得する。 <form id="fruitbox"> ... </form> <script> let fruitbox = document.getElementById('fruitbox'); </script> form 要素に含まれる RadioNodeList オブジェクトを取得する。取得するにはフォームを表す HTMLFormElement オブジェクトの elements プロパティの値に対して対象のラジオボタンで設定している name 属性の値を指定すると RadioNodeList オブジェクトを取得できる。 <form id="fruitbox"> ... </form> <script> let fruitbox = document.getElementById('fruitbox'); radioNodeList = fruitbox.elements['fruit']; </script> RadioNodeList オブジェクトの value オブジェクトを参照すると、現在選択されているラジオボタンの値を取得することができる。 <form id="fruitbox"> ... </form> <script> let fruitbox = document.getElementById('fruitbox'); radioNodeList = fruitbox.elements['fruit']; let checkValue = radioNodeList.value; </script> form 要素に含まれるラジオボタンの中で指定したラジオボタンを選択するには、フォームを表す HTMLFormElement オブジェクトの elements プロパティの値に対してインデックスを指定するとラジオボタンの要素を取得できる。取得したラジオボタンの checked プロパティに対して true を設定する。 <form id="fruitbox"> ... </form> <script> let fruitbox = document.getElementById('fruitbox'); radioNodeList = fruitbox.elements[1].checked = true; </script> ラジオボタンのイベント処理 ラジオボタンでは新しく選択されたラジオボタンで change イベントが発生する。(選択されていた状態から選択されていない状態に変わっても change イベントは発生しない)。 <label><input type="radio" name="fruit" value="orange" id="fruitRadio1"> オレンジ</label> <label><input type="radio" name="fruit" value="lemon" id="fruitRadio2"> レモン</label> <script> function valueChange(){ // イベントが発生した時の処理 } let fruitRadio1 = document.getElementById('fruitRadio1'); fruitRadio1.addEventListener('change', valueChange); let fruitRadio2 = document.getElementById('fruitRadio1'); fruitRadio1.addEventListener('change', valueChange); </script> また form 要素内にラジオボタンがある場合、新しいラジオボタンが選択されると form にて change イベントが発生する。 <form id="travelbox"> <label><input type="radio" name="travel" value="okinawa">沖縄</label> <label><input type="radio" name="travel" value="hokkaidou">北海道</label> </form> <script> function valueChange(){ // イベントが発生した時の処理 } let travelbox = document.getElementById('travelbox'); travelbox.addEventListener('change', valueChange); </script> 参考サイト ラジオボタンの値をJavaScriptを使って取得・設定する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CakePHP4 javascript document.getElementがnullになる時の解消

Every Qiita #18 のんびり独学初学者の毎日投稿チャレンジ 18日目 今回は・・・ cakephpでjavascriptファイルでdocument.getElementで要素を取得しようとした時nullになってしまった。解消したので備忘録として投稿します。 ファイルを読み込むタイミングが違う!! headタグの中に$this->Html->script('ファイル名')と記載していたためそんな要素ねえよと言われていた・・・ bodyの直前に読み込む bodyの直前に$this->Html->script('ファイル名')とすることで、idが定義された後に探しにいくのでエラーが解消されました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kintoneのポータルにアプリを指定したソート順で表示する

ポータルにアプリを全部表示させたい ポータル画面の内容をアプリで管理しよう(classic-appの使い方) ~ kintoneのポータルカスタマイズ紹介#3 ~ 上記をもとに無事アプリをポータルに表示することができました。 が、アプリのソート順が指定できず、登録した順に表示されるので、ちょこっとJSをいじりました。 やったこと ポータル管理アプリのサブテーブル「リンク」に、ソート順(フィールドコード:order)を追加 classic-days2020 const fetchKintoneRecord = (appId, recordId) => { return kintone.api(kintone.api.url("/k/v1/record", true), "GET", { app: appId, id: recordId }).then((res) => { const sorted_sections = res.record.sections.value.sort((a, b) => { if (a.value.sectionId.value*1 > b.value.sectionId.value*1) { return 1 } else { return -1 } }) const sorted_links = res.record.links.value.sort((a, b) => { if (a.value.order.value*1 > b.value.order.value*1) { return 1 } else { return -1 } }) const sorted = { record: { ...res.record, sections: { value: sorted_sections }, links: { value: sorted_links } } } return sorted }) } // 元のソースはこれ // const fetchKintoneRecord = function (appId, recordId) { // return kintone.api(kintone.api.url("/k/v1/record", true), "GET", { app: appId, id: recordId }); // }; fetchKintoneRecordを少し変更し、セクションはセクションIDで、リンクはorderの順にソートするようにしました。 感想 そもそもポータル管理アプリにアプリの名前とURLを登録するところがめんどくさかった。。 でもこれで今後のメンテが楽になると信じて。 以上、超小ネタでした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

input type="date"で昨日以前を選択できないようにする方法

概要 <input type="date>"を用いることで、簡単にカレンダーによる選択を実装できる。 昨日以前を選択できないようにしたかったが、検索しても中々ヒットしなかった。 個人的な備忘録の意味でも、方法と流れをまとめてみた ▼使用スキル HTML、Vue.js 手順① HTML <input type="date" min="2021-07-18"> HTMLにinput type="date"を設置。min='XXXX-XX-XX'を指定することで、'XXXX-XX-XX'がより前の日付に関しては、選択できなくなる。例えば、2021-07-18とすれば、7/17以前の日付は、選択できないようになる。 デモコード 詳しくは公式のリファレンスをご確認ください。 手順➁ ユーザーがサービスを利用する日にちによって、'XXXX-XX-XX'の中身を変更するようにしたい。つまり、動的に管理したい。本記事では、Vue.jsにて実装していく。 HTML <div id="app"> <input type="date" v-bind:min="today"> </div> Vue.js var vue = new Vue ({ el: '#app', data: { today:'' }, created: function(){ this.today= new Date(); } }) ここまでのデモはこちら 解説 this.today= new Date();により、今日の日付を取得している。今日が7/18ならば、minには7/18が入る。しかし、このままでは正常に動作しない。上記のデモは確認してみて欲しい。 検証ツールを確認すると分かりやすいのだが、 この時点では、min=Tue Jul 20 2021 12:56:01 GMT+0900 (日本標準時)とminに入っている形式がおかしい。minが正常に動作するためには、min='XXXX-XX-XX'という形式になっている必要がある。 todayの形式を'XXXX-XX-XX'に変更する記述は以下になる。 手順➂ Vue.js created: function(){ let todaySet = new Date(); let YYYY = todaySet.getFullYear(); let MM = ('00' + (todaySet.getMonth()+1)).slice(-2); let DD = ('00' + todaySet.getDate()).slice(-2); this.today = YYYY + '-' + MM + '-' + DD }, 解説 恐縮ではあるが、getFullYear()等の詳しい解説は割愛させていただく。別途、公式のリファレンスや、他Qiita記事をご覧願いたい。 一点解説しておくと、下記コードの'00'及びslice(-2)がポイントである。下記部分がない場合、例えば今日が7/7だとすると、todayは2021-7-7という形式になる。下記部分があることで、2todayは2021-07-07となる。minが正常に動作するのは、2021-07-07の方だ。 let MM = ('00' + (todaySet.getMonth()+1)).slice(-2); let DD = ('00' + todaySet.getDate()).slice(-2); 完成形はこちらでご確認いただきたい。 https://jsfiddle.net/zxyaukLh/2/ 最後に 分かりにくい点があったら大変申し訳ないが、以上とさせていただく。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MonacaとNCMBで目安箱アプリを作ってみよう

こちらは目安箱アプリを作ってみよう【その1:画面の説明と匿名認証まで】 – モナカプレスと目安箱アプリを作ってみよう【その2:意見の投稿と一覧画面の作成】 – モナカプレスをハンズオン用に再編集したものです。 体験できるもの Monacaを使ったアプリ開発 Onsen UIを使ったネイティブアプリ風UIの作成法 NCMBの匿名認証 NCMBのデータストアへのデータ保存 利用している技術、ライブラリ このアプリで利用している技術やライブラリは次の通りです。 Onsen UI ニフクラ mobile backend ベースアプリ ベースアプリは NCMBMania/Monaca_Survey_App: Monacaの目安箱アプリ開発ハンズオンのベースです。 になります。下記URLをZipインポートとして指定してください。 https://github.com/NCMBMania/Monaca_Survey_App/archive/refs/heads/main.zip 仕様 このアプリの仕様、機能は次の通りです。 入力画面 目安箱に要望を投稿する画面です。この画面が最初に表示されます。入力項目は次の通りです。 要望 この画面ではログインしているかどうかを確認します。ログインしていなかった場合、匿名認証を実行します。 ご意見一覧画面 目安箱に投稿された要望を閲覧する画面です。通常ユーザは自分の投稿した要望だけを一覧できます。管理者グループのユーザは他のユーザの投稿も見られます。自分の投稿であれば、タップして編集画面に遷移します。編集画面は入力画面を利用しています。 要望データの権限 要望データは本人と管理者ユーザだけが閲覧できます。また、要望の編集が行えるのは本人のみです。 ニフクラ mobile backendのキーを取得 ニフクラ mobile backendにユーザ登録、またはログインしてアプリを作成します。その結果として、次の2つのキーが取得できます。 アプリケーションキー クライアントキー ライブラリのインストール ではここから開発を行っていきます。今回はOnsen UIのテンプレートを利用しています。外部フレームワーク(VueやReactなど)は用いず、素のJavaScriptのものを選択しています。 外部ライブラリとして、ncmbを追加します。JavaScriptライブラリなので、JS/CSSの追加と削除から行ってください。 読み込むファイルとして ncmb.min.js をチェックするのを忘れないで下さい。 NCMBの初期化 www/js/app.js を開いて、下記のようにニフクラ mobile backendを初期化します。上記で紹介したアプリケーションキーとクライアントキーを適用してください。 // NCMBの初期化 const applicationKey = 'YOUR_APPLICATION_KEY'; const clientKey = 'YOUR_CLIENT_KEY'; window.ncmb = new NCMB(applicationKey, clientKey); index.html の紹介 Monacaアプリで一番最初に読み込まれる www/index.html では、タブバーのOnsen UIとなっています。読み込むファイルは、意見の投稿を行う form.html と 閲覧を行う view.html となっています。 <ons-page> <ons-tabbar position="auto"> <ons-tab label="投稿" page="form.html" active> </ons-tab> <ons-tab label="閲覧" page="view.html"> </ons-tab> </ons-tabbar> </ons-page> 入力画面実装 入力画面 form.html のHTMLを下記に紹介します。基本的に日報の入力項目を表示する部分と、入力した内容を表示する #reports があるだけです。 <!-- 記載済み --> <ons-page> <ons-toolbar> <div class="center">入力フォーム</div> </ons-toolbar> <div style="text-align: center; margin-top: 60px;"> <ons-row> <ons-col> <input type="hidden" id="objectId" /> <textarea id="body" class="textarea" rows="5" cols="40" placeholder="ご意見を入力してください"></textarea> </ons-col> </ons-row> <p style="margin-top: 30px;"> <ons-button id="post">目安箱に投稿する</ons-button> </p> </div> </ons-page> JavaScriptについて JavaScriptでは画面を表示した時にログインされているかどうかをチェックします。この処理は入力画面が表示された際に呼ばれる処理の ons.getScriptPage().onShow の中に実装します。 // 記載済み /* この画面が表示される度に実行されるメソッド */ ons.getScriptPage().onShow = async function() { // 匿名認証実行 await loginCheck(); if (this.data && this.data.objectId) { const post = await fetchPost(this.data.objectId); this.querySelector('#body').value = post.body; this.querySelector('#objectId').value = post.objectId; } } ログイン状態の確認はニフクラ mobile backendの会員管理機能を利用します。まず ncmb.User.getCurrentUser() でログインしているユーザの情報を取得します。これが空の場合には未ログイン状態なので、匿名認証処理を実行します。 www/js/app.js に実装してください /* 認証状態の確認を行い、ログインしていなかった場合は匿名認証を実行する */ async function loginCheck() { const user = ncmb.User.getCurrentUser(); if (!user) { // 未ログインの場合 await ncmb.User.loginAsAnonymous(); return true; } try { // セッションの有効性を確認 await ncmb.DataStore('Test').fetch(); } catch (e) { // セッションが不正だった場合は匿名認証し直し await ncmb.User.loginAsAnonymous(); } return true; } 匿名認証はユーザIDやパスワードといった情報なしで認証機能を提供します。デバイス個別のIDを生成し、それを認証に利用します。IDはセッションが切れると別なものが生成されてしまうので注意してください。目安箱は匿名の方が入力しやすいので、今回は匿名認証を利用しています。 もしユーザ情報が取得できたとしても、そのセッションが有効かどうかは分かりません。それを確かめるために、適当なクラス(今回はTestクラス。存在しなくても大丈夫です)へリクエストします。これが失敗する場合にはセッションが無効(有効期限切れなど)となっています。その場合には再度匿名認証を実行します。 ニフクラ mobile backendの管理画面での作業 最後にニフクラ mobile backendの管理画面にて、管理グループを作成しておきます。管理画面の会員管理にてロールを作成します。今回は分かりやすいように admin としておきます。 この admin グループを表示している状態を会員を追加したり、既存会員のobjectIdを使ってグループに追加できます。この管理グループにいる場合、他のユーザのデータも見られるようになります。これは匿名認証のユーザでも指定できます。 また、アプリ設定画面にて匿名認証を有効にしてください。これはアプリ設定をクリックした後の基本の一番下に設定があります。 目安箱への投稿 匿名認証が終わったら、目安箱への投稿を実装していきます。これは投稿ボタン ons.getScriptPage().onInit を押した際のイベントとして実装していきます。 /* 初期化時に一度だけ実行されるメソッド */ ons.getScriptPage().onInit = async function() { this.querySelector('#post').onclick = () => postSurvey.bind(this)(); } /* 目安箱への投稿を行う */ async function postSurvey() { // この中に実装します } 処理の内容です。まず投稿に必要な情報を収集します。 // 実装済み const body = this.querySelector('#body'); const objectId = this.querySelector('#objectId').value; if (body.value.trim() === '') { ons.notification.alert('ご意見を入力してください'); return; } 次にニフクラ mobile backendのデータストアに保存する準備をします(別関数 insertPost として定義しています)。詳細はコメントを参照してください。注意点としてACL(アクセス権限)として、データを入力した本人は読み書き可能、adminグループに対して読み込み権限を付与しておきます。こうすることで、adminグループに所属しているユーザはデータ閲覧が可能になります。 async function insertPost(body, objectId) { // 投稿用のクラス(Post)を準備 const Post = ncmb.DataStore('Post'); // Postクラスのインスタンス(DBでいう行相当)を準備 const post = new Post(); // 本文を設定 post.set('body', body); // ACL(アクセス権限)の準備 const acl = new ncmb.Acl(); const user = ncmb.User.getCurrentUser(); acl .setUserReadAccess(user, true) .setUserWriteAccess(user, true) .setRoleReadAccess('admin', true); post.set('acl', acl); // 保存 await post.save(); } 後は保存処理を実行します。 // 実装済み try { await insertPost(body.value, objectId); ons.notification.alert('ご意見ありがとうございました!'); body.value = ''; } catch (e) { ons.notification.alert('エラーが発生しました。もう一度お試しください。'); } 目安箱の投稿表示 次に目安箱への投稿を表示する画面を作成します。これは view.html に実装します。HTMLは次のようになっています。 <ons-navigator id="nav"> <ons-toolbar> <div class="center">意見閲覧</div> </ons-toolbar> <div style="text-align: center; margin-top: 60px;"> <ons-list id="posts"> </ons-list> </div> </ons-navigator> この画面が表示されたタイミングで、投稿されているデータを取得して表示します。つまり ons.getScriptPage().onShow での処理になります。 /* この画面が表示される度に実行されるメソッド(実装済み) */ ons.getScriptPage().onShow = async function() { // 匿名認証実行 await loginCheck(); const posts = await loadPosts(); showPosts.bind(this)(posts); } 実際の処理は次のようになります。まずニフクラ mobile backendからデータを取得します。 async function loadPosts() { return await ncmb.DataStore('Post') .order('createDate') .fetchAll(); } そして取得したデータを ons-list-item で表示します。この時、自分に書き込み権限があるかどうかを判断して、それによって編集画面に遷移できるかどうかを判定しています。 // 実装済み function showPosts(posts) { const user = ncmb.User.getCurrentUser(); this.querySelector('#posts').innerHTML = ''; // データを順番に表示 posts.forEach(p => { // <ons-list-item>を準備 const i = document.createElement('ons-list-item'); // 本文を準備 let text = p.body; // 自分に書き込み権限があるかチェック if (p.acl[user.objectId] && p.acl[user.objectId].write) { // あれば編集用アイコンの表示 text = `${p.body} ✏️`; // 編集イベントの設定 i.setAttribute('tappable', true); i.onclick = () => { // form.htmlに遷移 this.querySelector('#nav').pushPage('form.html', {data: { objectId: p.objectId}}); } } // 画面に表示 i.appendChild(document.createTextNode(text)); this.querySelector('#posts').appendChild(i); }); } 自分が編集できるデータをタップした際には、投稿画面 form.html に遷移します。この時、ニフクラ mobile backendのデータ管理上のユニークIDであるobjectIdを次の画面に送っています。 投稿の編集処理 編集処理を実装する際には、すでに実装されている保存処理とうまく流用しながら実装していくのがいいでしょう。そうすることで余分な開発が減ります。まず、一覧画面から送られてきた投稿データを表示する部分を作ります。これは ons.getScriptPage().onShow の中に実装します。 HTMLのhiddenを使ってobjectIdを隠しておきます。こうすることで新規保存なのか更新なのかを区別します。 /* この画面が表示される度に実行されるメソッド */ ons.getScriptPage().onShow = async function() { // 匿名認証実行 await loginCheck(); if (this.data && this.data.objectId) { const post = await fetchPost(this.data.objectId); this.querySelector('#body').value = post.body; this.querySelector('#objectId').value = post.objectId; } } // 実装してください async function fetchPost(objectId) { return await ncmb.DataStore('Post') .equalTo('objectId', objectId) .fetch(); } 更新処理 新規保存と更新の違いは次の通りです。 新規の場合は save メソッド。更新の場合は update メソッド。 更新の場合は objectId あり。新規の場合はなし。 更新の場合は一覧画面に移動 これらの仕様を反映した保存処理が次の通りになります。詳細はコメントを参照してください。 // 更新してください async function insertPost(body, objectId) { const Post = ncmb.DataStore('Post'); const post = new Post(); if (objectId) { post.set('objectId', objectId); } post.set('body', body); const user = ncmb.User.getCurrentUser(); const acl = new ncmb.Acl(); acl .setUserReadAccess(user, true) .setUserWriteAccess(user, true) .setRoleReadAccess('admin', true); post.set('acl', acl); await (objectId ? post.update() : post.save()); } まとめ ここまでで目安箱アプリが完成となります。今回は次のような画面を持ったアプリを開発しました。 ご意見投稿画面 ご意見一覧画面 ご意見編集画面 また、ニフクラ mobile backendの次の機能を利用しました。 会員管理 匿名認証 データストア Postクラス データ登録 データ更新 データ検索 今回のデータの保存と取得、更新といった機能はどのようなアプリでも使える機能だと思います。また匿名認証はIDやパスワードの入力が不要な分、ユーザストレスの少ない認証なのでお勧めです。今後のアプリ開発に応用してください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

package.jsonにfilesを書かないあなたは、誰かを少しだけ不幸にしています

先に結論だけ package.jsonにfilesフィールドを指定すると、node_modulesに保存されるファイルサイズが減ります。 はじめに この記事は、npmのpackage.jsonに指定するフィールド、filesの指定方法と効用を解説、共有するためのものです。 想定する読者 この記事は、以下の読者を想定して書かれています。 JavaScriptの開発経験がある npmを使ったことがある npmやGitHubでnpmモジュールを公開している 想定する環境 この記事は、以下の環境を想定して書かれています。記事を読む前に、お手元の環境をご確認ください。 $ npm --version 7.15.1 filesフィールドとは filesフィールドとは、package.jsonファイル内の設定フィールドです。このフィールドはnpmパッケージがインストールされたとき、どのファイルをnode_modulesにコピーするかを設定します。 たとえば、以下のようなnpmパッケージ用のリポジトリがあるとします。 example-project/ ├ src/ │ ├ srcA.js │ └ srcB.js ├ lib/ │ └ bundle.js ├ esm/ │ └ module.js ├ __test__/ │ ├ testA.spec.js │ └ testB.spec.js ├ package.json ├ LICENSE ├ jest.config.js └ README.md package.jsonには、以下のようにfilesフィールドが設定されています。 ▼package.json { "name": "example-project", "files": [ "lib", "esm" ], } このnpmパッケージをインストールします。 npm install --save-dev example-project すると、node_modulesには以下のようにファイルがインストールされます。 node_modules/ └ example-project/ ├ lib/ │ └ bundle.js ├ esm/ │ └ module.js ├ package.json ├ LICENSE └ README.md filesフィールドに指定されていないファイルは、node_modulesにはインストールされません。 filesフィールドの挙動 filesフィールドの挙動を、公式ドキュメントから読み解いていきます。 ファイルの指定方法は? filesフィールドにはstring配列を指定します。stringの中身は.gitignoreなどでおなじみのglobパターンを指定します。 省略するとどうなるか filesフィールドは省略可能です。省略した場合["*"]として解釈されます。このパターンはすべてのファイルにマッチします。 .npmignoreと併用するとどうなるか .npmignoreとfilesを併用した場合、ルートディレクトリとサブディレクトリで挙動が異なります。 ルートディレクトリの場合 : filesフィールドが優先されます。 サブディレクトリの場合 : .npmignoreが優先されます。 指定に関係なく無視されるファイルと、指定しても含まれるファイル filesフィールドの設定にかかわらず、必ずパッケージに含まれるファイルと無視されるファイルがあります。詳しいリストは公式ドキュメントを参照してください。 無視されるファイル .gitディレクトリやpackage-lock.jsonなど、インストール先に影響を与えないファイルは無視されます。 含まれるファイル package.jsonやLICENCEなど、インストール先に影響を与えたり尊重されるべきファイルはfilesフィールドの設定にかかわらずパッケージに含まれます。 filesフィールドの効用 filesフィールドを指定することで、インストール先のnode_modulesディレクトリのファイルサイズを減らせます。 たとえば以下のようなファイルはパッケージの動作に影響を与えません。filesフィールドを設定して除外するべきです。 単体テストファイル バンドル / トランスパイル前のソースファイル バンドル / トランスパイルのための設定ファイル(webpack.config.jsやtsconfig.json) APIドキュメント サンプルコード 以上、ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Denoで安全にevalしたい

なんでそんなことしたいの? 某サイトをスクレイピングするとき必要だった。 Denoのスクリプト内で使うんじゃなくて実行結果を見たいだけなら 普通にdeno run sample.tsかecho "1 + 1" | deno --でいいと思います。 Denoに権限を与えてないから何もできないはずなので。 とりあえずWorkerを使ってみる メイン側。 new Workerのオプションには必ず{type: "module"}をつける。 main.ts const workerPath = new URL("./worker.ts", import.meta.url).href; const worker = new Worker(workerPath, { type: "module" }); worker.postMessage("あなたが青く見えるなら、"); console.log("main: posted."); worker.onmessage = ({ data }) => { console.log("main: received: ", data!); }; ワーカー側。 イベントハンドラの型を認識させるため/// <reference lib="deno.worker" />を一行目に書く。 worker.ts /// <reference lib="deno.worker" /> self.onmessage = ({ data }) => { console.log(" worker: received: ", data); self.postMessage("りんごもうさぎの体も青くていいんだよ"); console.log(" worker: posted."); self.close(); }; 実行結果。 worker.tsがローカルにあるので--allow-readを指定してる。 外部にあるなら--allow-netが必要。 実行結果 % deno run -q --allow-read --unstable main.ts main: posted. worker: received: あなたが青く見えるなら、 worker: posted. main: received: りんごもうさぎの体も青くていいんだよ new Workerしたらメイン側からworker.terminate()するか、ワーカー側で自らself.close()しないと動き続ける。 受け渡しするデータは送信側で文字列(JSON)にされて受信側でオブジェクトにされるので同一じゃない。 どこでもドアみたい。怖い。 【閲覧注意】 どこでもドアの秘密 Worker側でDenoの機能を使ってみる オプションに{deno: {namespace: true}}をつけるとワーカー側でDenoが使えるようになる。 実行時に--unstableが必要になる。 ワーカー側からDeno.readTextFileで同じフォルダにあるファイルを読んでみる。 main.ts const workerPath = new URL("./worker.ts", import.meta.url).href; const worker = new Worker(workerPath, { type: "module", deno: { namespace: true }, }); // テキストファイルのパスを絶対パスにする。 // カレントフォルダにあるファイルを読みたいなら相対パスをそのまま渡す。 const textPath = new URL("./msg.txt", import.meta.url).href.replace("file://", ""); worker.postMessage(textPath); worker.onmessage = ({ data }) => { console.log(data) }; worker.ts /// <reference lib="deno.worker" /> self.onmessage = async ({ data }) => { const text = await Deno.readTextFile(data); self.postMessage(text); self.close(); }; 結果 % deno run --allow-read -q --unstable main.ts 「失敗」という概念は一度捨ててみて そしたら今まで得た技術知識...そしてこの絵が あなたの味方をしてくれますよ パーミッションの指定 パーミッションはデフォルトではmain側のものが引き継がれる。("inherit") deno run -Aで全ての権限を与えてnew Workerの第二引数でdenoを書かなかった場合、 Denoが使えないからDeno.readTextFileは使えないけど、fetchは使えるので通信はできちゃう。 全部拒否する場合はこう。 denoプロパティを指定するために--unstableが必要。 全部許可 const worker = new Worker(new URL("./worker.js", import.meta.url).href, { type: "module", deno: { namespace: false, permissions: "none", }, }); それかdeno: falseでnamespaceとpermissionsを全て無効にできる。 一部許可する場合はこんな感じ。 ここで許可してもmain.tsに与えられてる権限以上のことは出来ない。 一部許可 const worker = new Worker(new URL("./worker.js", import.meta.url).href, { type: "module", deno: { namespace: true, permissions: { net: [ "https://deno.land/", ], read: [ new URL("./file_1.txt", import.meta.url), new URL("./file_2.txt", import.meta.url), ], write: false, }, }, }); Denoで安全にevalしたい evalの危険性はさんざん言われてるけど eval() を使わないでください! - eval() - JavaScript | MDN Denoは許可(パーミッション)を得ないとファイル読んだりネットに繋いだりできない。 パーミッションを全て無効にすれば安全にevalできるはず。 メイン側とワーカー側でグローバルが違うし。 while(true) ;とかも怖いから処理が3秒以上になったら†自戒†するようにして... save_eval.ts const WORKER_PATH = "./safe_eval_worker.ts"; const TIMEOUT_SEC = 3; /** workerにpostMessageして返事を受け取るPromiseを返す */ function waitForWorkerResponse( worker: Worker, message: string, timeoutSeconds = TIMEOUT_SEC, ) { return new Promise((resolve, reject) => { worker.postMessage(message); // sec秒経過したら失敗 const timerId = setTimeout(() => { worker.terminate(); reject(new Error(`worker operation timeout (${timeoutSeconds} seconds)`)); }, timeoutSeconds * 1000); // 返事が帰ってきたら成功 worker.onmessage = ({ data }) => { clearTimeout(timerId); //タイマー解除 worker.terminate(); resolve(data); }; }); } /** Workerを使って安全にevalできるはずの関数 */ export async function safeEval(source: string, timeoutSeconds = TIMEOUT_SEC) { const workerURL = new URL(WORKER_PATH, import.meta.url).href; const worker = new Worker(workerURL, { type: "module", deno: false }); const result = await waitForWorkerResponse(worker, source, timeoutSeconds); return result; } safe_eval_worker.ts /// <reference lib="deno.worker" /> /** * evalの代替 * * from: * eval() を使わないでください! - eval() - JavaScript | MDN * https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_eval! */ function evaluator(source: string) { return Function(`'use strict'; return ${source}`)(); } self.onmessage = ({ data: source }) => { const evaluated = evaluator(source); self.postMessage(evaluated); self.close(); }; sample.ts import {safeEval} from "./safe_eval.ts" const source = `1 + 1`; const result = await safeEval(source); console.log(result); // 2 こんな感じにした。 なんか間違ってたり改善点あれば教えて欲しいです。 あと「こんなん全然ダメだよ」って思われた方もぜひ。 絶対ですよ。 参考 eval() を使わないでください! - eval() - JavaScript | MDN Type Checking Web Workers | Deno Manual Specifying worker permissions | Deno Manual
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Denoで安全にevalしたい (Web Worker)

なんでそんなことしたいの? 某サイトをスクレイピングするとき必要だった。 evalがアレなのはさんざん言われてるから隔離したい。 eval() を使わないでください! - JavaScript | MDN Web Workerを使えばメイン側とワーカー側でグローバルが違うから隔離できるはず。 Denoのデフォルトではワーカー側がメイン側のパーミッションを引き継ぐ。 メイン側で--allow-netしてるとワーカー側でネット通信とかできちゃう。 ので、ワーカー側のパーミッションを無効化して使います。 Denoのスクリプト内で使うんじゃなくて実行結果を見たいだけなら 普通にdeno run sample.tsかecho "1 + 1" | deno --でいいと思います。 Denoに権限を与えてないから何もできないはずなので。 とりあえずWorkerを使ってみる メイン側。 new Workerのオプションには必ず{type: "module"}をつける。 main.ts const workerPath = new URL("./worker.ts", import.meta.url).href; const worker = new Worker(workerPath, { type: "module" }); worker.postMessage("あなたが青く見えるなら、"); console.log("main: posted."); worker.onmessage = ({ data }) => { console.log("main: received: ", data!); }; ワーカー側。 イベントハンドラの型を認識させるため/// <reference lib="deno.worker" />を一行目に書く。 worker.ts /// <reference lib="deno.worker" /> self.onmessage = ({ data }) => { console.log(" worker: received: ", data); self.postMessage("りんごもうさぎの体も青くていいんだよ"); console.log(" worker: posted."); self.close(); }; 実行結果。 worker.tsがローカルにあるので--allow-readを指定してる。 外部にあるなら--allow-netが必要。 実行結果 % deno run -q --allow-read main.ts main: posted. worker: received: あなたが青く見えるなら、 worker: posted. main: received: りんごもうさぎの体も青くていいんだよ new Workerしたらメイン側からworker.terminate()するか、ワーカー側で自らself.close()しないと動き続ける。 受け渡しするデータは送信側で文字列(JSON)にされて受信側でオブジェクトにされるので同一じゃない。 どこでもドアみたい。怖い。 【閲覧注意】 どこでもドアの秘密 Worker側でDenoの機能を使ってみる オプションに{deno: {namespace: true}}をつけるとワーカー側でDenoが使えるようになる。 実行時に--unstableが必要になる。 ワーカー側からDeno.readTextFileで同じフォルダにあるファイルを読んでみる。 main.ts const workerPath = new URL("./worker.ts", import.meta.url).href; const worker = new Worker(workerPath, { type: "module", deno: { namespace: true }, }); // テキストファイルのパスを絶対パスにする。 // カレントフォルダにあるファイルを読みたいなら相対パスをそのまま渡す。 const textPath = new URL("./msg.txt", import.meta.url).href.replace("file://", ""); worker.postMessage(textPath); worker.onmessage = ({ data }) => { console.log(data) }; worker.ts /// <reference lib="deno.worker" /> self.onmessage = async ({ data }) => { const text = await Deno.readTextFile(data); self.postMessage(text); self.close(); }; 結果 % deno run --allow-read -q --unstable main.ts 「失敗」という概念は一度捨ててみて そしたら今まで得た技術知識...そしてこの絵が あなたの味方をしてくれますよ パーミッションの指定 パーミッションはデフォルトではmain側のものが引き継がれる。("inherit") deno run -Aで全ての権限を与えてnew Workerの第二引数でdenoを書かなかった場合、 Denoが使えないからDeno.readTextFileは使えないけど、fetchは使えるので通信はできちゃう。 全部拒否する場合はこう。 denoプロパティを指定するために--unstableが必要。 全部許可 const worker = new Worker(new URL("./worker.js", import.meta.url).href, { type: "module", deno: { namespace: false, permissions: "none", }, }); それかdeno: falseでnamespaceとpermissionsを全て無効にできる。 一部許可する場合はこんな感じ。 ここで許可してもmain.tsに与えられてる権限以上のことは出来ない。 一部許可 const worker = new Worker(new URL("./worker.js", import.meta.url).href, { type: "module", deno: { namespace: true, permissions: { net: [ "https://deno.land/", ], read: [ new URL("./file_1.txt", import.meta.url), new URL("./file_2.txt", import.meta.url), ], write: false, }, }, }); Denoで安全にevalしたいコード while(true) ;とかも怖いから、処理が長いときは†自戒†する。 save_eval.ts const WORKER_PATH = "./safe_eval_worker.ts"; const TIMEOUT_SEC = 3; /** workerにpostMessageして返事を受け取るPromiseを返す */ function waitForWorkerResponse( worker: Worker, message: string, timeoutSeconds = TIMEOUT_SEC, ) { return new Promise((resolve, reject) => { worker.postMessage(message); // sec秒経過したら失敗 const timerId = setTimeout(() => { worker.terminate(); reject(new Error(`worker operation timeout (${timeoutSeconds} seconds)`)); }, timeoutSeconds * 1000); // 返事がきたら成功 worker.onmessage = ({ data }) => { clearTimeout(timerId); //タイマー解除 worker.terminate(); resolve(data); }; }); } /** Workerを使って安全にevalできるはずの関数 */ export async function safeEval(source: string, timeoutSeconds = TIMEOUT_SEC) { const workerURL = new URL(WORKER_PATH, import.meta.url).href; const worker = new Worker(workerURL, { type: "module", deno: false }); const result = await waitForWorkerResponse(worker, source, timeoutSeconds); return result; } safe_eval_worker.ts /// <reference lib="deno.worker" /> /** evalの代替 */ function evaluator(source: string) { return Function(`'use strict'; return ${source}`)(); } self.onmessage = ({ data: source }) => { const evaluated = evaluator(source); self.postMessage(evaluated); self.close(); }; sample.ts import {safeEval} from "./safe_eval.ts" const source = `1 + 1`; const result = await safeEval(source); console.log(result); // 2 こんな感じにした。 何回も実行する必要があるなら毎回new Workerせず使いまわしたほうがいいと思います。 なんか間違ってたり改善点あれば教えて欲しいです。 あと「こんなん全然ダメだよ」って思われた方もぜひ。 絶対ですよ。 参考 eval() を使わないでください! - eval() - JavaScript | MDN Type Checking Web Workers | Deno Manual Specifying worker permissions | Deno Manual
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Denoで安全にevalしたい (permissionsを引き継がないでWorkerを使う)

なんでそんなことしたいの? 某サイトをスクレイピングするとき必要だった。 Denoのスクリプト内で使うんじゃなくて実行結果を見たいだけなら 普通にdeno run sample.tsかecho "1 + 1" | deno --でいいと思います。 Denoに権限を与えてないから何もできないはずなので。 Deno.run({cmd: ["deno", "run", "sample.ts"]})でもいいかもね。 とりあえずWorkerを使ってみる メイン側。 new Workerのオプションには必ず{type: "module"}をつける。 main.ts const workerPath = new URL("./worker.ts", import.meta.url).href; const worker = new Worker(workerPath, { type: "module" }); worker.postMessage("あなたが青く見えるなら、"); console.log("main: posted."); worker.onmessage = ({ data }) => { console.log("main: received: ", data!); }; ワーカー側。 イベントハンドラの型を認識させるため/// <reference lib="deno.worker" />を一行目に書く。 worker.ts /// <reference lib="deno.worker" /> self.onmessage = ({ data }) => { console.log(" worker: received: ", data); self.postMessage("りんごもうさぎの体も青くていいんだよ"); console.log(" worker: posted."); self.close(); }; 実行結果。 worker.tsがローカルにあるので--allow-readを指定してる。 外部にあるなら--allow-netが必要。 実行結果 % deno run -q --allow-read --unstable main.ts main: posted. worker: received: あなたが青く見えるなら、 worker: posted. main: received: りんごもうさぎの体も青くていいんだよ new Workerしたらメイン側からworker.terminate()するか、ワーカー側で自らself.close()しないと動き続ける。 受け渡しするデータは送信側で文字列(JSON)にされて受信側でオブジェクトにされるので同一じゃない。 どこでもドアみたい。怖い。 【閲覧注意】 どこでもドアの秘密 Worker側でDenoの機能を使ってみる オプションに{deno: {namespace: true}}をつけるとワーカー側でDenoが使えるようになる。 実行時に--unstableが必要になる。 ワーカー側からDeno.readTextFileで同じフォルダにあるファイルを読んでみる。 main.ts const workerPath = new URL("./worker.ts", import.meta.url).href; const worker = new Worker(workerPath, { type: "module", deno: { namespace: true }, }); // テキストファイルのパスを絶対パスにする。 // カレントフォルダにあるファイルを読みたいなら相対パスをそのまま渡す。 const textPath = new URL("./msg.txt", import.meta.url).href.replace("file://", ""); worker.postMessage(textPath); worker.onmessage = ({ data }) => { console.log(data) }; worker.ts /// <reference lib="deno.worker" /> self.onmessage = async ({ data }) => { const text = await Deno.readTextFile(data); self.postMessage(text); self.close(); }; 結果 % deno run --allow-read -q --unstable main.ts 「失敗」という概念は一度捨ててみて そしたら今まで得た技術知識...そしてこの絵が あなたの味方をしてくれますよ パーミッションの指定 パーミッションはデフォルトではmain側のものが引き継がれる。("inherit") deno run -Aで全ての権限を与えてnew Workerの第二引数でdenoを書かなかった場合、 Denoが使えないからDeno.readTextFileは使えないけど、fetchは使えるので通信はできちゃう。 全部拒否する場合はこう。 denoプロパティを指定するために--unstableが必要。 全部許可 const worker = new Worker(new URL("./worker.js", import.meta.url).href, { type: "module", deno: { namespace: false, permissions: "none", }, }); それかdeno: falseでnamespaceとpermissionsを全て無効にできる。 一部許可する場合はこんな感じ。 ここで許可してもmain.tsに与えられてる権限以上のことは出来ない。 一部許可 const worker = new Worker(new URL("./worker.js", import.meta.url).href, { type: "module", deno: { namespace: true, permissions: { net: [ "https://deno.land/", ], read: [ new URL("./file_1.txt", import.meta.url), new URL("./file_2.txt", import.meta.url), ], write: false, }, }, }); Denoで安全にevalしたい evalの危険性はさんざん言われてるけど eval() を使わないでください! - eval() - JavaScript | MDN Denoは許可(パーミッション)を得ないとファイル読んだりネットに繋いだりできない。 パーミッションを全て無効にすれば安全にevalできるはず。 メイン側とワーカー側でグローバルが違うし。 while(true) ;とかも怖いから処理が3秒以上になったら†自戒†するようにして... save_eval.ts const WORKER_PATH = "./safe_eval_worker.ts"; const TIMEOUT_SEC = 3; /** workerにpostMessageして返事を受け取るPromiseを返す */ function waitForWorkerResponse( worker: Worker, message: string, timeoutSeconds = TIMEOUT_SEC, ) { return new Promise((resolve, reject) => { worker.postMessage(message); // sec秒経過したら失敗 const timerId = setTimeout(() => { worker.terminate(); reject(new Error(`worker operation timeout (${timeoutSeconds} seconds)`)); }, timeoutSeconds * 1000); // 返事が帰ってきたら成功 worker.onmessage = ({ data }) => { clearTimeout(timerId); //タイマー解除 worker.terminate(); resolve(data); }; }); } /** Workerを使って安全にevalできるはずの関数 */ export async function safeEval(source: string, timeoutSeconds = TIMEOUT_SEC) { const workerURL = new URL(WORKER_PATH, import.meta.url).href; const worker = new Worker(workerURL, { type: "module", deno: false }); const result = await waitForWorkerResponse(worker, source, timeoutSeconds); return result; } safe_eval_worker.ts /// <reference lib="deno.worker" /> /** * evalの代替 * * from: * eval() を使わないでください! - eval() - JavaScript | MDN * https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_eval! */ function evaluator(source: string) { return Function(`'use strict'; return ${source}`)(); } self.onmessage = ({ data: source }) => { const evaluated = evaluator(source); self.postMessage(evaluated); self.close(); }; sample.ts import {safeEval} from "./safe_eval.ts" const source = `1 + 1`; const result = await safeEval(source); console.log(result); // 2 こんな感じにした。 なんか間違ってたり改善点あれば教えて欲しいです。 あと「こんなん全然ダメだよ」って思われた方もぜひ。 絶対ですよ。 あとがき いや実行時間長すぎて無理 (なんか間違ってる?) 参考 eval() を使わないでください! - eval() - JavaScript | MDN Type Checking Web Workers | Deno Manual Specifying worker permissions | Deno Manual
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】オブジェクト のバリューだけ取得するメソッドを書いたら、すでにそのメソッドあった話。

オブジェクト のバリューだけ取得するメソッドを書いたら、すでにそのメソッドあった話。 相変わらず語彙力がなくてタイトルがゴミですが気にせずいきましょう 企業様の課題に取り組んでる最中に、以下のようなオブジェクト のバリューだけを取得したくなった。 const obj = {foo: {num: 2}, bar: {num: -1}, buz: {num: null}, qux: {}, quux: null} 自分が書いたバリューだけを取得する方法 const newArray = Object.entries(obj) // オブジェクトを配列にするメソッド // ["foo", {…}] // ["bar", {…}] // ["buz", {…}] // ["qux", {…}] // 配列の配列みたいなものが返ってきた // 格配列の一番目の要素はオブジェクトのキーだったもの newArray.map(function(element){ // まず一つずつ配列取り出せた element.shift(); // そしてfoo,barにあたる要素を削除できた // shift();は一番目の要素を削除するメソッド }); console.log(newArray) 考え方としては、 格配列の一番目の要素はオブジェクトのキー なのでそれを削除するべく、 配列に各処理をする。 mapは各処理をした後に新しく配列を作り直してくれるらしい 模範解答 Object.values(obj) [{num: 2},{num: 4}]とバリューが配列で取得できます。 俺が頑張って考えたのに、なんと元からあるメソッド使えば1秒でできたのかい!! 悔しいけど、面白い、、、www 実装したいことをすぐコードに表すより、このメソッドないかなー?と 一旦立ち止まって調べるようにしよう。と思えたのが収穫でした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Astroとは何者なのか。

「Astro」について触れてみる Astroについての記載が全くなかったので、自分なりに公式のページを見ての内容をまとめる。 今後も何か触れる機会があれば記述していきたい。 詳細については公式ページへ ※あくまでもベータ版とのこと Astroとは ●特徴 Javascriptのフレームワークを組み合わせることやHTML+Javascriptを組み合わせることができる ビルドしたページはhtmlに変換されるためJavascriptを使用しない オンデマンドコンポーネントのため、Javascriptが必要なページについてもアクセス時に読み込まれる TypescriptやCSS,CSS Modules,Sassなどのnpmパッケージをサポートしている SEO対応によりシンジケーションの手間が省ける ●個人的にいいなぁと思った点 ReactやVueにも対応しているため、フレームワークからフレームワークへの技術移行が容易になる ポートフォリオを作成する際にReactもVueもかけますというアピールが一つのプロジェクトで行える HTMLのみになるため初期読み込みが高速になるよう(どれぐらい早いのかは不明) 様々なJavascriptに対応しているとプロジェクトへの入り口が広くなる コンポーネントは技術上位者が作成し、初心者はHTMLのみでページを作成するなど役割分担がしやすい snowpackを使用したモジュールバンドル 実際に触ってみる ●動作環境 Macbook:Big Sur version11.4 Node.js:v14.17.0 npm:6.14.13 ●構築 まずは公式ページに記載されている通りに実行する 今回は「qiitaAstro」のプロジェクト名で進めていく。 # create your project >> mkdir SampleAstro >> cd SampleAstro >> npm init astro npx: installed 11 in 2.21s Welcome to Astro! (create-astro v0.5.0) If you encounter a problem, visit https://github.com/snowpackjs/astro/issues to search or file a new issue. > Prepare for liftoff. > Gathering mission details... ? Which app template would you like to use? › - Use arrow-keys. Return to submit. ❯ Starter Kit (Generic) Blog Documentation Portfolio [Starter Kit]を選択していく ? Which frameworks would you like to use? › Instructions: ↑/↓: Highlight option ←/→/[space]: Toggle selection a: Toggle all enter/return: Complete answer ◉ Preact ◉ React ◉ Svelte ◉ Vue 使うフレームワークは全てを選択するため、全て選択してEnterを押下 ✔ Which frameworks would you like to use? › Preact, React, Svelte, Vue > Copying project files... ✔ Done! Next steps: 1: npm install (or pnpm install, yarn, etc) 2: git init && git add -A && git commit -m "Initial commit" (optional step) 3: npm start (or pnpm, yarn, etc) To close the dev server, hit Ctrl-C Stuck? Visit us at https://astro.build/chat 必要なファイルが用意されたら[Next steps]に記載されている通りにコマンドを実行するのもよし。 今回はドキュメント通りに以下のコマンドを実行していく。 # install your dependencies >> npm install # start the dev server and open your browser >> npm start ・・・(省略)・・・ [HH:MM:SS] [snowpack] Ready! [dev server] Server started in 16197ms. [dev server] Local: http://127.0.0.1:3000/ [HH:MM:SS] [snowpack] watching for file changes... 上記の指示通りにhttp://localhost:3000を開くと実行画面が表示される。 ●フォルダ構成 上記で構築した場合のフォルダ構成になります。 ├── node_modules ├── public │   ├── assets │   │   └── logo.svg │   ├── favicon.svg │   ├── robots.txt │   └── style │   ├── global.css │   └── home.css ├── src │ ├── components │ │   ├── PreactCounter.jsx │ │   ├── ReactCounter.jsx │ │   ├── SvelteCounter.svelte │ │   ├── Tour.astro │ │   └── VueCounter.vue │ └── pages │ └── index.astro ├── .gitignore ├── .npmrc ├── astro.config.mjs ├── package-lock.json ├── package.json └── README.md ●各コンポーネント プロジェクトの準備時点で各フレームワークでのコンポーネントでのカウントアップが作成されているので見ていく components/PreactCounter.jsx import { h } from 'preact'; import { useState } from 'preact/hooks'; export default function PreactCounter({ children }) { const [count, setCount] = useState(0); const add = () => setCount((i) => i + 1); const subtract = () => setCount((i) => i - 1); return ( <div id="preact" class="counter"> <button onClick={subtract}>-</button> <pre>{count}</pre> <button onClick={add}>+</button> </div> ); } components/ReactCounter.jsx import React, { useState } from 'react'; export default function ReactCounter({ children }) { const [count, setCount] = useState(0); const add = () => setCount((i) => i + 1); const subtract = () => setCount((i) => i - 1); return ( <div id="react" className="counter"> <button onClick={subtract}>-</button> <pre>{count}</pre> <button onClick={add}>+</button> </div> ); } components/SvelteCounter.svelte <script> let count = 0; function add() { count += 1; } function subtract() { count -= 1; } </script> <div id="svelte" class="counter"> <button on:click={subtract}>-</button> <pre>{ count }</pre> <button on:click={add}>+</button> </div> vue/components/VueCounter.vue <template> <div id="vue" class="counter"> <button @click="subtract()">-</button> <pre>{{ count }}</pre> <button @click="add()">+</button> </div> </template> <script> import { ref } from 'vue'; export default { setup() { const count = ref(0) const add = () => count.value = count.value + 1; const subtract = () => count.value = count.value - 1; return { count, add, subtract } } } </script> components/Tour.astro --- import { Markdown } from 'astro/components'; --- <article> <div class="banner"> <p><strong>?‍? Seasoned astronaut?</strong> Delete this file. Have fun!</p> </div> <section> <Markdown> ## ? Project Structure Inside of your Astro project, you'll see the following folders and files: ``` / ├── public/ │ ├── robots.txt │ └── favicon.ico ├── src/ │ ├── components/ │ │ └── Tour.astro │ └── pages/ │ └── index.astro └── package.json ``` Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. Any static assets, like images, can be placed in the `public/` directory. </Markdown> </section> <section> <h2>? Want to learn more?</h2> <p>Feel free to check <a href="https://github.com/snowpackjs/astro">our documentation</a> or jump into our <a href="https://astro.build/chat">Discord server</a>.</p> </section> </article> <style> article { padding-top: 2em; line-height: 1.5; } section { margin-top: 2em; display: flex; flex-direction: column; gap: 1em; max-width: 70ch; } .banner { text-align: center; font-size: 1.2rem; background: var(--color-light); padding: 1em 1.5em; padding-left: 0.75em; border-radius: 4px; } pre, code { font-family: var(--font-mono); background: var(--color-light); border-radius: 4px; } pre { padding: 1em 1.5em; } .tree { line-height: 1.2; } code:not(.tree) { padding: 0.125em; margin: 0 -0.125em; } </style> ●コンポーネントを使用したページについて 以下のファイルにてそれぞれのコンポーネントをインポートしてページを作成している pages/index.astro --- // Component Imports import Tour from '../components/Tour.astro'; // You can import components from any supported Framework here! import PreactCounter from '../components/PreactCounter.jsx'; import ReactCounter from '../components/ReactCounter.jsx'; import SvelteCounter from '../components/SvelteCounter.svelte'; import VueCounter from '../components/VueCounter.vue'; // Component Script: // You can write any JavaScript/TypeScript that you'd like here. // It will run during the build, but never in the browser. // All variables are available to use in the HTML template below. let title = 'My Astro Site'; // Full Astro Component Syntax: // https://docs.astro.build/core-concepts/astro-components/ --- <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{title}</title> <link rel="icon" type="image/svg+xml" href="/favicon.svg"> <link rel="stylesheet" href="/style/global.css"> <link rel="stylesheet" href="/style/home.css"> <style> header { display: flex; flex-direction: column; gap: 1em; max-width: min(100%, 68ch); } </style> </head> <body> <main> <header> <div> <img width="60" height="80" src="/assets/logo.svg" alt="Astro logo"> <h1>Welcome to <a href="https://astro.build/">Astro</a></h1> </div> </header> <Tour /> <!-- - You can also use imported framework components directly in your markup! - - Note: by default, these components are NOT interactive on the client. - The `:visible` directive tells Astro to make it interactive. - - See https://docs.astro.build/core-concepts/component-hydration/ --> <PreactCounter client:visible /> <ReactCounter client:visible /> <SvelteCounter client:visible /> <VueCounter client:visible /> </main> </body> </html> ●レンダーする設定 以下のファイルにてAstroプロジェクトについての設定を保持している 下記の[renderes]に記載されているフレームワークについてレンダリングされるようです。 astro.config.mjs export default { // projectRoot: '.', // Where to resolve all URLs relative to. Useful if you have a monorepo project. // pages: './src/pages', // Path to Astro components, pages, and data // dist: './dist', // When running `astro build`, path to final static output // public: './public', // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that don’t need processing. buildOptions: { // site: 'http://example.com', // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. sitemap: true, // Generate sitemap (set to "false" to disable) }, devOptions: { // port: 3000, // The port to run the dev server on. // tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js' }, renderers: [ "@astrojs/renderer-preact", "@astrojs/renderer-react", "@astrojs/renderer-svelte", "@astrojs/renderer-vue" ], }; 感想について 個人開発ではとても良いかと思いました。 ポートフォリオを作る上でもReactが人気ですが、Vueの方がとっつきやすさはあると思うので、最初のうちはVueでいくつか作った後にReactにも挑戦をしたポートフォリオを作成することもできますし、snowpackの恩恵で開発時の待ち時間も減らせることでしょう。 まだまだ、ベータ版であり正式リリースについてはまだ先のようですが、これからの個人開発にはastroをしばらく使用しながら腕を磨いていきたいと思います。 そろそろ何かサービスを作って運用してみたいとも思っていますので、astroで作ったプロジェクトでもチャレンジしてみたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む