AWDwR "Chapter19 Action Mailer"

目標、年内読了!Chapter19行きますよ。

Sending E-mail

まずはメールを送信する設定をしなきゃね。development, testing, productionで同じ設定を使う場合はconfig/environment.rbに設定し、別々に設定したい場合はconfig/environments以下の設定ファイルに設定。

最初に設定しなきゃいけない項目は↓ :testはテスト用でメールは送信しない。:sendmailはローカルの/usr/sbin/sendmailでメールを送信する。:smtpSMTPでメールを送信する

ActionMailer::Base.delivery_method = :smtp | :sendmail | :test

:smtpを選んだ場合はさらにいくつかパラメタを設定する必要がある。

ActionMailer::Base.server_setting = {
  :address => "smtp.foo.com",  # SMTPサーバのホスト名。デフォルトはlocalhost。
  :port => 25,  # SMTPサーバのポート番号。デフォルトは25。
  :domain => "bar.com",  # 自分のドメイン名。
  :authentication => :login,  # :plain, :login, :cram_md5のどれかをとる。認証が不要であれば省略できる。
  :user_name => "dave",  # 認証時に使用するユーザ名。
  :password => "password",  # 認証時に使用するパスワード。
}

その他の設定オプションとしては以下のものがある。

ActionMailer::Base.perform_deliveries = true | false  # falseにするとメールを送信しない。
ActionMailer::Base.raise_delivery_errors = true | false  # trueにするとメール送信時にエラーが発生すると例外を上げる。
ActionMailer::Base.default_charset = "utf-8"  # メールで使用するキャラクタセット。


設定が終わったらいよいよメールの送信。Railsはジェネレータスクリプトでmailersを作ってくれるけどそんなことで驚いてちゃいけない。mailersはなんとapp/modelsの中のクラスとして作成される。

ruby script/generate mailer OrderMailer confirm sent

こうするとOrderMailerというクラスがapp/modelsにでき、2つのテンプレートファイルがapp/views/order_mailerにできる。

mailerクラスの各メソッドはインスタンス変数を設定することでメール送信の設定をする。

class OrderMailer < ActionMailer::Base
  def confirm(sent_at = Time.now)
    @subject = 'subject'  # サブジェクト
    @recipients = 'foo@bar.com'  # 受取人
    @cc = ''  # Cc:
    @bcc = ''  # Bcc:
    @from = 'bar@foo.com'  # From:
    @sent_on = sent_at  # Date: 省略時は現在時刻
    @charset = ''  # Content-Type: 省略時はserver_settingのdefault_charset、それもない場合はutf-8
    @headers = {}  # ヘッダのハッシュ
    @body = {}  # テンプレートに渡すハッシュ。
  end

  def sent(sent_at = Time.now)
    ...
  end
end


テンプレートファイルはフツーのERbのrhtmlファイルなので<%= %>なんかがそのまま使える。ただしrender()を使うときはパスに./で始まるパスを指定すること。@bodyに設定するハッシュはテンプレートからはインスタンス変数として扱える。具体的には@body["order"]とするとテンプレートからは@orderというインスタンス変数として見えるよ。


で、実際には今定義したメソッドを使うんじゃなくて、クラスメソッドのcreate_xxx(), deliver_xxx()を使う。xxxにはインスタンスメソッドの名前が入る。
deliver_xxx()は実際にメールを送信する。例えばconfirm()で定義したメールを送信する場合はこうなる。

OrderMailer.deliver_confirm(order)

一方、create_xxx()はメールの内容をTMail::Mailクラスのインスタンスとして返すだけで送信は行なわない。

class TestController < ApplicationController
  def create_order
    order = Order.find_by_name("Dave Thomas")
    email = OrderMailer.create_confirm(order)
    render(:text => "<pre>" + email.encoded + "</pre>") # encoded()はメールのテキストを返すメソッド
  end
end

HTMLメールを送信する場合はテンプレートでHTMLを生成し、set_content_type("text/html")でコンテントタイプをセットすればOK。

Receiving E-mail

Action Mailerを使えば受信したメールを処理するのも簡単。でもサーバからメールを取り出してアプリケーションに渡す処理がちょっと面倒かも。

メールを受信する場合はActionMailerクラスの中でreceive()メソッドを作成する。receive()メソッドは受信されたメールに相当するTMail::Mailオブジェクトを引数としてとる。

class IncomingTicketHandler < ActionMailer::Base
  def receive(email)
    ticket = Ticket.new
    ticket.from = email.from[0]
    ticket.report = email.body
    ticket.save
  end
end

で、問題は受信したメールをどうやって横取りしてこのreceive()メソッドに渡すか。
メールサーバの設定を変更する権限があればメール受信時にスクリプトを走らせて横取りするという方法があるけど、権限がない場合は.procmailrcにルールを書いてメールを横取りする方法もある。次に横取りしたメールをどうやってアプリケーションに渡すかなんだけどRailsのrunnerという機能を使って以下のようにできるよ。

ruby script/runner "IncomingTicketHandler.receive(STDIN.read)"

receive()クラスメソッドにメールのテキストを渡すと、パースしてTMail::Mailオブジェクトを生成し、receive()インスタンスメソッドに渡してくれる。

Testing E-mail

メールのテストにはユニットテストと機能レベルのテストがあるよ。

生成されるメールの内容を確認するユニットテストの場合は、さっきのジェネレータスクリプトを使うとtest/unit/order_mailer_test.rbというファイルが作成されるのでそれを使う。
テストで比較するメールの内容はtest/fixtures/order_mailer/以下に用意しておけば、read_fixture()メソッドで読み込んで、生成されたメールと比較してくれるんだけれども、一字一句比較する必要がなければ動的に生成される部分だけを比較するという方法もある。

class OrderMailerTest < Test::Unit::TestCase
  def test_confirm
    response = OrderMailer.create_confirm(@order)
    assert_equal("Subject", response.subject)
    assert_equal("bar@foo.com", response.to[0])
    assert_match(/Hello/, response.body)
  end
end

一方、メールがきちんと送信されるかどうかを確認する機能テストは、test環境の場合はAction Mailerはメールを送信せずActionMailer::Base.deliveriesという配列にメールの内容をセットするので、それを確認するようにすればよい。

class OrderControllerTest < Test::Unit::TestCase
  fictures :orders
  def setup
    @controller = OrderController.new
    @emails = ActionMailer::Base.deliveries
    @emails.clear
  end

  def test_confirm
    get(:confirm, :id => @dave_order.id)
    assert_equals(1, @emails.size)
    email = @emails.first
    assert_equal("Subject", response.subject)
    assert_equal("bar@foo.com", response.to[0])
    assert_match(/Hello/, response.body)
  end
end

機能テストを実行するにはrake test_functionalとするか、以下のスクリプトを実行すればOK。

ruby test/functional/order_controller_test.rb

AWDwR "Chapter18 The Web, V2.0"

今、巷で話題のWeb2.0ですよ!!

Intorducing AJAX

は、みんな知ってるからいいよね:-)。

The Rails Way

Railsには最初からAJAXのサポートが組み込まれているからWeb2.0もバッチリ。で、具体的に言うと

  • prototype, effects, dragdrop, controlsのJavaScriptのライブラリが組み込まれている。
  • JavascriptHelperというモジュールがある。


RailsAJAXを使う場合は.rhtmlファイルのheadセクションで

<%= javascript_include_tag "prototype" %>

としておきましょう。レイアウトファイルのapplication.rhtmlに書いておけば全ページで使えるよ。

で、AJAXを使うための基本形はこんな感じ。

<%= link_to_remote("Do the Ajax thing", :update => 'mydiv', :url => {:action => :say_hello}) %>
<div id="mydiv">This text will be changed</div>

このリンクをクリックするとサーバから返される文字列でdiv内の文字列を置き換える。div内を置き換える文字列にはレイアウトを適用したくない場合がよくあると思うけど、その場合はrender()の:layoutオプションにfalseを設定するか、アクションの先頭でlayout nilとしておきましょう。

POSTメソッドでAJAXのリクエストをしたい場合はこんな感じ。

<%= form_remote_tag(:update => "update_div", :url => {:action => :guess}) %>

一方、フォームの値が変更されたときに呼び出されるオブザーバというメソッドがあって、POSTされたデータはrequest.raw_postで参照可能。

<%= observe_field(:search, :frequency => 0.5, :update => :results, :url => {:action => :search}) %>

また、一定周期でAJAXのリクエストをするということもできる。

<%= periodically_call_remote(:update => "process-list", :url => {:action => :ps}, :frequency => 2) %>

The User Interface, Revisited

prototype.jsにはいろいろな機能があるので紹介するよ。まずはDOMマニピュレーション。

  • $(id) 指定されたidのDOMエレメントを返す。
  • Element.toggle(element, ...) 指定されたエレメントを表示するかどうかを切り替える。
  • Element.show(element, ...) 指定されたエレメントを表示する。
  • Element.hide(element, ...) 指定されたエレメントを非表示にする。
  • Element.remove(element, ...) 指定されたエレメントをDOMから取り除く。

次に視覚効果。effects.jsが視覚効果のライブラリ。
1回きりの視覚効果にはこんなんがあって…

  • Effect.Appear(element) 徐々に現れる。
  • Effect.Fade(element) 徐々に消える。
  • Effect.Highlight(element) Yellow Fade Techniqueを適用する。
  • Effect.Puff(element) 煙の中に消える。
  • Effect.Squish(element) 小さくなって消える。

何回も使える視覚効果にはこんなんがある。

  • Effect.Scale(element, percent) 拡大、縮小する。
  • Element.setContentZoom(element, percent)

Advanced Techniques

エレメントを置き換えるメソッドには以下のものがある。

  • Insertion.Top(element, content) 先頭のエレメントの後ろに挿入する。
  • Insertion.Bottom(element, content) 最後のエレメントの前に挿入する。
  • Insertion.Before(element, content) 先頭のエレメントの前に挿入する。
  • Insertion.After(element, content) 最後のエレメントの後に挿入する。

で、リクエスト時に発生するイベントのコールバックメソッドは以下のとおり。

  • :loading() XMLHttpRequestがサーバにデータを送信し始める時に起動される。
  • :loaded() XMLHttpRequestがサーバからのレスポンスを待っている時に起動される。
  • :interactive() サーバからデータが返され始めたら起動される。これはブラウザ依存なので気をつけて。
  • :complete() サーバからのレスポンスが完了したら起動される。

でも:loaded()と:interactive()はブラウザによって異なる動作をするのであまり使わない方がいいかもかも。

link_to_remote()のパラメタはこんな感じで…

  • :confirm 確認のダイアログボックスを表示する。
  • :before, :after リクエストする前/した後に評価するJavaScript

requestオブジェクトのメソッドはこんな感じ。

  • request.responseText サーバから返されるレスポンスのボディを返します。
  • request.status サーバから返されるHTTPステータスコードを返します。
  • request.getResponseHeader(name) サーバから返されるヘッダを返します。

プログレスインジケータは以下のようにすれば簡単に表示できる。

<%= text_field_tag :search %>
<%= image_tag("indicator.gif",
              :id => 'search-indicator',
              :style => 'display:none') %>
<%= observe_field("search",
                  :update => :results,
                  :url => {:action => :search},
                  :loading => "Element.show('search-indicator')",
                  :complete => "Element.hide('search-indicato')") %>

ま、もっともtext_fieldのオートコンプレッションには最初からプログレスインジケータを表示する仕組みが組み込まれているけど。

<%= text_field(:items, :description,
               :remote_autocomplete => {:action => :autocomplete},
               :indicator => "/path/to/image") %>

例えば同時に複数箇所を更新したい等複雑なことをしたかったら、サーバからJavaScriptを返して、それをクライアント側で評価するようにすればOK。

<%= link_to_remote("Update many",
                   :complete => "eval(request.responseText)",
                   :url => {:action => :update_many}) %>

ここまでのまとめとして、まずはAJAXを使わないTodo Listを作って、それからそれをAJAX対応にしてみたんだけど、詳しくは本を見てね:-)。


AJAXのテストをする場合はテスト用にXMLHttpRequestをシミュレートするxml_http_request()、略してxhr()というメソッドがあるよ。JavaScriptのインスペクションやデバッグをするにはFirefoxVenkmanというアドオンが便利。

xhr :post, :index

request.xml_http_request?、略してrequest.xhr?というメソッドはprototypeライブラリから呼ばれた場合に真となるので、AJAXとして呼ばれたかどうかが判定できる。

link_to_remote()に:hrefというパラメタを設定しておくと、JavaScriptをオフにしているユーザに対して:hrefで指定したURLのリンクとして表示される。これはform_remote_tag()の場合は必要なくて、その場合はaction=が自動的に設定される。JavaScriptが有効かどうかでアクションを変えたい場合は:urlと:htmlの両方を設定しておきましょう。そうすればAJAXの場合は:url,そうじゃない場合は:htmlで指定したアクションがリクエストされるよ。

ありゃ、もうWeb V2.1だよ。AJAX周りは急速に変化しているのでRailsとそのAJAXサポートはよく見ておきましょう。これを書いている最中にも、オートコンプリートのテキストフィールド、進行状況を表示するファイルアップロード、ドラッグアンドドロップ、順序を入れ替えることができるリスト等々がサポートされ始めたよ。http://script.aculo.us/をチェキ!

AWDwR "Chapter17 Action View" つづきのつづきのつづきのつづき

いよいよChapter17大詰め。

Caching, Part Two

Chapter16でキャッシュの話があったけど、Railsにはさらにページの一部分をキャッシュする機能があって、ビューのテンプレートの一部をcache()のブロックで囲んでおくとその部分だけキャッシュしてくれる。

<%= @dynamic_content %> # ここは毎回変わる
<% cache do %>
  ...                   # ここはキャッシュされる
<% end %>
<%= @dynamic_content %> # ここは毎回変わる

ただしキャッシュはプロダクションモードの時のみ有効。WEBrickの場合は-e productionオプションをつけるとプロダクションモードになる。


ビューでいくらキャッシュを使ってもアクションの中でその部分を表示するためのロジックが実行されてしまうと意味なしなので、そういう時は以下のようread_fragment()を使ってフラグメントがないときだけロジックを実行するようにする。

class BlogController < ApplicationController
  def list
    unless read_fragment(:action => 'list')
      @article = Article.find_recent
    end
  end
end

キャッシュのフラグメントをエクスパイアするにはこうする。

expire_fragment(:controller => 'blog', :action => 'list')

また一つのページ内で二つ以上のキャッシュのフラグメントを使用したい場合はこうしておいて…

<% cache(:action => 'list', :part => 'articles') do %>
  ...
<% end %>
<% cache(:action => 'list', :part => 'counts') do %>
  ...
<% end %>

エクスパイアするフラグメントをpartオプションで指定する。

class BlogController < ApplicationController
  def edit
    expire_fragment(:action => 'list', :part => 'articles')
    ...
  end
  def delete
    expire_fragment(:action => 'list', :part => 'articles')
    expire_fragment(:action => 'list', :part => 'counts')
    ...
  end
end

キャッシュの保存方法はActionController::Base.fragment_cache_storeで指定。指定できるオプションは以下のとおり。

Adding New Templating Systems

Railsには2つのテンプレートシステムがあるんだけど、簡単に自分でテンプレートシステムを作ることができるよ。

まずは次の2つの条件を満たすテンプレートハンドラクラスを作る。

  • コンストラクタはビューオブジェクトをパラメタとしてとること
  • render()というメソッドを実装する。このメソッドはテンプレートのテキストとローカル変数のハッシュをパラメタとして受けとり、レンダリングしたテキストを返すようにする。

で、そのハンドラを環境設定ファイルかapplication.rbで登録すればOK。

ActionView::Base.register_template_handler("rdoc", RDocTemplate)


コントローラのインスタンス変数やローカル変数をテンプレートで使えるようにするためにはrubyバインディングを使ってね。ってバインディングよくわからん…orz。


Chapter17終了!!

AWDwR "Chapter17 Action View" つづきのつづきのつづき

Chapter17つづき、いきますよ。

Layouts and Components

ビューにもDRY原則に則って重複を避けるためのレイアウト、パーシャルズ、コンポーネントという仕組みがある。

まずはレイアウト。レイアウトのテンプレートは以下のような感じ。

<html>
  <body>
    <%= @content_for_layout %>
  </body>
</html>

ミソは@content_for_layoutで、アクションでレンダリングされた結果がセットされ、レイアウトの中でそれが表示される。

レイアウトのテンプレートファイルはコントローラ名がstoreの場合、デフォルトではapp/views/layout/store_layout.[rhtml|rxml]になる。またapplication.[rhtml|rxml]というテンプレートファイルを用意しておくと対応するテンプレートがないコントローラに適用される。

デフォルトのテンプレート以外を使いたい場合はコントローラクラスの中で以下のように指定。

layout "standard"
layout "standard", :except => [:rss, :atom] # 指定されたものには適用しない
layout "standard", :only => [:foo, :bar] # 指定されたものだけに適用する

以下のようにすれば動的にレイアウトを変えることもできるし…

class StoreController < ApplicationController
  layout :determine_layout

  private
  def determine_layout
    (レイアウト名を返す)
  end
end

アクションごとにレイアウトを指定することもできる。

def rss
  render(:layout => false) # レイアウトを使用しない
end

def checkout
  render(:layout => "layouts/simple")
end

また、個々のアクションのテンプレートからレイアウトに対して値を渡すこともできる。

# レイアウト
<html>
  <body>
    <h1><%= title %></h1>
  </body>
</html>

# 個々のアクションのテンプレート
<%= @title="Hello" %> # @titleがレイアウトに渡る 


ページの一部分をテンプレートにしておいて、テンプレート内からそのテンプレートを呼ぶことができる。この一部分のテンプレートのことをパーシャルズ(?)といって、ファイルの名前は_で始める。例えば_article.rhtmlというテンプレートを用意しておくと以下のようにしてテンプレートから呼ぶことができる。

<%= render(:partials => "article", :object => @an_article) %>

一覧表示みたいに繰り返して表示したい場合はパーシャルズを使って

<%= render(:partials => "article", :collection => @article_list) %>

とする。オプションに:spacer_templateというのを指定すると、間にはさむテンプレートを指定できる。


ビューやアクションの中でアクションを呼び出すのをコンポ−ネントというんだけど、コンポーネントはその名のとおりコンポーネント化できる。コンポーネントはcomponentsディレクトリ内にディレクトリを作ってその下にコントローラ、モデル、ビューなどを置いておく。

コンポーネントの呼び出し方は以下のようにディレクトリ付のコントローラを指定する。

<%= render_components(:controller => 'sidebar/link', :action => 'get_link') %>

コンポーネント用コントローラはこの場合sidebarディレクトリ以下に置いておく。

class Sidebar::LinkController < ActionController::Base
  uses_component_template_root
    Link = Struct.new(:name, :url)
    def self.find(*ignored)
      ...
    end
  def get_links
    ...
  end
end

コンポーネント用コントローラが普通のコントローラと違う点は、モジュール内で定義されていること、uses_component_template_rootが宣言されていることの2つ。コンポーネント化しておけば、お友達がそれを気に入った場合そのコンポーネントディレクトリを「はいっ」とあげることができるよ。


もう少し!

ライブドア資本論

ライブドア資本論

ライブドア資本論

新刊かと思ったら6月に出ていたのね。何軒か本屋を回ってようやく入手。こんな本が出ていたなんて知らなかったからあまり話題になっていなかったのかもしれないけど、ひさびさのヒット。面白い。

連日大騒ぎにはなっていたけど中身はよくわかんなかったライブドアvsフジテレビ騒動がドキュメントで描かれ、そこに登場した金融のテクニックが詳しく説明される。フジテレビを買収するために社債で調達した800億円、ライブドアは一銭も返済する必要はないんですってよ! じゃあこの800億円は結局誰が負担したことになるかというと…もうビックリですよ、奥さん。フーッ、ため息出ちゃいますよ。頭イイ人が勝つんですなぁ。

他にも、団塊ジュニアとしての堀江社長の思考や行動を分析し、それを理解できない旧世代を小気味よく切り捨て、一方、バブル崩壊後の不況からネットバブル、金融ビッグバンを経て現在に至る日本経済の変化とそれにうまく乗った堀江社長ライブドアについて詳しく描かれる。

ちょっと時機を逸した感じだけどもオススメ。

AWDwR "Chapter17 Action View" つづきのつづき

やべっ、また半年ほどサボるところだったよ:-)。

Form Helpers

送信されてきたフォームのパラメタは以下のようにparamsハッシュにマップされる。

id=123 #=> {:id => "123"}
user[name]=Dave #=> {:user => {:name => "Dave"}}
user[address][city]=Wien #=> {:user => {:address => {:city => "Wien"}}}


テンプレート内のフォームはform_tag()とend_form_tag()で囲むこと。フォームのヘルパメソッドを以下で順に説明するよ。

まずはテキスト系のフィールド。普通のテキスト、ヒドゥンテキスト、パスワード用テキストがある。

text_field(:variable, :attribute, options)
hidden_field(:variable, :attribute, options)
password_field(:variable, :attribute, options)

オプションは:size=>"nn", :maxsize=>"nn"。


次にテキストエリア。

text_area(:variable, :attribute, options)

オプションは:cols=>"nn", rows=>"nn"。


ラジオボタンは以下のようにする。

radio_button(:variable, :attribute, tag_value, options)


チェックボックスは以下のようにする。

check_box(:variable, :attribute, options, on_value, off_value)


選択リストはこんな感じ。

select(:variable, :attribute, choices, options, html_options)
例
<%= select(:user, :name, %w{Andy Bert Chas Dave}) %>
<%= select(:user, :id, [['Andy', 1], ['Bert', 2], ['Chas', 3], ['Dave', 4]]) %>
<%= collection_select(:user, :name, @users, :id, :name) %> 


グループ化された選択リスト?っつーのもある。

<select name="order[shipping_option]" id="order_shipping_option">
<%= option_groups_from_collection_for_select(SHIPPING_OPTIONS,
                                             :options, :type_name,
                                             :id, ;name.
                                             @order.shipping_option) %>
</select>


日付や時刻は専用のヘルパメソッドがある。

date_select(:variable, :attribute, options)
datetime_select(:variable, :attribute, options)

select_date(date, options)
select_day(date, options)
select_month(date, options)
select_year(date, options)
select_datetime(date, options)
select_hour(time, options)
select_minute(time, options)
select_second(time, options)
select_time(time, options)

例えばselect_minuteはparams[:date][:minute]で値を取得できる。また:include_blank=>trueとすると空の選択肢を設定できる。

select_minuteでは:minute_step=>nnというオプションで分の刻み幅を設定。
select_monthでは:add_month_numbers=>trueで月の番号も表示、use_month_numbers=>trueでは月の番号のみ表示。
select_yearはデフォルトでは現在の前後5年を選択肢として表示するけど、開始年、終了年を指定したい場合は:start_year, :end_yearで設定する。

date_select, datetime_selectは年月日、年月日時分秒のメニューを表示する。discard_month=>1等とすればそのフィールドは表示されなくなり、特に1つだけ指定した場合はそれ以降のフィールドは表示されなくなる。また:order=>[:day, :month, :year]とすればその順でフィールドを表示する。


で、ファイルのアップロード。なんだかややこしいのぉ。ちょっと端折るよ。まずは以下のようにアップロードする画像ファイルを選択するフォームを表示する。

<%= form_tag({:action => 'save'}, :multipart => true) %>
  Upload file: <%= file_field("picture", "picture") %>
  <%= submit_tag("Upload") %>
<%= end_form_tag %>

モデルはちょっと一工夫。

class Picture < ActiveRecord::Base
  def picture=(picture_field)
    self.name = base_part_of(picture_field.orginal_filename)
    self.content_type = picture_field.content_type.chomp
    self.data = picture_field.read
  end
  def base_part_of(file_name) ... end
end

アップロードされた画像ファイルを保存するアクションはこうして…

def save
  @picture = Picture.new(params[:picture])
  if @picture.save
    redirect_to(:action => 'show', :id => @picture.id)
  else
    render(:action => :get)
  end
end

画像データを送信するアクションはこうする。

def picture
  @picture = Picture.find(params[:id])
  send_data(@picture.data, :filename => @picture.name,
                           :type => @picture.content_type,
                           :disposition => "inline")
%>

で、最後に画像を表示するためのテンプレート。

<img src="<%= url_for(:action => "picture", :id = @picture.id) %>


ところでヘルパメソッドは対象のモデルのバリデーションでエラーだった場合は、生成するHTMLをclass="fieldWithErrors"を属性として持ったdivタグで囲むので、適当にスタイルシートを定義しておくとエラー時にビックリさせることができる。またerror_message_on(), error_message_for()を使えばエラーメッセージが表示できる。

それから、今までのヘルパメソッドはモデルに対応したものだったけどモデルに対応しないヘルパメソッドもあって、それらはメソッド名の最後に_tagをつけまーす。


とゆーことで、まだまだChapter17はつづく…。