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

【JavaScript】指定した並列数で並列処理を行う関数

記事「【備忘録】Javascriptで非同期処理を効率良くさばく方法」の「同時実行(concurrently)」で述べられていた
並列数を固定しての並列処理を
汎用関数に切り出してみた。

/**
 * 与えられたイテラブルから得られる関数を順に、
 * 指定された数まで並列に実行する。
 * 
 * @param iterable {Iterable<() => Promise<void>}
 *  実行したい関数を要素に持つイテラブル。
 *  各関数は引数を持たず、Promise を返す。
 * @param concurrency {number} この数まで並列に実行する。
 * @return {Promise<void>}
 *  全ての関数を実行し終えると resolve される Promise。
 */
async function runConcurrentlyAsync(iterable, concurrency) {
    const iterator = iterable[Symbol.iterator]();
    let index = 0; // ログ用
    const promises = Array.from({ length: concurrency }, (_, id) => {
        return new Promise(async (resolve) => {
            for (
                let result = iterator.next();
                !result.done;
                result = iterator.next()
            ) {
                const i = index++;
                console.log(`${id}: ${i}...`);

                await result.value();

                console.log(`        ...${id}: ${i}`);
            }

            resolve();
        });
    });
    await Promise.all(promises);
}

引数はイテラブルなので、配列やジェネレーターなどが使える。

ログには次のような情報が出力される。

0: 10...             // 0本目の並列処理が10番目の関数の処理を開始した。
            ...0: 10 // 0本目の並列処理が10番目の関数の処理を終了した。

使用例:

async function verySlowAsync(index) {
    return new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
}

function main() {
    const INIT = 0;
    const MAX = 100;
    const CONCURRENCY = 10; // 同時実行できる数を定義

    const generator = (function* createGenerator() {
        for (let index = INIT; index < MAX; index++) {
            yield async () => await verySlowAsync(index);
        }
    })();

    runConcurrentlyAsync(generator, CONCURRENCY);
}
main();

出力例:
0: 0...
1: 1...
2: 2...
3: 3...
4: 4...
5: 5...
6: 6...
7: 7...
8: 8...
9: 9...
        ...1: 1
1: 10...
        ...0: 0
0: 11...
        ...5: 5
5: 12...
        ...7: 7
7: 13...
        ...3: 3
3: 14...
        ...0: 11
0: 15...
        ...1: 10
1: 16...
        ...4: 4
4: 17...
        ...5: 12
5: 18...
        ...2: 2
2: 19...
        ...8: 8
8: 20...
        ...7: 13
7: 21...
        ...7: 21
7: 22...
        ...9: 9
9: 23...
        ...0: 15
0: 24...
        ...0: 24
0: 25...
        ...6: 6
6: 26...
        ...5: 18
5: 27...
        ...4: 17
4: 28...
        ...9: 23
9: 29...
        ...7: 22
7: 30...
        ...3: 14
3: 31...
        ...5: 27
5: 32...
        ...9: 29
9: 33...
        ...6: 26
6: 34...
        ...3: 31
3: 35...
        ...1: 16
1: 36...
        ...2: 19
2: 37...
        ...1: 36
1: 38...
        ...1: 38
1: 39...
        ...6: 34
6: 40...
        ...8: 20
8: 41...
        ...9: 33
9: 42...
        ...1: 39
1: 43...
        ...0: 25
0: 44...
        ...4: 28
4: 45...
        ...1: 43
1: 46...
        ...6: 40
6: 47...
        ...6: 47
6: 48...
        ...2: 37
2: 49...
        ...7: 30
7: 50...
        ...6: 48
6: 51...
        ...3: 35
3: 52...
        ...5: 32
5: 53...
        ...9: 42
9: 54...
        ...8: 41
8: 55...
        ...6: 51
6: 56...
        ...6: 56
6: 57...
        ...3: 52
3: 58...
        ...2: 49
2: 59...
        ...6: 57
6: 60...
        ...4: 45
4: 61...
        ...9: 54
9: 62...
        ...0: 44
0: 63...
        ...6: 60
6: 64...
        ...5: 53
5: 65...
        ...1: 46
1: 66...
        ...4: 61
4: 67...
        ...5: 65
5: 68...
        ...7: 50
7: 69...
        ...1: 66
1: 70...
        ...3: 58
3: 71...
        ...8: 55
8: 72...
        ...2: 59
2: 73...
        ...4: 67
4: 74...
        ...0: 63
0: 75...
        ...6: 64
6: 76...
        ...3: 71
3: 77...
        ...9: 62
9: 78...
        ...6: 76
6: 79...
        ...6: 79
6: 80...
        ...5: 68
5: 81...
        ...3: 77
3: 82...
        ...1: 70
1: 83...
        ...7: 69
7: 84...
        ...2: 73
2: 85...
        ...3: 82
3: 86...
        ...5: 81
5: 87...
        ...8: 72
8: 88...
        ...9: 78
9: 89...
        ...8: 88
8: 90...
        ...3: 86
3: 91...
        ...4: 74
4: 92...
        ...5: 87
5: 93...
        ...0: 75
0: 94...
        ...2: 85
2: 95...
        ...5: 93
5: 96...
        ...0: 94
0: 97...
        ...4: 92
4: 98...
        ...0: 97
0: 99...
        ...1: 83
        ...3: 91
        ...7: 84
        ...5: 96
        ...9: 89
        ...6: 80
        ...0: 99
        ...2: 95
        ...8: 90
        ...4: 98

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

Magic Leap MagicScript Landscape Application. UiLoadingSpinner

Prepare
Magic Leap One
https://www.magicleap.com/magic-leap-one

mlsdk v.0.20.0
https://creator.magicleap.com/downloads/lumin-sdk/overview

magic-script-cli v2.0.1
https://www.npmjs.com/package/magic-script-cli

magic-script-polyfills v2.2.0
https://www.npmjs.com/package/magic-script-polyfills

Create Project

magic-script init my-sp org.magicscript.sp "Spinner"
cd 

Code

Change app.js

import { LandscapeApp, ui, resources } from 'lumin';
const { 
    UiLinearLayout
  , UiCircleConfirmation
  , UiText
  , EclipseLabelType
  , Alignment
  , UiLoadingSpinner
  , LoadingSpinnerType} = ui;

const {
     FontStyle
   , FontWeight
} = resources;

export class App extends LandscapeApp {
  onAppStart () {
    const prism = this.requestNewPrism([0.5, 0.5, 0.5]);
    const layout = UiLinearLayout.Create(prism);
    layout.setAlignment(Alignment.CENTER_CENTER);

    const label3d = UiText.Create(
        prism, "Particle FX Style:", FontStyle.kItalic, FontWeight.kBold);
    label3d.setTextSize(0.0393);
    layout.addItem(label3d
      , [0, 0, 0.08, 0]
      , Alignment.CENTER_LEFT);

    const loadingSpinner3d = UiLoadingSpinner.Create(
        prism, LoadingSpinnerType.kParticlePackage
    );
    layout.addItem(loadingSpinner3d
      , [0, 0, 0.02, 0]
      , Alignment.CENTER_CENTER);

    const label2d = UiText.Create(
        prism, "2D Particle FX Style:", FontStyle.kItalic, FontWeight.kBold);
    label2d.setTextSize(0.0393);
    layout.addItem(label2d
      , [0.08, 0, 0.02, 0]
      , Alignment.BOTTOM_LEFT);

    const loadingSpinner2d = UiLoadingSpinner.Create(
        prism, LoadingSpinnerType.k2dSpriteAnimation
    );
    loadingSpinner2d.setSize([0.0314, 0.0314]);
    layout.addItem(loadingSpinner2d
      , [0, 0, 0.02, 0]
      , Alignment.BOTTOM_CENTER);
    prism.getRootNode().addChild(layout);
  }
}

Build

magic-script build -i

Run

magic-script run --port=10000

Reference
UiLoadingSpinner(Magic Script API Doc)
https://docs.magicscript.org/lumin.ui.UiLoadingSpinner.html

Loading Spinners (UiLoadingSpinner) (Guide C++)
https://creator.magicleap.com/learn/guides/luminrt-uiloadingspinner

magicscript
https://www.magicscript.org/

Thanks!

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

RedGL Release Master_V5.0

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

RedGL Release Master_V5.0 (WebGL Library)

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

Reactの最速チュートリアル

前文

まともなReactのセットアップがなかったので書きます。

Macの場合だけ Windowsもnodeのインストール以外はだいたい同じなはず

環境構築

nodeの環境構築

$ brew install node

$ npm install -g n

$ sudo n stable

Reactの雛形作成

$ npm install -g create-react-app

$ npx create-react-app "プロジェクト名"

これでReactの雛形のプロジェクトのフォルダが生成されます。
プロジェクト名は小文字英数字が望ましい というか大文字ではプロジェクト作れない

とりまブラウザで確認

$ cd "プロジェクト名"

$ npm start

後は勝手にビルドされてブラウザが開くはず

ディレクトリ構造の解説

これを解説しないとこの辺のファイルを妙に弄ってプロジェクトを破損させたりする輩がいるので解説

  • node_modules/
    • npm install ... とかで入るライブラリ等の実体がある所
    • いじるな
  • public/
    • index.htmlとかfaviconとかを入れておく場所
  • src/
    • Reactのソースコードを書く場所
  • build/
    • index.htmlとかfaviconとかが入る場所
  • package.json
    • npm等で操作したデータが入っている ここに依存パッケージとかビルドするスクリプトとかが入っている
    • 消すな
  • yarn.lock
    • パッケージ管理にyarnを使っている場合関連パッケージが記載されている 
    • 消すな
  • README.md
    • 読め

ReactというかWebpackの仕組み

正確な説明はしないけどイメージ程度に

Reactの独自の言語であるJSXをコンパイルして、普通のブラウザで見られるhtml,css,javascriptの形式にする。
これがnpm run buildのコマンドのやっていること

npm startはnpm run buildをやってくれ、ブラウザに対してリンクを開いてくれる。
この状態でソースコードを編集すると自動的にビルドしなおしてくれる

Webpackはいろいろなライブラリを導入したり、ReactやVueのソースコードをひとまとめにしてくれるやつのこと
基本的にReactはこのWebpackとかを通して、まとめてビルドしない動かない

詳しい人が見たら怒りそうな解説であるが、イメージが大事...

チュートリアル

読め
https://reactjs.org/tutorial/tutorial.html

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

Re:0から始めるReactのチュートリアル

前文

まともなReactのセットアップや説明がなかったので書きます。
周り見てた感じいろんなQiitaの記事を片っ端から試して凄まじい構成になっているものをよく見るので、環境構築から基礎まで書けたらよいと思います。ちょいちょい更新予定

環境構築はMacの場合だけ Windowsもnodeのインストール以外はだいたい同じなはず

環境構築

nodeの環境構築

$ brew install node

$ npm install -g n

$ sudo n stable

Reactの雛形作成

$ npm install -g create-react-app

$ npx create-react-app "プロジェクト名"

これでReactの雛形のプロジェクトのフォルダが生成されます。
プロジェクト名は小文字英数字が望ましい というか大文字ではプロジェクト作れない

とりまブラウザで確認

$ cd "プロジェクト名"

$ npm start

後は勝手にビルドされてブラウザが開くはず

ディレクトリ構造の解説

これを解説しないとこの辺のファイルを妙に弄ってプロジェクトを破損させたりする輩がいるので解説

  • node_modules/
    • npm install ... とかで入るライブラリ等の実体がある所
    • いじるな
  • public/
    • index.htmlとかfaviconとかを入れておく場所
  • src/
    • Reactのソースコードを書く場所
  • build/
    • index.htmlとかfaviconとかが入る場所
  • package.json
    • npm等で操作したデータが入っている ここに依存パッケージとかビルドするスクリプトとかが入っている
    • 消すな
  • yarn.lock
    • パッケージ管理にyarnを使っている場合関連パッケージが記載されている 
    • 消すな
  • README.md
    • 読め

ReactというかWebpackの仕組み

正確な説明はしないけどイメージ程度に

Reactの独自の言語であるJSXをコンパイルして、普通のブラウザで見られるhtml,css,javascriptの形式にする。
これがnpm run buildのコマンドのやっていること

npm startはnpm run buildをやってくれ、ブラウザに対してリンクを開いてくれる。
この状態でソースコードを編集すると自動的にビルドしなおしてくれる

Webpackはいろいろなライブラリを導入したり、ReactやVueのソースコードをひとまとめにしてくれるやつのこと
基本的にReactはこのWebpackとかを通して、まとめてビルドしない動かない

詳しい人が見たら怒りそうな解説であるが、イメージが大事...

src/ にあるコードの解説

ここは脳死でそういうもんなんやと考えてもいい
大事なのはせいぜいAppがpublic/index.htmlのrootにRenderされるという情報
後はのちのち考えればよい

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();

App.js
index.jsのrenderから呼ばれたコンポーネントはこちら
これはFunctional Componentと言ってちょっと新しい書き方なのでややこしい
要はfunction Appがコンポーネントのように振る舞う
returnの中身が描画して欲しいデータということ

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

しかしこれはわかりずらいので従来のコンポーネントに戻したコードがこちら
割とこっちの方が馴染みのあるコードなはず 
ネットに転がってるチュートリアルもこっちのほうがまだ多い気がする

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

class App extends React.Component {
  render() {
    return(
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
           Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    )
  }
}

export default App

チュートリアル

読め
https://reactjs.org/tutorial/tutorial.html

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

Re:ゼロから始めるReactのチュートリアル

前文

まともなReactのセットアップや説明がなかったので書きます。
周り見てた感じいろんなQiitaの記事を片っ端から試して凄まじい構成になっているものをよく見るので、環境構築から基礎まで書けたらよいと思います。ちょいちょい更新予定

環境構築はMacの場合だけ Windowsもnodeのインストール以外はだいたい同じなはず

そもそもReactって

React(リアクト)は、Facebookとコミュニティによって開発されているユーザインタフェースを構築するためのJavaScriptライブラリである[3]。React.jsまたはReactJSの名称でも知られている。wikipediaより引用

要はフロントを作るためのライブラリです。htmlとjsとcssの凝ったバージョンだと思えばよし
npmとかを使うから、Node.jsと混同したりサーバーサイドも関係あるんちゃう?って考える人も多いですが、関係ないです。ただのフロントエンドを構築するためのライブラリです。

環境構築

nodeの環境構築

$ brew install node

$ npm install -g n

$ sudo n stable

Reactの雛形作成

$ npm install -g create-react-app

$ npx create-react-app "プロジェクト名"

これでReactの雛形のプロジェクトのフォルダが生成されます。
プロジェクト名は小文字英数字が望ましい というか大文字ではプロジェクト作れない

とりまブラウザで確認

$ cd "プロジェクト名"

$ npm start

後は勝手にビルドされてブラウザが開くはず

ディレクトリ構造の解説

これを解説しないとこの辺のファイルを妙に弄ってプロジェクトを破損させたりする輩がいるので解説

  • node_modules/
    • npm install ... とかで入るライブラリ等の実体がある所
    • いじるな
  • public/
    • index.htmlとかfaviconとかを入れておく場所
  • src/
    • Reactのソースコードを書く場所
  • build/
    • index.htmlとかfaviconとかが入る場所
  • package.json
    • npm等で操作したデータが入っている ここに依存パッケージとかビルドするスクリプトとかが入っている
    • 消すな
  • yarn.lock
    • パッケージ管理にyarnを使っている場合関連パッケージが記載されている 
    • 消すな
  • README.md
    • 読め

ReactというかWebpackの仕組み

正確な説明はしないけどイメージ程度に

Reactの独自の言語であるJSXをコンパイルして、普通のブラウザで見られるhtml,css,javascriptの形式にする。
これがnpm run buildのコマンドのやっていること

npm startはnpm run buildをやってくれ、ブラウザに対してリンクを開いてくれる。
この状態でソースコードを編集すると自動的にビルドしなおしてくれる

Webpackはいろいろなライブラリを導入したり、ReactやVueのソースコードをひとまとめにしてくれるやつのこと
基本的にReactはこのWebpackとかを通して、まとめてビルドしない動かない

詳しい人が見たら怒りそうな解説であるが、イメージが大事...

src/ にあるコードの解説

ここは脳死でそういうもんなんやと考えてもいい
大事なのはせいぜいAppがpublic/index.htmlのrootにRenderされるという情報
後はのちのち考えればよい

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();

index.jsのrenderから呼ばれたコンポーネントはこちら
これはFunctional Componentと言ってちょっと新しい書き方なのでややこしい
要はfunction Appがコンポーネントのように振る舞う
returnの中身が描画して欲しいデータということ

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

しかしこれはわかりずらいので従来のコンポーネントに戻したコードが以下のコード
割とこっちの方が馴染みのあるコードなはず 
ネットに転がってるチュートリアルもこっちのほうがまだ多い気がする
もしこっちのほうがいいという人は、コピペすればよい

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

class App extends React.Component {
  render() {
    return(
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
           Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    )
  }
}

export default App

チュートリアル

読め
https://reactjs.org/tutorial/tutorial.html

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

完全に心が折れた人向けのReactチュートリアル

前文

まともなReactのセットアップや説明がなかったので書きます。
周り見てた感じいろんなQiitaの記事を片っ端から試して凄まじい構成になっているものをよく見るので、環境構築から基礎まで書けたらよいと思います。ちょいちょい更新予定

環境構築はMacの場合だけ Windowsもnodeのインストール以外はだいたい同じなはず

そもそもReactって

React(リアクト)は、Facebookとコミュニティによって開発されているユーザインタフェースを構築するためのJavaScriptライブラリである[3]。React.jsまたはReactJSの名称でも知られている。wikipediaより引用

要はフロントを作るためのライブラリです。htmlとjsとcssの凝ったバージョンだと思えばよし
npmとかを使うから、Node.jsと混同したりサーバーサイドも関係あるんちゃう?って考える人も多いですが、関係ないです。ただのフロントエンドを構築するためのライブラリです。

環境構築

nodeの環境構築

$ brew install node

$ npm install -g n

$ sudo n stable

Reactの雛形作成

$ npm install -g create-react-app

$ npx create-react-app "プロジェクト名"

これでReactの雛形のプロジェクトのフォルダが生成されます。
プロジェクト名は小文字英数字が望ましい というか大文字ではプロジェクト作れない

とりまブラウザで確認

$ cd "プロジェクト名"

$ npm start

後は勝手にビルドされてブラウザが開くはず

ディレクトリ構造の解説

これを解説しないとこの辺のファイルを妙に弄ってプロジェクトを破損させたりする輩がいるので解説

  • node_modules/
    • npm install ... とかで入るライブラリ等の実体がある所
    • いじるな
  • public/
    • index.htmlとかfaviconとかを入れておく場所
  • src/
    • Reactのソースコードを書く場所
  • build/
    • index.htmlとかfaviconとかが入る場所
  • package.json
    • npm等で操作したデータが入っている ここに依存パッケージとかビルドするスクリプトとかが入っている
    • 消すな
  • yarn.lock
    • パッケージ管理にyarnを使っている場合関連パッケージが記載されている 
    • 消すな
  • README.md
    • 読め

ReactというかWebpackの仕組み

正確な説明はしないけどイメージ程度に

Reactの独自の言語であるJSXをコンパイルして、普通のブラウザで見られるhtml,css,javascriptの形式にする。
これがnpm run buildのコマンドのやっていること

npm startはnpm run buildをやってくれ、ブラウザに対してリンクを開いてくれる。
この状態でソースコードを編集すると自動的にビルドしなおしてくれる

Webpackはいろいろなライブラリを導入したり、ReactやVueのソースコードをひとまとめにしてくれるやつのこと
基本的にReactはこのWebpackとかを通して、まとめてビルドしない動かない

詳しい人が見たら怒りそうな解説であるが、イメージが大事...

src/ にあるコードの解説

ここは脳死でそういうもんなんやと考えてもいい
大事なのはせいぜいAppがpublic/index.htmlのrootにRenderされるという情報
後はのちのち考えればよい

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();

index.jsのrenderから呼ばれたコンポーネントはこちら
これはFunctional Componentと言ってちょっと新しい書き方なのでややこしい
要はfunction Appがコンポーネントのように振る舞う
returnの中身が描画して欲しいデータということ

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

しかしこれはわかりずらいので従来のコンポーネントに戻したコードが以下のコード
割とこっちの方が馴染みのあるコードなはず 
ネットに転がってるチュートリアルもこっちのほうがまだ多い気がする
もしこっちのほうがいいという人は、コピペすればよい

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

class App extends React.Component {
  render() {
    return(
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
           Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    )
  }
}

export default App

チュートリアル

読め
https://reactjs.org/tutorial/tutorial.html

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

[JavaScript] 擬配列オブジェクトから配列を作る最適解の考察(スプレッド演算子 vs Array.from())

概要

  • 「配列型」や「反復型」の擬配列オブジェクトから配列のインスタンスを作る機会が多いので、最適解が何かを調べてみました。

方法

  • 構文が分かりやすい以下のコードのどちらが速いかを比較してみました。 回転数は 100万回です。
[...xxx]
const max = 1000000
console.time()
for (let i = 0; i < max; ++i) {
  const a = [...document.body.childNodes]
}
console.timeEnd()
Array.from(xxx)
const max = 1000000
console.time()
for (let i = 0; i < max; ++i) {
  const a = Array.from(document.body.childNodes)
}
console.timeEnd()

結果

  • スプレッド演算子がかなり速いようです。構文としてもスマートなので、こっちを使っていこうと思います。
statement time
[...xxx] 11421.31689453125ms
Array.from(xxx) 16186.26708984375ms
  • もっと速いのがある場合は、コメント欄で教えて下さい!

学習

日本語 英語
配列型 array-like arugments
NodeList
反復型 iterable Map
Set
String
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CORSでXMLHttpRequest がエラーになる対処方法

はじめに

GitHub Pagesで電車遅延を知らせるWebページを作っていた時にCORSにハマったので対処方法をまとめました。

環境

ブラウザ:chrome バージョン 74

JavaScript
const xhr = new XMLHttpRequest();
xhr.open("GET", `https://rti-giken.jp/fhc/api/train_tetsudo/delay.json`, true);
xhr.responseType = 'json';
xhr.onreadystatechange = function() {
  if(xhr.readyState === 4) {
    if(xhr.status === 200) {
      //
      // JSON遅延データ処理
      //
    }
  }
};
xhr.send();

エラーメッセージ

実行すると以下のメッセージが出る。

Access to XMLHttpRequest at 'https://rti-giken.jp/fhc/api/train_tetsudo/delay.json' from origin 'https://username.github.io' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

対処方法

chrome.exeの起動オプション(引数)に --disable-web-security、--disable-web-securityを付ける。クライアント側の対応のみで解決する。

参考

Access-Control-Allow-Originのエラーを回避してWebサービスを呼ぶには?

鉄道遅延情報のjson

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

Shapes & Drawing -p5.js Tutorial [JavaScript for Beginner]

レッスン動画

1.3: Shapes & Drawing - p5.js Tutorial

レッスンで使うWebサービス
p5*js Web Editor
p5*js Reference

メモ

// Syntax
instruction (__,__,__);

// Sample 1
createCanvas (400,300,200);
rect(100,50,25,75);

// functionName
”rect” | rectangle(長方形)

Code1

p5.js
// lineが最前面に表示
function setup(){
  createCanvas(400,400);
}

function draw() {
  backGround(220,0,200);
  rect(100,50,25,75);   //長方形 (x,y,w,h);
  line(0,50,400,300);
}

Preview1

ダウンロード.png

Code2

p5.js
function setup(){
  createCanvas(400,400);
}
// rectが最前面に表示
function draw() {
  background(220, 0, 200);
  line(0,50,400,300)
  rect(30,20,55,55);
}

Preview2

ダウンロード (1).png

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

Reduxアーキテクチャのサイクルをコードリーディングで一通り確認する

この文書では、React JS Tutorialsのソースコードをコードリーディングすることで、Reduxアーキテクチャのサイクルを一通り確認します。

fluximg.jpeg

事前知識

  • reactについては公式チュートリアルは完了しているものとします。

準備

node.jsのインストールは完了しているものとします。

React JS Tutorialsのソースコードをダウンロードしてください(※バージョン違いが生じないようにフォークしてあります)。

ダウンロードしたらプロジェクトのルートディレクトリで、

cd 5-redux-react
npm i
npm run dev

します。

そして、localhost:8080をブラウザで開いてください。

コードリーディング

まずsrc/js/client.jsを見てください。ここがアプリケーションのエントリポイントになります。普通のReactアプリケーションと同様に、ReactDOM.render()を呼んでいます。

import React from "react"
import ReactDOM from "react-dom"
import { Provider } from "react-redux"

import Layout from "./components/Layout"
import store from "./store"

const app = document.getElementById('app')

ReactDOM.render(<Provider store={store}>
  <Layout />
</Provider>, app);

Layoutコンポーネントを確認します。src/js/components/Layout.jsを見てください。

import React from "react"
import { connect } from "react-redux"

import { fetchUser } from "../actions/userActions"
import { fetchTweets } from "../actions/tweetsActions"

@connect((store) => {
  return {
    user: store.user.user,
    userFetched: store.user.fetched,
    tweets: store.tweets.tweets,
  };
})
export default class Layout extends React.Component {
  componentWillMount() {
    this.props.dispatch(fetchUser())
  }

  fetchTweets() {
    this.props.dispatch(fetchTweets())
  }

  render() {
    const { user, tweets } = this.props;

    if (!tweets.length) {
      return <button onClick={this.fetchTweets.bind(this)}>load tweets</button>
    }

    const mappedTweets = tweets.map(tweet => <li key={tweet.id}>{tweet.text}</li>)

    return <div>
      <h1>{user.name}</h1>
      <ul>{mappedTweets}</ul>
    </div>
  }
}

画面が表示される時(componentWillMount)にユーザーを取得し、画面に表示しています。この動作に沿って説明をしていきます。

src/js/components/Layout.jsの15行目を見てください。

  componentWillMount() {
    this.props.dispatch(fetchUser())
  }

propsのdispatchを呼んでいますね。

ここでpropsにdispatchがあると言うのは、このコンポーネントへconnectしているからです。connectは、7行目にあります。

@connect((store) => {
  return {
    user: store.user.user,
    userFetched: store.user.fetched,
    tweets: store.tweets.tweets,
  };
})
export default class Layout extends React.Component {

@はDecoratorといって、コード上「次」にある要素に対して、影響を及ぼすことができる機能です。今回のコード例では、これは「次」にあるLayoutコンポーネントに対するconnectの記述です。connectすることで、Reactコンポーネントにthis.props.dispatchが生えて、dispatchできるようになります。

ここでは、fetchUser()というActionをdispatchしていると言うことになります。

このdispatchの結果、ユーザーの情報が読み込まれて画面が更新されることになるわけです。ここではその流れを追っていきます。

src/js/actions/userActoins.jsを見てください。

1行目にfetchUser()があります。ここはFETCH_USER_FULFILLEDというタイプのアクションを作成しています。またpayloadにユーザーのデータが入っています。これがdispatchされているわけです。

dipatchされると、それをreducerが処理し、storeを変更します。

src/js/reducers/userReducer.jsを見てください。

19行目がFETCH_USER_FULFILLEDを処理するreducerです。

      case "FETCH_USER_FULFILLED": {
        return {
          ...state,
          fetching: false,
          fetched: true,
          user: action.payload,
        }
      }

reducerが何をやっているかと言うと、stateを変更します。

...state,で現在のstateを展開し、fetching,fetched,userを変更して、returnします。returnされた結果が新しいstateになります。

ここでいうstateというのはアプリケーション全体におけるただ一つのstateです。アプリケーション内のstateを全てまとめて一元管理するのがreduxの考え方です。

reactの感覚でいうとstateは個々のコンポーネントにあるもののように思われますが、それとは違うので考え方の切り替えが必要です。

さて、こうして変更されたstateは画面に反映されなければなりませんが、それはどのようにされるのでしょうか。再度、Layoutコンポーネントを確認します。src/js/components/Layout.jsの7行目を見てください。

@connect((store) => {
  return {
    user: store.user.user,
    userFetched: store.user.fetched,
    tweets: store.tweets.tweets,
  };
})
export default class Layout extends React.Component {

connectが、reduxのstateと画面のpropsの紐付けを行います。returnされた要素が画面のpropsとして渡されます。

stateが更新されると、reduxは以前のstateと比較を行い、更新されている部分のデータを使っている画面コンポーネントについて再描画を行います。

ここまででアクションの発行から画面の更新までの流れを一通り確認できました。

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

Ace.jsにスニペットを追加する

実行環境

・Electron
・Ace.js

Ace.jsはHTML上で動作するエディターを追加するものです
ちょっと独特な感じだけど

前回の記事のコードをもとに構築してます

スニペット(Snippet)は

fun

と入力すると

function name() {
  // function
}

みたいになんか簡単に生成してくれるやつです(語彙力皆無)
その解説が何一つなかったので実際にそれらしく作ってみました

const langTools = ace.require("ace/ext/language_tools");
editor.commands.on("afterExec", function(e) {
  if (e.args == "Callback.addCallback" && e.command.name == "insertstring") {
    editor.insertSnippet(
`("name", function () {
\t
})`);
  }
});
const words = [
  {
    "word": "Callback.addCallback"
  }
];
const rhymeCompleter = {
  getCompletions: function(editor, session, pos, prefix, callback) {
    callback(null, words.map(function(ea) {
      return {
        name: ea.word,
        value: ea.word,
        meta: "データ名"
      }
    }));
  }
}
langTools.addCompleter(rhymeCompleter);

解説

editor.commands.on("afterExec", function(e) {
  if (e.args == "予測変換の文字" && e.command.name == "insertstring") {
    editor.insertSnippet(
`("name", function () {
\t
})`);
  }
});

e.args == "予測変換の文字"
は登録してある word : "" を与えます
e.command.name == "insertstring"でインサートした場合のみに
insertSnippetで文字を書き足しています

スクリーンショット (308).png
スクリーンショット (309).png

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

【JavaScript】動画倍速機能の実装

Wordpressのプラグインで、動画倍速ってないんですね。
これを元に開発しようかな。

動画(音声)を倍速にする機能(ボタンクリック)

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="main.js"></script>
</head>
<body>
    <output id="res">
        <video src='/Users/matsumurashun/Downloads/pu-tyan.mp4' width='500' height='300' controls loop></video>
        <form>
            <input type="button" value="1倍速">
            <input type="button" value="2倍速">
            <input type="button" value="3倍速">
        </form>
    </output>    
</body>
</html>
main.js
window.onload = () => {
    const btn = document.getElementsByTagName("input");
    const video = document.getElementsByTagName("video")[0];

    btn[0].addEventListener('click', () => {
        video.playbackRate = 1.0;
        video.defaultPlaybackRate = 1.0;
    }, false);

    btn[1].addEventListener('click', () => {
        video.playbackRate = 2.0;
        video.defaultPlaybackRate = 2.0;
    }, false);

    btn[2].addEventListener('click', () => {
        video.playbackRate = 3.0;
        video.defaultPlaybackRate = 3.0;
    }, false);
}

動画(音声)を倍速にする機能(プルダウン選択)

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="main.js"></script>
</head>
<body>
    <output id="res">
        <video src='/Users/matsumurashun/Downloads/pu-tyan.mp4' width='500' height='300' controls loop></video>
        <form>
            <select>
                <option value="1">1倍速</option>
                <option value="2">2倍速</option>
                <option value="3">3倍速</option>
            </select>
        </form>
    </output>    
</body>
</html>
main.js
window.onload = () => {
    const selector = document.getElementsByTagName("select")[0];
    selector.addEventListener('change', (it) => {
        video.playbackRate = setRate(it.target.value);
    });
    const setRate = (val) => {
        return Number(val);
    }
}

playbackRate:再生速度の倍率
Number:Numberオブジェクトで、引数に入れれば文字列を数値に変換してくれる

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

[Vue.js]Phpstorm 絶対パスでcomponentのパスを指定

やりたいこと

絶対パスでcomponentのパスを指定する。
プログラムのエラーは出ないが、エディタ(私が使用しているものはPhpstormです)で
「文法違いますよ」というエラー(黄色の破線)が出る。
紛らわしいので、なんとかしたい。

修正前

<script>
// @ is an alias to /src

import TheClientHeader from '../../components/object/project/TheClientHeader' //←★修正前コレ
export default {
  name: 'Home',
  components: { TheClientFooter },
}
</script>

修正後

<script>
// @ is an alias to /src

import TheClientHeader from '@/components/object/project/TheClientHeader' //←★修正後コレ(文法エラーの表記は出ていない)
export default {
  name: 'Home',
  components: { TheClientFooter },
}
</script>

解決方法

import-resolver.jsというファイルをドキュメントルート直下に作成し、
以下を記述

/* eslint-disable no-undef */
System.config({
  paths: {
    '@/*': 'src/*',
  },
})


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

Nodeで対話型プログラム(readlineモジュールのasync iterator対応について)

NodeでCLIなソフトを作るとき、標準モジュールの readline を使うのだけれど「ユーザからの入力を同期的に待ち、それに応じた処理する」という一見楽勝な操作でさえ、煩雑な形の実装になってしまう。JavaScript/Nodeが非同期なAPIを提供することに起因する。

そのため、これまではreadline-syncというサードパーティ製のライブラリを使うことが多かったのだけれど Promise, await, async iterator を手に入れた今、(単純な要件なら)readline-sync を使わなくても 簡単に実現できる。

この辺は以前 考えた けどもう一度必要になったので再度整理したところ (Node v11 から?) readline.createInterface が async iterable を返すようになっていたという公式ドキュメントの記載を見つけたので、スッキリ書けるようになっていることに気づいた。

以下 Node v12で動作確認しており、たぶんv11以降じゃないと動作しない

レシピ1: 2数を行ごとに受け取って和を表示

イテレータを手動管理するパターン

const readline = require('readline');

async function main () {
  const rl = readline.createInterface({ input: process.stdin });
  const ait = rl[Symbol.asyncIterator]();
  const n1 = parseInt((await ait.next()).value, 10);
  const n2 = parseInt((await ait.next()).value, 10);
  rl.close();
  console.log(`answer is ${n1 + n2}`);
}

main();

ユーザからの入力が2行であるとわかっている場合、rl[Symbol.asyncIterator]()のように明示的にイテレータを取得してnextを呼び出すことで値を取得する。rl.close()は明示的に呼び出さないといつまでも入力を待ち続けることになる(Ctrl+Dを押さないとプログラムが終了しなくなる)。

レシピ2: 可変個の数を行ごとに受け取り総和を表示

イテレータ管理は for-await-of に任せるパターン

const readline = require('readline');

async function main () {
  const rl = readline.createInterface({ input: process.stdin });
  let sum = 0;
  for await (const val of rl) {
    sum += parseInt(val, 10);
    console.log(`accumulated: ${sum}`);
  }
  console.log(`answer is ${sum}`);
}

main();

入力の終わりを知らせるために Ctrl+D を押下する必要がある。その代わりイテレータの管理をしなくてもよい。forループの内部で何らかの脱出条件の判定をして明示的に rl.close() を呼び出すのもよいアイディアだと思う。

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

地獄からの使者、その名はTypeScript ~ 固かった型の形 ~

地獄のTypeScript

 TypeScriptは便利である。変な書き方をしていれば教えてくれるし、開発環境が入力補完の支援もしてくれる。まさにヘブンである。しかし書き方を知らないと、その世界は地獄と化す。真っ赤なエラーに焼き尽くされた地獄に。

 地獄を回避するためには解決法を知ることである。TypeScriptは今も刻一刻と進化しており、昨日の知識は原始の人間が棒の握り方を覚えたに等しい。

 ということで地獄を渡り、天国へ到達するための知恵の一部を紹介していきたいと思う。たぶん他にもあるような気がするのだが、TypeScriptを使い始めてだいぶ慣れてしまったので、何に困ったのか覚えていない。こういう場合にどう解決するべきなのかという疑問があれば、一緒に考えていきたいと思う。

1. JavaScriptからTypeScript移行時に最初に訪れる試練、連想配列

 これを一度もやらずにTypeScriptを使っている者がいたとすれば驚愕である。JavaScriptからTypeScriptに移行する場合に、最初に放ってくる地獄からの息吹である。これに心を折られ、踵を返してしまうものもいることだろう。

  • 失敗例
    プロパティの存在しないオブジェクトが作られ、aが入らない
const value1 = {}
value1.a = 10 //ここでエラー
console.log(value1.a)
  • 修正例
    連想配列にする
const value1:{[key:string]:any} = {}
value1.a = 10
console.log(value1.a)

2. interfaceを使うほどでもないので、何度も同じ型を定義してしまう

 こうして型を付けるのがかったるいと思ってしまうのだ

const value1: { a?: number, b?: number } = { a: 100 }
const value2: { a?: number, b?: number } = { b: 200 }

 typeofで型が再利用できる

const value1: { a?: number, b?: number} = { a: 100 }
const value2: typeof value1 = { b: 200 }
console.log(value2.a) //コンパイルが通る

3. イキってstrictをtrueにしたときに訪れる試練、オブジェクトのキー

 valuesは a|b|c のキーしか持たないので、string型のkeyを突っ込むとエラーにされる。Object.keysから推論して欲しいところだが、現在はそうなっていない。

const values =  {'a':100,'b':200,'c':300}
for(const key of Object.keys(values)){
    console.log(`${key}:${values[key]}`) //values[key]でエラー
}

 よく見かけるのはアンチパターンの対処法だ。型を吹き飛ばして無理やり引っ張り出す手法だ。テロリストを倒すのに、住民もろとも爆撃してしまうような暴挙と呼べる。この場合はkeyの型を正しく認識させなければいけない。typeofで型を取り出し、さらにkeyofでキーの型を取り出すという二重構造だ。

const value2 = { 'a': 100, 'b': 200, 'c': 300 }
for (const key of Object.keys(value2)) {
    console.log(`${key}:${value2[key as keyof typeof value2]}`)
    //console.log(`${key}:${(value2 as any)[key]}`) //よくあるアンチパターン
}

4. intarface中のプロパティの型が欲しい場合

 interface自体を使えればよいのだが、中のプロパティの型が欲しいケース
 巨大な構造だったら再定義は本当の地獄だ

interface AnyData{
    values:{
        a:number
        b:number}[]
}

const proc = (values)=>{ //←ここで型を付けないといけない
    console.log(`a:${values.a} b:${values.b}`)
}
const anyData: AnyData = { values: [{ a: 100, b: 200 }] }
proc(anyData.values) //インタフェイスの中の一部分を渡す

 ちなみにインタフェイス中のプロパティの型を取得する方法は、「もしかしてこれがやりたいんでしょ」とVSCodeが教えてくれた
 一所懸命調べたが、ネット上では探しきれなかった
 VSCode先生、一生はついていかないけど適当なところまではついていきます!

interface AnyData {
    value: {
        a: number
        b: number
    }
}
//const proc = (values: {a:number,b:number}) => { //再定義してしまう書き方
const proc = (values: AnyData["value"]) => { //元の定義を利用
    console.log(`a:${values.a} b:${values.b}`)
}
const anyData: AnyData = { value: { a: 100, b: 200 } }
proc(anyData.value)

5. オマケ、プロパティがあるか確認しようとするとエラーになるケース

 これは解説記事が多いので、いまさらかもしれない

//プロパティがあるか確認しようとするとエラー
interface A{
    a:number
}
interface B{
    b:number
}
const proc2 = (value: A | B)=>{
    if(typeof value.a !== 'undefined') //この時点でaが使えない
        console.log(value.a)
}
proc2({a:100})
interface A {
    a: number
}
interface B {
    b: number
}
const proc2 = (value: A | B) => {
    if ("a" in value) //TypeScriptではこう書かないといけない
        console.log(value.a)
}
proc2({ a: 100 })

地獄から天国への道

 TypeScriptの地獄から、天国へいたる道は長く険しい。そして何人もの人間がエンマ大王に舌を抜かれ沈黙する。しかしそこを抜けた先には、必ずやヘブンがあるはずだ。釜茹でにされようが、針で刺されようが、地道に賽の河原で石を積み上げていくしか方法はないのである。

 地獄を知らぬ者に、天国を感じることはできないのだから。

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

Udacity: ES6 JavaScript Improved メモ③-2

UdacityのES6 - JavaScript Improvedコースを、受講しながらメモしていきます。
Lesson3 Built-ins

長くなったので分割しました。

Map

Mapはキーと値のペアで構成される点で、オブジェクトに似ている。
(Setはどちらかというと配列に似ている。)
set()メソッドを使用して、キーと値のペアを格納できる。

example.js
const employees = new Map();

employees.set('james.parkes@udacity.com', { 
    firstName: 'James',
    lastName: 'Parkes',
    role: 'Content Developer' 
});

1つ目の引数にキーを、2つ目の引数に値を渡す。
変数employeesには、james.parkes@udacity.comというキーで、オブジェクトを格納している。
またSetと同じく、delete()メソッドやclear()メソッドを備えている。

値の取得にはget()メソッドを使用する。引数にはキーを渡す。

ループにはSetで使える方法に加えてforEach()メソッドが使える。

Promises

非同期処理を操作できる。
new Promise()とすることで書き始められる。
引数には非同期で行いたいfunctionを渡す。
このfunctionはexecutorと呼ばれ、引数にresolve関数とreject関数を受け取る。
executor関数はresolveとrejectを受け取り、Promiseが実装されるとすぐに実行される。

example.js
new Promise(/* executor */function (resolve, reject) {
    window.setTimeout(function createSundae(flavor = 'chocolate') {
        const sundae = {};
        // request ice cream
        // get cone
        // warm up ice cream scoop
        // scoop generous portion into cone!
        resolve(sundae);
    }, Math.random() * 2000);
});

executor は非同期の作業を開始して、完了した際にresolveもしくはrejectのいずれか一方を呼び出す。
resolveは、成功した場合。
rejectは失敗した場合。

詳しくはJavaScript Promisesコースで。非同期通信を使ったアプリを作れるらしい。

Generators

通常、functionは一度呼ばれると、処理が終了するまで止めることはできない。
これを可能にする新しいfunctionがgenerator functionsである。

example.js
function* getEmployee() { //functionの後ろに * をつける
    console.log('the function has started');

    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];

    for (const name of names) {
        console.log( name );
    }

    console.log('the function has ended');
}

ジェネレータ関数が呼ばれると、処理は実行されずに、iteratorを返す。
getEmployee関数で見ると…

example.js
const generatorIterator = getEmployee(); // 処理は実行されずにiteratorを変数に格納している
generatorIterator.next(); // 処理を実行

.next()メソッドを使うことで関数の処理を実行できる。
next()で実行されているジェネレータ関数の中で処理を一時停止させるには、yieldキーワードを使う。

example.js
function* getEmployee() {
    console.log('the function has started');

    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];

    for (const name of names) {
        console.log(name);
        yield; // yieldを追加
    }

    console.log('the function has ended');
}

getEmployee関数を実行すると(generatorIterator.next())、Amandaが表示されて処理が一度終了する。
再度実行すると、今度はDiegoが表示されて処理が一度終了する。
このようにジェネレータ関数は、前回停止した箇所を覚えておける

next()に引数を渡すと、ジェネレータ関数内のyieldにセットされる。

yieldキーワードは関数の一時停止と、ジェネレータ関数へデータを渡す2つの役割を持つ。

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

Adobe Illustrator ベジェ曲線の描画コマンドをスクリプトで取得する

はじめに

Adobe Illustratorのオブジェクトをcreate.js用の描画コマンドに変換するスクリプトを作りましたのでご紹介します。

Adobe Illustrator Extend Scriptとは

Adobe IllustratorはJavaScriptで操作が可能です。
公式リファレンス
定型処理をスクリプト化しておくことで、作業効率を向上させることができます。
本記事のスクリプトはこの機能を利用しています。

機能

  • パスで描画されたオブジェクトをcreate.jsのgraphicsコマンドに変換します。
  • 選択されたオブジェクトを1つのcreatejs.Shapeオブジェクトとして出力します。
  • オブジェクトの重ね順は維持されます。
  • レイヤー効果やグラデーションは再現できません。
  • リンク、埋め込みされた画像には非対応です。

スクリプト

スクリプトのソースコード
'use strict';

function getInitialSelections() {
  if (app == null) {
    return;
  }

  if (!app.activeDocument) {
    alert("対象となるドキュメントを開いてからスクリプトを実行してください");
    return;
  }

  var n = activeDocument.selection.length;

  if (n <= 0) {
    alert("座標を書き出したいオブジェクトを選択してからスクリプトを実行してください");
    return;
  }

  return activeDocument.selection;
}

/**
 * 指定された名前のレイヤーを作成する。
 * すでにその名前のレイヤーが存在する場合はそのレイヤーを返す。
 * @param {string} layerName
 * @returns {*} layer
 */
function addLayer (layerName) {
  var layer;
  var myDoc = app.activeDocument;
  var n = myDoc.layers.length;

  for (var i = 0; i < n; i++) {
    if (myDoc.layers[i].name === layerName) {
      layer = myDoc.layers[i];
      layer.locked = false;
      layer.visible = true;
      break;
    }
  }

  if (!layer) {
    layer = myDoc.layers.add();
    layer.name = layerName;
  }

  return layer;
}

/**
 * TextFrameItemの文字全てに塗りの色を指定する。
 * @param {TextFrameItem} obj
 * @param {number} r
 * @param {number} g
 * @param {number} b
 */
function setTextFrameColor (obj, r, g, b) {
  var fillColor = new RGBColor();
  fillColor.red = r;
  fillColor.green = g;
  fillColor.blue = b;

  for (var i = 0, n = obj.characters.length; i < n; i++) {
    var chr = obj.characters[i];
    chr.fillColor = fillColor;
  }
}

/**
 * 指定されたレイヤーにTextFrameItemを追加する。
 * @param layer TextFrameItemを追加するレイヤー
 * @param text TextFrameItemの内容
 * @param bounds TextFrameItemの座標 [0] = x, [1] = y
 */
function addTextFrameItem (layer, text, bounds) {
  var textFrame = layer.textFrames.add();
  textFrame.contents = text;
  textFrame.translate(bounds[0], bounds[1]);
  setTextFrameColor(textFrame, 255, 0, 255);
  return textFrame;
}

/**
 * 塗りカラー情報を取得する。
 * 書式はCSSのrgba(r,g,b,a)に準ずる。
 * @param {PathItem|TextFrameItem} obj
 */
function getFillColorAsCSS(obj) {
  switch (obj.typename) {
    case "TextFrame":
      return getTextFillColorAsCSS(obj);

    case "PathItem":
      return getPathFillColorAsCSS(obj);
  }

  return null;
}
/**
 * 線色情報を取得する。
 * 書式はCSSのrgba(r,g,b,a)に準ずる。
 * @param {PathItem|TextFrameItem} obj
 * @returns {string|null}
 */
function getStrokeColorAsCSS(obj) {
  switch (obj.typename) {
    case "TextFrame":
      return getTextStrokeColorAsCSS(obj);

    case "PathItem":
      return getPathStrokeColorAsCSS(obj);
  }

  return null;
}

/**
 * TextFrameItemの塗りからCSS文字列を取得する。
 * @private
 * @param obj
 * @returns {string}
 */
var getTextFillColorAsCSS = function getTextFillColorAsCSS(obj) {
  var chr = obj.characters[0];
  var color = chr.fillColor;

  if (color.typename !== "RGBColor") {
    return null;
  }

  var opacity = obj.opacity;
  return getColorAsCSS(color.red, color.green, color.blue, opacity);
};

/**
 * TextFrameItemの塗りからCSS文字列を取得する。
 * @private
 * @param obj
 * @returns {string}
 */
var getTextStrokeColorAsCSS = function getTextStrokeColorAsCSS(obj) {
  var chr = obj.characters[0];
  var color = chr.strokeColor;

  if (color.typename !== "RGBColor") {
    return null;
  }

  var opacity = obj.opacity;
  return getColorAsCSS(color.red, color.green, color.blue, opacity);
};

/**
 * PathItemの塗りからCSS文字列を取得する。
 * @param obj
 * @returns {string}
 */
var getPathFillColorAsCSS = function getPathFillColorAsCSS(obj) {
  var color = obj.fillColor;

  if (color.typename !== "RGBColor") {
    return null;
  }

  var opacity = obj.opacity;
  return getColorAsCSS(color.red, color.green, color.blue, opacity);
};

/**
 * PathItemの線色からCSS文字列を取得する。
 * @param obj
 * @returns {string}
 */
var getPathStrokeColorAsCSS = function getPathStrokeColorAsCSS(obj) {
  var color = obj.strokeColor;

  if (color.typename !== "RGBColor") {
    return null;
  }

  var opacity = obj.opacity;
  return getColorAsCSS(color.red, color.green, color.blue, opacity);
};

/**
 * rgbaの数値をCSS文字列に変換する。
 * @private
 * @param r
 * @param g
 * @param b
 * @param a
 * @returns {string}
 */
var getColorAsCSS = function getColorAsCSS(r, g, b, a) {
  if (a === 100) {
    return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  }

  return "rgba(" + r + ", " + g + ", " + b + ", " + a / 100 + ")";
};

/**
 * アートボード座標系の座標配列をルーラー原点の座標配列に変換する。
 * @param pos{Array}
 * @return {number[]}
 */
function convertToRulerOrigin(pos) {
  return [pos[0], -pos[1]];
}

/**
 * ランダムな文字列を指定された文字数で生成する。
 * @param len
 * @returns {string}
 */
function generateRandomString(len) {
  var l = len; // 生成する文字列の長さ

  var c = "abcdefghijklmnopqrstuvwxyz"; // 生成する文字列に含める文字セット

  var cl = c.length;
  var r = "";

  for (var i = 0; i < l; i++) {
    r += c[Math.floor(Math.random() * cl)];
  }

  return r;
}

/**
 * グループ / レイヤーアイテムのchildren配列を取得する。
 * 空の場合は空配列を返す。
 * @return {*[]}
 */
function getChildren(obj) {
  var children = [];
  /**
   * pageItemsは存在するか不定のプロパティなためtryブロックで処理を行う。
   * hasOwnPropertyで存在確認を行うとerrorで落ちる。
   */

  try {
    //pageItemsは配列ではなくコレクションなため、concatではなくpushでコピーを行う。
    var n = obj.pageItems.length;

    for (var i = 0; i < n; i++) {
      children.push(obj.pageItems[i]);
    }
  } catch (e) {}

  return children;
}

/**
 * ベジェ曲線をcreatejsのgraphicsコマンドとして取得する。
 *
 * ルーラー座標を原点とする場合、
 * 「表示->定規->アートボード定規に変更」
 * が設定されている必要がある。
 * https://helpx.adobe.com/jp/illustrator/using/rulers-grids-guides-crop-marks.html
 * ウィンドウ定規の設定では、ルーラーは無視される。
 *
 * 現状では、選択されたオブジェクトが全て共通のレイヤーに所属している前提で動作している。
 * 異なるレイヤーに所属するオブジェクトを選択した場合、重なり順が狂う可能性がある。
 */

var resultLayer;
var resultLayerName = "_Bezier"; //改行、区切りコード

var CR = String.fromCharCode(13); //ポイント配列のインデックス。x, yはそれぞれ0と1に格納される。

var X = 0;
var Y = 1;
main();

function main() {
  var selections = getInitialSelections();
  if (selections == null) return;
  $.writeln(activeDocument.pageItems); //app.selectionは必ず最前面から順に格納されているので、描画順のために反転させる。

  selections.reverse();
  resultLayer = addLayer(resultLayerName); //現状の座標系設定を保存、座標系をアートボード原点に変更

  var currentSystem = app.coordinateSystem;
  app.coordinateSystem = CoordinateSystem.ARTBOARDCOORDINATESYSTEM; //新規のshapeオブジェクトを宣言

  var shapeName = "Shape_" + generateRandomString(4);
  var result = "const get" + shapeName + " = () =>{" + CR + ("  const shape = new createjs.Shape();" + CR) + ("  const g = shape.graphics;" + CR) + ("  const drawBezier = ( points ) =>{" + CR) + ("    points.forEach ( ( p, index ) =>{" + CR) + ("      if( index === 0 ) { g.mt(...p); return;}" + CR) + ("      g.bt(...p);" + CR) + ("    });" + CR) + ("  };" + CR);

  for (var i = 0; i < selections.length; i++) {
    var command = outputPath(selections[i]);
    if (command != null) result += command;
  }

  result += "  return shape;" + CR + "}" + CR;
  addTextFrameItem(resultLayer, result, getSelectionsLeftTop(selections)); //座標系を元に戻す

  app.coordinateSystem = currentSystem;
}

function outputPath(obj) {
  //グループオブジェクトの場合、子アイテムの再帰処理。
  if (obj.typename === "GroupItem") {
    var children = getChildren(obj);
    children.reverse();
    var result = "";
    var n = children.length;

    for (var i = 0; i < n; i++) {
      result += outputPath(children[i]);
    }

    return result;
  } //選択した対象がパスではないので終了


  if (obj.typename !== "PathItem") {
    return;
  }

  return getPathCommand(obj);
}

/**
 * パスのグラフィックコマンドを取得する。
 * @param obj
 */
function getPathCommand(obj) {
  var result = "";
  var fillColor = getFillColorAsCSS(obj);
  var strokeColor = getStrokeColorAsCSS(obj); //塗り線のカラーがない場合は中断。

  if (fillColor == null && strokeColor == null) {
    return result;
  } //graphicsに塗りとストロークの開始を宣言


  result += "  g";

  if (fillColor != null) {
    result += ".f(\"" + fillColor + "\")";
  }

  if (strokeColor != null) {
    result += ".s(\"" + strokeColor + "\").ss(" + obj.strokeWidth + ")";
  }

  result += ";" + CR; //各アンカーポイントを繋ぐストロークコマンド

  result += getStrokes(obj); //ストロークの終了宣言

  result += "  g";

  if (fillColor != null) {
    result += ".ef()";
  }

  if (strokeColor != null) {
    result += ".es()";
  }

  result += ";" + CR;
  return result;
}

/**
 * パスのストロークコマンドを取得する。
 * @param obj
 * @returns {string}
 */
function getStrokes(obj) {
  //.mtコマンドで始点に移動。
  var anchor = convertToRulerOrigin(obj.pathPoints[0].anchor);
  var result = "  drawBezier([" + CR;
  result += "    [" + anchor[X] + ", " + anchor[Y] + "]" + CR; //.btコマンドでベジェ曲線を描画。

  var n = obj.pathPoints.length;

  for (var i = 1; i < n; i++) {
    result += addStroke(obj, i);
  } //クローズパスの場合、最後のアンカーと始点を結ぶ。


  if (obj.closed) {
    result += addStroke(obj, 0);
  }

  result += "  ]);" + CR;
  return result;
}

/**
 * 指定されたインデックスのアンカーポイントまでのカーブ描画コマンドを生成する。
 * 0が指定された場合は、パス末尾から始点に繋がるカーブの描画コマンドを生成する。
 * @param obj
 * @param i インデックス番号
 * @return {string}
 */
function addStroke(obj, i) {
  var result = "    , [";
  var cp1Index = i - 1;

  if (i <= 0) {
    cp1Index = obj.pathPoints.length - 1;
  }

  var pos = obj.pathPoints;
  var anchor = convertToRulerOrigin(pos[i].anchor);
  var cp1 = convertToRulerOrigin(pos[cp1Index].rightDirection);
  var cp2 = convertToRulerOrigin(pos[i].leftDirection);
  result += cp1[X] + ", " + cp1[Y] + ",";
  result += cp2[X] + ", " + cp2[Y] + ",";
  result += anchor[X] + ", " + anchor[Y] + "]" + CR;
  return result;
}

/**
 * 選択対象からバウンディングボックスの左上座標を取得する。
 * @param selections
 * @return {*[]} xy座標の配列 array[0] = x , array[1] = y; y座標はイラストレーター標準のY軸が下でマイナス方向のもの
 */
function getSelectionsLeftTop(selections) {
  var currentSystem = app.coordinateSystem;
  app.coordinateSystem = CoordinateSystem.ARTBOARDCOORDINATESYSTEM;
  var x, y;
  var n = selections.length;

  for (var i = 0; i < n; i++) {
    var target = selections[i];
    var bounds = target.geometricBounds;
    var targetX = Math.round(bounds[0]);
    var targetY = Math.round(bounds[1]);
    if (x === undefined) x = targetX;
    if (y === undefined) y = targetY;
    x = Math.min(x, targetX);
    y = Math.max(y, targetY);
  }

  if (x === undefined) x = 0;
  if (y === undefined) y = 0;
  app.coordinateSystem = currentSystem;
  return [x, y];
}

ソースコードが非常に長いため、折りたたんで掲載しています。

関数単位で分割しながら開発を行い、rollup.jsで連結してillustratorで動作するスクリプトを出力しています。バンドルの方法に関しては、以下の記事をご参照ください。

Babel + rollup.jsでES3環境向けトランスパイル

それぞれの関数は切り出して使用することもできます。ぜひご利用ください。

出力例

illustrator上でドキュメントを開き、出力したいオブジェクトを選択します。この状態でスクリプトを実行すると、以下のようなテキストオブジェクトが出力されます。
この例はハート型のオブジェクトを出力したものです。

const getShape_vsrb = () =>{
  const shape = new createjs.Shape();
  const g = shape.graphics;
  const drawBezier = ( points ) =>{
    points.forEach ( ( p, index ) =>{
      if( index === 0 ) { g.mt(...p); return;}
      g.bt(...p);
    });
  };
  g.s("#e61e2c").ss(1);
  drawBezier([
    [199.999692558295, 139.037809861326]
    , [199.999692558295, 139.037809861326,216.51750134205, 91.0703552349714,260.408864739811, 101.478048251449]
    , [304.306569058068, 111.887435228376,302.040473463536, 149.898168782486,297.969050788654, 167.089794944798]
    , [293.891253219555, 184.314831799952,266.291989537882, 213.247676318413,241.405592420213, 229.538070494225]
    , [216.51750134205, 245.828464670041,201.360366325656, 263.475297677716,199.999692558295, 269.360116287618]
    , [198.644947652663, 263.475297677716,183.481883774537, 245.828464670041,158.595486656872, 229.538070494225]
    , [133.707395578709, 213.247676318413,106.111932139902, 184.314831799952,102.030334327937, 167.089794944798]
    , [97.9589002789444, 149.898168782486,95.6966160382808, 111.887435228376,139.592214337274, 101.478048251449]
    , [183.481883774537, 91.0703552349714,199.999692558295, 139.037809861326,199.999692558295, 139.037809861326]
  ]);
  g.es();
  return shape;
}

出力されたアロー関数を実行するとcreatejs.Shapeのインスタンスが得られます。これをstageに配置すれば描画完了です。

座標値の書式について

drawBezier関数に読み込ませている配列の書式は、Canvas​Rendering​Context2D.moveTo()
およびCanvas​Rendering​Context2D.bezier​CurveTo()関数の引数に準じています。

また自作のモーションパスアニメーションモジュールを製作しており、座標配列はこのモジュールにそのまま読み込めます。アニメーションモジュールに関しては、以下の記事をご参照ください。

モーションパスアニメーションのモジュールを作った

以上、ありがとうございました。

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

Ace.jsに予測変換を追加する

実行環境

・Electron
Ace.js

Ace.jsはHTML上で動作するエディターを追加するものです
ちょっと独特な感じだけど

Electronじゃない場合は読み込みディレクトリを修正してください。(多分)

スぺニットはこちら

const editor = ace.edit("editor");
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/javascript");
editor.$blockScrolling = Infinity;
editor.setAutoScrollEditorIntoView(true);
editor.setOptions({
    enableBasicAutocompletion: true,
    enableSnippets: true,
    enableLiveAutocompletion: true
});
// const beautify = ace.require("ace/ext/beautify"); // get reference to extension
// beautify.beautify(editor.session);
const langTools = ace.require("ace/ext/language_tools");
const words = [
    {"word":"hello"},
    {"word":"word"}
];
const rhymeCompleter = {
    getCompletions: function(editor, session, pos, prefix, callback) {
        callback(null, words.map(function(ea)   {
            return {name: ea.word, value: ea.word, meta: "データ名"}
        }));
    }
}
langTools.addCompleter(rhymeCompleter);

重要な場所はここだけ

const langTools = ace.require("ace/ext/language_tools");
const words = [
    {"word":"hello"},
    {"word":"word"}
];
const rhymeCompleter = {
    getCompletions: function(editor, session, pos, prefix, callback) {
        callback(null, words.map(function(ea)   {
            return {name: ea.word, value: ea.word, meta: "データ名"}
        }));
    }
}
langTools.addCompleter(rhymeCompleter);

これでAce上で

スクリーンショット (305).png

と予測変換追加されました。

GITの質問場で英語で書かれた記事を探しまくってやっと見つけた
英語読めない人にとっては地獄

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

Node.jsのpakage.jsonで追加モジュールに頼らず、他のpakage.jsonのscriptを実行する方法

Node.jsのpakage.jsonで、追加モジュールに頼らず他のpakage.jsonのscriptを実行する方法

  • カレントディレクトリを変更してからコマンドを実行しなければならないのですが、意外に簡単な方法がありません
  • 今回の内容はNode.js自体の機能を使うだけなので、追加のモジュールのインストールは不要です
  • 欠点は記述が冗長なことと、コマンドの実行が終わらないと、コンソールに状況が表示されないことです
  • カレントディレクトリの変更が必要な、その他のコマンドにも対処可能です
package.json
{

    "scripts": {
        "コマンド名": "node -e \"process.chdir('ターゲットディレクトリ');console.log(require('child_process').execSync('npm run コマンド名').toString())\""
    }

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

Udacity: ES6 JavaScript Improved メモ③

UdacityのES6 - JavaScript Improvedコースを、受講しながらメモしていきます。
Lesson3 Built-ins

Symbols

新しく追加されたデータ型。
主に、オブジェクトのプロパティを特定するために使われる。
symbolを作るには、Symbol()を使用する。引数には文字列が入る。

example.js
const bowl = {
  'apple': { color: 'red', weight: 136 },
  'banana': { color: 'yellow', weight: 183 },
  'orange': { color: 'orange', weight: 170 },
  'banana': { color: 'yellow', weight: 176.845 } // 前に書いたbananaが上書きされる
};

この書き方だと、bananaというプロパティ名が重複するため、上書きされてしまう。
そこで…

example.js
const bowl = {
  [Symbol('apple')]: { color: 'red', weight: 136 },
  [Symbol('banana')]: { color: 'yellow', weight: 183},
  [Symbol('orange')]: { color: 'orange', weight: 170 },
  [Symbol('banana')]: { color: 'yellow', weight: 176 } // 上書きされない!
};

Symbol()を使うことで、プロパティ名の重複を許容することができる。
使いどころとしては、新しくメソッドなどを追加するとき。既存ライブラリとケンカせずに追加できる。

Set①

Setは、値の重複がないオブジェクト、のことを指す。
配列とは異なり、インデックスによるアクセスができないという点に注意。

example.js
const sample1 = [1, 1, 2, 3]; // 値の重複があるため、セットといえない
const sample2 = [1, 2, 3, 4]; // 値の重複がないため、セットといえる

sample2はセットといえるがただの配列にすぎず、sample2.push(2)とすれば、2が重複する。
そこで、セットを作り出すnew Set()が追加された。

example.js
const foo = new Set(); // 空のセットを作成
console.log(foo);
// Set{}

const games = new Set(['Pokemon', 'Mario', 'Zelda', 'Pokemon']); // 値を入力して初期化も可能
console.log(games);
// Set(3) {"Pokemon", "Mario", "Zelda"}

gamesには'Pokemon'を重複して入力しているが、Setは重複を許さないため、自動的に削除される。

Setの追加・削除にはそれぞれ、add()delete()メソッドが使える。
全ての要素を一括削除するにはclear()メソッドが使える。
先ほど作成したgames変数を使って…

example.js
games.add('Kirby');
games.add('Donkey Kong');
games.delete('Mario');

console.log(games);
// Set(4) {"Pokemon", "Zelda", "Kirby", "Donkey Kong"}

games.clear(); //一括削除

console.log(games);
// Set(0) {}

セットの長さ(配列のlengthに相当)は、set.sizeのようにsizeプロパティで取得できる。

Set②

values()メソッドは、イテレータオブジェクトを返す。
イテレータオブジェクトはnext()を使用して、格納された値を順番に取得できる。

example.js
const months = new Set(['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);

const iterator = months.values(); // 変数iteratorにイテレータオブジェクトを代入

console.log(iterator.next()); // {value: "January", done: false}
console.log(iterator.next()); // {value: "February", done: false}
console.log(iterator.next()); // {value: "March", done: false}

doneプロパティは、セットの最後に到達したかどうか、を表している。
つまり、trueになったら、セットの最後に到達したということ。

for...ofループを使えば、より簡単にアクセスできる。

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

JavaScriptはシングルスレッドで実行される

自分自身が勘違いしていたので記事にします。

問題

<!DOCTYPE html>
<html>

<head>
    <title>Document</title>
</head>

<body>
    <script>
        console.log(`Start ${new Date()}`)

        // 3秒後にCallbackと表示
        setTimeout(() => {
            console.log(`Callback ${new Date()}`)
        }, 3000)

        // 10秒間かかる処理を実行する
        const now = new Date()
        let dummy = 0
        while (new Date() - now < 10000) {
            dummy++ // 適当な計算処理
        }

        console.log(`End ${new Date()}`)
    </script>
</body>

</html>

このHTMLをブラウザで表示させると、コンソールにはStart, Callback, Endがどの順番で表示されるでしょうか?

  1. Startが表示される
  2. 3秒後にCallbackと表示されることが予約される
  3. 10秒間かかる処理を実行する
    この間に少なくとも3秒間経過するのでCallbackと表示される
  4. Endと表示される

よってStartCallbackEnd

だと思っていませんか?

残念!不正解です。

スクリーンショット 2019-05-21 11.14.17.png

実行結果

Start Tue May 21 2019 10:39:40 GMT+0900 (日本標準時)
End Tue May 21 2019 10:39:50 GMT+0900 (日本標準時)
Callback Tue May 21 2019 10:39:50 GMT+0900 (日本標準時)

実際にはStartEndCallbackの順番で実行されます。

ポイント

ここら辺がポイントです。

  • setTimeoutは規定時間後に関数の実行を予約する
  • しかし、JavaScriptはシングルスレッドで実行される
    • つまり、2つ以上の処理を並行して実行できない
    • 2つ以上の関数を同時実行できない

解説

つまり、正しくは以下のような順序になります。

  1. Startが表示される
  2. 3秒後にCallbackと表示されることが予約される
  3. 10秒間かかる処理を実行する
    この間に少なくとも3秒間経過するが、まだメインの処理が実行されているので2.の関数を実行開始できない
  4. Endと表示される
  5. メインの処理が完了して次の処理を実行可能になったので、2.の関数が実行される
    • Callbackと表示される

よってStartEndCallbackの順番で実行されます。

スクリーンショット 2019-05-21 11.14.22.png

ちなみに、setTimeoutを使う場合以外でも、

  • Ajaxでの非同期処理
  • Node.jsでのファイル読み込みなどの非同期処理

などの場合もシングルスレッドなので同様の処理順になります。

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

フロントエンドでもtrivyを使って脆弱性対策したい!

伝えたいこと

  • フロントエンドもコンテナ経由でアクセスさせよう
  • コンテナならtrivyを使ってヤバそうな脆弱性を知ることができる
  • trivyならCIに組み込むことも簡単
  • multi-stage buildの場合は工夫が必要
  • いまの状況でできる範囲の脆弱性対策からはじめよう

今回の記事でできること

trivy_flow.png

CI上で、本番用のフロントエンドのコンテナをmulti-satge buildでつくり、
trivyを利用して脆弱性を検知するところまでを目指します。

最後に、trivyを利用したレベル別の運用イメージも提案します。

導入

週末、trivyというコンテナ向けの脆弱性検知ツールが正式リリースされました。公開が5日目の5/21 12:00時点で 900star以上獲得しています。

trivyの詳細は原作者である @knqyf263 さんの「CIで使えるコンテナの脆弱性スキャナ」という記事を参照ください。

ただフロントエンドの方は「脆弱性はインフラ側が対応してくれるから、自分には関係ない」と思っていないでしょうか。
そのような方をメインに、今回は以下の内容を紹介します。

  • コンテナを使ってフロントエンドの本番環境を構築する
  • trivyを利用して、脆弱性が含まれていないかチェックする
  • trivyを通じた脆弱性の運用方法を学ぶ

僕は普段はフロントエンドも担当するエンジニアで、セキュリティに対して体系だって学んだことはありません。ただ、trivyのコミッタでもあり、脆弱性の運用も少し知っているので、フロントエンドでもDevSecOpsを定着させるきっかけになるといいなと考えています。

ソースコード

今回使うコードは、すべて以下のレポジトリにあります。

https://github.com/tomoyamachi/trivy-with-react
reactrivy.png

この記事では触れませんが、開発環境でもコンテナ経由でホットリロードするような仕組みになっています。

実践 : プロジェクト作成~trivyの導入

ここからはプロジェクトの作成と、CIにtrivyを組み込むまでの手順を書きます

1. プロジェクトの作成

今回はCreate React Appを利用して、Reactのプロジェクトをまず作成します。
そして、デフォルトではパッケージ管理ツールにyarnが利用されているので、今回はプロジェクトのパッケージ管理をnpmで実行します。

$ npx create-react-app trivy-with-react
$ cd trivy-with-react
$ rm -fr node_modules yarn.lock
$ npm install

2. 本番用コンテナの作成

  1. 本番用のコードを生成
  2. nginxコンテナに生成したコードを移し、nginxを起動

という手順をとります。

通常であれば1ファイル

そして通常2つのステップは、multi-stage buildを利用して、1つのDockerfileでコンテナを作成します。

Dockerfile
# build environment
FROM node:12.2.0-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build

# production environment
FROM nginx:1.16.0-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

from Dockerizing a React App

1ファイルでmulti-stage buildの問題点

multi-stage buildは1つのDockerfileで最終成果物のimageだけを出力してくれるので、通常はベストプラクティスです。
その場合、以下のように、builder側のイメージがIMAGE IDでしか特定できない形で作成されているのがわかります。

$ docker images
REPOSITORY         TAG    IMAGE ID ...
trivy-with-react   test   96c4fdfac287 # nginx側
<none>             <none> 41fd97016d25 # builder側

しかし、今回、脆弱性はbuilder側のコンテナにもあることに気がついた方もいるでしょう。package-lock.json もそうですし、一般的ではないですが、Static Linkでビルドする場合も考えられます。

trivyはREPOSITORY:TAGの形式でコンテナイメージを指定してスキャンするので、multi-stage buildにしてしまうと、builder側の脆弱性が放置されてしまいます。

そのため、builder側と、nginx側のコンテナイメージを独立させて、参照可能にしたいと思います。具体的には、Dockerfileを分けることで、それぞれのイメージにタグを付けます。
また、2回コマンド実行するのも微妙なのでmakefileを作成します。

※ もしも1つのDockerfileでmulti-stage buildをし、builder側のイメージ名を指定することができるのであれば、切実に情報がほしいです!

trivy用の構成

コードを見てもらえばわかりますが、Dockerfileをbuilder用(Dockerfile.builder)と、nginx用(Dockerfile.prod)に分けて、makefileは以下のようにしています。

Dockerfile.builder
FROM node:12.2.0-alpine as builder
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build
Dockerfile.prod
ARG tag
FROM local-builder:${tag} as builder

FROM nginx:1.16.0-alpine
ARG tag
COPY --from=builder /app/build /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
makefile
.PHONY: build all

TAG := $(shell git rev-parse HEAD)

all: build
build:
    docker build . -f Dockerfile.builder -t local-builder:$(TAG)
    docker build . --build-arg tag=$(TAG) -f Dockerfile.prod -t built:$(TAG)

このため、以下のようにデフォルトではコミットハッシュごとにコンテナを作成し、変数TAGが指定されていればそれを利用します。

$ make build  # 最新のコミットハッシュをコンテナのタグに利用
$ make build TAG=hoge # hogeをコンテナのタグに指定

実際のレポジトリでは、SPA用にnginx.confを置いたりしているので、必要な方はご確認ください。

1つのDockerfileで、同様のことができる方法をご存知の方いたら教えてください...

ビルドしたコンテナのテスト

では、さっそくビルドしたコンテナにアクセスしてみましょう。

$ make build TAG=test
$ docker run -p 8000:80 built:test

以上を実行し、 http://localhost:8000 を開くと、画面が表示されることが確認できました。

localhost.png

ついでに、nginxでSPA対応できているかを確認するため、 http://localhost:8000/spa/test が開けることを確認します。

localhost_spa

3. trivyでスキャン

では、builder側とnginx側をスキャンしましょう。
trivyはインストールしてある前提ですが、makefileに以下のコードを追加します。

buildと同じようにTAGを指定することで、指定したイメージをスキャンできるようになります。

ci-scan:
    trivy --exit-code 1 --severity HIGH,CRITICAL  --quiet --auto-refresh local-builder:$(TAG)
    trivy --exit-code 1 --quiet --auto-refresh built:$(TAG)

--exit-codeオプションで脆弱性を検知した場合の終了コードを指定できるので、スキャンはしたいけど、CIは止めたくない人は --exit-code 0 (デフォルトなので省略可) を利用しましょう。

4. CircleCIで実行

CircleCIの場合は以下のようにします。
trivyの脆弱性データの取得が重いので、脆弱性データなどはキャッシュに保存すると2回目以降のスキャンが高速になります。

version: 2
jobs:
  build:
    docker:
      - image: docker:18.09-git
    steps:
      - checkout
      - setup_remote_docker
      - restore_cache:
          key: vulnerability-db
      - run:
          name: Build image
          command: |
            apk add --update --no-cache make curl
            make build TAG=${CIRCLE_SHA1}
      - run:
          name: Install latest trivy
          command: |
            VERSION=$(
              curl -I https://github.com/knqyf263/trivy/releases/latest | \
              grep -o '/tag/v[0-9]\+.[0-9]\+\.[0-9]\+' | \
              sed -E 's:/tag/v([0-9\.]+):\1:'
            )
            wget https://github.com/knqyf263/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.tar.gz
            tar zxvf trivy_${VERSION}_Linux-64bit.tar.gz
            mv trivy /usr/local/bin
      - run:
          name: Scan local images with trivy
          command: |
            make ci-scan TAG=${CIRCLE_SHA1}
      - save_cache:
          key: vulnerability-db
          paths:
            - $HOME/.cache/trivy
workflows:
  version: 2
  release:
    jobs:
      - build

正常終了の場合、以下のようになります。
builder側とnginx側のどちらにも脆弱性がなかったということがわかりました。

ci-success.png

脆弱性があった場合CIを止めたい

脆弱性が見つかった場合に、CIを止めたい場合は--exit-code 1オプションを利用します。このオプションにより、脆弱性が見つかった場合、異常終了(終了コードが1)になります。
また、 severityオプションを利用することで、検知したい脆弱性の危険度を指定できます。

trivy --exit-code 1 --severity HIGH,CRITICAL \
  --quiet --auto-refresh \ 
  local-builder:$(TAG)

この状態で脆弱性のあるパッケージを追加して結果を見ると、CIが失敗することが確認できました!

ci-error.png
https://circleci.com/gh/tomoyamachi/trivy-with-react/8

trivyを使った運用スタイル

さて、では脆弱性が見つかった場合、どのようにすればよいでしょうか?

こうしたほうがいい、というのはありますが、それぞれの運用方針にそって決めればいいと思います。

以下に、「とりあえず導入」「ちょっと気になる」「がんばる」のコースとそれぞれによさそうな運用方針をまとめました。

とりあえず導入 ちょっと気になる がんばる
検知時のCI とめない(--exit-code 0) とめる(--exit-code 1) とめる(--exit-code 1)
対応方針 対応せずSlackなどに通知 バージョンアップして、駄目なら無視する (.trivyignore) 脆弱性を調べて、必要なら対応
検知タイミング ビルド時 ビルド時 ビルド時&定期実行

全く興味なくても、導入するだけ導入したい、という人がいてもいいと思うので、できる範囲ですすめられるといいかなと思います。

以下は、「とりあえず導入」以上の興味がある人に対しての方法論になります。

1. 脆弱性を調べ、本当にCRITICALかをチェック

まず、その脆弱性が自分のアプリケーションにとって本当に問題になるのかを理解しましょう。
NVDなどで脆弱性の内容を調べる方法もあるし、まわりの詳しい人に聞くのもありです。
自分で調べる場合、脆弱性が危険かどうかの判断は このスライドの「トリアージ」以降の章が役立つかと思います。

2. 重要ではない場合は、その脆弱性を無視する

プロジェクトに .trivyignore を作成し、無視したい脆弱性ID(以下、VULNERABILITY ID)を記載すると、そのIDを無視してくれます。
さきほどの例でいうと、以下のように指定すれば、それ以降同じVULNERABILITY IDは検知されなくなります。

.trivyignore
NSWG-ECO-328
CVE-2019-5428
CVE-2019-11358

この対応で先程停止していたワークフローがとおるようになったのがわかります。
https://circleci.com/gh/tomoyamachi/trivy-with-react/10

3. 重要な場合は...

重要な場合、バージョンアップをすれば良いと思うのですが、そうではない場合、自分で修正したコードを利用する方法などもあります。

アプリケーションパッケージの場合

たとえば、npmのパッケージであれば、 この記事の方法で でforkした自分のレポジトリを入れることが可能です。

ただし、その場合、バージョン情報が以下のようになり、そのパッケージの脆弱性チェックができなくなる点に留意ください。

"jquery": {
      "version": "git+https://<token>:x-oauth-basic@github.com/tomoyamachi/jquery.git#438b1a3e8a52d3e4efd8aba45498477038849c97",
      "from": "git+https://<token>:x-oauth-basic@github.com/tomoyamachi/jquery.git"
    },

OSのパッケージの場合

検出されるパッケージバージョン自体は変わらないので、patchfileを当てるコマンドを実行するのと同時に、.trivyignore に対象のVULNERABILITY IDを追加してください。

まとめ

今回は、以下のことを書きました。

  • フロントエンドをコンテナで運用する
  • trivyで脆弱性対策できる
  • 脆弱性のおおまかな運用方法

なお、コンテナになっていない場合は、Vulsという素晴らしい脆弱性スキャナがあるので、こちらをご利用ください。動作中のコンテナもスキャン可能です。
すこし導入に準備が必要ですが、あなたのサーバは本当に安全ですか?今もっともイケてる脆弱性検知ツールVulsを使ってみた を参考にお試しください。

謝辞

記事を書く際に、 @knqyf263 さん、 @sadayuki-matsuno さん、 @codehex さんにご意見をいただきました。ありがとうございました!!

以上です。

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

フロントエンドでもtrivyを使って脆弱性対策したい!

伝えたいこと

  • フロントエンドもコンテナ経由でアクセスさせよう
  • コンテナならtrivyを使ってヤバそうな脆弱性を知ることができる
  • trivyならCIに組み込むことも簡単
  • multi-stage buildの場合は工夫が必要
  • いまの状況でできる範囲の脆弱性対策からはじめよう

今回の記事でできること

trivy_flow.png

CI上で、本番用のフロントエンドのコンテナをmulti-satge buildでつくり、
trivyを利用して脆弱性を検知するところまでを目指します。

最後に、trivyを利用したレベル別の運用イメージも提案します。

導入

週末、trivyというコンテナ向けの脆弱性検知ツールが正式リリースされました。公開が5日目の5/21 12:00時点で 900star以上獲得しています。

trivyの詳細は原作者である @knqyf263 さんの「CIで使えるコンテナの脆弱性スキャナ」という記事を参照ください。

今回は、フロントエンドをメインに、今回は以下の内容を紹介します。

  • コンテナを使ってフロントエンドの本番環境を構築する
  • trivyを利用して、脆弱性が含まれていないかチェックする
  • trivyを通じた脆弱性の運用方法を学ぶ

僕も、普段はフロントエンドも担当するエンジニアで、セキュリティに対して体系だって学んだことはありません。ただ、trivyのコミッタでもあり、脆弱性の運用も少し知っているので、フロントエンドでもDevSecOpsを定着させるきっかけになるといいなと考えています。

ソースコード

今回使うコードは、すべて以下のレポジトリにあります。

https://github.com/tomoyamachi/trivy-with-react
reactrivy.png

この記事では触れませんが、開発環境でもコンテナ経由でホットリロードするような仕組みになっています。

実践 : プロジェクト作成~trivyの導入

ここからはプロジェクトの作成と、CIにtrivyを組み込むまでの手順を書きます

1. プロジェクトの作成

今回はCreate React Appを利用して、Reactのプロジェクトをまず作成します。
そして、デフォルトではパッケージ管理ツールにyarnが利用されているので、今回はプロジェクトのパッケージ管理をnpmで実行します。

$ npx create-react-app trivy-with-react
$ cd trivy-with-react
$ rm -fr node_modules yarn.lock
$ npm install

2. 本番用コンテナの作成

  1. 本番用のコードを生成
  2. nginxコンテナに生成したコードを移し、nginxを起動

という手順をとります。

通常であれば1ファイル

そして通常2つのステップは、multi-stage buildを利用して、1つのDockerfileでコンテナを作成します。

Dockerfile
# build environment
FROM node:12.2.0-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build

# production environment
FROM nginx:1.16.0-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

from Dockerizing a React App

1ファイルでmulti-stage buildの問題点

multi-stage buildは1つのDockerfileで最終成果物のimageだけを出力してくれるので、通常はベストプラクティスです。
その場合、以下のように、builder側のイメージがIMAGE IDでしか特定できない形で作成されているのがわかります。

$ docker images
REPOSITORY         TAG    IMAGE ID ...
trivy-with-react   test   96c4fdfac287 # nginx側
<none>             <none> 41fd97016d25 # builder側

しかし、今回、脆弱性はbuilder側のコンテナにもあることに気がついた方もいるでしょう。package-lock.json もそうですし、一般的ではないですが、Static Linkでビルドする場合も考えられます。

trivyはREPOSITORY:TAGの形式でコンテナイメージを指定してスキャンするので、multi-stage buildにしてしまうと、builder側の脆弱性が放置されてしまいます。

そのため、builder側と、nginx側のコンテナイメージを独立させて、参照可能にしたいと思います。具体的には、Dockerfileを分けることで、それぞれのイメージにタグを付けます。
また、2回コマンド実行するのも微妙なのでmakefileを作成します。

※ もしも1つのDockerfileでmulti-stage buildをし、builder側のイメージ名を指定することができるのであれば、切実に情報がほしいです!

trivy用の構成

コードを見てもらえばわかりますが、Dockerfileをbuilder用(Dockerfile.builder)と、nginx用(Dockerfile.prod)に分けて、makefileは以下のようにしています。

Dockerfile.builder
FROM node:12.2.0-alpine as builder
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build
Dockerfile.prod
ARG tag
FROM local-builder:${tag} as builder

FROM nginx:1.16.0-alpine
ARG tag
COPY --from=builder /app/build /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
makefile
.PHONY: build all

TAG := $(shell git rev-parse HEAD)

all: build
build:
    docker build . -f Dockerfile.builder -t local-builder:$(TAG)
    docker build . --build-arg tag=$(TAG) -f Dockerfile.prod -t built:$(TAG)

このため、以下のようにデフォルトではコミットハッシュごとにコンテナを作成し、変数TAGが指定されていればそれを利用します。

$ make build  # 最新のコミットハッシュをコンテナのタグに利用
$ make build TAG=hoge # hogeをコンテナのタグに指定

実際のレポジトリでは、SPA用にnginx.confを置いたりしているので、必要な方はご確認ください。

1つのDockerfileで、同様のことができる方法をご存知の方いたら教えてください...

ビルドしたコンテナのテスト

では、さっそくビルドしたコンテナにアクセスしてみましょう。

$ make build TAG=test
$ docker run -p 8000:80 built:test

以上を実行し、 http://localhost:8000 を開くと、画面が表示されることが確認できました。

localhost.png

ついでに、nginxでSPA対応できているかを確認するため、 http://localhost:8000/spa/test が開けることを確認します。

localhost_spa

3. trivyでスキャン

では、builder側とnginx側をスキャンしましょう。
trivyはインストールしてある前提ですが、makefileに以下のコードを追加します。

buildと同じようにTAGを指定することで、指定したイメージをスキャンできるようになります。

ci-scan:
    trivy --exit-code 1 --severity HIGH,CRITICAL  --quiet --auto-refresh local-builder:$(TAG)
    trivy --exit-code 1 --quiet --auto-refresh built:$(TAG)

--exit-codeオプションで脆弱性を検知した場合の終了コードを指定できるので、スキャンはしたいけど、CIは止めたくない人は --exit-code 0 (デフォルトなので省略可) を利用しましょう。

4. CircleCIで実行

CircleCIの場合は以下のようにします。
trivyの脆弱性データの取得が重いので、脆弱性データなどはキャッシュに保存すると2回目以降のスキャンが高速になります。

version: 2
jobs:
  build:
    docker:
      - image: docker:18.09-git
    steps:
      - checkout
      - setup_remote_docker
      - restore_cache:
          key: vulnerability-db
      - run:
          name: Build image
          command: |
            apk add --update --no-cache make curl
            make build TAG=${CIRCLE_SHA1}
      - run:
          name: Install latest trivy
          command: |
            VERSION=$(
              curl -I https://github.com/knqyf263/trivy/releases/latest | \
              grep -o '/tag/v[0-9]\+.[0-9]\+\.[0-9]\+' | \
              sed -E 's:/tag/v([0-9\.]+):\1:'
            )
            wget https://github.com/knqyf263/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.tar.gz
            tar zxvf trivy_${VERSION}_Linux-64bit.tar.gz
            mv trivy /usr/local/bin
      - run:
          name: Scan local images with trivy
          command: |
            make ci-scan TAG=${CIRCLE_SHA1}
      - save_cache:
          key: vulnerability-db
          paths:
            - $HOME/.cache/trivy
workflows:
  version: 2
  release:
    jobs:
      - build

正常終了の場合、以下のようになります。
builder側とnginx側のどちらにも脆弱性がなかったということがわかりました。

ci-success.png

脆弱性があった場合CIを止めたい

脆弱性が見つかった場合に、CIを止めたい場合は--exit-code 1オプションを利用します。このオプションにより、脆弱性が見つかった場合、異常終了(終了コードが1)になります。
また、 severityオプションを利用することで、検知したい脆弱性の危険度を指定できます。

trivy --exit-code 1 --severity HIGH,CRITICAL \
  --quiet --auto-refresh \ 
  local-builder:$(TAG)

この状態で脆弱性のあるパッケージを追加して結果を見ると、CIが失敗することが確認できました!

ci-error.png
https://circleci.com/gh/tomoyamachi/trivy-with-react/8

trivyを使った運用スタイル

さて、では脆弱性が見つかった場合、どのようにすればよいでしょうか?

こうしたほうがいい、というのはありますが、それぞれの運用方針にそって決めればいいと思います。

以下に、「とりあえず導入」「ちょっと気になる」「がんばる」のコースとそれぞれによさそうな運用方針をまとめました。

とりあえず導入 ちょっと気になる がんばる
検知時のCI とめない(--exit-code 0) とめる(--exit-code 1) とめる(--exit-code 1)
対応方針 対応せずSlackなどに通知 バージョンアップして、駄目なら無視する (.trivyignore) 脆弱性を調べて、必要なら対応
検知タイミング ビルド時 ビルド時 ビルド時&定期実行

全く興味なくても、導入するだけ導入したい、という人がいてもいいと思うので、できる範囲ですすめられるといいかなと思います。

以下は、「とりあえず導入」以上の興味がある人に対しての方法論になります。

1. 脆弱性を調べ、本当にCRITICALかをチェック

まず、その脆弱性が自分のアプリケーションにとって本当に問題になるのかを理解しましょう。
NVDなどで脆弱性の内容を調べる方法もあるし、まわりの詳しい人に聞くのもありです。
自分で調べる場合、脆弱性が危険かどうかの判断は このスライドの「トリアージ」以降の章が役立つかと思います。

2. 重要ではない場合は、その脆弱性を無視する

プロジェクトに .trivyignore を作成し、無視したい脆弱性ID(以下、VULNERABILITY ID)を記載すると、そのIDを無視してくれます。
さきほどの例でいうと、以下のように指定すれば、それ以降同じVULNERABILITY IDは検知されなくなります。

.trivyignore
NSWG-ECO-328
CVE-2019-5428
CVE-2019-11358

この対応で先程停止していたワークフローがとおるようになったのがわかります。
https://circleci.com/gh/tomoyamachi/trivy-with-react/10

3. 重要な場合は...

重要な場合、バージョンアップをすれば良いと思うのですが、そうではない場合、自分で修正したコードを利用する方法などもあります。

アプリケーションパッケージの場合

たとえば、npmのパッケージであれば、 この記事の方法で でforkした自分のレポジトリを入れることが可能です。

ただし、その場合、バージョン情報が以下のようになり、そのパッケージの脆弱性チェックができなくなる点に留意ください。

"jquery": {
      "version": "git+https://<token>:x-oauth-basic@github.com/tomoyamachi/jquery.git#438b1a3e8a52d3e4efd8aba45498477038849c97",
      "from": "git+https://<token>:x-oauth-basic@github.com/tomoyamachi/jquery.git"
    },

OSのパッケージの場合

検出されるパッケージバージョン自体は変わらないので、patchfileを当てるコマンドを実行するのと同時に、.trivyignore に対象のVULNERABILITY IDを追加してください。

まとめ

今回は、以下のことを書きました。

  • フロントエンドをコンテナで運用する
  • trivyで脆弱性対策できる
  • 脆弱性のおおまかな運用方法

ところで、 記事を書いているときにnpmを使うのであれば、npm auditで簡単に脆弱性を取得できることを知りました… が、環境側の脆弱性もチェックできるので、コンテナ化しているのであれば、trivyを使ってみてください。
S3などにホストしている場合は、npm auditだけでよさそうです。

なお、コンテナになっていない場合で実行環境をチェックしたい場合、Vulsという素晴らしい脆弱性スキャナがあるので、こちらをご利用ください。動作中のコンテナもスキャン可能です。
すこし導入に準備が必要ですが、あなたのサーバは本当に安全ですか?今もっともイケてる脆弱性検知ツールVulsを使ってみた を参考にお試しください。

謝辞

記事を書く際に、 @knqyf263 さん、 @sadayuki-matsuno さん、 @codehex さんにご意見をいただきました。ありがとうございました!!

以上です。

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

Vue 使いなら秒で覚えられる Svelte 入門

Svelteって何?

すごく速くていい感じの JS フレームワークです。
詳しい説明はこちらの記事をご参照ください。

当記事では Svelte の基本的な文法を Vue と比較しながら解説していきます。

Vueとの類似点

シンタックスはかなりVueに近いです。
以下ではVueの構成要素ごとに、Svelteではどう書けるかを解説します。
(使用バージョンはv3.4.2です)

data

Vueで言うところのdataはそのまま変数として定義すればいいだけです。

HTML内で表示するには波括弧1つで囲みます。

<h1>Hello {name}</h1>

<script>
  let name = 'world';
</script>

これで'Hello world'と表示されます。

またVueと同様に波括弧の中には JS 文を書くことができます。

<h1>Hello {name.toUpperCase()}</h1>
<!-- Hello WORLD と表示される -->

<script>
  let name = 'world';
</script>

v-bind

単方向バインドも波括弧で囲んだ中に入れるだけです。

<input type="text" value="{value}">

<script>
  let value = 'world';
</script>

また、属性名と変数名が等しい場合は次のような略記も可能です。

<input type="text" {value}>

<script>
  let value = 'world';
</script>

v-model

ここがVue使いにとっては少し紛らわしいのですが、
双方向バインディングの場合は属性にbind:を付けます。

<input type="text" bind:value="{value}">

<script>
  let value = 'world';
</script>

methods

dataに当たるものが普通の変数なので、methodsも普通の関数になります。

クリックイベントにバインドするにはon:clickディレクティブを用います。
他のイベントも同様です。

<button on:click="{handleClick}">ボタン</button>
ボタンは{count}回押されました。

<script>
  let count = 0
  const handleClick = () => {
    count += 1
  }
</script>

computed

computedは少しだけ独特な記法($:)を用います。
$:の後に書かれた文は、その文の中で参照された変数が変化するたびに再実行されます。

$: let computedProp = prop * 2

つまり上の例では、propが変化するたびに、computedPropertyprop * 2が代入されるようになっています。まさにcomputedですね。

全体の例も置いておきます。
この例ではisEvencountから算出されるようになっています。

<button on:click="{handleClick}">ボタン</button>
あなたはボタンを{isEven ? '偶数' : '奇数'}回押しています。

<script>
  let count = 0
  // computed property
  $: isEven = count % 2 === 0

  const handleClick = () => {
    count += 1
  }
</script>

また、computedでは複数行の処理を書くことがよくあると思いますが、
そういう場合波括弧で囲んでブロックを作ればOKです。

  let count = 0
  let double = 0    
  // computed: double
  $: {
    console.log(count)
    double = count * 2
  }

あるいは関数を作ってそこで処理を行ってもいいです。

  let count = 0;
  let double = 0;

  $: computeDouble(count)

  const computeDouble = (count) => {
    console.log(count)
    double = count * 2
  }

ちなみに$:記法は一見へんてこに見えますが、実は正当なJavaScriptです(label文)。

watch

実はwatchも先ほどの$:記法で書くことができます。
実際computedwatchも「データに変化があるたびに何かを行う」という点で共通していますね。

<button on:click="{handleClick}">ボタン</button>
count:  {count}
count2: {count2}

<script>
  let count = 0
  let count2 = 0
  // countが変化するたびcount2をインクリメント
  $: incrementCount2(count)

  const handleClick = () => {
    count += 1
  }

  const IncrementCount2 = () => {
    if(count > 0) count2 += 1
  }
</script>

この例では、countが変化するたびにincrementCount2()が呼ばれ、count21増加するようになっています。

その他

非常に単純なので説明は割愛しますが v-htmlv-ifv-for に当たるシンタックスも用意されています。

リンク先は英語ですが、英語嫌いの方でもコードを見れば一発で理解できるかと思います。

終わりに

以上 Vue の基本的な構文に沿ってざっと説明してきました。

かなり Vue に似た構文で、更に簡潔に書ける部分もあることがわかっていただけたかと思います。

次の JS フレームワークの潮流になるかどうかはわかりませんが、なった場合 Vue 使いはちょっとだけ有利かもしれませんね。

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

Vue使いなら秒で覚えられるSvelte入門

Svelteって何?

すごく速くていい感じの JS フレームワークです。
詳しい説明はこちらの記事をご参照ください。

当記事では Svelte の基本的な文法を Vue と比較しながら解説していきます。

Vueとの比較

以下では Vue の構成要素ごとに、Svelteではどう書けるかを解説します。
コードは全てREPLでサクッと試すことができます。
(Svelteのバージョン:v3.4.2)

data

Vueで言うところのdataはそのまま変数として宣言すればいいだけです。

HTML内で表示するには波括弧1つで囲みます。

<h1>Hello {name}</h1>

<script>
  let name = 'world';
</script>

これで'Hello world'と表示されます。

またVueと同様に波括弧の中には JS 文を書くことができます。

<h1>Hello {name.toUpperCase()}</h1>
<!-- Hello WORLD と表示される -->

<script>
  let name = 'world';
</script>

v-bind

単方向バインドも波括弧で囲んだ中に入れるだけです。

<input type="text" value="{value}">

<script>
  let value = 'world';
</script>

また、属性名と変数名が等しい場合は次のような略記も可能です。

<input type="text" {value}>

<script>
  let value = 'world';
</script>

v-model

ここが Vue 使いにとっては少し紛らわしいのですが、
双方向バインディングの場合は属性にbind:を付けます。

<input type="text" bind:value="{value}">

<script>
  let value = 'world';
</script>

methods

dataに当たるものが普通の変数なのでmethodsも普通の関数になります。

クリックイベントにバインドするにはon:clickディレクティブを用います。
他のイベントも同様です。

<button on:click="{handleClick}">ボタン</button>
ボタンは{count}回押されました。

<script>
  let count = 0
  const handleClick = () => {
    count += 1
  }
</script>

computed

computedは少しだけ独特な記法$:を用います。
$:の後に書かれた文は、その文の中で参照された変数が変化するたびに再実行されます。

$: let computedProp = prop * 2

つまり上の例では、propが変化するたびに、computedPropprop * 2が代入されるようになっています。

全体の例も置いておきます。
この例ではisEvencountから算出されるようになっています。

<button on:click="{handleClick}">ボタン</button>
あなたはボタンを{isEven ? '偶数' : '奇数'}回押しています。

<script>
  let count = 0
  // computed property
  $: isEven = count % 2 === 0

  const handleClick = () => {
    count += 1
  }
</script>

また、computedでは複数行の処理を書くことがよくあると思いますが、
そういう場合波括弧で囲んでブロックを作ればOKです。

  let count = 0
  let double = 0    
  // computed: double
  $: {
    console.log(count)
    double = count * 2
  }

あるいは関数を作ってそこで処理を行ってもいいです。

  let count = 0;
  let double = 0;

  $: computeDouble(count)

  const computeDouble = (count) => {
    console.log(count)
    double = count * 2
  }

ちなみに$:記法は一見へんてこに見えますが、実は正当なJavaScriptです(label文)。

watch

実はwatchも先ほどの$:記法で書くことができます。
実際computedwatchも「データに変化があるたびに何かを行う」という点で共通していますね。

<button on:click="{handleClick}">ボタン</button>
count:  {count}
count2: {count2}

<script>
  let count = 0
  let count2 = 0
  // countが変化するたびcount2をインクリメント
  $: incrementCount2(count)

  const handleClick = () => {
    count += 1
  }

  const IncrementCount2 = () => {
    if(count > 0) count2 += 1
  }
</script>

この例では、countが変化するたびにincrementCount2()が呼ばれ、count21増加するようになっています。

その他

非常に単純なので説明は割愛しますが v-htmlv-ifv-for に当たるシンタックスも用意されています。

リンク先は英語ですが、英語嫌いの方でもコードを見れば一発で理解できるかと思います。

終わりに

以上 Vue の基本的な構文に沿ってざっと説明してきました。

かなり Vue に似た構文で、更に簡潔に書ける部分もあることがわかっていただけたかと思います。

次の JS フレームワークの潮流になるかどうかはわかりませんが、なった場合 でも Vue 使いなら簡単に移行できそうです。

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

Safariの<input type="file">で問題が繰り返し起きるときの対応

はじめに

ファイルアップロードは、かなり鉄板機能なので実装することが多いのですが、ハマりかけたのでログに残しておきます。

エラーまでの経緯

ファイルアップロードのソースは、雰囲気こんな感じです。

inputタグをそのまま使うとダサいので、style="display: none"にして、その代わりにボタンやフォームのクリックイベントでinputタグのクリックイベントを発火します。

<template>
  <!-- 上は省略 -->
  <div class="text-xs-center">
    <img :src="imageUrl" height="200" v-if="imageUrl"/>
    <v-text-field color="primary" label="Select Image" @click='pickFile' v-model='imageName' prepend-icon='attach_file'></v-text-field>
    <input
     type="file"
     style="display: none"
     ref="image"
     accept="image/*"
     @change="onFilePicked"
    >
  </div>
  <!-- 下も省略 -->
</template>

<script>
export default {
  data () {
    return {
      imageName: '',
      contentType: '',
      imageUrl: '',
      imageFile: ''
    }
  },
  methods : {
    pickFile () {
      this.$refs.image.click ()
    },
    onFilePicked (e) {
      const files = e.target.files

      if(files[0] !== undefined) {
        this.imageName = files[0].name
        this.contentType = files[0].type  // contentType: image/jpegとか
        if(this.imageName.lastIndexOf('.') <= 0) { return }

        const fr = new FileReader ()
        fr.readAsDataURL(files[0])
        fr.addEventListener('load', () => {
          this.imageUrl = fr.result
          this.imageFile = files[0] // this is an image file that can be sent to server...
        })

      } else {
        this.imageName = ''
        this.imageFile = ''
        this.imageUrl = ''
      }
    }
  },
  /** いろいろ省略 **/
}

</script>

開発が一段落し、自分のiPhoneでぽちぽちやっていると、こんな現象に遭遇しました。

問題が起きたため、このWebページが再読込されました。
XXXXXXXXで問題が繰り返し起きました。

調べた

Safariでは、display:noneに問題があるらしいです。なので、『inputタグはdisplay:noneではなく、divで囲んでheightとwidthを0にせよ』とのことでした。

<template>
  <!-- 上は省略 -->
  <div class="text-xs-center">
    <img :src="imageUrl" height="200" v-if="imageUrl"/>
    <v-text-field color="primary" label="Select Image" @click='pickFile' v-model='imageName' prepend-icon='attach_file'></v-text-field>
    <div class="hiddenfile"> <!-- ←これで囲む -->
      <input
       type="file"
       ref="image"
       accept="image/*"
       @change="onFilePicked"
      >
    </div>
  </div>
  <!-- 下も省略 -->
</template>

<script>
/** 変更なし **/

</script>
<style scoped>
.hiddenfile {
  width: 0px;
  height: 0px;
  overflow: hidden;
}
</style>

う〜ん何故だろう、、、
むかしからdisplay:noneが常識かつ定石だと思っていましたら、Safariさんは違ったようです。

めでたし。

参考記事

Safari input='file' doesn't work

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

JWF(JavaScript Window Framework)で開発する、SPA(SinglePageApplication)開発

JWF(JavaScript Window Framework)で開発する、SPA(SinglePageApplication)開発

0. 動作画面

スクリーンショット

1. そもそもSPAって何?

 某雑誌の名前ではありません。Web開発においてHTMLデータをリロードせず、ページ内のDOMを操作しUIを構築する作り方です。

  • SPAの利点
    • ページの更新待ちが発生しない
    • 応答が早い
    • データのやりとりが最小限で済む
  • SPAの欠点
    • 開発方法に慣れるまでキツい
    • 覚えることが沢山ある

 その他の欠点でJavaScriptを読み込むので、初期表示までに時間がかかると言われていますが、それは組み方が悪いか、使っているフレームワークがお大便なだけなのです。HTTP2のServerPushを使えば、初期HTMLファイルと同時に.jsを送りつけることも可能です。さらにはページに表示する構造が複雑になってくると、HTMLで組まれたテキストの字句解析の時間より、JavaScriptの構文解析とプログラムによるDOM生成の方が速度が早くなります。

2.SPAの基本、ジークジオン

 最も重要なのはJavaScriptによるDOM操作です。ついにJavaScriptでジオンのモビルスーツの操作ができるようになったとか思わないでください。もしかしたらJavaScriptで動いている可能性はゼロではありませんが、その話ではありません。サブタイトルが間違っています。今回はDocument Object Modelの事です。HTMLの中の構造をオブジェクトとして操作していきます。

 下手なフレームワークに触れる前に、このDOM操作には慣れてください。手習いとしては、「document.querySelectorで拾ってきたHTMLElementに適当な操作をする」と言うのを繰り返すのをオススメします。jQueryとかは基礎の理解を阻害するので使わない方が良いです。到達ラインとしては、JavaScriptのコードで、表示したいものを表示したい位置にきっちり吐き出せるレベルが理想です。

3.「下積みなんてやってられないよ!」というためのフレームワーク

 SPAを扱うためのフロントエンドフレームワークは有名どころがいくつかあります。それらに私は馴染めませんでした。HTMLやそれに近いもの記述した後に、そこに割り込んでいくスタイルが好きになれないのです。

 理想の設計思想としては、ほとんどの部分をJavaScriptのコードで完結させることです。初期ページ以外でHTMLを一切書かない、そしてそれを不便な状況としない、それが理想的な状態です。フレームワーク自体も、基本的にJavaScriptでDOM操作を行っているので、HTMLをテキストで記述している部分は皆無です。

4.JavaScript-Window-Frameworkの使い方。

 ということで、JavaScript-Window-Frameworkの紹介をしていこうと思います。

 JavaScript-Window-Frameworkは、SPAでフロントエンド開発をするためのフレームワークです。一つのコンポーネントをウインドウという単位で管理します。ウインドウは特定の場所に貼り付けたり、フレームを作って可動式の仮想ウインドウとすることも出来ます。ブラウザのクライアント領域全体に展開することも可能です。

 以下、開発環境が整いやすいようにnpm版を紹介します。

4.1 開発環境の構築とサンプルテンプレート

 Node.jsが入っていてnpmが使える状態が前提です。使い方を確認するための環境を構築するコマンドが入っています。

  • パッケージファイルの作成
npm -y init
  • JavaScript-Window-Frameworkのインストール
npm -D i javascript-window-framework
  • サンプルテンプレートの作成(WebPackの設定ファイルなどを自分で作る場合は不要)
npx init-jwf
  • WebPackがらみで使いそうなモジュールを一通りインストール(自分で必要なものを設定するなら不要)
npm -D i typescript dts-bundle ts-loader node-sass style-loader sass-loader css-loader url-loader source-map-loader webpack webpack-cli webpack-dev-server
  • サンプルのビルド
npx webpack

dist/index.html をブラウザで開く
  • webpack-dev-serverを利用する場合
npx webpack-dev-server

http://localhost:8080/ をブラウザで開く

4.2 サンプルの紹介

 動作が分かりやすいように全てフレームウインドウ化してありますが、ブラウザのクライアント領域全体に展開することも出来ます。あとはあまりオススメしませんが、HTMLを普通に記述して、特定のノードの中に貼り付けることも出来ます。

  • [1] ウインドウを表示するだけのサンプル
     フレームウインドウはデフォルトでサイズ変更とドラッグによる移動機能を持っています
     モバイル系のブラウザでも操作可能です
function Sample001() {
    const win = new JWF.FrameWindow()           //フレームウインドウの作成
    win.setTitle('Sample01 ウインドウを表示')   //タイトルの設定
    win.setPos()                                //位置を中心に設定
}

image.png

  • [2] ウインドウの位置とサイズとイベントの扱い
     サイズ変更やウインドウで発生したイベントのトラップが可能です
function Sample002() {
    const win = new JWF.FrameWindow()   //フレームウインドウの作成
    win.setTitle('Sample02 位置サイズ指定')  //タイトルの設定
    win.setSize(100, 100)               //サイズの変更
    win.setPos(10, 10)                  //位置指定
    const client = win.getClient()      //クライアントノードの取得
    win.addEventListener('active', (e) => {
        client.innerText = e.active ? 'アクティブになった' : '非アクティブになった'
    })
}

image.png

  • [3] リストビューの使い方
     WindowsのListViewに似せて作っています
     ヘッダクリックによるソート、ヘッダ枠のリサイズ、縦横スクロール時のヘッダの固定など
     実は有名どころのフロントエンドフレームワークでこれを実装しているものはありません
     実際にこれをDOM操作で作ると、とんでもなく面倒くさいんです
function Sample003() {
    const listView = new JWF.ListView({ frame: true })  //リストビューをフレーム付きで作成
    listView.setTitle('Sample03 リストビュー')        //タイトルの設定
    listView.addHeader(['番号', '名前', '攻撃力'])   //ヘッダの作成
    listView.addItem([1, '竹槍', 5])              //アイテムの設定
    listView.addItem([2, '棍棒', 10])
    listView.addItem([3, '銅の剣', 10])
    listView.setPos(30, 30)                         //位置指定

    listView.addEventListener('itemClick', (e) => { //アイテムクリック処理
        const name = listView.getItemText(e.itemIndex, 1)   //リストビューからデータを取り出す
        new JWF.MessageBox('メッセージ', name + "が選択された")  //メッセージボックスの表示
    })
}

image.png

  • [4] ツリービューの使い方
     ツリービューは比較的簡単に実装可能なので、比較的よく見かけます
function Sample004() {
    //ツリービューの作成
    let treeView = new JWF.TreeView({ frame: true })
    treeView.setTitle('Sample04 ツリービュー')        //タイトルの設定
    //サイズ設定
    treeView.setSize(300, 300)
    //ルートアイテムに対して名前の設定
    treeView.getRootItem().setItemText('ルートアイテム')

    //アイテムを作成
    let item:JWF.TreeItem
    item = treeView.addItem('アイテム1')
    item.addItem('アイテム1-1')
    item.addItem('アイテム1-2')
    item = treeView.addItem('アイテム2')
    item.addItem('アイテム2-1')
    item.addItem('アイテム2-2')
    item.addItem

    //アイテムが選択された場合のイベント
    treeView.addEventListener('itemSelect', function (e) {
        const name = e.item.getItemText()   //リストビューからデータを取り出す
        new JWF.MessageBox('メッセージ', name + "が選択された")  //メッセージボックスの表示
    })
}

image.png

  • [5] 可動式分割バーのサンプル
     Webの過渡期にframesetというのがありましたが、動きとしては同じような動作をします  幅か足りなくなると、自動的に引っ込みます
function Sample005() {
    const frame = new JWF.FrameWindow()
    frame.setTitle('Sample005 分割ウインドウ')   //タイトル設定

    const splitter = new JWF.Splitter()     //分割バーの作成
    frame.addChild(splitter, 'client')      //分割バーをフレームウインドウに乗せる

    const tree = new JWF.TreeView()         //ツリービューの作成
    const list = new JWF.ListView()         //リストビューの作成

    splitter.addChild(0, tree, 'client')    //splitterの分割領域0にtreeを追加
    splitter.addChild(1, list, 'client')    //splitterの分割領域1にlistを追加

    //分割バーの分割サイズと方向設定(WestEast、左右)
    //weは左が領域0、右が領域1
    //nsにすると上下分割も可能
    splitter.setSplitterPos(200, 'we')
    //表示領域が300を切ると、動的なオーバーレイ表示にする
    splitter.setOverlay(true, 300)

    //treeにアイテムを追加
    tree.getRootItem().setItemText('最上位アイテム')
    for (let j = 0; j < 5; j++) {
        let item = tree.addItem("アイテム" + j, true)
        for (let i = 0; i < 5; i++)
            item.addItem("サブアイテム" + j + "-" + i, false)
    }
    //アイテムが選択された場合のイベント
    tree.addEventListener('itemSelect', (e)=> {
        const value = e.item.getItemText()
        if (value) {
            const no = list.getItemCount()
            const date = (new Date()).toLocaleString()
            list.addItem([no.toString(), value, date])
        }
    })

    //listにヘッダを追加
    list.addHeader(['番号', ['名前', 200], '時刻'])

    //位置とサイズの設定
    frame.setSize(800, 600)
    frame.setPos()

}

image.png image.png

5.HTMLを切り離し、DOMの操作を最小限に

 Web技術はHTMLの進化とともに発展してきました。今となってはGUIを構築する上での主要な選択肢となっています。ただしSPAにおいては、HTMLをベタ書きしていくと、その取り扱いに対して足を引っ張られることが多々あります。できるだけメインプログラムからは切り離して扱う方が、見通しも良くなりプログラムの扱いも簡単になります。ということで、できる限り分離した結果がこのフレームワークです。

6.TypeScriptに特化

 フレームワークを利用するプログラムは素のJavaScriptでも書くことが出来ますが、フレームワーク自体はTypeScriptで書いています。利点は型定義ファイル.d.tsが簡単に作れるからです。入力補完やドキュメントの表示が、開発環境から利用できるこの利点は途轍もなく大きいのです。敗北者にならないように、strictを付けてコンパイルをかけています。ただしasとanyは結構使っています。

7.ソースコード類

8.デザインをなんとかしたい

 一人で作っている限界です。私にデザインセンスがないので、見た目が微妙なのです。こればっかりはどうにもなりません。想像したとおりのものを作ることは出来ても、想像の段階で微妙なのだから救いようがありません。ということで、こういう感じにした方が良いとか、こういう機能があると便利とかいうご意見があったら、お待ちしております。

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

Youtube動画を音声でコントロールするためのChrome拡張が生まれた。

生まれしもの

  • Youtube再生中に
    • chrome拡張のアイコンをクリックすれば音声認識を開始し、
    • 「待って」と言えば一時停止をし、
    • 「いいよ」と言えば再生をし、
    • 「◯秒戻って」と言えば◯秒巻き戻して再生し、
    • 「◯秒進んで」と言えば◯秒早送りして再生し
    • 「終わり」と言えば音声認識を終了する

そんなchrome拡張がこの世に生まれました。

経緯

  • 「ほぇえ。Youtubeって色んな技術のサンプル実装動画あるんやなぁ」
  • 「動画見ながら写経して新しい技術触ってみるかあ」
  • 「ブラウザとエディタを2分割して両方画面に表示して、Youtube再生して写経開始っと・・・」
  • 「ちょww 解説早いww しかもww タイピングも早いw 待ってw 今どこwww」
  • 「巻き戻さなきゃ・・・。あぁ行き過ぎた・・・。うわああああシークバー動かすのめんどくせえええ」
  • 「うーむ、なんとかして写経の効率上がらないかなぁ」
  • 「SiriやらAlexaみたいに音声でコントロールできたら写経やりやすくなるのでは?」

という経緯で、「トラックパッド・マウス操作不要、音声でYoutubeをコントロールできる機能」の開発に着手しました。

最初はYoutube API使って自分のWebサイトに動画埋め込んで、音声認識開始・停止のボタンを用意して・・・みたいなことを考えたんですが、よく考えたらChromeの拡張機能でよくね?となったので、初めてですがChrome-extensionで作ることに。

成果物

作成にあたっては下記記事を大いに参考にさせていただきました。 ありがとうございます?
Chrome拡張の作り方 (超概要)
ブラウザで音声操作をする。(Speech Recognition API)

ファイルはこれだけです。本当にシンプル。Chrome拡張作ったの初めてですが、単純な機能でよければすごく簡単に作れますね。
JQueryはCDN使っても?‍♂️

manifest.json
background.js
content.js
jquery.min.js
manifest.json
{
    "manifest_version": 2,
    "name": "Youtube Voice Controller",
    "description": "Youtube動画を音声で操作します",
    "version": "1.0.0",
    "content_scripts": [{
        "matches": ["<all_urls>"],
        "js": ["content.js","jquery.min.js"]
    }],
    "background": {
        "scripts": ["background.js"]
    },
    "browser_action": {
        "default_title": "Youtubeを音声で操作します"
  },
    "permissions": [
        "tabs",
        "background",
        "http://*/*",
        "https://*/*"
    ]
}

background.js
chrome.browserAction.onClicked.addListener(function(tab) {
    chrome.tabs.sendMessage(tab.id, "Action");
});
content.js
SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
recognition = new SpeechRecognition();
recognition.lang = 'ja-JP';
recognition.continuous = true;
recognition.maxAlternatives = 1;
var hasSpeechRecognizeStarted = false;
var userAction = false;

chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
    if (request == "Action") {
      userAction = true;
      if(hasSpeechRecognizeStarted) {
        rec_stop();
      } else {
        rec_start();
      }
    }
});

function hasSpeeched(speech, action) {
  return speech.indexOf(action) > -1;
}

function rec_start() {
  recognition.start();
  hasSpeechRecognizeStarted = true;
}

function rec_stop(){
  recognition.stop();
  hasSpeechRecognizeStarted = false;
}

function getSecondBy(speech, action) {
    switch (action) {
        case "戻って":
            return parseInt(speech.replace('秒戻って', ''), 10);
            break;
        case "進んで":
            return parseInt(speech.replace('秒進んで', ''), 10);
            break;
        default:
            break;
    }
}

recognition.onresult = (event) => {
  var transcript = event.results[event.resultIndex][0].transcript;

  if(hasSpeeched(transcript, "いいよ")){
    $('video')[0].play();
  }

  if(hasSpeeched(transcript, "待って")){
    $('video')[0].pause();
  }

  if(hasSpeeched(transcript, "戻って")){
    var second = getSecondBy(transcript, "戻って");
    $('video')[0].currentTime -= second;
    if($('video')[0].paused) {
      $('video')[0].play();
    }
  }

  if(hasSpeeched(transcript, "進んで")) {
    var second = getSecondBy(transcript, "進んで");
    $('video')[0].currentTime += second;
    if($('video')[0].paused) {
      $('video')[0].play();
    }
  }

  if(hasSpeeched(transcript, "終わり")){
    userAction = true;
    rec_stop();
  }
}

recognition.onend = (event) => {
  if(!userAction) {
    rec_start();
  }
}

やってることは至極簡単なんですが、ほんのちょっと手を加えたところは以下。

  • 音声認識を開始してしばらく沈黙していると音声認識受付を終了してしまう
    • マイクの受付終了時はonendでイベントを取得できるので、ユーザーが能動的に止めた(userActionがtrue)でなければ再度音声認識を開始する

どうなったか

  • 「ブラウザとエディタを2分割して両方表示して、Youtube再生して写経開始っと・・・」
  • 「ちょww 英語www 解説早いww しかもww タイピングも早いw 待ってw 今どこwww」
  • 「よし、ちょっと戻そう。『30秒戻って』」
  • 「あー、なるほどね そう書き方なのね 理解理解」
  • 「動画進めよう。『いいよ』」
  • 「あー、そこもうちょっとじっくり見たい! 『止まって』」
  • 「あー、なるほどね そう書き方なのね 理解理解」

という感じで今までキーボードから手を離してトラックパットを使ってシークバーを動かして位置調整をして・・・という煩わしい手順から解放されて写経の効率が著しく向上しました(当社比)。

拡張の拡張をするなら

自分が使えればいいや精神で最低限の機能しか実装しませんでしたが、今後もう少し使いやすくするためにはこんな機能をつけたいなあと思っています

  • 音声コマンドをユーザーが独自に設定できるようにする
  • ◯秒戻るのか◯分戻るのかを設定可能もしくはそれぞれを認識可能にする
  • 音声認識中を視覚的に表示する
    • 発生してから数秒ラグがあるので認識されてるかどうか分かりづらい

おまけ

最近よく見ている技術系チャンネルをあげておきます。
ドットインストールで色々写経したけどもう少し踏み込んだものをやりたい!というような人には結構おすすめです。

  • Red Stapler
    • CSSを使ったデザインなど主にフロント系のチュートリアル動画が多い印象。なんかいいデザインないかなあとよく動画を漁ります
  • Code Inspire
    • Veu.jsとFirebaseとStripe使ってECサイトを作る、などフレームワークを使って特定のものを開発するチュートリアル系動画が多い。目的がはっきりしてるのでとっかかりやすい。個人的な感想だが連載物の動画の場合、サムネイルと動画タイトルの文頭に「第何回目なのか」が明記されているのは良動画が多い。
  • Traversy Media
    • JSのフレームワークやWeb Developmentに必要なHTML, CSSをCrash Course(短期集中)で学べるチャンネル。一本一本が1時間超えていたり長い動画が多い印象。更新頻度が高い。
  • The Net Ninja
    • チュートリアルがほとんど。色んな技術のチュートリアルが上がっているので新しく触りたいものがあれば検索してみると結構出てくる。
  • Engineer Man
    • ここもチュートリアルがほとんど。サムネイルのフォントが好きじゃないという極めて個人的な理由であまり見ていない。扱っている技術は幅広いので漁ってみるとよいかも。
  • Online Tutorials
    • CSSのデザインチュートリアルがメインのチャンネル。出来上がったデザインをサムネイルでバン!と見せてくれるのがありがたい。毎日見ることは少ないけど、Webサイト作ってておしゃれなデザインに困った時に動画リストをさーっと見て、いい感じのサムネイルのものがあったら写経する、という感じで使ってます。
  • Le Wagon
    • コーディングブートキャンプを提供しているLe Wagonの(おそらく)公式Youtubeチャンネル。コーディング以外の動画も結構あるがワークショップ動画が結構あり、しかもUI、UXデザイン関連のものもあるのがいい。Youtubeのタイムラインに気になる動画があればとりあえず「あとで見る」に保存しておく系チャンネルとして利用。
  • The Coding Train
    • 個人的にめちゃくちゃおすすめのチャンネル。愉快なおじさんが愉快にライブコーディングする動画がよく上がっている。こんなに愉快にコーディングする人を見たことがないくらいめちゃくちゃ楽しそうにコーディングする。アルゴリズムや考え方をホワイトボードを使って説明したりしてくれるので理解しやすい。使用されているのはほとんどがJSのフレームワークを使ったものなので、今のところ個人的に実務で使えるものは正直あまりないが、愉快なおじさんが愉快にコーディングしているのを見ているだけでも楽しい。
  • Lets Build That App
    • これだけ少し毛色が違って、ほとんどがSwiftを使ったXcodeでのコーディング動画。よく見かけるiOSのデザインを分かりやすく解説してくれる。成果物が具体的なので写経した後に個人的にカスタマイズして使ったりしています。

これでもまだ半分くらいなので、別記事でおすすめのチャンネルをまとめたいと思います。

終わりに

  • Chrome拡張はやってみると簡単でとっかかりやすい
  • ないものは自分で作ってみる精神でよいハックライフを!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む