sugiken のメモ帳

知ったことを書いていく

嘘を付きました。Rep サ終しません。サービスの売り方。これから。

↑これ嘘です。
多くの人に読んでいただいたこの記事ですが、結局サービス終了せずに済むことになりました。

サービス終了を判断した理由はモチベーションが続かなかったことなのですが、Rep を売却できることになりました。
売却先は Surfride Technologies 株式会社さんです。立教大学 OB である金田さんが代表を努めており、Rep 終了のツイートを見て Twitter で DM を頂きました。

twitter.com

そこから話が進み売却という終え方をすることができました。

サービスを売却に際して思うこと

実はこれまでも2,3度売却について検討したことがありましたが、毎回条件が折り合いませんでした。
個人開発者としてサービス売却は夢のような話ですが、市場をどれだけ取っているか、売上がどれくらいかなどでバリューは決まります。これが Rep を終了しようと思った理由にもなりました。
正直 Rep はそんなに売上もないし、市場を取っているとはいえ立教大学だけでした。

ただ何よりも立教大学生のためにこれまで育ててきた Rep が継続できそうなことが嬉しいです。金田さんに本当に感謝です。

サービスの売り方

実は意外と簡単です。これまでの数回の経験から大体こんな感じ。

  • リソースの整理と資料作成(何を売るのか)
  • 価値を伝える(いくらの価値があると思っているのか。そのための資料作成)
  • 売却額の決定(売る側提示。買う側が吟味。)
  • 法務的なやり取り(質問表を買う側が作る。諸々権利や法的リスクがないか確認)
  • 契約書にサイン

売却を見据えていてもいなくても、個人開発者といえど法的なことはちゃんと気にしたほうが良い。
例えばサイトで使用する挿絵を友だちに作ってもらった場合その権利の所在ははっきりしておくべき。

これから

今は PokerNuts というアミューズメントカジノ領域でのサービスを開発・運営しています。今回は売上も上げていきたいです。
小さく始めて大きく育てる。やっていきます。

PokerNuts ポーカートーナメント検索 | PokerNuts

kentear.hatenablog.com

【新米がいろんなアプリを見る その3】

今回はこれ!

Planta: 植物を元気に育てる

Planta: 植物を元気に育てる

  • Strömming AB
  • ライフスタイル
  • 無料
apps.apple.com

観葉植物を初めて買ってみたので使ってみた

初回起動後

実際にはこれが1画面目ではなかった気がするが、とにかくホームにたどり着いた時にやるべきことが明確になっている。
とはいえ観葉植物管理アプリだから植物を追加するのは当たり前なので、最初のチュートリアルステップで追加するフローを設けても良いんじゃないかと思った。

そしてこれ↑がホーム画面なわけだが、デザインめっちゃ良い。現在の天気 = 空 を一番上に表示しているのも世界観を感じるデザイン。

プレミアム推し

このアプリ、あらゆるところでプレミアムプランを推してきてすごい。
これが最初のプレミアムプラン推し。写真を撮って認識できるなんてちょっとやりたくなる。

他にもホーム画面は当然のこと、下部の TabBar に「P」というのがあり、これは完全に1画面がプレミアムプランの紹介となっている。多分課金するとプレミアムプランユーザーにしか使えない機能の詰まった画面になるのだろう。

フローティングボタン

ホームにはフローティングボタンがあり、少し Android チックだ。

フローティングボタンってかなり多くのアクションをギュッとまとめて設置できるから結構良いなと思った。「場所の追加」や「植物の追加」はあまり普段使わないのではと思ったが、逆に使いたい時に「どこ!?」ってなるのを防ぐこともできそう。

お問い合わせ画面

そもそもこういう toC の趣味的アプリでちゃんとお問い合わせあるのってすごい(あるところは多いだろうがみんなすごい)。
Planta のお問い合わせは見たことのない海外の SaaS を使っているのだろうか。モーダル的な感じで表示されている。完全に webView にしちゃうよりも多少なり一貫した体験ができそうで良い。

以上!

iOS エンジニア的にはこのアプリ、この画像認識を除くと他に特筆するべきところがない..。画像認識も処理はサーバーでやっているだろうしね。

【新米がいろんなアプリを見る その2】stand.fm

なんとか2回目。先週はサボったのではなく、旅行行っていたからしょうがない。

今回見るアプリは stand.fm 。音声配信系サービスを日常使いはしていないけど、パートナーから存在を知ったので勉強がてらインストールした。

stand.fm

ログイン直後

Twitter とかでもある、フォローさせるやつ。きっと定着率が上がるのだろう。
スキップがあるのもありがたい。

最初のPush通知権限確認

目的がはっきりと書かれていて、わかりやすい。Push通知って最初はオフにしがちだもんね。

初めて配信をしようとすると

5ステップでチュートリアルが入る。
もっと実際の操作をしながらのインタラクティブチュートリアルがあるかと思ったが、それは少なめ。ただ、重要な機能についてはモーダルで説明が入ったのでやはり丁寧。

共有ボタン押下後

こういう一手間加えた共有画面、 toC だと多い印象。いっぱいシェアしてほしいからだろうが、実装大変そう

Live 画面(視聴側)

この UI どうやって作るんだろ?
背景をグラデーションは意外と簡単ぽい。各コメントとか操作部は Storyboard でも組めそうか。
なんとなく脳内で Storyboard が描けるようになってきた。

【新米がいろんなアプリを見る その1】what3words

一体何回まで続くか。。。
freee でも web エンジニアからモバイルエンジニアに転生したのに、"アプリ"っていうのに触れることが少ないので、意識的にいろんなアプリを触ってみるシリーズ
サクッと書いていくことで、頑張って続ける

what3words とは

what3wordsについて | what3words

世界中を3メートル四方に区切り、それぞれのマス目に固有の3つの単語の組み合わせを割り当てました。what3wordsは、正確な位置を検索しシェアする最も簡単な方法です。

地図好きの自分としてはこの時点で面白いが、実用性はないなーと思ってこれまで web もアプリも触ってこなかった。
基本は地図アプリだが、マス目をタップすると 3words が表示される。

f:id:kentear:20220403000004j:plain

音声認識

3words を話すと検索できる。とはいえ全世界を分割してるんで、似た 3words が結構あるらしく精度は100%ではない。ただ候補を分かりやすく出してくれる。

f:id:kentear:20220403000033j:plain

音声認識画面では以下のロゴが出てたので、ここのエンジンを使ってるっぽい。what3words はモビリティー向けの展開も考えているみたいだが、この企業もモビリティー向けの製品が強いらしい。なるほど。 www.cerence.com

シェア機能

リアルタイム位置共有とかではなく、単なるマス目の共有だった。

f:id:kentear:20220403000044j:plain

カメラ撮影

自分の現在地の 3words を写真に載っけて共有できる。
インスタみたいにいろんなタイプもある。使うかどうかは考えてはいけない。こういう世界観なのだろう。

f:id:kentear:20220403000110j:plain

こういうアプリ内での画像の編集ってどうやるんだろう。freee では触れない技術なので全然知らない。
軽く調べると UIGraphicsBeginImageContext とかで画像と文字を簡単に合成することはできるようだ。

機能を紹介する機能

機能を紹介する機能がメニューにあった。一番面白かった機能。
起動時にそんなに丁寧なチュートリアルがあるわけではないが、いくつかのニッチな機能を知ってもらうための紹介だろう。

f:id:kentear:20220403000123j:plain

リストになっていて、初めて開いた時にすでに実行したことのある機能はチェックマークがついていた。導線はともかく使った機能は local storage に保持しているのだろう。

f:id:kentear:20220403000115j:plain

クリックすると、簡単な機能紹介モーダルが開く、「今すぐ試す」をタップするとそれが使える画面に飛ぶ。親切。自分がアプリ作るならこれっぽいの実装したいと思った。

f:id:kentear:20220403000128j:plain

create-react-app をあまりいじらずに Sentry にだけ SourceMap を送る方法

タイトルみたいな方法が意外と調べても出てこなかったのでまとめる。多分みんなちゃんと webpack の設定を書くことで解決してるんだろうな

SourceMap について

SourceMap は Minify されたりして圧縮&難読化された JS ファイルを元のソースコードレベルまで戻すことのできる鍵みたいなものだ。なのでこれを本番環境には deploy したくない。ありがたいことに Chrome の dev tool を使うと SourceMap があるだけで簡単に元のソースコードを見ることができてしまうからだ。

しかし今回の目的である Sentry のようなエラー収集 SaaS の画面では、難読化されたコードではなくソースコードでエラーの箇所が示されたほうが絶対にわかりやすい。これを実現するには create-react-app に二手間くらい加えないといけなかった。

JSファイルとの関係

例えば create-react-app で build したいくつかの JS ファイルを見ると、最終行にコメントアウト.js.map というファイル名の path( sourceMappingURL ) が書かれている。その path にあるファイルがこのファイルの SourceMap だ。 SourceMap は設定にもよるが、JS ファイルと同じディレクトリに生成されているのではないだろうか。

create-react-app(CRA) における SourceMap

単に source map を生成したくない時のビルドコマンドは以下の様にすれば良い。

GENERATE_SOURCEMAP=false react-scripts build

こうすると、上述した bundle された JS ファイルの最終行にコメントはされないし、当然 SourceMap も生成されない。本番環境ではこの状態で deploy されるべきである。調べた限り SourceMap について CRA が用意しているオプションはこれだけだった。

これだと Build 時にしか生成できない SourceMap を Sentry に送るのは無理だ。(念の書いておくが、Buildごとに bundle のされ方が変わるので、SourceMap 有り無しで2回 build するのは意味がない)

Sentry と SourceMap

上述した通り、SourceMap を送信してあげると収集したエラーの発生箇所を分かりやすく表示してくれる。ここまで読めば分かると思うが、ユーザーのクライアントで bundle された JS の情報を受け取り、それを Sentry 側で予めアップロードしてくれている SourceMap で解読できるからだ。

Sentry にはどうやってアップロードするか

これにはいくつかの方法がある CLI ツールもあれば、色々なライブラリ、プラットフォーム向けのツールを Sentry が作ってくれている。

Uploading Source Maps for JavaScript | Sentry Documentation

今回は以下の webpack 向けの plugin を使う。 docs.sentry.io

Sentry の仕様

ここまで読むととある疑問が浮かぶだろう。

Sentry はどうやって bundle された JS ファイルと SourceMap を突合するのか

設定にもよるが、bundle された結果複数の JS ファイルが deploy されることは多々ある。当然 SourceMap はその JS ファルの数だけ生成される。通常 bundle された JS ファイルに書かれる sourceMappingURL コメントによって SourceMap を特定するが、今回の方法では sourceMappingURL は JS ファイルに含まないようにしている。

これは、build するときは sourceMappingURL ありで bundle するが、deploy 直前に shell script でそのコメントを消すことで解決している。

create-react-app のカスタマイズ

では本題だ。そもそも CRA のカスタマイズは rewire といって推奨されるものではない。なぜなら CRA のアップデートによってそのカスタマイズが正常に動かなくなったり、カスタマイズが CRA にどういう影響を与えるのかが未知数だからだ(調べればいいが)。なのでこれ自体は自己責任でおなしゃす。

使用するのはこちら github.com

プロジェクトルートに config-overrides.js というファイルを作り、そこに webpack の設定を書くことで CRA をカスタマイズできる。コマンドの実行は react-scripts build から react-app-rewired build になるので注意。

override の内容

そのまんま貼り付けます。

const SentryCliPlugin = require('@sentry/webpack-plugin');

let plugins = [];

// ref. https://tech.pepabo.com/2021/03/10/enabling-source-map-with-sentry-and-heroku/
if (process.env.NODE_ENV === 'production') {
  plugins.push(
    new SentryCliPlugin({
      authToken: process.env.SENTRY_AUTH_TOKEN,
      org: process.env.SENTRY_ORG,
      project: process.env.SENTRY_PROJECT,
      include: './build',
      urlPrefix: '~/',
    })
  );
}

module.exports = {
  devServer: function (configFunction) {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      return config;
    };
  },
  jest: function (config) {
    return config;
  },
  // webpack.configのカスタマイズ
  webpack: function (config, env) {
    config.output = {
      ...config.output,
      sourceMapFilename: '[name].[chunkhash].js.map',
    };
    config.plugins = [...config.plugins, ...plugins];
    return config;
  },
};

SentryCliPlugin によって build 時に Sentry に bundle された JS ファイルと SourceMap を送ってくれる。

  • include: build後のファイルたちのパス
  • urlPrefix: SourceMap の build 先ディレクトリからの相対パス(この設定は結構重要)。bundle された JS ファイルは build/static/js 以下に生成されるが、SourceMap は build 直下に生成されていた。なので ~/ としている。

sourceMappingURL を削除

bundle された JS に残っているコメントを削除するための shell script を用意する 参考↓ Sentry だけに Production 環境のソースマップを表示させる | grgr-dkrkのブログ

#!/bin/bash
find 'build' -type f \( -iname \*.js -o -iname \*.css \) -exec sed -i -E 's/\/(\/|\*)# sourceMappingURL=[^ ]*\.(js|css)\.map(\*\/)?//' {} +

package.json

その結果できた package.json はこんな感じ。 最後に *.js.map を自体を削除している(これも shell script ファイルにまとめてよかったな)。

  "scripts": {
    "start": "react-scripts start",
    "build": "export REACT_APP_VERSION=$(git rev-parse HEAD) && react-app-rewired build; yarn removeSourceMapRef; rm -rf ./build/*.js.map",
    "removeSourceMapRef": "bash removeSourceMapRef.sh",
  },

export REACT_APP_VERSION=$(git rev-parse HEAD) はクライアントで利用して Sentry の Release 機能に使っている。 Sentry では git commit hash を Release name とすることができる。クライアントでその commit hash を指定することで、「この Release version で発生した Issue」というのが分かるのである。

こんな感じで init ↓

Sentry.init({
  dsn: process.env.REACT_APP_SENTRY_DSN,
  integrations: [new Integrations.BrowserTracing()],
  release: process.env.REACT_APP_VERSION,
  tracesSampleRate: 1.0,
});

最後に

ここまでするなら webpack の設定をゼロから書いても一緒かなーと思いつつ、現時点ではいい感じに CRA の恩恵を受けつつ Sentry に SourceMap を送れている。

また新たなサービスを作っている話

先日サービスを終了した。この記事は本当に多くの方に見ていただいてありがたかった。 kentear.hatenablog.com

このサービスを潔く終了しようと思えた理由の一つに、「自分として代わりとなるサービスを作ってるから」というのがあった。これがタイトルにも書いている新たなサービス。 作ってるのはこれ↓

pokernuts.jp

PokerNuts というサービスで、アミューズメントカジノでポーカーを楽しむ人のためのサービス。今は全国のポーカールームの情報やそのトーナメント情報を掲載している。今後はポーカープレイヤーにとって欠かせないサービスにしていく。

開発を始めたのは2021年12月。ユーザーは後から増えると信じて爆速で開発した(ソフトウェアは最初はだいたい爆速だ)。

なぜこれを作ろうかと思ったかというと、友人から誘いを受けたからだ。彼はエンジニアではないため開発はできない。そこで声をかけてもらって確かにビジネス的にチャンスが大きいなと思ったので一緒にやっている。

これまでの個人開発と違うのは、営業がいるとユーザーをゴリッと取ってきてくれること。どんなに勝ちがある(と自分では思っている)サービスを作ってもユーザーがすぐには増えず、モチベーションが低下するなんてことはあるあるだ。特にこのサービスはポーカールームとの関係性構築が欠かせないので、相方の存在は欠かせない。

かくいう自分はポーカーをプレイしたことはない。ストレートに言うとギャンブルは嫌いだ。では動機はどこにあるのかというと、なんか稼げそうというのと、古くからの友人たちと自分たちでサービスを作れる楽しさだ。自分も含めてみんな本業があるため、そっちには絶対に影響が出ない範囲で活動している。エンジニアが OSS 活動をするのと変わらないくらいの。その少しサークルのような、でもみんなスキルがあるから成り立つ活動というのが楽しい。

"みんな"と書いているが、自分と発起人の相方以外にも2人、開発に参加してくれている。みんな古くからの友人たちだ。

ありがてー

5年半個人で運営してきたwebサービスを終了する

最後の1年はもはや誰のためにサーバー代を払っていたのか分からなかった。アナリティクスも見ない。新たな機能も追加しない。毎月 Conoha から数千円引き落とされる。

っていうポエム。

Rep のロゴ
Rep のロゴ。これは友人に千円で作ってもらった。

続きを読む

iOS エンジニアになろうと思って半年でやったこと

前提

kentear.hatenablog.com

この記事を書いたのが8月。ただ実際には2021年7月には freee 内で web エンジニアから iOS エンジニアになるべくチームを異動していた。それから半年ほど経ったので自分がやったことを備忘録がてら書いていく。

どんな環境

freee のモバイルチームは1つ。プロダクトは複数あるが、1つのチームで横断的に各プロダクトの開発をおこなっている。自分は一番大きなサービスである freee 会計をメインで担当することになった。freee 会計は一番歴史のあるプロダクトなだけあって、コードも古めのコードがあったり、アーキテクチャが定まっていない時に生まれたと思われるコードもある。
ここ最近 iOS12 のサポートを切ったばかりなので、SwiftUI も使っておらず(部分的に使ってたっけな)UIKit を使って Storyboard で UI を組み立てている。

やったこと

文法

ど定番だと思う。Kindle版と紙媒体の両方で買った。もっとも辞書的に使っていて、異動直後は常に手の届くところにおいていた。

AutoLayout

AutoLayout って最初の関門だよね。freee の図書館にあったので借りて読んだが、若干内容は古い気はした。結局そんなに読んではいない。

AutoLayout は登場人物が多い(UIKit, Constraints, その他設定項目)!!これらを業務の中で色んな開発をすることで少しずつ学んでいて、まだ全体像を完璧に理解した気はしない。 あまりに複雑なレイアウトを自分一人で実装できるかは自身がないが、freee が業務で扱うくらいのレイアウトならほぼ大丈夫になったのでok。

ReactiveSwift

freee 会計で採用している。最初は「概念はわかるんだけどコードが読めん」という感覚だったので苦戦した。

qiita.com

このシリーズ化された記事はほんと何度も読むことになった。
あとは MyTestApp を作って ReactiveSwift のいろいろなメソッドを使ってみたがまだ和解していない。

アーキテクチャ

プロダクトのアーキテクチャを見直す必要があり、自分がそのサブメンバーに選ばれた。iOSを0から作ったことないし経験の浅い自分でも「勉強がてらやりたいっす」って言ったらやらせてくれる会社ありがてー

peaks.cc

この本ほんとすごい。iOSアプリのアーキテクチャがメインなのは当然なのだが、そもそもアーキテクチャってのはあらゆるシステムの共通項をまとめて生まれるよねっていう考えから、web が生まれる少し前の GUI アーキテクチャの歴史とかも学べて純粋に読んでいて面白い。 若干同人誌らしいが、全然そんな気はしない。

SwiftUI

freee で今触ることはないがこれからの iOSGUI の主流になることはほぼ確定だ(よね?)。
2冊読んだ。ただどちらも無駄だなと思ったことはないが、サラッと読んだ時点で「これはさっさと手を動かしたほうがいいな」と気づかせてくれた。それだけ SwiftUI の敷居は低いと思う。

実践

会社で

細かくは書けないが、新機能の開発プロジェクトやバグ対応をした。他人のコードレビューもしているので、単なるコーディングはある程度できるようになった感覚。設計はまだ一人ではやったことはない。

プライベート

web.rep-rikkyo.com

これのアプリを作ろうと土日に開発している。Rep はいつだって僕の練習場所を提供してくれる。 SwiftUI や PromiseKit など、本業では触ることない技術に触れる機会でもある。PromiseKit を使用したことで非同期通信のコードの見通しは良い気がしている。少なくともリアクティブプログラミングよりかは自分は好きだ。

ファーストリリースは Read 系中心だが、最終的にアプリを強化していってユーザーの定着率を上げたい。

なお私用スマホAndroid

未だわからないこと

実装とはちょっと離れる設定とか

xcconfig とか未だに何なのかわからないし、Schema ファイル(?)とかも何なのかちゃんとはわかっていない。freee の業務ではここをがっつり触るということがないので仕方がない。

アプリのリリースの仕方

freee のアプリのリリースは社内のドキュメントとかを見たらわかる。
自分がわからないのは、0からリリースする方法。これは Rep のアプリをさっさとリリースして勉強していくが、きっと審査に落ちるんだろうなー。

最後に

備忘録がてらっていうけど、将来見返す気はしない。

最近読んだ頭よくなる本

結論から言うとこの本。書いてある内容は具体と抽象を行き来して思考しようといった話。この思考法について概念だけでなく身につけ方も書いてある。
今まで日常生活や仕事で何気なくしていることを改めて言葉で定義してくれた感覚だった。もちろん定義されることで、この思考を意識しやすくなった。

そもそも本の紹介なんてできないし、Amazon レビューや他のブロガーの書評とかを見た方がいいと思うのでここでは書かない(書けない)。
ぜひ手に取ってほしい。

ただ、買った時はソフトウェアエンジニア向けのアーキテクチャを考えるための思考法が紹介されていると思っていたのは内緒。

webエンジニアやめるー

ちょっと釣りタイトル。
この夏からモバイルエンジニアにジョブチェンジします。

これまで何してきたの

2019年から新卒として freee に入社し、それから丸2年 freee会計 という freee で最も歴史のあるプロダクトでwebエンジニアとして働いていました。自分はワークフロー(経費精算とか稟議書とか)という機能群の開発・運用をしてきました。

freee のエンジニアはだいたいみんな設計から実装まで自分たちでやります。すると自分の力でリリースした機能が我が子のよう思えてきて、Redash でどれだけ自分が作った機能が使われているかを毎日チームに共有したり、追加の便利機能をサクッと実装したり(サクッとできないこともしばしば)と愛着を持ってサービス開発ができます。

そんな freee には面白い制度があって、原則新卒エンジニアは配属から2年で異動します。流石にエンジニアからセールスになる人は見たことないですが、エンジニアからUI/UXデザイナーになる人もいるくらい自由度の高い異動制度です。サービスに愛着があるとはいえ、色んなことをやってみたい自分からするとめっちゃありがたい制度。今回自分はそのタイミングでモバイルチームに異動することにしました。

他の異動候補

  • 他のプロダクトのwebエンジニア
  • SRE
  • QA

などなど。自分の中では SRE とモバイルで悩みましたが、最終的には「モバイル9割くらいの気持ちです」と伝えました。

モバイルチームでどんなことするの

iOS チームでfreee会計の iOS アプリの開発をしていく予定です。Swift はまったく経験がないので、まずは iOS アプリの開発について学びながら働きます。
すでに1ヶ月くらいモバイルチームで働いていますが、締切がかなり先のタスクをもらってじっくり理解しながら、試行錯誤しながら実装しています。(正直これでお給料が減らないのも最高)

個人的には2年間関わったワークフロー周りにモバイルチームでも携わりたいかなー

なんでモバイルエンジニアになるの

恥ずかしいのでうすーく書いていきます。気持ちが薄いわけじゃない。

リベンジ

Swift がまったく経験がない

これは嘘。大学2年生の頃初めて mac を買った動機も友達と「時間割アプリ作りたくね」って宅飲みで話してからの勢いでした。ただ、当時 Swift が難しくて挫折しました。5年くらいたった今、満を持してのリベンジです。

技術の幅を広げたい

Swift を挫折したあと web エンジニアとしてこれまで5年間 web エンジニアをやっていました。せっかく異動できるのなら、ガラッとやることを変えたいなと思えるくらいには web をやってきたつもりです。

よりハードウェアよりで、クライアントとして徹しているアプリ開発ってどういう感じなんだろうっていうふんわりとした興味もあります。

器用貧乏

同時にマネージャーには器用貧乏になってしまう可能性もあるとフィードバックされました。確かにあまり技術力を深掘るのが得意じゃないのでちょっと心配。このあたりは30代に向けて考えていきたい。

web版で関わってたところを強化したい

これは個人の気持ちです ←大事

web版でワークフローという機能群に関わっていましたが、これをモバイルでも充実させたいなと思いました。愛着、大事。

自分でアプリ作れるようになりたい

  1. 自分がずっと運営している Rep立教大学シラバス検索システム のモバイルアプリを作ってみたい
  2. アプリ作りたい人は多い
  3. 将来起業するときに自分で作れるほうが良い

結構打算的。ただこれまで Rep を中心にプログラミングを学んできたので、また Rep に助けられるはず。

車好き

突拍子もないですが、車に関する技術でも OSSアプリ開発のプラットフォームが生まれてきています。それを自分で触ってみるのにアプリ開発のイロハを知っているといつか役立つかなと。
これまた超ふんわりだけど、いつか自動車メーカーで働いてみたい(適当)。