2014年3月6日 星期四

如何在 GAE 上佈署 jQuery 與 ExtJS 專案

今天早上代班時把 GWT 很快看完, 結論是 : 不玩. 用 GWT 編譯出來的東西對網頁設計者而言不夠親民, 距離太遙遠, 還不如學 ExtJS 有趣.. 下午突發奇想, 能不能把 jQuery UI 與 ExtJS 佈署到 GAE 上呢? 這樣就可以利用 Google 強大的雲端主機, 不怕網站掛點了. 但是一想到 jQuery UI 與 ExtJS 檔案這麼多, 會不會超過 GAE 的免費額度啊? (好像是不能超過 1000 個檔案). 而且這些靜態檔案還必須在 app.yaml 檔裡面一個一個去定義, 我看會瘋掉.

我找出上官林傑的 "Google 應用服務引擎開發實戰", 在這本書的 285 頁有提到, 可以使用壓縮函式庫的方法, 但那是指 Python 函式庫, jQuery 與 ExtJS 函式庫可以這麼用嗎? 我覺得應該不行. 其實這兩個框架的檔案不需要自備, 可以直接用 CDN 所提供的檔案, 反正都是網路服務啊, 這些 CDN 都很穩, 自備函式庫除了占空間上傳費時外, 升版時還要重新上傳更新, CDN 就只要改一下版次編號即可. .

為了實驗看看 GAE 究竟能否或要如何佈署 jQuery 與 ExtJS 專案, 於是在我的 GAE 帳號下註冊了 mygaetestweb 這個應用程式, 網址在 :

# http://mygaetestweb.appspot.com

首先必須有 Gmail 帳號, 然後到 GAE 網站免費申請成為 GAE 開發者 :

https://appengine.google.com/

成功後就可以按 "Create new application" 鈕註冊應用程式名稱, 可按 "Check availability" 鈕檢查看看所要的應用程式名稱是否已被人註冊, 若為 invalid, 表示不能使用, available 則為可用 :


找到可用 App 名稱後, 按底下的 "Create Application" 鈕即可, 此 App 就會出現在 My Application 頁面列表中, 剛申請時其 Status 欄顯示 None Deployed (尚未佈署), 等專案測試 OK, 上傳成功後則顯示 "Running" (運行中) :


GAE 的後端設定就是如此簡單. 接下來要安裝 Python, 注意, GAE 只支援 Python 2 版, 尚未支援 3 版, 因此我們要下載 2 版的最後一個版本 2.7.6 版 (XP 下載 x86 的 msi 檔, Win7 以上 x86 或 x64 均可), 下載後直接執行 msi 檔即可 :

# 下載 Python 2.7.6

接著要下載安裝 GAE SDK, 以便在 App 開發階段於本機模擬 GAE 網站執行結果, 目前是 1.9.0 版, 請特別注意, 第一個下載表格是 PHP 的, 請往下拉到第二個寫著 "for Python" 那個才是, 要不然到時在 GAE Launcer 程式執行 App 時會無法執行, 我第一次就是這樣, 還以為這個最新版有 bug, 其實是下載了 PHP 版的 SDK 去跑 Python 寫的程式

# 下載 GAE SDK


GAE SDK 安裝時會尋找已安裝的 Python 程式與計算磁碟空間 (會花一點時間), 找到後才能進行安裝, 特別注意, GAE SDK 1.9.0 使用的是 Python 2.7 版, 如果電腦中還有原先舊版 GAE 所用的 Python 2.5 版, 必須先移除, 否則 GAE SDK 會抓到 Python 2.5, 而非 Python 2.7.6 :


注意, Python 2.7.6 版的 Lib 目錄下, 有一個 mimetypes.py 檔案需要修改, 否則執行 GAE SDK 時會遇到 UnicodeDecodeError, 詳見我另外一篇文章 :

GAE SDK Launcher 因 UnicodeDecodeError 無法運行問題

2015-12-29 補充 :
Python 2.7.11 與 GAE SDK 1.9.30 已經無此問題了. 

為了爾後使用 GAE 的影像 API 操作圖片, 還要下載 PIL 函式庫 (Python Imaging Library), 注意, 配合 Python 2.7 版的 PIL 是 1.1.7 版 :


# 下載 PIL 1.1.7 函式庫

安裝 PIL 時會確認是否已安裝 Python 2.7 版, 版本不對無法安裝. 

上面 GAE SDK 安裝完後會在桌面出現一個 GAE Launcher 飛機引擎圖示, 點擊執行後選擇 "File/Create New Application" 出現如下畫面 :



只要輸入上面兩個欄位即可, 其餘用預設值, 第一欄 Application Name 必須填入在 GAE 網站申請的應用程式名稱, 例如我上面申請的 mygaetestweb; 第二欄按 Browse 鈕選擇我們的應用程式專案要放在本機的哪一個資料夾, 按 Create 即建立新應用程式, 上圖設定會在 E:\GAE 下建立一個 mygaetestweb 目錄以存放應用程式檔案, SDK 會自動指定代表此應用程式的 port 號碼, 可以自行修改, 但有多個應用程式時須注意不要重複 (接受預設值就不用擔心重複) :


點選應用程式 mygaetestweb 後按 "Run" 鍵即執行該應用程式, 等到 Run 按鈕變灰色無法按, Stop 按鈕變紅色可以按之後, 表示此應用程式已運行, 開啟瀏覽器 輸入 localhost:8080 (務必輸入埠號), 就會顯示 Hello world! :


這是 GAE SDK 以內建的 webapp2 應用程式開發框架所建立的預設 App, 功能就是簡單地回應 "Hello world!" 字串給客戶端. 使用 webapp2 的好處是, 它已經幫我們處理好繁瑣的 HTTP 協定, 直接使用其函式庫或物件即可, 若使用 Python 的 CGI 模組來開發, 就得自行處理 HTTP 協定. 請打開應用程式目錄 mygaetestweb, 就可以看到 webapp2 為這個簡單的 App 所建立的四個檔案 :


其中只有兩個檔案是需要修改的 : 應用程式設定檔 app.yaml 與 URL 處理程式 main.py. 應用程式設定檔 app.yaml 分成三部份, 第一部份應用程式執行環境設定, 包括設定應用程式名稱 (application), 版本 (version), 執行環境 (Python27). 第二部份為 URL 處理器 (handlers), 第三部份為框架函式庫的版本, 新版 GAE 用的是 webapp2, 如下所示 :

application: mygaetestweb
version: 1
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: /favicon\.ico
  static_files: favicon.ico
  upload: favicon\.ico

- url: .*
  script: main.app

libraries:
- name: webapp2
  version: "2.5.2"

其中我們需要設定的部分只有 handlers, 這部份用來定義該如何處理連線到此應用程式之 HTTP要求, 是 GAE 網頁應用程式的路由器 (router), 除了 .py 以外的靜態檔案, 包括 html 檔, 圖檔, Javascript 檔, CSS 樣式檔等, 都必須在此宣告其資源位置 (但是 template 要用到的 HTML 檔不需要, 因為他們不是靜態檔案, 而是要為給 Python 程式處理的動態檔案). 這些資源雖然都寫在同一個網頁檔中, 但瀏覽器解析 HTML 網頁檔時, 發現網頁中有用到 JPG 圖檔, JS 檔, 或 CSS 檔, 還會再發出 HTTP 要求從伺服器下載, 因此瀏覽一個網頁並非只對 HTML 檔向伺服器提出一個要求而已, 網頁中所包含的資源檔都要另外提出要求.

如果有很多圖檔, 要一一在此宣告很麻煩, 可以將其放在 images 子目錄下, 然後定義一個靜態目錄 images 以及其 url 名稱 img 即可, 例如 :

- url: /img
   static_dir: images

我們在網頁中就用例如 <img src="/img/logo.jpg"> 來取得資源, 所以上面的宣告事實上就是把 img 這個 URL 目錄對應到實際的靜態目錄 images. 當然 URL 的名稱也可以跟靜態目錄同名, 這樣比較好維護.

最後一個 URL 是 .* (小數點與星號), 因為我們是用 webapp2 框架來開發 GAE 應用, 因此只要把靜態檔案以外的 URL 全部都丟到 main.app 程式去處理即可. 此 main.app 對應到實際的程式檔案就是 main.py 檔, 在 main.py 程式裡我們就可以利用 webapp2 來處理每一個可能的 URL 請求. 所以使用開發框架時, URL 的 Router 事實上分成兩部分, 靜態檔案位於 app.yaml 檔, 而動態部分則移到 main.py 去處理了. 若用 CGI 模組處理的話, 每一個 URL 都要在 app.yaml 中分派. 這裡並不一定要用 main.py, 可以自取名稱, 只要在 app.yaml 中設定即可, 例如某個 URL 的應用程式是 myapp.py, 則 script 要改為 myapp.app. 要注意的是, 所有的靜態檔案宣告都必須在 main.app 的前面, 否則這些靜態檔案將無法下載, 因為若 main.py 在前, 則所有的 URL 請求就會全部丟給 main.py 處理, 而我們並不會在 main.py 中處理靜態檔案.

特別注意, YAML 檔跟 Python 一樣都不是格式自由的, 冒號後面一定要空一格, 否則會執行時出現錯誤. 增加靜態檔案宣告時最好複製已有的資料去改, 以免格式不符. 我們可以直接用記事本編輯 app.yaml, 也可以用 GAE Launcher 的 "Edit/Open in External Editor" 打開 但必須先在 "Edit/Preferences" 中指定編輯器程式 (此處我用 EditPlus):


只要設定上面三個欄位即可, 第四個空白不要填 :

Python Path=C:\Python27\python.exe
App Engine SDK=C:\Program Files\Google\google_appengine
Editor=C:\Program Files\EditPlus 3\editplus.exe

其次需要編輯的是主程式 main.py 檔, 這是主要的 URL Router, 預設內容如下 :

import webapp2

class MainHandler(webapp2.RequestHandler):
    def get(self):
        self.response.write('Hello world!')

app = webapp2.WSGIApplication([
    ('/', MainHandler)
], debug=True)

新版 GAE 已經改用 webapp2 開發框架, 因此須先用 import 匯入類別庫. webapp2 實作了 Python 的網站伺服器程式標準界面 WSGI (Web Server Gateway Interface), 提供 WSGIApplication 類別, 只要呼叫其建構子即可直接建立 WSGI 相容程式. 我們要修改的部份是 WSGIApplication() 建構子的傳入參數, 此為一個 tuple 串列, 每一個 tuple 只有兩個元素, 第一個是 URL, 第二個是負責處理此 URL 的類別. 要加入 URL 時於此串列中添加元素即可 (要特別注意元素間的逗號). 此預設的程式會讓連線到根目錄的要求得到 "Hello world" 回應. 類別中的方法可以是 get, post, 或任一 HTTP 的方法, 視客戶端提出的要求而定, 若瀏覽器送出的是 POST 表單, 要定義 post 方法處理, 反之則是 get 方法.

接下來我們要以這四個預設的檔案為基礎, 修改成我們的應用程式. 首先來測試如何在 GAE 上部署 jQuery 專案. 我選定 jQuery UI 的 DataTables 為對象, 原始範例請參考 "jQuery 套件 DataTables 的測試" 中的範例 6 此範例會顯示一個用 jQuery UI 外掛 DataTables 所呈現的股市收盤價列表. 

我的初淺構想是使用 template 模板來簡化開發程序, 可以同時支援 jQuery 與 ExtJS 兩套客戶端 Javascript 框架. 先來看看 template. 新版 GAE SDK 已包含了 Django 0.96 與 1.2 版的模板引擎, 新舊皆有, 所以不需要為了使用新版 Django 去自行下載更新. 請參考下列說明 :

# webapp2 用法
# Using Templates

這裡我想照書上的作法, 使用 Django 0.96 版的 webapp 模板引擎. 所謂模板就是預先做好的 HTML 檔, 裡面嵌入模板的控制標籤, 當我們把模板檔案餵給 Python 程式描繪 (render) 時, 可以傳入變數, 達到網頁內容動態變化的目的. 更棒的是, 模板可以像物件導向設計中的類別那樣, 可以繼承, 像堆積木一樣添加新設計.

webapp 框架的 template 模板語言由三種標籤構成 :
  1. 變數輸出標籤 : {{ 與 }}
    由兩層大括號包起來的部份就是變數標籤, 裡面只能放 Python 程式傳進來的變數, 變數名稱必須與程式中的對應. template 還提供豐富的過濾器, 可以對變數值進行過濾後再輸出.
    例如, 模板為 :
    <p class="name">{{user.name}}</p>
    則程式 :
    x={"name":"tony","password":"123"}
    url="login.htm"                                                     #指定要輸出的網頁模板
    path=os.path.join(os.path.dirname(__file__), url)  #取得檔案路徑
    content=template.render(path,{"user":x})          #傳入變數輸出網頁
  2. 語法標籤 : {% 與 %}
    以 {% 與 %} 包起來的區域可以使用 Python 程式來控制網頁輸出, 指令非常多, 以下為常用的幾種指令 :
    模板繼承指令 :
    {% extends "html5.htm" %}
    區塊指令 :
    {% block link %}
    <link rel="stylesheet"  href="layout.css">
    {% endblock%}
    迴圈指令 :
    {% for item in list %}
    <p>{{ item }}</p>
    {% endblock%}
    判斷指令 :
    {% if male %}
    <p>男</p>
    {% else %}
    <p>女</p>
    {% endif %}
  3. 註解標籤 : {# 與 #}
    模板引擎不會對此標籤內容進行解析.
現在我們先做一個 HTML5 的母模板 html5.htm 如下 :

<!DOCTYPE html>
<!--[if lt IE 9]>
<script type="text/javascript" src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<html>
<head>
  <meta charset="utf-8">
  <title>{% block title %}{% endblock %}</title>
  {% block link %}{% endblock %}
  {% block javascript%}{%endblock%}
  <style>
  article,aside,figure,figcaption,footer,header,hgroup,menu,nav,section
  {display:block;}
  {% block style %}{% endblock %}
  </style>
</head>
<body>
  {% block body %}{% endblock %}
</body>
</html>

在此母模板中, 我們處理了 IE9 以下對 HTML5 的支援問題, 同時設置了五個嵌入區塊 : title, link, javascript, style, 以及 body, 方便子模板將特定內容套進區塊裡面. 接下來我們繼承此 HTML 母模板, 製作一個 jQuery UI 專案要使用的模板 jquery.htm 如下 :

{% extends "html5.htm" %}
{% block link %}
  <link rel="stylesheet"  href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.10.4/themes/hot-sneaks/jquery-ui.css">
  <link rel="stylesheet" href="http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/css/jquery.dataTables.css">
{% endblock %}
{% block javascript %}
  <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.0.min.js"></script>
  <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.10.4/jquery-ui.min.js"></script>
  <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/jquery.dataTables.min.js"></script>
{% endblock %}

在上面這個 jquery.htm 模板中, 先用 extends 指令繼承 html5.htm, 然後於 link 區塊中套入 jQuery UI 與外掛 DataTables 的樣式表, 而 javascript 區塊則套入微軟 CDN 所提供的 jQuery UI 函式庫.

做好這個模板, 就可以進入這次測試的核心了, 如何用 DataTables 外掛來顯示股市收盤資訊? 我們繼承 jquery.htm, 然後在 body 區塊中撰寫 jQuery UI 程式碼 jquery_datatable.htm 如下 :

{% extends "jquery.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
  <table id="table1"></table>
  <script language="JavaScript">
    $(document).ready(function(){
      var opt={"oLanguage":{"sUrl":"dataTables.zh-tw.txt"},
               "bJQueryUI":true,
               "aoColumns":[{"sTitle":"股票名稱"},
                            {"sTitle":"股票代號"},
                            {"sTitle":"收盤價 (元)"},
                            {"sTitle":"成交量 (張)"}],
               "aaData":[["台積電","2330",111.5,19268],
                         ["中華電","2412",95.1,7096],
                         ["中碳","1723",145.0,317],
                         ["創見","2451",104.0,459],
                         ["華擎","3515",104.0,95],
                         ["訊連","5203",98.5,326]]
               };
      $("#table1").dataTable(opt);
      });
  </script>
{% endblock%}

上面這個 jquery_datatable.htm 繼承了 jquery.htm 模板, 然後套入其祖模板 html5.htm 中定義的 style 區塊內容, 將整個表格大小縮小為 80%, 而 body 區塊就把我們在 "jQuery 套件 DataTables 的測試" 範例 6 中的程式碼原封不動套進去. 

注意喲, 上面的 DataTables 設定中有一個 oLanguage 參數, 其值是一個中文化設定檔 dataTables.zh-tw.txt, 這個檔是從 DataTables 外掛壓縮檔中取得的, 因為 沒有一個 CDN 提供此檔, 故必須自行提供, 我把它放在專案目錄下, 因為係靜態檔案, 因此必須修改 app.yaml 設定檔如下 : 

application: mygaetestweb
version: 1
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: /favicon\.ico
  static_files: favicon.ico
  upload: favicon\.ico
- url: /dataTables.zh-tw.txt
  static_files: dataTables.zh-tw.txt
  upload: dataTables.zh-tw.txt
- url: .*
  script: main.app

libraries:
- name: webapp2
  version: "2.5.2"  

修改的部分就是在 URL 路由器 handlers 中添加一個靜態檔案的 URL (/dataTables.zh-tw.txt) 的資源設定. OK, 至此完成網頁輸出部分, 還必須修改 URL 路由器 main.py 如下 :

# -*- coding: utf-8 -*-
import os
from google.appengine.ext.webapp import template
import webapp2

class jquery_datatable(webapp2.RequestHandler):
    def get(self):
        url="jquery_datatable.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class MainHandler(webapp2.RequestHandler):
    def get(self):
        self.response.write('Hello world!')

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/jquery_datatable', jquery_datatable)

], debug=True)

在這個 main.py 主程式裡, 因為需要取得 html 檔之實際路徑 以便傳遞給 render() 方法去輸出網頁, 因此必須匯入 os 函式庫, 而 template 則必須從 google.appengine.ext.webapp 函式庫匯入. 然後就可以新增一個類別 jquery_datatable 來輸出 jquery_datatable.htm 這個網頁. 最後必須在 WSGIApplication() 建構子的傳入參數中添加一筆 tuple 來將 URL 要求指引到 jquery_datatable 類別處理. 至此大功告成, 本機測試 OK 後, 就可以按 "Deploy" 鍵將應用程式佈署到 GAE 上, 如下圖所示會先要求輸入 GAE 帳密 (與 Gmail 同), 出現 "You can close this window now" 就表示佈署成功了 : 




測試範例 1 : http://mygaetestweb.appspot.com/jquery_datatable [下載 zip 檔]


以上便是在 GAE 上佈署 jQuery UI 專案的方法. 接下來是測試 ExtJS 的專案, 方法類似, 就是繼承 html5.htm 模板, 寫一個 extjs.htm 子模板, 把 ExtJS 的 CSS 與 Javascript 套進去即可. 此處我用的是 ExtJS 官網 Sencha 的 CDN, 如下所示 :

{% extends "html5.htm" %}
{% block link %}
<link rel="stylesheet"
href="http://cdn.sencha.com/ext/gpl/4.2.1/resources/css/ext-all.css">
{% end block%}
{% block javascript %}
<script type="text/javascript"
src="http://cdn.sencha.com/ext/gpl/4.2.1/ext-all.js"></script>
<script type="text/javascript" src="http://cdn.sencha.com/ext/gpl/4.2.1/locale/ext-lang-zh_TW.js"></script>
{% end style%}

接下來就是繼承 extjs.htm 這個模板, 開始撰寫 ExtJS 專案程式, 此處要使用 ExtJS 的 DataGrid 來寫一個跟上面 jQuery 一樣的股市收盤資訊網頁, 如下之 extjs_datatable.htm 所示 :

{% extends "extjs.htm" %}
{% block style %}
    #gridpanel {margin: 50px;}
{% endblock%}
{% block body %}
  <div id="gridpanel"></div>
  <script language="JavaScript">
    var gridpanel=Ext.Element.get("gridpanel");
    Ext.onReady(function(){
      var store=Ext.create("Ext.data.ArrayStore",{
                fields:["stock_name","stock_id","close","volumn"],
                data:[["台積電","2330",111.5,19268],
                      ["中華電","2412",95.1,7096],
                      ["中碳","1723",145.0,317],
                      ["創見","2451",104.0,459],
                      ["華擎","3515",104.0,95],
                      ["訊連","5203",98.5,326]]
                });
      Ext.create("Ext.grid.Panel",{
                 width:500,
                 renderTo:gridpanel,
                 store:store,
                 columns:[{text:"股票名稱",dataIndex:"stock_name"},
                          {text:"股票代號",dataIndex:"stock_id"},
                          {text:"收盤價 (元)",dataIndex:"close"},
                          {text:"成交量 (張)",dataIndex:"volumn",flex:1}]
                 });
      });
  </script>
{% endblock %}

在上面的 extjs_datatable.htm 中, 我們先繼承 extjs.htm, 就直接套入網頁的 body 內容. 表格在 ExtJS 中是由 GridPanel 元件來呈現, 並將其描繪於一個 div 元素中. 先把要呈現的資料放在 ArrayStore 物件中, 再將其餵給 GridPanel 物件來呈現. 如下所示 :

測試範例 2 : http://mygaetestweb.appspot.com/extjs_datatable [下載 zip 檔]



ExtJS 的中文化不需要額外檔案支援, 但 jQuery UI 就需要, 為了給 DtaTables 外掛加上中文支援, 放面範例 1 還為此修改 app.yaml 檔. 為了爾後測試方便, 不用為了加入一個靜態檔案再次修改 app.yaml 檔, 我把上面範例 1 稍作修改, 把全部靜態檔案全部放在根目錄下新建的目錄 static, 然後修改 app.yaml 如下 :

application: mygaetestweb
version: 1
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: /favicon\.ico
  static_files: favicon.ico
  upload: favicon\.ico
- url: /static
  static_dir: static
- url: .*
  script: main.app

libraries:
- name: webapp2

  version: "2.5.2"

修改的部分是刪除原先靜態檔案 dataTables.zh-tw.txt 的 URL 設定, 改為 static 靜態目錄. 其次, 因為此檔已被移到 static 目錄下, 因此 jquery_datatable.htm 中 DataTables 參數的路徑也要修改 :

{% extends "jquery.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
  <table id="table1"></table>
  <script language="JavaScript">
    $(document).ready(function(){ 
      var opt={"oLanguage":{"sUrl":"/static/dataTables.zh-tw.txt"},
               "bJQueryUI":true,
               "aoColumns":[{"sTitle":"股票名稱"},
                            {"sTitle":"股票代號"},
                            {"sTitle":"收盤價 (元)"},
                            {"sTitle":"成交量 (張)"}],
               "aaData":[["台積電","2330",111.5,19268],
                         ["中華電","2412",95.1,7096],
                         ["中碳","1723",145.0,317],
                         ["創見","2451",104.0,459],
                         ["華擎","3515",104.0,95],
                         ["訊連","5203",98.5,326]]
               };
      $("#table1").dataTable(opt);
      });
  </script>
{% endblock%}

經過這樣修改架構後, 後續的 GAE 測試裡應該就不用再動到 app.yaml 這個設定檔了, 只須改 main.py 即可. 

測試範例 3 : http://mygaetestweb.appspot.com/jquery_datatable [下載 zip 檔]

以上便是在 GAE 上部署 jQuery UI 與 ExtJS 專案的簡單測試, 我覺得會比用 GWT 效果要好. 最重要的是, Google 雲端伺服器遍布全球, 非常的穩, 反應速度快, 所提供的開發工具也簡單易用. 目前提供 Python, Java, Go, 以及 PHP (試用中) 四種開發用語言. 雖然 Java 我比較熟, 但我愛死 Python 了, 所以多學一種語言也是一種成長.




沒有留言 :