<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-4221096241867026900</id><updated>2025-02-17T20:48:16.176+08:00</updated><category term="嘴砲"/><category term="FLASH"/><category term="遊戲"/><category term="js"/><category term="心得"/><category term="數學"/><category term="設計"/><category term="演算法"/><category term="chatbot"/><category term="ruby"/><category term="Idea"/><category term="swift"/><category term="書評"/><category term="程式"/><category term="教育"/><category term="C#"/><category term="Design Pattern"/><category term="python"/><category term="記憶學"/><category term="電腦圖學"/><category term="link"/><category term="rails"/><category term="OPENGL"/><category term="XGraphics"/><category term="價值"/><category term="夢"/><category term="攻略"/><category term="C++"/><category term="google"/><category term="jQuery"/><category term="學習"/><category term="投資"/><category term="研究所"/><category term="網站推薦"/><category term="藝術"/><category term="chat知識"/><category term="google app engine"/><category term="html"/><category term="iPhone"/><category term="jsonp"/><category term="mac"/><category term="使用心得"/><category term="台灣大哥大"/><category term="工作"/><category term="推薦"/><category term="早午餐"/><category term="資費"/><category term="音樂"/><category term="食記"/><category term="JAVA"/><category term="Keras"/><category term="PAD"/><category term="canvas"/><category term="heroku"/><category term="iOS"/><category term="postgresql"/><category term="server"/><category term="web"/><category term="居家"/><category term="正規表示式"/><category term="短篇"/><category term="賭"/><category term="Android"/><category term="Matlab"/><category term="SQL"/><category term="UI"/><category term="asp.net"/><category term="git"/><category term="iPad"/><category term="linux"/><category term="plurk"/><category term="ubuntu"/><category term="video"/><category term="保險"/><category term="小故事"/><category term="改善生活品質"/><category term="書摘"/><category term="爐石戰記"/><category term="爬蟲"/><title type='text'>卡卡米的記憶體</title><subtitle type='html'></subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default?redirect=false'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default?start-index=26&amp;max-results=25&amp;redirect=false'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>411</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-7916777901621718618</id><published>2018-09-04T16:13:00.002+08:00</published><updated>2018-09-04T16:20:09.463+08:00</updated><title type='text'>在 rvm 上使用 ruby 2.5.1 跑 rails 5.2.1 時會發生的警告：already initialized constant FileUtils::VERSION</title><content type='html'>在 rvm 上使用 ruby 2.5.1 跑 rails 5.2.1 時會發生的警告：already initialized constant FileUtils::VERSION



詳細警告訊息如下：



```

/Users/etrex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/fileutils.rb:90: warning: already initialized constant FileUtils::VERSION

/Users/etrex/.rvm/gems/ruby-2.5.1/gems/fileutils-1.1.0/lib/fileutils.rb:92: warning: previous definition of VERSION was here

/Users/etrex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/fileutils.rb:1188: warning: already initialized constant FileUtils::Entry_::S_IF_DOOR

/Users/etrex/.rvm/gems/ruby-2.5.1/gems/fileutils-1.1.0/lib/fileutils.rb:1267: warning: previous definition of S_IF_DOOR was here

/Users/etrex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/fileutils.rb:1446: warning: already initialized constant FileUtils::Entry_::DIRECTORY_TERM

/Users/etrex/.rvm/gems/ruby-2.5.1/gems/fileutils-1.1.0/lib/fileutils.rb:1541: warning: previous definition of DIRECTORY_TERM was here

/Users/etrex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/fileutils.rb:1448: warning: already initialized constant FileUtils::Entry_::SYSCASE

/Users/etrex/.rvm/gems/ruby-2.5.1/gems/fileutils-1.1.0/lib/fileutils.rb:1543: warning: previous definition of SYSCASE was here

/Users/etrex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/fileutils.rb:1501: warning: already initialized constant FileUtils::OPT_TABLE

/Users/etrex/.rvm/gems/ruby-2.5.1/gems/fileutils-1.1.0/lib/fileutils.rb:1596: warning: previous definition of OPT_TABLE was here

/Users/etrex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/fileutils.rb:1555: warning: already initialized constant FileUtils::LOW_METHODS

/Users/etrex/.rvm/gems/ruby-2.5.1/gems/fileutils-1.1.0/lib/fileutils.rb:1650: warning: previous definition of LOW_METHODS was here

/Users/etrex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/fileutils.rb:1562: warning: already initialized constant FileUtils::METHODS

/Users/etrex/.rvm/gems/ruby-2.5.1/gems/fileutils-1.1.0/lib/fileutils.rb:1657: warning: previous definition of METHODS was here

Rails 5.2.1

```



排除方法：



```

gem uninstall fileutils

```</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/7916777901621718618/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/7916777901621718618' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/7916777901621718618'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/7916777901621718618'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/09/rvm-ruby-251-rails-521-already.html' title='在 rvm 上使用 ruby 2.5.1 跑 rails 5.2.1 時會發生的警告：already initialized constant FileUtils::VERSION'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-2863978172428256941</id><published>2018-06-13T01:22:00.001+08:00</published><updated>2018-06-13T10:09:47.604+08:00</updated><title type='text'>Ruby Kaigi 2018 會後心得</title><content type='html'>markdown

這是我第一次參加 Ruby Kaigi，Ruby Kaigi 是一個 Ruby 的大型研討會，參加者約 1000 人左右，在日本仙台國際中心舉辦。

我們公司([五倍紅寶石](https://5xruby.tw/))免費提供了機票、住宿、研討會門票和生活津貼等資源給想要參加 Ruby Kaigi 的員工，目的是避免員工成為邊緣人、促進員工多參與社群，我們的員工福利真的hen好，感恩五倍、讚嘆五倍。

我們搭乘虎航的飛機，從桃園機場前往日本仙台。

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsp-bghYWOEvejttfI06GML5Eg40y3lBYVQB_6WOuQopS_3Aw5grKJIh0eUVo70WMln76i3FZ6CL-cvk7hd8Qd6KjI7E8SClOKQ4EiHyPUEQAl7tvcT2HaX7PZzGISDh1D0n_4GdMhf1U/s1600/2018-05-29+13.13.46.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1600&quot; data-original-width=&quot;1600&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsp-bghYWOEvejttfI06GML5Eg40y3lBYVQB_6WOuQopS_3Aw5grKJIh0eUVo70WMln76i3FZ6CL-cvk7hd8Qd6KjI7E8SClOKQ4EiHyPUEQAl7tvcT2HaX7PZzGISDh1D0n_4GdMhf1U/s640/2018-05-29+13.13.46.jpg&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

抵達仙台後再搭從仙台空港站 to 仙台站的鐵路交通，類似台灣的捷運，但不確定要怎麼稱呼。

出仙台站後發現我們的飯店竟然就直接蓋在車站旁邊大約 50 公尺的距離。

後來發現我們甚至可以從車站內直接走到飯店，不需要走出車站。

抵達飯店後，同事就直接在大廳擺出了一個帥爆的姿勢。

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFpRRFlebOkA92ykeKJpsu8RvEGmx0DlFs7blAYXdlF3znRs7QyqWA5zBEUS7SdafL4CEAfBungQ3F1MhleohYh4rdAZsmC1oZ0bqIhlgGsERrq2yzYFek-SIL7wzGgl1x4r_EdlcGUGc/s1600/2018-05-29+19.52.17.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1200&quot; data-original-width=&quot;1600&quot; height=&quot;480&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFpRRFlebOkA92ykeKJpsu8RvEGmx0DlFs7blAYXdlF3znRs7QyqWA5zBEUS7SdafL4CEAfBungQ3F1MhleohYh4rdAZsmC1oZ0bqIhlgGsERrq2yzYFek-SIL7wzGgl1x4r_EdlcGUGc/s640/2018-05-29+19.52.17.jpg&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

為了之後的 Party 做準備，第一天的晚餐就是燒肉喝到飽。在日本，似乎很盛行喝酒交朋友的文化。

燒肉喝到飽是指飲料(包含酒類)可以無限點(如果你喝得夠快的話，就可以趨近於無限)，但是燒肉的部分是類似套餐，是固定的份量。

飲料的 Menu 上可以看見各種酒名以日文表示。因為我比較喜歡喝調酒，所以我的策略就是在調酒的分類按照順序點。點餐時基本上我就是隨便指一個，即使 Google 加上翻譯，還是很難知道自己喝到的是什麼，畢竟調酒名稱本來就都很詭異。


第二天是 Remote 工作日，據說是因為發現早一天到達仙台可以獲得比較便宜的總支出。

第二天的晚上，也就是 Ruby Kaigi 的前一天晚上，有一個由 Speee 主辦的 Preparty。

Speee 是 Ruby Kaigi 的贊助廠商之一，由於 Ruby Kaigi 的 Party 之多，我無法判斷到底哪些 party 才是所謂「官方」所舉辦。

現場看起來大概像這樣：

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiiKK_8N_I1pnGmNvuWERWJDNvPuljYknlrMpDe66rnm67gEz26Cw0MW3FqQ4kvYUwRhG3qW2QRB5yHL7Ghpiq8fPO77_fwChuq3mjJ2ReKJhpja2IYXZFilEsx2Y1Bu2quoY9tdQG2_t8/s1600/2018-05-30+19.03.26.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1200&quot; data-original-width=&quot;1600&quot; height=&quot;480&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiiKK_8N_I1pnGmNvuWERWJDNvPuljYknlrMpDe66rnm67gEz26Cw0MW3FqQ4kvYUwRhG3qW2QRB5yHL7Ghpiq8fPO77_fwChuq3mjJ2ReKJhpja2IYXZFilEsx2Y1Bu2quoY9tdQG2_t8/s640/2018-05-30+19.03.26.jpg&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

中間有一整條的食物區，旁邊則是大型方桌提供酒類。


&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEityeY42KN4aDSOQk5brmPvKiP3d-7CdoEB9NwP3OKo-3Q48BO6jiIG4F42LvNJdaqBv7-wIy31ABeqEYyqW4L9tT_gEbfsmFxId8Ko55R7cE9Ga9OWXf7Xbi17tPaqbkMMt3MxIPm34iE/s1600/2018-05-30+19.03.53.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1200&quot; data-original-width=&quot;1600&quot; height=&quot;480&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEityeY42KN4aDSOQk5brmPvKiP3d-7CdoEB9NwP3OKo-3Q48BO6jiIG4F42LvNJdaqBv7-wIy31ABeqEYyqW4L9tT_gEbfsmFxId8Ko55R7cE9Ga9OWXf7Xbi17tPaqbkMMt3MxIPm34iE/s640/2018-05-30+19.03.53.jpg&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

壽司是主食，只可惜我不吃壽司。

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEguVRhgtEdT83dJVQdxy6awaTqefYzZqh2bQ6I8Ym2i295pCQiRfURoeSg1f6lI4DQiOgnLD3Abs6C8cn6XWRhKq3pg2Ev6SemUI6c6KBo_8rUb1qioPTiT8hqKAyyk1rlPZnjZnR4Dnk0/s1600/2018-05-30+19.41.54.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1600&quot; data-original-width=&quot;1200&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEguVRhgtEdT83dJVQdxy6awaTqefYzZqh2bQ6I8Ym2i295pCQiRfURoeSg1f6lI4DQiOgnLD3Abs6C8cn6XWRhKq3pg2Ev6SemUI6c6KBo_8rUb1qioPTiT8hqKAyyk1rlPZnjZnR4Dnk0/s640/2018-05-30+19.41.54.jpg&quot; width=&quot;480&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

旁邊有一個調酒區，可以跟 NPC 點酒，這一罐紫色的酒(Crème de cassis)是我有興趣的酒，所以特地拍了一張。

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSoHVLB5_PmlBH_ZK10dgNQyY68ZN-b-KM5QoJf7wt_zUXdz_Tbj7_IAwA7KTHXR7go5h38LXDVthFtrJtvCy8KO2-m-Ul0zDFOX4doPojIJ-koJ9XGj6RN0mpet3tTHk9waxSoz49hOo/s1600/2018-05-30+20.23.53.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1600&quot; data-original-width=&quot;1200&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSoHVLB5_PmlBH_ZK10dgNQyY68ZN-b-KM5QoJf7wt_zUXdz_Tbj7_IAwA7KTHXR7go5h38LXDVthFtrJtvCy8KO2-m-Ul0zDFOX4doPojIJ-koJ9XGj6RN0mpet3tTHk9waxSoz49hOo/s640/2018-05-30+20.23.53.jpg&quot; width=&quot;480&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

在這個會場有個舞台，Party 中途有一些活動，其中一個活動是清酒盲測。這一罐看起來很高級的清酒會被倒在A杯或B杯中，而另一個杯子就會是普通清酒。

主持人在現場尋找自願參加者，有興趣的人就可以上去玩。

參加者需要戴上眼罩，隨後工作人員就會開始倒酒，此時只有參加者不知道哪一杯酒是高級清酒，現場所有的旁觀者都看得一清二楚。

參加者喝完之後需要舉牌表示意見，主持人也會詢問他們為何選擇這樣的答案，這是一個還蠻有趣的環節。

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidufFoK_pY7AL6vyqwpHOBYW4tOj2MltzIM7PDjIbJQVVUaIkUMKD_xOEv-cvjWIWIJBZItV0-KTlCdDekE-ZmdYpRkjjJKkQIH4ZN-1o_oBDCXE_8NGc2CBhaJ7vn8c1aDT__qd76suk/s1600/2018-05-30+20.24.01.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1600&quot; data-original-width=&quot;1200&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidufFoK_pY7AL6vyqwpHOBYW4tOj2MltzIM7PDjIbJQVVUaIkUMKD_xOEv-cvjWIWIJBZItV0-KTlCdDekE-ZmdYpRkjjJKkQIH4ZN-1o_oBDCXE_8NGc2CBhaJ7vn8c1aDT__qd76suk/s640/2018-05-30+20.24.01.jpg&quot; width=&quot;480&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

這是在遊戲說明時介紹高級清酒的部分。


在這個場合，我們需要多嘗試接觸其他人。這對我來說難度很高，因為即使是台灣人我都不知道該跟對方說什麼，更何況是外國人。

我覺得我就像是一隻被動怪，如果有人來跟我對話的話，我是OK的。但是我不會主動攻擊。

所以我後來就發展出一個策略：跟著一個主動怪同事(以下簡稱主動怪)，由他發動攻擊，其他人就圍上去這樣。

主動怪教我們如何發動攻擊：「你眼睛就四處看，有時候會剛好跟某人四目相對，這時候就直接走過去打招呼就行惹。」

我是覺得這就像神奇寶貝遊戲裡的路人訓練師，當你路過他面前時，他就會走過來找你PK。


在 Party 結束回到飯店後，一群人休息後移動到主動怪的房間聽同事試講，因為我們同行的同事包含兩位講者。

第三天，Kaigi 終於開始。

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbrEKEwYeHiywCS69dfp8PQrKjIL4QkxCW57241F6GnomosP4NPzmP-Hn0SziFn6VjPwSqATE3FssoYN8WY-4stOUzq6ljFTlQQQ1aNMWkOjQeJWz8ikwWLlzkfgph8zWdi54-CqfmnKo/s1600/2018-05-31+10.05.34.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbrEKEwYeHiywCS69dfp8PQrKjIL4QkxCW57241F6GnomosP4NPzmP-Hn0SziFn6VjPwSqATE3FssoYN8WY-4stOUzq6ljFTlQQQ1aNMWkOjQeJWz8ikwWLlzkfgph8zWdi54-CqfmnKo/s640/2018-05-31+10.05.34.jpg&quot; width=&quot;640&quot; height=&quot;480&quot; data-original-width=&quot;1600&quot; data-original-height=&quot;1200&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

# 午餐

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzfJ2P9Fh1M0w4aPVEdZUWEKiAUOA-lPDnVUHtGA0uyI3JbI-dxrPthxJ9eYhb2uKsAzFqeGicntyM2v-ErZvmGi6nuqJzycnbbIX_d_evUJzqarbMuhxMoVVmFNgglmVswJaZDmHj-HQ/s1600/2018-05-31+11.54.51.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzfJ2P9Fh1M0w4aPVEdZUWEKiAUOA-lPDnVUHtGA0uyI3JbI-dxrPthxJ9eYhb2uKsAzFqeGicntyM2v-ErZvmGi6nuqJzycnbbIX_d_evUJzqarbMuhxMoVVmFNgglmVswJaZDmHj-HQ/s640/2018-05-31+11.54.51.jpg&quot; width=&quot;640&quot; height=&quot;480&quot; data-original-width=&quot;1600&quot; data-original-height=&quot;1200&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

他們提供了多樣性的便當選擇，但因為我不吃素，不吃壽司，也不吃醃漬類食物，所以我三天都拿同一款便當，也就是這款。

我們帶著便當到附近的公園(似乎是古蹟)野餐，看起來像這樣：

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8A4vGE0ELB6qqxxpuGkvRZuvEiwvcxjnhoVF2g_CqgObAAAW3_uuJGAFVQlo21-GCn63gVWieb1xw6XiCBG5InTRe1HeZXXDoHO8gkm7vrbCYuIPhRTh8CklDqX-Bz9cWeU1he8XqV_U/s1600/2018-05-31+11.56.06-1-1.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8A4vGE0ELB6qqxxpuGkvRZuvEiwvcxjnhoVF2g_CqgObAAAW3_uuJGAFVQlo21-GCn63gVWieb1xw6XiCBG5InTRe1HeZXXDoHO8gkm7vrbCYuIPhRTh8CklDqX-Bz9cWeU1he8XqV_U/s640/2018-05-31+11.56.06-1-1.jpg&quot; width=&quot;640&quot; height=&quot;640&quot; data-original-width=&quot;1600&quot; data-original-height=&quot;1600&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

# 下午茶

下午茶提供各式日本茶點，有銅鑼燒和各種我講不出名字的當地名產還有一些水果拼盤。水果的部分還是台灣的比較多樣化一點。

若要論好吃程度的話，在每種都已經吃過的情況下，如果現場有鳳梨酥，我會選擇吃鳳梨酥。

# 各種 Party

除了官方辦的 Party 之外，其他 Party 都是免費的，但我認為免費的 Party 都表現得比較好。我想應該是因為免費，所以感覺比較好吧。

所有的 Party 都提供無限量的啤酒跟清酒，我比較喜歡清酒，因為啤酒有氣泡，而我不太能喝含有氣泡的飲料。

# 贊助商

我在贊助商區發現了一個很有趣的攤位。

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihO4p7gVWaOQpEeD9BsJiB-V3rY6_sjcIxpkJYk2oIWk2GkEGMyuLAJcqB3ARlB19ejhvonxCV2LRntFgbJ5g6l2IiEzILuSSWtck-rSFzkQG5Cq2mPILqJMOWdEo1cBnoMa6B8dHajLU/s1600/2018-05-31+12.49.48.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihO4p7gVWaOQpEeD9BsJiB-V3rY6_sjcIxpkJYk2oIWk2GkEGMyuLAJcqB3ARlB19ejhvonxCV2LRntFgbJ5g6l2IiEzILuSSWtck-rSFzkQG5Cq2mPILqJMOWdEo1cBnoMa6B8dHajLU/s640/2018-05-31+12.49.48.jpg&quot; width=&quot;640&quot; height=&quot;480&quot; data-original-width=&quot;1600&quot; data-original-height=&quot;1200&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgymyfNWnAaF8fHg-cvtbqUoKFPqqQd4aOza38SsecHhhM5ccanU1yn61KOMKQmqcY3cFsdxy8WZ5QWSPhY9lTyXxd_xh90JXMr_hvDWUBVgtAIzUYHF0tmJiB8YJaWU3RV94gXtCUbr58/s1600/2018-05-31+12.50.06.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgymyfNWnAaF8fHg-cvtbqUoKFPqqQd4aOza38SsecHhhM5ccanU1yn61KOMKQmqcY3cFsdxy8WZ5QWSPhY9lTyXxd_xh90JXMr_hvDWUBVgtAIzUYHF0tmJiB8YJaWU3RV94gXtCUbr58/s640/2018-05-31+12.50.06.jpg&quot; width=&quot;640&quot; height=&quot;480&quot; data-original-width=&quot;1600&quot; data-original-height=&quot;1200&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

使用不同顏色的樂高來區分，可以即時生成對應的室內設計圖。


他們的贊助商區提供了各式各樣的小禮物，藉此吸引你過去他們的攤位。有些則是會要求你在 Twitter 上發文或者要求你做一些事情才會送。

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFJrxzxuaYT6-VzEywbXf1SeS_A6TaRsnUyb37xTPys4WnMe8FTzdMwndNVVYQVslJ00rjVmdggHtSwLnARTBhePcSrdtE29kEN97I1Ekt95bco2SyNoe8XXNIv_CB1npVtBYsIibG0R0/s1600/2018-05-31+23.18.21-1.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFJrxzxuaYT6-VzEywbXf1SeS_A6TaRsnUyb37xTPys4WnMe8FTzdMwndNVVYQVslJ00rjVmdggHtSwLnARTBhePcSrdtE29kEN97I1Ekt95bco2SyNoe8XXNIv_CB1npVtBYsIibG0R0/s640/2018-05-31+23.18.21-1.jpg&quot; width=&quot;640&quot; height=&quot;640&quot; data-original-width=&quot;1600&quot; data-original-height=&quot;1600&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

有手提袋、資料夾、貼紙、扇子、徽章、入浴劑、香皂、溜溜球、杯墊、包包掛環、筷子、眼鏡布、pokey、筆記本、詐神筆記本等。

# 古蹟

趁中午吃完飯去看了一下博物館和仙台城跡，因為博物館不能拍照，所以就給大家看看仙台城跡上有什麼。

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFzAddGryoa8HsNORsVwLDzPO8C_YLTvoodFn2dn3loyNsxKx1_xt7ffGDuAxNNGcbnP-91BRjENkVP1H-_B_08FkmBf8fhyphenhyphen2q4_zczi1aT5ih715a60B8iCCnQCwh7VD5YA3XlPX8fpU/s1600/2018-06-02+12.24.41.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFzAddGryoa8HsNORsVwLDzPO8C_YLTvoodFn2dn3loyNsxKx1_xt7ffGDuAxNNGcbnP-91BRjENkVP1H-_B_08FkmBf8fhyphenhyphen2q4_zczi1aT5ih715a60B8iCCnQCwh7VD5YA3XlPX8fpU/s640/2018-06-02+12.24.41.jpg&quot; width=&quot;640&quot; height=&quot;480&quot; data-original-width=&quot;1600&quot; data-original-height=&quot;1200&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhW3oeSwCJWnS-hj_6NMbMjJ7LClJHFgjwc3jcSQxJ01cQHAGBneCQjToJzQZ4NjI4a71BqNYpgl4HZAFZyrf-XJ63iN-MmFtv7ctenFmQAKKpU9yODcn6wORspg6vr2CNxhCaklRUJwbU/s1600/2018-06-02+12.25.39.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhW3oeSwCJWnS-hj_6NMbMjJ7LClJHFgjwc3jcSQxJ01cQHAGBneCQjToJzQZ4NjI4a71BqNYpgl4HZAFZyrf-XJ63iN-MmFtv7ctenFmQAKKpU9yODcn6wORspg6vr2CNxhCaklRUJwbU/s640/2018-06-02+12.25.39.jpg&quot; width=&quot;640&quot; height=&quot;480&quot; data-original-width=&quot;1600&quot; data-original-height=&quot;1200&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaWPqs6r9Yvh97y6Apo7Z9VTDidaryIKai8cXPBC1dfdUX4ILq18Pv8qQnuPg8Hg4i1Ri1PplIAPcT-Ft2tp5B2__zaFSGFJJYhuflh871WEPXVEibatX5qXjg8lTCnJSsl-sf2NJnc0Y/s1600/2018-06-02+12.32.09-1.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaWPqs6r9Yvh97y6Apo7Z9VTDidaryIKai8cXPBC1dfdUX4ILq18Pv8qQnuPg8Hg4i1Ri1PplIAPcT-Ft2tp5B2__zaFSGFJJYhuflh871WEPXVEibatX5qXjg8lTCnJSsl-sf2NJnc0Y/s640/2018-06-02+12.32.09-1.jpg&quot; width=&quot;640&quot; height=&quot;480&quot; data-original-width=&quot;1600&quot; data-original-height=&quot;1200&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi99KP4HsVEiGoXhDcuRKAHu5y805xg3cEi2SA01gGR0EWgKajvu5HhzIbYRICSiXm9BdUbKVzia6KX1ypkrwlBiJoiJYEtrrfN33U8AxoVl6pcBtS5GGe-XjRkjG7VuTWyxKFsB3dS9qw/s1600/2018-06-02+13.04.01.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi99KP4HsVEiGoXhDcuRKAHu5y805xg3cEi2SA01gGR0EWgKajvu5HhzIbYRICSiXm9BdUbKVzia6KX1ypkrwlBiJoiJYEtrrfN33U8AxoVl6pcBtS5GGe-XjRkjG7VuTWyxKFsB3dS9qw/s640/2018-06-02+13.04.01.jpg&quot; width=&quot;480&quot; height=&quot;640&quot; data-original-width=&quot;1200&quot; data-original-height=&quot;1600&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

# 議程

## [Matz 的開場 Keynote](https://rubykaigi.org/2018/presentations/yukihiro_matz.html#may31)

- 命名很重要

- 命名會給予概念，而人們使用概念來進行交流。

- 當你覺得命名很難，表示你還沒正確理解那些概念。

- 命名專案的時候，可被 Google 很重要

- 不要使用已經存在的單字，像是 Go、Swift，而是使用兩個單字的組合，像是 TensorFlow，或者是修改單字中的幾個字母，藉此獲得可被 Google 的特性

- 時間很重要

- 時間就是錢，時間就是價值，但是人類經常浪費時間

- 如何增加開發者的產能很重要

- Ruby 超棒

- Ruby 有很強大的內建函數

- Ruby 有很強大的套件和框架

- Ruby 有友善的社群

- Ruby 很簡潔

- Ruby 目前著重於效能和 Concurrency 的優化。

- 時間就是錢：如果效能變成3倍，就等於可以使用1/3的雲端平台成本支撐目前的使用量

- Ruby 已死！？ Ruby 每年都在死

## [Hijacking Ruby Syntax in Ruby](https://rubykaigi.org/2018/presentations/joker1007.html#may31)

- 做了一些套件可以用來強迫檢查繼承後的 class 有沒有好好寫

- [Finalist](https://github.com/joker1007/finalist)：override 同名方法時會 raise
- [Overrider](https://github.com/joker1007/overrider)：當父類別沒有同名方法時會 raise
- [Abstriker](https://github.com/joker1007/Abstriker)：當繼承的 class 沒有實作抽象方法時會 raise

- [ImplicitParameter](https://github.com/joker1007/implicit_parameter)

- [With Resource](https://github.com/tagomoris/with_resources)：確保資源只會在block 內使用，結束後會被安全釋放

- [Deferred](https://github.com/tagomoris/deferral)：With Resource 有多重資源使用時的波動拳問題，考慮 Go 語言的 defer，所以在 Ruby 也幹一個

- 很多東西是使用神奇 der [TracePoint](https://ruby-doc.org/core-2.5.0/TracePoint.html) 來實作

- 提到 Ruby 的 undef_method：直接跳 NoMethodError 不問父類別

- 提到 Ruby 的 remove_method：會檢查父類別有沒有同名方法


## [Fast Numerical Computing and Deep Learning in Ruby with Cumo](https://rubykaigi.org/2018/presentations/sonots.html#may31)

- 他做了一個叫 Cumo 的套件，可以使用 CUDA 在 NVidia 顯示卡上做計算的加速。

- 介紹了 Ruby 跟機器學習有關的套件

- DNN : Red-chainer

- Tensor : Numo/NArray, Cumo

- CUDA binding : RbCUDA

- PyCall : binding to python

- tool : Rubyx

- CUDA 的記憶體配置很慢，所以 Cumo 有實作 Memory Pool 避免重複配置

- 目前在顯卡記憶體管理上還未完善，無法有效利用

- 未來想串接 cuDNN，cuDNN 是 NVidia 的針對 DNN (深度類神經網路)計算加速的另一款 Lib。

## [Scaling Teams using Tests for Productivity and Education](https://rubykaigi.org/2018/presentations/jules2689.html#jun01)

- 我們無法要求所有員工都能夠在「事前」記住所有該注意的事項，但「事後」才發現就已經太遲，最佳的教育時機就是在當下

- 提出 JIT 教育的概念

- 將 Rubocop 視為一種自動進行的 JIT 教育

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNqIB7yKNXOCERk_kq8_w-KDgdJkIYdHMgGx3D3-QG5THuMKvD1PgpIPg1V-b4nozPnoEYMJBvvADI9zDMDxb1VNgYJfo9LKNACbL7uIzwBhxT-BP2UP8ABrNKnAhMX28RNeH44TaEJK0/s1600/2018-06-01+14.24.38.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNqIB7yKNXOCERk_kq8_w-KDgdJkIYdHMgGx3D3-QG5THuMKvD1PgpIPg1V-b4nozPnoEYMJBvvADI9zDMDxb1VNgYJfo9LKNACbL7uIzwBhxT-BP2UP8ABrNKnAhMX28RNeH44TaEJK0/s640/2018-06-01+14.24.38.jpg&quot; width=&quot;640&quot; height=&quot;480&quot; data-original-width=&quot;1600&quot; data-original-height=&quot;1200&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

上圖說明了這樣的教育方式也能實踐到環境架設上

在其他的場次中沒有獲得值得寫下來的內容，或者大多數內容是聽不懂的。

# 會場周邊活動

在會場有一個小區域做了 Pair Programming 的直播，主題是使用 Ruby 寫出一個 [Game of Life](https://en.wikipedia.org/wiki/Conway&#39;s_Game_of_Life)

在這個活動進行的同時，議程也在進行中，所以在這邊看他們 Pair Programming 就無法看議程。

Game of Life 是假設這個世界就像棋盤，而每一個格子裡面可能有生物或沒有生物。

如果一個格子的周圍 8 格有其他生物，稱為鄰居，而鄰居的數量會影響生物出生或者死亡。設定不同的出生和死亡條件會讓結果不同。

本次實作的出生條件：某個空格剛好有 3 個鄰居，就會在某格生成 1 個生物

本次實作的死亡條件：某個生物的鄰居 &lt; 2 或者 &gt; 3，就會死亡

如果不屬於出生或死亡條件，格子的狀態就維持原樣。

看著看著覺得他們實作得很慢，於是就在旁邊也實作了個：[https://gist.github.com/etrex/9999836faa433046d44ab5af9e7c01fe](https://gist.github.com/etrex/9999836faa433046d44ab5af9e7c01fe)

結果自己實作也很慢。


# [三角棋(Triangular Nim)](https://zh.wikipedia.org/wiki/%E4%B8%89%E8%A7%92%E6%A3%8B)

在會場寫了 Game of Life 之後，找到了一些小時候寫 Console 小遊戲的手感，於是想要嘗試把小時候沒作完整的三角棋 AI，趁在聽不懂的議程中和回到飯店時。用 Ruby 再實作一次。



這次考慮了強化學習的世界觀，把遊戲分成 Environment 和 Agent，Environment 代表遊戲以及遊戲的主持人，而 Agent 則代表玩家。

目標是實作出強化學習的 AI，希望可以做到像 AlphaGo Zero，不提供人類的知識，而是讓 AI 透過自己跟自己玩，在遊戲經驗中變強。

目前實作了三角棋的 Environment 以及亂選的傻逼 AI 和人類玩家這兩個 Agent。

這是程式碼：[https://github.com/etrex/ai/tree/master/triangular_nim](https://github.com/etrex/ai/tree/master/triangular_nim)


# 整體心得

在某些議程中也許不能獲得什麼，但是可以引發自己的想法。因為它提供了一些時間和環境，讓你去思考一些平常不會去思考的問題。

我跟主動怪討論了為什麼 Ruby Kaigi 的議程選題都是這麼硬的主題之類的問題，我得到了一個結論。

網路效應不只發生在應用程式或服務上，同時也發生在程式語言上，也就是說強勢語言可以獲得較多的使用者，導致該語言更強勢。

Ruby 的優點是快速建置服務，從 Ruby 圈的人努力的方向來看，目前的主要發展是效能、Concurrency 和機器學習來看，主要的假想敵應該是有 TensorFlow 可以用的 Python。

如果在 Ruby 這邊搞一個 Gem，讓你只需要實作問題以及演算法，就能夠直接變成一個網路服務的形式存在的話，我認為應該會很有搞頭。

</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/2863978172428256941/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/2863978172428256941' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/2863978172428256941'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/2863978172428256941'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/06/ruby-kaigi-2018.html' title='Ruby Kaigi 2018 會後心得'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsp-bghYWOEvejttfI06GML5Eg40y3lBYVQB_6WOuQopS_3Aw5grKJIh0eUVo70WMln76i3FZ6CL-cvk7hd8Qd6KjI7E8SClOKQ4EiHyPUEQAl7tvcT2HaX7PZzGISDh1D0n_4GdMhf1U/s72-c/2018-05-29+13.13.46.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-7702012040425804945</id><published>2018-01-23T00:06:00.002+08:00</published><updated>2018-01-23T00:20:16.726+08:00</updated><title type='text'>只要有心，人人都可以作卡米狗 - 完賽心得</title><content type='html'># 參加感想
其實一開始參加的時候是想說反正隨時棄坑都沒關係，至少我有開始過。但沒想到讀者比我預想的還要多，情況有點不受控制，我似乎不得不把質跟量都作出來，不然就會辜負這些讀者。不過也感謝大家的支持，我才能順利完賽，在沒有任何文章存稿的情況下參賽，連我都不相信我能完賽。

# 讀者群的設定
因為卡米狗粉都是沒有接觸資訊領域，不會寫程式的人，所以在我寫文的一開始，就把讀者群設定在電腦只有開過 IE、只安裝過 MMORPG 的等級。要從檔案總管和記事本教起，這件事比我一開始想像中的還要累。在講到任何知識之前，我都得要先想一下，我應該要假設讀者已經學過了嗎？如果我這裡跳過不講，讀者會不會放棄治療，一輩子卡關在這裡呢？還是說我不應該講這麼細節的東西，應該讓讀者用肌肉記憶就好？我是覺得如果我不講，讀者放棄治療的機率很高啦。

對於 iT邦幫忙既有的讀者來說，我設定的讀者群程度可能就太淺了，抱歉占用到你們的版面。不過，我從一開始就不是打算寫給你們(工程師們)看的。

# 關於選題
只要有心，人人都可以作卡米狗，這個選題已經說明了讀者群的設定就是麻瓜。而聊天機器人說穿了就是個只有後端的網站，製作難度肯定低於架網站，我只需要確保每個讀者都懂 HTTP 協定，並且會架 HTTP Server 即可。主要目標是讓讀者看完之後能夠有基礎的網站概念，開始能看得懂工程師寫的技術文章，以及知道遇到問題時要在 GOOGLE 輸入什麼關鍵字的能力。

# 關於文章內容的編排
我首篇先講什麼是聊天機器人，並以卡米狗舉例說明，當然也是為了置入一波卡米狗。

在我作任何教學之前，我會希望讀者能夠先知道為什麼他要學這個，所以我選擇採用從上而下的講解方式，先講最大的框架是由什麼構成，接下來再去認識細節和實作的部分，而每一個實作的部分都是遇到才教。我就是怕我一教難的你們就跑了。

如果我今天第一篇開頭就說，我們要用 sublime、ruby、rails、git、heroku 哦～先安裝吧，然後前面10篇都在安裝，這樣的編排真的有人讀得下去嗎？我很懷疑。我認為要讓讀者能夠在初期就取得巨大的成就感，讀者才會有信心能夠跟著文章走下去。所以我在第三篇就讓讀者建立一個 Line chatbot 帳號，而且可以講一些廢話。後面花了20篇的篇幅在教怎麼作出跟 Line@ 提供的後台一模一樣的東西。

不過這樣的篇排有個缺點，就是不能跳著讀。

# 目錄
大致的切分如下：

### 基本觀念的建立
從聊天機器人帶到 Webhook，再帶到 HTTP 協定以及 Web Server。  
[第一天：認識聊天機器人](https://ithelp.ithome.com.tw/articles/10192259)  
[第二天：認識卡米狗](https://ithelp.ithome.com.tw/articles/10192575)  
[第三天：作一隻最簡單的 Line 聊天機器人](https://ithelp.ithome.com.tw/articles/10192928)  
[第四天：認識 Webhook](https://ithelp.ithome.com.tw/articles/10193212)  
[第五天：認識 Line Messaging API Webhook](https://ithelp.ithome.com.tw/articles/10193441)  
[第六天：認識網站](https://ithelp.ithome.com.tw/articles/10193664)  
[第七天：認識網頁伺服器](https://ithelp.ithome.com.tw/articles/10193904)  

### 開發環境的建立
從 Web Server 帶到 Rails，再帶到 Command Line、Sublime Text  
[第八天：安裝 Rails 和認識小黑框](https://ithelp.ithome.com.tw/articles/10194156)  
[第九天：作一個最簡單的 Rails 網站](https://ithelp.ithome.com.tw/articles/10194359)  
[第十天：認識文字編碼](https://ithelp.ithome.com.tw/articles/10194586)  
[第十一天：認識文字編輯器](https://ithelp.ithome.com.tw/articles/10194805)  

### HTTP 協定的深入了解
從各個角度了解 HTTP，從瀏覽器發送和接收、也從網站伺服器發送和接收  
[第十二天：從瀏覽器認識 HTTP 協定](https://ithelp.ithome.com.tw/articles/10194805)  
[第十三天：認識 Ruby 的資料型態](https://ithelp.ithome.com.tw/articles/10195196)  
[第十四天：最基本的 Rails 運作流程](https://ithelp.ithome.com.tw/articles/10195380)  
[第十五天：從 Rails 認識 HTTP 協定](https://ithelp.ithome.com.tw/articles/10195578)  
[第十六天：做一個最簡單的爬蟲](https://ithelp.ithome.com.tw/articles/10195760)  

### 發布環境的建立
介紹發布環境，帶到 Heroku 和 Git  
[第十七天：怎麼讓別人連到我作好的網站？](https://ithelp.ithome.com.tw/articles/10195920)  
[第十八天：發布網站到 Heroku](https://ithelp.ithome.com.tw/articles/10196129)  
[第十九天：發布網站到 Heroku (續)](https://ithelp.ithome.com.tw/articles/10196250)  

### LINE API 的串接
基礎知識備齊，終於來到正題。讀者設定為一般工程師的話，第一篇大概會從這邊開始寫起。  
[第二十天：串接 Line Messaging API Webhook](https://ithelp.ithome.com.tw/articles/10196397)  
[第二十一天：讓 Line Bot 回覆訊息](https://ithelp.ithome.com.tw/articles/10196544)  
[第二十二天：用 Line Messaging API 實作關鍵字回覆](https://ithelp.ithome.com.tw/articles/10196672)  

### 資料庫的操作
缺乏的一塊基礎知識，因為得在這個階段才能感受到為什麼需要資料庫，所以選擇在這個時候才講。寫給工程師看的話，這兩篇大概就略過了。  
[第二十三天：認識資料庫](https://ithelp.ithome.com.tw/articles/10196781)  
[第二十四天：認識資料庫(續)](https://ithelp.ithome.com.tw/articles/10196895)  

### 學習成果的應用
這是大家想看的部分  
[第二十五天：卡米狗學說話](https://ithelp.ithome.com.tw/articles/10197013)  
[第二十六天：卡米狗推齊](https://ithelp.ithome.com.tw/articles/10197128)  
[第二十七天：卡米狗見人說人話，見鬼說鬼話](https://ithelp.ithome.com.tw/articles/10197234)  
[第二十八天：建立管理後台](https://ithelp.ithome.com.tw/articles/10197333)  
[第二十九天：卡米狗發公告](https://ithelp.ithome.com.tw/articles/10197440)  
[第三十天：卡米狗查天氣](https://ithelp.ithome.com.tw/articles/10197544)  

# 關於開發環境
選擇在 windows 上開發 rails，而不是選在 macbook 上開發，是因為我認為大多數一般人家裡沒有 macbook，為了降低進入障礙，所以選擇在 windows 上開發，我的卡米狗從一開始就是在 macbook 上開發的，而在我寫文之前，我沒有用過 windows 開發過 rails。選擇用 windows 開發，在後期確實是導致比較多的障礙。不過讀者們會因為這樣而去安裝 linux 或者買一台 macbook 嗎？
# 關於瀏覽量
老實講，最前面的三篇文章我有在卡米狗上面發公告宣傳，成效不錯。但每次發公告，好友人數就掉1%是蠻傷的，應該要作個訂閱機制，針對那些有在 LINE 上訂閱系列文的人，我再每天 PUSH 就好。不過文章寫到一半也沒那個心力去加功能就是了。不過後面有兩篇莫名4千多，我是懷疑有別人在洗我的瀏覽量。

# 最後
在這裡感謝那些留言給我的人，不論你們是提出問題，或回報錯誤，或感謝我，你們都能幫助到我。之後可能會把在這三十篇裡面沒提到的，關於 Line Messaging API 部分也講一講，像是 [imagemap message](https://developers.line.me/en/docs/messaging-api/message-types/#imagemap-messages) 和 [template message](https://developers.line.me/en/docs/messaging-api/message-types/#carousel-template) 這種比較酷炫的功能。

以下開放許願，我考慮有時間的時候再回來講講。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/7702012040425804945/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/7702012040425804945' title='1 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/7702012040425804945'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/7702012040425804945'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/blog-post_23.html' title='只要有心，人人都可以作卡米狗 - 完賽心得'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-9108543781413891591</id><published>2018-01-18T03:20:00.000+08:00</published><updated>2018-01-18T03:28:25.508+08:00</updated><title type='text'>第三十天：卡米狗查天氣</title><content type='html'>今天就是最後一天惹，有些事情想跟你們講一下，那就是我們前幾天到底在幹嘛。

以下是一些示意圖，說明我們的 HTTP request 傳遞的路徑。

# 回覆訊息

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjIBBLpwmEsBflGznxGYNV4CLgnla3Lh4AhGHHzDm7Y6W5DOjq2wg30F-gixpI6LsMvRfl6HaItAng8R8T5T54c9skSRBFmh72com1rUaAG6rXWbsKN4GT7vn8ebUxUhS3Xb2oGwUCUuZI/s1600/1.jpg)

Line app 指的是手機或PC版的 Line，Line server 在收到訊息後會透過 webhook url 傳遞給我們。接著我們會打 `line.reply_message` 傳訊息給 Line server，最後再由 Line server 傳給 Line app (最後這段可能不是 HTTP request)。

# 發公告

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8T3lFxHxi4rhSMw9ndgSkirez5iQwkfbfGwicUQHLHILsTqN0EseIx5G8bxVRmIc1VK2yhzL0dxMePFlE-B6UcQfjd2jRLBl3bVafCaVEXLmWYQa7fyM0BnONZXXVRZPDenzCHGEtUV0/s1600/2.jpg)

我們透過後台管理介面填入公告訊息，用 `line.push_message` 傳訊息給 Line server。

# 排程公告

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhinS59WbIvMzo1jg4XW6HPlR4xBzN5OM7ZLsywJXMxVoZ-g6WYpjxXM3o4e8vr3P6zdJD2CK9rFHo0t1xravSHin4pQjp-tK2lKwKV_uJUX02mqZtEGuf0rQJHtJ_iplIs4husKWnGbaA/s1600/3.jpg)

有觀眾說想知道鬧鐘怎麼作，這裡再說明一下。

我們會用到 worker 來處理工作排程。首先是先在後台設定預約發訊息，然後將訊息儲存到工作清單，每個工作可以指定執行時間，接著就等時間到，worker 就會用 `line.push_message` 去打 Line server。

# 查天氣

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPy6B-QhMCDjGqgmspWGgDP5A51NwqV1Jc8J4wgIN4vXWRPa8RxuhpYycIlES-nmpsMiVO-c7OiOM9TBeTBjncQtxEc9gXAo_6vzCQ1YYCL1EHUSdTgZS-DIwMteKD1vtfi54mL_fCius/s1600/4.jpg)

查天氣就更複雜了，我們收到查天氣指令後，要先去氣象局取得圖片檔，然後再把圖上傳到 imgur，最後把圖片連結傳回給 Line server。

為什麼不是直接把氣象局的圖片傳給 Line server 呢？因為 Line server 要求圖檔必須是 https 開頭的網址，但是氣象局的圖檔連結卻是 http 開頭。

那為什麼不是我們自己保存圖片就好呢？因為存圖片要占空間跟頻寬，所以我選擇用 imgur 的空間放圖。imgur 有一個蠻好的地方是，你可以直接把圖片網址給他，他就會幫你備份圖片了，所以我們不用真的把圖檔抓回來再上傳到 imgur。


# 查天氣的運作流程

我們作簡單一點，當有人說到`天氣`的時候就傳回一張雷達回波圖。我們需要作的所有事情是：

調查階段：

- 學會怎麼抓到最新的雷達回波圖網址
- 學會怎麼把圖檔弄到 imgur

實作階段：
- 在主程式呼叫查天氣
- 增加一個查天氣函數
- 增加一個取得最新雷達回波圖的函數
- 增加一個上傳圖片到 imgur 的函數
- 傳送圖片到 line 的函數

一步步來吧。


# 學會怎麼抓到最新的雷達回波圖網址

當然，如果我們是用瀏覽器下載，那麼很簡單直接網頁打開`右鍵`-&gt;`另存圖片`就載好了。可是我們是要用程式去載圖，不是人工載圖。

所以我們要用程式去開啟網頁，然後從網頁原始碼裡面找到圖片連結就行了。

先開這個網頁：[http://www.cwb.gov.tw/V7/observe/radar/](http://www.cwb.gov.tw/V7/observe/radar/)

然後按下 `Ctrl`+`U`，就可以看到網頁原始碼了，把他認真的讀完之後會發現第 234~237 行很可疑，點進去看就會發現全都是圖檔連結，像這樣：[http://www.cwb.gov.tw/V7/js/HDRadar_1000_n_val.js](http://www.cwb.gov.tw/V7/js/HDRadar_1000_n_val.js)。

要能發現第 234~237 行很可疑，你必須要能看懂大部分的 html 跟 js，所以你得學會 html 跟 js。

如果你還沒學過 html 的話，可以參考看看：[深入淺出立即上手的 HTML 網頁設計](https://5xruby.tw/talks/css-html-2018-1)

如果你還沒學過 js 的話，也可以參考看看：[JavaScript &amp; jQuery 前端開發入門實戰](https://5xruby.tw/talks/JS-jQuery-2018-1)

```
var HDRadar_1000_n_val=new Array(
new Array(&quot;2018/01/18 01:20&quot;,&quot;/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180120.png&quot;),
new Array(&quot;2018/01/18 01:10&quot;,&quot;/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180110.png&quot;),
new Array(&quot;2018/01/18 01:00&quot;,&quot;/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180100.png&quot;),
new Array(&quot;2018/01/18 00:50&quot;,&quot;/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180050.png&quot;),
...
```
這是 js 程式碼，我們需要的部分在第二行後半段：`/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180120.png`，這是網頁路徑，省略了網域的寫法。

把網域加回去就會是 [http://www.cwb.gov.tw/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180120.png](http://www.cwb.gov.tw/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180120.png)：

![](http://www.cwb.gov.tw/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180120.png)

這就是我們要的圖片連結。

### 小結
抓 [http://www.cwb.gov.tw/V7/js/HDRadar_1000_n_val.js](http://www.cwb.gov.tw/V7/js/HDRadar_1000_n_val.js) 的原始碼，然後取出第二行的網頁路徑，最後在前面補上 `http://www.cwb.gov.tw` 就會是我們要的網址。

# 學會怎麼把圖檔弄到 imgur

imgur 有提供 api，這是說明文件：[https://apidocs.imgur.com/#4b8da0b3-3e73-13f0-d60b-2ff715e8394f](https://apidocs.imgur.com/#4b8da0b3-3e73-13f0-d60b-2ff715e8394f)。

使用 api 需要 Client-ID，這東西就跟 Line channel secret 那些東西差不多。

你可以透過這個網址：[https://api.imgur.com/oauth2/addclient](https://api.imgur.com/oauth2/addclient) 取得你的 Client-ID。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjer94avY65FjgMHD2HDhxK5GuUvm2ribzg_WSWlw4hx7azCULh3E7N81JzOymhCHracAx72jWM8Ue5oSHaS9saoU1wXpPAfAnCkJmkk5DsSUImBZnp0BsibSAnVK15-_ntYHA9wtJoXU0/s1600/6.jpg)

照著填就可以。

# 小結
透過使用 imgur 提供的 api，我們可以很容易就上傳圖片到 imgur。

接下來是實作階段的部分。

# 在主程式呼叫查天氣
```
  def webhook
    # 查天氣
    reply_image = get_weather(received_text)

    # 有查到的話 後面的事情就不作了
    unless reply_image.nil?
      # 傳送訊息到 line
      response = reply_image_to_line(reply_image)

      # 回應 200
      head :ok

      return 
    end

    # 紀錄頻道
    Channel.find_or_create_by(channel_id: channel_id)

    # 學說話
    reply_text = learn(channel_id, received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(channel_id, received_text) if reply_text.nil?

    # 推齊
    reply_text = echo2(channel_id, received_text) if reply_text.nil?

    # 記錄對話
    save_to_received(channel_id, received_text)
    save_to_reply(channel_id, reply_text)

    # 傳送訊息到 line
    response = reply_to_line(reply_text)

    # 回應 200
    head :ok
  end
```

我在最前面加入了這段程式碼：

```
# 查天氣
reply_image = get_weather(received_text)

# 有查到的話 後面的事情就不作了
unless reply_image.nil?
  # 傳送訊息到 line
  response = reply_image_to_line(reply_image)

  # 回應 200
  head :ok

  return 
end
```

我們要作一個查天氣函數 `get_weather` 如果輸入的文字包含`天氣`，就傳回 https 的雷達回波圖網址，然後就將圖片傳回給 line，這裡因為之前都是傳文字而已，所以還要多作一個函數 `reply_image_to_line` 來傳圖片。

# 增加一個查天氣函數

```
  def get_weather(received_text)
    return nil unless received_text.include? &#39;天氣&#39;
    imgur(get_weather_from_cwb)
  end
```

第一行是說如果輸入的文字不包含天氣，就傳回 nil。

第二行呼叫了兩個函數，第一個函數是 `get_weather_from_cwb`，這是取得雷達回波圖的函數，會得到一個網址，再把這個網址傳給 `upload_to_imgur` 這個上傳圖片到 imgur 的函數。


# 增加一個取得最新雷達回波圖的函數

在[第十六天：做一個最簡單的爬蟲](ttps://ithelp.ithome.com.tw/articles/10195760)學到的在 rails 發 HTTP request 跟在[第二十五天：卡米狗學說話](https://ithelp.ithome.com.tw/articles/10197013)學到的字串處理又要派上用場了，就跟你說前面的文章都是在打基礎吧，漏掉一篇你就做不出來了。

```
  def get_weather_from_cwb
    uri = URI(&#39;http://www.cwb.gov.tw/V7/js/HDRadar_1000_n_val.js&#39;)
    response = Net::HTTP.get(uri)
    start_index = response.index(&#39;&quot;,&quot;&#39;) + 3
    end_index = response.index(&#39;&quot;),&#39;) - 1
    &quot;http://www.cwb.gov.tw&quot; + response[start_index..end_index]
  end
```
前兩行就是第十六天講過的，後三行就是第二十五天講過的。比較難懂的可能會是第三行跟第四行，先看一下這張圖：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0ou1oNmbu3Vsu744XKQ6rLhJwcb5HOUktAAbRDXY6UAAfZZg5dOb4hxcZBPh47j0gk6LL_ecyO7bRN8lW9ooM9vnoL61k3dPnOAdCwdmyoN0_gnz-a0j3PzYku3GlHlKE7o-DB9Ed_7U/s1600/5.jpg)

總而言之就是網址的開頭前面是 `&quot;,&quot;` 後面是 `&quot;),` 如果你有學過 js 應該就會知道，這個開頭跟結尾應該是不會錯的，所以我們決定取出介於這中間的字。

這行是在抓起點：
```
start_index = response.index(&#39;&quot;,&quot;&#39;) + 3
```
這是在抓終點：
```
end_index = response.index(&#39;&quot;),&#39;) - 1
```

# 增加一個上傳圖片到 imgur 的函數

```
  def upload_to_imgur(image_url)
    url = URI(&quot;https://api.imgur.com/3/image&quot;)
    http = Net::HTTP.new(url.host, url.port)
    http.use_ssl = true
    request = Net::HTTP::Post.new(url)
    request[&quot;authorization&quot;] = &#39;Client-ID be2d83405627ab8&#39;

    request.set_form_data({&quot;image&quot; =&gt; image_url})
    response = http.request(request)
    json = JSON.parse(response.read_body)
    begin
      json[&#39;data&#39;][&#39;link&#39;].gsub(&quot;http:&quot;,&quot;https:&quot;)
    rescue
      nil
    end
  end
```
我們設定好 request header 和 request body 之後打一個 post request 出去，他會返回一個 json，接著我作了 json 的解析，並且在解析失敗時傳回 nil，確保程式不會隨意掛點。

```
request[&quot;authorization&quot;] = &#39;Client-ID be2d83405627ab8&#39;
```
這行是要填入你自己的 Client-ID，`be2d83405627ab8` 是我亂打的。

# 傳送圖片到 line 的函數

```
  # 傳送圖片到 line
  def reply_image_to_line(reply_image)
    return nil if reply_image.nil?
    
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 設定回覆訊息
    message = {
      type: &quot;image&quot;,
      originalContentUrl: reply_image,
      previewImageUrl: reply_image
    }

    # 傳送訊息
    line.reply_message(reply_token, message)
  end
```

其實跟傳文字幾乎一樣，只差在 message 裡面不一樣而已。

# 上傳實測

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhS1WcxYPIl9VoOutCXhiiZtKxCbxAf1IR3fhzmnYiJ6LJU190f9GFfZOEFf9_xNnjQTL6qr0_odQiqQ0OXxrg6EdRmoXY3LQIJdODgq2C1zH1VylSCSnnOZ6XXzwZMyktsmpYnYkiVPf0/s1600/7.jpg)

成功！

# 本日重點

- 學會抓雷達回波圖
- 學會用 imgur api
- 學會傳圖片到 line
- 想要學會作爬蟲，就要學會 html 跟 js
- 如果你還沒學過 html 的話，可以參考看看：[深入淺出立即上手的 HTML 網頁設計](https://5xruby.tw/talks/css-html-2018-1)
- 如果你還沒學過 js 的話，也可以參考看看：[JavaScript &amp; jQuery 前端開發入門實戰](https://5xruby.tw/talks/JS-jQuery-2018-1)

如果你原本是完全不會寫程式，你從第一篇一直看到這篇，最後有作出東西的話，請在底下留言：「感恩卡米，讚嘆卡米」，讓我能證明`只要有心，人人都可以作卡米狗`是真的。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/9108543781413891591/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/9108543781413891591' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/9108543781413891591'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/9108543781413891591'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/blog-post_18.html' title='第三十天：卡米狗查天氣'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-5263781124277430214</id><published>2018-01-17T22:12:00.000+08:00</published><updated>2018-01-17T22:12:03.402+08:00</updated><title type='text'>Rails - Windows 上會遇到的 LoadError (cannot load such file -- bcrypt_ext) 問題</title><content type='html'># 什麼時候會遇到這個問題？

當你使用任何需要加密功能的套件時，比方說 Devise。

# 成因
安裝了不能在 windows 下正常執行的 bcrypt 套件。

# 解法

先解除安裝所有 bcrypt
```
gem uninstall bcrypt-ruby
gem uninstall bcrypt
```

再安裝正確版本

```
gem install bcrypt --platform=ruby
```

你的 Gemfile 應該加入這行
```
gem &#39;bcrypt&#39;, &#39;~&gt; 3.1.11&#39;
```

# 參考連結

[https://github.com/codahale/bcrypt-ruby/issues/142#issuecomment-291345799](https://github.com/codahale/bcrypt-ruby/issues/142#issuecomment-291345799)</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/5263781124277430214/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/5263781124277430214' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/5263781124277430214'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/5263781124277430214'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/rails-windows-loaderror-cannot-load.html' title='Rails - Windows 上會遇到的 LoadError (cannot load such file -- bcrypt_ext) 問題'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-6282463208809819896</id><published>2018-01-17T02:07:00.003+08:00</published><updated>2018-01-17T02:07:32.335+08:00</updated><title type='text'>第二十九天：卡米狗發公告</title><content type='html'>今天我們要作的是主動傳訊息的功能。

目前我們用到的都只是回覆訊息的功能：

# 認識 Push Message API

```
  # 傳送訊息到 line
  def reply_to_line(reply_text)
    return nil if reply_text.nil?
    
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: reply_text
    } 

    # 傳送訊息
    line.reply_message(reply_token, message)
  end
```

上面這個函數是我們之前寫好的 `reply_to_line` 函數，裡面的最後一行：

```
line.reply_message(reply_token, message)
```

這是在呼叫 line 提供給我們的回覆訊息函數，而 line 也有提供讓我們主動發訊息的函數：

```
response = line.push_message(channel_id, message)
```

我們需要傳遞 channel_id，告訴 Line 誰應該收到這個訊息，channel_id 就是 userId, groupId 或 roomId。所以我們需要一個資料模型去保存所有頻道的 channel_id。

文件參考在這裡：[https://developers.line.me/en/docs/messaging-api/reference/#send-push-message](https://developers.line.me/en/docs/messaging-api/reference/#send-push-message)

# 保存所有頻道

### 建立資料模型

```
rails g model channel channel_id
```

建立一個資料表叫作 channel，裡面有個欄位叫作 channel_id。

### 資料庫遷移

```
rails db:migrate
```
bj4

### 儲存頻道

在主程式中加入一行：

```
Channel.create(channel_id: channel_id)
```
如果你覺得是這樣寫，那你就錯了，因為這樣會導致相同的資料會一直被存進去，到時候你發公告，同一個人就會收到超多次。

```
Channel.find_or_create_by(channel_id: channel_id)
```

先看有沒有相同的資料，如果已經有資料的話就不寫入。如果沒有資料才作寫入。這邊有詳細的說明：[https://rails.ruby.tw/active_record_querying.html#find-or-create-by](https://rails.ruby.tw/active_record_querying.html#find-or-create-by)

加入後的主程式長這樣：

```
  def webhook
    # 紀錄頻道
    Channel.find_or_create_by(channel_id: channel_id)

    # 學說話
    reply_text = learn(channel_id, received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(channel_id, received_text) if reply_text.nil?

    # 推齊
    reply_text = echo2(channel_id, received_text) if reply_text.nil?

    # 記錄對話
    save_to_received(channel_id, received_text)
    save_to_reply(channel_id, reply_text)

    # 傳送訊息到 line
    response = reply_to_line(reply_text)

    # 回應 200
    head :ok
  end
```

接下來要作一個後台網頁去發這個公告。我們會需要兩個 Action，一個 get new Action 用來顯示發公告的後台頁面，另一個 post create Action 用來接收發公告訊息的請求。

# 管理後台

這次我們手動新增，不使用產生器。

### 加 Route

修改 `config/routes.rb`，新增一行：

```
resources :push_messages, only: [:new, :create]
```
這是加入一組資源，但我們只使用其中的 new 和 create。

### 加 Controller

在 `app/controllers` 資料夾下建立一個叫作 `push_messages_controller.rb` 的檔案：
```
class PushMessagesController &lt; ApplicationController
  before_action :authenticate_user!

  # GET /push_messages/new
  def new
  end

  # POST /push_messages
  def create
  end
end
```

我們檢查使用者必須先登入，然後開了兩個空的 Action，之後再回頭來改。

### 加 View

我們要在 `app/views/push_messages` 下新增一個檔案 `new.html.erb`。

這是 `new.html.erb` 所需要的全部程式碼：

```
&lt;%= form_with(url: &#39;/push_messages&#39;, local: true) do |form| %&gt;
  &lt;%= text_area_tag &#39;text&#39; %&gt;
  &lt;%= submit_tag &quot;送出&quot; %&gt;
&lt;% end %&gt;
```

一個表單的開始是 `&lt;%= form_with ..... do ... %&gt;`，結束是 `&lt;% end %&gt;`。表單預設是用 post 方法，所以就不用特別寫出來。

`&lt;%= text_area_tag &#39;text&#39; %&gt;` 是輸入文字框。

`&lt;%= submit_tag &quot;送出&quot; %&gt;` 則是送出按鈕。

如果要了解更多的話可以參考：[Action View 表單輔助方法](https://rails.ruby.tw/form_helpers.html)

### 改 Controller

我們在接收到請求之後要作發訊息的動作：

```
  def create
    text = params[:text]
    Channel.all.each do |channel|
      push_to_line(channel.channel_id, text)
    end
    redirect_to &#39;/push_messages/new&#39;
  end
```

`text = params[:text]` 這是取得剛剛在輸入文字框填的文字

`Channel.all.each do |channel|`  ... `end` 這段是指我們想要對每一個 channel 作一些事情。

`push_to_line(channel.channel_id, text)` 這是說我們要主動發訊息 `text` 給頻道 `channel.channel_id`，這個函數我們待會才會寫。

### push_to_line

```
  # 傳送訊息到 line
  def push_to_line(channel_id, text)
    return nil if channel_id.nil? or text.nil?
    
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: text
    } 

    # 傳送訊息
    line.push_message(channel_id, message)
  end
```
長得跟之前的 `reply_to_line` 有 87% 像，就不解釋了。

### 對一下程式碼

完整的 `push_messages_controller.rb` 應該長這樣： 

```
require &#39;line/bot&#39;
class PushMessagesController &lt; ApplicationController
  before_action :authenticate_user!

  # GET /push_messages/new
  def new
  end

  # POST /push_messages
  def create
    text = params[:text]
    Channel.all.each do |channel|
      push_to_line(channel.channel_id, text)
    end
    redirect_to &#39;/push_messages/new&#39;
  end

  # 傳送訊息到 line
  def push_to_line(channel_id, text)
    return nil if channel_id.nil? or text.nil?
    
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: text
    } 

    # 傳送訊息
    line.push_message(channel_id, message)
  end

  # Line Bot API 物件初始化
  def line
    @line ||= Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
  end
end
```

# 發布和測試

自己試試看，你們需要練習自己解決卡關，要養成看 log 的習慣。

# 關於怎麼做鬧鐘

你需要把主動發訊息這件事情加入排程，請參考：[Active Job 基礎](https://rails.ruby.tw/active_job_basics.html#%E4%BB%BB%E5%8B%99%E6%8E%92%E7%A8%8B) 以及 [Delayed Job (DJ)](https://devcenter.heroku.com/articles/delayed-job)。

自己摸這個要有花上一週的心理準備，加油加油加油，你是最棒的，耶！

# 本日重點

- 學會記錄頻道
- 學會主動發訊息
- 學會寫 view 的表單

沒意外的話，明天就講怎麼查天氣。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/6282463208809819896/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/6282463208809819896' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/6282463208809819896'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/6282463208809819896'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/blog-post_17.html' title='第二十九天：卡米狗發公告'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-4183839901423347313</id><published>2018-01-16T00:32:00.000+08:00</published><updated>2018-01-16T00:32:05.387+08:00</updated><title type='text'>第二十八天：建立管理後台</title><content type='html'>應觀眾要求，今天我們作一個管理後台，讓我們可以在網頁上管理關鍵字。

在開始之前，先大概說明一下今天要學習的範圍有哪些：

- 網頁的呈現需要使用 HTML 和 CSS
- 既然是後台，就要作登入功能

我們作的網站到目前為止沒有碰過任何的 HTML 和 CSS，突然要寫個管理後台也許會很吃力。不過還好是作後台，不需要多美觀。

# 使用產生器製作後台

幸好 Rails 有一個內建指令直接生成網頁，不一定要自己寫。

指令是 `rails generate scaffold 資料模型名稱 和欄位們`

```
rails g scaffold keyword_mapping channel_id keyword message --skip
```
後面的 `--skip` 是指定當發生衝突時應該略過。衝突的意思是指 rails 想新增一個檔案，剛好在目錄裡已經有個同名的檔案。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails g scaffold keyword_mapping channel_id keyword message --skip
      invoke  active_record
        skip    db/migrate/20180115144538_create_keyword_mappings.rb
   identical    app/models/keyword_mapping.rb
      invoke    test_unit
   identical      test/models/keyword_mapping_test.rb
        skip      test/fixtures/keyword_mappings.yml
      invoke  resource_route
       route    resources :keyword_mappings
      invoke  scaffold_controller
      create    app/controllers/keyword_mappings_controller.rb
      invoke    erb
      create      app/views/keyword_mappings
      create      app/views/keyword_mappings/index.html.erb
      create      app/views/keyword_mappings/edit.html.erb
      create      app/views/keyword_mappings/show.html.erb
      create      app/views/keyword_mappings/new.html.erb
      create      app/views/keyword_mappings/_form.html.erb
      invoke    test_unit
      create      test/controllers/keyword_mappings_controller_test.rb
      invoke    helper
      create      app/helpers/keyword_mappings_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/keyword_mappings/index.json.jbuilder
      create      app/views/keyword_mappings/show.json.jbuilder
      create      app/views/keyword_mappings/_keyword_mapping.json.jbuilder
      invoke  test_unit
      create    test/system/keyword_mappings_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/keyword_mappings.coffee
      invoke    scss
      create      app/assets/stylesheets/keyword_mappings.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

以下說明到底生成了什麼東西。

# 生成 Routes

他會在 `config/routes.rb` 生成一個 resource：

```
resources :keyword_mappings
```

這是資源(resource)，提供一個資源的存取所需要的網址和 Controller 的對應。

這行會生成 8 組網址與 7 個 Controller Action 的對應，可以使用 `rails routes` 觀察：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails routes
                 Prefix Verb   URI Pattern                          Controller#Action
       keyword_mappings GET    /keyword_mappings(.:format)          keyword_mappings#index
                        POST   /keyword_mappings(.:format)          keyword_mappings#create
    new_keyword_mapping GET    /keyword_mappings/new(.:format)      keyword_mappings#new
   edit_keyword_mapping GET    /keyword_mappings/:id/edit(.:format) keyword_mappings#edit
        keyword_mapping GET    /keyword_mappings/:id(.:format)      keyword_mappings#show
                        PATCH  /keyword_mappings/:id(.:format)      keyword_mappings#update
                        PUT    /keyword_mappings/:id(.:format)      keyword_mappings#update
                        DELETE /keyword_mappings/:id(.:format)      keyword_mappings#destroy
             kamigo_eat GET    /kamigo/eat(.:format)                kamigo#eat
 kamigo_request_headers GET    /kamigo/request_headers(.:format)    kamigo#request_headers
    kamigo_request_body GET    /kamigo/request_body(.:format)       kamigo#request_body
kamigo_response_headers GET    /kamigo/response_headers(.:format)   kamigo#response_headers
   kamigo_response_body GET    /kamigo/response_body(.:format)      kamigo#show_response_body
    kamigo_sent_request GET    /kamigo/sent_request(.:format)       kamigo#sent_request
         kamigo_webhook POST   /kamigo/webhook(.:format)            kamigo#webhook
```
7 個 Action 分別為 index, create, new, edit, show, update, destroy，接下來說明各個 Action 的功能：

以下屬於 GET request，這些都是網頁：
- index：列表頁
- new：新增資料頁
- show：檢視資料頁
- edit：編輯資料頁

以下非 GET request，都是請求資料變更：
- create：請求新增資料
- update：請求更新資料
- destroy：請求刪除資料

# 生成 Controller

生成了一個 Controller 在：`app/controllers/keyword_mappings_controller.rb`。7 個對應的 Action 都寫好了，這裡就不多介紹。

# 生成 View

生成了一整個資料夾的 View，其中最重要的 4 個：

```
app/views/keyword_mappings/index.html.erb
app/views/keyword_mappings/edit.html.erb
app/views/keyword_mappings/show.html.erb
app/views/keyword_mappings/new.html.erb
```

這就是那些 GET request 會用到的網頁檔，也都寫好了。

# 實測

既然都寫好了就來試用看看，先執行網頁伺服器：

```
rails s
```

然後開啟網頁 [http://localhost:3000/keyword_mappings](http://localhost:3000/keyword_mappings)：

### index 列表頁

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEist8IdgbyY3iAIGlOyofKz1eCnlRALx4SRUXGxlWhdzor1anPL8AvYbf9NuWaOJcmEr3pZLcr0eqLOJG_QNoxpmzsQPQNm5RDYPVXUukTpM4A7TGRsKqx_U_Gbh57VDIKGNLtBt0oIMNM/s1600/1.jpg)

### new 新增資料頁 

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhAQEbyfGqL9bOS5Jii-T2ovBPGBvgninU-HovOPbFaU3sQcKcaRWII3sPjce9droyLKCJEzKbaE9Bsv2n2JknSVhXTF99JMZmyFHiyXgYb6HamLGq5pm_D420ZdboEkF4blIJYFK5Whb0/s1600/2.jpg)

隨便亂填：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-1GgB1iT6XwiUq92wHqLCYcn5gbDaS6AJqhO3qHQT3BubyklAHFTq0Q3bz-lNH6dOiZxW5Kejc5BYcT2yHKl_sxSobzHpE1RRGTWL3Yq5T2CaOgyPt-yFoVjAz8Rzox-Y3SKWrfSIaCM/s1600/3.jpg)

### show 檢視資料頁

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjCEGVY_kVAgDVOCwh5BSo9g3DEpfqcl9OUG3l3cN-NKNDHQe15EfGIndU3xtydKWlb4SoY3t4pwsmXf7F8aVovY1G16_ft24cIzhMXDSMle8UmjxU8EmK6W7KubR7b7l7-FmoW9rPe1KQ/s1600/4.jpg)

所以其實不考慮美觀性的話，其實後台只要一個指令就完成了。



# 建立登入功能

使用知名套件 [devise](https://github.com/plataformatec/devise) 來作。

我相信現在的你如果沒有英文閱讀障礙，應該已經能看懂這個[使用說明](https://github.com/plataformatec/devise#getting-started)。

總而言之先在 Gemfile 加這行：

```
gem &#39;devise&#39;
```

然後在小黑框打 `bundle install` 安裝套件。

裝好之後使用 devise 提供的產生器指令進行初始化： `rails generate devise:install`。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails generate devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven&#39;t yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: &#39;localhost&#39;, port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: &quot;home#index&quot;

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       &lt;p class=&quot;notice&quot;&gt;&lt;%= notice %&gt;&lt;/p&gt;
       &lt;p class=&quot;alert&quot;&gt;&lt;%= alert %&gt;&lt;/p&gt;

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```
他說有幾個步驟產生器沒搞頭，必須手動進行。我們先不管他，等到真正出問題再回頭來解決。

使用產生器產生用戶資料模型：

```
rails generate devise user
```

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails generate devise user
      invoke  active_record
      create    db/migrate/20180115152537_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```
跟剛剛的 scaffold 差不多，該生的都生好了。

註冊頁：[http://localhost:3000/users/sign_up](http://localhost:3000/users/sign_up)
登入頁：[http://localhost:3000/users/sign_in](http://localhost:3000/users/sign_in)

# 關閉註冊功能

我們要將註冊功能關閉，如果大家都能註冊，那還要後台幹嘛？

在 `app/models/user.rb`：

```
class User &lt; ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end
```

刪除 `:registerable,`：

```
class User &lt; ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable,
         :recoverable, :rememberable, :trackable, :validatable
end
```

# 登入後才能管理關鍵字

我們希望只有登入後的人才能進入管理關鍵字的頁面。

在 `app/controllers/keyword_mappings_controller.rb` 加入：

```
before_action :authenticate_user!
```

看起來像這樣：

```
class KeywordMappingsController &lt; ApplicationController
  before_action :authenticate_user!
  before_action :set_keyword_mapping, only: [:show, :edit, :update, :destroy]

  ...下略
```

這時候開啟網址：[http://localhost:3000/keyword_mappings](http://localhost:3000/keyword_mappings)，就會因為尚未登入，而被引導至登入頁。

# 發布流程

- 上傳程式碼
- Heroku 上的資料庫遷移

# 關閉了註冊功能後要怎麼新增自己的帳號？

使用 `rails console` 連上去新增帳號：
 
```
heroku run rails console
```

連上後會是 `rails console` 的樣子：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;heroku run rails console
Running rails console on people-all-love-kamigo... up, run.2165 (Free)
Loading production environment (Rails 5.1.4)
irb(main):001:0&gt;
```

寫一行程式碼新增資料：

```
User.create(email:&#39;kamigo.service@gmail.com&#39;, password:&#39;kamigo&#39;)
```

會有一些 SQL 的訊息：

```
irb(main):001:0&gt; User.create(email:&#39;kamigo.service@gmail.com&#39;, password:&#39;kamigo&#39;)
D, [2018-01-15T15:42:28.307402 #4] DEBUG -- :    (6.0ms)  BEGIN
D, [2018-01-15T15:42:28.313291 #4] DEBUG -- :   User Exists (2.1ms)  SELECT  1 AS one FROM &quot;users&quot; WHERE &quot;users&quot;.&quot;email&quot; = $1 LIMIT $2  [[&quot;email&quot;, &quot;kamigo.service@gmail.com&quot;], [&quot;LIMIT&quot;, 1]]
D, [2018-01-15T15:42:28.317361 #4] DEBUG -- :   SQL (1.9ms)  INSERT INTO &quot;users&quot; (&quot;email&quot;, &quot;encrypted_password&quot;, &quot;created_at&quot;, &quot;updated_at&quot;) VALUES ($1, $2, $3, $4) RETURNING &quot;id&quot;  [[&quot;email&quot;, &quot;kamigo.service@gmail.com&quot;], [&quot;encrypted_password&quot;, &quot;$2a$11$EyR.yuDYI3J2s9/Q8Etk5evQzsz2bGAPdvdcr.xmFQbzYbBPQk/kK&quot;], [&quot;created_at&quot;, &quot;2018-01-15 15:42:28.313883&quot;], [&quot;updated_at&quot;, &quot;2018-01-15 15:42:28.313883&quot;]]
D, [2018-01-15T15:42:28.320139 #4] DEBUG -- :    (2.0ms)  COMMIT
=&gt; #&lt;User id: 1, email: &quot;kamigo.service@gmail.com&quot;, created_at: &quot;2018-01-15 15:42:28&quot;, updated_at: &quot;2018-01-15 15:42:28&quot;&gt;
irb(main):002:0&gt;
```

看到倒數第二行：

```
=&gt; #&lt;User id: 1, email: &quot;kamigo.service@gmail.com&quot;, created_at: &quot;2018-01-15 15:42:28&quot;, updated_at: &quot;2018-01-15 15:42:28&quot;&gt;
```

就表示建立好帳號了。

# 線上實測

[https://people-all-love-kamigo.herokuapp.com/keyword_mappings](https://people-all-love-kamigo.herokuapp.com/keyword_mappings)：

大家可以用我的帳號登入看看。

帳號：kamigo.service@gmail.com
密碼：kamigo

# 本日重點

- 學會使用 scaffold
- 學會作登入系統

你們可以透過閱讀 scaffold 產生出來的程式碼來學習 HTML 和 Controller Action 的寫法。這跟學英文一樣，看到不懂的單字就 Google，這單字量還比英文少超多，大概 100~200 個字而已。

明天講怎麼發公告。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/4183839901423347313/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/4183839901423347313' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/4183839901423347313'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/4183839901423347313'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/blog-post_16.html' title='第二十八天：建立管理後台'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-6318795475437617842</id><published>2018-01-15T17:04:00.001+08:00</published><updated>2018-01-15T17:38:36.241+08:00</updated><title type='text'>Rails - 多檔上傳</title><content type='html'>假設有一個 Controller 叫做 `imgur`，大概是這樣：
```
rails g scaffold imgur pictures
```
而 Controller 內的 params 只允許傳遞 pictures 陣列：
```
def imgur_params
  params.require(:imgur).permit(pictures: [])
end
```

一個最基本的檔案上傳表單長這樣：

```
&lt;%= form_for(imgur) do |f| %&gt;
  &lt;%= f.file_field :pictures %&gt;
  &lt;%= f.submit %&gt;
&lt;% end %&gt;
```

會因為 pictures 只吃陣列的關係，就傳不進去。如果想要上傳多個檔案，要加上 `multiple: true`：

```
&lt;%= form_for(imgur) do |f| %&gt;
  &lt;%= f.file_field :pictures, multiple: true %&gt;
  &lt;%= f.submit %&gt;
&lt;% end %&gt;
```

可以使用多個 f.file_field 來做上傳：

```
&lt;%= form_for(imgur) do |f| %&gt;
  &lt;%= f.file_field :pictures, multiple: true %&gt;
  &lt;%= f.file_field :pictures, multiple: true %&gt;
  &lt;%= f.submit %&gt;
&lt;% end %&gt;
```

如果想要改用 `file_field_tag` 的話，就必須在 `form_for` 加上 `multipart: true`：

```
&lt;%= form_for(imgur, html: { multipart: true }) do |f| %&gt;
    &lt;%= file_field_tag &quot;imgur[pictures][]&quot; %&gt;
    &lt;%= f.submit %&gt;
&lt;% end %&gt;
```

加上 [] 之後，即使只傳一個檔也能通過 `params.permit`。

如果想要多檔上傳：

```
&lt;%= form_for(imgur, html: { multipart: true }) do |f| %&gt;
    &lt;%= file_field_tag &quot;imgur[pictures][]&quot;, multiple: true %&gt;
    &lt;%= f.submit %&gt;
&lt;% end %&gt;
```
也可以這樣：
```
&lt;%= form_for(imgur, html: { multipart: true }) do |f| %&gt;
    &lt;%= file_field_tag &quot;imgur[pictures][]&quot; %&gt;
    &lt;%= file_field_tag &quot;imgur[pictures][]&quot; %&gt;
    &lt;%= f.submit %&gt;
&lt;% end %&gt;
```
或者這樣：
```
&lt;%= form_for(imgur, html: { multipart: true }) do |f| %&gt;
    &lt;%= file_field_tag &quot;imgur[pictures][]&quot;, multiple: true %&gt;
    &lt;%= file_field_tag &quot;imgur[pictures][]&quot;, multiple: true %&gt;
    &lt;%= f.submit %&gt;
&lt;% end %&gt;
```

</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/6318795475437617842/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/6318795475437617842' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/6318795475437617842'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/6318795475437617842'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/rails-filefieldtag.html' title='Rails - 多檔上傳'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-4590834030759646297</id><published>2018-01-15T01:43:00.000+08:00</published><updated>2018-01-15T01:43:40.583+08:00</updated><title type='text'>第二十七天：卡米狗見人說人話，見鬼說鬼話</title><content type='html'>在[第二天：認識卡米狗](https://ithelp.ithome.com.tw/articles/10192575)提到過，見人說人話，見鬼說鬼話功能是考慮到多個群組都教了相同的關鍵字時，卡米狗應該在每個群組做出不同的回應，這樣才不會被討厭，於是就加入了這樣的功能。當有人說「姆咪姆咪」時，卡米狗會先檢查這個群組有沒有人教過看到「姆咪姆咪」要回應，如果教過多次，就回應最後一次學過的內容，如果都沒學過，那麼就再檢查其他群組有沒有學過「姆咪姆咪」。

也就是說，學說話指令在儲存時，應該也要儲存是在哪個頻道學會的。

# 修改學說話指令

目前的學說話指令：

```
  # 學說話
  def learn(received_text)
    #如果開頭不是 卡米狗學說話; 就跳出
    return nil unless received_text[0..6] == &#39;卡米狗學說話;&#39;
    
    received_text = received_text[7..-1]
    semicolon_index = received_text.index(&#39;;&#39;)

    # 找不到分號就跳出
    return nil if semicolon_index.nil?

    keyword = received_text[0..semicolon_index-1]
    message = received_text[semicolon_index+1..-1]

    KeywordMapping.create(keyword: keyword, message: message)
    &#39;好哦～好哦～&#39;
  end
```

應該要改成這樣：

```  
  # 學說話
  def learn(channel_id, received_text)
    ...略
    KeywordMapping.create(channel_id: channel_id, keyword: keyword, message: message)
    ...略
  end
```

重點是多傳入一個參數 `channel_id`，然後存入 `KeywordMapping`。

# 修改關鍵字回覆

在關鍵字回覆的部分，原本是：
```
  # 關鍵字回覆
  def keyword_reply(received_text)
    KeywordMapping.where(keyword: received_text).last&amp;.message
  end
```

則是改為：

```
  # 關鍵字回覆
  def keyword_reply(channel_id, received_text)
    message = KeywordMapping.where(channel_id: channel_id, keyword: received_text).last&amp;.message
    return message unless message.nil?
    KeywordMapping.where(keyword: received_text).last&amp;.message
  end
```

多加了這兩行：
```
message = KeywordMapping.where(channel_id: channel_id, keyword: received_text).last&amp;.message
return message unless message.nil?
```
這兩行的意思是，先找同一個頻道內教過的關鍵字，如果有找到的話就直接回傳。

如果你想要深入學習資料模型的查詢，官方也有提供中文版的說明文件，在這裡：[Active Record 查詢](https://rails.ruby.tw/active_record_querying.html#%E6%A2%9D%E4%BB%B6)。

# 主程式

要記得把參數也傳給剛剛改好的函數，原本的主程式是這樣：

```
  def webhook
    # 學說話
    reply_text = learn(received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(received_text) if reply_text.nil?

    # 推齊
    reply_text = echo2(channel_id, received_text) if reply_text.nil?

    # 記錄對話
    save_to_received(channel_id, received_text)
    save_to_reply(channel_id, reply_text)

    # 傳送訊息到 line
    response = reply_to_line(reply_text)

    # 回應 200
    head :ok
  end
```

要改成：

```
  def webhook
    # 學說話
    reply_text = learn(channel_id, received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(channel_id, received_text) if reply_text.nil?

    ...略
  end
```


這樣就作完了嗎！？

還沒呢，我們的 `KeywordMapping` 根本沒有 `channel_id` 欄位呀！

# 在 KeywordMapping 資料模型中新增欄位

我們需要使用資料庫遷移的方式來對 KeywordMapping 新增欄位，首先要先建立一個資料庫遷移檔。

指令是 `rails generate migration` 加上註解：
```
rails generate migration add_channel_id_to_keyword_reply
```

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails generate migration add_channel_id_to_keyword_reply
      invoke  active_record
      create    db/migrate/20180114163555_add_channel_id_to_keyword_reply.rb

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

生成了一個檔案在 `db/migrate` 裡面，我們要在這個資料庫遷移檔裡打一點字，這是他目前的樣子：

```
class AddChannelIdToKeywordReply &lt; ActiveRecord::Migration[5.1]
  def change
  end
end
```

要加一個欄位的話，要這樣寫：

```
class AddChannelIdToKeywordReply &lt; ActiveRecord::Migration[5.1]
  def change
    add_column :keyword_mappings, :channel_id, :string
  end
end
```

在 `add_column` 後面第一個參數是`資料表名稱`，第二個參數是`要新增的欄位名稱`，以及第三個參數：`要新增的欄位格式`。欄位格式的話，沒意外通常都會是 `:string`。

這裡寫好之後存檔，就可以作資料庫遷移了。為什麼我知道是這樣寫呢？文件在這裡：[Active Record 遷移](https://rails.ruby.tw/active_record_migrations.html#%E6%96%B0%E5%BB%BA%E7%8D%A8%E7%AB%8B%E7%9A%84%E9%81%B7%E7%A7%BB)

# 資料庫遷移

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails db:migrate
== 20180114163555 AddChannelIdToKeywordReply: migrating =======================
-- add_column(:keyword_mappings, :channel_id, :string)
   -&gt; 0.0012s
== 20180114163555 AddChannelIdToKeywordReply: migrated (0.0020s) ==============


D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

如果資料庫遷移檔沒打錯字的話，就會看到這個結果。

# 進行實測

首先上傳程式碼，要養成開著 `heroku logs -t` 的習慣。

測了一下會發現：

```
2018-01-14T16:56:36.918562+00:00 app[web.1]: I, [2018-01-14T16:56:36.918392 #4]  INFO -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] Started POST &quot;/kamigo/webhook&quot; for 203.104.146.154 at 2018-01-14 16:56:36 +0000
2018-01-14T16:56:36.920295+00:00 app[web.1]: I, [2018-01-14T16:56:36.920209 #4]  INFO -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] Processing by KamigoController#webhook as */*
2018-01-14T16:56:36.920486+00:00 app[web.1]: I, [2018-01-14T16:56:36.920397 #4]  INFO -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c]   Parameters: {&quot;events&quot;=&gt;[{&quot;type&quot;=&gt;&quot;message&quot;, &quot;replyToken&quot;=&gt;&quot;bffeaf21d2b64743b3268bd177ebbaff&quot;, &quot;source&quot;=&gt;{&quot;userId&quot;=&gt;&quot;Uc68d82df46b7899e7d716f396ae8e91a&quot;, &quot;type&quot;=&gt;&quot;user&quot;}, &quot;timestamp&quot;=&gt;1515948996430, &quot;message&quot;=&gt;{&quot;type&quot;=&gt;&quot;text&quot;, &quot;id&quot;=&gt;&quot;7310568889858&quot;, &quot;text&quot;=&gt;&quot;A&quot;}}], &quot;kamigo&quot;=&gt;{&quot;events&quot;=&gt;[{&quot;type&quot;=&gt;&quot;message&quot;, &quot;replyToken&quot;=&gt;&quot;bffeaf21d2b64743b3268bd177ebbaff&quot;, &quot;source&quot;=&gt;{&quot;userId&quot;=&gt;&quot;Uc68d82df46b7899e7d716f396ae8e91a&quot;, &quot;type&quot;=&gt;&quot;user&quot;}, &quot;timestamp&quot;=&gt;1515948996430, &quot;message&quot;=&gt;{&quot;type&quot;=&gt;&quot;text&quot;, &quot;id&quot;=&gt;&quot;7310568889858&quot;, &quot;text&quot;=&gt;&quot;A&quot;}}]}}
2018-01-14T16:56:36.920998+00:00 app[web.1]: W, [2018-01-14T16:56:36.920917 #4]  WARN -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] Can&#39;t verify CSRF token authenticity.
2018-01-14T16:56:36.925356+00:00 app[web.1]: D, [2018-01-14T16:56:36.925257 #4] DEBUG -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c]   KeywordMapping Load (1.6ms)  SELECT  &quot;keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; WHERE &quot;keyword_mappings&quot;.&quot;channel_id&quot; = $1 AND &quot;keyword_mappings&quot;.&quot;keyword&quot; = $2 ORDER BY &quot;keyword_mappings&quot;.&quot;id&quot; DESC LIMIT $3  [[&quot;channel_id&quot;, &quot;Uc68d82df46b7899e7d716f396ae8e91a&quot;], [&quot;keyword&quot;, &quot;A&quot;], [&quot;LIMIT&quot;, 1]]
2018-01-14T16:56:36.925763+00:00 app[web.1]: I, [2018-01-14T16:56:36.925658 #4]  INFO -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] Completed 500 Internal Server Error in 5ms (ActiveRecord: 1.6ms)
2018-01-14T16:56:36.927283+00:00 app[web.1]: F, [2018-01-14T16:56:36.927195 #4] FATAL -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c]
2018-01-14T16:56:36.927428+00:00 app[web.1]: F, [2018-01-14T16:56:36.927362 #4] FATAL -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR:  column keyword_mappings.channel_id does not exist
2018-01-14T16:56:36.927431+00:00 app[web.1]: LINE 1: ...keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; WHERE &quot;keyword_m...
2018-01-14T16:56:36.927432+00:00 app[web.1]:                                                              ^
2018-01-14T16:56:36.927438+00:00 app[web.1]: : SELECT  &quot;keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; WHERE &quot;keyword_mappings&quot;.&quot;channel_id&quot; = $1 AND &quot;keyword_mappings&quot;.&quot;keyword&quot; = $2 ORDER BY &quot;keyword_mappings&quot;.&quot;id&quot; DESC LIMIT $3):
2018-01-14T16:56:36.927567+00:00 app[web.1]: F, [2018-01-14T16:56:36.927496 #4] FATAL -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c]
2018-01-14T16:56:36.927701+00:00 app[web.1]: F, [2018-01-14T16:56:36.927580 #4] FATAL -- : [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] app/controllers/kamigo_controller.rb:82:in `keyword_reply&#39;
2018-01-14T16:56:36.927702+00:00 app[web.1]: [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] app/controllers/kamigo_controller.rb:10:in `webhook&#39;
2018-01-14T16:56:36.929306+00:00 heroku[router]: at=info method=POST path=&quot;/kamigo/webhook&quot; host=people-all-love-kamigo.herokuapp.com request_id=2a0784f2-c2b7-46c1-818e-5e5dd799e64c fwd=&quot;203.104.146.154&quot; dyno=web.1 connect=0ms service=11ms status=500 bytes=1827 protocol=https
```

我先把前面那些多餘的字移除：

```
Started POST &quot;/kamigo/webhook&quot; for 203.104.146.154 at 2018-01-14 16:56:36 +0000
Processing by KamigoController#webhook as */*
  Parameters: {&quot;events&quot;=&gt;[{&quot;type&quot;=&gt;&quot;message&quot;, &quot;replyToken&quot;=&gt;&quot;bffeaf21d2b64743b3268bd177ebbaff&quot;, &quot;source&quot;=&gt;{&quot;userId&quot;=&gt;&quot;Uc68d82df46b7899e7d716f396ae8e91a&quot;, &quot;type&quot;=&gt;&quot;user&quot;}, &quot;timestamp&quot;=&gt;1515948996430, &quot;message&quot;=&gt;{&quot;type&quot;=&gt;&quot;text&quot;, &quot;id&quot;=&gt;&quot;7310568889858&quot;, &quot;text&quot;=&gt;&quot;A&quot;}}], &quot;kamigo&quot;=&gt;{&quot;events&quot;=&gt;[{&quot;type&quot;=&gt;&quot;message&quot;, &quot;replyToken&quot;=&gt;&quot;bffeaf21d2b64743b3268bd177ebbaff&quot;, &quot;source&quot;=&gt;{&quot;userId&quot;=&gt;&quot;Uc68d82df46b7899e7d716f396ae8e91a&quot;, &quot;type&quot;=&gt;&quot;user&quot;}, &quot;timestamp&quot;=&gt;1515948996430, &quot;message&quot;=&gt;{&quot;type&quot;=&gt;&quot;text&quot;, &quot;id&quot;=&gt;&quot;7310568889858&quot;, &quot;text&quot;=&gt;&quot;A&quot;}}]}}
Can&#39;t verify CSRF token authenticity.
  KeywordMapping Load (1.6ms)  SELECT  &quot;keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; WHERE &quot;keyword_mappings&quot;.&quot;channel_id&quot; = $1 AND &quot;keyword_mappings&quot;.&quot;keyword&quot; = $2 ORDER BY &quot;keyword_mappings&quot;.&quot;id&quot; DESC LIMIT $3  [[&quot;channel_id&quot;, &quot;Uc68d82df46b7899e7d716f396ae8e91a&quot;], [&quot;keyword&quot;, &quot;A&quot;], [&quot;LIMIT&quot;, 1]]
Completed 500 Internal Server Error in 5ms (ActiveRecord: 1.6ms)

ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR:  column keyword_mappings.channel_id does not exist
LINE 1: ...keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; WHERE &quot;keyword_m...
                                                             ^
SELECT  &quot;keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; WHERE &quot;keyword_mappings&quot;.&quot;channel_id&quot; = $1 AND &quot;keyword_mappings&quot;.&quot;keyword&quot; = $2 ORDER BY &quot;keyword_mappings&quot;.&quot;id&quot; DESC LIMIT $3):

app/controllers/kamigo_controller.rb:82:in `keyword_reply&#39;
app/controllers/kamigo_controller.rb:10:in `webhook&#39;
at=info method=POST path=&quot;/kamigo/webhook&quot; host=people-all-love-kamigo.herokuapp.com request_id=2a0784f2-c2b7-46c1-818e-5e5dd799e64c fwd=&quot;203.104.146.154&quot; dyno=web.1 connect=0ms service=11ms status=500 bytes=1827 protocol=https
```

我們要關注的重點在：

```
Completed 500 Internal Server Error in 5ms (ActiveRecord: 1.6ms)
```

當你看到 `500 Internal Server Error`，表示程式跑到一半就掛了，掛點原因通常會寫在這個訊息後面。

一個正常的 Log 是長這樣：

```
 Completed 200 OK in 269ms (ActiveRecord: 9.9ms)
```

掛點原因：

```
ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR:  column keyword_mappings.channel_id does not exist
LINE 1: ...keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; WHERE &quot;keyword_m...
                                                             ^
SELECT  &quot;keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; WHERE &quot;keyword_mappings&quot;.&quot;channel_id&quot; = $1 AND &quot;keyword_mappings&quot;.&quot;keyword&quot; = $2 ORDER BY &quot;keyword_mappings&quot;.&quot;id&quot; DESC LIMIT $3):

app/controllers/kamigo_controller.rb:82:in `keyword_reply&#39;
app/controllers/kamigo_controller.rb:10:in `webhook&#39;
```

他說：`ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR:  column keyword_mappings.channel_id does not exist`，意思是 `keyword_mappings` 表格裡面沒有 `channel_id` 這個欄位。

這個叫做 `exception message`。一般來說遇到絕大多數的問題都可以拿 `exception message` 去餵給 google ，就能得到問題的答案。不過看到這裡應該就能猜到是忘記作 Heroku 上的資料庫遷移了。

另外，最後面的那兩行：

```
app/controllers/kamigo_controller.rb:82:in `keyword_reply&#39;
app/controllers/kamigo_controller.rb:10:in `webhook&#39;
```

這個叫做 `stack trace`。

意思是他死在 `kamigo_controller.rb` 的第 `82` 行，是在 `keyword_reply` 方法裡。而為什麼他會跑進這個方法呢？原來是在 `kamigo_controller.rb` 在 `webhook` 方法裡的第 `10` 行的呼叫了 `keyword_reply` 方法。

透過閱讀 `stack trace` 你通常就能夠找到錯誤的根源。

# 在 Heroku 上的資料庫遷移

一如往常：

```
heroku run rake db:migrate
```

如果你明明已經跑了資料庫遷移程式，但他還是找不到新欄位的話，可以試試看重開 heroku server：

```
heroku restart
```

### 正確的測試流程

- 把他邀請進群組 1
- 把他邀請進群組 2
- 在群組 1 教他看到 A 要回答 B
- 在群組 2 教他看到 A 要回答 C
- 在群組 1 說 A 看他是不是回答 B
- 在群組 2 說 A 看他是不是回答 C

應該是順利啦～

# 本日重點

- 學會怎麼對已經存在的資料模型加一個欄位
- 學會見人說人話，見鬼說鬼話的本領
- 學會在 Heoku 上除錯的方法</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/4590834030759646297/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/4590834030759646297' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/4590834030759646297'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/4590834030759646297'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/blog-post_15.html' title='第二十七天：卡米狗見人說人話，見鬼說鬼話'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-4208892180148640392</id><published>2018-01-14T01:51:00.000+08:00</published><updated>2018-01-14T01:55:55.068+08:00</updated><title type='text'>第二十六天：卡米狗推齊</title><content type='html'>今天要作的是卡米狗的推齊功能，也就是當看到有兩次以上有人說出相同的句子，那麼就跟著說的功能。要作到這件事，卡米狗必須要有一點記性才行，所以我們必須記錄每個群組中所發生的對話。當有人說出一句話時，就檢查最近有沒有人也說出相同的話，如果有的話卡米狗就跟著說。

# 使用情境

我們希望的是這樣：

```
B哥：「采瑤生日快樂～～」

小昕：「采瑤生日快樂～～」

卡米狗：「采瑤生日快樂～～」
```

但事實上是這樣：

```
B哥：「采瑤生日快樂～～」

小昕：「采瑤生日快樂～～」

卡米狗：「采瑤生日快樂～～」

毛毛：「采瑤生日快樂～～」

卡米狗：「采瑤生日快樂～～」
```

卡米狗不應該推齊兩次的，因為正常人推齊只會推一次，所以卡米狗要記得自己上次說了什麼。

# 推齊的邏輯

整理了一下之後，我們可以寫一個大概的程式碼如下：

```
def 推齊(channel_id, received_text)
  如果在 channel_id 最近沒人講過 received_text，卡米狗就不回應
  如果在 channel_id 卡米狗上一句回應是 received_text，卡米狗就不回應
  回應 received_text
end
```
這種不能執行的程式碼稱為虛擬碼，是用來表達邏輯、幫助思考和討論用的。

`channel_id` 代表目前的群組、聊天室或私聊的 ID，我們這裡姑且通稱為頻道 ID。

# 修改主程式

這是目前的程式碼：

```
  def webhook
    # 學說話
    reply_text = learn(received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(received_text) if reply_text.nil?

    # 傳送訊息到 line
    response = reply_to_line(reply_text)
    
    # 回應 200
    head :ok
  end 
```

在卡米狗中，推齊功能的順位是最低的，所以我們會把推齊擺在關鍵字回覆的後面。

```
  def webhook
    # 學說話
    reply_text = learn(received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(received_text) if reply_text.nil?

    # 推齊
    reply_text = echo2(channel_id, received_text) if reply_text.nil?

    # 傳送訊息到 line
    response = reply_to_line(reply_text)
    
    # 回應 200
    head :ok
  end 
```

我們還需要記錄對話：

```
  def webhook
    # 學說話
    reply_text = learn(received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(received_text) if reply_text.nil?

    # 推齊
    reply_text = echo2(channel_id, received_text) if reply_text.nil?

    # 記錄對話
    save_to_received(channel_id, received_text)
    save_to_reply(channel_id, reply_text)

    # 傳送訊息到 line
    response = reply_to_line(reply_text)

    # 回應 200
    head :ok
  end 
```

主程式大概就是這樣了。

我們需要實作 `channel_id`、`save_to_received`、`save_to_reply`、`echo2` 這四個函數，並且需要兩個資料模型，分別儲存`收到的對話`以及`回應的對話`。

# 建立資料模型

建立 `received` 資料模型：`rails generate model received channel_id text`
 
```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails generate model received channel_id text
      invoke  active_record
      create    db/migrate/20180113153959_create_receiveds.rb
      create    app/models/received.rb
      invoke    test_unit
      create      test/models/received_test.rb
      create      test/fixtures/receiveds.yml

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

建立 `reply` 資料模型：`rails generate model reply channel_id text`

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails generate model reply channel_id text
      invoke  active_record
      create    db/migrate/20180113154217_create_replies.rb
      create    app/models/reply.rb
      invoke    test_unit
      create      test/models/reply_test.rb
      create      test/fixtures/replies.yml

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

進行資料庫遷移：`rails db:migrate`

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails db:migrate
== 20180113153959 CreateReceiveds: migrating ==================================
-- create_table(:receiveds)
   -&gt; 0.5013s
== 20180113153959 CreateReceiveds: migrated (0.5027s) =========================

== 20180113154217 CreateReplies: migrating ====================================
-- create_table(:replies)
   -&gt; 0.0013s
== 20180113154217 CreateReplies: migrated (0.0024s) ===========================


D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

# 頻道 ID

根據 [Line Messaging API 的文件](https://developers.line.me/en/docs/messaging-api/reference/#common-properties)，我們知道要從 `params[&#39;events&#39;][0][&#39;source&#39;]` 底下去找 `groupId`、`roomId` 或者是 `userId`。

如果對話是發生在群組，`groupId` 就會有值，如果對話是發生在聊天室，`roomId` 就會有值。

所以我們要這樣寫：

```
  # 頻道 ID
  def channel_id
    source = params[&#39;events&#39;][0][&#39;source&#39;]
    return source[&#39;groupId&#39;] unless source[&#39;groupId&#39;].nil?
    return source[&#39;roomId&#39;] unless source[&#39;roomId&#39;].nil?
    source[&#39;userId&#39;]
  end
```

可以浪漫一點：

```
  # 頻道 ID
  def channel_id
    source = params[&#39;events&#39;][0][&#39;source&#39;]
    source[&#39;groupId&#39;] || source[&#39;roomId&#39;] || source[&#39;userId&#39;]
  end
```

# 儲存對話

在儲存前應該先檢查有沒有值，因為 `received_text` 不一定有值。

```
  # 儲存對話
  def save_to_received(channel_id, received_text)
    return if received_text.nil?
    Received.create(channel_id: channel_id, text: received_text)
  end

```

# 儲存回應
```
  # 儲存回應
  def save_to_reply(channel_id, reply_text)
    return if reply_text.nil?
    Reply.create(channel_id: channel_id, text: reply_text)
  end
```

# 推齊

按照我們一開始講的虛擬碼邏輯去寫：

```
  def echo2(channel_id, received_text)
    # 如果在 channel_id 最近沒人講過 received_text，卡米狗就不回應
    recent_received_texts = Received.where(channel_id: channel_id).last(5)&amp;.pluck(:text)
    return nil unless received_text.in? recent_received_texts
    
    # 如果在 channel_id 卡米狗上一句回應是 received_text，卡米狗就不回應
    last_reply_text = Reply.where(channel_id: channel_id).last&amp;.text
    return nil if last_reply_text == received_text

    received_text
  end
```

# 發布
### 對一下程式碼
```
require &#39;line/bot&#39;
class KamigoController &lt; ApplicationController
  protect_from_forgery with: :null_session

  def webhook
    # 學說話
    reply_text = learn(received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(received_text) if reply_text.nil?

    # 推齊
    reply_text = echo2(channel_id, received_text) if reply_text.nil?

    # 記錄對話
    save_to_received(channel_id, received_text)
    save_to_reply(channel_id, reply_text)

    # 傳送訊息到 line
    response = reply_to_line(reply_text)

    # 回應 200
    head :ok
  end 

  # 頻道 ID
  def channel_id
    source = params[&#39;events&#39;][0][&#39;source&#39;]
    source[&#39;groupId&#39;] || source[&#39;roomId&#39;] || source[&#39;userId&#39;]
  end

  # 儲存對話
  def save_to_received(channel_id, received_text)
    return if received_text.nil?
    Received.create(channel_id: channel_id, text: received_text)
  end

  # 儲存回應
  def save_to_reply(channel_id, reply_text)
    return if reply_text.nil?
    Reply.create(channel_id: channel_id, text: reply_text)
  end
  
  def echo2(channel_id, received_text)
    # 如果在 channel_id 最近沒人講過 received_text，卡米狗就不回應
    recent_received_texts = Received.where(channel_id: channel_id).last(5)&amp;.pluck(:text)
    return nil unless received_text.in? recent_received_texts
    
    # 如果在 channel_id 卡米狗上一句回應是 received_text，卡米狗就不回應
    last_reply_text = Reply.where(channel_id: channel_id).last&amp;.text
    return nil if last_reply_text == received_text

    received_text
  end

  # 取得對方說的話
  def received_text
    message = params[&#39;events&#39;][0][&#39;message&#39;]
    message[&#39;text&#39;] unless message.nil?
  end

  # 學說話
  def learn(received_text)
    #如果開頭不是 卡米狗學說話; 就跳出
    return nil unless received_text[0..6] == &#39;卡米狗學說話;&#39;
    
    received_text = received_text[7..-1]
    semicolon_index = received_text.index(&#39;;&#39;)

    # 找不到分號就跳出
    return nil if semicolon_index.nil?

    keyword = received_text[0..semicolon_index-1]
    message = received_text[semicolon_index+1..-1]

    KeywordMapping.create(keyword: keyword, message: message)
    &#39;好哦～好哦～&#39;
  end

  # 關鍵字回覆
  def keyword_reply(received_text)
    KeywordMapping.where(keyword: received_text).last&amp;.message
  end

  # 傳送訊息到 line
  def reply_to_line(reply_text)
    return nil if reply_text.nil?
    
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: reply_text
    } 

    # 傳送訊息
    line.reply_message(reply_token, message)
  end

  # Line Bot API 物件初始化
  def line
    @line ||= Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
  end


  def eat
    render plain: &quot;吃土啦&quot;
  end 

  def request_headers
    render plain: request.headers.to_h.reject{ |key, value|
      key.include? &#39;.&#39;
    }.map{ |key, value|
      &quot;#{key}: #{value}&quot;
    }.sort.join(&quot;\n&quot;)
  end

  def response_headers
    response.headers[&#39;5566&#39;] = &#39;QQ&#39;
    render plain: response.headers.to_h.map{ |key, value|
      &quot;#{key}: #{value}&quot;
    }.sort.join(&quot;\n&quot;)
  end

  def request_body
    render plain: request.body
  end

  def show_response_body
    puts &quot;===這是設定前的response.body:#{response.body}===&quot;
    render plain: &quot;虎哇花哈哈哈&quot;
    puts &quot;===這是設定後的response.body:#{response.body}===&quot;
  end

  def sent_request
    uri = URI(&#39;http://localhost:3000/kamigo/eat&#39;)
    http = Net::HTTP.new(uri.host, uri.port)
    http_request = Net::HTTP::Get.new(uri)
    http_response = http.request(http_request)

    render plain: JSON.pretty_generate({
      request_class: request.class,
      response_class: response.class,
      http_request_class: http_request.class,
      http_response_class: http_response.class
    })
  end

  def translate_to_korean(message)
    &quot;#{message}油~&quot;
  end

end
```

上傳程式碼囉～

### Heroku 上的資料庫遷移

要在上傳程式碼之後才能作資料庫遷移，因為資料庫遷移需要讀取資料庫遷移檔。Heroku 上的資料庫遷移指令是 `heroku run rake db:migrate`：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;heroku run rake db:migrate
Running rake db:migrate on people-all-love-kamigo... up, run.4769 (Free)
D, [2018-01-13T16:57:33.099237 #4] DEBUG -- :    (0.6ms)  SELECT pg_try_advisory_lock(8162367372296191845)
D, [2018-01-13T16:57:33.115389 #4] DEBUG -- :    (2.9ms)  SELECT &quot;schema_migrations&quot;.&quot;version&quot; FROM &quot;schema_migrations&quot; ORDER BY &quot;schema_migrations&quot;.&quot;version&quot; ASC
I, [2018-01-13T16:57:33.116984 #4]  INFO -- : Migrating to CreateReceiveds (20180113153959)
D, [2018-01-13T16:57:33.119682 #4] DEBUG -- :    (0.6ms)  BEGIN
== 20180113153959 CreateReceiveds: migrating ==================================
-- create_table(:receiveds)
D, [2018-01-13T16:57:33.166042 #4] DEBUG -- :    (45.6ms)  CREATE TABLE &quot;receiveds&quot; (&quot;id&quot; bigserial primary key, &quot;channel_id&quot; character varying, &quot;text&quot; character varying, &quot;created_at&quot; timestamp NOT NULL, &quot;updated_at&quot; timestamp NOT NULL)
   -&gt; 0.0463s
== 20180113153959 CreateReceiveds: migrated (0.0464s) =========================

D, [2018-01-13T16:57:33.170513 #4] DEBUG -- :   SQL (0.7ms)  INSERT INTO &quot;schema_migrations&quot; (&quot;version&quot;) VALUES ($1) RETURNING &quot;version&quot;  [[&quot;version&quot;, &quot;20180113153959&quot;]]
D, [2018-01-13T16:57:33.173887 #4] DEBUG -- :    (3.1ms)  COMMIT
I, [2018-01-13T16:57:33.174003 #4]  INFO -- : Migrating to CreateReplies (20180113154217)
D, [2018-01-13T16:57:33.174944 #4] DEBUG -- :    (0.6ms)  BEGIN
== 20180113154217 CreateReplies: migrating ====================================
-- create_table(:replies)
D, [2018-01-13T16:57:33.184287 #4] DEBUG -- :    (8.8ms)  CREATE TABLE &quot;replies&quot; (&quot;id&quot; bigserial primary key, &quot;channel_id&quot; character varying, &quot;text&quot; character varying, &quot;created_at&quot; timestamp NOT NULL, &quot;updated_at&quot; timestamp NOT NULL)
   -&gt; 0.0093s
== 20180113154217 CreateReplies: migrated (0.0093s) ===========================

D, [2018-01-13T16:57:33.185682 #4] DEBUG -- :   SQL (0.6ms)  INSERT INTO &quot;schema_migrations&quot; (&quot;version&quot;) VALUES ($1) RETURNING &quot;version&quot;  [[&quot;version&quot;, &quot;20180113154217&quot;]]
D, [2018-01-13T16:57:33.187624 #4] DEBUG -- :    (1.7ms)  COMMIT
D, [2018-01-13T16:57:33.193606 #4] DEBUG -- :   ActiveRecord::InternalMetadata Load (2.0ms)  SELECT  &quot;ar_internal_metadata&quot;.* FROM &quot;ar_internal_metadata&quot; WHERE &quot;ar_internal_metadata&quot;.&quot;key&quot; = $1 LIMIT $2  [[&quot;key&quot;, &quot;environment&quot;], [&quot;LIMIT&quot;, 1]]
D, [2018-01-13T16:57:33.201843 #4] DEBUG -- :    (0.5ms)  BEGIN
D, [2018-01-13T16:57:33.204353 #4] DEBUG -- :    (1.6ms)  COMMIT
D, [2018-01-13T16:57:33.205359 #4] DEBUG -- :    (0.7ms)  SELECT pg_advisory_unlock(8162367372296191845)

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

# 實測

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg85FHEL7CYrIQrode333JWYgEkIqrljuU_bdxvPZ4FyM_PXx5ud5lD1xy-qmDB63OolLgdwSTNcYoIyulCYuil9xK2Wp123dCUWMYQ8-phyWxxmNvpWmlMQV-cCJYIT9Y8HmwAKEcttC0/s1600/1.jpg)

成功！

失敗的在底下留言，謝謝。

# 本日重點

- 學會了判斷目前的頻道
- 學會了如何根據前後文作出不同的回應

# 接下來

剩沒幾天了，還有很多可以學的，我想知道你們比較想學些什麼？

接下來我們還可以做的事情有這些：

- 現在的程式有點亂了，而且還有些問題，需要整理
- 讓卡米狗的關鍵字回應能根據目前頻道，作出不同的回應
- 讓卡米狗能抽籤
- 讓卡米狗能擷取用戶的使用者名稱以及大頭貼
- 讓卡米狗能接收及傳送貼圖
- 讓卡米狗能接收及傳送圖片
- 讓卡米狗能傳送含有按鈕的選單
- 讓卡米狗能查天氣
- 打造一個管理後台
- 讓卡米狗能發公告
- 製作小遊戲，比方說井字遊戲

或者你有想到，但上面沒列出來的也可以。

請在本文留言讓我知道你想學些什麼，可複選。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/4208892180148640392/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/4208892180148640392' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/4208892180148640392'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/4208892180148640392'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/blog-post_14.html' title='第二十六天：卡米狗推齊'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-8146117745376879733</id><published>2018-01-13T03:15:00.000+08:00</published><updated>2018-01-14T22:51:50.270+08:00</updated><title type='text'>第二十五天：卡米狗學說話</title><content type='html'>卡米狗的學說話指令，最早期的語法設計是`卡米狗學說話;關鍵字;回覆`。用兩個半形分號作為分隔符號。為什麼選擇用分號作為分隔符號呢？因為我們的分隔符號不能出現在關鍵字或回覆內，所以要挑一個比較少人用的符號。

我們必須讓學說話功能的優先順序高於關鍵字回覆，這樣才能確保學說話指令不會被關鍵字覆蓋。

# 修改主程式

主程式：
```
  def webhook
    # 學說話
    reply_text = learn(received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(received_text) if reply_text.nil?

    # 傳送訊息到 line
    response = reply_to_line(reply_text)
    
    # 回應 200
    head :ok
  end 
```

第一行：

```
reply_text = learn(received_text)
```
`learn` 是一個待會要寫的新函數，如果使用者說出一句話剛好符合學說話語法，那麼就回應`好哦～好哦～`並儲存結果。如果使用者說出一句話不符合學說話語法，就傳回 `nil`。 `nil` 是代表空值的意思。

第二行：
```
reply_text = keyword_reply(received_text) if reply_text.nil?
```

如果 `reply_text` 是空值的話才進行關鍵字回覆的判斷。這樣就能確保學說話指令優先於關鍵字回覆。

我們要判斷輸入的文字開頭是不是`卡米狗學說話;`，要做到這件事情，我們需要學一點字串操作。

# 字串操作

這是一個字串：

```
&#39;ABCDEF&#39;
=&gt; &quot;ABCDEF&quot;
```

### 字串的切割

這是字串的第一個字：

```
&#39;ABCDEF&#39;[0]
=&gt; &quot;A&quot;
```

這是字串的第二個字：

```
&#39;ABCDEF&#39;[1]
=&gt; &quot;B&quot;
```

以此類推，
A,B,C,D,E,F 
分別對應到：
0,1,2,3,4,5


這是字串的倒數第一個字
```
&#39;ABCDEF&#39;[-1]
=&gt; &quot;F&quot;
```

這是字串的倒數第二個字
```
&#39;ABCDEF&#39;[-2]
=&gt; &quot;E&quot;
```

以此類推，
F,E,D,C,B,A
分別對應到：
-1,-2,-3,-4,-5,-6

另外，你可以透過 Range 取得一個區間。

```
&#39;ABCDEF&#39;[0..1]
=&gt; &quot;AB&quot;
```

```
&#39;ABCDEF&#39;[0..3]
=&gt; &quot;ABCD&quot;
```

```
&#39;ABCDEF&#39;[3..-1]
=&gt; &quot;DEF&quot;
```

# 字串的查詢

想知道字串中的 `A` 出現在哪裡：

```
&#39;ABCDEF&#39;.index(&#39;A&#39;)
=&gt; 0
```

想知道字串中的 `B` 出現在哪裡：

```
&#39;ABCDEF&#39;.index(&#39;B&#39;)
=&gt; 1
```

想知道字串中的 `C` 出現在哪裡：

```
&#39;ABCDEF&#39;.index(&#39;C&#39;)
=&gt; 2
```

找不到的情形會傳回 `nil`：

```
&#39;ABCDEF&#39;.index(&#39;G&#39;)
=&gt; nil
```

# 字串的相等

判斷兩個字串是否相等：

```
&#39;A&#39; == &#39;A&#39;
=&gt; true
```

```
&#39;卡米狗學說話&#39; == &#39;卡米狗學說話&#39;
=&gt; true
```

```
&#39;A&#39; == &#39;B&#39;
=&gt; false
```

學會以上三個技巧，就能夠解決大部分的問題。

# 學說話

現在我們要開始寫學說話函數，從空函數開始。

```
  # 學說話
  def learn(received_text)

  end
```

利用以上三個技巧，我們可以先取得前面七個字，看看是不是等於`卡米狗學說話;`，如果是的話，在後面的字串中找到分號作為分隔點。

先檢查開頭的字是不是`卡米狗學說話;`：

```
  def learn(received_text)
    #如果開頭不是 卡米狗學說話; 就跳出
    return nil unless received_text[0..6] == &#39;卡米狗學說話;&#39;
  end
```

`unless` 是 `if` 的相反，`unless` 是`除非`的意思。

除非前面七個字是`卡米狗學說話;`，不然就傳回 `nil`。

再來就是取得剩下來的字，以及找到第二個分號。

```
  # 學說話
  def learn(received_text)
    #如果開頭不是 卡米狗學說話; 就跳出
    return nil unless received_text[0..6] == &#39;卡米狗學說話;&#39;
    
    received_text = received_text[7..-1]
    semicolon_index = received_text.index(&#39;;&#39;)

    # 找不到分號就跳出
    return nil if semicolon_index.nil?
  end
```
   
因為前面七個字已經沒有用了，所以我們抓出第八個字到最後一個字。然後在剩下的字裡面找到分號的位置。如果找不到分號，就跳出。

接下來我們要根據分隔點，擷取出關鍵字以及回覆，並且新增到資料庫裡。

```
  # 學說話
  def learn(received_text)
    #如果開頭不是 卡米狗學說話; 就跳出
    return nil unless received_text[0..6] == &#39;卡米狗學說話;&#39;
    
    received_text = received_text[7..-1]
    semicolon_index = received_text.index(&#39;;&#39;)

    # 找不到分號就跳出
    return nil if semicolon_index.nil?

    keyword = received_text[0..semicolon_index-1]
    message = received_text[semicolon_index+1..-1]

    KeywordMapping.create(keyword: keyword, message: message)
    &#39;好哦～好哦～&#39;
  end
```

這就是完整的學說話指令。接著我們要修改關鍵字回覆。

# 關鍵字回覆

這是原本的關鍵字回覆：

```
  # 關鍵字回覆
  def keyword_reply(received_text)
    # 學習紀錄表
    keyword_mapping = {
      &#39;QQ&#39; =&gt; &#39;神曲支援：https://www.youtube.com/watch?v=T0LfHEwEXXw&amp;feature=youtu.be&amp;t=1m13s&#39;,
      &#39;我難過&#39; =&gt; &#39;神曲支援：https://www.youtube.com/watch?v=T0LfHEwEXXw&amp;feature=youtu.be&amp;t=1m13s&#39;
    }
    
    # 查表
    keyword_mapping[received_text]
  end
```

要改成從資料庫查詢，其實昨天已經寫好了：

```
  # 關鍵字回覆
  def keyword_reply(received_text)
    mapping = KeywordMapping.where(keyword: received_text).last
    if mapping.nil?
      nil
    else
      mapping.message
    end
  end
```

將查詢結果存到 `mapping` 變數中，然後檢查有沒有查到東西，如果有才傳回。

這裡可以加入一點浪漫：

```
  # 關鍵字回覆
  def keyword_reply(received_text)
    KeywordMapping.where(keyword: received_text).last&amp;.message
  end
```

如果 `&amp;.` 的前面是 `nil`，那他就不會做後面的事，直接傳回 `nil`。

到這裡算是開發完成，可以上傳程式碼了。
# 對一下程式碼
你的程式碼應該長得差不多像這樣：
```
require &#39;line/bot&#39;
class KamigoController &lt; ApplicationController
  protect_from_forgery with: :null_session

  def webhook
    # 學說話
    reply_text = learn(received_text)

    # 關鍵字回覆
    reply_text = keyword_reply(received_text) if reply_text.nil?

    # 傳送訊息到 line
    response = reply_to_line(reply_text)
    
    # 回應 200
    head :ok
  end 

  # 取得對方說的話
  def received_text
    message = params[&#39;events&#39;][0][&#39;message&#39;]
    message[&#39;text&#39;] unless message.nil?
  end

  # 學說話
  def learn(received_text)
    #如果開頭不是 卡米狗學說話; 就跳出
    return nil unless received_text[0..6] == &#39;卡米狗學說話;&#39;
    
    received_text = received_text[7..-1]
    semicolon_index = received_text.index(&#39;;&#39;)

    # 找不到分號就跳出
    return nil if semicolon_index.nil?

    keyword = received_text[0..semicolon_index-1]
    message = received_text[semicolon_index+1..-1]

    KeywordMapping.create(keyword: keyword, message: message)
    &#39;好哦～好哦～&#39;
  end

  # 關鍵字回覆
  def keyword_reply(received_text)
    KeywordMapping.where(keyword: received_text).last&amp;.message
  end

  # 傳送訊息到 line
  def reply_to_line(reply_text)
    return nil if reply_text.nil?
    
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: reply_text
    } 

    # 傳送訊息
    line.reply_message(reply_token, message)
  end

  # Line Bot API 物件初始化
  def line
    @line ||= Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
  end


  def eat
    render plain: &quot;吃土啦&quot;
  end 

  def request_headers
    render plain: request.headers.to_h.reject{ |key, value|
      key.include? &#39;.&#39;
    }.map{ |key, value|
      &quot;#{key}: #{value}&quot;
    }.sort.join(&quot;\n&quot;)
  end

  def response_headers
    response.headers[&#39;5566&#39;] = &#39;QQ&#39;
    render plain: response.headers.to_h.map{ |key, value|
      &quot;#{key}: #{value}&quot;
    }.sort.join(&quot;\n&quot;)
  end

  def request_body
    render plain: request.body
  end

  def show_response_body
    puts &quot;===這是設定前的response.body:#{response.body}===&quot;
    render plain: &quot;虎哇花哈哈哈&quot;
    puts &quot;===這是設定後的response.body:#{response.body}===&quot;
  end

  def sent_request
    uri = URI(&#39;http://localhost:3000/kamigo/eat&#39;)
    http = Net::HTTP.new(uri.host, uri.port)
    http_request = Net::HTTP::Get.new(uri)
    http_response = http.request(http_request)

    render plain: JSON.pretty_generate({
      request_class: request.class,
      response_class: response.class,
      http_request_class: http_request.class,
      http_response_class: http_response.class
    })
  end

  def translate_to_korean(message)
    &quot;#{message}油~&quot;
  end
end
```

上傳後，我們還有一些工作要做：
# postgresql 版本的指定
如果你在 `heroku logs -t` 上面看到這個錯誤訊息：
```
2018-01-12T18:30:39.687847+00:00 heroku[web.1]: Starting process with command `bin/rails server -p 18506 -e production`
2018-01-12T18:30:45.609275+00:00 app[web.1]: /app/vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.4/lib/active_record/connection_adapters/connection_specification.rb:188:in `rescue in spec&#39;: Specified &#39;postgresql&#39; for database adapter, but the gem is not loaded. Add `gem &#39;pg&#39;` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord). (Gem::LoadError)
```

請將你的 Gemfile 修改一下，原本是：
```
group :development, :test do
  gem &#39;sqlite3&#39;
end

group :production do
  gem &#39;pg&#39;
end
```

改為

```
group :development, :test do
  gem &#39;sqlite3&#39;
end

group :production do
  gem &#39;pg&#39;, &#39;~&gt; 0.21.0&#39;
end
```

由於三天前 pg 發布了[新版本](https://bitbucket.org/ged/ruby-pg/commits/tag/v1.0.0)，而新版本似乎有點問題，所以我們需要指定安裝[穩定的版本](https://bitbucket.org/ged/ruby-pg/commits/tag/v0.21.0)。如果我們不指定版本，就會安裝到有問題的最新版。

# 安裝 Heroku 上的資料庫

使用 `heroku addons:create heroku-postgresql:hobby-dev` 指令弄一台免費的資料庫來玩玩。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;heroku addons:create heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on people-all-love-kamigo... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-concave-22896 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

# 進行在 Heroku 上的資料庫遷移

在我們的小黑框輸入 `heroku run rake db:migrate`：
```
D:\只要有心，人人都可以作卡米狗\ironman&gt;heroku run rake db:migrate
Running rake db:migrate on people-all-love-kamigo... up, run.8915 (Free)
D, [2018-01-12T18:43:37.665151 #4] DEBUG -- :    (1852.2ms)  CREATE TABLE &quot;schema_migrations&quot; (&quot;version&quot; character varying NOT NULL PRIMARY KEY)
D, [2018-01-12T18:43:38.188458 #4] DEBUG -- :    (491.4ms)  CREATE TABLE &quot;ar_internal_metadata&quot; (&quot;key&quot; character varying NOT NULL PRIMARY KEY, &quot;value&quot; character varying, &quot;created_at&quot; timestamp NOT NULL, &quot;updated_at&quot; timestamp NOT NULL)
D, [2018-01-12T18:43:38.194442 #4] DEBUG -- :    (2.3ms)  SELECT pg_try_advisory_lock(8162367372296191845)
D, [2018-01-12T18:43:39.009656 #4] DEBUG -- :    (2.2ms)  SELECT &quot;schema_migrations&quot;.&quot;version&quot; FROM &quot;schema_migrations&quot; ORDER BY &quot;schema_migrations&quot;.&quot;version&quot; ASC
I, [2018-01-12T18:43:39.012007 #4]  INFO -- : Migrating to CreateKeywordMappings (20180110181744)
D, [2018-01-12T18:43:39.015455 #4] DEBUG -- :    (0.8ms)  BEGIN
== 20180110181744 CreateKeywordMappings: migrating ============================
-- create_table(:keyword_mappings)
D, [2018-01-12T18:43:39.833168 #4] DEBUG -- :    (815.6ms)  CREATE TABLE &quot;keyword_mappings&quot; (&quot;id&quot; bigserial primary key, &quot;keyword&quot; character varying, &quot;message&quot; character varying, &quot;created_at&quot; timestamp NOT NULL, &quot;updated_at&quot; timestamp NOT NULL)
   -&gt; 0.8170s
== 20180110181744 CreateKeywordMappings: migrated (0.8174s) ===================

D, [2018-01-12T18:43:39.853181 #4] DEBUG -- :   SQL (6.3ms)  INSERT INTO &quot;schema_migrations&quot; (&quot;version&quot;) VALUES ($1) RETURNING &quot;version&quot;  [[&quot;version&quot;, &quot;20180110181744&quot;]]
D, [2018-01-12T18:43:39.861444 #4] DEBUG -- :    (5.7ms)  COMMIT
D, [2018-01-12T18:43:39.880216 #4] DEBUG -- :   ActiveRecord::InternalMetadata Load (2.8ms)  SELECT  &quot;ar_internal_metadata&quot;.* FROM &quot;ar_internal_metadata&quot; WHERE &quot;ar_internal_metadata&quot;.&quot;key&quot; = $1 LIMIT $2  [[&quot;key&quot;, &quot;environment&quot;], [&quot;LIMIT&quot;, 1]]
D, [2018-01-12T18:43:39.896978 #4] DEBUG -- :    (1.1ms)  BEGIN
D, [2018-01-12T18:43:39.899766 #4] DEBUG -- :   SQL (1.0ms)  INSERT INTO &quot;ar_internal_metadata&quot; (&quot;key&quot;, &quot;value&quot;, &quot;created_at&quot;, &quot;updated_at&quot;) VALUES ($1, $2, $3, $4) RETURNING &quot;key&quot;  [[&quot;key&quot;, &quot;environment&quot;], [&quot;value&quot;, &quot;production&quot;], [&quot;created_at&quot;, &quot;2018-01-12 18:43:39.897705&quot;], [&quot;updated_at&quot;, &quot;2018-01-12 18:43:39.897705&quot;]]
D, [2018-01-12T18:43:39.902418 #4] DEBUG -- :    (1.8ms)  COMMIT
D, [2018-01-12T18:43:39.903709 #4] DEBUG -- :    (0.8ms)  SELECT pg_advisory_unlock(8162367372296191845)

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

# 進行實測

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjCC42Z8ZcN5KoIt5415XPVjw94yXDjb6-IwVnTVXVf6AbFQx6QY_ZDK1oVs-RAKsF9eWf5ryyuEphUJ0yEcB_8ClWwrM8PMMSVdYTt-uGD4gNSPiuUOnaJHPlVPVxYAltuV8GzJNgL3NE/s1600/1.jpg)

順利～不順利的人請在底下留言並附上不順利的截圖，謝謝。

# 本日重點

- 學習了字串操作
- 學習了 Heroku 上的資料庫建置
- 做出了學說話和觸發說話

明天講推齊功能。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/8146117745376879733/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/8146117745376879733' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/8146117745376879733'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/8146117745376879733'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/blog-post_13.html' title='第二十五天：卡米狗學說話'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-4064038086763701549</id><published>2018-01-11T23:22:00.000+08:00</published><updated>2018-01-12T00:17:14.376+08:00</updated><title type='text'>第二十四天：認識資料庫(續)</title><content type='html'>昨天我們講到資料模型產生器的用法：

```
rails generate model keyword_mapping keyword message
```
會產生兩個我們需要的檔案：

- 資料庫遷移檔：`db/migrate/20180110181744_create_keyword_mappings.rb`
- 資料模型：`app/models/keyword_mapping.rb`

其中，資料庫遷移檔就像是一張&lt;strike&gt;對資料庫施法的卷軸&lt;/strike&gt;設計圖，可以用來幫資料庫升級。

那要怎麼升級呢？

# 資料庫遷移

使用 `rails db:migrate` 指令就會進行資料庫升級。
```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails db:migrate
== 20180110181744 CreateKeywordMappings: migrating ============================
-- create_table(:keyword_mappings)
   -&gt; 0.4916s
== 20180110181744 CreateKeywordMappings: migrated (0.4927s) ===================


D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

還可以用 `rails db:rollback` 降級：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails db:rollback
== 20180110181744 CreateKeywordMappings: reverting ============================
-- drop_table(:keyword_mappings)
   -&gt; 0.5057s
== 20180110181744 CreateKeywordMappings: reverted (0.5211s) ===================


D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

可以用 `rails db:migrate:status` 查看目前等級。這是升級前：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails db:migrate:status

database: D:/只要有心，人人都可以作卡米狗/ironman/db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
  down    20180110181744  Create keyword mappings


D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

這是升級後：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails db:migrate:status

database: D:/只要有心，人人都可以作卡米狗/ironman/db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20180110181744  Create keyword mappings


D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

資料庫已就緒，接下來就只等我們把學習紀錄寫入了。接下來我會試著用 [Google 試算表](https://www.google.com/intl/zh-TW_tw/sheets/about/)來比喻目前資料庫的狀態(因為我家沒有 Excel)，現在的資料庫看起來像這樣：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEickLSuTINz3yHZ5q7CnPiF_T8L3ol03H_B9QD6aZTgXcgcRjTadqjjKvohbY7B9tL1wLPnDrDHwl6xMSeCJ9nBUO_1BLlp6-lDVJmFFgcLLXm7J0a0kePKxQPBKkRuOf55Ivb79LRI4qc/s1600/1.jpg)

# 資料模型

我們可以使用 `rails console` 或簡寫 `rails c` 去試著操作看看資料模型， `rails console` 是一個類似 `irb` 的互動式介面，他可以讓你輸入一行程式就立即生效。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails console
Loading development environment (Rails 5.1.4)
irb(main):001:0&gt;
```

### 列出所有資料

我們的資料模型叫做 `KeywordMapping`。可以用 `.all` 將它顯示出來看看：

```
irb(main):001:0&gt; KeywordMapping.all
  KeywordMapping Load (2.5ms)  SELECT  &quot;keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; LIMIT ?  [[&quot;LIMIT&quot;, 11]]
=&gt; #&lt;ActiveRecord::Relation []&gt;
irb(main):002:0&gt;
```
這一段：
```
SELECT  &quot;keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; LIMIT ?
```
是我們對資料庫進行查詢的 SQL 語法，幸好你不需要學會這個，就當作沒看到吧。

而這一段：
```
=&gt; #&lt;ActiveRecord::Relation []&gt;
```
是指 `KeywordMapping.all` 是一個 `ActiveRecord::Relation` 類別的實體， `[]` 表示它是空的。

讓我們弄點東西進去。

### 新增資料

用 `KeywordMapping.new` 可以獲得一筆新的空白資料。

```
irb(main):002:0&gt; new_data = KeywordMapping.new
=&gt; #&lt;KeywordMapping id: nil, keyword: nil, message: nil, created_at: nil, updated_at: nil&gt;
irb(main):003:0&gt;
```
用一個變數 `new_data` 去接住它，因為我們接下來要對他做事。

```
irb(main):003:0&gt; new_data.keyword = &quot;Q&quot;
=&gt; &quot;Q&quot;
```

設定 new_data 的 keyword 是 &quot;Q&quot;。

```
irb(main):004:0&gt; new_data.message = &quot;A&quot;
=&gt; &quot;A&quot;
irb(main):005:0&gt;
```

設定 new_data 的 message 是 &quot;A&quot;。

都設定好之後用 `new_data.save` 來存檔。

```
irb(main):005:0&gt; new_data.save
   (0.0ms)  begin transaction
  SQL (494.1ms)  INSERT INTO &quot;keyword_mappings&quot; (&quot;keyword&quot;, &quot;message&quot;, &quot;created_at&quot;, &quot;updated_at&quot;) VALUES (?, ?, ?, ?)  [[&quot;keyword&quot;, &quot;Q&quot;], [&quot;message&quot;, &quot;A&quot;], [&quot;created_at&quot;, &quot;2018-01-11 14:30:55.567172&quot;], [&quot;updated_at&quot;, &quot;2018-01-11 14:30:55.567172&quot;]]
   (52.5ms)  commit transaction
=&gt; true
irb(main):006:0&gt;
```

一樣，就是一堆不需要看懂的 SQL。現在使用 `KeywordMapping.all` 就能看到新加入的資料了。

目前資料表的狀態：

```
irb(main):006:0&gt; KeywordMapping.all
  KeywordMapping Load (0.0ms)  SELECT  &quot;keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; LIMIT ?  [[&quot;LIMIT&quot;, 11]]
=&gt; #&lt;ActiveRecord::Relation [#&lt;KeywordMapping id: 1, keyword: &quot;Q&quot;, message: &quot;A&quot;, created_at: &quot;2018-01-11 14:30:55&quot;, updated_at: &quot;2018-01-11 14:30:55&quot;&gt;]&gt;
irb(main):007:0&gt;
```

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkE0jCHbZillsWuf2MYvDXyzLpJGKfqt6Ow49-QI2SjGA7FlpbcIaRR93ijS24lMv6VPzWfx_ft4pvSDlfRJxnls2W0PrwZxjY1-ri1kHGCgLn_2RoXXrOJDJAo6P0l7oldkNRdbxiaYw/s1600/2.jpg)

新增資料也有簡寫的方式可以一行做完，指令是 `KeywordMapping.create({keyword:&quot;Q2&quot;, message:&quot;A2&quot;})`：

```
irb(main):007:0&gt; KeywordMapping.create(keyword:&quot;Q2&quot;, message:&quot;A2&quot;)
   (0.0ms)  begin transaction
  SQL (485.4ms)  INSERT INTO &quot;keyword_mappings&quot; (&quot;keyword&quot;, &quot;message&quot;, &quot;created_at&quot;, &quot;updated_at&quot;) VALUES (?, ?, ?, ?)  [[&quot;keyword&quot;, &quot;Q2&quot;], [&quot;message&quot;, &quot;A2&quot;], [&quot;created_at&quot;, &quot;2018-01-11 14:36:34.858893&quot;], [&quot;updated_at&quot;, &quot;2018-01-11 14:36:34.858893&quot;]]
   (48.5ms)  commit transaction
=&gt; #&lt;KeywordMapping id: 2, keyword: &quot;Q2&quot;, message: &quot;A2&quot;, created_at: &quot;2018-01-11 14:36:34&quot;, updated_at: &quot;2018-01-11 14:36:34&quot;&gt;
irb(main):008:0&gt;
```

這是傳入一個 hash 作為設定，用 `create` 方法的話就會自動 `save`，所以就不用自己再打 `save` 了。

目前資料表的狀態：
```
irb(main):009:0&gt; KeywordMapping.all
  KeywordMapping Load (0.0ms)  SELECT  &quot;keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; LIMIT ?  [[&quot;LIMIT&quot;, 11]]
=&gt; #&lt;ActiveRecord::Relation [#&lt;KeywordMapping id: 1, keyword: &quot;Q&quot;, message: &quot;A&quot;, created_at: &quot;2018-01-11 14:30:55&quot;, updated_at: &quot;2018-01-11 14:30:55&quot;&gt;, #&lt;KeywordMapping id: 2, keyword: &quot;Q2&quot;, message: &quot;A2&quot;, created_at: &quot;2018-01-11 14:36:34&quot;, updated_at: &quot;2018-01-11 14:36:34&quot;&gt;]&gt;
irb(main):010:0&gt;
```
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaVO4FJfMRkqihoMfkBGqYh8EjY_lJhNh7gL_zR9UTh3aZbIyVu7G5y7AHrF7DStxx0R2w62vcR2szqN822SCYNe6v3UjUsBJRRpkzMoGmaRImwEwIb2wEsr2UH6zu-PUO08Ua-FeoH-A/s1600/3.jpg)

### 查詢資料

我們通常不會想要拿出整個資料表，而是只想要查當中的一筆，這時候就要用 `where` 方法，以下示範 `KeywordMapping.where(keyword:&quot;Q2&quot;)`。

```
irb(main):008:0&gt; KeywordMapping.where(keyword:&quot;Q2&quot;)
  KeywordMapping Load (0.5ms)  SELECT  &quot;keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; WHERE &quot;keyword_mappings&quot;.&quot;keyword&quot; = ? LIMIT ?  [[&quot;keyword&quot;, &quot;Q2&quot;], [&quot;LIMIT&quot;, 11]]
=&gt; #&lt;ActiveRecord::Relation [#&lt;KeywordMapping id: 2, keyword: &quot;Q2&quot;, message: &quot;A2&quot;, created_at: &quot;2018-01-11 14:36:34&quot;, updated_at: &quot;2018-01-11 14:36:34&quot;&gt;]&gt;
irb(main):009:0&gt;
```

這是篩選功能，我們對 `keyword` 欄位做 `Q2` 篩選，在 Google 試算表按照順序點就可以達到相同的效果。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhUURzgRjIKIolm51sGciqE7YjmnesYZBadibFRZS9v3lfm-DtqbNBD3RthlmOtSng6bNfNnx3Zf_ok5KWGBSXxG8VLlZ096Y6aUG54Vprsiw4pu4iDns-Sc4RSIqtpJCR6g6gWr1AB_-E/s1600/4.jpg)

篩選出來可能會有多筆，我們可以 `.all` 取得全部或者 `.first` 取第一筆，或 `.last` 取最後一筆。

所以卡米狗觸發教學的寫法是這樣：`KeywordMapping.where(keyword:&quot;Q2&quot;).last.message`，對 `keyword` 欄位做篩選，找到最後一次教學紀錄，然後取出 `message` 欄位的內容。

```
irb(main):012:0* KeywordMapping.where(keyword:&quot;Q2&quot;).last.message
  KeywordMapping Load (0.5ms)  SELECT  &quot;keyword_mappings&quot;.* FROM &quot;keyword_mappings&quot; WHERE &quot;keyword_mappings&quot;.&quot;keyword&quot; = ? ORDER BY &quot;keyword_mappings&quot;.&quot;id&quot; DESC LIMIT ?  [[&quot;keyword&quot;, &quot;Q2&quot;], [&quot;LIMIT&quot;, 1]]
=&gt; &quot;A2&quot;
irb(main):013:0&gt;
```

丟 Q2 進去資料庫查，查到 A2 再回應給 Line。

# 本日重點

- 學會使用資料庫遷移
- 學會使用資料模型
- 了解卡米狗觸發教學的原理

明天會講怎麼用這兩天學到的東西做出卡米狗學習指令。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/4064038086763701549/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/4064038086763701549' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/4064038086763701549'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/4064038086763701549'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/blog-post_11.html' title='第二十四天：認識資料庫(續)'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-9086524662447461569</id><published>2018-01-11T02:44:00.001+08:00</published><updated>2018-01-15T11:22:54.865+08:00</updated><title type='text'>第二十三天：認識資料庫</title><content type='html'>我們預計下一個要完成的功能是教學指令。

這是昨天的關鍵字回覆：

```
# 關鍵字回覆
def keyword_reply(received_text)
  # 學習紀錄表
  keyword_mapping = {
    &#39;QQ&#39; =&gt; &#39;神曲支援：https://www.youtube.com/watch?v=T0LfHEwEXXw&amp;feature=youtu.be&amp;t=1m13s&#39;,
    &#39;我難過&#39; =&gt; &#39;神曲支援：https://www.youtube.com/watch?v=T0LfHEwEXXw&amp;feature=youtu.be&amp;t=1m13s&#39;
  }
  
  # 查表
  keyword_mapping[received_text]
end
```
其中的學習紀錄表應該要能隨著大家講的話而去新增內容。這表示學習紀錄表應該要保存在檔案或資料庫內，每當需要存取學習紀錄表時就去存取檔案或資料庫。

今天講資料庫應該就飽了。

# 安裝 postgresql

查了一下發現是一條艱難的路，`postgresql` 在 Windows 上安裝的過程太過繁瑣，這裡就跳過不講，我們可以選擇在開發環境使用 `sqlite3`，同時在 Heroku 上使用 `postgresql`。當然你要挑戰在開發環境使用 `postgresql` 也行。這裡提供一個連結給你參考一下：[https://stackoverflow.com/questions/11656410/postgresql-installation-failed](https://stackoverflow.com/questions/11656410/postgresql-installation-failed)，是不是令人看了就想崩潰呢～如果是 macOS 的話瞬間就裝完囉。

# 設定 database.yml

我們要修改的檔案位於 `config/database.yml`。
這是一開始的樣子：

```
# SQLite version 3.x
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem &#39;sqlite3&#39;
#
default: &amp;default
  adapter: postgresql
  pool: &lt;%= ENV.fetch(&quot;RAILS_MAX_THREADS&quot;) { 5 } %&gt;
  timeout: 5000

development:
  &lt;&lt;: *default
  database: db/development.sqlite3

# Warning: The database defined as &quot;test&quot; will be erased and
# re-generated from your development database when you run &quot;rake&quot;.
# Do not set this db to the same as development or production.
test:
  &lt;&lt;: *default
  database: db/test.sqlite3

production:
  &lt;&lt;: *default
  database: db/production.sqlite3
```

要改成這樣：

```
# SQLite version 3.x
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem &#39;sqlite3&#39;
#
default: &amp;default
  adapter: postgresql
  pool: &lt;%= ENV.fetch(&quot;RAILS_MAX_THREADS&quot;) { 5 } %&gt;
  timeout: 5000

development:
  &lt;&lt;: *default
  adapter: sqlite3
  database: db/development.sqlite3

# Warning: The database defined as &quot;test&quot; will be erased and
# re-generated from your development database when you run &quot;rake&quot;.
# Do not set this db to the same as development or production.
test:
  &lt;&lt;: *default
  adapter: sqlite3
  database: db/test.sqlite3

production:
  &lt;&lt;: *default
  database: ironman
```

先解釋一下，這裡有四段程式(其實不是程式，是設定檔)。

這是第一段：

```
default: &amp;default
  adapter: postgresql
  pool: &lt;%= ENV.fetch(&quot;RAILS_MAX_THREADS&quot;) { 5 } %&gt;
  timeout: 5000
```

這是作為一個預設值。

- adapter：要採用哪一套資料庫，預設使用 `postgresql`
- pool：是同時連線數量，可以理解成頻寬
- timeout：超過 5000 毫秒資料庫還不回應的話就當作逾時

```
development:
  &lt;&lt;: *default
  adapter: sqlite3
  database: db/development.sqlite3
```

這段是開發環境，也就是我們的電腦。

-  &lt;&lt;: *default：採用預設值
- adapter：要採用哪一套資料庫，這裡是使用 `sqlite3`，會把預設值覆蓋掉
- database：資料庫的儲存位置，`db/development.sqlite3` 這是一個路徑，你可以在專案資料夾裡面找到這個位置

```
test:
  &lt;&lt;: *default
  adapter: sqlite3
  database: db/test.sqlite3
```
這段是測試環境，目前我們沒有使用測試環境，所以這裡跳過不講。

```
production:
  &lt;&lt;: *default
  database: ironman
```
這段是發布環境或正式環境，也就是 heroku 上。

- database：使用 `postgresql` 的話就不是儲存位置了，而是資料庫的名稱，不過概念上差不多。和 `sqlite3` 的差別是你不會在專案資料夾裡頭找到資料庫的實體檔案。

# 設定 Gemfile

因為我們要在 development 環境下使用 `sqlite3`，production 環境下使用 `postgresql`，所以 Gemfile 也要改寫，這是原本的 Gemfile：

```
gem &#39;pg&#39;, &#39;~&gt; 0.21.0&#39;
```

要改寫為

```
group :development, :test do
  gem &#39;sqlite3&#39;
end

group :production do
  gem &#39;pg&#39;, &#39;~&gt; 0.21.0&#39;
end
```

意思就是在 development 和 test 環境下要使用 `sqlite3`，而在 production 環境下使用 `postgresql`。

# 建立資料庫

在小黑框輸入：

```
rails db:create
```

就可以得到一個空白的資料庫，你可以觀察它會出現在專案資料夾下的 `db` 資料夾下。

# 建立資料表

在小黑框輸入：

```
rails generate model keyword_mapping keyword message
```

會看到：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails generate model keyword_mapping keyword message
      invoke  active_record
      create    db/migrate/20180110181744_create_keyword_mappings.rb
      create    app/models/keyword_mapping.rb
      invoke    test_unit
      create      test/models/keyword_mapping_test.rb
      create      test/fixtures/keyword_mappings.yml

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```
表示有四個檔案被生成了，分別是：

- 資料庫遷移檔：`db/migrate/20180110181744_create_keyword_mappings.rb`
- 資料模型：`app/models/keyword_mapping.rb`
- 單元測試：`test/models/keyword_mapping_test.rb`
- 測試資料：`test/models/keyword_mapping_test.rb`

因為我們不寫自動測試，所以後面兩個就先略過。

# 資料庫遷移檔

一個資料庫會有多個資料表，一個資料表會有多個欄位。以通訊錄為例，欄位大概就是姓名、電話、地址、信箱等等。大概長這樣：[https://goo.gl/VMT3CR](https://goo.gl/VMT3CR)。

建立一個空的資料表需要定義出這個表格有哪些欄位，分別儲存什麼格式的資料。

假設你現在在人工建立表格，你可能會開啟一個 Excel 然後在第一列上面輸入各種標題，說明下面每個格子該填什麼。但是工程師最不喜歡手動做事了，自動化就是潮。所以我們寫一隻程式去幫我們建立資料庫裡的表格，這些程式碼被稱為資料庫遷移檔。

但是工程師連資料庫遷移檔也懶得寫，所以就寫了一行指令自動生成資料庫遷移檔，也就是你剛剛輸入的那個指令。

打開 `db/migrate/20180110181744_create_keyword_mappings.rb` 會看到：

```
class CreateKeywordMappings &lt; ActiveRecord::Migration[5.1]
  def change
    create_table :keyword_mappings do |t|
      t.string :keyword
      t.string :message

      t.timestamps
    end
  end
end
```
重點在這裡：
```
    create_table :keyword_mappings do |t|
      t.string :keyword
      t.string :message

      t.timestamps
```

建立一個資料表叫 `keyword_mappings`，資料表包含兩個欄位，分別是 `keyword` 和 `message`，都是存字串。

# 資料模型

打開 `app/models/keyword_mapping.rb` 會看到：

```
class KeywordMapping &lt; ApplicationRecord
end
```
空的，因為它用繼承 (`&lt;`)，其實有很多東西是藏在 ApplicationRecord 裡面。

所以我們剛剛輸入的指令是這樣：

```
rails generate model keyword_mapping keyword message
```

意思是我要生成一個資料模型和資料庫遷移檔，資料表名稱為 `keyword_mapping`，包含兩個欄位分別是 `keyword` 和 `message`。

今天先講到這裡。

資料庫博大精深，卡米狗不是一天造成的，要有耐心。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/9086524662447461569/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/9086524662447461569' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/9086524662447461569'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/9086524662447461569'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/blog-post.html' title='第二十三天：認識資料庫'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-1216130590546493820</id><published>2018-01-10T16:17:00.001+08:00</published><updated>2018-01-10T16:45:30.794+08:00</updated><title type='text'>在 rails 上傳圖片並進行裁切時遭遇到的神奇問題</title><content type='html'>markdown

我使用 carrierwave 來做圖片上傳，並使用產生器來生成 uploader，像這樣：

```
rails g uploader normal
```

會生成這樣的檔案：

```
class NormalUploader &lt; CarrierWave::Uploader::Base

  storage :file

  def store_dir
    &quot;uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}&quot;
  end
end
```

若想要在上傳圖片時，對圖片進行裁切操作，可以這樣寫：
```
class CropUploader &lt; CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  storage :file

  def store_dir
    &quot;uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}&quot;
  end

  process :crop
  
  def crop
    manipulate! do |img|
      x = 1
      y = 2
      w = 3
      h = 4
      img.crop(&quot;#{w}x#{h}+#{x}+#{y}&quot;)
    end
  end
end
```
其中
```
include CarrierWave::MiniMagick
```
需要 gem &quot;mini_magick&quot; 以及 brew install imagemagick。

```
process :crop
```
這是一個 callback，會在圖片儲存前給你一個機會對圖片做事。所以只要前端傳遞一個矩形座標到後端就能切圖。這裡就隨便用 4 個值意思一下。

```
img.crop(&quot;#{w}x#{h}+#{x}+#{y}&quot;)
```

這個 crop 方法會被轉為系統指令

```
mogrify -crop 3x4+1+2 file_path
```
mogrify 是 imagemagick 提供的指令，可以拿它來切圖。

說明書在這裡：[https://www.imagemagick.org/script/mogrify.php](https://www.imagemagick.org/script/mogrify.php)

一切運作良好，直到我遇到這張圖：[https://www.ncl.ucar.edu/Applications/Images/color_18_3_lg.png](https://www.ncl.ucar.edu/Applications/Images/color_18_3_lg.png)

怎麼切位置都是錯的。

[強者我同事](https://blog.frost.tw)爬了一下文，發現是 ImageMagick 支援叫做 Virtual Canvas （虛擬圖層？）的資訊，這種東西其實是圖片的 Metadata 的一部分。

 把出問題那張圖片拿去解析 Metadata：[https://www.get-metadata.com/result/56d8b843-db53-4d7f-9f8a-7b1cd1ebde9b](https://www.get-metadata.com/result/56d8b843-db53-4d7f-9f8a-7b1cd1ebde9b)

會發現

```
Image Offset: 54, 64
```

也就是 ImageMagick 發現他有設定位移，所以就照這個設定去裁切了。然後用 +repage 可以讓他把 Offset 設回 0,0

所以這是我們的目標指令：

```
mogrify +repage -crop 3x4+1+2 file_path
```

但 ruby 是要這樣寫：

```
img.combine_options do |c|
  c.repage.+
  c.crop(&quot;#{w}x#{h}+#{x}+#{y}&quot;)
end
```
因為有兩個以上的參數，所以需要用 combine_options 去串接參數。
```
c.repage.+
```
會生成出
```
+repage
```

事實上他會把函數名稱拿去當作參數名稱，如果我這樣寫：

```
img.combine_options do |c|
  c.jsdiofaodj.+
  c.crop(&quot;#{w}x#{h}+#{x}+#{y}&quot;)
end
```

他就會嘗試執行

```
mogrify +jsdiofaodj -crop 199x154+234+343 file_path
```

如果把 .+ 拔掉：

```
img.combine_options do |c|
  c.jsdiofaodj
  c.crop(&quot;#{w}x#{h}+#{x}+#{y}&quot;)
end
```

就會變成

```
mogrify -jsdiofaodj -crop 199x154+234+343 file_path
```

如果調換順序：

```
img.combine_options do |c|
  c.crop(&quot;#{w}x#{h}+#{x}+#{y}&quot;)
  c.repage.+
end
```

會變成

```
mogrify -crop 3x4+1+2 +repage file_path
```

你可能會想說，參數順序有差嗎？還真的有差。

因為他不是參數順序，而是執行順序。

總而言之，[強者我同事](https://blog.frost.tw)守護了世界的和平。

參考資料：
[https://github.com/minimagick/minimagick/issues/107](https://github.com/minimagick/minimagick/issues/107)</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/1216130590546493820/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/1216130590546493820' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/1216130590546493820'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/1216130590546493820'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/rails.html' title='在 rails 上傳圖片並進行裁切時遭遇到的神奇問題'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-5104418235034843018</id><published>2018-01-10T08:45:00.000+08:00</published><updated>2018-01-10T08:45:29.285+08:00</updated><title type='text'>第二十二天：用 Line Messaging API 實作關鍵字回覆</title><content type='html'>markdown

今天我要讓你能抓到一點寫程式的感覺，所以我們會一直不斷地修改程式碼，這麼做可以讓你對程式碼的操作更熟悉。

先從最簡單的功能開始作，今天的目標是讓卡米狗能針對關鍵字回應訊息。

# 程式碼的重構

在加程式碼之前，我們先整理一下目前的程式

```
  def webhook
    # Line Bot API 物件初始化
    client = Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
    
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]

    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: &#39;好哦～好哦～&#39;
    }

    # 傳送訊息
    response = client.reply_message(reply_token, message)
      
    # 回應 200
    head :ok
  end 
```

目前程式碼是這樣，我覺得有點太長了，我們要讓他更好閱讀。首先要制定一個目標。

```
  def webhook
    # 核心程式
    reply_message = reply(received_message)

    # 回覆訊息
    reply_to_line(message)

    # 回應 200
    head :ok
  end 
```

只保留最重要的，當卡米狗看到 `received_message` 時，要回應 `reply_message`，剩下的東西都放到別處。我們先假設卡米狗只會對純文字有反應，並且只會回應純文字。

# 移出 client

```
    # Line Bot API 物件初始化
    client = Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
    
```

我們把這段程式搬出去，變成這樣：

```
  # Line Bot API 物件初始化
  def line
    client = Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
  end
```

定義一個方法叫作 `line`，他會回傳一個 client。這裡可以省略區域變數 client 不寫。

```
  # Line Bot API 物件初始化
  def line
    Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
  end
```

這麼寫的話，每次呼叫 `line` 時，就都會去作一次 `Line::Bot::Client.new`。我們可以把它保存起來，第二次呼叫 `line` 的時候就把保存起來的部分拿出來用，這樣作可以增加效能。

```
  # Line Bot API 物件初始化
  def line
    return @line unless @line.nil?
    @line = Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
  end
```

如果 `@line` 有值的話，直接回傳 `@line`，沒有值的話才作 `Line::Bot::Client.new` 並保存到 `@line`。

這裡用到了 `@`，`@` 開頭的變數是實體變數，跟區域變數不同的是，實體變數的記憶比較持久，區域變數只要函數執行完就消失，但實體變數可以持續存活到第二次之後的函數執行，甚至我可以在A函數保存實體變數，在B函數去使用實體變數。

關於實體變數，詳細的教學請參考：[為你自己學 Ruby on Rails - 變數、常數、流程控制、迴圈](https://railsbook.tw/chapters/05-ruby-basic-1.html#variable-and-constant)

現在的程式已經足夠完美了，但有更精簡的寫法。

```
  # Line Bot API 物件初始化
  def line
    @line ||= Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
  end
```

`||` 是或的意思，這是一個很特殊的寫法，跟原本的程式碼效果幾乎相同，我就不多作解釋。沒學會 `||=` 也沒關係，這就是工程師der浪漫。

目前的完整程式碼如下：

```
require &#39;line/bot&#39;
class KamigoController &lt; ApplicationController
  protect_from_forgery with: :null_session

  def webhook    
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: &#39;移出 client&#39;
    }

    # 傳送訊息
    response = line.reply_message(reply_token, message)
      
    # 回應 200
    head :ok
  end 
  
  # Line Bot API 物件初始化
  def line
    @line ||= Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
  end
  
  ...下略
end
```

改到這邊可以上傳程式碼測試一下，如果你不測也沒關係，因為我們還要繼續改。

# 移出 reply token

我們作一個函數，讓他自己去抓 reply token，我們關心的是要發什麼話，不關心 reply_token，所以我希望我們的主程式能變成這樣。

```
  def webhook   
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: &#39;移出 reply_token&#39;
    } 

    # 傳送訊息
    response = reply_to_line(message)
      
    # 回應 200
    head :ok
  end 
```

所以我們要實作函數 `reply_to_line`，他是一個傳入 message 後，透過 line api 傳送訊息出去，並傳回 HTTP response 的函數。

```
  # 傳送訊息到 line
  def reply_to_line(message)
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 傳送訊息
    response = line.reply_message(reply_token, message)
  end
```

可以再精簡為：

```
  # 傳送訊息到 line
  def reply_to_line(message)
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 傳送訊息
    line.reply_message(reply_token, message)
  end
```

因為一個函數的傳回值是最後一行的執行結果。這就是工程師der浪漫。

其實我們大部分時間在作的事情都是搬移程式，其實寫程式就是一種整理的藝術。

你可以想像成我們東西一開始全都放在客廳。東西越來越多之後，客廳就會開始變亂。要整理客廳的方法就是買幾個櫃子後把東西放進櫃子。函數就是我們的櫃子。

目前的完整程式碼如下：

```
require &#39;line/bot&#39;
class KamigoController &lt; ApplicationController
  protect_from_forgery with: :null_session

  def webhook   
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: &#39;移出 reply_token&#39;
    } 

    # 傳送訊息
    response = reply_to_line(message)
      
    # 回應 200
    head :ok
  end 

  # 傳送訊息到 line
  def reply_to_line(message)
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 傳送訊息
    line.reply_message(reply_token, message)
  end

  # Line Bot API 物件初始化
  def line
    @line ||= Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
  end

  ...下略

end
```

# 移出 message

這是現在的主程式：

```
  def webhook   
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: &#39;移出 reply_token&#39;
    } 

    # 傳送訊息
    response = reply_to_line(message)
      
    # 回應 200
    head :ok
  end 
```

我不希望在主程式看到這些：

```
{
  type: &#39;text&#39;,
  text: &#39;ㄅㄌㄅㄌㄅㄌ&#39;
} 
```

因為我們只在乎 `&#39;ㄅㄌㄅㄌㄅㄌ&#39;` 的部分，所以剩餘的部分都要盡量外移。這就像你不會想把垃圾放在桌上一樣，找個垃圾桶放垃圾就對了。

設定目標：

```
  def webhook   
    # 設定回覆訊息
    reply_text = &#39;移出 message&#39;

    # 傳送訊息
    response = reply_to_line(reply_text)
      
    # 回應 200
    head :ok
  end 
```

這就是我們所希望的樣子，因此我們要修改 `reply_to_line` 這個函數。

```
  # 傳送訊息到 line
  def reply_to_line(reply_text)
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: reply_text
    } 

    # 傳送訊息
    line.reply_message(reply_token, message)
  end
```

其實就是把一開始的程式整個全搬到 `reply_to_line`。

目前的完整程式碼如下：

```
require &#39;line/bot&#39;
class KamigoController &lt; ApplicationController
  protect_from_forgery with: :null_session

  def webhook
    # 設定回覆文字
    reply_text = &#39;移出 message&#39;
    
    # 傳送訊息到 line
    response = reply_to_line(reply_text)
    
    # 回應 200
    head :ok
  end 

  # 傳送訊息到 line
  def reply_to_line(reply_text)
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: reply_text
    } 

    # 傳送訊息
    line.reply_message(reply_token, message)
  end

  # Line Bot API 物件初始化
  def line
    @line ||= Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
  end

  ...下略

end
```

現在的主程式已經比一開始乾淨許多，這時我們再來加功能，應該就會比較容易看出我們在加什麼功能。我們要加的功能是關鍵字回覆。

# 關鍵字回覆

我們希望主程式可以變成這樣：

```
  def webhook
    # 設定回覆文字
    reply_text = keyword_reply(received_text)

    # 傳送訊息到 line
    response = reply_to_line(reply_text)
    
    # 回應 200
    head :ok
  end 
```

所以我們需要兩個函數，一個是 `received_text`：傳回對方說的話，另一個函數是 `keyword_reply`：傳入對方說的話，傳回卡米狗應該說的話。

這種思考邏輯是先決定程式的大架構，再來描述細節。這就像在畫圖的時候，你會先打個草稿，草稿看起來OK了再去畫細節，這樣可以確保你不會太專注於細節而失去了整體比例。

```
  # 取得對方說的話
  def received_text
    params[&#39;events&#39;][0][&#39;message&#39;][&#39;text&#39;]
  end

  # 關鍵字回覆
  def keyword_reply(received_text)
    received_text
  end
```

現在這樣就表示你說什麼，卡米狗就會跟著說什麼。

目前完整的程式碼如下：

```
require &#39;line/bot&#39;
class KamigoController &lt; ApplicationController
  protect_from_forgery with: :null_session

  def webhook
    # 設定回覆文字
    reply_text = keyword_reply(received_text)

    # 傳送訊息到 line
    response = reply_to_line(reply_text)
    
    # 回應 200
    head :ok
  end 

  # 取得對方說的話
  def received_text
    params[&#39;events&#39;][0][&#39;message&#39;][&#39;text&#39;]
  end

  # 關鍵字回覆
  def keyword_reply(received_text)
    received_text
  end

  ...下略
end
```

這程式碼可以運作，你可以上傳程式碼玩玩看。

這兩個函數現在這樣都還算未完成，他們都有一些缺陷。

先講關鍵字回覆。這裡應該要作成看見A回答B，而不是看見A回答A，我們還缺一個學習紀錄表讓卡米狗查。

而 `received_text` 的問題是，如果 Line 傳來的通知並不是訊息通知，而是比方說有人加你好友，或邀請你進入群組，或傳送貼圖、圖片、聲音、檔案都可能會導致我們的程式直接掛掉，因為 `params[&#39;events&#39;][0][&#39;message&#39;][&#39;text&#39;]` 的值是空值，而我們沒有說當是空值的時候應該怎麼作。

# 處理 received_text 的空值問題

根據 Line Messaging API 的文件，當有人傳訊息來時，我們才會收到 `message` 這個 hash，也就是說，下面這行不一定有值。

```
params[&#39;events&#39;][0][&#39;message&#39;]
```

我們應該在他有值的時候才去取 [&#39;text&#39;]

```
  # 取得對方說的話
  def received_text
    message = params[&#39;events&#39;][0][&#39;message&#39;]
    if message.nil?
      nil
    else
      message[&#39;text&#39;]
    end
  end
```

這是我們第一次用到了 if ，當 `message.nil?` 是空值的時候，我們就會傳回 `nil`。不是空值的話，就傳回 `message[&#39;text&#39;]`。

加入一點工程師的浪漫就會變成這樣：

```
  # 取得對方說的話
  def received_text
    message = params[&#39;events&#39;][0][&#39;message&#39;]
    message[&#39;text&#39;] unless message.nil?
  end
```

有的時候不用太堅持什麼浪漫，因為這純粹只是工程師的自爽行為。

# 關鍵字回覆的學習紀錄表

```
  # 關鍵字回覆
  def keyword_reply(received_text)
    # 學習紀錄表
    keyword_mapping = {
      &#39;QQ&#39; =&gt; &#39;神曲支援：https://www.youtube.com/watch?v=T0LfHEwEXXw&amp;feature=youtu.be&amp;t=1m13s&#39;,
      &#39;我難過&#39; =&gt; &#39;神曲支援：https://www.youtube.com/watch?v=T0LfHEwEXXw&amp;feature=youtu.be&amp;t=1m13s&#39;
    }
    
    # 查表
    keyword_mapping[received_text]
  end
```

在這裡定義了學習紀錄表 `keyword_mapping` 之後就作查表。當查表查不到內容的時候，就會傳回 nil。

這表示當查不到內容時，卡米狗應該要不回應，這需要修改其他函數。

```
  # 傳送訊息到 line
  def reply_to_line(reply_text)
    return nil if reply_text.nil?
    
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: reply_text
    } 

    # 傳送訊息
    line.reply_message(reply_token, message)
  end
```
這裡加一行 `return nil if reply_text.nil?`，當傳入值為空時表示不回應，後面的程式碼就不用作了。

目前完整的程式碼如下：

```
require &#39;line/bot&#39;
class KamigoController &lt; ApplicationController
  protect_from_forgery with: :null_session

  def webhook
    # 設定回覆文字
    reply_text = keyword_reply(received_text)

    # 傳送訊息到 line
    response = reply_to_line(reply_text)
    
    # 回應 200
    head :ok
  end 

  # 取得對方說的話
  def received_text
    message = params[&#39;events&#39;][0][&#39;message&#39;]
    message[&#39;text&#39;] unless message.nil?
  end

  # 關鍵字回覆
  def keyword_reply(received_text)
    # 學習紀錄表
    keyword_mapping = {
      &#39;QQ&#39; =&gt; &#39;神曲支援：https://www.youtube.com/watch?v=T0LfHEwEXXw&amp;feature=youtu.be&amp;t=1m13s&#39;,
      &#39;我難過&#39; =&gt; &#39;神曲支援：https://www.youtube.com/watch?v=T0LfHEwEXXw&amp;feature=youtu.be&amp;t=1m13s&#39;
    }
    
    # 查表
    keyword_mapping[received_text]
  end

  # 傳送訊息到 line
  def reply_to_line(reply_text)
    return nil if reply_text.nil?
    
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: reply_text
    } 

    # 傳送訊息
    line.reply_message(reply_token, message)
  end

  # Line Bot API 物件初始化
  def line
    @line ||= Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
  end


  def eat
    render plain: &quot;吃土啦&quot;
  end 

  def request_headers
    render plain: request.headers.to_h.reject{ |key, value|
      key.include? &#39;.&#39;
    }.map{ |key, value|
      &quot;#{key}: #{value}&quot;
    }.sort.join(&quot;\n&quot;)
  end

  def response_headers
    response.headers[&#39;5566&#39;] = &#39;QQ&#39;
    render plain: response.headers.to_h.map{ |key, value|
      &quot;#{key}: #{value}&quot;
    }.sort.join(&quot;\n&quot;)
  end

  def request_body
    render plain: request.body
  end

  def show_response_body
    puts &quot;===這是設定前的response.body:#{response.body}===&quot;
    render plain: &quot;虎哇花哈哈哈&quot;
    puts &quot;===這是設定後的response.body:#{response.body}===&quot;
  end

  def sent_request
    uri = URI(&#39;http://localhost:3000/kamigo/eat&#39;)
    http = Net::HTTP.new(uri.host, uri.port)
    http_request = Net::HTTP::Get.new(uri)
    http_response = http.request(http_request)

    render plain: JSON.pretty_generate({
      request_class: request.class,
      response_class: response.class,
      http_request_class: http_request.class,
      http_response_class: http_response.class
    })
  end

  def translate_to_korean(message)
    &quot;#{message}油~&quot;
  end

end
```

用起來的效果是這樣：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgFvB2Z-FMMJHkST4KTa555Wus0qw2BxKG3gi_710LZiHKWiftzM6ABpty-50xdVSqE_yzcr9cTSsRV685sim-q3FYXN0N0bOJPDi3O7F_AI05GGJKKusSEGV2SS5Xs0Vh0BynxavFRNCg/s1600/1.jpg)

那個「讓我想想...」是我們在[第三天：作一隻最簡單的 Line 聊天機器人](https://ithelp.ithome.com.tw/articles/10192928)從 Line 後台作的設定，如果你不喜歡可以去後台把它關掉，它在這裡：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDVJWISLeewPF0mQvK0ocHoSmWUIhyphenhyphentsvrm_nSoK0v1F6aaV6NeoXn_S67y2lJ_fSCN78wwKRYC2xKXss15CtU_vgnD7pobZgbwMoN149oncnoq92YsNx9Q-76IIGr4U9jQf68wkvMGAs/s1600/2.jpg)

今天就講到這，明天講怎麼教卡米狗說話。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/5104418235034843018/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/5104418235034843018' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/5104418235034843018'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/5104418235034843018'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/line-messaging-api.html' title='第二十二天：用 Line Messaging API 實作關鍵字回覆'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-1458147080436600237</id><published>2018-01-09T03:05:00.000+08:00</published><updated>2018-01-09T03:05:28.534+08:00</updated><title type='text'>第二十一天：讓 Line Bot 回覆訊息</title><content type='html'>markdown

昨天我們把聊天機器人 webhook 串好了，今天我們要讓機器人回覆訊息。

我們先來看看我們能不能正常的收到訂閱通知，我說的訂閱通知是在[第五天：認識 Line Messaging API Webhook](https://ithelp.ithome.com.tw/articles/10193441) 介紹到的各種通知。

我們可以看在 heroku 上的 `rails server` 小黑框來觀察。

# 觀察 heroku 上的伺服器運作記錄

當我們在小黑框輸入 `heroku logs -t` 時，跟我們在小黑框輸入 `rails server` 有 87% 像，當有人開啟網頁時，就會看到運作紀錄(log)。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;heroku logs -t
```

先開個小黑框輸入 `heroku logs -t` 之後放著。

# 觀察 Line developer 後台的 verify 

我們先回到 Line developer 後台，點一下昨天點過的 `verify`，看看 Line 到底傳了什麼給我們的伺服器。

```
2018-01-08T15:04:43.091880+00:00 heroku[web.1]: Starting process with command `bin/rails server -p 21213 -e production`
2018-01-08T15:04:50.486685+00:00 app[web.1]: =&gt; Booting Puma
2018-01-08T15:04:50.486716+00:00 app[web.1]: =&gt; Rails 5.1.4 application starting in production
2018-01-08T15:04:50.486718+00:00 app[web.1]: =&gt; Run `rails server -h` for more startup options
2018-01-08T15:04:50.486719+00:00 app[web.1]: Puma starting in single mode...
2018-01-08T15:04:50.486725+00:00 app[web.1]: * Version 3.11.0 (ruby 2.3.4-p301), codename: Love Song
2018-01-08T15:04:50.486733+00:00 app[web.1]: * Min threads: 5, max threads: 5
2018-01-08T15:04:50.486735+00:00 app[web.1]: * Environment: production
2018-01-08T15:04:50.486886+00:00 app[web.1]: * Listening on tcp://0.0.0.0:21213
2018-01-08T15:04:50.487396+00:00 app[web.1]: Use Ctrl-C to stop
2018-01-08T15:04:38.156226+00:00 heroku[web.1]: Unidling
2018-01-08T15:04:38.156470+00:00 heroku[web.1]: State changed from down to starting
2018-01-08T15:04:52.254468+00:00 app[web.1]: I, [2018-01-08T15:04:52.254363 #4]  INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Started POST &quot;/kamigo/webhook&quot; for 203.104.156.74 at 2018-01-08 15:04:52 +0000
2018-01-08T15:04:52.276728+00:00 app[web.1]: I, [2018-01-08T15:04:52.276573 #4]  INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Processing by KamigoController#webhook as HTML
2018-01-08T15:04:52.277008+00:00 app[web.1]: I, [2018-01-08T15:04:52.276880 #4]  INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c]   Parameters: {&quot;events&quot;=&gt;[{&quot;replyToken&quot;=&gt;&quot;00000000000000000000000000000000&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100001&quot;, &quot;type&quot;=&gt;&quot;text&quot;, &quot;text&quot;=&gt;&quot;Hello, world&quot;}}, {&quot;replyToken&quot;=&gt;&quot;ffffffffffffffffffffffffffffffff&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100002&quot;, &quot;type&quot;=&gt;&quot;sticker&quot;, &quot;packageId&quot;=&gt;&quot;1&quot;, &quot;stickerId&quot;=&gt;&quot;1&quot;}}], &quot;kamigo&quot;=&gt;{&quot;events&quot;=&gt;[{&quot;replyToken&quot;=&gt;&quot;00000000000000000000000000000000&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100001&quot;, &quot;type&quot;=&gt;&quot;text&quot;, &quot;text&quot;=&gt;&quot;Hello, world&quot;}}, {&quot;replyToken&quot;=&gt;&quot;ffffffffffffffffffffffffffffffff&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100002&quot;, &quot;type&quot;=&gt;&quot;sticker&quot;, &quot;packageId&quot;=&gt;&quot;1&quot;, &quot;stickerId&quot;=&gt;&quot;1&quot;}}]}}
2018-01-08T15:04:52.297993+00:00 app[web.1]: W, [2018-01-08T15:04:52.297845 #4]  WARN -- : [34361d41-4be8-4293-971d-eca0151ea11c] Can&#39;t verify CSRF token authenticity.
2018-01-08T15:04:52.298635+00:00 app[web.1]: I, [2018-01-08T15:04:52.298565 #4]  INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Completed 200 OK in 21ms
2018-01-08T15:04:52.306974+00:00 heroku[router]: at=info method=POST path=&quot;/kamigo/webhook&quot; host=people-all-love-kamigo.herokuapp.com request_id=34361d41-4be8-4293-971d-eca0151ea11c fwd=&quot;203.104.156.74&quot; dyno=web.1 connect=0ms service=56ms status=200 bytes=289 protocol=https
```
我們需要看懂這些東西，我們一次讀一段，慢慢讀完。

第一行：
```
2018-01-08T15:04:43.091880+00:00 heroku[web.1]: Starting process with command `bin/rails server -p 21213 -e production`
```
這行就是我們平常在小黑框輸入的 rails server，heroku 上也需要輸入這行，但這是由 heroku 自動幫我們輸入。

heroku 的免費伺服器有一個缺點，如果連續 30 分鐘都沒人連到我們的網頁伺服器，他就會自動關機。當他處於關機狀態時，如果有人連到我們的伺服器，那他就會把我們的網頁伺服器叫醒。

```
2018-01-08T15:04:50.486685+00:00 app[web.1]: =&gt; Booting Puma
2018-01-08T15:04:50.486716+00:00 app[web.1]: =&gt; Rails 5.1.4 application starting in production
2018-01-08T15:04:50.486718+00:00 app[web.1]: =&gt; Run `rails server -h` for more startup options
2018-01-08T15:04:50.486719+00:00 app[web.1]: Puma starting in single mode...
2018-01-08T15:04:50.486725+00:00 app[web.1]: * Version 3.11.0 (ruby 2.3.4-p301), codename: Love Song
2018-01-08T15:04:50.486733+00:00 app[web.1]: * Min threads: 5, max threads: 5
2018-01-08T15:04:50.486735+00:00 app[web.1]: * Environment: production
2018-01-08T15:04:50.486886+00:00 app[web.1]: * Listening on tcp://0.0.0.0:21213
2018-01-08T15:04:50.487396+00:00 app[web.1]: Use Ctrl-C to stop
```
這是我們平常下 `rails server` 指令時會看到的訊息。

```
2018-01-08T15:04:38.156226+00:00 heroku[web.1]: Unidling
2018-01-08T15:04:38.156470+00:00 heroku[web.1]: State changed from down to starting
```

heroku 的狀態從關機變成開始運作中。

```
2018-01-08T15:04:52.254468+00:00 app[web.1]: I, [2018-01-08T15:04:52.254363 #4]  INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Started POST &quot;/kamigo/webhook&quot; for 203.104.156.74 at 2018-01-08 15:04:52 +0000
2018-01-08T15:04:52.276728+00:00 app[web.1]: I, [2018-01-08T15:04:52.276573 #4]  INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Processing by KamigoController#webhook as HTML
2018-01-08T15:04:52.277008+00:00 app[web.1]: I, [2018-01-08T15:04:52.276880 #4]  INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c]   Parameters: {&quot;events&quot;=&gt;[{&quot;replyToken&quot;=&gt;&quot;00000000000000000000000000000000&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100001&quot;, &quot;type&quot;=&gt;&quot;text&quot;, &quot;text&quot;=&gt;&quot;Hello, world&quot;}}, {&quot;replyToken&quot;=&gt;&quot;ffffffffffffffffffffffffffffffff&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100002&quot;, &quot;type&quot;=&gt;&quot;sticker&quot;, &quot;packageId&quot;=&gt;&quot;1&quot;, &quot;stickerId&quot;=&gt;&quot;1&quot;}}], &quot;kamigo&quot;=&gt;{&quot;events&quot;=&gt;[{&quot;replyToken&quot;=&gt;&quot;00000000000000000000000000000000&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100001&quot;, &quot;type&quot;=&gt;&quot;text&quot;, &quot;text&quot;=&gt;&quot;Hello, world&quot;}}, {&quot;replyToken&quot;=&gt;&quot;ffffffffffffffffffffffffffffffff&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100002&quot;, &quot;type&quot;=&gt;&quot;sticker&quot;, &quot;packageId&quot;=&gt;&quot;1&quot;, &quot;stickerId&quot;=&gt;&quot;1&quot;}}]}}
2018-01-08T15:04:52.297993+00:00 app[web.1]: W, [2018-01-08T15:04:52.297845 #4]  WARN -- : [34361d41-4be8-4293-971d-eca0151ea11c] Can&#39;t verify CSRF token authenticity.
2018-01-08T15:04:52.298635+00:00 app[web.1]: I, [2018-01-08T15:04:52.298565 #4]  INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c] Completed 200 OK in 21ms
```

每一行的開頭都有這個：
```
2018-01-08T15:04:52.254468+00:00 app[web.1]: I, [2018-01-08T15:04:52.254363 #4]  INFO -- : [34361d41-4be8-4293-971d-eca0151ea11c]
```
這不是很重要，因為會妨礙閱讀，所以我們先忽略他，以下是忽略後的結果：

```
Started POST &quot;/kamigo/webhook&quot; for 203.104.156.74 at 2018-01-08 15:04:52 +0000
Processing by KamigoController#webhook as HTML
  Parameters: {&quot;events&quot;=&gt;[{&quot;replyToken&quot;=&gt;&quot;00000000000000000000000000000000&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100001&quot;, &quot;type&quot;=&gt;&quot;text&quot;, &quot;text&quot;=&gt;&quot;Hello, world&quot;}}, {&quot;replyToken&quot;=&gt;&quot;ffffffffffffffffffffffffffffffff&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100002&quot;, &quot;type&quot;=&gt;&quot;sticker&quot;, &quot;packageId&quot;=&gt;&quot;1&quot;, &quot;stickerId&quot;=&gt;&quot;1&quot;}}], &quot;kamigo&quot;=&gt;{&quot;events&quot;=&gt;[{&quot;replyToken&quot;=&gt;&quot;00000000000000000000000000000000&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100001&quot;, &quot;type&quot;=&gt;&quot;text&quot;, &quot;text&quot;=&gt;&quot;Hello, world&quot;}}, {&quot;replyToken&quot;=&gt;&quot;ffffffffffffffffffffffffffffffff&quot;, &quot;type&quot;=&gt;&quot;message&quot;, &quot;timestamp&quot;=&gt;1515423877419, &quot;source&quot;=&gt;{&quot;type&quot;=&gt;&quot;user&quot;, &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;}, &quot;message&quot;=&gt;{&quot;id&quot;=&gt;&quot;100002&quot;, &quot;type&quot;=&gt;&quot;sticker&quot;, &quot;packageId&quot;=&gt;&quot;1&quot;, &quot;stickerId&quot;=&gt;&quot;1&quot;}}]}}
Can&#39;t verify CSRF token authenticity.
Completed 200 OK in 21ms
```
這看起來就有點眼熟了。

他打了一個  POST 到我們的 `/kamigo/webhook`，並且傳了這樣的參數：

```
{
  &quot;events&quot;=&gt;[
    {
      &quot;replyToken&quot;=&gt;&quot;00000000000000000000000000000000&quot;, 
      &quot;type&quot;=&gt;&quot;message&quot;, 
      &quot;timestamp&quot;=&gt;1515423877419, 
      &quot;source&quot;=&gt;{
        &quot;type&quot;=&gt;&quot;user&quot;, 
        &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;
      }, 
      &quot;message&quot;=&gt;{
        &quot;id&quot;=&gt;&quot;100001&quot;, 
        &quot;type&quot;=&gt;&quot;text&quot;, 
        &quot;text&quot;=&gt;&quot;Hello, world&quot;
      }
    }, 
    {
      &quot;replyToken&quot;=&gt;&quot;ffffffffffffffffffffffffffffffff&quot;, 
      &quot;type&quot;=&gt;&quot;message&quot;, 
      &quot;timestamp&quot;=&gt;1515423877419, 
      &quot;source&quot;=&gt;{
        &quot;type&quot;=&gt;&quot;user&quot;, 
        &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;
      }, 
      &quot;message&quot;=&gt;{
        &quot;id&quot;=&gt;&quot;100002&quot;, 
        &quot;type&quot;=&gt;&quot;sticker&quot;, 
        &quot;packageId&quot;=&gt;&quot;1&quot;, 
        &quot;stickerId&quot;=&gt;&quot;1&quot;
      }
    }
  ], 
  &quot;kamigo&quot;=&gt;{
    &quot;events&quot;=&gt;[
      {
        &quot;replyToken&quot;=&gt;&quot;00000000000000000000000000000000&quot;, 
        &quot;type&quot;=&gt;&quot;message&quot;, 
        &quot;timestamp&quot;=&gt;1515423877419, 
        &quot;source&quot;=&gt;{
          &quot;type&quot;=&gt;&quot;user&quot;, 
          &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;
        }, 
        &quot;message&quot;=&gt;{
          &quot;id&quot;=&gt;&quot;100001&quot;, 
          &quot;type&quot;=&gt;&quot;text&quot;, 
          &quot;text&quot;=&gt;&quot;Hello, world&quot;
        }
      }, 
      {
        &quot;replyToken&quot;=&gt;&quot;ffffffffffffffffffffffffffffffff&quot;, 
        &quot;type&quot;=&gt;&quot;message&quot;, 
        &quot;timestamp&quot;=&gt;1515423877419, 
        &quot;source&quot;=&gt;{
          &quot;type&quot;=&gt;&quot;user&quot;, 
          &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;
        }, 
        &quot;message&quot;=&gt;{
          &quot;id&quot;=&gt;&quot;100002&quot;, 
          &quot;type&quot;=&gt;&quot;sticker&quot;, 
          &quot;packageId&quot;=&gt;&quot;1&quot;, 
          &quot;stickerId&quot;=&gt;&quot;1&quot;
        }
      }
    ]
  }
}
```

### 解讀 POST BODY

經過我精美的排版之後變得比較好閱讀了，這是一個很大的 Ruby hash(雜湊陣列)，我們在[第十三天：認識 Ruby 的資料型態](https://ithelp.ithome.com.tw/articles/10195196)學過這個。我把這個 hash 存成檔案，名叫 `line_verify.rb`，這麼作有一些好處，sublime text 會幫文字加顏色，以及可以使用縮小/展開功能。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhdkzsNhAA2cBJPDvvPOPsB4K1HgCXaN8xaH2P9sFo00qRPPMS9B2q8cfghdUAifmfPEsJzwOEm5bHU5B_WByr2IOIYhTJnMe5vE_d6hFpe3lkMEYtRvnasucbufklhYlBzNFDHm4JdgSo/s1600/1.jpg)

這個 hash 有兩個 key，分別是 events 和 kamigo，events 是一個陣列，而 kamigo 是一個 hash。

kamigo 是一個只有一個 key 的 hash，而這個 key 也叫作，events。更巧的是，這兩個 events 裡面包含的資料是相同的，所以我們只要看其中一個 events 就好。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi0vgYz9C60_y5_uWDOuAXchjDkJmNPKL3XOVBzJnq9tqwkoalA8cZWdWrVItyAD-l6fBoz7Z-gtdcyKnhRW638QzLHTioVILU20nya6jjCBVmkErg-ui71mW3FvY-d970O_7qzRiKIr7U/s1600/2.jpg)

這個 events 陣列，包含兩個 hash。這個 hash 我們在[第五天：認識 Line Messaging API Webhook](https://ithelp.ithome.com.tw/articles/10193441) 時已經介紹過了。這裡再簡單複習一下：

- replyToken：是我們要回覆訊息時必須傳回的值，他代表收件者以及你的回覆權。
- type：通知類型，這是一則訊息
- timestamp：傳送時間，我們通常會忽略他
- source：發信者
- message：訊息內容

接下來看第一個 hash 裡的 source 和 message。

```
&quot;source&quot;=&gt;{
  &quot;type&quot;=&gt;&quot;user&quot;, 
  &quot;userId&quot;=&gt;&quot;Udeadbeefdeadbeefdeadbeefdeadbeef&quot;
}
```
這是發生在私訊對話框，對方的 id 是 Udeadbeefdeadbeefdeadbeefdeadbeef。

```
&quot;message&quot;=&gt;{
  &quot;id&quot;=&gt;&quot;100001&quot;, 
  &quot;type&quot;=&gt;&quot;text&quot;, 
  &quot;text&quot;=&gt;&quot;Hello, world&quot;
}
```
對方傳來的訊息是文字訊息：「Hello, world」。

再看第二個 hash。第二個 hash 裡的 source 跟第一個一樣，所以我們只需要看 message 的部分。

```
&quot;message&quot;=&gt;{
  &quot;id&quot;=&gt;&quot;100002&quot;, 
  &quot;type&quot;=&gt;&quot;sticker&quot;, 
  &quot;packageId&quot;=&gt;&quot;1&quot;, 
  &quot;stickerId&quot;=&gt;&quot;1&quot;
}
```
對方傳來的訊息是貼圖訊息。

# 觀察實際傳遞的訊息

讓我們試著用 Line 傳訊息給我們的聊天機器人，看看會收到什麼：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMlyQ7a8tL5J9LNC5HKqeEYEnxbFx5GWsV6HeyZO6C0kveLtZapIP1pRPTTvqW_-qcj2fhWFSpDgW3wItgqCrAolvQXvWk8Ye8OVUWMrn5bYMLjECXjplsSffUWoxTBPijw9fv_MyjJjA/s1600/4.jpg)

看看小黑框，什麼事也沒發生。原來是 Line developer 後台還有東西沒設定。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYMwu_rXKeZdke_vrCzWpmCi7-DFWSincuXup6uP7XSiiQeP4Y_ZVZOY8_81DP-4nvpzYhD_T1bUGSc92HLhpU4IBYHgF0g44UGh7or7NxdhSwuYGnIhBbzSU-TX8ok8JoZYDUWdAl44g/s1600/3.jpg)

這兩個分別是`使用 Webhook` 和`可被邀請進群`，都把他打開。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-PXDkSv3QyTPe3QCGxuJhe3lB2PUG-pbPYfeVHGEwBT-FBuc3Klr4wrLTBCLFW5UtcDIEiYFVvzMReclB3OnCay4nEd3n7E41kncvsvNSPqA1sKYq-8lIngKUe36E5-NThbfCJEbTNBU/s1600/5.jpg)

都開啟後的樣子：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj19dYqISo4Kdla-qB0i1RpuBfyBxz-HwkE5niTLpK0KVNZRTYjU0qZHZG6Hd1g78GFV4BANHoHl_ZoN5srguHahxElSAH88hv0-A9MhSnyGTQ5mAo6dJW5ZcnTWc77S_mLf3_6d7zy-zs/s1600/6.jpg)

都開啟後再次傳訊息就會在小黑框看到：

```
{
  &quot;events&quot;=&gt;[
    {
      &quot;type&quot;=&gt;&quot;message&quot;, 
      &quot;replyToken&quot;=&gt;&quot;1d0bc2113fd344deb8131750e8d2daa2&quot;, 
      &quot;source&quot;=&gt;{
        &quot;userId&quot;=&gt;&quot;Uc68d82df46b7899e7d716f396ae8e91a&quot;, 
        &quot;type&quot;=&gt;&quot;user&quot;
      }, 
      &quot;timestamp&quot;=&gt;1515432401148, 
      &quot;message&quot;=&gt;{
        &quot;type&quot;=&gt;&quot;text&quot;, 
        &quot;id&quot;=&gt;&quot;7279127481400&quot;, 
        &quot;text&quot;=&gt;&quot;五五六六得第一&quot;
      }
    }
  ]
}
```

我把不重要的部分都刪除了，剩下來的部分看起來差不多，應該不用多作解釋，現在我們來作回覆訊息。

# 回覆訊息

### 安裝 Line Bot API

我們要先安裝一個 Line 提供的 gem，在 Gemfile 加入以下程式：

```
# line
gem &#39;line-bot-api&#39;
```

記得要在小黑框輸入 bundle 下載套件。

### 引入 Line Bot API

修改 `app/controllers/kamigo_controller.rb` 檔，在第一行加上`require &#39;line/bot&#39;`：

```
require &#39;line/bot&#39;
```

這表示我們要使用 Line 的套件。

### 修改 webhook

這個是 webhook 寫好的樣子，我會慢慢解釋他。

```
def webhook
  # Line Bot API 物件初始化
  client = Line::Bot::Client.new { |config|
    config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
    config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
  }
  
  # 取得 reply token
  reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]

  # 設定回覆訊息
  message = {
    type: &#39;text&#39;,
    text: &#39;好哦～好哦～&#39;
  }

  # 傳送訊息
  response = client.reply_message(reply_token, message)
    
  # 回應 200
  head :ok
end 
```

### Line Bot API 物件初始化

```
  client = Line::Bot::Client.new { |config|
    config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
    config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
  }
```

這段程式是我們使用 Line Bot API 內提供的物件，需要傳入兩個字串分別為 `Channel secret` 和 `Channel access token`。

你可以在 Line Developer 後台找到他們：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMvtwmgO89PEdgHdB0V2NHW9qlg0zUcfvXdFIS1ylTW3DYO8Qiu7k5mxDNaYhElJ2kNl_A2b_chXFI3_ycGCIHa752GQRWxutaWFfJnsnJiE8F53lcYxWblotLJ9O7U-3slyNteUNhPLk/s1600/7.jpg)

只要按下 `Issue` 按鈕，值就會變。


### 取得 reply token
```
reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
```
根據剛剛我們觀察的 hash 結構，我們大膽假設 `events` 陣列裡只有一筆資料，用 `[0]` 取得第一筆，然後取得他裡面的 `replyToken`。

### 設定回覆訊息
```
  message = {
    type: &#39;text&#39;,
    text: &#39;好哦～好哦～&#39;
  }
```

我們回覆的訊息是純文字訊息，內容是「好哦～好哦～」

### 傳送訊息
```
response = client.reply_message(reply_token, message)
``` 

我們透過 Line Bot API 包裝好的函數來回覆訊息，這樣我們就不用去寫在[第十六天：做一個最簡單的爬蟲](https://ithelp.ithome.com.tw/articles/10195760)寫過的那些東西。

### 對一下程式碼

這是完整的 `app/controllers/kamigo_controller.rb` 檔內容：

```
require &#39;line/bot&#39;
class KamigoController &lt; ApplicationController
  protect_from_forgery with: :null_session

  def webhook
    # Line Bot API 物件初始化
    client = Line::Bot::Client.new { |config|
      config.channel_secret = &#39;9160ce4f0be51cc72c3c8a14119f567a&#39;
      config.channel_token = &#39;2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU=&#39;
    }
    
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]

    # 設定回覆訊息
    message = {
      type: &#39;text&#39;,
      text: &#39;好哦～好哦～&#39;
    }

    # 傳送訊息
    response = client.reply_message(reply_token, message)
      
    # 回應 200
    head :ok
  end 

  ...下略

end
```

### 上傳程式碼

確認沒問題之後，因為我們沒辦法進行本地端的測試，所以就直接上傳程式碼：

```
git add .
git commit -m &quot;回覆訊息&quot;
git push heroku master
```

上傳完成後進行測試。

### 實測

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiW5PWuoQ-kxXMRNjvTQt0Rvfzq-krPNr9UOVO5L0ny5rKfZEjALXF_VHVJ1blCn3sx2AOIGRrgKvi05O9Vh3hTYxRnqnMa01nhcxSOgwK085R2yTVY0miyy-ryT9J36mqp0lkLdSrX_DI/s1600/8.jpg)

耶～我們終於作出最簡單的回應了。

如果你沒有辦法順利抵達這裡，那麼你有幾個可以作的事情，你可以讓 `heroku logs -t` 打出你想知道的內容。比方說，你想知道你的 reply_token 有沒有抓對：

```
    # 取得 reply token
    reply_token = params[&#39;events&#39;][0][&#39;replyToken&#39;]
    
    p &quot;======這裡是 reply_token ======&quot;
    p reply_token 
    p &quot;============&quot;
```

像這樣加上幾行 `p`，接著只要對 Line Bot 傳訊息，你就能在小黑框看到這個：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkY2KcbiPvHLBz61ktvryWb6JL00hq45xDqHi6JCAkhF4zYbE2AhdnbKQhyRk2mRhHA9t4-AuDK68TQct562NB5rRwBgbsooyTV3Ru7xlgVxDZ8XU33_u4PE6shXX1L9sr5VpMlMfdD30/s1600/9.jpg)

你可以把所有的變數都用 `p` 印出來慢慢看，看是不是你想要的值。其中最值得觀察的變數是 `response`。

如果你看不懂，可以對 logs 截圖在底下留言。

# 總結
  
- 你知道不同的通知只有些許差異。
- 你看懂通知傳遞的內容，以及如何用程式碼擷取出他的值
- 你學會了使用套件
- 你學會了怎麼讓 Line Bot 回覆訊息
- 你學會了在 heroku 上的除錯方法

明天之後就會越來越難了，要寫的程式會越來越多。這裡有一些 Ruby 的基礎教學，有興趣的人可以補一下：

[為你自己學 Ruby on Rails - 變數、常數、流程控制、迴圈](https://railsbook.tw/chapters/05-ruby-basic-1.html)

[為你自己學 Ruby on Rails - 數字、字串、陣列、範圍、雜湊、符號](https://railsbook.tw/chapters/06-ruby-basic-2.html)

[為你自己學 Ruby on Rails - 方法與程式碼區塊（block）](https://railsbook.tw/chapters/07-ruby-basic-3.html)

[為你自己學 Ruby on Rails - 類別（Class）與模組（Module）](https://railsbook.tw/chapters/08-ruby-basic-4.html)

</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/1458147080436600237/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/1458147080436600237' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/1458147080436600237'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/1458147080436600237'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/line-bot.html' title='第二十一天：讓 Line Bot 回覆訊息'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-8203975706265477550</id><published>2018-01-08T03:21:00.000+08:00</published><updated>2018-01-08T03:21:55.941+08:00</updated><title type='text'>第二十天：串接 Line Messaging API Webhook</title><content type='html'>markdown

大家還記得為什麼我們要作網站嗎？是因為我們要作 Line 聊天機器人啦。因為作 Line 所提供的 Line Messaging API Webhook 是透過 HTTP 協定來完成，所以我們需要弄一台網頁伺服器，讓 Line 有事情時可以透過 HTTP 協定來通知我們。

我們從[第四天：認識 Webhook](https://ithelp.ithome.com.tw/articles/10193212)開始學習怎麼架網頁伺服器，直到[第十九天：發布網站到 Heroku (續)](https://ithelp.ithome.com.tw/articles/10196250)，終於成功作出一個能讓別人連線的網頁伺服器。

當然在這個過程中，我們學會的不只是會架網站。我們還學會了怎麼使用檔案總管、小黑框、Sublime Text、git 和 heroku。這中間並沒有浪費時間，因為所有的學習都會在這個時候用上。而你終於有能力可以開始來接 Line Messaging API 啦。

接下來的文章會採取一邊實作，一邊說明的方式。同時也會一邊複習之前學過的東西。而今天我們的目標是完成 Line Messaging API Webhook 的串接。

# 接收第一個 POST 請求

因為 Line 會傳東西給我們，所以 HTTP 請求方法會是 `POST`，不會是以往我們使用的 `GET`，我們先作一個能接受 `POST` 的網址出來：

在 `config/routes.rb` 加入一行：
```
  post &#39;/kamigo/webhook&#39;, to: &#39;kamigo#webhook&#39;
```

在 `app/controllers/kamigo_controller.rb` 加入以下函數：
```
  def webhook
    head :ok
  end 
```

`head :ok` 的意思是傳回 HTTP status code :`200` 以及空的 HTTP body。複習一下在[第十二天：從瀏覽器認識 HTTP 協定](https://ithelp.ithome.com.tw/articles/10195016)有學過的，HTTP status code :`200` 表示成功。也就是說不管誰，傳什麼內容過來，我都會回一個成功。

用 `rails server` 開啟網頁伺服器，試試看直接在瀏覽器輸入網址 [http://localhost:3000/kamigo/webhook](http://localhost:3000/kamigo/webhook)：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjyKvCT3iJVXTLqGzWH3jQE0Nj8sfB1Esz1cTS3PwjyaWF7984ypgpBcFhIh0ICd91ecM7TUhJ_KOomFT2NZfwG0ey-ewrRwHWPRMlRXc1c64xOplk8cu2WA70GqP5r3zPYJmLXZc28o94/s1600/3.png)

Rails 返回的結果是 `No route matches [GET] &quot;/kamigo/webhook&quot;`，意思是我們沒有設定 `GET` 連接到 `/kamigo/webhook`，這表示我們直接在瀏覽器網址列輸入網址的話，是不可能產生出一個 `POST` 請求的。

有幾個方法可以作出 `POST` 請求，最常見的 `POST` 請求是在一個網頁上填表單按下送出的時候。我們不會採用這個方案，因為這個方案要寫程式。我們可以使用現成的工具來發出 POST 請求。

# POST 的測試

有一款工具叫作 POSTMAN：[https://www.getpostman.com/](https://www.getpostman.com/)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3O6Lt1aoqaoYBoCydndkjTI3WFdSFwi2fGqTaEi6AGEy-XOYzH1LmGlFfSXLjukTbXJVEU-vAxSgtrLc2VSdVc0rE2Fj_pPxPQpOBIoxZr4GzXALllrkb0OYznwX0E8Y-KcTAcTskUPg/s1600/4.jpg)

點一下 `Windows`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEd1t6KT1Y21-6EOVLnHb-8Op-uM9FfppKBdyF3PLAFN9JNhHqYLJzmTYtujOnz-MYF_Vvgg-NJTOWAU9VjSqRgmQCzTiTEwBCLQz7cMeCUwJfhxAhf46jApBuxPZ4XeTy4CpFaAovfHw/s1600/5.jpg)

選 `x64` 就會開始下載，下載好就點開。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEilZn1o5kVlbp1Q-kmuTeqDj92ZECT0_krcVYd_BCUDQHFzUarFnuyVd4HMmMOzWUNAN_QLi6DYaaR2VwWX6RMtGKorTpBx0HkuLUUZk9E4I6qqt34WOrmEUXfGI-HHqOimeQgmTjdaLO0/s1600/6.jpg)

POSTMAN 是一款功能眾多的軟體，但我們用不到進階功能，所以我們就不註冊帳號了，點選下面圈起來的 `Take me straight to the app. I&#39;ll create an account another time.` 略過註冊，直接使用。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYcOIV5sakgZLI2-1DuE-fiF2eZ9-9YRN_dIidlK8BaE2wtOXWODr4M83rJKDvP4wqFNBWRileUFVdbhhlec0UYDftIiclS8ZZrGmfB29HxN4d-0VmaCSPZxMu0SmZ5k7tzSH8UHKQOgw/s1600/7.jpg)

這是我們用不到的各種功能，直接關掉即可。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoo5IEmS55g17gFO1qQtDm6XFHORXd_i0zfc3INCcdU_Z-Kb5iB-D4ZpxcLbhAKYC02kNIX-gPLzN5hXXBb8zPkHR_rfRcOG0r-W9ygehD5illS3-utmGXBjJJ39KmYdmbmPzvqrazo0A/s1600/8.jpg)

這個區塊是讓我們填入 HTTP 請求方法和網址的地方。我們要用 `POST` 方法，網址是 `http://localhost:3000/kamigo/webhook`，填好按送出。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNJ6zBAMuI-3DuD0YG65UJAoD4E6N_TBvWyefzQ6aqs__ymrbgriG51GDZZyDA1PHeOmhST7JbGmshZIpXbaTLbk9VLP2XxDv88BMOilfawLMoYpAF_UvWySKng-TvKxlfkcgltqb1CiU/s1600/9.jpg)

結果收到的 status code 是 `422`，這是怎麼回事呢？

檢查一下 rails server 的小黑框會看到：
```
Started POST &quot;/kamigo/webhook&quot; for 127.0.0.1 at 2018-01-08 01:55:37 +0800
Processing by KamigoController#webhook as */*
Can&#39;t verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 2ms



ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):

...下略
```

為了防止世界被破壞，為了守護世界的和平...這是為了防範一種叫作 `CSRF` 的入侵手法。大概解釋一下他的原理：我可以作一個網頁，讓你在開啟網頁時，就讓你的瀏覽器對另一個網站發出 `POST` 請求，因為是從你的瀏覽器發出的，所以另一個網站如果沒有作檢查，可能就會中招。`CSRF` 可以代替你做很多事，比方說如果 Line 沒有防的話，我就可以讓你買 Line 貼圖送給我。

# 圖解 CSRF

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhP4dXzikFb3w3rNtldyzAPIzekR3JqoZkjMUqdXX-SR2Aez3Ic86MdlbN92mF0HCo1DRrOm4Sg5_RMPPDJvOIbKeV7A0lIXyaOUdQiYSqiMxpMAYrT6kVtsyQWQl1NxUYP9KhXBaQC6SM/s1600/10.jpg)

這是正常的情況，一個人類應該會先開啟一個表單頁，填好表之後再送出。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5hdTVpKghiiL-YK4yCo1t-Cgcx_B7NFKb7cGGjYjP-d7Ens8GHDFKqilIbuppLlvOv1cFoBDabzxOIjRj4xA8jNI5sPRMNo4TQ37Ta6LDNB_prnTBv317GECPiBlRYeqePqgzZyF-vKQ/s1600/11.jpg)

這是被入侵時會發生的情況，壞網頁會讓你的瀏覽器在不經過你同意的情況下，自己發出 `POST` 請求給另一個網站。

# CSRF 的防治

為了防止這個情形，我們的 Rails 網站會在人類在發出 `GET` 請求開啟表單頁時，就先給你一個號碼牌(`CSRF token`)，只有帶著正確號碼的號碼牌的人才能進 `POST` 請求，要不然就會被門口的警衛擋下來。但因為 Line 只會直接對我們發 `POST`，所以我們不能使用表單號碼牌的機制，我們會使用的是 Line 給我們的 `Channel access token`，這部分之後會再說明。

# 關閉 CSRF 的檢查

我們不能透過 Rails 內建的檢查機制來防治 `CSRF`，所以我們得先關閉內建的 `CSRF` 檢查。

在 `app/controllers/kamigo_controller.rb` 中加入一行：
```
protect_from_forgery with: :null_session
```
通常這行會放在最上面，像這樣：
```
class KamigoController &lt; ApplicationController
  protect_from_forgery with: :null_session
  ...下略
```

弄好後存檔，此時再去按 POSTMAN 的 `Send`，就會得到 Status: 200 OK，而我們的小黑框顯示的變成這樣：
```
Started POST &quot;/kamigo/webhook&quot; for 127.0.0.1 at 2018-01-08 02:41:37 +0800
Processing by KamigoController#webhook as */*
Can&#39;t verify CSRF token authenticity.
Completed 200 OK in 3ms
```

其中的 Can&#39;t verify CSRF token authenticity. 是在講說他沒有完成 CSRF 檢查。

# 上傳程式碼

我們上傳程式碼，上傳程式碼的三步驟：

```
git add .
git commit -m &quot;新增 webhook&quot;
git push heroku master
```

應該是會成功啦，我就不貼訊息了。

# 串接 Line Messaging API Webhook

開啟 Line developer 後台 [https://developers.line.me/console/](https://developers.line.me/console/)：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjR4XX-0afM7EMloUrdMQYnWkLYLSdvpxhlXV-JcVIKH9kIZBj6RA6tp8Ih6g5hWEPlAPmhw0EJcyHFTc9OS7oJxlv02CciBe5rKYCh5Nw1ASNFQN2cU85IbKvJWrHMf0BLJnVa5DcZYgM/s1600/1.jpg)

點聊天機器人進入：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiysddxxB2o1qh_FCsPIsBUV10xO9lJnlSuxsQ_2i5-tARrmxkmOyDES3PF4vXnYwi6onl9wIcYczXSGvEXsHhU5qR3DI0og8bqEBAVg0jlPyVZTfJ0FkbwvhnEYic1aTg2lGM3BvuCyaQ/s1600/12.jpg)

進入頁面後，你要找到上圖所在的 `Messaging settings` 區塊，其中有一項叫作 `Webhook URL`，點一下右邊的筆進入編輯：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEggB-6lf6huwfkuQC2xakHwNk5xyhKS9YmVD60NWJ59wg6ZEUtp_ijAsiYlJITtII4aqJjyDBSTHUE66AyNMEMXzdPmCOjU4CEoChXNlNasEGw37LCs2TetA38f-DFG_dayuD7vzw_ym7Q/s1600/13.jpg)

輸入我們剛剛作好的 POST 網址 `people-all-love-kamigo.herokuapp.com/kamigo/webhook`，注意這裡你要填的是你的網站的網址，不要填到我的。還有一個注意事項，開頭的 `https://` 不可以填。

填好後按下 `Update`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhX5f7qOlwnuoxt0MHt1jeUtU-invHKJdgVbxQ36JK99tf0CopAMAZHVvjojkiNcV_DWRMDk4m90MQJPZtTtIQm5FLHZnooaeIq8cHPwyUVee0Ho5lX_44o9YogXSZCdMD7auPf-DbTApo/s1600/14.jpg)

就會在畫面上看到一個 `Verify` 按紐，按下去。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiq6UuZPKpPjFTYC9iWb_6nJFcifRkHSw-9mEmfTYXh4ecp6R-KJvANw8jjdc1VTVKeBQGCQK11Yay7jkUR-9J6WdLHD9X4k3pfu_Z7qmNIyTFeE-a2P0MYAU5LsSqPcsKwTzhgQFDFu7Y/s1600/15.jpg)

Success 成功！

今天就先到這，明天就能讓聊天機器人講話了。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/8203975706265477550/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/8203975706265477550' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/8203975706265477550'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/8203975706265477550'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/line-messaging-api-webhook.html' title='第二十天：串接 Line Messaging API Webhook'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-6550085376314936182</id><published>2018-01-07T03:48:00.003+08:00</published><updated>2018-01-14T22:48:58.645+08:00</updated><title type='text'>第十九天：發布網站到 Heroku (續)</title><content type='html'>markdown

# 前情提要

昨天我們在發布網站的過程中遇到了錯誤：

```
remote:        In Gemfile:
remote:        sqlite3
remote:  !
remote:  !     Failed to install gems via Bundler.
remote:  !     Detected sqlite3 gem which is not supported on Heroku:
remote:  !     https://devcenter.heroku.com/articles/sqlite3
```

heroku：「ㄉㄅㄑ，我不會用 sqlite3。」

所以我們必須移除 sqlite3 這個套件才行。

# 認識 Gemfile

Gemfile 在專案的根目錄下：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhCIgrMqVSdlHVBvicaxARz43nBR6GIk2IXl34zKwvZhme-qNPh0R1k-bOjbnzC8TnK7uuK8Rve3G917Pd9MfFpduvVFziskR90ecxIlhaFL8L0mnqvHHBS4zw8AKDo7y2KH_iFjPzAEnw/s1600/28.jpg)

第12行寫著：`gem &#39;sqlite3&#39;` 表示這個專案會使用 sqlite3 這個套件。套件是一群佛心來的人寫好後公開給大家用的程式。如果你想要安裝套件，就會需要在這裡加一行程式，如果想要移除某個套件，就要刪掉那一行程式。

這裡有所有能裝的套件：[https://rubygems.org/gems](https://rubygems.org/gems)。如果你想要成佛也可以在這裡貢獻一下你的程式。

每當修改過 Gemfile 之後，你要在小黑框輸入 `bundle install`。 bundle 是一個管理套件的套件，他會幫你下載套件程式碼。

如果你輸入 `bundle install` 之後看到的是這樣：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;bundle install
&#39;bundle&#39; 不是內部或外部命令、可執行的程式或批次檔。
```

表示你的電腦沒有裝過 bundler，需要在小黑框輸入 `gem install bundler` 來安裝。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;gem install bundler
Fetching: bundler-1.16.1.gem (100%)
Successfully installed bundler-1.16.1
Parsing documentation for bundler-1.16.1
Installing ri documentation for bundler-1.16.1
Done installing documentation for bundler after 20 seconds
1 gem installed 
```

# 移除 rails 專案中所使用的 sqlite3 套件

我們開始進行 sqlite3 的移除工作吧。

前面提到 Gemfile 第12行寫著：`gem &#39;sqlite3&#39;`，我們可以在這行的最前面加一個 `#` 號把這行變成註解，像這樣：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgesrc7RuIRieOnpvrexe3RDXuhWATO1w3oYLFCDc73Hh0CiwuSX2cDpj98O0bTqAvVe8Jfte_17iHc0XtHQO-T1DHgHIe_ZLziLGVlQlLNTSSKT-6B_6csR0DFym_4OHEkze9AjjtDMRE/s1600/29.jpg)

當然你要直接刪掉那行也不是不行啦，弄好之後記得存檔，存完檔之後，在小黑框輸入 `bundle install`。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;bundle
Using rake 12.3.0
Using concurrent-ruby 1.0.5
Using i18n 0.9.1
Using minitest 5.10.3
Using thread_safe 0.3.6
Using tzinfo 1.2.4
Using activesupport 5.1.4
Using builder 3.2.3
Using erubi 1.7.0
Using mini_portile2 2.3.0
Using nokogiri 1.8.1 (x64-mingw32)
Using rails-dom-testing 2.0.3
Using crass 1.0.3
Using loofah 2.1.1
Using rails-html-sanitizer 1.0.3
Using actionview 5.1.4
Using rack 2.0.3
Using rack-test 0.8.2
Using actionpack 5.1.4
Using nio4r 2.2.0
Using websocket-extensions 0.1.3
Using websocket-driver 0.6.5
Using actioncable 5.1.4
Using globalid 0.4.1
Using activejob 5.1.4
Using mini_mime 1.0.0
Using mail 2.7.0
Using actionmailer 5.1.4
Using activemodel 5.1.4
Using arel 8.0.0
Using activerecord 5.1.4
Using public_suffix 3.0.1
Using addressable 2.5.2
Using bindex 0.5.0
Using bundler 1.16.1
Using byebug 9.1.0
Using xpath 2.1.0
Using capybara 2.16.1
Using ffi 1.9.18 (x64-mingw32)
Using childprocess 0.8.0
Using coffee-script-source 1.12.2
Using execjs 2.7.0
Using coffee-script 2.4.1
Using method_source 0.9.0
Using thor 0.20.0
Using railties 5.1.4
Using coffee-rails 4.2.2
Using multi_json 1.12.2
Using jbuilder 2.7.0
Using puma 3.11.0
Using sprockets 3.7.1
Using sprockets-rails 3.2.1
Using rails 5.1.4
Using rb-fsevent 0.10.2
Using rb-inotify 0.9.10
Using rubyzip 1.2.1
Using sass-listen 4.0.0
Using sass 3.5.4
Using tilt 2.0.8
Using sass-rails 5.0.7
Using selenium-webdriver 3.8.0
Using turbolinks-source 5.0.3
Using turbolinks 5.0.1
Using tzinfo-data 1.2017.3
Using uglifier 4.0.2
Using web-console 3.5.1
Bundle complete! 12 Gemfile dependencies, 66 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

你會看到目前專案正在使用的所有套件。

# 進行本機測試

開啟 `rails server` 看看是否一切正常：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails s
=&gt; Booting Puma
=&gt; Rails 5.1.4 application starting in development
=&gt; Run `rails server -h` for more startup options
*** SIGUSR2 not implemented, signal based restart unavailable!
*** SIGUSR1 not implemented, signal based restart unavailable!
*** SIGHUP not implemented, signal based logs reopening unavailable!
Puma starting in single mode...
* Version 3.11.0 (ruby 2.4.2-p198), codename: Love Song
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
```

看似正常，開網頁 [http://localhost:3000/](http://localhost:3000/) 看卻發現：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj93wiegLHmtoY-k2IRmlMGYVNKyZhyphenhyphen6qTAu6tHSLpXhvler7QPjocLv9CQyhcf_viBMoQxaTFmKFMTfLZ6SXhcaG533T1sb6hUzm-Fd91vsjAhJsFm4gB-xPmPezAfPO3xFeS-AIMN118/s1600/4.jpg)

他說：「你的程式有用到 sqlite3，但是你沒有安裝 sqlite3，所以我又爆啦。」

# 我們在那裡用到了 sqlite3？

我們在 `config/database.yml` 中使用了 `sqlite3`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgE39PLsv_vnojsSJxkQaFkTQZXyHEIswHOauk6ientPAQMK-iLOeX89kGPJJnEgiH1xTVnM1RefKbCpz-w1nj6H1m3qDEaG3x091pa_uqthbAZgV6XRYxMk3F21iC92wSUpNVfxraO95o/s1600/5.jpg)

原來 `sqlite3` 是一個資料庫的套件，但是由於 heroku 不支援 `sqlite3`，所以我們必須找另一款 heroku 能支援的資料庫換上去，這裡我們要換的是 `postgresql`。

# 改用 postgresql

我們需要把 `config/database.yml` 中第 8 行的 `adapter: sqlite3` 改為 `adapter: postgresql`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhsgFjtli1QcsRvWdHgxph2z6bNOMw1D0deBt_N6s_MXYFdtuHVF1G_POByKLUbiRISdlMX_DAp-Bb2V8YN-k8nUbBKu5inDVMhb3gtZao_rjiw8sna4tNSgwGPHrRWxkJfELMQHdtkv3U/s1600/6.jpg)

我們還需要安裝 `postgresql` 這個套件，這個套件叫 `pg`，所以修改 Gemfile 如下：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQJNWxf8scoyZvVR3-pzHLOsdmt52X9CNE6RV_qded2mGB6vLjmw2nmnWMrNBdM8hCSt5uGw5FtpxhvU4pTadtETSkfeUBlDv7xJDbQxAAMhpBd7VZw0Q4LthVPcGqYGbD5vX5S6cFayc/s1600/7-2.jpg)

我在第 13 行寫 `gem &#39;pg&#39;, &#39;~&gt; 0.21.0&#39;`，其實要寫在第幾行都行。不過第 37 行有一個 group：

```
group :development, :test do
  # Call &#39;byebug&#39; anywhere in the code to stop execution and get a debugger console
  gem &#39;byebug&#39;, platforms: [:mri, :mingw, :x64_mingw]
  # Adds support for Capybara system testing and selenium driver
  gem &#39;capybara&#39;, &#39;~&gt; 2.13&#39;
  gem &#39;selenium-webdriver&#39;
end
```
`group :development, :test do` 這行的意思是，只有在開發和測試環境下才需要安裝的套件。因為我們的 heroku 的環境是 :production，所以我們如果把 `gem &#39;pg&#39;, &#39;~&gt; 0.21.0&#39;` 寫在這個 group 裡的話，heroku 就還是壞的。

其中`~&gt; 0.21.0` 表示版本號，一個套件會隨著時間不斷更新，就像 iOS 和 Android 也會一直系統更新一樣。我們可以指定想要安裝的版本。

改完 Gemfile 之後，先關閉網頁伺服器，然後在小黑框輸入 `bundle install` 安裝新套件完之後再打開網頁伺服器。

# 再次進行本機測試

打開網頁：[http://localhost:3000/](http://localhost:3000/)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjukkeD979LVT-WiYhFVPtZBIFTtaufDMGn06OyOe4JGxRk0xpHKA1LyoHyJeFyWco7WwCTv7625TpkEVIEIJL2XctPikaAQYDymtb6uJMd14ncnpVOi1SZKFcSQGP7FDVEls8804EqNxY/s1600/8.jpg)

正常！

# 把改動後的程式碼上傳到 heroku

上傳到 heroku 之前要先建立一個 git 版本，之前提到過的上傳三步驟：

```
git add .
git commit -m &quot;註解&quot;
git push heroku master
```

如果你覺得註解亂寫沒關係的話，我是沒差，你可以直接複製上面三行程式碼到小黑框貼上。不過我就一步步來。

### `git add .`

選擇所有檔案加入這次的版本：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;git add .
warning: LF will be replaced by CRLF in Gemfile.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in Gemfile.lock.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/database.yml.
The file will have its original line endings in your working directory.
```

### `git commit -m &quot;改用pg&quot;`

建立一個版本，我的註解是寫 `改用pg` 說明我們這次的改動：
```
D:\只要有心，人人都可以作卡米狗\ironman&gt;git commit -m &quot;改用pg&quot;
[master e6f3871] &#39;改用pg&#39;
 3 files changed, 5 insertions(+), 2 deletions(-)

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```
### `git push heroku master`
上傳程式碼到 heroku：
```
D:\只要有心，人人都可以作卡米狗\ironman&gt;git push heroku master
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 297 bytes | 297.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----&gt; Ruby app detected
remote: -----&gt; Compiling Ruby/Rails
remote: -----&gt; Using Ruby version: ruby-2.3.4
remote: ###### WARNING:
remote:        Removing `Gemfile.lock` because it was generated on Windows.
remote:        Bundler will do a full resolve so native gems are handled properly.
remote:        This may result in unexpected gem versions being used in your app.
remote:        In rare occasions Bundler may not be able to resolve your dependencies at all.
remote:        https://devcenter.heroku.com/articles/bundler-windows-gemfile
remote:
remote: -----&gt; Installing dependencies using bundler 1.15.2
remote:        Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin -j4
remote:        The dependency tzinfo-data (&gt;= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
remote:        Fetching gem metadata from https://rubygems.org/..........
remote:        Fetching version metadata from https://rubygems.org/..
remote:        Fetching dependency metadata from https://rubygems.org/.
remote:        Resolving dependencies...
remote:        Using rake 12.3.0
remote:        Using concurrent-ruby 1.0.5
remote:        Using minitest 5.11.1
remote:        Using thread_safe 0.3.6
remote:        Using builder 3.2.3
remote:        Using erubi 1.7.0
remote:        Using mini_portile2 2.3.0
remote:        Using crass 1.0.3
remote:        Using rack 2.0.3
remote:        Using nio4r 2.2.0
remote:        Using websocket-extensions 0.1.3
remote:        Using mini_mime 1.0.0
remote:        Using arel 8.0.0
remote:        Using bundler 1.15.2
remote:        Using coffee-script-source 1.12.2
remote:        Using execjs 2.7.0
remote:        Using method_source 0.9.0
remote:        Using thor 0.20.0
remote:        Using ffi 1.9.18
remote:        Using multi_json 1.12.2
remote:        Fetching pg 0.21.0
remote:        Using puma 3.11.0
remote:        Using rb-fsevent 0.10.2
remote:        Using tilt 2.0.8
remote:        Using turbolinks-source 5.0.3
remote:        Using tzinfo 1.2.4
remote:        Using nokogiri 1.8.1
remote:        Using i18n 0.9.1
remote:        Using rack-test 0.8.2
remote:        Using sprockets 3.7.1
remote:        Using websocket-driver 0.6.5
remote:        Using mail 2.7.0
remote:        Using coffee-script 2.4.1
remote:        Using uglifier 4.1.2
remote:        Using rb-inotify 0.9.10
remote:        Using turbolinks 5.0.1
remote:        Using loofah 2.1.1
remote:        Using activesupport 5.1.4
remote:        Using sass-listen 4.0.0
remote:        Using rails-html-sanitizer 1.0.3
remote:        Using rails-dom-testing 2.0.3
remote:        Using globalid 0.4.1
remote:        Using activemodel 5.1.4
remote:        Using jbuilder 2.7.0
remote:        Using sass 3.5.5
remote:        Using actionview 5.1.4
remote:        Using activejob 5.1.4
remote:        Using activerecord 5.1.4
remote:        Using actionpack 5.1.4
remote:        Using actioncable 5.1.4
remote:        Using actionmailer 5.1.4
remote:        Using railties 5.1.4
remote:        Using sprockets-rails 3.2.1
remote:        Using coffee-rails 4.2.2
remote:        Using rails 5.1.4
remote:        Using sass-rails 5.0.7
remote:        Installing pg 0.21.0 with native extensions
remote:        Bundle complete! 13 Gemfile dependencies, 56 gems now installed.
remote:        Gems in the groups development and test were not installed.
remote:        Bundled gems are installed into ./vendor/bundle.
remote:        Bundle completed (11.93s)
remote:        Cleaning up the bundler cache.
remote:        The latest bundler is 1.16.1, but you are currently running 1.15.2.
remote:        To update, run `gem install bundler`
remote: -----&gt; Installing node-v6.11.1-linux-x64
remote: -----&gt; Detecting rake tasks
remote: -----&gt; Preparing app for Rails asset pipeline
remote:        Running: rake assets:precompile
remote:        Yarn executable was not detected in the system.
remote:        Download Yarn at https://yarnpkg.com/en/docs/install
remote:        Asset precompilation completed (1.50s)
remote:        Cleaning assets
remote:        Running: rake assets:clean
remote:
remote: ###### WARNING:
remote:        You have not declared a Ruby version in your Gemfile.
remote:        To set your Ruby version add this line to your Gemfile:
remote:        ruby &#39;2.3.4&#39;
remote:        # See https://devcenter.heroku.com/articles/ruby-versions for more information.
remote:
remote: ###### WARNING:
remote:        Removing `Gemfile.lock` because it was generated on Windows.
remote:        Bundler will do a full resolve so native gems are handled properly.
remote:        This may result in unexpected gem versions being used in your app.
remote:        In rare occasions Bundler may not be able to resolve your dependencies at all.
remote:        https://devcenter.heroku.com/articles/bundler-windows-gemfile
remote:
remote: ###### WARNING:
remote:        No Procfile detected, using the default web server.
remote:        We recommend explicitly declaring how to boot your server process via a Procfile.
remote:        https://devcenter.heroku.com/articles/ruby-default-web-server
remote:
remote: -----&gt; Discovering process types
remote:        Procfile declares types     -&gt; (none)
remote:        Default types for buildpack -&gt; console, rake, web, worker
remote:
remote: -----&gt; Compressing...
remote:        Done: 36.7M
remote: -----&gt; Launching...
remote:        Released v8
remote:        https://people-all-love-kamigo.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/people-all-love-kamigo.git
   e6f3871..5ec30ea  master -&gt; master

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

這次看似上傳成功了，從最後面往上看，倒數第 5 行有一個網址：[https://people-all-love-kamigo.herokuapp.com/](https://people-all-love-kamigo.herokuapp.com/)，以及三個 WARNING。

先不管 WARNING，我們複製網址進去看(你的網址會跟我的不同，取決於你之前下 `heroku create` 指令時輸入的網站名稱)。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEixnZmC7ickUcyXxmw0QLPOKrbJ3DnkfEjt5R8bK2yArxyhnHlsZhgBL3eNC5An0l4J3x7wPsP8IKtevKR9dG-7c7XeIUUTbBWkavVNsEsXj9Ku5A099RrfUfZfmurJX463Kw3DBh7sYlA/s1600/9.jpg)

Heroku說：「找不到網頁。」

這是因為在我們本機的首頁，其實是不在專案資料夾裡面的，記得一開始修改網頁([第九天：作一個最簡單的 Rails 網站](https://ithelp.ithome.com.tw/articles/10194359))時，我們是去哪改嗎？是在 `C:/Ruby24-x64/lib/ruby/gems/2.4.0/gems/railties-5.1.4/lib/rails/templates/rails/welcome/index.html.erb`，而這個檔案並不是在專案資料夾下，所以遠端伺服器沒有這個檔案也是很合邏輯的。

事實上，只有在 `config/routes.rb` 檔案裏面有寫到的那些路徑才是我們應該測的路徑，所以我們應該連到這裡：[https://people-all-love-kamigo.herokuapp.com/kamigo/eat](https://people-all-love-kamigo.herokuapp.com/kamigo/eat)。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaenErZrdsI21jtkv98KRqSFI9KfemS3ixGIHVKNz28z1yVU-Do-prtjZDToBdzaODgl5cA3tTAZrTXO5EMm0xgfzRLt_MCI1V4AsQ0l57EuqycTMDnCpFfegMDcdekKT8zl10dsjL06I/s1600/10.jpg)

吃土啦～

# 總結

程式碼除錯的流程大致上為：

- 閱讀錯誤訊息
- 修改程式碼
- 測試網站，若失敗則回到第一步
- 發布網站

工程師的日常就是除了一個錯之後看到的不是正常，而是看到下一個錯誤訊息。

要有耐心。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/6550085376314936182/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/6550085376314936182' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/6550085376314936182'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/6550085376314936182'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/heroku.html' title='第十九天：發布網站到 Heroku (續)'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-725227137210721911</id><published>2018-01-06T12:54:00.001+08:00</published><updated>2018-01-06T12:54:17.155+08:00</updated><title type='text'>第十八天：發布網站到 Heroku（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）</title><content type='html'>markdown

今天我們要發布網站到 Heroku，第一步就是要先註冊一個 Heroku 帳號：

# 註冊 Heroku 帳號

點開這個網址：[https://signup.heroku.com/login](https://signup.heroku.com/login)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2nmfBk-6897FC70rtAZ_lJNNq5Htnm97eLJV4iWodgATwlygRE2Vya8SLf6QhEp0Rumon92A1Vct7zOAa8exAX93WL1Y5i2XY8PVwjADKMIirMhj99lbl9_lUoGuY_w7rCqtgjhWdiJk/s1600/2.png)

- First name：名字
- Last name：姓氏
- Email Address：信箱
- Role：職業
- Primary Development Language：主要開發語言

除了信箱之外，其他的都沒有很重要，怕亂填會出問題的話就抄我的。填好之後就按 `CREATE FREE ACCOUNT`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhCE9TVPZvqQHajbuwwGSoldALXG-yUSesvqS84PyBOjeTk_ZyRVfeF8fXuUlcyfDN7xP_KvNclhict_S1vKTK0LZe9S0sIUUx4u7ksvzNbrTXsCLgbmi6yf_jhiysRWaFZ-0KHFt8fX74/s1600/3.jpg)

他說：「請去收信，謝謝」，這個步驟是為了確認你真的有你所填的信箱。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh1cbyUx2bMdHt408IYMUx7P53gb4evUL4jYQG9eFSkXILlAqvxPdlTh_2gXRRTd7Np4WKH8RxQYucxo4EtP_7mE5c1tae07kAg-149ORXqQIq6wPHSj4LNY4IDzKbRFkvgnKlx_7aT01w/s1600/4.jpg)

這裡點一下信中的超連結：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgiu7eiO8U79Dg6JH9KBVCPmV8uWAFgkY2KGG6PcPnTW9hSyO7DpSw2z9PPb5_17JMA8tfimtKRMLbMFkzBtZ4o64QlgFiVA7S2sKYDbGNf7JhckMoiASWIP8vwMoEIp8wZYDANuEU6lS0/s1600/5.jpg)

這裡要輸入兩次密碼，密碼最少要 8 個字，輸入完之後按下 `SET PASSWORD AND LOG IN`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiiOMrRqwjIoRzZf5j_EE_sLtwutOadL57RAExj-mMFC-w4WiitRSMms-QVN95zBVfbsjs1kHov2lwLZjJUsCt1Is5_ugvdvaUwsqHhT4dEhbRRJ8ld2fweY65NDPNLlzDjCiFrHouUeo4/s1600/6.jpg)

他說：「歡迎光臨」，這裡點一下 `CLICK HERE TO PROCEED`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPABGUk9XXd-bJqQPtP0CNCyUNnu3BpZ2JrsegUJ_bqoec0Z9fmS6y07zVJg64hOgtzaPOOyGTympkqhQUMfWx8y219bDZN4AAABYpY57aQLpIMHbQ7e5JiZdEtNzf4RTpFV7VvAtV0EU/s1600/7.jpg)

這是平常登入會看到的畫面，他提供了各種教學，不過不用看他的，看我的就好。

我們平常不會透過這個後台去操作他，我們會使用 Heroku 提供的 Heroku CLI 來操作他。

# 安裝 Heroku CLI

點開這個網址：[https://devcenter.heroku.com/articles/heroku-cli#windows](https://devcenter.heroku.com/articles/heroku-cli#windows)

然後點 `64-bit` 下載安裝檔，下載後點開。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEind8QNL5kl77il4u7XBcwxJE_j6UUJ7IEg0t9yyoKhyphenhyphenwxTOGO6yccBQZjOF_04vxzgICkSpSJkycsDn0GuyjMvI9Dyip5rYuRrCQwjGzeah9ZDzHr-l9F23pp7J67Ej-EGfSG281HMnSw/s1600/8.jpg)

這邊直接點 `Next`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9g7jHuMNu8arG8I5to4wGNf5um2aGQF3zKgE32t3P6lG7QtjqiJpJNDKfdBbmyUuztoCbpfhNns0G05f70OxXGpCtw3XQBIueLIh56toaeZh-morFjTMJpRowc1aIl7AOE_-bA3yqaHA/s1600/9.jpg)

點 `Next`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtyo8ut0ymjSon6b1HdWzll-8eYZXyODNaBaybtMyIvbyAenS8PN3L_CMJNg5Oqa_1nPqP50RrnoOQ3ddynUBPFpfMyxiKRSYz1of4sWuT0jgLqy5CRn5OTa5Wg7-ZFJpGT_0x96w9cMs/s1600/10.jpg)

安裝完成！點 `Close`。

然後什麼事也沒發生。所以我們剛剛安裝了什麼？

# 試用 Heroku CLI

開啟一個小黑框，輸入 `heroku`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh33bkQ0dD7Q8QTCaV_0HXq-w8EBBIzszrczFHE5p1mvOrO6j1C-rpieAvwi56qTz-CNGAq5o5zymf30hmqsb1Mg9p-DjnRHXGcjBCvta_YL7VQGeok3hzrXYEDp2yaQOUnxtnh72_hEbU/s1600/11.jpg)

如果你沒有看到這個畫面，那表示你可能在安裝的過程中遭遇到一些問題。或許可以考慮下載 `32-bit` 的安裝檔重裝一次看看。

這裡是指令教學，跟卡米狗教學差不多，不過不用看他的，看我的就好。

輸入 `heroku login` 作登入：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj2zasHRsSQNOPyir94EH7qniOeRf4NkkftatCQnQ0Cu0hBGNKPxSMwmOE8Pzy7Bfw41_SmABEqwLbjrBIIOwb421F7A0jT5GDCKa2BK4dkL3ky2rHBBftRws-_GTa224t25STj4c7RTLU/s1600/13.jpg)

他說：「請輸入信箱：」

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhUZtN4gtX0S-qBCU9ZkSX2_K4Q4Y23UKiDY9CzeWCmdHes08vgbYzP5l5ZauQrucjhPpvnDKOnoePb7-7l9a0X0AE2qyLxCyDwqqYlzSyOJGmtoSBu6cg2K1SEXjcQ3fP2dgP6P6Z3cCg/s1600/14.jpg)

他說：「請輸入密碼：」

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE9vsPg38vviv0rkEr0bWHpu70xmBggasB8xSqUXJFtLocoT_-cX5Tx7lzamZUebOD3yXaek2wlEXgptuHXd0nJ7tudnf_2skN2nlgbAPqMTHFl_HJLgEAsbnM593y9dsGxLpGlSZ3AYM/s1600/12.jpg)

他說：「登入成功。」

heroku 的初始設定到這裡就算完成了，接下來我們安裝 Git。

# 安裝 Git

點開這個網址：[https://git-scm.com/download/win](https://git-scm.com/download/win)，沒意外的話點開就已經自動下載了。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi06PWHj3UNA0_YDhOfUX8cLDoVr3iyb2Hqk71LpG6FQaC6WiiDcE7odmWx9pZsfxqI_RLty63GTXBFH9jsAgcUyGLKnksxF_kD8cZWr98EpZeOFJeEfHTo17bcjfAqIGte8XyeQEkjais/s1600/18.jpg)

要開始安裝了，安裝流程很長，注意看我選什麼，跟著選就對了。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSlSD45arz98wnNrgujNfij8_JH1fjroLtmjQh92MLt5tC3lhEfgBTV1EkEnH5oiu_PyKy3LMkdYbo2-SqJoiSm2t5wTP-XZlrwJVvJmLvEzAn0o_pJZ1lGgTM-FL1ZKL4J0Z3xbID5qM/s1600/16.jpg)

這是填安裝路徑，沒事不要亂改。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXZWpXPpEoCLGp5IY_0Z1MhWKKJ5Y0SkHup4R0xjsHwYPo9ZiaESU0Q2UTYOzJJ0Zh3-sRBK2Yjo_cuQmIJ4M_Nouc9ZAnoqQXK3Hecntgbe-S2Ws6yQ3bBt3K-h7wYA1NUb48tRjzBJE/s1600/17.jpg)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg43xzGYrZ3jyQe9s9Tc9xr4SMr7yviKkjK7VZMHXbV_gV_H8OgTn7t58jCNPRIiKbV6fCcQUX9x99oQ0AmXUTgFRswOdsP0VeTeM5EAbPNJHoZ4YaIjyfe34sbJKtLf3wrcdH9E_AA-ZA/s1600/18.jpg)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7SY8D7L8_SMtQksABI2ZOaaJbIBGdXy8hyMg9Uv5X5O9zuu5h4f6ou5wt3n4AzJiJOY0zA0d-Y3iuqluVGrLWXon9DixXU6lQRUjZ7HTI0WczzWHoXdFfjH9rak8Pcydzj2f9hfjyMFQ/s1600/19.jpg)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhP5hHx47BIfw2eIwWgo9M_14wvcx59D0iw6V-OQnMSHLbq1oR-7B6j5wIsKyiWLRi0S8BbBsiJ1_opdRHXQQdVGyhZ-FWF1dRJqpqVvTvdt97uyiO10N_oNw7ZbB_1epoUUmSJoKayOWE/s1600/20.jpg)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjuRC79uaiWpcvuMRbbGzN-zjUhhuHlWigA9BqvoTzpi33n0O3HQh0epxF8lX0VhMGBvWBUWiqsqVCLnZPMs_IOVIhZlzZKRCndhL9gTHV9v-C4RleZ3QnnYt4-mj-KHWZ6Y5k2u4-Iw6s/s1600/21.jpg)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGW6dYOTJuShxBJonVDSf_lV7HCAjUpef66UXRXfwf_MGPrj3nTUP0lbHRzdkiE1MIt4sYdBignLQgNKbF1_QSHVfTwfySKhgVygD6f34xpbx-vUb6OglJyRfZojCWNdD4sFu3Zij3Yw0/s1600/22.jpg)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8RRKlRIpoQgKEmqUi5cEDgPY7UpYrXUEbrFU99dQy6J6MS85nax_RTWLSdOSKKUNRiEorad90KSJYODWx5oMhXD7ROleesvTxe-rnWsNuXVhChS89Ko3Y6_d8JHrZX-SnWruocqXZGmc/s1600/23.jpg)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYed0cklwMeVXi2bc9AnLC9958wEGlrLPsLp4i15eTjnpo8urRQV16kMvoaJ9C3wX1kTBwqyz5g0SaDhpSDInwgxcBHWLri5_VTmdvKkgw6B7DzUoteS91fOZu9wWfa7cFLhm7KZXdfzQ/s1600/24.jpg)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiq09wTG32u4QZde1RHGYNkg6gPVjpO9V2TDgQErPmceLwLXaZkZBOl7VfPXsFRLNjgpb3dOyM9md1aD403-CxPUd9r8wj12pXZ2ow1-qEgyI96smit05V4MEQkCuj2OjWkAmmLnR9OiUY/s1600/25.jpg)

跟 Heroku 一樣，這個安裝好之後要到小黑框才能用。

# 試用 Git

開啟一個小黑框，輸入 `git`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOd1nYHUrIyPq7OVqd0N-ulf1ywrvmD0NuPuVShPJv2V6WXjnM_la_31WZohO5_-e8DTsh-Ol9eBEg-xwG7wgdc2Z7Z4Ob9VxE6vSNOtJmMnGbuGbfhY5R58n0xeyqRX83oHkwko7IAIg/s1600/26.jpg)

如果你沒有看到上面的畫面，那表示你可能在安裝的過程中遭遇到一些問題。遇到問題的話請截圖留言。 #在我的電腦上是好的

到這裡 git 就安裝完成了。

# 專案的 git 初始設定

我們會使用小黑框來上傳程式，先開一個在專案目錄下的小黑框，然後輸入 `git init`。

你可能會看到：
```
D:\只要有心，人人都可以作卡米狗\ironman&gt;git init
Initialized empty Git repository in D:/只要有心，人人都可以作卡米狗/ironman/.git/
```
或者看到：
```
D:\只要有心，人人都可以作卡米狗\ironman&gt;git init
Reinitialized existing Git repository in D:/只要有心，人人都可以作卡米狗/ironman/.git/
```
看到這兩個其中一個都是正常現象，都沒關係，看到其他的就見鬼啦。

# 專案的 heroku 初始設定

我們要在 heroku 建立一台新的網頁伺服器，輸入的指令是 `heroku create ` + 專案名稱，這個名稱要夠特殊，要不然會跟別人撞名。

我選的專案名稱是 `people-all-love-kamigo`，所以我輸入 `heroku create people-all-love-kamigo`。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;heroku create people-all-love-kamigo
Creating people-all-love-kamigo... done
https://people-all-love-kamigo.herokuapp.com/ | https://git.heroku.com/people-all-love-kamigo.git

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```
建立好之後回到 heroku 的後台([https://dashboard.heroku.com/apps](https://dashboard.heroku.com/apps))上你會看到一個 app：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjVaOg88lWTvtL0iP7FQ9bgiOoeF2g_j86-utaQqEePbASGLMLG88mRER8c00k_9_Lh30x71UVBdOOqu36L7fpdjWgeHiF_tQ_0AdWSnw7ix8hT1vX1xjdgd9A5Aj7owDubwAhnV8CyyN0/s1600/27.jpg)

在 heroku 上面網頁伺服器被稱為 app，這就是我們剛剛建立的網頁伺服器。

# 上傳程式碼到 Heroku

我們要使用 git 來上傳程式碼，而 git 是一款非常強大的版本控管軟體，擁有非常多的功能。但我們要上傳程式碼只需要學習其中的 3 個指令。

- git add
- git commit
- git push

一個個來，首先是 git add，git add 可以指定這次想要加入控管的檔案，我們輸入 `git add .`，表示我們想把所有的檔案都加入控管。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;git add .
warning: LF will be replaced by CRLF in .gitignore.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in Gemfile.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in Gemfile.lock.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in README.md.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in Rakefile.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/assets/config/manifest.js.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/assets/javascripts/application.js.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/assets/javascripts/cable.js.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/assets/stylesheets/application.css.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/channels/application_cable/channel.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/channels/application_cable/connection.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/controllers/application_controller.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/helpers/application_helper.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/jobs/application_job.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/mailers/application_mailer.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/models/application_record.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/views/layouts/application.html.erb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/views/layouts/mailer.html.erb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in app/views/layouts/mailer.text.erb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in bin/bundle.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in bin/rails.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in bin/rake.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in bin/setup.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in bin/update.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in bin/yarn.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config.ru.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/application.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/boot.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/cable.yml.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/database.yml.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/environment.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/environments/development.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/environments/production.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/environments/test.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/initializers/application_controller_renderer.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/initializers/assets.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/initializers/backtrace_silencers.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/initializers/cookies_serializer.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/initializers/filter_parameter_logging.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/initializers/inflections.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/initializers/mime_types.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/initializers/wrap_parameters.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/locales/en.yml.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/puma.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/routes.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in config/secrets.yml.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in db/seeds.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in package.json.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in public/404.html.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in public/422.html.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in public/500.html.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in public/robots.txt.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in test/application_system_test_case.rb.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in test/test_helper.rb.
The file will have its original line endings in your working directory.

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```
一堆訊息但不是很重要，他說：「我把 windows 的換行符號改成別種系統的換行符號了哦。」

接下來是 git commit， git commit 表示我們想要建立一個新的版本，我們要留下一些紀錄說明這個版本作了什麼變更，因為是第一次，所以我們輸入 `git commit -m init`。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;git commit -m init
[master (root-commit) 8c48959] init
 76 files changed, 1203 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Gemfile
 create mode 100644 Gemfile.lock
 create mode 100644 README.md
 create mode 100644 Rakefile
 create mode 100644 app/assets/config/manifest.js
 create mode 100644 app/assets/images/.keep
 create mode 100644 app/assets/javascripts/application.js
 create mode 100644 app/assets/javascripts/cable.js
 create mode 100644 app/assets/javascripts/channels/.keep
 create mode 100644 app/assets/stylesheets/application.css
 create mode 100644 app/channels/application_cable/channel.rb
 create mode 100644 app/channels/application_cable/connection.rb
 create mode 100644 app/controllers/application_controller.rb
 create mode 100644 app/controllers/concerns/.keep
 create mode 100644 app/controllers/kamigo_controller.rb
 create mode 100644 app/helpers/application_helper.rb
 create mode 100644 app/jobs/application_job.rb
 create mode 100644 app/mailers/application_mailer.rb
 create mode 100644 app/models/application_record.rb
 create mode 100644 app/models/concerns/.keep
 create mode 100644 app/views/kamigo/eat.html
 create mode 100644 app/views/layouts/application.html.erb
 create mode 100644 app/views/layouts/mailer.html.erb
 create mode 100644 app/views/layouts/mailer.text.erb
 create mode 100644 bin/bundle
 create mode 100644 bin/rails
 create mode 100644 bin/rake
 create mode 100644 bin/setup
 create mode 100644 bin/update
 create mode 100644 bin/yarn
 create mode 100644 config.ru
 create mode 100644 config/application.rb
 create mode 100644 config/boot.rb
 create mode 100644 config/cable.yml
 create mode 100644 config/database.yml
 create mode 100644 config/environment.rb
 create mode 100644 config/environments/development.rb
 create mode 100644 config/environments/production.rb
 create mode 100644 config/environments/test.rb
 create mode 100644 config/initializers/application_controller_renderer.rb
 create mode 100644 config/initializers/assets.rb
 create mode 100644 config/initializers/backtrace_silencers.rb
 create mode 100644 config/initializers/cookies_serializer.rb
 create mode 100644 config/initializers/filter_parameter_logging.rb
 create mode 100644 config/initializers/inflections.rb
 create mode 100644 config/initializers/mime_types.rb
 create mode 100644 config/initializers/wrap_parameters.rb
 create mode 100644 config/locales/en.yml
 create mode 100644 config/puma.rb
 create mode 100644 config/routes.rb
 create mode 100644 config/secrets.yml
 create mode 100644 db/seeds.rb
 create mode 100644 lib/assets/.keep
 create mode 100644 lib/tasks/.keep
 create mode 100644 log/.keep
 create mode 100644 package.json
 create mode 100644 public/404.html
 create mode 100644 public/422.html
 create mode 100644 public/500.html
 create mode 100644 public/apple-touch-icon-precomposed.png
 create mode 100644 public/apple-touch-icon.png
 create mode 100644 public/favicon.ico
 create mode 100644 public/robots.txt
 create mode 100644 test/application_system_test_case.rb
 create mode 100644 test/controllers/.keep
 create mode 100644 test/fixtures/.keep
 create mode 100644 test/fixtures/files/.keep
 create mode 100644 test/helpers/.keep
 create mode 100644 test/integration/.keep
 create mode 100644 test/mailers/.keep
 create mode 100644 test/models/.keep
 create mode 100644 test/system/.keep
 create mode 100644 test/test_helper.rb
 create mode 100644 tmp/.keep
 create mode 100644 vendor/.keep

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

一樣一堆訊息，他說：「這次建立的版本新增了這些檔案哦。」

「好哦～好哦～」

接下來是 git push，git push 可以把我們建立好的版本，傳送到遠端的 git server，當然我們是要傳送到 heroku，所以我們輸入的是 `git push heroku master`。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;git push heroku master
Counting objects: 85, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (70/70), done.
Writing objects: 100% (85/85), 20.55 KiB | 1.28 MiB/s, done.
Total 85 (delta 2), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote:  !     Warning: Multiple default buildpacks reported the ability to handle this app. The first buildpack in the list below will be used.
remote:                         Detected buildpacks: Ruby,Node.js
remote:                         See https://devcenter.heroku.com/articles/buildpacks#buildpack-detect-order
remote: -----&gt; Ruby app detected
remote: -----&gt; Compiling Ruby/Rails
remote: -----&gt; Using Ruby version: ruby-2.3.4
remote: ###### WARNING:
remote:        Removing `Gemfile.lock` because it was generated on Windows.
remote:        Bundler will do a full resolve so native gems are handled properly.
remote:        This may result in unexpected gem versions being used in your app.
remote:        In rare occasions Bundler may not be able to resolve your dependencies at all.
remote:        https://devcenter.heroku.com/articles/bundler-windows-gemfile
remote:
remote: -----&gt; Installing dependencies using bundler 1.15.2
remote:        Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin -j4
remote:        The dependency tzinfo-data (&gt;= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
remote:        Fetching gem metadata from https://rubygems.org/..........
remote:        Fetching version metadata from https://rubygems.org/..
remote:        Fetching dependency metadata from https://rubygems.org/.
remote:        Resolving dependencies...
remote:        Fetching rake 12.3.0
remote:        Fetching concurrent-ruby 1.0.5
remote:        Fetching minitest 5.11.1
remote:        Installing minitest 5.11.1
remote:        Installing rake 12.3.0
remote:        Installing concurrent-ruby 1.0.5
remote:        Fetching thread_safe 0.3.6
remote:        Installing thread_safe 0.3.6
remote:        Fetching builder 3.2.3
remote:        Installing builder 3.2.3
remote:        Fetching erubi 1.7.0
remote:        Fetching mini_portile2 2.3.0
remote:        Installing erubi 1.7.0
remote:        Fetching crass 1.0.3
remote:        Installing mini_portile2 2.3.0
remote:        Fetching rack 2.0.3
remote:        Installing crass 1.0.3
remote:        Fetching nio4r 2.2.0
remote:        Installing rack 2.0.3
remote:        Installing nio4r 2.2.0 with native extensions
remote:        Fetching websocket-extensions 0.1.3
remote:        Installing websocket-extensions 0.1.3
remote:        Fetching mini_mime 1.0.0
remote:        Installing mini_mime 1.0.0
remote:        Fetching arel 8.0.0
remote:        Installing arel 8.0.0
remote:        Using bundler 1.15.2
remote:        Fetching coffee-script-source 1.12.2
remote:        Fetching execjs 2.7.0
remote:        Installing execjs 2.7.0
remote:        Installing coffee-script-source 1.12.2
remote:        Fetching method_source 0.9.0
remote:        Fetching thor 0.20.0
remote:        Installing method_source 0.9.0
remote:        Installing thor 0.20.0
remote:        Fetching ffi 1.9.18
remote:        Fetching multi_json 1.12.2
remote:        Installing multi_json 1.12.2
remote:        Fetching puma 3.11.0
remote:        Installing puma 3.11.0 with native extensions
remote:        Installing ffi 1.9.18 with native extensions
remote:        Fetching rb-fsevent 0.10.2
remote:        Installing rb-fsevent 0.10.2
remote:        Fetching tilt 2.0.8
remote:        Installing tilt 2.0.8
remote:        Fetching sqlite3 1.3.13
remote:        Installing sqlite3 1.3.13 with native extensions
remote:        Fetching turbolinks-source 5.0.3
remote:        Installing turbolinks-source 5.0.3
remote:        Fetching i18n 0.9.1
remote:        Installing i18n 0.9.1
remote:        Fetching tzinfo 1.2.4
remote:        Installing tzinfo 1.2.4
remote:        Fetching nokogiri 1.8.1
remote:        Installing nokogiri 1.8.1 with native extensions
remote:        Fetching websocket-driver 0.6.5
remote:        Installing websocket-driver 0.6.5 with native extensions
remote:        Fetching mail 2.7.0
remote:        Installing mail 2.7.0
remote:        Fetching rack-test 0.8.2
remote:        Installing rack-test 0.8.2
remote:        Fetching sprockets 3.7.1
remote:        Installing sprockets 3.7.1
remote:        Fetching uglifier 4.1.2
remote:        Installing uglifier 4.1.2
remote:        Fetching coffee-script 2.4.1
remote:        Installing coffee-script 2.4.1
remote:        Fetching turbolinks 5.0.1
remote:        Installing turbolinks 5.0.1
remote:        Fetching activesupport 5.1.4
remote:        Installing activesupport 5.1.4
remote:        Fetching rb-inotify 0.9.10
remote:        Installing rb-inotify 0.9.10
remote:        Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
remote:        current directory:
remote:        /tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/bundle/ruby/2.3.0/gems/sqlite3-1.3.13/ext/sqlite3
remote:        /tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/ruby-2.3.4/bin/ruby -r
remote:        ./siteconf20180106-281-1k06dok.rb extconf.rb
remote:        checking for sqlite3.h... no
remote:        sqlite3.h is missing. Try &#39;brew install sqlite3&#39;,
remote:        &#39;yum install sqlite-devel&#39; or &#39;apt-get install libsqlite3-dev&#39;
remote:        and check your shared library search path (the
remote:        location where your sqlite3 shared library is located).
remote:        *** extconf.rb failed ***
remote:        Could not create Makefile due to some reason, probably lack of necessary
remote:        libraries and/or headers.  Check the mkmf.log file for more details.  You may
remote:        need configuration options.
remote:        Provided configuration options:
remote:        --with-opt-dir
remote:        --without-opt-dir
remote:        --with-opt-include
remote:        --without-opt-include=${opt-dir}/include
remote:        --with-opt-lib
remote:        --without-opt-lib=${opt-dir}/lib
remote:        --with-make-prog
remote:        --without-make-prog
remote:        --srcdir=.
remote:        --curdir
remote:        --ruby=/tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/ruby-2.3.4/bin/$(RUBY_BASE_NAME)
remote:        --with-sqlite3-config
remote:        --without-sqlite3-config
remote:        --with-pkg-config
remote:        --without-pkg-config
remote:        --with-sqlite3-dir
remote:        --without-sqlite3-dir
remote:        --with-sqlite3-include
remote:        --without-sqlite3-include=${sqlite3-dir}/include
remote:        --with-sqlite3-lib
remote:        --without-sqlite3-lib=${sqlite3-dir}/lib
remote:        To see why this extension failed to compile, please check the mkmf.log which can
remote:        be found here:
remote:        /tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0/sqlite3-1.3.13/mkmf.log
remote:        extconf failed, exit code 1
remote:        Gem files will remain installed in
remote:        /tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/bundle/ruby/2.3.0/gems/sqlite3-1.3.13
remote:        for inspection.
remote:        Results logged to
remote:        /tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0/sqlite3-1.3.13/gem_make.out
remote:        An error occurred while installing sqlite3 (1.3.13), and Bundler cannot
remote:        continue.
remote:        Make sure that `gem install sqlite3 -v &#39;1.3.13&#39;` succeeds before bundling.
remote:        In Gemfile:
remote:        sqlite3
remote:        Bundler Output: The dependency tzinfo-data (&gt;= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
remote:        Fetching gem metadata from https://rubygems.org/..........
remote:        Fetching version metadata from https://rubygems.org/..
remote:        Fetching dependency metadata from https://rubygems.org/.
remote:        Resolving dependencies...
remote:        Fetching rake 12.3.0
remote:        Fetching concurrent-ruby 1.0.5
remote:        Fetching minitest 5.11.1
remote:        Installing minitest 5.11.1
remote:        Installing rake 12.3.0
remote:        Installing concurrent-ruby 1.0.5
remote:        Fetching thread_safe 0.3.6
remote:        Installing thread_safe 0.3.6
remote:        Fetching builder 3.2.3
remote:        Installing builder 3.2.3
remote:        Fetching erubi 1.7.0
remote:        Fetching mini_portile2 2.3.0
remote:        Installing erubi 1.7.0
remote:        Fetching crass 1.0.3
remote:        Installing mini_portile2 2.3.0
remote:        Fetching rack 2.0.3
remote:        Installing crass 1.0.3
remote:        Fetching nio4r 2.2.0
remote:        Installing rack 2.0.3
remote:        Installing nio4r 2.2.0 with native extensions
remote:        Fetching websocket-extensions 0.1.3
remote:        Installing websocket-extensions 0.1.3
remote:        Fetching mini_mime 1.0.0
remote:        Installing mini_mime 1.0.0
remote:        Fetching arel 8.0.0
remote:        Installing arel 8.0.0
remote:        Using bundler 1.15.2
remote:        Fetching coffee-script-source 1.12.2
remote:        Fetching execjs 2.7.0
remote:        Installing execjs 2.7.0
remote:        Installing coffee-script-source 1.12.2
remote:        Fetching method_source 0.9.0
remote:        Fetching thor 0.20.0
remote:        Installing method_source 0.9.0
remote:        Installing thor 0.20.0
remote:        Fetching ffi 1.9.18
remote:        Fetching multi_json 1.12.2
remote:        Installing multi_json 1.12.2
remote:        Fetching puma 3.11.0
remote:        Installing puma 3.11.0 with native extensions
remote:        Installing ffi 1.9.18 with native extensions
remote:        Fetching rb-fsevent 0.10.2
remote:        Installing rb-fsevent 0.10.2
remote:        Fetching tilt 2.0.8
remote:        Installing tilt 2.0.8
remote:        Fetching sqlite3 1.3.13
remote:        Installing sqlite3 1.3.13 with native extensions
remote:        Fetching turbolinks-source 5.0.3
remote:        Installing turbolinks-source 5.0.3
remote:        Fetching i18n 0.9.1
remote:        Installing i18n 0.9.1
remote:        Fetching tzinfo 1.2.4
remote:        Installing tzinfo 1.2.4
remote:        Fetching nokogiri 1.8.1
remote:        Installing nokogiri 1.8.1 with native extensions
remote:        Fetching websocket-driver 0.6.5
remote:        Installing websocket-driver 0.6.5 with native extensions
remote:        Fetching mail 2.7.0
remote:        Installing mail 2.7.0
remote:        Fetching rack-test 0.8.2
remote:        Installing rack-test 0.8.2
remote:        Fetching sprockets 3.7.1
remote:        Installing sprockets 3.7.1
remote:        Fetching uglifier 4.1.2
remote:        Installing uglifier 4.1.2
remote:        Fetching coffee-script 2.4.1
remote:        Installing coffee-script 2.4.1
remote:        Fetching turbolinks 5.0.1
remote:        Installing turbolinks 5.0.1
remote:        Fetching activesupport 5.1.4
remote:        Installing activesupport 5.1.4
remote:        Fetching rb-inotify 0.9.10
remote:        Installing rb-inotify 0.9.10
remote:        Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
remote:
remote:        current directory:
remote:        /tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/bundle/ruby/2.3.0/gems/sqlite3-1.3.13/ext/sqlite3
remote:        /tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/ruby-2.3.4/bin/ruby -r
remote:        ./siteconf20180106-281-1k06dok.rb extconf.rb
remote:        checking for sqlite3.h... no
remote:        sqlite3.h is missing. Try &#39;brew install sqlite3&#39;,
remote:        &#39;yum install sqlite-devel&#39; or &#39;apt-get install libsqlite3-dev&#39;
remote:        and check your shared library search path (the
remote:        location where your sqlite3 shared library is located).
remote:        *** extconf.rb failed ***
remote:        Could not create Makefile due to some reason, probably lack of necessary
remote:        libraries and/or headers.  Check the mkmf.log file for more details.  You may
remote:        need configuration options.
remote:
remote:        Provided configuration options:
remote:        --with-opt-dir
remote:        --without-opt-dir
remote:        --with-opt-include
remote:        --without-opt-include=${opt-dir}/include
remote:        --with-opt-lib
remote:        --without-opt-lib=${opt-dir}/lib
remote:        --with-make-prog
remote:        --without-make-prog
remote:        --srcdir=.
remote:        --curdir
remote:        --ruby=/tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/ruby-2.3.4/bin/$(RUBY_BASE_NAME)
remote:        --with-sqlite3-config
remote:        --without-sqlite3-config
remote:        --with-pkg-config
remote:        --without-pkg-config
remote:        --with-sqlite3-dir
remote:        --without-sqlite3-dir
remote:        --with-sqlite3-include
remote:        --without-sqlite3-include=${sqlite3-dir}/include
remote:        --with-sqlite3-lib
remote:        --without-sqlite3-lib=${sqlite3-dir}/lib
remote:
remote:        To see why this extension failed to compile, please check the mkmf.log which can
remote:        be found here:
remote:
remote:        /tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0/sqlite3-1.3.13/mkmf.log
remote:
remote:        extconf failed, exit code 1
remote:
remote:        Gem files will remain installed in
remote:        /tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/bundle/ruby/2.3.0/gems/sqlite3-1.3.13
remote:        for inspection.
remote:        Results logged to
remote:        /tmp/build_64f38be744eeec1bbcc825497e62d9fc/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0/sqlite3-1.3.13/gem_make.out
remote:
remote:        An error occurred while installing sqlite3 (1.3.13), and Bundler cannot
remote:        continue.
remote:        Make sure that `gem install sqlite3 -v &#39;1.3.13&#39;` succeeds before bundling.
remote:
remote:        In Gemfile:
remote:        sqlite3
remote:  !
remote:  !     Failed to install gems via Bundler.
remote:  !     Detected sqlite3 gem which is not supported on Heroku:
remote:  !     https://devcenter.heroku.com/articles/sqlite3
remote:  !
remote:  !     Push rejected, failed to compile Ruby app.
remote:
remote:  !     Push failed
remote: Verifying deploy...
remote:
remote: !       Push rejected to people-all-love-kamigo.
remote:
To https://git.heroku.com/people-all-love-kamigo.git
 ! [remote rejected] master -&gt; master (pre-receive hook declined)
error: failed to push some refs to &#39;https://git.heroku.com/people-all-love-kamigo.git&#39;

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

一樣是一堆訊息，其實他這步驟作了非常多的事情，不過我們只要關注結果就好，結果就是 `Push rejected, failed to compile Ruby app.`，失敗惹。

失敗的原因寫在上面：
```
remote:        In Gemfile:
remote:        sqlite3
remote:  !
remote:  !     Failed to install gems via Bundler.
remote:  !     Detected sqlite3 gem which is not supported on Heroku:
remote:  !     https://devcenter.heroku.com/articles/sqlite3
```
heroku：「ㄉㄅㄑ，我不會用 sqlite3。」

今天就先到這裡，明天再講怎麼修。

</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/725227137210721911/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/725227137210721911' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/725227137210721911'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/725227137210721911'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/heroku-2018-it.html' title='第十八天：發布網站到 Heroku（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-5280004540217595569</id><published>2018-01-05T02:24:00.002+08:00</published><updated>2018-01-05T02:45:17.426+08:00</updated><title type='text'>第十七天：怎麼讓別人連到我作好的網站？（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）</title><content type='html'>markdown

你知道為什麼在瀏覽器輸入網址 [http://localhost:3000/](http://localhost:3000/) 就能連到自己的網頁伺服器嗎？

# 認識 IP 位址(Internet Protocol Address)

IP 位址就像經緯度，是由數字所構成。每一台電腦只要連上網，都會有一個 IP 位址。如果你想知道你的 IP 位址，可以開啟這個網頁：[http://www.whatismyip.com.tw/](http://www.whatismyip.com.tw/)。

舉個例：`127.0.0.1` 就是一個 IP 位址。

`127.0.0.1` 是一個特殊的 IP 位址，他代表的意義是`我家`。也就是說，任何人在瀏覽器上輸入 `127.0.0.1` 只會連到他自己的電腦。

所以輸入 [http://127.0.0.1:3000/](http://127.0.0.1:3000/)，也能連到自己的網頁伺服器。

# 認識網址

一個網址包含很多資訊，讓我舉個例，以 [https://ithelp.ithome.com.tw/users/20107309/ironman/1253](https://ithelp.ithome.com.tw/users/20107309/ironman/1253) 來說：

- `https://`：這是通訊協定，代表寄出的是明信片還是包裹
- `ithelp.ithome.com.tw`：這是網域，代表收件者住址
- `/users/20107309/ironman/1253`：這是路徑，代表收件者姓名

# 通訊協定

我們只討論 HTTP 和 HTTPS 通訊協定，之前也提到過很多次，這裡就不多講了。

# 網域(Domain Name)

因為 IP 位址不好記憶，就跟經緯度不好記憶一樣。一個有腦子的人類會幫每個地方取名，你也會想幫你的網站取名，那就是網域名稱，當然你取的名字必須是人家沒取過的。

每個網域會對應到一個 IP 位址，而 IP 位址對應到一台電腦，所以每個網域都會對應到一台電腦。如果你想要幫自己的電腦取名，你得找一個網域商購買你想要的網域名稱，還要指定這個網域所對應的 IP 位址。如果你很有錢的話，你也可以幫同一台電腦取多個名字。

因為 `localhost` 網域所對應的 IP位址就是 `127.0.0.1`，所以在瀏覽器輸入網址 [http://localhost:3000/](http://localhost:3000/) 就能連到自己的網頁伺服器。

# 路徑(Path)

在古代，每個路徑都會對應到一個檔案，古代的網址結尾還可以看見副檔名呢！現代的網頁伺服器都有路由功能，可以讀取路徑後決定要用哪一段程式來回應，也就是我們之前寫過的 Route 和 Controller Action。

# Port

一台電腦可以同時運作許多的網路服務，你可以想像成一家百貨公司有很多專櫃，要買不同的商品就要到不同的櫃位。如果你想買東西，你得知道你要去哪個櫃位買。而櫃位就是 Port。

啟動一個網頁伺服器，就像是雇一個櫃姐去站櫃台一樣，他必須一直站在那等客人來。就算都沒客人來，他也不能偷偷滑手機。

一台電腦可以同時啟動許多網頁伺服器，只要他們的網域或者 Port 不相同就可以。

如果網址沒有特別標示 Port，那就會採用預設值。預設的 HTTP Port 是 80，而 HTTPS Port 是 443。

舉個例：我們的網頁伺服器會去站在 Port 3000 的櫃位，所以我們連線到網頁伺服器要輸入[http://localhost:3000/](http://localhost:3000/) 或輸入 [http://127.0.0.1:3000/](http://127.0.0.1:3000/)。

# 怎麼讓別人連到我的網站？

剛才說 `localhost` 代表自己的電腦，所以你如果把這個網址傳給別人，別人點了之後不會連到你的電腦，而是連到他自己的電腦。所以如果我想要讓別人連到我的網站，我要給他怎樣的網址呢？

你需要給的網址在這裡：[http://www.whatismyip.com.tw/](http://www.whatismyip.com.tw/)，這個網頁所告訴你的IP位址，是代表你電腦的IP位址，任何人輸入這個IP位址都是連到你這。

假設我的IP位址是 `100.100.100.100`，那麼在網址列輸入 [http://100.100.100.100:3000/](http://100.100.100.100:3000/) 就會連到我的網站。

# 可是我剛剛試了但沒有成功

事情並沒有這麼單純。如果你試了但沒有成功，表示你的電腦是透過防火牆連上網路，剛剛那個網頁所取得的 IP 位址是防火牆的 IP 位址，不是你的電腦的 IP 位址，所以人家連不進你的電腦。

你必須在防火牆設定當有人要從 Port 3000 連到防火牆時，就把連線轉到你的電腦上，這樣人家才連得到你的電腦。我不打算教你怎麼設定防火牆，因為每個人的網路環境不同，而且你的電腦應該也不會想一直開著。我試過了，開著一個月都沒關的電費大概是 600 元。

# 所以要怎麼讓別人連到我的網站？

我們在開發網站的時候，通常只有在測試網頁功能時會開啟網頁伺服器。像這種不給外人連線的環境，稱為開發環境。而我們會建立另一個專門給外人連線的環境，稱為發布環境。

當一個網站寫好之後，我們會把程式碼放到一台不關機的主機上，這台主機需要設定網域、網路環境，需要安裝網頁伺服器相關程式，然後啟動網頁伺服器就放著。建立一個發布環境也需要很多知識，根據設備不同，要作的事情也會不同。

# 那有比較簡單的方法嗎？

有的，如果我們使用別人家的發布環境，那我們只需要把我們的程式碼放到別人的伺服器上就搞定了。

以卡米狗為例，我是使用 Heroku 來發布卡米狗，Heroku 有一定的免費額度，如果用量小的話是不用花錢的。我一開始也是這樣想，所以就把卡米狗放上去了，但他已經變成一個吃貨。

如果你想要使用 Heroku，你需要在 Heroku 註冊一個帳號，然後再把自己的程式碼傳上去，上傳程式碼需要使用 git 這個軟體。

git 是一個版本控管軟體，你可以直接理解為他是用來上傳和下載程式碼的軟體就好，不用太在意什麼版本控管，接下來我只會講怎麼使用 git 上傳程式碼，不會提到其他部分。有興趣深入學習 git 的人可以參考這個電子書：[https://gitbook.tw/](https://gitbook.tw/)，如果覺得習慣看影片學的人可以走這邊：[https://campus.5xruby.tw/p/git](https://campus.5xruby.tw/p/git)。

明天會講怎麼把我們作好的廢物網站弄上 Heroku。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/5280004540217595569/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/5280004540217595569' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/5280004540217595569'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/5280004540217595569'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/2018-it.html' title='第十七天：怎麼讓別人連到我作好的網站？（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-5361175659647275589</id><published>2018-01-04T02:20:00.000+08:00</published><updated>2018-01-04T03:33:23.535+08:00</updated><title type='text'>第十六天：做一個最簡單的爬蟲（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）</title><content type='html'>markdown

一直以來，我們都是用瀏覽器發出 `HTTP request`，打到我們的 Rails Server，然後我們的 Rails Server 再傳回 `HTTP Response`。

我們還沒有試過用 Rails Server 發出 `HTTP request`，今天就來試一下。

# 用 Rails 發 HTTP request
### Routes
在 `config/routes.rb` 加入
```
  get &#39;/kamigo/sent_request&#39;, to: &#39;kamigo#sent_request&#39;
```

### Action
在 `app/controllers/kamigo_controller.rb` 加入
```
  def sent_request
    uri = URI(&#39;http://localhost:3000/kamigo/response_body&#39;)
    response = Net::HTTP.get(uri)
    render plain: response
  end
```

這是最簡單能發出 `Request` 的方法，我們先把網址字串轉換成 `URI` 物件，透過 `Net::HTTP.get` 這個方法，他會接受一個網址，然後把 `Response` 的 `Body` 部分以字串的形式傳回。在這段程式裡面我們只能看到傳入網址跟最終結果，我們不能調整 `Request Header` 和 `Request Body`，也不能觀察 `Response Header`。

我讓他連去 [http://localhost:3000/kamigo/response_body](http://localhost:3000/kamigo/response_body)，這是我們昨天作好的東西。

### 在瀏覽器開啟網址
網址：[http://localhost:3000/kamigo/sent_request](http://localhost:3000/kamigo/sent_request)
開啟網址後會看到：
```
虎哇花哈哈哈
```

### 小黑框的顯示結果
```
Started GET &quot;/kamigo/sent_request&quot; for 127.0.0.1 at 2018-01-04 01:06:38 +0800
Processing by KamigoController#sent_request as HTML
Started GET &quot;/kamigo/response_body&quot; for 127.0.0.1 at 2018-01-04 01:06:39 +0800
Processing by KamigoController#show_response_body as */*
===這是設定前的response.body:===
  Rendering text template
  Rendered text template (0.0ms)
===這是設定後的response.body:虎哇花哈哈哈===
Completed 200 OK in 344ms (Views: 9.2ms)


  Rendering text template
  Rendered text template (0.0ms)
Completed 200 OK in 1400ms (Views: 2.7ms)
```

突然想到有個東西還沒講，那就是 Ruby 的函數。

# Ruby 的函數

一個函數可以把一個東西變成另一個東西，或者把多個東西變成另一個東西。

舉個例：我們來作一個函數，輸入是一句話，我們要把這句話變成韓文。

### 函數宣告
```
  def translate_to_korean(message)
    &quot;#{message}油~&quot;
  end
```

一個函數裡面可以寫一段程式，最後一行程式的執行結果就會是函數的傳回值。

不管傳入什麼，我們就在後面加上`油~`。

### 函數呼叫
```
  translate_to_korean(&#39;愛老虎&#39;)
```

你想要使用小括號或者使用空白框住傳入的參數都可以。

```
  translate_to_korean &#39;愛老虎&#39;
```

### 結果
```
  &quot;愛老虎油~&quot;
```

成功翻譯成韓文！

# 把翻譯韓文的功能加入到上面的爬蟲

### 修改 Action
```
  def sent_request
    uri = URI(&#39;http://localhost:3000/kamigo/response_body&#39;)
    response = Net::HTTP.get(uri)
    render plain: translate_to_korean(response)
  end
```

### 開啟網頁

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj9MdSRnLh1llHSA6vjV0L2PIpk2vHDkN24yQojeqoZoi_o9qVpC4FweKUukoDZxTis3cZLZXR7-XI0LssKlU1a_AW-M0_7dSGpF-iqzqS_rlG2Cc43ATtI14Yp2aApZwDgD3TrbA4jZBs/s1600/1.jpg)

壞掉囉，錯誤訊息是 `incompatible character encodings: ASCII-8BIT and UTF-8`。

下面那個深灰色區域是給我們除錯用的，有點類似 `irb`，我們可以在下面輸入一點東西：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiubOD2gKe_ypBCjGrsay6W5JLpo946U0rvcn0lVbGndI6XsXh9et3HGSJwMbVqfAjC0py6bJbl-ypO2X-THDfllUyfl-ydpwJeuDeoy4RF5oZ-PPIGXaMroXGvz1nP9cpN1KCviL6jzIk/s1600/2.jpg)

我輸入了 `message`，看看 message 的值是什麼，結果看到是這樣：

```
&gt;&gt;  message
=&gt; &quot;\xE8\x99\x8E\xE5\x93\x87\xE8\x8A\xB1\xE5\x93\x88\xE5\x93\x88\xE5\x93\x88&quot;
&gt;&gt;  
```

因為我們獲得的 response 字串的編碼是 `ASCII-8BIT`，沒辦法直接跟 `UTF-8` 編碼的 &quot;油~&quot; 加在一起。

所以修正的方法就是把 response 字串轉碼為 UTF-8 即可：

### 再次修改 Action
```
  def sent_request
    uri = URI(&#39;http://localhost:3000/kamigo/response_body&#39;)
    response = Net::HTTP.get(uri).force_encoding(&quot;UTF-8&quot;)
    render plain: translate_to_korean(response)
  end
```
在字串後面寫 `.force_encoding(&quot;UTF-8&quot;)` 就可以把編碼轉為 `UTF-8` 格式了。

### 再次開啟網頁
```
虎哇花哈哈哈油~
```

玩樂時間結束，來作點正事，目前的寫法沒辦法觀察 `request` 和 `response`，我們需要能觀察 `request` 和 `response` 的寫法。

# 能觀察 request 和 response 的寫法

### 修改 Action
```
  def sent_request
    uri = URI(&#39;http://localhost:3000/kamigo/eat&#39;)
    http = Net::HTTP.new(uri.host, uri.port)
    http_request = Net::HTTP::Get.new(uri)
    http_response = http.request(http_request)

    render plain: JSON.pretty_generate({
      request_class: request.class,
      response_class: response.class,
      http_request_class: http_request.class,
      http_response_class: http_response.class
    })
  end
```

越來越複雜了。通常要作到細膩的控制，就要寫比較多的程式碼。

我們先觀察一下這四個物件：`request`、`response`、`http_request`、`http_response`，前兩個是 Rails 內建的，後兩個是我們在 Action 中定義的，為了能夠同時輸出 4 個字串，我用一個雜湊陣列把想看的字串都放進去，然後使用
`JSON.pretty_generate` 幫我排成比較好閱讀的格式，讓我們來看一下個別對應的類別：

### 開啟網頁
```
{
  &quot;request_class&quot;: &quot;ActionDispatch::Request&quot;,
  &quot;response_class&quot;: &quot;ActionDispatch::Response&quot;,
  &quot;http_request_class&quot;: &quot;Net::HTTP::Get&quot;,
  &quot;http_response_class&quot;: &quot;Net::HTTPOK&quot;
}
```

### 圖解

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4WpA9VIeFxBdMV5WVTmXAlmBdgq_Y8xE2cBurbze0HcbV5cb3wsgD0mNjA8ZskFBAgyOIaogEUqjJv_eeM8nQ_ic9wSx1aG1LjfyC-s65rz-qyG7tlSn3s0_t-SoKjNRuQ3BlDLTG1KA/s1600/3.jpg)

昨天我們學的是 `1` 跟 `4`，今天學 `2` 跟 `3`，這裡的四個物件，都是屬於不同的類別，所以我們可能需要不同的語法去存取他們。

今天只要了解到這裡就行了。

# 總結
- 不是只有瀏覽器才能發出 HTTP Request，網頁伺服器一樣可以發出 HTTP Request
- 今天學會了宣告函數和呼叫函數的正確觀念。

明天講怎麼讓別人能連到你作的網站。
</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/5361175659647275589/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/5361175659647275589' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/5361175659647275589'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/5361175659647275589'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/2018-it_4.html' title='第十六天：做一個最簡單的爬蟲（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-7469467887394854976</id><published>2018-01-03T03:55:00.000+08:00</published><updated>2018-01-03T04:17:37.992+08:00</updated><title type='text'>第十五天：從 Rails 認識 HTTP 協定（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）</title><content type='html'>markdown

昨天我們學會了怎麼新增 Route 跟 Controller，並且知道怎麼控制要回應什麼內容給瀏覽器。

今天要使用這兩天學會的東西，我們要來看看瀏覽器到底傳了什麼給 Rails，以及 Rails 傳回什麼給瀏覽器。

# 做一組新的 Route 跟 Controller

我們在 `app/controllers/kamigo_controller.rb` 新增一個 `request_headers` 方法，嘗試把 `request` 中的 `headers` 傳回給瀏覽器。
```
class KamigoController &lt; ApplicationController
  def eat
    render plain: &quot;吃土啦&quot;
  end 

  def request_headers
    render plain: request.headers
  end
end
```

然後在 `config/routes.rb` 加入一行 `get &#39;/kamigo/request_headers&#39;, to: &#39;kamigo#request_headers&#39;`。

```
Rails.application.routes.draw do
  get &#39;/kamigo/eat&#39;, to: &#39;kamigo#eat&#39;
  get &#39;/kamigo/request_headers&#39;, to: &#39;kamigo#request_headers&#39;
end
```
開啟網頁伺服器，並且用瀏覽器打開網頁：[http://localhost:3000/kamigo/request_headers](http://localhost:3000/kamigo/request_headers)。

你會看到：
```
#&lt;ActionDispatch::Http::Headers:0x0000000007ead268&gt;
```
表示 `request.headers` 是一個 [ActionDispatch::Http::Headers](http://api.rubyonrails.org/classes/ActionDispatch/Http/Headers.html) 物件，但是其實我們想知道的是他包含的內容是什麼，而不是他是什麼類別的物件。

# 從 Rails 觀察 request.headers

所以我們需要改一下程式：

```
class KamigoController &lt; ApplicationController
  def eat
    render plain: &quot;吃土啦&quot;
  end 

  def request_headers
    render plain: request.headers.to_h
  end
end
```

加一個 `to_h` ，試著把物件轉成雜湊陣列看看。

```
{&quot;rack.version&quot;=&gt;[1, 3],...
下略 180 萬字
```

結果是成功了，但是瀏覽器印出了 180 萬個字。

我們先試著忽略內容的部分，看一下他的關鍵字有哪些。

```
class KamigoController &lt; ApplicationController
  def eat
    render plain: &quot;吃土啦&quot;
  end 

  def request_headers
    render plain: request.headers.to_h.keys
  end
end
```

再加一個 `keys` ，試著把雜湊陣列轉成只包含關鍵字的陣列看看。

```
[&quot;rack.version&quot;, &quot;rack.errors&quot;, &quot;rack.multithread&quot;, &quot;rack.multiprocess&quot;, &quot;rack.run_once&quot;, &quot;SCRIPT_NAME&quot;, &quot;QUERY_STRING&quot;, &quot;SERVER_PROTOCOL&quot;, &quot;SERVER_SOFTWARE&quot;, &quot;GATEWAY_INTERFACE&quot;, &quot;REQUEST_METHOD&quot;, &quot;REQUEST_PATH&quot;, &quot;REQUEST_URI&quot;, &quot;HTTP_VERSION&quot;, &quot;HTTP_HOST&quot;, &quot;HTTP_CONNECTION&quot;, &quot;HTTP_CACHE_CONTROL&quot;, &quot;HTTP_USER_AGENT&quot;, &quot;HTTP_UPGRADE_INSECURE_REQUESTS&quot;, &quot;HTTP_ACCEPT&quot;, &quot;HTTP_ACCEPT_ENCODING&quot;, &quot;HTTP_ACCEPT_LANGUAGE&quot;, &quot;HTTP_COOKIE&quot;, &quot;HTTP_ALEXATOOLBAR_ALX_NS_PH&quot;, &quot;HTTP_IF_NONE_MATCH&quot;, &quot;SERVER_NAME&quot;, &quot;SERVER_PORT&quot;, &quot;PATH_INFO&quot;, &quot;REMOTE_ADDR&quot;, &quot;puma.socket&quot;, &quot;rack.hijack?&quot;, &quot;rack.hijack&quot;, &quot;rack.input&quot;, &quot;rack.url_scheme&quot;, &quot;rack.after_reply&quot;, &quot;puma.config&quot;, &quot;action_dispatch.parameter_filter&quot;, &quot;action_dispatch.redirect_filter&quot;, &quot;action_dispatch.secret_token&quot;, &quot;action_dispatch.secret_key_base&quot;, &quot;action_dispatch.show_exceptions&quot;, &quot;action_dispatch.show_detailed_exceptions&quot;, &quot;action_dispatch.logger&quot;, &quot;action_dispatch.backtrace_cleaner&quot;, &quot;action_dispatch.key_generator&quot;, &quot;action_dispatch.http_auth_salt&quot;, &quot;action_dispatch.signed_cookie_salt&quot;, &quot;action_dispatch.encrypted_cookie_salt&quot;, &quot;action_dispatch.encrypted_signed_cookie_salt&quot;, &quot;action_dispatch.cookies_serializer&quot;, &quot;action_dispatch.cookies_digest&quot;, &quot;action_dispatch.routes&quot;, &quot;ROUTES_57612100_SCRIPT_NAME&quot;, &quot;ORIGINAL_FULLPATH&quot;, &quot;ORIGINAL_SCRIPT_NAME&quot;, &quot;action_dispatch.request_id&quot;, &quot;action_dispatch.remote_ip&quot;, &quot;rack.session&quot;, &quot;rack.session.options&quot;, &quot;action_dispatch.request.path_parameters&quot;, &quot;action_controller.instance&quot;, &quot;action_dispatch.request.content_type&quot;, &quot;action_dispatch.request.request_parameters&quot;, &quot;rack.request.query_string&quot;, &quot;rack.request.query_hash&quot;, &quot;action_dispatch.request.query_parameters&quot;, &quot;action_dispatch.request.parameters&quot;, &quot;action_dispatch.request.formats&quot;, &quot;rack.request.cookie_hash&quot;, &quot;rack.request.cookie_string&quot;, &quot;action_dispatch.cookies&quot;, &quot;action_dispatch.request.unsigned_session_cookie&quot;]
```

看起來好多了，但是這不好閱讀，我們試著讓他自己換行。

```
class KamigoController &lt; ApplicationController
  def eat
    render plain: &quot;吃土啦&quot;
  end 

  def request_headers
    render plain: request.headers.to_h.keys.join(&quot;\n&quot;)
  end
end
```

再加一個 `join(&quot;\n&quot;)`，試著把陣列轉成字串，並且以 `\n` 作為分隔符號，`\n` 其實是換行的意思。

```
rack.version
rack.errors
rack.multithread
rack.multiprocess
rack.run_once
SCRIPT_NAME
QUERY_STRING
SERVER_PROTOCOL
SERVER_SOFTWARE
GATEWAY_INTERFACE
REQUEST_METHOD
REQUEST_PATH
REQUEST_URI
HTTP_VERSION
HTTP_HOST
HTTP_CONNECTION
HTTP_CACHE_CONTROL
HTTP_USER_AGENT
HTTP_UPGRADE_INSECURE_REQUESTS
HTTP_ACCEPT
HTTP_ACCEPT_ENCODING
HTTP_ACCEPT_LANGUAGE
HTTP_COOKIE
HTTP_ALEXATOOLBAR_ALX_NS_PH
HTTP_IF_NONE_MATCH
SERVER_NAME
SERVER_PORT
PATH_INFO
REMOTE_ADDR
puma.socket
rack.hijack?
rack.hijack
rack.input
rack.url_scheme
rack.after_reply
puma.config
action_dispatch.parameter_filter
action_dispatch.redirect_filter
action_dispatch.secret_token
action_dispatch.secret_key_base
action_dispatch.show_exceptions
action_dispatch.show_detailed_exceptions
action_dispatch.logger
action_dispatch.backtrace_cleaner
action_dispatch.key_generator
action_dispatch.http_auth_salt
action_dispatch.signed_cookie_salt
action_dispatch.encrypted_cookie_salt
action_dispatch.encrypted_signed_cookie_salt
action_dispatch.cookies_serializer
action_dispatch.cookies_digest
action_dispatch.routes
ROUTES_57612100_SCRIPT_NAME
ORIGINAL_FULLPATH
ORIGINAL_SCRIPT_NAME
action_dispatch.request_id
action_dispatch.remote_ip
rack.session
rack.session.options
action_dispatch.request.path_parameters
action_controller.instance
action_dispatch.request.content_type
action_dispatch.request.request_parameters
rack.request.query_string
rack.request.query_hash
action_dispatch.request.query_parameters
action_dispatch.request.parameters
action_dispatch.request.formats
rack.request.cookie_hash
rack.request.cookie_string
action_dispatch.cookies
action_dispatch.request.unsigned_session_cookie
```

我們需要過濾掉不想看的東西，但是現在還是沒有很容易閱讀，我們應該排序一下。

```
  def request_headers
    render plain: request.headers.to_h.keys.sort.join(&quot;\n&quot;)
  end
```

再加一個 `sort`，試著在轉完陣列後，先排序，再作排版文字。

```
GATEWAY_INTERFACE
HTTP_ACCEPT
HTTP_ACCEPT_ENCODING
HTTP_ACCEPT_LANGUAGE
HTTP_ALEXATOOLBAR_ALX_NS_PH
HTTP_CACHE_CONTROL
HTTP_CONNECTION
HTTP_COOKIE
HTTP_HOST
HTTP_IF_NONE_MATCH
HTTP_UPGRADE_INSECURE_REQUESTS
HTTP_USER_AGENT
HTTP_VERSION
ORIGINAL_FULLPATH
ORIGINAL_SCRIPT_NAME
PATH_INFO
QUERY_STRING
REMOTE_ADDR
REQUEST_METHOD
REQUEST_PATH
REQUEST_URI
ROUTES_57612100_SCRIPT_NAME
SCRIPT_NAME
SERVER_NAME
SERVER_PORT
SERVER_PROTOCOL
SERVER_SOFTWARE
action_controller.instance
action_dispatch.backtrace_cleaner
action_dispatch.cookies
action_dispatch.cookies_digest
action_dispatch.cookies_serializer
action_dispatch.encrypted_cookie_salt
action_dispatch.encrypted_signed_cookie_salt
action_dispatch.http_auth_salt
action_dispatch.key_generator
action_dispatch.logger
action_dispatch.parameter_filter
action_dispatch.redirect_filter
action_dispatch.remote_ip
action_dispatch.request.content_type
action_dispatch.request.formats
action_dispatch.request.parameters
action_dispatch.request.path_parameters
action_dispatch.request.query_parameters
action_dispatch.request.request_parameters
action_dispatch.request.unsigned_session_cookie
action_dispatch.request_id
action_dispatch.routes
action_dispatch.secret_key_base
action_dispatch.secret_token
action_dispatch.show_detailed_exceptions
action_dispatch.show_exceptions
action_dispatch.signed_cookie_salt
puma.config
puma.socket
rack.after_reply
rack.errors
rack.hijack
rack.hijack?
rack.input
rack.multiprocess
rack.multithread
rack.request.cookie_hash
rack.request.cookie_string
rack.request.query_hash
rack.request.query_string
rack.run_once
rack.session
rack.session.options
rack.url_scheme
rack.version
```

我們需要觀察一下每一個 key 裡面包含的內容，來決定哪些是我們不要的。

```
  def request_headers
    render plain: request.headers.to_h.map{ |key, value|
      key + &quot;: &quot; + value.class.to_s
    }.sort.join(&quot;\n&quot;)
  end
```

我們把 `keys` 改成了 `.map{ |key, value| key + &quot;: &quot; + value.class.to_s}`，map 的意思是把每一筆紀錄都拿去作一個轉換，然後傳回一個陣列。

如果我們是寫成 `.map{ |key, value| key }` 的話，就是每一筆紀錄我們只要保留關鍵字的部分，這樣就等於 `keys`，這兩段程式會有相同的結果。

但是因為我們想要看到更多的東西，所以我們再把 `key` 改寫成 `key + &quot;: &quot; + value.class.to_s`，我希望在每一個 key 後面加一個冒號，然後讓 Rails 告訴我，他們是什麼類型的資料。

得到的結果是這樣：

```
GATEWAY_INTERFACE: String
HTTP_ACCEPT: String
HTTP_ACCEPT_ENCODING: String
HTTP_ACCEPT_LANGUAGE: String
HTTP_ALEXATOOLBAR_ALX_NS_PH: String
HTTP_CACHE_CONTROL: String
HTTP_CONNECTION: String
HTTP_COOKIE: String
HTTP_HOST: String
HTTP_UPGRADE_INSECURE_REQUESTS: String
HTTP_USER_AGENT: String
HTTP_VERSION: String
ORIGINAL_FULLPATH: String
ORIGINAL_SCRIPT_NAME: String
PATH_INFO: String
QUERY_STRING: String
REMOTE_ADDR: String
REQUEST_METHOD: String
REQUEST_PATH: String
REQUEST_URI: String
ROUTES_57612100_SCRIPT_NAME: String
SCRIPT_NAME: String
SERVER_NAME: String
SERVER_PORT: String
SERVER_PROTOCOL: String
SERVER_SOFTWARE: String
action_controller.instance: KamigoController
action_dispatch.backtrace_cleaner: Rails::BacktraceCleaner
action_dispatch.cookies: ActionDispatch::Cookies::CookieJar
action_dispatch.cookies_digest: NilClass
action_dispatch.cookies_serializer: Symbol
action_dispatch.encrypted_cookie_salt: String
action_dispatch.encrypted_signed_cookie_salt: String
action_dispatch.http_auth_salt: String
action_dispatch.key_generator: ActiveSupport::CachingKeyGenerator
action_dispatch.logger: ActiveSupport::Logger
action_dispatch.parameter_filter: Array
action_dispatch.redirect_filter: Array
action_dispatch.remote_ip: ActionDispatch::RemoteIp::GetIp
action_dispatch.request.content_type: NilClass
action_dispatch.request.formats: Array
action_dispatch.request.parameters: ActiveSupport::HashWithIndifferentAccess
action_dispatch.request.path_parameters: Hash
action_dispatch.request.query_parameters: ActiveSupport::HashWithIndifferentAccess
action_dispatch.request.request_parameters: ActiveSupport::HashWithIndifferentAccess
action_dispatch.request.unsigned_session_cookie: Hash
action_dispatch.request_id: String
action_dispatch.routes: ActionDispatch::Routing::RouteSet
action_dispatch.secret_key_base: String
action_dispatch.secret_token: NilClass
action_dispatch.show_detailed_exceptions: TrueClass
action_dispatch.show_exceptions: TrueClass
action_dispatch.signed_cookie_salt: String
puma.config: Puma::Configuration
puma.socket: TCPSocket
rack.after_reply: Array
rack.errors: IO
rack.hijack: Puma::Client
rack.hijack?: TrueClass
rack.input: Puma::NullIO
rack.multiprocess: FalseClass
rack.multithread: TrueClass
rack.request.cookie_hash: Hash
rack.request.cookie_string: String
rack.request.query_hash: Hash
rack.request.query_string: String
rack.run_once: FalseClass
rack.session.options: ActionDispatch::Request::Session::Options
rack.session: ActionDispatch::Request::Session
rack.url_scheme: String
rack.version: Array
```

讓我們簡化一下程式碼

```
  def request_headers
    render plain: request.headers.to_h.map{ |key, value|
      &quot;#{key}: #{value.class}&quot;
    }.sort.join(&quot;\n&quot;)
  end
```

`&quot;#{key}: #{value.class}&quot;` 是更加簡潔的表示法，用雙引號包裝的字串可以作變數的代換，語法是 `#{變數的名字}`，這種寫法的程式碼會更好閱讀。

看起來那些 `key` 包含 `action_controller`、`action_dispatch`、`puma`、`rack` 的資料都是我們不想看到的，因為他們看起來像是 Rails 這邊才產生的資料，而不是瀏覽器傳來的資料。

我發現這些我們不想要的 key 都包含 `.`，讓我們把在 `key` 包含 `.` 的資料都過濾掉。

```
  def request_headers
    render plain: request.headers.to_h.reject{ |key, value|
      key.include? &#39;.&#39;
    }.map{ |key, value|
      &quot;#{key}: #{value.class}&quot;
    }.sort.join(&quot;\n&quot;)
  end
```
reject 可以過濾資料，滿足條件 `key.include? &#39;.&#39;` 的資料就沒有辦法進到 map。

得到的結果是這樣：
```
GATEWAY_INTERFACE: String
HTTP_ACCEPT: String
HTTP_ACCEPT_ENCODING: String
HTTP_ACCEPT_LANGUAGE: String
HTTP_ALEXATOOLBAR_ALX_NS_PH: String
HTTP_CACHE_CONTROL: String
HTTP_CONNECTION: String
HTTP_COOKIE: String
HTTP_HOST: String
HTTP_UPGRADE_INSECURE_REQUESTS: String
HTTP_USER_AGENT: String
HTTP_VERSION: String
ORIGINAL_FULLPATH: String
ORIGINAL_SCRIPT_NAME: String
PATH_INFO: String
QUERY_STRING: String
REMOTE_ADDR: String
REQUEST_METHOD: String
REQUEST_PATH: String
REQUEST_URI: String
ROUTES_57612100_SCRIPT_NAME: String
SCRIPT_NAME: String
SERVER_NAME: String
SERVER_PORT: String
SERVER_PROTOCOL: String
SERVER_SOFTWARE: String
```

現在應該可以試試看從顯示資料的類別改成顯示完整資料了。

```
  def request_headers
    render plain: request.headers.to_h.reject{ |key, value|
      key.include? &#39;.&#39;
    }.map{ |key, value|
      &quot;#{key}: #{value}&quot;
    }.sort.join(&quot;\n&quot;)
  end
```

看起來像這樣：

```
GATEWAY_INTERFACE: CGI/1.2
HTTP_ACCEPT: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
HTTP_ACCEPT_ENCODING: gzip, deflate, br
HTTP_ACCEPT_LANGUAGE: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6
HTTP_ALEXATOOLBAR_ALX_NS_PH: AlexaToolbar/alx-4.0
HTTP_CACHE_CONTROL: max-age=0
HTTP_CONNECTION: keep-alive
HTTP_COOKIE: _blog_session=RG5tTnJ2eUIyMkFCUFV1UHFaMDhHbXpYekJXbFhQQlBiRlk1UEhucUkvV0IwcDNtV09sNUJuVTJWOS9RU2cweTgxY0Q1TGQ5L21GRWNmS1Z6RmdEa3cxdmxFUnZPOEJObVN3ck10R3Frc2ZOWXBDT2hNY0VZUUg0RHowNlJuazFXeWJXZE5sNjlsTUFBMG12QVJNRmVnPT0tLXo3dEJjUFJtbzRjSy9HODVNcVJRRVE9PQ%3D%3D--02e477b161398d92622542cb43d8b515892b8c59; _ironman_session=SzRPTlE1VytFbFgwRUkxMHlXaDFUVlpqdzN4ekt1eVFtUFpBbmtxNUw0UFd6cytjNkI1MlBOdC9zcnBkejNUMmxLa1MzdnV6Z0dselhHOTEzSUtJTDlrU241empqS1BmanNHSTBLd1VnVzlHV2pDZis4QVVRUk5xandJaGpPNml5VHlHUTI5ZXNCa1NZdHNrNzBYT0lnPT0tLW5salNpWUNVRmlxbUI1U1ZLdXhwMGc9PQ%3D%3D--c56c1473a20c57c62c66a215e07fa14dd6f7fb94
HTTP_HOST: localhost:3000
HTTP_IF_NONE_MATCH: W/&quot;d220fb78d8ab10894ccdd2ef500f5ac8&quot;
HTTP_UPGRADE_INSECURE_REQUESTS: 1
HTTP_USER_AGENT: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36
HTTP_VERSION: HTTP/1.1
ORIGINAL_FULLPATH: /kamigo/request_headers
ORIGINAL_SCRIPT_NAME: 
PATH_INFO: /kamigo/request_headers
QUERY_STRING: 
REMOTE_ADDR: 127.0.0.1
REQUEST_METHOD: GET
REQUEST_PATH: /kamigo/request_headers
REQUEST_URI: /kamigo/request_headers
ROUTES_57612100_SCRIPT_NAME: 
SCRIPT_NAME: 
SERVER_NAME: localhost
SERVER_PORT: 3000
SERVER_PROTOCOL: HTTP/1.1
SERVER_SOFTWARE: puma 3.11.0 Love Song
```

這應該就是從瀏覽器那邊傳來的全部內容了，我們可以用瀏覽器的 `F12` 看一下差異。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjVLQmK5-8hobOxHwM7fDPKwozabp4QBB9nD5y1M7uWUnAaoTC26qgdazr63oKr1QUt9wkA-fiJDXi8o0RsPvhj3fUCCoFsre9sLFP2mq84JNUx93Jx-yWeLfeUmbQ06-LUZF3Elf0CdVs/s1600/1.jpg)

因為這樣我們無法同時看到兩邊的結果，所以需要調整一下開發人員工具的位置，把他調到右側的話會比較恰當。

先點一下這裡：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi17I9Fn8peVszVuVCgrMpIzaXYW81_aPzQLwIxaecUaWtn9I1ktlyTrvsPUR7WlEhlT9PPzrgeEa45C_bX4L-rTUGbh-hg6orDO-IR0Ih_92jaqiQqovyUOo9wHhZ8HKe-_KaLlpWxZOo/s1600/2.jpg)

再點一下這裡：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSl6Rh2CGVY5PgklfbthWbO1HEtakh-mo6EmmpD61LYt-lO8LknqHYh8rLmA0USMTb-tQx7RMFrvOoduJkiAsFNizUiYprtxxviQq-L46dpZPk8OIL6lInWX0jgA0CgYhUui_pHrpa2rg/s1600/3.jpg)

點完之後應該會變成這樣：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgodGMjV9ohQIhJfHwtTraSHzSa5huE3sDYkq3iwLYrxFRooCd0I30fyBoXDtoKLEo6LVYq9_w6oE9Locc26T1JG414bQGzDhwjzMRxtnSSMMgkm9SShxDA-lUSzaSPOwmV3t7hO7Ycp6Y/s1600/4.jpg)

仔細一看，發現這兩塊是一樣的：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_HKxX1Svx8CCBpNTgJnmav1jkJ71BN3c7xdOWaIKIUHHWKc0s07lZl8wvIFL2Aip8tSF2t0G-y3CDfWInp9uTUGJvQsvKtgWuo9X18ycAyRDIKcdIMukDnsQpibHVZ52KVbgVOL6Zscw/s1600/5.jpg)


# 從 Rails 觀察 request.body

再作一組 Route 和 Controller Action。

Route:
```
  get &#39;/kamigo/request_body&#39;, to: &#39;kamigo#request_body&#39;
```
Action:
```
  def request_body
    render plain: request.body
  end
```
用瀏覽器連過去看：[http://localhost:3000/kamigo/request_body](http://localhost:3000/kamigo/request_body)，發現是 `#&lt;Puma::NullIO:0x000000000673ae00&gt;`，表示瀏覽器傳了一個空的 request body 過來，因為他是來下載網頁，不是上傳資料。如果是上傳資料的話，就會在 request body 有東西了。

# 從 Rails 觀察 response.headers

再作一組 Route 和 Controller Action。

Route:
```
  get &#39;/kamigo/response_headers&#39;, to: &#39;kamigo#response_headers&#39;
```
Action:
```
  def response_headers
    render plain: response.headers.to_h.map{ |key, value|
      &quot;#{key}: #{value}&quot;
    }.sort.join(&quot;\n&quot;)
  end
```

用瀏覽器連過去看：[http://localhost:3000/kamigo/response_headers](http://localhost:3000/kamigo/response_headers)。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjHNFQ0c4JSoIiOcWfbd23S1W32PgGer8tRw5LxDgPYe588Q_49amNbEyTZNzAv5fgyMLEtSNu3SheQSAyGrckphvd-SWOZ708uFxgQKr3LMDe98HuLyJ9HEFUF-x9Q956Uy45SJWWMI4g/s1600/6.jpg)

資料少很多，這表示 Rails 回傳的東西，不是全放在 `response.headers`，不過這不是很重要，知道有 `response` 這個東西就行了。

我們可以試著加一個 header 看看：

```
  def response_headers
    response.headers[&#39;5566&#39;] = &#39;QQ&#39;
    render plain: response.headers.to_h.map{ |key, value|
      &quot;#{key}: #{value}&quot;
    }.sort.join(&quot;\n&quot;)
  end
```

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiW05FXWJNo1QOU3mDjAEhbHTKH97KppDbmgNZwglXEBiAKftpJiOf_5cUTmtYOpMj7khUSAofEvxRrg9sjM2pHsYzD8V5utjztugCaofwIyxPiaEsnNTK3FiMacbwYGf8mY5YRK2SuTvQ/s1600/7.jpg)

哈哈哈哈ㄚ哈

# 從 Rails 觀察 response.body

再作一組 Route 和 Controller Action。

Route:
```
  get &#39;/kamigo/response_body&#39;, to: &#39;kamigo#show_response_body&#39;
```
Action:
```
  def show_response_body
    render plain: response.body
  end
```
這裡我們不能用 `response_body` ，因為 `response_body` 已經被 Rails 用掉了，我們再用的話會出事，所以這邊稍微改一下名字。

用瀏覽器連過去看：[http://localhost:3000/kamigo/response_body](http://localhost:3000/kamigo/response_body)。

這樣會看到空的結果，因為我們傳回的內容就是 `response.body`，但此時的 `response.body` 其實還是空值。

所以我們必須改變一下觀察的方法，其實我們可以透過小黑框來觀察 Rails 的目前狀況。

在程式碼當中加入 `puts &quot;媽我在這 \\(￣▽￣)/&quot;` 意思是把 `媽我在這 \\(￣▽￣)/` 顯示在小黑框上。
```
  def show_response_body
    puts &quot;媽我在這 \\(￣▽￣)/&quot;
    render plain: response.body
  end
```
當有人開啟網頁 [http://localhost:3000/kamigo/response_body](http://localhost:3000/kamigo/response_body)，就會在小黑框看到這個結果：
```
Started GET &quot;/kamigo/response_body&quot; for 127.0.0.1 at 2018-01-03 03:46:48 +0800
Processing by KamigoController#show_response_body as HTML
媽我在這 \(￣▽￣)/
  Rendering text template
  Rendered text template (0.0ms)
Completed 200 OK in 2ms (Views: 0.8ms)
```

所以我們利用這種方式觀察，現在把程式改成這樣：

```
  def show_response_body
    puts &quot;===這是設定前的response.body:#{response.body}===&quot;
    render plain: &quot;虎哇花哈哈哈&quot;
    puts &quot;===這是設定後的response.body:#{response.body}===&quot;
  end
```

當有人開啟網頁 [http://localhost:3000/kamigo/response_body](http://localhost:3000/kamigo/response_body)，就會在小黑框看到這個結果：

```
Started GET &quot;/kamigo/response_body&quot; for 127.0.0.1 at 2018-01-03 03:51:40 +0800
Processing by KamigoController#show_response_body as HTML
===這是設定前的response.body:===
  Rendering text template
  Rendered text template (0.0ms)
===這是設定後的response.body:虎哇花哈哈哈===
Completed 200 OK in 16ms (Views: 11.5ms)
```

# 總結

那個要抄程式碼的，都幫你整理好了。

### app/controllers/kamigo_controller.rb
```
class KamigoController &lt; ApplicationController
  def eat
    render plain: &quot;吃土啦&quot;
  end 

  def request_headers
    render plain: request.headers.to_h.reject{ |key, value|
      key.include? &#39;.&#39;
    }.map{ |key, value|
      &quot;#{key}: #{value}&quot;
    }.sort.join(&quot;\n&quot;)
  end

  def response_headers
    response.headers[&#39;5566&#39;] = &#39;QQ&#39;
    render plain: response.headers.to_h.map{ |key, value|
      &quot;#{key}: #{value}&quot;
    }.sort.join(&quot;\n&quot;)
  end

  def request_body
    render plain: request.body
  end

  def show_response_body
    puts &quot;===這是設定前的response.body:#{response.body}===&quot;
    render plain: &quot;虎哇花哈哈哈&quot;
    puts &quot;===這是設定後的response.body:#{response.body}===&quot;
  end

end
```

### config/routes.rb
```
Rails.application.routes.draw do
  get &#39;/kamigo/eat&#39;, to: &#39;kamigo#eat&#39;
  get &#39;/kamigo/request_headers&#39;, to: &#39;kamigo#request_headers&#39;
  get &#39;/kamigo/request_body&#39;, to: &#39;kamigo#request_body&#39;
  get &#39;/kamigo/response_headers&#39;, to: &#39;kamigo#response_headers&#39;
  get &#39;/kamigo/response_body&#39;, to: &#39;kamigo#show_response_body&#39;
end
```

### 網址
[http://localhost:3000/kamigo/request_headers](http://localhost:3000/kamigo/request_headers)

[http://localhost:3000/kamigo/request_body](http://localhost:3000/kamigo/request_body)

[http://localhost:3000/kamigo/response_headers](http://localhost:3000/kamigo/response_headers)

[http://localhost:3000/kamigo/response_body](http://localhost:3000/kamigo/response_body)

今天就講到這裡。

嗯？今天是不是講太多了？

</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/7469467887394854976/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/7469467887394854976' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/7469467887394854976'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/7469467887394854976'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/rails-http-2018-it.html' title='第十五天：從 Rails 認識 HTTP 協定（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-7634228811010263629</id><published>2018-01-02T04:13:00.000+08:00</published><updated>2018-01-02T04:31:34.663+08:00</updated><title type='text'>第十四天：最基本的 Rails 運作流程（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）</title><content type='html'>markdown

這是目前我們理解的 HTTP 協定。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDDd1WmueuUPejQ2gCRQbVDSFbInhKL0oyAmw7sNqyig12R8zvki2y6erh0SWsyOjik9de9uR51K80kpdtT0fNOlP5zjoCBn_5Oggl4Cg74MLvclMcd24E2AvKj1EfiuzQxkSIWODcDcY/s1600/1.jpg)

今天會詳細說明 Rails 是怎麼處理一個 HTTP Request。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4g95h3LW5Mx3WOdL0uJ-WN9GPCgAz2VV7L7ZzHPgf4zai5c37xlMsWqYvef7lH2ye7QWxFkIzdGd8hlVpPYi3zzCHUCWHHRt9X0ZfpaxdsxJHC8V-dH09xgvluzFhB2rZahNoz1zeOQg/s1600/2.jpg)

當我們在瀏覽器輸入一個網址時，會發出一個 Get Request 到網頁伺服器。網頁伺服器會根據這個網址，決定他應該要幹嘛。而 Rails 中的 Route 負責分析網址， Route 會決定要把 Request 發給哪一個 Controller 的 Action 來做回應。因為 Action 畫上去圖會很亂，所以就乾脆不畫了。

應該很難懂吧？

用人話來說：Route 就像大樓警衛，當有信件送進來，他要負責把信交給對應的住戶，所以 Controller 就是住家，Action 就是裡面的人。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhiJvFOxKgtGVh-butNSV3OF4VjZ0pVUM0Y_NIIZMIcMZZdCipS2KWDYXgM6O2nehpsiuX39wV5hhyphenhypheneqpqKCthCdGqv1IGa26QETANR-7ywVmNczv9pCl8JJOu7AvNdI6MHWhqspWIlLQA/s1600/3.jpg)

當然，一棟大樓會有一間警衛室，以及有很多住戶，而且每一戶裡面還有住很多人，但是一封信只會寄給一個人。

# 所以我說那個警衛室在哪？

先打開 sublime text，然後點左上角的 `File`(檔案)。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEio98LWPjpMfPCkWhOHKuV_ggbon44B0ALKpgZrQiyC6Nx3og4DS6WoVNSUBZZmZHgSGfUR3bVyHXGdo84mvyJu9dv_VM1yUVqtAdUahrKm0ll3mnG_g57vgJfgAZ0-ZyJYqO2oinLfTbA/s1600/4.jpg)

選擇 `Open Folder...`(打開資料夾)，選到我們的專案資料夾：`D:\只要有心，人人都可以作卡米狗\ironman`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-ZL43UH34UAXlwpFN5dIZgyjbBEQyN92eadc9Icl3aqv_AkrJE8f6VrW7S08dWK9UURNXEjX701gvN9RpRnK4U_2UWGChyphenhyphenOuTWxJVqbyUY3XhMiT8lGd9_GJkfDuU-hsOh3SixDgqNhc/s1600/5.jpg)

按下`選擇資料夾`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh7EAS3iZpjgIu7256kzEz4hECrVGF0X5oNRcVNldn3_V2MyJDIUgrRMe6AQpgA-jhbSEZvgcZdEww3sdg0XGJGzFjrkCRL4SNN9XCMNaiVCOdyJfIIkuNhstamqeSbKAsMJBEwNJm5Qxg/s1600/6.jpg)

左側多了一塊是資料夾目錄，那個灰色邊界是可以拖曳的，像這樣：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjeblXfWUwiTu5EVnEYRPywNB558gi3umtgPJBuj1fOzolx5xPmSRGuhXQx2FdgFStGYyF6LyKUMwXXKQ587crB3MmS77cBcGxPqpByC9C5hrjwj6Uj7COBR1BwFwbusZdF0Q3-K19Jwpo/s1600/7.jpg)

如果覺得空間不夠大，可以自行調整。

我們的警衛室在 `config/routes.rb`，先點左側的 `config` 資料夾，點一下會展開，再點一下會收合，然後再點一下 `routes.rb`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGr7MCEVhVUkFS-xOzpRY_fwWwXJSMettVkjAFsdyYhZdA7Frd1G2wxxYhlkm3fpUGXT8wAP1UALJ_ziH9Nh964B7RoVnAn63vaobONPG8QbPUpssO7L0a-2cbyi_J81yI5jnbKDbQJGU/s1600/8.jpg)

這樣就是空的。

# 那我們的住戶呢？

住戶在 `app/controllers`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgcyEaaQIF5MyNLftPwLwx9aWLHMBKtA3JEiVfD6qB6CJT3APmYVBeagPJaVbhMV9WFsH9Pbs07wbueM-NQAdvk1U449pHwD3f18S1xXKLVC9RvpaIYdMaScBtaSUZDo3z9GTRT4lcBWtg/s1600/9.jpg)

看到一個 concerns 空的資料夾和一個 application_controller.rb，現階段就當作沒看到這個檔案好了。

所以我們現在蓋的樓裡面根本沒住人！總之還是先弄個住戶進去再說。

# 做一個 Controller

先開啟 cmd 到我們的專案目錄：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

輸入 `rails generate controller kamigo` ，請 rails 幫我們生成一個叫做 kamigo 的 controller。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails generate controller kamigo
      create  app/controllers/kamigo_controller.rb
      invoke  erb
      create    app/views/kamigo
      invoke  test_unit
      create    test/controllers/kamigo_controller_test.rb
      invoke  helper
      create    app/helpers/kamigo_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/kamigo.coffee
      invoke    scss
      create      app/assets/stylesheets/kamigo.scss

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

他幫我們產生了好多檔案，開頭有 create 的那些都是，但是其實我們只需要 `app/controllers/kamigo_controller.rb`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjASP0Ei1jZ7qEw8pvzkIJcgaBiTBIV1l1iFRFZPuWP2QC_WefW4pZF3glczpSRMtXO4wLZsfhMqSfVftCpKysQJMO-_jsxo-26ZeiFCnPao1p9YBeyDwarX9TJ9wUX2acdPlxGpcn-wY0/s1600/10.jpg)

# 我後悔了想復原怎麼辦？

我們可以用 `rails destroy controller kamigo` 來刪除這些檔案。

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails destroy controller kamigo
      remove  app/controllers/kamigo_controller.rb
      invoke  erb
      remove    app/views/kamigo
      invoke  test_unit
      remove    test/controllers/kamigo_controller_test.rb
      invoke  helper
      remove    app/helpers/kamigo_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      remove      app/assets/javascripts/kamigo.coffee
      invoke    scss
      remove      app/assets/stylesheets/kamigo.scss

D:\只要有心，人人都可以作卡米狗\ironman&gt;
```

# 這次我們手動新增 controller 

在左側的 `controllers` 目錄上按右鍵。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhpdNu41HWxkUMPNcEGSWagKL6kdhUmLJ2nyBU_ETBX7bmBWhzZKUhWBsfllb9tsDMckjiAw97qZ2jhqBZbuaxwBEszv-ZuqQYuUsHukEtEcwF948QMDUoA4vl83ThPNJ9o0kj82VdFW0/s1600/11.jpg)

選 `New File`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhG1Dc8JslNB1pRKZfTDMYz0bXcXB5zw1VMCDPKBCbo7_unnKFSyrVigCHgSV3SPcP3NCVJIQOzjsFrcQK7Bi3mc9_vdpez4hyiTX9_up9q2HcqLu8nIHK6sDyuaey_DkMH9AYPt92vy-Y/s1600/12.jpg)

在新的檔案裡填入我們想要的程式碼：
```
class KamigoController &lt; ApplicationController
end
```
然後按下 `Ctrl`+`S` 存檔。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgleLrv_RT_c5AjScQ-I94wKi3bXMJRj4KK8T3Fi8qtq0iUBID5BjMQ0TRbyD7tfPpPUErQzpD03i-vgtpCYaGuE8B-h7cp0lwfJiYCZxOPn3vSP-hunE2sMDZgv5QcBkP1El64N_xHLF0/s1600/14.jpg)

輸入檔名：`kamigo_controller.rb`，副檔名 `.rb` 是 ruby 的意思，這樣 sublime text 就能知道內文是 ruby 程式碼。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEieHtEzaTFMXGX5L-Gsv-PY6CQ9gQk0JSVHp1i7GF8r3pD7ijkhyc3ARUkPLSCVRAuWkzfpGivkKiF-kDRkxTVgwc526t6vrEN3N5T_hd_qE14Zgu8bBFcxMMpxTX6NbKaZKSPfdAWhOU8/s1600/15.jpg)

自從我用了 sublime text 之後，人生就變成彩色的呢！

前面有提到 controller 是一戶，每一戶裡面還要有人，所以我們弄點人進去。

# 加入 action

我們定義一個叫做 `eat` 的空方法。

```
class KamigoController &lt; ApplicationController
  def eat
  end
end
```

注意看這裡：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhn1Ep3DQVa6acNQ6rWlrsCwBgeGLwUDlZg_NWZOAVHCNjHMKRwN6FiARp58Y1Pz1o-VbbwuWO-C77lh68brlnOi66g70vBdXMAktJoUy8gvz2n14Zqvg5jXCxko6xyhSXUK6YQHJx5BuE/s1600/16.jpg)

他是一個灰色圈圈，代表這個檔案編輯過，但還沒存檔，按下 `Ctrl`+`S` 之後就會變成灰色X，像這樣：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSHBBCCU9hHnnbkfgCmfMcp42P5WzyTmubKJeJjr2yA7D5U8e1ik3y2EveoZapyKykXVo4NknpaXzqrgM5mrUTUq2bcgj0WgEBS3S7gp9uNRF5voURcx74q70PBl3Ik7MhNK8Tcc1e5P8/s1600/17.jpg)

現在我們已經有一戶，而且住了一個人了，我們來請警衛幫我們轉信。

# 加入 route

我們把 `config/routes.rb` 改成這樣：

```
Rails.application.routes.draw do
  get &#39;/kamigo/eat&#39;, to: &#39;kamigo#eat&#39;
end
```
`get &#39;/kamigo/eat&#39;, to: &#39;kamigo#eat&#39;` 的意思是當有人在瀏覽器輸入網址 `/kamigo/eat` 時，就把請求交給 kamigo 這個 controller 裡的 eat 方法來回應。

# 測試一下

在 cmd 輸入 `rails s` 開啟網頁伺服器：

```
D:\只要有心，人人都可以作卡米狗\ironman&gt;rails s
=&gt; Booting Puma
=&gt; Rails 5.1.4 application starting in development
=&gt; Run `rails server -h` for more startup options
*** SIGUSR2 not implemented, signal based restart unavailable!
*** SIGUSR1 not implemented, signal based restart unavailable!
*** SIGHUP not implemented, signal based logs reopening unavailable!
Puma starting in single mode...
* Version 3.11.0 (ruby 2.4.2-p198), codename: Love Song
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
```

用瀏覽器開啟這個網址：[http://localhost:3000/kamigo/eat](http://localhost:3000/kamigo/eat)，然後就爆炸了：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjHkka8-AzbNbUdz1GMWat2O_PwWaEuc5G5-zA0VsIZFMNrmHs9bOQAPwcQbnd59ARXCKVAMnfEJOvb9p-SNiGobUSfEKQCsiYpglju0SDoGL031BL8n_iTnwITx9MQQGTRzHZj0FIz17I/s1600/18.jpg)

是一個很眼熟的爆炸，這招我們在[第九天：作一個最簡單的 Rails 網站](https://ithelp.ithome.com.tw/articles/10194359)就玩過了。這是因為 Rails 找不到對應的 html 檔案，所以爆炸。可是我們並沒有在 eat 裡面要求 Rails 去找檔案呀！原來這是 Rails 的預設行為。要修好這個問題有兩個方法：

- 在正確的地方新增一個網頁
- 為了避免 Rails 亂幫我們做事，我們得明確說明我們想幹嘛

兩種我們都要會。

### 在正確的地方新增網頁

網頁應該要放在 `app/views/kamigo/` 下，檔名需要叫做 `eat.html` 或者 `eat.html.erb`。

在 sublime text 新增資料夾的方法是在 `views` 資料夾點右鍵：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1RaLqXOmoken4WK9olqnFq88vDTeWT7t9botyDu4uM3iZcwUWS4Gd2ZuthHsOlMbwtVDyItpMFDHUwbpTe3H6NdvkwmOcfyl3YwlTAEK3384xOj6AZjfVB3v4AGiNPShxx1lhqORvshs/s1600/20.jpg)

點選 `New Folder...`：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh1FT8GgwDYKjcf60yuI0ciaRW9V_Dh9nsK16m4oDHHJ9Jqd67nkOJdaHvdfvWLUS1xRQ3euSfDn2Jdh0Mvj8Yn6sGYWniRebGxZHyPWsMUzHfgxgQgZa_94femKkIrfy2ypj2RpQhw980/s1600/21.jpg)

輸入 `kamigo` 後按 `Enter`，資料夾就新增完成了。如果你不喜歡這樣，你也可以回到檔案總管去新增資料夾。

然後新增 `eat.html` 檔案，在裡面寫你想要吃的東西。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgXlybjzHMgKMBoWa12PnKw3BztTVLt1zboLV5r9NsJs9bfuYh8Mxq4AooPXTiV1XetDkvPZJizsSHKxxkXwvqGaEGPe6Hb0-LHKyJfU7nzRWu9x-Q855iFycyxDVXGvwDJWucMeneXV5o/s1600/22.jpg)

再開一次網址看他有沒有修好：[http://localhost:3000/kamigo/eat](http://localhost:3000/kamigo/eat)

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjz0o0IfSZZBF7gTYfXJ5GbuXu-c1DyqQ8HAmJl2mG15QJaTuV373Ycb6ONuXZM0tP-VCoHrKsLEz2pqGFYr9XfjCPy-jKPawJPTVh8PpRpdC7wnxuP-J3lUQHkIeqyZyovXfC1q4SFudA/s1600/23.jpg)

修好了！

### 為了避免 Rails 亂幫我們做事，我們得明確說明我們想幹嘛

把 `app/controllers/kamigo_controller.rb` 檔案改成下面的內容後存檔：

```
class KamigoController &lt; ApplicationController
  def eat
    render plain: &quot;吃土啦&quot;
  end 
end
```

我們加入了一行程式碼 `render plain: &quot;吃土啦&quot;`，意思是我們要用純文字`&quot;吃土啦&quot;`來回應這個請求，這樣 Rails 就不會用預設的網頁路徑作為回應了。

再開一次網址看他有沒有變化：[http://localhost:3000/kamigo/eat](http://localhost:3000/kamigo/eat)。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqGMNuknKbp8NPVilWr9REa8oY00fIjd_LGqyR8328383ZzfQ9TxSPXyYTgykiuJcZy959V8ggYpRp-lBfb3MPsWp-XjPdjLPJkG7lW6OyuRr_6mgrfn8OWSJv9qpIYtibjNfidLjonX0/s1600/24.jpg)

卡米狗好兇阿。

# 總結

- 今天了解到 Rails 最基本的運作流程
- 對於 Sublime Text 的操作又更加熟悉了

明天我們要從 Rails 的角度觀察 HTTP 協定。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/7634228811010263629/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/7634228811010263629' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/7634228811010263629'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/7634228811010263629'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/rails-2018-it.html' title='第十四天：最基本的 Rails 運作流程（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-119792017533919305</id><published>2018-01-01T05:29:00.001+08:00</published><updated>2018-01-01T05:34:27.262+08:00</updated><title type='text'>第十三天：認識 Ruby 的資料型態（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）</title><content type='html'>markdown

今天要來學寫程式囉，今天就只講資料型態的部分，想要一天學完全部，我怕你的腦子會爆炸，畢竟在學校的話，這是一學期的課程。我們慢慢來就好，30 天之內一定會讓你學會寫聊天機器人的。

# 認識 irb

平常我們寫程式是寫在文字檔，然後再拿程式碼檔去一行行的執行程式。但當我們想要測試某一行程式碼的結果時，我們會使用 irb(Interactive Ruby) 來做測試。irb 很方便，我們可以在 irb 的環境裡面輸入一行程式碼，就能馬上得到這行程式碼的結果。

### 進入 irb

先按 `Windows`+`R` 輸入 `cmd` 叫出小黑框：

```
D:\只要有心，人人都可以作卡米狗&gt;
```

輸入 `irb`。

```
D:\只要有心，人人都可以作卡米狗&gt;irb
irb(main):001:0&gt;
```

輸入完之後就會進入 irb 的世界，在左側看到 irb 開頭就代表現在在 irb 裡面。

### 離開 irb

要離開 irb 的話就輸入 `exit` 或者按 `Ctrl`+`C`。

```
D:\只要有心，人人都可以作卡米狗&gt;irb
irb(main):001:0&gt; exit

D:\只要有心，人人都可以作卡米狗&gt;
```

就像這樣，就離開了 irb 的環境。

# 認識資料型態

再次進到 irb ，我們先認識一下資料型態：

### 數字

數字就是我們一般理解的數字，數學那種。

輸入 5566：

```
D:\只要有心，人人都可以作卡米狗&gt;irb
irb(main):001:0&gt; 5566
=&gt; 5566
```
輸入 `5566` 之後看到 `=&gt; 5566` 代表 `5566` 這行程式碼的執行結果是 `5566`。 

接下來輸入 5566+9527 看看：

```
irb(main):002:0&gt; 5566 + 9527
=&gt; 15093
```

輸入 `5566+9527` 之後看到 `=&gt; 15093` 代表 `5566+9527` 這行程式碼的執行結果是 `15093`。 

輸入 `5566.class` 可以讓 ruby 告訴我們，5566 是什麼型態：

```
irb(main):003:0&gt; 5566.class
=&gt; Integer
```

5566 是 Integer(整數)。

### 字串

輸入 `&#39;5566&#39;.class` 看看：
```
irb(main):004:0&gt; &#39;5566&#39;.class
=&gt; String
```

用單引號或雙引號包起來的東西是字串，字串就是文字的意思。

輸入 `&#39;5566+9527&#39;` 看看：

```
irb(main):005:0&gt; &#39;5566+9527&#39;
=&gt; &quot;5566+9527&quot;
```

因為加號也是文字的一部分，所以就不會做計算。

讓我們試著把兩個字串加起來看看，輸入`&#39;5566&#39;+&#39;9527&#39;`：

```
irb(main):006:0&gt; &#39;5566&#39;+&#39;9527&#39;
=&gt; &quot;55669527&quot;
```

就像是在 5566 後面輸入 9527 一樣。

在 windows 的 irb 可能會遇到不能輸入中文的情形，目前請先別介意，我們用英文就好。

### 變數

我們可以使用等號來幫數字或文字取名字。

```
irb(main):007:0&gt; idol = 5566
=&gt; 5566
```

變數定義完之後，我們只要叫到他，就視同輸入等號右邊的結果。

```
irb(main):008:0&gt; idol
=&gt; 5566
```

事實上我們可以取名字的東西非常多，不是只有數字和文字能被取名。

### 陣列

我們可以一次宣告大量變數，語法是中括號 [] 的中間用逗號區隔。

```
irb(main):009:0&gt; eat = [&quot;Mcdonald&#39;s&quot;, &#39;KFC&#39;, &#39;BUG King&#39;, &#39;MOS BUG&#39;]
=&gt; [&quot;Mcdonald&#39;s&quot;, &quot;KFC&quot;, &quot;BUG King&quot;, &quot;MOS BUG&quot;]
```
我們這樣就宣告了4個字串變數，分別是：
- eat[0] = &quot;Mcdonald&#39;s&quot;
- eat[1] = &quot;KFC&quot;
- eat[2] = &quot;BUG King&quot;
- eat[3] = &quot;MOS BUG&quot;

在電腦的世界裡第一個數字是 0。，所以宣告一堆變數之後，第一個變數的索引是 0。

你也可以這樣寫，效果是相同的：
```
irb(main):010:0&gt; eat = []
=&gt; []
irb(main):011:0&gt; eat[0] = &quot;Mcdonald&#39;s&quot;
=&gt; &quot;Mcdonald&#39;s&quot;
irb(main):012:0&gt; eat[1] = &#39;KFC&#39;
=&gt; &quot;KFC&quot;
irb(main):013:0&gt; eat[2] = &#39;BUG King&#39;
=&gt; &quot;BUG King&quot;
irb(main):014:0&gt; eat[3] = &#39;MOS BUG&#39;
=&gt; &quot;MOS BUG&quot;
irb(main):015:0&gt; eat
=&gt; [&quot;Mcdonald&#39;s&quot;, &quot;KFC&quot;, &quot;BUG King&quot;, &quot;MOS BUG&quot;]
```

第一行是宣告一個空陣列，然後接下來的每一行都是塞入一個值。

你也可以這樣寫，效果也是相同的：
```
irb(main):016:0&gt; eat = []
=&gt; []
irb(main):017:0&gt; eat &lt;&lt; &quot;Mcdonald&#39;s&quot;
=&gt; [&quot;Mcdonald&#39;s&quot;]
irb(main):018:0&gt; eat &lt;&lt; &#39;KFC&#39;
=&gt; [&quot;Mcdonald&#39;s&quot;, &quot;KFC&quot;]
irb(main):019:0&gt; eat &lt;&lt; &#39;BUG King&#39;
=&gt; [&quot;Mcdonald&#39;s&quot;, &quot;KFC&quot;, &quot;BUG King&quot;]
irb(main):020:0&gt; eat &lt;&lt; &#39;MOS BUG&#39;
=&gt; [&quot;Mcdonald&#39;s&quot;, &quot;KFC&quot;, &quot;BUG King&quot;, &quot;MOS BUG&quot;]
```
`&lt;&lt;` 表示放一個東西進入陣列。

查看 `eat` 的資料型態會發現資料型態是 Array(陣列)。
```
irb(main):021:0&gt; eat.class
=&gt; Array
```

查看 `eat[0]` 的資料型態會發現資料型態是 String(字串)。
```
irb(main):022:0&gt; eat[0].class
=&gt; String
```

跟一般的變數一樣，輸入 `eat[2]` 可以取得他的內容。

```
irb(main):023:0&gt; eat[2]
=&gt; &quot;BUG King&quot;
```

但是你還可以這樣：
```
irb(main):024:0&gt; bgk = 2
=&gt; 2
irb(main):025:0&gt; eat[bgk]
=&gt; &quot;BUG King&quot;
```

也許現在還很難理解把 `2` 換成 `bgk` 會有什麼好處，本篇結尾會說明。

# 雜湊陣列

在陣列中，我們學到可以用數字作為索引，而雜湊陣列則是把數字改成任何資料型態的變數。

雜湊陣列(Hash)是使用大括號作為定義：

```
irb(main):026:0&gt; eat_hash = {}
=&gt; {}
irb(main):027:0&gt; eat_hash[0] = &quot;Mcdonald&#39;s&quot;
=&gt; &quot;Mcdonald&#39;s&quot;
irb(main):028:0&gt; eat_hash[1] = &quot;KFC&quot;
=&gt; &quot;KFC&quot;
irb(main):029:0&gt; eat_hash[2] = &quot;BUG King&quot;
=&gt; &quot;BUG King&quot;
irb(main):030:0&gt; eat_hash[3] = &quot;MOS BUG&quot;
=&gt; &quot;MOS BUG&quot;
irb(main):031:0&gt; eat_hash
=&gt; {0=&gt;&quot;Mcdonald&#39;s&quot;, 1=&gt;&quot;KFC&quot;, 2=&gt;&quot;BUG King&quot;, 3=&gt;&quot;MOS BUG&quot;}
```

你也可以這樣寫：
```
irb(main):032:0&gt; eat_hash = {0=&gt;&quot;Mcdonald&#39;s&quot;, 1=&gt;&quot;KFC&quot;, 2=&gt;&quot;BUG King&quot;, 3=&gt;&quot;MOS BUG&quot;}
=&gt; {0=&gt;&quot;Mcdonald&#39;s&quot;, 1=&gt;&quot;KFC&quot;, 2=&gt;&quot;BUG King&quot;, 3=&gt;&quot;MOS BUG&quot;}
```

和陣列不同的是，雜湊陣列不需要從 0 開始。

而且雜湊陣列的特性是可以用不是數字的變數作為索引，其中最常用的索引是字串索引，舉例來說：

```
irb(main):033:0&gt; kamigo = {}
=&gt; {}
irb(main):034:0&gt; kamigo[&#39;Q&#39;] = &#39;A&#39;
=&gt; &quot;A&quot;
```

而這就是卡米狗學說話的核心原理。

卡米狗有一個很大的雜湊陣列，當有人教卡米狗說話時，我就會將關鍵字和要回應的內容儲存到雜湊陣列裡，像這樣：

`kamigo[關鍵字] = 要回應的內容`

之後任何人說話，我就會去檢查 `kamigo[關鍵字]` 是不是有值，如果有值的話卡米狗就會做出回應。

講到這，應該就知道為什麼索引可以用變數代換是一件很重要的事情了吧？</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/119792017533919305/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/119792017533919305' title='1 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/119792017533919305'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/119792017533919305'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2018/01/ruby-2018-it.html' title='第十三天：認識 Ruby 的資料型態（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4221096241867026900.post-7762679409566577189</id><published>2017-12-31T01:52:00.000+08:00</published><updated>2017-12-31T01:59:11.362+08:00</updated><title type='text'>第十二天：從瀏覽器認識 HTTP 協定（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）</title><content type='html'>markdown

# 複習 HTTP 協定

我們在[第六天：認識網站](https://ithelp.ithome.com.tw/articles/10193664)，安裝了chrome瀏覽器，並且在[第九天：作一個最簡單的 Rails 網站](https://ithelp.ithome.com.tw/articles/10194359)成功架好一個 Rails 網站。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiRBpBQJf3MdaAk2bH5YQjq3bkppb1uZSr1Sq1QQBiok4gm92Er6mrovpzRuUnIGv8bNo-vSwkdy1AxfBcZQtVpsMXb5K76jwC1qCsYsEElr2s4uTjkvMeLkiOzlCVQ4yA0aIVpZ8m69UQ/s1600/01.PNG)

我們可以透過 Chrome 的開發人員工具來觀察瀏覽器開啟一個網頁的過程。

# 認識 Chrome 開發人員工具

從昨天結束的地方繼續，先開啟 rails server 之後開啟 [http://localhost:3000/](http://localhost:3000/)。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhafsjC9nYu_Ny_ewgmxFBJjUoNxBNbYSdbIdPWQ6gL_h9IhutFMp0nShUoXCjyX16Q3c7ftU_aRaU28rTQ_BkmZ1UVraMTxsYxnNyb-hbzy4iqLmMHnuO9UKNrxfg7VzTJIWCnmUPCVO8/s1600/2.jpg)

按下 `F12` 或 `Ctrl`+`Shift`+`I`，就可以叫出開發人員工具。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrSPig9FPhwZ-cU-7EpbwruiHZuDfOYmc3HO4QTMKN-ounYN_TlIHtVcM4MLbXx2tzdQ59nOUPk6uBpniFwX23e3_Hr6TgcaogRyX5miQIOnas9CHgc6gVWAcLwx1fqvG2D0tb0buow2w/s1600/3.jpg)

這個畫面有點複雜，總之先按這裡的`X`，把多餘的部分關掉。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh3YwITuKS6eMQD4yjJeuEUz2O8Qxmw3giG0QXscoV0bcm5VrgF7tX_7VWRAHndJWr4bXMRJv5yvuHx8S5MXXH9HKX8l-IuYKIqpUaujPJKuTj4wxisHeCTuImUhijZ4AzV6v62Ok13Yog/s1600/4.jpg)

按了之後應該會看起來像這樣：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsATxBJVctvQ08sFjB646pCwJY1bYK_KS0Nj-ypEQFEj_werFsAq6GWgFwVj1AIhHU-vO3T74YsWYlvGHmz4pYTu3iBJN1nu3P-yavkc9Ckc0O_i0YzBw9KaXeYTpnAV_oLamE7pV4-K4/s1600/5.jpg)

開發人員工具有很多的功能，我們只需要用到其中的一小部分，這裡就不一一介紹。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiUzDFLMuM_5XGNNwL9VM7hqTPr17oLWV0QdJRWxDJ5Ir_SvO0TsrpmqR3UFTMsbeVxRco7p2hOF4LJrZGhCwvdO3kDT3aPAL2Yekrrz4LN-pT7TodSJ80Git3N2W1XbIoWqHcp78yTKA8/s1600/6.png)

請先切換到 `Network` 分頁。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEioJoXW2mZMLEMWmfQ3yBosSgcbKGYt1SoI1-n9VsXuuLErSQVCWTW4cFS12VD5QnojlHiXNiAwRhaz8LKeQCJWkEzDj5KwKY5dd6sDCvENGmINhlc28hf7-ueZLyPDCkp_fpup9zVtVdI/s1600/7.jpg)

一個複雜的介面，但是什麼資料都沒有顯示，那是因為開發人員工具開啟後才會開始監控數據，所以我們要重開網頁，這裡按下 `Ctrl`+`R`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg1K1JUg-CDO35iZSq0BwNmDvgMVyHjbEo8SMFGmxf1PHC50a-QdAQFkzIYzNk7ssR1sKFdRJQsKp80tFsYy8wKzCHtW7fBxTDHsKqSIVv5_JkdoMIkcEvj6HB4SNWRzTLvDzCyqR2VK8Q/s1600/8.jpg)

出現了兩筆資料，一個是 `localhost`，另一個是 `favicon.ico`，我們點 `localhost`。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjLecZjpMhTMLm1OsmdKZROvapSeOlqLe8lK0H4DI9v2b55Z18Cll08ETAZBp5WgmQpzlY6tafXVlPl1dVxXLc-EVXb06dTiGKxtxHfmmLhzUffQRGHyoefTcQhS9TPjNkqMud6MEGILJo/s1600/9.jpg)

我們看見這裡有四個分頁：`Headers`、`Preview`、`Response`、`Timing`，我們只需要認識 `Headers` 和 `Response`。

# Headers

`Headers` 這裡的資料也很多，我們只需要認識其中的三項：

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3WuslVYMRKDFOQdFBohdxaRMvOkrV11MxxXytKoEcUCGkANhyGDXpDPVXtEnQ1thenCInGR0j1-F6VO3OMlsFRv7Xq8qexI55_m5cb0zxLLGXgDUhxKG_Hz3wNo8f65ve7L1ZBDWH2WQ/s1600/10.jpg)

- Request URL:http://localhost:3000/
- Request Method:GET
- Status Code:304 Not Modified

接下來就一項一項講解。

### Request URL

請求網址：就是你在瀏覽器上輸入的網址，因為你輸入的是 http://localhost:3000/，所以他顯示 http://localhost:3000/。

### Request Method

請求方法：瀏覽器希望網頁伺服器做些什麼事，我們這裡是用 `GET` 是表示我們想要下載檔案。

除了 `GET` 之外，還有其他的請求方法可以使用，比方說 `POST`、`PUT`、`DELETE`，以下是各個請求方法的用途：

- GET：下載檔案
- POST：上傳資料
- PUT：更新資料
- DELETE：刪除資料

舉個例：當你想在論壇發文，你把文章標題和內文填好，按下送出時，那就應該是一個 POST 請求方法。

依照目前可公開的情報，我們只需要知道 `GET` 和 `POST` 即可。

### Status Code

狀態碼：是網頁伺服器用一個數字來表示瀏覽器提出的請求最後有沒有完成，如果沒有，為什麼沒有。其實提款機也有類似的東西，叫做[訊息代碼](http://www.bot.com.tw/services/atmmsg.htm)。

當然這個[狀態碼](https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81)列出來會有很多，但因為我們只是要做一個聊天機器人，不需要認識全部，我們現階段只認識必須認識的那些。最主要的是你必須知道有狀態碼的存在，當你知道有狀態碼這種東西時，你需要相關資訊時就能在 Google 搜尋時輸入正確的關鍵字。

正式版說明：
- 200：表示成功。
- 304：表示瀏覽器已經有一模一樣的檔案。
- 400：瀏覽器發出的請求被網頁伺服器拒收，通常是發出的請求格式不正確。
- 404：找不到網頁。
- 500：網頁伺服器掛了。

網頁伺服器擬人：
- 200：網頁伺服器：「好哦～好哦～」
- 304：網頁伺服器：「你手上的檔案是最新的。」
- 400：網頁伺服器：「駁回！」
- 404：網頁伺服器：「你想找的東西不在我這。」
- 500：網頁伺服器：「阿阿我要壞掉惹。」

# Response
又稱為 Response Body(回應內容)：指的是網頁伺服器回應給瀏覽器的內文部分。

![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4MNfiIqOj4driKfxH6seWwnNo3HF2AHfZvnpLBD0zYFn9sLR27F77hu1UKdz_rIJNivbHA-GhDiQcWBS8yjqmQ5bF1Vct1YCh4lqbIJZi2A7bEaXdcrEodRlXrSB-M5Q2x8ez9oudZbE/s1600/11.jpg)

其實就是檔案內容的部分。

# HTTP 總結
- 由瀏覽器發給網頁伺服器的請求稱為 HTTP Request，HTTP Request 包含 Header 和 Body。
- 由網頁伺服器回應瀏覽器的請求稱為 HTTP Response，HTTP Response 也包含 Header 和 Body。
- Request Header 中的 Request Method 表示瀏覽器希望網頁伺服器做些什麼事。
- Response Header 中的 Status Code 表示網頁伺服器告訴瀏覽器事情辦好沒。

接下來我們從網頁伺服器的角度來觀察 HTTP Request 和 HTTP Response，但這需要寫點程式，所以我們明天會先從認識 Ruby 程式語言開始。</content><link rel='replies' type='application/atom+xml' href='http://etrex.blogspot.com/feeds/7762679409566577189/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4221096241867026900/7762679409566577189' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/7762679409566577189'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4221096241867026900/posts/default/7762679409566577189'/><link rel='alternate' type='text/html' href='http://etrex.blogspot.com/2017/12/http-2018-it.html' title='第十二天：從瀏覽器認識 HTTP 協定（ 2018 iT邦幫忙鐵人賽－只要有心，人人都可以作卡米狗 ）'/><author><name>卡卡米</name><uri>http://www.blogger.com/profile/04885803843782300210</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry></feed>