狛ログ

ラベル JavaScript の投稿を表示しています。 すべての投稿を表示
ラベル JavaScript の投稿を表示しています。 すべての投稿を表示

2022年2月24日木曜日

【JavaScript】メールテンプレート内のプレースホルダーを独自に式展開(置換)する方法。

2月 24, 2022
オフィス狛 技術部のyuckieee(ゆっきー)です。

■はじめに

今回は、私が関わったプロジェクトで「ユーザがメールテンプレートを自由に編集できるようにしたい!」という要望を受けて、その実現方法を検討・実装した際のお話です。
要件実現にあたって使用したのがJavaScriptの「replace」という関数なのですが、この関数が非常に使える子で...ふるえました(,,゚Д゚)。 ←初心者感丸出しなのと語彙力(笑)

今後も活用する場面がありそうなので、自分の理解向上と備忘録としてコチラにまとめます。

■今回の要件整理

最初に話したとおり、今回の要望はメールテンプレートをユーザが自由に編集・管理したいというものです。また、この要件には「メールの宛先によって一部の内容(名前とか、契約内容とか)はシステムで可変となるようにしたい」という要望も含まれています。
なので、ざっくりこんな感じの要件です。

[要件箇条書き]
     ①メールテンプレートは外部に保存可能とし、ユーザが自由に編集できるようにして欲しい
       ②ユーザが可変指定した箇所は、システムで自動的に置き換えて欲しい

      可変にしたいと言うのは、メールテンプレート内にプレースホルダーを埋め込みたいというイメージです。 そのため、イメージとしては、以下のようにユーザがメールテンプレートを定義したらプレースホルダー(${})部分は、該当する情報に置き換えられて欲しいという感じです。
      (まんま、JavaScriptのテンプレートリテラル内で式展開させる感じと同じです。)

      ■メールテンプレート
      ${last_name} {first_name} 様
      この度は、${company_name}の${plan_name}にご登録いただき、ありがとうございました。
       ↓
      ■メール送信内容
      オフ狛 ゆっきー 様
      この度は、株式会社オフ狛のスペシャルプランにご登録いただき、ありがとうございました。
      

      ■検討結果と実装内容

      テンプレートリテラレルを使えば解決じゃん?と思うのですが、外部から読み込んだ文字列にプレースホルダーが書いてあったとしても式展開されず、単なる文字列になっちゃうんですよね....(´・ω・)
      まぁ、そうですよね、、、ということで、結果、独自にプレースホルダー(${})を置換する関数を作ることにしました。それがコチラ↓

      /**
       * テンプレート置換処理
       * @param  {string} template // 外部から読み込んだメールテンプレート文字列
       * @param  {object} jsonObj  // メールテンプレート内のプレースホルダー置換対象情報
       */
       const templateRepalcer = (template, jsonObj) 
                               => template.replace(/\${([^}]+)}/g, (_, prop) => jsonObj[prop]);
      

      以上です!

      ■実装関数の使用例

      つづいて、実装した関数を使用して簡単な使い方例を以下に紹介します。

      // メールテンプレートのテンプレートリテラレルを顧客情報に置き換える関数を定義
       const templateRepalcer = (template, jsonObj) 
                               => template.replace(/\${([^}]+)}/g, (_, prop) => jsonObj[prop]);
      
      // メールテンプレート(!!実際は外部(DBなど)から読み込む!!)
      const mailTempalte = "${last_name} ${first_name} 様\nこの度は、株式会社オフ狛の${plan_name}にご登録いただきありがとうございました。"
      
      // 顧客情報(!!実際は外部(DBなど)から読み込む!!)
      const userInfo = {
          "user_id": "ID00001",
          "last_name": "オフ狛",
          "first_name": "ゆっきー",
          "plan_name": "スペシャルプラン",
          "mail_address": "yuckieee@officekoma.co.jp"
      }
      
      // メールテンプレートのプレースホルダーを顧客情報に入れ替え
      const mailContents = templateRepalcer(mailTempalte, userInfo);
      
      // 出来上がった変数を本文に編集すれば出来上がり!(この例ではコンソール出力のみ)
      console.log(mailContents);
      

      上記実行の結果、コンソールに出力される内容は以下のとおりです。

      オフ狛 ゆっきー 様
      この度は、株式会社オフ狛のスペシャルプランにご登録いただきありがとうございました。
      

      以降、実装関数について詳細を解説していこうと思います。

      ■関数の詳細説明

      この関数を説明する前にreplace関数についての知識が必要ですが、こちらの詳細は公式(?)ページを参照された方が分かりやすいですし、ここでの説明を割愛します。

      [replace関数の説明サイト]
      ・MDN Web Docs:String.prototype.replace()
      ・JavaScript Primer:文字列の置換/削除

      作成した関数は2つあり、replace関数を呼ぶ大元のtemplateReplacerという関数と、replace関数の第二引数で使用する無名関数です。 templateReplacer自体は、テンプレートと差し替えデータを引数に、replace関数を呼び出しているだけですので説明は省略します。
      以下より、replace関数の第2引数で指定している無名関数に焦点をあてて説明したいと思います。

      まず、replace関数全体を眺めてると、以下のとおり、第2引数は、第1引数の結果を引き継いで処理を行う作りとなっていることが分かります。

      template.replace(/\${([^}]+)}/g, (_, prop) => jsonObj[prop]);
      [置換の母体となる文字列].replace(第1引数:[置換対象を正規表現で指定], 第2引数:[第一引数の結果を元に置換処理]);

      無名関数を説明する上で、第1引数も重要となってくるため、第1引数と第2引数の両方を順に説明します。

      第1引数: 置換対象の文字列又は、正規表現を指定

      replace関数の第1引数には、置換対象にしたい文字列を固定で指定するか、正規表現を使って指定する事ができます。
      今回は、汎用性を高めるために正規表現を使用しています。
      なお、/\${([^}]+)}/の箇所は、書き方は色々あると思うので、正規表現をググってみてください。
      .?*とかでも行けると思います。
      ※()表記については、第2引数内で説明します。正規表現による抽出とは直接関わりません。

      また、/gを指定することで置換の母体となる文字列全体での抽出が可能になります。
      この指定を行わない場合は、対象が複数あった場合でも、最初に正規表現にひっかかった1件だけが置換される結果となるので注意が必要です。

      第2引数: 置換する文字列又は、置換処理を指定

      第2引数には、置換後の文字列を固定で指定するか、処理自体を指定することが出来ます。今回紹介した関数では、第2引数に無名関数を定義して、置換処理を行う形をとっています。
      なお、第1引数での結果をもとに、以下の引数を無名関数に引き渡すことが可能です。
      ※ブラウザによっては、5つ引数があるようですが、ここでは説明対象外とします。

      (match, p1, p2..., offset, string )

      それぞれの具体的な値について説明します。
      ※実装例で上げたメールテンプレート中の${last_name}を例に上げて説明します
      引数 説明
      match 第1引数の正規表現にマッチした文字列が設定されます。
      今回の例で言うところの${last_name}が該当箇所です。なお、今回使用しないため、関数では_と表記しています。
      p1,p2... 第1引数でキャプチャした文字列が引数として渡されます。
      キャプチャは、第1引数の正規表現内でキャプチャしたい箇所を()で囲むことで指定が可能です。
      複数指定した場合は、指定した分引数が増えていきます。今回は1対1で置換したかったので1つだけ指定しています。
      今回の例ではlast_nameという箇所が抜き出されることになります。
      offset 第1引数の正規表現でヒットした文字列の最初の位置を返します。${last_name}であれば、開始位置の0がセットされる感じです。今回は使用していません。
      string 検索対象全体がセットされます。今回でいうと"${last_name} ${first_name} 様\nこの度は、株式会社狛の"${plan_name}にご登録いただきありがとうございました。"です。今回は使用していません。

      上記の引数を元に置換対象を指定していますが、やっていることはJSONのキーを指定して、キーに紐づく値を取得しているだけです。

      (_, prop) => jsonObj[prop]

      今回の例では、以下のようなJSONが与えられていますので、第一引数でキャプチャしたキー(last_name)を元に、JSONのキーに紐づく値(オフ狛)を取得し、それを置換対象としています。

      {
          "user_id": "ID00001",
          "last_name": "オフ狛",
          "first_name": "ゆっきー",
          "plan_name": "狛スタンダードプラン",
          "mail_address": "yuckieee@officekoma.co.jp"
      }
      

      ■まとめ

      私は、今までreplace関数を単純に置換してくれるだけの関数ででしょ?って思っていました。
      ですが、こんなに汎用性の高い関数であるということを知って、目からウロコ...本当にreplace関数さん申し訳ありません...!!!ってなりました^^;
      特に引数として関数を指定するというスタイルは、自分の作る関数にも活かして行けると面白いなと感じます。

2021年3月29日月曜日

windows.openを使用した時に、iOSのSafariにて別タブでファイルを開けない場合の対処法。

3月 29, 2021

オフィス狛 技術部のmmm(むー)です。

タイトルの通りですが、windows.open()を使用した際にiOSのSafariにて別タブでファイルを開くことができなかったため、その対処法を書きます。

業務でしたかったこと
・フロント(Angular)にて、リンクを押下した後PDFを別タブで開く
 1. リンクを押下後、APIを呼ぶ
 2. API(Express)で、S3にあるPDFデータ(Blob)をレスポンスする
 3. フロントにてAPIからのレスポンスを取得した後、別タブを開きPDFを表示する

問題点
・iOS(Safari)だけリンクを押しても何も反応しない
・PCブラウザ(Chrome、 Firefox、 Sarfari)やiOSブラウザ(Chrome)はリンク押下後に別タブでPDFを開くことができる

原因
・Safariでは、非同期処理内でwindows.opon()を実行すると呼び出しをブロックするため
observable.subscribe(downloadData => {
	  const blob = new Blob([downloadData], { type: 'application/pdf' });
	  const fileURL = window.URL.createObjectURL(blob);
    window.open(fileURL, '_blank');
})

解決方法
・非同期処理が始まる前に、windows.open() を実行するように変更する
this.windowReference = window.open();  // <- 🌟変更

observable.subscribe(downloadData => {
	  const blob = new Blob([downloadData], { type: 'application/pdf' });
	  const fileURL = window.URL.createObjectURL(blob);
    this.windowReference.location = fileURL;  // <- 🌟変更
})

その他
・iOSのSafariはデフォルトの設定で、ポップアップブロックがONになっているので、OFFにすることで開けるようになります。
・ただ、別タブで開くか確認するポップアップが表示されますし、デフォルトはONになっています。
設定変更方法:iPhoneの設定 > Safari > ポップアップブロックをOFF

以上となります。
同じようにはまった方の参考になりましたら、幸いです🍭

2019年8月16日金曜日

node.jsでの日付時刻処理(moment.jsを使ってみる)

8月 16, 2019

オフィス狛 技術部のJoeです。

node.js(JavaScript)では日付時刻の扱いが難しく、特に文字列の時刻などを比較や計算するときはいつも調べているので、備忘録を兼ねてブログに書いておこうと思います。
また、日付時刻操作ではmoment.jsという便利なライブラリがあるので、同じ処理をmoment.jsでも書いてみます。

① 時刻(文字列)の比較と差分

【ライブラリを使用しない場合】
// Dateオブジェクトを生成
const dateTime1 = new Date('2019-08-01 09:00');
const dateTime2 = new Date('2019-08-01 14:00');

// 大小確認
if (dateTime1 < dateTime2) {
    処理;
}

// 一致確認の場合は「getTime()」でミリ秒形式に変換する
if (dateTime1.getTime() === dateTime2.getTime()) {
    処理;
}

// 差分算出の場合も「getTime()」でミリ秒形式に変換して計算、結果のミリ秒を欲しい単位に計算する
const diffTime = dateTime2.getTime() - dateTime1.getTime(); // ミリ秒で差分を算出
const diffSecond = Math.floor(diffTime / 1000 / 60); // 結果のミリ秒を分に変換
console.log(diffSecond); // 300(分)


【moment.jsを使用した場合】
const moment = require('moment');

// momentオブジェクトを生成
const dateTime1 = moment('2019-08-01 09:00');
const dateTime2 = moment('2019-08-01 14:00');

// 比較(<:isBefore  >:isAfter  <=:isSameOrBefore  >=:isSameOrAfter)
if (moment(dateTime1).isBefore(dateTime2)) {
    処理;
}

// 一致(第2パラメータで年から比較したい単位までを指定可能)
if (moment(dateTime1).isSame(dateTime2, 'minute')) { // 年~分までの一致確認
    処理;
}

// 差分(第2パラメータで取得したい差分の単位を指定可能)
const diffTime = dateTime2.diff(dateTime1, 'minutes'); // 差分を分で取得
console.log(diffTime); // 300(分)


② UTCからJSTに変換

【ライブラリを使用しない場合】
文字列の日時はUTCの時間と仮定しています。
// Dateオブジェクトを生成
const dateTime1 = new Date('2019-08-01 09:00'); // UTCの日時と仮定しています
// ミリ秒にして9時間(32,400,000ミリ秒)足す
dateTime1.setTime(dateTime1.getTime() + 1000 * 60 * 60 * 9);
console.log(dateTime1);  // Thu Aug 01 2019 18:00:00 GMT+0900 (日本標準時)


タイムゾーンを扱う場合は、「moment-timezone」を使います。
【moment.jsを使用した場合】
const momentTimezone = require('moment-timezone');

// UTCのmomentオブジェクトを生成
const dateTimeUtc = momentTimezone.tz('2019-08-01 09:00:11', 'UTC');
// JSTに変換(フォーマットも併せて変換)
const dateTimeJst = momentTimezone(dateTimeUtc).tz('Asia/Tokyo').format('YYYY/MM/DD HH:mm:ss');
console.log(dateTimeJst); // 2019/08/01 18:00:11



ご紹介したmoment.jsの機能はほんの一部ですので、日付時刻操作で悩んだらぜひ使ってみてください。


2018年3月27日火曜日

MANABIYA -teratail Developer Days- に行ってきた感想&レポート 2日目

3月 27, 2018

オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。

3月23日(金)、24日(土)の2日間、
「teratailがおくるITエンジニアの問題解決カンファレンス」というコンセプトのカンファレンス、
MANABIYA -teratail Developer Days- に行って来ました。

今回は2日目の感想&レポートです。
(1日目についてはこちらをご覧ください。)

というわけで、2日目も変わらず良い天気でした。

では、各セッションごとに感想を書いて行こうと思います。

【基調講演】技術者としての成長のための技術トレンド

まずは屋上で行われた「技術者としての成長のための技術トレンド」です。
SPEAKERは及川 卓也さん(@takoratta)です。
スライドはこちらです。

ご本人も後から「昔話の件」はツイートしていましたが、私としては、昔話が本当に面白かったです。
及川さんは華麗な経歴をお持ちなので、
「どうしてその会社に入ったのか?」「どうしてその会社を辞めて別の会社に入ったのか?」
どの話も興味深かったです。何より、その話が『技術トレンド』に繋がって行くんですよね。
(実際にその場で聞いてないと、なかなか伝わり難いのですが)

セッションでも、最後の方で「結局、気持ちが大事」とありました。
やっぱり「心踊らされる」ことが重要ですよね、成長には。

また、異なるドメインの事を学ぶ時、例え『仕事には不要』となっても、『仕事で必要なようにする』という動機付けで考えると良い、というのも納得でした。

「ワクワクする仕事をこれからして行きたいな」というのが、私の感想です。(小学生並みの感想になってしまった・・・)

【基調講演】エンジニアのための自分経営戦略

続いて、これまた屋上で「エンジニアのための自分経営戦略」というセッションを聴きました。
SPEAKERは西尾 泰和さん(@nishio)さんです。
スライドはこちらです。
ツイートの通り、西尾さんが、直前の及川さんのセッションを引用しつつの講演だったので、まさしく前編・後編のようでした。
「異なるドメインの事を学ぶことの重要性、心踊らされる事を学びに」というのは分かるけど、リソースは限られているよね、と。
知識を得る為の時間投資、時間というリソースの配分決定、これが自分経営である。
そして、そこから、知識獲得戦略としてのネットワーク作り・・・と、この辺は、実際にスライド見て頂いた方が良いです。

確かに、少しでも意識高いエンジニアは、とても忙しいんですよね。
もちろんそれが仕事であったり、勉強であったり・・・。
弊社では、エンジニアには積極的に勉強会に参加するように仕向けていますし、そういった事が可能な体制作りを行なっています。
何かを発信する、という意味だと、ブログも一つの手段です。
「Give&TakeはGiveから始まる」という事ですね。

ちなみに、機器トラブル等で、セッション中、何度か間が空いてしまいましたが、それが良い感じにアクセントになっていた気がします。
あと、きこりは少し休め。(スライド参照)

【生存戦略】技術顧問/テクニカルアドバイザーとしての生存戦略

こちら、諸事情で詳しい内容が書けません。
ただ、セッションは盛り上がったし、私自身も、現在同じような境遇で他社でアドバイザーとして活動させて頂いているので、共感出来る内容が多々ありました。

【Web】Web Application 2018 From Performance Perspective

ここまでWeb技術のセッションを見ていなかったので、是が非でも見たいセッションでした。
SPEAKERは古川 陽介さん(@yosuke_furukawa)です。
日本Node.jsユーザグループの代表の方です。スライドはこちらになります。

Webアプリケーションのパフォーマンスについての話でした。
過去から現在までのパフォーマンス改善の手段、Webの技術の変遷などをご説明頂けました。

スライド内にもありましたが、確かに、最近のWebアプリケーションで、JavaScriptのサイズが大きくなっている感ありますよね・・・。

余談ですが、パフォーマンス改善のバイブルは既に弊社の本棚にありました。
私だけ読んで無かったのは秘密にしながら、後でこそっと読んでおこうと思います。

興味深かったのは、これから先の未来のパフォーマンス改善でした。

あくまで予想として仰っていましたが、「レイテンシの壁を超える、ユーザーの動きを予測した投機的な先読み」です。
これに AI が関わって来ると、ユーザーが押しそうなリンクを予測して投機的に先読みする・・・なんて事が可能に??

後からツイートしたのですが、色々と問題もあるんだろうな、という気もします。

ここまでが、2日目の感想&レポートです。

実は、後2つ程セッションをまわる予定だったのですが、
最後のセッションが立ち見だったのと、部屋の広さにそぐわない人の多さにやられて体調悪くなったので、
そのまま帰宅しました。(技術スキルより、体力をつけるべき、というオチでした・・・)

以上です。前回と同様、ただの感想文になってしまいましたが、
2日間とも、本当に中身が詰まったセッションでした。


2014年9月7日日曜日

Highcharts JS で Highcharts error #14。

9月 07, 2014
オフィス狛 技術部です。

あるシステムで Highcharts JS を使っているのですが、
新しいグラフを追加する事になり、データ提供元のWebAPIを利用しJSONを取得して表示したところ、
以下のエラーになりました。
Highcharts error #14: www.highcharts.com/errors/14

軸のデータが変わっただけで、グラフ表示形式も、JSON形式も他のグラフと違いないはず・・・
とりあえず、エラーメッセージには親切にもURLが付いているのでアクセスしてみます。

String value sent to series.data, expected Number
This happens if you pass in a string as a data point, for example in a setup like this:
series: [{
data: ["3", "5", "1", "6"]
}]
Highcharts expects the data values to be numbers. The most common reason for this is that data is parsed from CSV or from a XML source, and the implementer forgot to run parseFloat on the parsed value. For performance reasons internal type casting is not performed, and only the first value is checked (since 2.3).

読み解くと、x、y軸に表示するデータは、数値扱いにしなければならない。
という事のようです。
上記の例だと、
 series: [{
     data: [3, 5, 1, 6]
 }]
このようにダブルクォーテーションを取る・・・という事ですね。

実は今回のWebAPIも弊社が作成しているので、WebAPI側を修正する事が出来ました。
逆に言うと、他社のWebAPIを利用する場合に、こんな現象に遭遇したら・・・嫌ですね。