20200104のJavaScriptに関する記事は26件です。

JavaScriptのmap()メソッドについて

はじめに

現在、JavaScript、jQueryを使って簡単な検索予測機能をつくっています。
その際、新しい配列の要素を作るために.map()メソッドを使用したのですが、引っかかった部分がありましたので、記事にしてみました。

疑問点

複数のワードに対して検索予測をかけるために
ここでは、inputタグにて入力された値を配列に入れ、正規表現に変換することまでを目的にしています。

//HTMLの<input>タグで下記のようにid="keyword"が与えられている状態
//<input type='text' id="keyword" class="form-control">

//配列inputsの要素に対して、入力された文字を先頭として始まる文字列を表現する
//ために"^"を付け足すための関数
  function editElement(element) {
      let result = "^" + element;
      return result;
    }

  //①入力された文字列をval()で受け取る。
  let input = $("#keyword").val();
  //②split("")で配列としてinputsに納める。
  let inputs = input.split(" ")
  //③inputsの要素に対して正規表現を加えるために、コールバック関数editElementを
  //呼び出す。
  let newInputs = inputs.map(editElement); //←ここで疑問点
  //④.join("|")でRegexpで使える形に変換し変数regに代入する
  let reg = new RegExp(newInputs.join("|"));

.map()によってeditElementが呼び出された際、実引数として何も指定されていないのに仮引数に(element)が使われ、配列の要素の値が変換されています。本来なら実引数として、配列の各要素を指定しななければいけないのではないかと疑問が湧いたので.map()について調べてみました。

調べた結果

まずリファレンスにある、.map()メソッドの構文を見てみると

array.map(function(currentValue, index, arr), thisValue)

となっています。
.map()の引数として使われるコールバック関数はcurrentValue(required), index(option), arr(option)という三つの引数をとり、currentValueの部分には配列の要素の値が渡されます。その値がコールバック関数で引数として使用されます。

これを踏まえて、元のコードに戻って確認してみると
inputs.map(editElement)で呼び出された
function editElement(element)には引数として、配列inpusの各要素の値が渡されていることがわかります。

要するに、.map()メソッドは「与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成する」ためのメソッドなので、こちらから引数を指定しなくても、自動で配列の各要素をコールバック関数に渡してくれるようです。

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

Apollo(GraphQL)+gRPC+WebFlux(Reactive)を利用して2020年のモダンアーキテクチャアプリを作ってみた

はじめに

本記事では2019年から2020年にかけての年末年始を使って開発した、"Hello Worldを出力するアプリ"の話をしたいと思います。

Hello Worldを出力する、と言っても単にフロントエンドでReactの<p>Hello, World!</p>と書いて出力したという話ではなく、

  1. フロントエンドでApollo Clientを使ってGraphQLでQuery/Mutationのリクエストを行い、
  2. BFF(Backend For Frontend)のApollo ServerでGraphQLのリクエストに応じたgRPCのリクエストをバックエンドへ内部的に行い、
  3. バックエンドで受け取ったgRPCリクエストに応じた処理(追加/削除)をWebFluxでリアクティブに実行し、逐次レスポンスを返却、
  4. BFFでgRPCのリクエストをGraphQLのレスポンスとして整形し、
  5. フロントエンドでGraphQLのレスポンスを用いて画面の表示、更新を行う

という内部的な処理を実行した上でHello Worldを出力するものを作りました。

早い話がオーバーエンジニアリングしたHello Worldアプリケーションですね。

アーキテクチャ図は大体下記のような感じになっています(外部から引用しているので図に記載されているKubernetes等はまだ利用できていません)。


参考: Medium When GraphQL meets gRPC… ?

モチベーションとしては2020年の年明けから上記技術を業務で使いそうな雰囲気になっているので、その予習としてこれらの技術を使ってHello Worldを出力するアプリを作ってみた、という感じになっています。

なお年末年始の時間の都合上クラウド上へデプロイすることは出来ませんでしたが、ローカルでDocker Composeを使ってコンテナとして各アプリを立ち上げ、ブラウザ上から操作できる、というところまで開発しています。

実際のアプリはこちらのリポジトリ から見ていただくことが可能です。

本記事ではこちらのアプリケーションの各レイヤについての詳細な技術スタックについて、バックエンド、BFF、フロントエンドの順に解説していきます。

GraphQLとgRPCを利用したアーキテクチャの導入の参考にしていただければと思います。

Backend(gRPCサーバー)

本アプリにおけるバックエンドの役割は、gRPCクライアントからのgRPCリクエストを受け取り、対応したCRUD処理を実行しレスポンスを返却することです。

なお今回のアプリケーションにおけるgRPCクライアントは、後述するBFFがこれに当たります。

gRPCサーバーの言語はKotlin、フレームワークはSpring Bootにしたかったので、リアクティブgRPCサービスを実現するために下記のdependenciesおよびpluginsを利用しました。
なおデータストアとしてはMySQLを利用しています。
バックエンドでは下記が主な技術スタックとなっています。

  • WebFlux
  • gRPC + Reactive gRPC
  • Armeria
  • Jib

WebFlux

WebFluxはSpringアプリケーションをリアクティブにするフレームワークです。

WebFluxを利用することでノンブロッキングな処理が可能となります。

一般的なブロッキング処理の場合は単一のリクエストの処理が完了するまでスレッドを占有するため、効率的にリクエストを捌くことが難しくなりますが、ノンブロッキングであればスレッドを複数のリクエストに対して利用でき、効率的にリクエストを処理することが可能になります
WebFluxを利用するためにはdependenciesにスターターを追加するだけでOKです。

build.gradle.kts
  dependencies {
    (...)
    implementation("org.springframework.boot:spring-boot-starter-webflux")
  }

WebFluxを利用するには、リクエストとレスポンスをMono<T>あるいはFlux<T>という型でラッピングする必要があります。

MonoはStringなどの単一の型を、FluxはList<String>などの連続する値のレスポンスに用います(なお正確にはMonoとFluxはReactorというリアクティブライブラリが提供しています)。

参考として、典型的なREST Controllerをリアクティブにすると、下記のようなコードになります。
なお本アプリでは実際には次に述べるgRPCのStubを利用してリクエストを捌くことになります。

GreetingController.kt
  @RestController
  @RequestMapping("/greetings")
  class GreetingController(
    private val service: GreetingService
  ) {

    @GetMapping("/{id}")
    fun getGreeting(@PathVariable("id") id: Int): Mono<Greeting> =
      Mono.just(service.getGreeting(id).get())

    @GetMapping
    fun getHelloWorldList(): Flux<Greeting> = Flux.fromIterable(service.getGreetingList())

    @PostMapping
    fun saveHelloWorld(@RequestParam("message") message: String): Mono<Greeting> =
      Mono.just(service.saveGreeting(message))
  }

Further Reading

gRPC + Reactive gRPC

WebFluxでアプリケーションをリアクティブにできた後は、次にgRPCを利用できるようにする必要があります。

dependenciesに下記を追加することでgRPCに対応できます。

build.gradle.kts
dependencies {
  (...)
  implementation("io.grpc:grpc-netty-shaded:1.26.0")
  implementation("io.grpc:grpc-protobuf:1.26.0")
  implementation("io.grpc:grpc-stub:1.26.0")
}

次にgRPCサービスを構築するため、protoファイルを作成し、サービスとメッセージを定義します。
実際には下記のように定義します。
こちらは一般的な定義ですね。

greeting.proto
  syntax = "proto3";

  package example.grpc.helloworld;

  option java_package = "com.example.helloworld.grpc.greeting";
  option java_multiple_files = true;

  service GreetingService {
    rpc SaveGreeting (SaveGreetingRequest) returns (SaveGreetingResponse) {
    }

    rpc GetGreeting (GetGreetingRequest) returns (GetGreetingResponse) {
    }

    rpc GetGreetings (GetGreetingsRequest) returns (GetGreetingsResponse) {
    }
  }

  message Greeting {
    int32 id = 1;
    string message = 2;
  }

  message SaveGreetingRequest {
    string message = 1;
  }

  message SaveGreetingResponse {
    Greeting greeting = 1;
  }

  message GetGreetingRequest {
    int32 id = 1;
  }

  message GetGreetingResponse {
    Greeting greeting = 1;
  }

  message GetGreetingsRequest {
  }

  message GetGreetingsResponse {
    repeated Greeting greetings = 1;
  }

また、gRPCサービスをリアクティブに動作させるためには、Reacive gRPCというDependencyも必要です。
gradleで下記の記述を行うことで、gradleのtaskとしてjavaクラスを生成することが可能になり、かつ生成されるjavaクラスがリアクティブなものになります。

build.gradle.kts
  protobuf {
    protoc { artifact = "com.google.protobuf:protoc:3.11.0" }
    plugins {
      id("grpc") {
        artifact = "io.grpc:protoc-gen-grpc-java:1.26.0"
      }
      id("reactorGrpc") {
        artifact = "com.salesforce.servicelibs:reactor-grpc:1.0.0"
      }
    }
    generateProtoTasks {
      ofSourceSet("main").forEach {
        it.plugins {
          id("grpc")
          id("reactorGrpc")
        }
      }
    }
  }

下記を記述の上、gradleでgenerateProtoタスクを実行すると、javaクラスが生成されます。

次にgRPCのサービスのロジックを生成する必要があります。
一例として下記のようなコードでサービスのロジックを記述することができます。

GreetingService.kt(Reactive)
  class GreetingGrpcService(
    private val repository: GreetingRepository
  ) : ReactorGreetingServiceGrpc.GreetingServiceImplBase() {
    override fun getGreeting(request: Mono<GetGreetingRequest>): Mono<GetGreetingResponse> =
      request.map {
        val greeting = repository.findById(it.id)
          if (greeting.isPresent) {
            GetGreetingResponse.newBuilder()
              .setGreeting(greeting.let {
                com.example.helloworld.grpc.greeting.Greeting
                  .newBuilder()
                  .setMessage(it.get().message)
                  .setId(it.get().id)
                  .build()
                })
                .build()
          } else {
            GetGreetingResponse.getDefaultInstance()
          }
        }

    override fun getGreetings(request: Mono<GetGreetingsRequest>): Mono<GetGreetingsResponse> =
      request.map {
        val greetings = repository.findAll().map {
          com.example.helloworld.grpc.greeting.Greeting
            .newBuilder()
            .setMessage(it.message)
            .setId(it.id)
            .build()
          }
          GetGreetingsResponse
            .newBuilder()
            .addAllGreetings(greetings)
            .build()
        }

    override fun saveGreeting(request: Mono<SaveGreetingRequest>): Mono<SaveGreetingResponse> =
      request.map {
        val greeting = repository.save(Greeting(it.message)).let {
          com.example.helloworld.grpc.greeting.Greeting
            .newBuilder()
            .setMessage(it.message)
            .setId(it.id)
            .build()
          }
          SaveGreetingResponse
            .newBuilder()
            .setGreeting(greeting)
            .build()
        }
  }

Reactive gRPCを利用した時に特徴的なのが、実装するgRPCのサービス(Stub)がReactiveなリクエストとレスポンスを利用する点です。

今回はMonoのみを使っていますが、gRPCのStreamを利用するとFluxを使うことになるはずです。

比較としてReactiveではない場合のコードも記載しておきます。

GreetingService.kt(non-Reacitve)
  class GreetingGrpcService(
    private val repository: GreetingRepository
  ) : GreetingServiceGrpc.GreetingServiceImplBase() {

    override fun getGreeting(request: GetGreetingRequest?, responseObserver: StreamObserver<GetGreetingResponse>?) {
      val id = request?.id ?: -1
      val greeting = repository
        .findById(id)
      val response = if (greeting.isPresent) {
        GetGreetingResponse.newBuilder()
          .setGreeting(greeting.let {
            com.example.helloworld.grpc.greeting.Greeting
              .newBuilder()
              .setMessage(it.get().message)
              .setId(it.get().id)
              .build()
            })
            .build()
      } else {
        GetGreetingResponse.getDefaultInstance()
      }
      responseObserver?.onNext(response)
      responseObserver?.onCompleted()
    }

    override fun getGreetings(request: GetGreetingsRequest?, responseObserver: StreamObserver<GetGreetingsResponse>?) {
      val greetings = repository.findAll().map {
        com.example.helloworld.grpc.greeting.Greeting
          .newBuilder()
          .setMessage(it.message)
          .setId(it.id)
          .build()
      }
      val response = GetGreetingsResponse
        .newBuilder()
        .addAllGreetings(greetings)
        .build()
      responseObserver?.onNext(response)
      responseObserver?.onCompleted()
    }

    override fun saveGreeting(request: SaveGreetingRequest?, responseObserver: StreamObserver<SaveGreetingResponse>?) {
      val message = request?.message ?: "Hello, World!"
      val greeting = repository.save(Greeting(message)).let {
        com.example.helloworld.grpc.greeting.Greeting
          .newBuilder()
          .setMessage(it.message)
          .setId(it.id)
          .build()
      }
      val response = SaveGreetingResponse
        .newBuilder()
          .setGreeting(greeting)
          .build()
      responseObserver?.onNext(response)
      responseObserver?.onCompleted()
    }
  }

Further Reading

Armeria

gRPCのサービスが実装できたら、次にそれを利用するようサービスをSpring Bootに登録する必要があります。

Spring Bootではgrpc-spring-boot-starterというdependencyによってgRPCサービスを利用することもできますが、今回は後述のArmeriaを使ってgRPCサービスを利用できるようにしました。

ArmeriaはLINEが開発しているマイクロサービスフレームワークで、去年参加したLINE DEV DAY 2019で紹介されていたため、試しに利用してみることにしました。

下記Dependencyの追加を行うことでArmeriaが利用可能になります。

build.gradle.kts
  dependencies {
    listOf(
      "armeria",
      "armeria-grpc",
      "armeria-logback",
      "armeria-spring-boot-webflux-starter"
    ).forEach {
      implementation("com.linecorp.armeria:$it:0.97.0")
    }

Armeriaの機能としては様々ありますが、今回は最低限として、

  • gRPCのインテグレーション
  • GUIでのgRPCのドキュメンテーションおよびデバッグ

を利用してみました。

下記のような設定ファイルを記述することで、ArmeriaでのgRPCのサービス登録やドキュメンテーションエンドポイントの利用が可能になります。

ArmeriaConfiguration.kt
  @Configuration
  class ArmeriaConfiguration(
    private val repository: GreetingRepository
  ) {
    @Bean
    fun armeriaServerConfigurator(): ArmeriaServerConfigurator {
      return ArmeriaServerConfigurator {
        it.serviceUnder("/docs", DocServiceBuilder()
          .exampleRequestForMethod(
            GreetingServiceGrpc.SERVICE_NAME,
            "GetGreeting",
            GetGreetingRequest.newBuilder().setId(1).build()
          )
          .exampleRequestForMethod(
            GreetingServiceGrpc.SERVICE_NAME,
            "GetGreetings",
            GetGreetingsRequest.newBuilder().build()
          )
          .exampleRequestForMethod(
            GreetingServiceGrpc.SERVICE_NAME,
            "SaveGreeting",
            SaveGreetingRequest.newBuilder().setMessage("Hello, World!").build()
          )
          .build()
        )
        it.decorator(LoggingService.newDecorator())
        it.accessLogWriter(AccessLogWriter.combined(), false)
        it.service(GrpcService.builder()
          .addService(GreetingGrpcService(repository))
          .supportedSerializationFormats(GrpcSerializationFormats.values())
          .enableUnframedRequests(true)
          .build())
      }
    }
  }

ArmeriaではgRPCのサービス登録とは別個に、ドキュメンテーション用のエンドポイントを設置することができます。

それを利用するとgRPCのサービス一覧、リクエストとレスポンスの構造体定義の参照、そしてGUIでリクエストの送信、デバッグ)が可能になります。

Screenshot 2020-01-04 14.09.49.png

Screenshot 2020-01-04 14.09.59.png

感覚としてはSwagger-UIをgRPCで利用しているような感覚で、直感的に利用することができました。

Further Reading

Jib

JibはGoogleが開発した、JVMアプリをコンテナ化するライブラリで、Docker imageを構築する際のベストプラクティスをカバーしてくれています。

下記のような設定をbuild.gradleに記述すると、設定した内容を利用してdocker imageをビルドすることができます。
簡単ですね!

build.gradle.kts
  plugins {
    (...)
    id("com.google.cloud.tools.jib") version "1.8.0"
  }

  jib {
    to {
      image = "wildmouse/greeting_api"
    }
    container {
      // TODO: make active profile variable
      args = listOf("--spring.profiles.active=local")
    }
  }

Further Reading

BFF

BFFはBackends For Frontendsの略称で、その名の通りフロントエンドのためのバックエンド を提供しています。

マイクロサービスアーキテクチャにおいては別名APIゲートウェイとも呼ばれ、一つのデザインパターンとして提唱されています。

BFFを利用することでフロントエンド側から受けたリクエストをバックエンドの手前で捌き、疎結合なアーキテクチャを実現することができます。

BFFのメリットとしては、

  • フロントエンド側では単一のAPIエンドポイント(BFF)のみを意識すれば良くなる、エンドポイントの管理が楽になる
  • フロントエンド側にマイクロサービス を隠蔽させ、それぞれの個別のマイクロサービスを意識させないようにできる
  • 各マイクロサービスのエンドポイントをフロントエンドで管理させずに済む
  • フロント側で有効なプロトコルから内部で別のプロトコルに変換して処理を行うことができる

等があります。

そして今回利用するApollo ServerはまさにBFFにうってつけの存在で、フロントエンドから受けたGraphQLのリクエストをgRPCへと変換し、先述のバックエンドへgRPCリクエストを出し、gRPCでレスポンスを受け取り、そしてフロントエンドへGraphQLのレスポンスとして返却することができます。

Apollo Serverを利用することでフロントエンド側からはAPIの実装を意識することなく、GraphQL Schemaの定義だけを参照してデータ取得を行えば良いことになり、レイヤの分離に役立ちます。

またgRPCを利用したマイクロサービスアーキテクチャ構成にする場合、追加したマイクロサービス についてのresolverを追加すればすぐにBFF、ひいてはフロントエンドからgRPCを利用することができるようになります。

実装としてはGraphQLを利用するために、GraphQL Schemaを定義します。
フロントエンド側ではこのSchemaに定義してある内容を利用することで、何がリクエストできるのか、そして何がレスポンスとして返却されるのかが一目瞭然になります。

GraphQL_Schema
const typeDefs = gql`
  type Greeting {
    id: Int!
    message: String!
  }
  type Query {
    greeting(id: Int!): Greeting
    greetings: [Greeting!]!
  }
  type Mutation {
    greeting(message: String!): Greeting!
  }
`

次に対応するGraphQLのリクエストが来た際に、対応する処理を定義するresolverを記述します。

今回はGraphQLで内部的にgRPCのリクエストを行うという記述をここに記載します。

Resolver
const resolvers = {
  Query: {
    greeting: async (_source, {id}) => {
      const result = await GetGreeting({id}, (err, result) => {
        return result
      })
      if (!result.hasOwnProperty("greeting")) {
        return undefined
      }
      return result.greeting
    },
    greetings: async (_source, {}) => {
      const result = await GetGreetings({}, (err, result) => {
        return result
      })
      if (!result.hasOwnProperty("greetings")) {
        return []
      }
      return result.greetings
    }
  },
  Mutation: {
    greeting: async (_source, {message}) => {
      const {greeting} = await SaveGreeting({message}, (err, result) => {
        return result
      })
      return greeting
    }
  }
}

次に実際にリクエストするgRPCクライアントを記述します。

@grpc/proto-loaderを使ってprotoファイルからサービスとメッセージの定義を読み込んで、対応するStubを記述します。

gRPC_Service
const protoLoader = require('@grpc/proto-loader')

const grpc = require('grpc')

const greetingProtoPath = '../api/src/main/proto/greeting.proto'
const greetingProtoDefinition = protoLoader.loadSync(greetingProtoPath)
const greetingPackageDefinition = grpc.loadPackageDefinition(greetingProtoDefinition).example.grpc.helloworld

const clientUri = process.env.CLIENT_URI || "localhost:8080"
const client = new greetingPackageDefinition.GreetingService(
  clientUri, grpc.credentials.createInsecure()
)

const GetGreeting = (params, context) => {
  return new Promise((resolve, reject) => {
    client.GetGreeting({id: params.id}, {}, (err, result) => {
      return resolve(result)
    })
  })
}

const GetGreetings = (params, context) => {
  return new Promise((resolve, reject) => {
    client.GetGreetings({}, {}, (err, result) => {
      return resolve(result)
    })
  })
}

const SaveGreeting = ({message}, context) => {
  return new Promise((resolve, reject) => {
    client.SaveGreeting({message}, {}, (err, result) => {
      return resolve(result)
    })
  })
}

最後にApollo Serverを定義・起動すればBFFは出来上がりです。

Apollo-Server
const server = new ApolloServer({
  typeDefs,
  resolvers
})

server.listen().then(({url}) => {
  console.log(`Server ready at ${url}`)
})

起動してlocalhost:4000を開くと、GraphiQLと呼ばれるGUIが開き、画面でGraphQLのリクエストと操作を行うことができます。

Screenshot 2020-01-04 21.00.03.png

Screenshot 2020-01-04 21.00.23.png

なお当然のことながら、Apollo ServerはgRPCの代わりにREST APIをresolverの内部で利用することが可能です。

REST APIを利用する場合のコードは下記のようになります。

RESTAPIResolver
class GreetingAPI extends RESTDataSource {
  constructor() {
    super()
    this.baseURL = 'http://localhost:8080/'
  }
  async getGreeting(id) {
    return this.get(`greetings/${id}`)
      .then(greeting => greeting)
      .catch(error => console.error(error))
  }
  async getHelloWorlds() {
    return await this.get('greetings')
      .then(greetings => greetings)
      .catch(error => console.error(error))
  }
  async saveHelloWorld(message) {
    return await this.post(`greetings?message=${message}`)
      .then(greeting => greeting)
      .catch(error => console.error(error))
  }
}

const resolvers = {
  Query: {
    greeting: async (_source, {id}, {dataSources}) => {
      const greeting = await dataSources.greetingAPI.getGreeting(id)
        return greeting
    },
    greetings: async (_source, {}, {dataSources}) => {
      const greetings = await dataSources.greetingAPI.getGreetings()
      return greetings
      }
    },
    Mutation: {
      greeting: async (_source, {message}, {dataSources}) => {
        await dataSources.greetingAPI.saveGreeting(message)
    }
  }
}

Further Reading

Frontend

フロントエンドではブラウザでユーザーの操作を受け付け、画面側での操作に応じてGraphQLのリクエストを行います。

リクエストはApollo Clientを利用して行い、フロントエンドからBFFのApollo Serverへリクエストが送られます。

Clientの利用をするにはApolloClientのインスタンスを作成すればOKです。

apollo_client
const clientUri = process.env.CLIENT_URI || "http://localhost:4000"
const link = createHttpLink({
  uri: clientUri,
  fetch: fetch
})
const client = new ApolloClient({
  link: link,
  cache: new InMemoryCache(),
})

フロントエンドのレンダリングはSSR用のフレームワークであるNext.jsを利用し、サーバサイドでレンダリングを行った上でブラウザへレンダリング結果を返却します。

Next.jsのgetInitialProps上でApollo Clientを利用しGraphQLのqueryリクエストをBFFに送信、取得した結果をcomponentのpropsとして返却します。

getInitialProps
Home.getInitialProps = async () => {
  const greetings = await client.query({
    query: gql`
      query {
        greetings {
          id
          message
        }
      }
    `
  })
  .then((result: ApolloQueryResult<{ greetings: Greeting[] }>) => result.data.greetings)
  .catch(error => console.error(error))
    return {
      greetings: greetings
    }
}

レンダリング後の画面ではユーザーの画面操作でメッセージを入力・保存する操作が行われた時にはmutationのリクエストを行い、BFFへリクエストします。

Home_component
const Home = ({greetings}: Props) => {
  const router = useRouter()

  const [message, setMessage] = useState('')

  const onClickGreet = useCallback(async () => {
    if (message == '') {
      return
    }

    const isSaved = await client.mutate({
      mutation: gql`
        mutation {
          greeting(message: "${message}") {
            id
              message
              }
          }
      `
    })
      .then(result => {
        return true
      })
      .catch(error => {
        return false
      })

      if (isSaved) {
        await router.push('/')
      }
  }, [message])

    if (greetings.length == 0) {
      return (
        <>
          <input value={message} onChange={(e) => setMessage(e.target.value)}/>
          <button onClick={onClickGreet}>Greet</button>
          <p>There is no greeting. Please say hi!</p>
        </>
        )
    }

    return (
      <>
        <input value={message} onChange={(e) => setMessage(e.target.value)}/>
        <button onClick={onClickGreet}>Greet</button>
          <ul>
            {
              greetings.map((greeting) =>
                <li key={greeting.id}>
                  <a href={`/hello-world/${greeting.id}`}>See greeting {greeting.id}</a>
                </li>
              )
            }
          </ul>
      </>
    )
}

上記Apollo Clientからのリクエストができ、想定したレスポンスが返ってきたとしたら、記事冒頭で述べた一連の処理ができるようになったということになります。

  1. フロントエンドでApollo Clientを使ってGraphQLでQuery/Mutationのリクエストを行い、
  2. BFF(Backend For Frontend)のApollo ServerでGraphQLのリクエストに応じたgRPCのリクエストをバックエンドへ行い、
  3. バックエンドで受け取ったgRPCリクエストに応じた処理(追加/削除)をWebFluxでリアクティブに実行し、逐次レスポンスを返却、
  4. BFFでgRPCのリクエストをGraphQLのレスポンスとして整形し、
  5. フロントエンドでGraphQLのレスポンスを用いて画面の表示、更新を行う

Awesome!

Further Reading

まとめ

以上、作成したアプリケーションで構築したアプリケーションのアーキテクチャと、利用した技術についてそれぞれ解説させていただきました。

単純なHello Worldを出力するアプリではありましたが、モダンな技術ばかり使って開発ができ、かなり楽しかったです。

今回ApolloとgRPC, Armeria, WebFluxについては私の方ではほぼほぼ初めて触りましたが、年末年始の1週間程度で構築できましたし、最初の導入コスト自体はそこまで高くなかった印象です。

当たり前のことかもしれないが、導入自体は公式ドキュメントを読んだりGitHub上のコードを参考にすることで動くものは作れますね。

ただ実務で導入するとなるとユニットテストの導入や各レイヤ間の整合性の担保、インフラストラクチャの考慮など、様々な点を考慮する必要が出てきます。

今後は本記事で構築したアーキテクチャのより発展的な部分について深掘りしていきたいと思いますが、ひとまずは上記の内容で一区切りにしたいと思います。

それでは、ありがとうございました。

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

Javascript勉強の記録その3: for文

Javascriptでfor文

for (初期値; 条件式; 増減値) {ループ毎に行いたい処理;}
と書くことでループ処理ができる。

以下の例では1から10までをコンソールに表示します。

index.js
for (let i = 1; i <= 10; i++) {
  console.log(i)
}

// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10

ちなみに``(バッククオート)の中に、${式}と書くことで、文字列の中に式を展開することができます。 これをテンプレートリテラルと言うそうです。

index.js
for (let i = 1; i <= 10; i++) {
  console.log(`hello ${i}`)
}

// hello 1
// hello 2
// hello 3
// hello 4
// hello 5
// hello 6
// hello 7
// hello 8
// hello 9
// hello 10
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript勉強の記録その3: for文

Javascriptでfor文

for (初期値; 条件式; 増減値) {ループ毎に行いたい処理;}
と書くことでループ処理ができる。

以下の例では1から10までをコンソールに表示します。

index.js
for (let i = 1; i <= 10; i++) {
  console.log(i)
}

// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10

ちなみに``(バッククオート)の中に、${式}と書くことで、文字列の中に式を展開することができます。 これをテンプレートリテラルと言うそうです。

index.js
for (let i = 1; i <= 10; i++) {
  console.log(`hello ${i}`)
}

// hello 1
// hello 2
// hello 3
// hello 4
// hello 5
// hello 6
// hello 7
// hello 8
// hello 9
// hello 10
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

humbergermenuのコード

filename.html
<body>
  <header>
    <nav class="global-nav">
      <ul class="global-nav__list">
    <li class="global-nav__item">
          <a href="">会社概要</a>
        </li>
    <li class="global-nav__item"><a href="">サービス</a></li>
    <li class="global-nav__item"><a href="">採用情報</a></li>
    <li class="global-nav__item"><a href="">アクセス</a></li>
    <li class="global-nav__item"><a href="">お問い合わせ</a></li>
      </ul>
    </nav>

        <div class="hamburger" id="js-hamburger">
          <span class="hamburger__line hamburger__line--1"></span>
          <span class="hamburger__line hamburger__line--2"></span>
          <span class="hamburger__line hamburger__line--3"></span>
        </div>
        <div class="black-bg" id="js-black-bg"></div>
  </header>

  <script src="humberger.js"></script>
</body>
filename.css
.global-nav {
  position: fixed;
  right: -320px; /* これで隠れる */
  top: 0;
  width: 300px; /* スマホに収まるくらい */
  height: 100vh;
  padding-top: 40px;
  background-color: #fff;
  transition: all .6s;
  z-index: 900;
  overflow-y: auto; /* メニューが多くなったらスクロールできるように */
}
.hamburger {
  position: absolute;
  right: 0;
  top: 0;
  width: 50px; /* クリックしやすいようにちゃんと幅を指定する */
  height: 50px; /* クリックしやすいようにちゃんと高さを指定する */
  cursor: pointer;
  z-index: 600;
}
.global-nav__list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.global-nav__item {
  text-align: center;
  padding: 0 -2px;
  line-height: 500%;
}
.global-nav__item a {
  display: block;
  padding: -2px 0;
  border-bottom: 1px solid #eee;
  text-decoration: none;
  color: #111;
}
.global-nav__item a:hover {
  background-color: #eee;
}
.hamburger__line {
  position: absolute;
  left: 11px;
  width: 18px;
  height: 1px;
  background-color: #111;
  transition: all .6;
}
.hamburger__line--1 {
  top: 14px;
}
.hamburger__line--2 {
  top: 20px;
}
.hamburger__line--3 {
  top: 26px;
}
.black-bg {
  position: fixed;
  left: 0;
  top: 0;
  width: 100vw;
  height: 100vh;
  z-index: 100;
  background-color: #000;
  opacity: 0;
  visibility: hidden;
  transition: all .6s;
  cursor: pointer;
}
/* 表示された時用のCSS */
.nav-open .global-nav {
  right: 0;
}
.nav-open .black-bg {
  opacity: .8;
  visibility: visible;
}
.nav-open .hamburger__line--1 {
  transform: rotate(45deg);
  top: 20px;
}
.nav-open .hamburger__line--2 {
  width: 0;
  left: 50%;
}
.nav-open .hamburger__line--3 {
  transform: rotate(-45deg);
  top: 20px;
}
filename.js
function toggleNav() {
  var body = document.body;
  var hamburger = document.getElementById('js-hamburger');
  var blackBg = document.getElementById('js-black-bg');

  hamburger.addEventListener('click', function() {
    body.classList.toggle('nav-open');
  });
  blackBg.addEventListener('click', function() {
    body.classList.remove('nav-open');
  });
}
toggleNav();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascript勉強の記録その2: Switch文

JavascriptでSwitch文

switchの引数でとった値を判定し、'赤'だった場合は「止まれ」、'黄'だった場合は「注意、'青'だった場合は「渡れ」をコンソールに表示します。

index.js
const signal = ''

switch (signal) {
  case '':
    console.log('止まれ');
    break;
  case '':
    console.log('注意');
    break;
  case '':
    console.log('渡れ');
    break;
}

ちなみにbreakはループ処理を抜ける命令ですので、breakの記述がない場合は下まで処理が続きます。

index.js
const signal = ''

switch (signal) {
  case '':
    console.log('止まれ');
    break;
  case '':
    console.log('注意');
  case '':
    console.log('渡れ');
    break;
}
//コンソールには「注意」と「渡れ」が表示される
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript勉強の記録その2: Switch文

JavascriptでSwitch文

switchの引数でとった値を判定し、'赤'だった場合は「止まれ」、'黄'だった場合は「注意、'青'だった場合は「渡れ」をコンソールに表示します。

index.js
const signal = ''

switch (signal) {
  case '':
    console.log('止まれ');
    break;
  case '':
    console.log('注意');
    break;
  case '':
    console.log('渡れ');
    break;
}

ちなみにbreakはループ処理を抜ける命令ですので、breakの記述がない場合は下まで処理が続きます。

index.js
const signal = ''

switch (signal) {
  case '':
    console.log('止まれ');
    break;
  case '':
    console.log('注意');
  case '':
    console.log('渡れ');
    break;
}
//コンソールには「注意」と「渡れ」が表示される
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascript勉強の記録その1: If文

Javascriptでif文

一番シンプルな形としてはif(条件式){Trueだった時の処理}と書くことができる。

index.js
const score = 90;

if (score >= 80) {
  console.log('Great!');
} 

if(条件式){Trueだった時の処理}else{Trueじゃなかった時の処理}という書き方もできる。

index.js
const score = 60;

if (score >= 80) {
  console.log('Great!');
} else {
  console.log('OK...');
}

条件式の中に&&や||を入れることで、ANDやORを表現することができる。

index.js
const score = 60;
const name = 'okuno'

if (score >= 50 && name === 'okuno') {
  console.log('Good Job!');
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript勉強の記録その1: If文

Javascriptでif文

一番シンプルな形としてはif(条件式){Trueだった時の処理}と書くことができる。

index.js
const score = 90;

if (score >= 80) {
  console.log('Great!');
} 

if(条件式){Trueだった時の処理}else{Trueじゃなかった時の処理}という書き方もできる。

index.js
const score = 60;

if (score >= 80) {
  console.log('Great!');
} else {
  console.log('OK...');
}

条件式の中に&&や||を入れることで、ANDやORを表現することができる。

index.js
const score = 60;
const name = 'okuno'

if (score >= 50 && name === 'okuno') {
  console.log('Good Job!');
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

知らないとはまってしまうJavaScriptの配列コピー

概要

JavaScriptで配列コピーをする際、
「=(イコール)」によるコピーでは意図した動作にならない場合があります。
意外にはまりやすい落とし穴だと思ったので、
初心者にもわかりやすいよう、図も載せておきました。

配列コピーによる動き

まずは、動きを確認しましょう。

変数aの配列を宣言し、変数bにコピーします。

test.js
var a = ['','','','',''];
var b = a;

その後、変数b[0]の内容を書き換えます。

test.js
b[0] = '';

この時、書き換えたいのは変数bの値ですが実際は

test.js
  console.log('変数a: ' + a); // ['か','い','う','え','お']
  console.log('変数b: ' + b); // ['か','い','う','え','お']

と、変数aの値も書き換わってしまいます。
書き換えたのは変数bだったのに、なぜ変数aも一緒に書き換わってしまったのでしょうか。

コピー元の値が書き換わる理由

では、なぜ変数bの値を書き換えたにも関わらず、変数aの値も書き換わってしまったのか
図を交えて説明していきます。

まず、この変数aに格納されている値は、配列の情報ではなく、
配列aのインスタンスを参照するアドレスが格納されています。

その為、変数bに変数aをコピーした際、
配列ではなくアドレスの情報が渡されているのです。

実際に図にすると、以下の状態です。
配列コピー.jpg

次に、変数b[0]に「か」という文字を代入しましたが、
この時、変数b[0]は変数bの値を書き換えるのではなく、
配列の値を保持したメモリを直接書き換えることになります。

配列値変更.jpg

つまり、このb=aという式では、配列は複製されず
同じアドレス先を共有しているだけなのです。

この動作を理解していないと、
まるで両方の変数で値が書き換わったように感じてしまうのです。

配列をコピー(複製)する方法

では、実際に配列を複製するためにはどうすればいいか、いくつか例を挙げておきます。

for文によるコピー

for文を使って、配列要素文のループにより値を代入します。
この時、変数bは空の配列で宣言します。

test.js
  var a = ['','','','',''];
  var b =[];

  for( var i =0; i < a.length; i++){
    b[i] = a[i];
  }

  b[0] = '';
  console.log('変数a: ' + a); // ['あ','い','う','え','お']
  console.log('変数b: ' + b); // ['か','い','う','え','お']

Array.concat()によるコピー

Arrayオブジェクトのconcatメソッドは指定された配列を連結して値を返すメソッドですが
引数を指定しない場合は、元の配列のコピーを返します。

test.js
  var a = ['','','','',''];
  var b = a.concat();

  b[0] = '';
  console.log('変数a: ' + a); // ['あ','い','う','え','お']
  console.log('変数b: ' + b); // ['か','い','う','え','お']

Array.slice()によるコピー

Arrayオブジェクトのsliceオブジェクトは配列の一部を取り出して値を返すメソッドですが、
開始を指定し、終了を省略すると元の配列のコピーを返します。

test.js
  var a = ['','','','',''];
  var b = a.slice(0);

  b[0] = '';
  console.log('変数a: ' + a); // ['あ','い','う','え','お']
  console.log('変数b: ' + b); // ['か','い','う','え','お']

まとめ

「=(イコール)」を使って変数の値をコピーする際には
その変数がなんの値を保持しているのか注意しましょう。

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

[連載]スーパーマリオ的なゲームをjavascriptで作ってみる 初級編 〜3章〜 画像を動かしてみる

本連載について

本章の概要

  • 本章では主人公の画像を表示して、キーボードの入力に応じて動かすとこまでいきます
  • ゲームっぽくジャンプしたり、落ちたり敵にぶつかってゲームオーバーになったりはこの後やります
  • 本章の内容は大きく4ステップです
    • その1 〜画像を表示できるよの巻〜
    • その2 〜画像が動くよの巻〜
    • その3 〜キーボードの入力が検知できるよの巻〜
    • その4 〜画像を動かせるよの巻〜
  • 各ステップごとに実際のソースをQiita上に記載しています
  • 上記と同じくソースの実態を保存しているgitのリポジトリも記載しています
    リンクにアクセスして実際のソースをダウンロードすることができます
    ぜひダウンロードして動かしながら試してみてください!

その1 〜画像を表示できるよの巻〜

ゴール

  • 特定の画像をcanvas要素のエリアに表示すること
  • マリオのゲーム作るにはマリオを画面に表示することは必要ですからね!

前提

  • 大枠の流れとしては、 canvas 要素に対して どこに どんな画像 を表示するかを指定します
  • canvas 要素に対して画像の指定する際は drawImage の関数を使用します
  • 画像を表示する場合、主に 画像ファイルのパス指定する または 画像をエンコードして文字列としたものを記載する の2択がありますが今回は前者を採用します

やること

  • 画像ファイルを作成し、配置します ([参考]ドッド絵を作成する)
  • index.jsindex.html で定義した canvas 要素を取得するように修正します
  • Image 要素を作成し、 canvas 要素に対して追加(draw)するように修正します
  • フォルダ階層は以下ようにしています
└┬─ src    ┬─ index.html
 │         └─ index.js
 └─ images ── character-01 ── base.png

▼ソース

index.js
// canvas要素の取得
const canvas = document.getElementById("maincanvas");
const ctx = canvas.getContext("2d");

// ロード時に画面描画の処理が実行されるようにする
window.addEventListener("load", update);

// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {
  // 画面全体をクリア
  ctx.clearRect(0, 0, 640, 480);

  // 主人公の画像を表示
  var image = new Image();
  image.src = "../images/character-01/base.png";
  ctx.drawImage(image, 0, 0, 32, 32);

  // 再描画
  window.requestAnimationFrame(update);
}

※ 実際のソースコードは こちら からダウンロードできます

▼CodePenのサンプル

See the Pen mario-game-tutorial-01-03-01 by taku7777777 (@taku7777777) on CodePen.

説明

  • document.getElementById("maincanvas")canvas 要素を取得しています
    maincanvasindex.htmlcanvas 要素の id として指定しているものです
    index.js 内から index.html に記載している要素を取得することができます
  • canvas.getContext("2d") で取得した canvas 要素からさらに二次元の情報(今回描画したい対象の情報)を取得します
    ここまでは定型文のようなものなので、深く突っ込まず「へぇーこうゆうふうに記載すればいいんだ」くらいでOKです!
  • ctx.clearRect(0, 0, 640, 480)canvas 要素の描画対象をクリアしている
    これをしないと、この後に実施する画像表示の処理が重複して、意図せず複数の画像が表示されかねないので注意です
  • var image = new Image() で新しく画像要素を作成し、 image.src = "../images/character-01/base.png" で画像要素の画像情報を定義しています
  • ctx.drawImage(image, 0, 0, 32, 32) では どの画像要素どこに どのような大きさで 表示するかを定義しています
    今回の例では、"../images/character-01/base.png" の画像をx座標が0(一番左から右に0ピクセル移動したとこ)、y座標が0(一番上から下に0ピクセル移動したとこ)に横幅32ピクセル、縦幅32ピクセルで画像を表示するということになります
    例えば ctx.drawImage(image, 100, 200, 10, 20) の場合x座標が100(一番左から右に100ピクセル移動したとこ)、y座標が200(一番上から下に200ピクセル移動したとこ)に横幅10ピクセル、縦幅20ピクセルで画像を表示するということになります

その2 〜画像が動くよの巻〜

ゴール

  • 表示した画像が横に少しづつ動くようにします!(超簡単なアニメーション!?)

やること

  • 「その1 〜画像を表示できるよの巻〜」で対応した drawImage の画像表示位置を少しづつ変えます
  • 最初は一番左に画像を表示し、一定の速度で右に移動するようにします

▼ソース

index.js
// canvas要素の取得
const canvas = document.getElementById("maincanvas");
const ctx = canvas.getContext("2d");

// ロード時に画面描画の処理が実行されるようにする
window.addEventListener("load", update);

// 主人公の初期座標を定義
var x = 0;

// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {
  // 画面全体をクリア
  ctx.clearRect(0, 0, 640, 480);

  // 座標を更新する
  x = x + 1;

  // 主人公の画像を表示
  var image = new Image();
  image.src = "../images/character-01/base.png";
  ctx.drawImage(image, x, 0, 32, 32);

  // 再描画
  window.requestAnimationFrame(update);
}

※ 実際のソースコードは こちら からダウンロードできます

▼CodePenのサンプル (右下の"return"をクリックして、リフレッシュしてから確認してみてください)

See the Pen mario-game-tutorial-01-03-02 by taku7777777 (@taku7777777) on CodePen.

説明

  • x座標を変数化し(x)、update処理が実施されるごとに1増えるようにしています

その3 〜キーボードの入力が検知できるよの巻〜

ゴール

  • キーボードで上下右左が押されたことを検知できるようにします
  • これができると入力に応じていろんなことをできるようになります!

やること

  • 以下のソースをコピペ
    (これも定型文みたいなものなので細かいとこは気にしなくて大丈夫ですー)
index.js
// キーボードの入力状態を記録する配列の定義
var input_key_buffer = new Array();

// キーボードの入力イベントをトリガーに配列のフラグ値を更新させる
window.addEventListener("keydown", handleKeydown);
function handleKeydown(e) {
  console.log("key down : " + e.keyCode);
  input_key_buffer[e.keyCode] = true;

  if (e.keyCode === 37) {
    alert("左が押されたよ");
  } else if (e.keyCode === 38) {
    alert("上が押されたよ");
  } else if (e.keyCode === 39) {
    alert("右が押されたよ");
  } else if (e.keyCode === 40) {
    alert("下が押されたよ");
  }
}

window.addEventListener("keyup", handleKeyup);
function handleKeyup(e) {
  console.log("key up : " + e.keyCode);
  input_key_buffer[e.keyCode] = false;

  if (e.keyCode === 37) {
    alert("左がはなされたよ");
  } else if (e.keyCode === 38) {
    alert("上がはなされたよ");
  } else if (e.keyCode === 39) {
    alert("右がはなされたよ");
  } else if (e.keyCode === 40) {
    alert("下がはなされたよ");
  }
}

※ 実際のソースコードは こちら からダウンロードできます

説明

  • window.addEventListener("keydown", XXXXX) キーボードが押された際に XXXXX の関数を実行するよって定義することができます
  • window.addEventListener("keyup", XXXXX) キーボードが押された状態からはなされた際に XXXXX の関数を実行するよって定義することができます
  • handleKeydown handleKeyup でキーボーが押された際/はなされた際に実行される関数を定義しています
    引数 (e) の部分でキーボードが押された/はなされたイベント情報をもらい、なんのキーが入力されたかを e.keyCode で取得することができます
  • alertのとこの分岐は処理を確認するためのおまけ、本当は不要なコードになります

その4 〜画像を動かせるよの巻〜

ゴール

  • その2、その3を組み合わせて上下左右のボタンを押した方向に画像が動くようにします
  • ちょっとゲームっぽくなってくるのではないでしょうか?

やること

  • 画像のx座標、y座標を変数化します
  • キーボードの入力状態に応じてx座標、y座標を更新します

▼ソース

index.js
// キーボードの入力状態を記録する配列の定義
var input_key_buffer = new Array();

// キーボードの入力イベントをトリガーに配列のフラグ値を更新させる
window.addEventListener("keydown", handleKeydown);
function handleKeydown(e) {
  console.log("key down : " + e.keyCode);
  input_key_buffer[e.keyCode] = true;
}

window.addEventListener("keyup", handleKeyup);
function handleKeyup(e) {
  console.log("key up : " + e.keyCode);
  input_key_buffer[e.keyCode] = false;
}

// canvas要素の取得
const canvas = document.getElementById("maincanvas");
const ctx = canvas.getContext("2d");

// 画像を表示するの座標の定義 & 初期化
var x = 0;
var y = 0;

// ロード時に画面描画の処理が実行されるようにする
window.addEventListener("load", update);

// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {
  // 画面全体をクリア
  ctx.clearRect(0, 0, 640, 480);

  // 入力値の確認と反映
  if (input_key_buffer[37]) {
    // 左が押されていればx座標を1減らす
    x = x - 1;
  }
  if (input_key_buffer[38]) {
    // 上が押されていればy座標を1減らす
    y = y - 1;
  }
  if (input_key_buffer[39]) {
    // 右が押されていればx座標を1増やす
    x = x + 1;
  }
  if (input_key_buffer[40]) {
    // 下が押されていればy座標を1増やす
    y = y + 1;
  }

  // 主人公の画像を表示
  var image = new Image();
  image.src = "../images/character-01/base.png";
  ctx.drawImage(image, x, y, 32, 32);

  // 再描画
  window.requestAnimationFrame(update);
}

※ 実際のソースコードは こちら からダウンロードできます

▼CodePenのサンプル

See the Pen mario-game-tutorial-01-03-04 by taku7777777 (@taku7777777) on CodePen.

説明

  • input_key_bufferkeyCode ごとにおされているか(true)、押されていないか(false)を保持しているので、update処理を実行する際に押されているキーに応じて座標を更新します

終わりに

  • これでちょっとはゲームっぽさが出てきましたね!
  • 次にいきましょう! (まだ準備中...)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript ブラウザイベント

イベントハンドラとイベントリスナ

  • イベントに対する処理自体をイベントハンドラまたはイベントリスナと呼ぶ。
  • イベントハンドラは一つの要素、一つのイベントに対して一つしか設定できない。
  • イベントリスナは複数設定できる。
  • イベントに対する処理の設定方法は以下の通り。
    • HTML要素の属性を指定する(イベントハンドラ)
    • DOM要素のプロパティを指定する(イベントハンドラ)
    • EventTarget.addEventListener()を利用する(イベントリスナ)

HTML要素の属性を指定する

<input id="foo" type="button" value="foo" onclick="alert('bar')">

ただしHTMLとJavaScriptを混在させると、可読性が低いので、通常はJavaScript のファイルは一つにまとめる。

DOM要素のプロパティを指定する

<script>
var btn = document.getElementById('foo');
function sayFoo(){
 alert('foo');
}
btn.onclick = sayFoo; 
//正 :関数自体(sayFoo)を設定する
//誤り:button.onclick = sayFoo();だと関数実行した返り値設定することになるので誤り
</script>
<input id="foo" type="button" value="foo" onclick="sayFoo()">

EventTarget.addEventListener()

  • addEventListener()メソッドを使うと、特定の要素の特定のイベントに対して複数のイベントリスナを設定できる。
  • 新しいDOMプロパティは既存のものを上書きする。
  • 第3引数でキャプチャリングフェーズとバブリングフェーズを指定できる。
  • DOM Level3では、第3引数を省略した場合にバブリングフェーズで実行される。
  • DOM Level3では、イベントリスナの実行順序は登録順で実行されると定義されている。
<script>
btn.addEventListener('click' sayHello,false)
btn.addEventListener('click' sayHello,false)// 前のイベントリスナを上書きします
// ...
btn.addEventListener('click' sayHello, true)// フェーズが異なれば別のものとして登録される
</script>

イベントの伝播

  • イベントは以下の3つのフェーズに分かれて処理される。
    • キャプチャリングフェーズ
    • ターゲットフェーズ
    • バブリングフェーズ

キャプチャリングフェーズ

Windowオブジェクトからはじまり、DOMツリーを下に辿りイベントが伝播していくフェーズ。

ターゲットフェーズ

イベントターゲットに登録されているイベントリスナが実行されるフェーズ。

バブリングフェーズ

イベントターゲットからDOMツリーを上に辿りイベントが伝播していくフェーズ。

標準処理のキャンセル

  • ブラウザが標準的に実装している処理を実行させないようにすることができる。
  • Event.preventDefault()メソッドを使う。
  • 例えばa要素をクリックすると、そのリンク先のページに遷移するが、preventDefault()メソッドを実行すると その動作が実行されなくなる。
  • イベント中にpreventDefault()メソッドで中止できないイベントもある。
  • イベントハンドラでfalseを返すのと同じ。
<a href="/" onclick="return false">Click here</a>
or
<a href="/" onclick="event.preventDefault()">here</a>

参考

EventTarget.addEventListener() - MDN Web docs
JAVASCRIPT.INFO - ブラウザイベントの紹介

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

JavaScriptでシークレットモードを検出する方法

最新の(Chrome76以降)のシークレットモード検出方法

2020年1月現在、Chromeブラウザのシークレットモードは下記のJSで検出できる。ただし、問題点も残っている。

function detectSecretMode () {
  if ('storage' in navigator && 'estimate' in navigator.storage) {
    navigator.storage.estimate().then(function (estimate) {
      var usage = estimate.usage;
      var quota = estimate.quota;

      if (quota < 120000000) {
        console.log('Incognito');
      } else {
        console.log('Not Incognito')
      }
    });
  } else {
    console.log('Can not detect');
  }

このシークレットモード検出方法は、ユーザーのストレージが少なすぎるなら、シークレットモードであるという仮定に基づいているが、この仮定は100%確実ではない。実際に自サイトで利用したところ、およそ一桁%の確率で誤検出が起きる。

これまでの誤検出状況を見る限り、写真等をたくさん保存するユーザーは、本当にストレージが少ないことがそれなりにある模様。

また、OSのバージョンが古い(Android4, 5, 6 等)ときも起きるようだが、ストレージ容量起因の誤検出と原因の分離ができていないためこちらは少し曖昧。

少し古い(Chrome76よりも前)シークレットモード検出方法

少し古いChromeでは、下記のJSでシークレットモードを検出できる。

function detectSecretMode () {
  var fs = window.RequestFileSystem || window.webkitRequestFileSystem;
  if (fs) {
    fs(window.TEMPORARY,
        100,
        function (fs) {
          console.log('Not Incognito');
        },
        function (fe) {
          console.log('Incognito');
        });
  } else {
    console.log('Can not detect');
  }
}

参考リンク

「Chrome」のシークレットモードを検知できる手法、研究者が指摘
Bypassing anti-incognito detection in Google Chrome

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

Reactにおけるスプレッド構文の使い所

スプレッド構文を使うようになってからコードの可読性が上がったように感じたので、
どのようなところで使っているのかまとめます。

スプレッド構文とは

配列 や オブジェクトをその場で展開する構文

example.js
// Array
const odd = [1, 3]
const even = [2, 4]
const numbers = [...odd, ...even]
console.log(numbers) // [1, 3, 2, 4]

// Object
const name = {first: "Tanaka", last: "Taro"}
const age = {age: 27}
const profile = {...name, ...age}
console.log(profile) // {first: "Tanaka", last: "Taro", age: 27}

配列やオブジェクトをコピーするだけでなく、マージすることが簡単にできます。
Arrayを見るとわかりますが並び順は変わらないので注意。
配列であればhoge.concat()
オブジェクトであれば、Object.asign()
を使い実現していたことを短い記述で同様のことを実現できるのが利点です。

example.js
// Array
const numbers = odd.concat(even)

// Object
const profile = Object.assign(name, age)

Reactではどう使うか

簡単なuserListアプリを作成し、どう使えるか見てみます。
コードは、ここ(GitHubに飛びます)

スクリーンショット 2020-01-04 12.55.02.png

今回のアプリは3つのファイルで構成され、Materialize Cssでスタイリングしています。

App.js
import React from 'react';
import InputForm from './components/InputForm';
import UserList from './components/UserList';

class App extends React.Component {

  constructor() {
    super()

    this.state = {
      users: []
    }

    this.addUser = this.addUser.bind(this)
  }

  addUser(inputUser) {
    this.setState({
      // ①App.jsにおけるスプレッド構文
      users: [...this.state.users, inputUser]
    })
  }

  render() {
    return (
      <div className="App" >
        <h1>Spread</h1>
        <div className="row">
          <div className="col s6 offset-s3">
            <div className="col s6">
              <InputForm addUser={this.addUser} />
            </div>
            <div className="col s6">
              <UserList users={this.state.users} />
            </div>
          </div>
        </div>
      </div >
    );
  }
}

export default App;
InputForm.jsx
import React, { useState } from 'react';

const InputForm = (props) => {

  const [form, setValues] = useState({
    firstName: '',
    lastName: ''
  });

  const changeValue = e => {
    setValues({
      // ②InputForm.jsxにおけるスプレッド構文
      ...form,
      [e.target.name]: e.target.value
    });
  };

  const addUser = () => {
    props.addUser(form)
    // formの初期化
    setValues({
      firstName: '',
      lastName: ''
    });
  }

  return (
    <div>
      <div className="input-field">
        <input
          id="first_name"
          type="text"
          className="validate"
          name="firstName"
          value={form.firstName}
          onChange={changeValue}
        />
        <label htmlFor="first_name">First Name</label>
      </div>
      <div className="input-field">
        <input
          id="last_name"
          type="text"
          className="validate"
          name="lastName"
          value={form.lastName}
          onChange={changeValue}
        />
        <label htmlFor="last_name">Last Name</label>
      </div>
      <div className="btn" onClick={addUser}>
        <span>Add</span>
      </div>
    </div>
  )
}

export default InputForm
UserList.jsx
import React from 'react';

const UserList = (props) => {
  return (
    <table>
      <thead>
        <tr>
          <th>First Name</th>
          <th>Last Name</th>
        </tr>
      </thead>
      {props.users.length !== 0 &&
        <tbody>
          {props.users.map((user, index) => {
            return (
              <tr key={index}>
                <td>{user.firstName}</td>
                <td>{user.lastName}</td>
              </tr>
            )
          })}
        </tbody>
      }
    </table>

  )
}

export default UserList

①App.jsにおけるスプレッド構文

App.jsx
  addUser(inputUser) {
    this.setState({
      users: [...this.state.users, inputUser]
    })
  }

子コンポーネントから送られてきたinputUserとstateで管理している既存のusersをマージするために使用しています。
実際にはDBから再取得しリストを更新するため、
今回のようにユーザーを作成⇨リストを更新する時にスプレッドを使った実装はあまりしないような気がしますが、
一時的に保存する配列...
例えば、グループメンバーの管理画面でユーザーを複数選択して、まとめて追加するといった実装には使えます。

②InputForm.jsxにおけるスプレッド構文

InputForm.jsx
  const [form, setValues] = useState({
    firstName: '',
    lastName: ''
  });

  const changeValue = e => {
    setValues({
      ...form,
      [e.target.name]: e.target.value
    });
  };

HooksのuseStateを使って複数の値を管理しているため、
formの値を全て展開してマージした後にセットしています。
Hooksで複数の値を用いる時は必須の処理です。

まとめ

今回はlastName,firstNameのみのフォームでしたが、
要素が増えればObject.entriesを使用してうまくまとめることもできます。
参考:React Hooksで複数のFormを扱う

またスプレッド構文自体はHOCなどで頻繁に登場するので、意味を把握しておくことは重要ですね。

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

JavaとJacksonでJSON その③HTMLにJSONを埋め込んでJavaScript から利用する

はじめに

JSONを使う場合、JavaScriptからAjax経由でデータの送受信を行うケースが多いと思う。しかしながら、サーバからHTMLを受信したタイミングで、JSONデータを受け取ってJavaScriptで利用したいケースもある。この場合、サーバから返却するHTMLの中にJSONデータを埋め込んで、それをJavaScriptのオブジェクトとして読み込むことになる。PHPを利用した場合は、HTML に JSON データを埋め込んで JavaScript から利用するに記載の事例があったが、我らがJava(Servlet/JSP)による事例がなかったため、悪戦苦闘した結果をここに残しておく。

環境

  • Java 1.8
  • Tomcat 8.0.53
  • Jackson 2.10.1

まずは何も考えずにやってみよう⇒失敗

サーバ側

サーバ側は以下の通りとした。"<"や">"については、前回同様、HTMLのタグとして解釈される恐れがあることから、Unicodeエスケープシーケンス変換はそのままとしている。前回までとの違いは、JSON文字列を、HttpRequestのパラメータとして保存し、それをJSPに処理させている点だ。詳しくはクライアント側の方で解説する。
また、意地悪データとして、"Programmer"⇒"Programmer\"のようにデータの最後に\を入れてみた。

ServletTest2.java
package servletTest;

import java.io.IOException;
import java.util.List;
import java.util.ArrayList;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletContext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@WebServlet("/helloworld2")
public class ServletTest2 extends HttpServlet {
    private static final long serialVersionUID = 1L;
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        //Javaオブジェクトに値をセット
        JsonBean jsonBean = new JsonBean();
        jsonBean.setId(1);
        jsonBean.setName("kimisyo");
        List<String> datas = new ArrayList<>();
        datas.add("Programmer\\");
        datas.add("Data Scientist<script>alert('hello!')</script>");
        jsonBean.setDatas(datas);

        ObjectMapper mapper = new ObjectMapper();
        mapper.getFactory().setCharacterEscapes(new CustomCharacterEscapes());

        //JavaオブジェクトからJSONに変換
        String testJson = mapper.writeValueAsString(jsonBean);

        //JSON文字列をrequestにセット
        request.setAttribute("jsonStr", testJson);
        ServletContext sc = getServletContext();
        sc.getRequestDispatcher("/clientTest2.jsp").forward(request, response);

    }
}

クライアント側

クライアント側はjspに処理を記載している。HttpRequestのパラメータとして設定されたJSON文字列を一旦Javaの変数に保存し、それをJavaScriptの中のJSONのParseの引数に設定している。
これがうまくいけば、dataというオブジェクトを通して、JSONのハンドリングが可能となる。

clientTest2.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%
    String jsonStr = (String)request.getAttribute("jsonStr");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
    var data = JSON.parse('<%=jsonStr%>');
     alert(data.datas);
</script>
</head>
<body>
</body>
</html>

失敗状況

上のServletを実行してみると、JSON.parseのところでSCRIPT1014: 文字が正しくありません。というJavaScriptのエラーがでる(IEのコンソールで確認)。最終的にブラウザに出力されたHTMLは以下の通りだ。
JSONに入れた\は、Jacksonによって\\にちゃんとエスケープされているし、"<"や">"はUnicodeエスケープシーケンスに変換されている。一見問題なさそうに見える。

さぁ、何がまずかったのか考えて見よう。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
    var data = JSON.parse('{"id":1,"name":"kimisyo","datas":["Programmer\\","Data Scientist\u003Cscript\u003Ealert(\u0027hello!\u0027)\u003C\u002Fscript\u003E"]}');
    alert(data.datas);
</script>
</head>
<body>
    <div id="test"></div>
</body>
</html>

失敗原因

失敗原因はJavaScript文字列のエスケープ漏れだ。JavaScriptでは、\はエスケープ用の文字として使われる。このため、"Programmer\\"はJavaScriptの文字列として"Programmer\"と認識される。これがJSON.Parseに引き渡されるが、JSONでも本来"\"の文字自体は、"\\"のようにエスケープしなければならないため、不正なJSONデータとして扱われるのだ。

対策

対策としては、JavaScript用のエスケープ処理をかました上でJSON.parseの引数に与えればよい。修正ソースを以下に記載しておく。ここではJavaScript文字列のエスケープとして、"\"と"'"をエスケープするための処理を入れている。

clientTest2.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%
    String jsonStr = (String)request.getAttribute("jsonStr");
    jsonStr = jsonStr.replace("\\", "\\\\");
    jsonStr = jsonStr.replace("'", "\\'");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
    var data = JSON.parse('<%=jsonStr%>');
     alert(data.datas);
</script>
</head>
<body>
</body>
</html>

これにより、正しくJSON文字列がJSONオブジェクトに読み込まれた。
余談であるが、JavaScript用に"\"をエスケープした場合、Unicodeエスケープシーケンス変換した文字(例えば、"\u0027")に使われている"\"もエスケープされ"\\u0027"に変換されておかしくならないかという思うかもしれない。
実は、"\\u0027"はJavaScriptによって"\u0027"と解釈され、それがJSON.parseの引数に与えれるため、JSON側で、Unicodeエスケープシーケンスとして解釈されているのだ。つまり前回のXSS対策の場合と同じ形のものがJSONに読み込まれており、むしろこちらの方が意図した動作となっているのである。JavaScript用のエスケープをする前は、実はHTML側でUnicodeエスケープシーケンスとして解釈されていたのだ。うーん、奥が深い。

おわりに

結局JavaScriptエスケープだったという地味なオチ。この連載を始めたときに、この記事を書きたいと思っていたので、とりあえず完結としたい。

ちなみに本記事のサーバ側の例で、WEBフレームワークを使わずにServletを使って説明している理由は、別にServletしか使えない、Servletを使いたいわけではなく、本記事のテーマに関係ない要素は除外し、本質的な部分のみにフォーカスしたかったためである。

参考

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

試しに「OpenID Connect Provider Certification」を通してみた

以前、なんちゃってOpenID Connectサーバを立ち上げました。( なんちゃってOAuth2/OpenID Connectサーバを自作する )

それはそれで役に立っているのですが、OpenID Connectに準拠しているかどうかを確認するためのTest Suite があったので、物は試しで通してみました。
結果は、散々でした。Optional機能は実装していないので当然ではありますが。

Conformance Testing for Ops
 https://openid.net/certification/testing/

テストの準備

まずは、準備として、テスト対象のエンドポイントのURLやサポートする機能を指定します。

https://op.certification.openid.net:60000/

image.png

New ボタンを押下します。

image.png

Issuerに、サーバで生成するトークンに含めるissの値を指定します。
Response Typeとして「code」を選択しました。

最後に、「Create」ボタンを押下します。

次は、各エンドポイントなどを指定します。

image.png

以下の値を指定することで、テストを開始できました。

・contact_email : 適当な値
・authorization_endpoint : 認証エンドポイント
・jwks_uri : 署名を検証するための公開鍵のエンドポイント
・token_endpoint : トークンエンドポイント
・userinfo_endpoint : USERINFOエンドポイント
・client_id : 適当な値
・client_secret : 適当な値

最後に、「Save & Start」ボタンを押下すると、テスト用のページに進みます。

image.png

いざ、テスト実施

左側の列にある再生ボタンを押下していくと、それぞれテストが実施されます。
結果はこんな感じです。

image.png

まあ、実装を手抜きしているので当然ですね。
右側の列にある「!」マークのボタンを押下すれば、実行経過やエラー理由が確認できます。
エラーとなった理由を列挙しておきます。

(OP-Response-Missing)
・エラーメッセージを返すべきだが、HTTPエラーステータスが返ってきている。Swaggerで必須パラメータを指定しているがそれがないため

(OP-ClientAuth-SecretPost-Static)
(OP-claims-essential)
(OP-nonce-code)
(OP-prompt-none-LotLoggedIn)
(OP-Req-acr_values)
(OP-Req-login_hint)
(OP-Req-max_age=1)
(OP-Req-max_age=100000)
・OpenID Connectの仕様上Optionalな機能であり、対応していません。

(OP-redirect_uri-NotReg)
・リダイレクトURIをチェックしていないため(手抜き実装)

(OP-Oauth-2nd)
(OP-OAuth-2nd-30s)
(OP-OAuth-2nd-Revokes)
・認可コードの期限チェックや利用済みのチェックをしていないため(手抜き実装)

終わりに

なんちゃっての実装でしたが、こうしてやって実施してみると、すんなり確認が通ってよかったよかった。

以上

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

querySelectorAll() は動的に生成・削除された要素をキャッチしない

querySelectorAll() はCSSセレクタの記法で複数の要素を取得できる便利なメソッドですが、対象の要素を動的に生成 or 削除する場合は記述の仕方で取得結果が異なります。

記述の違い

処理を直接記述するか、変数に格納して参照するかの違いです。

console.log(document.querySelectorAll('.card'));
const cards = document.querySelectorAll('.card');
console.log(cards);

取得結果はどう違ってくるのか

.card クラスを持つ要素を動的に生成し、両者の取得結果を比較しました。

サンプルHTML

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>サンプル</title>
  <style>
    *{ box-sizing: border-box; }
    .card{ width: 200px; height: 100px; padding: 5px 0 0 10px; margin-top: 10px; background-color:#dddddd; }
    #card01{ background-color:#e02f2f; }
    #card02{ background-color:#4973f2; }
    #card03{ background-color:#f0e90c; }
    #card04{ background-color:#48f026; }
    .isSemitransparent{ opacity: 0.5; }
  </style>
</head>
<body id="body">
  <div id="cardWrap">
    <div id="card01" class="card">カード1</div>
    <div id="card02" class="card">カード2</div>
    <div id="card03" class="card">カード3</div>
  </div>
  <script>
    // ここに処理を書く
  </script>
</body>
</html>

サンプルHTMLの初期表示。3枚のカードが表示されています。
shot_ini.jpg

コンソールで検証(4枚目の緑のカードを動的に生成)

処理を直接記述した場合の取得結果
shot01.jpg
最後に出力されたログで NodeList に生成した要素の情報が反映されています。

変数に格納して参照した場合の取得結果
shot02.jpg
最後に出力されたログの NodeList をご覧ください。カードは4枚表示されているのに、 NodeList は (3) のままです。生成した要素の情報が反映されていません。

変数の参照では動的に生成・削除された要素が反映されないのです。

なぜ取得結果に違いが出るのか

MDN(NodeList)に以下の説明がありました。

document.querySelectorAll() メソッドは、静的な NodeList を返します。

NodeList には静的・動的の概念が存在するようです。
静的・動的の違いはまだちゃんと理解できている自信がありませんが、色々調べたうえで下記のように解釈しました。

  • 動的な NodeList ... Webページを構成するNodeを参照(シャローコピー)している NodeList
  • 静的な NodeList ... Webページを構成するNodeを複製(ディープコピー)して作られた NodeList

※シャローコピー・ディープコピーという表現も、あくまでもイメージです

上記の解釈で考えると、変数に格納した静的な NodeList は処理が走った時点(変数に代入したタイミング)の Node を基に作られた複製となります。大元の Node と紐づいていないので、大元の Node に変化があっても影響を受けません。代入後に大元の Node が変化すると、変数に格納してある NodeList は実際の Node よりも古い情報になってしまいます。

動的に生成・削除した要素を反映したい

対象の要素が動的に生成・削除される状況 かつ それらの要素に対して何かを行いたいケースがあるかもしれません。
処理を直接記述する事で対応はできますが、 getElementsBy〜() メソッドを使用するのも一つの手です。

下記の前提で検証してみます。

  • 対象の要素が動的に生成される
  • 対象の要素はクリックすると半透明になる

scriptタグ内に以下のコードを記述します。

// getElementsByClassName()で取得したものを変数化
const cards = document.getElementsByClassName('card');

// cardクラスを持つ要素を生成
const cardWrap = document.getElementById('cardWrap');
const card04 = document.createElement('div');
card04.id = 'card04';
card04.classList = 'card';
card04.textContent = 'カード4';
cardWrap.appendChild(card04, cardWrap);

// for of文でcards内の要素全てにクリックイベントを与えてまわる
for (let item of cards) {
  item.addEventListener('click', () => {
    event.currentTarget.classList.add('isSemitransparent');
  });
};

動的に生成した緑のカードにもクリックイベントが適用されます。

console.log で確認するとわかる事ですが、 getElementsByClassName() で要素を取得すると HTMLCollection が返ってきます。
HTMLCollection は動的で実際の Node の要素を反映するため、変数に格納して参照しても問題ありません。

ちなみに querySelectorAll() で同じ事をしようとすると、緑のカードだけクリックイベントが付きません。

まとめ

querySelectorAll() を使う時には、取得したい要素に動的な処理が加わることがないか注意しよう。
取得したい要素に動的な処理が加わる場合は、getElementsBy〜() を使うとよさそう。

余談

jQueryでは $() で要素を取得できますが、これも変数化すると querySelectorAll() と同じ問題が起こるので注意しましょう。

また、jQueryは$('.card').on('click', function(){ 〜 処理 〜 });のような記述で.cardを持つ全ての要素にクリックイベントを適用できますが、querySelectorAll()やgetElementsBy〜()で同じ事をやろうとするとエラーを返されます。

const cards = document.querySelectorAll('.card');
cards.addEventListener('click', () => {
  event.currentTarget.classList.add('isSemitransparent');
});
// => cards.addEventListener is not a function
const cards = document.getElementsByClassName('card');
cards.addEventListener('click', () => {
  event.currentTarget.classList.add('isSemitransparent');
});
// => cards.addEventListener is not a function

イベント処理を加える addEventListener()メソッド は単体の要素に対して有効ですが、要素の集合に対しては使えません。jQueryの記述ではあたかも一括でイベント処理を適用しているように見えますが、裏側ではfor文による地道な反復処理が行われているようです。

参考:どうして!?document.querySelectorAll(selector).addEventListener()が動かないわけ

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

iOSのショートカットでWebサイトの二段階認証コード入力の手間を軽減する

はじめに

Webサイトの二段階認証コード(以下、認証コード)を入力するのは、以下のステップを踏む必要があり、なかなか手間がかかります。

  • Webサイトから二段階認証のアプリ(以下、認証アプリ)に切り替えて認証コードを取得する。
  • その後、Webサイトに戻って、認証コードをフォームに入力する。

上記の手間をiOSのショートカットを使って自動化することで軽減する方法について紹介したいと思います。

前提

  • iOSのショートカットを使用するため、iOS12以上が必要になります。
    • ただし、本記事はiOS13.3で検証を行なっています。
  • 認証アプリのインストールが必要です。本記事のショートカットでは、以下のアプリを例に説明しています。
    • Google Authenticator
    • Microsoft Authenticator
    • Authy

ショートカットの雛形

Webサイトの共有ボタンからショートカットを実行したいため、作成するショートカットはSafariのWebページを受け入れる設定を行います。

このWebページを受け入れる設定のショートカットを作成するにあたり、ショートカットアプリのギャラリーにあるJavaScriptショートカットのサンプルから複製する方法が手っ取り早いです。

例えば、JavaScriptショートカットのWebページを編集のショートカットを追加します。その後、このショートカットを編集し、JavaScriptを実行する部分のコードを一旦completion()のみにします。

個別のWebサイトに対するショートカット例

個別のWebサイトに対して認証コードを入力するケースについて、Amazonを例に説明します。
Amazonの認証コードの入力画面では、以下の要素が宣言されています。

  • auth-mfa-otpcodeのID属性を持つインプット要素(認証コードの入力フォーム)
  • auth-signin-buttonのID属性を持つボタン要素(サインインボタン)

上記の要素に対して、ショートカットからJavaScriptを実行し、インプット要素に認証コードを設定した後にサインインボタンをクリックすると手間を軽減できそうです。

ショートカットの主な手順

  • SafariのWebページを受け入れる
    • ここは、テンプレートの雛形で作成済みです。
  • スクリプティングのアプリを開くを追加する。
    • Amazonの認証コードを発行する認証アプリ(ここでは、Google Authenticatorアプリ)を起動します。
    • 認証アプリ起動後に、認証コードをクリップボードにコピーします。
  • スクリプティングの待機を追加します。
  • JavaScriptを実行のコードで以下の処理を行います。
    • クリップボードにコピーした認証コードをWebサイトのフォームに設定する。
      • auth-mfa-otpcodeのID属性を持つインプット要素を取得し、インプット要素の値に認証コードを設定します。
    • サインインボタンをクリックする。
      • auth-signin-buttonのID属性を持つ要素を取得し、click()を呼び出します。
    • JavaScriptの実行を完了する。
      • completion(true)を呼び出します。

JavaScriptの実行コード

// 入力フォームに認証コードを設定
document.getElementById("auth-mfa-otpcode").value = '<クリップボードの変数>';

// サインインボタンをクリック
document.getElementById("auth-signin-button").click()

// JavaScriptの実行を完了
completion(true)

作成したショートカット

iCloudの共有リンク

汎用的なWebサイトに対するショートカット例

個別のWebサイトに対してショートカットを作成すると自動化が最適化しやすいですが、二段階認証したいサイトが多いとショートカットの作成に時間がかかります。

そこで、汎用的なWebサイトに対しても認証コードの入力の手間を軽減する方法について説明します。

汎用版 ver1

ショートカットのメニューから任意の認証アプリの起動・認証コードをコピーし、認証コードの設定とボタンのクリックは手動で行う例です。

ショートカットの主な手順

  • SafariのWebページを受け入れる
    • ここは、テンプレートの雛形で作成済みです。
  • スクリプティングのメニューから選択を追加します。
    • メニューに複数の認証アプリを追加します。(この部分は個人の環境によって任意のアプリを指定します。)
      • Google Authenticator
      • Microsoft Authenticator
      • Authy
    • それぞれの認証アプリの項目に対して、スクリプティングのアプリを開くを追加し、アプリの設定を行います。
  • スクリプティングの待機を追加します。
  • JavaScriptを実行のコードで以下の処理を行います。
    • JavaScriptの実行を完了する。
      • completion(true)を呼び出します。

JavaScriptの実行コード

特に認証コードの設定とボタンのクリックはショートカット上では行わないため、単純に完了のコールバック関数を呼び出す処理のみになります。

// JavaScriptの実行を完了
completion(true);

認証コードの取得以降では、手動で認証コードのインプット要素にクリップボード上の認証コードをペーストし、その後、ボタンを押下します。

作成したショートカット

iCloudの共有リンク

汎用版 ver2

汎用版 ver1では、手動で認証コードをインプット要素にペーストする必要があり、若干煩わしいです。
ただし、認証コードのインプット要素は、各サイトによってID属性やクラス属性、type属性の値が統一されていないため、特定がしづらいです。

苦肉の策として、ここではインプット要素にフォーカスを当てた時に、フォーカスされた要素に認証コードを設定する方法について説明します。

ショートカットの主な手順

  • SafariのWebページを受け入れる
    • ここは、テンプレートの雛形で作成済みです。
  • スクリプティングのメニューから選択を追加します。
    • メニューに複数の認証アプリを追加します。(この部分は個人の環境によって任意のアプリを指定いsます。)
      • Google Authenticator
      • Microsoft Authenticator
      • Authy
    • それぞれの認証アプリの項目に対して、スクリプティングのアプリを開くを追加し、アプリの設定を行います。
  • スクリプティングの待機を追加します。
  • JavaScriptを実行のコードで以下の処理を行います。
    • 入力要素を取得し、focusイベントを追加する。
      • focusイベント時に、ターゲット要素にクリップボード上の認証コードを設定する
    • JavaScriptの実行を完了する。
      • completion(true)を呼び出します。

JavaScriptの実行コード

入力要素に対して、focusイベントを追加し、focusイベント時にターゲット要素に対して認証コードを設定します。ボタンの要素のクリックはJavaScriptでは行いません。
※ 認証コード以外のインプット要素にフォーカスした場合にも認証コードが設定されてしまうので、利用には注意してください。

// 入力要素に対して、focusイベントを追加する
// focusイベント時にターゲット要素にクリップボード上の認証コードを設定する。
var elms = document.querySelectorAll("input");
elms.forEach((elm) => {
  elm.addEventListener('focus', (event) => {
    event.target.value = '<クリップボードの変数>'
  });
});

// JavaScriptの実行を完了
completion(true);

認証コードの取得以降では、手動で認証コードのインプット要素にフォーカスを当てることでクリップボード上の認証コードを設定し、その後、ボタンを押下します。

作成したショートカット

iCloudの共有リンク

さいごに

iOSのショートカットを使うことで、認証コード入力の手間を少しは軽減できるようにはできたかと思います。さらに手間を軽減するには、認証アプリを起動して認証コードを取得するのではなく、ローカル あるいは リモートサーバ上(SSH経由)で認証コードを生成・取得すると良いのではと考えています。

参考

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

基礎の基礎 JavaScript 101

Udemyで学んだJavaScriptのメモ

Udemyの講座でJavaScriptの基礎を学んだのでメモ用に記録。

varで変数を作成。

プログラミングには欠かせない変数。
JavaScriptでは、variablesの頭文字をとって var を使用するよ。

basic.js
var a = 10;

変数って難しい。って思われるかも知れないけど。要するに a という変数に10という数字を代入しましたよ。的なこと。

ちなみに今回 変数a に10を代入したので、次回変数aに代入する時は var の記載はなくても大丈夫みたい。

簡単な関数とプロパティ

  • alert()  → ウィンドウに出力する
  • .console.log()  → コンソール内に出力する
  • typeof()  → データ型を調べる
  • prompt() → 関数内の文字列を表示して、入力を要求する
  • .length → その文字列の長さを出力
  • .slice(x,y) → x番目からy番目の手前までの文字列を切り出す
  • .toUpperCase() → 文字列を全て大文字にする
  • .toLowerCase() → 文字列を全て小文字にする
  • Math.ceil() → 小数点以下の切り上げ
  • Math.floor() → 小数点以下の切り捨て
  • Math.round() → 四捨五入
  • Math.random() → ランダムな数を生成 0<=n<1 の少数16桁

数字の取り扱い

演算子 処理
+ 足し算
- 引き算
* 掛け算
/ 割り算
% 余った数を求める
== イコール
=== イコール
!== ノットイコール

※注意
"==" は、データ型が同じじゃなくても良い。
"==="は、データ型が同じでなければならない。

ちなみに、簡単に記述したい場合は下記。

qiita.js
var number = 5;

// +1 したい場合

number++;

//  +3したい場合

number+=3;

//  変数xの数だけ足していく場合

var x = 10;
number += x;


JavaScriptで関数定義

例えば、30行くらいのコードがあって、ある処理をする時に毎回30行のコード書いてたら疲れちゃいますよね。指も痛くなるし、コードも長くなるし、時間ももったいない。

そんな時は、関数を作って、その30行のコードを関数にぶち込めば良いのです。

関数を作ると、なんと1行で処理できちゃうんですね。30行のコードが毎回1行に短縮される嬉しさ。嬉しきことこの上なし。

基本的な関数定義

qiita.js
//短縮される前のコード

alert("こんにちは");
alert("きびなごです。");
alert("今日も");
alert("いい天気ですね!!");

//firstGreetingの関数を定義

function firstGreeting() {
   alert("こんにちは");
   alert("きびなごです。");
   alert("今日も");
   alert("いい天気ですね!!");
}

//関数使うとき

firstGreeting();


めっちゃ簡略化されましたよね。ちなみに、関数作るときは{}のかっこの後に ; (セミコロン)はいらないです。関数を使用する時は、いつも通りいりますけど。

Inputがある関数定義

ちなみに関数を使用するときに firstGreeting();のかっこ内にデータをinputすることもできます。これをどうやら引数と呼ぶみたい。

qiita.js

//firstGreetingのデータ入力つき関数を定義

function firstGreeting(name) {
   alert("こんにちは");
   alert("きびなごです。");
   alert(name+"さん")
   alert("今日も");
   alert("いい天気ですね!!");
}

//関数使うとき

firstGreeting("もも太郎");


この場合は、関数内でfirstGreeting(name){}と定義し、firstGreeting("もも太郎")と使用することで、nameという変数に"もも太郎"を入れますよー。ってなるわけですね。説明下手か。。

Outputのある関数

上記のfirstGreeting関数は、関数内で処理が完結しました。

でも、実際に関数を使用していくと、関数で計算とかして、その計算結果を他の処理で使いたいってことあると思うんです。

そんな時は、戻り値(関数でできた数値等を関数外でも使えるようにする?)ものを使って、変数を作成することもできるのです。(説明が難しい)
これを戻り値というみたい。

例えば、自分の所持金 変数Moneyを使用して、150円の商品を買えるだけ買った後に、お釣りは何円かを計算する。

qiita.js

//おつりを計算する関数を作成
// returnを使用して、戻り値がでるようにする

function change(Money){

   return Money % 150;

}

//1000円持っていた場合

change(1000);

//  !!ここが大事!!
//  戻り値を使用すると、関数を代入して変数を作れる

var zankin =  change(1000);

//  この変数 zankin は他の計算にも使用することができるのがポイント
//  変数 chokinは定義してないけど、他の数式にも使えるのよね。

var zenZaisan = chokin + zankin; 


JavaScriptでランダムな数字を生成する

次にランダムな数字を生成するコード。ランダムな数って結構使うみたいだから、ここで覚えちゃいます。

今回はサイコロの出目を想定して 1-6 の数字をランダムに生成できるようにします。

randomNumbers.js
// 0-0.9999999999999999 までのランダムな数字を生成

var n = Math.random();

// 6 を掛けて +1 して、 1-6.99999999999999 のランダムな数字にする

n = n * 6 ;

// 小数点を切り捨てて、1-6 までの整数にする

n = Math.floor(n);



これでサイコロの目をランダムに生成するコードが完成しましたよと。

IF ELSE 文で条件分岐

次にIF ELSE文を使って条件分岐です。

もし〜がTRUEであったら、処理Aを実行する。もし~がFALSEなら処理Bを実行する。といったものですね。

if(もし)と else(それ以外)を使用します。

例えば、test の点数で
31点以上 → 合格です。
30点以下 → 赤点です。
と表示する時は。

testScore.js
// テストのスコアを入力してもらう

var test = prompt("テストの点数を入力してください。");

// 条件定義

if (test >= 31) {
  // 31点以上の場合
  console.log("合格です。");

}else{
  // 31点以上 に当てはまらない場合
  console.log("赤点です。");
}

これにてJavaScriptの基礎の基礎終了。

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

javascriptでclickしたらclass追加

やりたいこと

  • clickしたらevent発火
  • classを追加してcssを変更

コード

<html>
<body>
<div id="ababa"></div>
</body>
</html>
<script>
    document.getElementById('ababa').addEventListener('click', () => {
      document.getElementById('ababa').style.background = 'blue';
    });
  </script>

解説

DocumentはオブジェクトでgetElementByIdがメソッドになる。getElementByIdでdocument内にある指定したidと一致したものをとってくる。そのとってきたidに対してaddEventListenerメソッドを使っていてclickで発火するようにしている。

Document
https://developer.mozilla.org/ja/docs/Web/API/Document

getElementById
https://developer.mozilla.org/ja/docs/Web/API/Document/getElementById

addEventListener
https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener

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

JavaScriptでfor文(foreach)内でのgetElementsByClassNameの使用法

はじめに

趣味で作っているツイッタークローンにおいて、ツイート一覧にマスオーバーさせると背景色が変化するjsの処理を書きたかった。

JSをあてるPHPファイル

3行目のtweetcardが対象のclass名です。
注)実際は生のphpファイルではなくテンプレートエンジンのbladeを使用しています。

home.php
@if ($posts)
  @foreach($posts as $post)
    <div class="card tweetcard">
      <div class="card-body alert tweet">
        {{ $post->name }}
        {{ $post->created_at }}
        <br>
        {{ $post->content }}
      </div>
    </div>

間違った書きかた

samle.js
const tweetcard = document.getElementsByClassName('tweetcard');

  //クリックイベントで背景色を変える
  tweetcard.addEventListener('mouseenter', () => {
      tweetcard.style.backgroundColor  = "#e6ecf0";
  }, false);

}
  //クリックイベントで背景色を戻す
  tweetcard.addEventListener('mouseleave', () => {
      tweetcard.style.backgroundColor = "white";
  }, false);
}

エラーが出ました。
リファレンスを確認してみます。下記抜粋。

getElementsByClassName メソッドは、指定されたクラス名をすべて持つすべての子要素の配列風オブジェクトを返します。 引用元:MDN web docs

getElementsByClassNameで取得した要素は配列だったのですね!

だったらfor分で回してあげればいいですね。

正しい書き方

samle.js
const tweetcard = document.getElementsByClassName('tweetcard');

//mouseenter
for(let i = 0; i < tweetcard.length; i++){

  //クリックイベントで背景色を変える
  tweetcard[i].addEventListener('mouseenter', () => {
      tweetcard[i].style.backgroundColor  = "#e6ecf0";
  }, false);

}
//mouseenter
for(let i = 0; i < tweetcard.length; i++){

  //クリックイベントで背景色を戻す
  tweetcard[i].addEventListener('mouseleave', () => {
      tweetcard[i].style.backgroundColor = "white";
  }, false);
}

これでマウスをかざした領域の背景色が変更され、マウスを外せば背景色が元に戻るようになりました。

参考

MDN web docs

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

node.jsでAbemaをダウンロードする

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

【JavaScript】JavaScript、その前に〜ECMAScriptとは?

ECMAScriptとは?

読み方はエクマスクリプトです。

誕生の理由

まず、JavaScriptは旧Netscape Communications社(1998年、AOLにより買収)が開発したプログラミング言語です。自社ブラウザであるNetscape Navigatorにおける使用が想定されていました。

その手軽さから急速に普及していきましたが、登場初期は各ブラウザベンダーによる独自拡張が行われ、ブラウザ間の互換性が極めて低い状態でした。(これはブラウザ戦争と呼ばれています。ちなみに、Microsoft社による拡張はJScriptと呼ばれています。)

そのため、旧Netscape Communications社は外部団体に標準仕様の策定を依頼しました。その団体がEcma Internationalです。

そしてEcma Internationalにより標準化された仕様がECMAScriptです。

バージョンアップ

引用はこちらの記事からです。仕様策定の流れなども分かりやすく説明されています。

ECMAScript

ECMAScriptは、Ecma InternationalにてECMA-262という規格番号で標準化されています。また、拡張機能としてECMA-357ECMA-402ECMA-404という規格も存在します。

そして、仕様の更新が「標準」として承認される度に新しいEditionがリリースされます。

ただ、最新のECMAScriptの仕様はGitHubに公開され、日々更新されています。

このように更新ごとにバージョン番号を付けずに、常に最新版を公開する仕様のことをLiving Standardと呼びます。

ECMAScriptはLiving Standardですが、これに加えてECMAScript 2017のようにバージョン番号をつけたものも公開されています。 このバージョン付きECMAScriptは、毎年決まった時期のドラフトを元にしたスナップショットのようなものです。

Living Standardは、ブラウザの変化に柔軟に対応するためなんですね。

ブラウザなどに実際にJavaScriptとして実装される際には、Living StandardのECMAScriptを参照しています。 これは、ブラウザ自体も日々更新されるものであり、決まった時期にしかリリースされないバージョン付きよりもLiving Standardの方が適当であるためです。

ECMAScript2015の策定までは時間がかかっていましたが、ECMAScript2016からは策定プロセスを改善し、リリースまでの間隔も早くなっているそうです。

呼び名の違い

ECMAScript2015(ES6)を元に説明します。

ECMAScript2015とES6は同じ仕様を指しています。
どうして数字部分が違うのか気になりました。

呼び名の由来

ECMAScript2015の2015は仕様の発行年を表しています。仕様の名称に年号を使うようになったのは、ECMAScript2015からです。

ES6の6は規格のEditionを表しています。ES6はECMA-262 6th Editionの口語的な略称です。

どちらが正しいのか

現在も両方の呼び方が使われていますが、推奨されている呼び方はECMAScript2015もしくはES2015だそうです。

Some ECMAScript Explanations and Stories for Dave

Mozillaについて

現在、Netscape関連の資産・権利は、AOLにより設立された非営利団体Mozilla Foundationに引き継がれています。

そのため、Mozillaの公式サイトでもあるMDN Web Docsは、JavaScriptのリファレンスとして真っ先に挙げられています。

参考リンク

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

FPS視点ラジコンで冬空の下をドライブしよう。ラジコンの作り方[obniz編]

はじめに

LEGOとobnizでラジコンを作ったので、その作り方を紹介します。
今回は動力部分『obnizでのモーター制御とコントローラー』の作り方を紹介します。

参考

冬空の下でドライブしてみた

タイトルに偽り無し。ちゃんとドライブしました。ラジコンにはカメラを付けているので、FPS視点(ラジコン視点)でも楽しめます。いやぁ~ラジコンたのしい

完成図

このようにIoTデバイス(ラズパイ、obniz)と、給電用のバッテリーを2つ搭載しています。

今回紹介するのはobnizのモーター制御

後輪のモーター制御には、obnizを使用しています。

左車輪のモーターをobnizの0,1に、右車輪のモーターをobnizの10,11に接続します。赤と黒の配線を図のように接続します。

※今回使用したモーターはこちらです → 赤い歯車モーター

制御の概要

Windows上にNodejsでWebサーバーを用意し、obnizを制御するスクリプトcontroler.htmlを用意します。 ※Webサーバーは必須ではありません。

controler.htmlは、githubに置きました → controler.htmlのソース
11行目の『obniz-ID』を持っているobnizのIDに書き換えます。これで準備完了です。あとはcontroler.htmlをブラウザで開けばコントローラーとして使えます。

コントローラーの説明

コントローラーは、用意したWebサイト(例:http://localhost/controler.html)にアクセスすると開きます。コントローラー画面はこんな感じです。

操作は、画面に指をタッチします(マウスでは操作できません)
指と中心点との位置関係(指の距離、指の角度)でラジコンの動きを制御します。

これで説明は以上です。

おまけ

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

obnizでドライブしたい!FPS視点ラジコンを作ろう[obniz編]

はじめに

LEGOとobnizでラジコンを作ったので、その作り方を紹介します。
今回は動力部分『obnizでのモーター制御とコントローラー』の作り方を紹介します。

参考

冬空の下でドライブしてみた

タイトルに偽り無し。ちゃんとドライブしました。ラジコンにはカメラを付けているので、FPS視点(ラジコン視点)でも楽しめます。いやぁ~ラジコンたのしい

完成図

このようにIoTデバイス(ラズパイ、obniz)と、給電用のバッテリーを2つ搭載しています。

今回紹介するのはobnizのモーター制御

後輪のモーター制御には、obnizを使用しています。

左車輪のモーターをobnizの0,1に、右車輪のモーターをobnizの10,11に接続します。赤と黒の配線を図のように接続します。

※今回使用したモーターはこちらです → 赤い歯車モーター

制御の概要

Windows上にNodejsでWebサーバーを用意し、obnizを制御するスクリプトcontroler.htmlを用意します。 ※Webサーバーは必須ではありません。

controler.htmlは、githubに置きました → controler.htmlのソース
11行目の『obniz-ID』を持っているobnizのIDに書き換えます。これで準備完了です。あとはcontroler.htmlをブラウザで開けばコントローラーとして使えます。

コントローラーの説明

コントローラーは、用意したWebサイト(例:http://localhost/controler.html)にアクセスすると開きます。コントローラー画面はこんな感じです。

操作は、画面に指をタッチします(マウスでは操作できません)
指と中心点との位置関係(指の距離、指の角度)でラジコンの動きを制御します。

スクリプトの説明

controler.htmlのJavaScriptについて補足します。

<body ontouchmove="event.preventDefault()">

ontouchmove・・・は、iOSでページ全体のスクロールを無効にしています。コントローラーを指で操作したときにスクロールしないようにしています。

var motor1 = obniz.wired("DCMotor", {forward:0, back:1}); // 左車輪のモーター
var motor2 = obniz.wired("DCMotor", {forward:10, back:11}); // 右車輪のモーター

forward,backに設定している番号は、obnizデバイスの端子の番号です。

let x1 = div_x ;
let y1 = div_y ;
let x2 = event.changedTouches[0].pageX - div_x ;
let y2 = event.changedTouches[0].pageY - div_y ;

x1,y1は、コントローラー用の円の中心点です。
x2,y2は、画面にタッチしている指の座標です。中心点(x1,x2)を原点とした場合の座標にしています。

let mpow  = Math.round( Math.sqrt( Math.pow( x2-x1, 2 ) + Math.pow( y2-y1, 2 ) ) ) ; // 2点間の距離
if( mpow >=100 ) mpow = 100;

円の中心(x1,y1)とタップした指の位置(x2,y2)間の距離を算出します。距離が100を超える場合は、値を100とします。この値が、モーターの出力になります。数値が高いほどモーターの回転速度が上がりラジコンが速く移動します。

let angle = ( Math.atan2( y2 - y1, x2 - x1 ) * 180 / 3.1415 ).toFixed(2);

タップした指の位置(x2,y2)の角度を算出します。

el_hitarea.addEventListener('touchend', function(event) {
    console.log("[debyg] now_x=0 now_y=0");
    motor1.stop();
    motor2.stop();
}, false);

タップした指が画面から離れるとモーターが止まります。


これで説明は以上です。

関連リンク

おまけ

そーいえば年明けてた。2020年の干支はネズミだそうです。

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

【Nuxt.js】カスタムディレクティブ基礎編:とりあえず使ってみよう!

前置き

スクリーンショット 2020-01-03 18.42.11.png

Vue.jsのカスタムディレクティブをNuxt.jsで!
ローカルverとグローバルverで、
どちらでも使いやすく簡単な例で紹介。
今回は背景色を変えるだけです。

…とはいえfilterはよく使われますが
実際こちらはなかなか見かけませんね。
カスタムディレクティブの良い使い方があれば
教えていただけると嬉しいです…?

※併用する場合
同じディレクティブ名にしてしまうと
ローカルに記載された方が優先されます。
名前は分けましょう!

ローカルver

directivesで命名した物を
v-{{ name }}で記載するだけ!
今回はdiv全体に背景色をつけるため
divにv-bgを指定しております。

index.vue
<template>
  <div v-bg>
    <p>divで囲った部分の背景が変わる</p>
  </div>
</template>

<script>
export default {
 directives: {
   'bg': {
     bind(el, binding, vnode) {
       el.style.backgroundColor = 'lightgreen';
     }
   }
 },
}
</script>

グローバルver(JSファイル)

◾️パターン1
・/pluginsにjsファイルを追加
・nuxt.config.jsのpluginsに記載
・template内にv-{{ name }}を書くだけ!

file
pages/
--| index.vue

plugins/
--| bg.js

nuxt.config.js
bg.js
import Vue from 'vue'

Vue.directive('bg', {
 bind(el, binding, vnode) {
   el.style.backgroundColor = 'lightgreen';
 }
})
nuxt.config.js
plugins: [
   '~plugins/background.js'
 ],
index.vue
<template>
  <div v-bg>
    <p>divで囲った部分の背景が変わる</p>
  </div>
</template>
◾️パターン2-1
jsで直接値の指定はせず、
使用箇所で好きな値を指定できます。
bg.js
// 変更前
el.style.backgroundColor = 'green';

// 変更後
el.style.backgroundColor = binding.value;
index.vue
// 変更前
<div v-bg>

// 変更後
<div v-bg="'green'">

◾️パターン2-2
スクリーンショット 2020-01-03 19.04.43.png
argを使用した場合です。
argはpropsのようなイメージですね。
いくつもデータを渡せます♪
https://jp.vuejs.org/v2/guide/custom-directive.html#ディレクティブフック引数

bg.js
import Vue from 'vue'

Vue.directive('bg', {
 bind(el, binding, vnode) {
   // バインドされたargが文字列backgroundだった場合の処理
   if (binding.arg === 'background') {
     el.style.backgroundColor = binding.value;
   // そうでない場合の処理
   } else {
     el.style.color = binding.value;
   }
 }
})
index.vue
<template>
  <div>
    // argは:background部分
    <div v-bg:background="'lightgreen'">
      <p>背景が変わる</p>
    </div>
    <div v-bg:color="'green'">
      <p>文字色が変わる</p>
    </div>
  </div>
</template>

このアカウントでは
Nuxt.js、Vue.jsを誰でも分かるよう、
超簡単に解説しています??
これからも発信していくので、
ぜひフォローしてください♪

https://twitter.com/aLizlab

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