Fork me on GitHub

Programming Design Notes

助你快速開發網站的 Twitter Bootstrap

| Comments



Twitter Bootstrap 有點像以前曾經介紹過的 Blueprint960 Grid System 這類的 CSS FrameworkTwitter Bootstrap 一樣是能夠幫助你快速設置好網頁佈局的一個 Framework,那這個 Twitter Bootstrap 和前 2 者有什麼分別呢?

介面對一個網站是極奇重要的,就算你背後的程式有多強多勁,但沒有一個好的介面也是會把用戶趕走的。Twitter Bootstrap 不單只幫助你設置網頁佈局,亦提供了一體化的介面及一些常用的網頁元件,這些元件都經過精心設計,就算不懂任何任何圖像軟件也能砌出一個美觀的網站(我一直最對網站設計最感頭痛的就是整體一致化)。
CSS 元件方面有:
  1. 網頁佈局的 Grid (有 Fixed layoutFluid layout
  2. 常用標題大小設置 Typography
  3. 列舉 List
  4. 提示貼紙 Inline labels
  5. 媒體框架 Media grid
  6. Table
  7. Input box
  8. Select box
  9. 按鈕 Button
  10. 導航列 Navigation bar
  11. 標籤 Taps
  12. 顯示層級目錄的元件 Breadcrumbs
  13. 頁數工具 Pagination
  14. 提示 Alerts & Errors

除了 CSS 外元件外,Twitter Bootstrap 亦包括了一些 Javascript 元件:
  1. 對話框 Modals
  2. Tooltips
  3. Popovers

元件方面真的比其他 CSS Framework 多太多了,使用了 Twitter Bootstrap 都可以做出一個不太差的網站。

因為有些元件和我現有的 CSS 相衝,還是看示範比較好。

有興趣可到 http://twitter.github.com/bootstrap/ 玩玩看示範。
下載可到 https://github.com/twitter/bootstrap
到目前為止最新穩定版本是 1.4

自訂 EclipseLink Logger

| Comments



預設的 EclipseLink logger 是會將所有訊息都會打印到 System.out 去,但 System.out 的記錄通常很難查找。如果不同的應用程式都在同一台 Server 上運行就更加混亂,不同的應用程式都會向 System.out 送出資料,根本不能拿來分析特定一個應用程式的行為。通常做法是會將不同的應用程式都設定不同的日誌檔案,以便分析。

要將 EclipseLink 的日誌寫到自己的日誌檔案是很簡單的,只需 Extend org.eclipse.persistence.logging.AbstractSessionLog 這個 class,並且實現 public void log(SessionLogEntry sessionLogEntry) 就可以了,以下有一個例子是將 EclipseLink 的日誌經由 Slf4j 傳送到自訂的日誌檔案中。

package com.ctlok.pro.log

import org.eclipse.persistence.logging.AbstractSessionLog;
import org.eclipse.persistence.logging.SessionLogEntry;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyEclipseLinkLogger extends AbstractSessionLog {

private static final Logger logger = LoggerFactory.getLogger(MyEclipseLinkLogger.class);

@Override
public void log(SessionLogEntry sessionLogEntry) {
switch (sessionLogEntry.getLevel()) {
case SEVERE:
logger.error(sessionLogEntry.getMessage());
break;
case WARNING:
logger.warn(sessionLogEntry.getMessage());
break;
case INFO:
logger.info(sessionLogEntry.getMessage());
break;
case FINE:
default:
logger.debug(sessionLogEntry.getMessage());
}
}

}

然後在 persistence.xml 的 propertiese 中加上:
<property name="eclipselink.logging.logger" value="com.ctlok.pro.log.MyEclipseLinkLogger "/>

這樣就可以令 EclipseLink 使用自訂的 Logger 去打印日誌了。

PHP CodeIgniter 根據瀏覽器語言來顯示語言

| Comments




近排為客戶製作一個網頁,因為客戶已經買下了虛擬主機空間,不能用 Java 去作 Server side language,只好使用 PHP + CodeIgniter。他要求有轉換語言的功能,但發覺 CodeIgniter 本身的 i18n 功能不太完善,只好自己製作一個。

這個 i18n 有點像 JSFi18n 功能,是根據用戶的瀏覽器語言來判定顯示那一種語言,並且對 Controller 控制語言的程式碼減少(可以 1 行程式碼也不用寫就能夠自動設換各種語言)。這可以減少重複的程式碼和減低 Controller 的複雜性。

我己經將這個 Library 放上 github 了,有興趣可以下載來試試 https://github.com/lawrence0819/php-ci-internationalization

以下就是 I18n.php (Core),放到 application/libraries/I18n.php
<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

/**
* I18n class
*
* @package CodeIgniter
* @subpackage Libraries
* @category Libraries
* @author Lawrence Cheung
* @version 1.0
* @link https://github.com/lawrence0819
*/
class I18n{

protected $CI;
protected $auto = TRUE;
protected $loaded = FALSE;
protected $locale;

/**
* Constructor - get CI instance
*
*/
public function __construct(){
$this -> CI = get_instance();
}

/**
* Auto load language for CI HOOK
*
* @access public
* @return void
*/
public function auto_load_language(){
if ($this -> auto){
$this -> load_language();
}
}

/**
* Manual load language file
*
* @access public
* @return void
*/
public function load_language() {
$lang = $this -> get_current_locale();
$language = $this -> get_language_config();

if (!$this -> loaded) {
$files = $language['files'];
$locale = $language['locale'];

if (array_key_exists($lang, $locale)) {
$folder = $locale[$lang];
} else {
$shortLang = substr($lang, 0, 2);
if (array_key_exists($shortLang, $locale)) {
$folder = $locale[$shortLang];
} else {
$folder = $locale['default'];
}
}

foreach ($files as $file) {
$this -> CI -> lang -> load($file, $folder);
}

$this -> loaded = TRUE;
}
}

/**
* Prevent CI Hook to auto load language file
*
* @access public
* @return void
*/
public function prevent_auto(){
$this -> auto = FALSE;
}

/**
* Set current user locale and save locale to cookies
*
* @access public
* @Param string the locale string: en-US, en-UK, zh-TW, zh-CN
* @Param integer the cookies value expire time, default is 30 day
* @Param string cookies key
* @return void
*/
public function set_current_locale($locale, $expire = 259200, $cookie_key = 'locale') {
setcookie($cookie_key, $locale, time() + $expire);
$this -> locale = $locale;
}

/**
* Get current user locale
*
* @access public
* @Param string cookies key, if you changed the key at set_current_locale, please assign it
* @return string
*/
public function get_current_locale($cookie_key = 'locale') {
if (!$this -> locale){
if (isset($_COOKIE[$cookie_key])) {
$lang = $_COOKIE[$cookie_key];
} else {
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 5);
} else {
$language = $this -> get_language_config();
$lang = $language['default_locale'];
}
}
$this -> locale = $lang;
}
return $this -> locale;
}

/**
* Get configuration values
*
* @access default
* @return array
*/
function get_language_config(){
$this -> CI -> config -> load('i18n');
return $this -> CI -> config -> item('language');
}

}

/* End of file I18n.php */
/* Location: ./application/libraries/I18N.php */

以下的是所需要的設定檔案 i18n.php (Core),放到 application/config/i18n.php
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');

/**
* I18n library configuration file
*
* @package CodeIgniter
* @subpackage Libraries
* @category Libraries
* @author Lawrence Cheung
* @version 1.0
* @link https://github.com/lawrence0819
*/

//Add file in this array, if you want I18n library auto load them
$config['language']['files'] = array('messages');

//If user locale not found, set this valus as a defaul user locale
$config['language']['default_locale'] = 'en';

//Default language folder, if locale folder not found
$config['language']['locale']['default'] = 'tchinese';

//zh-CN locale mapped to schinese folder
$config['language']['locale']['zh-CN'] = 'schinese';

//zh-TW locale mapped to tchinese folder
$config['language']['locale']['zh-TW'] = 'tchinese';

//en locale mapped to schinese folder
$config['language']['locale']['en'] = 'english';

/* End of file i18n.php */
/* Location: ./application/config/i18n.php */

英文語言檔 messages_lang.php,放到 application/language/english/messages_lang.php
<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

$lang['site_name'] = 'Example site';
$lang['message'] = 'Hello World!';

/* End of file messages_lang.php */
/* Location: ./application/language/english/messages_lang.php */

正體中文語言檔 messages_lang.php,放到 application/language/tchinese/messages_lang.php
<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

$lang['site_name'] = '範例網站';
$lang['message'] = '世界,你好!';

/* End of file messages_lang.php */
/* Location: ./application/language/tchinese/messages_lang.php */

簡體中文語言檔 messages_lang.php,放到 application/language/schinese/messages_lang.php
<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

$lang['site_name'] = '范例网站';
$lang['message'] = '世界,你好!';

/* End of file messages_lang.php */
/* Location: ./application/language/schinese/messages_lang.php */


設定就完成了,如果你有更多語言或不使用 englishtchineseschinese 的名稱,請更改 application/config/i18n.php


使用方法如下:

手動選擇語言的 Controller:
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');

class I18nmanual extends CI_Controller{

public function __construct(){
parent::__construct();
$this -> load -> library('i18n');
}

public function index(){
$this -> i18n -> load_language();
$this -> load -> view('i18ntest');
}

}

/* End of file i18nmanual.php */
/* Location: ./application/controller/i18nmanual.php */

View:
<html>
<head>
<title><?php echo $this->lang->line('site_name'); ?></title>
</head>
<body>
<?php echo $this->lang->line('message'); ?>
</body>
</html>

將會見到和 Browser 相對應的語言。

如果覺得要在 __construct 內加上
$this -> load -> library('i18n');

太麻煩,可以在 application/config/autoload.php 自動載入 i18n library:
$autoload['libraries'] = array('i18n');

開啟全自動的功能要先在 application/config/config.php 更改以下程式碼:
$config['enable_hooks'] = TRUE;

再將 application/config/hooks.php 加入以下程式碼:
$hook['post_controller_constructor'][] = array(
'class' => 'I18n',
'function' => 'auto_load_language',
'filename' => 'I18n.php',
'filepath' => 'libraries'
);

Controller:
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');

class I18nauto extends CI_Controller{

public function __construct(){
parent::__construct();
$this -> load -> library('i18n');
}

public function index(){
$this -> load -> view('i18ntest');
}

}

/* End of file i18nauto.php */
/* Location: ./application/controller/i18nauto.php */

如果有設定自動載入 i18n library,就連 __construct 也不需要。

如果開啟了全自動的功能,但有些 Controller 又不想自動選擇語言,可以在 __construct 加上:
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');

class I18nmanual extends CI_Controller{

public function __construct(){
parent::__construct();
$this -> load -> library('i18n');
//prevent auto load language
$this -> i18n -> prevent_auto();
}

public function index(){
$this -> i18n -> set_current_locale('zh-TW');
$this -> i18n -> load_language();
$this -> load -> view('i18ntest');
}

}

/* End of file i18nmanual.php */
/* Location: ./application/controller/i18nmanual.php */

一經切換的語系,就會自動將此更改儲存到 Cookies,下一次就會以 Cookies 的語系來選擇語言檔。

說明完畢,有什麼問題可以留言給我。

放棄 Google App Engine 轉投 Amazon Web Service

| Comments

自從 Google App Engine 轉換收費模式後,已經計劃將現有的 Project 放到 Amazon Web Service 上,以前選用 GAE 的原因是因為便宜,而且有免費使用量。雖然限制很多,但我並不介意這些限制。例如將現有的 Application 移植到上 GAE 上是一件非常痛苦的事,JPA 有一大堆東西不能用,大部份 Entity 要重新規劃,資料移植又是另一件痛苦的事。因為每一個 HTTP Request 也有 30 秒限制,上傳檔案又要另外寫一次。因為價錢便宜,我不介意改。

現在 GAE 有新的收費模式,不是以前只計 CPU Hour 的方式,是一個小時一個小時地計算。向 Datasource 存取資料又另外收費,新的收費模式:

新的收費模式很像 Amazon Web Service,我最不能接受的是 GAE 也跟 AWS EC2 以小時計算費用,EC2 收費如下:

EC2 一個小型 (Small) Virtual Machine 一小時才 $0.085 USDGAE 一小時也要 $0.080 USD,但 GAEVirtual Machine 全自動起動的,即是說你不能控制機器數目,雖然你可以設定 Max Idle Instances 數目,但系統在需要時也一樣會新增比你設定的最大數目還多的機器 (我試過好幾次)。如果你起動了 2 台機器,就是 $0.080 USD * 2,一小時收費 $ 0.16 USDEC2 是可以一台機器放置多個 應用程式,而 GAE 只可以一台機器放置一個應用程式,還是 EC2 比較化算。

最後如果你仍然選擇留守 GAE,這裡有幾個方法可幫助你省錢:

1. 在 appengine-web.xml 加上:
<threadsafe>true</threadsafe>

可以令你的機器同時處理多個請求 (Request)

2. 設定 Max Idle InstancesGAE 盡量不起動超過這個數量的機器
3. 設定 Min Pending Latency 在機器處理一個請求超過這一個時間才新增機器 (其實我覺得這個設定很雞肋,每一個請求時間也不一樣,又不可以設定太高,亦不能設定太低)
4. 盡量用多一點 Cache ,減少存取 Datasource

使用 Google Closure Compiler 在執行期間壓縮 Javascript

| Comments

趁有空又寫寫文章,今次介紹的是 Google Closure Compiler,其實之前已經介紹過一次: 線上 Javascript 工具,這個工具可以將 Javascript 的大小大幅降低。

使用線上工具去壓縮有一個缺點就是要將 Javascript 檔案儲存成 2 份,1 份是原始檔案,另 1 份是經壓縮內容的 Javascript,因為不可能更改經壓縮過的內容,每一次更改檔案就需要更改原始檔案,然後利用 Google Closure Compiler 線上工具再壓縮一次,再更新壓縮內容,而且經壓縮的 Javascript 在瀏覽器 debug 亦比較困難。雖然你可以在 HTML 將引入的 Javascript 檔案改為未經壓縮然後 debug,但萬一忘記改回就麻煩了。

幸好 Google Closure Compiler 有提供到 Java 使用的 API 來達到執行期間 (Run Time)Javascript 壓縮。

首先到 Google Code 下載: Google Closure Compiler

可用以下其中一個方法去壓縮:
protected String compress(InputStream inputStream) throws IOException{
Compiler compiler = new Compiler();

CompilerOptions options = new CompilerOptions();
CompilationLevel.SIMPLE_OPTIMIZATIONS
.setOptionsForCompilationLevel(options);

JSSourceFile extern = JSSourceFile.fromCode("externs.js", " ");
JSSourceFile input = JSSourceFile.fromInputStream("origin.js", inputStream);
compiler.compile(extern, input, options);

return compiler.toSource();
}

protected String compress(String str) throws IOException{
Compiler compiler = new Compiler();

CompilerOptions options = new CompilerOptions();
CompilationLevel.SIMPLE_OPTIMIZATIONS
.setOptionsForCompilationLevel(options);

JSSourceFile extern = JSSourceFile.fromCode("externs.js", " ");
JSSourceFile input = JSSourceFile.fromCode("origin.js", str);
compiler.compile(extern, input, options);

return compiler.toSource();
}

其中 CompilationLevel 分別有 3 個選項:
  • CompilationLevel.WHITESPACE_ONLY
  • CompilationLevel.SIMPLE_OPTIMIZATIONS
  • CompilationLevel.ADVANCED_OPTIMIZATIONS

WHITESPACE_ONLY 只會移除 Javascript 的空白。

SIMPLE_OPTIMIZATIONS 是最常用的一種,移除 Javascript 的空白,而且將一些 Local VariableLocal Function 名稱改變,並將一些沒有用到的 Variable 移除,大幅提高 Javascript 的壓縮率。

ADVANCED_OPTIMIZATIONS 是最高壓縮率的模式,將所有 VariableFunction 的名稱改變,有使用 Javascript Framework 不建議使用這個選項。

在真實環境中我們可以加入一個 Filter 去將 Javascript 壓縮:
package com.ctlok.pro.filter;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.JSSourceFile;
import com.google.javascript.jscomp.WarningLevel;

public class ClosureCompilerFilter implements Filter {

//for cache
private final Map<String, String> compressedJs = new HashMap<String, String>();
private FilterConfig filterConfig;

public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;

//Turn off the compiler log
Compiler.setLoggingLevel(Level.OFF);
}

public void destroy() {

}

public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {

HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;

String uri = req.getRequestURI();

//Prevent compress the compressed Javascript, such as jquery.min.js, mootools.min.js, etc.
if (!uri.matches(".*\\.min\\.js$")){
String js = null;

if (compressedJs.containsKey(uri)){
//get from cache
js = compressedJs.get(uri);
}else{
String contextPath = req.getContextPath();
String jsPath = uri.substring(contextPath.length());

//get javascript file as stream
//getResourceAsStream cannot include context path
InputStream inputStream = filterConfig.getServletContext().getResourceAsStream(jsPath);
js = compress(inputStream);

//put to cache
compressedJs.put(uri, js);
}

resp.getWriter().write(js);
return;
}

chain.doFilter(request, response);
}

protected String compress(InputStream inputStream) throws IOException{
Compiler compiler = new Compiler();

CompilerOptions options = new CompilerOptions();
CompilationLevel.SIMPLE_OPTIMIZATIONS
.setOptionsForCompilationLevel(options);
WarningLevel.QUIET.setOptionsForWarningLevel(options);

JSSourceFile extern = JSSourceFile.fromCode("externs.js", "");
JSSourceFile input = JSSourceFile.fromInputStream("origin.js", inputStream);
compiler.compile(extern, input, options);

return compiler.toSource();
}

}

在 web.xml 加上:
<filter>
<display-name>ClosureCompilerFilter</display-name>
<filter-name>ClosureCompilerFilter</filter-name>
<filter-class>com.ctlok.pro.filter.ClosureCompilerFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ClosureCompilerFilter</filter-name>
<url-pattern>*.js</url-pattern>
</filter-mapping>

這樣就可以令到 .js 結尾的 Javascript 檔案經壓縮再傳出去,又不會將 .min.js 已經壓縮過的 Javascript 又再壓縮一次。
經過壓縮後會儲存在 Map 內,畢竟壓縮的時間也不短。

例如我有一個 /js/myjs.js 的檔案:
function ctlok() {
var self = this;
var $ = jQuery;

this.publicFunction = function() {
localFunction();
};

var localFunction = function() {
var aaaaa = 'aaaaa';
$(':input').val(aaaaa);
};
}

經壓縮後變成:
function ctlok(){var a=jQuery;this.publicFunction=function(){a(":input").val("aaaaa")}};

範例下載:: Google-Closure-Compiler.zip