顯示具有 html5 標籤的文章。 顯示所有文章
顯示具有 html5 標籤的文章。 顯示所有文章

2013年7月10日 星期三

[webapi]微軟 ASP.NET Web API Self Host 的應用(2)

(本篇程式是用 vb.net 寫成)

前一篇留下一個問題,若是要提供服務 .html, .js, .css, .jpg, .png, .gif ... 等等,我想到的辦法是自行撰寫程式來處理。在參考了[1] 的 route 用法,我在 config.Routes.MapHttpRoute 多加 4 個 route。img、css、js 分別提供 圖檔、css、js 三大類檔案的服務。而 default 則是提供第一個頁面的服務。

config.Routes.MapHttpRoute("img", "img/{name}", New With {.controller = "Img", .action = "g"})
config.Routes.MapHttpRoute("css", "css/{name}", New With {.controller = "Css", .action = "g"})
config.Routes.MapHttpRoute("js", "js/{name}", New With {.controller = "Js", .action = "g"})
config.Routes.MapHttpRoute("API", "{controller}/{action}/{id}", New With {.id = RouteParameter.Optional}) ' vs2010 just can not IntelliSense....
config.Routes.MapHttpRoute("default", "", New With {.controller = "Home", .action = "g"})

接下來則是加入 ImgController, CssController, JsController, HomeController 這四個 class

Public Class ImgController
    Inherits ApiController
    Dim accept_type As Dictionary(Of String, String) = New Dictionary(Of String, String) From {{".jpg", "jpg"}, {".png", "png"}, {".gif", "gif"}}
    <HttpGet()> Public Function g(ByVal name As String) As HttpResponseMessage
        Dim content As New StreamContent(New System.IO.FileStream(Request.RequestUri.LocalPath.Substring(1), IO.FileMode.Open))
        Dim ext As String = System.IO.Path.GetExtension(Request.RequestUri.LocalPath).ToLower()
        If accept_type.ContainsKey(ext) Then
            content.Headers.ContentType = New MediaTypeHeaderValue("image/" & ext)
        End If
        Dim response = New HttpResponseMessage() With {.Content = content}
        'response.Headers.CacheControl = New CacheControlHeaderValue() With {.MaxAge = New TimeSpan(1, 0, 0)}
        Return response
    End Function
End Class

Public Class CssController
    Inherits ApiController
    <HttpGet()> Public Function g(ByVal name As String) As HttpResponseMessage
        Dim content As New StreamContent(New System.IO.FileStream(Request.RequestUri.LocalPath.Substring(1), IO.FileMode.Open))
        content.Headers.ContentType = New MediaTypeHeaderValue("text/css")
        Dim response = New HttpResponseMessage() With {.Content = content}
        'response.Headers.CacheControl = New CacheControlHeaderValue() With {.MaxAge = New TimeSpan(1, 0, 0)}
        Return response
    End Function
End Class

Public Class JsController
    Inherits ApiController
    <HttpGet()> Public Function g(ByVal name As String) As HttpResponseMessage
        Dim content As New StreamContent(New System.IO.FileStream(Request.RequestUri.LocalPath.Substring(1), IO.FileMode.Open))
        content.Headers.ContentType = New MediaTypeHeaderValue("text/javascript")
        Dim response = New HttpResponseMessage() With {.Content = content}
        'response.Headers.CacheControl = New CacheControlHeaderValue() With {.MaxAge = New TimeSpan(1, 0, 0)}
        Return response
    End Function
End Class

Public Class HomeController
    Inherits ApiController
    <HttpGet()> Public Function g() As HttpResponseMessage

        Dim content As New StreamContent(New System.IO.FileStream("html/first.html", IO.FileMode.Open))
        content.Headers.ContentType = New MediaTypeHeaderValue("text/html")
        Dim response As New HttpResponseMessage() With {.Content = content}
        Return response
    End Function
End Class

 

接下來,就是把測試用的 first.html, .css, .js, .png 放到 html, css, js, img 目錄去。

以下是 first.html

<!doctype html>
<html>
<head></head>
<title>web browser</title>
<body>
<link href="css/site.css" rel="stylesheet" />
<script src="js/jquery.js" type="text/javascript" ></script>

<script type="text/javascript">
$(document).ready(function(){
//alert("jquery alert works")
});
</script>
<p>
this is html test
</p>
<p>
<input type="text" id="txtboxTest" />
</p>
<p>
<button id="btnTest">click me</button>
</p>
<script type="text/javascript"> $(document).ready(function(){
$('#btnTest').click(function(){
$('#txtboxTest').val('jquery click works!');
})
}); </script>
<p>
<img class="imgstyle" src="img/glyphicons-halflings.png" />
</p>
</body>
</html>

以下是 site.css

body { background-color: #0000ff }
.imgstyle {border:2px solid #00ffff;background-color: #ffffff}

而 jquery 及 png 圖檔還請自行準備。執行後,以 IE 來看。

image

下圖是用 chrome 來開。

image

這樣的程度,應該可以處理小小應用程式的需求,使用 SPA (Single Page Application) 的觀念來做,也就不需要多個 html 檔。若有其他需要,只需新增 route 及 controller 即可。

但是,這樣的程式,拿給我爸這種老人家,要先點一個執行檔,再開瀏覽器,然後輸入網址,他一定會嫌。在黑大的文章[2]底下有個留言說:

# GATTACA said on 04 June, 2013 10:44 PM
SignalR也有SelfHost可用啊,

ip過濾也可以只限自己127.0.0.1連線,

我現在所有單機版的操作介面都只剩下一個填滿的WebBrowser,啟動後再去讀Html檔案進來;

美工依照原本的WinForm排版Html順便美化,

程式設計師再去填SignalR JavaScript Client與Knockout ViewModel,

如果有用到COM元件,其callback事件處理後透過SignalR更新到Html上,也沒有背景執行緒更新主執行緒WinForm畫面的問題;

雖然是較極端的做法,但是可以擺脫微軟悲情的UI更迭(MFC->WinForm->WPF...),

避免程式設計師將業務邏輯綁死到UI元件上,

Html美工又很好找,

可說是利大於弊啊

對啊,這樣就會跟 app.js 一樣,看起來像是原生桌面程式有什麼不好的呢?

原來的程式執行起來長這樣,看來就想把它關掉。

image

現在,請在 Form1 加上一個 WebBrowser。然後在 httpserver.OpenAsync.Wait() 這一句的後面,加上

WebBrowser1.Navigate(http://localhost:32767)

在程式裡調好 Form1 寬度及寬度。接著在 WebBrowser1_DocumentTitleChanged 加上

Private Sub WebBrowser1_DocumentTitleChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles WebBrowser1.DocumentTitleChanged
    Me.Text = WebBrowser1.DocumentTitle
End Sub

執行程式就得到:

image

嗯,我現在感覺滿意耶。

我猜想這概念已經存在這麼久,.NET 的社群裡也許已經有類似的程式。

我這隻程式應該只是個幼稚園。

如果想要程式的話,我最近會整理放上 github 讓大家下載來用。

更新,程式在此:https://github.com/rickyteng/washapp

現在要說有什麼缺點的話,應該就是想把 webbrowser 換成 chrome 或 firefox 的引擎,html5 支援比較高。還有用 vb.net 寫 web api 的 route 及 對應的 controller,好像不如 c# 的簡易,c# 可以用 Get 當 function 名字,看起來若不特別指定,action 就是呼叫 Get(),vb.net 的我找不到對應的寫法,所以寫起來有點醜醜的。

終於用三天把兩個月來研究的「用 web 技術寫桌面程式」,的主題弄出一個段落。雖然,其實是 windows 桌面,因為使用者都在 windows 上,我技術上最後也跨不出 windows 平台。接下來就該寫出些有用的東西吧?

 

 

參考:

1. https://github.com/darrelmiller/HypermediaApiSite

2. http://blog2.darkthread.net/post-2013-06-04-self-host-web-api.aspx

[webapi]微軟 ASP.NET Web API Self Host 的應用(1)

(本篇程式是用 vb.net 寫成)

因為瀏覽器的能力大增,現在許多 GUI 的彩色糖果(新功能)都在瀏覽器上簡單易用。憑藉著 javascript 及瀏覽器就能做到原生桌面程式的事,而且不用在 c++ 裡打滾,也不用在意平台,真是一個全新的世界的來臨。

最早,mozilla 就試著使用 XUL 想要進入桌面程式,Google 也趁著 chrome 已融入大家的生活中的時機,推出 Chrome App。XUL 算是只使用前端瀏覽器引擎配合自有原生執行程式,Chrome App 則是採取與chrome 共生的執行環境來使用作業系統資源。接下來,有其他的人,想出自帶 server 端的方式來實現使用瀏覽器當 GUI 的解決方法,例如 app.js,利用 node.js 來當做程式的後端。

在黑暗執行緒看到的兩篇文章[1], [2]。才知道微軟出了個套件 ASP.NET Web API Self Host,可以當做 server 端的程式,可寫成 Console 程式,也可以寫成 WinForm。而這套件正好可以與 Chrome App、app.js 所提供的後端支援的功能相同。於是,我也來試試看是否能做出些什麼 app 來…。因為黑大已經示範了寫成 console 的程式,我就來試試 WinForm 的程式。黑大用的是 c#,我就來試試 vb.net 吧。

我還是一步步把畫面剪下來,免得以後我自己忘記,所以落落長。初學者才需要這樣。

首先,開啟一個 WinForm 的專案。

image

先存檔,然後開啟 NuGet。安裝 ASP.NET Web API Self Host。

image

以下是看到相依套件的授權。

image

實際安裝的套件如下:

image

接下來,安排程式碼。首先,在 Form1.vb 的最前面,加入 imports: (這樣可以少打幾個字。)

Imports System.Web.Http
Imports System.Web.Http.SelfHost

然後,加入兩個 form1 的變數,如下:

Dim config As HttpSelfHostConfiguration
Dim httpserver As HttpSelfHostServer

然後,加入一個 sub,是啟動 host 的部份,如下:

Sub hostinit()
    config = New HttpSelfHostConfiguration("http://localhost:32767")

    config.Routes.MapHttpRoute("API", "{controller}/{action}/{id}", New With {.id = RouteParameter.Optional}) ' vs2010 just can not IntelliSense....

    httpserver = New HttpSelfHostServer(config)

    'http://blog2.darkthread.net/post-2013-06-04-self-host-web-api.aspx
    httpserver.OpenAsync.Wait()
End Sub

選擇 Form1 的 New 事件,加入 hostinit(),使得 Form1 一出現就會接受來自 localhost:32767 的 request

Public Sub New()

     ' 此為設計工具所需的呼叫。
     InitializeComponent()

     ' 在 InitializeComponent() 呼叫之後加入任何初始設定。
     hostinit()
End Sub

在 Form1 的結束時,也要關掉 32767 的監聽。

Protected Overrides Sub Finalize()
    httpserver.CloseAsync.Wait()
    MyBase.Finalize()
End Sub

最後,也要加上 Controller 的物件,BlahController 來接受 request。(不要放在 Form1 裡面。我是放在另一個檔案 BlahController.vb)

Imports System.Web.Http
Public Class BlahController
    Inherits ApiController
    <HttpGet()> Public Function getDate() As String
        Return System.DateTime.Today.ToString("yyyy/MM/dd")
    End Function
End Class

完成了程式碼的安排後,還要記得黑大提醒的一件事,在 Windows 7、Windows 2008 這些作業系統開發時,因為權限控管較嚴,開 port 監聽需要較高權限來開放。我選擇了用 netsh 這個方法來開放權限。先開啟命令列(記得要用 administrator 權限開啟命令列)

netsh http add urlacl url=http://+:32767/ user=machine\username

記得上面的 machine 及 username 要換成你的電腦名稱及當下登入的使用者名稱。

接下來就可以試著啟動程式,開啟瀏覽器,輸入:

http://localhost:32767/Blah/getDate

如果瀏覽器的是 IE,會是這樣的結果:

image

按下開啟,內容是:

image

如果是用 chrome 的話,則會是:

image

 

出現了兩個不同的回傳內容。

 

接下來開始跟黑大不一樣了。

 

我們要開始問,如果我們要能夠服務 .html, .js, .css, .jpg, .png, .gif ... 等等一般的 web server 可以服務的檔案,該怎麼辦?

 

參考

1. 黑暗執行緒,不用IIS也能執行ASP.NET Web API,http://blog2.darkthread.net/post-2013-06-04-self-host-web-api.aspx
2. 黑暗執行緒,【範例】呼叫Self-Hosted ASP.NET Web API,http://blog2.darkthread.net/post-2013-06-05-post-to-web-api.aspx

3. http://www.dotnetcurry.com/ShowArticle.aspx?ID=896

4. http://stackoverflow.com/questions/17450846/is-it-possible-to-serve-a-web-page-from-a-self-hosted-web-api-in-a-windows-servi

5. https://github.com/darrelmiller/HypermediaApiSite

6. GetManifestResourceStream http://msdn.microsoft.com/en-us/library/System.Reflection.Assembly.aspx

7. return 405 while get .js http://stackoverflow.com/questions/17170277/jquery-get-returns-404-undefined-from-self-hosting-web-api

8. http://www.codeproject.com/Articles/455565/How-to-access-Manifest-Embedded-resources-from-an


類似 app.js 的平台,我試過的有:

google chrome packaged apps

appjs

還沒試過的平台:

node-webkit https://github.com/rogerwang/node-webkit

xul https://developer.mozilla.org/en/docs/XUL

Appcelerator Titanium http://en.wikipedia.org/wiki/Appcelerator_Titanium

Ember.js http://emberjs.com/

NACL https://developers.google.com/native-client/

 

VB.NET 的 IntelliSense 看不到 .Routes.MapHttpRoute 但 CS 可以。又多了一個換 CS 的理由。(逃)
http://forums.asp.net/t/1819836.aspx/1

2013年7月8日 星期一

[gae]增加對 svg 的支援

GAE 對於 svg 的支援,是把它當做 octet stream(application/octet-stream) 來處理。如果是這樣,當網址的結尾是 foo.svg 這種,就會得到下載的視窗,而不是想像的顯示圖案。

若是要讓它可以是顯示圖案,要讓 server 可以在 mime type 指定成 image/svg+xml。

因此,就要在 app.yaml 告訴 GAE 了。

以下是範例:[參考1]

- url: /svg/(.*\.svg)
  static_files: svg/\1
  upload: svg/(.*\.svg)
  mime_type: image/svg+xml

 

以下就是我測試用的 svg,是用 sozi 做的。使用 chrome, firefox 可以看。

http://rickystockinfo.appspot.com/svg/aot.svg

參考:

1. http://ryanarn.blogspot.tw/2011/04/using-googles-app-engine-to-serve-svg.html

2012年10月29日 星期一

[asp] 最近又用到 asp,幾個筆記需要記

判斷 access 資料庫某個欄位值為 null

我以為用 if cursor.fields(i).value = null then…可以判斷。我錯了。

要用 if isnull(cursor.fields(i).value) then … 才行。

 

到底權限怎麼開才能寫入?

首先,我的 os 是 windows 7,iis是 7 (應該吧…)

如果是 asp .net 的程式,IIS_USRS 這個使用者要開啟權限到寫入。

如果是 asp 的程式,IUSR 這個使用者要開啟權限到寫入。

 

如何存取utf-8 編碼的文字檔內容?

asp 直接 openTextFile 是不對的。它會按執行環境的預設值讀寫檔案。

我有改過CODEPAGE="65001",無效。

目前使用 ADODB.Stream 可。

有人提供小函數使用。

Sub Save2File (sText, sFile)
    Dim oStream
    Set oStream = CreateObject("ADODB.Stream")
    With oStream
        .Open
        .CharSet = "utf-8"
        .WriteText sText
        .SaveToFile sFile, 2
    End With
    Set oStream = Nothing
End Sub

Function ReadUtf8File(sFile)

    Set objStream = CreateObject("ADODB.Stream")
    With objStream
        .Charset = "utf-8"
        .Type=2
        .mode=3
        .Open
        .loadfromfile sFile
        ReadUtf8File=.readtext
        .Close
    End With
    Set objStream = Nothing

End Function

使用 javascript 的 escape 的文字,asp 怎麼解?

我基本上放棄了。試了幾次都不成功。這個問題很大,需要前後程式配合(html, javascript, asp)。因此會另外再寫詳細的文章。

簡單來說,我後來轉用 htmlencode。那 javascript 怎麼做 htmlencode 或 htmldecode?

以下有兩個小函數可以使用

function htmldecode(strHtmlEncode){
    var div =  document.createElement('div');
    div.innerHTML = strHtmlEncode;
    return div.innerText
}

function htmlencode(strHTML)
{
    var div = document.createElement('div');
    div.innerText = strHTML;
    return div.innerHTML;
}

asp 的程式,直接取值就好,不用解。

2012年10月23日 星期二

[html]用 javascript 查詢在網頁裡有多少定義過的 style?

原來,在 document.styleSheets 裡,存放了我們定義過的 style (class)。
也就是在 <style type="text/css"> 或 <link rel="stylesheet" type="text/css" href="src.css"> 這兩類地方寫的 style。

使用 javascript 可以取出內容。

var count = document.styleSheets.length;
    for (var y=0; y < count; y++)
    {
        var classes = document.styleSheets[y].rules || document.styleSheets[y].cssRules
        for(var x=0;x<classes.length;x++) {
            console.log("sheet"+y.toString()+" rule"+x.toString()+":"+classes[x].selectorText+":"+((classes[x].cssText) ? (classes[x].cssText) : (classes[x].style.cssText)));
        }
    }

2012年5月27日 星期日

[html5]在伺服器端怎麼支援 video 這個標籤

今天遇到的是,如果網頁中有個 video 標籤,伺服器該怎麼辦?一般人也許不會想到這個問題,而我卻遇到了。原因是我自己有寫一個小小 http server 的程式,只是為了要接收幾個按鈕,查詢並執行 ffmpeg 程式幫忙轉檔。小事情就自己動手了。所以,這種事情當然就要自己來處理囉。

從來自 chrome 的 request 之 headers 裡,有看到一個 range: 0- 這個,經過查詢(我當然還是查給人看的,而不是給超人看的文章),這個是通知伺服器說,從檔案的位置 0 開始給,結束位置隨便。感覺很隨便?其實是為了彈性。如此,伺服器該怎麼回應呢?

在 response 裡,要回覆的有

  1. Accept-Ranges: bytes
  2. Content-Range: xxx-ooo/sss

第一個是告訴瀏覽器說,我接受,而且是用 byte 當計算。 (有人說的,官方文件我真的看不懂,太像是給編譯器看的。) 而第二個就是告訴瀏覽器,從位置 xxx 開始(位置是從 0 開始算),到第 ooo 結束。整個檔案的大小是 sss。

舉例來說,一個 256 bytes 的小檔案,一次全部傳送完,Content-Range,就是 0-255/256。如果像大影片檔,就最好是一點一點的傳送,而就沒辦法一次全傳送了。

另外一個重點是回應碼,不是 200,而是 206 (參考),這樣瀏覽器才會再送一次 request 來要求其他的部份。像我提供 mp4 檔案的話,瀏覽器很聰明的先要了前面的部份之後,接下來要求最後的部份,據說是要影片的描述。有支援這種 partial content 的伺服器,大檔案傳送時應該很好用。

除了支援 range 之外,只要把 Content-Type 換成 video/mp4,chrome 就可以看影片。可以拉著時間軸跳到任意時間觀看。

程式的部份長這樣(我是用 python 寫):

self.send_response( 206 )
self.send_header( "Accept-Ranges", 'bytes' )
self.send_header( "Content-Type", 'video/mp4' )
self.send_header( "Content-Range", 'bytes %s-%s/%s' % (offset, end, size) )
self.log_message( 'bytes %s-%s/%s' % (offset, end, size))
self.end_headers()
f = open(src,'rb')
f.seek(offset, os.SEEK_SET )
self.wfile.write( f.read(block) )
f.close()
self.wfile.close()

2012年5月25日 星期五

[html5][gae]Server-Side Event

在 html5 的規格中,有看到一個 Server-Side Event (縮寫 SSE),這是什麼東西呢?是不是我最愛的伺器器主動通知呢?我好奇看了一下 w3schools 的說明,著實令我心動啊。

A server-sent event is when a web page automatically gets updates from a server.

This was also possible before, but the web page would have to ask if any updates were available. With server-sent events, the updates come automatically.

Examples: Facebook/Twitter updates, stock price updates, news feeds, sport results, etc.

這麼多大公司都用了,那我就可以放心的用吧。我們先來看怎麼用,再來講講我的感想吧。

因為我的環境是 Google App Engine,那麼我們就來改寫 w3schools 的範例吧。原來的網頁的程式碼,長這樣:

<!DOCTYPE html>
<html>
<body>
<h1>Getting server updates</h1>
<div id="result"></div>

<script>
if(typeof(EventSource)!=="undefined")
  {
  var source=new EventSource("demo_sse.php");
  source.onmessage=function(event)
    {
    document.getElementById("result").innerHTML+=event.data + "<br />";
    };
  }
else
  {
  document.getElementById("result").innerHTML="Sorry, your browser does not support server-sent events...";
  }
</script>

</body>
</html>

我們只要把 var source=new EventSource("demo_sse.php") 換成 var source=new EventSource("demo_sse")。整個網頁的 javascript 的精神就是,有 onmessage 事件發生的時候,就把事件裡的 data 值取出來,加到 id 為 result 的元素的內容去。所以改完變成:

<!DOCTYPE html>
<html>
<body>
<h1>Getting server updates</h1>
<div id="result"></div>

<script>
if(typeof(EventSource)!=="undefined")
  {
  var source=new EventSource("time");
  source.onmessage=function(event)
    {
    document.getElementById("result").innerHTML+=event.data + "<br />";
    };
  }
else
  {
  document.getElementById("result").innerHTML="Sorry, your browser does not support server-sent events...";
  }
</script>

</body>
</html>

我把上面的內容存成 main.html,放在跟 main.py 同一個目錄下。然後,main.py 的內容修改一下,在 MainHandler 的 get 方法,就改成直讀 main.html 在寫出到 out。

class MainHandler(webapp.RequestHandler):
    def get(self):
        path = os.path.join(os.path.dirname(__file__), 'main.html')
        render_content = codecs.open(path, 'r', encoding='utf8').read()
        self.response.out.write(render_content)

因為網頁會送出 /time 的要求,所以要處理他,必須要接受他。所以在 main() 要修改多一個處理

def main():
    application = webapp.WSGIApplication([('/', MainHandler),
                                          ('/time', TimeHandler),
                                          ],
                                         debug=True)

接下來寫一個 TimeHandler 接收來自網頁的要求,

class TimeHandler(webapp.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/event-stream'
        self.response.headers['Cache-Control'] = 'no-cache'
        p = "data: " + str(datetime.datetime.now()) + "\n\n"
        self.response.out.write(p)
    def post(self):
        pass

好,這樣就行了。試跑看看是不是有新的時間一直新增呢?

image

接下來是感想時間。

這個 Server-Side Event,我原本以為需要很新的 http Server 程式,例如新的 apache 或新的 IIS 或等等的。其實不用。看我的 TimeHandler 就知道,只要你會改 response 的 Content-Type 及 Cache-Control 的內容,就可以使用這個功能。一點也不難。要用得很難也行,進階版的用法請看 html5rocks。

再來,他真的是 Server 主動通知嗎?,讓我們來看看網路活動吧,打開 chrome 的檢查元素,點網路。

image

會發現到,每隔幾秒就會有網路活動,從瀏覽器送出來。有寫過的人就會想說,這我自己用 interval 或 timer 寫不就一樣?對,我也覺得一樣。既然一樣,那用哪個都好,html5 提供了這個功能,讓大家不用傷腦筋去管理 timer,也是有方便之處。也同時提醒一下不清楚的人,不要以為這就是伺服器主動通知的事件,這實際上還是 client 端 polling 的方法。

最後啊,我要提醒兩下,

(1)這個 text/event-stream 一律使用 utf-8 編碼,跟 json 一模一樣,所以中文的朋友一定要先轉好。

(2)看文件真的不是一件容易的事,尤其規格的文件這麼大一份,參考來參考去的。越是低階的規格就越大本,我從來不知道在哪裡可以找到正式文件說,遇到兩個換行,就代表傳送結束,所以,如果你真要結束,就一定要連續傳兩個換行。如果你不想要結束,傳送內容前,一定要檢查有沒有兩個換行。當然,有些 http server 也許會幫你加兩個換行在後面。如果出問題,請記得你的內容,有沒有兩個換行在你不想要的地方。