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

【ゆっくり導入するTypeScript】オブジェクトプロパティを補完しよう編

概要

みなさん、JavaScript書いてますか?動的型付け言語であるJavaScriptは、柔軟で好きなように書きやすい反面、型情報が無いので、エディタの支援を受けづらかったり、実行時undefinedに悩まされたり、つまらないtypoをしてしまうことがあると思います。
なので人々はTypeScript を導入するのでしょうが、学習コスト、導入コスト、移行コストをポンと出せる現場ばかりではないと思います。(学習コストに関してはチームの意識次第でなんとかなるかもしれませんが)
そのような場合、もしTSを導入するのであれば一度にソースコードを全部移行するのではなく、徐々にTS要素を増やしていって、最終的に完全に移行できれば素晴らしいですね。

今回は jetbrains信者の僕が 、 神が作りし 神器 WebStorm 、及び WebStorm が含まれている製品( PhpStorm 等)や、今大人気のVSCode でTSの型定義ファイルを活用し、オブジェクトプロパティの補完を効かせる方法をご紹介します。

今回のつらみ

バックエンドの設計にもよりますが、以下のようにネストしたモデルをそのまま返されると、つらいですね。

sampleResponse.json
{
  "user": {
    "id": 1,
    "name": "suguri",
    "createdAt": "2019-10-04 16:09:46",
    "updatedAt": "2019-10-04 16:09:46",
    "profile": {
      "profileImageUrl": "https://orengi.com/img/10000.png",
      "company": {
        "id": 1,
        "name": "orange company",
        "createdAt": "2019-10-04 16:09:46",
        "updatedAt": "2019-10-04 16:09:46",
        "companyInfo": {
          "capital": 1000000000,
          "contact": {
            "tel": "○○○○-××××-○○○○",
            "email": "example@gmail.com"
          }
        }
      }
    }
  }
}

少々正規化のやりすぎな気もしますが、他にいい例が思い浮かばなかったので、これでいきます。

このレスポンスから、 ユーザープロフィールから、所属会社情報を取得し、その会社のメールアドレスを取得する処理を考えてみます。つまりネストした一番下の情報を引っ張るわけです。

sample.js
import axios from 'axios'

async function getUser() {
  const res = await axios.get('/api/user/1')

  return res.data
}

getUser().then(user => {
  const email = user.profile.company.companyInfo.contact.email
  console.log(email)
})

この状態だと、.profile.company.companyInfo.contact.emailの部分を、入力補完の支援を受けずにタイプすることになります。
僕みたいなポンコツだと、盛大にtypoし、実行時undefinedと向き合う羽目になります。
WebStorm先生も、『型情報がねーからそんなプロパティあるかどうか分かんねーよ』とカンカンです。

コメント 2019-10-23 200143.png

こんなDXでは開発してて楽しくないですよね。IDEの警告を無視し続けるのも精神によくありません。

どうするか

特にコンパイルの設定とかしないけど、とりあえず型定義だけ作っちゃう

以下のような型定義を適当な場所に作っちゃいましょう

models.d.ts
interface contactModel {
  tel: string,
  email: string
}

interface companyInfo {
  capital: number,
  contact: contactModel
}

interface companyModel {
  id: number,
  name: string,
  createdAt: string,
  updatedAt: string,
  companyInfo: companyInfo
}

interface profileModel {
  profileImageUrl: string,
  company: companyModel
}

interface userModel {
  id: number,
  name: string,
  createdAt: string,
  updatedAt: string,
  profile: profileModel
}

そしてDocコメントを書きます

getUser().then(
+  /**
+   * @param user {userModel}
+   */
  user => {
    const email = user.profile.company.companyInfo.contact.email

    console.log(email)
  },
)

コメント 2019-10-23 222445.png

???これだけで補完が効くようになりました!!
IDEの警告もきれいに消えています。
とくにインポートやエクスポートをしなくてもWebStormが勝手にインデックスを作って
サポートしてくれます!awesame!!

VSCode編

VSCode で同じことをやるには以下のようにします。

1.使用する型をexport

models.d.tsを編集して、今回使用する型をexportしましょう。なお、exportする型の内部で使用されている型に関しては、exportする必要はありません。

//略

- interface userModel {
+ export interface userModel {
  id: number,
  name: string,
  createdAt: string,
  updatedAt: string,
  profile: profileModel
}

js側はほとんど同じですが、Docコメントの中で、型定義ファイルを読み込んでる部分だけ違っています。

vscode.sample.js
import axios from 'axios'

async function getUser() {
  const res = await axios.get('/api/user/1')

  return res.data
}

getUser().then(
  /**
   * @param user {import('./models').userModel}
   */
  user => {
    const email = user.profile.company.companyInfo.contact.email
    console.log(email)
  },
)

???これでVSCodeでも型の支援が受けられるようになりました!!

この方法でフロント側に型定義ファイルを増やしていけば、いずれは完全に移行できる日がくるかもしれません。

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

年末まで毎日webサイトを作り続ける大学生 〜5日目 ドラッグ&ドロップで簡単なゲームを作る〜

はじめに

初めまして。
年末まで毎日webサイトを作っている者です。
今日はドラッグ$ドロップを使って簡単なゲームを作りました。
扱う技術レベルは低いですが、同じように悩んでる初心者の方を勇気付けられれば幸いです。
今日は5日目。(2019/10/23)
よろしくお願いします。

サイトURL

https://sin2cos21.github.io/day5.html

やったこと

3つの箱を用意して、それをドラッグ&ドロップで右側に移動できるようにしました。
ドラッグした回数も表示しています。
レイアウトはこんな感じ↓

スクリーンショット 2019-10-23 23.15.24.png

移動させた時にドロップできるポイントに入っていれば、ドロップ先の色が少し変わります↓

test1.gif

こだわったところ

箱ごとドロップさせるのに苦労しました・・・。
終わってみたらシンプルな構造でしたが、今日はドラッグ&ドロップの基礎を学んだという感じです。
javascriptはこれだけです↓

<script>
        function drag($event) {
            $event.dataTransfer.setData("Text", $event.target.id);
        };

        function drop($event) {
            var test = event.dataTransfer.getData("text");
            var test2 = document.getElementById(test);
            $event.currentTarget.appendChild(test2);
            $event.preventDefault();
        }

        var $count = 0;
        function count() {
            document.getElementById("count").innerHTML = ++$count;
        }

        function change() {
            document.getElementById("wrapper2").style.backgroundColor = "rgba(0, 0, 0, .1)";
        }

        function change2() {
            document.getElementById("wrapper2").style.backgroundColor = "transparent";
        }

    </script>

関数dragとdropで移動させています。
countで移動回数を数えています。
changeとchange2はドロップ可能状態になったらドロップ先の色を変えるために加えました。

感想

やればやるほど分からないことが増えていきますが、それでも動くものを作れて発信できるのはいいですね。
まだ$eventやらdataTransferやらよく理解できていませんが、今後も作りつつ調べるスタイルで行こうと思います。
今週はドタバタしているので明日と明後日のサイト制作は苦労しそうですが頑張ります!

参考

上2つのサイトがとても分かりやすかったです。ありがとうございます。

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

log4js-node の使い方【v5.2.2】

1. はじめに

log4js-nodeは、log4js を javascript 用に移植した log4js を node 用に書き直したものらしい。
Java の log4j とは大きく違うとのこと。
ロガーの使い方がいまいち分かっていなかったので、調査ついでにまとめた。

テスト環境

  • Windows10, MINGW64, VSCode
  • node: 10.14.2
  • babel-node: 7.6.1
  • log4js: 5.2.2
  • ES6 形式で書きたかったので babel-node を使用
  • ログファイルは VSCode + Log File Highlighter で表示 (ちゃんと設定してないです…)
console
$ npm install log4js

参考:

2. 基本的な使い方

ログの出力(logging)

test.js
import log4js from 'log4js'

const logger = log4js.getLogger()
logger.level = 'all'

logger.trace('Some trace messages')
logger.debug('Some debug messages')
logger.info('Some info messages')
logger.warn('Some warn messages')
logger.error('Some error messages')
logger.fatal('Some fatal messages')

log4js.shutdown((err) => {
  if (err) throw err
  process.exit(0)
})

image.png

  • logger.level は ALL (最小) < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < MARK < OFF (最大)
  • default は OFF (console へ出力しない)
  • level は独自のものを設定することも可能だが取り扱わない
  • log4js.shutdown(cb(err)) はログを確実に保存してから終了する際に使用する (flush 処理)
  • 定義されている level は以下を参照

image.png

参考:

ログのカテゴリ (category)

test.js
const logger = log4js.getLogger()
logger.level = 'all'
logger.info('Some info messages', 'append', 'more')

const cheseLogger = log4js.getLogger('cheese')
cheseLogger.level = 'all'
cheseLogger.info('Cheese is Comte.')

image.png

  • ログを category ごとにグループ化して出力することができる
  • category は log4js.getLogger([category])で指定する
  • default は default というグループ
  • category ごとに出力する level を指定できる
  • 前項で触れなかったが、複数の引数を使用するとスペース区切りで出力される

参考:

3. 高度な使い方(Configure)

コンソールとファイルに出力 (stdout / file)

test.js
log4js.configure({
  appenders: {
    out: { type: 'stdout' },
    app: { type: 'file', filename: 'application.log' }
  },
  categories: {
    default: { appenders: ['out', 'app'], level: 'debug' }
  }
})

image.png
image.png

  • appender は渡されたログを出力する機械と考える
  • default の category を、 stdout に出力する out と file に出力する app という appender を定義した
  • appender の name(key) は自由に指定可能
  • categories で category ごとに使用する appender と level などを定義できる
  • logger.level は この level と同じ意味となる
  • ファイルに出力されるログは末尾に追記される

参考:

ログのローリング (file / fileSync)

test.js
log4js.configure({
  appenders: {
    app: { type: 'file', filename: 'application.log', maxLogSize: 100, backups: 1 }
  },
  categories: {
    default: { appenders: ['app'], level: 'all' }
  }
})

image.png

  • file 系の appender は書き込むファイルをローテートすることが可能
  • 上記は application.log.2 が保持上限を超えて削除されている
  • type: fileSync で同期的に書き込むことも可能 (test などの用途)
  • options
    • type: file / fileSync
    • filename: string - ログファイルの名称 (xxx.ext)
    • maxLogSize: integer - 1ファイルの最大サイズ (byte)、default は上限なし
    • backups: integer - 保持するファイル数、default は 5
    • compress: boolean - true で ローリングしたファイルを圧縮する(.gz)
    • keepFileExt: boolean - true で 拡張子を保持する (xxx.ext.1 を xxx.1.ext にする)
    • encoding: string - default は utf-8

参考:

日付ごとのローリング(dateFile)

test.js
log4js.configure({
  appenders: {
    app: { type: 'dateFile', filename: 'application.log', pattern: '.yyyyMMdd-hhmmss' }
  },
  categories: {
    default: { appenders: ['app'], level: 'all' }
  }
})

image.png

  • pattern で指定した日付の粒度でローテートすることが可能
  • 上記は1秒ごとに切り替える設定で二回実行した結果
  • file 系と違って保持数は指定できない?(要検証)
  • 保持日数は daysToKepp で指定できる
  • option
    • type: dateFile
    • filename: string - ログファイルの名称 (xxx.ext)
    • pattern: string - ローテートするタイミング、default は .YYYY-MM-dd、参照:date-format
    • daysToKeep: integer - 保持する日数、 default は上限なし
    • alwaysIncludePattern: boolean - true で現在のログファイル名にも日付を付与する
    • compress: boolean - true で ローリングしたファイルを圧縮する(.gz)
    • keepFileExt: boolean - true で 拡張子を保持する (xxx.ext.date を xxx.date.ext にする)
    • encoding: string - default は utf-8

参考:

appendars のレベルフィルター (logLevelFilter)

test.js
log4js.configure({
  appenders: {
    out: { type: 'stdout' },
    app: { type: 'file', filename: 'application.log' },
    wrapErr: { type: 'logLevelFilter', appender: 'app', level: 'warn' }
  },
  categories: {
    default: { appenders: ['out', 'wrapErr'], level: 'all' }
  }
})

image.png
image.png

  • logLevelFilter を用いると appender ごとに出力する level を指定可能
  • 出力する appender にラップするように使用する
  • これによりエラーログだけを別途で処理することも可能
  • ※ appender は上から処理されるので logLevelFilter は最後に配置する必要がある
  • options
    • type: logLevelFilter
    • level: string - 出力する最小の level
    • maxLevel: string - 出力する最大の level 、default は FATAL

参考:

その他

4. 出力ログのレイアウト

標準レイアウト(Built-in Layout)

test.js
log4js.configure({
  appenders: {
    out: { type: 'stdout', layout: { type: 'basic' } }
  },
  categories: {
    default: { appenders: ['out'], level: 'all' }
  }
})
  • 各種 appender に layout を指定することができる
  • 必須ではないので必要に応じて…
  • ※ file 系の appenders に colores を指定しないこと! (色付けは制御文字を使用しているため)

type: basic
image.png

type: coloured / colores
image.png

type: messagePassThrough
image.png

参考:

出力パターン (Pattern format)

test.js
log4js.configure({
  appenders: {
    out: { type: 'stdout', layout: { type: 'pattern', pattern: '%d %[%5p%] %c %m' } }
  },
  categories: {
    default: { appenders: ['out'], level: 'all' }
  }
})

image.png

  • 出力する文字列のパターンを独自に設定できる
  • field は %[padding].[truncation][field]{[format]} で定義されている
    • padding: integer - 空白埋め文字数、右詰め
    • truncation: integer - 最大文字数、切り捨て
    • field: string - フィールド名
    • format: string(option) - フィールドのオプション (任意)
  • 上記の場合、「日付 level(5文字埋め)を色付きで カテゴリ 本文」となる
  • ※ file 系の appenders に %[ %] を指定しないこと! (色付けは制御文字を使用しているため)
  • pattern field (サンプル)
    • %r: ローカル時刻 (02:19:11)
    • %p: ログ level (INFO)
    • %c: ログ category (default)
    • %h: ホスト名 (PCの名前等)
    • %m: メッセージ
    • %d: 日時、{ISO8601 | yyyy-MM-ddThh:mm:ss.SSS} (2019-10-23T02:13:53.028)、参照:date-format
    • %%: %のエスケープ
    • %n: 改行
    • %z: プロセスID、process.pid (28076)
    • %[: 色付け開始
    • %]: 色付け終了
    • enableCallStack: true 時の field (後述)
      • %f: ログ出力元のファイル名、{depth | 0} (C:\xxx\test.js)
      • %l: ログ出力元の行番号 (13)
      • %o: ログ出力元の列位置 (8)
      • %s: スタックトレース ( at Object.info (C:\xxx/test.js:13:8) \n ...)
  • トークンは取り扱わない

参考:

enableCallStack

test.js
log4js.configure({
  appenders: {
    out: { type: 'stdout', layout: { type: 'pattern', pattern: '%[[%d] %p %c -%] %m%n%f %l %o%n%s' } }
  },
  categories: {
    default: { appenders: ['out'], level: 'all', enableCallStack: true }
  }
})

const logger = log4js.getLogger()
logger.info('Some info messages')

image.png

  • category で enableCallStack: true を設定すると出力箇所のスタックトレースが取得できる
  • %s の前に %n で改行を入れないと出力が崩れるので注意
  • enableCallStack: false の際、だった各種 field は出力されないが %n は出力される点も注意
  • (スルーできる field があれば便利だったかも)
  • error 出力などの場面で使用するのが良い?

参考:

Tokens

  • key - value の値を埋め込める仕組み
  • token はハードコートせざるを得ないので、どの key を使用しているか定義しておく必要がある
  • これを使うなら message に変数を結合させるのがベストのような?
  • 発行ユーザーなどの用途なら悪くはなさそう
  • ここでは取り扱わない

参考:

カスタムレイアウト (Custom Layout)

test.js
log4js.addLayout('json', function(config) {
  return function(logEvent) {
    return JSON.stringify(logEvent, null, 2) + config.separator
  }
})

log4js.configure({
  appenders: {
    out: { type: 'stdout', layout: { type: 'json', separator: ',' } }
  },
  categories: {
    default: { appenders: ['out'], level: 'all', enableCallStack: true }
  }
})

const logger = log4js.getLogger()
logger.info('Some info messages')

image.png

  • 独自のレイアウトを定義することも可能
  • appender で指定した設定が config から取得できる
  • functionName 以下は enableCallStack: true が必要
  • callback の引数は logEvent の Object なので、これを加工して return に返す

参考:

5. サンプル

test.js
import os from 'os'
import log4js from 'log4js'
import dateFormat from 'date-format'
import chalk from 'chalk'

const LOG_LEVEL = 'ALL' // DEBUG: TRACE or DEBUG, PRODUCTION: INFO or OFF

const levelColors = {
  TRACE: { meta: 'grey', body: 'grey', trace: null },
  DEBUG: { meta: 'green', body: 'grey', trace: null },
  INFO: { meta: 'cyan', body: 'white', trace: null },
  WARN: { meta: 'yellow', body: 'yellow', trace: null },
  ERROR: { meta: 'red', body: 'red', trace: 'white' },
  FATAL: { meta: 'magenta', body: 'magenta', trace: 'white' }
}

const coloring = function(color, text) {
  if (color) {
    return chalk[color](text)
  }
  return text
}

log4js.addLayout('origin', function({ addColor }) {
  return function(e) {
    const date = new Date(e.startTime)
    const level = e.level.levelStr.toUpperCase() // 大文字
    const hasCallStack = e.hasOwnProperty('callStack') // callStack を持っているか

    const dateStr = dateFormat('yyyy-MM-dd hh:mm:ss.SSS', date)
    const message = e.data.join(' ') // データはスペース区切り
    const levelStr = level.padEnd(5).slice(0, 5) // 5文字
    const color = levelColors[level]

    // メタ情報
    const meta = `${levelStr} ${dateStr} [${e.categoryName}]`
    const prefix = addColor ? coloring(color.meta, meta) : meta

    // ログ本体
    const body = addColor ? coloring(color.body, message) : message

    // スタックトレース
    let suffix = ''
    if (hasCallStack && color.trace) {
      const callStack = e.callStack
      suffix += os.EOL
      suffix += addColor ? coloring(color.trace, callStack) : callStack
    }

    return `${prefix} ${body}${suffix}`
  }
})

log4js.configure({
  appenders: {
    out: { type: 'stdout', layout: { type: 'origin', addColor: true } },
    logFile: { type: 'file', filename: 'logs/application.log', layout: { type: 'origin', addColor: false } },
    errFile: { type: 'file', filename: 'logs/error.log', layout: { type: 'origin', addColor: false } },
    log: { type: 'logLevelFilter', appender: 'logFile', level: 'info' },
    err: { type: 'logLevelFilter', appender: 'errFile', level: 'warn' },
  },
  categories: {
    default: { appenders: ['out', 'log', 'err'], level: LOG_LEVEL, enableCallStack: true }
  }
})

const logger = log4js.getLogger()
logger.trace('Some trace messages')
logger.debug('Some debug messages')
logger.info('Some info messages')
logger.warn('Some warn messages')
logger.error('Some error messages')
logger.fatal('Some fatal messages')

log4js.shutdown(() => {})

image.png
image.png

  • console のカラーリングと、2つのファイルに書き出す形式
    • console への出力は LOG_LEVEL で制御する
  • ファイルは INFO 以上と WARN 以上の二種類を用意 (error を把握しやすくする)
    • これにローリングを加えれば完成となる (見やすさのため省略)
    • terminal が 256色 をサポートしていない場合、通常色出力となる
  • 独自の level を定義することは可能だが、カラーリングはサポートしないらしい?

参考:

以上

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

Chrome拡張開発 超概要

社内勉強会用資料です。

公式ドキュメント

https://developer.chrome.com/extensions
チュートリアルやAPI、サンプルなど豊富に揃ってます。

主な拡張の種類

  • Content Script ★
  • Background Page ★
  • Page Action / Browser Action
  • Override Pages
  • Context Menus
  • Omnibox
  • Option Page

Content Script

  • 表示しているページに挿入されるJS
  • タブを開いたりリロードした際に読み込まれる
  • マウス操作やキーボード操作、タブの操作等をイベントとして拾える
  • 表示中のDOMも操作できる
  • 外部通信は出来ない
  • 使用できるAPIが制限されている

Background Page

  • Chromeの裏で動くJS
  • ブラウザ起動時に1回だけ読み込まれる
  • 重い処理や通信系などはこちらで行う
  • 直接DOMの操作は取得出来ないのでタブを指定してスクリプトを送り込む等の工夫が必要
  • 外部通信が出来る
  • ほぼ全てのAPIを使える

Page Action / Browser Action

  • ブラウザの右上にある拡張のアイコンをクリックしたときに動くJSやHTML
  • 特定のページでのみ有効か全体で有効かの違い

Override Pages

  • 新規タブ作成画面、ブックマークマネージャ画面、履歴画面を差し替える

Context Menus

  • 右クリック時のメニューを拡張する

Omnibox

  • 検索バーの操作を拡張する

Option Page

  • 設定ページ
  • 右上のアイコンから飛べる

manifest.json

  • 拡張機能の定義ファイル
  • 機能名やバージョン、説明、JSの紐づけや権限設定など、全ての設定はこのファイルで行う
  • 複数指定できる項目は上から順に読み込まれるため、依存関係ある場合は注意が必要

(設定例)

manifest.json
{
  "name": "拡張名",
  "description": "説明",
  "version": "バージョン",
  "manifest_version": 2,
  "content_scripts": [
    {
      "matches": [ "<all_urls>" ],
      "js": ["js/content_script.js"],
      "css": ["css/main.css"]
    }
  ],
  "background": {
    "scripts": ["js/background.js"]
  },
  "permissions": [
    "tabs",
    "clipboardRead",
    "clipboardWrite",
    "storage",
    "cookies",
  ],
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "options_page": "html/options.html"
}

content_scripts > matches

  • Content Sciript を挿入するページを正規表現で指定出来る
    余計なページで実行すると不具合が起きたり遅くなったりするので最低限にしておくのがベスト
    <all_urls> で全てのページへ挿入される

permissions

  • 使用する Chrome API の種類をここで定義する
    定義されていないAPIは使えない
    セキュリティ上の観点から、最低限にしておくのがベスト
    必要以上の権限を設定すると審査に通りにくくなるらしい

Content Script と Background Page での通信(Message Passing)

  • DOMの操作は Content Script で行うが、APIが制限されているので細かいことが出来ない
  • Background Page へ制御を移して処理をしたい場合が多い
  • Message Passing という仕組みを利用して各ページでやり取りが出来る

content_script.js から background.js へ通信を行う例

content_script.js
chrome.runtime.sendMessage({
    // 好きなオブジェクトを渡せる
    mode: "foo",
    html: document.body.outerHTML // htmlを渡したり

}, response => {
    // background.js からのコールバック(sendResponse)
    console.log(response); // bar
});
background.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    // 渡されたオブジェクトが request として入ってくる
    let mode = request.mode; // foo
    let restext = request.html; // 表示中ページのhtml

    // 処理分岐書いたり
    switch(mode) {
        case "foo": {
            // 処理
            // ...
            // ...
            sendResponse("bar"); // content_script.js にコールバック
            break;
        }
        default: {
            break;
        }
    }
    return true;
});

よく使うAPI

公式ドキュメント参照。
処理はほぼ全てコールバックで続けていくので地獄にならないよう工夫が必要。

  • chrome.tabs.create(object createProperties, function callback)
    新しいタブを作成する。
    オプションで「どのウィンドウに作るか」「どの位置に作るか」「アクティブにするか」等を指定できる。

  • chrome.tabs.query(object queryInfo, function callback)
    タブの情報を取得する。
    オプションで「どのウィンドウから取得するか」「特定のURLを開いているタブ」「アクティブなタブ」等を指定できる。

  • chrome.tabs.executeScript(integer tabId, object details, function callback)
    指定したタブでスクリプトを実行できる。
    スクリプトを直接書いたり、ファイルを指定したりもできる。
    実行タイミングの指示も可能。
    タブを指定しないといけないので、 chrome.tabs.query とよく併用する。

  • StorageArea.get(string or array of string or object keys, function callback)
    StorageArea.set(object items, function callback)
    ローカルストレージを利用する。
    StorageArea は chrome.storage.sync / chrome.storage.local に分かれている。
    local は拡張ごとのローカルストレージ、syncはアカウントごとに共通なローカルストレージを利用する。

  • chrome.cookies.getAll(object details, function callback)
    クッキーを取得する。
    getAll ではなく get の方を使えばURL指定できる。

  • chrome.runtime.sendMessage(string extensionId, any message, object options, function responseCallback)
    Content Script から Background Page へメッセージを送る。
    上で紹介したもの。

  • chrome.tabs.sendMessage(integer tabId, any message, object options, function responseCallback)
    Background Page から Content Script へメッセージを送る。
    上で紹介した通信の逆方向バージョン。
    外部通信、処理を行った結果を Content Script へ渡したいときに使う。

まとめ

manifest.jsonで定義した構造、ファイル名で html / JS / css が動けばいいので、TypeScript + babel や webpack 使って作ることも出来ます。
ライブラリやAnguler・React等のフレームワークも動きます。

ストアに公開した拡張は利用者からソース見れてしまうので要注意。
直接ソースを見られるようにしたサイトもあります。(zipでDL出来るサービス付き)

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

書き込むと自動的に拡大するTEXTAREA

textareaは自動的に拡大してもらいましょう。

スクロールしながら長文を入力するのは嫌ですね。
下記のファイルを置いて、各htmlページに
<script src="./auto_resize.js" type="text/javascript"></script>
の1行を追加すると、すべてのtextareaが自動拡縮になります。

auto_resize.js
document.addEventListener('DOMContentLoaded',function(event){
    autoResize_all();
},false);

function autoResize_all()
{
    nodes=document.getElementsByTagName('TEXTAREA');
    for(n=0;n<nodes.length;n++){
        autoResize(nodes[n]);
        nodes[n].addEventListener('input',function(event){autoResize(event);},false);
    }
}

function autoResize(event)
{
    node=event.target;
    if(node){
        if(node.scrollHeight < node.offsetHeight){
            node.style.height="initial";
        }
        if(node.scrollHeight > node.offsetHeight){
            node.style.height = node.scrollHeight + "px";
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

いちいち見出しに「必須入力」と書く労力

自動的に「必須入力」を付記させる

form.html
<label>
    <span>メールアドレス(※必須)</span>
    <input type=>
</label>

…なんてことをしますが、書き忘れたり、仕様変更で必須ではなくなったりしますね。表記と挙動が違うのは避けたいものです。

inputタグに"required"が付いているものが必須なわけですから、自動的に「必須」と付記させましょう。下記のファイルを置いたら、各htmlページに
<script src="./check_required.js" type="text/javascript"></script>
の1行を追加してください。

check_required.js
document.addEventListener('DOMContentLoaded',function(event){
    check_required();
},false);

function check_required()
{
    for(i=0;i<document.forms.length;i++){
        for(j=0;j<document.forms[i].elements.length;j++){
            e=document.forms[i].elements[j];
            if(e.attributes.getNamedItem("required")){
                e.placeholder+="[必須]";
                e.previousElementSibling.innerHTML+="[必須]";
            }
        }
    }
}

これで少しは手間が省けますね。

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

大きなファイルをJavaScriptで分割アップロードする

FileReader関数を利用して分割アップロードする

アップロード前に画像のプレビューを表示するくらいしか使い道が思いつかなかったFileReader関数ですが、実用的な使途がありました。

サーバーの制限を回避する

php.iniなど設定を変更できないサーバーを使っている場合、タイムアウトやメモリ-不足などでアップロードが失敗してしまいます。
<?php
ini_set('upload_max_filesize', '128M');
ini_set('max_execution_time', '120');
?>

などの記述で回避できる場合もありますが、これらを書いても効かない場合もあります。ならば分割して少しずつ送信すれば良いわけです。

アップロードがメインになるようなサービスを作ろうというわけではないので、極力シンプルに書いてみたのが、以下のコードです。

ブラウザーからの分割アップロード(html部分)

FileReader.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>dividing_upload</title>
<script src="./dividing_upload.js" type="text/javascript"></script>
</head>
<body>
    <form>
        <input id="file" name="file" type="file" />
    </form>
    <span id="msg"></span>
    <form>
        <input type="text" id="from" name="from" readonly  />
        <textarea id="data" name="data" readonly></textarea>
    </form>
</body>
</html>

実用上は、2つめのformは非表示にした方が良いでしょうね。

ブラウザーからの分割アップロード(js部分)

dividing_upload.js
var cnt=0;
var total=0;

function post(result,from,size)
{
    plus=0;
    data=result.substr(from,size);
    if(data.substr(size-1,1)=="\r"){    // \r\nを分断すると\nを補ってくれちゃうので
        plus=1;
        data=result.substr(from,size+plus);
    }
    document.querySelector('#data').value=data;
    document.querySelector('#from').value=from;
    postForm('FileReaderPOST.php',document.forms[1],function(responseText){
        if(responseText==''){
            cnt+=data.length;
            if(cnt<total){  // 2回め以降の送信
                document.querySelector('#msg').innerHTML =""+cnt+"/"+total;
                post(result,from+size+plus,size);
            }else{          // 完了時のリセット
                document.forms[0].reset();
                document.forms[1].reset();
                cnt=0;
                total=0;
                document.querySelector('#msg').innerHTML ='completed => <a target="_blank" href="FileReaderPOST.txt">download</a><br />';
            }
        }else{
            alert(responseText);
        }
    });
}
function postForm(uri,formNode,callback1)
{
    form = new FormData(formNode);
    var xhr = new XMLHttpRequest();
    xhr.open('POST', uri);
    xhr.onreadystatechange=function(){
        if(xhr.readyState == 4 && xhr.status == 200){
            callback1(xhr.responseText);
        }
    };
    xhr.send(form);
}
window.addEventListener('DOMContentLoaded', function() {
    document.querySelector("#file").addEventListener('change', function(e) {
        if (window.File) {
            var input = document.querySelector('#file').files[0];
            var reader = new FileReader();
            reader.addEventListener('load', function(e) {
                base64=btoa(reader.result); // 文字バケ防止のエンコード
                post(base64,0,32768);       // 初回送信、分割サイズ指定
                total=base64.length;
                document.querySelector('#msg').innerHTML ="送信中";
            }, true);
            reader.readAsBinaryString(input);
        }
    }, true);
});

FileReaderを使う部分はコチラをベースに使いました

分割をまとめるため、サーバー側で再結合する処理が必要です。

FileReaderPOST.php
<?php
    $filename='FileReaderPOST.txt'; // とりあえず固定値
    if(is_file($filename)){
        if($_POST['from']==0){      // 初回送信なら前回のファイルを削除
            $data='';
            unlink($filename);
        }else{
            $data=file_get_contents($filename);
        }
    }else{
        $data='';
    }
    file_put_contents(  // デコードしてから追加書き込み
        $filename,
        $data.base64_decode ($_POST['data']));
?>

file_put_contentsじゃなくてfopen("……","a")にしろ、ってところでしょうか。要するにデコードして追加書き込みをするだけなので、phpでなくても構いません。
"$filename"がハードコーディングで".txt"になっていますが、エンコードされるのでバイナリーデータも送信できます。その場合はform内にファイルタイプを追加して、拡張子を変更させれば良いでしょう。

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

mithril.jsのrouteでSPAを作るときにhrefにつまずいた件

はじめに

最近ときめきアイドルスコアランキングサイトを作りなおそうとしている @AinoMegumi から依頼を受けてASP.NET CoreでのWebページ制作に協力している。

その中でランキングページをどう実装するかという話になった。

まずランキングの種類が

  • 総合
  • 月間(今月)
  • 月間(先月)

とあり、またときめきアイドルというゲームには

  • Easy
  • Normal
  • Hard
  • Extreme

という難易度があるらしく、最大で3×4通りのページが各曲ごとに必要になることがわかった。

同じ曲の複数のランキングを切り替えてみたいというユーザーの行動が予想されたが、ASP.NET Coreで実装するとアクセス回数が増えてしまう。ここはクライアントサイドで切り替えを実装するべきだ。

一方で単にdisplay: none;を切り替えるような実装では3×4通りのページが一意なURLにならないのでURLを共有することで見ているものを伝えられなくなる。

つまりルーティングが必要になってきた。

ライブラリ選定

こういうSPAを作るライブラリはvue.jsとかReactとかとか山程あるらしいのだが、自分はmithril.jsしか使ったことがない。

https://mithril.js.org/

mithril.jsはJSXみたいなIntelliSenseに優しくない謎な拡張には頼らず、pureなJavaScriptで記述できるライブラリで、ライブラリが小さく、描画も高速である。

See the Pen Mithril Component Example by Pat Cavit (@tivac) on CodePen.

ちなみについ先日、公式Twitterができた(これまでは開発者個人のTwitterで宣伝とかしていた)
@mithrildotjs

mithril jsでのルーティング

route(root, defaultRoute, routes) - Mithril.js

m.routeを使うことでできるらしい。

var Edit = {
    view: function(vnode) {
        return [
            m(Menu),
            m("h1", "Editing " + vnode.attrs.id)
        ]
    }
}
m.route(document.body, "/edit/1", {
    "/edit/:id": Edit,
})

今回のように複数パラメータがあるルーティングももちろんできる。

It's possible to have multiple arguments in a route, for example /edit/:projectID/:userID would yield the properties projectID and userID on the component's vnode attributes object.

ルーティングへのリンクを貼るにはm.route.Linkを使う。ちなみにm.route.LINKと書いて動かないとわめいて公式チャットに質問投げて秒殺されたのは内緒だ。

通常aタグを使うリンクを

m('a', {href: "/test"})

のように書くところを置き換えて次のようになる。

m(m.route.Link, {href: "/test"})

dataの欠損に対応したデザイン

最初に

最大で3×4通りのページ

と書いたのは、ランキングへデータが投稿されていないことがあるからだ。例えばExtreme難易度のデータ投稿が3ヶ月前が最後だったら総合ランキングにしかExtreme難易度の情報がない。

つまり以下のようにボタンを並べられず、

image.png

このようにデータがあるボタンだけ出したい。

image.png

hrefをどう作るか問題

ランキングの種類ボタンと難易度ボタンは別のクラスにして分離することにした。ただし実装が似かよるので継承を使った。

  class RankingType extends NavBase {
    /**
     * @param {string} rankingType
     */
    constructor(rankingType) {
      super(
        rankingTypeList,
        decideDefault(rankingTypeList, rankingType, 'total'),
        'ranking-type',
        v => `${rankingTypeDescriptionMap.get(v)}ランキング`
      );
    }
  }
  class GameMode extends NavBase {
    /**
     * @param {string} rankingType
     * @param {string} gameMode
     */
    constructor(rankingType, gameMode) {
      const list = createGameModeList(input[rankingType]);
      const selected = decideDefault(list, gameMode, 'extreme');
      super(list, selected, 'game-mode', v => v.charAt(0).toUpperCase() + v.slice(1));
      console.log(`${this.constructor.name}#constructor(): selected=${selected}`, list);
      this.prefix_ = rankingType;
    }
  }

今選択されているボタンはNavBaseクラスに

    /**
     * @returns {string}
     */
    get current() {
      return this.current_;
    }
    set current(selected) {
      console.log(`${this.constructor.name}#set current(${selected})`);
      this.current_ = selected;
    }

こんなのがあるのでとってこれる。

ルーティングはまずこれらを持つMainクラスを作る。

  class Main {
    constructor() {
      this.rankingType = new RankingType();
      this.gameMode = new GameMode(this.rankingType.current);
      ???
    }
    view(vnode) {
      if (
        Object.prototype.hasOwnProperty.call(vnode.attrs, 'rankingType') &&
        Object.prototype.hasOwnProperty.call(vnode.attrs, 'gameMode')
      ) {
        if (vnode.attrs['rankingType'] !== this.rankingType.current) {
          this.gameMode = new GameMode(vnode.attrs['rankingType'], vnode.attrs['gameMode']);
          this.rankingType.current = vnode.attrs['rankingType'];
          ???
        }
        if (vnode.attrs['gameMode'] !== this.gameMode.current) {
          this.gameMode.current = vnode.attrs['gameMode'];
        }
      }
      return [m(this.rankingType), m(this.gameMode), createRanking(this.rankingType.current, this.gameMode.current)];
    }
  }

では肝心のview()メソッドはどういう実装になるのか。

    view() {
      console.log(`${this.constructor.name}#view() this.current=${this.current}`);
      return m(
        'nav',
        { class: this.cssClassPrefix_ },
        m(
          'div',
          { class: `${this.cssClassPrefix_}-container` },
          this.list_.map(v =>
            m(
              'button',
              {
                class:
                  this.current_ === v
                    ? `${this.cssClassPrefix_}-container__item is-current`
                    : `${this.cssClassPrefix_}-container__item`,
              },
              m(
                m.route.Link,
                {
                  href: ???,
                  options: { replace: true },
                },
                this.valueConverter_(v)
              )
            )
          )
        )
      );
    }

さて、ボタンのリンク先であるhrefはどのように書けばいいだろうか。

ルーティング後のURLは例えば#!/cmonth/easyのようになる。#!の部分は勝手にm.route.Linkがやってくれるからいいが、問題は/cmonth/easyの部分だ。

ランキングの種類選択ボタンはデータのあるゲーム難易度を知ってさらにどの難易度を表示するかをボタンがクリックされる前に知る必要がある。ところがランキングの種類とゲーム難易度はクラスを分けてしまったから、ランキングの種類を管理するクラスはゲームの難易度のことは知らない。

結局コールバックしかないのか?

これに対してEventを使ってメッセージングみたいなことを実装しようとしたがコードが膨れ上がりすぎた。

Mainクラスはすべての情報を知っているのだからここから教えてあげればいい。

つまりhrefを生成する関数を注入すればいい。

    /**
     * @param {(s: string) => string} hrefCreater
     */
    injectHrefCreater(hrefCreater) {
      this.hrefCreater_ = hrefCreater;
    }

view()では

              m(
                m.route.Link,
                {
                  href: this.hrefCreater_(v),
                  options: { replace: true },
                },
                this.valueConverter_(v)
              )

のように呼び出す。

あとはMainクラスで

  class Main {
    constructor() {
      this.rankingType = new RankingType();
      this.gameMode = new GameMode(this.rankingType.current);
+     this.rankingType.injectHrefCreater(rankTypeSelected => {
+       const gameMode = decideDefault(createGameModeList(input[rankTypeSelected]), this.gameMode.current, 'extreme');
+       console.log(`this.rankingType.injectHrefCreater:: /${rankTypeSelected}/${gameMode}`);
+       return `/${rankTypeSelected}/${gameMode}`;
+     });
+     this.gameMode.injectHrefCreater(gameModeSelected => `/${this.rankingType.current}/${gameModeSelected}`);
    }
    view(vnode) {
      if (
        Object.prototype.hasOwnProperty.call(vnode.attrs, 'rankingType') &&
        Object.prototype.hasOwnProperty.call(vnode.attrs, 'gameMode')
      ) {
        if (vnode.attrs['rankingType'] !== this.rankingType.current) {
          this.gameMode = new GameMode(vnode.attrs['rankingType'], vnode.attrs['gameMode']);
          this.rankingType.current = vnode.attrs['rankingType'];
+         this.gameMode.injectHrefCreater(gameModeSelected => `/${this.rankingType.current}/${gameModeSelected}`);
        }
        if (vnode.attrs['gameMode'] !== this.gameMode.current) {
          this.gameMode.current = vnode.attrs['gameMode'];
        }
      }
      return [m(this.rankingType), m(this.gameMode), createRanking(this.rankingType.current, this.gameMode.current)];
    }
  }

のように書き換えればいい。

もっといい方法はなかったのか

そもそもランキングの種類とゲーム難易度のクラスが持っている状態は何だったか。

    /**
     * @param {readonly string[]} list target list
     * @param {string} selectedValue
     * @param {string} cssClassPrefix
     * @param {(v: string) => string} valueConverter
     */
    constructor(list, selectedValue, cssClassPrefix, valueConverter) {
      this.list_ = list;
      this.current_ = selectedValue;
      this.cssClassPrefix_ = cssClassPrefix;
      this.hrefCreater_ = () => '';
      this.valueConverter_ = valueConverter;
    }

ごちゃごちゃあるが、本質的には今どの種類か(this.current_)という一つの情報しかない。

いっそのことクラスじゃなくて関数にして毎回状態を渡して生成するみたいなダイナミックさを受け入れたらもっとシンプルになった気がしないでもない。

まあ今からはやらないけど。

もしくはステート管理ライブラリを導入するとなんかいい感じに解決できるのかもしれない(あんま良く知らない)

できたもの

https://yumetodo.github.io/kamioda20191018task/index2.html#!/total/extreme

image.png

アンケート

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

Javascriptのgetter setterメモ

getter/setterとは

以下のようにオブジェクトのpropertyにget/setを付けることにより、
プロパティの呼び出し時やプロパティへの値の代入時に関数が呼び出されるようになる。
正確には、getあるいはsetを付けるとpropetyの記述子が特別な「アクセサー記述子」になる。
(通常のpropertyはデータ記述子)

const hoge ={
 get hoge(){
    return "hoge";
 }
 set hoge(h) {
   console.log(h);
 }
}

hoge.hoge//hoge
hoge.hoge = "fuga"//consoleにfugaと出る

クロージャと組み合わせてオブジェクトのカプセル化やWrapperオブジェクトの作成に使える。

getterの挙動

getterの挙動は一見、通常のデータ記述子に対するアクセスするのと変わらない。
オブジェクト宣言中でgetterを定義した場合、通常アクセスの外、
スプレッド構文やfor...inループにも表れる。

getter、setter内で同名プロパティにアクセスした場合

getterやsetterはJavaなどの言語におけるgetterやsetterと異なり、
そのプロパティに紐づくもの自体をアクセサーに変える。
よって下記のようなコードは無限ループを引き起こす

get hoge(){
  return this.hoge;
}

getterやsetterを片方しか定義しなかった場合の挙動

getterしかないpropertyに値を代入しようとした場合には何も起こらない。
(そのプロパティが通常のデータ記述子型プロパティに変わったりはしない)
同様にsetterしかないプロパティに対して値を読みだそうとするとundefinedになる。

getterかsetterを一つでも定義した時点で、そのpropertyはアクセサー型のpropertyになるので、
未定義の側もデータ記述子型のように扱うことは出来ない。

消したい場合、更新したい場合

  • deleteを用いればpropertyごと消えるので、getter/setter共に消滅する deleteにはpropertyの種類がデータ記述かアクセサー記述かは関係ない
  • 上書きしたい場合はObject.definePropertyあるいはObject.definePropertiesを利用する。
  • 上記のdefineProperty系はプロパティの属性ごと書き換えることが可能なので、アクセサ型のpropertyの書き換えも可能。
  • かつてはObject.__defineGetter__とかdefineSetterもあったが非推奨になった
  • definePropertyを使うことで後からgetterやsetterの追加も可能。ただし、definePropertyをデフォルトオプションで使ってPropertyを追加すると、通常のプロパティ宣言とかなり挙動が異なる。(キーが列挙されない、書き換え不可になるなど)注意が必要
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Express+TypeScript だけで React を SSR する

Express+TypeScript だけで React を Server Side Rendering (SSR) してみます。Babel は使用しません。

TypeScript は JSX に対応しているので、JSX を変換するだけなら Babel は不要です。

ディレクトリ構成

.
├── app.ts
├── createEngine.ts
├── package.json
├── pages/
│   └── Hello.tsx
└── tsconfig.json

設定ファイル

package.json
{
  "name": "react-ssr-ts",
  "version": "1.0.0",
  "license": "CC0-1.0",
  "private": true,
  "scripts": {
    "dev": "ts-node-dev --no-notify app.ts",
    "build": "tsc",
    "start": "NODE_ENV=production node dist/app.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "react": "^16.11.0",
    "react-dom": "^16.11.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.1",
    "@types/react": "^16.9.9",
    "@types/react-dom": "^16.9.2",
    "ts-node-dev": "^1.0.0-pre.43",
    "typescript": "^3.6.4"
  }
}

ts-node-dev はファイルに変更があると自動で再起動してくれるツールです。ts-node と node-dev を掛け合わせたようなやつです。

start で NODE_ENV=production としていますが、その事情は後ほど。

tsconfig.json
{
  "compilerOptions": {
    "target": "es2019",
    "module": "commonjs",
    "jsx": "react",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  }
}

"jsx": "react" がミソです。tsc が JSX を React.createElement に置き換えてくれるので、普通に JS として実行可能になります。

ページ

pages/Hello.tsx
import React from 'react';

type HelloProps = {
  name: string;
};

const Hello: React.FC<HelloProps> = ({ name }) => (
  <h1>Hello, {name}</h1>
);

export default Hello;

ページは pages/ に置いていますが、app.ts の設定でディレクトリ名を自由に指定できます。

ページのコンポーネントは export default してください。

テンプレートエンジン

createEngine.ts
import React from 'react';
import ReactDOMServer from 'react-dom/server';

export interface EngineOptions {}

export const createEngine = (engineOptions?: EngineOptions) => {
  const renderFile = (path: string, options: object, callback: (e: any, rendered: string) => void): void => {
    const component = require(path).default as React.ComponentType<any>;
    const markup = ReactDOMServer.renderToStaticMarkup(
      React.createElement(component, options)
    );
    return callback(null, markup);
  };
  return renderFile;
};

tsx ファイルをテンプレートとして使うためのテンプレートエンジンです。渡された js または tsx ファイルを ReactDOMServer.renderToStaticMarkup で HTML の文字列に変換しています。

EngineOptions は特に使用していないので、createEngine でラップする必要はないのですが、将来の拡張用にとりあえずこうしています。

エントリポイント

app.ts
import express from 'express';
import { createEngine } from './createEngine';

const app = express();

const ext = app.get('env') === 'production' ? 'js' : 'tsx';
app.set('views', __dirname + '/pages');
app.set('view engine', ext);
app.engine(ext, createEngine());

app.get('/:name', (req, res) => {
  res.render('Hello', { name: req.params.name });
});

app.listen(8080, () => {
  console.log('> Ready on http://localhost:8080/');
});

先ほどのテンプレートエンジンを app.engine に登録します。ページのディレクトリ名を変えたい場合は、app.set('views', __dirname + '/pages') のところを変えてください。

view engine の値を js と tsx で分けている理由ですが、ts-node-dev で実行した場合 tsx が渡されてくる一方、tsc して node dist/app.js した場合 js が渡されてくるので、それぞれの場合でテンプレートの拡張子を変える必要があります。このあたりもう少しスマートにできないものか・・・

おわり

以上です。意外と簡単に React の SSR ができました。

普通は Next.js を使うのがいいと思います(Dynamic Routing で難がありますが)。

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

Express+TypeScript で React をテンプレートエンジンとして使用する

Express+TypeScript で Server Side Rendering (SSR) して、React をテンプレートエンジンとして使用してみます。Babel は使用しません。

TypeScript は JSX に対応しているので、JSX を変換するだけなら Babel は不要です。

ディレクトリ構成

.
├── app.ts
├── createEngine.ts
├── package.json
├── pages/
│   └── Hello.tsx
└── tsconfig.json

設定ファイル

package.json
{
  "name": "react-ssr-ts",
  "version": "1.0.0",
  "license": "CC0-1.0",
  "private": true,
  "scripts": {
    "dev": "ts-node-dev --no-notify app.ts",
    "build": "tsc",
    "start": "NODE_ENV=production node dist/app.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "react": "^16.11.0",
    "react-dom": "^16.11.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.1",
    "@types/react": "^16.9.9",
    "@types/react-dom": "^16.9.2",
    "ts-node-dev": "^1.0.0-pre.43",
    "typescript": "^3.6.4"
  }
}

ts-node-dev はファイルに変更があると自動で再起動してくれるツールです。ts-node と node-dev を掛け合わせたようなやつです。

start で NODE_ENV=production としていますが、その事情は後ほど。

tsconfig.json
{
  "compilerOptions": {
    "target": "es2019",
    "module": "commonjs",
    "jsx": "react",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  }
}

"jsx": "react" がミソです。tsc が JSX を React.createElement に置き換えてくれるので、普通に JS として実行可能になります。

ページ

pages/Hello.tsx
import React from 'react';

type HelloProps = {
  name: string;
};

const Hello: React.FC<HelloProps> = ({ name }) => (
  <h1>Hello, {name}</h1>
);

export default Hello;

ページは pages/ に置いていますが、app.ts の設定でディレクトリ名を自由に指定できます。

ページのコンポーネントは export default してください。

テンプレートエンジン

createEngine.ts
import React from 'react';
import ReactDOMServer from 'react-dom/server';

export interface EngineOptions {}

export const createEngine = (engineOptions?: EngineOptions) => {
  const renderFile = (path: string, options: object, callback: (e: any, rendered: string) => void): void => {
    const component = require(path).default as React.ComponentType<any>;
    const markup = ReactDOMServer.renderToStaticMarkup(
      React.createElement(component, options)
    );
    return callback(null, markup);
  };
  return renderFile;
};

tsx ファイルをテンプレートとして使うためのテンプレートエンジンです。渡された js または tsx ファイルを ReactDOMServer.renderToStaticMarkup で HTML の文字列に変換しています。

EngineOptions は特に使用していないので、createEngine でラップする必要はないのですが、将来の拡張用にとりあえずこうしています。

エントリポイント

app.ts
import express from 'express';
import { createEngine } from './createEngine';

const app = express();

const ext = app.get('env') === 'production' ? 'js' : 'tsx';
app.set('views', __dirname + '/pages');
app.set('view engine', ext);
app.engine(ext, createEngine());

app.get('/:name', (req, res) => {
  res.render('Hello', { name: req.params.name });
});

app.listen(8080, () => {
  console.log('> Ready on http://localhost:8080/');
});

先ほどのテンプレートエンジンを app.engine に登録します。ページのディレクトリ名を変えたい場合は、app.set('views', __dirname + '/pages') のところを変えてください。

view engine の値を js と tsx で分けている理由ですが、ts-node-dev で実行した場合 tsx が渡されてくる一方、tsc して node dist/app.js した場合 js が渡されてくるので、それぞれの場合でテンプレートの拡張子を変える必要があります。このあたりもう少しスマートにできないものか・・・

おわり

以上です。意外と簡単に React をテンプレートエンジンとして使うことができました。

普通は Next.js を使うのがいいと思います(Dynamic Routing で難がありますが)。

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

Nuxt.jsで異なるコンポーネントから共通で利用できる関数を定義する(inject編)

異なるコンポーネントから共通で利用できる関数を定義したかったので、試してみました。

以前の記事ではmixinを使って実現しましたが、この方法では asyncDatafetch 内で共通の関数を利用できませんでした。
そのため、今回はinjectを使って実現しています。

この方法では、 asyncDatafetch 等のSSR時も関数を利用できるため、こちらの方が良いかもしれません。

共通の関数を定義する

plugins/ 内に utils.js というファイルを作成し、ここに関数を定義していきます。
今回は例として hoge()fuga() という、2つの関数を定義するものとします。

使用する際は this.$huga() のように記述します。

plugins/utils.js
const hoge = () => {
  return "hoge";
}

const fuga = () => {
  return "fuga";
}

export default ({}, inject) => {
  inject('hoge', hoge);
  inject('fuga', fuga);
}

nuxt.config.jsに設定を追加

nuxt.config.js に設定を追加します。

nuxt.config.js
plugins: [
  '@/plugins/utils'
],

コンポーネントからの利用

asyncDatafetch 内で使用したい場合は、 context から呼び出します。
関数名の前に $ が付きます。

pages/index.vue
<script>
export default {
  asyncData(context){
    context.app.$hoge();  // ここで呼び出し
  }
}
</script>

SSR時以外は、 this.$hoge() のように呼び出します。

pages/index.vue
<script>
export default {
  methods: {
    hoge() {
      return this.$hoge();  // ここで呼び出し
    }
  }
}
</script>

便利!

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

Cognos Analytics 11.1.4 新機能紹介

Cognos Analytics 11.1.4 新機能紹介

2019/10/15にCognos Analytics 11.1.4がリリースされました。
主だった新機能や変更箇所をご紹介したいと思います。

ポータル画面の変更

こんな感じのポータル画面に変更されました。
赤枠のバナーには、何かしらお知らせ事項を表示する事ができるようになりました。
また、「手引きのカタログ」や「サンプル」がここで参照できます。
001.PNG

「「ようこそ」ページを表示」をOFFにすると、先ほどの「手引きのカタログ」や「サンプル」や右上の画像が非表示になりました。ユーザー毎に自分で選択できます。
また、赤枠の「リストView」を選択すると、履歴表示がリストで表示されます。
002.PNG

お知らせメッセージは、この手順で変更できます。
Customizing a message in the alerts banner
https://www.ibm.com/support/knowledgecenter/SSEP7J_11.1.0/com.ibm.swg.ba.cognos.ag_manage.doc/c_ag_manage_advanced_settings.html#t_ca_manage_customize_alert

ダッシュボードの新機能

D3.jsなどの、世の中一般のJavaScript技術を使ってチャートを自作してCognosに取り込めるようになりました。チャートの自由度が大きく広がりますね。
003.PNG

詳細手順はこちらで確認できます。
Developing custom visualizations
https://www.ibm.com/support/knowledgecenter/SSEP7J_11.1.0/com.ibm.swg.ba.cognos.dg_custom_vis.doc/ca_custom_visualizations_intro.html

実績のみのデータから、未来を予測する線を引けるようになりました。
赤枠の箇所のように、点線で表現され、確度の範囲も表示されます。
折れ線グラフだけではなく、棒グラフでも可能です。
赤枠のアイコンから、表示・非表示だけでなく、予測のカスタム設定が可能です。
004.PNG

ウォーターフォールチャートが使用可能となりました。
005.PNG

KPIチャートの使用も可能です。
二つの数値データの比較結果の表示や、「↑」のマークの変更などのカスタマイズも可能です。
006.PNG

クロス集計も使いやすくなっています。
列幅をマウス操作で調整可能となり、新規にデータアイテムを追加配置時に、図の赤枠のように水色線がドロップ位置を示すようになりました。
007.PNG

閲覧ビューの新機能

閲覧ビューの比較の機能がより使いやすくなっています。
一つのビューを元に「比較」を開始した時に、どのように比較を行うかのパターンを選択可能になりました。
009.PNG

横ぐしの比較線で比較した時に、線にぎりぎり引っかかるデータが、赤枠のように左右のビューで表示されています。
008.PNG

閲覧ビューを新規に開いた際にデータの関係性が表示されますが、この関係性を計算される対象のデータアイテムや関係度の強さなどを選択し、より柔軟に計算が行えるようになりました。
010.PNG

レポートの新機能

チャートの「凡例」の置き場所が画像のように、チャートの上部に置けるなど柔軟になりました。
011.PNG

また、ダッシュボード同様に、D3.jsで作成した自作のチャートをレポートでも使用できるようになりました。

さらに、ダッシュボードのみで可能だった、Jupyter Notebookでの出力結果を、レポートでもデータソースとして使用可能となりました。

データモジュールの新機能

結合のキーを「=」だけでなく、比較演算子を選択可能となり、範囲を指定した結合が可能となりました。Framework Managerには存在した機能ですね。
012.PNG

データのグループを作成時に、数値データを以下のように数値の範囲でグルーピングする事も、
013.PNG
文字列データとして、特定の値を個々に指定してグループを作成できるようになりました。
014.PNG

管理の新機能

ポータルでご紹介した、通知メッセージを指定できる機能と、
「Weather Company」へデータサーバーで接続できるようになった、というのが管理の機能の主な新機能ですね。

参考

https://www.ibm.com/support/knowledgecenter/SSEP7J_11.1.0/com.ibm.swg.ba.cognos.ca_new.doc/c_ca_nf_11_1_4_container.html

以上です。
個人的にはD3.jsでのチャート自作が注目で、問い合わせありそうなので、しっかり勉強しとかんとな~と思っています。

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

Vue.jsについての基礎(Nuxt.js)

はじめに

おはようございます。こんにちは。こんばんは。
ワタタクです。
今回はNuxt.jsについて見けいけたらいいなと書いています。

Nuxt.jsとは?

公式サイトに詳しく書いていますが、Vue.js アプリケーションを構築するためのフレームワークです。
サーバーサイドレンダリングするアプリケーションの開発のために必要な設定があらかじめセットされているのが特徴です。

サーバーサイドレンダリングとは?

Vue.jsなどでDOMの構築をクライアント側で行うと、サイトにアクセスした際に一瞬画面が表示されない状態になります。
サーバー側で初期状態のDOMレンダリングを完了した状態で返すことで、サイトにアクセスした際にすぐに画面を描画することができます。
これをサーバーサイドレンダリング(SSR)と呼びます。

Nuxt.jsを使ってみよう

では、早速使ってみましょう。
今回も「vue-cli」を使います。
「vue-cli」がインストールされていない場合は、以下のコマンドを実行してvue-cliをインストールしてください。

$ npm install -g vue-cli

vue-cliがインストールされたら、以下のコマンドを実行すると、Nuxtのテンプレートプロジェクトが作成されます。
ここでは「sample」というプロジェクト名で作成します。

$ vue init nuxt-community/starter-template sample

sampleディレクトリに、プロジェクトが作成されます。
以下のコマンドを実行して、必要なライブラリをインストールします。

$ cd sample
$ npm install

開発サーバーを起動します。

$ npm run dev

localhost:3000でNuxtのサンプルアプリが起動します。
sample.png

ページの追加

従来Vue.jsでSPA作るときはVue-Routerを使っていました。
その際、componentを作成して、Vue-RouterでURIとcomponentをマッチングするための定義を記述していましたが、Nuxt.jsでは、pagesディレクトリにページ用のcomponentを配置します。
pagesディレクトリに*.vueファイルを配置することで、自動でルーティングの定義を行ってくれます。
例えば、
/pages/users/index.vueを作成するだけで、自動的に/usersのURIとマッチングしてくれて、
http://localhost:3000/users
にアクセスすると、作成した/pages/users/index.vueのページが表示されます。

[TIPS]

ビューにあたる部分は/components /layouts /pages となる
このディレクリ名は変更不可
- components:layoutspagesに使用できるコンポーネントファイル
- layouts:pages内のVueファイルが表示される(デフォルト以外にも作れる)
- pages:ページごとのVueファイル、ここで作ったディレクトリ通りにルーティングが作られる

ページ間を遷移するためには <nuxt-link> コンポーネントの使用を推奨します。
あとはVue.jsの知識があれば、だいたいの事は出来ます。
Nuxt.jsを触る前に一通りVue.jsを勉強しといた方がいいかもしれませんね。
Vue.jsについての基礎(インストール〜基本構文)←勉強の参考までに

まとめ

従来のVue.jsでSPAを作る際は、自由度があるが故に、自分でVue-Routerをもってきたり、Vuexをもってきたり、最初のプロジェクトを作る時にはいろいろ手探りしながらやっていました。
それらがNuxt.jsには最初から含まれているので、フロントエンド開発に慣れていない方でもすごくとっつきやすいのでこっちの方が良いかもですねw。ルーティングが自動で生成されるのも手軽で良いですね。

最後に

最後まで読んでいただきありがとうございました。
もし間違い等、アドバイス、ご指摘等有れば教えていただけたら幸いです。
次回はVuex(store)の使い方をやって行こうかな?

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

getElementByIdの引数は式展開できる!

a=1; getElementById('id${a}'); (※「'」→「`」に変えてください)

JavaScriptでは、文字列をバッククオートで囲み、さらにその中で${変数}とすれば式展開ができます。(こちらの記事でとても分かりやすく書かれています)

そしてこれは、直接出力する文字列だけでなく、getElementByIdの引数として渡すid名でも同じことができます

たとえば下のコードでは、"id1"というid名を「id${a}」(a=1)という形で取得しています。

cake.html
<html>
<body>

<input type="text" id="id1">

<script>
let a = 1;
document.getElementById(`id${a}`).value = "ケーキ";
</script>

</body>
</html>

実行結果は
ケーキ.png
ちゃんと取得できています!

注意:「"(ダブルクォート)」ではなく「`(バッククォート)」で囲むこと!

getElementByIdでは基本的に、
getElementById("id1")のように引数をダブルクォートで囲みます。
しかしid名の中で式展開をしたい場合は、ダブルクォートではなくShift@ボタンの同時押しで出せる「`(バッククォート)」を絶対使わなければいけません。
また、Shift7同時押しの「'(シングルクォート)」は形が似ていますが、これでは上手くいきませんのでそこもご注意を!

以上です。
もしかしたら皆さんにとっては常識なことかもしれないのですが、「出力する文字列ではなくコード中で使うid名でも式展開が効く」ことを作業中に知って嬉しかったので投稿してみました。

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

「会社法人等番号」を「法人番号」へと変換するJavaScriptのコード

コード

12桁の会社法人等番号の左端に一桁のチェックディジットを付加して13桁の法人番号を生成するためのJavaScript (ES6)コード

// 12桁の会社法人等番号,0から始まることもあるので文字列で与えることにする
const n12 = "000012050002";

// 13桁の法人番号
const n13 =  9 - n12.split("").map(c => Number(c)).map((d, i) => i % 2 === 0 ? d * 2 : d)
  .reduce((acc, cur) => acc + cur, 0) % 9 + n12; // -> "7000012050002"

参考

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

JavaScript 覚えておくべき注意点!(8) 繰り返し処理について学ぶ while文 do...while文 for文 ジャンケンゲームで連勝回数を表示する

★ 繰り返し処理とは何かを知りましょう

プログラミングの世界では、同じ命令を繰り返し実行したい場面がよくあります。
こうした処理を「繰り返し処理」または「ループ処理」「反復処理」と言います。

繰り返し処理とは

簡単に言えば1万人にメールを送信する処理です
特に、繰り返しを指示するプログラムが書ければ、コードがぐっと短くなり、読みやすくなります。

kurikaeshi1.js
繰り返す (1万人になるまで) {
     メールアドレスを送信先にセットする
     メールを送信する
}

メールの送信先は毎回異なるので、繰り返し回数に応じて異なるメールアドレスを取得する処理も同時に行っております。

繰り返し処理で辞書を引く

繰り返し処理で、辞書を引く手間を減らすことができます。
紙の国語辞典をイメージします。調べる言葉を決めたら、先ず辞書全体の真ん中あたりを開いてみます。辞書は50音順に並んでいるので、調べたい言葉が開いたページの後ろにあるか前にあるか、判断することができます。ここで調べたい言葉がある、残りのページ全体の真ん中あたりを開きます。これを繰り返します。
このように、日常的な作業も、繰り返し処理で表現することができます。

kurikaesi2.js
繰り返す (見つかるまで) {
     残されたページの真ん中を開く
     調べたい言葉の載っていない半分のページを、検索対象から外す
}

繰り返し処理の終わり方

繰り返し処理では、処理を一回行うたびに、継続する条件を確認して、条件を満たさなくなったら、処理を終了するように出来ております。

★ 条件に応じた繰り返しを書いてみる

while文の書き方

kurikaesi3.js
/* while文の書き方 */
while (year <= 2000) {
    行いたい処理
}

// プログラムのイメージ
while (汚れが消えるまで) {
    拭く;
}

whileの後の( )で指定した条件式が真であるかぎりブロック{ }内の処理を繰り返します。while文で繰り返し処理を終了するには、繰り返し処理が行われていく中で条件式が「偽:false」になる必要があります。

最低1回は繰り返す do...while文

kurikaesi4.js
do {
    行いたい処理
} while (year <= 2000);

while文では、条件式が最初から「偽」だった場合、一度も繰り返し処理が実行されませんが、最低でも1回以上繰り返したいときには「do...while文」を使用します。do...while文では最初に一度{ }内の処理を実行した後に、さらに処理を繰り返すかどうか、条件式で判断します。

無限ループには要注意

whileでは条件式が偽にならなければ繰り返し処理が終了しません。
絶対に偽にならない条件式を指定してしまい、繰り返し処理が終了しない状態を「無限ループ」といいます。while文を使用するときは無限ループにならないように、繰り返し処理の中で条件が「偽:false」に変化するようにプログラムを記述する必要があります。

★ 例 オリンピックイヤーを表示してみる

1.while文で繰り返し処理を書く

①年数を記憶する変数yearを定義して開始年2000年を代入します。
②次に2001以下を条件とするwhile文を書き、ブロック内でyearに2を加えます。

kurikaesi5.js
var year = 2000; // ........①
while (year <= 2100) {
  year = year + 2; // ......②
}

2.夏季と冬季の出し分け処理を書く

①夏季オリンピックは4の倍数、冬季オリンピックはそれ以外のときに開催されるので、if文で出し分けの処理を記述します。
if文の条件式「year % 4」はyearを4で割ったときの余り(剰余)を表わします。夏季オリンピックが開催される4の倍数の年なら値が0となり、if文の条件式としては偽と判定され、冬のオリンピックのときには真と判定されます。

kurikaesi6.js
var year = 2000;
while (year <= 2100) {
  if (year % 4) {
    console.log(year + ':冬季オリンピック');
  } else {
    console.log(year + ':夏季オリンピック');
  } // ........................................①
  year = year + 2;
}

3.プログラムの動作を確認する

ブラウザの設定内のその他のツール、デベロッパーツールのconsoleにプログラムをコピペし、enterをして動作確認をしてみましょう。

★ 回数の決まった繰り返しを書く

for文の書き方

while文と比較して、回数の決まった繰り返し処理を書くのに便利なのがfor文です。
一般的なfor文では、カウント用変数(例文中の変数「var i = 1;」)を用意して、その値を繰り返し処理を行うごとに1ずつカウントアップ(例文中の「i++」)します。この「++」はインクリメント演算子と呼ばれます。

kurikaesi7.js
for(var i = 1; i <= 100; i++) {
    行いたい処理
}
/* var i = 1 は、カウント用変数の初期化、
i <= 100 は繰り返し条件式、
i++ はカウントを増やす式 */

1.for文で繰り返し処理を書く

さっそくfor文を試してみましょう。
単に100回こんにちはを表示するだけでは味気ないので、現在の繰り返し回数も合わせて表示します。

kurikaesi8.js
for(var i = 1; i <= 100; i++) {
    console.log(i + '回目の「こんにちは」');
}

2.プログラムを実行してみましょう

★ ジャンケンゲームで連勝回数を表示しましょう

完成をイメージする

1.janken()に戻り値を設定する

前回、JavaScript 覚えておくべき注意点!(7)で作成したジャンケンゲームは関数「janken()」で呼び出すことで使用できましたが、janken()には戻り値が設定されていないので、そのまま利用しても勝負に勝ったのか負けたのか判断することができません。そこでまずはjanken()に勝敗が分かる戻り値を設定します。勝敗の結果を関数「getResult()」で所得し、その値をreturn値に設定します。①
またjanken()を実行している関数は繰り返し処理の中で書き直すので、一旦消しておきましょう。②

kurikaesi9.js
function janken () {

 ...中略...

    var com = getComHand();
    alert(getResultMsg(com, hum));
    return getResult(com, hum); // ..................①
  }
}
janken(); // ........................................②

2.while文で繰り返し処理を記述します

続いて、繰り返し処理を記述します。
まずは連勝数を記載するための変数「win」と、負けたかどうかを記憶するための変数「isLose」を宣言します。①
次に、while文で繰り返し処理を表わし、先に宣言した変数「isLose」が真にならない限り、繰り返し処理を続けるように条件式を書きます。②

kurikaesi9.js
var win = 0;
var isLose = false; // ...............①
while (!isLose) {

} // .................................②

3.勝敗に応じて条件分岐を記述する

そして最後に、繰り返し処理の中身を記述していきます。繰り返し処理では、まずjanken()を実行して ① 、
その結果に応じて条件分岐を行います。
あいこの場合は、 continue文でその後の処理をスキップして、次の繰り返しに移ります。②
勝の場合には、++演算子で変数winを1増やし、これまでの連勝数を表示して、次の繰り返しに移ります。③
それ以外は負けの場合になるので、連勝数を表示して、繰り返し処理が終了するよう「isLose」の変数値をtureに変更します。④

kurikaesi10.js
while (!isLose) {
    var result = janken(); // .....................................①
    if (result === '結果はあいこでした。') {
        continue;
    } //...........................................................②
    if (result === '勝ちました。') {
        win++;
    alert('ただいま「' + win + '」勝でした。');
        continue;
    } // ..........................................................③
    alert('連勝はストップです。記録は「' + win + '」勝でした。');
    isLose = true;
} // ..............................................................④

★ まとめ

kurikaesi11.js
function janken () {
  /* 変数定義 ************************/
  // ジャンケンの手の番号を設定
  var GU    = 1;
  var CHOKI = 2;
  var PA    = 3;

  /* 関数定義 ************************/
  // 人間に手を入力してもらう機能
  function getHumHand() {
    var hum = prompt('半角数字で1~3の数字を入力してください。\n\n' + GU + ':グー\n' + CHOKI + ':チョキ\n' + PA + ':パー');
    hum = parseInt(hum, 10);
    if (hum !== GU && hum !== CHOKI && hum !== PA) {
      return undefined;
    } else {
      return hum;
    }
  }

  // コンピュータの手を決める
  function getComHand() {
    return Math.floor(Math.random() * 3) + 1;
  }

  // コンピュータの手の名前を取得
  function getHandName(num) {
    switch (num) {
      case GU:
      return 'グー';
      case CHOKI:
      return 'チョキ';
      case PA:
      return 'パー';
    }
  }

  // 結果の判定
  function getResult(com, hum) {
    if (hum === com) {
      return '結果はあいこでした。';
    } else if ((com === GU && hum === PA) || (com === CHOKI && hum === GU) || (com === PA && hum === CHOKI)) {
      return '勝ちました。';
    } else {
      return '負けました。';
    }
  }

  // 最終的な結果のメッセージ
  function getResultMsg(com, hum) {
    return getResult(com, hum) + 'コンピュータの出した手は「' + getHandName(com) + '」でした';
  }

  /* 実行する処理 ************************/
  var hum = getHumHand();
  if (!hum) {
    alert('入力値をうまく認識できませんでした。ブラウザを再読込すると、もう一度挑戦できます。');
  } else {
    var com = getComHand();
    alert(getResultMsg(com, hum));
    return getResult(com, hum);
  }
}
var win = 0;
var isLose = false;
while (!isLose) {
    var result = janken();
    if (result === '結果はあいこでした。') {
        continue;
    }
    if (result === '勝ちました。') {
        win++;
    alert('ただいま「' + win + '」勝でした。');
        continue;
    }
    alert('連勝はストップです。記録は「' + win + '」勝でした。');
    isLose = true;
}
janken();

ワンポイント 返し処理の中断(break文)

kurikaesi12.js
// 返し処理の中断(break文)
while (汚れが消えていない) {
    拭く;
    if (効果が無い) {
    break; // ...................繰り返し処理を中断
    }
}

ワンポイント 繰り返しをスキップする(continue文)

kurikaesi13.js
// 繰り返しをスキップする(continue文)
while (汚れが消えていない) {
    if (摩擦で熱い) {
        10分間休む;
    continue; // ................while文の先頭にジャンプ
    }
    拭く;
}

参照: いちばんやさしいJavaScriptの教本

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

JavaScriptの数値チェックについてハマったのでメモ

はじめに

こんにちは、普段は業務で求人系サービスの開発や社内向けツールの開発を行なっている@taku-0728です。
今回はJavaScriptの数値チェックについてまとめました。
調べると多くの記事がヒットしますが、true/falseを数値として扱ったりする記事を見つけたり色々なメソッドを見つけたりして混乱したので自分用メモとしてまとめます。

忙しい人向け

細かいことはいいから結論が知りたい忙しい人用に数値チェックについてまとめます。

isNaN(val) Number.isNaN(val) isFinite(val) Number.isFinite(val) Number.isInteger(val)
val=-1 false false true true true
val=0 false false true true true
val=1 false false true true true
val='-1' false false true false false
val='0' false false true false false
val='1' false false true false false
val=-0.1 false false true true false
val=0.1 false false true true false
val='-0.1' false false true false false
val='0.1' false false true false false
val=012 false false true true true
val='012' false false true false false
val=0x12 false false true true true
val='0x12' false false true false false
val=true false false true false false
val=false false false true false false
val='true' true false false false false
val='false' true false false false false
val=undefined true false false false false
val='undefined' true false false false false
val=null false false true false false
val='null' true false false false false
val=NaN true true false false false
val='NaN' true false false false false

解説

isNaN()

公式リファレンスより

isNaN() 関数は引数が NaN (非数)かどうかを判定します。
isNaN 関数の引数が 数値型ではない場合、その値はまず数へと型強制されます。

つまり、数字でない場合trueを返すということですが、引数が数値型ではない場合、その値はまず数値型へと型変換された上で比較される点に注意が必要です。
そのため上の表にもまとめていますが、以下のような条件式は全てtrueになってしまいます。

isNaN('true')
isNaN('false')
isNaN(undefined)
isNaN('undefined')
isNaN('null')

上記のような暗黙な型変換を許容しない場合、Number.isNaN()を使うべきです。

Number.isNaN()

公式リファレンスより

Number.isNaN() メソッドは、渡された値が NaN であり、かつその型が Number であるかどうかを判断します。元となるグローバルの isNaN() よりも堅牢な版です。

上記isNaN()とは暗黙な型変換を行わない点に違いがあります。
そのためNumber.isNaN()メソッドは下記の場合のみtrueを返します。

Number.isNaN(NaN)

isFinite()

公式リファレンスより

グローバル関数 isFinite() は渡された値が有限数かどうかを判定します。必要に応じて、引数はまず数値へと変換されます。

ただし、isNaN()と同様引数が数値型でなかった場合、暗黙な型変換が行われるため下記のような条件式は全てtrueになっていまいます。

isFinite(true)
isFinite(false)
isFinite(null)

true/falseやnullを全て1や0として変換するため上記は全てtrueになってしまいます。
これらを許容しない場合、Number.isFinite()を使うべきです。

Number.isFinite()

公式リファレンスより

Number.isFinite() メソッドは、引数として与えた数が有限数かどうかの真偽値を返します。

上記Number.isNaNメソッドと同様暗黙な型変換を行いません。

Number.isInteger()

公式リファレンスより

Number.isInteger() メソッドは渡された値が整数かどうかを判定します。

Number.isFinite()と似ていますが、Number.isInteger()は整数のみtrueとするため引数に小数を渡すとfalseになります。
8進数や16進数でも整数ならばtrueになります。

正規表現

一番上の表には記載していませんが、正規表現を用いて数値チェックを行うこともできます。

// 0以上の整数のみ
function isNumber(val){
  var regexp = new RegExp(/^[0-9]+(\.[0-9]+)?$/);
  return regexp.test(val);
}

目的に応じてRegExpの中身を書き換えてください

// 整数 (- のみ許容)
var regexp = new RegExp(/^[-]?([1-9]\d*|0)$/);

// 整数 (+, – 許容)
var regexp = new RegExp(/^[+,-]?([1-9]\d*|0)$/);

// 符号なし小数
var regexp = new RegExp(/^([1-9]\d*|0)(\.\d+)?$/);

// 符号あり小数 (- のみ許容)
var regexp = new RegExp(/^[-]?([1-9]\d*|0)(\.\d+)?$/);

// 符号あり小数 (+, – 許容)
var regexp = new RegExp(/^[+,-]?([1-9]\d*|0)(\.\d+)?$/);

結論

ここまでまとめて結局どれを使えばいいの?と思われるかもしれませんが、これといった正解がないのが正直な感想です。
数値型のチェックをしたいのであれば小数を許容するかに応じてNumber.isFinite()か、Number.isInteger()を使うのが適切だと思います。
文字列として渡ってきた数値も許容したい場合、引数を数値型に変換したうえでNumber.isFinite()を使うか、正規表現を使うのが適切だと思います。
他にいい方法があればコメント等で教えていただければ助かります。

参考

数値かどうか、型を判定する isNumber と、テスト駆動開発
正規表現を用いて JavaScript で数値チェックを行う方法

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

三項演算子を使って、数値の往復処理をちょっと綺麗に書く

まえがき

ある数値が下限に達したら増え、上限に達したら減るという処理を綺麗に書けたらいいなという記事です。
軽い内容なので休憩程度に読んでいただけたらと思います。

コード

let targetNum = 0
let status = 0
const max = 100
const min = -100

status === 0
  ? targetNum === max
    ? targetNum++
    : status = 1
  : targetNum === min
    ? targetNum--
    : status = 0

//if文で書いた場合こうなる
if (status === 0) {
  if (targetNum === max) {
    targetNum++
  } else {
    status = 1
  }
} else {
  if (targetNum === min) {
    targetNum-- 
  } else {
    status = 0
  }
}

説明

見ての通り、「targetNumが、max・minに達していなければ増減させ、達していればstatusを切り替える」というシンプルな処理を、三項演算子のネストを用いて書いています。(蛇足ですが個人的には、ネスト1階層までなら実用的かなと考えています)

今回はわかりやすく整数を使い、上限値、下限値を超える想定もしていないコードですが、条件式の部分をいじれば色々応用は効くかと思います。

それでは!

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

コードフォーマッターの導入

コードフォーマッターとは

  • ソースコードを整形してくれるツール
  • イケてる環境は何かしら導入されてるはず

導入することの利点

  • レビュー時に細かい指摘をしなくて済む
  • ソースコードの品質を最低限担保することが出来る

この記事で紹介しない事

  • babelnode-sass 等のトランスパイルの環境には触れません

完成形

最終的な内容はこちらのリポジトリにあります

リポジトリへのリンク

紹介するフォーマッターの一覧

導入

作業ディレクトリの作成と移動

$ mkdir lint-demo && cd lint-demo

ディレクトリの初期化

$ npm init -y

ライブラリのインストール

$ yarn add -D prettier eslint stylelint

設定ファイルの追加

設定ファイルが無いとLintタスクの実行が出来ないので設定ファイルの追加を行う

Prettier

プロジェクトのルートディレクトリで.prettierrcファイルを追加

Prettierの設定可能な項目

以下は設定の一例

{
  "singleQuote": true,
  "trailingComma": "all",
  "htmlWhitespaceSensitivity": "css",
  "printWidth": 120
}

ESLint

Parserを追加

$ yarn add -D babel-eslint

Prettier と併用する為に以下のpackagesを追加

$ yarn add -D eslint-plugin-prettier eslint-config-prettier

Google の推奨設定を利用する為に下記のpackageを追加

$ yarn add -D eslint-config-google

ESLintの設定ファイルはpackage.jsonに追加可能なので以下の用に記載

package.json
{
  "eslintConfig": {
    "parser": "babel-eslint",
    "env": {
      "browser": true
    },
    "extends": [
      "google",
      "plugin:prettier/recommended"
    ]
  }
}

stylelint

Prettier との併用の為に下記のpackageを追加

$ yarn add -D stylelint-config-prettier

SCSS で利用出来るように下記のpackagesを追加

$ yarn add -D stylelint-scss stylelint-config-recommended-scss

スタイルのプロパティの並べ替えを行えるように以下を追加

$ yarn add -D stylelint-order stylelint-config-rational-order

displayによって無効になるプロパティを検知する為に、以下のpluginを追加

$ yarn add -D stylelint-declaration-block-no-ignored-properties

以下の用にpackage.jsonに追加すると設定を読み込んでくれる

package.json
{
  "stylelint": {
    "plugins": [
      "stylelint-declaration-block-no-ignored-properties"
    ],
    "extends": [
      "stylelint-config-rational-order",
      "stylelint-config-recommended-scss",
      "stylelint-config-prettier"
    ],
    "rules": {
      "plugin/declaration-block-no-ignored-properties": true
    }
  }
}

実行環境の構築

Lintの実行にはnpm scriptsを利用する

Lint用にファイルを追加

# ディレクトリの作成
$ mkdir -p src/{scss,html,js}
# 空のファイルを追加
$ touch src/{html/index.html,js/main.js,scss/main.scss}

Git hooks でLintのタスクが実行される用に以下のパッケージを導入

$ yarn add -D husky lint-staged

package.json に以下を追加

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "src/html/**/*.html": "prettier --write",
    "src/js/**/*.js": "eslint --fix",
    "src/scss/**/*.scss": "stylelint --fix"
  }
}

これでGitのコミットを実行すると、ファイルに合わせてLintのタスクが自動で実行されます。

レガシー環境を撲滅する参考にご利用下さい :muscle:

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

【jQuery】snowfallプラグインで複数画像対応

画面に雪を降らせることができる、JQueryプラグイン「JQuery-Snowfall」は、
簡単設置ができるが、デフォルトでは降らせる画像を複数設定できない。
羽を降らせる演出を行うために、複数の種類の羽画像を用いるためのカスタマイズを行った

使用するプラグイン

github

https://github.com/loktar00/JQuery-Snowfall

詳しい説明

http://www.north-geek.com/entry/jquery-snow

カスタマイズ

2011年にすでに詳しい方法が紹介されている。参考にさせていただきました。
https://wispblog.tree-web.net/data/1/page_1_1951.html

手段は参考サイトと同じ

やりたいこと

呼び出し側のimageオプションに、複数のパスを記述できるようにし、その複数画像をランダムに配置する。

ランダムな数値を呼び出す関数設置(参考そのまま)

// Randam Array Tree-Web custom 2011/10/29

function randArray(_str){
   var i = _str.length;
   while (i) {
      var y = Math.floor(Math.random()*i);
      var t = _str[--i];
      _str[i] = _str[y];
      _str[y] = t;
   }
   return _str;
}

元記事では、

「//random fuction for generating random vals」と書かれているソースの下

という表記でしたが、プラグインが新しくなったのか、その箇所は消えていたので、
// Snow flake object の上に配置しました。(123行目あたり)

プログラム修正

if(options.image){
                    flakeMarkup = document.createElement("img");
                    flakeMarkup.src = options.image; //←この行
                }else{
                    flakeMarkup = document.createElement("div");
                    $(flakeMarkup).css({'background' : options.flakeColor});
                }

flakeMarkup.src = options.image; を編集します。

if(options.image){
                    flakeMarkup = document.createElement("img");
                    flakeMarkup.src = randArray(options.image)[0];
                }else{
                    flakeMarkup = document.createElement("div");
                    $(flakeMarkup).css({'background' : options.flakeColor});
                }

呼び出し/設定

<script type='text/javascript'>
    $(document).ready(function () {
        $(document).snowfall({
            image : [
                '/sp/common/images/hane.png',
                '/sp/common/images/hane2.png',
                '/sp/common/images/hane3.png',
                '/sp/common/images/hane4.png'
            ],
            flakeCount: 35,
            maxSpeed: 2,
            minSpeed: 1,
            maxSize: 150,
            minSize: 80,
            shadow : false
        });
    });
</script>

まとめ

参考にさせていただいたサイトをただ参考にしただけですが、
汎用性のある使いやすいプラグインなので、これからも要所で使えればと思います。
iOS13 アンドロイド端末でも問題なく動作しました。

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

JavaScriptdeユーザー定義関数の使い方

まず、関数とは?

何らかの処理を一つにまとめて、
関数名で呼び出せるようにしたもの

作成方法

funcion 関数名 (引数1,引数2,....)
{
 <処理>
 return 戻り値;
}

  • 関数は英語で[function]
  • 引数はカンマで区切る
  • 関数を行った時の結果の戻り値をreturnで指定する
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
 "http://www.w3.org/TR/html4/loose.dtd">
<html>
...
... 
<title>ユーザー定義関数の作成</title>
<script type="text/javascript">

function priceOfFruit(unitPrice,count){
    var fruit;
    fruit = unitPrice * count;
    return fruit;
}

</script>
</head>

※ユーザー定義関数は好きな所で定義することが出来るけども
head関数に定義することで分かりやすくなる。

...
...

<script type="text/javascript">

    var fruit1 priceOfFruit= (100, 5);
    document.write(fruit1,"個</br>")

</script>
</p>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML,JavaScriptについて(自分用メモ)

注意

この記事は、僕Marnyがhtml、javascriptを勉強する際のメモ代わりです。初心者なりにいろんなことをメモしていきます

HTML編

タイトル設定

<!DOCTYPE html>
<title>

</title>

テキスト記入

<!DOCTYPE html>
<body>

</body>

JavaScript編

htmlにjsを組み込む

<script>
ここにいろいろ書く
</script>

ダイアログボックスで表示

alert("表示させるメッセージ");
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript 初歩知識

記事を載せるにあたり

JavaScriptを学習中で、講義を聞いて個人的にまとめた内容になります。不明な点や間違いがある場合がありますので、お気づきの方はコメントしていただけると幸いです。

JavaScriptの構図と仕組み

  • 先頭は小文字、続く単語一つ一つの頭を大文字にする。

JavaScriptのプログラムが動くきっかけ

  1. 組込まれた時

    <script src="js/main.js"></script>
    
  2. html文書が読み込まれた後(エレメント:ボタンなどのこと)

    • onload時
    window.onload = function (){
    
    alert("ok");
    }
    
  3. イベント発生時

    • クリック、キーダウン、フォーカス
    function 名前(){
        <input type="button" onClick="名前()" />
    }
    

    更に詳しく解説

組込まれた時

  • 変数宣言の仕方

    var 変数名[=];
    
  • 配列宣言(値を入れない場合)

    var 配列名=new Array(個数);
    
  • 配列宣言(値を入れる場合)

    var 配列名=[,,・・・・];
    
  • サンプルaという配列を10個

    var a=new Array(10);
    var a=[0,0,0,0,0,0,0,0,0,0];
    

    onload時

  • htmlの文書の中

    <input type="text" id="id名" />
    
  • TextBoxの値の取得

    変数名=document.getElementById("id名").value;
    //valueはtextでもidでもok
    
  • TextBoxに値を設定

    document.getElementById("id名").value=;
    //値は変数でもok。文字列の囲み方は縛りがない。
    

    エレメント

    テキストボックス     id = "t1"
    テキストボックス     id = "t2"
    ボタン         id = "btn"
           onClick = "copy()"
    変数
    val
    

構文してみる

  • 組込まれた時

    var val;
    
  • onload時

    document.getElementById("t1").value = "";
    document.getElementById("t2").value = "";
    //F5で更新したときは、クリアはされない。
    
  • 記述方法1

    function copy(){
        val=document.getElementById("t1").value;
        //t1のテキストボックスの中身がvalに代入
        document.getElementById("t2").value = val;
        //t2に代入
    }
    
  • 記述方法2

    function copy(){
    document.getElementById("t2").value = document.getElementById("t1").value;
    }
    

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

Javascript 初歩知識

記事を載せるにあたり

Javascriptを学習中で、講義を聞いて個人的にまとめた内容になります。不明な点や間違いがある場合がありますので、お気づきの方はコメントしていただけると幸いです。

Javascriptの構図と仕組み

  • 先頭は小文字、続く単語一つ一つの頭を大文字にする。

Javascriptのプログラムが動くきっかけ

  1. 組込まれた時

    <script src="js/main.js"></script>
    
  2. html文書が読み込まれた後(エレメント:ボタンなどのこと)

    • onload時
    window.onload = function (){
    
    alert("ok");
    }
    
  3. イベント発生時

    • クリック、キーダウン、フォーカス
    function 名前(){
        <input type="button" onClick="名前()" />
    }
    

    更に詳しく解説

組込まれた時

  • 変数宣言の仕方

    var 変数名[=];
    
  • 配列宣言(値を入れない場合)

    var 配列名=new Array(個数);
    
  • 配列宣言(値を入れる場合)

    var 配列名=[,,・・・・];
    
  • サンプルaという配列を10個

    var a=new Array(10);
    var a=[0,0,0,0,0,0,0,0,0,0];
    

    onload時

  • htmlの文書の中

    <input type="text" id="id名" />
    
  • TextBoxの値の取得

    変数名=document.getElementById("id名").value;
    //valueはtextでもidでもok
    
  • TextBoxに値を設定

    document.getElementById("id名").value=;
    //値は変数でもok。文字列の囲み方は縛りがない。
    

    エレメント

    テキストボックス     id = "t1"
    テキストボックス     id = "t2"
    ボタン         id = "btn"
           onClick = "copy()"
    変数
    val
    

構文してみる

  • 組込まれた時

    var val;
    
  • onload時

    document.getElementById("t1").value = "";
    document.getElementById("t2").value = "";
    //F5で更新したときは、クリアはされない。
    
  • 記述方法1

    function copy(){
        val=document.getElementById("t1").value;
        //t1のテキストボックスの中身がvalに代入
        document.getElementById("t2").value = val;
        //t2に代入
    }
    
  • 記述方法2

    function copy(){
    document.getElementById("t2").value = document.getElementById("t1").value;
    }
    

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