- 投稿日:2019-10-09T23:56:45+09:00
【Rails/javascript(jQuery)】非同期での投稿削除機能
前回、編集機能の非同期化について記載させていただきましたが、今回は削除機能の非同期化について記事を書かせていただきます。
Deleteボタンを押すと、削除するかどうかの確認のアラートが出て、さらにアラートのOKボタンを押すと、きちんと削除処理がされていることが確認できますね。(正確にはこの動画だとちゃんと非同期になっているかわからないと思いますが、、)
削除処理の非同期化については、編集処理の非同期化に比べると、記述量も少なく、HTMLを入れ込む必要もないので楽でしたが、いくつか個人的にポイントだと思った箇所がありましたので記載します。
<削除機能実装におけるポイント>
①削除機能の場合、jbilderのファイルやコントローラへのjson.formatの記載は不要
②削除ボタンを押した際のアラートをどのようなビューにするか①については、当たり前といえば当たり前ですが、これまでの非同期化では、jbuilderとjson.formatをセットで使用していたので、自分は勢いでふたつとも作成してしまいました、、。
ただ、冷静に考えれば、削除処理の場合、動的な情報が入ったHTMLを新たに作成するわけではありませんし、DBから返してもらうデータはないのでいずれも必要ありませんね。
また、削除処理については、基本的に警告表示は必要だと思いますが、アラートをどのようなビューにするのかによってサイトの印象が変わってしまうので、今回は一番簡単なalertメソッドをそのまま使用しましたが、サイトの印象を重視するなら非同期でHTMLを作成するのもありかもしれません。ということで、以下コードを記載しますので、皆さまの何かのお役に立てば幸いです。
blog_destroy.js$(function() { $(document).on("click", ".delete_id", function (e) { e.preventDefault(); var deleteMessage = confirm('削除してよろしいでしょうか?'); if(deleteMessage == true) { var blog_element = $(this).parents('.content'); var blog_id = blog_element.attr("data-blog-id"); var url = location.href + "/" + blog_id; $.ajax({ url: url, type: "POST", data: {'id': blog_id, '_method': 'DELETE'} , dataType: 'json' }) .done(function(data) { blog_element.remove(); }) .fail(function() { alert('blog destroy error'); }) } }); });index.html.haml= render partial: 'devise/shared/header' .contents -@blogs.each do|blog| .content{"data-blog-id": "#{blog.id}"} .content__messages .content__message .content__message__box{"data-edit-id": "#{blog.id}"} - if blog.content.present? %p.lower-message__content = blog.content .content__message__info %p.message__upper-info__talker ユーザ: = blog.user.name %p.message__upper-info__date 投稿日時: = blog.created_at.strftime("%Y/%m/%d %H:%M") .content__message__edit-and-delete-btn %p.edit_id Edit %p.delete_id Delete .pict =image_tag blog.image.url, class: 'pict__img' if blog.image.present? = render partial: 'devise/shared/footer'
- 投稿日:2019-10-09T23:23:14+09:00
JavaScriptの関数について
はじめに
Java出身である自分自身、JavaScript関数のJava関数とは異なる挙動を整理しきれていないと感じていてため、一覧にまとめた。
JavaScript 関数概要
まずはじめにJavaScriptにおける関数(以下、関数と呼ぶ)は第1級オブジェクトである。第1級オブジェクトとは生成、代入、演算、受け渡しができる対象のこと(=値である)と定義されている。つまり関数はオブジェクトであり、値である。以下、JavaScriptにおける関数について他の言語にはない特徴や性質を中心に列挙していく。
関数宣言と関数式
定義
関数宣言は以下のように定義する
function add(num1, num2){ return num1 + num2; }関数式は以下のように定義する
var add = function(num1, num2){ return num1 + num2; }関数の巻き上げ
変数や関数の宣言が、自身のスコープ内のコードの一番上に来ているかのような動作を巻き上げという。関数を使用するコードを書いた後にその関数を宣言することができる。関数宣言では関数の巻き上げが行われる。
var result = add(5,5); function add(num1, num2){ return num1 + num2; }関数式では関数巻き上げは行われない。つまり以下はエラーが発生する。
var result=add(5,5); var add=function(num1, num2){ return num1 + num2; }先の関数の巻き上げについての説明にて
関数を使用するコードを書いた後にその関数を宣言することができる
と、あたかも便利であるというニュアンスで述べているが、基本的には関数の巻き上げが実行されるようなコードは書くべきではないとされる。他のたいていの言語なら当てはまるコードを上から読んでいくという行動ルールから外れることを余儀なくされるからである。他人がコードを見る可能性が有る限り、関数は必ず変数に代入し、使用する前に定義した方が良いとされる。関数の受け渡し
関数概要で述べたとおり、関数はオブジェクトであり、値である。つまり関数を値のように受け渡すことができる。
function hoge(){ console.log("hoge!"); } hoge(); // hoge!と表示 var hoge2 = hoge; hoge2(); // hoge!と表示関数の引数
関数には任意の数の引数を渡すことができる。
function reflect(value){ return value; } console.log(reflect("Hi")); console.log(reflect("Hi",25));オーバーロード
同じ関数名の複数の関数を、その関数が持つ引数の型、数、並び順によって実行する関数を呼び分けるのがオーバーロードである。しかし「関数の引数」の項目で述べたとおり、JavaScriptの関数は任意の引数の数を取ることができる、また型は指定されていない。以上よりJavaScriptはオーバーロードをサポートしていない。JavaScriptでは同じ名前で複数の関数を定義すると、最後に定義した関数が有効になり、先に定義した関数は消える。
function sayMessage(message){ if(arguments.length===0){ message="デフォルトメッセージです"; } console.log(message); } sayMessage("Hello"); sayMessage();最後に
コンストラクタの挙動は今後追記したい。
参考文献
- 投稿日:2019-10-09T22:51:51+09:00
gRPC-Web + React + Node.js + TypeScriptでシンプルなチャットサービスを作る
概要
かねてよりgRPCおよびgRPC-Webに興味があり、これを用いてシンプルなリアルタイムチャットサービスを制作し、公開した。
本稿では、その開発工程について解説する。ゴール
gRPC-Webを用いて「わいわいチャット」を作る。
https://waiwai-chat-2019.aanrii.com/
内容はシンプルなチャットアプリケーションだ。サイトを開くとまず過去ログが表示され、ほかの入室者の投稿が随時流れてくる。任意の名前で入室すると投稿欄が出現し、発言ができる。発言した内容はサイトにアクセスしている全員に、即座に共有される。過去ログは無限スクロールで遡ることができる。
フロントエンドはReactを用いたSPAとし、Netlifyを使って静的サイト生成・配信する。また、バックエンドはGKE上で動くNode.jsアプリケーションとし、かつenvoyをプロキシとして挟んで外部と通信させる。そして、フロントエンド-バックエンド間はgRPC-Web (HTTPS) で通信する。
なお、コードはここで公開している。
https://github.com/aanrii/waiwai-chat-2019やること
完成に到るまで、主に以下のことに取り組む。
- バックエンド開発
- gRPC-Web + Node.js + TypeScriptによるアプリケーション開発 (+ grpcurlによるデバッグ)
- envoy-proxyを通したフロントエンドとの接続
- GCPでの実行 (GKEへのデプロイ、およびCloud SQL + Cloud Pub/Subの導入。別稿にて解説)
- SSL有効化 (cert-managerによる証明書自動取得。別稿にて解説)
- フロントエンド開発
- gRPC-Web + React + TypeScriptによるアプリケーション開発
- Netlifyを用いた静的コンテンツ配信 (別稿にて解説)
本稿では、バックエンド・フロントエンドアプリケーションがローカル上で動くことを目標として解説を進める。
GCP上での実行とgRPCサーバの冗長化、SSL有効化などについては、別稿にて説明する。開発
Service (protobuf) の定義
gRPCアプリケーションを作るため、まずgRPCサーバ (バックエンド) のI/Fを定義する.protoファイルを作る。
proto/MessageService.protosyntax = "proto3"; import "google/protobuf/empty.proto"; service MessageService { rpc GetMessageStream(google.protobuf.Empty) returns (stream Message); rpc PostMessage(Message) returns (PostMessageResponse); } message PostMessageResponse { string status = 1; // メッセージの処理結果 } message Message { string text = 1; // 発言内容 int64 create_time = 2; // 発言日時 string author_name = 3; // 投稿者名 }MessageServiceはGetMessageStreamとPostMessageという、ふたつのメソッドをもつ。PostMessageはフロントエンドからバックエンドへのメッセージ送信に用いる。バックエンドはMessageを受け取り、PostMessageResponse (受信に成功したらOK、失敗したらError、等) を返す。このように、1回のリクエストに1回のレスポンスを返すようなRPCは、Unary RPCと呼ばれる。
一方、GetMessageStreamはフロントエンドがバックエンドからメッセージを受信するのに使う。バックエンドはgoogle.protobuf.Empty (void型のようなもの) を受け取り、Messageのstreamを返す。ようは、フロントエンドは初期化時に一回だけGetMessageStreamRequestをバックエンドに送り、その後はMessageがどこかでPostされるたびに、それをstreamで随時受け取ることができる。このように、1回のリクエストに対し複数個のレスポンスを含むストリームを返却するRPCは、Server streaming RPCと呼ばれる。詳細は、公式ガイドを参照のこと。
バックエンド (gRPCサーバ) の開発
パッケージのインストール
バックエンドの開発に入る。必要なパッケージをインストールする。まず、TypeScriptとNode.js。
% yarn add typescript ts-node @types/node続いて、gRPCを利用するためのパッケージを追加する。
% yarn add grpc google-protobuf @types/google-protobuf最後に、TypeScript用protobufコンパイラを導入する。これらは、protoファイルからTypeScript+Node.jsで使える型定義ファイルを生成するために用いる。
% yarn add grpc-tools grpc_tools_node_protoc_ts --dev
そして、以下のスクリプトを書く。
backend/server/protoc.sh#!/usr/bin/env bash set -eu export PATH="$PATH:$(yarn bin)" # protoファイルがあるディレクトリへの相対パス PROTO_SRC=../../proto # 生成したjs、tsファイルを格納したいディレクトリへの相対パス PROTO_DEST=./src/proto mkdir -p ${PROTO_DEST} grpc_tools_node_protoc \ --js_out=import_style=commonjs,binary:${PROTO_DEST} \ --grpc_out=${PROTO_DEST} \ --plugin=protoc-gen-grpc=$(which grpc_tools_node_protoc_plugin) \ -I ${PROTO_SRC} \ ${PROTO_SRC}/* grpc_tools_node_protoc \ --plugin=protoc-gen-ts=$(yarn bin)/protoc-gen-ts \ --ts_out=${PROTO_DEST} \ -I ${PROTO_SRC} \ ${PROTO_SRC}/*これで、
bash protoc.sh
を実行することで、protoファイルをjs/tsコードにコンパイルすることが可能になる。gRPCサーバの実装
生成されたファイル、サーバーサイドのinterface (IMessageServiceServer) が自動生成されるので、このクラスを実装する。
backend/server/src/MessageService.tsimport { EventEmitter } from 'events'; import { Empty } from 'google-protobuf/google/protobuf/empty_pb'; import * as grpc from 'grpc'; import { IMessageServiceServer } from './proto/MessageService_grpc_pb'; import { Message, PostMessageResponse } from './proto/MessageService_pb'; class MessageService implements IMessageServiceServer { // PostMessageにより投稿されたメッセージをGetMessageStreamで返却するstreamに流すための中継器 private readonly messageEventEmitter = new EventEmitter(); // 過去ログを保存する配列 private readonly pastMessageList: Message[] = []; public getMessageStream(call: grpc.ServerWriteableStream<Empty>) { // 過去ログをstreamに流し込む this.pastMessageList.forEach(message => call.write(message)); // PostMessageが実行されるたびに、そのメッセージをstreamに流し込む const handler = (message: Message) => call.write(message); this.messageEventEmitter.on('post', handler); // streamが切断された時、上記Listenerを消去する call.on('close', () => { this.messageEventEmitter.removeListener('post', handler); }); } public postMessage(call: grpc.ServerUnaryCall<Message>, callback: grpc.sendUnaryData<PostMessageResponse>) { // 受け取ったメッセージを過去ログに保存する const message = call.request; this.pastMessageList.push(message); // messageEventEmitter経由で、getMessageStreamで返却するstreamにメッセージを送る this.messageEventEmitter.emit('post', message); // レスポンスを返す const response = new PostMessageResponse(); response.setStatus('ok'); callback(null, response); } } export default MessageService;一旦ローカルで動かすために、インメモリ上にメッセージを貯めることとする。
gRPCサービスの実装ができたので、これをサーバ上で動かす。backend/server/src/index.tsimport * as grpc from 'grpc'; import MessageService from './MessageService'; import { MessageServiceService } from './proto/MessageService_grpc_pb'; (() => { const server = new grpc.Server(); server.bind(`0.0.0.0:9090`, grpc.ServerCredentials.createInsecure()); server.addService(MessageServiceService, new MessageService()); server.start(); })();デバッグ
以下のコマンドにより、ローカル (0.0.0.0:9090) でサーバを起動できる。
% yarn ts-node src/index.ts今回は動作確認のため、grpcurlを使う。
% brew install grpcurl
以下コマンドでGetMessageStreamを呼び出すと、待機状態に入る。
% grpcurl -plaintext -import-path proto/ -proto MessageService.proto 0.0.0.0:9090 MessageService/GetMessageStreamこの状態でターミナルを別に立ち上げ、Messageを投稿してみる。成功すればレスポンスが返ってくる。
% grpcurl -d "{\"text\":\"hello\",\"create_time\":$(node -e 'console.log(Date.now())'),\"author_name\":\"aanrii\"}" -import-path proto/ -proto MessageService.proto -plaintext -v 0.0.0.0:9090 MessageService/PostMessage Resolved method descriptor: rpc PostMessage ( .Message ) returns ( .PostMessageResponse ); Request metadata to send: (empty) Response headers received: accept-encoding: identity,gzip content-type: application/grpc grpc-accept-encoding: identity,deflate,gzip Response contents: { "status": "ok" } Response trailers received: (empty) Sent 1 request and received 1 responseGetMessageStreamを実行したウィンドウに戻ると、受信したMessageが表示されている。
{ "text": "hello", "createTime": "1570468135968", "authorName": "aanrii" }proxyの準備・実行
現状のgRPC-Webの仕様だと、ブラウザから直接gRPCサーバに接続することはできず、プロキシを挟む必要がある (詳細) 。
ここでは、公式の例に倣って、envoyを利用する。まず、envoy.yamlに設定を記述する。backend/proxy/envoy.yamladmin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: message_service max_grpc_timeout: 0s cors: allow_origin: - "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout max_age: "1728000" expose_headers: custom-header-1,grpc-status,grpc-message http_filters: - name: envoy.grpc_web - name: envoy.cors - name: envoy.router clusters: - name: message_service connect_timeout: 0.25s type: logical_dns http2_protocol_options: {} lb_policy: round_robin hosts: [{ socket_address: { address: host.docker.internal, port_value: 9090 }}](Docker Desktop for Macで動かすためには、公式のenvoy.yamlのL45を
hosts: [{ socket_address: { address: host.docker.internal, port_value: 9090 }}]
と書き換える必要がある)そして、envoyを実行するためのDockerfileを記述する。
backend/proxy/DockerfileFROM envoyproxy/envoy:latest COPY ./envoy.yaml /etc/envoy/envoy.yaml CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml EXPOSE 8080そして、以下のコマンドでDockerイメージのビルド、起動を行う。
% docker build -t waiwai-chat-grpc/envoy -f ./Dockerfile . % docker run -d -p 8080:8080 -p 9901:9901 waiwai-chat-grpc/envoy:latest前述のgrpcurlコマンドを、9090から8080にポートを変更して実行してみると、プロキシが機能していることがわかる。
% grpcurl -plaintext -import-path proto/ -proto MessageService.proto 0.0.0.0:8080 MessageService/GetMessageStream { "text": "hello", "createTime": "1570468135968", "authorName": "aanrii" }これで、最低限それらしい挙動をするgRPCサーバが完成した。
フロントエンドの開発
パッケージのインストール
今回はフロントエンドも、バックエンドと同じくTypeScriptで記述していく。
create-react-appを用いて、React + TypeScriptのボイラープレートから開発を始める。% yarn create react-app frontend --typescript
続いて、gRPCを利用するためのパッケージを追加する。
% yarn add @improbable-eng/grpc-web ts-protoc-genフロントエンドではts-protoc-genを用いて.protoファイルをjsファイルに変換する。ここでも、バックエンド同様protoc.shを用意する。
frontend/protoc.sh% #!/usr/bin/env bash set -eu # protoファイルがあるディレクトリへの相対パス PROTO_SRC=../proto # 生成したjs、tsファイルを格納したいディレクトリへの相対パス PROTO_DEST=./src/proto mkdir -p ${PROTO_DEST} # protoc-gen-tsへのパス PROTOC_GEN_TS_PATH="$(yarn bin)/protoc-gen-ts" protoc \ --plugin="protoc-gen-ts=${PROTOC_GEN_TS_PATH}" \ --js_out="import_style=commonjs,binary:${PROTO_DEST}" \ --ts_out="service=true:${PROTO_DEST}" \ -I ${PROTO_SRC} $(find ${PROTO_SRC} -name "*.proto")このスクリプトを動かすには別途protocのインストールが必要となる。
% brew install protoc
実装
まず、gRPCクライアントを生成し、あらゆるコンポーネントから利用できるようにするためのHOCを作る。
frontend/src/attachMessageServiceClient.tsximport React from 'react'; import { MessageServiceClient } from '../proto/MessageService_pb_service'; export type MessageServiceClientAttached = { client: MessageServiceClient; }; const client = new MessageServiceClient(`http://0.0.0.0:8080`); const attachMessageServiceClient = <P extends {}>(WrappedComponent: React.ComponentType<P & MessageServiceClientAttached>) => class MessageServiceAttached extends React.Component<P> { render() { return <WrappedComponent {...this.props} client={client} />; } }; export default attachMessageServiceClient;投稿フォームは次のようにする。フォームに入力された文字列をもとにMessageを生成し、postMessageを実行する。
frontend/src/components/PostForm.tsximport React, { useState, FormEvent } from 'react'; import { Message as ProtoMessage } from '../proto/MessageService_pb'; import attatchMessageServiceClient, { MessageServiceClientAttached } from './attatchMessageServiceClient'; const PostForm: React.FC<{ initialInputText?: string } & MessageServiceClientAttached> = ({ initialInputText = '', client, }) => { const [inputText, setInputText] = useState(initialInputText); const handleSubmit = (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); const currentDate = Date.now(); const message = new ProtoMessage(); message.setAuthorName('hoge'); // 一旦適当に埋める message.setCreateTime(currentDate); message.setText(inputText); client.postMessage(message, (error, response) => console.log(error == null ? error : response)); setInputText(''); }; return ( <div> <form onSubmit={handleSubmit}> <input type="text" name="inputText" value={inputText} onChange={e => setInputText(e.target.value)} /> <input type="submit" value="Submit" /> </form> </div> ); }; export default attatchMessageServiceClient(PostForm);ログの表示に再しては、まずgetMessageStreamによってstreamを得て、Messageが得られるたび随時更新するよう、ハンドラを登録しておく。
frontend/src/components/MessageList.tsximport React from 'react'; import Message from './Message'; import { Message as ProtoMessage } from '../proto/MessageService_pb'; import attatchMessageServiceClient, { MessageServiceClientAttached } from './attatchMessageServiceClient'; import { Empty } from 'google-protobuf/google/protobuf/empty_pb'; interface MessageListState { protoMessageList: ProtoMessage.AsObject[]; } class MessageList extends React.Component<void & MessageServiceClientAttached, MessageListState> { constructor(props: {} & MessageServiceClientAttached) { super(props); this.state = { protoMessageList: [] }; // message streamの取得 const messageStream = props.client.getMessageStream(new Empty()); // streamからmessageを受け取るたび、それをprotoMessageListに格納するハンドラを登録する messageStream.on('data', message => { const newProtoMessageList = [message.toObject()].concat(this.state.protoMessageList); this.setState({ protoMessageList: newProtoMessageList }); }); } render() { return ( <div> {this.state.protoMessageList.map(protoMessage => ( <Message {...protoMessage} key={protoMessage.createTime} /> ))} </div> ); } } export default attatchMessageServiceClient(MessageList);MessageのためのPresentational Componentも適当に作っておく。
frontend/src/components/Message.tsximport React from 'react'; import { Message as ProtoMessage } from '../proto/MessageService_pb'; const Message: React.SFC<ProtoMessage.AsObject> = protoMessage => ( <div> {protoMessage.text} ({new Date(protoMessage.createTime).toString()}) </div> ); export default Message;App,tsxも書き換えよう。
frontend/src/App.tsximport React from 'react'; import PostForm from './components/PostForm'; import MessageList from './components/MessageList'; const App: React.FC = () => { return ( <div> <PostForm /> <MessageList /> </div> ); }; export default App;ここで、
yarn start
を起動してみよう。ブラウザを確認すると、大量にエラーが出ているのがわかる。./src/proto/MessageService_pb.js Line 27: 'proto' is not defined no-undef Line 30: 'proto' is not defined no-undef Line 31: 'COMPILED' is not defined no-undef Line 36: 'proto' is not defined no-undefこれについては、実際のところ、protocで生成されたjsファイルをeslintのチェックから除外する方法が有効だ (参考)。ちょっとダサいが。
/* eslint-disable */再び
yarn start
を実行すると、問題なくフロントが表示されることがわかる。まとめ
ここまでで、gRPC-Web + React + Node.js + TypeScriptを用いて、少なくともローカルで動くチャットアプリケーションを作成した。続編 (今後書く予定) では、GCPへデプロイを行い、Kubernetes (GKE) 上でgRPCサーバを動かし、またその冗長化、負荷分散、SSL対応のための設定について紹介する。
参考文献
- 投稿日:2019-10-09T22:08:49+09:00
setInterval
- 投稿日:2019-10-09T21:54:45+09:00
electron-builderで詰まったこと
はじめに
electron-builderの記事古いーーーーー!!!!!
ということで自給自足する
まあ難しいことはないんだけどこの記事で助けられる人はいる(はず)node_modulesがない
そう、無いんですよ(全肯定)
参考になったのはこの記事
ソースコードは読みたくないのでありがたい...
完結にいうと「node_modules」が消されるとのこと
そのためsemantic-ui-less
をインストールする必要がある
とりあえずnpmで突っ込んで使いましょう
これでnode_modulesがapp.asarに入りますあれ?ないやつもあるけども
そう、無いんですよ(全肯定)
electronでサポートしてないものは消される感じ
なので使うモジュールはpackage.jsonのdependencies
に書くべし
自分ならtwitterを使うのでpackege.json"dependencies": { "twitter": "^1.7.1" }てな感じで
これでnode_modulesにファイルが入りますこのおかげでいらないやつは削除されるのでremoveし忘れても大丈夫ですよ奥さん!!!
コンパイルできない
そう、できないんですよ、昔の記事ならね(全肯定)
多分検索して一番最初にでてくるこの記事を見ると思います
新しい記事だし信用性は高いですね
この記事で紹介されてるbuild-win.jsは次の通りbuild-win.jsconst builder = require('electron-builder'); builder.build({ platform: 'win', config: { 'appId': 'local.test.app1', 'win':{ 'target': { 'target': 'zip', 'arch': [ 'x64', 'ia32', ] } } } });これです
でもこれ今だとエラーでるんですよね原因は簡単、使えないものがあるからです
electron-builderのgithubを見たらわかる通り大分使えなくなってます
特にplatform
は気づきにくいですよね
よって変更したのが下build-win.jsconst builder = require('electron-builder'); builder.build({ config: { 'appId': 'local.test.app1', 'win':{ 'target': 'zip' } } });いやーすっきり
さいごに
electron-packagerは下位互換っぽいですね
配布するならelectron-builderを使おう
- 投稿日:2019-10-09T20:46:33+09:00
食べログからGoogleでの評価を簡単に確認できるChrome拡張をつくってみた
はじめに
食べログの評価点をお金で売っていたんじゃないかという疑惑が話題になっていますね。
自分も振り返ってみると、食べログで評価が高いのにイマイチだったなぁというお店もあれば、
食べログでは評価が低いけど行ってみたら大満足!ということもありました。このような現状で、最近ではGoogleマップでのお店の評価を合わせて確認すると良い!という話をよく聞くようになりました。
身近なお店で検索してみると、そこそこ評価数もあり、情報も信頼できそうな感じです。
また訪日外国人のコメントなどもあり、よりグローバルな評価となっています。ただいちいちGoogleで検索かけるのも面倒ですよね。
そこでワンクリックで検索結果のページに飛べるChrome拡張をつくってみました。使い方
ダウンロード/インストール
最初は「食べログーグル」という名前で考えてましたが、怒られそうなのでやめました笑
ぜひインストールしてみてください!動作確認環境
Windows 10 Pro 64bit
Chrome 77.0.3865.90
ログアウト状態で確認Webの技術しか使っていないので、Macでも動くはず。
食べログやGoogleの仕様変更で使えなくなる場合あり。
食べログのURL規則などは詳しく調べてはいないため、想定外のページで実行されて、エラーなど出てしまうページがあるかもしれません。
またログイン状態やユーザー属性で取得対象のDOMが異なると思われるため、ログインしている場合はエラーが出ているかもしれません。仕組み
まず食べログのページ上から店名と住所を取得します。
その後店名と住所を含めたGoogleマップの検索結果表示用URLを生成し、DOM操作でリンクを追加します。
Googleの検索エンジンが優秀なので、こんな雑な検索方法でもしっかり見つけてくれます。ハマったポイント
・URLパターンマッチングで正規表現が使えない
manifest.jsというファイルで、Javascriptの実行対象とするURLを設定するのですが、なんと正規表現が使えません。
こちらはざっくりと設定した上で、Javascriptからlocation.hrefでURLを取得し、処理対象のページかを判定するようにしました。改善点
・食べログのページ上にGoogleの評価を併記したほうが良い。
→そのためにはバックグラウンドでHTTPリクエストを投げて、レスポンスから評価を抜き出す必要がある。
今回は早く完成させたかったため省きました。詳しくは調べていませんが、Google MapのPlaces APIというものでお店の情報を取得できるようなので、
それが使えればベストかもしれません。ソースコード
間違いや改善点があればご指摘いただけると幸いです。
main.js(() => { // manifest.jsonでは細かなURLパターンマッチングを指定できない // 改めてURLを厳密にチェックする // URLが対象外の場合は処理を終了する。 const pattern = new RegExp('https://tabelog\\.com/.*/.*/.*/[0-9]{8}/'); if(pattern.test(location.href) === false){return;} // お店の住所を取得する // プレーンテキストではないため、HTMLの状態からテキストのみを抜き出す const getShopAddress = () => { const shopAddressNodes = document.getElementsByClassName('rstinfo-table__address')[0].childNodes; let shopAddress = ''; for(let i = 0; i < shopAddressNodes.length; i++) { if(shopAddressNodes[i].tagName !== 'SPAN'){continue;} shopAddress += shopAddressNodes[i].innerText; } return shopAddress; } // リンク先URLの生成 const shopName = document.getElementsByClassName('display-name')[0].innerText; const shopAddress = getShopAddress(); const googleUrl = 'https://www.google.co.jp/maps/search/' + shopName + shopAddress; // タグの生成 const p = document.createElement('p'); const a = document.createElement('a'); a.href = googleUrl; a.innerText = 'Googleで検索'; a.target = "_blank"; a.rel = "noopener noreferrer"; // タグの挿入先取得 const targetDom = document.getElementsByClassName('rdheader-rstname')[0]; // タグの挿入 p.appendChild(a); targetDom.appendChild(p); })();おわりに
自分で作っていうのもアレですが、なかなか便利です。
また初めてChromeの拡張作りましたが、思ったより簡単につくれる!
勝手に敷居が高いものだと思っていましたが、そんなことなかったです。
GreasemonkeyなどのUserScriptを作った経験がある方でしたら楽勝だと思われます。
- 投稿日:2019-10-09T20:22:30+09:00
VueとCanvasでサクッと使えるルーレットアプリを作った
TL;DR
- VueとCanvasでルーレットを作ったよ
- ルーレットの各要素のウェイトを変えると確率を調整できるよ
- レスポンシブなのでモバイルでも使えるよ
こだわりとか
カラーテーマ
バンドリの25人のキャラのイメージカラーがルーレットの各要素に割り振られます
これがやりたかったために作ったと言っても過言ではないです
そのためルーレットの要素は25個までしか設定できません
バグではなく仕様です
ちなみにBanG RoulletというタイトルはBanG DreamからですUIをシンプルにした
結果の表示や、ルーレットの初期化の確認メッセージ等を除き、UIから文字を廃して直感的に操作できるようにしました
操作できるボタンはカラーにして、操作できないものはグレーにすることで視覚的なわかりやすさも意識しましたルーレットの減速時の挙動
停止ボタンを押すと徐々に減速するようにしました
停止するのが遅すぎるとイライラするし、早すぎると抽選してる感がないので、いい塩梅に減速させるのに苦労しました
結果としてなかなかリアルな動きになったのではないかと思いますレスポンシブにしてモバイルにも対応
ルーレットを使いたいときに目の前にPCがあることって稀だと思うんです
通常はモバイルで使いたいと思うので、頑張ってCanvasの描画を維持したままリサイズできるようにしました
addEventListener
でウィンドウのリサイズされるとCanvasの幅を更新するようにしています使った技術とかライブラリ
- Vue.js
- Canvas
- Bootstrap
- Element UI
- Firebase
できるだけライブラリは利用せずに作りました(リファレンスを読むのがめんどくさかった)
setInterval
でルーレットの描画処理をかなりの回数呼び出すことになるので、その処理を重くしたくなかったというのもあります
円グラフの描画ライブラリとかを使えば、見た目をもっとよくできるかもしれませんが…CSSフレームワークはElement UIを使用していますが、レスポンシブデザインには対応してないっぽいので結局Bootstrapも導入しました
正直Bootstrapだけで完結できる気がするので、Element UIはそのうちはがすかもしれませんデプロイ先としてFirebaseを初めて利用しましたが、サーバー側の機能はまったく使ってないので活用できてない感がすごいです
認証などの機能を利用するアプリを開発する際は使ってみたいですねできなかったこと
本当はルーレットの針も一緒に動くようにしたかったのですが諦めました
ルーレットの低速時には、要素の境界が針に差し掛かったときに針が動くようにしないと視覚的に違和感があるため、針は止めたままにしました
単純にルーレットの減速に合わせて針の動作頻度を落とすだけならできるのですが、境界に合わせて動作させるとなるとかなり難しいですまとめ
なかなか使いやすい操作性にできたんじゃないかと思います
要素の編集、削除、確率調整、ルーレットのリセット、リロードの機能もあるので、総じて満足いく作りにできました
ただ、要素を25個を超えて追加しようとしたときのアラートのデザインがデフォルトのままなので、UIはもう少し詰めようと思います使った感想や、こういう機能が欲しいという要望があればコメントしてもらえると嬉しいです!
最後に、バンドリはいいぞ。
ソースコード
GitHub
https://github.com/rootsuke/BanG-Roullet参考
こちらの記事でCanvasでルーレットを描画する着想を得ることができました
ありがとうございました
https://tech-blog.s-yoshiki.com/2019/01/1024/
- 投稿日:2019-10-09T19:37:26+09:00
【TC39 Proposals】do expressions
注意書き
2019 年 10 月 9 日に開催の #tc39_study 用に作成した資料です。
執筆時点で TC39 の Proposals に上がっている機能を紹介します。
将来的に JavaScript(ECMAScript)に取り入れられるかもしれませんし、取り入れられないかもしれませんし、紹介内容から仕様が大きく変更になるかもしれません。
TC39 の承認プロセスについては、https://tc39.es/process-document/ をご覧ください。
do-expressions
ブロックの末尾の式(completion value)を値として返せるようにします。
例えば、変数
x
を初期化するために一時変数tmp
を使うとします。
tmp
はx
のためにしか使わないので、適切にブロックで囲ってあげたくなります。
現時点だと以下のように即時関数を用いるのが簡単な方法でしょう。const x = (() => { let tmp = f(); return tmp * tmp + 1 })();これをより簡潔にするのが do-expressions です。
doブロックの最後の式が値として返るので、以下のように書くことができます。const x = do { let tmp = f(); tmp * tmp + 1 };
if文との組み合わせ
doブロックの末尾にif文を書くことで、if文のブロックの末尾の式がdoブロックの戻り値として返ります。
let x = do { if (foo()) { f() } else if (bar()) { g() } else { h() } };
JSXでの利用
JSX内で以下のような書き方ができるようになります。
return ( <nav> <Home /> { do { if (loggedIn) { <LogoutButton /> } else { <LoginButton /> } } } </nav> )
懸念点
do-while と組み合わせた場合の文法上の曖昧性を回避する必要があります。
do do f(); while (x);
その他の議論点
issueに上がってる論点をいくつか紹介します。
非同期まわり
do async {}
(orasync do {}
)とかhttps://github.com/tc39/proposal-do-expressions/issues/4
Generatorまわり
do * {}
とかhttps://github.com/tc39/proposal-do-expressions/issues/10
例外まわり
こんな感じで動いてほしい。
const foo = () => do { throw new Error('foo'); }; const bar = () => 3; const res = do { try { foo(); } catch (e) { bar(); } } console.log(res); // 3;https://github.com/tc39/proposal-do-expressions/issues/6
doいらなくね?
代替案
{>let x = 100; x * x}
let x = {| /* blah */ |}
- non ASCIIな記号を使う(« » 「 」 〈〉 《 》 【 】 〔 〕 ⦗ ⦘)
https://github.com/tc39/proposal-do-expressions/issues/32
doブロック内でのreturnとか
doブロック内で
return
した場合に、doブロックの値になるべきか関数の戻り値になるべきか?あと
var
continue
break
をどうするか。https://github.com/tc39/proposal-do-expressions/issues/30
https://github.com/tc39/tc39-notes/blob/master/meetings/2018-07/july-24.md#update-on-do-expressions
今すぐ使いたい人へ
babelのpluginが使えます。
https://babeljs.io/docs/en/babel-plugin-proposal-do-expressions
所感
気持ちはわかったが議論を見る限り標準化はなかなか難しそう。
条件分岐に関してはPattern Matchingでもできるので、もう少し複雑なケースでの住み分けか。
参考情報
- 投稿日:2019-10-09T18:47:47+09:00
Vue.js + moment.jsでカレンダーと連動する前日翌日ボタンをつくる
間違えや、もっと良い方法があれば教えていただけますと幸いです
必要なもの
・vue.js
・V-Calendar(カレンダーライブラリ)
・moment.js動作
See the Pen Vue 連動するカレンダーと前日翌日ボタン by natusme (@natsume0718) on CodePen.
解説
流れとしては
1.カレンダーがDateじゃないとダメなのでdataはDateで作成
2.filterでmoment.js を用いて表示形式を変える
3.前日翌日ボタンにclickイベント設定し、dateに±1する1.カレンダーがDateじゃないとダメなのでdataはDateで作成
HTML<v-date-picker class="mx-2" v-model="date" :popover="{ placement: 'bottom', visibility: 'click' }"> <button variant="outline-primary">カレンダー</button> </v-date-picker>JSdata: { date: new Date() }
V-Calendar
はDate型だと双方向バインディングできるのでDate型で作成2.filterでmoment.js を用いて表示形式を変える
次に日付の表示部分
HTML{{date | moment}}JSfilters: { moment: function(date) { return moment(date).format("YYYY-MM-DD"); } }
YYYY-MM-DD
の形式にフォーマットしているだけ3.前日翌日ボタンにclickイベント設定し、dateに±1する
HTML<button v-on:click="subDay">前日</button> {{date | moment}} <button v-on:click="addDay">翌日</button>JSmethods: { addDay: function() { const next = this.date.getDate() + 1; const tmp = new Date(this.date); tmp.setDate(next); this.$set(this, "date", tmp); }, subDay: function() { const prev = this.date.getDate() - 1; const tmp = new Date(this.date); tmp.setDate(prev); this.$set(this, "date", tmp); } },まず、翌日、前日を取得します。(数値が得られる)
現在のdateから一時用Date変数を作成
作成した一時用変数に翌日または、前日をセット
それをdataプロパティのdateにセットの流れです
参考にさせていただいた記事
https://ti-tomo-knowledge.hatenablog.com/entry/2018/07/09/163051
- 投稿日:2019-10-09T18:17:44+09:00
tc39/proposals Record & Tuple
注意事項
- これは#tc39_studyの発表資料です
- この機能はまだ提案/策定途中であり仕様に入るまでに大きな変更が入る可能性があり、またこの機能の策定自体が中止される可能性もあります。
ステータス
- proposal
- Champion
- Robin Ricard (Bloomberg)
- Richard Button (Bloomberg)
- Stage: 1 (0 to 1 per 2019.10.01 TC39)
- (Stage0の時点では "Const Value Types: Record & Tuple" 名前だった)
- 実装済みのエンジン: なし
モチベーション
言語レベルでimmutableなデータ型が欲しい
これまでと現状
- かつて別の提案があったが、仕様の複雑さとユースケースの観点から廃棄された
- Immutable.js などのライブラリが存在する
Record
基本のSyntax
const record1 = #{ a: 1, b: 2, c: 3, }; record1.a === 1; // true record1["a"] === 1; //trueスプレッド構文
const record2 = #{...record1, b: 5}; record1 !== record2; //true record2 === #{ a: 1, c: 3, b: 5 }; // trueIteration
const record = #{ a: 1, b: 2 }; // TypeError: record is not iterable for (const o of record) { console.log(o); }Recordはiterableではない
Tuple
基本のSyntax
const tuple1 = #[1, 2, 3]; tuple1[0] === 1; // true
with
const tuple2 = tuple1.with(0, 2); tuple1 !== tuple2; // true tuple2 === #[2, 2, 3]; // trueスプレッド構文
const tuple3 = #[1, ...tuple2]; tuple3 === #[1, 2, 2, 3]; // true
push
,pop
const tuple4 = tuple3.push(4); tuple4 === #[1, 2, 2, 3, 4]; // true const tuple5 = tuple4.pop(); tuple5 === #[2, 2, 3, 4]; // trueIteration
// 1 // 2 for (const o of tuple) { console.log(o); }
Equality
-0
のケースは常にfalse
、NaN
のケースは常にtrue
(?)- 議論のissue
-0 === +0; // true Object.is(-0, +0); //false #{ a: -0 } === #{ a: +0 }; //false #[-0] === #[+0]; //falseNaN === NaN; // false Object.is(NaN, NaN); // true #{ a: NaN } === #{ a: NaN }; // true #[NaN] === #[NaN]; // true
JSON.stringify
JSON.stringify(record)
はJSON.stringify(object)
と同じ結果JSON.stringify(tuple)
はJSON.stringify(array)
と同じ結果JSON.stringify(#{ a: #[1, 2, 3] }); // '{"a":[1,2,3]}' JSON.stringify(#[true, #{ a: #[1, 2, 3] }]); // '[true,{"a":[1,2,3]}]'
typeof
現在のところどちらも
record
になるのが妥当だと考えられているtypeof {a: 1} === "object"; typeof [1, 2] === "object"; typeof #{ a: 1 } === "record"; typeof #[1, 2] === "record";
- 投稿日:2019-10-09T18:02:31+09:00
Slice notation - TC39 Proposals
この記事は2019年10月9日に書いたものであり、#tc39_study TC39 Proposals LT 大会で使用したものです。
状況
- Stage : 1
- URL : https://github.com/tc39/proposal-slice-notation/
- Champion : ?
- Authors : ?
コミット履歴を見る限り、Sathya Gunasekaran(Google,v8の人)
提案内容
slice表記構文を追加する提案。
現状はArray.prototype.slice()
を使用して、配列から指定した要素を元にして配列をshallow copyする。
- 左は配列の下限(デフォルト0)
- 真ん中は配列の上限(デフォルトは配列の長さ)
- 右はステップ引数、1はなにもない、2は最初と最後の値の配列(デフォルトは1)
- すべて指定しなくてもOK
const arr = ['a', 'b', 'c', 'd']; arr[1:4:2]; // → ['b', 'd']
Prior artの章にもあるように、すでにPython、CoffeeScript、Go、Rubyでは同様のことができると説明がある(触ったことがないのでわからない)
slice表記構文を使用する場合はこのようになる
const arr = ['a', 'b', 'c', 'd']; const str = 'hello world'; arr.slice(1, 3); // → ['b', 'c'] str.slice(6); // → 'world'
Slice notationの場合はこうなる.
arr[1:3];
は2つめから3つめの要素の配列を生成するstr[6:];
は:
の右辺を指定していないので、6文字目から先のすべての要素が含まれた配列を生成するconst arr = ['a', 'b', 'c', 'd']; const str = 'hello world'; arr[1:3]; // → ['b', 'c'] str[6:]; // → 'world'
このようにobjectのindexにアクセスして操作もできる
const obj = { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 }; obj[1:3]; // → ['b', 'c']
const arr = ['a', 'b', 'c', 'd']; arr[:3:1]; // 下限引数は指定されていないので0 // → ['a', 'b', 'c'] arr[1::1]; // 上限引数は指定されていないので配列長さである4 // → ['b', 'c', 'd'] arr[1:]; // 上限引数は上記の通り4、ステップ引数は指定されていないので1 // → ['b', 'c', 'd'] arr[:3]; // 上限引数は上記の通り0、ステップ引数は上記の通り1 // → ['a', 'b', 'c'] arr[1::2]; // 上限引数は上記の通り4、ステップ引数は上記の通り2なので、要素の最初と最後を取得 // → ['b', 'd'] arr[:3:2]; // 下限引数は指定されていないので0、ステップ引数は2なので、要素の最初と最後を取得 // → ['a', 'c']
なにも指定しない場合は、配列のshallow copyになる
const arr = ['a', 'b', 'c', 'd']; arr[:]; // → ['a', 'b', 'c', 'd'] arr[::]; // → ['a', 'b', 'c', 'd']
- マイナス表記もある
- 下限計算
start = max(lowerBound + len, 0)
- 上限計算
end = max(upperBound + len, 0)
- 配列の要素を逆転させたい場合はステップ引数を
-1
にするconst arr = ['a', 'b', 'c', 'd']; arr[-2:]; // start = max((-2 + 4), 0) = max(2, 0) = 2 // → ['c', 'd'] arr[-10:]; // start = max((-10 + 4), 0) = max(-6, 0) = 0 // → ['a', 'b', 'c', 'd'] arr[:-2]; // end = max((-2 + 4), 0) = max(2, 0) = 2 // → ['a', 'b'] arr[:-10]; // end = max((-10 + 4), 0) = max(-6, 0) = 0 // → [] arr[::-1]; // → ['d', 'c', 'b', 'a']
上限と下限は配列の長さで制限がある
const arr = ['a', 'b', 'c', 'd']; arr[100:]; // → [] arr[:100]; // → ['a', 'b', 'c', 'd']
- 投稿日:2019-10-09T18:00:37+09:00
食べログを便利にするChrome拡張機能を作った話
食べログの店舗ページにGoogleMapの口コミ評価点数を表示するChrome拡張機能を作りました!
こちらからどうぞ!
食べログ GoogleMap評価追加背景
多くの人はデートとか合コンとかの店を探す時に、食べログを使うと思います。食べログは店舗情報とか写真とかメニューとかデータが整理されているので便利なのですが、口コミがイマイチです。最近体感として、googlemapsの口コミが量も溜まってきていい感じなので、googlemapの口コミを食べログに表示しようと思います。ついでに、googlemapのリンクも貼って、口コミの詳細等を確認できるようにしました。
環境構築
Google Extension の開発環境を簡単にセットアップするライブラリが巷に転がってますが、導入するだけで半日かかってしまうので、今回は使いません。サクサク進めます。
もしテスト書きたい場合やファイルの変更を追従したい場合は下のようなライブラリを使うといいと思います。
- https://github.com/yeoman/generator-chrome-extension
- https://github.com/HaNdTriX/generator-chrome-extension-kickstart
さらに、拡張機能はすべてブラウザで動くのでローカルでは、nodeとかもなにもいりません。
なので、必要な環境はchromeとエディタだけです。実装
Chrome拡張機能
まずは、Chrome拡張機能を作るのに必ず必要な
manifest.json
から作っていきます。manifest.json{ "name": "食べログ GoogleMap評価追加", "version": "1.0.0", "manifest_version": 2, "description": "食べログの店舗ページにGoogleMapの評価を表示します。", "content_scripts": [{ "matches": ["https://tabelog.com/*"], "js": [ "content.js" ] }] }chrome拡張機能はcontent_scriptsとbackgroundとbrowser_actionの大きく3つに分かれています。
- content_scripts
ブラウザのフロントで動く部分。DOMの操作とかができる。- background
バックグラウンドで動く部分。chromeの色々なAPIが使える。- browser_action
右上のアイコンを押して出てくる小さいページ。html,cssを使って作る。今回はブラウザのフロントで食べログのデータを取得してそのままGoogle Maps APIを叩けばよいので、content_scriptsだけ使います。matchesはどのサイトでスクリプトを走らせるか。jsは実際に走らせるコードを指定します。
次にメインのコードの
content.js
です。content.jswindow.onload = function() { var displayNameElement = document.getElementsByClassName('display-name')[0] if (!displayNameElement) { return } var query = "name=" query += displayNameElement.children[0].innerText.trim(); var address = document.getElementsByClassName('rstinfo-table__address')[0] if (address) { query += "&address=" query += address.innerText } // XMLHttpRequestオブジェクトの作成 var request = new XMLHttpRequest(); // URLを開く var url = "https://xxxxxx.cloudfunctions.net/functions?" + query request.open('GET', url, true); // レスポンスが返ってきた時の処理を記述 request.onload = function() { var res = JSON.parse(this.response) if (!res.candidates[0]) { return } var query = res.candidates[0].name var queryPlaceId = res.candidates[0].place_id var rating = res.candidates[0].rating var ratingElement = document.getElementById('js-header-rating').children[0] ratingElement.insertAdjacentHTML('beforeend', ` <li class="rdheader-counts__item"> <span class="rdheader-rating__review"> <span class="rdheader-rating__review-target"> <span class="unit">Googleによる評価</span> <a property="" href="https://www.google.com/maps/search/?api=1&query=${query}&query_place_id=${queryPlaceId}" target="_blank"> <i>口コミ</i> <em class="num" property="v:count">${rating}</em> </a> </span> </span> </li> `); } // リクエストをURLに送信 request.send(); };食べログのページから店舗名と住所を拾ってきます。本来はこれをそのままGoogleMapsAPIに叩きに行けばいいのですが、セキュリティ上良くないので(CORSエラーが発生します)Cloud Functionsを噛ませます。
https://xxxxxx.cloudfunctions.net/functions
はCloud Functionsのurlです。GoogleMapsAPIからのレスポンスとして名前とplace_idと評価をとってきます。評価を画面に表示し、名前とplace_idはgoogleにリンクを貼るために使います。
https://www.google.com/maps/search/?api=1&query=${query}&query_place_id=${queryPlaceId}
みたいな感じのurlでお店ページへのリンクが貼れます。Cloud Functions
先程出てきた、カマセ役のCloud Functionsです。GCPのCloud Functionsを使用しています。受けたアクセスをそのままGoogleMapsAPIに送信しているだけです。Pythonで書いてますが、node.jsでも書けます。APIKEYはベタ書きですが、うまくやると環境変数で設定できた気がします。せっかくなので、GoogleMapsAPIはこちらの公式ライブラリを使います。
main.pyimport googlemaps import json import flask import requests def place_api(request): request_json = request.get_json() if request.args and 'name' in request.args: key = 'xxxxxxxxxxxxxx' client = googlemaps.Client(key) input_string="Restaurant+"+request.args.get('name') if 'address' in request.args: input_string+="+"+request.args.get('address') place_result = client.find_place( input=input_string, input_type='textquery', fields=['name','rating','place_id'] ) response = flask.jsonify(place_result) response.headers.set('Access-Control-Allow-Origin', '*') response.headers.set('Access-Control-Allow-Methods', 'GET, POST') return response else: return f'Set args!'requirements.txtgooglemaps>=3.1.3気になったこと
- GoogleMapsAPIはそこそこ高いので、破産しないように一日あたりの上限を設定します。月10000リクエストまで無料らしいので、一日333を上限にしました。
- 開発者向けにパッケージ化されてない拡張機能は無料で読み込めるのですが、ChromeWebStoreの登録は有料です。初回5$します。昼飯一回抜いたと思って支払いましょう。
- 最初名前だけでGoogleMapの情報をとってきたのですが、とくにアルファベットの名前のときに全然違う国の店がヒットすることがあるので、検索クエリに住所を付け足しました。また、GoogleMapは内部のデータの持ち方が微妙で、同じ店でも複数データが存在します。口コミとかが乗っているレストランのデータが欲しいので、クエリにRestaurantを付け足しました。
まとめ
いい感じにサクッとできたので個人的には満足です。ぜひ皆さん使って下さい。
- 投稿日:2019-10-09T17:39:38+09:00
Partial Application Syntax - TC39 Proposals
この記事は2019年10月9日に書いたものであり、#tc39_study TC39 Proposals LT 大会で使用したものです。
状況
- Stage : 1
- URL : https://github.com/tc39/proposal-partial-application
- Champion : Ron Buckton
- MSの人でTypeScript Compiler周りの人
提案内容
bindやarrow functionを使わなくても、
?
を使用して引数を固定しないようにできる。
Partial Application Syntaxだとこれが
function add(x, y) { return x + y; } // Function#bind const addOne = add.bind(null, 1); addOne(2); // 3 // arrow functions const addTen = x => add(x, 10); addTen(2); // 12 // arrow functions and pipeline const newScore = player.score |> _ => add(7, _) |> _ => clamp(0, 100, _);
こうなる
const addOne = add(1, ?); addOne(2); // 3 const addTen = add(?, 10); addTen(2); // 12 // with pipeline let newScore = player.score |> add(7, ?) |> clamp(0, 100, ?); // partial template strings const Diagnostics = { unexpected_token: `Unexpected token: ${?}`, name_not_found: `'${?}' not found.` }; Diagnostics.name_not_found("foo"); // "'foo' not found."
- bindだと先行引数のみしか引数を変更できない、thisを明示的に示す必要がある
- arrow functionはpipelineと組み合わせると複雑になる
Examples
// ○ f(x, ?) // partial application from left f(?, x) // partial application from right f(?, x, ?) // partial application for any arg o.f(x, ?) // partial application from left o.f(?, x) // partial application from right o.f(?, x, ?) // partial application for any arg super.f(?) // partial application allowed for call on |SuperProperty| // × f(x + ?) // `?` not in top-level Arguments of call x + ? // `?` not in top-level Arguments of call ?.f() // `?` not in top-level Arguments of call new f(?) // `?` not supported in `new` super(?) // `?` not supported in |SuperCall|
const log = console.log({ toString() { return `[${new Date()}]` } }, ?); log("test"); // [2018-07-17T23:25:36.984Z] testbutton.addEventListener("click", this.onClick(?));
class Collator { constructor() { this.compare = this.compare(?, ?); } compare(a, b) { ... } }// doWork expects a callback of `(err, value) => void` function doWork(callback) { ... } function onWorkCompleted(err, value, state) { ... } doWork(onWorkCompleted(?, ?, { key: "value" }));
const slice = Array.prototype.slice.call(?, ?, ?); slice({ 0: "a", 1: "b", length: 2 }, 1, 2); // ["b"]
// AST transformation const newNode = createFunctionExpression(oldNode.name, visitNodes(oldNode.parameters), visitNode(oldNode.body)) |> setOriginalNode(?, oldNode) |> setTextRange(?, oldNode.pos, oldNode.end) |> setEmitFlags(?, EmitFlags.NoComments);
Support for rest/spread (...)
これ便利そう
function f(a, b, c, d) { console.log(`a: ${a}, b: ${b}, c: ${c}, d: ${d}`); } const g = f(?, 1, ...); g(2); // a: 2, b: 1, c: undefined, d: undefined g(2, 3); // a: 2, b: 1, c: 3, d: undefined g(2, 3, 4); // a: 2, b: 1, c: 3, d: 4
- 投稿日:2019-10-09T17:31:32+09:00
Nested import declarations - TC39 Proposals
この記事は2019年10月9日に書いたものであり、#tc39_study TC39 Proposals LT 大会で使用したもの。
この機能はまだ未策定かつ変更の可能性が高い。
状況
- Stage : 0
- URL : https://github.com/benjamn/reify/blob/master/PROPOSAL.md
- Champion : Ben Newman
- この頃はFacebookやMozillaに居たかもしれないが、今はMeteorでApolloの開発者ぽい
提案内容
importをtop level以外でも使用できるようにするというもの。
関数内にネスト可能で、スコープは関数内に限定し、同期的宣言。if (condition) import "./sometimes"; if (condition) { import "./sometimes"; } if (condition) let foo = bar;
Dynamic importとの違い
非同期関数内のみで動作する。
async function main() { const hoge = await import('./hoge.js'); hoge() }
この
Dynamic import
はすでに現在EdgeやIE以外の主要ブラウザで使用できる。
Dynamic importとの関係
Dynamic importのproposalの「A new binding form」の部分を読むと記載があるが、2016年7月のTC39 meetingでNested import declarationsの話し合いがあり、そこで当時Stage1にいたDynamic importの前身?である
await import
の提案を待つように言われている。同期的なimportの提案なので、ブラウザやtop levelのimportとの関係性に問題があるという話などが行われていた。
先ほど記載したようにawait import
の提案を待つように言われており、本人は同期宣言を望んでいたが、どうやら叶わなかったらしい。
まだInactive Proposalsに入らず、Stage0に居るので、もしかしたら動くこともあるかもしれない。
最後に
このNested import declarationsを読む前はDynamic importがあり、Stage0から上がらなさそうなのに、なんで読もうとしているのだろうという気持ちだったが。
Dynamic importとの関係性や2016年の話し合いを知れて、おもしろかった。
- 投稿日:2019-10-09T17:21:06+09:00
【爆速】ブラウザからJavaScriptでWebGLシェーダーを使ってGPGPUする
GPGPUという単語や意味は何となくぼんやり理解していたつもりですが、どのような実装や開発環境だとかは全く知りませんでした。
しかしJavaScriptのWebGL経由でブラウザから今すぐ試せると知って、じゃあ理解を深める為にもやってみるかとなったのがきっかけで、ぼちぼち調べた事を書き綴ってみます。
謎の言語
WebGLについて調べていくうちJavaScriptのソレとは全然かけ離れた、謎の言語がちらほら出てきて早速詰みかけました?
GLSL
その正体は、GPUのシェーダーコアを制御するためのシェーディング言語 "GLSL" というもので、GPUプログラミングにおいての高レベル言語となっています。
エントリポイントが
void main()
だったり、最初に変数宣言を済ませる必要があったりとC言語ベースですが、独自の構文や拡張が多く入っています。GLSLの仲間たち
なお "GLSL" はOpenGL向けですが、他にもDirect3D向けの "HLSL" やNVIDIA系列向けの "Cg" など、各プラットフォーム向けにいくつかのシェーディング言語が存在します。
WebGLとOpenGLとANGLE
WebGLは、OpenGLのモバイル向けサブセットであるOpenGL ESを使用しています。
バージョン
初代WebGLはOpenGL ES 2.0で、モダンブラウザでぼちぼち浸透してきているWebGL2はOpenGL ES 3.0です。
なお2019年10月現在、Google Chromeでは試験的にWebGL2が拡張されてOpenGL ES 3.1にも対応しています。ちなみに、初代WebGLは驚くべき事に何と、あのInternetExplorerにも対応しています。
コンピュートシェーダーへの対応
今までは頂点シェーダーとピクセルシェーダーしか使用できませんでしたが、上述のWebGL2拡張によりChrome試験機能としてコンピュートシェーダーが使用できるようになりました。
ANGLE
ChromeにおけるWebGL実装は "ANGLE" と呼ばれる、OpenGL ESをフルバージョンのOpenGLやDirect3DやVulkanに変換するAPIへ渡ってから実行されます。
そして、コンピュートシェーダーはDirect3Dでいうと11の機能、フルバージョンOpenGLでいうと4.3の機能なので、使用にあたってはOS側でそれぞれ対応している必要があります。
OpenGLのコンピュートシェーダー
コンピュートシェーダーは他のシェーダーと何が違うのか、簡単にまとめてみます。
シェーダーの歴史
その昔、GPGPUなんていう言葉が生まれる前から存在していた頂点シェーダーとピクセルシェーダーは、当然ですが描画するために作られたので "数値入力→座標計算→色計算→結果出力" という一連の流れ(パイプライン)以上の機能は不要でした。
それが徐々に「GPUって単純な数値計算ならCPUでやるより爆速じゃね?」となり、描画以外の汎用的な計算処理も行われるようになりました。
しかし、汎用計算となると処理を描画パイプラインに当てはめる必要があり、データのやり取りにもっと自由度が欲しくなります。
そこで、従来の描画パイプラインから切り離され汎用計算に特化したコンピュートシェーダーが登場しました。
データのやり取りが自由に
コンピュートシェーダーには、シェーダー側のバッファとアプリケーション側のバッファをバインドしてデータを自由にやり取りできる "SSBO" という機能が追加されました。
SSBOは、アプリケーション側だけでなくシェーダー内部の別の領域ともバインドできます。
このように、コンピュートシェーダーは汎用計算を前提としており、そもそも描画を目的として作られた頂点シェーダーやピクセルシェーダーとは全く異なる役割を担っているシェーダーというわけです。
必要環境
- Google Chrome 77以上
- DirectX 11 (Windowsの場合)
- OpenGL 4.3 (Linuxの場合)
PCが各種グラフィックAPIに対応しているかは、Chromeページから確認できます。
Chromeでコンピュートシェーダーを試用するには、試験機能フラグを有効化する必要があります。
- chrome://flags
- ✔
--enable-webgl2-compute-context
これらの通り、WebGL2のコンピュートシェーダー機能はかなり初期段階の実装で、実行可能な環境はそれなりに限られてくると思いますので、その点はご留意ください。
早速書いてみる
gpgpu.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="height=device-height, width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> <title>GPGPU</title> </head> <body> </body> <script id="shader" type="x-shader/x-compute"> #version 310 es #define bufferSize 0x00100000 layout (local_size_x = 1024, local_size_y = 1, local_size_z = 1) in; layout (std430, binding = 0) buffer SSBO { float data[]; } ssbo; void main(){ int maxCount = bufferSize / int(gl_WorkGroupSize.x); for(int i = 0; i < maxCount; i++){ uint index = gl_GlobalInvocationID.x * uint(maxCount) + uint(i); ssbo.data[index] = ssbo.data[index] * 2.0; } } </script> <script> class GPGPU{ ctx; constructor(src){ this.ctx = new OffscreenCanvas(1, 1).getContext("webgl2-compute"); const program = this.ctx.createProgram(); const shader = this.ctx.createShader(this.ctx.COMPUTE_SHADER); this.ctx.shaderSource(shader, src); this.ctx.compileShader(shader); if(!this.ctx.getShaderParameter(shader, this.ctx.COMPILE_STATUS)){ console.log(this.ctx.getShaderInfoLog(shader)); return; } this.ctx.attachShader(program, shader); this.ctx.linkProgram(program); if(!this.ctx.getProgramParameter(program, this.ctx.LINK_STATUS)){ console.log(this.ctx.getProgramInfoLog(program)); return; } this.ctx.useProgram(program); } compute(data){ const input = new Float32Array(data); const output = new Float32Array(input.length); const ssbo = this.ctx.createBuffer(); this.ctx.bindBuffer(this.ctx.SHADER_STORAGE_BUFFER, ssbo); this.ctx.bufferData(this.ctx.SHADER_STORAGE_BUFFER, input, this.ctx.DYNAMIC_COPY); this.ctx.bindBufferBase(this.ctx.SHADER_STORAGE_BUFFER, 0, ssbo); this.ctx.dispatchCompute(1, 1, 1); this.ctx.getBufferSubData(this.ctx.SHADER_STORAGE_BUFFER, 0, output); return output; } } // サンプルデータを用意 const sample = [...new Array(0x00100000)].map(() => Math.random()); // GPUインスタンス const gpgpu = new GPGPU(document.getElementById("shader").textContent.trim()); // GPUで実行タイム計測 performance.mark("start_gpu"); gpgpu.compute(sample); performance.mark("end_gpu"); // CPUで実行タイム計測 performance.mark("start_cpu"); new Array(sample).map(n => n * 2.0); performance.mark("end_cpu"); performance.measure("exec_gpu", "start_gpu", "end_gpu"); performance.measure("exec_cpu", "start_cpu", "end_cpu"); console.log(`GPU: ${performance.getEntriesByName("exec_gpu")[0].duration} ms`); console.log(`CPU: ${performance.getEntriesByName("exec_cpu")[0].duration} ms`); </script> </html>この例では、1,048,576要素の全要素に
Math.random()
が入った配列を各要素ずつ2倍にする処理の実行時間を計測しています。GLSL側とJavaScript側との処理を追っていきます。
GLSL
GLSLソースコードは
<script>
内に書き込めます。MIMEをJavaScript以外(例では
x-shader/x-compute
)とかにしておけば、ブラウザ側で不要な解析をされずに済みます。なお、GLSLのお作法として
.a
.b
.c
や.x
.y
.z
など、最初のアルファベット1文字を起点として、そこから続いてn番目というプロパティ指定方法があります。?version
これもGLSLのお作法で、1行目は必ずOpenGLバーション指定で始まる必要があります。
今回はコンピュートシェーダーを使用するので
310 es
とします。なお、タグのインデントもStringデータとして読み込むと改行や空白の扱いとなってしまうので、JavaScriptで読み込む時は
trim()
で取ってあげると良いでしょう。?layout
色々な大きさだったり配置だったりを指定する構文です。
- layout(1個目)
local_size_x,y,z
で各次元ごとにワークグループ(スレッドの纏まり)の大きさを指定します。
だいたい128
くらいが妥当らしいです(適当)- layout(2個目)
SSBOを定義します。
std430
というのがバッファ領域のメモリレイアウトの規格らしく、他にもstd140
などがありました。
binding
で何番目のバインドかを指定します。その下の括弧内にバインドする配列を指定します。
SSBOは可変長配列を1つだけ持てますが、可変長配列の宣言は必ず固定長配列を全て宣言した後でなければいけません。GLSL-NG{ float data0[3]; float data1[]; float data2[8]; }GLSL-OK{ float data0[10]; float data1[5]; float data2[]; }?gl_GlobalInvocationID
割り当てられたスレッド番号のようなものです。
今回は1次元なので.x
で整数のスレッド番号を取得できます。?gl_WorkGroupSize
ワークグループの大きさです。
すなわちlocal_size_x,y,z
で指定した値となります。?forループ
基本的にはよくあるfor文なのですが、カウンタ変数は
int
(符号付整数型)で宣言する必要があります。
最初書いた時、カウンタ変数をuint
(符合無整数型)で宣言したらエラーでした。JavaScript
WebGLに関する部分は、見慣れないメソッドや定数が多く出現しますが、基本的にはお作法を踏襲しながら記述していくので、難しい事をしているわけではありません。
?Canvas
普通の
createElement()
でも生成できるのですが、今回はせっかくなので新しいCanvasを使ってみました。- OffscreenCanvas
DOMから切り離されディスプレイ表示が出来なくなった代わりに、WebWorkerで扱えるようになったCanvasオブジェクトです。
従来の
createElement()
で生成するCanvasオブジェクトはDOMなのでWebWorkerでは扱えませんでした。ディスプレイ表示以外は、DOM有り版と同一に扱えます。
OffscreenCanvasの利点としては、アニメーションやゲームなど描画オブジェクトが大量な場合、Workerで予め各描画を済ませておき、必要になったらメインスレッドへ描画済データを転送する事で、メインスレッドは表示だけで済むのでレスポンス低下を避けられる、といったところです。
今回は、特に何かを描画させるわけではなくシェーダーを使うためのコンテキストが欲しいだけなので、コンストラクタ引数
(width, height)
はそれぞれ1
で問題ありません。- getContext()
webgl2-compute
を指定する事で、コンピュートシェーダーのレンダリングコンテキストを取得できます。?WebGL(初期化)
基本的に処理結果を
return
で返すのでは無く、引数として受け取ったオブジェクトに直接変更を加えます。1: createProgram()
WebGLのプログラムオブジェクトを初期化生成します。
2: createShader()
シェーダーオブジェクトを初期化生成します。
3: shaderSource()
GLSLソースコードをシェーダーオブジェクトに追加します。
4: compileShader()
③でシェーダーオブジェクトに追加されたソースコードを、実行可能データにコンパイルします。
5: attachShader()
④でコンパイルされたシェーダーを、プログラムオブジェクトに追加します。
この段階ではプログラムオブジェクトにただ追加されただけで、プログラムから実行される事はありません。
6: linkProgram()
⑤でプログラムオブジェクトに追加されたシェーダーを、実行するシェーダーとしてリンクを生成します。
プログラムオブジェクトが実際にシェーダーを実行するためには、この段階を踏む必要があります。
7: useProgram()
WebGLレンダリングコンテキストにプログラムを適用します。
?WebGL(計算開始)
定数
SHADER_STORAGE_BUFFER
は "このバッファはSSBOである" ということを指しています。1: createBuffer()
バッファオブジェクトを初期化生成します。
2: bindBuffer()
バッファオブジェクトをバインドします。
3: bufferData()
バッファにデータ配列を書き込みます。
DYNAMIC_COPY
は、シェーダー側とアプリケーション側どちらからでも読み書き出来ることを示しています。配列型は、GLSLソースコードの
layout SSBO
で指定した型と合わせる必要があります。4: bindBufferBase()
GLSLソースコードの
layout SSBO
のバインド番号binding = n
にバッファオブジェクトをバインドします。5: dispatchCompute()
各次元のワークグループ数を指定して計算実行します。
例えば、以下の例の場合
GLSLlayout (local_size_x = 128) in;JavaScriptdispatchCompute(2, 1, 1);
x
次元は1ワークグループあたり128スレッドで、2ワークグループ分を実行するので256スレッドが実行される、といった感じです。6: getBufferSubData()
GLSLソースコードの
layout SSBO
のバインド番号binding = n
のバッファデータを、引数で渡した配列の長さ分だけ取得計算する
上記
gpgpu.html
を実行した時の処理時間を計測しました。計測は以下の環境で行いました。
- GPD Pocket 2 (m3-8100Y)
- Google Chrome 77.0CPU
処理時間: 1078.6 ms
まぁこんなもんですよね。
GPU
処理時間: 50.6 ms
驚異の速さ?
何とCPUの21.3倍もの差です。
GPGPUやばいな...適材適所
なお、計算するデータが少ない場合は、都度バッファを読み書きする手間などでオーバーヘッドが重なり、トータルで見るとCPUの方が高速になります。
使い分けが大事ですね。
おわりに
WebGLという単語は見たことあっても、全く関わりのない分野だと思っていました、今までは。
でもこうして実際に、書いてみて、動いて、そして圧倒的な速さを見せつけられると、何かワクワクしてしまいます?
GLSLについては存在自体を知って数日なので、まだまだこれから感はありますが、地道に色々試してみようと思います。
では最後はこれで。
「あ、ありのまま今起こったことを話すぜ!おれはJavaScriptを書いていたはずが、いつの間にか全く別の言語を書いていた...頭がどうにかなりそうだった...TypeScriptだとか、Nodeだとか、そんなチャチなものじゃあ断じてねぇ...もっと恐ろしいものの片鱗を味わったぜ...」
おわり(?)
Special Thanks!
- 投稿日:2019-10-09T16:51:10+09:00
Go の Web フレームワーク Echo で IT 用語クイズアプリを作ってみた
はじめに
近年、世間ではクイズが流行っています(?)が、IT 用語に特化したクイズのアプリってあんまりないような気がしたので作ってみました。
Go の勉強を最近始めたので、 echo でクイズのデータの JSON を返すだけの Web サーバをたてることにします。作ったもの
https://go-itquiz.herokuapp.com/
Github: https://github.com/chgzm/go-itquizサーバ側
Go そのものについては、始めて 3 日くらいなのでよくわかってませんが、echo の Guite を読めば大体問題なくできました。
- main 部分. heroku にデプロイするために、環境変数 PORT からポート番号を取得するようにする
main.gopackage main import ( "html/template" "io" "os" "github.com/chgzm/go-itquiz/data" "github.com/chgzm/go-itquiz/handler" "github.com/labstack/echo" ) type TemplateRenderer struct { templates *template.Template } func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { if viewContext, isMap := data.(map[string]interface{}); isMap { viewContext["reverse"] = c.Echo().Reverse } return t.templates.ExecuteTemplate(w, name, data) } func setRouting(e *echo.Echo) { e.GET("/", handler.HandleIndex) e.GET("/question", handler.HandleQuestion) e.POST("/data", handler.HandleData) } func setStatic(e *echo.Echo) { e.Static("/public/css", "./public/css/") e.Static("/public/js", "./public/js/") } func main() { e := echo.New() renderer := &TemplateRenderer{ templates: template.Must(template.ParseGlob("public/*.html")), } e.Renderer = renderer err := data.InitTable(e, "./data") if err != nil { e.Logger.Error(err) return } setRouting(e) setStatic(e) port := os.Getenv("PORT") if port == "" { port = "1323" } e.Logger.Fatal(e.Start(":" + port)) }
- ハンドラの処理. /data へのアクセスでランダムに 10 問選んで JSON 形式で返す
handler/handler.gopackage handler import ( "math/rand" "net/http" "time" "github.com/chgzm/go-itquiz/data" "github.com/labstack/echo" ) func HandleIndex(c echo.Context) error { return c.Render(http.StatusOK, "index.html", "") } func HandleQuestion(c echo.Context) error { return c.Render(http.StatusOK, "question.html", "") } func HandleData(c echo.Context) error { entries := data.GetAllEntries() extract := extractEntry(entries) return c.JSON(http.StatusOK, extract) } func extractEntry(entries []data.Entry) []data.Entry { extract := make([]data.Entry, 0, 10) selected := make(map[int]bool) rand.Seed(time.Now().UnixNano()) entLen := len(entries) i := 0 for { index := rand.Intn(entLen) if selected[index] == true { continue } selected[index] = true extract = append(extract, entries[index]) i++ if i == 10 { break } } return extract }
- テキストからクイズのデータを読み込んでメモリ上に保持する処理
data/table.gopackage data import ( "bufio" "io/ioutil" "os" "path/filepath" "strings" "github.com/labstack/echo" ) type Entry struct { ID int `json:"id"` Question string `json:"question"` Answer string `json:"answer"` Dummy1 string `json:"dummy1"` Dummy2 string `json:"dummy2"` Dummy3 string `json:"dummy3"` } type Tag struct { ID int Name string EntryIDs []int } var entryTable []Entry var tagTable []Tag func InitTable(e *echo.Echo, dir string) error { entryTable = make([]Entry, 0, 2048) tagTable = make([]Tag, 0, 64) // // init entry // ent := Entry{} tagIDMap := make(map[string]int) entryID := 0 tagID := 0 for _, path := range getFilePaths(dir) { f, err := os.Open(path) if err != nil { e.Logger.Error(err) return err } scanner := bufio.NewScanner(f) for scanner.Scan() { str := scanner.Text() if len(str) == 0 { continue } else if strings.HasPrefix(str, "##") { // add to TagTable tagNames := strings.Split(strings.TrimLeft(strings.TrimPrefix(str, "##"), " "), ",") for _, tagName := range tagNames { if id, ok := tagIDMap[tagName]; ok == false { tagIDMap[tagName] = tagID tag := Tag{} tag.ID = tagID tag.Name = tagName tag.EntryIDs = make([]int, 0, 256) tag.EntryIDs = append(tag.EntryIDs, entryID) tagTable = append(tagTable, tag) tagID++ } else { tag := tagTable[id] tag.EntryIDs = append(tag.EntryIDs, entryID) } } } else if str[0] == '#' { ent.Question = strings.TrimLeft(strings.TrimPrefix(str, "#"), " ") } else if str[0] == '1' { ent.Answer = strings.TrimLeft(strings.TrimPrefix(str, "1."), " ") } else if str[0] == '2' { ent.Dummy1 = strings.TrimLeft(strings.TrimPrefix(str, "2."), " ") } else if str[0] == '3' { ent.Dummy2 = strings.TrimLeft(strings.TrimPrefix(str, "3."), " ") } else if str[0] == '4' { ent.Dummy3 = strings.TrimLeft(strings.TrimPrefix(str, "4."), " ") // add to EntryTable ent.ID = entryID entryID++ entryTable = append(entryTable, ent) ent = Entry{} } } f.Close() } return nil } func GetAllEntries() []Entry { return entryTable } func GetEntries(tagID int) []Entry { entries := make([]Entry, 0, 1024) for _, entryID := range tagTable[tagID].EntryIDs { entries = append(entries, entryTable[entryID]) } return entries } func getFilePaths(dir string) []string { files, err := ioutil.ReadDir(dir) if err != nil { panic(err) } paths := make([]string, 0, 32) for _, file := range files { if file.IsDir() { paths = append(paths, getFilePaths(filepath.Join(dir, file.Name()))...) continue } if file.Name() == "question.md" { paths = append(paths, filepath.Join(dir, file.Name())) } } return paths }クライアント側
CSS や Javascript のフレームワークは使わず実装しました(使うほどの規模でもないので)。作ってる途中に気づきましたが、ドットインストール にクイズアプリ作るレッスンがあったので大いに参考にしました。
おわりに
- echo は簡単でよい。Go もよい感じ。
- 肝心のクイズを作るのが面倒なので誰か作って欲しい
- 投稿日:2019-10-09T15:59:09+09:00
<script async> で DOMContentLoaded が発火しないことがある
async属性をつけた
<script async>
は非同期で読み込まれるため、
ドキュメントの読み込みが完了した時点でscript
が読み込まれていないことがあるその場合、読み込みの状況次第では
DOMContentLoaded
が発火しないold.jsfunction main() { // 読み込み後に動かしたいコード } document.addEventListener("DOMContentLoaded", main, false);改善策として、ドキュメントの読み込み状態をチェックする
document.readyState を見れば、状況が分かるNEW.jsfunction main() { // 読み込み後に動かしたいコード } if (document.readyState !== "loading") { main(); } else { document.addEventListener("DOMContentLoaded", main, false); }
- 投稿日:2019-10-09T15:38:12+09:00
tc39/proposals String.prototype.replaceAll
注意事項
- これは#tc39_studyの発表資料です
- この機能はまだ提案/策定途中であり仕様に入るまでに大きな変更が入る可能性があり、またこの機能の策定自体が中止される可能性もあります。
ステータス
- proposal
- Champion
- Stage: 3 (2 to 3 per 2019.10.02 TC39)
- 実装済みのエンジン: なし
モチベーション
文字列の中の特定の文字をすべて置換したい
e.g.q=query+string+parameters
->q=query string parameters
StackOverflowで4000近くのvote、700以上のstarがついている
現状の解決策1
const queryString = 'q=query+string+parameters'; const withSpaces = queryString.replace(/\+/g, ' ');RegExpで意味を持つ記号はエスケープが必要
現状の解決策2
const queryString = 'q=query+string+parameters'; const withSpaces = queryString.split('+').join(' ');一度Arrayに変換してからStringに戻すのでオーバーヘッドが大きい
String.prototype.replaceAll
const queryString = 'q=query+string+parameters'; const withSpaces = queryString.replaceAll('+', ' ');
空文字を置換したらどうなる?
'x'.replace('', '_'); // -> '_x' 'xxx'.replace(/(?:)/g, '_'); // -> '_x_x_x_' 'xxx'.replaceAll('', '_'); // -> '_x_x_x_'結果としては
replace(/(?:)/g, replaceValue)
と同じになる
- 投稿日:2019-10-09T15:17:19+09:00
レストパラメータと分割代入で要らない仮引数を捨てる
不要な仮引数を捨てたい
たとえばこんな感じで、
Array.from( Array(3), (x, i) => i ); // x は関数内で使わない。コールバック関数などで、不要な仮引数を渡されるとき、どう書いてますか?
Array.from( Array(3), (_, i) => i );みたいな感じで、アンダースコアなどの変数名で受けて、使用しないことを示したりしますよね。
しかし、要らない仮引数が複数ある場合はどうでしょうか。
hoge( (_, __, ___, i) => i );同名の仮引数を定義することはできないので、アンダースコアが伸びていってしまったりしてかっこ悪いですよね。
あと、アンダースコアはlodashなどのライブラリとぶつかるので、安易に使いたくない場面もあるでしょう。レストパラメータと分割代入で仮引数を捨てる
そこで、レストパラメータと分割代入を使うとこのように書けます。
hoge( (...[ , , , i]) => i );不要の変数を登場させなくてよいので、その点でメリットがありますが、可読性の点でちょっと疑問がありますかね……?
参考
- 投稿日:2019-10-09T15:17:19+09:00
レストパラメータと分割代入で要らない引数を捨てる
不要な引数を捨てたい
たとえばこんな感じで、
Array.from( Array(3), (x, i) => i ); // x は関数内で使わない。コールバック関数などで、不要な引数を渡されるとき、どう書いてますか?
Array.from( Array(3), (_, i) => i );みたいな感じで、アンダースコアなどの変数名で受けて、使用しないことを示したりしますよね。
しかし、要らない引数が複数ある場合はどうでしょうか。
hoge( (_, __, ___, i) => i );同名の引数を定義することはできないので、アンダースコアが伸びていってしまったりしてかっこ悪いですよね。
あと、アンダースコアはlodashなどのライブラリとぶつかるので、安易に使いたくない場面もあるでしょう。レストパラメータと分割代入で引数を捨てる
そこで、レストパラメータと分割代入を使うとこのように書けます。
hoge( (...[ , , , i]) => i );不要の変数を登場させなくてよいので、その点でメリットがありますが、可読性の点でちょっと疑問がありますかね……?
参考
- 投稿日:2019-10-09T15:14:18+09:00
webpack4 で HTML & JS なライブリロード可能なローカルサーバを立てる
webpack 4 を最近触って覚えてきたので情報を自分用にまとめていく。
webpack 4 でローカルサーバを立てる必要があり (やりたかったのは SPA 用のフロント確認用ローカルサーバを立てる事)、それを色々やっていた。
要件は PJ 事情があり複雑なので、一旦シンプルな形でそれだけをやれる環境作りをまとめる。webpack 4 でローカルサーバ
webpack 4 には開発用サーバを立てる機能があり、それでフロント開発を行なう (最近は ローカル html を直で開いで JS や外部との疎通が出来ない状況になりつつあるので、必要だったりする)。
導入パッケージとしては、webpack, webpack-dev-server, html-webpack-plugin となる。
ちなみに webpack のローカルサーバ周りは色々周り道した結果 webpack-dev-server に返ってきたらしいので、また何かある可能性あるかも (node 周りそんなのばっかりな印象)。環境用意
yarn で使う環境をたてる (npm でセットアップは入れるものが分かれば置き換えるだけなので割愛)。
$ yarn add webpack --dev $ yarn add webpack-dev-server --dev $ yarn add html-webpack-plugin --devhtml-webpack-plugin は HTML を webpack が扱うために使うプラグインで、/public 以下に配置された html ファイルをビルド後ファイルの配置ディレクトリに移すために使用。
設定
下記設定を package.json に入れる。
package.json"scripts": { "server": "webpack-dev-server" }webpack.config.js の設定。
webpack.config.jsconst HtmlWebpackPlugin = require('html-webpack-plugin') const path = require('path') module.exports = { plugins: [ new HtmlWebpackPlugin({ // 移動させる html ファイルを指定 (このファイルに webpack でビルドされた JS や CSS が自動で設定される) template: './public/index.html' }) ], mode: 'development', // 例なので固定 entry: { // /src/index.js を index.html に読込設定する 'js/app': './src/index.js', }, // 出力設定 output: { // /dist に出力 path: path.resolve(__dirname, 'dist'), // 公開される際の BASE URL 指定 // ビルドされたソース等の読込設定とあっていないと読めないので注意 // 参考: https://webpack.js.org/configuration/output/#outputpublicpath publicPath: '/', // JS の出力指定 [name] がビルドした各ファイルの名前になる filename: 'js/[name].js' }, // ローカルサーバの設定 (webpack-dev-server 用設定) devServer: { // リロードの為のファイル監視 watchContentBase: true, // コンテンツの提供元ディレクトリを設定 // 単純に静的なページを確認したいだけなら output で出力先を設定せず、静的ファイルを置いているディレクトリを直で指定でもいい contentBase: path.resolve(__dirname, 'dist'), // ローカルサーバ立ち上げ時にブラウザで指定ページを開くかどうか (openPage で指定がない場合、index に指定したものか、ローカルサーバのトップを開く) open: true, // index ファイル (指定がない場合に標準で開くファイル) としてどれを扱うか index: 'index.html', // ビルドメッセージをブラウザコンソールに出すかどうか inline: true, // HMR (HotModuleReplacement) を有効にするかどうか // HMR とは: https://webpack.js.org/concepts/hot-module-replacement/ hot: true, // 全てのコンテンツを gzip 圧縮かけるか compress: true, // 実行中の進捗をコンソールに出すか progress: true, // ローカルサーバの立ちあげ Port 番号 (8080 はサンプルで良く使われるので、他で使ってる場合変更する) port: 8080 } }実行
- 一応 JS を含めた (Vue など VDOM を扱ったりする所まで使えそうな) 想定
- 完全静的で、HTML, CSS (Sass 含めた) の確認程度なら JS 周り (output, html-webpack-plugin 導入と設定) は不要で、 devServer 周りだけで良いし、publicPath が静的ページを配置したディレクトリを指定していれば問題ない)
$ yarn server
感想
最初調べ初めた時は業務で使う為、業務の要件にあわせて調査していた。
でも要件が複雑で、webpack を知らない人間が取りかかるには情報が無さすぎるため、一つずつバラして公式と戦うというオーソドックスな戦法だけが正義だった。webpack 周りは全然情報が無い上に 3 と 4 で想定がしれっと変わってたり戻ったりするので割と事情通じゃないと辛そうだった。
今は Vue で扱ってるので、正直 vue cli のビルドに載せる方が良かったんだろうと思いつつ、あまりビルドがそこを主軸にすると辛そうなイメージがあったので昨今のフロント事情を知るために webpack でやった。
React 界隈が何を使ってビルドするのが普通か分からないけど、多分 webpack で理解しておけばしばらくは大体戦える筈。他にも設定出来る項目は webpack-dev-server にはあるので、下記を参考に調整するので良いと思う (主要なところは大体書いた気はする)。
https://webpack.js.org/configuration/dev-server/
- 投稿日:2019-10-09T13:58:16+09:00
10分でわかる:Vue.jsとaxios を利用した API の使用-初心者向け
はじめに
皆さん、こんにちは!Webシステム開発エンジニアの蘭です!
今日は【Vue.jsとaxios を使って、APIからJson型データの取り扱い】について語りたいと思います。※Vue.jsについて知りたい方こちらも見てください。
10分で基礎がわからるVue.js-入門やりたいこと:【APIで高機能なシングルページアプリケーション(SPA)を構築】
現在APIはWebアプリの開発にはデータのやり取りとして主な窓口となってます。そこで今回、高機能なシングルページアプリケーション(SPA)を構築したいと思い、フロントではVue.jsとaxiosを使用します。
・シングルページアプリケーション(SPA)の構築-非同期
・APIでJson型データのやり取り
・Vueの単一ファイルコンポーネント(.vue)でhtml、css、Javascriptを単一ファイルで管理することで、後修正が便利。1.実行環境
・vue.js
・axios2.やってみよう!
開発の際npmかyarnでvueとaxiosをインストールします。
vue_npm_install.npm install vueaxios_npm_install.npm install --save-dev axios試しであれば、CDNで
・https://cdn.jsdelivr.net からVue.jsとaxiosを読み込みます。
vue_cdn.<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>axios_cdn.<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>基本的な例
・以下の内容はこちらを引用
今回はCoinDesk APIを利用して Bitcoin の価格を表示します。
まずAPIの中身を見てみよう。
mount
ライフサイクルフックを利用し、data
プロパティにaxiosで取得したJsonデータを格納します。See the Pen Vue__axios_API by Uramaya (@uramaya) on CodePen.
実例: データの操作
・API から取得したデータを表示
See the Pen Vue__axios_API2 by Uramaya (@uramaya) on CodePen.
・API から取得したデータから必要な情報だけを表示
filterを使用して小数点以下を2桁に整えます。
See the Pen
WNNvoqp by Uramaya (@uramaya)
on CodePen.
・エラー処理
APIの取得にエラーが発生した場合、理由を把握します。よくあるエラー原因:
・API がダウンしている。
・リクエストが間違っている。
・API は予期した形式で情報を渡さなかった。axios コールでは、 catch を使用してエラー原因を取得します。
See the Pen Vue__axios_API_Error by Uramaya (@uramaya) on CodePen.
axiosライブラリをもっと知る
現在REST APIでは以下の通信種類があります:
・GET (参照)
・POST (登録)
・PUT (更新)
・DELETE (削除)上記の例はGET (参照)ですが、登録や更新、削除の通信を行う場合は、以下の記事をご参考ください。
・axiosライブラリを使ってリクエストするおまけに
今回はaxiosを使いましたが、APIでJsonデータを取得する方法はaxiosだけではありません。他にVueではFetch APIというネイティブAPIがあります。
Fetch API
・利点:外部リソースを読み込む必要がない。
・欠点: polyfill を使用する必要があり、全てのブラウザーでサポートされてるとは限りません。(特にIE)
polyfill:JavaScriptのブラウザごとの互換性をポリフィル・Babelで解決する!上記の原因でaxiosを好む方も多いようですね。
See the Pen Vue__axios_API_Fetch by Uramaya (@uramaya) on CodePen.
まとめ
いかがでしょうか。
今回はVue.jsとaxiosの使用方法についての簡単な紹介をしました。
是非開発でも使ってみてください!:D
- 投稿日:2019-10-09T12:27:49+09:00
Javascriptのイベントハンドラにクラス定義したfunctionを登録するには
今更ですがJavascriptのクラスについて勉強中です。
フォーム値を変更した場合にページ遷移確認ウィンドウ出す処理を汎用的にクラス化しようとしました。
その中で、イベントハンドラにクラス内関数を指定したらインスタンス内のオブジェクトにthisでアクセスできなくなるためクラスに定義した関数が使えない問題が発生しました。
そこそこ上手く解決できたと思うので記事にします。最初に作ったクラス
TransitionObserver.jsclass TransitionObserver { constructor(){ this.InitValueMap = new Map(); this.id = Math.random().toString(36).slice(-16); } addPattern(elementPattern) { const map = new Map(); $(elementPattern).each(function (i, e) { const element = $(e); const valueObj = {}; valueObj.name = element.attr("name"); valueObj.val = element.val(); valueObj.checked = element.is(':checked'); map.set(element, valueObj); }) this.InitValueMap.set(elementPattern, map); } start() { $(window).on('beforeunload.pagetransition' + this.id, this.pageTransitionEvent); } stop() { $(window).off('beforeunload.pagetransition' + this.id); } hasUpdateElement() { for(let mapEntry of this.InitValueMap) { for(let valueEntry of mapEntry[1]) { const element = valueEntry[0]; const valueObj = valueEntry[1]; if (valueObj.val != element.val() || valueObj.checked != element.is(':checked')) { return true; } } } return false; } pageTransitionEvent(event) { if (this.hasUpdateElement()) { // ←このhasUpdateElementがundefinedになる event.preventDefault(); return false; } } }コメントに書きましたがイベントハンドラからコールバックしたpreTransitionEventからhasUpdateElementが見えません。
これはイベントハンドラから呼ばれたときにthisがTransitionObserverのインスタンスではなく別のオブジェクトになっているためです。
イベントハンドラのコールバックメソッドでthisを使うなという話は多くされているのですが、クラス定義したときにthisを使うなってのは無理な話なので何とかする必要があります。何とかした
TransitionObserver.jsclass TransitionObserver { start() { $(window).on('beforeunload.pagetransition' + this.id, this.pageTransitionEvent(this)); } pageTransitionEvent(me) { return function (event) { if (me.hasUpdateElement()) { event.preventDefault(); return false; } }; } // その他関数変更無し }そういえば関数のカリー化っていう手法で任意の引数を関数内で使えたよなっていう記事を読んだのを思い出して、上記のように実装。
無事に想定通りの動作をするようになりました。
関数をオブジェクトとして使えるのはやっぱり便利ですね。参考文献
3歳娘「パパ、関数をカリー化して?」
https://qiita.com/Yametaro/items/99cc1c8ebcfc703b1410JavaScriptでコールバック関数にあらかじめ引数を渡したい!
https://qiita.com/Lewuathe/items/5827a9b429aa71c4f76e未勉強な独り言:TypeScriptならこんなん気にせずできるんちゃう?知らんけど。
- 投稿日:2019-10-09T11:38:18+09:00
JavaScript if文の超基礎①
if( 条件① ){
条件①に合致した時の処理
}
else if(条件②)
{
条件②に合致した時の処理
}
else{
全ての条件に合致しなかった時の処理
}
- 投稿日:2019-10-09T10:39:26+09:00
JavaScriptで自販機を作る方法
JavaScriptで自動販売機のブログラムを作りたいのですが、以下をできるようなプログラムを作りたいです。
1.お金を投入
2.商品を選択
3.商品とお釣りを受け取る。
まだ、JavaScriptを始めたばかりなので分かる方いましたら、教えていただきたいです。
- 投稿日:2019-10-09T08:27:26+09:00
親要素でスクロールイベントが発火されない
以下のようなDOM構成、JavaScriptだとして、親要素でスクロールイベントが発火しませんでした。
でも何故かDocumentオブジェクトで登録したらスクロールイベントが発火しました。その間の要素で発火されない。1結論、そういう仕様です。MDNに書いてありました。
スクロールイベントはバブリングフェーズでは受け取れないため、
Documentオブジェクト間の要素は無視されます。1 親要素で無視されます。
どうしても親要素で発火したい、というのであればaddEventListener
の第3仮引数userCapture
にtrueを指定し、キャプチャーフェーズで発火させます。バブリングフェーズやチャプチャーフェーズについてはQiitaで @hosomichi さんが記事を書いています。以下で実際に挙動を確認出来ます。
こちらがスクロールイベント確認用。
https://murashi-sn.github.io/sample-javascript-api/event_propagation/02こちらが比較のためのクリックイベント確認用。
https://murashi-sn.github.io/sample-javascript-api/event_propagation/01<div id="parent"> <div id="child">SCROLL BAR</div> <div>document.querySelector('#child').addEventListener('scroll', function() { console.log('child event called.'); // 呼ばれる }); document.querySelector('#parent').addEventListener('scroll', function() { console.log('parrent event called.'); // 呼ばれない }); document.querySelector('#parent').addEventListener('scroll', function() { console.log('parrent event called.'); // これなら呼ばれる }, true); document.addEventListener('scroll', function() { console.log('document event called.'); // 呼ばれない });参考
- DOMイベントのキャプチャ/バブリングを整理する 〜 JSおくのほそ道 #017
- StackOverflow
- MDN web docs スクロールイベントについて
- MDN web docs useCapuereについて
確認方法が間違えていました。失礼しました。 ↩
- 投稿日:2019-10-09T01:45:36+09:00
勉強して150時間目の進捗
youtubeでKENTAさんが準備期間は大体150時間くらいでその後はどんどんポートフォリオ作っていった方が良いよってことで
自分がそろそろそれくらいなので作ってみることにしました。勉強した教材は、Progate、paizaラーニング、はじめてのふりがなプログラミングJS編、それぞれやってることは一緒ですが、教材によって説明の解釈や見せ方が違うので似た内容でも良い復習になりました。
とりあえずトップページを作ってみました。大体イメージ通りです。
もっとスライドとか使ってヌルヌル動かしたいんですが、お手本にしたいHPの検証モードが何書いてあるのか全然わからないので調べてできる範囲でなるべく似せて作って行こうと思います。トップページできたらそれぞれコンテンツページを作っていくんですが、HOME、YouTube、Twitterはリンクにするだけで完了
galleryとproductionの部分は作っていくんですが、galleryを見せ方によっては超かっこよくしたいですが、なんかサンプル素材がググったら沢山出てきて、コピペするだけでまんまパクれるかなって思ってやってみたんですが、なんか上手くいかないので、シンプルなので良いから形にはしていかないかんので簡単そうなものを色々試していってみることにします。
production(制作代行)の部分は注文フォームの見せ方をどうするか、ある程度お客さんが注文しやすいようにJSで作り込んでいく必要がありそうです、てゆうかここくらいでしかJSの出番がなさそうなので、これもできる範囲でやっていきます。僕の大好きなプラモデル制作もそうですが、細かい部分が全然綺麗に作れない、塗れないからって、何回もやり直していたらいつまでたっても完成しません、ある程度自分の実力と見切りをつけて、なるべくパーツの少ないキャラを選んで、荒くても良いからガツガツ作っていって次の作品でまた学べば良いかなと考えています。
今の営業の仕事はどんなけ売りまくって稼げないので、もうウンザリしてます。はよプログラム覚えて仕事の幅を広げていきたいです。
- 投稿日:2019-10-09T00:32:56+09:00
JavaとJavaScript(平均の求め方での違い)
単純ではありますがこちらもアウトプット
Java
import java.util.*; public class Test12{ public static void main(String[] args){ Scanner scan = new Scanner(System.in); int sum = 0; //合計を保存するための変数 int a=0; while (true) { System.out.println("数値を繰り返し入力して下さい(0で合計と平均をだして終了)"); //メッセージに終了条件を追加 int num = scan.nextInt(); if (num == 0) { break; } else { sum += num; a++;//おわるまで加算していく } } //System.out.println("合計は"+sum);// System.out.println("平均は"+sum/a); } }しっくり来るのはやはりJava。
JavaScript
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <title>jsave</title> </head> <body> <h1>平均の計算</h1> <input type="number" id="num" value="" placeholder="数値を入力"/> <button onclick="add()">Add</button> <button onclick="btCalc()">Calc!</button> <div id="msg"></div> <script> var nums=[]; var str=""; var eleNum=document.getElementById("num"); var eleMsg=document.getElementById("msg"); function add(){ //intに変換だがJavaと違いIntegerではない var num=parseInt(eleNum.value); nums.push(num); eleMsg.textContent=num+"を配列に追加"; } function btCalc(){ var sum=0; for(var i=0;i<nums.length;i++){ str +=nums[i]+(i==nums.length-1?"":","); sum +=nums[i]; } document.getElementById("data").textContent=str; var msg='[{${nums.join(",")}]の平均は${sum/nums.length}です。'; eleMsg.textContent=msg; } </script> </body> </html>とりあえずvarはブロックスコープ(let,const)に変えるべきなんでしょうな。
躓いた所で言えば
<script> 'use strict'; window.onload=function(){ //dom取得 let ele=document.getElementById("bt"); let result=document.getElementById("result"); //特定のイベントが対象に配信されるたびに呼び出される関数を設定します。 ele.addEventListener("click",function(){ result.textContent="Clicked!"; }); ele.addEventListener("click",function(){ window.alert("Clicked!!!"); }); }; </script>のようにHTMLを読み込んだ後にメソッドを実行?とか。。意味や意義を考えてたら理解が難しかったですね。。
- 投稿日:2019-10-09T00:27:12+09:00
JavaScript:数値から文字列に変換する
JavaScript を勉強することになったのでとりあえず FizzBuzz を書いてみることにしました。
FizzBuzz 関数内において、引数として受け取った値を Number 型から String 型に変換する必要が出てきた際に詰まりました。
そこで、Number 型から String 型への変換方法を調べてみた結果、色々見つかったので残しておくことにしました。結論を先に書いておく。
Number 型の値をただ文字列に変換したいだけならば、
String()
で変換するのが一番いいと思う。String(Number)
ただ単純に Number 型のオブジェクト(以下、Number)を String 型のオブジェクトに変換するみたいです。
恐らくこれが最もオーソドックスな方法なのかな?と感じます。NumToStr1.jsvar num = 100; var s = String(num); //=> "100"Number.toString(n)
このメソッドは Number を n 進数に変換した文字列を返すみたいです。
そのため、Number が 10 進数ならば、引数 n に 10 を指定すればそのまま Number を String 型に変換することができます。
また、引数に何も指定しない場合はn = 10
として Number が変換されます。NumToStr2.jsvar num = 255; var s1 = num.toString(10); //=> "255" var s2 = num.toString(16); //=> "ff" var s3 = num.toString(32); //=> "7v" var s4 = num.toString(); //=> "255"Number.toFixed(n)
このメソッドは Number の小数点以下の桁数を n 桁にした文字列を返すみたいです。
そのため、文字列に変換したい Number が整数ならば、引数に 0 を指定すればそのまま文字列に変換することができます。NumToStr3.jsvar num = 3.141592; var s1 = num.toFixed(0); //=> "3" var s2 = num.toFixed(6); //=> "3.141592" var s3 = num.toFixed(10); //=> "3.1415920000"その他
+ 演算子を文字列連結演算子として解釈させることで、文字列に変換する方法もあるみたいです。
NumToStr4.jsvar num = 100; var s = num + ""; //=> "100"参考
SOFTELメモ
MDN web docs mozilla : Number.prototype.toString()
MDN web docs mozilla : Number.prototype.toFixed()まとめ
Number をただ文字列に変換したいだけならば、
String()
で変換するのが一番よさそう。
値を n 進数に変換し、さらに文字列として扱いたいなら、Number.toString()
。
Number.toFixed()
はきっとピンポイントで使うときがあるんだろう(よくわからないけど)。
- 投稿日:2019-10-09T00:26:37+09:00
Node.js で相対パス地獄(../../../../)を絶対パスっぽく解決させる
いつもつらい思いをしていたけどそこまでクリティカルじゃなかったから見て見ぬ振りをしてきたけどそろそろ不味くなってきたので解決策はないかと探してみたら結構良さそうなのを見つけたのでメモ。
こんな感じのやつで$ tree src src ├── lib │ └── dep1 │ └── dep2 │ └── dep3 │ └── dep4 │ └── dep5 │ └── index.js └── pages └── dep1 └── dep2 └── dep3 └── dep4 └── dep5 └── index.js
こういうやつを// src/pages/dep1/dep2/dep3/dep4/dep5/index.js import hey from "../../../../../../lib/dep1/dep2/dep3/dep4/dep5";こうしたいimport hey from "heyapp/lib/dep1/dep2/dep3/dep4/dep5";下記 npm module を使う
https://github.com/Rush/link-module-alias$ npm i -D link-module-aliaspackage.jsonに下記を追加{ "_moduleAliases": { "heyapp": "src" }, }$ link-module-alias
するとこれで参照できるようになるimport hey from "heyapp/lib/dep1/dep2/dep3/dep4/dep5";仕組みとしては、node_modules の下に dir を切って、指定された path に対して
fs.symlink
で シンボリックリンクを張っているだけっぽい。万が一競合していたら error 吐いてくれたりしそうなのでそんなに危険じゃなさそう。
https://github.com/Rush/link-module-alias/blob/master/index.js#L187node_modules 配下を多少なりとも荒らすので、postinstall にこいつをフックさせるようにと READEME に書かれているので従っておこう。
package.json{ "scripts": { "postinstall": "link-module-alias" }, }使い方次第では結構便利な感じになりそうなので良さそう
package.json{ "_moduleAliases": { "heyapp": "src", "heyapp:lib": "otherdir/some/lib", "heyapp:test": "__test__" }, }