20200818のNode.jsに関する記事は9件です。

【第2回】「みんなのポートフォリオまとめサイト」を作ります~REST API編~ 【Cover】成果物URL: https://minna.itsumen.com

ワイ 「この記事のカバーです」
https://qiita.com/kiwatchi1991/items/58b53c5b4ddf8d6a7053

バックナンバー

【第1回】「みんなのポートフォリオまとめサイト」を作ります~プロトタイプ作成編~

成果物

https://minna.itsumen.com

リポジトリ

フロントエンド

https://github.com/yuzuru2/minna_frontend

バックエンド

https://github.com/yuzuru2/minna_backend

コレクション定義(テーブル定義)

ワイ 「今回はNoSQLMongoDBを使ってます」
ワイ 「コレクションとは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.ts
import * 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.ts
import * 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.ts
import * 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;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 に限定されています。としっかりと書いてましたね......

https://docs.microsoft.com/ja-jp/azure/static-web-apps/apis

ちゃんとドキュメントみましょうという教訓をメモにしておきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SAP Cloud Platform 上で WebSocket を使ってみた

はじめに

この記事は chillSAP 夏の自由研究2020 の記事として執筆しています

今回は、 SAPUI5 の API Reference を眺めていると WebSocket( sap.ui.core.ws.WebSocket )のライブラリを発見したので試してみました。

WebSocket とは?

WebSocket(ウェブソケット)は、コンピュータネットワーク用の通信規格の1つである。ウェブアプリケーションにおいて、双方向通信を実現するための技術規格である。2011年にRFC 6455として>最初の標準仕様が定義された。

転載:WebSocket - Wikipedia

難しいのでなるべく簡単に説明すると、
Webの世界でブラウザとサーバが通信をする場合には、ブラウザからの呼びかけに対してサーバが返事する仕組みでしたが
WebSocket を使用すると、サーバからブラウザに対して呼びかけをしてくれる仕組みが作れるということです。

今回のブツ

SAP Cloud Platform 上で WebSocketを使用した ブラウザChat を作ってみました。
image.png

仕組み

  1. ブラウザから SAP UI5 で準備したWebチャットを動かすと、 Webチャット は SAP Cloud Platform 上に nodejs で構築したチャットサーバと、 WebSocket で接続します。
  2. 接続中の Webチャット から発言すると、発言内容は チャットサーバへと送られます。
  3. チャットサーバは、Webチャットからの発言を受け取ると、接続中のすべてのWebチャット に対して受信した内容を返します。
  4. Webチャットは、チャットサーバからの通信を受け取ると、画面上へ受信した内容を表示します。

WebChat.gif

まとめ

送信したメッセージは、ほぼリアルタイムに相手側へ送られているのがご覧いただけると思います。
WebSocketを使わなくても、ポーリングや Comet を使用すれば似たようなことは実装できますが
ほぼリアルタイムの相互通信が簡単に実装出来るので、一度 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.js
sap.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("");
            }
        }
    });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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) => {
トップ画面を表示する処理
});

キャプチャ.PNG

ルーティングの処理で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は画面に表示させないので <% %>で記述しましょう。

    { %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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からファイルを選択したら右クリックして、一番下の「選択したビデオファイルをエンコード」をクリックしてください(ちなみに、「選択したビデオファイルをエンコード」は複数ファイルを選択した状態でも可能です)。
image.png

「メディアをエンコード」というウィンドウが開くので、設定:オーディオのみに変更します。最後に'続ける'を押したら完了です。元のビデオファイルとは別に、音声ファイルが作成されます。
image.png

先述した通り、「選択したビデオファイルをエンコード」は複数ファイルを選択した状態でも可能なので、ある程度貯めてからまとめて変換するとラクです。

指定した時間で切り抜き

iMovieを使用します。

「新規作成(大きな「+」アイコン)」をクリックしたら、「ムービー」を選択します。
image.png

すると、動画の編集画面が開きます。
画面左上の「ファイル」から「メディアを読み込む」をクリックします。
image.png

またウィンドウが開くので、編集したいファイルを選択したら、画面右下の「選択した項目を取り込む」をクリックしてください。
image.png

iMovieにファイルが追加されました。
image.png

上の画面に追加されたファイルを、下の編集画面にドラッグ&ドロップしてください。
image.png

まずはクリップを分割します。
ファイルの適当なところで右クリックして、「クリップを分割」をクリック。
image.png

ファイルがクリップに分割されました。
クリップをcommand + Cでコピーしたら、いったんこのプロジェクトを閉じましょう。

新しくプロジェクトを作成します。そして新しく作成したプロジェクトの編集画面(下の方)でcommand + Vで貼り付けます。
画面左上の「ファイル」から「共有」をクリックします。

image.png

ファイルの形式(フォーマット)を任意のものに選択して、「次へ」でファイルを書き出します。
image.png

こういう場合は明らかにCUIよりGUIの方が適していますね。

当初はダウンロードする時点で開始地点と終了地点を指定していたのですが、秒数に変換して指定しなくていけないので、かなり面倒でした。計算ミスも頻発しましたし、毎回ダウンロードし直さなくてはいけないのも大変でした。

参考文献


Node.jsデザインパターン 第2版


Node.js超入門 第3版

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JS】Node.js と GoogleSpreadsheet で業務効率化

google-spreadsheet というパッケージを使用し、スプレッドシートへの書き込みの実装をしたので、その備忘録です。
google-spreadsheet - npm

下記の記事をみておけばほぼわかります。
【Node.js】 Googleスプレッドシートを簡易データベースとして使う - 一日一膳(当社比)
GoogleスプレッドシートからNode.jsでシフトデータを読み出す方法 - Twilio

準備

APIの有効化 & 認証

  1. GoogleSpreadsheet API 有効化
  2. サービスアカウントを発行
  3. スプレッドシートに 作成したサービスアカウントを招待(メールアドレス)
  4. アプリケーション側で読み込み

インストール

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

下記のようにスプレッドシートがあった場合、KeywaoooooooooooIsssssssssss が該当します。
https://docs.google.com/spreadsheets/d/waoooooooooooIsssssssssss/edit#gid=3333053444

const doc = new GoogleSpreadsheet("Key");

SHEET_ID

下記のようにスプレッドシートがあった場合、ID3333053444 が該当します。
https://docs.google.com/spreadsheets/d/waoooooooooooIsssssssssss/edit#gid=3333053444

const SHEET_ID = 3333053444;

addRows

年月:の箇所は、下記のようにスプレッドシートで記載している情報と合わせる必要があります。
もしあっていなければ、エラーは出力されず、書き込まれず、処理が終了します。
image.png

await sheet.addRows([
  { 年月: `${year}${month}月`, ユーザー名: 'Sergey Brin', 会社名: 'testCompany', url: "url" },
  { 年月: `${year}${month}月`, ユーザー名: 'Sergey Brin2', 会社名: 'testCompany', url: "url" },
]);

日付処理

【JavaScript】日付処理 - Qiita

アウトプット

node index.jsを実行すると、下記のようなアウトプットが得られます。

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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: :key2:」と表示されたら、PCのパスワードを入力してください。
ターミナル上でパスワードを入力しても文字は表示されませんが、間違いなく入力はされています。パスワードを入力し終わったらエンターキーを押してください。

その後、ダウンロードが完了し、再びコマンドを入力できるようになれば成功です。

Homebrewがインストールされているか確認

以下のコマンドを実行しましょう。

ターミナル
$ brew -v

Homebrewがインストールされているかを確認します。以下のように、Homebrewのバージョン情報が表示されれば無事にインストールされています。

ターミナル
$ brew -v
Homebrew 2.1.13

Homebrewをアップデート

ターミナル
$ brew update

Homebrewの権限を変更

ターミナル
$ sudo chown -R `whoami`:admin /usr/local/bin

rbenv と ruby-buildをインストール

ターミナル
$ brew install rbenv ruby-build

rbenvをどこからも使用できるようにする

ターミナル
$ echo 'eval "$(rbenv init -)"' >> ~/.zshrc

zshrcの変更を反映させる

$ source ~/.zshrc

readlineをinstallし、どこからも使用できるようにする

ターミナルのirb上で日本語入力を可能にする設定を行うために、以下のコマンドでインストールしましょう。

ターミナル
$ brew install readline
$ brew link readline --force

rbenvを利用して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.1

rbenvを読み込んで変更を反映させる

ターミナル
$ rbenv rehash

Rubyのバージョンを確認

ターミナル
$ ruby -v

Rubyのバージョンが、先ほどインストールした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.plist 

mysqlコマンドをどこからでも実行できるようにする

ターミナル
# 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/mysql

mysqlを起動を確認

ターミナル
# mysqlの状態を確認するコマンドです
$ mysql.server status

# 以下のように表示されれば成功
 SUCCESS! MySQL running

Railsを用意

Rubyの拡張機能(gem)を管理するためのbundler(バンドラー)をインストールします。

ターミナル
$ gem install bundler

Railsをインストール

ターミナル
$ gem install rails --version='5.2.3'

rbenvを再読み込み

ターミナル
$ rbenv rehash

Railsが導入できたか確認

以下のコマンドを実行して、Rails 5.2.3が表示されれば問題なくインストールが完了しています。

ターミナル
$ rails -v

Node.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アプリケーション開発のための環境構築は完了です!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 のアドオンを作るには以下の方法がある。

  1. 内部の V8/libuv/Node.js ライブラリを直接使う
  2. nan(Native Abstractions for Node.js) を使う
  3. N-API を使う
  4. 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.9

v8/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.js
const 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

参考サイト

以上


  1. 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)." 

  2. 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." 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node + OpenAPI + ReDocでおしゃれなAPI開発環境を作る

はじめに

こんにちは。夜に見てくれている方は、こんばんは。
どうも @little555 です。
夏季休暇なうでヒャッハー!しております。

この記事は株式会社富士通システムズウェブテクノロジーが企画するいのべこ夏休みアドベントカレンダー 2020の20日目の記事です。
※毎回参加させていただき感謝の限りです。ありがとうございます!!

とりあえず本題に入る前に大切なお約束です。
本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。

背景

API作りたいけど、管理とか横展開がめんどくさいなぁ。
横展開用のドキュメントとか作りたくないなぁ。。。
いい感じに開発に組み込めないかなぁ。。。

そんな人が対象読者です。

そんなときはOpenAPI(Swagger)です。
YamlやJson形式でAPIの仕様を下記のような感じで定義できます。
Swagger Editor.png
参考:Swagger Editor

でもこう、なんていうか。。。
ちょっと古い印象を受けませんか?

そこでいいツールないかなぁーと探していて、であったのがこのツール。
そうReDocです

下記のようなおしゃれ度の高いAPI一覧を作ることができます。
ReDoc.png
参考:ReDoc Interactive Demo

おしゃれですね。自分のデザインセンスではとても作れません。
では、さっそく取り掛かっていきましょう。

OpenAPI

とりあえず、まずは適当なOpenAPIを用意します。
公式のサンプルを取得するのが手っ取り早いでしょう。
OpenAPI-Specification/petstore.yaml at master · OAI/OpenAPI-Specification · GitHub

DLしたら、ローカルで動かせるように一部書き換えましょう

./spec/petstore.yaml
servers:
- - 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一式が生成されます。
image.png
起動するために階層を移動して操作したくないので、起動できるようにしておきましょう。
※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/

LocalOpenAPI.png

ReDoc

簡易表示

ReDocの素晴らしいところはJavaScriptYAMLを読み込むだけで起動できることです。
※ただし、ローカルの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>

LocalRedoc.png

これだけです。これだけで表示できるのです。
すごい!

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 server

http://localhost:3000/api-doc/
LocalRedoc.png

おまけ:単一の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 でした。

参考資料

非常に参考になりました。ありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む