20211201のNode.jsに関する記事は7件です。

【Node.js】ExpressとFirebaseで画像アップロードを実装する

■はじめに 普段はVue.js, Nuxt.jsを使っているのですが、Expressで画像アップロードを実装しようとしたらいつもと勝手が違ったのとサーバー側でFirebaseを使う時はFirebase SDKではなくFirebase Admin SDKを使わないといけないことを理解しておらず詰まったのでメモ。 plugins/firebase.js const admin = require("firebase-admin"); require("dotenv").config(); const ServiceAccount = require("../ServiceAccount.json"); if (admin.apps.length === 0) { admin.initializeApp({ credential: admin.credential.cert(ServiceAccount), storageBucket: process.env.FIREBASE_ADMIN_STORAGE_BUCKET }); } module.exports = admin; testRoute.js const express = require("express"); const multer = require("multer"); const router = express.Router(); const testController = require("../controllers/testController"); const upload = multer({ storage: multer.memoryStorage(), }); router.post("/", upload.single("file"), listController.list_create_post); module.exports = router; testController.js const admin = require("../plugins/firebase"); const db = admin.firestore(); const bucket = admin.storage().bucket(); const test_create_post = async (req, res) => { const file = bucket.file( `thumbnail/${req.file.originalname}` ); await file.save(req.file.buffer); }; module.exports = { list_create_post, }; create.ejs <!DOCTYPE html> <html lang="ja"> <body> <div class="create-blog content"> <form action="/lists" method="POST" enctype="multipart/form-data"> <label for="file">File:</label> <input type="file" id="file" name="file" required /> <button>Submit</button> </form> </div> </body> </html> おわりに ExpressとFirebaseで画像アップロードをするには下記が必要なことがわかりました。 ①formタグをmultipart/form-dataにする。 ②multipart/form-dataの処理ができるmulterをインポートする。 ③Firebase SDKではなくFirebase Admin SDKを使用する。 サーバーの方にとっては基本的なことかもしれませんが、フロントだけやってるとこんなところで躓くのかーという感想です。GCSにあげる際はCloud Functionsでとか、そもそもFirestoreは使わないでMongoDBにあげるなど色々あるので試してみたいです。 参考URL
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsをモダンなJSで実装する JavaScriptの歴史も理解しながら

はじめに Node.jsではrequire/module.exportを使うが、フロントエンドだとimport/exportを使うのがほぼであるのに、なんでそうなるの?と思う事があるかもしれない。 その辺りについてきちんと理解してみたのでそれの備忘録を残す。 JavaScriptの歴史からNodeとは何か?とNode.jsでimpoert/exportを使うにはwebpack/babelが必要になる事を理解する 最近(2015年以降)にフロントエンドをやり始めると、モダンな書き方でES6(ECMAScript2015)でJavaScriptを書くと思うが、その状態でNode.jsも書き始めると、なんか書き方が違うな~と感じると思う。 分かりやすいのがモジュール管理(他のファイルに実装した関数を使う、他のライブラリを使う、をする時の呼び込み方法)で、 import lodash from 'lodash'; という書き方と、 const lodash = require('lodash'); という書き方の2つを見た事があると思う。 何でこういうことになるのか?について少し理解してみる事にする。そしてNode.jsでimport/exportのモジュール管理方法(モダンな書き方)をするために何が必要か?も理解していく。 理解する上ではJavaScriptの歴史から入らないと分からないので、その歴史を見ていき、Node.jsがいつ誕生してその後何が起きたか?を見ていく事にする。 JavaScriptの歴史 歴史の全体感を見るには以下のサイトが分かりやすい。 この歴史の中で今回特に関係するのは、 2009年 Node.js の誕生 2015年 ES2015:大規模な ECMAScript のアップデート の2つ。 また、同時にJavaScriptのversionについても抑えておく必要があるが、version一覧は以下が分かりやすい。 Node.jsの誕生 2009年にNode.jsが誕生する。発端は、Chromeに搭載されたV8Engineと呼ばれるものがとても良いという事で、これをJavaとかRuby、Pythonと同じようにサーバーサイドの言語として使おうとして生まれたもの。 ここで前項のJavaScriptの歴史を見返すと、 年代 version名 1997-1999 ES1 ES2 ES3 2009- ES5 2015- ES6 2016- ES2016 ... ... のようになっており、Node.jsが生まれた時のJavaScriptのversionはES5(ECMAScript 5)と呼ばれるものである事が分かる。となると必然的にその当時のJavaScriptのversionでNode.jsが実装される事になるわけで、実際にそうなっている。 そして、モジュール管理の方法はCommonJSという仕様で行われており、それがまさしく const lodash = require('lodash'); という書き方のものになる。 参考:Node.js とは何か importとrequireという2つの書き方がある理由 上記で見てきたように、JavaScriptには色々な歴史があり、その過程で流派の違いのようなものが生まれた。そのため例えばモジュール管理の方法1つをとっても、その実装方法が違うものが世の中に存在しているという状態になっている。 もっともな疑問として新しいversionに統一すればいいのに、という事があるがそれについては次の項で見ていく。 JavaScriptの世界のルール webpack/babelが登場するわけ JavaScriptの歴史をみると新しいJavaScriptのversionが次々に生まれているが、後方互換(古いブラウザ(JavaScriptエンジン)でも動くようにしないといけない)というようなルールがある。これは昔からあるブラウザを使っているユーザを切り捨てないためにそういうルールになっている。実際にブラウザによってはES5の書き方でないとダメというブラウザ1もある。 そういった事情から、いくら新しいversionのJavaScriptの書き方があると言っても、昔からあるブラウザなどでもきちんと動くようにしてあげる必要が生じるのである。 ただ、折角新しいversionになったのに古くからある書き方をするのも…という事で、JavaScriptのversionを変えるツールがちゃんとある。それがbabelと呼ばれるオープンソースのライブラリで、これを使うと実装されたコードのJavaScriptのバージョンを変える事ができる。そしてそのbabelは、最近ではwebpackに組み込んで使われ、HTMLとかCSSとかその他色々なものをバンドルする際に同時にJavaScriptのversionを変えるという事が良く行われる。 これにより例えば、ES6(2015年のJavaScript version)で書いたコードをES5(2009年のJavaScript version)の書き方に変換する、という事ができる。そのため、フロントエンドでES6以降のモダンな書き方をする場合、必ずwebpack/babelがそのプロジェクトには組み込まれている事になる(React/Vue/Angularなどのフレームワークを使うと意識する事はないかもしれないが)。 では、バックエンド(サーバサイド)のJavaScriptであるNode.jsではどうなるのか?というと、それは次の項で見ていく。 Node.jsでimport/exportを使うには? 前項『Node.jsの誕生』で見たように、Node.jsはES5の書き方で書く必要があり、ES6以降のJavaScriptとはversionが違うものになっている(Node.jsもJavaScriptではあるのの、それはES5というルールで実装する必要があり、ES6とかES2016とかモダンと呼ばれるJavaScriptとは違う)。 ただ、前項のwebpack/babelでJavaScriptの書き方のversionを変える事ができるという話を踏まえると、単純に、Node.jsの方でもそのwebpack/babelの仕組みを使えばよいだけじゃんという事に気づく。 つまり、ES6以降のモダンな書き方をしても、それをwebpack/babelで変換してあげればES5の書き方をしたコードを生成できるので、Node.jsでもES6以降のモダンなJavaScriptの書き方で実装ができるという事。 これで何がうれしいかというと、 フロントエンドとバックエンドで書き方が違うという事をなくせるので分かりやすくなる(フロントエンドを新たに実装する場合、その大多数はES6以降の書き方であるので) versionが変わるのには理由(前のversionの良くない点などの改良)があるはずで、その改良を取り込んで最新の実装しやすい書き方ができる などのメリットがあるといった感じ。 実際にどんな設定・実装になるのか?については次の章『実際にNode.jsをモダンな書き方(ES6以降)で実装する』で扱う。 補足 webpackとbabel At its core, webpack is a static module bundler for modern JavaScript applications. webpackは基本的にはモジュールのバンドラーでしかないが、この中にbabelという機能を取り込むと、バンドルの過程でJavaScriptの書き方のバージョンを変えてくれる。 というわけで世の中的によくwebpack×babelの組み合わせが見られる。 実際にNode.jsをモダンな書き方(ES6以降)で実装する ES6以降の書き方でExpressのサーバを実装し、それを起動できるようにするまでをこの章では見ていく。 結論:どうすればいいか? webpack.config.js module.exports = { target: 'node', entry: './src/index.js', module: { rules: [ { test: /\.m?js$/, exclude: /node_modules/, use: { loader: 'babel-loader', }, }, ], }, }; .babelrc { "presets": [ "@babel/preset-env" ] } webpackでbuildするコードには以下を追記(npm install または yarn addも)。 import 'core-js/stable'; import 'regenerator-runtime/runtime'; npm installl --save-dev core-js@3 npm installl --save-dev regenerator-runtime/runtime buildするコードの全体としては以下のようになる。 index.js import 'core-js/stable'; import 'regenerator-runtime/runtime'; import express from 'express'; const app = express(); app.use(express.json()); app.get('/', async (req, res) => { const reqTime = Date.now(); console.log(Array.from('foo')); await new Promise((resolve) => { setTimeout(() => { resolve('sleep'); }, 500); }); res.status(200).send({ msg: 'hello world!', elaptime: Date.now() - reqTime, }); }); app.listen(3000, () => console.log('listening on port 3000!')); ※JavaScript configuration filesに書かれているように、BabelのConfigurationはJavaScriptでも書ける。その場合は以下のようになる。 babel.config.js const presets = [['@babel/preset-env']]; module.exports = { presets }; ※buildするコード内でDate.now(), Array.from, async/awaitなどを書いているがこれは意図的にそうしており、 Date.now(), Array.from標準組み込みオブジェクトのES5への変換を確認するため(polyfill ECMAScript features) async/awaitgenerator関数やasync/awaitのES5への変換を確認するため(use transpiled generator functions) の意味合いで実装している。 以降の項では、webpack.config.jsや.babelrc(babel.config.js)が上記のようになる理由について順にみていく。 何もせずに実装し実行するとどうなるか? まず、何もせずに単純にES6以降の書き方で書いたコードを実行してみるとどうなるか?を確認してみる。 import express from 'express'; const app = express(); // 省略 app.listen(3000, () => console.log('listening on port 3000!')); [root@localhost node-express]# node src/index.js (node:9816) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension. (Use `node --trace-warnings ...` to show where the warning was created) /root/workspace/node-express/src/index.js:1 import express from 'express'; ^^^^^^ SyntaxError: Cannot use import statement outside a module at ... 一部省略しているが、import文の所(モジュール管理の書き方)でエラーが出てしまう・・・。 webpack × babelの設定を追加し、webpackコマンドを実行する 次に、webpackとbabelを使ってJavaScriptのversionを下げる(ES6以降の書き方をES5に変換=トランスパイル)設定を追加する。 公式のbabel-loaderに沿って実装する。 webpack.config.js module.exports = { target: 'node', entry: './src/index.js', module: { rules: [ { test: /\.m?js$/, exclude: /node_modules/, use: { loader: 'babel-loader', }, }, ], }, }; .babelrc { 'presets': ['@babel/preset-env'] } ※target: 'node',については、webpackの公式ドキュメント(Targets)を読んでみると、 In the example above, using node webpack will compile for usage in a Node.js-like environment (uses Node.js require to load chunks and not touch any built in modules like fs or path). と書かれているように、Node.jsの場合はtarget: 'node'の設定が必要なので、webpack.config.jsにその設定を追加する。 上記の実装後にnpx webpack --mode=developmentを実行すると・・・2 $ webpack --mode=development asset main.js 949 KiB [emitted] (name: main) runtime modules 1.04 KiB 5 modules cacheable modules 716 KiB javascript modules 455 KiB 56 modules json modules 261 KiB ... 15 modules Done in 3.77s. のようになり、buildはうまくいっているように見える。さらにnode dist/main.jsを実行すると・・・ [root@localhost node-express]# node dist/main.js webpack://my-webpack-project/./src/index.js?:13 var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(req, res) { ^ ReferenceError: regeneratorRuntime is not defined at eval (webpack://my-webpack-project/./src/index.js?:13:46) ... のようにエラーが出てしまう。 core-js/regenerator-runtimeを使う regeneratorRuntime is not definedというエラーはES6以降の書き方でaysnc/awaitやgenerator関数というものをES5に変換するために必要な翻訳を担うものがないために起こっているエラー。 まずそもそもJavaScriptには標準組み込みオブジェクトと呼ばれるものが存在し、ライブラリを依存させたりせずに使える。例えば、Array.from('foo')とかが使えるのはこの標準組み込みオブジェクトのおかげ。 そしてこの標準組み込みオブジェクトもJavaScriptのversionが変わるごとに変わっており、ES6にはあるがES5にはないものがある。そうなるとES5で動く事を前提にしてるブラウザ(例えばIE11)ではES6の標準組み込みオブジェクトは使えないという事になる。 そこでpolyfillが登場する。これはJavaScriptの標準組み込みオブジェクトのversion間の差を埋めてくれるもので、例えば、ES6にある標準組み込みオブジェクトで実装したコードをES5のコードに変換する役割を担ってくれる。 このpolyfillだが、昔は@babel/polyfillというものがあり、これを利用していた。ただ、公式のサイトの注意書きの通りこのライブラリは非推奨になり、代わりにcore-js/stableとregenerator-runtime/runtimeを使い、ES5への変換を行う。それぞれ、 core-jspolyfill ECMAScript features regenerator-runtime/runtimetranspiled generator functions の役割がある。 実装としてはBabelのサイトにあるように、index.jsに index.js import "core-js/stable"; import "regenerator-runtime/runtime"; のようにimportを追加するだけでよい。 参考:@babel/polyfill おまけ 今回、Node.jsをモダン化する方法を見てきたが、上記のようにimport 'core-js/stable';, import 'regenerator-runtime/runtime';と書かないでも動く場合もあり混乱した。ちょっとどのパターンならbuildして動くのか?を調査してみたので以下にまとめてみた。 ※注意 今回検証に使ったコードだから動いただけの可能性もあり、この辺りはまだ理解が完全ではないため参考情報程度に見て頂ければと思います ※buildはnpx webpack --mode=developmentの事で、server起動はnode dist/main.jsの事 # パターン概要 build・server起動結果 package.json .babelrc(babel.config.js) index.js 1 webpackの設定のためのライブラリのみ ・build:成功・server起動:エラー 2 core-jsとregenerator-runtimeを依存に追加のみnpm install -D core-js regenerator-runtime ・build:成功・server起動:エラー(上と同じ) (上と同じ) (上と同じ) 3 babelの設定にuseBuiltInsを追加(usage)※必要なpolyfillだけ読み込むように設定 ・build:成功・server起動:成功 (上と同じ) (上と同じ) 4 babelの設定にuseBuiltInsを追加(entry)※全polyfillを読み込む ・build:成功・server起動:エラー (上と同じ) (上と同じ) 5 index.jsにimport 'core-js/stable';, import 'regenerator-runtime/runtime';を追記 ・build:成功・server起動:成功 (上と同じ) (上と同じ) ※5のパターンは、上記の結論どうすればいいかの設定に加えてbabelのconfigurationの設定も追加したものになる。 ※やってみて分かったが、パターン5のようにindex.jsにimport 'core-js/stable';, import 'regenerator-runtime/runtime';を書いた状態ではファイルサイズが大きくなるので、パターン3のようにuseBuiltInsの設定をusageにしてindex.jsでのimportはなしのやり方がいいかもしれないが、今までの@babel/polyfillでは全てのpolyfillを読み込んでいたので、素直にimportの書き方だけ(結論どうすればいいかの設定)をするでもいい気もする パターン main.jsのファイルサイズ 3 1.11 MiB 5(結論どうすればいいかのやり方) 1.72 MiB ※ちなみ、非推奨になった@babel/polyfillを使ってbuildをすると、サイズは1.42 MiBだった。 webpack.config.js module.exports = { target: 'node', entry: ["@babel/polyfill", "./app/js"], module: { rules: [ { test: /\.m?js$/, exclude: /node_modules/, use: { loader: 'babel-loader', }, }, ], }, }; 参考文献 Babel7.4で非推奨になったbabel/polyfillの代替手段と設定方法 IE11 ↩ --mode=developmentはトランスパイル後のソースコードを人が読めるものにするために設定 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メモ:Windows上でapache kafka+kafka-node

分散メッセージキューkafkaそのものについてはここの解説が詳しい。 あとはこれとか。 AWSのAmazonMSKのようなフルマネージドが楽そうだが、まずはWindows上でどんな感じか試してみる。 環境は少し古いWindows Server 2012 node.jsは16.x kafkaインストール こことかここを見ながら。 とりあえずhttps://kafka.apache.org/downloadsからバイナリダウンロード 7zipで解凍して適当なフォルダに配置 zookeeperは組み込みを利用するので、bin\windows\zookeeper-server-start.bat config\zookeeper.propertiesで起動 別のプロンプトを立ち上げてbin\windows\kafka-server-start.bat config/server.properties 起動してみるとjava.nio.file.AccessDeniedExceptionのエラー config下にあるlog.dirs関係を全てlog.dirs=G:\tmp\kraft-combined-logsのようにWindows形式のパスに変更すると java.nio.file.InvalidPathException: Illegal char < > at index 2: G: mpkafka-logs\meta.properties.tmp となったので、\を\でエスケープしてみるがやはりAccessDeniedExceptionエラー。 フォルダの権限の問題でもない。 調べてみると単純に2.12-3.0.0の問題のようなので古いバージョンのkafka_2.12-2.8.1に変更すると設定を変更しなくても動いた。 さらに別のコンソールを開いてトピックを作成して確認 topc名(test)が出てくるはず。 partitionsはとりあえず1で試せばよいが、consumerが複数の場合はそれ以上のpartitionsが存在している必要あり。 bin\windows\kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test bin\windows\kafka-topics.bat --list --zookeeper localhost:2181 なお、削除は bin\windows\kafka-topics.bat --delete --zookeeper localhost:2181 --topic test メッセージ送受信(kafka-node) ダウンロード数を見るとkafkajsやnode-rdkafkaよりkafuka-nodの方が優勢で、 サンプルも多そうなのでkafka-nodeを利用する。 こことかでnode.jsからのアクセス方法が書いてある。 npm install kafka-node サンプルとは少し変えて、expressでGETで投げたものを適当に直接kafkaに投げてみる 'use strict'; const kafka = require("kafka-node"); const express = require('express'); const app = express(); const port = 3000; const Producer = kafka.HighLevelProducer; const Consumer = kafka.Consumer; const client = new kafka.KafkaClient({ kafkaHost: "localhost:9092" }); const producer = new Producer(client, { // Partitioner type (default = 0, random = 1, cyclic = 2, keyed = 3, custom = 4), default 0 partitionerType: 1 }); const consumer = new Consumer( client, [{topic: "test", partision:0}], { groupId: "my-consumer", autoCommit: true, fromOffset: true } ); app.get("/", function(req, res, next){ const name = req.query.name; const age = req.query.age; const message = [ { topic: "test", messages: JSON.stringify({name: name, age: age}) } ]; producer.send(message, (err, data) => { if (err) console.log(err); else console.log('send messages'); //process.exit(); }); res.status(200).send("SEND"); // テストなので常に成功扱い }); // 受信する場合 consumer.on("message", (message, err) => { console.log(message); }); app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 送信出来ているかどうかをコマンドでも確認してみると、飛んできている事がわかる。 --from-beginningを指定すると最初から読み込む。 bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test --from-beginning 受信にはいくつかパターンがあるようで、ここ見ると new kafka.ConsumerGroupStreamを使うのが一番高機能っぽい。 詳細は公式 SQLを使ってkafkaをコントロールするKSQLのようなものもあるがMSKは対応していなさそう。 マルチConsumer化 起動時に Error while executing topic command : Replication factor: 1 larger than available brokers: 0. というようなエラーが出て立ち上がらない事があったが、tmpを全て削除して、アプリも終了させてから起動させると立ち上がった。 tmp等を削除してから再起動して bin\windows\kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 2 --topic test としてpartitionを複数で立ち上げる。 Consumerは先のnode.jsのものからポートを3001に変更したものを作成。 partitionを指定しているため、[{topic: "test", partition:1}],のようにして別のものを指定して起動。 ブラウザから登録してみると別々の受信をしている事が分かる。 とはいえ、これでは自動でpartitionが割り振られないので使いにくい。 ConsumerGroupStreamを使ってグループで割り振られるようにする。 2つ用意するが、ポートの違いのみ。 'use strict'; const kafka = require("kafka-node"); const express = require('express'); const app = express(); const port = 3000; const Producer = kafka.HighLevelProducer; const producer = new Producer(client, { partitionerType: 1 }); const consumer = new kafka.ConsumerGroupStream({ kafkaHost: "localhost:9092", groupId: 'my-consumer', autoCommit: true, fromOffset: 'earliest' }, 'test') app.get("/send", function(req, res, next){ const name = req.query.name; const age = req.query.age; const message = [ { topic: "test", messages: JSON.stringify({name: name, age: age}) } ]; producer.send(message, (err, data) => { if (err) {console.log(err); res.status(500);} else console.log('send messages'); //process.exit(); }); res.status(200).send("SEND"); }); // messageではなくdataになる consumer.on("data", (message, err) => { console.log(message); }); app.listen(port, () => console.log(`Example app listening on port ${port}!`)); これで実験すると各々が異なるデータを受信出来たので概ねOKぽい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

草を生やしてこどものモチベーションを上げる【その1】仕様〜設計編

前書き 去年から、AtCoderにどハマりしています。 毎日精進して、草を生やすのが楽しいです。 この精進、こどもにも応用できないのかな?と考えたのがきっかけ。 課題 子供(特に長男)が、「今何をやっているか?」を忘れがちで、宿題や準備が進みません。 宿題の途中で何か別のことをしたり、、、、 解決方法 子供用のToDoリストを見える化してみます ポイントで釣ってどうにかならないだろうか、、、、と思っている 仕様 タスクを完了にするとポイントが貯まる 毎日のルーティンタスクを発生させる&おわったことをアーカイブする 使う技術(サービス) Trello タスクをかんばんボードのように使えるサービス 使い方 「やること」「いまやっていること」「おわったこと」の3つのリストを準備 「やること」リストの先頭に、画像とメッセージ表示用のタスクを用意する 使い方(API) Trelloの操作イベントをGoogleAppScript宛に通知する Pixela 草を生やせるサービス 使い方(API) ポイントのデータを貯める 画像を表示する 合計と今日のptを取得 GoogleAppScript doPost タスクの移動先が「おわったこと」リストの場合 Trelloタスクのラベルを外す Pixelaを更新する(ラベルに合わせてポイントを増やす) Pixelaのstatsから合計と今日のptを取得 メッセージ表示用タスクの更新(今日と合計のポイントを設定、画像を更新) タスクボードの初期化 毎日発生させるタスク 「おわったこと」リストをアーカイブ 以下のシリーズものになる予定 草を生やしてこどものモチベーションを上げる【その1】仕様〜設計編 この記事 草を生やしてこどものモチベーションを上げる【その2】実装編 ソースコードを余すことなく。 草を生やしてこどものモチベーションを上げる【その3】テスト編 草を生やしてこどものモチベーションを上げる【おまけ】heroku編 svg画像がGASで扱えなかったのでパワープレイでpngに変換したお話 草を生やしてこどものモチベーションを上げる【その4】運用編 運用は今後なのでかけるかわかりませんのでこの記事は未定かもしれない。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【FlutterFire × StripeAPI】簡易版メルカリのようなCtoCプラットフォームアプリを作ってみた

はじめに はじめまして、ダイゴと申します。 Flutter × Firebase × StripeAPIで、CtoCプラットフォームアプリを作ってみました。 ユーザーの登録 クレジットカード情報の登録・削除 本人確認 決済 しかできない簡易的なアプリですが、 難しいイメージのある「決済機能」をできるだけミニマルに実装してあるので、 個人開発などでサクッと導入してみたい場合などの助けになれば幸いです。 本記事では StripeAPI を FlutterFire アプリに組み込む方法と、決済を成立させるまでの実装方法について解説していきます。 サンプルアプリ ログイン 商品一覧 アカウント     Googleアカウントでサインイン。同時にFirestore、Stripeの必要アカウントも作成。 右下の追加ボタンから商品を出品。購入ボタンで決済。 クレジットカードや、本人確認(被決済のために必要)ができる。 事前準備 Flutterアプリを作成 Firebaseにつなぐ Functionsを使うため、Firebaseのプランをアップグレード 実装の流れ Stripeのプロジェクトを作成 npm パッケージをインストール Customer (支払う側)と ConnectAccount (受け取る側)の作成 Customer のクレジットカードを登録 ConnectAccount の本人確認( Identification ) 決済 1. Stripeのプロジェクトを作成 まずは、Stripeプロジェクトを作成します。 以下のURLから自分のStripeアカウントを作成します。 メールアドレスの認証を終え、サインインができたらStripeのダッシュボード画面が表示されます。 「ビジネスの詳細」は、一通りの実装を終えて本番運用するタイミングで追加しても大丈夫なので、一旦テストモードのまま実装を進めます。 左上の「新規ビジネス」部分をタップし、 ここにサービス名(アプリ名)を追加しておきましょう。 これでStripeの新規プロジェクトが作成できました。 2. Stripe の npm パッケージをインストール Functions で StripeAPI を使えるように設定していきます。 package.jsonがあるディレクトリで、以下のコマンドを実行すると stripe の npm モジュールがインストールされます。 npm install stripe --save # or yarn add stripe 先程作成した Stripe プロジェクトと紐付けるためには、プロジェクトのsecret_keyが必要です。ダッシュボードの「開発者→APIキー」 から、secret_key を表示しコピーしておきます。 Functions 側で、 stripe パッケージの初期化を行います。 第一引数に、先程コピーしたsecret_keyを渡すことで、作成した Stripe プロジェクトに紐付けられます。 尚、今回のサンプルアプリでは、TypeScript で Functions の関数を書いていくので、typescript: trueを指定しておきます。 stripe.ts import * as functions from 'firebase-functions'; // Stripe の SecretKey(sk_〇〇〇〇) を環境変数に保存 const stripe = require('stripe')(functions.config().stripe.secret_key, { typescript: true, }); これで、Functions から StripeAPI を呼び出す準備が整いました。 決済に必要になるStripeリソースはこちらです。 リソース 概要 Customer 顧客。購入者でありお金を支払うユーザー。 Source 支払い方法(クレジットカード等)。customerに紐付けることが出来る。 ConnectAccount お金を受け取ることのできるアカウント。 Individual ConnectAccountの本人確認情報。被決済に必要。 Charge 決済データ。 これらが揃えば、StripeConnectAPIを使った、CtoCの決済が通るようになります。 3. Customer (支払う側)と ConnectAccount (受け取る側)の作成 ここからはサンプルアプリのコードをピックアップしながら解説していきます。 アプリユーザーの新規作成時に、Customer (お金を払う側のアカウント) と ConnectAccount (お金を受け取る側のアカウント) を Stripe 上でも作ります。 今回のサンプルアプリでは、GoogleSignIn を実行し、Firestore 上にデータが無い場合は生成する、という仕組みにしています。 signin_model.dart Future<void> signIn() async { // Google ログイン final credential = await _signInWithGoogle(); // user ドキュメントがあるか確認 final userId = credential.user?.uid; final doc = await FirebaseFirestore.instance.collection('users').doc(userId).get(); // user ドキュメントがない場合は作成 if (!doc.exists) { /// Stripe の customer(お金を払う側のアカウント)を作成 final customerId = await stripeRepo.createCustomer(credential.user?.email); /// Stripe の connectAccount (お金を受け取る側のアカウント)を作成(後述) final accountId = await stripeRepo.createConnectAccount(credential.user?.email); // user ドキュメントを作成 await userRepo.createUser( credential.user, customerId, accountId, ); } } Customer (支払う側アカウント)の作成 create メソッドを呼び出すと、Stripe 上でCustomerオブジェクトが生成されます。 オブジェクト内のid(cus_〇〇〇〇)を、サービス側のユーザーデータに保存することで、Stripe 上の Customer とサービス側のユーザーを紐付けることが出来ます。 stripe.ts // MARK: - stripeのcustomerを作ってcustomerIdを返す export const createCustomer = functions.region("asia-northeast1").https.onCall(async (data, context) => { const email = data.email; const customer = await stripe.customers.create( { email: email }, { idempotencyKey: data.idempotencyKey }); const customerId = customer.id; return { customerId: customerId } }); ConnectAccount (送金先アカウント)の作成 ConnectAccount には、 Standard Express Custom の 3 種類が存在します。 Standard → Express → Customの順で、カスタマイズ性が高くなり、よりユーザーに Stripe を意識させずに実装することが可能です。 今回は、一番カスタマイズ性の高いCustomアカウントで実装しました stripe.ts // MARK: - ConnectAccountを作成し、accountIdを返す export const createConnectAccount = functions.region("asia-northeast1").https.onCall(async (data, context) => { return await stripe.accounts.create({ type: 'custom', country: 'JP', email: data.email, business_type: 'individual', capabilities: { card_payments: { requested: true }, // カード決済 transfers: { requested: true }, // 送金 }, individual: { email: data.email, }, settings: { payouts: { schedule: { interval: 'manual', }, }, }, }, { idempotencyKey: data.idempotencyKey, }); }); business_typeは、individualかcompanyから選ぶことができ(non_profit、government_entity は US のみ)、どちらを選ぶかによって認証フローが少し変わってきます。今回は、個人であることを想定してindividualを設定しました。 capabilitiesは、アカウントに持たせる機能を指定するパラメータです。今回は、card_payments(カード決済)とtransfers(送金)を指定しました。 4. Customer のクレジットカードを登録 次に、先程作成した Customer (お金を支払うアカウント)にカード情報をもたせます。 カード情報を安全な形で登録するために、カード情報を Token 化したものを Customer に登録します。 このトークン化の処理はクライアントアプリ側から StripeAPI を直接呼び出すことで、自社のサーバーにはカード情報を送信せず、よりセキュアにカードの登録を行うことができます。 edit_card.dart /// 入力内容からカード情報をトークン化 final stripeAPI = StripeApi(dotenv.env['STRIPE_PK'] as String); final result = await stripeAPI.createToken( { 'card': { 'number': newCreditCard.cardNumber, 'exp_month': expMonth, 'exp_year': expYear, 'cvc': newCreditCard.cvc, }, }, ); final cardToken = result['id']; return cardToken; そして、 Functions 側で token 化したカード情報を customer に紐付けます。 stripe.ts export const createCardInfo = functions .region("asia-northeast1") .https.onCall((data, context) => { const customerId = data.customerId; const cardToken = data.cardToken; return stripe.customers.createSource(customerId, { source: cardToken }); }); 以下の記事で、カード情報をセキュアに管理するための実装方法がより詳しく紹介されています。 5. ConnectAccount の本人確認( Identification ) ConnectAccount を決済可能な状態にするには、 本人確認 利用規約への同意 を行い、charges_enabled(決済を受けれるかどうかのフラグ)を true にする必要があります。 本人確認 ConnectAccount 内の、individual が、本人確認用のパラメータです。 individual 内のパラメータはほぼ全て埋めなければ、決済可能な状態にならないので、アプリ側では以下のようなクラスを作り、account.updateメソッドを呼び出してアカウント情報を更新しています。 stripe_individual.dart /// 本人確認情報 class StripeIndividual { String? firstNameKanji; // 名 String? firstNameKana; // メイ String? lastNameKanji; // 姓 String? lastNameKana; // セイ Dob? dob; // 生年月日 Gender? gender = Gender.none; // 性別 String? phoneNumber; // 電話番号 Address? addressKanji = Address(); // 住所 Address? addressKana = Address(); // カナ住所 StripeVerification? verification; // 本人確認書類(免許証・パスポート等) また、Stripe は WebHook を設定することができ、イベントを検知して自動で関数を走らせることが可能です。 サンプルアプリでは、ConnectAccount の update をトリガーに、Firestore 内の認証ステータスを更新するメソッドを設定しています。 利用規約への同意 利用規約(tos_acceptance)は、アプリ側のIPアドレスと合意した日時(UNIX)を、Functions 側に渡しています。 identification_model.dart // 利用規約 final int date = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000; // 合意した日時(UNIX) final ip = await _getIP(); // IPアドレス tosAcceptance = TosAcceptance(); tosAcceptance?.date = date; tosAcceptance?.ip = ip; 前述の本人情報(individual)と、利用規約(tos_acceptance)をアプリ側から受け取り、accounts.update メソッドでアカウント情報を更新します。 stripe.ts // MARK: - ConnectAccountを更新する export const updateConnectAccount = functions.region("asia-northeast1").https.onCall(async (data, context) => { const result = await stripe.accounts.update( data.accountId, { individual: data.individual, tos_acceptance: data.tos_acceptance, }, { idempotencyKey: data.idempotencyKey } ); return result; }); 2つが正しく入力されていることが確認されたときに、charges_enabledが true になり、決済を受けることができるようになります。 6. 決済 カード情報を登録した Customerと、本人確認を終えた ConnectAccountが用意できたら、いよいよ本題である決済(Charge)を作成していきます。 Stripe には、3 種類の決済種別があり、どれを使うかによって、サービスの資金フローが大きく変わります。 ① Direct charge 「顧客と子アカウント間で直接決済を行い、決済額の一部をプラットフォームアカウントへ送金する」 という決済方式です。 この場合、子アカウントが決済に対しての責任を持つことになるため、決済手数料の負担や返金対応は子アカウント自身が行います。 ② Destination charge 「プラットフォームに支払いを作成すると同時に、送金先アカウント(単一)へいくら配分するかを決める」 という決済方式です。 この場合、プラットフォーム側が決済に対しての責任を持つため、決済手数料の負担や返金対応はプラットフォームが行います。 ③ Separate Charges and Transfers その名の通り、「決済と送金を分けて行う」 決済方式です。 プラットフォームに対して決済が行われ、プラットフォームの任意のタイミングで、任意のアカウント(複数可)に送金する、という資金フローを実現できます。 この場合も Destination charge と同様に、プラットフォーム側が決済に対しての責任を持つため、決済手数料の負担や返金対応はプラットフォームが行います。 今回は、 支払い総額の 10%をプラットフォーム手数料として徴収 残った 90%を ConnectAccount へ送金 というフローにしたかったので、② Destination chargeの形式で決済を作成します。 stripe.ts // MARK: - 決済を作成する export const createStripeCharge = functions.region("asia-northeast1").https.onCall(async (data, context) => { const customer = data.customerId; // 支払側(customer)のID const amount = data.amount; // 支払い総額 const feeAmount = Math.floor(amount * 1 / 10); // 10%の手数料を引いた値を小数点以下で切り捨てる const targetAccountId = data.targetAccountId; // 送金先のアカウント // 決済を作成 const charge = await stripe.charges.create({ customer: customer, amount: amount, currency: "jpy", application_fee_amount: feeAmount, // プラットフォーム手数料 transfer_data: { destination: targetAccountId, }, }, { idempotencyKey: data.idempotencyKey, },); console.log('charges %j', charge); return {chargeId: charge.id}; }); 第 2 引数として渡しているidempotencyKeyは、べき等性を担保するためのパラメータです。 べき等とは、ある操作を 1 回行っても複数回行っても結果が同じであることをいう概念です。つまりidempotencyKeyをつければ、その key がついたリクエストを複数行ったとしても1回だけの処理にしてくれるということです。 すべての stripe のメソッドに対して、追加の引数として渡すとべき等にしてるので、特に決済などの重複してはいけない処理には必須となるパラメータです。 Stripe のダッシュボードで支払いが確認できました。 さいごに 今回は、FlutterFire × StripeAPI を使ったプラットフォーム型のサンプルアプリの実装を紹介しました。 私は、昨年からプログラミングを始めた身なので、最初は「決済機能?絶対むずいやん」と思っていましたが、やってみると「案外不可能でもないな」と思えるようになりました。 実際のサービスとしてリリースするためには、 領収書の発行 銀行口座の登録 残高の表示、振り込み など、まだまだ色々なステップが必要ですが、この記事が決済機能実装のきっかけになれば幸いです。 最後まで読んでいただき、ありがとうございました。 参考 お世話になっているコミュニティ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactのコンポーネントを生成するCLIアプリをNode.jsの標準機能のみで作ってみた話

この記事はTDU CPSLab Advent Calendar 2021 - Adventarの1日目の記事です 前の記事はありません、次の記事はこちらから。 追記 研究室で共有したところ、 Typescriptでのリファクタリングをnaruminchoが早速してくれました!!! シンプルに自分が知らなかったNode.jsの機能使っていたり、 Typescriptの型定義の工夫も学べるかと思いますので、 是非こちらから御一読ください!! はじめに amplify codegen (生成されるファイル達) かっこいい〜!!! codegenってお前。。genってめっちゃかっこいいじゃん。。 generateだよね?それをgenってなんか良くない? 俺もgenしたいな!! なんかこうバシッとgenしたいな!! ...という良くわかんない動機があって、 普段私が手動で作っていたReactコンポーネントをgenするCLI(脳筋)を作るに至り、 そのご紹介をしたいと思います。 以下、回避運動ですが。 筆者は(ガチで)雰囲気でプログラミングしているので書き方がダサい・デフォルトでそういう機能がある・実現できる便利なパッケージがあるなどが起きていましたら申し訳ありません。追記しますので、コメント欄でボソッと教えてくださると嬉しいです。...記事は作ってみた記事的に供養として残させていただけたら嬉しいです。 目次 開発環境 どんなことができるの? 実装内容 入力受け取り処理の作成 コード生成処理の作成 package.jsonに登録 開発環境 実現目標的に筆者の土台は create-react-appで生成されたプロジェクト 及びそれを駆動させているNode.js環境 yarnを使っている という感じですが。 もちろん、Node.jsが入っていればそれだけで大丈夫です。 どんなことができるの? 用意されているコマンドと、その挙動の対応は以下の通りです。 yarn page:codegen      → pageフォルダ配下にコンポーネントを生成する yarn layout:codegen  → layoutフォルダ配下にコンポーネントを生成する yarn comp:codegen      → componentフォルダ配下にコンポーネントを生成する そもそもの前提としまして、私は再利用可能なコンポーネントが集まるcomponentフォルダ・ページ毎のUIを整形するlayoutフォルダ・APIとの通信を行うpageフォルダといった具合に階層構造で分けております。なので上記のような3つのコマンドが定義されております。 続いて、以下の質問を答えることでコードを生成します。 yarn comp:codegen yarn run v1.22.11 $ node ./scripts/comp-codegen.mjs Code-gen : Create Presentational Component ? What file path ex) common/Button > common/Button ? What class name ex) Button > Button ? Create story(Y/n) > Y { filepath: 'common/Button', classname: 'Button', story: true } Start Code Generate... Succeded!! ✨ Done in 11.76s. 生成されるコードはStorybookの定義ファイルとReactコンポーネントのファイルです。 上記の例ではcomponents/common/Buttonフォルダ内に生成されます。 実装内容 大きな流れは以下の通りです。 ユーザの入力を対話的に受け取る処理の作成 ファイル生成を行う処理の作成 package.jsonに登録 特筆したいところは、何か便利なフレームワークやパッケージの導入はせずに、Node.jsの標準的な機能のみで作成したいという欲求がありました。そこで参考記事を探していたところ、napoporitataso様の個人ブログの記事に出会うことができ、参考にさせていただきました。本当にありがとうございます! (自分初めて記事投稿するのでよくわかってないのですが、こういうのってご本人様に連絡差し上げたほうが良いのですかね?) 入力受け取り処理の作成 コードは以下のような感じ。 cli.mjs /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import * as readline from 'readline'; import * as process from 'process'; const reader = readline.createInterface({ input: process.stdin, output: process.stdout, }); // ④で使用 const getAnswer = (msg) => new Promise((res) => reader.question('? ' + msg + ' > ', res)); // ① 複数の質問を引数に取る export const cli = async (questions = []) => { const answers = {}; let answer, question; let name, type, message; // ② 各質問の構成要素 let result; // ③ それぞれ質問をしていく for (question of questions) { result = ''; ({ name, type, message } = question); // ④ ユーザの入力を待ち受ける result = await getAnswer(message); // ⑤ typeに合わせて簡単に値のチェックを行う switch (type) { case 'text': if (result !== '') { answer = result; } else { console.log('Invalid value'); } break; case 'number': if (result !== '' && parseInt(result)) { answer = parseInt(result); } else { console.log('Invalid value'); } break; case 'boolean': if (result.toLowerCase() === 'y' || result.toLowerCase() === 'n') { switch (result.toLowerCase()) { case 'y': answer = true; break; case 'n': answer = false; break; } } else { console.log('Invalid value'); } break; } // ⑥ answersに追加していく answers[name] = answer; } // ⑦ 返す return answers; }; ざっくり解説 ① 複数の質問を受け取り、その答えを返却する関数、cliをエクスポートしてます。 ② それぞれの質問は name,type,message の三要素の存在を保証します。例えばコンポーネント作成の際は以下のようなquestionsが入ってきます。 const questions = [ { type: 'text', name: 'filepath', message: 'What file path ex) common/Button', }, { type: 'text', name: 'classname', message: 'What class name ex) Button', }, { type: 'boolean', name: 'story', message: 'Create story(Y/n)', }, ]; ③ 上記のquestions配列をforで回して聞いていきます。 ④ getAnswer関数でユーザからの入力を待ち受けます。こちらの関数のほうはめちゃくちゃ参考記事様の方を参考させていただきました。 ⑤ questionsのtypeを参照して、簡単にですが値のチェックをしています。numberは使ってないから要らないんですけどもね。。。 ⑥ 値チェックが通過したらanswersに格納されていきます。 ⑦ 全ての質問が終わったらanswersを返して終了です。 ファイル生成処理の作成 コードは以下のような感じ。コンポーネントを作る時のスクリプトを代表して書きます。 comp-codegen.mjs import * as fs from 'fs'; import * as process from 'process'; import path, { dirname } from 'path'; import { fileURLToPath } from 'url'; import { cli } from './cli.mjs'; const questions = /*上で書いたやつと同じなので省略*/; const __dirname = dirname(fileURLToPath(import.meta.url)); const main = async () => { console.log('Code-gen : Create Presentational Component'); // ① 対話処理を使用 const answers = await cli(questions); console.log(answers); console.log('Start Code Generate...'); const from_dir = path.join(__dirname, '../scripts/template/component/'); const to_dir = path.join( __dirname, '../src/components/' + answers['filepath'] + '/' ); // ② フォルダ生成 fs.mkdirSync(to_dir, { recursive: true }); // ③ テンプレート生成(脳筋ポイント) let readcomp = fs.readFileSync(from_dir + 'index.tsx', 'utf-8'); readcomp = readcomp.replace(/COMPONENTTEMPLATE/g, answers['classname']); fs.writeFileSync(to_dir + 'index.tsx', readcomp); // ④ ストーリーテンプレート生成(脳筋ポイント2) if (answers['story']) { let readstory = fs.readFileSync( from_dir + 'componenttemplate.stories.tsx', 'utf-8' ); readstory = readstory.replace(/COMPONENTFILEPATH/g, answers['filepath']); readstory = readstory.replace(/COMPONENTTEMPLATE/g, answers['classname']); fs.writeFileSync( to_dir + answers['classname'].toLowerCase() + '.stories.tsx', readstory ); } console.log('Succeded!!'); process.exit(); }; (async () => { await main(); })(); ざっくり解説 ① 先ほど紹介した対話的にユーザの入力を取得する関数を使っています。 ② 質問で取得した「ファイルパス」の情報に従ってフォルダを作成しています。このmkdirSyncですが、Syncじゃない方も存在します。 ③ 作成したフォルダの中にReactコンポーネントを記述するindex.tsxを生成しています。そしてここからが脳筋(だと思っている)ポイントです。やっていることは以下の通りです。 予め../scripts/template/componentに用意されたindex.tsxの記述内容を読み込む。 正規表現を使ってCOMPONENTTEMPLATEという文字列を質問で取得した「クラス名」で置き換える。 書き出す つまり、テンプレート用のファイルが用意されており、それを複製しているような感じなんです。手動コピペを機械的にコピペできるようにしただけ..。もっといい感じの方法あるかなぁという気持ちがあります。テンプレートがどんな感じなのかは折り畳みで書いておきます。 index.tsxのテンプレート index.tsx import React from 'react'; export const COMPONENTTEMPLATE = (): JSX.Element => { return <div></div>; }; ④ 作成したフォルダの中にStorybookの定義ファイルを生成しています。やってることは③と全く同じです。 Storybookのテンプレート componenttemplate.stories.tsx import React from 'react'; // also exported from '@storybook/react' if you can deal with breaking changes in 6.1 import { Story, Meta } from '@storybook/react/types-6-0'; import { COMPONENTTEMPLATE } from '.'; export default { title: 'component/COMPONENTFILEPATH', component: COMPONENTTEMPLATE, argTypes: { backgroundColor: { control: 'color' }, }, } as Meta; const Template: Story = (args) => <COMPONENTTEMPLATE {...args} />; export const Default = Template.bind({}); Default.args = {}; package.jsonに登録 あとはpackage.jsonのscriptsの中で、以下のように追記すればOKです! package.json { "scripts": { ~省略~, "page:codegen": "node ./scripts/page-codegen.mjs", "layout:codegen": "node ./scripts/layout-codegen.mjs", "comp:codegen": "node ./scripts/comp-codegen.mjs" }, } そんなこんなで いつも手動でファイル作っていたのですが、cliで作れるようになりました。 まとめ 今回はよくわかんないモチベーションから非常に泥臭い感じの自己満なcliが完成しました。 特に予め用意したファイル読み込んで書き出す処理とか、なんかもっといいのないかなって思っております。 まあでも、こういうちょっとした自動化するのはなんか楽しいです。楽しいのでヨシ! 補足ですが、簡単にcliの作り方を調べた際に、もっとカラフルであったりアニメーションを付けたりできる高機能なcliアプリを作るためのフレームワークやライブラリがありました。npmに登録してってやる方はそういったもの使われた方が良い感じかと思います。 初めての記事作成で、お目汚し失礼しました。 記事書くの楽しかったのでまた書きたいと思います。 参考記事 Node.jsの標準機能で超単純な対話的CLIを作るサンプル Node.jsでのCLIの作り方と便利なライブラリまとめ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最もLGTMを獲得したのはあの記事!プロトアウトスタジオ2021年のアウトプットを振り返る

今年も始まりました!アドベントカレンダー! この記事はプロトアウトスタジオのカレンダー | Advent Calendar 2021 - Qiitaの1日目の記事です。 みなさんこんにちは。プロトアウトスタジオ講師の光岡(@mitsuoka0423)です。 今年も残すところ1ヶ月ですね。今年はどのような1年でしたでしょうか?? 毎年恒例のアドベントカレンダーもスタートしました。プロトアウトスタジオも毎日投稿していきますので、ぜひご覧ください。 プロトアウトスタジオのQiita記事が1000件に到達しました! 2021年もプロトアウトスタジオ学生によるたくさんのアウトプットが行われ、Qiita記事の投稿数が1000件に到達しました!祝 (10000LGTMも目の前ですね!) 本記事では、Qiita APIを利用して、2021年に作成されたQiita記事の中からLGTMを多く獲得した記事をピックアップして紹介していこうと思います。 2021年最もLGTMを獲得したのは... 1位: 227LGTM 疑似彼氏、作りました。 @mnana 1位は227LGTMを獲得した「疑似彼氏、作りました。」でした!888888888 "彼氏を作ろう"という見出しから始まり、"タケシ(仮)"というポケモンのジムリーダーを連想させる名前の彼氏が出来上がる様子が見事に描かれています。 またネタ記事と見せかけて、いろんな返信パターンを持っていたりや天気APIのデータから傘が必要か教えてくれるなど、細かいところまで実装されている点も評価につながったのではないでしょうか。 2位: 142LGTM たかしくんに時速10kmで日本全国を最短距離で走らせました @canonno 2位は142LGTMで「たかしくんに時速10kmで日本全国を最短距離で走らせました」でした。 数年後の中学校くらいの教科書に載ってきそうな内容ですね。 こちらもネタ記事と見せかけて、離散最適化問題を解くアルゴリズムが実装されています。 たかしくんは18日と4時間で日本を全国を走り切ることができるそうです。以外と早いですね。 3位: 138LGTM 「ひらがな化API」を使って、給食メニュー変換Botを作ったら、わが子に大好評だった話 @kokano23 3位は138LGTMで「「ひらがな化API」を使って、給食メニュー変換Botを作ったら、わが子に大好評だった話」でした。 ひらがなが読めるようになったお子さんに向けてLINE Botを作成するファミリーテックな記事です。 リッチメニューにポケモンのイラストを使うなど、お子さんが使いたくなる工夫も施されています。 また、@kokano23さんはのびすけラジオにも登場されているので、興味がある方はぜひお聞きください。 事務職勤めのワーママがテクノロジーを身に付けたら仕事でもプライベートでも驚きの変化が #身の丈DX #事務職DX 4位〜10位 4位〜10位は以下の記事でした。 4位: 90LGTM @n0bisuke 【初見にオススメ】Raspberry Pi PicoをブラウザだけでLチカする入門 (Web Serial API) 5位: 90LGTM @shoito66 AIは増えすぎたガンダムを見分けることができるのか?Teachable Machineで作った分類モデルで検証してみた 6位: 68LGTM @canonno 強化学習で酔っ払いの挙動を見る 7位: 54LGTM @naokiuc 顔からラグビー部かサッカー部かを判断する 8位: 43LGTM @tkyko13 YouTubeの動画をQiita記事に埋め込んでみた 9位: 40LGTM @mitsuoka0423 【ハンズオン】写真を送るとAIが分析してくれるLINE Botを1時間で作ってみよう 10位: 37LGTM @chihirokubota これで若い子とのカラオケも安心?!COBOLしか経験なし!文系出身SE(23歳女)が若者の音楽を伝授します! 11位以降の記事はこちらから確認できます。→プロトアウトスタジオ2021アウトプット集計 Qiita APIを使って記事を取得する 以降は技術な解説です。長いので流し読み推奨です。 さっと読んで、他のアドベントカレンダーの記事を見にいきましょう。 Qiita API v2を利用してデータを取得しています。 取得対象 取得対象は以下のとおりとします。 ①プロトアウトスタジオOrganizationに属しているメンバー ②2021/01/01〜2021/12/01の期間に作成された記事 ②の条件は、検索クエリでcreated:>2021-01-01を指定すれば良さそうです。 しかし、検索時に利用できるオプション - Qiita:Supportを見てみても、①を絞り込む方法は今のところ公式に提供されていないようです。 プロトアウトスタジオOrganizationを眺めていると、メンバーアイコンのリンクからQiitaのユーザーIDが取得できそうです。 今回は、Organizationに属する全メンバーのユーザーIDを取得して、それぞれのユーザーが投稿した記事の情報からLGTM数を取得していきます。 DevToolsを使って、OrganizationメンバーのユーザーIDを取得する 今回は手軽に、DevToolsを活用してユーザーIDを取得します。 まずは取得したい要素を特定できる属性を調べます。 メンバーアイコンは、class="op-Members_member"を持つliタグの要素として実装されているようです。 ユーザーIDは、liタグの子要素のaタグのhref属性に含まれているので、それを取得していきます。 DevToolsのコンソールタブで、以下のコードを実行します。 Array.from(document.getElementsByClassName('op-Members_member')).forEach(element => console.log(element.children[0].href.split('/')[3])); これで、プロトアウトスタジオメンバーのユーザーIDを取得できました。 Qiita APIを叩くプログラムを書く axiosを使ってQiita APIを叩くプログラムを書いていきます。 クリックしてプログラム全量を表示 'use strict'; const axios = require('axios'); const main = async () => { const users = [ 'cog1t0', 'n0bisuke', 'tseigo', 'tkyko13', 'mitsuoka0423', 'ukkz', 'shima-07', 'takeaship', 'UhRhythm', 'RatchoTetsugaku', 'kmaepu', 'Toshiki0324', 'banboo', 'sksk_go', 'MikH', 'suo-takefumi', '3yaka4', 'tatsuya1970', 'zerozeronineking', 'grayhamchan', 'iizuka2019', 'karu', 'mihoko-funatsu', 'taichi0128', 'dashgo5go', 'doikatsuyuki', 'takeatakea', '13sayu', 'marumaruchan', 'harach19k', 'kyanchi', 'khoahv', 'Teru_3', 'hiromae0213', 'cazzz', 'Naru0607', 'kaiser355', 'canonno', 'twtjudy1128', 'PmanRabbit', 'heihei15408697', 'Sugizo50073508', 'm3do', 'sawakoshi_yy', 'misa_m', 'ranchi1977', 'shoito66', 'kkyosuke17', 'kokano23', 'ShinsukeSutou', 'yuta-proto-biz', 'boriko', 'okinakamasayoshi', 'dsvvpxgcseldie', 'aya2648', 'Ziyasumin01', 'Junno', 'yudiramaruyama', 'kensyu20210726', 'komona', 'tanaka_LV5', 'NagaharaHitomi', 'tanakahiroki', 'naokiuc', 'Izumi0711', 'watanabe-tsubasa', 'TAKA_xedge', 'kk_puruzera', 'yui-kouy', 'mnana', 'ikumi623', 'chihirokubota', 'tishiyama', ]; for (const user of users) { const response = await axios.get( `https://qiita.com/api/v2/items?query=created:>2021-01-01+user:${user}&per_page=100`, { headers: { Authorization: 'Bearer [アクセストークン]', }, } ); response.data.forEach((content) => console.log( `${content.created_at},${content.user.id},${content.title},${content.likes_count},${response.data.length}` ) ); } }; main(); (オプション)StackBlitzを使って実行する 今回、オンラインIDEであるStackBlitzを利用して実行してみました。(ローカルで実行しても全然問題ありません。) 無料プランでもコードを書くだけでなく、ターミナルでコードを実行できました。 Pricingを見ても、現時点(2021/12/01)では実行時間の上限もなさそうです。 (これはgitpodの代替サービスとなりうるかも。引き続き調査します。) 結果は以下のように出力されます。 CSV形式で出力されるので、Googleスプレッドシートに読み込んでLGTM数でソートしてランキングを作成しました。 まとめ 2021年もたくさんのアイデアが形になり、Qiita記事として投稿されました。 疑似彼氏のような自分が作りたいから作ったというタイプのアウトプットや、ひらがな変換Botのように誰かのために作ったタイプのアウトプットのどちらもQiita上でLGTMを集められているというのは面白いですね。 残り1ヶ月で、疑似彼氏を超えるLGTMを獲得する記事は生まれてくるのでしょうか...? 12/25までアドベントカレンダーは続きますので、今後の記事もぜひご覧ください! 明日の担当は @suo-takefumi さんです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む