- 投稿日:2020-08-18T23:51:43+09:00
【第2回】「みんなのポートフォリオまとめサイト」を作ります~REST API編~ 【Cover】成果物URL: https://minna.itsumen.com
ワイ 「この記事のカバーです」
https://qiita.com/kiwatchi1991/items/58b53c5b4ddf8d6a7053バックナンバー
【第1回】「みんなのポートフォリオまとめサイト」を作ります~プロトタイプ作成編~
成果物
リポジトリ
フロントエンド
https://github.com/yuzuru2/minna_frontend
バックエンド
https://github.com/yuzuru2/minna_backend
コレクション定義(テーブル定義)
ワイ 「今回はNoSQLのMongoDBを使ってます」
ワイ 「コレクションとはRDBでいうテーブル的なやつです」
RDB MongoDB スキーマ データベース テーブル コレクション カラム フィールド レコード ドキュメント ①Users(ユーザ)
uid: unique
物理名 論理名 型 uid ユーザID string name 名前 string twitterUrl TwitterのURL string githubUrl GitHubのURL string createdAt 作成時間 Date updatedAt 更新時間 Date src/mongoose/collection/users.tsimport * as mongoose from 'mongoose'; import Schema from 'src/mongoose'; const model_name = 'users'; interface _interface { uid: string; name: string; twitterUrl: string; githubUrl: string; createdAt: Date; updatedAt: Date; } interface i_model extends mongoose.Document {} interface i_model extends _interface {} const model = mongoose.model( model_name, new Schema({ uid: { type: String }, name: { type: String, minlength: 1, maxlength: 15 }, twitterUrl: { type: String }, githubUrl: { type: String }, createdAt: { type: Date }, updatedAt: { type: Date }, }).index({ uid: 1 }, { unique: true }) ); // 作成 export const create = async (params: Pick<i_model, 'uid'>) => { const _data: _interface = { uid: params.uid, name: '名無し', twitterUrl: '', githubUrl: '', createdAt: new Date(), updatedAt: new Date(), }; return (await model.insertMany([_data])) as i_model[]; }; // 抽出 export const find = async (params: Pick<i_model, 'uid'>) => { const _data: Pick<i_model, 'uid'> = { uid: params.uid }; return (await model.find(_data)) as i_model[]; }; // 更新 export const update = async ( uid: string, params: Pick<i_model, 'name' | 'twitterUrl' | 'githubUrl'> ) => { return await model.updateOne( { uid: uid }, { $set: { ...params, updatedAt: new Date() } } ); };②Products(ポートフォリオ)
_id: unique
物理名 論理名 型 _id ポートフォリオのID string uid ユーザID string type ポートフォリオのタイプ number title ポートフォリオのタイトル string url ポートフォリオのURL string repo リポジトリのURL string createdAt 作成時間 Date updatedAt 更新時間 Date src/mongoose/collection/products.tsimport * as mongoose from 'mongoose'; import Schema from 'src/mongoose'; const model_name = 'products'; const pagingNum = 5; interface _interface { uid: string; type: number; title: string; url: string; repo: string; createdAt: Date; updatedAt: Date; } interface i_model extends mongoose.Document {} interface i_model extends _interface {} const model = mongoose.model( model_name, new Schema({ uid: { type: String }, type: { type: Number, min: 0, max: 5 }, title: { type: String, minlength: 1, maxlength: 30 }, url: { type: String, minlength: 1, maxlength: 100 }, repo: { type: String, maxlength: 100 }, createdAt: { type: Date }, updatedAt: { type: Date }, }) ); // 作成 export const create = async ( params: Pick<i_model, 'uid' | 'type' | 'title' | 'url' | 'repo'> ) => { const _data: _interface = { uid: params.uid, type: params.type, title: params.title, url: params.url, repo: params.repo, createdAt: new Date(), updatedAt: new Date(), }; return (await model.insertMany([_data])) as i_model[]; }; // 更新 export const update = async ( id: string, uid: string, params: Pick<i_model, 'type' | 'title' | 'url' | 'repo'> ) => { return await model.updateOne( { _id: id, uid: uid }, { $set: { ...params, updatedAt: new Date() } } ); }; // 削除 export const deleteProduct = async (id: string, uid: string) => { return await model.deleteOne({ _id: id, uid: uid }); }; // 全投稿数 export const countAll = async () => { return model.find({}).countDocuments(); }; // ジャンル別投稿数 export const countType = async (type: number) => { return model.find({ type: type }).countDocuments(); }; // タイトル別投稿数 export const countTitle = async (title: string) => { return model.find({ title: { $regex: title } }).countDocuments(); }; // ユーザ別投稿数 export const countUser = async (uid: string) => { return model.find({ uid: uid }).countDocuments(); }; // ページング全投稿 export const pagingAll = async (num: number) => { return await model.aggregate([ { $match: {}, }, { $lookup: { from: 'users', localField: 'uid', foreignField: 'uid', as: 'users_info', }, }, { $sort: { createdAt: -1 }, }, { $skip: num * pagingNum, }, { $limit: pagingNum }, { $project: { _id: '$_id', type: '$type', title: '$title', url: '$url', repo: '$repo', name: '$users_info.name', uid: '$uid', createdAt: '$createdAt', updatedAt: '$updatedAt', }, }, ]); }; // ページングタイプ別 export const pagingType = async (num: number, type: number) => { return await model.aggregate([ { $match: { type: type, }, }, { $lookup: { from: 'users', localField: 'uid', foreignField: 'uid', as: 'users_info', }, }, { $sort: { createdAt: -1 }, }, { $skip: num * pagingNum, }, { $limit: pagingNum }, { $project: { _id: '$_id', type: '$type', title: '$title', url: '$url', repo: '$repo', name: '$users_info.name', uid: '$uid', createdAt: '$createdAt', updatedAt: '$updatedAt', }, }, ]); }; // ページングタイトル別 export const pagingTitle = async (num: number, title: string) => { return await model.aggregate([ { $match: { title: { $regex: title }, }, }, { $lookup: { from: 'users', localField: 'uid', foreignField: 'uid', as: 'users_info', }, }, { $sort: { createdAt: -1 }, }, { $skip: num * pagingNum, }, { $limit: pagingNum }, { $project: { _id: '$_id', type: '$type', title: '$title', url: '$url', repo: '$repo', name: '$users_info.name', uid: '$uid', createdAt: '$createdAt', updatedAt: '$updatedAt', }, }, ]); }; // ページングユーザ別 export const pagingUser = async (num: number, uid: string) => { return await model.aggregate([ { $match: { uid: uid, }, }, { $lookup: { from: 'users', localField: 'uid', foreignField: 'uid', as: 'users_info', }, }, { $sort: { createdAt: -1 }, }, { $skip: num * pagingNum, }, { $limit: pagingNum }, { $project: { _id: '$_id', type: '$type', title: '$title', url: '$url', repo: '$repo', name: '$users_info.name', uid: '$uid', createdAt: '$createdAt', updatedAt: '$updatedAt', }, }, ]); };REST API
ユーザ作成・ログイン
リクエストURL
Post /v1/create/userリクエストヘッダー
Authorization: Firebase Authorizationで発行されたjwttoken Content-Type: application/jsonリクエストパラメーター
{}レスポンス
{}
ポートフォリオ投稿
リクエストURL
Post /v1/create/productリクエストヘッダー
Authorization: Firebase Authorizationで発行されたjwttoken Content-Type: application/jsonリクエストパラメーター
{ // ポートフォリオのタイトル title: string; // ポートフォリオのURL url: string; // ポートフォリオのリポジトリURL repo: string; // 0: Webアプリ // 1: スマホアプリ // 2: デスクトップアプリ // 3: スクレイピング // 4: ホムペ // 5: その他 type: number; }レスポンス
{}
ユーザプロフィール更新
リクエストURL
Put /v1/update/userリクエストヘッダー
Authorization: Firebase Authorizationで発行されたjwttoken Content-Type: application/jsonリクエストパラメーター
{ // ユーザ名 name: string; // GitHubのURL githubUrl: string; // TwitterのURL twitterUrl: string; }レスポンス
{}
ポートフォリオ更新
リクエストURL
Put /v1/update/productリクエストヘッダー
Authorization: Firebase Authorizationで発行されたjwttoken Content-Type: application/jsonリクエストパラメーター
{ // ポートフォリオのID id: string; // ポートフォリオのタイトル title: string; // ポートフォリオのURL url: string; // ポートフォリオのリポジトリURL repo: string; // 0: Webアプリ // 1: スマホアプリ // 2: デスクトップアプリ // 3: スクレイピング // 4: ホムペ // 5: その他 type: number; }レスポンス
{}
ポートフォリオ削除
リクエストURL
Delete /v1/cancel/productリクエストヘッダー
Authorization: Firebase Authorizationで発行されたjwttoken Content-Type: application/jsonリクエストパラメーター
{ // ポートフォリオのID id: string; }レスポンス
{}
ユーザプロフィール参照
リクエストURL
Get /v1/find/user/:uidリクエストヘッダー
Authorization: null以外の値リクエストパラメーター
{ // ユーザID uid: string; }レスポンス
{ // ユーザ名 name: string; // TwitterのURL twitterUrl: string; // GitHubのURL githubUrl: string; }
ページング全投稿(5件)
リクエストURL
Get /v1/paging/all/:numリクエストヘッダー
Authorization: null以外の値リクエストパラメーター
{ // 1ページ目は1, 2ページ目は2.... num: string; }レスポンス
{ // 件数 count: number; list: { // ポートフォリオのID _id: string; // 0: Webアプリ // 1: スマホアプリ // 2: デスクトップアプリ // 3: スクレイピング // 4: ホムペ // 5: その他 type: number; // ポートフォリオのタイトル title: string; // ポートフォリオのURL url: string; // ポートフォリオのリポジトリURL repo: string; // 投稿者名 name: string[]; // ユーザID uid: string; // 作成日 createdAt: Date; // 更新日 updatedAt: Date; }[]; }
ページングポートフォリオのタイトル別(5件)
リクエストURL
Get /v1/paging/title/:title/:numリクエストヘッダー
Authorization: null以外の値リクエストパラメーター
{ // 1ページ目は1, 2ページ目は2.... num: string; title: string; }レスポンス
{ // 件数 count: number; list: { // ポートフォリオのID _id: string; // 0: Webアプリ // 1: スマホアプリ // 2: デスクトップアプリ // 3: スクレイピング // 4: ホムペ // 5: その他 type: number; // ポートフォリオのタイトル title: string; // ポートフォリオのURL url: string; // ポートフォリオのリポジトリURL repo: string; // 投稿者名 name: string[]; // ユーザID uid: string; // 作成日 createdAt: Date; // 更新日 updatedAt: Date; }[]; }
ページングタイプ別(5件)
リクエストURL
Get /v1/paging/type/:type/:numリクエストヘッダー
Authorization: null以外の値リクエストパラメーター
{ // 1ページ目は1, 2ページ目は2.... num: string; // 0: Webアプリ // 1: スマホアプリ // 2: デスクトップアプリ // 3: スクレイピング // 4: ホムペ // 5: その他 type: string; }レスポンス
{ // 件数 count: number; list: { // ポートフォリオのID _id: string; // 0: Webアプリ // 1: スマホアプリ // 2: デスクトップアプリ // 3: スクレイピング // 4: ホムペ // 5: その他 type: number; // ポートフォリオのタイトル title: string; // ポートフォリオのURL url: string; // ポートフォリオのリポジトリURL repo: string; // 投稿者名 name: string[]; // ユーザID uid: string; // 作成日 createdAt: Date; // 更新日 updatedAt: Date; }[]; }
ページングユーザ投稿別
リクエストURL
Get /v1/paging/user/:uid/:numリクエストヘッダー
Authorization: null以外の値リクエストパラメーター
{ // 1ページ目は1, 2ページ目は2.... num: string; uid: string; }レスポンス
{ // 件数 count: number; list: { // ポートフォリオのID _id: string; // 0: Webアプリ // 1: スマホアプリ // 2: デスクトップアプリ // 3: スクレイピング // 4: ホムペ // 5: その他 type: number; // ポートフォリオのタイトル title: string; // ポートフォリオのURL url: string; // ポートフォリオのリポジトリURL repo: string; // 投稿者名 name: string[]; // ユーザID uid: string; // 作成日 createdAt: Date; // 更新日 updatedAt: Date; }[]; }
src/route/index.tsimport * as Express from 'express'; import * as Cors from 'cors'; import * as DotEnv from 'dotenv'; import Constant from 'src/constant'; // route--- import create_friend from 'src/route/create/friend'; import create_user from 'src/route/create/user'; import create_product from 'src/route/create/product'; import paging_all from 'src/route/paging/all'; import paging_title from 'src/route/paging/title'; import paging_type from 'src/route/paging/type'; import paging_user from 'src/route/paging/users'; import update_product from 'src/route/update/product'; import update_user from 'src/route/update/user'; import cancel_friend from 'src/route/cancel/friend'; import cancel_product from 'src/route/cancel/product'; import find_user from 'src/route/find/user'; // route--- DotEnv.config(); const app = Express(); const router = Express.Router(); // middleware--- app.use(Cors({ origin: process.env.ORIGIN_URL })); app.use('/.netlify/functions/api', router); app.use(Express.urlencoded({ extended: true })); app.use( (req: Express.Request, res: Express.Response, next: Express.NextFunction) => { req.headers.authorization !== undefined ? next() : res.sendStatus(403); } ); app.use((_, __, res: Express.Response, ___) => { res.sendStatus(500); }); // middleware--- // routing--- // ユーザ作成 router.post(Constant.API_VERSION + Constant.URL['/create/user'], create_user); // 投稿 router.post( Constant.API_VERSION + Constant.URL['/create/product'], create_product ); // フォローする router.post( Constant.API_VERSION + Constant.URL['/create/friend'], create_friend ); // ページング全投稿 router.get( Constant.API_VERSION + Constant.URL['/paging/all'] + '/:num', paging_all ); // ページングタイプ別 router.get( Constant.API_VERSION + Constant.URL['/paging/type'] + '/:type' + '/:num', paging_type ); // ページングタイトル別 router.get( Constant.API_VERSION + Constant.URL['/paging/title'] + '/:title' + '/:num', paging_title ); // ページングユーザ別 router.get( Constant.API_VERSION + Constant.URL['/paging/user'] + '/:uid' + '/:num', paging_user ); // プロフィール router.get( Constant.API_VERSION + Constant.URL['/find/user'] + '/:uid', find_user ); // 更新 ユーザ router.put(Constant.API_VERSION + Constant.URL['/update/user'], update_user); // 更新 記事 router.put( Constant.API_VERSION + Constant.URL['/update/product'], update_product ); // フォローはずす router.delete( Constant.API_VERSION + Constant.URL['/cancel/friend'], cancel_friend ); // 投稿削除 router.delete( Constant.API_VERSION + Constant.URL['/cancel/product'], cancel_product ); // routing--- export default app;
- 投稿日:2020-08-18T21:23:37+09:00
Static Web Appsでは現状TimerTriggerは使えないっぽい
Azure Functions のタイマー トリガーを使ってStatic Web Appsに紐づいたAPIプログラムもタイマー実行できないかと思いましたがどうやらできないっぽいのでメモ。
ビルドでこんなコケかたします。
Error in processing api build artifacts: the file 'myTimer/function.json' has specified an invalid trigger of type 'timerTrigger' and direction 'in'. Currently, only httpTriggers are supported. Visit https://github.com/Azure/azure-functions-host/wiki/function.json for more information.ドキュメントを見てみると
トリガーとバインドは、HTTP に限定されています。
としっかりと書いてましたね......ちゃんとドキュメントみましょうという教訓をメモにしておきます。
- 投稿日:2020-08-18T19:56:09+09:00
SAP Cloud Platform 上で WebSocket を使ってみた
はじめに
この記事は chillSAP 夏の自由研究2020 の記事として執筆しています
今回は、 SAPUI5 の API Reference を眺めていると WebSocket( sap.ui.core.ws.WebSocket )のライブラリを発見したので試してみました。
WebSocket とは?
WebSocket(ウェブソケット)は、コンピュータネットワーク用の通信規格の1つである。ウェブアプリケーションにおいて、双方向通信を実現するための技術規格である。2011年にRFC 6455として>最初の標準仕様が定義された。
難しいのでなるべく簡単に説明すると、
Webの世界でブラウザとサーバが通信をする場合には、ブラウザからの呼びかけに対してサーバが返事する仕組みでしたが
WebSocket を使用すると、サーバからブラウザに対して呼びかけをしてくれる仕組みが作れるということです。今回のブツ
SAP Cloud Platform 上で WebSocketを使用した ブラウザChat を作ってみました。
仕組み
- ブラウザから SAP UI5 で準備したWebチャットを動かすと、 Webチャット は SAP Cloud Platform 上に nodejs で構築したチャットサーバと、 WebSocket で接続します。
- 接続中の Webチャット から発言すると、発言内容は チャットサーバへと送られます。
- チャットサーバは、Webチャットからの発言を受け取ると、接続中のすべてのWebチャット に対して受信した内容を返します。
- Webチャットは、チャットサーバからの通信を受け取ると、画面上へ受信した内容を表示します。
まとめ
送信したメッセージは、ほぼリアルタイムに相手側へ送られているのがご覧いただけると思います。
WebSocketを使わなくても、ポーリングや Comet を使用すれば似たようなことは実装できますが
ほぼリアルタイムの相互通信が簡単に実装出来るので、一度 WebSocket に興味を持っていただければと思います。参考
- class sap.ui.core.ws.WebSocket - SAPUI5 SDK
- WebSocket - Wikipedia
- Simple chat server example using UI5 WebSocket
ここから先はソースコード
チャットサーバ実装
server.js/*eslint no-console: 0*/ "use strict"; var WebSocketServer = require('ws').Server; var ws = new WebSocketServer({ port: process.env.PORT || 8080 }); // 接続時に呼ばれる ws.on('connection', function (socket) { // クライアントからのデータ受信時に呼ばれる socket.on('message', message => { console.log('received: %s', message); // 受け取ったメッセージを接続中のクライアントへ返信 ws.clients.forEach(client => { client.send(message); }); }); socket.send(JSON.stringify({ user: 'chat server', text: 'Hello from Server' })) // 切断時に呼ばれる socket.on('close', function() { console.log('Disconnected from Server'); }); });Webチャット(クライアント)実装
View1.controller.jssap.ui.define([ "sap/ui/core/mvc/Controller", "sap/ui/core/ws/WebSocket" ], function (Controller, WebSocket) { "use strict"; return Controller.extend("cli.client.controller.View1", { oModel: new sap.ui.model.json.JSONModel(), // チャットサーバへデータ送信 notify: function(user, text) { var msg = user + ': ' + text; var msgAreaId = "chatInfo"; // 現在表示中のメッセージを取得 var chatMsg = sap.ui.getCore().byId(msgAreaId).getValue(); // var lastInfo = this.oModel.oData.chat; if (chatMsg.length > 0) { chatMsg += "\r\n"; } chatMsg += msg; sap.ui.getCore().byId(msgAreaId).setValue(chatMsg); }, onAfterRendering: function() { var thisController = this; sap.ui.getCore().byId("userName").setValue("ななし"); var url = "wss://チャットサーバ"; var ws = new sap.ui.core.ws.WebSocket(url); this.ws = ws; //接続時 ws.attachOpen(function (oControlEvent) { thisController.notify('system', 'connection opened...'); }); // エラー時 ws.attachError(function (oControlEvent) { thisController.notify('system', 'connection Error!'); }); //受信時 ws.attachMessage(function (oControlEvent) { var data = jQuery.parseJSON(oControlEvent.getParameter('data')); thisController.notify(data.user, data.text); }); //終了時 ws.attachClose(function (attachClose) { thisController.notify('system', 'Disconnected from Server'); }); }, // 送信ボタン押下 onSendMessage: function (oEvent) { var user = sap.ui.getCore().byId("userName").getValue(); var msg = sap.ui.getCore().byId("chatMsg").getValue(); if (msg.length > 0) { this.ws.send(JSON.stringify({ user: user, text: msg })); sap.ui.getCore().byId("chatMsg").setValue(""); } } }); });
- 投稿日:2020-08-18T17:00:23+09:00
Node.jsⅠ
◆Expressを導入する
$ npm install express →入力してenter
nodejs_lesson@1.0.0/home/..
express@4.17.1 →入力結果◆インストールしたExpressを利用する
const express = require('express') ;
const app = express();◆listenメソッド
app.listen(3000);
『ターミナル』
$ node.app.js →app.jsをターミナルを実行する
ファイルを実行するには「node ファイル名」とします。◆ルーティング
app.get('/top', (req,res) => {
トップ画面を表示する処理
});ルーティングの処理でres.renderと書くことで、指定したビューファイルをブラウザに表示できます。
app.get('/top', (req,res) => {
res.render('top.ejs');
});◆CSSを適用するには(1)
app.use(express.static(public));
今回はpublicというフォルダにこれらのファイルを置く◆JavaScriptを利用しよう
JavaScriptのコードを記述するには、<% %>または<%= %>で囲みます。
<% const item = {id:3, name: 'たまねぎ'};>
id: <%= item.id%>
name: <%=item.name%>
◆オブジェクトの配列を画面に表示しよう
①まずは、リストをまとめて配列を定義します
<%
const items = [
{id:1, name:'じゃがいも'},
{id:2, name:'とまと'},
{id:3, name:'しらす'},
];
%>②次に配列のオブジェクトを一覧画面に表示します。forEachは画面に表示させないので <% %>で記述しましょう。
{ %>
- 投稿日:2020-08-18T16:55:11+09:00
ytdlでYoutubeダウンロード+Macでファイル変換&編集
もう、無限に広告がポップしてくる詐欺ソフトに悩まされることはありません。
我々エンジニアはプログラミングとCUIという頼れる味方がいます。この記事を読むと
- ターミナルから簡単&爆速にYoutubeから動画をダウンロードできます。
- ファイル形式の変換と時間指定の切り抜きのお勧め方法もカバー
環境
- Macbook Pro 2017 13インチ
- MacOS Catalina ヴァージョン10.15.6
やり方比較
没案1.Youtube Premium
最も簡単にYoutubeの動画をオフラインで利用する方法はYoutube Premiumに登録することです。料金も月1000円程度と破格。
しかしなんでYoutube Premiumではダメなのかというと、PCのローカルに落とせないからです。例えば、私の場合は英語音声のみで字幕無しの動画に対して、Amazon Transcribeを利用して文字起こしや翻訳を行うのがそもそもの動機でした。また、スマホアプリ上でしかオフライン利用できません。
やはり普通のファイルとしてローカルに落とすのが一番取り回しが良いです。没案2.非公式のWebアプリ・Webサイト
無限に広告がポップしてくるだけで永遠にダウンロードできません。仮に運よく機能したとしてもダウンロードに膨大な時間がかかります。また、一定時間までのファイルにしか対応していません。
あと、そのようなWebアプリ・Webサイトは存在が違法です。
(後述するやり方で、「個人利用の範囲」かつ「合法的にYoutubeアップロードされた動画」をダウンロードするのは違法ではないという認識です。ただし、youtubeの利用規約には違反する可能性はあります)ytdlがベストアンサー
私がお勧めするのはytdlを利用してターミナルからダウンロードするやり方です。
ytdlはnode.jsのライブラリで、簡単にインストールできます。
ytdlならダウンロードが爆速です。
また、ファイルが長時間でもOK。
そして当然ですが、ターミナルなので一切広告等出てきません。ytdlの使い方
導入も利用も簡単ですが、いくつか注意点があります。一言で言えば、ytdlには複数の機能がありますが、あくまでファイルのダウンロードのみに使用するべきです。
環境構築
お馴染みのnpmコマンドを使って、グローバルにダウンロードします。
npm -g install ytdl動作確認
ytdlのインストールが完了したら、実際にYoutubeからファイルをダウンロードできるか確認してみましょう。
お好きなディレクトリのターミナルから、下記を実行してください。desktop辺りが手頃でしょうか。ytdl "http://www.youtube.com/watch?v=_HSylqgVYQI" > myvideo.mp4ダウンロードできたでしょうか。
コマンドの意味を解説すると、ytdl
の後の""
内でダウンロードしたいYoutube動画のURLを指定して、myvideoという名前でmp4ファイルとしてダウンロードしています。注意点
ffmpegはインストールしない。
公式ページに紹介されているように、
ffmpeg
を併せてインストールするとファイル形式の変換や時間指定による切り抜き等のオプションが利用できるようになり、一見便利そうです。しかし、例えばmp3としてダウンロードすると、一見変換に成功しているのですが、実はファイルが壊れてしまいます。ファイルが壊れると何が問題かと言うと、一部のプラットフォーム上でしかファイルが動作しなくなります。例えばMacで言うと、Quick Time Playerではファイルを再生できるが、iTunesではファイルを認識できないので、再生どころかライブラリへの追加もできません。また、
ffmpeg
のインストール自体も、環境を汚染するのでお勧めしません。ffmpeg
がかなり重い=大量のモジュールとの依存関係がありながら、そのどれもが古いので、メインで使用しているnode.jsのモジュールと競合を引き起こす危険性があります。ファイルダウンロードのみに使用する
ファイル形式の変換も時間指定の切り抜きも、どちらもMacのデフォルトの機能やアプリで簡単に実行可能です。CUIをGUIで、それぞれ適したことに使い分けましょう。詳しくは後述しますが、ファイル形式の変換はFinder、時間指定の切り抜きはiMoviesを使用するのがお勧めです。
お勧め運用方法
ダウンロード
先ほど紹介したコマンドではいちいちファイル名を手動で入力しなくてはいけません。
下記のコマンドでは、ファイルのタイトル - 作者
をファイル名としてダウンロードすることができます。ytdl -o "{author.name} - {title}" "http://www.youtube.com/watch?v=_HSylqgVYQI"あるいは、動画のタイトルだけも良いという場合は下記でも。
ytdl -o "{title}" "http://www.youtube.com/watch?v=_HSylqgVYQI"ファイル形式変換
ここでは音声ファイルに変換してみましょう。
Finderからファイルを選択したら右クリックして、一番下の「選択したビデオファイルをエンコード」をクリックしてください(ちなみに、「選択したビデオファイルをエンコード」は複数ファイルを選択した状態でも可能です)。
「メディアをエンコード」というウィンドウが開くので、
設定:
をオーディオのみ
に変更します。最後に'続ける'を押したら完了です。元のビデオファイルとは別に、音声ファイルが作成されます。
先述した通り、「選択したビデオファイルをエンコード」は複数ファイルを選択した状態でも可能なので、ある程度貯めてからまとめて変換するとラクです。
指定した時間で切り抜き
iMovieを使用します。
「新規作成(大きな「+」アイコン)」をクリックしたら、「ムービー」を選択します。
すると、動画の編集画面が開きます。
画面左上の「ファイル」から「メディアを読み込む」をクリックします。
またウィンドウが開くので、編集したいファイルを選択したら、画面右下の「選択した項目を取り込む」をクリックしてください。
上の画面に追加されたファイルを、下の編集画面にドラッグ&ドロップしてください。
まずはクリップを分割します。
ファイルの適当なところで右クリックして、「クリップを分割」をクリック。
ファイルがクリップに分割されました。
クリップをcommand + Cでコピーしたら、いったんこのプロジェクトを閉じましょう。新しくプロジェクトを作成します。そして新しく作成したプロジェクトの編集画面(下の方)でcommand + Vで貼り付けます。
画面左上の「ファイル」から「共有」をクリックします。ファイルの形式(フォーマット)を任意のものに選択して、「次へ」でファイルを書き出します。
こういう場合は明らかにCUIよりGUIの方が適していますね。
当初はダウンロードする時点で開始地点と終了地点を指定していたのですが、秒数に変換して指定しなくていけないので、かなり面倒でした。計算ミスも頻発しましたし、毎回ダウンロードし直さなくてはいけないのも大変でした。
参考文献
- 投稿日:2020-08-18T14:53:36+09:00
【JS】Node.js と GoogleSpreadsheet で業務効率化
google-spreadsheet
というパッケージを使用し、スプレッドシートへの書き込みの実装をしたので、その備忘録です。
google-spreadsheet - npm下記の記事をみておけばほぼわかります。
【Node.js】 Googleスプレッドシートを簡易データベースとして使う - 一日一膳(当社比)
GoogleスプレッドシートからNode.jsでシフトデータを読み出す方法 - Twilio準備
APIの有効化 & 認証
- GoogleSpreadsheet API 有効化
- サービスアカウントを発行
- スプレッドシートに 作成したサービスアカウントを招待(メールアドレス)
- アプリケーション側で読み込み
インストール
npm i google-spreadsheet実装したもの
const { GoogleSpreadsheet } = require('google-spreadsheet'); const SHEET_ID = 3333053444; const year = new Date().getFullYear(); const month = new Date().getMonth() + 1; async function writeSheet() { try { const doc = new GoogleSpreadsheet("Key"); await doc.useServiceAccountAuth(require("./gcp-creds.json")); // 認証 await doc.loadInfo(); // スプレッドシートの読み込み const sheet = await doc.sheetsByIndex[SHEET_ID]; // シートの読み込み await sheet.addRows([ { 年月: `${year}年${month}月`, ユーザー名: 'Sergey Brin', 会社名: 'testCompany', url: "url" }, { 年月: `${year}年${month}月`, ユーザー名: 'Sergey Brin2', 会社名: 'testCompany', url: "url" }, ]); } catch (error) { console.log(error) } } writeSheet()Key
下記のようにスプレッドシートがあった場合、
Key
はwaoooooooooooIsssssssssss
が該当します。
https://docs.google.com/spreadsheets/d/waoooooooooooIsssssssssss/edit#gid=3333053444
const doc = new GoogleSpreadsheet("Key");SHEET_ID
下記のようにスプレッドシートがあった場合、
ID
は3333053444
が該当します。
https://docs.google.com/spreadsheets/d/waoooooooooooIsssssssssss/edit#gid=3333053444
const SHEET_ID = 3333053444;addRows
年月:
の箇所は、下記のようにスプレッドシートで記載している情報と合わせる必要があります。
もしあっていなければ、エラーは出力されず、書き込まれず、処理が終了します。
await sheet.addRows([ { 年月: `${year}年${month}月`, ユーザー名: 'Sergey Brin', 会社名: 'testCompany', url: "url" }, { 年月: `${year}年${month}月`, ユーザー名: 'Sergey Brin2', 会社名: 'testCompany', url: "url" }, ]);日付処理
アウトプット
node index.js
を実行すると、下記のようなアウトプットが得られます。
- 投稿日:2020-08-18T14:36:32+09:00
OSがCatalina以降の環境構築
Command Line Toolsを用意
Command Line ToolsはWebアプリケーション開発に必要なソフトウェアをダウンロードするために必要な機能です。
ターミナルからCommand Line Toolsをインストール
ターミナル$ xcode-select --install出てくるポップアップには「インストール」→「同意する」→「完了」の順にクリック。
Homebrewを用意
Homebrewというソフトウェア管理ツールを導入します。
ターミナル$ cd # ホームディレクトリに移動 $ pwd # ホームディレクトリにいるかどうか確認 $ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" # コマンドを実行※処理に時間がかかる可能性のある操作
処理が進んでいくと、「press RETURN to continue or any other key to abort」(「続けるにはエンターキーを、やめるにはそれ以外の入力をしてください」)と表示されるので、ここではエンターキーを入力して先に進めましょう。
さらに、「password: 」と表示されたら、PCのパスワードを入力してください。
ターミナル上でパスワードを入力しても文字は表示されませんが、間違いなく入力はされています。パスワードを入力し終わったらエンターキーを押してください。その後、ダウンロードが完了し、再びコマンドを入力できるようになれば成功です。
Homebrewがインストールされているか確認
以下のコマンドを実行しましょう。
ターミナル$ brew -vHomebrewがインストールされているかを確認します。以下のように、Homebrewのバージョン情報が表示されれば無事にインストールされています。
ターミナル$ brew -v Homebrew 2.1.13Homebrewをアップデート
ターミナル$ brew updateHomebrewの権限を変更
ターミナル$ sudo chown -R `whoami`:admin /usr/local/binrbenv と ruby-buildをインストール
ターミナル$ brew install rbenv ruby-buildrbenvをどこからも使用できるようにする
ターミナル$ echo 'eval "$(rbenv init -)"' >> ~/.zshrczshrcの変更を反映させる
$ source ~/.zshrcreadlineをinstallし、どこからも使用できるようにする
ターミナルのirb上で日本語入力を可能にする設定を行うために、以下のコマンドでインストールしましょう。
ターミナル$ brew install readline $ brew link readline --forcerbenvを利用してRubyをインストール
Webアプリケーション開発用のRubyをインストールします。以下のコマンドを実行しましょう。
ターミナル$ RUBY_CONFIGURE_OPTS="--with-readline-dir=$(brew --prefix readline)" $ rbenv install 2.5.1※処理に10分程度かかる可能性のあるコマンドです。
2.5.1と書いてあるのは今回インストールするRubyのバージョンです。利用するRubyのバージョンを指定
インストールしたRuby 2.5.1を使用するために、以下のコマンドを実行しましょう。
ターミナル$ rbenv global 2.5.1rbenvを読み込んで変更を反映させる
ターミナル$ rbenv rehashRubyのバージョンを確認
ターミナル$ ruby -vRubyのバージョンが、先ほどインストールした2.5.1であることが表示されれば完了です。
MySQLのインストール
MySQLは、Webアプリケーションにおけるデータを蓄積する場所のことです。
ターミナル$ brew install mysql@5.6※処理に時間のかかる可能性があるコマンドです。
MySQLの自動起動設定をする
MySQLは本来であればPC再起動のたびに起動し直す必要がありますが、それは面倒であるため、自動に起動するようにしておきましょう。
ターミナル$ mkdir ~/Library/LaunchAgents $ ln -sfv /usr/local/opt/mysql\@5.6/*.plist ~/Library/LaunchAgents $ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql\@5.6.plistmysqlコマンドをどこからでも実行できるようにする
ターミナル# mysqlのコマンドを実行できるようにする $ echo 'export PATH="/usr/local/opt/mysql@5.6/bin:$PATH"' >> ~/.zshrc $ source ~/.zshrc # mysqlのコマンドが打てるか確認する $ which mysql # 以下のように表示されれば成功 /usr/local/opt/mysql@5.6/bin/mysqlmysqlを起動を確認
ターミナル# mysqlの状態を確認するコマンドです $ mysql.server status # 以下のように表示されれば成功 SUCCESS! MySQL runningRailsを用意
Rubyの拡張機能(gem)を管理するためのbundler(バンドラー)をインストールします。
ターミナル$ gem install bundlerRailsをインストール
ターミナル$ gem install rails --version='5.2.3'rbenvを再読み込み
ターミナル$ rbenv rehashRailsが導入できたか確認
以下のコマンドを実行して、Rails 5.2.3が表示されれば問題なくインストールが完了しています。
ターミナル$ rails -vNode.jsを用意
Railsを動かすためにはnode.jsが必要となり、それをHomebrewを用いてインストールします。
Node.jsのインストール
ターミナル$ brew install nodejsこの時、最後にError: node 13.10.0 is already installedと表示されても問題ありません。
Node.jsが導入できたか確認
以下のコマンドを実行して、v13.10.0のようにバージョンが表示されれば、問題なくインストールが完了しています。
ターミナル$ node -v以上でWebアプリケーション開発のための環境構築は完了です!
- 投稿日:2020-08-18T13:47:21+09:00
Node.js C++ アドオンの開発 (作業メモ)
この記事について
サンプルレベルの Node.js アドオンを C++ で書いてみた際の作業メモです。
内容:
- ソースは node-addon-examples をベースにしてます
- 足し算をする add という関数を JavaScript 側に見せる
2_function_arguments
にちょっと手を入れた程度ですNode.js アドオンとは
C++ addons の説明が分かりやすいので引用。
Addons are dynamically-linked shared objects written in C++. The require() function can load addons as ordinary Node.js modules. Addons provide an interface between JavaScript and C/C++ libraries.
Node.js アドオンを作る方法
Node.js のアドオンを作るには以下の方法がある。
- 内部の V8/libuv/Node.js ライブラリを直接使う
- nan(Native Abstractions for Node.js) を使う
- N-API を使う
- node-addon-api を使う
それぞれの特徴をざっくりまとめると以下の通り。
- v8/libuv/Node.js ライブラリを直接使う
- 複雑、かつ、各ライブラリのバージョンアップと変更の影響をモロに受けるので大変 (特に V8 はリリースごとに大きく変わるらしい1)
- 各ライブラリを直接触りたい場合以外は使うべきではない
- nan
- 各ライブラリのバージョン間の差異を吸収するツール(主にマクロ)を提供
- N-API
- アドオン開発向けの C言語 API で、ABI(Application Binary Interface)を保証する
- Node.js 本体と同じリポジトリでメンテされてる
- node-addon-api
- N-API を C++ でラップして使いやすくしたもの
- Node.js プロジェクトでメンテされてる
Node.js のドキュメントでは N-API と node-addon-api を推奨してる。
今回は、「v8/libuv/Node.jsライブラリを直接使う」 と 「node-addon-api」 を使ってみる。環境
今回試した環境は以下の通り。
- Ubuntu 18.04
- Node.js v12.18.3
- npm 6.14.6
- Python 3.6.9v8/libuv/Node.js ライブラリを直接使う場合
開発環境の準備
ビルドに必要なツール類をインストールする。
sudo apt install build-essential sudo npm install -g node-gyp開発
以下、適当な作業用ディレクトリで開発を行う。
まず main.cc を記述。
main.cc#include <node.h> #include <cstring> namespace demo { void ThrowTypeError(v8::Isolate *isolate, const char* msg) { size_t msgSize = std::strlen(msg); v8::Local<v8::String> v8Msg = v8::String::NewFromUtf8(isolate, msg, v8::NewStringType::kNormal, static_cast<int>(msgSize)).ToLocalChecked(); // Throw an Error that is passed back to JavaScript isolate->ThrowException(v8::Exception::TypeError(v8Msg)); } void AddMethod(const v8::FunctionCallbackInfo<v8::Value>& args) { v8::Isolate* isolate = args.GetIsolate(); // Check the number of arguments passed. if ( args.Length() < 2 ) { ThrowTypeError(isolate, "Wrong number of arguments"); return; } // Check the argument types if ( ! args[0]->IsNumber() || ! args[1]->IsNumber() ) { ThrowTypeError(isolate, "Wrong arguments"); return; } // Perform the operation double arg0 = args[0].As<v8::Number>()->Value(); double arg1 = args[1].As<v8::Number>()->Value(); v8::Local<v8::Number> answer = v8::Number::New(isolate, arg0 + arg1); // Set the return value (using the passed in FunctionCallbackInfo<Value>&) args.GetReturnValue().Set(answer); } void Initialize(v8::Local<v8::Object> exports) { NODE_SET_METHOD(exports, "add", AddMethod); } NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize) } // namespace demoビルド
同じディレクトリにビルド用の binding.gyp を用意。
binding.gyp{ "targets": [ { "target_name": "myaddon", "sources": [ "main.cc" ] } ] }上記ディレクトリからビルドを実行する。
# Makefile 等の作成。build ディレクトリ配下に出力される。 node-gyp configure # ビルドの実行。build/Release/ に myaddon.node というファイルが作成される。 node-gyp build実行
今回作ったアドオン
myaddon
を使う JavaScript を用意。成功する場合と失敗する場合の両方を試してる。sample.jsconst myaddon = require('./build/Release/myaddon') // 成功する場合 → 8 が返る。 const ans1 = myaddon.add(5, 3) console.log(ans1) // 失敗する場合(引数に数値ではなく文字列を渡してる) → 例外が投げられる。 try { const ans2 = myaddon.add(5, "abc") console.log(ans2) } catch (e) { console.log(e.message) }実行結果。意図した通りに動いてる。
$ node sample.js 8 Wrong arguments
node-addon-api を使う場合
開発環境の準備
ビルドに必要なツール類をインストールする。node-gyp の代わりに CMake も使えるが2、今回はそのまま node-gyp を使った。
sudo apt install build-essential sudo npm install -g node-gyp開発
以下、適当な作業用ディレクトリで開発を行う。
package.json を用意。
npm init # dependencies に node-addon-api を追加 npm install node-addon-api # package.json に `"gypfile": true` を追加する vi package.json以下のようになる。
{ "name": "myaddon", "version": "1.0.0", "description": "", "main": "sample.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "node-addon-api": "^3.0.0" }, "gypfile": true }次に main.cc を用意。内容は同じく足し算の関数 add のエクスポートだが、node-addon-api を使うと V8の複雑な型が消えてだいぶすっきりする。
なお、node-addon-api を使うには
napi.h
をインクルードする。v8.h
,uv.h
,node.h
などのライブラリのヘッダーを直接インクルードしてはいけない。#include <napi.h> namespace demo { Napi::Value AddMethod(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); // Check the number of arguments passed. if ( info.Length() < 2 ) { Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); return env.Null(); } // Check the argument type if ( ! info[0].IsNumber() || ! info[1].IsNumber() ) { Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException(); return env.Null(); } // Perform the operation double p1 = info[0].As<Napi::Number>().DoubleValue(); double p2 = info[1].As<Napi::Number>().DoubleValue(); Napi::Number answer = Napi::Number::New(env, p1 + p2); return answer; } Napi::Object Initialize(Napi::Env env, Napi::Object exports) { exports.Set(Napi::String::New(env, "add"), Napi::Function::New(env, AddMethod)); return exports; } NODE_API_MODULE(NODE_GYP_MODULE_NAME, Initialize) } // namespace demoビルド
ビルド用の binding.gyp を用意。少し複雑になるが、C++ から JavaScript への例外を無効にする設定等をしてる。詳細はここを参照。
{ "targets": [ { "target_name": "myaddon", "cflags!": [ "-fno-exceptions" ], "cflags_cc!": [ "-fno-exceptions" ], "sources": [ "main.cc" ], "include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" ], 'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ], } ] }ビルド手順は同じ。
node-gyp configure node-gyp build実行
実行手順と結果もまったく同じのため、簡易的に記載する。
# sample.js は同じものを用意しておく。 $ node sample.js 8 Wrong arguments参考サイト
以上
Native abstractions for Node.js には次の記述がある: " The V8 API can, and has, changed dramatically from one V8 release to the next (and one major Node.js release to the next)." ↩
CMake.js より引用 : "CMake.js is an alternative build system based on CMake. CMake.js is a good choice for projects that already use CMake or for developers affected by limitations in node-gyp." ↩
- 投稿日:2020-08-18T13:19:30+09:00
Node + OpenAPI + ReDocでおしゃれなAPI開発環境を作る
はじめに
こんにちは。夜に見てくれている方は、こんばんは。
どうも @little555 です。
夏季休暇なうでヒャッハー!しております。この記事は株式会社富士通システムズウェブテクノロジーが企画するいのべこ夏休みアドベントカレンダー 2020の20日目の記事です。
※毎回参加させていただき感謝の限りです。ありがとうございます!!とりあえず本題に入る前に大切なお約束です。
本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。
背景
API作りたいけど、管理とか横展開がめんどくさいなぁ。
横展開用のドキュメントとか作りたくないなぁ。。。
いい感じに開発に組み込めないかなぁ。。。そんな人が対象読者です。
そんなときはOpenAPI(Swagger)です。
YamlやJson形式でAPIの仕様を下記のような感じで定義できます。
参考:Swagger Editorでもこう、なんていうか。。。
ちょっと古い印象を受けませんか?そこでいいツールないかなぁーと探していて、であったのがこのツール。
そうReDocです下記のようなおしゃれ度の高いAPI一覧を作ることができます。
参考:ReDoc Interactive Demoおしゃれですね。自分のデザインセンスではとても作れません。
では、さっそく取り掛かっていきましょう。OpenAPI
とりあえず、まずは適当なOpenAPIを用意します。
公式のサンプルを取得するのが手っ取り早いでしょう。
OpenAPI-Specification/petstore.yaml at master · OAI/OpenAPI-Specification · GitHubDLしたら、ローカルで動かせるように一部書き換えましょう
./spec/petstore.yamlservers: - - url: http://petstore.swagger.io/v1/pets + - url: http://localhost:3000/v1ソースコード生成
次にソースコードを生成してみましょう。
@openapitools/openapi-generator-cliはYamlからソースコードを生成してくれます。
素晴らしいですね。
さっそくインストールします。$ npm init $ npm i -D @openapitools/openapi-generator-cli※いろんなプロジェクトで使いたい場合は
-g
オプションでインストールするのが推奨です。次に
@openapitools/openapi-generator-cli
を利用し、ソースコードを生成します。package.json"scripts": { "validate": "openapi-generator validate -i ./spec/petstore.yaml", "generate": "openapi-generator generate -g nodejs-express-server -i ./spec/petstore.yaml -o ./src" },$ npm run generate
すると
src
配下にnode一式が生成されます。
起動するために階層を移動して操作したくないので、起動できるようにしておきましょう。
※src配下にもpackage.json
がいるのでそれを利用する。package.json"scripts": { + "clean": "rm -rf src", + "server": "cd src && npm run prestart && npm run start", "validate": "openapi-generator validate -i ./spec/petstore.yaml", "generate": "openapi-generator generate -g nodejs-express-server -i ./spec/petstore.yaml -o ./src" },
$ npm run server
起動後下記URLにアクセスするとAPI一覧が動かせます。
http://localhost:3000/api-doc/ReDoc
簡易表示
ReDocの素晴らしいところは
JavaScript
とYAML
を読み込むだけで起動できることです。
※ただし、ローカルのYAMLファイル
はReDocの利用するJavaScriptのロジックの関係で読み込めません。サーバーを起動したのち、下記HTMLファイルを作成し開いてみてください。
<!DOCTYPE html> <html> <head> <title>ReDoc</title> <!-- needed for adaptive design --> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet"> <!-- ReDoc doesn't change outer page styles --> <style> body { margin: 0; padding: 0; } </style> </head> <body> <redoc spec-url='http://localhost:3000/openapi'></redoc> <script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script> </body> </html>これだけです。これだけで表示できるのです。
すごい!expressで表示できるようにする
せっかくなので、自動生成されるソースコードにReDocを表示するための機構を組み込みます。
そのためには、生成されるソースの元となるテンプレートファイルを取得&修正します。まず、テンプレート元となるファイルを取得します。
https://github.com/OpenAPITools/openapi-generator/tree/v4.3.1/modules/openapi-generator/src/main/resources/nodejs-express-server
取得するのは次の2ファイルです。
- package.mustache
- expressServer.mustache
取得したら、次のように追記します。
./spec/template/package.mustache"dependencies": { ... + "redoc-express": "^1.0.0", },
./spec/template/expressServer.mustache... const config = require('./config'); + const redoc = require('redoc-express'); ... this.app.use('/api-doc', swaggerUI.serve, swaggerUI.setup(this.schema)); + this.app.use('/redoc', redoc({title: '{{projectName}}', specUrl: '/openapi'}));これでテンプレートの準備は整いました。
次は、変換時にテンプレートを読み込むように設定しましょう。
package.json"scripts": { "clean": "rm -rf src", "server": "cd src && npm run prestart && npm run start", "validate": "openapi-generator validate -i ./spec/petstore.yaml", - "generate": "openapi-generator generate -g nodejs-express-server -i ./spec/petstore.yaml -o ./src" + "generate": "openapi-generator generate -g nodejs-express-server -i ./spec/petstore.yaml -o ./src -t ./spec/template" },再度変換して起動するとReDocが表示されます。
$ npm run generate $ npm run serverhttp://localhost:3000/api-doc/
おまけ:単一のHTMLでRedocを表示したい
横展開していくと、「サーバーの起動方法がわからない」「参照しようにもネットワークが断絶してる」など、APIドキュメントを共有しにくい相手もいるでしょう。
そんな時に役に立つのがredoc-cliです。
HTMLに全部内包して、単一のHTMLで表示できるようにしてくれる優れものです。
使い方は下記の通り
# 依存ライブラリのインストール $ npm i react react-dom mobx@^4.2.0 styled-components core-js -D # インストール $ npm i redoc redoc-cli -D起動用スクリプトの定義。
package.json"scripts": { "clean": "rm -rf src", "server": "cd src && npm run prestart && npm run start", "validate": "openapi-generator validate -i ./spec/petstore.yaml", "generate": "openapi-generator generate -g nodejs-express-server -i ./spec/petstore.yaml -o ./src -t ./spec/template", "create-doc" "redoc-cli bundle ./spec/petstore.yaml" },実行!!!
$ npm run create-doc
そうすると
redoc-static.html
が生成されます。
これ一つでどこでも起動できる!便利!おわりに
とりあえず、言いたいことはReDocすごくいいよ!に尽きます。
自分の大好きなチャットツールであるMattermostの公式APIリファレンスなどもReDocで作られています。結構使われていそうなので、みなさんも何かAPIを作る機会があればぜひ使ってみてください。
きれいなデザインはモチベを上げてくれます!
以上 @little555 でした。
参考資料
非常に参考になりました。ありがとうございます。