yn2011's blog

技術メモ

(Android) Expo bare workflow で 環境別に複数の applicationId を使い分ける

公式ドキュメントのExample: configuring development and production variants in a bare projectを参考にやってみたのでメモ。

環境

  • Expo SDK 44
  • bare workflow

applicationId とは

アプリの識別子。

すべての Android アプリには、「com.example.myapp」など、Java パッケージ名に似た一意のアプリケーション ID があります。 https://developer.android.com/studio/build/application-id?hl=ja

Expo manage workflow の app.json に存在する package プロパティはこの applicationId を設定している。また、iOS アプリにも同様の識別子は存在する。

概要

Android アプリのビルドバリアントという仕組みを利用する。環境別(dev, stg, prd...) にビルドバリアントを作成しそれぞれに applicationId を定義する。

ビルドバリアントは、ビルドタイプとプロダクトフレーバーの組合わせである。ビルドタイプが debug/release 、プロダクトフレーバーが dev/prd が存在する場合はビルドバリアントは 4つ存在することになる(dev-debug, prd-debug, ...)

したがって、厳密にはビルドバリアントに applicationId を定義するのではなく、ビルドタイプとプロダクトフレーバーそれぞれに定義している(どちらか片方でもいい)

今回の環境別に異なる applicationId を使い分けるという目的を達成するためには、このプロダクトフレーバーを使う。

build.gradle にプロダクトフレーバーを追加する

Expo の managed workflow から eject すると、/android/app/build.gradle が生成される。このファイルに新しいプロダクトフレーバーを追加する(今回は例として、development と production)

 buildTypes {...} // 省略
 // 以下を追記
 flavorDimensions "env"
    productFlavors {
        production {
            dimension "env"
            applicationId 'com.example.myapp'
        }
        development {
            dimension "env"
            applicationId 'com.example.myapp.dev'
        }
    }

ビルドバリアントを指定してビルドする

expo run:android の場合

expo run:android コマンドは --variant オプションを受け取ることができるので、ビルドバリアントを指定する。

ビルドバリアントは、ビルドフレーバーが development, ビルドタイプが debug の場合は以下のように指定ができる(ビルドタイプは大文字から始めることに注意)

expo run:android --variant developmentDebug

これで指定したビルドバリアントの applicationId を持つアプリをビルドできた。

EAS build の場合

ローカル環境で Android のビルドを行うと環境面で躓くことが多いので、自分は EAS build することの方が多い。

EAS build の場合は、eas.jsongradleCommand というプロパティを使ってビルドバリアントの指定を行う。

コマンドの書式は、(assemble|bundle)FlavorBuildType となっている。 assemble を指定すると、ビルドファイルの拡張子が.apk になり、bundle の場合は .aab になった。この辺の技術的な違いはよく分からない。 ビルドフレーバーとビルドタイプを大文字から始めることに注意(Upper Camel)

{
  "cli": {
    "version": ">= 0.35.0"
  },
  "build": {
    "development": {
     ...
      "android": {
        "gradleCommand": ":app:assembleDevelopmentDebug" // dev
      }
    },
    "production": {
      "android": {
        "gradleCommand": ":app:bundleProductionRelease" // prd
      }
    }
  },
  "submit": {
    "production": {}
  }
}

あとは、通常通りにプロファイルを指定して EAS build を開始すれば良い。

eas build --profile development --platform android

ビルドバリアント毎にアプリケーションの名前も変えたい

ホームスクリーン上に表示されるアプリケーションの名前もビルドバリアント毎に変えることができる。

プロダクトフレーバー毎に新しくディレクトリとファイルを作成する。development の場合だと以下になる。

android/app/src/development/res/value/strings.xml

このファイルに以下のように名前定義をする。

<resources>
    <string name="app_name">(dev)app-name</string>
</resources>

参考

2021 年を振り返る

技術

2021 年はプロダクトのグロースや新規構築を通じてずっと Next.js に触れ続けていた年だった。

現職で自分が関わるプロダクトは、事業領域の特性的にも機能要件があまり難しくないことが多い。複雑な状態を持つ SPA を開発しているというよりは、静的ページの自動生成と、静的ページを React で効率よく書くためのフレームワークとして Next.js の SSG を利用しているに過ぎない。それらは公式のドキュメントや過去の別プロダクトのコードベースなどの資産を活用すれば開発自体は難しくはない。一方で toC な感じのデザインをシュッと実装できることが必要で、自分はいわゆる Web 制作の経験がない。ちょっとこれは不味いなということで個人的に色々勉強して、HTML/CSS の知識体系を確立しようとしていた。

単なる知識ではなく、”知識体系”と書いたのは、特に CSS については、イディオムの丸暗記ではなく、なぜこういう動きになるのか? CSS 全体を支配する規則の中でどう説明可能なのか?みたいなことを意識していたからである。まあ分からんことも多いのだけど。

直近では技術調査として Expo (React Native) をよくやっている。もしかすると 2022 年は React Native をやる年になるのかもしれない。

引っ越し

交通の便が良いので駒込に住んでいたが、昨今の時勢や会社の方針としても最低あと 1 年以上は在宅勤務が続きそうと思い郊外へ引っ越すことにした。

2020年に結婚してからも1人暮らし用の部屋に住んでいて手狭だったのと、隣に引っ越してきた外国人の方々が夜間に騒ぎまくるとか色々環境に限界を感じていて、しかも通勤時間を考慮する必要がないなら都内にいる理由がないよねって感じだった。

今は千葉県の柏近辺に住んでいる。都内と比較すると家賃相場は 3~4 万円程度安い印象があり、前より少し高い家賃を払うだけで(前よりは)けっこう広い部屋を借りることができている。

自分は地方出身(青森) だからかもしれないが、都心のコンクリートジャングルよりは空の広い郊外の方が性には合っている気がしていて、なんとなく近所を散歩していても落ち着く感じがある。人も少ないし静かだ。

デスク周り

デスクワークの疲れを軽減したかったので色々揃えてみた。

昇降式デスクを買ったり

MD770 を買ったり

アーロンチェアを買ったり

それなりに出費は嵩んだが、ウォーレン・バフェット氏も言うように自分の身体は交換不可能であり必要経費だと思っている。

読書

今年のいつ頃からか忘れたが、所属部署のシニアマネージャー(@primunu) の企画した読書会(毎日 9:30 ~ 10:00 )に参加させて頂いている。

こう書くと何人も参加しているような印象を受けると思うが、最終的には自分とシニアマネージャー氏だけの 1on1 的な会になっている。毎日と書いたがお互い欠席は自由な感じで週 3 回ぐらいは同時に参加してるかな?という緩い感じ(参加者が自分だけでも読書する)。各自好きに本を読んだり、自分は本を読みながらシェル芸をしたりする時間になっている。

自分は元々通勤時間で読書していたので、在宅勤務が始まった去年はあまり読書していなかったが、今年はこの会によって読書習慣を取り戻している。今年読んだ本の列挙は別記事でやるかもしれない。

今年 1番影響を受けたのはデジタル・ミニマリスト 本当に大切なことに集中する だと思う。これがきっかけで Twitter の利用時間を 1日 15分に制限したり、その他の SNS はアプリを削除したりして安らかな日々を送ることができている。多分。

ちなみに完全な趣味としては、なぜか司馬遼太郎泉鏡花をけっこう読んでいた。

髪が伸びた

外出自粛も叫ばれていた中で、長髪にすれば散髪に行かなくても良いんじゃないか?と思ったのと、何となく憧れがあり伸ばしてみている。しかし短髪だった人が伸ばす場合は放置で済むわけではなく、定期的に散髪で調整しないとマジで変な髪型になってしまう。結局当初の目的は達成できてないけど、まあ在宅勤務で人に会うことも少ないし長髪もありかと思ってまあそんな感じになっている。

筋トレ

通っていたパーソナルジムのトレーナーがお寺で修行をしたいので退職するという事件発生?をきっかけに、ジムを解約し自主トレに移行。金銭的な負担もまあまあ大きかったし、トレーニングや食事について多少は知識と経験が身についたと思う。契約前と比べると体重 +10kg 前後を達成できている。

引越し後から近所のジムを契約して週 1 回を目安に通っている。週 1 だとトレーニングする部位を絞らないと成果がでにくいのと筋トレしてる感が出やすいのが上半身かな、という感じで胸・肩のフリーウェイトをやっている。

平日夜のジムは混雑して嫌なので、18時前には行くようにしている。17時頃の夕会には運動着に着替えた状態で参加して、夕会が終わったら即退勤して家を出る。駒込の近くのジムや体育館は平日 17 時台でも人がそこそこいたりするが、近所のジムはほぼ人がいないので最高。こういう点でも(少なくとも自分にとっては)都内に居住する利点は少ないと感じる。

ギター

在宅勤務で時間に余裕ができたのと、プログラミング以外に趣味が欲しかったのでエレキギターを始めた。毎日 30 分~練習するようにしている。といっても家に居ない日とか時間がない日もあるので実績としては週 6 ぐらいかもしれない。

練習する上で特に目標とかマイルストーンは設定してない。自分の性格的に目標を設定して達成できないと萎えて止めそうだし、達成のために真剣になりすぎても継続できなさそうだしというのが理由。

練習は毎日30分と書いたが、正確には毎日ギターに触るという部分だけはちゃんと守ろうと意識していて、時間は別に飽きるまでで良いかって感じで運用している。経験上、「やりたいときにやる」と「最低 1分でも毎日やる」は似ているようで全然違うと思っていて、結局ギターさえ持てば 30 分ぐらいはやる生き物だと思うよ、人間は。

完全初心者な上に気軽に相談できる人もいないので当初はスクール通いも検討したが、 YouTube の初心者向け動画がめちゃくちゃ充実していることが分かり、人に会わなくても動画見て自分が実際にやるだけだなって感じがしたので本当に孤独にやっている。プログラミングもそうかもしれないけど、人に教わるというよりは、自分の中に積み上げていくしかない部分の比重が大きいスキルだと思っている。

もうあと何ヶ月かで 1 年ぐらいは練習していることになるが、特にアウトプットとかはなく、1曲通して弾けるものもない。5曲ぐらいの弾きたいフレーズだけを集めて繰り返し毎日チマチマやっている(1曲だと飽きるので)

まあ成長してる実感はあって日々楽しいは楽しいので 5, 10 年後とかにはちゃんと趣味ですって言えるようになれてると良いなぐらいな感じに思っている。

株式投資

前から興味があったのと、よく「エンジニアはビジネスを理解してない」みたいに言われている印象があって、「じゃあ理解してみるか」と思い、積立ではなくあえて個別株の投資(と学習)を始めた。これも詳細を書くと長くなるので別記事でやるかもしれないが、とりあえずできるようになった気がするのは

  • 財務三表の意味が分かる、読める
  • DCF ツールで自分なりに理論株価を算出する
  • 事業の参入障壁を分析する
  • 実際に市場で売買する

など。振り返ってみると結構な時間を株式投資の勉強・分析に使っていたと思う。まあ粗利と営業利益の区別も分からないような状態から始めたにしてはけっこう進歩したんじゃないか。

なお投資成績としては、現時点で含み損。キャリア的に業界が近いのでマザーズの情報通信セクターでポジションを持っているが、今年後半は流動性の低いマザーズ市場全体が嫌気され厳しい状態になっていてツラミ(そもそも日本株自体が対して良くはないんだが、マザーズは更に厳しい)

実際やってみた感想として、個別株の集中投資は究極的にはギャンブルで、どれだけ後付でなにかもっともらしいことを言ったとしても、その時点では人が合理的に予想不可能なことを当てたら勝てるって感じのゲームなのかなって思っている。

短期は市場の予測が不可能だし運なのは当たり前だとして、株価が企業価値に収斂すると言われている中長期(5年~)でやるとしてもギャンブルの色は強いと思ったし、長期でやるなら投資信託でも良くない?ってなった。

そもそも自分が中で働いたこともなければ、社員や経営者と会ったこともなく、使ったこともないようなサービスを売る歴史の浅い会社に対して、決算資料とかだけ見て信用して投資するってなんか根本的におかしくないか?それは投機=ギャンブルだよね。

まあ自分が負けているので批判的な感じになっているのも否めないが、今年である程度の勉強にはなったので、来年は新規に購入はせずに市場を見ながら適当なタイミングで換金しつつ何らかの投資信託へ移行する気がする。

総括

業務時間で自分の市場価値が上がるような学習や経験をさせてもらっているし、在宅勤務で通勤時間がなくなって時間が増えたので自分の好きなことを沢山できた年だった。めでたい。

Expo のビルドで発生する Unable to find a specification for `UMCore` depended upon by `EX**` について

この記事は React Native Advent Calendar 2021 の10 日目の記事です。

Expo 歴 1 ヶ月ぐらいなので記述に間違いがあるかもしれないですが、同じエラーに悩んでいる方向けのヒントになればと思い公開。

環境

  • expo 42 -> 43 に更新する際に発生
  • managed work flow

事象

iOS 向けに Expo Build を実行し、expo.dev の Logs を確認すると、Install Pods で Unable to find a specification for UMCore depended upon by EXScreenOrientation というエラーが出てビルドに失敗した。

解決

Expo SDK 43 から、react-native-unimodules が 非推奨になり、expo-modules-coreexpo-modules-autolinking を利用するようになった。

react-native-unimodules に依存しているパッケージ (このエラーの例だと、expo-screen-orientation) が存在する場合に、このエラーが発生するようだ。

流石にこれらのパッケージを 1つずつ確認して更新や、対応していない場合は削除等の対応を行うのは大変なので、移行用ツールが提供されているようだ。

Migrating to Expo modules

$ expo upgrade 43

Your git working tree is clean
To revert the changes after this command completes, you can run the following:
  git clean --force && git reset --hard
✔ You are already using the latest SDK version. Do you want to run the update anyways? This may be useful to ensure that all of your packages are set to the correct version. … yes

✔ Validated configuration.
✔ No additional changes necessary to app config.

✔ Updated known packages to compatible versions.
✔ Cleared packager cache.
Failed to query all project files. Skipping `.expo.*` extension check...
✔ Validated project

👏 Automated upgrade steps complete.
...but this doesn't mean everything is done yet!

✅ The following packages were updated:
@react-native-async-storage/async-storage, react-native-safe-area-context, react-native-reanimated, react-native-gesture-handler, expo-updates, react-native-unimodules, expo-status-bar, react-native-svg, expo-linking, react-native-screens, expo-splash-screen, react-native-pager-view, expo-app-loading, expo-dev-client, react-native, react, react-dom, typescript, @babel/core, @types/react, react-native-web, @types/react-native, expo

🚨 The following packages were not updated. You should check the READMEs for those repositories to determine what version is compatible with your new set of packages:
@expo/html-elements, @expo/match-media, @react-navigation/bottom-tabs, @react-navigation/drawer, @react-navigation/material-top-tabs, @react-navigation/native, @react-navigation/stack, @types/react-native-snap-carousel, native-base, react-helmet-async, react-native-responsive-screen, react-native-safe-area-view, react-native-snap-carousel, react-native-tab-view, react-responsive, recoil, recoil-persist, styled-components, styled-system, use-media, @react-navigation/devtools, @typescript-eslint/eslint-plugin, @typescript-eslint/parser, @welldone-software/why-did-you-render, babel-loader, deepmerge, eslint, eslint-config-prettier, eslint-plugin-prettier, eslint-plugin-react, eslint-plugin-react-hooks, prettier, react-devtools, react-native-flipper

⬆️  To finish your react-native upgrade, update your native projects as outlined here:
  https://react-native-community.github.io/upgrade-helper/?from=0.63.4&to=0.64.3

🏗  Run pod install in your iOS directory and then re-build your native projects to compile the updated dependencies.

Please refer to the release notes for information on any further required steps to update and information about breaking changes:
https://blog.expo.dev/expo-sdk-43-aa9b3c7d5541

このように、expo upgrade 43 することで、自動的に SDK 43 対応のパッケージに更新してくれる。ただし、自動更新の対象になっていないパッケージもある(The following packages were not updated.)

これらについては、EAS Build を行ってエラーが出る度に 1つずつ対応していくか、node_modules 内をUMCoregrep してエラーになりそうなパッケージを探していくかする必要がある。

自分の場合は、grep@expo/match-media のコードがマッチしたためパッケージを確認したが特に対応されていなそうだったのと、現時点で利用していなかったのでパッケージ自体を削除した。なお、今確認すると SDK 43 対応の PR はマージされているが、npm に対して publish はされていなさそう。v0.2.0 がどの時点のコミットなのかも分からなかった。

まとめ

以上の対応を行うことで、Unable to find a specification for UMCore depended upon by EX** のエラーは発生しなくなった。

おすすめの .gitconfig 設定

おすすめの .gitconfig 設定、といってもそんなにマニアックなものでもないが何も設定してないという方向けに参考になれば。

 [alias]
         co = checkout
 [push]
         default = current
 [core]
         ignorecase = true
 [fetch]
         prune = true

[alias] co = checkout

開発業務をしていると、1日に何度も git checkout する。ブランチを切り替える、ブランチを作成する、ファイルの編集内容を破棄する等何かと使う。

しかし、よく使うコマンドの割には checkout の文字数は長いので、git co にしている。

[push] default = current

ローカルで新規に作成したブランチを push する際に、git push origin feature/xxx というコマンドを打つ必要がなくなる。origin 以外を指定したいとき、別のブランチを指定したいことがないのでこれにしている。

[core] ignorecase = true

適宜変更はするが、macOS に合わせている。

[fetch] prune = true

git fetch と同時に remote に存在せず local に存在するブランチを削除する。

  • ローカルにブランチが蓄積されていくデメリット
  • 間違って remote ブランチだけ削除してしまってもローカルから復旧可能なメリット

を天秤にかけて、自動的にブランチが整理されていく方を取った。ローカルから復旧できないことが致命傷になることもあるとは思うがチーム開発ならメンバー全員が削除してない限りは何とかなるでしょ多分。(他力本願)

Android バージョン毎の WebView と Chrome の対応表

自分用に整理していたが、けっこう有用かもしれないので公開する。

バージョン Name WebView更新*1 WebView 依存アプリ*2 Chrome バージョン*3 リリース日
4.1-4.3 KitKat 不可 - Chrome ベースではない 2012年7月9日
4.4 KitKat 不可 - Chrome Android version 30 2013年10月31日
4.4.3 KitKat 不可 - Chrome Android version 33 -
5 Lollipop 可能 Android System WebView 最新を利用可能 2014年11月12日
6 Marshmallow 可能 Android System WebView 最新を利用可能 2015年10月5日
7 Nougat 可能 Chrome 最新を利用可能 2016年8月22日
省略 - - - - -
12 - 可能 Chrome 最新を利用可能 2021年10月4日

要約

  • Android バージョン 4 の WebView に対応することは、Chrome 33 以下に対応することを意味するので注意しよう
  • Android バージョン 5~6 と 7 以降では、WebView のブラウザ機能を更新可能ではあるが、依存する(更新する)アプリケーションが違うので注意しよう

Android Emulator について

参考

*1:JavaScript エンジン、レンダリングエンジン等のブラウザ機能を更新することが可能かどうか

*2:JavaScript エンジン、レンダリングエンジン等のブラウザ機能を更新するために何のアプリを更新すればいいか

*3:WebView で提供可能なブラウザ機能がどのChrome バージョン相当か

自分が購読しているテックブログ一覧2021

自分がどうやって技術系のインプットを行っているかについて書く。

いつ

休日。平日はほぼ見ない(週に1~2 時間でインプットが完了するように量を調整している)

どうやって

InoreaderRSS を購読。

何を

大まかに分けると3つ

  • 企業のテックブログ
  • 技術(OSSSaaS) のメディア(一次ソース)
  • ほぼ確実に参考になる個人ブログ(blog.jxck.io, JSer.info 等)

企業のテックブログを読む理由は一言で言うと自分の業務に役立つから。

など、書籍からは得にくい”現実の(主に日本の) IT 企業" の現場の動きを観測している。

加えて、自分が所属企業でブログを書く際に

  • どういうことをテーマにしたら需要がありそうか
  • 同じテーマで既に公開されている記事がどんな内容か

などを知っている方が良い記事が書けるので日常的にテックブログを読んでおくのは役に立つ。

やらないこと

逆に、デイリーのニュースフロー的なものとか、ポエムっぽいものとかは自分の中で優先度が低いので読まない。(はてなのテクノロジーカテゴリ、GitHub トレンド等)

一覧

Inoreader からエクスポートした。

title url
LIVESENSE ENGINEER BLOG https://made.livesense.co.jp/
Pulp Note https://pulpxstyle.com/
microCMSブログ https://blog.microcms.io/feed.xml
メルカリエンジニアリングブログ https://engineering.mercari.com/blog/feed.xml
CodeGrid - フロントエンドに関わる人々のガイド https://www.codegrid.net/
TypeScript https://devblogs.microsoft.com/typescript
Overreacted https://overreacted.io/
Next.js Blog https://nextjs.org/
ZOZO Technologies TECH BLOG https://techblog.zozo.com/
BASE開発チームブログ https://devblog.thebase.in/
freee Developers Blog https://developers.freee.co.jp/
blog.jxck.io https://blog.jxck.io/
ECMAScript Daily https://ecmascript-daily.github.io/
メドピア開発者ブログ http://tech.medpeer.co.jp/
Ahmad Shadeed Blog http://ishadeed.com/
DeNA Engineers' Blog https://engineer.dena.com/
JSer.info https://jser.info/
食べログ フロントエンドエンジニアブログ https://note.com/tabelog_frontend
LINE ENGINEERING https://engineering.linecorp.com/ja/
SmartHR Tech Blog https://tech.smarthr.jp/
クックパッド開発者ブログ http://techlife.cookpad.com/
Goodpatch Blog https://goodpatch.com/blog
Google Developers Japan http://developers-jp.googleblog.com/
リクルートテクノロジーズ メンバーズブログ https://recruit-tech.co.jp/blog
Yahoo! JAPAN Tech Blog https://techblog.yahoo.co.jp/
Hatena Developer Blog http://developer.hatenastaff.com/
CyberAgent Developers Blog | サイバーエージェント デベロッパーズブログ https://developers.cyberagent.co.jp/blog
Social Change! https://kuranuki.sonicgarden.jp/
リクルートライフスタイル RECRUIT LIFESTYLE http://engineer.recruit-lifestyle.co.jp/techblog
GitHub Engineering http://githubengineering.com/

おまけ

Inoreader は フィードを xml でエクスポート/インポートできる。上記の表は、エクスポートした xmljson に変換してシェルで加工して作成した。 以下のコマンドを実行すると表のヘッダーより下の部分を生成できる(ファイル j には 変換した json を用意)

$ paste -d '|' <(cat j | jq -r '.opml.body.outline[]."-text"') <(cat j | jq -r '.opml.body.outline[]."-htmlUrl"') | sed -e 's/.*/|&|/g'

|LIVESENSE ENGINEER BLOG|https://made.livesense.co.jp/|
|Pulp Note|https://pulpxstyle.com/|
...

今回 Inoreader から エクスポートした xml は gist で公開しているので、多分インポートに使えるはず(試してはない)

https://gist.github.com/pokuwagata/eae3f30e2539f1f0de7e54615caaafd6

img タグからのリクエストは Origin ヘッダーを持たないのでレスポンスの Access-Control-Allow-Origin を満たしていなくても CORS 違反にならないらしいという話

歴史的経緯により img タグからのリクエストは(crossorigin 属性を付与しない限り)クロスオリジンの場合でも preflight リクエストは飛ばない。これを simple request と呼ぶことは知っていた。

しかし、画像のレスポンスヘッダーに例えばAccess-Control-Allow-Origin: https://hoge.com が含まれる場合に、hoge.com 以外から img タグを利用してリクエストを送ると CORS 違反になるんじゃないかと思っていた。

しかし実際に試してみたら、そのような画像に対して、img タグからリクエストを発行すると正常に画像を取得することができることが分かった。

例えば、localhost で以下のコードを動かすと画像が正常に表示される。 はてブ数を表示する画像 URL はレスポンスに Access-Control-Allow-Origin: https://b.hatena.ne.jp を含むので例として使用している。

コード例1 (画像を取得できる)

<img src="https://b.st-hatena.com/images/users/gif/normal/00001.gif"> // OK

コード例2 (これは駄目)

fetch("https://b.st-hatena.com/images/users/gif/normal/00001.gif"); // CORS エラー

コード例3 (crossorigin 属性を付与すると駄目)

<img src="https://b.st-hatena.com/images/users/gif/normal/00001.gif" crossorigin="anonymous"> // CORS エラー

Chrome の Network タブを見てみると、コード1 の場合はリクエストヘッダーに Origin が含まれていないが、コード2,3 の場合は含まれている。

これは、MDN の Origin に関する記述から引用すると

There are some exceptions to the above rules; for example if a cross-origin GET or HEAD request is made in no-cors mode the Origin header will not be added.

ブラウザは、Origin ヘッダーを基本的には付与するものの、いくつもの例外が存在し、no-cors モードのリクエストには付与しないという。

no-cors モードは、すなわち img タグが(crossorigin 属性を付与しない限り)クロスオリジンでリクエストを行う場合に該当する。 Origin ヘッダーがなければ、たとえレスポンスに Access-Control-Allow-Origin ヘッダーが含まれていても許可されている Origin なのか判定できないから CORS 違反にはならないということだろうか?

ちなみに、fetch を利用して img タグのリクエストを再現すると以下になり、この場合は CORS 違反とはならない。

コード4

fetch("https://b.st-hatena.com/images/users/gif/normal/00001.gif", {
  mode: "no-cors",
});

mode: no-cors の意味がやっと分かった気がする。

ちなみに、img タグを使われると画像配信側はクロスオリジンリクエストでレスポンスを必ず読み取られてしまうのか、というとそんなことはない。

Origin 解体新書の Origin をまたぐその他の仕様 にも記載があるが、サーバー側でリクエストのヘッダーに Origin が含まれているのか判定し、403 エラーを返す実装にすることで実現できる。

こうすると、コード1 のように crossorigin 属性が未付与の場合は Origin ヘッダーが含まれないので 403 エラーになるし、コード3 のように Origin ヘッダーを含めても許可されていない Origin は Access-Control-Allow-Origin に許容すべきオリジンを列挙してレスポンスに含めることでブラウザはレスポンスを読み取れない。

参考

https://stackoverflow.com/questions/47978252/how-img-tag-gets-content-over-cors-headers