カテゴリー
役立ち情報
2023/06/01・役立ち情報
経緯
Google Search Index用にサイトマップを作りたくなった。サイトマップの生成自体はsitemapパッケージでどうにかなるが、いちいち全部のルートを取得してリストを作るのは面倒臭い。そこで、nitroに備わっているクローラーを使って、サイト内にあるリンクからルート一覧を生成してみる。
nitroのクローラーは、HTMLに含まれる、「/」で始まるhref属性を含むタグを自動的に認識し、リンク先のルートを生成できる。
壁
nitroでは、クローラーによって自動的に動的ルートのページが生成される。しかし、クロール後のルート一覧を取得する方法が存在しない。そこで、ページを生成するタイミングでフックを挟んで、変数のリストに追加していくことにする。
コード
まず、sitemapパッケージをインストールしておく。
yarn add sitemap
次に、nuxt.config.tsのdefineNuxtConfigの上に以下を追加する。
const hostname = "<hostname>"
const routes: string[] = []
export default defineNuxtConfig({
...
続いて、defineNuxtConfigの中に以下を追加する。
export default defineNuxtConfig({
...
nitro: {
hooks: {
"prerender:route"(route) {
routes.push(route.route)
},
close() {
if (routes.length > 0) {
const links: SitemapItemLoose[] = routes.map(route => ({
url: route,
}))
const stream = new SitemapStream({
hostname,
})
return streamToPromise(Readable.from(links).pipe(stream))
.then((sm) => {
return fs.writeFileSync("dist/sitemap.xml", sm.toString())
})
}
},
},
},
...
})
2023/03/22・役立ち情報
問題発生
IntelliJ IDEA (というか全Jetbrains IDEで共通) でGitのコミットを署名する設定を、Macでこれに従って設定したところ、以下のようなエラーが出てコミットできなくなった。
error: gpg failed to sign the data
fatal: failed to write commit object
原因
いろいろ調べた結果、パスワード入力用のダイアログの表示に失敗している様子。
GPGではセキュリティーのためか、pinentryというソフトでGUIのパスフレーズ入力ダイアログを出し、コンソールに直接パスフレーズを入力することを避けるようにしている。しかし、macOS版のpinentry-macをインストールすると、pinentryとpinentry-macという2種類のコマンドが実行できるようになる。そして、なんと pinentryの方はmacOSでは動かない。
デフォルトではこのpinentryの方が使用されるようになっているため、エラーが発生している様子。~/.gnupg/gpg-agent.confでpinentryのパスを変更できるらしいが、なぜか設定しても反映されない。(いろいろ試したものの…)
そこで、シンボリックリンクの作成という力技で対処することにする。
対処法
まず、現在設定されているpinentryのパス と、 pinentry-macのパス を調査する。
前者は以下のコマンドで調べられる。
% gpgconf
gpg:OpenPGP:/opt/homebrew/Cellar/gnupg/2.4.0/bin/gpg
...
pinentry:パスフレーズ入力:/opt/homebrew/opt/pinentry/bin/pinentry
上の実行結果でいうと、/opt/homebrew/opt/pinentry/bin/pinentryの部分が現在設定されているパス。そして、このファイルを削除する。
% rm <現在設定されているpinentryのパス>
次に、pinentry-macのパスを調べる。
% which pinentry-mac
/opt/homebrew/bin/pinentry-mac
最後に、以下のコマンドでシンボリックリンクを作成する。
% ln -s <現在設定されているpinentryのパス> <pinentry-macのパス>
これでコミットできるようになるはず。
2023/03/14・役立ち情報
問題発生
OGPのサムネイル生成用に、Puppeteerを使ったCloud Functionsの関数をデプロイしようとした。
開発環境(エミュレーター)では正常に動作するのに、なぜかデプロイするとError: could not handle the requestと表示されて動作しなくなる。ログを確認すると、以下のようなエラーが出ていた。
Error: Could not find Chromium (rev. 1095492). This can occur if either
1. you did not perform an installation before running the script (e.g. `npm install`) or
2. your cache path is incorrectly configured (which is: /www-data-home/.cache/puppeteer).
For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.
どうやら「Chromiumが見つからねぇよ」というエラーの様子。
結論
以下の手順で解決できる。
functionsディレクトリに.puppeteerrc.cjsファイルを作成し、以下のような中身にする。
/**
* @type {import('puppeteer').Configuration}
*/
module.exports = {
cacheDirectory: require("path").join(__dirname, ".cache", "puppeteer"),
}
firebase.jsonに以下を追記する。(※JSONではコメントの記述はできないため、//以下は削除してください)
"functions": [
{
"source": "functions",
"codebase": "frontend",
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log",
".cache" // <= 追記
],
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint",
"npm --prefix \"$RESOURCE_DIR\" run build"
]
}
],
package.jsonを以下のように変更する。(※JSONではコメントの記述はできないため、//以下は削除してください)
"scripts": {
"lint": "eslint --ext .js,.ts .",
...
"postinstall": "node node_modules/puppeteer/install.js" // <= 追記
},
...
"engines": {
"node": "16" // <= 16にする。詳細は下記。
}
解説
調べてみると、Puppeteerのv19で以下のような破壊的変更があった模様。
use ~/.cache/puppeteer for browser downloads
(https://github.com/puppeteer/puppeteer/releases/tag/v19.0.0)
ホームディレクトリはCloud Functionsのデプロイの対象にならないため、実際に実行される環境でChromiumが存在せず、エラーとなっているようです。
そのため、Puppeteerのインストール先をプロジェクト内に変更してやります。これが1.です。
ちなみに、この状態でnode_modulesを削除してnpm installし直すと、設定したパスに開発端末のOSに合ったChromiumがインストールされます。ここでfirebase deployしようとすると、「パッケージが大きすぎるためデプロイできません」のようなエラーが発生します。これは、Chromium本体がデプロイの対象となっているためで、除外してやる必要があります。これが2.です。まぁどちらにせよCloud Fuctionsの実行OSはLinuxなので、Linux以外を使って開発していると結局デプロイ時にLinux版がインストールし直されます。
あとは、Cloud Functionsではなぜかnpm install時にChromiumが自動的にインストールされないので、postinstallでインストールスクリプトを実行してやります。(3.)
また、Nodeの最新バージョンは18ですが、2023/03/14現在、Node 18用のイメージにPuppeteerの実行に必要なライブラリが含まれていないため、16にします。
2023/01/06・役立ち情報
経緯
GitHub連携を用いて、Cloudflare Pagesでページを公開し、一部のパスでFunctionが実行されるようにしたかったが、なぜかFunctionsだけデプロイされない。
公式ドキュメントの通り、プロジェクトルートにfunctionsディレクトリを作成し、その中にapi/showcase.tsを作り、example.com/api/showcaseでAPIが走るようにしたかった。しかし、そのままデプロイしてもNuxtの静的ページに飛び、Functionsに繋がらない。
対処法
pages functions not recognizedで検索したところ、https://github.com/cloudflare/wrangler2/issues/1859がヒットした。
結論としては、nuxt.config.tsに以下を追記する。
...
nitro: {
preset: "node-server",
},
...
https://github.com/cloudflare/wrangler2/issues/1859#issuecomment-1269616054によると、Nitroでは環境に応じて自動的に適切なプリセットが選択され、Cloudflare Pagesではcloudflare-pagesが設定されるようになります。本来はnuxt generate時に.output/publicに/functionsがコピーされるべきですが、Nuxt3の/serverとして扱われ、認識できるexportが存在しないことからレンダリング前に/functionsが空っぽにされ、結果全くデプロイされないようです。(私の認識が正しければ)
2022/11/19・役立ち情報
問題は起きた。
Jetbrains社のIDE、IntelliJ IDEA / WebStorm でNuxt3を使おうとすると、#importsからインポートしないとメソッドが解決されない。どうやら、VSCodeのプラグインはインポートしなくても解決できるようになっている模様。
これはめちゃくちゃ面倒臭い。
対処法
__imlファイルを編集し、.nuxtディレクトリを処理(インデックス)の対象に含めます__。通常、IntelliJ IDEAでは、「.(ドット)」から始まる隠しディレクトリはGUIでインデックスの対象に含めることができません。なので、プロジェクトの設定ファイルを直接編集します。
[プロジェクトルート]/.idea/[プロジェクト名].imlを開きます。そして、以下を追記します。
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<!-- 略 -->
<content url="file://$MODULE_DIR$/.nuxt" /> <!-- 追記 -->
<!-- 略 -->
</component>
</module>
こうすると、composableなどがimportなしで使えるようになります。
2022/11/03・役立ち情報
問題
タイトルの通り。
対処法
「カスタムVMオプションの編集」から、以下のオプションを追加してMetalを利用したレンダリングを無効化します。こうすることで、ファイルを更新する度に起こる点滅が1回に減ります(ゼロにはなりません)。これで暫定的に対処できます。
-Dsun.java2d.metal=false
追記 (2022/11/25)
IntelliJ IDEA 2022.2.4がリリースされました。リリースノートには修正した旨の記載がありますが、なぜか現在も治っていない模様です。
2022/10/29・役立ち情報
Flutterでネイティブとやり取りしたい時、一番最初に知るのはMethodChannelを使った方法だと思います。しかし、MethodChannelは直接使うと非常に使いづらいです。引数はおろか、メソッド名すらString型で渡す必要があり、IDEの入力支援も受けられず、タイプミスしたら実行するまでエラーが吐かれず、引数の個数や名前、型も全部自由で何でも渡せてしまいます。
これでは非常に開発効率が悪いです。ということで、Flutterの公式パッケージである Pigeon を使います。
使う準備
pubspec.yaml の dev_dependencies に以下を追記します。
dev_dependencies:
flutter_test:
sdk: flutter
...
pigeon: ^4.2.3
次に、Flutterとネイティブでやり取りするクラスを定義するファイルを作ります。プロジェクトルートからpigeons/messages.dartを作ります。そして、中身を以下のように記述します。
(Objective-CおよびJavaで出力することになります。SwiftやKotlinでも出力できますが、まだ実験段階なのでおすすめしません。Objective-CもJavaも、SwiftやKotlinから呼び出せるので特に問題はありません。)
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(PigeonOptions(
dartOut: "lib/messages.dart", // Dartファイルの生成先
objcOptions: ObjcOptions(
prefix: "FLT" // iOS用に生成されるObjective-Cのクラス名の接頭辞
),
objcHeaderOut: "ios/Runner/messages.h", // iOS用Objective-Cヘッダーの出力先
objcSourceOut: "ios/Runner/messages.m", // iOS用Objective-Cの出力先
javaOut: "android/app/src/main/java/com/example/pigeon/Messages.java", // Android用Javaの出力先 (※ com/example/pigeon の部分はパッケージ名に変更)
javaOptions: JavaOptions(
package: "com.example.pigeon" // Android用Javaのパッケージ名
)
))
これはPigeonの設定を記述しています。適宜変更してください。
インターフェースの作成
次に、やり取りするメソッドをまとめたクラスを作ります。このクラスがメソッドチャンネルになります。同じファイルの下に以下のように記述します。
@HostApi() // Flutter -> Native
abstract class ExampleApi {
///
/// ドキュメントは全プラットフォームに反映されます
///
void example();
void openUrl(String url);
StateResult queryState();
@async
String getToken(); // 非同期メソッド
}
enum State {
pending,
success,
error,
}
class StateResult {
String? errorMessage;
late State state;
}
@FlutterApi() // Native -> Flutter
abstract class Example2Api {
void handleUri(String uri);
}
サポートされているデータ型
Pigeon実行用シェルスクリプトの作成
Pigeonで生成処理を走らせるシェルスクリプトを作ります。プロジェクトルートにrun_pigeon.shという名前で以下のように書きます。
flutter pub run pigeon --input pigeons/messages.dart
ターミナルを開きます。シェルスクリプトに実行権限がない場合があるので与え、実行します。
% chmod u+x ./run_pigeon.sh
% ./run_pigeon.sh
何も表示されなければ完了です。
Flutter側の実装
Flutter側からネイティブを呼ぶ場合
ExampleApi().openUrl("https://example.com");
await ExampleApi().getToken();
ネイティブ側からFlutterを呼ぶ場合
lib/api/example_2_flutter_api.dartファイルに以下のように書きます。
class Example2FlutterApi implements Example2Api {
@override
void handleUri(String uri) {
// ...
}
}
lib/main.dartで初期化します。
void main() {
Example2Api.setup(Example2FlutterApi());
}
Android (Kotlin) 側の実装
私の場合はapiパッケージを配下に作って、その中にまとめました。
(ExampleAndroidApi.kt)
class ExampleAndroidApi(private val activity: Activity) : ExampleApi {
override fun openUrl(url: String) {
// ...
}
override fun getToken(result: Result<String>) { // async function
FirebaseMessaging.getInstance().token.addOnSuccessListener {
result.success(it)
}.addOnFailureListener {
result.error(it)
}
}
}
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
ExampleApi.setup(flutterEngine.dartExecutor.binaryMessenger, ExampleAndroidApi(this)) // 初期化
// ...
}
}
ネイティブ側から呼ぶ場合は以下のとおりです。
Example2Api(flutterEngine.dartExecutor.binaryMessenger).handleUri(uri) {}
iOS (Swift) 側の実装
まず、Runner-Bridging-Header.hに以下を追記します。これをしないとSwift側からObjCを呼べません。
#import "messages.h"
New Group without Folderでapiグループを作り、その中にまとめました。
(ExampleIOSApi.swift)
class ExampleIOSApi: NSObject, FLTUtilsApi {
func openUrlUrl(_ url: String, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) {
<#code#>
}
func getTokenWithCompletion(_ completion: @escaping (String?, FlutterError?) -> Void) {
completion("token", nil)
}
// または
// func token() async -> (String?, FlutterError?) {
// return ("token", nil)
// }
}
メソッド名がおかしい場合
と、この段階で違和感に気づく場合があると思います。メソッド名がなんかおかしいです。Objective-CからSwiftに変換する際にObjective-Cのセレクターを参照するため、それを適切に設定しないとおかしなことになります。そのため、@ObjCSelectorアノテーションでObj-Cのセレクターを設定します。
(pigeons/messages.dart)
@ObjCSelector("openUrl:")
void openUrl(String url);
先程のrun_pigeon.shを実行します。
(ios/Runner/ExampleIOSApi.swift)
func openUrl(_ url: String, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) {
<#code#>
}
治りました。
BooleanやInt、Longなどの型が返せない場合
これらの型を戻り値に設定するとNSNumberで受けるようになっているため、Swiftの型はそのままでは返せません。そのため、as NSNumberでキャストします。
completion(true as NSNumber, nil)
completion(50 as NSNumber, nil)
2022/10/22・役立ち情報
事象
FlutterでIntegration Test内でのスクリーンショット撮影が、iOSでのみ以下のようなエラーで失敗する。
Failure in method: screenshot
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞═════════════════
The following MissingPluginException was thrown running a test:
MissingPluginException(No implementation found for method
captureScreenshot on channel plugins.flutter.io/integration_test)
When the exception was thrown, this was the stack:
#0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:294:7)
<asynchronous suspension>
#1 IOCallbackManager.takeScreenshot (package:integration_test/_callback_io.dart:94:33)
<asynchronous suspension>
#2 IntegrationTestWidgetsFlutterBinding.takeScreenshot (package:integration_test/integration_test.dart:190:39)
<asynchronous suspension>
#3 takeScreenshot (file:///Users/chika/StudioProjects/submon/integration_test/screenshot_test.dart:57:3)
<asynchronous suspension>
#4 main.<anonymous closure> (file:///Users/chika/StudioProjects/submon/integration_test/screenshot_test.dart:26:5)
<asynchronous suspension>
#5 testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:171:15)
<asynchronous suspension>
#6 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:841:5)
<asynchronous suspension>
The test description was:
screenshot
═════════════════════════════════════════════════════════════════
これは以下のIssueでも議論されているが、未だにCloseされていない。
[integration_test] Plugin not registered unless running via XCTest #91668
解決方法
以下のページを参考にした。
Grabs screenshots from your Flutter app on multiple platforms (iOS and Android)
flutter(Flutter SDKのフォルダー)/packages/integration_test/ios/Classes/IntegrationTestPlugin.mを開き、registerWithRegistrarメソッドの内容を以下のように書き換える。
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
// No initialization happens here because of the way XCTest loads the testing
// bundles. Setup on static variables can be disregarded when a new static
// instance of IntegrationTestPlugin is allocated when the bundle is reloaded.
// See also: https://github.com/flutter/plugins/pull/2465
[[IntegrationTestPlugin instance] setupChannels:registrar.messenger]; // 追加
}
そのままビルドして実行すると、撮影が可能になる。
2022/10/20・役立ち情報
症状
GitHub ActionsでFlutterのiOSアプリをビルドしようとしたら、エラーが出てきて失敗する。どうやら使っているデータベースの Isar が関係ある模様。
Error (Xcode): Invalid record (Producer: 'LLVM14.0.6-rust-1.64.0-stable' Reader: 'LLVM APPLE_1_1300.0.29.30_0') for architecture arm64
原因と解決方法
Xcodeのバージョンがプロジェクトのバージョンと違う のが原因だった。あのエラー文でそんなことある???
XcodeのProject Formatでは「Xcode 14.0-compatible」に設定してあるのにも関わらず、ワークフローのYMLファイルには runs-on: macos-latest と記述していた。これは現在(2022/10/20)、macOS 11 Big Surのことを指す ため、macOS 12以上にしか対応していないXcode 14は使えない。-latestは、GitHub側が安定版だと判断した場合に付けられるらしい。
そのため、以下のようにすれば解決する。
runs-on: macos-12
2022/03/09・役立ち情報
皆さん、こんにちは。
今回は、Vercelを用いてreCAPTCHAを導入してBot対策する方法を解説していきます。Vercelを用いた方法の日本語記事はざっと探しても見当たらなかったので、記事を書いてみます。
以前投稿した記事は一部誤っている部分があったため、再投稿です。
下準備
まず、reCAPTCHA のコンソール画面から利用登録します。以下のページに飛んでください。
reCAPTCHA
すると、以下の画面が表示されるはずです。
ここでは、以下のように選択・入力してください。
・__ラベル__:任意の名前 (サイト名)
・__reCAPTCHA タイプ__:reCAPTCHA v3 を選択
・__ドメイン__:公開予定のドメインを入力
・「reCAPTCHA 利用条件に同意する」にチェックを入れる
送信後、「サイトキー」 と 「シークレットキー」 が表示されますが、これは後で使用しますのでページは閉じないでください。
実装
ここからは実装です。reCAPTCHA は、以下のような仕組みで成り立っています。
ユーザーがフォームの「送信」をクリックする
クライアント側で検証し、トークンを生成
トークンをサーバーに送り、検証し、結果を返却
結果が設定したスコアより高ければフォームの送信処理をする
reCAPTCHAでは、利用者がどれだけ人間だと思われるかのスコアが結果として帰ってきます。しきい値を上げればボットをたくさん弾けますし、低くすれば誤検知のリスクが下がります。
クライアント側
今回はNuxt.jsを用いて実装しますが、それぞれのフレームワークに適宜読み替えてください。
まず、スクリプトをロードします。以下をnuxt.config.jsに追記してください。
export default {
head() {
return {
script: [
{
src: "https://www.google.com/recaptcha/api.js?render=<SITE-KEY>"
}
]
}
}
}
<SITE-KEY>部分は先程のサイトキーに置き換えてください。
次に、送信ボタンのクリック時の処理を書きます。axiosパッケージをインストールしていない場合は、npm i axios もしくは yarn add axios でインストールしておいてください。
grecaptcha.ready(() => {
grecaptcha.execute("<SITE-KEY>", {action: 'submit'}).then((token) => {
axios.get("/api/checkRecaptcha?response=" + encodeURIComponent(token)).then(async (response) => {
if (response.data.success) {
// スコアは任意で変更
if (response.data.score >= 0.5) {
// 送信処理
}
}
})
})
})
同じく、<SITE-KEY>部分は先程のサイトキーに置き換えてください。
ここで、VercelのサーバーのAPIを叩いて、トークンを検証します。スコアのしきい値の目安は0.5くらいが良いと思います。試してみたところ、ボタンを1回だけ押すと0.9、数回連打すると0.7、何回も連打すると0.3、というように下がっていきました。
サーバー側
次は、Nuxt.jsのプロジェクトルートにapiフォルダーを作成し、その中にcheckRecaptcha.jsを作成します。中身は以下です。
const axios = require("axios").default
module.exports = (req, res) => {
const response = req.query.response
console.log("recaptcha response", response)
axios.post(`https://recaptcha.google.com/recaptcha/api/siteverify?secret=<SECRET-KEY>&response=${response}`).then(result => {
// console.log("recaptcha result", result)
if (result.data.success) {
res.send({success: true})
} else {
res.send({
success: false
})
}
}).catch(reason => {
console.log("Recaptcha request failure", reason)
res.send("Recaptcha request failed.")
})
}
<SECRET-KEY>部分は最初のシークレットキーに置き換えてください。
エミュレーションするには、yarn devではなく、Vercelのコマンドラインツールを利用します。yarn add vercelでインストールし、vercel devコマンドを実行します。初回は色々表示されるかもしれませんが、読めば分かるはずです。
これでVercelにデプロイすれば、reCAPTCHAで認証が可能になります。