きまぐれweb/app開発日記

個人でWEBやアプリの開発してます。使用している言語・フレームワークはGo,Flutter,Rails,Vue,ionicです。これらの開発で発見したこと、思ったことを気まぐれに書いていきます。

ionicでFirebase関連のビルドエラーが多発!

ionicでは通知の処理などでFirebaseを使うことが多いのですが、4月頃からFirebaseがアップデートを繰り返しており、それによってビルドエラーが多発しています。

僕もアプリをアップデートしようとした所、Firebaseのエラーでどっぷりハマってしまい時間を浪費してしまいました。

同じような人がいると思いますので、自分なりの解決策を紹介します。

プラグインはcordova-plugin-firebase-libを使用

今までは"cordova-plugin-firebase"や"cordova-plugin-fcm"といったプラグインが使われてきましたが、これらは対応する更新がされていません。

そこで有志の方が作ったのがこちらのプラグインです。

cordova-plugin-firebase-lib github.com

このプラグインは迅速にアップデートされており、Firebaseの仕様変更に対応しています。

導入方法

導入方法はGitHubのページに書かれている通りなのですが、いくつか注意事項もあります。

まず、既存のFirebaseのプラグインを削除します。下記は"cordova-plugin-firebase"を使っていた場合。

ionic cordova platform remove android
ionic cordova plugin remove cordova-plugin-firebase
ionic cordova plugin add cordova-plugin-firebase-lib

仕様変更でAndroidXというプラグインが必要になったそうなので、そちらも入れます。

cordova plugin add cordova-plugin-androidx
cordova plugin add cordova-plugin-androidx-adapter

Githubに書かれている通り、cordova-android,cordova-iosのサポート対象のバージョンが決まっています。普通にplatform addすると僕の場合androidのバージョンが下位になってしまいます。そこでバージョンを8.0.0で指定するため、下記のように追加します。

ionic cordova platform add android@8.0.0

事後処理

これでビルドしてみると、gradleのバージョンが合わないというエラーが発生しました。

 > Minimum supported Gradle version is 4.10.1. Current version is 4.4. If using the gradle wrapper, try editing the distributionUrl in /Users/maruse/dev/native/foot4/gradle/wrapper/gradle-wrapper.properties to gradle-4.10.1-all.zip

そこで、~/.bashrcに記載していたCORDOVA_ANDROID_GRADLE_DISTRIBUTION_URLのバージョンをandroid/app/build.gradleのgradleVersion にある4.10.3に変更します。

ちなみにCORDOVA_ANDROID_GRADLE_DISTRIBUTION_URLを宣言していない場合は、android/gradle/wrapper/gradle-wrapper.propertiesのdistributionUrlを書き換えると解決するそうです。

gradleをアップデートしたためプラグインもアップデートしないといけませんので、Android/build.gradleとandroid/app/build.gradleを下記のように修正します。ちなみにgradleプラグインの対応バージョンはこちらに書かれています。

    dependencies {
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files

        classpath 'com.android.tools.build:gradle:3.3.2'

さらに"The minSdk version should not be declared in the android manifest file"なるエラーがでましたので、android/app/src/main/AndroidManifest.xmlandroid/CordovaLib/AndroidManifest.xmlにある、minSdKを宣言している行を削除しました。

感想

これだけアップデートを繰り返されてしまうと、プラグイン側の対応が間に合いませんね。Googleが作ったアプリフレームワークのFlutterはFirebaseへの対応が良いという噂を聞いていますので、そちらの利用も検討してみようかな。(それがGoogleの狙いか?)

ionic4でのngx-translateを使った多言語対応

ionic4で多言語に対応するにはangularのngx-translateを使用するのが一般的です。ネット上の情報はionic3のものがほとんどで、ionic4で導入しようとするとなかなか上手く行かないと思います。

そこでionic4でのngx-translateの使い方を紹介します。

プラグインのインストール

これはionic3までと変わりません。下記のようにngx-translateとngx-translate/http-loaderをインストールしてください。

npm install @ngx-translate/core @ngx-translate/http-loader --save

app.module.ts

app.module.tsでは、TranslateModuleをインポートするだけになりました。

import { TranslateModule } from '@ngx-translate/core';


imports: [
  TranslateModule.forRoot()
],

ページごとのmodule.ts

次にapp/ページ名/ページ名.module.tsにあるページごとのmodule.tsを更新します。ここではHomeというページを翻訳します。

ここでは、createTranslateLoaderというfunctionを作って、翻訳ファイルの置き場所を指示します。

import { HttpClient } from '@angular/common/http';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';

export function createTranslateLoader(http: HttpClient) {
  return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}

@NgModule({
  imports: [
      TranslateModule.forChild({
        loader: {
           provide: TranslateLoader,
           useFactory: (createTranslateLoader),
           deps: [HttpClient]
         }
    }),
  ],
  declarations: [HomePage]
})

page.ts

ページごとにTranslate Serviceを読み込み、言語を指定します。

import { TranslateService } from '@ngx-translate/core';


export class HomePage implements OnInit {
    constructor(
    public translate: TranslateService,
  ){
    this.translate.setDefaultLang('ja');
    this.translate.use('ja');
  }
}

page.html

ページに翻訳内容を埋め込みます。

<p>{{ 'HELLO' | translate }}</p>

翻訳ファイルの設置

翻訳のためのjsonファイルはsrc/assets/i18nフォルダに設置してください。今までのバージョンと間違えやすいので注意しましょう。

src/assets/i18n/ja.json

{
  "HELLO": "こんにちは"
}

まとめ

ポイントは、ページごとにTranslateLoaderを使ってファイルを読み込むことと、ファイルの置き場所をsrc/assetsの中にすることです。そこさえ注意すればトラブルなく多言語対応ができるはずです。

ionic4のページ遷移

前回 ionic3からionic4への移行について記事を書きましたが、ionic4になって最も大きく変更されたのがページ遷移の方法です。

今回の記事では前回書ききれなかったページ遷移について書きたいと思います。

Ionic4ではルーティングmoduleを使用

ionic4ではwebページと同じような、urlを元にしたページ遷移に変わっています。その鍵になるのがsrc/appフォルダの中にあるapp-routing.module.tsです。例えばHomeというページとContactというページにパスを登録するのであれば、下記のように記入します。

const routes: Routes = [
  { path: 'home', loadChildren: './home/home.module#HomePageModule' },
  { path: 'contact', loadChildren: './contact/contact.module#ContactPageModule' },
];

これによってsrc/app/homeとsrc/app/contactにあるページへのルーティングができます。

最も単純なページ遷移

homeページからcontactページ遷移したい時はどうしたらいいかというと、下記のようにangular/routerを利用して遷移できます。

home.page.ts

import { Router } from '@angular/router';

export class HomePage implements OnInit {
  constructor(
    private router:Router,
  ){}

this.router.navigateByUrl('/contact');

「this.router.navigateByUrl('/contact')」がionic3の

this.navCtrl.push(ContactPage);

に代わるメソッドです。

さらに、home.page.htmlから下記のようなリンクを使って遷移することもできます。

<a href="/contact">contact page</a>

パラメータを送るときのページ遷移のしかた

homeページからcontactページへidというパラメーターを送付したい場合は下記のようにurlの後にパラメータを設定します。

app-routing.module.ts

const routes: Routes = [
  { path: 'home', loadChildren: './home/home.module#HomePageModule' },
  { path: 'contact/:id', loadChildren: './contact/contact.module#ContactPageModule' },
];

パラメータを送るために変えるのはpath: contact/:idのところだけです。

送信側のhomeはこんな感じ。

home.page.ts

import { Router } from '@angular/router';

export class HomePage implements OnInit {
  constructor(
    private router:Router,
  ){}
var id:string = '1';
this.router.navigateByUrl('/contact/' + id);

home.page.html

<a href="/contact/1">contact page</a>

受け取る側のcontactではActivatedRouteとParamapを使って下記のように受け取ります。

contact.page.ts

import { Router, ActivatedRoute, ParamMap } from '@angular/router';

export class ContactPage implements OnInit {
   id:string;

  constructor(
    private router:Router,
    private route: ActivatedRoute,
    ){}

  ngOnInit(){
    this.route.paramMap.subscribe((params: ParamMap) => {
      this.id = params.get('id');
      });
  }

最初は少し戸惑うんですが、一度やってみると難しくは感じません。

複数のパラメータを送るページ遷移

複数のパラメータを送るときも基本的に同じで、/でつなげて後ろにパラメータを増やしていくだけです。下記はidに加えてnameとageを送る例です。

app-routing.module.ts

const routes: Routes = [
  { path: 'home', loadChildren: './home/home.module#HomePageModule' },
  { path: 'contact/:id/:name/:age', loadChildren: './contact/contact.module#ContactPageModule' },
];

home.page.ts

import { Router } from '@angular/router';

export class HomePage implements OnInit {
  constructor(
    private router:Router,
  ){}
var id:string = '1';
var name:string = "bob";
var age:string = '20';
this.router.navigateByUrl('/contact/' + id + '/' + name + '/' + age);

home.page.html

<a href="/contact/1/bob/20">contact page</a>

contact.page.ts

import { Router, ActivatedRoute, ParamMap } from '@angular/router';

export class ContactPage implements OnInit {
   id:string;
   name:string;
   age:string;

  constructor(
    private router:Router,
    private route: ActivatedRoute,
    ){}

  ngOnInit(){
    this.route.paramMap.subscribe((params: ParamMap) => {
      this.id = params.get('id');
      this.name = params.get('name');
      this.age = params.get('age');
      });
  }

ionic4のルーティングは恐れる必要なし

ionic3やり方が変わったので最初は慣れませんし、実際少しだけ複雑になっています。しかし、webアプリであるかのようにページを遷移していける自由度はionic3までには無かったものです。

この変更により、わずかな手間と引き換えに、大きな自由度を手にした印象です!Ionic team, good job!

ionic3からionic4への移行

ionic3で製作していてほぼ完成間近のアプリがある中、ionicチームから一通のメールが。「ionic4が正式にリリースされたよ。起動時間やパフォーマンスが向上してるよ。テーマやCSSも強化してるよ。」との事。パフォーマンスが上がってると聞いたら手を出さずにはいられません。早速アプリをionic4に移行させました。

移行方法

海外のサイトを漁った結果、移行する方法については、既存のアプリを書き換えるよりも、新たにionic4のアプリを作成するのが王道らしい。なので新規でアプリを作成します。

プロジェクト作成

ionic start myApp --type=angular

「--type=angular」のところがポイントで、「--type=ionic-angular」にするとionic3のアプリが作成されます。なおionicはangularだけでなく、vueをエンジンにするプロジェクトも進めていて、既にα版がリリースされているとのこと。こちらも正式版が楽しみです。

プロジェクトを作成して、ionic serveを試してみるとこんなエラーが。。。

[ERROR] Cannot perform Ionic serve/watch for project type: angular.

どうやら最新版でないionicもインストールされていて、パスの優先順位の問題でそちらが優先されて動いていたためこのようなエラーが出たようです。パスを修正して「ionic --version」で4.9.0と出たのを確認して再度ionic serveすると無事立ち上がりました。

pagesフォルダが無い!

ionic3の時、開発していて一番使うのがpagesフォルダでしたが、ionic4では見当たりません。どこに移動したかと言うと、appフォルダの中です。appフォルダの中にapp.component.tsやapp.module.tsと並んでページのフォルダが作られるように変わりました。

読み込みエラーの山

無事立ち上がったのを確認した私は猛然とionic3で作ったコードをionic4のプロジェクトにコピペ。そしていざionic serveしてみると山のようなエラーが。。。少々うんざりしながらもログに目をとうしてみると、moduleが見つからないというエラーを大量に吐いています。

ググって調べてみると、プラグインをimportするときの表記方法が微妙に変わっているとのこと。「勝手に変えんな、クソが!」と叫びつつ、修正していきます。

エラーの原因は主に2つでionic-nativeとionic-angularでした。

ionic nativeは3の場合

import { *** } from '@ionic-native/***';

だったものが、4では

import { *** } from '@ionic-native/***/ngx';

に変わっています。

ionic-angularの場合は3で

import { *** } from 'ionic-angular';

だったものが、4では

import { *** } from '@ionic/angular';

に変わっています。

プラグイン自体の名前が変わっているものがあるようで、「Content」は「IonContent」に変わっていました。さらに、ion-optionがion-select-optionに変わっているなど、タグの名前が変わっているものもあります。この種の変更はエラーが出た箇所を公式サイトで調べるしかありませんね。なんでプラグインの名前なんて変えるんでしょう。混乱するだけなのに。

ionViewDidLoadイベントが廃止に

ionViewDidLoadイベントが4から廃止されています。4の場合は、ngOnInit(){}の中に書くことになるようです。ionViewWillEnterはそのまま使えます。

最大の難関はルーティング!

ionic4で最も大きな変更が行われたのがルーティングです。今まではルーティングなどあまり意識せず、NavControllerを使ってpush、popしてページに飛んでいましたが、ionic4からangularのルーティングを利用するように一新されました。NavControllerを使った今までのページ遷移も一応残っているということになっていますが、私が試した限りはうまく動きませんでした。長くなるので、これについては次回書きたいと思います。

ionic3でホーム画面に表示されるアプリ名を日本語にする方法

ionicのアプリ名は何もしないとconfig.xmlのnameタグに設定されるプロジェクト名になります。アプリ名を変えたい時はこのconfig.xmlを書き換えればいいように思えます。

しかし、実際にやってみるとプラグインで不具合が出たり、ビルドが上手く行かなかったりといった不具合に見舞われることになりますので、この方法はオススメできません。。

そこで、下記のようにOSごとにビルド後にアプリ名を書き換える方法を使いました。なお、ビルドし直すとアプリ名が上書きされてしまいますので、毎回ビルドするたびに書き換えないといけませんので注意してください。

Android

Androidの場合は、"/platforms/android/app/src/main/res/values/strings.xml"に設定ファイルがあります。

<string name="app_name”>MyApp</string>

となっているところを任意の名称に書き換えてください。

iOS

iOSの場合は、"/platforms/ios/regist.codeproj"をダブルクリックしてxcodeで開いてください。

ナビバーにファイルツリーが表示されますので、そこで"/Resources/config/MyApp-info.plist"で、"Bundle display name"を任意のアプリ名に書き換えてください。


再度になりますが、ビルドし直すたびに上書きされますので、そのたびに書き換えないといけません。そこは注意してください。

Railsの jsonテンプレートエンジンjbuilder

RailsAPI作るならjbuilder

スマホアプリ用にjsonでデータを吐くAPIを自作するにあたって、jbuilderを使ったら便利でしたので紹介します。環境はRails 5.0.6です。

jbuilderの基本

htmlと同じようにrouteを設定し、htmlと同じようにアクションメソッドを書いてください。gemがインストールされてなければGemfileに"gem 'jbuilder' "と追記して、bundle installしてください。viewフォルダに〇〇.json.jbuilderファイルを配置します。以下、viewのファイルの書き方です。

json.jbuilderファイルは何も書かないと"{}"と表示され、Hashが置かれていることがわかります。ファイルに記述した内容がこのハッシュ内に追加されていきます。


Hash(連想配列)を作る方法

まずは一番簡単な例

 json.set! "user", "name"
json.user "name"

どちらでも{"user":"name"}というHashのキーと値を作り出します。


次にHashを入れ子にする方法

json.set! "group" do
     json.set! "user", "name"
end

または

json.group do
     json.set! "user", "name"
end

{"group":{"user":"name"}}という入れ子のHashができます。

Array(配列)の中にHashを作る方法

@usersというactiverecordオブジェクトに入ったidとnameを取り出すとします。

json.users @users, :id, :name

これによって下記のようなjsonが出来上がります。

{users:[
{"id":1, "name":"john"},
{"id":2, "name":"paul"}
]}


これには再利用可能でDRYな書き方も別に用意されています。

まずjson.jbuilderファイルで下記の通りテンプレートを呼び出します。

json.group do
    json.array! @users, partial:''userlist'', as: :users
end

次にテンプレートの"_userlist.json.jbuilder"を同じフォルダ内に作ります。

json.extract! users, :id, :name

これで上の記法と同じ結果が得られ、テンプレートの使い回しもできます。yay!

キャッシュの設定方法

htmlと同様にキャッシュを設定してサーバーの負荷を減らすことができます。

json.cache! ['chache1', @ursers], expires_in: 5.minutes do
     json.users @users, :id, :name
end

こんな感じでキャシュしたい部分をjson.cache!で囲んでデータと名前を渡すだけです。wow!

arrayの中にHashを作り、その中でさらにarray・hashを入れ子にする。

例えばこういうのを作りたかったのですが、なかなか苦労しました。

{"groups":[
    {"group_name": "American",
  " users":[
        {"name": "john"},
        {"name": "paul"}
    ]},
    {"group_name": "Japanese",
    "users":[
    {・・・・・・・}]}
]}

パッと見てもよくわかりませんが、jsonjson入れ子にしています。上位のjson(groups)はactive-recodのオブジェクトではなく、入れ子になった側(users)だけがactive-recordオブジェクトです。

2時間ほど詰まったのですが、下記のようにコントローラーでhashを作って渡せばいいようです。

controller.rb

@american = User.where("group = ?", "american")
@japanese = User.where("group" = ?", "japanese")
@groups = {
            "american" => @american,
           "japanese => @japanese } 


json.jbuilder

json.array! @groups do |key,value|
    json.set! "group", key
    json.users do
         json.users value, :name
    end
end

Hashを作ってそこにvalueactive-recordのオブジェクを渡すことで、なんとか作ることができました。haha!

ただ、複雑になった場合はjbuilderを使わず、controller側でhashを作って"render :json => hash"をした方が楽なようです。

github.com

Ruby on Railsでサイトマップを自作

Ruby on Railsサイトマップを自作

Railsのwebアプリをデプロイした後、SEO的につくらないといけないのがsitemapです。

ネットのサイトマップジェネレーターなんかもありますが、ページ数が制限されていたり、毎回手動で設置しないといけない。

また、sitemap-generatorなるgemもあるんですが、URLが複雑になってくると対応できない(できるのかもしれないけど、とにかくややこしい!)という問題があります。

 

そこでRailsそのものを使ってサイトマップを自作する方法を紹介したいと思います。よく考えてみるとRailsは優秀なWEB APIで、sitemapで使うxmlファイルにも対応しているわけです。WEBページを作る要領でカスタマイズできますし、ページが追加/削除されると自動的にsitemapに反映されるのでとても便利。Railsに慣れた方々にはおすすめの方法です。

 

1. Routesの設定

config/routes.rbに下記の行を追加。ドメイン/sitemapにアクセスするとsitemapコントローラーのインデックスメソッドを呼び出します。

 get '/sitemap' => 'sitemaps#index' 

2.Sitemaps Controllerを作る

まずはsitemapのみを扱うコントローラーを作成します。

$ rails g controller Sitemaps

Sitemapsコントローラーではホスト名とActive Recordのデータを取得します。Productモデルを表示する例です。

app/controllers/sitemap_controller.rbを下記のように設定します。

class SitemapsController < ApplicationController

  def index
    @domain = "#{request.protocol}#{request.host}"
    @products = Product.all
  end
end

3.app/views/sitemaps/index.xml.erbを作成

作るのはxmlファイルなので、html.erbではなく、xml.erbファイルを作ります。

@productを全て表示したいので、eachメソッドを使って全て取り出します。

他に追加したいURLがあれば、 <%= @domain %>/******という感じで書きます。

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
   <url>
      <loc><%= @domain %></loc>
   </url>
   <url>
      <loc><%= @domain %>/index</loc>
   </url>
  <% @products.each do |product| %>
    <url>
      <loc><%= "#{@domain}/products/#{product.id}" %></loc>
    </url>
   <% end %>
</urlset>

4.rails serverしてsitemapsを確認

Rails serverを起動して/sitemap.xmlを確認します。ローカル環境ではhttp://localhost/indexのようになっているはずです。

本番環境ではlocalhostの部分が、ホスト名に切り替わります。

5. google ウェブマスターツールで送信

ウェブマスターツールのクロール→サイトマップサイトマップの追加/テストに行き、http://ホスト/sitemap.xmlと入力して送信します。

sitemap.xmlは動的に作られますので、次回以降はデータが更新されると勝手にサイトマップも更新され、googleも勝手に見に来てくれます。あら便利。

問題点

僕の環境(rails 5.0.6)では、sitemap/xmlで親モデル/子モデル/孫モデルのデータをうまく取得できませんでした。

URLが /親モデル/id/子モデル/id/孫モデル/idという形だったので、URLを書くにあたって親モデルのidは必須。ここでしばらく詰まってしまいました

少しややこしいですが、下記のような状況です。

  1. 親モデル: Product
  2. 子モデル:Post
  3. 孫モデル:Comment

があった場合、CommentのURLに使うPostやProductのidが取得できないのです。

対応策としては子モデルが持っている親モデルのidを使えば、子モデル(ここではPost)までは問題解決できます。(下記@posts)

しかし、これでは孫モデル(@comments)は解決できません。

   <% @posts.each do |post| %>
      <url>
      <loc><%=@domain%>/products/<%= post.product_id %>/post/<%= post.id %></loc>
      </url>
   <% end %>

<% @comments.each do |comment| %> <url> <loc><%=@domain %>/products/<%= comment.post.product_id %>/post/<%= comment.post_id %>/comments/<%=comments.id %></loc> ←comment.post.product_id がエラー! </url> <% end %>

そこでINNER JOINを利用して@commentsでproduct_idを利用できるようにします。

        @comments = Comment.joins(:post).all.select("comments.*, posts.product_id AS product_id")
end

あとは@comments.eachの部分を以下のように書き換えるだけです。

  <% @comments.each do |comment| %>
      <url>
      <loc><%=@domain %>/products/<%= comment.product_id %>/post/<%= comment.post_id %>/comments/<%=comments.id %></loc>
      </url>
   <% end %>