webpackさわる

環境は webpack 2.2.1。

コード分割

アプリケーション用のjs (app.js)と、ライブラリをまとめたjs (vendor.js)を分割させたい場合。

webpack.config.js

var path = require('path');
var webpack = require('webpack');
module.exports = {
  entry: {
    app: './src/app',

    // ここで設定したライブラリは、利用していなくても読み込まれるので注意
    vendor: [
      'lodash',
      'moment-timezone',
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: "[name].js",
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: Infinity
    })
  ]
};

動的読み込み

  • require.ensureを使えばおk
  • 古めのブラウザに対応させる場合、PromiseのPolyfillが必要なので注意
    • e.g. import 'es6-promise/auto';
  • オフィシャルドキュメント

webpack.config.js

var path = require('path');
var webpack = require('webpack');
module.exports = {
  entry: {
    app: './src/app',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),

    // webpack.js.org/guides/code-splitting-require/#example
    // > output.publicPath is an important option when using code-splitting, it is used to tell webpack where to load your bundles on-demand, see the configuration documentation.
    publicPath: '/app/',

    filename: '[name].js',
    chunkFilename: '[name].js'
  }
};

app.js

// a.jsとb.jsを読み込む
// require.ensureはPromiseを返す
require.ensure(['./a', './b'], require => {
  // a.jsを実行
  // (requireしていないので、b.jsは実行されないまま)
  require('./a');
  console.log('loaded');
}, 'chunk');

TypeScriptを使う場合はrequireを定義する必要がある

// https://github.com/TypeStrong/ts-loader#loading-other-resources-and-code-splitting
declare var require: {
  <T>(path: string): T;
  (paths: string[], callback: (...modules: any[]) => void): void;
  ensure: (paths: string[], callback: (require: <T>(path: string) => T) => void) => void;
};

その他

moment読み込み時に全てのlocaleファイルを読み込んでしまう

  • ContextReplacementPluginを利用して回避
var webpack = require('webpack');
module.exports = {
  // ...
  plugins: [
    // moment/locale/ja.jsのみ読み込み
    new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /ja/)
  ]
};

シェルスクリプトを利用して、tsファイルの先頭にimport文を差し込む

tsファイル内に文字列angularが存在する場合、ファイルの先頭にimport * as angular from 'angular;を追加。

# 改行コード
LF=$'\\\x0A'

# 結果を配列に
files=(`find ./client/{app,components} -type f -name "*.ts" -print0 | xargs -0 grep "angular" -l`)

for i in "${files[@]}"

# 各ファイルの1行目に差し込む
do sed -i '' -e "1s/^/import * as angular from 'angular';"$LF"/" $i;

# 終了
done;

参考

GithubのLabel設定 Export, Import

Getting Started | GitHub Developer Guide

Create an OAuth token

2段階認証をしている場合。

curl -i -u ${your_username} -H "X-GitHub-OTP: ${your_2fa_OTP_code}" -d '{"scopes": ["repo"], "note": "labels"}' https://api.github.com/authorizations

https://github.com/settings/tokens からでもおk

Export

curl -i -H "Authorization: token ${token}" https://api.github.com/repos/:owner/:repo/labels

https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository

Import

curl -i -H "Authorization: token ${token}" -d '{ "name": "foo", "color": "ff0000" }' https://api.github.com/repos/${owner}/${repo}/labels
...

https://developer.github.com/v3/issues/labels/#create-a-label

テーブルとデータ

これは何か

データテーブルを表示する場合、どのようなデータの型が利用しやすいか、について整理してみました。

参考にしたのは以下2つのライブラリ

以下のようなテーブルを表示したい。

ID Date Name Age
1 March 5, 2017 Foo 26
2 March 6, 2017 Bar 31

データのみ用意

配列

const data = [
  [1, 'March 5, 2017', 'Foo', 26],
  [2, 'March 6, 2017', 'Bar', 31]
];
  • 一番シンプル
  • カラムタイトルなどは別途用意する必要がある(Htmlのテンプレート内など)

オブジェクト

const data = [
  { id: 1, date: 'March 5, 2017', name: 'foo', age: 26},
  { id: 2, date: 'March 6, 2017', name: 'bar', age: 31},
];
  • 1行目を利用して、カラムタイトルを作成
  • オブジェクトだからカラム項目の順番は保証できないはず

データとカラム用の設定データ

  • 汎用性が高いのはこっち

配列

const data = [
  [1, 'March 5, 2017', 'Foo', 26],
  [2, 'March 6, 2017', 'Bar', 31]
];
const columns = [
  { name: 'ID'},
  { name: 'Date'},
  { name: 'Name'},
  { name: 'Age'}
];

オブジェクト

const data = [
  { id: 1, date: 'March 5, 2017', name: 'foo', age: 26},
  { id: 2, date: 'March 6, 2017', name: 'bar', age: 31},
];
const columns = [
  { field: 'id', name: 'ID' },
  { field: 'date', name: 'Date' },
  { field: 'name', name: 'Name' },
  { field: 'age', name: 'Age' }
];
  • カラムタイトル(name)とプロパティ名(field)を別にできる
const data = [
  { id: 1, date: 'March 5, 2017', customer: { name: 'foo', age: 26 } },
  { id: 2, date: 'March 6, 2017', customer: { name: 'bar', age: 31} },
];
const columns = [
  { field: 'id', name: 'ID' },
  { field: 'date', name: 'Date' },
  { field: 'customer.name', name: 'Name' },
  { field: 'customer.age', name: 'Age' }
];
  • データのネストなども対応可能

複雑なテーブル

カラム設定データを用意する場合、以下の機能の実現も汎用的にできそう。

  • ソート機能のオンオフ
  • 合計値の表示
  • 平均値の表示
  • データと表示内容の変更
    • 2017-03-05 -> March 5, 2017 or 2017年3月5日

UI-Gridだとこのような感じで設定を追加しています。

// http://ui-grid.info/docs/#/tutorial/105_footer

$scope.gridOptions = {
    showGridFooter: true,
    showColumnFooter: true,
    enableFiltering: true,
    columnDefs: [
        { field: 'name', width: '13%' },
        { field: 'address.street',aggregationType: uiGridConstants.aggregationTypes.sum, width: '13%' },
        { field: 'age', aggregationType: uiGridConstants.aggregationTypes.avg, aggregationHideLabel: true, width: '13%' },
        { name: 'ageMin', field: 'age', aggregationType: uiGridConstants.aggregationTypes.min, width: '13%', displayName: 'Age for min' },
        { name: 'ageMax', field: 'age', aggregationType: uiGridConstants.aggregationTypes.max, width: '13%', displayName: 'Age for max' },
        { name: 'customCellTemplate', field: 'age', width: '14%', footerCellTemplate: '<div class="ui-grid-cell-contents" style="background-color: Red;color: White">custom template</div>' },
        { name: 'registered', field: 'registered', width: '20%', cellFilter: 'date', footerCellFilter: 'date', aggregationType: uiGridConstants.aggregationTypes.max }
    ],
    data: data,
    onRegisterApi: function(gridApi) {
            $scope.gridApi = gridApi;
    }
};

angular-translate読み直し

半年前にまとめたもの。こっちに転載。


これは何か

設定例

/*@ngInject*/
export function translateConfig($translateProvider) {
  $translateProvider

  // angular-translate-loader-static-files
  // assets/lang/[langKey].json から非同期で言語ファイルを読み込む
  .useStaticFilesLoader({
    prefix: 'assets/lang/',
    suffix: '.json'
  })

  // 言語keyの設定
  .registerAvailableLanguageKeys(['en', 'ja'], {
    // en_US などは en と認識させる
    'en_*': 'en',
    'ja_*': 'ja',
    // そのほかの言語keyは全て en にする
    '*': 'en'
  })

  // ブラウザ環境から優先言語を自動取得
  // (以前に設定されたkeyがあれば、そちらを優先する <- angular-translate-storage-local利用時)
  .determinePreferredLanguage()

  // enable escaping of HTML
  // https://angular-translate.github.io/docs/#/guide/19_security
  .useSanitizeValueStrategy('escape')

  // angular-translate-handler-log
  .useMissingTranslationHandlerLog()

  // angular-translate-storage-local
  .useLocalStorage();
}

以下3つのextensionを利用している

  • angular-translate-loader-static-files
    • 非同期読み込み
  • angular-translate-handler-log
    • 対応するメッセージがない場合にエラーを表示
  • angular-translate-storage-local
    • 前回選択した言語コードを保存
    • (fallback で cookie を利用しているので angular-translate-storage-cookie も読み込まれる)

逆引き

言語ファイル(JSON)の書き方

  • 通常:
{
  "TRANSLATION_ID": "This is a concrete translation for a specific language."
}
  • ショートカット:
// bar.foo でアクセス可
{
  "bar": {
    "foo": {
      "foo": "This is my text."
    }
  }
}
  • リンク(いい機能だなあ):
{
  "SOME_NAMESPACE": {
    "OK_TEXT": "OK"
  },
  "ANOTHER_NAMESPACE": {
    "OK_TEXT": "@:SOME_NAMESPACE.OK_TEXT"
  }
}

参考: https://angular-translate.github.io/docs/#/guide/02_getting-started

$translate利用時に、非同期読み込みされたメッセージを反映

  • Two-way binding されないので 明示的に処理を書く必要がある
    • $translateChangeSuccessを利用
/*@ngInject*/
app.controller('Ctrl', ($scope, $translate, $rootScope) => {
  $rootScope.$on('$translateChangeSuccess', () => {
    $translate('HEADLINE').then(
      // 成功
      translation => $scope.headline = translation,
      // 失敗
      translationId => $scope.headline = translationId
    );
  });
});

参考: https://angular-translate.github.io/docs/#/guide/03_using-translate-service

変数を利用

言語JSONファイル:

{
  "TRANSLATION_ID": "{{username}} is logged in."
}

$translate利用時

$translate('TRANSLATION_ID', { username: 'PascalPrecht' });

$filter利用時

パターン1:

<div>{{ 'TRANSLATION_ID' | translate:'{ username: "PascalPrecht" }' }}</div>

パターン2:

<div>{{ 'TRANSLATION_ID' | translate:translationData }}</div>
angular.module('myApp')
/*@ngInject*/
.controller('Ctrl', ($scope) => { 
  $scope.translationData = {
    username: 'PascalPrecht'
  };
});

そのほかの方法もある

参考: https://angular-translate.github.io/docs/#/guide/06_variable-replacement

設定されている言語を取得

$rootScope.$on('$translateChangeEnd', (event, args) => {
  // args.language
});
// or
$translate.proposedLanguage() ||  $translate.use()
// proposedLanguage() -> 非同期読み込み中の言語keyを取得

利用言語を設定する

  • preferredLanguageを利用
  • ブラウザ環境から優先言語を自動取得する場合はdeterminePreferredLanguageを利用
    • (内部的にnavigator.languages[0] navigator.languageなどを利用している)

参考: https://angular-translate.github.io/docs/#/guide/07_multi-language

en_US en_SGなどをenとして扱う

  • registerAvailableLanguageKeysを利用

参考: https://angular-translate.github.io/docs/#/guide/09_language-negotiation

Unit Testing

非同期読み込みをしている場合、ローダー書き換える

beforeEach(module('app', ($provide, $translateProvider) => {
  $provide.factory('customLoader', $q => 
    () =>  $q.resolve()
  );
  $translateProvider.useLoader('customLoader');
}));

アプリを起動させる時の流れ

  • we register a asynchronous loader
  • we define our preferred language
  • $translate service is instantiated the first time it gets injected
  • angular-translate notices that there’s no language locally available
  • it looks if there’s a registered asynchronous loader
  • the asynchronous loader is called with the preferred language locale
  • the translation data is loaded and ready to be used

参考: https://angular-translate.github.io/docs/#/guide/22_unit-testing-with-angular-translate

webpack2にした

Migrating from v1 to v2

今の開発環境ではwebpackに任せているタスクが少ないため、かなり簡単に移行できた。

grunt-webpackを利用してるのでこれもv2に変更。
GitHub - webpack-contrib/grunt-webpack: integrate webpack into grunt build process

webpackで処理が止まってしまったので、keepalive: trueを追加して完了。
(以前から必要だったような気もしないではない)