tag:blogger.com,1999:blog-42210962418670269002024-03-06T12:46:41.186+08:00卡卡米的記憶體卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.comBlogger411125tag:blogger.com,1999:blog-4221096241867026900.post-79167779016217186182018-09-04T16:13:00.002+08:002018-09-04T16:20:09.463+08:00在 rvm 上使用 ruby 2.5.1 跑 rails 5.2.1 時會發生的警告:already initialized constant FileUtils::VERSION在 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
```卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-28639781724282569412018-06-13T01:22:00.001+08:002018-06-13T10:09:47.604+08:00Ruby Kaigi 2018 會後心得markdown
這是我第一次參加 Ruby Kaigi,Ruby Kaigi 是一個 Ruby 的大型研討會,參加者約 1000 人左右,在日本仙台國際中心舉辦。
我們公司([五倍紅寶石](https://5xruby.tw/))免費提供了機票、住宿、研討會門票和生活津貼等資源給想要參加 Ruby Kaigi 的員工,目的是避免員工成為邊緣人、促進員工多參與社群,我們的員工福利真的hen好,感恩五倍、讚嘆五倍。
我們搭乘虎航的飛機,從桃園機場前往日本仙台。
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsp-bghYWOEvejttfI06GML5Eg40y3lBYVQB_6WOuQopS_3Aw5grKJIh0eUVo70WMln76i3FZ6CL-cvk7hd8Qd6KjI7E8SClOKQ4EiHyPUEQAl7tvcT2HaX7PZzGISDh1D0n_4GdMhf1U/s1600/2018-05-29+13.13.46.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1600" data-original-width="1600" height="640" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsp-bghYWOEvejttfI06GML5Eg40y3lBYVQB_6WOuQopS_3Aw5grKJIh0eUVo70WMln76i3FZ6CL-cvk7hd8Qd6KjI7E8SClOKQ4EiHyPUEQAl7tvcT2HaX7PZzGISDh1D0n_4GdMhf1U/s640/2018-05-29+13.13.46.jpg" width="640" /></a></div>
抵達仙台後再搭從仙台空港站 to 仙台站的鐵路交通,類似台灣的捷運,但不確定要怎麼稱呼。
出仙台站後發現我們的飯店竟然就直接蓋在車站旁邊大約 50 公尺的距離。
後來發現我們甚至可以從車站內直接走到飯店,不需要走出車站。
抵達飯店後,同事就直接在大廳擺出了一個帥爆的姿勢。
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFpRRFlebOkA92ykeKJpsu8RvEGmx0DlFs7blAYXdlF3znRs7QyqWA5zBEUS7SdafL4CEAfBungQ3F1MhleohYh4rdAZsmC1oZ0bqIhlgGsERrq2yzYFek-SIL7wzGgl1x4r_EdlcGUGc/s1600/2018-05-29+19.52.17.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1200" data-original-width="1600" height="480" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFpRRFlebOkA92ykeKJpsu8RvEGmx0DlFs7blAYXdlF3znRs7QyqWA5zBEUS7SdafL4CEAfBungQ3F1MhleohYh4rdAZsmC1oZ0bqIhlgGsERrq2yzYFek-SIL7wzGgl1x4r_EdlcGUGc/s640/2018-05-29+19.52.17.jpg" width="640" /></a></div>
為了之後的 Party 做準備,第一天的晚餐就是燒肉喝到飽。在日本,似乎很盛行喝酒交朋友的文化。
燒肉喝到飽是指飲料(包含酒類)可以無限點(如果你喝得夠快的話,就可以趨近於無限),但是燒肉的部分是類似套餐,是固定的份量。
飲料的 Menu 上可以看見各種酒名以日文表示。因為我比較喜歡喝調酒,所以我的策略就是在調酒的分類按照順序點。點餐時基本上我就是隨便指一個,即使 Google 加上翻譯,還是很難知道自己喝到的是什麼,畢竟調酒名稱本來就都很詭異。
第二天是 Remote 工作日,據說是因為發現早一天到達仙台可以獲得比較便宜的總支出。
第二天的晚上,也就是 Ruby Kaigi 的前一天晚上,有一個由 Speee 主辦的 Preparty。
Speee 是 Ruby Kaigi 的贊助廠商之一,由於 Ruby Kaigi 的 Party 之多,我無法判斷到底哪些 party 才是所謂「官方」所舉辦。
現場看起來大概像這樣:
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiiKK_8N_I1pnGmNvuWERWJDNvPuljYknlrMpDe66rnm67gEz26Cw0MW3FqQ4kvYUwRhG3qW2QRB5yHL7Ghpiq8fPO77_fwChuq3mjJ2ReKJhpja2IYXZFilEsx2Y1Bu2quoY9tdQG2_t8/s1600/2018-05-30+19.03.26.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1200" data-original-width="1600" height="480" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiiKK_8N_I1pnGmNvuWERWJDNvPuljYknlrMpDe66rnm67gEz26Cw0MW3FqQ4kvYUwRhG3qW2QRB5yHL7Ghpiq8fPO77_fwChuq3mjJ2ReKJhpja2IYXZFilEsx2Y1Bu2quoY9tdQG2_t8/s640/2018-05-30+19.03.26.jpg" width="640" /></a></div>
中間有一整條的食物區,旁邊則是大型方桌提供酒類。
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEityeY42KN4aDSOQk5brmPvKiP3d-7CdoEB9NwP3OKo-3Q48BO6jiIG4F42LvNJdaqBv7-wIy31ABeqEYyqW4L9tT_gEbfsmFxId8Ko55R7cE9Ga9OWXf7Xbi17tPaqbkMMt3MxIPm34iE/s1600/2018-05-30+19.03.53.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1200" data-original-width="1600" height="480" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEityeY42KN4aDSOQk5brmPvKiP3d-7CdoEB9NwP3OKo-3Q48BO6jiIG4F42LvNJdaqBv7-wIy31ABeqEYyqW4L9tT_gEbfsmFxId8Ko55R7cE9Ga9OWXf7Xbi17tPaqbkMMt3MxIPm34iE/s640/2018-05-30+19.03.53.jpg" width="640" /></a></div>
壽司是主食,只可惜我不吃壽司。
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEguVRhgtEdT83dJVQdxy6awaTqefYzZqh2bQ6I8Ym2i295pCQiRfURoeSg1f6lI4DQiOgnLD3Abs6C8cn6XWRhKq3pg2Ev6SemUI6c6KBo_8rUb1qioPTiT8hqKAyyk1rlPZnjZnR4Dnk0/s1600/2018-05-30+19.41.54.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1600" data-original-width="1200" height="640" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEguVRhgtEdT83dJVQdxy6awaTqefYzZqh2bQ6I8Ym2i295pCQiRfURoeSg1f6lI4DQiOgnLD3Abs6C8cn6XWRhKq3pg2Ev6SemUI6c6KBo_8rUb1qioPTiT8hqKAyyk1rlPZnjZnR4Dnk0/s640/2018-05-30+19.41.54.jpg" width="480" /></a></div>
旁邊有一個調酒區,可以跟 NPC 點酒,這一罐紫色的酒(Crème de cassis)是我有興趣的酒,所以特地拍了一張。
<div class="separator" style="clear: both; text-align: center;">
<a href="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" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1600" data-original-width="1200" height="640" src="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" width="480" /></a></div>
在這個會場有個舞台,Party 中途有一些活動,其中一個活動是清酒盲測。這一罐看起來很高級的清酒會被倒在A杯或B杯中,而另一個杯子就會是普通清酒。
主持人在現場尋找自願參加者,有興趣的人就可以上去玩。
參加者需要戴上眼罩,隨後工作人員就會開始倒酒,此時只有參加者不知道哪一杯酒是高級清酒,現場所有的旁觀者都看得一清二楚。
參加者喝完之後需要舉牌表示意見,主持人也會詢問他們為何選擇這樣的答案,這是一個還蠻有趣的環節。
<div class="separator" style="clear: both; text-align: center;">
<a href="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" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1600" data-original-width="1200" height="640" src="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" width="480" /></a></div>
這是在遊戲說明時介紹高級清酒的部分。
在這個場合,我們需要多嘗試接觸其他人。這對我來說難度很高,因為即使是台灣人我都不知道該跟對方說什麼,更何況是外國人。
我覺得我就像是一隻被動怪,如果有人來跟我對話的話,我是OK的。但是我不會主動攻擊。
所以我後來就發展出一個策略:跟著一個主動怪同事(以下簡稱主動怪),由他發動攻擊,其他人就圍上去這樣。
主動怪教我們如何發動攻擊:「你眼睛就四處看,有時候會剛好跟某人四目相對,這時候就直接走過去打招呼就行惹。」
我是覺得這就像神奇寶貝遊戲裡的路人訓練師,當你路過他面前時,他就會走過來找你PK。
在 Party 結束回到飯店後,一群人休息後移動到主動怪的房間聽同事試講,因為我們同行的同事包含兩位講者。
第三天,Kaigi 終於開始。
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbrEKEwYeHiywCS69dfp8PQrKjIL4QkxCW57241F6GnomosP4NPzmP-Hn0SziFn6VjPwSqATE3FssoYN8WY-4stOUzq6ljFTlQQQ1aNMWkOjQeJWz8ikwWLlzkfgph8zWdi54-CqfmnKo/s1600/2018-05-31+10.05.34.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbrEKEwYeHiywCS69dfp8PQrKjIL4QkxCW57241F6GnomosP4NPzmP-Hn0SziFn6VjPwSqATE3FssoYN8WY-4stOUzq6ljFTlQQQ1aNMWkOjQeJWz8ikwWLlzkfgph8zWdi54-CqfmnKo/s640/2018-05-31+10.05.34.jpg" width="640" height="480" data-original-width="1600" data-original-height="1200" /></a></div>
# 午餐
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzfJ2P9Fh1M0w4aPVEdZUWEKiAUOA-lPDnVUHtGA0uyI3JbI-dxrPthxJ9eYhb2uKsAzFqeGicntyM2v-ErZvmGi6nuqJzycnbbIX_d_evUJzqarbMuhxMoVVmFNgglmVswJaZDmHj-HQ/s1600/2018-05-31+11.54.51.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzfJ2P9Fh1M0w4aPVEdZUWEKiAUOA-lPDnVUHtGA0uyI3JbI-dxrPthxJ9eYhb2uKsAzFqeGicntyM2v-ErZvmGi6nuqJzycnbbIX_d_evUJzqarbMuhxMoVVmFNgglmVswJaZDmHj-HQ/s640/2018-05-31+11.54.51.jpg" width="640" height="480" data-original-width="1600" data-original-height="1200" /></a></div>
他們提供了多樣性的便當選擇,但因為我不吃素,不吃壽司,也不吃醃漬類食物,所以我三天都拿同一款便當,也就是這款。
我們帶著便當到附近的公園(似乎是古蹟)野餐,看起來像這樣:
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8A4vGE0ELB6qqxxpuGkvRZuvEiwvcxjnhoVF2g_CqgObAAAW3_uuJGAFVQlo21-GCn63gVWieb1xw6XiCBG5InTRe1HeZXXDoHO8gkm7vrbCYuIPhRTh8CklDqX-Bz9cWeU1he8XqV_U/s1600/2018-05-31+11.56.06-1-1.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8A4vGE0ELB6qqxxpuGkvRZuvEiwvcxjnhoVF2g_CqgObAAAW3_uuJGAFVQlo21-GCn63gVWieb1xw6XiCBG5InTRe1HeZXXDoHO8gkm7vrbCYuIPhRTh8CklDqX-Bz9cWeU1he8XqV_U/s640/2018-05-31+11.56.06-1-1.jpg" width="640" height="640" data-original-width="1600" data-original-height="1600" /></a></div>
# 下午茶
下午茶提供各式日本茶點,有銅鑼燒和各種我講不出名字的當地名產還有一些水果拼盤。水果的部分還是台灣的比較多樣化一點。
若要論好吃程度的話,在每種都已經吃過的情況下,如果現場有鳳梨酥,我會選擇吃鳳梨酥。
# 各種 Party
除了官方辦的 Party 之外,其他 Party 都是免費的,但我認為免費的 Party 都表現得比較好。我想應該是因為免費,所以感覺比較好吧。
所有的 Party 都提供無限量的啤酒跟清酒,我比較喜歡清酒,因為啤酒有氣泡,而我不太能喝含有氣泡的飲料。
# 贊助商
我在贊助商區發現了一個很有趣的攤位。
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihO4p7gVWaOQpEeD9BsJiB-V3rY6_sjcIxpkJYk2oIWk2GkEGMyuLAJcqB3ARlB19ejhvonxCV2LRntFgbJ5g6l2IiEzILuSSWtck-rSFzkQG5Cq2mPILqJMOWdEo1cBnoMa6B8dHajLU/s1600/2018-05-31+12.49.48.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihO4p7gVWaOQpEeD9BsJiB-V3rY6_sjcIxpkJYk2oIWk2GkEGMyuLAJcqB3ARlB19ejhvonxCV2LRntFgbJ5g6l2IiEzILuSSWtck-rSFzkQG5Cq2mPILqJMOWdEo1cBnoMa6B8dHajLU/s640/2018-05-31+12.49.48.jpg" width="640" height="480" data-original-width="1600" data-original-height="1200" /></a></div>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgymyfNWnAaF8fHg-cvtbqUoKFPqqQd4aOza38SsecHhhM5ccanU1yn61KOMKQmqcY3cFsdxy8WZ5QWSPhY9lTyXxd_xh90JXMr_hvDWUBVgtAIzUYHF0tmJiB8YJaWU3RV94gXtCUbr58/s1600/2018-05-31+12.50.06.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgymyfNWnAaF8fHg-cvtbqUoKFPqqQd4aOza38SsecHhhM5ccanU1yn61KOMKQmqcY3cFsdxy8WZ5QWSPhY9lTyXxd_xh90JXMr_hvDWUBVgtAIzUYHF0tmJiB8YJaWU3RV94gXtCUbr58/s640/2018-05-31+12.50.06.jpg" width="640" height="480" data-original-width="1600" data-original-height="1200" /></a></div>
使用不同顏色的樂高來區分,可以即時生成對應的室內設計圖。
他們的贊助商區提供了各式各樣的小禮物,藉此吸引你過去他們的攤位。有些則是會要求你在 Twitter 上發文或者要求你做一些事情才會送。
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFJrxzxuaYT6-VzEywbXf1SeS_A6TaRsnUyb37xTPys4WnMe8FTzdMwndNVVYQVslJ00rjVmdggHtSwLnARTBhePcSrdtE29kEN97I1Ekt95bco2SyNoe8XXNIv_CB1npVtBYsIibG0R0/s1600/2018-05-31+23.18.21-1.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFJrxzxuaYT6-VzEywbXf1SeS_A6TaRsnUyb37xTPys4WnMe8FTzdMwndNVVYQVslJ00rjVmdggHtSwLnARTBhePcSrdtE29kEN97I1Ekt95bco2SyNoe8XXNIv_CB1npVtBYsIibG0R0/s640/2018-05-31+23.18.21-1.jpg" width="640" height="640" data-original-width="1600" data-original-height="1600" /></a></div>
有手提袋、資料夾、貼紙、扇子、徽章、入浴劑、香皂、溜溜球、杯墊、包包掛環、筷子、眼鏡布、pokey、筆記本、詐神筆記本等。
# 古蹟
趁中午吃完飯去看了一下博物館和仙台城跡,因為博物館不能拍照,所以就給大家看看仙台城跡上有什麼。
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFzAddGryoa8HsNORsVwLDzPO8C_YLTvoodFn2dn3loyNsxKx1_xt7ffGDuAxNNGcbnP-91BRjENkVP1H-_B_08FkmBf8fhyphenhyphen2q4_zczi1aT5ih715a60B8iCCnQCwh7VD5YA3XlPX8fpU/s1600/2018-06-02+12.24.41.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFzAddGryoa8HsNORsVwLDzPO8C_YLTvoodFn2dn3loyNsxKx1_xt7ffGDuAxNNGcbnP-91BRjENkVP1H-_B_08FkmBf8fhyphenhyphen2q4_zczi1aT5ih715a60B8iCCnQCwh7VD5YA3XlPX8fpU/s640/2018-06-02+12.24.41.jpg" width="640" height="480" data-original-width="1600" data-original-height="1200" /></a></div>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhW3oeSwCJWnS-hj_6NMbMjJ7LClJHFgjwc3jcSQxJ01cQHAGBneCQjToJzQZ4NjI4a71BqNYpgl4HZAFZyrf-XJ63iN-MmFtv7ctenFmQAKKpU9yODcn6wORspg6vr2CNxhCaklRUJwbU/s1600/2018-06-02+12.25.39.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhW3oeSwCJWnS-hj_6NMbMjJ7LClJHFgjwc3jcSQxJ01cQHAGBneCQjToJzQZ4NjI4a71BqNYpgl4HZAFZyrf-XJ63iN-MmFtv7ctenFmQAKKpU9yODcn6wORspg6vr2CNxhCaklRUJwbU/s640/2018-06-02+12.25.39.jpg" width="640" height="480" data-original-width="1600" data-original-height="1200" /></a></div>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaWPqs6r9Yvh97y6Apo7Z9VTDidaryIKai8cXPBC1dfdUX4ILq18Pv8qQnuPg8Hg4i1Ri1PplIAPcT-Ft2tp5B2__zaFSGFJJYhuflh871WEPXVEibatX5qXjg8lTCnJSsl-sf2NJnc0Y/s1600/2018-06-02+12.32.09-1.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaWPqs6r9Yvh97y6Apo7Z9VTDidaryIKai8cXPBC1dfdUX4ILq18Pv8qQnuPg8Hg4i1Ri1PplIAPcT-Ft2tp5B2__zaFSGFJJYhuflh871WEPXVEibatX5qXjg8lTCnJSsl-sf2NJnc0Y/s640/2018-06-02+12.32.09-1.jpg" width="640" height="480" data-original-width="1600" data-original-height="1200" /></a></div>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi99KP4HsVEiGoXhDcuRKAHu5y805xg3cEi2SA01gGR0EWgKajvu5HhzIbYRICSiXm9BdUbKVzia6KX1ypkrwlBiJoiJYEtrrfN33U8AxoVl6pcBtS5GGe-XjRkjG7VuTWyxKFsB3dS9qw/s1600/2018-06-02+13.04.01.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi99KP4HsVEiGoXhDcuRKAHu5y805xg3cEi2SA01gGR0EWgKajvu5HhzIbYRICSiXm9BdUbKVzia6KX1ypkrwlBiJoiJYEtrrfN33U8AxoVl6pcBtS5GGe-XjRkjG7VuTWyxKFsB3dS9qw/s640/2018-06-02+13.04.01.jpg" width="480" height="640" data-original-width="1200" data-original-height="1600" /></a></div>
# 議程
## [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 教育
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNqIB7yKNXOCERk_kq8_w-KDgdJkIYdHMgGx3D3-QG5THuMKvD1PgpIPg1V-b4nozPnoEYMJBvvADI9zDMDxb1VNgYJfo9LKNACbL7uIzwBhxT-BP2UP8ABrNKnAhMX28RNeH44TaEJK0/s1600/2018-06-01+14.24.38.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNqIB7yKNXOCERk_kq8_w-KDgdJkIYdHMgGx3D3-QG5THuMKvD1PgpIPg1V-b4nozPnoEYMJBvvADI9zDMDxb1VNgYJfo9LKNACbL7uIzwBhxT-BP2UP8ABrNKnAhMX28RNeH44TaEJK0/s640/2018-06-01+14.24.38.jpg" width="640" height="480" data-original-width="1600" data-original-height="1200" /></a></div>
上圖說明了這樣的教育方式也能實踐到環境架設上
在其他的場次中沒有獲得值得寫下來的內容,或者大多數內容是聽不懂的。
# 會場周邊活動
在會場有一個小區域做了 Pair Programming 的直播,主題是使用 Ruby 寫出一個 [Game of Life](https://en.wikipedia.org/wiki/Conway's_Game_of_Life)
在這個活動進行的同時,議程也在進行中,所以在這邊看他們 Pair Programming 就無法看議程。
Game of Life 是假設這個世界就像棋盤,而每一個格子裡面可能有生物或沒有生物。
如果一個格子的周圍 8 格有其他生物,稱為鄰居,而鄰居的數量會影響生物出生或者死亡。設定不同的出生和死亡條件會讓結果不同。
本次實作的出生條件:某個空格剛好有 3 個鄰居,就會在某格生成 1 個生物
本次實作的死亡條件:某個生物的鄰居 < 2 或者 > 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,讓你只需要實作問題以及演算法,就能夠直接變成一個網路服務的形式存在的話,我認為應該會很有搞頭。
卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-77020120404258049452018-01-23T00:06:00.002+08:002018-01-23T00:20:16.726+08:00只要有心,人人都可以作卡米狗 - 完賽心得# 參加感想
其實一開始參加的時候是想說反正隨時棄坑都沒關係,至少我有開始過。但沒想到讀者比我預想的還要多,情況有點不受控制,我似乎不得不把質跟量都作出來,不然就會辜負這些讀者。不過也感謝大家的支持,我才能順利完賽,在沒有任何文章存稿的情況下參賽,連我都不相信我能完賽。
# 讀者群的設定
因為卡米狗粉都是沒有接觸資訊領域,不會寫程式的人,所以在我寫文的一開始,就把讀者群設定在電腦只有開過 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) 這種比較酷炫的功能。
以下開放許願,我考慮有時間的時候再回來講講。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com1tag:blogger.com,1999:blog-4221096241867026900.post-91085437814138915912018-01-18T03:20:00.000+08:002018-01-18T03:28:25.508+08:00第三十天:卡米狗查天氣今天就是最後一天惹,有些事情想跟你們講一下,那就是我們前幾天到底在幹嘛。
以下是一些示意圖,說明我們的 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 的函數
一步步來吧。
# 學會怎麼抓到最新的雷達回波圖網址
當然,如果我們是用瀏覽器下載,那麼很簡單直接網頁打開`右鍵`->`另存圖片`就載好了。可是我們是要用程式去載圖,不是人工載圖。
所以我們要用程式去開啟網頁,然後從網頁原始碼裡面找到圖片連結就行了。
先開這個網頁:[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 & jQuery 前端開發入門實戰](https://5xruby.tw/talks/JS-jQuery-2018-1)
```
var HDRadar_1000_n_val=new Array(
new Array("2018/01/18 01:20","/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180120.png"),
new Array("2018/01/18 01:10","/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180110.png"),
new Array("2018/01/18 01:00","/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180100.png"),
new Array("2018/01/18 00:50","/V7/observe/radar/Data/HD_Radar/CV1_1000_201801180050.png"),
...
```
這是 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? '天氣'
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('http://www.cwb.gov.tw/V7/js/HDRadar_1000_n_val.js')
response = Net::HTTP.get(uri)
start_index = response.index('","') + 3
end_index = response.index('"),') - 1
"http://www.cwb.gov.tw" + response[start_index..end_index]
end
```
前兩行就是第十六天講過的,後三行就是第二十五天講過的。比較難懂的可能會是第三行跟第四行,先看一下這張圖:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0ou1oNmbu3Vsu744XKQ6rLhJwcb5HOUktAAbRDXY6UAAfZZg5dOb4hxcZBPh47j0gk6LL_ecyO7bRN8lW9ooM9vnoL61k3dPnOAdCwdmyoN0_gnz-a0j3PzYku3GlHlKE7o-DB9Ed_7U/s1600/5.jpg)
總而言之就是網址的開頭前面是 `","` 後面是 `"),` 如果你有學過 js 應該就會知道,這個開頭跟結尾應該是不會錯的,所以我們決定取出介於這中間的字。
這行是在抓起點:
```
start_index = response.index('","') + 3
```
這是在抓終點:
```
end_index = response.index('"),') - 1
```
# 增加一個上傳圖片到 imgur 的函數
```
def upload_to_imgur(image_url)
url = URI("https://api.imgur.com/3/image")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["authorization"] = 'Client-ID be2d83405627ab8'
request.set_form_data({"image" => image_url})
response = http.request(request)
json = JSON.parse(response.read_body)
begin
json['data']['link'].gsub("http:","https:")
rescue
nil
end
end
```
我們設定好 request header 和 request body 之後打一個 post request 出去,他會返回一個 json,接著我作了 json 的解析,並且在解析失敗時傳回 nil,確保程式不會隨意掛點。
```
request["authorization"] = 'Client-ID be2d83405627ab8'
```
這行是要填入你自己的 Client-ID,`be2d83405627ab8` 是我亂打的。
# 傳送圖片到 line 的函數
```
# 傳送圖片到 line
def reply_image_to_line(reply_image)
return nil if reply_image.nil?
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: "image",
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 & jQuery 前端開發入門實戰](https://5xruby.tw/talks/JS-jQuery-2018-1)
如果你原本是完全不會寫程式,你從第一篇一直看到這篇,最後有作出東西的話,請在底下留言:「感恩卡米,讚嘆卡米」,讓我能證明`只要有心,人人都可以作卡米狗`是真的。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-52637811242774302142018-01-17T22:12:00.000+08:002018-01-17T22:12:03.402+08:00Rails - Windows 上會遇到的 LoadError (cannot load such file -- bcrypt_ext) 問題# 什麼時候會遇到這個問題?
當你使用任何需要加密功能的套件時,比方說 Devise。
# 成因
安裝了不能在 windows 下正常執行的 bcrypt 套件。
# 解法
先解除安裝所有 bcrypt
```
gem uninstall bcrypt-ruby
gem uninstall bcrypt
```
再安裝正確版本
```
gem install bcrypt --platform=ruby
```
你的 Gemfile 應該加入這行
```
gem 'bcrypt', '~> 3.1.11'
```
# 參考連結
[https://github.com/codahale/bcrypt-ruby/issues/142#issuecomment-291345799](https://github.com/codahale/bcrypt-ruby/issues/142#issuecomment-291345799)卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-62824632088098198962018-01-17T02:07:00.003+08:002018-01-17T02:07:32.335+08:00第二十九天:卡米狗發公告今天我們要作的是主動傳訊息的功能。
目前我們用到的都只是回覆訊息的功能:
# 認識 Push Message API
```
# 傳送訊息到 line
def reply_to_line(reply_text)
return nil if reply_text.nil?
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
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 < 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` 所需要的全部程式碼:
```
<%= form_with(url: '/push_messages', local: true) do |form| %>
<%= text_area_tag 'text' %>
<%= submit_tag "送出" %>
<% end %>
```
一個表單的開始是 `<%= form_with ..... do ... %>`,結束是 `<% end %>`。表單預設是用 post 方法,所以就不用特別寫出來。
`<%= text_area_tag 'text' %>` 是輸入文字框。
`<%= submit_tag "送出" %>` 則是送出按鈕。
如果要了解更多的話可以參考:[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 '/push_messages/new'
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: 'text',
text: text
}
# 傳送訊息
line.push_message(channel_id, message)
end
```
長得跟之前的 `reply_to_line` 有 87% 像,就不解釋了。
### 對一下程式碼
完整的 `push_messages_controller.rb` 應該長這樣:
```
require 'line/bot'
class PushMessagesController < 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 '/push_messages/new'
end
# 傳送訊息到 line
def push_to_line(channel_id, text)
return nil if channel_id.nil? or text.nil?
# 設定回覆訊息
message = {
type: 'text',
text: text
}
# 傳送訊息
line.push_message(channel_id, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
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 的表單
沒意外的話,明天就講怎麼查天氣。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-41838399014233473132018-01-16T00:32:00.000+08:002018-01-16T00:32:05.387+08:00第二十八天:建立管理後台應觀眾要求,今天我們作一個管理後台,讓我們可以在網頁上管理關鍵字。
在開始之前,先大概說明一下今天要學習的範圍有哪些:
- 網頁的呈現需要使用 HTML 和 CSS
- 既然是後台,就要作登入功能
我們作的網站到目前為止沒有碰過任何的 HTML 和 CSS,突然要寫個管理後台也許會很吃力。不過還好是作後台,不需要多美觀。
# 使用產生器製作後台
幸好 Rails 有一個內建指令直接生成網頁,不一定要自己寫。
指令是 `rails generate scaffold 資料模型名稱 和欄位們`
```
rails g scaffold keyword_mapping channel_id keyword message --skip
```
後面的 `--skip` 是指定當發生衝突時應該略過。衝突的意思是指 rails 想新增一個檔案,剛好在目錄裡已經有個同名的檔案。
```
D:\只要有心,人人都可以作卡米狗\ironman>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>
```
以下說明到底生成了什麼東西。
# 生成 Routes
他會在 `config/routes.rb` 生成一個 resource:
```
resources :keyword_mappings
```
這是資源(resource),提供一個資源的存取所需要的網址和 Controller 的對應。
這行會生成 8 組網址與 7 個 Controller Action 的對應,可以使用 `rails routes` 觀察:
```
D:\只要有心,人人都可以作卡米狗\ironman>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 'devise'
```
然後在小黑框打 `bundle install` 安裝套件。
裝好之後使用 devise 提供的產生器指令進行初始化: `rails generate devise:install`。
```
D:\只要有心,人人都可以作卡米狗\ironman>rails generate devise:install
create config/initializers/devise.rb
create config/locales/devise.en.yml
===============================================================================
Some setup you must do manually if you haven'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: 'localhost', 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: "home#index"
3. Ensure you have flash messages in app/views/layouts/application.html.erb.
For example:
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
4. You can copy Devise views (for customization) to your app by running:
rails g devise:views
===============================================================================
D:\只要有心,人人都可以作卡米狗\ironman>
```
他說有幾個步驟產生器沒搞頭,必須手動進行。我們先不管他,等到真正出問題再回頭來解決。
使用產生器產生用戶資料模型:
```
rails generate devise user
```
```
D:\只要有心,人人都可以作卡米狗\ironman>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>
```
跟剛剛的 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 < 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 < 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 < 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>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>
```
寫一行程式碼新增資料:
```
User.create(email:'kamigo.service@gmail.com', password:'kamigo')
```
會有一些 SQL 的訊息:
```
irb(main):001:0> User.create(email:'kamigo.service@gmail.com', password:'kamigo')
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 "users" WHERE "users"."email" = $1 LIMIT $2 [["email", "kamigo.service@gmail.com"], ["LIMIT", 1]]
D, [2018-01-15T15:42:28.317361 #4] DEBUG -- : SQL (1.9ms) INSERT INTO "users" ("email", "encrypted_password", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["email", "kamigo.service@gmail.com"], ["encrypted_password", "$2a$11$EyR.yuDYI3J2s9/Q8Etk5evQzsz2bGAPdvdcr.xmFQbzYbBPQk/kK"], ["created_at", "2018-01-15 15:42:28.313883"], ["updated_at", "2018-01-15 15:42:28.313883"]]
D, [2018-01-15T15:42:28.320139 #4] DEBUG -- : (2.0ms) COMMIT
=> #<User id: 1, email: "kamigo.service@gmail.com", created_at: "2018-01-15 15:42:28", updated_at: "2018-01-15 15:42:28">
irb(main):002:0>
```
看到倒數第二行:
```
=> #<User id: 1, email: "kamigo.service@gmail.com", created_at: "2018-01-15 15:42:28", updated_at: "2018-01-15 15:42:28">
```
就表示建立好帳號了。
# 線上實測
[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 個字而已。
明天講怎麼發公告。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-63187954754376178422018-01-15T17:04:00.001+08:002018-01-15T17:38:36.241+08:00Rails - 多檔上傳假設有一個 Controller 叫做 `imgur`,大概是這樣:
```
rails g scaffold imgur pictures
```
而 Controller 內的 params 只允許傳遞 pictures 陣列:
```
def imgur_params
params.require(:imgur).permit(pictures: [])
end
```
一個最基本的檔案上傳表單長這樣:
```
<%= form_for(imgur) do |f| %>
<%= f.file_field :pictures %>
<%= f.submit %>
<% end %>
```
會因為 pictures 只吃陣列的關係,就傳不進去。如果想要上傳多個檔案,要加上 `multiple: true`:
```
<%= form_for(imgur) do |f| %>
<%= f.file_field :pictures, multiple: true %>
<%= f.submit %>
<% end %>
```
可以使用多個 f.file_field 來做上傳:
```
<%= form_for(imgur) do |f| %>
<%= f.file_field :pictures, multiple: true %>
<%= f.file_field :pictures, multiple: true %>
<%= f.submit %>
<% end %>
```
如果想要改用 `file_field_tag` 的話,就必須在 `form_for` 加上 `multipart: true`:
```
<%= form_for(imgur, html: { multipart: true }) do |f| %>
<%= file_field_tag "imgur[pictures][]" %>
<%= f.submit %>
<% end %>
```
加上 [] 之後,即使只傳一個檔也能通過 `params.permit`。
如果想要多檔上傳:
```
<%= form_for(imgur, html: { multipart: true }) do |f| %>
<%= file_field_tag "imgur[pictures][]", multiple: true %>
<%= f.submit %>
<% end %>
```
也可以這樣:
```
<%= form_for(imgur, html: { multipart: true }) do |f| %>
<%= file_field_tag "imgur[pictures][]" %>
<%= file_field_tag "imgur[pictures][]" %>
<%= f.submit %>
<% end %>
```
或者這樣:
```
<%= form_for(imgur, html: { multipart: true }) do |f| %>
<%= file_field_tag "imgur[pictures][]", multiple: true %>
<%= file_field_tag "imgur[pictures][]", multiple: true %>
<%= f.submit %>
<% end %>
```
卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-45908340307596462972018-01-15T01:43:00.000+08:002018-01-15T01:43:40.583+08:00第二十七天:卡米狗見人說人話,見鬼說鬼話在[第二天:認識卡米狗](https://ithelp.ithome.com.tw/articles/10192575)提到過,見人說人話,見鬼說鬼話功能是考慮到多個群組都教了相同的關鍵字時,卡米狗應該在每個群組做出不同的回應,這樣才不會被討厭,於是就加入了這樣的功能。當有人說「姆咪姆咪」時,卡米狗會先檢查這個群組有沒有人教過看到「姆咪姆咪」要回應,如果教過多次,就回應最後一次學過的內容,如果都沒學過,那麼就再檢查其他群組有沒有學過「姆咪姆咪」。
也就是說,學說話指令在儲存時,應該也要儲存是在哪個頻道學會的。
# 修改學說話指令
目前的學說話指令:
```
# 學說話
def learn(received_text)
#如果開頭不是 卡米狗學說話; 就跳出
return nil unless received_text[0..6] == '卡米狗學說話;'
received_text = received_text[7..-1]
semicolon_index = received_text.index(';')
# 找不到分號就跳出
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)
'好哦~好哦~'
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&.message
end
```
則是改為:
```
# 關鍵字回覆
def keyword_reply(channel_id, received_text)
message = KeywordMapping.where(channel_id: channel_id, keyword: received_text).last&.message
return message unless message.nil?
KeywordMapping.where(keyword: received_text).last&.message
end
```
多加了這兩行:
```
message = KeywordMapping.where(channel_id: channel_id, keyword: received_text).last&.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>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>
```
生成了一個檔案在 `db/migrate` 裡面,我們要在這個資料庫遷移檔裡打一點字,這是他目前的樣子:
```
class AddChannelIdToKeywordReply < ActiveRecord::Migration[5.1]
def change
end
end
```
要加一個欄位的話,要這樣寫:
```
class AddChannelIdToKeywordReply < 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>rails db:migrate
== 20180114163555 AddChannelIdToKeywordReply: migrating =======================
-- add_column(:keyword_mappings, :channel_id, :string)
-> 0.0012s
== 20180114163555 AddChannelIdToKeywordReply: migrated (0.0020s) ==============
D:\只要有心,人人都可以作卡米狗\ironman>
```
如果資料庫遷移檔沒打錯字的話,就會看到這個結果。
# 進行實測
首先上傳程式碼,要養成開著 `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 "/kamigo/webhook" 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: {"events"=>[{"type"=>"message", "replyToken"=>"bffeaf21d2b64743b3268bd177ebbaff", "source"=>{"userId"=>"Uc68d82df46b7899e7d716f396ae8e91a", "type"=>"user"}, "timestamp"=>1515948996430, "message"=>{"type"=>"text", "id"=>"7310568889858", "text"=>"A"}}], "kamigo"=>{"events"=>[{"type"=>"message", "replyToken"=>"bffeaf21d2b64743b3268bd177ebbaff", "source"=>{"userId"=>"Uc68d82df46b7899e7d716f396ae8e91a", "type"=>"user"}, "timestamp"=>1515948996430, "message"=>{"type"=>"text", "id"=>"7310568889858", "text"=>"A"}}]}}
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'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 "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."channel_id" = $1 AND "keyword_mappings"."keyword" = $2 ORDER BY "keyword_mappings"."id" DESC LIMIT $3 [["channel_id", "Uc68d82df46b7899e7d716f396ae8e91a"], ["keyword", "A"], ["LIMIT", 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".* FROM "keyword_mappings" WHERE "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 "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."channel_id" = $1 AND "keyword_mappings"."keyword" = $2 ORDER BY "keyword_mappings"."id" 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'
2018-01-14T16:56:36.927702+00:00 app[web.1]: [2a0784f2-c2b7-46c1-818e-5e5dd799e64c] app/controllers/kamigo_controller.rb:10:in `webhook'
2018-01-14T16:56:36.929306+00:00 heroku[router]: at=info method=POST path="/kamigo/webhook" host=people-all-love-kamigo.herokuapp.com request_id=2a0784f2-c2b7-46c1-818e-5e5dd799e64c fwd="203.104.146.154" dyno=web.1 connect=0ms service=11ms status=500 bytes=1827 protocol=https
```
我先把前面那些多餘的字移除:
```
Started POST "/kamigo/webhook" for 203.104.146.154 at 2018-01-14 16:56:36 +0000
Processing by KamigoController#webhook as */*
Parameters: {"events"=>[{"type"=>"message", "replyToken"=>"bffeaf21d2b64743b3268bd177ebbaff", "source"=>{"userId"=>"Uc68d82df46b7899e7d716f396ae8e91a", "type"=>"user"}, "timestamp"=>1515948996430, "message"=>{"type"=>"text", "id"=>"7310568889858", "text"=>"A"}}], "kamigo"=>{"events"=>[{"type"=>"message", "replyToken"=>"bffeaf21d2b64743b3268bd177ebbaff", "source"=>{"userId"=>"Uc68d82df46b7899e7d716f396ae8e91a", "type"=>"user"}, "timestamp"=>1515948996430, "message"=>{"type"=>"text", "id"=>"7310568889858", "text"=>"A"}}]}}
Can't verify CSRF token authenticity.
KeywordMapping Load (1.6ms) SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."channel_id" = $1 AND "keyword_mappings"."keyword" = $2 ORDER BY "keyword_mappings"."id" DESC LIMIT $3 [["channel_id", "Uc68d82df46b7899e7d716f396ae8e91a"], ["keyword", "A"], ["LIMIT", 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".* FROM "keyword_mappings" WHERE "keyword_m...
^
SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."channel_id" = $1 AND "keyword_mappings"."keyword" = $2 ORDER BY "keyword_mappings"."id" DESC LIMIT $3):
app/controllers/kamigo_controller.rb:82:in `keyword_reply'
app/controllers/kamigo_controller.rb:10:in `webhook'
at=info method=POST path="/kamigo/webhook" host=people-all-love-kamigo.herokuapp.com request_id=2a0784f2-c2b7-46c1-818e-5e5dd799e64c fwd="203.104.146.154" 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".* FROM "keyword_mappings" WHERE "keyword_m...
^
SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."channel_id" = $1 AND "keyword_mappings"."keyword" = $2 ORDER BY "keyword_mappings"."id" DESC LIMIT $3):
app/controllers/kamigo_controller.rb:82:in `keyword_reply'
app/controllers/kamigo_controller.rb:10:in `webhook'
```
他說:`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'
app/controllers/kamigo_controller.rb:10:in `webhook'
```
這個叫做 `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 上除錯的方法卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-42088921801486403922018-01-14T01:51:00.000+08:002018-01-14T01:55:55.068+08:00第二十六天:卡米狗推齊今天要作的是卡米狗的推齊功能,也就是當看到有兩次以上有人說出相同的句子,那麼就跟著說的功能。要作到這件事,卡米狗必須要有一點記性才行,所以我們必須記錄每個群組中所發生的對話。當有人說出一句話時,就檢查最近有沒有人也說出相同的話,如果有的話卡米狗就跟著說。
# 使用情境
我們希望的是這樣:
```
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>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>
```
建立 `reply` 資料模型:`rails generate model reply channel_id text`
```
D:\只要有心,人人都可以作卡米狗\ironman>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>
```
進行資料庫遷移:`rails db:migrate`
```
D:\只要有心,人人都可以作卡米狗\ironman>rails db:migrate
== 20180113153959 CreateReceiveds: migrating ==================================
-- create_table(:receiveds)
-> 0.5013s
== 20180113153959 CreateReceiveds: migrated (0.5027s) =========================
== 20180113154217 CreateReplies: migrating ====================================
-- create_table(:replies)
-> 0.0013s
== 20180113154217 CreateReplies: migrated (0.0024s) ===========================
D:\只要有心,人人都可以作卡米狗\ironman>
```
# 頻道 ID
根據 [Line Messaging API 的文件](https://developers.line.me/en/docs/messaging-api/reference/#common-properties),我們知道要從 `params['events'][0]['source']` 底下去找 `groupId`、`roomId` 或者是 `userId`。
如果對話是發生在群組,`groupId` 就會有值,如果對話是發生在聊天室,`roomId` 就會有值。
所以我們要這樣寫:
```
# 頻道 ID
def channel_id
source = params['events'][0]['source']
return source['groupId'] unless source['groupId'].nil?
return source['roomId'] unless source['roomId'].nil?
source['userId']
end
```
可以浪漫一點:
```
# 頻道 ID
def channel_id
source = params['events'][0]['source']
source['groupId'] || source['roomId'] || source['userId']
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)&.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&.text
return nil if last_reply_text == received_text
received_text
end
```
# 發布
### 對一下程式碼
```
require 'line/bot'
class KamigoController < 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['events'][0]['source']
source['groupId'] || source['roomId'] || source['userId']
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)&.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&.text
return nil if last_reply_text == received_text
received_text
end
# 取得對方說的話
def received_text
message = params['events'][0]['message']
message['text'] unless message.nil?
end
# 學說話
def learn(received_text)
#如果開頭不是 卡米狗學說話; 就跳出
return nil unless received_text[0..6] == '卡米狗學說話;'
received_text = received_text[7..-1]
semicolon_index = received_text.index(';')
# 找不到分號就跳出
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)
'好哦~好哦~'
end
# 關鍵字回覆
def keyword_reply(received_text)
KeywordMapping.where(keyword: received_text).last&.message
end
# 傳送訊息到 line
def reply_to_line(reply_text)
return nil if reply_text.nil?
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
def eat
render plain: "吃土啦"
end
def request_headers
render plain: request.headers.to_h.reject{ |key, value|
key.include? '.'
}.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def response_headers
response.headers['5566'] = 'QQ'
render plain: response.headers.to_h.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def request_body
render plain: request.body
end
def show_response_body
puts "===這是設定前的response.body:#{response.body}==="
render plain: "虎哇花哈哈哈"
puts "===這是設定後的response.body:#{response.body}==="
end
def sent_request
uri = URI('http://localhost:3000/kamigo/eat')
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)
"#{message}油~"
end
end
```
上傳程式碼囉~
### Heroku 上的資料庫遷移
要在上傳程式碼之後才能作資料庫遷移,因為資料庫遷移需要讀取資料庫遷移檔。Heroku 上的資料庫遷移指令是 `heroku run rake db:migrate`:
```
D:\只要有心,人人都可以作卡米狗\ironman>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 "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" 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 "receiveds" ("id" bigserial primary key, "channel_id" character varying, "text" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
-> 0.0463s
== 20180113153959 CreateReceiveds: migrated (0.0464s) =========================
D, [2018-01-13T16:57:33.170513 #4] DEBUG -- : SQL (0.7ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20180113153959"]]
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 "replies" ("id" bigserial primary key, "channel_id" character varying, "text" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
-> 0.0093s
== 20180113154217 CreateReplies: migrated (0.0093s) ===========================
D, [2018-01-13T16:57:33.185682 #4] DEBUG -- : SQL (0.6ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20180113154217"]]
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 "ar_internal_metadata".* FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = $1 LIMIT $2 [["key", "environment"], ["LIMIT", 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>
```
# 實測
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg85FHEL7CYrIQrode333JWYgEkIqrljuU_bdxvPZ4FyM_PXx5ud5lD1xy-qmDB63OolLgdwSTNcYoIyulCYuil9xK2Wp123dCUWMYQ8-phyWxxmNvpWmlMQV-cCJYIT9Y8HmwAKEcttC0/s1600/1.jpg)
成功!
失敗的在底下留言,謝謝。
# 本日重點
- 學會了判斷目前的頻道
- 學會了如何根據前後文作出不同的回應
# 接下來
剩沒幾天了,還有很多可以學的,我想知道你們比較想學些什麼?
接下來我們還可以做的事情有這些:
- 現在的程式有點亂了,而且還有些問題,需要整理
- 讓卡米狗的關鍵字回應能根據目前頻道,作出不同的回應
- 讓卡米狗能抽籤
- 讓卡米狗能擷取用戶的使用者名稱以及大頭貼
- 讓卡米狗能接收及傳送貼圖
- 讓卡米狗能接收及傳送圖片
- 讓卡米狗能傳送含有按鈕的選單
- 讓卡米狗能查天氣
- 打造一個管理後台
- 讓卡米狗能發公告
- 製作小遊戲,比方說井字遊戲
或者你有想到,但上面沒列出來的也可以。
請在本文留言讓我知道你想學些什麼,可複選。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-81461177453768797332018-01-13T03:15:00.000+08:002018-01-14T22:51:50.270+08:00第二十五天:卡米狗學說話卡米狗的學說話指令,最早期的語法設計是`卡米狗學說話;關鍵字;回覆`。用兩個半形分號作為分隔符號。為什麼選擇用分號作為分隔符號呢?因為我們的分隔符號不能出現在關鍵字或回覆內,所以要挑一個比較少人用的符號。
我們必須讓學說話功能的優先順序高於關鍵字回覆,這樣才能確保學說話指令不會被關鍵字覆蓋。
# 修改主程式
主程式:
```
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` 是空值的話才進行關鍵字回覆的判斷。這樣就能確保學說話指令優先於關鍵字回覆。
我們要判斷輸入的文字開頭是不是`卡米狗學說話;`,要做到這件事情,我們需要學一點字串操作。
# 字串操作
這是一個字串:
```
'ABCDEF'
=> "ABCDEF"
```
### 字串的切割
這是字串的第一個字:
```
'ABCDEF'[0]
=> "A"
```
這是字串的第二個字:
```
'ABCDEF'[1]
=> "B"
```
以此類推,
A,B,C,D,E,F
分別對應到:
0,1,2,3,4,5
這是字串的倒數第一個字
```
'ABCDEF'[-1]
=> "F"
```
這是字串的倒數第二個字
```
'ABCDEF'[-2]
=> "E"
```
以此類推,
F,E,D,C,B,A
分別對應到:
-1,-2,-3,-4,-5,-6
另外,你可以透過 Range 取得一個區間。
```
'ABCDEF'[0..1]
=> "AB"
```
```
'ABCDEF'[0..3]
=> "ABCD"
```
```
'ABCDEF'[3..-1]
=> "DEF"
```
# 字串的查詢
想知道字串中的 `A` 出現在哪裡:
```
'ABCDEF'.index('A')
=> 0
```
想知道字串中的 `B` 出現在哪裡:
```
'ABCDEF'.index('B')
=> 1
```
想知道字串中的 `C` 出現在哪裡:
```
'ABCDEF'.index('C')
=> 2
```
找不到的情形會傳回 `nil`:
```
'ABCDEF'.index('G')
=> nil
```
# 字串的相等
判斷兩個字串是否相等:
```
'A' == 'A'
=> true
```
```
'卡米狗學說話' == '卡米狗學說話'
=> true
```
```
'A' == 'B'
=> false
```
學會以上三個技巧,就能夠解決大部分的問題。
# 學說話
現在我們要開始寫學說話函數,從空函數開始。
```
# 學說話
def learn(received_text)
end
```
利用以上三個技巧,我們可以先取得前面七個字,看看是不是等於`卡米狗學說話;`,如果是的話,在後面的字串中找到分號作為分隔點。
先檢查開頭的字是不是`卡米狗學說話;`:
```
def learn(received_text)
#如果開頭不是 卡米狗學說話; 就跳出
return nil unless received_text[0..6] == '卡米狗學說話;'
end
```
`unless` 是 `if` 的相反,`unless` 是`除非`的意思。
除非前面七個字是`卡米狗學說話;`,不然就傳回 `nil`。
再來就是取得剩下來的字,以及找到第二個分號。
```
# 學說話
def learn(received_text)
#如果開頭不是 卡米狗學說話; 就跳出
return nil unless received_text[0..6] == '卡米狗學說話;'
received_text = received_text[7..-1]
semicolon_index = received_text.index(';')
# 找不到分號就跳出
return nil if semicolon_index.nil?
end
```
因為前面七個字已經沒有用了,所以我們抓出第八個字到最後一個字。然後在剩下的字裡面找到分號的位置。如果找不到分號,就跳出。
接下來我們要根據分隔點,擷取出關鍵字以及回覆,並且新增到資料庫裡。
```
# 學說話
def learn(received_text)
#如果開頭不是 卡米狗學說話; 就跳出
return nil unless received_text[0..6] == '卡米狗學說話;'
received_text = received_text[7..-1]
semicolon_index = received_text.index(';')
# 找不到分號就跳出
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)
'好哦~好哦~'
end
```
這就是完整的學說話指令。接著我們要修改關鍵字回覆。
# 關鍵字回覆
這是原本的關鍵字回覆:
```
# 關鍵字回覆
def keyword_reply(received_text)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
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&.message
end
```
如果 `&.` 的前面是 `nil`,那他就不會做後面的事,直接傳回 `nil`。
到這裡算是開發完成,可以上傳程式碼了。
# 對一下程式碼
你的程式碼應該長得差不多像這樣:
```
require 'line/bot'
class KamigoController < 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['events'][0]['message']
message['text'] unless message.nil?
end
# 學說話
def learn(received_text)
#如果開頭不是 卡米狗學說話; 就跳出
return nil unless received_text[0..6] == '卡米狗學說話;'
received_text = received_text[7..-1]
semicolon_index = received_text.index(';')
# 找不到分號就跳出
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)
'好哦~好哦~'
end
# 關鍵字回覆
def keyword_reply(received_text)
KeywordMapping.where(keyword: received_text).last&.message
end
# 傳送訊息到 line
def reply_to_line(reply_text)
return nil if reply_text.nil?
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
def eat
render plain: "吃土啦"
end
def request_headers
render plain: request.headers.to_h.reject{ |key, value|
key.include? '.'
}.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def response_headers
response.headers['5566'] = 'QQ'
render plain: response.headers.to_h.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def request_body
render plain: request.body
end
def show_response_body
puts "===這是設定前的response.body:#{response.body}==="
render plain: "虎哇花哈哈哈"
puts "===這是設定後的response.body:#{response.body}==="
end
def sent_request
uri = URI('http://localhost:3000/kamigo/eat')
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)
"#{message}油~"
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': Specified 'postgresql' for database adapter, but the gem is not loaded. Add `gem 'pg'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord). (Gem::LoadError)
```
請將你的 Gemfile 修改一下,原本是:
```
group :development, :test do
gem 'sqlite3'
end
group :production do
gem 'pg'
end
```
改為
```
group :development, :test do
gem 'sqlite3'
end
group :production do
gem 'pg', '~> 0.21.0'
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>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>
```
# 進行在 Heroku 上的資料庫遷移
在我們的小黑框輸入 `heroku run rake db:migrate`:
```
D:\只要有心,人人都可以作卡米狗\ironman>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 "schema_migrations" ("version" character varying NOT NULL PRIMARY KEY)
D, [2018-01-12T18:43:38.188458 #4] DEBUG -- : (491.4ms) CREATE TABLE "ar_internal_metadata" ("key" character varying NOT NULL PRIMARY KEY, "value" character varying, "created_at" timestamp NOT NULL, "updated_at" 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 "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" 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 "keyword_mappings" ("id" bigserial primary key, "keyword" character varying, "message" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
-> 0.8170s
== 20180110181744 CreateKeywordMappings: migrated (0.8174s) ===================
D, [2018-01-12T18:43:39.853181 #4] DEBUG -- : SQL (6.3ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20180110181744"]]
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 "ar_internal_metadata".* FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = $1 LIMIT $2 [["key", "environment"], ["LIMIT", 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 "ar_internal_metadata" ("key", "value", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "key" [["key", "environment"], ["value", "production"], ["created_at", "2018-01-12 18:43:39.897705"], ["updated_at", "2018-01-12 18:43:39.897705"]]
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>
```
# 進行實測
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjCC42Z8ZcN5KoIt5415XPVjw94yXDjb6-IwVnTVXVf6AbFQx6QY_ZDK1oVs-RAKsF9eWf5ryyuEphUJ0yEcB_8ClWwrM8PMMSVdYTt-uGD4gNSPiuUOnaJHPlVPVxYAltuV8GzJNgL3NE/s1600/1.jpg)
順利~不順利的人請在底下留言並附上不順利的截圖,謝謝。
# 本日重點
- 學習了字串操作
- 學習了 Heroku 上的資料庫建置
- 做出了學說話和觸發說話
明天講推齊功能。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-40640380867637015492018-01-11T23:22:00.000+08:002018-01-12T00:17:14.376+08:00第二十四天:認識資料庫(續)昨天我們講到資料模型產生器的用法:
```
rails generate model keyword_mapping keyword message
```
會產生兩個我們需要的檔案:
- 資料庫遷移檔:`db/migrate/20180110181744_create_keyword_mappings.rb`
- 資料模型:`app/models/keyword_mapping.rb`
其中,資料庫遷移檔就像是一張<strike>對資料庫施法的卷軸</strike>設計圖,可以用來幫資料庫升級。
那要怎麼升級呢?
# 資料庫遷移
使用 `rails db:migrate` 指令就會進行資料庫升級。
```
D:\只要有心,人人都可以作卡米狗\ironman>rails db:migrate
== 20180110181744 CreateKeywordMappings: migrating ============================
-- create_table(:keyword_mappings)
-> 0.4916s
== 20180110181744 CreateKeywordMappings: migrated (0.4927s) ===================
D:\只要有心,人人都可以作卡米狗\ironman>
```
還可以用 `rails db:rollback` 降級:
```
D:\只要有心,人人都可以作卡米狗\ironman>rails db:rollback
== 20180110181744 CreateKeywordMappings: reverting ============================
-- drop_table(:keyword_mappings)
-> 0.5057s
== 20180110181744 CreateKeywordMappings: reverted (0.5211s) ===================
D:\只要有心,人人都可以作卡米狗\ironman>
```
可以用 `rails db:migrate:status` 查看目前等級。這是升級前:
```
D:\只要有心,人人都可以作卡米狗\ironman>rails db:migrate:status
database: D:/只要有心,人人都可以作卡米狗/ironman/db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
down 20180110181744 Create keyword mappings
D:\只要有心,人人都可以作卡米狗\ironman>
```
這是升級後:
```
D:\只要有心,人人都可以作卡米狗\ironman>rails db:migrate:status
database: D:/只要有心,人人都可以作卡米狗/ironman/db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
up 20180110181744 Create keyword mappings
D:\只要有心,人人都可以作卡米狗\ironman>
```
資料庫已就緒,接下來就只等我們把學習紀錄寫入了。接下來我會試著用 [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>rails console
Loading development environment (Rails 5.1.4)
irb(main):001:0>
```
### 列出所有資料
我們的資料模型叫做 `KeywordMapping`。可以用 `.all` 將它顯示出來看看:
```
irb(main):001:0> KeywordMapping.all
KeywordMapping Load (2.5ms) SELECT "keyword_mappings".* FROM "keyword_mappings" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
irb(main):002:0>
```
這一段:
```
SELECT "keyword_mappings".* FROM "keyword_mappings" LIMIT ?
```
是我們對資料庫進行查詢的 SQL 語法,幸好你不需要學會這個,就當作沒看到吧。
而這一段:
```
=> #<ActiveRecord::Relation []>
```
是指 `KeywordMapping.all` 是一個 `ActiveRecord::Relation` 類別的實體, `[]` 表示它是空的。
讓我們弄點東西進去。
### 新增資料
用 `KeywordMapping.new` 可以獲得一筆新的空白資料。
```
irb(main):002:0> new_data = KeywordMapping.new
=> #<KeywordMapping id: nil, keyword: nil, message: nil, created_at: nil, updated_at: nil>
irb(main):003:0>
```
用一個變數 `new_data` 去接住它,因為我們接下來要對他做事。
```
irb(main):003:0> new_data.keyword = "Q"
=> "Q"
```
設定 new_data 的 keyword 是 "Q"。
```
irb(main):004:0> new_data.message = "A"
=> "A"
irb(main):005:0>
```
設定 new_data 的 message 是 "A"。
都設定好之後用 `new_data.save` 來存檔。
```
irb(main):005:0> new_data.save
(0.0ms) begin transaction
SQL (494.1ms) INSERT INTO "keyword_mappings" ("keyword", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["keyword", "Q"], ["message", "A"], ["created_at", "2018-01-11 14:30:55.567172"], ["updated_at", "2018-01-11 14:30:55.567172"]]
(52.5ms) commit transaction
=> true
irb(main):006:0>
```
一樣,就是一堆不需要看懂的 SQL。現在使用 `KeywordMapping.all` 就能看到新加入的資料了。
目前資料表的狀態:
```
irb(main):006:0> KeywordMapping.all
KeywordMapping Load (0.0ms) SELECT "keyword_mappings".* FROM "keyword_mappings" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<KeywordMapping id: 1, keyword: "Q", message: "A", created_at: "2018-01-11 14:30:55", updated_at: "2018-01-11 14:30:55">]>
irb(main):007:0>
```
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkE0jCHbZillsWuf2MYvDXyzLpJGKfqt6Ow49-QI2SjGA7FlpbcIaRR93ijS24lMv6VPzWfx_ft4pvSDlfRJxnls2W0PrwZxjY1-ri1kHGCgLn_2RoXXrOJDJAo6P0l7oldkNRdbxiaYw/s1600/2.jpg)
新增資料也有簡寫的方式可以一行做完,指令是 `KeywordMapping.create({keyword:"Q2", message:"A2"})`:
```
irb(main):007:0> KeywordMapping.create(keyword:"Q2", message:"A2")
(0.0ms) begin transaction
SQL (485.4ms) INSERT INTO "keyword_mappings" ("keyword", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["keyword", "Q2"], ["message", "A2"], ["created_at", "2018-01-11 14:36:34.858893"], ["updated_at", "2018-01-11 14:36:34.858893"]]
(48.5ms) commit transaction
=> #<KeywordMapping id: 2, keyword: "Q2", message: "A2", created_at: "2018-01-11 14:36:34", updated_at: "2018-01-11 14:36:34">
irb(main):008:0>
```
這是傳入一個 hash 作為設定,用 `create` 方法的話就會自動 `save`,所以就不用自己再打 `save` 了。
目前資料表的狀態:
```
irb(main):009:0> KeywordMapping.all
KeywordMapping Load (0.0ms) SELECT "keyword_mappings".* FROM "keyword_mappings" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<KeywordMapping id: 1, keyword: "Q", message: "A", created_at: "2018-01-11 14:30:55", updated_at: "2018-01-11 14:30:55">, #<KeywordMapping id: 2, keyword: "Q2", message: "A2", created_at: "2018-01-11 14:36:34", updated_at: "2018-01-11 14:36:34">]>
irb(main):010:0>
```
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaVO4FJfMRkqihoMfkBGqYh8EjY_lJhNh7gL_zR9UTh3aZbIyVu7G5y7AHrF7DStxx0R2w62vcR2szqN822SCYNe6v3UjUsBJRRpkzMoGmaRImwEwIb2wEsr2UH6zu-PUO08Ua-FeoH-A/s1600/3.jpg)
### 查詢資料
我們通常不會想要拿出整個資料表,而是只想要查當中的一筆,這時候就要用 `where` 方法,以下示範 `KeywordMapping.where(keyword:"Q2")`。
```
irb(main):008:0> KeywordMapping.where(keyword:"Q2")
KeywordMapping Load (0.5ms) SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."keyword" = ? LIMIT ? [["keyword", "Q2"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<KeywordMapping id: 2, keyword: "Q2", message: "A2", created_at: "2018-01-11 14:36:34", updated_at: "2018-01-11 14:36:34">]>
irb(main):009:0>
```
這是篩選功能,我們對 `keyword` 欄位做 `Q2` 篩選,在 Google 試算表按照順序點就可以達到相同的效果。
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhUURzgRjIKIolm51sGciqE7YjmnesYZBadibFRZS9v3lfm-DtqbNBD3RthlmOtSng6bNfNnx3Zf_ok5KWGBSXxG8VLlZ096Y6aUG54Vprsiw4pu4iDns-Sc4RSIqtpJCR6g6gWr1AB_-E/s1600/4.jpg)
篩選出來可能會有多筆,我們可以 `.all` 取得全部或者 `.first` 取第一筆,或 `.last` 取最後一筆。
所以卡米狗觸發教學的寫法是這樣:`KeywordMapping.where(keyword:"Q2").last.message`,對 `keyword` 欄位做篩選,找到最後一次教學紀錄,然後取出 `message` 欄位的內容。
```
irb(main):012:0* KeywordMapping.where(keyword:"Q2").last.message
KeywordMapping Load (0.5ms) SELECT "keyword_mappings".* FROM "keyword_mappings" WHERE "keyword_mappings"."keyword" = ? ORDER BY "keyword_mappings"."id" DESC LIMIT ? [["keyword", "Q2"], ["LIMIT", 1]]
=> "A2"
irb(main):013:0>
```
丟 Q2 進去資料庫查,查到 A2 再回應給 Line。
# 本日重點
- 學會使用資料庫遷移
- 學會使用資料模型
- 了解卡米狗觸發教學的原理
明天會講怎麼用這兩天學到的東西做出卡米狗學習指令。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-90865246624474615692018-01-11T02:44:00.001+08:002018-01-15T11:22:54.865+08:00第二十三天:認識資料庫我們預計下一個要完成的功能是教學指令。
這是昨天的關鍵字回覆:
```
# 關鍵字回覆
def keyword_reply(received_text)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
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 'sqlite3'
#
default: &default
adapter: postgresql
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: db/test.sqlite3
production:
<<: *default
database: db/production.sqlite3
```
要改成這樣:
```
# SQLite version 3.x
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
adapter: postgresql
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
adapter: sqlite3
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
adapter: sqlite3
database: db/test.sqlite3
production:
<<: *default
database: ironman
```
先解釋一下,這裡有四段程式(其實不是程式,是設定檔)。
這是第一段:
```
default: &default
adapter: postgresql
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
```
這是作為一個預設值。
- adapter:要採用哪一套資料庫,預設使用 `postgresql`
- pool:是同時連線數量,可以理解成頻寬
- timeout:超過 5000 毫秒資料庫還不回應的話就當作逾時
```
development:
<<: *default
adapter: sqlite3
database: db/development.sqlite3
```
這段是開發環境,也就是我們的電腦。
- <<: *default:採用預設值
- adapter:要採用哪一套資料庫,這裡是使用 `sqlite3`,會把預設值覆蓋掉
- database:資料庫的儲存位置,`db/development.sqlite3` 這是一個路徑,你可以在專案資料夾裡面找到這個位置
```
test:
<<: *default
adapter: sqlite3
database: db/test.sqlite3
```
這段是測試環境,目前我們沒有使用測試環境,所以這裡跳過不講。
```
production:
<<: *default
database: ironman
```
這段是發布環境或正式環境,也就是 heroku 上。
- database:使用 `postgresql` 的話就不是儲存位置了,而是資料庫的名稱,不過概念上差不多。和 `sqlite3` 的差別是你不會在專案資料夾裡頭找到資料庫的實體檔案。
# 設定 Gemfile
因為我們要在 development 環境下使用 `sqlite3`,production 環境下使用 `postgresql`,所以 Gemfile 也要改寫,這是原本的 Gemfile:
```
gem 'pg', '~> 0.21.0'
```
要改寫為
```
group :development, :test do
gem 'sqlite3'
end
group :production do
gem 'pg', '~> 0.21.0'
end
```
意思就是在 development 和 test 環境下要使用 `sqlite3`,而在 production 環境下使用 `postgresql`。
# 建立資料庫
在小黑框輸入:
```
rails db:create
```
就可以得到一個空白的資料庫,你可以觀察它會出現在專案資料夾下的 `db` 資料夾下。
# 建立資料表
在小黑框輸入:
```
rails generate model keyword_mapping keyword message
```
會看到:
```
D:\只要有心,人人都可以作卡米狗\ironman>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>
```
表示有四個檔案被生成了,分別是:
- 資料庫遷移檔:`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 < 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 < ApplicationRecord
end
```
空的,因為它用繼承 (`<`),其實有很多東西是藏在 ApplicationRecord 裡面。
所以我們剛剛輸入的指令是這樣:
```
rails generate model keyword_mapping keyword message
```
意思是我要生成一個資料模型和資料庫遷移檔,資料表名稱為 `keyword_mapping`,包含兩個欄位分別是 `keyword` 和 `message`。
今天先講到這裡。
資料庫博大精深,卡米狗不是一天造成的,要有耐心。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-12161305905464938202018-01-10T16:17:00.001+08:002018-01-10T16:45:30.794+08:00在 rails 上傳圖片並進行裁切時遭遇到的神奇問題markdown
我使用 carrierwave 來做圖片上傳,並使用產生器來生成 uploader,像這樣:
```
rails g uploader normal
```
會生成這樣的檔案:
```
class NormalUploader < CarrierWave::Uploader::Base
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
end
```
若想要在上傳圖片時,對圖片進行裁切操作,可以這樣寫:
```
class CropUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
process :crop
def crop
manipulate! do |img|
x = 1
y = 2
w = 3
h = 4
img.crop("#{w}x#{h}+#{x}+#{y}")
end
end
end
```
其中
```
include CarrierWave::MiniMagick
```
需要 gem "mini_magick" 以及 brew install imagemagick。
```
process :crop
```
這是一個 callback,會在圖片儲存前給你一個機會對圖片做事。所以只要前端傳遞一個矩形座標到後端就能切圖。這裡就隨便用 4 個值意思一下。
```
img.crop("#{w}x#{h}+#{x}+#{y}")
```
這個 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("#{w}x#{h}+#{x}+#{y}")
end
```
因為有兩個以上的參數,所以需要用 combine_options 去串接參數。
```
c.repage.+
```
會生成出
```
+repage
```
事實上他會把函數名稱拿去當作參數名稱,如果我這樣寫:
```
img.combine_options do |c|
c.jsdiofaodj.+
c.crop("#{w}x#{h}+#{x}+#{y}")
end
```
他就會嘗試執行
```
mogrify +jsdiofaodj -crop 199x154+234+343 file_path
```
如果把 .+ 拔掉:
```
img.combine_options do |c|
c.jsdiofaodj
c.crop("#{w}x#{h}+#{x}+#{y}")
end
```
就會變成
```
mogrify -jsdiofaodj -crop 199x154+234+343 file_path
```
如果調換順序:
```
img.combine_options do |c|
c.crop("#{w}x#{h}+#{x}+#{y}")
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)卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-51044182350348430182018-01-10T08:45:00.000+08:002018-01-10T08:45:29.285+08:00第二十二天:用 Line Messaging API 實作關鍵字回覆markdown
今天我要讓你能抓到一點寫程式的感覺,所以我們會一直不斷地修改程式碼,這麼做可以讓你對程式碼的操作更熟悉。
先從最簡單的功能開始作,今天的目標是讓卡米狗能針對關鍵字回應訊息。
# 程式碼的重構
在加程式碼之前,我們先整理一下目前的程式
```
def webhook
# Line Bot API 物件初始化
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '好哦~好哦~'
}
# 傳送訊息
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 = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
```
我們把這段程式搬出去,變成這樣:
```
# Line Bot API 物件初始化
def line
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
```
定義一個方法叫作 `line`,他會回傳一個 client。這裡可以省略區域變數 client 不寫。
```
# Line Bot API 物件初始化
def line
Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
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 = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
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 = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
```
`||` 是或的意思,這是一個很特殊的寫法,跟原本的程式碼效果幾乎相同,我就不多作解釋。沒學會 `||=` 也沒關係,這就是工程師der浪漫。
目前的完整程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '移出 client'
}
# 傳送訊息
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 = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
...下略
end
```
改到這邊可以上傳程式碼測試一下,如果你不測也沒關係,因為我們還要繼續改。
# 移出 reply token
我們作一個函數,讓他自己去抓 reply token,我們關心的是要發什麼話,不關心 reply_token,所以我希望我們的主程式能變成這樣。
```
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
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['events'][0]['replyToken']
# 傳送訊息
response = line.reply_message(reply_token, message)
end
```
可以再精簡為:
```
# 傳送訊息到 line
def reply_to_line(message)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 傳送訊息
line.reply_message(reply_token, message)
end
```
因為一個函數的傳回值是最後一行的執行結果。這就是工程師der浪漫。
其實我們大部分時間在作的事情都是搬移程式,其實寫程式就是一種整理的藝術。
你可以想像成我們東西一開始全都放在客廳。東西越來越多之後,客廳就會開始變亂。要整理客廳的方法就是買幾個櫃子後把東西放進櫃子。函數就是我們的櫃子。
目前的完整程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
response = reply_to_line(message)
# 回應 200
head :ok
end
# 傳送訊息到 line
def reply_to_line(message)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 傳送訊息
line.reply_message(reply_token, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
...下略
end
```
# 移出 message
這是現在的主程式:
```
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
response = reply_to_line(message)
# 回應 200
head :ok
end
```
我不希望在主程式看到這些:
```
{
type: 'text',
text: 'ㄅㄌㄅㄌㄅㄌ'
}
```
因為我們只在乎 `'ㄅㄌㄅㄌㄅㄌ'` 的部分,所以剩餘的部分都要盡量外移。這就像你不會想把垃圾放在桌上一樣,找個垃圾桶放垃圾就對了。
設定目標:
```
def webhook
# 設定回覆訊息
reply_text = '移出 message'
# 傳送訊息
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['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
```
其實就是把一開始的程式整個全搬到 `reply_to_line`。
目前的完整程式碼如下:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆文字
reply_text = '移出 message'
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
# 傳送訊息到 line
def reply_to_line(reply_text)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
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['events'][0]['message']['text']
end
# 關鍵字回覆
def keyword_reply(received_text)
received_text
end
```
現在這樣就表示你說什麼,卡米狗就會跟著說什麼。
目前完整的程式碼如下:
```
require 'line/bot'
class KamigoController < 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['events'][0]['message']['text']
end
# 關鍵字回覆
def keyword_reply(received_text)
received_text
end
...下略
end
```
這程式碼可以運作,你可以上傳程式碼玩玩看。
這兩個函數現在這樣都還算未完成,他們都有一些缺陷。
先講關鍵字回覆。這裡應該要作成看見A回答B,而不是看見A回答A,我們還缺一個學習紀錄表讓卡米狗查。
而 `received_text` 的問題是,如果 Line 傳來的通知並不是訊息通知,而是比方說有人加你好友,或邀請你進入群組,或傳送貼圖、圖片、聲音、檔案都可能會導致我們的程式直接掛掉,因為 `params['events'][0]['message']['text']` 的值是空值,而我們沒有說當是空值的時候應該怎麼作。
# 處理 received_text 的空值問題
根據 Line Messaging API 的文件,當有人傳訊息來時,我們才會收到 `message` 這個 hash,也就是說,下面這行不一定有值。
```
params['events'][0]['message']
```
我們應該在他有值的時候才去取 ['text']
```
# 取得對方說的話
def received_text
message = params['events'][0]['message']
if message.nil?
nil
else
message['text']
end
end
```
這是我們第一次用到了 if ,當 `message.nil?` 是空值的時候,我們就會傳回 `nil`。不是空值的話,就傳回 `message['text']`。
加入一點工程師的浪漫就會變成這樣:
```
# 取得對方說的話
def received_text
message = params['events'][0]['message']
message['text'] unless message.nil?
end
```
有的時候不用太堅持什麼浪漫,因為這純粹只是工程師的自爽行為。
# 關鍵字回覆的學習紀錄表
```
# 關鍵字回覆
def keyword_reply(received_text)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
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['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
```
這裡加一行 `return nil if reply_text.nil?`,當傳入值為空時表示不回應,後面的程式碼就不用作了。
目前完整的程式碼如下:
```
require 'line/bot'
class KamigoController < 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['events'][0]['message']
message['text'] unless message.nil?
end
# 關鍵字回覆
def keyword_reply(received_text)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
keyword_mapping[received_text]
end
# 傳送訊息到 line
def reply_to_line(reply_text)
return nil if reply_text.nil?
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
def eat
render plain: "吃土啦"
end
def request_headers
render plain: request.headers.to_h.reject{ |key, value|
key.include? '.'
}.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def response_headers
response.headers['5566'] = 'QQ'
render plain: response.headers.to_h.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def request_body
render plain: request.body
end
def show_response_body
puts "===這是設定前的response.body:#{response.body}==="
render plain: "虎哇花哈哈哈"
puts "===這是設定後的response.body:#{response.body}==="
end
def sent_request
uri = URI('http://localhost:3000/kamigo/eat')
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)
"#{message}油~"
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)
今天就講到這,明天講怎麼教卡米狗說話。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-14581470804366002372018-01-09T03:05:00.000+08:002018-01-09T03:05:28.534+08:00第二十一天:讓 Line Bot 回覆訊息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>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]: => Booting Puma
2018-01-08T15:04:50.486716+00:00 app[web.1]: => Rails 5.1.4 application starting in production
2018-01-08T15:04:50.486718+00:00 app[web.1]: => 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 "/kamigo/webhook" 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: {"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}], "kamigo"=>{"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}]}}
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'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="/kamigo/webhook" host=people-all-love-kamigo.herokuapp.com request_id=34361d41-4be8-4293-971d-eca0151ea11c fwd="203.104.156.74" 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]: => Booting Puma
2018-01-08T15:04:50.486716+00:00 app[web.1]: => Rails 5.1.4 application starting in production
2018-01-08T15:04:50.486718+00:00 app[web.1]: => 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 "/kamigo/webhook" 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: {"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}], "kamigo"=>{"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}]}}
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'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 "/kamigo/webhook" for 203.104.156.74 at 2018-01-08 15:04:52 +0000
Processing by KamigoController#webhook as HTML
Parameters: {"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}], "kamigo"=>{"events"=>[{"replyToken"=>"00000000000000000000000000000000", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100001", "type"=>"text", "text"=>"Hello, world"}}, {"replyToken"=>"ffffffffffffffffffffffffffffffff", "type"=>"message", "timestamp"=>1515423877419, "source"=>{"type"=>"user", "userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"}, "message"=>{"id"=>"100002", "type"=>"sticker", "packageId"=>"1", "stickerId"=>"1"}}]}}
Can't verify CSRF token authenticity.
Completed 200 OK in 21ms
```
這看起來就有點眼熟了。
他打了一個 POST 到我們的 `/kamigo/webhook`,並且傳了這樣的參數:
```
{
"events"=>[
{
"replyToken"=>"00000000000000000000000000000000",
"type"=>"message",
"timestamp"=>1515423877419,
"source"=>{
"type"=>"user",
"userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"
},
"message"=>{
"id"=>"100001",
"type"=>"text",
"text"=>"Hello, world"
}
},
{
"replyToken"=>"ffffffffffffffffffffffffffffffff",
"type"=>"message",
"timestamp"=>1515423877419,
"source"=>{
"type"=>"user",
"userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"
},
"message"=>{
"id"=>"100002",
"type"=>"sticker",
"packageId"=>"1",
"stickerId"=>"1"
}
}
],
"kamigo"=>{
"events"=>[
{
"replyToken"=>"00000000000000000000000000000000",
"type"=>"message",
"timestamp"=>1515423877419,
"source"=>{
"type"=>"user",
"userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"
},
"message"=>{
"id"=>"100001",
"type"=>"text",
"text"=>"Hello, world"
}
},
{
"replyToken"=>"ffffffffffffffffffffffffffffffff",
"type"=>"message",
"timestamp"=>1515423877419,
"source"=>{
"type"=>"user",
"userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"
},
"message"=>{
"id"=>"100002",
"type"=>"sticker",
"packageId"=>"1",
"stickerId"=>"1"
}
}
]
}
}
```
### 解讀 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。
```
"source"=>{
"type"=>"user",
"userId"=>"Udeadbeefdeadbeefdeadbeefdeadbeef"
}
```
這是發生在私訊對話框,對方的 id 是 Udeadbeefdeadbeefdeadbeefdeadbeef。
```
"message"=>{
"id"=>"100001",
"type"=>"text",
"text"=>"Hello, world"
}
```
對方傳來的訊息是文字訊息:「Hello, world」。
再看第二個 hash。第二個 hash 裡的 source 跟第一個一樣,所以我們只需要看 message 的部分。
```
"message"=>{
"id"=>"100002",
"type"=>"sticker",
"packageId"=>"1",
"stickerId"=>"1"
}
```
對方傳來的訊息是貼圖訊息。
# 觀察實際傳遞的訊息
讓我們試著用 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)
都開啟後再次傳訊息就會在小黑框看到:
```
{
"events"=>[
{
"type"=>"message",
"replyToken"=>"1d0bc2113fd344deb8131750e8d2daa2",
"source"=>{
"userId"=>"Uc68d82df46b7899e7d716f396ae8e91a",
"type"=>"user"
},
"timestamp"=>1515432401148,
"message"=>{
"type"=>"text",
"id"=>"7279127481400",
"text"=>"五五六六得第一"
}
}
]
}
```
我把不重要的部分都刪除了,剩下來的部分看起來差不多,應該不用多作解釋,現在我們來作回覆訊息。
# 回覆訊息
### 安裝 Line Bot API
我們要先安裝一個 Line 提供的 gem,在 Gemfile 加入以下程式:
```
# line
gem 'line-bot-api'
```
記得要在小黑框輸入 bundle 下載套件。
### 引入 Line Bot API
修改 `app/controllers/kamigo_controller.rb` 檔,在第一行加上`require 'line/bot'`:
```
require 'line/bot'
```
這表示我們要使用 Line 的套件。
### 修改 webhook
這個是 webhook 寫好的樣子,我會慢慢解釋他。
```
def webhook
# Line Bot API 物件初始化
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '好哦~好哦~'
}
# 傳送訊息
response = client.reply_message(reply_token, message)
# 回應 200
head :ok
end
```
### Line Bot API 物件初始化
```
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
```
這段程式是我們使用 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['events'][0]['replyToken']
```
根據剛剛我們觀察的 hash 結構,我們大膽假設 `events` 陣列裡只有一筆資料,用 `[0]` 取得第一筆,然後取得他裡面的 `replyToken`。
### 設定回覆訊息
```
message = {
type: 'text',
text: '好哦~好哦~'
}
```
我們回覆的訊息是純文字訊息,內容是「好哦~好哦~」
### 傳送訊息
```
response = client.reply_message(reply_token, message)
```
我們透過 Line Bot API 包裝好的函數來回覆訊息,這樣我們就不用去寫在[第十六天:做一個最簡單的爬蟲](https://ithelp.ithome.com.tw/articles/10195760)寫過的那些東西。
### 對一下程式碼
這是完整的 `app/controllers/kamigo_controller.rb` 檔內容:
```
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# Line Bot API 物件初始化
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '好哦~好哦~'
}
# 傳送訊息
response = client.reply_message(reply_token, message)
# 回應 200
head :ok
end
...下略
end
```
### 上傳程式碼
確認沒問題之後,因為我們沒辦法進行本地端的測試,所以就直接上傳程式碼:
```
git add .
git commit -m "回覆訊息"
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['events'][0]['replyToken']
p "======這裡是 reply_token ======"
p reply_token
p "============"
```
像這樣加上幾行 `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)
卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-82039757062654775502018-01-08T03:21:00.000+08:002018-01-08T03:21:55.941+08:00第二十天:串接 Line Messaging API Webhookmarkdown
大家還記得為什麼我們要作網站嗎?是因為我們要作 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 '/kamigo/webhook', to: 'kamigo#webhook'
```
在 `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] "/kamigo/webhook"`,意思是我們沒有設定 `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'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 "/kamigo/webhook" for 127.0.0.1 at 2018-01-08 01:55:37 +0800
Processing by KamigoController#webhook as */*
Can'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 < ApplicationController
protect_from_forgery with: :null_session
...下略
```
弄好後存檔,此時再去按 POSTMAN 的 `Send`,就會得到 Status: 200 OK,而我們的小黑框顯示的變成這樣:
```
Started POST "/kamigo/webhook" for 127.0.0.1 at 2018-01-08 02:41:37 +0800
Processing by KamigoController#webhook as */*
Can't verify CSRF token authenticity.
Completed 200 OK in 3ms
```
其中的 Can't verify CSRF token authenticity. 是在講說他沒有完成 CSRF 檢查。
# 上傳程式碼
我們上傳程式碼,上傳程式碼的三步驟:
```
git add .
git commit -m "新增 webhook"
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 成功!
今天就先到這,明天就能讓聊天機器人講話了。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-65500853763149361822018-01-07T03:48:00.003+08:002018-01-14T22:48:58.645+08:00第十九天:發布網站到 Heroku (續)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 'sqlite3'` 表示這個專案會使用 sqlite3 這個套件。套件是一群佛心來的人寫好後公開給大家用的程式。如果你想要安裝套件,就會需要在這裡加一行程式,如果想要移除某個套件,就要刪掉那一行程式。
這裡有所有能裝的套件:[https://rubygems.org/gems](https://rubygems.org/gems)。如果你想要成佛也可以在這裡貢獻一下你的程式。
每當修改過 Gemfile 之後,你要在小黑框輸入 `bundle install`。 bundle 是一個管理套件的套件,他會幫你下載套件程式碼。
如果你輸入 `bundle install` 之後看到的是這樣:
```
D:\只要有心,人人都可以作卡米狗\ironman>bundle install
'bundle' 不是內部或外部命令、可執行的程式或批次檔。
```
表示你的電腦沒有裝過 bundler,需要在小黑框輸入 `gem install bundler` 來安裝。
```
D:\只要有心,人人都可以作卡米狗\ironman>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 'sqlite3'`,我們可以在這行的最前面加一個 `#` 號把這行變成註解,像這樣:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgesrc7RuIRieOnpvrexe3RDXuhWATO1w3oYLFCDc73Hh0CiwuSX2cDpj98O0bTqAvVe8Jfte_17iHc0XtHQO-T1DHgHIe_ZLziLGVlQlLNTSSKT-6B_6csR0DFym_4OHEkze9AjjtDMRE/s1600/29.jpg)
當然你要直接刪掉那行也不是不行啦,弄好之後記得存檔,存完檔之後,在小黑框輸入 `bundle install`。
```
D:\只要有心,人人都可以作卡米狗\ironman>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>
```
你會看到目前專案正在使用的所有套件。
# 進行本機測試
開啟 `rails server` 看看是否一切正常:
```
D:\只要有心,人人都可以作卡米狗\ironman>rails s
=> Booting Puma
=> Rails 5.1.4 application starting in development
=> 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 'pg', '~> 0.21.0'`,其實要寫在第幾行都行。不過第 37 行有一個 group:
```
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
# Adds support for Capybara system testing and selenium driver
gem 'capybara', '~> 2.13'
gem 'selenium-webdriver'
end
```
`group :development, :test do` 這行的意思是,只有在開發和測試環境下才需要安裝的套件。因為我們的 heroku 的環境是 :production,所以我們如果把 `gem 'pg', '~> 0.21.0'` 寫在這個 group 裡的話,heroku 就還是壞的。
其中`~> 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 "註解"
git push heroku master
```
如果你覺得註解亂寫沒關係的話,我是沒差,你可以直接複製上面三行程式碼到小黑框貼上。不過我就一步步來。
### `git add .`
選擇所有檔案加入這次的版本:
```
D:\只要有心,人人都可以作卡米狗\ironman>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 "改用pg"`
建立一個版本,我的註解是寫 `改用pg` 說明我們這次的改動:
```
D:\只要有心,人人都可以作卡米狗\ironman>git commit -m "改用pg"
[master e6f3871] '改用pg'
3 files changed, 5 insertions(+), 2 deletions(-)
D:\只要有心,人人都可以作卡米狗\ironman>
```
### `git push heroku master`
上傳程式碼到 heroku:
```
D:\只要有心,人人都可以作卡米狗\ironman>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: -----> Ruby app detected
remote: -----> Compiling Ruby/Rails
remote: -----> 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: -----> 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 (>= 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: -----> Installing node-v6.11.1-linux-x64
remote: -----> Detecting rake tasks
remote: -----> 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 '2.3.4'
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: -----> Discovering process types
remote: Procfile declares types -> (none)
remote: Default types for buildpack -> console, rake, web, worker
remote:
remote: -----> Compressing...
remote: Done: 36.7M
remote: -----> 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 -> master
D:\只要有心,人人都可以作卡米狗\ironman>
```
這次看似上傳成功了,從最後面往上看,倒數第 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)
吃土啦~
# 總結
程式碼除錯的流程大致上為:
- 閱讀錯誤訊息
- 修改程式碼
- 測試網站,若失敗則回到第一步
- 發布網站
工程師的日常就是除了一個錯之後看到的不是正常,而是看到下一個錯誤訊息。
要有耐心。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-7252271372107219112018-01-06T12:54:00.001+08:002018-01-06T12:54:17.155+08:00第十八天:發布網站到 Heroku( 2018 iT邦幫忙鐵人賽-只要有心,人人都可以作卡米狗 )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>git init
Initialized empty Git repository in D:/只要有心,人人都可以作卡米狗/ironman/.git/
```
或者看到:
```
D:\只要有心,人人都可以作卡米狗\ironman>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>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>
```
建立好之後回到 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>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>
```
一堆訊息但不是很重要,他說:「我把 windows 的換行符號改成別種系統的換行符號了哦。」
接下來是 git commit, git commit 表示我們想要建立一個新的版本,我們要留下一些紀錄說明這個版本作了什麼變更,因為是第一次,所以我們輸入 `git commit -m init`。
```
D:\只要有心,人人都可以作卡米狗\ironman>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>
```
一樣一堆訊息,他說:「這次建立的版本新增了這些檔案哦。」
「好哦~好哦~」
接下來是 git push,git push 可以把我們建立好的版本,傳送到遠端的 git server,當然我們是要傳送到 heroku,所以我們輸入的是 `git push heroku master`。
```
D:\只要有心,人人都可以作卡米狗\ironman>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: -----> Ruby app detected
remote: -----> Compiling Ruby/Rails
remote: -----> 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: -----> 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 (>= 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 'brew install sqlite3',
remote: 'yum install sqlite-devel' or 'apt-get install libsqlite3-dev'
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 '1.3.13'` succeeds before bundling.
remote: In Gemfile:
remote: sqlite3
remote: Bundler Output: The dependency tzinfo-data (>= 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 'brew install sqlite3',
remote: 'yum install sqlite-devel' or 'apt-get install libsqlite3-dev'
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 '1.3.13'` 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 -> master (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/people-all-love-kamigo.git'
D:\只要有心,人人都可以作卡米狗\ironman>
```
一樣是一堆訊息,其實他這步驟作了非常多的事情,不過我們只要關注結果就好,結果就是 `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。」
今天就先到這裡,明天再講怎麼修。
卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-52800045402175955692018-01-05T02:24:00.002+08:002018-01-05T02:45:17.426+08:00第十七天:怎麼讓別人連到我作好的網站?( 2018 iT邦幫忙鐵人賽-只要有心,人人都可以作卡米狗 )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。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-53611756596472755892018-01-04T02:20:00.000+08:002018-01-04T03:33:23.535+08:00第十六天:做一個最簡單的爬蟲( 2018 iT邦幫忙鐵人賽-只要有心,人人都可以作卡米狗 )markdown
一直以來,我們都是用瀏覽器發出 `HTTP request`,打到我們的 Rails Server,然後我們的 Rails Server 再傳回 `HTTP Response`。
我們還沒有試過用 Rails Server 發出 `HTTP request`,今天就來試一下。
# 用 Rails 發 HTTP request
### Routes
在 `config/routes.rb` 加入
```
get '/kamigo/sent_request', to: 'kamigo#sent_request'
```
### Action
在 `app/controllers/kamigo_controller.rb` 加入
```
def sent_request
uri = URI('http://localhost:3000/kamigo/response_body')
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 "/kamigo/sent_request" for 127.0.0.1 at 2018-01-04 01:06:38 +0800
Processing by KamigoController#sent_request as HTML
Started GET "/kamigo/response_body" 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)
"#{message}油~"
end
```
一個函數裡面可以寫一段程式,最後一行程式的執行結果就會是函數的傳回值。
不管傳入什麼,我們就在後面加上`油~`。
### 函數呼叫
```
translate_to_korean('愛老虎')
```
你想要使用小括號或者使用空白框住傳入的參數都可以。
```
translate_to_korean '愛老虎'
```
### 結果
```
"愛老虎油~"
```
成功翻譯成韓文!
# 把翻譯韓文的功能加入到上面的爬蟲
### 修改 Action
```
def sent_request
uri = URI('http://localhost:3000/kamigo/response_body')
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 的值是什麼,結果看到是這樣:
```
>> message
=> "\xE8\x99\x8E\xE5\x93\x87\xE8\x8A\xB1\xE5\x93\x88\xE5\x93\x88\xE5\x93\x88"
>>
```
因為我們獲得的 response 字串的編碼是 `ASCII-8BIT`,沒辦法直接跟 `UTF-8` 編碼的 "油~" 加在一起。
所以修正的方法就是把 response 字串轉碼為 UTF-8 即可:
### 再次修改 Action
```
def sent_request
uri = URI('http://localhost:3000/kamigo/response_body')
response = Net::HTTP.get(uri).force_encoding("UTF-8")
render plain: translate_to_korean(response)
end
```
在字串後面寫 `.force_encoding("UTF-8")` 就可以把編碼轉為 `UTF-8` 格式了。
### 再次開啟網頁
```
虎哇花哈哈哈油~
```
玩樂時間結束,來作點正事,目前的寫法沒辦法觀察 `request` 和 `response`,我們需要能觀察 `request` 和 `response` 的寫法。
# 能觀察 request 和 response 的寫法
### 修改 Action
```
def sent_request
uri = URI('http://localhost:3000/kamigo/eat')
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` 幫我排成比較好閱讀的格式,讓我們來看一下個別對應的類別:
### 開啟網頁
```
{
"request_class": "ActionDispatch::Request",
"response_class": "ActionDispatch::Response",
"http_request_class": "Net::HTTP::Get",
"http_response_class": "Net::HTTPOK"
}
```
### 圖解
![](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
- 今天學會了宣告函數和呼叫函數的正確觀念。
明天講怎麼讓別人能連到你作的網站。
卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-74694678873948549762018-01-03T03:55:00.000+08:002018-01-03T04:17:37.992+08:00第十五天:從 Rails 認識 HTTP 協定( 2018 iT邦幫忙鐵人賽-只要有心,人人都可以作卡米狗 )markdown
昨天我們學會了怎麼新增 Route 跟 Controller,並且知道怎麼控制要回應什麼內容給瀏覽器。
今天要使用這兩天學會的東西,我們要來看看瀏覽器到底傳了什麼給 Rails,以及 Rails 傳回什麼給瀏覽器。
# 做一組新的 Route 跟 Controller
我們在 `app/controllers/kamigo_controller.rb` 新增一個 `request_headers` 方法,嘗試把 `request` 中的 `headers` 傳回給瀏覽器。
```
class KamigoController < ApplicationController
def eat
render plain: "吃土啦"
end
def request_headers
render plain: request.headers
end
end
```
然後在 `config/routes.rb` 加入一行 `get '/kamigo/request_headers', to: 'kamigo#request_headers'`。
```
Rails.application.routes.draw do
get '/kamigo/eat', to: 'kamigo#eat'
get '/kamigo/request_headers', to: 'kamigo#request_headers'
end
```
開啟網頁伺服器,並且用瀏覽器打開網頁:[http://localhost:3000/kamigo/request_headers](http://localhost:3000/kamigo/request_headers)。
你會看到:
```
#<ActionDispatch::Http::Headers:0x0000000007ead268>
```
表示 `request.headers` 是一個 [ActionDispatch::Http::Headers](http://api.rubyonrails.org/classes/ActionDispatch/Http/Headers.html) 物件,但是其實我們想知道的是他包含的內容是什麼,而不是他是什麼類別的物件。
# 從 Rails 觀察 request.headers
所以我們需要改一下程式:
```
class KamigoController < ApplicationController
def eat
render plain: "吃土啦"
end
def request_headers
render plain: request.headers.to_h
end
end
```
加一個 `to_h` ,試著把物件轉成雜湊陣列看看。
```
{"rack.version"=>[1, 3],...
下略 180 萬字
```
結果是成功了,但是瀏覽器印出了 180 萬個字。
我們先試著忽略內容的部分,看一下他的關鍵字有哪些。
```
class KamigoController < ApplicationController
def eat
render plain: "吃土啦"
end
def request_headers
render plain: request.headers.to_h.keys
end
end
```
再加一個 `keys` ,試著把雜湊陣列轉成只包含關鍵字的陣列看看。
```
["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"]
```
看起來好多了,但是這不好閱讀,我們試著讓他自己換行。
```
class KamigoController < ApplicationController
def eat
render plain: "吃土啦"
end
def request_headers
render plain: request.headers.to_h.keys.join("\n")
end
end
```
再加一個 `join("\n")`,試著把陣列轉成字串,並且以 `\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("\n")
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 + ": " + value.class.to_s
}.sort.join("\n")
end
```
我們把 `keys` 改成了 `.map{ |key, value| key + ": " + value.class.to_s}`,map 的意思是把每一筆紀錄都拿去作一個轉換,然後傳回一個陣列。
如果我們是寫成 `.map{ |key, value| key }` 的話,就是每一筆紀錄我們只要保留關鍵字的部分,這樣就等於 `keys`,這兩段程式會有相同的結果。
但是因為我們想要看到更多的東西,所以我們再把 `key` 改寫成 `key + ": " + 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|
"#{key}: #{value.class}"
}.sort.join("\n")
end
```
`"#{key}: #{value.class}"` 是更加簡潔的表示法,用雙引號包裝的字串可以作變數的代換,語法是 `#{變數的名字}`,這種寫法的程式碼會更好閱讀。
看起來那些 `key` 包含 `action_controller`、`action_dispatch`、`puma`、`rack` 的資料都是我們不想看到的,因為他們看起來像是 Rails 這邊才產生的資料,而不是瀏覽器傳來的資料。
我發現這些我們不想要的 key 都包含 `.`,讓我們把在 `key` 包含 `.` 的資料都過濾掉。
```
def request_headers
render plain: request.headers.to_h.reject{ |key, value|
key.include? '.'
}.map{ |key, value|
"#{key}: #{value.class}"
}.sort.join("\n")
end
```
reject 可以過濾資料,滿足條件 `key.include? '.'` 的資料就沒有辦法進到 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? '.'
}.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
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/"d220fb78d8ab10894ccdd2ef500f5ac8"
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 '/kamigo/request_body', to: 'kamigo#request_body'
```
Action:
```
def request_body
render plain: request.body
end
```
用瀏覽器連過去看:[http://localhost:3000/kamigo/request_body](http://localhost:3000/kamigo/request_body),發現是 `#<Puma::NullIO:0x000000000673ae00>`,表示瀏覽器傳了一個空的 request body 過來,因為他是來下載網頁,不是上傳資料。如果是上傳資料的話,就會在 request body 有東西了。
# 從 Rails 觀察 response.headers
再作一組 Route 和 Controller Action。
Route:
```
get '/kamigo/response_headers', to: 'kamigo#response_headers'
```
Action:
```
def response_headers
render plain: response.headers.to_h.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
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['5566'] = 'QQ'
render plain: response.headers.to_h.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
```
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiW05FXWJNo1QOU3mDjAEhbHTKH97KppDbmgNZwglXEBiAKftpJiOf_5cUTmtYOpMj7khUSAofEvxRrg9sjM2pHsYzD8V5utjztugCaofwIyxPiaEsnNTK3FiMacbwYGf8mY5YRK2SuTvQ/s1600/7.jpg)
哈哈哈哈ㄚ哈
# 從 Rails 觀察 response.body
再作一組 Route 和 Controller Action。
Route:
```
get '/kamigo/response_body', to: 'kamigo#show_response_body'
```
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 "媽我在這 \\( ̄▽ ̄)/"` 意思是把 `媽我在這 \\( ̄▽ ̄)/` 顯示在小黑框上。
```
def show_response_body
puts "媽我在這 \\( ̄▽ ̄)/"
render plain: response.body
end
```
當有人開啟網頁 [http://localhost:3000/kamigo/response_body](http://localhost:3000/kamigo/response_body),就會在小黑框看到這個結果:
```
Started GET "/kamigo/response_body" 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 "===這是設定前的response.body:#{response.body}==="
render plain: "虎哇花哈哈哈"
puts "===這是設定後的response.body:#{response.body}==="
end
```
當有人開啟網頁 [http://localhost:3000/kamigo/response_body](http://localhost:3000/kamigo/response_body),就會在小黑框看到這個結果:
```
Started GET "/kamigo/response_body" 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 < ApplicationController
def eat
render plain: "吃土啦"
end
def request_headers
render plain: request.headers.to_h.reject{ |key, value|
key.include? '.'
}.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def response_headers
response.headers['5566'] = 'QQ'
render plain: response.headers.to_h.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def request_body
render plain: request.body
end
def show_response_body
puts "===這是設定前的response.body:#{response.body}==="
render plain: "虎哇花哈哈哈"
puts "===這是設定後的response.body:#{response.body}==="
end
end
```
### config/routes.rb
```
Rails.application.routes.draw do
get '/kamigo/eat', to: 'kamigo#eat'
get '/kamigo/request_headers', to: 'kamigo#request_headers'
get '/kamigo/request_body', to: 'kamigo#request_body'
get '/kamigo/response_headers', to: 'kamigo#response_headers'
get '/kamigo/response_body', to: 'kamigo#show_response_body'
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)
今天就講到這裡。
嗯?今天是不是講太多了?
卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-76342288110102636292018-01-02T04:13:00.000+08:002018-01-02T04:31:34.663+08:00第十四天:最基本的 Rails 運作流程( 2018 iT邦幫忙鐵人賽-只要有心,人人都可以作卡米狗 )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>
```
輸入 `rails generate controller kamigo` ,請 rails 幫我們生成一個叫做 kamigo 的 controller。
```
D:\只要有心,人人都可以作卡米狗\ironman>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>
```
他幫我們產生了好多檔案,開頭有 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>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>
```
# 這次我們手動新增 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 < 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 < 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 '/kamigo/eat', to: 'kamigo#eat'
end
```
`get '/kamigo/eat', to: 'kamigo#eat'` 的意思是當有人在瀏覽器輸入網址 `/kamigo/eat` 時,就把請求交給 kamigo 這個 controller 裡的 eat 方法來回應。
# 測試一下
在 cmd 輸入 `rails s` 開啟網頁伺服器:
```
D:\只要有心,人人都可以作卡米狗\ironman>rails s
=> Booting Puma
=> Rails 5.1.4 application starting in development
=> 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 < ApplicationController
def eat
render plain: "吃土啦"
end
end
```
我們加入了一行程式碼 `render plain: "吃土啦"`,意思是我們要用純文字`"吃土啦"`來回應這個請求,這樣 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 協定。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0tag:blogger.com,1999:blog-4221096241867026900.post-1197920175339193052018-01-01T05:29:00.001+08:002018-01-01T05:34:27.262+08:00第十三天:認識 Ruby 的資料型態( 2018 iT邦幫忙鐵人賽-只要有心,人人都可以作卡米狗 )markdown
今天要來學寫程式囉,今天就只講資料型態的部分,想要一天學完全部,我怕你的腦子會爆炸,畢竟在學校的話,這是一學期的課程。我們慢慢來就好,30 天之內一定會讓你學會寫聊天機器人的。
# 認識 irb
平常我們寫程式是寫在文字檔,然後再拿程式碼檔去一行行的執行程式。但當我們想要測試某一行程式碼的結果時,我們會使用 irb(Interactive Ruby) 來做測試。irb 很方便,我們可以在 irb 的環境裡面輸入一行程式碼,就能馬上得到這行程式碼的結果。
### 進入 irb
先按 `Windows`+`R` 輸入 `cmd` 叫出小黑框:
```
D:\只要有心,人人都可以作卡米狗>
```
輸入 `irb`。
```
D:\只要有心,人人都可以作卡米狗>irb
irb(main):001:0>
```
輸入完之後就會進入 irb 的世界,在左側看到 irb 開頭就代表現在在 irb 裡面。
### 離開 irb
要離開 irb 的話就輸入 `exit` 或者按 `Ctrl`+`C`。
```
D:\只要有心,人人都可以作卡米狗>irb
irb(main):001:0> exit
D:\只要有心,人人都可以作卡米狗>
```
就像這樣,就離開了 irb 的環境。
# 認識資料型態
再次進到 irb ,我們先認識一下資料型態:
### 數字
數字就是我們一般理解的數字,數學那種。
輸入 5566:
```
D:\只要有心,人人都可以作卡米狗>irb
irb(main):001:0> 5566
=> 5566
```
輸入 `5566` 之後看到 `=> 5566` 代表 `5566` 這行程式碼的執行結果是 `5566`。
接下來輸入 5566+9527 看看:
```
irb(main):002:0> 5566 + 9527
=> 15093
```
輸入 `5566+9527` 之後看到 `=> 15093` 代表 `5566+9527` 這行程式碼的執行結果是 `15093`。
輸入 `5566.class` 可以讓 ruby 告訴我們,5566 是什麼型態:
```
irb(main):003:0> 5566.class
=> Integer
```
5566 是 Integer(整數)。
### 字串
輸入 `'5566'.class` 看看:
```
irb(main):004:0> '5566'.class
=> String
```
用單引號或雙引號包起來的東西是字串,字串就是文字的意思。
輸入 `'5566+9527'` 看看:
```
irb(main):005:0> '5566+9527'
=> "5566+9527"
```
因為加號也是文字的一部分,所以就不會做計算。
讓我們試著把兩個字串加起來看看,輸入`'5566'+'9527'`:
```
irb(main):006:0> '5566'+'9527'
=> "55669527"
```
就像是在 5566 後面輸入 9527 一樣。
在 windows 的 irb 可能會遇到不能輸入中文的情形,目前請先別介意,我們用英文就好。
### 變數
我們可以使用等號來幫數字或文字取名字。
```
irb(main):007:0> idol = 5566
=> 5566
```
變數定義完之後,我們只要叫到他,就視同輸入等號右邊的結果。
```
irb(main):008:0> idol
=> 5566
```
事實上我們可以取名字的東西非常多,不是只有數字和文字能被取名。
### 陣列
我們可以一次宣告大量變數,語法是中括號 [] 的中間用逗號區隔。
```
irb(main):009:0> eat = ["Mcdonald's", 'KFC', 'BUG King', 'MOS BUG']
=> ["Mcdonald's", "KFC", "BUG King", "MOS BUG"]
```
我們這樣就宣告了4個字串變數,分別是:
- eat[0] = "Mcdonald's"
- eat[1] = "KFC"
- eat[2] = "BUG King"
- eat[3] = "MOS BUG"
在電腦的世界裡第一個數字是 0。,所以宣告一堆變數之後,第一個變數的索引是 0。
你也可以這樣寫,效果是相同的:
```
irb(main):010:0> eat = []
=> []
irb(main):011:0> eat[0] = "Mcdonald's"
=> "Mcdonald's"
irb(main):012:0> eat[1] = 'KFC'
=> "KFC"
irb(main):013:0> eat[2] = 'BUG King'
=> "BUG King"
irb(main):014:0> eat[3] = 'MOS BUG'
=> "MOS BUG"
irb(main):015:0> eat
=> ["Mcdonald's", "KFC", "BUG King", "MOS BUG"]
```
第一行是宣告一個空陣列,然後接下來的每一行都是塞入一個值。
你也可以這樣寫,效果也是相同的:
```
irb(main):016:0> eat = []
=> []
irb(main):017:0> eat << "Mcdonald's"
=> ["Mcdonald's"]
irb(main):018:0> eat << 'KFC'
=> ["Mcdonald's", "KFC"]
irb(main):019:0> eat << 'BUG King'
=> ["Mcdonald's", "KFC", "BUG King"]
irb(main):020:0> eat << 'MOS BUG'
=> ["Mcdonald's", "KFC", "BUG King", "MOS BUG"]
```
`<<` 表示放一個東西進入陣列。
查看 `eat` 的資料型態會發現資料型態是 Array(陣列)。
```
irb(main):021:0> eat.class
=> Array
```
查看 `eat[0]` 的資料型態會發現資料型態是 String(字串)。
```
irb(main):022:0> eat[0].class
=> String
```
跟一般的變數一樣,輸入 `eat[2]` 可以取得他的內容。
```
irb(main):023:0> eat[2]
=> "BUG King"
```
但是你還可以這樣:
```
irb(main):024:0> bgk = 2
=> 2
irb(main):025:0> eat[bgk]
=> "BUG King"
```
也許現在還很難理解把 `2` 換成 `bgk` 會有什麼好處,本篇結尾會說明。
# 雜湊陣列
在陣列中,我們學到可以用數字作為索引,而雜湊陣列則是把數字改成任何資料型態的變數。
雜湊陣列(Hash)是使用大括號作為定義:
```
irb(main):026:0> eat_hash = {}
=> {}
irb(main):027:0> eat_hash[0] = "Mcdonald's"
=> "Mcdonald's"
irb(main):028:0> eat_hash[1] = "KFC"
=> "KFC"
irb(main):029:0> eat_hash[2] = "BUG King"
=> "BUG King"
irb(main):030:0> eat_hash[3] = "MOS BUG"
=> "MOS BUG"
irb(main):031:0> eat_hash
=> {0=>"Mcdonald's", 1=>"KFC", 2=>"BUG King", 3=>"MOS BUG"}
```
你也可以這樣寫:
```
irb(main):032:0> eat_hash = {0=>"Mcdonald's", 1=>"KFC", 2=>"BUG King", 3=>"MOS BUG"}
=> {0=>"Mcdonald's", 1=>"KFC", 2=>"BUG King", 3=>"MOS BUG"}
```
和陣列不同的是,雜湊陣列不需要從 0 開始。
而且雜湊陣列的特性是可以用不是數字的變數作為索引,其中最常用的索引是字串索引,舉例來說:
```
irb(main):033:0> kamigo = {}
=> {}
irb(main):034:0> kamigo['Q'] = 'A'
=> "A"
```
而這就是卡米狗學說話的核心原理。
卡米狗有一個很大的雜湊陣列,當有人教卡米狗說話時,我就會將關鍵字和要回應的內容儲存到雜湊陣列裡,像這樣:
`kamigo[關鍵字] = 要回應的內容`
之後任何人說話,我就會去檢查 `kamigo[關鍵字]` 是不是有值,如果有值的話卡米狗就會做出回應。
講到這,應該就知道為什麼索引可以用變數代換是一件很重要的事情了吧?卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com1tag:blogger.com,1999:blog-4221096241867026900.post-77626794095665771892017-12-31T01:52:00.000+08:002017-12-31T01:59:11.362+08:00第十二天:從瀏覽器認識 HTTP 協定( 2018 iT邦幫忙鐵人賽-只要有心,人人都可以作卡米狗 )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 程式語言開始。卡卡米http://www.blogger.com/profile/04885803843782300210noreply@blogger.com0