20190318のJavaScriptに関する記事は24件です。

JS Consoleで直前に実行された値を取得できる変数

こんにちは、とくめいチャットサービス「ネコチャ」運営者のアカネヤ(@ToshioAkaneya)です。
ChromeのJS Consoleで直前に実行された値を取得できる変数を紹介します。

JS Consoleで直前に実行された値を取得できる変数

$_がその変数です。
Screen Shot 2019-03-18 at 23.45.36.png
便利なのでぜひ使ってみて下さい。

はてなブックマーク・Pocketはこちらから

はてなブックマークに追加
Pocketに追加

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

�JS Consoleで直前に実行された値を取得できる変数

こんにちは、とくめいチャットサービス「ネコチャ」運営者のアカネヤ(@ToshioAkaneya)です。
ChromeのJS Consoleで直前に実行された値を取得できる変数を紹介します。

JS Consoleで直前に実行された値を取得できる変数

$_がその変数です。
Screen Shot 2019-03-18 at 23.45.36.png
便利なのでぜひ使ってみて下さい。

はてなブックマーク・Pocketはこちらから

はてなブックマークに追加
Pocketに追加

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

CSSだけで、一方向に無限に流れるスライドショーを作る

作りたいもの

こういうやつ。画像が一方向にずっと流れるアニメーション(下はGIF画像なのでカクカクしているけども)。CodePenにも投稿しています(こちら)。これを、JavaScriptやjQueryのプラグインを使わずに。CSSのみで作ります。
1552899979999.gif

*CodePenを開くと画像がすべて読み込まれておらず、ところどころ空欄になってしまっているときがあるかもしれません。その時は、どこかのコードをコメントアウトにし、表示を更新させてから、そのコメントアウトを外して再度表示を更新させると、画像がすべて表示されるようになるかもしれません。

まずは全部のコード

このスライドショーは、作るのに若干テクニックがいるものです。コードを見ただけで仕組みが分かる人のために、まずは全てのコードを載せておきます。

HTML
<div class="container">
  <div class="slider-container">
    <img src="imgA.jpg" class="slider-img">
    <img src="imgB.jpg" class="slider-img">
    <img src="imgC.jpg" class="slider-img">
    <img src="imgD.jpg" class="slider-img">
    <img src="imgE.jpg" class="slider-img">
    <img src="imgA.jpg" class="slider-img">
    <img src="imgB.jpg" class="slider-img">
    <img src="imgC.jpg" class="slider-img">
    <img src="imgD.jpg" class="slider-img">
    <img src="imgE.jpg" class="slider-img">
  </div>
</div>

同じ画像を2回含めていますが、記述ミスではありません。こうする必要があるのです。

CSS
.container {overflow: hidden;}
:root {
  --numOfListA: 5;
  --imgW: 140px;
  --imgH: var(--imgW);
  --mBetweenImg: 50px;
}
.slider-container {
  display: flex;
  font-size: 0;
  animation: slideshow 15s linear infinite;
}
.slider-img {
  width: var(--imgW);
  height:  var(--imgH);
}
.slider-img + .slider-img {margin-left: var(--mBetweenImg);}
@keyframes slideshow {100% {transform: translateX(calc((var(--numOfListA) * var(--imgW) + var(--mBetweenImg) * var(--numOfListA)) * -1));}}

カスタムプロパティを使用していますが、使用しなくても作れます。修正が楽なので使っているだけです。
では仕組みを解説していきます。

仕組み

文章で説明するよりも実際に動いているのを見た方が分かりやすいでしょう。

下のGIFは、画像3枚を無限スライドショーにした際の動きを簡易的に表したものです。本来ならdiv.containeroverflow: hidden;が指定されていますが、それを指定しないとこういう動きになります。
1552913772314.gif
imgA・imgB・imgCはdiv.slider-containerの中のimgタグ(img.slider-img)を表しています。画像と画像の間はvar(--mBetweenImg)ですね。

ここで、div.containeroverflow: hidden;を指定してやると次のようになります。
1552914354142.gif
これを見ていただければ、なぜ同じ画像を含めているのかが分かっていただけるのではないでしょうか。文章で説明するとややこしくなりそうなので、文章での説明は省略させていただきます。

コードの説明

HTML

HTML
<div class="container">
  <div class="slider-container">
    <!-- リストA -->
    <img src="imgA.jpg" class="slider-img">
    <img src="imgB.jpg" class="slider-img">
    <img src="imgC.jpg" class="slider-img">
    <img src="imgD.jpg" class="slider-img">
    <img src="imgE.jpg" class="slider-img">

    <!-- リストB -->
    <img src="imgA.jpg" class="slider-img">
    <img src="imgB.jpg" class="slider-img">
    <img src="imgC.jpg" class="slider-img">
    <img src="imgD.jpg" class="slider-img">
    <img src="imgE.jpg" class="slider-img">
  </div>
</div>

リストAには、スライドショーで流したい画像を入れてください。
リストBには、リストAの画像を上から同じ順番適当な数だけ含めてください。なぜそうする必要があるのかは、言葉で説明するとややこしくなりそうなので、これもGIF画像を置くだけにとどめておきます。

●もし上から同じ順番にしなかったら
1552915568974.gif

●もしリストBのimgタグの数が不十分だったら
1552915660029.gif

CSS

CSS
.container {overflow: hidden;} /* はみ出す部分を見えなくする */
:root {
  --numOfListA: 5; /* リストAのimgタグの数 */
  --imgW: 140px; /* ここで画像の横幅を指定、px指定を推奨 */
  --imgH: var(--imgW); /* ここで画像の縦幅を指定、px指定を推奨 */
  --mBetweenImg: 50px; /* ここで画像間の余白を指定。px指定を推奨 */
}
.slider-container {
  display: flex; /* 画像を横並びにする */
  font-size: 0; /* 余計な余白を削除 */
  animation: slideshow 15s linear infinite;
}
.slider-img {
  width: var(--imgW);
  height:  var(--imgH);
}
.slider-img + .slider-img {margin-left: var(--mBetweenImg);}
@keyframes slideshow {100% {transform: translateX(calc((var(--numOfListA) * var(--imgW) + var(--mBetweenImg) * var(--numOfListA)) * -1));}} /* 下で説明 */

アニメーションでのtranslateXの値について説明します。

アニメーションでは、div.slider-containerを、0%ではこの状態で・・・
155081659966.jpg
100%ではこの状態に持っていきたい。
155081659966.jpg
ということは、移動量は、リストAの画像の枚数(var(--numOfListA))とimg.slider-imgの横幅(var(--imgW))のと、画像間の余白(var(--mBetweenImg))とリストAの画像の枚数(var(--numOfListA))のの分だけ左に移動すればよい。左に移動するということはX軸の負の方向に移動するということだから、-1をかけてやればよい。

応用

例えば、パソコンサイズの画面では画像をもっと大きく表示させたいという場合は、メディアクエリを使えばいいです。例えばこういう風に。

CSS
@media screen and (min-width: 900px) {:root {--imgW: 180px;}}

下の完成形では、実際にメディアクエリを使用して、画面幅によって画像の大きさを変えています。

完成形

こちらのCodePenに載せています。
下のCSSを追記することで、このスライドショーの仕組みが理解しやすくなるかもしれません。

CSS
body {transform: scale(0.5);}
.container {
  overflow: none;
  border: 5px solid black;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

簡単なハンバーガーメニューを作った。

HTML

<img src="img/menu.svg" id="menu">
<div id="spNav" class="off">
  <ul class="globalNav">
    <a href="#profile">
      <li>
        Profile
      </li>
    </a>
    <a href="#skill">
      <li>
        Skill
      </li>
    </a>
    <a href="#account">
      <li>
        Account
      </li>
    </a>
    <a href="#blog">
      <li>
        Blog
      </li>
    </a>
  </ul>
</div>

JavaScript

{
  const menu = document.getElementById('menu');

  menu.onclick = function() {
    const spNav = document.getElementById('spNav');
    spNav.classList.toggle('off');
  };
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでブラウザの接続状態を取得する

window.navigator.onLineプロパティ

ブラウザの接続状態を真偽値で表すプロパティ。

  • true: 接続されている
  • false: 接続されていない

ユーザがリンクをたどる、もしくは、スクリプトがリモートページを要求したときに値が更新される。
例えば、閲覧しているページからユーザがリンクをクリックした時に、インターネットへの接続が切れてていると、プロパティはfalse を返す。

ブラウザによる実装状況

Chrome, Safari

  • false: ブラウザがLANまたはルータに接続できないとき
  • true: 上記以外(必ずしも、インターネットにアクセスできる状態とは限らない)

Firefox, IE

  • false: ブラウザがオフラインモードにあるとき
  • true: 上記以外

*irefox 41以降では実際のネットワーク接続状態に従って値を返す

online / offline イベント

onLineプロパティの値更新に準じて、Windowに対しイベントが発生する。
onLineプロパティと同様、必ずしも、インターネットにアクセスできる状態であるかを示す値ではないので注意する。

//オンラインになった時
window.ononLine(function() {
//処理内容
)
//オフラインになった時
window.onoffLine(function() {
//処理内容
)

参考

https://developer.mozilla.org/ja/docs/Web/API/NavigatorOnLine/onLine

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

住所の地理的座標への変換ツールを作成してみました(Googleスプレッドシート+Google Geocoding API)

目的

Google Apps ScriptとGoogle Maps Platformの勉強のため、GoogleスプレッドシートとGoogle Maps PlatformGeocoding APIを用いて、住所を地理的座標へ変換するツールを作成してみました。

Geocoding API

  • 今回は、Google Maps Platformの中でも「プレイス」に分類される「Geocoding API」を使用しています。
  • Geocoding APIは、住所を地理的座標に変換したり、地理的座標から住所を特定したりできます。

Google Apps Script

  • Google Apps Scriptは、Google社が提供するJavaScriptベースのプログラミング言語であり、クラウドサーバ上で動作します。
  • Gmail、Googleスプレッドシート、Googleカレンダー、Googleドライブ、Google翻訳などのGoogleが提供する数々のアプリケーション群をプログラミングにより操作可能です。

実行環境

この記事ではGoogle Apps Scriptを使っています。
コードを実行するためにはGoogle Maps PlatformのAPIキーが必要です。

※Google Maps Platform APIキーの取得・発行についてはこちらを参照してください。
https://www.zenrin-datacom.net/business/gmapsapi/api_key/index.html

前提条件

住所

地理的座標に変換する住所は、道の駅133駅(中部地方)を対象としました。
道の駅

実行方法

入力設定

  • Googleスプレッドシートに住所を入力します。
  • 住所はB列に入力してください。
  • A列、C列、D列の入力内容は任意です。
  • シート名は「address」に変更してください。
  • 実行結果はE列、F列、G列に出力されます。 キャプチャ.PNG

実行

  • Googleスプレッドシートのツール>スクリプトエディタを開いて、下記のソースを貼り付け、「関数を選択」を「Main」にしてスクリプトを実行してください。

  • Googleスプレッドシートへのアクセス権の確認画面が出ますので許可してください。

gas1.gs
//基本的な設定
//**********************************************************************************************
// APIKEYを設定する
var APIKEY = "APIキーを入力する" // APIキー
//**********************************************************************************************

var Address;        // 住所
var ArrAddressLst;  // 住所リスト
var LastRow;        // 最終行

//**********************************************************************************************
// Main関数
// GeocodingAPIを実行する関数
//**********************************************************************************************
function Main() {

  // 住所を設定する
  setaddress();

  // 戻り値を格納する配列
  var ArrRes = [];
  for (var i = 0; i <= LastRow-2; i++) {

    // GeocodingAPI関数による住所と緯度経度の取得
    var data = GeocodingAPI(ArrAddressLst[i][0], APIKEY);

    // 戻り値を配列に格納する
    ArrRes[i] = [];
    ArrRes[i][0] = data[0]; // formatted_address
    ArrRes[i][1] = data[1]; // lat
    ArrRes[i][2] = data[2]; // lng

  }

  // 結果をスプレッドシートに出力する
  var bk = SpreadsheetApp.getActiveSpreadsheet();  // アクティブなスプレッドシートを取得する
  var sh = bk.getSheetByName('address');           // addressシートを取得する

  for (var i = 0; i <= LastRow-2; i++) {
    // 値をセルに入力する
    sh.getRange(i + 2, 5).setValue(ArrRes[i][0]);  // formatted_address
    sh.getRange(i + 2, 6).setValue(ArrRes[i][1]);  // lat
    sh.getRange(i + 2, 7).setValue(ArrRes[i][2]);  // lng
  }

}

//**********************************************************************************************
// 住所を設定する関数
//**********************************************************************************************
function setaddress() {
  // addressシートの内容を配列に格納する
  var bk = SpreadsheetApp.getActiveSpreadsheet();             // アクティブなスプレッドシートを取得する
  var sh = bk.getSheetByName('address');                      // addressシートを取得する
  LastRow = sh.getLastRow();                                  // 最終行を取得する
  // var colLst = sh.getLastColumn();                            // 最終列を取得する

  Logger.log(LastRow);

  // データを配列に格納する
  // Sheetオブジェクト.getRange(行番号, 列番号, 行数, 列数)
  ArrAddressLst = sh.getRange(2, 2, LastRow-1, 1).getValues();

  Logger.log(ArrAddressLst);

}

//**********************************************************************************************
// GeocodingAPI関数
//**********************************************************************************************
function GeocodingAPI(Address, APIKEY){
  // URLを作成する
  var url = "https://maps.googleapis.com/maps/api/geocode/json?language=ja" + 
            "&address=" + Address + 
            "&sensor=false" +
            "&key=" + APIKEY;

  Logger.log(url);

  // HTTPリクエストを行う
  var res  = UrlFetchApp.fetch(url,{muteHttpExceptions:true});
  Logger.log(res); 

  // HTTPレスポンスを文字列として取得、JSON形式の文字列を解析してオブジェクトとして返す
  var json = JSON.parse(res.getContentText()); 
  Logger.log(json);

  if (json["status"] == "OK") {
    var json_formatted_address = json["results"][0]["formatted_address"];
    var json_lat = json["results"][0]["geometry"]["location"]["lat"];
    var json_lng = json["results"][0]["geometry"]["location"]["lng"];

    // 「日本、」「郵便番号」を除外
    var formatted_address = json_formatted_address.split(' ');

    return [formatted_address[1],json_lat,json_lng];

  }
}

実行結果

  • 実行結果はE列、F列、G列に出力されます。
  • E列~G列がGeocodingAPIによりHTTPレスポンスとして受け取った結果になります。
  • E列が住所(formatted_address)になります(formatted_addressに含まれる「日本」や「郵便番号」は除外しています)。
  • F列が緯度(lat)、G列(lng)が経度になります。 キャプチャ2.PNG

留意事項

参考書

詳解! GoogleAppsScript完全入門 ~GoogleApps & G Suiteの最新プログラミングガイド~
Developer Guide(Geocoding API)

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

住所の地理的座標への変換ツール(Googleスプレッドシート+Google Geocoding API)

目的

Google Apps ScriptとGoogle Maps Platformの勉強のため、GoogleスプレッドシートとGoogle Maps PlatformGeocoding APIを用いて、住所を地理的座標へ変換するツールを作成してみました。

実行環境

この記事ではGoogle Apps Scriptを使っています。
コードを実行するためにはGoogle Maps PlatformのAPIキーが必要です。

※Google Maps Platform APIキーの取得・発行についてはこちらを参照してください。
https://www.zenrin-datacom.net/business/gmapsapi/api_key/index.html

前提条件

住所

地理的座標に変換する住所は、中部 道の駅133駅を対象としました。
道の駅

実行方法

入力設定

  • Googleスプレッドシートに住所を入力します。
  • 住所はB列に入力してください。
  • A列、C列、D列の入力内容は任意です。
  • シート名は「address」に変更してください。
  • 実行結果はE列、F列、G列に出力されます。 キャプチャ.PNG

実行

  • Googleスプレッドシートのツール>スクリプトエディタを開いて、下記のソースを貼り付け、「関数を選択」を「Main」にしてスクリプトを実行してください。

  • Googleスプレッドシートへのアクセス権の確認画面が出ますので許可してください。

gas1.gs
//基本的な設定
//**********************************************************************************************
// APIKEYを設定する
var APIKEY = "APIキーを入力する" // APIキー
//**********************************************************************************************

var Address;        // 住所
var ArrAddressLst;  // 住所リスト
var LastRow;        // 最終行

//**********************************************************************************************
// Main関数
// GeocodingAPIを実行する関数
//**********************************************************************************************
function Main() {

  // 住所を設定する
  setaddress();

  // 戻り値を格納する配列
  var ArrRes = [];
  for (var i = 0; i <= LastRow-2; i++) {

    // GeocodingAPI関数による住所と緯度経度の取得
    var data = GeocodingAPI(ArrAddressLst[i][0], APIKEY);

    // 戻り値を配列に格納する
    ArrRes[i] = [];
    ArrRes[i][0] = data[0]; // formatted_address
    ArrRes[i][1] = data[1]; // lat
    ArrRes[i][2] = data[2]; // lng

  }

  // 結果をスプレッドシートに出力する
  var bk = SpreadsheetApp.getActiveSpreadsheet();  // アクティブなスプレッドシートを取得する
  var sh = bk.getSheetByName('address');           // addressシートを取得する

  for (var i = 0; i <= LastRow-2; i++) {
    // 値をセルに入力する
    sh.getRange(i + 2, 5).setValue(ArrRes[i][0]);  // formatted_address
    sh.getRange(i + 2, 6).setValue(ArrRes[i][1]);  // lat
    sh.getRange(i + 2, 7).setValue(ArrRes[i][2]);  // lng
  }

}

//**********************************************************************************************
// 住所を設定する関数
//**********************************************************************************************
function setaddress() {
  // addressシートの内容を配列に格納する
  var bk = SpreadsheetApp.getActiveSpreadsheet();             // アクティブなスプレッドシートを取得する
  var sh = bk.getSheetByName('address');                      // addressシートを取得する
  LastRow = sh.getLastRow();                                  // 最終行を取得する
  // var colLst = sh.getLastColumn();                            // 最終列を取得する

  Logger.log(LastRow);

  // データを配列に格納する
  // Sheetオブジェクト.getRange(行番号, 列番号, 行数, 列数)
  ArrAddressLst = sh.getRange(2, 2, LastRow-1, 1).getValues();

  Logger.log(ArrAddressLst);

}

//**********************************************************************************************
// GeocodingAPI関数
//**********************************************************************************************
function GeocodingAPI(Address, APIKEY){
  // URLを作成する
  var url = "https://maps.googleapis.com/maps/api/geocode/json?language=ja" + 
            "&address=" + Address + 
            "&sensor=false" +
            "&key=" + APIKEY;

  Logger.log(url);

  // HTTPリクエストを行う
  var res  = UrlFetchApp.fetch(url,{muteHttpExceptions:true});
  Logger.log(res); 

  // HTTPレスポンスを文字列として取得、JSON形式の文字列を解析してオブジェクトとして返す
  var json = JSON.parse(res.getContentText()); 
  Logger.log(json);

  if (json["status"] == "OK") {
    var json_formatted_address = json["results"][0]["formatted_address"];
    var json_lat = json["results"][0]["geometry"]["location"]["lat"];
    var json_lng = json["results"][0]["geometry"]["location"]["lng"];

    // 「日本、」「郵便番号」を除外
    var formatted_address = json_formatted_address.split(' ');

    return [formatted_address[1],json_lat,json_lng];

  }
}

実行結果

  • 実行結果はE列、F列、G列に出力されます。
  • E列がGeocodingAPIにより取得した住所、F列が緯度、G列が経度になります。 キャプチャ2.PNG

留意事項

※無料枠を超えてAPIを使用すると課金されますのでご注意ください(>_<)

参考書

詳解! GoogleAppsScript完全入門 ~GoogleApps & G Suiteの最新プログラミングガイド~
Developer Guide(Geocoding API)

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

jQueryを使わずにHTML+JavaSccipt+CSSでドラックアンドドロップを遊んでみる

jQueryを使わずにHTML + Javascript + CSS でドラックアンドドロップ(dnd)を使った、
画面上でのアイテム編集、追加のサンプルを作ってみました。

アイテム削除機能や表示済みアイテム名編集機能はエラーが発生する箇所があります。

Githubのここに置いています。

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

Clipkitでギャラリーアイテムをカルーセルにする

はじめに

ギャラリーアイテムをカルーセルとして記事詳細ページに実装したいという要望にお応えすることがあったため、本記事でその手法をご紹介します。カルーセルのライブラリーは多数存在しますが、ここでは代表的なSlickを使用した実装方法をご紹介します。サンプルデザインについては、適宜調整してご利用ください。

サンプルデザイン

以下のような感じになります。「>」をクリックすると、次のスライドに進み、「<」をクリックすると、前のスライドに戻ります。それぞれタイトルとコメントが入力できるようになっています。PCでは画像背景をグレーでくり抜き、SPでは上下が画像によって可変になっています。

PCサンプルデザイン

Screenshot 2019-03-14 at 18.28.29.png

SPサンプルデザイン

Screenshot 2019-03-18 at 11.47.59.png

実装例

PCとSP共通準備

layout.html
    <!--SlickのCSSとJSを読み込む-->
    <link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick.css"/>
    <script type="text/javascript" src="//cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick.min.js"></script>

    <!--object-fit(IE対策→PCのみ!)-->
    <script src="{{ site.files | filename: "/theme/default/ofi.min.js" }}"></script>
    <script>$(function () { objectFitImages('.slick-slide .slide-img img') });</script>

<head>...</head>の中に以上のような実装を行います。また、jQueryを読み込んでいることが前提です。SlickにてCSSとJSのファイルをダウンロードしても良いですが、ここでは手っ取り早くCDNを読み込んでいます。PCでは画像のサイズ比率を保持するため、CSSにてobject-fitプロパティを使用しています。IEはobject-fitはに対応しておらず、画像の表示崩を起こすためofi.min.jsというファイルをダウンロードしています。objectFitImages()では該当クラス・IDをDOMで指定しています。全ての画像に適応したい場合は空欄にします。

PC

desktop.html
<div class="slider-container">
    <div class="slider">
        {% assign galleries = item.galleries %}
        {% for gallery in galleries reversed %}
        <div class="slick-slide">
            <div class="slide-img">
                {% if gallery.source_url == empty %}
                    <img data-lazy="{{ gallery.image_medium_path }}" alt="{{ gallery.title }}">
          {% else %}
                    <a href="{{ gallery.source_url }}">
                        <img data-lazy="{{ gallery.image_medium_path }}" alt="{{ gallery.title }}">
                    </a>
                {% endif %}
            </div>
            <div class="page-num"></div>
            <p class="slide-ttl">{{ gallery.title }}</p>
            <p class="slide-cmt">{{ gallery.comment | unescape | auto_link }}</p>
        </div>
        {% endfor %}
    </div>
</div>

<div class="page-num"></div>は空ですが、後に紹介するJSの実装でスライド番号を挿入します。不要でしたら削除してください。{% if gallery.source_url == empty %}...{% endif %}では、出典URLが入力されているならば、<a>タグを表示する仕組みです。

desktop.css
.slider-container {
  position: relative;
}
.slider {
  display: none;
  margin: 0 24px;
  overflow: hidden;
}
.slider.slick-initialized {
  display: block;
}
.slider-arrow {
  position: absolute;
  top: 50%;
  height: 36px;
  margin-top: -18px;  /* 高さの半分だけネガティブマージン */
  margin-left: -15px;
  margin-right: -15px;
  color: #000;
  line-height: 36px;
  font-size: 50px;
  cursor: pointer;
  z-index: 10;  /* 重要 */
}
.slider-prev {
  left: 0;
}
.slider-next {
  right: 0;
}
.slider-arrow.slider-next.fa.fa-angle-right.slick-disabled {
  visibility: hidden;
}
.slider-arrow.slider-prev.fa.fa-angle-left.slick-disabled {
  visibility: hidden;
}
.slick-slide {
  padding: 1.5em 0;
  color: #000;
  text-align: center;
  font-size: 1.1em;
  outline: 0;
  background-color: #fff;
  direction: ltr;
}
.slick-slide p {
  font-size: 12px;
  margin-bottom: 1px;
}
.slide-ttl {
  font-weight: bold;
  color: #000;
}
.slide-cmt {
  text-align: left;
}
.slider-counter {
  font-weight: bold;
  font-style: italic;
  font-size: 18px;
  text-align: center;
  color: #000;
}
.slick-slide .slide-img img {
  max-width: 100%;
  max-height: 100%;
  height: 500px;
  width: auto;
  margin: 0 auto;
  object-fit: contain;
  font-family: 'object-fit: contain;'; /*IE対策*/
}
.slide-img {
  background-color: whitesmoke;
}
desktop.js
var $slider_container = $('.slider-container');

$slider_container.each(function() {

  var $slider = $(this).find('.slider');

  $slider.on('init', function(event, slick) {
    $(this).find('.page-num').append('<div class="slider-counter"><span class="current"></span> / <span class="total"></span></div>');
    $(this).find('.current').text(slick.currentSlide + 1);
    $(this).find('.total').text(slick.slideCount);
  })
  .slick({
    appendArrows: $(this),
    // FontAwesomeのクラスを追加
    prevArrow: '<div class="slider-arrow slider-prev fa fa-angle-left"></div>',
    nextArrow: '<div class="slider-arrow slider-next fa fa-angle-right"></div>',
    infinite: true,
    speed: 300,
    slidesToShow: 1,
    lazyLoad: 'progressive'
  })
  .on('beforeChange', function(event, slick, currentSlide, nextSlide) {
    $(this).find('.current').text(nextSlide + 1);
  })
  .on('afterChange',function(event, slick, currentSlide, nextSlide){
    var slideNum = currentSlide + 1;
  });
});

関数には.eachメソッドを利用し、1記事に複数のギャラリーアイテムが存在しても、お互いの動作に干渉しないようにしています。スライド速度はミリ秒設定です。ここでは、speed: 300(0.3秒)で設定してます。infinite: trueでスライドが無限ループします。falseにすると、有限になります。lazyLoad: 'progressive'でHTMLのdata-lazy属性の箇所で画像遅延読み込み行います。

SP

smartphone.html
<div class="slider-container">
    <div class="slider">
        {% assign galleries = item.galleries %}
        {% for gallery in galleries reversed %}
        <div class="slick-slide">
            <div class="slide-img">
                {% if gallery.source_url == empty %}
                    <img data-lazy="{{ gallery.image_medium_path }}" alt="{{ gallery.title }}">
          {% else %}
                    <a href="{{ gallery.source_url }}">
                        <img data-lazy="{{ gallery.image_medium_path }}" alt="{{ gallery.title }}">
                    </a>
                {% endif %}
            </div>
            <p class="slide-ttl">{{ gallery.title }}</p>
            <p class="slide-cmt">{{ gallery.comment | unescape | auto_link }}</p>
        </div>
        {% endfor %}
    </div>
</div>

特にスマートフォンではデバイスによって、画像の読み込みに非常に時間がかかります。ロード時間短縮のために、画像遅延読み込みdata-lazy属性をsrc属性の代わりに使用します。

smartphone.css
.slider-container {
  position: relative;
  width: 100%;
}
.slider {
  display: none;
  overflow: hidden;
}
.slider.slick-initialized {
  display: block;
}
.slider-arrow {
  position: absolute;
  top: 50%;
  height: 36px;
  margin-top: -18px;  /* 高さの半分だけネガティブマージン */
  margin-left: 5px;
  margin-right: 5px;
  color: #fff;
  line-height: 36px;
  font-size: 40px;
  cursor: pointer;
  z-index: 10;  /* 重要 */
}
.slider-prev {
  left: 0;
}
.slider-next {
  right: 0;
}
.slider-arrow.slider-next.fa.fa-angle-right.slick-disabled {
  visibility: hidden;
}
.slider-arrow.slider-prev.fa.fa-angle-left.slick-disabled {
  visibility: hidden;
}
.slick-slide {
  padding: 1.5em 0;
  color: #000;
  text-align: center;
  font-size: 1.1em;
  outline: 0;
  background-color: #fff;
  direction: ltr;
}
.slick-slide p {
  font-size: 12px;
  margin-bottom: 1px;
}
.slide-ttl {
  font-weight: bold;
  color:#000;
}
.slide-cmt {
  text-align: left;
}
.slider-counter {
  font-weight: bold;
  font-size: 14px;
  text-align: right;
  color: #000;
}
smartphone.js
var $slider_container = $('.slider-container');

$slider_container.each(function() {
  var $slider = $(this).find('.slider');

  $slider.on('init', function(event, slick) {
    $(this).find('.slide-img').append('<div class="slider-counter">【<span class="current"></span> / <span class="total"></span>】</div>');
    $(this).find('.current').text(slick.currentSlide + 1);
    $(this).find('.total').text(slick.slideCount);
  })
  .slick({
    appendArrows: $(this),
    // FontAwesomeのクラスを追加
    prevArrow: '<div class="slider-arrow slider-prev fa fa-angle-left"></div>',
    nextArrow: '<div class="slider-arrow slider-next fa fa-angle-right"></div>',
    infinite: true,
    speed: 50,
    slidesToShow: 1,
    adaptiveHeight: true,
    lazyLoad: 'progressive'
  })
  .on('beforeChange', function(event, slick, currentSlide, nextSlide) {
    $(this).find('.current').text(nextSlide + 1);
  })
  .on('afterChange',function(event, slick, currentSlide, nextSlide){
    var slideNum = currentSlide + 1;

    $(this).find('.current').text(currentSlide + 1);
  });
});

スマホの場合、スライド速度はspeed: 50と設定しています。スマホのデバイスによってはスワイプの感度が非常に悪い機種があるため、推奨設定秒数は100ミリ秒(0.1秒)以下となります。adaptiveHeight: trueで画像の縦幅に応じて、ギャラリーの高さをを変更しています。SPもPCと同様にlazyLoad: 'progressive'でHTMLのdata-lazy属性の箇所で画像遅延読み込み行います。

参考

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

bootstrapでメニューからページ内ジャンプの際にメニューを自動で閉じる(js)

Fixed

@htsign さんからご指摘いただいた、jsの記述箇所『ready』に関して、『on.load』に変更しました。

GOAL

メニューバーからリンクをクリックし、ページ内じゃんするときに、自動でメニューを閉じる。
(bootstrapのメニューバー使用しているときに限る)

how to

test.html
<body>
.
.
.
<script>
    $(document).on("load" , function () {
        $(".navbar-nav li a").click(function(event) {
            $(".navbar-collapse").collapse('hide');
        });
    });
</script>

</body>

参考サイト

Bootstrap でリンククリック時にcollapseメニューを閉じる処理

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

JavaScriptのthisについてまとめてみた

this とは

JavaScript(以下JS)のコードを読んでいると、thisがたくさん出てくることあると思います。
JSのコードでthisが出てくると、「このthisって何を指しているのだろうか?」と勉強したての頃はよくなりました。
thisは日本語訳すると「これ」です。代名詞です。
JSになると「この」プロパティを指すか、「この」関数を指すか、このオブジェクトを指すか、thisの前後によって意味が異なります。JSの始めたての人はこのthisでつまずきやすいのでないでしょうか。しかし逆を言うとこのthisを把握することによってコードの理解力が上がると思います。

thisの注意点

上記でも述べましたが、「これ」といってるだけで呼び出した場所や方法によってその中身が変化します。要点を抑えて正しいthisの把握をしていきましょう。
最初に大事なことを言うと、thisの意味を把握するうえでの基準は.(ドット)です。

※最後の方ではドラえもんの例でごり押ししているので、ご了承ください。

thisの使い方

thisの中身はいろいろ変化しますが、だいたいパターンは決まっています。次の5種類くらいです。

  1. グローバルオブジェクト
  2. メソッドチェーン
  3. 関数呼び出し
  4. コンストラクタ呼び出し
  5. callapply

以下、それぞれの種類を説明していきます。

1.グローバルオブジェクト

説明

関数の呼び出しにおいて.がない場合は、グローバル変数的なthisを呼びます。つまりwindowプロパティです。

具体例

function test() {
    console.log(this);
}
test(); //.がない

//結果
//Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

2. メソッドチェーン

説明

関数の呼び出しにおいて.がある場合は、その前のオブジェクトを指します。これは一般的な代名詞使用法です。以下の具体例でいうと【「この」オブジェクトのshow関数を実行します!!】と叫んでいるのが「myObject.show();」です。ここではオブジェクトのthis.name、すなわちmyObjectnameであるmk0812を呼びます。ここまでのthis記法は多分まだ簡単だと思います。

具体例

var myObject = {
    name: 'mk0812',
    show: function(){
        console.log(this.name);
    }
}
myObject.show(); //.がある場合はその前のオブジェクトを指す

//結果
//mk0812

3.関数呼び出し

説明

ここから本番です。関数呼び出しとはメソッドの中で関数を呼ぶことです。言ってることがさっぱりわからないので具体例の中で説明しましょう。

具体例

var myObject = {
    name: 'mk0812',
    show: function(){
        console.log(this.name); //①

        function show(){
            console.log(this.name); //②
        }
        show();
    }
}
myObject.show(); //.がある場合はその前のオブジェクトを指す。がメソッドの中にthisを参照する関数がある。

//結果
//①.. mk0812
//②.. undefined

この場合は、①は2.メソッドチェーン同様にmk0812を指しますが、②のthisundefinedを指します。ここで②のthisはグローバルオブジェクトを呼び出しています。気を付けてほしいのはメソッド内で関数呼び出しになっている場合はthisの中身は引き継がれないことです。そのメソッド内ではshow();と定義されています。このshow();は誰からも定義されていないので、.がない関数呼び出しとしてグローバルオブジェクトを呼び出します。

undefinedはやだ!かわいそう!というのなら、そのthisを別の変数(self)で置き換えて呼びましょう。

var myObject = {
    name: 'mk0812',
    show: function(){
        var self = this;
        console.log(this.name); //①

        function show(){
            console.log(self.name); //②
        }
        show();
    }
}
myObject.show();

//結果
//①.. mk0812
//②.. mk0812

4.コンストラクタ呼び出し

説明

コンストラクタ呼び出しは、newをつけてインスタンスを生成します。
そのインスタンスの中身のthisはインスタンス自身になります。

これはnewをつけるかつけていないかでは、全然違います。
newをつけていない場合はただの関数です。つまりMyObject('mk0812');の場合は関数呼び出しです。グローバル変数のthisnameが定義されます。インスタンスとして呼びたい場合は必ずnewをつけましょう!

具体例

function MyObject(name){
    this.name = name;
    this.add = function(value){
        this.name = this.name + value;
    }
    this.show = function(){
        console.log(this.name);
    }
}

var mk = new MyObject('mk0812');
mk.add('aaa');
mk.show();

//結果
//mk0812aaa

5.callapply

説明

callapplyは「this」を自由に操りたい場合、この方法を用いられます。最初は僕はさっぱりわかりませんでした。僕の勝手なイメージですが、この記法は自作のライブラリとかを作りたい場合、よく使われる印象があります。この「apply」と「call」を使うと「強制的にthisを束縛」できます。僕は「は?束縛?意味わからん」となりました。ここは分かりやすく「ドラえもん」で具体例を出しましょう。

具体例

のび太とジャイアンの関係性でいうと、のび太はジャイアンより弱い設定です(秘密道具を使うとかはなしにしましょう)。のび太が手に入れた漫画はすべてジャイアンのものになります。悲しい世界だ。。。

var nobita = {
    book: 'これはのび太の漫画だよ',
    show: function() {
        console.log(this.book);
    }
};

var jian = {
    book: 'お前のものはおれのもの、おれのものはおれのもの'
}
nobita.show.apply(jian) //'お前のものはおれのもの、おれのものはおれのもの'
nobita.show.call(jian) //'お前のものはおれのもの、おれのものはおれのもの'

「apply」と「call」の第一引数は「this」にsetしたいオブジェクトです。のび太はジャイアンのjianオブジェクトに完全に支配され、bookプロパティが上書きされました。これだけ見ると「apply」と「call」の違いはあまりなさそうですが「apply」と「call」の違いは第二引数以降の取り方です。

var nobita = {
    book: 'のび太の漫画だよ',
    show: function(book1,book2) {
        console.log(book1,book2 + 'は' + this.book);
    }
};

var jian = {
    book: 'おれのもの'
}
nobita.show.apply(jian,['どら焼きの本','カレーの本']); //どら焼きの本 カレーの本はおれのもの
nobita.show.call(jian,'どら焼きの本','カレーの本'); //どら焼きの本 カレーの本はおれのもの

「apply」は第二引数に配列をとり、配列の中身が引数として渡されます。
「call」はそのまま第二引数以降が引数として渡されます。

その他

bind

bindは強制的にオブジェクトと結びつける関数です。

function nobita(){
    console.log(this);
}

var jian = {
    book: 'おれのもの'
}
var another_jian = nobita.bind(jian);
another_jian();

//結果
//'おれのもの'

関数 another_jian の呼び出しには .がついていませんが、 bindされているので、呼び出し時に jian. が付く形になります。そのためanother_jianthisjian(別のジャイアンもジャイアンとなる)

まとめ

結局、thisってなんだっけ?ドラえもんだっけ?となった方にまとめを書きます。基本的には.を見て判断しましょう。使い方は次の5種類です。

  1. グローバルオブジェクト .. .がない→グローバルオブジェクト
  2. メソッドチェーン .. .がある→.の前のオブジェクトを指す。
  3. 関数呼び出し .. メソッドの中の関数のthisはグローバル
  4. コンストラクタ呼び出し .. newをつけようね
  5. callapply .. this束縛マン

いろいろ、後で追記するかもしれませんが僕なりにまとめてみました。
以上、最後まで読んでいただきありがとうございました。

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

騙されるな。JavaScript の function は関数じゃない。コンストラクタだ。

JavaScript を使っている、あるいは少しでもかじったことのある人なら、

function foo() {
   /* ... */
}

const foo = function() {
   /* ... */
};

といった書き方を目にしたことがあると思います。また、普段から「関数」として使っている人も多いと思います。

しかし、結論から言うと、JavaScript の function は普通の「関数」ではなく「コンストラクタ」です。JavaScriptの構文・キーワードに騙されてはいけません。(私自身、少し勘違いをしていて、以前書いた記事(JavaScript初心者にはfunctionよりも、まずアロー関数を教えるべき)では function は「メソッド」だと主張していました。)

newができるからコンストラクタって言いたいだけなんでしょう」と思った方、とりあえずブラウザバックしようとする手を止めて最後までお読みください。

function を「関数」として使用すると、状況によってはパフォーマンスに影響する可能性があります(若干誇張表現です。最適化の度合いによります)。

以下、function 文・function 式で定義した関数を便宜上「function 関数」と呼びます。また、ECMAScript® 2018 Language Specification を参照・引用します。英語の仕様書に抵抗のある方は適当に読み飛ばしてください。最後の方には日本語も書いています。

function 関数の作られ方

function 文で関数オブジェクトがどのように作られるかは、14.1.20 Runtime Semantics: InstantiateFunctionObject で説明されています。

With parameter scope.

FunctionDeclaration: function BindingIdentifier ( FormalParameters ) { FunctionBody }

  1. If the function code for FunctionDeclaration is strict mode code, let strict be true. Otherwise let strict be false.
  2. Let name be StringValue of BindingIdentifier.
  3. Let F be FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict).
  4. Perform MakeConstructor(F).
  5. Perform SetFunctionName(F, name).
  6. Return F.

FunctionDeclaration: function ( FormalParameters ) { FunctionBody }

  1. Let F be FunctionCreate(Normal, FormalParameters, FunctionBody, scope, true).
  2. Perform MakeConstructor(F).
  3. Perform SetFunctionName(F, "default").
  4. Return F.

function 式の方は14.1.21 Runtime Semantics: Evaluation にあります。

FunctionExpression: function ( FormalParameters ) { FunctionBody }

  1. If the function code for FunctionExpression is strict mode code, let strict be true. Otherwise let strict be false.
  2. Let scope be the LexicalEnvironment of the running execution context.
  3. Let closure be FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict).
  4. Perform MakeConstructor(closure).
  5. Return closure.

FunctionExpression: function BindingIdentifier ( FormalParameters ) { FunctionBody }

  1. If the function code for FunctionExpression is strict mode code, let strict be true. Otherwise let strict be false.
  2. Let scope be the running execution context's LexicalEnvironment.
  3. Let funcEnv be NewDeclarativeEnvironment(scope).
  4. Let envRec be funcEnv's EnvironmentRecord.
  5. Let name be StringValue of BindingIdentifier.
  6. Perform envRec.CreateImmutableBinding(name, false).
  7. Let closure be FunctionCreate(Normal, FormalParameters, FunctionBody, funcEnv, strict).
  8. Perform MakeConstructor(closure).
  9. Perform SetFunctionName(closure, name).
  10. Perform envRec.InitializeBinding(name, closure).
  11. Return closure.

関数オブジェクトの構築に関わり、上記の全てのアルゴリズムで実行されているのは

  • Let F be FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict).
  • Perform MakeConstructor(F).

の2つです。

FunctionCreate

9.2.5 FunctionCreate ( kind, ParameterList, Body, Scope, Strict [ , prototype ] )

(中略)

  1. If prototype is not present, then
    1. Set prototype to the intrinsic object %FunctionPrototype%.
  2. If kind is not Normal, let allocKind be "non-constructor".
  3. Else, let allocKind be "normal".
  4. Let F be FunctionAllocate(prototype, Strict, allocKind).
  5. Return FunctionInitialize(F, kind, ParameterList, Body, Scope).

ここでは、FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict)と呼び出されているので、

  • Let F be FunctionAllocate(%FunctionPrototype%, strict, "normal").
  • Return FunctionInitialize(F, Normal, FormalParameters, FunctionBody, scope).

が実行されることになります。ここで注目するのはFunctionAllocateの処理です。

FunctionAllocate

9.2.3 FunctionAllocate ( functionPrototype, strict, functionKind )

(中略)

  1. Assert: Type(functionPrototype) is Object.
  2. Assert: functionKind is either "normal", "non-constructor", "generator", "async", or "async generator".
  3. If functionKind is "normal", let needsConstruct be true.
  4. Else, let needsConstruct be false.
  5. If functionKind is "non-constructor", set functionKind to "normal".
  6. Let F be a newly created ECMAScript function object with the internal slots listed in Table 27. All of those internal slots are initialized to undefined.
  7. Set F's essential internal methods to the default ordinary object definitions specified in 9.1.
  8. Set F.[[Call]] to the definition specified in 9.2.1.
  9. If needsConstruct is true, then
    1. Set F.[[Construct]] to the definition specified in 9.2.2.
    2. Set F.[[ConstructorKind]] to "base".
  10. Set F.[[Strict]] to strict.
  11. Set F.[[FunctionKind]] to functionKind.
  12. Set F.[[Prototype]] to functionPrototype.
  13. Set F.[[Extensible]] to true.
  14. Set F.[[Realm]] to the current Realm Record.
  15. Return F.

ここで、functionKindには"normal"が渡されるので、needsConstructtrueになります(3.)。よって、F.[[Construct]]スロットが設定されます(9.1.)。

[[Construct]]スロットによって、function 関数はコンストラクタとして呼び出すことが可能になっています。

MakeConstructor

9.2.10 MakeConstructor ( F [ , writablePrototype [ , prototype ] ] )

(中略)

  1. Assert: F is an ECMAScript function object.
  2. Assert: IsConstructor(F) is true.
  3. Assert: F is an extensible object that does not have a prototype own property.
  4. If writablePrototype is not present, set writablePrototype to true.
  5. If prototype is not present, then
    1. Set prototype to ObjectCreate(%ObjectPrototype%).
    2. Perform ! DefinePropertyOrThrow(prototype, "constructor", PropertyDescriptor { [[Value]]: F, [[Writable]]: writablePrototype, [[Enumerable]]: false, [[Configurable]]: true }).
  6. Perform ! DefinePropertyOrThrow(F, "prototype", PropertyDescriptor { [[Value]]: prototype, [[Writable]]: writablePrototype, [[Enumerable]]: false, [[Configurable]]: false }).
  7. Return NormalCompletion(undefined).

ここではパラメータprototypeは省略されているので、まず新しいオブジェクトを作ってからそれをprototypeに代入しています(5.)。prototypeは最終的に関数Fprototypeプロパティに格納されます(6.)。

結論

JavaScript の function 関数は「関数」ではなくて「コンストラクタ」です。理由は上で挙げた通り、

  1. [[Construct]]スロットが設定されていて、コンストラクタとして呼び出せる
  2. コンストラクタとして使用するためのprototypeプロパティに新しく作成されたオブジェクトが必ず設定される

1.はそのままです。ただnewを付けて呼び出したりできるというだけです。

2.がどういうことかと言うと、function 関数オブジェクトが作られる際にprototypeプロパティに設定するためだけに新しいオブジェクトが作られるということです。prototypeプロパティはコンストラクタ以外には必要ないにもかかわらず、全ての function 関数に設定されてしまいます。しかも新しいオブジェクトが作成されるので、その分のコストもかかってしまいます。

function 関数を「関数」として使った場合、弊害が出てくる可能性があります。

例えば、以下のようなコードでは

const arr = [1, 2, 3, 4, 5];
for (let i = 1; i <= 5; i++) {
   console.log(arr.map(function(n) {
      return n * i;
   }));
}

普通にコールバック関数を渡しているだけのつもりで、実際はループの度に余分なprototypeプロパティのオブジェクトが作成されてしまいます。Array.prototype.mapのような組み込み関数のコールバックの場合はおそらく最適化できると思いますが、全てのケースで最適化が使えるわけではありません。

アロー関数を使いましょう。

大事なことなのでもう一度言います。

アロー関数を使いましょう。

アロー関数はコンストラクタとして使えないようになっているので、普通の「関数」として問題なく使うことができます。

const arr = [1, 2, 3, 4, 5];
for (let i = 1; i <= 5; i++) {
   console.log(arr.map(n => n * i));
}

さらに、アロー関数には可読性が向上するという利点があります。

逆に普通の「関数」にアロー関数を使わない理由はありません。積極的に使っていきましょう。

おまけ: メソッドの記法

オブジェクトリテラルにはメソッドを簡単に記述するための糖衣構文があります。

こう書いていたものを

const obj = {
   method: function() { /* ... */ }  // (A)
};

こう書けるようになりました。

const obj = {
   method() { /* ... */ } // (B)
};

実は上の2つは厳密には等価ではありません。

(A)ではmethodプロパティに function 関数が設定されています。function 関数オブジェクトが作成された時点でその関数のprototypeプロパティにはオブジェクトが設定されます。つまり、obj.method.prototypeプロパティが存在します。また、obj.method[[Construct]]スロットも持つので、new obj.methodができます。

一方、(B)はメソッド定義専用の構文で、メソッドはコンストラクタである必要がないので、prototypeプロパティも[[Construct]]スロットも設定されません(14.3.7 Runtime Semantics: DefineMethod14.3.8Runtime Semantics: PropertyDefinitionEvaluation)。クラス構文もメソッドについても同様です。

こちらも、特別な意図のない限り、新しい(B)の構文を使った方がいいです。

ES5以前は全て function だったのが、

  • 関数 ⇒ アロー関数
  • メソッド ⇒ メソッド定義の構文
  • コンストラクタ ⇒ class 構文の constructor

と、役割によって書き分けられるようになりました。そのうち function キーワードも var 同様互換性のためだけのものになるのかもしれませんね。

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

AI-Techservices - AI Solutions Development Company

artificial-intelligence-the-amazing-technology-that-revolutionize-the-world.png

AI Techservices is the top grade Artificial Intelligence software development company specialized in AI development of Machine Learning and Deep Learning Solutions for enterprises.

Click Here To Visit The Site: https://www.ai-techservices.com/

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

Javascript(ES6)の開発環境を作るでつまっている。

せっかく作ったJSを動かしたい!
Javascript(ES6)の開発環境を作るチュートリアルをみながら環境を作っています。
しかしこれがまた、難しいのでした。

ここまでやったの記録です。

Homebrewの確認

robamimimnoMacBook-Air:~ robamimim$ brew -v
Homebrew 2.0.4
Homebrew/homebrew-core (git revision a290; last commit 2019-03-16)
Homebrew/homebrew-cask (git revision 757155; last commit 2019-03-18)
robamimimnoMacBook-Air:~ robamimim$ 

nodebrewインストール

robamimimnoMacBook-Air:~ robamimim$ brew install nodebrew
Updating Homebrew...
<以下略>
robamimimnoMacBook-Air:~ robamimim$ nodebrew -v
nodebrew 1.0.1

Node.jsインストール

.bash_profileにnodevrewを追加

robamimimnoMacBook-Air:~ robamimim$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
robamimimnoMacBook-Air:~ robamimim$ source ~/.bash_profile
-bash: eval: line 35: syntax error: unexpected end of file

ありゃ?35行目?

robamimimnoMacBook-Air:~ robamimim$ cat -n .bash_profile 
     1  # added by Anaconda3 5.3.0 installer
     2  # >>> conda init >>>
     3  # !! Contents within this block are managed by 'conda init' !!
     4  __conda_setup="$(CONDA_REPORT_ERRORS=false '/anaconda3/bin/conda' shell.bash hook 2> /dev/null)"
     5  if [ $? -eq 0 ]; then
     6      \eval "$__conda_setup"
     7  else
     8      if [ -f "/anaconda3/etc/profile.d/conda.sh" ]; then
     9          . "/anaconda3/etc/profile.d/conda.sh"
    10          CONDA_CHANGEPS1=false conda activate base
    11      else
    12          \export PATH="/anaconda3/bin:$PATH"
    13      fi
    14  fi
    15  unset __conda_setup
    16  # <<< conda init <<<
    17  eval "$(rbenv init -)"export PATH=$HOME/.nodebrew/current/bin:$PATH
robamimimnoMacBook-Air:~ robamimim$ 

おや?17行目までしかない。

~/.nodebrewがない。

robamimimnoMacBook-Air:~ robamimim$ ls $HOME/.nodebrew/current/bin
ls: /Users/robamimim/.nodebrew/current/bin: No such file or directory
robamimimnoMacBook-Air:~ robamimim$ 
robamimimnoMacBook-Air:~ robamimim$ ls $HOME/.nodebrew
ls: /Users/robamimim/.nodebrew: No such file or directory
robamimimnoMacBook-Air:~ robamimim$ 

whichではここにある。

robamimimnoMacBook-Air:~ robamimim$ which nodebrew
/usr/local/bin/nodebrew

どうしたことか。
Rubyの時も苦戦したけどまたここで!
再起動してみます。

robamimimnoMacBook-Air:~ robamimim$ ls .node*
ls: .node*: No such file or directory
robamimimnoMacBook-Air:~ robamimim$ source ~/.bash_profile
-bash: eval: line 35: syntax error: unexpected end of file
robamimimnoMacBook-Air:~ robamimim$ env | grep node

ディレクトリがないんだものね。

ちょっと戻ってインストールログを見直す。

######################################################################## 100.0%
==> Caveats
You need to manually run setup_dirs to create directories required by nodebrew:
  /usr/local/opt/nodebrew/bin/nodebrew setup_dirs

Add path:
  export PATH=$HOME/.nodebrew/current/bin:$PATH

To use Homebrew's directories rather than ~/.nodebrew add to your profile:
  export NODEBREW_ROOT=/usr/local/var/nodebrew

Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

zsh completions have been installed to:
  /usr/local/share/zsh/site-functions
==> Summary
?  /usr/local/Cellar/nodebrew/1.0.1: 8 files, 38.6KB, built in 19 seconds
==> `brew cleanup` has not been run in 30 days, running now...

Caveatsって「注目!」でしたっけ。
チュートリアルにはないけどやってみます。

robamimimnoMacBook-Air:~ robamimim$ /usr/local/opt/nodebrew/bin/nodebrew setup_dirs
robamimimnoMacBook-Air:~ robamimim$ ls .nodebrew
default iojs    node    src

おっ、.nodebrew ができた!
ここに.nodebrew/current/binはまだないけど・・・
NODEBREW_ROOTもないし

robamimimnoMacBook-Air:~ robamimim$ ls /usr/local/var/nodebrew
ls: /usr/local/var/nodebrew: No such file or directory

とりあえずenvにいれておく

export PATH=$HOME/.nodebrew/current/bin:$PATH
export NODEBREW_ROOT=/usr/local/var/nodebrew

nodebrewは使えるので、とりあえず進んでみる。

robamimimnoMacBook-Air:~ robamimim$ nodebrew help
nodebrew 1.0.1

あらら・・・

robamimimnoMacBook-Air:~ robamimim$ nodebrew install-binary stable
Fetching: https://nodejs.org/dist/v10.15.3/node-v10.15.3-darwin-x64.tar.gz
Warning: Failed to create the file                                                                                                                           
Warning: /usr/local/var/nodebrew/src/v10.15.3/node-v10.15.3-darwin-x64.tar.gz: 
Warning: No such file or directory
                                                                                                                                                          0.0%
curl: (23) Failed writing body (0 != 847)
download failed: https://nodejs.org/dist/v10.15.3/node-v10.15.3-darwin-x64.tar.gz

ダウンロードできない。ファイルが作れない。

robamimimnoMacBook-Air:~ robamimim$ ls /usr/local/var/nodebrew/src/
ls: /usr/local/var/nodebrew/src/: No such file or directory
robamimimnoMacBook-Air:~ robamimim$ ls /usr/local/var/
homebrew    mysql

ファイルもディレクトリもない。
うーん。。。

robamimimnoMacBook-Air:~ robamimim$ nodebrew use stable
No such file or directory at /usr/local/bin/nodebrew line 575.

だめか。。。

robamimimnoMacBook-Air:~ robamimim$ node -v
-bash: node: command not found

続きは後ほど。

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

Javascript(ES6)の開発環境、手間取りながらも完成!

せっかく作ったJSを動かしたい!
Javascript(ES6)の開発環境を作るチュートリアルをみながら環境を作っています。
しかしこれがまた、難しいのでした。

ここまでやったの記録です。

Homebrewの確認

robamimimnoMacBook-Air:~ robamimim$ brew -v
Homebrew 2.0.4
Homebrew/homebrew-core (git revision a290; last commit 2019-03-16)
Homebrew/homebrew-cask (git revision 757155; last commit 2019-03-18)
robamimimnoMacBook-Air:~ robamimim$ 

nodebrewインストール

robamimimnoMacBook-Air:~ robamimim$ brew install nodebrew
Updating Homebrew...
<以下略>
robamimimnoMacBook-Air:~ robamimim$ nodebrew -v
nodebrew 1.0.1

Node.jsインストール

.bash_profileにnodevrewを追加

robamimimnoMacBook-Air:~ robamimim$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
robamimimnoMacBook-Air:~ robamimim$ source ~/.bash_profile
-bash: eval: line 35: syntax error: unexpected end of file

ありゃ?35行目?

robamimimnoMacBook-Air:~ robamimim$ cat -n .bash_profile 
     1  # added by Anaconda3 5.3.0 installer
     2  # >>> conda init >>>
     3  # !! Contents within this block are managed by 'conda init' !!
     4  __conda_setup="$(CONDA_REPORT_ERRORS=false '/anaconda3/bin/conda' shell.bash hook 2> /dev/null)"
     5  if [ $? -eq 0 ]; then
     6      \eval "$__conda_setup"
     7  else
     8      if [ -f "/anaconda3/etc/profile.d/conda.sh" ]; then
     9          . "/anaconda3/etc/profile.d/conda.sh"
    10          CONDA_CHANGEPS1=false conda activate base
    11      else
    12          \export PATH="/anaconda3/bin:$PATH"
    13      fi
    14  fi
    15  unset __conda_setup
    16  # <<< conda init <<<
    17  eval "$(rbenv init -)"export PATH=$HOME/.nodebrew/current/bin:$PATH
robamimimnoMacBook-Air:~ robamimim$ 

おや?17行目までしかない。

~/.nodebrewがない。

robamimimnoMacBook-Air:~ robamimim$ ls $HOME/.nodebrew/current/bin
ls: /Users/robamimim/.nodebrew/current/bin: No such file or directory
robamimimnoMacBook-Air:~ robamimim$ 
robamimimnoMacBook-Air:~ robamimim$ ls $HOME/.nodebrew
ls: /Users/robamimim/.nodebrew: No such file or directory
robamimimnoMacBook-Air:~ robamimim$ 

whichではここにある。

robamimimnoMacBook-Air:~ robamimim$ which nodebrew
/usr/local/bin/nodebrew

どうしたことか。
Rubyの時も苦戦したけどまたここで!
再起動してみます。

robamimimnoMacBook-Air:~ robamimim$ ls .node*
ls: .node*: No such file or directory
robamimimnoMacBook-Air:~ robamimim$ source ~/.bash_profile
-bash: eval: line 35: syntax error: unexpected end of file
robamimimnoMacBook-Air:~ robamimim$ env | grep node

ディレクトリがないんだものね。

ちょっと戻ってインストールログを見直す。

######################################################################## 100.0%
==> Caveats
You need to manually run setup_dirs to create directories required by nodebrew:
  /usr/local/opt/nodebrew/bin/nodebrew setup_dirs

Add path:
  export PATH=$HOME/.nodebrew/current/bin:$PATH

To use Homebrew's directories rather than ~/.nodebrew add to your profile:
  export NODEBREW_ROOT=/usr/local/var/nodebrew

Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

zsh completions have been installed to:
  /usr/local/share/zsh/site-functions
==> Summary
?  /usr/local/Cellar/nodebrew/1.0.1: 8 files, 38.6KB, built in 19 seconds
==> `brew cleanup` has not been run in 30 days, running now...

Caveatsって「注目!」でしたっけ。
チュートリアルにはないけどやってみます。

robamimimnoMacBook-Air:~ robamimim$ /usr/local/opt/nodebrew/bin/nodebrew setup_dirs
robamimimnoMacBook-Air:~ robamimim$ ls .nodebrew
default iojs    node    src

おっ、.nodebrew ができた!
ここに.nodebrew/current/binはまだないけど・・・
NODEBREW_ROOTもないし

robamimimnoMacBook-Air:~ robamimim$ ls /usr/local/var/nodebrew
ls: /usr/local/var/nodebrew: No such file or directory

とりあえずenvにいれておく

export PATH=$HOME/.nodebrew/current/bin:$PATH
export NODEBREW_ROOT=/usr/local/var/nodebrew

nodebrewは使えるので、とりあえず進んでみる。

robamimimnoMacBook-Air:~ robamimim$ nodebrew help
nodebrew 1.0.1

あらら・・・

robamimimnoMacBook-Air:~ robamimim$ nodebrew install-binary stable
Fetching: https://nodejs.org/dist/v10.15.3/node-v10.15.3-darwin-x64.tar.gz
Warning: Failed to create the file                                                                                                                           
Warning: /usr/local/var/nodebrew/src/v10.15.3/node-v10.15.3-darwin-x64.tar.gz: 
Warning: No such file or directory
                                                                                                                                                          0.0%
curl: (23) Failed writing body (0 != 847)
download failed: https://nodejs.org/dist/v10.15.3/node-v10.15.3-darwin-x64.tar.gz

ダウンロードできない。ファイルが作れない。

robamimimnoMacBook-Air:~ robamimim$ ls /usr/local/var/nodebrew/src/
ls: /usr/local/var/nodebrew/src/: No such file or directory
robamimimnoMacBook-Air:~ robamimim$ ls /usr/local/var/
homebrew    mysql

ファイルもディレクトリもない。
うーん。。。

robamimimnoMacBook-Air:~ robamimim$ nodebrew use stable
No such file or directory at /usr/local/bin/nodebrew line 575.

だめか。。。

robamimimnoMacBook-Air:~ robamimim$ node -v
-bash: node: command not found

続きは後ほど。


パスなしでsetupを呼んでみる。

robamimim$ nodebrew setup
Fetching nodebrew...
Installed nodebrew in /usr/local/var/nodebrew

========================================
Export a path to nodebrew:

export PATH=/usr/local/var/nodebrew/current/bin:$PATH
========================================

おや。前回と違う応答。
.bash_profileexport PATH=/usr/local/var/nodebrew/current/bin:$PATHを加えておく。

robamimim$ nodebrew install-binary stable
Fetching: https://nodejs.org/dist/v10.15.3/node-v10.15.3-darwin-x64.tar.gz
########################################################################################################################################### 100.0%
Installed successfully

インストールできたし!

robamimim$ nodebrew use stable
use v10.15.3
robamimim$ node -v
v10.15.3

nodebrew完成!

npmパッケージの管理ファイルを作成


robamimim$ cd es6_sample/
robamimim$ ls
dist  src
robamimim$ npm init -y
Wrote to /Users/robamimim/Documents/JavaScript/es6_sample/package.json:

{
  "name": "es6_sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}


jsonできました!

babelのインストール

$ npm install --save @babel/core @babel/cli @babel/preset-env

> fsevents@1.2.7 install /Users/robamimim/Documents/JavaScript/es6_sample/node_modules/fsevents
> node install

node-pre-gyp WARN Using needle for node-pre-gyp https download 
[fsevents] Success: "/Users/robamimim/Documents/JavaScript/es6_sample/node_modules/fsevents/lib/binding/Release/node-v64-darwin-x64/fse.node" is installed via remote
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN es6_sample@1.0.0 No description
npm WARN es6_sample@1.0.0 No repository field.

+ @babel/core@7.3.4
+ @babel/cli@7.2.3
+ @babel/preset-env@7.3.4
added 329 packages from 155 contributors and audited 3493 packages in 20.96s
found 0 vulnerabilities

$ 

OK!

そのままではエラーになるスクリプトも・・・

$ node src/first.js
/Users/robamimim/Documents/JavaScript/es6_sample/src/first.js:1
(function (exports, require, module, __filename, __dirname) { import name from "./second";
<以下略>

babelでコンパイルすると動く!

$ ./node_modules/.bin/babel src --out-dir dist
Successfully compiled 2 files with Babel.
$ node dist/first.js
にんじゃわんこ

さらに、package.jsonにスクリプト"build": "./node_modules/.bin/babel src --out-dir dist"を登録

$ npm run build

> es6_sample@1.0.0 build /Users/robamimim/Documents/JavaScript/es6_sample
> babel src --out-dir dist

Successfully compiled 2 files with Babel.
$ node dist/first.js
にんじゃわんこ

babelを使って簡単にコンパイルができるようになりました!

完成!

nodebrewのパスの設定で行ったり来たり悩ました。
もしかしたらnodebrew setupを先にしたらすんなり行けるのかもしれません。

nodebrew setup
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
source ~/.bash_profile
##nodebrew setup

(所要時間 2時間)

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

TypeScript の概要

概要

AltJS としての言語 TypeScript の特徴をかいつまんで説明します。

対象者

  • JavaScript でコードを書いたことがある。
  • JavaScript でプログラムを書くのが辛い or もっと楽したい。
  • TypeScript ってなに?聞いたことはあるけど・・・

・・・な人。

TypeScriptの参考資料

AltJS (Alternative JavaScript) とは

AltJS とは、JavaScript の代わりとなる 言語の総称です。

その言語で書いたものを JavaScript に変換して使用します。

TypeScript の他には、CoffeeScript, Opal(Ruby から JavaScript に変換するツール)などがあります。

なぜ、そんな面倒くさいことをするのか、JavaScript がある定常以上の規模となると、下記の理由から実装・保守の効率が非常に悪くなります。

  • 型の定義がないので、意図しない値が入ることがある。
  • null safety でないので、意図しない null や undefined が入ることがある。
  • オブジェクト指向言語だが、インターフェースやクラス定義がなく、プロパティ名を間違っていても実行時までエラーにならず、エラーになっても原因の解析に時間がかかることが多い。
  • 型やインターフェース、クラス定義がないので、エディタによる入力補完があまり受けられない。

TypeScriptとは

マイクロソフトが開発したオープンソース言語で、一言で言うと、「型定義できるJavaScript」。

他の Alt JavaScript と比べて後発ながら人気が高く、Google の6番目の社内標準言語としても採用されました。

AltJS としては、Coffee Script が牽引してきましたが、最近では TypeScript に取って代わられた感じがあります。

参考:
* Githubの調査で、2018年急成長している言語ランキング3位
* Google社内の標準言語としてTypeScriptが承認される。ng-conf 2017 - Publickey

CoffeeScript について
* CoffeeScriptは本当に駄目なのか? - Qiita

引用: CoffeeScript が駄目でない事を理解していただけたと思う。いつまでも素の JavaScript しか使えない JSer はそのうちいらなくなることは必至だろう。今すぐ、素の JavaScript を捨てて、TypeScript を使うことをお勧めする。

はい、、、あ?、え?

AltJS とは全く別なアプローチとしてWebクライアント開発問題を解消するものとして、WebAssembly というものもあります。これは、JavaScriptでなくC/C++、Rustで書いたコードをバイナリコードに変換し、ブラウザで動かすことができる仕様です。
Chrome, FireFox, Edge, Safari など主要なブラウザでサポートされています。・・・がみんな大好き IE はサポートしていませんし、将来も無いでしょう。
将来的には、大規模なWebアプリはこちらに移っていく可能性もあるので、要ウォッチです。
WebAssemblyの紹介(2018年冬版) - Qiita

特徴としては

  • JavaScript のスーパーセット(上方互換)となっている。
    • JavaScriptの最新仕様である、"ES2018" の構文仕様が使える。
  • 型定義が使える。
  • インターフェース、クラスがつかえる。
  • null/undefined safe にできる。
  • 型定義ファイルがあれば外部ライブラリも型を利用できる。
  • ジェネリックが使える
  • エディタによる入力補完が強力。(Visual Studio Code など対応しているエディタ)

上で書いたように、TypeScriptで書いたコードをコンパイルをかけることで JavaScript に変換し、ブラウザ等で利用することになります。

TypeScriptでは、型の指定を矯正するなどの制約を設けることができ、コンパイルをするときに、制約のチェックを行います。そうすることで、JavaScript では実行時にしかわからないバグを未然に防ぐことができるのです。

各特徴について、詳しく説明していきます。

JavaScript のスーパーセット(上方互換)となっている

TypeScript は、JavaScriptの仕様を拡張したものになっているので、JavaScriptで書いたものは、TypeScript としても有効です。

例えば、if や for, case といったステートメントや、{a: "hoge"} や ["a", "b"] といったオブジェクトや配列のリテラルはそのまま使えます。

ただし、TypeScript のコンパイラオプションで、制約をつけている場合、その制約によって、コンパイルでエラーになる場合があります。
例えば、allowUnusedLabels を false にしていると、未使用の変数があったらコンパイルでエラーになります。(このプロパティはデフォルトで off ですが)

ですので、JavaScriptを使ったことがあれば、TypeScript の学習コストは低いです。

また、TypeScript は、JavaScript の最新の仕様(正確には、ECMAScript の仕様)を取り込んでいっているので、その学習にもなります。

さらに、それを ES5 などの古いバージョンの JavaScript に変換するトランスパイラとしても機能もあります。

Interface やClass の定義は、C# や Java のそれに似ているので、それらの言語を知っていれば、さらに学習しやすいでしょう。

型定義が使える

型定義の基本

JavaScript からの拡張として、変数の型定義があります。型定義には、さまざまな仕様があります。

変数に定義した型と割り当てた値の型が違う場合、コンパイルする際にエラーとなり、事前にバグに気づくことができます。

基本的な変数の型定義はこちら。

let name: string; // name を文字列型として宣言
name = "ebihara";
name = 0; // エラー: 文字列ではない

配列の場合、JavaScriptでは 文字列や数値を混在できますが、基本的には1つの型の値が入るとおもいます。TypeScriptではそのような制限ができます。

const array: string[] = [];
array.push("ebihara");
array.push(1); // エラー:配列の型と合わない

関数の引数と戻り値の型

関数の引数と戻り値にも型を指定できます。

戻り値の型を指定することで、関数を利用するときだけでなく、関数内の return の値もチェックされます。

function getName(id: string): string {
    // なんか処理
    return "hoge";
}
const name = getName("xxx");
console.log(name.length); // 4

function getAge(id: string): number {
    // なんか処理
    return 20;
}
const age = getAge("xxx");
console.log(age.length); // エラー: age は number 型で length は無い

const age2 = getAge(); // エラー: 定義された引数を渡していない

function getAge2(): number {
    return "hoge"; // エラー: 戻り値の方が異なるのでエラー
}

引数が必須でない場合もあります。その場合は、引数名の後ろに ? をつけます。

function getName(id?: string): string {
    // id は、string か undefined の可能性がある。
    return "hoge";
}
const name = getName("xxx");
console.log(name.length); // 4
// 型定義の後ろに = で値を渡すと、デフォルト値となる
function getName2(id?: string = "xxx"): string {
    // id に値が渡されない場合、"xxx" が入る。
    return "hoge";
}

オブジェクト型

いくつかのプロパティを持ったオブジェクトを変数や関数で扱う場合があります。その場合もプロパティの名前とその型が参照されます。

const user: { name: string, age: number} = {
    name: "ebihara",
    age: 44,
};
console.log(user.name); // ebihara
console.log(user.aga); // エラー: プロパティ名が間違っている
console.log(user.age.length); // エラー: age は number 型で length プロパティはない

関数型

JavaScript は関数型言語の仕様も一部取り混んでいることもあり、Java や C# と異なり、関数をオブジェクトのように扱うことができます。

つまり、関数だけで独立できたり、関数の引数に関数を与える、オブジェクトのプロパティとして関数を割り当てる、といったことができます。

TypeScriptではその関数の仕様も定義できます。

interface IUser {
    name: string;
    // 1つの文字列の引数、戻り値が文字列の関数を定義している
    getName: (keisho: string) => string;
}

class User implements IUser {
    name: string;
    // ここで、引数の型や数が違ったり、戻り値の型が違うと エラーとなる
    getName (keisho: string) {
        return `${this.name} (${keisho})`;
    }
}

const user = new User();
user.name = "ebiahra";
console.log(user.getName("さん"));

interface については、後述します。

また、アロー演算子(=>)が出てきていますが、これは、function の別な書き方です。

全く同じか、というとそうでもないので、これについては、こちらを御覧ください。

【JavaScript】アロー関数式を学ぶついでにthisも復習する話 - Qiita

型の推論

変数の宣言時に値を渡すと、型指定をしなくても自動的に型が宣言されたように振る舞います。

const age = 10;
console.log(age.length); // エラー: age は number 型となるので、length プロパティはない。
const user = {
    name: "ebihara",
    age: 44,
};
console.log(user.age.length); // エラー: age は number 型で length プロパティはない

関数の戻り値も同様です。

function getUser() {
    return {
        name: "ebihara",
        age: 44,
    };
}
const user = getUser();
console.log(user.age.length); // エラー: age は number 型で length プロパティはない

union型

JavaScript の場合、変数や引数に複数の型の値が入ることが多いです。

TypeScript ではそれも実現できますが、プロパティを参照するときなど、キャストが必要になる場合があります。

let nameOrAge: string | number = "ebihara";
nameOrAge = 12;
nameOrAge = true; // エラー: string か number の値のみ入る
console.log(nameOrAge.toString()); // どちらも持っているメンバーなら参照可能
console.log(nameOrAge.length); // エラー: 型が特定されていない
if (typeof nameOrAge === "string") { // length プロパティを持っているか
    console.log(nameOrAge.length); // 自動的に string にキャストされる
}
console.log((nameOrAge as string).length); // 明示的にキャストしても良い、が、number が入り込む可能性があるので注意

文字列リテラル型

文字列型だけど、決まった値しか入らない、ということも多いかと思います。

例えば、データの状態を示す値などです。これも TypeScript で制御することができます。

let status: "create" | "edit" | "view";
status = "creata"; // エラー: スペルミスで宣言にない文字列が割り当てられている。

その他

それ以外にも、タプル型、インデックスシグネチャ などまだ多くの機能があります。

こちらのページが詳しいので、参考にしてください。

TypeScriptの型入門 - Qiita

インターフェース、クラスがつかえる

Java や C# などと同じようにインターフェースとクラスの定義ができます。

実は、クラス定義は JavaScript の ES2015 というバージョンで導入されていますが、TypeScriptは更にそれを拡張しています。

interface IUser {
    name: string;
    age: number;
}

class User implements IUser {
    // インターフェースの実装なので、同じプロパティ定義が必要
    public name: string;
    public age: number;
    // getter setter の指定が可能
    public get displayName() {
        return `${this.name} ${this.keisho} (${this.age})`;
    }
    // puglic private のアクセサが使用可能
    private keisho = "さん";
    // コンストラクタの定義 new したときに呼ばれる
    public constructor() {
        this.name = "";
        this.age = 0;
    }
}

const user = new User();
user.name = "ebihara";
user.age = 44;
console.log(user.displayName);
user.displayName = "aa"; // エラー: 参照専用のプロパティ
user.keisho = "くん"; // エラー: 公開されていないプロパティ

// インターフェースから直接リテラルでオブジェクトを生成できる
const user2: IUser = {
    // インターフェースで定義されたプロパティが指定されなかったり、型が違うとエラーになる
    name: "ebihara",
    age: 44,
}

null/undefined safe にできる

JavaScript を書いていて、"Uncaught TypeError: Cannot read property 'propname' of undefined" というエラーに遭遇したことのない人はいないでしょう。

JavaScript は簡単に undefined が入り込んで、実行時エラーを引き起こします。

参考: null安全でない言語は、もはやレガシー言語だ - Qiita

それを防ぐために、コンパイラオプションで、"strictNullChecks" を on にすると、null や undefined である可能性の変数がチェックできるようになります。

具体的には、コードを見てみます。

class Sample {
    public name: string; // エラー: 初期値を定義していないので、 undefined の可能性がある。
    public name2: string = ""; // 初期値を指定しているので、OK
    public name3: string | undefined; // 明示的に undefined のこともあるのを定義しているので、OK
    public name4?: string; // name3 と同じ意味
    public name5: string | null; // エラー: null と undefined は別の値
    public fn() {
        console.log(this.name3.length); // エラー: undefined の可能性が排除できていない排除できていない
        if (!!this.name3) {
            console.log(this.name3.length); // undefined の可能性が事前チェクで来ているのでOK
        }
        console.log((this.name3 as string).length); // 明示的にキャストしてもOK。ただし実行時エラーの可能性あり
    }
}

!重要! null safety だからといって、null や undefined のエラーが張り込まないわけではありません。サーバーからのデータの受信、ユーザーの入力など外部からの入力があるからです。当然それらの値について、チェックを掛ける必要があります。

型定義ファイルがあれば外部ライブラリも型を利用できる

JavaScript でWebアプリ等を作成する場合は、React や jQuery、moment などのライブラリを利用することが多いと思います。

それらのライブラリの言語は当然、JavaScript で提供されているので、そのライブラリにどういったオブジェクトや関数が用意されているので、関数の引数、戻り値は何なのかは、ライブラリの仕様書を見ないとわかりません。

TypeScript では、ライブラリがTypeScript用の型定義ファイルを提供していることがあり、それがあれば TypeScript でライブラリのAPI情報がわかり、コンパイル時にチェックすることができます。

TypeScript 用の型定義ファイルは、npm リポジトリに @types/[ライブラリ名] で提供されています。

例
> npm install --save-dev @types/jquery

https://www.npmjs.com/package/@types/jquery

また、ライブラリに同梱されている場合もあります。

最近は、TypeScript で書いて JavaScript で提供するライブラリも多いことから、同梱している物も増えています。

TypeScript が急速にシェアを伸ばしていることから、ほとんどのメジャーな JavaScript ライブラリに、型定義ファイルが提供されています。

型定義ファイルがない場合は、自分で ".d.ts" を書くことで回避することもできます。大抵は、その中に any 型(型定義されない型)を定義してコンパイルを通す手段となります。その場合、型定義のチェックがされないことになるので、十分に注意してコーディングします。

型定義ファイルの拡張子は、".d.ts" です。

ジェネリックが使える

薬の話・・・ではなく、

ジェネリックとは、クラスや関数で、その中で使う型を抽象化し、外部から指定できるよにし、そのふるまいを変えることができるものです。

って言われてもはじめての人には難しいですよね。

ジェネリックは、主に汎用的な関数やクラスを定義するときに使われます。

C# や Java ではなじみがあると思いますが、それと同等の機能です。

例えば、任意の型の配列と、呼び出すたびに配列を順番に取り出していく関数をもつクラスを作ってみます。

ジェネリックを使えば配列は任意の方の配列ですが、それを利用するときに型を決めることができます。

class Iterator<T> { // <T> がジェネリックの宣言 Tは仮の名前
    // クラス内では、T型 として扱う
    private currentIndex: number = 0;
    private array: T[] = []; // 外から与えられる型で配列を定義する
    // 配列に値を登録 引数は、T型 である必要がある
    public push = (value: T) => {
        this.array.push(value);
    }
    public get = (): T => { // 関数の戻り値の型は T型
        // this.currentIndex が配列の数を超えたときの確認とかは省略。
        // this.array は、Tの配列なので、value は T型
        const value = this.array[this.currentIndex];
        this.currentIndex += 1;
        return value;
    };
}

const numberIterator = new Iterator<number>(); // 数値型を扱うと宣言
numberIterator.push(999);
numberIterator.push(998);
numberIterator.push("ghi"); // number 型ではないのでエラー

const a = numberIterator.get();
console.log(a.length); // a は number 型でないのでエラー

エディタによる入力補完が強力

エディタが TypeScript の入力支援機能をサポートしている場合、型定義から入力候補を表示する事ができます。

これのために、TypeScriptを採用していると言ってもいいくらい便利な機能で、開発効率が格段に違います。

TypeScript と同じマイクロソフトが開発している、 Visual Studio Code がおすすめです。

入力補完の例:
image.png

まとめ

TypeScript の主だった機能を紹介しました。

ここで説明した以外にも機能はまだあるので、公式ページ等と見てください。

近年、JavaScrpt は Webアプリケーション のみにかかわらず node.js でのサーバーサイド処理や、Electron でのデスクトップアプリ、React Native を使ったモバイルアプリなど、その活用範囲は広がっています。

TypeScript はそういった中~大規模になるプロジェクトで活躍する事ができる言語ですので、是非身につけてください。

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

Nuxt.js + TypeScript + Vuetify でWebアプリ開発環境を構築する

1. はじめに

社内で Nuxt.js + TypeScript + Vuetify の構成で、SPAを開発しました。
その際に開発効率が高くて良いと感じたので、環境構築や開発の方法をご紹介します。

良いなと思ったことは下記の3点です。

  • Nuxt.jsを使うことで環境構築が簡単に行える
  • TypeScriptを使うことで型の恩恵を受けながら開発を行える
  • Vuetifyを使うことでマテリアルデザインのUIを手軽に扱える

今回は下の画像のようなSPAを作成するところをゴールとします。
現在の時刻から、今年が何%進んでいるかをプログレスバーで表示します。

Screen Shot 2019-03-14 at 22.28.51.png

※この記事ではSSRは取り扱いません。ご了承ください。

2. 技術紹介

この記事で取り扱う下記の言語やフレームワークについて概要をご紹介します。

  1. Nuxt.js
  2. TypeScript
  3. Vuetify

2.1. Nuxt.js

Nuxt.js は、 Vue.js のアプリケーションを構築するためのフレームワークです。
Vue.js で開発する際に必要になることが多い、下記のようなフレームワークを含んでいます。

  1. Vue-Router
  2. Vuex
  3. Vue Server Renderer

本来であれば上記のフレームワークが必要かどうか検討し、必要なものを組み合わせる設定をしなければいけないのですが、 Nuxt.js を使用すればこれらの面倒から開放されます。

2.2. TypeScript

TypeScript はマイクロソフトによって開発された言語で、 JavaScript に静的型付けやクラスベースのオブジェクト指向といった機能を追加したような言語です。

2.3. Vuetify

Vue.js をベースに開発されたUIフレームワークで、下記のような特徴があります。

  1. マテリアルデザインを手軽に扱える
  2. ドキュメントやサンプルのソースコードが豊富

特に後者の「サンプルが豊富」である点が、他のフレームワークに比べていいところだと思います。

3. 構築手順

上記でご紹介した技術が使えるような環境を構築する手順をご紹介します。
下記の4つの手順に分けてご紹介します。

  1. テンプレートのプロジェクトから始める
  2. UIのソースコードを削除する
  3. Vuetifyを導入する
  4. 自分のアプリを実装する

手順1と2でプロジェクトの準備、手順3と4でアプリの実装を進めます。

3.1. テンプレートのプロジェクトから始める

Nuxt.js では環境構築を簡単にするめにいくつかのテンプレートが準備されています。
今回は TypeScript 用のテンプレートを使用します。

まずは vue-cli のインストールです。
この記事ではグローバルにインストールします。
コマンドを実行してバージョン番号が表示されたら問題なしです。

$ npm install -g @vue/cli

$ vue --version
> 3.5.1

$ npm install -g @vue/cli-init

$ vue init --version
> 3.5.1

 

vue-cli の準備が整ったら、いよいよテンプレートからプロジェクトを作成します。
下記ページを参考に進めます。
https://github.com/nuxt-community/typescript-template

$ vue init nuxt-community/typescript-template my-project

プロジェクト名などを聞かれますので適宜答えてください。
プロジェクトが作成されたら早速ローカルで動かしてみましょう。

$ cd my-project

$ npm install

$ npm run dev

 

ブラウザで localhost:3000 に接続してページが表示されたら成功です。

Screen Shot 2019-03-14 at 21.23.40.png

3.2. UIのソースコードを削除する

次に、UIのソースコードを削除して自分のアプリを作る準備をします。
現在はロボットの顔が表示されているので、これが表示されないようにします。
言語が TypeScript なので、エディタはVisual Studio Codeがおすすめです。
https://code.visualstudio.com/

この記事の対象外になっているSSRの記述部分を削除します。
my-project/store/index.tsnuxtServerInit() を削除してください。
その際に不要になった import もあれば削除してください。

// 中略

export const actions: ActionTree<RootState, RootState> = {

  // ここから下を削除

  async nuxtServerInit({ commit }, context) {
    let people: Person[] = []

    people = context.isStatic ?
      localRandomData :
      await context.app.$axios.$get("./random-data.json")

    commit("setPeople", people.slice(0, 10))
  }

  // ここから上を削除

}

コードを削除して保存すると、自動で再ビルドが走り、すぐにブラウザで変更を確認できます。
変更が反映されていないと思ったら念の為リロードしてみてください。
Nuxt TypeScript Template の文字だけがページに表示されていたら成功です。

Screen Shot 2019-03-14 at 21.37.59.png

3.3. Vuetifyを導入する

次にUIフレームワーク Vuetify を導入していきます。
まずは Ctrl + cnpm run dev を停止させ、下記コマンドで Vuetify を依存性に追加します。

$ npm install @nuxtjs/vuetify

次に設定ファイルに @nuxtjs/vuetify を追記します。

export default {
  // 中略
  modules: [
    "@nuxtjs/axios",
    // 以下の行を追記
    "@nuxtjs/vuetify"
  ],
}

なんとこれだけで Vuetify が使えるようになっています。
導入が簡単なフレームワークは嬉しいです。
早速 Vuetify を使ってみるために、ページを編集していきます。
my-project/pages/index.vue を編集します。

<template> 内を下記のように書き換えます。
少し行数が多いですが、ほとんど Vuetify のドキュメントやサンプルのページに載っているものなので安心してください。
https://vuetifyjs.com/ja/components/navigation-drawers

<template>
  <v-app>
    <v-navigation-drawer app permanent>
      <v-toolbar flat>
        <v-list>
          <v-list-tile>
            <v-list-tile-title class="title">
              Sample App
            </v-list-tile-title>
          </v-list-tile>
        </v-list>
      </v-toolbar>

      <v-divider></v-divider>

      <v-list dense class="pt-0">
        <v-list-tile
          v-for="item in items"
          :key="item.title"
        >
          <v-list-tile-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-tile-action>

          <v-list-tile-content>
            <v-list-tile-title>{{ item.title }}</v-list-tile-title>
          </v-list-tile-content>
        </v-list-tile>
      </v-list>
    </v-navigation-drawer>

    <v-content>
      <h1 class="header">Nuxt TypeScript Starter</h1>
    </v-content>
  </v-app>
</template>

<v-xxx> のように、 v から始まる要素は Vuetify で準備されている要素です。
これらを組み合わせることで簡単にマテリアルデザインのUIを構築することができます。

保存すると、これだけでナビゲーションドロアーが実装できました。
手軽にUIを構築できるので Vuetify はいいですね。

Screen Shot 2019-03-14 at 21.53.21.png

3.4. 自分のアプリを実装する

最後に目標の、今年のプログレスバーを実装したいと思います。

まずはナビゲーションドロアーのメニューを作ります。
今回はVueコンポーネントにメニューに関するデータをもたせたいと思います。
my-project/pages/index.vue を下記のように編集します。

// 中略

@Component({
  components: {}
})
export default class extends Vue {

  items: Object[] = []

  mounted() {
    this.items = [
      {
        title: 'Progress',
        icon: 'timer'
      }
    ]
  }
}

ビューコンポーネントに items というフィールドを持たせ、 mounted() が実行された時点で初期化しています。

mounted() は特別なメソッドで、ビューコンポーネントがページにロードされた時に実行されるライフサイクルです。

実は 3.3 で書いた <template> の中に、この items を参照する記述があります。
v-for="item in items"{{ item.title }} がこれに該当します。
この様に「データや処理に関する記述」と「デザインに関する記述」を分けて書くことができるのが、 Vue.jsNuxt.jsのいいところだと思います。

次にプログレスバーを準備し、UIを作っていきます。
下記のように <v-content> 内を書き換えます。

// 中略

    <v-content class="bar-container">
      <p class="title">
        This year progress
      </p>
      <v-progress-linear
        v-model="yearProgress"
      ></v-progress-linear>
      <p class="percentage">
        {{ `${this.yearProgress.toFixed(6)}%` }}
      </p>
    </v-content>

<v-progress-linear> という要素を書くだけでプログレスバーを実現できます。
さすが Vuetify、便利です。

このままだと yearProgress が定義されていないというエラーになってしまいます。
ビューコンポーネントにフィールドを追加し、初期化するようにしましょう。

@Component({
  components: {
  }
})
export default class extends Vue {
  items: Object[] = []
  yearProgress: number = 20

  mounted() {
    this.items = [
      {
        title: 'Progress',
        icon: 'timer'
      }
    ]
  }
}

最後にCSSを整えましょう。

// 中略

<style scoped>

.title {
  font-family: 'Roboto', sans-serif;
  font-size: 20px;
  text-align: center;
}

.percentage {
  font-family: 'Roboto', sans-serif;
  font-size: 18px;
  text-align: center;
}

.bar-container {
  margin: 40px;
}

</style>

ここまでの記述で20%のプログレスバーができました。

Screen Shot 2019-03-14 at 22.20.56.png

あとはこの yearProgress を動的に計算していくだけです。
ビューコンポーネントを下記のように書き換えます。

@Component({
  components: {}
})
export default class extends Vue {

  items: Object[] = []

  yearProgress: number = 0

  mounted() {
    this.items = [
      {
        title: 'Progress',
        icon: 'timer'
      }
    ]

    this.start()
  }

  start() {
    setInterval(() => {
      const date: Date = new Date()
      const numDaysOfMonth: number = this.calculateDaysOfMonth(date.getFullYear(), date.getMonth())

      const seconds: number = date.getSeconds() + date.getMilliseconds() / 1000.0
      const minutes: number = date.getMinutes() + seconds / 60.0
      const hours: number = date.getHours() + minutes / 60.0
      const days: number = date.getDate() + hours / 24.0
      const months: number = 1.0 * date.getMonth() + days / numDaysOfMonth

      this.yearProgress = 100.0 * months / 12
    }, 100)
  }

  calculateDaysOfMonth(year: number, month: number) {
    return new Date(year, month, 0).getDate()
  }
}

start() が実行されると、その後100msごとに今年の進捗を計算するように実装しました。
計算結果は yearProgress に格納しています。

もしこの実装中に型を間違えるような記述をすると、ビルド時にエラーとなります。
TypeScript を採用したことで、型の間違いを早く知ることができます。

ここまでで下記のような、今年の進捗を表示するアプリが完成しました。

Screen Shot 2019-03-14 at 22.28.51.png

4. まとめ

この記事では Nuxt.js, TypeScript, Vuetify の環境構築方法と、簡単なアプリケーションの実装をご行いました。
モダンなフレームワークを取り入れることで下記のようなメリットを享受できました。

  1. Nuxt.jsとそのテンプレートによる簡単な環境構築
  2. TypeScriptによる静的型付けの恩恵
  3. Vuetifyによる簡単でリッチなUIの組み立て

現在Web開発のフレームワークは他にもありますが、この記事を読んで Nuxt.js, TypeScript, Vuetify を使ってみようと思ってくださる方がいらっしゃれば幸いです。

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

geoguessrに感化されて駅名当てクイズをleafletと国土地理院地図で作ってみた

前置き

geoguessrとは?

Googleのストリートビューを利用した場所あてクイズで、看板や車両から国・地域を推定していく地理好きにはたまらないゲーム。以前このサイトを初めて目にした際その面白さにドハマリしました。
地理ネタと鉄道ネタと最近APIが改定されたGoogleMapに代わるものとして知った leaflet の勉強の合わせ技として、「この鉄道の駅は何駅でしょうゲーム」を作成しました。

leafletとは?

GoogleMapと同じように画面上にマップを生成するJavascriptのオープンソースライブラリーです。つかいかたもGoogleMapAPIとあまり変わらない感じで使いやすいと思います。また、このleaflet、地図情報のタイルを変えることで地図以外でも、地質図やドラクエなどのオリジナルマップの表示、画像表示の用途にも使えるみたいです。

実際にできたもの

GitHubPagesで簡単に公開できるらしいのでせっかくなので利用してみました。

STATION_GUESSR

どう作るか

必要なデータ

  1. マップ・空中写真
  2. 駅の座標データと駅名データ

1番目のデータにははじめは国土地理院の地図を利用するつもりでしたが、よく考えてみれば地図には駅名が書かれているわけでクイズになりません。ということで空中写真を利用することになりました。

(ちなみにGoogleMapを利用すれば、駅名などのラベルを消した、道路や路線しか書かれていない地図を利用することができるのですが、手続きが面倒なのであきらめました。)

2番目のデータは駅データ.jpという、求めていたものが全て入った完璧なサービスがあったのでそれを利用させていただくことにしました。ありがとうございます。

また、APIで呼び出す事も考えましたが、駅名当てクイズとあまり相性の良くないデータ型だったため、断念してデータをダウンロードで利用する形になりました。

流れ

  1. 駅データからランダムで一つ出題する駅を抽出する
  2. 正解となるその駅を地図に表示する。
  3. 他のダミー選択肢を選んでくる。
  4. 回答者によって選択された駅の正誤の判断をする。

必要な技術

  • 根幹となるページの動きにはVueを利用(使いやすいですね。)
  • デザインは面倒なのでbootstrap4を利用
  • マップは先述の通りleaflet.js

ソースコード

自分の勉強も兼ねて解説していきます。

以下はメインページの抜粋です。

index.html
...

        <div id="app">

<!-- この要素内がvueで操作するものとなります。-->

            <div class="modal" id="answer_area" tabindex="-1" role="dialog">
                <div class="modal-dialog" role="document">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h5 class="modal-title">正解は...</h5>
                            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
                        </div>
                        <div class="modal-body">
                            <p>{{ train_line_name }}</p>
                            <h2>{{ train_station_name }}</h2>
                            <h3> {{ result }}</h3>

<!-- vue では{{}}の中に変数名を書くとそこを置き換えて表示してくれます。
jQuerryとはまた違ったやり方ですが、こちらの方が直接的で理解しやすいと思います。-->

                        </div>
                        <div class="modal-footer">
                            <button type="button" class="btn btn-primary " v-on:click="restart">次へ進む</button>

<!-- このボタンをクリックするとvue側で定義したrestart関数が発動する。-->

                            <button type="button" class="btn btn-secondary" v-if="!IsCorrect" data-dismiss="modal">戻って見てみる</button>

<!-- 不正解の場合はこのボタンが表示され、見直すことができる。-->

                        </div>
                    </div>
                </div>
            </div>

            <div id="quiz_area">
                <ul class="text-center">
                    <li class="btn btn-outline-info btn-lg" v-for="option in options" v-on:click="answer">{{ option.split(",")[2] + "駅"}}</li>
                </ul>

<!-- JS側で作成した選択肢が入ったoptionsという配列内の要素をlist表示します。
このようにちょっとした操作をわざわざJSで行わなくても表示できるのがvueのいい所です。-->

            </div>
        </div>

        <div id="map">

<!-- ここにマップが入ってきます。CSSでサイズをうまく設定しないと
高さが0などになって表示されないことがあるので注意が必要 -->

</div>

...

要点

  • 地図を表示する際、他の要素内に組み込むとサイズが0になることがあるので、CSSで指定してあげなければならないです。詳しくは検索してみるといろいろ出てきます。

index.html
<!-- body下部に書くvueのメインとなる部分です。-->
    <script>
//以下のURLで国土地理院から持ってくる地図の種類を変更できます。

        //通常地図:https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png 
        //空中写真:https://cyberjapandata.gsi.go.jp/xyz/ort/{z}/{x}/{y}.jpg 
        //シームレス画像:https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg 

        var quiz_data;

        var map = L.map('map');

//mapというidが付与された要素にmapを描画します。
        L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg', {
            attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>|駅データ:<a href='http://www.ekidata.jp' target='_blank'>駅データ.jp</a>"
        }).addTo(map);

//タイルレイヤーに国土地理院の地図を入れます。
//また、地図の右下に引用元を記載できます。

        var app = new Vue({
            el: '#app',
            data: {
                train_line_name: '',//正解とする路線名
                train_station_name: '',//正解とする駅名
                IsCorrect: false,//正解かどうか
                options: [],//選択肢
                result:""//「正解」か「不正解」を表示する欄
            },
            methods: {

//ここで独自の関数を設定できます。

                answer: function(event) {

                        $("#answer_area").modal('show');

//jQuerryでやる以外の簡単な方法が思いつきませんでした。
//bootstrapのJS操作ととvueの親和性はあまり良くないみたいですね。
//何か簡単にできる方法がありましたら教えていただけるとありがたいです。

                        if(event.target.innerText == quiz_data.answer_data.name + "駅"){

//ちょっとここは問題がありそうですが、大目にみてくださいm(_ _)m

                            app.result = "正解❗";
                            app.IsCorrect = true;

                        }else{
                            app.result = "❌不正解❌";
                            app.IsCorrect = false;
                        }

                },
                restart: function(event){

                    $("#answer_area").modal('hide');

                    render_station();

//station.js側で定義した関数です。

                }
            }
        })
    </script>

要点

leafletはタイルを変えるだけで様々な地図を表示できる。
BootstrapをJSで動かす際、やはりjQuerryが必要となるので、jQuerryを使わず簡単にできる方法はまだ模索中。何かあったら教えていただけると幸いです。


以下はJSのファイルです。

station.js
//</body>タグの直前で読み込みます。

//定義する関数たち

//render_station() : 地図の情報・選択肢の情報を統合して画面に表示します。
//get_station() : 駅データが格納されたcsvファイルからランダムに4つ駅データを取得してきます。
//map_render(map,駅のデータ) : 地図に駅の座標データを取り込んで表示します。
//get_line_name(駅データ) : 駅データ内に路線情報がないのでもう一つのデータから路線名を検索して引っ張ってくる。
//random_int_array(最大,個数) : ランダムな整数の配列を返す。

//基本的に大きなデータを扱うのでpromiseを使っています。
//まだpromiseの扱いには慣れていないので不適切なやり方などあると思いますので、何かありましたら教えていただけると幸いです。

//駅データの型:
// lat: "42.626353"
// line_id: "11102"
// line_name: Array(2)
//     0: "JR函館本線(長万部~小樽)"
//     1: "ハコダテホンセン"
// line_pref: "1"
// lon: "140.313353"
// name: "蕨岱"

function render_station() {

    get_station().then((back) => {

//backに選択肢・正解・正解となる駅のデータが格納されて返ってくる。

        quiz_data = back;

        return map_render(map, back);

    }).then((back)=> {

        app.options = back.options;

//駅データ選択肢をリスト表示します。

    }).catch(e => {

        console.log("ERROR",e);

    });

}

const get_station = function() {
    return new Promise(function(resolve, reject) {

        var link = "station_data/stations_kanto.csv";

        var options = 4;

        var req = new XMLHttpRequest(); // HTTPでファイルを読み込むためのXMLHttpRrequestオブジェクトを生成
        req.open("get", link, true); // アクセスするファイルを指定
        req.send(null); // HTTPリクエストの発行

        req.onload = function() {

            //読み込まれ次第、ランダムに一要素をとってくる。

            var str = req.responseText;
            var list = str.split("\n");
//list:全駅データの配列

            var station_options = [];//正解・ダミーを含む選択肢
            var answer_station_data = {};//正解データだけ

            var random_numbers = random_int_array(list.length, options);//[4つのランダムな路線番号]
            var answer_num = Math.floor(Math.random() * (options));//0~3のうち一つのランダムな数字(正解となる選択肢が4つのうち何番目かを指定。)

            for(var i=0; i<random_numbers.length; i++){

                station_options.push(list[random_numbers[i]]);//選択肢配列に格納

            }

            var station_chosen = list[random_numbers[answer_num]].split(",");

//正解となる選択肢を全駅データの配列から選択してくる。

            get_line_name(station_chosen[5]).then(line_name => {

                answer_station_data.name = station_chosen[2];
                answer_station_data.lon = station_chosen[9];
                answer_station_data.lat = station_chosen[10];
                answer_station_data.line_id = station_chosen[5];
                answer_station_data.line_pref = station_chosen[6];

                answer_station_data.line_name = line_name;

//正解となる駅データの情報を整理して格納。

                    resolve({
                    options : station_options,
                    answer :  station_chosen,
                    answer_data : answer_station_data
                    });

                });
        }
    });
};

const map_render = function(l_map, quiz_data) {
    return new Promise(function(resolve, reject) {

        let station_data = quiz_data.answer_data;

        l_map.setView([station_data.lat, station_data.lon], 16);

//駅の座標を中心としてマップをセット

        L.marker([station_data.lat, station_data.lon]).addTo(map);

//駅の座標にピンを立てる

        app.train_station_name = station_data.name + "駅";
        app.train_line_name = station_data.line_name[0];

//vueのプロパティに答えのデータをセット

        resolve(quiz_data);

    });
}

function random_int_array(max, how_many){

//ランダムにmax以下のhow_many個の整数を配列で返す関数です。

    var result = [];

    for(var i = 0;i<how_many; i++){

        let tmp_int_random = Math.floor(Math.random() * (max + 1));

        if (!result.includes(tmp_int_random)) {
            result.push(tmp_int_random);
        }

    }

    return result;
    }

要点

  1. 駅データからランダムで4つ駅データを取ってくる。
  2. 4つのうちからランダムで1つを正解とする。
  3. 正解となる駅データについて路線データを取得する。
  4. マップを作り、そこに正解となる駅を表示する。
  5. 選択肢を表示する。
  6. 選択肢が押されたら正誤の判定をする。
  7. 次へ進む(次の問題)

という流れになりました。案外単純ですね。

割と大きなデータを扱うため、promiseを使わないと順番通りに処理されず、エラーを吐く場合もあるので注意が必要です。まだpromise初心者のため不適切な書き方をしているかもしれませんが、そのようなことがありましたら、ぜひ教えていただけると幸いです。

まとめ

駅データをダウンロードしてきた際、日本の駅の多さにびっくりしました。
あまりデータが多いと、検索などに時間がかかるなど問題点も多くなってしまうため、今回は関東地区に限定しました。データを差し替えてコードをちょっといじれば他の地区版も作れると思います。
また、データさえあれば駅だけでなく、空港・城・インターチェンジなどのクイズも作れそうです。(需要はなさそうですが)

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

iOS、Safariの<input type="file" multiple>で発生するエラーに対応した際の備忘録

はじめに

とあるサイトを制作していた時、iOS、Safariの<input type="file" multiple>でファイルを選択した場合にエラーが発生する事象に遭遇しました。
ネット上で有益な情報を見つけられなかったので、備忘録として残します。

事象

下記のような、選択したファイル名をリスト表示させるものを作る際に発生しました。

ソース

HTML
<form action="" method="get">
<div>
<ul>
</ul>
<label>
<input type="file" multiple="multiple">
</label>
</div>
</form>
JavaScript
$(function () {
  /*------------------------------------------
    ファイルアップロード
  --------------------------------------------*/ 
  $('body').on('change', 'input[type="file"]', function() {
    var $this = $(this)
    var $ul = $this.parent('label').siblings('ul')
    var listHtml = ''

    if($ul.length > 0) {
      var files = $this[0].files
      var filesLength = files.length

      // ファイルを1件以上選択している場合
      if(filesLength > 0) {
        // HTMLを生成
        for(var i = 0; i < filesLength; i++) {
          listHtml += '<li>' + files[i].name + '</li>'
        }

        // HTMLに反映
        $ul.html(listHtml)
      } else {
        // ファイルの選択をキャンセルした場合
        // リストを削除する
        $ul.html('')
      }
    }
  })
})

See the Pen MultipleFileSample01 by N/NE (@inumberx) on CodePen.

発生端末

OS iOS 12.1.4
端末 iPhoneX
ブラウザ Safari、Google Chrome

現象

<input type="file" multiple>でファイルを複数選択した後に、再度ファイルを再選択・キャンセルすると下記画像のようなエラーが発生するというものです。

img_001.jpg

  • 問題が発生したため、このWebページが再読み込みされました。というメッセージが表示される

img_002.jpg

  • "https://XXX/YYY/ZZZ"で問題が繰り返し起きました。というメッセージが表示される

発生タイミング

色々と検証した結果、エラーは下記のようなタイミングで発生しているようでした。

  • ファイル選択済の状態でファイルを再選択する
  • ファイル選択済の状態でキャンセルする
  • ファイルを選択した時に、既にファイル選択済の他の<input type="file" multiple>が表示状態(display:none;以外)で存在する

対応方法

エラーを発生させないためには、ファイルを選択した時に既にファイル選択済の<input type="file" multiple>がdisplay:none;以外で存在していてはいけません。
そこで、ファイルを選択した時にJS(jQuery)でその要素を非表示にし、新しく空の<input type="file" multiple>を作ることによってエラーが発生しないようにしました。

ソース

HTML
<form action="" method="get">
<div>
<ul>
</ul>
<label>
<input type="file" multiple="multiple">
</label>
</div>
</form>
CSS
input[type="file"][multiple="multiple"].ios {
position: absolute;
opacity: 1;
z-index: 1;
}
input[type="file"][multiple="multiple"].ios + input[type="file"][multiple="multiple"] {
display: none;
}
JavaScript
$(function () {
  /*------------------------------------------
    ファイルアップロード
  --------------------------------------------*/
  // iOSの場合
  // すでにファイルを選択済みの<input type="file" multiple>の場合、ファイルを再選択・キャンセルした時にブラウザのエラーが発生するので個別対応を行う
  if(isIOs && $('input[type="file"][multiple="multiple"]').length > 0) {
    var $file = $('input[type="file"][multiple="multiple"]')
    var fileLength = $file.length

    for(var i = 0; i < fileLength; i++) {
      var $thisFile = $($file[i])
      // inputを複製
      var $cloneFile = $thisFile.clone(true)
      // 複製した要素にクラスを付与
      $cloneFile.val('').addClass('ios')
      // 複製した要素はabsoluteで複製元の要素に被せるため、親要素をrelativeにする
      $thisFile.parent().css(
        {
          'position': 'relative',
          'min-width': $thisFile.outerWidth(true) + 'px',
          'min-height': $thisFile.outerHeight(true) + 'px'
        }
      );
      // 複製した要素をDOMに追加
      $thisFile.before($cloneFile)
    }

    // 複製した要素に隣り合う<input type="file" multiple>がタッチされた時の処理
    $('body').on('touchstart', 'input[type="file"][multiple="multiple"].ios + input[type="file"][multiple="multiple"]', function() {
      // 何もしない
      return false
    })

    // サブミット時の処理
    $('body').on('submit', function() {
      // 複製した要素を削除する
      $('input[type="file"][multiple="multiple"].ios').remove()
    })
  }

  $('body').on('change', 'input[type="file"]', function() {
    var $this = $(this)
    var $ul = $this.parent('label').siblings('ul')
    var listHtml = ''

    if($ul.length > 0) {
      var files = $this[0].files
      var filesLength = files.length

      // ファイルを1件以上選択している場合
      if(filesLength > 0) {
        // HTMLを生成
        for(var i = 0; i < filesLength; i++) {
          listHtml += '<li>' + files[i].name + '</li>';
        }

        // HTMLに反映
        $ul.html(listHtml)
      } else {
        // ファイルの選択をキャンセルした場合
        // リストを削除する
        $ul.html('')
      }
    }

    // iOSの場合
    if($this.hasClass('ios')) {
      // inputを複製
      var $cloneFile = $this.clone(true)
      // 複製した要素のvalueを空にする
      $cloneFile.val('')
      // すでに存在するinputを削除する
      $this.siblings('input[type="file"][multiple="multiple"]').remove()
      // 複製した要素をDOMに追加
      $this.removeClass('ios').before($cloneFile)
    }
  })
})

/*------------------------------------------
 iOSチェック
--------------------------------------------*/
function isIOs() {
  var ua = navigator.userAgent
  var bw = window.navigator.userAgent.toLowerCase()

  // iOSの場合
  if(ua.indexOf('iPhone') > 0 || ua.indexOf('iPod') > 0 ) {
    return true
  }

  return false
}

See the Pen MultipleFileSample02 by N/NE (@inumberx) on CodePen.

問題点

  • 常に「ファイル未選択」と表示される → 選択したファイル数を「ファイル未選択」の文言の上に無理やり被せる?
  • 根本的な解決方法ではない気がする → もっと良い解決方法をご存知の方がいらっしゃったらご教示ください
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FirebaseアプリからSlackへ通知を行う。アクセストークンも自動連携する編。

前回より、あるWEBサービスのユーザへの通知の方法として、そのユーザのSlackに通知が飛ばせたらイイねという件に着手しています。

前回はHello World 的な Slack App を作成しました。つぎはそのSlack Appへのアクセストークンの受け渡しをどうするかについて書くといったので、今回はそれを整理していきます。

今回目指す処理シーケンスは下記の通りとなります。
image.png

  • アクセストークン(Access Token)は Slackが用意した Slack Appの開発画面で(目視で)確認するのではなく、DBMSやFirebase Firestoreなどへ自分で永続化する。今回は Firestoreへ保存します。
  • Firestoreへ保存する処理を動かすために、OAuth認可サーバ(上図のSlack認可サーバ)からのリダイレクト先として(Slack Appの開発画面ではなく)、自前の処理を動かせるところを指定する。上図では「Firebase Functionsのoauth関数」をリダイレクト先にしている。
  • 前回 Curlで実行した箇所は 今回は Firebase Functionsなどでスケジュール実行させる。上図では「Firebase Functionsのchat関数」がその役割で、chat関数はFirestoreからアクセストークンを取り出し、API(/api/chat.postMessage)を呼び出すことで、Slackへ投稿を行う

やってみる

さあやってみます。がその前に準備や設定などを。

前提の環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.3
BuildVersion:   18D42

$ node --version
v10.14.2    <-ホントはFunctionsとかのバージョンに合わせるべきなんだけどいったん気にしない :-)

Firebaseのサインアップと準備

まずはFirebaseのサインアップですが、サインアップはいろんなヒトが書いているので割愛。
いわゆる

  var config = {
    apiKey: "##FIREBASE API KEY##",
    authDomain: "##FIREBASE AUTH DOMAIN##",
    databaseURL: "https://##PROJECT ID##.firebaseio.com",
    projectId: "##PROJECT ID##",
    storageBucket: "##PROJECT ID##.appspot.com",
    messagingSenderId: "YOUR-SENDER-ID"
  };

とかまでは取得できていて、そしてFirestoreが有効になっていて、そしてAuthentication機能が有効になっていて、ログインプロバイダとしてGoogle が有効になっている前提ですすめます。

ちなみにこの辺をご参考にしていただくと、上記の設定を有効に出来るかと思います

あとは

ココの「firebase-tools のインストール」「Firebaseにログイン」を実施しておいてください。

Host名を設定する

今回動かそうとしているWEBアプリは、Cookieを用いているのですがその関係上、WEBアプリにはlocalhostではなくホスト名 client.example.com でアクセスしたいです。なので /etc/hosts などで名前解決しておきます。Macの例ですがこんな感じ。

$ cat /etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1   localhost
255.255.255.255 broadcasthost
::1             localhost
127.0.0.1 client.example.com

さらに、URLをlocalhostではなく client.example.com でアクセスすることになるので、FirebaseのAuthenticationの設定にこのドメインを登録する必要があります。承認済みドメインを追加する の手順に従って、承認済みドメインに「client.example.com」 を追加してください。

Slack App のCredentials確認

前回の記事で紹介したSlack Appの管理画面 https://api.slack.com/apps より該当するSlack Appを探し、App Credentials にあるClient ID/Client Secretをひかえておきます。

image.png

Slack App のRedirect URLsの設定追加

Slack Appでもう一つ。今回「Slack認可サーバからのリダイレクト先として、Slack Appの開発画面ではなく Firebase Functionsのoauth関数をリダイレクト先にする」 ことにしましたが、OAuthではそのRedirect先のURLを登録しておく必要があります。

その画面は Slack Appの開発画面にアクセスして、「OAuth & Permissions」を開きます。Redirect URLs を設定出来る箇所があるので下記のとおり http://client.example.com:5001/##PROJECT ID##/us-central1/oauth を追加し Save URLs をクリックして保存しましょう。「##PROJECT ID##」は、Firebaseの設定値 projectIdの値となります。

image.png

準備が長くてスイマセン。。けっきょく、

項目
Firebaseプロジェクト名 ##PROJECT ID##
WEBアプリのアクセスURL http://client.example.com:5000/
Functions上の関数(oauth) http://client.example.com:5001/##PROJECT ID##/us-central1/oauth
Functions上の関数(chat) http://client.example.com:5001/##PROJECT ID##/us-central1/chat
投稿先のSlackワークスペース Slack Appを開発しているワークスペース
Slack App のclient_id ##SLACK CLIENT ID##
Slack App のclient_secret ##SLACK CLIENT SECRET##
認可後の、Redirect URLs http://client.example.com:5001/##PROJECT ID##/us-central1/oauth

などを準備した感じです。。

ビルドする

さて、説明のためのコードをつくってGitHubにおいてあるので、下記の通り落としてビルドしていきます。

$ git clone --branch for_qiita_slack000  https://github.com/masatomix/todo-examples.git

まずはWEBアプリ。

$ cd todo-examples/
$ npm install

src/firebaseConfig.js を自分の設定に書き換え

$ cat src/firebaseConfig.js
export default {  ↓さきほどひかえておいた値を設定
  apiKey: '##FIREBASE API KEY##',
  authDomain: '##FIREBASE AUTH DOMAIN##',
  databaseURL: 'https://##PROJECT ID##.firebaseio.com',
  projectId: '##PROJECT ID##',
  storageBucket: '##PROJECT ID##.appspot.com',
  messagingSenderId: 'YOUR-SENDER-ID'
}

src/restConfig.js を自分の設定に書き換え

$ cat src/restConfig.js
export default {
  ##PROJECT ID## 書き換え(上記のprojectIdの値)
  apiUri: 'http://client.example.com:5001/##PROJECT ID##/us-central1/oauth'
}

$ npm run build

つづいて、Firebase Functionsのビルド。

$ cd functions/
$ npm install

functions/src/oauthConfig.ts  を自分の設定に書き換え

$ cat functions/src/oauthConfig.ts 
export default {
  client_id: '##SLACK CLIENT ID##', ← さきほどひかえておいたSlackのClient IDの値を設定
  client_secret: '##SLACK CLIENT SECRET##',← さきほどひかえておいたSlackのClient Secretの値を設定
  authorization_endpoint: 'https://slack.com/oauth/authorize', ←ココはこのまま
  token_endpoint: 'https://slack.com/api/oauth.access', ←ココはこのまま
  redirect_uri: 'http://client.example.com:5001/##PROJECT ID##/us-central1/oauth', ##PROJECT ID## 書き換え(上記のprojectIdの値)
  scope: 'chat:write:user' ←ココはこのまま
}

$ npm run build
$ cd ../

つづいて下記コマンドで、このコード群がデフォルトで使用するFirebaseプロジェクト名を指定します。

$ firebase use --add
? Which project do you want to add? xxxxxxxxxxx   ← 複数選択肢が表示された場合は、上記の「 ##PROJECT ID## 」の値を選びます
? What alias do you want to use for this project? (e.g. staging) default

Created alias default for xxxxxxxxxxx.
Now using alias default (xxxxxxxxxxx)
$

参考: FirebaseとGoogle Cloud Platform をさわれる環境を構築する

動かしてみる

さあ、ローカルでWEBアプリとFunctionsを起動してみます。

$ firebase serve --only hosting,functions

さて、ブラウザで http://client.example.com:5000/ にアクセスしてください。ログイン画面が表示されるとおもいます。Googleアカウントでログインできるようにしてあるのでログインしましょう。ログインできると「Add to Slack」ボタンが配置されている画面が表示されると思います。

image.png

ボタンをクリックすると、ウィンドウが開き、Slackの認可サーバへリダイレクトされます。すでにWEBブラウザでSlackを使っていれば、下記の通り、前回記事と同様の認可画面が表示されます。

image.png
(WEBブラウザでSlackを使っていない場合は、ワークスペースを選択したりログインしたりする画面が表示されたのち、上記画面が表示されると思います。)

さて「許可する」をクリックすると、前回の記事では「Slack Appの開発画面」にリダイレクトされましたが、今回はRedirect URLsで設定追加した、 http://client.example.com:5001/##PROJECT ID##/us-central1/oauth へリダイレクトされるはずです。 firebase serve --only hosting,functionsによって、ローカルで Firebase Functionsも起動しているので、ローカルで oauth 関数が動いた結果、Firestoreへアクセストークンが保存されるとおもいます。

後述しますが、
image.png
こんな感じにFirestoreに保存されるはずです。

chat関数を呼び出す

さて、保存したアクセストークンを取り出す処理を動かすために、chat関数を呼び出します。

$ curl http://client.example.com:5001/##PROJECT ID##/us-central1/chat
ok
$

認可をおこなったSlackに通知が飛んだと思います!
image.png

サンプルアプリによる動作の紹介は以上です。

各ソースの説明

各ソースの主要なとこだけ紹介します。

Add to Slack ボタンを配置してあるVus.jsのWEBアプリ

まずはリンクを配置するWEBアプリから。

WEBアプリはVue.jsで構築され、Firebase認証でログイン出来るようにしてあります。
ログインすると表示される、Add to Slack のボタンがある Slack.vue ファイルのソースは下記の通り。

src/components/Slack.vue
<template>
  <main v-if="$store.state.loginStatus" class="container">
    <h1>
      <img
        alt="Add to Slack"
        height="40"
        width="139"
        src="https://platform.slack-edge.com/img/add_to_slack.png"
        srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x"
        @click="popup()"
        style="cursor:pointer"
      >
    </h1>
  </main>
</template>

<script>
import restConfig from '@/restConfig'
export default {
  name: 'Slack',
  methods: {
    popup () {
      const url = [
        restConfig.apiUri,
        '?userid=',
        this.$store.state.user.uid
      ].join('')
      window.open(
        url,
        'pop',
        () =>
          `toolbar=0,status=0,top=100,left=200,width=700,height=600,modal=yes,alwaysRaised=yes`
      )
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}
</style>

中に出てくる$store.state.user.uid には、FirebaseのユーザUIDが入るようにしてあります。「FirebaseのユーザUID」とは、コレのことです。
image.png

さて読み込んでいる設定ファイル restConfig.jsは以下。

src/restConfig.js
export default {
  ##PROJECT ID## 書き換え(上記のprojectIdの値)
  apiUri: 'http://client.example.com:5001/##PROJECT ID##/us-central1/oauth'
}

この値が、Add to Slackボタンを押したときに別ウィンドウで開かれるブラウザのURLに設定してあります。よって別ウィンドウで http://client.example.com:5001/##PROJECT ID##/us-central1/oauth?userid=[FirebaseのユーザUID] が開かれます。

クエリパラメタにFirebaseのユーザUIDを渡しているのは、ずっとあとでSlack側で認可処理が完了してアクセストークンを取得できた後に、FirebaseのユーザUIDをキーにアクセストークンをFirestoreに格納しておきたいから、です。

Firebase Functionsのoauth関数

つづいて上記で呼ばれたFunctions のoauth関数を見てみます。

functions/src/index.ts(ほぼ全部)
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import * as request from 'request'
import * as cookie from 'cookie'
import oauthConfig from './oauthConfig'

admin.initializeApp()

export const oauth = functions.https.onRequest(async (req, res) => {
  if (req.query.userid) {
    addCookie(res, 'userid', req.query.userid)
    res.redirect('./oauth')
    return
  }

  const code = req.query.code

  // errorでリダイレクトされたとき
  // ユーザがキャンセルしたときはココなので、そこそこちゃんと実装しないと。。(今んとこ適当実装)
  if (req.query.error) {
    res.setHeader('Content-Type', 'text/plain;charset=UTF-8')
    const message = `
error: ${req.query.error}
error_uri: ${req.query.error_uri}
error_description: ${req.query.error_description}
`
    res.send(message)
    return
  }

  // codeがなかったとき、まずは認可画面へ遷移
  if (!code) {
    const randomValue = getRandomString()
    console.log('randomValue: ' + randomValue)

    const authorization_endpoint_uri = [
      oauthConfig.authorization_endpoint,
      '?client_id=',
      oauthConfig.client_id,
      '&redirect_uri=',
      oauthConfig.redirect_uri,
      '&state=',
      randomValue,
      '&response_type=code',
      '&scope=',
      oauthConfig.scope
    ].join('')

    addCookie(res, 'state', randomValue)
    res.redirect(authorization_endpoint_uri)
  } else {
    if (!checkCSRF(req, res)) {
      res
        .status(400)
        .send('前回のリクエストと今回のstate値が一致しないため、エラー。')
      return
    }

    const formParams = {
      redirect_uri: oauthConfig.redirect_uri,
      client_id: oauthConfig.client_id,
      client_secret: oauthConfig.client_secret,
      grant_type: 'authorization_code',
      code: code
    }

    const options = {
      uri: oauthConfig.token_endpoint,
      method: 'POST',
      headers: {
        'content-type': 'application/x-www-form-urlencoded'
      },
      form: formParams,
      json: true
    }

    const body: any = await doRequest(options)

    const cookies = cookie.parse(req.headers.cookie || '')
    const userId = cookies.userid

    console.log(userId)

    admin
      .firestore()
      .collection('slackToken')
      .doc(userId)
      .set(body)

    res.send('登録完了。ブラウザ閉じちゃってください。')
  }
})

function doRequest (option) {
  return new Promise((resolve, reject) => {
    request(option, (error, response, body) => {
      if (!error && response.statusCode == 200) {
        resolve(body)
      } else {
        reject(error)
      }
    })
  })
}

# https://qiita.com/fukasawah/items/db7f0405564bdc37820e 感謝!
function getRandomString () {
  var S = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  var N = 50
  const randomValue = Array.from(Array(N))
    .map(() => S[Math.floor(Math.random() * S.length)])
    .join('')
  return randomValue
}

function checkCSRF (req, res) {
  const state = req.query.state

  const cookies = cookie.parse(req.headers.cookie || '')
  const sessionState = cookies.state

  console.log('requestState: ' + state)
  console.log('sessionState: ' + sessionState)
  return state === sessionState
}

function addCookie (res, key, value) {
  res.setHeader('Cache-Control', 'private') // Hosting経由だと、これがないとset cookieが削除される
  const expiresIn = 60 * 60 * 24
  const options = { maxAge: expiresIn, httpOnly: true }
  // const options = { maxAge: expiresIn, httpOnly: true, secure: true }
  res.setHeader('Set-Cookie', cookie.serialize(key, value, options))
}

超ザックリいうと、クエリパラメタに「code」が入っているかで場合分けしていて

  • codeが入っていない

    • → 初回のリクエストと見なし、設定ファイル(functions/src/oauthConfig.ts)よりクエリパラメタを生成しながら、Slack認可サーバ「https://slack.com/oauth/authorize」へリダイレクト。
  • codeが入っている

    • → Slack認可サーバから認可コードが渡ってきたとみなし、Slack認可サーバ「https://slack.com/api/oauth.access」にアクセスしてアクセストークンを取得し、Firestoreへアクセストークンを保存する処理を実行

という動きをします。

code が入ってる場合をもう少し丁寧に書くと

  • 初回のリクエスト時に cookie経由でrandomな文字列(state変数)を下ろしてあってそれがcookieに乗ってくるので、そのstate値を取得。
  • cookie経由のstate値と、認可サーバからリダイレクト時に渡ってくるstateパラメタの文字列をチェックし、CSRF対策を実施
  • OKだったら、認可コード(code)と、client_id/client_secretを使ってSlack認可サーバ「https://slack.com/api/oauth.access」へアクセストークンを要求。Slack認可サーバは、client_idによって「自分が認可コードを渡したかったクライアントかな?」という判定client_secret によって「(接続を許可したWEBアプリだという)正当なヤツからのアクセストークン要求かな?」という判定をして、認可コードに紐付くアクセストークンを発行して返す
  • 関数はcookieからFirebaseのユーザUID を取得、それをキー値にして、さきほど取得したアクセストークンをFirestoreの「slackToken」テーブルに格納する。
  • 関数は、画面に「登録完了。ブラウザ閉じちゃってください。」と表示して、完了する。

となります。

上記によってFirestoreには下記のような形式でアクセストークンが格納されます
image.png

Firebase Functionsのchat関数

さてWEBアプリから Add to Slackを押したあとSlackでの認証・認可をおこなうことで、Firebase Firestoreにアクセストークンが格納されました。あとは Functionsから周期的に、この値を取り出してAPI経由でSlackへ投稿をおこなえば完成です。

今回は WEBから呼び出せるchat関数をつくってあり、それ経由でAPIを呼び出します。コードは、以下の通り。

functions/src/index.ts(ほぼ全部といった、残り)
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import * as request from 'request'
import * as cookie from 'cookie'
import oauthConfig from './oauthConfig'

admin.initializeApp()

export const chat = functions.https.onRequest(async (req, res) => {
  await sendSlack()
  res.send('ok')
})

export const chat_pub = functions.pubsub
  .topic('slackChatTopic')
  .onPublish(async message => {
    await sendSlack()
  })

// $ gcloud pubsub topics publish slackChatTopic  --message '{"name":"Xenia"}'

async function sendSlack () {
  const querySnapshot = await admin
    .firestore()
    .collection('slackToken')
    .get()

  querySnapshot.forEach(doc => {
    const fbUserId = doc.id
    const jsonData = doc.data()

    const option = {
      url: 'https://slack.com/api/chat.postMessage',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
        Authorization: `Bearer ${jsonData.access_token}`
      },
      json: {
        channel: '#general',
        text: `${fbUserId} です、今日は!`
      }
    }
    request(option, (error, response, body) => {
      if (error) {
        console.log('error:', error)
        return
      }
      if (response && body) {
        console.log('status Code:', response && response.statusCode)
        console.log(body)
      }
    })
  })
}

async function sendSlack () では、FirestoreのslackTokenテーブルのデータを全件取得し、アクセストークンを取りだします。そのアクセストークンを Authorization ヘッダの Bearer トークンとしてセットし「https://slack.com/api/chat.postMessage」へPOSTすることで、該当のアクセストークンが紐付いたSlackへ、メッセージが投稿されます。

今回はchat関数をWEB経由で起動しましたが、本来はスケジューラから起動したいですよね。じつはすでに Firebase の関数をスケジューラから定期的に呼び出す の記事で用いた形式の関数 chat_pub を作成済みなので、次回は

  • WEBアプリとFunctionsの、本番へのデプロイ
  • スケジューラから chat関数(chat_pub関数)を呼び出す事で、よりSlack上で動くアプリっぽくする
  • そのための諸々の環境設定

をやっていきます。

おつかれ様でしたー。。

関連リンク

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

24日目。Javascriptのモジュール、パッケージについて。

24日目 Javascriptのモジュール編。
クラスのインポートエクスポート、パッケージの使い方など。
なるほどわかりやすいのでした!

ところが、完成したJavascriptを実行しようとすると・・・
クラスのimport/exportがエラーになってしまいました。
どうしたらいいか、さっとは分からなかったので、今日のところはこの辺で。。。
そのうち分かってくるでしょう。あーくやしい!

24日目
http://appdays.herokuapp.com/Day24/

モジュール

ファイルの分割

JSファイルを分割する。

クラスのexport,import

クラスの定義の後で「export default クラス名」として、他のファイルへ渡せるようにする。
使用するファイルの先頭で「import クラス名 from "./ファイル名"」と書いて、他のファイルのクラスを読み込む。
拡張子jsは省略、ファイル名は""でくくる。

値のexport,import

文字列や数値や関数など、どんな値でもエクスポートが可能です。
「export default 定数名」
「import 定数名 from "./ファイル名"」

データ定義を分割する

わかりやすい。

デフォルトエクスポート

export defaultは自動的に「export default 値」の値がインポートされます。
そのためエクスポート時の値の名前と、インポート時の値の名前は違っても問題ありません。
デフォルトエクスポートは1ファイル1つの値のみ使える。
複数の値をエクスポートする場合は「名前付きエクスポート」

名前付きエクスポート

defaultを書かずに、名前を{}で囲んでエクスポートする書き方です。
インポートする値は、「import { 値の名前 } from "./ファイル名"」
「export { 名前1, 名前2 }」1つのファイルから複数のエクスポートが出来ます。

相対パス

使える。

パッケージ

パッケージも使える。
import 定数名 from パッケージ名;

完成!

さくさく進めました!

これをhtmlに読ませて実行してみると「SyntaxError: Unexpected token」となりました。
そうそう、script.jsしか読み込んでいなかった。
index.htmlに追記しましょう。

<html>
  <head>
    <meta charset="utf-8">
    <title>Javascriptの練習</title>
    <script type="text/javascript" src="animal.js"></script>
    <script type="text/javascript" src="dog.js"></script>
    <script type="text/javascript" src="script.js"></script>
    <script type="text/javascript" src="./data/dogData.js"></script>
  </head>
  <body>
    <h1>このページは</h1>
    console.log(xxxx); を使って開発ツールのコンソールに実行結果を出力しています。
  </body>
</html>

本当にこれでいいのかな。もっとスマートな方法がありそうなものですが・・・

実行
js5err.png

クラスのimportできず!
あれこれやってみましたが、動かず。
一旦ギブアップします。どうしたらいいんだろう。

(所要時間 1時間)

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

24日目。Javascriptのモジュール、パッケージ、配列、npm

24日目 Javascriptのモジュール編。
クラスのインポートエクスポート、パッケージの使い方など。
なるほどわかりやすいのでした!

ところが、完成したJavascriptを実行しようとすると・・・
クラスのimport/exportがエラーになってしまいました。
どうしたらいいか、さっとは分からなかったので、今日のところはこの辺で。。。
そのうち分かってくるでしょう。あーくやしい!

24日目
http://appdays.herokuapp.com/Day24/

モジュール

ファイルの分割

JSファイルを分割する。

クラスのexport,import

クラスの定義の後で「export default クラス名」として、他のファイルへ渡せるようにする。
使用するファイルの先頭で「import クラス名 from "./ファイル名"」と書いて、他のファイルのクラスを読み込む。
拡張子jsは省略、ファイル名は""でくくる。

値のexport,import

文字列や数値や関数など、どんな値でもエクスポートが可能です。
「export default 定数名」
「import 定数名 from "./ファイル名"」

データ定義を分割する

わかりやすい。

デフォルトエクスポート

export defaultは自動的に「export default 値」の値がインポートされます。
そのためエクスポート時の値の名前と、インポート時の値の名前は違っても問題ありません。
デフォルトエクスポートは1ファイル1つの値のみ使える。
複数の値をエクスポートする場合は「名前付きエクスポート」

名前付きエクスポート

defaultを書かずに、名前を{}で囲んでエクスポートする書き方です。
インポートする値は、「import { 値の名前 } from "./ファイル名"」
「export { 名前1, 名前2 }」1つのファイルから複数のエクスポートが出来ます。

相対パス

使える。

パッケージ

パッケージも使える。
import 定数名 from パッケージ名;

完成!

さくさく進めました!

これをhtmlに読ませて実行してみると「SyntaxError: Unexpected token」となりました。
そうそう、script.jsしか読み込んでいなかった。
index.htmlに追記しましょう。

<html>
  <head>
    <meta charset="utf-8">
    <title>Javascriptの練習</title>
    <script type="text/javascript" src="animal.js"></script>
    <script type="text/javascript" src="dog.js"></script>
    <script type="text/javascript" src="script.js"></script>
    <script type="text/javascript" src="./data/dogData.js"></script>
  </head>
  <body>
    <h1>このページは</h1>
    console.log(xxxx); を使って開発ツールのコンソールに実行結果を出力しています。
  </body>
</html>

本当にこれでいいのかな。もっとスマートな方法がありそうなものですが・・・

実行
js5err.png

クラスのimportできず!
あれこれやってみましたが、動かず。
一旦ギブアップします。どうしたらいいんだろう。

(所要時間 1時間)


配列を操作するメソッド

push

配列の最後に新しい要素を追加するメソッド

forEach

配列の中の要素を1つずつ取り出して、全ての要素に繰り返し同じ処理を行うメソッド
characters.forEach((character)=>{ console.log(character)});

おお、スッキリ!

find

配列の中から条件に合う最初の1つを返す。
配列の要素がオブジェクトで、プロパティを条件にすると、findはオブジェクトごと返す。

const evenNumbers = numbers.find((number)=>{
  return number % 2 === 0;
});

filter

配列の中から条件に合う要素全部を返す。

const evenNumbers = numbers.filter((number)=>{
  return number % 2 === 0;
});

map

配列内のすべての要素に処理を行い、その戻り値から新しい配列を作成する。

const doubledNumbers = numbers.map((number)=>{
  return number*2;
});

完成!

(所要時間 30分)

npmパッケージを開発環境で使えるようにする

インストール

$ npm install

> fsevents@1.2.7 install /Users/robamimim/Documents/JavaScript/npm_sample/node_modules/fsevents
> node install

node-pre-gyp WARN Using needle for node-pre-gyp https download 
[fsevents] Success: "/Users/robamimim/Documents/JavaScript/npm_sample/node_modules/fsevents/lib/binding/Release/node-v64-darwin-x64/fse.node" is installed via remote
npm notice created a lockfile as package-lock.json. You should commit this file.
added 329 packages from 155 contributors and audited 3493 packages in 14.638s
found 0 vulnerabilities

使ってみる。

$ npm run start

> npm_sample@1.0.0 start /Users/robamimim/Documents/JavaScript/npm_sample
> babel src --out-dir dist && node dist/index.js

Successfully compiled 1 file with Babel.
これはテストメッセージです
t$ npm install --save readline-sync
+ readline-sync@1.4.9
added 1 package from 1 contributor and audited 3501 packages in 4.304s
found 0 vulnerabilities

できた!

ノートラブルであっさりできました!
(所要時間 30分)

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

全部 TypeSctipt で書こう!

こんにちは。
突然ですが、TypeSctipt って便利ですよね。

私は今まで数年間 JavaScript のまま書いていました。
(TypeScript も知っていたが、なぜ変換させてまで別言語で書くの?と思っていた)

半年前、異動先の部署で使われていた TypeScript を使うことに。
導入コストがそれなりにあるし、億劫な気分でした。

ですが現在、 TypeScript にメロメロです。

TypeScript の何が良いのか

  • JavaScript の拡張言語であり、 ECMAScript の実装に準拠している
    • async/await や const/let など、 JavaScript の知識で書ける
  • JavaScript なのに型の恩恵が受けられる
    • 型の補完とか
    • 存在しないプロパティへの書き込みに対する警告とか
  • Visual Studio Code という TypeScript を正しく完璧に使うためのツールが無料

JavaScript という無法地帯に降り立った天使。それが TypeScript :angel:

TypeScript がチームの生産性を向上させた話

現在、私の開発チームはバックエンドエンジニアが私を含め3名とフロントエンドエンジニアが1名いますが、
私がフロントエンド (React.js) を書いたり、
フロントエンドエンジニアがバックエンド (Node.js) を書いたりしています。

JavaScript より型が安全で補完も効くので手が出しやすく、
コードレビューもフレームワークや思想など、
本質的な議論になる事が多いです。

少しずつそれぞれの範囲を超えて仕事できるようになってきており、
ボトルネックなタスクにリソースが集中できて効率的です。

また、もう2名も元々 Java エンジニアでしたが、
チームにジョイン後1ヶ月もかからず新機能のコンポーネントを開発してくれました。
他言語からの開発の入りやすさも言うこと無しです。

バッチ処理などのスクリプトも JavaScript の真骨頂。
もちろん TypeScript で書いています。

もう Web サービスは全部 TypeScript で書こう!

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

即時関数で囲って引数を渡す

メモ

'use strict';

var i = "global";
console.log(i);  // global

(function(i){  // 即時関数に引数を渡す
    console.log('in ' + i);  // in global
    i = "dainyu 1";
    console.log('in ' + i);  // in dainyu 1
    i = "dainyu 2";
    console.log('in ' + i);  // in dainyu 2
})(i);

console.log(i);  // global

'use strict';

var i = "global";
console.log(i); // global

(function(e){
    console.log('in ' + e);  // in global
    e = "dainyu 1";
    console.log('in ' + e);  // in dainyu 1
    e = "dainyu 2";
    console.log('in ' + e);  // in dainyu 2
})(i);

console.log(i);  // global
console.log('out ' + e);   // Uncaught ReferenceError: e is not defined
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む