B-Teck!

お仕事からゲームまで幅広く

【JavaScript/AngularJS】今更AngularJSに入門したメモ

AngularJSをサクッと覚える必要があったので勉強したメモ。
生のJSで扱おうと思うとアロー関数使うとうまく動かない箇所があったので、昔ながらの無名関数で書く。
中で呼び出すスクリプト等は学習時に利用した教材に合わせているため古い。
記事中のコードはここにあります

最小限のアプリケーションの作成

AngularJSの仕組みを使って画面に要素を表示するまで。

<!DOCTYPE html>
<!--  ng-appでsampleAppモジュールを関連付け  -->
<html lang="ja" ng-app="sampleApp">
<head>
    <meta charset="UTF-8">
    <title>sample</title>
    <!--  AngularJSの読み込み  -->
    <script src="//code.angularjs.org/1.5.0/angular.min.js"></script>
    <script src="app.js"></script>
    <!--  bootstrapの読み込み  -->
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"/>
</head>
<body>
<!--  ng-controllerでsampleControllerを関連付け  -->
<div ng-controller="sampleController">
    <!--  sampleControllerのScopeのmsgを出力  -->
    <h2>{{ msg }}</h2>
</div>
</body>
</html>
// sampleAppモジュールを作成
const sampleApp = angular.module('sampleApp', []);

// sampleAppモジュールにsampleControllerを関連付ける
// AngularJSの機能で$scopeサービスを利用してコントローラーに紐づく変数を定義する
sampleApp.controller('sampleController', ['$scope', function ($scope) {
    $scope.msg = 'Hello, World!';
}]);

ng-modelによる双方向バインディング

操作によって変更された値を即座に画面に反映する双方向バインディングの仕組みがある。
テキストボックスの操作に応じて表示が変化するのを確かめられるサンプル。

<!DOCTYPE html>
<html lang="ja" ng-app="sampleApp">
<head>
    <meta charset="UTF-8">
    <title>sample</title>
    <script src="//code.angularjs.org/1.5.0/angular.min.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"/>
</head>
<body>
<div ng-controller="sampleController">
    <h2>{{ msg }}</h2>
    <!--  ng-modelによるbindingで入力に対応して表示が変わる  -->
    <input type="text" ng-model="msg">
</div>
</body>
</html>

ngRouteを使ったルーティング

AngularJSではURLの#より後ろの文字列をpathとしてルーティングを行うことができる。

<!DOCTYPE html>
<html lang="ja" ng-app="sampleApp">
<head>
    <meta charset="UTF-8">
    <title>sample</title>
    <script src="//code.angularjs.org/1.5.0/angular.min.js"></script>
    <!--  ルーティング用のモジュールを読み込む  -->
    <script src="//code.angularjs.org/1.5.0/angular-route.min.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"/>
</head>
<body>
<!--  ルーティング結果を埋め込む場所をng-viewでマークする  -->
<div ng-view></div>
<!--  切り替えを確認するためにボタンを置く  -->
<a href="#/" class="btn">Sample</a>
<a href="#/sample2" class="btn">Sample2</a>
</body>
</html>
const sampleApp = angular.module('sampleApp', ['ngRoute']);

sampleApp
    .controller('sampleController', ['$scope', function ($scope) {
        $scope.msg = 'Hello, World!';
    }])
    .controller('sampleController2', ['$scope', function ($scope) {
        $scope.msg = 'Hello, NewWorld!';
    }]);


// #/ の場合と、 #/sample2の場合のルーティングを設定する
sampleApp.config(['$routeProvider', function ($routeProvider) {
    $routeProvider
        .when('/', {
            template: '<h2>{{ msg }}</h2>',
            controller: 'sampleController'
        })
        .when('/sample2', {
            template: '<h2>{{ msg }}</h2>',
            controller: 'sampleController2'
        })
}]);

ルーティング時にpathからパラメータを渡す

ルーティングのpathにパターンを定義して、処理の中で値として利用することができる。
パターンはいくつかあるけど、単純なケースのサンプル。

<!DOCTYPE html>
<html lang="ja" ng-app="sampleApp">
<head>
    <meta charset="UTF-8">
    <title>sample</title>
    <script src="//code.angularjs.org/1.5.0/angular.min.js"></script>
    <script src="//code.angularjs.org/1.5.0/angular-route.min.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"/>
</head>
<body>
<div ng-view></div>
<a href="#/" class="btn">Sample</a>
<a href="#/sample2" class="btn">Sample2</a>
</body>
</html>
const sampleApp = angular.module('sampleApp', ['ngRoute']);

sampleApp.config(['$routeProvider', function ($routeProvider) {
    // #/ の場合と、 #/{何らかの文字列} の場合のルーティングを設定する
    // この定義の場合、#/{何らかの文字列}における何らかの文字列がparamにバインドされてControllerに渡される
    $routeProvider
        .when('/', {
            template: '<h2>{{ msg }}</h2>',
            controller: 'sampleController'
        })
        .when('/:param', {
            template: '<h2>{{ msg }}</h2>',
            controller: 'sampleController'
        })
}]);

// $routeParamsを利用してpathに含まれる文字列(paramに対応するもの)をViewに反映する
// 何も渡されなかったときはデフォルトの文字列を表示する
sampleApp
    .controller('sampleController',['$scope', '$routeParams', function ($scope, $routeParams) {
        $scope.msg = $routeParams.param || 'Hello, World!';
    }]);

templateを別のファイルに切り出す

ルーティングのサンプルでは直接記載したtemplateを別ファイルに切り出せるようにする。

<!--  元々templateの中に書いてたhtmlをここに書く  -->
<h2>{{ msg }}</h2>
const sampleApp = angular.module('sampleApp', ['ngRoute']);

// templateUrlに切り出したhtmlを指定する
sampleApp.config(['$routeProvider', function ($routeProvider) {
    $routeProvider
        .when('/', {
            templateUrl: 'template.html',
            controller: 'sampleController'
        })
        .when('/:param', {
            templateUrl: 'template.html',
            controller: 'sampleController'
        })
}]);

sampleApp
    .controller('sampleController',['$scope', '$routeParams', function ($scope, $routeParams) {
        $scope.msg = $routeParams.param || 'Hello, World!';
    }]);

controllerAsを使って$scopeを使わないでcontrollerを書く

ng-controllerの記述を ng-controller="sampleController as main" のように変えたり、$routeProviderの指定をcontollerAs に変えることによって$scopeを利用せず記述することができる。
Angularの考え方に近く、後期に書かれたAngularJSはこちらで書かれていることが多いっぽい。

<!--  $routeProviderで指定したctrlの別名から値を参照する  -->
<h2>{{ ctrl.msg }}</h2>
const sampleApp = angular.module('sampleApp', ['ngRoute']);

sampleApp.config(['$routeProvider', function ($routeProvider) {
    $routeProvider
        // controllerとcontrollerAsを分けて書く場合
        .when('/', {
            templateUrl: 'template.html',
            controller: 'sampleController',
            controllerAs: 'ctrl'
        })
        // templateに埋め込む形で書く場合
        .when('/:param', {
            templateUrl: 'template.html',
            controller: 'sampleController as ctrl'
        })
}]);

sampleApp
    .controller('sampleController',['$routeParams', function ($routeParams) {
        // $scopeじゃなくてthisに値を設定する
        this.msg = $routeParams.param || 'Hello, World!';
    }]);

AngularJSのService

ビジネスロジックをまとめるための仕組み。
AngularJSの仕組みで名称を指定すると自動でDIされる。($scope、$routeProvider、$routeParamsなど)。
$scope以外は基本的にシングルトンなので、コンポーネント間の値の共有に使われたりする。
Serviceは後述の方法で自作してAngularJSから理由することができる。

Serviceを自作する

const sampleApp = angular.module('sampleApp', ['ngRoute']);

sampleApp.config(['$routeProvider', function ($routeProvider) {
    $routeProvider
        .when('/', {
            templateUrl: 'template.html',
            controller: 'sampleController',
            controllerAs: 'ctrl'
        })
        .when('/:param', {
            templateUrl: 'template.html',
            controller: 'sampleController as ctrl'
        })
}]);

// Serviceを作成してモジュールに登録する
sampleApp.service('sampleService', function () {
    this.firstName = '太郎';
    this.lastName = '山田';
    this.fullName = () => this.firstName + ' ' + this.lastName;
});

// 登録したServiceをInjectして処理を呼び出す
sampleApp
    .controller('sampleController', ['$routeParams', 'sampleService', function ($routeParams, sampleService) {
        // $scopeじゃなくてthisに値を設定する
        this.msg = ($routeParams.param || 'Hello, World!') + ' ' + sampleService.fullName();
    }]);

AngularJSのDirective

Directiveを自作する

AngularJSの世界におけるDirectiveはコンポーネントのようなもので、独自に定義したtemplateや処理をelementやattributeの形で埋め込めるもの。
今回はelementとして扱う単純な例と、親から子のDirectiveにどのように値を渡すかのサンプルを書く

<!DOCTYPE html>
<html lang="ja" ng-app="sampleApp">
<head>
    <meta charset="UTF-8">
    <title>sample</title>
    <script src="//code.angularjs.org/1.5.0/angular.min.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"/>
</head>
<body>
<div ng-controller="sampleController as ctrl">
    <!--  双方向バインディングでディレクティブに値を渡す  -->
    <sample-directive-a value="ctrl.msg"></sample-directive-a>
    <!--  文字列でディレクティブに値を渡す  -->
    <sample-directive-b value="ctrl.msg"></sample-directive-b>
    <!--  interpolationで渡すとディレクティブに渡る文字列自体が変化するので見た目上双方向バインディングのものと同じになる  -->
    <sample-directive-b value="{{ ctrl.msg }}"></sample-directive-b>
    <!--  ディレクティブに関数を渡す  -->
    <sample-directive-c function="ctrl.upperMsg()"></sample-directive-c>
    <!--  値の変更の確認用  -->
    <input type="text" ng-model="ctrl.msg">
</div>
</body>
</html>
const sampleApp = angular.module('sampleApp', []);

// Directiveに渡す用の値を作るController
sampleApp
    .controller('sampleController', [function () {
        this.msg = 'Hello, World!';
        this.upperMsg = function () {
            return this.msg.toUpperCase();
        }
    }]);

// Directive
sampleApp
    .directive('sampleDirectiveA', () => {
        return {
            restrict: 'E', // elementとしてのみ呼び出せるよう制限
            template: '<h2>{{ value }}</h2>',
            scope: {
                value: '=' // scopeを=にすると親から渡されたその値は双方向バインディング状態になる
            }
        }
    })
    .directive('sampleDirectiveB', () => {
        return {
            restrict: 'E',
            template: '<h2>{{ value }}</h2>',
            scope: {
                value: '@' // scopeを@にすると文字列で渡されたことになる。
            }
        }
    })
    .directive('sampleDirectiveC', () => {
        return {
            restrict: 'E',
            template: '<h2>{{ function() }}</h2>',
            scope: {
                function: '&' // scopeを&にすると関数や配列を渡されたことになる。
            }
        }
    });

参考

【GAS】SpreadSheetの表をjsonに変換し、ZIPで固めてGoogleDriveに配置する

静的なjsonを作成して配信する必要があり、メンテの楽なスプレッドシートの表から作れないかと思ってやってみたメモです。

JSONを作る

GASはほぼJavaScriptなので、 JSON.stringify が使えます。

JSON.stringify({ x: 5, y: 6 });
// expected output: "{"x":5,"y":6}"

そのため、まずstringifyできるobjectを作ります。
実装はちょっと変えたけど基本この記事ママです。
コピペでスプレッドシートをJSON形式のAPIにする方法 - Qiita

/**
 * 与えられたシート名からシートを取得し、表からjsonに変換可能なオブジェクトを生成して返却する
 * @param sheetName 
 */
function getData(sheetName) {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
    const rows = sheet.getDataRange().getValues();
    const keys = rows.splice(0, 1)[0];

    return rows.map(row => {
        var obj = {}
        row.forEach((item, index) => { obj[keys[index]] = item; });
        return obj;
    });
}

この処理で返ってきたオブジェクトをJSON.stringifyに通すと

id name
001 hoge
002 fuga

みたいな表がこういうオブジェクトになります。

{
    [
        {
            "id": "001",
            "name": "hoge"
        },
        {
            "id": "002",
            "name": "fuga"
        }
    ]
}

keyをつけてstringifyしてあげると、トップレベルにkey名がついて取り回しがよくなります

const json = JSON.stringify({ jsonKey: getData(sheetName) });
  • keyのついたjson
{
    "jsonKey": [
        {
            "id": "001",
            "name": "hoge"
        },
        {
            "id": "002",
            "name": "fuga"
        }
    ]
}

Zipに圧縮する

GASには Utilities.zip があるのでこれを使います。

blobsをつくる

Utilities.zipはblobsを渡してやる必要があるので、blobの配列を作成します。
Utilities.newBlobで作成できます。
json以外を作成するときはcontent-typeを適切なものにしてください。
また、このときファイル名を / 区切りにすると、Zip圧縮時にそのディレクトリごと作成されます。

const blobs = new Array();
blobs.push(Utilities.newBlob(json, 'application/json', 'hoge/fuga.json'));

ZIPをつくる

作成したblobsをUtilities.zipに渡してやれば完成です。

const fileName = 'archive.zip';
const zip = Utilities.zip(blobs, fileName);

Driveに配置する

何も考えずにファイルを作成するなら DriveApp.createFile すればよいです。
ただ、これで作ると別IDの新しいファイルが作成されるので、実行するたびに同名の新しいファイルが作られます。
なので今回は、
* 特定のフォルダに出力する。なければ作成する。
* 常に同じファイル名で出力する。すでに存在すれば削除する。 という方針で実装しました。

フォルダを取得する

DriveApp.getFoldersByName を使うと同名フォルダの一覧が取れるので、同じフォルダ名は存在しないという強い意志で、最初に取れたやつを返します。
前述したように、同名のフォルダ・ファイルは内部的なIDが異なるだけで普通に存在できてしまうので、諦めましょう。 なかったら新しく作ります。

/**
 * 指定したフォルダ名がすでに存在していればそのフォルダを、
 * 存在しなければ作成したフォルダのオブジェクトを返す
 * @param folderName 
 */
function getOrCreateTempFolder(folderName) {
    // 未作成なら作成してフォルダを取得する
    const folders = DriveApp.getFoldersByName(folderName);
    if (folders.hasNext()) return folders.next()
    else return DriveApp.createFolder(folderName);
}

ファイルを作成する

getOrCreateTempFolder で取得したFolderのオブジェクトと、作成したZIPファイルを渡して配置します。
フォルダと同様に同名ファイルが存在しないという強い意志をもって取得してremoveしています。

/**
 * 与えられたオブジェクトをファイル化して指定したフォルダ内に配置する
 * 
 * @param folder 
 * @param fileName 
 */
function createFile(folder, file) {
    const files = folder.getFilesByName(file.getName());
    if (files.hasNext()) folder.removeFile(files.next());
    return folder.createFile(file);
}

まとめ

これらの処理を一通りつなげて書いたものがこちらです。

function doCreateZip() {
    const sheetName = 'sample';
    const jsonKey = 'sample_json';
    const json = JSON.stringify({ jsonKey: getData(sheetName) });

    // 圧縮用のblob作成
    const blobs = new Array();
    blobs.push(Utilities.newBlob(json, 'application/json', 'hoge/fuga.json'));
  
    const fileName = 'archive.zip';
    const zip = Utilities.zip(blobs, fileName);
  
  
    const folderName = 'temporary';
    const folder = getOrCreateTempFolder(folderName);
    const file = createFile(folder, zip);
}

/**
 * 与えられたシート名からシートを取得し、表からjsonに変換可能なオブジェクトを生成して返却する
 * @param sheetName 
 */
function getData(sheetName) {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
    const rows = sheet.getDataRange().getValues();
    const keys = rows.splice(0, 1)[0];

    return rows.map(row => {
        const obj = {}
        row.forEach((item, index) => { obj[keys[index]] = item; });
        return obj;
    });
}

/**
 * 指定したフォルダ名がすでに存在していればそのフォルダを、
 * 存在しなければ作成したフォルダのオブジェクトを返す
 * @param folderName 
 */
function getOrCreateTempFolder(folderName) {
    // 未作成なら作成してフォルダを取得する
    const folders = DriveApp.getFoldersByName(folderName);
    if (folders.hasNext()) return folders.next()
    else return DriveApp.createFolder(folderName);
}

/**
 * 与えられたオブジェクトをファイル化して指定したフォルダ内に配置する
 * 
 * @param folder 
 * @param fileName 
 */
function createFile(folder, file) {
    const files = folder.getFilesByName(file.getName());
    if (files.hasNext()) folder.removeFile(files.next());
    return folder.createFile(file);
}

これを実行すると、こういう感じになります。便利ですね。
f:id:beatdjam:20200830060410p:plain:w300
f:id:beatdjam:20200830060514p:plain:w300
f:id:beatdjam:20200830060610p:plain:w300

【JavaScript/jQuery】We don't need jQuery?

昔書いてたメモが出てきたので、今更ですが供養。
正直今でも作り捨てとか、とりあえず作るならjQuery使ってもいいんじゃないかな?って思ってる。

jQueryとはなんだったのか

  • ブラウザ間でJavaScriptの実装が大きく異なっていた時代に、共通の実装が行えるように差異を吸収していたライブラリ。
    • ぶっちゃけ今でも差異はあるけど頑張れる範囲になってきた
  • 簡潔な実装からWebFront界隈で広く利用されており、jQueryに依存するライブラリも多数存在した。

なぜjQueryをやめるのか

不要なライブラリを読み込むことによる動作遅延

  • ただし70〜100KB前後なので、現在の通信環境ではほぼ問題なさそう

動作が遅い

  • jQueryはブラウザ間の差異を吸収するようにできているため、現代のブラウザには不要な分岐がある。
  • jQueryオブジェクト等の生成のコストもあり、現在の環境では不必要な処理が多い。
    • ついでにDOMを直接触るため、最近の仮想DOMを用いるフレームワークとの親和性が低い

jQueryでできていたことが、ES自体の仕様として取り込まれつつある

この辺参考
- jQueryのfindなどをquerySelectorで書く - Qiita
- You Don&#39;t Need jQuery - Qiita

jQueryで指定していたアニメーションがCSSで実現できるようになった

Pros/Cons

Pros

  • 不要なファイル(jQuery本体)を読み込まなくて良くなる
  • 本来必要ではない処理コストを削減できる
    • 標準APIに移行することでjQueryへのロックインがなくなり、他のフレームワークなどと組み合わせやすい
    • 近年流行っている仮想DOMとjQueryはだいぶ相性が悪い
  • スクリプトで行うべきこと、CSSで行うべきことの責務が明確になる

Cons

  • なんだかんだいってもjQueryの互換性担保は魅力
    • 現状、IE対応でPolyfillやBabelによるトランスパイルが必要となるケースはまだある
  • jQueryの方が記述が簡潔になるケースも多い
  • 長い間使われてきているプラグインにはjQuery必須のものもある
  • 小規模の開発ではjQuery使ってさっくり作るほうが良いこともある
    • VueやReactを入れるほどではない開発規模とか、標準APIだとまだ冗長になる部分とか