- 投稿日:2020-12-23T23:58:02+09:00
Svelteでカレンダーを作ってみる
この記事はラクス Advent Calendar 2020 23日目の投稿です。
今回は、フロントエンド開発で最近注目されているSvelteを試してみた結果を記事にまとめます。
Svelteって?
Svelte(スヴェルトと読むらしい)って何?と思う人も多いかもしれません。
が、すでにQiitaにもいくつか記事があるので、そちらに譲ります。Svelteでカレンダーを作ってみる
こういったチュートリアルの定番といえばTODOアプリですが、すでに作っている方がいたので今回はカレンダーアプリを作ることにします。
ひとまず、今月1か月の日付を表示して、ボタンで前月・次月を行き来できるようにしてみます。準備
公式サイトを参考に、Svelteで開発を始める準備をします。
https://svelte.dev/blog/svelte-and-typescript#Try_it_nowNode.jsとnpmはインストール済みだったので、サイトの記述どおりに下記のコマンドを実行します。
SvelteはJavaScriptでもTypeScriptでもコーディングできますが、今回はTypeScriptで開発することにしましょう。$ npx degit sveltejs/template svelte-calendar $ cd svelte-calendar $ node scripts/setupTypeScript.js $ npm installエディタも公式サイトに従い、公式の拡張機能をインストールしたVS Codeを使います。
ここまで来たら
npm run devを実行し、http://localhost:5000にアクセスしてみます。以下のような画面が表示されるはずです。
コンポーネントを作る
それではカレンダーを実装していきます。
Svelteはコンポーネント指向のフレームワークですので、表示する各要素をコンポーネントに分けて作成していきます。
今回は、以下のように分けることにしました。
- カレンダー(全体)
- 週
- 日
Dayコンポーネント
まず最も小さい単位の"日"コンポーネントから作っていきましょう。
コンポーネントは、srcディレクトリの下に拡張子.svelteのファイルとして作成します。
実際に実装したDay.svelteはこのような形になりました。Day.svelte<script lang="ts"> export let date: Date; </script> <style> div { flex: 1; border: 1px solid #ccc; border-top-width: 0; } div:nth-child(n + 2) { border-left-width: 0; } </style> <div> {#if date.getDate() === 1} {date.getMonth() + 1}/{date.getDate()} {:else} {date.getDate()} {/if} </div>Svelteファイルの中身は、script + CSS + テンプレート になっています。
<script>タグ
- 変数をexportすることで、コンポーネントの外からデータを受け取ります。
- 今回はTypeScriptを使っているので、
lang="ts"を付けます。<style>タグ
- このコンポーネントで利用するCSSを書きます。ここではセレクタにdivを使っていますが、これはコンポーネント外のdivタグには影響しません(描画時に自動でclass属性を追加してくれます)。
- テンプレート
- 通常のHTMLの中に、変数や式を埋め込んだり、条件分岐やループを書けます。上記では、日付が1日の場合だけ
12/1のように月も表示するように分岐しています。Weekコンポーネント
次に"週"のコンポーネントを作っていきます。上で作ったDayコンポーネントを7日分並べるようにすればできそうです。初日の日付だけ外から受け取るようにしましょう。
Week.svelte<script lang="ts"> import Day from "./Day.svelte"; export let startDate: Date; // 1週間のDateオブジェクトの配列 const week = Array.from(Array(7).keys(), (i) => { const date = new Date(startDate); date.setDate(startDate.getDate() + i); return date; }); </script> <style> div { display: flex; flex: 1; } </style> <div class="week"> {#each week as date} <Day {date} /> {/each} </div>基本はDayコンポーネントと変わりませんが、他のコンポーネントを利用している点が先程と違います。
コンポーネントから他コンポーネントを利用するには、
importで対象のコンポーネントを読み込みます。
そして、HTML部分にコンポーネント名のタグ<Day />を書けば、そのコンポーネントを呼び出すことができます。
ただし、Dayコンポーネントは、外部からdateという変数を受け取る必要があります。変数は、コンポーネントのタグの属性として変数名={データ}とすることで渡せます。上の例で言うと<Day date={date} />なのですが、今回は変数名が一致しているので、=の前を省略できます。Calendarコンポーネント
次にWeekコンポーネントを組み合わせてCalendarコンポーネントを作ります。
と言っても、表示する日付の範囲をDateオブジェクトをゴリゴリ操作して決めている以外はWeekコンポーネントと同じような書き方をしているので、折りたたんだ中にコードを載せるだけにしておきます。
Calendar.svelteのコードCalendar.svelte<script lang="ts"> import Week from "./Week.svelte"; const today = new Date(); // 今月1日 const firstDayOfMonth = new Date(today); firstDayOfMonth.setDate(1); // 1日が属する週の日曜日 const firstDayOfFirstWeek = new Date(firstDayOfMonth); firstDayOfFirstWeek.setDate(1 - firstDayOfMonth.getDay()); // 表示するすべての日曜日 const sundays = Array.from(Array(6).keys(), (i) => { const sunday = new Date(firstDayOfFirstWeek); sunday.setDate(sunday.getDate() + 7 * i); return sunday; }).filter((date, i) => i === 0 || date.getMonth() === today.getMonth()); </script> <style> h1 { margin: 0; } .calendar { display: flex; flex-direction: column; flex: 1; padding-bottom: 18px; } .week-header { display: flex; } .dow { flex: 1; border: 1px solid #cccccc; } .dow:nth-child(n + 2) { border-left-width: 0; } .days { display: flex; flex-direction: column; flex: 1; } </style> <div> <h1>{today.getFullYear()}年{today.getMonth() + 1}月</h1> </div> <div class="calendar"> <div class="week-header"> {#each ['日', '月', '火', '水', '木', '金', '土'] as dow} <div class="dow">{dow}</div> {/each} </div> <div class="days"> {#each sundays as sunday} <Week startDate={sunday} /> {/each} </div> </div>
あとは
App.svelteからCalendarコンポーネントを呼び出すようにすれば、画面に今月のカレンダーが表示されるようになります!
なお、ここまでで実装したコード全体は以下のURLから確認できます。
https://github.com/takaram/svelte-calendar-sample/tree/f51892c54bf4848d2041847219cb46e79022f0d3ページに動きをつける
このカレンダーを表示するだけであれば、PHPでHTMLを出力するのとさほど変わらないでしょう。
ここから、前月・次月に移動できるようにしていきましょう。まず、CalendarコンポーネントのHTMLで利用している変数
todayとsundaysを動的に変更できるよう、letでの宣言に変更します(ついでにtodayの変数名をcurrentDayに変更しました)。- const today = new Date(); + let currentDay: Date; + let sundays: Date[];そして、これらの変数へ代入する部分のコードを関数化します。
const setCurrentDay = (currentDay_: Date) => { currentDay = currentDay_; // 今月1日 const firstDayOfMonth = new Date(currentDay); firstDayOfMonth.setDate(1); // 1日が属する週の日曜日 const firstDayOfFirstWeek = new Date(firstDayOfMonth); firstDayOfFirstWeek.setDate(1 - firstDayOfMonth.getDay()); // 表示するすべての日曜日 sundays = Array.from(Array(6).keys(), (i) => { const sunday = new Date(firstDayOfFirstWeek); sunday.setDate(sunday.getDate() + 7 * i); return sunday; }).filter( (date, i) => i === 0 || date.getMonth() === currentDay.getMonth() ); };この関数を使って、表示を前月・次月に切り替える関数を作ることができます。
const goToPrevMonth = () => { currentDay.setMonth(currentDay.getMonth() - 1); setCurrentDay(currentDay); }; const goToNextMonth = () => { currentDay.setMonth(currentDay.getMonth() + 1); setCurrentDay(currentDay); };
currentDayに破壊的にsetMonth()しているので、setCurrentDayの1行目のcurrentDay = currentDay_;は不要なのでは?と思うかもしれませんが、これをコメントアウトすると上手く動きません。
これはSvelteが代入をトリガーに画面の再描画を行うためです1。あとは、月を移動するボタンを付けます。要素にクリックイベントを設定するには、
on:click={イベントハンドラ}とします。この例は関数名ですが、直接関数リテラルをon:click={() => ...}のように書くこともできます。+ <span class="month-control" role="button" on:click="{goToPrevMonth}"><</span> <h1>{currentDay.getFullYear()}年{currentDay.getMonth() + 1}月</h1> + <span class="month-control" role="button" on:click="{goToNextMonth}">></span>ここまで来れば完成……と思いきや、実はこれでは
YYYY年M月のタイトル部分しか変わりません(私はここでハマりました)。
Week.svelteも以下のように変更する必要があります。- const week = Array.from(Array(7).keys(), (i) => { + $: week = Array.from(Array(7).keys(), (i) => { const date = new Date(startDate); date.setDate(startDate.getDate() + i); return date; });代入文の頭に
$:をつけると、右辺で使われている値が変更された際に再代入・再描画が行われます2。これでようやく表示する月を変更できるようになりました。
完成したコードは以下のリポジトリで確認できます。
https://github.com/takaram/svelte-calendar-sample/tree/09f7dbe9bb588b661a5947aa83e8f9f8ce4e0ddf所感
あまり他のフレームワークに詳しくないため比較はできませんが、比較的少ないコード量で実装できたのではないでしょうか。
表示される値の更新に多少クセがあるような気もしますが、慣れれば記述量が少なくて問題なさそうです。ちなみに、開発中に編集したファイルを保存すると、変更が自動的に反映されるのがとても楽でした。ブラウザの更新ボタンすら押す必要がないので、行った変更を即座に確認することができます。
今度はもう少し複雑なアプリも作成してみたいですね。
- 投稿日:2020-12-23T23:52:25+09:00
【Nuxt.js】API(バックエンド)でMySQLからテーブル情報を取得しフロントエンドで表示させるアプリの作成
1 はじめに
【アプリ作成の目的】
フロントエンド(Nuxt.js),バックエンド(express)を利用したアプリ作成を理解する。フロントエンドとサーバーエンドを分離したアプリとする。【フロントエンドで行うこと】
・axiosを利用してバックエンド側(API)にリクエストを投げ、取得したデータを表示させる。
・ホームディレクトリは、「nuxt-scraping-app」とする。【バックエンドで行うこと】
・事前にスクレイピングしてMySQLに保存したデータをjsonで返すAPIを作成する。
・ホームディレクトリは、「api-nuxt-puppeteer」とする。【注意点】
※今回スクレイピング部分のコードに関しては、割愛させていただきます。
※スクレイピングの情報取得先はファッション系サイトであるfarfetchの商品を選択しました(sacaiのアイテム)。
※まだ理解が浅い為間違っている箇所はご指摘いただけると助かります!2 APIの作成(サーバーエンド)
サーバーサイドにExpressを利用する。
※Expressとは、Node.jsのフレームワーク。Rubyで言うところのRails。
こちらの説明がわかりやすかったです↓
https://qiita.com/ganariya/items/85e51e718e56e7d128b8【手順】
nuxt-scraping-app$ npm install express # npmを利用 $ yarn add express # yarnを利用ホームディレクトリで上記のようにどちらかでexpressを導入する。
ちなみに、「npm install express --save」のように記載している記事がありますが、「--save」はnpmバージョン5.0.0からオプションを付けなくてもデフォルトでsaveされる為、必要ないようです→https://qiita.com/havveFn/items/c5beda8572aa8c1e6be6今回npmコマンドを利用する為、package.jsonのdependenciesにexpressが追加されていることを確認する↓
package.json"dependencies": { "@nuxtjs/axios": "^5.12.2", "core-js": "^3.6.5", "express": "^4.17.1", # New! "mysql": "^2.18.1", "mysql2": "^2.2.5", "nuxt": "^2.14.6", "sequelize": "^6.3.5" },今回、APIを作成する為、apiディレクトリを作成し、index.jsを作成し以下のように記載。
api/index.jsconst express = require('express'); #expressを利用することを定義 const app = express(); # expressをappと定義 const mysql = require('mysql'); #今回はMySQLを利用する const connection = mysql.createConnection({ # 以下、各自のMySQLへの接続情報を書く host : 'localhost', user : 'root', password : '******', database : 'db_development' }); app.get('/', function (req, res) { # app.get...(expressの構文)、req=request。 res=response res.set({ 'Access-Control-Allow-Origin': '*' }); # この記載により、※1:CORSを許可する connection.query('select * from scrapings', function (error, results) { # scrapingsテーブルから全てのカラムを取得する if (error) throw error; # エラー処理 res.send(results[0]); # results[0]により、一番目のデータを返答する }); }); app.listen(5000, function () { # port 5000をlistenする console.log('Example app listening on port 5000!'); # console.logによりファイル実行時にコンソールに文字表示させる });※1 CORS...
↓MySQLのカラム情報は以下の通りです。
MySQL(Local)mysql> describe scrapings;+--------------+--------------+------+-----+---------+----------------+| Field | Type | Null | Key | Default | Extra | +--------------+--------------+------+-----+---------+----------------+ | id | int | NO | PRI | NULL | auto_increment | | imageUrl | varchar(255) | YES | | NULL | | | brandName | varchar(255) | YES | | NULL | | | itemName | varchar(255) | YES | | NULL | | | price | int | YES | | NULL | | | material | varchar(255) | YES | | NULL | | | brandStyleId | varchar(255) | YES | | NULL | | | createdAt | datetime | NO | | NULL | | | updatedAt | datetime | NO | | NULL | | +--------------+--------------+------+-----+---------+----------------+ 9 rows in set (0.00 sec)MySQLについて、Sequelizeと言うORM(*ORM...オブジェクトリレーションマッピング)を利用するとMySQL操作が容易になります。ここでは詳しく触れませんが、MySQLにターミナルからQUery操作しなくても、テーブルに改修を加えることができます。私を含め、初学者の方は直のSQL操作をで基礎的事項として覚えた方がよいと感じました。
Sequelizeについてはこちらが分かりやすかったです!→https://qiita.com/markusveeyola/items/64875c9507d5fa32884e$ node index.jsにより起動させます。
API(サーバーエンド)側は以上です。3 フロントエンドの作成(Nuxt.js)
CSSフレームワークとしてVuetifyを利用。モダンなフロントデザインにできるのでオススメです。
nuxt-create-appでデフォルトでVuetifyを選択できるので、特にinstallは必要はありません。
同時にaxiosも選択しておきます(※axios...)Nuxtではpagesディレクトリ配下にファイルを置くと、自動的にルーティングされる。
(例: pages/about.js...http://localhost:3000/about)今回は特にルーティングは使用しない為、index.vueに書いていきます(フロントは簡素化しています)。
pages/index.js<template> <v-card class="mx-auto" max-width="500" max-height="500"> <br /> <v-list-item three-line> <div class="scraping"> <h5>ID:{{ items.id }}</h5> <br /> <h5>商品画像URL:{{ items.imageUrl }}</h5> <br /> <h5>ブランド名:{{ items.brandName }}</h5> <br /> <h5>アイテム名:{{ items.itemName }}</h5> <br /> <h5>価格:¥{{ items.price }} (税込)</h5> <br /> <h5>素材:{{ items.material }}</h5> <br /> <h5>ブランドスタイルID:{{ items.brandStyleId }}</h5> </div> </v-list-item> <br /> </v-card> </template> <script> export default { async asyncData({ $axios }) { const items = await $axios.$get("http://localhost:5000"); return { items }; }, }; </script>pages/index.js<script> export default { async asyncData({ $axios }) { const items = await $axios.$get("http://localhost:5000"); return { items }; }, }; </script>asyncDataで外部からデータを取得します。今回の場合は、http://localhost:5000
に表示しているMySQLから取得した情報(api側)をフロント側から表示させます。itemsとしてreturnします。pages/index.js<h5>ID:{{ items.id }}</h5>このように、itemsとして返された情報を、例としてidカラムと指定することで、フロントに表示させます。
以上で、npm run dev, yarn devでローカルでサーバーを実行させると、最初のように表示できるはずです。
学び
今までRailsを主に触ってきたこともありAPIやJavaScriptについての理解が浅く、更に深い理解が必要だと感じた。インプットを更に励む必要がある。
- 投稿日:2020-12-23T23:52:25+09:00
【Nuxt.js】API(サーバーサイド)でMySQLからテーブル情報を取得しクライアントサイドで表示させる【Express】
1 はじめに
・APIアプリ(サーバーサイド)...MySQLに接続し、テーブルから情報を取得させます。
・フロントアプリ(クライアントサイド)... axiosによりlocalhost:5000に繋いでjsonで返してindex.vueに表示させます。上記をNuxt.jsにより作成し、ローカル環境でフロントアプリからaxiosにより情報取得し、表示させる。
※各種installコマンド等は省きます
※まだ理解が浅い為間違っている箇所はご指摘いただけると助かります!API側でMySQLからテーブルとカラムを取得し、フロント側でシンプルに表示させる。
2 APIアプリの作成
サーバーサイドにExpressを利用する。
※Expressとは、Node.jsのフレームワーク。Rubyで言うところのRails。
こちらの説明がわかりやすかったです。→https://qiita.com/ganariya/items/85e51e718e56e7d128b8各種コマンドにより、Nuxt.jsにExpressをインストールする。
今回、APIを作成する為、apiディレクトリを作成し、index.jsを作成し以下のように記載。api/index.jsconst express = require('express'); const app = express(); const mysql = require('mysql'); const connection = mysql.createConnection({ host : 'localhost', user : 'root', password : '******', database : 'db_development' }); app.get('/', function (req, res) { res.set({ 'Access-Control-Allow-Origin': '*' }); connection.query('select * from users', function (error, results, fields) { if (error) throw error; res.send(results[0]); }); }); app.listen(5000, function () { console.log('Example app listening on port 5000!'); });const mysql = require('mysql'); const connection = mysql.createConnection({ host : 'localhost', user : 'root', password : '******', database : 'db_development' });↑こちらでMySQLに接続します。
app.get('/', function (req, res) { res.set({ 'Access-Control-Allow-Origin': '*' }); connection.query('select * from users', function (error, results, fields) { if (error) throw error; res.send(results[0]); }); });↑こちらでMySQLのdb-developmentのusersテーブルからカラム全てを取得します。
app.get(...)はExpressの構文です。reqはリクエスト、resはレスポンス。↓MySQLのカラム情報は以下の通りです。
MySQL(Local)mysql> select * from users; +----+-----------+------------------------+-------+--------------------------------------------------------------------------------+--------------+---------------------+---------------------+ | id | brandName | itemName | price | material | brandStyleId | createdAt | updatedAt | +----+-----------+------------------------+-------+--------------------------------------------------------------------------------+--------------+---------------------+---------------------+ | 1 | Sacai | パネル セーター | 86900 | ナイロン 100%、ウール 100%、コットン 50%、ポリエスエル 50% | 2002359M | 2020-12-20 01:30:07 | 2020-12-20 01:30:10| +----+-----------+------------------------+-------+--------------------------------------------------------------------------------+--------------+---------------------+---------------------+ 1 row in set (0.00 sec)以下のようにport:5000をlistenします。console.logによりターミナルでの文字表示をさせます。
app.listen(5000, function () { console.log('Example app listening on port 5000!'); });MySQLについて、Sequelizeと言うORM(*ORM...オブジェクトリレーションマッピング)を利用するとMySQL操作が容易になります。ここでは詳しく触れませんが、MySQLにターミナルからQUery操作しなくても、テーブルに改修を加えることができます。私を含め、初学者の方は直のSQL操作をで基礎的事項として覚えた方がよいと感じました。
Sequelizeについてはこちらが分かりやすかったです!→https://qiita.com/markusveeyola/items/64875c9507d5fa32884e$ node index.jsにより起動させます。
API側は以上です。3 フロントアプリの作成
CSSフレームワークとしてVuetifyを利用。モダンなフロントデザインにできるのでオススメです。
nuxt-create-appでデフォルトでVuetifyを選択できるので、特にinstallは必要はありません。
同時にaxiosも選択しておきます(※axios...非同期通信を行う。Nuxtではpagesディレクトリ配下にファイルを置くと、自動的にルーティングされる。
(例: pages/about.js...http://localhost:3000/about)今回は特にルーティングは使用しない為、index.vueに書いていきます(フロントは簡素化しています)。
index.js<template> <v-card class="mx-auto" max-width="500" max-height="500"> <v-list-item> <div class="scraping"> <h5>ID:{{ items.id }}</h5> <h5>ブランド名:{{ items.brandName }}</h5> <h5>アイテム名:{{ items.itemName }}</h5> <h5>価格:¥{{ items.price }} (税込)</h5> <h5>素材:{{ items.material }}</h5> <h5>ブランドスタイルID:{{ items.brandStyleId }}</h5> </div> </v-list-item> <br /> </v-card> </template> <script> export default { async asyncData({ $axios }) { const items = await $axios.$get("http://localhost:5000"); return { items }; }, }; </script><script> export default { async asyncData({ $axios }) { const items = await $axios.$get("http://localhost:5000"); return { items }; }, }; </script>asyncDataで外部からデータを取得します。今回の場合は、http://localhost:5000
に表示しているMySQLから取得した情報(api側)をフロント側から表示させます。itemsとしてreturnします。<h5>ID:{{ items.id }}</h5>このように、itemsとして返された情報を、例としてidカラムと指定することで、フロントに表示させます。
以上で、npm run dev, yarn devでローカルでサーバーを実行させると、最初のように表示できるはずです。
学び
今までRailsを主に触ってきたこともありAPIやJavaScriptについての理解が浅く、更に深い理解が必要だと感じた。モダンなWEBアプリ開発を行っていく中で、Rest APIを利用したSPA,SSRアプリ制作は今後デファクトスタンダードになっていくと思うので、インプットを更に励む必要がある。
- 投稿日:2020-12-23T23:19:28+09:00
【2020年クリスマスイブ版】npm install で Merry Christmas ?✨??
本記事は 株式会社ピーアールオー(あったらいいな!を作ります) Advent Calendar 2020 の24日目、クリスマスイブの投稿です。
昨日(23日目)は @pro_matuzaki さんの「SpreadSheetのビジター共有がかゆいところに微妙に届かなかった話」でした。それでは、今回のテーマに入りたいと思います。
今年も早くもクリスマス・・・
あっという間に年末、クリスマスですね。。。今年も残り僅かですが、今回はnode.jsユーザーならすぐにでも試せる
クリスマスカードのご紹介です。以下の記事は
名刺を作る記事ですが、せっかくなのでこのを名刺をクリスマスカードに変えてみたいと思います。ターミナルからコマンドを叩くと自己紹介カードが表示される楽しいツールの作り方が紹介されております。
上記の記事を参考に、プログラマーのならではの 手作りのクリスマスカード をお届けいたしましょう〜?1. プロジェクトの作成
さて、まずはプロジェクトを作成いたします。今回は諸事情により
scoped(スコープ付き)のパッケージを作成します。$ mkdir xmascard $ cd xmascard $ npm init --scope=@xxxxx
npmのスコープとはなんぞや?と思われるかもしれませんが、@awsとか@angularとかの@グループ名の接頭辞っぽいアレです。あれをscopeというそうです。自分も作ってみるまで気にしたことはなかったのですが、公式サイトにもちゃんと解説がございました。
以下では筆者のアカウント名と合わせるため、スコープ名を@roomtvとしております。2. 依存パッケージの追加とカードの作成
今回はターミナルを派手に彩るために以下のパッケージを使います。
以下のコマンドでパッケージを追加します。
$ npm i boxen chalk clear inquirer asciify-imageこれらのパッケージは、それぞれ機能はシンプルなのでここでは詳しい解説は割愛しますが、以下のようなコードで大体、イメージができるかと思います。
index.js#!/usr/bin/env node "use strict"; const boxen = require("boxen"); const chalk = require("chalk"); const inquirer = require("inquirer"); const clear = require("clear"); var asciify = require('asciify-image'); clear(); const prompt = inquirer.createPromptModule(); // Questions after the card const questions = [ { type: "list", name: "action", message: "What you want to do?", choices: [ { name: "Just quit.", value: () => { console.log("Merry X'mas!\n"); } } ] } ]; asciify('[画像のURL]',{ fit: `box`, width:20, height:20, }, (_, rendered)=>{ // Data for the card const data = { name: chalk.bold.green("From koinori @ PRO"), ... }; // Build the card const me = boxen( [ `${chalk.bold("メリークリスマス !")}`, ... boxen(rendered, {mergin:1, borderColor: "green"}), ... `${data.name}` ].join("\n"), { margin: 1, float: 'center', padding: 1, borderStyle: { topLeft: '?', ... }, borderColor: "cyan" } ); // Print the card console.log(me); // Optional tip to help users use the links const tip = [ `Tip: Try ${chalk.cyanBright.bold( "cmd/ctrl + click" )} on the links above`, '', ].join("\n"); // Show the tip console.log(tip); // Ask the Inquirer questions. prompt(questions).then(answer => answer.action()); });文言の部分をクリスマスカードにしただけで、プログラムはほとんどオリジナルです。すいません。。。
以下のコマンドでクリスマスカードが表示されればとりあえずOKです。
$ node index.jsここは正直、センスですね。。。文言とスペースの調整でほとんどの時間を費やしました。
3. コマンドとしての動作確認
続いて、
merryxmasコマンドとして動作するように、package.jsonに以下の定義を追加します。package.json... "bin": { "merryxmas": "index.js" }, ...続いて以下のコマンドでローカルのnpm環境に対して
merryxmasコマンドのインストールをおこないます。$ npm link
npm linkが無事に完了したところで、npxで実行してみましょう。$ npx merryxmasクリスマスカードが表示されればOKです!
4. npmjs 本家サイトにデプロイ
さて知り合いにクリスマスカードを届けるために、npmjs サイトにデプロイしましょう。
アカウントがなければ、これをきっかけにアカウントを作ってみましょう!以下からどうぞ!!
アカウントが作成できたら
npm adduserもしくはnpm loginコマンドで、ローカルにてログインいたします。$ npm adduser Username: xxxxx Password: Email: (this IS public) xxxxx@xxxxx.co.jp Logged in as xxxxx on https://registry.npmjs.org/.ログイン完了後、
npm publishコマンドにて、npm リポジトリにデプロイいたします。$ npm publish --access public npm notice npm notice ? @roomtv/xmascard@1.0.0 npm notice === Tarball Contents === npm notice 842B .devcontainer/Dockerfile npm notice 3.4kB index.js npm notice 1.0kB .devcontainer/devcontainer.json npm notice 868B package.json npm notice 0 README.md npm notice === Tarball Details === npm notice name: @roomtv/xmascard npm notice version: 1.0.0 npm notice package size: 2.9 kB npm notice unpacked size: 6.2 kB npm notice shasum: 8a95b0822213c297d9cae0c1022d0d02c759389e npm notice integrity: sha512-wYvpyQglAYgjM[...]eMeeOH6DruMBg== npm notice total files: 5 npm notice + @roomtv/xmascard@1.0.0はい、本家リポジトリにクリスマスカードのデプロイ完了です!
ちなみに公開されたサイトは以下です。クリスマスカードの見た目の調整で力尽きてしまい、README の記述にはパワーが残っていなかったので、ドキュメントの方はぼちぼち追加してまいりたいと思います。。。すいません。。。
5. リポジトリから取得&ローカルで実行
開発した環境とは別ディレクトリとするか、
nvmやVSCodeのRemote Containerなどで新規のnode.js環境に切り替え、npm installをしてみましょう。$ npm i -g @roomtv/xmascard ... ... + @roomtv/xmascard@1.0.0 added 336 packages from 239 contributors in 62.997sいつもは絶対反対の
-gですが、今回だけは-gを使ってしまいます。大人なんてそんなもんです。それでは早速、
merryxmasコマンドを叩いてみましょう。$ npx merryxmasクリスマスカードが無事に表示されたでしょうか?!枠の中の文言の位置やセンタリング?など、スペースだけで調整してます。めっちゃ手作りですね!
はい、お疲れ様でした!
6. まとめと感想など
日頃、
npm installばかりですが、まさかこんなお手軽に自分の NPMパッケージを作成して公開できるとは思いませんでした。
主に気持ちの問題なのですが、きっかけの記事であったような名刺を表示するだけのコマンドでパッケージを公開しちゃってもいいのかよ・・・と思いましたが、アドベントカレンダーのネタにはなるなと考え、チャレンジしてみました。
手順は簡単ですが、いつも使っているnpmコマンドでもなかなかお世話にならないadduserやlink、publishなどが試せたので非常に面白かったです。相変わらずのコンソールネタとなってしまいましたが、新人研修には是非、おすすめです。
クリスマスカードや名刺に限らず、オリジナルのデザインとメッセージを表示するだけのジョークアプリでも、公開することに意味があるのだよ。(た、たぶん。。。)それでは、メリークリスマス !! ?✨??
- 投稿日:2020-12-23T23:10:07+09:00
JavaScriptを用いて複数の投稿にプルダウンメニューを実装する方法
記事投稿系のポートフォリオを作成する中で、各記事に対してプルダウンメニューで編集・削除ボタンを作成しました。この時、複数の要素に対してイベント発火させるコードを書いたのが初めてだったので、1つの要素とイベント発火させる場合と比較しながら要点をまとめてみました。
各コードの比較
各コードの①, ②部分について以下で詳しく見ていきます。
① HTMLの要素取得部分
上の画像では、getElementByIdで要素を取得しています。1箇所だけイベント発火させたい場合にはこの表現でいいのですが、今回のように複数の要素にイベント発火させたい場合、これでは指定した要素の内、一番上にある要素しか取得されません。
一方、下の画像では、querySerectorAllで要素を取得しているため、指定した要素全てを取得できます。取得した要素は下記のように、配列の形で取得されます。
② プルダウンメニュー実装部分
プルダウンメニュー実装部のul要素には、display: none;が指定してあるため、通常時、編集・削除ボタンは表示されません。これに対し、上記コードでは、プルダウンメニューボタンクリック時に、display: block;が記述され、編集・削除ボタンが表示されます。
これを、複数の要素に対してどう記述するかですが、①で述べた通り、querySerectorAllで取得した要素は配列で取得されます。そのため、pullDownButton.addEventListner(...)のように、直接イベント発火する記述ができません。これを解決するため、下の画像では、forを用いて、各i番目の要素に対して、イベント発火しています。
以上より、下記のように、複数の投稿に対してプルダウンメニューを実装することができました。
終わりに
これまで、getElementByIdで要素を取得するコードしか書いて来なかったので、複数の要素を取得するコードは、書いてみれば基本的なことばかりなのですが、中々苦労しました。
中でも、querySerectorAllで要素を取得するということと、要素が配列で取得されるという点が重要だと感じました。
- 投稿日:2020-12-23T23:06:42+09:00
宛先ごとに本文をカスタムしたメールをGoogle Apps Script( javascript )で一括送信する
背景
アカウント名やパスワードを一括配布する機会があったので、GAS(Google Apps Script)で実施しました。
処理の概要
下記のスプレッドシートの情報を基に一括メール送信します。
シート名は「スクリプト用データ」です。
完了フラグが
completeもしくはfail以外のみ、メールの送信対象となります。
送信後は完了フラグ列を自動で更新します。処理の流れ
① スプレッドシートからデータを取得する
② ①のデータを基にメール送信に必要なデータを作成する
③ ②で作成したデータを使いメール送信する
④ シートを更新する
以上です。メール送信に失敗した場合のエラーハンドリングでは、送信者にメール通知します。
ソースコード
▼メール送信
mainfunction main() { //申請者向けメール本文; const EMAIL_BODY_PREFIX = "お疲れ様です。\n下記があなたのアカウントです。\n"; const EMAIL_BODY_SUFFIX = "\n以上になります。よろしくお願いします。"; //スプレッドシート操作クラス; const sheetDataManager = new SheetDataManager(); //メール送信対象情報を一括取得; const sheetData = sheetDataManager.getSheetData(); //メール送信対象分メールを送信する sheetData.forEach((item) => { try { //シート処理中フラグ更新 sheetDataManager.updateProcessingFlag(item.get("No.")); MailApp.sendEmail({ to: item.get("宛先"), cc: item.get("cc"), bcc: item.get("bcc"), subject: "アカウントの通知", body:item.get("宛先名") + EMAIL_BODY_PREFIX + item.get("アカウント") + EMAIL_BODY_SUFFIX, }); //シート完了フラグ更新 sheetDataManager.updateCompleteFlag(item.get("No.")); } catch (e) { //シート失敗フラグ更新 sheetDataManager.updateFailFlag(item.get("No.")); MailApp.sendEmail({ to: item.get("送信者"), subject: "エラー発生報告", body: "No." + item.get("No.") + " でエラーが発生しました。", }); } }); }▼スプレッドシートデータ操作クラス
SheetDataManagerclass SheetDataManager { constructor() { //スプレッドシート取得 this.spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); this.sheet = this.spreadsheet.getSheetByName("スクリプト用データ"); } //メール送信対象情報を一括取得 getSheetData() { let sheetData = new Array(); //入力のある最終行数 const lastRow = this.sheet.getLastRow(); //スプレッドシートの行数分繰り返す for (let i = 2; i <= lastRow; i++) { //スプレッドシートから完了フラグを取得 const flag = this.sheet.getRange(i, 8).getValue(); //メール送信が未完了の対象を配列に追加する if (flag != "complete" && flag != "fail") { //配列に追加する前にMapにまとめる let singleSheetData = new Map(); //メール本文作成 const userName = this.sheet.getRange(i, 5).getValue() + "さん"; const account = this.sheet.getRange(i, 7).getValue(); //Mapにまとめる singleSheetData.set("宛先名", userName); singleSheetData.set("アカウント", account); singleSheetData.set("No.", this.sheet.getRange(i, 1).getValue()); singleSheetData.set("宛先", this.sheet.getRange(i, 2).getValue()); singleSheetData.set("cc", this.sheet.getRange(i, 3).getValue()); singleSheetData.set("bcc", this.sheet.getRange(i, 4).getValue()); singleSheetData.set("送信者", this.sheet.getRange(i, 6).getValue()); //Mapを配列に追加 sheetData.push(singleSheetData); } } //メール送信が未完了の対象を追加した配列を返す return sheetData; } //送信完了フラグ更新 updateCompleteFlag(num) { this.sheet.getRange(num + 1, 8).setValue("complete"); } //送信失敗フラグ更新 updateFailFlag(num) { this.sheet.getRange(num + 1, 8).setValue("fail"); } //処理中フラグ更新 updateProcessingFlag(num) { this.sheet.getRange(num + 1, 8).setValue("processing"); } }蛇足
実際はメール本文のカスタマイズがもう少しだけ複雑でしたが省きました。
javascriptの書き方でおかしなところがあれば教えていただきたいです。
短時間で作成したコードですが、問題なく動作しますので、何かの参考にしていただければと思います。
- 投稿日:2020-12-23T22:55:00+09:00
続・Webの技術だけで作るQRコードリーダー
この記事はPWA Advent Calendar 2020の16日目の記事です。
(だいぶ遅れてすみません)以前に書いたWebの技術だけで作るQRコードリーダーの続編です。
以前の記事ではjsQRというライブラリを使用してQRコードの読み込みをしていましたが、ブラウザ標準のShape Detection APIというAPIで同じことが実現できそうだったので試してみました。Shape Detection APIはEditor's draft(2020年12月23日現在)ですが、デスクトップ版とAndroid版のChrome ver83からはデフォルトで有効になっているようです。
※以前はchrome://fragからフラグを有効化しないと使えませんでした。
参考:https://www.chromestatus.com/feature/4757990523535360Shape Detection APIは、QRコードのスキャンだけではなく以下の3つの事が可能です。
- Barcode Detection(バーコードスキャン)
- Face Detection(顔検出)
- Text Detection(テキスト認識)
これらについては、以前にイベントで発表したので興味がある人は見てみてください。
スライド:ブラウザの新しいAPIで遊んでみる
動画:https://youtu.be/CS2tzUpYvQA?t=5253Barcode Detectionの使い方
こんな感じでインスタンスを作成してimg要素を渡せばOKです。
// インスタンスの作成 const barcodeDetector = new BarcodeDetector() // 画像要素を取得 const image = document.getElementById('image'); // 取得した画像要素をdetectに渡す barcodeDetector.detect(image) .then(barcodes => { barcodes.forEach(barcode => console.log(barcode.rawValue)) } .catch(err => { console.log(err) })上手く検出できれば以下のようなオブジェクトで値を受け取ることができます。
座標なども取得できますが、実際に使うのは
rawValueのところになると思います。
また、Barcode Detection APIはQRコードだけではなく、様々なバーコードのフォーマットに対応しています。詳しくはMDNなどで確認できます。作ったもの
動作しているGIFです。(上部の隙間はPCにスマホの画面を写しているためなので気にしないでください…)
実際に公開されていますので、AndroidでChrome(ver83以上)を使っている方はぜひ実機で試してみてください。
GitHubのdevelopブランチでソースコードも公開しています。サイト:https://dev-simple-qr.netlify.com/
GitHub:https://github.com/KanDai/simple-qr-reader/tree/develop実装
JavaScript全体のソースコードは以下のようになっています。
HTMLやCSSはGitHubから確認ください。app.jsif (!navigator.mediaDevices) { document.querySelector('#js-unsupported').classList.add('is-show') } if (window.BarcodeDetector == undefined) { console.log('Barcode Detector is not supported by this browser.') document.querySelector('#js-unsupported').classList.add('is-show') } const video = document.querySelector('#js-video') const checkImage = () => { const barcodeDetector = new BarcodeDetector() barcodeDetector .detect(video) .then((barcodes) => { if (barcodes.length > 0) { // QRコードの読み取りに成功したらモーダル開く for (let barcode of barcodes) { openModal(barcode.rawValue) } } else { // QRコードが見つからなかったら再度実行 setTimeout(() => { checkImage() }, 200) } }) .catch((e) => { console.error('Barcode Detection failed, boo.') }) } navigator.mediaDevices .getUserMedia({ audio: false, video: { facingMode: { exact: 'environment', }, }, }) .then((stream) => { video.srcObject = stream video.onloadedmetadata = () => { video.play() checkImage() } }) .catch((err) => { alert('Error!!') }) const openModal = (url) => { document.querySelector('#js-result').innerText = url document.querySelector('#js-link').setAttribute('href', url) document.querySelector('#js-modal').classList.add('is-show') } document.querySelector('#js-modal-close').addEventListener('click', () => { document.querySelector('#js-modal').classList.remove('is-show') checkImage() })前回から変わったところを中心に説明していきます。
全体像から確認したい方は前回の記事も合わせてご覧ください。Canvasが不要になった
最初の説明でimg要素を渡すと書きましたが、実はvideo要素もそのまま渡せます。
なので、前回の記事で行っていたCanvasで画像化するいう実装が不要になりました。検出結果は配列
複数の検出結果が得られる場合があるからだと思いますが、画像検出の結果は配列で受け取るため、結果をループで回して処理するような書き方になります。
また、検出できなかった場合もエラーではなく配列のlengthが0になります。所感
Barcode Detectionの実装も難しくなく、全体的に以前の実装を少し変えるだけで簡単に実装することができました。
さらに、Canvasの処理が要らなくなったこともあって少し手軽になりました。ブラウザ標準のAPIでこれができるのはとても良いですね。早く勧告になってほしい…
- 投稿日:2020-12-23T22:50:17+09:00
【Androidアプリ開発】WebViewのJavaScriptからネイティブ�のメソッドを呼ぶ
ウェブページをネイティブで包んだアプリを作る際などに、Web側からネイティブアプリ側に情報を渡したいことがある。
普通にJavascriptInterfaceを使えばよいのだが、少しハマった部分があったのでメモしておく。実装方法
基本的には公式の下記のページが参考になる。
https://developer.android.com/guide/webapps/webview?hl=ja#BindingJavaScript
- 呼び出されるメソッドの定義
MainActivity.kt/** Instantiate the interface and set the context */ class WebAppInterface(private val mContext: Context) { /** Show a toast from the web page */ @JavascriptInterface fun showToast(toast: String) { Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show() } }
- WebViewにインターフェースを追加
MainActivity.ktval webView: WebView = findViewById(R.id.webview) webView.addJavascriptInterface(WebAppInterface(this), "Android")
- 呼び出すJS
test.html<input type="button" value="Say hello" onClick="showAndroidToast('Hello Android!')" /> <script type="text/javascript"> function showAndroidToast(toast) { Android.showToast(toast); } </script>ネイティブアプリ以外でも表示するサイトであれば、tryでくくってエラーが出ないようにしてあげた方がよさそう。
必要な記述
上記に従って実装してみたのだが動かない。
WebViewではデフォルトでJavaScriptが無効になっているようだ。
下記で有効にしてあげる必要があった。MainActivity.ktmyWebView.getSettings().setJavaScriptEnabled(true)おまけ
WebViewではストレージも無効になっていたので、必要な場合はオンにする。
MainActivity.ktmyWebView.getSettings().setDomStorageEnabled(true)
- 投稿日:2020-12-23T22:30:29+09:00
canvasでクリスマスツリーをチカチカさせた習作
はじめに
クリスマスだし、なんとなくそれっぽいことをしたい気持ちです。
ずっと仲良くなりたいと思っていたcanvasを使って、クリスマスツリーをチカチカさせてみようかと思います。詳細
index.html
canvasを設置して、bodyのonloadに仕掛けた初期化用の関数を実行するだけです。
index.html<html> <head> <!-- 省略 --> <script src="main.js"></script> </head> <body onload='initialize();'> <p> <canvas id='tutorial' width='320' height='320'></canvas> </p> </body> <html>初期化
bodyのonloadで実行される部分です。
setIntervalで一定間隔で再描画しています。main.js// 省略 const interval = 500; // 明滅間隔(ミリ秒) // 省略 function initialize() { treeImage.src = 'tree.png' window.setInterval(draw, interval); } // 省略描画
一度白紙に戻してから、木の絵を書いて、明かりを描画しています。
明かりの位置は…テキトーに描いた絵だったので、機械的に決められず…座標を保持しています。
明かりは60%の確率でついているようにしています。本当は見えないcanvasに一通り書き終わってから、見えるcanvasにコピーするなどしたほうがちらつきが抑えられるかもしれませんが、平気そうだったのでやってません。
main.js// 省略 const lightPositions = [ // 明かりの位置 [128, 76], [158, 221], [150, 94], [174, 156], [177, 100], [203, 161], [193, 224], [116, 140], [105, 195], [225, 224], [157, 75], [164, 130], [155, 191], [186, 202], [208, 201], [94, 229], [124, 238], [162, 250], [215, 247] ]; const lightRate = 0.6; // 明かりが光る割合 // 省略 function draw() { var ctx = document.getElementById('tutorial').getContext('2d'); ctx.clearRect(0, 0, 320, 320); ctx.drawImage(treeImage, 0, 0, 320, 320); for(var i = 0; i < lightPositions.length; i++) { if(Math.random() > lightRate) { continue; } var [x, y] = lightPositions[i]; drawLight(ctx, x, y); } } // 省略明かりの描画
渡されたコンテキストに、指定の位置を中心に、決まった色で円を描いています。
少し色気を持たせたかったので、透明度を変えて円を描いています。ここでハマったのは
beginPath()です。これを最初適宜挟まなかったために、透明度が利かなかったり、円と円の間に線が書かれてしまったりしてました。// 省略 const lightRadius = 3; // 明かりの半径 const aroundLightRadiusMagnification = 1.7; // 明かりの近い周辺 const outerLightRadiusMagnification = 3.0; // 明かりの遠い周辺 const lightColor = 'rgb(255, 255, 255)'; // 明かりの色 const aroundLightColor = 'rgba(255, 255, 255, 0.6)'; // 明かりの近い周辺の色 const outerLightColor = 'rgba(255, 255, 255, 0.2)'; // 明かりの遠い周辺の色 // 省略 function drawLight(ctx, x, y) { ctx.beginPath(); ctx.fillStyle = outerLightColor; ctx.strokeStyle = outerLightColor; ctx.arc(x, y, lightRadius * outerLightRadiusMagnification, 0, 2 * Math.PI, false); ctx.fill(); ctx.beginPath(); ctx.fillStyle = aroundLightColor; ctx.strokeStyle = aroundLightColor; ctx.arc(x, y, lightRadius * aroundLightRadiusMagnification, 0, 2 * Math.PI, false); ctx.fill(); ctx.beginPath(); ctx.fillStyle = lightColor; ctx.strokeStyle = lightColor; ctx.arc(x, y, lightRadius, 0, 2 * Math.PI, false); ctx.fill(); }終わりに
単純しか使わなかったですが、canvas、結構楽しいですね。
ちゃんとやれば、オブジェクトごとに描画、みたいな進化もさせられそう、などと思ったり思わなかったり…。
ゲームを作っているひとが結構いますが、たしかにわかる…。作りたくなってきました。
canvas、楽しい!参考
- 投稿日:2020-12-23T22:07:14+09:00
【rails】railsとjsを用いて「いいね機能」を実装してみた
今回はrailsとjsでいいね機能を実装していきたいと思います
** また最後におまけでユーザーがいいねした投稿を表示できるような機能も実装していきます**
jsを読み込んだりする説明は割愛!
参考にさせていただいた記事
https://techtechmedia.com/favorite-function-rails/
https://qiita.com/hayabusa3703/items/2b916e652a1dc85bb6e3完成予想図
下準備
ユーザーはたくさんの投稿にいいねをして、投稿もたくさんのユーザーにいいねされるので
likesテーブルを中間テーブルにした、ユーザと投稿の多対多のテーブル構造
rails g model likeマイグレーションファイル
class CreateLikes < ActiveRecord::Migration[5.0] def change create_table :likes do |t| t.integer :user_id t.integer :drink_id t.timestamps end end endrails g controller likesアソシエーション
like.rb
class Like < ApplicationRecord belongs_to :user belongs_to :drink, counter_cache: :likes_count end・counter_cahce: :likes_countはリレーションされているlikeの数の値をリレーション先のlikes_countというカラムの値に入れますよっていう意味です。なのでlikes_countカラムをstoriesテーブルに追加しましょう。(rails g migration AddLikes_countToStories likes_count:integerをターミナルで実行すればオッケーです。)
この文章の参照元drink.rb
class Drink < ApplicationRecord has_many :likes has_many :liking_users, through: :likes, source: :user endliking_usersモデルは無いので、likesテーブルを中間テーブルにして、userモデルとアソシエーションを汲みますよーってことをrailsに伝えてます
has_manyはbelongs_toはアソシエーションを組むのが本質ではなくて、メソッドを作るメソッド。
つまり,@drink.liking_userとかやったら、その投稿にいいねしたユーザー一覧を取得できるメソッドができるし、アソシエーションも組める
user.rb
has_many :likes has_many :like_drinks, through: :likes, source: :drinkこれも、user.like_drinksとかやったら、そのユーザーがいいねした投稿一覧が取得できる
これは、インスタ、Twitterによくある、そのユーザーがいいねした投稿を表示する時に便利has_manyはbelongs_toはアソシエーションを組むのが本質ではなくて、メソッドを作るメソッド。
これを覚えて帰りましょう。
いいねボタンの記述
drinks/index.html.erb
こちらは投稿一覧のページになります
<%if @drinks%> <% @drinks.each do |drink|%> <li class='list'> <%= link_to drink_path(drink.id) do %> <%= link_to user_path(drink.user.id) do%> <div class="user-info-timeline"> <%=image_tag drink.user.image.variant(resize: '60x60'),class: "user-img-timeline" if drink.user.image.attached?%> <div class="username-timeline"> <%= drink.user.nickname %> </div> </div> <% end %> <div class='item-img-content'> <%= image_tag drink.image , class: "item-img" if drink.image.attached? %> <%# if drink.trade%> <%# end %> </div> <div class='item-info'> <h3 class='item-name'> <%= drink.name %> </h3> <div class='item-price'> <span><%= drink.price %>円<br>(税込み)</span> <div class='star-btn'> <%# image_tag "star.png", class:"star-icon" %> <span class='star-count'>0</span> </div> </div> <div class='item-explain'> <%= drink.explain%> </div> <div class='item-tag'> <% drink.tags.each do |tag| %> #<%=tag.tag_name%> <%end%> </div> <%= render "likes/like",drink: drink%> </div> <% end %> </li> <%end%><%= render "likes/like",drink: drink%>に注目して欲しいです!
まずは可読性を高めるために
画像のいいねボタンを部分テンプレートで切り出しています、そして、,drink: drinkの部分ですが、
<% @drinks.each do |drink|%>のeach文内のブロック変数を、likes/likeにも適用するために変数を受け渡しています。
ブロック変数とは(分かる人は飛ばして)
ブロック変数とは、each文やらtimes文,form_withとか、そのメソッド内だけで使える変数です。
つまり、eachだったらeachから endまでの範囲無で使える変数@drinksにはいろんな情報が、配列として入っていますが、|drink|
とすることで、配列の中の一つ一つの情報がdrinkに入っていって、@drinksにある配列の数だけ表示しますlikes/_like.html.erb
パーシャル(部分テンプレート)であることを分かりやすくするために慣習的にファイル名を_likeとしてます。
ただ<%= render "likes/like",drink: drink%>
で呼び出す時はアンダーバーはいりません
<div class="like" id="like-link-<%= drink.id %>"> <% if current_user.likes.find_by(drink_id: drink.id) %> <%= link_to unlike_path(drink.id), method: :delete, remote: true do %> <div class = "iine__button">❤️<%= drink.likes.count %></div> <% end %> <% else %> <%= link_to like_path(drink.id), method: :post, remote: true do %> <div class = "iine__button">♡️<%= drink.likes.count %></div> <% end %> <% end %> </div>id="like-link-<%= drink.id %>"
がミソ。
jsで非同期で画面を切り替えたいので、idを取得できるように、投稿ごとにidを区別するために
このように記述しましょう。<%= link_to unlike_path(drink.id), method: :delete, remote: true do %>, remote: true
と記述することにより、
リンクを押した時にajaxが発火するので非同期で通信が行われます。
いいねボタンを押したらいいねがすでについてれば、unlike_pathそうじゃなければlike_pathに飛びます
それぞれのpathをまだ定義してないので、このままじゃルーティングエラーになってしまうので
routes.rb
post 'like/:drink_id' ,to: 'likes#like', as: 'like' delete 'like/:drink_id',to: 'likes#unlike', as: 'unlike'と記述しましょう
as: 'like' とすることにより本来ならlikes_like_path(drink.id)とパス指定をしなきゃいけないのですが、
like_path(drink.id)でlikes#likeにpostリクエストを送ることができますこれで、リンクを踏んでリクエストを送ることができたので、次はコントローラーをみていきましょう
likes_controller
class LikesController < ApplicationController include SessionsHelper before_action :set_variables def like like = current_user.likes.new(drink_id: @drink.id) #redirect_to drinks_path # jsを用いるので画面遷移は行わない #binding.pry like.save end def unlike like = current_user.likes.find_by(drink_id: @drink.id).destroy #binding.pry end private def set_variables @drink = Drink.find(params[:drink_id]) @id_name = "#like-link-#{@drink.id}" end endremote: trueのリンクからlike,unlikeアクションが呼び出されるので、
デフォルトの遷移先はilke.js.erb,unlike.js.erbとそれぞれなります。「⚠︎ @id_name = "#like-link-#{@drink.id}"
とControllerにViewの処理を書くのは、MVCパターン的にあまりよろしくないと思いますね。」とご指摘をいただいたので、あまりよく無いですが、機能的には問題無いので一旦次いきます。
likes/like.js.erb
$("<%= @id_name %>").html('<%= escape_javascript(render("likes/like", drink: @drink )) %>');/likes/unlike.js.erb
$("<%= @id_name %>").html('<%= escape_javascript(render("likes/like", drink: @drink )) %>');likes/_like.html.erbにまた戻ります
この時にまた
drink: @drink
と書いて_like.html.erbに変数を受け渡してあげましょうこの@drinkは
likes_controllerの
private def set_variables @drink = Drink.find(params[:drink_id]) @id_name = "#like-link-#{@drink.id}" endの@drinkです。
以上で実装終了です。お疲れ様でした。
おまけ、ユーザーがいいねした投稿を表紙
users/show.html.erb
<%= link_to "#{@user.nickname}がいいねした投稿",user_likes_path(@user.id)%>こんな感じのリンクを作成
@userhはusers#showで@user = User.find(params[:id])
とかよくある感じで定義してますuser_like_pathはまだ定義してないので
routes.rb
get 'user/likes/:id', to: 'users#likes',as: 'user_likes' resources :users do member do get :following,:followers # memberメソッドを使うと # ユーザーidが含まれてるURlを扱うようになる end endresources :userとかみんなやると思うので、resourcesの上に get 'user/likes/:id', to: 'users#likes',as: 'user_likes'
を書きましょう
これで、 リンクを踏んだらusers#likesにGETリクエストを飛ばすことができます
users_controller
def likes @user = User.find(params[:id]) @drinks = @user.like_drinks.paginate(page: params[:page],per_page: 10).order("created_at DESC") endこんな感じで実装しましょう
.paginate(page: params[:page],per_page: 10)
はページネーション をまだ取り入れてなければ書かなくて大丈夫です。
.like_drinksメソッドは
has_many :like_drinks, through: :likes, source: :drinkとuser.rbで書いたので、ユーザーがいいねした投稿一覧を取得できます。
デフォルトで、users/likes.html.erbにリダイレクトされるので、そのビューも用意しましょう
users/likes.html.erb
<div class="user-profile"> <h2 class="user-profile-name"><%= current_user.nickname %></h2> <h2><%= image_tag @user.image.variant(resize: '100x100'),class: 'user-img' if @user.image.attached? %></h2> <div class="user-like-post"> <%= link_to "#{@user.nickname}がいいねした投稿",user_likes_path(@user.id)%> </div> <div class="user-edit"> <% if current_user?(@user) %> <%= link_to "プロフィールを編集",edit_user_path(@user)%> <% end %> </div> <% unless current_user?(@user) %> <div id="follow_form"> <% if current_user.following?(@user) %> <%= render 'unfollow' %> <% else %> <%= render 'follow' %> <% end %> </div> <% end %> </div> <% @user ||= current_user %> <div class="stats"> <a href="<%= following_user_path(@user) %>"> <strong id="following" class="stat"> <%= @user.following.count %> </strong> following </a> <a href="<%= followers_user_path(@user) %>"> <strong id="followers" class="stat"> <%= @user.followers.count %> </strong> followers </a> </div> <div class='main'> <%# 商品一覧 %> <div class='item-contents'> <h2 class='title'><%= @user.nickname%>の投稿</h2> <%= will_paginate @drinks%> <ul class='item-lists'> <%# 商品のインスタンス変数になにか入っている場合、中身のすべてを展開できるようにしましょう %> <%if @drinks%> <% @drinks.each do |drink|%> <li class='list'> <%= link_to drink_path(drink.id) do %> <div class='item-img-content'> <%= image_tag drink.image , class: "item-img" if drink.image.attached? %> <%# if drink.trade%> <%# end %> </div> <div class='item-info'> <h3 class='item-name'> <%= drink.name %> </h3> <div class='item-price'> <span><%= drink.price %>円<br>(税込み)</span> <div class='star-btn'> <%# image_tag "star.png", class:"star-icon" %> <span class='star-count'>0</span> </div> </div> <div class="item-explain"> <%= drink.explain%> </div> </div> <% end %> </li> <%end%> </ul> <%= will_paginate @drinks%> </div> <%end%> </div>自分はこんな感じ
これで以上です。お疲れ様でした。
今までのコードのまとめ
- 投稿日:2020-12-23T21:42:07+09:00
Flash Advent Calendar 23日目 - セキュリティーと脆弱性の解決 -
FlashPlayerで一番問題になっていた事
それは脆弱性の問題だと思います。swf2jsは安全ですか?
そういった質問や疑問があると思います。
今日はFlashPlayerとswf2jsがどう違うのか書こうと思います。マルウェアの感染
インストールが必要か不要か、ここがマルウェアに感染するかしないかの大きな切り分けになります。
FlashPlayerを利用するにはインストールが必要です。このインストールするという行為がマルウェアの侵入経路になっています。
インストールしようとしているインストーラーは本当にAdobeが配布している安全なインストーラーですか?・・・分かりません。
専門知識があれば、判断できるかもしれません。
ですが、専門知識がなければ、そういった判断は難しいと思います。ですが、FlashPlayerはインストールしないと利用できません。。。
そして「インストールする」というボタンを押下すれば
安全でも安全でなくとも、問答無用で利用中のPCにインストールされます。では、次にswf2jsです。
swf2jsはインストールが不要です。なぜならば、swf2jsはJavaScriptファイルだからです。
もし、swf2jsに問題があるとすれば
それは全世界のJavaScriptと同じ問題を抱えている事と同義になると思います。脆弱性の問題
ここでも先に記載した、インストールの有無が関連してきます。
FlashPlayerをインストールすると、PC(OS)からFlashPlayerに対してある程度のアクセス権限が付与されます。
この権限があればPCの内部に直接アクセスする事が可能になります。悪意のあるコードはこの権限を利用して利用中のPCへ攻撃を行います。
これもインストールが必要という点が大きいところです。次にswf2jsです。
先にの述べた通り、swf2jsはJavaScriptファイルです。
ブラウザが許可した権限しかありません。
また、ブラウザが定めた基準(使用方法)に準拠しないと正しく動作しません。
勝手にPCの内部にアクセスしたり、バックドアの起動などできません。このような事から、インストールする事による
FlashPlayerが抱えていた脆弱性の問題を解決したと言い切れます。
ですが、次に出てくる疑問「JavaScriptは安全なの?」っという疑問が出てきます。JavaScriptは安全なの?
JavaScriptも万能ではありません。
セキュリティの脆弱性があります。
- クロスサイトスクリプティング(XSS)
- クロスサイトリクエストフォージェリ(CSRF)
- サーバーサイドJavaScriptインジェクション
などが有名な問題かと思います。
ですが、これらの脆弱性は実装手法で回避が可能です。
また、これらの脆弱性の回避方法も広く知られています。今後はどうなるの?
度々になるのですが、swf2jsはJavaScriptファイルです。
つまり、ブラウザの成長と共に機能の拡張や進化が可能です。
今後の進化に是非ご期待頂ければと思います。いかがだったでしょうか?
明日は紀平さんの記事「Acquiring について」です。
- 投稿日:2020-12-23T21:29:26+09:00
ESP32のすゝめ
はじめに
この記事はAizu Advent Calendar 202023日目の記事です。
時間がギリギリで少し適当な節がありますが、ご了承ください。ESP32 とは
ESP32シリーズは Wi-FiとBluetoothを内蔵する低コスト、低消費電力なSoCのマイクロコントローラ (Wikipediaより)
ということらしいです。
下記に自分が思った利点を簡単にまとめます。Wi-Fiが扱える
Webサーバやアクセスポイント、Webクライアントになる。
GETを送れば、Webからマイコンを操作できます。(今回はこれを説明します。)Bluetoothが扱える
スマホとの通信やBluetoothマウス・キーボードになる。
特にBluetoothマウス・キーボードを使えば、ゲームコントローラーの自作も可能。安い
RaspberryPiなどの他のBluetoothやWi-Fiを扱えるマイコンに比べて安いです。
今回説明していく、Arduino互換のESP32はAmazonで1000円ぐらいです。WebからLEDを操作
実際にESP32で、簡単なWebサーバを作って、WebからLEDを操作していこうと思います。
1.Webページを作る
操作用のWebページを作っていきます。
※CSSとJavaScriptはHTMLファイルの中に直接書いてください。
※ダブルクォーテーションはなるべく使わずにシングルクォーテーションを使ってください。
ctrl.html<!DOCTYPE html> <html> <head> <meta charset='utf-8' name='viewport' content='width =device-width, initial-scale=1'> <title>ESP32 RGB LED controller</title> <script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script> <style> .btn{ width:30%; font-size:100px; } </style> </head> <body> <script> $.ajaxSetup({ timeout: 1000 }); function Send(btn) { $.get('/' + btn); { Connection: close }; } </script> <input type='button' class='btn' id='cold' value='H' onclick='Send("H")'/> <input type='button' class='btn' id='stop' value='L' onclick='Send("L")'/> </body> </html>JS部分の説明をしていきます
16行目$.ajaxSetup({ timeout: 1000 });Ajax通信のタイムアウトを設定しています。
Send関数function Send(btn) { $.get('/' + btn); { Connection: close }; }ESP側に
"/"+btnのGETを送ります。
つまり、Hのボタンを押すと、/Hが、Lのボタンを押すと/Lが送られます。2.ESP32に書き込むプログラムを作る
WebServer.ino#include <WiFi.h> const char* ssid = "ssid"; const char* password = "password"; const String HTML="さっき作ったWebページ(改行なし)"; WiFiServer server(80); const int LED=23; void setup() { pinMode(LED, OUTPUT); Serial.begin(115200); Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("WiFi connected."); Serial.println("IP address: "); Serial.println(WiFi.localIP()); server.begin(); } void loop() { WiFiClient client = server.available(); if (client) { Serial.println("New Client."); String header=""; while (client.connected()) { if (client.available()) { char c = client.read(); if (c=='\r')continue; header.concat(String(c)); if (c == '\n') { Serial.print(header); if(header.indexOf("GET")>=0){ if(header.indexOf("/H")>=0){ digitalWrite(LED,HIGH); }else if(header.indexOf("/L")>=0){ digitalWrite(LED,LOW); }else{ client.println("HTTP/1.1 200 OK"); client.println("Content-type:text/html"); client.println("Connection: close"); client.println(); client.println(HTML); } break; } } } } client.stop(); Serial.println("Client Disconnected."); } }ほとんど、おまじないなので重要な部分だけ説明していきます。
3,4行目const char* ssid = "ssid"; const char* password = "password";ここには、自分の家のルータのssidとパスワードを入れてください。
因みに、Windowsの人はモバイルホットスポットを使うと楽だと思います。
6行目const String HTML="さっき作ったWebページ(改行なし)";ここには、先ほど作ったWebページを改行を無くして代入してください。
改行をなくすには、このようなサイトが便利です。
setup関数はほとんどおまじないなので、軽い説明だけです。
setup関数void setup() { pinMode(LED, OUTPUT); Serial.begin(115200); Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); //WiFiに接続 while (WiFi.status() != WL_CONNECTED) { //接続できるまでループ delay(500); Serial.print("."); } Serial.println("WiFi connected."); Serial.println("IP address: "); Serial.println(WiFi.localIP()); //IPアドレスの表示 server.begin(); //Webサーバの開始 }
loop関数のwhileの中while (client.connected()) { // Webサーバに接続している間、ループする if (client.available()) { //ESP32にデータが送られている場合に実行 char c = client.read(); //送られてきたデータを一文字読み込む if (c == '\r')continue; //\rは無視 header.concat(String(c)); //headerの末尾にcを追加する if (c == '\n') { Serial.print(header); if (header.indexOf("GET") >= 0) { //GETだったら実行 if (header.indexOf("/H") >= 0) { // /Hなら、LEDを光らせる digitalWrite(LED, HIGH); } else if (header.indexOf("/L") >= 0) { // /Lなら、LEDを消す digitalWrite(LED, LOW); } else { // /等の場合はWebページを送る client.println("HTTP/1.1 200 OK"); client.println("Content-type:text/html"); client.println("Connection: close"); client.println(); client.println(HTML); } break; } } } }簡単に説明すると、
1. GETなどのデータがheaderに入り、
2. それがGETの/Hや/Lだった場合はLEDを制御して、
3. GETの/の場合はWebページ(HTML)のデータを送る。まとめ
ESP32は安い&簡単にBluetooth通信やWebの通信が取り扱えます。
皆さんも、RaspberryPiだけでなくESP32も使っていきましょう!
今度、書けなかったBluetoothに関する記事を書こうと思っています。
- 投稿日:2020-12-23T20:45:32+09:00
【JavaScript】JavaScript入門(1)
はじめに
JavaScriptは現在人気となっている言語の一つですが、その生い立ちや作成された背景などを知っている人は意外に少ないのではないでしょうか?
そこでこのような作成背景や生い立ちなどを知ると、JavaScriptをより多面的に深く理解できるのではないか?と思い、本記事を作成しました。
JavaScriptを一度学んだことがある人でも意外と知らなかったことがあると思いますので、ぜひ最後までご覧になってください。
対象読者
- JavaScriptを学び始めた人
- プログラミング始めたての人
- JavaScriptとECMAScriptの違いがわからない人
本記事の内容
1. JavaScriptの生い立ち
2. JavaScriptとECMAScript
3. 実行環境による違い
4. まとめ
5. 最後に
6. 参照サイト1. JavaScriptの生い立ち
JavaScriptとは?
JavaScriptは、1990年代のインターネット黎明期にNetscape Communications社によって開発された、ブラウザ向けのスクリプト言語1です。
開発当初は
LiveScriptと呼ばれていましたが、当時非常に注目を浴びていたJava言語にあやかって、その後JavaScriptと名前を改めることになります。このため、誤解を招きやすいのですが、JavaとJavaScriptとは全くの別言語であり、互換性もありません。
JavaScriptの歴史①:実装開始〜不遇期
続いてJavaScriptの歴史について軽く紹介します。
JavaScriptは、1995年に当時最大規模のシェアを誇っていた
Netscape Navigator2.0と呼ばれるブラウザで実装されたのを皮切りに、1996年にはInternet Explorer3.0でも実装され、そしてその後ブラウザ標準のスクリプト言語として定着していきました。しかし、JavaScriptが実装されたことにより様々なエフェクトを実現できるようになったため、多くの人が過剰な装飾をJavaScriptに勝手に盛り込んでいきました。
その結果、装飾過剰で、使い勝手の悪いWebページが量産されるようになりました。
そのためJavaScriptは
「ダサいページを作成するための言語」「使い勝手が非常に悪く、プログラミングの素人が使う低俗な言語」というイメージだけが定着してき、不遇の時代へと入っていくことになります。JavaScriptの歴史②:復権
そのような状況に光明が見えたのが、2005年、
Ajax(Asynchronous JavaScript + XML)という技術が登場した時です。Ajaxというのは、一言でいうならブラウザを再読み込みすることなく情報を更新する通信方法(非同期通信)をJavaScriptを用いて行う処理のプログラム手法のことを言います。
Google Mapのように、
読み込むことがなく情報を取得できるアプリケーションを想像してもらえればわかりやすいかもしれません。Ajaxにより、HTML、CSS、JavaScriptといったブラウザ標準の技術だけでよりリッチなコンテンツを作ることができるようになり、Ajax技術は瞬く間に普及を遂げました。
そしてそのAjax技術の中核を担っていたJavaScriptは、その価値が再度見直されるようになっていきました。
またさらに2000年代後半にはHTML5が登場したことにより、さらに追い風が吹きます。HTML5により、アプリ開発のためのJavaScript API2が強化され、より機能が充実しました。
機能 概要 Geolocation API ユーザーの地理的な位置を取得 Canvas JavaScriptから動的に画像を描画 File API ローカルのファイルシステムを読み書き Web Storage ローカルデータを保存するためのストレージ Indexed Database キー/値のセットでJavaScriptのオブジェクトを管理 Web Workers JavaScriptをバックグラウンドで並列実行 Web Sockets クライアントーサーバー間の双方向通信を行うためのAPI
またこれ以降、SPA(Single Page Application)3の流行などにより、JavaScript人気にさらに拍車がかかっていき、現在に至ります。2. JavaScriptとECMAScript
ECMAScriptの誕生
1995年、1996年と立て続けに当時最大規模のシェアを誇っていた
Netscape NavigatorとInternet ExploreでJavaScriptは実装されました。しかし、この二つで実装されたJavaScriptというのは言語仕様が異なっており、二つの言語の間に互換性がありませんでした。
そのため、開発者サイドはブラウザ間で仕様の異なる言語を用いることになり、苦労することになりました。
そこで、出てきたのが
ECMAScriptと呼ばれるものです。どういうことかというと、JavaScript言語のコアな部分をECMAScriptとして仕様策定し、切り出すことにしたのです。
こうすることにより、ブラウザ間での言語仕様を統一することができ、JavaScriptは開発者にとって使い勝手の良いものへとなりました。
JavaScriptとECMAScript
先ほども述べた通り、ECMAScriptというのはプログラミング言語の仕様であり、その仕様に基づいて実装されたプログラミング言語がJavaScriptということになります。
また、JavaScript以外にECMAScriptに基づいて実装されたプログラミング言語はほとんど存在していません。
これらにより、実質的にJavaScriptの一部の仕様がECMAScriptということになってしまっているのです。
3. 実行環境による違い
JavaScriptの言語仕様の一部がECMAScriptということは、JavaScriptはその実行環境によって機能が変わってくるということを意味します。
元々はブラウザ上で動作することを想定して作られた言語ですが、現在はブラウザ上での用途に止まりません。
ここでは最も基本的な
ブラウザ環境下とPC上でJavaScriptを動作させるソフトウェアのNode.js環境下でのJavaScriptの機能について見ていきましょう。(これ以外にも様々な実行環境が存在する。)
ブラウザ環境
ブラウザ環境の場合、ECMAScriptの他に
Web APIsと呼ばれるものがあります。これはJavaScripからブラウザの機能を操作する際に用いられるもので、例えば画面の更新をする際には
DOM APIと呼ばれるものを使用します。つまり、ブラウザ環境下でJavaScriptを使用する場合はECMAScriptとWeb APIsの機能を用いることができるということになります。
Node.js環境下
Node.js環境下の場合は、ECMAScriptに加えて、
CommonJSと呼ばれるモジュールを管理するための仕様を定めたものが存在します。これによりNode.js環境下でJavaScriptを使用する場合はECMAScriptとCommonJsの機能を用いることができるということが言えますね。
まとめると、ここで言いたいことはJavaScriptというのは実行環境によって使用できる機能が異なってくるということです。
4. まとめ
- JavaScriptとは
1990年代に開発されたブラウザ向けのスクリプト言語のこと- ECMAScriptとは
JavaScriptのコアな部分の仕様のこと- JavaScriptとは
ECMAScriptの仕様に基づいて実装されているプログラミング言語のこと- JavaScriptは
実行環境によって使用できる機能が変わってくる5. 最後に
本記事の内容がみなさんの参考になれば嬉しいです。
最後までご覧いただきありがとうございました。
6. 参考文献
Udemy講座: 【JS】初級者から中級者になるためのJavaScriptメカニズム
書籍:改訂新版JavaScript本格入門
スクリプト言語とは簡単にいうと、誰でも簡単にキャッチアップできるように作成されたプログラミング言語のことです。 ↩
APIとはApplication Programming Interface」の頭文字で、何らかの機能をその外部のプログラムから利用するための決まり事のことを指します。簡単にいうと、プログラムとソフトウェアとの接点と思ってもらったら結構です。 ↩
SPAとは単一のページで構成されるWebアプリケーションのことです。初回のアクセスではまずページ全体を取得しますが、以降のページ更新は基本的にjavaScriptだけでまかなっていきます。デスクトップアプリによく似た操縦性や敏速な動作を実現するためのアプローチとして近年注目を浴びているワードです。 ↩
- 投稿日:2020-12-23T19:37:24+09:00
【TypeScript】リテラルとオブジェクト型【勉強メモ】
リテラル
TypeScript,JavaScript初学者がリテラルとオブジェクト型について勉強したのでメモを残す。
リテラルとは
- JavaScript のプリミティブである値そのものである。
リテラルは「文字通り」という意味の単語で、ソースコードに数値や文字列をベタガキしてその値を表現する式。
- Boolean 型
trueとfalseの2種類の真偽値リテラル。- Number 型
100や−50のように数字を記述する数値リテラル。先頭に0xをつけると 16 進数、0oで 8 進数、obで 2 進数を表現できる。- BigInt 型
100nのように数字の後ろにnをつけて表現する数値リテラル。- Null 型
- null リテラルである
nullは、プリミティブ値nullを返す。- オブジェクト型
- 配列リテラル
[1,2,3]の形式で記述する。Arrayオブジェクトのインスタンスとして生成される。([1,2,3]って書くと配列になるよってことかな?)- オブジェクトリテラル
{key: value}の形式で記述する。obj.keyまたはobj[key]の 2 つの構文が利用できる。Objectオブジェクトのインスタンスとして生成される。- 正規表現リテラル
/pattern/ig形式で記述する。正規表現パターンでの特殊文字の使い方は、ほかの言語とほぼ共通。RegExpオブジェクトのインスタンスとして生成される。- オブジェクト型は全てビルドインオブジェクトの
objectがベースになっている。> const isBoolean = true > isBoolean.__proto__.constructor undefined [Function: Boolean] > isBoolean.__proto__.__proto__.constructor [Function: Object] > isBoolean.__proto__.__proto__.__proto__ null > > const num = 100 undefined > num.__proto__.constructor [Function: Number] > num.__proto__.__proto__.constructor [Function: Object] > > const bigInt = 100n undefined > bitInt.__proto__.constructor [Function: BigInt] > bitInt.__proto__.__proto__.constructor [Function: Object] > num.__proto__.__proto__.__proto__ nullリテラル型を定義する
```typescript // 任意のリテラル型を定義する type Gender = `man` | `woman`; let gender: Gender; gender = "man"; // OK gender = "woman"; // OK gender = "boy"; // NG gender = 10; // NG ```参考
- リテラル型 - TypeScript Deep Dive 日本語版
- TypeScript の型: リテラル型を定義する (Literal types)|まくろぐ
- りあクト! TypeScript で始めるつらくない React 開発 第 3 版【Ⅰ. 言語・環境編】
何か指摘等ございましたら、コメントでお願いいたします。
- 投稿日:2020-12-23T17:37:48+09:00
VueのWatchをJavascriptでやる方法
MutationObserverを使うことでやりたかったことができた。
以下DOMを監視して変化したときに関数を実行したい。
//監視対象のDOM <p id="target">2020-12-23 11:00</p> //監視ターゲットの取得 const target = document.getElementById('target') // オブザーバーの作成 const observer = new MutationObserver(records => { //実行したい処理 }) // 監視の開始 observer.observe(target, { //今回はtarget配下の要素が変化した時なのでchildListを指定 childList:true } //アロー関数で書くことでもう少しコンパクトになった new MutationObserver(() => { //実行したい処理 }) .observe(target, {childList: true});参考
- 投稿日:2020-12-23T17:37:48+09:00
DOMの監視をJavascriptでやる方法
MutationObserverを使うことでやりたかったことができた。
以下DOMを監視して変化したときに関数を実行したい。
//監視対象のDOM <p id="target">2020-12-23 11:00</p> //監視ターゲットの取得 const target = document.getElementById('target') // オブザーバーの作成 const observer = new MutationObserver(records => { //実行したい処理 }) // 監視の開始 observer.observe(target, { //今回はtarget配下の要素が変化した時なのでchildListを指定 childList:true } //アロー関数で書くことでもう少しコンパクトになった new MutationObserver(() => { //実行したい処理 }) .observe(target, {childList: true});参考
- 投稿日:2020-12-23T17:34:13+09:00
E2Eテスト -マークアップチェック編 clientScriptsメソッド-
これまで、
- 各ページのスクリーンショットを撮る
- フォーム送信の動作確認
- metaなどの情報取得チェック
をTestcafeで実装してきました。
今回はさらに「h1があるか」「画像のaltが抜けていないか」などSEOにも関わってくるマークアップチェックも行いましたので実装方法を書いていこうと思います!
。。。が、その前に!今回はTestcafeの
.clientScriptsメソッドを使って実装していくので、まずはこのメソッドについて公式ドキュメントから抜粋した内容を書いていきます。.clientScriptsメソッド
Fixture.clientScripts / Test.clientScripts
テスト中にアクセスしたすべてのページにスクリプトを挿入します
【Fixtureの場合】
fixture.clientScripts( script[, script2[, ...[, scriptN]]] ) → thissample.jsfixture `My fixture` .page `http://example.com` .clientScripts('assets/jquery.js'); fixture .clientScripts({ page: /\/user\/profile\//, content: 'Geolocation.prototype.getCurrentPosition = () => new Positon(0, 0);' });【Testの場合】
test.clientScripts( script[, script2[, ...[, scriptN]]] ) → thissample.jstest ('My test', async t => { /* ... */ }) .clientScripts({ module: 'async' }); test ('My test', async t => { /* ... */ }) .clientScripts({ page: /\/user\/profile\//, content: 'Geolocation.prototype.getCurrentPosition = () => new Positon(0, 0);' });*
pageオプションを利用して、スクリプトを挿入するページを指定できます。
このオプションがない場合は、テスト中にアクセスしたすべてのページにスクリプトを挿入します。Javascriptファイルを挿入する
pathプロパティを使用して文字列またはオブジェクトを渡すことができます。【Fixtureの場合】
fixture.clientScripts(filePath | { path: filePath })
fixture.clientScripts(filePath | { path: filePath }, ...)
fixture.clientScripts([ filePath | { path: filePath } ])sample.jsfixture `My fixture` .page `https://example.com` .clientScripts('assets/jquery.js');【Testの場合】
test.clientScripts(filePath | { path: filePath })
test.clientScripts(filePath | { path: filePath }, ...)
test.clientScripts([ filePath | { path: filePath } ])sample.jstest ('My test', async t => { /* ... */ }) .clientScripts('assets/jquery.js');モジュールを挿入する
テストされたページにコンテンツを注入するNode.jsモジュールの名前を指定します。
moduleプロパティを持つオブジェクトを使用します。
TestCafeはNode.jsの仕組みを利用してモジュールのエントリーポイントを検索し、その内容をテストされたページに注入します。【Fixtureの場合】
fixture.clientScripts( { module: moduleName } )
fixture.clientScripts( { module: moduleName }, ... )
fixture.clientScripts([ { module: moduleName } ])sample.jsfixture `My fixture` .page `https://example.com` .clientScripts({ module: 'lodash' });【Testの場合】
test.clientScripts( { module: moduleName } )
test.clientScripts( { module: moduleName }, ... )
test.clientScripts([ { module: moduleName } ])sample.jstest ('My test', async t => { /* ... */ }) .clientScripts({ module: 'lodash' });スクリプトコードを挿入する
contentプロパティを持つオブジェクトを渡して、挿入されたスクリプトを文字列として提供できます。【Fixtureの場合】
fixture.clientScripts({ content: code })
fixture.clientScripts({ content: code }, ...)
fixture.clientScripts([ { content: code } ])sample.jsconst mockDate = ` Date.prototype.getTime = function () { return 42; }; `; fixture `My fixture` .page `https://example.com` .clientScripts({ content: mockDate });【Testの場合】
test.clientScripts({ content: code })
test.clientScripts({ content: code }, ...)
test.clientScripts([ { content: code } ])sample.jsconst mockDate = ` Date.prototype.getTime = function () { return 42; }; `; test ('My test', async t => { /* ... */ }) .clientScripts({ content: mockDate });特定のページにスクリプトを提供する
スクリプトを挿入するページを指定することもできます。これにより、指定したページでブラウザAPIをモックし、他のすべての場所でデフォルトの動作を使用できるようになります。
スクリプトのターゲットページを指定するには、clientScriptsに渡すオブジェクトにpageプロパティを追加します。【Fixtureの場合】
fixture.clientScripts({ page: url, path: filePath | module: moduleName | content: code }) fixture.clientScripts({ page: url, path: filePath | module: moduleName | content: code }, ...) fixture.clientScripts([ { page: url, path: filePath | module: moduleName | content: code } ])sample.jsfixture `My fixture` .page `https://example.com` .clientScripts({ page: /\/user\/profile\//, path: 'dist/jquery.js' });【Testの場合】
test.clientScripts({ page: url, path: filePath | module: moduleName | content: code }) test.clientScripts({ page: url, path: filePath | module: moduleName | content: code }, ...) test.clientScripts([ { page: url, path: filePath | module: moduleName | content: code } ])sample.jstest ('My test', async t => { /* ... */ }) .clientScripts({ page: /\/user\/profile\//, path: 'dist/jquery.js' });挿入されたスクリプトでDOMにアクセスする
TestCafe はカスタムスクリプトを head タグに注入します。
これらのスクリプトは、DOM がロードされる前に実行されます。
これらのスクリプトでDOMにアクセスするには、DOMContentLoadedイベントが発生するまで待ちます。sample.jsconst scriptContent = ` window.addEventListener('DOMContentLoaded', function () { document.body.style.backgroundColor = 'green'; }); `; fixture `My fixture` .clientScripts({ content: scriptContent });その他の方法
コマンドラインオプション
--cs(--client-scripts)コマンドラインオプションは、同様に複数の引数をサポートしています。
testcafe chrome test.js --client-scripts mockDate.js,assets/react-helpers.js
- JSファイルを挿入する場合
testcafe chrome my-tests --cs assets/jquery.js
- 複数のスクリプトを指定する場合
testcafe chrome test.js --client-scripts mockDate.js,assets/react-helpers.jsAPIメソッド
runner.clientScripts
page、contentおよびmoduleプロパティは配列を取ることができないことに注意してください。同じページに複数のスクリプトを挿入するには、スクリプトごとに1つの引数を渡します。runner.clientScripts('mockDate.js', 'scripts/react-helpers.js');
- JSファイルを挿入する場合
runner.clientScripts('assets/jquery.js');
- 特定のページにスクリプトを提供する場合
runner.clientScripts({ page: /\/user\/profile\//, path: 'dist/jquery.js' });
- スクリプトをiframeに挿入する場合
runner.clientScripts({ path: 'scripts/helpers.js', page: 'https://example.com/iframe/' }));
- 複数のスクリプトを指定する場合
runner.clientScripts(['scripts/react-helpers.js', 'dist/jquery.js']);const scripts = ['test1.js', 'test2.js', 'test3.js']; runner.clientScripts(scripts.map(script => { path: script, page: 'http://example.com' }));設定ファイルのプロパティ
clientScripts設定ファイルのプロパティは、配列を取ることができます。testcaferc.json{ "clientScripts": ["mockDate.js", "scripts/react-helpers.js"] }
- JSファイルを挿入する場合
testcaferc.json{ "clientScripts": "assets/jquery.js" }
- モジュールを挿入する場合
testcaferc.json{ "clientScripts": { "module": "lodash" } }
- スクリプトコードを挿入する場合
testcaferc.json{ "clientScripts": { "content": "Date.prototype.getTime = () => 42;" } }
- 特定のページにスクリプトを提供する場合
testcaferc.json{ "clientScripts": { "page": "https://myapp.com/page/", "content": "Geolocation.prototype.getCurrentPosition = () => new Positon(0, 0);" } }
- 複数のスクリプトを指定する場合
testcaferc.json{ "clientScripts": ["vue-helpers.js", { "page": "https://mycorp.com/login/", "module": "lodash" }] }以上が公式から参照した
.clientScriptsメソッドについてのドキュメントになります。
次回の記事ではこのメソッドを使ったマークアップチェックの実装方法を書いていきますので併せてご覧いただけると嬉しいです。参照
- 投稿日:2020-12-23T17:20:27+09:00
kintoneプラグインのカレンダーPlusのリソース別スケジュール管理機能をカスタマイズする
カレンダーPlus Advent Calendar 2020 の12/23担当分です。
目次
0.はじめに
1.カレンダーPlusとは?
2.カレンダーPlus JavaScript APIを使う
3.FullCalendarについて
4.注意事項
5.カスタマイズ
6.課題
7.おわりに0. はじめに
実はQiita初投稿です。
はじめまして。
札幌でkintoneを活用した業務改善ソリューションを提供している、株式会社インセンブルの濱内です。
最近はコードを書くことも少なくなってきているのですが、最近お客様の希望を叶えるために行ったカレンダーPlusのカスタマイズをご紹介します。
具体的には、リソース別スケジュール管理機能の#月/#週の表示において、日付が記載されているヘッダ部分にその日の件数を表示させてみます(ニッチすぎる内容。。。。)
なお、DOMを操作するカスタマイズですので、サポートの対象外であり、バージョンアップなどにより動作しなくなるリスクなどがありますことをご了承ください。1. カレンダーPlusとは?
※kintoneの説明はここでは省略します。
カレンダーPlusはラジカルブリッジが開発・販売を行っている、kintoneプラグインです。
kintone標準のカレンダー表示は機能に乏しく、実用性は厳しいものがあります。
それをステキに機能強化してくれるプラグインです。Basic版とPro版
カレンダーPlusには、Basic版とPro版が存在します。
Basic版は月別/週別/日別表示ができる、Googleカレンダーのような使い勝手を実現してくれるものです。
Pro版は、リソース別スケジュール管理機能が使えるようになり、担当者や会議室別など、最大5軸を自由に切り替えてスケジュール表示できるというものです。
試用期間無制限で使うことができるという太っ腹なプラグイン。
試用期間中は1回カレンダー上で操作するたびにうざいAlertポップアップが出てくるだけです。
その試用期間でじっくり動作を検証しましょう。
そして、購入はなんとライセンス買い切り!(※日本国内。海外は国によってはサブスクの料金体系があるようです)
バージョンアップによって機能強化が続けられていますが、自分でアップデートすれば新機能も使うことができます。ちなみに、最近、カレンダーPlusエバンジェリスト制度ができまして、私もエバンジェリストに任命していただきました。
2. カレンダーPlus JavaScript APIを使う
カレンダーPlusにはカレンダーPlus用のJavaScript APIが用意されています。
これを使うことで、動作順序を保障した処理が可能になり、安全にカスタマイズを行うことができます。
ただし、今はまだeventは結構限られている印象です。
マニアックなカスタマイズを行おうとすると、実現できないこともあります。
今後の充実に期待しましょう。APIリファレンスはこちら。
例えばこのように書きます(リファレンスP.9より引用)
kintone.events.on('app.record.index.show', function(e) { calendarplus.events.on('cp.calendar.show', function(event) { alert("カレンダーが表示されました"); }); });kintone JavaScript APIみたいですよね?
kintone開発者なら習得コストが極小で書けることでしょう。カレンダーPlusアドベントカレンダー12/19の記事でrex0220さんが更に詳しく記載されていますので、そちらをご参照ください。
https://qiita.com/rex0220/items/ac9077762f29d2c3d4a13. FullCalendarについて
カレンダーPlusは、FullCalendarというJavaScriptのライブラリを用いて開発されています。
https://fullcalendar.io/この記事を書いている今日現在、最新版はv5ですが、カレンダーPlusが使用しているバージョンはv3となります。
カレンダーPlusはv3でだいぶ作り込まれているので、FullCalendarのバージョンが上がる予定は今のところは無さそうです。カレンダーPlus JavaScript APIでは、FullCalendarのviewオブジェクトを取得することができ、viewオブジェクトを介して操作することが可能です(開発元のサポート対象外となります)
4. 注意事項
- クラス名などを用いてDOMを操作しています。kintoneおよびカレンダーPlusのアップデートによって動作しなくなる可能性があります。
- 同じ理由で、kintoneおよびカレンダーPlusの動作に影響を与える可能性があります。
5. カスタマイズ
冒頭にも記載しましたが、今回やりたいのは、リソース別管理機能の表示時に、日毎のヘッダ部分に、その日の予定数を記載したいというものです。
予定の管理として、予定数がひと目で把握できると便利ですよね。多分。今回対応するのは「#月」表示と「#週」表示とします。
カレンダーPlusの「カレンダー画面の描画後イベント」を登録する
カレンダーPlus JavaScript APIで使用できるイベントハンドラーを参照します。
今回はカレンダー表示に情報を付加しますので、カレンダー画面の描画後イベントを使用します。
登録は下記のようなコードとなります。kintone.events.on("app.record.index.show", function(e) { if (e.viewType !== 'custom') return e; calendarplus.events.on('cp.calendar.show', function(event) { // #月表示/#週表示に対応 if (event.view.type === 'timelineWeek' || event.view.type === 'timelineMonth') { /* 今回の処理 */ } }); return e; });ポイントは、「#月」表示と「#週」表示の判定です。
eventハンドラーで取得できるviewオブジェクトのtypeプロパティに格納されている値を参照することで可能です。
typeプロパティの値はFullCalendarのドキュメントを参照し、カレンダーPlusの表示モードと対応させると下記のようになっています。
カレンダーPlusの表示モード プロパティの値 #年 timelineYear #月 timelineMonth #週 timelineWeek #日 timelineDay 参考:https://fullcalendar.io/docs/v3/timeline-view
日付リストの作成
event.recordsをすべて参照し、日毎の数を計算します。
日付のリストを作成しておきましょう。FullCalendarのDOMを参照して作成しました。
// 日付リスト作成 event.view.el.find('th').each(function (k, v) { if ($(v).attr('data-date')) { dateList.push($(v).attr('data-date')); } });コード全体
以下の処理はコード全体を参照ください。
jQuery.noConflict(); (function($) { "use strict"; // カレンダーの開始日時・終了日時のフィールド名を設定 const cpCustomConfig = { startFieldCode: "開始日", endFieldCode: "終了日" } var records = {}; // カレンダー上で更新される最新のレコード情報を格納する kintone.events.on("app.record.index.show", function(e) { if (e.viewType !== 'custom') return e; for (const record of e.records) { records[record['$id'].value] = record; } // イベントレコード描画時イベントのrecordオブジェクトを格納して最新のrecord情報を保持する calendarplus.events.on('cp.event.show', function (event) { if (event.view.type === 'timelineWeek' || event.view.type === 'timelineMonth') { records[event.record['$id'].value] = event.record; render(records, event); } }); calendarplus.events.on('cp.calendar.show', function(event) { // #月表示/#週表示に対応 if (event.view.type === 'timelineWeek' || event.view.type === 'timelineMonth') { render(records, event); } }); return e; }); function render(records, event) { const dateList = []; // 日付リスト作成 event.view.el.find('th').each(function (k, v) { if ($(v).attr('data-date')) { dateList.push($(v).attr('data-date')); } }); const countList = []; // 日ごとの予定数を格納 // 対象日のスケジュールをカウント for (const currentDate of dateList) { const currentDateM = moment(currentDate); let count = 0; for (const key in records) { // 開始日・終了日 未設定は対象外 const record = records[key]; if (record[cpCustomConfig.startFieldCode].value == null || record[cpCustomConfig.endFieldCode].value == null) continue; const startDateM = moment(record[cpCustomConfig.startFieldCode].value, 'YYYY-MM-DD'); const endDateM = moment(record[cpCustomConfig.endFieldCode].value, 'YYYY-MM-DD'); if (startDateM.isSame(endDateM)) { // From Toが同じ日の場合 if (startDateM.isSame(currentDateM)) { count++; } } else if ( // 終了日が翌日0:00で登録されているため、momentで1日戻す !(startDateM.isBefore(currentDateM, 'day') && endDateM.subtract(1, 'd').isBefore(currentDateM, 'day')) && !(startDateM.isAfter(currentDateM, 'day') && endDateM.subtract(1, 'd').isAfter(currentDateM, 'day')) ) { count++; } } countList[currentDate] = count; } // 数量表示を削除 event.view.el.find('th .cp-custom-count').remove(); // 数量表示の挿入 event.view.el.find('th').each(function (k, v) { if ($(v).attr('data-date')) { const dateStr = $(v).attr('data-date'); const $elem = $('<span class="cp-custom-count">(' + countList[dateStr] + ')</span>'); $(v).append($elem); } }); } })(jQuery);できあがり
6. 課題
- 言うまでもありませんが、DOMを操作してますので、アップデートなどで動かなくなるリスクがあります。ただし、FullCalendarのバージョンが上がる可能性は直近では低いと思われますし、kintoneアップデートの影響を受けるようなカスタマイズはしてないので、低リスクとは考えています。
- 開始/終了は、日付前提で作成しています。日時の場合は更に分岐が必要でしょう。
- デバッグ足りない気がするので、不具合を見つけた方はこっそり教えて下さい。。。
7. おわりに
実は記事を書き始めてから致命的なバグに気づいてしまい、逃げの仕様に変更したりしたのですが、なんとか完成に漕ぎ着けました。イケてないところを見つけた方は教えて下さい?♂️
カレンダーPlus JavaScript APIのイベントがもっと増えると、カスタマイズの可能性がさらに広がりますね!
みなさんもカスタマイズを考えてみて、このイベントが欲しい!などとじゃんじゃんリクエストしてみましょう。
- 投稿日:2020-12-23T15:14:13+09:00
S3 で ホスティングしたウェブサイトをSSL化し、かつ特定の IP のみに開示する方法
はじめに
この記事は2020年の RevComm アドベントカレンダー25日目の記事です。クリスマス当日ですね!
前日は @qii-purine さんの「pythonでのアーキテクチャを考える」でした。
今回は最後となりますが、S3 で ホスティングしたウェブサイトをSSL化し、かつ特定の IP のみに開示する方法について紹介します。
やりたいこと
aws 使っている会社で、自分の作ったサイト(例えばデモサイト)を社内で共有するとき、みなさまどのように共有しますか。
典型的なやり方だと EC2 や Fargate でフロントエンドのサーバを立てるのではないかと思います。また、複数の静的ウェブページであれば、サーバーレスも検討するのではないかと思います。
そんな中、A) 頻繁にアクセスしない、B) 静的ファイルでも良い といった場合、 S3でのホスティング はコスト的に有効です。
しかし A) 社内のVPN のみで共有したい、B) サイトを転送時に暗号化したい と言う条件が加わると、SSL化 と 特定のIPのみに開示 する必要があります。そのためには SSL 証明書発行 と ファイヤーウォール を用意する必要があります。
今回 Route 53, WAF, CloudFront, Certificate Manager, S3 を使って, どのようにSSL化して S3 でホスティングするかを紹介します。
また、今回 CloudFront を使いますが、CloudFront の場合はデータがキャッシュされるため、TTL (Time to Live) を意図的に設定しない限り変更が即時反映されません。TTL を短くすればいい話ですが、今回 S3 にあるサイトを変更したときにどのようにサイトを更新し、変更を即反映させるかについて紹介します。
( 内容はこちらのリンクとほぼ被りますので、もし、本記事でわからないことがあればそちらを読んでいただけると幸いです)
全体構成
全体構成はこんな感じです。
作成手順
取り組む前の準備
- Route 53 でメインドメインをまだ作成していない場合は作成してください。
- Route 53 に追加するレコード名を予め、検討してください。( revcomm-christmas-demo.example.com とします。)
素材の準備
まず、画像を用意します。(頑張ってコピー等でダウンロードしてください。)
コードの準備
index.html<!DOCTYPE html> <html> <head> <meta charset='utf-8'> <title>Merry Christmas</title> <link rel='stylesheet' type='text/css' media='screen' href='main.css'> </head> <body> <div class="content"> <img class="tree" src="./christmas_tree.png"> </div> </body> </html>main.css.content{ text-align:center; position: relative; width: 100%; height: 100%; } .content .text{ position: absolute; text-align:center; top: 50%; left: 50%; transform: translate(-50%, -50%); font-family: 'Charm', cursive; font-size: 5em; } html { width: 100%; height: 100%; } body{ width: 100%; height: 100%; background: radial-gradient(#ffffff 50%, #91bee5 100%); } .ball{ position: absolute; padding: 0px; margin: 0px; width: 20px; height: 20px; border-radius: 10px; background-color: rgb(218, 233, 247); }S3 の設定
S3 にアクセスし、バケットを新規作成してください。(revcomm-christmas-demo にします)
次にコンテンツをアップロードしてください。(ドラッグアンドドロップで可能)
これで S3 にコンテンツが追加されました。
Certificate Manager の設定
SSL 証明書の作成です。
Certificate Manager にアクセスし、Region を N.Virginia にした状態で SSL 証明書の新規作成をクリックしてください。予め検討した URL を入力してください。
DNS検証 にしてください。
レコード作成を選択してください。
レコードを追加すると、 Route 53 に SSL 証明書用のレコードが追加されます。
CloudFront の設定
CloudFront にアクセスし、CloudFront Distribution の新規作成をクリックしてください。
以下のようにパラメータを修正してください。(英語のままですが、ご容赦願います。)
- Enable Origin Shield: No
- Restrict Bucket Access: Yes
- Origin Access Identity: Create New Identity
- Generate Read Permissions on Bucket: Yes
- Viewer Protocol Policy: Redirect
- Price class: Use All Edge
- Default Root Object: index.html
- Alternate Domain Names: 予め検討した URL (revcomm-christmas-demo.example.com)
- SSL Certificate: Custom SSL Certificate
- Custom SSL: 予め検討した URL をタイプしてみてください。対応する SSL が出ます。(revcomm-christmas-demo と書けば出るはず)
しばらくすると、デプロイが完了します。(15分以上かかる可能性あり)
完了したら、 CloudFront のドメイン名をメモってください。補足
- S3 のポリシーも自動的に更新します。
- OAI (Origin Access Identity) を使用することで、 S3 をpublic化をしない状態でホスティングすることができます。
Route 53 のレコード作成
Route 53 にアクセスし、ドメイン名 > レコードを追加 をクリックしてください。
レコードを作成してください。
Route ポリシーを Simple Route、 レコード名を予め検討した URL のサブドメイン名(revcomm-christmas-demo)、 レコードタイプをCNAME、 Value を先ほどメモった CloudFront を URL 入力してください。WAF の設定
WAF にアクセスし、IP Sets で Region を Global (CloudFront) にした状態で新規 IP Set を作成してください。
IP を設定してください。
次にファイヤウィールの設定です。Web ACLs (Access Control List) で Region を Global (CloudFront) にした状態で 新規 ACL を作成してください。
任意の名前を設定して、関連リースの追加をクリックしてください。
WAF に関連するリソースを追加してください。(CloudFrontのID番号が表示する)
次へをクリックし、Add rules > Add my own rules ... をクリックしてください。
ルールタイプ IP Set を選択し、 ルール名(任意)を好きな名前にした状態で先ほど作成した IP set を入力し、作成してください。Default Action を Allow にしてください。
デフォルトを Block にしてください。
あとは ACL を作成して完成です。
結果
予め検討した URL (revcomm-christmas-demo.example.com) にアクセスすると、以下のようになります。
ちなみに IP アドレスを変えると、以下のようになります。
更新手順
次に更新方法です。
コードの修正
以下のようにコードを修正してください。
index.html<!DOCTYPE html> <html> <head> <meta charset='utf-8'> <title>Merry Christmas</title> <link rel='stylesheet' type='text/css' media='screen' href='main.css'> <link rel="preconnect" href="https://fonts.gstatic.com"> <link href="https://fonts.googleapis.com/css2?family=Charm:wght@700&family=Pacifico&display=swap" rel="stylesheet"> </head> <body> <div class="content"> <img class="tree" src="./christmas_tree.png"> <div class="text"> We wish you a Merry Christmas <br /> and a Happy New Year </div> </div> </body> <script> function drop_snow(){ var snow_ball = document.createElement("div") snow_ball.className = "ball" snow_ball.style.top = 0 + 'px' snow_ball.style.left = Math.random() * document.body.clientWidth + 'px' var content = document.getElementsByClassName('content')[0] content.appendChild(snow_ball); var pos = 0 var refreshIntervalId = setInterval(frame, 10); function frame() { if (pos > document.body.clientHeight) { content.removeChild(snow_ball); clearInterval(refreshIntervalId); } else { pos+= 1; snow_ball.style.top = pos + 'px'; snow_ball.style.opacity = (1 - pos/document.body.clientHeight) } } } for (let i=0; i < 10; i ++){ drop_snow() } setInterval(drop_snow, 500); </script> </html>main.css.content{ text-align:center; position: relative; width: 100%; height: 100%; } .content .text{ position: absolute; text-align:center; top: 50%; left: 50%; transform: translate(-50%, -50%); font-family: 'Charm', cursive; font-size: 5em; } html { width: 100%; height: 100%; } body{ width: 100%; height: 100%; background: radial-gradient(#ffffff 50%, #91bee5 100%); } .ball{ position: absolute; padding: 0px; margin: 0px; width: 20px; height: 20px; border-radius: 10px; background-color: rgb(218, 233, 247); }S3に再度アップロード
再度作成した S3 バケットにアップロードしてください。
CloudFront のキャッシュ無効化
CloudFront を通して一度サイトにアクセスすると、作成したウェブページがキャッシュされます。
そこで、キャッシュを無効化する必要があります。キャッシュを無効化するためにはまず CloudFront にアクセスし、作成した CloudFront Distribution をクリックしてください。Invalidations で 無効化の作成 をクリックしてください。
あとは index.html 入力し、無効化を実行してください。
結果、こんな感じになります。(codepen です。画像だとつまらないので。)
See the Pen aws_s3_ssl_example1 by zomaphone1 (@zomaphone) on CodePen.
まとめ
以上で、S3 で ホスティングしたウェブサイトをSSL化し、かつ特定の IP のみに開示する方法について一つ紹介しました。
通常のホスティングであれば S3 で設定が済みますが、SSL化 と 特定の IP に対して公開したい場合、上記の手段は有効です。
もちろん他の手段として、S3 だけで解決する手段もあります。
こちらの記事のようにアクセスポイントを変えるだけ https で公開することもできます。IP と https のみを許容する場合は bucket policy を変えるだけで済みます。
Before: https://revcomm-christmas-demo.s3-website-ap-northeast-1.amazonaws.com/ After: https://s3-ap-northeast-1.amazonaws.com/revcomm-christmas-demo/index.htmlただし、このやり方で注意していただきたいのが、
httpsとindex.htmlを明示的に示さないといけないことです。そのため、共有するときに URL の扱いに注意しないといけなくなります。もし、リダイレクト等も含めたい、index.html まで記述させたくない場合、ぜひこちらの記事を参考に実施していただけると幸いです。
最後に
いかがでしたでしょうか。
今回の Qiita advent calendar は RevComm として初の試みではありましたが、有益な情報は得られましたでしょうか。
RevComm では「コミュニケーションを再発明し、人が人を想う社会を創る」というミッションを基に、電話営業をディープラーニングの技術で支援するプロダクト( Miitel ) をはじめ、コミュニケーションに関わる様々なプロダクト開発を行っており、日頃からコミュニケーションの在り方を再定義するという難しい課題に取り組んでいます。
弊社ではテックに関わらず成長に貪欲な人がたくさんいます。会社としてまだまだ若いところもありますが、新しい技術等に積極的に取り入れる会社ではあるので、エンジニアとしてテックスタックを広げたいという人にとってはいい会社です。
もし、弊社で一緒に働きたいという想いがあれば、あるいは少しでも興味があればぜひぜひ弊社の採用ページに応募してみてください。
ちなみに働き方について興味があれば、ぜひ CTO が書いたこちらの記事を読んでください。
では良いクリスマスを!!
- 投稿日:2020-12-23T13:59:56+09:00
ツクールMZ コアスクリプト v1.0.0 ~ v1.1.1 までの差分
コアスクリプトのバージョンが上がるのはいいけど、
結局のところ、どの辺が変わったのかが分からなかったので、
各バージョンの差分を取ってみた。プラグイン制作者と、俺用です。
あと、バージョンが変わるごとに、
ファイル先頭のバージョン表示とUtils.RPGMAKER_VERSIONも変わりますが、
あまり処理系に支障がないので、ここでは省きます。Diff表示に慣れてない人のために書いておきますが、
赤い部分が古いコードで、緑の部分が新しいコードです。v1.0.0 → v1.0.2
- effekseer.min.js のバージョンを v1.52k → v1.52n に変更
- それに伴い、effekseer.wasm も更新
js/rmmz_core.js
@@ -5361,7 +5361,9 @@ WebAudio.prototype._readLoopComments = function(arrayBuffer) { while (segments[0] === 255) { packetSize += segments.shift(); } - packetSize += segments.shift(); + if (segments.length > 0) { + packetSize += segments.shift(); + } packets.push(packetSize); } let vorbisHeaderFound = false;js/rmmz_scenes.js
@@ -1509,7 +1509,8 @@ Scene_ItemBase.prototype.itemTargetActors = function() { }; Scene_ItemBase.prototype.canUse = function() { - return this.user().canUse(this.item()) && this.isItemEffectsValid(); + const user = this.user(); + return user && user.canUse(this.item()) && this.isItemEffectsValid(); }; Scene_ItemBase.prototype.isItemEffectsValid = function() {js/rmmz_sprites.js
@@ -1141,7 +1141,7 @@ Sprite_Enemy.prototype.revertToNormal = function() { }; Sprite_Enemy.prototype.updateWhiten = function() { - const alpha = 128 - (16 - this._effectDuration) * 10; + const alpha = 128 - (16 - this._effectDuration) * 8; this.setBlendColor([255, 255, 255, alpha]); };v1.0.2 → v1.1.0
js/rmmz_managers.js
@@ -688,8 +688,8 @@ StorageManager.saveToForage = function(saveName, zip) { setTimeout(() => localforage.removeItem(testKey)); return localforage .setItem(testKey, zip) - .then(localforage.setItem(key, zip)) - .then(this.updateForageKeys()); + .then(() => localforage.setItem(key, zip)) + .then(() => this.updateForageKeys()); }; StorageManager.loadFromForage = function(saveName) { @@ -704,7 +704,7 @@ StorageManager.forageExists = function(saveName) { StorageManager.removeForage = function(saveName) { const key = this.forageKey(saveName); - return localforage.removeItem(key).then(this.updateForageKeys()); + return localforage.removeItem(key).then(() => this.updateForageKeys()); }; StorageManager.updateForageKeys = function() { @@ -998,7 +998,7 @@ EffectManager.load = function(filename) { EffectManager.startLoading = function(url) { const onLoad = () => this.onLoad(url); - const onError = () => this.onError(url); + const onError = (message, url) => this.onError(url); const effect = Graphics.effekseer.loadEffect(url, 1, onLoad, onError); this._cache[url] = effect; return effect; @@ -2742,6 +2742,7 @@ BattleManager.startAction = function() { this._phase = "action"; this._action = action; this._targets = targets; + subject.cancelMotionRefresh(); subject.useItem(action.item()); this._action.applyGlobal(); this._logWindow.startAction(subject, action, targets);js/rmmz_objects.js
@@ -3342,6 +3342,10 @@ Game_Battler.prototype.requestMotionRefresh = function() { this._motionRefresh = true; }; +Game_Battler.prototype.cancelMotionRefresh = function() { + this._motionRefresh = false; +}; + Game_Battler.prototype.select = function() { this._selected = true; }; @@ -4706,7 +4710,7 @@ Game_Actor.prototype.makeActionList = function() { Game_Actor.prototype.makeAutoBattleActions = function() { for (let i = 0; i < this.numActions(); i++) { const list = this.makeActionList(); - let maxValue = Number.MIN_VALUE; + let maxValue = -Number.MAX_VALUE; for (const action of list) { const value = action.evaluate(); if (value > maxValue) { @@ -6333,11 +6337,11 @@ Game_Map.prototype.isOverworld = function() { }; Game_Map.prototype.screenTileX = function() { - return Graphics.width / this.tileWidth(); + return Math.round((Graphics.width / this.tileWidth()) * 16) / 16; }; Game_Map.prototype.screenTileY = function() { - return Graphics.height / this.tileHeight(); + return Math.round((Graphics.height / this.tileHeight()) * 16) / 16; }; Game_Map.prototype.adjustX = function(x) { @@ -8188,11 +8192,11 @@ Game_Player.prototype.isCollided = function(x, y) { }; Game_Player.prototype.centerX = function() { - return (Graphics.width / $gameMap.tileWidth() - 1) / 2.0; + return ($gameMap.screenTileX() - 1) / 2; }; Game_Player.prototype.centerY = function() { - return (Graphics.height / $gameMap.tileHeight() - 1) / 2.0; + return ($gameMap.screenTileY() - 1) / 2; }; Game_Player.prototype.center = function(x, y) {js/rmmz_sprites.js
@@ -1424,6 +1424,9 @@ Sprite_Animation.prototype.targetPosition = function(renderer) { Sprite_Animation.prototype.targetSpritePosition = function(sprite) { const point = new Point(0, -sprite.height / 2); + if (this._animation.alignBottom) { + point.y = 0; + } sprite.updateTransform(); return sprite.worldTransform.apply(point); }; @@ -2151,7 +2154,11 @@ Sprite_Gauge.prototype.gaugeHeight = function() { }; Sprite_Gauge.prototype.gaugeX = function() { - return this._statusType === "time" ? 0 : 30; + if (this._statusType === "time") { + return 0; + } else { + return this.measureLabelWidth() + 6; + } }; Sprite_Gauge.prototype.labelY = function() { @@ -2432,6 +2439,11 @@ Sprite_Gauge.prototype.setupLabelFont = function() { this.bitmap.outlineWidth = this.labelOutlineWidth(); }; +Sprite_Gauge.prototype.measureLabelWidth = function() { + this.setupLabelFont(); + return this.bitmap.measureTextWidth(this.label()); +}; + Sprite_Gauge.prototype.labelOpacity = function() { return this.isValid() ? 255 : 160; };v1.1.0 → v1.1.1
js/rmmz_core.js
@@ -1684,7 +1684,7 @@ Bitmap.prototype.measureTextWidth = function(text) { context.font = this._makeFontNameText(); const width = context.measureText(text).width; context.restore(); - return width; + return Math.ceil(width); }; /**js/rmmz_objects.js
@@ -10099,7 +10099,7 @@ Game_Interpreter.prototype.command119 = function(params) { const command = this._list[i]; if (command.code === 118 && command.parameters[0] === labelName) { this.jumpTo(i); - return; + break; } } return true;js/rmmz_sprites.js
@@ -3332,7 +3332,7 @@ Spriteset_Base.prototype.removeAnimation = function(sprite) { }; Spriteset_Base.prototype.removeAllAnimations = function() { - for (const sprite of this._animationSprites) { + for (const sprite of this._animationSprites.clone()) { this.removeAnimation(sprite); } }; @@ -3544,7 +3544,7 @@ Spriteset_Map.prototype.removeBalloon = function(sprite) { }; Spriteset_Map.prototype.removeAllBalloons = function() { - for (const sprite of this._balloonSprites) { + for (const sprite of this._balloonSprites.clone()) { this.removeBalloon(sprite); } };js/rmmz_windows.js
@@ -468,8 +468,8 @@ Window_Base.prototype.drawFace = function( const sh = Math.min(height, ph); const dx = Math.floor(x + Math.max(width - pw, 0) / 2); const dy = Math.floor(y + Math.max(height - ph, 0) / 2); - const sx = (faceIndex % 4) * pw + (pw - sw) / 2; - const sy = Math.floor(faceIndex / 4) * ph + (ph - sh) / 2; + const sx = Math.floor((faceIndex % 4) * pw + (pw - sw) / 2); + const sy = Math.floor(Math.floor(faceIndex / 4) * ph + (ph - sh) / 2); this.contents.blt(bitmap, sx, sy, sw, sh, dx, dy); }; @@ -1998,7 +1998,7 @@ Window_MenuStatus.prototype.drawItemStatus = function(index) { const actor = this.actor(index); const rect = this.itemRect(index); const x = rect.x + 180; - const y = rect.y + rect.height / 2 - this.lineHeight() * 1.5; + const y = rect.y + Math.floor(rect.height / 2 - this.lineHeight() * 1.5); this.drawActorSimpleStatus(actor, x, y); }; @@ -5198,9 +5198,13 @@ Window_ScrollText.prototype.update = function() { Window_ScrollText.prototype.startMessage = function() { this._text = $gameMessage.allText(); - this.updatePlacement(); - this.refresh(); - this.show(); + if (this._text) { + this.updatePlacement(); + this.refresh(); + this.show(); + } else { + $gameMessage.clear(); + } }; Window_ScrollText.prototype.refresh = function() {
- 投稿日:2020-12-23T13:59:56+09:00
【メモ】ツクールMZ コアスクリプト v1.0.0 ~ v1.1.1 までの差分
コアスクリプトのバージョンが上がるのはいいけど、
結局のところ、どの辺が変わったのかが分からなかったので、
各バージョンの差分を取ってみた。プラグイン制作者と、俺用です。
あと、バージョンが変わるごとに、
ファイル先頭のバージョン表示とUtils.RPGMAKER_VERSIONも変わりますが、
あまり処理系に支障がないので、ここでは省きます。Diff 表示に慣れてない人のために書いておきますが、
赤い部分が古いコードで、緑の部分が新しいコードです。v1.0.0 → v1.0.2
js/libs/effekseer.min.js
v1.52k → v1.52n にバージョン変更
js/libs/effekseer.wasm
v1.52k → v1.52n にバージョン変更
js/rmmz_core.js
WebAudio.prototype._readLoopComments
while (segments[0] === 255) { packetSize += segments.shift(); } - packetSize += segments.shift(); + if (segments.length > 0) { + packetSize += segments.shift(); + } packets.push(packetSize); } let vorbisHeaderFound = false;js/rmmz_scenes.js
Scene_ItemBase.prototype.canUse
}; Scene_ItemBase.prototype.canUse = function() { - return this.user().canUse(this.item()) && this.isItemEffectsValid(); + const user = this.user(); + return user && user.canUse(this.item()) && this.isItemEffectsValid(); }; Scene_ItemBase.prototype.isItemEffectsValid = function() {js/rmmz_sprites.js
Sprite_Enemy.prototype.updateWhiten
}; Sprite_Enemy.prototype.updateWhiten = function() { - const alpha = 128 - (16 - this._effectDuration) * 10; + const alpha = 128 - (16 - this._effectDuration) * 8; this.setBlendColor([255, 255, 255, alpha]); };v1.0.2 → v1.1.0
js/rmmz_managers.js
StorageManager.saveToForage
setTimeout(() => localforage.removeItem(testKey)); return localforage .setItem(testKey, zip) - .then(localforage.setItem(key, zip)) - .then(this.updateForageKeys()); + .then(() => localforage.setItem(key, zip)) + .then(() => this.updateForageKeys()); }; StorageManager.loadFromForage = function(saveName) {StorageManager.removeForage
StorageManager.removeForage = function(saveName) { const key = this.forageKey(saveName); - return localforage.removeItem(key).then(this.updateForageKeys()); + return localforage.removeItem(key).then(() => this.updateForageKeys()); }; StorageManager.updateForageKeys = function() {EffectManager.startLoading
EffectManager.startLoading = function(url) { const onLoad = () => this.onLoad(url); - const onError = () => this.onError(url); + const onError = (message, url) => this.onError(url); const effect = Graphics.effekseer.loadEffect(url, 1, onLoad, onError); this._cache[url] = effect; return effect;this._phase = "action"; this._action = action; this._targets = targets; + subject.cancelMotionRefresh(); subject.useItem(action.item()); this._action.applyGlobal(); this._logWindow.startAction(subject, action, targets);js/rmmz_objects.js
Game_Battler.prototype.cancelMotionRefresh
this._motionRefresh = true; }; +Game_Battler.prototype.cancelMotionRefresh = function() { + this._motionRefresh = false; +}; + Game_Battler.prototype.select = function() { this._selected = true; };Game_Actor.prototype.makeAutoBattleActions
Game_Actor.prototype.makeAutoBattleActions = function() { for (let i = 0; i < this.numActions(); i++) { const list = this.makeActionList(); - let maxValue = Number.MIN_VALUE; + let maxValue = -Number.MAX_VALUE; for (const action of list) { const value = action.evaluate(); if (value > maxValue) {Game_Map.prototype.screenTileX,Y
}; Game_Map.prototype.screenTileX = function() { - return Graphics.width / this.tileWidth(); + return Math.round((Graphics.width / this.tileWidth()) * 16) / 16; }; Game_Map.prototype.screenTileY = function() { - return Graphics.height / this.tileHeight(); + return Math.round((Graphics.height / this.tileHeight()) * 16) / 16; }; Game_Map.prototype.adjustX = function(x) {Game_Player.prototype.centerX,Y
}; Game_Player.prototype.centerX = function() { - return (Graphics.width / $gameMap.tileWidth() - 1) / 2.0; + return ($gameMap.screenTileX() - 1) / 2; }; Game_Player.prototype.centerY = function() { - return (Graphics.height / $gameMap.tileHeight() - 1) / 2.0; + return ($gameMap.screenTileY() - 1) / 2; }; Game_Player.prototype.center = function(x, y) {js/rmmz_sprites.js
Sprite_Animation.prototype.targetSpritePosition
Sprite_Animation.prototype.targetSpritePosition = function(sprite) { const point = new Point(0, -sprite.height / 2); + if (this._animation.alignBottom) { + point.y = 0; + } sprite.updateTransform(); return sprite.worldTransform.apply(point); };Sprite_Gauge.prototype.gaugeX
}; Sprite_Gauge.prototype.gaugeX = function() { - return this._statusType === "time" ? 0 : 30; + if (this._statusType === "time") { + return 0; + } else { + return this.measureLabelWidth() + 6; + } }; Sprite_Gauge.prototype.labelY = function() {Sprite_Gauge.prototype.measureLabelWidth
this.bitmap.outlineWidth = this.labelOutlineWidth(); }; +Sprite_Gauge.prototype.measureLabelWidth = function() { + this.setupLabelFont(); + return this.bitmap.measureTextWidth(this.label()); +}; + Sprite_Gauge.prototype.labelOpacity = function() { return this.isValid() ? 255 : 160; };v1.1.0 → v1.1.1
js/rmmz_core.js
Bitmap.prototype.measureTextWidth
context.font = this._makeFontNameText(); const width = context.measureText(text).width; context.restore(); - return width; + return Math.ceil(width); }; /**js/rmmz_objects.js
Game_Interpreter.prototype.command119
const command = this._list[i]; if (command.code === 118 && command.parameters[0] === labelName) { this.jumpTo(i); - return; + break; } } return true;js/rmmz_sprites.js
Spriteset_Base.prototype.removeAllAnimations
}; Spriteset_Base.prototype.removeAllAnimations = function() { - for (const sprite of this._animationSprites) { + for (const sprite of this._animationSprites.clone()) { this.removeAnimation(sprite); } };Spriteset_Map.prototype.removeAllBalloons
}; Spriteset_Map.prototype.removeAllBalloons = function() { - for (const sprite of this._balloonSprites) { + for (const sprite of this._balloonSprites.clone()) { this.removeBalloon(sprite); } };js/rmmz_windows.js
Window_Base.prototype.drawFace
const sh = Math.min(height, ph); const dx = Math.floor(x + Math.max(width - pw, 0) / 2); const dy = Math.floor(y + Math.max(height - ph, 0) / 2); - const sx = (faceIndex % 4) * pw + (pw - sw) / 2; - const sy = Math.floor(faceIndex / 4) * ph + (ph - sh) / 2; + const sx = Math.floor((faceIndex % 4) * pw + (pw - sw) / 2); + const sy = Math.floor(Math.floor(faceIndex / 4) * ph + (ph - sh) / 2); this.contents.blt(bitmap, sx, sy, sw, sh, dx, dy); };Window_MenuStatus.prototype.drawItemStatus
const actor = this.actor(index); const rect = this.itemRect(index); const x = rect.x + 180; - const y = rect.y + rect.height / 2 - this.lineHeight() * 1.5; + const y = rect.y + Math.floor(rect.height / 2 - this.lineHeight() * 1.5); this.drawActorSimpleStatus(actor, x, y); };Window_ScrollText.prototype.startMessage
Window_ScrollText.prototype.startMessage = function() { this._text = $gameMessage.allText(); - this.updatePlacement(); - this.refresh(); - this.show(); + if (this._text) { + this.updatePlacement(); + this.refresh(); + this.show(); + } else { + $gameMessage.clear(); + } }; Window_ScrollText.prototype.refresh = function() {
- 投稿日:2020-12-23T13:56:58+09:00
【Vue.js】Vue.jsをつかむ①
私のアウトプットです。
Vue.jsも人気のフロントエンドフレームワークのはずですが
なぜかReact推しであるブログ記事やYouTubeが多いので
私自身、Vue.jsを学習する上で、理解しておきたいことをまとめました。今回は、敢えてv2.6.11を前提に投稿いたします。
コード全体
index.html<html> <head> <title>Hello Vue</title> <!-- (1) CDNからのVueの読み込み --> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> </head> <body> <!-- (5) Vueインスタンスの有効範囲ここから --> <div id="app"> <!-- (6) マスタッシュ構文 --> <p>Hello {{world}}</p> <p>Counter: {{count}}</p> <!-- (7) v-ifディレクティブ --> <p v-if="count == 5">見えました!</p> <!-- (8) v-modelディレクティブ --> <input v-model="world"><br> <input type="number" v-model="count" /> </div> <script> // (2) Vueインスタンスの作成 new Vue({ el: "#app", // (3) elプロパティ data() { // (4) data()メソッド return { world: "Vue", count: 0 } } }) </script> </body> </html>Vue.jsの読み込み
index.html<!-- (1) CDNからのVueの読み込み --> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script>(1)は、CDNで公開されているVue.jsを読み込んで実行する為のコードです。
プロトタイピングや学習を目的とする場合は、このCDNを使用するのが良さそうです。Vueインスタンスの有効範囲
<body>の中の<script></script>を見ます。index.html<script> // (2) Vueインスタンスの作成 new Vue({ el: "#app", // (3) elプロパティ data() { // (4) data()メソッド return { world: "Vue", count: 0 } } }) </script>(2) は、
new Vue()でVueインスタンスを作っています。
引数に渡しているオブジェクトがVueのコンポーネントになっています。
このオブジェクトを見ていくと、elプロパティ(3)と、data()メソッド(4)があります。elプロパティはDOMに対して指定した値に該当するエレメントを見つけて
対象のエレメントが見つかった時にVue.jsのインスタンスとHTMLをマッピングするセレクタです。data()メソッドはオブジェクトを返すメソッドとして定義しています。
このときのオブジェクトが持つプロパティ((4)では、worldとcount)をVue.jsは監視し続けます。
この監視対象が変化したとき、必要に応じて画面への反映を即座に行ってくれます。DOM上での変数展開
<body>の中の<div id="app"></div>を見ます。index.html<!-- (5)Vueインスタンスの有効範囲ここから --> <div id="app"> <!-- (6) マスタッシュ構文 --> <p>Hello {{world}}</p> <p>Counter: {{count}}</p> <!-- (7) v-ifディレクティブ --> <p v-if="count == 5">見えました!</p> <!-- (8) v-modelディレクティブ --> <input v-model="world"><br> <input type="number" v-model="count" /> </div>これがVueインスタンスが有効な範囲です。
このid="app"で先ほど作ったVueインスタンスとHTMLをマッピングしていきます。
Vueインスタンスのelプロパティ指定と併せて、
別のidを指定したり、classで指定することも可能です。
<p>Hello {{world}}</p>(6)で使われている{{}}という構文はマスタッシュ構文と言います。
見た目が『口ひげ』に似ているからだそうです。
この中ではdata()の中のプロパティにアクセスができ、
data()の中にあるプロパティが変化すると、それに応じた結果が即座に反映されます。v-if
index.html<!-- (7) v-ifディレクティブ --> <p v-if="count == 5">見えました!</p>
v-で始まる属性をVue.jsではディレクティブと言います。
このv-ifは条件付きレンダリングと呼ばれるディレクティブで、
v-ifの後に記述する条件に一致していればDOMにレンダリングされ、
不一致ならDOM上から消えるようになっています。
(言わば、『if文』ですね。)
今回の場合はcountプロパティの値が5の場合に
『見えました!』と表示されることになります。v-model
index.html<!-- (8) v-modelディレクティブ --> <input v-model="world">これは、双方向バインディングと言って、
<input>タグの様な入力を受け付けるタグに対して
data()プロパティを指定できます。
つまり、<input v-model="world">の場合は、
テキストボックスに値を入力すると
その値がdata()のworldに設定されることになります。
これにより、入力フォームのinputイベントを監視して、
data()のプロパティへ随時反応されていくというコードになります。
なお、Vue.jsではv-modelによってフォームと対応付けられているプロパティが認識されている為
識別用のクラスや名前を付与しなくても値の読み取りが可能です。まとめ
(1)CDNからのVueの読み込み
CDNで公開されているVue.jsを読み込んで実行する為のコードです。
(2)Vueインスタンスの作成
new Vue()でVueインスタンスを作っています。(3)elプロパティ
DOMに対して指定した値に該当するエレメントを見つけて
対象のエレメントが見つかった時にVue.jsのインスタンスとHTMLをマッピングするセレクタです。(4)data()メソッド
オブジェクトを返すメソッドです。
(5)Vueインスタンスの有効範囲
id="app"でVueインスタンスとHTMLをマッピングしていきます。(6)マスタッシュ構文
<p>Hello {{world}}</p>で使われている{{}}です。(7)v-ifディレクティブ
v-ifの後に記述する条件に一致していればDOMにレンダリングされ、
不一致ならDOM上から消えるようになっています。(8)v-modelディレクティブ
<input>タグの様な入力を受け付けるタグに対してdata()プロパティを指定できます。
- 投稿日:2020-12-23T13:43:07+09:00
html, javasript, jquery (OOP) 作成 P5(private, publicプロパティ)
初めに
ジャバスクリプトもprivate, publicプロパティがあります。
private, publicプロパティに対して
①HTML分に以下のHTMLソースを追加する
<html> <head> <title>Hoge</title> </head> <body id="main-id"> </body> </html>②JQUERYを選択してから、JAVASRIPT分に以下のソースを追加する
// マインオブジェクト var $main = $("#main-id"); // クラス定義 var Hoge = function() { // private プロパティ var privateString = "サンプルテキスト"; // public プロパティの場合、 thisを使う this.privateString = privateString; }; // ボタンオブジェクト var $button = $("<button>").html("hoge"); // ボタンクリックイベント $button.click(function() { // インスタンス作成 var hoge = new Hoge(); // public プロパティを呼ぶ alert(hoge.privateString); }); //親オブジェクトに子オブジェクトを追加 $main.append($button);private, public関数に対して
①HTML分に以下のHTMLソースを追加する
<html> <head> <title>Hoge</title> </head> <body id="main-id"> </body> </html>②JQUERYを選択してから、JAVASRIPT分に以下のソースを追加する
// マインオブジェクト var $main = $("#main-id"); // クラス定義 var Hoge = function() { // private プロパティ var privateString = "サンプルテキスト"; // public プロパティの場合、 thisを使う this.privateString = privateString; // private 関数 var show = function() { alert(privateString); }; // public 関数の場合、thisを使う this.show = function() { return show(); } }; // ボタンオブジェクト var $button = $("<button>").html("hoge"); // ボタンクリックイベント $button.click(function() { // インスタンス作成 var hoge = new Hoge(); // public 関数を呼ぶ hoge.show(); }); //親オブジェクトに子オブジェクトを追加 $main.append($button);以上
- 投稿日:2020-12-23T13:36:33+09:00
わかりにくいvue.jsのwatch(ウォッチャ)のオプションを使った書き方
概要
vue.jsのwatchにはdeepとimmediateという2つのオプションがあるのですが、書き方がちょっと特殊なのと、マニュアル上で探しにくくいので、癖があります。
毎回ググってしまうので備忘録として残します。※2019/9にブログに書いていた記事からの転記です
watchとは
vueのドキュメントの説明
vueのドキュメントの説明(API)普通は算出プロパティ(computed)でいいんだけど、複雑な処理(重い処理?)の時はこっちの方が良いよ〜との事。
オプション
オプションについてはなぜかちょっと遠いところで、APIの中に記載があります。
[vueのドキュメントの説明]https://jp.vuejs.org/v2/api/index.html#vm-watch
deep
通常、objectのプロパティの変更は、watchが発火しません。
data: () => ({ someObject: {}, )}, watch: { someObject(newVal, oldVal) { console.log(newVal); }, } methods: { onClick: function() { // watchが発火しない! someObject.hoge = 'hoge'; },deepをtrueにする場合はこう
data: () => ({ someObject: {}, )}, watch: { someObject: { deep: true, handler(newVal, oldVal) { console.log(newVal); }, } } methods: { onClick: function() { // watchが発火する someObject.hoge = 'hoge'; },handlerという関数を書かないといけないのが唐突なので、いつも書き方忘れる、、、、
immediate
初期化のタイミングでもwatchが発火するようにしたい場合に使います。
これも、
data: () => ({ someObject: {hoge: 'piyo'}, )}, watch: { someObject: { immediate: true, handler(newVal, oldVal) { console.log(newVal); }, } }という感じで、handler関数にいつもの処理を書いてあげる必要があります。
所感
なんでここだけマニュアルわかりにくいんだろ、、、
- 投稿日:2020-12-23T11:40:45+09:00
React Server Componentについてまとめてみた
先日、ReactがReact Server Componentsを予告しました。まだ開発中とのことなので使えるのは先になりそうですが、非常に面白い内容となっているのでこちらで共有したいと思います。
What is React Server Component
React Server Componentとは、サーバーサイドのみで実行され、バンドルサイズへの影響を与えません。React Server Componentはクライアントでダウンロードされないため、アプリの起動時間などの向上などが期待できます。
また、Server Componentは、データベースなどサーバサイドのデータソースにアクセスすることができたり、レンダリングするClient Componentを動的に選択することができます。
ここで簡単な例に触れてみましょう。React Server Componentを使った例
以下はノートのタイトルと本文をサーバーサイドから取得し表示し、ノートのエディタをClient Reactコンポーネントとしてレンダリングする例です。
まず、Server Componentを実装するには拡張子を
.server.jsまたは.server.jsx、.server.tsxなどにします。import db from 'db.server'; // (A1) import NoteEditor from 'NoteEditor.client'; function Note(props) { const {id, isEditing} = props; // (B) const note = db.posts.get(id); return ( <div> <h1>{note.title}</h1> <section>{note.body}</section> {/* (A2) */} {isEditing ? <NoteEditor note={note} /> : null } </div> ); }この例からいくつか重要なことがわかります。
- A1 :Client Reactコンポーネントをインポートする時は
.client.jsまたは.client.jsx,.client.tsxの拡張子をつける。- B : データベースなどのサーバーサイドのデータソースに直接アクセスしている
- A2: Client Componentは
isEditingがtrueの時のみクライアントにロードされる。つまり必要に応じて動的にロードされることになる続いて動的にロードされる
NoteEditorコンポーネント(Client Component)について見ていきましょう。export default function NoteEditor(props) { const note = props.note; const [title, setTitle] = useState(note.title); const [body, setBody] = useState(note.body); const updateTitle = event => { setTitle(event.target.value); }; const updateBody = event => { setTitle(event.target.value); }; const submit = () => { // ...save note... }; return ( <form action="..." method="..." onSubmit={submit}> <input name="title" onChange={updateTitle} value={title} /> <textarea name="body" onChange={updateBody}>{body}</textarea> </form> ); }この例で重要な点は、Server Componentの結果をクライアントにレンダリングするときに、以前にレンダリングされた可能性のあるClient Componentの状態を保持することです。具体的には、Reactは、サーバーから渡された新しいPropsを既存のクライアントコンポーネントにマージし、これらのコンポーネントの状態(およびDOM)を維持して、focus、stateや進行中のアニメーションなどを保持します。
React Server Componentのメリット
他にもたくさんのメリットがあるので今確認できるものを紹介していきます。
Zero-Bundle-Size Components
冒頭に述べたように、React Server Componentはサーバーサイドのみで実行されるので、バンドルサイズが0となります。そのため開発時にコードサイズによるパフォーマンス低下を回避することができます。
// NoteWithMarkDown.js // 従来のReactコンポーネントなのでそのままのバンドルサイズとなる import marked from 'marked'; // 35.9K (11.2K gzipped) import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped) function NoteWithMarkdown({text}) { const html = sanitizeHtml(marked(text)); return (/* render */); } // NoteWithMarkdown.server.js // ServerComponentなのでバンドルサイズが0になる import marked from 'marked'; // zero bundle size import sanitizeHtml from 'sanitize-html'; // zero bundle size function NoteWithMarkdown({text}) { // same as before }バックエンドへのフルアクセス
Reactでアプリを作成する際の課題として、データへのアクセス方法及び、データの保存場所が挙げられるそうです。Server Componentはバックエンドに直接アクセスできるので、例えば新しいアプリを作成し始めた時などに、データの保存場所がわからない場合にはファイルシステムを用いることもできます。
// Server Componentなのでバックエンドにアクセスできる import fs from 'react-fs'; function Note({id}) { const note = JSON.parse(fs.readFile(`${id}.json`)); return <NoteWithMarkdown note={note} />; }自動コード分割
コード分割により、アプリケーションを小さなバンドルに分割することができ、クライアントに送信するコードを減らせます。これの一般的なアプローチは、ルート毎にバンドルを遅延ロードするか、実行時になんらかの基準により異なるモジュールを遅延ロードすることが挙げられます。
// Client component import React from 'react'; // これらの1つは、クライアントでレンダリングされるとロードを開始する const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js')); const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.js')); function Photo(props) { // Switch on feature flags, logged in/out, type of content, etc: if (FeatureFlags.useNewPhotoRenderer) { return <NewPhotoRenderer {...props} />; } else { return <PhotoRenderer {...props} />; } }コード分割はパフォーマンス向上に役立ちますが、import時に
React.lazyを用いて動的インポートする必要が出てきます。また、このアプローチはコンポーネントのロード開始タイミングを遅らせるため、コードのロードする量を減らすといった利点を弱めてしまいます。そこで、Server Componentを用いることでこれらの問題に対応することができます。コード分割を自動化するために、Server Componentは全てのClient Componentを潜在的なコード分割点として扱います。
加えて、Server Componentは開発者により早く使用するコンポーネントを選ばせることができるので、クライアントはレンダリングプロセスの早期段階でコンポーネントをダウンロードできます。
// Server Componentなので自動コード分割される import React from 'react'; // これらの1つは、レンダリングされてクライアントにストリーミングされると、ロードを開始する import OldPhotoRenderer from './OldPhotoRenderer.client.js'; import NewPhotoRenderer from './NewPhotoRenderer.client.js'; function Photo(props) { // Switch on feature flags, logged in/out, type of content, etc: if (FeatureFlags.useNewPhotoRenderer) { return <NewPhotoRenderer {...props} />; } else { return <PhotoRenderer {...props} />; } }No waterfall
パフォーマンスが低下する原因の1つとして、アプリケーションがデータをフェッチするために連続してリクエストを行うときに発生します。たとえば、以下の例のように最初にコンポーネントをレンダリングしてから、
useEffect()内でデータをフェッチすることで生じます。// Note.js function Note(props) { const [note, setNote] = useState(null); useEffect(() => { // 子のwaterfallによりレンダリング後にロードされます fetchNote(props.id).then(noteData => { setNote(noteData); }); }, [props.id]); if (note == null) { return "Loading"; } else { return (/* render note here... */); } }親コンポーネントと子コンポーネント両方でこのアプローチをとってしまうと、子コンポーネントは親コンポーネントがデータをロードし終わるまでデータをロードし始めることができません。ただし、このアプローチをとるとアプリケーションが必要なデータを正確にフェッチすることができ、レンダリングされていないUIの部分のデータをフェッチしないようにするといったメリットがあります。
Server Componentを使用すると、サーバとクライアントの連続した往復処理をサーバに移すことでこのメリットをそのままに、問題点を解決することができます。またこの往復処理をサーバーに移動することで、リクエストのレイテンシーを減らし、パフォーマンスを向上させることができます。さらに、Server Componentはコンポーネント内から必要最小限のデータを直接フェッチし続けることができます。
// Note.server.js - Server Component function Note(props) { // サーバに低いレイテンシでデータにアクセスし、レンダリング中にロードされます const note = db.notes.get(props.id); if (note == null) { // handle missing note } return (/* render note here... */); }まとめ
今回説明したServer Componentの特徴を以下にまとめます。
- React Server Componentはサーバーサイドのみで実行されるので、バンドルサイズが0となる
- Client Component(従来のReactコンポーネント)とServer Componentをわけるために
.client.js、.server.jsのように拡張子を変える- データベース、ファイルシステムサーバー側のデータソースにアクセスできる
- レンダリングするClient Componentを動的にロードできるのでクライアントではページのレンダリングに必要な最小限のコードのみダウンロードできる
- リロード時にクライアントの情報を保持する
さらに詳しい情報が知りたい場合は、公式からデモ動画を見て、デモ用のプロジェクトを試してみてください?
参考
- 投稿日:2020-12-23T11:39:22+09:00
Swiper.js(Ver6系)でタブの横スクロールを実現する
デモ
https://codepen.io/qwe001/pen/JjRrWqm
なんか知らないうちにSwiperのバージョンめっちゃ上がりましたね…
私の知っている時はVer3ぐらいだったと思いますが、いつの間にかVer6に…
WEBの進化はやすぎわろた。オプション名とか引数の並び順とかも結構変わってたので、
改めて実装しました。ミソ
var tabSwiper = new Swiper('.tabs-box', tabSwiperOptions); var boardSwiper = new Swiper('.board-box', boardSwiperOptions); tabSwiper.slides.each(function(event, index){ $(this).on("click", function(){ $(".tabs .tab").removeClass("selected"); // init $(this).addClass("selected"); boardSwiper.slideTo(index, 500, false); }); });タブ要素はページネーションを使うのではなく、スライダーとして生成します。
コンテンツも同じくスライダーとして生成します。この時、これら二つのスライダー要素の数は同じである必要があります。
任意のタブ要素(スライダー)をクリックすると、
タブ要素(スライダー)のインデックス番号が取得できます。このインデックス番号を利用して、
タブ要素のクリック時に、コンテンツも同じインデックス番号のスライダーに移動させることで、
タブ移動が実現します。また、ボード側のスワイプで移動しないように
noSwiping:trueを設定しています。
動かしたくないスライダーにswiper-no-swipingクラスをつけることで、動かないようにできます<div class="swiper-container board-box"> <div class="swiper-wrapper boards swiper-no-swiping"> <div class="swiper-slide board">内容 1</div> <div class="swiper-slide board">内容 2</div> ... </div> </div>ボード側のスワイプでも動くようにしたければ、以下のページの実装を参考にしてください(別作者)
https://codepen.io/pangmr/pen/OXvaEk
実装上の注意
CSSで、内容部分の背景色に白色を明示的に設定していますが、
これは消さないでください。
背景色がないと、透過扱いになるのか、
タブを変更した際に後ろにあるコンテンツと被ります.boards .board { background: #FFF; text-align: center; }依存するライブラリ
- jQuery
- Swiper.js
- Swiper.css
参考サイト
- 投稿日:2020-12-23T11:32:31+09:00
Vue.js 3 入門 「Vuex」
はじめに
Vue.js 3 の Vuex について、自分が学んだことを備忘録として記載します。
Vue.js に殆ど触れたことが無い方に少しでも参考になれば幸いです。
誤り等あれば、ご指摘頂けますと大変喜びますVuex とは
Vuexはアプリで利用するデータを、一箇所に集中管理するためのライブラリです。
管理だけではなく、データの操作(更新)方法も標準化することができます。コンポーネント間でのデータ共有がシンプルに実現できるようになりますね。今回のお題
今回は、数字をカウントできる簡単なアプリを作成してみます。
プロジェクトの作成
まずは Vue CLI を用いてプロジェクトを作成します。
Vue CLI についてはこちらの記事を参照してください。プロジェクトを作成するには、作成したいフォルダで以下のコマンドを実行します。
hello-vuexはプロジェクト名です。任意のプロジェクト名を設定してください。cd 任意のフォルダ vue create hello-vuexプリセットの選択
すると、以下のように利用するプリセット(プロジェクト設定)の選択を求められます。
まずは最低限の構成とするので「Manually select features」(手動で選択)を選択します。
versionはご自身のバージョンに読み替えてください。Vue CLI v4.5.9 ? Please pick a preset: Default ([Vue 2] babel, eslint) Default (Vue 3 Preview) ([Vue 3] babel, eslint) > Manually select featuresプロジェクトに組み込むモジュールを選択
プロジェクトに組み込むモジュールを選択します。
ここでBabelとLinterに加えて、Vuexを選択します。
[Space]キーで選択することができ、[Enter]キーで確定となります。
Vuexを選択することによって、Vuexというアプリで利用するデータの、集中管理機能を提供するライブラリが組み込まれます。Vue CLI v4.5.9 ? Please pick a preset: Manually select features ? Check the features needed for your project: (*) Choose Vue version (*) Babel ( ) TypeScript ( ) Progressive Web App (PWA) Support ( ) Router >(*) Vuex ( ) CSS Pre-processors (*) Linter / Formatter ( ) Unit Testing ( ) E2E TestingVue.js のバージョンを選択
Vue.js のバージョンを選択します。
本記事では 3.x を選択します。Vue CLI v4.5.9 ? Please pick a preset: Manually select features ? Check the features needed for your project: Choose Vue version, Babel, Vuex, Linter ? Choose a version of Vue.js that you want to start the project with 2.x > 3.x (Preview)Linter の設定を選択
Linterの設定を選択します。
今回は最低限のESLint with error prevention only(エラー防止のみ)を選択します。Vue CLI v4.5.9 ? Please pick a preset: Manually select features ? Check the features needed for your project: Choose Vue version, Babel, Vuex, Linter ? Choose a version of Vue.js that you want to start the project with 3.x (Preview) ? Pick a linter / formatter config: (Use arrow keys) > ESLint with error prevention only ESLint + Airbnb config ESLint + Standard config ESLint + Prettier続けて、Lintの実行タイミングの選択を求められます。
Lint on save(保存時)を選択します。Vue CLI v4.5.9 ? Please pick a preset: Manually select features ? Check the features needed for your project: Choose Vue version, Babel, Vuex, Linter ? Choose a version of Vue.js that you want to start the project with 3.x (Preview) ? Pick a linter / formatter config: Basic ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection) >(*) Lint on save ( ) Lint and fix on commit設定情報の格納先を選択
BabelとESLintの設定情報を個別の設定ファイルとするか、package.jsonにまとめるかを選択します。
個別の設定ファイルとしたほうが綺麗なのでIn dedicated config filesを選択します。Vue CLI v4.5.9 ? Please pick a preset: Manually select features ? Check the features needed for your project: Choose Vue version, Babel, Vuex, Linter ? Choose a version of Vue.js that you want to start the project with 3.x (Preview) ? Pick a linter / formatter config: Basic ? Pick additional lint features: Lint on save ? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys) > In dedicated config files In package.json今回の設定を保存しておくかを選択
今回の設定を保存しておくかを選択します。
今回はあくまでお試しなのでN(保存しない)とします。Vue CLI v4.5.9 ? Please pick a preset: Manually select features ? Check the features needed for your project: Choose Vue version, Babel, Vuex, Linter ? Choose a version of Vue.js that you want to start the project with 3.x (Preview) ? Pick a linter / formatter config: Basic ? Pick additional lint features: Lint on save ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? (y/N) Nプロジェクトの生成開始
ここまでの設定内容を元に、プロジェクトの生成が開始されるので、完了するまで待機します。
正常に完了すると、以下のような文言が表示されます。Vue CLI v4.5.9 Creating project in 任意のフォルダ\hello-vuex. Installing CLI plugins. This might take a while... 途中省略... Running completion hooks... Generating README.md... Successfully created project hello-vuex. Get started with the following commands: $ cd hello-vuex $ npm run serve生成されたフォルダを確認
カレントフォルダに、指定したプロジェクト名のフォルダが生成されています。
アプリの実行
早速実行してみましょう。
上記のプロジェクト生成完了時の文言(Get started with the following commands:)にある通り、以下のコマンドを実行します。
プロジェクトルートに移動して、開発用のサーバーを実行するコマンドです。cd hello-vuex npm run serve以下のような文言が表示されれば、開発用のサーバーが起動できています。
ブラウザを起動しhttp://localhost:8080にアクセスしてください。App running at: 途中省略... Note that the development build is not optimized. To create a production build, run npm run build.動作確認
以下のような画面が表示されれば、プロジェクトの作成は成功です。
開発用サーバーは[Ctrl] + [C]で終了することができます。ストアの定義
まずはストアを定義します。
ストアは主に、データと、データを更新するためのメソッド で構成されます。プロジェクトを作成した時点で、空のストア定義(/src/store/index.js)が用意されているので、こちらを編集していきます。
/src/store/index.jsimport { createStore } from 'vuex' export default createStore({ state: { }, mutations: { }, actions: { }, modules: { } })ストアは
createStoreメソッドで定義することができます。createStoreメソッド
引数
- defs
- ストアを構成する要素(ステート、ミューテーション、etc...)を
要素名:定義の形式で記述します- 複数の要素を定義する際は
,区切りステートの定義
ステートとは、ストアで管理されるデータの本体です。
ステートで管理すべき情報を名前: 初期値の形式で定義します。(複数の場合は,区切り)今回は、カウンターを示す
countを定義します。
/src/store/index.jsimport { createStore } from 'vuex' export default createStore({ state: { count: 0 //追加 }, mutations: { }, actions: { }, modules: { } })ミューテーションの定義
Vuexでは、ステートを専用のメソッド経由で更新します。
ステートの更新フローが限定されるので、コードの見通しが良くなります。このような、ステートを更新するためのメソッドのことを、ミューテーションと呼びます。
今回はステートcountを1増やすincrementメソッドと
countを1減らすdecrementメソッドを定義します。ミューテーションは引数にステート(
state)を受け取るので、実際の各情報にはstate.名前とするとアクセスできます。import { createStore } from 'vuex' export default createStore({ state: { count: 0 }, mutations: { increment(state){ state.count += 1 }, decrement(state){ state.count -= 1 } }, actions: { }, modules: { } })ストアの有効化
なお、ストアは
/src/main.jsで有効化されています。import { createApp } from 'vue' import App from './App.vue' import store from './store' createApp(App).use(store).mount('#app')Vueインスタンスにライブラリを組み込む
useメソッドに、定義したストア(store)を渡すことで有効化しています。ストアにアクセスする
ステートの表示
実際にコンポーネントからストアにアクセスしてみましょう。
まずはストアに定義したステート
countの値を画面に表示してみます。
ステートには、this.$store.state.データの名前でアクセスできます。
/src/App.vueを以下のように修正します。<template> {{count}} </template> <script> export default { name: 'App', computed:{ count(){ return this.$store.state.count }, } } </script>実際の画面を確認すると、初期値に指定した0が表示されています。
ミューテーションの呼び出し
次に、ステート
countの値を増減できるようにしてみます。
ステートを増減させるには、先程定義したミューテーションを呼び出します。
ミューテーションはthis.$store.commit(ミューテーション名)で呼び出すことができます。
/src/App.vueを以下のように修正します。<template> <input type="button" v-on:click="ondecrement" value="-" /> {{count}} <input type="button" v-on:click="onincrement" value="+" /> </template> <script> export default { name: 'App', computed:{ count(){ return this.$store.state.count }, }, methods:{ onincrement(){ this.$store.commit('increment') }, ondecrement(){ this.$store.commit('decrement') } } } </script>実際の画面を確認すると、[+]ボタンと[-]ボタンが増えており、
ステートの値を増減させることができるようになりました。以上となります。
ありがとうございました。
他の機能(ゲッター/アクション/etc...)については別の記事にします。
- 投稿日:2020-12-23T11:02:00+09:00
html, javasript, jquery (OOP) 作成 P4(リスト追加)
html、 javasriptのツールの開発
→オンラインツールを使えるため、ツールのダウンロードが不要です。
https://jsfiddle.net/テーブルに対して(例1)
①HTML分に以下のHTMLソースを追加する
<html> <head> <title>Hoge</title> </head> <body id="main-id"> </body> </html>②JQUERYを選択してから、JAVASRIPT分に以下のソースを追加する
// マインオブジェクト var $main = $("#main-id"); // テーブルオブジェクト var $table = $("<table>"); // クラス var Row = function(table) { // table:変数 // プロパティ var $tr = $("<tr>"); // 行オブジェクト var $td = $("<td>").html("サンプルテキスト"); // カラムオブジェクト // メソッド this.addRow = function() { table.append($tr.append($td)); }; }; // ボタンオブジェクト var $button = $("<button>").html("追加"); // ボタンクリックイベント $button.click(function() { // インスタンス作成 var row = new Row($table); // クラスのメソッドを呼ぶ row.addRow(); }); //親オブジェクトに子オブジェクトを追加 $main.append($button).append($table);テーブルに対して(例2)
①HTML分に以下のHTMLソースを追加する
<html> <head> <title>Hoge</title> </head> <body id="main-id"> </body> </html>②JQUERYを選択してから、JAVASRIPT分に以下のソースを追加する
// マインオブジェクト var $main = $("#main-id"); // テーブルオブジェクト var $table = $("<table>"); // クラス定義 var Row = function(table) { // table:変数 // プロパティ var $tr = $("<tr>"); // 行オブジェクト var $td = $("<td>").html("インプット:"); // カラムオブジェクト var $input = $("<input>"); // インプットオブジェクト // 追加メソッド this.addRow = function() { // 行を追加 table.append($tr.append($td.append($input))) }; // 削除メソッド this.removeRow = function() { // 行を削除 table.find("tr:last").remove() }; }; // ボタンオブジェクト var $buttonAdd = $("<button>").html("追加"); // ボタンクリックイベント // ボタンをクリックすると、行が追加される $buttonAdd.click(function() { // インスタンス作成 var row = new Row($table); // クラスのメソッドを呼ぶ row.addRow(); }); // ボタンオブジェクト var $buttonRemove = $("<button>").html("削除"); // ボタンクリックイベント // ボタンをクリックすると、行が削除される $buttonRemove.click(function() { // インスタンス作成 var row = new Row($table); // クラスのメソッドを呼ぶ row.removeRow(); }); //親オブジェクトに子オブジェクトを追加 $main.append($buttonAdd).append($buttonRemove).append($table);ul,liに対して
①HTML分に以下のHTMLソースを追加する
<html> <head> <title>Hoge</title> </head> <body id="main-id"> </body> </html>②JQUERYを選択してから、JAVASRIPT分に以下のソースを追加する
// マインオブジェクト var $main = $("#main-id"); // ulオブジェクト var $ul = $("<ul>"); // クラス var Row = function(ul) { // ul:変数 // プロパティ var $li = $("<li>").html("サンプルテキスト"); // liブジェクト // メソッド this.addRow = function() { ul.append($li); }; }; // ボタンオブジェクト var $button = $("<button>").html("追加"); // ボタンクリックイベント $button.click(function() { // インスタンス作成 var row = new Row($ul); // クラスのメソッドを呼ぶ row.addRow(); }); //親オブジェクトに子オブジェクトを追加 $main.append($button).append($ul);以上
- 投稿日:2020-12-23T11:02:00+09:00
html, javasript, jquery (OOP) 作成 P4(クラス定義)
html、 javasriptのツールの開発
→オンラインツールを使えるため、ツールのダウンロードが不要です。
https://jsfiddle.net/テーブルに対して(例1)
①HTML分に以下のHTMLソースを追加する
<html> <head> <title>Hoge</title> </head> <body id="main-id"> </body> </html>②JQUERYを選択してから、JAVASRIPT分に以下のソースを追加する
// マインオブジェクト var $main = $("#main-id"); // テーブルオブジェクト var $table = $("<table>"); // クラス定義 var Row = function(table) { // table:変数 // プロパティ var $tr = $("<tr>"); // 行オブジェクト var $td = $("<td>").html("サンプルテキスト"); // カラムオブジェクト // メソッド this.addRow = function() { table.append($tr.append($td)); }; }; // ボタンオブジェクト var $button = $("<button>").html("追加"); // ボタンクリックイベント $button.click(function() { // インスタンス作成 var row = new Row($table); // クラスのメソッドを呼ぶ row.addRow(); }); //親オブジェクトに子オブジェクトを追加 $main.append($button).append($table);テーブルに対して(例2)
①HTML分に以下のHTMLソースを追加する
<html> <head> <title>Hoge</title> </head> <body id="main-id"> </body> </html>②JQUERYを選択してから、JAVASRIPT分に以下のソースを追加する
// マインオブジェクト var $main = $("#main-id"); // テーブルオブジェクト var $table = $("<table>"); // クラス定義 var Row = function(table) { // table:変数 // プロパティ var $tr = $("<tr>"); // 行オブジェクト var $td = $("<td>").html("インプット:"); // カラムオブジェクト var $input = $("<input>"); // インプットオブジェクト // 追加メソッド this.addRow = function() { // 行を追加 table.append($tr.append($td.append($input))) }; // 削除メソッド this.removeRow = function() { // 行を削除 table.find("tr:last").remove() }; }; // ボタンオブジェクト var $buttonAdd = $("<button>").html("追加"); // ボタンクリックイベント // ボタンをクリックすると、行が追加される $buttonAdd.click(function() { // インスタンス作成 var row = new Row($table); // クラスのメソッドを呼ぶ row.addRow(); }); // ボタンオブジェクト var $buttonRemove = $("<button>").html("削除"); // ボタンクリックイベント // ボタンをクリックすると、行が削除される $buttonRemove.click(function() { // インスタンス作成 var row = new Row($table); // クラスのメソッドを呼ぶ row.removeRow(); }); //親オブジェクトに子オブジェクトを追加 $main.append($buttonAdd).append($buttonRemove).append($table);ul,liに対して
①HTML分に以下のHTMLソースを追加する
<html> <head> <title>Hoge</title> </head> <body id="main-id"> </body> </html>②JQUERYを選択してから、JAVASRIPT分に以下のソースを追加する
// マインオブジェクト var $main = $("#main-id"); // ulオブジェクト var $ul = $("<ul>"); // クラス定義 var Row = function(ul) { // ul:変数 // プロパティ var $li = $("<li>").html("サンプルテキスト"); // liブジェクト // メソッド this.addRow = function() { ul.append($li); }; }; // ボタンオブジェクト var $button = $("<button>").html("追加"); // ボタンクリックイベント $button.click(function() { // インスタンス作成 var row = new Row($ul); // クラスのメソッドを呼ぶ row.addRow(); }); //親オブジェクトに子オブジェクトを追加 $main.append($button).append($ul);以上
- 投稿日:2020-12-23T09:24:21+09:00
React import/exportについて
export名とimport名が違うのはなんで??となったので調べたことをメモしておく。
import
①defaultでexportされているものについてはimport時にファイルパスさえあっていれば自由に名付けて良い。(当然同じ名前でも構わない)
import 〇〇 from 'ファイルのPath等'
import 『export時の名前』AS 〇〇 from 'ファイルのPath等'②defaultではないものについては
import { 〇〇 } from 'ファイルのPath等'と記載する。
この際、export時と同じ名前を使用する必要がある。③②で名前を変えたい時は、
import { 〇〇 AS つけたい名前 } from 'ファイルのPath等'で変更することができる。export
default
・export 〇〇でファイル・変数を自由にexortできる。
・クラス名・変数名に関わらず、export default 〇〇で自由に名付けられる。(1ファイルにつき1つのみ)以上、参考になれば幸いです。





































































