20190330のJavaScriptに関する記事は21件です。

【JavaScript】変数と定数の宣言(var, let, constの違い)【自己学習】

letとconst

JavaScriptでは変数の宣言は従来よりvarが使われていたが、ES2015よりletが追加された。また、同じくES2015よりconstを用いた定数の宣言も可能になった。

varとletの違い

変数を宣言できる点において共通しているvarとletではあるが、下記の点において2つは異なる。

varは宣言を省略できる

variable-1.js
greet = "Hello World!";  //変数の宣言を省略
console.log(greet);  // > Hello World!

1行目において変数の宣言は省略されているが、greetに値が代入された時点で暗黙的にvarによる宣言がされたとみなされている。だが、省略した場合はすべてグローバル変数になるという性質があるため、省略は避けたほうが良い。

variable-2.js
scope = "global";

function value() {
  scope = "local";  //グローバル変数として更新
  console.log(scope);
}

value();  // > local
console.log(scope);  // > local

関数内での変数の宣言は本来ローカル変数として行われるが、上記のコードではvarを省略したため、グローバル変数の更新とみなされている。

そのため、関数内外どちらの変数scopeを参照してもlocalという値が出力されることになる。

letは変数名の重複が許可されない

varとは異なり、letは同じ名前の変数の宣言が許可されない。

variable-3.js
var value1 = 1;
var value1 = 2;  //変数valueの値の更新

console.log(value1);  // > 2

let value2 = 1;
let value2 = 2;  //同名の変数のため宣言が許可されない

console.log(value2);  // > エラー

varでは同名の変数の宣言は許可され、変数valueの値が2に更新される。

一方letの場合は許可されず、参照すると「変数valueは既に宣言されています」という内容のエラーメッセージが表示される。

letはブロックスコープを認識する

従来のJavaScriptでは、他の言語と異なりブロックスコープは存在しなかったが、letが導入されたことでブロックスコープを認識できるようになった。

variable-4.js
if (true) {
  var value1 = 1;  //ブロックスコープは認識されていない 
}

console.log(value1); // > 1


if (true) {
  let value2 = 2;  //ブロックスコープは認識されている
}

console.log(value2); // > エラー

ブロック内でvarを使って変数の宣言を行った場合、ブロックスコープは認識されず、value1はコード全体で参照可能となるため、1が出力される。

それに対してletを使った場合は、ブロックスコープは認識され、value2の参照できる範囲はブロック内のみとなるため、グローバル変数として参照するとエラーが表示される。

constの特徴

値の更新ができない

constを使って定数を宣言した場合、同名の定数を宣言できないだけでなく、定数内の値も更新できなくなる

constant-1.js
const value1 = 1;
const value1 = 2;  //同名の定数の宣言はできない(letと同じ)

console.log(value1);  // > エラー

const value2 = 1;
value2 = 2;  //値の更新もできない

console.log(value2);  // > エラー

定数として代入された値は意図せず更新されることがなくなることから、定数を使うことで不具合等を防ぐことにつながる。

ブロックスコープは認識する

constant-2.js
if (true) {
  const value = 1; //ブロックスコープは認識されている
}

console.log(value); // > エラー

ブロック内でconstを使って定数の宣言を行った場合、ブロックスコープは認識される(letと同様)。

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

NodeでES modulesからCommon JSを使う、たったひとつ?の全く冴えないやりかた(2019年版)

先日ようやく拙作のnpmパッケージ(1)(2)(3)をnodeの --experimental-modules フラグ付きでの直接実行(babelやwebpackによる前処理無し)に対応させたので、その方法をご紹介いたします。

まずは復習

ES Modules (以下、ESM)では、Common JS (以下、CJS)と異なり、他のファイル上の関数や値にアクセスするためには、import / export構文を用います。

CJS

foo.js(CJS)
// 複数の関数・値を名前付きでエクスポートする場合
exports.bar = function(x) {
    return x * x;
}

// 一つの関数・値をエクスポートする場合
// module.exports = function(x) {
//     return x * x;
// }
index.js(CJS)
// 複数の関数・値が名前付きでエクスポートされている場合
const foo = require('./foo');
console.log(foo.bar(3)); // 9

// 一つの関数・値がエクスポートされている場合
// const bar = require('foo');
// console.log(bar(3)); // 9

ESM

foo.mjs(ESM)
// 複数の関数・値を名前付きでエクスポートする場合
export function bar(x) {
    return x * x;
}

// 一つの関数・値をエクスポートする場合 (デフォルトエクスポート)
// export default function bar(x) {
//     return x * x;
// }
index.mjs(ESM)
// 複数の関数・値を名前付きでエクスポートする場合
import * as foo from './foo';
console.log(foo.bar(3)); // 9

// 一つの関数・値がエクスポートされている場合 (デフォルトインポート=デフォルトエクスポートのインポート)
// import bar from 'foo';
// console.log(bar(3)); // 9

ESMのインポートにおける種類

// namespace import
import * as foo from 'foo';

// named import
import { bar, baz } from 'foo';

// default import
import bar from 'foo';

で、問題は何?

ESMからCJSをインポートする際の動作が歴史的な経緯によって統一されておらず、複数のターゲット(Web app(Webpack)/Node app(Webpack)/APIとしてnpm package(そのまま)/...)を考慮すると破綻をきたします。

Node(--experimental-modules .mjs)の場合

Nodeは、ESMからCJSをインポートする方法をデフォルトインポートのみとしています。
といっても、現時点の挙動(node 11)では、インポート時にエラーになるのではなく、CJSの中身がdefaultに包まれます。
つまり、デフォルトインポートしていないと、いざ使用する際に undefined is not a function などというエラーに遭遇します。

注: ESMでは、デフォルトエクスポートは default という名前でエクスポートされるお約束です。

foo.js(CJS)
exports.bar = function(x) {
    return x * x;
}
index.mjs(ESM)
import * as foo from './foo';
console.dir(foo);
出力
[Module] { default: { bar: [Function] } }

Babelの場合 (babel-core=7.4.0)

Nodeと同様です。CJSの中身がdefaultに包まれます。

トランスパイル前
import * as foo from './foo';
console.dir(foo);
トランスパイル後
"use strict";
var foo = _interopRequireDefault(require("./foo"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
console.dir(foo);
出力
{ default: { foo: [Function] } }

Webpack 4.x(.js .ts)の場合

CJSの中身がそのまま渡されます。
デフォルトインポートされたものを使おうとすると、default is not a function等のエラーを見ることになるでしょう。

index.ts_トランスパイル前
import * as foo from './foo';
console.dir(foo);
トランスパイル後
...
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _foo__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./foo.js");
/* harmony import */ var _foo__WEBPACK_IMPORTED_MODULE_0___default =
        /*#__PURE__*/__webpack_require__.n(_foo__WEBPACK_IMPORTED_MODULE_0__);
console.dir(_foo_WEBPACK_IMPORTED_MODULE_0__);
...
出力
{ foo: [Function] }

Webpack 4.x(.mjs)の場合

実は、Webpack 4.xは、Node(--experimental-modules)と同じモジュール解決をサポートしています。
方法は、webpack.config.js.mjs を受け入れるようにし、入力ファイルに.mjsを渡すか、(インポートしようとしている名前).mjsが存在するパスをimportします。

webpack.config.js
...
resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.mjs', '.cjs', '.js']
},
...
index.mjs_トランスパイル前
import * as foo from './foo';
console.dir(foo);
パッケージング中にエラー
ERROR in ./modules/index.mjs 3:12-19
Can't import the namespace object from non EcmaScript module (only default export is available)
@ multi ./modules/index.mjs

デフォルトインポート以外(namespace import, named import)が存在するとエラーになります。

TypeScriptの場合

幸いなことに、TypeScriptのトランスパイラーは、インポート先が何であるかに関わらず、固定的な変換をするだけです。

module (コンパイラオプション) namespace import named import default import
CommonJS const foo = require('foo') const foo = require('foo')
const bar = foo.bar
const foo = require('foo').default
ES2015 そのまま出力 そのまま出力 そのまま出力

どうすればよいのか?

彼方立てれば此方が立たぬ、といった状況ですが
見苦しいコードを受け入れることで回避できます。

defaultに包まれていれば剥がすことで、どちらの場合も動作します。

index.ts(TypeScriptの場合)
import * as foo_ from './foo';
const foo: typeof foo_ = (foo_ as any).default || foo_;
console.dir(foo);
index.js
import * as foo_ from './foo';
const foo = foo_.default || foo_;
console.dir(foo);
トランスパイル後
"use strict";
const foo_ = require("./foo");
const foo = foo_.default || foo_;
console.dir(foo);

追記 ( __filename / __dirname について)

ESMでは、 __filename __dirname といったシンボルも定義されません。

代替手段として、import.meta.urlからファイルURLを取得し、パスに変換します。

const thisFileName = url.fileURLToPath(import.meta.url);
const thisDirName = path.dirname(thisFileName);

ただ、こちらは webpack が対応しておらず
Support for the experimental syntax 'importMeta' isn't currently enabled のようにトランスパイル時にエラーとなります。
@open-wc/webpack-import-meta-loader という webpackプラグインを導入することでトランスパイル時エラーは回避できますが、
実行時に下記のエラーが発生します。

url: `${window.location.protocol}//${window.location.host}/C:\path\to\source\file\index.mjs`
ReferenceError: window is not defined

ブラウザでのみ動作するようです。
動作させるには、次のコードに変更します。

const isWebpack = typeof __webpack_require__ === 'function';
let thisFileName = '';
let thisDirName = '';
if (isWebpack) {
    thisFileName = __filename;
    thisDirName = __dirname;
} else {
    thisFileName = url.fileURLToPath(import.meta.url);
    thisDirName = path.dirname(thisFileName);
}
webpack.config.js
...
module: {
    rules: [{
        test: /\.m?jsx?$/,
        use: [
            'babel-loader',
            require.resolve('@open-wc/webpack-import-meta-loader'),
        ],
        exclude: /node_modules[\/\\]/
    }, {
...

追記 (parcelについて)

今回、webpackと併せてparcelでの動作も比較しました。
parcelでは、webpackのtypeof __webpack_require__ === 'function'のように、パックされたコードかどうかを判定する手段が無いこと、import.meta.urlのエラーを回避する手段を見つけられなかったことで、私のnpmパッケージでのparcel対応ができませんでした。

注:私のnpmパッケージでは、CSSの require() について、パックされているかどうかで動作を変えています。
ESMのファイル内のため、Nodeからの実行では require() が存在しないため、呼び出しをCJS経由でバイパスしていますが、
パックされている場合は require() が存在するため、直接 require() を呼び出します。
※ 文字列リテラルで渡す require('path/to/stylesheet.css') が存在しないとバンドルできないため行っています。

ソース

上述のnpmパッケージを node --experimental-modules と webpack 両方で動作させる全コードです。
https://github.com/shellyln/menneu-api-usage-on-esm

参考資料

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

問.以下のAPIにAjaxでリクエストを送り、レスポンスを表示しなさい(Promiseと再帰)

問題

以下のAPIにAjaxでGETリクエストを送り、レスポンスデータの "message" フィールドの値をコンソールに表示してください。

https://sheltered-island-72837.herokuapp.com/

なお、上記APIはそこそこの確率でエラーレスポンス(ステータスコード500)を返してきます。
もし、1回のリクエストで正常なレスポンス(ステータスコード200)が得られなかった場合は、再度APIへの問い合わせを行ってください。
その際、各リクエストは3秒以上の間を設けるようにし、また最大リトライ回数は2回とします。
リクエストにはaxiosなどのライブラリを使用して構いません。

実装例

const axios = require('axios')
const url = 'https://sheltered-island-72837.herokuapp.com/'

const fetch = (executeCount = 1) => {
  return axios
    .get(url)
    .then(({data}) => data.message)
    .catch(({response}) => {
      if (executeCount < 3) {
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(fetch(++executeCount))
          }, 3000)
        })
      }
      return response.data.message
    })
}

fetch().then(res => console.log(res))

解説

APIへリクエストを送る

まずは単純に、APIに1回だけ問い合わせてコンソールに表示してみます。

Ajaxをする際、axiosを利用することが多いと思います。
もちろん、Fetch API を使ってもいいでしょう。
今回は axios でいきます。

const axios = require('axios')
const url = 'https://sheltered-island-72837.herokuapp.com/'

axios
  .get(url)
  .then(({data}) => console.log(data.message))
  .catch(({response}) => console.log(response.data.message))

{data} という書き方は、分割代入 (Destructuring assignment) といいます。
オブジェクトや配列の一部だけを取り出して、別の変数に代入してくれます。

リトライ機能

メソッド化

まず、先ほどの「APIへリクエストを送る」処理を再利用できるようにメソッドにします。
合わせて、thenやcatch内で直接console.logを呼んでいたのを、値を返すように修正します。

const axios = require('axios')
const url = 'https://sheltered-island-72837.herokuapp.com/'

const fetch = () => {
  return axios
    .get(url)
    .then(({data}) => data.message)
    .catch(({response}) => response.data.message)
}

fetch().then(res => console.log(res))

thenやcatchのなかで値を返すとその値はPromiseでラップされる、ということはとても大事なので覚えておきましょう。
Promise.prototype.then()

「値を返すように修正って、return書いてなくない...?」と思った方は、アロー関数の構文を復習しましょう。
アロー関数

再帰

実現したいことは、「エラーになったら再度リクエストを送る」です。
「エラーになったら」なので、catchの中にその処理を書いていくことになります。
そして、「リクエストを送る」処理は、まさにfetchメソッド自身がそのための処理なのですから、自分を呼び出してしまえばいいわけです。

まず、fetchメソッドが実行回数を取るように引数を追加します。
そしてエラーになった時、実行された回数が上限(初回 + リトライ2回 = 3回)に達していなければ実行回数を+1して自分自身を呼び出します。
そうでなければ普通にレスポンスからメッセージを取り出して return します。

再帰を使うときはその終了条件をしっかり書いてあげないと、いつまでたっても自分を呼び出し続けて終わらなくなりますので十分意識してください。

なお、処理の流れを確認するため、一時的にconsole.logを追加しています。

const axios = require('axios')
const url = 'https://sheltered-island-72837.herokuapp.com/'

const fetch = (executeCount = 1) => {
  console.log(`${executeCount}回目の試行です`)
  return axios
    .get(url)
    .then(({data}) => data.message)
    .catch(({response}) => {
      if (executeCount < 3) {
        console.log('失敗したので再度リクエストします')
        return fetch(++executeCount)
      }
      return response.data.message
    })
}

fetch().then(res => console.log(res))

間隔を開けてリクエストする

最後に、リトライ時、リクエストごとに間隔を開ける処理を実装しましょう。
リトライ時の挙動の調整ですから、再帰でfetchを呼び出している部分を修正します。

〜秒処理を待つ、といえば setTimeout ですね。

console.log('a')

setTimeout(() => {
  console.log('b')
}, 1)

console.log('c')

//出力順
// a
// c
// b

では、setTimeout内で実行される関数から、返り値を受け取りたいときはどうすれば良いでしょう。

const res = setTimeout(() => {
  return 'b'
}, 1)

これで res = 'b' となるでしょうか? 違いますね。
setTimeoutは、指定した時間(第2引数)待って、第1引数に設定した処理を行う、というタイマーを設定する関数です。
setTimeoutの返り値は、そのとき設定されたタイマーのidです。

正解は、setTimeout全体をPromiseでラップして、返したい値でresolveすることです。

const res = new Promise(resolve => {
  setTimeout(() => {
    resolve('b')
  }, 1)
})

res.then(data => console.log(data))
// b が出力される

したがって、修正は以下のようになります。
3秒待つので、setTimeout(() => {}, 3000)、setTimeout自体をPromiseでラップして、返したい値(fetch(++executeCount)) でresolveしています。

const axios = require('axios')
const url = 'https://sheltered-island-72837.herokuapp.com/'

const fetch = (executeCount = 1) => {
  return axios
    .get(url)
    .then(({data}) => data.message)
    .catch(({response}) => {
      if (executeCount < 3) {
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(fetch(++executeCount))
          }, 3000)
        })
      }
      return response.data.message
    })
}

fetch().then(res => console.log(res))

実際に外部のAPIを叩く時でも、エラーが返ってくるということはAPIに何かしら不具合・過負荷が起きている可能性がありますから、優しいリクエストを送ってあげることは大事です。

よくわからなかったという人へ

解説と言いつつ全く詳細を説明していませんので、初心者の方にはちんぷんかんぷんだと思います。
(当たり前のように require とか使ってるし、Promiseって何なのかも説明していないし...)
でも、Javascriptを書いていく上で理解しておいた方がいいことばかりなので、勉強しましょう!
特にPromiseは大事です。フロントエンドでもサーバーサイドでも、Javascriptにおいて非同期処理の扱いはとっっても大事です。

https://sheltered-island-72837.herokuapp.com/

上記APIはしばらく稼働させておきますので、練習にお使いください。
(節度のあるリクエストをお願いします!)

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

REPELIS Aquaman 2018 P E L I C U L A (Completa) En Español Latino Gratis htsq

Información del Estreno : 2018-12-07 (143min)

Lema : Protector of the Deep

Géneros : Action, Fantasy, Science Fiction, Adventure

Compañías de producción: DC Comics, DC Entertainment, Warner Bros. Pictures, The Safran Company, Mad Ghost Productions, Rodeo FX, Panoramic Pictures

Países de producción : Australia, United States of America

SINOPSIS :
Un icono durante más de 70 años, Aquaman (Jason Momoa) es el Rey de los Siete Mares. Este reacio gobernante de Atlantis se encuentra atrapado entre los constantes estragos causados al mar por los habitantes de la superficie y los atlantes buscando rebelarse. Pese a todo está decidido a proteger el mundo entero.

Aquaman Critics Consensus. Aquaman swims with its entertainingly ludicrous tide, offering up CGI superhero spectacle that delivers energetic action with an emphasis on good old-fashioned fun. Directed by James Wan. With Jason Momoa, Amber Heard, Willem Dafoe, Patrick Wilson. Arthur Curry, the human-born heir to the underwater kingdom of Atlantis, goes on a quest to prevent a war between the worlds of ocean and land. Aquaman is a fictional superhero appearing in American comic books published by DC Comics.Created by Paul Norris and Mort Weisinger, the character debuted in More Fun Comics #73 (November 1941). Initially a backup feature in DC's anthology titles, Aquaman later starred in several volumes of a solo comic book series. During the late 1950s and 1960s superhero-revival period known as the Silver Directed by James Wan. With Jason Momoa, Amber Heard, Willem Dafoe, Patrick Wilson. Arthur Curry, the human-born heir to the underwater kingdom of Atlantis, goes on a quest to prevent a war between the worlds of ocean and land. Aquaman (2018) cast and crew credits, including actors, actresses, directors, writers and more. Aquaman is a fictional superhero appearing in American comic books published by DC Comics.Created by Paul Norris and Mort Weisinger, the character debuted in More Fun Comics #73 (November 1941). Initially a backup feature in DC's anthology titles, Aquaman later starred in several volumes of a solo comic book series. During the late 1950s and 1960s superhero-revival period known as the Silver Aquaman is a fictional superhero appearing in American comic books published by DC Comics.Created by Paul Norris and Mort Weisinger, the character debuted in More Fun Comics #73 (November 1941). Initially a backup feature in DC's anthology titles, Aquaman later starred in several volumes of a solo comic book series. During the late 1950s and 1960s superhero-revival period known as the Silver Directed by James Wan. With Jason Momoa, Amber Heard, Willem Dafoe, Patrick Wilson. Arthur Curry, the human-born heir to the underwater kingdom of Atlantis, goes on a quest to prevent a war between the worlds of ocean and land. Aquaman Critics Consensus. Aquaman swims with its entertainingly ludicrous tide, offering up CGI superhero spectacle that delivers energetic action with an emphasis on good old-fashioned fun.

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

TypeScript: Duplicate identifier 'LibraryManagedAttributes'. 解消法

環境

ts-lint: 5.14.0
yarn: 1.15.2
node: v10.15.1

エラーログ

/Users/ユーザー名/プロジェクト名/node_modules/@types/react-dom/node_modules/@types/react/index.d.ts
(2777,14): Duplicate identifier 'LibraryManagedAttributes'.

原因

調べてみたところ、LibraryManagedAttributesの定義が
①node_modules/@types/react/index.d.ts
②node_modules/@types/react-dom/node_modules/@types/react/index.d.ts
と2箇所存在。
@types/reactの定義がyarn.lockにバージョン違いで2つ定義されていたことが原因。

解消法

①package.jsonに"resolutions"を追加。
②yarn install実施

package.json
  "devDependencies": {
    "@types/react": "^16.8.10",
    "@types/react-dom": "^16.8.3",
    ...
  },
  "resolutions": {
    "@types/react": "^16.8.10"
  }

③yarn.lockを確認してみると
"@types/react@*", "@types/react@^16.8.10":
といった形でバージョンを統一。

yarn.lock
"@types/react-dom@^16.8.3":
  version "16.8.3"
  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.3.tgz#6131b7b6158bc7ed1925a3374b88b7c00481f0cb"
  integrity sha512-HF5hD5YR3z9Mn6kXcW1VKe4AQ04ZlZj1EdLBae61hzQ3eEWWxMgNLUbIxeZp40BnSxqY1eAYLsH9QopQcxzScA==
  dependencies:
    "@types/react" "*"

"@types/react@*", "@types/react@^16.8.10":
  version "16.8.10"
  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.10.tgz#1ccb6fde17f71a62ef055382ec68bdc379d4d8d9"
  integrity sha512-7bUQeZKP4XZH/aB4i7k1i5yuwymDu/hnLMhD9NjVZvQQH7ZUgRN3d6iu8YXzx4sN/tNr0bj8jgguk8hhObzGvA==
  dependencies:
    "@types/prop-types" "*"
    csstype "^2.2.0"
    ...

参考サイト

https://stackoverflow.com/questions/52399839/typescript-duplicate-identifier-librarymanagedattributes

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

ESLint v5.16.0

v5.15.0 | 次 (2019/04/13 JST)

ESLint 5.16.0 がリリースされました。
小さな機能追加とバグ修正が行われました。

特に問題がなければ、これが ESLint 5.x 系の最後のリリースになります。次回は ESLint 6.0.0 プレリリースの予定です。

質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。

? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット
? 本家リポジトリ
? 本家サポート チャット

? 本体への機能追加

RFC 10: カスタム フォーマッターからルールの定義データにアクセスできるようになりました。

従来はフォーマッターはリント結果のみにアクセスできました。しかし、各ルールのメタデータ (例えば各ルールのドキュメントへの URL) を使いたいという要望が強くあったため、これにアクセスできるようになりました。

詳細はドキュメントを参照ください。

RFC 10: フォーマッター json-with-metadata が追加されました。

リント結果を、各ルールのメタデータを含む JSON 形式で出力するフォーマッターです。

? 新しいルール

特になし。

? オプションが追加されたルール

特になし。

✒️ eslint --fix をサポートしたルール

特になし。

⚠️ 非推奨になったルール

特になし。

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

【JavaScript】スコープ【自己学習】

スコープとは

スコープとは、変数を参照できる範囲を決める概念のこと。

スコープの種類

  • グローバルスコープ → コード内のどこからでも参照できる
  • ローカルスコープ → 変数を定義した関数内でのみ参照できる

グローバルスコープを持つ変数をグローバル変数、ローカルスコープを持つ変数をローカル変数と呼び、スコープが異なる変数は同名であっても別の変数として扱われる。

scope-1.js
var scope = "global";    //グローバル変数の定義

function value() {
  var scope = "local";   //ローカル変数の定義
  console.log(scope);
}

value();  // > local
console.log(scope); // > global

value関数内の先頭でローカル変数scopeを定義したことで、関数内の変数scopeの値はglobalからlocalに上書きされている。そのため、value関数を呼び出すとコンソール上ではlocalと出力される。

一方、関数の外にある変数scopeを呼び出した場合、グローバル変数scopeの値はコードの一番で定義したままであるためglobalが出力される。

変数の巻き上げ(ホイスティング)

ローカル変数は関数内のどこで定義しても、変数の宣言自体は関数内の先頭で行われたとみなされる。これを変数の巻き上げ(ホイスティング)という。

scope-2.js
var scope = "global";

function value() {      //実際の関数内の処理
  console.log(scope);   //var scope;
  var scope = "local";  //console.log(scope);
                        //scope = "local"
}                        

value();  // > undefined

上記のコードで関数valueを呼び出した場合、ローカル変数scopeの定義が関数の一番最後で行われていることから、globalという値が出力されるようにもみえるが、実際はundefinedが出力される。

これは、ホイスティングにより実際の処理では関数の先頭でローカル変数scopeが宣言され、値の代入自体はconsole.log(scope)の後に行われていることで生じている。

scope-3.js
var scope = "global";

function value() {
  var scope = "local";
  console.log(scope);
}

value();  // > local

ホイスティングにより思わぬ不具合を引き起こすことを防ぐために、上記のコードのようにローカル変数は関数内の先頭で定義するのが望ましい。

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

良く使うjavascriptのメモ

開発者モード

ブラウザでF12を押すと出てきます
コンソール出力を見たり、HTMLソースを見たり、ブレイクポイントを仕掛けたり、通信状況を見たりできます。

ページ読み込み完了時

ページ読み込み完了時に処理する
function xxx(){
alert("");
}
window.addEventListener("load", xxx);

数値

■絶対値を返す
var XXX = Math.abs(xxx);

■文字列→浮動小数点数
var XXX = parseFloat(xxx);

■文字列→整数
var XXX = parseInt(xxx);

■0~xxx-1までの整数を返す
var XXX = Math.floor(Math.random() * xxx);

■四捨五入
var XXX = Math.round(xxx);

■現在の日付と時刻 YYYY/MM/DD hh:flag_mm:ss
var now = new Date();
var Y = now.getFullYear();
var M = now.getMonth()+1;
var D = now.getDate();
var h = now.getHours();
var m = now.getMinutes();
var s = now.getSeconds();
var dateStr = Y + "/" + M + "/" + D + " " + h + ":" + m + ":" + s;

■経過時間(ミリ秒)
var startTime = new Date();
var endTime = new Date();
var nowTime = endTime - startTime;

文字列

■string文字列を100文字にカット
string.slice(0,100);

■string文字列を,で分割
string.split(",");

■数値→文字列
var XXX = (xxx).toString();

■xxxが3桁の数字文字列になるようにする
("00"+(xxx)).slice(-3);

配列操作

■配列のxxx番目を削除
arrayObj.splice(xxx, 1);

■配列に追加
arrayObj.push(xxx);

■配列arrayを参照ではなく値でコピーする
var newarray = array.concat();

■配列に含まれるか
if (arr.indexOf("a") >= 0)

[ES6]
const index = ary.findIndex(item => item === a)
if (index >= 0)

変数操作

■BがundefineならCを代入する
A = B || C;

■if( A === B )
null,undefineなどを厳密に条件としてチェックする

■変数に含まれるデータ型を得る
typeof num
返却されるのは"number","string","boolean","object","undefined","function"

■「ObjectA.keyA」と「ObjectA[keyA]」意味は同じ

■オブジェクト
ObjectKeyには$も.も使える
var obj = new Object;
obj.$in = "aaa";
obj["aaa.bbb"]="ccc";

■オブジェクトのプロパティを得る
for(let ky of Object.keys(obj)) {
console.log(obj[ky]);
}
kyはキー
obj[ky]は値

■オブジェクトのプロパティ削除
dalete obj.xxx
または
delete obj[xxx]

要素

■要素の最後に追加する
document.getElementById('XXXXX').appendChild(xxxx);

■要素のテキストを書き換える
document.getElementById('XXXXX').InnerText(xxxx);

■要素のHTMLを書き換える
document.getElementById('XXXXX').InnerHTML(xxxx);

■名前でエレメント取得
document.getElementsByName("XXXXX");

■セレクトボックスの選択中の値を取得
vals = document.getElementById(ID).options.value;

■画面幅・高さを取得
var w = window.innerWidth;
var h = window.innerHeight;

キー入力取得

■キー入力取得
//キーが押された時のイベントリスナーの登録
document.addEventListener('keydown', handleKeyDown, false);
//キーが離された時のイベントリスナーの登録
document.addEventListener('keyup', handleKeyUp, false);

//キーボードのキーが押された時の処理
var keyFlags = [false, false, false, false];
function handleKeyDown(event) {
if (event.keyCode==40 || event.keyCode==83) {//↓ s ボタン
keyFlags[0] = true;
} else if (event.keyCode==38 || event.keyCode==87) {//↑ w ボタン
keyFlags[1] = true;
} else if (event.keyCode==37 || event.keyCode==65) {//← a ボタン
keyFlags[2] = true;
} else if (event.keyCode==39 || event.keyCode==68) {//→ d ボタン
keyFlags[3] = true;
}
}
//キーボードのキーが離された時の処理
function handleKeyUp(event) {
if (event.keyCode==40 || event.keyCode==83) {//↓ s ボタン
keyFlags[0] = false;
} else if (event.keyCode==38 || event.keyCode==87) {//↑ w ボタン
keyFlags[1] = false;
} else if (event.keyCode==37 || event.keyCode==65) {//← a ボタン
keyFlags[2] = false;
} else if (event.keyCode==39 || event.keyCode==68) {//→ d ボタン
keyFlags[3] = false;
}
}

リンク

//移動
location.href=パス名;

// 新しいタブを開き、ページを表示
window.open('パス名', '_blank');

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

ドラッグ&ドロップされた画像を leaflet で表示してみる

成果物

デモはこちら
ソースはこちら

leaflet を利用するメリット

  • パン&ズームはおまかせ
  • svg,canvas のどちらも用途に合わせて選択できる
  • 「画面中央配置」が(比較的)簡単

ドラッグ&ドロップのイベントを扱う

index.html
<!DOCTYPE html>
<html lang="en" style="height: 100%; margin: 0;">
<head>
  <meta charset="UTF-8" />
</head>
<body style="height: 100%; margin: 0; background-color: #dddddd;">
  <script>
    // ドラッグ中
    window.addEventListener('dragover', (e) => {
      // ブラウザのデフォルトアクションを阻止
      e.preventDefault();
      // イベントの伝播を止める
      e.stopPropagation();
    }, false);

    // ドロップされたとき
    window.addEventListener('drop', (e) => {
      e.preventDefault();
      e.stopPropagation();

      // ドロップされたファイルリストの先頭のみ扱う
      const file = e.dataTransfer.files[0];

      // ファイル名とファイルタイプの出力
      console.log(file.name);
      console.log(file.type);
    }, false);
  </script>
</body>
</html>

ドロップされた画像を読み込む

index.html
    window.addEventListener('drop', (e) => {
      e.preventDefault();
      e.stopPropagation();

      const file = e.dataTransfer.files[0];

      console.log(file.name);
      console.log(file.type);

      // 画像ファイル以外は無視する
+     if (!file.type.match('image')) {
+       return;
+     }
+
+     const reader = new FileReader();
+     
      // ファイルの読み込みが完了したとき
+     reader.onload = (e) => {
+       const img = new Image();
        // 念の為に画像の読み込みの完了も待つ
+       img.onload = () => {
+         console.log(img.width);
+         console.log(img.height);
+       }
        // ソースのパスはイベントが持っている
+       img.src = e.target.result;
+     }
+
      // ファイルを読み込む
+     reader.readAsDataURL(file);
    }, false);

手順のメモ

  • FileReader (MDN) を作成
  • readAsDataURL (MDN) メソッドでの読み込みが完了すると loadend イベントが発生し、その result プロパティにファイルデータが格納される
  • img 要素を作成し、そのソースに result を指定 (「画像を使う」 (MDN))

leaflet の読み込み

とりあえず CDN で。

index.html
<html lang="en" style="height: 100%; margin: 0;">
<head>
  <meta charset="UTF-8" />
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css"
+  integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=="
+   crossorigin=""/>
+ <script src="https://unpkg.com/leaflet@1.4.0/dist/leaflet.js"
+  integrity="sha512-QVftwZFqvtRNi0ZyCtsznlKSWOStnDORoefr1enyq5mVL4tmKB3S/EnC3rRJcxCPavG10IcrVGSmPh6Qw5lwrg=="
+  crossorigin=""></script>
</head>
<body style="height: 100%; margin: 0; background-color: #dddddd;">
+  <!-- leaflet map のマウントポイント --->
+  <div id="map" style="height: 100%; margin: 0; overflow: hidden;"></div>
  <script>

leaflet のマップに画像を貼り付ける

index.html
      reader.onload = (e) => {
        const img = new Image();
        img.onload = () => {
          const bounds = L.latLngBounds([0, 0], [img.height, img.width]);
          const map = L.map('map', {
            crs: L.CRS.Simple,
          });
          map.fitBounds(bounds);
          map.setMaxBounds(bounds);
          L.imageOverlay(e.target.result, bounds).addTo(map);
        }
        img.src = e.target.result;
      }

ポイント

参照リンク

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

Container Bound Scriptをclaspで管理

Container Bound Scriptとは?

公式ページ

Google Apps Script のプロジェクトでスプレットシートに紐付くプロジェクト。
Container Bound Scriptと逆のGoogleドライブ上に単独で存在するStandalone Scriptがある。

:computer:環境構築


  1. Google Drive上でスプレットシート新規作成し、「ツール」>「スクリプトエディッタ」を選択します
    image1.png

  2. スクリプトエディッタ上で「ファイル」>「保存」でプロジェクト名を設定し保存します

  3. 手元でclaspを使って環境を構築します

    $ yarn add @google/clasp cpx gas-webpack-plugin tslint @types/google-apps-script @types/node ts-loader ts-node webpack webpack-cli --dev
    
  4. claspでログインしてない場合はログインを行います

    $ ./node_modules/.bin/clasp login
    

    ブラウザが立ち上がるので対象のアカウントでログインを行います
    claspの初期設定等はここでは省略

  5. .clasp.jsonscriptId に作成したスクリプトのIDを設定します

:pencil: 実装


簡単なサンプル

簡単にできる「スプレットシート開いたらメニューを追加する」をやってみたいと思います。

declare var global: any;

global.onOpen = (): void => {
  SpreadsheetApp
    .getUi()
    .createMenu('Test')
    .addItem('ホゲホゲする', 'showDialog')
    .addToUi();
}

global.showDialog = (): void => {
  Browser.msgBox('メニューからダイアログ表示');
}

デプロイしてリロードするとメニューが表示されています。
image2.png

メニューをクリックするとダイアログが表示されます。
image3.png

編集された時のEventの値をダンプ

global.onEdit = (e: any): void => {
  const range: GoogleAppsScript.Spreadsheet.Range = e.range;
  const authMode: GoogleAppsScript.Script.AuthMode = e.authMode;
  const oldValue: any = e.oldValue;
  const source: GoogleAppsScript.Spreadsheet.Spreadsheet = e.source;
  const triggerUid: number = e.triggerUid;
  const user: GoogleAppsScript.Base.User = e.user;
  const value: any = e.value;

  Logger.log(`range: ${range.getValue()}`);
  Logger.log(`authMode: ${authMode}`);
  Logger.log(`oldValue: ${oldValue}`);
  Logger.log(`source: ${source.getName()}`);
  Logger.log(`triggerUid: ${triggerUid}`);
  Logger.log(`user: ${user.getEmail()}`);
  Logger.log(`value: ${value}`);
}

スプレットシートのセルを編集してみた結果が以下です :eyes:
image4.png
↑なぜか triggerUidは取得出来ず。。

ひとまずclaspでContainer Bound Scriptを管理できるようになったので、
テスト等も書けて良い感じになりました :sparkles:

:bomb: バッドノウハウ


  • html-webpack-pluginで以下のエラーが発生
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html

意味のない警告なので無視していいらしいです。。
https://github.com/jantimon/html-webpack-plugin/issues/895#issuecomment-379006202

:link: 参考リンク


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

Raspberry PI 3 Model B でNordic SoftDevice UART BLE ペリフェラル として動作させる

cf: https://qiita.com/uzuki_aoba/items/346e28b6e9170ce85a6c

前提

  • nodeenv
  • Ubuntu 16.04

色々入れる

https://github.com/noble/bleno#ubuntudebianraspbian

sudo apt install bluetooth bluez libbluetooth-dev libudev-dev

なんやかんやで完成
https://gist.github.com/katsusuke/a1a9ae11c62e7321e155df5f093bd1d1

npm install
sudo ~/.nodenv/shims/node main.js

Central 側は nRF Toolbox
https://itunes.apple.com/jp/app/nrf-toolbox/id820906058?mt=8

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

Raspberry PI 3 Model B をNordic SoftDevice UART BLE ペリフェラル として動作させる

cf: https://qiita.com/uzuki_aoba/items/346e28b6e9170ce85a6c

前提

  • nodeenv
  • Ubuntu 16.04

色々入れる

https://github.com/noble/bleno#ubuntudebianraspbian

sudo apt install bluetooth bluez libbluetooth-dev libudev-dev

なんやかんやで完成
https://gist.github.com/katsusuke/a1a9ae11c62e7321e155df5f093bd1d1

npm install
sudo ~/.nodenv/shims/node main.js

Central 側は nRF Toolbox
https://itunes.apple.com/jp/app/nrf-toolbox/id820906058?mt=8

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

Vuejsでのプロトタイピングを簡単にできるアプリが欲しかったので作っている話

はじめに

タイトルの通り作っているものがあるが、公開することで既にこれできるよみたいな情報も得られると思ったので公開しようと思った。

本当はもう少し作りこんでから公開したかった。
ただ、残業が多すぎてアプリケーションが作れないから転職しようとしていて、エージェントから公開しているGitHubとかQiitaのアカウントありますかと聞かれたからROM専から卒業しようと思った。

とりあえず動くもの

https://sterashima78.github.io/vue-webpage-builder

とりあえず動いている絵

img.gif

作っている動機

基本的には READMEに書いておいたが、書きなぐりました感が強いのでもう少し丁寧に書く。

似ていたもの

既存のもので十分な人もいると思うので、作る前に調べていた時に触ったものを書いておく。

  • Vuegg
    • いいツールだったのだが、次に書く私のほしかったものには合わなかった
  • Grapes
    • 初めはこれのプラグインを作ろうとしたのだが、無理やり感が出てきて断念した

ほしかったもの

  • 実用的なソースが出力される
    • プロトタイピングに使ったあとで、じゃあこれで行こうとなった時に全部書き直すよりも一部直せば済むほうが嬉しい
    • Vueggはコンポーネントの配置が絶対座標だった。絵を作るのには適していたのだけど、出力をそのまま使う気にはならなかった
  • 任意のコンポーネントを利用できる
    • Vuetifyや、Elementなどたくさんコンポーネントフレームワークがあるので、任意のものが使えてほしい
    • 自分が作ったコンポーネントも利用できるようになっていてほしい
    • Vueggは単一のコンポーネントフレームワークしか利用できなかった (Issueにもなっている)

どうするか

任意のコンポーネントを利用するという点はアプリケーションを構築に利用しているVueインスタンスと、アプリケーション内で構築されるプロトタイプで利用するVueインスタンスが同一であると絶対に解決できないと考えた。

初めはインスタンスを複数作って Shadow DOMを使えば何とかなるとも考えたけど素直にiframeを使うことにした。

  1. iframe要素内でscriptタグでvuejsを読み込み、アプリケーションからiframe内のVueインスタンスを参照する
  2. メインのアプリケーション部分でvnodeに相当するツリーを構築する
  3. iframe内のvueに渡して renderメソッド で描画

これができれば、iframe内で任意のスクリプトを読ませる機能を作ることで、『ほしかったもの』に書いた、任意のコンポーネントが利用できるの要件を満たせる。

面倒だったこと

Vuex Store は複数のインスタンスで共有できない

アプリケーションで構築するvnodeデータをVuexで管理して、そのデータをiframe内のvueでも共有して renderでstateを参照できれば一番シンプルだと考えて、Vuexベースで作ってた。(入門したてのTypeScriptでstoreに型つけるにはどうすればいいんだとか調べてまぁまぁ時間かかった)。
いざ描画しようと思ったら、できないということを知って困った困った。

結局 Rx を併用して無理やりやっている感がある。

Vueに登録されている全コンポーネントが知りたい

アプリケーションの性質的に利用できるコンポーネントがリストされていて、選択できないといけないが、それを取得するためのインターフェースが見つからなかった。

devtoolsとか見ていたら方法がありそうだったので調べたら、Vue.options.components に入っていた。

おわりに

Vuejsでのプロトタイピングを簡単にできるアプリを作っているという話でした。
もしも、上に書いた『ほしかったもの』をカバーするサービスやアプリケーションがあれば教えていただけると嬉しいです。

以下が、今後やりたいこと。

  • 追加したい機能
    • 任意のコンポーネントフレームワークを使えるようにする (いまはvuetifyのみだけど仕組み的には可能なのですぐやりたい)
    • 複数ページへの対応 (vue-routerで)
    • 作成途中の保存・読み込み
  • 機能以外で対応したいこと
    • リファクタリング
    • CI・CDの導入
      • ある程度機能がそろったら
  • 残業が減ってアプリケーションを作れるようになる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのPromiseに関するまとめ(async/await含む)

JavaScriptのPromiseについて、これまでに学んだことをまとめました。
async/awaitもfetchも扱っているものはPromiseなので、JavaScriptの非同期処理に於いて、基礎のような存在と言えなくもない?

確認環境

Windows 10
Chrome DevTools 73
Sources>snippets にて確認しました。

Promiseの基本

他の方も紹介されていますが、Promiseの基礎知識についてはこちらのサイトが大変参考になります。

JavaScript Promiseの本
Promiseを使う | MDN

1箇所からデータを取得する

setTimeoutを非同期通信処理に見立てて、一つのデータを受け取り検査します。
奇数を受け取ったら通信成功、偶数もしくは数値と評価できない値の場合は通信失敗です。

// Promiseオブジェクトを作成して返す
function createPromise(param) {
  // インスタンス作成時にコールバック関数を渡します。
  return new Promise((resolve, reject) => {
    // 非同期通信処理を定義
    setTimeout((result) => {
      console.log('Result:', result);
      // 非同期通信で受け取った値 result を検査する
      if (result % 2) {
        // 成功と判断した際は1番目に受け取った関数を実行
        resolve(result);
      } else {
        // 失敗と判断した際は2番目に受け取った関数を実行
        reject(`${result} is not odd`);
      }
    }, 500, (param | 0));
  });
}

function testPromise(param){
  // 作成されたPromiseオブジェクトを使って、非同期通信後の処理を定義する
  createPromise(param)
    .then((result) => {
      // 成功時の後続処理。resolve実行時に渡された値を受け取る
      console.log('Hello Promise!:', result);
    })
    .catch((error) => {
      // 失敗時の後続処理。reject実行時に渡された値を受け取る
      console.log('Error:', error);
    })
    .finally(() => {
      // 成功・失敗、いずれの場合も最後に実行したい処理、引数は受け取らない
      console.log('Fin...');
    });
}
実行結果
> testPromise(5)
Result: 5
Hello Promise!: 5
Fin...

> testPromise(4)
Result: 4
Error: 4 is not odd
Fin...

Promise関数を使ってPromiseオブジェクトを作成します。
この際、結果を待ちたい非同期処理を定義したコールバック関数executorを渡します。
このexecutorは、引数として二つの関数を受け取ります。
一つ目の関数resolveは、非同期処理が解決(成功)したと判断した場合に実行します。
二つ目の関数rejectは、非同期処理が拒否(失敗)したと判断した場合に実行します。
それぞれ、後続処理に渡したいデータを引数に渡します。
例えば、HTTPのレスポンスコードがHTTP 200 OKでも、受け取った中身が不正の為に失敗と判断したい場合はrejectを実行します。

// 通信成功し、データ(responseText)を取得することは出来た。が、まだ油断は出来ない…
try {
  const data = JSON.parse(responseText || null);
  if (data === null) {
    reject('データが無いよ');
  } else {
    // 受け取ったJSONデータを無事にパースできた
    resolve(data);
  }
} catch {
  reject('JSONをパース出来なかったよ');
}

作成したPromiseオブジェクトは3つのメソッドを持ちます。それぞれに引数で解決時・拒否時・完了時に実行するコールバック関数を渡します。
各メソッドの戻り値はPromiseオブジェクトなので、メソッドチェーンで繋げることが可能です。
thenは、解決時(executor内でresolve実行時)に呼び出される処理onFulfilledを一つ目の引数にとります。
catchは、拒否時(executor内でreject実行時)に呼び出される処理onRejectedを引数にとります。
また、executor内でエラーが発生した場合もonRejectedが呼ばれます。
finallyは、解決・拒否にかかわらず、onFulfilledまたはonRejectedが実行された後に実行される処理onFinallyを引数にとります。

thenは、二つ目の引数にonRejectedを受け取ります。これは省略が可能です。catchを利用せずに、thenに二つの関数を渡すことも可能です。
catchを利用した場合との違いは、onFulfilled内でエラーが発生した場合、thenに渡したonRejectedは実行されませんが、catchに渡したonRejectedは実行されます。

Promise | MDN
Promise.prototype.then() | MDN
Promise.prototype.catch() | MDN
Promise.prototype.finally() | MDN

2箇所からデータを受け取る処理を直列でつなげる

ある場所から取得したデータをもとに、また別の場所へリクエストを投げたい場合は、Promiseオブジェクトを作成する処理をメソッドチェーン内で繰り返します。
一つ目のthen内で再びcreatePromise関数を呼び出し、新たに作成したPromiseオブジェクトを戻り値に設定しています。

直列処理
function createPromise(param) {
  return new Promise((resolve, reject) => {
    setTimeout((result) => {
      console.log('Result:', result);
      if (result % 2) {
        resolve(result);
      } else {
        reject(`${result} is not odd`);
      }
    }, 500, (param | 0));
  });
}

function testPromise(param) {
  // Promiseオブジェクトを作成して最初の非同期処理Aを実行
  createPromise(param)
    .then((result) => {
      // 処理Aが解決した場合の後続処理
      // 処理Aから受け取った値を加工して次の非同期処理に利用する
      const newNum = (result / 3) | 0;

      console.log(`Next: ${result} to ${newNum}`);

      // 新たなPromiseオブジェクトを作成して非同期処理Bを実行
      return createPromise(newNum);
    })
    .then((result) => {
      // 処理Bが解決した場合の後続処理
      console.log('Hello Promise!:', result);
    })
    .catch((error) => {
      // 処理Aまたは処理Bが拒否した場合の後続処理
      console.log('Error:', error);
    })
    .finally(() => {
      console.log('Fin...');
    });
}
実行結果
> testPromise(5)
Result: 5
Next: 5 to 1
Result: 1
Hello Promise!: 1
Fin...

> testPromise(7)
Result: 7
Next: 7 to 2
Result: 2
Error: 2 is not odd
Fin...

Promiseオブジェクトを作成するとresolve,rejectの使い捨てバケツを渡され、それを使って次に荷物を受け渡す。
受け取った側は再びPromiseオブジェクトを作成してバケツを入手し、次に荷物を受け渡す。
そんなイメージでしょうか。。。

Promiseの状態

3つの状態

作成したPromiseオブジェクトは、下記三つのうちいずれかの"状態"にあります。

状態 条件 条件成立時の挙動
pending (初期状態) 作成時 待機
fulfilled (解決済み) resolve()の実行 onFulfilledを呼ぶ
rejected (拒否済み) reject()の実行 onRejectedを呼ぶ

一度状態がfulfilledまたはrejectedに変化すると、もう変わることはありません。
いずれかに変化済みの状態はsettledと呼ばれます。
そのため、一つのオブジェクトのthencatchメソッドに登録したコールバック関数は1回しか呼ばれません。
故に使い捨てバケツであり、直列接続をするには毎回新しいPromiseを作成する必要があります。

静的メソッドのresolveとreject

静的メソッドのPromise.resolve(), Promise.reject()を利用すると、最初からsettledの状態のPromiseオブジェクトを作成できます。
then()catch()内では、pendingのPromiseオブジェクトを返すとその結果を待ちますが、settledのPromiseオブジェクトを返すとその状態に応じて後続処理が実行されます。(非同期)
Promiseオブジェクト以外のものはfulfilledのPromiseオブジェクトにラップされ、後続のthen()へ進みます。
後続のcatch()へ進みたい場合はrejectedのオブジェクトを返す必要があり、Promise.reject()を利用して作成します。
executor内でこのメソッドを利用しても、executorを実行したPromiseオブジェクト自体の状態は変わらないため、後続処理が実行されることはありません。
状態を変えたいオブジェクトから渡されたresolve,rejectedを利用する必要があります。

Promise.allで複数箇所からデータを受け取る処理を並列でつなげる

取得順序は問わないが全て揃ったら次のステップに進みたい、そんなあなたのニーズにお答え出来るのが、静的メソッドのPromise.allです。

createPromise関数の処理内容は上記と同一ですが、setTimeout関数の待機時間をランダムで1~5秒に設定されるようになっています。

Promise.all
function createPromise(param) {
  const sec = Math.floor(Math.random() * 5 + 1) * 1000;

  return new Promise((resolve, reject) => {
    setTimeout((result) => {
      console.log('Result:', result, `(${sec}ms)`);
      if (result % 2) {
        resolve(result);
      } else {
        reject(`${result} is not odd`);
      }
    }, sec, (param | 0));
  });
}

function testPromise(param1, param2, param3) {

  // 作成したPromiseオブジェクトの配列を渡す
  Promise.all([
    createPromise(param1),
    createPromise(param2),
    createPromise(param3),
    'MoiMoi'
  ])
    .then(([first, second, third, forth]) => {

      // all()に渡した全ての処理が解決した場合、実行される
      // 取得順に関係なく、all()に渡した順番の配列で結果を受け取る
      console.log('Complete!:', first, second, third, forth);
    })
    .catch((error) => {

      // いずれか一つでも拒否すると1回だけ実行される
      console.log('Error:', error);
    });
}
実行結果
> testPromise(3,5,7)
Result: 5 (1000ms)
Result: 3 (3000ms)
Result: 7 (4000ms)
Complete!: 3 5 7 MoiMoi

> testPromise(1,2,3)
Result: 2 (1000ms)
Error: 2 is not odd
Result: 1 (2000ms)
Result: 3 (3000ms)

> testPromise(9,4,6)
Result: 4 (1000ms)
Error: 4 is not odd
Result: 9 (3000ms)
Result: 6 (5000ms)

all()に渡す配列には、Promiseオブジェクト以外のものを要素にすることが出来ます。上記では3番目に文字列を渡しています。
この場合解決済みのPromiseとしてラップされ、後続のthenで値を取得できます。
一度拒否が発生すると、その後に実行されたresolve(),reject()は無視されます。

Promise.raceで一番最初に出た結果を採用する

概要

Promise.allに良く似た並列処理で、Promise.raceがあります。
all同様に配列で処理を渡し、並列で実行されるのを待機するのですが、いずれか一つが解決または拒否の結果を出せばそれを採用し、後続処理を実行します。

Promise.race
/**
 * createPromise関数は上記 Promise.all のサンプルコードと同じ
 */

function testPromise(param1, param2, param3) {
  // Promise.all と同様に配列を渡す
  Promise.race([
    createPromise(param1),
    createPromise(param2),
    createPromise(param3)
  ])
    .then((result) => {
      // 取得できるのは一つ
      // 一番最初に帰ってきた結果を採用する
      console.log('Win!:', result);
    })
    .catch((error) => {
      console.log('Error:', error);
    });
}
実行結果
> testPromise(2,7,3)
Result: 7 (2000ms)
Win!: 7
Result: 2 (3000ms)
Result: 3 (3000ms)

> testPromise(2,7,3)
Result: 2 (4000ms)
Error: 2 is not odd
Result: 7 (5000ms)
Result: 3 (5000ms)

タイムアウト処理を実装する

当初、使いどころが良く分からなかったのですが、4.5. Promise.raceとdelayによるXHRのキャンセル | JavaScript Promiseの本によると、タイムアウト処理の実装に利用できることを知りました。
従来からAjax通信時に利用されてきたXMLHttpRequest(XHR)には、標準でタイムアウトの機能があります。
XHRの後続であるFetchAPIには、タイムアウト機能がありません。(2019年3月現在)
もしかしたら今後は出番が増えていくのかもしれませんね。

Promise.raceによるタイムアウト
// データ取得処理
function fetchWrp(path) {
  // FetchAPIでデータを取得する
  return fetch(path)
    .then((response) => {
      if (response.ok) {
        return response.json();
      } else {
        return Promise.reject(response.status);
      }
    })
    .catch((error) => {
      return Promise.reject(error);
    })

  // DevToolでの実行用テストコード
  // 1~6秒後に結果を返す
  /*
  const sec = Math.floor(Math.random() * 6 + 1) * 1000;
  console.log(`will take ${sec}ms...`);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`fetched (${sec}ms)`);
    }, sec)
  });
  */
}
// タイムアウト処理
function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(`timeout (${ms}ms)`);
    }, ms);
  });
}

// timeoutに指定した時間内にfetchWrpの結果が出なければ、タイムアウト
Promise.race([
  fetchWrp('/sample/api/data/'),
  timeout(3000)
])
  .then((result) => {
    console.log('Result:', result);
  })
  .catch((error) => {
    console.log('Error:', error);
  });

Fetch API |MDN

FetchAPIでreject
fetch()は実行するとPromiseオブジェクトを返すので、thencatchを利用できます。
取得失敗時はPromise.rejectを利用することで、Promise.racecatchへ処理が進むようにしています。

いろいろなメソッドチェーン

実務で利用機会があるか否かは別として、理解を深める為に書いたサンプルコードです。

単純な直列

下記の例では、executor内で拒否されて【A】へ進んでも、その後は【B】、【C】と続き、【D】が実行されることはありません。
メソッドチェーンの途中で、Promise.reject()の結果を返すと、後続のcatch()が実行されます。

SampleA
function testPromise(param) {
  const num = param | 0;

  new Promise((resolve, reject) => {
    if (num % 2) {
      resolve('resolve');
    } else {
      reject('reject');
    }
  })
    .then((res) => {
      return `${res} → hoge`;
    })
    .then((res) => {
      return `${res} → fuga`;
    })
    .catch((res) => {
      return `${res} → boo`; //【A】
    })
    .then((res) => {
      return `${res} → piyo`; //【B】
      // return Promise.reject(res); とした場合、【D】に進む
      // resolve → hoge → fuga → rejected
    })
    .then((res) => {
      console.log(`${res} → fulfilled`); //【C】
    })
    .catch((res) => {
      console.log(`${res} → rejected`); //【D】
    })
}
実行結果
> testPromise(5)
resolve → hoge → fuga → piyo → fulfilled

> testPromise(4)
reject → boo → piyo → fulfilled

ラップされたように見えるチェーン

関数で分割されていますが、挙動としては上記のSampleAと同じです。
【B】で解決済みのPromiseオブジェクトが返されることになる為、続いて【C】が実行され、【D】は実行されません。

SampleB
function createPromise(param) {
  const num = param | 0;
  return new Promise((resolve, reject) => {
    if (num % 2) {
      resolve('resolve');
    } else {
      reject('reject');
    }
  })
    .then((res) => {
      return `${res} → innerThen`; //【A】
    })
    .catch((res) => {
      return `${res} → innerCatch`; //【B】
    });
}

function testPromise(param) {

  createPromise(param)
    .then((res) => {
      console.log(`${res} → outerThen`); //【C】
    })
    .catch((res) => {
      console.log(`${res} → outerCatch`); //【D】
    });
}
実行結果
> testPromise(5)
resolve → innerThen → outerThen

> testPromise(4)
reject → innerCatch → outerThen

再帰関数de直列

予め用意した、取得データ検査用の関数のリストを渡して、再帰で順次実行します。

SampleC
(function testPromise(fncAry, param) {
  const doSomething = fncAry.shift();

  if (!doSomething) {
    return;
  }

  new Promise((resolve, reject) => {
    setTimeout(doSomething, 500, param, resolve, reject);
  })
    .then((res) => {
      console.log('Then:', res);
      testPromise(fncAry, res);
    })
    .catch((res) => {
      console.log('Catch:', res);
    });
}(
  [
    // 1番目
    (param, resolve, reject) => {
      resolve('hoge');
    },
    // 2番目
    (param, resolve, reject) => {
      const rum = Math.floor(Math.random() * 10 + 1);
      console.log('Number is ' + rum);

      if (rum % 2) {
        resolve(`${param} → fuga`);
      } else {
        reject(`${param} → booo`);
      }
    },
    // 3番目
    (param, resolve, reject) => {
      resolve(`${param} → piyo`);
    }
  ]
));
実行結果
>
Then: hoge
Number is 9
Then: hoge → fuga
Then: hoge → fuga → piyo

>
Then: hoge
Number is 4
Catch: hoge → booo

Promiseさん家のasync・await兄弟

es2017でasync/awaitが登場しました。
then()のチェーンを使うことなく、直列処理を実現できます。
async関数内でawait式を使うと、そこで処理を一時停止します。
再開するには、await式に渡されたPromiseオブジェクトが解決済みの状態になる必要があります。
再開時、await式はPromiseにラップされていた値を返します。つまりonFulfilledの引数で受け取る値と同じものが返されます。
Promiseオブジェクト以外のものを渡すと、その値をラップした解決済みのPromiseオブジェクトに変換される為、そのまま処理が続行します。
ただしawait以降の実行は非同期です。

async関数の戻り値はPromiseオブジェクトなので、then,catch,finallyメソッドが利用可能です。
挙動としては、上記のSampleBと似ています。
並列処理はこれまでどおりPromise.allを利用します。

async/await
function createPromise(param) {
  return new Promise((resolve, reject) => {
    setTimeout((result) => {
      // 値が0以下は却下
      if (result > 0) {
        resolve(result);
      } else {
        reject(`result:${result} is rejected`);
      }
    }, 500, (param | 0));
  });
}

async function testAsync(param) {
  // 1番目
  const paramA = await createPromise(param);
  console.log(paramA);
  // 奇数は却下
  if (paramA % 2) {
    // Promiseの静的関数の結果を返してcatchに進む
    return Promise.reject(`paramA:${paramA} is rejected`);
  }

  // 2番目
  const paramB = await createPromise(paramA * 2);
  console.log(paramB);
  // 3の倍数は却下
  if (!(paramB % 3)) {
    // エラーを発生させて終了させる
    throw new Error(`paramB:${paramB} is rejected`);
  }

  // 3番目
  // paramBよりrumが大きいとcreatePromise内で却下される
  const rum = Math.floor(Math.random() * 10 + 1);
  console.log(`${paramB} minus ${rum}`);
  const paramC = await createPromise(paramB - rum);
  console.log(paramC);

  // 4番目
  // 解決済みとしてそのまま処理が続行(非同期)
  const paramD = await 'wait a moment';
  console.log(paramD);

  // thenに進む
  return 'End!';
}

function testPromise(param) {
  testAsync(param)
    .then((res) => {
      // async関数内で戻された値を受け取る
      console.log(res);
    })
    .catch((err) => {
      // async関数内でPromiseの却下もしくはエラーの発行で実行される
      console.log(err);
    });
}
実行結果
> testPromise(0)
result:0 is rejected

> testPromise(7)
7
paramA:7 is rejected

> testPromise(6)
6
12
Error: paramB:12 is rejected

> testPromise(2)
2
4
4 minus 5
result:-1 is rejected

> testPromise(8)
8
16
16 minus 2
14
wait a moment
End!

async function | MDN
await | MDN

Promiseさんとは仲良くなれましたでしょうか。

参考情報

【javascript】Promiseについて学ぶ①
Promiseについて0から勉強してみた
【JavaScript】ちゃんと理解しておきたいPromiseの勘所など

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

外部ツール無しでSeleniumのような自動化を実現しよう

はじめに

セキュリティ上の理由により、プラグイン等のインストールを実施できない中で、
なんとかSeleniumのような動作を実現できないか調査した所、
少し無理矢理ながらもできたので、メモを残したいと思います。

課題整理

単純に画面へJavascriptを埋め込むのでは、遷移時にクリアされてしまいます。
では、遷移する度に埋め込むのか?そのような事では自動化とは言えません。
ということで、iframeにて操作したい画面を描画し、
親画面からcontentsメソッドを使用して操作する事にしました。

問題発生

試しに以下のスクリプトを実行した所、エラーが発生しました。

// 流したスクリプト
$("#iframeのid").contents().find("#操作したいtextのid").val("hoge");

// エラーメッセージ
VM183:1 Uncaught TypeError:$(...).contents is not function
   at<anonymous>1:11

存在するはずのメソッドが使用できない、、、?なぜ??
というわけで調査した所、どうやらクロスドメイン制約によるものという事がわかりました。
(参考サイト https://qiita.com/growsic/items/a919a7e2a665557d9cf4)

先人の方々も調査を行っているようで、既に対策も確認する事ができました。
(参考サイト http://shinimae.hatenablog.com/entry/2015/12/09/183057)

同じ方法でやってもなぁ、、、さて、どうするか。

斜め上の発想で対応

同一ドメインであれば問題ないのであれば、
一度、操作したいページを描画し、そのhtmlを書き換えてしまえばいいのでは?
※具体的にはbodyの中身をクリアし、そこへiframe要素を追加してそこで描画し、操作してみる。

というわけで試してみたところ、、、成功しました!
これはセキュリティ的に大丈夫なのか?という怪しい所を残しながらも突破できたので、とりあえずは良しとすることにしました。
※この手法を採用する際は、自己責任でおねがいします。

// 埋め込み時
$("body").html("<iframe id='hoge' src='操作したいurl' width='好きな幅' height='好きな高さ'/>");

// 操作時
$("#hoge").contents().find("#書き換えたいtextのid").val("test");

// 画面遷移判定
$("#hoge").load(function() {
  console.log("遷移完了");
}

最後に

アカウント情報等の利用者毎に事なる値を入力する運用に使用したい場合は、
html書き換え時にtext等を配置し、その値を使用するのも良いかと思います。
ただ、amazonなどは、iframeでの表示を制限した作りになっているようなので、注意ですね。

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

[JavaScript] 正規表現

正規表現

今回は正規表現の基礎的な部分を説明していきたいと思います。

正規表現とは

特定の文字列などを切り取ったり、置き換えたり、検索したりする技術です。

特に多く利用するケースが

・文字列を置き換える
・文字列を検索し抽出する
・条件にマッチしているか調べる

かと思います。
今回は上記の際によく利用するreplaceメソッドとmatchメソッドについて説明していきたいと思います。

replaceメソッド

replaceメソッドは読んだままの通り文字列を置き換える事ができます。
実際のコードで説明します。

正規表現.js
var name = "私はゆーいちです"
    name.replace(/ゆーいち/, "エンジニア");
   => "私はエンジニアです"

これで文字列を置き換える事ができました。
まずvarというのはJavaScript内で変数を定義するときの宣言のようなものです。
文字を置き換える時には、replaceメソッドの第一引数に置き換える部分の文字列を//の中に入れます。
そして第二引数に置き換えた後の文字列を指定します。

matchメソッド

matchメソッドは文字列の中に特定の文字列があるかどうかを確かめてくれるメソッドです。
実際のコードで説明していきます。

正規表現.js
var name = "私はゆーいちです"
    name.match(/ゆーいち/);
   => ["ゆーいち"]

  name.match(/エンジニア/)
   =>null

これで定義する事ができました。
特定の文字列を抽出する時には、matchメソッドの引数に抽出したい文字列を//の中に入れて定義します。
もしその文字列があればそのまま検索した文字列を配列で返し、なければnullを返します。

最後に

今回は正規表現の基礎的な部分を説明していきました。
今回説明したものは、とても簡単な例を紹介しましたが、実際はもっと複雑なものを抽出したりするので次回からもう少し掘り下げた内容を書いていきたいと思います。

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

JavaScriptで重複のあるソート済み配列から指定した値の範囲をバイナリサーチで取得する

ソート済み配列から指定した値を持つインデックスの範囲をバイナリサーチで取得する方法です。

binarysearchMinIndexは重複要素があるソート済み配列arrayから指定した値targetを持つ最も小さいインデックスを取得する関数です。引数のfromtoはサーチする配列の範囲を指定します。binarysearchMaxIndexbinarysearchMinIndexとは逆に指定した値を持つ最も大きいインデックスを取得する関数です。
binarysearchRangeが目的のソート済み配列arrayから指定した値targetを持つインデックスの範囲を取得する関数です。内部ではbinarysearchMinIndexbinarysearchMaxIndexを使用しています。

function binarysearchMinIndex(array, target, from, to) {
  while (from !== to) {
    const middle = from + Math.floor((to - from) / 2);
    if (array[middle] < target) {
      from = middle + 1;
    } else {
      to = middle;
    }
  }
  if (array[from] === target) {
    return from;
  } else {
    return -1;
  }
}

function binarysearchMaxIndex(array, target, from, to) {
  while (from !== to) {
    const middle = from + Math.ceil((to - from) / 2);
    if (array[middle] > target) {
      to = middle - 1;
    } else {
      from = middle;
    }
  }
  if (array[from] === target) {
    return from;
  } else {
    return -1;
  }
}

function binarysearchRange(array, target) {
  const from = binarysearchMinIndex(array, target, 0, array.length - 1);
  const to = from !== -1 ? binarysearchMaxIndex(array, target, from, array.length - 1) : -1;
  return { from: from, to: to };
}

通常のバイナリサーチのコードも載せておきます。重複要素のないソート済み配列arrayから指定した値targetのインデックスを取得します。もし指定した値の要素がない場合は-1を返します。

function binarysearch(array, target) {
  let from = 0;
  let to = array.length - 1;
  while (from <= to) {
    const middle = from + Math.floor((to - from) / 2);
    if (array[middle] < target) {
      from = middle + 1;
    } else if (array[middle] > target) {
      to = middle - 1;
    } else {
      return middle;
    }
  }
  return -1;
}

今回は目的のbinarysearchRange関数をその値を持つ最も小さなインデックスと最も大きなインデックスをそれぞれバイナリサーチで見つけるような実装をしました。同じ値を持つ要素が少ないことが分かっている場合、通常のバイナリサーチでその値を持つインデックスを見つけてからその前後を線形探索で調べるというアルゴリズムもあります。


ソースコードをgistに置いておきました。
https://gist.github.com/aadebdeb/964ff754d4045e2b7b1fea8044e7420d

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

javascriptでcookie操作

メモ

cookieとはブラウザに保存される文字情報
- 有効範囲は基本domain毎。さらに細かくpathごとに区切れる
- 有効期限も設定できる
- cookieを

取得

全てのcookieを取り出す

document.cookie;

上記だと全てのcookieが取り出されます。

こんな感じに
key=value;key=value;key=value;key=value;key=value;key=value;key=value;

特定のcookieだけ取り出したい

特定のcookieを取り出すには前述したやつを分解して、希望のkeyで探して取り出すしかない。

  • 一つだけ取り出す
パターン1
var cookies = document.cookie; //全てのcookieを取り出して
var cookiesArray = cookies.split(';'); // ;で分割し配列に

for(var c of cookiesArray){ //一つ一つ取り出して
    var cArray = c.split('='); //さらに=で分割して配列に
    if( cArray[0] == 'key名'){ // 取り出したいkeyと合致したら
        console.log(cArray);  // [key,value] 
    }
}

正規表現で特定のkeyの文字列をマッチしたものを全て取り出したい

  • アンダーバー(_)を使っているcookieを全て取り出す
パターン2
var cookies = document.cookie;
var cookiesArray = cookies.split(';');

for(var c of cookiesArray){
    var cArray = c.split('=');

    var pattern = /\_/;   // 正規表現の条件
    if( cArray[0].match(pattern) ){  // keyになる文字列が正規表現にマッチしたら
      console.log(cArray);
    }
}

cookieをセット

そのまま入れる

  • key=valueをdocument.cookieに入れる
そのまんまvalueをつっこむ
document.cookie = "hakkeyoi=のここった";  // "hakkeyoi=のここった"

エスケープして入れる

  • valueに encodeURIComponent や encodeURI といったメソッドを使ってcookieを保存
valueをエスケープ
document.cookie = "onakaga=" + encodeURIComponent('へった');  // "onakaga=%E3%81%B8%E3%81%A3%E3%81%9F"

document.cookie = "dokkoisho=" + encodeURI('よっこらせ'); // "dokkoisho=%E3%82%88%E3%81%A3%E3%81%93%E3%82%89%E3%81%9B"

とりあえずencodeURIComponentを使うらしい
encodeURIComponentの方がエスケープされる特殊文字が多い。

エスケープしものを取り出すときは

  • decodeURIComponentメソッドを使ってエスケープした文字を置き換える
var cookies = document.cookie;
var cookiesArray = cookies.split(';');

for(var c of cookiesArray){
    var cArray = c.split('=');
    if( cArray[0] == 'key'){ // 取り出したいkeyと合致したら
        console.log(decodeURIComponent(
cArray[1]));  // 元の文字列 = エスケープ文字を正しく置き換えた文字列
    }
}

有効期間

  • 10分の有効期限を設定してみる
  • 方法は2種類ある。

max-age

max-age有効期限の秒数で指定
var m = 60*10  // 秒で指定
document.cookie = "name=" + encodeURIComponent(name) + ";max-age=" + m.

expires

expires有効期限の日付で指定
var now = new Date();
now.setMinutes(now.getMinutes() + 10);  // 10分後
document.cookie = "name=" + encodeURIComponent(name) + ";expires=" + now.toUTCString();

cookie削除したい

前述した有効期間を0にするか、過去の日付を指定する

var cookies = document.cookie;
var cookiesArray = cookies.split(';');
var now = new Date();
now.setYear(now.getYear() - 1);
for(var c of cookiesArray){
    console.log(c);
    var cArray = c.split('=');
    document.cookie = cArray[0] + '=;max-age=0'
    document.cookie = cArray[0] + '=;expires=' + now.toGMTString();
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SkywayのJavaScript SDKの中身を読んでみよう

こんにちは pco2699というものです。
普段は自分のはてなブログに記事を書くのですが、たまにはQiitaに書いてみます。

この記事は Skyway Advent Calendar 2018の15日目の記事です。

やること

他のSkywayアドベントカレンダーだと、「つくってみた」系が多かったので
違う観点でSDKのコードリーディングとかしてみるのはどうだろう、と思いました。

以前、Skywayつかったことあるんですか結構SDKの中身を見てデバッグすることが多かったので
役立つと思います。

願わくば、将来的にはプルリクとか出したい...!!!

早速見てみよう

リポジトリはこちらです。
https://github.com/skyway/skyway-js-sdk

全部見るのは、流石に無理なので
今回はとりあえず、接続を確立する部分を見ていきたいと思います。

個人的には、WebRTCのAPIを呼びまくってる部分を見てみて
「ほぉ...WebRTCってやっぱよくわからんな...、Skyway偉大...!」という結論を出すまで読んでいきたいと思います。

リポジトリの構成

ざっとリポジトリは以下のような構成になってます。

.
├── CHANGELOG.md
├── examples // サンプルが入っている
├── karma.conf.js
├── LICENSE
├── package.json
├── package-lock.json
├── README.md
├── scripts // CI用のコード
├── src // SDKのソースコード
├── tests // テストコード
└── webpack.config.js

まずリポジトリ構成をみて面白かったのは

  • CI用のスクリプトがかなり手厚い
  • CI用のスクリプトもJavaScriptで書かれてる

という点でしょうか。
自分だったらCI用のスクリプトはbashとかで書いてしまうので、のっぴきならぬJS好きか、なにか環境制約があったのか、気になるところです。

package.jsonもみてみよう

せっかくなんで、依存しているライブラリがまとまっているpackage.jsonも見てみます。

  "devDependencies": {
    "babel-loader": "^7.1.4",
    "babel-plugin-espower": "^2.4.0",
    "babel-plugin-istanbul": "^4.1.6",
    "babel-preset-es2015": "^6.24.1",
    "del-cli": "^1.1.0",
    "eslint": "^5.0.0",
    "eslint-config-prettier": "^2.9.0",
    "eslint-plugin-prettier": "^2.4.0",
    "inject-loader": "^4.0.1",
    "karma": "^2.0.0",
    "karma-chrome-launcher": "^2.2.0",
    "karma-coverage": "^1.1.2",
    "karma-mocha": "^1.3.0",
    "karma-mocha-reporter": "^2.2.5",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-webpack": "^3.0.0",
    "mocha": "^5.2.0",
    "power-assert": "^1.4.4",
    "prettier": "^1.9.2",
    "sinon": "^6.0.1",
    "webpack": "^4.12.1",
    "webpack-cli": "^3.0.8"
  },
  "dependencies": {
    "detect-browser": "^4.2.0",
    "enum": "git+https://github.com/eastandwest/enum.git#react-native",
    "events": "^3.0.0",
    "js-binarypack": "0.0.9",
    "object-sizeof": "^1.3.0",
    "query-string": "^6.4.0",
    "sdp-interop": "^0.1.13",
    "sdp-transform": "^2.7.0",
    "socket.io-client": "^2.2.0"
  }

気になるパッケージはinject-loaderpower-assertですね。
調べてみたところinject-loaderはモックテスト用にimportするモジュールを差し替えられるライブラリ。
power-assertはテスト時のassertがFailしたときの内容をきれいに出してくれるライブラリ、ということで両方ともテスト用のライブラリでした。

さすがに、企業のSDKだけあってテストはしっかりしてそうです。(当たり前

いよいよ srcの中へ

というわけでソースコードが格納されているsrcの中を見ていきます。

.
├── peer
│   ├── connection.js
│   ├── dataConnection.js
│   ├── mediaConnection.js
│   ├── meshRoom.js
│   ├── negotiator.js
│   ├── room.js
│   ├── sfuRoom.js
│   └── socket.js
├── peer.js
└── shared
    ├── config.js
    ├── logger.js
    ├── sdpUtil.js
    └── util.js

srcの中はこんな感じになってます。

早速、最初のスタートポイントであるpeer.jsを見てみましょう。

peer.js
import EventEmitter from 'events';
import Enum from 'enum';

// (省略)
const PeerEvents = new Enum([
  'open',
  'error',
  'call',
  'connection',
  'expiresin',
  'close',
  'disconnected',
]);

/**
 * Class that manages all p2p connections and rooms.
 * This class contains socket.io message handlers.
 * @extends EventEmitter
 */
class Peer extends EventEmitter {
  /**
   * Create new Peer instance. This is called by user application.
   * @param {string} [id] - User's peerId.
   * (省略)
   */
  constructor(id, options) {
    super();

    this.connections = {};
    this.rooms = {};

    // messages received before connection is ready
    this._queuedMessages = {};

    if (id && id.constructor === Object) {
      options = id;
      id = undefined;
    } else if (id) {
      id = id.toString();
    }

    const defaultOptions = {
      debug: logger.LOG_LEVELS.NONE,
      secure: true,
      token: util.randomToken(),
      config: config.defaultConfig,
      // (省略)
    };

    this.options = Object.assign({}, defaultOptions, options);

    logger.setLogLevel(this.options.debug);
    // (省略)
  }

ES2015のクラス構文でめちゃくちゃキレイに書かれてますね。読みやすい!(素人の感想)
ここから、どこを見れば、最初の接続部分の処理になるのか簡単にわかりそうです。

ここで、APIのリファレンスを見てみて、どのAPIを呼べば接続開始するのか見てみましょう。
SkywayのAPIのリファレンス

Peer - JS SDK API Reference - Google Chrome 2019-03-30 00.12.28.png

peerのcallメソッドで、指定したpeerとメディアチャンネルをつくるようです。
APIの通り実際にコードの中でもMediaConnectionオブジェクトが作られていますね。
そして、作られたMediaConnectionオブジェクトのstartConnection()メソッドを呼んで
接続を開始しているようです。

peer.js
  /**
   * Creates new MediaConnection.
   * (省略)
   */
  call(peerId, stream, options = {}) {
    if (!this._checkOpenStatus()) {
      return;
    }

    options.originator = true;
    options.stream = stream;
    options.pcConfig = this._pcConfig;
    const mc = new MediaConnection(peerId, options);
    mc.startConnection();
    logger.log('MediaConnection created in call method');
    this._addConnection(peerId, mc);
    return mc;
  }

次はMediaConnectionのstartConnection()部のソースコードを見てましょう。

MediaConnection.js
  /**
   * Start connection via negotiator and handle queued messages.
   * @return {Promise<void>} Promise that resolves when starting is done.
   */
  async startConnection() {
    if (!this._options.originator) {
      return;
    }

    await this._negotiator.startConnection({
      type: 'media',
      stream: this.localStream,
      // 省略
    });

    this._pcAvailable = true;
    this._handleQueuedMessages();
  }

ふむふむ、どうやらnegotiatorstartConnectionを呼んでるみたいです。
次はnegotiatator内を見てみましょう。ここらへんからはAPIのリファレンスには説明があまり載ってこない世界になってきます。

negotiator.js
  async startConnection(options = {}) {
    this._pc = this._createPeerConnection(options.pcConfig);
    // 省略

    if (this._type === 'media') {
      if (options.stream) {
        if (this._isAddTrackAvailable && !this._isForceUseStreamMethods) {
          options.stream.getTracks().forEach(track => {
            this._pc.addTrack(track, options.stream);
          });
        } else {
          this._pc.addStream(options.stream);
        }
      } else if (this.originator) {
        // This means the peer wants to create offer SDP with `recvonly`
        const offer = await this._makeOfferSdp();
        await this._setLocalDescription(offer);
      }
    }
  /**
   * Create new RTCPeerConnection.
   * (省略)
   */
  _createPeerConnection(pcConfig = {}) {
    logger.log('Creating RTCPeerConnection');

    const browserInfo = util.detectBrowser();

    this._isAddTrackAvailable =
      typeof RTCPeerConnection.prototype.addTrack === 'function';
    this._isOnTrackAvailable = 'ontrack' in RTCPeerConnection.prototype;
    this._isRtpSenderAvailable =
      typeof RTCPeerConnection.prototype.getSenders === 'function';

    // 省略
    return new RTCPeerConnection(pcConfig);
  }

ここでようやくRTCPeerConnectionが出てきました!RTCPeerConnectionはWebRTCのAPIです。
WebRTCのAPIはMozillaのホームページなどに内容の説明が出てきます。
MozillaのWebRTC APIリファレンス

このnegotiatorクラスが主にWebRTC周りのAPIを呼んでいるクラスでした。

ここまで読んで、なんとなく、各ファイルのうっすら責務がわかってきました。
Untitled Diagram.png

まとめ

という感じでざっくり、SkywayのJavaScript SDKの中身で、接続開始のWebRTC APIを叩くところまで呼んでみました。
WebRTCを用いたチャットアプリ、ビデオチャットを作りたい場合には、
WebRTCの実装をうまいこと抽象化してくれているSkywayは非常に便利です。

「ほぉ...WebRTCってやっぱよくわからんな...、Skyway偉大...!」という当初の結論を変えず、本記事を締めたいと思います。
どうもありがとうございました。

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

Google Apps Script でクラスの継承を行う

概要

Google Apps Script で親クラスを継承した子クラスを作成するためのメモ。
Google Apps Script は ES6 非対応のため Object.setPrototypeOf() などが使用できない。

方法

Object.setPrototypeOf() の代わりに継承用の関数を作成して使用する。

継承用の関数を作成

inherits
function inherits(ctor, superCtor) {

  ctor.super_ = superCtor;

  ctor.prototype = Object.create(superCtor.prototype, {

    constructor: {

      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true

    }

  });

}

クラスの作成と継承

class
function parentClass(arg){

  /* コンストラクタ */

}

function childClass(arg){

  parentClass.call(this, arg); // 親クラスのコンストラクタを呼び出す

}

inherits(childClass, parentClass); // prototype を継承

コード例

test
function inherits(ctor, superCtor) {

  ctor.super_ = superCtor;

  ctor.prototype = Object.create(superCtor.prototype, {

    constructor: {

      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true

    }

  });

}

function test(){

  function parentClass(arg){

    this.property1 = arg;

  }

  parentClass.prototype.property2 = 'fuga';

  function childClass(arg){

    parentClass.call(this, arg);

  }

  inherits(childClass, parentClass);

  var instance = new childClass('hoge');

  Logger.log(instance.property1); // 'hoge'
  Logger.log(instance.property2); // 'fuga'

}

参考

ES5なJavascriptでモダンなクラス的継承&スーパー呼び出し
Google流 JavaScript におけるクラス定義の実現方法

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

ブラウザ拡張でのidle.onStateChangedの実行タイミングがWindows10でのみ異なる件

idle.onStateChangedとは

ブラウザの拡張機能で使用できる idle というAPIがあります。

chrome.idle - Google Chrome

私はChromeでしか試していないですが、一応、Firefoxなどでも実装されているみたいです。

idle - Mozilla | MDN

OSがスクリーンロック(Macだと⌘+Control+Q、WindowsだとWin+L)になったり、
一定時間操作が無かったかなどを取得できるAPIです。

サンプルコード

chrome.idle.onStateChanged.addListener((newState) => {
    // newState "active", "idle", or "locked"
});

onStateChanged というイベントにCallbackを指定することで、
状態変化時にコードを実行することができます。

用途

用途は様々に考えられますが、例えば、チャットのような拡張機能を作ったのであれば、
一定期間何も操作されていなかったり、スクリーンロックされたら離席状態にする、というような機能に使えます。

本題:Windows10における問題

Mac、Windows7、Windows10で動作するChrome Extensionを作った際に発覚した問題として、
Mac、Windows7においては、スクリーンロックの操作(ショートカットキーを押したり、メニューからロックを選んだり)した段階で、chrome.idle.onStateChangedのイベントは実行されます。

問題点

しかし、Windows10においては、スクリーンロックの操作を行った時点では chrome.idle.onStateChanged のイベントが実行されません。

Windows10からは、スクリーンロック操作を行った時点でロックスクリーン(綺麗な壁紙に日時が表示されているアレ)が表示されますが、
この時点ではonStateChangedイベントは発生せず、
任意のキーを押すなどしてログイン画面を表示した時点でonStateChangedイベントが発生します。

バグ報告

一応、この現象はChromiumのForumにもバグとして報告されているみたいですが…
https://bugs.chromium.org/p/chromium/issues/detail?id=524663

個人的には、
* Chrome側は「OSの通知してくるスクリーンロックのイベントを拾ってるだけだから仕様です」
* Windows側は「即イベントを発生しないのは仕様です」

…って言いそうな気がするので、OS・ブラウザ側のバージョンアップでは対応されないのではないかと予測しています。

対応方法

上記の通り、恐らくこの動作は「仕様」であるため完全に対応することはできません。
しかし、Windows10側の設定を変更し、ロックスクリーンを無効にすることでWindows7と同じ挙動にすることが可能です。

ロックスクリーンを無効にする方法

以下のサイトに手順がまとめられています。

Windows10でロック画面を無効または有効にする方法 – iBitzEdge

いくつかの方法がありますが、今回、私の場合はWindows10 Homeだったため、
レジストリエディタで値を追加することを選択しました。

また、対象のPCが複数台あったため、バッチファイルを作成し対応しました。

ロックスクリーンを無効にするコマンド

reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Personalization" /v NoLockScreen /t REG_DWORD /d 1 /f

上記を記述したバッチファイルを作成し、
PC上でバッチファイルを「管理者として実行」することで、レジストリに書き込まれます。

なお、管理者として実行しなかった場合は、「エラー: アクセスが拒否されました。」と表示されます。

おまけ:他に考えたけど採用しなかった対応方法

一定時間でactiveからidleになったらlockedと同じ状態として扱う

chrome.idle.setDetectionInterval()idleになるまでの秒数を設定することができるので、
ある程度はこの方法でもまかなえるかと思います。

ただ、今回の要件では、どうしてもロック時で正確に状態を取りたかったので採用しませんでした。

WindowsAPIを直接使ってどうにかする

昔はExtensionでDLLを使ったりできたので可能だったかもしれませんが…今は不可能なので諦めました。
Electronアプリで構築していた場合などは、もしかすると可能かもしれません。

詳しくどのAPIを使えば良いのかなどは調べていないので、本当に可能かどうかは不明です。

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