在 Web 开发中,img 标签用来呈现图片,而且一般来说,浏览器是会对这些图片进行缓存的。

比如访问百度,我们可以发现,图片、脚本这种都是从缓存(内存缓存/磁盘缓存)中加载的,而不是再去访问一次百度的服务器,这样一方面改善了响应速度,另一方面也减轻了服务端的压力。

但是,对于 WPF 和 UWP 开发来说,原生的 Image 控件是只有内存缓存的,并没有磁盘缓存的,所以一旦程序退出了,下次再重新启动程序的话,那还是得从服务器上面取图片的。因此,打造一个具备缓存(尤其是磁盘缓存)的 Image 控件还是有必要的。

在 WPF 和 UWP 中,我们都知道 Image 控件 Source 属性的类型是 ImageSource,但是,如果我们使用数据绑定的话,是可以绑定一个字符串的,在运行的时候,我们会发现 Source 属性变成了一个 BitmapImage 类型的对象。那么可以推论出,是框架给我们做了一些转换。经过查阅 WPF 的相关资料,发现是 ImageSource 这个类型上有一个 TypeConverterAttribute:

查看 ImageSourceConverter 的源码(https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/ImageSourceConverter.cs,0f008db560b688fe),我们可以看到这么一段

因此,在对 Source 属性进行绑定的时候,我们的数据源是可以使用:string、Stream、Uri、byte[] 这些类型的,当然还有它自身 ImageSource(BitmapImage 是 ImageSource 的子类)。

虽然有 5 种这么多,然而最终我们需要的是 ImageSource。另外 Uri 就相当于 string 的转换。再仔细分析的话,我们大概可以得出下面的结论:

string –> Uri –> byte[] –> Stream –> ImageSource

其中 Uri 到 byte[] 就是相当于从 Uri 对应的地方加载图片数据,常见的就是 web、磁盘和程序内嵌资源。

在某些节点我们是可以加上缓存的,如碰到一个 http/https 的地址,那可以先检查本地是否有缓存文件,有就直接加载不去访问服务器了。

经过整理,基本可以得出如下的流程图。

可以看出,流程是一个自上而下,再自下而上的流程。这里就相当于是一个管道处理模型。每一行等价于一个管道,然后整个流程相当于整个管道串联起来。

在代码的实现过程中,我借鉴了 asp.net core 中的 middleware 的处理过程。https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1&tabs=aspnetcore2x

在 asp.net core 中,middleware 的其中一种写法如下:

public class AspNetCoreMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // before
        await next(context);
        // after
    }
}

先建立一个类似 HttpContext 的上下文,用于在这个管道模型中处理,我就叫 LoadingContext:

public class LoadingContext<TResult> where TResult : class
{
    private byte[] _httpResponseBytes;
    private TResult _result;

    public LoadingContext(object source)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }

        OriginSource = source;
        Current = source;
    }

    public object Current { get; set; }

    public byte[] HttpResponseBytes
    {
        get => _httpResponseBytes;
        set
        {
            if (_httpResponseBytes != null)
            {
                throw new InvalidOperationException("value has been set.");
            }

            _httpResponseBytes = value;
        }
    }

    public object OriginSource { get; }

    public TResult Result
    {
        get => _result;
        set
        {
            if (_result != null)
            {
                throw new InvalidOperationException("value has been set.");
            }

            _result = value;
        }
    }
}

这里有四个属性,OriginSource 代表输入的原始 Source,Current 代表当前的 Source 值,在一开始是与 OriginSource 一致的。Result 代表了最终的输出,一般不需要用户手动设置,只需要到达管道底部的话,如果 Result 仍然为空,那么将 Current 赋值给 Result 就是了。HttpResponseBytes 一旦设置了就不可再设置。

可能你们会问,为啥要单独弄 HttpResponseBytes 这个属性呢,不能在下载完成的时候缓存到磁盘吗?这里考虑到下载回来的不一定是一幅图片,等到后面成功了,得到一个 ImageSource 对象了,那才能认为这是一个图片,这时候才缓存。

另外为啥是泛型,这里考虑到扩展性,搞不好某个 Image 的 Source 类型就不是 ImageSource 呢(*^_^*)

而 RequestDelegate 是一个委托,签名如下:

public delegate System.Threading.Tasks.Task RequestDelegate(HttpContext context);

因此我仿照,代码里就建一个 PipeDelegate 的委托。

public delegate Task PipeDelegate<TResult>([NotNull]LoadingContext<TResult> context, CancellationToken cancellationToken = default(CancellationToken)) where TResult : class;

NotNullAttribute 是来自 JetBrains.Annotations 这个 nuget 包的。

另外微软爸爸说,支持取消的话,那是好做法,要表扬的,因此加上了 CancellationToken 参数。

接下来那就可以准备我们自己的 middleware 了,代码如下:

public abstract class PipeBase<TResult> : IDisposable where TResult : class
{
    protected bool IsInDesignMode => (bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue;

    public virtual void Dispose()
    {
    }

    public abstract Task InvokeAsync([NotNull]LoadingContext<TResult> context, [NotNull]PipeDelegate<TResult> next, CancellationToken cancellationToken = default(CancellationToken));
}

跟 asp.net core 的 middleware 很像,这里我加了一个 IsInDesignMode 属性,毕竟在设计器模式下面,就没必要跑缓存相关的分支了。

那么,我们自己的 middleware,也就是 Pipe 有了,该怎么串联起来呢,这里我们可以看 asp.net core 的源码

https://github.com/aspnet/HttpAbstractions/blob/a78b194a84cfbc560a56d6d951eb71c8367d17bb/src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs

        public RequestDelegate Build()
        {
            RequestDelegate app = context =>
            {
                context.Response.StatusCode = 404;
                return Task.CompletedTask;
            };

            foreach (var component in _components.Reverse())
            {
                app = component(app);
            }

            return app;
        }

其中 _components 的定义如下:

private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();

Func<RequestDelegate, RequestDelegate> 代表输入了一个委托,返回了一个委托。而上面 app 就相当于管道的最底部了,因为无法处理了,因此就赋值为 404 了。至于为啥要反转一下列表,这个大家可以自己手动试试,这里也不好解析。

因此,我编写出如下的代码来组装我们的 Pipe。

internal static PipeDelegate<TResult> Build<TResult>(IEnumerable<Type> pipes) where TResult : class
{
    PipeDelegate<TResult> end = (context, cancellationToken) =>
    {
        if (context.Result == null)
        {
            context.Result = context.Current as TResult;
        }
        if (context.Result == null)
        {
            throw new NotSupportedException();
        }

        return Task.CompletedTask;
    };

    foreach (var pipeType in pipes.Reverse())
    {
        Func<PipeDelegate<TResult>, PipeDelegate<TResult>> handler = next =>
        {
            return (context, cancellationToken) =>
            {
                using (var pipe = CreatePipe<TResult>(pipeType))
                {
                    return pipe.InvokeAsync(context, next, cancellationToken);
                }
            };
        };
        end = handler(end);
    }

    return end;
}

代码比 asp.net core  的复杂一点,先看上面 end 的初始化。因为到达了管道的底部,如果 Result 仍然是空的话,那么尝试将 Current 赋值给 Result,如果执行后还是空,那说明输入的 Source 是不支持的类型,就直接抛出异常好了。

在下面的循环体中,handler 等价于上面 asp.net core 的 component,接受了一个委托,返回了一个委托。

委托体中,根据当前管道的类型创建了一个实例,并执行 InvokeAsync 方法。

构建管道的代码也有了,因此加载逻辑也没啥难的了。

        private async Task SetSourceAsync(object source)
        {
            if (_image == null)
            {
                return;
            }

            _lastLoadCts?.Cancel();
            if (source == null)
            {
                _image.Source = null;
                VisualStateManager.GoToState(this, NormalStateName, true);
                return;
            }

            _lastLoadCts = new CancellationTokenSource();
            try
            {
                VisualStateManager.GoToState(this, LoadingStateName, true);

                var context = new LoadingContext<ImageSource>(source);

                var pipeDelegate = PipeBuilder.Build<ImageSource>(Pipes);
                var retryDelay = RetryDelay;
                var policy = Policy.Handle<Exception>().WaitAndRetryAsync(RetryCount, count => retryDelay, (ex, delay) =>
                {
                    context.Reset();
                });
                await policy.ExecuteAsync(() => pipeDelegate.Invoke(context, _lastLoadCts.Token));

                if (!_lastLoadCts.IsCancellationRequested)
                {
                    _image.Source = context.Result;
                    VisualStateManager.GoToState(this, OpenedStateName, true);
                    ImageOpened?.Invoke(this, EventArgs.Empty);
                }
            }
            catch (Exception ex)
            {
                if (!_lastLoadCts.IsCancellationRequested)
                {
                    _image.Source = null;
                    VisualStateManager.GoToState(this, FailedStateName, true);
                    ImageFailed?.Invoke(this, new ImageExFailedEventArgs(source, ex));
                }
            }
        }

我们的 ImageEx 控件里面必然需要有一个原生的 Image 控件进行承载(不然咋显示)。

这里我定义了 4 个 VisualState:

Normal:未加载,Source 为 null 的情况。

Opened:加载成功,并引发 ImageOpened 事件。

Failed:加载失败,并引发 ImageFailed 事件。

Loading:正在加载。

在这段代码中,我引入了 Polly 这个库,用于重试,一旦出现异常,就重置 context 到初始状态,再重新执行管道。

而 _lastLoadCts 的类型是 CancellationTokenSource,因为如果 Source 发生快速变化的话,那么先前还在执行的就需要放弃掉了。

最后奉上源代码(含 WPF 和 UWP demo):

https://github.com/h82258652/HN.Controls.ImageEx

先声明,如果你在真实项目中使用出了问题,本人一概不负责的说。

本文只是介绍了一下具体关键点的实现思路,诸如磁盘缓存、Pipe 的服务注入(弄了一个很简单的)这些可以参考源代码中的实现。

另外源码中值得改进的地方应该是有的,希望大家能给出一些好的想法和意见,毕竟个人能力有限。

在 Web 开发中,img 标签用来呈现图片,而且一般来说,浏览器是会对这些图片进行缓存的。的更多相关文章

  1. [转]移动web开发中meta标签作用

    今天在尝试做移动页面的时候遇到了一个问题,<meta content="telephone=no,email=no" name="format-detection& ...

  2. Java Web开发中路径问题小结

     Java Web开发中,路径问题是个挺麻烦的问题,本文小结了几个常见的路径问题,希望能对各位读者有所帮助. (1) Web开发中路径的几个基本概念 假设在浏览器中访问了如下的页面,如图1所示: 图1 ...

  3. Java Web 开发中路径相关问题小结

    Java Web开发中路径问题小结 (1) Web开发中路径的几个基本概念 假设在浏览器中访问了如下的页面,如图1所示: 图1 Eclipse中目录结构如图2所示: 图2 那么针对这个站点的几个基本概 ...

  4. Java Web开发中路径问题小结(getRequestUrl getContextUrl getServletUrl)

    看以博客感觉不错,分享一下http://www.cnblogs.com/tianguook/archive/2012/08/31/2665755.html (1) Web开发中路径的几个基本概念 假设 ...

  5. 今日推荐:10款在 Web 开发中很有用的占位图片服务

    设计网站时,将要使用的图像在一开始通常还不存在,这个时候布局是最重要的.然而,图像的尺寸通常是预先设置,实用一些占位图像可以帮助我们更好地预览和分析布局. 如今,有免费的占位图片自动生成工具可以使用, ...

  6. 移动Web 开发中的一些前端知识收集汇总

    在开发DeveMobile 与EaseMobile 主题 的时候积累了一些移动Web 开发的前端知识,本着记录总结的目的,特写这篇文章备忘一下. 要说移动Web 开发与传统的PC 端开发,感觉也没什么 ...

  7. web开发中目录路径问题的解决

    web开发当中,目录路径的书写是再常用不过了,一般情况下不会出什么问题,但是有些时候出现了问题却一直感到奇怪,所以这里记录一下,彻底解决web开发中路径的问题,开发分为前端和服务端,那么就从这两个方面 ...

  8. (转)Web开发中最致命的小错误

    Web开发中最致命的小错误 现在,有越来越多所谓的“教程”来帮助我们提高网站的易用性.本文收集了一些在 Web 开发中容易出错和被忽略的小问题,并且提供了参考的解决方案,以便于帮助 Web 开发者更好 ...

  9. Web开发中Listener、Filter、Servlet的初始化及调用

    我们在使用Spring+SpringMVC开发项目中,web.xml中一般的配置如下: <?xml version="1.0" encoding="UTF-8&qu ...

随机推荐

  1. 安装exe4j出现jre不匹配问题

    在安装exe4j 客户端,提示如下错误: 提示的错误信息大意如下:install4j安装时,在本系统中没有找到JRE(JavaRuntime Environment)(版本要求:最低1.5,最高1.6 ...

  2. x264介绍

    x264 编辑   H.264是ITU(International Telecommunication Unite 国际通信联盟)和MPEG(Motion Picture Experts Group ...

  3. Core Animation教程

    http://dev.wo.com.cn/bbs/viewthread.jsp?tid=141767&page=1 http://blog.csdn.net/lvxiangan/article ...

  4. js中事件冒泡和事件捕获

    什么时候存在这种问题? 当一个行为触发了多个对象的事件时.   <body>   <div class="fa">     <div class=&q ...

  5. C++命名空间、标准库(std,全局命名空间)

    背景 别人遇到的问题: C++ 全局变量不明确与 using namespace std 冲突 我遇到的问题与他相似,函数调用冲突 using namespace std; class compare ...

  6. 笔试算法题(36):寻找一棵二叉树中最远节点的距离 & 根据二叉树的前序和后序遍历重建二叉树

    出题:求二叉树中距离最远的两个节点之间的距离,此处的距离定义为节点之间相隔的边数: 分析: 最远距离maxDis可能并不经过树的root节点,而树中的每一个节点都可能成为最远距离经过的子树的根节点:所 ...

  7. Vue实例方法之事件的实现

    开始 这段时间一直在看vue的源码,源码非常多和杂,所以自己结合资料和理解理出了一个主线,然后根据主线去剥离其他的一些知识点,然后将各个知识点逐一学习.这里主要是分析的Vue事件处理的实现. 正文 一 ...

  8. Python自动化测试-使用Pandas来高效处理测试数据

    一.思考 1.Pandas是什么? 功能极其强大的数据分析库 可以高效地操作各种数据集 csv格式的文件 Excel文件 HTML文件 XML格式的文件 JSON格式的文件 数据库操作 2.经典面试题 ...

  9. selenium切换frame(iframe)

    例如网页代码为: <!DOCTYPE html><html lang="en"><head> <meta charset="UT ...

  10. Uva 4916 Selling Cells(随机算法)

    题意: 给定n个圆的 坐标 和半径, 求第一个圆与其他圆相交的面积占第一个圆的面积的多大. 分析: 如果从局部去想, 处理每个圆之间的关系, 求出他们与第一个圆的交集, 这样可能就会十分复杂了. 不妨 ...