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

Svelteでカレンダーを作ってみる

この記事はラクス Advent Calendar 2020 23日目の投稿です。

今回は、フロントエンド開発で最近注目されているSvelteを試してみた結果を記事にまとめます。

Svelteって?

Svelte(スヴェルトと読むらしい)って何?と思う人も多いかもしれません。
が、すでにQiitaにもいくつか記事があるので、そちらに譲ります。

Svelteでカレンダーを作ってみる

こういったチュートリアルの定番といえばTODOアプリですが、すでに作っている方がいたので今回はカレンダーアプリを作ることにします。
ひとまず、今月1か月の日付を表示して、ボタンで前月・次月を行き来できるようにしてみます。

準備

公式サイトを参考に、Svelteで開発を始める準備をします。
https://svelte.dev/blog/svelte-and-typescript#Try_it_now

Node.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にアクセスしてみます。以下のような画面が表示されるはずです。
image.png

コンポーネントを作る

それではカレンダーを実装していきます。
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コンポーネントを呼び出すようにすれば、画面に今月のカレンダーが表示されるようになります!
image.png

なお、ここまでで実装したコード全体は以下のURLから確認できます。
https://github.com/takaram/svelte-calendar-sample/tree/f51892c54bf4848d2041847219cb46e79022f0d3

ページに動きをつける

このカレンダーを表示するだけであれば、PHPでHTMLを出力するのとさほど変わらないでしょう。
ここから、前月・次月に移動できるようにしていきましょう。

まず、CalendarコンポーネントのHTMLで利用している変数todaysundaysを動的に変更できるよう、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}">&lt;</span>
  <h1>{currentDay.getFullYear()}年{currentDay.getMonth() + 1}月</h1>
+ <span class="month-control" role="button" on:click="{goToNextMonth}">&gt;</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

所感

あまり他のフレームワークに詳しくないため比較はできませんが、比較的少ないコード量で実装できたのではないでしょうか。
表示される値の更新に多少クセがあるような気もしますが、慣れれば記述量が少なくて問題なさそうです。

ちなみに、開発中に編集したファイルを保存すると、変更が自動的に反映されるのがとても楽でした。ブラウザの更新ボタンすら押す必要がないので、行った変更を即座に確認することができます。

今度はもう少し複雑なアプリも作成してみたいですね。


  1. そのため、配列に要素を追加するような場合はlist.push(item)ではダメで、list = [...list, item]のようにします。 

  2. $:は文法的には、多重ループを抜けるときなどに使うラベル文です。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】API(バックエンド)でMySQLからテーブル情報を取得しフロントエンドで表示させるアプリの作成

1 はじめに

【アプリ作成の目的】
フロントエンド(Nuxt.js),バックエンド(express)を利用したアプリ作成を理解する。フロントエンドとサーバーエンドを分離したアプリとする。

【フロントエンドで行うこと】
・axiosを利用してバックエンド側(API)にリクエストを投げ、取得したデータを表示させる。
・ホームディレクトリは、「nuxt-scraping-app」とする。

【バックエンドで行うこと】
・事前にスクレイピングしてMySQLに保存したデータをjsonで返すAPIを作成する。
・ホームディレクトリは、「api-nuxt-puppeteer」とする。

【注意点】
※今回スクレイピング部分のコードに関しては、割愛させていただきます。
※スクレイピングの情報取得先はファッション系サイトであるfarfetchの商品を選択しました(sacaiのアイテム)。
※まだ理解が浅い為間違っている箇所はご指摘いただけると助かります!

【完成形】
スクリーンショット 2020-12-24 13.18.32.png

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.js
const 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についての理解が浅く、更に深い理解が必要だと感じた。インプットを更に励む必要がある。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】API(サーバーサイド)でMySQLからテーブル情報を取得しクライアントサイドで表示させる【Express】

1 はじめに

・APIアプリ(サーバーサイド)...MySQLに接続し、テーブルから情報を取得させます。
・フロントアプリ(クライアントサイド)... axiosによりlocalhost:5000に繋いでjsonで返してindex.vueに表示させます。

上記をNuxt.jsにより作成し、ローカル環境でフロントアプリからaxiosにより情報取得し、表示させる。
※各種installコマンド等は省きます
※まだ理解が浅い為間違っている箇所はご指摘いただけると助かります!

完成形↓
スクリーンショット 2020-12-23 18.31.53.png

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.js
const 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アプリ制作は今後デファクトスタンダードになっていくと思うので、インプットを更に励む必要がある。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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 コマンドでもなかなかお世話にならない adduserlinkpublishなどが試せたので非常に面白かったです。

相変わらずのコンソールネタとなってしまいましたが、新人研修には是非、おすすめです。
クリスマスカードや名刺に限らず、オリジナルのデザインとメッセージを表示するだけのジョークアプリでも、公開することに意味があるのだよ。(た、たぶん。。。)

それでは、メリークリスマス !! ?✨??

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptを用いて複数の投稿にプルダウンメニューを実装する方法

記事投稿系のポートフォリオを作成する中で、各記事に対してプルダウンメニューで編集・削除ボタンを作成しました。この時、複数の要素に対してイベント発火させるコードを書いたのが初めてだったので、1つの要素とイベント発火させる場合と比較しながら要点をまとめてみました。

各コードの比較

  • HTMLの対象箇所 対象のHTML(編集済).jpeg
  • 1つの要素に対してプルダウンメニューを実装する場合 単一の要素を取得(編集済).jpeg
  • 複数の要素に対してプルダウンメニューを実装する場合 複数の要素を取得(編集済).jpeg

各コードの①, ②部分について以下で詳しく見ていきます。

① HTMLの要素取得部分

上の画像では、getElementByIdで要素を取得しています。1箇所だけイベント発火させたい場合にはこの表現でいいのですが、今回のように複数の要素にイベント発火させたい場合、これでは指定した要素の内、一番上にある要素しか取得されません。
一方、下の画像では、querySerectorAllで要素を取得しているため、指定した要素全てを取得できます。取得した要素は下記のように、配列の形で取得されます。
console.log(pullDownButton)の出力.jpeg

② プルダウンメニュー実装部分

プルダウンメニュー実装部のul要素には、display: none;が指定してあるため、通常時、編集・削除ボタンは表示されません。これに対し、上記コードでは、プルダウンメニューボタンクリック時に、display: block;が記述され、編集・削除ボタンが表示されます。
これを、複数の要素に対してどう記述するかですが、①で述べた通り、querySerectorAllで取得した要素は配列で取得されます。そのため、pullDownButton.addEventListner(...)のように、直接イベント発火する記述ができません。これを解決するため、下の画像では、forを用いて、各i番目の要素に対して、イベント発火しています。
以上より、下記のように、複数の投稿に対してプルダウンメニューを実装することができました。
2020-12-23 22.42のイメージ.jpeg

終わりに

これまで、getElementByIdで要素を取得するコードしか書いて来なかったので、複数の要素を取得するコードは、書いてみれば基本的なことばかりなのですが、中々苦労しました。
中でも、querySerectorAllで要素を取得するということと、要素が配列で取得されるという点が重要だと感じました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

宛先ごとに本文をカスタムしたメールをGoogle Apps Script( javascript )で一括送信する

背景

アカウント名やパスワードを一括配布する機会があったので、GAS(Google Apps Script)で実施しました。

処理の概要

下記のスプレッドシートの情報を基に一括メール送信します。
シート名は「スクリプト用データ」です。
image.png

完了フラグがcompleteもしくはfail以外のみ、メールの送信対象となります。
送信後は完了フラグ列を自動で更新します。

処理の流れ

① スプレッドシートからデータを取得する
② ①のデータを基にメール送信に必要なデータを作成する
③ ②で作成したデータを使いメール送信する
④ シートを更新する
以上です。

メール送信に失敗した場合のエラーハンドリングでは、送信者にメール通知します。

ソースコード

▼メール送信

main
function 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.") + " でエラーが発生しました。",
            });
        }
    });
}

▼スプレッドシートデータ操作クラス

SheetDataManager
class 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の書き方でおかしなところがあれば教えていただきたいです。
短時間で作成したコードですが、問題なく動作しますので、何かの参考にしていただければと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

続・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/4757990523535360

Shape Detection APIは、QRコードのスキャンだけではなく以下の3つの事が可能です。

  • Barcode Detection(バーコードスキャン)
  • Face Detection(顔検出)
  • Text Detection(テキスト認識)

これらについては、以前にイベントで発表したので興味がある人は見てみてください。
スライド:ブラウザの新しいAPIで遊んでみる
動画:https://youtu.be/CS2tzUpYvQA?t=5253

Barcode 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)
    })

上手く検出できれば以下のようなオブジェクトで値を受け取ることができます。

image.png

座標なども取得できますが、実際に使うのは rawValue のところになると思います。
また、Barcode Detection APIはQRコードだけではなく、様々なバーコードのフォーマットに対応しています。詳しくはMDNなどで確認できます

作ったもの

動作しているGIFです。(上部の隙間はPCにスマホの画面を写しているためなので気にしないでください…)

m.gif

実際に公開されていますので、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.js
if (!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で画像化するいう実装が不要になりました。

image.png

検出結果は配列

複数の検出結果が得られる場合があるからだと思いますが、画像検出の結果は配列で受け取るため、結果をループで回して処理するような書き方になります。
また、検出できなかった場合もエラーではなく配列の length が0になります。

所感

Barcode Detectionの実装も難しくなく、全体的に以前の実装を少し変えるだけで簡単に実装することができました。
さらに、Canvasの処理が要らなくなったこともあって少し手軽になりました。

ブラウザ標準のAPIでこれができるのはとても良いですね。早く勧告になってほしい…

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.kt
val 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.kt
myWebView.getSettings().setJavaScriptEnabled(true)

おまけ

WebViewではストレージも無効になっていたので、必要な場合はオンにする。

MainActivity.kt
myWebView.getSettings().setDomStorageEnabled(true)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

canvasでクリスマスツリーをチカチカさせた習作

はじめに

クリスマスだし、なんとなくそれっぽいことをしたい気持ちです。
ずっと仲良くなりたいと思っていたcanvasを使って、クリスマスツリーをチカチカさせてみようかと思います。

xmas_tree.gif

詳細

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、楽しい!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【rails】railsとjsを用いて「いいね機能」を実装してみた

今回はrailsとjsでいいね機能を実装していきたいと思います

** また最後におまけでユーザーがいいねした投稿を表示できるような機能も実装していきます**

jsを読み込んだりする説明は割愛!

参考にさせていただいた記事

https://techtechmedia.com/favorite-function-rails/
https://qiita.com/hayabusa3703/items/2b916e652a1dc85bb6e3

完成予想図

スクリーンショット 2020-12-22 21.34.19.png

下準備

ユーザーはたくさんの投稿にいいねをして、投稿もたくさんのユーザーにいいねされるので

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
end
rails 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
end

liking_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

end

remote: 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
  end

resources :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>

自分はこんな感じ

これで以上です。お疲れ様でした。

今までのコードのまとめ

自分のgithub

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 について」です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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ファイルの中に直接書いてください。
※ダブルクォーテーションはなるべく使わずにシングルクォーテーションを使ってください。
ESPWebページ.PNG

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に関する記事を書こうと思っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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でも実装され、そしてその後ブラウザ標準のスクリプト言語として定着していきました。

image.png

しかし、JavaScriptが実装されたことにより様々なエフェクトを実現できるようになったため、多くの人が過剰な装飾をJavaScriptに勝手に盛り込んでいきました。

その結果、装飾過剰で、使い勝手の悪いWebページが量産されるようになりました。

そのためJavaScriptは「ダサいページを作成するための言語」「使い勝手が非常に悪く、プログラミングの素人が使う低俗な言語」というイメージだけが定着してき、不遇の時代へと入っていくことになります。

JavaScriptの歴史②:復権

そのような状況に光明が見えたのが、2005年、Ajax(Asynchronous JavaScript + XML)という技術が登場した時です。

Ajaxというのは、一言でいうならブラウザを再読み込みすることなく情報を更新する通信方法(非同期通信)をJavaScriptを用いて行う処理のプログラム手法のことを言います。

Google Mapのように、読み込むことがなく情報を取得できるアプリケーションを想像してもらえればわかりやすいかもしれません。

Image from Gyazo

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というのは言語仕様が異なっており、二つの言語の間に互換性がありませんでした。

そのため、開発者サイドはブラウザ間で仕様の異なる言語を用いることになり、苦労することになりました。

image.png

そこで、出てきたのがECMAScriptと呼ばれるものです。

どういうことかというと、JavaScript言語のコアな部分をECMAScriptとして仕様策定し、切り出すことにしたのです。

こうすることにより、ブラウザ間での言語仕様を統一することができ、JavaScriptは開発者にとって使い勝手の良いものへとなりました。

image.png

JavaScriptとECMAScript

先ほども述べた通り、ECMAScriptというのはプログラミング言語の仕様であり、その仕様に基づいて実装されたプログラミング言語がJavaScriptということになります。

また、JavaScript以外にECMAScriptに基づいて実装されたプログラミング言語はほとんど存在していません。

これらにより、実質的にJavaScriptの一部の仕様がECMAScriptということになってしまっているのです。

image.png

3. 実行環境による違い

JavaScriptの言語仕様の一部がECMAScriptということは、JavaScriptはその実行環境によって機能が変わってくるということを意味します。

元々はブラウザ上で動作することを想定して作られた言語ですが、現在はブラウザ上での用途に止まりません。

ここでは最も基本的なブラウザ環境下とPC上でJavaScriptを動作させるソフトウェアのNode.js環境下でのJavaScriptの機能について見ていきましょう。

(これ以外にも様々な実行環境が存在する。)

ブラウザ環境

ブラウザ環境の場合、ECMAScriptの他にWeb APIsと呼ばれるものがあります。

これはJavaScripからブラウザの機能を操作する際に用いられるもので、例えば画面の更新をする際にはDOM APIと呼ばれるものを使用します。

つまり、ブラウザ環境下でJavaScriptを使用する場合はECMAScriptとWeb APIsの機能を用いることができるということになります。

image.png

Node.js環境下

Node.js環境下の場合は、ECMAScriptに加えて、CommonJSと呼ばれるモジュールを管理するための仕様を定めたものが存在します。

これによりNode.js環境下でJavaScriptを使用する場合はECMAScriptとCommonJsの機能を用いることができるということが言えますね。

image.png


まとめると、ここで言いたいことはJavaScriptというのは実行環境によって使用できる機能が異なってくるということです。

4. まとめ

  • JavaScriptとは1990年代に開発されたブラウザ向けのスクリプト言語のこと
  • ECMAScriptとはJavaScriptのコアな部分の仕様のこと
  • JavaScriptとはECMAScriptの仕様に基づいて実装されているプログラミング言語のこと
  • JavaScriptは実行環境によって使用できる機能が変わってくる

5. 最後に

本記事の内容がみなさんの参考になれば嬉しいです。

最後までご覧いただきありがとうございました。

6. 参考文献

Udemy講座: 【JS】初級者から中級者になるためのJavaScriptメカニズム
書籍:改訂新版JavaScript本格入門


  1. スクリプト言語とは簡単にいうと、誰でも簡単にキャッチアップできるように作成されたプログラミング言語のことです。 

  2. APIとはApplication Programming Interface」の頭文字で、何らかの機能をその外部のプログラムから利用するための決まり事のことを指します。簡単にいうと、プログラムとソフトウェアとの接点と思ってもらったら結構です。 

  3. SPAとは単一のページで構成されるWebアプリケーションのことです。初回のアクセスではまずページ全体を取得しますが、以降のページ更新は基本的にjavaScriptだけでまかなっていきます。デスクトップアプリによく似た操縦性や敏速な動作を実現するためのアプローチとして近年注目を浴びているワードです。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【TypeScript】リテラルとオブジェクト型【勉強メモ】

リテラル

TypeScript,JavaScript初学者がリテラルオブジェクト型について勉強したのでメモを残す。

リテラルとは

  • JavaScript のプリミティブである値そのものである。
  • リテラルは「文字通り」という意味の単語で、ソースコードに数値や文字列をベタガキしてその値を表現する

    • Boolean 型
    • truefalseの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
```

参考

何か指摘等ございましたら、コメントでお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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});

参考

JavaScriptのMutationObserverでDOMの変化を監視する方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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});

参考

JavaScriptのMutationObserverでDOMの変化を監視する方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

E2Eテスト -マークアップチェック編 clientScriptsメソッド-

これまで、

  • 各ページのスクリーンショットを撮る
  • フォーム送信の動作確認
  • metaなどの情報取得チェック

をTestcafeで実装してきました。

今回はさらに「h1があるか」「画像のaltが抜けていないか」などSEOにも関わってくるマークアップチェックも行いましたので実装方法を書いていこうと思います!

。。。が、その前に!今回はTestcafeの.clientScriptsメソッドを使って実装していくので、まずはこのメソッドについて公式ドキュメントから抜粋した内容を書いていきます。

.clientScriptsメソッド

Fixture.clientScripts / Test.clientScripts

テスト中にアクセスしたすべてのページにスクリプトを挿入します

【Fixtureの場合】

fixture.clientScripts( script[, script2[, ...[, scriptN]]] ) → this

sample.js
fixture `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]]] ) → this

sample.js
test
  ('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.js
fixture `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.js
test
  ('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.js
fixture `My fixture`
  .page `https://example.com`
  .clientScripts({ module: 'lodash' });

【Testの場合】
test.clientScripts( { module: moduleName } )
test.clientScripts( { module: moduleName }, ... )
test.clientScripts([ { module: moduleName } ])

sample.js
test
  ('My test', async t => { /* ... */ })
  .clientScripts({ module: 'lodash' });

スクリプトコードを挿入する

contentプロパティを持つオブジェクトを渡して、挿入されたスクリプトを文字列として提供できます。

【Fixtureの場合】
fixture.clientScripts({ content: code })
fixture.clientScripts({ content: code }, ...)
fixture.clientScripts([ { content: code } ])

sample.js
const 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.js
const 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.js
fixture `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.js
test
  ('My test', async t => { /* ... */ })
  .clientScripts({
    page: /\/user\/profile\//,
    path: 'dist/jquery.js'
  });

挿入されたスクリプトでDOMにアクセスする

TestCafe はカスタムスクリプトを head タグに注入します。
これらのスクリプトは、DOM がロードされる前に実行されます。
これらのスクリプトでDOMにアクセスするには、DOMContentLoadedイベントが発生するまで待ちます。

sample.js
const 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.js

APIメソッド

runner.clientScripts

pagecontentおよび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メソッドについてのドキュメントになります。
次回の記事ではこのメソッドを使ったマークアップチェックの実装方法を書いていきますので併せてご覧いただけると嬉しいです。

参照

公式ドキュメント

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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カレンダーのような使い勝手を実現してくれるものです。
スクリーンショット 2020-12-23 15.53.00.png
Pro版は、リソース別スケジュール管理機能が使えるようになり、担当者や会議室別など、最大5軸を自由に切り替えてスケジュール表示できるというものです。
スクリーンショット 2020-12-23 15.55.29.png

試用期間無制限で使うことができるという太っ腹なプラグイン。

試用期間中は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/ac9077762f29d2c3d4a1

3. 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を参照して作成しました。
プレゼンテーション1.jpg

          // 日付リスト作成
          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);

できあがり

下記のようになります!地味ですが、役立ちそうですね!?
スクリーンショット 2020-12-23 17.30.50.png

6. 課題

  • 言うまでもありませんが、DOMを操作してますので、アップデートなどで動かなくなるリスクがあります。ただし、FullCalendarのバージョンが上がる可能性は直近では低いと思われますし、kintoneアップデートの影響を受けるようなカスタマイズはしてないので、低リスクとは考えています。
  • 開始/終了は、日付前提で作成しています。日時の場合は更に分岐が必要でしょう。
  • デバッグ足りない気がするので、不具合を見つけた方はこっそり教えて下さい。。。

7. おわりに

実は記事を書き始めてから致命的なバグに気づいてしまい、逃げの仕様に変更したりしたのですが、なんとか完成に漕ぎ着けました。イケてないところを見つけた方は教えて下さい?‍♂️

カレンダーPlus JavaScript APIのイベントがもっと増えると、カスタマイズの可能性がさらに広がりますね!
みなさんもカスタマイズを考えてみて、このイベントが欲しい!などとじゃんじゃんリクエストしてみましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 にあるサイトを変更したときにどのようにサイトを更新し、変更を即反映させるかについて紹介します。

( 内容はこちらのリンクとほぼ被りますので、もし、本記事でわからないことがあればそちらを読んでいただけると幸いです)

全体構成

全体構成はこんな感じです。

diagram.png

作成手順

取り組む前の準備

  • Route 53 でメインドメインをまだ作成していない場合は作成してください。
  • Route 53 に追加するレコード名を予め、検討してください。( revcomm-christmas-demo.example.com とします。)

素材の準備

まず、画像を用意します。(頑張ってコピー等でダウンロードしてください。)

christmas_tree.png

コードの準備

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 にします)

s00.png
s01.png

次にコンテンツをアップロードしてください。(ドラッグアンドドロップで可能)

s03.png

これで S3 にコンテンツが追加されました。

Certificate Manager の設定

SSL 証明書の作成です。
Certificate Manager にアクセスし、Region を N.Virginia にした状態で SSL 証明書の新規作成をクリックしてください。

image.png

予め検討した URL を入力してください。

image.png

DNS検証 にしてください。

image.png

レコード作成を選択してください。

image.png

レコードを追加すると、 Route 53 に SSL 証明書用のレコードが追加されます。

CloudFront の設定

CloudFront にアクセスし、CloudFront Distribution の新規作成をクリックしてください。

image.png

以下のようにパラメータを修正してください。(英語のままですが、ご容赦願います。)

  • 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 と書けば出るはず)

cf01.png
cf02.png

しばらくすると、デプロイが完了します。(15分以上かかる可能性あり)
完了したら、 CloudFront のドメイン名をメモってください。

cf03.png

補足

  • S3 のポリシーも自動的に更新します。
  • OAI (Origin Access Identity) を使用することで、 S3 をpublic化をしない状態でホスティングすることができます。

Route 53 のレコード作成

Route 53 にアクセスし、ドメイン名 > レコードを追加 をクリックしてください。

image.png

レコードを作成してください。
Route ポリシーを Simple Route、 レコード名を予め検討した URL のサブドメイン名(revcomm-christmas-demo)、 レコードタイプをCNAME、 Value を先ほどメモった CloudFront を URL 入力してください。

image.png

WAF の設定

WAF にアクセスし、IP Sets で Region を Global (CloudFront) にした状態で新規 IP Set を作成してください。

image.png

IP を設定してください。

image.png

次にファイヤウィールの設定です。Web ACLs (Access Control List) で Region を Global (CloudFront) にした状態で 新規 ACL を作成してください。

image.png

任意の名前を設定して、関連リースの追加をクリックしてください。

image.png

WAF に関連するリソースを追加してください。(CloudFrontのID番号が表示する)

image.png

次へをクリックし、Add rules > Add my own rules ... をクリックしてください。

image.png

ルールタイプ IP Set を選択し、 ルール名(任意)を好きな名前にした状態で先ほど作成した IP set を入力し、作成してください。Default Action を Allow にしてください。

image.png

デフォルトを Block にしてください。

image.png

あとは ACL を作成して完成です。

結果

予め検討した URL (revcomm-christmas-demo.example.com) にアクセスすると、以下のようになります。

f00.png

ちなみに IP アドレスを変えると、以下のようになります。

f01.png

更新手順

次に更新方法です。

コードの修正

以下のようにコードを修正してください。

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 をクリックしてください。

cf10.png

Invalidations で 無効化の作成 をクリックしてください。

cf11.png

あとは 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

ただし、このやり方で注意していただきたいのが、httpsindex.html を明示的に示さないといけないことです。そのため、共有するときに URL の扱いに注意しないといけなくなります。

もし、リダイレクト等も含めたい、index.html まで記述させたくない場合、ぜひこちらの記事を参考に実施していただけると幸いです。

最後に

いかがでしたでしょうか。

今回の Qiita advent calendar は RevComm として初の試みではありましたが、有益な情報は得られましたでしょうか。

RevComm では「コミュニケーションを再発明し、人が人を想う社会を創る」というミッションを基に、電話営業をディープラーニングの技術で支援するプロダクト( Miitel ) をはじめ、コミュニケーションに関わる様々なプロダクト開発を行っており、日頃からコミュニケーションの在り方を再定義するという難しい課題に取り組んでいます。

弊社ではテックに関わらず成長に貪欲な人がたくさんいます。会社としてまだまだ若いところもありますが、新しい技術等に積極的に取り入れる会社ではあるので、エンジニアとしてテックスタックを広げたいという人にとってはいい会社です。

もし、弊社で一緒に働きたいという想いがあれば、あるいは少しでも興味があればぜひぜひ弊社の採用ページに応募してみてください。

ちなみに働き方について興味があれば、ぜひ CTO が書いたこちらの記事を読んでください。

では良いクリスマスを!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ツクール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() {
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【メモ】ツクール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() {
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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()プロパティを指定できます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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);

実行
スクリーンショット 2020-12-23 13.31.10.png

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-23 13.35.56.png

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

わかりにくい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関数にいつもの処理を書いてあげる必要があります。

所感

なんでここだけマニュアルわかりにくいんだろ、、、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Server Componentについてまとめてみた

先日、ReactがReact Server Componentsを予告しました。まだ開発中とのことなので使えるのは先になりそうですが、非常に面白い内容となっているのでこちらで共有したいと思います。

What is React Server Component

React Server Componentとは、サーバーサイドのみで実行され、バンドルサイズへの影響を与えません。React Server Componentはクライアントでダウンロードされないため、アプリの起動時間などの向上などが期待できます。

  • 従来のReact Component(以下Client Comonentとする)
    client_react.jpg

  • React Server Component
    server_react.jpg

また、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はisEditingtrueの時のみクライアントにロードされる。つまり必要に応じて動的にロードされることになる

続いて動的にロードされる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を動的にロードできるのでクライアントではページのレンダリングに必要な最小限のコードのみダウンロードできる
  • リロード時にクライアントの情報を保持する

さらに詳しい情報が知りたい場合は、公式からデモ動画を見て、デモ用のプロジェクトを試してみてください?

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js 3 入門 「Vuex」

はじめに

Vue.js 3 の Vuex について、自分が学んだことを備忘録として記載します。
Vue.js に殆ど触れたことが無い方に少しでも参考になれば幸いです。
誤り等あれば、ご指摘頂けますと大変喜びます

Vuex とは

Vuexはアプリで利用するデータを、一箇所に集中管理するためのライブラリです。
管理だけではなく、データの操作(更新)方法も標準化することができます。コンポーネント間でのデータ共有がシンプルに実現できるようになりますね。

今回のお題

今回は、数字をカウントできる簡単なアプリを作成してみます。

image.png

プロジェクトの作成

まずは 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    

プロジェクトに組み込むモジュールを選択

プロジェクトに組み込むモジュールを選択します。
ここでBabelLinterに加えて、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 Testing                                                                                                                                                                                                                                                                   

Vue.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      

設定情報の格納先を選択

BabelESLintの設定情報を個別の設定ファイルとするか、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

生成されたフォルダを確認

カレントフォルダに、指定したプロジェクト名のフォルダが生成されています。

image.png

アプリの実行

早速実行してみましょう。
上記のプロジェクト生成完了時の文言(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]で終了することができます。

image.png

ストアの定義

まずはストアを定義します。
ストアは主に、データと、データを更新するためのメソッド で構成されます。プロジェクトを作成した時点で、空のストア定義(/src/store/index.js)が用意されているので、こちらを編集していきます。

/src/store/index.js

import { createStore } from 'vuex'

export default createStore({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

ストアはcreateStoreメソッドで定義することができます。

createStoreメソッド

引数

  • defs
    • ストアを構成する要素(ステート、ミューテーション、etc...)を要素名:定義の形式で記述します
    • 複数の要素を定義する際は区切り

ステートの定義

ステートとは、ストアで管理されるデータの本体です。
ステートで管理すべき情報を名前: 初期値の形式で定義します。(複数の場合は区切り)

今回は、カウンターを示すcountを定義します。

/src/store/index.js

import { 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が表示されています。
image.png

ミューテーションの呼び出し

次に、ステート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>

実際の画面を確認すると、[+]ボタンと[-]ボタンが増えており、
ステートの値を増減させることができるようになりました。

image.png

以上となります。
ありがとうございました。
他の機能(ゲッター/アクション/etc...)については別の記事にします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

html, javasript, jquery (OOP) 作成 P4(リスト追加)

html、 javasriptのツールの開発

 →オンラインツールを使えるため、ツールのダウンロードが不要です。
   https://jsfiddle.net/

スクリーンショット 2020-12-21 10.23.23.png

テーブルに対して(例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);

実行
スクリーンショット 2020-12-23 10.56.53.png

テーブルに対して(例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);

実行
スクリーンショット 2020-12-23 11.52.16.png

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-23 10.54.32.png

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

html, javasript, jquery (OOP) 作成 P4(クラス定義)

html、 javasriptのツールの開発

 →オンラインツールを使えるため、ツールのダウンロードが不要です。
   https://jsfiddle.net/

スクリーンショット 2020-12-21 10.23.23.png

テーブルに対して(例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);

実行
スクリーンショット 2020-12-23 10.56.53.png

テーブルに対して(例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);

実行
スクリーンショット 2020-12-23 11.52.16.png

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-23 10.54.32.png

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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つのみ)

以上、参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む