Electron & React で ContextBridge を使って安全に DB 操作する

HTML や CSS などの Web 技術を用いてクロスプラットフォームなデスクトップアプリを開発できるフレームワーク ELECTRON

動的なウェブサイトを構築できるライブラリ React (ReactJS)

この2つを組み合わせて、データベース (CRUD) 操作を行えるアプリ作成を考えます。

[toc]

セキュリティを考える

一見すると、この課題には難しい点はないように思われます。

Renderer プロセスから ipcRenderer を読み込み、Main プロセスの ipcMain との間でプロセス間通信を実行すれば、Main プロセスを介して Renderer プロセスと DB との間でデータの受け渡しが可能です。

ところが、2020年現在、こうした設計はセキュリティ上の理由から推奨されていません。XSS で Node.js Modules にアクセスされて SSH キーを不正取得されたりしても困るといった動機は理解できますが、不便でもあります。

そこで Renderer プロセスから安全に Node.js Modules を利用するため、ほかのスクリプト実行前にあらかじめ preload で読み込むことを行います。

この preload において contextBridge を用いて関数を API 化し Renderer プロセスから利用可能にすることが最近のセキュリティ重視の設計です。

ここで問題となるのは contextBridge によって作成された API (オブジェクト) には型定義が存在しないため TypeScript のコンパイル時にエラーの原因となります。

$ npx tsc
Property 'api' does not exist on type 'Window & typeof globalThis'.

TypeScript は React を安全に使うために必要です。

ELECTRON が用いる HTML/CSS/JS にはデスクトップアプリに適した UI フレームワークが存在しないため React がそれを補います。つまり React もあったほうがいいものです。

それらのリソースを管理するのは Webpack です。

こうした一つ一つの要素技術に設定ファイルがありますので、組み合わせると面倒なことになります。




整理すると、やるべきことは次のようになります。

(1) Main プロセスにおいて preload を読み込む
(2) Preload において関数を API 化する
(3) API の型定義を行う
(4) Webpack の設定ファイルを書き換える
(5) データベースに接続して操作を行う

ここで断っておきますが、私はフロントエンドもスクリプト言語も嫌いです。当然ながら専門家ではありません。最新の公式ドキュメントには目を通していますが、より良い方法がほかにあるかもしれません。

実行環境

実行環境は以下のとおりです。

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.5 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic

$ node --version
v12.16.3

$ npm --version
6.14.8

$ yarn version
yarn version v1.22.5

$ cat package.json |grep dependencies -A 10
  "dependencies": {
    "eslint-loader": "^4.0.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-router-dom": "^5.2.0",
    "react-scripts": "^3.4.1",
    "sqlite3": "^5.0.0",
    "typeorm": "^0.2.26",
    "typescript-fsa": "^3.0.0",
    "typescript-fsa-reducers": "^1.2.2"
  },

$ cat package.json |grep devDependencies -A 21
  "devDependencies": {
    "@types/react": "^16.9.49",
    "@types/react-dom": "^16.9.8",
    "@types/react-router": "^5.1.8",
    "@types/sqlite3": "^3.1.6",
    "@typescript-eslint/eslint-plugin": "^4.1.0",
    "@typescript-eslint/parser": "^4.1.0",
    "electron": "^10.1.1",
    "electron-rebuild": "^2.0.3",
    "eslint": "^7.8.1",
    "eslint-config-google": "^0.14.0",
    "eslint-config-prettier": "^6.11.0",
    "eslint-plugin-prettier": "^3.1.4",
    "eslint-plugin-react": "^7.20.6",
    "html-webpack-plugin": "^4.4.1",
    "prettier": "^2.1.1",
    "react-devtools": "^4.8.2",
    "ts-loader": "^8.0.3",
    "typescript": "^4.0.2",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12"
  }

TypeORM をインストールはしていますが後述の理由から使用しません。また機能要件に対して必要性を感じないので Redux も使用しません。

Main プロセスにおいて preload を読み込む

ELECTRON の main プロセスの内容を記述したファイルを main.ts とすると、その内容は次のようになるはずです。

import { app, BrowserWindow } from 'electron';
import path from 'path';

const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      allowRunningInsecureContent: false,
      contextIsolation: true,
      enableRemoteModule: false,
      nodeIntegration: false,
      nodeIntegrationInWorker: false,
      preload: path.join(app.getAppPath(), 'preload.js'), // これを追記
    },
  });
  win.loadFile(path.join(app.getAppPath(), 'index.html'));

  win.on('closed', () => {
    win = null;
  });
};

ここでドキュメントに従い、BrowserWindow の中に preload を設定します。

ファイルパスは環境に応じて適宜変更してください。

Preload において関数を API 化する

Main プロセスから preload を読み込むよう設定したあとは、main プロセスと renderer プロセスとの間で通信を行う準備を行います。

ここで contextBridge を用いて関数を API 化することが肝要です。

import { ipcRenderer, contextBridge } from 'electron';

contextBridge.exposeInMainWorld(
  'api', {
    electronIpcInvoke: (channel: string, ...arg: any) => {
      ipcRenderer.invoke(channel, arg);
    },
  }
);

先述のように contextBridge によって作成された API (オブジェクト) には型定義が存在しないため TypeScript のコンパイル時にエラーの原因となります。

API の型定義を行う

そこで正統な対処法として、型定義ファイルを作成して tsconfig に定義ファイルを読み込ませることがよく行われます。

$ mkdir -p ./src/\@types
$ vim ./src/@types/global.d.ts

個人的に確認はしていませんが、原則的にはこれで問題なく動作すると思われます。

この代わりに、私は API に対して interface を作成し、実行時に import することを行いました。

export default interface Api{
  electronIpcInvoke:(channel: string, ...arg: any) => Promise ;
}
declare global {
  interface Window {
    api: Api;
  }
}

これで main プロセスと renderer プロセスとの間でデータの受け渡しを行えるようになります。

Webpack の設定ファイルを書き換える

型定義を与えてコンパイル(トランスパイル)できるようになっても、肝心の preload.js ファイル自体が実行ファイルとして読み込まれなければ意味がありません。

そのために次は webpack.config.js の設定を書き換えて preload を読み込ませます。

module: {
     entry: {
        index: './src/index.tsx',
        main: './src/main.ts',
        preload:'./src/preload.ts'
     },
     output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].js',
     },
}

具体的には entry と output の項目に preload を書き加えるだけで大丈夫です。




データベースに接続して操作を行う

ようやくデータベースを利用する準備が整いましたので、main プロセスからデータベースに接続する関数を記述します。

import { app, BrowserWindow, ipcMain, IpcMainEvent } from 'electron';
import path from 'path';
import * as sqlite3 from 'sqlite3';

const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      allowRunningInsecureContent: false,
      contextIsolation: true,
      enableRemoteModule: false,
      nodeIntegration: false,
      nodeIntegrationInWorker: false,
      preload: path.join(app.getAppPath(), 'preload.js'),
    },
  });
  win.loadFile(path.join(app.getAppPath(), 'index.html'));

  win.on('closed', () => {
    win = null;
  });
};

// DB操作を追記
ipcMain.handle('connectTest', async () =>{
  console.log('-- connection starts --');
  const db = new sqlite3.Database('database.sqlite3');
  db.serialize(function() {
    db.run("CREATE TABLE lorem (info TEXT)");
    let stmt = db.prepare("INSERT INTO lorem VALUES (?)");
    for (let i = 0; i < 10; i++) {
        stmt.run("Ipsum " + i);
    }
    stmt.finalize();
   
    db.each("SELECT rowid AS id, info FROM lorem", function(err, row) {
        console.log(row.id + ": " + row.info);
    });
  });
  db.close();
  console.log('-- It worked --');
});

これを IPC 通信を用いて renderer プロセスから呼び出してやれば良いわけです。

ここに来てついに React の出番となります。

preload にて定義した API のelectronIpcInvoke を呼び出します。

import React from 'react';
import ReactDOM from 'react-dom';
import './config/api.interface';

function btnClick(e):void {
  window.api.electronIpcInvoke('connectTest','');
}

const container = document.getElementById('contents');
ReactDOM.render(
  
, container);

これで Connect ボタンをクリックすると IPC 通信で main プロセスの connectTest を呼び出して、データベースに接続・操作を行います。

この状態でビルド、実行してボタンをクリックした際に以下のように表示されれば成功です。

$ npm start
> electron ./dist/main.js

-- connection starts --
-- It worked --
1: Ipsum 0
2: Ipsum 1
3: Ipsum 2
4: Ipsum 3
5: Ipsum 4
6: Ipsum 5
7: Ipsum 6
8: Ipsum 7
9: Ipsum 8
10: Ipsum 9

TypeORM を使えない

以下は余談です。

TypeORM (version 0.2.26) にて ormconfig.json を作成せずに createConnection に直接的に引数を渡そうとするとエラーになります。

より具体的には以下のようなことをやっています。

import {createConnection} from "typeorm";
import options from './config/config/ormconfig';

const connection = createConnection(options);
import { ConnectionOptions, DatabaseType } from "typeorm";
import { Customers } from "../db/entity/customers.schema";

const sqliteDB : DatabaseType = 'sqlite';
export const options: ConnectionOptions = {
  type: sqliteDB,
  database: 'database.sqlite',
  synchronize: true,
  logging: false,
  entities: [Customers,],
};

明らかに現状では開発中のライブラリですし、どうしても必要なものではありませんので私は使用することを諦めました。

ちなみに下記のようなエラーはコンストラクタを作成して各カラムの変数を初期化すると消えます。


TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.


エンティティを定義されているファイルに constructor を追加してやれば問題解決です。

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class Customers {
  @PrimaryGeneratedColumn()
  id?: string;

  @Column()
  name:string;

  @Column()
  zip:string;
  
  @Column()
  address:string;

  @Column()
  countrycode:number;
  
  @Column()
  phone:number;

  constructor(name:string, zip:string, address:string, countrycode:number, phone:number){
    this.name=name;
    this.zip=zip;
    this.address=address;
    this.countrycode=countrycode;
    this.phone=phone;
  }
}

また次のようなエラーは tsconfig.json の設定により表示を消すことができます。


TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning.


お好きなエディタで設定ファイルを開いて該当箇所のコメントアウトを削除してください。

$ vim tsconfig.json

/* Experimental Options */
    "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */

こうしたマイナーなエラーは簡単に対処できますが、データベースに接続できないことにはどうしようもありません。

改善されることを待つか、使用することを諦めるほかにありません。私はもう諦めてしまいました。

ラズパイの時刻を合わせて自動起動する

Raspberry Pi (RasPi) を電源に接続して常時稼働させ続けていると、時計の時刻がズレていたり、USB接続している外部デバイスを読み込めなくなっていることがあります。

そんな時は標準時刻サーバの公開 NTP(Network Time Protocol)に時刻を問合せて、定期的に RasPi の再起動を実行すれば問題が解決することがあります。

日本標準時 (JST) に関しては NICT(ニクト)こと国立研究開発法人情報通信研究機構という国立研究機関が配信を行っていますので、こちらに問合せを行えば大丈夫です。

香港時間なら香港天文台、中央ヨーロッパ時間なら PTB という物理工学研究所といった具合に標準時刻サーバは複数ありますので必要に応じて書き換えてください。

まずは実行環境から

$ cat /etc/os-release 
PRETTY_NAME="Raspbian GNU/Linux 10 (buster)"
NAME="Raspbian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"

Debian 派生の Raspbian GNU/Linux 10 では時刻同期にシステムサービス systemd-timesyncd を使用します。これを用いるためにはエディタを使って /etc/systemd/timesyncd.conf を開き、

#NTP=

と書かれている行のコメントアウトを削除して ntp.nict.jp のアドレスを書き込みます。

$ vim /etc/systemd/timesyncd.conf 
#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.
#
# Entries in this file show the compile time defaults.
# You can change settings by editing this file.
# Defaults can be restored by simply deleting this file.
#
# See timesyncd.conf(5) for details.

[Time]
NTP=ntp.nict.jp 
#FallbackNTP=0.debian.pool.ntp.org 1.debian.pool.ntp.org 2.debian.pool.ntp.org 3.debian.pool.ntp.org
#RootDistanceMaxSec=5
#PollIntervalMinSec=32
#PollIntervalMaxSec=2048

FallbackNTP は NTP が定義されている場合には無視されますので気にしなくて結構です。

設定ファイルを書き換えましたらシステムサービスの NTP 機能を有効化して、状態を表示します。

$ sudo timedatectl set-ntp true
$ sudo timedatectl status
               Local time: Sat 2020-09-05 23:24:53 CDT
           Universal time: Sun 2020-09-06 04:24:53 UTC
                 RTC time: n/a
                Time zone: America/Chicago (CDT, -0500)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

ステータス表示 NTP service: active になっていれば、NTP 機能が稼働しています。




ただ、私の環境ではタイムゾーンがおかしなことになっているので、こちらも変更しておきます。

$ sudo cp /usr/share/zoneinfo/Asia/Hong_Kong /etc/localtime

準備が整いましたら設定を反映させるためにシステムサービスを再起動して、ステータスを表示します。

$ sudo systemctl restart systemd-timesyncd.service
$ sudo systemctl status systemd-timesyncd.service
\u25cf systemd-timesyncd.service - Network Time Synchronization
   Loaded: loaded (/lib/systemd/system/systemd-timesyncd.service; enabled; vend
  Drop-In: /lib/systemd/system/systemd-timesyncd.service.d
           \u2514\u2500disable-with-time-daemon.conf
   Active: active (running) since Sun 2020-09-06 12:45:38 HKT; 11s ago
     Docs: man:systemd-timesyncd.service(8)
 Main PID: 22048 (systemd-timesyn)
   Status: "Synchronized to time server for the first time [2001:df0:232:eea0::fff4]:123 (ntp.nict.jp)."
   Memory: 856.0K
   CGroup: /system.slice/systemd-timesyncd.service
           \u2514\u250022048 /lib/systemd/systemd-timesyncd

以下のようにNICT サーバに接続できていることが表示されれば成功です。

“Synchronized to time server for the first time [2001:df0:232:eea0::fff4]:123 (ntp.nict.jp).”

もし以下の例のように NICT 以外のサーバに接続している場合には、設定内容に誤りがあるか、設定が反映されていない場合がありますので見直しを行ってください。

Status: “Synchronized to time server for the first time 194.0.5.123:123 (0.debian.pool.ntp.org).”

また、ネットワークに接続できない環境で Ras Pi をお使いの場合には、以下のコマンドで手動で時刻合わせを行うことも可能です。

$ date +%T -s "13:00:00"

時刻合わせが完了しましたら、あとは再起動しても問題ない時間に reboot するように cron を設定しておけば、毎日、決まった時間にシステムを停止して再起動を行うようにできます。

$ sudo crontab -e

# m h  dom mon dow   command
0 0 * * * /sbin/reboot

こうしておけば、より長時間安定して稼働し続けることが期待できるはずです。

GARMIN ForeAthlete 245 – 普段使いできるスマートウォッチ

GARMIN ForeAthlete 245 の導入をはじめて早くも数週間。この新しいランニングデバイス兼、スマートウォッチについての個人的な評価もようやく定まってきました。

サイクルコンピュータ (サイコン) の GARMIN Edge に対しては割と厳し目な評価を下している私ですが、ランニングウォッチにおける GARMIN 製品に対しては信頼性、防水性、機能性、そして何をおいても稼働時間の点から唯一無二の選択として高く評価しています。

ただ最近は GARMIN のウェアラブルデバイスも数が増えてきて、違いが分かりづらくなってきているのも事実です。

[toc]

どれを選べばいいの

GARMIN のウェアラブルデバイスにはフラッグシップモデルの fenix のほかに、ライフログデバイスから派生した vivo (vivosmart や vivoactive や vivo VENU) そしてランニングウォッチから進化した ForeAthlete (日本以外では Forerunner) があります。

基本的に上位モデルほど多くの機能を備えていますので、出自は違っても現在の製品ラインナップでは機能が重複しているモデルが数多くあります。

これらは見た目で選んでも構わないのですが、細かく見ていくと製品ラインによって備えている機能や設計思想が微妙に異なっているので、購入に際しては必要な機能を備えているかどうかに気をつける必要があります。

まずは SUICA 機能を備えた GARMIN Pay を使用して、運動後などにウォッチ単体で買い物をしたいかどうかによって選択肢が狭まります。




つぎに運動中にスマートフォンすら持たず、ウォッチ単体で音楽を聞きたいかどうかで選択肢を絞れます。

最後に — もしかしたら一番重要なところかもしれないですが — タッチスクリーンを搭載したスマートウォッチのほうが好きか、ボタン操作のほうが好きかによって購入するモデルを限定できます。

ランニングや登山などのスポーツ用デバイスを出自にもつ fenix や ForeAthlete は濡れた手でも使いやすいようにボタン操作を採用しており、ランやバイクなどのアクティビティ(運動)ログの保存領域も大きい傾向があります。

対して Vivoactive や Vivo VENU などのライフログ用デバイスはタッチスクリーン搭載で、ボタン数が少なくシンプルなケースにスマートウォッチ寄りのインターフェースを備えています。

私はランニングやサイクリング用のアクティビティトラッカーとして利用するつもりなので、たくさんの選択肢のなかから ForeAthlete 245 (音楽再生機能なし) を選択しました。


GARMIN( ForeAthlete 245 Black Slate 010-02120-42

過去モデルとの比較

ForeAthlete 245 を購入する以前は、旧モデルの ForeAthlete 230 J を約3年間ほど使用していました

この旧モデルの 230 J / 235 J (モデルの違いは光学心拍計搭載の有無) と比較して 245 はハードウェアの面でもソフトウェアの面でも大きく改良が加えられています。

ハードウェアとしての変更点は、いかにもスポーツウェア然としていた旧モデルから普段遣いできるデザインに変わった外観だけではなく、旧来の充電クリップから変更された充電ケーブルと柔軟素材に切り替えられたバンドが挙げられます。


GARMIN ForeAthlete 230J【日本正規品】

この充電ケーブルはUSBデータ転送に対応しており、Bluetooth でスマートフォンに接続して専用アプリを利用しなければアクティビティログも見られなかった 230 J / 235 J 時代とは異なり、本体とケーブル単体のみでデバイス中のデータを管理できるようになりました。

サイコンの Edge シリーズでは汎用 Micro USB ケーブルを使用できることと比較すると、この専用ケーブルが必須になる点は全然イケてないと個人的には思ってしまうのですが、ForeAthlete シリーズの利便性が大きく向上したことは間違いありません。

230 J / 235 J 時代は硬めだったベルトも柔らかいシリコン素材に変更され、長時間装着し続けても違和感を覚えなくなりました。

このベルトはおそらく光学心拍計を利用するためのもので、腕時計を外すとあとが残るほどに皮膚に密着させることを考慮して採用されたことが推定されます。

本体 (ケース) が小型化されたことも相まって、長時間の装着にも違和感がなくなりました。

ランニングウォッチとして

ForeAthlete の旧モデルと比較すると、じつはあまり変化がない点がランニングウォッチとして見た場合の機能です。

たしかにカタログスペック上の稼働時間は約11時間から最大22時間と2倍になり、画面解像度も 215 x 180 から 240 x 240 pixel へと向上しているので、より長時間稼働で画面も見やすくはなっています。

しかしランニングは身体への負荷が大きく、何時間も続けられるスポーツではないので、既に ForeAthlete の旧モデルをお持ちの方が新しく 245 を購入された場合には、本体の軽量化ぐらいしかメリットを実感できないかもしれません。

ForeAthlete の機能は旧モデルの時点で完成しており、245 にモデルチェンジした際に変更された部分はわずかです。

より具体的にはスマートフォン連携時の気温や風速の表示、1日の総移動距離の表示がアクティビティ毎に細分化されたことなどです。

言い換えると旧モデルと比較した場合の機能や操作性はほぼ同一なので、新モデルへの乗り換えも違和感なく行えると思われます。

サイコンとして

従来からウェアラブルデバイスの ForeAthelete とサイクルコンピュータの Edge は機能が重複しており、ForeAthlete をお持ちであれば Edge とほとんど同じことができました。

どちらも ANT+ 規格のバイク・スピードケイデンスセンサと同期し、GPS データを受信して走行ログを FIT 形式で保存することができます。異なるのは Edge では実走のサイクリングとローラー台トレーニングのログのみを記録できるのに対して、ForeAthlete ではそれらに加えて1日の歩数やランニングのログまで記録までできる点です。

Edge と ForeAthlete とで記録できるログデータは全く同一です。それどころか、ForeAthlete では自動で心拍まで計測してくれるため、ログの記録内容は Edge より充実するかもしれません。

このままではサイコン Edge の存在意義が失われると考えられたのか、2020年現在の現行モデル Edge 530 / 830 / 1030 Plus では地図表示とナビゲーション機能が強化され、さながらスポーツバイクにおけるカーナビのように使用できるように改良されました。




しかし、そうした地図表示やナビゲーションが不要であれば、ForeAthlete 245 も Edge 同様にサイコンとして走行距離や時速やケイデンスを表示し、GPS 走行記録をデータとして保存できる機能を備えています。

従来の ForeAthlete 230 / 235 J では走行時の稼働時間が約11時間と走行距離 220km を越えるライドで使用するには不安がありましたが、245 に更新されて稼働時間が約22時間に延長されたことによって 300km 程度のライドでも記録用デバイスとして選択肢に入るようになりました。

ForeAthlete のモデルチェンジの恩恵を正しく実感できるのは、実際にはランニングよりサイクリング用途での利用時なのかもしれません。

スマートウォッチとして

かねてより、いかにもなスポーツウェア形状をしていた ForeAthlete シリーズですが、こと ForeAthlete 245 に関しては普段着に合わせても違和感の少ない形状をしています。

最近では腕時計をしていない人も珍しくはないですが Apple Watch 等の普及によって仕事で使われる時計の選択も自由度が高まってもいます。身近なところでは顧問の弁理士などを見ていても CASIO や TISSOT といったスポーティな時計を着けている専門職も見かけるほどで、時計として GARMIN を使用していても不思議な感じはありません。

245 にモデルチェンジしてからは UTF-8 で定義されている文字はおおよそ全てが表示されるようになったことも特筆すべき点です。

旧モデルでは着信や SNS 通知などが表示される際 ç や ß といったアルファベット26文字に入らない文字は表示されず、そこだけ空白表示されていたところを新モデルでは顔文字まで表示されるように改善され表現力が格段に向上しています。

ただし文字の入力やメッセージへの返信は行うことができませんので、スマートウォッチとしては機能が限定されていると割り切る必要があります。

それでもランニングウォッチのオマケとしては十分過ぎるぐらい高機能ですし、あまり機能が増えてもバッテリの消耗が激しくなるので、ウェアラブルデバイスとしてはこれぐらいが調度いいとも考えられます。

総合すると ForeAthlete の旧モデルをお持ちのランナーの方で、お使いのモデルが重いと感じていない人であれば急いで買い換える必要性は低いかも知れませんが、ランニングだけでなく普段遣いやサイクリングでも使用される方なら満足できるモデルだと言えそうです。