在面向大众类型的网站应用中,我们常常需要知道网站的访问情况,特别是站长。就目前来说,有很多网站可以为你提供统计服务,比如:CNZZ、百度统计、Google Analytics等等,而你只需要在你的网站的每个页面的底部添加一些 Javascript 脚本就可以了,比如:
<!-- 百度统计 --> <script type="text/javascript"> var _bdhmProtocol = (("https:" == document.location.protocol) ? " https://" : " http://"); document.write(unescape("%3Cscript src='" + _bdhmProtocol + "hm.baidu.com/h.js%3F5ba98b01aa179c8992f681e4e11680ab' type='text/javascript'%3E%3C/script%3E")); </script> <!-- Google 统计 --> <script type="text/javascript"> var _gaq = _gaq || []; _gaq.push(['_setAccount', 'UA-18157857-1']); _gaq.push(['_trackPageview']); (function () { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); </script>
添加这些脚本的方式有多种,第一种就是在每个页面都手动添加,这种方式适合与一些小网站,只有几个静态的 html 页面。第二种方式在“模板(或母板)”页中添加,这种也是比较好的方法。第三种就是在服务器响应的时候,动态添加,这种方法适合与一些网站前期开发时没有添加统计脚本,又没有模板(或母板)页,又可能包含静态的 html 页面的网站,为了不改变原有的代码,又节省时间,又利用维护,这也是我今天写这篇博客的目的。
新建自己的 HttpModule 类,比如我这里叫 SiteStatModule,实现 IHttpModule 接口,在 Init 方法给 HttpApplication 注册 ReleaseRequestState 事件,这个事件的解释如下:
在 ASP.NET 执行完所有请求事件处理程序后发生。该事件将使状态模块保存当前状态数据。
在这个事件中,我们需要做的就是判断 HttpResponse.StatusCode 是否等于 200,并且响应的内容的类型是否为 "text/html",如果是,我们就对它进行处理。
public class SiteStatModule : IHttpModule { private const string Html_CONTENT_TYPE = "text/html"; #region IHttpModule Members public void Dispose() { } public void Init(HttpApplication app) { app.ReleaseRequestState += OnReleaseRequestState; } #endregion public void OnReleaseRequestState(object sender, EventArgs e) { HttpApplication app = (HttpApplication)sender; HttpResponse response = app.Response; string contentType = response.ContentType.ToLowerInvariant(); if (response.StatusCode == 200 && !string.IsNullOrEmpty(contentType) && contentType.Contains(Html_CONTENT_TYPE)) { response.Filter = new SiteStatResponseFilter(response.Filter); } } }
这里的 response.Filter 需要一个 Stream 类的实例,于是我们自己建一个 SiteStatResponseFilter 类。
新建自己的 Response.Filter 类,比如我这里叫 SiteStatResponseFilter 。我们需要重写 Stream 相关的成员(Property + Method),其中主要还是 Write 方法里。为了便于重复利用,我自己抽象出一个公用的 AbstractHttpResponseFilter,代码如下:
public abstract class AbstractHttpResponseFilter : Stream { protected readonly Stream _responseStream; protected long _position; protected AbstractHttpResponseFilter(Stream responseStream) { _responseStream = responseStream; } public override bool CanRead { get { return true; } } public override bool CanSeek { get { return true; } } public override bool CanWrite { get { return true; } } public override long Length { get { return 0; } } public override long Position { get { return _position; } set { _position = value; } } public override void Write(byte[] buffer, int offset, int count) { WriteCore(buffer, offset, count); } protected abstract void WriteCore(byte[] buffer, int offset, int count); public override void Close() { _responseStream.Close(); } public override void Flush() { _responseStream.Flush(); } public override long Seek(long offset, SeekOrigin origin) { return _responseStream.Seek(offset, origin); } public override void SetLength(long length) { _responseStream.SetLength(length); } public override int Read(byte[] buffer, int offset, int count) { return _responseStream.Read(buffer, offset, count); } }
然后让我们前面新建的 SiteStatResponseFilter 类继承自 AbstractHttpResponseFilter。在 WriteCore 方法中判断当前缓冲的字节流是否存在 "</body>",因为我们的统计脚本需要插入到 "</body>" 前。如果当前缓冲的字节流中存在 "</body>",我们就动态地往 HttpResponse 中写统计脚本。PS:由于 HttpResponse 在响应时是一点一点地输出,所以需要在 WriteCore 中判断。完整代码如下:
public class SiteStatResponseFilter : AbstractHttpResponseFilter { private static readonly string END_HTML_TAG_NAME = "</body>"; private static readonly string SCRIPT_PATH = "DearBruce.ModifyResponseSteamInHttpModule.CoreLib.site-tongji.htm"; private static readonly string SITE_STAT_SCRIPT_CONTENT = ""; private const string FLAG_IsHasAppend_CrossDomainScript = "CrossDomainScriptAppend"; static SiteStatResponseFilter() { Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(SCRIPT_PATH); if (stream == null) { throw new FileNotFoundException(string.Format("The file \"{0}\" not found in assembly", SCRIPT_PATH)); } using (StreamReader reader = new StreamReader(stream)) { SITE_STAT_SCRIPT_CONTENT = reader.ReadToEnd(); reader.Close(); } } public SiteStatResponseFilter(Stream responseStream) : base(responseStream) { } protected override void WriteCore(byte[] buffer, int offset, int count) { string strBuffer = Encoding.UTF8.GetString(buffer, offset, count); strBuffer = AppendSiteStatScript(strBuffer); byte[] data = Encoding.UTF8.GetBytes(strBuffer); _responseStream.Write(data, 0, data.Length); } /// <summary> /// 附加站点统计脚本 /// </summary> /// <param name="strBuffer"></param> /// <returns></returns> protected virtual string AppendSiteStatScript(string strBuffer) {
//加入标识到上下文中,防止内容过长的时候,内容分片导致的重复注入!
if (null != HttpContext.Current && null != HttpContext.Current.Response)
{
object flagAppend = HttpContext.Current.Items[FLAG_IsHasAppend_CrossDomainScript];
if (null!=flagAppend&&(bool)flagAppend==true)
{
return strBuffer;
}
}
if (string.IsNullOrEmpty(strBuffer)) { return strBuffer; } int endHtmlTagIndex = strBuffer.IndexOf(END_HTML_TAG_NAME, StringComparison.InvariantCultureIgnoreCase); if(endHtmlTagIndex <= 0) { return strBuffer; } return strBuffer.Insert(endHtmlTagIndex, SITE_STAT_SCRIPT_CONTENT); } }
对了,为了不把这些统计脚本(本文最上面的那段脚本)硬编码到代码中,我把它放到了 site-tongji.htm 中,作为内嵌资源打包到 DLL 中,你也可以把它放到你网站下的某个目录。我的解决方案如下,请暂时忽略 JsonpModule.cs、JsonResponseFilter.cs
我把这些类放到了一个单独的程序集中,是为了让以前的 ASP.NET WebForms 程序和现在使用的 ASP.NET MVC 程序共用。
最后一步就很简单了,在项目中添加对这个程序集的引用,我这里是添加 DearBruce.ModifyResponseSteamInHttpModule.CoreLib.dll,然后在 Web.Config 中注册一下就可以了。
<httpModules> <add name="SiteStatModule" type="DearBruce.ModifyResponseSteamInHttpModule.CoreLib.SiteStatModule,DearBruce.ModifyResponseSteamInHttpModule.CoreLib"/> </httpModules>
运行查看网页源代码,就可以看到统计脚本了。
如果部署在 IIS 上,需要添加一个映射,让 IIS 把 .htm 或 .html 的后缀的请求交给 ASPNET_ISAPI.dll。
上面提到的 JsonpModule.cs 和 JsonResponseFilter.cs 是为了把程序中返回的 JSON 数据,转换为支持跨域的 JSONP 格式即 jsoncallback([?]),有兴趣的话你可以下载看看。
JsonpModule.cs
public class JsonpModule : IHttpModule { private const string JSON_CONTENT_TYPE = "application/json"; private const string JS_CONTENT_TYPE = "text/javascript"; #region IHttpModule Members public void Dispose() { } public void Init(HttpApplication app) { app.ReleaseRequestState += OnReleaseRequestState; } #endregion public void OnReleaseRequestState(object sender, EventArgs e) { HttpApplication app = (HttpApplication)sender; HttpResponse response = app.Response; if (response.ContentType.ToLowerInvariant().Contains(JSON_CONTENT_TYPE) && !string.IsNullOrEmpty(app.Request.Params["jsoncallback"])) { response.ContentType = JS_CONTENT_TYPE; response.Filter = new JsonResponseFilter(response.Filter); } } }
JsonResponseFilter.cs
public class JsonResponseFilter : AbstractHttpResponseFilter { private bool _isContinueBuffer; public JsonResponseFilter(Stream responseStream) : base(responseStream) { } protected override void WriteCore(byte[] buffer, int offset, int count) { string strBuffer = Encoding.UTF8.GetString(buffer, offset, count); strBuffer = AppendJsonpCallback(strBuffer, HttpContext.Current.Request); byte[] data = Encoding.UTF8.GetBytes(strBuffer); _responseStream.Write(data, 0, data.Length); } private string AppendJsonpCallback(string strBuffer, HttpRequest request) { string prefix = string.Empty; string suffix = string.Empty; if (!_isContinueBuffer) { strBuffer = RemovePrefixComments(strBuffer); if (strBuffer.StartsWith("{")) prefix = request.Params["jsoncallback"] + "("; } if (strBuffer.EndsWith("}")) { suffix = ");"; } _isContinueBuffer = true; return prefix + strBuffer + suffix; } private string RemovePrefixComments(string strBuffer) { var str = strBuffer.TrimStart(); while (str.StartsWith("/*")) { var pos = str.IndexOf("*/", 2); if (pos <= 0) break; str = str.Substring(pos + 2); str = str.TrimStart(); } return str; } }
Demo 下载:http://files.cnblogs.com/Music/ModifyResponseSteamInHttpModuleDemo.rar
谢谢浏览!