20191009のJavaScriptに関する記事は30件です。

【Rails/javascript(jQuery)】非同期での投稿削除機能

前回、編集機能の非同期化について記載させていただきましたが、今回は削除機能の非同期化について記事を書かせていただきます。

それでは、まず動画をご覧ください。
alt

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'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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();

最後に

コンストラクタの挙動は今後追記したい。

参考文献

オブジェクト指向JavaScriptの原則

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

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.proto
syntax = "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.ts
import { 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.ts
import * 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 response

GetMessageStreamを実行したウィンドウに戻ると、受信したMessageが表示されている。


{
  "text": "hello",
  "createTime": "1570468135968",
  "authorName": "aanrii"
}

proxyの準備・実行

現状のgRPC-Webの仕様だと、ブラウザから直接gRPCサーバに接続することはできず、プロキシを挟む必要がある (詳細) 。
ここでは、公式の例に倣って、envoyを利用する。まず、envoy.yamlに設定を記述する。

backend/proxy/envoy.yaml
admin:
  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/Dockerfile
FROM 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.tsx
import 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.tsx
import 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.tsx
import 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.tsx
import 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.tsx
import 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対応のための設定について紹介する。

参考文献

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

setInterval

setInterval

function hello(){ alert("hello")}


setInterval(hello, 5000)

※メモ
- 5000は5秒
- 公式: setInterval(関数, 時間)

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

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し忘れても大丈夫ですよ奥さん!!!

コンパイルできない

そう、できないんですよ、昔の記事ならね(全肯定)

Electronでアプリビルドまでのフロー

多分検索して一番最初にでてくるこの記事を見ると思います
新しい記事だし信用性は高いですね
この記事で紹介されてるbuild-win.jsは次の通り

build-win.js
const 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.js
const builder = require('electron-builder');

builder.build({
    config: {
        'appId': 'local.test.app1',
        'win':{
             'target': 'zip'
         }
    }
});

いやーすっきり

さいごに

electron-packagerは下位互換っぽいですね
配布するならelectron-builderを使おう

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

食べログからGoogleでの評価を簡単に確認できるChrome拡張をつくってみた

はじめに

食べログの評価点をお金で売っていたんじゃないかという疑惑が話題になっていますね。

自分も振り返ってみると、食べログで評価が高いのにイマイチだったなぁというお店もあれば、
食べログでは評価が低いけど行ってみたら大満足!ということもありました。

このような現状で、最近ではGoogleマップでのお店の評価を合わせて確認すると良い!という話をよく聞くようになりました。
身近なお店で検索してみると、そこそこ評価数もあり、情報も信頼できそうな感じです。
また訪日外国人のコメントなどもあり、よりグローバルな評価となっています。

ただいちいちGoogleで検索かけるのも面倒ですよね。
そこでワンクリックで検索結果のページに飛べるChrome拡張をつくってみました。

使い方

ss.png
ss2.png

ダウンロード/インストール

飲食店探しの達人 - 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を作った経験がある方でしたら楽勝だと思われます。

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

VueとCanvasでサクッと使えるルーレットアプリを作った

TL;DR

  • VueとCanvasでルーレットを作ったよ
  • ルーレットの各要素のウェイトを変えると確率を調整できるよ
  • レスポンシブなのでモバイルでも使えるよ

BanG Roullet!
demo_1

こだわりとか

カラーテーマ

バンドリの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/

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

【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 を使うとします。
tmpx のためにしか使わないので、適切にブロックで囲ってあげたくなります。
現時点だと以下のように即時関数を用いるのが簡単な方法でしょう。

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 {} (or async 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でもできるので、もう少し複雑なケースでの住み分けか。


参考情報

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

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>

JS
data: {
    date: new Date()
}

V-CalendarはDate型だと双方向バインディングできるのでDate型で作成

2.filterでmoment.js を用いて表示形式を変える

次に日付の表示部分

HTML
{{date | moment}}
JS
 filters: {
    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>
JS
methods: {
    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

https://blog.photosynthesic.jp/2018/10/vue-js%E3%81%A8%E3%81%AA%E3%81%8B%E3%82%88%E3%81%8F%E3%81%AA%E3%82%8D%E3%81%86%EF%BC%9A%E6%97%A5%E6%9C%AC%E6%99%82%E9%96%93%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%ABfilter%E3%82%92%E4%BD%BF%E3%81%A3/

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

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 }; // true

Iteration

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]; // true

Iteration

// 1
// 2
for (const o of tuple) { console.log(o); }

Equality

  • -0のケースは常にfalseNaNのケースは常にtrue (?)
  • 議論のissue
-0 === +0; // true
Object.is(-0, +0); //false

#{ a: -0 } === #{ a: +0 }; //false
#[-0] === #[+0]; //false
NaN === 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";
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Slice notation - TC39 Proposals

この記事は2019年10月9日に書いたものであり、#tc39_study TC39 Proposals LT 大会で使用したものです。


状況


提案内容

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']
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

食べログを便利にするChrome拡張機能を作った話

食べログの店舗ページにGoogleMapの口コミ評価点数を表示するChrome拡張機能を作りました!

image.png

こちらからどうぞ!
食べログ GoogleMap評価追加

背景

多くの人はデートとか合コンとかの店を探す時に、食べログを使うと思います。食べログは店舗情報とか写真とかメニューとかデータが整理されているので便利なのですが、口コミがイマイチです。最近体感として、googlemapsの口コミが量も溜まってきていい感じなので、googlemapの口コミを食べログに表示しようと思います。ついでに、googlemapのリンクも貼って、口コミの詳細等を確認できるようにしました。

環境構築

Google Extension の開発環境を簡単にセットアップするライブラリが巷に転がってますが、導入するだけで半日かかってしまうので、今回は使いません。サクサク進めます。
もしテスト書きたい場合やファイルの変更を追従したい場合は下のようなライブラリを使うといいと思います。

さらに、拡張機能はすべてブラウザで動くのでローカルでは、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.js
window.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.py
import 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.txt
googlemaps>=3.1.3

気になったこと

  • GoogleMapsAPIはそこそこ高いので、破産しないように一日あたりの上限を設定します。月10000リクエストまで無料らしいので、一日333を上限にしました。
  • 開発者向けにパッケージ化されてない拡張機能は無料で読み込めるのですが、ChromeWebStoreの登録は有料です。初回5$します。昼飯一回抜いたと思って支払いましょう。
  • 最初名前だけでGoogleMapの情報をとってきたのですが、とくにアルファベットの名前のときに全然違う国の店がヒットすることがあるので、検索クエリに住所を付け足しました。また、GoogleMapは内部のデータの持ち方が微妙で、同じ店でも複数データが存在します。口コミとかが乗っているレストランのデータが欲しいので、クエリにRestaurantを付け足しました。

まとめ

いい感じにサクッとできたので個人的には満足です。ぜひ皆さん使って下さい。

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

Partial Application Syntax - TC39 Proposals

この記事は2019年10月9日に書いたものであり、#tc39_study TC39 Proposals LT 大会で使用したものです。


状況


提案内容

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] test
button.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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nested import declarations - TC39 Proposals

この記事は2019年10月9日に書いたものであり、#tc39_study TC39 Proposals LT 大会で使用したもの。

この機能はまだ未策定かつ変更の可能性が高い。


状況


提案内容

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以外の主要ブラウザで使用できる。

スクリーンショット 2019-10-09 11.34.50.png


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年の話し合いを知れて、おもしろかった。

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

【爆速】ブラウザから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でコンピュートシェーダーを試用するには、試験機能フラグを有効化する必要があります。

これらの通り、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()

各次元のワークグループ数を指定して計算実行します。

例えば、以下の例の場合

GLSL
layout (local_size_x = 128) in;
JavaScript
dispatchCompute(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.0

CPU

処理時間: 1078.6 ms

まぁこんなもんですよね。

GPU

処理時間: 50.6 ms

驚異の速さ?
何とCPUの21.3倍もの差です。
GPGPUやばいな...

適材適所

なお、計算するデータが少ない場合は、都度バッファを読み書きする手間などでオーバーヘッドが重なり、トータルで見るとCPUの方が高速になります。

使い分けが大事ですね。

おわりに

WebGLという単語は見たことあっても、全く関わりのない分野だと思っていました、今までは。

でもこうして実際に、書いてみて、動いて、そして圧倒的な速さを見せつけられると、何かワクワクしてしまいます?

GLSLについては存在自体を知って数日なので、まだまだこれから感はありますが、地道に色々試してみようと思います。

では最後はこれで。

「あ、ありのまま今起こったことを話すぜ!おれはJavaScriptを書いていたはずが、いつの間にか全く別の言語を書いていた...頭がどうにかなりそうだった...TypeScriptだとか、Nodeだとか、そんなチャチなものじゃあ断じてねぇ...もっと恐ろしいものの片鱗を味わったぜ...」

おわり(?)

Special Thanks!

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

Go の Web フレームワーク Echo で IT 用語クイズアプリを作ってみた

はじめに

近年、世間ではクイズが流行っています(?)が、IT 用語に特化したクイズのアプリってあんまりないような気がしたので作ってみました。
Go の勉強を最近始めたので、 echo でクイズのデータの JSON を返すだけの Web サーバをたてることにします。

作ったもの

https://go-itquiz.herokuapp.com/
Github: https://github.com/chgzm/go-itquiz

  • 動作イメージ itquiz-screenshot.png

サーバ側

Go そのものについては、始めて 3 日くらいなのでよくわかってませんが、echo の Guite を読めば大体問題なくできました。

  • main 部分. heroku にデプロイするために、環境変数 PORT からポート番号を取得するようにする
main.go
package 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.go
package 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.go
package 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 のフレームワークは使わず実装しました(使うほどの規模でもないので)。作ってる途中に気づきましたが、ドットインストール にクイズアプリ作るレッスンがあったので大いに参考にしました。

おわりに

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

<script async> で DOMContentLoaded が発火しないことがある

async属性をつけた <script async> は非同期で読み込まれるため、
ドキュメントの読み込みが完了した時点で script が読み込まれていないことがある

その場合、読み込みの状況次第では DOMContentLoaded が発火しない

old.js
function main() {
  // 読み込み後に動かしたいコード
}

document.addEventListener("DOMContentLoaded", main, false);

改善策として、ドキュメントの読み込み状態をチェックする
document.readyState を見れば、状況が分かる

NEW.js
function main() {
  // 読み込み後に動かしたいコード
}

if (document.readyState !== "loading") {
  main();
} else {
  document.addEventListener("DOMContentLoaded", main, false);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

tc39/proposals String.prototype.replaceAll

注意事項

  • これは#tc39_studyの発表資料です
  • この機能はまだ提案/策定途中であり仕様に入るまでに大きな変更が入る可能性があり、またこの機能の策定自体が中止される可能性もあります。

ステータス


モチベーション

文字列の中の特定の文字をすべて置換したい
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)と同じになる

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

レストパラメータと分割代入で要らない仮引数を捨てる

不要な仮引数を捨てたい

たとえばこんな感じで、

Array.from( Array(3), (x, i) => i ); // x は関数内で使わない。

コールバック関数などで、不要な仮引数を渡されるとき、どう書いてますか?

Array.from( Array(3), (_, i) => i );

みたいな感じで、アンダースコアなどの変数名で受けて、使用しないことを示したりしますよね。

しかし、要らない仮引数が複数ある場合はどうでしょうか。

hoge( (_, __, ___, i) => i );

同名の仮引数を定義することはできないので、アンダースコアが伸びていってしまったりしてかっこ悪いですよね。
あと、アンダースコアはlodashなどのライブラリとぶつかるので、安易に使いたくない場面もあるでしょう。

レストパラメータと分割代入で仮引数を捨てる

そこで、レストパラメータと分割代入を使うとこのように書けます。

hoge( (...[ , , , i]) => i );

不要の変数を登場させなくてよいので、その点でメリットがありますが、可読性の点でちょっと疑問がありますかね……?

参考

Rest parameters - JavaScript | MDN
分割代入 - JavaScript | MDN

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

レストパラメータと分割代入で要らない引数を捨てる

不要な引数を捨てたい

たとえばこんな感じで、

Array.from( Array(3), (x, i) => i ); // x は関数内で使わない。

コールバック関数などで、不要な引数を渡されるとき、どう書いてますか?

Array.from( Array(3), (_, i) => i );

みたいな感じで、アンダースコアなどの変数名で受けて、使用しないことを示したりしますよね。

しかし、要らない引数が複数ある場合はどうでしょうか。

hoge( (_, __, ___, i) => i );

同名の引数を定義することはできないので、アンダースコアが伸びていってしまったりしてかっこ悪いですよね。
あと、アンダースコアはlodashなどのライブラリとぶつかるので、安易に使いたくない場面もあるでしょう。

レストパラメータと分割代入で引数を捨てる

そこで、レストパラメータと分割代入を使うとこのように書けます。

hoge( (...[ , , , i]) => i );

不要の変数を登場させなくてよいので、その点でメリットがありますが、可読性の点でちょっと疑問がありますかね……?

参考

Rest parameters - JavaScript | MDN
分割代入 - JavaScript | MDN

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

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 --dev

html-webpack-plugin は HTML を webpack が扱うために使うプラグインで、/public 以下に配置された html ファイルをビルド後ファイルの配置ディレクトリに移すために使用。

設定

下記設定を package.json に入れる。

package.json
  "scripts": {
    "server": "webpack-dev-server"
  }

webpack.config.js の設定。

webpack.config.js
const 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/

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

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
・axios

2.やってみよう!

開発の際npmかyarnでvueとaxiosをインストールします。
vue_npm_install.
npm install vue
axios_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

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

Javascriptのイベントハンドラにクラス定義したfunctionを登録するには

今更ですがJavascriptのクラスについて勉強中です。

フォーム値を変更した場合にページ遷移確認ウィンドウ出す処理を汎用的にクラス化しようとしました。
その中で、イベントハンドラにクラス内関数を指定したらインスタンス内のオブジェクトにthisでアクセスできなくなるためクラスに定義した関数が使えない問題が発生しました。
そこそこ上手く解決できたと思うので記事にします。

最初に作ったクラス

TransitionObserver.js
class 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.js
class 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/99cc1c8ebcfc703b1410

JavaScriptでコールバック関数にあらかじめ引数を渡したい!
https://qiita.com/Lewuathe/items/5827a9b429aa71c4f76e

未勉強な独り言:TypeScriptならこんなん気にせずできるんちゃう?知らんけど。

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

JavaScript if文の超基礎①

if( 条件① ){
条件①に合致した時の処理
}
else if(条件②)
{
条件②に合致した時の処理
}
else{
全ての条件に合致しなかった時の処理
}

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

JavaScriptで自販機を作る方法

JavaScriptで自動販売機のブログラムを作りたいのですが、以下をできるようなプログラムを作りたいです。
1.お金を投入
2.商品を選択
3.商品とお釣りを受け取る。
まだ、JavaScriptを始めたばかりなので分かる方いましたら、教えていただきたいです。

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

親要素でスクロールイベントが発火されない

以下のような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.'); // 呼ばれない
});

参考


  1. 確認方法が間違えていました。失礼しました。 

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

勉強して150時間目の進捗

youtubeでKENTAさんが準備期間は大体150時間くらいでその後はどんどんポートフォリオ作っていった方が良いよってことで
自分がそろそろそれくらいなので作ってみることにしました。

スクリーンショット 2019-10-07 23.38.48.png

勉強した教材は、Progate、paizaラーニング、はじめてのふりがなプログラミングJS編、それぞれやってることは一緒ですが、教材によって説明の解釈や見せ方が違うので似た内容でも良い復習になりました。

とりあえずトップページを作ってみました。大体イメージ通りです。
もっとスライドとか使ってヌルヌル動かしたいんですが、お手本にしたいHPの検証モードが何書いてあるのか全然わからないので調べてできる範囲でなるべく似せて作って行こうと思います。

トップページできたらそれぞれコンテンツページを作っていくんですが、HOME、YouTube、Twitterはリンクにするだけで完了
galleryとproductionの部分は作っていくんですが、galleryを見せ方によっては超かっこよくしたいですが、なんかサンプル素材がググったら沢山出てきて、コピペするだけでまんまパクれるかなって思ってやってみたんですが、なんか上手くいかないので、シンプルなので良いから形にはしていかないかんので簡単そうなものを色々試していってみることにします。
production(制作代行)の部分は注文フォームの見せ方をどうするか、ある程度お客さんが注文しやすいようにJSで作り込んでいく必要がありそうです、てゆうかここくらいでしかJSの出番がなさそうなので、これもできる範囲でやっていきます。

僕の大好きなプラモデル制作もそうですが、細かい部分が全然綺麗に作れない、塗れないからって、何回もやり直していたらいつまでたっても完成しません、ある程度自分の実力と見切りをつけて、なるべくパーツの少ないキャラを選んで、荒くても良いからガツガツ作っていって次の作品でまた学べば良いかなと考えています。
今の営業の仕事はどんなけ売りまくって稼げないので、もうウンザリしてます。はよプログラム覚えて仕事の幅を広げていきたいです。

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

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を読み込んだ後にメソッドを実行?とか。。意味や意義を考えてたら理解が難しかったですね。。

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

JavaScript:数値から文字列に変換する

JavaScript を勉強することになったのでとりあえず FizzBuzz を書いてみることにしました。
FizzBuzz 関数内において、引数として受け取った値を Number 型から String 型に変換する必要が出てきた際に詰まりました。
そこで、Number 型から String 型への変換方法を調べてみた結果、色々見つかったので残しておくことにしました。

結論を先に書いておく。

Number 型の値をただ文字列に変換したいだけならば、String() で変換するのが一番いいと思う。

String(Number)

ただ単純に Number 型のオブジェクト(以下、Number)を String 型のオブジェクトに変換するみたいです。
恐らくこれが最もオーソドックスな方法なのかな?と感じます。

NumToStr1.js
var num = 100;
var s = String(num); //=> "100" 

Number.toString(n)

このメソッドは Number を n 進数に変換した文字列を返すみたいです。
そのため、Number が 10 進数ならば、引数 n に 10 を指定すればそのまま Number を String 型に変換することができます。
また、引数に何も指定しない場合は n = 10 として Number が変換されます。

NumToStr2.js
var 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.js
var num = 3.141592;
var s1 = num.toFixed(0); //=> "3"
var s2 = num.toFixed(6); //=> "3.141592"
var s3 = num.toFixed(10); //=> "3.1415920000"

その他

+ 演算子を文字列連結演算子として解釈させることで、文字列に変換する方法もあるみたいです。

NumToStr4.js
var 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()はきっとピンポイントで使うときがあるんだろう(よくわからないけど)。

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

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-alias
package.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#L187

node_modules 配下を多少なりとも荒らすので、postinstall にこいつをフックさせるようにと READEME に書かれているので従っておこう。

package.json
{
  "scripts": {
    "postinstall": "link-module-alias"
  },
}

使い方次第では結構便利な感じになりそうなので良さそう

package.json
{
  "_moduleAliases": {
    "heyapp": "src",
    "heyapp:lib": "otherdir/some/lib",
    "heyapp:test": "__test__"
  },
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む