代码改变世界

在 ASP.NET WebForms/MVC 中利用 HttpModule 添加全局站点统计(CNZZ、百度统计、Google Analytics等)脚本

2012-09-21 13:38  音乐让我说  阅读(2826)  评论(1编辑  收藏  举报

在面向大众类型的网站应用中,我们常常需要知道网站的访问情况,特别是站长。就目前来说,有很多网站可以为你提供统计服务,比如: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 类

新建自己的 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 类

新建自己的 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 = "";

    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 (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 程序共用。

在 Web.Config 中注册你的 HttpModule 类

最后一步就很简单了,在项目中添加对这个程序集的引用,我这里是添加 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 下载:https://files.cnblogs.com/Music/ModifyResponseSteamInHttpModuleDemo.rar

谢谢浏览!