Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8837 articles
Browse latest View live

webpackをかんたんに使う(Zero Configulation)

$
0
0

5年前の記事のメンテを行っていたんですが、流石にBrowserifyは実務で使う可能性は低いよなあとは思います。
Browserifyの単純さも初心者にはとっつきやすいのですが、今の主流はwebpackです。
webpack入門記事はたくさんあるはずですが、設定ファイルの各パラメータの羅列だったり、BabelやReact, TypeScriptの設定まで一気に済ませてしまうような記事は初心者には難しいかもしれないですね。

というわけで、Node.jsは3日前にはじめました、みたいな人向けに書いてみようと思います。

初心者なのでwebpackだけを使ってみたいんだけど

今どきだと初心者はReactならcreate-react-app、VueならVue CLIを使って入門するんじゃないかと思います。これらはwebpackの設定を代わりに済ませてくれる便利なツールで、難しいことを知らなくて済みます。

ただ、何も理解しないままにWebアプリが作れてしまうので、一つ一つ理解していきたいタイプの人には気持ちが悪いかもしれません。

というわけで、webpackのみを使ってWebアプリを作ってみましょう。

プロジェクトを作成

下記のコマンドを実行してpackage.jsonを作ります。

npm init -y

次にnode_modules下にwebpackと開発ツールをインストールします。

npm install -D webpack webpack-cli webpack-dev-server

その結果、下記のようなpackage.jsonが生成されます。

{"name":"your-folder","version":"1.0.0","description":"","main":"index.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1"},"keywords":[],"author":"","license":"ISC","devDependencies":{"webpack":"^4.44.1","webpack-cli":"^3.3.12","webpack-dev-server":"^3.11.0"}}

ソースコードを配置する

下記の2ファイルを作成してください。

  • dist/index.html
  • src/index.js
//index.jsdocument.write("Hello, webpack")
<!-- index.html --><!DOCTYPE html><html><head><title>Hello</title></head><body><script src="main.js"></script></body></html>

結果、下記のようなフォルダ構成になります。

image.png

webpackを動かすには色々設定が必要ですが、無設定の場合はsrc/index.jsが入り口になります。distはwebpackが処理した後のjsファイルが出力されます。上記のフォルダ構成にしておけば無設定でも動かせるわけです。

npm-scriptsを追加

開発はnpm run dev、ビルドはnpm run buildで行えるようにしましょう。package.json内にwebpackの実行時に渡すオプションを直書きしていきます。

package.jsonscriptsにbuild, devの2行を追加します。

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode=production",
    "dev": "webpack-dev-server --hot --open --content-base dist/"
  },

実行してみる

下記のコマンドを実行してみましょう。

npm run dev

ブラウザが開いてHello, webpackが表示されればOKです。

npmのライブラリを使ってみる

今回は日時処理ライブラリdate-fnsを使ってみましょう。
下記のコマンドを実行してください。

npm install -D date-fns

次に、index.jsの内容を下記のように書き換えてください。

//index.js
const dateFns = require("date-fns")
const ja = require("date-fns/locale/ja")

document.write(dateFns.format(new Date(), "'今日は' MMM do iiii", {locale: ja.default}))

その後、npm run devをもう一度実行してみてください。

image.png

こんな表示になりましたでしょうか。

webpackはjsを張り合わせる糊

webpackにはいろんな機能がありますが、一番基本的な機能はjsの結合処理です。今回の場合はindex.jsdate-fnsライブラリが張り合わせられて、一本のjsファイルにまとめられています。

npm run buildを実行後、dist/main.jsを開くと、結合されたjsファイルを見ることができます。
npm run dev実行時にはmain.jsは出力されません。これはwebpack-dev-serverというツールの機能で、ブラウザからはmain.jsがあるように見える状態になっています。

webpackが何をしているのか、詳細を知りたい方は下記記事がおすすめです。

https://mizchi.hatenablog.com/entry/2018/11/26/164523

終わりに

この状態ではかなりできることが限られていて、例えばReactのJSXファイルやVue SFC, TypeScriptファイル等を読ませてもエラーになります。
BabelやTypeScriptの設定に進みたい方は下記記事を参考にしてください。

https://qiita.com/clockmaker/items/8620cf6bd99d810dbf2a

あるいは、ここまでで概要を把握できたら、もうcreate-react-appやVue CLIを使っても良いでしょう。
個人的にはParcelをよく使っていて、今後はviteに移行する予定です。


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

$
0
0

はじめに

こんにちは。夜に見てくれている方は、こんばんは。
どうも @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 --><metacharset="utf-8"/><metaname="viewport"content="width=device-width, initial-scale=1"><linkhref="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><redocspec-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でした。

参考資料

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

Node.js C++ アドオンの開発 (作業メモ)

$
0
0

この記事について

サンプルレベルの 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>
namespacedemo{voidThrowTypeError(v8::Isolate*isolate,constchar*msg){size_tmsgSize=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 JavaScriptisolate->ThrowException(v8::Exception::TypeError(v8Msg));}voidAddMethod(constv8::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 typesif(!args[0]->IsNumber()||!args[1]->IsNumber()){ThrowTypeError(isolate,"Wrong arguments");return;}// Perform the operationdoublearg0=args[0].As<v8::Number>()->Value();doublearg1=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);}voidInitialize(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
constmyaddon=require('./build/Release/myaddon')// 成功する場合 → 8 が返る。constans1=myaddon.add(5,3)console.log(ans1)// 失敗する場合(引数に数値ではなく文字列を渡してる) → 例外が投げられる。try{constans2=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>
namespacedemo{Napi::ValueAddMethod(constNapi::CallbackInfo&info){Napi::Envenv=info.Env();// Check the number of arguments passed.if(info.Length()<2){Napi::TypeError::New(env,"Wrong number of arguments").ThrowAsJavaScriptException();returnenv.Null();}// Check the argument typeif(!info[0].IsNumber()||!info[1].IsNumber()){Napi::TypeError::New(env,"Wrong arguments").ThrowAsJavaScriptException();returnenv.Null();}// Perform the operationdoublep1=info[0].As<Napi::Number>().DoubleValue();doublep2=info[1].As<Napi::Number>().DoubleValue();Napi::Numberanswer=Napi::Number::New(env,p1+p2);returnanswer;}Napi::ObjectInitialize(Napi::Envenv,Napi::Objectexports){exports.Set(Napi::String::New(env,"add"),Napi::Function::New(env,AddMethod));returnexports;}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." 

OSがCatalina以降の環境構築

$
0
0

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-vHomebrew2.1.13

Homebrewをアップデート

ターミナル
$brewupdate

Homebrewの権限を変更

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

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

ターミナル
$brewinstallrbenvruby-build

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

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

zshrcの変更を反映させる

$source~/.zshrc

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

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

ターミナル
$brewinstallreadline$brewlinkreadline--force

rbenvを利用してRubyをインストール

Webアプリケーション開発用のRubyをインストールします。以下のコマンドを実行しましょう。

ターミナル
$RUBY_CONFIGURE_OPTS="--with-readline-dir=$(brew --prefix readline)"$rbenvinstall2.5.1

※処理に10分程度かかる可能性のあるコマンドです。
2.5.1と書いてあるのは今回インストールするRubyのバージョンです。

利用するRubyのバージョンを指定

インストールしたRuby 2.5.1を使用するために、以下のコマンドを実行しましょう。

ターミナル
$rbenvglobal2.5.1

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

ターミナル
$rbenvrehash

Rubyのバージョンを確認

ターミナル
$ruby-v

Rubyのバージョンが、先ほどインストールした2.5.1であることが表示されれば完了です。

MySQLのインストール

MySQLは、Webアプリケーションにおけるデータを蓄積する場所のことです。

ターミナル
$brewinstallmysql@5.6

※処理に時間のかかる可能性があるコマンドです。

MySQLの自動起動設定をする

MySQLは本来であればPC再起動のたびに起動し直す必要がありますが、それは面倒であるため、自動に起動するようにしておきましょう。

ターミナル
$mkdir~/Library/LaunchAgents$ln-sfv/usr/local/opt/mysql\@5.6/*.plist~/Library/LaunchAgents$launchctlload~/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のコマンドが打てるか確認する$whichmysql# 以下のように表示されれば成功/usr/local/opt/mysql@5.6/bin/mysql

mysqlを起動を確認

ターミナル
# mysqlの状態を確認するコマンドです$mysql.serverstatus# 以下のように表示されれば成功SUCCESS!MySQLrunning

Railsを用意

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

ターミナル
$geminstallbundler

Railsをインストール

ターミナル
$geminstallrails--version='5.2.3'

rbenvを再読み込み

ターミナル
$rbenvrehash

Railsが導入できたか確認

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

ターミナル
$rails-v

Node.jsを用意

Railsを動かすためにはnode.jsが必要となり、それをHomebrewを用いてインストールします。

Node.jsのインストール

ターミナル
$brewinstallnodejs

この時、最後にError: node 13.10.0 is already installedと表示されても問題ありません。

Node.jsが導入できたか確認

以下のコマンドを実行して、v13.10.0のようにバージョンが表示されれば、問題なくインストールが完了しています。

ターミナル
$node-v

以上でWebアプリケーション開発のための環境構築は完了です!

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

$
0
0

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');constSHEET_ID=3333053444;constyear=newDate().getFullYear();constmonth=newDate().getMonth()+1;asyncfunctionwriteSheet(){try{constdoc=newGoogleSpreadsheet("Key");awaitdoc.useServiceAccountAuth(require("./gcp-creds.json"));// 認証awaitdoc.loadInfo();// スプレッドシートの読み込みconstsheet=awaitdoc.sheetsByIndex[SHEET_ID];// シートの読み込みawaitsheet.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

constdoc=newGoogleSpreadsheet("Key");

SHEET_ID

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

constSHEET_ID=3333053444;

addRows

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

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

日付処理

【JavaScript】日付処理 - Qiita

アウトプット

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

image.png

ytdlでYoutubeダウンロード+Macでファイル変換&編集

$
0
0

もう、無限に広告がポップしてくる詐欺ソフトに悩まされることはありません。
我々エンジニアはプログラミングと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版

Node.jsⅠ

$
0
0

◆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は画面に表示させないので <% %>で記述しましょう。

    { %>

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

$
0
0

はじめに

この記事は 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";varWebSocketServer=require('ws').Server;varws=newWebSocketServer({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";returnController.extend("cli.client.controller.View1",{oModel:newsap.ui.model.json.JSONModel(),// チャットサーバへデータ送信 notify:function(user,text){varmsg=user+': '+text;varmsgAreaId="chatInfo";// 現在表示中のメッセージを取得varchatMsg=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(){varthisController=this;sap.ui.getCore().byId("userName").setValue("ななし");varurl="wss://チャットサーバ";varws=newsap.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){vardata=jQuery.parseJSON(oControlEvent.getParameter('data'));thisController.notify(data.user,data.text);});//終了時ws.attachClose(function(attachClose){thisController.notify('system','Disconnected from Server');});},// 送信ボタン押下onSendMessage:function(oEvent){varuser=sap.ui.getCore().byId("userName").getValue();varmsg=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("");}}});});

Static Web Appsでは現状TimerTriggerは使えないっぽい

$
0
0

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

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

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

$
0
0

ワイ 「この記事のカバーです」
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でいうテーブル的なやつです」

RDBMongoDB
スキーマデータベース
テーブルコレクション
カラムフィールド
レコードドキュメント

①Users(ユーザ)

uid: unique
物理名論理名
uidユーザIDstring
name名前string
twitterUrlTwitterのURLstring
githubUrlGitHubのURLstring
createdAt作成時間Date
updatedAt更新時間Date
src/mongoose/collection/users.ts
import*asmongoosefrom'mongoose';importSchemafrom'src/mongoose';constmodel_name='users';interface_interface{uid:string;name:string;twitterUrl:string;githubUrl:string;createdAt:Date;updatedAt:Date;}interfacei_modelextendsmongoose.Document{}interfacei_modelextends_interface{}constmodel=mongoose.model(model_name,newSchema({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}));// 作成exportconstcreate=async(params:Pick<i_model,'uid'>)=>{const_data:_interface={uid:params.uid,name:'名無し',twitterUrl:'',githubUrl:'',createdAt:newDate(),updatedAt:newDate(),};return(awaitmodel.insertMany([_data]))asi_model[];};// 抽出exportconstfind=async(params:Pick<i_model,'uid'>)=>{const_data:Pick<i_model,'uid'>={uid:params.uid};return(awaitmodel.find(_data))asi_model[];};// 更新exportconstupdate=async(uid:string,params:Pick<i_model,'name'|'twitterUrl'|'githubUrl'>)=>{returnawaitmodel.updateOne({uid:uid},{$set:{...params,updatedAt:newDate()}});};

②Products(ポートフォリオ)

_id: unique
物理名論理名
_idポートフォリオのIDstring
uidユーザIDstring
typeポートフォリオのタイプnumber
titleポートフォリオのタイトルstring
urlポートフォリオのURLstring
repoリポジトリのURLstring
createdAt作成時間Date
updatedAt更新時間Date
src/mongoose/collection/products.ts
import*asmongoosefrom'mongoose';importSchemafrom'src/mongoose';constmodel_name='products';constpagingNum=5;interface_interface{uid:string;type:number;title:string;url:string;repo:string;createdAt:Date;updatedAt:Date;}interfacei_modelextendsmongoose.Document{}interfacei_modelextends_interface{}constmodel=mongoose.model(model_name,newSchema({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},}));// 作成exportconstcreate=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:newDate(),updatedAt:newDate(),};return(awaitmodel.insertMany([_data]))asi_model[];};// 更新exportconstupdate=async(id:string,uid:string,params:Pick<i_model,'type'|'title'|'url'|'repo'>)=>{returnawaitmodel.updateOne({_id:id,uid:uid},{$set:{...params,updatedAt:newDate()}});};// 削除exportconstdeleteProduct=async(id:string,uid:string)=>{returnawaitmodel.deleteOne({_id:id,uid:uid});};// 全投稿数exportconstcountAll=async()=>{returnmodel.find({}).countDocuments();};// ジャンル別投稿数exportconstcountType=async(type:number)=>{returnmodel.find({type:type}).countDocuments();};// タイトル別投稿数exportconstcountTitle=async(title:string)=>{returnmodel.find({title:{$regex:title}}).countDocuments();};// ユーザ別投稿数exportconstcountUser=async(uid:string)=>{returnmodel.find({uid:uid}).countDocuments();};// ページング全投稿exportconstpagingAll=async(num:number)=>{returnawaitmodel.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',},},]);};// ページングタイプ別exportconstpagingType=async(num:number,type:number)=>{returnawaitmodel.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',},},]);};// ページングタイトル別exportconstpagingTitle=async(num:number,title:string)=>{returnawaitmodel.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',},},]);};// ページングユーザ別exportconstpagingUser=async(num:number,uid:string)=>{returnawaitmodel.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*asExpressfrom'express';import*asCorsfrom'cors';import*asDotEnvfrom'dotenv';importConstantfrom'src/constant';// route---importcreate_friendfrom'src/route/create/friend';importcreate_userfrom'src/route/create/user';importcreate_productfrom'src/route/create/product';importpaging_allfrom'src/route/paging/all';importpaging_titlefrom'src/route/paging/title';importpaging_typefrom'src/route/paging/type';importpaging_userfrom'src/route/paging/users';importupdate_productfrom'src/route/update/product';importupdate_userfrom'src/route/update/user';importcancel_friendfrom'src/route/cancel/friend';importcancel_productfrom'src/route/cancel/product';importfind_userfrom'src/route/find/user';// route---DotEnv.config();constapp=Express();constrouter=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---exportdefaultapp;

Serverless Framework でエラーを検知して Webhook で Slack に通知を飛ばす方法

$
0
0

はじめに

AppSync の Lambda リゾルバを書く際に Serverless Frameworkを使用したのですが、
デプロイ後のバグ調査の際、毎回ブラウザから AWS Console を開いて該当 Lambda の CloudWatch のログを見に行くのが面倒でした。。 :upside_down:

そのため、エラーレポートの仕組みが欲しくなり、Lambda のエラーを Slack に通知する仕組みを Serverless Framework で実装する方法について調査したので、備忘録も兼ねて記事にまとめました :writing_hand:

動作環境

1. Slack で Webhook URL を発行する

まずは 公式サイトの手順に従って Webhook URL を発行します :earth_americas:

無事発行できると、
https://hooks.slack.com/services/~~~~~/~~~~~/~~~~~~~~~~~のようなフォーマットの URL が取得出来るはずなのでメモっておきます :pencil:

2. 必要な npm パッケージをインストールする

Slack の Incoming Webhook の仕組みを使用し、
チャンネルにメッセージを送信するための npm パッケージをインストールします :arrow_down:

npm install @slack/webhook --save

3. Slack にエラーレポートを送信する Lambda 関数を作成する (TypeScript)

Serverless Framework の handler に Slack にエラーレポートを送信する関数を追加します :arrow_down:

handlers/Reporter.ts
import{gunzip}from"zlib";import{IncomingWebhook}from"@slack/webhook";/**
 * CloudWatch のログ情報
 */interfaceCloudWatchLogContent{messageType:string;owner:string;logGroup:string;logStream:string;subscriptionFilters:string[];logEvents:{id:string;timestamp:string;message:string;}[];}/**
 * Lambda リゾルバの型定義
 */typeLambdaResolver<TEvent=any>=(event:TEvent)=>Promise<any>|any;/**
 * CloudWatch のロググループに出現するエラーを通知する
 * @param event 該当するエラーログの内容
 * @return {object} event オブジェクトをそのまま返却する
 */exportconstNotifyError:LambdaResolver=async(event:any)=>{/**
{
  awslogs: {
    data: 'H4sIAAA...'
  }
}
CloudWatch から呼ばれた際の event には上記フォーマットでデータが入っている。
data 内には Base64 でエンコードされた gzip 形式で圧縮されたデータが入っているので、
gzip 形式のデータを解凍しつつ、Base64 デコードを行い JSON 文字列を取得するための関数
*/constgunzipAsync=async(base64Logs):Promise<string>=>{returnnewPromise(function(resolve,reject){gunzip(base64Logs,function(err,binary){err?reject(err):resolve(binary.toString("ascii"));});});};// 1. Base64 でエンコードされた gzip 形式で圧縮されたデータを Base64 でデコードし、gzip のバイナリとして取得する// gunzipAsync 関数で gzip 解凍して ascii 文字列として取得することで CloudWatch のログ内容を JSON 文字列で取得するconstbase64Logs=Buffer.from(event["awslogs"]["data"],"base64");constuncompressedLogs=awaitgunzipAsync(base64Logs);console.log(uncompressedLogs);// 2. 取得した JSON 文字列を CloudWatchLogContent に変換して取得するconstcontent=<CloudWatchLogContent>JSON.parse(uncompressedLogs);console.log(content);// 3. 発行した Slack の Webhook URL で IncomingWebhook クラスを生成し、// send 関数で Slack チャンネル名 (ex. #serverless-error-report) と、// CloudWatchLogContent の内容を元に作成したテキストを引数に指定して、// 該当する Slack チャンネルにテキストを投稿するconstwebhook=newIncomingWebhook("<1. で発行した Slack の Incoming Webhook URL>");awaitwebhook.send({channel:"<通知したいチャンネル名 (例: #serverless-error-report)>",icon_emoji:"hammer",// icon_emoji パラメタを指定すると Slack へのメッセージ通知の際のアイコンを変更することが可能text:`*Group*\n_${content.logGroup}_\n\n*Message*\n\`\`\`${content.logEvents[0].message}\`\`\``,});returnevent;};

4. 3. の関数をデプロイする関数として追加し、各種 Lambda 関数の CloudWatch のログを監視するイベントと紐付ける

serverless.yml手順 3.で作成した関数 NotifyErrorを記載すると共に、
監視したい関数の CloudWatch ロググループを eventscloudwatchLogに定義し、filterERRORを指定します :heavy_check_mark:

これで、
該当ロググループに ERRORが含まれていた場合、
都度 Lambda 関数が実行されるようになります :fire::arrow_down:

serverless.yml
#...# 3. で作成した Slack にエラーレポートを送信する関数 NotifyError を functions に追記し、# events を用いて、他 Lambda 関数のロググループに 'ERROR' が出力されていた場合、 NotifyError 関数が実行されるようにするfunctions:#...NotifyError:handler:CloudWatch.NotifyErrorevents:-cloudwatchLog:logGroup:/aws/lambda/${self:service.name}-${self:provider.stage}-TestFunction1filter:ERROR-cloudwatchLog:logGroup:/aws/lambda/${self:service.name}-${self:provider.stage}-TestFunction2filter:ERROR-cloudwatchLog:logGroup:/aws/lambda/${self:service.name}-${self:provider.stage}-TestFunction3filter:ERROR#...

:arrow_up:が完了したら sls deployNotifyError関数をデプロイします :keyboard:

最後に AWS CLI で CloudWatch にイベントログデータを送信してみて、
本当に Slack に通知が飛んでくるか動作確認を行いましょう :hammer_pick:

注意事項 (複数の cloudwatchLogを定義した場合)

eventsに複数の cloudwatchLogを定義した場合、
関数のデプロイ後、AWS Console で該当する Lambda 関数を見に行くと、
正しく CloudWatch のイベントが紐付けられていないように見えます :arrow_down:
492d4761e7fadb62612e539c17b3d05f.png

同様のケースが発生している方は他にもいらっしゃるようですが、
手順 5.で Slack への通知まで確認出来れば問題なく設定できています :thumbsup:

5. AWS CLI を利用してCloudWatch にイベントログデータを送信して Slack に通知が飛んでくるか検証する

CloudWatch にイベントログデータを送信するコマンドです :white_check_mark::arrow_down:

aws logs put-log-events \--log-group-name'<該当するロググループ名>(例: /aws/lambda/test-dev-TestFunction1)'\--log-stream-name'<ログストリーム名(例: test-stream)>'--log-events\timestamp=(node -e'console.log(Date.now())'),message="This is ERROR"

コマンド実行後、
AWS Console から CloudWatch の該当するロググループのログストリームを見に行くと、
This is ERRORという文字列が出力されている事が確認できるはずです :mag:

あとは Slack に通知が飛んできたことまで確認できれば動作確認完了です! :tada::arrow_down:
4a5d063485ed635c83e3ea1917043673.png

おわりに

Serverless Framework 内で完結する形で、
Lambda 関数のエラーを捕捉して Slack に通知を飛ばす方法についてまとめました :writing_hand:

eventsには cloudwatchLogの他にも eventBridgeというものも指定できます。
eventBridgeを使用すると CloudWatch 以外の様々な AWS サービスのイベント駆動で Lambda 関数を実行することが可能です :muscle:

eventsを有効活用することで効率よくイベント駆動の処理を書いていけるので、
是非とも有効活用していきましょう! :runner::dash:

参考リンク

json-serverでstaticを指定してもプレビューのページを表示する

$
0
0

json-server

わたしは自分のことをxRアプリエンジニアだと思っていたのですが、最近はいろんなサーバをいっぱい立てています。
そのうちのモックサーバなんてjsonを返せればいいのでjson-serverを使いました。

Get a full fake REST API with zero coding in less than 30 seconds (seriously)

が嘘じゃないすごいやつです。
あと、たのしい顔文字がいっぱい出力されます。

--staticの罠

2020-08-08 02.02.01 localhost 65269d951fd9.png

http://localhost:3000

にアクセスするとこんなページが出てきて現在の設定を確認することができます。
CLIでも確認することはできますが、ブラウザで手軽に可視化して、クリックでAPIのレスポンスも確認できるので、これがあると楽です。
ですがこのページ、publicフォルダを実行ディレクトリに作成するか--staticでオプションを渡して静的ファイルを指定すると見られなくなってしまいます。

2020-08-18 23.54.57 localhost 7211da89cc19.png

なぜかというと、ページの最後に書いてある以下の設定が適用されて、上書きされた空のパスにアクセスしてしまうためです。

To replace this page, create a ./public/index.html file.

解決法

(((何事も暴力で解決するのが一番だ)))

output-palette.gif

-gでインストールしていれば、目指すファイルは以下のパスに入っています。
/Users/***/node_modules/json-server/public
なのでそれをぶっこ抜いて自分が静的ファイルを格納している場所に展開してしまえばいいのです。

middleware

json-serverにはmiddlewareのオプションもあります。
なので「特定のURLが要求されたらnode_modules内のファイルを読み込んで返す」middlewareを作れば返すこともできます。

gist

home.js
constfs=require('fs');constmoduleId="json-server";constserveUrl=["/","/script.js","/style.css","/favicon.ico",];module.exports=(request,response,next)=>{if(serveUrl.includes(request.url)){constfileName=request.url=="/"?"/index.html":request.url;constpath=require.resolve(moduleId).split(moduleId)[0]+moduleId+"/public"+fileName;fs.readFile(path,(err,data)=>{if(!err){response.end(data);}});return;}next();}

起動するときの引数に-mでこのmoduleを指定してやればOKです。

json-server --watch db.json --routes routes.json -m home.js

なんでか絵文字が大変なことになってますが動いてっからよォ!

まとめ

どちらの解決法も暴力で解決していることには変わりないので、よりスマートな暴力である最初の方法がよいと思います。殴ればわかる。

ちなみにわたしが考えるjson-serverのベストプラクティスとしては、下手にmoduleとして扱ったりせずに、CLIで運用してmiddlewareで細かいところを補うのがよいと思っています。
ただ、json-serverのレスポンスをフックして改造したい場合はどうもmiddlewareではできないように見えたので、結局わたしはガシガシ書いて改造する羽目になりました。

で、この記事の本題なんですけど同じ作者のhotelを使うとローカルサーバのssl化が簡単だよ! って書いてあるんですが、やり方よくわかんないので詳しい人書いといて!1

おしまい。

参考

今更使うJSON Server
json-server に、express ミドルウェアを指定するオプションが追加された
NodeJSのrequireがどのファイルを探索しているのかを調べる


  1. 名前のせいで検索してもホテルしか出てこないの困る 

Eclipse 2020-06 で、“Node.js version could not be determined” と言われる件

$
0
0

Eclipse 2020-06 で、“Node.js version could not be determined”

対象バージョンのnodeはインストールして、パスも通っているのに、eclipse起動時に下記の画像のメッセージが出る

Eclipseのバージョン

Eclipse IDE for Enterprise Java Developers (includes Incubating components)

Version: 2020-06 (4.16.0)

nodeのバージョン

$ node -v
v10.17.0

OS

スクリーンショット 2020-08-19 10.58.57.png

メッセージ

image.png

原因としては

ユーザーが指定したPATH(.bash_profileとかの)を利用せずにMacOSでプログラムを起動する方法が原因で、PATH環境変数にノードの実行可能場所を追加することを説明するメッセージが表示されて混乱を引き起こしていると思われます。

解決策

cd /Applications/Eclipse_2020-06.app/Contents/Eclipse
echo -Dorg.eclipse.wildwebdeveloper.nodeJSLocation=$(which node) >> eclipse.ini
eclipse.ini
--launcher.library../Eclipse/plugins/org.eclipse.equinox.launcher.cocoa.macosx.x86_64_1.1.1200.v20200508-1552-productorg.eclipse.epp.package.jee.product--launcher.defaultActionopenFile--launcher.appendVmargs-startup../Eclipse/plugins/org.eclipse.equinox.launcher_1.5.700.v20200207-2156.jar-vmargs-Djava.net.preferIPv4Stack=true-Dosgi.requiredJavaVersion=1.8-Dosgi.dataAreaRequiresExplicitInit=true-XX:+UseG1GC-XX:+UseStringDeduplication--add-modules=ALL-SYSTEM-XstartOnFirstThread-Dorg.eclipse.swt.internal.carbon.smallFonts-Xdock:icon=../Resources/Eclipse.icns-Xverify:none-javaagent:../Eclipse/dropins/MergeDoc/eclipse/plugins/jp.sourceforge.mergedoc.pleiades/pleiades.jar-javaagent:lombok.jar-Dorg.eclipse.wildwebdeveloper.nodeJSLocation=/Users/ユーザー名/.nvm/versions/node/v10.17.0/bin/node

フルパスで無いとダメでした。

Node.js Ⅱ

$
0
0

◆MySQLに接続しよう

const mysql = require('mysql'); →mysqlパッケージを読み込み

const connection = mysql.createConnection({
//データベース名、パスワード
});

◆クエリの実行

connection.query('クエリ', クエリ実行後の処理)と書くことで、
Node.jsからデータベースに対してクエリを実行することができます。

例)

connection.query(
'SELECT*FROM items', →クエリ
()=> {
クエリ後の実行処理
}
);

キャプチャ.PNG

例)実行後の処理

connection.query(
'SELECT*FROM items', →クエリ
(error,result)=> {
console.log(result);
res.render('index.ejs');
)
};

キャプチャ.PNG

第1引数のerrorにはクエリが失敗したときのエラー情報が、
第2引数のresultsにはクエリの実行結果(ここでは取得したメモ情報)が入ります。

◆取得した値の表示 EJSに値を渡す

connection.query(
'SELECT*FROM items', →クエリを実行
(error,result)=> {
console.log(result);
res.render('index.ejs',{items: results}); →オブジェクトを渡す
)
};

HTMLで
<% items.forEach((item)=>{ %>
}) ;

キャプチャ.PNG

◆フォームを使ったリクエスト

app.post('create/',(req,res)=>{
追加する処理
一覧を表示する処理
});

キャプチャ.PNG

◆フォーム作成方法

◆フォームに入力した値を入れる
キャプチャ.PNG

メモ追加部分を作成

①フォームの値を受け取る準備をしよう(1)

input要素にname属性を指定すると、下の図のようなオブジェクトの形で
情報がサーバーに送信されます。よってサーバー側では
req.body.name属性の値でフォームの値を取得できます。

キャプチャ.PNG

②フォームの値を受け取る準備をしよう(2)

app.use(express.urlencoded({extended: false}));

app.('/create',(req,res)=> {
console.log(req.body.itemName)→フォームの値を取得する

});

◆データベースに追加する

connection.query(
'INSERT INTO items(name)VALUE('とまと')',
(error,result)=> {
connection.query(
'SELECT*FROM items', →クエリを実行
(error,result)=> {
console.log(result);
res.render('index.ejs',{items: results}); →オブジェクトを渡す
)
};

◆フォームからの値をクエリに使うとき

connection.query(
'INSERT INTO items(name)VALUES(?)',
[req.body.itemName], →配列の要素が入る
(error,result)=> {
connection.query(
'SELECT*FROM items', →クエリを実行
(error,result)=> {
console.log(result);
res.render('index.ejs',{items: results}); →オブジェクトを渡す
)
};

◆リダイレクト

app.post('/create', (req, res) => {
connection.query(
'INSERT INTO items (name) VALUES (?)',
[req.body.itemName],
(error, results) => {
res.redirect('/index');
}
);
});

Node.js Ⅲ

$
0
0

◆削除機能をつける 削除ボタンの準備

①ルーティングを用意
app.post('/delete',(req,res) => {
処理
});

②削除ボタンを用意

③リダイレクト
app.post('/delete',(req,res) => {
res.redirect('/index');
});

◆削除の処理
①index.ejs


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

$
0
0

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

バックナンバー

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

【第2回】「みんなのポートフォリオまとめサイト」を作ります~REST API編~

成果物

https://minna.itsumen.com

リポジトリ

フロントエンド

https://github.com/yuzuru2/minna_frontend

バックエンド

https://github.com/yuzuru2/minna_backend

構成

フロントエンド: Netlify(月間100GBまで転送量無料)
バックエンド: Netlify Functions(月間125000回まで無料 いわゆるFaaS)
データベース: MongoDB Atlas(ストレージ512MBまで無料 いわゆるDaaS)
SNSログイン: Firebase Authentication(基本使い放題(例外あり))

ワイ 「今回はSNSログインTwitterのみです」

ログイン流れ

無題.png

フロントエンド ログインのモジュール

src/logic/util/loginButton.ts
import*asReactfrom'react';import*asfirebasefrom'firebase/app';import{store}from'src';importactionfrom'src/action/basic';importConstantfrom'src/constant';import{getHeaders}from'src/util';importgetJwtTokenfrom'src/firebase';// ツイッターログインexportdefault()=>{constprovider=newfirebase.auth.TwitterAuthProvider();firebase.auth().signInWithPopup(provider).then(asyncresult=>{store.dispatch(action.loadingFlag(true));store.dispatch(action.uid(awaitresult.user.uid));const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/create/user']}/`,{method:'post',headers:getHeaders(awaitgetJwtToken()),body:JSON.stringify({}),});if(_res.status!==200){alert('ログイン失敗');return;}location.reload();});};

バックエンド JwtTokenが改ざんされていないかチェックするモジュール

src/util/index.ts
import*asadminfrom'firebase-admin';import*asExpressfrom'express';import*asDotEnvfrom'dotenv';DotEnv.config();if(!admin.apps.length){admin.initializeApp({credential:admin.credential.cert(JSON.parse(process.env.FIREBASE_SDK)asany),});}exportconstgetUid=async(jwtToken:string)=>{const_decode=((awaitadmin.auth().verifyIdToken(jwtToken).catch(()=>{}))asunknown)as{uid:string};if(_decode===undefined){thrownewError('不正なトークン');}return_decode.uid;};exportconstgetParameter=async(req:Express.Request)=>{const_buf=Buffer.from(req.body,'utf-8');returnJSON.parse(_buf.toString('utf-8',0,_buf.length));};

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

$
0
0

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

バックナンバー

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

【第2回】「みんなのポートフォリオまとめサイト」を作ります~REST API編~

【第3回】「みんなのポートフォリオまとめサイト」を作ります~SNSログイン編~

成果物

https://minna.itsumen.com

リポジトリ

フロントエンド

https://github.com/yuzuru2/minna_frontend

バックエンド

https://github.com/yuzuru2/minna_backend

部品

store router

src/index.tsx
import*asReactfrom'react';import{render}from'react-dom';import{Provider}from'react-redux';import{Route,Switch}from'react-router';import{ConnectedRouter,connectRouter}from'connected-react-router';import{createStore,combineReducers}from'redux';import{createBrowserHistory}from'history';import'bootstrap/dist/css/bootstrap.min.css';import'babel-polyfill';importinitfrom'src/firebase';// reducerimportredcuer_basicfrom'src/reducer/basic';importredcuer_pagingfrom'src/reducer/paging';importredcuer_productfrom'src/reducer/product';importredcuer_profilefrom'src/reducer/profile';// componentimportinfofrom'src/component/info';importtypefrom'src/component/type';importsearchfrom'src/component/search';importprofilefrom'src/component/profile';consthistory=createBrowserHistory();exportconststore=createStore(combineReducers({router:connectRouter(history),basic:redcuer_basic,paging:redcuer_paging,product:redcuer_product,profile:redcuer_profile,}));constid=setInterval(()=>{if(!store.getState().basic.firebaseInitFlag){return;}render(<Providerstore={store}><ConnectedRouterhistory={history}><Switch><Routeexactpath={'/'}component={info}/>
<Routeexactpath={'/info/:num'}component={info}/>
<Routeexactpath={'/type/:id/:num'}component={type}/>
<Routeexactpath={'/search/:id/:num'}component={search}/>
<Routeexactpath={'/profile/:id/:num'}component={profile}/>
</Switch>
</ConnectedRouter>
</Provider>,
document.getElementById('app'));clearInterval(id);},250);// ブラウザバックでリロードwindow.addEventListener('popstate',e=>{window.location.reload();});init();

reducer

src/reducer/basic.ts
importConstantfrom'src/constant';importi_reducdrfrom'src/interface/reducer';constinitialState={uid:null,loadingFlag:true,firebaseInitFlag:false,};exportdefault(state=initialState,action:i_reducdr['basic'])=>{switch(action.type){caseConstant.reducer.basic.uid:return{...state,uid:action.uid};caseConstant.reducer.basic.loadingFlag:return{...state,loadingFlag:action.loadingFlag};caseConstant.reducer.basic.firebaseInitFlag:return{...state,firebaseInitFlag:action.firebaseInitFlag};default:returnstate;}};
src/reducer/paging.ts
importConstantfrom'src/constant';importi_reducdrfrom'src/interface/reducer';constinitialState={count:0,list:[],};exportdefault(state=initialState,action:i_reducdr['paging'])=>{switch(action.type){caseConstant.reducer.paging.count:return{...state,count:action.count};caseConstant.reducer.paging.list:return{...state,list:action.list};default:returnstate;}};
src/reducer/product.ts
importConstantfrom'src/constant';importi_reducdrfrom'src/interface/reducer';constinitialState={ptype:0,id:'',title:'',url:'',repo:'',};exportdefault(state=initialState,action:i_reducdr['product'])=>{switch(action.type){caseConstant.reducer.product.ptype:return{...state,ptype:action.ptype};caseConstant.reducer.product.id:return{...state,id:action.id};caseConstant.reducer.product.title:return{...state,title:action.title};caseConstant.reducer.product.url:return{...state,url:action.url};caseConstant.reducer.product.repo:return{...state,repo:action.repo};default:returnstate;}};
src/reducer/profile.ts
importConstantfrom'src/constant';importi_reducdrfrom'src/interface/reducer';constinitialState={name:'',twitterUrl:'',githubUrl:'',};exportdefault(state=initialState,action:i_reducdr['profile'])=>{switch(action.type){caseConstant.reducer.profile.name:return{...state,name:action.name};caseConstant.reducer.profile.twitterUrl:return{...state,twitterUrl:action.twitterUrl};caseConstant.reducer.profile.githubUrl:return{...state,githubUrl:action.githubUrl};default:returnstate;}};

action

src/action/basic.ts
importConstantfrom'src/constant';constaction={uid:(uid:string)=>{return{type:Constant.reducer.basic.uid,uid:uid};},loadingFlag:(loadingFlag:boolean)=>{return{type:Constant.reducer.basic.loadingFlag,loadingFlag:loadingFlag,};},firebaseInitFlag:(firebaseInitFlag:boolean)=>{return{type:Constant.reducer.basic.firebaseInitFlag,firebaseInitFlag:firebaseInitFlag,};},};exportdefaultaction;
src/action/paging.ts
importConstantfrom'src/constant';importi_reducerfrom'src/interface/reducer';constaction={count:(count:number)=>{return{type:Constant.reducer.paging.count,count:count};},list:(list:i_reducer['paging']['list'])=>{return{type:Constant.reducer.paging.list,list:list};},};exportdefaultaction;
src/action/product.ts
importConstantfrom'src/constant';constaction={ptype:(ptype:number)=>{return{type:Constant.reducer.product.ptype,ptype:ptype};},id:(id:string)=>{return{type:Constant.reducer.product.id,id:id};},title:(title:string)=>{return{type:Constant.reducer.product.title,title:title};},url:(url:string)=>{return{type:Constant.reducer.product.url,url:url};},repo:(repo:string)=>{return{type:Constant.reducer.product.repo,repo:repo};},};exportdefaultaction;
src/action/profile.ts
importConstantfrom'src/constant';constaction={name:(name:string)=>{return{type:Constant.reducer.profile.name,name:name};},twitterUrl:(twitterUrl:string)=>{return{type:Constant.reducer.profile.twitterUrl,twitterUrl:twitterUrl,};},githubUrl:(githubUrl:string)=>{return{type:Constant.reducer.profile.githubUrl,githubUrl:githubUrl,};},};exportdefaultaction;

component

src/component/info/index.ts
import*asReactfrom'react';import{useParams}from'react-router-dom';import{connect}from'react-redux';import{store}from'src';// componentimportLoagingfrom'src/component/util/loading';importHeaderfrom'src/component/util/header';importBreadcrumbfrom'src/component/info/breadcrumb';importSectionfrom'src/component/util/pagingList';importAsidefrom'src/component/util/aside';importLoginButtonfrom'src/component/util/loginButton';importModalfrom'src/component/util/productPost';importSearchfrom'src/component/util/search';importPaginationfrom'src/component/info/pagination';importTypefrom'src/component/util/type';// logicimportfirstfrom'src/logic/info/first';exportdefaultconnect(store=>store)(()=>{constparams:{num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);/**
   * componentDidMount
   */React.useEffect(()=>{(async()=>{awaitfirst(_pageNum);})();},[]);if(store.getState().basic.loadingFlag){return<Loaging/>;}return(<><Header/><Breadcrumb/><divstyle={{textAlign:'center',marginTop:15}}>{store.getState().basic.uid!==null?<Modal/>:<LoginButton/>}<Type/></div>
<divstyle={{textAlign:'center',marginTop:15}}><Search/></div>
<mainclassName="row"><Sectionpage={Pagination}/>
<Aside/></main>
</>
);});
src/component/info/breadcrumb.tsx
import*asReactfrom'react';import{store}from'src';exportdefault()=>{returnReact.useMemo(()=>{return(<><divaria-label="パンくずリスト"><olclassName="breadcrumb mb-1"><liclassName="breadcrumb-item"><ahref="/">ホーム</a>
</li>
<liclassName="breadcrumb-item active">全投稿</li>
<liclassName="breadcrumb-item active">{store.getState().paging.count}</li>
</ol>
</div>
</>
);},[location.href]);};
src/component/info/pagination.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';importPaginationfrom'react-js-pagination';import{store}from'src';importConstantfrom'src/constant';exportdefault()=>{constparams:{num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);returnReact.useMemo(()=>{return(<><divclassName="d-flex justify-content-center"><PaginationactivePage={_pageNum}itemsCountPerPage={Constant.pagingNum}totalItemsCount={store.getState().paging.count}onChange={async(num:number)=>(location.href=`/info/${num}`)}itemClass="page-item"linkClass="page-link"/></div>
</>
);},[location.href]);};
src/component/profile/index.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';import{connect}from'react-redux';import{store}from'src';// componentimportLoagingfrom'src/component/util/loading';importHeaderfrom'src/component/util/header';importBreadcrumbfrom'src/component/profile/breadcrumb';importSectionfrom'src/component/util/pagingList';importAsidefrom'src/component/util/aside';importPaginationfrom'src/component/profile/pagination';importUserfrom'src/component/profile/user';importfirstfrom'src/logic/profile/first';exportdefaultconnect(store=>store)(()=>{constparams:{id:string;num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);/**
   * componentDidMount
   */React.useEffect(()=>{(async()=>{awaitfirst(params.id,_pageNum);})();},[]);if(store.getState().basic.loadingFlag){return<Loaging/>;}return(<><Header/><Breadcrumb/><divstyle={{textAlign:'center',marginTop:15}}><User/><hr/></div>
<br/><mainclassName="row"><Sectionpage={Pagination}/>
<Aside/></main>
</>
);});
src/component/profile/breadcrumb.tsx
import*asReactfrom'react';import{store}from'src';exportdefault()=>{returnReact.useMemo(()=>{return(<><divaria-label="パンくずリスト"><olclassName="breadcrumb mb-1"><liclassName="breadcrumb-item"><ahref="/">ホーム</a>
</li>
<liclassName="breadcrumb-item active">ユーザ</li>
<liclassName="breadcrumb-item active">{store.getState().profile.name}</li>
</ol>
</div>
</>
);},[location.href]);};
src/component/profile/pagination.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';importPaginationfrom'react-js-pagination';import{store}from'src';importConstantfrom'src/constant';exportdefault()=>{constparams:{id:string;num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);returnReact.useMemo(()=>{return(<><divclassName="d-flex justify-content-center"><PaginationactivePage={_pageNum}itemsCountPerPage={Constant.pagingNum}totalItemsCount={store.getState().paging.count}onChange={async(num:number)=>(location.href=`/profile/${params.id}/${num}`)}itemClass="page-item"linkClass="page-link"/></div>
</>
);},[location.href]);};
src/component/profile/user.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';import{Modal,Button}from'react-bootstrap';importcopyfrom'copy-to-clipboard';import{store}from'src';importactionfrom'src/action/profile';importlogicfrom'src/logic/profile/userUpdate';exportdefault()=>{constparams:{id:string}=useParams();const[showModal,setshowModal]=React.useState(false);returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-secondary"onClick={()=>setshowModal(true)}>プロフィール</button>
<Modalshow={showModal}onHide={()=>setshowModal(false)}><Modal.HeadercloseButton><h5>プロフィール</h5>
</Modal.Header>
<Modal.Body><divclassName="form-group"><label>ニックネーム</label>
<inputreadOnly={store.getState().basic.uid!==params.id}className="form-control"placeholder="ニックネーム"maxLength={15}value={store.getState().profile.name}onChange={e=>store.dispatch(action.name(e.target.value))}/>
</div>
<divclassName="form-group"><label>GitHub</label>
<inputtype="url"readOnly={store.getState().basic.uid!==params.id}className="form-control"placeholder="GithubのURL"maxLength={100}value={store.getState().profile.githubUrl}onChange={e=>store.dispatch(action.githubUrl(e.target.value))}onClick={()=>{if(store.getState().basic.uid!==params.id){copy(store.getState().profile.githubUrl);alert('コピーしました');}}}/>
</div>
<divclassName="form-group"><label>Twitter</label>
<inputtype="url"readOnly={store.getState().basic.uid!==params.id}className="form-control"placeholder="TwitterのURL"maxLength={100}value={store.getState().profile.twitterUrl}onChange={e=>store.dispatch(action.twitterUrl(e.target.value))}onClick={()=>{if(store.getState().basic.uid!==params.id){copy(store.getState().profile.twitterUrl);alert('コピーしました');}}}/>
</div>
</Modal.Body>
<Modal.Footer>{store.getState().basic.uid===params.id?(<ButtononClick={async()=>awaitlogic()}>変更</Button>
):('')}</Modal.Footer>
</Modal>
</>
);},[showModal,JSON.stringify(store.getState().profile)]);};
src/component/search/index.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';import{connect}from'react-redux';import{store}from'src';// componentimportLoagingfrom'src/component/util/loading';importHeaderfrom'src/component/util/header';importBreadcrumbfrom'src/component/search/breadcrumb';importSectionfrom'src/component/util/pagingList';importAsidefrom'src/component/util/aside';importLoginButtonfrom'src/component/util/loginButton';importModalfrom'src/component/util/productPost';importSearchfrom'src/component/util/search';importPaginationfrom'src/component/search/pagination';importTypefrom'src/component/util/type';importfirstfrom'src/logic/search/first';exportdefaultconnect(store=>store)(()=>{constparams:{id:string;num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);/**
   * componentDidMount
   */React.useEffect(()=>{(async()=>{awaitfirst(params.id,_pageNum);})();},[]);if(store.getState().basic.loadingFlag){return(<><Loaging/></>
);}return(<><Header/><Breadcrumb/><divstyle={{textAlign:'center',marginTop:15}}>{store.getState().basic.uid!==null?<Modal/>:<LoginButton/>}<Type/></div>
<divstyle={{textAlign:'center',marginTop:15}}><Search/></div>
<mainclassName="row"><Sectionpage={Pagination}/>
<Aside/></main>
</>
);});
src/component/search/breadcrumb.tsx
import*asReactfrom'react';import{store}from'src';import{useParams}from'react-router-dom';exportdefault()=>{constparams:{id:string}=useParams();returnReact.useMemo(()=>{return(<><divaria-label="パンくずリスト"><olclassName="breadcrumb mb-1"><liclassName="breadcrumb-item"><ahref="/">ホーム</a>
</li>
<liclassName="breadcrumb-item active">検索</li>
<liclassName="breadcrumb-item active">{params.id}</li>
<liclassName="breadcrumb-item active">{store.getState().paging.count}</li>
</ol>
</div>
</>
);},[location.href]);};
src/component/search/pagination.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';importPaginationfrom'react-js-pagination';import{store}from'src';importConstantfrom'src/constant';exportdefault()=>{constparams:{id:string;num:string}=useParams();returnReact.useMemo(()=>{return(<><divclassName="d-flex justify-content-center"><PaginationactivePage={Number(params.num)}itemsCountPerPage={Constant.pagingNum}totalItemsCount={store.getState().paging.count}onChange={async(num:number)=>(location.href=`/search/${params.id}/${num}`)}itemClass="page-item"linkClass="page-link"/></div>
</>
);},[location.href]);};
src/component/type/index.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';import{connect}from'react-redux';import{store}from'src';// componentimportLoagingfrom'src/component/util/loading';importHeaderfrom'src/component/util/header';importBreadcrumbfrom'src/component/type/breadcrumb';importSectionfrom'src/component/util/pagingList';importAsidefrom'src/component/util/aside';importLoginButtonfrom'src/component/util/loginButton';importModalfrom'src/component/util/productPost';importSearchfrom'src/component/util/search';importPaginationfrom'src/component/type/pagination';importTypefrom'src/component/util/type';importfirstfrom'src/logic/type/first';exportdefaultconnect(store=>store)(()=>{constparams:{id:string;num:string}=useParams();const_pageNum=params.num===undefined?1:Number(params.num);/**
   * componentDidMount
   */React.useEffect(()=>{(async()=>{awaitfirst(params.id,_pageNum);})();},[]);if(store.getState().basic.loadingFlag){return(<><Loaging/></>
);}return(<><Header/><Breadcrumb/><divstyle={{textAlign:'center',marginTop:15}}>{store.getState().basic.uid!==null?<Modal/>:<LoginButton/>}<Type/></div>
<divstyle={{textAlign:'center',marginTop:15}}><Search/></div>
<mainclassName="row"><Sectionpage={Pagination}/>
<Aside/></main>
</>
);});
src/component/type/breadcrumb.tsx
import*asReactfrom'react';import{store}from'src';importConstantfrom'src/constant';import{useParams}from'react-router-dom';exportdefault()=>{constparams:{id:string}=useParams();returnReact.useMemo(()=>{return(<><divaria-label="パンくずリスト"><olclassName="breadcrumb mb-1"><liclassName="breadcrumb-item"><ahref="/">ホーム</a>
</li>
<liclassName="breadcrumb-item active">タイプ</li>
<liclassName="breadcrumb-item active">{Constant.type[params.id]}</li>
<liclassName="breadcrumb-item active">{store.getState().paging.count}</li>
</ol>
</div>
</>
);},[location.href]);};
src/component/type/pagination.tsx
import*asReactfrom'react';import{useParams}from'react-router-dom';importPaginationfrom'react-js-pagination';import{store}from'src';importConstantfrom'src/constant';exportdefault()=>{constparams:{id:string;num:string}=useParams();returnReact.useMemo(()=>{return(<><divclassName="d-flex justify-content-center"><PaginationactivePage={Number(params.num)}itemsCountPerPage={Constant.pagingNum}totalItemsCount={store.getState().paging.count}onChange={async(num:number)=>(location.href=`/type/${params.id}/${num}`)}itemClass="page-item"linkClass="page-link"/></div>
</>
);},[location.href]);};
src/component/util/aside.tsx
import*asReactfrom'react';exportdefault()=>{returnReact.useMemo(()=>{return(<><aside><divstyle={{textAlign:'right'}}><ahref="https://www.youtube.com/channel/UCuRrjmWcjASMgl5TqHS02AQ/videos"><imgsrc="/img/1.jpg"width="70%"/></a>
<br/><br/><ahref="https://twitter.com/yuzuru_program"><imgsrc="/img/2.jpg"width="70%"/></a>
<br/><br/><ahref="https://yuzuru.itsumen.com"><imgsrc="/img/3.jpg"width="70%"/></a>
<br/><br/><ahref="https://code.itsumen.com"><imgsrc="/img/4.jpg"width="70%"/></a>
<br/><br/><ahref="https://board.itsumen.com"><imgsrc="/img/5.jpg"width="70%"/></a>
<br/><br/><ahref="https://nuxtchat.itsumen.com"><imgsrc="/img/6.jpg"width="70%"/></a>
<br/><br/></div>
</aside>
</>
);},[location.href]);};
src/component/util/header.tsx
import*asReactfrom'react';importConstantfrom'src/constant';import{store}from'src';import{Navbar}from'react-bootstrap';importLoginButtonfrom'src/component/util/loginButton';importLogoutButtonfrom'src/component/util/logoutButton';exportdefault()=>{returnReact.useMemo(()=>{return(<><headerstyle={{background:'rgb(255, 141, 153)'}}><Navbarexpand={false}><ahref="/"style={{fontSize:15,color:'#fff'}}>{Constant.tile}</a>
<Navbar.Togglearia-controls="basic-navbar-nav"aria-expanded="false"/><Navbar.Collapse><ulclassName="nav navbar-nav ml-auto">{store.getState().basic.uid===null?(<listyle={{textAlign:'center'}}><LoginButton/></li>
):(<><br/><listyle={{textAlign:'center'}}><buttontype="button"className="btn btn-info"onClick={()=>(location.href=`/profile/${store.getState().basic.uid}/1`)}>マイページ</button>
</li>
<br/><listyle={{textAlign:'center'}}><LogoutButton/></li>
</>
)}</ul>
</Navbar.Collapse>
</Navbar>
</header>
</>
);},[store.getState().basic.uid]);};
src/component/util/loading.tsx
import*asReactfrom'react';exportdefault()=>{return(<><div><divclassName="position-absolute h-100 w-100 m-0 d-flex align-items-center justify-content-center"><divclassName="spinner-border text-primary"role="status"><spanclassName="sr-only">Loading...</span>
</div>
</div>
</div>
</>
);};
src/component/util/loginButton.tsx
import*asReactfrom'react';import{store}from'src';importlogicfrom'src/logic/util/loginButton';exportdefault()=>{returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-primary"onClick={()=>logic()}>ログイン</button>
</>
);},[store.getState().basic.uid]);};
src/component/util/logoutButton.tsx
import*asReactfrom'react';import{store}from'src';importlogicfrom'src/logic/util/logoutButton';exportdefault()=>{returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-danger"onClick={()=>logic()}>ログアウト</button>
</>
);},[store.getState().basic.uid]);};
src/component/util/pagingList.tsx
import*asReactfrom'react';importmomentfrom'moment';moment.locale('ja');importConstantfrom'src/constant';import{store}from'src';importi_reducerfrom'src/interface/reducer';importProductUpdatefrom'src/component/util/productUpdate';importlogicfrom'src/logic/util/productDelete';exportdefault(params:{page:React.SFC})=>{returnReact.useMemo(()=>{constlist:i_reducer['paging']['list']=store.getState().paging.list;return(<><section><ul>{list.map((m,i)=>{return(<listyle={{paddingLeft:15,marginTop:15}}key={i}><divclassName="card"><divclassName="card-body">{/* タイトル */}<divclassName="form-group"><label>タイトル</label>
<br/><p>{m.title}</p>
</div>
<hr/>{/* URL */}<divclassName="form-group"><label>URL</label>
<br/><ahref={m.url}>{m.url}</a>
</div>
<hr/>{/* タイプ */}<divclassName="form-group"><label>タイプ</label>
<br/><ahref={`${location.protocol}//${location.host}/type/${m.type}/1`}>{Constant.type[m.type]}</a>
</div>
<hr/>{/* リポジトリ */}<divclassName="form-group"><label>リポジトリ</label>
<br/><ahref={m.repo}>{m.repo}</a>
</div>
<hr/>{/* ニックネーム */}<divclassName="form-group"><label>ニックネーム</label>
<br/><ahref={`/profile/${m.uid}/1`}>{m.name[0]}</a>
</div>
<hr/>{/* 投稿・更新日 */}<divclassName="form-group"style={{textAlign:'right'}}><time>投稿:{''}{moment(m.createdAt).format('YYYY-MM-DD HH:mm:ss')}</time>
<br/><time>更新:{''}{moment(m.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
</div>
{/* 編集・削除 */}<divclassName="form-group"style={{textAlign:'right'}}>{store.getState().basic.uid===m.uid?(<><ProductUpdateid={m._id}title={m.title}url={m.url}repo={m.repo}ptype={m.type}/>
<br/><br/><buttontype="button"className="btn btn-danger"onClick={()=>{confirm('削除しますか')?logic(m._id):'';}}>削除</button>
</>
):('')}</div>
</div>
</div>
</li>
);})}</ul>
<params.page/></section>
</>
);},[JSON.stringify(store.getState().paging.list),JSON.stringify(store.getState().product),]);};
src/component/util/productPost.tsx
import*asReactfrom'react';import{Modal,Button}from'react-bootstrap';import{store}from'src';importConstantfrom'src/constant';importactionfrom'src/action/product';importlogicfrom'src/logic/util/productPost';exportdefault()=>{const[showModal,setshowModal]=React.useState(false);returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-success"onClick={()=>{store.dispatch(action.id(''));store.dispatch(action.title(''));store.dispatch(action.url(''));store.dispatch(action.repo(''));store.dispatch(action.ptype(0));setshowModal(true);}}>投稿</button>
<Modalshow={showModal}onHide={()=>setshowModal(false)}><Modal.HeadercloseButton><h5>投稿</h5>
</Modal.Header>
<Modal.Body><divclassName="form-group"><label>タイトル</label>
<textareaclassName="form-control"rows={3}placeholder="30文字内"maxLength={30}value={store.getState().product.title}onChange={e=>store.dispatch(action.title(e.target.value))}></textarea>
</div>
<divclassName="form-group"><label>URL</label>
<inputtype="url"className="form-control"placeholder="成果物URL"maxLength={100}value={store.getState().product.url}onChange={e=>store.dispatch(action.url(e.target.value))}/>
</div>
<divclassName="form-group"><label>リポジトリ</label>
<inputtype="url"className="form-control"placeholder="リポジトリURL"maxLength={100}value={store.getState().product.repo}onChange={e=>store.dispatch(action.repo(e.target.value))}/>
</div>
<divclassName="form-group"><label>タイプ</label>
<selectclassName="form-control"value={store.getState().product.ptype}onChange={e=>store.dispatch(action.ptype(Number(e.target.value)))}>{Object.keys(Constant.type).map(key=>{return(<optionkey={key}value={key}>{Constant.type[key]}</option>
);})}</select>
</div>
</Modal.Body>
<Modal.Footer><ButtononClick={async()=>confirm('投稿してもよいですか?')?awaitlogic():''}>投稿</Button>
</Modal.Footer>
</Modal>
</>
);},[showModal,JSON.stringify(store.getState().product)]);};
src/component/util/productUpdate.tsx
import*asReactfrom'react';import{Modal,Button}from'react-bootstrap';import{store}from'src';importConstantfrom'src/constant';importactionfrom'src/action/product';importlogicfrom'src/logic/util/productUpdate';exportdefault(params:{id:string;title:string;url:string;repo:string;ptype:number;})=>{const[showModal,setshowModal]=React.useState(false);returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-success"onClick={()=>{store.dispatch(action.id(params.id));store.dispatch(action.title(params.title));store.dispatch(action.url(params.url));store.dispatch(action.repo(params.repo));store.dispatch(action.ptype(Number(params.ptype)));setshowModal(true);}}>変更</button>
<Modalshow={showModal}onHide={()=>setshowModal(false)}><Modal.HeadercloseButton><h5>変更</h5>
</Modal.Header>
<Modal.Body><divclassName="form-group"><label>タイトル</label>
<textareaclassName="form-control"rows={3}placeholder="30文字内"maxLength={30}value={store.getState().product.title}onChange={e=>store.dispatch(action.title(e.target.value))}></textarea>
</div>
<divclassName="form-group"><label>URL</label>
<inputtype="url"className="form-control"placeholder="成果物URL"maxLength={100}value={store.getState().product.url}onChange={e=>store.dispatch(action.url(e.target.value))}/>
</div>
<divclassName="form-group"><label>リポジトリ</label>
<inputtype="url"className="form-control"placeholder="リポジトリURL"maxLength={100}value={store.getState().product.repo}onChange={e=>store.dispatch(action.repo(e.target.value))}/>
</div>
<divclassName="form-group"><label>タイプ</label>
<selectclassName="form-control"value={store.getState().product.ptype}onChange={e=>store.dispatch(action.ptype(Number(e.target.value)))}>{Object.keys(Constant.type).map(key=>{return(<optionkey={key}value={key}>{Constant.type[key]}</option>
);})}</select>
</div>
</Modal.Body>
<Modal.Footer><ButtononClick={async()=>awaitlogic()}>変更</Button>
</Modal.Footer>
</Modal>
</>
);},[showModal,JSON.stringify(store.getState().product)]);};
src/component/util/search.tsx
import*asReactfrom'react';exportdefault()=>{const[text,setText]=React.useState('');returnReact.useMemo(()=>{return(<><divclassName="mx-auto"style={{maxWidth:'300px'}}><divclassName="input-group"><inputtype="text"className="form-control"placeholder="検索"maxLength={15}onChange={e=>setText(e.target.value)}onKeyPress={e=>{if(e.key=='Enter'){text.length===0?(location.href='/'):(location.href=`${location.protocol}//${location.host}/search/${text}/1`);}}}/>
<divclassName="input-group-append"><buttonclassName="btn btn-info"type="button"onClick={()=>text.length===0?(location.href='/'):(location.href=`${location.protocol}//${location.host}/search/${text}/1`)}><iclassName="fa fa-search"></i>
</button>
</div>
</div>
</div>
</>
);},[text]);};
src/component/util/type.tsx
import*asReactfrom'react';import{Modal,Button}from'react-bootstrap';import{store}from'src';importConstantfrom'src/constant';exportdefault()=>{const[showModal,setshowModal]=React.useState(false);returnReact.useMemo(()=>{return(<><buttontype="button"className="btn btn-warning"style={{marginLeft:15}}onClick={()=>setshowModal(true)}>ジャンル検索</button>
<Modalshow={showModal}onHide={()=>setshowModal(false)}><Modal.HeadercloseButton><h5>ジャンル検索</h5>
</Modal.Header>
<Modal.Body>{Object.keys(Constant.type).map(key=>{return(<divclassName="form-group"key={key}><ahref={`/type/${key}/1`}>{Constant.type[key]}</a>
</div>
);})}</Modal.Body>
<Modal.Footer><ButtononClick={async()=>setshowModal(false)}>閉じる</Button>
</Modal.Footer>
</Modal>
</>
);},[showModal,JSON.stringify(store.getState().product)]);};###constant```src/constant/index.ts
let reducerCount = 0;

export default class {
  static readonly tile = 'みんなのポートフォリオ';

  static readonly api_url = {
    development: `http://localhost:9000/.netlify/functions/api/v1`,production:`https://eager-spence-3a3400.netlify.app/.netlify/functions/api/v1`,};staticreadonlyreducer={basic:{uid:String(reducerCount++),loadingFlag:String(reducerCount++),firebaseInitFlag:String(reducerCount++),},profile:{name:String(reducerCount++),twitterUrl:String(reducerCount++),githubUrl:String(reducerCount++),},product:{ptype:String(reducerCount++),id:String(reducerCount++),title:String(reducerCount++),url:String(reducerCount++),repo:String(reducerCount++),},paging:{count:String(reducerCount++),list:String(reducerCount++),},};staticreadonlyurl={['/create/friend']:'/create/friend',['/create/user']:'/create/user',['/create/product']:'/create/product',['/paging/all']:'/paging/all',['/paging/title']:'/paging/title',['/paging/type']:'/paging/type',['/paging/user']:'/paging/user',['/update/user']:'/update/user',['/update/product']:'/update/product',['/cancel/friend']:'/cancel/friend',['/cancel/product']:'/cancel/product',['/find/user']:'/find/user',};staticreadonlyfirebase_config={apiKey:'AIzaSyA1Z1xnyt5-cp3YXNcyMzR30a2oh5zWaR4',authDomain:'minna-eee2e.firebaseapp.com',databaseURL:'https://minna-eee2e.firebaseio.com',projectId:'minna-eee2e',storageBucket:'minna-eee2e.appspot.com',messagingSenderId:'861193838038',appId:'1:861193838038:web:97f1254de063da49221877',measurementId:'G-ZYXZCK1RDS',};staticreadonlytype={0:'Webアプリ',1:'スマホアプリ',2:'デスクトップアプリ',3:'スクレイピング',4:'ホムペ',5:'その他',};staticreadonlypagingNum=5;constructor(){thrownewError('new禁止');}}

firebase

src/firebase/index.ts
import*asfirebasefrom'firebase/app';import'firebase/auth';importConstantfrom'src/constant';importactionfrom'src/action/basic';import{store}from'src';firebase.initializeApp(Constant.firebase_config);firebase.auth().onAuthStateChanged(asyncdata=>{if(!store.getState().basic.firebaseInitFlag){store.dispatch(action.firebaseInitFlag(true));}if(data===null){store.dispatch(action.uid(null));return;}store.dispatch(action.uid(data.uid));});exportdefaultasync()=>{try{returnawaitfirebase.auth().currentUser.getIdToken(true);}catch(e){return'';}};

interface

src/interface/reducer.ts
interfacereducer{home:{type:string;name:string;count:number;};basic:{type:string;uid:string;loadingFlag:boolean;firebaseInitFlag:boolean;};profile:{type:string;name:string;twitterUrl:string;githubUrl:string;};product:{type:string;id:string;ptype:number;title:string;url:string;repo:string;};paging:{type:string;count:number;list:{_id:string;type:number;title:string;url:string;repo:string;name:string[];uid:string;createdAt:Date;updatedAt:Date;}[];};}exportdefaultreducer;

logic

src/logic/info/first.ts
importConstantfrom'src/constant';import{getHeaders}from'src/util';import{store}from'src';importbasic_actionfrom'src/action/basic';importpaging_actionfrom'src/action/paging';importi_reducerfrom'src/interface/reducer';importgetJwtTokenfrom'src/firebase';exportdefaultasync(num:number)=>{const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/paging/all']}/${num-1}`,{method:'get',headers:getHeaders(awaitgetJwtToken()),});if(_res.status!==200){return;}const_json:i_reducer['paging']=await_res.json();store.dispatch(paging_action.count(_json.count));store.dispatch(paging_action.list(_json.list));store.dispatch(basic_action.loadingFlag(false));};
src/logic/profile/first.ts
importConstantfrom'src/constant';import{getHeaders}from'src/util';import{store}from'src';importbasic_actionfrom'src/action/basic';importpaging_actionfrom'src/action/paging';importprofile_actionfrom'src/action/profile';importi_reducerfrom'src/interface/reducer';importgetJwtTokenfrom'src/firebase';exportdefaultasync(id:string,num:number)=>{// プロフィールconst_profile=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/find/user']}/${id}`,{method:'get',headers:getHeaders(awaitgetJwtToken()),});if(_profile.status!==200){return;}const_profile_json:i_reducer['profile'][]=await_profile.json();if(_profile_json.length===0){location.href='/';return;}store.dispatch(profile_action.name(_profile_json[0].name));store.dispatch(profile_action.githubUrl(_profile_json[0].githubUrl));store.dispatch(profile_action.twitterUrl(_profile_json[0].twitterUrl));// 投稿const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/paging/user']}/${id}/${num-1}`,{method:'get',headers:getHeaders(awaitgetJwtToken()),});if(_res.status!==200){return;}const_json:i_reducer['paging']=await_res.json();store.dispatch(paging_action.count(_json.count));store.dispatch(paging_action.list(_json.list));store.dispatch(basic_action.loadingFlag(false));};
src/logic/profile/userUpdate.ts
importvalidatorfrom'validator';import{store}from'src';importConstantfrom'src/constant';import{getHeaders}from'src/util';importgetJwtTokenfrom'src/firebase';// プロフィール変更exportdefaultasync()=>{constvalidate=()=>{if(store.getState().profile.name.length===0){alert('ニックネームが入力されていません');returnfalse;}if(!validator.isURL(store.getState().profile.githubUrl,{protocols:['http','https'],require_protocol:true,})){if(store.getState().profile.githubUrl.length!==0){alert('GitHubのURLが不正です');returnfalse;}}if(!validator.isURL(store.getState().profile.twitterUrl,{protocols:['http','https'],require_protocol:true,})){if(store.getState().profile.twitterUrl.length!==0){alert('TwitterのURLが不正です');returnfalse;}}returntrue;};if(!validate()){return;}const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/update/user']}`,{method:'put',headers:getHeaders(awaitgetJwtToken()),body:JSON.stringify({name:store.getState().profile.name,githubUrl:store.getState().profile.githubUrl,twitterUrl:store.getState().profile.twitterUrl,}),});if(_res.status!==200){alert('更新に失敗しました');return;}alert('変更しました');location.href=`/profile/${store.getState().basic.uid}/1`;};
src/logic/search/first.ts
importConstantfrom'src/constant';import{getHeaders}from'src/util';import{store}from'src';importbasic_actionfrom'src/action/basic';importpaging_actionfrom'src/action/paging';importi_reducerfrom'src/interface/reducer';importgetJwtTokenfrom'src/firebase';exportdefaultasync(id:string,num:number)=>{const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/paging/title']}/${id}/${num-1}`,{method:'get',headers:getHeaders(awaitgetJwtToken()),});if(_res.status!==200){return;}const_json:i_reducer['paging']=await_res.json();store.dispatch(paging_action.count(_json.count));store.dispatch(paging_action.list(_json.list));store.dispatch(basic_action.loadingFlag(false));};
src/logic/type/first.ts
importConstantfrom'src/constant';import{getHeaders}from'src/util';import{store}from'src';importbasic_actionfrom'src/action/basic';importpaging_actionfrom'src/action/paging';importi_reducerfrom'src/interface/reducer';importgetJwtTokenfrom'src/firebase';exportdefaultasync(id:string,num:number)=>{const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/paging/type']}/${id}/${num-1}`,{method:'get',headers:getHeaders(awaitgetJwtToken()),});if(_res.status!==200){return;}const_json:i_reducer['paging']=await_res.json();store.dispatch(paging_action.count(_json.count));store.dispatch(paging_action.list(_json.list));store.dispatch(basic_action.loadingFlag(false));};
src/logic/util/loginButton.ts
import*asReactfrom'react';import*asfirebasefrom'firebase/app';import{store}from'src';importactionfrom'src/action/basic';importConstantfrom'src/constant';import{getHeaders}from'src/util';importgetJwtTokenfrom'src/firebase';// ツイッターログインexportdefault()=>{constprovider=newfirebase.auth.TwitterAuthProvider();firebase.auth().signInWithPopup(provider).then(asyncresult=>{store.dispatch(action.loadingFlag(true));store.dispatch(action.uid(awaitresult.user.uid));const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/create/user']}/`,{method:'post',headers:getHeaders(awaitgetJwtToken()),body:JSON.stringify({}),});if(_res.status!==200){alert('ログイン失敗');return;}location.reload();});};
src/logic/util/logoutButton.ts
import*asfirebasefrom'firebase/app';import{store}from'src';importactionfrom'src/action/basic';exportdefault()=>{if(!confirm('ログアウトしますか?')){return;}store.dispatch(action.loadingFlag(true));firebase.auth().signOut().then(()=>{location.href='/';}).catch(error=>{alert('エラーが発生しました');});};
src/logic/util/productDelete.ts
import{store}from'src';importConstantfrom'src/constant';import{getHeaders}from'src/util';importactionfrom'src/action/basic';importgetJwtTokenfrom'src/firebase';// 投稿削除exportdefaultasync(id:string)=>{store.dispatch(action.loadingFlag(true));const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/cancel/product']}`,{method:'delete',headers:getHeaders(awaitgetJwtToken()),body:JSON.stringify({id:id,}),});if(_res.status!==200){alert('削除に失敗しました');return;}alert('削除しました');location.reload();};
src/logic/util/productPost.ts
importvalidatorfrom'validator';import{store}from'src';importConstantfrom'src/constant';import{getHeaders}from'src/util';importactionfrom'src/action/basic';importgetJwtTokenfrom'src/firebase';// 投稿exportdefaultasync()=>{constvalidate=()=>{if(store.getState().product.title.length===0){alert('タイトルが入力されていません');returnfalse;}if(store.getState().product.url.length===0){alert('URLが入力されていません');returnfalse;}if(!validator.isURL(store.getState().product.url,{protocols:['http','https'],require_protocol:true,})){alert('URLが不正です');returnfalse;}if(!validator.isURL(store.getState().product.repo,{protocols:['http','https'],require_protocol:true,})){if(store.getState().product.repo.length!==0){alert('リポジトリURLが不正です');returnfalse;}}returntrue;};store.dispatch(action.loadingFlag(true));if(!validate()){store.dispatch(action.loadingFlag(false));return;}const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/create/product']}`,{method:'post',headers:getHeaders(awaitgetJwtToken()),body:JSON.stringify({title:store.getState().product.title,url:store.getState().product.url,repo:store.getState().product.repo,type:store.getState().product.ptype,}),});if(_res.status!==200){alert('投稿に失敗しました');return;}location.href='/';};
src/logic/util/productUpdate.ts
importvalidatorfrom'validator';import{store}from'src';importConstantfrom'src/constant';import{getHeaders}from'src/util';importgetJwtTokenfrom'src/firebase';// 投稿exportdefaultasync()=>{constvalidate=()=>{if(store.getState().product.title.length===0){alert('タイトルが入力されていません');returnfalse;}if(store.getState().product.url.length===0){alert('タイトルが入力されていません');returnfalse;}if(!validator.isURL(store.getState().product.url,{protocols:['http','https'],require_protocol:true,})){alert('URLが不正です');returnfalse;}if(!validator.isURL(store.getState().product.repo,{protocols:['http','https'],require_protocol:true,})){if(store.getState().product.repo.length!==0){alert('リポジトリURLが不正です');returnfalse;}}returntrue;};if(!validate()){return;}const_res=awaitfetch(`${Constant.api_url[process.env.NODE_ENV]}${Constant.url['/update/product']}`,{method:'put',headers:getHeaders(awaitgetJwtToken()),body:JSON.stringify({id:store.getState().product.id,title:store.getState().product.title,url:store.getState().product.url,repo:store.getState().product.repo,type:store.getState().product.ptype,}),});if(_res.status!==200){alert('変更に失敗しました');return;}alert('変更しました');location.reload();};

util

src/util/index.ts
exportconstgetHeaders=(jwtToken:string)=>{return{Authorization:jwtToken,'Content-Type':'application/json',};};

css

src/css/style.css
body{margin:0;}/* bootstrapの謎の右の余白を消す */#app{overflow:hidden;}li{list-style:none;}main{background:rgb(255,250,250);}section{width:70%;}aside{width:30%;}/* スマホ用 */@mediascreenand(max-width:768px){section{width:95%;padding-left:0;}sectionul{padding-left:5px;}aside{display:none;width:0%;}}

npmのライブラリの比較と評価方法:npm trends & Github

$
0
0

人気のあるライブラリをnpm trendsで探す

 npm trendsは、指定したライブラリのカテゴリと近いものを提示し、選択するとダウンロード数を比較してくれます。
 たとえば、dom parserを比較すると以下の用になります。

npm trends: dom parserの比較

screencapture-npmtrends-dom-parser-vs-cheerio-vs-htmlparser2-vs-parse5-vs-scraper-vs-xml2js-vs-xml-js-vs-fast-xml-parser-vs-xml2json-vs-jsdom-2020-08-20-07_45_01.png

とりあえず、ネットで一つライブラリを探してきて、npm trendsに入れると、候補を提示してくれるので、それでダウンロード数を比較をします。

npm trendsで提示されたライブラリのGithub上でのstarチェック

npm trendsのグラフだけでは、人気を計るのは不十分です。Githubのstarもチェックします。
例えば、上のdom parserの例だと、parse5が一番多いですが、starの数は、cheerioの方が多いのです。

  • parse5 : start 2.5k
    image.png

  • cheerio:start 22.4k

image.png

google → npm trends → Githubの順でライブラリの調査

 タイトルの順番でライブラリを調査すると良さそうなものが見つかるように思います。
 ネットの情報はかならず、「比較」と「評価」が必要になりますが、npmについてはこういう手順もよいのではと思います。

nvmをインストールしたのに、-bash: nvm: command not foundになる。

$
0
0

https://github.com/nvm-sh/nvm#install--update-script
こちらの

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash

を使ってnvmをインストールし、特にエラーもなく完了したはずなのに

$ nvm --version


-bash: nvm: command not found
と言われた。

色々試した結果、

$ source ~/.nvm/nvm.sh

を実行することで解決。

頑張った末に、手順の下の方に”Troubleshooting on macOS”を見つけたので、ドキュメントを読むのが大事だと思いました。

Express 入門

$
0
0

公式ドキュメントもかなりシンプルでわかりやすいですが、自分用メモとして。
Express(日本語ドキュメント)

最小構成

最低限、これで動かせるよ!というコードです
さくっとWebサーバ立ててなにか試したい時に

インストール

$ mkdir myapp
$ cd myapp
$ npm init # entry point: app.js => 任意のファイル名$ npm install express

ディレクトリ構成

(root)
    ├ package.json
    ├ package-lock.json
    ├ app.js
    └ node_modules

コード

app.js
constexpress=require('express')constapp=express()app.get('/',(req,res)=>res.send('Hello World!'))app.listen(3000,()=>console.log('Example app listening on port 3000!'))

実行

$ node app.js
# go to http://localhost:3000/

http://localhost:3000/にアクセスすると「Hello World!」と表示されます

プロジェクトの雛形を作成する

Express プロジェクトの雛形を作成します
テンプレートエンジンの設定や、ディレクトリの作成もしてくれます
本格的にプロジェクトを作成する場合はこちらがおすすめ
詳しくはこちら

CLIインストール

雛形作成に必要なパッケージをグローバルにインストールします

$ npm install express-generator -g

PJ作成

$ express --view=pug myapp
# --view=pug => 任意のテンプレートエンジン(未指定の場合はjadeになります)# myapp => 任意のPJ名$ cd myapp
$ npm install

指定できるテンプレートエンジンやその他のオプションは↓で確認できます

$ express -h

実行

$ DEBUG=myapp:* npm start
# go to http://localhost:3000/

ルーティング

メソッド

app.METHOD(PATH, HANDLER)のような形で記述します
Express は、すべてのHTTPリクエストメソッドに対応するメソッドをサポートしています
メソッドの一覧はこちら

varexpress=require('express')varapp=express()app.get('/',function(req,res){res.send('GET request')})app.post('/',function(req,res){res.send('POST request')})

また、 all()を用いて、全てのメソッドのミドルウェア的なメソッドを定義することも可能です
next()でそれぞれのメソッドのハンドラに引き渡します

app.all('/',function(req,res){res.send('Any request')next()})

パス

ルートのパスには正規表現も利用可能です

app.get(/.*fly$/,function(req,res){res.send('/.*fly$/')})

パラメータ

:paramの形で記述します
req.paramsで取得できます

app.get('/users/:userId/books/:bookId',function(req,res){res.send(req.params)// => { "userId": "hoge", "bookId": "fuga" }})

レスポンス

よく使いそうなもの掲載
一覧はこちら

// さまざまなタイプのレスポンスを送信しますres.send('GET request')res.send('<p>some html</p>')res.send({some:'json'})// etc...// ビュー・テンプレートをレンダリングしますres.render('index')// JSON レスポンスを送信しますres.json({user:'tobi'})res.status(500).json({error:'message'})

メソッドをまとめて記述する

GET, POST などのメソッドをルートごとにまとめて記述することができます
すっきりしてよき!

app.route('/book').get(function(req,res){res.send('Get a random book')}).post(function(req,res){res.send('Add a book')}).put(function(req,res){res.send('Update the book')})

ルートハンドラをモジュール化する

ルートごとにファイルを分けて管理できます
大規模なアプリケーション向き

books.js
varexpress=require('express')varrouter=express.Router()router.get('/',function(req,res){res.send('GET Books')})router.get('/:bookId',function(req,res){res.send('GET Some Book')})module.exports=router
app.js
varbooks=require('./books')// ...app.use('/books',books)
$ curl http://localhost:3000/books # => GET Books$ curl http://localhost:3000/books/12 # => GET Some Book

静的ファイル

画像や CSS/JS など、静的ファイルをロードできるようにします
詳しくはこちら

app.js
// public ディレクトリに入っているファイルをロードできるようにするapp.use(express.static('public'));// http://localhost:3000/images/kitten.jpg// http://localhost:3000/css/style.css// http://localhost:3000/js/app.js// http://localhost:3000/hello.html
Viewing all 8837 articles
Browse latest View live