20200401のJavaScriptに関する記事は27件です。

プログラミングド素人でもAPIを使えるのか試してみた

プロトアウトスタジオという、プログラミングスクールではなく、プロトタイピングをアウトプットするというスクールに参加し始めた人の記事です。

前回の投稿で、LINE bot とAPIを繋げる宿題を達成できず、講師の方に対応策をご指南いただきました。
※前回投稿:ド素人がプロトタイピングスクールに入ったらどうなったか&宿題公開
https://qiita.com/hiromae0213

繋げ先のAPIは、「リクエストをもらったらランダムに柴犬画像を返す」というAPIを使っています。

※API:
https://shibe.online/

下記記事を参考にしています。

物知りLINE BOTを作ろうとしたらfugaしか答えない残念な奴になった話
https://qiita.com/zgw426/items/a5196ab7f26b785479ec

'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
const PORT = process.env.PORT || 3000;

const config = {
    channelSecret: 'xxxx',
    channelAccessToken: 'xxxx'
};

const app = express();

app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);
    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let mes = 'ちょっとまってね'; // 待ってねってメッセージだけ先に処理

  getQiitaTag(event.source.userId , event.message.text); // スクレイピング処理が終わったらプッシュメッセージ

  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: mes
  });
}

const getQiitaTag = async (userId,tag) => {

    let mes = '';
    try {
        const res = await axios.get('http://shibe.online/api/shibes?count=1&urls=true&httpsUrls=true' );
        const item = res.data;
        mes = item[0];
    } catch (error) {
        // 該当しないものは404でエラーになる
        const {
            status,
            statusText
        } = error.response;
        console.log(`Error! HTTP Status: ${status} ${statusText}`);
        if( status == 404 ){
            mes = 'Qiitaのタグ「' + tag + '」の記事はありませんでした';
        } else {
            mes = `Error! HTTP Status: ${status} ${statusText}`;
        }
    }

    await client.pushMessage(userId, {
        type: 'text',
        text: mes,
    });
}

app.listen(PORT);
console.log(`Server running at ${PORT}`);

LINEで何かを入力したら柴犬画像を返すAPIを呼び出して、柴犬画像のURLがLINE上に表示されました。

LINEトリミング.png

流れ的には下記のようなイメージを持っていますが、
今の私のレベルではわからない領域が多くてなぜ上記コードが
上手く動いているかは理解できていません。

LINE botの流れ.png

プロトアウトスタジオ第2回の宿題は、
「今日学んだこと(APIの仕組み)やLine Botを発展させる」です。
LINE Botは現状わからないことが多すぎて無理そうなので、まずは自分が理解できる領域で何かできないか考えてみました。

Progateを通じてHTMLとJavaScriptの基本中の基本はわかってきたことをふまえ、
下記のような形で、APIで取得した画像をHTML上に表示させる仕組みの実装にチャレンジしました。

HTML案①.png

が、エラー!
理由は、「axiosはサーバ側で動かすものなのでフロント側で使うものではない」そうです。
ただ、HTML上のscriptタグで記載しておけば使えることは使えるそうで、下記のような形で再チャレンジしました。

HTML案②.png

が、再度エラー。
理由としては、取得した画像データを直接フロント側で読み込むのは何とかオリジン(←名前忘れた、、、)の制約に引っかかるケースが多く難しいそうです。

最終的にはその制約に引っかからないAPIを講師の方に見繕っていただき、つなぐことができました!

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>犬の画像を呼び出す</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>      
    <button id="btn">ボタン</button>
    <img src="#" id="shiba">
    <script src="app.js"></script>
</body>
</html>
let dogImage='';

document.querySelector('button').addEventListener('click',()=>{
        axios.get('https://dog.ceo/api/breeds/image/random')
       .then(function (response) {
         dogImage=response.data.message;
         document.getElementById('shiba').src= dogImage;
       })
       .catch(function (error) {
         // handle error
         console.log(error);
       });
    })

感性物.png

ということで、
ProgateのHTML/CSSとJavaScriptを2周やった程度でもAPIを呼んで取得した情報を表示させる、までは何とかたどり着けました。

これを作りながら、本当は下記のような形にできれば柴犬APIで取得した画像も表示できるのでは?と考えましたが、まだ時間が足りないのでこれから勉強します。
今後できるようになりたいこと.png

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

Swiper.jsが動作しなかった時の対処法[備忘録]

はじめに

バージョン確認の重要性

Swiper.jsはバージョンによってサポートしてくれるブラウザが違う。ver5.0以降から、IEは完全にサポート対象外になった。今回私はchromeで開発を行っているので最新版でいけると思いきや、挙動が意図したものにならなかったので、4.5.0系を使うことにした。

便利なオプションたち

オプションが豊富で有名なSwiper.js

ここでは、私が参考にした記事を載せておくだけに留めておく。

参考記事

実例12パターン】画像スライダーはSwiper使っておけば間違いない!実用的な使い方を紹介
https://haniwaman.com/swiper/
swiper.js使ってみたからそのオプションについて(v4.1.6)
https://reiwinn-web.net/2018/03/15/swiper-4-1-6/

実際のコード

swiper.js
$(function(){
  var mySwiper = new Swiper('.swiper-container', {
    slidesPerView: 3,
    slideToClickedSlide: true,
    pagination: {
      el: '.swiper-pagination',
      type: 'bullets',
      clickable: true
    },
    navigation: {
      nextEl: '.swiper-button-next',
      prevEl: '.swiper-button-prev'
    },
    breakpoints: {
      767: {
        slidesPerView: 1,
        spaceBetween: 0
      }
    }
  });
});


html.haml
        .swiper-container.p-2.rounded
          .swiper-wrapper.p-1
            - @posts.each do |post| 
              .events__content.col-sm-6.col-md-4.mb-3
                .swiper-slide
                  .card{id: post.id}
                    %label.m-1
                      - if post.image.present?
                        %img.card-img-top.img-fluid.rounded{src: "#{post.image}"}
                      - else
                        %img.card-img-top.img-fluid.rounded{src: "/assets/noimage.png"}
                      .card-body.event
                        %h5= link_to "#{post.title}", post_path(post.id), class: "event-title stretched-link text-decoration-none"
                        .event__name 
                          #{post.user.name} さん
                        .text-right
                          = l post.created_at, format: :long     
           .swiper-button-prev
           .swiper-button-next
           .swiper-pagination

これでprevボタンnextボタンを押しても、スライドされない。consoleをみてもエラーはない。ドラッグしながらスクロールすると、一応.swiper-containerに要素が入っていることはわかる。jsファイルは読み込まれているが、メソッドが実行されていない。

階層構造は崩すな

原因がわかった。先ほどのコードは、.swiper-wrapperと.swiper-slidesの間に、.events__content......というクラスが入っている。これが原因だった。Swiper.jsのswiper-container, wrapper, slidesのクラスたちは必ず親子孫の関係でなくてはならなかった。

修正後のコード

html.haml
.swiper-container.p-2.rounded
          .swiper-wrapper.p-1
            - @posts.each do |post| 
              .swiper-slide.events__content.col-sm-6.col-md-4.mb-3
                .card{id: post.id}
                  %label.m-1
                    - if post.image.present?
                      %img.card-img-top.img-fluid.rounded{src: "#{post.image}"}
                    - else
                      %img.card-img-top.img-fluid.rounded{src: "/assets/noimage.png"}
                    .card-body.event
                      %h5= link_to "#{post.title}", post_path(post.id), class: "event-title stretched-link text-decoration-none"
                      .event__name 
                        #{post.user.name} さん
                      .text-right
                        = l post.created_at, format: :long
                .card.col-auto
                  .text-left
                    - post.genre_list.each do |genre|
                      .badge.badge-primary{data:{role: "tagsinput"}}
                        = link_to "#{genre}", tag_path(genre), class: 'text-decoration-none text-white'
                - if user_signed_in?
                  .offset-8.col-auto.card
                    .text-center.likes
                      = render partial: '/posts/posts', locals: {post: post}
          - if @posts.count >= 4
            .swiper-button-prev
            .swiper-button-next
            .swiper-pagination

これで正常に動くようになった。

終わりに

私のように、こんなことで何時間も無駄にしないように、皆さんも気をつけてください。

参考記事

【Rails5】「Swiper」を使ってスライダー、カルーセルを作る方法
https://qiita.com/emincoring/items/18d07d0aec5d9836227c
[Rails]Swiperで画像スライド作成
https://qiita.com/yummy888/items/8528c7542f85ae7bbc55

サンプル付き!簡単にスライドを作れるライブラリSwiper.js超解説(各種ナビゲーションカスタマイズ編)
https://garigaricode.com/swiper_navigation/

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

JavaScript JavaScriptでポップアップウィンドウを作成

完成品

スクリーンショット 2020-04-01 22.34.48.png
スクリーンショット 2020-04-01 22.35.07.png

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="style.css">
    <script src="script.js"></script>
</head>
<body>
    <input type="button" id="btn" value="押す"> <!-- 変数に格納 -->
    <div id="box"> <!-- 変数に格納 -->
        <p id="close">× <!-- 変数に格納 -->
            <h2>ポップアップ</h2>
        </p>
    </div>

</body>
</html>

CSS

@charset "UTF-8";

#box {
  background: #FFF;
  border: 1px solid #333;
  box-shadow: 0 10px 10px #999;
  display: none;  /*! id要素に対してdisplay:none  */
  font-family: serif;
  padding: 10px;
  position: relative;
  text-align: center;
  width: 200px;
}

#box > #close {
  background-color: #EEE;
  color: #333;
  cursor: pointer;
  height: 30px;
  line-height: 30px;
  margin: 0;
  position: absolute;
  right: 1px;
  text-align: center;
  top: 1px;
  width: 30px;
}

#box > #close:hover {
  background-color: #F9F9F9;
  color: #999;
}

#btn {
  background-color: rgb(20, 114, 236);
  border: 0;
  color: #FFF;
  cursor: pointer;
  padding: 5px 20px;
}

#btn:hover {
  color: rgb(20, 114, 236);
  border: 1px solid rgb(20, 114, 236);
  background-color: rgb(255, 255, 255);
}

#btn:active {
  background-color: #4A4;
}

JavaScript

window.onload= function(){ //window の load イベントに対応するイベントハンドラ

let box = document.querySelector("#box"); //id要素取得
let btn = document.querySelector("#btn"); //id要素取得
let close = document.querySelector("#close") //id要素取得

let boxstyle = box.style; //boxのstyle値をboxstyleに格納

btn.onclick = function(){ //btnがクリックされた時動かす関数
    if(boxstyle.display === "block"){ 
        boxstyle.display = "none";
    }else{
        boxstyle.display = "block";
    }
};

close.onclick= function(){ //closeがクリックされた時の関数
    boxstyle.display = "none";
};

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

JavaScript の論理演算子 (&&, ||) の応用

基本

  • JavaScript の処理は左から右へ進む!
  • AND / OR 演算子は2つの値を左から順に Boolean にキャストしてチェックし、どちらかの値を返す.
  • Boolean を返すわけではない.
x = {foo:4} && null; // = null
y = 1 || window; // = 1

AND 演算

AND 演算 a1 && a2 の返り値は Boolean(a1)===false ? a1 : a2 と等価.
AND 演算は〔先に false と判定された値〕or〔最後の値〕を返す.
計算効率を上げるために、false 判定される可能性が高い方の値を先(左)に置く.

AND_演算子(&&)
//  1        2
   true && true  // = true   ← 2
   true && false // = false  ← 2
  false && true  // = false  ← 1
  false && false // = false  ← 1

OR 演算

OR 演算 a1 || a2 の返り値は Boolean(a1)===true ? a1 : a2 と等価.
OR 演算は〔先に true と判定された値〕or〔最後の値〕を返す.
計算効率を上げるために、true 判定される可能性が高い方の値を先(左)に置く.

OR_演算子(||)
//  1        2
   true || true  // = true   ← 1
   true || false // = true   ← 1
  false || true  // = true   ← 2
  false || false // = false  ← 2

応用: 論理演算をたくさん直列した場合

JavaScript の処理は左から右へ進むので、まず最も左の演算子が評価される。
その返り値に基づき次の演算子が評価され、右へと進んでいく。

例-1
       a || b && c && d && e || f && g || h
=((((((a || b)&& c)&& d)&& e)|| f)&& g)|| h
例-2
//    1         2       3      4
  undefined && true ||  0  && [6] // = 0  ← 3
=(undefined && true)||  0  && [6]
= undefined         ||  0  && [6]
=(undefined         ||  0 )&& [6]
=                       0  && [6]
=                       0

上記の例のように括弧を一切使わずに論理演算を直列すると、必ず全ての値がチェックされることになるので計算効率が悪い
各値が true / false 判定される確率を考慮し、適切に括弧で括るべき.

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

TypeScriptで学ぶデザインパターン〜Adapter編〜

対象読者

  • デザインパターンを学習あるいは復習したい方
  • TypeScriptが既に読めるあるいは気合いで読める方
    • いずれかのオブジェクト指向言語を知っている方は気合いで読めると思います
  • UMLが既に読めるあるいは気合いで読める方

環境

  • OS: macOS Mojave
  • Node.js: v12.7.0
  • npm: 6.14.3
  • TypeScript: Version 3.8.3

本シリーズ記事一覧(随時更新)

Adapterパターンとは

インターフェースに互換性の無いクラス同士を組み合わせるためのパターンです。

AdapterのAdaptというのは"適合させる"という意味です。皆さんの中にも、USB Type-Aに対応していない端末にアダプターを用いてそれを使えるようにした...という経験をした方もいらっしゃると思います。そのアダプターと同様の意味合いです。

サンプルコード

Adapterパターンで作られたクラス群がどんなものになるのか確認していきましょう。

今回は、題材として"エディター"を想定します。GitHubにも公開しています。Adapterパターンには"継承を用いたもの"と"委譲を用いたもの"の2種類あります。題材は同じなので、どちらの説明なのかを意識しつつこれ以降読んでみてください。

継承

modules/inheritance/Editor.ts

エディターのインターフェースです。

Editor.ts
export default interface Editor {
  strong(): string;
  strike(): string;
}

strongメソッドでは、文字列を太字にする処理を想定しています。たとえば、'テキスト'という文字列をMarkdown('**テキスト**')やHTML('<strong>テキスト</strong>')に変換します。
strikeメソッドでは、文字列に取り消し線を付与する処理を想定しています。

今回はMarkdown記法で太字にしたり取り消し線を付与したりする処理を作り込んでいくこととします。

modules/BlogEditor.ts

実現したい機能(今回は太字化と取り消し線付与)が実装されているクラスです。

BlogEditor.ts
export default class BlogEditor {
  private text: string;

  constructor(text: string) {
    this.text = text;
  }

  addAsteriskForStrong(): string {
    const strongText: string = '**' + this.text + '**';

    return strongText;
  }

  addTildeForStrike(): string {
    const strikeText: string = '~~' + this.text + '~~';

    return strikeText;
  }
}

ここで重要な点はEditorインターフェースとメソッド名が異なっていることです。つまり、Editorインターフェースを実装することができないわけです。

modules/inheritance/MarkdownEditor.ts

インターフェースとクラスを結びつけるクラスです。このクラスがAdapterパターンの主人公です。

MarkdownEditor.ts
import Editor from './Editor';
import BlogEditor from '../BlogEditor';

export default class MarkdownEditor extends BlogEditor implements Editor {
  constructor(text: string) {
    super(text);
  }

  strong(): string {
    return this.addAsteriskForStrong();
  }

  strike(): string {
    return this.addTildeForStrike();
  }
}

Editorインターフェースを実装して、かつBlogEditorクラスを継承することでその機能を実現しています。

MainInheritance.ts

Adapterパターンで作られたクラス群を実際に使う処理が書かれています。

MainInheritance.ts
import Editor from './modules/inheritance/Editor';
import MarkdownEditor from './modules/inheritance/MarkdownEditor';

const editor: Editor = new MarkdownEditor('こんにちは!');

console.log(editor.strong());
console.log(editor.strike());

委譲

継承セクションと処理内容は同様です。本セクション独自の説明のみ記載します。

modules/delegation/Editor.ts

エディターの抽象クラスです。継承セクションでEditorはインターフェースだったことを思い出しましょう。

Editor.ts
export default abstract class Editor {
  abstract strong(): string;
  abstract strike(): string;
}

modules/BlogEditor.ts

上述の継承セクションと同ファイルであるため割愛します。

modules/delegation/MarkdownEditor.ts

抽象クラスとクラスを結びつけるクラスです。このクラスがAdapterパターンの主人公です。(重要なことなので繰り返します)

MarkdownEditor.ts
import Editor from './Editor';
import BlogEditor from '../BlogEditor';

export default class MarkdownEditor extends Editor {
  private blogEditor: BlogEditor;

  constructor(text: string) {
    super();
    this.blogEditor = new BlogEditor(text);
  }

  strong(): string {
    return this.blogEditor.addAsteriskForStrong();
  }

  strike(): string {
    return this.blogEditor.addTildeForStrike();
  }
}

TypeScriptにおいて多重継承は認められていないため、Editor抽象クラスとBlogEditorクラスを同時に継承することはできません。そのため、Editor抽象クラスを継承してBlogEditorインスタンスの利用は委譲によって実現しています。

MainDelegation.ts

Adapterパターンで作られたクラス群を実際に使う処理が書かれています。

MainDelegation.ts
import Editor from './modules/delegation/Editor';
import MarkdownEditor from './modules/delegation/MarkdownEditor';

const editor: Editor = new MarkdownEditor('こんにちは!');

console.log(editor.strong());
console.log(editor.strike());

クラス図

ここまでAdapterパターンで作られたクラス群を1つずつ確認してきました。次にクラス図を示します。Adapterパターンの全体像を整理するのにお役立てください。

Adapter.png

  • 継承
    • Client: サンプルコードではMainInheritanceが対応
    • Target: サンプルコードではEditorインターフェースが対応
    • Adapter: サンプルコードではMarkdownEditorクラスが対応
    • Adaptee: サンプルコードではBlogEditorクラスが対応
  • 委譲
    • Client: サンプルコードではMainDelegationが対応
    • Target: サンプルコードではEditor抽象クラスが対応
    • Adapter: サンプルコードではMarkdownEditorクラスが対応
    • Adaptee: サンプルコードではBlogEditorクラスが対応

解説

最後に、このデザインパターンの存在意義を考えます。これを考える上でサンプルコードに少々ストーリーを加えます。

BlogEditorクラスは十分にテストされたものだとします。つまり、BlogEditorは手を加えたくないわけです。一方で、Editorインターフェース(または抽象クラス)のメソッド名もこれが設計上最善だとします。冒頭でも触れた通り、たとえば太字にする際にはMarkdownでもHTMLでもどちらでも良いわけです。そういった前提がある以上、Editorインターフェース(または抽象クラス)のメソッド名はaddAsteriskForStrongなど具体的過ぎる名前だと不適切なわけです。

このように、両者(EditorBlogEditor)の事情を鑑みた上で、両者に全く手を加えずに結びつける役割をMarkdownEditorクラスが担っているわけです。

補足

サンプルコードの実行方法はこちらと同様です。

参考

あとがたり

TypeScriptの学習というよりデザインパターンの学習の方が多い。TypeScriptの学習も続けていきたい。

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

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

Kinx ライブラリ - Signal

Zip

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。言語はライブラリが命。ということでライブラリの使い方編。

今回は Zip です。

アプリ作る時に Zip 機能は欠かせないですね。あと、脆弱だ、脆弱だ、言われてますけど、業務アプリの中ではパスワード付き Zip が(建前上)求められることも事実。なので、パスワード付き Zip が作れないと実用にならない(日本では...?)。

Zip

Zip アーカイブ作成

Zip インスタンスの作成(class Zip

基本的にはこんな感じ。

var zip = new Zip("zipfile.zip", File.READ|File.WRITE);

ファイル名とモードを指定する。

モード
モード 意味
File.READ 読み込みモード
File.WRITE 書き込みモード
File.APPEND 追記モード

File と同じ。

メソッド

Zip インスタンスのメソッドは以下の通り。

メソッド 内容 オプション
extract(name, [opts]) 展開して文字列で取得 { password, overwrite, skip, }
extractTo(name, file [, opts]) ファイルに展開 (同上)
find(name) エントリを検索、エントリ・オブジェクトを返す
addFile(filename [, opts]) Zip ファイルにエントリを追加 { password, method, aes, level }
addString(text [, opts]) Zip ファイルにエントリを追加 (同上)
setPassword(password) 全 Zip エントリで共通して使用するパスワードを設定
setOverwrite(truefalse) 上書き設定を一括で指定しておくために使用する

エントリの追加(addFile()/addString()

addFile() または addString() を使います。即座にエントリが追加されます。

zip.addFile("README.md");

ファイルにディレクトリ名を与えた場合、ディレクトリは以下のファイルが追加される。また、第 2 引数にションを渡せる。オプションの内容は次の通り。

  • password: パスワード付き Zip のパスワード。デフォルトは無し。
  • method: 圧縮方法。デフォルトは deflate。その他、指定できるのは storebzip2lzma
  • aes: WinZIP 互換の AES 暗号化を有効にするか(true/false)。デフォルト false。尚、addString の場合には無視される。
  • level: 圧縮レベル。0-9。

暗号化

addFile() の際にオプションで指定できるが、一括で最初に設定して置く場合は setPassword() を使うことができる。

var zip = new Zip("zipfile.zip", File.READ|File.WRITE);
zip.setPassword("password");

Zip ファイル一覧の表示

zip インスタンスには既に配列としてエントリ・オブジェクトが格納されている。以下のようにすると一覧表示することができる。尚、zip.totalFiles にエントリ数が格納されている。

var zip = new Zip("zipfile.zip", File.READ);
System.println("totalFiles = ", zip.totalFiles);
zip.each(function(e) {
    System.println("%s:" % e.filename);
    e.keySet().each(&(key) => {
        if (e[key].isFunction || e[key].isObject || e[key].isUndefined) {
            return; // 展開系の関数などはスキップ。
        }
        if (key == "crc32") { // CRC は 16 進表示
            System.println("    %-14s = %10X" % key % e[key]);
        } else if (key != "time" && key != "filename") { // 別で表示
            System.println("    %-14s = %10d" % key % e[key]);
        }
    });
    // time はさらにオブジェクト構造になっている
    e.time.keySet().each(&(k) => {
        System.println("      time.%-7d = %10d" % k % e.time[k]);
    });
    // // エントリを個別に展開することも可能。
    // if (e.filename == "README.md") {
    //     e.extractTo("READMEXX.md", { password: "text", overwrite: true });
    // }
});

以下のような感じで表示される。

totalFiles = 4
README.md:
    compsize       =       4413
    size           =      11621
    isDirectory    =          0
    crc32          =   EFD9A09C
    isEncrypted    =          1
    method         =    deflate
      time.month   =          3
      time.minute  =          1
      time.day     =         19
      time.year    =        120
      time.second  =          2
      time.hour    =         16
...

展開

Zip ファイルの展開は、以下の 2 通りの方法が可能。

  • 直接 Zip インスタンスから展開する。
  • Zip エントリオブジェクトから展開する。

Zip エントリから展開する場合は個別の展開になる。その場合、上記のようにイテレートして選択する方法と、find メソッドを使う方法の 2 種類がある。find メソッドは、指定したファイル名のエントリがあれば Zip エントリオブジェクトを返す。

展開時のオプションの意味は、以下の通り。

  • password: 展開に使うパスワード。指定されなかった場合、setPassword() で設定されたものを使う。setPassword() でも設定されてなかった場合、パスワードなしで展開しようとする。
  • overwrite: true を指定し、同名ファイルが既に存在した場合、上書きする。
  • skip: true を指定し、同名ファイルが既に存在した場合、スキップする。

尚、overwriteskip も指定されずに同名ファイルが存在した場合、ZipException 例外が送出される。

すべて展開

すべて展開するには、上記イテレートしたエントリに対して extractTo を実施する。必要なディレクトリは自動的に作成される。

zip.each(&(e) => e.extractTo("examples/zip/dst" / e.filename, { password: "text", skip: true }));

なんか説明してなかった気がするが、文字列に対して / オペレータを適用すると、/ で連結された文字列になる。

ファイルを指定して展開

直接 Zip インスタンスに対して extract または extractTo メソッドを使うことが可能。

zip.extractTo("README.md", "READMEXX.md", { password: "text", skip: true });

extract を使用した場合、展開した内容を文字列として返す。

var text = zip.extract("README.md", { password: "text" });

現在、バイナリで取得する方法が無いのに気がついたので、追加する予定。オプションに { binary: true } をつけるイメージ。

Zip エントリオブジェクトの場合、エントリ名を指定する引数がなくなる。

メソッド 内容 オプション
extract([opts]) 展開して文字列で取得 { password, overwrite, skip, }
extractTo(file [, opts]) ファイルに展開 (同上)

find を使った例は以下の通り。

zip.find("README.md")
   .extractTo("READMEXX.md", { password: "text", skip: true });
var text = zip.find("README.md")
              .extract({ password: "text" });

その他

使っているライブラリ

これです。

機能一覧。全然 Mini な感じがしないですね。

  • Features
    • Creating and extracting zip archives.
    • Adding and removing entries from zip archives.
    • Read and write raw zip entry data.
    • Reading and writing zip archives from memory.
    • Zlib, BZIP2, and LZMA compression methods.
    • Password protection through Traditional PKWARE and WinZIP AES encryption.
    • Buffered streaming for improved I/O performance.
    • NTFS timestamp support for UTC last modified, last accessed, and creation dates.
    • Disk split support for splitting zip archives into multiple files.
    • Preservation of file attributes across file systems.
    • Follow and store symbolic links.
    • Unicode filename support through UTF-8 encoding.
    • Legacy character encoding support CP437, CP932, CP936, CP950.
    • Turn off compilation of compression, decompression, or encryption.
    • Windows (Win32 & WinRT), macOS and Linux platform support.
    • Streaming interface for easy implementation of additional platforms.
    • Support for Apple's compression library ZLIB implementation.
    • Zero out local file header information.
    • Zip/unzip of central directory to reduce size.
    • Ability to generate and verify CMS signature for each entry.
    • Recover the central directory if it is corrupt or missing.
    • Example minizip command line tool.

Zip64 対応

Zip64 も対応されている模様。4G 超えもいけるとの話だがテストできていない。

おわりに

Zip/Unzip はスクリプト言語を使う目的としては上位に来る機能でしょう。間違っても C で組みたいとは思わないし、簡単に Zip ファイル作りたい。

では、また次回。

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

Kinx ライブラリ - Zip

Zip

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。言語はライブラリが命。ということでライブラリの使い方編。

今回は Zip です。

アプリ作る時に Zip 機能は欠かせないですね。あと、脆弱だ、脆弱だ、言われてますけど、業務アプリの中ではパスワード付き Zip が(建前上)求められることも事実。なので、パスワード付き Zip が作れないと実用にならない(日本では...?)。

Zip

Zip アーカイブ作成

Zip インスタンスの作成(class Zip

基本的にはこんな感じ。

var zip = new Zip("zipfile.zip", File.READ|File.WRITE);

ファイル名とモードを指定する。

モード
モード 意味 動作概要
File.READ 読み込みモード 単独で指定した場合、ファイルが存在しなければ ZipException
File.WRITE 書き込みモード ファイルが存在しても新規に作成しなおすモード
File.APPEND 追記モード ファイルが存在した場合、そのファイルに追記するモード

File と同じ。

メソッド

Zip インスタンスのメソッドは以下の通り。

メソッド 内容 オプション
extract(name, [opts]) 展開して文字列で取得 { password, overwrite, skip, }
extractTo(name, file [, opts]) ファイルに展開 (同上)
find(name) エントリを検索、エントリ・オブジェクトを返す
addFile(filename [, opts]) Zip ファイルにエントリを追加 { password, method, aes, level }
addString(text [, opts]) Zip ファイルにエントリを追加 (同上)
setPassword(password) 全 Zip エントリで共通して使用するパスワードを設定
setOverwrite(truefalse) 上書き設定を一括で指定しておくために使用する

エントリの追加(addFile()/addString()

addFile() または addString() を使います。即座にエントリが追加される。

zip.addFile("README.md");

ファイルにディレクトリ名を与えた場合、ディレクトリは以下のファイルが追加される。また、第 2 引数にオプションを渡せる。オプションの内容は次の通り。

  • password: パスワード付き Zip のパスワード。デフォルトは無し。
  • method: 圧縮方法。デフォルトは deflate。その他、指定できるのは "store""bzip2""lzma"
  • aes: WinZIP 互換の AES 暗号化を有効にするか(true/false)。デフォルト false。尚、addString の場合には無視される。
  • level: 圧縮レベル。0-9。

オプションを付ける例は以下の通り。

zip.addFile("README.md", {
    method: "bzip2",
    password: "password",
    aes: true,
});
zip.addString("test/test1.txt", {
    content: "test/test\n",
    // aes: true,  // addString では無視される.
});

パスワードは、展開の際に個別に指定するようにすればエントリごとに別々につけることもできる。

暗号化

addFile() の際にオプションで指定できるが、一括で最初に設定して置く場合は setPassword() を使うことができる。

var zip = new Zip("zipfile.zip", File.READ|File.WRITE);
zip.setPassword("password");

Zip ファイル一覧の表示

zip インスタンスには既に配列としてエントリ・オブジェクトが格納されている。以下のようにすると一覧表示することができる。尚、zip.totalFiles にエントリ数が格納されている。

var zip = new Zip("zipfile.zip", File.READ);
System.println("totalFiles = ", zip.totalFiles);
zip.each(function(e) {
    System.println("%s:" % e.filename);
    e.keySet().each(&(key) => {
        if (e[key].isFunction || e[key].isObject || e[key].isUndefined) {
            return; // 展開系の関数などはスキップ。
        }
        if (key == "crc32") { // CRC は 16 進表示
            System.println("    %-14s = %10X" % key % e[key]);
        } else if (key != "time" && key != "filename") { // 別で表示
            System.println("    %-14s = %10d" % key % e[key]);
        }
    });
    // time はさらにオブジェクト構造になっている
    e.time.keySet().each(&(k) => {
        System.println("      time.%-7d = %10d" % k % e.time[k]);
    });
    // // エントリを個別に展開することも可能。
    // if (e.filename == "README.md") {
    //     e.extractTo("READMEXX.md", { password: "text", overwrite: true });
    // }
});

以下のような感じで表示される。

totalFiles = 4
README.md:
    compsize       =       4413
    size           =      11621
    isDirectory    =          0
    crc32          =   EFD9A09C
    isEncrypted    =          1
    method         =    deflate
      time.month   =          3
      time.minute  =          1
      time.day     =         19
      time.year    =       2020
      time.second  =          2
      time.hour    =         16
...

展開

Zip ファイルの展開は、以下の 2 通りの方法が可能。

  • 直接 Zip インスタンスから展開する。
  • Zip エントリオブジェクトから展開する。

Zip エントリから展開する場合は個別の展開になる。その場合、上記のようにイテレートして選択する方法と、find メソッドを使う方法の 2 種類がある。find メソッドは、指定したファイル名のエントリがあれば Zip エントリオブジェクトを返す。

展開時のオプションの意味は、以下の通り。

  • password: 展開に使うパスワード。指定されなかった場合、setPassword() で設定されたものを使う。setPassword() でも設定されてなかった場合、パスワードなしで展開しようとする。
  • overwrite: true を指定し、同名ファイルが既に存在した場合、上書きする。
  • skip: true を指定し、同名ファイルが既に存在した場合、スキップする。

尚、overwriteskip も指定されずに同名ファイルが存在した場合、ZipException 例外が送出される。

すべて展開

すべて展開するには、上記イテレートしたエントリに対して extractTo を実施する。必要なディレクトリは自動的に作成される。

zip.each(&(e) => e.extractTo("examples/zip/dst" / e.filename, { password: "text", skip: true }));

なんか説明してなかった気がするが、文字列に対して / オペレータを適用すると、/ で連結された文字列になる。

ファイルを指定して展開

直接 Zip インスタンスに対して extract または extractTo メソッドを使うことが可能。

zip.extractTo("README.md", "READMEXX.md", { password: "text", skip: true });

extract を使用した場合、展開した内容を文字列として返す。

var text = zip.extract("README.md", { password: "text" });

現在、バイナリで取得する方法が無いのに気がついたので、追加する予定。オプションに { binary: true } をつけるイメージ。

Zip エントリオブジェクトの場合、エントリ名を指定する引数がなくなる。

メソッド 内容 オプション
extract([opts]) 展開して文字列で取得 { password, overwrite, skip, }
extractTo(file [, opts]) ファイルに展開 (同上)

find を使った例は以下の通り。

zip.find("README.md")
   .extractTo("READMEXX.md", { password: "text", skip: true });
var text = zip.find("README.md")
              .extract({ password: "text" });

その他

使っているライブラリ

これです。

機能一覧。全然 Mini な感じがしないですね。

  • Features
    • Creating and extracting zip archives.
    • Adding and removing entries from zip archives.
    • Read and write raw zip entry data.
    • Reading and writing zip archives from memory.
    • Zlib, BZIP2, and LZMA compression methods.
    • Password protection through Traditional PKWARE and WinZIP AES encryption.
    • Buffered streaming for improved I/O performance.
    • NTFS timestamp support for UTC last modified, last accessed, and creation dates.
    • Disk split support for splitting zip archives into multiple files.
    • Preservation of file attributes across file systems.
    • Follow and store symbolic links.
    • Unicode filename support through UTF-8 encoding.
    • Legacy character encoding support CP437, CP932, CP936, CP950.
    • Turn off compilation of compression, decompression, or encryption.
    • Windows (Win32 & WinRT), macOS and Linux platform support.
    • Streaming interface for easy implementation of additional platforms.
    • Support for Apple's compression library ZLIB implementation.
    • Zero out local file header information.
    • Zip/unzip of central directory to reduce size.
    • Ability to generate and verify CMS signature for each entry.
    • Recover the central directory if it is corrupt or missing.
    • Example minizip command line tool.

Zip64 対応

Zip64 も対応されている模様。4G 超えもいけるとの話だがテストできていない。

おわりに

Zip/Unzip はスクリプト言語を使う目的としては上位に来る機能でしょう。間違っても C で組みたいとは思わないし、簡単に Zip ファイル作りたい。

では、また次回。

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

JavaScript の配列の要素をすべて削除する

clear とか clearAll とか remove とか delete とか配列を空にするシンプルなメソッドがあるのかと思ったけどなかった。

もう少し高機能な splice メソッドを使うのが良さそう。

Array.prototype.splice() - JavaScript | MDN

splice() メソッドは、 (in place で) 既存の要素を取り除いたり、置き換えたり、新しい要素を追加したりすることで、配列の内容を変更します。

サンプルコード。

let array = ['Tanis'] // 配列要素を指定して初期化
array.push('Sturm') // 要素を1つ追加
array.push('Flint', 'Tasslehoff') // 要素を複数追加
Array.prototype.push.apply(array, ['Caramon', 'Raistlin']); // 配列の要素を追加
console.log(`配列の要素数: ${array.length}`);
console.log(array);
console.log();

// 配列の要素を全削除する (インデックス0以降のすべての要素を削除)
array.splice(0)
console.log(`配列の要素数: ${array.length}`);
console.log(array);

実行結果 (Node.js 13.12.0 にて確認)

$ node array.js 
配列の要素数: 6
[ 'Tanis', 'Sturm', 'Flint', 'Tasslehoff', 'Caramon', 'Raistlin' ]

配列の要素数: 0
[]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フィボナッチ数列 Ruby Perl JavaScript

はじめに

フィボナッチ数列の記事ってフィボナッチ数列100番目より遅いところですが、勉強中のもっとプログラマ脳を鍛える数学パズル アルゴリズムが脳にしみ込む70問 のアウトプットということでお許しください。

Ruby

rubymemo.rb
@memo = {0 => 1, 1 => 1}
def f(n)
  return @memo[n] if @memo[n]
  @memo[n] = f(n - 1) + f(n - 2)
end

puts f(10) # => 89
rubyarray.rb
N = 10

f = Array.new()
f[0] = f[1] = 1
2.upto(N) do |i|
  f[i] = f[i - 1] + f[i - 2]
end

puts f[N] # => 89

javascript の様にハッシュではなく配列でも実装可能と思われます。

Perl

perlmemo.pl
my %memo = (0 => 1, 1 => 1);
sub f {
  my $n = shift;
  return $memo{$n} if defined $memo{$n};
  $memo{$n} = f($n - 1) + f($n - 2);
}
print f(10), "\n"; # => 89
perlarray.pl
my $n = 10;

my @f;
$f[0] = $f[1] = 1;
for my $i (2..$n) {
  $f[$i] = $f[$i - 1] + $f[$i -2];
}

print $f[$n], "\n"; # => 89
ruby_perl.pl
  return @memo[n] if @memo[n]            # ruby

  return $memo{$n} if defined $memo{$n}; # perl
  return $memo{$n} if exists $memo{$n};  # perl

ruby と異なり、defined関数もしくは exists関数を使用する必要があります。

JavaScript

javascriptmemo.js
var memo = [];
memo[0] = memo[1] = 1;
function f(n) {
    if (memo[n]) return memo[n];
    return memo[n] = f(n - 1) + f(n - 2);
}

console.log(f(10)); // => 89
javascriptarray.js
N = 10;

var f = [];
f[0] = f[1] = 1;
for (var i = 2; i <= N; i++) {
    f[i] = f[i - 1] + f[i - 2];
}

console.log(f[N]); // => 89

javascript に詳しくないので、ハッシュ未使用で実装しています。

まとめ

  • フィボナッチ数列を実装した
  • それぞれの言語に違いがあって面白かった

参照したサイト
フィボナッチ数列
メモ化から学ぶ早期リターン
javascriptのハッシュライブラリを比較する
exists関数 - ハッシュのキーの存在確認

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

途中で戻したり進めたりできるイテレータ

構文解析する際に、文字列イテレート中に戻したりスキップしたりできるイテレータがあれば便利かと思い作ってみました。
参考: https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Iterators_and_Generators

function iter(pattern) {
    let index = 0
    return {
        [Symbol.iterator]: function() {
            return this
        },
        next: function() {
            if (index < pattern.length) {
                return { value: pattern[index++], done: false }
            } else {
                return { done: true }
            }
        },
        fetch: function() {
            return pattern[index]
        },
        back: function() {
            index > 0 && index--
        },
        skip: function() {
            index++
        },
    }
}

実際に利用したコードは長くなってしまったので、ちょっと無理矢理な使用例を示します。

使用例
let it = iter("Helo!!!!!, world!")
for (let c of it) {
    console.log(c)                    // 'Hel'
    if (c === 'l') break
}
it.back()                             // back to 'l'
console.log(it.next().value)          // 'l'
console.log(it.next().value)          // 'o'
while (it.fetch() === '!') it.skip()  // skip '!'
for (let c of it) {
    console.log(c)                    // ', world!'
}
実行結果
H
e
l
l
o
,

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

JavaScript : ブラウザ中央にポップアップウィンドウを開く

今どきポップアップウィンドウなんて時代でもないですが、TwitterやFacebookなどSNSのシェアボタンでは未だに使われており、それとよく併用するコードです。

window.open()は特定の位置を指定してウィンドウを開けます。
ブラウザの位置・広さをもとにブラウザの中央にウィンドウを配置するよう指定します。

function popupWindow(url, name, w, h) {
  url = encodeURI(decodeURI(url));
  wTop = window.screenTop + (window.innerHeight / 2) - (h / 2);
  wLeft = window.screenLeft + (window.innerWidth / 2) - (w / 2);
  window.open(url, name, 'width=' + w + ', height=' + h + ', top=' + wTop + ', left=' + wLeft + ', personalbar=0, toolbar=0, scrollbars=1, resizable=!');
}

//popupWindowへ投げる変数
//url  : ウィンドウに出すURL
//name : ウィンドウ名
//w    : ウィンドウの横幅
//h    : ウィンドウの縦幅
//wTop : 配置する横位置
//wToop: 配置する縦位置
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Apps Scriptで快適にWEBスクレイピングするためのライブラリを作ったら、重すぎて動かなかったお話。

efgrhj.png
ウガアッ!

作るに至った経緯

GASでのWEBスクレイピングといえば、送られてきたhtmlソースをただの文字列として受け取り、正規表現やmatch、split、indexOfなどで無理矢理目的の要素・テキストを削り出してくるもの。一応スクレイピング用のライブラリもあるが、やっていることは対して変わらない。

「我々が求めるものはPythonのbs4のような、CSSセレクタなどで簡単に要素を見つけ出し、情報を抽出するものではなかろうか?」

そんなこんなで火が付いた。作ったプロトタイプもGoogle Chromeの開発者ツール上では、少々レスポンスが遅いもののキチンと想定道理に動いていたから、やれる自信はあった。再帰呼び出しの回数が多すぎて、どうやってもGASでは動かないと気付いた時、私は燃え尽きた。

どういうライブラリだったか

CSSセレクタベースで要素を見つけるもの。最初は親・子孫要素や属性([attr="hogehoge"])などにも対応させていたが、少しでも再帰呼び出しを減らすため、どんどんオプションを削られ、しまいには親要素からの検索も露と消えた。まあ結局動かなかったけど。というわけでこのライブラリ(笑)は、Google Chromeの開発者ツール上でしか、筆者は正常に動かしたことがない。悲しい。せっかく作ったのに。

呼び出し

var h = HTMLparser(`
<html>
<head></head>
<body>
<p class="cls">htmlソーステキスト</p>
...
</body>
</html>
`);
var tag_p = h.search("p.cls");
//[{tagname: "p", attr: {…}, innerText: "htmlソーステキスト", tree: Array(1), parent: {…}}]

呼び出し元のコード

function HTMLparser(html_str) {
    if(!(this instanceof HTMLparser)) {
        return new HTMLparser(html_str);
    }else {
        this.html = [];
        this.attr = {};
        this.tags = {};
        var html_obj = {tree: []};
        //str: htmlソース, ary: htmlのツリー, parent: 親要素, self: HTMLparserのthisの参照用
        function per(str, ary, parent, self) {
            var obj = {tagname: "_Node", attr: null, innerText: "", tree: [], parent: null};
            obj.parent = parent;

            //開始タグを見つける
            var matchTag = str.match(/<([a-zA-Z][^\t\n\r\f \/>\x00]*?)(| [a-zA-Z][^\t\n\r\f>\x00]*?[^\/])>([\s\S]*?)$/);
            //[0]: 全体, [1]: タグ名, [2]: 属性, [3]: その開始タグ以降のテキスト

            function sameTagBothReg(tagname) {
                return new RegExp("(<" + tagname + ">|<" + tagname + " [a-zA-Z][^\t\n\r\f>\x00]*?[^\/]>|<\\/" + tagname + ">)");
            }
            function sameTagStartReg(tagname) {
                return new RegExp("(<" + tagname + ">|<" + tagname + " [a-zA-Z][^\t\n\r\f>\x00]*?[^\/]>)");
            }
            function sameTagEndReg(tagname, count) {
                return new RegExp("(<\\/" + tagname + ">)");
            }
            var attrReg = /([a-zA-Z][^\t\n\r\f >\x00]*=\".*?\")/g;
            var attrReg_g = /([a-zA-Z][^\t\n\r\f >\x00]*)=\"(.*?)\"/g;
            if(matchTag) {
                var attr_obj = {};
                var attr_node_list = matchTag[2].split(attrReg).filter(function(r) {return r.match(attrReg);})
                attr_node_list.forEach(function(r) {
                    //タグの属性を収集
                    var a = r.split(attrReg_g);

                    if(!self.attr[a[1]]) {
                        self.attr[a[1]] = {};
                    }

                    var v;
                    //クラスのみリスト化
                    if(a[1] == "class") {
                        v = a[2].split(" ");
                        v.forEach(function(r) {
                            if(!self.attr[a[1]][r]) {
                                self.attr[a[1]][r] = [];
                            }
                            self.attr[a[1]][r].push(obj);
                        });
                    }else {
                        v = a[2];
                        if(!self.attr[a[1]][v]) {
                            self.attr[a[1]][v] = [];
                        }
                        self.attr[a[1]][v].push(obj);
                    }

                    attr_obj[a[1]] = v;
                });
                obj.attr = attr_obj;
                obj.tagname = matchTag[1];

                //その開始タグの対となる終了タグを見つける
                //もし開始タグがspanだった場合、その開始タグ以降のテキストから、
                //spanの開始タグと終了タグをまとめて探索する
                var st_cnt = 1;//開始タグの数(開始タグはすでに一個あるので、初期値は1)
                var ed_cnt = 0;//終了タグの数
                var sp_idx = 0;//目的の終了タグのインデックス
                var splitted_same_tag = matchTag[3].split(sameTagBothReg(matchTag[1]));
                splitted_same_tag.forEach(function(v, i) {
                    if(sp_idx) {
                        return;
                    }
                    //開始タグがマッチしたら+1
                    else if(v.match(sameTagStartReg(matchTag[1]))) {
                        st_cnt++;
                        return;
                    }
                    //終了タグがマッチしたら+1
                    else if(v.match(sameTagEndReg(matchTag[1]))) {
                        ed_cnt++;
                        //開始タグ数と終了タグ数が一致したら、インデックスを記録
                        if(st_cnt == ed_cnt) {
                            sp_idx = i;
                        }else {
                            return;
                        }
                    }
                });
                //始点からインデックスまでが、そのタグの子要素
                var child = splitted_same_tag.slice(0, sp_idx).join("");
                if(matchTag[1] == "title") {
                    self.title = child;
                }
                //scriptタグの中には稀にhtmlのタグが紛れ込んでいるので、
                //誤マッチ回避のため、scriptタグの中身は切り捨て
                if(matchTag[1] !== "script") {
                    //子要素をターゲットにして自身(obj)を親とし、perを再帰的に呼び出し
                    per(child, obj.tree, obj, self);
                }
                //インデックスから終点までが、そのタグの兄弟要素
                var bro = splitted_same_tag.slice(sp_idx + 1).join("");
                if(bro !== "") {
                    //兄弟要素をターゲットにして自身と同じ親要素(parent)
                    //を親とし、perを再帰的に呼び出し
                    per(bro, ary, parent, self);
                }
            }else {
                obj.tree = [str];
            }

            ary.unshift(obj);

            //もしタグ名が見つからなかったら(_Nodeのままなら)、
            //自身と親・先祖のinnerTextに中身の文字列を追加
            if(obj.tagname == "_Node") {
                Text(obj, obj.tree.join(""));
                function Text(o, txt) {
                    o.innerText += txt;
                    if(o.parent) {
                        Text(o.parent, txt);
                    }
                }
            }else {
                if(!self.tags[obj.tagname]) {
                    self.tags[obj.tagname] = [];
                }
                self.tags[obj.tagname].push(obj);
            }

        }
        per(html_str.replace(/<!--[\s\S]*?-->/g, ""), this.html, null, this);
    }
}

//検索機能(最も削り取られた部分)
HTMLparser.prototype.search = function(selector) {
    var sel = sel_parse(selector);
    //終端の1要素のみ読み取る
    //CSSセレクタは「右から」読み取るのが効率的
    return check_tree(sel[0], this);

    function check_tree(s, self) {
        var r = [];
        var _tag = s.tag;
        if(_tag) {
            if(self.tags[_tag]) {
                for(var t of self.tags[_tag]) {
                    var _atr = t.attr;
                    if(s.class.length) {
                        if(_atr.class) {
                            var _chk = false;
                            for(var cls of s.class) {
                                if(_atr.class.indexOf(cls) < 0) {
                                    _chk = true;
                                    break;
                                }
                            }
                            if(_chk) {continue;}
                        }else {
                            continue;
                        }
                    }
                    if(s.id.length) {
                        if(s.id[0] !== _atr.id) {
                            continue;
                        }
                    }
                    r.push(t);
                }
            }
        }else {
            var c_ary = [];
            var i_ary = [];
            var a_ary = [];//削り取られた残滓

            if(s.class.length) {
                var _chk = true;
                var clsList = Object.keys(self.attr.class);
                for(var cls of s.class) {
                    if(clsList.indexOf(cls) < 0) {
                        break;
                    }else {
                        if(!c_ary.length) {
                            c_ary = self.attr.class[cls];
                        }else {
                            c_ary = c_ary.concat(self.attr.class[cls]).filter(function(x, i, self) {
                                return self.indexOf(x) === i && i !== self.lastIndexOf(x);
                            });
                        }
                    }
                }
            }
            if(s.id.length) {
                var idList = Object.keys(self.attr.id);
                if(-1 < idList.indexOf(s.id[0])) {
                    i_ary = self.attr.id[s.id[0]];
                }
            }
            if(s.attr.length) {
                var _chk = false;
                for(var a of s.attr) {
                    var _k = a.atr;
                    var _m = false;
                    if(_k.match(/(\*|\^|\$)$/)) {
                        _m = _k.match(/(\*|\^|\$)$/)[1];
                        _k = _k.replace(/(\*|\^|\$)$/, "");
                    }
                }
            }
            var full_cnt = 0;
            if(c_ary.length) {
                full_cnt++;
                r = r.concat(c_ary);
            }
            if(i_ary.length) {
                full_cnt++;
                r = r.concat(i_ary);
            }
            if(a_ary.length) {
                full_cnt++;
                r = r.concat(a_ary);
            }
            if(1 < full_cnt) {
                r = r.filter(function(x, i, self) {
                    return self.indexOf(x) === i && i !== self.lastIndexOf(x);
                });
            }
        }
        return r;
    }

    //CSSセレクタの解析用
    function sel_parse(sel) {
        if(sel.match(/ ?[\+\~] ?/g)) {
            throw Error('You cannot use Adjacent sibling combinator "+/~".');
        }
        else if(sel.match(/\:(nth-child\(|nth-of-type\(|not\(|first-child|first-of-type|last-child|last-of-type)/g)) {
            throw Error('You cannot use Pseudo-elements like ":nth-of-type()"');
        }
        var sp = sel.split(/( ?> ?|(?<=[a-zA-Z0-9\]\_\-]) (?=[a-zA-Z\[\.\#\_]))/g);
        var a = [], nxt = false;
        sp.forEach(function(s, idx) {
            if(s.match(/^ $/g)) {
                return;
            }
            else if(s.match(/^ ?> ?$/g)) {
                nxt = true;
                return;
            }
            var _o = {"tag": null, "class": [], "id": [], "attr": [], "next": false};
            if(nxt) {
                _o.next = true;
                nxt = false;
            }
            var _s = s.split(/(\[.*?\]|(?<=(?:[a-zA-Z\]]|^))(?:\.|\#)[a-zA-Z\_][a-zA-Z0-9\_\-]*)/g).filter(function(r) {return r;});
            _s.forEach(function(p) {
                if(p.match(/^\#/)) {
                    _o.id.push(p.replace(/\#/, ""));
                }
                else if(p.match(/^\./)) {
                    _o.class.push(p.replace(/\./, ""));
                }
                else if(p.match(/^[a-zA-Z]/)) {
                    _o.tag = p;
                }
                else if(p.match(/^\[(.*?)(?:\=(?:\"(.*?)\"|\'(.*?)\')|)\]/)) {
                    var _m = p.match(/^\[(.*?)(?:\=(?:\"(.*?)\"|\'(.*?)\')|)\]/);
                    _o.attr.push({"atr": _m[1], "que": _m[2] ? _m[2] : ""});
                }
            });
            a.unshift(_o);
        });
        return a;
    }
}

ちなみに、HTMLparserオブジェクトの中身は以下の通り。
dfdgfh.png
treeにはその要素の子要素のリストが、parentにはその要素の親要素への参照が渡されている(図では一番外側のタグのため参照が無い)。下のattrとtagsは検索用の参照のリストが詰まっている。

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

TypeORM x AuroraDataAPI - タイムゾーン問題への対処

TypeORM x AuroraDataAPI - TIMEZONE問題への対処

ローカルタイムゾーンがJSTだとしたとき、
DBに書き込まれる時刻が9時間未来になる(JSTのまま書かれてしまう)
DBから読み込まれる時刻が9時間過去になる(JST扱いで読まれてくる)問題への対処です。

前置き

本記事の解決法では、TypeORMの外側で自力で時刻を補正するため、
ライブラリ(TypeORMとAuroraプラグイン)の仕様が変わったり、バグが治った場合は毒になりえます。
そのへんの影響まで自分で管理してやんよシュババってひとむけです。

環境

  • Aurora Serverless MySQL 5.6
    • time_zone, system_time_zone: UTC
  • TypeORM 0.2.24
  • typeorm-aurora-data-api-driver 1.1.8
  • Node 10
    • timezone(TZ環境変数): Asia/Tokyo

上記環境 (TypeORM x AuroraDataAPI) での観測ですが、
DBが違う場合でも同じ症状には同じ対策が適用可能なはずです。

原因

おそらく、 typeorm-aurora-data-api-driver に関する、以下のバグです。

  • Data型を日付時刻Stringに変換して書き込む処理
    • TZ環境変数にもとづいて、出力を補正していない
    • 結果、書き込まれる時刻がズレる(JSTなら+9時間されてる)
    • 配布物でいうと、 typeorm-aurora-data-api-driver.umd.jsformatDate()
      • リポジトリでいうとどこかは、軽く見たけどわからなかった
  • 日付時刻StringをDate型に変換して読み出す処理
    • TZ環境変数にもとづいて、入力を補正していない
    • 結果、読み出される時刻がズレる(JSTなら-9時間されてる)
    • ソースで具体的にどこに該当するかは、ざっくりデバッグしたけどわからなかった。
      • query() 中のresult補正かけてるあたりのどこかでやるべき、なのかも。

結果、JST環境からの読み書きだけ見ると、同じだけズレるので 書き込み前=読み込み前 となっていますが、
CreatedDateColumn() 等でDB側で生成された時刻を読みだした場合ズレたり、
そもそもDB直接覗くとUTCのくせにJST扱いで記録されてたりと、
いろいろ気持ち悪い現象が発生してしまっていました。

(補足)原因はわかってるけどコントリビュートしたくない

ざっくり調べた結果、上記原因が解消されれば治るは治るはずなんだけど、
以下の背景からコストが高くなりそうなのでしてません。
issueくらいは投稿してあげてもいいかも。手が空いたらやる。

  • それがTypeORM由来なのかプラグイン由来なのか正直わかりきらない
  • TypeORMのプラグインにコントリビュートするにはTypeORMの知識も必要である
  • プラグインの更新が活発でない(8ヶ月前とか)

対策

@Column() の option である transformer を使い、
書く直前と読んだ直後に自力で時刻を補正します。
Entity インスタンス自体の時刻には影響しないよう気を使っています。

orm-opts.ts
import moment, { Moment } from "moment-timezone";
import { ColumnOptions, EntityMetadata, EntitySchema } from "typeorm";

/**
 * Date型のoffsetに基づいて読み書きされているので
 */
const TYPEORM_LOCAL_OFFSET = new Date().getTimezoneOffset();
/**
 * DBのタイムゾーンは基本UTCの前提とする。
 * 異なる場合は、setCorrectOffset()で任意の補正時間をセットする。
 */
const DB_OFFSET = moment.tz("UTC").utcOffset();

export class OrmOpts {
  /**
   * TypeORM ごしにDatetime型を書く直前、読んだ直後に、この分数だけ時刻をずらす。
   * DB時刻がUTCにも関わらず、JSTで(+09:00のまま)書き込まれたりする問題への対処のため。
   *
   * 初期値は 0(UTC) - TZ環境変数のoffset(JSTなら540)
   */
  static CORRECT_OFFSET = DB_OFFSET - TYPEORM_LOCAL_OFFSET;

  static MOMENT: ColumnOptions = {
    type: "datetime",
    transformer: {
      from: (from: Date) => {
        if (!from) return from;
        const fromM = moment(from).add({
          minutes: OrmOpts.CORRECT_OFFSET
        });
        return fromM;
      },
      to: (to: Moment) => {
        if (!to) return to;
        const toD = to
          .clone()
          .subtract({ minutes: OrmOpts.CORRECT_OFFSET })
          .toDate();
        return toD;
      }
    }
  };

  static DATE: ColumnOptions = {
    type: "datetime",
    transformer: {
      from: (from: Date) => {
        if (!from) return from;
        const fromM = (OrmOpts.MOMENT.transformer as any).from(from);
        return fromM.toDate();
      },
      to: (to: Date) => {
        if (!to) return to;
        to = (OrmOpts.MOMENT.transformer as any).to(moment(to));
        return to;
      }
    }
  };
}
types.ts
export class TestEntity extends BaseEntity {
  @PrimaryGeneratedColumn("increment") seq: number;
  @CreateDateColumn(OrmOpts.DATE) createdAt: Date;
  @CreateDateColumn(OrmOpts.MOMENT) createdAtM: Moment;
  @Column(OrmOpts.DATE) dateAt: Date;
  @Column(OrmOpts.MOMENT) dateAtM: Moment;
}

結果

これで、ローカルのTIMEZONEに関わらず、
正しい時刻(このコードではUTC)でDBに書き込まれるようになります。
読み出すときはローカルTIMEZONEに補正されて読まれるようになります。

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

Vue.js テンプレート制御ディレクティブ まとめ

はじめに

Vue.jsでTO DOアプリを作るの続きです。

今回はディレクティブを深掘りしていきます。

v-once

  • 初回だけテキストバインディングを行う。
  • 初回以降は静的なコンテンツとして扱う。
  • 描画更新のパフォーマンスを上げたいときに利用できる
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  },
  methods: {
    click(e) {
      // 文字列を反転するメソッドを定義
      this.message = this.message
        .split('')
        .reverse()
        .join('')
    }
  }
})

以下はv-onceを定義しているので、初回だけテキストバインディングを行っている。

初回以降は静的なコンテンツとして扱われるので、ボタンをクリックしてもメソッドが発火しない。

<p v-once> <!-- v-onceを定義 -->>
  {{ message }} <!-- Hello Vue.js -->
</p>
<button v-on:click="clickHandler">
    文字が反転
</button>

v-pre

  • 要素と全ての子要素のコンパイルをスキップする。
  • 生のマスタッシュタグが表示される。
<p v-pre>
  {{ message }} <!-- {{ message }} -->
</p>

v-html

  • プレーンなHTMLを挿入する。
  • 指定した要素のinnerHTMLを更新できる。
  • 主にサーバーサイドから取得したHTMLを表示したいときに使う。

dataオプションのプロパティにHTMLをセットする。

data: {
  messageHtml: 'Hello <span style="color:red;">Vue.js!</span>'
}

v-htmlディレクティブで展開する。

<p v-html="messageHtml"></p>

※XSS脆弱性を引き起こす恐れがあるため、サービスを利用するユーザーが入力したコンテンツには使用しないこと。

v-cloak

ページを表示開始してから、インスタンスの作成が終わるまでにマスタッシュタグなどのコンパイル前にテンプレートが表示されてしまうのを防ぐ。

<p v-cloak> <!-- v-cloakを定義 -->
  {{ message }}
</p>

v-cloakに対してdisplay: noneを設定する。

[v-cloak] {
  display: none;
}

v-text

dataオプションのプロパティをマスタッシュ構文以外で表示したい時。

使い道あんまりなさそう。

<p v-text="message"></p> <!-- Hello Vue.js! -->

JavaScript式

データバインディング内部ではJavaScript式を利用する事ができる。

data: {
  message: 'Hello Vue.js!',
  number: 100,
  ok: true
}
<p>
  {{ number +  1}} <!-- 101 -->
  {{ message.split('').reverse().join('') }} <!-- !sj.euV olleH -->
</p>

フロー制御は三行演算子を利用する。

<p>
  {{ ok? 'YES' : 'NO' }} <!-- YES -->
</p>

以下は式では無く文なのでエラーになる。

<p>
  {{ var x = 1 }} <!-- error -->
</p>

フィルタ(ローカル)

Vue.jsでは式の終わりにフィルタを追加することができる。

data: {
  price: 29800 //dataオプションにプロパティを設定
},
filters: { // filterにメソッドを追加
  numberFormat(value) {
    return value.toLocaleString()
}

{{ 式 | フィルタ }}でfilterを実行する

<p>
  {{ price | numberFormat(price) }} <!-- 29,800 -->
</p>

v-bindディレクティブで使用する場合

<input type="text" v-bind:value="price | numberFormat(price)">

フィルタ(グローバル)

フィルタはグローバルにも定義できる。

グローバルに定義するにはVueインスタンスを生成するよりも前に記述する。

// グルーバルフィルタを定義
Vue.filter('numberFormat', function(value) {
  return value.toLocaleString()
})

// Vueインスタンス生成
var app = new Vue({
  el: '#app',
  data: {
    price: 29800
  }
})

フィルタの連結

フィルタは複数連結できる。

フィルタで加工した返り値に対して、さらにフィルタをかけることができる。

<p>
  {{ A | filterX(A) | filterY(A) }}
</p>

フィルタの引数

フィルタで引数を利用する。

例として、長いテキストを省略して...を末尾に付与するフィルタを作成する。

その際、表示する文字列末尾に付与する文字を引数で指定できるようにする。

dataオプションに長い文字列を持ったtextプロパティを用意する。

text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt

続いてフィルタを定義する。

JavaScriptのメソッドsubstringは第一引数が開始位置、第二引数が抽出する文字数を指定する。

以下のフィルタは、引数で文字列抽出する文字数連結する文字を受け取る。

Vue.filter('readMore', function(text, length, suffix) {
  return text.substring(0, length) + suffix
})

実行してみる。

<p>
  <!-- textは引数に含まないことに注意 -->
  {{ text | readMore(10, '...') }} <!-- Lorem ipsu... -->
</p>
<p>
  {{ text | readMore(5, '***') }} <!-- Lorem*** -->
</p>

v-bind省略記法

v-bindは省略記法が存在する。

プロジェクト内で統一するのが望ましい。

以下は同じ結果が出力される。

<!-- 完全な構文 -->
<a v-bind:href="url">

<!--  省略記法-->
<a :href="url">

更新履歴

:zap:Vue.jsの基本的な使い方まとめ
:zap:Vue.jsでTO DOアプリを作る
:zap:Vue.js テンプレート制御ディレクティブ まとめ :point_left:今ココ
:zap:Vue.js 算出プロパティとメソッドの違い

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

JavaScript学習 - 関数 -

JavaScriptにおける『関数』

関数そのものの仕組み(入力に対してなんらかの処理を行い、その結果を返す)はJavaと同じ。
JavaScriptでは関数はデータ型の一種として扱うのがJavaとの大きな違い。

・『関数の引数に名前を指定する』
・『複数の戻り値をそれぞれ指定した変数に代入する』 ...etc

型推論もあるので、かなり柔軟に関数や変数の設定が出来る。

関数の定義

主に以下の4つ(3つの)の方法がある

function命令による定義
・ functionコンストラクターによる定義
関数リテラルによる定義(無名関数)
アロー関数による定義

これら4つのうちfunctionコンストラクターは外部から任意のコードが実行できてしまう可能性があるので使用は推奨されていない、原則としてfunction命令・関数リテラルあるいはアロー関数のどれかで定義することになるので実際には3つの方法ということになる。

function命令による定義

関数の定義の最も基本的な方法。

function命令
function hoge(hage, ...){
   //任意の処理
   return piyo
}

関数の実装といえば基本的にはこれ。
関数の中に関数を実装したり、処理の中で一回限りの処理をする関数を実装したい場合は後述の関数リテラルアロー関数などを使う。

関数リテラル・アロー関数

匿名関数無名関数とも呼ばれる。
変数として代入したり、関数の引数や戻り値として利用できるので非常に柔軟なコーディングができるようになる。

関数リテラル
 var hoge = function(hage, piyo){
   return hoge * piyo
}

アロー関数を利用する事でfunctionキーワードを省略できるのでさらにシンプルに記述できるようになる。

アロー関数
 var hoge = (hage, piyo) => return hoge * piyo;

引数が1つの場合は引数の()も省略することが出来る。
省略する際は引数が0個の場合は省略できないので注意。

変数のスコープ

変数がコードのどの部分から参照できるのかを表す概念的なもの。
以下の3種類のスコープがある。
ローカルスコープ
グローバルスコープ
ブロックスコープ

ローカルスコープとptにおける『関数』
関数そのものの仕組み(入力に対してなんらかの処理を行い、その結果を返す)はJavaと同じ。
JavaScriptでは関数はデータ型の一種として扱うのがJavaとの大きな違い。

・『関数の引数に名前を指定する』
・『複数の戻り値をそれぞれ指定した変数に代入する』 ...etc

型推論もあるので、かなり柔軟に関数や変数の設定が出来る。

関数の定義

主に以下の4つ(3つの)の方法がある

function命令による定義
・ functionコンストラクターによる定義
関数リテラルによる定義(無名関数)
アロー関数による定義

これら4つのうちfunctionコンストラクターは外部から任意のコードが実行できてしまう可能性があるので使用は推奨されていない、原則としてfunction命令・関数リテラルあるいはアロー関数のどれかで定義することになるので実際には3つの方法ということになる。

function命令による定義

関数の定義の最も基本的な方法。

function命令
function hoge(hage, ...){
   //任意の処理
   return piyo
}

関数の実装といえば基本的にはこれ。
関数の中に関数を実装したり、処理の中で一回限りの処理をする関数を実装したい場合は後述の関数リテラルアロー関数などを使う。

関数リテラル・アロー関数

匿名関数無名関数とも呼ばれる。
変数として代入したり、関数の引数や戻り値として利用できるので非常に柔軟なコーディングができるようになる。

関数リテラル
 var hoge = function(hage, piyo){
   return hoge * piyo
}

アロー関数を利用する事でfunctionキーワードを省略できるのでさらにシンプルに記述できるようになる。

アロー関数
 var hoge = (hage, piyo) => return hoge * piyo;

引数が1つの場合は引数の()も省略することが出来る。
省略する際は引数が0個の場合は省略できないので注意。

変数のスコープ

変数がコードのどの部分から参照できるのかを表す概念的なもの。
以下の3種類のスコープがある。

ローカルスコープ :関数の外で宣言した変数
グローバルスコープ :関数の中で宣言した変数
ブロックスコープ :if文などの{}内で宣言した変数

ローカル・グローバルなどと名がついているので身構えてしまうが基本はJavaと同じである。
ES2015にてブロックスコープが実装されたため、挙動は基本的にJavaと似たような感じになった。

引数

主に以下の3つの機能があげられる。

・引数のデフォルト値
・可変長引数
・名前付き引数

ES2015までは実装にオブジェクトを必要としたが、ES2015からは実装にあたっての記法が簡略化され、簡素なコードでより柔軟なコーディングが可能になった。

引数のデフォルト値

『仮引数 = デフォルト値』の形式で記述することで仮引数にデフォルトの値を設定できる

デフォルト値の設定
 function gethoge(hage = 1, piyo = 1){
   return hage * piyo / 2
}  

if文などで条件文気してやる必要がなく、簡単に実装できる。
例外をスローするだけの関数をデフォルト値として利用することで必須の引数を宣言することもできる。
hage = 1, piyo = hoge * 2 のように引数のデフォルトに仮引数の値を利用する事もできる。
仮引数の値を利用する際は自身より後に定義された引数は使えないので注意(上記のケースの場合、hageのデフォルト値にpuyoの値を使おうとするとエラーが起こる)。
また、関数呼び出しの引数にnullを使うとデフォルト値ではなくnullが代入してしまう点、デフォルト値を設定した引数は引数の末尾に追加しないと思わぬ値が入ってしまうことがある事にも注意。

可変長引数の関数

仮引数の前に... を入れることで可変長引数となる。
これによって、任意の数の引数を配列としてまとめて受け取る事が出来るようになる

可変長引数の定義
 function hoge(...piyos){
   //任意のコード
   return piyos
}  

配列として受け取るのでlengthやpush/shiftなどのすべての配列操作が可能。
また実引数で ... 演算子を利用することで、配列内のすべての数値を展開可能。Math.maxメソッドなど配列の受け取りを許容していないメソッドに有効。

名前付きの引数

{プロパティ名 = デフォルト値}の形で記述する事で、名前付きの引数を簡素に表現する事が出来る。

引数に名前を付けて定義
 function hoge({hage = 1, piyo = 1}){
   //任意のコード
   return piyos
}  

一度オブジェクトリテラル{}として渡された引数を分解して個別の値としてアクセスしている、やっていることはフィールドの値を取り出すのに近い
オブジェクトなので複数値を持つこともできる、値を取り出すときはプロパティ名を指定してやると値を取り出せる。呼び出し側のコードを変えることなく取り出す値を変えたりする時に便利。

長くなってしまったので今回はここまで

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

WebUSBでFeliCaのIDmとMIFAREのIDも読み込む

はじめに

WebUSBでFeliCaの一意なIDであるIDmを読む / Qiita
こちらの記事を参考に、FeliCaだけではなく、MIFAREのIDも拾いたい。
いや、むしろ、どちらも拾えるものをということで、公開していただいているgithubをフォークして、書き加えてみましたので、
経緯、やったこと、調べたことをこちらにまとめておこうと思います。

結果のソースだけあればいいという人は、完成物をDLしてご利用ください。

注意

それらしく動いてますが、pythonは読んだことないわ、仕様書は難しくて頭痛くなるわで、
サンプルのパケットを解析して、コピペするという作業をしてますので、
追加した内容については、どうして動いているのかということは分かっていません。
使う際は自己責任で。もしくはコメントを頂ければ。

完成物

https://github.com/marioninc/webusb-felica
元ネタと同様で、権利フリーです。

要件

Google Chrome最新(2020/04/01現在)
PaSori(RC-S380)
MIFARE Classicカード
Windows10(MacOSや、Linuxですと、ドライバーを書き換えず動くようです) (Windowsで)WebUSBでPasoriを扱ってみる / Qiita

元ソースとの変更箇所

1.Google Chromeのバージョンアップでの仕様変更対応

最近(2019/03/27)、自分で書いた方法に従って実験してみたのですが、Windowsで動作させる事が出来なくなっていました。
下記の様なエラーが発生して、正常に動作しません。
原因は不明なのですが、変わった事と言えば、ChromeのVersionが上がった位なので、それが何か影響しているのでしょうか…。
後は、Windows 10のUpdateが入ったこと、エディションがHomeからProに変わった事くらいですが、それは関係無いと思っています。
因みに解決策は下記の通り。
ソースコード(html)をローカルに保存して、185行目位にある、protocolCodeの代わりに、productIdを指定します。

引用元:(Windowsで)WebUSBでPasoriを扱ってみる | 補足(2019/3/27追記) / Qiita
についての修正

2.今回の本題

MIFAREのID取得部分
await send(device,[0x00,0x00,0xff,0xff,0xff,0x03,0x00,0xfd,0xd6,0x06,0x00,0x24,0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd707002200

  await send(device,[0x00,0x00,0xff,0xff,0xff,0x06,0x00,0xfa,0xd6,0x00,0x02,0x03,0x0f,0x03,0x13,0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd701002800

  await send(device, [0x00, 0x00, 0xff, 0xff, 0xff, 0x28, 0x00, 0xd8, 0xd6, 0x02, 0x00, 0x18, 0x01, 0x01, 0x02, 0x01, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x08, 0x08, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0e, 0x04, 0x0f, 0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, 0x06, 0x4b, 0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd703002600

  await send(device, [0x00,0x00,0xff,0xff,0xff,0x0c,0x00,0xf4,0xd6,0x02,0x01,0x00,0x02,0x00,0x05,0x01,0x00,0x06,0x07,0x07,0x0b,0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd703002600

  await send(device, [0x00,0x00,0xff,0xff,0xff,0x05,0x00,0xfb,0xd6,0x04,0x36,0x01,0x26,0xc9,0x00]);
  await receive(device, 6);
  await receive(device, 20);
  // <<< 0000ffffff0900f7d705000000000804001800

  await send(device, [0x00,0x00,0xff,0xff,0xff,0x06,0x00,0xfa,0xd6,0x02,0x04,0x01,0x07,0x08,0x14,0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd703002600

  await send(device, [0x00,0x00,0xff,0xff,0xff,0x06,0x00,0xfa,0xd6,0x02,0x01,0x00,0x02,0x00,0x25,0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd703002600

  await send(device, [0x00,0x00,0xff,0xff,0xff,0x06,0x00,0xfa,0xd6,0x04,0x36,0x01,0x93,0x20,0x3c,0x00]);
  await receive(device, 6);
  let idt = (await receive(device, 22)).slice(15, 19);
  // <<< 0000ffffff0c00f4d705000000000800000000490c00
  if (idt.length > 2) {
    let idtStr = '';
    for (let i = 0; i < idt.length; i++) {
      if (idt[i] < 16) {
        idtStr += '0';
      }
      idtStr += idt[i].toString(16);
    }
    idmMessage.innerText = "Card Type : MIFARE  カードのID: " + idtStr;
    idmMessage.style.display = 'block';
    waitingMessage.style.display = 'none';
    return;
  } else {
    idmMessage.style.display = 'none';
    waitingMessage.style.display = 'block';
  }

の2か所です。

なぜ改造したのか

この度、WEB上で動く勤怠管理システム上で、ICカードタッチで出退勤をしたいという話をいただきました。
Google先生に聞いてみると元ネタの記事が出てきたので、
これで簡単にできそうだぞと、とりあえずPaSoriを用意して、社員証をかざしても、あれ、IDが出ない……

このIDカードFeliCa対応のものではなく、MIFARE Classicと呼ばれる規格のもののようで、
使い捨てを前提として、IDが一意ではない(IDが被らないように管理する機関がない)カードらしいのです。
このことが原因で、IDを取得するためのリクエストコマンドも違えば、返ってくる応答も違うという具合で、FeliCa的にはIDmが取得できなかっただけでした。

さて、どうしたものかという話

そもそもデバイス関係は専門外で、JIS規格の仕様書は入手しても、不慣れなので読むのが大変。
元ネタの元ネタになっているライブラリはpythonで書かれているようで、pythonも触ったことがない。
nfcpyは、Windows で nfcpy のセットアップ / Qiitaを見ながら、
とりあえず、動くようにはなりましたが、元ネタの記事みたいに、
ライブラリを読み解いて自分のものにはできず、うまく実装までこぎつけられませんでした。

ライブラリが読めないなら、パケットを読めばいいじゃない

WEBUSBというものは、直接、パケットを叩くかなり低レイヤーな印象のものです。とどこかの記事で書かれていました。
つまり、意味はちゃんと分かっていなくても、通信さえマネできれば動くということではないか。
そう。ライブラリが読めないなら、直接パケットを読めばいいじゃない。
というわけで、パケットを盗み見るなら、おなじみWireshark
実は、標準の機能だけで、TCPの通信だけではなくUSBのパケットも読めるそうです。

そうとなれば、
1. nfcpyのサンプルにある、tagtool.pyを実行して、
2. Wiresharkで監視。
3. 手元にあったSuicaをタッチ。
4. もう一度、tagtool.pyを実行して、
5. Wiresharkで監視。
6. MEFAREの社員証をタッチ。
7. 適当にデータを加工して、比較する。

読んだ結果(MIFARE)
out // FeliCaのIDmを取得するコマンド
0000ffffff0a00f6d6046e000600ffff0100b300
in // カードがない状態と同じ
0000ffffff0600fad70580000000a400

out
0000ffffff0300fdd606002400
in
0000ffffff0300fdd707002200

out
0000ffffff0600fad60002030f031300
in
0000ffffff0300fdd701002800

out
0000ffffff2800d8d6020018010102010300040005000600…
in
0000ffffff0300fdd703002600

out
0000ffffff0c00f4d602010002000501000607070b00
in
0000ffffff0300fdd703002600

out // カードを識別しているコードぽい
0000ffffff0500fbd604360126c900
in //MIFAREを置いているときの状態
0000ffffff0900f7d705000000000804001800

out
0000ffffff0600fad602040107081400
in
0000ffffff0300fdd703002600

out
0000ffffff0600fad602010002002500
in
0000ffffff0300fdd703002600

out // MIFAREのIDを取得するコード
0000ffffff0600fad604360193203c00
in // *がMIFAREのID
0000ffffff0c00f4d7050000000008**********0c00

こんな感じになっていました。
outがRC-380に送信しているパケット
inがnfcpyに返信されたパケットです。

…にっているところは、ログを書き出したときに、長すぎて省略されてしまったものです。
ソースコードに乗せてあります。

ここまでの部分では、待機状態でループ処理が走っています。

下から3番目の
0000ffffff0600fad602040107081400から3つは、Suicaや、タッチしていない状態では、一度も流れていないコマンドでした。
その上のカードを識別しているコードぽいの部分で、初めて返信パケットに変化があり、そこから分岐がされているようです。

カードを識別しているコードぽい
では、未検出時とSuicaの時は、0000ffffff0600fad70580000000a400でした。

実装

今回は、決済をしたいわけでも、なにかデータを書き込みたいわけでもないので、IDが読めれば十分でした。
ですので、せっかく分岐点を見つけたのですが、FeliCaの読み込みに失敗したら、MIFAREのIDを読み込んでみるという感じで実装しました。
FeliCaのIDmを読んだ直後にMIFAREのIDを読みに行っても、IDは返ってこなかったので、
上の結果のFeliCaのIDmを取得するコマンドのすぐあとから、コピペして、実装しました。

感想など

今回はかなり無理やりな感じがします。よくわかってないけど動いたはよろしくないですね。
実際問題、IDを見るだけならこれでもいいかもしれませんが、読み書きをする場合はちゃんと仕様書を読まないといけないなと。

pythonのライブラリを読み解こうとしたり、ICカードの仕様書を読み解こうとしたり、USBのパケットをのぞいたり……
色々なことがやって見れたので面白くはありましたが、ちゃんと作らないと規模が大きくなると使えないなあ。

解説など、いただける方がいれば、拝読させていただきます。

使用したもの

python3
nfcpy
libusb-1.0.dll
Wireshark
Zadig

参考文献

WebUSBでFeliCaの一意なIDであるIDmを読む / Qiita
(Windowsで)WebUSBでPasoriを扱ってみる / Qiita
WebUSBことはじめ / Qiita
WiresharkでUSBメモリのパケットキャプチャしてみた! / LoT ラブオーティー
Windows で nfcpy のセットアップ / Qiita

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

Next.js + Firebase Hosting + SSR 環境でBasic認証をかける

概要

Next.js + Firebase Hosting + SSR 環境でBasic認証をかけようという記事。

Next が持っているメソッドだけでは対応できなかったので、Express も使用します。

また、Next.js + Firebase Hosting + SSR 環境については、
with-firebase-hosting-and-typescript こちらのテンプレートを利用します。

Firebase を利用する関係上 node.js のバージョンは10系を使用します。
firebase-toolsは最新を使用しましょう。

手順

Next.js + Firebase Hosting + SSR 環境のセットアップ

README.md を参考にセットアップしてみてください。

yarn create next-app --example with-firebase-hosting-and-typescript with-firebase-hosting-and-typescript-app

モジュールのインストール

  • express
  • @types/express
  • next-routes
  • basic-auth-connect
yarn add express next-routes basic-auth-connect
yarn add -D @types/express

エラー対応

Error: > Couldn't find a `pages` directory. Please create one under the project root

2020/04 現在ではデプロイ時に上記エラーが出るため、functionsを修正します。

src/functions/index.ts
const app = next({ dev, conf: { distDir: 'next' } })

src/functions/index.ts
const app = next({ dev: false, conf: { distDir: 'next' } })

実装

Express 側でベーシック認証を通して、 Next のアプリに流します。

src/functions/index.ts
import * as functions from 'firebase-functions';
import next from 'next';
import express from 'express';

/* eslint-disable @typescript-eslint/no-var-requires */
const routes = require('next-routes');
const basicAuth = require('basic-auth-connect');

const USERNAME = 'user';
const PASSWORD = 'password';

const server = express();

const app = next({ dev: false, conf: { distDir: 'next' } });
const handler = routes().getRequestHandler(app);
server.use(basicAuth(USERNAME, PASSWORD));
server.get('*', (req, res) => handler(req, res));

export const nextApp = functions.https.onRequest(server);

Firebase Project の設定

<project-name-here> こちらを自身のプロジェクト名に変更します。

{
  "projects": {
    "default": "<project-name-here>"
  }
}

実行

localで実行

firebase-emulators を使ってローカルでもBasic認証の確認ができます。

Build

yarn preserve

Run

firebase emulators:start

デプロイ

firebase deploy

以上 ☺️

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

初めてのVue.js備忘録 vol.01

はじめに

今回は、エンジニアになって約半年の私がjavascriptのフレームワークであるVue.jsを使って開発を行ったので、そのVue.jsの基本的な記述について備忘録としてまとめます。

データバインディング

テキストのバインディング

  • データのバインディグのもっとも基本的な形 マスタッシュ {{ }}を利用したもの
  • 以下のような使い方ができます。
App.vue
 <template lang="pug">
    #app
        span {{ msg }} //hello
 </template>
 <script>
    export default {
        data(){
            return{
                msg:"hello"
        }
    }
 </script>
App.vue
<template lang="pug">
  .app
    span {{inc}} //numに +1された数字がバインディングされる
</template>
<script>
export default {
  data(){
    return{
      num:10
    }
  },
  computed:{
    inc(){
      return this.num + 1
    }
  }
};
</script>

属性のバインディング

  • {{ }}マスタッシュはHTML属性の内部では使えない
  • 代わりに v-bindディレクティブを使用する
App.vue
  <template lang="pug">
    #app
        span(v-bind:class="dynamicClass") {{ msg }}
  </template>
  <script>
    export default {
        data(){
            return{
                msg:"hello",
                dynamicClass:"hoge"
        }
    }
  </script>
  <style>
  .hoge{
    display:inline-block;
  }
  .fuga{
    display:none;
  }
  </style>

dynamicClassの値をhoge⇨fugaに変更すると、spanのクラスが変更され、非表示になります。

親子間のデータの受け渡し

  • Vue.jsでの親子間のデータの受け渡しは、『props down, events up』
  • 親はプロパティを経由してデータを子に伝える
  • 子はイベントを経由して親にメッセージを送る

という認識でいます。

以下のように子コンポーネントでpropsオプションを指定します。

CustomInput.vue
<template>
...
input(type="text" :placeholder="customPlaceholder" @input="updateValue")
...
</template>
<script>
export default = {
 props:{
  customPlaceholder:{
   type:String,
   default:""
  }
 },
 methods: {
  updateValue (e) {
   this.$emit('input', e.target.value)
   this.$emit('change', e.target.value)
   }
  }
}
</script>
CustomInput.vue
this.$emit('input', e.target.value)

この部分で子コンポーネントからinputに入力されたvalueを親に渡しています。

親コンポーネントでは、子に渡すデータを指定、inputEmailの値が子のコンポーネントに渡されます。

Form.vue
<template>
 custom-input(:customPlaceholder="inputEmail")
</template>
<script>
import CustomInput from "./components/CustomInput"
export default = {
  components:{
   CustomInput
 },
 data(){
  return{
   inputEmail: "メールアドレスを入力してください"
  }
 }
}
</script>

だいぶ掻い摘んで書いてみましたが、書き方どうだっけ?的なときのために。

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

babelの設定ファイルをTypeScriptで書く

表題の通りです。
babel の設定ファイルといえば .babelrc babel.config.js での記述が一般的だと思います。これを babel.config.ts に記述できるようにします。

1. 必要モジュールのインストール

とりあえず webpack 前提で必要最低限なものだけインストールします。

npm i -D webpack webpack-cli typescript ts-node @babel/core @types/babel__core babel-loader @babel/preset-env

ほとんど説明するほどのものではないですが、重要なのは @types/babel__core です。
ここに設定ファイルで使用する型情報が載っています。

2. TypeScriptの設定

tsconfig.json を書きます。

tsconfig.json
{
  "ts-node": {
    "compilerOptions": {
      "module": "commonjs",
      "target": "es5",
    },
  },
}

ここでは ts-node の設定だけ行います。

3. babelの設定

ここからが本題です。 babel.config.ts を書きますが、 設定の読み込み方によって書き方も変わってきます。
だいたい次の2通りに分けられるんじゃないでしょうか。

  1. プロジェクトルートにある babel の設定ファイル( .babelrc babel.config.js )を babel が自動的に読み込む
  2. webpack から実行環境によって手動(設定ファイルのコード上)で呼び出す設定を変える

上記2パターンそれぞれについて説明をしていきます。

3.1. 設定ファイルを自動的に読み込む場合

この場合、 babel.config.ts は次のように書くことができます。

babel.config.ts
import { TransformOptions, ConfigAPI } from '@babel/core'

export default function (api: ConfigAPI): TransformOptions {
  return {
    presets: [
      '@babel/preset-env',
    ],
  }
}

TransformOptionsConfigAPI という見慣れないものが出てきましたが、 TransformOptions はお馴染みの babelの設定ファイルの内容ConfigAPIbabelの設定API のことです。
これを .ts ではなく .js で書くと次のようになります(ドキュメントにほぼ同じものが載っていますが)。

babel.config.js
module.exports = function(api) {
  return {
    presets: [
      '@babel/preset-env',
    ],
  };
}

ここまでくればあとは babel.config.tsbabel に読み込ませるだけですが、 babelwebpack のように .ts の設定ファイルをいい感じに解釈してくれません。

webpack.config.ts
// ...
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          configFile: 'babel.config.ts'
        },
      },
// ...

このように書いたとしても babelbable.config.ts をJSONとして読み込もうとするので構文エラーになります。
そこで tsc コマンドで .js ファイルにトランスパイルする必要があります。

npx tsc babel.config.ts

これで babel.config.js が生成されるので設定ファイルを読み込むことができますが、いちいちトランスパイルするのは面倒ですし、 babel.config.tsbabel.config.js の2ファイルが並ぶのはあまり気持ちのいいものではないですね。

3.2. webpackから実行環境によって設定を変える場合

3.1. の方法だとあまりすっきりできないので、自分はこちらの方法を採用しています。
特に webpack4 では webpack-merge を使って呼び出す設定ファイルを切り替えるのがメジャーらしい(?)のでこちらの方が合っている気がします。

babel.config.ts
import { TransformOptions } from '@babel/core'

// 開発環境用の設定
export const dev: TransformOptions = {
  presets: [
    '@babel/preset-env',
  ],
  sourceMaps: true,
}

// 本番環境用の設定
export const prod: TransformOptions = {
  presets: [
    '@babel/preset-env',
  ],
}
webpack.config.dev.ts
import { Configuration } from 'webpack'
import merge from 'webpack-merge'
import config from './webpack.config.common'
import { dev as devBabelConfig } from './babel.config'

const devConfig: Configuration = merge(config, {
  mode: 'development',
  devtool: 'source-map',
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: devBabelConfig,
      },
  // ...
}

export default devConfig
webpack.config.prod.ts
import { Configuration } from 'webpack'
import merge from 'webpack-merge'
import config from './webpack.config.common'
import { prod as prodBabelConfig } from './babel.config'

const prodConfig: Configuration = merge(config, {
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: prodBabelConfig,
      },
  // ...
}

export default prodConfig

おわりに

以上、 babel の設定ファイルを TypeScript で書いてみました。
TypeScript で記述することでタイポなども減らせていいんじゃないでしょうか。
もっといい方法などあれば教えて頂けるとうれしくなります。

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

JavaScriptの配列操作

配列操作

メソッド

2020-04-01_09h18_38.png

splice(開始する位置, 削除数 [, 追加する要素, ...])

その他操作

  const a = [1, 2, 3, 4, 5];
  const b = [6, 7, 8, 9, 10];

  //配列をなくす方法
  const c = [...a, ...b];

  //分割代入
  const [d, e, f] = a

  //rest構文
  const [g, h, ...i] = a


  console.log(...a); // 1 2 3 4 5
  console.log(c); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
  console.log(d, e, f); // 1 2 3
  console.log(g, h, i); // 1 2 [ 3, 4, 5 ]


  //値の交換
  let [x,y,z] = [1,2,3]
  console.log(x,y,z); // 1 2 3

  [x, y, z] = [y, z, x];
  console.log(x, y, z); // 2 3 1


  //オブジェクトの取り出し
  //プロパティ名にないものを指定しようとするとエラーになる
  const j = {k: 1, l: 2, m: 3, n: 4, o: 5};
  const {k, l, ...p} = j;

  console.log(k, l, p); // 1 2 { m: 3, n: 4, o: 5 }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの配列とオブジェクト操作

配列操作

  const a = [1, 2, 3, 4, 5];
  const b = [6, 7, 8, 9, 10];

  //配列の結合
  const c = [...a, ...b];

  //分割代入
  const [d, e, f] = a

  //rest構文
  const [g, h, ...i] = a


  console.log(...a); // 1 2 3 4 5
  console.log(c); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
  console.log(d, e, f); // 1 2 3
  console.log(g, h, i); // 1 2 [ 3, 4, 5 ]


  //値の交換
  let [x,y,z] = [1,2,3]
  console.log(x,y,z); // 1 2 3

  [x, y, z] = [y, z, x];
  console.log(x, y, z); // 2 3 1

参照渡したくない場合

  const a = [1, 2];

  const b = a;
  const c = [...a];

  a[0] = 3;

  console.log(b); // [3, 2]
  console.log(c); // [1, 2]

配列操作のメソッド

2020-04-01_09h18_38.png

splice(開始する位置, 削除数 [, 追加する要素, ...])

オブジェクト操作

  const a = {c: 1, d: 2, e: 3};

  //オブジェクトの結合
  const b = {...a, f: 4, g: 5};

  //分割代入,存在しないキーを指定するとエラーになる
  const {c, d, ...h} = b;

  console.log(b); // { c: 1, d: 2, e: 3, f: 4, g: 5 }
  console.log(c, d, h); // 1 2 { e: 3, f: 4, g: 5 }

  //キーを取り出す方法
  const i = Object.keys(b);
  console.log(i); // [ 'c', 'd', 'e', 'f', 'g' ]

  //キーと値を表示
  for(let key of i) console.log(`key:${key} value:${b[key]}`);
  /*
  key:c value:1
  key:d value:2
  key:e value:3
  key:f value:4
  key:g value:5
  */
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EdgeHTML の browser API 不具合集 (2020年4月)

概要

EdgeHTM エンジン版 Edge の browser/chrome API の不具合の覚え書き。
Chrome (V8 8.0.426 / Blink 80) で普通に動作するかどうかを基準に判断。
Chromium エンジン版 Edge は未検証。
どうせ Chromium に移行するつもりだったからなのか EdgeHTML は色々ガバガバ。
※ browser/chrome API のほぼ全ての非同期関数は、最終引数に関数を渡せばコールバック型で利用できる。また、Chrome でも「Promise を return する事になっているが実はコールバック型でしか使えない関数」が幾つか存在する (具体例要追記)。MDN (英語版しか読んでいないが) ではそれらに関する記述がほぼ皆無なので要注意。

検証環境

  • 2020年4月時点
  • Windows 10 Pro x64
  • Microsoft Edge 44.18362.449.0 (EdgeHTML 18.18363 / ChakraCore 1.11.15)
  • 下記のモンキーパッチ(?)を適用.
  • chromebrowser と同義
background.js/content.js
if(/edge/i.test(window.navigator.userAgent)){
    window.chrome = window.browser;
    console.log('EdgeHTML SUCKS!!!');
}

Microsoft Edge の長所

ブラウザ拡張機能開発において Edge を使う利点は content script の変更が即時反映される点。ただこの一点に尽きる。
Chrome では拡張機能自体を「更新」しなければ content script の変更が反映されない上、「更新」しても content script や各種リソースの変更が反映されないバグがある為、変更を加える度にブラウザを再起動する必要がある。
これに対して Edge は、content script を変更してから対象URLのページをリロードすれば即座に新しい content script が適用されるのでデバッグが非常に楽。

非同期関数は Promise ではなく undefined を返す

見出しの通り。なので、Chrome では then でチェーン出来る関数(メソッド)でも、Edge では 絶対に 直接チェーン出来ない。
本来 Promise を返すことになっているメソッドは、最後の引数にコールバック関数を入れて使う。
(失敗判定は try...catchchrome.runtime.lastError または chrome.extension.lastError などで行う。)

background.js
// [エラーになる例]
chrome.tabs.query({'url':'https://qiita.com/*'})
    .then(tabs=>console.log(tabs.length));
//"Error: Invocation of form tabs.query(object) doesn't match definition tabs.query(object queryInfo, function callback)."

// [上手くいく例]
chrome.tabs.query(
    {'url':'https://qiita.com/*'},
    tabs=>console.log(tabs.length)
);
 //1

background script が cross origin 非対応

例えば background script で WebSocket を開こうとすると「Access-Control-Allow-Headersがないです~:stuck_out_tongue_winking_eye: べろべろば~:stuck_out_tongue:」などのエラーを吐く。
Fetch や XHR は未検証だが同じだろう。
Chrome では問題なく接続できる。
_generated_background_page.html のヘッダーを生成しているのは Edge の拡張機能管理機能なので、恐らく回避は不可能。

比較的重要なメソッドが初めから存在しない

  • chrome.tabs.connect がない為、background script → content script の port 接続ができない.

他にもあったけど忘れたので後日追記.

native messaging が異常に難しい

普通の (Edge 以外の) ブラウザでは比較的容易に 拡張機能 <―> ネイティブアプリ間通信 (native messaging) を実装できる。
いくつかの設定ファイルを用意して少しレジストリを弄るだけなので、テキストエディタさえあればいい。
これに対して Edge で native messaging を実装するには、絶対に最新の Visual Studio を使わなければならない
Edge では、拡張機能とネイティブアプリの通信は特別なプロトコル (AppService) を用いる。そのプロトコルに対応するアプリケーション (UWP application) は Visual Studio でしか作成できない (Visual Studio でセキュリティートークンを発行しないと AppService を起動できない)。
ネイティブアプリの代わりにローカルサーバーを立てて WebSocket や Fetch で通信しようとしても、前述のように「cross origin access forbidden :hugging:」とか言われる。
EdgeHTML 版 Edge での native messaging は潔く諦めた方がいい
どうしてもやりたいなら Chromium 版 Edge に切り替えよう。
最終決定!「でも」とか「やっぱり」は許さん!二度と試すな!EdgeHTML SUCKS!!!!

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

Vue.js公式チュートリアルをゆっくり読んでいく2

前置き

前回に引き続き、Vue.jsの公式チュートリアルをゆっくり読んでいきます。
チュートリアルに沿ってはいますが、サンプルはよりわかりやすく単純なものに一部変えているところもあります。

前回は主にVue.jsのリアクティブシステムに触れました。今回は、Vue.js上では「ディレクティブ」と呼ばれているVue.js特有の属性を見ていきたいと思います。

Vue.jsのバージョン:v2.6.11

今回のチュートリアル参照箇所

はじめに - 宣言的レンダリング

はじめに - 条件分岐とループ

はじめに - ユーザー入力の制御

v-bind:要素の紐付け

前回はVueインスタンスのdataプロパティに持たせたデータオブジェクトの内容を、elプロパティと紐付けた「HTML要素の中で{{ message }}という形式で表示させました。

<div id="app">
    {{ message }}
</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!'
        }
    });
</script>

この「HTML要素の中で」ということですが、HTML要素の属性部分には効くのでしょうか?
次のようにdataオブジェクトのmyFavoriteColorStyleに持たせたcssを#app要素のstyle属性に反映できるかやってみます。

<div id="app" style="{{ myFavoriteColorStyle }}">
    {{ message }}
</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
            myFavoriteColorStyle: 'color:red',
        }
    });
</script>

image.png
image.png

style属性は反映されず、警告がでてしまいました。HTML属性部分には効かないようです。
しかし、Vue.jsが親切に警告内でどう書けばいいか教えてくれています。
For example, instead of <div style="{{ val }}">, use <div :style="val">.
これで書き直してみます。

<div id="app" :style="myFavoriteColorStyle"> <!-- {{ }} で囲まない -->
    {{ message }}
</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
            myFavoriteColorStyle: 'color:red',
        }
    });
</script>

image.png
image.png

今度は警告もでず、スタイルが適用されました。
ここで使用した:styleですが、正式にはv-bind:styleと書きます。:styleは省略記法です。

v-bind属性はVue.js上ではディレクティブと呼ばれており、v-bind:属性名とすることで、
その属性に対してデータオブジェクトの内容を紐付けることができます。
もちろんこれもリアクティブなものなので、chromeのデベロッパーツールのコンソールで
vm.myFavoriteColorStyle = "color:blue"と打つと、すぐにそれが反映されます。

myReactive.gif

v-if:条件分岐

v-ifディレクティブは要素の表示、非表示を切り替えることができます。

<div id="app" v-if="seen">
    {{ message }}
</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
            seen: true,
        }
    });
</script>

v-if.gif

v-ifディレクティブは厳密には、要素の表示・非表示を切り替えるのではなく、要素を削除・再生成しています。
vm.seen = falseしたときにHTML要素を確認してみます。

v-if2.gif

要素の表示・非表示を切り替えるディレクティブは、v-ifの他にv-showもあります。
こちらは、要素のスタイルをdisplay:noneにするだけなので、要素自体は削除されません。
v-ifは要素を削除・再生成する一方、v-showはスタイルを切り替えるだけなので後者のほうが高速に動作します。

<div id="app" v-show="seen">
    {{ message }}
</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
            seen: true,
        }
    });
</script>

v-show.gif

ところでチュートリアルにも記載がありますが、Vue.jsは上記のような要素の更新が行われたときにトランジション効果を
簡単につけることができます。例えばさきほどのv-ifディレクティブの例に、フェード効果をつけてみます。

<div id="app">
    <!-- トランジション効果をつけたい要素をVue.jsに予め用意されているtransitionタグ(コンポーネント)で囲みます -->
    <transition name="fade"> <!-- name属性で指定した名前を後述のstyleで使います -->
        <p v-if="seen">{{ message }}</p>
    </transition>
</div>

<style>
    /* 「transitionタグのname属性で指定した名前 + "-enter"」で要素が追加される直前、
       「transitionタグのname属性で指定した名前 + "-leave-to"」で要素が削除された直後を表すクラス名 */
    .fade-enter, .fade-leave-to {
        opacity: 0; /* 透明状態 */
    }
    /* 「transitionタグのname属性で指定した名前 + "enter-active"」で要素の追加中
       「transitionタグのname属性で指定した名前 + "leave-active"」で要素の削除中を表すクラス名 */
    .fade-enter-active, .fade-leave-active {
        transition: opacity .5s; /* 0.5秒かけて透明度を変化させる */
    }
</style>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
            seen: true,
        }
    });
</script>

v-if_transition.gif

参考:Enter/Leave とトランジション一覧 — Vue.js

v-for:ループ

v-forは配列の内容を取り出して表示してくれます。

<ul id="app">
    <li v-for="message in messages">
        {{ message }}
    </li>
</ul>

<script>
    let messages = ['Hello','v-for','directive'];
    let vm = new Vue({
        el: '#app',
        data: {
            messages: messages,
        }
    });
</script>

v-for.gif

もちろんリアクティブ。

v-on:イベントハンドリング

v-onディレクティブはその要素に対してイベントリスナを設定してくれます。

<button id="app" v-on:click="changeMessage">
    {{ message }}
</button>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
        },
        methods: {
            changeMessage: function() { this.message = 'Hello Vue World!' },
        }
    });
</script>

v-on.gif

ところで、これをVue.jsを使わないで実現するとどうでしょうか。

<button id="app" onclick="changeMessage()">
    Hello Vue!
</button>

<script>
    function changeMessage() {
        document.getElementById("app").innerHTML = 'Hello Vue World!';
    }
</script>

Vue.jsを使わなくてももちろんできます。むしろこれくらいならこちらのほうが全然シンプルです。
ではVue.jsを使うメリットは何でしょうか。次の場合を考えてみます。

例えば、button要素の中のテキストを赤くしたとします。

<button id="app" onclick="changeMessage()">
    <span style="color:red">Hello Vue!</span>
</button>

<script>
    function changeMessage() {
        document.getElementById("app").innerHTML = 'Hello Vue World!';
    }
</script>

v-on_not_use.gif

せっかく赤くしたテキストがクリックすると黒に戻ってしまいました。
innerHTMLは要素内のHTMLを設定するプロパティなので、テキスト部分だけ変更したい場合は、
例えば次のようにスクリプト部分を修正する必要があります。

<button id="app" onclick="changeMessage()">
    <span style="color:red">Hello Vue!</span>
</button>

<script>
    function changeMessage() {
        document.querySelector("#app span").innerHTML = 'Hello Vue World!';
    }
</script>

v-on_not_use2.gif

これでうまくいきました。でもこれは、「innerHTMLが要素内のHTMLを設定するプロパティであること」を知っていること、
それを知った上で、要件を満たすために、「querySelectorを使う」「Hello Vue!部分だけを書き換えるためにid="app"要素の
中のspan要素を指定するセレクタを記載する」など、DOMを操作するためのいくつかの追加の知識が必要です。

これをVue.jsで実現すると、

<button id="app" v-on:click="changeMessage">
    <span style="color:red">{{ message }}</span>
</button>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!',
        },
        methods: {
            changeMessage: function() { this.message = 'Hello Vue World!' },
        }
    });
</script>

スクリプト部分は一切変更せずにできました。これはVue.jsがDOM操作をやってくれるからです。
Vue.jsを使うことで、DOM操作を気にせず、ロジックのみに集中できることがVue.jsのメリットの一つでもあります。

今回はここまでです。

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

自分やチームのLGTMを可視化するツールを作ってみた

qiitaracingshort2.gif

はじめに

チームの1年間のいいね数をD3.jsを使ってアニメーションで可視化する
以前この記事で、チームのいいねLGTMをD3を使ったアニメーションで可視化するということをやりました。
今回はQiitaのユーザーIDを入力するだけで、同じような可視化ができるQiitaRacerというものを作ったのでご紹介します。

サービス

Qiita Racer
※Github Pagesで公開しています

ソースもこちらに公開しています。
https://github.com/tonio0720/QiitaRacing

使い方

URLにアクセスすると「アクセスの許可を求めています」と出るのでそのまま許可してください。
ユーザーID(最大8個)を入力し、データ取得を実行すると動きます。
※QiitaAPIの制限の都合により、総LGTM数が1000以上のユーザーは抽出できないように制御しています。

所感

今回はツールを作って、Github Pagesに公開するということをやってみました。
初めて使いましたが、とても便利ですね。
Qiita APIもCORS制約に引っかかることなく使えたので助かりました。

Qiita APIの1時間に1000リクエストの制約は少し不便ですね。
本ツールではユーザーに紐づくすべての記事を取得して、それぞれの記事のLGTM数を取得するということをやっているので、結構APIを消費します。
1ページにおける最大取得件数が100件というのもあり、あまりこういう使い方は向いていないのだなと感じました。
本ツールでは総LGTM数が1000を超えるユーザーは抽出できないように制御しています。

可視化においてはReact+AntDesign+D3.jsの合わせ技でやっています。

利用したもの

  • React
  • Ant Design
  • D3.js
  • Qiita API
  • Github Pages(公開用)

参照したサイト

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

ムーモ(moomo)の口コミや評判は悪い?実際に使用した方の口コミを調べました

ムーモ(moomo)の口コミや評判は悪い?実際に使用した方の口コミを調べました

実際の所どうなのでしょうか?

詳しい内容はこちらです

https://www.jyomo.xyz/

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

JavaScript オブジェクト

はじめに

皆さん、こんばんは?
yuです?
最近、ここ2日間ぐらい、投稿ができませんでした。
理由として、
転職先を、フロントエンドエンジニアになることを目標に置き、
どのような、言語を学ベば良いのか迷っていました。

私なりに考え、それは、JavaScript関連だと思いました。
より一層、追求していこうと思いました。

これからも、JavaScriptを中心に投稿していきます。
よろしくお願いします?

オブジェクトについて、初めていきます。

オブジェクト

まず「オブジェクト」を日本語に訳すと、「物」と出てきます。
本来はそうなのですが、Javascriptでは、「データの集まり」と、
理解しましょう。

オブジェクト文を詳しく見ていきましょう↓?↓
const Giants = {

sports : "野球",//プロパティ
color : "オレンジ",//プロパティ
playing : function() {
 document.write("ボールを投げる。");//メソッド
}
}

上記のようになります。
オブジェクト文には
1 プロパティ
2 メソッド
というものが、入ってます。

「プロパティ」とは、オブジェクト内の、データとなります。
特徴などで例えると、分かりやすいと思います。
上記で、例えると、Giantsというチームは、
野球を行い、色はオレンジというデータが入ってます。

「メソッド」とは、オブジェクト内の、機能になります。
動きなどで例えると、分かりやすいと思います。
上記で例えると、Giantsというチームの動きは、
ボールを投げるで、これが機能になっています。

まとめ

オブジェクトは2つの要素で出来ている。
・プロパティ
・メソッド

オブジェクトは、
・人
・団体
などで、例えると、「プロパティ」、「メソッド」との関係性が
分かりやすくなる。

本日は以上になります。

ありがとうございました?

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

asyncとdeferの仕様を誤解していた。

解決したい問題

勤務先のWebサイトで、他社製のスクリプトAの実行が完了する前に、自社製のスクリプトBが実行されるとIE11がクラッシュするバグが見つかりました。具体的には以下のように呼び出しています。
なお、A.jsの呼び出し用タグは他社からの指示で1文字たりとも変更できません。

<html>
  <head>
    <!-- 自社製スクリプト -->
    <script src="./B.js"></script>
    <!-- 他社製スクリプト -->
    <script src="./A.js" async></script>
  </head>
  <body>
  </body>
</html>

クラッシュする原因は「B.jsの作りに問題があり、その問題をIE11だけが踏み抜くから」なのですが、A.jsの実行が完了していればクラッシュは回避できるため、B.jsがA.jsの実行を待てば良いと思い、以下のように書換えました。

<html>
  <head>
    <!-- 他社製スクリプト -->
    <script src="./A.js" async></script>
    <!-- 自社製スクリプト -->
    <!-- deferはasyncの完了も待ってくれることを期待した -->
    <script src="./B.js" defer></script>
  </head>
  <body>
  </body>
</html>

仕様を誤解していた箇所

deferはasyncの完了を待ってくれると勝手に思い込んでいたのですが、クラッシュしたりしなかったりする挙動になってしまい、状況が悪化しました。
WHATWGの規格書を読んでもどうしてもピンと来なかったため、検証用のコードを書くことにします。
挙動さえわかればいいのでコード自体はめちゃくちゃ適当です。色んな人から怒られそうなコードですね。

検証用コード

html

<html>
  <head>
    <script src="./first.js" async></script>
    <script src="./second.js" defer></script>
  </head>
  <body>
  </body>
</html>

asyncで実行するjs (first.js)

var i = 0;
for (i = 0; i < 500000000; i++) { }
console.log("first.js: " + i);

deferで実行するjs (second.js)

console.log("second.js: " + i);

検証結果

Chrome 80で検証しました。

スクショ
3回に1回位はエラーが出るという感じです。

どちらもdeferがついていれば想定通りfirst->secondの順で呼び出されますが、asyncとdeferだと必ずasync->deferになるわけではないのですね。
実際に検証した後に <script> タグに async / defer を付けた場合のタイミング - Qiita を読むと大変わかりやすかったです。というか最初からこちらの記事を読んでいれば無駄に検証しなくても良かったのでは…。

以下も検証しましたがやはりNGでした。10回に1回位はエラーが出ます。

<html>
  <head>
    <script src="./first.js" async></script>
  </head>
  <body>
    <script src="./second.js"></script>
  </body>
</html>

ということで、今の所思いつく解決策としては「A.jsの作成元にdeferで呼び出していいか確認する」か、「自社スクリプトB.jsを修正する」のどちらかになりそうです。思い込みは良くないですね。
フロントエンドは初心者なので頑張って勉強していきたいです。

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