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

JavaScriptでinputのパスワードの黒丸のところを文字で表示させてみた!!

はじめに

某プログラミングスクールで、メ○○リのコピーサイトを作成しました。
チームメンバーが実装していた、ボタンを押すとパスワードの黒丸を文字として表示させる
実装を今日はやっていきたいと思います!と思ったのですが、
すでに下記の参考記事で簡単に作成できました。
パスワード表示時にマスキング有無を選択できるようにする方法

なので、自分のメモ用で記載していきます。
ちなみにこんな感じのものを実装していきます。
a1a64a26379e2a3036884ebbf0b4aa88.gif

解説

1.まずパスワードを文字として表示していきます。

 (1)hamlを記載していきます。(一部のみ表示しています。)

password.html.haml
= form_for @resume do |f|
  = f.password_field :password,placeholder: '検索時に使用するパスワードを入力',class:"content_main9__form9--password",id:'password'
  %input#js-passcheck{type: "checkbox"}
  %label{for: "js-passcheck"} パスワードを表示する

コードはこんな感じです。

 (2)jsを記載していきます。

password.js
$(function(){
  var password = '#password'; #haml内のid:'password'を取得して代入。
  var passcheck = '#js-passcheck'; #haml内のid:'js-passcheck'を取得して代入。

  $(passcheck).change(function(){  #チェックボックスを押した際に発火するイベントを作成
    if ($(this).prop('checked')){   #チェックボックスにチェックが入った場合発火
      $(password).attr('type','text');  #パスワードのタイプをtypeからtextへ変更することで文字表示
    }
  });
});

チェックボックスを押した際に発火するイベントを作成しています。
そして、チェックボックスにチェックが入った時に、
input内のtypeをpasswordからtextへと変更する形となっています。
なので、input内の文字が表示されるようです。

2.パスワードを黒丸に戻します。

password.js
$(function(){
  var password = '#password'; #haml内のid:'password'を取得して代入。
  var passcheck = '#js-passcheck'; #haml内のid:'js-passcheck'を取得して代入。

  $(passcheck).change(function(){  #チェックボックスを押した際に発火するイベントを作成
    if ($(this).prop('checked')){   #チェックボックスにチェックが入った場合発火
      $(password).attr('type','text');  #パスワードのタイプをtypeからtextへ変更することで文字表示
    } else {                            #チェックボックスからチェックがなくなった際に発火
      $(password).attr('type','password');  #パスワードのタイプをpasswordへと変更
    }
  });
});

次に、elseの部分では、チェックボックスからチェックがなくなった時のイベントとなっています。
この文で、inputのtypeをtextからpasswordへと変更しているため、
文字が再び黒丸へと戻るようです。

まとめ

もし間違えている理解が間違っている部分等が、ありましたらコメントをいただけると幸いです。

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

javascript : 2つの配列を比較して、差分(増分,減分)を取得

proxy で配列の変更を検知したあとの処理で、「変更前の配列」と「変更後の配列」を比較したくなりました。

以下、2つの配列を受け取って、増分値と減分値の配列をオブジェクトで返す関数です。

// 配列の差異を取得する処理
const get_difference = ( _old_arr, _next_arr ) => {

  const additions = _next_arr.filter(
    _next => _old_arr.every(
      _old => _old !== _next
    )
  );

  const subtractions = _old_arr.filter(
    _old => _next_arr.every(
      _next => _old !== _next
    )
  );

  return {
    additions : additions,       //  増えた分
    subtractions : subtractions  //  減った分
  };
}

以下、関数の挙動を確かめます。

//  要素を増やします
const arr1 = [ 1, 2, 3 ];
const arr2 = [ ...arr1, 4, 5 ];

const result1 = get_difference( arr1, arr2 );
console.log( result1.additions );      //  [ 4, 5 ]
console.log( result1.subtractions );   //  []

//  要素を増減させます
const arr3 = [ 'a', 'b', 'c', 'd' ];
const arr4 = [ 'a', 'b', 'e', 'f' ];

const result2 = get_difference( arr3, arr4 );
console.log( result2.additions );      // [ 'e', 'f' ]
console.log( result2.subtractions );   // [ 'c', 'd' ]

もっといい方法があったら、ぜひ教えてください。

以下、おまけです。(proxy で用いた例です)

let obj = {
  array : [ 1, 2, 3 ]
}

let proxy = new Proxy( obj, {
  set( _obj, _prop, _next ){

    const old = _obj[ _prop ];

    if( _prop === 'array' ){
      const difference = get_difference( old, _next );
      console.log( 'additions : ' + difference.additions );
      console.log( 'subtractions : ' + difference.subtractions );
    }
    _obj[ _prop ] = _next;

  }
} );

proxy.array = [ ...obj.array, 4, 5 ];
//  additions : 4, 5

proxy.array = [ 1, 2, 4, 6 ];
//  additions : 6
//  subtrctions 3, 5

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

jQueryのバージョンを確認するには$.fn.jquery

コンソールに貼る

コンソール
$.fn.jquery 

このように表示される

コンソール
$.fn.jquery 
"1.12.4"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NestJsのDB接続周りでハマった話

何を作成する?

Nest.jsで環境ごとにデータベースの接続先を分けるために,接続情報を実行環境の環境変数から非同期で取得し作成する.

環境

  • Node.js v12.14.1
  • Nest.js v6.7.2
  • TypeORM v0.2.22
  • Postgresql v11.6

実装ログ

必要最小限の実装

参考)Nest.js Document > TECHNIQUES >Database

ライブラリインストール

TypeORM, Database Driver (Postgresql)をインストールする.

$ npm install @nestjs/typeorm typeorm pg
DB接続情報を定義

app.module.tsにデータベースの接続情報を定義する.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ItemModule } from './item/item.module';
import { Connection } from 'typeorm';
import { join } from 'path';

@Module({
  imports: [
    ItemModule,
    // DBの接続情報を定義
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'postgres',
      database: 'postgres',
      entities: [join(__dirname + '/**/*.entity{.ts,.js}')],
      synchronize: false,
    }),
  ],
})
export class AppModule {}

一番シンプルな書き方です.自分一人しか触らず,環境もこれだけ!ということであればこの書き方で良いでしょう.

しかし,実際の開発では個人の開発環境,テスト環境,ステージング環境,本番環境と複数の環境が存在し,上記のような実装では環境ごとに接続情報をハードコードし直し → ビルド → デプロイという手順を踏む必要がありナンセンスです.

そのため,通常は環境変数に定義し,接続情報はその環境変数を参照し作成します.

と,いうことで環境変数を参照するようにapp.module.tsを修正します.

環境変数を参照するように実装を修正

※この方法では実行時に依存関係が解決できずエラーとなります.

参考)Nest.js Document > TECHNIQUES > Configuration

ライブラリインストール

環境変数を参照するために必要なライブラリをインストールします.

$ npm install @nestjs/config
ダミーの環境変数を用意

本来は,環境変数に定義するのですがサンプル実装なので環境変数ファイル(.env)をプロジェクトルートに作成する.

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=postgres
環境変数を参照するように接続定義を修正
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ItemModule } from './item/item.module';
import { Connection } from 'typeorm';
import { join } from 'path';

@Module({
  imports: [
    ItemModule,
    ConfigModule.forRoot({
      envFilePath: '.env',
      isGlobal: true,
      // ignoreEnvFile: true, <- 環境変数から取得する場合はコメントアウトを外す.
    }),
    // 非同期で環境変数から値を取得し,接続情報を作成する.
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configServide: ConfigService) => ({
        type: 'postgres' as 'postgres',
        host: configServide.get('DATABASE_HOST'),
        port: Number(configServide.get('DATABASE_HOST')),
        username: configServide.get('DATABASE_USERNAME'),
        password: configServide.get('DATABASE_PASSWORD'),
        entities: [join(__dirname + '/**/*.entity{.ts,.js}')],
        synchronize: false,
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {
  constructor(private readonly connection: Connection) {}
}

起動後,以下のエラーが発生.

2:25:34 PM - Found 0 errors. Watching for file changes.
[Nest] 19111   - 01/13/2020, 2:25:35 PM   [NestFactory] Starting Nest application...
[Nest] 19111   - 01/13/2020, 2:25:35 PM   [InstanceLoader] TypeOrmModule dependencies initialized +24ms
[Nest] 19111   - 01/13/2020, 2:25:35 PM   [InstanceLoader] ConfigModule dependencies initialized +1ms
[Nest] 19111   - 01/13/2020, 2:25:35 PM   [ExceptionHandler] Nest can't resolve dependencies of the TypeOrmModuleOptions (?). Please make sure that the argument ConfigService at index [0] is available in the TypeOrmCoreModule context.

Potential solutions:
- If ConfigService is a provider, is it part of the current TypeOrmCoreModule?
- If ConfigService is exported from a separate @Module, is that module imported within TypeOrmCoreModule?
  @Module({
    imports: [ /* the Module containing ConfigService */ ]
  })
 +1ms
Error: Nest can't resolve dependencies of the TypeOrmModuleOptions (?). Please make sure that the argument ConfigService at index [0] is available in the TypeOrmCoreModule context.

Potential solutions:
- If ConfigService is a provider, is it part of the current TypeOrmCoreModule?
- If ConfigService is exported from a separate @Module, is that module imported within TypeOrmCoreModule?
  @Module({
    imports: [ /* the Module containing ConfigService */ ]
  })

    at Injector.lookupComponentInExports (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:185:19)
    at processTicksAndRejections (internal/process/task_queues.js:94:5)
    at async Injector.resolveComponentInstance (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:142:33)
    at async resolveParam (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:96:38)
    at async Promise.all (index 0)
    at async Injector.resolveConstructorParams (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:111:27)
    at async Injector.loadInstance (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:78:9)
    at async Injector.loadProvider (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:35:9)
    at async Promise.all (index 3)
    at async InstanceLoader.createInstancesOfProviders (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/instance-loader.js:41:9)

環境変数を参照するためのConfigServiceTypeOrmModuleOptions内で依存関係が解決できないことが原因らしい.

同じような事象がGithubのIssueにあがっていたので参考に載せておきます.
Can't init TypeOrmModule using factory and forRootAsync

環境変数を参照するように実装を修正

app.module.tsを以下のように修正します.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { ItemModule } from './item/item.module';
import { TypeOrmConfigService } from './common/database/type-orm-config.service';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: '.env',
      isGlobal: true,
      // ignoreEnvFile: true, <- 環境変数から取得する場合はコメントアウトを外す.
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      // 接続情報を作成するServiceクラスを定義
      useClass: TypeOrmConfigService, 
    }),
    ItemModule,
  ],
})
export class AppModule {
}

type-orm-config.service.ts

import { TypeOrmOptionsFactory, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { join } from 'path';

/**
 * DBの接続設定.
 */
@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  /**
   * DBの接続設定を環境変数をもとに作成します.</br>
   * 環境変数に設定されていない場合は,デフォルトの設定値を返却します.
   * @returns 接続情報
   */
  createTypeOrmOptions(): TypeOrmModuleOptions {
    const configService = new ConfigService();
    return {
      type: 'postgres' as 'postgres',
      host: configService.get('DATABASE_HOST', 'localhost'),
      port: Number(configService.get('DATABASE_PORT', 5432)),
      username: configService.get('DATABASE_USERNAME', 'postgres'),
      password: configService.get('DATABASE_PASSWORD', 'postgres'),
      database: configService.get('DATABASE_NAME', 'postgres'),
      entities: [join(__dirname + '../**/*.entity{.ts,.js}')],
      synchronize: false,
    };
  }
}

ConfigServiceをDIするのではなく,自分でnewするのがポイントです.

最後に

最近使い始めたのですが,素晴らしいフレームワークだとひしひしと感じております.
フレームワーク自体の良さはこちらの記事で紹介されています.

参考

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

[JavaScript]input要素に入力した場所によって対応した場所に出力をする

メモ

開発メモ

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <title>練習</title>
</head>
<body>
    <input name="kanso" id = "num_1" rows="1" cols="10"></input>
    <input name="kanso" id = "num_2" rows="1" cols="10"></input>
    <p id="result_1"></p>
    <p id="result_2"></p>
</body>
<script>
    $("[id^=num_]").change(function () {
        let value = $(this).val(); // 値を取得
        let id = $(this).attr('id'); // id取得
        // idによってresult_{id}に値を入れる
        let res = id.replace(/[^0-9]/g, ''); // idの数字の部分のみを取得
        let result_id = "result_" + res; // 取得した数字でidを生成
        var result = document.getElementById(result_id); // 生成したidの要素を取得
        result.innerHTML = value ? value * 100 : ''; // 取得した要素に値を入れる(入力がない場合は空)
    });
</script>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

focusイベントがキャンセルできない問題。

Vueを触っている時、ブラウザを切り替えたタイミングでフォーカスを外したいコンポーネントにあうことはありませんか?
そんな時まず思い浮かぶのは、eventをpreventDefaultすることだと思います。
事実、私もそう思い実行しました。
しかし、うまくいきませんでした。
なぜ?調べてみました。

focusイベントについて

MDNを見てみる。

focus イベントは、要素がフォーカスを受け取ったときに発生します。このイベントと focusin との違いは、 focusin がバブリングを行うのに対し focus は行わないことです。
focus の反対は blur です。

バブリング なし
キャンセル可能 いいえ

キャンセル不可のため、preventDefalutは効かないらしい・・・。
というこで、フォーカスを発生させないためには逆にフォーカスを外してやればいい。
まぁそういえばそうかといった感じですが・・・

正解コード(javascript)

el-tooltipはコンポーネント内のフォーカスを外したい要素のクラス名称です。

App.js
mounted: () => {
    window.onfocus = () => {
        const onFocusElm = window.document.activeElement;
        if (onFocusElm.classList.contains("el-tooltip")) {
            onFocusElm.blur();
        }
    };
}

正解コード(typescript)

App.ts
mounted() => {
    window.onfocus = () => {
        const onFocusElm = window.document.activeElement;
        if (onFocusElm.classList.contains("el-tooltip")) {
            (onFocusElm as HTMLElement).blur();
        }
    };
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エラー解決  Javascript 非同期通信によるメッセージの自動更新機能の実装

javascriptのエラー解決に苦心したので、その記録。

状況

チャットアプリのメッセージ機能の更新を非同期通信上で、実施しようとしたところ、
投稿まで非同期で通信できなくなった。

問題のjsファイル
(function(){
  var buildHTML = function(message){
    console.log(message)
    if (message.image){
      console('1')
      var html =  
        `<div class = "message" data-message-id=${message.id}>
        <div class="messages__message" >
          <div class="messages__message__info">
            <div class="messages__message__info__member">
              ${message.user_name}
            </div>
            <div class="messages__message__info__date">
              ${message.created_at}
            </div>
          </div>
          <div class="messages__message__text">
            <p class="messages__message__text__content">
              ${message.content}
            </p>
          </div>
          <img src=${message.image} >
        </div>`
        return html;
    } else {
      console.log('2')
      var html =
        `<div class = "message" data-message-id=${message.id}>
        <div class="messages__message" >
          <div class="messages__message__info">
            <div class="messages__message__info__member">
              ${message.user_name}
            </div>
            <div class="messages__message__info__date">
              ${message.created_at}
            </div>
          </div>
          <div class="messages__message__text">
            <p class="messages__message__text__content">
              ${message.content}
          </p>
        </div>`
        return html;
    };
  }

    $('.new_message').on('submit', function(e){
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action')
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){
      console.log(data)
      var html = buildHTML(data);
      $('.middle').append(html);
      $('form')[0].reset();
      $('.middle').animate({ scrollTop: $('.middle')[0].scrollHeight},'fast');
      $('input').prop('disabled', false);
    })
    .fail(function(){
      alert('メッセージ送信に失敗しました');
    })
  })

  var reloadMessages = function() {
    //カスタムデータ属性を利用し、ブラウザに表示されている最新メッセージのidを取得
    last_message_id = $('.message:last').data("message-id");
    $.ajax({
      //ルーティングで設定した通りのURLを指定
      url: "api/messages",
      //ルーティングで設定した通りhttpメソッドをgetに指定
      type: 'get',
      dataType: 'json',
      //dataオプションでリクエストに値を含める
      data: {id: last_message_id}
    })
    .done(function(messages) {
      console.log(messages)
      if (messages.length !== 0) {
        //追加するHTMLの入れ物を作る
        var insertHTML = '';
        //配列messagesの中身一つ一つを取り出し、HTMLに変換したものを入れ物に足し合わせる
        $.each(messages, function(i, message) {
          insertHTML += buildHTML(message)
        });
        //メッセージが入ったHTMLに、入れ物ごと追加
        $('.middle').append(insertHTML);
        $('.middle').animate({ scrollTop: $('.middle')[0].scrollHeight});
        $("#new_message")[0].reset();
        $(".form__submit").prop("disabled", false);
      }
    })
    .fail(function() {
      console.log('error');
    });
  };
  if (document.location.href.match(/\/groups\/\d+\/messages/)) {
    setInterval(reloadMessages, 7000);
  }
});
関連するhamlファイル
.messages
  .messages__message
    .messages__message__info
      .messages__message__info__member
        = message.user.name
      .messages__message__info__date
        = message.created_at.strftime("%Y年%m月%d日 %H時%M分")
    .messages__message__text
      - if message.content.present?
        %p.messages__message__text__content
          = message.content
      = image_tag message.image.url, class: 'lower-message__image' if message.image.present?

問題解決までの変更点は以下のとおり。
1. jsファイル、"buildHTML"内で定義されたhtmlにreturn html;を2箇所追記する。(エラーの解決にはこれだけでよかった気もする...)
2. hamlファイルから".message"のクラスを削除、hamlファイルとscssファイルをそれに合わせて調整する。
3. そのままでは自動更新された文にcssが当たらないので、"div class="messages__message" "の記述を削除する。(本来は閉じタグも消去しなければいけないが、記述ミスで上記コードには表記されていなかった)
4. .done内の".message"でクラス指定されている部分のクラスを指定し直す。

変更後のコードは以下

jsファイル
$(function(){
  var buildHTML = function(message){
    if (message.image){
      var html =  
        `<div class = "message" data-message-id=${message.id}>
          <div class="message__info">
            <div class="message__info__member">
              ${message.user_name}
            </div>
            <div class="message__info__date">
              ${message.created_at}
            </div>
          </div>
          <div class="message__text">
            <p class="message__text__content">
              ${message.content}
            </p>
          </div>
          <img src=${message.image} >
        </div>`
        return html;
    } else {
      var html =
        `<div class = "message" data-message-id=${message.id}>
          <div class="message__info">
            <div class="message__info__member">
              ${message.user_name}
            </div>
            <div class="message__info__date">
              ${message.created_at}
            </div>
          </div>
          <div class="message__text">
            <p class="message__text__content">
              ${message.content}
            </p>
        </div>`
        return html;
    };
  }

    $('.new_message').on('submit', function(e){
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action')
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){
      var html = buildHTML(data);
      $('.middle').append(html);
      $('form')[0].reset();
      $('.middle').animate({ scrollTop: $('.middle')[0].scrollHeight},'fast');
      $('input').prop('disabled', false);
    })
    .fail(function(){
      alert('メッセージ送信に失敗しました');
    })
  })

  var reloadMessages = function() {
    //カスタムデータ属性を利用し、ブラウザに表示されている最新メッセージのidを取得
    last_message_id = $('.message:last').data("message-id");
    $.ajax({
      //ルーティングで設定した通りのURLを指定
      url: "api/messages",
      //ルーティングで設定した通りhttpメソッドをgetに指定
      type: 'get',
      dataType: 'json',
      //dataオプションでリクエストに値を含める
      data: {id: last_message_id}
    })
    .done(function(messages) {
      if (messages.length !== 0) {
        //追加するHTMLの入れ物を作る
        var insertHTML = '';
        //配列messagesの中身一つ一つを取り出し、HTMLに変換したものを入れ物に足し合わせる
        $.each(messages, function(i, message) {
          insertHTML += buildHTML(message)
        });
        //メッセージが入ったHTMLに、入れ物ごと追加
        $('.middle').append(insertHTML);
        $('.middle').animate({ scrollTop: $('.middle')[0].scrollHeight});
        $("#new_message")[0].reset();
        $(".form__submit").prop("disabled", false);
      }
    })
    .fail(function() {
      console.log('error');
    });
  };
  if (document.location.href.match(/\/groups\/\d+\/messages/)) {
    setInterval(reloadMessages, 7000);
  }
});
hamlファイル
.message{data: {message: {id: message.id}}}
  .message__info
    .message__info__member
      = message.user.name
    .message__info__date
      = message.created_at.strftime("%Y年%m月%d日 %H時%M分")
  .message__text
    - if message.content.present?
      %p.message__text__content
        = message.content
    = image_tag message.image.url, class: 'lower-message__image' if message.image.present?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エラー解決  Javascript

javascriptのエラー解決に苦心したので、その記録。

状況

チャットアプリのメッセージ機能の更新を非同期通信上で、実施しようとしたところ、
投稿まで非同期で通信できなくなった。

問題のjsファイル
(function(){
  var buildHTML = function(message){
    console.log(message)
    if (message.image){
      console('1')
      var html =  
        `<div class = "message" data-message-id=${message.id}>
        <div class="messages__message" >
          <div class="messages__message__info">
            <div class="messages__message__info__member">
              ${message.user_name}
            </div>
            <div class="messages__message__info__date">
              ${message.created_at}
            </div>
          </div>
          <div class="messages__message__text">
            <p class="messages__message__text__content">
              ${message.content}
            </p>
          </div>
          <img src=${message.image} >
        </div>`
        return html;
    } else {
      console.log('2')
      var html =
        `<div class = "message" data-message-id=${message.id}>
        <div class="messages__message" >
          <div class="messages__message__info">
            <div class="messages__message__info__member">
              ${message.user_name}
            </div>
            <div class="messages__message__info__date">
              ${message.created_at}
            </div>
          </div>
          <div class="messages__message__text">
            <p class="messages__message__text__content">
              ${message.content}
          </p>
        </div>`
        return html;
    };
  }

    $('.new_message').on('submit', function(e){
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action')
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){
      console.log(data)
      var html = buildHTML(data);
      $('.middle').append(html);
      $('form')[0].reset();
      $('.middle').animate({ scrollTop: $('.middle')[0].scrollHeight},'fast');
      $('input').prop('disabled', false);
    })
    .fail(function(){
      alert('メッセージ送信に失敗しました');
    })
  })

  var reloadMessages = function() {
    //カスタムデータ属性を利用し、ブラウザに表示されている最新メッセージのidを取得
    last_message_id = $('.message:last').data("message-id");
    $.ajax({
      //ルーティングで設定した通りのURLを指定
      url: "api/messages",
      //ルーティングで設定した通りhttpメソッドをgetに指定
      type: 'get',
      dataType: 'json',
      //dataオプションでリクエストに値を含める
      data: {id: last_message_id}
    })
    .done(function(messages) {
      console.log(messages)
      if (messages.length !== 0) {
        //追加するHTMLの入れ物を作る
        var insertHTML = '';
        //配列messagesの中身一つ一つを取り出し、HTMLに変換したものを入れ物に足し合わせる
        $.each(messages, function(i, message) {
          insertHTML += buildHTML(message)
        });
        //メッセージが入ったHTMLに、入れ物ごと追加
        $('.middle').append(insertHTML);
        $('.middle').animate({ scrollTop: $('.middle')[0].scrollHeight});
        $("#new_message")[0].reset();
        $(".form__submit").prop("disabled", false);
      }
    })
    .fail(function() {
      console.log('error');
    });
  };
  if (document.location.href.match(/\/groups\/\d+\/messages/)) {
    setInterval(reloadMessages, 7000);
  }
});
関連するhamlファイル
.messages
  .messages__message
    .messages__message__info
      .messages__message__info__member
        = message.user.name
      .messages__message__info__date
        = message.created_at.strftime("%Y年%m月%d日 %H時%M分")
    .messages__message__text
      - if message.content.present?
        %p.messages__message__text__content
          = message.content
      = image_tag message.image.url, class: 'lower-message__image' if message.image.present?

問題解決までの変更点は以下のとおり。
1. jsファイル、"buildHTML"内で定義されたhtmlにreturn html;を2箇所追記する。(エラーの解決にはこれだけでよかった気もする...)
2. hamlファイルから".message"のクラスを削除、hamlファイルとscssファイルをそれに合わせて調整する。
3. そのままでは自動更新された文にcssが当たらないので、"div class="messages__message" "の記述を削除する。(本来は閉じタグも消去しなければいけないが、記述ミスで上記コードには表記されていなかった)
4. .done内の".message"でクラス指定されている部分のクラスを指定し直す。

変更後のコードは以下

jsファイル
$(function(){
  var buildHTML = function(message){
    if (message.image){
      var html =  
        `<div class = "message" data-message-id=${message.id}>
          <div class="message__info">
            <div class="message__info__member">
              ${message.user_name}
            </div>
            <div class="message__info__date">
              ${message.created_at}
            </div>
          </div>
          <div class="message__text">
            <p class="message__text__content">
              ${message.content}
            </p>
          </div>
          <img src=${message.image} >
        </div>`
        return html;
    } else {
      var html =
        `<div class = "message" data-message-id=${message.id}>
          <div class="message__info">
            <div class="message__info__member">
              ${message.user_name}
            </div>
            <div class="message__info__date">
              ${message.created_at}
            </div>
          </div>
          <div class="message__text">
            <p class="message__text__content">
              ${message.content}
            </p>
        </div>`
        return html;
    };
  }

    $('.new_message').on('submit', function(e){
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action')
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){
      var html = buildHTML(data);
      $('.middle').append(html);
      $('form')[0].reset();
      $('.middle').animate({ scrollTop: $('.middle')[0].scrollHeight},'fast');
      $('input').prop('disabled', false);
    })
    .fail(function(){
      alert('メッセージ送信に失敗しました');
    })
  })

  var reloadMessages = function() {
    //カスタムデータ属性を利用し、ブラウザに表示されている最新メッセージのidを取得
    last_message_id = $('.message:last').data("message-id");
    $.ajax({
      //ルーティングで設定した通りのURLを指定
      url: "api/messages",
      //ルーティングで設定した通りhttpメソッドをgetに指定
      type: 'get',
      dataType: 'json',
      //dataオプションでリクエストに値を含める
      data: {id: last_message_id}
    })
    .done(function(messages) {
      if (messages.length !== 0) {
        //追加するHTMLの入れ物を作る
        var insertHTML = '';
        //配列messagesの中身一つ一つを取り出し、HTMLに変換したものを入れ物に足し合わせる
        $.each(messages, function(i, message) {
          insertHTML += buildHTML(message)
        });
        //メッセージが入ったHTMLに、入れ物ごと追加
        $('.middle').append(insertHTML);
        $('.middle').animate({ scrollTop: $('.middle')[0].scrollHeight});
        $("#new_message")[0].reset();
        $(".form__submit").prop("disabled", false);
      }
    })
    .fail(function() {
      console.log('error');
    });
  };
  if (document.location.href.match(/\/groups\/\d+\/messages/)) {
    setInterval(reloadMessages, 7000);
  }
});
hamlファイル
.message{data: {message: {id: message.id}}}
  .message__info
    .message__info__member
      = message.user.name
    .message__info__date
      = message.created_at.strftime("%Y年%m月%d日 %H時%M分")
  .message__text
    - if message.content.present?
      %p.message__text__content
        = message.content
    = image_tag message.image.url, class: 'lower-message__image' if message.image.present?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

押すと色が変わるボタンの作り方を教えてください

初めましてプログラミング初心者です

個人アプリ作成しています

ボタンが押されたときにボタンの色が変わるように実装しています

教えていただける方いましたらお願いします

haml
%input{type: "button", value: "12", class: "btn"}

js
$(function(){
$('.btn').on('click', function(event){
event.preventDefault();
$(this).toggleClass('active');
});
});

scss
input{
width: calc(100% / 5);
height: 40px;
color:whitesmoke;
margin-left: 8px;
.btn.active{
color: red;
background-color: red;
}

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

【GAS】YYYY/MM/DD/hh/mm/ssからUNIXタイムスタンプを生成する

GASで引数に日時を取るスラッシュコマンドを作るにあたって、「スラッシュコマンドで指定する日付と時刻はわかりやすいYYYY/MM/DD/hh/mm/ssがいい。でもSlack APIを叩く時はUNIXタイムスタンプで指定しないといけない」という都合により、GASで「YYYY/MM/DD/hh/mm/ss→UNIXタイムスタンプ」の変換をすることになったんですが、思いの外ネット上にベストマッチなソリューションが見当たらなかったので書き残しておきます。

実装①

//2020年1月1日12時34分56秒
var str = "2020/01/01/12/34/56"
var array = str.split(/\//).map(Number)
var date = new Date(array[0], array[1] - 1, array[2], array[3], array[4], array[5])
date = (date.getTime() / 1000) + ''
Logger.log(date) // -> 1577849696

実装②

var str = "2020/01/01/12/34/56"
var array = str.split(/\//)
var date = Utilities.formatString("%s-%s-%sT%s:%s:%s+09:00", array[0], array[1], array[2], array[3], array[4], array[5])
date = Date.parse(date) / 1000 + ''
Logger.log(date) // -> 1577849696

確認

UNIX時間⇒日付変換 - 高精度計算サイト - Keisan - CASIO
スクリーンショット (47).png

1000で割る意味

getTime()Date.parse()によって返ってくるUNIX時は単位がミリ秒になっています。Slack APIで指定するタイムスタンプは秒単位でなければならないので、1000で割った値を文字列に変換しています。

実装①のメリデメ

メリット

  • 一度数値に変換するので、引数のバリデーションとかゼロ埋めとか気にしなくていい(?)
  • 短くて読みやすい(?)

デメリット

  • Dateオブジェクトを経由するのが煩わしくないこともない
  • Dateクラスのメソッドの仕様が厄介

デメリットの方について少し説明しますと、数列からDateオブジェクトを生成するnew Date()の構文は

new Date(year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);

なのですが、このmonthIndexは0から11で指定するんですね。

注: 引数 monthIndex は 0 から始まります。 つまり 1 月 = 0、 12 月 = 11 です。

Date - JavaScript | MDN

なので、new Date()に渡す時にそこだけ- 1する必要があります。

あと、公式マニュアルを見返していて気付いたんですが(強調は筆者)、

注: Date を複数の引数を伴ってコンストラクタとして呼び出された場所では、値が論理範囲より大きくても (month 値に 13 を与えたり、minute 値に 70 を与える)、調整された値になります。つまり、new Date(2013, 13, 1) は、new Date(2014, 1, 1) と等しくなるように調整され、両者とも 2014-02-01 の日付を生成します

……何…… ……だと……(画像略)

var str = "2020/13/01/01/03/05"
var array = str.split(/\//).map(Number)
var date = new Date(array[0], array[1] - 1, array[2], array[3], array[4], array[5])
Logger.log(date) // -> Fri Jan 01 01:03:05 GMT+09:00 2021

……ほん…… ……とだ……(来年の元日って金曜なんだ)

これだとうっかりタイポした時にちゃんと動作してくれないどころか全く予想外の結果になってしまいますね・・・。

結局バリデーションを自分で設定しないといけないとなると、最初に挙げたメリットもおじゃんになってしまいました。無念。

実装②のメリデメ

メリット

  • 分割してフォーマットに流し込んでパースするだけ
  • パースした返り値がUNIX時の整数値になっているため目的に合致している
  • フォーマットが厳密に決まっているので、実装①のような厄介な挙動をしない

デメリット

  • 特にない?

↓の記事ではDate.parse()の返り値がDateではなく整数になることを「」と形容していますが、今回のような使い方をする場合にはまさしく割れ鍋に綴じ蓋ですね。

JavaScript の Date は罠が多すぎる - Qiita

そして"2020/13/01/01/03/05"のようなありえない日付を指定した場合の挙動ですが、

var str = "2020/01/13/12/34/56"
var array = str.split(/\//)
var date = Utilities.formatString("%s-%s-%sT%s:%s:%s+09:00", array[0], array[1], array[2], array[3], array[4], array[5])
date = Date.parse(date) / 1000 + ''
Logger.log(date) // -> NaN

ちゃんと失敗してくれています。これなら例外処理を入れるにもif(isNaN(date))で済みます。いいですね。っていうかマニュアルにちゃんと書いてあるんですけども。

文字列を解釈できなかったり不正な日付 (例えば 2015-02-31) が指定された場合 NaN を返します。

Date.parse() - JavaScript | MDN

ちなみに、Date.parse()といえばブラウザによって挙動が異なる1ことで有名(らしい)ですが、今回はGASなのでGASで動けば十分ということで、これでいいと思います(GASの仕様が変わらなければ)。

まとめ

2つの実装の長所・短所を比較した結果、実装②の方が良さそうだなと思いました。

書き始める前は実装①推しだったんですが、やはりマニュアルはちゃんと読んでおくべきですね。色んな意味で勉強になりました。

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


  1. でも→を見る限りでは、最新バージョンならほとんどのブラウザで問題なく動作するんですかね?Date.parse() ブラウザー実装状況 - JavaScript | MDN 

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

初心者によるプログラミング学習ログ 211日目

100日チャレンジの211日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

211日目は

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

Rails (with sorcery)+ React でOAuth認証を実装してみた

sorceryでRails +SPA構成のOauthサンプルがあまりなさそうなので
参考になれば。

環境

backend
- rails 6系

front
- react:16.12
- react-router: v5系

以下、sorceryのwikiに沿いつつ、SPA用でカスタムしたところを中心に記載
sorcery wiki

今回はgithub と連携してみる


開発の概要

Backend(Rails)
- oauthからのcallbackを受け取り、ユーザー作成及びログイン処理

Front(React)
- githubAPIへQueryString形式でパラメーターをもたせてアクセス。
- 認証後callBackURLにリダイレクトし、Backendに取得したパラメーターを送信


Backend側開発


external module関連のセットアップ

# external moduleのインストール
bundle exec rails g sorcery:install external --only-submodules

# migration
bundle exec rails db:migrate

# 認証用モデル作成
bundle exec rails g model Authentication --migration=false




sorceryのgithub認証関連設定変更

認証用情報の取得
github側の認証設定は以下から行う
https://github.com/settings/developers

initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:external] #:external追加
Rails.application.config.sorcery.configure do |config|
 ...
  config.github.key = "your github key"
  config.github.secret = "your github secret"
  config.github.callback_url = ""
  config.github.user_info_mapping = {:email => "email" }
  config.github.scope = "user:email"
end



Oauth用controllerの作成

oauths_controller
class OauthsController < ApplicationController
# Frontで取得したToken情報をもとにユーザー認証をするMethod
  def callback
    provider = params[:provider]
    # loginできた場合はここで200を返す
    if @user = login_from(provider)
      render json: { status: 'OK' }
    else
      begin
       # loginできない場合は送られてきた情報をもとにユーザー作成
        @user = create_from(provider)

        reset_session
        auto_login(@user)
        render json: { status: 'OK' }
      rescue
        render json: { status: 'NG' }, status: 400
      end
    end
  end
end



ルーティングの設定

  • wiki記載の内容と異なり、oauth用tokenが送信されてくるAPIのみでOK
config/routes.rb
Rails.application.routes.draw do 
  ...
  post "oauth/callback" => "oauths#callback"
end




Front側開発


関連するルーティングの定義

router.jsx
const AppRouter = () => (
  <Router>
    <Switch>
      <Route
        path="/callback/:provider/"
        component={ExternalAuthCallback}
      />
      <Route path="/sign_in" component={SignIn} />
      <Route component={NotFound} />
    </Switch>
  </Router>
);

export default AppRouter;



サインインページ

  • サインインページの1機能としてGithub認証があるイメージ
  • 通常のRailsのOauthと異なり、callbackされるURLはReactで構成されたSPAのURL(=>"/callback/:provider/")が叩かれることに注意
SignIn.jsx
// CONST.GITHUB.REDIRECT_URL = "http://localhost:3001/callback/github/"

const GITHUB_AUTH_URL = `https://github.com/login/oauth/authorize?client_id=${CONST.GITHUB.APP_ID}&redirect_url=${CONST.GITHUB.REDIRECT_URL}&scope=user:email`;

// ただ queryStringの付与したURLのリンクを踏ませるだけ
const signInForm = () => (
      <div className={styles.submitBox}>
        <Button href={GITHUB_AUTH_URL}>GITHUBで認証</Button>
      </div>
);

export default signInForm;



CallbackURLコンポーネント

  • callback時に叩かれるURLで利用するコンポーネント
  • URLパラメーターでprovider名(今回は"github")を取得
  • callbackURLのquesyStringに付与された認証情報(code=XXXX)を取得
  • ReactからバックエンドAPIへPostする
oAuthCallback/index.jsx
import React, { useState } from "react";
import queryString from "query-string";
import { useHistory } from "react-router";
import { useParams, useLocation } from "react-router-dom";
import { api } from "../../../modules/user";
import Circular from "../../atoms/circular";

const BEFORE = "BEFORE";
const DOING = "DOING";

const ExternalAuth = () => {
  const location = useLocation();
  const history = useHistory();
  const { code = "" } = queryString.parse(location.search);
  const { provider = "" } = useParams();
  const [requestStatus, setRequestStatus] = useState(BEFORE);

  const request = () => {
    setRequestStatus(DOING);
    api.sendExternalAuthRequest({ code, provider }).then(isSuccess => {
      if (isSuccess) {
        history.push("/member/dashboard"); // login後ページ
      } else {
        history.push(PAGE_PATH.AUTH_SIGN_IN); //認証失敗した場合
      }
    });
  };
    
  if (requestStatus === BEFORE) {
    request();
  }
  return (
    <div className={styles.container}>
      <Circular />
    </div>
  );
};

export default ExternalAuth;


Rails APIへのリクエスト

api.js
export const sendExternalAuthRequest = async ({
  code,
  provider
}) => {
  const requester = requestManager.get();
  return requester
    .post(
      "/oauth/callback",
      {
        code,
        provider
      },
    )
    .then(() => true)
    .catch(() => false);
};

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

【最短サンプル】ブラウザのJavaScriptで、bitFlyerのAPIにWebSocket接続してビットコイン最新価格を次々と受け取る

以下をindex.htmlとして保存して、ブラウザで開けばconsoleに現在価格が次々と出ます。

index.html
<script>
    var sock = new WebSocket('wss://ws.lightstream.bitflyer.com/json-rpc');

    sock.addEventListener("open", e => {
        sock.send('{"method": "subscribe","params": {"channel": "lightning_ticker_FX_BTC_JPY" }}');
    });
    sock.addEventListener("message", e => {
        var json = JSON.parse(e.data).params.message;
        console.log(json);
    });
</script>

1.bitFlyerにWebSocket接続して
2.価格のチャンネルを購読(subscribe)すれば
3.次々と最新価格が送られて来て、そのたびにmessageイベントが発生する
という動きになります。

公開されているAPIですので、bitFlyer契約者でなくても試せます。

例ではビットコインFXの価格を取得しています。詳しくは以下
https://bf-lightning-api.readme.io/docs/realtime-ticker

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

JavaScript関数・引数・戻り値

関数

main.js
const kansu = function(){
    console.log("関数がうごいた!");
}
kansu();
関数がうごいた!

アロー関数

main.js
const arrawKansu = () =>{
    console.log('アロー関数が動いた!');
}

arrawKansu();
アロー関数が動いた!

引数

main.js
'use strict';
const eat = (name) =>{
    console.log(`飯の名前${name}`);
}

eat('カレー');
eat('チャーハン');
eat('白飯');
飯の名前カレー
飯の名前チャーハン
飯の名前白飯

複数の引数

main.js
const person = (firstName,lastName) =>{
    console.log(`「姓」:${firstName}「名」:${lastName}`);
}
person('鈴木','一郎');
person('田中','太郎');
「姓」:鈴木「名」:一郎
「姓」:田中「名」:太郎

戻り値

main.js
const person = (firstName,lastName) =>{
    console.log(firstName);
    console.log(lastName);
    return firstName + lastName;
}
const personName = person('鈴木','一郎');
console.log(personName);
鈴木
一郎
鈴木一郎
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Hooksでインスタンスメソッドを実装する

コンポーネントの特定の動作を外部から呼び出すための方法です。関数コンポーネントでも実装できました。もはやクラスではないのでインスタンスメソッドと呼ぶのは正確ではないかもしれませんが…。

propsの値変更で多くのことは実現できるので出番は限られてくると思いますが、入力フィールドにフォーカスする、CSSアニメーションを再生するといった場合には有効だと思います。クラスを使わずに実装する際の備忘録としてまとめました。

メソッドの実装(子コンポーネント)

useImperativeHandleフックとforwardRefを組み合わせて実装します。この例の場合、インスタンス.doSomething()が呼ばれたタイミングでログが出力されます。

import React, { useImperativeHandle, forwardRef } from 'react';

function MyComponent(props, ref) {
  useImperativeHandle(ref, () => ({
    doSomething: () => {
      console.log('Do something');
    }
  }));
  return <div>My Component</div>;
}

export default forwardRef(MyComponent);

メソッドの呼び出し(親コンポーネント)

useRefフックとref属性によって子コンポーネントのインスタンスが参照できるようになります。ref.current.メソッド()で先程のメソッドを呼び出します。

import React, { useRef } from 'react';
import MyComponent from './MyComponent';

export default function App() {
  const myComponent = useRef(null);
  return (
    <div>
      <MyComponent ref={myComponent} />
      <button onClick={() => myComponent.current.doSomething()}>
        Click
      </button>
    </div>
  );
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ChatWorkのデータをcsvバックアップ.ついでにSlackへ・・・

先に謝罪しておきます.
Qiita初投稿です.
読みづらい点あると思います.
ごめんなさい.

Chatworkのデータをcsvでバックアップしておきたい!

なんらかの事情があって,ChatWorkのデータをcsvで手元に置いたり印刷したい!
そんな人もいるのではないでしょうか?

まぁ,だいたいエクスポート機能あるやろ....
と思っているあなた.

公式ページのよくある質問
https://help.chatwork.com/hc/ja/articles/203352770-%E3%83%81%E3%83%A3%E3%83%83%E3%83%88%E3%81%AE%E3%83%AD%E3%82%B0%E3%82%92%E4%BF%9D%E5%AD%98%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%81%8B-%E5%8D%B0%E5%88%B7%E3%81%AA%E3%81%A9-

恐れ入りますが、現時点ではチャットのログの保存機能や印刷機能はありません。
なお、エンタープライズプラン、およびKDDI Chatworkにはチャットログのエクスポート機能があります。
フリープラン、パーソナルプラン、ビジネスプランではエクスポート機能はありませんので
コピーペーストなどでログを保存・印刷してください。

まじですか!
コピペですか!

フリープランの私は,
コピペ保存というワードと
エクスポート機能がないことに驚きました...

こういうのって,だいたいツール出てるよね〜
っとqiitaを見てみれば一応参考はあったものの, 

ChatWorkのAPI仕様変更やらで,なにやら不穏な気配...

プランアップグレードして課金するか....
どうしよう....

よし,domから抜くか!

ということで,簡単に概要を説明

  • web版のchatworkを表示
  • chrome開発者ツールのコンソールを表示
  • 画面サイズをモバイルにして読み込み
  • 「タップして過去のメッセージを読み込む」クリックしてバックアップしたいとこまで読み込む
  • 以下のjavascriptを開発者ツールのコンソールから実行
  • コンソールに整形されたデータが出力される
  • 整形されたデータを煮るなり焼くなり
  • もはやコピペと変わらないんじゃ
  • コピペで保存よりは整形して出力できる・・・かな

ということで実際にやっているスクショがこちら.

スクリーンショット 2020-01-15 0.11.16.png

スクリーンショット 2020-01-15 0.15.48.png

webブラウザ上に表示されているデータはたいてい構造化されているので,
チャット形式のような形式が決まっているものであれば,割と簡単に文章を抽出できます.

やっていることは,ブラウザで読み込んだhtmlの構造をみて,整形し,コンソールに表示しているだけです

PC表示だと上にスクロールしまくって過去の投稿を読み込ませなければいけないのですが,
スマホ表示にしておけば,「タップして過去のメッセージを読み込む」のクリックで過去の表示読み込みができます.
(最初の画像の赤矢印のところ)

ChatWorkに限らず,スマホ表示にした場合に構造が見やすくなったり
諸々の手順が楽になる事例は他にも有ると思いますので知っておくといいかも.

あとは,javascriptできれば,応用して使えると思います.
以下は試しにslack用のcsvインポート形式に合わせて整形しています.

無理矢理なところあります,あくまでも参考に...

/**
* Chatwork to csv to Slack script 
*
* 「スマホ表示」のWeb版Chatworkのコンソールに
* 以下のスクリプトを貼り付けて使用する.
* PC表示時のdomよりシンプルなので,処理が楽?と思ったのでやってみた
*/

//-------状況に合わせて変更-----------
function changeUserName(str){
    //return str; //名前をそのまま使いたい場合はそのままリターンしてください

    if(str==='チャットワークユーザ名1'){ //適に変更してください
        return 'UFS******';    //適に変更してください
    }
    else if(str==='チャットワークユーザ名2'){ //適に変更してください
        return 'UHH******';         //適に変更してください
    }
}
var slackChannelName = '#インポートテスト'; //適に変更してください
//----------------------------------


//SlackのcsvインポートはUnixタイム指定なので,chatwork上の文字列から変換.
//Yearは2019で決め打ちしちゃってます!
//TODO:要修正
function strToUnixTime(str){
    //return str; //日付をそのまま使いたい場合はそのままリターンしてください

    var split = str.split(' ');
    var md = split[0];
    md = md.replace('','/');
    md = md.replace('','');
    var ymd = '2019/' + md + ' ';
    var allstr = ymd + split[1];
    var dateobj = new Date(allstr);
    return parseInt( dateobj /1000 );
}

//出力用の配列.これを最終的に出力します.
var arr = [
    ['ts', 'team', 'user', 'text']
]
var cachDate;
var cashUser;


//タイムラインのチャット部分を取得し,投稿の数だけループ
$('#cw_timeline').find('.ui_chat').each((idx, item) => {

    //この変数に一行分(一投稿)のデータを入れていきます
    var field = []

    //日付を抜き出して挿入
    //TODO:雑に抜き出しているので要修正
    if ($(item).find('.ui_chat_date').length > 0) {
        var t =  $(item).find('.ui_chat_date').text().trim();
        if( t.includes("") ){
            cashDate = t;
        }else{
            var month = cashDate.split(' ');
            t = month[0] + ' ' + t;
        }
        t = strToUnixTime(t);
        field.push( '"'+ t +'"' );
    } else {
        field.push('');
    }

    //チャンネル名を挿入
    field.push( '"'+ slackChannelName + '"' );

    //ユーザー名を挿入.連続投稿はユーザー名が無いパターンがあるので前投稿のキャッシュで埋める
    if ($(item).find('.cw_chat_name').length > 0) {
        var name = $(item).find('.cw_chat_name').text();
        if(name===''){          
            field.push( '"' + changeUserName(cashUser) +'"' );          
        }
        else{
            field.push( '"' + changeUserName(name) +'"' );
            cashUser = name;
         }
    }
    else {
        field.push( '"' + changeUser(cashUser) +'"' );
    }

    //本文を抜き出して挿入
    var message = $(item).find('pre').text().replace(/(\r\n|\n|\r)/g, "");
    field.push( '"'+ message +'"' );
    arr.push(field)
});

//コンソール出力
arr.forEach((value) => {
  console.log( value[0] + ','  + value[1] + ','  + value[2] + ','  + value[3] );
});

補足

のちにSlackでインポートするために,ChatWork上のユーザ名をSlackのUserIDに置き換えています.

console出力した一行ずつはこんな形になりますが,
>"1563328680","#インポートテスト","UFSRY3QEN","マイチャットを作成しました。" VM283:84

コンソールからコピペした場合に
最後のVM283:84みたいなやつが余分で付いてきてしまうので,
適当なテキストエディターで置換して一括削除してください.

excelにもインポートしたりして使ったりできます.

スクリーンショット 2020-01-15 0.18.42.png

ちなみに,

//コンソール出力
/*
arr.forEach((value) => {
  console.log( value[0] + ','  + value[1] + ','  + value[2] + ','  + value[3] );
});
*/
window.open(encodeURI("data:text/csv;charset=utf-8," + arr.map(e => e.join(",")).join("\n")))

みたいにすれば,csvでそのまま出力できます.
ただ,自分の環境では大量のデータだと出力に失敗することがあり,
最終的にはコンソール出力でコピペしました.

ChatworkからSlackへのインポートのために

  • 文字列の日付情報から,UNIXタイムスタンプへ変更しなければならない.
  • 同一ユーザの連続投稿は,ユーザ名情報が無いパターンが存在するので,その対応をする.
  • "UNIXタイムスタンプ","チャンネル名","ユーザ名","本文" 形式のcsvがSlackのインポート形式

このポイントを押さえて,整形すればインポートできます.
上記のjavascriptのサンプルでは,ChatWork上の時間文字列からUNIXタイムスタンプへ変換して出力しています.

※日付を2019年に決め打ちで作ってしまっているので,時間があれば修正します.

スクリーンショット 2020-01-15 0.25.35.png

あとは,Slackのcsvインポートのところでインポートさせてあげれば完了です.

ちゃんとインポートされるまで,いくつか手順をふみますが,
以下のようになれば完了です!

スクリーンショット 2020-01-15 0.28.48.png

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

stage.mouseXの注意点

概要

createJSでのcanvasを使ったゲームを作成する際に

canvas = document.getElementById("canvas");
// stage作成
stage = new createjs.Stage(canvas);
createjs.Ticker.addEventListener("tick", stage);

上記のようにステージを作成することが一般的
このような状況で画面上のタップした座標を取得したい場合に

touchPosX = stage.mouseX / stage.scaleX;
touchPosY = stage.mouseY / stage.scaleY;

上記コードで取得できる。
PC上では問題ないが、
実機対応のためにタッチ操作を有効にした状態で

// タッチ操作を有効にする
createjs.Touch.enable(stage);

画面に配置したcanvasの上下左右の領域の端をタップすると
stage.mouseX, stage.mouseYの値が最後にタップした座標に固定されてしまう

対策

タップした座標を取る際にeventから座標を取得するようにする

var button;
button.addEventListener("mousedown", function(event) {
    var tapPointX = event.stageX;
});

原因

どうやら画面に配置したcanvasの上下左右の領域の端を押したときに、
イベントが二つ飛んでくるらしい(touchStartとmousedown)
イベントの種類ごとに_primaryPointerIDが設定される

mouseDown touchStart
_primaryPointerID -1 0

canvas内のオブジェクトをタップしているだけでは

stageの_primaryPointerIDが0であるが画面に配置したcanvasの上下左右の領域の端を押すと、-1になってしまう

タップした点に関する情報はstage.pointerDataにprimaryPointerIDごとに保存されているのだが、一度_primaryPointerIDを-1にしてしまうと、そのときのx,y座がpointerData[-1]に保存され、stage.mouseXはその値を参照してしまう

参考サイト

https://createjs.com/docs/easeljs/files/easeljs_display_Stage.js.html
(200行目、60行目、)

https://qiita.com/calmbooks/items/6df5e01a7a29417e55c6
(タッチデバイスにて、mousedownが2回発火)

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

【Nuxt.js】pagination導入編:まずは大枠を理解しよう!

前置き

picture_pc_094b7d42251383e46c5e27f6fac4da99.png
ページネーションをやっていきます。
いくつかに分けて書いていきます。
今回は導入編として
どんな形でつくるかの大枠を説明?

いつも通り
超簡潔に説明できれば良いので
まずはこれだけ作りましょう??
picture_pc_14b7bf6157e97653327461fadf0fda96.png


は?これだけ?


そうです?笑

表示はこれだけですが、
導入としてこちらをやります。
・ページ遷移のmethods
・現在いるページにclassを付与

Let's try?

Step1: dataを用意

基本はページ番号 = 配列番号を
増やしたり減らしたり、
該当番号にクラスをつけるだけですね。

なのでまずはdataを用意?
初期値はそれぞれ0です。
・now = 現在のページ
・last = 残りのページ

一応ページコンテンツとして
数字の繰り返しだけいれておきましょう。

index.vue
<template>
 <div class="page">
   <ul>
     <li v-for="n in 5">
       {{ n }}
     </li>
   </ul>
 </div>
</template>

<script>
export default {
 data() {
   return {
     newsSection: {
       nav: {
         now: 0,
         last: 0,
       },
     },
   }
 },
}
</script>

Step2: methodsを用意

【動作】
矢印を押すとそれぞれmethodsで
Step1で用意したdataを動かしていきます。
5ページ(0~4番号)あるので、
はみ出す分はそれぞれ-1と5
はみ出したら0か4に戻します。
・now
・last
現在のページ数を反映させるものは
実践編で解説します。

【methods】
・←を押すとclickPrevNewsPage
・→を押すとclickNextNewsPage

【式】
三項演算を使用
式1 ? 式2 : 式3
式1がtrueなら式2、falseなら式3

index.vue
<template>
 <div class="nav">
   <div
     class="prev"
     @click="clickPrevNewsPage()"
   >
     <svg /> // 省略
   </div>
   <ul>
     <li>
       ページ番号
     </li>
   </ul>
   <div
     class="next"
     @click="clickNextNewsPage()"
   >
     <svg /> // 省略
   </div>
 </div>
</template>

<script>
export default {
 data() {
   return {
     newsSection: {
       nav: {
         now: 0,
         last: 0,
       },
     },
   }
 },
 methods: {
   clickPrevNewsPage() {
     this.newsSection.nav.now = this.newsSection.nav.now - 1 < 0
       ? this.newsSection.nav.now
       : this.newsSection.nav.now - 1
   },
   clickNextNewsPage() {
     this.newsSection.nav.now = this.newsSection.nav.now + 1 > this.newsSection.nav.last - 1
       ? this.newsSection.nav.now
       : this.newsSection.nav.now + 1
   },
 },
}
</script>

【解説】
◾️clickPrevNewsPage
 ※nowが0の時は1ページ目。
式1
・パターン1
 現在1ページ(配列番号0)なら
 0 = 0 - 1 < 0
 trueなので式2を実行
・パターン2
 現在4ページ(配列番号3)なら
 3 = 3 - 1 < 0
 falseなので式3を実行

 式2 true
  now = 0なので0番目(1ページ目)にする
  = - 1番目(0ページ目)にはいけない
 式3 false
  - 1して1ページ戻る

順番的に分かりにくいかもしれませんが要は
nowが0以上の時は - 1していって、
- 1になったら0に戻すと言うことですね。

◾️clickNextNewsPage
※nowが4の時は最終ページ。
式1
・パターン1
 現在5ページ(配列番号4)なら
 残り2ページ(配列番号4, 5)
 4 = 4 + 1 > 2 - 1
 2 - 1を左辺にもっていくので
 ・ - 1にチェンジ
 ・符号も逆向きにチェンジ
 4 - 1 =< 5
 trueなので式2を実行
・パターン2
 現在4ページ(配列番号3)なら
 残り3ページ(配列番号3, 4, 5)
 3 = 3 + 1 > 3 - 1
 3 + 2 =< 4
 falseなので式3を実行

 式2 true
 now = 4なので4番目(5ページ目)にする
  = 5番目(6ページ目)にはいけない
 式3 false
  + 1して1ページ進む

複雑になってきましたが
1度理解すればテンプレとして使えますね?

classを付与

1ページ目にいる時は
そこだけボーダーをつけます。
ついでにクリックしたら
該当ページに飛ぶように@clickも?

index.vue
ページ表示部分を変更します。

// 変更前
   <ul>
     <li>
       ページ番号
     </li>
   </ul>

// 変更後
   <ul>
     <li
       :class="{ selected: newsSection.nav.now === 0}"
       @click="newsSection.nav.now = 0"
     >
       1
     </li>
   </ul>

クラス部分追加。

index.vue
<style lang="scss" scoped>
  .nav {
     ul {
       li {
         &.selected {
           padding: 3px 5px;
           border: 1px solid #4B4B4B;
         }
       }
     }
  }
}
</style>

【解説】
:class="{ class名: 式 }"
クラスバインディングで
nowが0と完全一致した場合のみ
selectedクラスを付与します。
つまり0番目の1ページにいる時は
1ページだけにボーダーつけてくれます。
https://jp.vuejs.org/v2/guide/class-and-style.html
==と===の違いはこの記事が分かりやすいです!
https://www.sejuku.net/blog/23942

@click="newsSection.nav.now = 0"
now = 現在いるページ
0にするので0番目(1ページ目)にする

ここまでのコード

毎度のことですが、
cssはほとんど省いております。
今回は
・ページ遷移のmethods
・現在いるページにclassを付与
でした?
続きは実践編などで公開します。

index.vue
<template>
 <div class="nav">
   <div
     class="prev"
     @click="clickPrevNewsPage()"
   >
     <svg /> // 省略
   </div>
   <ul>
     <li
       :class="{ selected: newsSection.nav.now === 0}"
       @click="newsSection.nav.now = 0"
     >
       1
     </li>
   </ul>
   <div
     class="next"
     @click="clickNextNewsPage()"
   >
     <svg /> // 省略
   </div>
 </div>
</template>

<script>
export default {
 data() {
   return {
     newsSection: {
       nav: {
         now: 0,
         last: 0,
       },
     },
   }
 },
 methods: {
   clickPrevNewsPage() {
     this.newsSection.nav.now = this.newsSection.nav.now - 1 < 0
       ? this.newsSection.nav.now
       : this.newsSection.nav.now - 1
   },
   clickNextNewsPage() {
     this.newsSection.nav.now = this.newsSection.nav.now + 1 > this.newsSection.nav.last - 1
       ? this.newsSection.nav.now
       : this.newsSection.nav.now + 1
   },
 },
}
</script>

<style lang="scss" scoped>
  .nav {
     ul {
       li {
         &.selected {
           padding: 3px 5px;
           border: 1px solid #4B4B4B;
         }
       }
     }
  }
}
</style>

このアカウントでは
Nuxt.js、Vue.jsを誰でも分かるよう、
超簡単に解説しています??
これからも発信していくので、
ぜひフォローしてください♪

https://twitter.com/aLizlab

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

機械学習による推論をJavaScript上で簡単に実装できるml5.js

機械学習については解説記事をかじった程度の知識しかない僕でも、物体検出プログラムを簡単に作れたので紹介します。

ml5.js

機械学習プラットフォームでお馴染みのTensorFlowをJavaScriptで実装した TensorFlow.js というライブラリがあります。

WebGLを利用したGPGPUで計算を行うことによりネイティブを肉薄するパフォーマンスを得た、ブラウザで動く機械学習ライブラリです。
そしてTensorFlow.jsを、専門知識がなくても使えるよう至れり尽くせりしてくれるラッパーがml5.jsというわけです。

使い方

学習済モデルやアルゴリズムを選択してデータを入力することで、推論結果を得ることができます。

学習済モデルはデータ量が大きいのでライブラリ本体とは別に存在しており、選択した時点で配信サーバーへAjaxする挙動になります。
巨大なものだと100MB近くにまで達するので、スマホで利用する際は注意したほうが良いかもしれません。

入力可能なデータはHTMLMediaElementImageDataなどがあります。
MediaStream API でカメラやマイクなどのデバイスからデータストリームを取得しHTMLMediaElementに紐付けることで、リアルタイム検出が可能です。

利用可能なモデル

MobileNetやDarknetなどのニューラルネットワークモデルが使えます。
(ここら辺は付け焼き刃なので深い言及は避けておきます)

物体検出プログラム

早速、実際のプログラムを作ってみます。

物体検出には YOLO というアルゴリズムを使用し、映像はウェブカメラやスマホカメラから取得します。

HTML
<div class="overlay">
    <video id="capture" width="640" height="480"></video>
    <canvas id="detect" width="640" height="480"></canvas>
</div>
CSS
.overlay{
    position: relative;
}
.overlay > *{
    display: block;
    position: absolute;
}

まずHTML/CSSです。

いたって単純で、カメラから取得したリアルタイム映像を表示させる<video>と、検出結果をオーバーレイで表示させる<canvas>から成ります。

JavaScript
function getStream(video){
    return navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
            facingMode: "environment"
        }
    })
    .then((stream)=>{
        video.srcObject = stream;
        return video;
    });
}

最初にMediaStream APIを初期化します。

navigator.mediaDevices.getUserMedia()でデバイスのデータストリームを取得し、実際に表示させるHTMLMediaElementへ紐付けます。
初期化オプションは以下となります。

  • audio: 音声を拾うか否か
  • video: 映像関連の設定
  • facingMode: リアカメラorフロントカメラ

environmentがリアカメラでuserがフロントカメラとなります。

なお、設定値が無効だった場合は有効値を順々に探索していきますが、明示した設定値のみに限定したい場合はexactプロパティ経由で設定します。

JavaScript
function detectObject(video, canvas){
    const render = canvas.getContext("2d");

    render.beginPath();
    render.lineWidth = 2;
    render.strokeStyle = "#2fad09";

    render.font = "16px consolas";

    render.fillStyle = "#ffffff";
    render.fillRect(0, 0, canvas.width, canvas.height);
    render.fillStyle = "#000000";
    render.fillText("Model Loading...", 4, 14);
    render.fillStyle = "#2fad09";

    return ml5.YOLO({
        filterBoxesThreshold: 0.01,
        IOUThreshold: 0.2,
        classProbThreshold: 0.5
    }).ready
    .then((model)=>{
        render.clearRect(0, 0, canvas.width, canvas.height);
        video.play();

        return setInterval(()=>{
            if(!model.isPredicting){
                model.detect(video)
                .then((results)=>{
                    render.clearRect(0, 0, canvas.width, canvas.height);

                    for(const result of results){
                        render.strokeRect(result.x * canvas.width, result.y * canvas.height, result.w * canvas.width, result.h * canvas.height);
                        render.fillText(`${result.label}: ${Math.round(result.confidence * 100)} %`, result.x * canvas.width + 4, result.y * canvas.height + 14);
                    }
                });
            }
        }, 67);
    });
}

物体検出プログラム本体です。

ml5.YOLO().readyでYOLOモデルを取得しmodel.detect()で検出処理を実行します。
モデル取得時に閾値オプションを渡せます。

なお検出処理は入力された映像のスナップショットに対して実行されるので、物体をトラッキングするにはループさせる必要があります。
単純な実装であればsetInterval()でひたすら回せば良いのですが、検出処理はヘビータスクゆえ、もし前コンテキストが実行中のまま次ループへ入ってしまうと、リソースの奪い合いで時間経過とともにどんどん重くなってしまいます。

その対策として、モデルオブジェクトには "推論を実行中か" をBooleanで返すisPredictingというプロパティがあります。
これを使い、確実に処理が終了してから次のループに入るためのロック処理を入れました。
この例だと67ms毎(≒15fps)に検出処理中でないか確認してから実行しています。

結果は、検出された物体の "座標" と "ラベル" と "確率" が検出数の分だけ配列で返されます。

JavaScript
getStream(document.getElementById("capture"))
.then(video => detectObject(video, document.getElementById("detect")))
.catch(error => alert(error.message));

最終的にこのようなかたちで実行します。

完成

作ったサンプルはGitHubPagesに上げておきました。

Screenshot_20200115-155718_Chrome.jpg

この通り、スマホでも問題なく実行できます。
が、前述の通りヘビータスクなので結構重くなります。

おわりに

「機械学習で色々やってみたいけど、そもそも入口が複雑すぎてお手上げ...」
みたいな敷居の高さを取り払ってくれる、とても良いライブラリだと思いました。

もちろん、理解を深めるにはより低レベルで高度な知識が必要になると思いますが、そのきっかけ作りには丁度良いなと感じました。

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

初心者による JavaScriptで行うスタイルシートの操作

概要

javascriptを用いることで、DOM操作からスタイルシートを操作できます。DOMでスタイルシートを操作するには、以下のアプローチがあります。

  1. インラインスタイルにアクセスする(styleプロパティ)
  2. 外部のスタイルシートを適用する(classNameプロパティ)

styleプロパティ

javascriptでスタイルを操作するのに最もシンプルなのが、 インラインスタイルに対してアクセスする方法です。インラインスタイルとは、個々の要素に対して設定されたスタイルのことです。

sample.js
elem.style.prop[= value]
 //elem : 要素オブジェクト prop : スタイルプロパティ value : 設定値

たとえば、

sample.html
<div id="elem">マウスポインターを乗せると色が変わります</div>
sample.js
document.addEventListener('DOMContentLoaded', function() {
 let elem = document.getElementById('elem')

 //マウスポインターが乗ったタイミングで背景色を変更
 elem.addEventListener('mouseover', function() {
  this.style.backgroundColor = 'Yellow'
 }, false)

 //マウスポインターが外れたタイミングで背景色を戻す
 elem.addEventListener('mouseout', function() {
  this.style.backgroundColor = ''
 }, false)
}, false)

イベントリスナーの元では、thisはイベントの発生元を指している。
スタイルプロパティ名には注意が必要です。JavaScriptでは、「ハイフンを取り除いたうえで、2単語目以降の頭文字は大文字とする」必要があります。

background-color : backgroundColor
border-top-style : borderTopStyle

ただし、floatはstyleFloatとなる。

classNameプロパティ

styleプロパティは確かにシンプルだが、javascriptコードの中にCSSのコードが混在してしてしまうため、やはりCSSのコードは別にすることが望ましい。外部スタイルシートで定義されたスタイルにアクセスするのは、classNameプロパティの役割です。

sample.js
elem.className[= clazz]
 //elem : 要素オブジェクト clazz : スタイルクラス

たとえば、

sample.html
<link rel="stylesheet" href="css/style.css" />
//コード
<div id="elem">マウスポインターを乗せると色が変わります</div>
style.css
.highlight {
 background-color : Yellow;
}
sample.js
document.addEventListener('DOMContentLoaded', function() {
 let elem = document.getElementById('elem')
 //マウスポインターが乗ったタイミング
 elem.addEventListener('mouseover', function() {
  this.className = 'highlight'
 }, false)
 elem.addEventListener('mouseout', function() {
  this.className = ''
 }, false)
}, false)

このようにすることによって、編集がしやすくなります。また、classNameプロパティには複数のクラスを関連づけることもできます。その場合は、

sample.js
this.className = 'clazz1, clazz2'

スタイルクラスを外す

classNameプロパティを利用して、特定のスタイルクラスを着脱することができます。

sample.html
<link rel="stylesheet" href="css/style.css" />
//コード
<div id="elem">クリックすると背景色が変わります</div>
sample.js
document.addEventListener('DOMContentLoaded', function() {
 let elem = document.getElementById('elem')

 //クリックしたタイミングで背景色を変更
 elem.addEventListener('click', function() {
  this.className = (this.classNmae === 'highlight' ? '' : 'highlight')
 }, false)
}, false)

また、複数のスタイルクラスが適用されている場合は、

sample.html
<link rel="stylesheet" href="css/style.css" />
//コード
<div id="elem" class="line">クリックすると背景色が変わります</div>
css.style.css
.highlight {
 background-color: Yellow;
}

.line {
 border: 1px solid Red;
}
sample.js
document.addEventListener('DOMContentLoaded', function() {
 let elem = document.getElementById('elem')

 elem.addEventListener('click', function() {
  //空白区切りの文字列を分割
  let classes = this.className.splite(' ')
  //highlightが存在する位置を検出
  let index = classes.indexOf('highlight')
  if(index === -1){
   //存在しなければ、動的に追加
   classes.push('highlight')
  }else{
   //存在する場合には、highlightを除名
   classes.splice(index, 1)
  }
  //配列を空白入りの文字列に
  this.className = classes.join(' ')
 }, false)
}, false)

classListプロパティ

classListプロパティを利用することで、class属性の値をDOMTokenListオブジェクトとして取得する。classListでは、

length : リストの長さ
item(index) : インデックス番目のクラスを取得
contain(clazz) : 指定したクラスがあるかどうか
add(clazz) : リストにクラスを追加
remove(clazz) : リストのクラスを削除
toggle(clazz) : クラスのオン/オフを切り替え

toggleを用いることで、条件演算子が不要になる。コードが直感的になる。

参考資料

山田祥寛様 「javascript本格入門」

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

出来る限り何も考えずにVue.jsを扱う

概要

Vue.jsもすでにバージョンを重ねて、開発環境等々とり得る選択肢が増え、自分のような初学者(元々組込み系ソフトウェア設計者として何年か働いているので、初学者というには微妙ですが)は混乱してしまったため、備忘録。

初学者ほど、意外にツールや設計の重要性についてはあまり意識していないようなので、そういった観点で本記事は作成しています。

 前提知識は以下の通り。

  • Node.js
  • Bootstrap
  • なんか適当にWebの知識がある
  • ターミナルでコマンド

Vue.jsとは

Webフレームワークの一つ。基本的には中規模~大規模向けと言われています。この規模ベースの表現は分かりにくいですが、個人的には以下のような機能の有無が関係しているものと思います。

  • 小規模:DOM操作ができる
  • 中規模:グローバル変数を格納できる機能がある
  • 大規模:クライアント側(Webブラウザ側)にてルーティング機能がある

開発環境

 あらかじめ「node.js」が導入されている前提で記述します。ここでは、ほとんど「npm」を用いたパッケージ管理ソフトとして使用しています。パッケージ管理ソフトとは、コマンド一発でパッケージ(ライブラリ)をインストールできるものと考えておいてもらえればいいと思います。

余談ですが、私は「node.js」はバージョン管理ソフト「nodist」から入れています。「node.js」はバージョンアップが早く、またweb系は何だかんだ様々な環境を想定するだろうなと思い、導入しています。

今回は「vue/cli」を採用します。「nuxt.js」を採用している人もいますが、現行の「vue/cli」であれば簡単なアプリケーションを開発分には事足りそうだったので。コマンド一発で「router」と「vuex」を使えますし。

言語も基本は「Javascript」にします。オブジェクト指向的に書きたいと思い、typescriptの採用も検討しましたが、しっかりコンポーネント毎に作ることとしていれば良さそう。静的型付けしたい感もありますが、そこまで大規模化しなければ神経質になる必要もないかなと思い、「typescript」の採用は見送りました。単純にそのあたりの勉強がメンドイということもありますが。

また、「bootstrap」も利用します。デザインセンスが壊滅的にないので、付け焼き刃ですが使用します。

Bootstrapとは?

wikipedia的には「Webアプリケーションフレームワーク」。「見た目」を「それっぽく」「レスポンシブ」にするフレームワークです。ここでいう「レスポンシブ」とは画面の大きさに合わせて見た目を変えられるという意味です。

エディタ(VScode)

Javascriptを使用する上でよく使用されているのは「VScode」だと思います。特別宗教上の理由がない限りはVScodeを使用すると良いと思います。

ブラウザ(Google Chrome)

これは私の場合ですが、私は「Google Chrome」を使用しています。一応ChromeのプラグインとしてVue.js用のデバッグツールがあります。ここでは、このツールの使い方を説明しませんが、一応入れとくといいのかもしれません。

「vue/cli」の導入/プロジェクトの作成

コンソールにて、以下を入力。そうすると、npm環境におけるグローバル領域において「vue/cli」がインストールされます。

npm install -g @vue/cli

導入出来たら、何も考えずに次のコマンド。

vue create myProject
※「myProject」は任意の名前

このコマンドは「vue/cli」におけるプロジェクト作成のコマンドです。このコマンドを打つと以下のようになります。

Please puck preset:(Use arrow keys)
 default(babel,eslint)
>Manually select features

defeultでもいいですが、自分はManuallyでプロジェクトを作成します。その後、自分は最低限以下の用のような項目にチェックを入れてプロジェクトを作成します。あとは適当にEnterを押します。

?Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i
> to invert selection)
>(*) Babel
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support
 (*) Router
 (*) Vuex
 ( ) CSS Pre-processors
 (*) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing

Bootstrap-vueのインストール

Bootstrap-vueは、そのBootstrapをVue.jsをベースにして使用するためのライブラリです。インストールの仕方は簡単で、以下のコマンドを押下すると良いです。

cd myProject
npm install bootstrap-vue

「cd myProject」でプロジェクトフォルダに移動後、インストールします。

Vus.jsによるプロジェクトのファイル構成

プロジェクトを作成すると、自動に様々なファイルが作成されます。私はレガシーな組込み系ばかり関わっていたので、作成されるファイル数に面食らいました。主なファイルの構成は以下の通りです。「:」以下は自分の何となくのイメージ。

myProject
 ├─node_modules:npmコマンドで入れたライブラリの格納する場所。ほとんどいじらない
 ├─public
 │  └─index.html:ほとんど何もいじらないhtmlファイル。いじってもCDNのリンクを張るくらい
 └─src
    ├─assets:静的ファイルリソース
    ├─components:単一コンポーネントファイルを格納する場所
    ├─router
   │  └─index.js:ルーティング設定ファイル
    ├─store
  │  └─index.js:グローバル変数を格納するところ
    ├─views:ページレイアウトのためのコンポーネント
    ├─App.vue:最上位コンポーネント
    └─main.js:ライブラリとかの設定するファイル

ユーザは基本的に「src」以下のファイルしか編集しません。Vue.jsでは、基本的に「コンポーネント」ごとにまとめやすいようにしているように思えます。このあたりは最近のwebフレームワークでは共通した特徴だと思いますが、他のフレームワークよりは意識するファイルが少ない印象を受けます。似たフレームワークであるところの「Anguler」の方が、プラグイン的なフレームワークが整っているように思いますが、初学者としてはBootstrap+Vue.jsの方が理解が早いし、業務上の応用が利きそうな雰囲気があります。

コンポーネントの作成

 コンポーネントとは、ソフトウェアを構成する部品である。ただし、ここでいうコンポーネントとは「コンポーネント指向におけるコンポーネント」ということに注意すること。具体的には以下の4つ。

  • 構造
  • スタイル
  • 状態(≒データ)
  • 機能

 思想としては「オブジェクト指向」もあるが、こちらが指向するのは「データ」と「機能」だけど、「コンポーネント指向」ではそれをもう少し大きくしたイメージである。

単一コンポーネントファイル

 上記のコンポーネントを作成するため、vue.jsでは拡張子「.vue」にて単一コンポーネントファイルを作成する。

 単一コンポーネントファイルは以下のような要素で構成される。個人的な見方だが、コンポーネント指向としては以下のような対応付け。

  • template:構造
  • script:状態、機能
  • style:スタイル
sample.vue
<template>
  <div class="com">
  </div>
</template>

<script>
  default export {
    name:'com',
    data(){
      return {
        dt1: "",
        dt2: ""
      }
    },
    components:{}
  }
</script>

<style>
</style>

ページ作成

 新しいページを作成するときは「view」ディレクトリを編集します。基本的な構成は単一コンポーネントファイルと同様である。

 違いは、基本的にここでは、単一コンポーネントファイルの組み合わせで記述し、データの保持は基本的にはこっちで行う(ここで基本的にと言っているのは、ページ間で共有する場合は「vuex」にてデータの保持を行うが、ここでは詳しく記述しない)。

ルーティング(router)

 基本的に「router」ディレクトリのindex.jsしか編集しません。
 念頭に置くのは以下のふたつです。

  • ルーティングの設定(これは「router」のindex.js)
  • 設定したパスに移動するための設定

「routes」変数の中を以下のような感じに定義します。
「views」フォルダの中に定義したコンポーネントファイルを呼び出すように設定します。

/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
// viewsフォルダにあるコンポーネントファイルを呼び出し
import Auth from '../views/Auth.vue'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',// パスの設定
    name: 'auth',// 名前
    component: Auth// 呼び出したコンポーネントファイルを設定
  },
  {
    path: '/home',// パスの設定
    name: 'home',// 名前
    component: Home// 呼び出したコンポーネントファイルを設定
  }
]

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

export default router

グローバル変数の管理(vuex)

 ここでは「vuex」について説明します。正直個人的にこれが一番面倒だった。可能であれば、ページ間でのデータの共有のない設計をすべきだと考えます。
 「vuex」はページ間にてデータを共有するための機能です。

変数/ゲッター関数/セッター関数の定義

「store」フォルダにあるindex.jsに定義します。以下に例を示します。

  • state:変数の定義
  • getters:ゲッター関数(値の取得)の定義
  • mutations:セッター関数(値の変更)の定義
/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        // 変数の定義
        user: "",
        password: ""
    },
    getters:{
        // ゲッター関数の定義
        user(state){return state.user},
        password(state){return state.password}

    },
    mutations: {
        // セッター関数の定義
        setUser(state, payload){state.user = payload},
        setPassword(state, payload){state.password = payload}
    },
    actions: {
    },
    modules: {
    }
})

関数の呼び出し

this.$store.commit("setUser", this.user);

手順

最後に簡単に手順を示します。

  • プロジェクトを作成
  • 「views」に作成したいページ分コンポーネントファイルを用意
  • 「router」のindex.jsに「views」に用意したページ分パスを設定
  • 「store」のindex.jsにページ間をまたいで使用する変数を設定
  • あとはガツガツコンポーネントファイルを編集する

最後に

ここでは、Vue.jsについて簡単にではありますが、説明しました。詳細な説明は何かしらの参考書を見ることをお勧めします。以下は私が買った参考書です。

参考

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

Javascriptでもぐら叩きゲーム

開発したもの

もぐら叩きゲーム(Webアプリケーション)
http://yusuke97.html.xdomain.jp/index.html

きっかけ

javascriptを触れる機会があり、勉強していくうちに楽しくなっていき何か形にしたかったこと。

こだわったところ

  • スマホでもPCでも遊べるようにする
  • 誤ってズームしたり,画像を選択しないようにする
  • レスポンスを速くする
  • シンプルなUI

使ったもの

  • javascript
  • CSS
  • html
  • Cent OS
  • Apache HTTP Server(ローカル開発環境)
  • XFREEのレンタルサーバー

開発の流れ(1人, 2週間)

  1. 使う写真をダウンロード、加工
  2. もぐらが動くようにする(1匹だけ)
  3. 10匹のもぐらが動くくようにする
  4. 配置を変える。
  5. もぐらを叩いてから反応する時間を短くする。
  6. 初級と上級に分ける。

難しかったこと

  • javascriptの非同期通信
  • もぐらを叩いてから反応する時間を短くする。
  • スマホでもPCでも遊べるようにすること。
  • ブラウザによって挙動が違うところ。

感想

コードによって全然レスポンスの速さが違ったりブラウザによってもぐらの挙動、位置、bgmが違っていたので、難しかった。しかし、javascript,CSSが意外と深いことがわかり面白かった。もっと速いWebアプリケーションを開発するためにもっと深く知りたいと思った。

最後に

間違いがあればコメントくださるとありがたいです。

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

Typescriptチュートリアル

Typescriptを始めたいけどどこから初めて良いかわからないという方の、TypescriptのWebサイトに掲載されていた同名のページを少しだけアレンジして紹介します。

環境の準備

まず、node.jsがPCにインストールされている必要がある。インストールされていない場合、下記のページからLTSの最新版をダウンロードしてインストールします。
https://nodejs.org/en/

Typescriptのインストール

下記のコマンドを実行して、typescriptのライブラリをダウンロードしてきます。会社の環境などで、プロキシサーバが間にある方は、プロキシありの方のコマンドを実行してください。

プロキシなし
npm install -g typescript
プロキシあり
npm install -g typescript

最初のTypescriptファイルの作成

まずテキストエディタを開いて、下記のJavascriptコードをgreeter.tsというファイルに保存してください。

greeter.ts
function greeter(person) {
    return "Hello, " + person;
}

let user = "Jane User";

document.body.innerHTML = greeter(user);

Typescriptコードのコンパイル

次にコマンドプロンプトを開き、さっき作ったgreeter.tsが保存されているパスに移動してから下記のコマンドを実行します。

コマンド
tsc greeter.ts

実行すると、同じコードが書かれたgreeter.jsというファイルが作成されます。

次に、Typescriptが提供している機能を使ってコードを改良します。

greeterの引数にstringの型定義を追加しましょう。

function greeter(person: string) {
    return "Hello, " + person;
}

let user = "Jane User";

document.body.innerHTML = greeter(user);
Type annotations

上記のコードによって、javascriptにはない型定義の機能を追加することができました。試しに、変数userに数値を入れて、greeterメソッドを呼んでみます。

function greeter(person: string) {
    return "Hello, " + person;
}

let user = 12345;

document.body.innerHTML = greeter(user);

結果はこの通り。エラーが出力されました。

error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

この場合でもgreeter.jsは作成され、使うこともできますが期待通りに動作しない可能性があることをTypescriptが警告を出します。

typeアノテーション

インタフェース

今度は、firstNameとlastNameを持っているオブジェクトを表現するインターフェースを作成します。Typescriptでは、

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person: Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

let user = { firstName: "Jane", lastName: "User" };

document.body.innerHTML = greeter(user);
Classes

クラス

class Student {
    fullName: string;
    constructor(public firstName: string, public middleInitial: string, public lastName: string) {
        this.fullName = firstName + " " + middleInitial + " " + lastName;
    }
}

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person : Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

let user = new Student("Jane", "M.", "User");

document.body.innerHTML = greeter(user);

TypescriptWebAppの起動

<!DOCTYPE html>
<html>
    <head><title>TypeScript Greeter</title></head>
    <body>
        <script src="greeter.js"></script>
    </body>
</html>

デバッグ

クロームで実行したJavascriptを実行し、デバッグをしてみましょう。
コードを止めると、javascriptのコードではなく、typescriptが表示されます。

まとめ

これにより、typescriptの開発環境ができました・
typescriptはjavascriptと異なりコンパイルというプロセスが入るため、実行しなくても記述みすや型の間違いを見つけてくれます。
記述量は多少増えますが、規模が大きくなればなるほど、開発を助けてくれるはずです。

参考

https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html

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

createJSのテキストとスペースについて

概要

createJSのテキストにて

var textObj = _lib.text;
var sentence = "テスト用の文章";
textObj.text = sentence;

上記のように、テキストを表示させる場合に、
半角スペース、全角スペースを入れると
文章の途中で勝手に改行されてしまうことがある
上記の解決方法について記載する

原因

drawTextの仕様

テキストへの文字表示はcreatejs内のdrawText関数で行われている

drawText (createJS)
p._drawText = function(ctx, o, lines) {
        var paint = !!ctx;
        if (!paint) {
            ctx = Text._workingContext;
            ctx.save();
            this._prepContext(ctx);
        }
        var lineHeight = this.lineHeight||this.getMeasuredLineHeight();

        var maxW = 0, count = 0;
        var hardLines = String(this.text).split(/(?:\r\n|\r|\n)/);
        for (var i=0, l=hardLines.length; i<l; i++) {
            var str = hardLines[i];
            var w = null;

            if (this.lineWidth != null && (w = ctx.measureText(str).width) > this.lineWidth) {
                // text wrapping:
                var words = str.split(/(\s)/);
                str = words[0];
                w = ctx.measureText(str).width;

                for (var j=1, jl=words.length; j<jl; j+=2) {
                    // Line needs to wrap:
                    var wordW = ctx.measureText(words[j] + words[j+1]).width;
                    if (w + wordW > this.lineWidth) {
                        if (paint) { this._drawTextLine(ctx, str, count*lineHeight); }
                        if (lines) { lines.push(str); }
                        if (w > maxW) { maxW = w; }
                        str = words[j+1];
                        w = ctx.measureText(str).width;
                        count++;
                    } else {
                        str += words[j] + words[j+1];
                        w += wordW;
                    }
                }
            }

            if (paint) { this._drawTextLine(ctx, str, count*lineHeight); }
            if (lines) { lines.push(str); }
            if (o && w == null) { w = ctx.measureText(str).width; }
            if (w > maxW) { maxW = w; }
            count++;
        }

        if (o) {
            o.width = maxW;
            o.height = count*lineHeight;
        }
        if (!paint) { ctx.restore(); }
        return o;
    };

上記コードの中の
テキストオブジェクトの幅(lineWidth)と文字の長さを比較して

w = ctx.measureText(str).width; //今描画している文字の長さ
var wordW = ctx.measureText(words[j] + words[j+1]).width; //次に描画する単語と次の次に描画する単語を合わせた長さ
w + wordW > this.lineWidth //2つ先の単語まで描画したときにテキストオブジェクトの幅を超えてしまうかどうか

いまから描画しようとしている単語の次の単語を描画した場合に、テキストオブジェクトの横幅を超えてしまうようであれば今回の描画後に改行をする仕様になっている

if (w + wordW > this.lineWidth) {
    this._drawTextLine(ctx, str, count*lineHeight); //改行は、テキストを描画する高さを変更する形で実装されている?
    count++; //超えてしまったら、改行する
}

単語の定義

createJSでは単語を区切るのにスペースが使用されている

var words = str.split(/(\s)/);
str = words[i];

対策

対策としては主に2つある
・表示する文章データの中にはスペース(\s)は入れないというルールを決める
・テキストオブジェクトの横幅を長めに設定しておく
基本的には、文章中にスペースは使いたくなることが多いと思うので、
テキストオブジェクトの横幅を大きめにとっておく対策がいいかと思われる。

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

TypeScriptでobjectの値の参照渡しでコケた時の回避方法

何をしようとしたか

特定の項目が同じ値のままUserを使いまわそうとした。
今回の例だと User.age

entities/User.ts
export class User {
  name: string;
  age: number;
}
typescript/main.ts
import { User } from "./entities/User";

export const main = () => {
  let users: User[] = [];

  let user: User = { name: "alpha", age: 20 };
  users.push(user);

  // nameのみ変更
  user.name = "beta";
  users.push(user);

  console.log({ users });
};

main();

求めていた結果

{ users: [ { name: 'alpha', age: 20 }, { name: 'beta', age: 20 } ] }

実際に出てきた結果

{ users: [ { name: 'beta', age: 20 }, { name: 'beta', age: 20 } ] }

betaくんが2人になってる…

回避手段

調べたらjavascriptはobjectの場合、値の参照渡しを行うらしい

なら毎回新しいobjectを生成すればいいのかと思った

丁度nestjsを使用していたので、class-transformer/plainToClassを使用することにした

npm i class-transformer
main.ts
import { plainToClass } from "class-transformer";
import { User } from "./entities/User";

export const main = () => {
  let users: User[] = [];
  let user: User = { name: "alpha", age: 20 };
  users.push(plainToClass(User, user));
  user.name = "beta";
  users.push(plainToClass(User, user));

  console.log({ users });
};

main();

実行結果

{  users: [ User { name: 'alpha', age: 20 }, User { name: 'beta', age: 20 } ]}

求めている形で返ってきた

感想

objectの取り扱いには気をつけよう
そもそも使いまわしたい部分に対して処理をちゃんと書けばこうならなかったはず
とりあえず動かしたい精神で書いてたら死んだ

参考

[Qiita]JavaScriptに参照渡し/値渡しなど存在しない

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

アプリにLINE Pay決済を導入する簡易手引き(LINE Pay API - v3)

株式会社OKANで「おかんPay」の開発(フロント/バックエンド)に携わっているsuzu_Dと申します。
このアプリを簡単に説明すると、「オフィスおかん」という置型の社食サービスで、小銭を使わずキャッシュレス決済ができるアプリとなっております。
今まで当アプリではクレジットカードを登録し支払う、という方法でのみの決済でしたが、
2019年12月の目玉アップデートとして、当アプリ経由でLINE Pay決済ができる機能をリリースしました。

今回は、技術的なお話というよりLINE Payを組み込む際に、今現在ネットの情報だけだと自分的には不足している部分があり
「そもそもLINE Pay APIに通信できない・・・」といった事が多発し、LINE Payの技術スタッフの方と何回かやり取りさせていただいたので、
その中で改めて知った、LINE Payの知見を、自分なりに纏めてアウトプットし、
もし皆さんが関わっているプロダクトで「LINE Pay決済」を導入する機会がありましたら
少しでも参考になればなと思い、本記事を寄稿しました。

本文献の購読対象者と、目的・出来るようになること

購読対象者

・開発中のネイティブアプリやWEBアプリでLINE Pay決済を組み込もうとしている方
・実際にいまLINE Payを組込中で「そもそもLINE Pay APIと通信がうまくいかない!」と嘆いている方

目的・出来るようになること

・自プロダクトでLINE Payで決済ができる仕組みが、最低限なんとなく理解できて、LINE Pay APIとREST通信出来るようになる。

LINE Pay APIのバージョンと参考ガイドについて

当記事で対象としているLINE Pay APIのバージョン

当記事で対象としているLINE Pay APIのバージョンは
v3(2019/12 時点) 
の情報になります、v3以前のバージョンや、これ以降APIのバージョンが上がった際
参考にならない記述が出る可能性があることを、ご了承願います。

当記事で参考にしているLINE Pay API ガイド

https://pay.line.me/jp/developers/apis/onlineApis?locale=ja_JP

LINE Pay APIの環境について

API通信に使う環境として、主に使うのは以下2つになると思います。

  • 本番環境
  • テスト加盟店環境

API通信に使う環境について

本番環境

加盟店申込後に審査が完了してから利用可能になる環境です。
実際のお金のやりとりをする環境になります。
加盟店申込後にLINE Pay側から連絡があり、申請したアカウント(メールアドレス)にて
LINE PayのMy Pageからログインを行うと、取引の履歴を見たり、決済のキャンセル・返金を行うことが出来ます。
また、本番のAPI通信に必要なキーの情報を見ることが出来ます。

テスト加盟店環境

Sandbox生成後利用可能になる環境です。
このページにて登録したメールアドレス宛に、テスト加盟店環境にログインできるアカウントが通知されます。
通知されたアカウントでLINE PayのMy Pageからログインを行うと、テスト加盟店環境で行った取引の履歴を見たり、決済のキャンセル・返金を行うことが出来ます。また、テスト加盟店環境にてAPI通信をするために必要なキーの情報を見ることが出来ます。
こちらも、本番環境同様、決済処理の通信を行った場合、実際のお金のやり取りが発生する(LINE Pay残高が減る)のですが、毎日23:55と2:55に支払いを行った決済に対して自動的に決済のキャンセル・返金処理が行われます。
アプリに決済機能を実装し、LINE Pay決済をテスト的につかう場合はこちらの環境を使うことをおすすめします。

各環境にAPI通信するために必要なエンドポイントとキーについて

API通信する際のエンドポイント

エンドポイント(本番、テスト加盟店環境 共通)
https://api-pay.line.me
例 - Request APIをコールする場合のエンドポイント
https://api-pay.line.me/v3/payments/request

各環境にAPI通信するためのキー、Channel IDとChannel SecretKeyについて

LINE Pay API通信する際、必要な認証情報としてChannel IDとChannel SecretKeyを使用することになります。
このキーが本番環境、もしくはテスト加盟店環境の違いになるので、本番環境用とテスト用で接続先の設定ファイルを作る際は間違えないようにしてください(テスト用の設定ファイルに本番用のChannel IDとChannel SecretKeyを登録してしまった等の間違いをしないようにしてください。)
確認方法は以下になります。

①本番環境、もしくはテスト加盟店環境のアカウントにてLINE PayのMy Pageからログインを行う

②ログイン後画面にて「決済連動管理」>「連動キー管理」と進み、ログイン用のパスワードを入力する
スクリーンショット 2019-12-23 14.27.41.png (168.7 kB)

③Channel ID とChannel Secret Keyを確認することが出来る(このChannel ID とChannel Secret Keyは、絶対外部に漏らさないようにしてください。)
スクリーンショット 2019-12-23 14.57.13.png (605.5 kB)

このChannel IDとChannel SecretKeyによって、どの加盟店での決済かを判別しているようです、APIのリクエストを投げる際、共通ヘッダーにキーを設定することによって、通信が可能となります、このキーの設定が間違っている場合にhttps://api-pay.line.meに通信を行うと以下のようなレスポンスが返ってきます、

{
    "returnCode": "1106",
    "returnMessage": "Header information error. request verification Failed"
}

このキーを共通ヘッダーに設定する際、少々特別な処理が必要になります、詳しい説明は後述する共通ヘッダーの設定方法を参照してください。

※SandBoxについて

本番環境、テスト加盟店環境 の双方どちらも「SandBox」で通信できる環境があります。
スクリーンショット 2019-10-30 11.40.56.png (190.5 kB)
こちらの環境は以下のエンドポイントで通信が行えます

https://sandbox-api-pay.line.me

テスト的に使うのであれば、この環境を使いたいところですが、
SandBox環境だと決済の流れが、実際アプリではなくエミュレータのようなもので動き、
本物の決済の流れが体感的に分かりにくいのと、お試しでREST通信をしたい場合(Postman等を使用して疎通確認したい場合)
何らかの原因でエラーがでてしまい、通信が成功しないなどあるので、テスト的に使う場合はhttps://sandbox-api-pay.line.meのエンドポイントは使わず、少額の決済であれば、実際のお金を幾らかLINE Payに入金しておき、テスト加盟店環境https://api-pay.line.meをコールするようにした方がよいと考えます。
(PostmanがSandbox環境にて使えない原因をLINE Payの技術スタッフの方に問い合わせしたところ、通信時に特殊なパラメータが付与されている為のことだそうです。)

環境まとめ

本番環境 テスト加盟店環境 Sandbox(本番・テスト加盟店環境)
利用制限 加盟店に申込後、審査完了後利用可能 なし、無料利用可能 なし、無料利用可能(テスト加盟店環境の場合)
利用料 加盟店契約により変動 無料 無料 (テスト加盟店環境の場合)
利用方法 加盟店申込をする Sandbox環境を作成する(ややこしいが、この環境がテスト加盟店環境という)
接続先 https://api-pay.line.me https://api-pay.line.me https://sandbox-api-pay.line.me
決済処理時の動き LINEアカウントの残高を利用 LINEアカウントの残高を利用 Sandboxの架空残高を利用(この利用に関しては実際に試していないのでどんな感じで決済されるかについてはよく分かっていない)
返金方法 MyPageから手動 MyPageから手動 or 毎日23:55と2:55に自動的に返金 架空残高を使うので特に無し

LINE Pay APIにPOST通信する為の共通ヘッダーについて

共通ヘッダーについて

共通ヘッダーに設定する値

リクエストを送る時の、最低限の必要な共通ヘッダーが下記になります。

キー データの型 設定する値
Content-Type String 'application/json'
X-LINE-ChannelId String LINE PayのMy Pageで確認したChannel ID
X-LINE-Authorization-Nonce String 1回限りのランダムな値を設定する、UUIDを生成したり、現在日時のミリ秒等を生成して設定する。
X-LINE-Authorization String ※後述

X-LINE-Authorization-Nonceに設定する値としては、一度切りに使う値を生成し使ってください、(例えばJavaScriptでヘッダーを実装する場合var nonce = (new Date()).getTime();と現在のミリ秒を生成しnonceの値に設定する、といった実装です。)
X-LINE-Authorizationに関しては少々ややこしいので、次項に詳細を書きます。

X-LINE-Authorization に設定する値について

X-LINE-Authorizationに設定する値はガイドによるとこう記述されています。

HTTP Method : GET

Signature = Base64(HMAC-SHA256(Your ChannelSecret, (Your ChannelSecret + URL Path + Query String + nonce))) Query String : ?を除いたクエリ文字列(例 : Name1=Value1&Name2=Value2...)

HTTP Method : POST

Signature = Base64(HMAC-SHA256(Your ChannelSecret, (Your ChannelSecret + URL Path + RequestBody + nonce)))

今回は「POST」の時のX-LINE-Authorizationについて説明します。
POST時にX-LINE-Authorizationを設定するために、以下の4つの値が必要です。
①Your ChannelSecret - LINE PayのMy Pageで確認したChannel SecretKey
②URL Path - POSTするURLのパス(例えば https://api-pay.line.me/v3/payments/request 等)
③RequestBody - POSTする時のRequest Body
④nonce - X-LINE-Authorization-Nonceに設定した値
そして上記の値を文字列連結し、ハッシュ メッセージ認証コード (HMAC) で計算します。
(HMACとは?)
この計算に使う、秘密鍵とメッセージ(データ)とハッシュ関数は以下になります
秘密鍵: LINE PayのMy Pageで確認したChannel SecretKey
メッセージ(データ):上記①〜④を文字列連結した値
ハッシュ関数:SHA256

そして、ハッシュ化した値をBase64 でエンコードし、
エンコードした値をX-LINE-Authorizationに設定します。

この処理をJAVAで書いた例はガイドに記載されてあります。
JavaScriptで書く場合は以下のようになるかと思います。

var crypto = require('crypto-js');// crypto-jsという暗号化、復号化のライブラリを使う

var channelSecret = '上記①の値';
var path = '上記②の値';
var body = '上記③の値';
var nonce = (new Date()).getTime(); // nonceに使うランダムな値の生成をする

var message = channelSecret + path + body + nonce ;
var hash = crypto.HmacSHA256(message,channelSecret); // channelSecretの値をキーにSHA256ハッシュ関数を使用してハッシュ (HMAC) を計算
var hashHeader = CryptoJS.enc.Base64.stringify(hash); // ここの値がX-LINE-Authorizationに設定する値になる

また、今回自分が実際実装した言語はrubyを使ったので
rubyで実装する場合は下記のように①〜④の値を受けとって処理するようなメソッドを実装すればよいと思います。

    def authorization(channel_secret, params, url, nonce)
      message = "#{channel_secret}#{url}#{params.to_json}#{nonce}"
      hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), channel_secret, message)
      Base64.strict_encode64(hash)
    end

ヘッダーの実装例(ruby)

rubyでヘッダー部分を実装するとしたら、以下のようになります。

    def headers(params, url)
      # ヘッダーを作る処理をする時にPOSTするURLのパスとPOSTする時のRequest Bodyを渡す
      nonce = authorization_nonce
      {
        'Content-Type' => 'application/json',
        'X-LINE-ChannelId' => channel_id,
        'X-LINE-Authorization-Nonce' => nonce,
        'X-LINE-Authorization' => authorization(params, url, nonce),
      }
    end

    def channel_id
      Rails.application.credentials.dig(# ここはcredentials.yml.encに設定した値を取り出す).to_s
    end

    def channel_secret
      Rails.application.credentials.dig(# ここはcredentials.yml.encに設定した値を取り出す).to_s
    end

    def authorization_nonce
      Time.zone.now.to_s(:db_jst)
    end

    def authorization(params, url, nonce)
      message = "#{channel_secret}#{url}#{params.to_json}#{nonce}"
      hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), channel_secret, message)
      Base64.strict_encode64(hash)
    end

決済するためのAPIについて

「決済」を行うためのAPI

商品の決済をする為に必要最低限のAPIは以下の2つのみです。
・Request API (POST /v3/payments/request)
https://pay.line.me/documents/online_v3_ja.html#request-api

・Confirm API (POST /v3/payments/{transactionId}/confirm)
https://pay.line.me/documents/online_v3_ja.html#confirm-api

その他決済取り消し等のAPIありますが、本記事では取り上げません。
また、上記のAPIを毎回コールし決済するパターンを「一般決済」と言い、
上記APIを初回のみコールし、以降上記APIをコールしなくても決済が出来るパターンを「自動決済」と言うのですが
今回は一般決済のみの事を記述します。

決済の処理をアプリに組み込む際のざっくりな流れ(一例)

①Request API (POST /v3/payments/request)をコールする

②成功時のレスポンスにLINE Pay取引番号(transactionId)と
 決済用のURL(info.paymentUrl.web 又は info.paymentUrl.web.app) が返ってくる

③アプリまたはWEB上で ②のレスポンス内にある決済用のURL(info.paymentUrl.web 又は info.paymentUrl.app)
 に遷移する処理をする。

④(LINEが端末にインストールされている状態でinfo.paymentUrl.appのURLに遷移した場合)
  LINEアプリが立ち上がり LINE Payの支払画面に遷移するので、LINEアプリ内で決済の操作を行う

⑤決済が完了すると ①のRequest APIをコール時に設定した画面に遷移する(設定の仕方は後述)

⑥ ⑤にて遷移した瞬間にConfirm API (POST /v3/payments/{transactionId}/confirm)をコールする処理をする、
 {transactionId} には②のレスポンスに含まれていたLINE Pay取引番号(transactionId)を設定する。
 設定例: /v3/payments/2019010100000000000/confirm

⑦決済が完了する。

Request APIについて

Request API のざっくりとした説明

「こんな商品を買うから、購入するために整理券ちょうだい」的なAPIです。
「商品の値段は○○で、個数が○○で〜」といった情報をRequest Bodyに詰め込んで送ると
「整理券あげるよ、整理番号(LINE Pay取引番号)はこれだよ、この場所(決済用のURL)で買ってね」
というようなレスポンスが返ってきます。
返ってきた、整理番号(LINE Pay取引番号)は次にコールするConfirm APIで使う事になるので
どこかに保持しておく必要があります。

Request API のRequest Bodyの例とレスポンス

実際通信する時のRequest Bodyの例は以下のようになっています。
必要最低限のことだけ記述してあるので、「商品の送料、配送先」等、詳細にせってしたい場合は公式ドキュメントを参照してください。
Request Bodyの例-コピペ用

{
    "amount": 300,
    "currency": "JPY",
    "orderId": "20190101",
    "packages": [
        {
            "id": "1",
            "amount": 300,
            "name": "LINE Pay商品",
            "products": [
                {
                    "name": "豚の角煮(赤みそ)",
                    "imageUrl": "https://exmple.jpg",
                    "quantity": 1,
                    "price": 100
                },
                {
                    "name": "れんこんサラダ", 
                    "imageUrl": "https://exmple.jpg", 
                    "quantity": 2, 
                    "price": 100
                }
            ]
        }
    ],
    "redirectUrls": {
        "confirmUrl": "myapp://confirm",
        "cancelUrl": "myapp://cancel"
    }
}

Request Bodyの例-各パラメータの簡単な説明

{
    "amount": 300, // 合計金額を設定、この金額はpackages[].amountの合計金額と一致していないとエラーになる。
    "currency": "JPY", // 決済通貨の種類 日本であれば"JPY"で問題ないかと
    "orderId": "20190101", // 実装者側で発行するユニークなID、特に決まっていなければ、ランダムなUUIDを生成したり、現在日付のTIME_STAMP等設定すれば良い
    "packages": [
        {
            "id": "1", // 特に決まっていなくても設定する必要あり
            "amount": 300, // products[].priceの合計金額、一致してないとエラーになる
            "name": "LINE Pay商品", // 購入する商品の全体的な名前、特に決まっていなければ、商品と同じ名前でよいと思う。
            "products": [
                {
                    "name": "豚の角煮(赤みそ)", // 商品の名前 ここの値がLINE Payの決済画面で表示される。
                    "imageUrl": "https://linepay.exmple.image.jpg", // 商品の画像のサムネイル LINE Payの決済画面で表示される。
                    "quantity": 1, // 商品の個数
                    "price": 100 // 商品の値段
                },
                {
                    "name": "れんこんサラダ", 
                    "imageUrl": "https://linepay.exmple.image.jpg", 
                    "quantity": 2, 
                    "price": 100
                }
            ]
        }
    ],
    "redirectUrls": {
        "confirmUrl": "myapp://confirm", // LINE Payで商品決済したときに遷移するURL、遷移させたいのがWEBページならhttp://~ ネイティブアプリならアプリのDeepLinkを設定する
        "cancelUrl": "myapp://cancel" // LINE Payで商品決済をキャンセルしたときに遷移するURL
    }
}

※"imageUrl" に設定している値は一例なので、実際にサムネイルとして何か画像を表示してみたい場合は、実在するURLを入れてください、若しくは設定しないでください。

上記のリクエストを飛ばし、成功すると以下のようなレスポンスが返ってきます(~の部分は可変します)

{
    "returnCode": "0000",
    "returnMessage": "Success.",
    "info": {
        "paymentUrl": {
            "web": "https://web-pay.line.me/web/payment/wait?~~~~~",
            "app": "line://pay/payment/~~~~~"
        },
        "transactionId": ~~~~~~~~,
        "paymentAccessToken": "~~~~~~~~"
    }
}

端末にLINEがインストールされており、LINE Payが使える設定にしている場合に、上記のレスポンスの「info.paymentUrl.app」に遷移すると以下のような画面に遷移します。
Screenshot_20191031_124159_jp.naver.line.android.jpg (22.7 kB)
※上記の画像にサムネイルが表示されていますがRequest BodyのimageUrlで設定した「linepay.exmple.image.jpg」とは異なる画像を使っています。「linepay.exmple.image.jpg」というURLはあくまで一例であって実際には存在しませんのでご了承ください。
※上記の画像で決済やキャンセルを行うとRequest Bodyで設定したconfirmUrlやcancelUrlに遷移します。

Confirm APIのRequest Bodyの例とレスポンス

Confirm API のざっくりとした説明

「さっき教えてもらった場所で決済したよ、承認してね。」的なAPIです。
LINE Payのアプリで決済処理をせずにコールするとエラーになるので注意が必要です。

Request Bodyの例-コピペ用

{
    "amount": 300,
    "currency": "JPY"
}

Request Bodyの例-各パラメータの簡単な説明

{
    "amount": 300, //商品の合計金額 Request APIの時と違う金額だとエラーになるので注意
    "currency": "JPY" // 決済通貨の種類
}

上記のリクエストを飛ばし、成功すると以下のようなレスポンスが返ってきます(~の部分は可変します)

{
    "returnCode": "0000",
    "returnMessage": "Success.",
    "info": {
        "transactionId": ~~~~~~~~,
        "orderId": "20190101",
        "payInfo": [
            {
                "method": "BALANCE",
                "amount": 300
            }
        ],
        "packages": [
            {
                "id": "1",
                "amount": 300,
                "name": "LINE Pay商品",
                "products": [
                    {
                        "name": "豚の角煮(赤みそ)",
                        "imageUrl": "https://exmple.jpg",
                        "quantity": 1,
                        "price": 100
                    },
                    {
                        "name": "れんこんサラダ", 
                        "imageUrl": "https://exmple.jpg", 
                        "quantity": 2, 
                        "price": 100
                    }
                ]
            }
        ],
    }
}

上記のようなレスポンスが返ってきた場合、実際にLINE Payの残高が減り、LINE PayのMy Pageにて取引の履歴が残っていることを確認できるかと思います。
LINE PayのMy Page
[取引管理]>[取引内訳]
スクリーンショット 2019-12-23 18.05.47.png (198.1 kB)

以上で、LINE Pay APIにて一般決済する時の処理の流れになります。

(おまけ)Postmanで疎通確認を行う

まだ実際にはアプリには組み込まないけど、LINE Pay APIを叩いたらどんなレスポンスが返ってくるか見たい場合は
Postman」を使うのが便利です、ここではPostmanを使ってLINE PayのAPIをコールするときの設定を明記します。(Postmanの導入方法についてはここでは説明しませんので、各自で調べていただくよう、宜しくお願いします。)

各種設定

環境ファイルの設定(Channel ID とChannel SecretKeyの設定)

①赤枠部分の歯車のアイコンをクリックします。
スクリーンショット 2019-12-25 11.35.13 2.png (141.6 kB)

②赤枠部分の[Add]をクリックします。
スクリーンショット 2019-12-25 11.38.10.png (182.5 kB)

③以下のように設定し[Add]をクリックします。

VARIABLE INITIAL VALUE CURRENT VALUE
channelId LINEのMyPageで確認したChannel ID ←INITIAL VALUEで設定した値と同じ
channelSecret LINEのMyPageで確認したChannel SecretKey ←INITIAL VALUEで設定した値と同じ

スクリーンショット 2019-12-25 11.40.19.png (197.0 kB)

④赤枠部分のように環境ファイルを先程追加したものに設定しておきます。
スクリーンショット 2019-12-25 11.35.13.png (141.4 kB)

Headersの設定

以下のように設定します。

KEY VALUE
Content-Type application/json
X-LINE-ChannelId {{channelId}}
X-LINE-Authorization-Nonce {{nonce}}
X-LINE-Authorization {{authorization}}

Content-Typeは文字列で'application/json'と設定しておきます。
その他の設定はPostmanの環境ファイルから取得するようにします。
{{channelId}}等、中括弧で囲むと、先程設定した設定ファイルのkey参照し、その値を参照するようになります。
スクリーンショット 2019-12-25 11.35.13.png (141.4 kB)

Bodyの設定

rawを選択し、以下のように設定します。

{
    "amount": 300,
    "currency": "JPY",
    "orderId": "20190101",
    "packages": [
        {
            "id": "1",
            "amount": 300,
            "name": "LINE Pay商品",
            "products": [
                {
                    "name": "豚の角煮(赤みそ)",
                    "imageUrl": "",
                    "quantity": 1,
                    "price": 100
                },
                {
                    "name": "れんこんサラダ", 
                    "imageUrl": "", 
                    "quantity": 2, 
                    "price": 100
                }
            ]
        }
    ],
    "redirectUrls": {
        "confirmUrl": "myapp://confirm",
        "cancelUrl": "myapp://cancel"
    }
}

スクリーンショット 2019-12-25 11.37.06.png (224.2 kB)

Pre-request Scriptの設定

ヘッダーの「X-LINE-Authorization」に設定するための処理を記述します。

var crypto = require('crypto-js');
var time = (new Date()).getTime();
pm.environment.set("nonce", time);
var path = pm.request.url.getPath();
var body = pm.request.body.toString();
var message = pm.environment.get("channelSecret") + path + body + pm.environment.get("nonce") ;
var hash = crypto.HmacSHA256(message,pm.environment.get("channelSecret"));
var hashHeader = CryptoJS.enc.Base64.stringify(hash);
pm.environment.set("authorization",hashHeader);

スクリーンショット 2019-12-25 11.56.47.png (171.9 kB)
pm.〜で始まる記述はPostman独自の処理で、リクエストBodyを取得したり、環境ファイルの設定を読み込んだり書き込んだりするための記述です。

レスポンス

Sendした結果、以下のようなレスポンスが返ってきたら成功です。
以下の結果はRequest API (POST /v3/payments/request)をコールした際のレスポンスです。
スクリーンショット 2019-12-25 11.57.49.png (240.9 kB)

returnCodeが'0000'以外の場合はLINE APIへの疎通は成功しているが、処理が成功していません
各APIのReturn Codesの欄を参考にどこがミスをしているか探してみてください。

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

[WSL] VS Code上でChrome DebuggerのブレイクポイントがUnverified breakpointとなりヒットしない

事象

image.png

ブレイクポイントをセットしても、Unverified breakpoint(未検証のブレイクポイント)となり、セットしたポイントでデバッグが止まりません。

この記事では、上記の事象を解消します。

環境

  • WSL Ubuntu 16.04.6 LT

手順

こちらの記事を参考にしています。

launch.json内で、sourceMapPathOverridesでフォルダのマッピングを定義する必要があるようです。(こちらのオプションの詳細については調べていないため掘り下げません。)

launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "chrome",
            "request": "launch",
            "name": "Launch Chrome against localhost",
            "url": "http://localhost:3000",
            "webRoot": "${workspaceFolder}",
            "trace": true,
            "sourceMapPathOverrides": {
                "/mnt/c/*": "C:\\*"
            }
        }
    ]
}

上記の例ではCドライブをマッピングしていますが、もしDドライブにWSLをマウントしているのであれば、 "/mnt/d/*": "D:\\*"となります。

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

Identifier (識別子)とは

はじめに

Javascriptの勉強を進めるうちに、コンソールに
Identifier '変数名' has already been declared」と出力されることが増えた。

上記のエラーが出てくるときは、
2度変数を定義してしまった時である。

(例)

let a = 123;
let a = 456;
Identifier 'a' has already been declared

上記を読み解くと、エラーメッセージには
「識別子aはすでに宣言されている」と出力される。

識別子って何??という疑問が生まれ、
調べた結果をここに残す。

Identifier (識別子)とは

まず、識別子について説明する。
識別子とは、様々な対象から特定の一つを識別するのに用いられる名前や符号、数字を指している。(参考:e-Words)

プログラミングの場合、関数名や変数名、プロパティ名
識別子に当たる。

重要なのは識別子aで、
const a = 1
で定義されたのか、
const a = () => {}
で定義されたのか、
それは重要ではない。

重要なのは、すでにaが定義されているということ。

今回の例の場合、Identifier '変数名' has already been declaredはすでに識別子「変数名」は宣言されていますよーと教えてくれている。

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

初心者による DOM 要素の追加/置換/削除

概要

DOMの役割は既存のノードを参照するばかりではなく、文書ツリーに対して、新規のノードを追加したり、既存のノードを置換/削除することもできる。

innerHTMLを使うかどうか

innerHTMLプロパティを使うことで、HTMLを編集することができる。しかし、以下のような問題点がある。

  1. コンテンツが複雑になった場合に、コードの見通しが悪くなる。
  2. ユーザからの入力値に基づいてコンテンツを作成した場合、任意のスクリプトを実行されてしまう可能性がある。

これを踏まえたうえで、新しい方法は

  1. オブジェクトツリーとして操作できるので、対象となるコンテンツが複雑になった場合でも、コードの可読性が劣化しにくい。
  2. 要素/属性とテキストを区別して扱うので、ユーザからの入力によってスクリプトが混入するようなリスクを回避しにくい。
  3. ただし、少しのコンテンツの埋め込みにも、ある程度の操作が必要になるので、コードは冗長になりがちになる。

よってまとめると、

シンプルなコンテンツの編集 -> innerHTMLプロパティ
複雑なコンテンツの編集 -> 新しいアプローチ 

新規にノードを作成する

新規にノードを追加する流れは次のようになる。

1. 要素/テキストノードを作成する
2. ノード同士を組み立てる
3. 属性ノードを追加する

2の追加したいノードを固めることを省いてはいけない。一つずつ追加していくと一回ずつ再描画がかかるため、パフォーマンスの面から望ましくない。一回で追加することで、パフォーマンスを下げない。

要素/テキストを作成する

コンテンツを作成するにはまず、createElement/createTextNodeメソッドを利用して、新たに挿入すべき要素/ノードを生成します。

sample.js
document.createElement(name)
document.createTextNode(text)
 //name : 要素名 text : テキスト

また、create...には様々な種類がある。

sample.js
document.createAttribute(属性名)
document.createCDATASection(テキスト) //CDATAセクション
document.createComment(テキスト) //コメント
document.createEntityReference(実体名) //実体参照ノード
document.createProccesingInstruction(ターゲット名、データ) //処理命令ノード
document.createDocumentFragment() //ドキュメント 

これらによって作られた要素/ノードはまだドキュメントツリーには属していないため次は、これらをドキュメントツリーに接続させる。

ノード同士を組み立てる

この作業を行うのが、appendChildです。appendChildメソッドは、指定された要素を現在の要素の最後の子要素として追加します。

sample.js
elem.appendChild(node)
 //elem : 要素オブジェクト node : 追加するノード

appendChildメソッドは、insertBeforeメソッドで置き換えることもできます。

sample.js
list.appendChild(anchor)
list.insertBefore(anchor, null)

insertBeforeメソッドは、第一引数で指定したノードを、第二引数で指定した子ノードの直前!に挿入します。appnedChild同様に、最後尾に追加したい場合には、第二引数にnullを指定します。

属性ノードを追加する

属性ノードの設定は、属性と同名のプロパティを設定するだけですが、より汎用的なコードを記述するためにcreateAttributeメソッドを使う。

sample.js
anchor.href = url.value

let href = document.createAttribute('href')
href.value = url.value
anchor.setAttributeNode(href)

属性ノードの値を設定するには、valueプロパティを使用します。また、属性ノードを要素ノードに関連づけるには、appendChildやinsertBeforeメソッドではなく、setAttributeNodeメソッドを使用する点に注意。
なぜならば、属性ノードは要素ノードの子要素ではなく「属性」という扱いであるからである。

まとめ1

まとめると、

sample.html
<form>
 <div>
  <label for="name">サイト名:</label><br />
  <input id="name" name="name" type="text" size="30" />
 </div>
 <div>
  <label for="url">URL:</label><br />
  <input id="url" name="url" type="url" size="50" />
 </div>
 <div>
  <input id="btn" type="button" value="追加" />
 </div>
</form>
<div id="list"></div>
sample.js
document.addEventListener('DOMContentLoaded', function() {
 document.getElementById('btn').addEventListener('click', function() {
  //テキストボックスの取得
  let name = document.getElementById('name')
  let url = document.getElementById('url')

  //<a>要素を生成
  let anchor = document.createElement('a')
  //<a>要素のhref属性を設定
  anchor.href = url.value
  //テキストノードを生成し、<a>要素の直下に追加
  let text = document.createTextNode(name.value)
  anchor.appendChild(text)
  //<br>要素を生成
  let br = document.createElement('br')
  //<div id="list">を取得
  let list = document.getElementById('list')
  //<div>要素の直下に<a>/<br>の順に追加
  list.appendChild(anchor)
  list.appendChild(br)
 }, false)
}, false)

既存のノードを置換/削除する

ノードを置換する

子ノードを置き換えるのは、replaceChildメソッドを使う。

sample.js
elem.replaceChild(after, before)
 //elem : 要素オブジェクト after : 置き換え後のノード before : 置き換え対象ノード

注意点としては、置き換え対象のノードは現在のノードに対する子ノードでなければならない。

ノードを削除する

子ノードを削除するのは、removeChildメソッドです。

sample.js
elem.removeChild(node)
 //elem : 要素オブジェクト node : 削除対象ノード

削除対象ノードは、現在のノードに対するノードである必要があります。

属性を削除する

属性を削除する場合には、removeAttributeメソッドを使います。

sample.js
elem.removeAttribute(attribution)
 //elem : 要素オブジェクト attribution : 属性値

カスタムデータ属性を設定する

data-xxxxはカスタムデータ属性と呼ばれ、自由に値を設定できる特別な値です。これを使うメリットは、可変な情報(パラメータ)と機能(イベントリスナー)とを切り離しておくことで、後からコードを再利用しやすくなることだ。

まとめ2

まとめると

sample.html
<ul id="list">
 <li><a href="JavaScript:void(0)" data-isbn="987-4-7981-3547-2">独習PHP 第三版</a></li>
 <li><a href="JavaScript:void(0)" data-isbn="978-4-7741-8030-4">Javaポケットリファレンス</a></li>
 <li><a href="JavaScript:void(0)" data-isbn="978-4-7741-7984-1">Swiftポケットリファレンス</a></li>
 <li><a href="JavaScript:void(0)" data-isbn="978-4-7981-4402-3">独習ASP.NETポケットリファレンス</a></li>
 <li><a href="JavaScript:void(0)" data-isbn="978-4-8222-9644-5">アプリを作ろう!Android入門</a></li>
</ul>
<input id="del" type="button" value="削除" disabled />
<div id="pic"></div>
sample.js
document.addEventListener('DOMContentLoaded', function() {
 let list = document.getElementById('list')
 let del = document.getElementById('del')
 let pic = document.getElementById('pic')

 //<ul id="list">配下をクリックしたときの配下
 list.addEventListener('click', function(e) {
  //data-isbn属性からアンカータグに紐付いたisbn値を取得
  let isbn = e.target.getAttribute('data-isbn')
  //isbn値が取得できた場合にのみ処理を実行
  if(isbn) {
   //img要素を生成
   let img = document.createElement('img')
   img.src = 'http://windows./img/' + isbn + '.jpg'
   img.alt = e.innerHTML
   img.height = 150
   img.width = 108
   //<div>要素配下に<img>要素が存在するか(画像表示中か)を確認
   if(pic.getElementByTagName('img').length > 0){
    //<img>要素が存在する場合、あらたな<img>要素で置換
    pic.replaceChild(img, pic.lastChild)
   }else{
    //<img>要素が存在しない場合、新たに追加し、[削除]ボタンを追加
    del.disable = false
    pic.appendChild(img)
   }
  }
 }, false)

 //[削除]ボタンがクリックされたときの処理
 del.addEventListener('click', function() {
  //<div id="pic">配下の子要素を削除し、[削除]ボタンを無効に
  pic.removeChild(pic.lastChild)
  del.disable = true
 }, false)
}, false)

HTMLCollection / NodeList の注意点

getElementsByTagName / getElementsByName / getElementsByClassNameメソッドは、戻り値としてHTMLCollectionまたはNodeListオブジェクトを返します。この二つは、「オブジェクトが文書ツリーを参照しており、文書ツリーへの変更がHTMLCollection / NodeListオブジェクトへもリアルタイムに反映される。」ということです。

たとえば、appendChildメソッドで

要素を追加すると、HTMLCollectionオブジェクトlistの内容がリアルタイムで変化します。

これは一見便利にも見えますが、注意しなければいけないのがul.lengthの値が変化することからfor文等で使うときにはループが終わらないなどのバグが出やすい。これは、for文で一度初期化してから使うとよし。

参考資料

山田祥寛様 「javascript本格入門」

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

君は知ってるかい?テンプレートリテラルの罠を知らず死ぬ事を

テンプレートリテラルの罠にハマった話

結論: テンプレートリテラルを使った文字列(ヒアドキュメント)はインデントも含まれるから気を付けろ

どんな風にハマったのか

先日、Webトークンを生成するためにこんなコードを書きました。

    const keyID = 'keykeykey';
    const teamID = 'teamteamteam';
    const secret = `-----BEGIN PRIVATE KEY-----
    abcabcabcabcabcabcabcabcabcabcabcabcabcabc
    abcabcabcabcabcabcabcabcabcabcabcabcabcabc
    abcabcabcabcabcabcabcabcabcabcabcabcabcabc
    -----END PRIVATE KEY-----`;

    const jwtToken = jwt.sign({}, secret, {
        algorithm: "ES256",
        expiresIn: "180d",
        issuer: teamID,
        header: {
            alg: "ES256",
            kid: keyID
        }
    });

すると、こんなエラーが。

PEM routines:PEM_read_bio:bad end line

なんで!あってるじゃん!テンプレートリテラル `` 使ってるから、改行しても大丈夫じゃないの??

テンプレートリテラルを使った文字列(ヒアドキュメント)ではインデントしちゃいけない

よくなかったのは以下の部分です。

    const secret = `-----BEGIN PRIVATE KEY-----
    abcabcabcabcabcabcabcabcabcabcabcabcabcabc
    abcabcabcabcabcabcabcabcabcabcabcabcabcabc
    abcabcabcabcabcabcabcabcabcabcabcabcabcabc
    -----END PRIVATE KEY-----`;

この状態では左側のインデントも文字列として認識されてしまいます。
正しくは、以下のような感じです。

const secret = `-----BEGIN PRIVATE KEY-----
abcabcabcabcabcabcabcabcabcabcabcabcabcabc
abcabcabcabcabcabcabcabcabcabcabcabcabcabc
abcabcabcabcabcabcabcabcabcabcabcabcabcabc
-----END PRIVATE KEY-----`;

このように完全に左側に寄せてあげると、エラーが出ません。

何か間違いがあったら教えてください。

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