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

【比較検証】Next.js の Server Side Rendering (SSR) を理解する。create-react-app と比べてみた。

image.png

Next.jsのサイト、かっこいいですよね ?
クールで、パフォーマンスにも優れていてエンジニアを魅了します。
日本では Nuxt.js が人気のようですが、個人的には Next.js を推しています。

さて、先日 Next.js のチュートリアル を通してサーバサイドレンダリングについて考えさせられる機会がありました。本記事では、そもそもサーバサイドレンダリングのメリットとは?というところから初めて、create-react-app によって実装された SPA と、nextによって実装された SSR ではどのような違いがあるのかを検証してみました。

以下の動画は本記事のサマリーです。
作成したアプリケーションへのリンクも貼っておきます。
右の方がちょっとだけ描画が遅いのがわかりますね。

next react

?(左)next.js で SSR、(右)create-react-app で SPA ?

サーバサイドレンダリング(SSR)とは

main.png

従来の React ベースのアプリケーションの構成を振り返ってみましょう(右図)。この構成の場合、ユーザからのリクエストは、まずはじめに React サーバ(S3 や Netlify)から JavaScript のソースと必要最小限のほとんど空っぽな HTML を返します。それからフロントエンドで HTML 要素をレンダリングする方法をとります。

このようにバックエンド API とフロントエンドの描画を完全に分離する事によって、開発体制を分離した生産性向上や、ユーザに優れた UX を提供できるようになりました。
その一方で、過度なネットワーク通信が発生したり、JavaScript によって生成された Web サイトを検索エンジンのクローラが検知できなくなりました。その結果として、Google の検索項目の上位に自サイトが表示されにくいなどのデメリットも招いてしました。※こちらの記事で紹介されていますが、最近ではあまり問題にならなくなっているようです。

さて、このような問題を解消するためのテクニックがサーバサイドレンダリング(ServerSideRendering)です(左図)。サーバサイドレンダリングは従来フロントエンドで行なっていたレンダリングをバックエンドの Node.js サーバにも移譲しようという考え方です。これにより、モバイル端末がどんなに脆弱でも、ハイパフォーマンスなサーバを使用してレンダリングできます。さらに無駄なネットワーク通信回数も最小限に減らせるでしょう。「バックエンドの Node.js サーバにも」と強調しているのは、フロントエンドでももちろん描画ができる、ということです。初期ページの一部だけはサーバサイドでレンダリングして、残りの要素はフロントエンドからフェッチしてきてレンダリングするといったように用途に応じて使い分けができます。

パフォーマンス

遅いデバイスを使用していると、最初のページのレンダリングに時間がかかり、ユーザ体験が低下します。計算をより強力なサーバーにオフロードすることで、ユーザーが待機する時間を最小限に抑えることができます。
また、サーバーで初期データをプリフェッチしてページを構築すると、サイトを表示するために必要なラウンドトリップの回数が大幅に削減されます。これにより、待ち時間の短縮と帯域幅消費の削減につながります。

fmp.png

SEO 対策

SSR を行なっているサイトは、ページが検索エンジンで簡単にインデックス化されます。クライアント側でルーティング制御を行なっていると、検索エンジンのウェブクロールを遅らせてしまいます。この結果、検索エンジンの上位にリンクを表示することが難しくなります。

Next.js ことはじめ

SSR を理解するために必要最小限の構成で Next.js アプリケーションを組み立てていきます。

必要なライブラリとアプリケーションの実行

$ mkdir next.ssr
$ cd next.ssr
$ yarn init -y

Next.js を最小構成で始めるために必要なライブラリは nextreact, react-dom だけです。早速 yarn でインストールしましょう(npm でもよいですよ)

$ yarn add react react-dom next

package.json には以下の npm scripts を記載しておいて開発を楽に進められるようにしておきましょう。

package.json
"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

それぞれのコマンドは以下のように使用します。

  • dev - ローカルでアプリケーションを起動します。
  • build - プロダクション用にアプリケーションをビルドします。
  • start - プロダクション環境でアプリケーションを実行します。

ルーティング

Next.js は非常にシンプルな設計思想でフロント画面が作れるように構成されています。/pages ディレクトリ配下に配置されている js ファイルごとにパスルーティングが行われます。はじめの一歩として /pages/index.jsにファイルを配置して、/という URL で表示できるようにしてみましょう。詳細なドキュメントはこちら

$ mkdir pages
$ touch pages/index.js
pages/index.js
const Index = () => {
  return <h1>Hello World</h1>;
};
export default Index;

ファイルパスと URL パスには以下のような対応関係があります。

ファイルパス URL パス
pages/index.js /
pages/blog/index.js /blog
pages/blog/first-post.js /blog/first-post
pages/dashboard/settings/username.js /dashboard/settings/username
pages/blog/[slug].js /blog/:slug (/blog/hello-world)
pages/[username]/settings.js /:username/settings (/foo/settings)
pages/post/[...all].js /post/* (/post/2020/id/title)

さて、ここまでできれば準備完了です。アプリケーションを起動してみましょう。

$ yarn dev

ブラウザを起動し、/ にアクセスすると画面が表示されるはずです。

image.png

サーバサイドレンダリングの実装

ここから SSR ができるような機能を作っていきましょう。

<Link> コンポートを使用して、他ページに遷移します。以下の例だと /shows/[id] へ遷移させようとしています。また、Next.js には、ページのデータを取得するための標準 API が付属しています。 getInitialProps という非同期関数を使用して実行します。
getInitialProps を使用すると、特定のページのデータをフェッチしてページに渡すことができます。 getInitialProps はサーバーとクライアントの両方で動作します。
この getInitialProps の振る舞いを観測し、SSR を理解していきましょう。

index.js
import Link from "next/link";
import fetch from "isomorphic-unfetch";

const Index = props => (
  <div>
    <h1>Batman TV Shows</h1>
    <ul>
      {props.shows.map(show => (
        <li key={show.id}>
          <Link href="/shows/[id]" as={`/shows/${show.id}`}>
            <a>{show.name}</a>
          </Link>
        </li>
      ))}
    </ul>
  </div>
);

Index.getInitialProps = async function() {
  const res = await fetch("https://api.tvmaze.com/search/shows?q=batman");
  const data = await res.json();
  console.log(`Show data fetched. Count: ${data.length}`);
  return { shows: data.map(entry => entry.show) };
};

export default Index;

pages 配下に /shows/[id].js を配置し、Dynamic Routing ができるようにしておきます。

pages/[id].js
import fetch from "isomorphic-unfetch";

const Post = props => (
  <div>
    <h1>{props.show.name}</h1>
    <p>{props.show.summary.replace(/<[/]?[pb]>/g, "")}</p>
    {props.show.image ? <img src={props.show.image.medium} /> : null}
  </div>
);

Post.getInitialProps = async function(context) {
  const { id } = context.query;
  const res = await fetch(`https://api.tvmaze.com/shows/${id}`);
  const show = await res.json();
  console.log(`Fetched show: ${show.name}`);
  return { show };
};

export default Post;

/ を表示してみましょう。サーバサイドにログ Show data fetched: 10 が表示されるはずです。 index.js をサーバサイドでレンダリングしたという事になりますね。
次にリンクをクリックして /shows/975 に遷移するとブラウザのコンソールにログが表示されてます。これはフロントエンドでデータフェッチとレンダリングが行われたということを意味しています。

ssr.gif

デプロイ

最後にビルドして、デプロイします。ZEIT の now にデプロイします。素晴らしい DX(DeveloperExperimence)です。本当に必要な要素以外全て削ぎ落とした、最高の PaaS だと思ってます。いつも愛用しています。こちらの記事にて丁寧に解説されていました。

bash
$ yarn build # ビルド
$ now        # デプロイ

デプロイしたら動作を確認してパフォーマンスを検証しましょう。Chrome の開発者コンソールを開き、Audit を実行します。

https://batman-tv-shows.geeawa.now.sh/

First Meaningful Paint が 1.0s とでました。まずまずです。

image.png

create-react-app との比較

ここまでできたので Next.js で作成されたアプリケーションと create-react-app で作成されたアプリケーションを比較してみましょう。

以下のようにほぼ同様のソースを使用して、create-react-app アプリケーションを作成します。以下にデプロイしてあります。

https://badman-tv-shows-create-react-app.now.sh/

index.js
import React from "react";
import fetch from "isomorphic-unfetch";

class Index extends React.Component {
  constructor(props) {
    super(props);
    this.state = { shows: [] };
  }
  async componentDidMount() {
    const res = await fetch("https://api.tvmaze.com/search/shows?q=batman");
    const data = await res.json();

    console.log(`Show data fetched. Count: ${data.length}`);

    this.setState({ shows: data.map(entry => entry.show) });
  }

  render() {
    return (
      <div>
        <h1>Batman TV Shows</h1>
        <ul>
          {this.state.shows.map(show => (
            <li key={show.id}>
              <a href="">{show.name}</a>
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

export default Index;

デプロイができたので Audit を実行します。
First Meaningful Paint は 1.4s となり、Next.js によって SSR できるようになったサイトと比較すると少しだけ遅い結果がでました。

image.png

さいごに

今回作成されたアプリケーションは非常にシンプルで、1つの API しか実行しませんし、レンダリングする DOM 要素も少なかったためパフォーマンスにそれほど大きな違いはみられませんでした。それでもアプリケーションが肥大したり、ネットワークの遅い環境、古くて脆弱なモバイルデバイスを使用するとパフォーマンスの違いは顕著になってくるでしょう。SSR の技術は適材適所を見極めて投下していきたいですね。

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

IFRAMEにコンテンツをフィットさせるには

固定サイズのHTMLを、それよりも小さいサイズのIFRAMEに縮小して表示したかったので、その対処方法です。

課題

たとえば1440px X 1000pxの大きさのHTMLを幅800pxのIFRAMEに表示します。

解決方法

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link rel="stylesheet" href="default.css">
  </head>
  <body>
    <div id="wrapper">
      <h1>Resize 1440px X 1000px iframe to width 800px</h1>
      <iframe id="iframe" src="iframe-content.html"></iframe>
    </div>
    <a href="iframe-content.html" target="_blank">iframe-content.html</a>
  </body>
  <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
  <script src="default.js"></script>
</html>
default.css
body {
  text-align: center;
}
#wrapper {
  margin: 0 auto 0 auto;
  overflow: hidden;
  width: 800px;
}
#iframe {
  height: 1000px;
  width: 1440px;
}
default.js
(function (window, document, $) {
  let iframe = $('#iframe');
  let wrapper = iframe.parent();
  let width = wrapper.width();
  let ratio = width / iframe.width();
  console.log(`Ratio: ${ratio}`);

  // IFRAME自体は読み込みページの大きさにCSSで適用している。
  // それを#wrapperのサイズにスケールインする。
  // https://stackoverflow.com/questions/166160/how-can-i-scale-the-content-of-an-iframe
  iframe
    .css('-ms-transform',     `scale(${ratio})`)
    .css('-moz-transform',    `scale(${ratio})`)
    .css('-o-transform',      `scale(${ratio})`)
    .css('-webkit-transform', `scale(${ratio})`)
    .css('transform',         `scale(${ratio})`)
    .css('-ms-transform-origin',     '0 0')
    .css('-moz-transform-origin',    '0 0')
    .css('-o-transform-origin',      '0 0')
    .css('-webkit-transform-origin', '0 0')
    .css('transform-origin',         '0 0');

  // #iframeのひとつ上のラッパー#wrapperの高さを同じ倍率で変更する。
  // これをしないとうまくもともとのIFRAMEの高さのままになる。
  wrapper.height(wrapper.height() * ratio);

})(window, window.document, window.jQuery); 

ポイント

  • 同じ倍率で高さも変える必要もあるので、JavaScriptで対応します。
  • 参考サイトにある通り、CSSで縮小します。
  • IFRAMEのサイズは読み込んでいるコンテンツのサイズと同じにしておく。
  • IFRAMEをラッパーで囲みます。CSSで指定したサイズの領域が取られているのでラッパーのdivで高さを調整しています。
  • 面倒だったのでjQueryを使用。

demo

参考サイト

How can I scale the content of an iframe?

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

Google OAuth 2.0 認証を使ったGoogle Sign-Inの実装(サーバー編)

概要

今回はこちらの記事の続きです。
Google OAuth 2.0 認証を使ったGoogle Sign-Inの実装(JS編)
クライアントサイドでログインしてバックエンドサーバーで認証といった感じでやります。

前回の記事でユーザー情報などをレスポンスで取得することができました。
ただこれをこのままサーバーサイドに送ったりするのは危険です。
例えば、ユーザーIDをサーバーに送信する際にユーザーを偽装できちゃったりします。。

なので、その代わりに検証可能なIDトークンを使用して、サーバー側でサインインしているユーザー情報を安全に取得します。

さっそく実装していきましょう。

フロントサイドの実装

まずJSの部分を書きかえます。

JavaScript
function onSignIn(googleUser) {
  var id_token = googleUser.getAuthResponse().id_token; // IDトークンを取得
  // IDトークンをサーバー側に送る処理
}

ここのサーバー側に送る処理はAjax使ったり、hidden属性で送ったりいろいろなやり方があると思いますが、
今回はAjaxのXMLHttpRequestを使用してHTTPリクエストを発行する方法でサーバーとの通信をやって来ます。

先ほどの処理に追加して

javaScript
var req = new XMLHttpRequest();
req.open('POST', '[URL]’);
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
req.onload = function() {
req.send('idtoken=' + id_token);

こんな感じでサーバー側に送信しましょう。
[URL]の部分にはサーバー側のURLを入れましょう。

サーバー側の実装

今回は、JavaのSpring bootを使って実装していきます。

まずは依存関係の追加です。

build.gradle
dependencies {

implementation("com.google.api-client:google-api-client:1.30.5")

}

Googleクライアントライブラリを使えば簡単にIDトークンの検証ができます。

Java
 GoogleIdTokenVerifier verifier =
        new GoogleIdTokenVerifier.Builder(
                new NetHttpTransport(), JacksonFactory.getDefaultInstance())
            .setAudience(Collections.singletonList("YOUR_CLIENT_ID.apps.googleusercontent.com"))
            .build();

 var idtokenStriing = getIdToken(); // 取得したIDトークン

 GoogleIdToken idToken = verifier.verify(idTokenString); // IDトークン検証


YOUR_CLIENT_IDには自分のクライアントIDを入れてください。

もしGoogleクライアントライブラリを使わない場合はGoogleの公開鍵(PEM形式)を使用して、トークンの署名を検証する方法もありますが、GoogleでもGoogleクライアントライブラリを使って検証することを推奨しているので使いましょう。

あとはユーザー情報を取得するだけです。

Java
  Payload payload = idToken.getPayload();

  String userId = payload.getSubject(); // ユーザーID
  String email = payload.getEmail(); // ユーザーメールアドレス
  String name = (String) payload.get("name"); // ユーザー名
  String pictureUrl = (String) payload.get("picture"); // ユーザープロフィール画像

こんな感じで取得できちゃいます。

まとめ

今はいろんなサイトでOAuth認証使ってるところが増えてますね。
今回はGoogleですがFacebookやAppleなどのOAuth認証もまたあげていこうと思います。

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

[Angular]カスタムパイプの作り方

概要

  • Angularにはtemplate上でフォーマットなど簡易な変換ができるPipeという機能があります
  • フレームワークが提供するPipeもありますが独自に作ることもできるようなのでその手順のメモ

パイプとは

  • Angularではtemplateの中で{{}}を使うと変数を埋め込むことができます
  • 埋め込んだ変数の後ろに{{ 変数 | xxx }}といった形でPipeを適用することでフォーマット変換などができます
<div>
  <p>{{ 1234567 | number }}</p>
  <p>{{ new Date(2020, 1, 5) | date: 'yyyy年MM月dd日' }}</p>
</div>
  • 上記の例はAngularが用意しているnumberdateのPipeを使った例です
  • 以下のように画面に表示されます
1,234,567
2020年02月05日

カスタムパイプを作る

  • 今回は文字列の末尾をマスク化するPipeを作ってみます

雛形の生成

  • AngularCLIでPipeをgenerateします
ng generate pipe --name=mask
  • Pipeファイルが作成されました
% ng generate pipe --name=mask
CREATE src/app/mask.pipe.spec.ts (179 bytes)
CREATE src/app/mask.pipe.ts (201 bytes)
UPDATE src/app/app.module.ts (949 bytes)
  • 雛形の内容の確認します
src/app/mask.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'mask' })
export class MaskPipe implements PipeTransform {
  transform(value: any, ...args: any[]): any {
    return null;
  }
}
  • アノテーションで定義されている{ name: 'mask' }はPipeを使うときの名前です
    • この例では{{ 文字列 | mask }}といった具合で使うことになります
  • transform関数はpipeで渡された値を受け取り、returnした内容が画面に表示される値となります
    • 第2引数は{{ 文字列 | mask: 1 }}といった感じでPipeの後に任意の値を渡した場合に受け取ることができます

Pipeの処理を実装

  • Pipeの処理を作ります
  • マスク化の処理を書いていきます
src/app/mask.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'mask' })
export class MaskPipe implements PipeTransform {
  transform(value: string): string {
    return `${value.slice(0, -4)}****`;
  }
}
  • 渡された文字列の末尾4文字をマスク化して返す実装をしました
    • サンプルなので特殊ケースは考えません

Pipeを適用する

  • 作ったMaskPipeを使ってみます
src/app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: '<h1>{{ cardNumber | mask }}</h1>',
})
export class AppComponent {
  cardNumber = '1111-1111-1111-1111';
}
  • 画面上に1111-1111-1111-****と表示されているはずです!簡単!

Pipeに引数を渡す

  • もう少し機能を加えてみます
  • マスク化する文字数を指定できるようにしてみます
src/app/mask.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'mask' })
export class MaskPipe implements PipeTransform {
  transform(value: string, _size?: number): string {
    const size = _size || 4;
    return `${value.slice(0, -_size)}${'*'.repeat(size)}`;
  }
}
  • 第2引数でsizeを受け取るようにしてみました
    • 例によって例外ケースは考慮しません
  • 適用してみます
src/app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: '<h1>{{ cardNumber | mask: 2 }}</h1>',
})
export class AppComponent {
  cardNumber = '1111-1111-1111-1111';
}
  • 画面上に1111-1111-1111-11**と表示されているはずです!

テストコードを書く

  • せっかくなのでテストも書いてみます
  • Pipeのテストはtransform関数のテストを行う形になります
src/app/mask.pipe.spec.ts
import { MaskPipe } from './mask.pipe';

describe('MaskPipe', () => {
  const pipe = new MaskPipe();
  describe('文字数を指定しなかった場合', () => {
    it('末尾4文字がマスク化されること', () => {
      expect(pipe.transform('12345678')).toBe('1234****');
    });
  });
  describe('文字数を指定した場合', () => {
    it('末尾から指定した文字数分だけマスク化されること', () => {
      expect(pipe.transform('12345678', 2)).toBe('123456**');
    });
  });
});
  • 純粋なJavaScriptのロジックのテストなので書きやすいですね
  • 実行してみます
npm test -- --include src/app/mask.pipe.spec.ts

スクリーンショット 2020-02-05 18.57.57.png

  • すべてグリーンになりました!

まとめ

  • Pipeの実装は純粋なJavaScriptの関数なので学習コスト低く作れる
  • 同じ理由でテストコードの実装も簡単
  • Pipeは用法用量を守って使ったほうが良さそうだけどうまく使いこなすととても便利そうです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

100日チャレンジの230日目

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

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

230日目は

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

【WebAudioAPI】録音した音声をバイナリデータ化、PHPへ受け渡し

概要

Node.js上で、IBMのWatsonによって人が話した音声データを自動で文字起こしするスクリプトを作成しました。
その中で、結構苦労した
PCのマイクに直接アクセス→録音した音声データをバイナリデータ化、PHPへ受け渡し
の部分をメモがてら貼り付け。

環境

$php -v
PHP 7.1.23 (cli) (built: Feb 22 2019 22:19:32) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies

録音部分

hogehoge.js
// 音声データのバッファをクリアする
    audioData = [];

     //様々なブラウザでマイクへのアクセス権を取得する
    navigator.mediaDevices = navigator.mediaDevices || navigator.webkitGetUserMedia;

    //audioのみtrue。Web Audioが問題なく使えるのであれば、第二引数で指定した関数を実行
    navigator.getUserMedia({
        audio: true,
        video: false
    }, successFunc, errorFunc);

    function successFunc(stream) {
        const audioContext = new AudioContext();
        sampleRate = audioContext.sampleRate;

        // ストリームを合成するNodeを作成
        const mediaStreamDestination = audioContext.createMediaStreamDestination();

        // マイクのstreamをMediaStreamNodeに入力
        const audioSource = audioContext.createMediaStreamSource(stream);
        audioSource.connect(mediaStreamDestination);

        // 接続先のstreamをMediaStreamに入力
        for(let stream of remoteAudioStream){
            try{
                audioContext.createMediaStreamSource(stream).connect(mediaStreamDestination);
            } catch(e){
                console.log(e);
            }
        }

        // マイクと接続先を合成したMediaStreamを取得
        const composedMediaStream = mediaStreamDestination.stream;
        // マイクと接続先を合成したMediaStreamSourceNodeを取得
        const composedAudioSource = audioContext.createMediaStreamSource(composedMediaStream);

        // 音声のサンプリングをするNodeを作成
        const audioProcessor = audioContext.createScriptProcessor(1024, 1, 1);
        // マイクと接続先を合成した音声をサンプリング
        composedAudioSource.connect(audioProcessor);

        audioProcessor.addEventListener('audioprocess', event => {
            audioData.push(event.inputBuffer.getChannelData(0).slice());
        });

        audioProcessor.connect(audioContext.destination);
    }

録音した音声をバイナリデータ化

hogehoge.js
//音声をエクスポートした後のwavデータ格納用配列
    const waveArrayBuffer = [];
    //仕様の関係で、大きなデータを分けたうちの1つのデータ容量が25MB以下になるよう制御
    if (audioData.length > 250){
        const num = audioData.length/250;
        const count = Math.round(num);

        for (let i=0; i < count; i++){
            const sliceAudioData = audioData.slice(0,249);
            audioData.pop(0,249);
            const waveData = exportWave(sliceAudioData);
            waveArrayBuffer.push(waveData);
        }   
    }else{
        waveArrayBuffer.push(exportWave(audioData));
    }
   //PHPへPOST
    var oReq = new XMLHttpRequest();
    oReq.open("POST", '任意のパス', true);
    oReq.onload = function (oEvent) {
    // Uploaded.
    };

    //複数のデータをblob化するための配列
    const blob = [];
    //waveArrayBufferに入っている複数のデータを1つずつ配列に格納
    waveArrayBuffer.forEach(function(waveBuffer){
        blob.push(new Blob([waveBuffer], {type:'audio/wav'}));
    })

    var fd = new FormData();
    for (let i=0; i < blob.length; i++){
        fd.append('blob'+i,blob[i]);
    }
    // oReq.setRequestHeader('Content-Type','multipart/form-data; name="blob" boundary=\r\n');
    //配列ごとリクエスト送信
    oReq.send(fd);

    function exportWave(audioData) {
    // Float32Arrayの配列になっているので平坦化
    const audioWaveData = flattenFloat32Array(audioData);
    // WAVEファイルのバイナリ作成用のArrayBufferを用意
    const buffer = new ArrayBuffer(44 + audioWaveData.length * 2);

    // ヘッダと波形データを書き込みWAVEフォーマットのバイナリを作成
    const dataView = writeWavHeaderAndData(new DataView(buffer), audioWaveData, sampleRate);

    return buffer;
    }

    // Float32Arrayを平坦化する
    function flattenFloat32Array(matrix) {
        const arraySize = matrix.reduce((acc, arr) => acc + arr.length, 0);
        let resultArray = new Float32Array(arraySize);
        let count = 0;
        for(let i = 0; i < matrix.length; i++) {
            for(let j = 0; j < matrix[i].length; j++) {
            resultArray[count] = audioData[i][j];
            count++;
            }
        }
        return resultArray;
    }
    // ArrayBufferにstringをoffsetの位置から書き込む
    function writeStringForArrayBuffer(view, offset, str) {
        for(let i = 0; i < str.length; i++) {
            view.setUint8(offset + i, str.charCodeAt(i));
        }
    }

    // 波形データをDataViewを通して書き込む
    function floatTo16BitPCM(view, offset, audioWaveData) {
        for (let i = 0; i < audioWaveData.length; i++ , offset += 2) {
            let s = Math.max(-1, Math.min(1, audioWaveData[i]));
            view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
        }
    }

    // モノラルのWAVEヘッダを書き込む
    function writeWavHeaderAndData(view, audioWaveData, samplingRate) {
        // WAVEのヘッダを書き込み(詳しくはWAVEファイルのデータ構造を参照)
        writeStringForArrayBuffer(view, 0, 'RIFF'); // RIFF識別子
        view.setUint32(4, 36 + audioWaveData.length * 2, true); // チャンクサイズ(これ以降のファイルサイズ)
        writeStringForArrayBuffer(view, 8, 'WAVE'); // フォーマット
        writeStringForArrayBuffer(view, 12, 'fmt '); // fmt識別子
        view.setUint32(16, 16, true); // fmtチャンクのバイト数(第三引数trueはリトルエンディアン)
        view.setUint16(20, 1, true); // 音声フォーマット。1はリニアPCM
        view.setUint16(22, 1, true); // チャンネル数。1はモノラル。
        view.setUint32(24, samplingRate, true); // サンプリングレート
        view.setUint32(28, samplingRate * 2, true); // 1秒あたりのバイト数平均(サンプリングレート * ブロックサイズ)
        view.setUint16(32, 2, true); // ブロックサイズ。チャンネル数 * 1サンプルあたりのビット数 / 8で求める。モノラル16bitなら2。
        view.setUint16(34, 16, true); // 1サンプルに必要なビット数。16bitなら16。
        writeStringForArrayBuffer(view, 36, 'data'); // サブチャンク識別子
        view.setUint32(40, audioWaveData.length * 2, true); // 波形データのバイト数(波形データ1点につき16bitなのでデータの数 * 2となっている)

        // WAVEのデータを書き込み
        floatTo16BitPCM(view, 44, audioWaveData); // 波形データ

        return view;
    }

リクエスト受け取り部分(超絶一部抜粋)

hogehoge.php
//リクエスト受け取り
$req = $_FILES
var_dump($req);

//出力結果
array(2) {
  ["blob0"]=>
  array(5) {
    ["name"]=>
    string(4) "blob"
    ["type"]=>
    string(9) "audio/wav"
    ["tmp_name"]=>
    string(14) "/tmp/ランダム文字列"
    ["error"]=>
    int(0)
    ["size"]=>
    int(509996)
  }

おわりに

ご指摘等ありましたら宜しくお願い致します!

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

gulp + webpack + babelをつかってみた

はじめに

以前作ったこれ(文字列→塩基配列の相互変換ツールをつくってみた(アプリ版))のWeb版でせっかくなんでよく聞くgulp, webpack, babelをつかってみました。色々みながらやりましたがバージョンの違いなどでエラー出まくってなかなか苦労しました。

あんまりwebさわったことないので間違ってたら優しく指摘していただけるとありがたいです。

とりあえず動くものはできました!!

やりたいこと

  • SassをCSSに変換
  • CSS圧縮
  • 画像圧縮
  • js圧縮
  • babelでjsをES5で出力
  • webpackでjsをまとめる

ソース

環境

  • macOS Catalina 10.15.2
  • node.js v13.7.0

npm導入

yarn ていうのもあるらしいですがとりあえず npm をインストール!

node.js で最新版をいれました。

ターミナルで下記コマンドが実行できれば導入完了。

node -v

ターミナルでプロジェクトフォルダに移動し下記コマンドを実行し、 package.json を作成!

npm init -y

gulp導入

下記コマンドで gulp を導入!

npm install -D gulp

gulp でやること

  • SassをCSSに変換
  • CSS圧縮
  • 画像圧縮

下記コマンドで必要なやつを入れる!

npm install -D gulp-sass gulp-changed gulp-imagemin imagemin-pngquant imagemin-mozjpeg
  • gulp-sass
    SassをCSSに変換/CSS圧縮用
  • gulp-changed
    画像圧縮の際に差分のみやる用
  • gulp-imagemin imagemin-pngquant imagemin-mozjpeg
    画像圧縮用

gulpfile.js を下記のように作成する。

gulpfile.js
var gulp = require('gulp');
var sass = require('gulp-sass');
var changed  = require('gulp-changed');
var imagemin = require('gulp-imagemin');
var imageminPngquant = require("imagemin-pngquant");
var imageminMozjpeg = require("imagemin-mozjpeg");

var dist = './dist'; // 出力先

// sassをコンパイルしてcss圧縮
gulp.task('sass', function(done){
  gulp.src('./src/sass/*.scss')
    .pipe(sass({outputStyle: 'compressed'})) // 圧縮
    .pipe(gulp.dest(dist + '/css'));
  done();
});

// 圧縮前と圧縮後のディレクトリを定義
var paths = {
  srcDir : 'src',
  dstDir : dist
};

// いろんな拡張子を圧縮するためのオプション
var imageminOption = [
  imageminPngquant({ quality: [0.65, 0.8] }),
  imageminMozjpeg({ quality: 85 }),
  imagemin.gifsicle({
    interlaced: false,
    optimizationLevel: 1,
    colors: 256
  }),
  imagemin.optipng(),
  imagemin.svgo()
];

// 画像圧縮
gulp.task('imagemin', function(done){
  var srcGlob = paths.srcDir + '/**/*.+(jpg|jpeg|png|gif|svg)'; // 元画像
  var dstGlob = paths.dstDir; // 圧縮先
  gulp.src(srcGlob)
    .pipe(changed(dstGlob)) //差分のみ圧縮
    .pipe(imagemin(imageminOption))
    .pipe(gulp.dest(dstGlob));
  done();
});

gulp.task('default', gulp.series('sass', 'imagemin'), function(done) {
  done();
});

フォルダ構成

project/
  ├ src/
  │   ├ /sass
  │   │  └ index.scss
  │   └ /img
  │      └ image.png
  ├ dist/
  ├ gulpfile.js
  └ package.json

下記コマンドを実行!!

npx gulp

Sass を CSS に変換、CSS 圧縮、画像圧縮ができました:tada:

ハマったとこ

CSSの圧縮

最初は css の圧縮を gulp-minify-css でやろうとして、sass のコンパイル後に実行する方法がわからず...

結局 gulp-sass でオプションを設定するだけで圧縮までできました。

画像の圧縮

jpeg 画像圧縮を imagemin.jpegtran({progressive: true}) で書いててエラーでつまりました。

jpegtran は使えない?みたいなエラーだったので imagemin-mozjpeg をいれました。

webpack導入

webpackでやること

  • js圧縮
  • webpackでjsをまとめる

下記コマンドで必要なやつを入れる!

npm install -D webpack webpack-stream terser-webpack-plugin
  • webpack-stream
    gulpでwebpackを使う用
  • terser-webpack-plugin
    js圧縮用

webpack.config.js を下記のように作成する

webpack.config.js
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'production', // 本番用(開発ならdevelopment(圧縮されない))
  entry: './src/js/index.js', // バンドル前のやつのエントリポイント
  output: { // バンドル先
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist/js')
  },
  optimization: {
        minimizer: [ // js圧縮
        new TerserPlugin({
            extractComments: 'all', // コメント削除
            terserOptions: {
                compress: {
                  drop_console: true, // console.log削除
                },
            },
        }),
      ],
    }
   };

js 圧縮参考:Webpack 4 圧縮時にJavaScriptのコメントを全て削除する

フォルダ構成

project/
  ├ src/
  │   ├ /sass
  │   │  └ index.scss
  │   ├ /js
  │   │  ├ other.js 
  │   │  └ index.js
  │   └ /img
  │       └ image.png
  ├ dist/
  ├ gulpfile.js
  ├ webpack.config.js
  └ package.json

gulpfile.js に下記を追加する。

gulpfile.js
var webpackStream = require("webpack-stream");
var webpack = require("webpack");
var webpackConfig = require("./webpack.config");

gulp.task("webpack", function(done){
  webpackStream(webpackConfig, webpack)
    .pipe(gulp.dest(dist + '/js'));
  done();
});

// webpack追加
gulp.task('default', gulp.series('sass'', 'imagemin', 'webpack), function(done) {
  done();
});

npx gulp でバンドルされる:tada:

ハマったとこ

jsの圧縮

  • UglifyJsPlugin でやろうとしたら対応してなかった...
  • mode: 'development', にしてたので圧縮されなかった...

importとexport

import * from モジュール でモジュール内のやつが全部呼べると思ったがそんなことはなかった

export default function test(){}import Default from モジュールexport default つけたやつ全部呼べると思ったけどそんなことはなかった(そもそも export default でエラーになる)

最終的には下記の様な感じで落ち着いた。

test.js
export function testA() {
}

export function testB() {
}
index.js
import {testA, testB} from './test';

testA();
testB();

import, export についてはもうちょっと勉強が必要:frowning2:

出力先

下記の path: path.join(__dirname, 'dist/js') のところが出力先だと思うのですが

webpack.config.js
output: { // バンドル先
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist/js')
  },

github pages で公開したかったのでフォルダ名を dist から docs に変えたのですが gulpfile.js だけ書き換えて webpack.config.js は dist のままでしたが docs/js にちゃんと bundle.js ができてました:thinking:

babel導入

下記コマンドで必要なやつを入れる!

npm install -D webpack babel-loader @babel/core @babel/preset-env

webpack.config.js に下記を追加。

webpack.config.js
optimization {



},
module: { // ここ追加
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env', // デフォルトでES5になるはず
              ]
            }
          }
        ]
      }
    ]
  }

npx gulp で ES5 で出力される:tada:

インストールしたやつのバージョン

npm install したやつのバージョンを記載しておきます。

"@babel/core": "^7.8.4",
"@babel/preset-env": "^7.8.4",
"babel-loader": "^8.0.6",
"gulp": "^4.0.2",
"gulp-changed": "^4.0.2",
"gulp-imagemin": "^7.1.0",
"gulp-sass": "^4.0.2",
"imagemin-mozjpeg": "^8.0.0",
"imagemin-pngquant": "^8.0.0",
"terser-webpack-plugin": "^2.3.4",
"webpack": "^4.41.5",
"webpack-stream": "^5.2.1"

さいごに

前作ったやつは XFREE のPHPサーバーで公開していたのですが、これ PHP いらなくね?と思いPHP の処理部分を js で置き換えてHTMLサーバーで公開しました。これで広告は出ない:clap:(が、まだhttp...)

PHP なくなったから github pages でいけるやんと思い完成したのがこちら

https://adventam10.github.io/DNAConverter-web_vue/

https!!:clap:

せっかくなんでユニバーサルリンクとかやりたい

参考

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

【JavaScript】Pagination.jsでページネーションをさくっと実装

人生には、JavaScriptでページネーションすることもあるでしょう。僕はありました。
というわけでPagination.jsを使った例を丁寧めに紹介します。
image.png

HTML, JavaScript

Pagination.jsjQueryを公式から持ってきて下さい。スタイルは次の章で説明します。

<head>
  <link rel="stylesheet" href="./contents.css"><!-- 後で -->
  <link rel="stylesheet" href="./pagination.css"><!-- 後で -->
</head>
<body>
  <ul>
    <div id="diary-all-contents"></div><!-- コンテンツの埋め込み先をid指定 -->
  </ul>
  <div class="pager" id="diary-all-pager"></div><!-- ページャーの埋め込み先をid指定 -->
  <script src="./jquery-3.3.1.min.js"></script>
  <script src="./pagination.min.js"></script>
  <script>
    // [1] 配列のデータを用意
    var diary = [
      {
        title: "○○を買った",
        link: "https://foobar/diary/1234",
        category: "交流",
        class: "Relationship",
        date: "2020年02月04日"
      },
      // ...以下略
    ]
    // [2] pagination.jsの設定
    $(function() {
      $('#diary-all-pager').pagination({ // diary-all-pagerにページャーを埋め込む
        dataSource: diary,
        pageSize: 5, // 1ページあたりの表示数
        prevText: '&lt; 前へ',
        nextText: '次へ &gt;',
        // ページがめくられた時に呼ばれる
        callback: function(data, pagination) {
          // dataの中に次に表示すべきデータが入っているので、html要素に変換
          $('#diary-all-contents').html(template(data)); // diary-all-contentsにコンテンツを埋め込む
        }
      });
    });
    // [3] データ1つ1つをhtml要素に変換する
    function template(dataArray) {
      return dataArray.map(function(data) {
        return '<li class="list"><a href="' + data.link + '">'
        + '<p class="category category-' + data.class + '">' + data.category + '</p>'
        + '<p class="title">' + data.title + '</p>'
        + '<p class="date">' + data.date + '</p></a></li>'
      })
    }
  </script>
</body>

[1] データの用意

Pagination.jsに渡すデータは配列であればなんでもOK。
今回はこういうデータの配列があるとしましょう。

var diary = [
  {
    title: "○○を買った",
    link: "https://foobar/diary/1234",
    category: "交流",
    class: "Relationship",
    date: "2020年02月04日"
  },
  // ...以下略
]

[2] Pagination.jsの設定

要素の埋め込み先、1ページあたりの表示数、「戻る」「進む」の表記などを設定できる。

$(function() {
  $('#diary-all-pager').pagination({ // diary-all-pagerにページャーを埋め込む
    dataSource: diary,
    pageSize: 5, // 1ページあたりの表示数
    prevText: '&lt; 前へ',
    nextText: '次へ &gt;',
    // ページがめくられた時に呼ばれる
    callback: function(data, pagination) {
      // dataの中に次に表示すべきデータが入っているので、html要素に変換
      $('#diary-all-contents').html(template(data)); // diary-all-contentsにコンテンツを埋め込む
    }
  });
});

以下のコールバックはページがめくられた時に呼び出され、次に表示すべきデータも渡ってくる。
このデータをhtml要素に変換することで自由な表示が可能。関数の中身は次で。

    // ページがめくられた時に呼ばれる
    callback: function(data, pagination) {
      // dataの中に次に表示すべきデータが入っているので、html要素に変換
      $('#diary-all-contents').html(template(data)); // diary-all-contentsにコンテンツを埋め込む
    }

他の設定は公式ドキュメントを見て下さい。

[3] データをHTMLへ

ここでは、Pagination.jsが渡してくれた表示データをHTML要素に変換する。
今回は予め外側を<ul>要素で囲んでおいたので、そこに埋め込むための<li>のリストを作る。
ここにスタイルを当てるのは、素のJavaScriptだとちょっと辛い。

function template(dataArray) {
  return dataArray.map(function(data) {
    return '<li class="list"><a href="' + data.link + '">'
    + '<p class="category category-' + data.class + '">' + data.category + '</p>'
    + '<p class="title">' + data.title + '</p>'
    + '<p class="date">' + data.date + '</p></a></li>'
  })
}

4行目ではカテゴリによってクラスが変わるようにしている。

    + '<p class="category category-' + data.class + '">' + data.category + '</p>'

data.classがQiitaならcategory-Qiitaというクラスに。対応するCSSは後述。
image.png

スタイル

コンテンツのスタイル

この部分です。
image.png
ここは表示するデータや目的に応じて好きなスタイルを付けて下さい。
あくまで一例として今回使用したスタイルを記載。

contents.css
p, a, ul, li {
  margin: 0;
  padding: 0;
  border: 0;
}
ul {
  list-style: none;
}
.list {
  line-height: 1;
  border-bottom: 1px solid #d8d8d8;
  padding: 12px 0;
}
.list a {
  display: flex;
  align-items: center;
  text-decoration: none;
}
.list .date {
  color: darkblue;
  margin: 0 15px 0 auto;
}
.list .category {
  margin: 0 15px 0;
  padding: 4px;
  display: flex;
  align-items: center;
  color: white;
}
 /* カテゴリによって違う背景色に */
.list .category-Qiita {
  background-color: lightblue;
}
.list .category-Relationship {
  background-color: lightseagreen;
}
.list .category-Food {
  background-color: lightslategray;
}
.list .category-Family {
  background-color: lightcoral;
}
.pager {
  margin: 18px 0 0 0;
}

ページャーのスタイル

この部分です。
image.png
デフォルトでpaginationjsなどのクラス名が付いたHTMLを出力されるので、それにスタイルを付けます。
もっとよい方法があるかもしれません。

pagination.css
.paginationjs-pages > ul > li > a {
  padding: 6px 18px;
  color: white;
  background-color: lightgreen;
  border: 1px solid darkcyan;
}
.paginationjs-pages > ul > li > a:hover {
  color: black;
  background-color: white;
}
.paginationjs-pages > ul > li.active > a {
  color: black;
  background-color: white;
}
.paginationjs-pages > ul > li.disabled > a {
  color: black;
  background-color: white;
}
.paginationjs-prev {
  margin: 0 16px 0 0;
}
.paginationjs-next {
  margin: 0 0 0 16px;
}
.paginationjs-page {
  margin: 0 4px;
}
.paginationjs-pages > ul {
  display: flex;
  align-items: baseline;
}
.paginationjs-pages > ul > li.paginationjs-ellipsis.disabled > a {
  border: none;
  color: black;
  margin: 0 4px;
  padding: 0;
}

感想

さくっと実装できました。遷移もスムーズでよかったです。
Pagination.jsはけっこう奥が深そうなので、次の機会があればもう少し掘り下げようと思います。

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

[React]テーブルのセルを結合する

概要

  • Reactで配列をループさせてテーブルを描画する
  • 特定の項目についてセルを結合させたい

作成イメージ

スクリーンショット 2020-02-05 15.01.31.png

  • 共通する項目についてセルを結合する

調べたこと

<table border="3">
  <tr>
    <td rowspan="3">垂直方向の結合</td>
    <td>データ1</td>
  </tr>
  <tr>
    <td>データ2</td>
  </tr>
  <tr>
    <td>データ3</td>
  </tr>
</table>

実装

  • 表示する配列
src/constants/GroupList.js
import { nogizaka, keyakizaka, hinatazaka } from "./Color";

const groupList = [
  {
    id: 1,
    name: "乃木坂46",
    color: nogizaka.color,
    memberList: [
      {
        id: 1,
        name: "齋藤飛鳥",
        age: 21
      }
    ]
  },
  {
    id: 2,
    name: "欅坂46",
    color: keyakizaka.color,
    memberList: [
      {
        id: 1,
        name: "渡邉理佐",
        age: 21
      },
      {
        id: 2,
        name: "小林由依",
        age: 20
      }
    ]
  },
  {
    id: 3,
    name: "日向坂46",
    color: hinatazaka.color,
    memberList: [
      {
        id: 1,
        name: "齊藤京子",
        age: 22
      },
      {
        id: 2,
        name: "小坂菜緒",
        age: 17
      },
      {
        id: 3,
        name: "上村ひなの",
        age: 15
      }
    ]
  }
];

export default groupList;

  • 表示するコンポーネント
src/App.js
import React from "react";
import { Table } from "react-bootstrap";
import styled from "styled-components";
import groupList from "./constants/GroupList";

const ColorTr = styled.tr`
  color: ${({ color }) => color};
`;

function App() {
  return (
    <Table bordered>
      <thead>
        <tr>
          <th>グループ</th>
          <th>名前</th>
          <th>年齢</th>
        </tr>
      </thead>
      <tbody>
        {groupList.map(group =>
          group.memberList.map((member, i) =>
            i === 0 ? (
              <ColorTr key={member.id} color={group.color}>
                <td rowSpan={group.memberList.length}>{group.name}</td>
                <td>{member.name}</td>
                <td>{member.age}</td>
              </ColorTr>
            ) : (
              <ColorTr key={member.id} color={group.color}>
                <td>{member.name}</td>
                <td>{member.age}</td>
              </ColorTr>
            )
          )
        )}
      </tbody>
    </Table>
  );
}

export default App;

行ったこと

  • 配列の個数を結合させる行数としてrowspanの値に設定
src/App.js
<td rowSpan={group.memberList.length}>{group.name}</td>
  • ループさせる配列のインデックスをとり、先頭行の場合(インデックスが0の場合)とそうでない場合を分岐
src/App.js
{groupList.map(group =>
  group.memberList.map((member, i) =>
    i === 0 ? (
      <ColorTr key={member.id} color={group.color}>
        <td rowSpan={group.memberList.length}>{group.name}</td>
      // 省略
      </ColorTr>
    ) : (
      // 省略
    )
  )
)}

終わりに

  • とりあえず実現してみたけど何だか冗長・・・
  • もっと良い方法をご存知の方いましたらコメントください

余談

本題とは全く関係ありませんが、今回使用したカラーコードはそれぞれ以下を参考にしています。

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

JSで要素同士に線や矢印を引けるライブラリ"LeaderLine"

LeaderLineとは

矢印や線の描画、範囲の描画が簡単に行えるオープンソース・ソフトウェアです。

https://anseki.github.io/leader-line/

使用方法

1、ホームページでフォルダのダウンロードを行います

2、ダウンロードしたフォルダの"leader-line.min.js"をwebページに埋め込みます

test.html
<script src="leader-line.min.js"></script>

3、矢印や線でつなぎたい要素をLeaderLineのコンストラクタに渡す

test.js
new LeaderLine(
  document.getElementById('要素1'),
  document.getElementById('要素2')
);

使用例を一部紹介します

1、ノーマル

test.js
new LeaderLine(
  document.getElementById('start'),
  document.getElementById('end')
);

image.png

2、サイズや形、色の変更

test.js
new LeaderLine(element1, element2, {
  startPlugColor: '#1a6be0',
  endPlugColor: '#1efdaa',
  gradient: true
});
new LeaderLine(element2, element3, {dash: {animation: true}});
new LeaderLine(element4, element5, {dropShadow: true});
new LeaderLine(element5, element6, {dash: true});

image.png

3、一定条件に矢印がひかれる

test.js
new LeaderLine(LeaderLine.mouseHoverAnchor(startElement), endElement);

ex-110.gif

4、範囲を描画できる

test.js
new LeaderLine(startElement, LeaderLine.areaAnchor(endElement));

image.png

最後に

この他にもスクロールによる要素の位置の変更にも対応できたり、
div要素だけでなくbutton,ul,text,circle,,,などにも線をつなげることができます。詳しくはこちら
使ってみて便利だったので紹介させていただきました。

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

JavaScriptのつまずきやすいとこ

オブジェクト

配列とオブジェクトの違い

どちらも複数の値をひとまとめにし、key-valueの形になっている。
配列はkeyが番号だが、オブジェクトはkeyが名前を持っている。
(実は配列もオブジェクトの一種)

オブジェクトの作り方

new演算子を使うか、オブジェクトリテラルかの2パターン。

new演算子

new演算子を使うとコンストラクタ(ある種の関数)が呼び出される。
コンストラクタがnewで呼び出された場合、新しいオブジェクトが作成され、それがthisになる。
newにより作成されたオブジェクトのことをコンストラクタから見てインスタンスと呼ぶ

関数=オブジェクト

関数定義とは、作った関数を変数に入れる作業である。

function hello(){
  alert("hello");
}
console.log(typeof(hello));//functionと表示

functionと表示されているが、オブジェクトである。
ただ()をつけると呼び出せるというのは関数だけの特徴である。

const a = {};
a.hello = greeting;
a.hello();

function greeting(){
    alert("hello");
}

関数定義は実行される場所より後ろに書いてあっても機能する

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

WebWorker

WebWorkerのお勉強。

まずは最小限のコードでWebWorkerを動かしてみる。
任意のデータをワーカーに渡して、オウム返しして表示するだけ。

メインスレッド
const worker = new Worker('worker.js')
worker.onmessage = event => console.dir(event.data)
worker.postMessage([1,2,3])
worker.js
onmessage = event => postMessage(event.data)

インラインworker

worker.jsが外部ファイルなのが気に入らない場合は、内部化できる。

クラスを文字列化し、それをBlob・URL化すれば、worker.jsの代わりになる。
文字列化するクラスは、ワーカーのコンテキストで動作することに注意すれば普通に書ける。

また、ワーカーは非同期処理なので、プロミス化するのが好手。

データはクラスのコンストラクタに渡され、コンストラクタの戻り値がプロミスの値となる。

これら一連の作業を関数化したのが、次のコードになる。

使い方
const workerが返す値 = await inlineWorker(worker化するクラス, workerに渡すデータ)
function inlineWorker(fn, data){
    const code = `onmessage = event => postMessage(new ${fn.toString()}(event.data))`
    const blob = new Blob([code], {type: 'text/javascript'})
    const url  = URL.createObjectURL(blob)

    function async(ok, ng){
        const worker = new Worker(url)
        worker.onmessage = event => ok(event.data)
        worker.onerror   = event => ng(event)
        worker.postMessage(data)
    }
    return new Promise(async)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Chromeがsourcemapをリクエストするタイミング

jsとかのsourcemapがどのタイミングでリクエストされるか気になったので挙動を確認した。
chrome devtoolsを開いてるかどうかを変えて挙動を見る。

前提

  • chrome version: 80.0.3987.87
  • bundler: webpack
    • version: 4系
    • devtool option: source-map(コンパイルされたコードとは別ファイルでsourcemapを用意するやつ)

結果

  • chrome devtoolsのパネルを開かないで(画面に含まれる<script>経由で)jsをリクエストした場合
    • js.mapはリクエストされない
    • その後、chrome devtoolsを開くとjs.mapがリクエストされる。画面のリロードは必要ない。
  • chrome devtoolsを開いた状態でjsをリクエストした場合
    • js.mapはリクエストされる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript上でObjectをRailsで取得できる形のFormDataへ変換する

概要

TypeScript上で、Fileを含んだObjectをRails APIにputやpostしたい。
→ そのままだとFileがうまく渡らないのでFormDataにする必要がある。
File以外のパラメータも含んだObjectFormDataに変換したい。

コード

/**
 * Convert object to FormData which rails can use.
 * This function is useful for uploading files.
 *
 * ex)
 * { id: 1, hero: { id: 1, name: 'NewHero' }, items: [1, 2] }
 * -> FormData with following parameters
 * id: 1
 * hero[id]: 1
 * hero[name]: NewHero
 * items[]: 1
 * items[]: 2
 *
 * @param params
 * @return FormData
 */
export const convertParamsToForm = (params: object): FormData => {
  const formData = new FormData();
  const appendParamsToForm = (variable, prefix = '') => {
    if (typeof variable !== 'object' || variable instanceof File) {
      formData.append(prefix, variable);
      return;
    }
    if (Array.isArray(variable)) {
      variable.forEach(value => appendParamsToForm(value, `${prefix}[]`));
      return;
    }
    Object.keys(variable).forEach(key => {
      appendParamsToForm(
        variable[key] || '',
        prefix ? `${prefix}[${key}]` : key
      );
    });
  };
  appendParamsToForm(params);
  return formData;
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript でも Kotlin の trimMargin と trimIndent を使いたい

Kotlin には String#trimMarginString#trimIndent という便利なメソッドがあり、raw strings (JS のテンプレートリテラルに近いもの) を使いやすくしています。

String#trimMargin は各行の第2引数に指定した文字列(省略した場合は|) までの空白を削除し、String#trimIndent は各行の左側の空白を一番少ない行に合わせて削除します。
ついでに最初と最後の行が空白のみ場合はその行を削除してくれます。

Kotlin
println("String#trimMargin()")
val withoutMargin1 = """ABC
            |123
                |456""".trimMargin()
println(withoutMargin1)

println("String#trimMargin(marginPrefix: String)")
val withoutMargin2 = """
#XYZ
    #foo
    #bar
""".trimMargin("#")
println(withoutMargin2)

println("String#trimIndent()")
val withoutIndent = """
        ABC
         123
          456
        """.trimIndent()
println(withoutIndent)
Output
String#trimMargin()
ABC
123
456
String#trimMargin(marginPrefix: String)
XYZ
foo
bar
String#trimIndent()
ABC
 123
  456

これを JavaScript にテンプレートリテラルに付けられるタグ、及び関数として利用できるように再現します。

JavaScript
console.log('trimMargin`~`')
val withoutMargin1 = trimMargin`ABC
            |123
                |456`
console.log(withoutMargin1)

console.log('trimMargin(str: string, marginPrefix: string)')
val withoutMargin2 = trimMargin(`
#XYZ
    #foo
    #bar
`, '#')
console.log(withoutMargin2)

console.log('String`~`')
val withoutIndent = trimIndent`
        ABC
         123
          456
        `
console.log(withoutIndent)
Output
trimMargin`~`
ABC
123
456
trimMargin(str: string, marginPrefix: string)
XYZ
foo
bar
String`~`
ABC
 123
  456

実装

テンプレートリテラル付けられるタグを作成する場合、関数に渡る引数は (literals: TemplateStringsArray, ...placeholders: string[]) になります。
今回の場合は普通に展開してくれれば良いので、愚直に展開する関数を作成します。

function resolveTenplate(literals: TemplateStringsArray, ...placeholders: string[]): string {
  return literals.reduce((str, literal) => (str += literal + (placeholders.shift() ?? '')), '');
}

trimMargin

第一引数が string かどうかでタグとして呼び出されたか関数として呼び出されたかの判別が付きます。
除去には手っ取り早く正規表現を使用します。

export function trimMargin(string: string, marginPrefix?: string): string;
export function trimMargin(literals: TemplateStringsArray, ...placeholders: string[]): string;
export function trimMargin(
  arg1: string | TemplateStringsArray,
  arg2 = '',
  ...args: string[]
): string {
  const string = typeof arg1 === 'string' ? arg1 : resolveTenplate(arg1, arg2, ...args);
  const strings = string.split('\n');
  if (!strings?.[0].trim()) strings.shift();
  if (!strings?.[strings.length - 1].trim()) strings.pop();
  const marginPrefix = (typeof arg1 === 'string' && arg2) || '|';
  const regexp = marginPrefix === '|' ? /^\s*\|/ : new RegExp(`^\\s*${arg2}`);
  return strings.map(s => s.replace(regexp, '')).join('\n');
}

trimIndent

こちらも一応関数として使えるようにオーバーロードを用意します。
正規表現で頭の空白のみを取り出し、その length が一番短いものに合わせて Array#slice します。

export function trimIndent(string: string): string;
export function trimIndent(literals: TemplateStringsArray, ...placeholders: string[]): string;
export function trimIndent(arg1: string | TemplateStringsArray, ...args: string[]): string {
  const string = typeof arg1 === 'string' ? arg1 : resolveTenplate(arg1, ...args);
  const strings = string.split('\n');
  if (!strings?.[0].trim()) strings.shift();
  if (!strings?.[strings.length - 1].trim()) strings.pop();
  const indent = Math.min(...strings.map(s => /^\s+/.exec(s)?.[0].length ?? 0));
  return strings.map(s => s.slice(indent)).join('\n');
}

※あんまり詳しく仕様を調べていないので、Kotlin と挙動が異なるかもしれません。

参考文献

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

Google Sites を旧型から新型へ移行する際にやったこと&ついでにやったこと

初めに

ここでは業務で Google Sites を旧型から新型へ移行する際にやったこと、ついでにやったことをまとめています。旧型の Google Sites はいずれ近いうちにサポートが切れるので旧型を使い続けている方々は注意が必要です

そもそも Google Sites とは何か

その名の通り、Google 社によるウェブサイト作成サービスです。「忘れられたサービス」という異名が一部ではつけられています。

Google Sites の強み

  • ハイパーリンクを張りやすい(いちいち<a>タグを貼らなくていい)
  • 文字フォントの変更がやりやすい
  • ウェブページにファイルを添付しやすい
  • サイトマップを自動で作れる(検索エンジン最適化で必ず必要になります)
  • FTPが不要
  • 自分でサーバーを設定する必要がない
  • (何も設定しなくても)文字化けを心配しなくて大丈夫(試してみたら日本語や英語だけでなく、簡体字なども問題なく使えます)

文字化け対策を心配しなくていいのは助かります。というのも通常のHTMLだと全てのページで head に以下を追加する手間がかかるからです:

text.html
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<META http-equiv="Content-Type" content="text/html; charset=ISO-2022-JP">
<META http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<META http-equiv="Content-Type" content="text/html; charset=EUC-JP">
</head>

この手間を省けるのはありがたいです。

弱点

  • 付属しているHTMLエディタが小さくて見えづらい
  • FTPを使いたくても使えない
  • 数式を出力するのに必要なMathJax が使えない(代わりにCodecogsを使えばよい、これを使えば<img>タグで数式を画像として埋め込められる)

作業環境

Google Sites の編集作業は全てブラウザ上で行います。Windows 10 Pro 上で Google Chrome, Mozilla Firefox, Brave などで動作確認してます。また、文法チェックや入力補助系のブラウザ拡張機能は無効にしています(Google Sites の編集を妨げる可能性があるからです)。

新型 Google Sites では何が変わるのか

新型 Google Sites は2016年11月に公開されました。新しい機能がいろいろあるのですが(各種ショートカットキーは一緒です)、以下が特筆すべき点ではないかと思います:

  • 編集画面とプレビューを別々に閲覧できる(旧型では編集中のプレビュー画面はなかった)
  • ウェブサイトや添付ファイルが自動で Google Drive に保存される(各ページにある添付ファイルはそれぞれ別個のフォルダに振り分けられる)
  • HTML/JavaScript などの埋め込みが容易になった(旧型では埋め込みを勝手に差し戻されることが多々あった)
  • 画面横にあるサイトマップが詳細に表示される(旧型では Subsection までしか表示されなかった)
  • ドラッグするだけでテキストボックスの大きさ・位置を調整できる

特に簡単な操作でテキストボックスを操れるのは魅力的ではないかと思います。というのも、テキストボックスをHTMLで設定するのにはひと手間かかるからです:

textbox.html
  <div style="margin:0px;padding:0px;" align="center">
    <div style="margin:0px;padding:0px;line-height:1.3;">
      <div style="margin:0px;padding:10px;line-height:1.3;overflow:auto;text-align:left;height:350px;">
Enter to textbox.
</div></div></div>

この手間を省けるのは助かります。

旧型から新型 Google Sites に移行したら必ずやるべきこと

ここからが本題です。

フォントとレイアウトの確認

旧型から新型に移行した際にはテキスト及びハイパーリンクは保持されるものの、文字のサイズやテキストボックス、プラグインの位置は保持されません。適宜手動で再設定しなくてはいけません。旧型では文字サイズは大体自由に決められたのですが新型では

  • Normal Text
  • Title (<h1>に相当)
  • Heading (<h2>に相当)
  • Subheading (<h3>に相当)
  • Small

しか選択肢がありません。テキストボックスの大きさ、位置はドラッグするだけで十分です。

プラグインの動作確認

旧型で使えていたプラグインが新型でもしっかり動作するという保証はありません。もし動かなくなったら面倒でも設定しましょう。私が移行作業をした際は連絡フォームが動作しなくなったので新しく作り直しました(123FormBuilder という無料サービスを使っています)

ついでにやったこと(おまけ)

ここからはおまけです(随時追加します)。

旧型で作成したサイトのバックアップ

旧型の Google Sites ではページ全体のHTMLソースを表示するエディタが付属していたので(新型だとないみたいです)、HTMLソースをGitLab/BitBucket などに保存しておきました。ウェブ魚拓Webpage Archive も使えます。

コードの埋め込み

上述の通り、新型ではコードの埋め込みが容易になったので、旧型ではできなかったこと(もしくはやりづらかったこと)を色々やってみました。

JavaScript の埋め込み

最終更新日の自動表示

旧型の Google Sites だと最終更新日を自動で表示してくれましたが、新型ではこれがなくなったみたいなので、JavaScript で埋め込みました。

LastUpdate.js
document.write("Last Update: " + new Date(document.lastModified))

更新時刻まで表示したくない場合は次の書き方もできます:

LastUpdateJPN.js
var day = new Date(document.lastModified);
var y = day.getFullYear();          // Year
var m = day.getMonth() + 1;         // Month
var d = day.getDate();              // Day
document.write("最終更新: " + y + "" + m + "" + d + "");

コピーライト年号の自動更新

これも JavaScript を使えばできます:

copyright.html
Copyright &copy; 2000<script type="text/javascript">new Date().getFullYear()>2010&&document.write("-"+new Date().getFullYear());</script>, All Rights Reserved.

各種プラグインの埋め込み

アクセスカウンターの埋め込み

無料でアクセスカウンターを埋め込む方法はいろいろありますが、私はこちらを使ってます。

参考資料

関連サイト

JavaScript 関連

組み合わせて使うと便利なウェブツール

  • Pandoc, Online Converter (いろんなファイルをHTML形式に変換します)
  • TAG index (各種ウェブ制作支援ツールが豊富にあります)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

google api  住所から経度緯度取得してgoogle map に表示

やりたいこと

レストラン(写真。店名。店の説明。住所)の投稿の際に
住所入力したら、selfで経度緯度所得してgoogle map にピン立てる。

環境

ruby 2.5.1
rails 5.2.3

実装

DBの中身
postsテーブル

カラム名 内容
address string 住所
latitude float 住所緯度
longitude float 住所経度
title text 店の名前
description text 店の説明
image string レストランの写真

google API 所得

参考サイト
(https://nendeb.com/276)

Maps JavaScript API
Geocoding API
この二つの有効化

gem 導入

Gem.file
gem "gmaps4rails"
gem "geocoder"

JS

ターミナル.
rails g gmaps4rails:copy_js
application.html.haml
    %script(src="//maps.google.com/maps/api/js?v=3.23")
    %script(src="//cdn.rawgit.com/mahnunchik/markerclustererplus/master/dist/markerclusterer.min.js")
    %script(src="//cdn.rawgit.com/printercu/google-maps-utility-library-v3-read-only/master/infobox/src/infobox_packed.js" type="text/javascript")
    %script(src="/javascripts/gmaps_google.js")

Model

post.rb
  geocoded_by :address
  after_validation :geocode

  private
  def geocode
    uri = URI.escape("https://maps.googleapis.com/maps/api/geocode/json?address="+self.address.gsub(" ", "")+"&key=#{Rails.application.credentials.google_map_api}")
    res = HTTP.get(uri).to_s
    response = JSON.parse(res)
    self.latitude = response["results"][0]["geometry"]["location"]["lat"]
    self.longitude = response["results"][0]["geometry"]["location"]["lng"]
  end

controller

posts_controller.rb
 def show
    @post = Post.find(params[:id])
  end

  private
  def post_params
    params.require(:post).permit(:image, :description, :text, :address, :latitude, :longitude)
  end

veiw

show.html.haml
    #map
      :javascript
        function initMap() {

          var test = {lat: #{@post.latitude}, lng: #{@post.longitude}};
          var map = new google.maps.Map(document.getElementById('map'), {
            zoom: 15,
            center: test
          });
          var transitLayer = new google.maps.TransitLayer();
          transitLayer.setMap(map);

          var contentString = '住所:#{@post.address}';
          var infowindow = new google.maps.InfoWindow({
            content: contentString
          });

          var marker = new google.maps.Marker({
            position:test,
            map: map,
            title: contentString
          });

          marker.addListener('click', function() {
            infowindow.open(map, marker);
          });
        }
      %script{:async => "", :defer => "defer", :src => "https://maps.googleapis.com/maps/api/js?v=3.exp&key=#{Rails.application.credentials.google_map_api}&callback=initMap"}
show.scss
#map {
  height: 400px;
  margin-left:auto;
  margin-right:auto;
  text-align:left;
  width: 80%
}

大切なこと

post.rbとshow.html.hamlのところに
下記の記載があると思います。

#{Rails.application.credentials.google_map_api}

この記載を消して自分で所得したAPIを打ち込めば動きますが
gitでプロジェクト管理してる時にgitにあげるとAPI_KEYが悪用される可能性があり、gitパトロールから注意が入ります。
なので環境変数を使います。

環境変数の設定

rails のバージョンによって異なります。
今回はrails 5.2.3を使用しています。

ターミナル.
EDITOR="vi" bin/rails credentials:edit

上記コマンドで環境変数の設定を行います。
aws:~~~~はデフォルトで書いてあると思います。
今回はgoogle_map_apiという変数に所得したAPI_KEYを代入している形です。
変数は自分の好きな名前で結構です。

iで入力モードで編集追加できます。
:wqで保存、上書き保存できます。

# aws:
#   access_key_id: 123
#   secret_access_key: 345


google_map_api: 所得したAPI_KEY

終わりに

私は今回このようなコードで住所から緯度経度所得してmapに表示させました。
gitにもあげているので詳細なコード確認したい方がおられましたら、キータの私のページにgitのリンクありますので見てください。
favorite_food_shareというプロジェクト名です。(キータ記事とコード少し違うところあります。)

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

ツイキャス配信で赤字になった時にデスクトップ通知する

https://qiita.com/JeYA9JoHojZtZYG/items/b404178dde49a1422470 でツイキャスの閲覧者数を通知するためのブックマークレットを書いたが、同じように赤字通知も作った。

javascript:(new MutationObserver((t,e)=>{t.find(t=>t.target.classList.contains('tw-timeup-timer--will-end-soon'))&&new Notification('赤字です。')})).observe(document.querySelector('.tw-status-indicator'),{attributes:!0});

配信中は以下のように左上に配信時間が表示されている。

スクリーンショット 2020-02-05 12.12.31.png

配信は通常30分で切れるが残り時間が少なくなる(多分3分前)と配信時間表示が赤くなる。

スクリーンショット 2020-02-05 12.13.26.png

この要素は tw-status-indicator のclass属性がついているが、赤くなるときは tw-timeup-timer--will-end-soon が追加される。その変更をMutationObserverで拾った。

以下のように通知される。

スクリーンショット 2020-02-05 12.07.33.png

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

create-react-appで生成したReactのimportを絶対パスでやる(VSCode対応)

方法

プロジェクトルートに jsconfig.json を生成し、中身を↓にする。
※プロジェクトルート = package.json があるところ

{
  "compilerOptions": {
    "baseUrl": "src"
  },
  "include": ["src"]
}

src 配下のファイルに対して、↓でimport可能になる。

import Button from 'components/Button';

参考リンク

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

Web系のファイルやツールの概要メモ

はじめに

ざっくり調べた際の個人的なメモです。

■クライアント側

Webページの3つの基礎ファイル

.html/.css/.js
この3つがわかれば、基本的なWebページを作ることができる。

javascript

javascriptはクライアントサイド用ただ一つのプログラム言語(のはず)。
ECMAScript(ES5とかES6)という標準仕様に従って各ブラウザでパーサーが実装されている。
各ブラウザごとに対応状況が異なる。そのための対応策として後述の諸々が発生しているような?

AltJs

Alternative Js。javascriptを拡張して独自構文を持つ「言語」の総称。
最終的には.jsファイルに変換してから使う。
CoffeeScript/TypeScript/Dart/Haxeなどが具体的な言語。

Node.js

サーバサイドjavascriptの環境ソフト。
jsライブラリやツールを導入する際の親玉になる。
npmコマンドを持つ。

jsのフロントエンド用ライブラリ

js記述を補助するライブラリ。
Anglur/jQuery/React/Vue.jsなど。
併用する場合はBowerを使った方がよいか。

React用のフレームワーク

Redux/Flux
併用はしない。

Bower

フロントエンド用ライブラリの管理ソフト。
フロントエンド用ライブラリを併用する場合は使った方がいいか。
npmで入れる。

Gulp

ファイル変換系の「タスク」を管理してくれるツール。
npmで入れる。

Babel

ECMAScripの標準仕様で書いたjavascriptをどのブラウザでも使えるようにしてくれるツール。
npmで入れる。

Webpack

js,css,jpgなどのWeb用ファイルを一つのjsファイルにまとめてくれるツール。
npmで入れる。

■サーバ側

サーバ側ミドルウェア

Apache/Nginx/IIS。
Post/Getを伴うような動的Webページを作るなら、これらのミドルウェアの助けが必要。
読まれるだけの静的Webページなら、NASで公開すれば可能。

サーバ側プログラム言語

PHPやRubyが有名。
ただし、POST/GETを受け渡しできる言語なら、すべてサーバサイドにはなりえる(はず)。

PHP用のフレームワーク

Laravel/CakePHP

Rubyのフレームワーク

Ruby on Rails

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

GoogleTagManagerを使ってHTML要素の追加や削除を行う

まえがき

webアプリケーションで特定条件時のみ画面のカスタマイズを行いたい場合に、あんまりソースコードにif文いっぱい書きたくないな……と思ったのでGoogleTagManager(以下GTM)を使用することにしました。
Analyticsと組み合わせて使われることが多い(と思われる)GTMですが、Javascriptを使用してソースコード本体に手を加えずにHTML要素のカスタマイズを行うこともできます。

タグ作成

GTMから「新しいタグを作成」を押下し、「タグの設定」を選択します。
スクリーンショット 2020-02-05 10.50.19.png

タグタイプで「カスタムHTML」を選択します。
スクリーンショット 2020-02-05 11.16.25.png

タグの設定を行う
スクリーンショット 2020-02-05 11.17.20.png
「HTML」の箇所にJavascriptを記載します。
今回はシンプルにdiv要素の追加を行うことにします。要素の削除や、CSSスタイルの変更も可能です。
Javascriptは「ライブラリを使わない素のJavaScriptでDOM操作」を参考にさせていただきました。

<script>
  var container = document.querySelector('.test-tag');
  container.innerHTML = '<div>グーグルタグマネージャテスト挿入</div>';
</script>

詳細設定から、タグ配信の優先度を100に設定しておきました。
タグを複数設定した場合、詳細設定でそれぞれのタグの優先順位や順序付けが可能なので色々調整が効きます。

トリガー作成

タグの作成が完了したら「トリガーの選択」右上にある「+」マークからトリガーを新規追加します。

今回トリガーは「ページビュー > ページビュー」にしました。
スクリーンショット 2020-02-05 11.32.55.png

サンプルでは特定IDのページだけカスタム画面を見せたいという設定なので、「一部のページビュー」を選択して条件を記載します。
スクリーンショット 2020-02-05 11.33.20.png

サイトに差し込み用の要素を仕込む

<div class="test-tag"></div>

メッセージを差し込みたい位置に、GTMで指定したクラスを入れておきます。
これだけ。

プレビューで確認し、問題なければ公開します。
GTMのプレビューと公開を正しく行おう

メリット

  • if文でソースコードを汚さずに済む。
  • デプロイ不要。画像やリンクの追加も楽。
  • かなり柔軟に設定できるので、慣れるとソースがりがり書くよりスムーズ。
  • GTMのバージョン管理も便利。
  • 運用者と開発者が異なっており、細かい変更が定期的に入る箇所の修正を運用者が行える(ニュースとか?)。

デメリット

  • うっかり不具合があった場合に、原因がGTMに記述したscriptなのかソースコード本体側にあるのかの検証がややめんどうくさい。
  • 使用しているクラスやIDの管理をきちんと行わないと、後日混乱しやすい。(「あれ、このクラス使ってない?いらないなら消していい?」「あーそれGTMで使ってます」)
  • Git上でソースコードと合わせて一括でバージョン管理できない。

まとめ

トリガーにCookieを使用することで、LPページ踏んだお客さんにはフォームにキャンペーンコード入力欄を表示するけど、LPページ見てないお客さんにはキャンペーンコード入力欄は非表示にするよ!
Javascriptにエラーがあったときにブラウザでアラート出すよ!
QAページのスクロール距離が長い場合に、ヘルプチャットへの誘導を行うよ!
などなど、いろんなことがGTMでできました。
便利ですが、チーム開発では管理がやや煩雑になるなあという印象はあるため、うまく使っていきたいと思います。

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

Imageの表示タイミングを揃える

方針

  1. 画像の取得タイミングを知る
  2. 画像の表示を切り替える

実装

JQuery

<div class="content" style="display: none;">
  <img id="sample1"></img>
  <img id="sample2"></img>
</div>
var allImages = $("#sample1,#sample2");
var allImagesCount = allImages.length;
var completeImageCount = 0;
for(var i = 0; i < allImagesCount; i++){
  $(allImages[i]).bind("load", function(){
    completeImageCount++;
    if (allImagesCount == completeImageCount) {
      $(".content").css('display', 'block');
    }
  });
}
  • display vs visibility

visibilityは、要素自体は消えない方が好ましいので、visibilityを使用する。

Vue

img(:src="images" @load="loaded")

Vueも同じ要領で

再現

Network conditionsの設定で表示の遅延を再現することができます。

Network conditionsの設定

1.「More tools」 => 「Network conditions」 で、「Network conditions」のタブを開きます。

2.「Network conditions」の「Network throttling」=>「Custom」 => 「Add」へ進みます。

3.「Add」で作成した「My LTE」を設定します。

再度、ロードすると画像の読み込みが遅くなり、遅延が発生する箇所が分かります。

Reference

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

手に馴染む道具(プログラミング言語)

はじめに

職人は、自分の道具を使えば使うほど手に馴染んで使いやすくなっていく。プログラミング環境でもエディタなんかはそう。そしてプログラミング言語もそう。

プログラミング言語の老舗といえばCであり、多くの職業プログラマはCを通ってきているはず。

そうでもないか。

いや、ある程度のスキルを持っている人はCに精通しているし、アセンブラも分かるだろう。

その前提で、やはり言語仕様(文法)はCライクなものが手に馴染みやすいと思うんだ。そういった点で、JavaScriptは手に馴染みやすい。JavaScript系は割りと好きな部類だ。

本題

しかし、世の人気を二分するメジャー・スクリプト言語RubyとPython、どちらもCライクではない。なぜだ。慣れてしまえばそれまでなのだが。

動的型付け言語は散々な言われようだが、スキルさえ問題なければアドホックでスピーディーな開発では超絶な生産性を達成できると思うんだ。

JavaScriptでnode.jsでも良いのだか、あれはもう全くLightweightではないよね。挙動も独特だし。

なぜ動的型付け言語でCライクな汎用かつlightweightなスクリプト言語がない、もしくは人気が無いのか。

という気持ちだけ表明してみた。

本日は以上。

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

[Vue.js]配列の変更が検知できない理由の「JavaScriptの制限」って何よ!

タイトルのJavaScriptの制限はこちらに記載されているものです。
リストレンダリング(注意事項) — Vue.js

要約すると、
配列に対して直接インデックスの設定、またはlengthプロパティを変更する場合、viewは更新されません。

具体的な操作はこちら。

インデックスでアイテムを直接設定するとき。例: vm.items[indexOfItem] = newValue
配列の長さを変更するとき。例: vm.items.length = newLength

その回避策が Vue.setArray.prototype.splice を使用するというものです。
簡単にできる回避策なので、ふーんって感じで書き換えて終了なのですが、
JavaScriptの制限が気になったので調べてみました。

ざっくり リアクティブシステム

Vue.jsの値の変更の検知の仕組みは、リアクティブシステム1で実現されています。
ざっくり言うと、Vueインスタンス化の際にプロパティに対してリアクティブになるよう、よしなにやってくれるシステムです。

data.png

こちらの図でいうと、プロパティの変更をgetter/setterをトリガーにしてWatcherで再描画してくれているということです。

んで、JavaScriptの制限って何さ!

ヒントはここに書いてありました。
リストレンダリング(配列の置き換え) — Vue.js

例えば、filter()、concat()、そしてslice() のような、元の配列を変更しませんが、常に新しい配列を返します。

リアクティブシステムは新しい配列を返してくれることを望んでいるんです!

つまり

JavaScriptの制限というのは、新しい配列を返さない操作のことを言っているのです。

vm.items[indexOfItem] = newValuevm.items.length = newLengthも配列を返しません。

なんだそれだけなのか

それだけなんです。
リストレンダリング(変更メソッド) — Vue.js
ここに書かれているメソッドは新しい配列を返すから検知できるんです。

まとめ

私の日本語を読む能力が低かったせいで、
JavaScriptの制限が配列の置き換えの章に掛かっていることに気づくのに時間がかかってしまいました。(汗)

お間違い等ございましたら、ご指摘いただけますと幸いです。:bow:

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

[Vue.js]配列の更新が検知できない理由の「JavaScriptの制限」って何よ!

タイトルのJavaScriptの制限はこちらに記載されているものです。
リストレンダリング(注意事項) — Vue.js

要約すると、
配列に対して直接インデックスの設定、またはlengthプロパティを変更する場合、viewは更新されません。

具体的な操作はこちら。

インデックスでアイテムを直接設定するとき。例: vm.items[indexOfItem] = newValue
配列の長さを変更するとき。例: vm.items.length = newLength

その回避策が Vue.setArray.prototype.splice を使用するというものです。
簡単にできる回避策なので、ふーんって感じで書き換えて終了なのですが、
JavaScriptの制限が気になったので調べてみました。

ざっくり リアクティブシステム

Vue.jsの値の変更の検知の仕組みは、リアクティブシステム1で実現されています。
ざっくり言うと、Vueインスタンス化の際にプロパティに対してリアクティブになるよう、よしなにやってくれるシステムです。

data.png

こちらの図でいうと、プロパティの変更をgetter/setterをトリガーにしてWatcherで再描画してくれているということです。

んで、JavaScriptの制限って何さ!

ヒントはここに書いてありました。
リストレンダリング(配列の置き換え) — Vue.js

例えば、filter()、concat()、そしてslice() のような、元の配列を変更しませんが、常に新しい配列を返します。

リアクティブシステムは新しい配列を返してくれることを望んでいるんです!

つまり

JavaScriptの制限というのは、新しい配列を返さない操作のことを言っているのです。

vm.items[indexOfItem] = newValuevm.items.length = newLengthも配列は返しません。

なんだそれだけなのか

それだけなんです。
リストレンダリング(変更メソッド) — Vue.js
ここに書かれているメソッドは新しい配列を返すから検知できるんです。

まとめ

私の日本語を読む能力が低かったせいで、
JavaScriptの制限が配列の置き換えの章に掛かっていることに気づくのに時間がかかってしまいました。(汗)

お間違い等ございましたら、ご指摘いただけますと幸いです。:bow:

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

【202002追試】Object.prototype.hasOwnProperty()は変数に入れた方が速い?

はじめに

Object.prototype.hasOwnProperty()は変数に入れた方が速い? - JavaScript
というブログ記事が私のMastodonのTLに出ていました。

趣旨としてはhasOwnPropertyをどう呼び出すかで4通りの検証が以下のコードでなされていたようです。

(function() {
    var myObj = {
        prop: 111
    };

    // テスト1: ユーザー定義の対象オブジェクトから呼び出す。
    function test1() {
        var i, startTime, stopTime;

        startTime = new Date();

        for (i = 0; i < 10000000; i++) {
            myObj.hasOwnProperty('prop');
        }

        stopTime = new Date();
        console.log(stopTime - startTime + 'ms');
    }

    // テスト2: ネイティブオブジェクトメソッドからcallを使って呼び出す(Object.prototype.hasOwnProperty.call())。
    function test2() {
        var i, startTime, stopTime;

        startTime = new Date();

        for (i = 0; i < 10000000; i++) {
            Object.prototype.hasOwnProperty.call(myObj, 'prop');
        }

        stopTime = new Date();
        console.log(stopTime - startTime + 'ms');
    }

    // テスト3: objProtoHasOwnというローカル変数に代入してキャッシュ化。
    function test3() {
        var i, startTime, stopTime, objProtoHasOwn;

        startTime = new Date();
        objProtoHasOwn = Object.prototype.hasOwnProperty;

        for (i = 0; i < 10000000; i++) {
            objProtoHasOwn.call(myObj, 'prop');
        }

        stopTime = new Date();
        console.log(stopTime - startTime + 'ms');
    }

    // テスト4: ローカル変数でキャッシュ化した上で、hasOwnという関数にして使用。
    function test4() {
        var i, startTime, stopTime, objProtoHasOwn, hasOwn;

        startTime = new Date();
        objProtoHasOwn = Object.prototype.hasOwnProperty;

        hasOwn = function(obj, prop) {
            return objProtoHasOwn.call(obj, prop);
        };

        for (i = 0; i < 10000000; i++) {
            hasOwn(myObj, 'prop');
        }

        stopTime = new Date();
        console.log(stopTime - startTime + 'ms');
    }

    test1();
    test2();
    test3();
    test4();
})();

元の記事の検証結果

テスト環境:
Chrome 60.0.3112.113(64bit)
Firefox 55.0.3(64bit)
IE 11.413.15063.0(32bit)
JScript Panel 1.2.3.2

PCは2600K, Memory8G, Windows10(64bit)

テスト種類 Chorome Firefox IE Microsoft Edge JScript Panel
テスト1 (myObj.hasOwnProperty) 105 26 2986 3306 781
テスト2 (Object.prototype.hasOwnProperty.call) 100 1298 6534 7758 907
テスト3 (Object.prototype.hasOwnPropertyを変数に) 122 21 4437 5097 893
テスト4 (Object.prototype.hasOwnPropertyを変数→関数) 125 47 4907 6332 923

今回の再検証結果

Chrome 79.0.3945.130(64bit)
Firefox 72.0.2(64bit)
IE 11.592.18362.0(32bit)
Microsoft Edge 44.18362.449.0(64bit)

PCはIntel Core i7-7500U, Memory16G, Windows10(64bit)

テスト種類 Chrome Firefox Internet Explorer Edge
テスト1 (myObj.hasOwnProperty) 99(87.4~110.6) 58.25(55.4~61.1) 3083.65(3025.5~3141.8) 3172.95(3125.4~3220.5)
テスト2 (Object.prototype.hasOwnProperty.call) 85(80.6~89.4) 55.9(51.7~60.1) 4567.1(4512.7~4621.5) 5660.65(5610.7~5710.6)
テスト3 (Object.prototype.hasOwnPropertyを変数に) 103.2(98.0~108.4) 49.45(45.2~53.7) 3612.75(3555.3~3670.2) 4316.85(4255.6~4378.1)
テスト4 (Object.prototype.hasOwnPropertyを変数→関数) 117.9(110.5~125.3) 46.25(45.5~47.0) 3752.75(3688.9~3816.6) 4836.55(4751.9~4921.2)

元記事とは違って20回計測で、95%信頼区間を括弧中に表示しています。TBのことを考え、測定の間には30秒~1分程度の間隔があります。

考察

今回と元記事の差

  • 元記事のようにFirefoxのテスト2だけ猛烈に遅いということは確認されなかった。
  • 全般にPCスペックなどの要因から速度が速いが、なぜかFirefoxのテスト2以外だけ遅い

いずれの検証にも言えること

  • 全般にFirefoxとChromeが速いのはなんか最適化がかかって処理が飛んでいるのではないか?(検証方法がわからず断念
  • IRよりEdgeのほうが遅い。いっちょんわからん

結論

まあIEとかもう誰も使ってないし(ぇ)、Edgeは中身Chromeになるので、ぶっちゃけどれ使ってもいいんじゃないですかね。

データ

Chrome

測定回数 1 2 3 4
1 202 106 122 118
2 98 95 102 178
3 85 96 101 120
4 91 89 102 118
5 137 98 131 118
6 101 77 96 106
7 94 79 89 110
8 90 96 96 120
9 100 80 103 119
10 95 85 129 114
11 80 70 96 113
12 102 79 90 102
13 94 77 89 102
14 82 87 98 96
15 76 77 101 118
16 88 99 111 112
17 87 82 108 111
18 91 71 99 144
19 95 81 91 114
20 92 76 110 125
平均 99 85 103.2 117.9
標準偏差 26.50283004 9.959919678 11.96076921 16.85200285
標準誤差 5.926212956 2.227105745 2.674509301 3.768222393
95%信頼区間 11.61516405 4.365047085 5.241941947 7.385580233
95%信頼区間最大 110.6151641 89.36504708 108.4419419 125.2855802
95%信頼区間最小 87.38483595 80.63495292 97.95805805 110.5144198

Firefox

測定回数 1 2 3 4
1 44 41 41 44
2 59 42 44 47
3 61 40 42 49
4 56 55 48 48
5 65 58 54 46
6 65 58 54 46
7 55 56 63 47
8 60 60 43 46
9 59 67 71 44
10 66 57 45 46
11 58 57 44 46
12 51 64 44 47
13 53 49 42 45
14 65 55 44 46
15 46 49 62 51
16 68 45 45 45
17 65 72 43 47
18 60 55 42 44
19 59 60 45 45
20 50 78 73 46
平均 58.25 55.9 49.45 46.25
標準偏差 6.594505288 9.658674857 9.754358 1.669580786
標準誤差 1.47457621 2.159745355 2.181140757 0.373329613
95%信頼区間 2.890116287 4.233023146 4.274957362 0.731712602
95%信頼区間最大 61.14011629 60.13302315 53.72495736 46.9817126
95%信頼区間最小 55.35988371 51.66697685 45.17504264 45.5182874

IE

測定回数 1 2 3 4
1 3464 5035 4125 4225
2 2928 4499 3556 3667
3 3059 4371 3567 4052
4 3091 4469 3680 3804
5 3107 4548 3616 3756
6 3019 4510 3565 3692
7 3266 4627 3652 3704
8 2804 4511 3518 3574
9 3079 4544 3562 3652
10 2942 4646 3458 3625
11 3031 4522 3532 3679
12 3019 4610 3540 3728
13 3108 4497 3576 3677
14 3122 4585 3603 3789
15 3192 4576 3551 3709
16 3217 4632 3691 3852
17 3041 4539 3596 3670
18 3091 4540 3681 3785
19 3088 4577 3635 3707
20 3005 4504 3551 3708
平均 3083.65 4567.1 3612.75 3752.75
標準偏差 132.7472316 124.0503527 131.150629 145.5853272
標準誤差 29.68318337 27.73850212 29.32617218 32.55386882
95%信頼区間 58.17797081 54.36646557 57.47824174 63.80441094
95%信頼区間最大 3141.827971 4621.466466 3670.228242 3816.554411
95%信頼区間最小 3025.472029 4512.733534 3555.271758 3688.945589

Edge

測定回数 1 2 3 4
1 3116 5658 4239 4682
2 3116 5658 4239 4682
3 3118 5641 4390 4910
4 3071 5583 4205 4662
5 3170 5816 4466 5031
6 3352 5714 4523 4899
7 3352 5714 4523 4899
8 3053 5521 4175 4569
9 3059 5462 4043 4514
10 3093 5606 4225 4965
11 3323 5674 4490 4967
12 2978 5443 4190 4623
13 3167 5532 4264 4876
14 3191 5684 4379 4746
15 3174 5779 4306 4799
16 3300 5762 4238 5262
17 3244 5754 4439 4876
18 3058 5569 4113 4618
19 3205 5776 4420 5021
20 3319 5867 4470 5130
平均 3172.95 5660.65 4316.85 4836.55
標準偏差 108.4976843 114.0461639 139.7155235 193.0822299
標準誤差 24.26081975 25.50149751 31.2413408 43.17449913
95%信頼区間 47.55033332 49.98201706 61.23190328 84.62046401
95%信頼区間最大 3220.500333 5710.632017 4378.081903 4921.170464
95%信頼区間最小 3125.399667 5610.667983 4255.618097 4751.929536
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Andorid のブラウザで加速度の値を取得してグラフ表示したりローカルなファイルにセーブしたりする

やること

タイトルそのままです。Chrome でしか動作は確認していません。多分 iOS + Safari でも動きます。下記サイトで動作を見れます。

image

正常に動くと上のような感じになります。加速度センサがついてない PC などでは、加速度の値やグラフは表示されません。

ソースコード

全体のコードは下記に置いてます。

要点

加速度データの取得

下記のコードで、加速度を取得するたびに arr に加速度データを記録していきます、
- https://kkblab.com/make/javascript/acc.html
上記サイトのコードほぼそのままです。

acc.js
     var aX = 0, aY = 0, aZ = 0; // 加速度データの記録用
     var startTime = Date.now(); // 開始時間の記録
     var arr = []; // セーブする用

     // 加速度データを取得する
     window.addEventListener("devicemotion", (dat) => {
         aX = dat.accelerationIncludingGravity.x;
         aY = dat.accelerationIncludingGravity.y;
         aZ = dat.accelerationIncludingGravity.z;

          arr.push( {"t": Date.now() - startTime, "x": aX, "y": aY, "z": aZ }); 
     }); 

データをローカルなファイルにセーブする

arr を json に変換して、Blob を使ってダウンロードできるようにします。
- https://qiita.com/wadahiro/items/eb50ac6bbe2e18cf8813
上記ページのコードを json でダウンロードできるように変更したものです。

アンカーではなく、ボタンを押したらセーブするようにするには、少し複雑なコードを書く必要あり。

save.js
    function handleDownload() {

        var blob = new Blob([ JSON.stringify(arr) ], { "type" : "application/json" });

        if (window.navigator.msSaveBlob) {
            window.navigator.msSaveBlob(blob, "acc.json");
            window.navigator.msSaveOrOpenBlob(blob, "acc.json");
        } else {
            document.getElementById("download").href = window.URL.createObjectURL(blob);
        }

        // セーブしたらデータをリセットする。リセットしたくなければ下記2行をコメントアウトする。
        arr = [];
        startTime = Date.now();
    }

グラフを表示する

arr の末尾から canvas の幅と同じ pixel 分だけデータを取りだして、頭から順に線をつないで引いていっているだけです。縦方向の座標値は適当なので、端末によっては縦幅を越えて描画されるかもしれません。

graph.js
    var canvas;
    var ctx;
    var preX = 0, preY = 0, preZ = 0; // 描画用
    // 加速度データをグラフ表示する displayData
    function displayData() {

        // 加速度の値を文字で表示する
        var txt = document.getElementById("txt");
        txt.innerHTML = "x: " + aX + "<br/>" + "y: " + aY + "<br/>" + "z: " + aZ;

        // canvas の背景をグレーで塗る
        if ( ! canvas || ! canvas.getContext ) { return false; }
        ctx.fillStyle = "gray";
        ctx.fillRect( 0, 0, canvas.clientWidth, canvas.clientHeight );
        ctx.lineWidth = 1 ;

        // 最新から 300px 分のデータを取りだす。
        var varr = [];
        if ( arr.length > canvas.clientWidth){
            varr = arr.slice(-canvas.clientWidth,-1);
        }else{
            varr = arr;
        }

        for ( idx in varr ){
            if ( idx > canvas.clientWidth ) break;
            var dat = varr[idx];

            // X 軸加速度を赤の線で表示する
            var dy1 = canvas.clientHeight/2 + dat.x * 10.0;
            var dx = idx;
            ctx.strokeStyle = "red";
            ctx.beginPath();
            ctx.moveTo(dx,dy1);
            ctx.lineTo(dx-1,preX);
            ctx.stroke();

            // Y 軸加速度を青の線で表示する
            var dy2 = 50 + dat.y * 10.0;
            ctx.strokeStyle = "blue";
            ctx.beginPath();
            ctx.moveTo(dx,dy2);
            ctx.lineTo(dx-1,preY);
            ctx.stroke();

            // Z 軸加速度を緑の線で表示する
            var dy3 = 50 + dat.z * 10.0;
            ctx.strokeStyle = "green";
            ctx.beginPath();
            ctx.moveTo(dx,dy3);
            ctx.lineTo(dx-1,preZ);
            ctx.stroke();

            // 次のデータを表示するときに前のデータから線を描く用
            preX = dy1;
            preY = dy2;
            preZ = dy3;
        }
    }

端末にもよりますが、それなりに高速にデータ取りつつ描画ができるので、最近のスマフォはすごいなーと思ったり。

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