これをこうした (Viewのリファクタ)

命名、フォルダ構造、Viewと機能(性質)をごっちゃにしないのは大切ですよ、という反省。

f:id:ryotah:20170519233751p:plain

フォルダ構造

├── components
│   ├── baz
│   │   ├── abstract
│   │   ├── a
│   │   └── b
│   ├── bazes-container
│   ├── main
│   │   └── bg
│   ├── qux
│   │   ├── abstract
│   │   ├── a
│   │   └── b
│   ├── quxes-container
│   └── utils.ts
├── container.ts
└── services
<main>
  <!-- bg -->
  <main-bg>
  </main-bg>
  <!-- bazes -->
  <bazes-container>
    <baz-a ng-repeat="..."></baz-a>
    <baz-b ng-repeat="..."></baz-b>
  </bazes-container>
  <!-- quxes -->
  <quxes-container>
    <qux-a></qux-a>
    <qux-b ng-repeat="..."></qux-b>
  </quxes-container>
</main>

変更前

フォルダ構造

├── components
│   ├── a-baz
│   ├── a-bazes-container
│   ├── a-qux
│   ├── bazes-container
│   ├── main
│   ├── b-baze
│   ├── b-bazes-container
│   ├── qux
│   └── quxes-container
├── container.ts
└── services
<main>
  <bazes-container>
    <!-- a(s) -->
    <a-bazes-container>
      <a-baz ng-repeat="..."></a-baz>
      <a-qux></a-qux>
    </a-bazes-container>
    <!-- b(s) -->
    <b-bazes-container>
      <b-baze ng-repeat="..."></b-baze>
      <quxes-container>
        <qux ng-repeat="..."></qux>
      </quxes-container>
    </b-bazes-container>
  </bazes-container>
</main>

Hubotを使ってGoogle AnalyticsのデータをSlackに流す

1年ほど前にまとめたものです。


botつくりです。

slackhq/hubot-slack · GitHub

環境準備

(公式ページ通りですが)

hubot, coffee-script, yo, generator-hubotをグローバル環境にインストールします。

npm install -g hubot coffee-script yo generator-hubot

yoでベースをつくります。

yo hubot

Bot adapterをslackにします。

? Bot adapter slack

起動

起動の前にHubot用のSlack Tokenが必要です。 SlackのIntegrationsからHubotを追加してトークンを取得してください。

起動は以下コマンドで。

HUBOT_SLACK_TOKEN=xoxb-1234-5678-91011-00e4dd ./bin/hubot --adapter slack

Slackでbotのステータスがオンラインになれば、ひとまず成功です。

いくつか試してみたい場合はscripts/example.coffeeが便利です。 例えば以下部分のコメントアウトをはずします。

robot.hear /badger/i, (msg) ->
    msg.send "Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS"

badgerと話しかけると、返信してくれるようになります。

Herokuで動かす

Gitの初期化がまだの場合、git initしておきます。

create

heroku create my-company-slackbot

config

heroku config:add HUBOT_HEROKU_KEEPALIVE_URL=https://my-company-slackbot.herokuapp.com
heroku config:add HUBOT_SLACK_TOKEN=xoxb-1234-5678-91011-00e4dd

push

git push heroku master

これで完了です。

うまく動かない場合は、Procfileを確認してみてください。

web: bin/hubot -a slack

HUBOT_HEROKU_KEEPALIVE_URL

configで設定しているこの項目は、HubotをHeroku上で起動させ続けるために必要になります。

hubot-scripts/hubot-heroku-keepalive: A hubot script that keeps the hubot Heroko web dyno alive

Google Analyticsの情報を取得

GoogleのNode.js client libraryを利用します。

google/google-api-nodejs-client: Google’s officially supported Node.js client library for accessing Google APIs.

install

npm install googleapis --save

OAuth 2.0を利用した認証と各種APIの利用が可能です。 今回はサーバー上で認証をおこなうため、JWT (Service Tokens) を利用します。

Google Developersの設定

googleapisを利用するためにGoogle Developersの設定をします。

Google Developers Console から以下2つの設定を行います。 (「プロジェクト」がない場合は、先に作成を済ませてください。)

1. APIを有効にする

[API Manager] –> [概要] からAnalytics APIを有効にします。

2. サービス アカウント キーを作成する

[API Manager] –> [認証情報] から新しいサービス アカウント キーを作成します。キーのタイプはJSONにします。

認証

まだAnalyticsにアクセスはできませんが、認証処理がうまくいくか先に確認してみます。

var google = require('googleapis');
var key = require('path/to/key.json');
var jwtClient = new google.auth.JWT(key.client_email, null, key.private_key, ['https://www.googleapis.com/auth/analytics'], null);

jwtClient.authorize(function(err, tokens) {
  if (err) {
    console.log(err);
    return;
  }
  console.log(tokens)
});

key.jsonはアカウントキー作成時にダウンロードしたJSONファイルです。 認証処理がうまくいけば、tokensの情報が確認できます。

補足: Scopeについて

['https://www.googleapis.com/auth/analytics']の部分には必要なScopeを設定します。 Google Analyticsを利用するだけならこのままで大丈夫です。

OAuth 2.0 Scopes for Google APIs  |  Google Identity Platform  |  Google Developers

Analyticsの設定

Analytics側の設定を確認します。

1. ビューIDの確認

[アナリティクス設定] –> [ビュー] –> [ビュー設定] から「ビュー ID」の確認をします。

2. メールアドレスの追加

[アナリティクス設定] –> [ビュー] –> [ユーザー管理] から「表示と分析」が可能なメールアドレスを追加します。

Developer Consoleから取得したJSONclient_emailを利用します。

Analyticsのデータを取得

ここまで準備ができたら Core Reporting API を利用してデータの取得が可能です。

analytics.data.ga.getメソッドを利用してデータのリクエストを行います。

var google = require('googleapis');
var key = require('path/to/key.json');
var jwtClient = new google.auth.JWT(key.client_email, null, key.private_key, ['https://www.googleapis.com/auth/analytics'], null);
var analytics = google.analytics('v3');
var viewId = 'xxx'; // 設定画面で確認した「ビューID」

function authTask() {
  return new Promise((resolve, reject) => {
    jwtClient.authorize((err, result) => {
      if (err) {
        console.log('authTask/reject');
        reject(err);
      } else {
        console.log('authTask/resolve');
        resolve(result);
      }
    });
  });
}

function gaTask(token, viewId) {
  return new Promise((resolve, reject) => {
    analytics.data.ga.get({
      'ids': 'ga:' + viewId,
      'start-date': '7daysAgo',
      'end-date': 'today',
      'metrics': 'ga:pageviews',
      'dimensions': 'ga:pagePath',
      'sort': '-ga:pageviews',
      'access_token': token
    }, (err, result) => {
      if (err) {
        console.log('gaTask/reject');
        reject(err);
      } else {
        console.log('gaTask/resolve');
        resolve(result);
      }
    });
  });
}

module.exports = robot => {
  robot.respond(/hi$/i, msg => {
    authTask().then(result => {
      return gaTask(result.access_token, viewId);
    })
    .then(result => {
      console.log(JSON.stringify(result));
    })
    .catch(err => {
      console.log(err);
    });
  });
};

botに対してhiと話しかけると、過去7日間のページビューランキングを取得します。

ページビューランキング以外のデータもanalytics.data.ga.getを利用して取得します。下記リンク先を参考にしてみてください。

その他

取得した情報を特定のルームに流す

robot.send({ room: 'roomname' }, message);

#roomnameのように#をつけると、3系からはエラーになります。

自動実行させる

ncb000gt/node-cron

z-indexとスタック文脈

重なり順の制御をしているときに、スタック文脈というものを理解していなかったことに気づきました。 以下の記事がわかりやすかったです。MDNにはいつもお世話になります。ありがたい。

CSS の z-index の理解 - CSS | MDN

  1. z-index なしのスタック : デフォルトのスタック規則
  2. スタックとフロート : フロート要素の扱われ方
    • フロート(浮遊)ブロックでは、積み重ね順が少し違います。フロートブロックは位置指定されていないブロックとされているブロックの間に置かれます:
      1. ルート要素の背景とボーダー
      2. 通常フローに乗る子孫要素。HTML 内の出現順
      3. 浮遊ブロック
      4. 通常フローに乗るインラインの子孫要素
      5. 位置指定された子孫要素。HTML 内の出現順
  3. z-indexの追加 : z-index を使ってデフォルトのスタックを変える
  4. スタックの文脈 : スタック文脈についての覚書
    • スタック文脈の内部で、子要素は前に説明したルールに従って積み重なります。重要なのは、子要素の z-index 値は、その親要素に対してのみ意味を持つということです。スタック文脈は、その親のスタック文脈では不可分な一つの固まりとして扱われます。
  5. スタック文脈の例 1 : 2レベルの HTML 階層構造、最終レベルで z-index を使う
  6. スタック文脈の例 2 : 2レベルの HTML 階層構造、すべてのレベルで z-index を使う
  7. スタック文脈の例 3 : 3レベルの HTML 階層構造、2番めのレベルで z-index を使う

(WIP) Pub-Sub

(1年前に途中までまとめたもの。こっちに転載。)


AngularJS: Notifying about changes from services to controllers - codelord.net を参考にStoreっぽいものを実装しようとした時のメモ

// component
class FooController {
  constructor(
    private $scope: ng.IScope,
    private NotifyingService: NotifyingService,
    ) {
  }
  public $onInit() {
    this.NotifyingService.subscribe(this.$scope, () => {
      this.doSomething();
    });
  }
  private doSomething() {
    // do something
    // 
  }
}
const FooComponent: ng.IComponentOptions = {
  template: '...'
  controller: FooController,
  bindings: {
    // ...
  }
};

// service
const EVENT_NAME: string = 'notifying-service-event';
class NotifyingService {
  $data: any;
  constructor(private $rootScope: ng.IRootScopeService) {
  }
  public initialize() {
    // this.$data = new Data();
  }
  public subscribe(scope: ng.IScope, callback: (any) => void) {
    const handler = this.$rootScope.$on(EVENT_NAME, callback);
    scope.$on('$destroy', handler);
  }
  public update(data, noti: boolean = true) {
    const prev = angular.copy(this.$data);
    angular.extend(this.$data, data);
    if (!angular.equals(prev, this.$data)) {
      if (noti) {
        this.notify();
      }
    }
  }
  private notify() {
    this.$rootScope.$emit(EVENT_NAME);
  }
}

angular.module('app', [])
.component('foo', FooComponent)
.service('NotifyingService', NotifyingService);
  • $destroyがある
  • $emitしか利用しない
  • eventの挙動は隠蔽されている

簡易版

angular.module('app').controller('TheCtrl', function(NotifyingService) {
  NotifyingService.subscribe(function somethingChanged() {});
});
angular.module('app').factory('NotifyingService', function() {
  var onChanges = [];
  return {
    subscribe: function subscribe(callback) {
      onChanges.push(callback);
    },
    notify: function notify() {
      onChanges.forEach(function(cb) { cb(); });
    }
  };
});
  • 「変更した」というアクションに限定
  • $scope, $rootScopeが不要
  • React • TodoMVC に似ている
  • 複数のCtrlから利用するときに、個別リスナー解除ができない

実際にやろうとして失敗したこと

f:id:ryotah:20170517004339p:plain
(構成)

f:id:ryotah:20170517183037p:plain
(例: destory時の流れ)

  • ui-viewがまだコンポーネント対応していなかった
    • inputs/outputsにほうがシンプルでよかったかも
  • storeの並列が一番無理があった
    • 両方で同じようなデータを利用するとき
      • 両方に持つのも、主従にするのも…
    • 初期化のタイミング、更新順
  • containerとstoreのデータ被り
    • templateで利用する必要があるため、多数の変数をcontainer上で展開
    • (これは $data: FooData; みたいにすればよかったのかもしれない↓)
class FooController {
  $data: FooData;
  constructor(
    private $scope: ng.IScope,
    private FooStateService: ParentStateService
    ) {
  }
  public $onInit() {
    this.FooStateService.initialize();
    this.FooStateService.subscribe(this.$scope, () => {
      this.$data = this.FooStateService.data;
    })
    this.$data = this.FooStateService.data;
  }
  public handleUpdateBar($event: { value: number }) {
    this.ParentStateService.update({
      bar: $event.value
    });
  }
}

マルチバイト文字, fromCharCode, charCodeAt, Unicode

マルチバイト文字かどうか確認

/**
 * UTF-8における、1Byte以外の文字かどうか
 * -> 0-127 (ASCII 文字セット) 以外
 */
function hasMultibyte(str) {
  return /[^\u0000-\u007f]/.test(str);
}
hasMultibyte('a');
// -> false
hasMultibyte('>');
// -> false
hasMultibyte('ÿ');
// -> true
hasMultibyte('あ');
// -> true

fromCharCode

String.fromCharCode() - JavaScript | MDN

returns a string created by using the specified sequence of Unicode values

String.fromCharCode(0x3e)
// -> '>'
String.fromCharCode(62)
// -> '>'
(62).toString(16)
// -> '3e'

charCodeAt

String.prototype.charCodeAt() - JavaScript | MDN

returns an integer between 0 and 65535 representing the UTF-16 code unit at the given index

'>'.charCodeAt(0)
// -> 62
'\u003E'.charCodeAt(0)
// -> 62
'>'.charCodeAt(0).toString(16)
// -> '3e'

Unicode

その他参考

(WIP) Cloud Node.js Clientを利用してBigQueryのクエリ結果を取得

0. 動機

  • BigQueryの実行結果を整形・表示・チャート化しやすい環境をつくる
  • ほぼ自分用

1. Nodeからクエリを実行

GitHub - GoogleCloudPlatform/google-cloud-node: Google Cloud Client Library for Node.js

準備

npm install --save @google-cloud/bigquery

index.js

const config = {
  projectId: 'foo',
  keyFilename: '/path/to/keyfile.json'
};
const bigquery = require('@google-cloud/bigquery')(config);

const query = `
SELECT
  id
FROM
  [foo:bar]
LIMIT
  10
`;

bigquery.query(query)
.then(data => {
  const rows = data[0];
  console.log(rows);
})
.catch(err => {
  console.log(err);
  res.json(err);
});

2. Express上で動かす

Express application generator

const express = require('express');
const router = express.Router();
const path = require('path');
const config = {
  projectId: 'foo',
  keyFilename: path.resolve('/path/to/keyfile.json')
};
const bigquery = require('@google-cloud/bigquery')(config);

router.get('/', (req, res, next) => {

  // クライアントからクエリを受け取る
  const query = req.query.query;
  bigquery.query(query)
  .then(data => {
    const rows = data[0];
    res.json({ data: rows })
  })
  .catch(err => {
    res.json(err);
  });
});

module.exports = router;

自動再起動

  • nodemon

CORS

app.use(function (req, res, next) {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type');
  res.setHeader('Access-Control-Allow-Credentials', false);
  next();
});

3. クライアント側でデータの整形と表示

  • ロジックテスト
  • HighCharts