20200301のJavaScriptに関する記事は25件です。

GitHub Actions を利用して自動的にコードを整形する。

コード整形していますか?

複数人数でコードを書いていたりするとエディターの設定によってインデントの幅が違ったり、改行の位置が違ったりして困ってしまうことってありませんか?

GitHub Actions を利用すると、コードをプッシュしたときに自動的にコードを整形するワークフローが構築できます。

Git Commit -> Push -> GitHub Actions でコード整形 -> 整形コミットを GitHub にコミット&プッシュ

本記事は GitHub Actions を用いた自動コード整形環境の構築方法を紹介します。

今回の紹介ツール

  • Prettier ( JavaScript 整形ツール )
  • GitHub Actions
  • stefanzweifel/git-auto-commit-action

Prettier

Prettier とは JavaScript や HTML, CSS を自動整形してくれるツールです。

自分は JavaScript ( TypeScript ) をよく記述しているので、今回は Prettier を利用してコード整形する方法を紹介します。

Prettier のインストール

npm install -D prettier
// or
yarn add -D prettier 

prettierの設定ファイル .prettierrc.js の作成(オプション)

'use strict';

module.exports = {
  bracketSpacing: false,
  singleQuote: true,
  jsxBracketSameLine: true,
  trailingComma: 'es5',
  printWidth: 80
};

以下のコマンドで、Pretteir でコード整形できます。

npx prettier --write "src/**/*.{ts,tsx,js,jsx}"
//or
yarn prettier --write "src/**/*.{ts,tsx,js,jsx}"

GitHub Actions

GitHub Actionsとは GitHub が提供している CI ツールです。CircleCI や Travis CI などのように Git の Push やプルリクのアクションに応じて、あらかじめ設定しておいたタスクを実行してくれるものです。

GitHub でコード管理 && CI 実行が完結するので管理が楽になります。

.github/workflow/******.yml に保存されたコードを GitHub Actions は実行してくれます。

GitHub Actions でコード変更を行ったときにコミットする

GitHub Actions でコードの内容を変更したとき、変更内容をコミットするために以下の Action を利用します。

stefanzweifel/git-auto-commit-action

使いかたはこんな感じです。

   steps:
     - uses: stefanzweifel/git-auto-commit-action@v3.0.0
        with:
          commit_message: Apply Prettier Change

GitHub Actions 内で Pretteir でコード整形をしたあとに、この Action を実行すれば、整形済みコードをコミットできるというわけです。

自動的にコードを整形

サンプルコードを二つ載せます

  • master ブランチが更新されたら、自動的に pretteir でコードを整形
  • Pull Request をもらったときに PR ブランチを整形

master ブランチが更新されたら、自動的に pretteir でコードを整形

Master ブランチがコミットされると自動で整形コミットを行います。
プルリクエストが取り込まれたあとに、コード整形が行われるので GitHub Flow や Git Flow などといった直接 master に push しない運用だと、push 時にコンフリクトしにくいです。

.github/workflows/prettier.yml

name: Pretter

on:
  push:
    branches:
      - master

env:
  FILE_PATTERN: "{src,__tests__}/**/*.{ts,tsx,js,jsx}"

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: 12.x
      - name: Run Prettier
        run: |
          yarn install
          yarn prettier --write ${FILE_PATTERN}
      - uses: stefanzweifel/git-auto-commit-action@v3.0.0
        with:
          commit_message: Apply Prettier Change

Pull Request をもらったときに PR ブランチを整形

プルリクエストブランチに対して、整形を行います。プルリクエストがきたときに整形を行うので master ブランチには整形済みのコードしかマージされません。

ただ注意点は、プルリクエストをだした後に整形コミットを GitHub 側で追加するので、プルリクエストに追加コミットをプッシュするとき、git push -f をするか、もしくは、git pull してから git push する必要があります。

.github/workflows/prettier.yml

name: Pretter

on:
  pull_request:

env:
  FILE_PATTERN: "{src,__tests__}/**/*.{ts,tsx,js,jsx}"

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          ref: ${{ github.head_ref }}
      - uses: actions/setup-node@v1
        with:
          node-version: 12.x
      - name: Run Prettier
        run: |
          yarn install
          yarn prettier --write ${FILE_PATTERN}
      - uses: stefanzweifel/git-auto-commit-action@v3.0.0
        with:
          commit_message: Apply Prettier Change
          ref: ${{ github.head_ref }}

まとめ

コード整形とコード修正がまざってしまうと、肝心のコード変更点がどこなのかわかりにくくなってしまいがちです。
GitHub Actions を利用してコード整形を自動化してしまうことで、コード整形忘れをなくしましょう。

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

GitHub にコード整形してもらおう - GitHub Actions でコード整形&コミット

コード整形していますか?

複数人数でコードを書いていたりするとエディターの設定によってインデントの幅が違ったり、改行の位置が違ったりして困ってしまうことってありませんか?

GitHub Actions を利用すると、コードをプッシュしたときに自動的にコードを整形するワークフローが構築できます。

Git Commit -> Push -> GitHub Actions でコード整形 -> 整形コミットを GitHub にコミット&プッシュ

本記事は GitHub Actions を用いた自動コード整形環境の構築方法を紹介します。

今回の紹介ツール

  • Prettier ( JavaScript 整形ツール )
  • GitHub Actions
  • stefanzweifel/git-auto-commit-action

Prettier

Prettier とは JavaScript や HTML, CSS を自動整形してくれるツールです。

自分は JavaScript ( TypeScript ) をよく記述しているので、今回は Prettier を利用してコード整形する方法を紹介します。

Prettier のインストール

npm install -D prettier
// or
yarn add -D prettier 

prettierの設定ファイル .prettierrc.js の作成(オプション)

'use strict';

module.exports = {
  bracketSpacing: false,
  singleQuote: true,
  jsxBracketSameLine: true,
  trailingComma: 'es5',
  printWidth: 80
};

以下のコマンドで、Pretteir でコード整形できます。

npx prettier --write "src/**/*.{ts,tsx,js,jsx}"
//or
yarn prettier --write "src/**/*.{ts,tsx,js,jsx}"

GitHub Actions

GitHub Actionsとは GitHub が提供している CI ツールです。CircleCI や Travis CI などのように Git の Push やプルリクのアクションに応じて、あらかじめ設定しておいたタスクを実行してくれるものです。

GitHub でコード管理 && CI 実行が完結するので管理が楽になります。

.github/workflow/******.yml に保存されたコードを GitHub Actions は実行してくれます。

GitHub Actions でコード変更を行ったときにコミットする

GitHub Actions でコードの内容を変更したとき、変更内容をコミットするために以下の Action を利用します。

stefanzweifel/git-auto-commit-action

使いかたはこんな感じです。

   steps:
     - uses: stefanzweifel/git-auto-commit-action@v3.0.0
        with:
          commit_message: Apply Prettier Change

GitHub Actions 内で Pretteir でコード整形をしたあとに、この Action を実行すれば、整形済みコードをコミットできるというわけです。

自動的にコードを整形

サンプルコードを二つ載せます

  • master ブランチが更新されたら、自動的に pretteir でコードを整形
  • Pull Request をもらったときに PR ブランチを整形

master ブランチが更新されたら、自動的に pretteir でコードを整形

Master ブランチがコミットされると自動で整形コミットを行います。
プルリクエストが取り込まれたあとに、コード整形が行われるので GitHub Flow や Git Flow などといった直接 master に push しない運用だと、push 時にコンフリクトしにくいです。

.github/workflows/prettier.yml

name: Pretter

on:
  push:
    branches:
      - master

env:
  FILE_PATTERN: "{src,__tests__}/**/*.{ts,tsx,js,jsx}"

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: 12.x
      - name: Run Prettier
        run: |
          yarn install
          yarn prettier --write ${FILE_PATTERN}
      - uses: stefanzweifel/git-auto-commit-action@v3.0.0
        with:
          commit_message: Apply Prettier Change

Pull Request をもらったときに PR ブランチを整形

プルリクエストブランチに対して、整形を行います。プルリクエストがきたときに整形を行うので master ブランチには整形済みのコードしかマージされません。

ただ注意点は、プルリクエストをだした後に整形コミットを GitHub 側で追加するので、プルリクエストに追加コミットをプッシュするとき、git push -f をするか、もしくは、git pull してから git push する必要があります。

.github/workflows/prettier.yml

name: Pretter

on:
  pull_request:

env:
  FILE_PATTERN: "{src,__tests__}/**/*.{ts,tsx,js,jsx}"

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          ref: ${{ github.head_ref }}
      - uses: actions/setup-node@v1
        with:
          node-version: 12.x
      - name: Run Prettier
        run: |
          yarn install
          yarn prettier --write ${FILE_PATTERN}
      - uses: stefanzweifel/git-auto-commit-action@v3.0.0
        with:
          commit_message: Apply Prettier Change
          ref: ${{ github.head_ref }}

まとめ

コード整形とコード修正がまざってしまうと、肝心のコード変更点がどこなのかわかりにくくなってしまいがちです。
GitHub Actions を利用してコード整形を自動化してしまうことで、コード整形忘れをなくしましょう。

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

gonを使ってJavaScriptでRailsの環境変数を使用する

概要

Railsアプリケーションを作成時、
dotenv-railsを使って.envファイルに環境変数を書き込んだのですが
JSファイルではそのまま使用することができず、gonというGemを使用するとJSと連携ができるとのことだったので備忘録としてまとめてみます。

(間違いや改善点があればご教示いただけますと幸いです!)

Gemをインストール

Gemfile.
gem 'dotenv-rails'
gem 'gon'

bundle installを実行

.envファイルを作成

appファイル直下に作成して、環境変数を記述します

MY_PRIVATE_KEY = '************'

Rails側の呼び出し

(JSファイルのみでの使用であればなくてOK)

  def new
    my_private_key = ENV["MY_PRIVATE_KEY"]
  end

JSファイルへの連携

  def new
    gon.my_private_key = ENV['MY_PRIVATE_KEY']
  end

JSファイルでの表記

var mykey = gon.my_private_key;

これで環境変数が取ってこれます!

.envを.gitignoreに追記

/.env

これで安全に環境変数を扱えますね。

gonは便利と聞いたのでもっと理解を深めていきたいです!

間違いがあればご指摘くださいm(__)m

以上となります、ありがとうございました。

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

Nuxt.jsのpluginsにaxiosの共通部品を実装する

はじめに

Nuxt.jsにてクライアントサイドのvueからサーバのREST APIを呼び出す際に、vue内のスクリプトにロジックを書くと、他の場所で流用できない。そのため、外部のjsに共通ロジックとしてサーバのAPIを呼び出す処理を記述したかったが、あまり良い例が無かったので、検討&実装してみた(正しいかは不明)
Vuexストア内でaxiosを使ってサーバAPIを呼び出すみたなことをやってる人も居たけど、それはちょっと違うだろう(というか気持ち悪い)ということで、pluginsに共通ロジックを作成することにした。

構成

修正する対象は以下の3ファイル(pages/index.vue , plugins/axios.js , plugins/api.js)

ROOT
 ├─.nuxt
 ├─assets
 ├─components
 ├─layouts
 ├─middleware
 ├─pages
 │      index.vue
 ├─plugins
 │      api.js
 │      axios.js
 ├─server
 ├─static
 ├─store
 └─test

axios.js

以下の記事を参考に、pluginsフォルダ配下にaxios.jsを作成する。
今回は記事のまま修正はしていない。このプラグインを作成することで、axiosでサーバアクセスする際、
リクエスト発生時、エラー発生時に処理をフックして任意の処理を実行することができる。
$axios.onResponseでレスポンス時にフックすることも可能。

https://axios.nuxtjs.org/extend

axios.js
export default function ({ $axios, redirect }) {
  $axios.onRequest((config) => {
    console.log('Making request to ' + config.url)
  })

  $axios.onError((error) => {
    const code = parseInt(error.response && error.response.status)
    if (code === 400) {
      redirect('/400')
    }
  })
}

api.js

このjsがapiを呼び出す共通部品。

api.js
export default function ({ $axios }, inject) {
  const api = new API($axios)

  inject('api', api)
}

class API {
  constructor (axios) {
    this.axios = axios
  }

  async getHoge () {
    const res = await this.axios.$get('http://localhost:3000')
    console.log(res)
  }
}

次の処理を入れることで、後述するvue内で $api としてアクセスすることができるようになる。

  inject('api', api)

ここまで作成したら、 nuxt.config.js 内の plugins に次のように記載する。

nuxt.config.js
  plugins: [
    '~/plugins/axios',
    '~/plugins/api'
  ],

idnex.vue

index.vueの<script>部分だけを抜粋。
asyncData() 内ではthisが使えないので、context.app.\$api.getHoge()
mounted() 内などのthisが使える箇所では、this.\$api.getHoge()
とすることで、api.jsに記載したaxiosの共通ロジックを呼び出すことができる。

index.vue
<script>
import Logo from '~/components/Logo.vue'

export default {
  components: {
    Logo
  },
  asyncData (context) {
    context.app.$api.getHoge()
    return {
    }
  },
  mounted () {
    this.$api.getHoge()
  }
}
</script>

まとめ

この記述がNuxt.jsとして正しいかどうかは、少し微妙なところ。
そもそもAPIクラスを作成する必要が無く、inject('メソッド名', function)とすればよい。
しかし、今回作成したかったアプリケーションは、バックエンドが複数あり、
そのバックエンド毎に処理を実行する共通インスタンス(この例では\$api)を
作成したかったからこのようにしている。
\$backendA.getHoge(), \$backendB.getHoge() のような感じ。

参考

  • Axios Module
    https://axios.nuxtjs.org/
    なぜNuxt.jsの本家サイトにこの例が記載されていないのかが謎ってくらい「なるほどね」ってなった。
  • Nuxt.jsでaxiosの共通処理を作成し、API呼び出し処理をラップして使用する
    https://qiita.com/itouuuuuuuuu/items/4132e3b7ddf2cbf02442
    最初この記事を見つけたときは「おぉ!!」ってなった。次の2点を自分なりに改善してみたのが本記事。
    • export let axios;
      plugins/axios/index.js 内で export let axios; としている。
      axiosをexportしたかったからこのようにしたんだと思うけども、
      これだとESLintのimport/no-mutable-exports に引っかかってしまう。
    • import UserApi from '@/plugins/axios/modules/user'
      作成した共通部品をvue内で使用する際にimportしなければならない。
      これは地味にメンドイ。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

やってみよう!GAS入門

GASとは

GASとは、GoogleAppsScriptというJavascriptをベースに開発された言語です。
特徴としては、サーバーサイドで実行できる点と、Googleのサービスと連携できる点が挙げられます。
今回の記事では、GASを用いてスプレッドシートの操作をやってみます。

GASの編集画面を開こう

まずはGoogleドライブにアクセスし、スプレッドシートを開きましょう。
スクリーンショット 2020-03-01 22.00.21.png
[ツール]→[スクリプトエディタ]をクリックすれば以下のような画面が表示されると思います。
スクリーンショット 2020-03-01 22.02.29.png
ここがコードを書く画面となります。

セルに文字列を書き込む

GASの編集画面で以下のコードを実行してみましょう。(⌘+Rで実行)

function myFunction() {

  //現在開いているスプレッドシートを取得
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

 //現在開いているシートを取得
  var sheet = spreadsheet.getActiveSheet();

  //getRangeで処理を行うセルを指定。setValueで書き込む内容を指定
  sheet.getRange("A1").setValue("hoge");  

}

A1のセルにhogeが書き込まれました!続いて、セル内容の取得をやってみましょう。
スクリーンショット 2020-03-01 22.21.35.png

セル内容の取得

セル内容の取得ではこんな感じで書いてみましょう。(⌘+Rで実行)

function myFunction() {

  //現在開いているスプレッドシートを取得
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

 //現在開いているシートを取得
  var sheet = spreadsheet.getActiveSheet();

  //B2セルを取得
  range = sheet.getRange("B2")

  //B2セルの内容を取得
  var value = range.getValue();

  //B2セルの内容をポップアップ上に出力
  Browser.msgBox(value);
}

するとRunning function myFunction...となって処理が止まるはずです。
スクリーンショット 2020-03-01 22.47.23.png
ここでスプレッドシートに移動しましょう。以下のようにB2セルの内容がポップアップ上に表示されていますね!
スクリーンショット 2020-03-01 22.48.55.png

まとめ

今回はセルへの書き込み、セル内容の取得といった非常に簡単な例で紹介しました。EXCELでいうVBAのように、スプレッドシートでもGASを使ってマクロを組めます。Slackのアプリケーション作成などにも使えますのでぜひ使ってみてください!

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

【最新版】Tinderで自動右スワイプできなくなった難民を救うコード

はじめに

2020年3月1日現在、Tinderのいいねボタンを含むコンポーネントのクラス名が変わった為、以前と同じように自動スワイプできなくなる問題が発生しました。これは昨今流行している「新型コロナウィルス」に勝るとも劣らない、日本社会における文化的な交流を妨げる由々しき事態と考え、急ぎ解決策を書き起こした次第です。

コード

御託はいいからさっさと教えろって方、お待たせしました('ω')

『一度LIKEを手動でクリックしてから』

F12でDeveloper Toolを開き、下記のコードをConsoleにコピペしてEnterを叩けば新たな出会いが開かれるでしょう。
ブラウザを更新しなければ、検索距離や年齢等の条件を変更も可能で、再度の手動クリックは不要です。
image.png

Tinder.js
count = setInterval(
function(){
var buttons = document.getElementsByClassName("button Lts($ls-s) Z(0) CenterAlign Mx(a) Cur(p) Tt(u) Bdrs(50%) P(0) Fw($semibold) focus-button-style Bxsh($bxsh-btn) Expand D(b) Bgc(#fff) Trstf(e) Trsdu($normal) Wc($transform) Scale(1.1):h Scale(.9):a");
buttons[2].click()
},1000)

コード解説(開発者向け)

setInterval - 処理を定期実行するメソッド(第一引数:処理内容、第二引数:処理のインターバル(ミリ秒))
getElementsByClassName - クラス名を指定して、そのクラスが持つメソッドなんかを丸々取得
buttons - 4つのボタンのリスト(Dislike, Super Like, Like, Boost)3番目がLike
※上述しましたが、手動クリック前後で各ボタンの挙動が変わる場合がある為注意。

クラス名はボタンを右クリックして、検証を押せば見れます。
image.png
image.png

おわりに

参考になった方は右スワイプ、もとい、いいねをお願いします!
 

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

webpack & node-sass & eslintでシンプルなフロント開発環境構築

目的

  • Web Frameworkで使うことを前提としてES6, Sassをトランスパイルする。
  • フロント開発に必要な人権を満たす開発環境を作る。

指針

  • フロントエンドをcreate-react-app, vue-cliなどのツールに頼らずに0ベースで作る。 -> 拡張性の問題(変な依存、余計な設定を盛り込みたくない。cli使っても結局Document読むから面倒臭い)
  • 多人数の開発チームを想定して構築する。
  • シンプルが正義。

経緯

Spring bootで個人開発していたが、やっぱりプレーンJSは書きたくなかった。
それと今までcreate-react-appでごちゃごちゃやっていたので、1からフロント環境を設定したかった。
著者は執筆時点で実務1年ちょいのフロント知識浅めなペーペーなのでマサカリお願いします。

要件・設計

  • yarn buildコマンドでjsのバンドルファイルとScssのトランスパイル結果をデプロイ先のフォルダに配置する。
  • yarn devコマンドでJS, cssのホットリローディングに対応する。
  • メディアクエリーをcssに書きたくないので、SPとPCのcssファイルを分離しておく(そうでないケースもあると思います)。

実装

自分はspring bootのstaticフォルダを資源の配置先にしているので、そうでない方は読み替えてください。
なお、node_modulesについて開発環境でしか使わないものはyarnなら-Dオプション。npmなら--save-devオプションをつけるようにしてください。

webpack

yarn add -D webpack webpack-cli
webpack.config.js
module.exports = {
  mode: process.env.NODE_ENV || 'development',
  // バンドル対象のファイルのエントリーポイント
  entry: ['./src/index.js'],
  output: {
    // ビルド後のファイル名
    filename: 'bundle.js',
    // ビルド後の配置先ディレクトリ
    // webアプリにビルド結果を配置
    path: __dirname + '../../src/main/resources/static/js'
  },
  module: {
    // どのファイルに対してどんなことをするか
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        query: {
          presets: ['@babel/preset-env']
        }
      }
    ]
  }
};

次に、設定したようにトランスパイルするため、以下のようにyarn addnpm installしてください。

yarn add -D @babel/core @babel/polyfill @babel/preset-env babel-loader

これでsrc/indexファイルがwebpack --config webpack.config.jsを実行すると指定先にコンパイルされたバンドルファイルが生成される。便利!
src/indexでimportしているファイルがバンドル対象になるので、他の設定は不要です。

ESLint

cliツールに頼らないと言いつつもいつも初期化するときはcliでやっていたので、

yarn add -D eslint

して

npx eslint --init

npxコマンドはnpm initされた階層のnode_modulesのcliモジュールをPATHを通さなくても呼び出せる、そんなノリのコマンドっぽい。

で対話形式で設定ファイルを作成する。自分はJSONで作成して以下の通りになった。

.eslintrc.json
{
  "env": {
    "browser": true,
    "es6": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:vue/essential"
  ],
  "globals": {
    "Atomics": "readonly",
    "SharedArrayBuffer": "readonly"
  },
  "plugins": ["vue"],
  "rules": {
    "semi": "error",
    "quotes": ["error", "single"]
  }
}

ESLintのエディタ設定はVScodeのみ次回更新。

node-sass

ついでにsassを使いたかったのでnode-sassのcliツールで.scss.cssにトランスパイルする。

yarn add -D node-sass

webpackでもできそうだが、このコマンドで必要なことはできるので採用。

npx node-sass [トランスパイル対象ファイル] [出力先]

とりあえずここまでで必要なことは全て揃っている。
そしたらあとはnpm scriptsの設定をする。
なお、npm scriptsでスクリプトを書くことになるわけだが、このスクリプトに環境依存を持たせないために、今回追加でnpm-run-allモジュールを追加する。

npm-run-allとは

以下がここまで依存関係を追加した結果と、npm scriptsの設定になる。

package.json
{
  "name": "client",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "run-p run:js run:sass",
    "build": "run-s build:js; yarn build:sass",
    "build:js": "webpack --config webpack.config.js",
    "run:js": "webpack --progress --colors --watch --config webpack.config.js",
    "build:sass": "run-s build:sass:pc build:sass:sp",
    "build:sass:pc":"node-sass src/style/pc/index.scss ../src/main/resources/static/css/pc.css",
    "build:sass:sp": "node-sass src/style/sp/index.scss ../src/main/resources/static/css/sp.css",
    "run:sass": "run-p run:sass:pc run:sass:sp",
    "run:sass:pc": "node-sass -w src/style/pc/index.scss ../src/main/resources/static/css/pc.css",
    "run:sass:sp": "node-sass -w src/style/sp/index.scss ../src/main/resources/static/css/sp.css"
  },
  "dependencies": {},
  "devDependencies": {
    "@babel/core": "^7.8.6",
    "@babel/polyfill": "^7.8.3",
    "@babel/preset-env": "^7.8.6",
    "babel-core": "^6.26.3",
    "babel-loader": "^8.0.6",
    "babel-preset-es2015": "^6.24.1",
    "eslint": "^6.8.0",
    "node-sass": "^4.13.1",
    "npm-run-all": "^4.1.5",
    "webpack": "^4.41.6",
    "webpack-cli": "^3.3.11"
  }
}

重要なのはここ。

package.json
"scripts": {
    "dev": "run-p run:js run:sass",
    "build": "run-s build:js; yarn build:sass",
    "build:js": "webpack --config webpack.config.js",
    "run:js": "webpack --progress --colors --watch --config webpack.config.js",
    "build:sass": "run-s build:sass:pc build:sass:sp",
    "build:sass:pc":"node-sass src/style/pc/index.scss ../src/main/resources/static/css/pc.css",
    "build:sass:sp": "node-sass src/style/sp/index.scss ../src/main/resources/static/css/sp.css",
    "run:sass": "run-p run:sass:pc run:sass:sp",
    "run:sass:pc": "node-sass -w src/style/pc/index.scss ../src/main/resources/static/css/pc.css",
    "run:sass:sp": "node-sass -w src/style/sp/index.scss ../src/main/resources/static/css/sp.css"
  }

コマンドの内容は雰囲気でわかると思うので、軽く説明。

  • JSのホットリローディング
# ファイル更新のたびにビルドが走り、コンパイルの進捗も表示 + 色付け
webpack --progress --colors --watch
  • Scssのホットリローディング
# src/style/pc/index.scssをファイル更新のたびに
# ../src/main/resources/static/css/pc.cssにトランスコンパイル
# 
node-sass -w src/style/pc/index.scss ../src/main/resources/static/css/pc.css
  • npm-run-allのコマンド
# 1と2を並列で実行
run-p [npm scripts1] [npm scripts2]

なお、ビルド時には失敗したならすぐに処理落ちして欲しいのでシーケンシャルに実行する。
ここはケースバイケースでいいと思う。

# 1と2を直列で実行
run-s [npm scripts1] [npm scripts2]

Example

自分の場合は最終的にこうなった。

directory
|--.eslintrc.json
|--package-lock.json
|--package.json
|--src
|  |--index.js
|  |--style
|  |  |--_define.scss
|  |  |--pc
|  |  |  |--index.scss
|  |  |--sp
|  |  |  |--index.scss
|--webpack.config.js
|--yarn.lock
directory
--resources
|  |  |  |--application.properties
|  |  |  |--static
|  |  |  |  |--css
|  |  |  |  |  |--normalize.css
|  |  |  |  |  |--pc.css
|  |  |  |  |  |--sp.css
|  |  |  |  |--js
|  |  |  |  |  |--bundle.js
|  |  |  |  |--test.html

ビルドを実行すると狙った通りの箇所に配置できていることがわかる。

test.html
<!-- for check js and css files without application server -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="css/normalize.css" rel="stylesheet"></link>
    <link rel="stylesheet" type="text/css" media="only screen and (min-width: 0px) and (max-width: 480px)" href="css/sp.css">
    <link rel="stylesheet" type="text/css" media="only screen and (min-width: 481px)" href="css/pc.css">
    <title>Document</title>
</head>
<body>
    <h2>title</h2>
    <div id="header">header</div>
    <div class="body">body</div>
    <script src="js/bundle.js"></script>
</body>
</html>

まとめ

やりたいことが他にあったので端折りまくってしまいましたが、所詮備忘録程度に見ていただければ幸いです。

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

(自分用)debounceとthrottleをasync/awaitでシンプルに作成してみたサンプル

※自分で試してみた感じでは良さそうだったのですが、おかしい部分があるかもしれません、その時は教えていただければありがたいです。

追記
すみません、throttleの方が間違っていたので、また考え直して追記します:bow:

function debounce(callback, wait) {
  let lastRunTimestamp;
  return async () => {
    lastRunTimestamp = Date.now();

    await new Promise(resolve => setTimeout(resolve, wait));

    if (Date.now() - lastRunTimestamp >= wait) {
      callback();
    }
  };
}

See the Pen MWwooyj by 奥村健吾 (@okumurakengo) on CodePen.

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

VueとCSSとTypeScriptでシューティングゲーム「ネコメザシアタック2020」を作ったのでソースと解説

こんにちは:cat: 今日は2/22の猫の日に合わせて個人開発したゲーム「ネコメザシアタック」の技術的なポイントを解説する記事です。去年のバージョンはこちら

作ったもの

ezgif-6-22637686d4bf.gif

ソース: https://github.com/yuneco/mezashi2
アプリ: https://nekomzs2.web.app/
(PCでも遊べるけどスマホ推薦です)

使っている技術

そろそろリリース見込みのVue3を先取りした構成です

  • Vue(Vue2 + CompositionAPI)
  • TypeScript
  • CSS Transition(ほとんどのアニメーション)
  • SVG(画像 + 一部のアニメーション)
  • Firebase (Hosting + FireStoreでランキング)

おしながき(この記事の内容)

作ったもの全部を解説していくとキリがないので、主に去年からの差分を中心に面白いポイントだけ説明していきます。

アニメーションのポイント
 :cat: 角丸の地面を歩くアニメーション
 :cat: 星から星に飛び移るアニメーション
CSS Transitionでゲームを作るときの悩みと解決法
 :cat: アニメーションの途中で現在の位置や角度をどうやって取得するか
新しい技術スタックのポイント
 :cat: TypeScript + CompositionAPIの採用

それでは行ってみましょう :cat2::dash:

ポイント1: 角丸の地面を歩くアニメーション

まず今回の目玉である「たまさんが角丸の星の上をぴょんぴょん跳ねたり歩いたりする表現」を作っていきましょう。(※たまさんはこのゲームのメインキャラクターです)

長方形の上を歩く

いきなり角丸は難しそうなので、ひとまず星はただの長方形にしました。こんな感じでパラメーターtamaXを渡すといい感じにコーナーリングしてくれるようにしてみます。

たまさんコンポーネント
<TamaSan :tamaX="1.39" /> 

image.png

この計算は単純だけど面倒なので、独立した関数にしておきます。

Angle8.ts
import Pos from './Pos' // x, y, r(角度)をセットで保持する値クラス

export default {
  /**
  * 指定したX位置(一周=8)に対応する座標・回転角を求めます
  * @param val X位置
  * @param gw 地面の幅
  * @param gh 地面の高さ
  */
  at: (val: number, gw: number, gh: number): Pos => {
    const segIndex = Math.floor(val)
    const prog = val - segIndex
    const turnsR = Math.floor(val / 8) * 360

    switch (segIndex % 8) {
      case 0:
        return new Pos(gw * prog, 0, 0 + turnsR)
      case 1:
        return new Pos(gw, 0, prog * 90 + turnsR)
      case 2:
        return new Pos(gw, gh * prog, 90 + turnsR)
      case 3:
        return new Pos(gw, gh, 90 + prog * 90 + turnsR)
      case 4:
        return new Pos(gw * (1 - prog), gh, 180 + turnsR)
      case 5:
        return new Pos(0, gh, 180 + prog * 90 + turnsR)
      case 6:
        return new Pos(0, gh * (1 - prog), 270 + turnsR)
      default:
        return new Pos(0, 0, 270 + prog * 90 + turnsR)
    }
  }
}

これでTamaSanコンポーネントでは

TamaSan.vue
  const tamaPos = computed<Pos>(() => Angle8.at(props.tamaX, ground.w, ground.h))

こんな感じで算出プロパティとして簡単に位置と角度を取得できます。

角丸を歩けるようにする

つづけてこの長方形の角をとって角丸にしていきます。
一見難しそうに思える角丸ですが、実は種明かしをするとすごく簡単。「たまさんのキャラクター本体を角丸のコーナーサイズと同じだけ宙に浮かせているだけ」です

Pasted Graphic 3.png

角丸に限らず、惑星の公転のような円運動は全て同様の理屈で単純な回転角の変更のみで表現できます。覚えておくといろんなものをくるくる回せて楽しいですよ:relaxed::star2:

ポイント2: 星から星に飛び移るアニメーション

今回のゲームでは角丸の星を一周するごとに次の星に飛び移ってゲームが進んでいきます。
一見するとこれも複雑なアニメーションを計算しているように思えますが、実は簡単なCSS Transitionのみで実現しています。

コードより前に動画を見てみると仕掛けがわかります。

ezgif-6-ba6528371c82.gif

そう、実はたまさんは星から星に飛び移っていたのではなく、土台の長方形が移動していただけ(たまさんはその場でジャンプしていただけ)だったのです。
わかってしまえば簡単ですね。

コードで見てみるとこんな感じ:

テンプレート部分
<!-- たまさんの土台(中にたまさん本体もいる) -->
<TamaHome
  :tamaX="tamaHomeState.tamaX / 100"
  :groundPos="activePlanet.pos"
  :groundSize="activePlanet.size"
  :groundRound="activePlanet.round"
/>

<!-- 惑星1 -->
<Planet
  :round="planet1State.round"
  :pos="planet1State.pos"
  :size="planet1State.size"
/>

<!-- 惑星2 -->
<Planet
  :round="planet2State.round"
  :pos="planet2State.pos"
  :size="planet2State.size"
/>

2つの惑星とたまさん(の土台。中にたまさん本体も入ってる)が並列に並んでいます。
たまさんの土台は、activePlanet(後述)の位置・角度・サイズに合わせていることに注目してください。

スクリプト部分も見てみます:

スクリプト部分
setup () {
  /** たまさん(土台)の状態 */
  const tamaHomeState = reactive<TamaHomeState>({
    tamaX: 0,
    planetIndex: 0 // 乗っている惑星のindex
  })

  const planet1State = /* 略:惑星1の位置・角度・サイズ */
  const planet1State = /* 略:惑星2の位置・角度・サイズ */

  /** planetIndexの値によって惑星1か惑星2のどちらかを返す */
  const activePlanet = computed<PlanetState>(() =>
    tamaHomeState.planetIndex % 2 === 0 ? planet1State : planet2State
  )
  // ...略
}

たまさんの土台の位置を決めるactivePlanetはcomputedを使って惑星1か惑星2のどちらかを返すようにしています。ボタンを押すたびにこのactivePlanetが切り替わることで、隣の星に飛び移る(かのような)アニメーションを表現することができるのです。

おまけ:パフォーマンス戦略

先ほどの動画の右側にVueのプロパティを表示してみました。

ezgif-6-83547f826881.gif

位置やサイズの値が書き変わるのはボタンをクリックした瞬間の一度だけなのがわかるかと思います。Tweenライブラリを使ったり、一コマごとに座標を計算してプロパティを変更すればより柔軟なアニメーションを作れますが、その分パフォーマンスは大きく低下します。CSS Transitionで実現できる部分はできるだけまかせて、JavaScript側の処理を減らしてあげると滑らかなアニメーションを実現できます。1

ポイント3:アニメーションの途中で現在の位置や角度をどうやって取得するか

上記したように、アニメーションをTweenやコマ計算ではなく、できるだけCSS Transitionに任せていくのがパフォーマンス向上の重要な戦略です。その一方で途中のアニメーションを全てCSSに任せてしまうとゲームとしては困ったこと:cat::sweat_drops: もでてきます。

今回の場合、

「タップした瞬間にカツオをタップした方に向け、メザシを発射する」
Pasted Graphic 8.png

という部分。
これを実現するにはアニメーションの途中であっても、タップしたその瞬間の位置・角度を取得する必要があります。

image.png

しかし残念なことに、CSS Transitionで変化している途中のプロパティを直接取得する方法がありません:cry:。 0.5秒後にDOMから直接style.transformを取得してもトランジション終了後の値であるtransitionX(500px) rotate(30deg)しか取得できないのです。

任意の時点の位置を取得する

まずは位置からです。
位置の取得は実は去年、当たり判定の処理を作る中でもやっています。具体的にはElement.getBoundingClientRect()を使ってピューポート上での位置を求めればOK。

TamaSan.vue
const getTamaPos = (): Pos | null => {
  // テンプレート内のDOMを取得
  // ※Vue2のthis.$refs('tamaBody')と同じ
  // この部分は後ろの節でも解説しています
  const tamaBody = tamaBody.value // div要素
  if (!tamaBody) { return null }
  const p = tamaBody.getBoundingClientRect()
  return new Pos(p.x + p.width / 2, p.y + p.height / 2, 0)
}

クリックされた時点でたまさんの本体が入っている要素の表示領域(BoundingClientRect)を取得し、その中心を現在の位置として返しています。

角度も取得する

先ほどのgetTamaPosメソットでは角度を0で返してしまっていましたが、カツオをタップした方に向けるためには、今たまさんがどっちを向いているのかを知る必要があります。角度も取得するようにしましょう。

あいにく、Element.getBoundingClientRectでは位置を知ることはできても角度はわかりません。これは、getBoundingClientRectがあくまで画面描画において要素がどこに描画されるかを求める機能しか持たないためです。位置のみを使って角度を求めるため、たまさんのなかに2つの小さなdivを置き、この2つの位置関係から角度を求めることにします。

こんな感じでたまさんの中に2つのDiv要素を配置します

Pasted Graphic 9.png

TamaSan.vue
<div class="pos-detector">
  <div class="detector-top" ref="detTop"></div>
  <div class="detector-bottom" ref="detBottom"></div>
</div>

2つDivを作って

TamaSan.vue
<style lang="scss" scoped>
.pos-detector {
  position: absolute;
  width: 0px;
  height: 100px;
  top: calc(50% - 50px);
  left: 50%;
  div {
    position: absolute;
    width: 1px;
    height: 1px;
  }
  .detector-top {
    top: 0;
  }
  .detector-bottom {
    bottom: 0;
  }
}

たまさん中央に縦に並べるだけ。
あとはこの2つのDivの位置から角度を計算します。先ほどのgetTamaPosメソッドに角度を求める処理を追加します。

TamaSan.vue
const detTop = ref<HTMLDivElement>(null) // Vue2の this.$refs.detTop の宣言
const detBottom = ref<HTMLDivElement>(null) // 同上
const getTamaPos = (): Pos | null => {
  const elTop = detTop.value
  const elBtm = detBottom.value
  if (!elTop || !elBtm) { return null }
  const pTop = elTop.getBoundingClientRect() // 上側の位置を取得
  const pBtm = elBtm.getBoundingClientRect() // 下側の位置を取得
  const cx = (pTop.x + pBtm.x) / 2 // 中心X
  const cy = (pTop.y + pBtm.y) / 2 // 中心Y
  const rad2ang = (rad: number) => rad / Math.PI * 180 // ラジアン→角度の変換関数
  const r = rad2ang(Math.atan2((pBtm.y - pTop.y), (pBtm.x - pTop.x))) // Math.atan2で角度を求める
  return new Pos(cx, cy, r)
}

「2点の座標がわかれば回転角を簡単に求められる」というのは覚えておいて損のない知識かと思います。CSSアニメーションの文脈で使うことは滅多にないと思いますが、ゲームやビジュアル表現ではよく使う計算です。

ポイント4:TypeScript + CompositionAPIの採用

:angel:この節はコードばっかりなので興味ない方は飛ばしつつ見てくださいませ:angel:

冒頭でも書いた通り、今回はもうすぐやってくるVue3を見据えて、CompositionAPI + TypeScriptの構成に挑戦しています。CompositionAPI + TypeScriptで何が変わるの?って部分は以前の記事を見てみてください。従来の書き方との対応がわかりやすいかと思います。

Vue.jsレベルを上げよう!○×ゲームを作ってTypeScript&Vue3のCompositionAPIと仲良くなる

ここでは、基本のCompositionAPI + TypeScriptは理解した上で、つまづきポイントと解決策を共有します。

$refs(テンプレートRef)どこいった問題

テンプレートRefは以下のようにしてtemplate部分で指定した要素や子コンポーネントを参照する機能です。

Vue2標準のテンプレートRef
<template>
  <div>
     <button @click="getSpan">Get ref</button>
     <span ref="msg">Hello</span>
  </div>
</template>

<script>
export default {
  methods: {
    // this.$refsでテンプレート内のSpan要素を取得できる
    getSpan () { console.log(this.$refs('msg')) }
  }
}
</script>

CompositionAPIではrefを使います。名前は似てるけど使い方はだいぶん違うので注意

CompositionAPIのテンプレートRef
<template>
  <!-- 同じなので省略 -->
</template>

<script lang="ts">
import { createComponent, ref } from '@vue/composition-api'
export default createComponent({
  setup () {
    const msg = ref() // 中身のないrefを作る。※重要なのは名前※
    // msg.valueでテンプレート内の要素にアクセスできるようになる
    const getSpan = () => { console.log(msg.value) }
    return {
      msg,
      getSpan
    }
  }
})
</script>

紛らわしいのが、このrefは基本的には従来の$dataを代替するものなのに、なぜかテンプレートRefの機能も兼ねているところ。RFCの解説にRefの説明はあるのですが、これを読んでもいまいちテンプレートRefについては理解できないのでは?という気がします....

テンプレートRefの型を決めたい(HTMLElement編)

なんとかテンプレートの要素にアクセスできたところで、次に問題なるのはTypeScriptの型問題です。このままだとmsg.valueのようにして取得した要素をspanとして扱えないのでちょっと嫌ですよね。

前項までのはなしは一応公式にサンプルも書かれているのですが、これ、JSですね...
https://vue-composition-api-rfc.netlify.com/api.html#template-refs

TSでのやり方がなぜか見つからないのですが、一応、下記のようにすれば型を明示することができます。

テンプレートRefの型を明示する
<script lang="ts">
    ...  ...
    const msg = ref<HTMLSpanElement>() // 型を明示してrefを作る
    const getSpan = () => {
      const msgSpan = msg.value // HTMLSpanElementとして取得できる
    }
    ...  ...
</script>

テンプレートRefで子コンポーネントにアクセスしたいんだってば:angry:

OK、普通のSpanやDivならなんとかなった。じゃあコンポーネントだと?
↓これでいけそうな気がするじゃないですか?

子コンポーネントにアクセス(ダメな例)
<template>
  <div>
    <TamaSan ref="tamaRef" /><!-- このたまさんにアクセスしたい -->
  </div>
</template>

<script lang="ts">
import { createComponent, ref } from '@vue/composition-api'
import TamaSan from './TamaSan.vue' // たまさんコンポーネント読み込み

export default createComponent({
  components: { TamaSan },
  setup () {
    const tamaRef = ref<TamaSan>() // TamaSan型
    return { tamaRef }
  }
})
</script>

怒られます:anger: 。「TamaSanは型ではなく値なので、型を指定しろ」とのお言葉。以下のようにするとうまくいきます:

子コンポーネントにアクセス(うまくいく例1)
const tamaRef = ref<InstanceType<typeof TamaSan>>()

TS分かんね:innocent:ってなるやつですね。
TS初心者の私はこれ見つけるまでにStackOverflowを2時間くらいさまよいました。(そしてリンク失念しました...ごめんなさい:innocent:

また、コンポーネント固有のデータやメソッドは不要で、単にVueのコンポーネントとして扱いたいだけであれば、以下のようにすることもできます:

子コンポーネントにアクセス(うまくいく例2)
const tamaRef = ref<Vue>()

(ちなみにこの書き方だと従来の$refs$elにアクセスすることもできます)

うん、複雑。。
しかもこのあたりの型は別に自動的に判別してくれているわけではなく、あくまでも宣言に従って型を当てはめてくれているにすぎません。ちゃんと宣言すればエディタ上での作業は快適になりますが、宣言を誤ればそのまま実行時エラーなので、あまり安全とは言えない気がします。

このあたりはまだまだVue + TypeScriptの辛いところだなぁ...というのが正直な感想です。。

まとめ

そんなわけで今年も気合いで新ゲームをリリースすることができました:sob:
去年一年ことあるごとにVueでゲーム作るの楽しいよ!!!って言い続けてるのですが、イマイチまだ流れが来ていない気がします。

:relaxed: もっとみんなVueで遊ぼう :relaxed:

この記事では駆け足で流してしまった部分も、過去にいくつか解説している記事があるので、よろしければご参照くださいませ:


  1. 特にスペックの低い旧機種のiPhoneではこの恩恵が大きく出ます。今回のゲームの場合、iPhone6レベルでも一度アニメーションを開始してしまえばコマ落ちをほぼ感じずにプレイすることができます。このあたりは以前の記事will-changeで目指す60fpsのぬるぬるCSSアニメーションをご参照くださいませ。 

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

AndroidをBLEペリフェラルにしよう

AndroidのAPIを使ってアプリを作成してスマホをBLEペリフェラルにして、PCのブラウザからBLEでつないでみます。
Androidのスマホには、豊富なハードウェアやネットワーク機能がありますので、これがあれば、同じWiFiアクセスポイントにつながなくても、PCのブラウザからBLEでつないでみることができます。

Android

(i) BLEアダプタの取得

        mBleManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        mBleAdapter = mBleManager.getAdapter();

(ii) GATTサーバの作成

        mBtGattServer = mBleManager.openGattServer(this, mGattServerCallback);

mGattServerCallback は、BLEセントラルからの接続が完了したときや、ATT Read/Write が来た時の処理を実装するためのコールバックです。後で説明します。

(iii) GATT構造の作成

PrimaryServiceの下に、3つのキャラクタリスティックを配置します。

・Write用
・Read用
・Notification用

    /* GATT構造(PrimaryService) */
        btGattService = new BluetoothGattService(UUID_LIFF_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY);

    /* GATT構造(Characteristic) */
        mBtCharacteristic1 = new BluetoothGattCharacteristic(UUID_LIFF_WRITE, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE);
        btGattService.addCharacteristic(mBtCharacteristic1);
        mBtCharacteristic2 = new BluetoothGattCharacteristic(UUID_LIFF_READ, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ);
        btGattService.addCharacteristic(mBtCharacteristic2);
        mNotifyCharacteristic = new BluetoothGattCharacteristic(UUID_LIFF_NOTIFY, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
        btGattService.addCharacteristic(mNotifyCharacteristic);
        BluetoothGattDescriptor dataDescriptor = new BluetoothGattDescriptor(UUID_LIFF_DESC, BluetoothGattDescriptor.PERMISSION_WRITE | BluetoothGattDescriptor.PERMISSION_READ);
        mNotifyCharacteristic.addDescriptor(dataDescriptor);
        mBtGattServer.addService(btGattService);

(iv) アドバタイジングデータの作成

PrimaryServiceのUUIDをサービスUUIDとしてアドバタイジングデータに含めています。そうすることで、BLEセントラル側からBLEペリフェラルをScanする際に検索対象を絞ることができます。
あと、BLEセントラルからわかりやすいように、デバイス名も含めています。

        AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder();
        dataBuilder.setIncludeTxPowerLevel(true);
        dataBuilder.addServiceUuid(ParcelUuid.fromString(UUID_LIFF_SERVICE_STR));

        AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder();
        settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED);
        settingsBuilder.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM);
        settingsBuilder.setTimeout(0);
        settingsBuilder.setConnectable(true);

        AdvertiseData.Builder respBuilder = new AdvertiseData.Builder();
        respBuilder.setIncludeDeviceName(true);

(v) アドバタイズの開始

        mBtAdvertiser.startAdvertising(settingsBuilder.build(), dataBuilder.build(), respBuilder.build(), new AdvertiseCallback(){
            @Override
            public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                Log.d("bleperi", "onStartSuccess");
            }
            @Override
            public void onStartFailure(int errorCode) {
                Log.d("bleperi", "onStartFailure");
                handler.sendTextMessage("BLEを開始できませんでした。");
            }
        });

(vi) コールバック関数

BLEの状態が変わったり、BLEセントラルからのリクエストを受け取った時に呼び出される関数を実装しています。
ここが一番重要なところです。

    private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {

主なものとして以下があります。

・public void onMtuChanged (BluetoothDevice device, int mtu){

 クライアントからの要求により、MTUサイズが変更されたときに呼び出されます。MTUサイズが必要な場合はここで取得した値を覚えておくようにします。
 ブラウザのWeb Bluetooth APIからは、512バイトに拡大されました。NordicのnRF Connect for Mobileは拡大されず20バイトずつの送受信となりました。クライアント(BLEセントラル)によって違うようです。

・public void onConnectionStateChange(android.bluetooth.BluetoothDevice device, int status, int newState) {

 BLEセントラルと接続されたり切断されたときに呼び出されます。ですが、最新のAndroidOSバージョンでは、誰も接続していないはずなのに、接続状態となります。謎です。

・public void onCharacteristicReadRequest(android.bluetooth.BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {

 BLEセントラルから、ATT Readが呼び出されたときに、呼び出されます。要は、読み出し要求です。
 内部のバッファに、受信しておいたデータを返してあげましょう。
 ポイントは以下の部分です。

        if( offset > charValue.length ) {
            mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
        }else {
            byte[] value = new byte[charValue.length - offset];
            System.arraycopy(charValue, offset, value, 0, value.length);
            mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
        }

BLEセントラルとBLEペリフェラルの間での1回のBLEパケットの長さは、MTUサイズです。この長さを超えて送受信するには、複数のパケットを投げる必要があります。
offsetは、どこを起点として読み出すかをBLEセントラル側が指定します。BLEセントラル側は、BLEペリフェラルから受信したデータ長がMTUサイズ以下か、エラーが返ってくるまで、offsetを進めながらATT Readを繰り返し呼び出します。それに耐えられるような実装にしたつもりです。

・public void onCharacteristicWriteRequest(android.bluetooth.BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {

 BLEセントラルから、ATT Writeが呼び出されたときに、呼び出されます。要は、書き込み要求です。
 内部のバッファに、受信データを保存しましょう。BLEセントラル側からoffsetを使って分割してATT Writeされる場合があるので、offsetが内部保持可能なバッファサイズを超えたらエラーを返しています。

        if(offset < charValue.length ) {
            int len = value.length;
            if( (offset + len ) > charValue.length)
                len = charValue.length - offset;
            System.arraycopy(value, 0, charValue, offset, len);
            mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
        }else {
            mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
        }

・public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {

 主に、CCCD(Client Characteristic Configuration Descriptor)の操作に使います。
 CCCDは2バイトです。受信データの1バイト目が1だったら、Notificationが有効で、0だったら無効ですので、Notification送信時に確認するようにします。

・public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {

 主に、CCCD(Client Characteristic Configuration Descriptor)の値の読み出しに使います。

(vii) その他

  • 各種UUIDを使っていますが、適当に払い出したものなので、UUIDは各自払い出してください。
  • Manifestには以下の許可が必要だと思います。
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

ちなみに、画面はこんな感じ。

image.png

クライアント側(ブラウザ)からつなぐ

動作確認用に、Webページを作ってみました。
HTML5に、Web Bluetooth APIがあり、ChromeからBLEペリフェラルと通信することができるので、それを使いました。
Windows10で確認しましたが、iMacやAndroidでも動くはずです。

image.png

まずは、「接続」ボタンを押下します。

image.png

そうすると、設定したPrimaryServiceのUUIDを持ったサービスがリストアップされます。
いづれかを選択して、「ペア設定」を押下すると、BLE接続処理が走ります。Notificationも有効にしています。
接続が完了すると、connected のところが、 true になっているかと思います。

あとは、write valueのテキストエリアところに、16進数文字列を指定して、「Write」ボタンを押下すると、ATT Writeが実行され、「Read」ボタンを押下すると、ATT Readが実行されます。
ちなみに、write valueの先頭1バイトが0xFFの場合には、Notificationが発行されるようにAndroid側を実装しています。
ATT Readで受信したデータはread valueのテキストエリアに、Notificationで受信したデータはnotify valueのテキストエリアに表示するようにしています。

HTMLファイルです。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src * 'unsafe-inline'; media-src *; img-src * data: content:;">
    <meta name="format-detection" content="telephone=no">
    <meta name="msapplication-tap-highlight" content="no">
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <title>BLE Peripheral Test</title>

  <script src="js/methods_utils.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="dist/js/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>BLE Peripheral Test</h1>
        <label>connected</label> {{ble_connected}}<br>
        <label>deviceName</label> {{ble_devicename}}<br>
        <button class="btn btn-default" v-on:click="ble_connect">接続</button>
        <br><br>
        <label>write value</label>
        <textarea type="string" rows="5" class="form-control" v-model="ble_write_value"></textarea>
        <button class="btn btn-default" v-on:click="ble_write">Write</button>
        <br><br>
        <button class="btn btn-default" v-on:click="ble_read">Read</button>
        <br>
        <label>read value</label>
        <textarea type="string" rows="5" class="form-control" v-model="ble_read_value" readonly></textarea>
        <br>
        <label>notify value</label>
        <textarea type="string" rows="5" class="form-control" v-model="ble_notify_value" readonly></textarea>



        <div class="modal fade" id="progress">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">{{progress_title}}</h4>
                    </div>
                    <div class="modal-body">
                        <center><progress max="100" /></center>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="js/start.js"></script>
</body>
</html>

Javascriptです。
UUIDは、Androidで払い出したUUIDに合わせてください。

start.js
'use strict';

//var vConsole = new VConsole();

const UUID_ANDROID_SERVICE = 'a9d158bb-9007-4fe3-b5d2-d3696a3eb067';
const UUID_ANDROID_WRITE = '52dc2801-7e98-4fc2-908a-66161b5959b0';
const UUID_ANDROID_READ = '52dc2802-7e98-4fc2-908a-66161b5959b0';
const UUID_ANDROID_NOTIFY = '52dc2803-7e98-4fc2-908a-66161b5959b0';

const ANDROID_WAIT = 200;

var bluetoothDevice = null;
var characteristics = new Map();

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

        ble_connected: false,
        ble_devicename: '', 
        ble_write_value: '',
        ble_read_value: '',
        ble_notify_value: '',
    },
    computed: {
    },
    methods: {
        ble_connect: function(){
            return this.requestDevice(UUID_ANDROID_SERVICE)
            .then( (name) => {
                this.progress_open();
                this.ble_devicename = name;
                return bluetoothDevice.gatt.connect()
                .then(server => {
                    console.log('Execute : getPrimaryService');
                    return wait_async(ANDROID_WAIT)
                    .then(() =>{
                        return server.getPrimaryService(UUID_ANDROID_SERVICE);
                    });
                })
                .then(service => {
                    console.log('Execute : getCharacteristic');
                    characteristics.clear();
                    return Promise.all([
                        this.setCharacteristic(service, UUID_ANDROID_WRITE),
                        this.setCharacteristic(service, UUID_ANDROID_READ),
                        this.setCharacteristic(service, UUID_ANDROID_NOTIFY)
                    ]);
                })
                .then(values => {
                    return wait_async(ANDROID_WAIT)
                    .then(() =>{
                        return this.startNotify(UUID_ANDROID_NOTIFY);
                    });
                })
                .then(() =>{
                    this.ble_connected = true;
                    console.log('ble_connect done');
                    return bluetoothDevice.name;
                })
                .catch(error =>{
                    alert(error);
                })
                .finally(() => {
                    this.progress_close();
                });
            })
            .catch(error =>{
                alert(error);
            });
        },
        ble_read: function(){
            return this.readChar(UUID_ANDROID_READ);
        },
        ble_write: function(){
            return this.writeChar(UUID_ANDROID_WRITE, hexs2bytes(this.ble_write_value, ''));
        },
        requestDevice: function(service_uuid){
            console.log('Execute : requestDevice');

            return navigator.bluetooth.requestDevice({
                filters: [{services:[ service_uuid ]}]
        //      acceptAllDevices: true,
        //      optionalServices: [service_uuid]
                }
            )
            .then(device => {
                console.log("requestDevice OK");
                characteristics.clear();
                bluetoothDevice = device;
                bluetoothDevice.addEventListener('gattserverdisconnected', this.onDisconnect);
                return bluetoothDevice.name;
            });
        },
        setCharacteristic: function(service, characteristicUuid) {
            console.log('Execute : setCharacteristic : ' + characteristicUuid);

            return wait_async(ANDROID_WAIT)
            .then(() => {
                return service.getCharacteristic(characteristicUuid);
            })
            .then( (characteristic) =>{
                characteristics.set(characteristicUuid, characteristic);
                characteristic.addEventListener('characteristicvaluechanged', this.onDataChanged);
                return service;
            });
        },
        onDisconnect: function(event){
            console.log('onDisconnect');
            characteristics.clear();
            this.ble_connected = false;
        },
        onDataChanged: function(event){
            console.log('onDataChanged');

            let characteristic = event.target;
            let packet = uint8array_to_array(characteristic.value);
            if( characteristic.uuid == UUID_ANDROID_READ ){
                this.ble_read_value = bytes2hexs(packet, '');
            }else if( characteristic.uuid == UUID_ANDROID_NOTIFY ){
                this.ble_notify_value = bytes2hexs(packet, '');
            }
        },    
        startNotify: function(uuid) {
            if( characteristics.get(uuid) === undefined )
                throw "Not Connected";

            console.log('Execute : startNotifications');
            return characteristics.get(uuid).startNotifications();
        },
        stopNotify: function(uuid){
            if( characteristics.get(uuid) === undefined )
                throw "Not Connected";

            console.log('Execute : stopNotifications');
            return characteristics.get(uuid).stopNotifications();
        },
        writeChar: function(uuid, array_value) {
            if( characteristics.get(uuid) === undefined )
                throw "Not Connected";

            console.log('Execute : writeValue');
            let data = Uint8Array.from(array_value);
            return characteristics.get(uuid).writeValue(data);
        },
        readChar: function(uuid){
            if( characteristics.get(uuid) === undefined )
                throw "Not Connected";

            console.log('Execute : readValue');
            return characteristics.get(uuid).readValue((dataView) =>{
                console.log(dataView);
            });
        }
    },
    created: function(){
        proc_load();

        var ary = [];
        for( var i = 0 ; i < 500 ; i++ )
            ary[i] = i & 0xff;
        this.ble_write_value = bytes2hexs(ary, '');
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );

function hexs2bytes(hexs, sep = ' ') {
    hexs = hexs.trim(hexs);
    if( sep == '' )
    {
        hexs = hexs.replace(/ /g, "");
        var array = [];
        for( var i = 0 ; i < hexs.length / 2 ; i++)
            array[i] = parseInt(hexs.substr(i * 2, 2), 16);
        return array;
    }else{
        return hexs.split(sep).map(function(h) { return parseInt(h, 16) });
    }
}

function bytes2hexs(bytes, sep = ' ') {
    return bytes.map(function(b) { var s = b.toString(16); return b < 0x10 ? '0'+s : s; }).join(sep).toUpperCase();
}

function uint8array_to_array(array)
{
    var result = new Array(array.byteLength);
    var i;
    for( i = 0 ; i < array.byteLength ; i++ )
        result[i] = array.getUint8(i);

    return result;
}

function wait_async(timeout){
    return new Promise((resolve, reject) =>{
        setTimeout(resolve, timeout);
    });
}

足りないソースファイルは以下にあります。

 https://github.com/poruruba/swagger_template/tree/master/public

主要な関数について解説します。

〇ble_connect
 BLEデバイスに接続し、PrimaryServiceやCharacteristicを検索します。また、Notificationも有効化します。
 HTML5のWeb Bluetooth APIを使っているため、本Webページは、HTTPSでホスティングされている必要があります。
 (随所にウェイトをいれていますが、なくても大丈夫かもしれません。)

〇ble_write
 ATT Writeの関数です。
 MTUサイズに合わせて、勝手に分割送信してくれているようです。ですが、最大でも512バイトだそうです。

〇ble_read
 ATT Readの関数です。ただし、読み出したデータは、 onDataChanged がコールバックされて取得できます。

〇onDataChanged
 ATT Readしたときに読みだしが完了したときと、Notificationを受信したときに呼び出されます。

参考情報

・Android Deveopers Reference
https://developer.android.com/reference/android/bluetooth/package-summary
https://developer.android.com/reference/android/bluetooth/le/package-summary

・Web Bluetoot
https://tkybpp.github.io/web-bluetooth-jp/

(参考) Android側のソースコードです。長いです。

MainActivity.java
package com.example.test.bleperi.bleperipheraltest;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.Context;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;
import java.util.UUID;
import static com.example.test.bleperi.bleperipheraltest.UIHandler.MSG_ID_OBJ_BASE;
import static com.example.test.bleperi.bleperipheraltest.UIHandler.MSG_ID_TEXT;

public class MainActivity extends Activity implements UIHandler.Callback {
    UIHandler handler;
    BluetoothManager mBleManager;
    BluetoothAdapter mBleAdapter;
    BluetoothLeAdvertiser mBtAdvertiser;
    BluetoothGattCharacteristic mPsdiCharacteristic;
    BluetoothGattCharacteristic mBtCharacteristic1;
    BluetoothGattCharacteristic mBtCharacteristic2;
    BluetoothGattCharacteristic mNotifyCharacteristic;
    BluetoothGattService btPsdiService;
    BluetoothGattService btGattService;
    BluetoothGattServer mBtGattServer;
    boolean mIsConnected = false;
    BluetoothDevice mConnectedDevice;

//    private static final short WIRELESS_BLE_MAX_L2CAP_SIZE = 23;

    private static final UUID UUID_LIFF_PSDI_SERVICE = UUID.fromString("e625601e-9e55-4597-a598-76018a0d293d");
    private static final UUID UUID_LIFF_PSDI = UUID.fromString("26e2b12b-85f0-4f3f-9fdd-91d114270e6e");
    private static final String UUID_LIFF_SERVICE_STR = "a9d158bb-9007-4fe3-b5d2-d3696a3eb067";

    private static final UUID UUID_LIFF_SERVICE = UUID.fromString(UUID_LIFF_SERVICE_STR);
    private static final UUID UUID_LIFF_WRITE = UUID.fromString("52dc2801-7e98-4fc2-908a-66161b5959b0");
    private static final UUID UUID_LIFF_READ = UUID.fromString("52dc2802-7e98-4fc2-908a-66161b5959b0");
    private static final UUID UUID_LIFF_NOTIFY = UUID.fromString("52dc2803-7e98-4fc2-908a-66161b5959b0");
    private static final UUID UUID_LIFF_DESC = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

    private static final int UUID_LIFF_VALUE_SIZE = 500;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        handler = new UIHandler(this);

        mBleManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        mBleAdapter = mBleManager.getAdapter();

        if(mBleAdapter != null){
            prepareBle();
        }
    }

    @Override
    public boolean handleMessage(Message message) {
        switch (message.what) {
            case MSG_ID_TEXT: {
                TextView txt;
                txt = (TextView) findViewById(R.id.txt_message);
                txt.setText((String) message.obj);
                return true;
            }
            case MSG_ID_OBJ_BASE: {
                if( message.arg1 == 1 ){
                    TextView txt;
                    txt = (TextView) findViewById(message.arg2);
                    txt.setText((String)message.obj);
                }
                break;
            }
        }
        return false;
    }

    private void prepareBle(){
        mBtAdvertiser = mBleAdapter.getBluetoothLeAdvertiser();
        if( mBtAdvertiser == null ){
            Toast.makeText(this, "BLE Peripheralモードが使用できません。", Toast.LENGTH_SHORT).show();
            return;
        }

        mBtGattServer = mBleManager.openGattServer(this, mGattServerCallback);

        btPsdiService = new BluetoothGattService(UUID_LIFF_PSDI_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY);
        mPsdiCharacteristic = new BluetoothGattCharacteristic(UUID_LIFF_PSDI, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ);
        btPsdiService.addCharacteristic(mPsdiCharacteristic);
        mBtGattServer.addService(btPsdiService);

        try { Thread.sleep(200); }catch(Exception ex){}

        btGattService = new BluetoothGattService(UUID_LIFF_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY);

        mBtCharacteristic1 = new BluetoothGattCharacteristic(UUID_LIFF_WRITE, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE);
        btGattService.addCharacteristic(mBtCharacteristic1);
        mBtCharacteristic2 = new BluetoothGattCharacteristic(UUID_LIFF_READ, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ);
        btGattService.addCharacteristic(mBtCharacteristic2);
        mNotifyCharacteristic = new BluetoothGattCharacteristic(UUID_LIFF_NOTIFY, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
        btGattService.addCharacteristic(mNotifyCharacteristic);
        BluetoothGattDescriptor dataDescriptor = new BluetoothGattDescriptor(UUID_LIFF_DESC, BluetoothGattDescriptor.PERMISSION_WRITE | BluetoothGattDescriptor.PERMISSION_READ);
        mNotifyCharacteristic.addDescriptor(dataDescriptor);
        mBtGattServer.addService(btGattService);

        try { Thread.sleep(200); }catch(Exception ex){}

        startBleAdvertising();
    }

    private void startBleAdvertising(){
        AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder();
        dataBuilder.setIncludeTxPowerLevel(true);
        dataBuilder.addServiceUuid(ParcelUuid.fromString(UUID_LIFF_SERVICE_STR));

        AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder();
        settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED);
        settingsBuilder.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM);
        settingsBuilder.setTimeout(0);
        settingsBuilder.setConnectable(true);

        AdvertiseData.Builder respBuilder = new AdvertiseData.Builder();
        respBuilder.setIncludeDeviceName(true);

        mBtAdvertiser.startAdvertising(settingsBuilder.build(), dataBuilder.build(), respBuilder.build(), new AdvertiseCallback(){
            @Override
            public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                Log.d("bleperi", "onStartSuccess");
            }
            @Override
            public void onStartFailure(int errorCode) {
                Log.d("bleperi", "onStartFailure");
                handler.sendTextMessage("BLEを開始できませんでした。");
            }
        });
    }

    private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
        private byte[] psdiValue = new byte[8];
        private byte[] notifyDescValue = new byte[2];
        private byte[] charValue = new byte[UUID_LIFF_VALUE_SIZE]; /* max 512 */

        @Override
        public void onMtuChanged (BluetoothDevice device, int mtu){
            Log.d("bleperi", "onMtuChanged(" + mtu + ")");
        }

        @Override
        public void onConnectionStateChange(android.bluetooth.BluetoothDevice device, int status, int newState) {
            Log.d("bleperi", "onConnectionStateChange");

            if(newState == BluetoothProfile.STATE_CONNECTED){
                mConnectedDevice = device;
                mIsConnected = true;
                Log.d("bleperi", "STATE_CONNECTED:" + device.toString());
                handler.sendTextMessage("接続されました。");
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_target_address, device.getAddress());
            }
            else{
                mIsConnected = false;
                Log.d("bleperi", "Unknown STATE:" + newState);
                handler.sendTextMessage("切断されました。");
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_target_address, "");
            }
        }

        public void onCharacteristicReadRequest(android.bluetooth.BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            Log.d("bleperi", "onCharacteristicReadRequest");

            if( characteristic.getUuid().compareTo(UUID_LIFF_PSDI) == 0) {
                mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, psdiValue);
            }else if( characteristic.getUuid().compareTo(UUID_LIFF_READ) == 0){
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_access, "Read");
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_offset, Integer.toString(offset));
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_length, "");
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_value, "");

                if( offset > charValue.length ) {
                    mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
                }else {
                    byte[] value = new byte[charValue.length - offset];
                    System.arraycopy(charValue, offset, value, 0, value.length);
                    mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
                }
            }else{
                mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null );
            }
        }

        public void onCharacteristicWriteRequest(android.bluetooth.BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            Log.d("bleperi", "onCharacteristicWriteRequest");

            if( characteristic.getUuid().compareTo(UUID_LIFF_WRITE) == 0 ){
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_access, "Write");
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_offset, Integer.toString(offset));
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_length, Integer.toString(value.length));
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_value, MainActivity.toHexString(value));

                if(offset < charValue.length ) {
                    int len = value.length;
                    if( (offset + len ) > charValue.length)
                        len = charValue.length - offset;
                    System.arraycopy(value, 0, charValue, offset, len);
                    mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
                }else {
                    mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
                }

                if( (notifyDescValue[0] & 0x01) != 0x00 ) {
                    if (offset == 0 && value[0] == (byte) 0xff) {
                        mNotifyCharacteristic.setValue(charValue);
                        mBtGattServer.notifyCharacteristicChanged(mConnectedDevice, mNotifyCharacteristic, false);
                        handler.sendTextMessage("Notificationしました。");
                    }
                }
            }else{
                mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
            }
        }

        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
            Log.d("bleperi", "onDescriptorReadRequest");

            if( descriptor.getUuid().compareTo(UUID_LIFF_DESC) == 0 ) {
                mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, notifyDescValue);
            }
        }

        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            Log.d("bleperi", "onDescriptorWriteRequest");

            if( descriptor.getUuid().compareTo(UUID_LIFF_DESC) == 0 ) {
                notifyDescValue[0] = value[0];
                notifyDescValue[1] = value[1];

                mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
            }
        }
    };

    public static String toHexString(byte[] data) {
        StringBuffer sb = new StringBuffer();
        for (byte b : data) {
            String s = Integer.toHexString(0xff & b);
            if (s.length() == 1)
                sb.append("0");
            sb.append(s);
        }
        return sb.toString();
    }
}

あとがき

投稿したはいいけど、読者には結構BLEの知識が必要かも。。。
実験したいことがあればお知らせください。

以上

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

気になるページに関するtweetを1クリックで検索するChrome Extensionを作ってみた

Web上で面白い記事を見つけたとき、twitterで他に誰かつぶやいてないかなー、どんなことつぶやいてるかなーと気になることありませんか?
twitterの検索機能にURLをブッ込めば検索できるんですが、それも手間なんでChrome拡張(Chrome Extension)を作ってみました。

完成品

test.gif
こんな感じにワンクリックでそのページのリンクが貼ってあるtweetを検索したページに遷移する。

ソースコードはGitHubにあります。

作るよ

Chrome extensionの基本の作り方はいろいろな方の記事を参考にさせていただきました。

このあたりのサンプルありの記事を参考にサンプルアプリを言われた通りに作ることからはじめました。
で、なんとなくの感覚を掴んだら実際に今回作りたかったchrome extensionの開発を開始!

manifest.jsonを書く

chrome extensionのベースの設定ファイルmanifest.jsonを作ります。

manifest.json
{
  "name": "Search tweets about this page",
  "description": "You can search tweets about this page only one click.",
  "manifest_version": 2,
  "version": "1.0",
  "browser_action": {},
  "background": {
    "scripts": [ "jquery.min.js", "scripts.js" ],
    "persistent": false
  },
  "permissions": [
    "tabs"
  ]
}

書き方は「Chrome 拡張機能のマニフェストファイルの書き方 - Qiita」にきれいにまとめられています。

大切なところは以下。

  • background.scriptsjquery.min.jsscripts.jsを読み込んでる
  • permissionstabsを指定してchrome extensionが実行されたときのタブの情報を取得できるようにしてる

jquery.min.jsをダウンロード

scripts.jsでjQueryを使うためにjquery.min.js公式サイトからダウンロードしておきます。
image.png
なんかまぁこんな感じでリンクがあるので「Download the compressed, production」のやつを選びましょう。
そうするとえげつない文字列のページにとばされると思うので全文コピーしてjquery.min.jsって名前で保存します。

image.png

scripts.jsを書く

で、これがほぼ実態となるscripts.jsを記述していきましょう!

まずちょっと戦略を。
今回、「今閲覧しているサイトのURLをTwitterの検索窓で検索する」、ということをしようとしています。
Twitterの検索窓の検索は、実はURLパラメーターを使ってやっているだけなんですよね(参考: Using standard search — Twitter Developers
参考の公式サイトにも書いてある通り、例えば「twitterdev new premium」を検索したい時は、https://twitter.com/search?q=twitterdev%20new%20premiumにアクセスすればいいだけとなります。%20はスペース(" ")をURLエンコードした値なのでURLエンコードが必要なことがわかりますね。

なので、今回は今閲覧しているサイトのURLをhttps://xxxxxxxxxx.com/だとした場合、それをURLエンコードした値をhttp://twitter.com/search?q=の後に連結したURLを別タブで開かせればいいことになります。

この戦略でコーディングをすると、scripts.jsを以下のように記述することができます。

scripts.js
function search_tweets () {
  chrome.tabs.getSelected(tab => {
    target_url = "https://twitter.com/search?q=" + encodeURIComponent(tab.url)
    window.open(target_url, '_blank')
  })
}

$(function() {
  chrome.browserAction.onClicked.addListener(search_tweets)
})

超少量の記述である。

ちょっと解説。

$(function() {
  chrome.browserAction.onClicked.addListener(search_tweets)
})

最後の3行、この部分でchrome extensionのアイコンがクリックされたことをトリガーにしてsearch_tweetsファンクションを実行します。

function search_tweets () {
  chrome.tabs.getSelected(tab => {
    target_url = "https://twitter.com/search?q=" + encodeURIComponent(tab.url)
    window.open(target_url, '_blank')
  })
}

それで呼び出されるsearch_tweetsファンクションがこちらですね。

chrome.tabs.getSelected(tab => {
  ...
})

で現在開いているタブの情報を変数tabから取得できるようにしています。(参考: [ChromeExtension] 表示しているタブのURLを取得するサンプル | Tips of Rubbish

target_url = "https://twitter.com/search?q=" + encodeURIComponent(tab.url)

ここでtwitter検索のURLを生成しています。
大切なのは、encodeURIComponent(tab.url)の部分。
chrome.tabs.getSelected(tab)のおかげでtab.urlで今閲覧しているページのURLを取得できるようになっています。
encodeURIComponentは文字列をURLエンコードしてくれる便利な関数です。(参考: escape と encodeURI と encodeURIComponent を正しく使い分ける

これでtarget_urlにアクセスすれば、今閲覧しているページを検索キーワードとしてtweetを検索できるようになるので最後に、window.open(target_url, '_blank')で別タブでそのURLにアクセスさせています。(参考: jQueryを使って遷移先リンクを別タブで開く - Qiita

クエリパラメーターにWeb解析用のパラメーターがあるとうまく挙動しない

ここまでのコードでchrome://extensions/でコードを読み込んでやればやりたいことができるようになったんです。
ただちょっと落とし穴で、今って例えばGoogle analyticsで流入元を調査するためだったりでクエリパラメーターにいろいろなコードがついているリンクって多いんですよね。
例えばhttps://xxxxxxxxxx.com?magazine_no=xxxxxxxxxxみたいな。
この場合そのページ自体はhttps://xxxxxxxxxx.comまでで十分で、tweetでもこっちでつぶやかれているのでhttps://xxxxxxxxxx.com?magazine_no=xxxxxxxxxxで検索してもヒットしないってことがあります。

なので、クエリパラメーター以降を削除するようにscripts.jsを書き換えました。
クエリパラメーターは?以降に記述するルールなのでtab.urlの文字列を?で分割して、前半部分だけをtarget_urlに連結します。

scripts.js
function search_tweets () {
  chrome.tabs.getSelected(tab => {
    base_url = tab.url.split("?")[0]
    target_url = "https://twitter.com/search?q=" + encodeURIComponent(base_url)
    window.open(target_url, '_blank')
  })
}

$(function() {
  chrome.browserAction.onClicked.addListener(search_tweets)
})

変更点は

base_url = tab.url.split("?")[0]
target_url = "https://twitter.com/search?q=" + encodeURIComponent(base_url)

この2行です。
まず、tab.url.split("?")[0]で今開いているページのURLの?より前のURLをbase_urlに代入しています。(参考: jQueryで文字列を分割したり、特定の文字以降を削除する | フロントエンド | kigiroku
そして、encodeURIComponentの対象をtab.urlからbase_urlに変更することで無駄なクエリパラメーターを除外してtweet検索ができるようになりました!

更なる落とし穴、クエリパラメーターでページを指定してる場合もある

これで行けるかなとも思ったんですけどね...
サイトの中にはhttps://xxxxxxxxxx.com?id=xxみたいな形式でクエリパラメーターで表示するコンテンツを指定しているサイトもありました...
あれ、これはもうどっちかを救えばどっちかを救えないパターン...

と悩んだんですが、Twitterの検索機能にはOR検索があったので、もうクエリパラメーターあり版もなし版も両方検索してしまおうという結論に至りました。
TwitterのOR検索は「キーワードA OR キーワードB」って形で複数のキーワードの間にORを入れることでどちらかにヒットするtweetを検索してくれます。

ということで、最終版のscripts.jsがこちらです。

scripts.js
function search_tweets () {
  chrome.tabs.getSelected(tab => {
    base_url = tab.url.split("?")[0]
    target_url = "https://twitter.com/search?q=" + encodeURIComponent(base_url) + "%20OR%20" + encodeURIComponent(tab.url)
    window.open(target_url, '_blank')
  })
}

$(function() {
  chrome.browserAction.onClicked.addListener(search_tweets)
})

変更点はtarget_urlのところで、+ "%20OR%20" + encodeURIComponent(tab.url)を追記しています。
%20は先ほども出てきましたがスペースのURLエンコードされた文字列です。ので最終的には「クエリパラメーターを削除したURLをエンコードした文字列 OR クエリパラメーターを含んだURLをエンコードした文字列」を検索キーワードにしていることになります。

完成!!!

chrome extensionに追加する

これはGitHubのREADMEにもちょっと書きましたが、

  1. Chromeで chrome://extensions にアクセスする(拡張機能ページにいきます)
  2. デベロッパーモードをONにする(ページ右上部にあるはず)
  3. 「パッケージ化されていない拡張機能を読み込む」をクリックしてソースコードをおいているディレクトリを選択する

で完了です!

まとめ

初めてのchrome extensionでしたが、コード量の少なさに驚きを隠しきれませんでした。
今回はやっていることも簡単なものなのでというのはありますが、大きくつまずくこともなく行けた感じがする。

成果物については、まだid=xxxmagazine_no=xxxxxが混在していたらどうするの?みたいな課題があったりするんですが、chrome extensionでやってみよう!が今回の目的だったりしたのでまぁいいです。笑

エゴサや他の人がどんなコメントしてるか気になるページに出会ったときに、つかってこうと思います♪
成果物 => at946/search_tweets_about_this_page_chrome_extension: You can search tweets about this page only one click.

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

JavaScriptで素因数分解アプリを作る

こんなやつ

Image from Gyazo

動作デモ → https://jsfiddle.net/vw98komL/

自分流素因数分解のアルゴリズム

前提

そもそも素因数分解とは、「自然数を素数の掛け算の形にすること」です。

例: 120 → 2 * 2 * 2 * 3 * 5 → 2^3 * 3 * 5

単純に約数を並べる訳ではないので、一つの数字が複数回でてくる事もあります。

本題

前提を踏まえて考えると、まず対象の自然数(valueと置く)の約数を見つける必要があります。

①約数はループ処理を用いて2から順にvalueに対して%演算子で剰余が0のもの(divisorと置く)。
②次に指数は、こちらもループ処理を用いて、valuedivisorで剰余が0である間は除算し続け、その中でカウンターを増やしていき求める。

この流れで1つ目の素因数が見つかります。

以降はこれの繰り返しで、前の処理で除算されたvalueに対してまた①で約数を見つけて②でその指数を求めます。
そしてdivisorvalueと等しくなる(value / divisor === 1)までそれを繰り返せば素因数分解は完了です。

コード

HTML
<input id="prime-factorization" type="number">
<p>結果:<span id="result"></span></p>
JavaScript
const box = document.querySelector('#prime-factorization');
const result = document.querySelector('#result');

const checkDivisors = () => {
  let value = box.value;
  let exponent = 0;
  let divisors = [];

  for(let number = 2; number <= value; number++) {
    //前述の①の処理
    if(value % number === 0) {
      //前の約数の分のカウントをリセットする
      exponent = 0;
      //同様に②の処理
      while(value % number === 0) {
        //valueをnumberで除算していき、ループごとにカウントを一つ増やして累乗の指数を求める
        exponent++;
        value /= number;
      }
      //一つ目の約数と指数を配列にpush
      divisors.push({number, exponent});
    }
    //この流れを(value / number === 1)になるまで繰り返す
  }

  //連続して使用する時のために一度中身を空にする
  box.value = null;
  result.textContent = null;

  //上記のループ処理で求めた素因数が格納された配列を展開
  divisors.forEach((divisor, index) => {
    if(index === divisors.length - 1) {
      //最後はスペースと*が要らない
      result.textContent += `${divisor.number}^${divisor.exponent}`;
      return;
    }
    result.textContent += `${divisor.number}^${divisor.exponent}\n*\n`;
  })
}

box.addEventListener('keydown', e => {
  if(e.key === 'Enter') {
    checkDivisors();
  }
})

出来ました。(*゚▽゚ノノ゙☆パチパチパチ

Image from Gyazo

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

VueでFirebaseを使いログイン認証が必要な専用ページを作ろう

はじめに

今回はVueとVue RouterとFirebaseを用いてログイン認証後の必要ページを作ってみましょう!
前準備の部分が長くなるので、認証後の処理だけを見たい人はこ↓こ↑をクリック

環境

  • Vue.js 2(CLI)
  • Vue Router
  • Firebase
  • Yarn

下準備

まずは新しいプロジェクト作り

4be703b086ad849f77fce01da0fdfc7a.png
マニュアルで作りましたが、Routerが必要なのでインストールされてあれば他のをインストールしていても問題ありません。
この記事では、説明のために最小構成でプロジェクトを作成します。
b5cda66b7fe426d4c00d0d9c9affc9f8.png
次ですが、ここはhistoryモードを使うのでyで。
655d71fb30622b2d19fca7d2eef1fe27.png
この後も数回、環境の設定をどうするか聞かれますが、今回のRoutingには影響しないので好みの設定で大丈夫です。
5197b02c266ef40242a00b41e11d373c.png
無事に作成が完了したら、一度yarn serveを行い、バニラ状態のページを確認しておきましょう!
a34062308900b9e39a4610b3c333a8ef.png

Firebaseをインストール

次にFirebase CLIをインストールしましょう。
Firebase CLI 公式ドキュメント

npm

npmの場合ですと、公式ドキュメントにある通り、

グローバル

npm install -g firebase-tools

ローカル

npm install -D firebase-tools

yarn

yarnを使用している例は公式にはありませんが、いつもの方法でインストールすることは可能です。

グローバル

yarn global add firebase-tools

ローカル

yarn add firebase-tools

正しくインストールされてるか確認

最後に正しくインストールされてるか確認しましょう。

firebase -V

と入力して、バージョン表記が表示されれば正しくインストールされています!
(※2020年2月29日現在では、7.14.0が最新)

Firebaseの設定をする

インストールが無事終えたら、次はインストールしたプロジェクトのFirebaseの設定をします。

Firebase init

このコマンドを打ち込みます。
すると、英語で「初期化を行う手続きを続行してもいいかー?」(意訳)の問が出現し、
その後に、Firebaseのどの機能を使用するか選択する問が現れます。
abbda024bb2d5a0d8345252559651297.png

今回は認証だけをやりたいのですが、少なくとも1つは機能を選択しなければいけないため、Hostingを選択しました。

次にFirebaseのどのプロジェクトを使用するか?を聞かれます。
930f375809aeb1f7a759796d9674947c.png

1つ目の選択肢はすでにFirebase上に存在する既存プロジェクトを使用する
2つ目は新しくプロジェクトを作成する
となっています。
もしプロジェクトを作っていなかったら2を、既存のを使用するなら1を選択しましょう。

この後、選択した機能毎をどう設定するかの問が行われ、
その問に答えた後に、SPA(シングルページアプリケーション)として設定するかの問が行われます。
SPAを使用したいので、これにはyと入力します。

a8f960e01b79f035f9c9ccbe0d5962df.png
Firebaseの初期化の操作が終わり、このように表示されたら無事完了です!

Firebase ConsoleからAPI情報を作成

先にAPI情報などを記載するfirebase.jsを作成しましょう。
srcフォルダ配下に作成します。

firebase.js
import firebase from "firebase";
import "firebase/auth";

const config = {
  apiKey: /**/,
  authDomain: /**/,
  databaseURL: /**/,
  projectId: /**/,
  storageBucket: /**/,
  messagingSenderId: /**/,
  appId: /**/,
  measurementId: /**/
};

firebase.initializeApp(config);

export default firebase;

configの中の情報は、Firebase Consoleのプロジェクト設定から取得してきましょう。

firebase/authをImportすることで、Firebaseの認証機能を使えるようにImportしています。
これらは必ず行うようにしましょう。

これで下準備は完了です!
次から実際に作成していきましょう!

必要なコンポーネントを作ろう

ルーティングや認証を行う処理などを作る前に、先に必要なコンポーネントを作ります。
必要なコンポーネントは、

  • 登録(sing up)
  • ログイン(sing in)
  • ログアウト(sing out)
  • 認証専用ページ

の4つになるので、これらを作っていきましょう。
サイトトップは初期作成で作られたコンポーネントをそのまま使ってしまいましょう。

コンポーネントを作成

先程必要になる4つのコンポーネントを作っていきましょう。

  • 登録(sing up)
  • ログイン(sing in)
  • ログアウト(sing out)

の3つはコンポーネントは、Firebaseとの通信が必要になるため、
先ほど作っておいたAPI情報を含め初期化しておいたfirebase.jsをImportしておきましょう。
そうしないと、Firebaseの機能を使うことができません。

登録(sing up)

singup.vue
<template>
  <div>
    <h1>SING UP</h1>
    <div>
      <h3>E-mail</h3>
      <input type="text" placeholder="E-mail" v-model="email" />
    </div>
    <div>
      <h3>Password</h3>
      <input type="text" placeholder="Password" v-model="password" />
    </div>
    <button @click="createUserAccount">Sing UP!!</button>
  </div>
</template>

<script>
import firebase from "../firebase.js";
export default {
  name: "singup",
  data() {
    return {
      email: "",
      password: ""
    };
  }
};
</script>

ログイン(sing in)

singin.vue
<template>
  <div>
    <h1>SING IN</h1>
    <div>
      <h3>E-mail</h3>
      <input type="text" placeholder="E-mail" v-model="email" />
    </div>
    <div>
      <h3>Password</h3>
      <input type="text" placeholder="Password" v-model="password" />
    </div>
    <button @click="userSingIn">Sing in Now!!</button>
  </div>
</template>

<script>
import firebase from "../firebase.js";
export default {
  name: "singin",
  data() {
    return {
      email: "",
      password: ""
    };
  }
};
</script>

ログアウト(sing out)

singout.vue
<template>
  <div>
    <h1>SING OUT</h1>
    <button @click="singout">Sing out Now!!</button>
  </div>
</template>

<script>
import firebase from "../firebase.js";
export default {
  name: "singout"
};
</script>

認証専用ページ

mypage.vue
<template>
  <div>
    <h1>SING OUT</h1>
    <p>My Page!!<p>
  </div>
</template>

<script>
export default {
  name: "mypage"
};
</script>

ルーティング

次に作成したコンポーネントを合わせてルートレコードを設定していきましょう

ルートファイルを設定

router.js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
/*ここから新規追加*/
import Singup from "../components/singup.vue";
import Singin from "../components/singin.vue";
import Singout from "../components/singout.vue";
import Mypage from "../components/mypage.vue";
/*ここまで*/

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "home",
    component: Home
  },
  {
    path: "/about",
    name: "About",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue")
  },
  /*ここから新規追加*/
  {
    path: "/singup",
    name: "singup",
    component: Singup
  },
  {
    path: "/singin",
    name: "singin",
    component: Singin
  },
  {
    path: "/singout",
    name: "singout",
    component: Singout
  },
  {
    path: "/mypage",
    name: "mypage",
    component: Mypage
  }
  /*ここまで*/
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

export default router;

先程作成したコンポーネントをrouter.jsに追加し、ルーティング設定しました。
次に、App.vueの中に作ったコンポーネントへ遷移するためのリンクを設定しましょう。

App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      /*ここから新規追加*/
      <router-link to="/singup">Sing up</router-link> |
      <router-link to="/singin">Sing in</router-link> |
      <router-link to="/singout">Sing out</router-link> |
      <router-link to="/mypage">Mypage</router-link> |
      /*ここまで*/
    </div>
    <router-view />
  </div>
</template>

ここまでのを確認

yarn serveを行い、一旦ここまでの作成段階のを確認しましょう。
e0668afb493addf7c04efaa1a61ce9c7.png
Sing upからMypageまでのリンクを押し、無事ページが遷移したら成功です。

Firebaseを使い認証

FirebaseでE-mailとPasswordで登録できるように設定しよう

認証などの処理を行う前に、Firebase Console画面で、Authenticationの設定を行いましょう。

Authenticationのログイン方法で、メール/パスワードを有効化します。
8ff1cf5ec77c4e7b587ca1546b1a069e.png

02d64008938d5b32fbc3ef791a6c133f.png

メールリンクは今回は有効化しなくてもOKです。
これでメール/パスワードを用いてユーザー登録を行う準備が完了しました。

Firebaseにアカウントを登録しよう

先程作ったSingupコンポーネントに、ユーザーアカウントを追加する処理を組み込みましょう。
Firebaseのユーザーを管理する

singup.vue
<script>
  methods: {
    createUserAccount() {
      firebase
        .auth()
        .createUserWithEmailAndPassword(this.email, this.password)
        .then(() => {
          alert("Create Account");
        })
        .catch(error => {
          alert("Error!", error.message);
          console.error("Account Regeister Error", error.message);
        });
    }
  }
</script>

E-mailPasswordを用いてのよくあるユーザー登録を行うには、
firebase.authcreateUserWithEmailAndPasswordメソッドを使用します。
第一引数にE-mail、第二引数にPasswordを渡してあげると、あとはよしなにやってくれます。
非同期で行うため、結果はthenチェーン内で行う必要があります。
あとは、このメソッドをButtonコンポーネントより呼び出せば登録処理部分は完了です。

ログイン処理をしよう

次はログインを行う処理を追加するために、作っておいたSing inコンポーネントに処理を書き込んでいきましょう。

singin.vue
<script>
  methods: {
    userSingIn() {
      firebase
        .auth()
        .signInWithEmailAndPassword(this.email, this.password)
        .then(() => {
          alert("ログイン成功!");
          this.$router.push("/mypage");
        });
    }
  }
</script>

firebase.authsignInWithEmailAndPasswordメソッドを使い、ログイン処理を行います。
ユーザー登録時と同じ様に、引数にそろぞれE-mailPasswordを渡ししてあげればOKです。
無事ログイン処理が行えたら、thenチェーンで内でRouterpushメソッドを使い、ページを遷移させています。

お試しにログインしてみる

先程作ったユーザ登録画面とログイン画面で、ユーザー登録を行えログインできるか試してみましょう。
ユーザ登録画面で登録が成功とalertが表示され、Firebase Consoleの認証関連のページで、ユーザーが登録されているはずです。
b1a67ca936b76a15b9d4cc8d3fbc95cc.png

ログイン画面で、登録したE-mailPasswordでログインができ、画面がMypageに遷移したらログイン画面も無事作成できています。

ログアウト処理も作ってみる

ログインページが完了したので、次はログアウトページも作っておきましょう。

singout.vue
<script>
  methods: {
    singout() {
      firebase
        .auth()
        .signOut()
        .then(() => {
          alert("Logout!");
        })
        .catch(error => {
          alert(error);
        });
    }
  }
</script>

firebase.authsignOutメソッドを使いログアウト処理を行っています。
ログアウト処理はこれだけで完了です。
ログアウト後にページトップに戻るような動きをつけたい場合には、
ログイン処理時に使った、

this.$router.push();

を使い、pushの引数の中に遷移させたい先のルート名を渡しましょう。

ページにログイン認証を組み込もう

ログイン/ログアウトできるようになりましたが、現状ではログイン時のみしか表示させたくないMypageがログアウト時でも見ることができています。
次は、このMypageをログイン時のみ表示するようにしましょう。
(ようやくここに来て本題に到達)

認証が必要なことの状態をもたせよう

まず、router.jsに認証処理を追加していきましょう。

router.js
  {
    path: "/mypage",
    name: "mypage",
    component: Mypage,
    meta: { requiresAuth: true }
  }

まずは、mypageのルーティング部分にmetaフィールドを追加します。
Vue Router - ルートメタフィールド
metaフィールドに{ requiresAuth: true }のオブジェクトをもたせて、このルートは認証が必要であることの状態をもたせます。
ただし、これだけでは状態を持っているだけなので、ナビゲーションガード、未ログインユーザーがこのページにアクセスしてもページが表示されてしまいます。
別途、ルートガードの処理を作る必要があります。

ナビゲーションガードを実装しよう

Vue Router - ナビゲーションガード
公式にある、router.beforeEachを用いナビゲーションガードを実装してみましょう。
``

router.js
const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

router.beforeEach((to, from, next) => {
  const requiresAuth = to.matched.some(recode => recode.meta.requiresAuth);
  if (requiresAuth) {
    next({ path: "/singin", query: { redirect: to.fullPath } });
  } else {
    next();
  }
});

export default router;

router.beforeEachexport default routerの前に追加しました。
to.matched.some(recode => recode.meta.requiresAuth)で、requiresAuthの状態をもつルートレコードなのかを確認しています。
もし、requiresAuthならnextを用いてsinginへ、そうでないならユーザーが選択したルートレコードへ遷移します。
ただし、この状態ですとログインしているかどうかを確認していないので、mypageへアクセスすると誰でもログインページへ戻ってしまいます。
次は、Firebaseからログイン状態を取得してみましょう。

Firebaseからログイン状態を取得

router.beforeEachの中にログイン状態を取得する方法もありますが、
今回はfirebase.jsに取得処理を書き、処理を分離化させる方法でやってみましょう。
firebase.jsの中に書いていきましょう。

firebase.js
firebase.getCurrentUser = () => {
  return new Promise((resolve, reject) => {
    const unsubscribe = firebase.auth().onAuthStateChanged(user => {
      unsubscribe()
      resolve(user);
    }, reject);
  });
};

Firebase のユーザーを管理する
onAuthStateChangedを用い、ユーザーがログイン状態かを確認できます。
そして、このメソッドがログイン状態になったら即時にその情報を返します。

最後に先程のrouter.beforeEachにこのログイン状態かを確認する処理を追加しましょう。

router.js
const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

router.beforeEach(async (to, from, next) => {
  const requiresAuth = to.matched.some(recode => recode.meta.requiresAuth);
  if (requiresAuth && !(await firebase.getCurrentUser())) {
    next({ path: "/singin", query: { redirect: to.fullPath } });
  } else {
    next();
  }
});

export default router;

await firebase.getCurrentUser()は先程firebase.jsに作成したメソッドとなります。
firebase.getCurrentUser()Promiseの非同期メソッドになるので、
router.beforeEach内の関数にasyncをつけ、これも非同期メソッドとして上げる必要があります。
そして、if文の条件にfirebase.getCurrentUser()を追加しますがこれは!をつけ否定の条件にしておきます。

if (requiresAuth && !(await firebase.getCurrentUser())

そして、2つをand条件にすることで
- 認証が必要なコンポーネント
- ログイン状態でではない

を満たす場合のみに、ログインページへ遷移するようにしました。

最後に無事動作するか確認

最後に、yarn serveを行い、ログインを行ってみてページ遷移するか、
ログアウト状態でMypageへアクセスした場合に、ログイン画面へ戻るかを確認してみましょう。
無事動けば完成です!

おわり

長くなりましたが、今回はFirebaseを用いてVueでのログイン認証が必要なページの作り方を記事にしてみました。
Vueの認証が必要なページの作り方は、Firebaseを使わなくてもrouter.beforeEachの中で処理するのは共通になるので、一度覚えておけば他の認証サービスやAPIを使った場合でも応用は楽にできると思います。

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

《FE編》数式を使って質問できるQ&Aサービス「el-pot」をローンチしました

はじめに

現在法政大学の4年のKATUOと申します。大学2年生のときに授業で習ったC言語がきっかけでプログラミングを初め、ここまで複数のIT企業でインターン生の枠組みでエンジニアの業務を行ってきました。大学生の間に個人サービスをリリースするのが目標で、既に社会人として働くインターン同期の友人と2人で週末開発を進め、何とかローンチまで漕ぎ着けました。その報告をこの場をお借りしてしたいと思います。

開発したサービス

電気電子工学の知識を共有できる「el-pot」と呼ばれるサービスを開発しました。

Screen Shot 2020-03-01 at 16.22.39.png

このサービスの特徴は数式(tex)を使って質問をすることができるという点です。いままで大学の授業でわからなかった箇所を調べてSEO上位に出てくるサイトはどれも数式対応しておらず次のような表記になっていた為、非常に文章が読みづらいという課題がありました。

[(x-1)/(x+1)]’=[1(x+1)-1(x-1)]/(x+1)^2=2/(x+1)^2

この式をもっとわかりやすく次のように表示させることができます。

Screen Shot 2020-03-01 at 16.37.05.png

このように数式を文章中に挿入できるようになったことで従来のQ/Aサービスよりもわかりやすく質問/回答を行うことができるプラットフォームを実現しました。

開発の話

私はフロントエンド、友人はバックエンド・インフラ周りを担当しました。今回の記事ではフロントエンドについて簡単にお話ししたいと思います。バックエンド・インフラについては別途記事を友人が書きますので興味のある方はそちらをご覧ください。(まだ公開してないです。すいません。)

vue

フロントエンドを作るに当たって採用したフレームワークは「Vue.js」です。

vuejs

選んた理由は普段の業務で使っていて慣れているからという単純な理由です。では今回のサービス開発で使用した主要なVueライブラリ・アーキテクチャーを紹介します。

vuex

状態管理パッケージです。コンポーネント間のデータのやりとりを実装する為に採用しました。

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。 これは予測可能な方法によってのみ状態の変異を行うというルールを保証し、アプリケーション内の全てのコンポーネントのための集中型のストアとして機能します。 また Vue 公式の開発ツール拡張と連携し、設定なしでタイムトラベルデバッグやステートのスナップショットのエクスポートやインポートのような高度な機能を提供します。

vuetify

ゼロベースでコンポーネントのデザインを考えるは大変だった為、「Vuetify」というUIライブラリを採用しました。alert系やdialog系のコンポーネントが使い易かったです。

vuetify

Vuetifyは、美しく手作りされたマテリアルコンポーネントを備えたVue UIライブラリです。設計スキルは必要ありません。素晴らしいアプリケーションを作成するために必要なものはすべて手元にあります。

参照元:「vuetify 公式サイト」

mavon-editor

主要機能である数式エディタは「mavon-editor」というライブラリを使用しました。

mavon-editor

このライブラリは更新が2年前から開発がストップしているみたいです。issueなどがたくさん立っていますが、ほとんどスルーされています。なので個人的には使うことをお勧めしません。実際「el-pot.v2」では自作のライブラリを導入する予定です。(mavon-editor自体のカスタマイズが難しいという点)

atomic design

コンポーネントのディレクトリのアーキテクチャーを設計するにあたって「atomic design」を採用しました。atomic designとはコンポーネントの種類を「atoms」「molecules」「oraganisms」に分類して管理する方法です。

atomic-web-design

今後の予定

ひとまずリリースはしましたがまだまだサービスとしての質は低いです。インフラ面での可用性であったり、UI/UXの低さ、機能の少なさ。などなど数々の問題を抱えています。これらを改善する為、今後も開発を進めていく予定です。その中でも特に自分がやりたいのは「OSSの自作エディタのリリース」です。エンジニアである以上、OSSを世の中に出してみたいという気持ちがあります。なので今回自作できなかった数式エディタのOSSを作ろうと思っています。おそらくVue.jsを使って開発することになりそうです。

最後に

1日最低3回、Twitterで技術のことなどをつぶやいますのでよかったらフォローしてください。また、MAU1万程度のBlogもほぼ毎日更新していますのでこちらもよかったらご覧ください。

最後まで読んでくれてありがとうございました。

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

JavaScriptで音声ファイル確認&再生

const soundPlay = function(url) {
    // オブジェクト生成
    var soundObj = new Audio();

    soundObj.onerror = function() {
        // エラー時の処理
        alert("再生できませんでした");
    }
    // URL設定
    soundObj.src = url;
    // 再生(できないときはエラーに行く)
    soundObj.play();
}

使い方

soundPlay("assets/sound/temp.mp3");
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スライドショーを作りつつ文法のおさらいをする

はじめに

JavaScriptの基本文法はひと通りやったけども...この後どうするの?と悩む少し前の私へ。今までに習ったこの文法で、スライドショーができるよ!
この記事は、自動で切り替わるスライドショーをつくりながら使用文法を簡単におさらいしていく記事です。

おさらいする文法は、次の通り。

  • 要素の取得とカウント
  • 関数(即時関数)
  • 配列と要素の生成
  • for文
  • 関数(関数宣言)
  • if文
  • classの追加と削除
  • addEventListener
  • 三項演算子
  • タイマー処理
  • タイマー処理の解除

これからつくるモノ

See the Pen slider_fadeinout by Nozomi (@non_123) on CodePen.

仕様

⑴子要素数に合わせてインジケーターをJSで生成する
⑵スライド要素切り替わりの条件は、①指定した秒数が経過する、または、②インジケーターをクリックする
⑶スライド要素にホバーしている間は、自動切り替えが止まる

HTML/CSSのつくり

index.html
<ul id="js-slider_wrap">
  <li class="show">1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
</ul>
<ul id="js-slider_dot"></ul>

#js-slider_wrapの子要素、liをposition: absolute;で同じ位置に重ね、更にopacity: 0;で見えなくしています。class="show"が付いたli要素のみopacity: 1;となり、見えるようになる、という仕組みです。
ここからJSを使って『class="show"を順番に追加/削除することで表示される子要素が切り替える動き』をつくります。

JavaScriptを組み立てる

段階ごとに切り出して説明していきますが、先ほどのCodePenを別タブで開いて全体像も確認しながら見ていただく方が、わかりやすいかもです。

①必要な要素を取得する

index.js
(function(d) {
  //①要素の取得
  const slide = d.getElementById('js-slider_wrap');
  const slideitem = slide.getElementsByTagName('li');
  const num = slide.childElementCount;
})(document);

まずは操作するDOM要素と子要素数を取得します。
定数『num』には、slideの子要素数をカウントして格納しています。

おさらい : 要素の取得とカウント

//IDで要素を取得
document.getElementById("ID名");

//要素の種類から要素を取得
document.getElementsByTagName("要素名")[n番目の要素];

//子要素の数をカウント
document.childElementCount;

ID取得の際は、同じIDは存在しないため『Element』と単数形です。
一方Tag要素は複数存在できるので、『Elements』と複数形になります。[ ]内で、取得した要素のうちの何番目に処理を適用するのかを数字で書きます。カウントは0から始まるので、1番目の要素を指すときは[0]と書きます。

おさらい:関数(即時関数)

(function(仮引数1,仮引数2) {
 //処理
}(引数1,引数2);

これは使用しなくてもスライドショーはできますが、勉強なので使ってみました。
即時関数は、スクリプトが読み込まれたらすぐに実行される関数です。{}に処理を書くことでローカルスコープができるので、他箇所のスクリプトとバッティングしない便利な書き方です。
引数にdocumentを渡して仮引数に省略形を設定すれば、この後のコードもスッキリ書けます。

②インジケーターをつくる

仕様⑴子要素数に合わせてインジケーターをJSで生成する、の部分をつくります。

index.js
  //②インジケーターを作る
  const dotwrap = d.getElementById('js-slider_dot'); 
  const dot = dotwrap.getElementsByTagName('li');

  let li = [];
  for (let i = 0; i < num; i++) {
    li.push(d.createElement('li')); //li要素を生成して配列liに入れる
    dotwrap.appendChild(li[i]); //dotwrapの子要素に配列liの中身を追加する
  }
  dotwrap.firstElementChild.classList.add('active');

現在位置を示すインジケーターを、先ほど取得した子要素数numと同じだけJSで作ります。
dotwrapでインジケーターを作成する位置を取得しています。dotは次のfor文で生成したliを取得しています。この過程では使いませんが、インジケーターに関する要素なので、まとめてこの部分に記載しておきます。

おさらい : 配列と要素の生成

//空の配列を作成
var sampleArray = [];

//要素を生成
document.createElement('要素');

//要素を配列の最後に追加する
sampleArray.push(document.createElement('要素'));

配列は、複数の値を1つのデータにまとめて扱うことのできるデータ構造です。
配列自体のデータ型はオブジェクトで、中には数値、文字列、オブジェクトとどのデータ型でも格納できます。

おさらい : for文

for (初期化式; 条件式; 加算式) {
 //処理
}

条件式の値がtrueの間、処理→加算式の順に繰り返されます。
加算式でよく表記される「i++」の書き方は「インクリメント演算子」と呼ばれ、変数iの初期値に1追加した後の数値を変数iに再代入しています(= 変数iが+1ずつ増えていく)。
反対に1ずつ減らす場合は「デクリメント演算子」と呼ばれ、「i--」という書き方をします。


ここまでできたら、以下のように動きのないスライドができているかと思います。
ここまでの完成図

次からは、スライドの肝となる動きの部分、仕様⑵スライド要素切り替わりの条件は、①指定した秒数が経過する、または、②インジケーターをクリックする、をつくっていきます。

③index数を制御する関数を作る

スライドショー部分は『class="show"を順番に追加/削除することで表示される子要素が切り替える』仕組みです。この『何番目の要素にclass="show"を追加/削除するのか』の『何番目』の部分を③で作っていきます。

index.js
  //③index数を制御するfunctionを作る
  let cur_index = 0;  //番号の初期設定
  function count_index() {
    cur_index++; //functionが動くと番号が1ずつ追加される
    if (cur_index === num) { //番号が子要素数と同数になったら、
      cur_index = 0; //番号をリセット (= 0番目の要素に戻す)
    }
  }

『何番目』部分を変数cur_indexとして、初期値を0に設定しています。
関数count_indexが動くごとにcur_indexは1ずつ増え、もし子要素数と同数(= 最後の要素)になったら、cur_indexをリセットして0番目の要素に戻るようにします。

おさらい:関数(関数宣言)

//関数宣言
function 関数名(引数1, 引数2,...) {
 //処理
}

//関数の呼び出し
関数名(引数A, 引数B,...);

任意の関数名をつけて宣言する書き方です。先に出てきた即時関数と異なり、処理を動かすには関数の呼び出しが必要になります。
関数の呼び出し時に引数を渡した場合、その引数を関数の処理内で変数のように使うことができます。引数の数に制限はなく、また、無しでも大丈夫です。

おさらい:if文

if (条件式1) {
  //条件式1がtrueの時の処理
} else if (条件式2) {
  //条件式2がtrueの時の処理
} else {
  //条件式1、2以外の時の処理
}

条件によって処理を分岐することができます。
条件は上から順に判別されていくので条件式の順番にも注意が必要です。

④インジケーターのクリックで表示要素を切り替える関数をつくる

index.js
  //④インジケーターのクリックで表示要素を切り替え
  function click_dot() {
    for (let j = 0; j < num; j++) {
      dot[j].addEventListener('click', function() {
        dot[cur_index].classList.remove('active'); //cur_index番目のdot(li要素)から.activeを削除
        dot[j].classList.add('active'); //j番目のdot(li要素)に.activeを追加

        slideitem[cur_index].classList.remove('show'); //cur_index番目のslideitemから.activeを削除
        slideitem[j].classList.add('show');  //cur_index番目のslideitemに.activeを追加

        cur_index = j; //cur_indexをjと合わせる
      });
    }
  }

インジケーターがクリックされたら、インジケーターとスライド要素のクラスを操作して表示要素を切り替えています。cur_indexはこの後作成する⑤スライドの切り替え用関数でも現在位置を示すために利用しているので、インジケーターのクリックによってスライド要素が切り変わった場合には『cur_index = j』番目に上書きします。

おさらい:classの追加と削除

//classを追加
document.getElementsByClassName[0].classList.add('クラス名');

//classを削除
document.getElementsByClassName[0].classList.remove('クラス名');

おさらい:addEventListener

index.js
イベント対象の要素.addEventListener('イベントタイプ', function () {
 //処理
}, false);

addEventListenerは、「ページが読み込まれた時」「要素がクリックされた時」といったイベント処理を実行できるメゾットです。
イベント対象の要素には、イベントを発火させる対象の要素を書きます。
イベントタイプには、click、scrollといったイベントの種類を「" "」または「' '」で囲って書きます。指定したイベントタイプが、対象要素に対して起こると、処理が実行されます。
falseの部分ではイベントの伝播方式を指定できます。デフォルト値はfalseで、falseの場合は表記を省略することができます。

⑤スライド切り替え用関数をつくる

index.js
  function fade_infinite() {
    count_index(); //③で作成した関数でcount_indexの制御を呼び出す
    let rm_index = (cur_index == 0 ? num - 1 : cur_index - 1); //何番目の要素のクラスを削除するのかを設定

    slideitem[rm_index].classList.remove('show'); 
    slideitem[cur_index].classList.add('show');

    dot[rm_index].classList.remove('active');
    dot[cur_index].classList.add('active');

    click_dot(cur_index); //④で作成した関数でインジケータークリック時の処理を呼び出す
  }

何番目のスライド要素のクラスを削除するのかを、変数rm_indexで制御しています。
現在位置(cur_index)が0番目の時は最後の要素番号、0以外の時は現在位置の1つ前の要素番号が変数に入れられています。

おさらい:三項演算子

条件式 ? trueの場合の処理 : falseの場合の処理

条件式の結果で処理が切り替わる、if文の書き換えバージョンです。
あまり複雑な分岐だとコードが分かりにくくなりますが、今回のようにシンプルな条件分岐をしたい時には簡潔なコードにすることができます。

⑥スライド切り替えを自動的に行う関数をつくる

index.js
  //自動切り替えをスタートする関数
  const sec = 3000; //表示する時間
  let timer;
  function start_timer() {
    timer = setInterval(fade_infinite, sec);
  }
  start_timer(); //関数の呼び出し

タイマー処理のメゾットを使って、先ほど⑤で作成したスライド切り替え用関数を一定時間ごとに発動させる関数をつくっています。

おさらい:タイマー処理

//決められた時間間隔で繰り返し処理を実行する
setInterval(関数, 時間);

//決められた時間が経過したら1回だけ処理を実行する
setTimeout(関数,時間);

タイマー処理には2種類あり、今回は繰り返し実行したいのでsetIntervalを利用します。時間部分はミリ秒で表します。どちらもタイマーIDを返すようになっていて、

var timer = setInterval(関数, 時間);

のように、タイマーIDを変数に格納して使うことが多いです。


ここまでできたら、仕様の
⑴子要素数に合わせてインジケーターをJSで生成する
⑵スライド要素切り替わりの条件は、①指定した秒数が経過する、または、②インジケーターをクリックする
ができましたね...!最後に、
⑶スライド要素にホバーしている間は、自動切り替えが止まる
をつくりましょう。

⑦ホバーしている間は自動切り替えをストップする

index.js
  //タイマー処理をストップする関数
  function stop_timer() {
    clearInterval(timer);
  }

  for (let k = 0; k < num; k++) {
    slideitem[k].addEventListener('mouseover', () => { 
      stop_timer(); //スライド要素にホバーしたら、タイマー処理をストップ
    });
    slideitem[k].addEventListener('mouseleave', () => {
      start_timer(); //スライド要素からホバーが外れたら、タイマー処理をスタート
    });
  }

おさらい:タイマー処理の解除

//clearInterval()でセットしたタイマーを解除する
clearInterval(対象のタイマーID);

//setTimeout()でセットしたタイマーを解除する
clearTimeout(対象のタイマー処理ID);

setIntervalやsetTimeoutで返されたID(ここではtimer)を渡すことで、タイマー処理をストップすることができます。


お疲れさまでした!
これで、自動で切り替わるスライドの完成です。

おわりに

文章もコードも、スッキリ書くのは難しいですね...!

間違いや「ここはこの書き方がいいよ!」といったアドバイスがありましたら、お知らせいただけると嬉しいです。ここまで読んでいただきありがとうございました!

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

HTML/CSS/JSデモ

HTML/CSS/JSが動くか試してみよう!!

今回は、Visual Studio Codeでプロジェクトを入れるためのフォルダ内にそれぞれのファイルを作り、動くか試して行きたいと思います:relaxed:

今回の記事

  • HTMLでWebページの土台ファイルを作ろう!
  • CSSで見た目の調整をしよう!
  • リセットCSSを入れよう!
  • JavaScriptで動きを付けよう!
  • jQueryでJavaScriptを簡単に導入しよう!

HTMLでWebページの土台ファイルを作ろう!

HTMLとは「Hyper Text Markup Langage」の略で、簡単に言うとWebページの土台となるファイルを作成する言語です。
Webページに表示させたい文章などを「<」「>」で挟まれた「タグ」と呼ばれる文字列で囲んでいきます。
詳しい書き方については別記事にまとめてありますので、今回はHTMLがブラウザーで反映できるか試してみましょう。
※プロジェクトを入れるためのフォルダは作成済みとします。

プロジェクトを入れるためのフォルダの作り方はこの記事参照:point_down_tone2:
Visual Studio Codeの導入と使い方

HTMLのファイルを作ってみましょう!

新規ファイルを作成を選択
スクリーンショット 2020-03-01 11.15.46.png

ファイル名は「~.html」にします。
スクリーンショット 2020-03-01 11.17.40.png

HTMLファイルに試しに書き込んでいきます。
「!」を入れるとVisual Studio Codeでは自動でHTMLの型を作成してくれます。
image.png

ここまで作ってくれます:thumbsup_tone2:
image.png

titleタグの中身を変更したり、bodyタグの中にh1タグ(見出し)やpタグを追加してみます。
コメント 2020-03-01 115942.png

HTMLが反映されているか見てみましょう。
index.htmlファイルの上で右クリック→エクスプローラーで表示する
image.png

クロームのマークが入ったindex.htmlが表示されます。
コメント 2020-03-01 113427.png

クロームで表示されば成功です。
コメント 2020-03-01 113905.png

※うまくいかない場合はVisual Studio Codeでしっかり上書き保存しているか確認しましょう:bangbang:

これでHTMLファイルは反映されました。

CSSで見た目の調整をしよう!

次はCSSファイルを作り、反映させてみます。
CSSとは、「Cascading Style Sheets」の略で、見た目の装飾をするするための言語です。
つまり先ほど作成したHTMLの見た目を華やかに装飾できるのです。

先ほどのHTMLで作成したWebページは白い背景、黒い文字ですが、文字の色を変えたり、大きさを変えたり、レイアウトの変更ができます。

CSSをフォルダ作り、その中にCSSファイルを作成します。
コメント 2020-03-01 124308.png

ファイル名は「~.css」と付けます。
コメント 2020-03-01 124547.png

試しにh1タグ(見出し)の色を変えるコードをCSSファイルに入力します。
image.png

sample.css
h1{
    color: red;
}

しかしこれだけでは、CSSは反映されません。
HTMLファイルにCSSのファイルを読み込ませる必要があります。
コメント 2020-03-01 125523.png

index.html
<link rel="stylesheet" href="">

herfの中にCSSのファイルがある場所を書きます。

エクスプローラーから実行し、CSSが反映されていることを確認しましょう。
image.png

リセットCSSを入れよう

リセットCSSとは、ブラウザによって異なるデフォルトのCSSを打ち消してブラウザ間の表示を揃えるためのCSSファイルのことを言います。

リセットCSSを使用すれば、Google ChromeやSafari、Internet Explorerなど異なるブラウザを使っても同じようにWebサイトが表示されます。

リセットCSSは基本的にコピペでOK!:point_down_tone2:
2019年版!おすすめのリセットCSSまとめ

CSSフォルダの中にreset.cssファイルを作成し、コピーしてきたものを張ります。
コメント 2020-03-01 132812.png

先ほどと同じようにHTMLファイルに反映されておきましょう。
コメント 2020-03-01 133039.png

リセットCSSを反映させ、エクスプローラーで表示するとこんな感じになります。
image.png

このようにデフォルトのCSSを打ち消すのでh1の文字の大きさなども自分で設定することが可能です。
今回はリセットCSSを入れた結果どのような表示になるか見てみたのですが、本来はプロジェクトを作成した際に最初にリセットCSSを入れておくことをオススメします。

JavaScriptで動きを付けよう!

JavaScriptは簡単いうとWEBサイトに「動き」を加えるものです。
例えば、目立たせたいボタンやメニューにアニメーションや効果を追加したりできるので、ユーザーが使いやすいサイトを作ることが可能なのです。

jsフォルダーを作り、ファイル「~.js」を作成します。
image.png

HTMLでjsファイルを読み込みます。
コメント 2020-03-01 140602.png

index.html
 <script type="text/javascript" src=""></script>

srcの中にJSファイルがある場所を書きます。

わかりやすいようにHTML内にボタンを配置し、ボタンを押したら何か動きが出るようにしてみましょう。
コメント 2020-03-01 141417.png

ボタンを押したらアラートを出してみます。
image.png

sample.js
function btnClick() {
    alert("ボタンをクリックしましたね。");
}

HTMLのボタンタグの中にもこのように追加しましょう。

index.html
 <button  onclick="btnClick()">ボタン</button>

ブラウザで確認すると、JSが反映されています。
image.png

jQueryでJavaScriptを簡単に導入しよう!

jQueryは、簡単に言うとJavaScriptを使いやすいように拡張してくれているものです。
「JavaScriptのライブラリ」なので、中身事態はJavaScriptです。
余裕がある人はjQueryを導入してみましょう。

jQueryの読み込み
コメント 2020-03-01 144244.png

sample.js
 <script type="text/javascript"src="https://code.jquery.com/jquery-3.4.1.js"></script>

jQueryの公式サイトからのコピペがおすすめです:point_down_tone2:
https://code.jquery.com/

コメント 2020-03-01 144555.png

image.png

先ほどのJavaScriptのコードをjQueryに書き換えてみましょう。

sample.js
$(function() {
    $('button').click(function() {
    alert("jQuery:ボタンがクリック")
    });
  });

index.html
 <button>ボタン</button>

jQueryでの見込みに成功!
image.png

お疲れ様でした:smiley:
HTML/CSS/JSのざっくりとした説明と、読み込み方法でした。
今後は1つ1つじっくり勉強していきましょう:point_up_tone2:

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

node-canvasで絵文字を扱う

ユーザ生成コンテンツは、OGPを画像化して見せるのがデファクトスタンダードのようになってきました。
node.jsから、画像処理をcanvasのように扱える node-canvas を使う人も多いのではないでしょうか。

その方法は色々紹介されています。例えばこちらの記事など。
node.jsではてなブログ風アイキャッチ画像を動的に生成する

このnode-canvasですが、デフォルトでは絵文字は描画ができません。

今回は、node-canvasで、絵文字を Twemoji の見た目で描画するモジュールの紹介です。

node-canvasで絵文字を扱う

node module を npm で公開しておきました。

$ npm install --save node-canvas-with-emoji 
const { createCanvas } = require('canvas');
const { fillTextWithTwemoji } = require('node-canvas-with-twemoji');

async function main () {
    const canvas = createCanvas(500, 300);
    const context = canvas.getContext('2d');

    context.font = '30px Arial';
    await fillTextWithTwemoji(context, '絵文字も描画?', 140, 160);
}

main();

localhost.png

提供しているのは fillTextWithTwemoji(context, text, x, y, options?) という関数です。
contextを渡す以外は、本家の CanvasRenderingContext2D.fillText のように扱えるようにしています。

ただし、
fillTextWithTwemoji は非同期(Promise)で扱われるため、Promise や async/await も利用する必要があります。
(関数内で絵文字画像をダウンロードしているため)

また、現時点(2020.03.01)ではイタリック書体や、maxWidth引数などに対応していません。
こちらで開発しているので何かあれば。 GitHub
スターがつくだけでもやる気が出て実装するかもしれません。

雑記

Twemojiではなく、Apple Color Emojiなど、絵文字フォントを使う方法もあるようです。
- Any emoji support?
- Colored Emoji

ただし、こちらは著作権周りで、実際に使う場合は注意が必要です。
- 「絵文字」の利用で気をつけた方が良いこと、安全な使い方について

雑記2

同じようなこと考えてる人もいました。
- node-canvas でカラー絵文字対応について考えてたこと

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

依存OSSのライセンス一覧(JSON)生成ツールを作ってみた

はじめに

「Qiita - Angular CLIビルド時にできる利用OSSリスト(3rdpartylicenses.txt)をWebアプリ上に表示する」で自動生成される 3rdpartylicenses.txt を読み込んで画面に表示しました。
これは簡単にOSSのライセンスに準拠できるというメリットがあります。
ただ、テキストをそのまま表示するだけでしたので、自由なレイアウトができない、といった課題がありました。

そこでテキストではなく、JSONで出力する方法を調査してみました。
結果、、、ツールを自作したので、紹介します。

類似ツールの調査

まず、思い描いているツールや機能の存在を確認してみました。
私の検索能力だと2つ見つけられました。

license report tool

node_modules以下のpackage.jsonを解析してライセンスを含むパッケージ情報をリスト化してくれます。
実行した結果は、以下となります。

> npx license-report --output=json --only=prod
npx: 62個のパッケージを4.652秒でインストールしました。
[
    {
        "department": "kessler",
        "relatedTo": "stuff",
        "name": "@oclif/command",
        "licensePeriod": "perpetual",
        "material": "material",
        "licenseType": "MIT",
        "link": "https://github.com/oclif/command.git",
        "comment": "1.5.19"
    },
    ...
    {
        "department": "kessler",
        "relatedTo": "stuff",
        "name": "tslib",
        "licensePeriod": "perpetual",
        "material": "material",
        "licenseType": "Apache-2.0",
        "link": "git+https://github.com/Microsoft/tslib.git",
        "comment": "1.11.1"
    }
]

これでも十分ですが、勝手な主観で申し訳ないけど、、以下が気に入りません。

  • link要素に入るURLに"git+~"が付くことがある
  • 不要な情報をフィルタできない

NPM License checker

「ハイブリッドアプリのライセンス一覧を自動生成しよう」でも紹介されてますね。
使ってはいけないライセンスのOSSが含まれているか確認するために利用するみたいです。
以下の実行結果が出力されます。

>npx license-checker --direct --production --json
{
  "@oclif/command@1.5.19": {
    "licenses": "MIT",
    "repository": "https://github.com/oclif/command",
    "publisher": "Jeff Dickey @jdxcode",
    "path": "D:\\\\dependent-licenses\\src\\dependent-licenses\\node_modules\\@oclif\\command",
    "licenseFile": "D:\\\\dependent-licenses\\src\\dependent-licenses\\node_modules\\@oclif\\command\\LICENSE"
  },

  ...

  "wrappy@1.0.2": {
    "licenses": "ISC",
    "repository": "https://github.com/npm/wrappy",
    "publisher": "Isaac Z. Schlueter",
    "email": "i@izs.me",
    "url": "http://blog.izs.me/",
    "path": "D:\\\\dependent-licenses\\src\\dependent-licenses\\node_modules\\wrappy",
    "licenseFile": "D:\\\\dependent-licenses\\src\\dependent-licenses\\node_modules\\wrappy\\LICENSE"
  }
}

イイ感じですが、以下が気に入らない…、ごめんなさい。

  • 不要な情報をフィルタできない
  • 深い依存関係を含む全てのライセンスを表示してしまう。ライセンスの確認のためには必要だけど、表示するだけなら直接参照しているライブラリだけにしたいんです。。。

自作ツール紹介(dependent-licenses)

やはりほしい機能って、ピンポイントでは中々ないですね。
ですので、作ってみました。
とありあえず、license-checkerを利用したラッパーといった実装をしてみました。

ソースはGithubで公開しています。また、npmjsは以下となります。
とりあえず、動かすことを優先しているので、ドキュメント等や最適化はできていません。
本記事と同時並行でブラッシュアップしていこうかと考えています。

使い方

package.jsonのあるプロジェクトルート上で、以下を実行してください。
ここでは3rdpartylicenses.jsonに保存しています。

> npx dependent-licenses > 3rdpartylicenses.json

以下は、出力されたJSONファイルです。
必要最低限の情報となっています。
今後はオプション等でフォーマットや情報を追加していければと考えています。

3rdpartylicenses.json
{
    "@oclif/command@1.5.19": {
        "licenses": "MIT",
        "repository": "https://github.com/oclif/command"
    },
    "@oclif/config@1.14.0": {
        "licenses": "MIT",
        "repository": "https://github.com/oclif/config"
    },
    "@oclif/plugin-help@2.2.3": {
        "licenses": "MIT",
        "repository": "https://github.com/oclif/plugin-help"
    },
    "tslib@1.11.1": {
        "licenses": "Apache-2.0",
        "repository": "https://github.com/Microsoft/tslib"
    }
}

Angularでの利用例

生成したJSONを「Qiita - Angular CLIビルド時にできる利用OSSリスト(3rdpartylicenses.txt)をWebアプリ上に表示する」と同様に画面表示してみます。

@angular/cli インストール

適当な作業ディレクトリを作成し、作業ディレクトリ上で以下を実行してください。
1. npm install -g @angular/cli@latest

プロジェクト作成

新しいプロジェクトを作成します。
1. ng new display-license

Component作成

以下を実行すると、app/license/license.component.*ファイルが生成されます。クラスはLicenseComponentとなります。

commandline
cd display-license
ng g component license

ソース修正

各ソースを修正してください。

app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

import { AppComponent } from './app.component';
import { LicenseComponent } from './license/license.component';

@NgModule({
  declarations: [
    AppComponent,
    LicenseComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
app/app.component.html
<h2>Open Source Licenses</h2>
<div>
  <!-- LicenseComponentを挿入 -->
  <app-license></app-license>
</div>
app/license/license.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';
import { share, map } from 'rxjs/operators';

interface License {
  package: string;
  licenses: string;
  repository: string;
}

/**
 * 3rdpartylicenses.jsonライセンスを
 * 読み込んで表示するコンポーネント
 */
@Component({
  selector: 'app-license',
  templateUrl: './license.component.html',
  styleUrls: ['./license.component.scss']
})
export class LicenseComponent implements OnInit {
  /**
   * ライセンスを読み込むストリーム
   */
  public licenses$: Observable<License[]>;

  /**
   * コンストラクタ
   * @param http HttpClient
   */
  constructor(private http: HttpClient) { }

  /**
   * 初期化イベントハンドラ
   */
  ngOnInit(): void {
    // 3rdpartylicenses.jsonを読み込むストリームを生成
    const url = 'assets/3rdpartylicenses.json';

    this.licenses$ = this.http.get(url).pipe(
      share(),
      map(json => {
        const licenses: License[] = [];
        for (const key in json) {
          if (json.hasOwnProperty(key)) {
            const element = json[key];
            const license = {} as License;
            license.package = key;
            license.licenses = element.licenses;
            license.repository = element.repository;
            licenses.push(license);
          }
        }

        // console.log(licenses);
        return licenses;
      })
    );
  }
}
app/license/license.component.scss
th,td {
    border: solid 1px;
    padding: 5px;
}

table {
    border-collapse:  collapse;
}
app/license/license.component.html
<table>
    <tr>
        <td>package</td>
        <td>License</td>
        <td>repository</td>
    </tr>
    <tr *ngFor="let license of licenses$ | async">
        <td> {{ license.package }} </td>
        <td> {{ license.licenses }} </td>
        <td> {{ license.repository }} </td>
    </tr>
</table>

ビルド&実行

ng serve実行後、ブラウザでhttp://localhost:4200を開いてください。
こんなふうに出力されれば、成功です。各自CSS/htmlを修正して、デザインを整えてください。

license.png

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

GoogleStreetViewを足踏みで仮想散歩するPGM

JavaScriptからカメラを起動し、カメラの画素変化を検知し、その変化率に応じて、前進・右回り・左回りを判定し、それに応じてGoogleStreetViewの画像を変化させるPGMを作成しました。

以下の手順で使用できます
1. googleApiキーをセット
2. カメラを起動
3. 仮想散歩開始ボタンをクリック
4. 再度仮想散歩開始ボタンをクリック
5. 以後足踏みで前進、左腕を一定時間振っていることで左回り、右腕を一定時間振ることで右回りの操作になります。

googleStreetSampoSampleGazo.JPG
ボタンをクリック

これで雨の日でも雪の日でも室内で散歩できますねww

参考にしたサイト

Street View Service  |  Maps JavaScript API  |  Google Developers


GoogleStreetViewが勝手に動く!?GoogleMapAPIを使った自動再生プログラムを作ってみる。 : ビジネスとIT活用に役立つ情報


Canvas とピクセル操作 - 開発者ガイド | MDN


[JavaScript] 関数から複数値を返す | コピペで使える JavaScript逆引きリファレンス


CSS floatを初心者向けに図で解説 抑えるべき注意点…|Udemy メディア

サンプルスクリーンショット
googleStreetSampoSampleGazo.JPG

参考画像
画像の変化を検出する領域は以下のようになっています

googleStreetSampoSampleGazo2.jpg

githubURL:https://github.com/NanjoMiyako/cameraTest

サンプルページURL:https://nanjomiyako.github.io/cameraTest/

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

IndexedDBをKey Value Storeとして扱う

はじめに

自分でPWAのアプリケーションを作成した際、データの保存のためIndexedDBを使いました。
ただ機能的にはLocalStorageで十分だったため、Key Value Storeのように簡単に扱えるようなライブラリを作成しました。

npm
github

本記事はIndexedDBとは何かと、どのようにKey Value Storeのように使うかを説明します。

IndexedDBとは

IndexedDB APIによると、

IndexedDB は、ファイルや blob を含む構造化された多くのデータを保存する、クライアントサイドのローレベル API です。

とのことです。
クライアントサイドでデータを保存する仕組みで、以下の特徴があります。

  • オブジェクト指向データベース
  • データのやり取りが非同期
  • ブラウザでもPWAでも使える

特に「ブラウザでもPWAでも使える」のが重要で、WebStorageはPWAにて使えないようになっています。
そのため、IndexedDBでKey Value Storeを構成しようと思いました。

indexed-kv

今回作ったライブラリのindexed-kvですが、sampleは以下のようになっています。

import { IndexedKv } from "indexed-kv";
const inputKey = "test";
const inputValue = "testValue";

const indexedKV = new IndexedKv(); // オブジェクトの作成
indexedKV
  .set(inputKey, inputValue) // keyとvalueのセットを登録
  .then(() =>
    indexedKV.get(inputValue).then(
      value => console.log(value) // "testValue"
    )
  );

まず、new IndexedKv()にてオブジェクトを生成します。
この際にパラメータとして、dbNametableNameが与えられるため、必要であれば与えてください。

その後、keyとvalueの組み合わせでset()を呼ぶとデータが登録されます。
また、登録したものを取り出す際にはget()を呼ぶとデータを取得できます。

型定義上、WebStorageと同じようにvaluestringで定義しています。

あとは使いやすいように使ってみてください。

browserのsample

以下にブラウザで試せるsampleがあるので、試してください。

Indexed KV sample

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

Yahooニュースに無限ループを見つけた

Yahooニュースの非同期無限ループ

見つけたYahooニュースの非同期無限ループはsetInterval(checkPosition, 100)
どのニュースでもHTMLの<script>に見つかる。
0.1秒ごとではあるがプログラムコードが無限に実行される。
やむをえずsetIntervalを使用するにしてもこの用途では1秒以上がいい。

<script type="text/javascript">
(function() {
    // checkPositionセット
    $(window).load(function() {
        // windowsだと計算が遅いため、scrollイベントのみだと右カラムがフッターに被ることがあるため、setIntervalにて常時再計算
        // スクロール、リサイズ時に即時にポジション確認するためにイベント取得
        $(window).on('scroll resize', checkPosition);
        setInterval(checkPosition, 100);
    });

    var checkPosition = function() {
        var main = $('#main');
        var rightColumn = $('#sub');
        var fixedArea = $('#fixedArea');
        var contentsFooter = $('#contentsFooter');

        // mainカラムより右カラムの方が長い場合は何もしない
        if (main.height() <= rightColumn.height()) {
            return false;
        }

        // フッター上部の座標と固定開始位置
        // 高さや位置はJSの実行タイミングによって変わるので毎度計算する
        var footerTop = contentsFooter.offset().top;
        var fixStart = rightColumn.offset().top + rightColumn.height();

        // 表示領域高さ
        var windowHeight = $(window).innerHeight();

        // 画面スクロール領域を取得
        var windowPos = $(window).scrollTop() + windowHeight;
        var documentHeight = $(document).height();
        // 画面スクロール領域がドキュメントの高さより大きい場合は領域をドキュメントの高さに
        if (windowPos > documentHeight) {
            windowPos = documentHeight;
        }

        // 縦方向の制御
        var baseSelector = '#sub';
        var fixingSelector = baseSelector + ' #fixedArea';
        var fixed = fixingSelector + '.fixed';

        // 固定開始位置に画面が差し掛かってない場合
        if (windowPos <= fixStart) {
            $(fixingSelector).removeClass('fixed');
            $(baseSelector).height('auto');
            $(fixingSelector).width('auto');

        // 画面がフッターまで到達していない場合
        } else if (windowPos <= footerTop) {
            $(fixingSelector).addClass('fixed');
            $(baseSelector).height($(fixingSelector).outerHeight());
            $(fixed).css('bottom',0);
            $(fixingSelector).width($(baseSelector).width());

        // 画面がフッターに到達した場合
        } else {
            $(fixingSelector).addClass('fixed');
            $(baseSelector).height($(fixingSelector).outerHeight());
            $(fixed).css('bottom',(windowPos - footerTop));
            $(fixingSelector).width($(baseSelector).width());
        }
    };
}());
</script>

サイト訪問者に利益を生じさせるか

東京高裁はコインハイブのプログラムについて「サイト訪問者に利益を生じさせない」と判断したが、マイニングした仮想通貨をサイト運営資金にしてウェブサービスの質を維持向上するものであるなら、「サイト訪問者に利益を生じさせる」と判断できる。

Yahooは、右カラムがフッターに被ることを解消させることで、サイト訪問者に利益を生じさせるとするかもしれないが、Chromeで下記の方法でsetIntervalを止めてスクロール、リサイズを行ってもそのような不利益は確認できなかった。

Yahooは逮捕されるか

本件プログラムコードは、サイト訪問者のPCの電力を無限に消費するので金銭的な損害がある。
下記の方法でsetIntervalを止めたところ、Firefoxのタスクマネージャーの「消費電力への影響」が中から低に低減した。

プログラム使用者に生じる不利益に対する注意喚起はなく、その機能を認識した上で実行できないことから、反意図性を肯定できる。

Yahooが不正指令電磁的記録に関する罪で逮捕される可能性は否定できない。

微罪逮捕の是非

プログラマーは法規制とリベラリズムの間で葛藤しながらプログラミングする。
警察はガイドラインを提示し「やめないなら逮捕」と事前に注意喚起すればプログラマーはやめたはずだ。
アラートループ事件やコインハイブ事件のような微罪逮捕を正当化する余地はない。
警察が注意喚起せず逮捕することは、プログラマーを萎縮させ、安全にプログラミングできる裁量範囲を狭め、国家の競争力を削ぐ。

Yahooニュースの無限ループを止める方法

for (var i = 1; i < 99999; i++) window.clearInterval(i);

(参考: javascript - How can I clearInterval() for all setInterval()?

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

JavaScriptオブジェクトで識別子として不正な名前のプロパティにアクセスする方法

ブラケット記法を使わない縛りです笑

*かなりどうでもいい内容ですので、あらかじめご了承ください。

JavaScript初学者の方にはもしかしたら学習の助けになる情報も含まれていないこともないかもしれないと思い、書いてみようと思います。(自分もまだまだ勉強中です)

まずは以下のコードをご覧ください。

const obj = {
  "ho-ge": "ほーげ"
}

突然ですがここで問題です!

Q. objオブジェクトのho-geプロパティの値をブラケット記法を使わずに出力してください。

どうでしょう。意外と悩みませんか?

またまた突然ですが、JavaScriptの識別子には英数字、$、_(アンダースコア)のみを含めることができ、数字からはじめてはいけないという決まりがあります。
識別子とは、変数や関数やオブジェクトのプロパティにつける名前のことです。要は変数名や関数名のことです。

オブジェクトのプロパティ名ももちろん識別子ですが、内部的には文字列(もしくはシンボル)です。
文字列であればどのような値でも問題なく、識別子として有効である必要はありません。
プロパティ名は、識別子としては不正な値でも、クオートで囲んであげれば(文字列リテラルで書けば)どんな名前であってもエラーにはなりません。

obj.js
const obj = {
  hoge: "ほげ", // 識別子として正しい名前なのでクオートで囲んでなくてもok
  "1hoge": "1ほげ", // 数字から始まっているがクオートで囲んでいるのでok
  "ho-ge": "ほーげ" // 識別子に含めることができない文字(-)が入っているがクオートで囲んでいるのでok
}

上記のコードは問題なく実行できます。
しかし、上記オブジェクトのプロパティにアクセスする時には注意が必要です。
以下のコードをご覧ください。

obj.js
const obj = {
  hoge: "ほげ", // 識別子として正しい名前なのでクオートで囲んでなくてもok
  "1hoge": "1ほげ", // 数字から始まっているがクオートで囲んでいるのでok
  "ho-ge": "ほーげ" // 識別子に含めることができない文字(-)が入っているがクオートで囲んでいるのでok
}
console.log(obj.hoge) // => ほげ
console.log(obj.1hoge) // エラー
console.log(obj.ho-ge) // エラー

識別子としては不正な名前がつけられているプロパティにドット記法でアクセスしようとするとエラーになります。
ドット記法とは、obj.hogeのようにオブジェクト名.プロパティ名の形でプロパティにアクセスする書き方です。

さて、ここで登場するのがブラケット記法です。
ブラケット記法はobj['hoge']のようにオブジェクト名['プロパティ名']という形でプロパティにアクセスする書き方です。
ブラケット記法を使えば、識別子としては不正なプロパティの値にもアクセスすることができます。

const obj = {
  hoge: "ほげ",
  "1hoge": "1ほげ",
  "ho-ge": "ほーげ"
}
console.log(obj["1hoge"]) // => 1ほげ
console.log(obj["ho-ge"]) // => ほーげ

ここでもう一度問題をみてみましょう。

Q. objオブジェクトのho-geプロパティの値をブラケット記法を使わずに出力してください。

そうなんです。今回はブラケット記法を使ってはいけないのです。(じゃあなんで説明した?)

さて、それでは早速答えです。笑
実際のところ色々とやり方はあるのだと思いますが、
私の答えは以下です。

answer.js
const obj = {
  "ho-ge": "ほーげ"
}
const {"ho-ge": hoge} = obj // オブジェクトの分割代入
console.log(hoge) // => ほーげ

コード中にブラケット([])は存在していませんよね。

分割代入(Destructuring assignment)は、ES6から導入された文法で、代入式の一種です。
配列の分割代入とオブジェクトの分割代入の2種類があり、今回はオブジェクトの分割代入を利用しています。

↓オブジェクトの分割代入を利用したコード例です。

destructuring.js
// movieオブジェクトを定義
const movie = {
  title: "ミッドサマー",
  director: "アリ・アスター",
  released: {
    jpn: "2020-02-21",
    usa: "2019-06-24",
  }
}

// 分割代入式を使ってmovieオブジェクトのtitle, director, jpn, usaプロパティを
// それぞれのプロパティ名と同名の変数に宣言代入
const {title, director, released: {jpn, usa}} = movie

console.log(title, director, jpn, usa)
// => ミッドサマー アリ・アスター 2020-02-21 2019-06-24
// ちなみにこの場合released変数は定義されないので注意

オブジェクトの宣言代入を使うと、一文でオブジェクトの任意の複数のプロパティを変数に一発で代入できるのです。
便利ですよね。

・左辺:{}の中に代入元となるオブジェクトのプロパティ名と同じ名前の変数をカンマ区切りで列挙する。
・右辺:代入元となるオブジェクト
プロパティの値がオブジェクトで、そのオブジェクトのプロパティも分割代入したい場合はコード例のように{released: {jpn, usa}}とします。

JavaScriptのオブジェクトのプロパティは基本的には列挙順が保証されないので、分割代入する際はプロパティ名と同じ名前で変数を宣言する必要があります。
例えば、titleプロパティをmovieName変数に代入したいと思って以下のようにすると、

const {movieName, director, released: {jpn, usa}} = movie
console.log(movieName, director, jpn, usa)
// => undefined アリ・アスター 2020-02-21 2019-06-24

のようにmovieName変数はundefinedになります。

でも変えたいですよね、変数名。
その場合は{プロパティ名: 変数名}とすればいけます。

destructuring.js
const movie = {
  title: "ミッドサマー",
  director: "アリ・アスター",
  released: {
    jpn: "2020-02-21",
    usa: "2019-06-24",
  }
}

// titleプロパティをmovieName変数に、released.jpnプロパティをreleasedJpn変数に、released.usaプロパティをreleasedUsa変数に代入
// 入れ子になっているオブジェクトのプロパティにアクセスするためのコロンと、別名の変数に代入するためのコロンの役割の違いに注意
const {title: movieName, director, released: {jpn: releasedJpn, usa: releasedUsa}} = movie

console.log(movieName, director, releasedJpn, releasedUsa)
// => ミッドサマー アリ・アスター 2020-02-21 2019-06-24

ここで、answer.jsをもう一度見てみます。

answer.js
const obj = {
  "ho-ge": "ほーげ"
}
const {"ho-ge": hoge} = obj // オブジェクトの分割代入
console.log(hoge) // => ほーげ

最初の方に書いたように、オブジェクトのプロパティ名は文字列です。
answer.jsでは分割代入を使って、識別子としては不正な名前のプロパティを識別子として有効な変数に代入して出力しているということです。

いろいろと脱線して長くなってしまいましたが以上になります。
分割代入他にも様々な仕様があって面白ですよ。
あと、ミッドサマー面白いですよ。

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

JavaScript でライブラリを使わず日時文字列 (yyyyMMddHHmmss) を Date オブジェクトに変換する

日時文字列 (yyyyMMddHHmmss) → Date オブジェクト

/**
 * yyyyMMddHHmmss な文字列を Date オブジェクトに変換
 * @param {string} strDate
 */
function stringToDate(strDate) {
    const year = parseInt(strDate.substring(0, 4))
    const month = parseInt(strDate.substring(4, 6))
    const date = parseInt(strDate.substring(6, 8))
    const hour = parseInt(strDate.substring(8, 10))
    const min = parseInt(strDate.substring(10, 12))
    const sec = parseInt(strDate.substring(12, 14))
    return new Date(year, month, date, hour, min, sec)
}

Date オブジェクト→ 日時文字列 (yyyyMMddHHmmss)

/**
 * Date オブジェクトを yyyyMMddHHmmss 形式の文字列に変換
 * @param {Date} date 変換対象の Date オブジェクト
 */
function dateToString(date) {
    const strYear = String(date.getFullYear()).padStart(4, '0')
    const strMonth = String(date.getMonth()).padStart(2, '0')
    const strDate = String(date.getDate()).padStart(2, '0')
    const strHour = String(date.getHours()).padStart(2, '0')
    const strMin = String(date.getMinutes()).padStart(2, '0')
    const strSec = String(date.getSeconds()).padStart(2, '0')
    return strYear + strMonth + strDate + strHour + strMin + strSec
}

※ padStart() は IE 未対応なので注意

参考ページ

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

Django サジェスト機能付きフォーム

この記事はDjangoのCreateView(汎用クラスビュー)を使い、
viewからtemplateへ値を渡すところのみの記述になります。

サジェスト機能とは

検索サイトに調べたいことを入力する際に検索サイト側が
文字に続く候補を自動的に表示してくれる機能。

アプリ概要

顧客管理アプリの顧客名入力フォームにてサジェスト機能を実装。
下記画像ではと入力すると、DBに保存されてる顧客名をリストで
フォーム下に表示し、該当の名前を選択するとフォームに値を挿入してくれる機能となります。
ss 2.png

ModelFormを継承したフォームの作成

models.py
from django.db import models
from datetime import datetime


class Client(models.Model):
    class Meta:
        db_table ="client"
        verbose_name ="顧客"
        verbose_name_plural ="顧客"

    client_name = models.CharField(verbose_name="顧客名", max_length=255,unique=True)
    receipt_name = models.CharField(verbose_name="領収書宛名", max_length=500, blank=True)
    memo = models.TextField(verbose_name="備考", max_length=1000, blank=True)

    def __str__(self):
        return '{0}'.format(self.client_name)
forms.py
from django import forms
from .models import Client


class ClientForm(forms.ModelForm):
    class Meta:
        model = Client
        fields = ['client_name', 'receipt_name', 'memo']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs["class"] = "form-control"
            field.widget.attrs['placeholder'] = field.label

フォームについてもコードを見ればわかると思うので説明は省略

CreateView(汎用クラスビュー)の作成

views.py
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views.generic import CreateView
from .forms import ClientForm
from .models import Client

# 省略
class ClientCreateView(CreateView):
    model = Client
    form_class = ClientForm
    success_url = reverse_lazy('client:create_done')

    def get_context_data(self, **kwargs):
        client_name_list = [] # templateに渡す空のlist型の変数を定義
        context = super().get_context_data(**kwargs)
        client_data = Client.objects.all() # Clientモデルのデータを全件取得
        for client in client_data: # Clientモデルのデータを1件ずつ取り出し、リストに値を格納
            client_name_list.append(client.client_name)
        context["client_name_list"] = client_name_list
        return context

get_context_dataメソッドを利用し、templateに渡す値を関数内で作成。

templateの作成

サジェスト機能についてはJavaScriptを使用して実装。

client_form.html
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/client/client_form.css' %}">
{% endblock %}
{% block content %}
<div class="form_contents">
    <h1 class="form_title">顧客情報入力</h1>
    <form action="" method="POST">
        {{ form.non_field_errors }}
        <div class="field">
            <p>{{ form.client_name.label }}</p>
            {{ form.client_name }}
            <div class="js-suggestions"></div>
            {{ form.client_name.errors }}
        </div>
        <div class="field">
            <p>{{ form.receipt_name.label }}(任意)</p>
            {{ form.receipt_name }}
            {{ form.receipt_name.errors }}
        </div>
        <div class="field">
            <p>{{ form.memo.label }}(任意)</p>
            {{ form.memo }}
            {{ form.memo.errors }}
        </div>
        <div class="field submit_btn">
            <button type="submit">送信</button>
        </div>
        {% csrf_token %}
    </form>
</div>
{% endblock %}
{% block script %}
<script>
    const list = [
        {% for name in client_name_list %}
            "{{name}}",
        {% endfor %}
    ];
    const elInput = document.querySelector("#id_client_name");
    const elSuggestions = document.querySelector(".js-suggestions");
    const state = {
        inputValue: elInput.value,
        inputting: false
    };
    const dataMap = new WeakMap();

    function render() {
        renderInput();
        renderSuggestions();
    }

    function renderInput() {
        elInput.value = state.inputValue;
    }

    function renderSuggestions() {
        const elNewList = document.createElement("ul");
        if (state.inputting) {
            list.filter(text => text.includes(state.inputValue)).forEach(text => {
            const elItem = document.createElement("li");
            dataMap.set(elItem, { text });
            elItem.textContent = text;
            elItem.addEventListener("click", handleClickItem);
            elNewList.appendChild(elItem);
            });
        }
        elSuggestions.innerHTML = "";
        elSuggestions.appendChild(elNewList);
    }

    function handleClickItem(e) {
        state.inputValue = dataMap.get(e.currentTarget).text;
        state.inputting = false;

        render();
    }

    elInput.addEventListener("input", () => {
        state.inputValue = elInput.value;
        state.inputting = elInput.value !== "";

        renderSuggestions();
    });
</script>
{% endblock %}

contextで渡された値を<script></script>内でfor文を使って値を取り出し、
JavaScript内で再度リストを形成する。
フォームに入力された値と、const list = []内の値で一致した場合は、
一致した値をフォームの下に挿入する仕様。
JavaScriptのコードについては説明省略。
コードコピペで使用してもらえればサジェスト機能として動きます。

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