2015年12月31日 星期四

如何在 GAE 上佈署 jQuery EasyUI 專案 (二) : CDN 資源

今天是 2015 年的最後一天了, 以往都是必休假已休完, 只得乖乖上班到年尾; 今年則是該怎麼請休假拿捏不定, 乾脆年底一起請, 變成連休五天, 真是爽 (同事說這樣社會觀感不好 ...).

早上在家看以前寫的 GAE 程式, 已經很久沒摸 Python 了有點生疏 (我是為了要用 GAE 服務才接觸 Python 的), 不過複習一下馬上就能恢復戰力. 參考去年十月底寫的這篇 :

# 如何在 GAE 佈署 jQuery EasyUI 專案

當時我的作法是採用自備框架資源的方式, 也就是自行下載 jQuery 與 EasyUI 檔案到 GAE 的專案目錄下, 但由於 GAE 對於免費帳戶有檔案數目限制, 而 EasyUI 又包含了一堆 UI 排版的圖檔與樣式檔, 上傳到 GAE 後就佔硬碟空間; 比較理想的方式是採用 CDN 供檔方式, 反正 GAE 專案本來就是要佈署到 Internet 上才有意義的. 因此我把 jqueryeasyui.htm 修改為 jqueryeasyui_1.htm 如下 :

{% extends "html5.htm" %}
{% block link %}
  <link rel="stylesheet" href="http://www.jeasyui.com/easyui/themes/default/easyui.css">
  <link rel="stylesheet" href="http://www.jeasyui.com/easyui/themes/icon.css">
{% endblock %}
{% block javascript %}
  <script type="text/javascript" src="http://www.jeasyui.com/easyui/jquery.min.js"></script>
  <script type="text/javascript" src="http://www.jeasyui.com/easyui/jquery.easyui.min.js"></script>
  <script type="text/javascript" src="http://www.jeasyui.com/easyui/locale/easyui-lang-zh_TW.js"></script>
{% endblock %}

關於 Template 用法可參考這篇 :

# Google App Engine 初學(8) – 共用 Template (Layout)

然後就可以寫一個 easyui_1.htm 來測試 Datagrid 了 :

{% extends "jqueryeasyui_1.htm" %}
{% block body %}
  <table class="easyui-datagrid" title="台股" style="width:600px;height:230px"
    data-options="singleSelect:true,collapsible:true,rownumbers:true">
    <thead>
      <tr>
        <th data-options="field:'name',width:80">股票名稱</th>
        <th data-options="field:'id',width:80">股票代號</th>
        <th data-options="field:'close',width:100,align:'right'">收盤價 (元)</th>
        <th data-options="field:'volumn',width:100,align:'right'">成交量 (張)</th>
        <th data-options="field:'meeting',align:'left'">股東會日期</th>
        <th data-options="field:'election',width:80,align:'center'">董監改選</th>
        <th data-options="field:'category',width:60,align:'center'">類股</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>台積電</td>
        <td>2330</td>
        <td>123</td>
        <td>4425119</td>
        <td>2014-06-04</td>
        <td>0</td>
        <td>半導體</td>
      </tr>
      <tr>
        <td>中華電</td>
        <td>2412</td>
        <td>96.4</td>
        <td>5249</td>
        <td>2014-06-15</td>
        <td>0</td>
        <td>通信</td>
      </tr>
      <tr>
        <td>中碳</td>
        <td>1723</td>
        <td>192.5</td>
        <td>918</td>
        <td>2014-07-05</td>
        <td>0</td>
        <td>塑化</td>
      </tr>
      <tr>
        <td>創見</td>
        <td>2451</td>
        <td>108</td>
        <td>733</td>
        <td>2014-06-30</td>
        <td>0</td>
        <td>模組</td>
      </tr>
      <tr>
        <td>華擎</td>
        <td>3515</td>
        <td>118.5</td>
        <td>175</td>
        <td>2014-07-20</td>
        <td>0</td>
        <td>主機板</td>
      </tr>
      <tr>
        <td>訊連</td>
        <td>5203</td>
        <td>97</td>
        <td>235</td>
        <td>2014-05-31</td>
        <td>0</td>
        <td>軟體</td>
      </tr>
    </tbody>
  </table>
{% endblock %}

但是為了符合 MTV 設計要求, 我在專案目錄下面建了一個 templates 目錄, 把上面的 htm 模板 (Template) 檔案通通放到 templates 下, 這樣檔案結構比較不會凌亂, 如下圖所示 :


所以路由分派程式 main.py 中的 url 必須加上 templates 路徑 :

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

class easyui_1(webapp2.RequestHandler):
    def get(self):
        url="templates/easyui_1.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):
        url="templates/default.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

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

這裡 templates 下還有一個 default.htm 是做為程式目錄列表的, 當存取網站根目錄 (即上面的 "/") 時, 會執行 MainHandler 類別, 抓取 templates 下的 default.htm 來渲染 (輸出). 其內容暫時如下, 當程式增加時就增加 li 項目即可 :

{% extends "jqueryeasyui_1.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
<b><p>EasyUI on GAE</p></b>
<ol>
  <li>
    <a href="easyui_1" target="_blank">easyui_1</a>
    (Datagrid : 顯示股價資訊)
  </li>
</ol>
{% endblock%}

這樣就大功告成了, 在 SDK 上執行 OK, 結果與之前第一篇文章一樣. 上傳到 GAE 時顯示不超過十個檔案, 使用 CDN 的好處就是可以節省硬碟容量, 檔案結構也很清爽.

 接下來我想測試 EasyUI 主題布景的切換, 直覺是用一個下拉式選單來選取主題, 然後提交時傳一個參數給 GAE, 用來置換樣式表路徑中預設的 default 主題:

<link rel="stylesheet" href="http://www.jeasyui.com/easyui/themes/default/easyui.css">

這可以用 template 引擎來傳送參數, 在上面 main.py 程式中呼叫 render 方法時, 所傳入的第二參數為一個空串列 template.render(path,{}), 那是因為我們沒有變數要傳給模板檔案的緣故; 我們可以用 self.request.get() 方法擷取網頁中下拉式選單傳來的主題布景參數, 然後透過此串列變數傳給模板.

我把 jqueryeasyui_1.htm 模板加入變數 theme, 改成 jqueryeasyui_2.htm 如下

{% extends "html5.htm" %}
{% block link %}
  <link rel="stylesheet" href="http://www.jeasyui.com/easyui/themes/{{theme}}/easyui.css">
  <link rel="stylesheet" href="http://www.jeasyui.com/easyui/themes/icon.css">
{% endblock %}
{% block javascript %}
  <script type="text/javascript" src="http://www.jeasyui.com/easyui/jquery.min.js"></script>
  <script type="text/javascript" src="http://www.jeasyui.com/easyui/jquery.easyui.min.js"></script>
  <script type="text/javascript" src="http://www.jeasyui.com/easyui/locale/easyui-lang-zh_TW.js"></script>
{% endblock %}

注意這裡樣式表路徑中, 用兩個大括弧括起來的 theme 就是從 main.py 程式傳來的模板變數, 而 main.py 則是從 url 處理中從前端網頁取得我們傳送之樣式參數. 而前端網頁則繼承 jqueryeasyui_2.htm, 加入 Combobox 選單, 改為 easyui_2.htm 如下 :

{% extends "jqueryeasyui_2.htm" %}
{% block body %}
  <form id="theme_form" method="get"  action="/easyui_2">
    <select id="theme_sel" name="theme" class="easyui-combobox" panelHeight="auto">
      <option value="default">主題布景</option>
      <option value="gray">gray</option>
      <option value="black">black</option>
      <option value="bootstrap">bootstrap</option>
      <option value="metro">metro</option>
      <option value="metro-blue">metro-blue</option>
      <option value="metro-gray">metro-gray</option>
      <option value="metro-green">metro-green</option>
      <option value="metro-orange">metro-orange</option>
      <option value="metro-red">metro-red</option>
      <option value="ui-cupertino">ui-cupertino</option>
      <option value="ui-dark-hive">ui-dark-hive</option>
      <option value="ui-pepper-grinder">ui-pepper-grinder</option>
      <option value="ui-sunny">ui-sunny</option>
    </select><br>
  </form><br>
  <table class="easyui-datagrid" title="台股" style="width:600px;height:230px"
    data-options="singleSelect:true,collapsible:true,rownumbers:true">
    <thead>
      <tr>
        <th data-options="field:'name',width:80">股票名稱</th>
        <th data-options="field:'id',width:80">股票代號</th>
        <th data-options="field:'close',width:100,align:'right'">收盤價 (元)</th>
        <th data-options="field:'volumn',width:100,align:'right'">成交量 (張)</th>
        <th data-options="field:'meeting',align:'left'">股東會日期</th>
        <th data-options="field:'election',width:80,align:'center'">董監改選</th>
        <th data-options="field:'category',width:60,align:'center'">類股</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>台積電</td>
        <td>2330</td>
        <td>123</td>
        <td>4425119</td>
        <td>2014-06-04</td>
        <td>0</td>
        <td>半導體</td>
      </tr>
      <tr>
        <td>中華電</td>
        <td>2412</td>
        <td>96.4</td>
        <td>5249</td>
        <td>2014-06-15</td>
        <td>0</td>
        <td>通信</td>
      </tr>
      <tr>
        <td>中碳</td>
        <td>1723</td>
        <td>192.5</td>
        <td>918</td>
        <td>2014-07-05</td>
        <td>0</td>
        <td>塑化</td>
      </tr>
      <tr>
        <td>創見</td>
        <td>2451</td>
        <td>108</td>
        <td>733</td>
        <td>2014-06-30</td>
        <td>0</td>
        <td>模組</td>
      </tr>
      <tr>
        <td>華擎</td>
        <td>3515</td>
        <td>118.5</td>
        <td>175</td>
        <td>2014-07-20</td>
        <td>0</td>
        <td>主機板</td>
      </tr>
      <tr>
        <td>訊連</td>
        <td>5203</td>
        <td>97</td>
        <td>235</td>
        <td>2014-05-31</td>
        <td>0</td>
        <td>軟體</td>
      </tr>
    </tbody>
  </table>
  <script>
    $(document).ready(function(){
      $("#theme_sel").combobox({
        onSelect:function(rec){
          $("#theme_form").submit();
          }
        });
      });
  </script>
{% endblock %}

這裡下拉式選單列舉了 EasyUI 的各種主題, 注意 select 元素須給予 name 屬性, 當有選取動作時呼叫 submit() 方法將表單傳送給自己 (/easyui_2). 其次, 為了簡單起見, 表單用 get 方法提交, 因為若使用 post, 則 main.py 裡面也要有處理 post 的方法 (否則會出現 405 Method Not Allowed : The method POST is not allowed for this resource. 錯誤). 以下是修改路徑處理之 main.py :

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

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

class easyui_2(webapp2.RequestHandler):
    def get(self):
        theme=self.request.get('theme',default_value='default')
        url="templates/easyui_2.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{'theme':theme})
        self.response.out.write(content)

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

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

此處我們用 self.request.get() 取得前端傳來的 theme 參數 (第一次存取網頁時不會傳參數, 因此用 default_value 設置預設為 default), 然後在呼叫 render() 渲染網頁時, 將此 theme 變數傳給 easyui_2.htm 模板, 從而代換了 jqueryeasyui_2.htm 中的決定主題布景之樣式表路徑.

不過實際操作會發現, 這種每次選取就提交表單的做法不好, 因為這樣會重新載入頁面, 導致前端會短暫看到 UI 尚未套用前的原始面貌. 理想上應該不須重載頁面, 直接利用 jQuery 去操作 DOM 裡的 link 元素即可, 因此我修改模板, 在 link 元素中加入 id 屬性方便 jQuery 操作, 同時恢復預設之 default 主題, 改為如下的 jqueryeasyui_3.htm 模板 :

{% extends "html5.htm" %}
{% block link %}
  <link  id="theme" rel="stylesheet" href="http://www.jeasyui.com/easyui/themes/default/easyui.css">
  <link rel="stylesheet" href="http://www.jeasyui.com/easyui/themes/icon.css">
{% endblock %}
{% block javascript %}
  <script type="text/javascript" src="http://www.jeasyui.com/easyui/jquery.min.js"></script>
  <script type="text/javascript" src="http://www.jeasyui.com/easyui/jquery.easyui.min.js"></script>
  <script type="text/javascript" src="http://www.jeasyui.com/easyui/locale/easyui-lang-zh_TW.js"></script>
{% endblock %}

然後寫一個 easyui_3.htm 繼承此模板如下 :

{% extends "jqueryeasyui_3.htm" %}
{% block body %}
  <form id="theme_form">
    <select id="theme_sel" name="theme" class="easyui-combobox" panelHeight="auto">
      <option value="default">主題布景</option>
      <option value="gray">gray</option>
      <option value="black">black</option>
      <option value="bootstrap">bootstrap</option>
      <option value="metro">metro</option>
      <option value="metro-blue">metro-blue</option>
      <option value="metro-gray">metro-gray</option>
      <option value="metro-green">metro-green</option>
      <option value="metro-orange">metro-orange</option>
      <option value="metro-red">metro-red</option>
      <option value="ui-cupertino">ui-cupertino</option>
      <option value="ui-dark-hive">ui-dark-hive</option>
      <option value="ui-pepper-grinder">ui-pepper-grinder</option>
      <option value="ui-sunny">ui-sunny</option>
    </select><br>
  </form><br>
  <table class="easyui-datagrid" title="台股" style="width:600px;height:230px"
    data-options="singleSelect:true,collapsible:true,rownumbers:true">
    <thead>
      <tr>
        <th data-options="field:'name',width:80">股票名稱</th>
        <th data-options="field:'id',width:80">股票代號</th>
        <th data-options="field:'close',width:100,align:'right'">收盤價 (元)</th>
        <th data-options="field:'volumn',width:100,align:'right'">成交量 (張)</th>
        <th data-options="field:'meeting',align:'left'">股東會日期</th>
        <th data-options="field:'election',width:80,align:'center'">董監改選</th>
        <th data-options="field:'category',width:60,align:'center'">類股</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>台積電</td>
        <td>2330</td>
        <td>123</td>
        <td>4425119</td>
        <td>2014-06-04</td>
        <td>0</td>
        <td>半導體</td>
      </tr>
      <tr>
        <td>中華電</td>
        <td>2412</td>
        <td>96.4</td>
        <td>5249</td>
        <td>2014-06-15</td>
        <td>0</td>
        <td>通信</td>
      </tr>
      <tr>
        <td>中碳</td>
        <td>1723</td>
        <td>192.5</td>
        <td>918</td>
        <td>2014-07-05</td>
        <td>0</td>
        <td>塑化</td>
      </tr>
      <tr>
        <td>創見</td>
        <td>2451</td>
        <td>108</td>
        <td>733</td>
        <td>2014-06-30</td>
        <td>0</td>
        <td>模組</td>
      </tr>
      <tr>
        <td>華擎</td>
        <td>3515</td>
        <td>118.5</td>
        <td>175</td>
        <td>2014-07-20</td>
        <td>0</td>
        <td>主機板</td>
      </tr>
      <tr>
        <td>訊連</td>
        <td>5203</td>
        <td>97</td>
        <td>235</td>
        <td>2014-05-31</td>
        <td>0</td>
        <td>軟體</td>
      </tr>
    </tbody>
  </table>
  <script>
    $(document).ready(function(){
      $("#theme_sel").combobox({
        onSelect:function(rec){
          var css="http://www.jeasyui.com/easyui/themes/" + rec.value + "/easyui.css";
          $("#theme").attr("href", css);
          }
        });
      });
  </script>
{% endblock %}

這裡重點是在 onSelect() 方法中, 透過傳回值的 value 屬性取得所選取之值, 然後重組主題樣式的路徑, 再用 jQuery 的 attr() 方法將 href 屬性更換掉. 當然控制程式 main.py 也要添加 easyui_3 的路徑處理器, 但其實只要複製 easyui_1 的過來改即可, 如下所示 :

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

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

class easyui_2(webapp2.RequestHandler):
    def get(self):
        theme=self.request.get('theme',default_value='default')
        url="templates/easyui_2.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{'theme':theme})
        self.response.out.write(content)

class easyui_3(webapp2.RequestHandler):
    def get(self):
        url="templates/easyui_3.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):
        url="templates/default.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

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

這個做法比較好, 切換主題時完全不會閃動, 可說是無縫變換, 只有在第一次載入頁面時可能會看到尚未化妝完畢的原始頁面. 後續的測試將以此作法所使用的模板 jqueryeasyui_3.htm 為標準, 故複製一份為 jqueryeasyui.htm 做後續繼承之用.


以上網頁可以在下列網址看到實際效果 :

http://jqueryeasyui.appspot.com/

OK, 今天先做到這邊, 今天是休假耶, 我卻寫了一整天的程式. 昨晚睡覺前菁菁問我說, 今天我休假可不可以先煮菜, 放學後回來洗完澡吃完趕去補習班. 所以我三點半就開始準備, 趁四點菁菁回來前煮了一小鍋豬肉丼, 她吃第一口就說讚讚讚, 這就是讓我愛上烹飪的原因啦.

以上範例程式碼可在下列網址下載 :

下載原始碼


沒有留言 :