Quantcast
Channel: 黑暗執行緒
Viewing all 2311 articles
Browse latest View live

Coding4Fun-馬拉松照片搜尋輔助工具

$
0
0

現在的跑者愈來愈幸福,每逄賽事總能見到馬拉松世界、運動筆記所出動或熱心自發的攝影大人,揹著沈重攝影器材,不畏日曬雨淋地在賽道旁補捉跑友身影,賽後再上傳至網站供跑友自取留念。(在此特向辛苦的攝影大哥大姐們致敬)

不過一場比賽常有成千上萬張照片,要在茫茫照片大海找尋自己的英姿有點小挑戰。照片雖然都已依拍攝時間排序,有時更會貼心註明拍攝地點,但要記下自己在何時何地遇到那家攝影師談何容易? (對我來說,記得要吸呼都快來不及了 XD) 我研究出最好的方法,是先由照片裡其他跑友的號碼布查出比賽成績,與自己的成績比較,藉以推算自己的出場時機,決定該要向前找還是向後翻,要跳過30分鐘還是一個小時,有了成績線索,找照片快多了。

手工作業過幾次,身為程式魔人手癢難耐,便用jQuery及Knockout寫了小工具網頁,配合以Greasemonkey預先由大會網站取得的成績資料,輸入號碼後立刻會帶出跑友成績。即能練功又實用,摸蛤仔兼洗褲來著~

程式即然都寫好了,不分享也是浪費,決定放上網站提供渣打馬跑友使用。網址: http://www.darkthread.net/mph/

使用方法很簡單,先選好半馬或全馬,接著輸入號碼,下方就會出現吻合的成績資料,按ESC則可清除輸入內容。

主程式不多,照例在一百行內搞定,一併補上供參:

<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>馬拉松成績速查</title>
<style>
        body { font-family:'Arial Unicode MS' }
        .hdr { padding: 12px; font-size: 16pt; background-color: #0094ff; color: white; }
        .query { padding: 6px; }
        .title { margin-right: 6px; }
        .keywd { width: 100px; padding: 3px; }
</style>
</head>
<body>
<divclass="hdr">
        2014渣打公益馬拉松 成績速查
</div>
<divclass="query">
<selectdata-bind="options: Groups, value: Group"></select>
<labelclass="title">號碼布</label>
<inputtype='text'class="keywd"
data-bind="value: RunnerNo, valueUpdate: 'afterkeyup', event: { keyup: onKeyUp }"/>
        (按ESC清除)
</div>
<divclass="hints">
<uldata-bind="foreach: Hints">
<li><spandata-bind="text: no"></span> - 
<spandata-bind="text: time"></span></li>
</ul>
</div>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js "></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js "></script>
<script src="Chartered2014-42K.js"></script>
<script src="Chartered2014-21K.js"></script>
<script>
function myViewModel() {
var self = this;
            self.RunnerNo = ko.observable("");
            self.onKeyUp = function (d, e) {
if (e.which == 27)
                {
                    self.RunnerNo("");
                }
            };
            self.Hints = ko.observableArray();
            self.Groups = ko.observableArray();
            self.Group = ko.observable();
            self.DataGroups = {};
for (var grpName in dataSource) {
var records = dataSource[grpName];
var data = $.map(Object.keys(records).sort(), function (n) {
return { no: n, time: records[n] };
                });
                self.DataGroups[grpName] = data;
                self.Groups.push(grpName);
            }
            self.Data = ko.computed(function () {
return self.DataGroups[self.Group()];
            });
 
            ko.computed(function () {
var key = self.RunnerNo();
if (key.length == 0 || isNaN(key)) {
                    self.Hints([]);
return;
                }
var count = 0;
var data = self.Data.peek();
var res = [];
                $.each(data, function (i, item) {
if (count >= 10) returnfalse;
if (item.no.indexOf(key) == 0) {
                        count++;
                        res.push(item);
                    }
                });
                self.Hints(res);
            }).extend({ throttle: 100 });
        }
var vm = new myViewModel();
        ko.applyBindings(vm);
</script>
</body>
</html>

2014萬金石馬拉松~

$
0
0

第14馬,萬金石馬拉松(先報萬金石,後來報渣打馬時算錯日期,以為隔兩週,卻不知不覺"連馬"),依教練團指示,本場仍採HRDR(Heart Rate Driven Running)策略,以輕鬆完賽為目標。

上週五(228)豔限高照,熱到讓人誤以為夏天到了。未料週六老天翻臉如翻書,立刻由晴轉陰,氣象局更預測週日將下探14度,萬里則有90%下雨機率,跟去年簡直如出一轍(週五26度,週日降到13度),更慘的是今年雨勢恐怕不小,狀況只會更糟。

聽了一夜的淅瀝雨聲,躺在被窩裡快萌生棄賽念頭。兩點多鬧鐘響起還是乖乖起床梳洗,清晨四點到自由廣場搭接駁車,台北市區細雨不停,凌晨時分天色昏暗,心頭更是灰暗~

   

5點多抵達會場,萬里的雨勢更大,只穿慢跑短褲的雙腿被風吹到直抖。有了去年穿風衣才沒變冰棒的經驗,今年沒多猶豫,乖乖拿出拋棄式雨衣套上,這回多了新裝備--魔術腰包,必要時可將整個雨衣塞進去,不用綁身上也不用手拿,重量也不構成負擔,Perfect!

6:30準時開跑,天色漸亮但雨勢未歇,約有一半的跑友都穿雨衣或外套,亦不乏只穿背心的勇者。

   

前半馬雨勢不斷。昨天在別號碼布時發現這回的號碼片紙質接近一般印DM的雪銅紙,不怎麼堅韌,上面還黏了訂書針盒子大小的晶片,有點重量。若下雨沾濕加上跑步甩動晶片,別針固定處八成要被扯破,為此我特別貼了膠帶強化。果不其然,才跑了八公里就當場目擊有人號碼布掉落,全程也遇到好幾位號碼布拿在手上的跑友。

補給站頗密集,即便是海綿站也常會加設飲水補給,但運動飲料的數量較少。固體食物則有小餐包、香蕉跟巧克力條(士力架的,比去年的迷你77乳加好些,不必咬到齜牙咧嘴)。救護站如去年準備了很多噴劑,但連問了幾個站,沒有凡士林沒有凡士林沒有凡士林... (出門塗的潤滑不足沒得補,只好繼續摩擦生火 orz)

   

雨勢在9點多10點左右總算轉小,海岸的風浪氣勢驚人,一路上節制心跳速度不超上限,跑起來輕鬆許多(說來好笑,我還旁跑旁為上週接到的新需求做系統分析,跑完Interface也開得差不多了 XD)。天氣不好,也怕淋濕手機,故沒拍太多相片。而萬金石變裝跑友不多,但還是看到了斑馬頭先生、麥當勞叔叔、爆炸頭拳擊手、穿制服的小學女生...

   

雖然是跑海岸線,還是有幾段上下坡。本次的額外收獲是不用去動物園也觀察到斑馬喝水(誤)

沒刻意設定目標,最後8K體力還足,小衝了幾段(心跳飆上來就改步行調息,降下後再衝一波,有點像在跑間歇),意外地保住43X,晶片時間4:31:47,還比去年進步3分鐘。

今年的獎牌很有分量,造型頗具設計感,個人認為完勝去年的女王頭。 XD

看影片偷按讚-Clickjacking活用入門(誤)

$
0
0

記得有一陣子,FB很流行"必須先按讚才能看影片"的分享貼文。討厭強迫中獎,我多半選擇不看,再不然就是自己Google去找YouTube原始影片。前陣子在JavaScript.tw FB社團看到TonyQ分享影片網站利用Clickjacking偷抓你的手按讚的伎倆,想起最近固定會收到幾個影片網站的新訊息通知(在粉絲團按過讚,預設會接收通知。PS: 想訂閱本站訊息通知的朋友,知道該怎麼做了吧? :P ),莫非自己被陰了還知道? 趕緊檢查按讚記錄,果然... 過去曾大言不慚說自己資安偏執,這下丟臉丟大了~~ (掩面)

丟臉沒關係,學到教訓增廣見聞才是重點,決定花點功夫研究學習一番。

所謂的Clickjacking,意指"誘導使用者誤認滑鼠將點選某個項目,背地裡卻藉由該點擊行為觸發其他動作"的手法,某些網頁行為必須使用者親自點擊才有效,進而以使用者身份存取資料或執行操作,惡意人士可藉此建構資安陷阱。在這次案例中,影片網站用Iframe內嵌了FB按讚鈕網頁,因隸屬不同網域,瀏覽器基於安全不允許影片網站透過JavaScript操控FB網頁執行按讚動作,誘導使用者不知不覺間點擊FB按讚鈕成為唯一的突破方式。

開始研究前,先來段示範。我們先查詢FB按讚記錄--空空如也。接著到影片網站按下影片紅色播放鈕,影片播了,讚也按了。回到FB按讚記錄重新查詢,馬上可查到對該FB社群的按讚記錄。

對Clickjacking手法好奇,特地開啟瀏覽器工具偵察一番。用Firefox的Page Inspector 3D檢視,可以看到播放鈕上藏了一堆透明的FB按讚鈕,按下播放鈕的同時,無形中也按了讚。

在另一個網站也看到類似手法。

除了將按鈕藏在固定位置,還有一種更巧妙的如影隨形法。如以下動態展示,滑鼠移動的同時,<iframe>的left與top會不斷修正,目的在使iframe追著滑鼠游標跑,不管滑鼠點在網頁的任何角落,都將不偏不倚點在iframe上,"讚"吧?

初步觀察,這些行為應未構成嚴重資安危害,頂多讓人在不知情下為特定對象按讚(網站可能因此獲利)。不得不承認這些網站的確投入精力蒐羅新奇有趣養眼資訊,不然人們也不會被吸引點閱,但強迫取分的手法實在不怎麼光明正大。

如果不想"讚"被人偷走,是否有防範之道? 有骨氣一點,打死不看不就好了? XD  我想,每次開啟此類連結都用無痕模式(或私密瀏覽模式),會是個不錯的方法。

【延伸閱讀】

【答客問】用CSS實現上下欄排版切換

$
0
0

網友小黑提問:

網頁配置分為上下兩欄,上欄高度固定,下欄佔滿剩餘空間。當上欄隱藏時下欄佔滿全部版面,上欄顯示時恢復原有版面配置,應如何實作?

一時技癢,順便也想測驗自己的CSS技巧,拿來當家庭作業暖暖手指。

在頁面放入兩個<div>當成上下欄容器,上欄<div class="top">指定height固定高。要讓下欄<div class="bottom">佔滿剩餘空間,可用position: absolute配合left:0; right: 0; bottom: 0;讓<div>向左、右、下方延伸佔滿空間,但記得top值需等於上欄高度保留空間。

接下來是切換上欄顯示功能。若純以jQuery實作,可用$(".top").hide(); $(".bottom").css("top", 0);隱藏,用$(".top").show(); $(".bottom").css("top", "200px");恢復顯示,也不難寫。但是仿效上回用CSS切換語系按鈕圖鈕的技巧,寫起來又更簡潔一些。透過切換<body> class="hide-top",配合
    .hide-top .top { display: none; }
    .hide-top .bottom { top: 0px; }
指定.top及.bottom於隱藏狀態的另一組設定,再用一行$("body").toggle("hide-top")就搞定囉!

程式範例如下:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>上下欄CSS版面配置</title>
<style>
    body { margin: 0px; }
    .top {
      background-color: #ffccdd; height: 200px;
    }
    .bottom {
      background-color: #ccffdd; padding: 6px;
      position: absolute; left: 0px; right: 0px;bottom: 0px;
      top: 200px; 
    }
    .hide-top .top { display: none; }
    .hide-top .bottom { top: 0px; }
</style>
</head>
<body>
<divclass='top'></div>
<divclass='bottom'>
<inputtype="button"id='btn'value="Toggle Top Row"/>
</div>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js "></script>
<script>
    $("#btn").click(function() {
      $("body").toggleClass("hide-top");
    });
</script>
</body>
</html>

Live Demo

Big5-HKSCS編碼補遺

$
0
0

前陣子找到將Big5-HKSCS編碼轉為Unicode的解決方案,實際應用卻發現問題 -- 若字串已是Unicode編碼且混雜其他語系字元,HKSCS_Big5ToUnicode41()便無法招架。

延續上回的例子:

在"滙豐銀行 警衞室"後方故意加上"喆"跟"犇"這兩個BIG5編碼不支援的難字,經轉換後,滙與衞轉對了,喆與犇兩個字卻變成"?"。推敲原因是HKSCS_Big5ToUnicode41()將傳入字串視為Big5-HKSCS編碼,在.NET裡加入的喆犇二字則是Unicode編碼,在Big5-HKSCS沒有對應,便成了"?"。

想起hkscs04.dll還有另一個方法 -- HKSCS_PUAToUnicode41(),當初搞不太懂PUA(Private User Area)便多沒理它。這回遇上Unicode難字的困境,反而讓我意會到PUA的意義,一併解開殘留的謎團。當我們用GetEncoding(950).GetString()解讀Big5-HKSCS編碼內容,香港擴充字其實已被成功解析,只是它們被對映成使用者造字區(PUA)的字元,而標準字型檔未提供造字區字型,顯示時便以空白或方框呈現。網友rick提到套用"細明體_HKSCS字型檔"的解法,就是補齊了香港擴充字在造字區編碼對應的字型,讓文字得以正確顯示。然而,在Unicode 4.1標準裡,已收納了這些香港擴充字,不再需要PUA造字區,而HKSCS_PUAToUnicode41()的目的便是將PUA造字區的香港擴充字對應成Unicode 4.1內建的標準字元。

決定改寫HkscsHelper,將HKSCS_Big5ToUnicode41()及HKSCS_PUAToUnicode41()分別包裝成ConvertBig5()及ConvertPua(),並採用網友CIHsieh補充的[MarshalAs(UnmanagedType.LPWStr)]技巧,直接用StringBuilder接收結果,不必扯上Marshal.AllocHGlobal、Marshal.Copy、Marshal.FreeHGlobal,程式清爽許多。

using System;
using System.Runtime.InteropServices;
using System.Text;
 
class HkscsHelper
{
constuint HKSCS_ERR_INVALID_CHARS = 0x00000001;
 
    [DllImport("hkscs04.dll")]
publicstaticexternint HKSCS_Big5ToUnicode41(
uint dwFlags, 
        [In][MarshalAs(UnmanagedType.LPStr)]string lpBig5Str, int cbBig5,
        [Out][MarshalAs(UnmanagedType.LPWStr)]StringBuilder lpUnicode41Str, 
int cchUnicode41);
 
    [DllImport("hkscs04.dll")]
publicstaticexternint HKSCS_PUAToUnicode41(
        [In][MarshalAs(UnmanagedType.LPWStr)]string lpPUAStr, int cchPUA,
        [Out][MarshalAs(UnmanagedType.LPWStr)]StringBuilder lpUnicode41Str, 
int cchUnicode41);
 
publicstaticstring ConvertBig5(string srcString)
    {
int srcLen = Encoding.GetEncoding(950).GetByteCount(srcString);
int len = HKSCS_Big5ToUnicode41(HKSCS_ERR_INVALID_CHARS, 
                  srcString, srcLen, null, 0);
        StringBuilder sb = new StringBuilder(len);
        len = HKSCS_Big5ToUnicode41(HKSCS_ERR_INVALID_CHARS,
                  srcString, srcLen, sb, len);
return sb.ToString().Substring(0, len);
    }
 
publicstaticstring ConvertPua(string srcString)
    {
int srcLen = srcString.Length;
int len = HKSCS_PUAToUnicode41(srcString, srcLen, null, 0);
        var sb = new StringBuilder(len);
        len = HKSCS_PUAToUnicode41(srcString, srcLen, sb, len);
return sb.ToString().Substring(0, len);
    }
}

有了ConvertPua()後,就不怕字串混雜HKSCS及多語系Unicode難字囉~

我不確定二者是否有效能上的差別,由於.NET字串骨子裡都是Unicode編碼,在轉換時,統一使用ConvertPUA()應該就夠了。

感謝rick與CIHsieh的回饋補充,只是起了個頭,靠著大家分享專長與經驗,才漸漸拼湊出完整輪廓揭開謎團,找到更完善的解決方案,這感覺真好! :D

HTML5練習-從桌面拖拉檔案到網頁

$
0
0

這年頭,網頁如果不支援從桌面或檔案總管直接拖拉檔案,想自稱HTML5都心虛,只能稱作HTML4.5(誤),老闆客戶還會不時打你臉: "Gmail、OneDrive(SkyDrive)、DropBox幾百年前就有了! 為什麼你到現在還做不出來?" (網頁攻城獅內心的OS: 你有給我Google或Microsoft等級的薪水嗎?) 搞網頁好辛苦,客戶老闆上網胡亂逛,不小心看到酷炫網站,馬上把規格放進專案,還說什麼瀏覽器打開HTML、CSS、JavaScript都能看,快點抄一抄給我做出來... 想到這我都鼻酸了,Web Developer們應該團結起來,技術衝那麼快是要逼死誰,我數一二三,大家一起放手吧... orz


"大家不要這麼辛苦,好不好?" "好,大家都不要這麼辛苦。" "一、二、三!"

話說回來,既然走了技術這行就得認命,還是乖乖學習怎麼實現檔案拖拉吧!

先看執行效果。目標如下,網頁左上角有個圖檔拖放區,由檔案總管選取多個檔案,拖拉至拖放區放下,網頁接收選取的檔案清單,透過HTML5 File API讀取圖檔轉為Data URI作為<img> src,在頁面呈現圖示清單,點選圖示時可檢視原圖。

【原理解析】

只簡單說明原理,細節請直接看程式碼及註解:

  1. 拖放操作: 在圖檔拖放區元素加掛ondragover、ondragleave、ondrop事件,滑鼠移拉物件進入離開時切換底色,放下物件時透過transferData.files取得檔案資訊。為避免拖拉操作觸發原有瀏覽器行為,記得要呼叫event.stopPropagation()及event.preventDefault()。
  2. 檔案資訊: event.transferData.files裡的檔案資訊有name, type, size等資訊,基於安全不包含檔案路徑等資訊,但可當成File API參數讀出內容。
  3. 讀取檔案: 透過HTML5 File API FileReader(),將圖檔讀入轉成Data URI,可直接做為<img> src顯示圖檔。【延伸閱讀: 】
  4. 清單及檢視: 採用Knockout MVVM實作清單、檢視介面。

程式範例如下: Live Demo

<!DOCTYPEhtml>
 
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>HTML5 拖拉圖檔顯示在網頁上</title>
<style>
        .drop-zone {
            position: absolute; top: 6px; width: 120px; height: 90px; 
            background-color: green; color: white; text-align: center;
        }
        .drop-zone.hover {
            background-color: blue;
        }
        .img-list {
            position: absolute; height: 90px; background-color: #444;
            top: 6px; left: 135px; right: 6px; overflow-y: hidden;
            overflow-x: auto; white-space: nowrap;
        }
        .thumbnail {
            max-width: 100px; max-width: 75px; vertical-align: top;
            margin: 3px; cursor: pointer;
            border: 1px solid transparent;
        }
        .thumbnail:hover {
            border: 1px solid red;
        }
        .display {
            position: absolute; top: 110px; 
            left: 6px; right: 6px; bottom: 6px;
            padding: 12px;
        }
</style>
</head>
<body>
<divclass="drop-zone"><span>圖檔拖放區</span></div>
<divdata-bind="foreach: images"class="img-list">
<imgdata-bind="attr: { src: dataUri }, click: $root.currImage"class="thumbnail"/>
</div>
<fieldsetclass="display"data-bind="with: currImage">
<legend>
<spandata-bind="text: name"></span>
<spandata-bind="text: size"></span>
</legend>
<div>
<imgdata-bind="attr: { src: dataUri }"/>
</div>
</fieldset>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js "></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js "></script>
<script src="http://cdn.kendostatic.com/2013.3.1119/js/kendo.core.min.js"></script>
<script>
        $(function () {
var $drop = $(".drop-zone");
//抑制瀏覽器原有的拖拉操作效果
function stopEvent(evt) {
                evt.stopPropagation();
                evt.preventDefault();
            }
            $drop.bind("dragover", function (e) {
//滑鼠經過上方時加入特效
                stopEvent(e);
                $(e.target).addClass("hover");
            }).bind("dragleave", function (e) {
//滑鼠移開時移除特效
                stopEvent(e);
                $(e.target).removeClass("hover");
            }).bind("drop", function (e) {
//拖放操作完成事件
                stopEvent(e);
                $(e.target).removeClass("hover");
//由dataTransfer.files取得檔案資訊
var files = e.originalEvent.dataTransfer.files;
var imageFiles = $.map(files, function (f, i) {
//只留下type為image/*者,例如: image/gif, image/jpeg, image/png...
return f.type.indexOf("image") == 0 ? f : null;
                });
//清除ViewModel
                vm.images.removeAll(); vm.currImage(null);
//逐一讀入各圖檔,取得DataURI,顯示在網頁上
                $.each(imageFiles, function (i, file) {
//使用File API讀取圖檔內容轉為DataUri
var reader = new FileReader();
                    reader.onload = function (e) {
//將檔名、檔案大小、DataURI放入ViewModel
                        vm.images.push({
                            name: file.name,
                            size: kendo.format("{0:n0} bytes", file.size),
                            dataUri: e.target.result
                        })
                    }
                    reader.readAsDataURL(file);
                });
            });
 
function myViewModel() {
var self = this;
                self.images = ko.observableArray();
                self.currImage = ko.observable();
            }
var vm = new myViewModel();
            ko.applyBindings(vm);
        });
</script>
</body>
</html>

【參考資料】

  1. HTML5 File Drag & Drop API
  2. HTML5 drag and drop asynchronous multi file upload with ASP.NET WebAPI
  3. [HTML5] HTML5 File API by 小朱

HTML5檔案上傳進度條

$
0
0

在傳統網頁上傳大檔案,得等到全部傳完才會有回應,等待期間沒消沒息,搞不清楚是沒傳完還是當掉常為人詬病,也嚴重破壞使用者體驗。想在傳輸過程回報上傳進度,過去有些Flash、Java Applet或ActiveX的解決方案,但依賴外掛元件有部署及無法跨平台的疑慮。當HTML5規格漸成主流,長久以來的問題總算有了簡潔有效的解法。

要掌握上傳進度有一個關鍵: Client Script必須掌握檔案大小以及已上傳資料量,才可能計算上傳百分比回報狀態。傳統使用<input type="file">選取檔案配合<input type="submit">送出鈕的做法,一來無法得知所選取檔案大小,二來在按下POST鈕後Script即失去主導權,一切交由瀏覽器主控,更別奢談得知上傳進度。在從桌面拖拉檔案到網頁一文提到的HTML5 File API,一舉突破JavaScript無從得知檔案大小的盲點,邁進一大步。而透過XHR(XMLHttpRequest),改用jQuery.ajax()非同步上傳檔案,便能在上傳過程繼續更新網頁回報進度。這樣子只剩下一個挑戰 -- 如何得知已上傳資料量?

好消息! 隨著瀏覽器日新月異,XHR也跟著進化,HTML5世代瀏覽器(IE需為IE10+,IE9又哭哭了...)已內建XMLHttpRequest Level 2(XHR2),增加不少新功能,包含直接處理ArrayBuffer/Blob等二進位資料的能力,也多了onprogress事件,能在傳輸過程中持續觸發回報上傳進度! 有了新武器,要實現上傳進度回報就簡單多了。

先看成品展示:

選取三個檔案,下方即出現三個包含檔案名稱、狀態文字及檔案大小的進度條,按下【Upload】鈕上傳,狀態會由Waiting轉為Uploading,右方則會顯示已上傳Byte數及百分比,為求酷炫(謎之聲: 上回是誰說要一、二、三大家一起放手的?),我還用CSS做了一個依百分比呈現不同長度的的綠色條。後端接收程式用ASP.NET MVC寫,上傳檔案會被寫入App_Data,展示過程能看到圖案上傳完後出現在App_Data的時機,代表的確是用大骨及珍貴藥材下去熬湯絕非添加湯塊

Client端程式碼如下(ASP.NET MVC cshtml):

@{
    Layout = null;
}
<!DOCTYPEhtml>
 
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Ajax Upload Lab</title>
<style>
        .item {
            background-color: #6699CC; border: 1px solid gray;
            font-family: 'Courier New'; font-size: 8.5pt;
            margin-bottom: 6px; padding: 3px; color: yellow;
            box-shadow: 3px 3px 3px 1px rgba(128, 128, 128, 0.7);
        }
            .item .name { text-shadow: 1px 1px gray;  }
 
        .prg-zone { margin-top: 10px; }
 
        .bar {
            background-color: #666666;
            height: 15px; position: relative;
            margin: 3px; margin-top: 6px;
            margin-bottom: 6px; border: 1px solid #ccc;
            border-top-color: #444; border-left-color: #444;
        }
            .bar > div {
                position: absolute; color: white; font-size: 8pt;
            }
            .bar .color-bar {
                background-color: #99bb33; top: 0px; bottom: 0px;
                left: 0px;
            }
            .bar .status { top: 0px; left: 6px; }
            .bar .progress { top: 0px; right: 4px; }
</style>
</head>
<body>
<div>
<inputtype="file"id="fileSelector"multiple
data-bind="event: { change: selectorChange }"/>
<inputtype="button"value="Upload"data-bind="click: upload"/>
</div>
<divdata-bind="foreach: files"class="prg-zone">
<divclass="item">
<divdata-bind="text: name"class="name"></div>
<divclass="bar">
<divclass="color-bar"data-bind="attr: { 'style': widthStyle }"></div>
<divclass="status"data-bind="text: status"></div>
<divclass="progress"data-bind="text: progress"></div>
</div>
</div>
</div>
<scriptsrc="~/Scripts/jquery-2.1.0.js"></script>
<script src="~/Scripts/knockout-3.1.0.debug.js"></script>
<script>
        $(function () {
 
function viewModel() {
var self = this;
                self.files = ko.observableArray();
                self.selectorChange = function (item, e) {
                    self.files.removeAll();
                    $.each(e.target.files, function (i, file) {
//加入額外屬性
                        file.uploadedBytes = ko.observable(0); //已上傳Bytes
                        file.percentage = ko.computed(function () { //上傳百分比
return (file.uploadedBytes() * 100 / file.size).toFixed(1);
                        });
                        file.widthStyle = ko.computed(function () {
return"right:" + (100 - file.percentage()) + "%";
                        });
//上傳進度數字顯示
                        file.progress = ko.computed(function () {
var perc = file.percentage();
return file.uploadedBytes.peek() + "/" + file.size +
"(" + perc + "%)";
                        });
                        file.message = ko.observable();
                        file.status = ko.computed(function () {
var msg = file.message(), perc = file.percentage();
if (msg) return msg;
if (perc == 0) return"Waiting";
elseif (perc == 100) return"Done";
elsereturn"Uploading...";
                        });
                        self.files.push(file);
                    });
                };
 
                self.upload = function () {
                    $.each(self.files(), function (i, file) {
var reader = new FileReader();
                        reader.onload = function (e) {
var data = e.target.result;
//https://gist.github.com/HenrikJoreteg/2502497
//以XHR上傳原始格式
                            $.ajax({
                                type: "POST",
                                url: "@Url.Content("~/xhr2/upload")" + "?file=" + file.name,
                                contentType: "application/octect-stream",
                                processData: false, //不做任何處理,只上傳原始資料
                                data: data,
                                xhr: function () {
//建立XHR時,加掛onprogress事件
var xhr = $.ajaxSettings.xhr();
                                    xhr.upload.onprogress = function (evt) {
                                        file.uploadedBytes(evt.loaded);
                                    };
return xhr;
                                }
                            });
                        };
                        reader.readAsArrayBuffer(file);
                    });
                };
            }
var vm = new viewModel();
            ko.applyBindings(vm);
 
        });
</script>
</body>
</html>

簡要說明程式重點:

  1. 進度條呈現使用Knockout.js MVVM
    直接擴充File API傳回的File資料,加上uploadedBytes(已上傳大小)、percentage(已上傳百分比)、progress(顯示1024/2048(50.0%)等進度數據)、status(依進度百分比傳回Waiting、Uploading或Done)、message(保留額外狀態訊息用)、widthStyle(控制綠色進度條長度的CSS Style參數)等observable。KO的相依性追蹤機制很好用,只需更新uploadedBytes性,其餘屬性就會自動更新。
  2. 長度會改變的綠色進度條
    用了一點CSS技巧。將<div class='color-bar'>設成position: absolute、top: 0px、bottom: 0px、left: 0px向上左下三方填滿,向右的邊界則隨百分比進度增加不斷遞減: right: 100%, right: 99%, … , rigth: 0%,就可做出愈來愈長的色條。
  3. $.ajax()上傳二進位資料
    前面提到XHR2支援ArrayBuffer,想將檔案內容原汁原味傳送到伺服器端,故使用FileReader.readAsArrayBuffer()讀成ArrayBuffer,$.ajax()上傳時processData要設false,就能以Byte Array方式將二進位資料完整上傳,在ASP.NET MVC端則以Request.InputStream讀出,確保取得沒被編碼或轉換過的原始內容。

接下來看ASP.NET MVC端:

        [HttpPost]
public ActionResult Upload(string file)
        {
//由InputStream取得XHR上傳的內容
            var stream = Request.InputStream;
long totalLen = stream.Length, uploadedBytes = 0;
 
//為了展示傳輸進度,故意一次1K慢慢讀
byte[] buffer = newbyte[1024];
string outPath = Path.Combine(Server.MapPath("~/App_Data"), file);
using (FileStream fs = new FileStream(outPath, FileMode.Create)) 
            {
while (uploadedBytes < totalLen)
                {
                    var len = stream.Read(buffer, 0, buffer.Length);
                    fs.Write(buffer, 0, len);
                    uploadedBytes += len;
//故意延遲1ms
                    Thread.Sleep(1);
                }
            } 
return Content("OK");
        }

理論上用InputStream.CopyTo(FileStream)就可以一次將資料寫成檔案。但為展示Server端慢慢消化資料的效果,我採取較曲折的讀取方法,只開1K的byte[],分批慢慢讀取,每讀1K再穿插Thread.Sleep(1)的延遲。

即使在Server端加入機制慢慢消化資料,$.ajax()呼叫後需等一段時間才執行完成,但進度條卻比保時捷911還凶猛,0到100花不到0.1秒。(註: 文首的操作展示屬節目效果,除非網路極慢或檔案超大,瞬間從0到100是正常的) 仔細一想才驚覺 -- MVC端取得InputStream時,XHR已傳完所有資料,後續處理再慢,從XHR的角度資料已100%傳完。換句話說,XHR onprogress回報的進度是指資料上傳進度,而非Server端處理進度。但是,若以"上傳資料"的角度,XHR2進度條在Internet傳輸大檔時能提供使用者即時的狀態回饋,確實是有效的解決方案。

做到這裡,上傳資料進度條完成。但興起一個念頭,手邊專案不乏上傳CSV、Excel或文字檔寫入DB的作業,這類操作中資料傳輸時間很短,大部分時間花在逐筆寫入資料庫,上傳進度條概念可否進一步改成反應伺服器端的處理進度呢? 很有趣的題目,若做得出來手邊有一票模組可以套用,但該怎麼玩呢? 下回待續。

HTML5上傳作業進度條-SignalR進階版

$
0
0

【前情提要】利用File API與XHR2 onprogress事件,我們成功做出檔案上傳進度條。但我在工作上常遇到另一種情境 -- 內部系統的上傳轉檔作業。營運資料檔案一般不大,加上在Intranet裡傳輸,上傳只在彈指間,Server端解析資料、塞入資料庫才是重頭戲,常得耗上幾十秒到幾分鐘。這種狀況下,用XHR2做進度條的意義不大,咻! 一秒不到就從0%到100%,但上傳資料何時能處理完只有天知道? 使用者終究又陷入無法得知系統還在跑或者已經當掉的焦慮。我想起了"SignalR",不如結合SignalR來打造一個由Server端回報的作業進度條吧~

SignalR版會以上一篇的程式為基礎加以改良,重複部分將略過,如有需要請參考前文。

照慣例,先來看成品:

我虛擬了一個馬拉松成績匯入作業,打算將如下格式的大會成績文字檔(以\t分隔)上傳到ASP.NET MVC,模擬解析後逐筆寫入資料庫的上傳作業。

ASP.NET MVC接收資料的部分如下:

using AjaxUpload.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
 
namespace AjaxUpload.Controllers
{
publicclass HomeController : Controller
    {
public ActionResult Index()
        {
return View();
        }
 
        [HttpPost]
public ActionResult Upload(string file, string connId)
        {
//註: 程式僅為展示用,實際開發時應加入更嚴謹的防錯防呆
string errMsg = null;
try
            {
//取得檔案內容,解析為List<string>
string content = new StreamReader(Request.InputStream).ReadToEnd();
                var sr = new StringReader(content);
                List<string> lines = new List<string>();
string line = null;
while ((line = sr.ReadLine()) != null)
                {
                    lines.Add(line);
                }
//總筆數及目前處理筆數
int total = lines.Count, count = 0;
//使用Task進行非同步處理
                var task = Task.Factory.StartNew(() =>
                {
                    Random rnd = new Random();
for (var i = 0; i < lines.Count; i++)
                    {
string[] p = lines[i].Split('\t');
if (p.Length != 8)
thrownew ApplicationException("Not a valid text file!");
//假裝將資料寫入資料庫,Delay 0-4ms
                        Thread.Sleep(rnd.Next(5));
                        count++;
                    }
                });
//透過SignalR
float ratio = 0;
                Action updateProgress = () =>
                {
                    ratio = (float)count / total;
                    UploaderHub.UpdateProgress(connId, file, ratio * 100,
string.Format("{0:n0}/{1:n0}({2:p1})", count, total, ratio));
                };
 
//每0.2秒回報一次進度
while (!task.IsCompleted && !task.IsCanceled && !task.IsFaulted)
                {
                    updateProgress();
                    Thread.Sleep(200);
                }
                updateProgress();
 
//若正確完成,傳回OK
if (task.IsCompleted && !task.IsFaulted)
return Content("OK");
else
                    errMsg = string.Join(" | ", 
                        task.Exception.InnerExceptions.Select(o => o.Message).ToArray());
            }
catch (Exception ex)
            {
                errMsg = ex.Message;
            }
            UploaderHub.UpdateProgress(connId, file, 0, "-", errMsg);
return Content("Error:" + errMsg);
        }
    }
}

上傳時除了POST是二進位的檔案內容,還另外以QueryString傳入connId(SingalR的連線Id,當有多個網頁連線時才知道要回傳給哪一個Client)及file(檔案名稱)。Action內部先用StreamReader讀入上傳內容轉為字串,再用StringReader將字串解析成List<string>,接著逐行讀取。由於只是展示用途並不需要真的寫入資料庫,每讀一筆後用Thread.Sleep()暫停0-4ms(亂數決定),把處理兩千多筆的時間拉長到幾秒鐘方便觀察。至於回報進度部分,我決定採固定時間間隔回報一次的策略,故將處理資料邏輯放在Task裡非同執行,啟動後另外跑迴圈每0.2秒回報一次進度到前端。UploaderHub是這個專案自訂的SingalR Hub類別,它提供一個靜態方法UpdateProgress,可傳入connId、file、percentage(進度百分比)、progress(由於Client端不知道資料解析後的行數,故總行數及目前處理行數資訊全由Server端提供)、message(供錯誤時傳回訊息)。

安裝及設定SignalR的細節此處略過(基本上透過NuGet下載安裝並依Readme文件加上RouteTable.Routes.MapHubs();就搞定)。至於UploaderHub.cs,幾乎是個空殼子。繼承Hub之後,絕大部分的工作皆由父類別定義的方法搞定。唯一增加的UpdateProgress()靜態方式,在其中由GlobalHost.ConnectionManager.GetHubContext<UploaderHub>()取得UploaderHub執行個體,再經由Clients.Client(connId).updateProgress()呼叫JavaScript端的updateProgress函式。理論上這段程式可以寫在任何類別,因為UploaderHub太空虛怕引來其他類別抗議基於相關邏輯集中的考量,決定將它納為UploaderHub的方法。

using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
 
namespace AjaxUpload.Models
{
publicclass UploaderHub : Hub
    {
//從其他類別呼叫需用以下方法取得UploaderHub Instance
static IHubContext HubContext = 
                        GlobalHost.ConnectionManager.GetHubContext<UploaderHub>();
 
publicstaticvoid UpdateProgress(string connId, string name, float percentage, 
string progress, string message = null)
        {
            HubContext.Clients.Client(connId)
                .updateProgress(name, percentage, progress, message);
        }
    }
}

最後來看Client端。CSS與HTML部分完全沿用前文的範例,只有JavaScript部分做了小幅修改。

  1. 引用jquery.signalR-*.js(我的專案是.NET 4.0,故用1.2.1版,若是.NET 4.5可用2.x版)以及~/signalr/hubs
  2. percentage, progress改由Server端提供(XHR2版抓已上傳Byte數自行計算)
  3. 同時上傳多檔時,要由SignalR呼叫時傳回的file參數決定該更新哪個檔案的進度,故建立一個以檔名為Key的Dictionary資料結構便於尋找
  4. 加入建立SignalR連線的程式碼
  5. 宣告updateProgress函式,等待Server呼叫以更新進度資訊
<scriptsrc="~/Scripts/jquery-2.1.0.js"></script>
<script src="~/Scripts/jquery.signalR-1.2.1.js"></script>
<script src="@Url.Content("~/signalr/hubs")"></script>
<script src="~/Scripts/knockout-3.1.0.debug.js"></script>
<script>
        $(function () {
 
function viewModel() {
var self = this;
                self.files = ko.observableArray();
                self.selectorChange = function (item, e) {
                    self.files.removeAll();
                    $.each(e.target.files, function (i, file) {
//加入額外屬性
                        file.percentage = ko.observable(0);
                        file.progress = ko.observable();
                        file.widthStyle = ko.computed(function () {
return"right:" + (100 - file.percentage()) + "%";
                        });
                        file.message = ko.observable();
                        file.status = ko.computed(function () {
var msg = file.message(), perc = file.percentage();
if (msg) return msg;
if (perc == 0) return"Waiting";
elseif (perc == 100) return"Done";
elsereturn"Uploading...";
                        });
                        self.files.push(file);
                    });
                };
//以檔名為索引建立Dictionary,方便更新進度資訊
                self.dictionary = {};
                ko.computed(function () {
                    self.dictionary = {};
                    $.each(self.files(), function (i, file) {
                        self.dictionary[file.name] = file;
                    });
                }).extend({ throttle: 100 });
 
                self.upload = function () {
                    $.each(self.files(), function (i, file) {
var reader = new FileReader();
                        reader.onload = function (e) {
var data = e.target.result;
//以XHR上傳原始格式
                            $.ajax({
                                type: "POST",
                                url: "@Url.Content("~/home/upload")" + 
"?file=" + file.name + "&connId=" + connId,
                                contentType: "application/octect-stream",
                                processData: false, //不做任何處理,只上傳原始資料
                                data: data
                            });
                        };
                        reader.readAsArrayBuffer(file);
                    });
                };
            }
var vm = new viewModel();
            ko.applyBindings(vm);
//建立SignalR連線並取得Connection Id
var connId;
var hub = $.connection.uploaderHub;
            $.connection.hub.start().done(function () {
                connId = $.connection.hub.id;
            });
//Server端傳回上傳進度時,更新檔案狀態
            hub.client.updateProgress = function (name, percentage, progress, message) {
var file = vm.dictionary[name];
if (file) {
                    file.percentage(percentage);
                    file.progress(progress);
if (message) file.message(message);
                }
            };
 
        });
</script>

就這樣,一個會即時回報Server處理進度的網頁介面就完成囉! HTML5 File API + jQuery + ASP.NET MVC + SignalR + Knockout.js 合作演出大成功~


TIPS-在.NET4 ASP.NET MVC專案安裝SignalR

$
0
0

目標平台為Windows Server 2003,ASP.NET MVC專案只能選擇.NET 4.0,在NuGet用關鍵字signalr找到程式包,安裝時卻出現以下錯誤:

Could not install package 'Microsoft.Owin.Security 2.0.2'. You are trying to install this package into a project that targets '.NETFramework,Version=v4.0', but the package does not contain any assembly references or content files that are compatible with that framework. For more information, contact the package author.

查過官方文件,很好! SignalR 2.0需要.NET 4.5,可憐的Windows Server 2003... 那,偷偷把Server升級到2008吧! 腦海立刻浮現: 正式機升級隔天,某個系統異常公司大亂,胡亂升級OS的那個老傢伙被眾人拉到牆角餵磚頭,沒多久就被推出午門問斬了... 不行,我要冷靜,想想怎麼在.NET 4安裝SingalR比較實在。

NuGet GUI上提供的元件以最新版為主,若要安裝舊版,需透過Package Manager Console下指令: (輸入指令時,可以按Tab帶出可用選項。例如,敲入Version後按Tab,會列出所有可用版本,就甘心A)

Install-Package Microsoft.AspNet.SignalR –Version 1.2.1


提醒: 若解決方案中有多個專案,記得要選對Default Project(目上圖右上方的下拉選單)

安裝完成,就能開心地在.NET 4中使用SignalR囉~

註: 依實際經驗,SingalR 1.2與SignalR 2.0的程式寫法差異不大,不太需要依版本調整。至於二者部署上的差別,可以參考文件

HTML5筆記–Object URL

$
0
0

相信大家對於Data URI已不陌生,這回再介紹另一個HTML5的好東西 --- Object URL。

Data URI的格式為="data:image/gif;base64,R0lGOD....",Object URL則是"blob:"再串上網址以及GUID,例如: blob:http%3A//www.darkthread.net/c94d498b-7818-49b3-8e79-d3959938ba0f

大家應該已經注意到一點明顯差異 -- Object URL不像Data URI包含內容的Base64編碼,不管背後代表的File或Blob物件有多大,都只有一個短短的GUID,真正的內容被儲存在瀏覽器記憶體中,Object URL像個號碼牌,憑著它可以向瀏覽器提領內容。換個角度,http%3A//www.darkthrad.net/c94d498b-7818-49b3-8e79-d3959938ba0f的形式,有點像是跟http: //www.darkthread.net網站取得名為c94d498b-7818-49b3-8e79-d3959938ba0f的資源,但讀取時不需真的發出HTTP Request,直接由瀏覽器提取即可。由於物件內容被儲存在瀏覽器記憶體,註定了Object URL的生命週期得緊緊地跟網頁綁在一起,就像JavaScript變數一般,需等到網頁載入後,透過URL.createObjectURL()為File或Blob物件建立Object URL,網頁結束後自動失效,或是確定不要後呼叫URL.revokeObjectURL()主動將其註銷節省記憶體。(補充: File物件的使用方式可參考從桌面拖拉檔案到網頁檔案上傳進度條)

Object URL如果標準URL,可以套用在原本使用URL的場合,例如: <img> src、<a> href,甚至當成$.ajax()的下載來源,在應用上比Data URI廣泛。來看看兩個有趣的範例:

[瀏覽器版本需求: 依循往例,IE9繼續哭哭,IE10+與其他瀏覽器都已支援]

【以JavaScript產生可下載檔案】

如以下示範,在<textarea>輸入文字,將其轉為Blob物件後建立Object URL,產生<a href="object_url" download="file_name">的下載連結,可將剛才輸入的文字匯出成檔案,很酷吧! 而按下註銷鈕後Object URL將被註銷,再下載檔案時就會失敗。Live Demo

程式碼:

<!DOCTYPEhtml>
 
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>HTML5 Blob URL應用-產生可下載檔案</title>
<style>
      .revoke { text-decoration: line-through; }
</style>
</head>
<body>
<div>
<textareaid='taText'style='width: 300px; height: 50px;'>
是程式問題,但程式問題不等於程式設計師的問題。
是網頁問題,但網頁問題不等於網頁攻城獅的問題。
</textarea>
<br/>
<inputtype="button"id='btnGenFile'value='產生檔案'/>
<inputtype="button"id="btnRevokeUrl"value='註銷Blob URL'/>
<br/>
<aid='aDownload'></a>
</div>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js "></script>
<script>
        $(function () {
var $link = $("#aDownload");
          $('#btnGenFile').click(function() {
var blob = new Blob([$("#taText").text()], 
                      { type:"application/octect-stream" });
var blobUrl = URL.createObjectURL(blob);
var fileName = "words.txt";
            $link.attr({ href: blobUrl, download: fileName })
                 .text(fileName).removeClass("revoke");
          });
          $('#btnRevokeUrl').click(function() {
            URL.revokeObjectURL($link.attr("href"));
            $link.addClass("revoke");
          });
 
        });
</script>
</body>
</html>

【模擬XHR下載來源】

第二個示範也很有趣,我寫了一段$.get()由指定的網址下載內容,顯示於下方<textarea>。第一次先輸入httq:jsbin.com/xocul(網頁在jsbin.com執行時,URL必須是jsbin.com的網址,否則會違反XHR資安原則拒絕存取),按鈕後可取回該網址的HTML。接著,改用<input type="file">選取本機檔案,使用HTML5 File API取得該檔案的File物件,用URL.createObjectURL()轉成URL當成$.get()的URL參數,按鈕後XHR成功讀出本機檔案的內容並顯示在下方。Live Demo

程式碼:

<!DOCTYPEhtml>
 
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>HTML5 Blob URL應用-XHR網址來源</title>
<style>
</style>
</head>
<body>
<div>
<inputtype="file"id="fileSelector"/>
<br/>
      URL: <inputtype="text"id="txtUrl"style="width: 60%"/>
<br/>
<inputtype="button"id='btnReadFile'value='XHR讀取檔案'/>
<br/>
<textareaid="taDisplay"style="width: 80%; height: 50px;"></textarea>
</div>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js "></script>
<script>
        $(function () {
var $link = $("#aDownload");
          $("#fileSelector").change(function(e) {
var file = e.target.files[0];
              $("#txtUrl").val(URL.createObjectURL(file));
          });
          $('#btnReadFile').click(function() {
            $.get($("#txtUrl").val(), function(res) {
              $("#taDisplay").text(res);
            }, "text");
          });
 
        });
</script>
</body>
</html>

學會愈多新武器,愈覺得HTML5有趣,Rock~ :P

【茶包射手日記】純JavaScript DOCX套版元件Debug

$
0
0

網友ping留言問了JS DOCX套版元件問題,由其所提供下載位置(https://github.com/djpate/docxgen)的Readme文件,判斷是PHP所開發,直覺地認為所謂JS套版是JavaScript呼叫PHP完成。但ping後續補充: 真的是純JavaScript,並提供可執行的程式包,我這才驚覺:

啥咪!! 真有神人用純JavaScript寫出DOCX套版元件?

DOCX的本質是ZIP壓縮打包的一堆XML檔案,理論上只要能解壓ZIP、解析及操作XML,就能完成文件修改。但從沒想過這一切可以全用JavaScript解決? node.js揮軍Server端,Espruino殺進嵌入系統,今天的JavaScript,沒有極限!

做了簡單解析,發現它依賴base64.js(Base64編碼)、jszip.js(ZIP壓縮解壓),其餘只靠一個docxgen.js搞定全部工作。要執行套版時,只要先準備一個Word文件,在其中穿插{ tag1}、{tag2}註明要置換成其他文字的位置,執行如下JavaScript,就可得到套版後的Word檔。

new DocxGen().loadFromFile("tagExample.docx", { async: true })
.success(function(doc){
    doc.setTags({ tag1:"tag1Value", tag2:"tag2Value" });
    doc.applyTags() //apply them replace all occurences of {tag*}
    doc.output() //Output the document using Data-URI
});

原程式的tagExample.docx需透過XHR存取,Chrome預設開啟本機HTML執行時不允許XHR存取本地磁碟機檔案,嫌外加Chrome啟動參數麻煩,又不想為此丟到IIS測試,於是用了Object URL技巧,改選取本機檔案作為XHR下載對象。如此,就能直接在本機用Chrome測試。操作示範如下:

tagExample.docx內含{last_name} {first_name}等標籤,在<textarea>編輯JSON字串定義好first_name, last_name等要置換的文字,選取檔案後按下【套版】,馬上能下載套版好的Word檔,其中{last_name}等標籤都已被換成指定的文字。但示範也突顯ping所說的中文變亂碼問題。例如: "黑暗執行緒"變成"黑暗執行緒"。

既是純JavaScript,就一定能Trace Code、找出問題並修正。想到這裡,手就癢了,馬上開啟Chrome偵錯工具,找到docxgen.js。但發現它被Minify壓縮:

不怕,點一下左下的Pretty print鈕,Chrome立刻將JavaScript程式碼縮排整理妥當,恭請長官審閱:

設下中斷點一路追蹤,查出一處可疑:

DocUtils.encode_utf8 = function (e) {  return unescape(encodeURIComponent(e));  }

函式目的應是將文字轉為utf8編碼,但感覺套在中文會出包。以console測試unescape(encodeURIComponent("黑暗執行緒")),果然傳回"黑暗執行緒",跟Word成品中的亂碼一致,驗證是encode_utf8把中文轉壞了!

如果這樣不行,該怎麼克服? 想起"&#unicode;"的Unicode字元表示法,將encode_utf8改成:

DocUtils.encode_utf8 = function (e) {
        var r = [];
        for (var i = 0; i < e.length; i++) {
            var asc = e.charCodeAt(i);
            r.push("&#" + asc + ";");
        }
        return r.join("");
    }

雖然轉為"&#nnnn;"後長度會增加,至少確保編碼正確無誤。薑!薑!薑!薑~ Bug被修掉囉!

最後,花一點時間聊聊這個JavaScript版套版元件—docxgen.js。

網路上關於它的討論很少,較完整的說明只有法國javascript-ninja網站的Demo網頁,由網站直接瀏覽目錄找到Readme.md(文件寫入時間是2014-2-18),說明裡提到它支援Chrome 26+、Firefox 3+,Safari沒測過,IE不行(連IE10都不行,理由是XHR無法處理純Binary檔案)。由於檔案日期很新,或許是個尚未公開的Open Source Project吧! 我個人認為,直接置換XML {tag}的做法在遇到Word儲存的複雜樣式時,可能還有些地雷要掃。若限定必須在非Windows主機直接進行DOCX套表,這絕對是很出色的解決方案。

【茶包射手日記】當IE遇上Enter

$
0
0

專案網頁要求用Enter代替Tab,寫了簡單jQuery Plugin搞定,在Chrome、Firefox測試OK,改用IE測試卻出現怪異反應。網頁有多個區塊,在輸入區塊某個<input>按Enter,確有Tab之效,焦點順利跳至下一欄位,但另一個查詢區的Grid卻也同時自動呼叫AJAX重新查詢。在下一個<input>按Enter,焦點移動後Gird又再自動查詢,屢試不爽。而問題只發生在IE,測了IE9/10皆然,Chrome、Firefox、Safari、Opera則不會。

想不通為什麼按Enter會讓Grid自動查詢,採消去法逐一移掉可疑元素,歷經抽絲剝繭終於找到元兇 -- 查詢區有個<button> onclick事件裡寫了AJAX重新查詢邏輯,在IE按下Enter鍵似乎會觸發<button> click事件。

以下網頁可在IE重現問題:

<!DOCTYPEhtml>
<html>
<head>
<scriptsrc="http://code.jquery.com/jquery-2.1.0.min.js"></script>
<meta charset="utf-8">
<title>IE Button Focus Issue</title>
</head>
<body>
<input type="text" value="T1" id='t1' />
<input type="text" value="T2" id='t2' />
<br />
<input type="button" id="b1" value="Button1" />
<button id='b2'>
<div>Button2</div>
</button>
<script>
        $(function () {
            $('#b1').click(function () { 
              alert("Button1 Clicked!"); 
            });
            $('#b2').click(function () { 
              alert("Button2 Clicked!"); 
            });
            $('#t2').focus().select();
        });
</script>
</body>
</html>

如上圖所示,焦點停在T2<input>,按下Enter卻觸發Button2 Clicked!

Microsoft Support KB裡有答案:

When the focus is on an HTML form, and the user presses the ENTER key, by default, Internet Explorer treats this action as if the user clicks Submit. However, not all browsers behave the same way, and you may want to disable this behavior. This article demonstrates how to use script to prevent this behavior.

在網頁按下Enter,IE視同使用者按下Submit鈕,此行為與其他瀏覽器不同,故MS KB提供停用的做法。

實驗將Button1改為<input type="submit">,按下Enter後變成Button1 Clicked(Demo),驗證按下Enter時,IE會找到網頁裡第一個Submit鈕,觸發其點擊動作。我的網頁純粹靠AJAX運作,沒有<form>,所以觸發的不是PostBack,而是Client端事件,不然或許我會更快察覺。

專案之所以用<button>而非<input type="button">,在於按鈕規格要求包含圖檔及複雜文字樣式,<input type="button">只能指定純文字,而<button>像是容器,內部能自訂HTML標籤,才具足夠彈性。而我一直誤以為<button>等同<input type="button">,但在IE8+ 標準模式下其實是<input type="submit">,而要指定它不當Submit,需額外指定,寫成<button type="button">。

註: IE8+,標準模式為submit、IE6/7及IE8+相容模式則為button,宜留意其會因模式改變。
Windows Internet Explorer 8 and later. The default value of the type attribute depends on the current document compatibility mode. In IE8 Standards mode, the default value is submit. In other compatibility modes and earlier versions of Windows Internet Explorer, the default value is button. 參考來源

修改加上type="button"後,就可避免Enter觸發<button>囉! [Demo]

這枚茶包是個知難行易的經典案例: 找到問題根源才是重點,解決只是舉手之勞。各位茶包射手們,今天就來回味孫文學說做為總結吧!

謂有某家水管偶生窒礙,家主即雇工匠為之修理。工匠一至,不過舉手之勞,而水管即復回原狀。而家主叩以工值幾何,工匠曰:「五十元零四角。」家主曰:「此舉手之勞,我亦能為之,何索值之奢而零星也?何以不五十元,不五十一元,而獨五十元零四角何為者?」工匠曰:「五十元者,我知識之值也;四角者,我勞力之值也。如君今欲自為之,我可取消我勞力之值,而只索知識之值耳。」家主啞然失笑,而照索給之。

HTML表格欄寬分配實驗

$
0
0

接獲報案,某個KendoUI Grid網頁,調整瀏覽器顯示比例縮放後,最後一欄會莫名消失。

試著重現問題。如上圖所示,顯示比例100%時可以看到Column1到Column5共五欄,調整比例到125%,欄位放大,Gird下方出現捲軸,但向右捲動到底只見Column4,Column5不見了。除了調整顯示比例,改變視窗寬度也有同樣效果,只要寬度不足導致水平捲軸,Column5就會消失。

程式碼如下: [Live Demo]

<!DOCTYPEhtml>
<html>
<head>
<linkhref="http://cdn.kendostatic.com/2013.3.1119/styles/kendo.common.min.css"
rel="stylesheet"type="text/css"/>
<linkhref="http://cdn.kendostatic.com/2013.3.1119/styles/kendo.default.min.css"
rel="stylesheet"type="text/css"/>
<metacharset="utf-8">
<title>消失的最後一欄</title>
<style>
    .grid { margin-top: 50px; }
</style>
</head>
<body>
<divclass='grid'>
</div>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js "></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js "></script>
<script src="http://cdn.kendostatic.com/2013.3.1119/js/kendo.web.min.js"> 
</script>
<script>
function item() {
for (var i = 1; i <= 5; i++) {
this["C" + i] = parseInt(Math.random() * 100000);
      }
    }
var dummyData = [];
for (var i = 0; i < 5; i++) {
      dummyData.push(new item());
    }
    $(".grid").kendoGrid({
      dataSource: { data: dummyData },
      height: 200,
      columns: [
        { field: "C1", title: "Column1", width: 120 }, 
        { field: "C2", title: "Column2", width: 120 }, 
        { field: "C3", title: "Column3", width: 120 }, 
        { field: "C4", title: "Column4", width: 120 }, 
        { field: "C5", title: "Column5" }
      ]
    });
</script>
</body>
</html>

經過一番檢查,發現kendoGrid產生的表格HTML如下:

追根究底,問題網頁在設定kendoGrid欄位時為前四欄指定寬度,第五欄則無,原意是要第五欄吃下所有剩餘寬度。但由結果來看,第五欄的確是吃剩餘寬度,當寬度不夠時,變成0好像也沒錯。使用Chrome觀察,得到更有趣的結果 -- 第五欄的寬度是負值(-20.1875px)!

找到原因,第五欄消失的茶包在指定寬度後瞬間被KO,但留下兩個疑問:

  1. 使用<col style="width: 120px">指定固定欄寬,當<table>寬度不等於所有欄寬設定加總時,是否會依比例分配?
  2. 如何讓最後一欄吃下剩餘寬度,但寬度不足時不被犠牲?

做個實驗來找答案吧! 寫了一個測試網頁:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>欄寬分配</title>
<style>
    #t { 
      border: 1px solid brown; 
      background-color: yellow;
      border-spacing: 3px;
    }
    #t th { 
      border: 1px solid gray; padding: 1px; 
      background-color: orange; color: black;
    }
    #disp {
      margin-top: 20px;
      padding: 6px; background-color: #ccc; height: 15px;
    }
</style>
</head>
<body>
<divclass='grid'>
</div>
<tableid='t'style="width: 100%">
<colgroup>
<colstyle="width:60px">
<colstyle="width:60px">
<colstyle="width:120px">
<colstyle="width:150px">
<colstyle="width:180px">
</colgroup>
<thead>
<trid='r'>
<th>Column1</th><th>Column2</th><th>Column3</th>
<th>Column4</th><th>Column5</th>
</tr>
</thead>
</table>
<divid='disp'></div>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js "></script>
<script>
    $(window).bind("resize", function() {
var res = [];
var tw = $("#t").width();
      res.push("table : " + tw);
var sum = 0;
      $("#r th").each(function(i) {
var w = $(this).width();
        sum += w;
        res.push("th" + (i+1) + " : " + w);
      });
      res.push("sum : " + sum + "(" + (tw-sum) + ")");
      $("#disp").text(res.join(' / '));
    }).resize();
</script>
</body>
</html>

table寬度預設為100%,會隨視窗大小改變寬度。表格下方會顯示table寬度、5個th的寬度、5個th寬度加總,括號中則為th加總與table寬度的差距。在這個範例中,會有38px的差異,來自三個地方:

1) table border-spacing: 3px, 共6個(上圖紅箭頭所指), 3px * 6 = 18px
2) th border: 1px, 左右各1px,(1px + 1px) * 5 = 10px
3) th padding: 1px, 左右各1px,(1px + 1px) * 5 = 10px

18+10+10 = 38,這就是38px的由來。

試著改變視窗寬度,觀察不同table寬度下的欄寬變化:

table : 819 / th1 : 83 / th2 : 83 / th3 : 163 / th4 : 205 / th5 : 247 / sum : 781(38)
table : 688 / th1 : 69 / th2 : 69 / th3 : 135 / th4 : 171 / th5 : 206 / sum : 650(38)
table : 579 / th1 : 59 / th2 : 59 / th3 : 112 / th4 : 141 / th5 : 170 / sum : 541(38)
table : 492 / th1 : 59 / th2 : 59 / th3 : 93 / th4 : 112 / th5 : 131 / sum : 454(38)
table : 342 / th1 : 59 / th2 : 59 / th3 : 60 / th4 : 62 / th5 : 64 / sum : 304(38)
table : 333 / th1 : 59 / th2 : 59 / th3 : 59 / th4 : 59 / th5 : 59 / sum : 295(38)

由測試結果可以歸納出瀏覽器決定欄寬的原則: table寬度扣除border-sapcing, border, padding後,會視各欄寬設定值依比例分配寬度,但不能小於內容寬度(在本例中為59)。

當然,不能小於內容寬度的限制可透過CSS技巧解決,例如: th { word-break: break-all; }

搞懂欄寬分配原則後,剩下一個問題,怎麼讓最後一欄接收剩餘寬度卻又不會在寬度不足時消失?

試試<col style="min-width: 60px" />吧! 成功~ (灑花)

切換.NET 4.5導致ASP.NET MVC非同步作業錯誤

$
0
0

為執行SignalR 2.0,將ASP.NET MVC 4專案目標平台改成.NET 4.5。測試了一陣子,今天才由事件檢視器發現: 雖然已編譯成.NET 4.5,因web.config <system.web><httpRuntime />未指定4.5,這段時間一直是用.NET 4.0執行,導致SignalR 2無法啟用WebSocket! (登楞)

修改web.config還不簡單? 順手調了,ASP.NET MVC也壞了! orz

某個Controller Action出現以下錯誤

InvalidOperationException: An asynchronous operation cannot be started at this time. Asynchronous operations may only be started within an asynchronous handler or module or during certain events in the Page lifecycle. If this exception occurred while executing a Page, ensure that the Page is marked <%@ Page Async="true" %>.

InvalidOperationException: 非同步作業目前無法開始。非同步作業只有在非同步處理常式或模組或是頁面生命週期中特定事件期間中才能開始。如果執行頁面時發生此例外狀況,請確認頁面已標示為 <%@ Page Async="true" %>。

研究發現問題出在該Action叫用某個第三方元件,其中引用BackgroundWorker(事實上只要是非同步作業都會導致錯誤,例如WebClient.DownloadStringAsync()),違反ASP.NET 4.5的政策。問題可簡化成以下範例:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;
 
namespace Mvc4.Controllers
{
publicclass HomeController : Controller
    {
public ActionResult Index()
        {
            SomeJobBy3rdPtyLibrary();
return Content("Done");
        }
 
privatevoid SomeJobBy3rdPtyLibrary()
        {
            BackgroundWorker worker = new BackgroundWorker();
            AutoResetEvent are = new AutoResetEvent(false);
            worker.DoWork += (sender, e) =>
            {
                Thread.Sleep(3000);
                are.Set();
            };
            worker.RunWorkerAsync();
            are.WaitOne();
        }
 
    }
}

設成<httpRuntime targetFramework="4.0" />OK,一改成<httpRuntime targetFramework="4.5" />馬上出錯!

解決之道要將Action改為async Task<ActionResult> Index(),並在其中使用await同步等待結果。理想上SomeJobBy3rdPtyLibrary()應改為非同步呼叫配合,但基於程式不在控制範圍,故決定採萬用寫法await Task.Run(() => { … });解決。

public async Task<ActionResult> Index()
        {
            await Task.Run(() =>
            {
                SomeJobBy3rdPtyLibrary();
            });
return Content("Done");
        }

如果SomeJobBy3rdPtyLibrary()屬可控制範圍,改成以下形式會更理想:

public async Task<ActionResult> Index()
        {
            await SomeJobBy3rdPtyLibrary();
return Content("Done");
        }
 
private Task SomeJobBy3rdPtyLibrary()
        {
return Task.Factory.StartNew(() =>
            {
                Thread.Sleep(3000);
            });
        }

為什麼加上<httpRuntime targetFramework="4.5" />會有此差異? 而ASP.NET MVC又為何要對非同步作業作出上述限制? MSDN有篇文章提供頗詳細的說明:

當指定<httpRuntime targetFramework="4.5" />時,等同在web.config加入以下設定:

<configuration>
<appSettings>
<addkey="aspnet:UseTaskFriendlySynchronizationContext"value="true"/>
<addkey="ValidationSettings:UnobtrusiveValidationMode"value="WebForms"/>
</appSettings>
<system.web>
<compilationtargetFramework="4.5"/>
<machineKeycompatibilityMode="Framework45"/>
<pagescontrolRenderingCompatibilityVersion="4.5"/>
</system.web>
</configuration>

而這回遇到的問題,即是受aspnet:UseTaskFriendlySynchronizationContext設定影響。UseTaskFriendlySynchronizationContext會帶來以下好處:

Enables the new await-friendly asynchronous pipeline that was introduced in 4.5. Many of our synchronization primitives in earlier versions of ASP.NET had bad behaviors, such as taking locks on public objects or violating API contracts. In fact, ASP.NET 4's implementation of SynchronizationContext.Post is a blocking synchronous call! The new asynchronous pipeline strives to be more efficient while also following the expected contracts for its APIs. The new pipeline also performs a small amount of error checking on behalf of the developer, such as detecting unanticipated calls to async void methods.
Certain features like WebSockets require that this switch be set. Importantly, the behavior of async / await is undefined in ASP.NET unless this switch has been set. (Remember: setting <httpRuntime targetFramework="4.5" /> is also sufficient.)

相較於ASP.NET 4,新一代的await-Friendly Asynchronous Pipeline、SynchronizationContext移除掉一些會相互阻擋的同步呼叫,改善舊版的一些不良行為,並加入非同步作業的防呆檢查,能提升效率及減少錯誤,WebSocket及ASP.NET async/await特性都需依賴它才能執行。

試著在web.config加入<add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />,果然原先無async版的Action就能順利執行,印證的確是該設定造成差異。但由於專案需要WebSocket,故修改為async版Action是唯一解。

【茶包射手日記】Windows換版後程式出現Registry錯誤

$
0
0

手邊有個運作多年的程式,安裝在廠商提供的Gateway機器上,呼叫廠商提供的API元件收送資料。

接獲通報,因廠商更換Gateway機器,重裝程式卻無法運作,出現無法讀取HKEY_LOCAL_MACHINE\SOFTWARE\BlahAPI機碼(Registry)錯誤。由於機器、Windows、API元件、.NET程式全都可能異動,一堆變數並存,猶如在一片小籠包間射茶包... orz

初步判斷非API元件版本問題,而檢查Registry的確找不到HKLM\SOFTWARE\BlahAPI,但若真缺少機碼,為何其他相關程式卻運作正常?

查詢API文件發現一則寶貴提示,HKLM\SOFTWARE\Wow6432Node\BlahAPI。啊! Wow6432Node,莫非問題出在32bit vs 64bit!。果然,Windows機碼缺少HKLM\SOFTWARE\BlahAPI,但有HKLM\SOFTWARE\Wow6432Node\BlahAPI。

由此做出推論:

BlahAPI為32位元程式,安裝於Windows 7 x64t時,機碼會寫在HKLM \ SOFTWARE \ Wow6432Node (參考: TIPS-64位元Windows的系統登錄),其他可運作程式為32位元,故能順利讀取HKLM\SOFTWARE\BlahAPI (實際上在HKLM\SOFTWARE\Wow6432Node\BlahAPI)。當.NET程式以64位元模式執行,便出現找不到HKLM\SOFTWARE\BlahAPI錯誤;之前沒出錯,是因為機器原本安裝Windows 7 32bit,所有程式都在32bit模式執行。

使用corflags檢查問題.NET程式的設定(參考: 檢查.NET程式平台目標(Platform Target)),目標平台確實被設為Any CPU,在x64 Windows平台會以64位元程序(Process)執行,佐證以上推測。

知道原因,解決是小事一椿,懶得重新編譯程式,使用corflags 32BIT+ myApp.exe將.NET程式目標平台指定成x86,錯誤便隨風而逝~~ 又成功射下茶包一枚,開心!


2014八卦山台地馬拉松~

$
0
0

我的第15馬,八卦山台地馬拉松。參加這場最主要為了領馬拉松普查的十馬獎,普查網每年會選取一場到兩場馬拉松比賽頒獎,身為馬拉松三年級生,到去年底已累積12場,符合十馬獎資格。今年普查獎春季場選在八卦山馬,適逄清明又辦在南投,便來個掃墓兼跑馬,一舉兩得 :P (有興趣了解馬拉松普查的朋友,可參考獅子頭大哥整理的FAQ)


早上五點多到達會場,天色明亮,與萬金石相距不足一月,差異卻很大,終於不用摸黑寄物、集合,但換來破30度的高溫,不划算啊~

   

普查獎頒獎區已有志工在準備,還有精心規劃的頒獎舞台,十馬獎,等著我吧!

出發前,軍容盛大的台積電三百馬團正在會場內揮旗吶喊誓師。300馬! 恐怖的數字,累積到此等跑量,出門去買個醬油都會不小心繞路跑完一個全馬吧?

特別的開場,鳳鳴國中的同學舉著會旗進場並升旗,這些運動會儀式早已遠離我的生活,難得有機會回味卻也新鮮。此時離起跑已不到15分鐘,趕緊前往起跑點集結。

這是我跑馬生涯以來最另類的起跑了!

跑友們在Moves Like Jagger的熱血音樂陪伴下活動手腳暖身,隨著6:30一分一秒逼近,預期的鳴槍倒數卻沒出現,正狐疑該不會要延後起跑吧? 吵雜音樂聲忽然夾雜一聲鳴槍!! 啥? 出發了? 大伙面面相覷,但隊伍確實開始往前移動,真的起跑了! 特命名為"出其不意驚愕起跑法",哈! XD

  

  

賽道主要都在139縣道,由鳳鳴國中出發先下坡跑15公里折返,接著把去程的200公尺開心下坡吐回去,乖乖吞完上坡回到出發點剛好30K,再往彰化方向跑6K,最後有個近百米落差急下坡到第二折返點,折返爬完陡坡回到八卦山台地抵達終點。途中經過微熱山丘及天空之橋等著名景點,而139縣道在近彰化段路旁綠路成陰(應該是小葉欖仁吧?),部分路段能俯覽山下景色,很是優美,一路上見到不少自行車隊甚至重型機車群,想必是有名的運動休閒路線。

而在八卦山台地一帶,只見道路兩旁盡是鳳梨田,綿延好幾公里都是鳳梨鳳梨鳳梨,這場稱之"鳳梨馬"亦不為過。土包子蹲在路旁拍鳳梨,忽然背後有人叫學長,原來是當兵時的直屬學弟,退伍後失去聯絡,卻在2012櫻花初馬會場重逄,之後便常在馬場相遇,有趣的人生境遇。學弟今天帶著公司同事跑初馬,三人索性邊跑邊聊,後半馬再也不無聊,少了詛咒自己手賤報名要在大熱天受罪的內心戲,也不需要在心裡一直吶喊"撐下去,不要放棄",聊著聊著,里程1K 1K增加,不知不覺就跑完了。過去從來都是獨跑,這回才知"聊天馬"這麼有趣!

老話一句: 馬拉松其實是場10公里賽跑! 不過,開始前要先來個32公里暖身。鳳梨馬(咦?)每公里都有里程標示,牌子跟地面噴潻都很精美,深得我心。

  

鳳梨馬(喂,別給人家亂改名字)的補給挺好,有香蕉、小蕃茄、茂谷柑、鳳梨、豆干(讚)、餅乾、檸檬片、水餃(哈)、水、運動飲料、沙士,最特別的是微熱山丘的土鳳梨汁(據說比汽油還貴,好喝)。過去在馬場常看到檸檬,我卻很少入口,這回首嚐"檸檬片沾鹽咬一口,喝一大杯水"的龍舌蘭(Tequila)式補水法,超 解 渴 der! 決定將其沾鹽檸檬片訂為奧林匹克指定補給。XD

第二次戴Bryton Cardio 40 GPS錶參賽,表現不錯,里程顯示幾乎跟大會標示完全一致,後來卻發生悲劇。首先是最後6K才驚覺手錶的跑步時間比實際時間少8分鐘(應該是傳說中的"智慧暫停"作祟),導致我過度樂觀,以為拿下Sub 5如探嚢取物,等聊天討論尾段配速發現大家認知有差,這才知道我少算了8分鐘才覺老神在在 orz 更慘的在後頭,最後3K手錶開始出現電量不足警示,上回跑萬金石四個半小時完賽電量仍過半,今天怎麼耗這麼快? 倒數1.5K手錶,直接沒電關機,幸好已近尾聲且有朋友同行,跟著抓時間,總算保住Sub 5。事後檢討,電量不足有兩種可能: 1) 前一晚充電時因原本電量接近全滿,故一出現充飽符號就拔掉,或許應繼續再充一段時間 2) 忘了關閉心率警示,加上門檻值偏低,全程不時嗶嗶,警示時會開啟背光雪上加霜,導致電力提早耗盡。

沒了GPS記錄,只能以大會成績單為憑。晶片成績為4:56:37,守住Sub5。(拍到的終點計時器為何不到4H53M是個謎) 領了獎牌、毛巾、礦泉水、還有一罐土風梨汁伴手禮(大心),也如願領到我的"十馬獎"。

獎牌挺可愛的~

十馬獎!! 謝謝馬拉松普查,晶片押金順手投進捐款箱當成微薄回饋,繼續向30馬邁進~(握拳)

【茶包射手日記】IIS的29小時魔咒

$
0
0

趁著假日對一台ASP.NET MVC網站進行長時間壓測,初期數據表現不俗,顯示調校策略奏效,放著讓程式跑測試穩定性。中午因事外出,回家後馬上檢查系統是否穩定,登楞! 測試畫面顯示Web Application已重啟… orz

猜想是程式寫法有問題導致Crash,心頭涼了半截,沮喪地檢查IIS主機的事件檢視器,想找出ASP.NET錯誤或IIS Crash的線索,卻發現以下記錄:

A worker process with process id of 'xxx' service application pool 'zzz' has requested a recyle because the worker process reached its allowed processing time limit.

因為工作者處理序已達到允許的處理時間上限,所以伺服應用程式集區 'zzz' 且處理序識別碼為 'xxx' 的工作者處理序已要求回收。

哈! 原來不是程式當掉,而是Application Pool(應用程式集區)被IIS強制回收導致重啟。一則以喜,一則以憂,喜的是程式沒錯,不需要抓Bug ^_^;憂的是自己的IIS經驗不及格,無法一眼看出這是什麼妖魔鬼怪? orz

抱著慚愧的心情查文件,整理筆記如下:

從IIS6起,就有定期重啟Application Pool機制,以解決程式跑久可能出現記憶體洩漏(Memory Leaking,指記憶體用完沒歸還,導致可用記憶體愈來愈少)或其他稀奇古怪的問題,這類疑難雜症通常在重新啟動程序後就會一掃而空。但,這是那門子鴕鳥心態? 程式沒寫好不是該徹底抓漏除盡一切Bug嗎? 靠重開逃避問題算什麼男子漢?

的確,天下沒有抓不到的Bug,端看你願意付出多少心血跟它對抗,無奈人生苦短、老闆/客戶耐心有限! 花上半年反覆實驗,靠奇技淫巧解開程式持續跑三天當機之謎,其興奮度勝過拿下NBA總冠軍,這點我絕對相信。但是,若放任正式系統不穩超過一星期,開發團隊早已萬箭穿心。更何況機房、系統裡常存在人類至今無法理解的神祕力量(不然機房也不會有那麼多"乖乖傳奇"),如果3R(Reset、Restart、Reinstall)可以立刻解決又不傷身體,何苦跟自己過不去? 要追根究底不是不行,把戰場拉回實驗室再談成長學習,正式環境還是得求快速解決問題。

於是,IIS有定期回收(Recyling)機制,預設每29小時(1740分鐘)回收Worker Process。莫非我就是中了IIS 29小時魔咒? 由IIS Log證實了這點:

由回收時間4/4 12:50向前推29小時,為4/3 07:50,換成UTC時間為4/2 23:50,Bingo!!

任意回收Worker Process,難道不會影響線上服務? IIS設想周到,回收前會先悄悄另起新的Worker Process,開始接收Request,現有Process則會等到進行中的Request完成再下線,達到無縫接軌的效果(稱為Overlapped Recycle)。如果網站程式被設計成Stateless(無狀態,指IIS主機本身不保存任何狀態資料),切換過程如同Web Farm裡換一台主機連線,使用者是完全無感的。但我的專案並非Stateless,一旦切換主機,儲存於記憶體中的資料遺失,前端就必須重新初始化才能繼續運作。換句話說,如果專案非Stateless,就會遇到每隔29小時程序無故重啟、狀態遺失的魔咒!

幸好在壓測階段發現,等上線後在尖峰時刻爆開,肯定被罵到臭頭!

希望定期重啟維持穩定又不想影響線上服務怎麼辦? 這次的專案受到一些限制,要改成Stateless並非易事,幸好IIS提供另一個選擇 -- 允許管理者自行排定回收Worker Process的時程。剛好系統有每日維護作業時段並對使用者公告,因此在本案例只需將回收作業排在該時段執行就萬事OK。

修改方式如下,透過IIS管理員,找到Appliction Pool,開啟進階設定:

如上圖所示,預設Regular Time Interval(minutes)為1740=29小時

將1740改為0,並在Specific Times加上指定的回收時間即可完成設定。
(除了使用IIS管理員,也可修改ApplicationHost.config <recycling>使用PowerShell設定。)

【結論】
如果你的網站會因Worker Process重啟導致使用者狀態遺失,務必調整IIS預設的29小時回收設定,以免在尖峰時間發生悲劇。

透過JavaScript將HTML轉為圖檔

$
0
0

最近寫了小工具用ASP.NET MVC及Knockout讀取跑道計圈GPS資料轉成HTML表格,當成運動記錄的圖檔附件,但每次產生HTML表格後都得用螢幕擷取工具將網頁畫面另存圖檔,雖然手續還算單純,但你知道,懶惰是沒有極限的,我開始動腦筋,打算將產生圖檔的動作也自動化。

薑薑薑薑~ 如上圖所示,真的被我找到方法! 一切要感謝神奇的程式庫—html2canvas,它可以將整張網頁或局部元素轉為HTML5 Canvas,一旦轉成Canvas,匯出圖檔便是小事一椿囉!

html2canvas的原理並非擷取網頁畫素,而是解析DOM及CSS的所有細節,在HTML5 Canvas裡以線、矩形、圓弧、文字... 等試著模擬出相同外觀,因此產出結果可能有細微誤差,也存在無法讀取跨網域內容的限制,但經實際使用,效果已讓人滿意,html2canvas幾乎是用Canvas打造出靜態網頁瀏覽器引擎,是神人級的作品。(咦? 我怎麼跪著寫範例程式... )

範例程式碼如下,有興趣的朋友也可試玩看看:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>HTML to Image</title>
<style>
    table { background-color: #ccddff; }
    td { padding: 2px 6px; }
    table input { 
      width: 100px; padding: 2px 6px; 
      color: blue; border: 1px solid gray; 
    }
    fieldset { width: 200px; height: 120px; margin-top: 6px; }
</style>
</head>
<body>
<table>
<tr><td>姓名</td><td><inputvalue="Darkthread"></td></tr>
<tr><td>積分</td><td><inputvalue="9999"></td></tr>
</table>
<hr/>
<inputtype="button"value="轉為圖檔"/>
<a></a>
<fieldset>
<legend>圖檔</legend>
<div>
</div>
</fieldset>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js "></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js "></script>
<script src="http://html2canvas.hertzen.com/build/html2canvas.js"></script>
<script>
    $(":button").click(function() {
      html2canvas($("table")[0], {
        onrendered: function(canvas) {
var $div = $("fieldset div");
          $div.empty();
          $("<img />", { src: canvas.toDataURL("image/png") }).appendTo($div);
        }
      });
    });
</script>
</body>
</html>

集合物件的多執行緒存取注意事項

$
0
0

小敏,你有看過Dictionary<string, string>的Key塞null值嗎?

薑! 薑! 薑! 薑~~

依照MSDN文件的說明,Dictionary<TKey, TValue>的Key值不接受null值(A key cannot be null),如果你想硬幹dict.Add(null, "BlahBlah"),.NET將賞你一個"System.ArgumentNullException: 值不能為 null。"

那麼,上圖的狀況是怎麼搞出來的呢?

進入多執行緒的世界,一些原本再簡單也不過的程式小片段,就像沾到火種源,頓時變身狂派機器人跑來咬你屁股 orz 原本執行好好的程式,移到多執行緒環境,開始隨機冒出稀奇古怪的錯誤,而且只在多人使用或壓測時才出現,難以預測及重現,很難追蹤除錯。(例如: 要上百個Client同時使用才出現的問題,除了加入大量Log記錄執行痕跡配合壓測抓蟲,別無捷徑)

關鍵在Thread-Safty! 當程式被單緒執行時,邏輯單純較易想像,因為永遠只有一段程式在執行,物件的狀況改變完全操之在我;一旦轉換到多執行緒場景,就得擔心這行才存取的資料,會不會下一行就變了? A方法使用到B變數,但同時可能有其他Thread執行C方法也去更改B變數,彼此會不會打架? 就像這樣,撰寫每一段Code都得考慮涉及的每項資源、物件同一時間是否可能被其他Thread所存取變更,只要有一處沒設想保護妥當,就等同埋下一顆不定時炸彈,等待未來某一天機緣成熟爆炸... 唯有一切考慮周到,有信心程式不會因多個Thread同時執行出錯,才能宣告程式是Thread-Safe!

不共用任何變數、資源是確保Thread-Safe最簡單的方式,但並非所有情境都能實現,只能靠在變數、物件加上保護並謹慎使用才能保證在多執行緒環境下不出錯。

先來看前面讓Dictionary<string, string>錯亂的程式範例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace IEnumThreadSafty
{
class Test1
    {
static Dictionary<string, string> dict = 
new Dictionary<string, string>();
 
publicstaticvoid Run()
        {
bool running = true;
            Random rnd = new Random();
            Task.Factory.StartNew(() =>
            {
while (running)
                {
try
                    {
                        dict.Add(Guid.NewGuid().ToString(), "A");
                    }
catch (Exception ex)
                    {
                        Console.WriteLine("Error: " + ex.ToString());
                    }
                    Thread.Sleep(rnd.Next(5));
                }
            });
 
            Task.Factory.StartNew(() =>
            {
while (running)
                {
                    Console.WriteLine("Dictionary Count={0}", dict.Count);
                    Thread.Sleep(1000);
                }
            });
            Thread.Sleep(1000);
 
            Task.Factory.StartNew(() =>
            {
while (running)
                {
if (dict.Count > 0)
                    {
string key = null;
try
                        {
                            key = dict.Keys.First();
                            dict.Remove(key);
                        }
catch (Exception ex)
                        {
                            Console.WriteLine("Error: " + ex.ToString());
                        }
                    }
                    Thread.Sleep(rnd.Next(4));
                }
            });
 
            Console.ReadLine();
            running = false;
        }
    }
}

很簡單,只要開兩條Thread,一條不斷的Add,一條不斷的Remove(用First()取第一筆移除),就能見識許多古怪離奇的錯誤,例如:

  1. Add()時抱怨"索引在陣列的界限之外"
    Error: System.IndexOutOfRangeException: Index was outside the bounds of the array. (索引在陣列的界限之外。)
       at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boo
    lean add)
  2. First()時遇到"集合已修改; 列舉作業可能尚未執行"
    (First所遇到的問題,Where、ToList、Select等LINQ操作都可能遇到)
    Error: System.InvalidOperationException: Collection was modified; enumeration op
    eration may not execute. (集合已修改; 列舉作業可能尚未執行。)
       at System.Collections.Generic.Dictionary`2.KeyCollection.Enumerator.MoveNext()
       at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
  3. 明明Count>0才執行,First()卻冒出"序列未包含項目"
    Error: System.InvalidOperationException: Sequence contains no elements. (序列未包含項目)
       at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)

這些只是冰山一角,實際玩一回,還能看到更多離奇狀況。看似簡單的程式碼忽然處處是雷,讓人疑神疑鬼,且需壓測模擬加上一些運氣才能重現,偵錯難度極高,除了在執行路徑加上Debug Log,事後再從數千行Log裡拼湊推敲,還原出錯情境,似乎沒有更好的方法。即便是老練的程式設計師,也常會深陷多時方能脫身,稱之"多執行緒地獄"也不為過。

所幸,比起尋找爆炸點,修正工作簡單許多。只要能重現問題找到出錯根源(通常是某個被共用的物件出現離奇錯誤),用lock機制限定同一時間只能有一個Thread存取(讀與寫都要加入保護),多半就能藥到病除。(但要注意: lock範圍的程式執行時間不可太長,否則輕則阻擋其他Thread執行傷及效能,重則增加Deadlock的發生機率)

以先前的程式為例,只需在Add、First、Remove處加上lock,程式便不致出錯!

//...省略...
lock (dict)
    {
        dict.Add(Guid.NewGuid().ToString(), "A");
    }
//...省略...
lock (dict)
    {
if (dict.Count > 0)
        {
string key = dict.Keys.First();
            dict.Remove(key);
        }
    }

雖然加上lock能有效防止多Thread存取衝突,但缺點是"所有可能發生衝突的讀寫動作都要加lock",只要有一 處漏寫照樣會出包。(跟SQL Injection一樣,只要一個漏洞就滿盤皆輸)

如果lock要保護的對象是Dictionary<T, T>或List<T>,.NET 4.0開始有更方便的選擇: ConcurrentDictionary<T, T>ConcurrentBag<T>(除此之外System.Collections.Concurrent命名空間還有支援多執行緒存取的Queue、Stack等類別,適用不同場合),內建lock保護機制,能在多執行緒環境執行不出錯,等同Dictionary<T, T>及List<T>的Thread-Safe版本。

於是,上面的程式可用ConcurrentDictionary改寫如下: (註: 需以TryAdd及TryRemove取代Add, Remove)

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace IEnumThreadSafty
{
class Test3
    {
static ConcurrentDictionary<string, string> dict =
new ConcurrentDictionary<string, string>();
 
publicstaticvoid Run()
        {
bool running = true;
            Random rnd = new Random();
            Task.Factory.StartNew(() =>
            {
while (running)
                {
                    dict.TryAdd(Guid.NewGuid().ToString(), "A");
                    Thread.Sleep(rnd.Next(10));
                }
            });
 
            Task.Factory.StartNew(() =>
            {
while (running)
                {
                    Console.WriteLine("Dictionary Count={0}", dict.Count);
                    Thread.Sleep(1000);
                }
            });
            Thread.Sleep(1000);
 
            Task.Factory.StartNew(() =>
            {
while (running)
                {
string key = dict.Keys.FirstOrDefault();
if (!string.IsNullOrEmpty(key))
                    {    
stringvalue;
                        dict.TryRemove(key, outvalue);
                    }
                    Thread.Sleep(rnd.Next(10));
                }
            });
 
            Console.ReadLine();
            running = false;
        }
    }
}

如此不用擔心漏加lock闖禍,應可讓人在多執行緒地獄少爬幾十公尺刀山吧? 呵~

【茶包射手日記】SignalR導致Mac Safari Crash

$
0
0

測試發現用Mac Safari連上專案網站,在某些狀況下會連續Crash:

接著網頁因重複發生問題宣告停用:

在Crash報告中看到WebSocketChannel、ScoketStreamHandleBase等字眼,該網頁唯一會涉及WebSocket的只有SignalR,改用Chrome測試,找到SignalR在特定情境建立連線出錯的證據:

由HTTP 500錯誤訊息追進程式,發現PushHub的public override Task OnConnected()有Bug會throw Exception,造成SignalR Script在網頁載入後歷經"試圖建立連線->失敗->嘗試建立連線->失敗->..."的循環(猜想連帶也有WebSocket相關動作),在Chrome及其他瀏覽器只視為網路存取錯誤,但Safari卻因此Crash。SignalR Github Issue回報區有類似錯誤報告,Safari(不限iMac,也有iPad) + WebSocket導致Crash,但沒找到跟我相同的錯誤訊息。

問題在修正OnConnected Bug後消失!

結論: SignalR + WebSocket + Safari如發生Crash,可優先檢查是否為連線過程有錯導致。

Viewing all 2311 articles
Browse latest View live