OSSにbase64エンコードされた画像を保存する

base64の画像データをOSSに保存します。

これは20x20ピクセルの正方形画像をbase64エンコードしたものです。

iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAACNklEQVQ4EeWTz0tUURTHP+/xnN7kZAjZkIYllpX0uxYtLBAiqIWioVAglIt+yeAigvAfaNOuRW6UIkiCIFwEQWmrCMqiCEm0smZQMcmZ9+bnc957J96bZiIIW+TOC/fecy/n+73nfM+5SjqdFlZwqCvI5VOtQkLNVvQVlXEVavj/KVtLEE+AFP5HgXBiksyRoyiOg2LbyOMnkLP+WSz34SPMhq2Y+xtJbqtHPoxTitBNp3wCcYXklW7I5ZYnjMZIXe0hNHif9Z+iBC/1kj5z9jehh5aZWVIHD/tE5oE9yJt3sLiI1X0Zc0s1Zl0tzq1+cF3IZNGbTqI2H0PKNLRTJ5B8DhIjL8V4+1Fm6veKEZ2X+OADiYVrJN5/T4zpOZk/3i7zza1ijI1LfGhYYlVhSbyfFCPjlKZpWDK3r0kWzkVEtZ+NllJTKitR21tRgxWo7S0Q3kigtwd94DY07kRt2I6i6fB9oYTxipG7GME1fhC4eQPN/fK1kELRxbYLlmVBKIRStYFMWyf27FTRA9SS9BCNYY0MU/HiNawNogb6rv3pUIRpZb5OqY7T6JEeX/iKqW+o66qKHqU92NYF1Zv8s+o8HYViVN6V6yBOHiYmIZsFO4+yuQZxHNyBu7jJBfCi/zXETKKEysHrR0DVznchuo5aHvIvlDU6gd2HMDtb4PM0wQsRkl0dmDvqyI88R6vdRfZ6n9+zHsB9NUZ26A5k0gW8Vy3fWm7xXvdaJaj7RLKU9+2/QX4CzuX6MmaoX4UAAAAASUVORK5CYII=

ちなみにこんなの↓。適当なスクショ。

f:id:asmsuechan:20180904195454p:plain

上の画像をali-oss.put()メソッドを使ってOSSに送信します。

// index.js
const OSS = require('ali-oss')
const client = new OSS({
  region: 'oss-ap-northeast-1',
  accessKeyId: 'xxxxxxxxxx',
  accessKeySecret: 'xxxxxxxxxxxxxxx',
});

client.useBucket('myBucket');
const base64EncodedImage = "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAACNklEQVQ4EeWTz0tUURTHP+/xnN7kZAjZkIYllpX0uxYtLBAiqIWioVAglIt+yeAigvAfaNOuRW6UIkiCIFwEQWmrCMqiCEm0smZQMcmZ9+bnc957J96bZiIIW+TOC/fecy/n+73nfM+5SjqdFlZwqCvI5VOtQkLNVvQVlXEVavj/KVtLEE+AFP5HgXBiksyRoyiOg2LbyOMnkLP+WSz34SPMhq2Y+xtJbqtHPoxTitBNp3wCcYXklW7I5ZYnjMZIXe0hNHif9Z+iBC/1kj5z9jehh5aZWVIHD/tE5oE9yJt3sLiI1X0Zc0s1Zl0tzq1+cF3IZNGbTqI2H0PKNLRTJ5B8DhIjL8V4+1Fm6veKEZ2X+OADiYVrJN5/T4zpOZk/3i7zza1ijI1LfGhYYlVhSbyfFCPjlKZpWDK3r0kWzkVEtZ+NllJTKitR21tRgxWo7S0Q3kigtwd94DY07kRt2I6i6fB9oYTxipG7GME1fhC4eQPN/fK1kELRxbYLlmVBKIRStYFMWyf27FTRA9SS9BCNYY0MU/HiNawNogb6rv3pUIRpZb5OqY7T6JEeX/iKqW+o66qKHqU92NYF1Zv8s+o8HYViVN6V6yBOHiYmIZsFO4+yuQZxHNyBu7jJBfCi/zXETKKEysHrR0DVznchuo5aHvIvlDU6gd2HMDtb4PM0wQsRkl0dmDvqyI88R6vdRfZ6n9+zHsB9NUZ26A5k0gW8Vy3fWm7xXvdaJaj7RLKU9+2/QX4CzuX6MmaoX4UAAAAASUVORK5CYII=";

client.put('great.jpeg', new Buffer(base64EncodedImage, 'base64'), {mime: 'image/jpeg'}).then((r) => {
  console.log(r)
})

できました。

$ node index.js
{ name: 'great.jpeg',
  url:
   'http://xxxxxxx.oss-ap-northeast-1.aliyuncs.com/great.jpeg',
  res:
   { status: 200,
     statusCode: 200,
     headers:
      { server: 'AliyunOSS',
        date: 'Tue, 04 Sep 2018 10:46:21 GMT',
        'content-length': '0',
        connection: 'keep-alive',
        'x-oss-hash-crc64ecma': '4336817615697645645',
        'content-md5': 'hgOG0apst+UHhylCs8lorQ==',
        'x-oss-server-time': '19' },
     size: 0,
     aborted: false,
     rt: 146,
     keepAliveSocket: false,
     data: <Buffer >,
     requestUrls:
      [ 'http://xxxxxxxxxx.oss-ap-northeast-1.aliyuncs.com/great.jpeg' ],
     timing: null,
     remotePort: 80,
     socketHandledRequests: 1,
     socketHandledResponses: 1 } }

react-native + auth0でSomething went wrongが出る

react-native + auth0を使ったときログイン画面に飛ばずにSomething went wrongとだけ表示されてログにも何も出ない現象で困りました。

解決法

アプリのパッケージ名にアンダースコア(_)が入っているとダメみたいです。作り直したら無事できるようになりました。

webAuth redirect_uri doesn't work with app scheme (expects http:// or https:// ) · Issue #66 · auth0/react-native-auth0 · GitHub

error: cannot find symbol import com.reactnativenavigation.NavigationReactPackage;

react-native-navigationをビルドした時に失敗したのでその解決法をメモします。

/Users/asmsuechan/src/my_mobile/android/app/src/main/java/com/my_mobile/MainApplication.java:6: error: cannot find symbol
import com.reactnativenavigation.NavigationReactPackage;
                                ^
  symbol:   class NavigationReactPackage
  location: package com.reactnativenavigation
1 error
:app:compileDebugJavaWithJavac FAILED

import法が変わっているみたいで、import com.reactnativenavigation.NavigationReactPackage;ではなくimport com.reactnativenavigation.bridge.NavigationReactPackage;としたら解決しました。

FAILURE: Build failed with an exception. · Issue #1242 · wix/react-native-navigation · GitHub

react-nativeでPinterestみたいなUIを実現する

Pinterestの2カラムで高さ可変のUIをreact-nativeで作ります。

目標としたUI

f:id:asmsuechan:20180903203039p:plain

このレイアウトはMansoryといいます。

できたUI

f:id:asmsuechan:20180903203900p:plain

ライブラリ

Mansoryレイアウトを実現するJSのライブラリはいくつかあって、PackeryMansory.jsBricks.jsなどが有名です。しかしこれらはそのままreact-nativeでは使うことができないのでreact-native用のコンポーネントを探しました。

すると以下の2つが見つかりました。

github.com

github.com

まずスター数が多いreact-native-masonryの方を試したのですが、これが結構曲者でなかなか期待通りに動いてくれないしバグってるしなんかオーナー修正するつもりなさそうだしで諦めました。

次に試したのがreact-native-masonry-layoutです。今回はこちらを使用しました。

これも正直スター数が少なかったりインタフェースが微妙だったりといろいろ不穏な雰囲気はあるのですが、動くのでよしとしました。

コード

実際に使ったコードはこちらです。

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, Image, TouchableHighlight, Dimensions} from 'react-native';
import Masonry from 'react-native-masonry-layout';

type Props = {};
export default class App extends Component<Props> {
  constructor() {
    super();

    this.state = {
    };
  }

  componentDidMount() {
    const { width } = Dimensions.get( "window" );
    const columnWidth = ( width - 10 ) / 2 - 10;
    const data2 = [
      {
        image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPL2GTXDuOzwuX5X7Mgwc3Vc9ZIhiMmZUhp3s1wg0oHPzSP7qC',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 500
      },
      {
        image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQGYfU5N8lsJepQyoAigiijX8bcdpahei_XqRWBzZLbxcsuqtiH',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 600
      },
      {
        image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQGYfU5N8lsJepQyoAigiijX8bcdpahei_XqRWBzZLbxcsuqtiH',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 700
      },
      {
        image: 'https://img.buzzfeed.com/buzzfeed-static/static/2017-03/30/12/asset/buzzfeed-prod-fastlane-01/sub-buzz-24597-1490890739-1.jpg',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 600
      },
      {
        image: 'https://img.buzzfeed.com/buzzfeed-static/static/2017-03/30/12/asset/buzzfeed-prod-fastlane-01/sub-buzz-24597-1490890739-1.jpg',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 300
      },
      {
        image: 'https://img.buzzfeed.com/buzzfeed-static/static/2017-03/30/12/asset/buzzfeed-prod-fastlane-01/sub-buzz-24597-1490890739-1.jpg',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 400
      },
      {
        image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPL2GTXDuOzwuX5X7Mgwc3Vc9ZIhiMmZUhp3s1wg0oHPzSP7qC',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 500
      },
      {
        image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPL2GTXDuOzwuX5X7Mgwc3Vc9ZIhiMmZUhp3s1wg0oHPzSP7qC',
        text: 'wwwww',
        key: Math.random().toString(36).slice(-8),
        height: columnWidth / 400 * 500
      },
    ]
    this.refs.list.addItems( data2 );
  }

  render() {
    return (
      <View style={{ flex:1 }}>
        <View style={{flex: 1, flexGrow: 10, padding: 5}}>
          <Masonry
            ref="list"
            columns={2}
            renderItem={(item)=><View
                          style={{
                            margin: 5,
                            backgroundColor: "#fff",
                            borderRadius: 5,
                            overflow: "hidden",
                          }}>
              <Image source={{ uri: item.image }} style={{ height: item.height }}/>
              <Text>some text</Text>
            </View>}
          />
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
});

環境

$ react-native --version
react-native-cli: 2.0.1
react-native: 0.56.0

AliEaters meetup #6 に参加した

8/29にあった Alibaba Cloud Developers Meetup #6に参加しました。

togetterにまとめがあります。これを見たら雰囲気はわかると思います。 8/29 AliEaters Meetup#6 - Togetter

会場は霞が関にあるWeWork日比谷パークフロントです。行く途中に国会議事堂があったのですけど、初めて見ました。国会議事堂。

f:id:asmsuechan:20180831112615p:plain 会場には想像以上に人がいたのですが、WeWorkのドロップインメンバーもいたのでそう見えたみたいです。

そしてなんと会場はビール飲み放題でした。最高です。めちゃくちゃ飲みました。ごちそうさまです。

セッション

今回は15分のセッションが5つあって、

  • 中国のIT事情~中国のITリーディングカンパニー~
  • 一番身近なAlibaba『Aliexpress』を使おう!
  • アリババクラウドK8S上のGreen/Blueデプロイメントの実現方法
  • IoT Platformで作るスマートロックシステム
  • The Solution of IoT ー農業IoTー

でした。私はスマートロックの話をしました。あんなに酒飲んで発表したのは初めてです

会場の雰囲気はちょっとザワザワしていて(コワーキングスペースだから)発表するにはちょっと楽です。傾注!って感じで聞かれるとドキドキしてしまいますからね。

それぞれめちゃくちゃ簡単にまとめます。

中国のIT事情~中国のITリーディングカンパニー~

SBクラウドのSissyさんの発表です。

f:id:asmsuechan:20180831121134j:plain

中国のBAT(Baidu, Alibaba, Tencent)に関する情報や独身の日の爆買いなど、中国のインターネット企業に関することを概要的に説明してくれました。

独身の日の購入額が楽天の1年の流通額に近いって話があったんですが、すごいですね。

そして上の画像にあるCreate@TOKYOが10月9日に開催されます。これはビジコンで、優勝賞金はAlibaba Cloudのクーポン300万円分!x.largeのインスタンスを立てまくれます。日本を勝ち抜けば中国に行けるみたいです。

peatix.com

さらにAlibabaはAI/機械学習コンテストもやっていて、優勝賞金は300万円くらいです。すごい。

天池大赛列表页

Sissyさん写真提供ありがとうございます!

一番身近なAlibaba『Aliexpress』を使おう!

スターティアの石橋さんの発表です。

f:id:asmsuechan:20180831121336j:plain

www.slideshare.net

Aliexpressはいわゆる中華スマホやガジェットを安く見つけることができて、シャオミのスマホも楽に(?)手に入るそうです。

ちなみに私はEssential Phoneを使っています(布教)

次デバイス系の何かを探すときはAliexpressを当たってみることにします。

アリババクラウドK8S上のGreen/Blueデプロイメントの実現方法

SBクラウドのWangzzさんの発表です。

f:id:asmsuechan:20180831120950j:plain

今回のミートアップで唯一のガッツリとテクノロジーな発表でした。ブルーグリーンデプロイメントとは何か、の話から始まって実演動画(ターミナルのスクショ動画)に終わるよい発表でした。

たぶんAlibaba Cloud Kubernetesに関する知見は日本語だとどこにもないからとてもありがたいですね。

k8sやっていかねば。。。

IoT Platformで作るスマートロックシステム

不肖私の発表です。

f:id:asmsuechan:20180831121023j:plain

内容はただスマートロックを作ったよ、という話でIoT Platformをゆるふわに布教したといった感じです。

短い発表でしたが終始ゴキゲンな感じに進んだのでなかなか良かったように思っています。

この発表で私が言いたかったことはこれだけです。

f:id:asmsuechan:20180831114207p:plain

The Solution of IoT ー農業IoTー

アイフォーカスの小野さんの発表です。

f:id:asmsuechan:20180831121049j:plain

前々から地味に話題になりつつもパンピの私はなかなかお目にかかることがなかった農業IoTの話です。聞けて良かったです。

こういう大きな事業は動くお金も大きそうです。

IoT+AIによる農業の専門家のための農業クラウドサービス開始 | クレバアグリ株式会社

まとめ

AliEatersはこんな人にオススメです。

  • 中国展開を考えている人
  • Alibabaが気になる人
  • Alibaba Cloudの知見を発表したい人
  • Alibaba Cloud使いと知り合いたい人

来月(9月)あるみたいなのでまた参加したいです!

GraphQLのミューテーションを使う

GraphQLのミューテーションはリソースを変更するものです。

実行順の問題で、argsで渡された引数を元にリソースを変更することは好ましくありません。(詳しくは公式ドキュメントに任せます)

const express = require('express')
const graphqlHTTP = require('express-graphql')
const {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLList,
  GraphQLString,
  GraphQLInt,
  GraphQLInputObjectType,
  GraphQLNonNull
} = require('graphql');
const atob = require('atob');

const userType = new GraphQLObjectType({
    name: 'user',
    fields: () => ({
      id: { type: GraphQLInt },
      sub: { type: GraphQLString },
      created_at: { type: GraphQLString },
      updated_at: { type: GraphQLString }
    })
  })

function createUser(query) {
  console.log(query)
  return {
    id: 1,
    sub: 'auth0|aaaaaaaaaaaaaaa'
  }
}

const createUserType = new GraphQLObjectType({
  name: 'createUserType',
  fields: {
    sub: { type: new GraphQLNonNull(GraphQLString) }
  }
})

const mutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    createUser: {
      type: createUserType,
      args: { sub: { type: GraphQLString } },
      resolve(obj, { sub }) {
        return createUser(sub);
      }
    }
  }
})

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: () => ({
      user: {
        type: userType,
        args: {
          sub: { type: GraphQLString }
        },
        resolve: (value, { sub }) => {
          return createUser(sub)
        }
      }
    })
  }),
  mutation: mutation
});

var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  graphiql: true,
}));
app.listen(4000, () => console.log('Now browse to localhost:4000/graphql'));

以下のリクエストを投げたところ、正しく結果が返ってきたことが確認できました。ここでMとfirstは任意の名前です。

mutation M {
  first: createUser(sub: "aaaaa") {
    sub
  }
}

f:id:asmsuechan:20180902173017p:plain

GraphQLでパラメータを投げる

最近GraphQLを使っているのですがリクエストにパラメータを乗せるのに四苦八苦したのでメモしておきます。

const express = require('express')
const graphqlHTTP = require('express-graphql')
const {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLList,
  GraphQLString,
  GraphQLInt,
  GraphQLInputObjectType,
  GraphQLNonNull
} = require('graphql');
const atob = require('atob');

const postType = new GraphQLList(
    new GraphQLObjectType({
      name: 'post',
      fields: {
        id: { type: GraphQLInt },
        imageUrl: { type: GraphQLString },
        created_at: { type: GraphQLString },
        updated_at: { type: GraphQLString }
      }
    })
  )

function getPosts(limit) {
  return [
    { imageUrl: 'aaaaa' },
    { imageUrl: 'bbbbb' }
  ]
}

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: () => ({
      post: {
        type: postType,
        args: {
          limit: { type: GraphQLInt }
        },
        resolve: (value, { limit }) => {
          return getPosts(limit)
        }
      }
    })
  })
});

var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  graphiql: true,
}));
app.listen(4000, () => console.log('Now browse to localhost:4000/graphql'));

以下のリクエストを投げて結果を確認します。getPostsは任意の名前、postはフィールド名、limitは引数名、imageUrlはpostで返ってくるpostTypeの属性名です。

query getPosts {
  post(limit: 10) {
    imageUrl
  }
}

$ node index.jsをするとgraphiqlが見えてレスポンスを確認できます。

f:id:asmsuechan:20180902155059p:plain

コードを書くときはgraphql-jsのテストが参考になります。

https://github.com/graphql/graphql-js/blob/master/src/tests/starWarsQuery-test.js

https://github.com/graphql/graphql-js/blob/master/src/tests/starWarsSchema.js

にしてもGraphQLめちゃくちゃ取っつきにくいですね。RESTは思考停止でも書けるってことを強く感じました。