20200407のJavaScriptに関する記事は21件です。

PdfMakeを使ったPDF作成APIをZeit Nowで動かしたメモ

概要

PDFをサーバサイドで作成し、ダウンロードするAPIを作成した。
ローカルで動いていたものがデプロイ時に動かないなどのトラブルがあったが、
最終的には成功できた。

作成したソース

ローカル環境

  • node:13.12.0
  • Now CLI 17.1.1

フォルダ構成

- api
  - pdf.ts
- fonts
  - ipaexg.ttf
  - ipaexm.ttf
- src
  - pages
    - index.tsx
    - PdfArea.tsx
- .babelrc.js
- next-env.d.ts
- next.config.js
- now.json
- package.json
- tsconfig.json

ハマった点

APIをsrc/pages/api/pdf.tsに書いていたとき、ローカルで動かすときには成功し、デプロイしたときにエラーとなる現象が発生した。
デプロイ時にはfontsフォルダが見えなくてファイルが開けない問題が発生していた。
apiはソース通りのフォルダ構成ではなく、ラムダ環境にコンパイル済で配置されていたことが原因と考えられる。
apiファイルの場所を、src/pages/apiから直下のapiに移し、now.jsonにincluceFilesを追加することで解決した。

now.json
{
  "version": 2,
  "functions": {
    "api/pdf.ts": {
      "includeFiles": "fonts/**"
    }
  }
}

incluceFilesを使うときは、nextの仕組みのpages/apiは使えない模様。
以下では、コンパイルは通るが、フォルダがアップロードされなかった。

now.jsonのうまくいかなかった例
{
  "version": 2,
  "functions": {
    "src/pages/api/pdf.ts": {
      "includeFiles": "fonts/**"
    }
  }
}

ファイル

src/pages/index.tsx
import Head from 'next/head'
import * as React from 'react'
import { NextPage } from 'next'
import PdfArea from './PdfArea'

const Home: NextPage = () => {
  return (
    <div className="container">
      <main>
        <PdfArea />
      </main>
    </div>
  )
}

export default Home
src/pages/PdfArea.tsx
import React, { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import scenarioModule, {
  useScenario,
  usePdf
} from '../store/modules/scenarioModule'

const makePdf = async (scenario, dispatch) => {
  const pdf = await (
    await fetch('/api/pdf', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json; charset=utf-8' },
      body: JSON.stringify(scenario)
    })
  ).text()
  dispatch(scenarioModule.actions.setPdf(pdf))
}

const PdfArea: React.FC = () => {
  const scenario = useScenario()
  const pdf = usePdf()
  const dispatch = useDispatch()

  if (!scenario) {
    return <div>読込失敗</div>
  }
  return (
    <>
      <button onClick={(e) => makePdf(scenario, dispatch)}>PDFを作る</button>
      {pdf !== '' && (
        <a href={pdf} download="scenario.pdf">
          作成したPDFをダウンロード
        </a>
      )}
    </>
  )
}

export default PdfArea
api/pdf.ts
import { NextApiRequest, NextApiResponse } from 'next'
import path from 'path'
import PdfPrinter from 'pdfmake'
import { Scenario } from '../src/store/modules/scenarioModule'

function createPdfBinary(pdfDoc, callback) {
  const baseDir = 'fonts/'
  const gPath = path.resolve(baseDir + 'ipaexg.ttf')
  const mPath = path.resolve(baseDir + 'ipaexm.ttf')
  const fontDescriptors = {
    IPASerif: {
      normal: mPath,
      bold: mPath,
      italics: mPath,
      bolditalics: mPath
    },
    IPAGothic: {
      normal: gPath,
      bold: gPath,
      italics: gPath,
      bolditalics: gPath
    }
  }
  const printer = new PdfPrinter(fontDescriptors)
  const doc = printer.createPdfKitDocument(pdfDoc)
  const chunks = []
  doc.on('data', function(chunk) {
    chunks.push(chunk)
  })
  doc.on('end', function() {
    const result = Buffer.concat(chunks)
    callback('data:application/pdf;base64,' + result.toString('base64'))
  })
  doc.end()
}

export default (req: NextApiRequest, res: NextApiResponse) => {
  const scenario: Scenario = req.body
  const docDefinition = {
    content: [
      { text: scenario.title, fontSize: 55, font: 'IPASerif' },
    ],
    defaultStyle: {
      font: 'IPAGothic',
      alignment: 'center'
    }
  }
  createPdfBinary(docDefinition, (binary) => {
    res.setHeader('Content-Type', 'application/json')
    res.status(200).send(binary)
  })
}
tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./src/*"
      ],
    },
    "target": "es6",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ]
}
next.config.js
const { resolve } = require('path')
const withOffline = require('next-offline')

const nextConfig = {
  webpack: (config) => {
    // src ディレクトリをエイリアスのルートに設定
    config.resolve.alias['~'] = resolve(__dirname, 'src')
    return config
  },
  // manifest設定
  target: 'serverless',
  transformManifest: (manifest) => ['/'].concat(manifest),
  generateInDevMode: true,
  workboxOpts: {
    swDest: 'static/service-worker.js',
    runtimeCaching: [
      {
        urlPattern: /^https?.*/,
        handler: 'NetworkFirst',
        options: {
          cacheName: 'https-calls',
          networkTimeoutSeconds: 15,
          expiration: {
            maxEntries: 150,
            maxAgeSeconds: 30 * 24 * 60 * 60 // 1 month
          },
          cacheableResponse: {
            statuses: [0, 200]
          }
        }
      }
    ]
  }
}

// PWA に対応
module.exports = withOffline(nextConfig)
.babelrc.js
// ローカルの開発サーバー側の SSR 時と クライアント側のCSR 時に styled-components が付与するクラス名に差が生まれるエラーの対応
module.exports = {
  presets: ['next/babel'],
  plugins: [
    ['styled-components', { ssr: true, displayName: true, preprocess: false }]
  ]
}

参考

including-additional-files
pdfmake
IPAex フォント ダウンロードページ
IPAex フォント ダウンロードページ
Zeit Now の具体的な Tips 集
Javascript で PDF を作成する

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

Kinx ライブラリ - Integer

Integer

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。言語はライブラリが命。ということでライブラリの使い方編。

リポジトリ(https://github.com/Kray-G/kinx) のほうの説明を "Looks like JavaScript, feels like Ruby, and it is the script language fitting in C programmers." に変えてみた。意味的には例の名探偵のオマージュですが、英語の表現は違っていてオリジナルです。

今回は Integer です。

Integer オブジェクトに括り付いたメソッドは特殊メソッドで、整数値に直接作用させることができる。特殊メソッド、および特殊オブジェクトに関する詳細は Kinx ライブラリ - String を参照してください。

Integer 特殊オブジェクト

Integer オブジェクトに対して関数定義する例は以下の通り。

Integer.times100 = function(value) {
    return value * 100;
};
var val = 100.times100();
System.println(val);

実行してみよう。

10000

レシーバーが第 1 引数に来ます。

Integer

組み込み特殊メソッド

メソッド 意味
Integer.times(val, callback) i = 0 ~ (val - 1) の範囲として、callback があれば callback(i) の結果で、無ければ i で配列を作成して返す。
Integer.upto(val, max, callback) i = val ~ max の範囲で引数として callback(i) を呼ぶ。
Integer.downto(val, min, callback) i = min ~ val の範囲で引数として callback(i) を呼ぶ。
Integer.toString(val, base) val を文字列に変換する。base は 10 と 16 のみサポート。
Integer.toDouble(val) val を Double に変換する。

Math オブジェクト・メソッド

Integer オブジェクトには Math オブジェクトと同じ特殊メソッドが存在する。詳細は以下を参照。

具体例で書くと、例えば以下のように書ける。

var a = 2.pow(10);     // Math.pow(2, 10) と同じ  => 1024
var b = (-10).abs();   // Math.abs(-10)   と同じ  => 10

単項マイナス(-)は関数呼び出しより優先順位が低いため、カッコで括る必要があることに注意。

特殊オペレーター

単項 * オペレーター

単項 * オペレーターを整数値に適用した場合、文字コードに対応した文字列(1 文字)を返す。

var a = *97;  // => "a"

ちなみに逆変換(*a)は元に戻らない。文字列に対する単項 * オペレーターは配列になるため、上記例で *a とすると [97] と配列となることに注意。単独で文字コードを得るには、a[0] とする。

おわりに

Integer に特殊メソッドがあることでグッと Ruby っぽい感じがしてますが気のせいですかね。2.pow(10) とか書けるのが結構感慨深いなー。

ここ 見ながら、今サポートしてないメソッドとかサポートしてみようかなー、と。

ではまた次回。

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

【Node.js】複数のファイルパスをオブジェクトでの階層表現に変換する

経緯

Node.jsのfsを使って特定のディレクトリの中身をファイルパスにて取得した際に、ディレクトリの階層と互換性のあるオブジェクトに変換したかった。

結論

index.js
const data = [
  '/public/aaa/1.file',
  '/public/aaa/2.file',
  '/public/bbb/1.file',
  '/public/ccc/1.file',
  '/public/ccc/2.file',
  '/public/ccc/3.file',
  '/public/ddd/1.file'
];

const output = {};
let current;

for (const path of data) {
    current = output;
    for (const segment of path.split('/')) {
        if (segment !== '') {
            if (!(segment in current)) {
                current[segment] = {};
            }
            current = current[segment];
        }
    }
}

console.log(output);

/* 実行結果
{ public:
   { 
     aaa: { '1.file': {}, '2.file': {} },
     bbb: { '1.file': {} },
     ccc: { '1.file': {}, '2.file': {}, '3.file': {} },
     ddd: { '1.file': {} } 
   } 
}
*/

あとは取得した結果から

子要素に値が入っていたらディレクトリ
子要素に値が入っていなかったらファイル

と認識して分岐させれば色々使えると思う。

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

googleスプレッドシートのPDF化を楽にするブックマークレット

googleスプレッドシートをPDF化するときの自分用設定をブックマークレット化してみました。

javascript:(function(){const current = location.href;if (current.match(/^.+docs\.google\.com\/spreadsheets\/d\//)) {const m = current.match(/^.+\/d\/(.+)\/edit#gid=(.+)$/);const printUrl = `https://docs.google.com/spreadsheets/d/${m[1]}/export?exportFormat=pdf&format=pdf&size=A4&portrait=false&sheetnames=false&printtitle=false&fitw=true&pagenumbers=false&gridlines=false&top_margin=0.75&bottom_margin=0.75&left_margin=0.25&right_margin=0.25&horizontal_alignment=LEFT&vertical_alignment=TOP&fzr=false&gid=${m[2]}`;window.open(printUrl);}})()

下記のように各種パラメータを設定しています。

  • exportFormat=pdf format=pdf PDF形式
  • size=A4 A4サイズ
  • portrait=false 横向き印刷
  • sheetnames=false シート名を印刷しない
  • printtitle=false タイトルを印刷しない
  • fitw=true 幅に揃える
  • pagenumbers=false ページ番号を表示しない
  • gridlines=false グリッド線を印刷しない
  • top_margin=0.75 bottom_margin=0.75 上下余白0.75インチ(プリセットの余白「狭」)
  • left_margin=0.25 right_margin=0.25左右余白0.25インチ(プリセットの余白「狭」)
  • horizontal_alignment=LEFT vertical_alignment=TOP 左上寄せ
  • fzr=false 固定行を表示しない

コード

(function(){
    const current = location.href;
    if (current.match(/^.+docs\.google\.com\/spreadsheets\/d\//)) {
        const m = current.match(/^.+\/d\/(.+)\/edit#gid=(.+)$/);
        const printUrl = `https://docs.google.com/spreadsheets/d/${m[1]}/export?exportFormat=pdf&format=pdf&size=A4&portrait=false&sheetnames=false&printtitle=false&fitw=true&pagenumbers=false&gridlines=false&top_margin=0.75&bottom_margin=0.75&left_margin=0.25&right_margin=0.25&horizontal_alignment=LEFT&vertical_alignment=TOP&fzr=false&gid=${m[2]}`;
        window.open(printUrl);
    }
})()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptで学ぶデザインパターン〜Factory Method編〜

対象読者

  • デザインパターンを学習あるいは復習したい方
  • TypeScriptが既に読めるあるいは気合いで読める方
    • いずれかのオブジェクト指向言語を知っている方は気合いで読めると思います
  • UMLが既に読めるあるいは気合いで読める方

環境

  • OS: macOS Mojave
  • Node.js: v12.7.0
  • npm: 6.14.3
  • TypeScript: Version 3.8.3

本シリーズ記事一覧(随時更新)

Factory Methodパターンとは

インスタンス生成における大枠を持たせるためのパターンです。

factoryというのが工場という意味であることを知っておくと理解が進みやすいと思います。

サンプルコード

Factory Methodパターンで作られたクラス群がどんなものになるのか確認していきましょう。

今回は、題材として"PCの製造"を想定します。GitHubにも公開しています。

modules/framework/Product.ts

製品を表現する抽象クラスです。

Product.ts
export default abstract class Product {
  public abstract check(): void;
}

checkメソッドのみ定義されています。少なくとも"動作確認ができるモノ"という定義がなされています。

modules/framework/Factory.ts

工場を表現する抽象クラスです。

Factory.ts
import Product from '../framework/Product';

export default abstract class Factory {
  create(id: string): Product {
    const product: Product = this.createProduct(id);
    this.registerProduct(product);

    return product;
  }

  protected abstract createProduct(id: string): Product;
  protected abstract registerProduct(product: Product): void;
}

"Productのインスタンスを製造する工場"という定義がなされています。具体的には、インスタンスを作成(createProduct)して登録(registerProduct)するという大枠をcreateメソッドが規定しています。

ちなみに本クラスとそのサブクラスはTemplate Methodパターンになっています。createメソッドがテンプレートメソッドになっています。

modules/pc/Pc.ts

具体的な製品を表現するクラスです。

Pc.ts
import Product from '../framework/Product';

export default class Pc extends Product {
  private id: number;

  constructor(id: number) {
    super();
    console.log('製造番号' + id + 'のPCを製造します。');
    this.id = id;
  }

  check(): void {
    console.log('製造番号' + this.id + 'のPCは正常に動作しています。');
  }
}

modules/pc/PcFactory.ts

具体的な工場を表現するクラスです。

PcFactory.ts
import Product from '../framework/Product';
import Factory from '../framework/Factory';
import Pc from './Pc';

export default class PcFactory extends Factory {
  private pcList: Pc[];
  private last: number;

  constructor(lot: number) {
    super();
    this.pcList = new Array(lot);
    this.last = 0;
  }

  protected createProduct(id: number) {
    return new Pc(id);
  }

  protected registerProduct(product: Product) {
    this.pcList[this.last] = product as Pc;
    this.last++;
  }
}

単にPcのインスタンスを作成するだけでなく登録処理(registerProduct)も実現しています。登録といっても配列につめているだけではありますが、インスタンス生成時の手順を規定できているところがポイントです。

Main.ts

Factory Methodパターンで作られたクラス群を実際に使う処理が書かれています。

Main.ts
import Factory from './modules/framework/Factory';
import Product from './modules/framework/Product';
import PcFactory from './modules/pc/PcFactory';

const factory: Factory = new PcFactory(3);

const pc1: Product = factory.create(1);
const pc2: Product = factory.create(2);
const pc3: Product = factory.create(3);

pc1.check();
pc2.check();
pc3.check();

クラス図

ここまでFactory Methodパターンで作られたクラス群を1つずつ確認してきました。次にクラス図を示します。Factory Methodパターンの全体像を整理するのにお役立てください。

FactoryMethod.png

  • 枠組み
    • Product: サンプルコードではProductが対応
    • Creator: サンプルコードではFactoryが対応
  • 肉付け
    • ConcreteProduct: サンプルコードではPcが対応
    • ConcreteCreator: サンプルコードではPcFactoryが対応

解説

最後に、このデザインパターンの存在意義を考えます。

とはいっても、Template Methodの応用なのでTypeScriptで学ぶデザインパターン〜Template Method編〜と同様です。インスタンス生成時の手順を切り出すことができます。

ProductFactoryに全く手を加えずともPcPcFactoryといった具体的な製品や工場を追加していくことができるのです。

補足

サンプルコードの実行方法はこちらと同様です。

参考

あとがたり

Laravelなどのアプリケーション開発フレームワークでFactoryは見てきたので、理解が進みやすいデザインパターンだった。

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

100日後にエンジニアになるキミ - 18日目 - Javascript - JavaScriptの基礎1

今日からプログラミングの方に入って行きます。
本日はJavaScriptというプログラム言語についてのお話です。

JavaScriptについて

JavaScriptはwebブラウザーなどで動作するスクリプト言語で
ESxxというバージョンで呼ばれたりしています。

6th editionの「ECMAScript2015」以降は年号で呼ばれており
新仕様は従来のものよりも効率的にコードが書けます。

JavaScriptはHTMLのスクリプトタグに直接記述するか
jsファイルとして分離しておく方法があります。

jsファイルはスクリプトタグの中身を書き
.js と拡張子を付けて保存します。

なおJavaScriptとJavaは別物であるため
略称をJavaにすることは混乱を招きます、間違えないようにしましょう。

もしJavaって呼んでいる人がいたら
DS(だっせえ!!!)って言ってあげましょう。

略称は js です。jkでもjdでもなくwwwwww

Javascriptはブラウザ上ではwebサイトの機能部分を司ります。
PCやスマホなど、ブラウザ側のことをクライアントと読んでいます。

一方で、JavaScriptはサーバー上でも動きます。
こちら側はサーバーサイドと読んでいたりします。

クライアントサイドだけでなく
Node.jsなどでサーバー側でも処理をさせることができるので
両側を同じ言語で書けることも大きな利点となります。

フロントエンドエンジニアになるならば必須
サーバーサイドエンジニアになるとしても覚えておいて
損はないと思います。

習得にはやや癖があるので初めて覚える言語としては
難しいかもしれません。

もし難しいと感じた場合は、後日行うPython言語の講義を先に見てから
こちらを参照してみてください。

JavaScriptの動かし方

HTMLのscriptタグに書く方法

jsファイルに書いてHTMLから呼び出す方法

ブラウザーで直接記述して動かす方法

などがあります。

もっとも簡単な方法として
ブラウザーから動かしてみましょう。

GoogleChromeを立ち上げて「右クリック」から「検証」を選択。

スクリーンショット 2020-04-07 18.29.58.png

検証ツールを開いたらメニューの中から「Console」を選択。

スクリーンショット 2020-04-07 18.31.19.png

これでスクリプトを直接打ち込む準備ができました。

試しに 1 + 1 とか打ち込んで「エンターキー」を押して実行してみましょう。

スクリーンショット 2020-04-07 18.32.12.png

コードの実行結果が反映されるはずです。
短めのコードの確認は簡単に行うことができます。

次にHTMLに打ち込んでみましょう。

sample.html
<!DOCTYPE html>
<html>
  <head>
    <script>
        console.log('おはよう');
    </script>
  </head>
  <body>
  </body>
</html>

HTMLでスクリプトを実行させるには
スクリプトタグ<script> ~ </script>が必要です。

タグの中に実行するコードを書きます。

ブラウザでみてみましょう。

スクリーンショット 2020-04-07 18.37.02.png

何も写っていませんね。

上記のコードconsole.logはコンソール上に結果を出力するコードになっています。
コンソールは検証ツールの中にありますので、検証ツールからConsoleをみてみましょう。

スクリーンショット 2020-04-07 18.38.21.png

コンソールの方には結果が反映されています。

Javascriptではコンソールにだけ表示をさせたり
HTMLの方に反映させたり、結果の出力先を制御できます。

確かめながらコードを書く際はコンソールに出力すると分かりやすいです。

コードはheadタグbodyタグの中に書くことができます。

次にjsファイルに分離して書く方法です。
まずはjsファイルを用意します。

sample.js
console.log('sample.js');

HTML側では読み込むjsファイルの指定を行います。
<script type="text/javascript" src="ファイルパス"></script>

jsファイルをHTMLファイルと同じ場所においている場合は
jsファイル名を書くだけで実行できます。

sample.html
<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript" src="sample.js"></script>
  </head>
  <body>
  </body>
</html>

結果を見てみると

スクリーンショット 2020-04-07 18.46.00.png

動かし方は自由ですが
WEBサイトとして公開する場合、ScriptファイルとHTMLファイルを分けておくと
複数人で開発する場合、後々都合が良くなります。

最初からjsファイルに書き出しておいて、それをHTMLから呼び出すのが良いでしょう。

通常のWEBサイトの場合
CSS , Javascript , 画像などのファイル種別ごとに階層を区切って
配置することがよくあります。

コードをどこに書くのかは分かりやすい配置を考えながら書くと良いでしょう。

まとめ

JavaScriptの動かし方を把握しておき
すぐに動かせる体制を取っておこう

明日からはプログラムの文法に。

君がエンジニアになるまであと82日

作者の情報

乙pyのHP:
http://www.otupy.net/

Youtube:
https://www.youtube.com/channel/UCaT7xpeq8n1G_HcJKKSOXMw

Twitter:
https://twitter.com/otupython

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

opencv.jsのダウンロード方法

結論

以下のコマンドでとってこれる。(ver 4.3.0)

curl https://docs.opencv.org/4.3.0/opencv.js -o opencv.js

OpenCV.jsとは

公式から出ているOpenCVのJavascript版です。

https://docs.opencv.org/4.3.0/d5/d10/tutorial_js_root.html

公式での入手方法

一応、公式ではopencv.jsに関しては自前でビルドするように書いてある。
https://docs.opencv.org/4.3.0/d4/da1/tutorial_js_setup.html
これをwslのUbuntu18.04でやってみたが、環境変数周りがうまくいかないのか、ビルドできなかった。

↓これのあと

$ source ./emsdk_env.sh
Setting environment variables:
EMSDK = /mnt/c/Users/hattt/Desktop/emsdk

This is rm_wrap: Do not use any options.

↓これになってビルドが通らない

$ python ./platforms/js/build_js.py build_js
Args: Namespace(build_dir='build_js', build_doc=False, build_flags=None, build_perf=False, build_test=False, build_wasm=False, build_wasm_intrin_test=False, clean_build_dir=False, cmake_option=None, config='/mnt/c/Users/hattt/Desktop/opencv/platforms/js/opencv_js.config.py', config_only=False, disable_wasm=False, emscripten_dir=None, enable_exception=False, opencv_dir='/mnt/c/Users/hattt/Desktop/opencv', simd=False, skip_config=False, threads=False)
Cannot get Emscripten path, please specify it either by EMSCRIPTEN environment variable or --emscripten

ここで粘ってもよかったのだが、もうめんどくさくなったので公式のサイト上で動いているビルド済みのものをダウンロードすることにした。
意外とダウンロードについて触れている記事がなかったので記述しておく。

↓赤丸のところにある
opencvjs.png

ダウンロード方法

curlでさくっと取ってくる。
取ってきたもので試してみたが、問題なく動いた。
ビルドしないといけない理由をいまいち理解していないので、不具合もしかしたら使用方法によっては不具合があるのかも。
curlコマンドはWindowsでも最近実装されたので、どのOSでも行けるはず。

curl https://docs.opencv.org/4.3.0/opencv.js -o opencv.js
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

htmlで共通メニュー部分をajaxで対応させる

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>タイトル</title>

<script
  src="https://code.jquery.com/jquery-3.4.1.js"
  integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU="
  crossorigin="anonymous"></script>
  <script type="text/javascript" src="template.js"></script>
</head>

<body>
  <script type="text/javascript">header();</script>
  <script type="text/javascript">footer();</script>
</body>
</html>
footer.html
表示させたい内容を記入
template.js
function footer(){
    $.ajax({
        url: "footer.html",
        cache: false,
        async: false,
        dataType: 'html',
        success: function(html){
            document.write(html);
        }
    });

}

function header(){
    $.ajax({
        url: "header.html",
        cache: false,
        async: false,
        dataType: 'html',
        success: function(html){
            document.write(html);
        }
    });

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

【React】ルーティング画面を作ってみた【react-router-dom】

React Router: Declarative Routing for React.js
このサイトを参考に自分なりにアレンジ

import React from 'react';
import { useSelector } from "react-redux";
import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import Top from './components/Top';
import Nav from './components/Nav';
import SignIn from './components/SignIn';
import SignUp from './components/SignUp';

export default () => {
  return (
    <Router>
      <Switch>
        <Route path="/signin" component={SignIn} />
        <Route path="/signup" component={SignUp} />
        <PrivateRoute path="/" token={token}>
          {/* ↓↓↓ ここに書かれているものが children に渡される ↓↓↓ */}
          <Top />
          <Nav />
          {/* ↑↑↑ ここに書かれているものが children に渡される ↑↑↑ */}
        </PrivateRoute>
      </Switch>
    </Router>
  );
}

const PrivateRoute = ({ children, ...rest }) => {
  const token = useSelector(state => state.token);

  return (
    <Route
      {...rest}
      render={({ location }) => {
        if (isAuthenticated(token)) {
          // 認証済みの場合は、トップページを表示する
          return children;
        }
        if (location.pathname === "/") {
          // URL が "/" の場合はサインインページを表示する
          return <SignIn />;
        }
        // 未認証で、URL が "/" 以外の場合は "/" にリダイレクトする
        // URL が "/" となるので、結果的にサインインページが表示される
        return <Redirect to={{ pathname: "/" }} />;
      }}
    />
  );
}

// 認証判定用のダミー関数
const isAuthenticated = token => token !== null;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js: child_process.fork()で起動したプロセスを子子孫孫殺す方法

本稿では、Node.jsにて、子プロセス、そこから派生した孫プロセス、さらにそこから派生したひ孫プロセス……を、一括して終了する方法を説明します。

※説明にあたって、実行環境はUNIX/Linuxを前提にしています。

子プロセスを殺しても、孫プロセスは死なない

Node.jsのchild_process.fork()は、子プロセスを起動できて便利です。子プロセスの中で、fork()を使って、孫プロセスを起動することもでき、さらに、孫プロセスでfork()して、ひ孫プロセスを、といった具合に子プロセスはネストして起動することができます。

起動した子プロセスはsubprocess.kill()で終了することができます。しかし、これは直接の子プロセスしか殺すことができません。どういうことかというと、

  1. oya.js が ko.js のプロセスを起動する。
  2. ko.js が mago.js のプロセスを起動する。
  3. このとき、 oya.js が ko.js のプロセスをkill()したとする。
  4. ko.js は終了する。
  5. mago.js は生存する。 (※このとき、 mago.js はinitプロセスの養子に出され、親pidは1になる)

といった事態が発生します。

孫プロセスが残存するサンプルコード

上のようなシナリオを再現できるコードを書いてみたいと思います。

まず、oya.jsの実装:

oya.js
console.log('oya.js: running')

// SIGINTを受け付けたとき
process.on('SIGINT', () => {
  console.log('oya.js: SIGINT')
  process.exit()
})

// プロセスが終了するとき
process.on('exit', () => {
  console.log('oya.js: exit')
})

// 子プロセスを起動
const ko = require('child_process')
  .fork(__dirname + '/ko.js')

// 3秒後にproc2.jsを終了する
setTimeout(() => {
  console.log('oya.js: ko.jsを終了させてます...')
  ko.kill('SIGINT')
}, 3000)

// ko.jsが終了したとき
ko.on('exit', () => {
  console.log('> Ctrl-Cを押してください...')
})

// このプロセスがずっと起動し続けるためのおまじない
setInterval(() => null, 10000)

oya.jsはko.jsを起動し、3秒後にko.jsを終了するコードになっています。ko.jsをkill()する際には、SIGINTシグナルを送るようにしています。Linuxのシグナルについては、ここでは詳しく説明しません。ここでは単にSIGINTシグナルはプロセス終了を指示するものと考えてください。

次に、ko.js:

ko.js
console.log('ko.js: running')

// SIGINTを受け付けたとき
process.on('SIGINT', () => {
  console.log('ko.js: SIGINT')
  process.exit()
})

// プロセスが終了するとき
process.on('exit', () => {
  console.log('ko.js: exit')
})

// 孫プロセスを起動する
require('child_process')
  .fork(__dirname + '/mago.js')

// このプロセスがずっと起動し続けるためのおまじない
setInterval(() => null, 10000)

最後に、mago.js:

mago.js
console.log('mago.js: running')

// SIGINTを受け付けたとき
process.on('SIGINT', () => {
  console.log('mago.js: SIGINT')
  process.exit()
})

// プロセスが終了するとき
process.on('exit', () => {
  console.log('mago.js: exit')
})

// このプロセスがずっと起動し続けるためのおまじない
setInterval(() => null, 10000)

このコードを実行してみます:

$ node oya.js
oya.js: running
ko.js: running
mago.js: running
oya.js: ko.jsを終了させてます...
ko.js: SIGINT
ko.js: exit
> Ctrl-Cを押してください...

3秒後にこのような出力がされ、oya.jsがko.jsをkill()し、ko.jsが終了したことが確認できます。

一方、mago.jsはまだSIGINTを受け取っていませんし、終了もしておらず、残存しています。

ここで、Ctrl-Cを押すと、oya.jsとmago.jsにSIGINTが送信されます:

...
> Ctrl-Cを押してください...
^Coya.js: SIGINT
mago.js: SIGINT
mago.js: exit
oya.js: exit

このタイミングではじめて、mago.jsが終了することが分かります。

感想を言うと、ko.jsにSIGINTを送信したら、mago.jsにもSIGINTが伝搬されていくものと誤解していたので、この結果は意外でした。

起動したプロセスを子子孫孫殺す方法

では、起動した子プロセスをkill()したタイミングで、孫プロセスも終了になるようにするにはどうしたらいいのでしょうか? それについて、ここで説明したいと思います。

プロセスグループ = 「世帯」

まず、Linuxのプロセスの基本として、プロセスグループというものがあります。これはプロセスの「世帯」のような概念で、親プロセス、子プロセス、孫プロセスをグループ化するものです。たとえば、Bashでnodeプロセスであるoya.jsを起動すると、そこからfork()したko.jsやmago.jsは、同じプロセスグループに属し、同一のグループIDが与えられます。

psコマンドでグループID(GPID)を確認すると、現に同じグループIDが3つのnodeプロセスに割り当てられていることが分かります:

$ ps -xo pid,ppid,pgid,command | grep node | grep .js
PID   PPID  GPID  COMMAND
17553  3528 17553 node oya.js
17554 17553 17553 node ko.js
17555 17554 17553 node mago.js

この結果をよく見ると分かりますが、GPIDはoya.jsのプロセスID(PID)と同じです。つまり、親のPIDが子孫のGPIDになるわけです。

プロセスを「世帯」ごと殺す方法

Node.jsでは、グループIDを指定して、プロセスを終了させることができます。やりかたは、process.kill()にGPIDを渡すだけです。このとき、与える値は負の数にしてあげます。正の数を渡してしまうと、プロセスグループではなく個別のプロセスをkill()するだけになるので注意です。

const groupId = 123456
process.kill(-groupId, 'SIGINT')

ちなみに、シェルでCtrl-Cを押したときに、親・子・孫がもろとも終了されるのは、Ctrl-Cが送るSIGINTが親プロセスに対してではなく、プロセスグループに対して送られているからです。(要出典)

detached = 別世帯を作る

今回やりたいことは、oya.jsのプロセスは生かしつつ、ko.jsとmago.jsをkill()したいことです。しかし、GPIDを指定したkill()では、oya.jsまで終了してしまいます。三者とも同じGPIDだからです:

PID   PPID  GPID  COMMAND
17553  3528 17553 node oya.js
17554 17553 17553 node ko.js
17555 17554 17553 node mago.js

ko.jsとmago.jsを別のGPIDを割り振る必要があります。それをするには、fork()のオプションにdetachedを指定します。

oya.js
// 子プロセスを起動
const ko = require('child_process')
  .fork(__dirname + '/ko.js', [], {detached: true})

これを指定すると、ko.jsとmago.jsがいわば「別世帯」になり、別のプロセスグループに属するようになります。GPIDもoya.jsとは別のものが割り当てられているのが確認できます:

$ ps -xo pid,ppid,pgid,command | grep node | grep .js
PID   PPID  GPID  COMMAND
21404  3528 21404 node oya.js
21405 21404 21405 node ko.js
21406 21405 21405 node mago.js

プロセスを子子孫孫殺すoya.jsの完成形

以上を踏まえて、oya.jsを子プロセス、孫プロセスを一括して終了できるように変更すると次のようになります:

oya.js
console.log('oya.js: running')

// SIGINTを受け付けたとき
process.on('SIGINT', () => {
  console.log('oya.js: SIGINT')
  process.exit()
})

// プロセスが終了するとき
process.on('exit', () => {
  console.log('oya.js: exit')
})

// 子プロセスを起動
const ko = require('child_process')
  .fork(__dirname + '/ko.js', [], {detached: true}) // 重要な変更箇所!

// 3秒後にko.jsを終了する
setTimeout(() => {
  console.log('oya.js: ko.jsを終了させてます...')
  process.kill(-ko.pid, 'SIGINT') // 重要な変更箇所!
}, 30000)

// ko.jsが終了したとき
ko.on('exit', () => {
  console.log('> Ctrl-Cを押してください...')
})

// このプロセスがずっと起動し続けるためのおまじない
setInterval(() => null, 10000)

最後に、このoya.jsを実行して、ko.jsとmago.jsが一緒に終了しているか確認してみましょう:

$ node oya.js
oya.js: running
ko.js: running
mago.js: running
oya.js: ko.jsを終了させてます...
mago.js: SIGINT
ko.js: SIGINT
mago.js: exit
ko.js: exit
> Ctrl-Cを押してください...
^Coya.js: SIGINT
oya.js: exit

期待通り、ko.jsとmago.jsは同じタイミングでSIGINTを受け取り終了しています。oya.jsはCtrl-Cを押すまで生存していることも分かります。

以上、Node.jsのchild_process.fork()で起動したプロセスを子子孫孫殺す方法についての説明でした。

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

Nuxt.js Vuetify2系でFontAwesome5(無料)とmaterial-icon、Font Awesome SVGを導入する

はじめに

Nuxt.jsでVuetify2系を使っている場合に、FontAwesome5(無料)を導入する方法がわからなくて、調べちゃったので備忘録として残します。

以下の手順でGithubで公開しています。

https://github.com/idani/nuxt-vuetify-fontAwesome5

初期状態

Nuxt.jsでVuetifyを導入した初期プログラムを作成しました。
ダークモードは見づらいのでオフにして、
index.vueを以下のように修正しています。

<template>
  <v-layout column justify-center align-center>
    <v-flex xs12 sm8 md6>
      <v-card>
        <v-row justify="space-around">
          <v-col>
            <v-icon>mdi-domain</v-icon> //デフォルトのMaterial Design Icons
            <v-icon>home</v-icon> //Material Icons
            <v-icon>fas fa-lock</v-icon> //Font Awesome 5
          </v-col>
        </v-row>
      </v-card>
    </v-flex>
  </v-layout>
</template>

<script>
export default {}
</script>

v-iconで、Material Design Iconsフォント(以後MDI)はデフォルトで表示されました。
Material Iconsフォント(以後MI)、Font Awesome 5フォント(fa5)は表示されません。

2020-04-07_14h49_39.png

FontAwesome5の導入

vuetifyFont Awesome 5 アイコンのインストールを参考にしています。

まずは、フォントの導入

yarn add @fortawesome/fontawesome-free -D

Pluginsフォルダにvuetify.jsを作成して、ドキュメントの通りに記載

src/plugins/vuetify.js
// src/plugins/vuetify.js

import '@fortawesome/fontawesome-free/css/all.css' // Ensure you are using css-loader
import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

export default new Vuetify({
  icons: {
    iconfont: 'fa',
  },
})

nuxt.config.jsに先程作成したplugins/vuetifyを追記します。

nuxt.config.js
~~~省略
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: ['plugins/vuetify'],
  /*
   ** Nuxt.js dev-modules
   */
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module',
    '@nuxtjs/vuetify'
  ],
  /*
~~~省略

FontAwesome5のフォントが表示されるようになりました。

2020-04-07_15h02_00.png

material-iconの導入

Material Iconsのインストールを参考に進めます。

material-iconをインストールします。

yarn add material-design-icons-iconfont -D

plugins/vuetify.jsを以下のように修正します。

src/plugins/vuetify.js
// src/plugins/vuetify.js

import '@fortawesome/fontawesome-free/css/all.css' // Ensure you are using css-loader
import 'material-design-icons-iconfont/dist/material-design-icons.css'
import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

export default new Vuetify({
  icons: {
    iconfont: ['fa', 'md']
  }
})

以下のように表示されました
2020-04-07_15h08_06.png

Font Awesome SVG アイコンのインストール

Font Awesome SVG アイコンのインストールを参考にしていきます。

まずは、インストール済みのFont Awesome 5を削除します。

yarn remove @fortawesome/fontawesome-free -D

plugins/vuetify.jsも以下のよう修正します。

plugins/vuetify.js
// src/plugins/vuetify.js

import 'material-design-icons-iconfont/dist/material-design-icons.css'
import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

export default new Vuetify({
  icons: {
    iconfont: ['md']
  }
})

FontAwesome5を使った時計アイコンが非表示になりました。
2020-04-07_15h16_23.png

FontAwesome5 SVGをインストールします。

yarn add @fortawesome/fontawesome-svg-core @fortawesome/vue-fontawesome @fortawesome/free-solid-svg-icons -D

plugins.vuetify.jsを以下のように書き換えます。

plugins/vuetify.js
// src/plugins/vuetify.js

import 'material-design-icons-iconfont/dist/material-design-icons.css'
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { fas } from '@fortawesome/free-solid-svg-icons'

Vue.component('font-awesome-icon', FontAwesomeIcon) // Register component globally
library.add(fas) // Include needed icons

Vue.use(Vuetify)

export default new Vuetify({
  icons: {
    iconfont: ['md', 'faSvg']
  }
})

最後に、SVG版のフォントはv-iconではなく、font-awesome-iconタグで使うようです。
以下のようにfont-awesome-iconを追加します。

index.vue
<template>
  <v-layout column justify-center align-center>
    <v-flex xs12 sm8 md6>
      <v-card>
        <v-row justify="space-around">
          <v-col>
            <v-icon>mdi-domain</v-icon>
            <v-icon>home</v-icon>
            <v-icon>fas fa-lock</v-icon>
            <font-awesome-icon :icon="['fas', 'lock']"></font-awesome-icon>
          </v-col>
        </v-row>
      </v-card>
    </v-flex>
  </v-layout>
</template>

<script>
export default {}
</script>

以下のように表示されます。

2020-04-07_15h49_45.png

以下のURLで、v-iconで表示する方法も書かれています。
私自身が、これ以上必要としないので、以下のURLを出しておしまいといたします。

https://stackoverflow.com/questions/55207257/fontawesome-svg-icons-with-vuetify-how-to-use-within-v-icon-prepend-icon

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

カルーセル実装で気をつけたこと

前提条件

  • React環境で動くこと
  • カルーセルを入れることによりパフォーマンスが低下しないこと

実装

props

  • 表示領域に表示するコンテンツ数
  • keyExtractor
  • カルーセルに表示するデータ(array)
  • カルーセルに表示するコンポーネント(render props)

ライブラリの導入検討

  • react-slick(https://react-slick.neostack.com/ )について導入検討したが、以下理由で断念。
    • パフォーマンスは問題なし
    • リンク数が奇数だった場合にうまく挙動しない
    • 表示数が端数の場合うまく挙動しない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WIP: カルーセル実装で気をつけたこと

前提条件

  • React環境で動くこと
  • カルーセルを入れることによりパフォーマンスが低下しないこと

設計

  • 表示数が端数の場合、端数分だけ遷移するように調整する
    ex.表示領域に表示する数が4なのに対し総数が5の場合、1つ分だけスクロールさせる
  • 画面幅をリサイズした際にカルーセルの表示サイズ、移動距離を可変させる
  • SPページではスクロール表示に切り替え

実装

カルーセルコンポーネント全体

import React, { useState, useEffect, useRef } from 'react'
import styled from '@emotion/styled'
import { PressableView, Row, View } from '../../Foundations'
import { layout, motion, color } from '../../../style'
import { Ticker } from '../../../lib/client/Ticker'

const MD_PADDING = layout.grid * 4 //画像間のpadding

interface Props<T> {
  displayContentsNum: number
  keyExtractor: (arg: T, index: number) => string
  list: T[]
  render: (arg: T) => React.ReactNode
}

// カルーセルの移動距離を計算
const createTranslateXByRawWidth = (
  width: number,
  displayContentsNum: number,
  mdPadding: number,
  nowRowNum: number,
  isRemainder: boolean,
  remainder: number
) => {
  const bodyWidth = -width - mdPadding
  const remainderWidth = (bodyWidth / displayContentsNum) * remainder
  const val = isRemainder
    ? bodyWidth * (nowRowNum - 1) + remainderWidth // eslint-disable-line no-mixed-operators
    : bodyWidth * nowRowNum // eslint-disable-line no-mixed-operators

  return `translateX(${val}px)`
}

const CarouselComponent = <T extends any>(props: Props<T>) => {
  const { displayContentsNum, keyExtractor, list, render } = props

  const mdPadding = MD_PADDING
  const totalContentsNum = list.length
  const remainder = totalContentsNum % displayContentsNum
  const totalRowNum = Math.ceil(totalContentsNum / displayContentsNum) - 1
  const isLessThanTotal = totalRowNum > 0

  const elem = useRef<HTMLDivElement>(null)
  const scrollElem = useRef<HTMLDivElement>(null)
  const [bodyWidth, setBodyWidth] = useState(0)
  const [isRemainder, setIsRemainder] = useState(false)
  const [rowIndex, setRowIndex] = useState(0)
  const [leftBtnIsActive, setLeftBtnIsActive] = useState(false)
  const [rightBtnIsActive, setRightBtnIsActive] = useState(isLessThanTotal)
  const [animationIsActive, setAnimationIsActive] = useState(true)
  const rowTransform = createTranslateXByRawWidth(
    bodyWidth,
    displayContentsNum,
    mdPadding,
    rowIndex,
    isRemainder,
    remainder
  )
  const resetScroll = () => {
    if (scrollElem.current) {
      scrollElem.current.scrollLeft = 0
    }
  }

  useEffect(() => {
    const getWidthTicker = new Ticker({
      onTicking: () => {
        if (elem.current) {
          setBodyWidth(elem.current.clientWidth)
        }
      },
      onStart: () => {
        setAnimationIsActive(false)
      },
      onEnd: () => {
        setAnimationIsActive(true)
      },
      throttle: 30,
    })

    const handler = (e: Event) => {
      // Tickingぜす、また2重でわざわざifで判定しているのは、リサイズイベントの最初で確実に走らせて、
      // 二回め以降window.innerWidthプロパティを参照したくないので細かく制御しています。
      if (scrollElem.current) {
        const isScrollLeftNotZero = scrollElem.current.scrollLeft !== 0
        if (isScrollLeftNotZero) {
          const isOverMd = window.innerWidth > layout.breakpoint.md
          if (isOverMd) {
            resetScroll()
          }
        }
      }
      getWidthTicker.dispatch(e)
    }

    window.addEventListener('resize', handler)
    return () => window.removeEventListener('resize', handler)
  }, [])

  useEffect(() => {
    setAnimationIsActive(false)
    setIsRemainder(false)
    setRowIndex(0)
    resetScroll()
    setLeftBtnIsActive(false)
    setRightBtnIsActive(isLessThanTotal)
    requestAnimationFrame(() => setAnimationIsActive(true))
  }, [list])

  const setBtnIsActive = (rowNum: number) => {
    const newLeftBtnIsActive = rowNum !== 0
    const newRightBtnIsActive = rowNum !== totalRowNum
    setLeftBtnIsActive(newLeftBtnIsActive)
    setRightBtnIsActive(newRightBtnIsActive)
  }

  const onButtonLeftClick = () => {
    if (leftBtnIsActive) {
      const nextRowNum = rowIndex - 1
      const isNotRemaindable = nextRowNum === 0 && remainder
      setRowIndex(nextRowNum)
      setBtnIsActive(nextRowNum)
      if (elem.current) {
        setBodyWidth(elem.current.clientWidth)
      }
      if (isNotRemaindable) {
        setIsRemainder(false)
      }
    }
  }

  const onButtonRightClick = () => {
    if (rightBtnIsActive) {
      const nextRowNum = rowIndex + 1
      const isRemaindable = totalRowNum === nextRowNum && remainder
      setRowIndex(nextRowNum)
      setBtnIsActive(nextRowNum)
      if (elem.current) {
        setBodyWidth(elem.current.clientWidth)
      }
      if (isRemaindable) {
        setIsRemainder(true)
      }
    }
  }

  return (
    <View ref={elem} onSwipeRightAuto={onButtonLeftClick} onSwipeLeftAuto={onButtonRightClick}>
      <CarouselButton
        moveDirection="left"
        onClick={onButtonLeftClick}
        isActive={leftBtnIsActive}
        animationIsActive={animationIsActive}
      />
      <CarouselButton
        moveDirection="right"
        onClick={onButtonRightClick}
        isActive={rightBtnIsActive}
        animationIsActive={animationIsActive}
      />
      <ContentsWrap ref={scrollElem}>
        <ContentsRow style={{ transform: rowTransform }} animationIsActive={animationIsActive}>
          {list.map((data, index) => (
            <BoxWrap key={keyExtractor(data, index)} displayContentsNum={displayContentsNum} mdPadding={mdPadding}>
              {render(data)}
            </BoxWrap>
          ))}
        </ContentsRow>
      </ContentsWrap>
    </View>
  )
}

export const Carousel = React.memo(CarouselComponent) as typeof CarouselComponent

const Button = styled(PressableView)({
  display: 'none',

    display: 'flex',
    position: 'absolute',
    top: '50%',
    transform: 'translateY(-50%)',
    width: 48,
    height: 48,
    border: `solid 1px ${color.ui.bgDark}`,
    borderRadius: '50%',
    background: color.pallet.surface,
    zIndex: 10,
    '&::after': {
      position: 'absolute',
      top: 0,
      bottom: 0,
      margin: 'auto',
      content: '""',
      width: 12,
      height: 12,
      borderRight: `2px solid ${color.ui.anotherArea.onWhite}`,
      borderTop: `2px solid ${color.ui.anotherArea.onWhite}`,
    },
    ':hover': {
      borderColor: color.pallet.accent,
      '&::after': {
        borderColor: color.pallet.accent,
      },
    },
  },
})

type CarouselButtonProps = {
  moveDirection: 'left' | 'right'
  isActive: boolean
  animationIsActive: boolean
}

const CarouselButton = styled(Button)<CarouselButtonProps>(({ moveDirection, isActive, animationIsActive }) => {
  const dirStyle = (dir => {
    switch (dir) {
      case 'left': {
        return {
          left: -32,
          '&::after': {
            left: 19,
            transform: 'rotate(225deg)',
          },
        }
      }

      case 'right': {
        return {
          right: -32,
          '&::after': {
            right: 19,
            transform: 'rotate(45deg)',
          },
        }
      }

      default:
        return {}
    }
  })(moveDirection)

  return {
    ...dirStyle,
    [layout.mqOfBreakpoint.md]: {
      transition: animationIsActive ? `all ${motion.duration.expand}ms ${motion.curves.base}` : 'unset',
      display: isActive ? 'flex' : 'none',
      '&::after': {
        transition: animationIsActive ? `all ${motion.duration.expand}ms ${motion.curves.base}` : 'unset',
      },
    },
  }
})

const ContentsWrap = styled(View)({
  overflowX: 'scroll',

    overflow: 'hidden',
  },
})

type ContentsRowProps = {
  animationIsActive: boolean
}

const ContentsRow = styled(Row)<ContentsRowProps>(({ animationIsActive }) => ({
  flexWrap: 'nowrap',

    transform: 'none !important',
  },

    transition: animationIsActive ? `transform ${motion.duration.base}ms ${motion.curves.base}` : 'unset',
    willChange: 'transform',
  },
}))

type BoxWrapProps = {
  displayContentsNum: number
  mdPadding: number
}

const BoxWrap = styled(Row)<BoxWrapProps>(({ displayContentsNum, mdPadding }) => ({
  marginRight: layout.grid,
  flexGrow: 0,
  ':first-of-type': {
    marginLeft: layout.grid * 2,
  },
  ':last-of-type': {
    marginRight: 0,
    paddingRight: layout.grid * 2,
  },

    width: `calc((100% - ${mdPadding * (displayContentsNum - 1)}px) / ${displayContentsNum})`,
    maxWidth: `calc((100% - ${mdPadding * (displayContentsNum - 1)}px) / ${displayContentsNum})`,
    marginRight: mdPadding,
    ':first-of-type': {
      marginLeft: 0,
    },
    ':last-of-type': {
      paddingRight: 0,
    },
  },
}))

props

  • 表示領域に表示する数
  • keyExtractor
  • カルーセルに表示するデータ(array)
  • カルーセルに表示するコンポーネント(render props)

実装中の気づきなど

画像間のpaddingは固定で持つ

  • 同サイトに使うカルーセルであればデザインが統一されるため固定で持ったほうがいい
  • デザイナーと調整してどうしても変更が必要になった場合に可変できるよう調整する

画像幅については画像自体に持たせずにCarouselComponent内でcalcで算出する

  • 画像幅は下記計算にてcalcで算出。
// mdPadding ・・・画像間のpadding
// displayContentsNum - 1 ・・・表示領域上の画像間のpaddingの個数
// displayContentsNum ・・・表示領域上の画像の個数

width: `calc((100% - ${mdPadding * (displayContentsNum - 1)}px) / ${displayContentsNum})`,
maxWidth: `calc((100% - ${mdPadding * (displayContentsNum - 1)}px) / ${displayContentsNum})`,

カルーセルの表示位置は position: left ではなく transform: translateX() をつかって動かす

  • カルーセルの表示位置を position: left で持つとパフォーマンス性が低く、カルーセル自体の動きもなめらかにならないため、transform: translateX() で使う
  • willChange: 'transform' を入れることにより、よりなめらかな動きを再現できる

参考:https://www.webprofessional.jp/achieve-60-fps-mobile-animations-with-css3/

  position:left でカルーセル移動させた場合
  

  transform: translateX() でカルーセル移動させた場合
  

カルーセルの移動距離の計算

  • 移動距離については以下関数で計算。
const createTranslateXByRawWidth = (
  width: number,
  displayContentsNum: number,
  mdPadding: number,
  nowRowNum: number,
  isRemainder: boolean,
  remainder: number
) => {
  const bodyWidth = -width - mdPadding
  const remainderWidth = (bodyWidth / displayContentsNum) * remainder
  const val = isRemainder
    ? bodyWidth * (nowRowNum - 1) + remainderWidth // eslint-disable-line no-mixed-operators
    : bodyWidth * nowRowNum // eslint-disable-line no-mixed-operators

  return `translateX(${val}px)`
}
  • 画面幅がリサイズされた際に関数が発火するようにする

レンダリング時のみアニメーションを無効化する

// アニメーション実行有無
const [animationIsActive, setAnimationIsActive] = useState(true)

useEffect(() => {
  //アニメーションの実行をfalseにする
  setAnimationIsActive(false)

  // 表示を初期化
  setCarouselPosition(0)
  setNowContentsNum(displayContentsNum)
  setLeftBtnIsActive(false)
  setRightBtnIsActive(totalContentsNum > displayContentsNum)

  // アニメーション実行をtrueにする
  requestAnimationFrame(setAnimationIsActive(true))
}, [totalContentsNum])

ライブラリの導入検討

  • react-slick(https://react-slick.neostack.com/ )について導入検討したが、以下理由で断念。
    • パフォーマンスは問題なし
    • リンク数が奇数だった場合にうまく挙動しない
    • 表示数が端数の場合うまく挙動しない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Componentの外部をクリックしたら発火するCustom Hooks【React】

概要

Componentの外部をクリックしたときに発火するイベントを管理したい。

コード

Componentの外部をクリックしたら発火するCustom Hooks作りました。

export const useOutsideClickEvent = (
  ref: MutableRefObject<any>,
  onClick: () => void
) => {
  const clickListener = useCallback(
    (e: MouseEvent) => {
      if ((ref?.current as any).contains(e.target)) {
        return;
      }
      onClick();
    },
    [ref.current, onClick]
  );

  useEffect(() => {
    document.addEventListener('click', clickListener);
    return () => {
      document.removeEventListener('click', clickListener);
    };
  }, []);
};

こんな感じで使うと、refで指定されたdivの外部をクリックされたときにsetIsOpen(false)が実行されます。

const Dialog: FC = () => {
    const ref = useRef(null);
    const [isOpen, setIsOpen] = useState<boolean>(false);
    useOutsideClickEvent(ref, () => setIsOpen(false));

    return (
        <div ref={ref}>
            ...
        </div>
    )
}

注意

ClickEventの実行時にrefで指定したDOMの中に要素が入っていないといけないので、動的にコンテンツを書き換える場合は注意が必要です。

if ((ref.current as any).contains(e.target)) {
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js クラスとスタイルのバインディング

クラスのデータバインディングの基本

  • クラスを動的に変更するにはv-bind:classを利用する
  • クラスの付け替えが楽になる
<p v-bind:class="{クラス名: 真偽値}">
js
var app = new Vue({
  el: '#app',
  data: {
    isLarge: true // 真偽値を定義
    }
})
html
<!-- isLargeがtrueなので、largeクラスが適用される-->
<p v-bind:class="{large: isLarge}">Hello Vue.js!</p>

複数のクラスを動的に切り替える

  • カンマ区切りで、複数のクラスを指定できる。
html
<p v-bind:class="{クラス名: 真偽値, クラス名: 真偽値, ...}">
html
<p v-bind:class="{large: isLarge, 'text-danger': hasError}">Hello Vue.js!</p>

'text-danger'のように、クラス名にハイフンを含む場合は''(シングルクォート)で囲む。

プレーンなクラス属性と共存させる

一般的なクラス定義と、データバインディングしたクラスは共存できる。

<p class="クラス名" v-bind:class="{クラス名: 真偽値, クラス名: 真偽値, ...}">

配列構文によるクラスのデータバインディング

v-bind:classにクラスのリストを渡す事で展開できる

<p v-bind:class="[プロパティ名, プロパティ名, ...]">
js
data: { // dataにクラス名を持ったプロパティを定義
  largeClass: 'large', 
  dangerClass: 'text-danger'
}
html
<!-- クラスを適用するにはブランケットで囲む -->
<p v-bind:class="[largeClass, dangerClass]">Hello Vue.js!</p>

<!-- <p class="large text-danger">Hello Vue.js!</p>

オブジェクトのデータを利用する

複数のクラスをテンプレート(HTML)に書くと見通しが悪くなるので、オブジェクトで定義してv-bind:classに渡すことができる。

<p v-bind:class="オブジェクト">
js
data: {
  classObject: { //クラスのオブジェクトを定義
    large: true,
    'text-danger': true
    }
}
html
<!-- largeクラスとtext-dangerクラスが適用される -->
<p v-bind:class="classObject">Hello Vue.js!</p>

クラスの条件に三項演算子を使う

html
<!-- isTrueがtrueの時、trueClassクラスが適用される -->
<p v-bind:class="isTrue ? trueClass : '' ">Hello Vue.js!</p>

三項演算子の結果に関わらず、常に適用したいプロパティがある場合は[]で囲んでカンマで区切る。

html
<!-- fooClassは常に適用される -->
<span v-bind:class="[isTrue ? trueClass : '', fooClass ]">

インラインスタイルのデータバインディング

  • インラインスタイルを動的に変更するにはv-bind:styleを利用する

dataオプションに、インラインスタイルで使用したいプロパティをセット

js
data: {
    color: 'blue',
    fontSize: 48
}

v-bind:styleでデータバインディングさせたインラインスタイルを適用できる。

CSSのプロパティ名はキャメルケースにする必要がある。

html
<p v-bind:style="{color: color, fontSize: fontSize + 'px'}">Hello Vue.js!</p>

<!-- <p style="color: blue; font-size: 48px;">Hello Vue.js!</p> -->

ケバブケースで書きたいときは''(シングルクォート)で囲う

html
<p v-bind:style="{'font-size': fontSize + 'px'}"></p>

また、クラスのようにオブジェクトを渡すこともできる。

js
data: {
  styleObject: { // オブジェクトを定義
    color: 'blue',
    fontSize: '48px'
    }
}
html
<p v-bind:style="styleObject"></p>

<!-- <p style="color: blue; font-size: 48px;">Hello Vue.js!</p> -->
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Ajaxを用いた非同期投稿の実装

目標

ezgif.com-video-to-gif (1).gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

ログイン機能を実装済み。

ログイン機能 ➡︎ https://qiita.com/matsubishi5/items/5bd8fdd45af955cf137d

投稿機能の実装

テーブル

schema.rb
ActiveRecord::Schema.define(version: 2020_04_05_115005) do

    create_table "books", force: :cascade do |t|
        t.string "title"
        t.text "body"
        t.integer "user_id"
        t.datetime "created_at", null: false
        t.datetime "updated_at", null: false
    end

    create_table "users", force: :cascade do |t|
        t.string "email", default: "", null: false
        t.string "encrypted_password", default: "", null: false
        t.string "reset_password_token"
        t.datetime "reset_password_sent_at"
        t.datetime "remember_created_at"
        t.integer "sign_in_count", default: 0, null: false
        t.datetime "current_sign_in_at"
        t.datetime "last_sign_in_at"
        t.string "current_sign_in_ip"
        t.string "last_sign_in_ip"
        t.string "name"
        t.datetime "created_at", null: false
        t.datetime "updated_at", null: false

        t.index ["email"], name: "index_users_on_email", unique: true
        t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
    end
end

モデル

user.rb
class User < ApplicationRecord
    has_many :books, dependent: :destroy
end
book.rb
class Book < ApplicationRecord
    belongs_to :user
end

ルーティング

routes.rb
Rails.application.routes.draw do
    devise_for :users
    resources :users
    resources :books
end

コントローラー

books.controll.rb
class BooksController < ApplicationController
    def create
        book = Book.new(book_params)
        book.user_id = current_user.id
        if book.save
            redirect_to book, notice: "successfully created book!"
        else
            @books = Book.all.order(created_at: :desc) #降順
            render 'index'
        end
    end

  def index
    @book = Book.new
    @books = Book.all.order(created_at: :desc) #降順
  end

    private
        def book_params
            params.require(:book).permit(:title, :body)
        end
end

ビュー

books/index.html.erb
<% if @book.errors.any? %>
    <h2><%= @book.errors.count %>error</h2>
    <div>
        <ul>
            <% @book.errors.full_messages.each do |msg| %>
                <li><%= msg %></li>
            <% end %>
        </ul>
    </div>
<% end %>

<%= form_with model: @book do |f| %>
    <div class="field row">
            <%= f.label :title %>
            <%= f.text_field :title, class: "col-xs-12 book_title" %>
    </div>

    <div class="field row">
        <%= f.label :body %>
        <%= f.text_area :body, class: "col-xs-12 book_body" %>
    </div>

    <div class="actions row">
            <%= f.submit class: "btn btn-primary col-xs-12" %>
    </div>
<% end %>
<h2>Books index</h2>
<table class="table table-hover table-inverse">
    <thead>
        <tr>
            <th></th>
            <th>Title</th>
            <th>Opinion</th>
        </tr>
    </thead>
    <tbody>
        <% @books.each do |book| %>
            <tr>
                <td>
                    <%= link_to book.user do %>
                         <%= attachment_image_tag book.user, :profile_image, :fill, 50, 50, fallback: "no-image-mini.jpg" %>
                    <% end %>
               </td>
                <td><%= link_to book.title, book %></td>
                <td><%= book.body %></td>
            </tr>
        <% end %>
    </tbody>
</table>

非同期機能の実装

1. jQueryの導入

Gemfile
    gem 'jquery-rails'
ターミナル
    $ bundle
application.js
    //= require rails-ujs
    //= require activestorage
    //= require turbolinks
    //= require jquery
    //= require_tree .

2. booksコントローラーのcreateアクションを編集

books.controller.rb
class BooksController < ApplicationController
#ローカル変数をインスタンス変数に変更
    def create
        @book = Book.new(book_params)
        @book.user_id = current_user.id
        unless @book.save
            render 'index'
        end
    end
end

3. フォームを編集する

非同期投稿を行うときは必ず「form_with」を使用する。

※「form_for」「form_tag」だとレイアウトが崩れた。

books/index.html.erb
<!-- form_withの引数に「remote: true」を追記 -->
<%= form_with model: @book, remote: true do |f| %> 
    <div class="field row"> 
            <%= f.label :title %>
            <%= f.text_field :title, class: "col-xs-12 book_title" %>
    </div>

    <div class="field row">
        <%= f.label :body %>
        <%= f.text_area :body, class: "col-xs-12 book_body" %>
    </div>

    <div class="actions row">
            <%= f.submit class: "btn btn-primary col-xs-12" %>
    </div>
<% end %>

4. 投稿一覧を編集する

books/index.html.erb
<tbody class="new_book"> <!-- 投稿一覧の親要素にクラスをつける -->
    <% @books.each do |book| %>
        <%= render 'books', book: book %> <!-- 投稿一覧をパーシャル化 -->
    <% end %>
</tbody>

books/_books.html.erb
<tr>
    <td>
        <%= link_to book.user  do %>
            <%= attachment_image_tag book.user, :profile_image, :fill, 50, 50, fallback: "no-image-mini.jpg" %>
        <% end %>
    </td>
    <td><%= link_to book.title, book %></td>
    <td><%= book.body %></td>
</tr>

5. JavaScriptファイルの作成

books/create.js.erb
$(".book_title").val('');
$(".book_body").val('');
$(".new_book").prepend("<%= j(render 'books', book: @book) %>");

$(".new_book")
➡︎ 「4」で付けたクラスを指定

.prepend("<%= j(render 'books', book: @book) %>");
➡︎ 既存投稿の先頭に新規投稿を表示

$(".book_title").val('');
$(".book_body").val('');
➡︎ 入力フォームを空にする

参考サイト

https://qiita.com/hiro266/items/56ec2c350fd9d8ca22d8

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

Jest encountered an unexpected token解決法

react-native-cameraを使ったアプリのテストをJestでする際、こんなエラーに遭遇した

  TypeError: Cannot read property 'Aspect' of undefined

  at Object.<anonymous> (node_modules/react-native-camera/src/Camera.js:425:113)
  at Object.<anonymous> (node_modules/react-native-camera/src/index.js:3:38)

他にもreact-native-unimodulesで

Jest encountered an unexpected token

なんかが出たりする

結論から先に書くと、これらのエラーは

jest.mock('{モジュール名}', () => '{クラス名}');

でモック化すると解決した。

react-native-cameraなら

jest.mock('react-native-camera', () => 'Camera');

となる。

Jest公式によると、react-nativeに組み込まれたJestプリセットにはデフォルトのモックが付属しているものの、いくつかのコンポーネントにはそれが無く、ネイティブコードに依存しているためマニュアルでモック化する必要があるとかなんとか

正直Jestのモックとはそもそも何ぞや?というところからわかっておらず現状おまじないと化してるので、後で調べておきたい

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

Nuxt.jsでhead要素・404ページの設定を行う

前提

$ npx create-nuxt-app プロジェクト名 でモジュールをインストールして開始すると、nuxt.config.jsファイルが生成されます。
このファイルで行う設定内容まとめです。

nuxt.config.js ファイル

  • export default { ... 以下をJSON形式で記述します。
  • アプリケーション共通設定を行う事ができます。

head要素の記述

export default {
  ...
  head: {
    title: "タイトル",
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: process.env.npm_package_description || '' },
      /* OGP設定も可能です */
      { property: 'og:title', content: 'OGPタイトル'},
      { property: 'og:type', content: 'website'},
      { property: 'og:url', content: 'OGP URL'},
      { property: 'og:image', content: 'OGP image'},
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      /* CDN等 */
      { rel: 'stylesheet', type: '', href: 'https://fonts.googleapis.com/css?family=Sen&display=swap' }
    ]
  },
  ...
}

ローディングカラーの変更

  • Nuxt.jsでは、初期状態でローディングアニメーションが設定されています。
  • 好みのカラーへ変更が可能です。
export default {
    ...
    loading: { color: '#カラーコード' },
    ...
}

404ページの設置

  • Nuxt.jsでは、/pages配下で自動的にルーティングが行えます。
  • 存在しないURLへのアクセスが行われた場合の404リクエストで表示するページ設定を行う事ができます。
export default {
    ...
    router: {
    extendRoutes (routes, resolve) {
      routes.push({
        name: 'custom',
        path: '*',
        component: resolve(__dirname, './components/404.vue')
      })
    }
  },
  ...
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npmパッケージのvulnerability対応フロー

概要

  • npmプロジェクトで利用しているnpmパッケージ(依存パッケージ)でvulnerability(脆弱性)が見つかったときの対処フローについて記載します。
    image.png

(GitHub等が親切に"We found potential security vulnerabilities in your dependencies."のように通知してくれるので便利)

対応フロー

ざっくり全体像は以下のとおり。

image.png

①最新のコードを取得する

いうまでもないが、手元のコードは最新のコードにしておく

git pull

最新であることが確認された

Already up to date.

ちなみに、今回は自作プロジェクト(https://github.com/riversun/simple-date-format) で実際にハンズオンした

②プロジェクトが使用しているnpmパッケージが最新かどうか確認する

脆弱性対応の前に、いま自分のプロジェクトが使っているnpmパッケージを最新のものにする。

プロジェクトが使っているnpmパッケージが最新かどうかはnpm outdatedコマンドで確認することができ、最新バージョンがある場合は、その情報を表示してくれる

npm outdated

するとこのように、最新版が存在するパッケージを一覧表示してくれる
いくつかのパッケージで最新版があるようだ

image.png

パッケージをアップデートする

package.jsonに記載されるnpmパッケージ一覧は以下のようになっている。
(「^」がついたキャレット表記についてはこちらで説明)

package.json抜粋
  "@babel/core": "^7.8.4",
  "@babel/preset-env": "^7.8.4",
  "babel-jest": "^25.1.0",
   ...

パッケージのバージョンの付け方セマンティックバージョニングに従っている。

セマンティックバージョニングにおいて、メジャーバージョン、マイナーバージョンは以下の意味をもっている。
image.png

これをふまえ、「パッケージを最新にする」には2種類の方法があるといえる。(パッチバージョンを上げるというのもいれれば3種類だが本筋にあまり影響ないので割愛)

  • ②-1 マイナーバージョンまで最新にする
  • ②-2 メジャーバージョンまで最新にする

②-1をとるか ②-2をとるかはポリシー次第だがメジャーバージョンを最新にする場合はAPIの後方互換が無いことを想定しておいたほうがいい。

②-1 マイナーバージョンまで最新にする場合

キャレット表記「^」つきで定義されたパッケージのバージョンを、マイナーバージョンまでを最新にするにはnpm updateコマンドで可能

npm update

以下のように、マイナーバージョンまでが最新になった
(ただし、メジャーバージョンは最新になっていない。)

+ jest@25.2.7
+ babel-loader@8.1.0
+ cross-env@7.0.2
+ @babel/core@7.9.0
+ babel-jest@25.2.6
+ @babel/preset-env@7.9.0
+ webpack@4.42.1
added 36 packages from 17 contributors, removed 6 packages, updated 140 packages, moved 1 package and audited 263490 packages in 18.399s

②-2 メジャーバージョンまで最新にする場合

npm updateでは、マイナーバージョンまでしかアップデートしてくれなかったが、ここではメジャーバージョンまで容赦なくアップデートしてくれるパッケージnpm-check-updatesを導入する

以下のようにしてnpm-check-updatesをインストールする

npm install -g npm-check-updates

npm-check-updatesをインストールするとncuというコマンドが使えるようになる。

ちなみに、ncuコマンドだけをたたくと現在のパッケージバージョンと最新のパッケージバージョンを表示してくれるnpm outdatedコマンドのような動作をする

ncu

を実行すると、以下のように現在のバージョンと最新バージョンが表示される。

image.png

つづいて、ncu -uを実行すれば、上でみたとおりパッケージが最新バージョンになるようにpackage.jsonを書き換えてくれる

ncu -u

image.png

npm updateと違ってパッケージのインストールそのものはやってくれない。package.jsonが書き換わるだけなので、自分で npm install する必要がある

npm install

これでパッケージは最新になった

③npm auditで脆弱性のある依存パッケージを確認する

今、最新のパッケージにした状態だが、この状態でも脆弱性(valnerability)のあるパッケージが含まれていることがある。
npm auditコマンドを使えば脆弱性のあるパッケージを洗い出すことができる。

npm audit

をやってみたら、180個の脆弱性がみつかった。レベルはlow

found 180 low severity vulnerabilities in 263397 scanned packages
  run `npm audit fix` to fix 174 of them.
  6 vulnerabilities require manual review. See the full report for details.

④npm audit fixで自動修復をこころみる

npm audit fixをすると、脆弱性のあるパッケージのバージョンを自動的に脆弱性の無いバージョンに置き換えてくれる(努力をしてくれる)

npm audit fix

すると以下のようになった。
180個の脆弱性のうち174個が修正された。

removed 1 package and updated 2 packages in 4.641s

31 packages are looking for funding
  run `npm fund` for details

fixed 174 of 180 vulnerabilities in 263397 scanned packages
  6 vulnerabilities required manual review and could not be updated

残り6個はマニュアルレビューしてくれとかいてある。

⑤npm dedupeで重複したパッケージの整理統合をこころみる

npm dedupeを理解する

まず、dedupeの概念を理解するために以下の状態を考える

  • 自プロジェクトnpmパッケージAnpmパッケージBに依存している
  • npmパッケージAnpmパッケージC @2.1.1に依存している。
  • npmパッケージBnpmパッケージC @2.2.0に依存している。

この場合、npmを使っていると1npmパッケージC @2.1.1npmパッケージC @2.2.0もインストールされる。

パッケージとしてはnpmパッケージCで同じなのに、npmパッケージC @2.1.1npmパッケージC @2.2.0でバージョンが違いがあるので両方保持されしまう。

だったら、バージョンを新しいほうの@2.2.0のほうに合わせて、npmパッケージC @2.2.0のほうを、npmパッケージAnpmパッケージBで共通化して使いましょう、というのがdedupeの発想。

npm dedupeは新しいバージョンをインストールしてくれない

npm update等でdedupeはひととおりのパッケージを最新にした後にやる。
なぜなら、dedupe自身はパッケージの重複排除などはやってくれるが、最新のパッケージを入れてくれるわけではないので古いパッケージバージョンで状態でdedupeしてもあまり意味がない。

dedupeする

さて、ではnpm dedupe (npm ddpでもOK)してみる

removed 10 packages and audited 263298 packages in 3.622s
found 0 vulnerabilities

脆弱性が0になった!

なぜdedupeで脆弱性がゼロになった?

これは、dedupeが脆弱性を排除しているというより、古いパッケージバージョンに依存していたライブラリが新しいパッケージバージョンを参照するようにdedupeが変更してくれた効果のため。

上の例でいうと以下のようになる。

  • 自プロジェクトnpmパッケージAnpmパッケージBに依存している
  • npmパッケージAnpmパッケージC @2.1.1(脆弱性ありバージョン)に依存している。
  • npmパッケージBnpmパッケージC @2.2.0(安全バージョン)に依存している。

の状況をdedupeが↓のように変更してくれた効果

  • 自プロジェクトnpmパッケージAnpmパッケージBに依存している
  • npmパッケージAnpmパッケージC @2.2.0(安全バージョン)に依存している。
  • npmパッケージBnpmパッケージC @2.2.0(安全バージョン)に依存している。

⑦回帰テストを実行する

さてここまでで、脆弱性の無い状態にできたら、最後にテストをしてパッケージバージョンを変更したことによるプロジェクトのデグレが発生していないかどうかを確認する。

npm test

image.png

テスト無事通過!

(といっても、今回はdevDependenciesの依存のみだったけど)

これで対応完了!

途中でつまずいた場合

上述のコマンドだけでうまくいかず、つまずくことも多々ある。そうした場合は、コマンドだけで楽に突破できない状況になっている可能性があるので、それなりの調査分析工数を覚悟するしかない。

つまずき例

  • vulnerabilityが無くならなかった

    • 対応案:npm auditでvulnerabilityのあるパッケージに依存している上位のパッケージを特定する。そのパッケージがdeprecateになっていないか。ちゃんとメンテされているか確認する。deprecatedだったりメンテされていなければ使うのをやめたりPR送ってみたり、自分で直してみたり、代替パッケージを探したり。そういった工数を覚悟、確保する。
  • 最後のテストでつまづいた

    • 対応案:手順を1手ずつロールバック(手順を戻す)しながら、都度npm testを実行し、どの段階でつまづいたのか特定する。ありがちなのは②-1メジャーアップデートでAPIに破壊的変更

まとめ


  1. yarnは自動的にdedupeしてくれる(https://classic.yarnpkg.com/ja/docs/cli/dedupe/

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

obnizとLineBotで防犯ツールを作ってみた(NO MORE XX 泥棒)

この記事を見てできること

LINEBOTに「スタート」とメッセージを送ると遠隔操作で監視をしてくれるものが作れます。
異常があったら必要以上にしつこく知らせてくれます。
(処理をストップさせる処理は書いてないです)

obnizの配線

配線はこんな感じ。
IMG_0140.JPG

コード内にも書いてありますが、人感センサーは{vcc:11, signal:10, gnd:9} につなぎます。

ソースコード

はじめobniz.onconnectをするタイミングでハマって動かなかった。
handleEventよりも先にobniz.onconnectをしておく必要がある。

line_sensor.js
'use strict';

// obniz呼び出し
const Obniz = require('obniz');
var obniz = new Obniz("XXXXXX");  // Obniz_ID に自分のIDを入れます

const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000;

const config = {
    channelAccessToken: 'XXXXXX',
    channelSecret: 'XXXXXX'
};

const app = express();

app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);
    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

// obniz接続//////////
// スコープ(変数の影響範囲)を最上部に置いておく
var sensor;
var led;

// 接続した段階でセンサーの準備はしておく
obniz.onconnect = async function(){
    obniz.display.clear();
    obniz.display.print("obniz LINE bots");
    sensor = obniz.wired("HC-SR505", {vcc:11, signal:10, gnd:9});
    led = obniz.wired("LED",{anode:0,cathode:1});
}

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let mes = ''
  if(event.message.text === 'スタート'){
    mes = 'OK!監視始めるね!'; //メッセージだけ先に処理
        // ディスプレイ処理
        obniz.display.clear();  // 一旦クリアする
        obniz.display.print("Hello obniz!!!!!!");  // Hello obniz!という文字を出す

        // setIntervalで間隔を作る
        setInterval(async function(){
          // 非同期で取得
          var detected = await sensor.getWait();
          // console.log(detected);
          // displayに反映
          obniz.display.clear();  // 一旦クリアする
          // obniz.display.print(detected);  // 英語が出力できる
          // 近づいてきたら判定する
         if(detected == true ){ // 何かを感知したら
            obniz.display.clear();  // 一旦クリアする
            obniz.display.print("someone moving!");
            led.on(); // ライトON
            getAskObniz(event.source.userId);  // message
          }else{
            led.off();
          } 
        },1000); // 1000ミリ秒 = 1秒

  }else{ mes = event.message.text;}

  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: mes
  });
}

const getAskObniz = async (userId) => {
  await client.pushMessage(userId, {
      type: 'text',
      text: "誰かいるかも?",
  });
}

app.listen(PORT);
console.log(`Server running at ${PORT}`);

XXXXXXになっている箇所は自分のObniz IDや、channelAccessToken、channelSecretを入れることをお忘れなく。

ほぼ完成

ここまで機能としては完成です。こんな感じで動きます。
IMG_0141.jpg
人を感知すると知らせてくれます。

このままでは終われない

こういった手で触れるものは見た目も大事。そして遊び心も必要です(たぶん)

この時点で平日の夜中2時を回っている。当然明日も朝から仕事である。

考えても何もアイデア浮かばない。。
ティッシュがあるからてるてる坊主?
いや、最近ティッシュやトイレットペーパーがなかなか売ってないから無駄使いはダメだ...

「よし、映画館のあいつを作ろう」
IMG_0134.JPG

こんな感じで動きます

最後に

物理的に触れるものは愛着湧きますね。もっと時間がある時にもっとそれっぽく作りたい。
あと、LINEでストップとメッセージしたらモニタリングがストップするような処理も書きたい。

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

[Rails]JS(jquery)が動かない 初歩的ミス

jquery使用の際、同じコード引用下のに別アプリで
なぜか動かない。
と言う原因の一例です。

確認した場所

■Gemfile→jqueryがインストールされているか
■application.jsにjqueryの記述があるか。
■js内にturbolinks:loadが入っているかどうか

原因

コメントアウトされているコードの並び順
*これ、すごく大事でした。
初歩の初歩でテキストに書いてあった気がする。。。

//= require_tree .が、最初はこの位置におりました。
image.png
移動!!最下部へ
image.png
こちらでjqueryが動かない問題は解決いたしました。

require_tree .とは?

参考資料によると以下の記載があります(ちなみにrequire_treeはcssにも記述あります)
そしてアセットパイプラインと言うそうです。
application内にあるcssやjsの読み込み順を司るものです。

require_treeディレクティブは、指定されたディレクトリ以下の
すべての JavaScriptファイルを再帰的にインクルードし、出力に含めます。
このパスは、マニフェストファイルからの相対パスとして指定する必要があります。
require_directoryディレクティブを使用すると、指定されたディレクトリの
直下にあるすべてのJavaScriptファイルのみをインクルードします。
この場合サブディレクトリを再帰的に探索しません。

日本語難しい(´・ω・)
少し簡単な説明

require の部分は ディレクティブ (何種類かあります)
require_directory
→与えられたディレクトリ以下のファイルを、
自身よりも前に挿入する。
順番はアルファベット順(さらに大文字→小文字)になる

require_tree
→require=directory と同じ動きをするが、再帰的に読み込む
読み込みはコードの 上から順番に 読み込まれます。

上記より考えると、require_treeより下に書かれていたjqueryが読み込まれなかったため動かなかった(と、解釈いたしました。)

[備考]
require_treeには引数として与えられたディレクトリ以下のファイルをアルファベット順に全て読み込むという意味があります。
現在require_treeの引数には.(ドット)が渡されています。
引数.(ドット)はカレントディレクトリを表します。
つまり、この記述によってapp/assets/javascriptsというディレクトリにあるファイルは全て読み込まれることになります。

参考ページ

Rails のアセットパイプライン(Asset Pipeline)について

RailsでCSSの読み込む順番を制御する方法

終わりに

こちらは、個人的解釈をもとに解決した方法を備忘録として書いております。
プログラミング初学者ゆえ、
誤記や不備、アドバイス等ございましたら御指摘いただけると幸いです。
最後まで読んでいただきありがとうございます。

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