目前用起來什麼都好,就是在匯出 PDF 時會有中文變亂碼的問題;我翻了一下官方論壇,發現相關的討論被記錄於 2020 年;既然這個問題在短期內無法被修復的話,身為工程師又只好自己下場啦!
首先看一下 Demo 用的工作項目,在系統中是可以正常顯示繁體中文的。
接著使用系統提供的匯出 PDF 功能。
得到了一份除了檔名外,內容面目全非的 PDF。
沒啥好說的,原始碼直接拿起來啃,找到處理 PDF (view.rb) 的地方就完成一半了。
可以發現原來 PDF 的處裡使用了知名的套件 Prawn。
1 | class WorkPackage::PDFExport::View |
在產生 PDF 時,OpenProject 為 Prawn 設置了 Noto Sans 字型。
1 | def document |
1 | def register_fonts!(document) |
問題的根源已經出現了,由於 NotoSans 無法顯示中文導致了亂碼,而解法也很簡單粗暴,它缺什麼就給它什麼,把繁中字型給它送過去。
1 | def register_fonts!(document) |
而 Prawn 其實對這種字型異常是有處理的,你可以自行選擇要取代或是把 NotoSansTC 變成它發生異常時的後備字型。
1 | def fallback_fonts |
最後再匯出一次,就可以看到正常顯示的 PDF。
研究過程中有發現到官方的無奈,據了解,目前還沒有一種字型可以支持世界上的所有語言,如果真的要做到支持全語系,那專案勢必會肥上許多。
我也因此產生了「每次在更新 OpenProject 後,都需要做一次上述修改」的困境;沒錯,這篇文章寫了那麼多,其實只是要提醒自己沒事不要亂更新而已!
]]>一款好的 RSS 閱讀器基本上可以解決這些問題,市面上的閱讀產品也已經非常成熟,但總有例外的時候,如果我們喜歡的資訊來源沒有提供 RSS 時該怎麼辦呢?網路上有許多開源的專案,讓大家自己架設 RSS Server,使用網路爬蟲的方式將指定內容轉換成 RSS 的格式,這樣就做到了所有東西都可以使用 RSS 閱讀的功能。
RSS Reader + Server 看起來是一套很理想的解決方案,但無法避免的也提高了使用門檻,畢竟可想而知有多少人會為了 RSS 而自己架 Server,但如果能夠在手機上完成網路爬蟲功能是不是就好一點了呢?稍微 Google 了一下沒有看到類似的產品,所以就來自己開發一個吧。
畢竟對網頁程式設計比較熟悉,最終還是選擇了以混合型開發框架來快速寫 APP,下面列出幾個比較重要的第三方技術。
使用者可以設定簡單的 Request 參數(Method、URL、Header)。
程式會抓取 Response body 並顯示於畫面中。
除了 DOM 外,還支援抓取 JSON 並高亮顯示。
可以使用 querySelector 對 DOM 或 JSON 中的元素進行搜尋,並取得該元素的 innerText、innerHTML、attribute 放入對應的 RSS 欄位中。
定義完爬蟲後,程式會根據規則把抓取資料形成列表。
這個 APP 還額外的解決了我長久以來的問題,每次開發完爬蟲都要煩惱該怎麼檢視那些爬出來的資料,現在統統在手機上就可以搞定,而這也真正做到了意義上的打造個人閱讀器
,畢竟看什麼資料都是由使用者決定,對這個 APP 感興趣的朋友可以去 RSSAny - 蜘蛛閱讀器 下載,雖然可能有很多 BUG 就是了。
學習心得就是簡單好用,尤其是結合 Chrome 的 Headless 功能,簡直讓我驚為天人。
那到底 Puppeteer 要做什麼事呢?首先把要模擬的行為先簡單列出來,仔細一看原來也就一項而已。
User | Puppeteer |
---|---|
開啟某個實況主頻道 | 開啟某個 Url |
觀看 | ❌ |
而這樣的一支程式已經有人寫好了,專案名稱就叫 Twitch-watcher,這個專案讓我學習到很多東西,實在是非常感謝該作者。
我這邊要做的,就是以現有的 Twitch-watcher 加入自己的需求進行客製化,以下列出這次的目標:
使用 readline 重新設計使用者操作流程,並加入 help(幫助)、list(列出追隨頻道)、watch(開始觀看指定頻道)、state(查詢觀看狀態)、leave(離開指定頻道)、exit(離開程式)。
1 | const readline = require("readline"); |
1 | rl.on("line", async (input) => { |
因為原專案是列出 Valorant 的相關頻道,所以只要修改取得頻道的網址和 Query 就 OK 了。
1 | const streamersUrl = |
配合前面的 watch 指令重新設計了一個方法(watch)來接收頻道資料,每個觀看任務都有獨立的 Browser 和 Page,初始化完成後開始觀看任務(loopWatch)。
1 | switch (command) { |
1 | async function watch(streamer) { |
每小時重整一次頁面,當偵測到頻道離線時,停止任務並關閉瀏覽器(killStream)。
1 | async function loopWatch(streamer) { |
偵測領取按鈕,每秒執行一次,直到任務結束;每成功領取一次則計數器(claim)++。
1 | async function claimPoint(streamer) { |
任務開始時紀錄初始數值,每次查詢(state)時計算相差多少,簡直不要太簡單。
1 | async function state(token, streamer) { |
這次額外學習到的新工具 asciinema,可以非常輕量的錄製 terminal 執行過程並發布成影片,但在 Windows 上只能用 PowerSession 就是了(我自己測試有亂碼問題)。
版本為 1.1.0 → 3.1.0,過程一切正常。
1 | npm i hexo-cli -g |
Hexo 與專案相依的 Lib 可以直接透過npm-upgrade
來找出最新版本,安裝執行後它會更新package.json
,再使用npm update
把它們全部升級。
1 | npm install -g npm-upgrade |
1 | gulp-htmlclean ^2.7.15 → ^2.7.22 |
更新完後實際執行,看一下效能比較,升級果然有差,速度已經快了不少。
Before
After
官方有提供升級指南,先將最新版本 Clone 下來。
1 | git clone https://github.com/theme-next/hexo-theme-next themes/next-reloaded |
再將 Hexo 設定檔_config.yml
內的 theme 改為next-reloaded
。
1 | # Extensions |
最後,將新舊版的 Next 設定檔進行 Diff 比較,參考舊的進行配置;下圖可以看到,兩個檔案的差異看似很多,其實只是多了一些細節設定,整體來說大同小異。
在這次的升級過程中,最大的意外就是沒發生任何意外,明明是間隔兩年、跨越多個版本的升級,真是不得不佩服這些專案的維護團隊。
我有個相簿功能,進入畫面時會從 Storage 中取出照片的檔案路徑列表(file://),並用 Ion-img 延遲載入顯示,但重複多次這個流程可能會造成 Ionic 崩潰重整頁面;下圖在第四次進入相簿時崩潰。
同樣是相簿功能,如果在網頁上想做圖片的拖曳、雙擊/雙點放大縮小…等特效,使用 Canvas 可能是一種選擇,但和 Ion-img 一起使用時卻會發生 Canvas 不正常閃爍的問題;下圖在放大的過程中閃爍。
原本的 Calendar Componet 長成下圖的樣子,預設顯示目前月份,可以點擊按鈕,切換上、下個月的視圖。
計畫是把該功能拆分成兩個 Component,最後再組合起來,功能分別如下
操作情境為 scroll-calendar 顯示目前,與近幾個月的日曆,當卷軸往上或往下時,自動長出後續的 month-view;下圖用 IOS 的日曆來解釋。
展示用的專案架構。
將原本的 calendar 改成 month-view,控制顯示月份的month
參數加上@Input
裝飾器,使其可以接收到 scroll-calendar 傳入的月份。
1 | export class MonthViewComponent implements OnInit { |
為了顯示目前月份與前後 n 個月,在這裡定義目前為current
,n 為buffer
,組出這個區間的資料後,用迴圈顯示 month-view。
1 | export class ScrollCalendarPage implements OnInit { |
1 | <ion-header> |
這時應該要能正確顯示正負四個月的 month-view。
可以注意到一開始的卷軸會在最上方,而不是當前月份的位置(二月),若要做到比較好的效果,可以在迴圈時添加 month-view 元件的id
,並配合 content 的 scrollToPoint,將卷軸移至指定 month-view。
1 | <ion-content #content> |
1 | export class ScrollCalendarPage implements OnInit { |
要做到無限滾動卷軸也很簡單,對迴圈的資料來源months
做點變動就行了;這邊監聽卷軸事件,當卷軸往下滾時,若 scrollTop 高於目前月份(month-view)的offsetTop
,則對 months 插入最後一筆資料,同時,為了避免頁面內容過於肥大,也刪除第一筆資料;卷軸往上也做相對應的處理。
1 | <ion-content |
1 | export class ScrollCalendarPage implements OnInit { |
可以看到這個頁面永遠只會顯示buffer * 2 + 1
個 month-view,一個簡單的垂直滾動日曆就這樣完成了。
整個環境大致如下圖。
假設有多個站台,發生錯誤時將資訊記錄到DB,而要重作畫面的話就必須重寫存取這些資料的邏輯以及畫面,也就是圖片右半部的WebApi與React View,而這部分我們可以直接使用Vsiaul Studio提供的.Net Core React Project來完成。
這裡用EntityFramework來做到簡易的串接DB,跟上面架構圖比較不同的是,因為我自己維護的錯誤日誌散落在不同架構的DB內(SqlServer、Postgres),所以這裡多了一層Factory來幫助我建立不同DB的Context。
1 | public class ElmahDbContextFactory : IDesignTimeDbContextFactory<ElmahDbContext> |
接下來實作查詢錯誤日誌列表的Api,這裡可以用剛剛的Factory產生Context讀取錯誤日誌,除了能搜尋ErrorMessage之外,我還想要有分頁功能,所以Model中加入了一些必要參數。
1 | public class ReceiveGetLogsModel |
1 | public class ResponseGetLogsModel |
1 | [ ] |
這樣就簡單的完成該功能了,實際用Postman測試一下看是否運作正常。
Material-UI官方推薦了一些不錯的Theme,這裡也是直接選擇免費的Material Dashboard,下載完後把相關東西放進我們的.Net Core專案的ClientApp
目錄下,應該就可以正常執行了。
把專案站台啟動起來,應該可以看到Preview中的漂亮Dashboard,React專案中也提供了許多現成的View來教開發者如何使用它們提供的UI Component,稍微看一下後就可以開始修改和去除我們不需要的東西,大致結果如下圖。
前端的大致框架搞定後,接下來要把資料接回來並顯示在畫面上,這裡可以順道學習一下Redux,建立Store來處理這件事情。
state
1 | const requestGetLogs = 'REQUEST_GET_LOGS'; |
action
1 | export const actionCreators = { |
reducer
1 | export const reducer = (state, action) => { |
再將State和Dispatche綁到我們自己寫的View Component上。
1 | class Logs extends React.Component { |
資料就可以正常顯示了。
詳情頁我們也可以依樣畫葫蘆,這樣近似於原本Elmah的功能就完成了。
至於搜尋功能,包含API和畫面都是我們自己做的,未來擴充也相當方便。
所有學習目標應該都完成了,目前Angular、Vue、React通通都寫過,個人覺得React相較於前兩者真的是有比較難一點,而這個專案未來我也會抽時間繼續開發,希望有機會能幫到同為使用Elmah的大家。
]]>你畫我猜
功能吧。我想畫畫
,機器人回覆相關繪畫模式按紐,點擊按鈕後開啟我們的 LIFF APP。基本有很多開源的套件,我目前是使用這一款angular-canvas-painter,套用的方式也很簡單,大致上如下。
1 | <div pw-canvas options="{width: 400, height: 300, color: '#ff0'}"></div> |
成功的後你的 Canvas 現在應該可以畫畫了,接著再加入送出的程式碼,由於我們取得的圖片是 Base64 字串,所以直接跟題目包成 JSON 送就好了,送出成功後呼叫liff.closeWindow()
關閉我們的 LIFF APP。
1 | var image = $scope.canvas.toDataURL(); |
畫面基本上隨你設計,看個人喜好,大致成果如下。
畫面完成後,就可以把頁面登錄到 LIFF 上了,這部分官網有詳細的教學,我這裡不多作介紹。
在我的設計中遊戲是有開始與結束的,那我們勢必要記錄這個狀態,除此之外圖片也要保存起來,我這裡偷了個懶,直接把圖片存到 DB 去,大家自己找地方放,反正能讀出來就行。
1 | var drawGuess = new DrawGuessTable() |
由於 LINE API 傳送圖片只收連結,所以必須實做一個 Route 來返回圖檔。
1 | public IActionResult View(int id) |
推播訊息由於要打 API,較快捷的方法也是用套件,我個人使用這套line-bot-sdk-dotnet,接著簡單的使用裡面的 Push 方法,並且傳入我們剛寫好的圖片連結。
1 | public void PushGameStart(string groupId, string message, string imgId) |
沒意外的話,文字訊息與圖片會出現至聊天群組中。
接著開始判斷 webhook 接收到的訊息是否等於答案,若為是,則該局遊戲結束並推播訊息。
1 | private void CheckAnswer(ILineEvent evt, DrawGuessTable drawGuess, string userName) |
群組中的使用者們在一番激烈的競猜後(?),系統公佈了最終勝利者。
程式完成後,這段時間群組中發生了許多爆笑的橋段,大家實在是太有創意了,哈哈。
這次使用到的技術相對簡單,感覺還可以用 WebSocket 作一些更有趣的東西,例如即時連線遊戲什麼的,下次有空再來玩看看。
]]>跨平台桌面應用程式
,而桌面應用程式的畫面設計一直是我頭疼的地方,如果這個問題可以用HTML+CSS解決那就太棒啦。在開始學習前一樣先給自己訂立個目標,這次就來寫個工具管理器,使用情境大致如下:
除此之外還想順便學習下面兩套工具,所以專案必須用到它們:
GitHub上已經有人把Electron與Vue包好了,我們就用它來開始專案吧,先照著下方步驟輸入CMD。
1 | # Install vue-cli and scaffold boilerplate |
接下來應該可以看到程式正常運作起來,成功之後就來寫程式吧。
首先要完成的是第一個需求,將檔案拖放至程式中,並顯示相關資訊。
我們需要新增一個可拖放的頁面用來取代目前的首頁,所以將其命名為HomePage
並撰寫拖放的dropFile
方法。
1 | export default { |
將template
的drop
繫結dropFile
方法,再用迴圈顯示出拖拉進來的檔案名稱。
1 | <template> |
最後把HomePage添加到router
內,並設為首頁。
1 | export default new Router({ |
可以看到下圖,程式已經可以讀取到我們拖放進入的檔案了。
再來開始第二項需求,讓使用者可以依靠點擊畫面開啟應用程式。
這裡要注意的是Electron的核心畢竟是瀏覽器,想要用瀏覽器開啟檔案明顯是不對勁的(可能會變成下載),所以應該透過Electron提供的模組來呼叫原生功能做到開啟檔案這件事情。
1 | openFile(file) { |
一樣要將openFile事件繫結到我們的畫面上。
1 | <p v-for="file in files" v-on:click="openFile(file)">{{ file.name }}</p> |
這時我們點擊擋案名稱就可以順利開啟該檔案了。
先前提到的Element-UI
是時候上場了,趕快來用它將畫面質感提升。
使用npm或yarn安裝Element。
1 | npm install element-ui --save |
在main.js
中寫入以下程式。
1 | import ElementUI from 'element-ui'; |
我想讓左邊變成分類,上方列出該分類頁的功能選項,右下方顯示工具列表,用圖片畫出Layout大致如下。
先將選單資料先定義出來。
1 | categories: [ |
並將資料顯示於畫面之中。
1 | <el-aside> |
這樣左側選單就完成了。
使用Electron的Card元件來重新製作畫面,過程中加入xs、sm、md…等屬性作簡單的RWD,讓視窗在改變大小時能有更好的檢視效果。
1 | <el-row :gutter="20"> |
接著使用Dropdown顯示打開檔案
、打開資料夾
、刪除捷徑
…等控制按鈕,並實作相關功能。
1 | <el-dropdown size="mini" split-button type="primary" trigger="click" @command="handleCommand($event, file)" @click="openFile(file)"> |
這樣捷徑列表也完成了。
Header只負責顯示目前的分類名稱與刪除該分類,先把畫面設計出來如下圖。
在刪除時可以使用Element提供的$message
功能來讓使用者做Confirm。
1 | this.$confirm('此操作會刪除該標籤與底下的捷徑, 是否繼續?', '提示', { |
Message的效果圖。
終於到了最後一個步驟,程式已經寫的差不多了,該來考慮如何發佈程式了。
在開發時期一直陪伴著我們的DevTool
當然不能讓使用者看到,這時就要進入src/main/index.js
修改視窗設定。
1 | mainWindow = new BrowserWindow({ |
由於預設是編譯成安裝檔,但我想大多數人都討厭安裝,所以我們就來把它改成單一執行檔的Portable
版本,該設定位於package.json
下。
1 | "win": { |
在CMD輸入以下指令。
1 | npm run build |
成功後可以看到以下訊息。
這個專案包含這篇文章大概寫了四天左右,總地來說,畫面的確是變好寫了,但不確定是否因為不熟Vue的關係,功能的完成也是非常不簡單,除此之外還發生了各種大大小小的問題,這時才深刻的感覺到,一直在用的VsCode
是真的厲害阿!雖然都是用Electron開發,但自己還差得太遠了,需要再好好努力。
SignalR
打造桌面版的彈幕接收器,快來為你的工作增添一絲活力吧。這份專案大致分為三份程式,分別是接收端、Server、發送端。
訊息發送端將訊息傳給 Server,Server 再傳給所有接收端,於是彈幕出現在所有接收端畫面上。
有時候在白底的畫面工作時,有彈幕飛過難免會看不清楚,所以特別設計了一些簡單的參數來修改文字樣式。
這部分我是參考Danmu-V2做的,對它的理解只到會動而已,稍作簡單的修改後,大致上可以如圖呼叫它,並把我們的參數傳給它。
1 | private void ShootDanmaku(string text) |
先安裝套件Microsoft.AspNet.SignalR.Client。
程式開啟時,建立 Server 連線,並註冊 Server 傳來的shootDanmu
事件。
1 | Connection = new HubConnection("http://xx.xx.xx.xx/"); |
確定連接上後,還可以在 Server 上加入一個 SignalR 的 Group(Server 那段會提到),方便 Server 區分 Client。
1 | Connection.Start().ContinueWith(task => |
剛好以前寫過的聊天室有用到 SignalR,只要把彈幕使用者用 Group 功能區分開來就 OK 了。使用情境是這樣,當彈幕接收端開啟時,會呼叫BilibiliJoin
方法,並將自己的 Id 添加進 Group。
1 | public void BilibiliJoin() |
Server 接收到訊息時,會將訊息轉傳到此 Group 中,並呼叫shootDanmu
方法。
1 | public void SyncMessage(string message) { |
隨便寫支程式,反正只要能呼叫到 Server 的SyncMessage
方法就成了,這裡不贅述。
最近從上一份工作離職了,但有了這支程式偶爾用來洗洗對方的畫面,感覺就好像我還在同事們身邊一樣呢,聊天室還不快刷一波 6666666666。
]]>低潮
之中,那該如何快速擺脫低潮期就變成了一個至關重要的問題,而身為一個程式設計師,就應該利用自己的優勢「程式設計」來解決問題,於是我就開發了這支低潮管理器
。低潮是沒有底線的,隨著時間的增長低潮也會越來越大,這個時候我們也會愈發的難以管理它,所以我寫了一個路徑遞迴搜尋低潮
功能,來做到低潮一鍵建檔,降低低潮的維護難度,理所當然地,第二次選擇相同路徑時,也會過濾掉已重複建檔的檔案,並挑出尚未建檔的資料進行匯入。
能夠清清楚楚地看到低潮與其相關資訊無疑可以幫助我們管理它,這裡依照自己的需求設計了三個分別為卡片
、列表
、混合型
的檢視模式,若是有長方形的封面還會幫你切出右半部方便預覽。
好的搜尋工具可以避免自己陷入重複的低潮中。理論上,低潮的檔名都有一個所謂的前綴,例如「ABCD
-001」、「ZZ
-987」,在匯入時挑出這些類別,並幫相關檔案進行分類,這麼做的好處除了可以方便做搜尋引擎,也能使自己更瞭解自己的低潮喜好與分佈狀況。
低潮發生時,偶爾會出現檔案名稱雜訊過多的問題,例如「[balabala](05-14)ABCD-001hd」之類的,發生這種問題時會不利於我們之後要做的線上取得低潮資訊
功能,所以我寫了一個小工具來幫助使用者挑出其關鍵字ABCD-001
,並將檔案重新命名。
在我的電腦會發生一種情況,當低潮的同個資料夾下有過多的多媒體檔案時,開啟此資料夾的速度會變得極慢,所以我也加入了自動檢測該檔案是否有建立同檔名資料夾的功能,並自動建立其上層資料夾,透過這個方式來解決問題。
其實這才是我做這支程式的主要目的。因為一定要打開檔案才能百分之百的知道低潮的內容物是什麼
這件問題深深的困擾著我,除了有時候會沒有封面外,封面與內容不符也是個重大的問題。所以取得該低潮資訊,如封面
、品名
、廠商
、參與者
、標籤
…等,無疑可以幫助使用者提高自己低潮的掌握度。
低潮是如此的博大精深,當然也無法做到盡善盡美,當無法從線上取得資訊時,就靠自己來建立吧。
找到自己想要的低潮後就可以嘗試解決問題啦。
真的很想放出這支程式來幫助人,但我在開發時使用了Devexpress的多個套件,而這套工具是所在公司購買的授權,發佈怕是會有違法嫌疑,或許等我有一天自己買了授權就可以公開啦。
]]>Windows Forms App
時遇到了一些小問題,於是就想到或許可以透過一些Dependency Injection
的特性來解決這些問題,於是有了這篇文章。之前在寫網頁時都是靠Autofac實現依賴注入,但在Windows Form又該如何做到勒?
我Google後試用了下面兩款套件:
兩款都可以簡單的做到DI,但最終我選擇了Ninject
,原因在於它相較於第一款更簡單(這點我不確定,也可能是我理解不夠)。
決定好之後就立刻用Nuget把它裝起來吧。
若有特別的需求時,可以在Ninject Module
進行相關設定與處理,下面我們就來實作它。
1 | public class DependencyModule : NinjectModule |
Kernel
可以自動尋找程式中的相依性進行注入並建立該物件,我們改用Kernal來建立Windows Form的起始程序Form1
。
1 | static void Main() |
Ninject到這裡基本上就設定完成了,下面我們就來看看它改變了什麼,並解決了些什麼問題吧。
如果你的程式有分層的話,那你可能也有這個問題,使用DI前我的建構子充斥著各種各樣的實作物件。
1 | public partial class Form1 : Form |
使用DI之後,只需要將ClassA加入建構子參數,其內的相依性建立都會自動完成,程式也容易閱讀了許多。
1 | public partial class Form1 : Form |
假設有一個處理資料的訊息紀錄框Form2
,像下圖。
一般我們由Form1呼叫出Form2,可能會使用下面的程式。
1 | public partial class Form1 : Form |
但如果Form3也要叫出Form2,並且Form2要保留被Form1呼叫時的狀態的話,程式會變得非常麻煩,必須由Form1將已建立的Form2傳給Form3。
1 | public partial class Form1 : Form |
這些都可以透過DI解決,只要在NinjectModule
中將Form2的被注入時的規則改成InSingletonScope
讓它一直被重複使用就行了。
1 | public class DependencyModule : NinjectModule |
其實就是個小工具而已,能夠讓開發者省略很多實作物件的步驟,非常推薦大家使用。
]]>我的那幾個同事在忙啥?
,這篇文章就來實作查詢同事在幹麻
功能,讓大家在互相幫助上無遠弗屆,增進彼此之間的情誼。同事們主要是使用Skype來傳遞訊息,最終目標是希望可以達成用傳送訊息的方式,完成關心對方的目的,並返回可以快速了解對方在幹麻的結果。
要串接Skype勢必要申請該服務,詳情這裡不介紹。
當Bot正常運作後,要讓Server能夠即時的查詢各個同事在幹麻,我只想到在大家的電腦都裝上個程式來提供呼叫,在呼叫之前,我們必須讓接收到的訊息可以區分出各個同事,並且建立各個同事的相對應IP。
發送指令給各個同事的電腦,就選用Tcp吧。
1 | var tcpClient = new TcpClient(); |
接收命令當然也如上面使用Tcp。
1 | var listener = new TcpListener(IPAddress.Any, 8787); |
要如何簡單有效的知道對方在幹麻呢?或許直接回傳螢幕截圖可以達到目的。
1 | var bounds = Screen.GetBounds(Point.Empty); |
將接收到的stream轉為byte。
1 | int totalrecbytes = 0; |
這時當有人在聊天室詢問某人在幹麻時,接收端就會收到訊息。
並在聊天室顯示該名同事的桌面。
對方關閉的話也會提示相關訊息。
哇~同事在幹麻真的一覽無遺了呢!但後來他們都不開,我寫這麼辛苦,真的hen心寒。
]]>只要使用上一篇文章提到的SignalR
,偵測連接事件,這樣就可以即時顯示連線中的使用者。
前些時候為了方便文章截圖,我還特別建置了一個測試環境來避免洩漏個資,現在直接用Css
寫了模糊效果,並用JS
開關來保護機敏資料;轉念一想,偶爾開啟這個功能,讓聊天室的使用者不知道彼此是誰,這樣聊起來也是別有一番趣味。
輸入的對話若出現了連結,總不可能叫使用者自己複製去瀏覽器貼來看。所以就用正規式
把連結挑出來,然後使用<a href=""></a>
替換原文,這樣連結就好讀、好點多了對吧。
隨著對話紀錄往下增長,捲軸也會面臨幾個問題,譬如說正在往上觀看聊天紀錄的人不應該因為新的紀錄而被至底,只有正在至底的人才會被新紀錄至底…等等,實際上做起來並不是那麼容易,但好在找到了一個外掛解決了這個問題「angularjs-scroll-gule」。
使用者當然不會保持聊天室的視窗至頂,那有新訊息時就用「Chrome Notifications Api」來通知使用者吧,這樣就可以簡單做到螢幕右下角彈出訊息視窗了。
聊天室必不可少的東西,可以有效幫助氣氛更活絡、語氣更生動,還好也是有套件「EmojiOne Area」可以快速使用,相似的套件我挑了很久,還是最喜歡這款。
都說笑容會感染,雖然不知文字有沒有同樣的效果,但或多或少應該可以修飾一下語氣吧?
檔案上傳真的是不太好做,但如果只是讓使用者貼個圖片連結那還是可以的。
同傳送圖片。
簡單的連線遊戲也可以使用iFrame+SignalR做到。
同圈圈叉叉。
到底產生了什麼,真的不方BANG多縮,應同仁要求加入了點擊解除模糊的事件。
暫時只做了這些,還有很多改進空間,嗯…只能繼續努力。
]]>SignalR
這個東西,總算是解決了多年前的一個遺憾。其實我也還沒有很理解「SignalR」的原理,官方的介紹有看沒有很懂,但還是嘗試在這裡記錄下自己觀察到的東西。
必須在Client端中實作HubProxy,它的作用是跟Server端中的Hub進行持續連線,Server端也由此獲得了主動通知Client端的能力,並且多個Client端之間可以藉由Hub廣播的特性傳遞資料或執行Client端的方法。
使用者A、使用者B,分別進入了聊天室,並都與後端進行了持續連線;當使用者A傳送了訊息”Hello”後,Hub會找到目前正在連線中的使用者A、B,並廣播該訊息”Hello”顯示於畫面上。
可以先看看「官方教學」中的Tutorial,然後參考下面步驟依樣畫葫蘆就可以簡單完成。
當Hub收到訊息後,必須找出所有連接中的使用者,並傳送此訊息,程式如下。
1 | public class ChatHub : Hub |
在網頁載入完成後,使用HubProxy並指定Hub網址,與其建立持續連線。
1 | $(function () { |
當使用者在聊天室輸入訊息,並按下Enter鍵後,就可透過前面建立的HubProxy傳送資料給Hub。
1 | $scope.chatInputKeyDown = function (event) { |
Hub廣播訊息後,HubProxy理所當然地也接收到了這些資料,這時就可以將之加入於訊息中,透過Data Binding的特性更新於畫面上。
1 | $.connection.chatHub.client.receiveMessage = function (json) { |
下圖為使用兩個視窗開啟聊天室,並於左方發送訊息同步至另一方,這樣就輕而易舉的完成訊息同步功能了。
SignalR到底有沒有解決效能問題?持續連線與廣播會不會給Server帶來更大的負擔?這些我都不知道,但目前已開發完成的聊天室運行狀況還算穩定。不管如何,這東西絕對值得一玩。
]]>DB紀錄訊息、Cache優化存取速度;原本我是這麼想的,但仔細思考後決定捨棄DB,因為那些存在於DB的訊息我不會再拿來利用,可以說是一點保存的必要都沒有,於是就決定把訊息短暫的紀錄在Cache就好。
但總會發生例外,萬一有天我必須要將對話紀錄查詢出來時該怎麼辦呢?這時突然就想起了Slack
,雖然免費的也有限制,卻也不失為一個優秀的中短期訊息保存方案。
必要的屬性有唯一識別碼、訊息內容、建立者、建立時間,寫成類別後如下。
1 | public class ChatsResponseModel |
因為Cache現在還承包了原本DB該做的事情,所以相關的邏輯也必須先撰寫好才行。
為了後續方便使用以及統一存取的處理方式,先把Read、Write的方法寫好,並暫訂訊息的保存時間為一天。
1 | public TEntity CacheRead<TEntity>(string cacheKey) where TEntity : class |
1 | public void CacheWrite<TEntity>(string cacheKey, TEntity cacheEntity) where TEntity : class |
結合剛剛提到的訊息類別,就可以簡單的使用Cache做訊息的讀寫了。
1 | public List<ChatsResponseModel> Read() { |
1 | public ChatsResponseModel Write(MessageRequestModel model, string userId, bool sce = false) { |
訊息讀寫既然已經處理完畢,那Api也能取得目前已有的訊息紀錄了,以下為Api的回傳結果。
1 | [ |
推薦看Slack Api,除了整體風格我很喜歡外,寫得也很清楚、測試也很好用,看完後直接使用Web Api,在聊天室接收到訊息時順便轉傳Slack作儲存之用。
撰寫一個方法,收聊天室之訊息與使用者名稱,並填入至Slack Api所需要的參數中。
1 | public void SlackPostMessage(string message, string name) |
下圖為同時開啟聊天室與Slack,已經可以看到Slack會幫聊天室備份訊息紀錄了。
聊天室已經完成存取訊息(Cache)和查詢紀錄(Slack)的功能了,少了DB設計起來就是快,雖然存在著一些致命的硬傷,但我不說😈;如果大家有更好的處理方式的話,不妨分享給我吧!
]]>本文基於Visual Studio 2015
與AspNet.Mvc5
撰寫,因為後來在2017建立範本專案時,找不到下面文章所提到的會員系統頁面了,並且各種後端程式語言的差異較大,還用到了一些框架提供的Scaffold,所以在這特別提一下。
要能夠讓使用者在聊天室中識別彼此的身分,會員制是不可或缺的。因為此專案的需求相對簡單,所以直接使用範本專案的ASP.NET Identity
會員機制就可以了,這樣也同時完成了註冊、登入、登出、忘記密碼…等麻煩的功能。
會員系統需要什麼資料表?這些我們可以統統不考慮,因為全部都在套件提供的IdentityDbContext
裡面,但之後我們也有可能會加入自己的資料表,所以必須自己實作一個DbContext來繼承IdentityDbContext。
1 | public class ApplicationDbContext : IdentityDbContext<ApplicationUser> |
接下來使用Code First
布署DB,就可以看到會員系統所需的相關資料表都建好了,理論上在這時,我們範本專案的會員系統頁面也可以正常運作了。
ASP.NET Identity雖然便利,但註冊時的預設密碼驗證卻相當嚴謹,居然需要大小寫、特殊符號、英數字、長度限制,這也導致了使用者註冊完後常常會忘記自己的密碼,如果大家不介意的話,就像我一樣把驗證拔光光吧。
1 | UserManager.PasswordValidator = new PasswordValidator |
重置密碼需要串接Email來寄送重置連結給使用者,使用者收信後再透過連結重置密碼;雖然我覺得這沒什麼問題,但同事們覺得麻煩,討論過後就改成了輸入Email與使用者名稱可直接導向重置連結,這也造成了被他人重置密碼的可能性,不過大家都是自己人,於是我也就這麼做了。
1 | public class ForgotPasswordViewModel |
1 | [ ] |
可以從下面的Gif中看到登入、註冊、登出的操作情境,畫面跟範本專案比起來當然是有稍微調整過的,這在後續章節才會提到。
聊天室所需要的會員系統就這樣完成了,雖然ASP.NET Identity還有很多的潛力可以發掘,但我這次用不到,就放到以後有機會再來研究吧!
]]>開始寫程式前當然是要先把開發環境建置好,讓LineBot可以順利連到自己的站台,這樣就事半功倍啦!
申請Line開發者帳號後,建立一個Messaging API
,未來不排除會使用到推送訊息功能,所以我這邊選擇開發者試用計畫。
將Webhook URL填入自己撰寫的Web API網址,開發時可填入本機IP,完成後推上Server再於此處修改(我這邊使用Heroku)。
設定允許機器人加入群組。
官方列出了各種語言的開發工具包,沒列在官方的也可以在Github上找到,像我自己平時是寫.NET,官方居然沒有列,可…可惡;回歸正傳,SDK大大減化了開發流程,依據本篇主題,這裡就使用pip把官方推薦的line-bot-sdk裝起來。
1 | $ pip install line-bot-sdk |
把SDK的使用說明看一看,要讓Bot與API溝通應該不是件難事,這裡讓Bot回傳使用者輸入的訊息後,下一步就可以正式開始寫邏輯啦!
思路大概是這樣子的,Bot預設是不回應請求的,當使用者輸入關鍵字後會啟動Bot,也可輸入關鍵字後關閉Bot,或是於五分鐘後Bot自動關閉。下圖畫的有點醜,大概明白就好。
因為後端需要判斷Bot所在的群組是否需要回覆訊息,以及目前Bot的狀態,所以這張表需要紀錄的欄位是Line的群組ID、過期時間、建立時間,程式碼大致如下。
1 | class GroupTicket(models.Model): |
我希望後端在接收到特定關鍵字時能夠有相對應的特殊處理,所以在這裡撰寫程式對機器人出來
為喚醒,機器人再見
為關閉的條件判斷,並新增或刪除上述的GroupTicket
狀態資料,若處在喚醒狀態時,才回傳訊息。
1 | if event.source.type == 'group' and event.message.text == '機器人出來': |
現在Bot的開關已經生效,不把它喚醒的話它就只會乖乖地安靜,這樣Bot是不是更好用了呢?
這次的學習Python計劃算是蠻成功的,除了兩篇文章內所提及的東西外還學了很多,例如APIAI
做語意分析、Migrate
佈署DB,除此之外還嘗試串接了公司內的員工打卡服務,點子雖然源源不絕,但奈何時間有限,就決定到此告一段落,Python 我們下次再見。
既然是從零開始,當然是從官方網站看起,先把該裝的東西裝起來,看一下基礎語法如何撰寫,做個如何架站的功課…等。
目前主流版本似乎是2與3,並且各自都有大量的擁護者,官方還列出了一份詳細的比較文件,但我只是來學習的,沒有興趣去搞懂為何會有這種分歧,索性就直接安裝最新版吧!(3.6.3)
除了官方網站外,同事還推薦了個學習網站「codecademy」,裡面有豐富的教學與測驗讓使用者快速學習一門程式語言,嗯…最終進度停在了9%,未來看有沒有機會把它補完,現階段我就先邊做邊學就好☺
找到了兩款不錯的Framework,分別是Django
、Flask
;Django快速開發、Flask輕量,都有各自的優點,最後我選擇了Django,原因是怕Flask太輕量讓我無法快速上手,Flask我們有緣下次再見😝
這裡我偷了一個懶,因為此服務預計會上Heroku
,然後又在Heroku看到了配置好的專案範本,於是就這麼用了,當然你也可以在「Django」自己從頭開始,差別應該是不大吧(我猜),Heroku範本如下。
1 | git clone https://github.com/heroku/python-getting-started.git |
大概列一下自己的學習順序:
不知道這篇文章會不會水分過多,因為我只記錄了過程中自己的大略思路與執行方向而已,但就我自己而言上手Python真的不難(有一點點程式底子),大家也可以來玩玩看,嗯…晚點再來寫Bot實作篇。
]]>本專案是還在開發中的App,功能大概是企業內部GPS打卡、提醒、查詢紀錄,這些功能理論上不影響本次實驗。
1 | cli packages: |
這裡使用版本2.4.2
,共會用到兩個Css,分別是AdminLTE.min.css
和_all-skins.min.css
,將它們加入網頁參考,要特別注意的是AdminLTE是相依於Bootstrap的,但Ionic與Bootstrap會激烈衝突,所以在這裡不用Bootstrap。
1 | <link href="assets/css/AdminLTE.min.css" rel="stylesheet"> |
美化時盡量使用Ionic Component
取代AdminLTE元件,配色選擇為skin-blue
。
1 | <body class="skin-blue"> |
1 | <ion-header> |
1 | .main-header .logo { |
1 | <ion-list class="main-sidebar"> |
1 | .main-sidebar { |
1 | .timeline { |
https://adminlte.io/themes/AdminLTE/pages/examples/login.html
1 | .content { |
過程中遇到很多Ionic與AdminLTE的Css衝突問題,目前只能遇到一個解一個,或許還有更好的套版方法?
]]>