ぽっちぽちにしてやんよ

技術ネタとかアプリに関する話とか

いい感じでレイアウトして画像化し,SNSに画像投稿する手段とAnimiteruで採用した方法について

こんにちは,ぽち@pchwです.

昨今Twitterに画像を付けて投稿をしてバズを生み出す的なサービスが増えたように思います.

自分も似たようなコンセプトで何個かサービスを出していて,

ta1usho.com

no1tweet.com

animiteru.com

作品タイトルで一番面白いこと言った奴、優勝などは,AWS Lambda上で用意した画像テンプレートに対して指定された文字を合成をする程度です. 実装的には,gmを使って合成し,Twitterに投稿しています.

f:id:poChi:20180416090930p:plain

しかし,Animiteruでは画像とタイトルをいい感じで並べて合成する必要があり,gmの合成で全てやるには少し荷が重い感じでした. (とはいえ,画像間の隙間を計算したり,文字のサイズを適切に指定して帯の矩形を合成した上に配置して・・・ということをやれば可能です)

f:id:poChi:20180416091123p:plain

そこで,色々と模索したのを残しておきます.

まず,考えついたのがHTMLでなら簡単にレイアウト出来るということです.それを画像化出来れば,簡単にデザインの変更も出来ますし筋が良いように思えました. しかし,それをサーバサイドで行うとなると,手法が限られました.

  1. jsdomなどでHTML文字列からDOMを構築して,canvasに描画し画像化とする
  2. ヘッドレスブラウザ的なものでHTML文字列を解釈してキャプチャを撮って画像化する

といったところでした.

(1) に関しては,「canvasに描画し」というところでnode-canvasがネイティブのモジュールcairoを要求したりで,AWS Lambda上に構築するには容量とかその辺りでかなり厳しいものがありました.

(2) に関しても,AWS Lambda上でヘッドレスブラウザ的なものを使うようにするためには容量とかその辺りが厳しい.

つまり両方の手法ともに,AWS Lambdaにバンドルされているものでは実現出来ないため,パッケージを上げるモジュールの中に含める必要があり,そこには容量の制限があるため難しいという形になりました.

やはりgmで地道にレイアウトしかないのか・・・と思ったのですが,Animiteruは少し裏技的な方法で実現しています.

それは,画像の生成をサーバサイドではなくクライアントサイドで行うことです.

クライアントサイドにはdomを理解するものも,canvasも備えています.(当たり前ですが)

  1. クライアントサイドでレイアウトしHTMLを描画
  2. そのHTMLをcanvasに描画して画像を得る
  3. 投稿

というフローを取っています.

本当は,投稿のところでサーバを介せずクライアントサイドだけで出来れば良かったのですが,TwitterのAPIを叩く必要があり,ConsumerKey/Secretをクライアントサイドに埋め込むわけにはいかなかったので,投稿の部分はサーバサイドを介しています.

「クライアントでレイアウトしてHTMLを描画」の部分は特に言うことはありません.普通にHTMLを降らすかjsonで描画に必要なデータを降らせてクライアント側でHTMLを構築します.

Animiteruではjsonでデータを降らせて,HTMLを構築しています.見た目の部分はFoundationのグリッドレイアウトがベースです.

「HTMLをcanvasに描画して画像を得る」の部分はhtml2canvasというモジュールを使っています.

import html2canvas from 'html2canvas'
html2canvas(captureTarget, {
  logging: true,
  useCORS: true
}).then( canvas => {
  const image = canvas.toDataURL('image/jpeg', {quality: 0.5});
});

という感じです.

ここで障害になったのは,HTMLをCanvas化するときにimgタグは同じドメインでないとダメです. Animiteru では,その制限を回避するためにAWS Lambdaを使って画像をプロキシするURLを用意しています.

severless.yml で言うと,このあたり.

functions:
  thumbnail:
    handler: handler.thumbnail
    timeout: 15
    events:
      - http:
          path: /thumbnail/{text}
          method: get
          cors: true

「投稿」の部分は普通にTwitterに得られた画像を投稿しています. 上記に書いたように本当ならクライアントサイドで投稿が完結すればよかったのですが,文字投稿はポップアップウィンドウで投稿するみたいなことが出来ますが,画像はTwitter APIを使う必要があります.

media/upload のAPIで画像を投稿し,得られたmedia_id_stringを伴ってstatuses/updateのAPIを叩きます.

// Twitterへ投稿
    const Twitter = require('twitter');

    const client = new Twitter({
      consumer_key: conf.consumerKey,
      consumer_secret: conf.consumerSecret,
      access_token_key: user.accessToken,
      access_token_secret: user.accessTokenSecret
    });

    const image = body.image.replace('data:image/jpeg;base64,', '');
    const media = yield client.post('media/upload', {
      media: new Buffer(image, 'base64')
    });

    const result = yield client.post('statuses/update', {
      status: statusText,
      media_ids: media.media_id_string
    });

端折りましたが,こんな感じです.

AnimiteruではこのようにクライアントサイドでDOMを画像化するという手法を選びましたが,色々デメリットがあるのであまりオススメしません.

まず,クライアントサイドで行うために描画結果の見た目が安定しません.フォントが違っていたり,ブラウザによって余白などに差異が出てしまうこともあります.

canvasに画像を描画させるには同じドメインから画像を配信する必要があり,直接APIなどで取れる画像のパスを使えないケースがあります.

他にも,imgタグが正しく描画される前にcanvasに描画させるとimgタグの画像が描画されずに画像化されてしまったりします.

簡単なレイアウトであれば,gmで計算して画像合成していくほうが良かったですね.

なんか他にいい方法があれば教えてください! → @pchw

ApolloClientを使ってAnnictのGraphQL APIを叩く

こんにちは,ぽち@pchwです.

Annict にはGraphQLのAPIがあり,色んなアニメの情報を取得することが出来ます.

Animiteru ではAnnictと連携して特定ユーザの今期視聴アニメ一覧を画像1枚にする機能やトップページの今期アニメ一覧でAnnictのGraphQL APIを叩いています.

Animiteru のバックエンド処理はAWS Lambdaで行っているので,Node.jsを使ってGraphQL APIを叩きます.

animiteru.com

npmモジュールでApollo Clientという便利なライブラリがあり,それを使うと簡単に扱うことができます.

今回は,OAuthによる認証は不要な造りだったので,ここから個人用アクセストークンを発行して使います.(発行にはログインが必要です)

まずは,ApolloClientとnodeのfetch実装であるnode-fetchをインストールします.

$ npm install --save graphql  apollo-client-preset node-fetch
const { ApolloClient } = require('apollo-client');
const client = new ApolloClient()

と出来れば良いのですが,色々と設定があります.

まず,AnnictのGraphQL APIのエンドポイントの指定と,Authorizationの設定が必要です.

それには,apollo-link-httpapollo-link-contextを使ってlinkプロパティの設定と,cacheプロパティの設定が必要です. cacheプロパティはapollo-cache-inmemoryを使ってインメモリキャッシュを指定することにします.

また,apollo-link-httpがfetchを要求するので,node-fetchを与えます(フロントエンドでApolloClientを使うこともできますが,その時はIEなどを対応する場合は同様にfetch pollyfilを入れる必要があります)

$ npm install --save apollo-link-context
const fetch = require('node-fetch');
const { ApolloClient } = require('apollo-client');
const { createHttpLink } = require('apollo-link-http');
const { setContext } = require('apollo-link-context');
const { InMemoryCache } = require('apollo-cache-inmemory');

const client = new ApolloClient({
      link: setContext((_x, { headers }) => {
        return {
          headers: Object.assign({}, headers, {
            authorization: `Bearer ${conf.annict.accessToken}`
          })
        };
      }).concat(
        createHttpLink({
          uri: 'https://api.annict.com/graphql',
          fetch: fetch
        })
      ),
      cache: new InMemoryCache()
    });

このような形になります.得られたclientを使ってGraphQL APIに対してGQLを投げて結果を受け取ります.

GQLを使うには,graphql-tagを使います.

$ npm install --save graphql-tag

あとは,client.queryqueryを持つオブジェクトを渡してAPIを叩きます.

require('co')(function*(){
    const gql = require('graphql-tag');
  const result = yield client.query({
      query: gql`
      query {
        searchWorks(annictIds: [5407], orderBy:  { field: WATCHERS_COUNT, direction: DESC }) {
            edges {
              node {
                annictId
                title
                titleEn
              }
            }
          }
        }
      `
    });
});

GQLの記述や確認は,

github.com

を使うと楽ちんです.

春アニメもはじまったことなので,AnnictのGraphQL APIを使って遊んでみるのもいいんじゃないでしょうか?

うちはダリフラが好きです(前期からの継続)

darli-fra.jp

AWS Lambdaで画像を返すAPIを実装する

こんにちは!ぽち@pchwです!

AWS Lambdaは気軽に処理が書けて,API Gatewayと組み合わせるAPIサーバを作るのにとても便利です.

色々とサービスを作っていると,HTTPのエンドポイントを叩くと,画像が返ってきて欲しいケースなどが出てきます.(例えば,画像のURLを投げるとバックエンド側で画像を取得して加工を施して返すなど)

お行儀のいい方法であれば,処理を行ったあとにAWS S3にアップロードし,APIレスポンスはそのURLが格納されたjsonが返り,クライアント側がそのURLに対して取得しに行くというのが筋な気がしますが,そんなに大した画像処理をせずに大量の人が使う可能性がある時などに「そのまま画像を返したいな〜」となることがあります.

やっていきましょう.

まず,めんどくさいので,Serverless Frameworkで雛形を作ります.

$ serverless create --template aws-nodejs
$ more handler.js 
module.exports.hello = (event, context, callback) => {
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
    }),
  };

  callback(null, response);
};

デフォルトの handler.js の処理は, messageinput というキーを持ったJSONを返す造りになっています.

単純に考えれば,ここに読み込んだ画像のBufferを入れればいいんじゃないか?と思います.

しかし,callbackの第二引数に渡すものはJSON.stringifyしていることから分かるように,文字列でないといけません.

なら.toString()だ!という考えになりますよね?

module.exports.hello = (event, context, callback) => {
  const fs = require('fs');
  fs.readFile('./assets/noimage.png', (err, image) => {
    const response = {
      statusCode: 200,
      body: new Buffer(image).toString('base64')
    };

    callback(null, response);
  });
});

しかし,これでも上手くいきません.

どうすれば良いかとというと,

  1. ちゃんと画像を返すということをヘッダーに書く必要がある
  2. API Gatewayの設定で画像を返すという設定をしないといけない

の二点が追加で必要です.

module.exports.hello = (event, context, callback) => {
  const fs = require('fs');
  fs.readFile('./assets/noimage.png', (err, image) => {
    const response = {
      statusCode: 200,
      headers: {
            'Content-Type': "'image/png'",
        },
      body: new Buffer(image).toString('base64'),
        isBase64Encoded: true
    };

    callback(null, response);
  });
});

これで処理はOKです.

API Gatewayの方はAWS Consoleでも可能ですが,Serverless Frameworkのプラグインを使うのが楽です.

$ npm install serverless-apigw-binary --save-dev

でプラグインをインストールし,serverless.ymlを編集します.

plugins:
  - serverless-apigw-binary # for custom.apigwBinary

pluginsセクションに追加し,customセクションで設定を記述します.

custom:
  apigwBinary:
    types: #list of mime-types
      - 'image/*'
      - 'text/html'

あと,ルートにアクセスされたら画像を返すようにHTTPエンドポイントの設定も記述しておきましょう.

functions:
  hello:
    handler: hello
    timeout: 15
    events:
      - http:
          path: /
          method: get

これで $ serverless deploy すればAPI Gatewayの設定が行われて,バイナリを返せるようになります.

あとはクライアント側で

<img src="DeployしたやつのURL" alt="image"/>

のようにすれば画像が読み込まれるはずです.

画像返すAPIバンバン作っていきましょう!

国税庁URL変換器にみる素早くウェブサービスを提供する方法

こんにちは,ぽち@pchwです. 先週作った 国税庁URL変換器 がちょっとバズって,「どうやって作ってるの?」みたいな疑問が寄せられたので,サラッと解説したいと思います.

今年に入って,

などを作ってきてからの,国税庁URL変換器 のバズでした.

基本的に,ベースとなる構成は全部同じです.認証周りが必要な時にAmazon Cognitoを追加で使ったり,処理が複雑になる場合はUI側の構築時にredux-sagaを入れたりしてる感じです.

国税庁URL変換器 は上記に上げた中でもかなりシンプルな造りをしている方ですね.

ベースとなる構成

まず,クライアント側で行う処理とサーバ側で行う処理を分けています. 重ためな処理とかDBとのやり取りとか(今回はありませんが)認証などはサーバ側で行います. 入力を受け取ったり,データを表示したり,簡単な通信で完結するものはクライアント側で行います.

クライアント側に必要なjsファイルやhtmlファイルは全てAmazon S3に置いています. HTTPSで配信するために,Amazon CloudFrontAWS Certificate Managerを利用しています.

UIライブラリは,Reactを使っています. 見た目は,React + Foundation経由でFoundation for Sitesを使っています. あと,別に無くてもいいのですが,タイポグラフィをいい感じにするために,ShevyJS を使っています.

サーバ側は,AWS Lambdaです. クライアント側から叩くために,HTTPのエンドポイントを公開する必要があるので,Amazon API Gatewayを使っています.こちらにもAmazon ClowdFrontを立てています.

ドメインの取得は Amazon Route 53 を使っています.

AWSの色んなサービスを使うので,Serverless Framework を使って一気に構築してしまいます. クライアント側のファイルを一気にAmazon S3にアップロードするのも,Serverless Frameworkのpluginの serverless-finch を使って上げてしまいます. サーバ側のHTTPエンドポイントの部分はapi.ドメイン名のようなサブドメインを切ってやってるのですが,そのあたりもserverless-domain-manager というpluginを使ってやっています.

データベースが必要になった際はMongoDB Atlas を使っています. 自分がMongoDBばっかり使っていて,SQLが書けない身体になってしまったので,大体MongoDBです.

構成図にするとこんな感じです.

f:id:poChi:20180409131156p:plain

構築の流れ

  • Route 53でドメインを取る(使えるようになるまで時間がかかる)
    • .com なら$12かかる
  • $ serverless create --template aws-nodejs
    • API Gatewayのテンプレートを使うともっと早いかもしれないけど,大体aws-nodejsを使っている
  • $ create-react-app clientでクライアント側を作る
  • $ npm init -y して,$ npm installでlambdaで使うモジュールを入れる
    • serverless-finch
    • serverless-domain-manager
    • serverless-offline
    • debug
    • mongoose
  • $ cd client && npm init -yして,$ npm installでクライアント側で使うモジュールを入れる
    • axios
    • foundation-sites
    • foundation-icons
    • react-foundation
    • shevyjs
    • react-loading-skeleton
    • react-router(1枚ペラのページなら不要)
  • client/src以下を色々いじってアプリケーションを構築
    • サーバの処理が不要ならこれだけで完成する
    • 確認が必要であれば,$ npm start で確認ができる
  • serverless.ymlを変更して,HTTPエンドポイントの設定とhandler.jsを変更して処理を書く.
    • 確認が必要であれば,都度$ serverless offlineでローカル実行して確認したりする
  • $ serverless deploy でS3 bucketの用意からAPI Gatewayの設定やlambdaの構築までやってくれる
    • 必要であれば, --aws-profileのオプションで適切なAWSアカウントを選択すること(一つしか無いなら不要)
  • $ cd client && npm run build でクライアントのソースを本番用にビルドして,$ cd .. && serverless client deployserverless-finchを使ってクライアント側のファイルをS3に載せることができる
  • $ serverless create domain でAPI用のサブドメイン設定を行う
    • たまに成功してるのに実は失敗してることがある
    • ドメイン名で接続してうまく繋がらない場合は $ serverless remove domain して再度行う(今回はここでハマった)

serverless.ymlの編集

基本はServerless Framework公式のドキュメントやCloudFormationのドキュメントが参考になります.

プラグインの設定

pluginsセクションを追加して,使うプラグインを指定します.

plugins:
  - serverless-offline
  - serverless-finch
  - serverless-domain-manager

それぞれのプラグインの設定は,customセクションで行います.

custom:
  bucket: nta-go.com
  client:
    bucketName: ${self:custom.bucket}
    distributionFolder: client/build
    indexDocument: index.html
    errorDocument: index.html
  serverless-offline:
    port: 4000
  customDomain:
    domainName: api.nta-go.com
    basePath: ''
    stage: ${self:provider.stage}
    certificateName: nta-go.com
    createRoute53Record: true

他のプラグインを使う場合は,それぞれのREADME.mdを確認して設定してください.

HTTPエンドポイントの設定

Lambdaの設定とHTTPエンドポイントの設定を一気に行えます. functionsセクションに

functions:
  search:
    handler: handler.search
    timeout: 15
    events:
      - http:
          path: /search
          method: get
          cors: true

のように書けば,handler.jsmodule.exportsしているsearchという関数が/searchというパスでHTTP GETされた時に呼び出される形になります.

timeoutのプロパティは必要に応じて設定します. デフォルトは7secなので,外部サービスと連携するなど時間がかかる場合は少し伸ばしてあげないと,タイムアウトします.

S3 bucketやCloudFrontの設定など

S3 bucketなどの設定はresoucesセクションを追加して行います.

resources:
  Resources:
    NewResource:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.bucket}
        AccessControl: PublicRead
        WebsiteConfiguration:
          IndexDocument: index.html
          ErrorDocument: index.html
    WebAppCloudFrontDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Origins:
            - DomainName: ${self:custom.bucket}.s3.amazonaws.com
(長いので以下略)

のように行います.

NewResource(名前はなんでもいいです)のところでは,S3のbucketを作成し,アクセス権の設定や静的サイト配信の設定を行っています.

WebAppCloudFrontDistribution(名前はなんでもいいです)のところでは,CloudFrontの設定を行っています.(しかし,Originの設定がAWS Consoleからプルダウンで行うものと異なるので,設定後にAWS Consoleから再設定します・・・何故・・・)

その他

serverless.yml は色々な設定が出来るので,詳しく設定したい場合は,ドキュメントを確認することをオススメします.

serverless.com

MongoDB Atlasの設定

アプリケーションに少しDBが必要だったので,設定した. 自分がMongoDBばっかり使っていて,SQLを書けない体なのでMongoDB Atlas.

www.mongodb.com

SQLが好きな人はAWSのサービスなどを,BigQueryが好きな人はGoogleを使えば良いと思う.

AWS Consoleでの設定

  • S3で配信するのをHTTPSにするため,AWS Certification Managerで証明書を発行
    • Route53連携で確認ができる方法があるのでそれを選ぶ
    • Step 1: Add domain names
      • ドメイン名を追加して,Add another name to this certificateで*.ドメイン名を指定すると良い(api.ドメイン名もHTTPSにするため)
    • Step 2: Select validation method
      • DNS validationを選択する(これで,Route 53を使ってvalidationすることが出来る)
    • 次のStepで Create record in Route 53のボタンを押すと自動的にRoute 53でレコードが追加される
  • Cloud Frontの設定をする
    • Originの設定
      • Origin Domain Nameをプルダウンからドメイン名.s3.amazonaws.comを選択する(Serverless frameworkのresourcesセクションで指定してるのにうまく出来てない謎)
    • 作成した証明書を指定
      • Generalの設定で,SSL CertificateでCustom SSL Certificateを選んでプルダウンからドメイン名のを選ぶ
    • Cacheの時間を変更(自分は1000msにしてる)
      • BehaviorのObject CachingでCustomizeを選択して,Minimum/Maximum/Default TTLを1000にする
  • Route53の設定をする
    • api.ドメイン名. の設定をする(この時に選択肢にCloud Frontのものがプルダウンで出てくるので選択する)
      • Type: AでAlias: Yesを選択して,Alias Target: をクリックしてcloudfront.netのものを選ぶ
    • ドメイン名.(ベアドメイン.こちらもCloud Frontのものがプルダウンで出てくるので選択する)
      • Type: AでAlias: Yesを選択して,Alias Target: をクリックしてcloudfront.netのものを選ぶ

問い合わせ窓口・寄付先の設置

  • Twitter 国税庁URL変換器(@nta_go)さん | Twitter
    • サービス用に新規でアカウントを作る
    • DMを開放
    • アップデート情報や簡単にリプやDMに対して返信ができる
    • 取材依頼や買収の依頼などDMも来るかもしれない
  • お題箱( お題箱 | ツイッターアカウントで作れるリクエストボックス
    • 匿名での意見を募れる.
    • Twitterアカウントに紐づく.5秒で設置出来る.
  • Amazon Wishlist Amazon.co.jp
    • 投げ銭のように使える
    • Amazonギフト券を入れておくと楽
      • ちゃんと数量を指定しておかないと1枚だけになって枯渇するので注意(今回ミスって枯渇した)
      • 普通に自分の好きなものを入れていてもいい
    • 他のサービスより気軽に使える

素早く作るために

  • Serverless Frameworkを使って,一気に環境を整えてしまう
  • create-react-appでサッとクライアント側を整える
  • 見た目はReact Foundationで提供されるパーツでなんとかする
  • 問い合わせはTwitterのアカウントとお題箱で設置
  • 投げ銭先はAmazon Wishlistを貼るだけ

という感じにして,世の中に公開してフィードバックを得るための仕組みは手間をかけない形で構築して,メインのルーチンを考える・組み上げる時間を多く確保することに重きを置いています.

金額は,ドメイン登録で$12はかかってしまいますが,それ以外の部分はそれほどかかりません. 今回であれば,現時点でCloudFrontが$3・その他$1程度です.

そして,「これだけあれば,とりあえず使ってもらえる!」という状態で公開して反応を見て随時改善を入れていきます.

というのも,誰にも刺さらないサービスを作り込んで公開して全然使われないと,「めっちゃ頑張ったのに誰も使ってくれない・・・自分は世の中で必要とされていないんだ・・・オワタ・・・」みたいになったりするのを予防していたりします.

なお,「公開したサービスが誰にも使ってもらえない」ということと「自分が世の中で必要とされるかどうか」は全く別の事象なので,気にしないことをオススメします.

またなんか作りますね.

サッとReact+Electronでデスクトップアプリを作る

社内ツールや自分で使うデスクトップアプリを作りたいことがあります. あまり細かいことは気にせず,UIとロジックだけに注力したい場合に

  • 「どのバンドルツールを使うか?」
  • 「UIフレームワークをどうしよう」
  • 「Electron触るの久しぶりだから調べないと」

などの本質じゃない所でハマりたくなかったので,その辺りを補助するライブラリを作りました.

まず,UI周りはReactを使います. これは,create-react-appが優秀で,ウェブの場合はcreate-react-appを叩けばすぐにUIの構築に取り掛かれるからです.

(とても簡単なもの以外はreduxを入れたりとか,そういうのはありますが,bundleとかHMRだとかに悩む必要がなくなります)

また,3rd Partyのボイラープレートなどではメンテがされていなくて出来るコードが古くて最新のReactにするとエラーや警告がめっちゃ出るということがあって,どのボイラープレートを使うのが正解かわからなかったからです.

そのため,create-react-appを使いたいという要求があります. 最新のReactでcreate-react-appをして,何か少し手を加えれば最新のReactを使ったElectronアプリがサッと作れる感じがベストです.

海外の記事で How to build an Electron app using create-react-app. No webpack configuration or “ejecting” necessary. というものがありました.

似たようなことに悩んでいる人が手順を追って解決していく記事でした.

しかし,毎回これをやるのはちょっと大変だったので,コマンド1発で行えるようにしました.

react-electron-shell がそれです. 使い方は,

$ create-react-app <YOUR APP NAME>
$ cd <YOUR APP NAME>
$ npm install react-electron-shell --save
$ npm run dev

とやれば,Electron上でcreate-react-appしたロゴがくるくる回るデモが立ち上がります.

react-electron-shellが行っていることはそんなに多くないです.

  • Electron関連パッケージインストール
    • electron
    • electron-packager
  • foremanのインストール
  • Procfile/Electron.js をコピー
  • package.jsonのmain/homepage/scriptsの書き換え

です.

Electron.jsはElectronのブートストラップコードの一部で,windowサイズとかを設定するコードです.後々ユーザが書き換えたいケースが多いので,node_modules内を参照するのではなく,コピーしています.

foremanのインストールとProcfileのコピーはnpm run devでElectronを起動させるための処理を行うために必要でした.

その他の隠蔽しても大丈夫なものはreact-electron-shellの内部に保持して,./node_modules/react-electron-shell/...という形で参照する形にしました.

このあたりの仕組みはReact NativeのExpoが上手くやっていたのでソースを読んでいたら./node_modules内を参照させるという手法だったので,参考にしました.

気になっているのは,npm install react-electron-shellをした時に,カレントディレクトリのpackage.jsonを無理やり書き換えていることで,もう少しいい方法があるのでは・・・と思っています.

最近は新規でElectronアプリを作ってないですが,バンバン小さいデスクトップアプリを作る必要がある人は使ってみるのはいかがでしょうか?

react-electron-shell

React Nativeで画像をdata-uriにしてハンドリングする

こんにちは,ぽちです.

来週7/12のReact Native Meetup#6でLTします.

ネタはたぶんExpo関連の何かをゆるっと話します.

react-native-meetup.connpass.com

本題です.

React Nativeでインターネット上の(Assetじゃない)画像ファイルをTwitterなどにシェアをしたい時ことが有ったのですが,その時にちょっとハマった知見を共有します.

困ったこと

WebサービスのAPIを叩くと,画像のリソースは基本的にURLで返されます.

React Native上表示させるのであれば,返されたURLをImageコンポーネントのsourceアトリビュートに{uri: xxxx}という形で突っ込めばいいのですが,それを何処かに投稿するとなるとURLのままだと困るケースがあります.

Node.jsだと単純に画像のURLにGETしてpipeでStreamを繋いで投稿先にPOSTするとかが使えるかもしれませんが,React Nativeだと同等の手段がなく,困りました.

ShareコンポーネントはURLが受けれる

今回必要になったのは,Twitterへのシェアだったので,

Share コンポーネントを使えばURLのままでも行けるのでは?と思ったのですが,クライアントによってはいい感じで表示してくれなかったため,data-uriの形にして画像としてShareコンポネントに突っ込みたいからでした.

解決法その1: Expo.takeSnapshotAsync

まず初めに思いついたのは,Expoのモジュールにある画面のスナップショットを撮るメソッドtakeSnapshopAsyncを使って,Imageコンポーネントで描画し,そのImageコンポーネントのスナップショットを撮るという手法でした.

<Image
  source={{ uri: this.state.thumbnail }}
  resizeMode="contain"
  style={styles.thumbnail}
  ref="thumbnail"
/>

という形で,得られた画像のurlをthis.state.thumbnailなどに入れておき,それをImageコンポーネントで描画します.

takeSnapshopAsyncで指定するために,refアトリビュートを設定しておきます.

share() {
  Expo.takeSnapShotAsync(this.refs.thumbnail, {
    format: 'jpg',
    quality: 0.9,
    result: 'data-uri',
  })
  .then((image)=>{
    Share.share({
      message: 'message',
      url: img,
      title: 'Share'
    }, {
      dialogTitle: 'Share',
      excludedActivityTypes: [],
      tintColor: 'green'
     });
  });
}

このようにtakeSnapShotAsyncresultプロパティにdata-uriを指定して画像を取得してShareコンポーネントのurlプロパティに設定すればTwitterなどのアプリに画像をシェアすることが出来ます.

しかし,実際にシェアされた画像を見ると・・・

f:id:poChi:20170705135745p:plain

このように白帯が入ってしまっています.

これはImageコンポーネントのスタイルにbackgroundColor: 'transparent'を指定しても同様です.

自分はこれがどうしても気に食わなかったので別の方法を探しました.

解決法その2: buffer

次は,bufferというnpmモジュールを使いました.

GitHub - feross/buffer: The buffer module from node.js, for the browser.

これは,node.jsの標準で用意されているBufferをブラウザにも持ってくるという意図で作られているモジュールです.

これを使って,画像のarraybufferを取得しdata-uriに変換して渡してやろうと考えました.

import axios from 'axios';
import { Buffer } from 'buffer/';
axios
  .get(this.state.thumbnail, {
    responseType: 'arraybuffer'
  })
  .then(response => {
    const image = Buffer.from(response.data).toString('base64');
    Share.share({
      message: `message`,
      url: `data:${response.headers['content-type'].toLowerCase()};base64,${image}`,
      title: 'Share'
    }, {
      dialogTitle: 'Share',
      excludedActivityTypes: [],
      tintColor: 'green'
    });
});

buffer/となっているのはTypoではなく,そう指定するようにドキュメントに書かれています.

画像のarraybufferの取得は GitHub - mzabriskie/axios: Promise based HTTP client for the browser and node.js を使って,responseTypearaybufferを指定してGETすることで取得しました.

request.jsとかsuperagentとかfetchでも出来るような気もします.

得られたarraybufferをBuffer.fromに入れ,toString('base64')で変換します.

このままだと,data-uriではないので,data:${response.headers['content-type'].toLowerCase()};base64,${image}という形にして,data-uriとしてShareコンポーネントのurlプロパティに入れます.

これで元画像を白い帯などが付与されることなく共有できます.

React Nativeで画面を作ってるときにキーボードでボタンが隠れて困るときに使えるKeyboardAvoidingViewが良いという話

こんにちは,ぽちです.

何かとReact Nativeでアプリを作っているとフォーム的なやつよく作りますよね. 上にテキストフォームを置いて,下に決定ボタンを置くデザインなどが非常に良くあるデザインですが,単純に実装してしまうと、下の決定ボタンがキーボードによって隠れてしまって使い勝手が良くない…ということ,良くありますよね. 上部に長文を書くTextInputを,下部に決定ボタンを置いたものを例に挙げると,

f:id:poChi:20170607132211g:plain

このような形で決定ボタン(Tap Hereと書いてある領域)がテキスト入力時に出てきたキーボードによって隠れてしまいます.

そんな時にReact Nativeではこの現象を回避するためのコンポーネントが用意されてたりします. それがKeyboardAvoidingViewです.

このKeyboardAvoidingViewですが,設定するbehaviorプロパティが直感的には分かりにくく「どれを使えばいいんだ!」となりがちです.それぞれbehaviorに設定した値によって動きがどう変わるのかを見ていきます.

(コードサンプルだけ欲しい方は KeyboardAvoidingViewSample · GitHub をどうぞ)

まず,KeyboardAvoidingViewを使うのでimportします.

import { KeyboardAvoidingView } from 'react-native';

KeyboardAvoidingViewを使わない場合のrender()で返すJSXのコードは,

<View style={{flex: 1}}>
  <TextInput style={{flex: 10}} />
  <TouchableOpacity style={{flex: 1, (略)}}>
    <Text>Tap Here</Text>
  </TouchableOpacity>
</View>

のような形になっています.

よく全体を<View>で囲ったりしますが(単一のコンポーネントを返さないといけないため),その直下に<KeyboardAvoidingView>で囲みます.

<View style={{flex: 1}}>
  <KeyboardAvoidingView behavior={this.state.behavior}>
    <TextInput style={{flex: 10}} />
    <TouchableOpacity style={{flex: 1, (略)}}>
      <Text>Tap Here</Text>
    </TouchableOpacity>
  <KeyboardAvoidingView>
</View>

そこで,問題となるのがbehaviorプロパティです. このプロパティには以下の文字列値を取ります.

  • padding
  • height
  • position

それぞれ図を交えて説明します.

behavior=‘padding’

大体paddingを指定しておけばうまく行くケースが多いです. 動作的には,画面を構成する要素(今回だとTextInputとかTouchableOpacityとかの部分)がflexプロパティで構成されていれば,キーボードで狭くなった領域を100%として調整してくれます.

f:id:poChi:20170607132245g:plain

そしてキーボードを閉じると,キーボードを開く前と同じ状態に戻ってきます.

behavior=‘height’

使い道がよくわからないのですが,大体の動作はpaddingを指定した際と同等の動きをします. 違うのは,キーボードを閉じたときに,キーボードを開いて狭まった領域がそのまま維持されるということです.(キーボードがあった領域がそのまま空く)

f:id:poChi:20170607132312g:plain

behavior=‘position’

positionを指定した場合は,<KeyboardAvoidingView>のプロパティにcontentContainerStyleを指定します.(positionを指定したときだけ有効なプロパティです) positionを指定し,キーボードが現れたときに<View>コンポーネントで囲まれるため,{flex: 1}heightを指定しないと要素がグシャっとなってしまうことがよく有ります. キーボードを閉じるとまた元の状態に戻ります. 細かく指定したい場合はpositionを使うと良いのではないでしょうか.

f:id:poChi:20170607132342g:plain

まとめ

キーボードでUI要素が隠れて困る場合はKeyboardAvoidingViewコンポーネントを活用しよう!

動作を確認するために使ったコードは KeyboardAvoidingViewSample · GitHub に置いておくので,いじったりして動作を色々見てもらったら分かりやすいと思います.