新着記事

Viewing posts from December, 2022

当社プロダクトにおける、nuxt2→nuxt3 への移行時の記録

2022年11月16日、Nuxt3 が正式リリースされました。

当社でサービスしているプロダクトの1つを、Nuxt2 から Nuxt3 に書き換えましたので、変更した実施内容を書きます。

Nuxt2 から Nuxt3 への変更点

概要としては、 Zennの下記記事が参考になります。

祝・正式リリース!5つのテーマで理解する Nuxt3 の魅力

具体的な変更点や使い方は、今回の記事には記載しません。公式サイトの docs タブにまとめられています。マイグレーションを行う際には、docs 内の記事は一通り読むことをおすすめします。

Introduction · Get Started with Nuxt

移行(マイグレーション)の進め方

既存の Nuxt2プロジェクトを Nuxt3 にマイグレーションする場合、リポジトリをそのまま使うか新しく作るかの選択がありますが、新しく作ることを推奨します。

変更点は膨大になるため、既存のリポジトリをそのまま使うのは現実的ではありませんし、既存リポジトリを流用していく方法では、最初の動作確認をするまでにしなければいけないコード修正の量が膨大です。最小のコード修正で最初の動作確認ができるようにしたほうが、間違った時のコストを少なくできます。

npx nuxi init <project-name>

で新しいプロジェクトを開始し、リポジトリも新しく作ってください。

新しいプロジェクトの作成後は、まずは利用規約ページ、FAQページ、問い合わせページ等、依存リソースの少ないページを1つづつ移行していくと良いと思います。

移行にかかった時間

今回のプロジェクトは、pages が 20, components が 100 ほどの規模でしたが、空き時間にちまちまやって3週間かかりました。集中して時間がとれれば1週間ぐらいでできると思います。

  • ユニットテストはありません。
  • この時間には、スタッフによる品質検証の時間は含まれていません。

移行時の目立った問題

Vue2 で使用していたプラグインが Vue3 に対応していない場合がある

Nuxt ではなく Vue の話になるのですが、いくつかの Vue2 用のライブラリが Vue3 に対応していなため、代替を探したり自作する必要があります。

Nuxt2 から Nuxt3 のマイグレーションで、一番時間がかかったのはこの部分でした。

今回は、以下の変更がありました。

  • hooper → swiper (トップページのヒーローカルーセルで使用)
  • vue-markdown → vue3-markdown-it (MarkDown のレンダリング)
  • vue-multiselect → @vueform/multiselect (Selectウィジェットの代替)

動作に互換性は無いため、使用箇所は新しいライブラリの API に適合できるよう、大きく書き直しています。

以下のライブラリは、バージョンアップで Vue3 にも対応していたため、そのまま使用できました。(プラグインファイルだけ書き直しました)

  • v-calendar

以下のライブラリは、Vue3 に対応しておらず、代替のライブラリも無かったため、一旦機能を無効化しています。
再現するには自作する等の対応が必要になります。

  • vue-highlight-words (検索語句のハイライト)

コード修正箇所

Nuxt2 プロジェクトから Nuxt3 プロジェクトにマイグレーションする際の目立った変更箇所です。

APIプロキシ

当社のプロダクトは、 /api/ 等のリクエストをバックエンドサーバにプロキシを行うようにしています。

(Nuxt3 にビルトインされている、 /server/ ディレクトリを用いるサーバサイド API は使っていません。)

プロダクトによっては、Nuxt に入ってくる前に AWS の アプリケーションロードバランサー や Kubernetes Ingress でルーティングする場合と、一度 Nuxt で受けてから Nuxt のサーバサイドプロセスでプロキシする場合と、両方のパターンがあります。

Mac での開発環境では、Kubernetes Ingress や Nginx は使わないため、必ず Nuxtでのプロキシが必要になります。

Nuxt2 では、nuxt.config.ts の proxy 設定で書いていましたが、Nuxt 3 では本番環境で使うか (つまりビルド成果物に含めるか) どうかで2通りのプロキシ方法があります。

本番環境ではプロキシは必要なく、開発環境でだけ実現だければ良い場合

本番環境では、ALB や Kubernetes Ingress でルーティングが実現できるため必要なく、開発環境でのみプロキシを行いたい場合は、Nitro の devProxy の機能で実現できます。

主に、SSR を行わず、成果物を SSG ( generate ) して動作させる場合はこの方式になると思います。

nitro の標準機能で実現できるため、追加のライブラリは必要がなく、設定ファイルを1つ追加させるだけで可能です。

nitro.config.ts の例
import {defineNitroConfig} from 'nitropack'

function createNitroConfig() {
// 開発環境用のプロキシを設定する
const configFile = process.env.CONFIG_FILE || 'local'

const config = require(`./config/${configFile}.ts`)

if (!config.makeProxyValue) {
console.error(`./config/${configFile}.ts に makeProxyValue がありません`)
}

const proxyPaths = [
'/api/',
'/login/',
'/media/',
...
]

for (const proxyPath of proxyPaths) {
proxySettings[proxyPath] = config.makeProxyValue(proxyPath)
}
return {
devProxy: proxySettings
}
}

export default defineNitroConfig(createNitroConfig())
config/development.ts
// nitro プロキシの設定
export function makeProxyValue(path: string) {
return {
target: `https://api.example.com${path}`,
changeOrigin: true,
hostRewrite: true,
cookieDomainRewrite: true,
headers: {
'X-Forwarded-Host': 'localhost:3006',
'X-Forwarded-Proto': 'http',
'Referer': 'https://api.example.com'
}
}
}

詳しくは、 Configuration | ⚗️ Nitro の devProxy の項目をご覧ください。

本番環境でも開発環境でも同様にプロキシを行う場合

本番環境でも Nuxt のプロセス(コンテナ) でプロキシを行う場合、標準機能では実現できないため nuxt-proxy のライブラリを使います。

SSR を行う場合はこの形になると思います。

nuxt-proxy - npm

npm でインストール後、 nuxt.config.tsdefineNuxtConfigproxy 設定を追加します。

nuxt.config.ts
export default defineNuxtConfig({
  ...
  modules: ['nuxt-proxy'],
  proxy: {
    options: {
    target: ...,
changeOrigin: true,
headers: ...,
pathRewrite: {
'^/some/url/path/': '/api/path/'
...
},
pathFilter: [
'/more/url/path/',
...
]
}
}

環境設定変数の変更

Nuxt3 とは直接的な関係が薄い内容ですが、
当社では、今までのプロジェクトでは環境変数 NODE_ENV に、development, production 以外の設定名も指定していました。
例えば、ステージングサーバでは NODE_ENV=staging といった形での指定がありました。

ただし、ライブラリによっては NODE_ENVdevelopmentproduction のどちらかのみ想定しているもが多く、ビルドする上でも不都合がありましたので、 NODE_ENVdevelopmentproduction 以外の値を入れるのはやめました。

サーバ環境で設定を分岐する場合は、 CONFIG_FILE という新たな環境変数を使用するようにしています。

CONFIG_FILE で設定ファイルを指定し、記載されている設定を Nuxt に伝えるために、環境別の設定ファイルと nuxt.config.ts はこのような指定となっています。

config/devserver.ts
export const runtimeConfig = {
  // public 内で定義したものはクライアントでも使える
public: {
baseUrl: 'https://example.com',
}
}

export const proxyConfig = {
target: 'http://host.docker.internal:8000',
headers: {
'Host': 'example.com',
'X-Forwarded-Host': 'example.com',
'X-Forwarded-Proto': 'https',
'Referer': 'https://example.com'
}
}
nuxt.config.ts
const configBaseName = process ? (process.env.CONFIG_FILE || 'development') : 'development'
const configContent = require(`./config/${configBaseName}.ts`)

export default defineNuxtConfig({
// runtimeConfig は useRuntimeConfig() で参照できる
runtimeConfig: configContent.runtimeConfig,
...
modules: ['nuxt-proxy'],
proxy: {
options: {
target: configContent.proxyConfig.target,
changeOrigin: true,
headers: configContent.proxyConfig.headers,
...
}
}

※ 定義した設定をクライアント側で使うには、nuxt.config.ts の中で runtimeConfig.public に設定を入れ、クライアント側で useRuntimeConfig を実行します。

useRuntimeConfig · Nuxt Composables

vueコンポーネントの変更

script セクションは、すべて script setup に書き換えました。

setup 構文は、Vue3 の一番目立つ変更点だと思います。

Nuxt3の自動インポートの機能もあいまって、今までの Vue コンポーネントの script 部分を 1/2 程度の分量で記述でき、
とても良い開発体験があります。

多くの記事で説明されているため、概要の紹介はここでは書きませんが、ざっくり書いたカウンターのコンポーネントは以下のようになります。

<script lang="ts" setup>
const count = ref<number>(0)

function increment() {
count.value++
}
</script>

<template>
<button @click.prevent="increment">
{{ count }}
</button>
</template>

本当に必要な内容だけをコードで書けばよくなっています。コンポーネント内で、import を書くケースはほとんどありません。

以下の記事が参考になります。

【Vue.js 3.2】`<script setup>` 構文がすごくすごい

page コンポーネントの変数プレースホルダの変更

今まで、 _id.vue と命名していたファイルは、 [id].vue とつける必要があります。

components ディレクトリの自動インポート

components ディレクトリ内に書いた vue コンポーネントは、 import 文無しで使うことができます。

例えば、components/header/HeaderSearchButton.vue という名前のコンポーネントは、 インポート無し<HeaderSearchButton> として使えます。

また、 components/header/SearchButton.vue という名前のコンポーネントも、同じく、 <HeaderSearchButton> として使えます。

ディレクトリ名 + ファイル名 でコンポーネント名になりますが、ファイル名の先頭とディレクトリ名が重複する場合、重複する部分は打ち消されます。

components/global/ ディレクトリは特別で、この中に入れたものは Global を省略して、ファイル名そのままでコンポーネントとして使えます。

今までの当社のプロダクトは、区分が必要無いコンポーネントについては components/common/ ディレクトリに配置することが多かったのですが、components/common/ ディレクトリは components/global/ ディレクトリに変えるようにしています。

Vuex ストア → composables ディレクトリ内の composable 関数

Nuxt2 では、グローバルストアは Vuex + vuex-module-decorators (もしくは nuxt-typed-vuex) を使って書いていました。

Nuxt3 では、Vuex は使わなくなり、かわりに composable を使ってグローバル状態管理を構築します。

composables/ · Nuxt Directory Structure

composables/ 以下のディレクトリにある useXxxxx 関数は、自動インポートの対象になります。

例えば、vuex-module-decorators では、カウンターの Vuex ストアは以下のように書いていました。

Nuxt2: store/counter.ts ( vuex-module-decorators の場合)
import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'

@Module({
name: 'counter',
stateFactory: true,
namespaced: true
})
export default class extends VuexModule {
count: number = 0

get doubleCount (): number {
return this.count * 2
}

@Mutation
setCount (value: number) {
this.count = value
}

@Action({ rawError: true })
async increment () {
this.setCount(this.count + 1)
}
}

nuxt-typed-vuex で書いた場合は以下のようになります。

Nuxt2: store/counter.ts ( nuxt-typed-vuex の場合)
import { getterTree, mutationTree, actionTree } from 'typed-vuex'

export const state = () => ({
count: 0 as number
})

export type RootState = ReturnType<typeof state>

export const getters = getterTree(state, {
doubleCount(state): number {
return state.count * 2
}
})

export const mutations = mutationTree(state, {
setCount(state, count: number) {
state.count = count
}
})

export const actions = actionTree(
{ state, mutations },
{
increment({ commit }) {
commit('setCount', state.count + 1)
}
}
)

Nuxt3 composable の場合、composable を使って以下のように書きます。

composables/counter.ts
export const useCounterComposable = () => {
const count = useState<number>('count', () => 0)

const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}

return {
count,
doubleCount,
increment,
}
}

Vueコンポーネント内で使う際は、このようになります。

composables/counter.vue
<template>
<button @click.prevent="counterComposable.increment" >
{{ counterComposable.count}} * 2
= {{ counterComposable.doubleCount }}
</button>
</template>
<script lang="ts" setup>
const counterComposable = useCounterComposable()
</script>

Vuex Store から composable 関数へのマイグレーションは、完全に 1:1 で行えます。
(ミューテーションは不要になります。)

ファイル名はそのままに、手動で機械的に composable 関数に変換していくと良いでしょう。

注意点

composables/counter.ts の

const count = useState<number>('count', () => 0)

は、

const count = ref<number>(0)

と、ref を使って書いても良さそうに見えます。実際それでも動くのですが、refsetup セクション以外で使うとサーバで状態が共有されメモリリークが発生するため、 setup セクション以外では使えません。

State Management · Get Started with Nuxt

useState の第一引数のキー名は、利用者のアプリ内で一意のものであり、キー名が一致したらどこでコールしても共通のカウント数が取得できます。

const count = useState<number>('count', () => 0)

ただし、使用箇所が分散するとメンテナンスが難しくなるため、composables/ の中だけで扱うと決めるのが良いと思います。

サイトすべてで共通の HTML の head セクション

title タグ、viewport や文字コード等の meta タグ、共通スタイルシートの link タグなどの、サイト内で共通の HTML の head セクションの定義は2通りの方法があります。

nuxt.config.ts の app.head に書く

Nuxt2では、nuxt.config.tshead というセクションに書いていましたが、Nuxt3 以降は app.head に書きます。

Nuxt Configuration Reference · Nuxt #head

基本的な書き方は変わっていません。

app.vue で useHead を使う

一番大枠のVueコンポーネントである、app.vue に、script setup を作り、useHead 関数を使って Head 項目の定義をすることもできます。

nuxt.config.ts で定義すれば良いものなので基本的には使うことは無いと思いますが、昔にこの形式でやってたこともあるので、これでも設定できることを書き残しておきます。

テンプレートフィルタ構文の廃止

テンプレートでの、下記のようなフィルタ構文廃止になり、Vue3 からは使えません。

{{ taxIncluedTotalPrice | addComma }}

そのため、普通の関数に変更する必要があります。

{{ $addComma(taxIncluedTotalPrice) }}

プラグインの書き方の変更

上記、addComma 関数は、プラグイン関数として定義すると、どこからでもインポート無しで使えて便利です。プラグイン関数はこのように書きます。

plugins/filter.ts
export default defineNuxtPlugin((nuxtApp) => {

/**
* 金額をカンマ区切りに表示します。
* $addComma(1000) => 1,000
*/
nuxtApp.provide('addComma', function (val: number) {
if (val === 0) {
return '0'
}

if (!val) {
return ''
}

if (typeof val === 'string') {
val = Number(val)
}

return val.toLocaleString()
})
})

nuxtApp.provide を実行することでで、システムグローバルに $xxxxx という名前で使える関数を定義できます。
テンプレート内でも、script setup 内でも使えます。

app.use する場合

v-calendar のように、Vue3 で app.use でプラグインを登録するライブラリを Nuxt3 で使う場合はこのように書きます。

Vue 3 | V-Calendar

plugins/v-calendar.ts
import {defineNuxtPlugin} from 'nuxt/app'
import VCalendar from 'v-calendar'

// https://vcalendar.io/vue-3.html
export default defineNuxtPlugin((nuxtApp) => {
if(process.client) {
nuxtApp.vueApp.use(VCalendar, {})
}
})

ページを遷移した時に自動的にページの先頭にスクロールアップするプラグイン

ページを移動した時にページの先頭にスクロールアップし、さらにブラウザを戻った場合に前回見ていたスクロール位置をキープする必要がある時、下記ディスカッションで書かれているコードを適用すると良いです。

scrollToTop in v3 ? · Discussion #1661 · nuxt/framework

このディスカッションの、この発言が要件を満たす完璧なコードです。

https://github.com/nuxt/framework/discussions/1661#discussioncomment-3967225

長くて仰々しいですが、使用感は一番良いです。

とはいえ…正直な所、JS のフレームワークを使っている以上、ブラウザの「戻る」の体験は、ページごとに HTML を返すクラシックなスタイルに並ぶまでは至りません。

検索結果のリストを表示して、クリックして詳細表示に遷移し、ブラウザの戻る機能で戻り、その下のものをクリックして…といった当たり前に行う操作において、クラシックなHTMLアプリに近い操作感に達するまでに、書かなければいけないコードや気にしなければいけないことが多すぎます。今後も研究していく課題だと思っています。

データフェッチ周りの改修

当社では、Nuxt2 では axios を使ってデータフェッチを行うこと多かったですが、Nuxt3 からは ohmyfetch 改め ofetch を使います。

リアクティブな状態変化に対応できるのは良いのですが、サーバでフェッチした内容がクライアントにハイドレーションされる時の挙動など慣れが必要となると思います。

また内容をキャッシュする機能があり、場合によっては意図しない所で内容がキャッシュされていまい、利用者が操作しても内容が切り替わらないこともよくあります。

データフェッチ用の関数も、 useAsyncData, useLazyAsyncData, useFetch, useLazyFetch, $fetch と、いくつもあります。

Data Fetching · Get Started with Nuxt

await useFetch('/api/count') と書いた場合、クライアント側とサーバ側で処理が変わります。

これは、私のプロダクトが /server/api/ を作っておらず、外部サーバへプロキシしているためかもしれません。

サーバ側で、 useFetch('/api/count') が発生した場合、リクエストするホスト名が不明なため、リクエストが失敗します。

そのため、サーバ側では、useFetch のオプションの baseUrl でスキーマとホストhttps://example.com を指定するか、リクエスト先のURLの先頭にスキーマとホスト等をつけて、 'https://example.com/api/count' といった完全な形のURL に変換する必要があります。

リクエストヘッダについても、HostAccept 等のヘッダを必要に応じて付与します。サーバサイドプロセスから外部のサーバにリクエストする場合、プロキシではないため nuxt.config.ts の proxy の設定は使われません。少し混乱しやすい箇所だと思いますのでご注意ください。

また、useFetch のキャッシュのキーの生成についても、私の使い方の問題だとは思うのですが、期待通りに一意のキーができてないように思うので、 useFetch のラッパーを作って明示的にキーを作っています。

export async function useFetchAPI<T>(url: string, options: any = {}) {

if (!options.key) {
// キャッシュキーを明示的に作る
options.key = url + JSON.stringify(options)
}
let requestUrl: string = url
if (process.server) {
// サーバリクエストの場合は完全な URL を作ってリクエストする
const runtimeConfig = useRuntimeConfig()
options.baseURL = runtimeConfig.proxy.options.target

// Docker 内から host.docker.internal にアクセスする際に Host ヘッダを付与する
if (runtimeConfig.proxy.options.headers.Host) {
if (!options.headers) {
options.headers = {}
}
options.headers.Host = runtimeConfig.proxy.options.headers.Host
}
}

return await useFetch<T>(
requestUrl, options)
}

$fetch は、 axios の .$get のように、レスポンスの中で必要なコンテンツのみを返してくれる関数ですが、axios の .get や、python の requests, また生の fetch のように、リクエストの結果をレスポンスオブジェクトとして取得し、レスポンスオブジェクトの HTTP ステータスや Content-Type を見て処理を分岐するといったコードは書けません。

ただ、データの Put や Post 等の更新系のリクエストは、$fetch を使わずにもっとプリミティブなレスポンスを扱ってエラーハンドリングをしたいのと、CSRF を防ぐためのトークン送信なども必要になると思いますので、$fetchuseFetch を使わず、 fetch のラッパーを作って使っています。

package.json

Nuxt3 の場合、 package.jsondependencies空のオブジェクトにして、必要な依存関係はすべて devDependencies に書きます
Svelte でもそうで、Vite を使う場合は基本的にこの形になるのだと思います。

package.json
{
"private": true,
"scripts": {
"build": "nuxt build",
"build:devserver": "CONFIG_FILE=devserver nuxt build",
"build:production": "CONFIG_FILE=production nuxt build",
"dev": "PORT=3006 nuxt dev",
"dev:local": "CONFIG_FILE=local PORT=3006 nuxt dev",
"generate": "nuxt generate",
"preview": "PORT=3006 nuxt preview",
"postinstall": "nuxt prepare"
},
"devDependencies": {
"nuxt": "3.0.0",
"sass": "^1.56.1",
"@vueform/multiselect": "^2.5.6",
"nuxt-proxy": "^0.3.4",
"swiper": "^8.4.4",
"v-calendar": "^3.0.0-alpha.8",
"vue3-markdown-it": "^1.0.10"
},
"dependencies": {
}
}

ビルドを行うと、nitro/h3 の Webサーバエンジンも含めたすべての依存ライブラリを output ディレクトリにまとめて書き出します。
output ディレクトリさえあれば他に依存せずアプリを動作させることができるようになっています。

Dockerfile

動作時に外部の依存ライブラリが必要無いということは、 Dockerfile をかなりシンプルに書けます。

Dockerfile
FROM node:16-slim

WORKDIR /app

COPY .output /app/.output

USER nobody
CMD [ "node", ".output/server/index.mjs"]

当社の環境では、ビルドごとに Docker イメージを作っています。
.output ディレクトリを実行環境にコピーできれば、node の docker コンテナにマウントさせて実行させるのも簡単なため、独自の Dockerイメージすら必要無いでしょう。

本番の実行環境の構築が非常に楽になったのは Vite を使うようになってからの大きな利点だと感じます。

Raspberry Pi Pico W で Httpサーバ(microdot)とセンサーによるHTTPリクエスト機能を同時に稼働させる

Raspberry Pi Pico W が発表されました。日本ではまだ未発売ですが、技適は取得されたようですので近いうちに国内販売がされそうです。

試しに、Webサーバ ( Microdot )とWebクライアント(urequest) を uasyncio で並列実行するコードを書きましたので、紹介します。

今回作成したコードや動作している動画は、Github で公開しています。

ytyng/rpi-pico-w-webserver-and-client: Raspberry Pi Pico W webserver and client sample code

Raspberry Pi Pico W とは

コストパフォーマンスが高いマイクロコントローラです。カテゴリとしては Arduino 等に近く、今までの Raspberry Pi のように、Linux OS を動作させるようなマシンではありません。

RP2040 というラズベリーパイ財団が開発したチップのデモボードという位置づけとなります。

実際の商品開発では、Raspberry Pi Pico で製品のR&Dを行い、実際は RP2040 を搭載した製品として生産するという流れとなると思いますが、ホビーや SOHOでは Raspberry Pi Pico をそのまま使うことも多いと思います。

実際、当社でも Raspberry Pi Pico を用いてイベント用の機材を作る場合がありますが、
RP2040 を使ったの製品を作るわけではなく、Raspberry Pi Pico をそのままケースに入れて使います。

MicroPython が動作するため、Python に慣れていれば開発は容易にできます。

商品名に W がつかない今までの機種は、ネットワーク機能はありませんでしたが、今回 Raspberry Pi Pico W となって無線チップが搭載され、コストパフォーマンスと使い勝手が最高の IoT デモボードとなりました。

↑ 左が Raspberry PI Pico, 右 が無線LAN チップが搭載された Raspberry Pi Pico W

考えられる用途

Raspberry Pi Pico W の用途で多く使われると考えられる用途は、

  • 接続させているデバイスのセンシング情報を元に、HTTP リクエストを発生させる
  • HTTP サーバを起動し、外部から HTTP リクエストを受け取って、接続されているデバイスを動作させる

この2つが主なものとなると考えられます。

今回は、この2つを Raspberry Pi Pico W の中で同時に実行する方法を書きます。

一通りのチュートリアル

Raspberry PI の公式ページが提供している PDF が充実います。

https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf

ただ、PDF なので少し読みにくいのと、Thonny に関しては言及されていないため、Mac や Windows を普段使われている方は、この PDF だけでなく、他のサイトで紹介されているような Thonny を使ったセットアップを行うと良いでしょう。

ファームウェアの準備

https://micropython.org/download/rp2-pico-w/

上記 URL で、Raspberry Pi Pico 用の MicroPython ファームウェアの uf2 ファイルが入手できます。

最新版への直リンクはこちらです。 https://micropython.org/download/rp2-pico-w/rp2-pico-w-latest.uf2

ファームウェアのファイルをダウンロードし、 Pico へコピーしてください。

W 対応でない uf2 ファームウェアは別に存在します。そちらを使った場合、Wi-fi の機能が使えませんのでご注意ください。

コピー方法

Pico の BOOTSEL ボタンを押したまま USB で PC に接続すると、PCが Pico をストレージとして認識します。

ダウンロードした uf2 ファイルを Pico にドラッグアンドドロップでコピーすると、自動的にファームウェアがロードされ、 Pico が再起動します。

Wifi に接続する

一番最初に、Wifi に接続する必要があります。
SSID と パスワードが変数化されていれば、後は簡単なコードで接続が行えます。

接続用の関数を作っておくと便利で、他の方を見ても関数化しているようです。

StackOverflow の話題を見ると、Wifi との接続は main.py の中でやらずに boot.py の中でやったほうがいい、というコメントをいくつか見かけましたが、私は 開発のしやすさから main.py の中で行うようにしています。

Wi-fi に接続するコード

https://github.com/ytyng/rpi-pico-w-webserver-and-client/blob/main/network_utils.py

import rp2
import network
import uasyncio
import secrets


async def prepare_wifi():
"""
Prepare Wi-Fi connection.
https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf # noqa
"""
# Set country code
rp2.country(secrets.COUNTRY)

wlan = network.WLAN(network.STA_IF)
wlan.active(True)

wlan.connect(secrets.WIFI_SSID, secrets.WIFI_PASSWORD)

for i in range(10):
status = wlan.status()
if wlan.status() < 0 or wlan.status() >= network.STAT_GOT_IP:
break
print(f'Waiting for connection... status={status}')
uasyncio.sleep(1)
else:
raise RuntimeError('Wifi connection timed out.')

# CYW43_LINK_DOWN (0)
# CYW43_LINK_JOIN (1)
# CYW43_LINK_NOIP (2)
# CYW43_LINK_UP (3)
# CYW43_LINK_FAIL (-1)
# CYW43_LINK_NONET (-2)
# CYW43_LINK_BADAUTH (-3)

wlan_status = wlan.status()

if wlan_status != network.STAT_GOT_IP:
raise RuntimeError(
'Wi-Fi connection failed. status={}'.format(wlan_status))

print('Wi-fi ready. ifconfig:', wlan.ifconfig())
return wlan

接続させているデバイスのスイッチ(センサー)情報を元に、HTTP リクエストを発生させる

Raspberry Pi Pico W Wi-Fi Doorbell tutorial (HTTP requests & IFTTT) — PiCockpit | Monitor and Control your Raspberry Pi: free for up to 5 Pis!

こちらの方が開発されているドアベルのコードが参考になります。
Youtube 動画もあってわかりやすいです。

urequests

Python でよく使う、requests ライブラリに変わり、MicroPython では似たような使い勝手の urequests ライブラリを使うことができます。

ネットワーク接続が確立されていれば、あとは urequests.get(...) 等で簡単にリクエストが発行できます。

Thonny の tools -> Manage packages からインストールできます。

HTTP サーバとして稼働させる

ソケットをそのまま使って簡易的な HTTP サーバにする

こちらの記事が参考になりました。大変わかりやすく日本語で説明されているので、Pico の初学者にもおすすめします。

Raspberry Pi Pico W で無線Lチカ

リンクされている、mimoroni 社のコードは、 Pico 用の拡張されたファームウェア

https://github.com/pimoroni/pimoroni-pico/releases

や、 Pico W 用の各種ユーティリティコードがあり、開発の参考になります。

このコードの HTTP サーバの部分は、TCP ソケットをそのまま使い、リクエスト本文の中のパス名と文字列一致して判定してい分岐を行っています。

Raspberry Pi 公式のチュートリアルPDFでもその方式で行っていました。

規模が小さいようであれば十分だと思いますが、HTTP ヘッダーを扱いたい場合や、少し規模を拡張したい場合はこの形では難しいでしょう。

Microdot を起動する

Flask や Bottle、fastApi が動けば良いのですが、現状は動作しません。
代わりに、Microdot という ウェブフレームワークがあり、使い勝手としては Flask や Bottle によく似ていて大変勝手が良いです。

こちらを使って ウェブサーバを起動してみます。

Microdot

Thonny で Tools -> Manage packages からインストールすることができます。

ネットワーク接続が確立したら、

app = Microdot()

@app.get('/')
async def _index(request):
return 'Microdot on Raspberry Pi Pico W'

app.run(port=80)

このような親しみやすいコードで HTTP サーバが起動します。

ウェブサーバとセンサーリクエストを同時に使う

Raspberry Pi Pico は、通常シングルスレッド動作です。(ちなみにCPUはデュアルコアです)

一応、 _threading という疑似スレッドができるライブラリはありますが、処理によっては本体が暴走したり固まることが多く、かなりおすすめしません。
暴走すると、最悪、何度もファームウェアのリセットをするこになり、開発体験は良くありません。

代わりに、 asyncio を使ったコルーチン処理を標準で行うことができ、こちらは安定して動作しますので、 Pico で開発する際は、基本的にメソッドはコルーチンで書くのをおすすめします。

Pico は、待機ループで sleep を使うことも多いですし、コルーチンと相性が良いと感じます。

Microdot も非同期対応の起動ができるものが既に開発されています。

Pico 上の MicroPython でのコルーチンは、通常の Python にビルトインされている asyncio を使うのではなく、
uasyncio というパッケージを使います。

Pico 用の uf2 ファームウェアに含まれていますので、別途新たなインストールは必要ありません。

例えば下記のようなコードで、asyncio が有効な処理を開始することができます。

import uasyncio

async def main():
uasyncio.create_task(any_async_method())
await other_async_method()


if __name__ == '__main__':
    uasyncio.run(main())

Pico の起動後、無線 LAN に接続した後は、スイッチ押下待機のループと、Micorodot の起動を
両方ともコルーチンで書くことで、無理なく並列動作をさせることができます。

実際に動作するコードは Github で公開しています。

rpi-pico-w-webserver-and-client/main.py at main · ytyng/rpi-pico-w-webserver-and-client

メインのコードとしてはこのようになります。

"""
Raspberry Pi Pico Web Server with Microdot and Switch Sample Code.
Pin 14 is used for switch input.
"""
import machine
import urequests
import network_utils
from microdot_asyncio import Microdot
import uasyncio


async def switch_loop():
"""
Switch listener loop
Pin 14 is used for switch input.
When press switch, send request to http web server.
"""
print('start switch_loop')
switch_pin = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)

while True:
current_state = switch_pin.value()
if current_state:
# Change the URL to your own server, IFTTT, Slack, etc.
response = urequests.get('https://example.com/')
print(response.content)
response.close()
await uasyncio.sleep(2)
else:
await uasyncio.sleep(0.1)

async def run_web_server():
"""
Start microdot web server
https://microdot.readthedocs.io/en/latest/index.html
"""
app = Microdot()
led_pin = machine.Pin('LED', machine.Pin.OUT)

@app.get('/')
async def _index(request):
return 'Microdot on Raspberry Pi Pico W'

@app.get('/led/<status>')
async def _led(request, status):
"""
/led/on : LED ON
/led/off : LED OFF
"""
if status == 'on':
led_pin.on()
return 'LED turned on'
elif status == 'off':
led_pin.off()
return 'LED turned off'
return 'Invalid status.'

print('microdot run')
app.run(port=80)


async def main():
wlan = await network_utils.prepare_wifi()
print('LED ON: http://{}/led/on'.format(wlan.ifconfig()[0]))

uasyncio.create_task(switch_loop())
await run_web_server()


if __name__ == '__main__':
uasyncio.run(main())

Svelte で作ったWebコンポーネントを接続国によって出し分ける

概要

11月28日、当社 TORICO の初の海外店舗、マンガ展 台湾 がオープンしました。

漫畫展 台灣

オープンする直前で、「漫画全巻ドットコム、スキマ、マンガ展、ホーリンラブブックスのサイトに、台湾からアクセスされた方に告知を出したい。告知を1回クリックしたらもう出さないようにしたい。」という開発要件が出たので、作ってみました。

接続元の判定と出し分け

今回は、 AWS の CloudFront で、接続元の国名のヘッダを付与する機能を使うことにしました。

CloudFront の入口でリクエストヘッダが付与されますので、CloudFront Functions でそのヘッダを見て動的な処理が行えます。
ヘッダに応じて、対象国であれば、バックエンドのパスを変化させることで、接続国に応じた処理を行えるように設計しました。

サーバの管理はなるべく減らしたいため、今回は完全にサーバレスでサービスを作っています。動的な処理は、CloudFront Function を使いました。

コンポーネントの開発

全サイトに共通のモーダルポップアップ表示を行う必要があります。当社エンジニアのまるさんから、Svelteを進められたので、Svelte で Web コンポーネントを書きました。

完成物

これらのサイトに、台湾からアクセスすると、モーダルポップアップが表示されます。

仕組みとしては、リクエストするスクリプトの内容が、接続国に応じて変化するようになっています。

Svelte で Web コンポーネントを作る

Svelte の概要

Svelte は、Vue のシングルファイルコンポーネントによく似た形で、1ファイルで1コンポーネントをちょうどよく書けます。

CSS はスコープドなものになり、変数は特に意識せずともリアクティブに扱うことができます。

Web コンポーネントとは、JS によって作られた独自の HTML タグをどんなサイトでも使えるようにする技術です。
コンポーネントを定義した JS ファイルをロードすることで、そのサイトで <my-component></my-component> のような独自のタグを使えるようになります。
そのサイトが 素の HTML + jQuery で作られているサービスでも、React や Vue で作られているサイトでも、Google Tag Manager からでも同じようにコンポーネントが使えます。

複数サービスで共通のチャットウィジェットや、広告ウィジェットの開発に適しています。

プロジェクトの開始

Svelte のプロジェクトの開始方法は、チュートリアルがあるのでこの手順で作成できます。

https://svelte.jp/docs#getting-started

npm create vite@latest myapp

対話的に質問されますので、適切に答えます。

Select a framework: Svelte
Select a variant: TypeScript

※ SvelteKit は今回使いませんでした。
単純な Web コンポーネントであれば、特に使わなくても良いと思います。

起動

npm run dev

カウンターが起動します。

Webコンポーネント化する

このカウンターを、Webコンポーネントとしてビルドをできるようにします。

実際の商用コードはカウンターとは違うものですが、今回は例としてカウンターのコンポーネントを使います。

Svelte コンポーネントを Web コンポーネントとして書き出すには、既に良い記事がいくつかあります。

SvelteではじめるWeb Components開発 - Qiita

src/main.ts の内容を下記の1行に修正

export * from './lib/Counter.svelte'

src/lib/Counter.svelte の最上部に1行追加

<svelte:options tag="my-counter" />

ルートディレクトリの index.html の内容を書きに修正

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte + TS</title>
</head>
<body>
<my-counter></my-counter>
<script type="module" src= "/src/main.ts"></script>
</body>
</html>

vite.config.ts を以下のように修正します。

import {defineConfig} from 'vite'
import {svelte} from '@sveltejs/vite-plugin-svelte'

// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: './src/main.ts',
name: 'my-counter',
fileName: (format) => `my-counter.${format}.js`,
formats: ['es'],
},
},
plugins: [svelte({
compilerOptions: {
customElement: true,
},
}),]
})

formats については、 ブラウザ用の JS を作るか、サーバ用の JS を作るかでいくつかの形式を指定できます。
デフォルトでは esumd で、umd はサーバサイドでの実行に使うものです。今回は、ブラウザ JS だけあれば良いので、es のみにしています。

独自のコンポーネントを開発する

今回は、開発したプロダクションコードの具体的な内容は割愛します。

開発サーバを実行

npm run dev

ビルド

動作が確認できたら、JS ファイルをビルドします。

npm run build

dist/my-counter.es.js が作成されます。

成果物のテスト

ビルド成果物を確認するには、プロジェクトのルートディレクトリに

<my-counter></my-counter>
<script type="module" src= "dist/my-counter.es.js"></script>

の内容を含む HTML を作り、ダブルクリックで起動すると良いと思います。

npm run preview というコマンドもありますが、dist ディレクトリ以下に index.html が必要で、
dist ディレクトリはビルドするたびに消えてしまうのでコンポーネント開発では使いづらそうです。

実際に他のサービスからコンポーネントを使う際も、上記のようなスクリプトタグで使うことができます。その際、 script タグに type="module" を忘れずに指定します。ビルドしたコードには、ファイル直下に $ といった短い名前の関数が作られます。そのため、type="module" なしでグローバル領域に読み込んだ場合、既存のスクリプトで jQuery 等を使っている場合に誤動作につながります。

なお、type="module" をつけた場合、そのスクリプトは CORS 判定の対象となりますので、サーバ側は access-control-allow-origin の HTTP ヘッダを必ず返す必要があります。

S3の設定

本番環境で使えるようにするにするため、ビルド成果物の JS を、S3 にデプロイして静的サイトホスティングの機能でホスティングします。

バケットの命名の注意

S3 のバケットを作る際は、バケット名をサービスを提供する URL のホスト名と完全に同じにする必要があります

CloudFront で、CloudFront Functions を使ってリクエストヘッダーを操作する場合、S3に伝わる Host ヘッダーは必ずリクエストしている Host ヘッダーとなり、 Functions 内で変更することは制限されておりできないためです。

静的ウェブサイトホスティングの設定

Hosting a static website using Amazon S3 - Amazon Simple Storage Service

S3 の「プロパティ」タブの一番下から設定します。

バケットポリシーの設定

「アクセス許可」タブ内にバケットポリシーの設定欄があります。

パブリックでの読み取りを許可するポリシーにします。

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "arn:aws:s3:::world-visitor.torico-corp.com/*"
}
]
}

CORSヘッダーの設定

「アクセス許可」タブ内の一番下に Cross-Origin Resource Sharing (CORS) の設定欄があります。

[
{
"AllowedHeaders": [
"Authorization"
],
"AllowedMethods": [
"GET",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]

ファイルのデプロイ

ビルド成果物を S3 にアップロードします。

接続国により2つのファイルを出し分ける設計とするため、今回のビルド成果物の他に、空の JS も別のファイル名でデプロイしておきます。

CloudFront の設定

オリジン

先程作成した、S3 の WebホスティングのURLをオリジンとします。

接続国に応じてリクエストヘッダーを付与する設定

CloudFront の「オリジンリクエストポリシー」という機能で、リクエスト国別のヘッダを付与することができます。

CloudFront の ポリシーページの、オリジンリクエストのカスタムポリシーで、「オリジンリクエストポリシーの作成」を選択し、追加するヘッダーを選べます。

Adding the CloudFront HTTP headers - Amazon CloudFront

CloudFront-Viewer-Country を追加すると、ヘッダーの値として2文字の国名コードが追加されます。「JP」等。大文字になります。

リクエストヘッダによってオリジンに要求するファイルのパスを変えるファンクション

付与したリクエストヘッダーを CloudFront Functions で判定し、リクエストパスを変化させます。
今回はこのようなスクリプトになりました。

なお、handle の引数の event の構造はこのページで紹介されています。

CloudFront Functions event structure - Amazon CloudFront

/*
接続元の国によってリクエストパスにプレフィックスをつけるファンクション
*/

/// 国コードを取得
function getCountryCode(event) {
return event.request.headers['cloudfront-viewer-country']
? event.request.headers['cloudfront-viewer-country'].value
: ''
}

/// 国コード(大文字)から判断するディレクトリプレフィックス
/// SG は初回は必要無いが、検証のために追加
function getDirectoryPrefix(countryCode) {
if(['TW', 'SG'].includes(countryCode)) {
return '/tw';
}
return ''
}

/// uri (パス) はルーティング対象か
// 検証用の index.html はルーティングしたくない。
function uriRootingRequired(uri) {
return ['/my-component.es.js'].includes(uri);
}

function handler(event) {
var countryCode = getCountryCode(event);
var directoryPrefix = getDirectoryPrefix(countryCode);
if (directoryPrefix && uriRootingRequired(event.request.uri)) {
event.request.uri = directoryPrefix + event.request.uri;
}
return event.request;
}

Google Tag Manager でロードする

今回作った Svelte のコンポーネントは、 Google Tag Manager からでも使うことができます。

ただし、Google Tag Manager で独自の HTML タグを書くとエラーとなって公開できないため、
document.createElement でタグを作成しています。

また、script タグに type="module" 忘れずに指定します。これをつけないとグローバル変数領域にロードされてしまうため、既存のスクリプトに悪影響があります。

そして、type="module" を入れた場合、なぜか「document.writeをサポートする」にチェックが入ってないとスクリプトが動作しないため、チェックを入れます。

Search