ผลต่างระหว่างรุ่นของ "01204223/flask"

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
 
(ไม่แสดง 47 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน)
แถว 7: แถว 7:
 
* [https://www.youtube.com/watch?v=JMkKRhhnfRk Flask ตอน 2 - การส่งข้อมูลไปยัง template, คำสั่งใน template jinja, การสร้างลิงก์ด้วย tag a, การส่งพารามิเตอร์ทาง url, และการใช้ url_for]
 
* [https://www.youtube.com/watch?v=JMkKRhhnfRk Flask ตอน 2 - การส่งข้อมูลไปยัง template, คำสั่งใน template jinja, การสร้างลิงก์ด้วย tag a, การส่งพารามิเตอร์ทาง url, และการใช้ url_for]
 
* [https://www.youtube.com/watch?v=FbcnJj3ivxs Flask ตอน 3 - การใช้ form ในการส่งข้อมูล การใช้ redirect]
 
* [https://www.youtube.com/watch?v=FbcnJj3ivxs Flask ตอน 3 - การใช้ form ในการส่งข้อมูล การใช้ redirect]
 +
 +
แต่จะมีการเพิ่มเนื้อหา css ในตอนท้ายเอกสาร
  
 
ในคลิปดังกล่าวจะไม่ได้มีการใช้ git ประกอบการทำ แต่เราจะพยายามแสดงจุดที่ควรจะ commit งานเป็นระยะ ดังนั้นระหว่างการหัดใช้ Flask ให้ทดลองใช้ git ไปด้วยเลย
 
ในคลิปดังกล่าวจะไม่ได้มีการใช้ git ประกอบการทำ แต่เราจะพยายามแสดงจุดที่ควรจะ commit งานเป็นระยะ ดังนั้นระหว่างการหัดใช้ Flask ให้ทดลองใช้ git ไปด้วยเลย
 +
 +
== Web application architecture ==
 +
 +
สถาปัตยกรรมของเว็บแอพลิเคชันนั้น เป็นสถาปัตยกรรมแบบ client-server โดยมี browser เป็น client และเชื่อมต่อกับ web server
 +
 +
[[Image:223-web-client-server-arch.jpeg|400px]]
 +
 +
อย่างไรก็ตาม ภาพดังกล่าวเป็นภาพที่เราเห็นในระยะไกลมากเท่านั้น จริง ๆ แล้วด้านในของแต่ละชิ้นส่วนอาจจะมีส่วนประกอบที่ทำงานภายในมากมาย
 +
 +
ในช่วงแรกที่เราหัดพัฒนาเว็บ เราจะสนใจในส่วน server side ก่อน โดยจะพิจารณาว่า browser มีหน้าที่แสดงผลลัพธ์จาก response ที่เป็น html เพียงอย่างเดียว
 +
 +
ในช่วงถัดไป พอเราได้เรียนรู้เกี่ยวกับ javascript library สำหรับการจัดการฝั่ง client แล้ว เราจะค่อย ๆ ปรับให้ฝั่ง client (ที่ทำงานบน browser) ให้มีบทบาทมากขึ้นต่อไป
  
 
== เริ่มต้น และการสร้าง repository ==
 
== เริ่มต้น และการสร้าง repository ==
แถว 19: แถว 33:
  
 
== การติดตั้งและเริ่มเปิดหน้าเว็บ ==
 
== การติดตั้งและเริ่มเปิดหน้าเว็บ ==
Flask เป็น library บน python ซึ่งเราจะติดตั้งผ่านทางคำสั่ง pip  อย่างไรก็ตาม เราจะพยายามแบ่งส่วนการติดตั้ง library ของ python ให้แยกกันไปตามแต่ละงาน โดยใช้ virtual environment (venv)  ดังนั้นก่อนจะติดตั้ง Flask เราจะสร้าง virtual environment ก่อน โดยสั่ง
+
Flask เป็น library บน python ซึ่งเราจะติดตั้งผ่านทางคำสั่ง pip  อย่างไรก็ตาม เราจะพยายามแบ่งส่วนการติดตั้ง library ของ python ให้แยกกันไปตามแต่ละงาน โดยใช้ virtual environment (venv)  ดังนั้นก่อนจะติดตั้ง Flask เราจะสร้าง virtual environment ก่อน  
 +
 
 +
หมายเหตุ: ถ้าใช้ windows ให้เปิด terminal แบบ command (cmd) อย่าเปิด powershell
 +
 
 +
โดยสั่ง
  
 
  python -m venv venv
 
  python -m venv venv
แถว 25: แถว 43:
 
(พารามิเตอร์ venv ตัวแรกเป็นชื่อ module ใน python พารามิเตอร์ venv ตัวที่สองเป็นชื่อไดเร็กทอรีที่จะเก็บ environment ซึ่งเราเปลี่ยนเป็นชื่ออื่นได้)
 
(พารามิเตอร์ venv ตัวแรกเป็นชื่อ module ใน python พารามิเตอร์ venv ตัวที่สองเป็นชื่อไดเร็กทอรีที่จะเก็บ environment ซึ่งเราเปลี่ยนเป็นชื่ออื่นได้)
  
ถ้าจะเริ่มใช้จะเรียก activate ใน venv/bin/activate (เรียกแตกต่างกันตาม os)
+
ถ้าจะเริ่มใช้จะเรียกคำสั่ง activate ที่อยู่ใน venv การเรียกนี้จะแตกต่างกันตาม os
 +
 
 +
ถ้าใช้ Linux / Mac ให้เรียก
 +
 
 +
venv/bin/activate  
 +
 
 +
ถ้าใช้ Windows ให้เรียก
 +
 
 +
venv\Scripts\activate
  
 
ถ้าเรียก activate แล้ว prompt ใน terminal จะเปลี่ยนไป (ลองสังเกต) จากนั้นเราจะสามารถสั่งให้ติดตั้ง Flask ได้ โดยสั่ง
 
ถ้าเรียก activate แล้ว prompt ใน terminal จะเปลี่ยนไป (ลองสังเกต) จากนั้นเราจะสามารถสั่งให้ติดตั้ง Flask ได้ โดยสั่ง
แถว 66: แถว 92:
  
 
จากนั้นเราจะสามารถเปิดเว็บได้ที่ลิงก์ http://localhost:5000  (โดยปกติ Flask จะเปิดเว็บเซิร์ฟเวอร์ไว้ที่พอร์ต 5000)
 
จากนั้นเราจะสามารถเปิดเว็บได้ที่ลิงก์ http://localhost:5000  (โดยปกติ Flask จะเปิดเว็บเซิร์ฟเวอร์ไว้ที่พอร์ต 5000)
 +
 +
ในคลิปจะมีการทำให้ flask ทำงานใน DEBUG mode โดยการตั้งค่า FLASK_ENV ซึ่งใช้ได้กับ Flask เวอร์ชันเก่า ในที่นี้ เราจะสั่งโดยเรียก
 +
 +
flask run --debug
 +
 +
แทน
 +
 +
การทำงานใน debug mode ทำให้สิ่งที่เราแก้มีผลทันที '''อย่าลืมเรียกแบบนี้ทุกครั้ง''' ไม่เช่นนั้นอาจจะสับสนใจได้ ว่าทำไมสิ่งที่เราแก้ในโค้ดไม่มีผลต่อการทำงาน
 +
 +
==== ผลลัพธ์บน terminal (console) ====
 +
 +
ในการพัฒนาเว็บ นอกจากเราจะดูผลลัพธ์ทางหน้าจอบน browser แล้ว ใน terminal ที่เราเรียก flask run ก็ยังคงพิมพ์รายละเอียดของ request ต่าง ๆ ที่เข้ามาที่ development server ด้วย     ดังนั้นอย่าลืมใช้รายละเอียดเหล่านี้เป็นช่องทางในการตรวจสอบและติดตามการทำงานของโปรแกรมด้วย
 +
 +
[[Image:223-flask-consolelog.png|400px]]
  
 
=== git commit ===
 
=== git commit ===
แถว 130: แถว 170:
 
: '''🄶''' เมื่อแก้เสร็จแล้ว ให้ทดลองกดหน้าเว็บสักนิด ถ้าทุกอย่างโอเค อย่าลืม commit
 
: '''🄶''' เมื่อแก้เสร็จแล้ว ให้ทดลองกดหน้าเว็บสักนิด ถ้าทุกอย่างโอเค อย่าลืม commit
 
}}
 
}}
 +
 +
=== โครงสร้างปัจจุบัน ===
 +
 +
สำหรับโค้ดที่เรามีอยู่ในปัจจุบันจะพิจารณาโครงสร้างได้ดังรูปด้านล่าง
 +
 +
[[Image:223-web-flask-raw-html.jpeg|400px]]
 +
 +
Browser จะส่ง request มา โดยระบุ end point ผ่านทาง url &nbsp;&nbsp;&nbsp; ระบบ routing ภายใน Flask จะส่ง request นั้นต่อมาให้กับฟังก์ชันที่เราเขียนไว้ โดยใช้ url mapping ที่เราระบุผ่านทาง annotation <tt>@app.route</tt>  &nbsp;&nbsp;&nbsp; จากนั้นฟังก์ชันของเราจะคืน html กลับไป (เป็น string)
  
 
== การแสดง template ==
 
== การแสดง template ==
แถว 171: แถว 219:
 
: '''🄶''' เมื่อทดสอบว่าแสดงผลเรียบร้อย อย่าลืม commit อย่าลืม add ไฟล์ template ที่เราสร้างลงใน repo ก่อนด้วย
 
: '''🄶''' เมื่อทดสอบว่าแสดงผลเรียบร้อย อย่าลืม commit อย่าลืม add ไฟล์ template ที่เราสร้างลงใน repo ก่อนด้วย
 
}}
 
}}
 +
 +
ในคลิปแรกอาจจะมีการทดลองแก้โค้ดต่าง ๆ ให้ดูผล อย่าลืมลองทำตามและพิจารณาเหตุผลว่าทำไมได้รับผลลัพธ์ดังกล่าว
 +
 +
=== โครงสร้างปัจจุบัน ===
 +
 +
เราได้แยกการ render html ออกมาเป็นส่วน template แล้ว ทำให้โครงสร้างของระบบเราปรับเป็นรูปด้านล่าง
 +
 +
[[Image:223-web-flask-template.jpeg|400px]]
  
 
== การใช้งาน template ==
 
== การใช้งาน template ==
  
=== main.py ===
+
เราจะเริ่มในส่วนของ [https://www.youtube.com/watch?v=JMkKRhhnfRk คลิปที่สอง]
 +
 
 +
สิ่งที่คลิปนี้จะทำคือ
 +
* ในขั้นแรกเราจะทดลองส่งตัวแปร name และ time ไปยัง template
 +
* ส่งรายการข่าว news_items ไปที่ template และใช้ <tt>for</tt> template tag เพื่อแสดงรายการ
 +
* ทำลิงก์เพื่อแสดงรายละเอียดข่าว (body) แต่ละอัน
 +
* เพิ่มฟังก์ชัน <tt>show_news_item</tt> ที่รับ route <tt>/news/&lt;id&gt;/</tt> ที่แสดงข่าวผ่านทาง template <tt>news_item.html</tt>
 +
* ใช้ template tag <tt>url_for</tt> เพื่ออ้างถึง end point แทนที่จะเขียน url pattern ตรง ๆ
 +
 
 +
ให้ทดลองทำตามคลิป ด้านล่างจะเป็นรายละเอียดของบางขั้นตอน
 +
 
 +
=== แสดงรายการข่าว ===
 +
 
 +
เราจะเพิ่ม list ของรายการข่าวใน main.py ในอนาคต ข้อมูลส่วนนี้จะอ่านมาจาก database
 +
 
 +
ให้เพิ่มตัวแปร <tt>news_items</tt> ไว้ก่อนฟังก์ชัน index เราจะให้ตัวแปรดังกล่าวเป็น dict ที่มี key เป็น id ของข่าวแต่ละหัวข้อ และข้อมูลในข่าวเป็น dict ที่ประกอบด้วย id, title, และ body &nbsp;&nbsp;&nbsp; ที่เราใช้ dict กับ news_items เพราะว่าเราต้องการความสะดวกในการอ้างถึงข่าวแต่ละข่าวเวลาแสดงรายละเอียด
 +
 
 +
<syntaxhighlight lang="python">
 +
news_items = {
 +
    1: {'id': 1,
 +
        'title': 'COVID-19 update',
 +
        'body': 'This is a news on COVID-19'},
 +
    2: {'id': 2,
 +
        'title': 'Facemasks found',
 +
        'body': 'Recent news on facemask production'},
 +
    3: {'id': 3,
 +
        'title':'Python 4',
 +
        'body':'Python 4 will be out soon.... this is FAKE'},
 +
}
 +
</syntaxhighlight>
 +
 
 +
เราจะส่งตัวแปรดังกล่าวให้ template โดยเพิ่มไปใน argument ตอนเรียก render_template
 +
 
 +
<syntaxhighlight lang="python">
 +
def index():
 +
    # ...
 +
    return render_template('index.html',
 +
                          name=name,
 +
                          time=time,
 +
                          news_items=news_items.values())
 +
</syntaxhighlight>
 +
 
 +
เราใช้ news_items.values() เพราะต้องการส่งเป็น list ของรายการข่าว
 +
 
 +
ระหว่างการพัฒนาเราอาจจะทดลอง refresh หน้าเว็บเพื่อจะได้ดูว่าเราแก้ใน main.py ได้ถูกต้องหรือไม่  ซึ่งถ้าเราทำถูกต้อง หน้าเว็บก็จะแสดงได้ตามปกติ เพราะว่าเรายังไม่ได้ปรับ template เลย
 +
 
 +
ในการแสดงผล เราจะแก้ template index.html และใช้ for tag ในการไล่พิจารณาข้อมูลใน news_items ดังด้านล่าง
 +
 
 +
<syntaxhighlight lang="html">
 +
    <ul>
 +
      {% for item in news_items %}
 +
        <li>{{ item.title }}</li>
 +
      {% endfor %}
 +
    </ul>
 +
</syntaxhighlight>
 +
 
 +
{{กล่องฟ้า|
 +
: '''🄶''' ให้ลองกดแสดงผล ถ้ารายการขึ้นถูกต้องก็ควรจะ commit งาน
 +
}}
 +
 
 +
=== แสดงรายละเอียดแต่ละข่าว ===
 +
เราจะเพิ่มหน้าเว็บเพื่อแสดงรายละเอียด (body) ของแต่ละข่าว โดยทำเป็นลิงก์จากรายการ
 +
 
 +
หน้าเว็บที่แสดงข่าวจะมี url เฉพาะ โดยทั่วไปเราจะกำหนดให้ url ของหน้าที่เราจะสร้าง ระบุรายละเอียดของหน้าเว็บนั้นด้วย ยกตัวอย่างเช่น ถ้าเราต้องการแสดงข่าว ใน url ก็ควรจะระบุหมายเลขข่าวมาด้วย เพื่อที่ web application จะได้นำข่าวมาแสดงได้ถูกต้อง
 +
 
 +
สังเกตว่า '''เราอาจจะมองว่า url เป็นวิธี (api) ที่ browser เรียกขอข้อมูลมาจากเว็บแอพ''' ก็ได้
 +
 
 +
ดังนั้น เราจะสร้าง endpoint <tt>/news/<id>/</tt> เพื่อแสดงข่าวที่มีหมายเลขเท่ากับ id
 +
 
 +
เราจะปรับจากส่วน template ก่อน โดยแก้ template เพื่อใส่ลิงก์ดังด้านล่าง
 +
 
 +
<syntaxhighlight lang="html">
 +
    <ul>
 +
      {% for item in news_items %}
 +
        <li><a href="/news/{{ item.id }}/">{{ item.title }}</a></li>
 +
      {% endfor %}
 +
    </ul>
 +
</syntaxhighlight>
 +
 
 +
ให้ลอง refresh ว่าลิงก์แสดงหรือไม่ ถ้าแสดง ลองกดเล่นดูได้ (จะพบ error)
 +
 
 +
จากนั้นเราจะเพิ่มฟังก์ชัน <tt>show_news_item</tt> ที่รับ request จาก url ดังกล่าว &nbsp;&nbsp; สังเกตว่าใน <tt>@app.route</tt> เราจะระบุเป็น pattern ที่มีตัวแปร &lt;id&gt; และตัวแปรนี้จะถูกแกะออกมาส่งให้กับฟังก์ชัน <tt>show_news_item</tt> โดย Flask
 +
 
 +
<syntaxhighlight lang="python">
 +
@app.route('/news/<id>/')
 +
def show_news_item(id):
 +
    # ...
 +
    # ...
 +
</syntaxhighlight>
 +
 
 +
ในฟังก์ชันดังกล่าว จะทำงานเพียงแค่นำรายละเอียดจาก news_items[id] ส่งให้กับ template ดังนี้
 +
 
 +
<syntaxhighlight lang="python">
 +
@app.route('/news/<id>/')
 +
def show_news_item(id):
 +
    news_item = news_items[int(id)]
 +
    return render_template('news_item.html',
 +
                          id=news_item['id'],
 +
                          title=news_item['title'],
 +
                          body=news_item['body'])
 +
</syntaxhighlight>
 +
 
 +
ให้ refresh และลองกดลิงก์ข่าวอีกครั้ง จะพบ error เนื่องจากเรายังไม่ได้สร้าง template <tt>news_item.html</tt>
 +
 
 +
ให้เพิ่ม <tt>templates/news_item.html</tt> ดังนี้
 +
 
 +
<syntaxhighlight lang="html">
 +
<html>
 +
  <body>
 +
    <h1>{{ title }}</h1>
 +
    <p>{{ body }}</p>
 +
  </body>
 +
</html>
 +
</syntaxhighlight>
 +
 
 +
ให้ refresh หน้าเว็บ และลองกดลิงก์อีกครั้ง ลองกดหลาย ๆ ข่าว ถ้าแสดงผลข่าวต่าง ๆ ได้โอเคเราควรจะ
 +
 
 +
{{กล่องฟ้า|
 +
: '''🄶''' commit อย่าลืม add ไฟล์ template ที่เราสร้างลงใน repo ก่อนด้วย
 +
}}
 +
 
 +
==== UPDATE เพิ่มเติมจากในคลิป ====
 +
 +
สังเกตว่าเราต้องการให้ <id> match กับเฉพาะหมายเลขที่เป็นจำนวนเต็มเท่านั้น ใน Flask เราสามารถระบุ pattern ใน app.route ได้ดังนี้ (เพิ่ม int: เข้าไปด้านหน้าชื่อตัวแปร id) การใส่ pattern แบบเฉพาะเจาะจงนี้จะช่วยลดปัญหาการ match route ผิดพลาดได้ในอนาคต
 +
 
 +
<syntaxhighlight lang="python">
 +
@app.route('/news/<int:id>/')
 +
def show_news_item(id):
 +
    # ....
 +
</syntaxhighlight>
 +
 
 +
=== การใช้ url_for ===
 +
 
 +
หลังจากนั้น ในคลิปจะอธิบายการใช้ tag url_for เพื่อแยกการขึ้นต่อกัน (decouple) ของ template กับ url pattern &nbsp;&nbsp;&nbsp; การจัดการดังกล่าว ทำให้เราสามารถเปลี่ยน url pattern ใน <tt>@app.route</tt> ได้ โดยไม่ต้องไปไล่แก้ template ที่อาจจะมีมากมาย
 +
 
 +
หลักการดังกล่าว เรียกรวม ๆ กันว่าหลักการ
 +
 
 +
'''DRY: Don't Repeat Yourself'''
 +
 
 +
เป็นหลักการที่สำคัญมากในการพัฒนาซอฟต์แวร์
 +
 
 +
ให้ลองทำตามในคลิป และถ้าทุกอย่างทำงานได้ดี อย่าลืม
 +
 
 +
{{กล่องฟ้า|
 +
: '''🄶''' commit งาน
 +
}}
 +
 
 +
=== ไฟล์หลังจบคลิปที่สอง ===
 +
ด้านล่างจะเป็นไฟล์ต่าง ๆ หลังจบคลิปที่สอง
 +
 
 +
==== main.py ====
 
<syntaxhighlight lang="python">
 
<syntaxhighlight lang="python">
 
from flask import Flask
 
from flask import Flask
แถว 212: แถว 418:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== templates/index.html ===
+
==== templates/index.html ====
 
<syntaxhighlight lang="html">
 
<syntaxhighlight lang="html">
 
<html>
 
<html>
แถว 229: แถว 435:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== templates/show.html ===
+
==== templates/news_item.html ====
 
<syntaxhighlight lang="html">
 
<syntaxhighlight lang="html">
 
<html>
 
<html>
แถว 242: แถว 448:
  
 
== การสร้าง form ==
 
== การสร้าง form ==
=== main.py ===
+
 
 +
ในส่วนนี้จะเป็นโค้ดสำหรับคลิปที่ 3: [https://www.youtube.com/watch?v=FbcnJj3ivxs หัดเขียนเว็บด้วย Flask #3 - การใช้ form ในการส่งข้อมูล การใช้ redirect]  ให้ทดลองตามคลิป ในส่วนของรายละเอียดอาจจะมีการเพิ่มเติมให้ภายหลัง
 +
 
 +
: '''ให้ทดลองทำทีละขั้นตามคลิป และหมั่น commit งานเป็นระยะ ๆ ด้วย &nbsp;&nbsp;&nbsp; อย่า copy โค้ดด้านล่างแล้วนำไปรันเลย'''
 +
 
 +
=== การแสดงฟอร์มและการเรียกข้อมูลจาก request ===
 +
 
 +
เราจะเพิ่มฟอร์มในหน้าแรก ให้เพิ่ม element form ด้านล่าง ลงใน <tt>templates/index.html</tt>
 +
 
 +
<syntaxhighlight lang="html">
 +
    <form>
 +
      Title: <input name="title"/><br />
 +
      Body: <textarea name="body"></textarea><br />
 +
      <input type="submit" value="Save"/>
 +
    </form>
 +
</syntaxhighlight>
 +
 
 +
ให้ลอง refresh ดูว่าเห็นฟอร์มขึ้นมาหรือไม่
 +
 
 +
ในการจะส่งข้อมูลจากฟอร์มมาที่ Flask app เราจะต้องพิจารณาว่าจะใช้ end point ใด ในการประมวลผล ซึ่งเราจะต้องไประบุผ่านทาง attribute <tt>action</tt> ใน form
 +
 
 +
ดังนั้นเราจะไปสร้าง function เปล่า ๆ มาเตรียมไว้ก่อน ให้เพิ่มฟังก์ชัน <tt>create_news_item</tt> ลงใน <tt>main.py</tt> ดังนี้
 +
 
 +
<syntaxhighlight lang="python">
 +
@app.route('/news/create/')
 +
def create_news_item():
 +
    pass
 +
</syntaxhighlight>
 +
 
 +
จากนั้นเราจะชี้ action ของฟอร์มมาที่ฟังก์ชันนี้ โดยใช้ url_for &nbsp;&nbsp;&nbsp; ให้ปรับ tag form ให้เป็นดังด้านล่าง
 +
 
 +
<syntaxhighlight lang="html">
 +
    <form action="{{ url_for('create_news_item') }}">
 +
      Title: <input name="title"/><br />
 +
      Body: <textarea name="body"></textarea><br />
 +
      <input type="submit" value="Save"/>
 +
    </form>
 +
</syntaxhighlight>
 +
 
 +
ให้ refresh แล้วทดลองกดส่งฟอร์ม  ให้ลองอ่าน error message จะพบว่ามีการเข้ามาทำงานที่ฟังก์ชัน create_news_item แล้ว แต่ฟังก์ชันดังกล่าวไม่คืนค่าอะไร
 +
 
 +
หน้าจอ error จะมีข้อความว่า '''TypeError'''
 +
 
 +
TypeError: The view function for 'create_news_item' did not return a valid response. The function either returned None or ended without a return statement.
 +
 
 +
: '''หมายเหตุ''': ถ้าไม่ได้ error นี้ ให้ลองอ่านรายละเอียดเพิ่มเติม ถ้าเป็น error เกี่ยวกับการทำงานของฟังก์ชัน เช่น <tt>ValueError: invalid literal for int() with base 10: 'create'</tt> และมีบรรทัดที่มีปัญหาอยู่ในฟังก์ชัน <tt>show_news_item</tt> เป็นไปได้ว่า url /news/create/ จะถูก route ไปที่ฟังก์ชันดังกล่าว เพราะว่ามันตรงกับ pattern /news/<id>/ ที่ id=="create"  ซึ่งอาจจะเกิดขึ้นได้เพราะว่าเราไม่ได้เขียน pattern ให้รัดกุมพอ  ในกรณีนี้ ให้แก้ annotation ของฟังก์ชัน show_news_item ให้เป็น <tt>@app.route('/news/<int:id>/')</tt> (เพิ่ม int: ก่อนหน้าชื่อ parameter id) จะทำให้ route ดังกล่าวจะไม่ match กับ /news/create/  พอแก้แล้วให้ลองส่งฟอร์มใหม่
 +
 
 +
เมื่อ browser ส่งฟอร์มมาให้กับ web application, จะมีการส่งข้อมูลต่าง ๆ กลับมาด้วย &nbsp;&nbsp;&nbsp; ใน Flask เราจะเข้าถึงข้อมูลดังกล่าวผ่านทางตัวแปร <tt>request</tt> ซึ่งจะเป็นตัวแปร global ในโมดูล Flask &nbsp;&nbsp;&nbsp; เราจะเพิ่มการ import ที่ตอนต้น main.py ดังนี้
 +
 
 +
<syntaxhighlight lang="python">
 +
from flask import request
 +
</syntaxhighlight>
 +
 
 +
เราจะให้ฟังก์ชัน create_news_item พิมพ์ค่าที่ส่งมาใน request โดยเพิ่มบรรทัด print ดังด้านล่าง
 +
 
 +
<syntaxhighlight lang="python">
 +
@app.route('/news/create/')
 +
def create_news_item():
 +
    print(request.args)
 +
</syntaxhighlight>
 +
 
 +
ให้ทดลองส่งฟอร์มอีกครั้งเพื่อดูผลลัพธ์  ที่หน้า page เราจะเห็น error เพราะว่าฟังก์ชันไม่ได้คืนค่า แต่ถ้าดูใน terminal (console) ที่เรียก flask run ทิ้งไว้จะเห็นว่าใน request.args เป็น ImmutableMultiDict ดังรูปด้านล่าง ที่เก็บข้อมูลจากฟอร์มที่เราส่งมา
 +
 
 +
[[Image:223-flask-requestargs.png|500px]]
 +
 
 +
อย่างไรก็ตาม การส่งข้อมูลฟอร์มแบบที่เราทำข้างต้นเป็นการใช้งานที่ไม่ค่อยจะถูกต้องนัก
 +
 
 +
ในรูปแบบการติดต่อแบบ HTTP จะมีการระบุรูปแบบในการขอข้อมูลไว้หลายแบบ ที่เราจะเห็นในเอกสารนี้มีสองแบบคือ
 +
 
 +
* GET - ใช้สำหรับขอข้อมูล  โดยหลักการพื้นฐาน การเรียก GET request จะต้องไม่ทำให้ข้อมูลเปลี่ยนสถานะ
 +
* POST - โดยมากใช้สำหรับการเพิ่มหรือแก้ไขข้อมูล
 +
 
 +
(หมายเหตุยังมี PUT, UPDATE, DELETE แต่เราจะยังไม่ใช้)
 +
 
 +
ประเภทของ request เหล่านี้ จะถูกแสดงใน Flask development server เวลาที่มีการประมวลผลการเรียกใช้ (ลองสังเกตดู)
 +
 
 +
จากที่เขียนมาข้างต้น เราจึงไม่ควรประมวลผลฟอร์มด้วย GET &nbsp;&nbsp; ดังนั้นในส่วนถัดไปเราจะปรับรูปแบบให้เป็น POST
 +
 
 +
=== การประมวลผลฟอร์ม และการ redirect ===
 +
 
 +
การระบุให้การส่ง request ของฟอร์มเป็นการส่งแบบ POST ก็ทำได้โดยการระบุ attribute ใน tag form ดังด้านล่าง
 +
 
 +
<syntaxhighlight lang="html">
 +
    <form action="{{ url_for('create_news_item') }}" method="POST>
 +
      ...
 +
    </form>
 +
</syntaxhighlight>
 +
 
 +
ให้ทดลองกดส่งฟอร์มอีกครั้ง จะพบ error เพราะว่าใน Flask ถ้าไม่ระบุอะไร @app.route จะรับแค่ GET request เท่านั้น  เราจะแก้ annotation ของฟังก์ชัน create_new_item โดยเพิ่มการระบุ methods ดังนี้  (และเราเพิ่มให้คืนสตริงว่างด้วย จะได้ไม่เห็น error ที่หน้าเว็บเพจ)
 +
 
 +
<syntaxhighlight lang="python">
 +
@app.route('/news/create/', methods=['POST'])
 +
def create_news_item():
 +
    print(request.form)        # change from request.args to request.form
 +
    return ''                  # added to show empty page instead of error
 +
</syntaxhighlight>
 +
 
 +
สังเกตว่า เราจะอ้างถึงข้อมูลที่ส่งมาทาง POST request ด้วย request.form แทนที่จะเป็น request.args ซึ่งจะเป็นข้อมูลที่ส่งมาทาง GET (ซึ่งจะโผล่ใน url)
 +
 
 +
ให้ทดลองกดส่งฟอร์มอีกครั้ง และดูผลลัพธ์ที่ใน console ด้วย ว่าโปรแกรมพิมพ์อะไรออกมาบ้าง
 +
 
 +
เราจะรับข้อมูลดังกล่าว แล้วนำมาเพิ่มใน news_items &nbsp;&nbsp;&nbsp; เพื่อให้โค้ดเป็นที่เป็นทาง เราจะเพิ่มฟังก์ชัน new_news_item เพื่อทำงานดังกล่าว
 +
 
 +
ในคลิปจะทดลองทำหลาย ๆ ขั้น (ตั้งแต่กำหนดให้ id=100) ให้ทดลองทำตาม แต่โค้ดสุดท้ายที่คืน news_item ที่ถูกต้องเป็นดังด้านล่าง
 +
 
 +
<syntaxhighlight lang="python">
 +
def new_news_item(title, body):
 +
    new_id = max(news_items.keys()) + 1
 +
    return {
 +
        'id': new_id,
 +
        'title': title,
 +
        'body': body
 +
    }
 +
</syntaxhighlight>
 +
 
 +
และปรับฟังก์ชัน create_news_item ให้เรียกใช้ new_news_item ดังด้านล่าง
 +
 
 +
<syntaxhighlight lang="python">
 +
@app.route('/news/create/', methods=['POST'])
 +
def create_news_item():
 +
    item = new_news_item(request.form['title'],
 +
                        request.form['body'])
 +
    news_items[item['id']] = item
 +
    return ''
 +
</syntaxhighlight>
 +
 
 +
ให้ทดลองส่งฟอร์ม เมื่อส่งแล้วจะเห็นหน้าจอว่าง ๆ แต่ถ้ากลับไปโหลดหน้าแรกใหม่ ข่าวใหม่ที่เราเพิ่มเข้าไปน่าจะปรากฏขึ้น (ห้าม restart flask เพราะว่าข้อมูลใหม่จะหายไปหมด)
 +
 
 +
ก่อนจะไปต่อ ถ้าแก้เรียบร้อย
 +
{{กล่องฟ้า|
 +
: '''🄶''' ได้เวลา commit แล้ว
 +
}}
 +
 
 +
เราจะทำให้การส่งฟอร์มเสร็จสมบูรณ์ หลังประมวลผล POST request โดยการ redirect กลับไปหน้าที่เหมาะสม เช่น หน้าแสดงข่าวใหม่ หรือหน้าหลักของเรา  เราจะใช้ฟังก์ชัน <tt>redirect</tt> ใน Flask ซึ่งต้องไป import มาก่อน ให้เพิ่มฟังก์ชันดังกล่าวในรายการ import ตอนต้นไฟล์ main.py
 +
 
 +
<syntaxhighlight lang="python">
 +
from flask import redirect
 +
</syntaxhighlight>
 +
 
 +
และจบฟังก์ชัน create_new_item โดยการคืน rediect (แทนที่จะเป็น return '') ดังนี้
 +
 
 +
<syntaxhighlight lang="python">
 +
@app.route('/news/create/', methods=['POST'])
 +
def create_news_item():
 +
    # ...
 +
    # ...
 +
    return redirect(url_for('index'))
 +
</syntaxhighlight>
 +
 
 +
ให้ทดสอบว่าสามารถทำงานได้ถูกต้อง เมื่อทดสอบเสร็จแล้ว
 +
{{กล่องฟ้า|
 +
: '''🄶''' อย่าลืม commit งานที่ทำด้วย
 +
}}
 +
 
 +
=== การส่งงานสัปดาห์นี้ ===
 +
 
 +
'''การส่งงานสัปดาห์นี้''': เมื่อทำขั้นตอนนี้เสร็จสิ้น  ให้ส่ง repository ที่ทำนี้ด้วย โดยสร้าง gihub repository และ push งานทั้งหมดที่ทำไปที่ repo นั้น (มันจะมาพร้อม history) แล้วนำ url มาแปะใน google sheet ที่จะระบุให้ส่งงานต่อไป
 +
 
 +
=== โค้ดหลังจบคลิปที่ 3 ===
 +
 
 +
==== main.py ====
 
<syntaxhighlight lang="python">
 
<syntaxhighlight lang="python">
 
from flask import Flask
 
from flask import Flask
แถว 297: แถว 663:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== templates/index.html ===
+
==== templates/index.html ====
 
<syntaxhighlight lang="html">
 
<syntaxhighlight lang="html">
 
<html>
 
<html>
แถว 319: แถว 685:
 
</html>
 
</html>
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
== Stylesheet (CSS) ==
 +
 +
เนื้อหาส่วนนี้ไม่มีใน clip ให้ทดลองทำตามด้านล่างได้เลย  (อย่างไรก็ตาม สามารถดูคลิปพื้นฐานเกี่ยวกับ css ได้ที่นี่ [https://www.youtube.com/watch?v=-hbEX7UYN5Y youtube css intro])
 +
 +
เราได้เห็นการแยกโค้ดส่วนของการจัดการข้อมูล (ใน python) และส่วนแสดงผลออกจากกัน โดยเราแยกส่วนแสดงผลให้เป็น template &nbsp;&nbsp;&nbsp; ในส่วนนี้เราจะแยกส่วนการจัดการ style (ความสวยงาม) ออกจากเนื้อหาใน template อีกที
 +
 +
เราจะสร้างไฟล์เพื่อเก็บรายละเอียดของ style และส่งไฟล์นั้นให้กับ browser เพื่อระบุวิธีการจัดการความสวยงาม &nbsp;&nbsp; ไฟล์เหล่านี้จะเป็นเนื้อหาส่วนที่ไม่มีการเปลี่ยนแปลง (เรียกรวม ๆ ว่าเป็น static content) ดังนั้นเรามักจะไม่ให้ web app เป็นผู้รับผิดชอบในการจัดการ
 +
 +
ใน Flask ไฟล์พวกนี้จะถูกเก็บแยกไว้ในไดเรกทอรี <tt>static</tt> และบน development server จะสามารถเรียกใช้ได้ทันที แต่ในการใช้งานจริงบน production เราจะต้องจัดการหาวิธีส่งไฟล์พวกนี้ให้กับ browser แบบอื่น (จะได้เรียนรายละเอียดต่อไป)
 +
 +
เราจะสร้างไฟล์ style.css ในไดเรกทอรี <tt>static/css<tt> ให้สร้าง ไดเรกทอรี <tt>static</tt> และสร้างไดเร็กทอรี <tt>css</tt> ไว้ด้านในอีกที ก่อนจะสร้างไฟล์ style.css
 +
 +
ในตอนแรก ให้เพิ่มรายละเอียดด้านล่างลงในไฟล์ style.css
 +
 +
<syntaxhighlight lang="css">
 +
h1 {
 +
  background: gray;
 +
  padding: 20px;
 +
}
 +
</syntaxhighlight>
 +
 +
จากนั้นให้ลอง refresh หน้าเว็บว่าเห็นการเปลี่ยนแปลงหรือไม่  จะพบว่าไม่มีผลอะไร เพราะว่าเรายังไม่ได้ระบุใน html ว่าให้ใช้ไฟล์ดังกล่าวในการ style หน้าเว็บของเรา
 +
 +
เราจะระบุผ่านทาง tag <tt>link</tt> ซึ่งนิยมเอาไว้ในส่วนของ <tt>head</tt> ของเอกสาร html
 +
 +
ให้เพิ่ม section <tt>head</tt> ที่มีการระบุ tag <tt>link</tt> ลงในไฟล์ <tt>templates/index.html</tt> ดังด้านล่าง
 +
 +
<syntaxhighlight lang="html">
 +
<html>
 +
  <head>
 +
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
 +
  </head>
 +
  <body>
 +
    ...
 +
  </body>
 +
</html>
 +
</syntaxhighlight>
 +
 +
สังเกตว่าเรามีการใช้ url_for และระบุฟังก์ชัน static เพื่อสร้าง url สำหรับการส่ง static file &nbsp;&nbsp;&nbsp; ความสามารถส่วนนี้เป็นสิ่งที่ Flask เตรียมไว้สำหรับการพัฒนา
 +
 +
ให้ลอง refresh หน้ารายการของเราอีกครั้ง จะเห็นการเปลี่ยนแปลง  ให้ลองอธิบายว่า style sheet ของเราระบุอะไร และทำให้เกิดผลอะไรกับการเปลี่ยนแปลงนี้
 +
 +
ให้ลองกดไปดูที่หน้าข่าวแต่ละหัวข้อ จะพบว่า h1 ยังแสดงผลแบบเดิม  ให้เข้าไปแก้ไฟล์ <tt>templates/news_item.html</tt> ให้มีการลิงก์ไปยัง style sheet ดังกล่าวด้วย
 +
 +
'''หมายเหตุ:''' การที่ต้องแก้ template ซ้ำ ๆ หลายครั้งเมื่อต้องการปรับหน้าตาของเว็บ เช่นเพิ่ม link css ให้กับทุกหน้า เป็นสิ่งที่สร้างความลำบากและปัญหามาก เฟรมเวิร์คในการพัฒนาเว็บที่ดีจะต้องทำให้เราลดภาระพวกนี้ได้ ใน Flask เราจะสามารถลดปัญหาดังกล่าวด้วย template inheritance ซึ่งเราจะได้เรียนต่อไป
 +
 +
=== CSS Selector ===
 +
 +
คำว่า css ย่อมาจาก cascading style sheet คำว่า cascade (มีการลดหลั่น หรือมีลักษณะคล้ายน้ำตก) หมายถึงหลักการในการให้ความสำคัญของกฎต่าง ๆ เวลามีกฎระบุ style ใหักับ element บนหน้าจอหลายอัน
 +
 +
สิ่งที่อยู่ในไฟล์ style sheet คือ กฎ (rule) ที่ระบุ style ซึ่งจะอยู่ในรูปแบบ
 +
 +
selectors {
 +
  css declaration1;
 +
  css declaration2;
 +
  css declaration3;
 +
  ...
 +
}
 +
 +
โดย selector (อาจมีได้หลายอัน) จะเลือกว่ากฎนี้จะทำงานกับ element ใด และ css declarations จะระบุค่าให้กับ property ต่าง ๆ เช่น color, border, font เป็นต้น
 +
 +
ทดลองแก้ไฟล์ style.css โดยเพิ่มกฎ li ดังด้านล่าง ก่อนจะ refresh ให้ลองคิดก่อนว่าหน้าตาเว็บของเราจะเป็นอย่างไร
 +
 +
<syntaxhighlight lang="css">
 +
li {
 +
  background-color: greenyellow;
 +
  border: 1px solid black;
 +
  margin-bottom: 5px;
 +
}
 +
</syntaxhighlight>
 +
 +
'''ก่อนจะทดลองต่อ ให้ลบกฎ li ด้านบนออกก่อน ไม่งั้นจะดูผลลัพธ์ได้ยาก'''
 +
 +
CSS selector นั้นจะเลือก element ในหน้าเว็บได้หลายแบบ แต่เราจะต้องเข้าใจคุณสมบัติพื้นฐานของแต่ละ element บนหน้าเว็บก่อน
 +
 +
แต่ละ element บนหน้าเว็บจะมี
 +
 +
* '''id''' - แทนชื่อของ element นั้น ในแต่ละ document จะต้องไม่มี element ที่ id ซ้ำกัน (โดยหลักการ)
 +
* '''class''' - แต่ละ element จะมี class ได้หลายอัน แสดงการเป็นสมาชิกของ element ต่อประเภทหลาย ๆ ประเภท
 +
 +
ดังนั้น css selector จะมีรูปแบบพื้นฐานมีสองแบบเช่นเดียวกัน คือ
 +
 +
* '''การเลือกด้วย class''' ตัวอย่างเช่น ถ้าจะเลือก element ทั้งหมดที่มี class <tt>news-items</tt> จะเขียน selector เป็น <tt>.news-items</tt> (ใช้จุดนำหน้า)
 +
* '''การเลือกด้วย id''' ตัวอย่างเช่น ถ้าจะเลือก element ที่มี id <tt>news_item_2</tt> จะเขียน selector เป็น <tt>#news_item_2</tt> (ใช้ # นำหน้า)
 +
 +
ให้แก้ไฟล์ <tt>templates/index.html</tt> ให้เป็นดังด้านล่างก่อนที่จะทำต่อ เพื่อที่เราจะได้มี element ที่มากพอและซับซ้อนพอที่จะทดลองกฎใน css ได้
 +
 +
<syntaxhighlight lang="html">
 +
<html>
 +
  <head>
 +
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
 +
  </head>
 +
  <body>
 +
    <h1>Hello!! {{ name }}</h1>
 +
    Hi. This is {{ time }}.  This is recent news.
 +
    <ul id="main_news_list">
 +
      {% for item in news_items %}
 +
        <li class="news-items" id="news_item_{{ item.id }}">
 +
          <a href="{{ url_for('show_news_item', id=item.id) }}">{{ item.title }}</a>
 +
        </li>
 +
      {% endfor %}
 +
    </ul>
 +
 +
    This is the first dummy list
 +
    <ul>
 +
      <li class="news-items">
 +
        <span class="highlight">Somying</span> is the first year student.
 +
      </li>
 +
      <li class="news-items">
 +
        Somchai is the second year student
 +
      </li>
 +
      <li class="news-items">Sommai</li>
 +
      <li class="news-items">Somjai</li>
 +
    </ul>
 +
 +
    This is the <span class="highlight">second dummy list</span>:
 +
    <ul>
 +
      <li>breakfast</li>
 +
      <li id="lunch_item">lunch</li>
 +
      <li>dinner</li>
 +
    </ul>
 +
 +
    <form action="{{ url_for('create_news_item') }}" method="POST">
 +
      Title: <input name="title"/><br />
 +
      Body: <textarea name="body"></textarea><br />
 +
      <input type="submit" value="Save"/>
 +
    </form>
 +
  </body>
 +
</html>
 +
</syntaxhighlight>
 +
 +
==== การทดลองที่ 1 ====
 +
 +
ให้เพิ่มกฎต่อไปนี้ลงใน style.css ให้พิจารณากฎและหน้าเว็บและคาดเดาผลลัพธ์ที่เกิดขึ้น ก่อนจะ refresh และสังเกตผลจริง ๆ
 +
 +
<syntaxhighlight lang="css">
 +
.news-items {
 +
  margin: 10px 0;
 +
}
 +
 +
#main_news_list .news-items {
 +
  border: 1px solid gray;
 +
  padding: 5px;
 +
}
 +
 +
.highlight {
 +
  color: red;
 +
}
 +
</syntaxhighlight>
 +
 +
==== แบบฝึกหัด ====
 +
 +
แก้ style.css เพื่อให้ผลลัพธ์เป็นดังด้านล่าง
 +
 +
[[Image:223-web-flask-css.png|500px]]
 +
 +
ควรปรับการแสดงผลดังนี้
 +
 +
* รายการข่าวที่สอง เปลี่ยน background เป็นสีเขียว ปรับฟอนต์ใหญ่ขึ้น เปลี่ยนเป็นสีขาว  (สี text อาจจะต้องแก้ที่ element <a>)
 +
* ปรับให้ข้อความใน highlight ตัวใหญ่ และ background สีเหลือง แต่ปรับเฉพาะที่อยู่ใน first dummy list
 +
 +
=== Bootstrap ===
 +
 +
ถ้าต้องการหน้าจอเว็บที่สวย (แต่อาจจะดูโบราณนิด) เราสามารถใช้ bootstrap css library ได้ สามารถดูคลิปเกี่ยวกับการใช้งานพื้นฐานได้ที่ [https://www.youtube.com/watch?v=4hnR949BFqY youtube Bootstrap]
 +
 +
== เอกสารเพิ่มเติม ==
 +
 +
: ''จะเพิ่มเร็ว ๆ นี้''

รุ่นแก้ไขปัจจุบันเมื่อ 22:01, 26 ธันวาคม 2568

เป็นส่วนหนึ่งของวิชา 01204223

ในขั้นแรกของการฝึกพัฒนาโปรแกรมประยุกต์บนเว็บ เราจะเริ่มโดยการใช้ Flask ที่เป็นเฟรมเวิร์คในการพัฒนาเว็บบน Python ที่มีความเรียบง่าย เพื่อให้เราให้ภาพรวมคร่าว ๆ ก่อน จากนั้นเราจะขยับขยายไปใช้ระบบอื่น ๆ ต่อไป

เอกสารนี้จะเป็นเอกสารประกอบการเรียนด้วยคลิปตามรายการด้านล่าง

แต่จะมีการเพิ่มเนื้อหา css ในตอนท้ายเอกสาร

ในคลิปดังกล่าวจะไม่ได้มีการใช้ git ประกอบการทำ แต่เราจะพยายามแสดงจุดที่ควรจะ commit งานเป็นระยะ ดังนั้นระหว่างการหัดใช้ Flask ให้ทดลองใช้ git ไปด้วยเลย

Web application architecture

สถาปัตยกรรมของเว็บแอพลิเคชันนั้น เป็นสถาปัตยกรรมแบบ client-server โดยมี browser เป็น client และเชื่อมต่อกับ web server

223-web-client-server-arch.jpeg

อย่างไรก็ตาม ภาพดังกล่าวเป็นภาพที่เราเห็นในระยะไกลมากเท่านั้น จริง ๆ แล้วด้านในของแต่ละชิ้นส่วนอาจจะมีส่วนประกอบที่ทำงานภายในมากมาย

ในช่วงแรกที่เราหัดพัฒนาเว็บ เราจะสนใจในส่วน server side ก่อน โดยจะพิจารณาว่า browser มีหน้าที่แสดงผลลัพธ์จาก response ที่เป็น html เพียงอย่างเดียว

ในช่วงถัดไป พอเราได้เรียนรู้เกี่ยวกับ javascript library สำหรับการจัดการฝั่ง client แล้ว เราจะค่อย ๆ ปรับให้ฝั่ง client (ที่ทำงานบน browser) ให้มีบทบาทมากขึ้นต่อไป

เริ่มต้น และการสร้าง repository

เราจะสร้าง directory สำหรับทำโครงงาน todo list แบบง่าย (ไม่มี database) ให้สร้างไดเร็กทอรีและย้ายไปในไดเร็กทอรีนั้น แล้วสั่ง

git init 

เพื่อเริ่มสร้าง git repository สำหรับการทำงาน

การติดตั้งและเริ่มเปิดหน้าเว็บ

Flask เป็น library บน python ซึ่งเราจะติดตั้งผ่านทางคำสั่ง pip อย่างไรก็ตาม เราจะพยายามแบ่งส่วนการติดตั้ง library ของ python ให้แยกกันไปตามแต่ละงาน โดยใช้ virtual environment (venv) ดังนั้นก่อนจะติดตั้ง Flask เราจะสร้าง virtual environment ก่อน

หมายเหตุ: ถ้าใช้ windows ให้เปิด terminal แบบ command (cmd) อย่าเปิด powershell

โดยสั่ง

python -m venv venv

(พารามิเตอร์ venv ตัวแรกเป็นชื่อ module ใน python พารามิเตอร์ venv ตัวที่สองเป็นชื่อไดเร็กทอรีที่จะเก็บ environment ซึ่งเราเปลี่ยนเป็นชื่ออื่นได้)

ถ้าจะเริ่มใช้จะเรียกคำสั่ง activate ที่อยู่ใน venv การเรียกนี้จะแตกต่างกันตาม os

ถ้าใช้ Linux / Mac ให้เรียก

venv/bin/activate 

ถ้าใช้ Windows ให้เรียก

venv\Scripts\activate

ถ้าเรียก activate แล้ว prompt ใน terminal จะเปลี่ยนไป (ลองสังเกต) จากนั้นเราจะสามารถสั่งให้ติดตั้ง Flask ได้ โดยสั่ง

pip install Flask

Flask จะมาพร้อม development web server เพื่อการพัฒนาและทดสอบเบื้องต้น เราจะเรียกโดยสั่ง

flask run

ซึ่งในตอนแรกถ้าเรายังไม่ได้เขียนอะไร มันจะแสดง error ออกมา

main.py

เราจะเริ่มโดยการสร้าง main.py ดังด้านล่าง

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

เราต้องตั้งค่าใน environment ก่อนว่าโปรแกรมหลักเราชื่ออะไร

ถ้าใช้ windows สั่ง

set FLASK_APP=main.py

ถ้าใช้ unix (linux,mac) สั่ง

export FLASK_APP=main.py

และสั่งให้ Flask ทำงานใน terminal โดยสั่ง

flask run

จากนั้นเราจะสามารถเปิดเว็บได้ที่ลิงก์ http://localhost:5000 (โดยปกติ Flask จะเปิดเว็บเซิร์ฟเวอร์ไว้ที่พอร์ต 5000)

ในคลิปจะมีการทำให้ flask ทำงานใน DEBUG mode โดยการตั้งค่า FLASK_ENV ซึ่งใช้ได้กับ Flask เวอร์ชันเก่า ในที่นี้ เราจะสั่งโดยเรียก

flask run --debug

แทน

การทำงานใน debug mode ทำให้สิ่งที่เราแก้มีผลทันที อย่าลืมเรียกแบบนี้ทุกครั้ง ไม่เช่นนั้นอาจจะสับสนใจได้ ว่าทำไมสิ่งที่เราแก้ในโค้ดไม่มีผลต่อการทำงาน

ผลลัพธ์บน terminal (console)

ในการพัฒนาเว็บ นอกจากเราจะดูผลลัพธ์ทางหน้าจอบน browser แล้ว ใน terminal ที่เราเรียก flask run ก็ยังคงพิมพ์รายละเอียดของ request ต่าง ๆ ที่เข้ามาที่ development server ด้วย     ดังนั้นอย่าลืมใช้รายละเอียดเหล่านี้เป็นช่องทางในการตรวจสอบและติดตามการทำงานของโปรแกรมด้วย

223-flask-consolelog.png

git commit

ถ้าเริ่มแสดงหน้าเว็บได้ ก็เป็นจุดที่ดีที่เราควรจะ commit เนื่องจากเราใช้ terminal ใน vscode ในการรัน flask run (ซึ่งเราจะรันทิ้งไว้เลย) ในการสั่ง git ควรจะต้องเปิดอีก terminal หนึ่ง

เราจะเพิ่ม main.py ลงไปใน repo โดยสั่ง

git add main.py

ignore venv. ปกติเราจะไม่เก็บ virtual environment ลงใน version control ดังนั้นให้สร้างไฟล์ .gitignore และเพิ่มบรรทัด

venv/

ลงไปในนั้นด้วย เพื่อบอก git ให้ไม่ต้องสนใจไดเร็กทอรีดังกล่าว จากนั้นให้เพิ่ม .gitignore ลงใน repo โดยสั่ง

git add .gitignore

อาจจะสั่ง git status ดูอีกทีว่าเราเพิ่มไฟล์ครบแล้ว เมื่อเรียบร้อยแล้วให้สั่ง git commit

git commit -m "first hello page"

เราจะใส่ commit message สั้น ๆ ให้พอทราบว่าในแต่ละขั้นเราทำอะไร

ในการทำงานต่อไป จะมีกล่องสีฟ้าดังด้านล่างเพื่อบอกเตือนจุดที่ควรจะ commit

🄶 ได้เวลา commit แล้ว

ให้ add ไฟล์ที่เกี่ยวข้องและ commit งานที่ทำขณะนั้นลงใน git เลือก commit message ให้เหมาะสมด้วย

route

การเรียกใช้งานเว็บแอพลิเคชันทำผ่านทางการส่ง request มาที่ web server โดยสิ่งที่ระบุเป้าหมายของการทำงานคือ url (ดูตรงหัวของ browser) ตัวอย่างของ url เช่น

ในการเขียน Flask เราจะระบุให้ฟังก์ชันใด เป็นฟังก์ชันที่รับผิดชอบเป้าหมาย (target หรือ end point) ใด ๆ ผ่านทาง annotation @app.route ดังตัวอย่างด้านบนที่เราระบุ

@app.route("/")

ไว้ก่อนฟังก์ชัน ซึ่งเป็นการแจ้งกับ web server ของ Flask ว่าถ้ามี request มาที่ / ให้เรียกใช้ฟังก์ชันดังกล่าว

เราจะเพิ่มอีกฟังก์ชันเพื่อรับ request ที่ /hello/ ดังด้านล่าง

@app.route('/hello/')
def hello_world1():
    return 'Hi my name is someone'

ให้ทดลองเรียกใช้ และเรียกไปยัง url http://localhost:5000/hello/

🄶 ถ้าเปิดได้ ให้ commit ไว้ก่อนเลย

เรามักจะตั้งชื่อ function ให้สื่อความหมาย เดี๋ยวให้แก้ main.py โดยลบ hello_world1 ที่เราทดสอบ route ออกและแก้ชื่อฟังก์ชัน hello_world เป็น index เพื่อเตรียมหัดใช้ template ในส่วนต่อไป

🄶 เมื่อแก้เสร็จแล้ว ให้ทดลองกดหน้าเว็บสักนิด ถ้าทุกอย่างโอเค อย่าลืม commit

โครงสร้างปัจจุบัน

สำหรับโค้ดที่เรามีอยู่ในปัจจุบันจะพิจารณาโครงสร้างได้ดังรูปด้านล่าง

223-web-flask-raw-html.jpeg

Browser จะส่ง request มา โดยระบุ end point ผ่านทาง url     ระบบ routing ภายใน Flask จะส่ง request นั้นต่อมาให้กับฟังก์ชันที่เราเขียนไว้ โดยใช้ url mapping ที่เราระบุผ่านทาง annotation @app.route     จากนั้นฟังก์ชันของเราจะคืน html กลับไป (เป็น string)

การแสดง template

ในการเขียนโปรแกรมที่ดีนั้น เราจะแยกส่วนของโค้ดออกตามความรับผิดชอบและภาระหน้าที่ สิ่งหนึ่งที่เราพยายามแยกจากกันคือส่วนประมวลผลกับส่วนแสดงผล (จัดการหน้าจอต่างๆ) โค้ดที่เขียนมาในตอนต้นมีโค้ด html (สำหรับนำเสนอ) ปนอยู่กับโค้ด python (สำหรับประมวลผล) ซึ่งเป็นเครื่องหมายที่ดีว่าเราน่าจะต้องแยกของสองอย่างนี้ออกจากกัน

เราจะแยกส่วนแสดงผลออกมาไว้ใน template สาเหตุที่เรียกว่า template เพราะว่าในส่วนนี้เรายังสามารถเขียนโค้ดบางอย่างเพื่อใช้ในการแสดงผลได้ เช่นการทำซ้ำ แต่ลักษณะโครงสร้างของไฟล์ template นั้นจะเหมือนเป็นไฟล์ html ที่มีการเจาะช่องเป็นโครงเอาไว้เพื่อให้เราใส่ข้อมูลไปแสดงผลได้

เราจะ import function render_template

from flask import render_template

จากนั้นปรับฟังก์ชัน index ให้แสดงผลจากการ render template index.html

@app.route('/')
def index():
    return render_template('index.html')

ให้ทดลองเรียกเว็บ จะพบ error แจ้งเตือนว่าไม่พบไฟล์ index.html เนื่องจากเรายังไม่ได้สร้าง template เลย

ให้สร้างไดเร็กทอรี templates และเพิ่มไฟล์ templates/index.html ดังด้านล่าง

Hello! This is my first template.

ทดลองดูว่าข้อความดังกล่าวแสดงหรือไม่ ถ้าแสดงผลเรียบร้อย ให้แก้ไฟล์ให้อยู่ในรูปแบบ html ที่ดีขึ้นดังด้านล่าง

<html>
  <body>
    <h1>Hello!!</h1>
    Hi
  </body>
</html>
🄶 เมื่อทดสอบว่าแสดงผลเรียบร้อย อย่าลืม commit อย่าลืม add ไฟล์ template ที่เราสร้างลงใน repo ก่อนด้วย

ในคลิปแรกอาจจะมีการทดลองแก้โค้ดต่าง ๆ ให้ดูผล อย่าลืมลองทำตามและพิจารณาเหตุผลว่าทำไมได้รับผลลัพธ์ดังกล่าว

โครงสร้างปัจจุบัน

เราได้แยกการ render html ออกมาเป็นส่วน template แล้ว ทำให้โครงสร้างของระบบเราปรับเป็นรูปด้านล่าง

223-web-flask-template.jpeg

การใช้งาน template

เราจะเริ่มในส่วนของ คลิปที่สอง

สิ่งที่คลิปนี้จะทำคือ

  • ในขั้นแรกเราจะทดลองส่งตัวแปร name และ time ไปยัง template
  • ส่งรายการข่าว news_items ไปที่ template และใช้ for template tag เพื่อแสดงรายการ
  • ทำลิงก์เพื่อแสดงรายละเอียดข่าว (body) แต่ละอัน
  • เพิ่มฟังก์ชัน show_news_item ที่รับ route /news/<id>/ ที่แสดงข่าวผ่านทาง template news_item.html
  • ใช้ template tag url_for เพื่ออ้างถึง end point แทนที่จะเขียน url pattern ตรง ๆ

ให้ทดลองทำตามคลิป ด้านล่างจะเป็นรายละเอียดของบางขั้นตอน

แสดงรายการข่าว

เราจะเพิ่ม list ของรายการข่าวใน main.py ในอนาคต ข้อมูลส่วนนี้จะอ่านมาจาก database

ให้เพิ่มตัวแปร news_items ไว้ก่อนฟังก์ชัน index เราจะให้ตัวแปรดังกล่าวเป็น dict ที่มี key เป็น id ของข่าวแต่ละหัวข้อ และข้อมูลในข่าวเป็น dict ที่ประกอบด้วย id, title, และ body     ที่เราใช้ dict กับ news_items เพราะว่าเราต้องการความสะดวกในการอ้างถึงข่าวแต่ละข่าวเวลาแสดงรายละเอียด

news_items = {
    1: {'id': 1, 
        'title': 'COVID-19 update', 
        'body': 'This is a news on COVID-19'},
    2: {'id': 2, 
        'title': 'Facemasks found', 
        'body': 'Recent news on facemask production'},
    3: {'id': 3,
        'title':'Python 4', 
        'body':'Python 4 will be out soon.... this is FAKE'},
}

เราจะส่งตัวแปรดังกล่าวให้ template โดยเพิ่มไปใน argument ตอนเรียก render_template

def index():
    # ...
    return render_template('index.html', 
                           name=name, 
                           time=time,
                           news_items=news_items.values())

เราใช้ news_items.values() เพราะต้องการส่งเป็น list ของรายการข่าว

ระหว่างการพัฒนาเราอาจจะทดลอง refresh หน้าเว็บเพื่อจะได้ดูว่าเราแก้ใน main.py ได้ถูกต้องหรือไม่ ซึ่งถ้าเราทำถูกต้อง หน้าเว็บก็จะแสดงได้ตามปกติ เพราะว่าเรายังไม่ได้ปรับ template เลย

ในการแสดงผล เราจะแก้ template index.html และใช้ for tag ในการไล่พิจารณาข้อมูลใน news_items ดังด้านล่าง

    <ul>
      {% for item in news_items %}
        <li>{{ item.title }}</li>
      {% endfor %}
    </ul>
🄶 ให้ลองกดแสดงผล ถ้ารายการขึ้นถูกต้องก็ควรจะ commit งาน

แสดงรายละเอียดแต่ละข่าว

เราจะเพิ่มหน้าเว็บเพื่อแสดงรายละเอียด (body) ของแต่ละข่าว โดยทำเป็นลิงก์จากรายการ

หน้าเว็บที่แสดงข่าวจะมี url เฉพาะ โดยทั่วไปเราจะกำหนดให้ url ของหน้าที่เราจะสร้าง ระบุรายละเอียดของหน้าเว็บนั้นด้วย ยกตัวอย่างเช่น ถ้าเราต้องการแสดงข่าว ใน url ก็ควรจะระบุหมายเลขข่าวมาด้วย เพื่อที่ web application จะได้นำข่าวมาแสดงได้ถูกต้อง

สังเกตว่า เราอาจจะมองว่า url เป็นวิธี (api) ที่ browser เรียกขอข้อมูลมาจากเว็บแอพ ก็ได้

ดังนั้น เราจะสร้าง endpoint /news/<id>/ เพื่อแสดงข่าวที่มีหมายเลขเท่ากับ id

เราจะปรับจากส่วน template ก่อน โดยแก้ template เพื่อใส่ลิงก์ดังด้านล่าง

    <ul>
      {% for item in news_items %}
        <li><a href="/news/{{ item.id }}/">{{ item.title }}</a></li>
      {% endfor %}
    </ul>

ให้ลอง refresh ว่าลิงก์แสดงหรือไม่ ถ้าแสดง ลองกดเล่นดูได้ (จะพบ error)

จากนั้นเราจะเพิ่มฟังก์ชัน show_news_item ที่รับ request จาก url ดังกล่าว    สังเกตว่าใน @app.route เราจะระบุเป็น pattern ที่มีตัวแปร <id> และตัวแปรนี้จะถูกแกะออกมาส่งให้กับฟังก์ชัน show_news_item โดย Flask

@app.route('/news/<id>/')
def show_news_item(id):
    # ...
    # ...

ในฟังก์ชันดังกล่าว จะทำงานเพียงแค่นำรายละเอียดจาก news_items[id] ส่งให้กับ template ดังนี้

@app.route('/news/<id>/')
def show_news_item(id):
    news_item = news_items[int(id)]
    return render_template('news_item.html',
                           id=news_item['id'],
                           title=news_item['title'],
                           body=news_item['body'])

ให้ refresh และลองกดลิงก์ข่าวอีกครั้ง จะพบ error เนื่องจากเรายังไม่ได้สร้าง template news_item.html

ให้เพิ่ม templates/news_item.html ดังนี้

<html>
  <body>
    <h1>{{ title }}</h1>
    <p>{{ body }}</p>
  </body>
</html>

ให้ refresh หน้าเว็บ และลองกดลิงก์อีกครั้ง ลองกดหลาย ๆ ข่าว ถ้าแสดงผลข่าวต่าง ๆ ได้โอเคเราควรจะ

🄶 commit อย่าลืม add ไฟล์ template ที่เราสร้างลงใน repo ก่อนด้วย

UPDATE เพิ่มเติมจากในคลิป

สังเกตว่าเราต้องการให้ <id> match กับเฉพาะหมายเลขที่เป็นจำนวนเต็มเท่านั้น ใน Flask เราสามารถระบุ pattern ใน app.route ได้ดังนี้ (เพิ่ม int: เข้าไปด้านหน้าชื่อตัวแปร id) การใส่ pattern แบบเฉพาะเจาะจงนี้จะช่วยลดปัญหาการ match route ผิดพลาดได้ในอนาคต

@app.route('/news/<int:id>/')
def show_news_item(id):
    # ....

การใช้ url_for

หลังจากนั้น ในคลิปจะอธิบายการใช้ tag url_for เพื่อแยกการขึ้นต่อกัน (decouple) ของ template กับ url pattern     การจัดการดังกล่าว ทำให้เราสามารถเปลี่ยน url pattern ใน @app.route ได้ โดยไม่ต้องไปไล่แก้ template ที่อาจจะมีมากมาย

หลักการดังกล่าว เรียกรวม ๆ กันว่าหลักการ

DRY: Don't Repeat Yourself

เป็นหลักการที่สำคัญมากในการพัฒนาซอฟต์แวร์

ให้ลองทำตามในคลิป และถ้าทุกอย่างทำงานได้ดี อย่าลืม

🄶 commit งาน

ไฟล์หลังจบคลิปที่สอง

ด้านล่างจะเป็นไฟล์ต่าง ๆ หลังจบคลิปที่สอง

main.py

from flask import Flask
from flask import render_template
from datetime import datetime

app = Flask(__name__)

news_items = {
    1: {'id': 1, 
        'title': 'COVID-19 update', 
        'body': 'This is a news on COVID-19'},
    2: {'id': 2, 
        'title': 'Facemasks found', 
        'body': 'Recent news on facemask production'},
    3: {'id': 3,
        'title':'Python 4', 
        'body':'Python 4 will be out soon.... this is FAKE'},
}

@app.route('/')
def index():
    name = 'Somchai'
    time = datetime.now()
    return render_template('index.html', 
                           name=name, 
                           time=time,
                           news_items=news_items.values())

@app.route('/news/<id>/')
def show_news_item(id):
    news_item = news_items[int(id)]
    return render_template('news_item.html',
                           id=news_item['id'],
                           title=news_item['title'],
                           body=news_item['body'])

templates/index.html

<html>
  <body>
    <h1>Hello!! {{ name }}</h1>
    Hi. This is {{ time }}.  This is recent news.
    <ul>
      {% for item in news_items %}
        <li>
          <a href="{{ url_for('show_news_item', id=item.id) }}">{{ item.title }}</a>
        </li>
      {% endfor %}
    </ul> 
  </body>
</html>

templates/news_item.html

<html>
  <body>
    <h1>{{ title }}</h1>
    <p>{{ body }}</p>

    <a href="{{ url_for('index') }}">Home</a>
  </body>
</html>

การสร้าง form

ในส่วนนี้จะเป็นโค้ดสำหรับคลิปที่ 3: หัดเขียนเว็บด้วย Flask #3 - การใช้ form ในการส่งข้อมูล การใช้ redirect ให้ทดลองตามคลิป ในส่วนของรายละเอียดอาจจะมีการเพิ่มเติมให้ภายหลัง

ให้ทดลองทำทีละขั้นตามคลิป และหมั่น commit งานเป็นระยะ ๆ ด้วย     อย่า copy โค้ดด้านล่างแล้วนำไปรันเลย

การแสดงฟอร์มและการเรียกข้อมูลจาก request

เราจะเพิ่มฟอร์มในหน้าแรก ให้เพิ่ม element form ด้านล่าง ลงใน templates/index.html

    <form>
      Title: <input name="title"/><br />
      Body: <textarea name="body"></textarea><br />
      <input type="submit" value="Save"/>
    </form>

ให้ลอง refresh ดูว่าเห็นฟอร์มขึ้นมาหรือไม่

ในการจะส่งข้อมูลจากฟอร์มมาที่ Flask app เราจะต้องพิจารณาว่าจะใช้ end point ใด ในการประมวลผล ซึ่งเราจะต้องไประบุผ่านทาง attribute action ใน form

ดังนั้นเราจะไปสร้าง function เปล่า ๆ มาเตรียมไว้ก่อน ให้เพิ่มฟังก์ชัน create_news_item ลงใน main.py ดังนี้

@app.route('/news/create/')
def create_news_item():
    pass

จากนั้นเราจะชี้ action ของฟอร์มมาที่ฟังก์ชันนี้ โดยใช้ url_for     ให้ปรับ tag form ให้เป็นดังด้านล่าง

    <form action="{{ url_for('create_news_item') }}">
      Title: <input name="title"/><br />
      Body: <textarea name="body"></textarea><br />
      <input type="submit" value="Save"/>
    </form>

ให้ refresh แล้วทดลองกดส่งฟอร์ม ให้ลองอ่าน error message จะพบว่ามีการเข้ามาทำงานที่ฟังก์ชัน create_news_item แล้ว แต่ฟังก์ชันดังกล่าวไม่คืนค่าอะไร

หน้าจอ error จะมีข้อความว่า TypeError

TypeError: The view function for 'create_news_item' did not return a valid response. The function either returned None or ended without a return statement.
หมายเหตุ: ถ้าไม่ได้ error นี้ ให้ลองอ่านรายละเอียดเพิ่มเติม ถ้าเป็น error เกี่ยวกับการทำงานของฟังก์ชัน เช่น ValueError: invalid literal for int() with base 10: 'create' และมีบรรทัดที่มีปัญหาอยู่ในฟังก์ชัน show_news_item เป็นไปได้ว่า url /news/create/ จะถูก route ไปที่ฟังก์ชันดังกล่าว เพราะว่ามันตรงกับ pattern /news/<id>/ ที่ id=="create" ซึ่งอาจจะเกิดขึ้นได้เพราะว่าเราไม่ได้เขียน pattern ให้รัดกุมพอ ในกรณีนี้ ให้แก้ annotation ของฟังก์ชัน show_news_item ให้เป็น @app.route('/news/<int:id>/') (เพิ่ม int: ก่อนหน้าชื่อ parameter id) จะทำให้ route ดังกล่าวจะไม่ match กับ /news/create/ พอแก้แล้วให้ลองส่งฟอร์มใหม่

เมื่อ browser ส่งฟอร์มมาให้กับ web application, จะมีการส่งข้อมูลต่าง ๆ กลับมาด้วย     ใน Flask เราจะเข้าถึงข้อมูลดังกล่าวผ่านทางตัวแปร request ซึ่งจะเป็นตัวแปร global ในโมดูล Flask     เราจะเพิ่มการ import ที่ตอนต้น main.py ดังนี้

from flask import request

เราจะให้ฟังก์ชัน create_news_item พิมพ์ค่าที่ส่งมาใน request โดยเพิ่มบรรทัด print ดังด้านล่าง

@app.route('/news/create/')
def create_news_item():
    print(request.args)

ให้ทดลองส่งฟอร์มอีกครั้งเพื่อดูผลลัพธ์ ที่หน้า page เราจะเห็น error เพราะว่าฟังก์ชันไม่ได้คืนค่า แต่ถ้าดูใน terminal (console) ที่เรียก flask run ทิ้งไว้จะเห็นว่าใน request.args เป็น ImmutableMultiDict ดังรูปด้านล่าง ที่เก็บข้อมูลจากฟอร์มที่เราส่งมา

223-flask-requestargs.png

อย่างไรก็ตาม การส่งข้อมูลฟอร์มแบบที่เราทำข้างต้นเป็นการใช้งานที่ไม่ค่อยจะถูกต้องนัก

ในรูปแบบการติดต่อแบบ HTTP จะมีการระบุรูปแบบในการขอข้อมูลไว้หลายแบบ ที่เราจะเห็นในเอกสารนี้มีสองแบบคือ

  • GET - ใช้สำหรับขอข้อมูล โดยหลักการพื้นฐาน การเรียก GET request จะต้องไม่ทำให้ข้อมูลเปลี่ยนสถานะ
  • POST - โดยมากใช้สำหรับการเพิ่มหรือแก้ไขข้อมูล

(หมายเหตุยังมี PUT, UPDATE, DELETE แต่เราจะยังไม่ใช้)

ประเภทของ request เหล่านี้ จะถูกแสดงใน Flask development server เวลาที่มีการประมวลผลการเรียกใช้ (ลองสังเกตดู)

จากที่เขียนมาข้างต้น เราจึงไม่ควรประมวลผลฟอร์มด้วย GET    ดังนั้นในส่วนถัดไปเราจะปรับรูปแบบให้เป็น POST

การประมวลผลฟอร์ม และการ redirect

การระบุให้การส่ง request ของฟอร์มเป็นการส่งแบบ POST ก็ทำได้โดยการระบุ attribute ใน tag form ดังด้านล่าง

    <form action="{{ url_for('create_news_item') }}" method="POST>
       ...
    </form>

ให้ทดลองกดส่งฟอร์มอีกครั้ง จะพบ error เพราะว่าใน Flask ถ้าไม่ระบุอะไร @app.route จะรับแค่ GET request เท่านั้น เราจะแก้ annotation ของฟังก์ชัน create_new_item โดยเพิ่มการระบุ methods ดังนี้ (และเราเพิ่มให้คืนสตริงว่างด้วย จะได้ไม่เห็น error ที่หน้าเว็บเพจ)

@app.route('/news/create/', methods=['POST'])
def create_news_item():
    print(request.form)        # change from request.args to request.form
    return ''                  # added to show empty page instead of error

สังเกตว่า เราจะอ้างถึงข้อมูลที่ส่งมาทาง POST request ด้วย request.form แทนที่จะเป็น request.args ซึ่งจะเป็นข้อมูลที่ส่งมาทาง GET (ซึ่งจะโผล่ใน url)

ให้ทดลองกดส่งฟอร์มอีกครั้ง และดูผลลัพธ์ที่ใน console ด้วย ว่าโปรแกรมพิมพ์อะไรออกมาบ้าง

เราจะรับข้อมูลดังกล่าว แล้วนำมาเพิ่มใน news_items     เพื่อให้โค้ดเป็นที่เป็นทาง เราจะเพิ่มฟังก์ชัน new_news_item เพื่อทำงานดังกล่าว

ในคลิปจะทดลองทำหลาย ๆ ขั้น (ตั้งแต่กำหนดให้ id=100) ให้ทดลองทำตาม แต่โค้ดสุดท้ายที่คืน news_item ที่ถูกต้องเป็นดังด้านล่าง

def new_news_item(title, body):
    new_id = max(news_items.keys()) + 1
    return {
        'id': new_id,
        'title': title,
        'body': body
    }

และปรับฟังก์ชัน create_news_item ให้เรียกใช้ new_news_item ดังด้านล่าง

@app.route('/news/create/', methods=['POST'])
def create_news_item():
    item = new_news_item(request.form['title'],
                         request.form['body'])
    news_items[item['id']] = item
    return ''

ให้ทดลองส่งฟอร์ม เมื่อส่งแล้วจะเห็นหน้าจอว่าง ๆ แต่ถ้ากลับไปโหลดหน้าแรกใหม่ ข่าวใหม่ที่เราเพิ่มเข้าไปน่าจะปรากฏขึ้น (ห้าม restart flask เพราะว่าข้อมูลใหม่จะหายไปหมด)

ก่อนจะไปต่อ ถ้าแก้เรียบร้อย

🄶 ได้เวลา commit แล้ว

เราจะทำให้การส่งฟอร์มเสร็จสมบูรณ์ หลังประมวลผล POST request โดยการ redirect กลับไปหน้าที่เหมาะสม เช่น หน้าแสดงข่าวใหม่ หรือหน้าหลักของเรา เราจะใช้ฟังก์ชัน redirect ใน Flask ซึ่งต้องไป import มาก่อน ให้เพิ่มฟังก์ชันดังกล่าวในรายการ import ตอนต้นไฟล์ main.py

from flask import redirect

และจบฟังก์ชัน create_new_item โดยการคืน rediect (แทนที่จะเป็น return ) ดังนี้

@app.route('/news/create/', methods=['POST'])
def create_news_item():
    # ...
    # ...
    return redirect(url_for('index'))

ให้ทดสอบว่าสามารถทำงานได้ถูกต้อง เมื่อทดสอบเสร็จแล้ว

🄶 อย่าลืม commit งานที่ทำด้วย

การส่งงานสัปดาห์นี้

การส่งงานสัปดาห์นี้: เมื่อทำขั้นตอนนี้เสร็จสิ้น ให้ส่ง repository ที่ทำนี้ด้วย โดยสร้าง gihub repository และ push งานทั้งหมดที่ทำไปที่ repo นั้น (มันจะมาพร้อม history) แล้วนำ url มาแปะใน google sheet ที่จะระบุให้ส่งงานต่อไป

โค้ดหลังจบคลิปที่ 3

main.py

from flask import Flask
from flask import render_template, redirect, url_for
from flask import request

from datetime import datetime

app = Flask(__name__)

news_items = {
    1: {'id': 1, 
        'title': 'COVID-19 update', 
        'body': 'This is a news on COVID-19'},
    2: {'id': 2, 
        'title': 'Facemasks found', 
        'body': 'Recent news on facemask production'},
    3: {'id': 3,
        'title':'Python 4', 
        'body':'Python 4 will be out soon.... this is FAKE'},
}

@app.route('/')
def index():
    name = 'Somchai'
    time = datetime.now()
    return render_template('index.html', 
                           name=name, 
                           time=time,
                           news_items=news_items.values())

@app.route('/news/<id>/')
def show_news_item(id):
    news_item = news_items[int(id)]
    return render_template('news_item.html',
                           id=news_item['id'],
                           title=news_item['title'],
                           body=news_item['body'])

def new_news_item(title, body):
    new_id = max(news_items.keys()) + 1
    return {
        'id': new_id,
        'title': title,
        'body': body
    }

@app.route('/news/create/', methods=['POST'])
def create_news_item():
    item = new_news_item(request.form['title'],
                         request.form['body'])
    news_items[item['id']] = item
    return redirect(url_for('index'))

templates/index.html

<html>
  <body>
    <h1>Hello!! {{ name }}</h1>
    Hi. This is {{ time }}.  This is recent news.
    <ul>
      {% for item in news_items %}
        <li>
          <a href="{{ url_for('show_news_item', id=item.id) }}">{{ item.title }}</a>
        </li>
      {% endfor %}
    </ul> 

    <form action="{{ url_for('create_news_item') }}" method="POST">
      Title: <input name="title"/><br />
      Body: <textarea name="body"></textarea><br />
      <input type="submit" value="Save"/>
    </form>
  </body>
</html>

Stylesheet (CSS)

เนื้อหาส่วนนี้ไม่มีใน clip ให้ทดลองทำตามด้านล่างได้เลย (อย่างไรก็ตาม สามารถดูคลิปพื้นฐานเกี่ยวกับ css ได้ที่นี่ youtube css intro)

เราได้เห็นการแยกโค้ดส่วนของการจัดการข้อมูล (ใน python) และส่วนแสดงผลออกจากกัน โดยเราแยกส่วนแสดงผลให้เป็น template     ในส่วนนี้เราจะแยกส่วนการจัดการ style (ความสวยงาม) ออกจากเนื้อหาใน template อีกที

เราจะสร้างไฟล์เพื่อเก็บรายละเอียดของ style และส่งไฟล์นั้นให้กับ browser เพื่อระบุวิธีการจัดการความสวยงาม    ไฟล์เหล่านี้จะเป็นเนื้อหาส่วนที่ไม่มีการเปลี่ยนแปลง (เรียกรวม ๆ ว่าเป็น static content) ดังนั้นเรามักจะไม่ให้ web app เป็นผู้รับผิดชอบในการจัดการ

ใน Flask ไฟล์พวกนี้จะถูกเก็บแยกไว้ในไดเรกทอรี static และบน development server จะสามารถเรียกใช้ได้ทันที แต่ในการใช้งานจริงบน production เราจะต้องจัดการหาวิธีส่งไฟล์พวกนี้ให้กับ browser แบบอื่น (จะได้เรียนรายละเอียดต่อไป)

เราจะสร้างไฟล์ style.css ในไดเรกทอรี static/css ให้สร้าง ไดเรกทอรี static และสร้างไดเร็กทอรี css ไว้ด้านในอีกที ก่อนจะสร้างไฟล์ style.css

ในตอนแรก ให้เพิ่มรายละเอียดด้านล่างลงในไฟล์ style.css

h1 {
  background: gray;
  padding: 20px;
}

จากนั้นให้ลอง refresh หน้าเว็บว่าเห็นการเปลี่ยนแปลงหรือไม่ จะพบว่าไม่มีผลอะไร เพราะว่าเรายังไม่ได้ระบุใน html ว่าให้ใช้ไฟล์ดังกล่าวในการ style หน้าเว็บของเรา

เราจะระบุผ่านทาง tag link ซึ่งนิยมเอาไว้ในส่วนของ head ของเอกสาร html

ให้เพิ่ม section head ที่มีการระบุ tag link ลงในไฟล์ templates/index.html ดังด้านล่าง

<html>
  <head>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
  </head>
  <body>
    ...
  </body>
</html>

สังเกตว่าเรามีการใช้ url_for และระบุฟังก์ชัน static เพื่อสร้าง url สำหรับการส่ง static file     ความสามารถส่วนนี้เป็นสิ่งที่ Flask เตรียมไว้สำหรับการพัฒนา

ให้ลอง refresh หน้ารายการของเราอีกครั้ง จะเห็นการเปลี่ยนแปลง ให้ลองอธิบายว่า style sheet ของเราระบุอะไร และทำให้เกิดผลอะไรกับการเปลี่ยนแปลงนี้

ให้ลองกดไปดูที่หน้าข่าวแต่ละหัวข้อ จะพบว่า h1 ยังแสดงผลแบบเดิม ให้เข้าไปแก้ไฟล์ templates/news_item.html ให้มีการลิงก์ไปยัง style sheet ดังกล่าวด้วย

หมายเหตุ: การที่ต้องแก้ template ซ้ำ ๆ หลายครั้งเมื่อต้องการปรับหน้าตาของเว็บ เช่นเพิ่ม link css ให้กับทุกหน้า เป็นสิ่งที่สร้างความลำบากและปัญหามาก เฟรมเวิร์คในการพัฒนาเว็บที่ดีจะต้องทำให้เราลดภาระพวกนี้ได้ ใน Flask เราจะสามารถลดปัญหาดังกล่าวด้วย template inheritance ซึ่งเราจะได้เรียนต่อไป

CSS Selector

คำว่า css ย่อมาจาก cascading style sheet คำว่า cascade (มีการลดหลั่น หรือมีลักษณะคล้ายน้ำตก) หมายถึงหลักการในการให้ความสำคัญของกฎต่าง ๆ เวลามีกฎระบุ style ใหักับ element บนหน้าจอหลายอัน

สิ่งที่อยู่ในไฟล์ style sheet คือ กฎ (rule) ที่ระบุ style ซึ่งจะอยู่ในรูปแบบ

selectors {
  css declaration1;
  css declaration2;
  css declaration3;
  ...
}

โดย selector (อาจมีได้หลายอัน) จะเลือกว่ากฎนี้จะทำงานกับ element ใด และ css declarations จะระบุค่าให้กับ property ต่าง ๆ เช่น color, border, font เป็นต้น

ทดลองแก้ไฟล์ style.css โดยเพิ่มกฎ li ดังด้านล่าง ก่อนจะ refresh ให้ลองคิดก่อนว่าหน้าตาเว็บของเราจะเป็นอย่างไร

li {
  background-color: greenyellow;
  border: 1px solid black;
  margin-bottom: 5px;
}

ก่อนจะทดลองต่อ ให้ลบกฎ li ด้านบนออกก่อน ไม่งั้นจะดูผลลัพธ์ได้ยาก

CSS selector นั้นจะเลือก element ในหน้าเว็บได้หลายแบบ แต่เราจะต้องเข้าใจคุณสมบัติพื้นฐานของแต่ละ element บนหน้าเว็บก่อน

แต่ละ element บนหน้าเว็บจะมี

  • id - แทนชื่อของ element นั้น ในแต่ละ document จะต้องไม่มี element ที่ id ซ้ำกัน (โดยหลักการ)
  • class - แต่ละ element จะมี class ได้หลายอัน แสดงการเป็นสมาชิกของ element ต่อประเภทหลาย ๆ ประเภท

ดังนั้น css selector จะมีรูปแบบพื้นฐานมีสองแบบเช่นเดียวกัน คือ

  • การเลือกด้วย class ตัวอย่างเช่น ถ้าจะเลือก element ทั้งหมดที่มี class news-items จะเขียน selector เป็น .news-items (ใช้จุดนำหน้า)
  • การเลือกด้วย id ตัวอย่างเช่น ถ้าจะเลือก element ที่มี id news_item_2 จะเขียน selector เป็น #news_item_2 (ใช้ # นำหน้า)

ให้แก้ไฟล์ templates/index.html ให้เป็นดังด้านล่างก่อนที่จะทำต่อ เพื่อที่เราจะได้มี element ที่มากพอและซับซ้อนพอที่จะทดลองกฎใน css ได้

<html>
  <head>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
  </head>
  <body>
    <h1>Hello!! {{ name }}</h1>
    Hi. This is {{ time }}.  This is recent news.
    <ul id="main_news_list">
      {% for item in news_items %}
        <li class="news-items" id="news_item_{{ item.id }}">
          <a href="{{ url_for('show_news_item', id=item.id) }}">{{ item.title }}</a>
        </li>
      {% endfor %}
    </ul> 

    This is the first dummy list
    <ul>
      <li class="news-items">
        <span class="highlight">Somying</span> is the first year student.
      </li>
      <li class="news-items">
        Somchai is the second year student
      </li>
      <li class="news-items">Sommai</li>
      <li class="news-items">Somjai</li>
    </ul>

    This is the <span class="highlight">second dummy list</span>:
    <ul>
      <li>breakfast</li>
      <li id="lunch_item">lunch</li>
      <li>dinner</li>
    </ul>

    <form action="{{ url_for('create_news_item') }}" method="POST">
      Title: <input name="title"/><br />
      Body: <textarea name="body"></textarea><br />
      <input type="submit" value="Save"/>
    </form>
  </body>
</html>

การทดลองที่ 1

ให้เพิ่มกฎต่อไปนี้ลงใน style.css ให้พิจารณากฎและหน้าเว็บและคาดเดาผลลัพธ์ที่เกิดขึ้น ก่อนจะ refresh และสังเกตผลจริง ๆ

.news-items {
  margin: 10px 0;
}

#main_news_list .news-items {
  border: 1px solid gray;
  padding: 5px;
}

.highlight {
  color: red;
}

แบบฝึกหัด

แก้ style.css เพื่อให้ผลลัพธ์เป็นดังด้านล่าง

223-web-flask-css.png

ควรปรับการแสดงผลดังนี้

  • รายการข่าวที่สอง เปลี่ยน background เป็นสีเขียว ปรับฟอนต์ใหญ่ขึ้น เปลี่ยนเป็นสีขาว (สี text อาจจะต้องแก้ที่ element <a>)
  • ปรับให้ข้อความใน highlight ตัวใหญ่ และ background สีเหลือง แต่ปรับเฉพาะที่อยู่ใน first dummy list

Bootstrap

ถ้าต้องการหน้าจอเว็บที่สวย (แต่อาจจะดูโบราณนิด) เราสามารถใช้ bootstrap css library ได้ สามารถดูคลิปเกี่ยวกับการใช้งานพื้นฐานได้ที่ youtube Bootstrap

เอกสารเพิ่มเติม

จะเพิ่มเร็ว ๆ นี้