daily-dev.net

React, firebase, 機械学習など

Gatsby

React.js製の静的サイトのジェネレータである、Gatsbyと、NetlifyCMS を使って、キュレーションメディアを作ることにしました。

Gatsby のよさ

  • React.jsで作り込める
  • 高速な表示
  • Wordpressのようなセキュリティアタックがない
  • Wordpressのようなコードの煩雑さがない
  • カスタムフィールドをカンタンに作れる

NetlifyCMSで、記事の作成を行い、Gatsbyで、ビルド中にNetlifyCMSからデータを取得して、静的ファイルに変換します。 NetlifyとGatsbyを組み合わせれば、特定のブランチにpushすれば、自動でビルドして、サイトを更新することができます。

管理画面はどうなっているのか

cms-demo.netlify.com

からチェックできます(ログイン不要)。

ログインユーザー&デプロイ設定

Identityで、invite usersでログインユーザのアドレスを追加します。

publicは嫌なので、github上でprivateレポジトリに変更します。その場合、以下の手続きが必要になるようです。

If you change ownership on your repository, or convert a repository from public to private, you may need to reconnect Git Gateway with proper permissions:

Go to Settings > Build & deploy > Continuous Deployment > Build settings to re-link the repository.
Go to Settings > Identity > Services > Git Gateway to add a new API access token following the instructions in the section above.

ServiceにGithub Gatewayを追加します。 Build&Deployセクションで、Continuous Deploymentのところに指定のレポジトリが表示されているか確認し、されていなければRelinkします。

公開サイトに /admin パスでアクセスします。

フィールド定義

公式
Widgets define the data type and interface for entry fields. Netlify CMS comes with several built-in widgets. We’re always adding new widgets, and you can also create your own!

とあります。

ビルトインウィジェットには下記があります。

f:id:serendipity4u:20190417122026p:plain

static/admin/config.yml にて、

fields:
      - {label: "Template Key", name: "templateKey", widget: "hidden", default: "blog-post"}
      - {label: "Title", name: "title", widget: "string"}
      - {label: "Publish Date", name: "date", widget: "datetime"}
      - {label: "Description", name: "description", widget: "text"}
      - {label: "Body", name: "body", widget: "markdown"}
      - {label: "Tags", name: "tags", widget: "list"}

このように定義していきます。

widgetとは、NetlifyCMSの管理画面上で、どのようなUIで表示するか、ということを指定するものです。

      - {label: "Price", name: "price", widget: "number"}

を追加すると、

f:id:serendipity4u:20190417133704p:plain

がCMS上に表示されました。

Gmailの独自ドメインでcontact form7 を利用してwordpressからメール配信をする方法の最新版

一個も正式な方法が日本語文献でヒットしなかったので書いておきます。

まず、プラグイン

https://cantas.co.jp/wp-content/plugins/wp-mail-smtp/assets/images/logo.png

を入れる。

Gmailを選択。

https://wpforms.com/wp-content/uploads/2017/12/Choose-Gmail-for-SMTP-mailer.png

画像元:  https://wpforms.com/wp-content/uploads/2017/12/Choose-Gmail-for-SMTP-mailer.png

なぜかOther SMTPを選択させる解答ばかり。これでは会社ドメイン(gmail.com でない独自ドメインと紐づけたもの)などでうまくいかない。

console.developers.google.com

から認証情報を取得する必要あり。

上記リンクをクリックして、サイトURLとリダイレクト先URLを設定する。

リダイレクト先URLは2018/10時点で

wordpressサイトURL/wp-admin/options-general.php?page=wp-mail-smtp&tab=auth になる。

設定画面下側に表示されているのでコピーしてgoogle側に貼る。

上記によってクライアントID,secretが生成できるはずなのでそれをプラグインに貼る。

参考

How to Securely Send WordPress Emails using Gmail with WP Mail SMTP

React - production環境でconsole.logなどの出力を消す

f:id:serendipity4u:20181002170351p:plain

このためには、webpackのコンパイル設定を手動で変えられるようにする必要がある。 package.jsonのscriptsに

    "eject": "react-scripts eject"

を追加して、

npm run ejectを走らせる。

f:id:serendipity4u:20181002170243p:plain

これは簡単のためcreate-react-appが隠蔽している、パッケージの依存関係や設定ファイルを書き出してくれるコマンド。

ejectする前に、commitし忘れているファイルがないか確認する。commitしていないと動かない。

さてこれを行うと configディレクトリが作成されてその中にwebpack.config.prod.jsがある

このなかの

    new webpack.optimize.UglifyJsPlugin({
    })

という箇所でconsole.logを表示しないように

  new webpack.optimize.UglifyJsPlugin({
      drop_console: true,
      compress: {
        drop_console: true,

drop_consoleオプションをtrueにしておく。

これで本番環境でデバッグ情報がでなくなった。

firebaseでTwitter認証がproductionのみ効かないとき

f:id:serendipity4u:20181002165802p:plain

承認済みドメインという欄を忘れがちなので記録しておきます。

ログインにおいて、それ以外でのハマりピントはほぼない。

firebaseでユーザーが自身のアカウントを削除できるようにする

cloud functionsを利用するのが一番早そうだった。

deleteUserByAdmin というエンドポイントをcloud functionで作って、

それを fire.functions().httpsCallable('deleteUserByAdmin');でクライアントサイドから叩くというコードです。

クライアントサイド


  function confirmAndDeleteAccount() {
    let result = window.confirm('本当に削除しますか?');
    if (result) {
      var deleteUserByAdmin = fire.functions().httpsCallable('deleteUserByAdmin');
      deleteUserByAdmin().then(function (result) {
        console.log(result)
        window.location.href = "/";
      });

    }
  }

cloud functions


// ユーザー削除
exports.deleteUserByAdmin = functions.https.onCall((data, context) => {
    const uid = context.auth.uid;
    console.log('delete start.', uid)
    // admin によってuser削除する
    admin.auth().deleteUser(uid)
        .then(() => {
            functions.database.ref(`items/${uid}`).remove();
            functions.storage.ref(`users/${uid}`).remove();
            return 'ok'
        })
        .catch((error) => console.error('There was an error while deleting user:', error));
})

cloud functions http requestについては以下記事を参照

firebase cloud functions で爆速でAPIサーバー作ってcron-job.orgで定期実行する

github pagesにreact-create-appで作成したReactアプリを独自ドメインでデプロイしたのに表示されないときのチェックポイント

f:id:serendipity4u:20180926114133p:plain

公式リファレンスに沿ってデプロイしたのに表示されない。なぜだろう?

  • 独自ドメインをpackage.jsonの"homepage" 欄に設定していますか?
  • gh-pagesブランチをデプロイブランチに設定していますか?
  • CNAMEファイルに接続したいカスタムドメイン(独自ドメイン)を記入していますか?
  • CNAMEファイルがpublic配下にありますか?

を確認してみてください。

f:id:serendipity4u:20180926113854p:plain

自分の場合は

CNAMEをルートディレクトリに置いていたことによるエラーだった。

このissueで気づくことが出来ました。

gh-pages -d dist overwrites custom domain · Issue #127 · tschaub/gh-pages · GitHub

react-create-appの初期設定のままならpublic 配下に置いておけばbuild時にbuild配下に配置されるはずです。

Webデザイナーさんに伝えるために書いた最速でコーディングに入るためのReact.js初心者向け概論

一緒にやっているWebデザイナーさんに初めてReactアプリのコーディングを頼むときに書いた概論です。

最速でコーディングに入ることを目的としていた文章ですので、

ザックリしすぎる一般化もありますし、深いReact理解/実装を求めるなら公式リファレンスを最初から最後までやるのが一番です。

記事を読んでから行うコーディングとしては

  • htmlの組み直し
  • スタイリングに利用するためのクラス/idの付与
  • 動的なスタイリング

を念頭に書いています。

Reactとは?

javascriptでhtmlを書くという発想の、フロントエンドを司るプログラミング言語。 Webの画面、すべてをコンポーネント(Railsでいう、テンプレートのようなもの)で表現する。

一言で言えば、html、ページといった概念からの解放。

たとえば、 <UserIcon> というコンポーネントを定義したとする。いったん定義してしまえば、ユーザーページでも、記事一覧の著者アイコンでも、コメント一覧のコメント主の画像にも使うことができる。いろんなファイルから、 import UserIcon from './to/userIcon/file/path'とするだけで、利用することができるようになる。

何が便利?

stateが便利。Reactの世界では、コンポーネントはそれぞれ、stateつまり状態を持つ。例えば、<NotificationDropdown>という、通知ボッックス全体を表すコンポーネントがあるとしよう。

f:id:serendipity4u:20180925113845p:plain

この NotificationDropdownにはどんなstateが考えられるだろうか?

ドロップダウン式なので、開いているか・開いていないか の2通りのstateがあると便利だろう。最初は閉じているはずなので、isOpen: falseと設定できるといいだろう。

stateは以下のように定義することができる。

import React from 'react'

class NotificationDropdown extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      isOpen: false, // ここに定義する。falseを初期値として入れた
    };
  }
...省略

上記のコードで、Reactを用いて、NotificationDropdownというコンポーネントのクラスを作成し、そのコンポーネントが内部に isOpen=true / false という状態を持つことを定義できた。

状態があると何がいいのか??

NotificationDropdownクラスの中で、

if (this.state.isOpen === true) {
  console.log('開いてるよ!');
  // 新しい通知を読み込んだり...
 render ( <NotificationContent /> )
} else {
  console.log('閉じているよ!'); 
}

簡単に状態を参照できるため、 * 条件分岐に this.state.isOpen の値を用いたり、 * 上記によってisOpen === trueのときだけ別のコンポーネントをレンダリング(表示すること)したりといったことができる(例では<NotificationContent/>

stateの例

  • ユーザーページにおいて、今閲覧しているユーザーがそのユーザーをフォローしているかどうか: this.state.following === true なら<UnfollowButton>コンポーネントをrenderする、など。

app/javascript/packs/compornent/relationButton.jsxは、

f:id:serendipity4u:20180925113913p:plainf:id:serendipity4u:20180925113916p:plain

上記のようなフォロー・アンフォローボタンを表示するクラスである。

このボタンの種類は、

  • <UnFollowButton user={user}>
  • <FollowButton user={user}>

の2種類があって、

this.state.following_status

というstateの値によって、表示するコンポーネントを出し分けているのである。 つまり、render() { }の中身は以下のようになっている。


 if (this.state.following_status === 'following'){
      return(
        <div className="follow_form" onClick={ () => this.unfollow(user)}>
          <UnFollowButton user={user}></UnFollowButton>
        </div>
      )
    } else {
      return(
        <div className="follow_form" onClick={ () => this.follow(user) }>
          <FollowButton user={user}></FollowButton>
        </div>
      )
    }

大事なrender関数

すべてのReactのクラスは、render()メソッドを有する。これは、このクラスが結局何を表示するのか?という問いの、「何」に当たる。


import CommentBox from './components/commentBox.jsx' 

React.render( // 表示する
  <CommentBox />, // このコンポーネントを
  document.getElementById('container') // ここに
);

これはCommentBoxクラスで定義したHTML要素を、#containerの中にレンダリングする(表示する)というコードである。

もっと実践的な例として、

記事のタイトルと画像を表示するArticleContentコンポーネントのクラス定義を見てみよう。 f:id:serendipity4u:20180925114311p:plain


import React, { Component } from 'react';
import { render } from 'react-dom';
import UserIcon from './components/userIcon.jsx'


class ArticleContent extends React.Component {
  constructor(props) {
    super(props);
  }

  render(){
    const item = this.props.item;

    return (
      <section className="article-content">
        <UserIcon user= {item.user} /> // 注目
          <div className="article-content__title">
            <h4 className="title">
              { item.title } // タイトルを表示
            </h4>
          </div>
        <div className="article-content__img">
        </div>
      </section>
    )
  }

}

export default ArticleContent;  //exportすることで、他のページからimportして<ArticleContent>で呼び出せるようになる。

このように、render() { return( htmlの内容 ) }で記述していく。

また、render(){}のなかに、UserIconがあることにも注目してほしい。3行目の

import UserIcon from './components/userIcon.jsx'

で、他のファイルからUserIconというクラスを持ってきていることがわかる。jsxというファイルの拡張子は、javascriptとhtmlを一緒に書くReactのファイルにつける拡張子である。これは'./components/userIcon'と拡張子を省略して読み込むこともできる。

どう別のファイルにあるコンポーネントに変数を渡すのか?

さて、ここで、上記のコードを見て、

 <UserIcon user= {item.user} />

の記述を疑問に思っただろうか。

Reactで、stateの次に重要な概念として、propsがある。propは小道具という意味の英語である。

これは、別のファイルのコンポーネントを表示するときに、そのコンポーネントに、変数を渡してあげる役割を持つ。

itemという変数に、以下のようなjsonデータが格納されていたとしよう。

  "id":141633,
  "url":"https://www.flaticon.com/free-icon",
  "title":"Settings - Free Tools and utensils icons",
  "eyecatch_url":"https://imageog.on.com/7.png",
  "user":
      {
        "username":"kansiho",
        "profile_image":"profile_image1231243.png"
      },

このアイテムの所有者の情報は、item.userで取得できる。<UserIcon user= {item.user} /> としてユーザー情報をUserIconコンポーネントに渡してあげよう。render関数の中のreturnの値は基本的にhtmlタグ要素と解釈されるが、{}で括ることでhtmlタグ要素ではなく、変数として渡すことが可能だ。

(つまり、id="myId"だと"myId"という文字列のへんてこなIDになるけど、id={myId}だとmyIdという変数を代入するという意味になるということ。たとえば var myId="main"id={myId}とすればid="main"になる。)


    return (
      <section className="article-content">
        <UserIcon user={item.user} /> // 注目
          <div className="article-content__title">
            <h4 className="title">
              { item.title } // タイトルを表示
            </h4>
          </div>
        <div className="article-content__img">
        </div>
      </section>

さて、渡された側のUserIconコンポーネントではどう表示したらいいのだろう? UserIconのコンポーネントの中のrender関数の中身を見てみよう。

// app/javascript/packs/component/userIcon.jsx

const user = props.user;
  
return(
    <a href={'/@' + user.username }>
      <div className="user-icon">
        <div>
         <img src={props.user.profile_image} />
         { props.user.username } さん
        </div>
      </div>
    </a>

user={user} で渡した値は、props.userから引き出すことができる。

もし、別の変数名、たとえば以下のようにaccountというprop名 <UserIcon account= {item.user} /> で渡したなら、props.accountから引き出すことができる。つまり、usernameを表示するなら{ props.account.username }となる。

styleはどう適用すべきか?

// app/javascript/packs/component/userIcon.jsx

const user = props.user;
  const iconStyle = user.profile_image ? ( {
    backgroundImage: 'url(' + user.profile_image + ')'
  }) : (null)
  return(
    <a href={'/@' + user.username }>
      <div className="user-icon">
        <div style={iconStyle} className={"user-icon--circle " + (user.profile_image ? '' : ' user-icon--circle--noimg')}>
          { user.profile_image === null && user.username ? user.username.charAt(0) : ''}
        </div>
      </div>
    </a>

ややややこしいのが、動的なスタイルの適用である。 動的というのは、つまり、与えられたjsonデータの内容に応じて背景画像を変えたい、といった場合である。

その場合は、

const iconStyle = {
    backgroundImage: 'url(' + user.profile_image + ')'
}

以上のように、スタイルをオブジェクトで定義し、その中で変数を利用しなくてはいけない。

便利なこととしては、ここにおいて、stateを利用したりできることだろう。

たとえば、三項演算子を用いて、

user.profile_image ? (profile_imageがあるとき) : (profile_imageが無い、つまりnullのとき)

const iconStyle = user.profile_image ? ( 
// あるとき
{backgroundImage: 'url(' + user.profile_image + ')'}
) : (
// ないとき
{backgroundImage: 'url(example.com/default.png)'}
)

のようにして、プロフィール画像がある時と、無い時で、画像を出し分けるといったことができる。

なぜReactを使うのか?

  • より高速に、
  • リッチな体験を、
  • 高い可読性のコードによって届けるため。

json って何?

JSONとはJavaScript Object Notationの略で、XMLなどと同様のテキストベースのデータフォーマット。その名の通り、JavaScriptと相性が良い書き方ルール。

画像引用元: wa3.i-3-i.info

{
 "hogehoge": [ 1, 2 ],
 "piyopiyo": {
  "piyota": [ "21", "lovely" ],
  "id": "200"
 }
}

ブラウザでjsonを確認する際に、見やすいインデント表示するためには、下記URLから拡張機能を入れることをすすめる。

chrome.google.com

ES6

2015年に発表された、よりオブジェクト指向に、かつ、堅牢になったモダンなjavascriptの文法。

ES2015 (ES6)についてのまとめ

を読むといい。

Reactリファレンスを読むにあたって最低限分かっていた方が良い文法まとめ↓

let,constによる変数宣言

constは定数(中身が変わらない変数)を宣言したい時。 letは再代入したい時。 どちらもブロック{}の外への影響力を持たないことに注意する。

let foo = [1, 2, 3];
{
  let foo = [4, 5, 6];
  console.log(foo);
  // => 4, 5, 6
}
console.log(foo);
// => 1, 2, 3

foo = [2,3,4]; // 再代入してもエラーは出ない

const name = 'shiho'
name = 'hanako' // 再代入なので、エラー「Uncaught TypeError: Assignment to constant variable」になる

constructor

constructorメソッドは、クラスがnewされた時に実行されるメソッドであり、クラス内で共通して使われるプロパティの初期値の定義などを行う。1つのクラスに1つしか定義できない。 (Rubyでいうinitializeメソッド)。

class Human {

  constructor(name) {
    this.name = '山田太郎';
  }

  hello() {
    console.log('ぼくのなまえは' + this.name);
  }

}

obj = new Human('アレクサンダー')
obj.hello // ぼくのなまえはアレクサンダー

アロー関数「=>」

従来のfuncitonを使った関数宣言に加えて、=>を用いた関数宣言が可能。

let plus = (x, y) => {
  return x + y;
};
plus(1,6) // 7

配列展開の「...」

var array = [1, 2, 3]
[...array, 4, 5, 6] // [1,2,3,4,5,6] 

どのファイル(コンポーネント)を見れば・いじればいいのか?

chrome.google.com

このchrome拡張機能を入れましょう!

f:id:serendipity4u:20180926120657p:plain

コンポーネント名がデバッガーに表示されます。

次に読むといいかな?

mae.chab.in

qiita.com

Youtube Video List APIからyoutube動画情報(ビュー数、説明、サムネイル)を取ってくる【javascirpt/node.js】

公式APIにリクエストをしてレスポンスから抽出するだけのシンプルな実装です。

動画のタイトルやサムネイルなど基本情報はitem.snippet、 ビュー数などの情報は item.statisticsに入っています。

partに取得したい情報を明記する必要があります。

まずyoutube apiのアクセスキーを取得する必要があるのに注意です(今回は省略)。

複数のビデオ情報が配列となって返ってきますが、 この場合は一つのvideoIdを渡しただけなので data.items[0]と最初の情報だけを取り出しています。

    var key = functions.config().api.key
    var videoId = "xxxx"
    const request_url = "https://www.googleapis.com/youtube/v3/videos?id=" + videoId + "&key=" + key + "&part=snippet,contentDetails,statistics,status"
    return axios.get(request_url).then(response => {
        let data = response.data
        console.log('response: ', data)
        console.log('after fetchYoutubeDataAndSave')
        let item = data.items[0] // 一つの動画のみの場合
        console.log('item: ', item)

        const title = item.snippet.title
        const description = item.snippet.description
        const published_at = item.snippet.publishedAt
        const channel_id = item.snippet.channelId
        const viewCount = item.statistics.viewCount
  })

ライブラリを入れるまでもなく、非常に簡単です。

宣伝

youtube美容動画を探せる、登場プロダクトがすぐ買えるプラットフォーム

makeup-tube

をyoutube APIを利用して開発しました。

react-router-v4 でリンクが変わってもスクロールの高さが初期化されない問題を解決する

リンクが変わっているのに、前のページのscrollHeightを引き継いでしまうことがありました。

以下のように、browserHistory.listen()にトリガーさせて、スクロール位置を初期化させるコードを挿入するようにしたら、解決しました。

import { Router, Route, Link, Switch } from "react-router-dom";
import { createBrowserHistory } from 'history';

const browserHistory = createBrowserHistory();

browserHistory.listen((location, action) => {
  window.scrollTo(0, 0);
});

...
class App extends Component {
  ...
  render(){

      <Router history={browserHistory}>
...
      </Router>
  }
}

processing & p5.jsの直感的な説明ー円の生成からアニメーション、ランダム増殖まで

f:id:serendipity4u:20180923110402g:plain

p5.jshはアーティスト向けビジュアルコーディングツールprocessing(Javaベース)をjavascriptで実行可能にしたものです。jsなのでDOMの操作が可能で、サイトの組み込みが非常に楽なのが特徴です。

導入

こちらのページからzipをダウンロードもしくは

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.2/p5.js"></script>

のCDNが利用可能です。

codepenの empty-example というボイラーテンプレートでもブラウザ実行が可能です。

動く円を描く

p5.jsを動かすための必須メソッドはsetup()draw()だけです(これはprocessingとおなじ)。

setup()は初期化に利用されるメソッドで、一回のみ実行されますが、draw()は永続的にループします。

elipse(x, y, width, height)は楕円を描きます。つまりwidth=heightにすれば円を描きます。

noStroke()は楕円のアウトラインを描かないことを意味します。

f:id:serendipity4u:20180923103253p:plain

f:id:serendipity4u:20180923103244p:plain

上が普通にelipse()で描画した円、 下がelipse()メソッドの前にnoStroke()を指定した円です。

先程、draw()`は永続的にループすると書きました。

そのため、なにかオブジェクトを動かしたいならば、draw()ループの中で変化していく(x,y)を作れば良いことになります。

background(color) でキャンバスの背景色を指定し、塗りつぶします。background(color)をdraw()の中に入れると、draw()が実行されるたびに前の画像の上に背景色を上塗りしていることになります。そのため、たとえば以下のような「円が動いているように見える、アニメーションのようなもの」が作れます。

codepen.io

実際には、円を描く→上に黄色で塗りつぶす→位置を変えて円を描く→塗りつぶす

を繰り返している、つまり、フレームごとに背景を塗りつぶしているー塗りつぶさないと前のフレームの画像が残ってしまうことに注意します。

前のフレームの画像が残る場合

codepen.io

このように、円を斜め下にずらしながら、前のフレームの画像を消さず描画すると、 直接を描いているようになりました。

setup()の中で、createCanvas(width, height)を明記することで描画する画面幅を指定することができます。明記しないとデフォルトサイズの100px*100pxになります。

文字の描画

textSize(fontSize)で文字の大きさを指定します。 text(content, x, y)で描画する文字列と位置を指定します。

draw()関数内で、フォントサイズと描画位置yを変更していくことで、動く文字列が実装できます。

codepen.io

ランダムに円を生成する

random(start, end)でランダムなfloating numberを生成できます。 random(1,1000)なら1~1000までのランダムなfloating numberが返ります。 半径radiusと描画領域x,yをランダムにしたコードのサンプルです。

codepen.io

さて、このコードに、フレームごとに背景を塗りつぶすbackground('black')を足してやると、ひじょうに慌ただしくなります。

codepen.io

慌ただしすぎるので、フレームレートをゆっくりにしてやりましょう。

codepen.io

参考

Generative Design with p5.js - [p5.js版ジェネラティブデザイン] ―ウェブでのクリエイティブ・コーディング

Generative Design with p5.js - [p5.js版ジェネラティブデザイン] ―ウェブでのクリエイティブ・コーディング

  • 作者: Benedikt Gross,Hartmut Bohnacker,Julia Laub,深津貴之,国分宏樹,Claudius Lazzeroni,Benedikt Gross(編著),Hartmut Bohnacker(編著),Julia Laub(編著),Claudius Lazzeroni(編著),美山千香士,杉本達應
  • 出版社/メーカー: ビー・エヌ・エヌ新社
  • 発売日: 2018/06/22
  • メディア: 単行本
  • この商品を含むブログを見る

p5.jsプログラミングガイド

p5.jsプログラミングガイド

What is p5.js and How to use it? – mhiratsuka – Medium

サーバーサイドjavascript・canvasでpng画像をリアルタイム描画する仕組み

これは以下の記事のつづきです。

www.daily-dev.net

これは https://text2ogp.com から、 もう一つ別に立てた画像生成サーバーにリクエストを送って、返却されたpng画像をプレビュー画像として表示するという仕組みで作っています。

将来的に自分が色々なサービスを作ることになったときに、中心的な画像処理サーバーにしたいなという気分でこういう構成にしました。

画像生成サーバー

サーバーサイドでcanvas画像を作成するところが今回のポイントです。 node-canvasというライブラリの2系を利用させてもらいました。

https://github.com/Automattic/node-canvas

※2系の利用は npm install canvas@next で可能です。

node-canvasはcairoという描画ライブラリをバックエンドとしています。

こいつがやや曲者で、cairoをスムーズに入れるため、自由度の低いAWS Lambda や herokuなどを使わず、EC2のubuntuの無料インスタンスを選びました。

デプロイ自動化は pm2のecosystemを使うと便利でした。

詳しくはこちらにて👇

Node.jsのWebアプリケーションの自動デプロイ【pm2のecosystem】

描画

まず描画領域を決めてコンテクストを定義します。

const canvas = createCanvas(1200, 630)
const ctx = canvas.getContext('2d');

フォントの読み込み

ubuntuに欲しいフォントは無い気がしたのでttfファイルをgoogle fontから落として読み込んでいます。

const { createCanvas, loadImage, registerFont } = require('canvas')

function fontFile(name) {
    return path.join(__dirname, '/fonts/', name)
}

registerFont(fontFile('NotoSerifJP-Bold.otf'), { family: 'noto' })

ctx.font = '50px noto'; // これでnotoが使えます!実際にはここはパラメータになっています。

画像の読み込み

const { createCanvas, loadImage, registerFont } = require('canvas')

function bgFile(name) {
    return path.join(__dirname, '/bg/', name)
}

loadImage(bgFile('fabian-grohs-597395-unsplash.jpg')).then(img => {
            ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
});

グラデーション

my_gradient = ctx.createLinearGradient(0, 0, 800, 0);
my_gradient.addColorStop(0, "#8E54E9");
my_gradient.addColorStop(1, "#4776E6");
ctx.fillStyle = my_gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);

文章を改行しつつ、真ん中に表示する

改行とセンタリングがやっかいです。普通のhtmlみたいに、折り返して次の行に行くということを自動でやってくれませんから、座標(x,y)どこから次の行を描画するかを指示しなくてはいけません.......。真ん中の位置も計算しないといけない。

// lines = ['やってみた', '改行を。']
ctx.textAlign = "center";
fixStartHeight = (canvas.height - fontSize * lineHeight * lines.length) / 2
    for (lines, i = 0, l = lines.length; l > i; i++) {
        var line = lines[i];
        var addY = fontSize;
        // 2行目以降の水平位置は行数とlineHeightを考慮する
        if (i) addY += fontSize * lineHeight * i;
        // fillText('text', x:左から, y:上から)
        ctx.fillText(line, canvas.width / 2, fixStartHeight + addY);
        console.log('fill text')
    }

これでいけました。これでいけたように見えたんだけどこれでいいのかな?lineHeight, fontSizeは文字数により変化させるので変数にしてます。

生成したcanvasオブジェクトをpngとして返す

res.setHeader('content-type', 'image/png');
res.writeHead(200, { 'Content-Type': 'image/png' })
canvas.toBuffer(function (err, buffer) {
        res.write(buffer)
        res.end()
})

CORS

text2ogp.comからプレビュー画像の表示が出来るようにAccess-Control-Allow-Originヘッダをセットします。

res.header("Access-Control-Allow-Origin","https://text2ogp.com");

SSL化

さて、こちらにリクエストを送ってtext2ogp.comで画像をダウンロードするためには、この画像生成サーバも独自ドメインでSSL化しなくてはなりません(ブロックされてしまう)。

なるべくお金を使わないため、letsencrypt を使おうかと思ったけど、更新トラブルがあったら面倒なので、サーバー証明書は自動更新してくれるAWS Certificate Managerで発行しました。こちらも無料です。

text2ogp.com 本体

ogp_of_text2ogp.png

概要

ユーザーにフォントやテキスト編集、背景選択をおこなうインターフェースを提供し、 画像生成サーバーにURLでパラメータを渡す役割を持っています。

具体的には ?string=あああ&font=a&color=3 のような形でパラメータを渡しています。

テキストは改行文字や記号などを含むので、パラメータはencodeURI()してから渡すのを忘れないようにします。

クリックしてリモートの画像生成サーバーの画像をダウンロードする

ダウンロードボタンを押したときに、発火し、文章内容、fontやcolorのパラメータ付きでリモートにURLを渡してあげます。

function forceDownload(url, fileName) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.responseType = "blob";
    xhr.onload = function () {
        var urlCreator = window.URL || window.webkitURL;
        var imageUrl = urlCreator.createObjectURL(this.response);
        var tag = document.createElement('a');
        tag.href = imageUrl;
        tag.download = fileName;
        document.body.appendChild(tag);
        tag.click();
        document.body.removeChild(tag);
    }
    xhr.send();
}

デプロイ

手っ取り早く公開したかった&ソースコードが公開されても問題がなかったので、nowというサービスの無料のOSSプランを使ってデプロイしました! $now コマンドでカレントディレクトリのnodeプロジェクトをデプロイできるというものなのですが、毎回ユニークなURLを生成してデプロイしてくれるので、そのURLでデバッグして、正しく動くようなら $now alias {自動生成URL} text2ogp.comのような形で独自ドメインに紐付けられて便利です。HTTPS化もデフォルトでなされています。

詳しくはこちらにて👇 【3分デプロイ】nodeプロジェクトをデプロイする最速の手段・now【登録から独自ドメインまで】

以上、テキストを画像化する方法、それをちょっとしたAPIサーバーにする方法についての雑なメモでした。

そのうち、この実装を使ったサービスを公開する予定なのでよろしくおねがいします。

テキストからアイキャッチ画像を自動生成するサービスをつくりました

テキストからアイキャッチ画像を生成するサービスを作りました。

lighttext2ogp.gif

text2ogp.com

というサービスです。

良かったらブログのサムネイル画像づくりにお使いください。

テキストを入力して、背景色を変えたり、フォントを変えたりして、ワンクリックでダウンロードができます。

もともと、別のサービスの実装の一貫で、テキストを画像化してシェアするという機能が必要になりました。今回のサービスはその「テキストを画像化」のみを切り出した副次的に生まれたサービスになります。

サーバーサイドで描画する仕組みについても記事を書きましたのでよろしければご覧ください。