- 投稿日:2020-02-05T23:42:44+09:00
【比較検証】Next.js の Server Side Rendering (SSR) を理解する。create-react-app と比べてみた。
Next.jsのサイト、かっこいいですよね ?
クールで、パフォーマンスにも優れていてエンジニアを魅了します。
日本では Nuxt.js が人気のようですが、個人的には Next.js を推しています。さて、先日 Next.js のチュートリアル を通してサーバサイドレンダリングについて考えさせられる機会がありました。本記事では、そもそもサーバサイドレンダリングのメリットとは?というところから初めて、
create-react-app
によって実装された SPA と、next
によって実装された SSR ではどのような違いがあるのかを検証してみました。以下の動画は本記事のサマリーです。
作成したアプリケーションへのリンクも貼っておきます。
右の方がちょっとだけ描画が遅いのがわかりますね。?(左)next.js で SSR、(右)create-react-app で SPA ?
サーバサイドレンダリング(SSR)とは
従来の React ベースのアプリケーションの構成を振り返ってみましょう(右図)。この構成の場合、ユーザからのリクエストは、まずはじめに React サーバ(S3 や Netlify)から JavaScript のソースと必要最小限のほとんど空っぽな HTML を返します。それからフロントエンドで HTML 要素をレンダリングする方法をとります。
このようにバックエンド API とフロントエンドの描画を完全に分離する事によって、開発体制を分離した生産性向上や、ユーザに優れた UX を提供できるようになりました。
その一方で、過度なネットワーク通信が発生したり、JavaScript によって生成された Web サイトを検索エンジンのクローラが検知できなくなりました。その結果として、Google の検索項目の上位に自サイトが表示されにくいなどのデメリットも招いてしました。※こちらの記事で紹介されていますが、最近ではあまり問題にならなくなっているようです。さて、このような問題を解消するためのテクニックがサーバサイドレンダリング(ServerSideRendering)です(左図)。サーバサイドレンダリングは従来フロントエンドで行なっていたレンダリングをバックエンドの Node.js サーバにも移譲しようという考え方です。これにより、モバイル端末がどんなに脆弱でも、ハイパフォーマンスなサーバを使用してレンダリングできます。さらに無駄なネットワーク通信回数も最小限に減らせるでしょう。「バックエンドの Node.js サーバにも」と強調しているのは、フロントエンドでももちろん描画ができる、ということです。初期ページの一部だけはサーバサイドでレンダリングして、残りの要素はフロントエンドからフェッチしてきてレンダリングするといったように用途に応じて使い分けができます。
パフォーマンス
遅いデバイスを使用していると、最初のページのレンダリングに時間がかかり、ユーザ体験が低下します。計算をより強力なサーバーにオフロードすることで、ユーザーが待機する時間を最小限に抑えることができます。
また、サーバーで初期データをプリフェッチしてページを構築すると、サイトを表示するために必要なラウンドトリップの回数が大幅に削減されます。これにより、待ち時間の短縮と帯域幅消費の削減につながります。SEO 対策
SSR を行なっているサイトは、ページが検索エンジンで簡単にインデックス化されます。クライアント側でルーティング制御を行なっていると、検索エンジンのウェブクロールを遅らせてしまいます。この結果、検索エンジンの上位にリンクを表示することが難しくなります。
Next.js ことはじめ
SSR を理解するために必要最小限の構成で Next.js アプリケーションを組み立てていきます。
必要なライブラリとアプリケーションの実行
$ mkdir next.ssr $ cd next.ssr $ yarn init -yNext.js を最小構成で始めるために必要なライブラリは
next
とreact
,react-dom
だけです。早速yarn
でインストールしましょう(npm
でもよいですよ)$ yarn add react react-dom next
package.json
には以下のnpm scripts
を記載しておいて開発を楽に進められるようにしておきましょう。package.json"scripts": { "dev": "next", "build": "next build", "start": "next start" }それぞれのコマンドは以下のように使用します。
- dev - ローカルでアプリケーションを起動します。
- build - プロダクション用にアプリケーションをビルドします。
- start - プロダクション環境でアプリケーションを実行します。
ルーティング
Next.js は非常にシンプルな設計思想でフロント画面が作れるように構成されています。
/pages
ディレクトリ配下に配置されている js ファイルごとにパスルーティングが行われます。はじめの一歩として/pages/index.js
にファイルを配置して、/
という URL で表示できるようにしてみましょう。詳細なドキュメントはこちら$ mkdir pages $ touch pages/index.jspages/index.jsconst Index = () => { return <h1>Hello World</h1>; }; export default Index;ファイルパスと URL パスには以下のような対応関係があります。
ファイルパス URL パス pages/index.js / pages/blog/index.js /blog pages/blog/first-post.js /blog/first-post pages/dashboard/settings/username.js /dashboard/settings/username pages/blog/[slug].js /blog/:slug (/blog/hello-world) pages/[username]/settings.js /:username/settings (/foo/settings) pages/post/[...all].js /post/* (/post/2020/id/title) さて、ここまでできれば準備完了です。アプリケーションを起動してみましょう。
$ yarn devブラウザを起動し、
/
にアクセスすると画面が表示されるはずです。サーバサイドレンダリングの実装
ここから SSR ができるような機能を作っていきましょう。
<Link>
コンポートを使用して、他ページに遷移します。以下の例だと/shows/[id]
へ遷移させようとしています。また、Next.js には、ページのデータを取得するための標準 API が付属しています。getInitialProps
という非同期関数を使用して実行します。
getInitialProps
を使用すると、特定のページのデータをフェッチしてページに渡すことができます。getInitialProps
はサーバーとクライアントの両方で動作します。
このgetInitialProps
の振る舞いを観測し、SSR を理解していきましょう。index.jsimport Link from "next/link"; import fetch from "isomorphic-unfetch"; const Index = props => ( <div> <h1>Batman TV Shows</h1> <ul> {props.shows.map(show => ( <li key={show.id}> <Link href="/shows/[id]" as={`/shows/${show.id}`}> <a>{show.name}</a> </Link> </li> ))} </ul> </div> ); Index.getInitialProps = async function() { const res = await fetch("https://api.tvmaze.com/search/shows?q=batman"); const data = await res.json(); console.log(`Show data fetched. Count: ${data.length}`); return { shows: data.map(entry => entry.show) }; }; export default Index;
pages
配下に/shows/[id].js
を配置し、Dynamic Routing ができるようにしておきます。pages/[id].jsimport fetch from "isomorphic-unfetch"; const Post = props => ( <div> <h1>{props.show.name}</h1> <p>{props.show.summary.replace(/<[/]?[pb]>/g, "")}</p> {props.show.image ? <img src={props.show.image.medium} /> : null} </div> ); Post.getInitialProps = async function(context) { const { id } = context.query; const res = await fetch(`https://api.tvmaze.com/shows/${id}`); const show = await res.json(); console.log(`Fetched show: ${show.name}`); return { show }; }; export default Post;
/
を表示してみましょう。サーバサイドにログShow data fetched: 10
が表示されるはずです。index.js
をサーバサイドでレンダリングしたという事になりますね。
次にリンクをクリックして/shows/975
に遷移するとブラウザのコンソールにログが表示されてます。これはフロントエンドでデータフェッチとレンダリングが行われたということを意味しています。デプロイ
最後にビルドして、デプロイします。ZEIT の now にデプロイします。素晴らしい DX(DeveloperExperimence)です。本当に必要な要素以外全て削ぎ落とした、最高の PaaS だと思ってます。いつも愛用しています。こちらの記事にて丁寧に解説されていました。
bash$ yarn build # ビルド $ now # デプロイデプロイしたら動作を確認してパフォーマンスを検証しましょう。Chrome の開発者コンソールを開き、Audit を実行します。
https://batman-tv-shows.geeawa.now.sh/
First Meaningful Paint が 1.0s とでました。まずまずです。
create-react-app との比較
ここまでできたので Next.js で作成されたアプリケーションと
create-react-app
で作成されたアプリケーションを比較してみましょう。以下のようにほぼ同様のソースを使用して、
create-react-app
アプリケーションを作成します。以下にデプロイしてあります。https://badman-tv-shows-create-react-app.now.sh/
index.jsimport React from "react"; import fetch from "isomorphic-unfetch"; class Index extends React.Component { constructor(props) { super(props); this.state = { shows: [] }; } async componentDidMount() { const res = await fetch("https://api.tvmaze.com/search/shows?q=batman"); const data = await res.json(); console.log(`Show data fetched. Count: ${data.length}`); this.setState({ shows: data.map(entry => entry.show) }); } render() { return ( <div> <h1>Batman TV Shows</h1> <ul> {this.state.shows.map(show => ( <li key={show.id}> <a href="">{show.name}</a> </li> ))} </ul> </div> ); } } export default Index;デプロイができたので Audit を実行します。
First Meaningful Paint は 1.4s となり、Next.js によって SSR できるようになったサイトと比較すると少しだけ遅い結果がでました。さいごに
今回作成されたアプリケーションは非常にシンプルで、1つの API しか実行しませんし、レンダリングする DOM 要素も少なかったためパフォーマンスにそれほど大きな違いはみられませんでした。それでもアプリケーションが肥大したり、ネットワークの遅い環境、古くて脆弱なモバイルデバイスを使用するとパフォーマンスの違いは顕著になってくるでしょう。SSR の技術は適材適所を見極めて投下していきたいですね。
- 投稿日:2020-02-05T23:20:29+09:00
IFRAMEにコンテンツをフィットさせるには
固定サイズのHTMLを、それよりも小さいサイズのIFRAMEに縮小して表示したかったので、その対処方法です。
課題
たとえば1440px X 1000pxの大きさのHTMLを幅800pxのIFRAMEに表示します。
解決方法
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <link rel="stylesheet" href="default.css"> </head> <body> <div id="wrapper"> <h1>Resize 1440px X 1000px iframe to width 800px</h1> <iframe id="iframe" src="iframe-content.html"></iframe> </div> <a href="iframe-content.html" target="_blank">iframe-content.html</a> </body> <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> <script src="default.js"></script> </html>default.cssbody { text-align: center; } #wrapper { margin: 0 auto 0 auto; overflow: hidden; width: 800px; } #iframe { height: 1000px; width: 1440px; }default.js(function (window, document, $) { let iframe = $('#iframe'); let wrapper = iframe.parent(); let width = wrapper.width(); let ratio = width / iframe.width(); console.log(`Ratio: ${ratio}`); // IFRAME自体は読み込みページの大きさにCSSで適用している。 // それを#wrapperのサイズにスケールインする。 // https://stackoverflow.com/questions/166160/how-can-i-scale-the-content-of-an-iframe iframe .css('-ms-transform', `scale(${ratio})`) .css('-moz-transform', `scale(${ratio})`) .css('-o-transform', `scale(${ratio})`) .css('-webkit-transform', `scale(${ratio})`) .css('transform', `scale(${ratio})`) .css('-ms-transform-origin', '0 0') .css('-moz-transform-origin', '0 0') .css('-o-transform-origin', '0 0') .css('-webkit-transform-origin', '0 0') .css('transform-origin', '0 0'); // #iframeのひとつ上のラッパー#wrapperの高さを同じ倍率で変更する。 // これをしないとうまくもともとのIFRAMEの高さのままになる。 wrapper.height(wrapper.height() * ratio); })(window, window.document, window.jQuery);ポイント
- 同じ倍率で高さも変える必要もあるので、JavaScriptで対応します。
- 参考サイトにある通り、CSSで縮小します。
- IFRAMEのサイズは読み込んでいるコンテンツのサイズと同じにしておく。
- IFRAMEをラッパーで囲みます。CSSで指定したサイズの領域が取られているのでラッパーのdivで高さを調整しています。
- 面倒だったのでjQueryを使用。
参考サイト
- 投稿日:2020-02-05T21:09:05+09:00
Google OAuth 2.0 認証を使ったGoogle Sign-Inの実装(サーバー編)
概要
今回はこちらの記事の続きです。
Google OAuth 2.0 認証を使ったGoogle Sign-Inの実装(JS編)
クライアントサイドでログインしてバックエンドサーバーで認証といった感じでやります。前回の記事でユーザー情報などをレスポンスで取得することができました。
ただこれをこのままサーバーサイドに送ったりするのは危険です。
例えば、ユーザーIDをサーバーに送信する際にユーザーを偽装できちゃったりします。。なので、その代わりに検証可能なIDトークンを使用して、サーバー側でサインインしているユーザー情報を安全に取得します。
さっそく実装していきましょう。
フロントサイドの実装
まずJSの部分を書きかえます。
JavaScriptfunction onSignIn(googleUser) { var id_token = googleUser.getAuthResponse().id_token; // IDトークンを取得 // IDトークンをサーバー側に送る処理 }ここのサーバー側に送る処理はAjax使ったり、hidden属性で送ったりいろいろなやり方があると思いますが、
今回はAjaxのXMLHttpRequestを使用してHTTPリクエストを発行する方法でサーバーとの通信をやって来ます。先ほどの処理に追加して
javaScriptvar req = new XMLHttpRequest(); req.open('POST', '[URL]’); req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); req.onload = function() { req.send('idtoken=' + id_token);こんな感じでサーバー側に送信しましょう。
[URL]の部分にはサーバー側のURLを入れましょう。サーバー側の実装
今回は、JavaのSpring bootを使って実装していきます。
まずは依存関係の追加です。
build.gradledependencies { implementation("com.google.api-client:google-api-client:1.30.5") }Googleクライアントライブラリを使えば簡単にIDトークンの検証ができます。
JavaGoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder( new NetHttpTransport(), JacksonFactory.getDefaultInstance()) .setAudience(Collections.singletonList("YOUR_CLIENT_ID.apps.googleusercontent.com")) .build(); var idtokenStriing = getIdToken(); // 取得したIDトークン GoogleIdToken idToken = verifier.verify(idTokenString); // IDトークン検証YOUR_CLIENT_IDには自分のクライアントIDを入れてください。
もしGoogleクライアントライブラリを使わない場合はGoogleの公開鍵(PEM形式)を使用して、トークンの署名を検証する方法もありますが、GoogleでもGoogleクライアントライブラリを使って検証することを推奨しているので使いましょう。
あとはユーザー情報を取得するだけです。
JavaPayload payload = idToken.getPayload(); String userId = payload.getSubject(); // ユーザーID String email = payload.getEmail(); // ユーザーメールアドレス String name = (String) payload.get("name"); // ユーザー名 String pictureUrl = (String) payload.get("picture"); // ユーザープロフィール画像こんな感じで取得できちゃいます。
まとめ
今はいろんなサイトでOAuth認証使ってるところが増えてますね。
今回はGoogleですがFacebookやAppleなどのOAuth認証もまたあげていこうと思います。
- 投稿日:2020-02-05T20:24:22+09:00
[Angular]カスタムパイプの作り方
概要
- Angularにはtemplate上でフォーマットなど簡易な変換ができるPipeという機能があります
- フレームワークが提供するPipeもありますが独自に作ることもできるようなのでその手順のメモ
パイプとは
- Angularではtemplateの中で
{{}}
を使うと変数を埋め込むことができます- 埋め込んだ変数の後ろに
{{ 変数 | xxx }}
といった形でPipeを適用することでフォーマット変換などができます<div> <p>{{ 1234567 | number }}</p> <p>{{ new Date(2020, 1, 5) | date: 'yyyy年MM月dd日' }}</p> </div>
- 上記の例はAngularが用意している
number
とdate
のPipeを使った例です- 以下のように画面に表示されます
1,234,567 2020年02月05日カスタムパイプを作る
- 今回は文字列の末尾をマスク化するPipeを作ってみます
雛形の生成
- AngularCLIでPipeをgenerateします
ng generate pipe --name=mask
- Pipeファイルが作成されました
% ng generate pipe --name=mask CREATE src/app/mask.pipe.spec.ts (179 bytes) CREATE src/app/mask.pipe.ts (201 bytes) UPDATE src/app/app.module.ts (949 bytes)
- 雛形の内容の確認します
src/app/mask.pipe.tsimport { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'mask' }) export class MaskPipe implements PipeTransform { transform(value: any, ...args: any[]): any { return null; } }
- アノテーションで定義されている
{ name: 'mask' }
はPipeを使うときの名前です
- この例では
{{ 文字列 | mask }}
といった具合で使うことになりますtransform
関数はpipeで渡された値を受け取り、returnした内容が画面に表示される値となります
- 第2引数は
{{ 文字列 | mask: 1 }}
といった感じでPipeの後に任意の値を渡した場合に受け取ることができますPipeの処理を実装
- Pipeの処理を作ります
- マスク化の処理を書いていきます
src/app/mask.pipe.tsimport { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'mask' }) export class MaskPipe implements PipeTransform { transform(value: string): string { return `${value.slice(0, -4)}****`; } }
- 渡された文字列の末尾4文字をマスク化して返す実装をしました
- サンプルなので特殊ケースは考えません
Pipeを適用する
- 作ったMaskPipeを使ってみます
src/app/app.component.tsimport { Component } from '@angular/core'; @Component({ selector: 'app-root', template: '<h1>{{ cardNumber | mask }}</h1>', }) export class AppComponent { cardNumber = '1111-1111-1111-1111'; }
- 画面上に
1111-1111-1111-****
と表示されているはずです!簡単!Pipeに引数を渡す
- もう少し機能を加えてみます
- マスク化する文字数を指定できるようにしてみます
src/app/mask.pipe.tsimport { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'mask' }) export class MaskPipe implements PipeTransform { transform(value: string, _size?: number): string { const size = _size || 4; return `${value.slice(0, -_size)}${'*'.repeat(size)}`; } }
- 第2引数で
size
を受け取るようにしてみました
- 例によって例外ケースは考慮しません
- 適用してみます
src/app/app.component.tsimport { Component } from '@angular/core'; @Component({ selector: 'app-root', template: '<h1>{{ cardNumber | mask: 2 }}</h1>', }) export class AppComponent { cardNumber = '1111-1111-1111-1111'; }
- 画面上に
1111-1111-1111-11**
と表示されているはずです!テストコードを書く
- せっかくなのでテストも書いてみます
- Pipeのテストは
transform
関数のテストを行う形になりますsrc/app/mask.pipe.spec.tsimport { MaskPipe } from './mask.pipe'; describe('MaskPipe', () => { const pipe = new MaskPipe(); describe('文字数を指定しなかった場合', () => { it('末尾4文字がマスク化されること', () => { expect(pipe.transform('12345678')).toBe('1234****'); }); }); describe('文字数を指定した場合', () => { it('末尾から指定した文字数分だけマスク化されること', () => { expect(pipe.transform('12345678', 2)).toBe('123456**'); }); }); });
- 純粋なJavaScriptのロジックのテストなので書きやすいですね
- 実行してみます
npm test -- --include src/app/mask.pipe.spec.ts
- すべてグリーンになりました!
まとめ
- Pipeの実装は純粋なJavaScriptの関数なので学習コスト低く作れる
- 同じ理由でテストコードの実装も簡単
- Pipeは用法用量を守って使ったほうが良さそうだけどうまく使いこなすととても便利そうです
- 投稿日:2020-02-05T20:03:44+09:00
初心者によるプログラミング学習ログ 230日目
100日チャレンジの230日目
twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。
230日目は
おはようございます
— ぱぺまぺ@webエンジニアを目指したい社畜 (@yudapinokio) February 4, 2020
230日目
・udemyで、css+javascript講座
・webサイト部分的模写#早起きチャレンジ#駆け出しエンジニアと繋がりたい#100DaysOfCode
- 投稿日:2020-02-05T19:58:01+09:00
【WebAudioAPI】録音した音声をバイナリデータ化、PHPへ受け渡し
概要
Node.js上で、IBMのWatsonによって人が話した音声データを自動で文字起こしするスクリプトを作成しました。
その中で、結構苦労した
PCのマイクに直接アクセス→録音した音声データをバイナリデータ化、PHPへ受け渡し
の部分をメモがてら貼り付け。環境
$php -v PHP 7.1.23 (cli) (built: Feb 22 2019 22:19:32) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies録音部分
hogehoge.js// 音声データのバッファをクリアする audioData = []; //様々なブラウザでマイクへのアクセス権を取得する navigator.mediaDevices = navigator.mediaDevices || navigator.webkitGetUserMedia; //audioのみtrue。Web Audioが問題なく使えるのであれば、第二引数で指定した関数を実行 navigator.getUserMedia({ audio: true, video: false }, successFunc, errorFunc); function successFunc(stream) { const audioContext = new AudioContext(); sampleRate = audioContext.sampleRate; // ストリームを合成するNodeを作成 const mediaStreamDestination = audioContext.createMediaStreamDestination(); // マイクのstreamをMediaStreamNodeに入力 const audioSource = audioContext.createMediaStreamSource(stream); audioSource.connect(mediaStreamDestination); // 接続先のstreamをMediaStreamに入力 for(let stream of remoteAudioStream){ try{ audioContext.createMediaStreamSource(stream).connect(mediaStreamDestination); } catch(e){ console.log(e); } } // マイクと接続先を合成したMediaStreamを取得 const composedMediaStream = mediaStreamDestination.stream; // マイクと接続先を合成したMediaStreamSourceNodeを取得 const composedAudioSource = audioContext.createMediaStreamSource(composedMediaStream); // 音声のサンプリングをするNodeを作成 const audioProcessor = audioContext.createScriptProcessor(1024, 1, 1); // マイクと接続先を合成した音声をサンプリング composedAudioSource.connect(audioProcessor); audioProcessor.addEventListener('audioprocess', event => { audioData.push(event.inputBuffer.getChannelData(0).slice()); }); audioProcessor.connect(audioContext.destination); }録音した音声をバイナリデータ化
hogehoge.js//音声をエクスポートした後のwavデータ格納用配列 const waveArrayBuffer = []; //仕様の関係で、大きなデータを分けたうちの1つのデータ容量が25MB以下になるよう制御 if (audioData.length > 250){ const num = audioData.length/250; const count = Math.round(num); for (let i=0; i < count; i++){ const sliceAudioData = audioData.slice(0,249); audioData.pop(0,249); const waveData = exportWave(sliceAudioData); waveArrayBuffer.push(waveData); } }else{ waveArrayBuffer.push(exportWave(audioData)); } //PHPへPOST var oReq = new XMLHttpRequest(); oReq.open("POST", '任意のパス', true); oReq.onload = function (oEvent) { // Uploaded. }; //複数のデータをblob化するための配列 const blob = []; //waveArrayBufferに入っている複数のデータを1つずつ配列に格納 waveArrayBuffer.forEach(function(waveBuffer){ blob.push(new Blob([waveBuffer], {type:'audio/wav'})); }) var fd = new FormData(); for (let i=0; i < blob.length; i++){ fd.append('blob'+i,blob[i]); } // oReq.setRequestHeader('Content-Type','multipart/form-data; name="blob" boundary=\r\n'); //配列ごとリクエスト送信 oReq.send(fd); function exportWave(audioData) { // Float32Arrayの配列になっているので平坦化 const audioWaveData = flattenFloat32Array(audioData); // WAVEファイルのバイナリ作成用のArrayBufferを用意 const buffer = new ArrayBuffer(44 + audioWaveData.length * 2); // ヘッダと波形データを書き込みWAVEフォーマットのバイナリを作成 const dataView = writeWavHeaderAndData(new DataView(buffer), audioWaveData, sampleRate); return buffer; } // Float32Arrayを平坦化する function flattenFloat32Array(matrix) { const arraySize = matrix.reduce((acc, arr) => acc + arr.length, 0); let resultArray = new Float32Array(arraySize); let count = 0; for(let i = 0; i < matrix.length; i++) { for(let j = 0; j < matrix[i].length; j++) { resultArray[count] = audioData[i][j]; count++; } } return resultArray; } // ArrayBufferにstringをoffsetの位置から書き込む function writeStringForArrayBuffer(view, offset, str) { for(let i = 0; i < str.length; i++) { view.setUint8(offset + i, str.charCodeAt(i)); } } // 波形データをDataViewを通して書き込む function floatTo16BitPCM(view, offset, audioWaveData) { for (let i = 0; i < audioWaveData.length; i++ , offset += 2) { let s = Math.max(-1, Math.min(1, audioWaveData[i])); view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); } } // モノラルのWAVEヘッダを書き込む function writeWavHeaderAndData(view, audioWaveData, samplingRate) { // WAVEのヘッダを書き込み(詳しくはWAVEファイルのデータ構造を参照) writeStringForArrayBuffer(view, 0, 'RIFF'); // RIFF識別子 view.setUint32(4, 36 + audioWaveData.length * 2, true); // チャンクサイズ(これ以降のファイルサイズ) writeStringForArrayBuffer(view, 8, 'WAVE'); // フォーマット writeStringForArrayBuffer(view, 12, 'fmt '); // fmt識別子 view.setUint32(16, 16, true); // fmtチャンクのバイト数(第三引数trueはリトルエンディアン) view.setUint16(20, 1, true); // 音声フォーマット。1はリニアPCM view.setUint16(22, 1, true); // チャンネル数。1はモノラル。 view.setUint32(24, samplingRate, true); // サンプリングレート view.setUint32(28, samplingRate * 2, true); // 1秒あたりのバイト数平均(サンプリングレート * ブロックサイズ) view.setUint16(32, 2, true); // ブロックサイズ。チャンネル数 * 1サンプルあたりのビット数 / 8で求める。モノラル16bitなら2。 view.setUint16(34, 16, true); // 1サンプルに必要なビット数。16bitなら16。 writeStringForArrayBuffer(view, 36, 'data'); // サブチャンク識別子 view.setUint32(40, audioWaveData.length * 2, true); // 波形データのバイト数(波形データ1点につき16bitなのでデータの数 * 2となっている) // WAVEのデータを書き込み floatTo16BitPCM(view, 44, audioWaveData); // 波形データ return view; }リクエスト受け取り部分(超絶一部抜粋)
hogehoge.php//リクエスト受け取り $req = $_FILES var_dump($req); //出力結果 array(2) { ["blob0"]=> array(5) { ["name"]=> string(4) "blob" ["type"]=> string(9) "audio/wav" ["tmp_name"]=> string(14) "/tmp/ランダム文字列" ["error"]=> int(0) ["size"]=> int(509996) }おわりに
ご指摘等ありましたら宜しくお願い致します!
- 投稿日:2020-02-05T19:52:17+09:00
gulp + webpack + babelをつかってみた
はじめに
以前作ったこれ(文字列→塩基配列の相互変換ツールをつくってみた(アプリ版))のWeb版でせっかくなんでよく聞くgulp, webpack, babelをつかってみました。色々みながらやりましたがバージョンの違いなどでエラー出まくってなかなか苦労しました。
あんまりwebさわったことないので間違ってたら優しく指摘していただけるとありがたいです。
とりあえず動くものはできました!!
やりたいこと
- SassをCSSに変換
- CSS圧縮
- 画像圧縮
- js圧縮
- babelでjsをES5で出力
- webpackでjsをまとめる
環境
- macOS Catalina 10.15.2
- node.js v13.7.0
npm導入
yarn ていうのもあるらしいですがとりあえず npm をインストール!
node.js で最新版をいれました。
ターミナルで下記コマンドが実行できれば導入完了。
node -vターミナルでプロジェクトフォルダに移動し下記コマンドを実行し、 package.json を作成!
npm init -ygulp導入
下記コマンドで gulp を導入!
npm install -D gulpgulp でやること
- SassをCSSに変換
- CSS圧縮
- 画像圧縮
下記コマンドで必要なやつを入れる!
npm install -D gulp-sass gulp-changed gulp-imagemin imagemin-pngquant imagemin-mozjpeg
- gulp-sass
SassをCSSに変換/CSS圧縮用- gulp-changed
画像圧縮の際に差分のみやる用- gulp-imagemin imagemin-pngquant imagemin-mozjpeg
画像圧縮用gulpfile.js を下記のように作成する。
gulpfile.jsvar gulp = require('gulp'); var sass = require('gulp-sass'); var changed = require('gulp-changed'); var imagemin = require('gulp-imagemin'); var imageminPngquant = require("imagemin-pngquant"); var imageminMozjpeg = require("imagemin-mozjpeg"); var dist = './dist'; // 出力先 // sassをコンパイルしてcss圧縮 gulp.task('sass', function(done){ gulp.src('./src/sass/*.scss') .pipe(sass({outputStyle: 'compressed'})) // 圧縮 .pipe(gulp.dest(dist + '/css')); done(); }); // 圧縮前と圧縮後のディレクトリを定義 var paths = { srcDir : 'src', dstDir : dist }; // いろんな拡張子を圧縮するためのオプション var imageminOption = [ imageminPngquant({ quality: [0.65, 0.8] }), imageminMozjpeg({ quality: 85 }), imagemin.gifsicle({ interlaced: false, optimizationLevel: 1, colors: 256 }), imagemin.optipng(), imagemin.svgo() ]; // 画像圧縮 gulp.task('imagemin', function(done){ var srcGlob = paths.srcDir + '/**/*.+(jpg|jpeg|png|gif|svg)'; // 元画像 var dstGlob = paths.dstDir; // 圧縮先 gulp.src(srcGlob) .pipe(changed(dstGlob)) //差分のみ圧縮 .pipe(imagemin(imageminOption)) .pipe(gulp.dest(dstGlob)); done(); }); gulp.task('default', gulp.series('sass', 'imagemin'), function(done) { done(); });フォルダ構成
project/ ├ src/ │ ├ /sass │ │ └ index.scss │ └ /img │ └ image.png ├ dist/ ├ gulpfile.js └ package.json下記コマンドを実行!!
npx gulpSass を CSS に変換、CSS 圧縮、画像圧縮ができました
ハマったとこ
CSSの圧縮
最初は css の圧縮を gulp-minify-css でやろうとして、sass のコンパイル後に実行する方法がわからず...
結局 gulp-sass でオプションを設定するだけで圧縮までできました。
画像の圧縮
jpeg 画像圧縮を
imagemin.jpegtran({progressive: true})
で書いててエラーでつまりました。
jpegtran
は使えない?みたいなエラーだったので imagemin-mozjpeg をいれました。webpack導入
webpackでやること
- js圧縮
- webpackでjsをまとめる
下記コマンドで必要なやつを入れる!
npm install -D webpack webpack-stream terser-webpack-plugin
- webpack-stream
gulpでwebpackを使う用- terser-webpack-plugin
js圧縮用webpack.config.js を下記のように作成する
webpack.config.jsconst path = require('path'); const TerserPlugin = require('terser-webpack-plugin'); module.exports = { mode: 'production', // 本番用(開発ならdevelopment(圧縮されない)) entry: './src/js/index.js', // バンドル前のやつのエントリポイント output: { // バンドル先 filename: 'bundle.js', path: path.join(__dirname, 'dist/js') }, optimization: { minimizer: [ // js圧縮 new TerserPlugin({ extractComments: 'all', // コメント削除 terserOptions: { compress: { drop_console: true, // console.log削除 }, }, }), ], } };js 圧縮参考:Webpack 4 圧縮時にJavaScriptのコメントを全て削除する
フォルダ構成
project/ ├ src/ │ ├ /sass │ │ └ index.scss │ ├ /js │ │ ├ other.js │ │ └ index.js │ └ /img │ └ image.png ├ dist/ ├ gulpfile.js ├ webpack.config.js └ package.jsongulpfile.js に下記を追加する。
gulpfile.jsvar webpackStream = require("webpack-stream"); var webpack = require("webpack"); var webpackConfig = require("./webpack.config"); gulp.task("webpack", function(done){ webpackStream(webpackConfig, webpack) .pipe(gulp.dest(dist + '/js')); done(); }); // webpack追加 gulp.task('default', gulp.series('sass'', 'imagemin', 'webpack), function(done) { done(); });
npx gulp
でバンドルされるハマったとこ
jsの圧縮
UglifyJsPlugin
でやろうとしたら対応してなかった...mode: 'development',
にしてたので圧縮されなかった...importとexport
import * from モジュール
でモジュール内のやつが全部呼べると思ったがそんなことはなかった
export default function test(){}
でimport Default from モジュール
でexport default
つけたやつ全部呼べると思ったけどそんなことはなかった(そもそもexport default
でエラーになる)最終的には下記の様な感じで落ち着いた。
test.jsexport function testA() { } export function testB() { }index.jsimport {testA, testB} from './test'; testA(); testB();import, export についてはもうちょっと勉強が必要
出力先
下記の
path: path.join(__dirname, 'dist/js')
のところが出力先だと思うのですがwebpack.config.jsoutput: { // バンドル先 filename: 'bundle.js', path: path.join(__dirname, 'dist/js') },github pages で公開したかったのでフォルダ名を dist から docs に変えたのですが gulpfile.js だけ書き換えて webpack.config.js は dist のままでしたが docs/js にちゃんと bundle.js ができてました
babel導入
下記コマンドで必要なやつを入れる!
npm install -D webpack babel-loader @babel/core @babel/preset-envwebpack.config.js に下記を追加。
webpack.config.jsoptimization { ・ ・ ・ }, module: { // ここ追加 rules: [ { test: /\.js$/, use: [ { loader: 'babel-loader', options: { presets: [ '@babel/preset-env', // デフォルトでES5になるはず ] } } ] } ] }
npx gulp
で ES5 で出力されるインストールしたやつのバージョン
npm install
したやつのバージョンを記載しておきます。"@babel/core": "^7.8.4", "@babel/preset-env": "^7.8.4", "babel-loader": "^8.0.6", "gulp": "^4.0.2", "gulp-changed": "^4.0.2", "gulp-imagemin": "^7.1.0", "gulp-sass": "^4.0.2", "imagemin-mozjpeg": "^8.0.0", "imagemin-pngquant": "^8.0.0", "terser-webpack-plugin": "^2.3.4", "webpack": "^4.41.5", "webpack-stream": "^5.2.1"さいごに
前作ったやつは XFREE のPHPサーバーで公開していたのですが、これ PHP いらなくね?と思いPHP の処理部分を js で置き換えてHTMLサーバーで公開しました。これで広告は出ない(が、まだhttp...)
PHP なくなったから github pages でいけるやんと思い完成したのがこちら
https://adventam10.github.io/DNAConverter-web_vue/
https!!
せっかくなんでユニバーサルリンクとかやりたい
参考
- 投稿日:2020-02-05T18:56:33+09:00
【JavaScript】Pagination.jsでページネーションをさくっと実装
人生には、JavaScriptでページネーションすることもあるでしょう。僕はありました。
というわけでPagination.jsを使った例を丁寧めに紹介します。
HTML, JavaScript
Pagination.jsとjQueryを公式から持ってきて下さい。スタイルは次の章で説明します。
<head> <link rel="stylesheet" href="./contents.css"><!-- 後で --> <link rel="stylesheet" href="./pagination.css"><!-- 後で --> </head> <body> <ul> <div id="diary-all-contents"></div><!-- コンテンツの埋め込み先をid指定 --> </ul> <div class="pager" id="diary-all-pager"></div><!-- ページャーの埋め込み先をid指定 --> <script src="./jquery-3.3.1.min.js"></script> <script src="./pagination.min.js"></script> <script> // [1] 配列のデータを用意 var diary = [ { title: "○○を買った", link: "https://foobar/diary/1234", category: "交流", class: "Relationship", date: "2020年02月04日" }, // ...以下略 ] // [2] pagination.jsの設定 $(function() { $('#diary-all-pager').pagination({ // diary-all-pagerにページャーを埋め込む dataSource: diary, pageSize: 5, // 1ページあたりの表示数 prevText: '< 前へ', nextText: '次へ >', // ページがめくられた時に呼ばれる callback: function(data, pagination) { // dataの中に次に表示すべきデータが入っているので、html要素に変換 $('#diary-all-contents').html(template(data)); // diary-all-contentsにコンテンツを埋め込む } }); }); // [3] データ1つ1つをhtml要素に変換する function template(dataArray) { return dataArray.map(function(data) { return '<li class="list"><a href="' + data.link + '">' + '<p class="category category-' + data.class + '">' + data.category + '</p>' + '<p class="title">' + data.title + '</p>' + '<p class="date">' + data.date + '</p></a></li>' }) } </script> </body>[1] データの用意
Pagination.jsに渡すデータは配列であればなんでもOK。
今回はこういうデータの配列があるとしましょう。var diary = [ { title: "○○を買った", link: "https://foobar/diary/1234", category: "交流", class: "Relationship", date: "2020年02月04日" }, // ...以下略 ][2] Pagination.jsの設定
要素の埋め込み先、1ページあたりの表示数、「戻る」「進む」の表記などを設定できる。
$(function() { $('#diary-all-pager').pagination({ // diary-all-pagerにページャーを埋め込む dataSource: diary, pageSize: 5, // 1ページあたりの表示数 prevText: '< 前へ', nextText: '次へ >', // ページがめくられた時に呼ばれる callback: function(data, pagination) { // dataの中に次に表示すべきデータが入っているので、html要素に変換 $('#diary-all-contents').html(template(data)); // diary-all-contentsにコンテンツを埋め込む } }); });以下のコールバックはページがめくられた時に呼び出され、次に表示すべきデータも渡ってくる。
このデータをhtml要素に変換することで自由な表示が可能。関数の中身は次で。// ページがめくられた時に呼ばれる callback: function(data, pagination) { // dataの中に次に表示すべきデータが入っているので、html要素に変換 $('#diary-all-contents').html(template(data)); // diary-all-contentsにコンテンツを埋め込む }他の設定は公式ドキュメントを見て下さい。
[3] データをHTMLへ
ここでは、Pagination.jsが渡してくれた表示データをHTML要素に変換する。
今回は予め外側を<ul>
要素で囲んでおいたので、そこに埋め込むための<li>
のリストを作る。
ここにスタイルを当てるのは、素のJavaScriptだとちょっと辛い。function template(dataArray) { return dataArray.map(function(data) { return '<li class="list"><a href="' + data.link + '">' + '<p class="category category-' + data.class + '">' + data.category + '</p>' + '<p class="title">' + data.title + '</p>' + '<p class="date">' + data.date + '</p></a></li>' }) }4行目ではカテゴリによってクラスが変わるようにしている。
+ '<p class="category category-' + data.class + '">' + data.category + '</p>'data.classが
Qiita
ならcategory-Qiita
というクラスに。対応するCSSは後述。
スタイル
コンテンツのスタイル
この部分です。
ここは表示するデータや目的に応じて好きなスタイルを付けて下さい。
あくまで一例として今回使用したスタイルを記載。contents.cssp, a, ul, li { margin: 0; padding: 0; border: 0; } ul { list-style: none; } .list { line-height: 1; border-bottom: 1px solid #d8d8d8; padding: 12px 0; } .list a { display: flex; align-items: center; text-decoration: none; } .list .date { color: darkblue; margin: 0 15px 0 auto; } .list .category { margin: 0 15px 0; padding: 4px; display: flex; align-items: center; color: white; } /* カテゴリによって違う背景色に */ .list .category-Qiita { background-color: lightblue; } .list .category-Relationship { background-color: lightseagreen; } .list .category-Food { background-color: lightslategray; } .list .category-Family { background-color: lightcoral; } .pager { margin: 18px 0 0 0; }ページャーのスタイル
この部分です。
デフォルトでpaginationjs
などのクラス名が付いたHTMLを出力されるので、それにスタイルを付けます。
もっとよい方法があるかもしれません。pagination.css.paginationjs-pages > ul > li > a { padding: 6px 18px; color: white; background-color: lightgreen; border: 1px solid darkcyan; } .paginationjs-pages > ul > li > a:hover { color: black; background-color: white; } .paginationjs-pages > ul > li.active > a { color: black; background-color: white; } .paginationjs-pages > ul > li.disabled > a { color: black; background-color: white; } .paginationjs-prev { margin: 0 16px 0 0; } .paginationjs-next { margin: 0 0 0 16px; } .paginationjs-page { margin: 0 4px; } .paginationjs-pages > ul { display: flex; align-items: baseline; } .paginationjs-pages > ul > li.paginationjs-ellipsis.disabled > a { border: none; color: black; margin: 0 4px; padding: 0; }感想
さくっと実装できました。遷移もスムーズでよかったです。
Pagination.jsはけっこう奥が深そうなので、次の機会があればもう少し掘り下げようと思います。
- 投稿日:2020-02-05T18:50:41+09:00
[React]テーブルのセルを結合する
概要
- Reactで配列をループさせてテーブルを描画する
- 特定の項目についてセルを結合させたい
作成イメージ
- 共通する項目についてセルを結合する
調べたこと
<table border="3"> <tr> <td rowspan="3">垂直方向の結合</td> <td>データ1</td> </tr> <tr> <td>データ2</td> </tr> <tr> <td>データ3</td> </tr> </table>実装
- 表示する配列
src/constants/GroupList.jsimport { nogizaka, keyakizaka, hinatazaka } from "./Color"; const groupList = [ { id: 1, name: "乃木坂46", color: nogizaka.color, memberList: [ { id: 1, name: "齋藤飛鳥", age: 21 } ] }, { id: 2, name: "欅坂46", color: keyakizaka.color, memberList: [ { id: 1, name: "渡邉理佐", age: 21 }, { id: 2, name: "小林由依", age: 20 } ] }, { id: 3, name: "日向坂46", color: hinatazaka.color, memberList: [ { id: 1, name: "齊藤京子", age: 22 }, { id: 2, name: "小坂菜緒", age: 17 }, { id: 3, name: "上村ひなの", age: 15 } ] } ]; export default groupList;
- 表示するコンポーネント
src/App.jsimport React from "react"; import { Table } from "react-bootstrap"; import styled from "styled-components"; import groupList from "./constants/GroupList"; const ColorTr = styled.tr` color: ${({ color }) => color}; `; function App() { return ( <Table bordered> <thead> <tr> <th>グループ</th> <th>名前</th> <th>年齢</th> </tr> </thead> <tbody> {groupList.map(group => group.memberList.map((member, i) => i === 0 ? ( <ColorTr key={member.id} color={group.color}> <td rowSpan={group.memberList.length}>{group.name}</td> <td>{member.name}</td> <td>{member.age}</td> </ColorTr> ) : ( <ColorTr key={member.id} color={group.color}> <td>{member.name}</td> <td>{member.age}</td> </ColorTr> ) ) )} </tbody> </Table> ); } export default App;行ったこと
- 配列の個数を結合させる行数として
rowspan
の値に設定src/App.js<td rowSpan={group.memberList.length}>{group.name}</td>
- ループさせる配列のインデックスをとり、先頭行の場合(インデックスが0の場合)とそうでない場合を分岐
src/App.js{groupList.map(group => group.memberList.map((member, i) => i === 0 ? ( <ColorTr key={member.id} color={group.color}> <td rowSpan={group.memberList.length}>{group.name}</td> // 省略 </ColorTr> ) : ( // 省略 ) ) )}終わりに
- とりあえず実現してみたけど何だか冗長・・・
- もっと良い方法をご存知の方いましたらコメントください
余談
本題とは全く関係ありませんが、今回使用したカラーコードはそれぞれ以下を参考にしています。
- 投稿日:2020-02-05T18:44:53+09:00
JSで要素同士に線や矢印を引けるライブラリ"LeaderLine"
LeaderLineとは
矢印や線の描画、範囲の描画が簡単に行えるオープンソース・ソフトウェアです。
使用方法
1、ホームページでフォルダのダウンロードを行います
2、ダウンロードしたフォルダの"leader-line.min.js"をwebページに埋め込みます
test.html<script src="leader-line.min.js"></script>3、矢印や線でつなぎたい要素をLeaderLineのコンストラクタに渡す
test.jsnew LeaderLine( document.getElementById('要素1'), document.getElementById('要素2') );使用例を一部紹介します
1、ノーマル
test.jsnew LeaderLine( document.getElementById('start'), document.getElementById('end') );2、サイズや形、色の変更
test.jsnew LeaderLine(element1, element2, { startPlugColor: '#1a6be0', endPlugColor: '#1efdaa', gradient: true }); new LeaderLine(element2, element3, {dash: {animation: true}}); new LeaderLine(element4, element5, {dropShadow: true}); new LeaderLine(element5, element6, {dash: true});3、一定条件に矢印がひかれる
test.jsnew LeaderLine(LeaderLine.mouseHoverAnchor(startElement), endElement);4、範囲を描画できる
test.jsnew LeaderLine(startElement, LeaderLine.areaAnchor(endElement));最後に
この他にもスクロールによる要素の位置の変更にも対応できたり、
div要素だけでなくbutton,ul,text,circle,,,などにも線をつなげることができます。詳しくはこちら
使ってみて便利だったので紹介させていただきました。
- 投稿日:2020-02-05T17:35:49+09:00
JavaScriptのつまずきやすいとこ
オブジェクト
配列とオブジェクトの違い
どちらも複数の値をひとまとめにし、key-valueの形になっている。
配列はkeyが番号だが、オブジェクトはkeyが名前を持っている。
(実は配列もオブジェクトの一種)オブジェクトの作り方
new演算子を使うか、オブジェクトリテラルかの2パターン。
new演算子
new演算子を使うとコンストラクタ(ある種の関数)が呼び出される。
コンストラクタがnewで呼び出された場合、新しいオブジェクトが作成され、それがthisになる。
newにより作成されたオブジェクトのことをコンストラクタから見てインスタンスと呼ぶ関数=オブジェクト
関数定義とは、作った関数を変数に入れる作業である。
function hello(){ alert("hello"); } console.log(typeof(hello));//functionと表示functionと表示されているが、オブジェクトである。
ただ()
をつけると呼び出せるというのは関数だけの特徴である。const a = {}; a.hello = greeting; a.hello(); function greeting(){ alert("hello"); }関数定義は実行される場所より後ろに書いてあっても機能する
- 投稿日:2020-02-05T17:27:32+09:00
WebWorker
WebWorkerのお勉強。
まずは最小限のコードでWebWorkerを動かしてみる。
任意のデータをワーカーに渡して、オウム返しして表示するだけ。メインスレッドconst worker = new Worker('worker.js') worker.onmessage = event => console.dir(event.data) worker.postMessage([1,2,3])worker.jsonmessage = event => postMessage(event.data)インラインworker
worker.jsが外部ファイルなのが気に入らない場合は、内部化できる。
クラスを文字列化し、それをBlob・URL化すれば、worker.jsの代わりになる。
文字列化するクラスは、ワーカーのコンテキストで動作することに注意すれば普通に書ける。また、ワーカーは非同期処理なので、プロミス化するのが好手。
データはクラスのコンストラクタに渡され、コンストラクタの戻り値がプロミスの値となる。
これら一連の作業を関数化したのが、次のコードになる。
使い方const workerが返す値 = await inlineWorker(worker化するクラス, workerに渡すデータ)function inlineWorker(fn, data){ const code = `onmessage = event => postMessage(new ${fn.toString()}(event.data))` const blob = new Blob([code], {type: 'text/javascript'}) const url = URL.createObjectURL(blob) function async(ok, ng){ const worker = new Worker(url) worker.onmessage = event => ok(event.data) worker.onerror = event => ng(event) worker.postMessage(data) } return new Promise(async) }
- 投稿日:2020-02-05T16:35:10+09:00
Chromeがsourcemapをリクエストするタイミング
jsとかのsourcemapがどのタイミングでリクエストされるか気になったので挙動を確認した。
chrome devtoolsを開いてるかどうかを変えて挙動を見る。前提
- chrome version: 80.0.3987.87
- bundler: webpack
- version: 4系
- devtool option: source-map(コンパイルされたコードとは別ファイルでsourcemapを用意するやつ)
結果
- chrome devtoolsのパネルを開かないで(画面に含まれる<script>経由で)jsをリクエストした場合
- js.mapはリクエストされない
- その後、chrome devtoolsを開くとjs.mapがリクエストされる。画面のリロードは必要ない。
- chrome devtoolsを開いた状態でjsをリクエストした場合
- js.mapはリクエストされる
- 投稿日:2020-02-05T15:13:06+09:00
JavaScript上でObjectをRailsで取得できる形のFormDataへ変換する
概要
TypeScript上で、
File
を含んだObjectをRails APIにputやpostしたい。
→ そのままだとFile
がうまく渡らないのでFormData
にする必要がある。
→File
以外のパラメータも含んだObject
をFormData
に変換したい。コード
/** * Convert object to FormData which rails can use. * This function is useful for uploading files. * * ex) * { id: 1, hero: { id: 1, name: 'NewHero' }, items: [1, 2] } * -> FormData with following parameters * id: 1 * hero[id]: 1 * hero[name]: NewHero * items[]: 1 * items[]: 2 * * @param params * @return FormData */ export const convertParamsToForm = (params: object): FormData => { const formData = new FormData(); const appendParamsToForm = (variable, prefix = '') => { if (typeof variable !== 'object' || variable instanceof File) { formData.append(prefix, variable); return; } if (Array.isArray(variable)) { variable.forEach(value => appendParamsToForm(value, `${prefix}[]`)); return; } Object.keys(variable).forEach(key => { appendParamsToForm( variable[key] || '', prefix ? `${prefix}[${key}]` : key ); }); }; appendParamsToForm(params); return formData; };
- 投稿日:2020-02-05T15:02:41+09:00
JavaScript でも Kotlin の trimMargin と trimIndent を使いたい
Kotlin には
String#trimMargin
やString#trimIndent
という便利なメソッドがあり、raw strings (JS のテンプレートリテラルに近いもの) を使いやすくしています。
String#trimMargin
は各行の第2引数に指定した文字列(省略した場合は|
) までの空白を削除し、String#trimIndent
は各行の左側の空白を一番少ない行に合わせて削除します。
ついでに最初と最後の行が空白のみ場合はその行を削除してくれます。Kotlinprintln("String#trimMargin()") val withoutMargin1 = """ABC |123 |456""".trimMargin() println(withoutMargin1) println("String#trimMargin(marginPrefix: String)") val withoutMargin2 = """ #XYZ #foo #bar """.trimMargin("#") println(withoutMargin2) println("String#trimIndent()") val withoutIndent = """ ABC 123 456 """.trimIndent() println(withoutIndent)OutputString#trimMargin() ABC 123 456 String#trimMargin(marginPrefix: String) XYZ foo bar String#trimIndent() ABC 123 456これを JavaScript にテンプレートリテラルに付けられるタグ、及び関数として利用できるように再現します。
JavaScriptconsole.log('trimMargin`~`') val withoutMargin1 = trimMargin`ABC |123 |456` console.log(withoutMargin1) console.log('trimMargin(str: string, marginPrefix: string)') val withoutMargin2 = trimMargin(` #XYZ #foo #bar `, '#') console.log(withoutMargin2) console.log('String`~`') val withoutIndent = trimIndent` ABC 123 456 ` console.log(withoutIndent)OutputtrimMargin`~` ABC 123 456 trimMargin(str: string, marginPrefix: string) XYZ foo bar String`~` ABC 123 456実装
テンプレートリテラル付けられるタグを作成する場合、関数に渡る引数は
(literals: TemplateStringsArray, ...placeholders: string[])
になります。
今回の場合は普通に展開してくれれば良いので、愚直に展開する関数を作成します。function resolveTenplate(literals: TemplateStringsArray, ...placeholders: string[]): string { return literals.reduce((str, literal) => (str += literal + (placeholders.shift() ?? '')), ''); }trimMargin
第一引数が string かどうかでタグとして呼び出されたか関数として呼び出されたかの判別が付きます。
除去には手っ取り早く正規表現を使用します。export function trimMargin(string: string, marginPrefix?: string): string; export function trimMargin(literals: TemplateStringsArray, ...placeholders: string[]): string; export function trimMargin( arg1: string | TemplateStringsArray, arg2 = '', ...args: string[] ): string { const string = typeof arg1 === 'string' ? arg1 : resolveTenplate(arg1, arg2, ...args); const strings = string.split('\n'); if (!strings?.[0].trim()) strings.shift(); if (!strings?.[strings.length - 1].trim()) strings.pop(); const marginPrefix = (typeof arg1 === 'string' && arg2) || '|'; const regexp = marginPrefix === '|' ? /^\s*\|/ : new RegExp(`^\\s*${arg2}`); return strings.map(s => s.replace(regexp, '')).join('\n'); }trimIndent
こちらも一応関数として使えるようにオーバーロードを用意します。
正規表現で頭の空白のみを取り出し、そのlength
が一番短いものに合わせてArray#slice
します。export function trimIndent(string: string): string; export function trimIndent(literals: TemplateStringsArray, ...placeholders: string[]): string; export function trimIndent(arg1: string | TemplateStringsArray, ...args: string[]): string { const string = typeof arg1 === 'string' ? arg1 : resolveTenplate(arg1, ...args); const strings = string.split('\n'); if (!strings?.[0].trim()) strings.shift(); if (!strings?.[strings.length - 1].trim()) strings.pop(); const indent = Math.min(...strings.map(s => /^\s+/.exec(s)?.[0].length ?? 0)); return strings.map(s => s.slice(indent)).join('\n'); }※あんまり詳しく仕様を調べていないので、Kotlin と挙動が異なるかもしれません。
参考文献
- 投稿日:2020-02-05T14:15:16+09:00
Google Sites を旧型から新型へ移行する際にやったこと&ついでにやったこと
初めに
ここでは業務で Google Sites を旧型から新型へ移行する際にやったこと、ついでにやったことをまとめています。旧型の Google Sites はいずれ近いうちに
サポートが切れるので旧型を使い続けている方々は注意が必要です
。そもそも Google Sites とは何か
その名の通り、Google 社によるウェブサイト作成サービスです。「忘れられたサービス」という異名が一部ではつけられています。
Google Sites の強み
- ハイパーリンクを張りやすい(いちいち
<a>
タグを貼らなくていい)- 文字フォントの変更がやりやすい
- ウェブページにファイルを添付しやすい
- サイトマップを自動で作れる(検索エンジン最適化で必ず必要になります)
- FTPが不要
- 自分でサーバーを設定する必要がない
- (何も設定しなくても)
文字化けを心配しなくて大丈夫
(試してみたら日本語や英語だけでなく、簡体字なども問題なく使えます)文字化け対策を心配しなくていいのは助かります。というのも通常のHTMLだと全てのページで head に以下を追加する手間がかかるからです:
text.html<head> <META http-equiv="Content-Type" content="text/html; charset=UTF-8"> <META http-equiv="Content-Type" content="text/html; charset=ISO-2022-JP"> <META http-equiv="Content-Type" content="text/html; charset=Shift_JIS"> <META http-equiv="Content-Type" content="text/html; charset=EUC-JP"> </head>この手間を省けるのはありがたいです。
弱点
- 付属しているHTMLエディタが小さくて見えづらい
- FTPを使いたくても使えない
- 数式を出力するのに必要なMathJax が使えない(代わりにCodecogsを使えばよい、これを使えば
<img>
タグで数式を画像として埋め込められる)作業環境
Google Sites の編集作業は全てブラウザ上で行います。Windows 10 Pro 上で Google Chrome, Mozilla Firefox, Brave などで動作確認してます。また、文法チェックや入力補助系のブラウザ拡張機能は無効にしています(Google Sites の編集を妨げる可能性があるからです)。
新型 Google Sites では何が変わるのか
新型 Google Sites は2016年11月に公開されました。新しい機能がいろいろあるのですが(各種ショートカットキーは一緒です)、以下が特筆すべき点ではないかと思います:
編集画面とプレビューを別々に閲覧できる
(旧型では編集中のプレビュー画面はなかった)ウェブサイトや添付ファイルが自動で Google Drive に保存される
(各ページにある添付ファイルはそれぞれ別個のフォルダに振り分けられる)HTML/JavaScript などの埋め込みが容易になった
(旧型では埋め込みを勝手に差し戻されることが多々あった)画面横にあるサイトマップが詳細に表示される
(旧型では Subsection までしか表示されなかった)- ドラッグするだけでテキストボックスの大きさ・位置を調整できる
特に簡単な操作でテキストボックスを操れるのは魅力的ではないかと思います。というのも、テキストボックスをHTMLで設定するのにはひと手間かかるからです:
textbox.html<div style="margin:0px;padding:0px;" align="center"> <div style="margin:0px;padding:0px;line-height:1.3;"> <div style="margin:0px;padding:10px;line-height:1.3;overflow:auto;text-align:left;height:350px;"> Enter to textbox. </div></div></div>この手間を省けるのは助かります。
旧型から新型 Google Sites に移行したら必ずやるべきこと
ここからが本題です。
フォントとレイアウトの確認
旧型から新型に移行した際にはテキスト及びハイパーリンクは保持されるものの、文字のサイズやテキストボックス、プラグインの位置は保持されません。適宜手動で再設定しなくてはいけません。旧型では文字サイズは大体自由に決められたのですが新型では
- Normal Text
- Title (
<h1>
に相当)- Heading (
<h2>
に相当)- Subheading (
<h3>
に相当)- Small
しか選択肢がありません。テキストボックスの大きさ、位置はドラッグするだけで十分です。
プラグインの動作確認
旧型で使えていたプラグインが新型でもしっかり動作するという保証はありません。もし動かなくなったら面倒でも設定しましょう。私が移行作業をした際は連絡フォームが動作しなくなったので新しく作り直しました(123FormBuilder という無料サービスを使っています)
ついでにやったこと(おまけ)
ここからはおまけです(随時追加します)。
旧型で作成したサイトのバックアップ
旧型の Google Sites ではページ全体のHTMLソースを表示するエディタが付属していたので(新型だとないみたいです)、HTMLソースをGitLab/BitBucket などに保存しておきました。ウェブ魚拓や Webpage Archive も使えます。
コードの埋め込み
上述の通り、新型ではコードの埋め込みが容易になったので、旧型ではできなかったこと(もしくはやりづらかったこと)を色々やってみました。
JavaScript の埋め込み
最終更新日の自動表示
旧型の Google Sites だと最終更新日を自動で表示してくれましたが、新型ではこれがなくなったみたいなので、JavaScript で埋め込みました。
LastUpdate.jsdocument.write("Last Update: " + new Date(document.lastModified))更新時刻まで表示したくない場合は次の書き方もできます:
LastUpdateJPN.jsvar day = new Date(document.lastModified); var y = day.getFullYear(); // Year var m = day.getMonth() + 1; // Month var d = day.getDate(); // Day document.write("最終更新: " + y + "年" + m + "月" + d + "日");コピーライト年号の自動更新
これも JavaScript を使えばできます:
copyright.htmlCopyright © 2000<script type="text/javascript">new Date().getFullYear()>2010&&document.write("-"+new Date().getFullYear());</script>, All Rights Reserved.各種プラグインの埋め込み
アクセスカウンターの埋め込み
無料でアクセスカウンターを埋め込む方法はいろいろありますが、私はこちらを使ってます。
参考資料
関連サイト
JavaScript 関連
組み合わせて使うと便利なウェブツール
- Pandoc, Online Converter (いろんなファイルをHTML形式に変換します)
- TAG index (各種ウェブ制作支援ツールが豊富にあります)
- 投稿日:2020-02-05T13:39:34+09:00
google api 住所から経度緯度取得してgoogle map に表示
やりたいこと
レストラン(写真。店名。店の説明。住所)の投稿の際に
住所入力したら、selfで経度緯度所得してgoogle map にピン立てる。環境
ruby 2.5.1
rails 5.2.3実装
DBの中身
postsテーブル
カラム名 型 内容 address string 住所 latitude float 住所緯度 longitude float 住所経度 title text 店の名前 description text 店の説明 image string レストランの写真 google API 所得
参考サイト
(https://nendeb.com/276)Maps JavaScript API
Geocoding API
この二つの有効化gem 導入
Gem.filegem "gmaps4rails" gem "geocoder"JS
ターミナル.rails g gmaps4rails:copy_jsapplication.html.haml%script(src="//maps.google.com/maps/api/js?v=3.23") %script(src="//cdn.rawgit.com/mahnunchik/markerclustererplus/master/dist/markerclusterer.min.js") %script(src="//cdn.rawgit.com/printercu/google-maps-utility-library-v3-read-only/master/infobox/src/infobox_packed.js" type="text/javascript") %script(src="/javascripts/gmaps_google.js")Model
post.rbgeocoded_by :address after_validation :geocode private def geocode uri = URI.escape("https://maps.googleapis.com/maps/api/geocode/json?address="+self.address.gsub(" ", "")+"&key=#{Rails.application.credentials.google_map_api}") res = HTTP.get(uri).to_s response = JSON.parse(res) self.latitude = response["results"][0]["geometry"]["location"]["lat"] self.longitude = response["results"][0]["geometry"]["location"]["lng"] endcontroller
posts_controller.rbdef show @post = Post.find(params[:id]) end private def post_params params.require(:post).permit(:image, :description, :text, :address, :latitude, :longitude) endveiw
show.html.haml#map :javascript function initMap() { var test = {lat: #{@post.latitude}, lng: #{@post.longitude}}; var map = new google.maps.Map(document.getElementById('map'), { zoom: 15, center: test }); var transitLayer = new google.maps.TransitLayer(); transitLayer.setMap(map); var contentString = '住所:#{@post.address}'; var infowindow = new google.maps.InfoWindow({ content: contentString }); var marker = new google.maps.Marker({ position:test, map: map, title: contentString }); marker.addListener('click', function() { infowindow.open(map, marker); }); } %script{:async => "", :defer => "defer", :src => "https://maps.googleapis.com/maps/api/js?v=3.exp&key=#{Rails.application.credentials.google_map_api}&callback=initMap"}show.scss#map { height: 400px; margin-left:auto; margin-right:auto; text-align:left; width: 80% }大切なこと
post.rbとshow.html.hamlのところに
下記の記載があると思います。#{Rails.application.credentials.google_map_api}この記載を消して自分で所得したAPIを打ち込めば動きますが
gitでプロジェクト管理してる時にgitにあげるとAPI_KEYが悪用される可能性があり、gitパトロールから注意が入ります。
なので環境変数を使います。環境変数の設定
rails のバージョンによって異なります。
今回はrails 5.2.3を使用しています。ターミナル.EDITOR="vi" bin/rails credentials:edit上記コマンドで環境変数の設定を行います。
aws:~~~~はデフォルトで書いてあると思います。
今回はgoogle_map_apiという変数に所得したAPI_KEYを代入している形です。
変数は自分の好きな名前で結構です。iで入力モードで編集追加できます。
:wqで保存、上書き保存できます。# aws: # access_key_id: 123 # secret_access_key: 345 google_map_api: 所得したAPI_KEY終わりに
私は今回このようなコードで住所から緯度経度所得してmapに表示させました。
gitにもあげているので詳細なコード確認したい方がおられましたら、キータの私のページにgitのリンクありますので見てください。
favorite_food_shareというプロジェクト名です。(キータ記事とコード少し違うところあります。)
- 投稿日:2020-02-05T13:33:49+09:00
ツイキャス配信で赤字になった時にデスクトップ通知する
https://qiita.com/JeYA9JoHojZtZYG/items/b404178dde49a1422470 でツイキャスの閲覧者数を通知するためのブックマークレットを書いたが、同じように赤字通知も作った。
javascript:(new MutationObserver((t,e)=>{t.find(t=>t.target.classList.contains('tw-timeup-timer--will-end-soon'))&&new Notification('赤字です。')})).observe(document.querySelector('.tw-status-indicator'),{attributes:!0});配信中は以下のように左上に配信時間が表示されている。
配信は通常30分で切れるが残り時間が少なくなる(多分3分前)と配信時間表示が赤くなる。
この要素は
tw-status-indicator
のclass属性がついているが、赤くなるときはtw-timeup-timer--will-end-soon
が追加される。その変更をMutationObserverで拾った。以下のように通知される。
- 投稿日:2020-02-05T13:18:30+09:00
create-react-appで生成したReactのimportを絶対パスでやる(VSCode対応)
- 投稿日:2020-02-05T12:58:19+09:00
Web系のファイルやツールの概要メモ
はじめに
ざっくり調べた際の個人的なメモです。
■クライアント側
Webページの3つの基礎ファイル
.html/.css/.js
この3つがわかれば、基本的なWebページを作ることができる。javascript
javascriptはクライアントサイド用ただ一つのプログラム言語(のはず)。
ECMAScript(ES5とかES6)という標準仕様に従って各ブラウザでパーサーが実装されている。
各ブラウザごとに対応状況が異なる。そのための対応策として後述の諸々が発生しているような?AltJs
Alternative Js。javascriptを拡張して独自構文を持つ「言語」の総称。
最終的には.jsファイルに変換してから使う。
CoffeeScript/TypeScript/Dart/Haxeなどが具体的な言語。Node.js
サーバサイドjavascriptの環境ソフト。
jsライブラリやツールを導入する際の親玉になる。
npmコマンドを持つ。jsのフロントエンド用ライブラリ
js記述を補助するライブラリ。
Anglur/jQuery/React/Vue.jsなど。
併用する場合はBowerを使った方がよいか。React用のフレームワーク
Redux/Flux
併用はしない。Bower
フロントエンド用ライブラリの管理ソフト。
フロントエンド用ライブラリを併用する場合は使った方がいいか。
npmで入れる。Gulp
ファイル変換系の「タスク」を管理してくれるツール。
npmで入れる。Babel
ECMAScripの標準仕様で書いたjavascriptをどのブラウザでも使えるようにしてくれるツール。
npmで入れる。Webpack
js,css,jpgなどのWeb用ファイルを一つのjsファイルにまとめてくれるツール。
npmで入れる。■サーバ側
サーバ側ミドルウェア
Apache/Nginx/IIS。
Post/Getを伴うような動的Webページを作るなら、これらのミドルウェアの助けが必要。
読まれるだけの静的Webページなら、NASで公開すれば可能。サーバ側プログラム言語
PHPやRubyが有名。
ただし、POST/GETを受け渡しできる言語なら、すべてサーバサイドにはなりえる(はず)。PHP用のフレームワーク
Laravel/CakePHP
Rubyのフレームワーク
Ruby on Rails
- 投稿日:2020-02-05T12:19:16+09:00
GoogleTagManagerを使ってHTML要素の追加や削除を行う
まえがき
webアプリケーションで特定条件時のみ画面のカスタマイズを行いたい場合に、あんまりソースコードにif文いっぱい書きたくないな……と思ったのでGoogleTagManager(以下GTM)を使用することにしました。
Analyticsと組み合わせて使われることが多い(と思われる)GTMですが、Javascriptを使用してソースコード本体に手を加えずにHTML要素のカスタマイズを行うこともできます。タグ作成
GTMから「新しいタグを作成」を押下し、「タグの設定」を選択します。
タグの設定を行う
「HTML」の箇所にJavascriptを記載します。
今回はシンプルにdiv要素の追加を行うことにします。要素の削除や、CSSスタイルの変更も可能です。
Javascriptは「ライブラリを使わない素のJavaScriptでDOM操作」を参考にさせていただきました。<script> var container = document.querySelector('.test-tag'); container.innerHTML = '<div>グーグルタグマネージャテスト挿入</div>'; </script>詳細設定から、タグ配信の優先度を100に設定しておきました。
タグを複数設定した場合、詳細設定でそれぞれのタグの優先順位や順序付けが可能なので色々調整が効きます。トリガー作成
タグの作成が完了したら「トリガーの選択」右上にある「+」マークからトリガーを新規追加します。
今回トリガーは「ページビュー > ページビュー」にしました。
サンプルでは特定IDのページだけカスタム画面を見せたいという設定なので、「一部のページビュー」を選択して条件を記載します。
サイトに差し込み用の要素を仕込む
<div class="test-tag"></div>メッセージを差し込みたい位置に、GTMで指定したクラスを入れておきます。
これだけ。プレビューで確認し、問題なければ公開します。
GTMのプレビューと公開を正しく行おうメリット
- if文でソースコードを汚さずに済む。
- デプロイ不要。画像やリンクの追加も楽。
- かなり柔軟に設定できるので、慣れるとソースがりがり書くよりスムーズ。
- GTMのバージョン管理も便利。
- 運用者と開発者が異なっており、細かい変更が定期的に入る箇所の修正を運用者が行える(ニュースとか?)。
デメリット
- うっかり不具合があった場合に、原因がGTMに記述したscriptなのかソースコード本体側にあるのかの検証がややめんどうくさい。
- 使用しているクラスやIDの管理をきちんと行わないと、後日混乱しやすい。(「あれ、このクラス使ってない?いらないなら消していい?」「あーそれGTMで使ってます」)
- Git上でソースコードと合わせて一括でバージョン管理できない。
まとめ
トリガーにCookieを使用することで、LPページ踏んだお客さんにはフォームにキャンペーンコード入力欄を表示するけど、LPページ見てないお客さんにはキャンペーンコード入力欄は非表示にするよ!
Javascriptにエラーがあったときにブラウザでアラート出すよ!
QAページのスクロール距離が長い場合に、ヘルプチャットへの誘導を行うよ!
などなど、いろんなことがGTMでできました。
便利ですが、チーム開発では管理がやや煩雑になるなあという印象はあるため、うまく使っていきたいと思います。
- 投稿日:2020-02-05T11:38:33+09:00
Imageの表示タイミングを揃える
方針
- 画像の取得タイミングを知る
- 画像の表示を切り替える
実装
JQuery
<div class="content" style="display: none;"> <img id="sample1"></img> <img id="sample2"></img> </div>var allImages = $("#sample1,#sample2"); var allImagesCount = allImages.length; var completeImageCount = 0; for(var i = 0; i < allImagesCount; i++){ $(allImages[i]).bind("load", function(){ completeImageCount++; if (allImagesCount == completeImageCount) { $(".content").css('display', 'block'); } }); }
- display vs visibility
visibilityは、要素自体は消えない方が好ましいので、visibilityを使用する。
Vue
img(:src="images" @load="loaded")Vueも同じ要領で
再現
Network conditionsの設定で表示の遅延を再現することができます。
Network conditionsの設定
1.「More tools」 => 「Network conditions」 で、「Network conditions」のタブを開きます。
2.「Network conditions」の「Network throttling」=>「Custom」 => 「Add」へ進みます。
3.「Add」で作成した「My LTE」を設定します。
再度、ロードすると画像の読み込みが遅くなり、遅延が発生する箇所が分かります。
Reference
- 投稿日:2020-02-05T08:09:47+09:00
手に馴染む道具(プログラミング言語)
はじめに
職人は、自分の道具を使えば使うほど手に馴染んで使いやすくなっていく。プログラミング環境でもエディタなんかはそう。そしてプログラミング言語もそう。
プログラミング言語の老舗といえばCであり、多くの職業プログラマはCを通ってきているはず。
そうでもないか。
いや、ある程度のスキルを持っている人はCに精通しているし、アセンブラも分かるだろう。
その前提で、やはり言語仕様(文法)はCライクなものが手に馴染みやすいと思うんだ。そういった点で、JavaScriptは手に馴染みやすい。JavaScript系は割りと好きな部類だ。
本題
しかし、世の人気を二分するメジャー・スクリプト言語RubyとPython、どちらもCライクではない。なぜだ。慣れてしまえばそれまでなのだが。
動的型付け言語は散々な言われようだが、スキルさえ問題なければアドホックでスピーディーな開発では超絶な生産性を達成できると思うんだ。
JavaScriptでnode.jsでも良いのだか、あれはもう全くLightweightではないよね。挙動も独特だし。
なぜ動的型付け言語でCライクな汎用かつlightweightなスクリプト言語がない、もしくは人気が無いのか。
という気持ちだけ表明してみた。
本日は以上。
- 投稿日:2020-02-05T07:20:41+09:00
[Vue.js]配列の変更が検知できない理由の「JavaScriptの制限」って何よ!
タイトルの
JavaScriptの制限
はこちらに記載されているものです。
リストレンダリング(注意事項) — Vue.js要約すると、
配列に対して直接インデックスの設定、またはlengthプロパティを変更する場合、view
は更新されません。具体的な操作はこちら。
インデックスでアイテムを直接設定するとき。例: vm.items[indexOfItem] = newValue
配列の長さを変更するとき。例: vm.items.length = newLengthその回避策が
Vue.set
かArray.prototype.splice
を使用するというものです。
簡単にできる回避策なので、ふーんって感じで書き換えて終了なのですが、
JavaScriptの制限
が気になったので調べてみました。ざっくり リアクティブシステム
Vue.jsの値の変更の検知の仕組みは、リアクティブシステム1で実現されています。
ざっくり言うと、Vueインスタンス化の際にプロパティに対してリアクティブになるよう、よしなにやってくれるシステムです。こちらの図でいうと、プロパティの変更をgetter/setterをトリガーにしてWatcherで再描画してくれているということです。
んで、
JavaScriptの制限
って何さ!ヒントはここに書いてありました。
リストレンダリング(配列の置き換え) — Vue.js例えば、filter()、concat()、そしてslice() のような、元の配列を変更しませんが、常に新しい配列を返します。
リアクティブシステムは新しい配列を返してくれることを望んでいるんです!
つまり
JavaScriptの制限
というのは、新しい配列を返さない操作
のことを言っているのです。
vm.items[indexOfItem] = newValue
もvm.items.length = newLength
も配列を返しません。なんだそれだけなのか
それだけなんです。
リストレンダリング(変更メソッド) — Vue.js
ここに書かれているメソッドは新しい配列を返すから検知できるんです。まとめ
私の日本語を読む能力が低かったせいで、
JavaScriptの制限
が配列の置き換えの章
に掛かっていることに気づくのに時間がかかってしまいました。(汗)お間違い等ございましたら、ご指摘いただけますと幸いです。
- 投稿日:2020-02-05T07:20:41+09:00
[Vue.js]配列の更新が検知できない理由の「JavaScriptの制限」って何よ!
タイトルの
JavaScriptの制限
はこちらに記載されているものです。
リストレンダリング(注意事項) — Vue.js要約すると、
配列に対して直接インデックスの設定、またはlengthプロパティを変更する場合、view
は更新されません。具体的な操作はこちら。
インデックスでアイテムを直接設定するとき。例: vm.items[indexOfItem] = newValue
配列の長さを変更するとき。例: vm.items.length = newLengthその回避策が
Vue.set
かArray.prototype.splice
を使用するというものです。
簡単にできる回避策なので、ふーんって感じで書き換えて終了なのですが、
JavaScriptの制限
が気になったので調べてみました。ざっくり リアクティブシステム
Vue.jsの値の変更の検知の仕組みは、リアクティブシステム1で実現されています。
ざっくり言うと、Vueインスタンス化の際にプロパティに対してリアクティブになるよう、よしなにやってくれるシステムです。こちらの図でいうと、プロパティの変更をgetter/setterをトリガーにしてWatcherで再描画してくれているということです。
んで、
JavaScriptの制限
って何さ!ヒントはここに書いてありました。
リストレンダリング(配列の置き換え) — Vue.js例えば、filter()、concat()、そしてslice() のような、元の配列を変更しませんが、常に新しい配列を返します。
リアクティブシステムは新しい配列を返してくれることを望んでいるんです!
つまり
JavaScriptの制限
というのは、新しい配列を返さない操作
のことを言っているのです。
vm.items[indexOfItem] = newValue
もvm.items.length = newLength
も配列は返しません。なんだそれだけなのか
それだけなんです。
リストレンダリング(変更メソッド) — Vue.js
ここに書かれているメソッドは新しい配列を返すから検知できるんです。まとめ
私の日本語を読む能力が低かったせいで、
JavaScriptの制限
が配列の置き換えの章
に掛かっていることに気づくのに時間がかかってしまいました。(汗)お間違い等ございましたら、ご指摘いただけますと幸いです。
- 投稿日:2020-02-05T02:49:51+09:00
【202002追試】Object.prototype.hasOwnProperty()は変数に入れた方が速い?
はじめに
Object.prototype.hasOwnProperty()は変数に入れた方が速い? - JavaScript
というブログ記事が私のMastodonのTLに出ていました。趣旨としては
hasOwnProperty
をどう呼び出すかで4通りの検証が以下のコードでなされていたようです。(function() { var myObj = { prop: 111 }; // テスト1: ユーザー定義の対象オブジェクトから呼び出す。 function test1() { var i, startTime, stopTime; startTime = new Date(); for (i = 0; i < 10000000; i++) { myObj.hasOwnProperty('prop'); } stopTime = new Date(); console.log(stopTime - startTime + 'ms'); } // テスト2: ネイティブオブジェクトメソッドからcallを使って呼び出す(Object.prototype.hasOwnProperty.call())。 function test2() { var i, startTime, stopTime; startTime = new Date(); for (i = 0; i < 10000000; i++) { Object.prototype.hasOwnProperty.call(myObj, 'prop'); } stopTime = new Date(); console.log(stopTime - startTime + 'ms'); } // テスト3: objProtoHasOwnというローカル変数に代入してキャッシュ化。 function test3() { var i, startTime, stopTime, objProtoHasOwn; startTime = new Date(); objProtoHasOwn = Object.prototype.hasOwnProperty; for (i = 0; i < 10000000; i++) { objProtoHasOwn.call(myObj, 'prop'); } stopTime = new Date(); console.log(stopTime - startTime + 'ms'); } // テスト4: ローカル変数でキャッシュ化した上で、hasOwnという関数にして使用。 function test4() { var i, startTime, stopTime, objProtoHasOwn, hasOwn; startTime = new Date(); objProtoHasOwn = Object.prototype.hasOwnProperty; hasOwn = function(obj, prop) { return objProtoHasOwn.call(obj, prop); }; for (i = 0; i < 10000000; i++) { hasOwn(myObj, 'prop'); } stopTime = new Date(); console.log(stopTime - startTime + 'ms'); } test1(); test2(); test3(); test4(); })();元の記事の検証結果
テスト環境:
Chrome 60.0.3112.113(64bit)
Firefox 55.0.3(64bit)
IE 11.413.15063.0(32bit)
JScript Panel 1.2.3.2PCは2600K, Memory8G, Windows10(64bit)
テスト種類 Chorome Firefox IE Microsoft Edge JScript Panel テスト1 (myObj.hasOwnProperty) 105 26 2986 3306 781 テスト2 (Object.prototype.hasOwnProperty.call) 100 1298 6534 7758 907 テスト3 (Object.prototype.hasOwnPropertyを変数に) 122 21 4437 5097 893 テスト4 (Object.prototype.hasOwnPropertyを変数→関数) 125 47 4907 6332 923 今回の再検証結果
Chrome 79.0.3945.130(64bit)
Firefox 72.0.2(64bit)
IE 11.592.18362.0(32bit)
Microsoft Edge 44.18362.449.0(64bit)PCはIntel Core i7-7500U, Memory16G, Windows10(64bit)
テスト種類 Chrome Firefox Internet Explorer Edge テスト1 (myObj.hasOwnProperty) 99(87.4~110.6) 58.25(55.4~61.1) 3083.65(3025.5~3141.8) 3172.95(3125.4~3220.5) テスト2 (Object.prototype.hasOwnProperty.call) 85(80.6~89.4) 55.9(51.7~60.1) 4567.1(4512.7~4621.5) 5660.65(5610.7~5710.6) テスト3 (Object.prototype.hasOwnPropertyを変数に) 103.2(98.0~108.4) 49.45(45.2~53.7) 3612.75(3555.3~3670.2) 4316.85(4255.6~4378.1) テスト4 (Object.prototype.hasOwnPropertyを変数→関数) 117.9(110.5~125.3) 46.25(45.5~47.0) 3752.75(3688.9~3816.6) 4836.55(4751.9~4921.2) 元記事とは違って20回計測で、95%信頼区間を括弧中に表示しています。TBのことを考え、測定の間には30秒~1分程度の間隔があります。
考察
今回と元記事の差
- 元記事のようにFirefoxのテスト2だけ猛烈に遅いということは確認されなかった。
- 全般にPCスペックなどの要因から速度が速いが、なぜかFirefoxのテスト2以外だけ遅い
いずれの検証にも言えること
- 全般にFirefoxとChromeが速いのはなんか最適化がかかって処理が飛んでいるのではないか?(検証方法がわからず断念
- IRよりEdgeのほうが遅い。いっちょんわからん
結論
まあIEとかもう誰も使ってないし(ぇ)、Edgeは中身Chromeになるので、ぶっちゃけどれ使ってもいいんじゃないですかね。
データ
Chrome
測定回数 1 2 3 4 1 202 106 122 118 2 98 95 102 178 3 85 96 101 120 4 91 89 102 118 5 137 98 131 118 6 101 77 96 106 7 94 79 89 110 8 90 96 96 120 9 100 80 103 119 10 95 85 129 114 11 80 70 96 113 12 102 79 90 102 13 94 77 89 102 14 82 87 98 96 15 76 77 101 118 16 88 99 111 112 17 87 82 108 111 18 91 71 99 144 19 95 81 91 114 20 92 76 110 125 平均 99 85 103.2 117.9 標準偏差 26.50283004 9.959919678 11.96076921 16.85200285 標準誤差 5.926212956 2.227105745 2.674509301 3.768222393 95%信頼区間 11.61516405 4.365047085 5.241941947 7.385580233 95%信頼区間最大 110.6151641 89.36504708 108.4419419 125.2855802 95%信頼区間最小 87.38483595 80.63495292 97.95805805 110.5144198 Firefox
測定回数 1 2 3 4 1 44 41 41 44 2 59 42 44 47 3 61 40 42 49 4 56 55 48 48 5 65 58 54 46 6 65 58 54 46 7 55 56 63 47 8 60 60 43 46 9 59 67 71 44 10 66 57 45 46 11 58 57 44 46 12 51 64 44 47 13 53 49 42 45 14 65 55 44 46 15 46 49 62 51 16 68 45 45 45 17 65 72 43 47 18 60 55 42 44 19 59 60 45 45 20 50 78 73 46 平均 58.25 55.9 49.45 46.25 標準偏差 6.594505288 9.658674857 9.754358 1.669580786 標準誤差 1.47457621 2.159745355 2.181140757 0.373329613 95%信頼区間 2.890116287 4.233023146 4.274957362 0.731712602 95%信頼区間最大 61.14011629 60.13302315 53.72495736 46.9817126 95%信頼区間最小 55.35988371 51.66697685 45.17504264 45.5182874 IE
測定回数 1 2 3 4 1 3464 5035 4125 4225 2 2928 4499 3556 3667 3 3059 4371 3567 4052 4 3091 4469 3680 3804 5 3107 4548 3616 3756 6 3019 4510 3565 3692 7 3266 4627 3652 3704 8 2804 4511 3518 3574 9 3079 4544 3562 3652 10 2942 4646 3458 3625 11 3031 4522 3532 3679 12 3019 4610 3540 3728 13 3108 4497 3576 3677 14 3122 4585 3603 3789 15 3192 4576 3551 3709 16 3217 4632 3691 3852 17 3041 4539 3596 3670 18 3091 4540 3681 3785 19 3088 4577 3635 3707 20 3005 4504 3551 3708 平均 3083.65 4567.1 3612.75 3752.75 標準偏差 132.7472316 124.0503527 131.150629 145.5853272 標準誤差 29.68318337 27.73850212 29.32617218 32.55386882 95%信頼区間 58.17797081 54.36646557 57.47824174 63.80441094 95%信頼区間最大 3141.827971 4621.466466 3670.228242 3816.554411 95%信頼区間最小 3025.472029 4512.733534 3555.271758 3688.945589 Edge
測定回数 1 2 3 4 1 3116 5658 4239 4682 2 3116 5658 4239 4682 3 3118 5641 4390 4910 4 3071 5583 4205 4662 5 3170 5816 4466 5031 6 3352 5714 4523 4899 7 3352 5714 4523 4899 8 3053 5521 4175 4569 9 3059 5462 4043 4514 10 3093 5606 4225 4965 11 3323 5674 4490 4967 12 2978 5443 4190 4623 13 3167 5532 4264 4876 14 3191 5684 4379 4746 15 3174 5779 4306 4799 16 3300 5762 4238 5262 17 3244 5754 4439 4876 18 3058 5569 4113 4618 19 3205 5776 4420 5021 20 3319 5867 4470 5130 平均 3172.95 5660.65 4316.85 4836.55 標準偏差 108.4976843 114.0461639 139.7155235 193.0822299 標準誤差 24.26081975 25.50149751 31.2413408 43.17449913 95%信頼区間 47.55033332 49.98201706 61.23190328 84.62046401 95%信頼区間最大 3220.500333 5710.632017 4378.081903 4921.170464 95%信頼区間最小 3125.399667 5610.667983 4255.618097 4751.929536
- 投稿日:2020-02-05T00:25:55+09:00
Andorid のブラウザで加速度の値を取得してグラフ表示したりローカルなファイルにセーブしたりする
やること
タイトルそのままです。Chrome でしか動作は確認していません。多分 iOS + Safari でも動きます。下記サイトで動作を見れます。
正常に動くと上のような感じになります。加速度センサがついてない PC などでは、加速度の値やグラフは表示されません。
ソースコード
全体のコードは下記に置いてます。
要点
加速度データの取得
下記のコードで、加速度を取得するたびに arr に加速度データを記録していきます、
- https://kkblab.com/make/javascript/acc.html
上記サイトのコードほぼそのままです。acc.jsvar aX = 0, aY = 0, aZ = 0; // 加速度データの記録用 var startTime = Date.now(); // 開始時間の記録 var arr = []; // セーブする用 // 加速度データを取得する window.addEventListener("devicemotion", (dat) => { aX = dat.accelerationIncludingGravity.x; aY = dat.accelerationIncludingGravity.y; aZ = dat.accelerationIncludingGravity.z; arr.push( {"t": Date.now() - startTime, "x": aX, "y": aY, "z": aZ }); });データをローカルなファイルにセーブする
arr を json に変換して、Blob を使ってダウンロードできるようにします。
- https://qiita.com/wadahiro/items/eb50ac6bbe2e18cf8813
上記ページのコードを json でダウンロードできるように変更したものです。アンカーではなく、ボタンを押したらセーブするようにするには、少し複雑なコードを書く必要あり。
save.jsfunction handleDownload() { var blob = new Blob([ JSON.stringify(arr) ], { "type" : "application/json" }); if (window.navigator.msSaveBlob) { window.navigator.msSaveBlob(blob, "acc.json"); window.navigator.msSaveOrOpenBlob(blob, "acc.json"); } else { document.getElementById("download").href = window.URL.createObjectURL(blob); } // セーブしたらデータをリセットする。リセットしたくなければ下記2行をコメントアウトする。 arr = []; startTime = Date.now(); }グラフを表示する
arr の末尾から canvas の幅と同じ pixel 分だけデータを取りだして、頭から順に線をつないで引いていっているだけです。縦方向の座標値は適当なので、端末によっては縦幅を越えて描画されるかもしれません。
graph.jsvar canvas; var ctx; var preX = 0, preY = 0, preZ = 0; // 描画用 // 加速度データをグラフ表示する displayData function displayData() { // 加速度の値を文字で表示する var txt = document.getElementById("txt"); txt.innerHTML = "x: " + aX + "<br/>" + "y: " + aY + "<br/>" + "z: " + aZ; // canvas の背景をグレーで塗る if ( ! canvas || ! canvas.getContext ) { return false; } ctx.fillStyle = "gray"; ctx.fillRect( 0, 0, canvas.clientWidth, canvas.clientHeight ); ctx.lineWidth = 1 ; // 最新から 300px 分のデータを取りだす。 var varr = []; if ( arr.length > canvas.clientWidth){ varr = arr.slice(-canvas.clientWidth,-1); }else{ varr = arr; } for ( idx in varr ){ if ( idx > canvas.clientWidth ) break; var dat = varr[idx]; // X 軸加速度を赤の線で表示する var dy1 = canvas.clientHeight/2 + dat.x * 10.0; var dx = idx; ctx.strokeStyle = "red"; ctx.beginPath(); ctx.moveTo(dx,dy1); ctx.lineTo(dx-1,preX); ctx.stroke(); // Y 軸加速度を青の線で表示する var dy2 = 50 + dat.y * 10.0; ctx.strokeStyle = "blue"; ctx.beginPath(); ctx.moveTo(dx,dy2); ctx.lineTo(dx-1,preY); ctx.stroke(); // Z 軸加速度を緑の線で表示する var dy3 = 50 + dat.z * 10.0; ctx.strokeStyle = "green"; ctx.beginPath(); ctx.moveTo(dx,dy3); ctx.lineTo(dx-1,preZ); ctx.stroke(); // 次のデータを表示するときに前のデータから線を描く用 preX = dy1; preY = dy2; preZ = dy3; } }端末にもよりますが、それなりに高速にデータ取りつつ描画ができるので、最近のスマフォはすごいなーと思ったり。