20200604のJavaScriptに関する記事は29件です。

入門: Vue.jsでCRUDアプリケーションを作る

はじめに

フロントエンド開発、最近はやはりJSで書くのが基本だとは思うけどReactやAngularは少し敷居が高いのでVue.jsに手を出してみました。
とりあえず、自分へのメモの意味合でVue JSのCRUDアプリケーションのチュートリアルを書いてみます。CRUDが書ければあとは必要に応じて普通に拡張できるでしょうし。たぶん。

今回はAPIサイドをJavaのQuarkusで、フロントエンドをVue.jsで書いています。

サーバサイドをQuarkusで作成

プロジェクト作成

まずサーバサイドを作成します。Mavenでまずは以下のようにQuarkusプロジェクトを生成します。なお、mavenのバージョンが3.6.2より低い場合は事前に上げておくこと。

$ mvn io.quarkus:quarkus-maven-plugin:1.3.0.Final:create \
    -DprojectGroupId=crud \
    -DprojectArtifactId=crud-server \
    -DclassName="crud.ItemResource" \
    -Dpath="/items"

つづいて、必要なプラグインをインストール。今回はDB周りとJAX-RS周りを入れています。

$ cd crud-server/
$ ./mvnw quarkus:add-extension -Dextensions="quarkus-hibernate-orm-panache,quarkus
-jdbc-h2,quarkus-resteasy-jackson,quarkus-resteasy-jsonb"

ビルドの確認

$ ./mvnw compile quarkus:dev

永続化層の作成

まずは、application.propertiesの設定を変更。

# DB Config
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:
quarkus.hibernate-orm.database.generation = drop-and-create

続いてEntityの作成。getter/setterは長いので省略。

@Entity
public class Item {
    @Id
    @GeneratedValue(generator = "UUID")
    @GenericGenerator(
            name = "UUID",
            strategy = "org.hibernate.id.UUIDGenerator"
    )
    @Column(name = "id", updatable = false, nullable = false)
    private UUID id;
    private String name;
    private int price;

    public Item() { }

    public Item(UUID id, String name, int price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
    public UUID getId() {
        return id;
    }
    public void setId(UUID id) {
        this.id = id;
    }
-  -
}

最後にRepository.

@ApplicationScoped
public class ItemRepository implements PanacheRepository<Item> {

    public Item findById(UUID id) {
        return this.find("id", id).list().get(0);
    }

    public void update(UUID id, Item item) {
        var x = findById(id);
        x.setName(item.getName());
        x.setPrice(item.getPrice());
    }
}

エンドポイント作成

エンドポイントとしてJAX-RSで簡単なREST APIを作成する

@Path("/items")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public class ItemResource {
    @Inject
    ItemRepository repository;

    @GET
    @Path("{id}")
    public Item get(@PathParam("id") UUID id) {
        return repository.findById(id);
    }
    @GET
    public List<Item> list() {
        return repository.listAll();
    }
    @POST
    public void create(Item item) {
        repository.persist(item);
    }
    @PUT
    @Path("{id}")
    public void update(@PathParam("id") UUID id, Item item) {
        repository.update(id, item);
    }
    @DELETE
    @Path("{id}")
    public void delete(@PathParam("id") UUID id) {
        repository.delete("id", id);
    }
}

CROSの設定

このままではポート番号が違うため例えローカルホストであってもCROSでエラーになる。
そのため、CROS対応に設定をapplication.propertiesに追加する. この例ではQuarkus側がポート3000, Vue.js側が8080で起動する想定。

本番環境では構成によっては同一サーバで動くと思うので、その場合はCROSに関する設定はしなくて良い。

# HTTP Config
quarkus.http.port=3000
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:8080,http://172.19.27.127:8080
quarkus.http.cors.methods=GET,PUT,POST,DELETE

動作確認

サーバサイドの動作確認を行います。

$ ./mvnw compile quarkus:dev

crulでAPIを実行。

$ URL=http://localhost:3000/items
$ JSON_TYPE="Content-Type:application/json"

$ curl -XGET ${URL} -H $JSON_TYPE
[]

$ curl -XPOST ${URL} -H $JSON_TYPE -d '{"name" : "new item 02", "price" : 128}'
$ curl -XPOST ${URL} -H $JSON_TYPE -d '{"name" : "new item 02", "price" : 128}'
$ curl -XGET ${URL} -H $JSON_TYPE
[{"id":"57cda117-474a-48bf-85f3-8f83dd33dac9","name":"new item 01","price":64},{"id":"bc5cc9fd-0ba9-4b02-91e8-93fdf9ed59fd","name":"new item 02","price":128}]

$ curl -XPUT ${URL}/"57cda117-474a-48bf-85f3-8f83dd33dac9" -H $JSON_TYPE -d '{"name" : "new item 01", "price" : 1024}'
$ curl -XGET ${URL} -H $JSON_TYPE
[{"id":"bc5cc9fd-0ba9-4b02-91e8-93fdf9ed59fd","name":"new item 02","price":128},{"id":"57cda117-474a-48bf-85f3-8f83dd33dac9","name":"new item 01","price":1024}]

$ curl -XDELETE ${URL}/"bc5cc9fd-0ba9-4b02-91e8-93fdf9ed59fd" -H $JSON_TYPE
$ curl -XGET ${URL} -H $JSON_TYPE
[{"id":"57cda117-474a-48bf-85f3-8f83dd33dac9","name":"new item 01","price":1024}]

APIが適切に動作してるのが分かります。OpenAPIのUIで確認したいなら下記のURLから確認。
http://localhost:3000/swagger-ui/

クライアントサイドをVue.jsで作成

続いて本命のクライアントサイドアプリをVue.jsで実装します。

プロジェクトの作成

まずはvue-cliのインストールを行います。

$ yarn global add @vue/cli

続いてプロジェクトの作成。選択肢はマニュアルを選択してRouterを追加します。面倒なので今回はLintも外します。後の選択肢は好みで。

$ vue create crud-client
Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◯ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

開発サーバを立てて動作確認をします。

$ cd crud-client
$ yarn serve

ネットワークライブラリの追加

JS標準のFetch APIだと記述が冗長なのでaxiosを使います。

$ yarn add axios vue-axios

UIフレームワークの追加

UIフレームワークとしてBootstrapを入れます。合わせてリッチなアラートを提供するsweetalertもインストールします。

$ yarn add bootstrap vue-sweetalert2

Vue Configの設定をする

通常、本番環境やテスト環境と開発環境ではAPIの接続先が異なると思います。APIサーバをローカルに立てたりするので。
なので、開発サーバの設定を変更して、そこで接続先を書き換えます。ルートディレクトリにvue.config.jsを追加して以下のように修正します。

また、タイトルも同様にここで設定するので追加しておきます。

vue.config.js
module.exports = {
    devServer: {
        proxy: {
            "^/items": {
                target: "http://localhost:3000",
                ws: false,
                pathRewrite: {
                    "^/items": "/items"
                }
            }
        }
    },
    pages: {
        index: {
            entry: 'src/main.js',
            title: 'My CRUD Apps',
        }
    }
};

ライブラリのインポート

先ほど、yarnでインストールしたライブラリを読み込むためにmain.jsを以下のように修正します。

src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import axios from 'axios';
import VueAxios from 'vue-axios';
import VueSweetalert2 from 'vue-sweetalert2';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
import '../node_modules/sweetalert2/dist/sweetalert2.min.css';

Vue.config.productionTip = false
Vue.use(VueSweetalert2);
Vue.use(VueAxios, axios);

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

CRUD画面を作る

本題のCRUD画面を作ります。メイン画面はviews配下に作るのが作法のようです。
とりあえず不要なファイルを消しましょう。

$ rm src/views/About.vue
$ rm src/views/Home.vue

作成画面

Script側で定義したデータモデルに対してリアクティブにテンプレート側の値が変更されます。
サーバ側でエラーが発生すればmessageにメッセージとして表示されるようにしています。
thisを多用しているので無名関数をアロー演算子ではなくfunctionで作成してしまうとスコープが変わって動かなくなるので注意。

また、作成完了時には一覧画面に遷移しますが、遷移後に作成完了のメッセージを一覧画面に表示する代わりにSweetAlertでメッセージを出しています。
これは、Vue.jsではJavaEEやRailsのFlush領域のように画面遷移に際して値をバックエンドで受け渡す仕組みが無いためエラーメッセージを同じUIで出すのは無駄に困難です。なので似たUXとなるリーズナブルなUIを選択しています。

src/views/Create.vue
<template>
  <div class="container">
    <div class="card">
      <div class="card-header">
        <h3>Add Item</h3>
      </div>
      <div class="card-body">
        <form v-on:submit.prevent="addItem">
          <div v-show="message" class="alert alert-danger">{{message}}</div>
          <div class="form-group">
            <label>Item Name:</label>
            <input type="text" class="form-control" v-model="item.name" />
          </div>
          <div class="form-group">
            <label>Item Price:</label>
            <input type="text" class="form-control" v-model="item.price" />
          </div>
          <div class="form-group">
            <input type="submit" class="btn btn-primary" value="Add Item" />
          </div>
        </form>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  components: {
    name: "AddItem"
  },
  data() {
    return {
      item: {},
      message: ""
    };
  },
  methods: {
    addItem() {
      let uri = "/items/";
      this.axios
        .post(uri, this.item)
        .then(() => {
          this.$swal({
            icon: "success",
            text: "Created Success!"
          });
          this.$router.push({ name: "Index" });
        })
        .catch(error => {
          this.message = `status: ${error.response.status}, message: ${error.response.data}`;
        });
    }
  }
};

一覧画面

一覧画面では画面を表示した際に呼ばれるcreatedメソッドの中で、一覧を取得するAPIを呼んでいます。
また、削除のAPIコールと編集への遷移も行っています。

src/views/Index.vue
<template>
  <div>
    <h1>Items</h1>

    <table class="table table-hover">
      <thead>
        <tr>
          <td>ID</td>
          <td>Item Name</td>
          <td>Item Price</td>
          <td>Actions</td>
        </tr>
      </thead>

      <tbody>
        <tr v-for="item in items" :key="item._id">
          <td>{{ item.id }}</td>
          <td>{{ item.name }}</td>
          <td>{{ item.price }}</td>
          <td>
            <router-link :to="{name: 'Edit', params: { id: item.id }}" class="btn btn-primary">Edit</router-link>
          </td>
          <td>
            <button class="btn btn-danger" v-on:click="deleteItem(item.id)">Delete</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: []
    };
  },
  created: function() {
    this.fetchItems();
  },
  methods: {
    fetchItems() {
      let uri = "/items";
      this.axios.get(uri).then(response => {
        this.items = response.data;
      });
    },
    deleteItem(id) {
      let uri = "/items/" + id;
      this.axios.delete(uri).then(() => {
        this.fetchItems();
      });
    }
  }
};
</script>

編集画面

編集画面は基本的に作成画面と同様です。違いはサーバサイドにIDで値を問い合わせてるかくらいですね。

src/views/Edit.vue
<template>
  <div class="container">
    <div class="card">
      <div class="card-header">
        <h3>Edit Item</h3>
      </div>
      <div class="card-body">
        <form v-on:submit.prevent="updateItem">
          <div v-show="message" class="alert alert-danger">{{message}}</div>
          <div class="form-group">
            <label>Item Name:</label>
            <input type="text" class="form-control" v-model="item.name" />
          </div>
          <div class="form-group">
            <label>Item Price:</label>
            <input type="text" class="form-control" v-model="item.price" />
          </div>
          <div class="form-group">
            <input type="submit" class="btn btn-primary" value="Update Item" />
          </div>
        </form>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      item: {},
      message: ""
    };
  },
  created: function() {
    this.getItem();
  },
  methods: {
    getItem() {
      let uri = "/items/" + this.$route.params.id;
      this.axios.get(uri).then(response => {
        this.item = response.data;
      });
    },
    updateItem() {
      let uri = "/items/" + this.$route.params.id;
      this.axios.put(uri, this.item).then(() => {
        this.$swal({
          icon: "success",
          text: "Updated Success!"
        });
        this.$router.push({ name: "Index" });
      });
    }
  }
};
</script>

メインテンプレートの変更

App.vueを変更してメインのテンプレートを変更します。

src/App.vue
<template>
  <div id="app" class="container">
    <nav class="navbar navbar-expand-sm bg-light">
      <ul class="navbar-nav">
        <li class="nav-item">
          <router-link :to="{ name: 'Create' }" class="nav-link">Add Item</router-link>
        </li>
        <li class="nav-item">
          <router-link :to="{ name: 'Index' }" class="nav-link">All Items</router-link>
        </li>
      </ul>
    </nav>
    <transition name="fade">
      <div class="gap">
        <router-view></router-view>        
      </div>
    </transition>
  </div>
</template>

<script>

export default {
}
</script>

<style>
    .fade-enter-active, .fade-leave-active {
      transition: opacity .5s
    }
    .fade-enter, .fade-leave-active {
      opacity: 0
    }
    .gap {
      margin-top: 50px;
    }
</style>

ルーティングの変更

先ほど作成した画面をルーティングに登録するためにsrc/router/index.jsを修正します。

src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Create from '../views/Create.vue'
import Edit from '../views/Edit.vue'
import Index from '../views/Index.vue'

Vue.use(VueRouter)

const routes = [
  {
    name: 'Index',
    path: '/',
    component: Index
  },
  {
    name: 'Create',
    path: '/create',
    component: Create
  },
  {
    name: 'Edit',
    path: '/edit',
    component: Edit
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

まとめ

とりあえずVue.jsとJavaで作ったAPIでCRUDを実現することができました。
正直、そこまでスケーラビリティを求めないならアプリではPHPやJFR、あるいはRailsで作った方が楽じゃないかの疑惑も拭えないですが、

次はログイン機能あたりを実装してみたいと思います。

それでは、Happy Hacking!

参考

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

Vue JSでCRUDアプリケーションを作る

はじめに

フロントエンド開発、最近はやはりJSで書くのが基本だとは思うけどReactやAngularは少し敷居が高いのでVue.jsに手を出してみました。
とりあえず、自分へのメモの意味合でVue JSのCRUDアプリケーションのチュートリアルを書いてみます。CRUDが書ければあとは必要に応じて普通に拡張できるでしょうし。たぶん。

今回はAPIサイドをJavaのQuarkusで、フロントエンドをVue.jsで書いています。

サーバサイドをQuarkusで作成

プロジェクト作成

まずサーバサイドを作成します。Mavenでまずは以下のようにQuarkusプロジェクトを生成します。なお、mavenのバージョンが3.6.2より低い場合は事前に上げておくこと。

$ mvn io.quarkus:quarkus-maven-plugin:1.3.0.Final:create \
    -DprojectGroupId=crud \
    -DprojectArtifactId=crud-server \
    -DclassName="crud.ItemResource" \
    -Dpath="/items"

つづいて、必要なプラグインをインストール。今回はDB周りとJAX-RS周りを入れています。

$ cd crud-server/
$ ./mvnw quarkus:add-extension -Dextensions="quarkus-hibernate-orm-panache,quarkus
-jdbc-h2,quarkus-resteasy-jackson,quarkus-resteasy-jsonb"

ビルドの確認

$ ./mvnw compile quarkus:dev

永続化層の作成

まずは、application.propertiesの設定を変更。

# DB Config
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:
quarkus.hibernate-orm.database.generation = drop-and-create

続いてEntityの作成。getter/setterは長いので省略。

@Entity
public class Item {
    @Id
    @GeneratedValue(generator = "UUID")
    @GenericGenerator(
            name = "UUID",
            strategy = "org.hibernate.id.UUIDGenerator"
    )
    @Column(name = "id", updatable = false, nullable = false)
    private UUID id;
    private String name;
    private int price;

    public Item() { }

    public Item(UUID id, String name, int price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
    public UUID getId() {
        return id;
    }
    public void setId(UUID id) {
        this.id = id;
    }
-  -
}

最後にRepository.

@ApplicationScoped
public class ItemRepository implements PanacheRepository<Item> {

    public Item findById(UUID id) {
        return this.find("id", id).list().get(0);
    }

    public void update(UUID id, Item item) {
        var x = findById(id);
        x.setName(item.getName());
        x.setPrice(item.getPrice());
    }
}

エンドポイント作成

エンドポイントとしてJAX-RSで簡単なREST APIを作成する

@Path("/items")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public class ItemResource {
    @Inject
    ItemRepository repository;

    @GET
    @Path("{id}")
    public Item get(@PathParam("id") UUID id) {
        return repository.findById(id);
    }
    @GET
    public List<Item> list() {
        return repository.listAll();
    }
    @POST
    public void create(Item item) {
        repository.persist(item);
    }
    @PUT
    @Path("{id}")
    public void update(@PathParam("id") UUID id, Item item) {
        repository.update(id, item);
    }
    @DELETE
    @Path("{id}")
    public void delete(@PathParam("id") UUID id) {
        repository.delete("id", id);
    }
}

CORS対応のためにいくつか設定をapplication.propertiesに追加する. この例ではQuarkus側がポート3000, Vue.js側が8080で起動する想定。

本番完了では構成によってはCORSは起こらないので設定不要。

# HTTP Config
quarkus.http.port=3000
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:8080,http://172.19.27.127:8080
quarkus.http.cors.methods=GET,PUT,POST,DELETE

動作確認。

$ ./mvnw compile quarkus:dev

crulでAPIを実行。

$ URL=http://localhost:3000/items
$ JSON_TYPE="Content-Type:application/json"

$ curl -XGET ${URL} -H $JSON_TYPE
[]

$ curl -XPOST ${URL} -H $JSON_TYPE -d '{"name" : "new item 02", "price" : 128}'
$ curl -XPOST ${URL} -H $JSON_TYPE -d '{"name" : "new item 02", "price" : 128}'
$ curl -XGET ${URL} -H $JSON_TYPE
[{"id":"57cda117-474a-48bf-85f3-8f83dd33dac9","name":"new item 01","price":64},{"id":"bc5cc9fd-0ba9-4b02-91e8-93fdf9ed59fd","name":"new item 02","price":128}]

$ curl -XPUT ${URL}/"57cda117-474a-48bf-85f3-8f83dd33dac9" -H $JSON_TYPE -d '{"name" : "new item 01", "price" : 1024}'
$ curl -XGET ${URL} -H $JSON_TYPE
[{"id":"bc5cc9fd-0ba9-4b02-91e8-93fdf9ed59fd","name":"new item 02","price":128},{"id":"57cda117-474a-48bf-85f3-8f83dd33dac9","name":"new item 01","price":1024}]

$ curl -XDELETE ${URL}/"bc5cc9fd-0ba9-4b02-91e8-93fdf9ed59fd" -H $JSON_TYPE
$ curl -XGET ${URL} -H $JSON_TYPE
[{"id":"57cda117-474a-48bf-85f3-8f83dd33dac9","name":"new item 01","price":1024}]

APIが適切に動作してるのが分かります。

クライアントサイドをVue.jsで作成

続いて本命のクライアントサイドアプリをVue.jsで実装します。

プロジェクトの作成

まずはvue-cliのインストールを行います。

$ yarn global add @vue/cli

続いてプロジェクトの作成。選択肢はマニュアルを選択してRouterを追加します。面倒なので今回はLintも外します。後の選択肢は好みで。

$ vue create crud-client
Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◯ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

開発サーバを立てて動作確認をします。

$ cd crud-client
$ yarn serve

ネットワークライブラリの追加

JS標準のFetch APIだと記述が冗長なのでaxiosを使います。

$ yarn add axios vue-axios

UIフレームワークの追加

UIフレームワークとしてBootstrapを入れます。合わせてリッチなアラートを提供するsweetalertもインストールします。

$ yarn add bootstrap vue-sweetalert2

Vue Configの設定をする

通常、本番環境やテスト環境と開発環境ではAPIの接続先が異なると思います。APIサーバをローカルに立てたりするので。
なので、開発サーバの設定を変更して、そこで接続先を書き換えます。ルートディレクトリにvue.config.jsを追加して以下のように修正します。

また、タイトルも同様にここで設定するので追加しておきます。

vue.config.js
module.exports = {
    devServer: {
        proxy: {
            "^/items": {
                target: "http://localhost:3000",
                ws: false,
                pathRewrite: {
                    "^/items": "/items"
                }
            }
        }
    },
    pages: {
        index: {
            entry: 'src/main.js',
            title: 'My CRUD Apps',
        }
    }
};

ライブラリのインポート

先ほど、yarnでインストールしたライブラリを読み込むためにmain.jsを以下のように修正します。

src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import axios from 'axios';
import VueAxios from 'vue-axios';
import VueSweetalert2 from 'vue-sweetalert2';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
import '../node_modules/sweetalert2/dist/sweetalert2.min.css';

Vue.config.productionTip = false
Vue.use(VueSweetalert2);
Vue.use(VueAxios, axios);

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

CRUD画面を作る

本題のCRUD画面を作ります。メイン画面はviews配下に作るのが作法のようです。
とりあえず不要なファイルを消しましょう。

$ rm src/views/About.vue
$ rm src/views/Home.vue

作成画面

Script側で定義したデータモデルに対してリアクティブにテンプレート側の値が変更されます。
サーバ側でエラーが発生すればmessageにメッセージとして表示されるようにしています。
thisを多用しているので無名関数をアロー演算子ではなくfunctionで作成してしまうとスコープが変わって動かなくなるので注意。

また、作成完了時には一覧画面に遷移しますが、遷移後に作成完了のメッセージを一覧画面に表示する代わりにSweetAlertでメッセージを出しています。
これは、Vue.jsではJavaEEやRailsのFlush領域のように画面遷移に際して値をバックエンドで受け渡す仕組みが無いためエラーメッセージを同じUIで出すのは無駄に困難です。なので似たUXとなるリーズナブルなUIを選択しています。

src/views/Create.vue
<template>
  <div class="container">
    <div class="card">
      <div class="card-header">
        <h3>Add Item</h3>
      </div>
      <div class="card-body">
        <form v-on:submit.prevent="addItem">
          <div v-show="message" class="alert alert-danger">{{message}}</div>
          <div class="form-group">
            <label>Item Name:</label>
            <input type="text" class="form-control" v-model="item.name" />
          </div>
          <div class="form-group">
            <label>Item Price:</label>
            <input type="text" class="form-control" v-model="item.price" />
          </div>
          <div class="form-group">
            <input type="submit" class="btn btn-primary" value="Add Item" />
          </div>
        </form>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  components: {
    name: "AddItem"
  },
  data() {
    return {
      item: {},
      message: ""
    };
  },
  methods: {
    addItem() {
      let uri = "/items/";
      this.axios
        .post(uri, this.item)
        .then(() => {
          this.$swal({
            icon: "success",
            text: "Created Success!"
          });
          this.$router.push({ name: "Index" });
        })
        .catch(error => {
          this.message = `status: ${error.response.status}, message: ${error.response.data}`;
        });
    }
  }
};

一覧画面

一覧画面では画面を表示した際に呼ばれるcreatedメソッドの中で、一覧を取得するAPIを呼んでいます。
また、削除のAPIコールと編集への遷移も行っています。

src/views/Index.vue
<template>
  <div>
    <h1>Items</h1>

    <table class="table table-hover">
      <thead>
        <tr>
          <td>ID</td>
          <td>Item Name</td>
          <td>Item Price</td>
          <td>Actions</td>
        </tr>
      </thead>

      <tbody>
        <tr v-for="item in items" :key="item._id">
          <td>{{ item.id }}</td>
          <td>{{ item.name }}</td>
          <td>{{ item.price }}</td>
          <td>
            <router-link :to="{name: 'Edit', params: { id: item.id }}" class="btn btn-primary">Edit</router-link>
          </td>
          <td>
            <button class="btn btn-danger" v-on:click="deleteItem(item.id)">Delete</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: []
    };
  },
  created: function() {
    this.fetchItems();
  },
  methods: {
    fetchItems() {
      let uri = "/items";
      this.axios.get(uri).then(response => {
        this.items = response.data;
      });
    },
    deleteItem(id) {
      let uri = "/items/" + id;
      this.axios.delete(uri).then(() => {
        this.fetchItems();
      });
    }
  }
};
</script>

編集画面

編集画面は基本的に作成画面と同様です。違いはサーバサイドにIDで値を問い合わせてるかくらいですね。

src/views/Edit.vue
<template>
  <div class="container">
    <div class="card">
      <div class="card-header">
        <h3>Edit Item</h3>
      </div>
      <div class="card-body">
        <form v-on:submit.prevent="updateItem">
          <div v-show="message" class="alert alert-danger">{{message}}</div>
          <div class="form-group">
            <label>Item Name:</label>
            <input type="text" class="form-control" v-model="item.name" />
          </div>
          <div class="form-group">
            <label>Item Price:</label>
            <input type="text" class="form-control" v-model="item.price" />
          </div>
          <div class="form-group">
            <input type="submit" class="btn btn-primary" value="Update Item" />
          </div>
        </form>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      item: {},
      message: ""
    };
  },
  created: function() {
    this.getItem();
  },
  methods: {
    getItem() {
      let uri = "/items/" + this.$route.params.id;
      this.axios.get(uri).then(response => {
        this.item = response.data;
      });
    },
    updateItem() {
      let uri = "/items/" + this.$route.params.id;
      this.axios.put(uri, this.item).then(() => {
        this.$swal({
          icon: "success",
          text: "Updated Success!"
        });
        this.$router.push({ name: "Index" });
      });
    }
  }
};
</script>

メインテンプレートの変更

App.vueを変更してメインのテンプレートを変更します。

src/App.vue
<template>
  <div id="app" class="container">
    <nav class="navbar navbar-expand-sm bg-light">
      <ul class="navbar-nav">
        <li class="nav-item">
          <router-link :to="{ name: 'Create' }" class="nav-link">Add Item</router-link>
        </li>
        <li class="nav-item">
          <router-link :to="{ name: 'Index' }" class="nav-link">All Items</router-link>
        </li>
      </ul>
    </nav>
    <transition name="fade">
      <div class="gap">
        <router-view></router-view>        
      </div>
    </transition>
  </div>
</template>

<script>

export default {
}
</script>

<style>
    .fade-enter-active, .fade-leave-active {
      transition: opacity .5s
    }
    .fade-enter, .fade-leave-active {
      opacity: 0
    }
    .gap {
      margin-top: 50px;
    }
</style>

ルーティングの変更

先ほど作成した画面をルーティングに登録するためにsrc/router/index.jsを修正します。

src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Create from '../views/Create.vue'
import Edit from '../views/Edit.vue'
import Index from '../views/Index.vue'

Vue.use(VueRouter)

const routes = [
  {
    name: 'Index',
    path: '/',
    component: Index
  },
  {
    name: 'Create',
    path: '/create',
    component: Create
  },
  {
    name: 'Edit',
    path: '/edit',
    component: Edit
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

まとめ

とりあえずVue.jsとJavaで作ったAPIでCRUDを実現することができました。
正直、そこまでスケーラビリティを求めないならアプリではPHPやJFR、あるいはRailsで作った方が楽じゃないかの疑惑も拭えないですが、

次はログイン機能あたりを実装してみたいと思います。

それでは、Happy Hacking!

参考

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

Web VRとobnizを連携することに成功!

概要

「Web VR(A-Framee)のボタンを押すと、obnizの画面にメッセージが表示される」

というものが作りたくて奮闘しました
https://qiita.com/kmaepu/items/55689772aa41bd5d8433
こちらの記事ではobnizとの連携まで至りませんでした。

というのをTwitterにつぶやいた所、中の人からアドバイス頂き、実現することができました!
ありがとうございます。

実際の動き

少し見づらいですが、PC画面上のボタンを押すと、obnizの液晶画面に文字が映し出されます。

ソースコード解説

前回の記事にあったところとの差分を抜き出すと次の箇所になります。

let obniz = new Obniz("obniz_id");  // obniz idを入力する
  let connected = false;
  obniz.onconnect =  async function () {
    console.log("Connect obniz");
    connected = true;
  }
  obniz.onclose = async function () {
    connected = false;
  }

  async function handlerClick(event) {
    console.log('handlerClick');
    console.log(event);
    event.target.object3D.position.z -= 0.5;

    if (connected) {
      obniz.display.clear();
      obniz.display.print("3D A-Frame");
      obniz.display.print(" ↑↓");
      obniz.display.print("obniz");
    }
  }

何が原因だったかというと、obnizのconnectはscriptの走った最初だけ実行し、それ以降に何度もconnnectしようとしても動かないとのことです。

function内で何度もconnnectしちゃいけなかったのか!

<!DOCTYPE html>
<html>

<head>
  <title>Hello, WebVR!  A-Frame</title>
  <meta name="description" content="Hello, WebVR! • A-Frame">
  <script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>

</head>

<body>
<a-scene>
  <!--背景画像-->
  <a-sky src="https://aframe.io/aframe/examples/boilerplate/panorama/puydesancy.jpg" rotation="0 -130 0"></a-sky>
  <!--仮想ボタン-->
  <a-box color="#EEEEEE" position="0 2 -3" height="2" width="2"></a-box>
  <a-box color="#C0C0C0" position="0 2 -2.5" height="0.5" width="0.5" onclick="handlerClick(event)"
         onmouseenter="handlerMouseEnter(event)" onmouseleave="handlerMouseLeave(event)">
    <a-animation attribute="scale" to="-3.5 -5 2" direction="alternate" dur="2000" repeat="indefinite"
                 easing="linear">
    </a-animation>
  </a-box>
  <!--足もとのブロック-->
  <a-box color="#99FFFF" position="0 0 0" height="2" width="2"></a-box>
  <!--自分の視点-->
  <a-camera>
    <a-cursor></a-cursor>
  </a-camera>
</a-scene>

<script src="https://unpkg.com/obniz@3.5.0/obniz.js" crossorigin="anonymous"></script>
<script>
  let obniz = new Obniz("obniz_id");  // obniz idを入力する
  let connected = false;
  obniz.onconnect =  async function () {
    console.log("Connect obniz");
    connected = true;
  }
  obniz.onclose = async function () {
    connected = false;
  }

  async function handlerClick(event) {
    console.log('handlerClick');
    console.log(event);
    event.target.object3D.position.z -= 0.5;

    if (connected) {
      obniz.display.clear();
      obniz.display.print("3D A-Frame");
      obniz.display.print(" ↑↓");
      obniz.display.print("obniz");
    }
  }

  function handlerMouseEnter(event) {
    console.log('handlerMouseEnter');
    console.log(event);
  }

  function handlerMouseLeave(event) {
    console.log('handlerMouseLeave');
    console.log(event);
  }
</script>

</body>

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

WebSocket通信におけるJSONデータの利用方法 (Java , JavaScript)

背景

チャットアプリ作成を基準とし、サーバー⇔ブラウザ間でのWebSocket通信の記事を書いた。
サーバーをJava、クライアントをjavascriptで実装。

[ソケット通信に関する過去記事]
JavaとJavaScriptでwebブラウザとのソケット通信①
JavaとJavaScriptでwebブラウザとのソケット通信②

今回はチャットアプリを改造しつつ、『JSONデータの取扱方法』について学んでいく。
また、『ソケット通信でのJSONデータの送受信』についても学ぶ。

目的

  1. Java、JavaScriptでのJSONの扱い方法を学ぶ。
  2. Webソケット通信においてJSONでのデータの送受信方法を学ぶ。
  3. JSONを用いてWebソケット通信の複数パスを実現する。

前提

この記事では主にJSONの扱い方を中心に記述していく。
ソケット通信についての解説は過去記事やググったページを参考にしてほしい。
⇒ サーバープログラムはJava、クライアントプログラムはJavaScriptで実装。

JSONとは

http://www.tohoho-web.com/ex/json.html
例:{ "name": "Tanaka", "age": 26 }
連想配列のような形で値を保持しているもの。
要はただの文字列と考えると楽に理解することができる。(本当は違うけど)
詳細は割愛。

実践内容

  1. JavaScriptでJSONデータの取り扱い (エンコード、デコード)
  2. JavaでJSONデータの取り扱い (エンコード、デコード)
  3. ソケット通信におけるJSONデータの送受信方法
  4. JSON送受信を利用した複数パスのチャットアプリ作成

1. JavaScriptでJSONデータの取り扱い

JavaScriptでは容易にJSONデータを扱うことができる。

エンコード

オブジェクト ⇒ JSON
JSON.stringify()メソッドを使用する。

使用例
var obj = {
    name: '太郎',
    age: 30,
    area: 'Tokyo'
}
var json = JSON.stringify(obj);
console.log(json);
実行結果
{"name":"Taro Tanaka","age":30}
  1. 変数objに連想配列の形式で値を代入。(オブジェクトの作成)
  2. JSON.stringify(obj)を用いてエンコードし変数jsonに代入。(JSONへ変換)
  3. console.log(json)でJSONデータをコンソールに表示。

実行結果を見ると、連想配列形式のオブジェクトがJSON形式に変換されていることが分かる。
⇒ キーや文字列は「" "」で囲われている。数値は裸のまま。

デコード

JSON ⇒ オブジェクト
JSON.parse()メソッドを使用する。

使用例
var obj1 = {
        name: '太郎',
        age: 30,
        area: 'Tokyo'
    }
var json = JSON.stringify(obj1);
//-----ここまでJSONデータの準備-----
var obj2 = JSON.parse(json);
console.log(obj2);
console.log(obj2.name);
実行結果
{name: "太郎", age: 30, area: "Tokyo"}
太郎
  1. 変数obj1に連想配列の形式で値を代入。(オブジェクトの作成)
  2. JSON.stringify(obj1)を用いてエンコードし変数jsonに代入。(JSONへ変換)
    ----------ここまでJSONデータの準備 (前項のエンコードと同様)----------
  3. JSON.parse(json)を用いてデコードし変数obj2に代入。(オブジェクトへ変換)
  4. console.log(obj2)でオブジェクトをコンソールに表示。
  5. obj2.~で各プロパティにもアクセス可能。

実行結果を見ると、JSONデータがオブジェクトに変換されていることが分かる。
obj1(オブジェクト) ⇒ json(JSON) ⇒ obj2(オブジェクト)

2. JavaでJSONデータの取り扱い

Javaのオブジェクトはクラスを指し、JavaScriptのように簡単に作成できない。
JSONとオブジェクトを変換する場合、事前にJSONの変数に対応したプロパティを持つクラスを作成しておく必要がある。

JavaでJSONを扱うなら、外部ライブラリを使用することを推奨。
⇒ Java標準APIにもJSONを扱うものは用意されているが、かなり手間がかかる。

JSONを扱う外部ライブラリは以下のものが有名。

  • Jackson
  • GSON
  • JSONIC
  • JSON in Java などなど

基本の利用方法は似たようなものだが、今回は「Jackson」を使用する。
他ライブラリの使用方法や外部ライブラリの適用方法は、参考ページを見るか適宜ググってほしい。

エンコード

オブジェクト ⇒ JSON
エンコードにはObjectMapperクラスのwriteValueAsString()メソッドを使用する。

使用方法
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(オブジェクトインスタンス);
使用例
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;

//JSONに変換するプロパティを持つクラス
class Info {
    public String name = "Taro Tanaka";
    public int age = 30;
}

//エンコードを実行するクラス
public class Main {
    public static void main(String[] args) {
        Info info = new Info();//JSONへ変換するクラスをインスタンス化
        ObjectMapper mapper = new ObjectMapper();//ObjectMapperクラスのインスタンスを作成
        try {
            //writeValueAsString()メソッドでエンコード実施
            String script = mapper.writeValueAsString(info);
            System.out.println(script);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
実行結果
{"name":"Taro Tanaka","age":30}
  1. InfoクラスにてJSONに変換するためのプロパティを作成する。
  2. エンコードを実行するMainクラスを作成。
  3. Info info = new Info()にてJSONに変換するオブジェクトをインスタンス化。
  4. ObjectMapper mapper = new ObjectMapper()にてObjectMapperクラスをインスタンス化。
  5. mapper.writeValueAsString(info)にてオブジェクトからJSONに変換。
    このとき、try,catchでのエラー対応が必要。
  6. System.out.println(script)でJSONデータをコンソールに出力。

実行結果を見ると、オブジェクトクラスがJSON形式に変換されていることが分かる。
各プロパティの変数名と値がペアとなって格納されている。

デコード

JSON ⇒ オブジェクト
デコードにはObjectMapperクラスのreadValue()メソッドを使用する。

使用方法
ObjectMapper mapper = new ObjectMapper();
mapper.readValue(JSONデータ,オブジェクトクラス.class);
使用例
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;

//JSONから変換されるプロパティを持つクラス
class Info {
    public String name;
    public int age;
}

//デコードを実行するクラス
public class Main {
    public static void main(String[] args) {
        String script = "{ \"name\":\"Taro Tanaka\", \"age\":30}";//文字列としてJSONデータを作成
        ObjectMapper mapper = new ObjectMapper();//ObjectMapperクラスのインスタンスを作成
        try {
            //readValue()メソッドでデコード実施
            Info info = mapper.readValue(script, Info.class);
            System.out.println(info.name);
            System.out.println(info.age);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
実行結果
Taro Tanaka
30
  1. InfoクラスにてJSONから変換されるプロパティを持つクラスを作成する。
  2. デコードを実行するMainクラスを作成。
  3. ここではStringの文字列としてJSONデータを用意する。
  4. ObjectMapper mapper = new ObjectMapper()にてObjectMapperクラスをインスタンス化。
  5. mapper.readValue(script, Info.class)にてJSONからオブジェクトに変換。
    このとき、try,catchでのエラー対応が必要。
  6. System.out.println(info.~)でオブジェクトプロパティをコンソールに出力。

実行結果を見ると、JSONデータがオブジェクトクラスに変換されていることが分かる。
デコード後は指定したオブジェクトの各プロパティにアクセスすることで値を取得することができる。

3. ソケット通信におけるJSONデータの送受信方法

サーバープログラム:Java
クライアントプログラム:JavaScript

ソケット通信を行う際、JSONでの送受信方法を記述する。
JavaとJavaScriptで扱い方が異なるため、それぞれ解説する。

JavaScriptでのJSON送受信

JavaScriptに関しては特筆すべきことはない。
前述した通り、JSON.stringify()メソッド及びJSON.parse()メソッドで簡単にオブジェクト ⇔ JSON間の変換が可能。
クライアントから送信前にエンコード、受信後にデコードすれば問題無い。

送信時エンコード

オブジェクト ⇒ JSON

送信時
var obj = { type:'A' , msg:'a' };
var json = JSON.stringify(obj);
socket.send(json);
  1. オブジェクトを用意。
  2. JSON.stringify()メソッドでJSONへエンコード。
  3. WebSocketのsend()メソッドでJSONデータ送信

受信時デコード

JSON ⇒ オブジェクト

受信時
socket.onmessage = function(e) {
    var obj = JSON.parse(e.data);
};
  1. onmessageでJSONデータを受信。
  2. JSON.parse(e.data)でオブジェクトへデコード

JavaでのJSON送受信

Javaのソケット通信時にJSONを用いる場合、JavaScriptのように簡単にはいかない。
⇒ エンコーダー、デコーダー、オブジェクトクラスの3つを用意する必要がある。

送信時エンコード

オブジェクト ⇒ JSON

ソケット通信でJSONデータを送信する場合、エンコーダーを用いてオブジェクトからJSONに変換してから送信する。

通常、テキストデータを送信する場合にはsendText()メソッドを使用するが、JSONを送信する場合はsendObject()メソッドを使用する。
引数をオブジェクトとし、エンコーダーによってJSONに変換したのち送信する。

  1. 送信するオブジェクトを作成する。
  2. エンコーダーを作成する。
  3. @ServerEndpointアノテーションにエンコーダーを登録する。

1. 送信するオブジェクトを作成

通常のクラスと同様にプロパティを持つクラスを作成する。
コンストラクタ、セッター、ゲッターも通常通り記述。(無くても変換可能。)

オブジェクトクラス
public class JsonObj {

    private String type = "type1";
    private String msg = "msg1";

    //コンストラクタ
    public JsonObj() {}

    //セッター
    public void setType(String type) {this.type = type;}
    public void setMsg(String msg) {this.msg = msg;}

    //ゲッター
    public String getType() {return type;}
    public String getMsg() {return msg;}
}

2. エンコーダーを作成

エンコーダーはjavax.websocketパッケージのEncoder.Textクラスを実装する。
(Encoder.Binaryクラスも存在するが、バイナリデータを扱うためのクラスなので割愛)

Encoder.Text<>のジェネリクスにはエンコードするオブジェクトクラスを記述しておく。

エンコーダー
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonEncoder implements Encoder.Text<JsonObj>{

    @Override//初期化は何もしない
    public void init(EndpointConfig config) {}

    @Override//エンコード処理 ( オブジェクト → JSON )
    public String encode(JsonObj obj) throws EncodeException {
        ObjectMapper mapper = new ObjectMapper();
        String json = "";
        try {
            json = mapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return json;
    }

    @Override//破棄は何もしない
    public void destroy() {}
}

Encoder.Textクラスを継承した場合、以下のメソッドをオーバーライドする必要がある。

  1. init(EndpointConfig config):エンコーダーが起動したときの処理。
  2. encode(Object obj):エンコード処理。
  3. destroy():エンコーダが破棄された時の処理。

基本的にinit()及びdestroy()は何も処理しなくてよい。
encode(Object)にて引数をオブジェクトとし、エンコード後に戻り値としてJSONを指定。

3. エンコーダーを登録

@ServerEndpoint
//エンコーダークラスを指定
@ServerEndpoint(value = "/json" , encoders = JsonEncoder.class)
public class JsonTest  {
    //中略

    //sendObject()の引数はオブジェクト
    //送信前にエンコードされる
    session.getAsyncRemote().sendObject(obj)

    //中略
}

エンコーダーを利用するとき、@ServerEndpoint()にエンコーダークラスを指定する。
ここで指定しておくことで、オブジェクト送信時に指定したエンコーダーによってJSONデータへ変換される。

  • sendObject(obj) ⇒ エンコード処理 (obj→JSON) ⇒ 送信

受信時デコード

JSON ⇒ オブジェクト

ソケット通信でJSONデータを受信する場合、デコーダーを用いてJSONからオブジェクトに変換してから受信する。

通常はonMessage()メソッドの引数にString型文字列として受信するが、デコーダーを用いることで受信前にデコード処理が実施され、オブジェクトとして受信することとなる。

  1. 受信するJSONの変換先オブジェクトを作成する。
  2. デコーダーを作成する。
  3. @ServerEndpointアノテーションにデコーダーを登録する。

1. 変換先のオブジェクトを作成

デコードする場合、JSONの要素に対応したプロパティを持つオブジェクトクラスを用意しておく必要がある。
複数のJSONを受信するとき内容や形式が異なるなら、それぞれのJSONに対応した複数のオブジェクトクラスを用意しなければならない。

エンコードの場合、全てのオブジェクトは任意のタイミングでJSONに変換し送信することが可能
デコードの場合、受信するJSONの内容をあらかじめ把握し、デコード前に受け口となるオブジェクトクラスを用意しておかなければならない。

オブジェクトクラス
public class JsonObj {

    private String type;
    private String msg;

    //コンストラクタ
    public JsonObj() {}

    //セッター
    public void setType(String type) {this.type = type;}
    public void setMsg(String msg) {this.msg = msg;}

    //ゲッター
    public String getType() {return type;}
    public String getMsg() {return msg;}
}

2. デコーダーを作成

デコーダーはjavax.websocketパッケージのDecoder.Textクラスを実装する。
(Decoder.Binaryクラスも存在するが、バイナリデータを扱うためのクラスなので割愛)

Decoder.Text<>のジェネリクスにはデコード先のオブジェクトクラスを記述しておく。

デコーダー
import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonDecoder  implements Decoder.Text<JsonObj> {

    @Override//初期化は何もしない
    public void init(EndpointConfig config) {}

    @Override//デコードできるかの判定
    public boolean willDecode(String text) {
        return (text != null);
    }

    @Override//デコード処理 ( JSON → オブジェクト)
    public JsonObj decode(String text) throws DecodeException {
        ObjectMapper mapper = new ObjectMapper();
        JsonObj obj = null;
        try {
            obj = mapper.readValue(text, JsonObj.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return obj;
    }

    @Override//破棄は何もしない
    public void destroy() {}
}

Decoder.Textクラスを継承した場合、以下のメソッドをオーバーライドする必要がある。

  1. init(EndpointConfig config):デコーダーが起動したときの処理。
  2. willDecode(String text):デコード処理を実行するかどうかの判定。
  3. decode(Object obj):デコード処理。
  4. destroy():デコーダが破棄された時の処理。

基本的にinit()及びdestroy()は何も処理しなくてよい。
willDecode()引数をJSONとし、戻り値がtrueなら以下のデコードを実行。falseならデコードされず以降の@OnMessageメソッドは実行されない。
decode()にて引数をJSONとし、エンコード後に戻り値としてオブジェクトを指定。

3. デコーダーを登録

@ServerEndpoint
//デコーダークラスを指定
@ServerEndpoint(value = "/json" , decoders = JsonDecoder.class)
public class JsonTest  {
    //中略

    //@OnMessageメソッドの前にデコードされる
    @OnMessage
    public void onMessage(JsonObj obj , Session mySession) {
    }

    //中略
}

デコーダーを利用するとき、@ServerEndpoint()にデコーダークラスを指定する。
ここで指定しておくことで、データ受信時に指定したデコーダーによってオブジェクトへ変換される。
@OnMessageメソッドの引数はオブジェクト型となる。(通常はString型)

  • 受信 ⇒ デコード処理 (JSON→obj) ⇒ @OnMessageメソッド

4. 複数パスのチャットアプリ作成

過去に作成したチャットアプリを改変する。

変更点

  1. 送受信するデータをテキストからJSONへ変更 (JSONの使用)
  2. チャット欄を1つから2つへ増加 (複数パスの実現)

WebSocket通信ではデータを受信するための受け口が1つしかない。
つまりワンパスのため複数の送信元があっても見分けがつかない。
もし見分けるなら、文字列の内容を分解して区別する他無いだろう。
どうせデコードするのならJSONを扱うのが都合がよい。

実際は 1.テキスト 2.バイナリ 3.PingPong と3種類の受信メソッドがあるが、ここではテキスト形式のみを扱うため受け口は1つと考える。

作成ファイル

  1. JsonIndex.html:ブラウザ表示用のHTMLファイル
  2. JsonSocket.js:ソケット通信のクライアントプログラム
  3. JsonTest.java:ソケット通信のサーバープログラム
  4. JsonObj.java:JSONと相互変換するオブジェクトクラス
  5. JsonEncoder.java:Javaエンコーダー
  6. JsonDecoder.java:Javaデコーダー

1. 表示用HTML

JsonIndex.html
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>JSON送受信</title>
    <script type="text/javascript" src="JsonSocket.js"></script>
</head>
<body>
    <div
        style="width: 500px; height: 200px; overflow-y: auto; border: 1px solid #333;"
        id="show1"></div>
    <input type="text" size="80" id="msg1" name="msg1" />
    <input type="button" value="送信" onclick="sendMsg1();" />
    <p></p>
    <div
        style="width: 500px; height: 200px; overflow-y: auto; border: 1px solid #333;"
        id="show2"></div>
    <input type="text" size="80" id="msg2" name="msg2" />
    <input type="button" value="送信" onclick="sendMsg2();" />
</body>
</html>

ポイント
1. チャット欄を2つに増加した。

表示枠はそれぞれshow1show2、テキストボックスはmsg1msg2とした。

後述するJavaScriptファイルでこれらを操作する。

2. クライアントプログラム

JsonSocket.js
//JSON用のオブジェクト作成
var obj = { type:null , msg:null };

//WebSocketオブジェクト生成
var wSck= new WebSocket("ws://localhost:8080/jsonTest/json");

//ソケット接続時のアクション
wSck.onopen = function() {
    document.getElementById('show1').innerHTML += "接続しました。" + "<br/>";
    document.getElementById('show2').innerHTML += "接続したよ~" + "<br/>";
};

//メッセージを受け取ったときのアクション
wSck.onmessage = function(e) {
    //JSONデータをオブジェクトへデコード
    var json = JSON.parse(e.data);

    //JSONデータのtype値によって実行内容を変更
    if(json.type === 'msg1'){document.getElementById('show1').innerHTML += json.msg + "<br/>";}
    else if(json.type === 'msg2'){document.getElementById('show2').innerHTML += json.msg + "<br/>";}
};

//メッセージ送信1
var sendMsg1 = function(val) {
    var element = document.getElementById('msg1')
    obj.type = element.name;//オブジェクトの内容を代入
    obj.msg = element.value;
    var json = JSON.stringify(obj);//オブジェクトをJSONへエンコード
    wSck.send(json);//JSONを送信
    element.value = "";//内容をクリア
};

//メッセージ送信2
var sendMsg2 = function(val) {
    var element = document.getElementById('msg2');
    obj.type = element.name;
    obj.msg = element.value;
    var json = JSON.stringify(obj);
    wSck.send(json);
    element.value = "";
};

ポイント
1. JSON用のオブジェクトobjを作成 ⇒ これがJSONへ変換される。
2. ソケット接続時のアクションをチャット欄増加に合わせて追加。
3. メッセージ受信時にJSONをオブジェクトへデコードし、type値によって処理内容を変更。
4. メッセージ送信1,2:objに値を代入後、サーバーへの送信前にJSONへの変換を実施。

3. サーバープログラム

JsonTest.java
package jsonTest;

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

// 引数にデコーダー、エンコーダーを記述
@ServerEndpoint(value = "/json" , decoders = JsonDecoder.class , encoders = JsonEncoder.class)
public class JsonTest  {

    private static Set<Session> user = new CopyOnWriteArraySet<>();

    @OnOpen
    public void onOpen(Session mySession) {
        System.out.println("connect ID:"+mySession.getId());
        user.add(mySession);
    }

    //このメソッドの前にデコードされる
    @OnMessage
    public void onMessage(JsonObj obj , Session mySession) {
        for (Session user : user) {
            user.getAsyncRemote().sendObject(obj);//送信するものはオブジェクト(送信前にエンコードされる)
            System.out.println(user.getId()+"番目に"+mySession.getId()+"番目のメッセージを送りました!");
        }
        if(obj.getMsg().equals("bye")) {onClose(mySession);}
    }

    @OnClose
    public void onClose(Session mySession) {
        System.out.println("disconnect ID:"+mySession.getId());
        user.remove(mySession);
        try {
            mySession.close();
        } catch (IOException e) {
            System.err.println("エラーが発生しました: " + e);
        }
    }
}

ポイント
1. @ServerEndpointにデコーダー及びエンコーダーのクラスを指定。
2. クライアントからデータを受信したとき、@OnMesaageメソッドが実行される前に指定したデコーダーが実行されJSONからオブジェクトに変換される。

@OnMesaageメソッドの引数はObj型となる。
3. sendObject(obj)メソッドでオブジェクトを送信するとき、クライアントへ送信する前に指定したエンコーダーが実行されオブジェクトからJSONに変換される。

※ 文字列を送信する場合はsendText()を使用するが、オブジェクトを送信する場合はsendObject()を使用する。

4. オブジェクトクラス

JsonObj.java
package jsonTest;

public class JsonObj {

    private String type;
    private String msg;

    //コンストラクタ
    public JsonObj() {}

    //セッター
    public void setType(String type) {this.type = type;}
    public void setMsg(String msg) {this.msg = msg;}

    //ゲッター
    public String getType() {return type;}
    public String getMsg() {return msg;}
}

ポイント
1. このクラスのプロパティはtypemsgの2つ。

⇒ クライアントプログラムのオブジェクト (受信するJSON) に対応したクラスを作成している。
2. デコード後、typeプロパティ値によって送信元を判断する。

5. エンコーダー

JsonEncoder.java
package jsonTest;

import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

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

public class JsonEncoder implements Encoder.Text<JsonObj>{

    @Override//初期化は何もしない
    public void init(EndpointConfig config) {}

    @Override//エンコード処理 ( オブジェクト → JSON )
    public String encode(JsonObj obj) throws EncodeException {
        ObjectMapper mapper = new ObjectMapper();
        String json = "";
        try {
            json = mapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return json;
    }

    @Override//破棄は何もしない
    public void destroy() {}
}

ポイント
1. Encoder.Text<JsonObj>を実装したクラスを作成。
2. encode()メソッドの引数はオブジェクト、戻り値はJSONとなる。
3. エンコードにはObjectMapperクラスのwriteValueAsString()メソッドを使用する。

6. デコーダー

package jsonTest;

import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;

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

public class JsonDecoder  implements Decoder.Text<JsonObj> {

    @Override//初期化は何もしない
    public void init(EndpointConfig config) {}

    @Override//デコードできるかの判定
    public boolean willDecode(String text) {
        return (text != null);
    }

    @Override//デコード処理 ( JSON → オブジェクト)
    public JsonObj decode(String text) throws DecodeException {
        ObjectMapper mapper = new ObjectMapper();
        JsonObj obj = null;
        try {
            obj = mapper.readValue(text, JsonObj.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return obj;
    }

    @Override//破棄は何もしない
    public void destroy() {}
}

ポイント
1. Decoder.Text<JsonObj>を実装したクラスを作成。
2. decode()メソッドの引数は文字列(JSON)、戻り値はオブジェクトとなる。
3. エンコードにはObjectMapperクラスのreadValue()メソッドを使用する。

実行結果

ChromeにてJsonIndex.htmlを実行。
JSONチャット.gif

チャット欄が2つに増加した。
各チャット欄で独立して送受信が可能。
⇒ 複数パスの実現が完了。

実行順序

長くなったので、クライアントとサーバーでどのような処理が行われているか記述する。

  1. HTMLでページを表示。
  2. ハンドシェイク完了後、ソケット通信開始。
  3. テキストボックスにメッセージを記述し送信ボタンをクリック。
  4. JavaScriptによりオブジェクトをJSONにエンコードしサーバーへ送信。
  5. サーバーでJSONデータを受信。
  6. デコーダーによりJSONからオブジェクトへデコード。
  7. デコードされたオブジェクトを引数に@OnMessageメソッドを実行。
  8. sendObject()メソッドでオブジェクトを送信。
  9. 送信前にエンコーダーによりオブジェクトからJSONへエンコードし、クライアントへ送信。
  10. クライアントでデータ受信。
  11. JSONデータをオブジェクトへデコード。
  12. typeプロパティ値によって送信元を判別し、各送信元へメッセージを表示。

その他

改善点

・未知のJSONデータを受信すると、対応したオブジェクトクラスを用意していないためエラーとなる。
⇒ 対応している人はいるので、そのうち調べるかも。結構ややこしそう。

・複数の型のJSONデータを受信する場合に未対応。
⇒ そこまで難しくなさそう。デコーダーとオブジェクトクラスを増やしてJSONの中身に対応すればできる。

・入れ子構造のJSONに対応するにはどういうオブジェクトクラスを作成すればいいか不明。
⇒ ググれば出るし、必要となれば調べる。

・バイナリデータの扱いは理解が進んでいないため、バイナリデータの送受信には未対応。
⇒ 画像の送受信など、必要となれば調べる。

感想

思った以上にややこしかった。長くなってしまったので反省。
もっとまとめるべきだった。

JavaでJSONを扱うのに、外部ライブラリを使用したりオブジェクトクラスを用意したりと面倒。
JavaScriptなら1行で終わるのにね。

Javaのソケット通信でJSONを扱うときに、エンコードクラスやデコードクラスが用意されているのは便利なのかどうなのか。
わざわざエンコーダークラスやデコーダークラスを用意しなくちゃいけないのは、面倒といえば面倒。

面倒だが、JSONを使いたいならしょうがない。
JavaでのJSONの扱い方について理解が深まって良かった。
個人制作レベルなら利用していけそう。

参考ページ

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

consoleオブジェクト完全版

consoleオブジェクト

フロントの開発をしてる時、いつもお世話になるconsole.log()
先日、console.logでフォーマット指定子を使う!という記事を書きましたが、今回はconsole.log()以外のconsole.XXX()について、紹介したいと思います。

一覧

関数名 用途 ○:おすすめ
△:非推奨、
 非標準
assert 引数の妥当性チェック
clear コンソールをクリア
count 回数をカウント
debug デバッグログ出力
dir オブジェクト出力
dirxml オブジェクト出力
※XML形式
error エラーログ出力
exception errorの別名
group ログをグループ化
groupCollapsed ログをグループ化
※折りたたんだ状態で出力
groupEnd グループ化終了
info 出力ログに!マーク付与
log 通常のログ出力
profile プロファイル開始
profileEnd プロファイル停止
table 表形式でログ出力
time 処理時間計測
timeEnd 処理時間計測
timeLog 処理時間計測
timeStamp タイムラインにマーカー追加
trace スタックトレース出力
warn 警告ログ出力

(2020/05時点)
※MDN日本語版と英語版で情報に差異があるのですが、更新日が新しい英語版を基にしています。

動作確認

それでは、それぞれ動作を見ていきます。
ブラウザはChromeです。
一部Chromeでは対応しておらず、他のブラウザを使用している箇所は都度明記しています。

console.assert()

第1引数を評価してfalseの場合、第2引数のメッセージを出力します。
trueの場合は、何も出力されません。
image.png

console.clear()

コンソールを初期化します。
image.png

console.count(), console.countReset()

第1引数の値をラベルとして、呼ばれた回数をカウントし出力します。
image.png

console.dir(), console.dirxml()

console.dir()はDOMをJSON形式で、
console.dirxml()はXML形式で、出力してくれます。
image.png
image.png

console.log(), console.info(), console.warn(), console.error(), console.debug()

標準的なログを出力します。
関数によって、それぞれログレベルが違います。
image.png

Chromeでは、console.log()console.info()が同じでしたので、Safariでも確認しました。
image.png

また、一部で「console.debug()はChromeで表示されない」との記事を見かけますが、おそらくログの出力レベルがDefaultになっているのだと思います。
console.debug()はレベルVerboseですが、DefaultではVerboseにチェックがついていないので注意が必要です。
image.png

ログをconsole.debug()で出力して、フィルタをVerboseレベルでかければ、必要なログだけ抽出して確認できるので、おすすめです。

また、フォーマット指定子(置換文字列)を使用して、出力フォーマットを整形することもできます。
こちらの記事で紹介していますので、参考にしてみてください。
console.logでフォーマット指定子を使う!

console.exception()

一応MDNの一覧ページには載ってますが、個別のページは削除されているようです。
console.error()を使いましょう。

console.group(), console.groupCollapsed(), console.groupEnd()

ログをグループ化して出力します。
console.groupCollapsed()だと初期表示の時に、折りたたまれた状態で出力されます。
(個人的にはgroupCollapsedのほうが好きです)
image.png
image.png

console.profile(), console.profileEnd()

プロファイルを記録して、各関数の処理性能の計測などができます。
image.png
image.png

console.table()

配列やオブジェクトを表形式で出力できます。
image.png

見出しをクリックしたら、ソートすることもできます。
image.png

第2引数に値を設定すると、出力するプロパティを絞ることもできます。
image.png

console.time(), console.timeEnd(), console.timeLog()

処理時間を計測できます。
console.time()で計測を開始し、
console.timeLog()で途中経過を出力し、
console.timeEnd()で計測時間を出力し、計測を止めます。
image.png

console.timeStamp()

開発者ツールでPerformanceを計測している間だけ有効になります。
image.png

何のために使うのかよくわかっていないです。

めちゃくちゃ小さく表示されるので、ずっと見つからず何も起こらない…と思って、何度もやり直しました。
この画面ショット撮るために2日かかりました…すみません、愚痴です。

そもそも古い記事だと開発者ツールのTimelineパネルにTimeStampが表示されるのですが、今はPerformanceパネルに集約されているようです。
下記の記事に出会わなければ、見つけられなかったです。
進化を続けるChrome DevToolsの最新情報 2017

console.trace()

スタックトレースを出力し、処理の流れを確認できます。
「この処理どこから呼ばれているんだろう…」というときに使えます。
image.png

まとめ

これだけのconsole.XXX()を使いこなせれば、見づらいデバッグログ地獄からも脱却できますね!

と言いつつ、結局console.log()を打った方が楽だと思ってしまう自分もいますが…

参考

MDN日本語版
MDN英語版

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

【consoleオブジェクト完全版】ログ出力を極める!

consoleオブジェクト

フロントの開発をしてる時、いつもお世話になるconsole.log()
先日、console.logでフォーマット指定子を使う!という記事を書きましたが、今回はconsole.log()以外のconsole.XXX()について、紹介したいと思います。

一覧

関数名 用途 ○:おすすめ
△:非推奨、
 非標準
assert 引数の妥当性チェック
clear コンソールをクリア
count 回数をカウント
debug デバッグログ出力
dir オブジェクト出力
dirxml オブジェクト出力
※XML形式
error エラーログ出力
exception errorの別名
group ログをグループ化
groupCollapsed ログをグループ化
※折りたたんだ状態で出力
groupEnd グループ化終了
info 出力ログに!マーク付与
log 通常のログ出力
profile プロファイル開始
profileEnd プロファイル停止
table 表形式でログ出力
time 処理時間計測
timeEnd 処理時間計測
timeLog 処理時間計測
timeStamp タイムラインにマーカー追加
trace スタックトレース出力
warn 警告ログ出力

(2020/05時点)
※MDN日本語版と英語版で情報に差異があるのですが、更新日が新しい英語版を基にしています。

動作確認

それでは、それぞれ動作を見ていきます。
ブラウザはChromeです。
一部Chromeでは対応しておらず、他のブラウザを使用している箇所は都度明記しています。

console.assert()

第1引数を評価してfalseの場合、第2引数のメッセージを出力します。
trueの場合は、何も出力されません。
image.png

console.clear()

コンソールを初期化します。
image.png

console.count(), console.countReset()

第1引数の値をラベルとして、呼ばれた回数をカウントし出力します。
image.png

console.dir(), console.dirxml()

console.dir()はDOMをJSON形式で、
console.dirxml()はXML形式で、出力してくれます。
image.png
image.png

console.log(), console.info(), console.warn(), console.error(), console.debug()

標準的なログを出力します。
関数によって、それぞれログレベルが違います。
image.png

Chromeでは、console.log()console.info()が同じでしたので、Safariでも確認しました。
image.png

また、一部で「console.debug()はChromeで表示されない」との記事を見かけますが、おそらくログの出力レベルがDefaultになっているのだと思います。
console.debug()はレベルVerboseですが、DefaultではVerboseにチェックがついていないので注意が必要です。
image.png

ログをconsole.debug()で出力して、フィルタをVerboseレベルでかければ、必要なログだけ抽出して確認できるので、おすすめです。

また、フォーマット指定子(置換文字列)を使用して、出力フォーマットを整形することもできます。
こちらの記事で紹介していますので、参考にしてみてください。
console.logでフォーマット指定子を使う!

console.exception()

一応MDNの一覧ページには載ってますが、個別のページは削除されているようです。
console.error()を使いましょう。

console.group(), console.groupCollapsed(), console.groupEnd()

ログをグループ化して出力します。
console.groupCollapsed()だと初期表示の時に、折りたたまれた状態で出力されます。
(個人的にはgroupCollapsedのほうが好きです)
image.png
image.png

console.profile(), console.profileEnd()

プロファイルを記録して、各関数の処理性能の計測などができます。
image.png
image.png

console.table()

配列やオブジェクトを表形式で出力できます。
image.png

見出しをクリックしたら、ソートすることもできます。
image.png

第2引数に値を設定すると、出力するプロパティを絞ることもできます。
image.png

console.time(), console.timeEnd(), console.timeLog()

処理時間を計測できます。
console.time()で計測を開始し、
console.timeLog()で途中経過を出力し、
console.timeEnd()で計測時間を出力し、計測を止めます。
image.png

console.timeStamp()

開発者ツールでPerformanceを計測している間だけ有効になります。
image.png

何のために使うのかよくわかっていないです。

めちゃくちゃ小さく表示されるので、ずっと見つからず何も起こらない…と思って、何度もやり直しました。
この画面ショット撮るために2日かかりました…すみません、愚痴です。

そもそも古い記事だと開発者ツールのTimelineパネルにTimeStampが表示されるのですが、今はPerformanceパネルに集約されているようです。
下記の記事に出会わなければ、見つけられなかったです。
進化を続けるChrome DevToolsの最新情報 2017

console.trace()

スタックトレースを出力し、処理の流れを確認できます。
「この処理どこから呼ばれているんだろう…」というときに使えます。
image.png

まとめ

これだけのconsole.XXX()を使いこなせれば、見づらいデバッグログ地獄からも脱却できますね!

と言いつつ、結局console.log()を打った方が楽だと思ってしまう自分もいますが…

参考

MDN日本語版
MDN英語版

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

本当に0からVue.js環境構築

Homebrewインストール

1. インストールされているか確認

brew -v

バージョンが返ってくればインストールされている。(node.jsインストールへ)

2. brewインストール

  • ターミナルに以下のスクリプトを実行
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
  • パスワードを入力
  • Installation Success! と表示されたらインストール完了。

node.jsインストール

1. インストールされているか確認

node -v
  • バージョンが返ってくればインストールされている。(yarnインストールへ)
  • 返ってこなかった場合はnode.jsインストールへ

2. node.jsインストール

ターミナルに以下のコマンドを入力

brew install nodebrew

nodebrewのバージョンを確認してバージョンが返ってくればインストールされている

nodebrew -v

node.jsインストール準備

nodebrew ls-remote

バージョン番号がいっぱい出てきたら成功

こんな感じのエラーが出てきたら↓

Fetching: https://nodejs.org/dist/v7.10.0/node-v7.10.0-darwin-x64.tar.gz
Warning: Failed to create the file 
Warning: /Users/whoami/.nodebrew/src/v7.10.0/node-v7.10.0-darwin-x64.ta
Warning: r.gz: No such file or directory

curl: (23) Failed writing body (0 != 941)
download failed: https://nodejs.org/dist/v7.10.0/node-v7.10.0-darwin-x64.tar.gz

以下のコマンドでディレクトリ作成

mkdir -p ~/.nodebrew/src

node.jsインストール

nodebrew install-binary stable

バージョン確認

nodebrew ls

以下のように表示されたら

v7.1.0

current: none

表示されているバージョンを有効化する

nodebrew use v7.1.0

もう1度nodebrew lsを入力するとこんな感じになっているはず

v7.1.0

current: v7.1.0

環境パスを通す

echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zprofile

ここでターミナルを再起動する or 以下のコマンドを入力

source ~/.bash_profile

最後にnodeのバージョンを確認

node -v

yarnインストール

brew install yarn

バージョンを確認する

yarn -v

バージョンが返ってくれば成功

Vueインストール(本題)

yarn global add @vue/cli
yarn global add @vue/cli-init

これでVueがインストールされるのでバージョン確認

vue --version

バージョンが返ってくれば成功

Vueプロジェクトを作成する

vue init webpack vue-app

質問がいっぱいくるので選択していく。基本的には全てpackage.jsonで変更できるのであまり気にする必要はない

? Project name (vue-app) //Enterを押す
? Project description (A Vue.js project) //Enterを押す
? Author (名前 <メアド>) //Enterを押す
  Runtime + Compiler: recommended for most users 
  Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONLY allowed in .vue files - render functions are required elsewhere //Runtime+Compilerの方を選択してEnterを押す
? Install vue-router? (Y/n) //nを押してEnterを押す
? Use ESLint to lint your code? (Y/n) //nを押してEnterを押す
? Set up unit tests (Y/n) //nを押してEnterを押す
? Setup e2e tests with Nightwatch? (Y/n) //nを押してEnterを押す
? Should we run `npm install` for you after the project has been created? (recommended) (Use arrow keys)
❯ Yes, use NPM 
  Yes, use Yarn 
  No, I will handle that myself //Yes, use Yarnを選択してEnterを押す

ダウンロードが完了したらディレクトリができているので移動

cd vue-app

必要なモジュールをインストール

yarn install

ローカルでアプリケーションを起動する

yarn dev

以下のように表示されたら成功。http://localhost:8080 に行くと最初のページが表示されている

最後のlocalhostが起動しなかった場合

  • エディタでvue-appを開いてconfig/index.jsに行く
  • 17行目のポート番号を8081に変更する
  • もう一度ターミナルでyarn devを実行

image

vue環境構築完了!

参考文献

https://qiita.com/white0221/items/d371a19b59af4cba8e8b

https://qiita.com/Mitsuzara/items/4dea8c0aa95d6284980a

https://utano.jp/entry/2018/02/vue-cli-genearte-webpack-project/

https://qiita.com/zaburo/items/29fe23c1ceb6056109fd

https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09

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

はじめてのポートフォリオ(技術<制作過程)

プログラミングを始めて半年を迎えます、上野栞音です。
スクールでは主にRailsアプリの作り方を教わり、現在は株式会社Wilicoでインターンとしてお世話になりながら就活中です。
プログラミングも字書きも不慣れなもんで、至らぬ点がありましたら教えてください。

今回はスクール3ヶ月目のフェーズで作成したポートフォリオについて、雑多にはなりますが色々と書き留めていこうと思います。

ポートフォリオ概要

ToT
github
※かんたんログイン("Signin as a trial user")実装済み。

開発コミュニティ向けのQAサイトを想定して制作しました。
詳細はgithubのREADMEに。

開発経緯と目的

今回学習したかったことは大きく分けると2つ。

:thinking: ユーザーのアクションを経て、どのようなデータが集められるのか。
:thinking: そのデータを基にどのような分析結果を返せるのか。

もともと、ユーザーに評価を付与→チャートで表示の機能は絶対に実装しようと決めていました。
きっかけとしてはAI(データの統計や分析)に興味が湧いたからなので、最初はPythonを使って何かしようと考えていたのですが却下。
理由としては、

  1. 初学者が1ヶ月という期限付きで新しい技術を得てアプリを作っても、強みのあるポートフォリオにはならない気がしたから。
  2. 当時教わっていたRailsについての理解が明らかに浅く、その理解度のまま別の技術に手を出すのが腑に落ちなかったから。

そんなこんなで いくつかサイトをチラ見しつつアプリの企画を練りました。
QAサイトの制作に至った理由としては、評価基準の設け方がパッとイメージできたからです。

DB設計

:bulb:静的(不変的)なデータのみを保管するようにするのがベスト!
というのが今回の学びです。開発はSQLite3、本番はMySQLで実装しています。

はじめに考えていたもの

旧UR図.jpg

ユーザーの評価はユーザーテーブルに、それ以外の評価もコメントやスレッドのアクションに紐づけてアップデートする予定でしたが、途中でこんがらがってスクールのメンターさんに相談。
諸々差し置いて問題点として大きかったのが、この設計だとユーザーが何か操作するたびにアップデートが掛かってしまうため、正常にアップデートされない可能性があることです。(回避方も色々あるみたいですが、もっと掘り下げたいので省略)

後々算出できるデータやユーザーの動作に依存して頻繁にデータが更新されるものは、理由がない限りDBで管理しないようにするため、分析するための材料だけ保管してチャートを描画するタイミングで算出する設計にしました。

最終的な設計

最新UR図.jpg

赤い部分をごっそり消しましたが、時間系の評価については他の評価に比べて算出するステップが一つ多い(2つのテーブルに登録されている登録日を基に差分を算出 → その差分を基に平均値算出)こともあり、集計のスピードを上げるために用途を変えてテーブルを残しています。

基本的な機能の実装

チャート実装を除いた部分です。チャートに時間が割きたかったこともあり、検索機能以外を1週間くらいでスケジュールを組んで実装しました。
さして難しい機能は実装してないのでアピールポイントを挙げると、
:information_desk_person:タグ付け機能と検索機能はgemを使わずに実装しました
改めて調べてみると「gemで出来たのでは…?」なんて思いますが、いい運動(?)になったので結果オーライ:confetti_ball:

以下備考
  • タグ付け機能
    • 同じ意味なのに違う表記(rails, Railsみたいな)のデータが入ると分析の精度が下がってしまうため、新規タグ作成の動作を重くしたかった
    • 基本1つのtext_fieldにカンマ区切りで書き込むようなやり方しか見つからず軽いなぁと悩んだあげく、普通に中間テーブルで結んでフォーム作った方が慣れたやり方だし早そうだと判断。
    • 実装後、Qiitaのタグ付け機能を見て目ぇひん剥きました。なるほど。この手があったか。なるほど…
  • 検索機能
    • 検索対象や解決/未解決フラグごとなど少し細かく条件を指定して検索できるようにしたかった。
    • 改めて調べてみるとアドバンストモードでいけそう。当時触ってみたけど、このモードを理解するより自力で実装した方が早いなと判断。(条件分岐はスクールの応用課題で実装済み、or検索はこのサイトを参考に実装。)

チャート実装

開発経緯の通り、今できる事→これやりたい!軸で企画を立ててここまで来たので、為せば成る精神で詳しい実装の目論見はほぼありませんでした。
調べてみるものの、当時jsに馴染みが無さ過ぎて悶絶寸前。

:fearful:何をどうすれば これが出来るんですか…?
自分で考えたアプリのくせに、ここに来て教室の隅で静かに絶望してました。

この時の学びとして大きかったのは、
:raised_hands:分からない、初めて触るものは一度触ってみる大切さ

何も分からず嘆いていた最中、スクールの同期生に相談したらchart.jsのcodepenを教えてくれました。
ここで少し触ってみた瞬間、chart.jsが面白いほど読める。要因としては、すでに完成しているコードを触れることが大きかったと思います。
どの値がどの軸のデータに影響しているかや、どの値がどのデザインに影響しているかなどが直観的に分かり、ここに配列渡せば勝ちじゃん!とゴールを定める事が出来ました。

ちょっと無謀にも思いますが、試行錯誤も含めて工数を割くために基本的な機能 頑張ったので潔く実装に移って良かったなと思います(結果論)。

いざ、尋常にチャート実装

jsファイルとのデータの受け渡しは、gem 'gon'で行っています。
json形式に変換するのが基本ですが学習目的に含まれていないのと、これから実装するチャートの工数が読みきれずスピードを重視したかったので採用しました。

大体こう。

  1. UserModelのロジックでチャートに渡す配列を計算するメソッド作成、Controllerで呼び出す
  2. UsersControllerでgonに渡す
  3. Viewでgonのタグ→jsファイルに渡す
  4. jsファイル→canvasタグに渡してチャート描画

※自分が流れを掴むために搔い摘んだものです。gon周りは特にもう少し検証しながら理解を深めたい。

UserModelのロジックは、大体こう。

コードがぼちぼち長いため、流れだけ伝わりますようにといった感じで書きます:pray:
代わりと言っては何ですが行単位でGithubのリンクを貼るので、気になる方はご覧ください。
評価基準ごとにまとめます。

:cactus: Questioner/Tags, Answer/Tags

ドーナツチャートの2項目です。少しデータを引っ張るロジックが違うだけなので、Questioner/Tagsを例にします。

  1. 対象のユーザーが投稿したIssueに紐づくタグを算出
  2. タグの名前だけを格納した配列を作る
  3. 対象のユーザーが投稿したIssueに紐づくタグを算出
  4. タグの割合だけ格納した配列を作る

:cactus: Time to response, Time to solved, Total

バーチャートとレーダーチャートの項目です。
データを引っ張ってくるテーブルが違うだけなので、Time to responseを例にします。
ドーナツチャート以外は基本この流れです。細かい処理は省きます。

  1. 全ユーザーの平均値を算出
    ・ユーザーの動作に依存してグラフの階級を変えるため、このデータを基に基準になる値を算出します。
  2. チャートの諸々を決めるのに使う値を算出
    ・返すのは各ユーザー平均値の [ 最小値, 最大値, それを基にした階級幅 ]。
    ・投稿されたIssueが1つも無い場合はfalseで返して例外処理。
  3. 2を基に境界値を算出
    ・〇秒~〇秒のユーザーはスコア1、〇秒~〇秒のユーザーはスコア2… の〇だけ入ったような配列です。
  4. 3の境界値を基に
  5. 各階級に何人ユーザーが含まれるかを算出して配列を作りながら
  6. 各階級に対象のユーザーが含まれるか否かを0,1で算出して配列を作る
  7. 6の配列を基に、ユーザーのスコアを算出
    ・3点の場合、[0,0,1,0,0,0,0,0,0,0] → each_with_indexで回すと2番目の値が1になる → 2+1でスコア算出
  8. 5,7をControllerに返す
  9. レーダーチャートの配列だけControllerで作ります。
    ・ ControllerからModelのメソッドを呼び出す際、Model上に配列を作ろうとすると呼び出すたびに配列がリセットされるためです。
    ・[ 5(チャートに渡す値), 7(ユーザーのスコア) ] の配列が返り値なので、この配列の[1]を拾って配列を作ります。

:cactus: ほか

Time to response とほぼ同じなので、差分だけまとめます。

1.平均値
平均値が割り出せるほど絶対値が大きくなかったため合計値を算出しています。チャートの値が0ばかりになって変わり映えしなかった:frowning2:

3. 階級の算出
階級の誤差をスコア1に寄せるため、呼び出すメソッドを変えています。
評価基準によってスコアが高くなる条件が分岐するのが肝で、
Time to ~ → 1.平均値算出の結果が低いと高スコアcalculate_evaluation_datas_sort_by_max
それ以外 → 1.合計値算出の結果が高いと高スコアcalculate_evaluation_datas_sort_by_min
大きく違う点としては、階級を決める基準が 最大値 → 最小値 であること(こちら基準で命名してます)と、配列をreverseするタイミングです。

リファクタリング

制作期間が 2/15~3/15 くらいだったのですが、3月頭にβ版をデプロイして色んな方にレビューを頂きました。ありがとうございました:sob:

UserModelのメソッド
1. 最初は全てControllerに記述していたメソッドを
2. Modelに移行して
3. 共通するロジックをメソッド化(最新版)

Viewの描画まわり
1. HTMLの部分テンプレートでscriptタグをrenderしていたのを
2. jsファイルに移行して(リンクはcomment_tags)
3. 一つのファイルにまとめて共通するロジックをメソッド化(最新版)

チャートまとめ

このポートフォリオにおいて最大の学びでもあるのですが、
:wave:手を動かせば必ず答えは見つかる!
と確信を得られた制作物でした。
今扱えるパラメータを読み、それを基にロジックを組み、足りなければパラメータを送る。これを自分自身の経験をもって得られたのはとても貴重な学びだったなと思います。

全てのチャートを最低限実装するまでの1週間くらいはコンソール画面にかじりついて模索する日々でしたが、ロジックができた瞬間の達成感が最高すぎて何だかんだ楽しかったです:v:

さいごに

ポートフォリオについて調べていると「こういうのが有利!」ばかりで自分と同じレベル感の記事が上手いこと見つからず、望んでいた判断材料では無かったので書いてみました。

もちろん まだ改善の余地があるアプリとは思いますが、キリがないので一旦区切りにしようかなと思います。

これからポートフォリオを制作する方の目に留まり、少しでもインスピレーションの助けになれば幸いです:ramen:
あと色んな方のこんな感じの記事見たいので是非書いてください:eyes:

ありがとうございました!

参照
Ransackで簡単に検索フォームを作る73のレシピ
railsで複数ワードでの検索機能(or)とマイナス検索機能(-)を実装してみる
chart.jsのcodepen
gem 'gon'

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

microCMSのプレビュー機能をGatsby.jsで使う

Gatsby.jsは言わずと知れたReact製の静的サイトジェネレーターですが、ビルド時にページを生成するという性質ゆえに、microCMSのプレビュー機能を使うにはひと工夫必要だったので共有したいと思います。
この機能をご存知の方はコードの部分からご覧ください。

microCMSの記事プレビュー機能について

microCMSのプレビュー機能について少し説明しておくと、記事執筆画面で
image.png
公開ボタンの左にある画面プレビューを押した時に、執筆中の記事が実際にはどのように表示されるか確認できる機能です。仕組みとしては、

  • 「画面プレビュー」をクリック
  • 予め設定しておいたURLの変数部分(contentsIdとdraftKey)がクリックした記事のものに置き換えられたURLにアクセス image.png

という感じです。
詳しくはhttps://microcms.io/blog/draftkey_and_preview/ に掲載されています。

 何が問題でどうしたらいいのか?

この機能をGatsbyで使用する時、考えなければならないのは、Gatsby.jsの記事データは基本ビルド時に生成された物なので、
「画面プレビュー」クリック時に送られてきたcontentIddraftKeyを基に記事データをリアルタイムに取得して、表示する仕組みを作らないといけない。ということです。

 ということでコード

前置きはそのくらいにしてコードはこのようになりました。

pages/previewPage.jsx
import React, { useState, useEffect } from "react";
import { getSearchParams } from "gatsby-query-params";
//...その他コンポーネントなど

function previewPage() {
  //microCMS側に設定するurlはhttps://xxxxxx.com/previewPage/?contentId={CONTENT_ID}&draftKey={DRAFT_KEY}みたいな感じ
  const queryParams = getSearchParams();

  //queryParamsには
  //{contentId:"xxxx",
  //draftKey:"xxxx"}
  //というようなデータが入ります。

  const contentId = queryParams.contentId;
  const draftKey = queryParams.draftKey;

  const [postData, setPostData] = useState(null);//最初、postDataにはnullが入ります。

  useEffect(() => {
    if (!postData) {
      fetch(
        `https://xxxxxxxxx.microcms.io/api/v1/blogs/${contentId}?draftKey=${draftKey}`,
        {
          headers: {
            "X-API-KEY": "xxxxxxx-xxxx-xxxxx-xxxx-xxxxxxxx",
          },
        }
      )
        .then((response) => {
          if (response.ok) {
            return response.json();
          }
        })
        .then((json) => {
          postData = setPostData(json);//ここで、postDataに取得したコンテンツが格納されます。
        });
    } else {
      return function cleanup() {
        console.log("done");
      };
    }
  });
  return (
        <div
          dangerouslySetInnerHTML={{
            __html: postData ? postData.sentence : "",
          }}
        ></div>
  );
}

export default previewPage;

このようなページを作成することで、アクセスした時にurlクエリパラメータの情報を基にコンテンツをとってきてくれます。
以下、詳しく説明していきます。

API部分について

microCMSはPOSTとGETのAPI-KEYが違うのでGETのAPIが露出しても大して問題はないですが、なんとなく気持ちが悪いので、
X-API-KEY: "xxxxx-xxxx"としている部分とmicroCMSのurl部分は実際には環境変数に置き換えるのが望ましいです。環境変数の使い方に関してはこちらの記事の中でも説明しているので是非ご覧ください。

gatsby-query-paramsについて

2行目に登場するgatsby-query-paramsこちらです。
使用するにはターミナルでnpm i gatsby-query-paramsしてください。

コメントアウト部分にもありますが、getSearchParamsによって、urlクエリパラメータの情報をqueryParamsにオブジェクト形式で格納しています。

useState useEffectについて

これらはReactのhookで、詳しい説明はこちらにあります。
useEffectの中で、↑で取得したクエリパラメータを基にmicroCMSにデータを取得しにいき、取得できたら、それをpostDataに格納しています。

responce.okについて

これがないとmicroCMSのレスポンスが404の時(useStateがnullの間)返ってくる空文字列""を処理しようとしてしまい、jsonオブジェクトでないのでエラーが出てしまいます。

レンダリング部分について

return()内のpostData ? postData.sentence : ''としている部分で、postDataに↑でとってきた記事データが格納されたら、それが表示されるようになっています。postData.sentenceと書いていますが、sentenceの部分はmicroCMSで定義した表示したいデータのフィールドIDが入ります。レンダリング部分は他の記事ページのレイアウトに合わせて、煮るなり焼くなりしてください!

まとめ

サイトを見にきた人が、/今回作ったページと打つとアクセスできてしまいますが、contentsIdとdraftKeyがないので何も表示されないですし、一応は問題ないと思います。

色々と改善の余地はありそうですが、とりあえずこれで投稿前の記事をプレビューで確認できるので、是非活用してみてください!

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

Chart.jsで横スクロール可能なグラフを作る

概要

Chart.jsでは普通にグラフを作ると画面の幅に合わせて1つのデータ当たりのサイズをレスポンシブに調整して表示してくれます.

とても便利ですが,データの量が多いとかなり見づらくなってしまいます.

そこでデータ1つ当たりの横幅を固定して画面に収まりきらない分は横スクロールで見えるようにしようと思いましたが,かなり苦労したのでメモを残します.参考になれば幸いです.

完成図

今回は棒グラフが必要だったので,棒グラフを作成しました.
他の種類のグラフは試していませんが,データ構造が同じようなグラフなら上手くいくと思います.
スクリーンショット 2020-05-26 12.09.53.png
図の下にスクロールバーが描画され,スクロールバーを動かすとスクロールすることができます.
(右の方はラベルしか描画されていませんが,値を入れなかっただけなので気にしないでください.)

実行環境

ブラウザ: Google Chrome
Chart.js バージョン: 2.9.3
CDNを読み込んで使っています

<head>
  <script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js"></script>
</head>

HTML

まずhtmlがこちらです.

<div class="chartWrapper" style="position: relative; overflow-x: scroll;">
  <div class="chartContainer" style="height:200px;"> //高さは好きに設定
    <canvas id="chart" style="position: absolute; left: 0; top: 0;"></canvas>
  </div>
</div>

canvasを2つのdivで囲っています.

Javascript

続いてJavasciptがこちらです.

var xAxisLabelMinWidth = 15; // データ当たりの幅を設定
var data = [12, 19, 3, 5, 2];
var width = data.length*xAxisLabelMinWidth; // グラフ全体の幅を計算
document.getElementById('chart').style.width = width+"px"; // グラフの幅を設定
document.getElementById('chart').style.height = "200px"; //htmlと同じ高さを設定

var myChart = new Chart(document.getElementById('chart').getContext('2d'), {
    type: 'bar',
    data: {
      labels: ['a', 'b', 'c', 'd', 'e'],
        datasets: [{
            label: 'sample data',
            data: data,
        }]
    },
    options: {
        responsive: false, //trueにすると画面の幅に合わせて作図してしまう
    }
});

データ当たりの幅をまず設定して,そこからグラフ全体の幅を計算しています.
これによりデータごとの描画サイズを固定することができます.

完成

以上のコードで横スクロール可能な図を作成できると思います.

参考

https://stackoverflow.com/questions/39473991/how-to-make-a-chart-js-bar-chart-scrollable
https://stackoverrun.com/ja/q/10874652

これらを参考にしましたが,自分の環境ではうまくいかなかったので修正しています.

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

[JS1日クッキング]APIサーバーをCircleCIで自動テスト

何かを簡単に作って、ちょっとした勉強になる。そんなシリーズになる予定のものの第3回です。

今回は、シンプルなAPIサーバーをCircleCIで自動テストをします。テストは前回にしてあるものを使います。

完成品はこちら -> sequelize-todo-api-server

JS1日クッキング まとめページ - Qiita

材料

作り方

1. テストメタデータの設定

テストの結果をテストメタデータとしてファイルへ保存できるようにします。CircleCIでテストした後、テストメタデータをCircleCIへアップロードすると、テスト結果がCircleCIのダッシュボードで確認できるようになります。

Jestのテストメタデータを作成するために、jest-junitをインストールします。

npm install -D jest-junit

jest.config.js というファイルを作り、設定を書いていきます。

jest.config.js
module.exports = {
  reporters: ["default", ["jest-junit", { outputDirectory: "reports/jest" }]],
};

ここまでできたら、npm run testでテストを実行すると、reports/jestディレクトリ内にjunit.xmlが生成されるようになります。この中にテストメタデータが入っています。

2. CircleCIの設定ファイルの用意

npm-scriptに

"test:ci": "export NODE_ENV=test && npx jest --ci --runInBand"

を加えます。--ci--runInBandがCIでJestを使うときに必要になります。

CircleCIで使用する設定ファイルを用意します。設定ファイルは、.circleciディレクトリ内のconfig.ymlに書きます。

.circleci/config.yml
version: 2.1

jobs:
  build:
    docker:
      - image: circleci/node:lts
      - image: circleci/mysql:8-ram
        environment:
          MYSQL_USER: sequelize
          MYSQL_PASSWORD: sequepass
          MYSQL_DATABASE: database_test
    steps:
      - checkout
      - restore_cache:
          name: キャッシュの読み込み
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: パッケージをインストール
          command: npm install
      - save_cache:
          name: キャッシュを保存
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - node_modules
      - run:
          name: db を待機
          command: dockerize -wait tcp://localhost:3306 -timeout 1m
      - run:
          name: JUnit をレポーターとしてテストを実行
          command: npm run test:ci
      - store_test_results:
          name: テスト結果を保存
          path: reports

jobsのbuild内に、使うコンテナと作業を記述していきます。

dockerキーに使用するコンテナを指定します。最初に使うイメージがコマンドを実行するコンテナになります。なので、Node.jsのコンテナを最初に書き、続いてDBのイメージを書きます。DBの設定は環境変数を使用して設定します。circleci/mysqlの環境変数は、MySQLの公式イメージと同じです。

mysql - Docker Hub

ここでは、sequelizeで設定したユーザー名とパスワードだけでなく、使用するデータベースも設定します。使用するデータベースを設定しないとアクセスが拒否されます。

stepsキーに処理を順番に書いていきます。主に、

  • checkoutでリポジトリからデータをダウンロード
  • npm installでライブラリのインストール
  • dockerize -wait tcp://localhost:3306 -timeout 1mでDBが準備できるまで待つ
  • npm run test:ciでCI用のテストを実行
  • store_test_resultsでテストメタデータをCircleCIへアップロード

ということをしています。

3. CircleCIへリポジトリの登録

CircleCIで自動テストができるようにします。CircleCIに登録をした後、「Projects」のページに移動します。

1-project-add.png

「Set Up Project」をクリックします。

2-start-building.png

「Start Building」をクリックします。

3-add-config.png

新しいブランチ作って、そこにデフォルトの設定ファイルを加えるか尋ねられますが、今回は自分で用意したものを使うので、「Add Manually」をクリックします。

4-start-building.png

「Start Building」をクリックします。これで、CircleCIで使用するリポジトリの設定ができました。

4. 自動テストをする

では、実際に自動テストをしましょう。Githubにプッシュすると、自動的にCircleCIが処理を始めます。

「Piplines」で、CircleCIの処理中の様子や結果をみることができます。

5-piplines.png

処理が終わった後、「build」をクリックすると、以下のように処理結果をみることができます。

6-ci-result.png

「TESTS」をクリックすると、テストメタデータからテスト結果を表示してくれます。

7-ci-tests-display.png

テストが失敗しているときは、以下のようにテストメッセージが表示されます。

test-failure.png

5. ステータスバッヂの表示

CircleCIのステータスバッヂをGithubに表示することができます。CircleCIのステータスバッヂは、以下のようなものです。

8-ci-badge.png

これは、

[![CircleCI](https://circleci.com/gh/[Githubアカウント]/[リポジトリ]/tree/[ブランチ].svg?style=svg)]([画像のリンク先(大抵はGithubのブランチ)])

の中の[]を全部埋めると、MDファイルにステータスバッヂを表示できます。例えば、今回使ったブランチだと、

[![CircleCI](https://circleci.com/gh/kei-lvngbk/sequelize-todo-api-server/tree/ci-test.svg?style=shield)](https://circleci.com/gh/kei-lvngbk/sequelize-todo-api-server/tree/ci-test)

のようになります。これをREADME.mdに含めて、Githubでステータスバッヂを表示しています。

詳しくは下のページを参考にしてください。

Adding Status Badges - CircleCI

おわりに

CircleCIで自動テストをしました。workflowを使ってテスト成功後にデプロイをすることもできるので、使いこなせれば楽ができそうです。Netlifyもそうですけど、設定すればGithubにプッシュすれば自動に何かやってくれます系のサービスはとても便利ですね。

コード -> sequelize-todo-api-server

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

【array-foreach-async】forEach()でasync/awaitを使おうとして失敗した皆へ

AWS Lambda関数でNode.jsを使用し、forEach()でasync/awaitしたい状況に陥ったのですが、コードを書いて実行してもawaitの文が実行されていませんでした。色々調べたところ、Node.jsのarray-foreach-asyncというライブラリを利用することで簡単に解決することができたので、まとめていきます。

環境

  • macOS
  • AWS Cloud9
  • Node.js 12

修正前の実行コード

今回の説明において不要なコードは省略しています。

const env = process.env
const requestPromise = require('request-promise');
const AWS = require('aws-sdk');
const DB = new AWS.DynamoDB.DocumentClient();

exports.handler = async(event, context) => {

    // (省略)

    event.Records.forEach(async(record) => { // ここでasync  

        // (省略)

        const data = await DB.put(dbParams).promise(); // 1つ目のawait
        console.log(data);

        // (省略)

        const req = await requestPromise.post(options); // 2つ目のawait
        console.log(req);
    });
    return "success!"
};

実行すると、出力にはsuccess!のみが表示され、console.logは無視されます。forEach()がawaitの処理完了を待たずに終了し、Lambda関数が閉じてしまっているためです。どちらか一方のawaitをコメントアウトしても結果は変わらず。forEach()ではasync/awaitが効いていないことが分かります。
そもそもArray.prototype.forEach()自体がasync関数ではないため、引数のcallback関数にasync/awaitを付けたところで無意味ということです。

修正後の実行コード

Node.jsのarray-foreach-asyncというライブラリをインストールすることで、forEach()をasync関数にしたforEachAsync()を使用できるようになります。
これを使えば、forEach()と同じ動作でasync/awaitを実現できます(参考:array-foreach-async - npm)。

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

npm install array-foreach-async

ソースコードは次のようになります。

const env = process.env
const requestPromise = require('request-promise');
const AWS = require('aws-sdk');
const DB = new AWS.DynamoDB.DocumentClient();
require('array-foreach-async'); // ライブラリの読み込み

exports.handler = async(event, context) => {

    // (省略)

    await event.Records.forEachAsync(async(record) => { // 変更箇所

        // (省略)

        const data = await DB.put(dbParams).promise(); 
        console.log(data);

        // (省略)

        const req = await requestPromise.post(options);
        console.log(req);

    });
    return "success!"
};

forEachforEachAsyncに変更し、文の先頭にawaitを付けます。
実行すると、awaitの文が両方ともきちんと実行され、ログの出力を確認できました。

補足

ずっとforEach()を使うことしか考えていなかったため後から気が付いたのですが、そもそもforEach()の代わりにfor〜ofを使用すれば解決できました。JavaScriptに慣れていなさすぎる...
修正前の実行コードを次のように変更すればOKです。

const env = process.env
const requestPromise = require('request-promise');
const AWS = require('aws-sdk');
const DB = new AWS.DynamoDB.DocumentClient();

exports.handler = async(event, context) => {

    // (省略)

    for(const record of event.Records) {

        // (省略)

        const data = await DB.put(dbParams).promise(); // 1つ目のawait
        console.log(data);

        // (省略)

        const req = await requestPromise.post(options); // 2つ目のawait
        console.log(req);
    });
    return "success!"
};

コードも簡潔であり、わざわざarray-foreach-asyncをインストールする必要がないので、どうしてもforEach()を使わなければならない状況以外では、素直にfor〜ofを使う方が良いかもしれませんね。

ご参考までに。

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

並列処理数を指定できてキャンセルできる Promise.all の実装

実装

import type { AbortSignal } from 'abort-controller'

export const DOMException = 
  ('undefined' !== typeof window && window.DOMException) ||
  ('undefined' !== typeof self && self.DOMException) ||
    class DOMException extends Error {
      constructor(message: string, name: string) {
        super(message)
        this.name = name
      }
    }

type Options = number | { concurrency: number; signal?: AbortSignal }

export const promiseAllWithConcurrencyLimit =
  async <T>(promises: (() => Promise<T>)[], options: Options) => {
    const opts = 'number' === typeof options ? { concurrency: options } : options
    const { concurrency, signal } = opts
    const results: T[] = []
    let idx = 0
    await Promise.all(Array.from({ length: concurrency }).map(async () => {
      while (true) {
        if (signal?.aborted) 
          return Promise.reject(new DOMException('aborted', 'AbortError'))
        const cur = idx++
        const task = promises[cur]
        if (!task) return
        results[cur] = await task()
      }
    }))
    return results
  }

記事を書くまでの経緯

以前、並列処理数を指定できる Promise.all の実装という記事を書きました

長時間かかるタスクの実行中になんらかの要因でやっぱり中止したいと思うかもしれません
fetch の仕様を参考にキャンセルできるようにしたのが上記になります

使用例

const controller = new AbortController()
const onCancelButtonClicked = () => {
  controller.abort()
}
try {
  await promiseAllWithConcurrencyLimit(
    userIds.map(id => async () => fetchUserById(id)),
    { concurrency: 3, signal: controller.signal }
  )
} catch (err) {
  if ('AbortError' !== err?.name) throw err
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelさんはどうやってAJAXかどうか区別してResponseでJSON返してくるの?

我らがBOOM TECH CAFEの若きエース、 @miriwo さんの最新記事「Laravel API データポスト時のバリデーションで弾かれた後、http://127.0.0.1:8000/にリダイレクトしてしまう」を読んで、「おや? ワイのLaravelちゃんはAJAXでアクセスすると普通にエラーのJSON返してくるんやが……?」と思って、調べてみました。

そもそもAJAXかどうかどうやって判定しているの?

Laravelの公式ドキュメント日本語訳にはこんなふうに書かれてるわけですよ。
https://readouble.com/laravel/5.5/ja/validation.html

※一応、Laravel5.5での話をしていますが、後方バージョンでもだいたい同じだと思います。

伝統的なHTTPリクエストの場合は、リダイレクトレスポンスが生成され、一方でAJAXリクエストにはJSONレスポンスが返されます。

ほうほう、そうなんすね。
んで、AJAXリクエストか伝統的なHTTPリクエストかどうかはどこで判別してるねんと言うと、ここなわけです。

vendor/laravel/framework/src/Illuminate/Http/Request.php(226-234)
    /**
     * Determine if the request is the result of an AJAX call.
     *
     * @return bool
     */
    public function ajax()
    {
        return $this->isXmlHttpRequest();
    }
vendor/symfony/http-foundation/Request.php(1820-1833)
    /**
     * Returns true if the request is a XMLHttpRequest.
     *
     * It works if your JavaScript library sets an X-Requested-With HTTP header.
     * It is known to work with common JavaScript frameworks:
     *
     * @see http://en.wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript
     *
     * @return bool true if the request is an XMLHttpRequest, false otherwise
     */
    public function isXmlHttpRequest()
    {
        return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
    }

なるほど、つまりRequest Headerに X-Requested-With: XMLHttpRequest ってのが入ってたらAJAXリクエストとして扱うってわけね……え、そんなHeaderどこで入れてたっけ……?

そして我々は X-Requested-With を求めて旅に出た

ソースグレップするとこんなところにいるのを見つけます。

resources/assets/js/bootstrap.js(16-24)
/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

window.axios = require('axios');

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

なるほど、axiosのHeaderにデフォルトで入るようにしてるのね……え、でもBootstrap使ってないんやが……
そこで、肝心のAJAXしてるjsをのぞいてみると……

require('./bootstrap');

バッチリrequireしてたーーーーーー!!

やー、無意識って怖いっすね。今回はいい勉強になりました。

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

LaravelさんはどうやってAJAXかどうか区別してバリデーションエラーレスポンスでJSONを返してくるの?

我らがBOOM TECH CAFEの若きエース、 @miriwo さんの最新記事「Laravel API データポスト時のバリデーションで弾かれた後、http://127.0.0.1:8000/にリダイレクトしてしまう」を読んで、「おや? ワイのLaravelちゃんはAJAXでアクセスすると普通にバリデーションエラーのJSON返してくるんやが……?」と思って、調べてみました。

そもそもAJAXかどうかどうやって判定しているの?

Laravelの公式ドキュメント日本語訳にはこんなふうに書かれてるわけですよ。
https://readouble.com/laravel/5.5/ja/validation.html

※一応、Laravel5.5での話をしていますが、後方バージョンでもだいたい同じだと思います。

伝統的なHTTPリクエストの場合は、リダイレクトレスポンスが生成され、一方でAJAXリクエストにはJSONレスポンスが返されます。

ほうほう、そうなんすね。
んで、AJAXリクエストか伝統的なHTTPリクエストかどうかはどこで判別してるねんと言うと、ここなわけです。

vendor/laravel/framework/src/Illuminate/Http/Request.php(226-234)
    /**
     * Determine if the request is the result of an AJAX call.
     *
     * @return bool
     */
    public function ajax()
    {
        return $this->isXmlHttpRequest();
    }
vendor/symfony/http-foundation/Request.php(1820-1833)
    /**
     * Returns true if the request is a XMLHttpRequest.
     *
     * It works if your JavaScript library sets an X-Requested-With HTTP header.
     * It is known to work with common JavaScript frameworks:
     *
     * @see http://en.wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript
     *
     * @return bool true if the request is an XMLHttpRequest, false otherwise
     */
    public function isXmlHttpRequest()
    {
        return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
    }

なるほど、つまりRequest Headerに X-Requested-With: XMLHttpRequest ってのが入ってたらAJAXリクエストとして扱うってわけね……え、そんなHeaderどこで入れてたっけ……?

そして我々は X-Requested-With を求めて旅に出た

ソースグレップするとこんなところにいるのを見つけます。

resources/assets/js/bootstrap.js(16-24)
/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

window.axios = require('axios');

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

なるほど、axiosのHeaderにデフォルトで入るようにしてるのね……え、でもBootstrap使ってないんやが……
そこで、肝心のAJAXしてるjsをのぞいてみると……

require('./bootstrap');

バッチリrequireしてたーーーーーー!!

やー、無意識って怖いっすね。今回はいい勉強になりました。

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

Javascript用語集

5-3イメージの切り替え

:sunny:マッチする要素を全て取得する

document.querySelectorAll('CSSセレクタ')

例:const thumbs = document.querySelectprAll('.thumb');

:sunny:配列の各項目を繰り返し処理する(forEachメソッド)

配列.forEach(function(item,index){
  処理内容をここに書く
});

:sunny:Javascriptでdata-なんでも属性の値を切り替える

<タグ名 data-A(自由に決めて良い) = "img1.jpg">

取得した要素.dataset.A

item.onclick =function(){
  console.log(this.dataset.image);
}

属性の書き換えについて

:sunny:属性の値を読み取る

取得した要素.属性

:sunny:属性の値を書き換える

取得した要素.属性 = 値;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】jQueryはじめの一歩

はじめに

理解せずに進めていってしまいそうなので、基本に立ち返るための忘備録
基本はjQuery 日本語リファレンスに書いてあることだけど、用語が全然わからない人(自分が筆頭)でも理解できるように書くつもり。

ただひとつ、jQueryは覚えてなくてもググればいいというのは確実なので、その前段階にいる人向けの文章です。

jQueryとは

jQuery 日本語リファレンスによれば

JavaScriptのコーディングを強力に支援するライブラリ

らしい。分かるような分からないような日本語だなあ。
私自身はざっくり「JavaScriptを少ない記述で簡単に使えるようにいろいろなコードがまとめられたもの」だと解釈しています。

もう少しエンジニアっぽい表現をすると、「JavaScriptのさまざまな機能を簡単に実装できるコード集」あたりでしょうか。
つまりやっていることはJavaScriptのプログラム(クリックすると色が変わったり、マウスをあてると大きくなったりなど)だけど、いちいちコードをイチから考えて記述するのが面倒だから、最初からある程度の形を用意しておいてそこから読み込めるようにしたものがjQueryの正体です。

jQueryを使う手順

まずはjQueryを使う場所の確認。

  • HTMLファイル、cssファイル、JavaScriptファイルを用意
  • HTMLファイルにjQueryを読み込む
  • JavaScriptファイルにjQueryコードを記述

おそらくファイルはHTMLとJavaScriptのみでも大丈夫だけど、メソッドの中にはcssプロパティを変化させる.css()というのもあるので一応あるほうがベター。

jQueryの始め方

いよいよ本題!とは言え始めるといっても、説明した通りjQueryは読み込むものなので読み込めばほとんど終わり。

  • 公式サイトからjQueryファイルをダウンロードして読み込む
  • Web上のjQueryファイルを読み込む

方法としてはこの2パターンのどちらかだけど、2番目のWeb上のものを読み込む方法(CDNと言うらしい)のほうが手軽。私もこちらを習いました。
一応どちらもやり方は記述します。

公式サイトからjQueryファイルをダウンロードして読み込む

ダウンロードページからダウンロードして、jQueryを使いたいファイル内の</body>直前に以下を記述。

このファイルでjQueryを使う.html
<script src="jQueryの保存先ファイル名/jquery-min.js"></script>

Web上のjQueryファイルを読み込む

jQueryを使いたいファイル内の</body>直前に以下を記述。

このファイルでjQueryを使う.html
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
余談1

記述場所が</body>直前の理由。
2つとも記述場所(=読み込む場所)は別にどこでも良いけど一旦ページの表示内容をすべて読み込んでからjQueryを読み込むほうが、ユーザーがjQueryの読み込み時間を気にすることなくページを閲覧できる、ということらしい。なるほどね。
たいていjQueryの発動場所は「このボタンをクリックしたら」とか「ここにカーソルが当たったら」とかなので、ユーザーがページを読んでいるうちに読み込めばよいと。

余談2

ファイルの保存先(保存されている場所)のことをファイルのパスファイルパスパスなど呼称する。やっているうちに覚えたのであんまり気にしなくていいと思います。

jQueryを使ってみる--基本の型

sample.js
$(function(){
  //ここにメインの処理を記述
});

jQuery(function(){
  //ここにメインの処理を記述
});

基礎の基礎はこれ。

$jQueryは同じ意味。$が省略版という立ち位置らしいけどまあこっち使うよねという。
以下の記述も$で統一します。

sample.js
$(function(){
  $("セレクタ").メソッド("引数");
});

基礎はこれ。jQueryの中身は3つの要素で構成されている。

  • セレクタ:どこが変化するかの場所を示す。
  • メソッド:どんな動作をするのかを示す。
  • 引数  :メソッドの詳細を指定。

sample.js
$(function(){
  $("h1").css(color, "#ffff00");
});

これはhtmlファイルでh1タグの部分のcssプロパティ「color#ffff00」に変化させるメソッド。

jQueryを使ってみる--メソッド

まずはサンプルページのhtml。これをいろいろいじっていきます。

表示されるページ.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>サンプルサイト</title>
        <link href="/style.css" rel="stylesheet" />
    </head>
    <body>
        <header>
            <h1>Sample Page</h1>
        </header>

        <div id="samplePage">
            <h2>ようこそサンプルページへ</h2>
            <ul class="akasa">
                <li>あいうえお</li>
                <li>かきくけこ</li>
                <li>さしすせそ</li>
            </ul>

            <p class="tanaha">たちつてと</p>
            <p class="tanaha">なにぬねの</p>
            <p class="tanaha">はひふへほ</p>
        </div>

        <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
    </body>
</html>

なにも指定していないので、表示はこんな感じ。白い。
flat.PNG

セレクタによる記述の違い

少しずつ実例を出します。

sample.js
$(function(){
  //色を指定するのはどちらでもできる
  $("h2").css("color", "#0000ff");
  $("li").css("color", "blue");

  //cssで指定するものであれば色以外のプロパティもOK
  $("#samplePage").css("margin-left", '50px');

  $(".tanaha").css("color", "red");
});

結果
1.PNG

解説
h1h6ul liimg pなどhtmlで最初から用意されているものをタイプセレクタ(要素セレクタ)と言って、特に何もつけずダブルクォーテーション” ”で囲む。
ここではh2li(リストの子要素)の色を#0000ffに指定した。

htmlでid="〇〇"として自分が付与したIDに変化をつけたいときは、ID名の前にシャープ#をつけてダブルクォーテーション" "で囲む。
ここではsamplePageIDに左側の余白(mairgin-left)を指定した。

htmlでclass="〇〇"として自分が付与したクラスに変化をつけたいときは、クラス名の前にドット.をつけてダブルクォーテーション" "で囲む。
ここではtanahaクラスの色をredに指定した。

メソッド色々

正直あんまり覚えなくていい(どうせググればいくらでも出てくる)ので軽く表にしただけ。

メソッド 動き
.css() CSSプロパティを変化させる
.on() イベント発生時に実行する関数を割り当てる(後述)
.text() 要素の文字列を取得したり書き換えたりする
.fadeIn() 要素を徐々に表示する
.fadeOut() 要素を徐々に薄くしていき最後は非表示にする
.hide() 要素を非表示にする

jQueryを使ってみる--イベント

イベントについて

多分jQueryの本領。
メソッドと組み合わせて使うもので、「イベントが起きた時メソッドの処理をする」という動きを与える。…よくわからないと思うので↓で説明します。

sample.js
$(function(){
  $("セレクタ1").イベント(function(){
    $("セレクタ2").メソッド("引数");
  });
});

セレクタ1イベントが起きた時、セレクタ2メソッドの変化が起こる。

sample.js
$(function(){
  $("h2").click(function(){
    $(".akasa").fadeOut();
  });
});

htmlファイルでh2タグの部分をクリックすると、akasaクラスがフェードアウトするイベント。
結果
2.PNG
h2タグである「ようこそサンプルページへ」の部分をクリックすると・・・
3.PNG
akasaクラスを与えた要素(リスト部分)が消えた。
実際はちゃんとゆっくりフェードアウトするのですが、私がGIF動画を作る技術がないのでお見せできず申し訳ないです。

イベント色々

例によって雑な表。当然ほかにもいろいろある。

イベント タイミング
.click() クリックしたら
.mouseover() マウスをのせたら(その後もずっとメソッド発生)
.mouseout() マウスを外したら(その後ずっとメソッド発生)
.hover() マウスをのせているその時間のみ(詳細は次)

hoverイベント

hoverイベントはほかのイベントと違い、「マウスをのせている間」と「マウスを外した後」の2種類の処理について記述が必要になる。

sample.js
$(function(){
  $("セレクタ1").hover(
    function(){
    $("セレクタ2").メソッド2();
    },
    function();{
    $("セレクタ3").メソッド3();
    });
});

メソッド2がマウスをあてている時のメソッド、メソッド3がマウスを外した時のメソッド。
ほとんどの場合、セレクタ2セレクタ3は同じになるしメソッド2メソッド3は対になる動きになると思う。

sample.js
$(function(){
  $("h2").hover(
    function(){
    $(".akasa").css("color", "blue");
    },
    function();{
    $(".akasa").css("color", "red");
    });
});

htmlファイルでh2タグの部分にマウスをあてるとakasaクラスが青くなり、マウスを外すとakasaクラスが赤くなるイベント。

thisについて

イベント発生時、その対象のみにメソッドが適用される。

sample.js
$(function(){
  $(".tanaha").click(function(){
    $(this).css("font-weight", "bold");
  });
});

結果
tanahaクラスの中でも「なにぬねの」をクリックすると・・・
4.png
クリックした対象のみが太字になり、ほかのtanahaクラスに変化はない。

jQueryを使ってみる--onメソッド

最後の難関、onメソッドについて。基本の型は

sample.js
$(function(){
  $("セレクタ1").on(イベント名, セレクタ2, データ, 関数)
});

それぞれの要素の説明をすると

  • セレクタ1 :対象とするセレクタを指定
  • イベント名:発生するイベントを指定
  • セレクタ2 :さらにセレクタを指定したいときに設定
  • データ  :任意のデータを渡したいときに設定
  • 関数   :イベント発生時に実行したい処理を記述

データセレクタ2は任意で設定するものなので、一番シンプルな形にすると

sample.js
$(function(){
  $("セレクタ1").on(イベント名, 関数)
});

じゃあ関数ってどんなもんかという話ですね。
サンプルページにボタンを追加します。

sample.html
<button>ここをクリック</button>

javaScriptファイルに、clickを使用したonイベントを追記します。

sample.js
$(function() {
  $("button").on("click", function(){
    $("button").text("クリックされました");
 })
});

5.PNG
ボタンをクリックすると・・・
6.PNG

htmlで<button>要素を付与した文字列が書き換わりました。

onイベントのいいところ①同じ要素に複数のイベントを追加できる

イベント名部分に複数設定できる。イベント名同士はスペースで区切る。

sample.html
<button>一度マウスを外してね</button>
sample.js
$(function() {
  $("button").on("dblclick mouseleave", function(){
    $("button").text("ダブルクリックでも同じだよ");
 })
});

7.PNG
ボタン部分に一度マウスをのせて外すと下の画像の表示になる。
ダブルクリックでも同じ挙動をする。
8.PNG

onイベントのいいところ②イベントごとに処理を設定できる

上ではスペース区切りで一気に指定しましたが、連想配列の形でイベントそれぞれに違う処理を設定することができます。

sample.js
$(function() {
  $("button").on({
    "dblclick": function(){
      $("h1").css("color", "red")
    },
    "mouseleave": function(){
       $("button").text("ダブルクリックだとh1が赤くなるよ")
    }
  });
});

9.PNG
同じページ
10.PNG
ボタンをダブルクリックしてみる
11.PNG

onイベントのいいところ③デリゲート

シンプルなコードだとわかりにくい利点。
親要素に対してイベントを設定し、引数に子要素を指定することで、あとから追加した子要素にもイベントが適用される。
(※めちゃくちゃざっくり説明してます)

sample.js
$(function() {
  $("ul").on("click", "li", function() {
    $(this).css("color", "magenta");
  });
});

12.PNG

クリックしたli要素が濃いピンクになった。
これだけだとわかりにくいが、今後サンプルページに情報が増えてliの中身がなんらかの処理によって増えた時でも同じ挙動をする。
メインの起動セレクタをulではなくliで指定してしまうと、要素が増えた時には対応できなくなってしまうらしい。

onイベントのいいところ④オブジェクト形式のデータを関数に設定できる

onイベントが発生した時に行われる処理をオブジェクト形式のデータにすることで、より複雑な処理をすることが可能になる。

sample.js
$("button").on("click", btnClick);

function btnClick(){
  alert("ボタンがクリックされました!");
}

13.PNG
ボタンをクリックすると・・・
14.PNG
アラートが出る。

関数の名前は何でもいいし、基本はどんな処理も(関数であれば)できるので、onイベントが使われるのはほとんどこのためじゃないかなと思う・・・。
関数を別のファイルにまとめておいて、読み込む形にすればコードもスッキリする。


参考ページ
実践、.on()とoff()を使いこなす
便利なonを使おう(on click)

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

【備忘録】ブラウザで音声認識する

ブラウザに内蔵のAPIで音声認識する場合。
パラメータで認識中の途中結果を取得したり、連続で認識するといった変更はあるけど、とりあえず日本語の設定のみ。

検証環境

  • Google Chrome バージョン: 81.0.4044.138

ソースコード

html
<button id='start'>開始</button>
javascript
const start  = document.getElementById('start');

SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;

const recognition = new SpeechRecognition();
recognition.lang = 'ja-JP';

recognition.onresult = (event) => {
  console.log(event.results[0][0].transcript);
}

start.onclick = (event) => {
  recognition.start();
};

チラシの裏(読まなくてもいい余談)

試してみると、結構正確に認識してくれます。音声の文書起こしをしたいときにわざわざ外部サービスを使わなくていいので便利です。
どちらかといえば、どんな局面で使うのか、を考える方が重要ですね。

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

【Rails】turbolinksを無効化する方法

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

完全に無効化する方法

1.Gemを無効化

Gemfile
# コメントアウトする
# gem 'turbolinks', '~> 5'
ターミナル
$ bundle update

2.application.jsを編集

=を削除する。

application.js
// 変更前
//= require turbolinks 

// 変更後
// require turbolinks 

3. application.html.slimを編集

'data-turbolinks-track': 'reload'を削除する。

application.html.slim
/ 変更前
= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application', 'data-turbolinks-track': 'reload'

/ 変更後
= stylesheet_link_tag    'application', media: 'all'
= javascript_include_tag 'application'

部分的に無効化する方法

1.JavaScriptを編集する方法

~.jsファイルの場合

~.js
$(document).on('turbolinks:load', function() {
  // turbolinksを無効化したい処理
});

~.coffeeファイルの場合

~.coffee
$(document).on 'turbolinks:load', -> 
  # turbolinksを無効化したい処理

2.リンクを編集する方法

①link_toに属性を追加する場合

~html.slim
= link_to '', root_path, 'data-turbolinks': false

②divで囲う場合

~html.slim
div data-turbolinks='false'
  = link_to '', root_path
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Google Mapに複数マーカーを表示し、クリックしたら吹き出しを出す方法

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
ログイン機能実装
Google Map表示
Gocoding APIで緯度経度を算出

実装

1.コントローラーを編集

users_controller.rb
def index
  @users = User.all
  gon.users = User.all
end

2.ビューを編集

users/index.html.slim
#map style='height: 500px; width: 500px;'

- google_api = "https://maps.googleapis.com/maps/api/js?key=#{ ENV['GOOGLE_MAP_API'] }&callback=initMap".html_safe
script{ async src=google_api }

javascript:

  let map;
  let marker = []; // マーカーを複数表示させたいので、配列化
  let infoWindow = []; // 吹き出しを複数表示させたいので、配列化
  let markerData = gon.users; // コントローラーで定義したインスタンス変数を変数に代入

  function initMap() {
    geocoder = new google.maps.Geocoder()

    map = new google.maps.Map(document.getElementById('map'), {
      center: { lat: 35.6585, lng: 139.7486 }, // 東京タワーを中心に表示させている
      zoom: 12,
    });

    // 繰り返し処理でマーカーと吹き出しを複数表示させる
    for (var i = 0; i < markerData.length; i++) {
      let id = markerData[i]['id']

      // 各地点の緯度経度を算出
      markerLatLng = new google.maps.LatLng({
        lat: markerData[i]['latitude'],
        lng: markerData[i]['longitude']
      });

      // 各地点のマーカーを作成
      marker[i] = new google.maps.Marker({
        position: markerLatLng,
        map: map
      });

      // 各地点の吹き出しを作成
      infoWindow[i] = new google.maps.InfoWindow({
        // 吹き出しの内容
        content: markerData[i]['address']
      });

      // マーカーにクリックイベントを追加
      markerEvent(i);
    }
  }

  // マーカーをクリックしたら吹き出しを表示
  function markerEvent(i) {
    marker[i].addListener('click', function () {
      infoWindow[i].open(map, marker[i]);
    });
  }

吹き出しの内容ををリンクにしたい場合は下記の様に記述する。

// 各ユーザーのIDを変数化
let id = markerData[i]['id']

infoWindow[i] = new google.maps.InfoWindow({
  // <a>タグでリンクを作成
  content: `<a href='/users/${ id }'>${ markerData[i]['address'] }</a>`
});

注意

turbolinksを無効化しないと地図が切り替わらないので、必ず無効化しておきましょう。

turbolinksを無効化する方法

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

JScriptからダイアログを出す(Powershell、Windows.Forms利用)

例としてログインダイアログ

login.png

login.js
var echo = function (str) {
    WScript.Echo(str);
    // console.log(str)
};

var quit = function () {
    WScript.Quit();
};

var CredentialDialog = function () {
    var code = '\
Add-Type -AssemblyName System.Windows.Forms\r\n\
Add-Type -AssemblyName System\r\n\
\r\n\
$form = New-Object Windows.Forms.Form\r\n\
$form.Size = New-Object Drawing.Size @(400, 200)\r\n\
$form.ControlBox = $false\r\n\
$form.MinimizeBox = $false\r\n\
$form.MaximizeBox = $false\r\n\
$form.FormBorderStyle = [Windows.Forms.FormBorderStyle]::FixedDialog\r\n\
$form.Text = "ログイン"\r\n\
\r\n\
$okButton = New-Object Windows.Forms.Button\r\n\
$okButton.Location = New-Object Drawing.Size @(297, 115)\r\n\
$okButton.Size = New-Object Drawing.Size @(72, 25)\r\n\
$okButton.Font = New-Object Drawing.Font "MS UI Gothic", 12\r\n\
$okButton.TabIndex = 0\r\n\
$okButton.Text = "OK"\r\n\
$okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK\r\n\
$okButton.Add_Click({[System.Console]::Write("OK\t" + $userName.Text + "\t" + $password.Text)})\r\n\
$form.Controls.Add($okButton)\r\n\
$form.AcceptButton = $okButton\r\n\
\r\n\
$cancelButton = New-Object Windows.Forms.Button\r\n\
$cancelButton.Location = New-Object Drawing.Size @(192, 115)\r\n\
$cancelButton.Size = New-Object Drawing.Size @(72, 25)\r\n\
$cancelButton.Font = New-Object Drawing.Font "MS UI Gothic", 12\r\n\
$cancelButton.TabIndex = 1\r\n\
$cancelButton.Text = "Cancel"\r\n\
$cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel\r\n\
$form.Controls.Add($cancelButton)\r\n\
$form.CancelButton = $cancelButton\r\n\
\r\n\
$userName = New-Object Windows.Forms.TextBox\r\n\
$userName.Location = New-Object Drawing.Point @(108, 22)\r\n\
 $userName.Size = New-Object Drawing.Size @(231, 25)\r\n\
 $userName.Font = New-Object Drawing.Font "MS UI Gothic", 12\r\n\
 $userName.TabIndex = 2\r\n\
 $form.Controls.Add($userName)\r\n\
\r\n\
 $password = New-Object Windows.Forms.TextBox\r\n\
 $password.Location = New-Object Drawing.Point @(108, 69)\r\n\
 $password.Size = New-Object Drawing.Size @(231, 25)\r\n\
 $password.Font = New-Object Drawing.Font "MS UI Gothic", 12\r\n\
 $password.PasswordChar = "*"\r\n\
 $password.TabIndex = 3\r\n\
 $form.Controls.Add($password)\r\n\
\r\n\
 $label1 = New-Object System.Windows.Forms.Label\r\n\
 $label1.Location = New-Object Drawing.Point @(12, 25)\r\n\
 $label1.Size = New-Object Drawing.Point @(80, 16)\r\n\
 $label1.Font = New-Object Drawing.Font "MS UI Gothic", 12\r\n\
 $label1.Text = "ユーザーID"\r\n\
 $form.Controls.Add($label1)\r\n\
\r\n\
 $label2 = New-Object System.Windows.Forms.Label\r\n\
 $label2.Location = New-Object Drawing.Point @(16, 72)\r\n\
 $label2.Size = New-Object Drawing.Point @(80, 16)\r\n\
 $label2.Font = New-Object Drawing.Font "MS UI Gothic", 12\r\n\
 $label2.Text = "パスワード"\r\n\
 $form.Controls.Add($label2)\r\n\
\r\n\
 $result = $form.ShowDialog()\r\n\
 ';
    var shellObj = WScript.CreateObject("WScript.Shell");
    var execObj = shellObj.Exec("PowerShell -NonInteractive -WindowStyle Hidden -NoLogo -ExecutionPolicy Unrestricted -Command -");
    execObj.StdIn.Write(code);
    execObj.StdIn.Close();
    var res = execObj.StdOut.ReadAll();
    var rex = new RegExp("^(OK)\t(.*)\t(.*)$");
    var rer = rex.exec(res);
    if (rer) {
        var cred = {
            "userid": rer[2],
            "password": rer[3]
        }
        return cred;
    } else {
        echo("ログイン情報を入力してください");
        quit();
    }
}
var cred = CredentialDialog();
echo(cred.userid + cred.password);

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

jbuilderの扱い方

前提条件

以下のブログを読んでから本記事をお読みください
非同期通信の手始め
respond_toについて

jbuilder

結論、jbuilderを用いるとrender json 〇〇をしなくていいです。
jbuilderは、viewと同じように該当するアクションと同じ名前にする必要があります。
今回はコメント機能の非同期通信を実装するので
commentのcreateアクションに対応するjbuilderのファイルを作成します。
作成する場所はviews/comments/create.json.jbuilderになります。

  json.text  @comment.text
  json.user_id  @comment.user.id
  json.user_name  @comment.user.nickname

このような形で記述します。
とても見やすいですよね。jbuilderの最大のメリットは
見やすく分かりやすくコードがかけることです。
積極的に使っていきましょう。

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

Slackとgoogleカレンダーで勤怠記録をしよう

はじめに

新型コロナウイルスの影響で自粛期間が長らく続いていましたが,6月あたりから感染対策を実施した上で各大学,企業でのが行われるようになりましたね.

僕の大学では研究室の入退室時間を管理することが義務付けられました.これは感染者が出た場合に,感染経路及び感染した可能性のある人を特定するためだと思われます.
他の大学や企業でもこのような感染対策を実施しているのではないでしょうか.

特に管理法についての指定はなかったので,googleカレンダーに記入することで入退室時間を記録することになったのですが,研究室全員で使用するアプリはできる限り少ない方が効率的であるため,指導教員の先生からSlackだけで入退室時間を管理できるようにうまいことやってくれないかと依頼されました.

そこでSlackに勤怠管理チャンネルを作り,「おはよう」や,「さようなら」と呟けばgoogleカレンダーに予定が追加されるプログラムを作成しました.

初投稿であることと,GAS経験が少ないことをご容赦ください.

ユーザーが行う操作のイメージ
slack.PNG

googleカレンダーに名前,時間,場所が登録される
カレンダー.PNG

スプレッドシートにも登録される
spread.PNG

用意するものと使ったツール

  • チームで共有しているgoogleカレンダー
  • 勤怠管理用のSlackのチャンネル
  • Outgoing webhook
  • Incoming webhook
  • Google Apps Script

Outgoing webhook

トリガーワードが呟かれたら反応して色々するアプリです
こちらからSlackの勤怠管理用のチャンネルにOutgoing webhookを追加します.
中身はGoogleAppsScriptで書いていきます.
outgoing.PNG
引き金となる言葉は自由に変更できます.inとoutだけで構いませんが,味気ないので多めに設定しました.
URLは,GoogleAppsScriptのアプリのURLを張り付けるので,この時点では空欄で構いません.
トークンはGoogleAppsScriptで使います.

Incoming webhook

呟いてレスポンスが無かったらカレンダーに追加されたのか懐疑的になるのと,味気ないので,反応してくれるアプリを追加します.
さきほどと同様にこちらから勤怠管理用のチャンネルにIncoming webhookを追加します.
incoming.PNG
レスポンスしてくれるbotのアイコンはここで設定できます.

Google Apps Script

googleドライブから,Googleスプレッドシートを新規作成します.
(1, 1)から(1, 6)セルに
カレンダーへの登録,日付,タイトル(人),開始時間,終了時間,場所,
と記入します.スプレッドシートでも勤怠管理を登録していきます.
次にツールタブのスクリプトエディタを開きます.
以下のソースコードをコピペします.

SlackToCalendar.gs
function doPost(e) {
  //シート1はシート名に応じて変更
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シート1');

  // googleカレンダーのID
  const calendar = CalendarApp.getCalendarById("[googleカレンダーの設定からIDをコピペ]")

  //Outgoing Webhookのトークン
  var token = '[Outgoing webhookのトークンをコピペ]'

  //送られてきたトークンが正しければ勤怠を記録
  //parameterは必要に応じて変更
  if (token == e.parameter.token){
    var id_to_name = {"U0*********":"金子","U0*********":"本多","U0*********":"阿部"}
    var datetime     = new Date();
    var date         = (datetime.getFullYear()  + ('0' + (datetime.getMonth() + 1)).slice(-2) + ('0' + datetime.getDate()).slice(-2))
    var user_name    = id_to_name[e.parameter.user_id];
    var trigger_word = e.parameter.trigger_word;
    var text         = e.parameter.text;
  // 入室に反応する言葉(Outgoing webhookで設定した物)をリストに追加
    var list = ["in","おはよう","出勤","こんにちは"]
    var message

    if (list.includes(trigger_word)) {
      // 出勤時のプログラム

      // 場所を追加
      text = text.replace("_","_")
      var mark = "_"
      var index = text.indexOf(mark)
      text = text.slice(index + 1)


      //追加する配列を作成
      array = ["",date,user_name,datetime,"",text];
      //シートの最下行に配列を記述
      sheet.appendRow(array);

      message = Utilities.formatString("%sさんが%sに%sしました",user_name,text,trigger_word)

    } else {
      // 退勤時のプログラム
      message = Utilities.formatString("%sさん%s",user_name,trigger_word)

      for(var i=2; i<= sheet.getLastRow(); i++) {
        // カレンダーに登録済みならパス
        if (sheet.getRange(i, 1).getValue().toString() == "") {
          continue;
        }
        // 日付とuser_nameが一致しているものを探す
        var in_date = sheet.getRange(i,2).getValue()
        var in_datetime = sheet.getRange(i,4).getValue()
        var in_name = sheet.getRange(i,3).getValue().toString()
        var in_location = sheet.getRange(i,6).getValue().toString()

        // 一致していたらoutの時間を登録,カレンダーにも登録
        if (( in_date == date ) && ( in_name == user_name )) {

          sheet.getRange(i,5).setValue(Utilities.formatDate(datetime,"JST", "yyyy/MM/dd HH:mm:ss"))
          var startTime = new Date(in_datetime)
          var endTime = datetime
          var options = {
            location: in_location
          }
          calendar.createEvent(user_name, startTime, endTime, options)
          sheet.getRange(i,1).setValue("")
          break

        }
      }
    }
    var payload = {
      "text" : "\n" + message, // メッセージの本文
      "channel" : "#勤怠管理", // チャネルの指定
      "username" : "勤怠管理クマ", // Botの名前
      }
    postSlack(payload);
  }
  return
}

// Slackでレスポンスする関数
function postSlack(payload)
{
  var options = {
    "method" : "POST",
    "payload" : JSON.stringify(payload)
  }

  var url = "[Incoming webhookのURL]"; // Webhook URL
  var response = UrlFetchApp.fetch(url, options);
  var content = response.getContentText("UTF-8");

}

コピペしたらこちらを参考にアプリケーションのURLを取得し,Outgoingq webhookのURL欄に入力します.

急ぎで作ったので仕様として次のような欠陥がありますので,実装する際は注意してください.

  • 入室記録を忘れると退室記録をしても登録されない
  • 0時をまたぐ入退室記録に対応していない
  • Googleスプレッドシートの日付,開始時間,終了時間の見た目が頭悪そう

場所を指定するのに普通は@を使いますが,slackは"@"を入力するとメンションが入力されてしまい煩わしいので"_"を採用しました.
messageは所属するチームの雰囲気に合わせて調整してください.
user_idは,Slackのプロフィール欄からコピペできます.大人数のチームだと大変ですが,Outgoing webhookの仕様で名前を取得できないため,ひと手間かかります.

おわりに

最後まで読んでいただきありがとうございました.
質問やコメントお待ちしております.

以下のページを参考にしました.
Slackで簡単に勤怠管理!【GAS】

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

コーディング規約チェッカーを運用してみた使用感 (C#(Unity) JavaScript(CocosCreator), PHP(FuelPHP))

はじめに

コーディング規約を自動的にチェックしてくれるコーディング規約チェッカーは、
複数人で開発する際の可読性向上や各人の書き方の癖を平準化できる便利なものです。

コードレビュー等で生産性のない指摘をし合わないよう、
チェックはシステムにやってもらうというのはとても合理的。

一方で、規約に縛られた書き方を窮屈に感じたりすることもあると思います。
そこで、各開発環境における規約チェッカーを使ってみた所感を書き残します。

C# (Visual Studio)

規約チェッカーであるStyleCopは、
・プロジェクトにNuGetパッケージとしてインストール
・Visual Studioの拡張機能としてインストール

という2種類の方法があります。
前者が適用できるならそちらのほうがベターなようです。

・プロジェクトにNuGetパッケージとしてインストール

NuGetでインストールするとプロジェクト内に管理される形となり、
自動でチェックが走るため扱いやすいです。

また、NuGetではStyleCopの後継であるStyleCop.Analyzersが使用できるため、
その点でもオススメできる方法です。

規約のカスタマイズはソリューションにファイルを追加することで可能です。
https://anderson02.com/cs/cs-rules/cs-rules19/

・Visual Studioの拡張機能としてインストール

拡張機能としてインストールした場合はVisualStuioの右クリックメニューから、
「Run StyleCop」を選択することでファイルのチェックを走らせることができます。

規約のルール設定は右クリックメニューの「StyleCop Settings」からGUIで行うことができ、
編集結果はXMLファイル「Settings.StyleCop」として保存されるため、
このファイルをGitで共有するなどすれば、
チームでカスタマイズされたルールを共有できます。

Unityでの利用

Unityのプロジェクトで利用する場合、NuGetによる導入はやや煩雑です。
https://t-tutiya.hatenablog.com/entry/2019/11/07/200330

拡張機能のほうはテキストファイルを検査するだけなので、
Unityプロジェクトであっても、通常のC#プロジェクトと同様の使い方ができます。

Unityの場合は、とりあえず拡張機能のほうを使うのが現状は無難かと思います。

JavaScript (Visual Studio Code)

C#と違ってフレームワーク等により様々な書き方の流儀が存在するJavaScriptにおいては、
チェッカーもいくつかあり、それを適用するIDEも選択肢が多いため、
今回はVisual Studio Code + ESLintという組み合わせを選択しました。

npmでESLintパッケージをプロジェクトにインストールし、
Visual Studio Codeの拡張機能としてのESLintをインストールする、という流れです。
https://qiita.com/yohei_nakamura/items/4cf4876b3e36a46f3750

設定は「.eslintrc.json」というファイルに記述します。
npmのパッケージ設定とルールファイルをGitで共有することでチーム内でルールが共通化できる点については、StyleCopとほぼ同じです。

チェッカー本体とIDEの連携機能を別々にインストールする必要がある、という点がStyleCopとは異なります。
上手く動作しないときの原因切り分けが少々厄介です。

Cocos Creatorでの利用

Cocos Creatorで利用する場合であっても、C#のようにプログラム側にプロジェクトの概念がないため(単なるJavaScriptファイル群)、
Webサービス等でJavaScriptを使用する場合と同じ使い方ができます。

ただし、Cocos Creatorの形式(クラス定義型とか)とルールがマッチしない部分もあるので、
必要に応じてルールをカスタマイズする等の対応は必要になります。

PHP (Visual Studio Code)

PHPの場合もJavaScriptのように、複数のポリシーとIDEの組み合わせが想定されます。
今回はPHPの標準っぽさがあるPSR-2を使ってみました。

導入等は他記事を参照してください。
https://mseeeen.msen.jp/php-codesniffer-with-vscode/

PHPの場合、使用するフレームワークそのものが規約のような縛りがあったりするので、
ルールを調整して運用する必要がありそうです。

FuelPHPでの利用

FuelPHPでも、フレームワークでクラスの書き方(スネークケースで書くなど)等が決まっているので、パスカルケースを使うようなルールになっているとそれだけで正しくかけないといった問題が発生します。

FuelPHP準拠のルールを使用するか、PSR-2のようなルールをベースにフレームワークに合わせてカスタマイズしていくか、

いずれにしろ運用に工夫が必要になります。

おわりに 複数言語の比較

複数言語を比較してみると、言語やチェッカーの特性などが見えてきます。

C#は.NET Frameworkの統一感があるため、規約チェッカーを導入する場合もあれこれ悩む必要が
少なかったです。

Unityで公式に使えるようになると、もっといいですね。

JavaScriptとPHPは言語仕様が緩いことに加え、フレームワーク毎にまったく違う書き方があり、またエディタのデファクトも決定打がないため、様々な選択肢を試行錯誤する必要がありました。

カッコで改行するかどうか、など、細かいところも差異がでてくるため、しっかりと統一したルールを策定し、共有し、運用することが重要だと思います。

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

プログラミング言語毎のコーディング規約チェッカー使用感 (C#(Unity) JavaScript(CocosCreator), PHP(FuelPHP))

はじめに

コーディング規約を自動的にチェックしてくれるコーディング規約チェッカーは、
複数人で開発する際の可読性向上や各人の書き方の癖を平準化できる便利なものです。

コードレビュー等で生産性のない指摘をし合わないよう、
チェックはシステムにやってもらうというのはとても合理的。

一方で、規約に縛られた書き方を窮屈に感じたりすることもあると思います。
そこで、各開発環境における規約チェッカーを使ってみた所感を書き残します。

C# (Visual Studio)

規約チェッカーであるStyleCopは、
・プロジェクトにNuGetパッケージとしてインストール
・Visual Studioの拡張機能としてインストール

という2種類の方法があります。
前者が適用できるならそちらのほうがベターなようです。

・プロジェクトにNuGetパッケージとしてインストール

NuGetでインストールするとプロジェクト内に管理される形となり、
自動でチェックが走るため扱いやすいです。

また、NuGetではStyleCopの後継であるStyleCop.Analyzersが使用できるため、
その点でもオススメできる方法です。

規約のカスタマイズはソリューションにファイルを追加することで可能です。
https://anderson02.com/cs/cs-rules/cs-rules19/

・Visual Studioの拡張機能としてインストール

拡張機能としてインストールした場合はVisualStuioの右クリックメニューから、
「Run StyleCop」を選択することでファイルのチェックを走らせることができます。

規約のルール設定は右クリックメニューの「StyleCop Settings」からGUIで行うことができ、
編集結果はXMLファイル「Settings.StyleCop」として保存されるため、
このファイルをGitで共有するなどすれば、
チームでカスタマイズされたルールを共有できます。

Unityでの利用

Unityのプロジェクトで利用する場合、NuGetによる導入はやや煩雑です。
https://t-tutiya.hatenablog.com/entry/2019/11/07/200330

拡張機能のほうはテキストファイルを検査するだけなので、
Unityプロジェクトであっても、通常のC#プロジェクトと同様の使い方ができます。

Unityの場合は、とりあえず拡張機能のほうを使うのが現状は無難かと思います。

JavaScript (Visual Studio Code)

C#と違ってフレームワーク等により様々な書き方の流儀が存在するJavaScriptにおいては、
チェッカーもいくつかあり、それを適用するIDEも選択肢が多いため、
今回はVisual Studio Code + ESLintという組み合わせを選択しました。

npmでESLintパッケージをプロジェクトにインストールし、
Visual Studio Codeの拡張機能としてのESLintをインストールする、という流れです。
https://qiita.com/yohei_nakamura/items/4cf4876b3e36a46f3750

設定は「.eslintrc.json」というファイルに記述します。
npmのパッケージ設定とルールファイルをGitで共有することでチーム内でルールが共通化できる点については、StyleCopとほぼ同じです。

チェッカー本体とIDEの連携機能を別々にインストールする必要がある、という点がStyleCopとは異なります。
上手く動作しないときの原因切り分けが少々厄介です。

Cocos Creatorでの利用

Cocos Creatorで利用する場合であっても、C#のようにプログラム側にプロジェクトの概念がないため(単なるJavaScriptファイル群)、
Webサービス等でJavaScriptを使用する場合と同じ使い方ができます。

ただし、Cocos Creatorの形式(クラス定義型とか)とルールがマッチしない部分もあるので、
必要に応じてルールをカスタマイズする等の対応は必要になります。

PHP (Visual Studio Code)

PHPの場合もJavaScriptのように、複数のポリシーとIDEの組み合わせが想定されます。
今回はPHPの標準っぽさがあるPSR-2を使ってみました。

導入等は他記事を参照してください。
https://mseeeen.msen.jp/php-codesniffer-with-vscode/

PHPの場合、使用するフレームワークそのものが規約のような縛りがあったりするので、
ルールを調整して運用する必要がありそうです。

FuelPHPでの利用

FuelPHPでも、フレームワークでクラスの書き方(スネークケースで書くなど)等が決まっているので、パスカルケースを使うようなルールになっているとそれだけで正しくかけないといった問題が発生します。

FuelPHP準拠のルールを使用するか、PSR-2のようなルールをベースにフレームワークに合わせてカスタマイズしていくか、

いずれにしろ運用に工夫が必要になります。

おわりに 複数言語の比較

複数言語を比較してみると、言語やチェッカーの特性などが見えてきます。

C#は.NET Frameworkの統一感があるため、規約チェッカーを導入する場合もあれこれ悩む必要が
少なかったです。

Unityで公式に使えるようになると、もっといいですね。

JavaScriptとPHPは言語仕様が緩いことに加え、フレームワーク毎にまったく違う書き方があり、またエディタのデファクトも決定打がないため、様々な選択肢を試行錯誤する必要がありました。

カッコで改行するかどうか、など、細かいところも差異がでてくるため、しっかりと統一したルールを策定し、共有し、運用することが重要だと思います。

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

Web初心者がobnizとWeb VRで遊んでみようと試みた話...。

概要

 javascriptで制御できるマイコンボード「obniz」。

javascriptで書けるなら、A-Frameと連携して何かできそう!と思い試みた話です。

試してみたこと

「A-Frameで表現したボタンを押すと、obnizのOLEDにメッセージが表示される」ということに試みました。
結果としては、obnizにつながらないような状況です。

 コードを見てコメントいただけると嬉しいです。

できたこと

 A-Frameでボタンのようなものを作れました。(押したら戻ってこない)

できなかったこと

「ボタンを押したらobnizにメッセージが表示される」は実現できませんでした。

エラーは出ていないようなので、obnizの処理が実行されていないのではないかと考えています。
次のように、ボタンを押したらobnizを接続し、コンソールへログ出力させているのですが、ログがでないです。

obniz.onconnect = async function () {
       console.log("Connect obniz");
       obniz.display.clear();
       obniz.display.print("3D A-Frame");
       obniz.display.print(" ↑↓");
       obniz.display.print("obniz");
}

ソースコード

index.html
<!DOCTYPE html>
<html>

<head>
    <title>Hello, WebVR!  A-Frame</title>
    <meta name="description" content="Hello, WebVR! • A-Frame">
    <script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>

</head>

<body>
    <a-scene>
        <!--背景画像-->
        <a-sky src="https://aframe.io/aframe/examples/boilerplate/panorama/puydesancy.jpg" rotation="0 -130 0"></a-sky>
        <!--仮想ボタン-->
        <a-box color="#EEEEEE" position="0 2 -3" height="2" width="2"></a-box>
        <a-box color="#C0C0C0" position="0 2 -2.5" height="0.5" width="0.5" onclick="handlerClick(event)"
            onmouseenter="handlerMouseEnter(event)" onmouseleave="handlerMouseLeave(event)">
            <a-animation attribute="scale" to="-3.5 -5 2" direction="alternate" dur="2000" repeat="indefinite"
                easing="linear">
            </a-animation>
        </a-box>
        <!--足もとのブロック-->
        <a-box color="#99FFFF" position="0 0 0" height="2" width="2"></a-box>
        <!--自分の視点-->
        <a-camera>
            <a-cursor></a-cursor>
        </a-camera>
    </a-scene>

    <script src="https://unpkg.com/obniz@2.3.0/obniz.js" crossorigin="anonymous"></script>
    <script>
        let obniz = new Obniz("obniz-id");  // obniz idを入力する

        async function handlerClick(event) {
            console.log('handlerClick');
            console.log(event);
            event.target.object3D.position.z -= 0.5;

            obniz.onconnect = async function () {
                console.log("Connect obniz");
                obniz.display.clear();
                obniz.display.print("3D A-Frame");
                obniz.display.print(" ↑↓");
                obniz.display.print("obniz");
            }
        }

        function handlerMouseEnter(event) {
            console.log('handlerMouseEnter');
            console.log(event);
        }

        function handlerMouseLeave(event) {
            console.log('handlerMouseLeave');
            console.log(event);
        }
    </script>

</body>

</html>

参考

A-Frame
MDN web docs A-Frameを使った基本的なデモの作成
カラーコード一覧表

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

初学者が躓くreturnの概念

プログラミング初心者が躓くreturn(返り値)

なんとか理解することができたので
忘れないようにアウトプットも兼ねて投稿しておく

returnの意味は大きく2つ

・処理を終わらせる

・値を返す

値を返すという概念はなかなか理解しづらいですが
私は関数で処理して得た値を使いまわしたい時はretrunで返り値を持たせる!と覚えている

試しに2パターン関数を書いてみる

returnなし
$num = 10;
function a($i) {
     $i * $i;
}

echo a($num);

//結果は出力なし

returnあり
$num = 10;
function a($i) {
     return $i * $i;
}

echo a($num);

//100

結果returnをつけてやらないと、全く使いまわすことのできない関数となってしまう

$num = 10;
function a($i) {
     echo $i * $i;
}

a($num);

//100

上記の場合は関数内で処理が完結しているため、そのまま出力しても問題ないが

処理結果を別の処理では使いまわせないため使い方は限定的だ

なぜreturnで躓くか

これは私の勝手な妄想である

プログラミングを勉強する方々はまずはじめにHTML・CSSを勉強するとおもう
そこからJavaScriptやphpを学習する人がほとんどのため、どの教材でも

「HTMLに出力してみよう」

ここからプログラミングを試していることがほとんどである
HTMLに出力するこということは、出力して終わりである
関数の値を他で使いまわさないのだ

実際にプログラミングを使って開発をする場合、ほとんどの関数には返り値をもたし他で使いまわせるようにするらしい

まとめ

WEB制作からプログラミングを勉強した方は躓きやすい
プログラミング言語から勉強した方は、理解しやすい

私は最初WEB制作を勉強したのでガッツリ躓いた:joy:

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

[JavaScript] window.を省略してはいけない場面に注意しよう

windowオブジェクト

ブラウザでJavaScriptを実行する場合、windowがグローバルオブジェクトとして存在します。

みなさんご存知のとおり、windowオブジェクトのプロパティにアクセスする際の window. は省略できます。

// OK
window.alert('Hello world')
// 省略してもOK
alert('Hello world')

しかし、省略してはいけない場面があります。

省略してはいけない場面

省略してはいけない場面、それはプロパティの存在チェック時です。

一部ブラウザにしか実装されていない実験的な機能を使う場面や、古いブラウザに対応させるために、windowオブジェクトにその実装が存在するかチェックするコードを書くことがあると思います。

このとき、window.を省略してしまうと思わぬエラーに遭遇します。

省略時に発生するエラー

windowオブジェクトにチェック対象のプロパティが存在しない場合、ReferenceErrorとなってしまいます。

これは、宣言していない変数への参照とみなされてしまうためです。

// ====== NG! ======
// window.xxxxが存在するかチェックする意図だけど…
// 宣言していない変数を参照してしまい、ReferenceErrorとなる
if (xxxx) {
   xxxx()
}

// ====== OK! ======
// windowオブジェクトのプロパティの存在チェック時には明示的にwindow.を付与する必要がある
// windowオブジェクトにxxxxプロパティが存在しなくてもundefinedが返り、エラーにはならない
if (window.xxxx) {
   xxxx()
}

これの厄介なところは、対象の機能の実装がないブラウザで確認しないと、この問題に気づけない点です。

やはり、クロスブラウザのテストが大事ですね…

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

【A-Frame】WebVRを自分のサイトに組み込んでみる

A-Frameを使用してWebVRを自分のサイトに組み込んでみました。短いコードでWebVRが作成できるのがすごいです。

実際の画面がこちらです。
gazo1.png

公開サイト

https://3dblock.jp

以下の「ブロックを作成」メニューから飛べます。
here.png

今回追加した機能

  • 視点を合わせてクリックをするとブロックを配置
  • 置いたブロックをクリックするとランダムで色変更
  • 十字キーで左右上下の移動
  • テキスト表示(英語)

今回追加しなかった機能

  • 今後は置いたブロックをクリックしたら色を指定できるようにしたい
  • テキスト表示(日本語) ここで出来そうです
  • 空間(sky)と地面(ground)は最初はテクスチャを張っていたが、雰囲気に合わず一旦削除

コード

<!DOCTYPE html>
<html>

<head>
    <script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
    <script src="https://unpkg.com/aframe-teleport-controls@0.2.x/dist/aframe-teleport-controls.min.js"></script>
    <script src="https://unpkg.com/aframe-controller-cursor-component@0.2.x/dist/aframe-controller-cursor-component.min.js"></script>
    <script src="https://rawgit.com/ngokevin/kframe/csstricks/scenes/aincraft/components/random-color.js"></script>
    <script src="https://rawgit.com/ngokevin/kframe/csstricks/scenes/aincraft/components/snap.js"></script>
    <script src="https://rawgit.com/ngokevin/kframe/csstricks/scenes/aincraft/components/intersection-spawn.js"></script>

    <body>
        <a-scene>
            <a-assets>
                <a-mixin id="voxel" geometry="primitive: box; height: 0.3; width: 0.3; depth: 0.3" material="shader: standard" random-color snap="offset: 0.25 0.25 0.25; snap: 0.3 0.3 0.3 " img id="boxTexture" src="https://i.imgur.com/mYmmbrp.jpg"></a-mixin>
            </a-assets>

            <a-cylinder id="ground" collar="white" radius="30" height="0.1"></a-cylinder>
            <a-sky color="#CCFFFF" radius="900"></a-sky>

            <!-- Hands. -->
            <a-entity id="teleHand" hand-controls="left" teleport-controls="type: parabolic; collisionEntities: [mixin='voxel'], #ground"></a-entity>
            <a-entity id="blockHand" hand-controls="right" controller-cursor intersection-spawn="event: click; mixin: voxel"></a-entity>

            <!-- Camera. -->
            <a-camera>
                <a-cursor intersection-spawn="event: click; mixin: voxel"></a-cursor>
            </a-camera>

            <a-text position="-3.5 1.25 -3" value="welcome to my website!&#13;&#10;(Move or Click!)" color="#222222" scale="3"></a-text>
        </a-scene>

    </body>

</html>

本当に短いです。。笑

解説

以下の部分でクリックしたらブロックが表示されるようにしています。
ブロックの大きさは、height・width・depthで定義しています。

<a-assets>
<a-mixin id="voxel" geometry="primitive: box; height: 0.3; width: 0.3; depth: 0.3" material="shader: standard" random-color snap="offset: 0.25 0.25 0.25; snap: 0.3 0.3 0.3 ></a-mixin>
</a-assets>

<a-scene>
<a-text position="-3 2.25 -3" value="welcome to my website!&#13;&#10;(Move or Click!)" color="#222222" scale="3"></a-text>
</a-scene>

positionでx軸、y軸、z軸の順で座標を定義しており、半角スペースで区切ります。忘れた時には以下のイラストが役立ちます。

zahyo.png

参考にしたサイト

公式ページ
- https://aframe.io/docs/1.0.0

今後の改善点

  • この状態でデザインがしやすいとは決して言えないため、改良していきたい
  • objファイルの取込も活用して、もう少し世界観を表現したい
  • 他の技術ももう少し組み合わせたかった
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む