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

Next.jsで静的HTMLエクスポートしたアプリをローカルで確認する方法

経緯

Next.jsの静的HTMLエクスポート機能(Static HTML Export)を使うと、サーバーにNode.jsを必要とせずに、クライントのみで実行できる静的HTMLにアプリを出力することできます。

ただし、create-next-appでスキャフォールディングしたプロジェクトに用意されているdevコマンドで起動したアプリはサーバーサイドレンダリング(以下SSR)が有効になっており、静的HTMLのみの確認ができません。

そこでNext.jsで静的HTMLエクスポートしたアプリをローカルで確認する方法を調べました。

確認環境

  • Node.js - 12.4.1
  • Next.js - 9.5.2

設定方法

サンプルプロジェクトを作成します。すでに作成済みの場合は飛ばします。

$ npx create-next-app my-static-site
$ cd my-static-site

serveというパッケージをインストールします。静的ファイルをホスティングするローカルサーバーを建てることができます。
開発でのみ使用するため-Dオプションを指定しています。

$ yarn add -D serve

最後にpackage.jsonのnpmスクリプトを修正します。
静的ファイルを出力するexportコマンドと、ローカルサーバーを立ち上げるserveコマンドを定義します。

next exportではファイルはデフォルトで./outディレクトリに出力されます。
そのため、serve ./outでホスティングするディレクトリを指定しています。

SSRせず、完全に静的HTMLエクスポートしかしない場合、誤認防止の為に不要なコマンド削除しても良いと思います。

  "scripts": {
-   "dev": "next dev",
-   "build": "next build",
-   "start": "next start"
+   "export": "next build && next export",
+   "serve": "yarn export && serve ./out"
  },

実際に下記のコマンドを打つと以下のように表示されます。

$ yarn serve

image.png

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

eject後のwebpack.config.jsを1から読み解いてみる①

はじめに

最近ReactやGraphQL, TypeScriptの環境構築をやっているのですがWebpackがあまりに難しすぎて挫折しかけたのでWebpackに対する苦手意識をなくすためにもwebpack.configの内容を1から調べていこうと思います。僕と同じくWebpackの内容の多さに絶望した方の助けになれば幸いです

動作環境

  • npm 6.14.5
  • node.js 14.3.0
  • create-react-app 3.4.1

Ln1-Ln51

use strict

strictモードの呼び出し
- 一部の問題が起こりやすいコードをエラーとして処理する
- javascriptの処理を高速化する

strictモードについて細かく書くとそれだけで1記事分になりそうなので細かい違いはこちらをご参照ください

const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
const PnpWebpackPlugin = require('pnp-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const safePostCssParser = require('postcss-safe-parser');
const ManifestPlugin = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const paths = require('./paths');
const modules = require('./modules');
const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');

const postcssNormalize = require('postcss-normalize');

const appPackageJson = require(paths.appPackageJson);

基本的にインポートなので省略

// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';

SourceMapを使用するかどうかのフラグ
使用することでWebpackによってコードがまとめられたときにもとのコードの情報が残るようになりデバッグがしやすくなる

// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';

InlineChunkHtmlPluginを使用するかのフラグ
使用することで共通の設定をChunkの中に埋め込み、http通信の数を減らすことができる

const isExtendingEslintConfig = process.env.EXTEND_ESLINT === 'true';

Eslintrcを使用するかのフラグ
ただし2020/6/7時点で機能してないと思われる
(こちらに報告あり)

const imageInlineSizeLimit = parseInt(
  process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);

url-loader使用時に事前読み込みを行うファイルサイズの上限を設定する
事前読み込みを行うことでhttp接続を減らすことができる

// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);

TypeScriptを使用しているかをチェックしている
paths.jsで設定されているpaths.appTsConfigが存在するかで判定している(初期ではtsconfig.json)

// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;

styleファイルの正規表現をまとめたもの

Ln53-Ln129

module.exports = function(webpackEnv) {
  const isEnvDevelopment = webpackEnv === 'development';
  const isEnvProduction = webpackEnv === 'production';

以降はmodule.exportsという関数の定義となる。webpackEnvは開発環境か本番環境かを分ける引数

// Variable used for enabling profiling in Production
// passed into alias object. Uses a flag if passed into the build command
const isEnvProductionProfile =
  isEnvProduction && process.argv.includes('--profile');

コードの圧縮時にclassnameや関数名を保存するか、パフォーマンスの計測を可能にするかのフラグを設定する

// We will provide `paths.publicUrlOrPath` to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
// Get environment variables to inject into our app.
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));

環境変数の読み込み。process.envの内容がenvに読み込まれる
なお、process.envの設定はdotenvにより行われているが、dotenvファイルのパスはpaths.dotenvやenv.jsで変更できる

// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
  const loaders = [
    isEnvDevelopment && require.resolve('style-loader'),
    isEnvProduction && {
      loader: MiniCssExtractPlugin.loader,
      // css is located in `static/css`, use '../../' to locate index.html folder
      // in production `paths.publicUrlOrPath` can be a relative path
      options: paths.publicUrlOrPath.startsWith('.')
        ? { publicPath: '../../' }
        : {},
    },
    {
      loader: require.resolve('css-loader'),
      options: cssOptions,
    },
    {
      // Options for PostCSS as we reference these options twice
      // Adds vendor prefixing based on your specified browser support in
      // package.json
      loader: require.resolve('postcss-loader'),
      options: {
        // Necessary for external CSS imports to work
        // https://github.com/facebook/create-react-app/issues/2677
        ident: 'postcss',
        plugins: () => [
          require('postcss-flexbugs-fixes'),
          require('postcss-preset-env')({
            autoprefixer: {
              flexbox: 'no-2009',
            },
            stage: 3,
          }),
          // Adds PostCSS Normalize as the reset css with default options,
          // so that it honors browserslist config in package.json
          // which in turn let's users customize the target behavior as per their needs.
          postcssNormalize(),
        ],
        sourceMap: isEnvProduction && shouldUseSourceMap,
      },
    },
  ].filter(Boolean);
  if (preProcessor) {
    loaders.push(
      {
        loader: require.resolve('resolve-url-loader'),
        options: {
          sourceMap: isEnvProduction && shouldUseSourceMap,
        },
      },
      {
        loader: require.resolve(preProcessor),
        options: {
          sourceMap: true,
        },
      }
    );
  }
  return loaders;
};

style-loaderの設定
・style-loader (develop環境のみ)
 CSSをhtmlに埋め込む
・MiniCssExtractPlugin.loader (production環境のみ)
 CSSを別ファイルに分離してまとめる
 →htmlに埋め込まないためstyle-loaderは不要になる
・css-loader
 CSSのメソッドをjavascriptのメソッドに変換する
・postcss-loader
 pcssファイルをcssファイルに展開する
 postcss-flexbugs-fixes→frexboxの挙動のズレを吸収する
 postcss-preset-env→postcss-nestingなどを利用する
 postcssNormalize→ブラウザごとのズレを吸収する
・resolve-url-loader
 外部からファイルを読み込むときのパスを通す
 getStyleLoadersで追加のloaderが指定されているときのみ使う
 追加のloaderではsourceMapをtrueにする必要がある

Ln131-Ln195

return {

ここからLn55の関数の返り値となる

mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',

本番環境か開発環境かを返す

// Stop compilation early in production
bail: isEnvProduction,

エラーが発生したときにコンパイルを早期終了させる
(具体例などがなかったのでちょっとイメージが掴みづらいです...)

devtool: isEnvProduction
  ? shouldUseSourceMap
    ? 'source-map'
    : false
  : isEnvDevelopment && 'cheap-module-source-map',

bundle後のファイルでエラーが発生したときにbundle前のファイルをどう参照するかの設定
Ln33のshouldUseSourceMapもここで使われる
本番環境だともとのコードのままSourceMapが作られますが、開発環境だと少し簡略化された形でSourceMapが作られるようです
(1ファイルが1行で表された形でbundleファイルが作られる...?)

entry: [
  // Include an alternative client for WebpackDevServer. A client's job is to
  // connect to WebpackDevServer by a socket and get notified about changes.
  // When you save a file, the client will either apply hot updates (in case
  // of CSS changes), or refresh the page (in case of JS changes). When you
  // make a syntax error, this client will display a syntax error overlay.
  // Note: instead of the default WebpackDevServer client, we use a custom one
  // to bring better experience for Create React App users. You can replace
  // the line below with these two lines if you prefer the stock client:
  // require.resolve('webpack-dev-server/client') + '?/',
  // require.resolve('webpack/hot/dev-server'),
  isEnvDevelopment &&
    require.resolve('react-dev-utils/webpackHotDevClient'),
  // Finally, this is your app's code:
  paths.appIndexJs,
  // We include the app code last so that if there is a runtime error during
  // initialization, it doesn't blow up the WebpackDevServer client, and
  // changing JS code would still trigger a refresh.
].filter(Boolean),

読み取りの起点を設定する
初期だとpaths.appIndexJs(src/indexが指定されている)と、
WebpackDevServer(ファイルセーブしたときに再読込する)に接続するためのclient
の2つ

output: {
  // The build folder.
  path: isEnvProduction ? paths.appBuild : undefined,
  // Add /* filename */ comments to generated require()s in the output.
  pathinfo: isEnvDevelopment,
  // There will be one main bundle, and one file per asynchronous chunk.
  // In development, it does not produce real files.
  filename: isEnvProduction
    ? 'static/js/[name].[contenthash:8].js'
    : isEnvDevelopment && 'static/js/bundle.js',
  // TODO: remove this when upgrading to webpack 5
  futureEmitAssets: true,
  // There are also additional JS chunk files if you use code splitting.
  chunkFilename: isEnvProduction
    ? 'static/js/[name].[contenthash:8].chunk.js'
    : isEnvDevelopment && 'static/js/[name].chunk.js',
  // webpack uses `publicPath` to determine where the app is being served from.
  // It requires a trailing slash, or the file assets will get an incorrect path.
  // We inferred the "public path" (such as / or /my-project) from homepage.
  publicPath: paths.publicUrlOrPath,
  // Point sourcemap entries to original disk location (format as URL on Windows)
  devtoolModuleFilenameTemplate: isEnvProduction
    ? info =>
        path
          .relative(paths.appSrc, info.absoluteResourcePath)
          .replace(/\\/g, '/')
    : isEnvDevelopment &&
      (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
  // Prevents conflicts when multiple webpack runtimes (from different apps)
  // are used on the same page.
  jsonpFunction: `webpackJsonp${appPackageJson.name}`,
  // this defaults to 'window', but by setting it to 'this' then
  // module chunks which are built will work in web workers as well.
  globalObject: 'this',
},

出力内容の設定
・path: どこにファイルを作成するか
・pathinfo: 使用したファイルをコメントで表示するか
・filename: 生成するファイルの名前
・futureEmitAssets:

Tells webpack to use the future version of asset emitting logic, which allows freeing memory of assets after emitting. It could break plugins which assume that assets are still readable after they were emitted.

ドキュメントより抜粋。常に最新のものを取得することで内部にデータを持たないようにするって感じだと解釈。ただ何を指すのかピンと来ない
※この機能はwebpack5.0以降ではデフォルトになるようです

・chunkFileName: ファイルをchunkで区切ったときのchunkのファイル名
・publicPath: bundleファイルのアップロード先
・devtoolModuleFilenameTemplate: sourceMapの名前の付け方
・jsonpFunction: 外部のデータを取得するのに使われるjsonpの設定
・globalObject: bundle後のコードでの即時関数の第一引数。Node.jsを使う場合デフォルトだとエラーになるのでthisにしないといけない

Ln196-Ln273

optimization: {

以降はバンドル後のコードの最適化についての設定を表している

minimize: isEnvProduction,

コードの最小化を行うかを判断するフラグ。trueなら後述のminimizerによりコードの最小化が行われる

minimizer: [

以降最小化の方法を指定するminimizerに関する設定となる
デフォルトではTenserPlugin, OptimizeCSSAssetsPluginの2つが用意されている

// This is only used in production mode
new TerserPlugin({
  terserOptions: {
    parse: {
      // We want terser to parse ecma 8 code. However, we don't want it
      // to apply any minification steps that turns valid ecma 5 code
      // into invalid ecma 5 code. This is why the 'compress' and 'output'
      // sections only apply transformations that are ecma 5 safe
      // https://github.com/facebook/create-react-app/pull/4234
      ecma: 8,
    },
    compress: {
      ecma: 5,
      warnings: false,
      // Disabled because of an issue with Uglify breaking seemingly valid code:
      // https://github.com/facebook/create-react-app/issues/2376
      // Pending further investigation:
      // https://github.com/mishoo/UglifyJS2/issues/2011
      comparisons: false,
      // Disabled because of an issue with Terser breaking valid code:
      // https://github.com/facebook/create-react-app/issues/5250
      // Pending further investigation:
      // https://github.com/terser-js/terser/issues/120
      inline: 2,
    },
    mangle: {
      safari10: true,
    },
    // Added for profiling in devtools
    keep_classnames: isEnvProductionProfile,
    keep_fnames: isEnvProductionProfile,
    output: {
      ecma: 5,
      comments: false,
      // Turned on because emoji and regex is not minified properly using default
      // https://github.com/facebook/create-react-app/issues/2488
      ascii_only: true,
    },
  },
  sourceMap: shouldUseSourceMap,
}),

minimizerの1つ、TerserPluginについての設定

parse: コードをどの型に変換するか。ECMAScript8に変換するように設定されている
※ここでは8だが、他の場所で5に上書き設定される

compress: 圧縮方法についての設定
- ecma: 変換後の型。ここではECMAScript5になっている
- warnings: 記述なし...おそらくwarningを表示するかを表す
- comparisons: 論理系の表現を簡略化する
- inline: 関数を1行に圧縮するかの設定。初期では変数宣言のない関数まで圧縮

mangle: 変数名を短くするなど
- safari10: safari10/11のバグに対応させるかのフラグ

keep_classnames, keep_fnames: classnameなどをそのままにするか

output: 出力内容の設定
- ecma: 変換後の型。ここではECMAScript5になっている
- comments: コメントアウトされた部分を残すか
- ascii_only: 対応する文字の範囲の指定

sourceMap: 圧縮時にsourceMap対応するか(Ln33参照)

// This is only used in production mode
new OptimizeCSSAssetsPlugin({
  cssProcessorOptions: {
    parser: safePostCssParser,
    map: shouldUseSourceMap
      ? {
          // `inline: false` forces the sourcemap to be output into a
          // separate file
          inline: false,
          // `annotation: true` appends the sourceMappingURL to the end of
          // the css file, helping the browser find the sourcemap
          annotation: true,
        }
      : false,
  },
  cssProcessorPluginOptions: {
    preset: ['default', { minifyFontValues: { removeQuotes: false } }],
  },
}),

minimizerのもう一つ、OptimizeCSSAssetsPluginについての設定
cssProcessorとして使われているcssnanoにオプションを引き渡している

cssProcessorOptions
- parser: safePostCssParserによってCSSが壊れていても読み込むことができる
- map
- inline: sourceMapを同じファイルに作成するか
- annotation: sourceMapのURLをCSSに入れるか

cssProcessorPluginOptions
- minifyFontValues: 文字をサイズの小さい形に変換する
removeQuotesがfalseなのでクオートは省略されない

// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
splitChunks: {
  chunks: 'all',
  name: false,
},

各ファイルの呼び出し回数をもとに各chunkを適切に分解、統合する
chunks: どのタイプのchunkを最適化対象にするか
name: chunkのファイル名(falseの場合変更なし)

// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
// https://github.com/facebook/create-react-app/issues/5358
runtimeChunk: {
  name: entrypoint => `runtime-${entrypoint.name}`,
},

各エントリーポイントで動いているファイルのみでchunkを新しく作成する
name: 新しく作られるchunkのファイル名

終わりに

今回はwebpack.configの前半部分の内容についてどんなことをやっているかの簡単な説明をさせていただきました。残りの部分は1記事にまとまるかわかりませんが今月(2020/08)中には出したいなと考えております

また、調べてもいまいち内容のつかめなかった部分もあったので詳しい方は教えていただけるとありがたいです

参考ページ

この記事を書くにあたっていろいろなサイトを参考にさせていただきました
量の問題で書ききれなかった部分も多いのでもしわかりにくい点があれば以下のページを参考にしていただけると幸いです

全体
https://webpack.js.org/
http://js.studio-kingdom.com/webpack/api/configuration
https://qiita.com/soarflat/items/28bf799f7e0335b68186

strictモード
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Strict_mode

sourceMap, devtool
https://chuckwebtips.hatenablog.com/entry/2016/03/02/000000
https://t-hiroyoshi.github.io/webpack-devtool/
https://webpack.js.org/configuration/devtool/

InlineChunkHtmlPlugin
https://www.npmjs.com/package/html-webpack-inline-chunk-plugin

Eslintrc
https://github.com/facebook/create-react-app/issues/9047

env
https://maku77.github.io/nodejs/env/dotenv.html

MiniCssExtractPlugin
https://reffect.co.jp/html/webpack-4-mini-css-extract-plugin

css-loader, style-loader
https://ics.media/entry/17376/

postcss
https://qiita.com/okumurakengo/items/a10f6fa4b77b5b088cb9
https://unformedbuilding.com/articles/php-based-css-preprocessor-pcss-and-css-crush/
https://qiita.com/naru0504/items/86bc7c6cab22a679553e
https://techacademy.jp/magazine/19732

resolve-url-loader
https://e-joint.jp/907/

entry(webpackHotDevClient)
https://www.slideshare.net/ssuserc9c8d8/reactscriptswebpack-130687608

output.publicPath
https://www.it-swarm.dev/ja/javascript/webpack%E3%81%AE-publicpath%E3%81%AF%E4%BD%95%E3%82%92%E3%81%99%E3%82%8B%E3%81%AE%E3%81%A7%E3%81%99%E3%81%8B%EF%BC%9F/1050990542/

output.globalObject
https://qiita.com/riversun/items/1da0c0668d0dccdc0460

optimization
https://webpack.js.org/configuration/optimization/

ECMAScript
https://ja.wikipedia.org/wiki/ECMAScript

terserOptions
https://github.com/terser/terser
https://gist.github.com/shqld/d101cae50dd83ab7d3487cdb10b80f4d

cssProcessor
https://github.com/NMFR/optimize-css-assets-webpack-plugin
https://site-builder.wiki/posts/9654
https://cssnano.co/optimisations/minifyfontvalues

splitChunks
https://blog.hiroppy.me/entry/mechanism-of-webpack#SplitChunksPlugin-v4

runtimeChunk
https://webpack.js.org/configuration/optimization/#optimizationruntimechunk

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

Teachable Machineで学習したデータをNode.jsでシンプルに利用する

いい感じのサンプルがなかった

node-redenebularとはTeachable Machineの連携が結構あったり、フロントエンド側ばかりたったので、Node.jsで画像ファイルを読み込んで判定するシンプルな実装をメモ

https://www.npmjs.com/package/@teachablemachine/image
ここもやはりフロントエンドのサンプルしかなくて…

https://www.tensorflow.org/js
tensorflowをそのまま利用しようとも考えたけど、もっとTeachableMachineに特化して使いやすくできるのでは…

インストールしたもの

  • @teachablemachine/image: 0.8.4
  • @tensorflow/tfjs: 2.1.0
  • canvas: 2.6.1
  • jsdom: 16.4.0
npm i @teachablemachine/image @tensorflow/tfjs canvas jsdom

ソースコード

const { JSDOM } = require('jsdom');
var dom = new JSDOM('');
global.document = dom.window.document;
global.HTMLVideoElement = dom.window.HTMLVideoElement;
const canvas = require('canvas');
global.fetch = require('node-fetch');

const tmImage = require('@teachablemachine/image');
const fs = require('fs');

// https://teachablemachine.withgoogle.com/
// ここでエクスポート、クラウドにモデルをアップロードした後に取得できる
const URL = '{{URL}}';

async function init() {
  const modelURL = URL + 'model.json';
  const metadataURL = URL + 'metadata.json';
  // モデルデータのロード
  const model = await tmImage.load(modelURL, metadataURL);

  // クラスのリストを取得
  const classes = model.getClassLabels();
  console.log(classes);

  // 同じフォルダ内の画像を読み込む 今回は自分の顔画像
  const image = fs.readFileSync('ono.png');
  // 読み込んだ画像で判定してみる
  const predictions = await model.predict(image);
  console.log(predictions);
}

init();

実際実装してみたらフロントエンド前提のものでちょっとスマートじゃない。
もしかしたらアップデート待ちでいいのかも

出力結果

Image from Gyazo

「おの」は自分の顔

一応成功しました!

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

GithubのプロフィールにTwitterのツイートを表示する

はじめに

今年になってからGithubのプロフィールに好きな文面を追加できる機能が追加されました。
既に色々なサービスが開発されていています。

プロフィールに最新のつぶやきを載せたい!

唐突ですが、プロフィールページにつぶやきを載せることはできないかなと思い、調べてみました。

どうやって載せる?

結論としては、以下のように実装しました。

  1. Twitter APIを叩いて、ツイートを取得する
  2. 取得したツイートをSVG形式に変換
  3. これらのコードをVercelでホスティング
  4. README.mdにSVGへの画像リンクを追加

サンプル

出力画像(SVGからPNGに変換済み)

sample.png

動作しているページ

github.com/gazf

使い方

画像リンクの?id=の部分は表示したいアカウントのスクリーンネームに書き換えてください。

[![github-readme-twitter](https://github-readme-twitter.gazf.vercel.app/api?id=gazff)](https://github.com/gazf/github-readme-twitter)

コード

Github gazf/github-readme-twitter

問題点

Vercelって無料?

公式サイトを読んでも特にクォータ等の記述がない気がします。

Twitter APIのクォータに引っ掛かりそう

そんなに大量にリクエスト来ないと思うし、大丈夫だよね・・・?

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

Vue.jsでBootStrapをつまみ食い的に使う

概要

Vue.jsのVue CLIからBootStrapを使う方法。

「BootStrapを使おう!さぁ先ずは基本を学ぼう」と構えて臨むのではなく、「このデータ構造の表現にちょうどよいUI無いかな? お、BootStrapのxxのコンポーネントが良さ気じゃん」と気軽につまみ食いで使う、ことを目指すとする。

サンプルに用いるデータ構造

次のような配列データを、Vue.jsを用いたUIで表現する場合を考える。
表示対象は「datetime, type, notes」の3つとする。

activitylist = [
    {
        id: 5,
        datetime: "1596229200",
        type: 1,
        notes: '翌日の6時に起きたとする'
    },{
        id: 4,
        datetime: "1596223800",
        type: 1,
        notes: '翌日の4時半に目が覚めたとする'
    },{
        id: 3,
        datetime: "1596201000",
        type: 0,
        notes: '2020-07-31 22:10、つまり夜22時過ぎに寝た場合を仮定'
    },{
        id: 2,
        datetime: "1596164400",
        type: 2,
        notes: '薬を昼12時に飲んだとする。'
    }
];

typeに指定された値に対して、その値を配列番号と見なして、それぞれ配列要素のtitleキーに設定した文字列に置き換えて表示する、ものとする。

typelist = [
    { 
        title: '寝た'
    }, {
        title: '起きた'
    }, {
        title: '服薬'
    }
];

これらの配列データをViewListCard.vueで定義し、
コンポーネントItemCard.vueに配列をPropsで渡して、
表示の仕方はコンポーネントItemCard.vueに任せる、
という設計を仮定する。

具体的なサンプルコードは次のようになる。

https://github.com/hoshimado/qiita-notes/tree/master/qiita-card-bootstrap

  • ./src/components/ViewListCard.vue
  • ./src/components/ItemCard.vue
    • ItemCard0.vueItemCard3.vue がそれぞれの段階ごとのサンプルコード

テキストをマスタッシュ構文でそのまま表示する

上述の配列データactivitylistの各要素の3項目を、次の変換のみを行って
マスタッシュ(Mustache)を用いてテキストで表示すると
次のようになる。

  • datetime: UNIX時間(秒)を「YYYY-MM-DD . HH:MM:00」の文字列に変換する
  • typeを、typelistの配列番号に応じた要素のtitleキーに設定された文字列に変換する
  • notesはそのまま表示する。

▼card0.png
https://gyazo.com/820baa2b9aa19d6d5a62f000292b80e2

本サンプルでは、それぞれのカード(様の部分)をタップすると編集モードになるる、という設計とする。その編集モードは、先ずはHTML標準のinputタグを用いて実装すれば、次のような表示となる。

▼card0-edit.png
https://gyazo.com/51143f81d3283087fc2c59a673d16f0b

ここまでのサンプルコードは次のようになる。
ItemCard0.vue を、実際にはItemCard.vueとして動作させる。

https://github.com/hoshimado/qiita-notes/tree/master/qiita-card-bootstrap/src/components/ItemCard0.vue

以下、上述までの表示形式を、BootStrapを用いていい感じにする方法を述べる。

BootStrap(BootStrapVue)を使う準備をする

Vue.js上でBootStrapを利用するには、BootStrapVueを用いるのが簡単。

BootstrapVue provides one of the most comprehensive
implementations of Bootstrap v4 for Vue.js.

Vue CLIのプロジェクトのルートフォルダにて、以下のコマンドでインストールする。

npm install bootstrap-vue    --save

続いて、ルートにあるmain.jsを開いて、次の2行(本サンプルではコメント含めて4行)を追加する。

main.js
import Vue from 'vue'
import App from './App.vue'

// +++ add for bootstrap +++
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
// -------------------------

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

以上で、BootStrapをVue CLI上で使う準備は完了。

ref. https://bootstrap-vue.org/docs

項目のタイトル相当を見やすくしたい

先ほどの「card0.png」の画面を見やすくすることを考える。

「起きた」や「寝た」などの表示を上手く装飾するものがないか?
と公式BootStrapVueのComponentのページを見ていく。

https://bootstrap-vue.org/docs/components

このページは、簡単な説明を一覧出来て、それぞれのComponentの説明ページに飛ぶとサンプル表示もあるので、「そのComponentによる装飾がどういうものか?」を掴みやすくて、助かる。

どんなコンポーネントがあるか?を上から順に見ていく中で、
今回のケースなら「バッジ(Badge)」で「起きた」「寝た」などを表示するのがよさそうだ、などのように装飾の仕方を決める。

本サンプルでは、上述のBadgeによる装飾とnotesの値をReadOnlyのForm Textareaで表示するものとする。
この場合は、次のような表示になる。

▼card1.png
https://gyazo.com/12d6d9bb3df6660d55e1b9d349875143

上記を実装するには、コンポーネントItemCard.vueに対して、次のような変更を加える。

  • {{typeStr}}としていた部分を、<b-badge v-bind:variant="typeVariant">{{typeStr}}</b-badge>とする
  • {{notesCurrent}}としていた部分を、<b-form-textarea v-bind:value="notesCurrent" readonly rows="2" max-rows="2"></b-form-textarea>とする
  • 利用するコンポーネントを「import { BBadge, BFormTextarea } from 'bootstrap-vue'」で読み込んで、「components: {}」に指定する

たったこれだけのコード修正で、で上図(card1.png)の様な見やすい表示に変更できる。BootStrapはとても簡単で使いやすい。

なお、編集モード(card0-edit.png)の表示については、notesCurrentの部分は、Form Textareaを用いてreadonly属性を外せばよいだろう。編集モード側のUI変更を含めたサンプルコードは以下。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard1.vue

編集モード側の表示は以下のようになる。

▼card1-edit.png
https://gyazo.com/fe5ba1dc8bbf9e7b4e3e834ad62d0b43

なお、<b-badge>コンポーネントはvariant属性でカラーリングを変更できる。サンプルコードでは、typelist配列の各要素に、variantキーを追加し、それに従ったBadgeカラーを表示する実装にしてある。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ViewListCard.vue

[
    { 
        title: '寝た',
        variant: 'primary'
    }, {
        title: '起きた',
        variant: 'secondary'
    }, {
        title: '服薬',
        variant: 'success'
    }
]

variant属性への指定において、デフォルトで利用可能な値は以下を参照。

https://bootstrap-vue.org/docs/components/badge#contextual-variations

編集モードで選択項目をラジオボタンで、ついでに確定ボタンもいい感じに装飾する

続いて、上図の編集モード(card0-edit.png)における「起きた」「ネタ」をラジオボタンで選べるようにする。HTML標準の<input type="radio">でも良いのだが、BootStrapにForm Radioコンポーネントがあるので、これを使う。

次の公式ガイドに従って、<b-form-group><b-form-radio-group>を用いる。

https://bootstrap-vue.org/docs/components/form-radio#grouped-radios

編集対象は(propsで渡されたtypeをもとに生成した)typeCurrentなので、これをv-model属性で<b-form-radio-group>にバインドする。

<b-form-group label="記録の種別を選んでください">
    <b-form-radio-group
        v-model="typeCurrent"
        :options="typeOptions"
    ></b-form-radio-group>
</b-form-group>

選択肢は、v-bind:options属性(略記して:options属性)で設定する。設定すべき変数のフォーマットは配列で、各要素は次の2つのキーを持つ。

  • 選択肢の文字列としてtext
  • 選択されたときに編集対象(=v-modelでバインドされた変数)へ代入する値としてvalue

したがって、本サンプルでは(propsで渡された)typelistを元にして次のようにtypeOptins配列を生成しておく。

this.typelist.forEach((elem, index)=> {
    this.typeOptions.push({
        text: elem.title,
        value: String(index)
    })
})

ついでなので、「確定」ボタンもBootStrapVueが提供するButtonコンポーネントで装飾する。これは、「<button @click="clickBtnEditFinish">確定</button>」としていたところを、「<b-button @click="clickBtnEditFinish">確定</b-button>」と置き換えるだけで良い。

以上の変更を加えたコンポーネントItemCard.vueのコード全体は以下となる(※importcomponentsへの追加も忘れずに→リンク先のコードを参照)。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard2.vue

上記のコードへの変更によって、編集モードの表示は次のように変わる。

▼card2-edit.png
https://gyazo.com/4db021ef51d4331edddc5ab860f6d7de

日時の編集ボックスを、いい感じに装飾する

最後に、上図(card2-edit.png)の日付と時刻の入力を良い感じに装飾する。

(※HTML5利用可能環境であれば、素のHTML inputタグが実装している入力支援のpickerを利用可能なので、BootStrap版に置き換えるか否かは好みの問題かもしれない。一応、IEとPC版SafariはHTML5に未対応のため同じ表示にならないが、BootStrap版なら同じ表示が可能、という差はある)

これまでと同様にBootStrapVueのコンポーネント一覧から、日付と時刻のPickerを探す。

https://bootstrap-vue.org/docs/components

Form DatepickerForm Timepickerがあるので、これを使う。

https://bootstrap-vue.org/docs/components/form-datepicker

https://bootstrap-vue.org/docs/components/form-timepicker

使い方は、それぞれを次のように置き換えるだけ。

置き換え前:

<input v-model="dateCurrent" type="date">
<input v-model="timeCurrent" type="time">

置き換え後:

<b-form-datepicker v-model="dateCurrent" class="mb-2"></b-form-datepicker>
<b-form-timepicker v-model="timeCurrent" locale="ja"></b-form-timepicker>

置き換え後のサンプルコードは次のようになる。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard3.vue

※ここで「class=mb-2」を指定しているが、これはBootStrap v4.5で定義されているclassのこと。BootStrapVueでは、BootStrapで準備されているClassをそのまま利用できる。

上記のコードへの変更によって、編集モードの表示は次のように変わる。

▼card3-1edit.png
https://gyazo.com/11097020fcca048dd5f9ae00f32fd7ef

▼card3-2date.png
https://gyazo.com/e9461f4372ce6acff2410bee18da2235

▼card3-3time.png
https://gyazo.com/f5f32c9de592b855dd2cb498a13ce8aa

なお、「Picker経由だけでなく、時刻を直にテキストとして入力もしたい」という場合は、inputタグを組わせることで実現できる。BootStrapVueでの、その実装例も公式サイトの以下に記載がある。とても親切♪

https://bootstrap-vue.org/docs/components/form-timepicker#button-only-mode

以上ー。

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

IBM Cloud Functions でLINEとSlack連携させてみた

はじめに

この記事は、LINEとIBM Cloud Functionsを連携したSlackへの通知機能を作ったので、3回に分けて紹介したい記事の第3回になります。
 第1回:IBM Cloud Functons 動かしてみた
 第2回:IBM Cloud FunctionsでNode.jsのパッケージを利用してみた
 第3回:IBM Cloud Functions でLINEとSlack連携させてみた ← この記事

なんでLINEとSlackを連携させようと??

近年、自社の社内コミュニケーションツールがSlackに移行したのですが、それ以前からチームメンバーの他愛のないやり取りをLINEで行っていました。協業するベンダーさんも含まれており、全員がSlackに移行するのは面倒(招待は本来可能)と思い、しかし現場を管理するマネージャーをLINEグループに招待したいかというと別の話。バカなやり取りを見せたいとは思わず、ただ、急な勤怠連絡が入るとLINEにいるメンバーは知っているけど、マネージャーが把握していないという状況は避けたい。結果、LINEで勤怠連絡だけ、社内のSlackに通知したら良いのではないかと思い、作りました。

必要なもの

  • LINEアカウント
  • Slackの目的のチャンネルに投稿可能なInoming WebhookのURL

何はともあれ、IBM Cloud Functionsの受け口を作成

LINEと連携する前に、LINEのメッセージを受取るFunctionsのAPIを作成しましょう。
前々回、前回の記事を読まれた方は余裕なはずですが、簡単に。
Functionsのトップからアクションを選んで、作成を選択しましょう。
image.png
作成対象でアクションを選択したら、アクション名を入力し、 ランタイムを選択、 作成を選択してください。
image.png
1点だけ、自動生成されたコードに、呼び出された時のparamsログを出力するように1行追加しておいてください。
image.png

コード
console.log(JSON.stringify(params));

APIとして公開するために、Functionsのトップから、APIを選択して、以前作成したAPIを選択します。
APIが表示されたら定義および保護を選択し、操作の作成から新しいAPIを作成しましょう。
image.png
LINEからPOSTされるので、verbPOSTを選択するように注意してください。あとは、適当に入力して、作成します。
image.png
これで、画面下部の保存を選択すれば、APIが公開されます。

LINE側作業

LINE Developers

LINEでアプリ開発するなら必ず必要な LINE developers のサイトがあるので、ご自身のラインアカウントでログインしてください。
image.png
ログインしたら、こんな感じの画面が出るので、Create でProviderを作成しましょう。
適当な名前を入れてCreateしましょう。
image.png
Create a Messaging API channelを選択します。
image.png
各種項目を入力して、Createを選択しましょう。
Channel type:そのまま
Provider:そのまま
Channel icon :そのまま
Channel name:かたひろのSlack通知 ※入力必須 マルチバイト文字推奨
Channel description:katahiro-qiita ※入力必須
Category:個人 ※入力必須
Subcategory:個人(その他) ※入力必須
Email address:※入力必須
Privacy policy URL:不要
Terms of use URL:不要
image.png
このようにChannelが無事に作成できました。
image.png

Messaging APIの設定

各種設定をしてきます。
image.png
Webhook URLに先ほど作成したFunctionsのAPIのURLを設定します。
image.png
最下部にあるChannel access tokenIssueを選択して発行しておいてください。後ほど利用します。
image.png

動作検証

では、これで簡易な設定は完了したので、画面中ほどにあるQRコードをLINEで読み込んで、友達になりましょう。
では、「テスト」とメッセージを送ってみます。
image.png
自動応答の設定が残っているので、何かしらメッセージが返ってきますが、ここでは無視します。
ここで、IBM Cloud Functionsにはどのようなメッセージが届いているのでしょうか?
Functionsのトップから、モニターを選択すると、動作したAPIの情報が参照できます。
確認すると、LINEに「テスト」と送った時間に、アクティビティーログが出力されています。
image.png
ここで、今回のアクティビティーログを開くと、アクション作成時に、console.log(JSON.stringify(params))を記述したメリットが出てきます。そう、LINEから受け取った全量がどのようなものなのか確認できます。
image.png
ヘッダー情報は無視して、LINEから受け取ったeventsに限定してお見せすると、このような情報を受取っています。

{
  "events":[
    {
      "message":{
        "id":"12457265946945",
        "text":"テスト",
        "type":"text"
      },
      "mode":"active",
      "replyToken":"2af72141cd3f42319f30edd7e87dbc94",
      "source":{
        "type":"user",
        "userId":"U19ca3e54ebdd50e1fa26dxxxxx"
      },
      "timestamp":1596792352030,
      "type":"message"
    }
  ]
}

大事なのが、以下です。
text:もちろんLINEで投稿されたメッセージ
replyToken:応答メッセージを返す場合はこのTokenが必要になります
userId:LINEのAPIを利用して、メッセージを送ったユーザー名を取得する場合、API呼出しに必要になります

LINEで応答

LINE developers側のMessaging API Settingsの画面下部に、下記のLINE Official Account featureの設定項目があるので、右側のEditを選択します。
image.png

下記のようなLINE Official Account Managerの画面が表示されます。
image.png

画面左部の応答設定のメニューから、あいさつメッセージオフに、応答メッセージオフにしましょう。これで、友達になった時に自動でメッセージが送られたり、メッセージを送ると自動で応答される、ということが無くなります。
image.png
このように、自動応答メッセージも返答されなくなりました。
image.png
さて、オウム返しのように、メッセージ内容を返してくれるように、アクションのコードを行いましょう。

package.json
{
  "name": "qiita-action",
  "version": "1.0.0",
  "main": "action.js",
  "dependencies": {
    "request": "^2.88.0"
  }
}

注意点としては、LINEのAPIを実行する時は、リクエストのヘッダーにAuthorization:Bearerを設定して、先ほどのChannel access tokenを指定するようにしてください。

action.js
const main = async ( params ) => {
  // bearer には Channel access tokenを設定する
  const bearer = "HW9WPp3A33xxxxxxx"
  const lineReplyApiUrl = "https://api.line.me/v2/bot/message/reply";

  // LINEから来ている場合にのみ限定
  if(params.events && params.events[0].type === "message" ){

    // LINE情報取得
    const message = params.events[0].message.text;
    const replyToken = params.events[0].replyToken;
    const userId = params.events[0].source.userId;

    // LINE ユーザー名取得(1対1のトーク用)
    const getUserNameUrl = "https://api.line.me/v2/bot/profile/" + userId;
    const getUserNameOption = {
      method : "GET",
      url : getUserNameUrl,
      headers : {
        "Authorization" : "Bearer " + bearer,
        "Content-Type": "application/json",
      }
    }

    const userLineAPIObj = await callAPI(getUserNameOption);
    const userNameObj = JSON.parse(userLineAPIObj.body);
    const userName = userNameObj.displayName;

    // LINE返信
    const lineMessage = {
      "type" : "text",
      "text" : `${userName}さん、「${message}」と言いましたね`
    }
    const lineReplyOption = {
      method : "POST",
      url : lineReplyApiUrl,
      headers : {
        "Authorization" : "Bearer " + bearer,
        "Content-Type"  : "application/json",
      },
      json : {
        messages :[ 
          lineMessage
        ],
        replyToken : replyToken,
      }
    }
    const replyLineAPIObj = await callAPI(lineReplyOption);
    return true;
  }
  return true
}

const callAPI = (httpOption) => {
  return new Promise((resolve,reject) => {
    const request = require( 'request' );
    request( httpOption, ( err, res, buf ) => {
      if( err ){
        reject( { status: false, error: err } );
      }else{
        resolve(res);
      }
    });
  })
}

exports.main = main;

お見せ出来ませんが、私のLINEの登録名を取得した上で、取得したメッセージ内容も応答に含められていることが確認できました。
image.png

Slackへ通知

後は、勤怠連絡の場合のみ、Slackに通知するように書き換えるだけです。

action.js
const main = async ( params ) => {
  // bearer には Channel access tokenを設定する
  const bearer = "HW9WPp3A33xxxxxxx"
  const lineReplyApiUrl = "https://api.line.me/v2/bot/message/reply";
  const incomingWebhookUrl = "https://hooks.slack.com/services/xxx/xxx/xxxxx";

  // LINEから来ている場合にのみ限定
  if(params.events && params.events[0].type === "message" ){
    // LINEメッセージを取得
    const message = params.events[0].message.text;
    // 勤怠連絡扱いのワードが含まれているかチェック
    if (message.match(/勤怠連絡/)){
      // LINE情報取得
      const replyToken = params.events[0].replyToken;
      const userId = params.events[0].source.userId;

      // LINE ユーザー名取得(1対1のトーク用)
      const getUserNameUrl = "https://api.line.me/v2/bot/profile/" + userId;
      const getUserNameOption = {
        method : "GET",
        url : getUserNameUrl,
        headers : {
          "Authorization" : "Bearer " + bearer,
          "Content-Type": "application/json",
        }
      }

      const userLineAPIObj = await callAPI(getUserNameOption);
      const userNameObj = JSON.parse(userLineAPIObj.body);
      const userName = userNameObj.displayName;

      // Slack通知
      const slackMessage = {
        "text"       : "[ LINE通知 ] " + message,
        "channel"    : "Cxxxxx",
        "username"   : userName,
        "icon_emoji" : ":warning:",
      }

      const slackSendOption = {
        method: 'POST',
        url: incomingWebhookUrl,
        encoding: null,
        headers: {
          "Content-type": "application/json",
        },
        json: slackMessage
      };
      const slackResult = await callAPI(slackSendOption);

      // LINE返信
      const replyText = (slackResult.statusCode === 200) ? "Slackに通知しました" : "Slack連携に失敗しました";
      const lineMessage = {
        "type" : "text",
        "text" : replyText,
      }
      const lineReplyOption = {
        method : "POST",
        url : lineReplyApiUrl,
        headers : {
          "Authorization" : "Bearer " + bearer,
          "Content-Type"  : "application/json",
        },
        json : {
          messages :[ 
            lineMessage
          ],
          replyToken : replyToken,
        }
      }
      await callAPI(lineReplyOption);
      return true;
    }
    return true
  }
}

const callAPI = (httpOption) => {
  return new Promise((resolve,reject) => {
    const request = require( 'request' );
    request( httpOption, ( err, res, buf ) => {
      if( err ){
        reject( { status: false, error: err } );
      }else{
        resolve(res);
      }
    });
  })
}

exports.main = main;

こんな感じで、勤怠連絡がメッセージに含まれる場合だけ、反応していることがわかります。
image.png
Slackのチャンネルを確認すると、通知が飛んでいることが確認できます。
image.png

グループトークへの対応

目的通りに動いたことを確認しましたが、いざ、この勤怠連絡してくれるBotをグループチャットに招待すると、動きません。
原因は、1対1の友達の状態でトーク相手のユーザー名を取得するAPIと、グループトークで投稿したユーザー名を取得するAPIが異なるためです。
全て記述すると長いので、部分的に以下のコードを追加するか書き換えてください。

action.js
// LINE情報取得
const replyToken = params.events[0].replyToken;
const userId = params.events[0].source.userId;
const groupId = params.events[0].source.groupId; // 追加:グループトークの場合はグループIDが取得できます。

// LINE ユーザー名取得(上は1対1のトーク用、下はグループトーク用)
//const getUserNameUrl = "https://api.line.me/v2/bot/profile/" + userId;
const getUserNameUrl = "https://api.line.me/v2/bot/group/" + groupId + "/member/" + userId;

グループトークの場合は、グループIDが取得できるので、それを用いてユーザー情報を取得しなければならない点が面倒なところです。三項演算子使えば良かったかな。。。

さいごに

いかがでしたでしょうか?LINEはLINE developersに登録しさえすれば、簡単に連携できますし、Slackも通知だけであれば、Incoming Webhookの設定さえしてしまえば、投稿は簡単に行えるので、連携させると意外に便利なものが出来るかもしれません。
今回の記事は、コード成分が多めでしたが、難しいコードはそんなに無いので、試しに作ってみようという方もチャレンジできる範囲かと思います。
また、IBM Cloud Functionsを利用した記事も書いてみたいと思いますが、第3回までお付き合いありがとうございました。
 第1回:IBM Cloud Functons 動かしてみた
 第2回:IBM Cloud FunctionsでNode.jsのパッケージを利用してみた
 第3回:IBM Cloud Functions でLINEとSlack連携させてみた

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

IBM Cloud FunctionsでNode.jsのパッケージを利用してみた

はじめに

この記事は、LINEとIBM Cloud Functionsを連携したSlackへの通知機能を作ったので、3回に分けて紹介したい記事の第2回になります。
 第1回:IBM Cloud Functons 動かしてみた
 第2回:IBM Cloud FunctionsでNode.jsのパッケージを利用してみた ← この記事
 第3回:IBM Cloud Functions でLINEとSlack連携させてみた
今回は、アクションにNode.jsのパッケージを利用して、より自由な処理を作れることを紹介したいと思います。

用意するもの

注意

今回ご提示する方法は、ローカルPCでデバッグできるようなやり方ではなく、都度IBM Cloud Functionsにアクションを登録(更新)して動作を確認するものになります。

注意2

普通の反応「せっかくブラウザ上でコード編集できるのに、ローカルPCでコード書かないとあかんの?」
IBM Cloud「せやねん。すまんな。。。」

例えば、Functionsで利用可能なNode.jsのパッケージをリンク先で公開されていますが、ブラウザで編集可能なアクションのコードにパッケージを記述しても利用できなくて、ローカルPCで作成したコードと、利用したいパッケージを宣言した package.json をFunctionsにアクションとして登録する必要があります。

コーディング

さて、紹介なので、簡易なコードにしましょう

package.json

request パッケージを利用したいので、dependencies に登録しています

package.json
{
  "name": "qiita-action",
  "version": "1.0.0",
  "main": "action.js",
  "dependencies": {
    "request": "^2.88.0"
  }
}

action.js

どんな紹介コードが良いかと考えましたが、前回に公開したAPIにアクセスするようにしましょう。

action.js
// 実行するメソッド
const main = async ( params ) => {
  // 前回に公開したAPIを設定、params に name があれば、それを設定しています
  const apiUrl = 'https://81fe65f5.jp-tok.apigw.appdomain.cloud/function-api/helloworld';
  const callUrl = params.name ? `${apiUrl}?name=${params.name}` : apiUrl;
  // HTTPリクエストのオプションを指定します
  const getHttpRequestOption = {
    method : "GET",
    url : callUrl,
    headers : {
      "Content-Type": "application/json",
    }
  }
  const response = await callAPI(getHttpRequestOption);
  const bodyJson = JSON.parse(response.body); // bodyのJSONがescapeされているので、JSONに戻します
  return bodyJson;
}

// requestのパッケージを利用する処理を外出ししています
const callAPI = (httpOption) => {
  return new Promise((resolve,reject) => {
    const request = require( 'request' );
    request( httpOption, ( err, res, buf ) => {
      if( err ){
        reject( { status: false, error: err } );
      }else{
        resolve(res);
      }
    });
  })
}
// IBM Cloud Functions では、 `main` メソッドが外部から呼び出されるので、exportsが必須です
exports.main = main;

アクションの登録

IBM Cloudへのログインと環境の確認

とりあえず、CLIでIBM Cloudにログインしましょう。今回は東京リージョン( jp-tok )を選択しています。

コマンドプロンプト
# ログイン
ibmcloud login -u [登録アドレス] -p [登録パスワード]
# リージョンの指定
ibmcloud target -r jp-tok
# Functionsのプラグインが導入されているか確認
ibmcloud plugin list | findstr function
  cloud-functions/wsk/functions/fn       1.0.44

コードをZIPに固める

今回作成した package.jsonaction.js をZIPファイルにする必要があります。
作業ディレクトリに両ファイルがある前提で、以下のコマンドでZIPファイルを作成します。qiita-action.zipが出力されるはずです。

Windows
powershell Compress-Archive -Path action.js,package.json -DestinationPath qiita-action.zip -Force
Mac/Linux
zip -r qiita-action.zip action.js package.json

アクションをCLIで登録

初回(新規登録)と2回目以降(更新)でコマンドの指定が変わるので注意してください。正常に登録できると ok が表示されます。

# 初回アクション作成時
ibmcloud fn action create qiita-action qiita-action.zip --kind nodejs:10
  ok: created action qiita-action

# 2回目以降(更新時)
ibmcloud fn action update qiita-action qiita-action.zip --kind nodejs:10
  ok: updated action qiita-action

ブラウザで確認

無事に、アクションが登録されていますね。今回はアクションを何かしらのFunctionsのパッケージに紐づけているわけではないので、デフォルト・パッケージ の部分に追加されているはずです。
image.png

起動してみよう

起動 をクリックすると、しばらく時間がかかってアクティベーションの欄に、前回のAPIを呼び出した結果が取得できていることがわかります。
吹き出しに記載しましたが、ZIPでアクションを登録すると、コード自体をブラウザで編集することはできなくなってしまいます。
image.png

パラメータも指定してみよう

前回操作したように、パラメータを付けて起動をクリックして、nameのパラメータを指定してみます。
image.png
起動してみると、無事にパラメータを前回公開したAPIに渡せているようです。
image.png

作成したアクションをAPIで公開

基本的に操作は、前回と同じです。
Functionsのトップページから API を画面左部から選択して、前回作成したAPIが表示されるので、そちらをクリックします。
image.png
次に、画面左部の定義および保護を選択し、操作の作成を選択して、新しいAPIを作成します。
image.png
今回のAPIはPOSTでリクエストを受取るようにしてみましょう。
アクションを含むパッケージでは、今回アクションが登録されているデフォルトを選択し、アクションの部分で、今回登録したアクションを選択していることを確認しましょう。
image.png
操作を作成したら、元の画面下部で保存を忘れずにクリックして更新してください。

公開されたAPIの確認

ブラウザで、今回登録したAPIを呼び出してみましょう。
image.png
今回はPOSTでリクエストを処理する設定にしているので、ブラウザにURLを貼り付けるだけのGETでは処理されないことが確認できました。想定通りですね。

では、VSCodeの拡張機能REST Clientを利用して POST で呼び出してみましょう。
無事にレスポンスで、前回公開したAPIの応答結果を受取れていることがわかります。
image.png

さいごに

いかがでしたでしょうか?Node.jsのrequestのパッケージを利用して、呼び出されたAPIが別のAPIを呼び出して、その結果を応答するということが出来ました。
つまり、IBM Cloudに関わらず、世の中のAPIを利用したIBM Cloud Functionsのアクションを作成する準備が完了したということです。
次回は、今回のコードをベースに、LINESlackのAPIを利用して、LINEに投稿された内容をSlackに通知する機能の実装について紹介したいと思います。
 第1回:IBM Cloud Functons 動かしてみた
 第2回:IBM Cloud FunctionsでNode.jsのパッケージを利用してみた
 第3回:IBM Cloud Functions でLINEとSlack連携させてみた

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

npx create-react-appで"Error: EPERM: operation not permitted, mkdir 'C:\Users\〇〇 ' command not found: create-react-app"

概要

node.jsをインストーラーで入れ直して直後、npx create-react-app hogeをした時に、

Error: EPERM: operation not permitted, mkdir 'C:\Users\〇〇 '
command not found: create-react-app

と出た時の対処法の覚書。

環境情報

Windows10
node.js v12.18.3

原因

create-react-appをすると、node.jsは自身のインストール場所に関わらず、デフォルトでC:ドライブのAppDataにcacheフォルダを作ろうとします。もしPCの名前に半角スペースが入っているとフォルダの作成に失敗するためエラーが生じます。

自分が行った対処法

npm config set cache <任意のpath> --global

としてcacheフォルダをパスに半角スペースを含まない場所に変えてやるといけました。
正直なところ --globalは必要なのかわかりませんが ノリで付けてやったらうまいこといけてしまったので、未検証 です。(あった方がいいのかなくても良いのか知っている人いれば教えてください)

ちなみに

この解決法を見つけたオリジナルのgithubのissue
では「半角スペース以下を~1にすれば行ける!」みたいなことが書かれていますが自分はそれでは解決しませんでした。とりあえず、cacheの設定をnpm condig set cacheで変えれるんやなって気付きにはなりましたが。

npmの作るフォルダーに関しては以下にも載っていて、一応globalの説明もしてあります。知識がないのでよくわからないですが......↓
https://docs.npmjs.com/configuring-npm/folders.html

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

GLTFモデルをNode.js上のヘッドレスなthree.jsで読み込み3Dの計算を行う(レンダリングは行わない)

概要

  • Node.jsで3Dの計算だけしたい
  • 画のレンダリングは不要
  • Raycasterによる当たり判定程度まではできることがわかった、それ以上は未検証

時間がない人向けの内容ざっくり(tl;dr)

  • GLTFLoaderをNode.js上で動くように改変することが必要
    • BufferをUint8Arrayに変換するメソッドの追加
    • それに伴うparseメソッド内でのmagic周辺の改変
  • 改造したGLTFLoader.parseにfs.readFileSyncしたBufferを食わせる
  • callbackの引数にオブジェクトが返るので、通常のGLTFLoaderのときのように、THREE.Sceneにロードできる

動機

シンプルなWeb上で動くオンライン3Dゲームを作りたい。

オンラインなのでサーバが必要。

オンラインかつ3Dなので、3D位置情報が同期的である必要があるだろうと考えた。

またサーバ側でマスタ3D位置情報を持つ必要もあるだろうと考えた。

前提

Web上で動く3Dゲームである以上フロントはThree.jsが楽だろうと考えた。Godot, Unity, Cocos2D などの選択肢もあるがjsのほうが慣れている。個人プロジェクトのため選定は自由。

一方でサーバ側でも3D情報を持つ必要がある。このためサーバ側でもThree.jsを使うと楽だろうと考えた。サーバ側でグラフィックを出す必要はないのでレンダリング等は不要だが、3Dの計算はサーバ側で行える必要がある。

幸いjavascriptは実行時に変数等が評価され解決されるため、windowオブジェクトやXHR、WebGL Rendererなどを呼び出すメソッドにさえ触れなければ、Three.jsのうち単なるjsで書かれている部分は実行環境非依存で動くはずであり、3Dの計算だけを行うことができるはずである。

また、サーバサイドでもゲームの情報である3Dモデルを読み込む必要がある。Three.jsにおいてはGLTFLoaderを用いることが多いのでこちらを用いることにした。

結果

少なくともモデルの読み込みと、Raycasterによる当たり判定などができる。

メインソースファイル(index.js)

const THREE = require('three');
const GLTFLoader = require('./gltf-loader');
const fs = require('fs');

const map = fs.readFileSync('map.glb', {encoding: null});

function init() {
  const scene = new THREE.Scene();

  var loader = new GLTFLoader();
  loader.parse(map, 'map.glb', (gltf)=>{
    scene.add(gltf.scene)
    const raycaster = new THREE.Raycaster(new THREE.Vector3(parseFloat(process.argv[2]), 500, parseFloat(process.argv[3])), new THREE.Vector3(0, -1, 0), 1, 2000);
    const intersects = raycaster.intersectObjects(scene.children, true);
    for ( var i = 0; i < intersects.length; i++ ) {
      console.log(intersects[ i ].distance)
    }
  })
}

init();

map.glb というGLTFファイルを読み込んでいる。Blenderでテストモデルとして作成した。PlaneをSubdivision Surface→Apply→適当に形状変更→Triangulateにて作成している。Export設定はSelected Objectsにしたこと以外デフォルト。以下の画像のような形状をしている。

map-glb.png

本index.jsファイルは実行時の第一引数と第二引数をRay位置の水平方向座標に割り当てることで、端的に言えば空中からマップ地形までの距離を測定するサンプルコードとなっている。

GLTFLoaderの改変

GLTFLoaderのソースはここにある。

そのまま用いると動かない。Node.jsに対応させるためにいくつか改変が要る。改変したGLTFLoaderを含めたプロジェクト/プロジェクトソースを配布する場合はライセンスに注意すること。

require構文への変更

import / export構文を前提としたコードになっているので、require構文に変更する。

diff抜粋は以下の通り

1c1
< import {
---
> var {
65c65
< } from "../../../build/three.module.js";
---
> } = require('three');
3665c3677
< export { GLTFLoader };
---
> module.exports = GLTFLoader;

toArrayBufferメソッドの追加

以下を追加する。

こちらを参考にして、len引数だけ追加した。

toArrayBuffer: function(buf, len) {
  len = len || buf.length
  var ab = new ArrayBuffer(len);
  var view = new Uint8Array(ab);
  for (var i = 0; i < len; ++i) {
      view[i] = buf[i];
  }
  return ab;
},

これを追加する理由は、fs.readFileSyncで以下のように読み込んでいるが、こちらがBufferであり、GLTFLoaderがインスタンスタイプが違うとエラーを出すため。

const map = fs.readFileSync('map.glb', {encoding: null});

parseメソッド中でtoArrayBufferメソッドを使用するように変更

以下のdiff抜粋のように変更する。以下の変更によってindex.jsコードは動くようになるはずである。

なお、なるべくdiffが少なくなるようにコードを書いてしまったので、実際にはparseメソッドのdata引数をbufなどにリネームしたりしてdata変数の再宣言を行わない方がコードの治安が良いと思う。そちらは読者の方々の方で適宜やっていただければ幸いである。

234,235c244,247
< 
<                               var magic = LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) );
---
> 
>                               var magicSrc = this.toArrayBuffer(data, 4);
>                               var data = this.toArrayBuffer(data);
>                               var magic = LoaderUtils.decodeText(magicSrc);
254c266
<                                       content = LoaderUtils.decodeText( new Uint8Array( data ) );
---
>                                       content = LoaderUtils.decodeText( data );
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む