Quantcast
Channel: Darkthread
Viewing all 428 articles
Browse latest View live

Oracle NVarChar2 可存中文字數上限問題

$
0
0

同事分享在 Oracle 踩到 NVarChar2 中文字數上限的地雷,一句話點醒我夢中人,嚇得我屁滾尿流失了魂,原來我也搞錯多年。

不囉嗦,直接看圖。

我們都知道,NVarChar2 的長度上限是 4000,而 NVarChar2 支援 Unicode,不管是中文或英數字,一個字元都算1,所以 NVarChar2(4000) 可以儲存 4000 個中文字? 錯了! 是 1,333 個,如果你試圖塞入 1,334 個中文字元,將會得到「ORA-01401 插入值過大」錯誤。

用 LEGNTH()、LENGTHB() 觀察就很清楚了。當資料庫使用 UTF8 編碼,一個中文字元算 3 個 Byte,1333 個中文耗用 3999 Byte,1334 就會破表。

官方文件有提到這點:(參見Table 6-5 Character Set Advantages and Disadvantages for a Unicode Datatype Solution)

The maximum lengths for the NCHARand NVARCHAR2 columns are 2000 and 4000 characters respectively, which is more than those for NCHAR(1000) and NVARCHAR2 (2000) in AL16UTF16. Although the maximum lengths of the NCHAR and NVARCHAR2columns are larger in UTF8, the actual storage size is still bound by the byte limits of 2000 and 4000 bytes, respectively. For example, you can store 4000 UTF8 characters in an NVARCHAR2 column if all the characters are single byte, but only 4000/3 characters if all the characters are three bytes.

總之,這是 Oracle 的設計始然,只是我誤解了十多年,用 SQL Server NVarChar 最大長度行為想當然爾。

改用 SQL Server 驗證:

NVarChar(4000) 可以放 4000 中文字沒問題,SQL Server 內部使用 UCS-2 編碼,實際儲存 Byte 數為 8000。

總論:Oracle NVarChar2 跟 SQL Server NVarChar 的最大長度都是 4000,只是 Oracle 要抓字串轉為 byte[] 後的長度,依編碼而異,放中文或英數字的字數上限不同,若為 UTF8 純中文可放 1333 個字元;SQL Server 則是抓字串字數,可放足 4000 個中文字元。


如何自訂 OpenCC 字彙轉換表

$
0
0

OpenCC 已提供十分優質的繁簡轉換,不過呢,實際使用下來難免會有些不到位的地方。所幸,OpenCC 的架構開放又有彈性,修改 json 設定檔就能載入自訂轉換字典,如果對既有轉換表或轉換規則不滿意,OpenCC 開放源碼,絕對讓你改到開心為止。

用個簡單例子示範如何自訂字彙轉換。假設我想將「黑暗執行緒在雲霄飛車上吃便當」翻成簡體,如使用包含常用詞彙轉換的設定檔 tw2sp.json,轉換結果如下:

輸出結果為「黑暗綫程在云霄飞车上吃便当」,而我希望保留「執行緒」不要翻成「綫程」,並將「云霄飞车」與「便当」 翻成大陸用語「过山车」與「盒饭」。

遇到 OpenCC 未包含的轉換字彙,最簡單的解決方法是在 json 設定加掛自訂的轉換表。如下圖,每行文字以 Tab 鍵相隔,前方是繁體中文詞彙,後方是希望轉換的簡體中文詞彙:

將 tw2sp.json 另存為 my-tw2sp.json 再修改加入{ "type": "text", "file": "TWCustMapping.txt" }:

改用 my-tw2sp.json,「过山车」與「盒饭」對了,但「綫程」沒改過來:

細究原因,是 TWPhrasesRev.ocd 裡定義了將「執行緒」轉為「綫程」。要修正這點可從原始碼中找到 TWPhrasesRev.txt,新增一條專有名詞,指定「黑暗執行緒」還是翻成「黑暗執行緒」,值得注意的是 TWPhrasesRev.txt 每一行前後都是繁體。

如下圖,我們將 TWPhrasesRev.ocd 換成 TWPhrasesRev.txt,type 則改成 "text":

重跑一次,結果就完全符合預期了:

OpenCC 支援 .txt 跟 .ocd 兩種格式的字典檔,修改並改用 TWPhrasesRev.txt 即可自訂轉換詞彙,如希望提高轉換效率,可利用 opencc_dict.exe 工具將修改版 .txt 轉換成 .ocd。(注意:.ocd 檔 32/64 位元有別,請用正確位元版本的 opencc_dict.exe 進行轉換)

掌握以上技巧,我們就能微調轉換結果,符合客戶的各式要求囉~ 祝大家轉換愉快。

【笨問題】Word 使用非細明體時行距過大

$
0
0

我有個困擾多時的 Word 問題,每回將細明體、標楷體換改成微軟正黑體或其他字體時,行距會變成超大(例如以下示範):

之前我的鴕鳥做法是修改行距為固定行距,但一直不知其所以然,最近花了點時間研究才理解問題所在與正確解法。

關鍵在於 Word 預設啟用了「文件格線被設定時,貼齊格線」,勾選「檢視格線」後便可一目膫然。

平平是 12 號字,細明體及標楷體尺寸較小,恰好可以塞入兩條格線之間:

 

當選用微軟正黑體、Google 思源黑體等其他字型,同樣是 12 號字,尺寸卻超過兩條格線的高度,於是 Word 選擇三條格線放一行字並垂直置中,造成行距變得超大:

實測將字型縮小,微軟正黑體縮到 10 號,Google 思源黑體縮到 9 號,行距便與細明體或標楷體一致:

 

既然與貼齊格線有關,解法有二:

  1. 修改版面設定,設成「沒有格線」

  2. 修改段落設定,取消「文件格線被設定時,貼齊格線」

第一種做法範圍為整份文件,第二種做法需要逐段設定,實務上則可使用「設定成預設值」套用到預設範本,如此就不用每次開新文件都需調整囉~

【茶包射手日記】網頁特定連結失效疑案

$
0
0

遇上個人射手生涯數一數二的坑爹茶包...

故事是這樣的。接獲報案,有使用者投訴他換新電腦後無法點選內部網站選單的某個連結,其餘功能正常,而全公司只有這一起案例。

起初懷疑是 JavaScript 故障,實際連上使用者電腦測試並未發現 JavaScript 錯誤,而在使用 F12 偵察過程連結忽然正常,正要以「新電腦需經開光才會正常」的靈異理由結案,判定前為求謹慎再試了一次,這才發現問題未解 - 問題只出現在瀏覽器最大化時!

接著我懷疑網頁上有東西遮蔽了連結,想用 F12 開發工具檢查元素卻選不到那個無法點選的連結,而滑鼠移動到該連結也不會顯示手指圖示,我弄了一個現場摸擬還原當時情況,如下圖,滑鼠移到最右側漢堡選單理應出現手指,但並沒有:

用力觀察看出一些異樣,無法點選區域有個顏色非常淺的圓弧(見下圖,要張大眼睛才看得到),瀏覽器縮小後拖拉時該圓弧不會跟著移動,看起來是浮在瀏覽器上方。若在該區域按右鍵則更明顯,跳出陌生選單:

一查之下,原來桌面右上角藏了一個名為 ClocX 的桌面時鐘,其透明度被設到很低並指定浮在桌面最上層(Always on top),且關閉了滑鼠經過現身設定(Mouse-Over Transparent)... 如果這不是坑人,什麼是坑人呢?

關閉透明效果,真相大白!

為什麼使用者自己裝了這種東西卻不知道?透明度調那麼低是哪招?這樣搞應該一堆軟體卡到陰,為何要輪到我來射茶包?

嗯,我肯定就是那個「被選中的人」,好一個「靠盃的考驗」!

LibreOffice docx 轉 pdf 評估筆記

$
0
0

我有寫了一個 Word 套表服務,最早是用 C# 呼叫 Word執行置換及轉 PDF,後來改走 OpenXML SDK,罝換速度快了五倍以上,唯獨轉 PDF 這段還只能仰賴 Word 完成。從 ASP.NET 呼叫 Word Application 會受限執行身分權限過低,Word 程序的生命周期亦較難掌控,最後我決定寫成 Windows Service,以特定登入帳號啟動固定數量的 Word 程序,以 Web API 方式接收並平均消化套表需求。做法可行且運行了一陣子,但有以下缺點:

1.    套表服務為獨立 Windows Service,當網頁執行套表出錯,追查範圍變大,追查路徑較複雜
2.    必須在伺服器安裝 Word,且為配合背景執行有額外設定步驟
3.    Word 為前景程式,以 Windows Service 長期執行時偶爾會出鎚,需人工介入排除

因此,我老惦記住想找到不需 Word 也能將 DOCX 轉 PDF 的替代方案,攻下「非 Word 不可」的最後一里路,把整個套表轉 PDF 作業都搬進 ASP.NET 簡化系統架構。

DOCX 轉 PDF 有不少商業元件選項,但一致特色是大多價格不斐(數百至數千美金不等),且是否能精確轉換亦需驗證。所以我決定先從免費開源解決方案看起,LibreOffice是最佳選擇,它延續 OpenOffice 的使命,提供跨平台的
免費開放文書軟體,社群很活躍持續有更新,對 MS Office 格式支援也夠完整。

LibreOffice 有安裝程式,但實測 Copy 檔案也能部署,使用命令列 soffice  --headless --convert-to pdf filename.docx 即可將 .docx 轉 .pdf,另外驗證可在 ASP.NET 直接呼叫執行沒問題。(Github 有相關元件可以省工) 餘下的問題只剩相容性 – LibreOffice 讀取 DOCX 轉 PDF 是否能保有 Word 編譯格式,決定其可接受度。

為此我做了一次批次測試,請同事提供五百多個線上實際使用的套表範本,以 ASP.NET 程式呼叫 LibreOffice 批次轉成 PDF。除了一個檔案有問題需要手工前置轉檔(先存 doc 用 LibreOffice 開啟另存 docx,很無厘頭但有效),其餘檔案都成功轉成 PDF,每次耗時在 3 秒內,速度很 OK。

而最關鍵的問題 – 轉出的PDF排版有跑掉嗎?有壞消息也有好消息。

壞消息是大約有六至七成出現程度不一的排版差異,有些只影響美觀,有些影響閱讀必須調整。
好消息是所有排版差異都有解法,使用者必須改變習慣或重新修校,但沒有遇到無解項目。

整理遇到的問題典型如下:

  1. 簽章圖檔只有部分顯示
    原因:圖檔被貼在表格儲存格裡。
    解法:剪下改貼至文件本體,文繞圖改為「文字在後」即可
  2. 表格框線消失、變粗
    原因:多為 Word 重複設定框線造成(上欄設了下框線,下欄位又設上框線),在 Word 合併只顯示一條,但 LibreOffice 兩條都顯示會變成粗黑線。
    解法:重新調整框線可修正
  3. 表格行高不足,文字下半截消失 
    原因:在 Word 指定不足以放入字型大小的固定列高,Word 會自動撐大但 LibreOffice 不會
    解法:修改為合理列高
  4. 段落下半部被遮蔽
    原因:段落設定了過小的固定行高,Word 會強迫撐大但 LibreOffice 不會
    解法:調為單行間距
  5. 簽章圖檔變形(雙倍寬度且只顯示左半邊)
    原因不明,將圖檔另存GIF重新插入後恢復正常
  6. 頁首圖檔錯位
    原因是插入的圖檔未經修剪,幾與紙張同寬,在 Word 裡部分已超出頁面範圍。而 LibreOffice 不允許圖檔超出範圍,導致圖案左移,裁剪圖檔至適當尺寸可解決。
  7. 頁首、頁碼跑位
    原因:使用未使用 Word 頁首功能,是自己算位置放標題、頁碼,LibreOffice 排版結果與 Word 有出入位置就錯了
    解法:回歸使用頁首、頁尾正統做法
  8. 表格浮至上方並覆蓋文字
    原因為 Word 排版抓太緊,LibreOffice 與 Word 顯示位置有誤差,需加入緩衝區
  9. docx 無法使用 LibreOffice 開啟
    五百多份文件僅有一例,原因不明。
    Workaround 為 Word 先改存 doc,交給 LibreOffice 開啟再另存回 docx,過水後問題排除。

【結論】

使用 LibreOffice 取代 Word 轉 PDF 作業,直接在 ASP.NET 執行是可行的。但改用第三方轉換軟體排版結果難免有所出入(實測差異性比原本想像來得大),需使用者配合在上傳範本前自行調整 Word 文件確認輸出 PDF 正常。最簡單的做法是請使用者安裝 LibreOffice,上傳前自行檢查試轉 PDF 目視確認,另外還需提供 FAQ 引導遇到輸出不一致時如何調整。與現行使用者可無腦使用 Word 任意修改文件都不用擔心出錯不便,將影響使用者接受度。至於二者輸出結果差異有些屬於對文件規格實作的見解不同,無關對錯,嘗試其他文書轉換元件應也會面臨相似問題,只差在嚴重程度有別。若要求零誤差,使用 Word 才能保證萬無一失。

小技巧 - ASP.NET MVC 限定 POST 但開放本機 GET

$
0
0

分享 ASP.NET MVC 私房小技巧一則。

AJAX 呼叫 ASP.NET MVC 時,基於安全考量應限定 POST 方法。(參考:隱含殺機的GET式AJAX資料更新 - 黑暗執行緒)
不過在開放測試階段,開放 GET 可在瀏覽器網址列輸入 URL 測試較方便,有沒有兩全其美的方法?

於是我寫了一個 Action Attribute,實現「從 localhost 呼叫可用 GET,從正常 IP 存取只能 POST」的效果,像是這樣:

HomeController 的 Test Action(),瀏覽器透過 localhost 可讀取,改用實際 IP 則傳回 HTTP 404。應用方法很簡單,在 Action 加上 [LocalHttpGetOrHttpPost] 即可,基於安全考量,預設只開放 POST,必須在 appSetting 加入 <add key="EnableRestrictedGet" value="true"/> 才開放本機使用 GET :

[LocalHttpGetOrHttpPost] 
public ActionResult Test() 
{ 
    return Content("Test OK"); 
} 

為求彈性起見,再多支援透過 appSetting 指定開放 GET 存取的 IP 清單。例如:

[RestrictedHttpGetOrHttpPost("TestIps")] 
public ActionResult Test() 
{ 
    return Content("Test OK"); 
}

設定檔需加入對應的 IP 清單:

<appSettings> <add key="EnableRestrictedGet" value="true"/> <add key="AllowGetIPs" value="::1,127.0.0.1,172.28.1.1"/> </appSettings>

完整程式範例如下,有需要的同學請自取:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Reflection; 
using System.Web; 
using System.Web.Mvc;

namespace System.Web.Mvc 
{ 
    public class LocalHttpGetOrHttpPostAttribute : RestrictedHttpGetOrHttpPostAttribute 
    { 
        public LocalHttpGetOrHttpPostAttribute() : base("localhost") 
        { 
        } 
    }

    public class RestrictedHttpGetOrHttpPostAttribute : ActionMethodSelectorAttribute 
    { 
        //REF: https://github.com/mono/aspnetwebstack/blob/master/src/System.Web.Mvc/HttpGetAttribute.cs 
        private static readonly AcceptVerbsAttribute getCheck = new AcceptVerbsAttribute(HttpVerbs.Get); 
        private static readonly AcceptVerbsAttribute postCheck = new AcceptVerbsAttribute(HttpVerbs.Post);

        private static readonly bool EnableRestrictedGet = 
            System.Configuration.ConfigurationManager.AppSettings["EnableRestrictedGet"] == "true";

        private readonly string[] AllowedGetIPAddresses = "::1,127.0.0.1".Split(',');

        public RestrictedHttpGetOrHttpPostAttribute(string allowedIpSettingName) 
        { 
            if (allowedIpSettingName != "localhost") 
            { 
                var allowedIps = System.Configuration.ConfigurationManager.AppSettings[allowedIpSettingName]; 
                if (string.IsNullOrEmpty(allowedIps)) 
                    throw new ArgumentException($"appSetting '{allowedIpSettingName}' not found!"); 
                AllowedGetIPAddresses = allowedIps.Split(',', ';'); 
            } 
        } 
        public override bool IsValidForRequest(ControllerContext controllerContext, 
            MethodInfo methodInfo) 
        { 
            return 
                postCheck.IsValidForRequest(controllerContext, methodInfo) || 
                EnableRestrictedGet && 
                getCheck.IsValidForRequest(controllerContext, methodInfo) && 
                AllowedGetIPAddresses.Contains(controllerContext.HttpContext.Request.UserHostAddress); 
        } 
    } 
}

使用 Headless Chrome 擷圖、轉存PDF、爬資料

$
0
0

Chrome 自 59 版起內建了 Headless 模式,允許透過命令列啟動 Chrome 以無 GUI 方式執行,具備與正常開啟完全相同的網頁渲染及 JavaScript 引擎,還可透過網路連線遙控。這個功能可以用於不少有趣應用,這裡列舉幾種實用情境。

註:Headless Chrome 的完整參數可參考 List of Chromium Command Line Switches « Peter Beverloo

網頁擷圖

將網頁存成圖檔或 PDF,過去我是用 PhantomJs。Headless Google 的出現,能取代 PhantomJS 功能且更快更穩,讓 PhantomJS 作者決定停止辛苦的獨力維護工作,PhantomJS 的Github 專案也已封存,故改用 Headless Chrome 已成定局。

要使用 Headless Chrome 擷圖很簡單,在命令列工具輸入:
"c:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --headless --screenshot=E:\Chrome\test.png --disable-gpu --window-size=320,568 https://www.microsoft.com

即可取得圖檔如下:

很簡單吧?

如要模擬行動裝置,則可加入--user-agent="…" 指定User-Agent。例如 Google 網站預設是以桌上電腦解析度呈現,遇直式小螢幕解析度不會自動切換:
"c:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --headless --screenshot=E:\Chrome\test.png --disable-gpu --window-size=320,568 https://www.google.com.tw

擷圖時只能看到部分內容:

遇此狀況,可加上--user-agent="" 傳入 iPhone 5/SE 的 User-Agent 字串克服:

"c:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --headless --screenshot=E:\Chrome\test.png --disable-gpu --window-size=320,568 --user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1" https://www.google.com.tw

加上 User-Agent 後,Google 網站改回傳行動裝置專屬排版(底下還有 App 安裝提示):

 

網頁另存PDF

使用參數--print-to-pdf 可模擬瀏覽器列印功能,將網頁存成PDF,例如:

"c:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --headless --print-to-pdf=E:\Chrome\test.pdf --disable-gpu http://www.microsoft.com

 

線上偵錯

遇到 Headless Chrome 行為未如預期時,一樣可用 F12 開發者工具偵錯,做法是透過--remote-debugging-port=xxxx 參數開啟線上偵錯功能:

"c:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --headless --remote-debugging-port=9222 --disable-gpu http://www.microsoft.com

啟動 Headless Chrome 後,再另開一個 Chrome 連上 localhost:9222,可得到已開啟網頁清單:

點選後即可比照一般網頁使用 F12 開發者工具偵錯:

 

網頁爬蟲、自動測試

除了擷圖跟存 PDF,我們也可以寫 JavaScript 程式操作 Headless Chrome 執行較複雜的動作,很適合用來執行自動測試或擷取網頁內容。要透過 JavaScript 操作 Headless Chrome,需借助一個 Node.js 程式庫 - Puppeteer。 開始之前需先安裝 Node.js,再使用 npm 安裝 Puppeteer。(註:安裝 Puppeteer 預設將一併安裝 Chromium 執行檔,如想直接使用 Chrome 不額外下載,可使用指令 env PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm i --save puppeteer 參考)

Node.js 支援 ECMAScript 6,跟我平常網頁在寫的 JavaScript 寫法大異其趣。但我爬了幾篇文章,倒是也能寫出 Google 查詢關鍵字並抓回搜尋結果的簡單程式。(補充:Puppeteer API 文件 https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md )

程式範例如下:

const puppeteer = require('puppeteer');

(async() => {
const browser = await puppeteer.launch({
	executablePath: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
});
const page = await browser.newPage();
//連上Google搜尋網頁
await page.goto('https://www.google.com.tw', {waitUntil: 'networkidle2'});
var res  = await page.evaluate(() => {
	document.querySelector("input[type=text]").value = "darkthread";
});
//點選並等待結果
await page.click("input[value='Google 搜尋'][type=submit]");
await page.waitForNavigation({ waitUntil: 'networkidle0' });
//擷取影像
await page.screenshot({path: 'result.png'});
//擷取搜尋結果傳回JSON
res = await page.evaluate(() => {
	return [].slice.call(document.querySelectorAll("#ires div.g h3 a"))
	.map((a) => { return { "text": a.innerHTML, "link": a.href }; });
});
console.log(res);
await browser.close();
})();

實測如下:

 

工具箱再添新武器一件。

修改 NTFS 權限會改變檔案修改日期嗎?

$
0
0

修改 NTFS 權限、變更檔名會改變檔案修改日期嗎?

這問題乍聽之下無關緊要,在射茶包過程卻可能是左右偵辦方向的關鍵,有追究到底的必要。

今天遇到一起案例。同事報案,某個運行多時的網站忽然故障,由錯誤訊息懷疑是系統無法從設定檔讀取連線字串,但檢查過「設定檔沒有被修改的痕跡」,格外離奇。注意到了嗎?(謎:你都加了「」,要不注意很難吧?)一般我們判斷檔案是否被修改,主要會依據檔案修改日期,而這裡隱藏了一個假設 - 檔案只要被更動修改,就一定會反映在檔案修改日期上。

事實不然,例如這次的網站故障,最後查出來是 AppPool 身分對設定檔的 NTFS 讀取權限被移除造成的,而該設定檔的最後修改日期與去年上線時間吻合,差點被當成不在場證明讓犯人逍遙法外。

為求加深印象,我做了以下實驗:證明修改 NTFS 權限及檔案更名都不會改變檔案的最後修改時間(一直是下午 09:04:23)。

補充:除了修改權限、檔案更名不會改變檔案修改日期,檔案修改日期可透過 Windows API 修改,而複製或解壓縮覆寫檔案時也可能被置換成來源檔的修改時間。故在判斷檔案是否有異動時,不宜單依修改日期做判斷。


Coding4Fun–網頁遙控可動式樹莓派相機

$
0
0

前陣子入手 3D 印表機,從網路下載現成模型幫老古董 Raspberry Pi B+ 印了外個殼,一時懷舊之心大發,翻出舊零件拼裝了一台可轉動鏡頭角度的網路照相機,還騷包寫了網頁版控制介面,摸到一大票新東西,筆記留念一下。

影片

Raspberry Pi 的硬體 IO 介面不如 Arduino 豐富,能控制伺服馬達的 PWM 輸出只有一組,想控制多個伺服馬達通常會外接 I2C 介面控制板,前陣子玩 Arduino時入手一塊 PCA9685 16 路舵機控制板還沒拆封,這回正好派上用場。(下圖前方的長方形藍色電路板就是)

PCA9685 控制板用 4 條線連上 Raspberry Pi,二者使用 I2C 協定溝通,讓 RPi 可控制多達 16 個伺服馬達。而攝影鏡頭旋轉座只需兩個,一個水平旋轉,一個控制仰角。下圖的黑色 2 軸塑膠雲台也是之前買的,現在雖然又多了自己 3D 列印選項,但規格已標準化的東西,大量生產的成品還是遠比自幹省時省力也省錢。

Raspberry Pi 的程式開發資源以 Python/C 為大宗,Pi B+ 沒法玩 .NET Core,就用 Python 吧! 反正這年頭什麼語言都可以用 Visual Studio Code 寫。 XD

照著範例改寫出一支 servo.py(註:Python 有個奇妙特色,縮排是有意義的不能隨興亂調,寫慣 C#/JavaScript 有點不適應 ),藉由 python survo.py x y控制水平/垂直伺服馬達角度,拍照則用 raspistill -o photo.jpg –w 640 –h 480,不過 Raspberry Pi 拍照超慢,拍一張照片耗時 5 秒,調參數可縮短曝光時間,但照片亮度會嚴重偏暗。無論如何,到這裡一台由命令列控制的可轉鏡頭照相機算是完成了。但,身為前端攻城師,沒提供網頁操作介面像話嗎? 於是,我又踏上另一段奇幻旅程...

前面說過我的樹莓派是古老的 B+ 版,最新版 ASP.NET Core 不是選項,回頭花時間鑽研 Mono 投資效益不佳。要在 Pi 寫網頁,Apapche 與 PHP 有現成的套件可裝,一個 sudo apt-get  install … 指令就順利搞定,那就用 PHP 來寫網站好了。

爬文惡補 PHP,透過 URL 控制伺服馬達及拍照是靠底下這一小段程式搞定,程式接收參數並呼叫外部程式,透過 Python 程式旋轉鐘頭,使用 raspistill 拍照。

<html><?php
$h = intval($_GET['h']); //200-600, mid 400
$v = intval($_GET['v']); //100-500, mid 350
if ($_GET['m'] == "R") {
    $h = 400;
    $v = 350;
}

if ($h > 0 || $v > 0) {
    if ($h == 0) 
        $h = -1;
    else {
        $h = max($h, 200);
        $h = min($h, 600);
    }
    if ($v == 0) {
        $v = -1;
    }
    else { 
        $v = max($v, 150);
        $v = min($v, 500);
    }
    $cmd = sprintf("sudo python /home/pi/python/servo/Adafruit_Python_PCA9685/examples/runservo.py %d %d", $h, $v);
    $result = exec($cmd, $output);
}
if ($_GET['m'] == 'S')
    exec('sudo raspistill -w 640 -h 480 -o /home/pi/php/webcam/photo/current.jpg -n -ex auto -awb auto');
?></html>

在陌生程式領域裡探險,資深老鳥重溫菜鳥心境,意外有個感想:

避免寫出不安全的程式絕對是必要,但對菜鳥還真是一項考驗。

依據多年的開發經驗,我一眼看出裡面隱藏了資安風險:一個是將使用者輸入內容轉為呼叫外部程式參數有指令碼注入風險,二則是開放網頁程式以管理者身分執行,一旦程式有漏洞,損害程度加劇。

前者倒不難處理,只需強制將參數轉成數字,即能社絕偷渡 Shell 指令。至於後者,較好的做法是另寫一個本機服務,透過 IPC / Socket 接收指令做事,如此網站使用低權限執行身分即可。這種中間服務如在 Windows / C# 我是信手拈來,如今身處 Raspberry Pi 環境就難如登天了,只能先接受這種可行但不安全的解決方案。實務上,應該有不少的安全漏洞也在類似情境下誕生,更何況,有更多情況是開發者壓根沒意識某些做法或寫法是不安全的... (尤其是無任何開發經驗的正港菜鳥) 

PHP 搞定後,前端我用 SPA 來做,總算又回到我熟悉的領域,不過我決定用 Vue.js 當成練習,強迫自己跟它培養感情。

我在 Raspiberry Pi 設定 Samba 分享程式相關資料夾,Python、PHP、Vue.js 都是在 Windows 10 用 Visaul Studio Code 連上網路磁碟機修改,開發體驗還不錯。

影片部分,我又順手練習簡單後製還假掰配上 YouTuber 經典 BGM,RPi 相機的速度太慢,不過拿來拍縮時影片應該OK,至於監控用途,另外要走 Streaming 模式,下回再研究。總結是一次綜合格鬥練習,用上了 Ubuntu、GPIO、I2C、Python、PHP、Vue.js、影片後製... 呵。

謝謝收看。

【參考資料】

VS2017 開啟專案找不到 System.Net.Http 參照

$
0
0

某個使用 Microsoft.AspNet.WebApi.SelfHost 4.0.20918 NuGet Package 的 Console Application 專案使用 VS2017 開啟時出現 System.Net.Http 及 System.Net.Http.WebRequest 參照失效,改用 VS2015 開啟則無此問題。

VS2017 開啟失敗但 VS2015 正常的狀況之前遇過(VS2017 無法載入 MVC4 專案),但這回發生在 Console Appliation 專案,狀況不同。

優先檢查導致 NuGet 參照失敗的頭號戰犯 - .csproj 的 HintPath 設定 - 發現 System.Net.Http 的 HintPath 有正確指向 packages 目錄:

    <Reference Include="System.Net.Http, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
      <HintPath>..\packages\Microsoft.Net.Http.2.0.20710.0\lib\net40\System.Net.Http.dll</HintPath>
    </Reference>

而 packages\Microsoft.Net.Http.2.0.20710.0\lib\net40\System.Net.Http.dll 確實存在,但我留意到有另一個目錄 net45,底下有個 _._ 空白檔案。

爬文學到新知識,_._ 檔案是用來確保 net45 資料夾在 ZIP 壓縮時不會被略過,而沒放 DLL 檔的 net45 資料夾代表 Package 相容於 .NET 4.5,但組件已內建於 GAC 不需額外複製安裝:

This is important because the existence of an "empty" net46 folder means that the package supports .NET Framework 4.6, but does not require any assemblies (DLLs) in order to run on that version of .NET. This is most likely because the implementation of the package is in the GAC.

專案為 .NET 4.5.2,故應改用 .NET 4.5+ 內建的 System.Net.Http.dll,換句話說,是參照 GAC 的 System.Net.Http 出了問題。回想專案升級 4.5.2 是很久以前的事,而升級後曾用 VS2017 編譯過,何以忽然不行?

鎖定關鍵字查到兩篇相關討論:

得到結論:這是 VS2017 15.5.x 的 Bug (我的 VS2017 版本是 15.5.7),將會在 VS2017 15.5.8 修正(目前進入 Preview 階段)。在修正釋出前,最簡單的 Workaround 是將 .csproj <Reference Include="System.Net.Http"> 及 <Reference Include="System.Net.Http.WebRequest"> 的 HintPath 刪除,即可藥到病除。

閒聊 - RWD、React Native、Xamarin、Cordova,一魚兩吃到底行不行?

$
0
0

RWD、React Native、Xamarin/Cordova,一魚兩吃到底行不行?

前陣子 Airbnb 發表了系列文章,訴說其在傾力投入兩年之後忍痛放棄 React Native 的心路歷程,來自資深用戶的親身心得特別有參考價值。原文為英文長篇,Oursky (一家創立於香港的Web / Mobile 產品開發工作室) 佛心整理了中文摘要: Airbnb: 們一起寫過的 React Native

很巧,前陣子剛好也被問了幾次:網站該用 RWD 還是做成大小網?用 Xamarin / Cordova 寫 App OK嗎?聽起來是個 FAQ,寫篇文章整理我的想法。

RWD、Xamarin、Cordova,連同 React Native / Vue Native,問題本質相同,這些技術都強調開發一次同時滿足桌機及行動裝置平台,聽起來很美好,而提問者想問的是「一魚兩吃解決方案到底行不行?」

RWD 想實現的是寫一套網頁,PC、手機與平板都能看;Xamarin 強調用 .NET 開發背景一次囊括 UWP、Android、iOS App 開發;React Native / Vue Native 則是主打重複利用 JavaScript 打造網站跟 App。終極目標都在強調節省開發成本,而天下沒有白吃的午餐,節省開發成本的同時,犧牲掉一部分使用者體驗的最佳化。

這類一魚兩吃方案猶如瑞士刀,小小一把可權充鋸子剪刀起子開罐器,但就只是「權充」,其操作體驗及順手度絕對比不上正統的鋸子剪刀起子開罐器。要一魚多吃,勢必在體驗最佳化上有所折衷將就。

但程式跟實體工具有很大一點不同,軟體不受物理空間限制,你很難將二十種不同規格的起子塞進一把瑞士刀,但在程式上是可能的。因此,軟體追求最佳化到極致就是針對各不同情境各寫一套,再塞進同一支程式裡,但如此已失去重複利用開發結果的初衷;要回到初衷,得從各式情境的設計異中求同,找出可共用部分。當情境一多,設計愈複雜,要想出可跨情境共用的模組、元件的難度就愈高,架構複雜度也跟著上升。到頭來,還不如不求共用,針對不同情境各寫一套來得單純,開發成本還比較低。

因此,我認為 RWD、React Native、Xarmarin/Cordova 等解決方案值不值得採用,能不能省成本,決策曲線會如下圖。

當需求單純,使用者隨和肯將就,一魚兩吃方案可節省大量開發成本;一旦需求變得複雜刁鑽,使用者龜毛,要維持開發核心共用又要滿足規格的難度便會急劇上升,終究出現交叉 - 一魚兩吃為了同時滿足兩種平台絞盡腦汁,還不如各寫一套來得單純省事。

不管 RWD、React Native、Xamarin,這個成本交叉原則都相同,差別在各技術議題與應用情境下的斜率、曲線形式不盡相同,值不值得採用得依個案而論。但至少有一點可以銘記在心,當聽到有人鼔吹 RWD、React Native、Xamarin/Cordova 這類一魚兩吃方案講得天花亂墜之際,別傻傻相信世上真有銀彈(Silver Bullet)能一勞永逸解決任何問題,任何技術決策都是取捨,該不該買單請依實際狀況評估決定。

閒聊 - Web API 是否一定要 RESTful?

$
0
0

傳說 C 語言風格(C#/Java/JavaScript…)程序員依其信仰分為兩大派,自古以來不共戴天:

依我的觀點,寫 WebAPI 的程序員也分成兩派,RESTful 派跟非 REST 派。我屬於後者,是非主流的少數派。

前幾天跟同事聊到 Web API 是否一定要 RESTful,三言兩語說不清,寫篇文章梳理思緒好了。

RESTful API 是指實踐 REST Representational State Transfer精神的 API 設計風格,其核心精神在於借用 HTTP 協定做為基礎,讓 API 規格簡單一致,大致有以下特色 :

  • 透過 URI 指定要存取或操作的資源
  • 可使用 QueryString,但只應拿來傳遞額外過濾條件或參數,不應包含識別資源的鍵值
  • 使用 HTTP 方法 POST、GET、PUT、DELETE 對應到建立、讀取、更新、刪除等動作。
    也有人主張 PUT 是 Relace (Create 或 Update),另外增加 PATCH 用於部分更新( Partial Update )
  • 透過 Accept Header 指明可接收的內容格式,例如:XML 或是 JSON
  • 伺服器透過 HTTP 狀態碼回傳執行結果,例如:200 成功、401 存取被拒、404 找不到資源、500 伺服器錯誤

而 REST 概念的提出者 Roy Fielding是 HTTP 規範的主要作者及 Apache HTTP Server 專案的發起人之一,這也是讓 RESTful API 風格備受推崇的原因之一。

延伸閱讀

採用 RESTful API 最大的好處是風格統一,API 名稱簡潔(不會冒出一堆 QueryThese、UpdateThat、DeleteBlah,動詞隱藏在 HTTP Method ),靠直覺及經驗就能快速上手;除錯時也可由 URL、HTTP 方法及傳回狀態直接解析各項操作的意義及結果。多年來,RESTful 設計已是 Web API  設計的主流,例如:ASP.NET MVC Web API 即是走 RESTful 風格,當在專案新增繼承 ApiController 的 API 類別,預設需實作 Get()、Post()、Put()、Delete() 方法以對映 HTTP GET、POST、PUT、DELETE 等動作。(註:當然,你也可以額外定義路由或使用 [Route("actionName")] 與 [HttpPost] Attribute 加入自訂方法,但要小心濫用會違反 RESTful 精神) ApiController 實做範例可參考:建置使用 ASP.NET Web API 的 RESTful Api - Microsoft Docs

RESFful API 是當今主流,伴隨而來的好處是相關資源豐富(Visual Studio 直接支援,還有自動為 REST API 產生說明文件及測試程式的 Swagger等),無疑是好物,但不幸地,我對它沒有愛。

實做過幾次 ,我最後選擇回歸使用一般的 ASP.NET MVC Action 實做 Web API,不使用 MVC 提供的 ApiController 機制。身為 KISS (Keep It Simple, Stupid) 法則的忠實信徒,說穿只有一項考量 – 簡單!

貫徹 RESTful 精神是件麻煩事,導致在設計 API 介面時會受到諸多限制。舉一個最簡單的例子:我想要刪除四本書,BookId 分別為 9,5,2,7,若要求依循 RESTful 精神,做法就蠻分歧的:

  1. 跑迴圈 DELETE /books/9, DELETE /books/5, DELETE /books/2, DELETE /books/7 (絕對符合 RESTful,但有點蠢...)
  2. 先透過 POST /books/selections 將 9,5,2,7 四本書打包,賦與唯一資源代碼,例如 /books/pack32767,再 DELETE /books/pack32767 參考
  3. Amazon S3 REST 的做法是 POST /?delete,傳入包含要刪除項目識別碼的XML
  4. Facebook Graph API、Parse Server REST API、Google Drive REST API 則採用將多個 DELETE 作業打包成 JSON 放在一個 Request 裡送出,在伺服器收到後再解開一一執行。 參考
  5. 也有人主張 DELETE /books/9,5,2,7 之類的做法,但如此有沒有違背 REST 精神? 我不知道

除此之外,像是混合多個異種資源的更新 URI 該怎麼取?無明確資源對象的作業 URI 該用誰? 用 MVC Action 實做只要取個能望文生義的 Action 名稱,定義好傳入參數及傳回結果就可搞定,一旦被要求符合 RESTful,難度瞬間上升,發生次數多了,RESTful 帶來的優勢是否足以彌補額外增加的成本?到頭來有可能早已無關優劣利弊,流於「當然要 RESTful,不然別人會以為我們不懂」。

而另一方面,要配合 RESTful API,JavaScript 呼叫時也變得較複雜,需要自訂 HTTP Method,解析 HTTP Status Code,雖不是大事,但不能用最簡單的 $.get()、$.post() 搞定,測試偵錯變得麻煩些 。

至於使用 HTTP Status Code 302/401/404/500 傳遞狀態,以 .NET WebClient.UploadData() 呼叫時將被視為 Exception,需要 try ... catch 攔截,會增加些許困援。

上述提到種種問題,其實都能有解,不然 RESTful API 如何能走到今天? 回到前面提到刪除多本書的例子,我總覺得原本單純可用 MVC Action DeleteBooks(string[] bookIds) 就能搞定的事,為了符合 RESTful 得大顯神通,有違 KISS 精神。

基於以上考量,設計 Web API 時我習慣寫成一般 ASP.NET MVC 方法而不用 ApiController,並一律限定 POST (多少降低一些 XSS 風險,參考:隱含殺機的GET式AJAX資料更新 ),執行結果無論成功失敗都傳回統一的 ApiResult 型別件:

    /// <summary>
    /// API呼叫時,傳回的統一物件
    /// </summary>
    public class ApiResult
    {
        /// <summary>
        /// 執行成功與否
        /// </summary>
        public bool Succ { get; set; }
        /// <summary>
        /// 結果代碼(0000=成功,其餘為錯誤代號)
        /// </summary>
        public string Code { get; set; }
        /// <summary>
        /// 錯誤訊息
        /// </summary>
        public string Message { get; set; }
        /// <summary>
        /// 資料時間
        /// </summary>
        public DateTime DataTime  { get; set; }
        /// <summary>
        /// 資料本體
        /// </summary>
        public object Data { get; set; }

        public ApiResult()
        {
        }

        /// <summary>
        /// 建立成功結果
        /// </summary>
        /// <param name="data"></param>
        public ApiResult(object data)
        {
            Code = "0000";
            Succ = true;
            DataTime = DateTime.Now;
            Data = data;
        }

        /// <summary>
        /// 建立失敗結果
        /// </summary>
        /// <param name="code"></param>
        /// <param name="message"></param>
        public ApiResult(string code, string message)
        {
            Code = code;
            Succ = false;
            this.DataTime = DateTime.Now;
            Data = null;
            Message = message;
        }
    }

傳回型別統一,前則可撰寫共用 AJAX 呼叫函式,先統一處理錯誤再將資料拋回原呼叫端,錯誤代碼依系統別統一管理,在前後端產生對應。這樣的做法實際跑過幾過多個專案,沒有遇到什麼大問題,看來是可行的。(API 說明文件及測試程式我是自幹程式產生器搞定,必須承認不能套用現成工具額外多花了工夫,但量身訂做的西裝格外合身,整體上仍屬值得)

總體來看,我偏好用 ASP.NET MVC 寫 Web API 不走 RESTful ApiController,在這個議題上,應該會繼續非主流下去。

ASP.NET Core 練功筆記 1

$
0
0

也差不多該開始玩 ASP.NET Core 了。最近剛好有個適合練功的題材,拿了 ASP.NET Core + Vue.js 上場演練,將一路上參考到的資源及瑣碎心得理成筆記備忘。

  1. 關於 ASP.NET Core,MVP John Wu 有一系列 IT 鐵人文,是新手上路很不錯的參考:
    [鐵人賽 Day01] ASP.NET Core 2 系列 - 從頭開始 - John Wu's Blog
  2. 如果你習慣 IDE 開發不喜歡下指令,那麼 Visual Studio 仍是開發 ASP.NET Core 的首選,有可能從建立專案、寫程式到測試都不必動用命令列視窗。不過如果你看中的是 .NET Core 跨平台優勢,那麼學習使用 dotnet 命令列指令是必修課程。
    用命令工具建專案沒想像中複雜,例如以下幾個步驟就能建好一個可執行的空白 ASP.NET Core 網站:(要建立包含 MVC Controllers/Views 基本結構專案的話,則是用 dotnet new mvc)
  3. 免費且跨平台的 Visual Studio Code(簡稱VSCode),有愈來愈流行的趨勢,尤其涉及前端開發時,VSCode 已是主流,網路參考資源豐富超多,如果想學習進階的前端開發,無可避免得將 VSCode 列為必修課程。
  4. 簡短試用比較過 VSCode 跟 Visual Studio 2017,二者是不同量級的開發工具,各有其專長。
    VS2017 對 ASP.NET Core 的支援十分完整(記得要更新到最新 Update),具備像是 Add View、Go To View等便捷功能,跟開發 ASP.NET MVC 4/5 的體驗差不多。
    VSCode 則較像一個開發環境框架,支援程度由所安裝的擴充套件決定,想補足平日慣用的 VS2017 功能,一方面需要花時間尋找組裝,另一方面有些功能還沒有現成的擴充套件可用,故開發體驗不如 VS2017。但在 JavaScript / TypeScript 方面,VSCode 支援的完整度與時效性,以及網路參考資源的豐富程度則遠勝 VS2017。
    最後我琢磨出來較順手的做法是 - 用 VSCode 及 VS2017 開啟同一個專案,寫 MVC/C# 端時用 VS2017,寫到 TypeScript 再切換成 VSCode,各取其優勢。
  5. ASP.NET Core 的專案結構跟 ASP.NET 大不相同,已經看不到 Global.asax / web.config 這些東西,取而代之的是改用 Program.cs 起始,以 OWIN方式設定網站執行細節。也因此,ASP.NET Core 完全擺脫對 IIS 的依賴,可靠內建的 Kestrel 伺服器單獨執行,也可以搭配 IIS、Ngix、Appach 等 Reverse Proxy 伺服器對外提供服務,借重 Reverse Proxy 滿足安全防護、負載平衡、快取上的較高規格要求。
    ASP.NET Core 中的網頁伺服器實作 - Microsoft Docs
  6. 原本想在 ASP.NET Core 使用 SQLite + Dapper,不幸目前 SQLite ADO.NET 套件還不支援 Core 版本,倒是在 EntityFramework 相關支援已經齊全。順勢調整架構,也練習一下 EF 吧!
    -NET Core 使用者入門 - 新資料庫 - EF Core - Microsoft Docs
  7. .NET Core 預設不再使用 app.config 或 web.config 設定檔,取而代之是多樣化的設定保存選項,像是 INI/JSON/XML/環境變數/記憶體。我個人偏好 JSON,做法可參考 ASP.NET Core 的設定 - Microsoft Docs

未完待續...

ASP.NET Core 練習筆記 2 – Ubuntu + SQLite + Dapper

$
0
0

繼續 ASP.NET Core 專案練習,本階段的戰術目標:嘗試在 Linux Ubuntu 16.04 上跑 .NET Core + SQLite + Dapper。

  • Ubuntu 遠端桌面設定
    Ubuntu xrdp 支援使用 Windows 的「連線遠端桌面」程式(RDP Client)登入桌面環境,還可在主機螢幕操作桌面之外另開一個虛擬獨立桌面環境。
    但有個問題是 Ubuntu 13.10 之後 xrdp 不再支援系統預設的 Gnome 和 Unity 桌面,大部分使用者只好改用精緻度與功能較差的 xfac4 代替,網路上有配合 tightvncserver 繞道的解法(參考:xrdp完美实现Windows远程访问Ubuntu 16.04 - 法号阿兴 - 博客园),但我試不出來。最後決定改走遙控鍵盤滑鼠的路,用 RealVNC Viewer連接 Ubuntu 內建的桌面分享功能順利搞定。(參考:How to Remote Access to Ubuntu 16.04 from Windows - UbuntuHandbook)。
    從 Windows 使用 RealVNC Viewer 連接 Ubuntu 會遇到以下安全錯誤,Workaround 是使用 "sudo gsettings set org.gnome.Vino remote-access false" 關閉 Vino 加密:

  • Ubuntu Samba 伺服器設定 
    程式主要還是會用 Windows VS2017 / VSCode 開發,我習慣在 Ubuntu 執行跑 Samba Server 分享目錄, 在 Windows 寫好程式透過網路磁碟機部署檔案,再使用 Putty 或遠端桌面測試,這是我找到較順暢的整合方式。
    參考:在 Ubuntu 11.10 架設 Samba Server 及windows 7上的設定 @ 永˙宗˙看˙視˙界 -- 痞客邦 --
  • 在 Ubuntu 16.04 安裝 .NET SDK
    這部分較簡單,官方文章寫得很詳細。在 -NET Tutorial - Hello World in 10 minutes Tutorial Guid / Linux / Install the .NET SDK / 選好作業系統版本,依據說明步驟操作即可。

    wget -q https://packages.microsoft.com/config/ubuntu/16.04/packages-microsoft-prod.deb
    sudo dpkg -i packages-microsoft-prod.deb
    sudo apt-get install apt-transport-https
    sudo apt-get update
    sudo apt-get install dotnet-sdk-2.1
  • System.Data.SQLite 支援問題
    NuGet 上的 System.Data.Sqlite Package 執行時需要一個與平台相依的 Unmanaged Sqlite.Interop.dll 才能運作,而它只支援 net20/40/45/451/46,Sqlite.Interop.dll 分 x86/x64 只支援 Windows,故加入 .NET Core 專案編譯時將出現警告:'System.Data.SQLite.Core 1.0.108' was restored using '.NETFramework,Version=v4.6.1' instead of the project target framework '.NETCoreApp,Version=v2.1'. This package may not be fully compatible with your project.
    下場是程式在 Windows 測試正常,移到 Ubuntu 執行將出現錯誤: 
    Exception: System.DllNotFoundException: Unable to load shared library 'SQLite.Interop.dll' or one of its dependencies. In order to help diagnose loading problems, consider setting the LD_DEBUG environment variable: libSQLite.Interop.dll: cannot open shared object file: No such file or directory
       at System.Data.SQLite.UnsafeNativeMethods.sqlite3_config_none(SQLiteConfigOpsEnum op)
       at System.Data.SQLite.SQLite3.StaticIsInitialized()
       at System.Data.SQLite.SQLiteLog.Initialize()
       at System.Data.SQLite.SQLiteConnection..ctor(String connectionString, Boolean parseViaFramework)
       at SqliteDapper.Program.InitSQLiteDb() in /home/jeffrey/Labs/dotnet/SqliteDapper/Program.cs:line 51
       at SqliteDapper.Program.Main(String[] args) in /home/jeffrey/Labs/dotnet/SqliteDapper/Program.cs:line 17

    爬文找到編譯 Linux 專屬 libSQLite.Interop.so 取代 SQLite.Interop.dll 的做法:Using System.Data.SQLite under Linux and Mono - Wezeku,但似乎只適用於 Mono,幾經嘗試都不成功。
  • Microsoft.EntityFrameworkCore.Sqlite 突圍
    發現配合 EF 使用的 Microsoft.Data.Sqlite.Core NuGet Package 可在 Ubuntu 執行,其中也實作了 IDbConnection (SqliteConnection),讓我燃起一絲希望,但實測自己建立 Microsoft.Data.Sqlite.SqliteConnection 配合 Dapper 執行 .Query() / .Execute() 時都會彈出錯誤:
    You need to call SQLitePCL.raw.SetProvider().  If you are using a bundle package, this is done by calling SQLitePCL.Batteries.Init().
    靈機一動,先建立 EF DbContext(),再由 DbContext.GetDbConnection() 產生連線物件,搭配 Dapper .Query()/.Execute() 居然就成功了! 高興到差點從椅子上跳起來歡呼,總算沒枉費我跟它奮戰了大半個週末~

在 Ubuntu 跑 SQLite + Dapper 實測成功,算是攻上了小山頭,繼續推進。

小技巧 - 運用 Form target 模擬 AJAX 表單傳送效果

$
0
0

先定義我所謂的「AJAX 表單傳送」:意指撰寫 JavaScript 蒐集 HTML 表單欄位,再藉由 XHR 傳送到伺服器端,取代傳統的 Postback。其優點包含送單時畫面不閃動、沒有重新載入網頁的延遲以及傳輸運算成本、表單處理失敗使用者輸入狀態不變方便修改重送... 等等,近年來 AJAX 表單已成網頁操作設計主流。相較之下,傳統 Postback 方式(指用<form action="…">配合<input type="submit">或<button>按鈕送單的做法)雖然使用者體驗略遜一籌,但寫法簡單,在一些要求不高,殺雞不必用牛刀的場合,仍是省時省力的好選擇。

另一種我會繼續使用 Postback 的情境發生於修現有網站的 Bug,總不能看不習慣就怒把 Postback 表單都翻寫成 AJAX 版,叫你割痔瘡卻搞成切盲腸,母湯喔母湯~

依據實務經驗,Postback 表單有個棘手問題是送單出錯時得恢復欄位原本輸入狀態以便使用者修正重試,使用 ASP.NET WebForm 的 ViewState 可以解決部分問題,但遇到有 JavaScript 參與互動時,要完整還原的難度便提高許多。我有時會使出 Postback 時傳回 <script>alert(msg);history.go(-1);</script> 退回前一頁的技巧,大部部情況下瀏覽器會神奇地保留原本的輸入內容,但遇到由 JavaScript 控制的部分還是很容易破功。舉個例了:

假設網頁有兩個<intput type="text">,一個手動輸入,另一個由 JavaScript 填入:

<html><body><form action="post1.aspx" method="post"><input type="text" name="userId" value="" /><input type="text" name="seed" value="" /><button>Submit Form</button></form><script>
document.querySelector("input[name=seed]").value = Math.random();</script></body></html>

伺服器模擬送單出錯,傳回一段 JavaScript,alert 錯誤訊息後用 history.go(-1) 退回上一頁:

<%@Page Language="C#"%><script runat="server">
void Page_Load(object sender, EventArgs e)
{
	Response.Write("<script>alert('Something wrong!');history.go(-1);</" + "script>");
	Response.End();
}</script>

如以下展示,退回上一頁時,手動輸入的內容還在,但亂數欄位已重新產生。

如果不想改寫 $.post() 抓欄位呼叫 post1.aspx,其實還有一招取巧做法 - 利用 <form> 的 target 將傳回結果嵌入 <iframe>,傳回結果以 JavaScript 呼叫 parent 的函式或直接修改 parent 的 DOM。如此,輸入頁面的所有欄位狀態不因送出表單而變動,能簡單實現類似 AJAX 送單的效果。

做法是在頁面放置一個 <iframe>,取名 name="result",並透過 style="display:none" 隱形,而 <form> 則加上 target="result" 指定將 Postback 結果顯示於 <iframe>:

<html><body><iframe name="result" style="display:none"></iframe><form action="post2.aspx" target="result" method="post"><input type="text" name="userId" value="" /><input type="text" name="seed" value="" /><button>Submit Form</button><div id="msg" style="color:red"></div></form><script>
document.querySelector("input[name=seed").value = Math.random();
function showMessage(msg) {
	document.querySelector("#msg").innerHTML = msg;
}</script></body></html>

Post.aspx 小做修改,傳回結果改傳一小段 JavaScript 程式,將呼叫 parent 預先寫好的函式顯示執行結果:

<%@Page Language="C#"%><script runat="server">
void Page_Load(object sender, EventArgs e)
{
	Response.Write("<script>parent.showMessage('Something wrong! - " + Guid.NewGuid() + "');</" 
		+ "script>");
	Response.End();
}</script>

如下所示,不必大費周章就能輕鬆用 Postback 實現類似 AJAX 送單的效果囉!


程式範例:byte[] 不落地壓縮 ZIP 檔

$
0
0

.NET 4.5 起加入 ZipArchive、ZipFile 等列類別,自此不用額外安裝第三方程式庫就能製作 ZIP 檔。微軟官方文件則有篇範例文章,操作說明:壓縮與解壓縮檔案 - Microsoft Docs,介紹如何使用 System.IO.Compression 的一系列類別壓縮及解壓縮檔案。

我遇到一個需求,要將使用者在網站查詢的結果,以一筆資料一個檔案形式匯出,再集結壓縮成單一 ZIP 檔方便使用者下載。爬文找到的範例多以檔案形式處理為主,而我想省掉將資料寫成檔案再壓縮的步驟,但直接將記憶體 byte[] 壓成 ZIP(也是 byte[])的完整範例不多,索性將摸索成果整理成筆記:如何將記憶體中的 byte[] 直接壓成 ZIP 保存於記憶體?如此,直接 Response.BinaryWrite() 即可下載,全程資料不落地(不寫暫存檔),有利減少IO、提升效能。(註:前題是處理資料量不構成伺服器記憶體的壓力)

完整程式範例如下:

    class Program
    {
        static void Main(string[] args)
        {
            var src = new Dictionary<string, byte[]>()
            {
                ["name.txt"] = Encoding.UTF8.GetBytes("Jeffrey"),
                ["score.txt"] = Encoding.UTF8.GetBytes("32767")
            };
            var zip = ZipHelper.ZipData(src);
            System.IO.File.WriteAllBytes("test.zip", zip);
            var res = ZipHelper.UnzipData(zip);
            foreach (var fileName in res.Keys)
            {
                Console.WriteLine($"FileName={fileName}");
                Console.WriteLine($"Content={Encoding.UTF8.GetString(res[fileName])}");
            }
            Console.Read();
        }
    }

    public class ZipHelper
    {
        public static byte[] ZipData(Dictionary<string, byte[]> data)
        {
            using (var zipStream = new MemoryStream())
            {
                using (var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Update))
                {
                    foreach (var fileName in data.Keys)
                    {
                        var entry = zipArchive.CreateEntry(fileName);
                        using (var entryStream = entry.Open())
                        {
                            var buff = data[fileName];
                            entryStream.Write(buff, 0, buff.Length);
                        }
                    }
                }
                return zipStream.ToArray();
            }
        }

        public static Dictionary<string, byte[]> UnzipData(byte[] zip)
        {
            var dict = new Dictionary<string, byte[]>();
            using (var msZip = new MemoryStream(zip))
            {
                using (var archive = new ZipArchive(msZip, ZipArchiveMode.Read))
                {
                    archive.Entries.ToList().ForEach(entry =>
                    {
                        //e.FullName可取得完整路徑
                        if (string.IsNullOrEmpty(entry.Name)) return;
                        using (var entryStream = entry.Open())
                        {
                            using (var msEntry = new MemoryStream())
                            {
                                entryStream.CopyTo(msEntry);
                                dict.Add(entry.Name, msEntry.ToArray());
                            }
                        }
                    });
                }
            }
            return dict;
        }
    }

實測將 Dictionary<string, byte[]> 壓成 ZIP,解壓後內容還原無誤:

將壓縮結果另存 ZIP 檔,使用 7-Zip 可正常檢視解壓,測試成功!

前端小筆記-Progressive Web App (PWA)

$
0
0

抓了開源專案 MiniBlog.Core 回來玩,想在其中套用 Form target 模擬 AJAX 表單傳送技巧時踢到鐵板,開啟 F12 偵錯工具後驚呼:天吶,這世界又變了!

新時代的 Postback 不再是單純送出一個 Request 拿回 HTTP 200,而是像這樣子:

如上圖,表單送出行為被拆成三個動作,並出現關鍵字 ServiceWorker。

另外,我發現 js、css 也變成由 ServiceWorker 載入。

爬文得知原來是專案引用了 WebEssentials.AspNetCore.PWA套件(參考:Introduction to PWA in ASP.NET Core Application – Beginner's Guide to Mobile Web Development ),MiniBlog.Core 恰巧就是 WebEssentials 作者的大作,吃自己的狗食是一定要滴。

事到如今,只好花點時間了解什麼是 PWA - Progress Web Application。

以下是我參考的資料:

歸納重點如下:

  1. PWA 是 Google 2015 提出的概念,希望讓 Web Application 可以在各種網路狀況、不同手機 OS 下均能順利運作,並可被安裝到桌面、離線使用、支援推播... 等。
  2. 設計要點:
    * Progress - 漸進式,運行環境支援度愈高提供的服務愈多,在簡陋環境亦可優雅降級提供基本功能。
    * Responsivve - RWD,自動適應各種螢幕尺寸
    * App-like - 模仿 Native App 風格及資料更新方式(Service Worker、快取)
    * Fresh – 使用 Service Worker API 自動更新(不依賴App Store/Google Play)
    * Safe – 全面 HTTPS
    * Discoverable - 透過 manifest 進行 SEO
    * Re-engageable - 透過推播與使用者互動
    * Installable – 可 Add To Home 將 Web App 裝到手機桌面,並可列於應用程式清單,不需(也不能)經由 App 商店下載。
    * Linkable - 可透過 URL 分享
  3. Service Worker 是 PWA 可以離線使用的關鍵,Service Worker 在瀏覽器背後運行,有自己的生命週期,與網頁獨立。
  4. SPA 存在首次載入過慢、JS 過大及不利 SEO 優化等問題,PWA 則可克服這些問題。
  5. PWA 將不常變動內容(App Shell)與動態內容(Content)分開,AppShell 下載後將 Content 透過 Service Worker 儲存在本地資料庫作為 Cache,即使網路中斷仍可繼續使用。而使用者進入網頁後馬上看到完整 Shell,之後再填入內容,相較於畫面空白一陣子再一次顯示,即使等待時間相近,也會有效降低使用者的煩躁感。
  6. PWA 講的是整個網站的設計哲學,網站當然不可能加一個 NuGet 套件就變 PWA。WebEssentials.AspNetCore.PWA 套件主要提供:強制轉 HTTPS、Web App Manifest(讓網站可被加成手機桌面 App)、Service Worker JS 等三項功能。
  7. Service Worker 在 localhost 以外的網站限定 HTTPS 才能啟用,啟用後會接管原本的 JS/CSS/Form 等 HTTP Request 傳送。 目前除了 IE 全系列、iOS 11.2-(含)、Opera Mini 外,主流瀏覽器都已支援(參考:Can I use Service Worker... Support tables for HTML5, CSS3, etc)。
  8. 使用 Chrome F12 可以查看 Serivce Worker 啟用狀態:


    實測 Unregister Service Worker 後重整網頁,JS/CSS 等即恢後使用傳統方式接收。(網頁載入後會再次 Register)

  9. 一般要使用 Service Worker,需攔截 install, activate, fetch 等事件加入處理邏輯,WebEssentials.AspNetCore.PWA 透過 ServiceWorkerTagHelperComponent在 <body> 尾端注入 <script>'serviceWorker'in navigator&&navigator.serviceWorker.register('/serviceworker')</script> 加載 /serviceworker,其中已實作好 Service Worker 相關事件:

又見識了新東西,呼~ (到底還有多少新東西要學呢?)

ASP.NET Core 值得學嗎?

$
0
0

連發了幾篇 ASP.NET Core 文章,果不其然接到各方詢問:

喵的媽呀,微軟又推新東西了?」
「WebForm 玩完了嗎?」
「我 ASP.NET MVC 還沒開始玩耶,是不是不用學了?」

先簡單答覆以上疑問:

是的,ASP.NET Core是下一代的 ASP.NET,能跨平台執行,預期是未來的主流。它是兩年前推出的新東西沒錯,但做資訊這行一天到晚學新東西剛好而已好嗎?你要是體驗過前端框架「放煙火式的生命週期」,這根本不算什麼。

至於 WebForm,再戰十年或二十年應該不是問題。大型企業或組織求穩重於求新,系統愈大愈複雜,革新速度愈慢,但可預期也不會再挹注資源擴大發展,相關工作機會註定愈來愈少,由於不再有新鮮肝投入這塊領域,將演變成留守老鳥們靠寫很快或領很少或娶了老闆女兒角逐稀有維護職缺的場面。(補充參考:丞相,起風了!從ASP.NET 5的變革談起)

如果你正要或正在學 ASP.NET MVC 5,請繼續學好學滿,相關知識技巧在 ASP.NET Core 絕大部分都能沿用。企業若無強烈的跨平台需求,ASP.NET MVC 5 的 Windows 及第三方程式庫支援較完整成熟,穩定性及技術資源勝過仍在起步的 ASP.NET Core,仍是現階段開發網站的好選擇(依據官方文件,ASP.NET 與 ASP.NET Core 為可替換選項,開發人員可視自身需求擇一使用),請安服用。

回到正題,如果 ASP.NET MVC 5 仍是現役主力,那 ASP.NET Core 值學習嗎? 看我最近寫了不少 ASP.NET Core 筆記,想當然爾是投贊成票的,個人觀點如下:

跨平台優勢

二十幾年的 Coding 人生,C# 是我用過最成熟最順手的程式語言,搭配地表最強的 Visual Studio IDE,簡直削鐵如泥。可惜早年它被封印在 Windows 裡,錯失與 Java 競爭主流開發語言霸主的先機,直到 .NET Core 終於正式跨平台,雖然晚了十幾年,但總算讓我等到了。
跨平台有什麼好處? 有選擇就是爽!


依據 Netcraft 的統計,2018 七月全球前 100 萬網站使用的網站伺服器 Apache 佔 35.2%,nginx 佔 24.9%(市佔持續擴大中),Microsoft 佔 9.4 %。各家作業系統、網站伺服器的成本、效能、穩定性、管理方便性各有優劣,各有愛好者。身為網站開發人員,ASP.NET Core 跟 Apache、Nginx、IIS 都能搭,甚至丟到 NAS Docker 跑也成,不必為了網站主機限制跟客戶戰作業系統戰伺服器,光想到嘴角就上揚。 (謎:是以前有多常被打搶?) 
想像一下,跟錙銖必較的老闆報告網站用 Linux 主機、 VPS 或 Cloud 就能跑,租金省一半,老闆開心你加薪。

效能優勢

ASP.NET Core 採用輕巧的 Kestrel Web Server 處理核心 HTTP 通訊(甚至可抽換成更效能取向的伺服器以適應極端情境),一般會配合 Nginx、Apache、IIS 等反向代理伺服器(Reverse Proxy Server)補足安全、負載平衡、靜態內容快取、壓縮、HTTP 認證等需求。ASP.NET 受限於 IIS,功能豐富但較笨重,在一些評測(12)中 ASP.NET Core 的效能數字(RPS,Request Per Second)至少嬴過 ASP.NET on IIS 3-4 倍。
當然純用 Kestrel 對比 IIS,多少帶有「徒手跑步 vs 武裝跑步」相比的差偏,實際情境 Kestrel 搭配反向代理伺服器後差距應會縮小一些,但不可否認,當你不計代價極想擠出效能時,ASP.NET Core 更能超越巔峰。

趨勢 趨勢 趨勢

ASP.NET MVC 5 仍是檯面上的主流選項,但若無意外未來 ASP.NET Core 將是王道。舉兩條線索:


ASP.NET 5 is dead - Introducing ASP.NET Core 1.0 and .NET Core 1.0 - Scott Hanselman
ASP.NET MVC 5 的下一代不是 ASP.NET MVC 6,而是 ASP.NET Core 1.0,非常令人困惑的命名,但 Scott 他們盡力了。ASP.NET MVC NuGet Package目前最新版本為 5.2.6;而 ASP.NET Core 這兩年從 1.0 躍升到 2.1,3.0 預計在今年下半年釋出預覽並於 2019 推出正式版,處於急速抽高的青春期。

比較 EF Core 與 EF6 - Microsoft Docs
官方文件提到 EF6 仍是受支援的產品,未來仍會看到 Bug 修正及小幅改善。EF Core 的 API 與 EF6 相近,但核心已重寫故未繼承 EF6 所有功能,成熟度也不及 EF6,但未來將會加入一些 EF6 沒有的新功能(替代鍵、批次更新、LINQ 查詢混用用戶端及資料庫端運算) 。

由此推論,微軟仍會繼續支援 ASP.NET / EF6,但新功能將會在 ASP.NET Core 跟 EF Core 出現。

Open Source 萬歲

.NET Core / ASP.NET Core 完全開源,開發社群的每一份子都可以回報問題、提供建議、協助修 Bug、新增功能,讓平台更貼近自己的需求。即便意見最終未被接受,還有一招大絕,那裡用不爽就改到爽,你功力的極限的就是系統功能與效能的極限 :P (呃,這樣以後不能跟老闆說「這是平台限制沒辦法了」... Orz)
 

結論

小結我的看法:如果你未來五到十年還打算靠 ASP.NET 吃飯,ASP.NET MVC 一定要學,寫 WebForm 工作機會將變得很少,具備 MVC 技能才有本錢跟年輕小夥子們搶飯碗,很高比例的 MVC 知識搬到 ASP.NET Core 仍受用,毫不猶豫投資下去就對了。至於 ASP.NET Core,我個人認為雖然已經 3.0 在即,但其穩定性及成熟度尚待更多實戰驗證,還有第三方元件支援度尚未完全跟上來的問題,是進行大規模商轉前要考量的風險,但時間會消除這些疑慮。我建議現在就可提早接觸,試著在小型新專案上練習,應是不錯的入水角度。現在累積實力,待未來市場接受度變高,對於提供競爭力大有助益,不妨提早投資。

程式範例 - 使用 C# 寄送圖文並茂郵件

$
0
0

在 Outlook 寫信時,直接在文字穿插圖片是再自然也不過的事(如下圖),但是用 C# 程式走 SMTP 寄信,夾帶附檔的經驗很多,直接在內文內嵌圖檔倒是沒試過。

很快在 Stackoverflow 查到範例,照方煎藥,就寄出像上面圖文並茂的信件了:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mail;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;

namespace EmbImgMail
{
    class Program
    {
        static void Main(string[] args)
        {
            var mail = new MailMessage();
            mail.IsBodyHtml = true;

            //建立連結資源
            var res = new LinkedResource("netcore3.png");
            res.ContentId = Guid.NewGuid().ToString();
            //使用<img src="cid:..."方式引用內嵌圖片
            var htmlBody = $@"<div>.NET Core 3 架構圖如下:</div><div><img src='cid:{res.ContentId}'/></div>";
            //建立AlternativeView
            var altView = AlternateView.CreateAlternateViewFromString(
                htmlBody, null, MediaTypeNames.Text.Html);
            //將圖檔資源加入AlternativeView
            altView.LinkedResources.Add(res);
            //將AlternativeView加入MailMessage
            mail.AlternateViews.Add(altView);
            //設定寄件人收件人主旨
            mail.To.Add("jeffrey@mail.com");
            mail.From = new MailAddress("jeffrey@mail.com");
            mail.Subject = "內嵌圖檔測試";
            //送出郵件
            SmtpClient smtp = new SmtpClient("relayServerIp");
            smtp.Send(mail);
        }
    }
}

補充,AlternativeView 源自 RFC2046規範,理論上主流郵件軟體及網路信箱應該都支援。

突破 32 位元 .NET 程式 2GB 記憶體上限

$
0
0

同事分享了一記讓 32 位元 .NET 程式突破 2GB 記憶體上限的密技,讓我不禁獻上了膝蓋,當然要轉分享一下。

.NET 編譯成 32 位元與 64 位元最大的差異在於可用記憶體上限,32 位元的記憶體定址上限為 4GB,其中 2GB 配置給作業系統核心模式,應用程式為使用者模式只有 2GB 可用,實際執行需再扣除 Runtime 本身耗用的記憶體,依經驗只能用到 1.6GB 左右。所以若無特殊限制,程式最好編譯成 AnyCPU 或 x64 以充分享用記憶體。但實務上 .NET 程式一旦引用了 32 位元 Unmanaged 元件,就毫無選擇只能以 32 位元執行。

Windows 有個 /3GB 啟動參數,可調整只配置 1GB 給核心模式,留下 3GB 給應用程式使用,但 /3GB 的設定步驟繁瑣,要部署大量客戶端很有麻煩。Visual C++ 有個 EDITBIN 命令列工具,可修改 OBJ/DLL/EXE 檔案旗標,其中有個 /LARGEADDRESSAWARE 參數可針對特定 EXE 開放 3GB 模式,突破 1.6GB 上限。

以下是個簡單的測試程式,透過不斷產生 1M 長度字串消耗記憶體直接 OutOfMemoryException,並用以前介紹過的記憶體用量觀察函式測量佔用的 Managed Heap 記憶體:

using System;
using System.Collections.Generic;

namespace _32BitAppTest
{
    public class Program
    {
        static void Main(string[] args)
        {
            try
            {
                CreateBigData();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString()); 
            }
            Console.WriteLine("Press any key for exit... ");
            Console.ReadKey();
        }

        static void DumpMemSize(int count)
        {
            //強制回收記憶體清出空間,以充分利用所有記憶體 
            var memSz = GC.GetTotalMemory(true) / 1024 / 1024;
            Console.WriteLine(
                $"Managed Heap={memSz}MB, Count={count}");
        }

        static void CreateBigData()
        {
            var dic = new Dictionary<long, string>();
            var src = new string(' ', 1024 * 1024 - 1);
            for (int i = 0; i < 4096; i++)
            {
                dic.Add(i, src + (i % 10));
                if (i % 32 == 0)
                     DumpMemSize(i);                
            }
        }
    }
}

實測結果,大約用到 1.5GB 左右出現 OutOfMemoryException,跟一般認知的 2GB 上限相近。

用 EDITBIN /LARGEADDRESSAWARE 32BitAppTest.exe 開光後,同一支 32 位元 .NET程式便能吃到 3GB 記憶體,神奇吧!

如果要每次編譯後自動修改,可加在專案 Post-Build Event,以下是適用 VS2015/VS2017 的寫法:

使用以下腳本則可適用多個 VS 版本:參考

IF  EXIST  "%VS140COMNTOOLS%"  CALL  "%VS140COMNTOOLS%vsvars32.bat"
IF  EXIST  "%VS120COMNTOOLS%"  CALL  "%VS120COMNTOOLS%vsvars32.bat"
IF  EXIST  "%VS110COMNTOOLS%"  CALL  "%VS110COMNTOOLS%vsvars32.bat"
IF  EXIST  "%VS100COMNTOOLS%"  CALL  "%VS100COMNTOOLS%vsvars32.bat"
editbin.exe /LARGEADDRESSAWARE $(TargetPath)

補充,EDITBIN 為 Visual C++ 附屬工具,Visual Studio 記得要安裝 Visual C++ 才有的用。

【同場加映】

C/C++ Build Tools 有另一件工具 - DUMPBIN,可檢查 EXE 是否已設定 LARGEADDRESSAWARE 旗標,如下圖:

Viewing all 428 articles
Browse latest View live