20200809のJavaScriptに関する記事は19件です。

jQueryでできる基本的なこと(初心者)

この記事について

progateでjQueryを学習した後にアウトプットの為に書いている記事です。Javascriptに精通している方には参考にならないと思うのでスルーしてください。

モーダルウィンドウの表示

モーダルウィンドウとは、ウィンドウ内で指定された操作を完了したり、キャンセル操作するまで他の画面にすることが出来ないウィンドウの事です。モーダルウィンドウの表示もjQueryで実装することができます。
スクリーンショット 2020-08-09 12.23.27.png

sample.html
<!--モーダル部分のみ抜粋しています-->
 <div class="signup-modal-wrapper" id="signup-modal">
    <div class="modal">
      <div>
        <i class="fa fa-2x fa-times" id="close-modal"></i>
      </div>
      <div id="signup-form">
        <h2>Emailで新規登録</h2>
        <form action="#">
          <input class="form-control" type="text" placeholder="メールアドレス">
          <input class="form-control" type="password" placeholder="パスワード">
          <div id="submit-btn">新規登録</div>
        </form>
      </div>
    </div>
  </div>

sample.css
.signup-modal-wrapper {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: rgba(0, 0, 0, 0.6);
  z-index: 100;
/*cssで非表示にしておきます*/
  display: none;
}

script.js
$(function() {
//クリックイベントを作成
  $('.signup-show').click(function() {
//モーダルを表示するようにする
    $('#signup-modal').fadeIn();
  });
//閉じるボタンが押されたら消えるようにする
  $('#close-modal').click(function() {
    $('#signup-modal').fadeOut();
  });
});

ホバーで文字を表示する

jQueryで画像や文字にマウスを乗せると文字が表示される機能を実装できます。以下の方法で実装しております。

①ホバー時に表示させたい文章はcssで非表示にする。
②「display: block;」を含むclassを作成しておく。
③hoverイベントをJavascriptのファイルに記入。
④ホバー時に「display: block;」を含むclassを追加する(addClassメソッドで)
⑤ホバーが外れる時は追加したclassを取り除く(removeメソッドで)

sample.html
<!-- レッスン一覧部分 -->
<div class="lesson-wrapper">
    <div class="container">
      <div class="heading">
        <h2>Learn Where to Get Started!</h2>
      </div>
      <div class="lessons">
        <div class="lesson">
          <div class="lesson-icon">
            <img src="https://prog-8.com/images/html/advanced/html.png">
            <p>HTML & CSS</p>
          </div>
          <p class="text-contents">ウェブページの作成に使用される言語です。HTMLとCSSを組み合わせることで、静的なページを作り上げることができます。</p>
        </div>
        <div class="lesson">
          <div class="lesson-icon">
            <img src="https://prog-8.com/images/html/advanced/jQuery.png">
            <p>jQuery</p>
          </div>
          <p class="text-contents">素敵な動きを手軽に実装できるJavaScriptライブラリです。 アニメーション効果をつけたり、Ajax(エイジャックス)を使って外部ファイルを読み込んだりと色々なことができます。</p>
        </div>
        <div class="lesson">
          <div class="lesson-icon">
            <img src="https://prog-8.com/images/html/advanced/ruby.png">
            <p>Ruby</p>
          </div>
          <p class="text-contents">オープンソースの動的なプログラミング言語で、 シンプルさと高い生産性を備えています。大きなWebアプリケーションから小さな日用ツールまで、さまざまなソフトウェアを作ることができます。</p>
        </div>
        <div class="lesson">
          <div class="lesson-icon">
            <img src="https://prog-8.com/images/html/advanced/php.png">
            <p>PHP</p>
          </div>
          <p class="text-contents">HTMLだけではページの内容を変えることはできません。PHPはHTMLにプログラムを埋め込み、それを可能にします。</p>
        </div>
      </div>
    </div>
  </div>
  <!-- レッスン一覧ここまで -->

sample.css
.text-contents {
  margin: 3% auto;
  width: 80%;
  font-size: 12px;
  color: #b3aeb5;
/*非表示にしておく*/
  display: none;
}
/*表示させる時のclass*/
.text-show {
  display: block;
}

script.js
$(function() {

  // 言語一覧

  $('.lesson').hover(function() {
//ホバーされているオブジェクトにクラスを追加(文章は子要素なのでfindメソッドを使用)
    $(this).find('.text-contents').addClass('text-show');
  }, 
//ホバーが外された時にクラスを取り除く
  function() {
    $(this).find('.text-contents').removeClass('text-show');
  });
  }
  );

アコーディオン

Webアプリケーションにおけるアコーディオンとはクリックすると文字がスライドして表示され、もう一度クリックすると非表示になる機能のことです。この動作が楽器のアコーディオンに似ているからそう呼ばれているのだと思います。
スクリーンショット 2020-08-09 22.39.39.png

アコーディオン.png

以下の手順で実装できます。

①(アコーディオンで表示する)文章をCSSで非表示にしておく。
②クリックする部分には同一のclassを付与しておく。
③クリックして表示する時はaddClassメソッドを用いてclassを追加し、表示させる。非表示にする時はremoveClassでclassを取り除く

このような手順で実装する為に、hasClassメソッドを用います。

hasClassメソッド

hasClassメソッドは、オブジェクトが引数に使用しているclassを持っているか判定し、trueかfalseで返すメソッドです。このメソッドとif文による条件分岐を利用してアコーディオンを実装します。

アコーディオン実装のコード

sample.html
<div class="faq-wrapper">
    <div class="container">
      <div class="heading">
        <h2>FAQ</h2>
      </div>
      <div class="faq">
        <ul id="faq-list">
          <li class="faq-list-item">
            <h3 class="question">Progateの公式キャラクターはなんですか?</h3>
            <span>+</span>
            <div class="answer">
              <p>にんじゃわんこといいます。忍者の格好をしたわんこです。ネコではありません。</p>
            </div>
          </li>
          <li class="faq-list-item">
            <h3 class="question">にんじゃわんこはオスですか?それともメスですか?</h3>
            <span>+</span>
            <div class="answer">
              <p>にんじゃわんこはオスです。</p>
            </div>
          </li>
          <li class="faq-list-item">
            <h3 class="question">にんじゃわんこは何歳ですか?</h3>
            <span>+</span>
            <div class="answer">
              <p>にんじゃわんこは14歳です。</p>
            </div>
          </li>
        </ul>
      </div>
    </div>
  </div>

sample.css
#faq-list {
  width: 500px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
}

.faq-list-item {
  margin:10px;
  border-bottom:1px solid #ccc;
  position:relative;
  cursor:pointer;
  text-align: left;
}

.faq-list-item h3 {
  font-size: 14px;
}

.faq-list-item span {
  position:absolute;
  top:0;
  right:5px;
  color:#ccc;
  font-size:13px;
}

.answer {
  font-size: 12px;
  padding: 5px 0px;
  margin-bottom: 15px;
/*非表示にしておく*/
  display: none;
}


script.js
$(function() {

  // FAQ

  $('.faq-list-item').click(function() {
//何度も書くのが面倒臭いので、前もって変数にする。
    var $answer = $(this).find('.answer')
//もしactiveクラスがある場合(既に表示されている場合)はactiveクラスを取り除いて非表示にする。
    if ($answer.hasClass('active')) {
      $answer.removeClass('active');
      $answer.slideUp();
//非表示の時は"+"にするというコード
      $(this).find('span').text('+');
    } else {
//activeクラスがない場合(非表示の場合)はactiveクラスを追加する。
      $answer.addClass('active');
      $answer.slideDown();
//表示の時は"-"にするというコード
      $(this).find('span').text('-');
    }
  });

});


ホバーの時と同様にaddClassとremoveClassを用いていますが、クラスの追加及び除去では表示の変化は起きません(あくまでも表示か非表示かを判断するための目印)。表示や非表示はslideUpとslideDownで行なっております。

感想

Javascriptのコースではコンソール上の変化しか見ていなかったので、いまいち実力がついている気がしなかったが、Webアプリケーションで使用されている機能の実装方法を今回の学習で身に付けたので実感が湧いた。今後もjQueryの学習を進めていきたいと思う。

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

Kinx Tips - 実行形式ファイルを作る

Kinx Tips - 実行形式ファイルを作る

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。今回は Tips です。

皆さん、実行ファイルを作る時どうしてますか? ...そう、普通は C や C++ で書いてコンパイルしますね。もっと簡単に作れるといいですね。

Kinx は独立した exe を作ることはサポートしていませんが、Kinx のワールドの中であれば exe にして単体実行コマンドのように見せかけて実行できるモジュールを作れるようにしています。

その辺を少々。

コマンド kxrepl.exekxtest.exe

実は v0.13.1 リリースより kxrepl.exekxtest.exe というコマンドが同梱されています。Linux 版では .exe はついて無くて kxreplkxtest です。

kxrepl を実行してみましょう。

$ kxrepl
kinx[  0]> .quit

REPL が動作しましたね。

次は kxtest です。

$ kxtest -v -T declaration.md
Test Cout = 11
Entry: doc/spec/statement/declaration.md
    Suite: Declaration statement
        Case[ 0]: Normal case ................................... successful (  0.10s)
        Case[ 1]: With initializer .............................. successful (  0.09s)
        Case[ 2]: With initializer of expression ................ successful (  0.09s)
        Case[ 3]: Multiple variable declaration ................. successful (  0.10s)
        Case[ 4]: Constant value (1) ............................ successful (  0.07s)
        Case[ 5]: Constant value (2) ............................ successful (  0.10s)
        Case[ 6]: Constant value (3) ............................ successful (  0.07s)
        Case[ 7]: Constant value (4) ............................ successful (  0.07s)
        Case[ 8]: Constant value (5) ............................ successful (  0.09s)
        Case[ 9]: Destructuring assignment (1) .................. successful (  0.09s)
        Case[10]: Destructuring assignment (2) .................. successful (  0.07s)


<Test Result>
    Total Test Cases:       11
        Successful  :       11
        Failed      :        0
        Warning     :        0

SpecTest が動作しましたね。

もう一つ、試しにやってみましょう。

$ diff -s kxrepl kxtest
Files kxrepl and kxtest are identical

「ファイルは同一です」 というメッセージがでました。というか、そうなんです。この 2 つは バイナリとして全く同じ です。

--exec オプションの話

この話の前に、一つ Kinx のオプションに関する情報を書いておきます。--exec というオプションです。README にも書いてある通り、このオプションによって以下の 2 つのオプションがサポートされています。

  • --exec:repl ... REPL を実行する。
  • --exec:specttest ... SpecTest を実行する。

このメカニズムは、以下の通りになっています。

  1. --exec:xxx を認識したら、Kinx の実行ファイルのあるフォルダから見て lib/exec/xxx.kx、または lib/exec/3rdparty/xxx.kx を探す。
  2. そのファイルがあったら、そのファイルがスクリプトファイルとして指定されたとみなして実行する。

なので、REPL と SpecTest はそれぞれ lib/exec/repl.kxlib/exec/spectest.kx ファイルを探してそれを実行する、という動作をしているのです。この仕組みによって、REPL と SpecTest の修正はバイナリを修正せずに実施できるようにもなっています。

kxrepl

さて、kxrepl のバイナリ(= kxtest のバイナリ)ですが、どういう作りになっているのでしょう(概ね見当は付くと思いますが)。答えはこうです。

  1. 自分自身の実行ファイル名(name としましょう)を取得(argv[0] から取得できます)。
  2. コマンドライン引数の先頭に --exec:name を割り込ませて引数リストを更新。
  3. Kinx のメインロジックに制御を渡す。

するとどうでしょう。自動的に lib/exec/name.kx(または lib/exec/3rdparty/name.kx)を参照して実行してくれるのです!

ということで、kxrepllib/exec/kxrepl.kx を、kxtestlib/exec/kxtest.kx を自動的に実行してくれるという寸法です。

ん?、そんなファイルあったっけ?はい、追加したんです(=あります)。中身を見てみましょう。

lib/exec/kxrepl.kx
using exec.repl;
lib/exec/kxtest.kx
using exec.spectest;

これだけです。中で using するだけのファイルを用意して実現しました。using の検索パスに従って repl.kxspectest.kx を見つけてくれるので、これで正しく動作します。というわけで、kxrepl.exerepl.exe に書き換えても同様に動きます。

ということで、もうお分かりですね。

オリジナル exe を作ろう

オリジナル exe ファイルの作り方は以下の通りです。動作には Kinx の dll と各種ライブラリが必要なので、kinx.exe と同じ場所に、作成した exe を置かなくてはなりませんが、やりたいことをコマンド一発でできるようにはなります。

具体的な例で。

kxcat.exe

cat コマンドみたいなものとして、kxcat コマンドを作ってみましょう。指定されたファイルを指定された順に出力します。cat コマンドと名前が重ならないように kxcat にしておきましょう。一先ずオプションは無しで、ファイルだけ複数受け付けるようにします。

lib/exec/3rdparty/kxcat.kx
$$.each {
    // Ignoring the script file name.
    if (_2 > 0) {
        System.print(File.load(_1));
    }
};

さて、そうしたら kxtest.exe をコピーして名前を変えましょう。Windows でも Linux でもコマンドは違いますが、やりたいことは同じです。

Windows
$ copy /y kxtest.exe kxcat.exe
Linux
$ cp -f kxtest kxcat

コピーしたらそのまま kxcat コマンドを実行!

$ ./kxcat README.md ChangeLog.md
<p align="right">
    <img src="https://github.com/Kray-G/kinx/workflows/Unit%20Test/badge.svg?branch=master"/>
    <img src="http://img.shields.io/badge/license-MIT-blue.svg?style=flat"/>
</p>

...(省略)

## V0.1.0 (1st Preview Release)

*   Initial Release.

できましたね。

インストールされたもので実行したい場合、Linux では以下の場所に kinx コマンドのバイナリがあるので、例えば上記の場合、kxcat コマンドは同じ位置に配置してください。kxreplkxtest も同じ場所に配置してあります。

$ which kinx kxrepl kxtest
/usr/bin/kinx
/usr/bin/kxrepl
/usr/bin/kxtest

おわりに

C でがっつり書いてコンパイルして実行ファイルを作る、でもいいんですけど、スクリプトでササっと書いたのを実行形式ファイルにしたいですよね。かといって、必要なライブラリとか dll とかを全部含めると結構なサイズになったりするので、ここは割り切って Kinx 自体が存在する前提でコマンド化できるようにしてみました。

これはこれで便利かなー、と思います。

あと、やればすぐできるんですけど、例えば kinx.dll の位置を環境変数で指定したり、オプションで渡したりできると、.exe ファイル自体はどこにおいても良くなるのでそのくらいの対応は今後するかも入れません(要望があれば)。xxx.exe と同じ場所にある xxx.kx を実行するとかね。そのほうが便利かなー。dll の場所は何かしらの方法で指定しておく必要はありますが。

ではでは、また次回。

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

自分への未来のメッセージをプログラムで書いてみる【超初心者向け】

前置き

前回からの続き
未来考えるの大変だから臨機応変にメッセージを組むプログラムを書いてみます。

基本的な機能

  1. とある値を元にメッセージを変化させる
  2. とある値を元にメッセージの表示方法を変化させる

この二つの機能を元にメッセージを組んでみます。とある値というのは個人で考えて意味を持つ値にしてください。例えば一年後の目標の達成度や何かの指標を決めておき、その値によってメッセージが変わるようにします。

とある値を元にメッセージを変化させる

プログラムの基本「if文」を使ってプログラミングをします。
言葉の通り、もし〇〇だったら××を行うという処理を書くことができます。
以下の場合だと「a」という中身が「1」であるかを判定し、「aは1です」と表示されます。

let a = 1;
if( a === 1 ){
  console.log("aは1です");
} else {
  console.log("aは1ではありません");
}
// 出力結果:「aは1です」

以下の場合だと「a」という中身が「5」なので、「aは1ではありません」と表示されます。

let a = 5;
if( a === 1 ){
  console.log("aは1です");
} else {
  console.log("aは1ではありません");
}
// 出力結果:「aは1ではありません」

ここでいう「a」の中身の値がとある値ということにすれば、こんな感じにメッセージを書くことができます。

function checkBHAGDriven(achievement) {
    let bHAGDrivenMessage = "BHAG Drivenの達成度は" + achievement + "か。";
    if(achievement >= 80){
        bHAGDrivenMessage 
            += "まじすごい。考えられない。理想にしているところも達成してしまうパワーを手に入れたようやな"
            + "あとは理想を広げ考え高めることが必要だと思う。と言っても一年前の自分には考えられない領域にいるんかな。自由に高みを目指すんやで。";
    } else if(achievement < 80 && achievement >= 50){
        bHAGDrivenMessage 
            += "ぼちぼち目標達成に拘って達成できるようになってきたかな。でもまだ自分の理想は達成できてないんちゃう?"
            + "もっと自分出して周りを巻き込めるような野郎になるんやで。";
    }else if(achievement < 50 && achievement >= 0){
        bHAGDrivenMessage 
            += "まだまだBHAGに取り組めていないようやな。もっとたくさんのことを行動ファーストでチャレンジせなあかん。"
            + "君は考えすぎでやらない理由を考えてしまうんやから、たまには頭空っぽにして行動する意識くらいの方がちょうどええんやで。";
    }else if(achievement < 0){
        bHAGDrivenMessage 
            += "何してんの?";
    }
    return bHAGDrivenMessage;
}

とある値を元にメッセージの表示方法を変化させる

メッセージの表示には console.log() を使っていますが、別の書き方で表示方法が変わります。
本来の使い方は違いますが、見かけが変わるので使ってみます。

console.log()
image.png
console.warn()
image.png

console.error()
image.png

これをif文使って処理してみます

/**
 * 達成度に応じてメッセージの表示方法を変えて出力する
 * @param {*} bHagAchievement BHAGDrivenの達成度
 * @param {*} icebergAchievement IcebergMindの達成度
 * @param {*} growingTogetherAchievement GrowingTogetherの達成度
 * @param {*} featureLetter 出力する手紙
 */
function outputYourFeatureLetter(
    bHagAchievement, icebergAchievement, 
    growingTogetherAchievement, featureLetter) {
    if(bHagAchievement  >= 80 && icebergAchievement >= 80 && growingTogetherAchievement >= 80){
        console.log(featureLetter);
    } else if(bHagAchievement  >= 50  || icebergAchievement >= 50 || growingTogetherAchievement >= 50){
        console.warn(featureLetter);
    } else {
        console.error(featureLetter); 
    }       
}

達成度が低いと赤くメッセージが出るようになるので、よろしくない状態を煽ります笑

どんなメッセージ?

最終的に書いてみたプログラムはこちらに置いてありますのでぜひ実行してみてください。
https://github.com/taka-guevara/FutureLetter/blob/master/futureLetter.js
実行の仕方はこちら

達成度を値に設定しておくとその値に応じてメッセージが変わります。
image.png
達成度が悪いとこんな感じ
image.png

おまけ:今回の扱った達成度

私の勤めている株式会社POLのバリューをどれほど達成しているかを指標にしました。
あくまで私の主観であり感覚値です。
興味のある方はこちらをご覧ください。

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

正規表現入門 第三回 数量詞を用いた検索

数量詞とは

  • 「数量を示す単語または句」のことを言う。
  • メタ文字と同様、数量詞もそのものを検索する場合はエスケープが必要です。

先頭の文字を検索 "^"

  • 文字列の「先頭」を検索するには「^」を使います。
  • mオプションを加えると、改行直後の値も対象になります。
3-1(先頭の文字を検索).js
console.log("This is a pen!".match(/^This/));
console.log("This is a pen!".match(/^is/));

console.log("This is a pen!\nThis is a ball!".match(/^This/g));
console.log("This is a pen!\nThis is a ball!".match(/^This/gm));

console.log("This is a pen!".match(/^This\s../));
console.log("This is a pen.This is a ball.".replace(/^This/, "That"));

実行結果
[ 'This', index: 0, input: 'This is a pen!', groups: undefined ]
null
[ 'This' ]
[ 'This', 'This' ]
[ 'This is', index: 0, input: 'This is a pen!', groups: undefined ]
That is a pen.This is a ball.

末尾を検索 "$"

  • 文字列の「末尾」を検索するには「$」を使います。
3-2(末尾を検索).js
console.log("This is a pen!".match(/pen!$/));
console.log("This is a pen!".match(/is$/));

console.log("This is a pen!\nThis is a ball!".match(/!$/g));
console.log("This is a pen!\nThis is a ball!".match(/!$/gm));

console.log("This is a pen!".match(/\spen!$/));

console.log("This is a pen.This is a pen.".replace(/pen.$/, "ball."));

実行結果
[ 'pen!', index: 10, input: 'This is a pen!', groups: undefined ]
null
[ '!' ]
[ '!', '!' ]
[ ' pen!', index: 9, input: 'This is a pen!', groups: undefined ]
This is a pen.This is a ball.

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

【Nuxt】SSR・SSG・SPAにおける『nuxt build』と『nuxt generate』の実行結果の違いまとめ

NuxtにはSSR(Server Side Rendering)、SSG(Static Site Generator)1、SPA(Single Page Application)の3種類のモードが用意されています。

また、本番環境でNuxtアプリケーションを実行するにあたりnuxt buildnuxt generateの2つのコマンドが用意されています。

nuxt buildはアプリケーションをWebpackでビルドし、JSとCSSをミニファイするコマンド2です。ビルドファイルの出力先は.nuxt配下です。

nuxt generateは静的ウェブサイトへデプロイする静的ファイルを生成するコマンド2です。静的ファイルの出力先はdist配下です。
静的ウェブサイトのホスティングサービスではNetlifyやAmazon S3などが有名です。

SSRでアプリケーションを運用する場合、nuxt buildでファイルをビルド後、nuxt startでNuxtアプリケーションをサーバー上で起動という流れになります。
一方、SSGやSPAではnuxt generateで静的ファイルを生成後、静的ウェブサイトにアップロードするという流れになります。

では、SSGでnuxt buildを実行したり、SSRでnuxt generateを実行したりするとどうなるでしょうか。

今回はSSR・SSG・SPAにおけるnuxt buildnuxt generateの出力の違いについて紹介します。
なお、Nuxtは2.14.1を利用します。

NuxtにおけるSSR・SSG・SPAの設定方法について

Nuxtではnuxt.config.jsmodeプロパティtargetプロパティによってアプリケーションのモードが管理されています。
modeにはuniversalspatargetにはserverstaticの設定値が用意されています。

modespaにするとSPAになります。
modeuniversalにした場合、targetstaticにすればSSG、targetserverにすればSSRになります。

Nuxtアプリケーションのモードの初期設定は、新規作成時の対話の回答によって決定されます。
? Rendering mode:modeの設定値に関する質問、? Deployment target:targetの設定値に関する質問です。

image.png
image.png

検証に利用するサンプルアプリケーションについて

SSR・SSG・SPAにおけるnuxt buildnuxt generateの挙動を確認するにあたり、今回はNuxt公式ドキュメントで紹介されているCustom Routesを利用します。

Custom Routesはユーザー一覧画面と各ユーザーの詳細画面が用意されているシンプルなアプリケーションです。

スクリーンショット 2020-08-09 17.11.51.png

スクリーンショット 2020-08-09 17.11.56.png

各パターンにおける実行結果の違いについて

2パターンのmodeuniversalspa)と2パターンのtargetserverstatic)を組み合わせた、計4パターンでnuxt buildnuxt generateを実行しました。

以下にパターンごとの結果を掲載します。

『mode: universal』『target: server』の場合

SSRに該当するパターンです。

nuxt buildの実行ログは以下の通りです。

ℹ Production build
ℹ Bundling for server and client side
ℹ Target: server
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 5.36s

✔ Server
  Compiled successfully in 539.09ms


Hash: 5a5cd4d0c2a79cef9011
Version: webpack 4.44.1
Time: 5358ms
Built at: 2020/08/08 15:59:26
                          Asset       Size  Chunks                         Chunk Names
 ../server/client.manifest.json   7.83 KiB          [emitted]
                       LICENSES  389 bytes          [emitted]
                 app.dce2e9b.js   55.3 KiB       0  [emitted] [immutable]  app
node_modules/commons.48315ec.js    168 KiB       1  [emitted] [immutable]  node_modules/commons
         pages/index.9ce75e2.js   1.51 KiB       2  [emitted] [immutable]  pages/index
     pages/users/_id.5722ee2.js   1.45 KiB       3  [emitted] [immutable]  pages/users/_id
             runtime.84feac3.js   2.35 KiB       4  [emitted] [immutable]  runtime
 + 2 hidden assets
Entrypoint app = runtime.84feac3.js node_modules/commons.48315ec.js app.dce2e9b.js

Hash: 583b6fd0b31f116a80e2
Version: webpack 4.44.1
Time: 540ms
Built at: 2020/08/08 15:59:26
               Asset       Size  Chunks             Chunk Names
      pages/index.js   6.63 KiB       1  [emitted]  pages/index
  pages/users/_id.js   6.59 KiB       2  [emitted]  pages/users/_id
           server.js   86.6 KiB       0  [emitted]  app
server.manifest.json  307 bytes          [emitted]
 + 3 hidden assets
Entrypoint app = server.js server.js.map
ℹ Ready to run nuxt start
✨  Done in 9.20s.

実行ログをもとに、SSRモードのnuxt buildの内容をまとめると以下のようになります。

  • サーバーサイドとクライアントサイドのビルドが実行される
  • Targetはserverに設定される

nuxt generateの実行ログは以下の通りです。

ℹ Production build
ℹ Bundling for server and client side
ℹ Target: static
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 5.25s

✔ Server
  Compiled successfully in 578.63ms


Hash: 413f8a0717bc47e1c0fb
Version: webpack 4.44.1
Time: 5253ms
Built at: 2020/08/08 16:01:35
                          Asset       Size  Chunks                         Chunk Names
 ../server/client.manifest.json   7.83 KiB          [emitted]
                       LICENSES  389 bytes          [emitted]
                 app.c20583b.js   55.5 KiB       0  [emitted] [immutable]  app
node_modules/commons.48315ec.js    168 KiB       1  [emitted] [immutable]  node_modules/commons
         pages/index.9ce75e2.js   1.51 KiB       2  [emitted] [immutable]  pages/index
     pages/users/_id.5722ee2.js   1.45 KiB       3  [emitted] [immutable]  pages/users/_id
             runtime.84feac3.js   2.35 KiB       4  [emitted] [immutable]  runtime
 + 2 hidden assets
Entrypoint app = runtime.84feac3.js node_modules/commons.48315ec.js app.c20583b.js

Hash: cef8c098609baff3310b
Version: webpack 4.44.1
Time: 579ms
Built at: 2020/08/08 16:01:35
               Asset       Size  Chunks             Chunk Names
      pages/index.js   6.63 KiB       1  [emitted]  pages/index
  pages/users/_id.js   6.59 KiB       2  [emitted]  pages/users/_id
           server.js   86.6 KiB       0  [emitted]  app
server.manifest.json  307 bytes          [emitted]
 + 3 hidden assets
Entrypoint app = server.js server.js.map
ℹ Generating output directory: dist/
ℹ Generating pages
✔ Generated route "/"
✔ Generated route "/users/3"
✔ Generated route "/users/5"
(略)
✔ Client-side fallback created: 200.html

実行ログをもとに、SSRモードのnuxt generateの内容をまとめると以下のようになります。

  • サーバーサイドとクライアントサイドのビルドが実行される
  • Targetはstaticに設定される
  • dist配下に各ページの静的ファイルが作成される

『mode: universal』『target: static』の場合

SSGに該当するパターンです。

nuxt buildの実行ログは以下の通りです。

ℹ Production build
ℹ Bundling for server and client side
ℹ Target: full static
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 6.34s

✔ Server
  Compiled successfully in 561.19ms

Hash: 009f5cce6a4034eb2970
Version: webpack 4.44.1
Time: 6345ms
Built at: 2020/08/08 15:51:05
                          Asset       Size  Chunks                         Chunk Names
 ../server/client.manifest.json   7.86 KiB          [emitted]
                       LICENSES  389 bytes          [emitted]
                 app.edced78.js   58.5 KiB       0  [emitted] [immutable]  app
node_modules/commons.501805f.js    168 KiB       1  [emitted] [immutable]  node_modules/commons
         pages/index.fe29326.js   1.51 KiB       2  [emitted] [immutable]  pages/index
     pages/users/_id.7d0c948.js   1.45 KiB       3  [emitted] [immutable]  pages/users/_id
             runtime.715f042.js   2.35 KiB       4  [emitted] [immutable]  runtime
 + 2 hidden assets
Entrypoint app = runtime.715f042.js node_modules/commons.501805f.js app.edced78.js

Hash: b97105c83cc5f7b09c53
Version: webpack 4.44.1
Time: 562ms
Built at: 2020/08/08 15:51:06
               Asset       Size  Chunks             Chunk Names
      pages/index.js   6.63 KiB       1  [emitted]  pages/index
  pages/users/_id.js   6.59 KiB       2  [emitted]  pages/users/_id
           server.js   87.7 KiB       0  [emitted]  app
server.manifest.json  307 bytes          [emitted]
 + 3 hidden assets
Entrypoint app = server.js server.js.map

ℹ Ready to run nuxt generate
✨  Done in 12.29s.

実行ログをもとに、SSGモードのnuxt buildの内容をまとめると以下のようになります。

  • サーバーサイドとクライアントサイドのビルドが実行される
  • Targetはfull staticに設定される

nuxt generateの実行ログは以下の通りです。

✔ Skipping webpack build as no changes detected
ℹ Generating output directory: dist/
ℹ Generating pages with full static mode
✔ Generated route "/"
✔ Generated route "/users/4"
✔ Generated route "/users/6"
(略)
✔ Client-side fallback created: 200.html
✨  Done in 3.19s.

実行ログをもとに、SSGモードのnuxt generateの内容をまとめると以下のようになります。

  • dist配下に各ページの静的ファイルが作成される

『mode: spa』『target: server』の場合

デプロイ先をサーバーに指定したSPAです。

nuxt buildの実行ログは以下の通りです。

ℹ Production build
ℹ Bundling only for client side
ℹ Target: static
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 5.23s


Hash: 286e8db275c753c4ebd5
Version: webpack 4.44.1
Time: 5227ms
Built at: 2020/08/08 16:04:10
                          Asset       Size  Chunks                         Chunk Names
 ../server/client.manifest.json   7.79 KiB          [emitted]
                       LICENSES  389 bytes          [emitted]
                 app.43c0549.js   55.4 KiB       0  [emitted] [immutable]  app
node_modules/commons.48315ec.js    168 KiB       1  [emitted] [immutable]  node_modules/commons
         pages/index.9ce75e2.js   1.51 KiB       2  [emitted] [immutable]  pages/index
     pages/users/_id.5722ee2.js   1.45 KiB       3  [emitted] [immutable]  pages/users/_id
             runtime.84feac3.js   2.35 KiB       4  [emitted] [immutable]  runtime
 + 1 hidden asset
Entrypoint app = runtime.84feac3.js node_modules/commons.48315ec.js app.43c0549.js
ℹ Generating output directory: dist/
ℹ Generating pages
✔ Generated route "/"
✔ Client-side fallback created: 200.html
✨  Done in 8.36s.

実行ログをもとに、SPA(target: server)モードのnuxt buildの内容をまとめると以下のようになります。

  • クライアントサイドのビルドが実行される
  • Targetはstaticに設定される
  • dist配下にルート(/)の静的ファイル(index.html)が作成される

nuxt generateの実行ログは以下の通りです。

ℹ Production build
ℹ Bundling only for client side
ℹ Target: static
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 5.15s


Hash: 286e8db275c753c4ebd5
Version: webpack 4.44.1
Time: 5155ms
Built at: 2020/08/08 16:05:27
                          Asset       Size  Chunks                         Chunk Names
 ../server/client.manifest.json   7.79 KiB          [emitted]
                       LICENSES  389 bytes          [emitted]
                 app.43c0549.js   55.4 KiB       0  [emitted] [immutable]  app
node_modules/commons.48315ec.js    168 KiB       1  [emitted] [immutable]  node_modules/commons
         pages/index.9ce75e2.js   1.51 KiB       2  [emitted] [immutable]  pages/index
     pages/users/_id.5722ee2.js   1.45 KiB       3  [emitted] [immutable]  pages/users/_id
             runtime.84feac3.js   2.35 KiB       4  [emitted] [immutable]  runtime
 + 1 hidden asset
Entrypoint app = runtime.84feac3.js node_modules/commons.48315ec.js app.43c0549.js
ℹ Generating output directory: dist/
ℹ Generating pages
✔ Generated route "/"
✔ Client-side fallback created: 200.html
✨  Done in 7.95s.

実行ログをもとに、SPA(target: server)モードのnuxt generateの内容をまとめると以下のようになります。

  • クライアントサイドのビルドが実行される
  • Targetはstaticに設定される
  • dist配下にルート(/)の静的ファイル(index.html)が作成される

『mode: spa』『target: static』の場合

デプロイ先を静的ウェブサイトに指定したSPAです。

nuxt buildの実行ログは以下の通りです。

ℹ Production build
ℹ Bundling only for client side
ℹ Target: static
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 5.23s


Hash: 286e8db275c753c4ebd5
Version: webpack 4.44.1
Time: 5236ms
Built at: 2020/08/08 16:06:27
                          Asset       Size  Chunks                         Chunk Names
 ../server/client.manifest.json   7.79 KiB          [emitted]
                       LICENSES  389 bytes          [emitted]
                 app.43c0549.js   55.4 KiB       0  [emitted] [immutable]  app
node_modules/commons.48315ec.js    168 KiB       1  [emitted] [immutable]  node_modules/commons
         pages/index.9ce75e2.js   1.51 KiB       2  [emitted] [immutable]  pages/index
     pages/users/_id.5722ee2.js   1.45 KiB       3  [emitted] [immutable]  pages/users/_id
             runtime.84feac3.js   2.35 KiB       4  [emitted] [immutable]  runtime
 + 1 hidden asset
Entrypoint app = runtime.84feac3.js node_modules/commons.48315ec.js app.43c0549.js
ℹ Ready to run nuxt generate
✨  Done in 7.75s.

実行ログをもとに、SPA(target: static)モードのnuxt buildの内容をまとめると以下のようになります。

  • クライアントサイドのビルドが実行される
  • Targetはstaticに設定される

nuxt generateの実行ログは以下の通りです。

ℹ Doing webpack rebuild because nuxt.config.js modified
ℹ Production build
ℹ Bundling only for client side
ℹ Target: static
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 6.29s


Hash: 47d36e3c6f1bf3363c94
Version: webpack 4.44.1
Time: 6296ms
Built at: 2020/08/08 16:07:19
                          Asset       Size  Chunks                         Chunk Names
 ../server/client.manifest.json   7.79 KiB          [emitted]
                       LICENSES  389 bytes          [emitted]
                 app.ef33196.js   55.4 KiB       0  [emitted] [immutable]  app
node_modules/commons.48315ec.js    168 KiB       1  [emitted] [immutable]  node_modules/commons
         pages/index.9ce75e2.js   1.51 KiB       2  [emitted] [immutable]  pages/index
     pages/users/_id.5722ee2.js   1.45 KiB       3  [emitted] [immutable]  pages/users/_id
             runtime.84feac3.js   2.35 KiB       4  [emitted] [immutable]  runtime
 + 1 hidden asset
Entrypoint app = runtime.84feac3.js node_modules/commons.48315ec.js app.ef33196.js
ℹ Generating output directory: dist/
ℹ Generating pages
✔ Generated route "/"
✔ Client-side fallback created: 200.html
✨  Done in 8.93s.

実行ログをもとに、SPA(target: static)モードのnuxt generateの内容をまとめると以下のようになります。

  • クライアントサイドのビルドが実行される
  • Targetはstaticに設定される
  • dist配下にルート(/)の静的ファイル(index.html)が作成される

まとめ

実行結果をまとめると以下のようになります。

  • プロセスは異なるものの、nuxt buildではビルドファイル、nuxt genearteでは静的ファイルが生成される結果は同じ
  • SPA(target: server)のときに限り、nuxt buildでビルドファイルだけでなく静的ファイルも生成される
  • targetが異なっていてもmodeが同じであれば成果物は変わらない
  • modeが異なる、つまりSPAかSPAじゃない(SSR or SSG)かで成果物が異なる
  • nuxt buildによる成果物は、SSR/SSGの場合はサーバーとクライアント、SPAの場合はクライアントのみのビルドファイル
  • nuxt generateによる成果物は、SSR/SSGの場合は各画面、SPAの場合はルートのみの静的ファイル

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考記事


  1. SSGは『静的化』や『静的ファイル生成』などとも呼ばれることがあります。 

  2. NUXTJS『コマンド一覧』 

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

【備忘】google apps script でslack通知

usage

sendSlack("hoge");

関数

function getSlackURL(){
    return "*** slack webhook ***";
}
function sendSlack(text){

    const channel = "#channel_name";
    const data = {
        'channel': channel,
        'username': 'bot',
        'text': text,
        'icon_emoji': ':ghost:'
    };
    const option = { 
        "method": "post",
        "payload": JSON.stringify(data),
        "muteHttpExceptions": true 
    };
    UrlFetchApp.fetch(getSlackURL(), option);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsで名前の表示/非表示を切り替える

はじめに

ボタンを押すと画面上にある名前の表示と非表示の切り替えができるプログラムを書いていきます。

ソースコード

全体のソースコードは以下の通りになります。(headタグやvueの読み込み箇所は省きます。)

HTMLファイルに書くソースコード

        <div id="app">
            <button @click="toggleBtn">クリックしてね</button>
            <p v-if="show">
                {{name}}
            </p>
        </div>

jsファイルに書くソースコード

var app = new Vue({
    el: "#app",
    data: {
        name: "Naoki",
        show: true,
    },
    methods: {
        toggleBtn: function () {
            this.show = !this.show;
        },
    },
});

ソースコードの解説

コードを書く順番で見ていきます。

条件分岐で名前を表示させる

    <!-- 条件分岐 -->
       <p v-if="show"> 
           {{name}} <!-- nameの出力 -->
       </p>

showがtrueの場合、nameのデータが出力されます。

//保持しているデータ
    data: {
        name: "Naoki",
        show: true,
    },

nameに"Naoki"という文字データとshowにブーリアン型の true が入っている状態です。

ここでの処理内容はv-if="show"で条件分岐が行われ、今のshowのデータの中身はtrueで条件を満たしているので、nameに入っている"Naoki"という文字データが画面上に出力されることになります。
※コメント欄に補足説明があるので、よろしければご覧ください。

クリックで名前の表示/非表示を切り替える

<!-- クリックイベントでの関数の呼び出し -->
<button @click="toggleBtn">クリックしてね</button>

ボタンをクリックするとtoggleBtnの処理が呼び出されます。

// 関数の設定
    methods: {
        toggleBtn: function () {
            this.show = !this.show;
        },
    },

toggleBtnが呼び出されると、反転したshowshowに代入されます。
ちなみに、thisというのは、jsファイルのappを指しています。

ここでの処理内容は、ボタンをクリックするとtoggleBtnの処理が呼び出されて、showデータのtruefalseが切り替わる仕組みになっています。この切り替えによって、名前の表示/非表示ができるようになるというわけです。

おわりに

はじめてQiitaに記事を書いたのですが、こんなに簡単な処理内容でも文字にして説明するとなると全然スムーズにいかず、かなり頭を使いました。人に教えることに慣れていないので、これからもっと練習して人に教えるスキルとプログラミングスキルを向上させていきたいと思います。

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

vue-youtubeを使用して、複数のモーダル上で動画を再生させる

実装結果

git映像

動画.gif

実装サイトURL

https://miwa-vue-youtube.netlify.app/

実装コード

  • ディレクトリ

src
├── App.vue
├── assets
│   └── scss
│       └── main.scss
├── components
│   ├── MovieButton.vue
│   └── MovieModal.vue
├── main.js
└── plugins
    └── vue-youtube.js
  • バージョン
"@vue/cli": "4.4.6",
"vue": "^2.6.11",
"vue-youtube": "^1.4.0"

コードの内容を見る
  • src/App.vue
<template>
  <div id="app">
    <movie-button
      :btn-num="1"
      :movie-id="movieData[0]"
      @modal-open="modalOpen($event)"
    />
    <movie-button
      :btn-num="2"
      :movie-id="movieData[1]"
      @modal-open="modalOpen($event)"
    />
    <movie-button
      :btn-num="3"
      :movie-id="movieData[2]"
      @modal-open="modalOpen($event)"
    />
    <movie-modal
      :modal-content="modalContent"
      :is-open="isOpen"
      @modal-close="modalClose()"
    />
  </div>
</template>

<script>
import MovieButton from "./components/MovieButton";
import MovieModal from "./components/MovieModal";
export default {
  name: "App",
  components: {
    MovieButton,
    MovieModal
  },
  data() {
    return {
      isOpen: false,
      modalContent: {},
      movieData: ["r8bECyGsw6Q", "2MqjzMeD3Uo", "4Vsi174LRgg"]
    };
  },
  methods: {
    modalOpen(event) {
      console.log(event);
      this.modalContent = event;
      this.isOpen = true;
    },
    modalClose() {
      this.isOpen = false;
    }
  }
};
</script>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
  • src/components/MovieButton.vue
<template>
  <transition name="fade" mode="out-in">
    <div class="modal" v-if="isOpen">
      <div class="modal__overlay" @click="onClick()"></div>
      <div class="modal__body">
        <p>モーダル</p>
        <div class="youtube__wrapper">
          <youtube :video-id="modalContent.movieId" ref="youtube"></youtube>
        </div>
        <button type="button" @click="onClick()">ボタン閉じる</button>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  props: {
    modalContent: {
      type: Object
    },
    isOpen: {
      type: Boolean
    }
  },
  methods: {
    onClick() {
      this.$emit("modal-close");
    }
  }
};
</script>

<style lang="scss" scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
.modal__overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #0008;
}
.modal__body {
  position: relative;
  top: 50%;
  right: 0;
  bottom: 0;
  left: 0;
  max-width: 1000px;
  margin: auto;
  background: #fff;
  transform: translateY(-50%);
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>
  • src/components/MovieModal.vue
<template>
  <button type="button" @click="onClick()">動画{{ btnNum }}</button>
</template>

<script>
export default {
  props: {
    btnNum: {
      default: 0,
      type: Number
    },
    movieId: {
      type: String
    }
  },
  methods: {
    onClick() {
      this.$emit("modal-open", {
        movieId: this.movieId
      });
    }
  }
};
</script>

<style lang="scss" scoped></style>

  • src/plugins/vue-youtube.js
import Vue from "vue";
import VueYoutube from "vue-youtube";

Vue.use(VueYoutube);
  • src/main.js
import Vue from "vue";
import App from "./App.vue";

import "./plugins/vue-youtube.js";

Vue.config.productionTip = false;

require("@/assets/scss/main.scss");

new Vue({
  render: h => h(App)
}).$mount("#app");
  • src/assets/main.scss
.youtube__wrapper {
  position: relative;
  width: 100%;
  margin: 0 auto;
  height: 0;
  padding-bottom: 56.25%;
  overflow: hidden;
  background: #aaa;

  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
}

実装手順

1. vue-youtubeインストール

vue-youtube - npm

$ npm install vue-youtube // or yarn add vue-youtube

2. vue-youtube記載

  • src/plugins/vue-youtube.js
import Vue from "vue";
import VueYoutube from "vue-youtube";

Vue.use(VueYoutube);
  • src/main.js
import "./plugins/vue-youtube.js";

※Nuxt.jsの場合、pluginsに記載

export default{
  plugins: ["~plugins/vue-youtube.js"]
}

参考リンク

3. ボタン追加

  • src/components/MovieButton.vue
<template>
  <button type="button" @click="onClick()">動画{{ btnNum }}</button>
</template>

<script>
export default {
  props: {
    btnNum: {
      default: 0,
      type: Number
    },
    movieId: {
      type: String
    }
  },
  methods: {
    onClick() {
      this.$emit("modal-open", {
        movieId: this.movieId
      });
    }
  }
};
</script>

<style lang="scss" scoped></style>
  • src/App.vue
<template>
  <div id="app">
    <movie-button
      :btn-num="1" 
      :movie-id="movieData[0]"
      @modal-open="modalOpen($event)"
    />
    <movie-button
      :btn-num="2"
      :movie-id="movieData[1]"
      @modal-open="modalOpen($event)"
    />
    <movie-button
      :btn-num="3"
      :movie-id="movieData[2]"
      @modal-open="modalOpen($event)"
    />
  </div>
</template>

<script>
import MovieButton from "./components/MovieButton";
export default {
  components: {
    MovieButton
  },
  data() {
    return {
      isOpen: false,
      modalContent: {},
      movieData: ["r8bECyGsw6Q", "2MqjzMeD3Uo", "4Vsi174LRgg"]
    };
  },
  methods: {
    modalOpen(event) {
      this.modalContent = event;
      this.isOpen = true;
    }
  }
};
</script>

movie-buttonについて

  • btn-num
    ボタンの番号を識別させるためのもの(今回の実装では必要なし)
  • movie-id
    動画のidで動画の内容を識別させるためのもの(管理しやすくするため、data内のmovieDataに登録)
  • @modal-open="modalOpen($event)"
    子コンポーネントのクリックイベントを検知して、実行(モーダルを開くためのもの)

参考

4. モーダル実装

  • src/components/MovieModal.vue
<template>
  <transition name="fade" mode="out-in">
    <div class="modal" v-if="isOpen">
      <div class="modal__overlay" @click="onClick()"></div>
      <div class="modal__body">
        <p>モーダル</p>
        <div class="youtube__wrapper">
          <youtube :video-id="modalContent.movieId" ref="youtube"></youtube>
        </div>
        <button type="button" @click="onClick()">ボタン閉じる</button>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  props: {
    modalContent: {
      type: Object
    },
    isOpen: {
      type: Boolean
    }
  },
  methods: {
    onClick() {
      this.$emit("modal-close");
    }
  }
};
</script>

<style lang="scss" scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
.modal__overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #0008;
}
.modal__body {
  position: relative;
  top: 50%;
  right: 0;
  bottom: 0;
  left: 0;
  max-width: 1000px;
  margin: auto;
  background: #fff;
  transform: translateY(-50%);
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>
  • src/App.vue
<template>
  <div id="app">
    <movie-modal
      :modal-content="modalContent"
      :is-open="isOpen"
      @modal-close="modalClose()"
    />
  </div>
</template>

<script>
import MovieModal from "./components/MovieModal";
export default {
  name: "App",
  components: {
    MovieModal
  },
  data() {
    return {
      isOpen: false,
      modalContent: {},
      movieData: ["r8bECyGsw6Q", "2MqjzMeD3Uo", "4Vsi174LRgg"]
    };
  },
  methods: {
    modalClose() {
      this.isOpen = false;
    }
  }
};
</script>
  • src/assets/main.scss
.youtube__wrapper {
  position: relative;
  width: 100%;
  margin: 0 auto;
  height: 0;
  padding-bottom: 56.25%;
  overflow: hidden;
  background: #aaa;

  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
}

movie-modalについて

  • modal-content
    モーダルに送る情報(今回はmovieIdのみ)
  • is-open
    モーダルが開いているか判定させるもの
  • @modal-close="modalClose()"
    子コンポーネントのクリックイベントを検知して、実行(モーダルを閉じるためのもの)

参考リンク

1. モーダルを開いたときに自動で再生を行う

PCのみ自動再生を行いたい場合 autoplay = 1を追加

<template>
  <youtube
    :video-id="modalContent.movieId"
    ref="youtube"
    :player-vars="playerVars"
  ></youtube>
</template>
<script>
  export default {
    data() {
      return {
        playerVars: {
          autoplay: 1
        }
      };
    },
  }
</script>

参考リンク

autoplay = 1のみだとSPなどのデバイスで対応することができないため、SPでも対応させたい場合、無音でインライン再生に変更を行うことで自動再生を行うことができる。

<template>
  <youtube
    :video-id="modalContent.movieId"
    ref="youtube"
    :player-vars="playerVars"
    @ready="ready"
  ></youtube>
</template>
<script>
export default {
  data() {
    return {
      playerVars: {
        playsinline: 1
      }
    };
  },
  methods: {
    async fetchYoutube() {
      await (this.isOpen = ture);
      this.$refs.youtube.fetchData();
    },
    ready() {
      const youtubePlayer = this.$refs.youtube.player
      youtubePlayer.mute()
      youtubePlayer.playVideo()
    }
  }
};
</script>

参考リンク

2. 字幕を自動で追加を行う

cc_lang_pref = 1に設定を行うことで字幕を追加できる

スクリーンショット 2020-08-09 17.52.34.png

export default {
  data() {
    return {
      playerVars: {
        cc_lang_pref: 1 // cc_lang_prefを1に行う
      }
    };
  },
}

参考リンク

詰まった部分について

1. youtubeのスタイルが一部ずれる

  • vueコンポーネント内に
.youtube__wrapper {
  position: relative;
  width: 100%;
  margin: 0 auto;
  height: 0;
  padding-bottom: 56.25%;
  overflow: hidden;
  background: #aaa;

  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
}

のような記載を行うと iframe内のコンポーネントにスタイルが当たらないため以下のような状態になる。
スクリーンショット 2020-08-09 16.05.30.png

また、スタイルを当てていないとモーダルを閉じる時、動画の高さがなくなり、以下のような状態になる。

スクリーンショット 2020-08-09 16.06.07.png

なので、グローバルcssの場所に以下を記載

.youtube__wrapper {
  position: relative;
  width: 100%;
  margin: 0 auto;
  height: 0;
  padding-bottom: 56.25%;
  overflow: hidden;
  background: #aaa;

  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
}

参考リンク

2. v-ifを用いるとthis.$refsが取得できない

v-ifを用いるとコンポーネントが描画されていない状態でthis.$refsの内容を取得しようとするので、取得できないエラーになる。
なので、async/awaitを用いることになり、コンポーネント描画 → $refs取得を行うことができるようにする

async fetchYoutube() {
  await (this.isOpen = ture); // isOpenフラグがtrueになったら
  this.$refs.youtube.fetchData(); // this.$refs.youtubeの取得を行う
},

参考リンク

3. モーダルが上下中央によらない

.modal__body {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  max-width: 1000px;
  margin: auto;
  background: #fff;
}

のような状態で配置した場合、

スクリーンショット 2020-08-09 17.42.06.png

のような状態で、高さが取得されないため、 position: relativetop: 50%などで変更を行う

.modal__body {
  position: relative; /* relativeに変更 */
  top: 50%; /* 50%に変更 */
  right: 0;
  bottom: 0;
  left: 0;
  max-width: 1000px;
  margin: auto;
  background: #fff;
  transform: translateY(-50%); /* -50%を追加 */
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue-CLI 3でnpm run serveが失敗するときの解決方法

事象

Dockerコンテナ内の環境にて、npm run serveを実行すると、下記のようなエラーが発生していました。

ERROR Failed to compile with 2 errors

This relative module was not found:

* ./src/main.js in multi (webpack)-dev-server/client/index.js (webpack)/hot/dev-server.js ./src/main.js, multi (webpack)/hot/dev-server.js (webpack)-dev-server/client/index.js ./src/main.js

環境

  • Microsoft Windows 10 Pro
  • Docker for Windows
  • Visual Studio Code
  • node.js : 12.18.2
  • npm : 6.14.5

原因

筆者の場合、npm installを実行してもpackage.json内に含まれているdevDependenciesがインストールされておらず、必要なライブラリ(ここでは@vue/cli-service)が存在していなかったというのが原因でした。

解決策

node.js側の環境変数であるNODE_ENVdevelopment(あるいはdev)にしてからnpm installを実行する必要がありました。

NODE_ENVを設定する方法は、①.コマンドラインからexportする方法と、docker-composeを使っている場合は②.docker-compose.xmlに記載する方法があります。

①.exportコマンドで設定する

下記のコマンドでNODE_ENVに環境値を設定できます。

# NODE_ENVを設定
$ export NODE_ENV=development

# 設定した値を確認する
$ echo $NODE_ENV
development

②.docker-compose.xmlのenvironmentキーに設定する

docker-composeでコンテナを立ち上げている場合、environmentキーを使って設定することができます。

version: '3'
services:
  web:
    build: .
    ports:
      - "8080:8080"
    environment:
     - NODE_ENV=development
    tty: true

exportで設定したときと同様に、$NODE_ENVをechoすると設定値を確認すると、developmentが設定されているのが確認できるはずです。

問題の原因を特定するには?

今回の場合、package.jsonに記載されているパッケージが正しくインストールできていると早とちりし、「パスが通っていない」か「windowsでシンボリックリンクが使えないのが悪さしている」と思い込んでいたのが、原因の特定が遅れてしまった原因でした。

ちなみに、パッケージが正しくインストールされているかは、npm list --depth=0で確認することができます。

# インストールされているパッケージの一覧(一階層のみ)を表示
$ npm list --depth=0
project@0.1.0 /app
+-- core-js@3.6.5
`-- vue@2.6.11
# core-js と vue しか入ってないやん!

もし、筆者と同様にnpm run serveが正しく動作しない場合は、このコマンドで確認してみるといいかもしれません。

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

テクトロジーによる実践的組織構造学

今回の記事では、ソ連の革命家、医師、哲学者、小説作家であったアレクサンダーボグダノフが提唱したテクトロジーと呼ばれる実践的組織構造学について紹介する。
テクトロジーでは組織が安定、成長、破綻する環境、条件について詳細に解説し、安定した組織を生成する手法について解説している。
テクトロジーの概念を拝借し、創造的な組織創造の手法を紹介したいと思う。

テクトロジーにおける組織の定義

テクトロジーでは、組織をオープンクローズ型の成長する有機体システムと定義している。
組織とは以下の要素で構成されている。
ビジョン・・・組織の目指すべき方向性。
経済・・・組織のボディ。巨大であるほど収容できる人の人数が増加する
金融・・・組織を循環する血液。
生産・・・もの、サービスを生産し、組織の経済を巨大化する手段。

これらの有機的な要素が相互作用し、成長することで組織という有機体が構成されていると考える。

テクトロジーにおける成長する有機体システムとは

テクトロジーでは、組織をオープンクローズ型の成長する有機体システムと定義している。
テクトロジーにおける組織の定義を中国の陰陽論によって説明することができる。
陰陽論とは、原初は混沌(カオス)の状態であると考え、この混沌の中から光に満ちた明るい澄んだ気、すなわち陽の気が上昇して天となり、重く濁った暗黒の気、すなわち陰の気が下降して地となった。この二気の働きによって万物の事象を理解し、また将来までも予測しようというのが陰陽思想である。
組織が外部からエネルギーを取り入れ、出力するインプット、アウトプットの運動を陰陽論における陽の気と捉えることができる。
逆に組織内部に沈殿し、成長し、ヒエラルキーを形成する秩序生成を担う運動を陰陽論における陰の気と捉えることができる。

テクトロジーにおける生産の定義

テクトロジーでは、組織における生産活動は以下の3つに分類されている。
人の生産・・・人に教育を施し、組織活動に従事する生産者を作成する
モノ、サービスの生産・・・外部から取得した資材を用いて、モノ、サービスの生産を行う。
アイデア・・モノ、サービスを生成するための知識、アイディアを作成する。
組織における生産活動を高めることで、組織の経済を成長させることができる。

テクトロジーおける組織のフォーム(形態)について

現実の世界で、人が活動を行う場合、必ず外部からの影響、抵抗を受ける。
外部からの影響、抵抗を抑えるために、組織は環境に合わせた最適なフォーム(形態)を取る必要がある。
魚やイルカなど、魚と哺乳類で種族は異なるが、水の抵抗を抑えるために同様の流線形フォルムを取っている。
組織のフォーム(形態)は外部環境によって決定される。
外部環境からの抵抗を最小限にするために、外部との接触の最小化、不要な組織的機能の削除などが求められる。
最適なフォーム(形態)によって、組織は外部からの抵抗を減少させ、健全に成長することができる。

テクトロジーにおける組織が不安定化する条件

テクトロジーにおける組織が不安定化する条件として以下の2点が挙げられる。
・外部からのエネルギー取得の減少・・外部から人、モノ、金の循環が減少することで組織のサイズ、経済を維持することできなくなる。
・ヒエラルキーシステムの固定化・・・ヒエラルキーシステムが巨大化し、組織が硬直化してしまう。
組織不安定化を回避する手法として以下の手段が有効とされている。
生産手段を研究、開発、更新を行い、組織の経済成長のスピードを増加させる。
組織が硬直化の原因になっているヒエラルキーシステムを解体し、適切なサイズに組み替える。

まとめ

アレクサンダーボグダノフテクトロジーに関するアイディアを発表した時期は1920年代である。
独学で組織が破綻する条件、環境を発見し、持続可能な成長のコンセプトを提唱したアレクサンダーボグダノフの先見性は恐るべきものである。
ソ連は軍事、IT、経済においてアメリカと張り合うことができた超大国だった。
ソ連時代に考えられたアイディア、思想などは現代においても見直されるべきものだと思われる。

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

Deno の公式レジストリの登録方法が手動から自動になりました

Deno の公式レジストリの登録方法が変わりました! 本記事ではその登録方法について解説します。

(この記事は Deno の公式レジストリに自分の書いたモジュールを登録してみたい人向けの記事です。npm にモジュールを登録した経験がある人などが主な対象読者です。)

Deno の公式レジストリとは

Deno の公式レジストリは https://deno.land/x にあります。(特に固有の名称などはありません。単にレジストリと呼ばれることが多いです。)

これまで、ここに自分の作ったモジュールを登録するには、公式ホームページのレポジトリにある JSON ファイルに PR を出して、
手動でマージしてもらうことが必要でした。

スクリーンショット 2020-08-09 14.16.47.png

(旧来のワークフローでは手動のレビューとマージをしていました)

この若干面倒だった登録フローが、8/3のアップデートで、
PR を出す必要のない自動的なワークフローに置き換わりました。

本記事では、その登録方法を解説します。

新レジストリの概要

まず新レジストリに登録されるものは GitHub レポジトリのタグのみになります。これまでは branch名 (master など) や commit hash などでも import 出来ていましたが、これらの import は出来なくなります。

OK
import { myFunc } from "https://deno.land/x/my_module@v1.0.0/my_func.ts";
NG
import { myFunc } from "https://deno.land/x/my_module@master/my_func.ts";
import { myFunc } from "https://deno.land/x/my_module@1e587a0/my_func.ts";

そしてタグの登録方法が少し特殊で、npm のようなコマンドによる登録ではなく、github から Webhook で Deno レジストリの API を叩くことでタグが登録される仕組みになっています。

以下ではこの Webhook の登録方法を解説します。

Webhook の登録方法

レジストリに登録したいレポジトリの Settings から Webhook のページに行き、以下のように Webhook を設定しましょう。

  • Payload URL: https://api.deno.land/webhook/gh/モジュール名 (モジュール名の部分は登録したいモジュール名に置き換える)
  • Content type: application/json
  • Events: Let me select individual events. を選び Branch or tag creation のみにチェック
  • その他はデフォルト設定

この設定をすることで、タグを作った時に Deno レジストリのタグ登録用 API に POST リクエストが飛ぶようになります。

この状態で実際にタグを作ってみましょう

git tag v0.1.0
git push origin v0.1.0

Webhook がうまく飛ぶと、Webhook 設定ページで以下のように、リクエストとレスポンスのログを見ることが出来ます。

スクリーンショット 2020-08-09 14.04.08.png

スクリーンショット 2020-08-09 14.05.25.png

この例では、筆者の kt3k/deno_license_checker という github レポジトリを deno.land/x/license_checker として登録しています。

登録がうまくいくと、deno.land 上の当該ページ ( https://deno.land/x/モジュール名 ) 上で作ったタグのバージョンが追加されます。

スクリーンショット 2020-08-09 14.07.25.png

モジュール登録の注意点

モジュールの各タグはイミュータブルになるという原則があります。したがって、同じタグを2回以上登録して上書きするような事は出来ません。何かを変えたい場合は必ずバージョンを上げましょう。

また、当然ですが、モジュールの乗っ取りも出来ないようになっています。ある GitHub レポジトリに紐付いたモジュール名に別の GitHub レポジトリからタグ登録しようとしてもエラーになります。

npm との違い

npm では npm publish というコマンドでパッケージのバージョンを登録する仕組みでしたが、これに相当するコマンドは用意されていません。Webhook を設定したレポジトリで、タグを切ってプッシュすることがイコール新しいバージョンを公開することになります。

なお npm の場合は publish する人の認証のために npm コマンドにログインすることが必要でした。Deno のレジストリではそのような認証の仕組みはありません。Deno のレジストリの場合は GitHub の特定のレポジトリとモジュールがリンクされる (かつそのリンクが変わることはない) ため GitHub 上での認証がモジュールの認証を兼ねていると言えます。

まとめ

本記事では、Deno の新しいレジストリでのモジュールの登録方法を紹介しました。Webhook を設定してレジストリに publish するという仕組みは若干目新しいですが、慣れれば難しい設定ではありません。ぜひ Deno の新しいレジストリにモジュールを登録してみましょう ?

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

【javascript】書籍のアウトプット

こちらの記事は以下の書籍を参考にアウトプットとして執筆しました。

入門JavaScriptプログラミング (日本語) 単行本

文字の検索

最初の文字が$かどうかの判断はこう書く

if(price[0]=='$'){

}

出典:入門JavaScriptプログラミング (日本語) 単行本

最初の3文字の場合はこうかもしれない

if(phone.substr(0,3)===user.areaCode){

}

substr

str.substr(start[, length])
引数 説明
start 最初の文字の位置
length 取り出す数

返り値は、指定された部分が入った新しい文字列

出典:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/substr

先のコードはphoneの最初の文字から3文字を抜き出している。

ここまでの2つのコードは何をしているのかわかりにくい
そこで自己文書化**が使える

文字列の検索では以下の3つのメソッドが使えるようになった

新しいメソッド 説明
includes その文字が含まれているか
startsWith その文字で始まっているか
endsWith その文字で終わっているか

戻り値はbool値
これらは大文字小文字を区別する

これらは第2引数に検索し始める位置を指定できる(インデックス)
第2引数にindexOfメソッドを使えば効率よく使える
indexOfは検索してマッチした文字の位置を返り値としている

文字列のパディング

特定の文字で特定の長さだけ埋めるというもの
例えば以下は10進数のIPアドレスを2進数へ変換する関数のコード

function binaryIP(decimalIPStr){
  return decimalIPStr.split('.').map(function(octet){
    return Number(octet).toString(2)
  }).join('.')
}

出典:入門JavaScriptプログラミング (日本語) 単行本

Array.prototype.map()

条件に合う新しい配列を生成する

構文
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
    // 新しい配列の要素を返す
}[, thisArg])
コールバック引数 説明
currentValue 現在処理中の値
index 現在処理中の要素の配列内のインデックス
array mapが実行されている配列
thisArg callbackを実行するときにthisとして使う値

このコードでは以下のようなことをしている

  • .split('.')でIPアドレスを4つの配列に分割
  • map(function(octed)により新しいい配列を生成
    • octedは現在処理中の値
  • toString()で引数に入る進数に変換する

以上のコードでは0梅がされないなどうまく行かない
そこでrepeatを使う

function binaryIP(decimalIPStr){
  return decimalIPStr.split('.').map(function(octet){
    let bin= Number(octet).toString(2)
    return '0'.repeat(8-bin.length)+bin
  }).join('.')
}

出典:入門JavaScriptプログラミング (日本語) 単行本

repeat

repeatが呼び出した文字列を繰り返す

'0'.repeat(4);//'0000'

0が4回繰り返される

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

【JS】空文字判定の注意

 空文字("")を判定したい!!

function isEmptyString(str) {
    return !Boolean(str);
}

console.log(isEmptyString("")); // => true
console.log(isEmptyString(0)); // => true
console.log(isEmptyString()); // => true

空文字でなくても暗黙的な型変換が起こってしまう。そのため、falsyな値はこの関数でtrueを返してしまう。

 こうしましょ

function isEmptyString(str) {
    // `string`型かつ`length`が0の場合に`true`を返す
    return typeof str === "string" && str.length === 0;
}

console.log(isEmptyString("")); // => true
console.log(isEmptyString(0)); // => false
console.log(isEmptyString()); // => false

 参考

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

聞いたことはあるがよくわからないJavaScript周辺のあれこれ

JavaScriptを学習していると、よくわからない概念やライブラリに出会う機会が多いです。
その中でも特によく耳にするものをざっくりまとめてみました。(ホントにざっくり)
各内容をもっと掘り下げた参考記事も貼っているので気になる方はそちらも読んでみてください。

ECMAScript

ECMAScriptとはJavaScriptの言語仕様の取り決め。
よく耳にする ES2015ES6 といった用語はJavaScriptのバージョンを表し、ここで出てくる ES がECMAScriptのこと。

【JavaScript】JavaScript、その前に〜ECMAScriptとは?

npm

Node Package Manager、すなわちNode.jsのパッケージを管理するもの。
npmのおかげで、 npm install 〇〇 と打つだけで便利なライブラリを簡単にインストールして利用することができる。

npmとは

yarn

2016年にFacebookが公開したかなり新しめのJavaScriptパッケージマネージャ。
役割はnpmとほぼ同じだが、npmと比べてインストール・セキュリティ・バージョン管理の面で優れている。

yarnとは

package.json

パッケージマネージャを用いてプロジェクトを作成する際に、プロジェクトが依存するパッケージに関する情報(さらにはプロジェクト全体に関する情報)を記録するファイルがpackage.json。
プロジェクトを動作させるために必要なパッケージをdependencies属性とdevDependencies属性に記述しておけば、npm install コマンドを打つだけでプロジェクト環境を復元できるため、非常に便利。

【初心者向け】NPMとpackage.jsonを概念的に理解する

Babel

BabelはJavaScriptのコンパイラ。
これを使うとJavaScriptのコードを新しい書き方から古い書き方へと変換してくれる。
ブラウザによって対応しているJavaScriptのバージョンや仕様が異なるので、各ブラウザの環境に合わせて記法を変換する必要がある。

【5分でなんとなく理解!】Babel入門
webpackとBabelの基本を理解する(1) ―Babel編―

webpack

webpackはモジュールバンドラ。
モジュールバンドラとは、複数のファイルを1つにまとめて出力してくれるツールのこと。
webpackはJSファイルだけでなく、CSSや画像ファイルも1つにまとめてくれる。
webpackを使えば、開発時には機能ごとにファイルを分割して開発を進めることができ、読み込み時には1つのファイルとして読み込めるので、非常に便利。

webpackってどんなもの?
webpackとBabelの基本を理解する(1) ―webpack編―

ESLint

ESLint は JavaScript のための静的検証ツール。
コードを実行する前に明らかなバグを見つけたり、括弧やスペースの使い方などのスタイルを統一したりするのに役立つ。

ESLint 最初の一歩

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

GASで毎朝その日の予定とかを通知するLINE Bot [第4回]

この記事について

前回の続きです。

コード解説

前回の続きをどんどん解説していきます。まずはこれ。

Flex Message

let flex = {
  'type': 'bubble',
  'size': 'giga',
  'body': {
    'type': 'box',
    'layout': 'vertical',
    'contents': [
      {
        'type': 'text',
        'text': today,
        'weight': 'bold',
        'size': 'xxl',
        'flex': 0
      }, {
        'type': 'box',
        'layout': 'horizontal',
        'contents': [
          {
            'type': 'filler'
          }, {
            'type': 'text',
            'text': weather,
            'size': 'lg',
            'color': '#444444',
            'flex': 0,
            'gravity': 'center'
          }
        ]
      }, {
        'type': 'box',
        'layout': 'horizontal',
        'contents': [
          {
            'type': 'filler'
          }, {
            'type': 'text',
            'text': temp_l + '',
            'size': 'lg',
            'color': '#3f51b5',
            'flex': 0
          }, {
            'type': 'text',
            'text': '/',
            'size': 'lg',
            'color': '#444444',
            'margin': 'xs',
            'flex': 0
          }, {
            'type': 'text',
            'text': temp_h + '',
            'size': 'lg',
            'color': '#f44336',
            'margin': 'xs',
            'flex': 0,
            'gravity': 'center'
          }
        ]
      }, {
        'type': 'separator',
        'margin': 'xl',
        'color': '#808080'
      }, {
        'type': 'box',
        'layout': 'vertical',
        'contents': [
          //EVENTS
        ],
        'margin': 'xl'
      }
    ]
  }
};

これがBotの送信するデータの中核になるわけです。これはFlex Messageというもので、LINEのメッセージをCSS Flexboxライクのフォーマットで表示するものなのだそう。
無の状態からこのデータを作ったわけではなく、LINEがシミュレータを公開してくれている。

右上のView as JSONでJSONデータを見ることができます。これをコピペして変数に置き換えるなどの処理をするだけでFlex Messageのデータを作れます。

祝日・ごみ収集日の表示

if (holiday != '') {
  flex.body.contents[1].contents.splice(0, 0, {
    'type': 'text',
    'text': holiday,
    'size': 'md',
    'color': '#808080',
    'flex': 0,
    'gravity': 'center'
  });
}

holidayの中身があるときに実行されます。
splice(start, count, data)でJSONデータに割り込む形でデータを挿入します。startで割り込む位置、countで割り込む際に元データから消去するstartからの要素の数を、dataに挿入するデータを指定します。
flex.body.contents[1].contents[0]の位置にデータを割り込ませ、countは0なので元のデータは1ずつずれて更新されます。

if (garbage[d.getDay()]) {
  let line = holiday == '' ? 1 : 2;
  flex.body.contents[line].contents.splice(0, 0, {
    'type': 'text',
    'text': garbage[d.getDay()],
    'size': 'md',
    'color': '#808080',
    'flex': 0,
    'gravity': 'center'
  });
}

ごみ収集日が0(=false)以外である時に実行されます。
その日が祝日なら2行目、祝日でないなら1行目に表示させるため、三項演算子を用いてlineを指定しています。
こちらも同じくsplice(start, count, data)で挿入。

予定の表示

予定にはTimeTreeに登録したデータを利用します。

const props = PropertiesService.getScriptProperties();

const TIMETREE_TOKEN = props.getProperty('TIMETREE_TOKEN');
const opt = {
  'headers': {
    'Authorization': 'Bearer ' + TIMETREE_TOKEN
  },
  'method': 'get'
};

propsには、GAS拡張サービスのPropertiesServicegetScriptProperties()を実行し、プロジェクトのプロパティに保存したAPIのトークンの配列を読み込んで代入しています。
getProperty(key)で指定したキーに対応する値を返します。

optには、UrlFetchApp.fetch(url, option)のオプションを設定しています。
主にデータを取得する際に用いられるGETリクエストでは、オプションには基本的に以下のような内容を利用できます。

{
  "method": "get",
  "headers": {
    /* header */
  }
}

"method""get""headers"にはヘッダというものを入れます。

今回のHTTPリクエストではAPIが要求するリクエストタイプ('method': 'get')とAPIキー(トークン)をAPI側に送信する必要があるため、ヘッダに認証情報を入れています。

このトークンは、IDやパスワードなしにAPIが利用可能なので、Bearerトークンとよばれており、'Authorization': 'Bearer XXXXXXXXという記法で認証を受けるのが一般的です。

const calendars = JSON.parse(UrlFetchApp.fetch('https://timetreeapis.com/calendars', opt).getContentText()).data;
const z = (t) => ('0' + t).slice(-2);

TimeTree API > カレンダー一覧の取得
https://timetreeapis.com/calendars
TimeTree API ドキュメント

calendarsにはリクエストして返ってきたカレンダー一覧のJSONデータが入ります。


const z = (t) => ('0' + t).slice(-2);
ここでは、tが1文字の'X'だった時、'0X'となる「ゼロ埋め」の処理になっています。
slice(n, m)は、配列のn番目からm番目まで(m番目は含まない)をコピーする関数です。
mを省略し、nに負の数を入れると、最後からn番目までをコピーします。
第3回でも書きましたが、文字列は配列の一種なのでこの関数を利用できます。最後から2文字分を取得できるわけです。

アロー関数V8 という記法で、zに関数を定義しています。

アロー関数
let x = (a, b) => {
  return a * b;
}

//1行にすると、その計算結果がそのまま返される
let x = (a, b) => a * b;


let ev_exists = false;
for (let calendar of calendars) {
  let cal = JSON.parse(UrlFetchApp
                       .fetch('https://timetreeapis.com/calendars/' + calendar.id + '/upcoming_events?timezone=Asia/Tokyo&days=1', opt)
                       .getContentText()).data;
  for (let event of cal) {
    let {title, start_at, end_at, all_day} = event.attributes;
    start_at = new Date(start_at);
    end_at = new Date(end_at);
    let time = all_day ? '終日' : z(start_at.getHours()) + ':' + z(start_at.getMinutes()) + '-' + 
      z(end_at.getHours()) + ':' + z(end_at.getMinutes());
    let schedule = {
      'type': 'box',
      'layout': 'horizontal',
      'contents': [
        {
          'type': 'text',
          'text': time,
          'flex': 0,
          'color': '#808080',
          'gravity': 'center',
          'size': 'md'
        }, {
          'type': 'text',
          'text': title,
          'size': 'lg',
          'weight': 'bold',
          'color': '#606060',
          'flex': 0,
          'gravity': 'center',
          'margin': 'lg'
        }
      ],
      'margin': 'sm'
    };
    flex.body.contents[4].contents.push(schedule);
    ev_exists = true;
  }
}

第3回ではfor-in関数を紹介しましたが、今回はfor-of関数V8 です。

for-of
let json = {a: 1, b: 3, c: 5};
let x = 0;
for (let data of json) {
  x += data;
}
// x => 9;

配列(連想配列含む)の要素ひとつひとつに対して処理を行う反復処理です。
配列の要素の数だけ実行されます。dataには要素のデータが入ります。

let cal = JSON.parse(UrlFetchApp
                       .fetch('https://timetreeapis.com/calendars/' + calendar.id + '/upcoming_events?timezone=Asia/Tokyo&days=1', opt)
                       .getContentText()).data;

ここでは、取得した全種類のカレンダーひとつひとつのその日の予定を取得します。
そこから先は、

  • 予定をひとつひとつ取り出して
  • 分割代入で時間とタイトルと終日かどうかのデータを取得して
  • 三項演算子でtimeに終日なら'終日'、そうでないなら時間をゼロ埋めして代入して
  • JSONデータ作って
  • Flex Messageに突っ込む

だけです。

もし予定が何もないなら、ev_existsfalseとなり、以下のコードが実行されます。

if (!ev_exists) {
  flex.body.contents.splice(3, 2);
}

splice(start, count, data)についてはすでにご紹介しましたが、ここでは、dataを省略すると、startからcountの分だけデータが削除されるという仕様を使っています。
予定の上に引かれている罫線と、予定を格納するボックスを削除しています。

Messaging APIに送信

さあ、いよいよ大詰めです。

const LINE_TOKEN = props.getProperty('LINE_TOKEN');
const payload = {
  'messages': [
    {
      'type': 'flex',
      'altText': today,
      'contents': flex
    }
  ]
};

const opt_line = {
  'headers': {
    'Content-Type': 'application/json; charset=UTF-8',
    'Authorization': 'Bearer ' + LINE_TOKEN
  },
  'method': 'post',
  'payload': JSON.stringify(payload)
};
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/broadcast', opt_line);

PropertiesServiceから、保存しているMessaging APIのトークンをLINE_TOKENに代入します。
今回のHTTPリクエストの種類はPOSTなので、GAS側からデータを送信します。
payloadにデータを代入します。
opt_lineUrlFetchApp.fetch(url, option)のオプションを設定します。
データを送信するので、今回はヘッダに'Content-Type'を設定します。データの形式です。
また、TimeTree APIと同様に認証情報を設定します。
POSTのリクエストの場合は、'method': 'post'となります。
'payload'にデータを入れることで、Massaging APIにメッセージのデータが送信されます。

まとめ

いかがでしたでしょうか。記事を書いている途中から力尽きそうになっていましたが、書ききることができました。
分割代入やアロー関数など、まだあまり使用例が多くないものをご紹介できたかと思います。
ご感想やご指摘などありましたらコメントいただけますと幸いです。
それでは、よいプログラミングライフを!

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

GASで毎朝その日の予定とかを通知するLINE Bot [第3回]

この記事について

前回の続きです。初回でご紹介したコードの解説をしていきます。知識のある方にはつまらない記事になるかと思います。

V8 ランタイムにする理由

letconstやアロー関数などといった、現在では標準的になったコードを使用するためです。
また、少しでも動作が低負荷でメモリも消費せず標準的なコードにするための選択でもあります。
解説中にV8 ランタイムからの機能が登場した際には、V8 と付けることにします。

コード解説

それでは、解説をしていきます。

  // Change these variables fit to your condition
  const garbage = [0, '資源ごみ', '可燃ごみ', 'カン・ビン', '不燃ごみ', '可燃ごみ', 'プラスチックごみ'];
  const area = '13101';

  const d = new Date();
  const today = (d.getMonth() + 1) + '' + d.getDate() + '日(' + '日月火水木金土'[d.getDay()] + '';

最初にユーザに変更してもらう項目、実行時の日付などを宣言しておきます。

'日月火水木金土'[d.getDay()]
変わっている箇所と言えばここでしょうか。文字列は文字コードの配列を扱いやすくしたものなので、所詮は配列と同じように処理できます。

  // これと同じことをしている
  const array = ['', '', '', '', '', '', ''];
  const d = new Date();
  const today = (d.getMonth() + 1) + '' + d.getDate() + '日(' + array[d.getDay()] + '';

祝日の取得

  const [event_holiday] = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com').getEventsForDay(d);
  const holiday = event_holiday ? event_holiday.getTitle() : '';

GASの拡張サービスCalendarAppを使用します。
getCalendarById(id)でカレンダーを取得します。
Googleカレンダーには、公式で祝日のカレンダーが存在します。
日本の祝日には、ja.japanese#holiday@group.v.calendar.google.comというIDのカレンダーが用意されているので、これを使います。
getEventsForDay(day)でその日のイベントを取得します。2つ以上の祝日が同じ日に来ることはないという前提で、event_holidayにはこの配列の1つ目の値を取り出して代入しています。

event_holidayの宣言方法に、分割代入V8 を使用しています。配列の任意の要素を簡潔な表記で変数に代入する機能です。

分割代入
let array = [3, 5, 7, 9, 11];
let [first, second, third] = array;
// first => 3, second => 5, third =>7

let [saisyo] = array; // 3
// これと同じ
let saisyo = array[0]; // 3

また、holidayには、三項演算子を使用しています。祝日がない日のevent_holidayは空配列の中身を参照するためundefinedになります。これは条件式上ではfalseと同じ挙動をします(0も同じ)。

三項演算子
let check = false;
let result = check ? "trueだよ" : "falseだよ"; // "falseだよ"

check = undefined; // 0, nullなどでも同じ
result = check ? "trueだよ" : "falseだよ"; // "falseだよ";

天気情報の取得

  let content = UrlFetchApp.fetch('https://static.tenki.jp/static-api/history/forecast/' + area + '.js').getContentText();
  content = JSON.parse(content.substring(content.indexOf('(') + 1, content.indexOf(');')));
  let {max_t: temp_h = "不明", min_t: temp_l = "不明", t: weather = "不明"} = content;

  const words = {
    '時々': '|',
    '一時': '|',
    'のち': '»',
    '': '',
    '': '',
    '': '',
    '': ''
  };
  for (let key in words) {
    weather = weather.replace(key, words[key]);
  }

拡張サービスUrlFetchAppを使用します。
fetch(url, option)でHTTPリクエストを送信することができます。第2引数のない状態では、特にオプションのないGETリクエストになります。リンク先のデータをそのままもらいます。
UrlFetchApp.fetch(url, option)ではレスポンスデータなるものが返ってきます。データを見たいのでgetContentText()で文字列として取得します。

天気情報の取得にはtenki.jpのデータの変なところから引っ張ってきています。
自分で使っていたときは気象庁のページのWebスクレイピングだったのですが、公開するにあたり一般化が非常に困難だと判明したので代替策を探っていました。

当初はWebスクレイピングにしようと思っていたのですが、ページのサイズが大きいので高負荷になってしまう懸念があり、APIを探し回っていたところたまたま発見しました。
tenki.jpで天気予報を見ていると画面上部に自分が最近閲覧した地域の天気予報が小さく表示されるんですが、それを取得するAPIがありました。

URLのフォーマットは
https://static.tenki.jp/static-api/history/forecast/XXXXX.js
XXXXXには標準地域コードが入ります。で、返ってくるのが
'__r__XXXXX({"i":"画像番号","j":"標準地域コード","max_t":"最高気温","min_t":"最低気温","n":"地域名","p":"降水確率","t":"天気"});'
画像番号はサイト表示用の画像の番号ですね。
https://static.tenki.jp/images/icon/forecast-days-weather/XX.pngで表示できるみたい。
今回はこのデータをありがたく使わせていただくことにしました。

substringで両端の要らない文字を捨てて、JSON.parse(str)(文字列をJSONデータに)に通すと、以下のデータができあがります。

content
{
  "i":"画像番号",
  "j":"標準地域コード",
  "max_t":"最高気温",
  "min_t":"最低気温",
  "n":"地域名",
  "p":"降水確率",
  "t":"天気"
}

ここでまた、分割代入の登場です。
let {max_t: temp_h = "不明", min_t: temp_l = "不明", t: weather = "不明"} = content;
ここでは、さらに高度なことをやっています。
前提として、以下のJSONデータを宣言しておきます。

let json = {a: 1, b: 3, c: 5};


連想配列やJSONデータの分割代入

let {a, b, c} = json;
// a => 1, b => 3, c => 5

配列の分割代入では[ ]を使用して宣言しましたが、連想配列(JSON)では{ }に入れて宣言します。

任意の名前の変数への代入

let {a, b: beta, c: charlie} = json;
// a => 1, beta => 3, charlie => 5


変数の初期値V8 の設定
変数の初期値は他にも関数などで適用可能です。

let {a, b: beta, c: charlie, d: delta = 10} = json;
// a => 1, beta => 3, charlie => 5, delta => 10



次に、天気の内容を絵文字に変換します。ここでは、for-in関数V8 を使用します。
先程のjsonを使ってご説明します。

for-in
let s = '';
let x = 0;
for (let key in json) {
  s += key;
  x += json[key];
}
// s => 'abc', x => 9

これは、配列(連想配列含む)の要素ひとつひとつに対して処理を行う反復処理です。
配列の要素の数だけ実行されます。keyには要素のインデックスまたはキーが入ります。
今回は連想配列なのでキーが入ります。'晴'とか。



次回に続きます。

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

GASで毎朝その日の予定とかを通知するLINE Bot [第2回]

この記事について

前回の続きです。前回は汚いコードを見せつけるだけという愚行を繰り広げたわけですが、この先で丁寧に解説してまいります。
今回は、Botの作成からMessaging APIのトークン取得,TimeTree APIのトークン取得,使用する方に合わせて書き換える箇所をご紹介します。

LINE Botを作成

さっそく、肝心のBotを作っていきましょう。
こちらから自分のLINEアカウントでログインし、以下の項目を入力してチャネル(=Bot)を作成します。

項目 内容
チャネルの種類 Messaging API
プロバイダー 自分のLINEアカウント
チャネル名 任意のBot名
チャネル説明 適当な説明
大業種 自分の業種
小業種 自分の業種
メールアドレス 自分のメールアドレス

規約類をよく読んで項目にチェックし、作成をクリック。
Botの作成が完了するとBotの設定画面に移動するので、Messaging API設定の項目に移動し、ボットのベーシックIDまたはその下のQRコードで友だち追加をしておきます。

LINE Messaging APIのトークンを取得

先程のページの最下部に チャネルアクセストークン という項目があります。
チャネルアクセストークン(長期)の右側にある発行をクリックすると、このBotのアクセストークンが発行されます。これをコピーします。

発行されたアクセストークンを、開いているGAS プロジェクトに登録します。
今回は、GASの標準機能のPropertiesServiceを使用します。

ファイルプロジェクトのプロパティ
スクリプトのプロパティ行の追加をクリック
プロパティLINE_TOKENにコピーしたトークンを貼り付け

以上でLINE Botの作成,プロジェクトへの登録は終わりです。

TimeTree APIのトークンを取得

こちらから自分のアカウントでログインし、トークンの作成をクリック。
トークン名を入力する画面になりますが、好きな名前で大丈夫です。
名前で後に影響が出ることはありません。判別しやすければOK。
ガイドラインをよく読みチェックを入れ作成をクリック。
作成したトークンが表示されますが、再表示されないという仕様なので要注意(また新しく発行すればOK)。

作成したトークンをLINE Messaging APIと同様にGAS プロジェクトに登録します。

ファイルプロジェクトのプロパティ
スクリプトのプロパティ行の追加をクリック
プロパティTIMETREE_TOKENにコピーしたトークンを貼り付け

最終的にスクリプトのプロパティ欄が以下のような状態になっていれば完了です。
fig1

コードを変更する箇所

前回ご紹介したコードで、使用する方によって変えなければいけない箇所のご説明。

notify
// Change these variables fit to your condition
const garbage = [0, '資源ごみ', '可燃ごみ', 'カン・ビン', '不燃ごみ', '可燃ごみ', 'プラスチックごみ'];
const area = '13101';

garbageには各曜日において収集されるごみの分別を配列で設定します。
回収がない日は0を設定します。
日曜始まりです。

areaには天気情報を表示したい地域の標準地域コードを設定します。
標準地域コード…国が定める地域ごとに割り振られたコードです。こちらから参照できます。

初回の実行

初回の実行で、Google カレンダーとの連携など、いくつかの権限が求められます。
自動実行では許可できないため、必ず一度は実行しましょう。
実行する関数をnotifyに設定し、をクリックしてみてください。
fig3

承認が要求されます。GASの承認欲求を満たしてあげましょう。許可を確認をクリック。

GASの開発中にログインしているGoogleアカウントでログインすると、以下のような表示が出ることがあります。これは、Googleが確認していないアプリケーションで連携を行なうためです。

左下の詳細をクリックします。

(スクリーンショットを撮り忘れたので「無題のプロジェクト」になっています、すみません)
Morning Assistant(安全ではないページ)に移動をクリックすると、このアプリに許可する内容が表示されます。
問題なければ許可をクリックします。するとBotが実行されます。

トリガーを設定する

毎朝このBotを実行させるには、トリガーを設定する必要があります。
fig2
矢印の指しているボタンをクリックすると、トリガーの一覧が表示されます。
画面右下のトリガーを追加をクリックし、下の画像のように設定します。

時刻を選択の項目には、自分が通知を受け取りたい時間を選択します。

以上でBotが機能するようになりました。次回はコードの詳しい解説をしていきます。
「動けばいいよ!」とか、「解説は要らないよ!」という方は、ここまで見るだけでOKです。お疲れ様でした!

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

GASで毎朝その日の予定とかを通知するLINE Bot [第1回]

この記事について

Qiita初投稿のうえ、プログラミング初心者です。至らぬ点がたくさんあるとは思いますが暖かく見守ってください…
Google Apps Scriptを用いて毎朝LINEでその日のいろいろを教えてくれるBotを作ったので解説も兼ねてご紹介します。
第1回, 第2回ではセットアップの説明を、第3回以降ではコードの詳細な解説をしていきます。

この記事で取り扱う内容

  • Google Apps Script (GAS)の基本
  • JavaScriptの基本~発展
  • LINE Messaging API
  • Flex Message
  • TimeTree API(予定取得)
  • Googleカレンダー(祝日取得)

実際どんな感じ?

下の画像のような通知が届きます。

表示されるもの

  • 日付
  • 祝日
  • 収集があるごみ
  • 天気(気温)
  • 予定(TimeTreeから)

GASプロジェクトを作成

こちらから新規のGASプロジェクトを作成します。

アドレスバーにscript.newで新規プロジェクトを作成できます。
.new ドメインを利用したショートカット一覧(英語)

注意すること

V8 ランタイムを搭載したプロジェクトでのみ機能します(新規プロジェクトは最初から搭載しています)
既存のプロジェクトを使う場合は、それがV8 ランタイムを搭載したプロジェクトであることを確認してください。
プロジェクトを開いた際にこのプロジェクトは Chrome V8 を搭載した新しい Apps Script ランタイムで実行しています。という表示があれば問題ありません(詳細)。

ええい、これが完成したコードだ!

このコードをコピーしてGASプロジェクトにドーン!!

コード.gs
function notify() {

  // Change these variables fit to your condition
  const garbage = [0, '資源ごみ', '可燃ごみ', 'カン・ビン', '不燃ごみ', '可燃ごみ', 'プラスチックごみ'];
  const area = '13101';


  const d = new Date();
  const today = (d.getMonth() + 1) + '' + d.getDate() + '日(' + '日月火水木金土'[d.getDay()] + '';


  const [event_holiday] = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com').getEventsForDay(d);
  const holiday = event_holiday ? event_holiday.getTitle() : '';


  let content = UrlFetchApp.fetch('https://static.tenki.jp/static-api/history/forecast/' + area + '.js').getContentText();
  content = JSON.parse(content.substring(content.indexOf('(') + 1, content.indexOf(');')));
  let {max_t: temp_h = "不明", min_t: temp_l = "不明", t: weather = "不明"} = content;

  const words = {
    '時々': '|',
    '一時': '|',
    'のち': '»',
    '': '',
    '': '',
    '': '',
    '': ''
  };
  for (let key in words) {
    weather = weather.replace(key, words[key]);
  }


  let flex = {
    'type': 'bubble',
    'size': 'giga',
    'body': {
      'type': 'box',
      'layout': 'vertical',
      'contents': [
        {
          'type': 'text',
          'text': today,
          'weight': 'bold',
          'size': 'xxl',
          'flex': 0
        }, {
          'type': 'box',
          'layout': 'horizontal',
          'contents': [
            {
              'type': 'filler'
            }, {
              'type': 'text',
              'text': weather,
              'size': 'lg',
              'color': '#444444',
              'flex': 0,
              'gravity': 'center'
            }
          ]
        }, {
          'type': 'box',
          'layout': 'horizontal',
          'contents': [
            {
              'type': 'filler'
            }, {
              'type': 'text',
              'text': temp_l + '',
              'size': 'lg',
              'color': '#3f51b5',
              'flex': 0
            }, {
              'type': 'text',
              'text': '/',
              'size': 'lg',
              'color': '#444444',
              'margin': 'xs',
              'flex': 0
            }, {
              'type': 'text',
              'text': temp_h + '',
              'size': 'lg',
              'color': '#f44336',
              'margin': 'xs',
              'flex': 0,
              'gravity': 'center'
            }
          ]
        }, {
          'type': 'separator',
          'margin': 'xl',
          'color': '#808080'
        }, {
          'type': 'box',
          'layout': 'vertical',
          'contents': [
            //EVENTS
          ],
          'margin': 'xl'
        }
      ]
    }
  };


  if (holiday != '') {
    flex.body.contents[1].contents.splice(0, 0, {
      'type': 'text',
      'text': holiday,
      'size': 'md',
      'color': '#808080',
      'flex': 0,
      'gravity': 'center'
    });
  }


  if (garbage[d.getDay()]) {
    let line = holiday == '' ? 1 : 2;
    flex.body.contents[line].contents.splice(0, 0, {
      'type': 'text',
      'text': garbage[d.getDay()],
      'size': 'md',
      'color': '#808080',
      'flex': 0,
      'gravity': 'center'
    });
  }


  const props = PropertiesService.getScriptProperties();

  const TIMETREE_TOKEN = props.getProperty('TIMETREE_TOKEN');
  const opt = {
    'headers': {
      'Authorization': 'Bearer ' + TIMETREE_TOKEN
    },
    'method': 'get'
  };
  const cals = JSON.parse(UrlFetchApp.fetch('https://timetreeapis.com/calendars', opt).getContentText()).data;
  const z = (t) => ('0' + t).slice(-2);

  let ev_exists = false;
  for (let calendar of calendars) {
    let cal = JSON.parse(UrlFetchApp
                         .fetch('https://timetreeapis.com/calendars/' + calendar.id + '/upcoming_events?timezone=Asia/Tokyo&days=1', opt)
                         .getContentText()).data;
    for (let event of cal) {
      let {title, start_at, end_at, all_day} = event.attributes;
      start_at = new Date(start_at);
      end_at = new Date(end_at);
      let time = all_day ? '終日' : z(start_at.getHours()) + ':' + z(start_at.getMinutes()) + '-' + 
        z(end_at.getHours()) + ':' + z(end_at.getMinutes());
      let schedule = {
        'type': 'box',
        'layout': 'horizontal',
        'contents': [
          {
            'type': 'text',
            'text': time,
            'flex': 0,
            'color': '#808080',
            'gravity': 'center',
            'size': 'md'
          }, {
            'type': 'text',
            'text': title,
            'size': 'lg',
            'weight': 'bold',
            'color': '#606060',
            'flex': 0,
            'gravity': 'center',
            'margin': 'lg'
          }
        ],
        'margin': 'sm'
      };
      flex.body.contents[4].contents.push(schedule);
      ev_exists = true;
    }
  }

  if (!ev_exists) {
    flex.body.contents.splice(3, 2);
  }

  const LINE_TOKEN = props.getProperty('LINE_TOKEN');
  const payload = {
    'messages': [
      {
        'type': 'flex',
        'altText': today,
        'contents': flex
      }
    ]
  };

  const opt_line = {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_TOKEN
    },
    'method': 'post',
    'payload': JSON.stringify(payload)
  };

  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/broadcast', opt_line);
}

もちろん、これだけでは機能しません。

まだまだ設定することがありますが、長くなりましたので、その手順は次の記事にてご紹介します。

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

AnglarのビルドとFirebaseのデプロイを連動させる

この記事はAngular+Firebaseでチャットアプリを作るのエントリーです。
前記事:WEBアプリでFirebaseのデプロイ環境を構築する

この記事で行うこと

前回の記事ではFirebaseのステージング運用とデプロイ方法について書きましたが、今回の記事ではAngularとFirebaseのデプロイを連動させる方法について書いていきます。

Angularのデプロイ

Angularに限らず、近年のJavaScriptアプリケーションでは開発環境と本番環境のビルドファイルを分けて作成することが多いです。開発環境ではデバッグをする必要があるため、SourceMapの表示を許容し、デバッグのしやすさを重視します。一方、本番環境ではファイルの読み込み速度や機密性が重視されるため、ファイルをminifyし、余計な情報を減らす作業が必要になります。

このビルド作業の使い分けは自力でやろうとすると面倒が多いため、webpackやgulpといったタスクランナーを使って管理することがデファクトスタンダードになっています。
Angularでも例に漏れず、裏側ではwebpackを使ってタスク処理をしているようですが、webpack.config.jsではなく、angular.jsonにビルド方式やテストツールの設定情報を記載しています。
「自前のwebpackでやりたい!」という方向けの@angular-builders/custom-webpackというものもあるので、自分で設定を書きたい方は以下のリンクを参考にしてください。

参考:Angular + カスタマイズWebpack 開発環境構築

実装内容

AngularとFirebaseを連動させてデプロイを行う場合は、AngularFireのng deployを使用します。このコマンドにより、開発環境に応じたビルド、デプロイを実現できるようになります。
AngularFireについては既に以前の記事でインストール方法等について紹介していますので、この記事から参照された方はそちらを確認してください。

参考:https://angular.io/guide/deployment

注1)ng deploy@angular/cliに導入されたのはv8.3.0以降となります。それ以前の@angular/cliを利用している場合は使用できませんのでご注意ください。
注2)@angular/cling deployは2020年の5月にバグが報告され、修正されています。下記の実装はそれ以前の@angular/cliだとエラーがでますので、最新の状態にしてください。
参考:https://github.com/angular/angular-cli/issues/17613

なお、今回使用するFirebaseのプロジェクトは2つで、次の様に設定します。

環境 Project ビルド方式
開発環境 開発用 開発用
ステージング環境 開発用 本番用
本番環境 本番用 本番用

開発環境にデプロイする

まず、angular.jsonのソースコードを確認します。

angular.json
"deploy": {
 "builder": "@angular/fire:deploy",
 "options": {},
}

@angular/cliv8.3.0以降のangular.jsonであれば、最初からng deployのための設定が記載されています。とりあえずこの状態でng deployを実行すると、ng build --prodコマンドを打った時と同じ挙動をします。

ただ、開発環境ではデバッグをする必要があるため、--prodをしないように設定する必要があります。

angular.json
"deploy": {
 "builder": "@angular/fire:deploy",
 "options": {
     "buildTarget": "NgChat:build",
 },
}

これで開発環境の設定は完了です。
ng deployをして、開発環境の挙動を確認します。

実行結果

デプロイ後の画面(ソースマップが確認できる)
スクリーンショット 2020-08-08 23.17.59.png

ステージング環境にデプロイする

まず、staging環境用の環境設定ファイルを作成します。
開発環境のsrc/environments/environment.tsをコピーし、productionをtrueにします。

src/environments/environment.staging.ts
export const environment = {
  production: true,
  firebase: {
    apiKey: '<your-key>',
    authDomain: '<your-project-authdomain>',
    databaseURL: '<your-database-URL>',
    projectId: '<your-project-id>',
    storageBucket: '<your-storage-bucket>',
    messagingSenderId: '<your-messaging-sender-id>',
    appId: '<your-app-id>',
    measurementId: '<your-measurement-id>',
  },
};

次にangular.jsonでビルド情報を設定します。buildオブジェクトにあるconfigurationsを見ると、すでにproduction用の設定は記載されているので、この内容をstaging用に修正します。
*設定ファイルパス以外は変更していません。

angular.json
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.staging.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            }
          }

ここに書いてある設定情報が何を意味しているのかをざっくりまとめると、以下のようになります。

項目名 説明
fileReplacements ファイル差し替え。主にenvironmentファイルに使われる。
optimization 最適化する。{scripts: true, styles: false} というような個別設定も可能
outputHashing 出力ファイル名にハッシュをつける。キャッシュバスター。"none","all","media","bundles"
sourceMap sourceMapを出力する。{scripts: true, styles: true, hidden: false, vendor: true}というような個別設定も可能
extractCss グローバル指定のcssを展開する
namedChunks 遅延読み込みのファイルに名前をつける
extractLicenses 利用ライブラリのライセンスファイルをまとめる
vendorChunk ライブラリだけで単独のファイルにする。ライブラリは変更頻度が低いため。
buildOptimizer aot利用時、@angular-devkit/build-optimizerを有効にする
budgets 生成ファイルのファイルサイズ制限を設定できる。気軽に巨大なライブラリをimportすると使わないコードが大量に含まれてしまったりするのを警告する。

この内容は以下記事からの抜粋です。他のパラメータについてはそちらを参照してみてください。
参考:angular.jsonの中身

ここで指定した内容をもとにデプロイできるよう、angular.jsonのデプロイ情報に追記します。

angular.json
        "deploy": {
          "builder": "@angular/fire:deploy",
          "options": {
            "buildTarget": "NgChat:build"
          },
          "configurations": {
            "staging": {
              "buildTarget": "NgChat:build:production"
            }
          }
        }

これで準備が整いました。
あとは-cオプション(--configurationのエイリアス)をつけてデプロイします。

ng deploy -c staging

実行結果

デプロイ後の画面(ソースマップが確認できず、エラーは表示される)
スクリーンショット 2020-08-08 23.50.35.png

本番環境にデプロイする

最後に本番環境のデプロイ設定します。
Angularのプロジェクトにあるenvironment.prod.tsファイルにfirebaseの情報を追記します。この際、本番用プロジェクトのパラメータを使用してください。

src/environments/environment.prod.ts
export const environment = {
  production: true,
  firebase: {
    apiKey: '<your-key>',
    authDomain: '<your-project-authdomain>',
    databaseURL: '<your-database-URL>',
    projectId: '<your-project-id>',
    storageBucket: '<your-storage-bucket>',
    messagingSenderId: '<your-messaging-sender-id>',
    appId: '<your-app-id>',
    measurementId: '<your-measurement-id>',
  },
};

次にangular.jsonに本番用プロジェクトの追加をします。
servetestは開発・ステージング環境でのみ使用するため、本番用のプロジェクトからは除外しています。

angular.json
    "NgChatProd": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/NgChat",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "aot": true,
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "node_modules/bootstrap/dist/css/bootstrap.min.css",
              "src/styles.css"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            }
          }
        },
        "deploy": {
          "builder": "@angular/fire:deploy",
          "options": {}
        }
      }
    }

この設定だけだどhostingデプロイ時にエラーがでるため、Firebase側の設定ファイルにも追記をします。
今回、ステージング用のFirebaseプロジェクトは使用しないため、開発用と本番用の2プロジェクトを作成します。

.firebaserc
{
  "projects": {
    "dev": "開発用FirebaseプロジェクトID",
    "prod": "本番用FirebaseプロジェクトID"
  },
  "targets": {
    "開発用FirebaseプロジェクトID": {
      "hosting": {
        "NgChat": [
          "開発用FirebaseプロジェクトID"
        ]
      }
    },
    "本番用FirebaseプロジェクトID": {
      "hosting": {
        "NgChatProd": [
          "本番用FirebaseプロジェクトID"
        ]
      }
    }
  }
}
firebase.json
    {
      "target": "NgChat",
      "public": "dist/NgChat",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    },
    {
      "target": "NgChatProd",
      "public": "dist/NgChat",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    }

これで本番環境の設定は完了です。ng deployの後にプロジェクト名をいれて実行します。

ng deploy NgChatProd

実行結果

デプロイ後の画面(ソースマップが確認できず、エラーは表示される)
スクリーンショット 2020-08-08 23.54.05.png

本記事をもって「Angular+Firebaseでチャットアプリを作る」のエントリーはすべて終了です。
お疲れ様でした。

ソースコード

この時点でのソースコード
※apiKeyは削除しているので、試すときは自身で作成したapiKeyを入れてください。

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