换个姿势访问图片

开篇先来聊一聊缩略图吧,其经典应用场景就是商品列表、详情以及查看大图时返回不同尺寸的图片。好处,额,闭上眼睛自己体会。我看到过一些做法是在图片保存时就生成三套缩略图提供给前端访问,这样其实是可以满足基本需求的,只是局限比较大,譬如下面这几种情况:
1、系统初期是基于pc做的开发,生成的缩略图也是提供给pc端使用。某个月黑风高的夜晚,领导突然拉着你的小手说我们明天开始做移动端,并且移动端的需要的图片尺寸和pc端不一样。
2、换领导了,新来的领导说列表页为什么加载这么慢,把宽320px的图片换成319px的,以前的数据全部要换~换~换~。
 
关于生成缩略图的思考
为了技(yi)术(lao)创(yong)新(yi),我们重新设计了缩略图的生成方式,与其生成固定不可更改的缩略图,不如根据前端的需(bian)求(hua)来生成缩略图,这样无论前端是需要什么尺寸我们都可以轻松应对。至于安全性,我们可以在后端配置允许访问的尺寸集合,遇到非法的请求,直接给他一张小黄图就好。于是我们的图片访问流程大概变成了这个样子:
1、用户发起图片请求,服务端拿到请求,检查图片是否存在,不存在则返回小黄图。
2、验证图片请求是否带有尺寸参数,没有则返回原图,尺寸超过原图也直接返回原图。
3、如果带有尺寸参数,验证是否在服务器允许的参数范围内,不在则返回小黄图。
4、判断该尺寸的缩略图是否存在,不存在则生成缩略图,存在即直接返回缩略图。
 
缩略图只会在第一次访问时生成,总体来说不会太影响图片访问速度。思路大概就是这个样子,接下来就只需要从服务端拿到用户的图片请求并处理。
 
关于图片请求url的思考
平时我们访问图片的方式大概是这个样子,http://xx.com/xx.png,那么带参数的图片请求会是个什么样子呢,我想最好像这样吧:http://xx.com/xx.png?width=320。
 
后端如何拿到这个请求的参数并控制其返回的内容。我能想到的有两个办法:
1、将图片访问集中到一个webapi接口,将图片路径和尺寸当做参数传过来统一处理,webapi接口以流的形式返回图片。
2、是否可以将特定的图片请求使用一个自定义的IHttpHandler来处理。
 
其实仔细一想,第一个办法实现起来并不是那么优雅,并且也不能达到我们http://xx.com/xx.png?width=320的需求,甚至图片返回的速度也会变慢不少,那么我们来分析第二个办法的可行性。我的图片都是存储在/upload/image/这个目录之下,那么前端的图片访问也必然是http://file.com/upload/image/xx.png,我们只需要将带/upload/image/的请求映射到我们自定义的IHttpHandler来处理即可。
 
解决方案
1、自定义HttpHandler:
 
using System.Drawing;
using System.Web;
using cczcrv.Web.File.Uploader.Image;
using System.IO;
using System.Linq;
using System;

namespace cczcrv.Web.File.Filters
{
    public class GetImageHandler : IHttpHandler
    {
        private int[] _imageWidthLimits = new[] { 100, 255, 320 };

        public void ProcessRequest(HttpContext context)
        {
            //防盗链
            //if (context.Request.UrlReferrer == null || !context.Request.UrlReferrer.Host.Contains("cczcrv.com"))
            //{
            //    CreateNotFoundResponse(context);
            //    return;
            //}

            DateTime lastCacheTime;
            if (DateTime.TryParse(context.Request.Headers["If-Modified-Since"], out lastCacheTime))
            {
                if ((DateTime.Now - lastCacheTime).TotalMinutes < 20)
                {
                    CreateCacheResponse(context);
                    return;
                }
            }

            //图片不存在,返回默认图片
            var path = context.Server.MapPath(context.Request.Url.AbsolutePath);
            if (!System.IO.File.Exists(path))
            {
                CreateNotFoundResponse(context);
                return;
            }

            int width = 0;
            var strWidth = context.Request.Params["width"];
            if (!string.IsNullOrWhiteSpace(strWidth) && int.TryParse(strWidth, out width))
            {
                //验证请求的图片的尺寸是否在允许的范围内
                if (!_imageWidthLimits.Contains(width))
                {
                    CreateNotFoundResponse(context);
                    return;
                }

                var index = path.LastIndexOf('\\');

                //缩略图目录不存在,创建目录
                var thumbnailDirectory = $"{path.Substring(0, index)}/thumb_{width}";
                if (!Directory.Exists(thumbnailDirectory))
                {
                    Directory.CreateDirectory(thumbnailDirectory);
                }
                var thumbnailPath = $"{thumbnailDirectory}/{path.Substring(index + 1)}";
                //缩略图不存在,生成缩略图
                if (!System.IO.File.Exists(thumbnailPath))
                {
                    var image = Image.FromFile(path);

                    //width大于图片本身宽度,则返回原图
                    if (width >= image.Width)
                    {
                        CreateImageResponse(context, path);
                        return;
                    }
                    ThumbnailHelper.MakeThumbnail(image, thumbnailPath, width, 100, ThumbnailModel.W);
                }
                CreateImageResponse(context, thumbnailPath);
                return;
            }
            CreateImageResponse(context, path);
        }

        public bool IsReusable { get { return false; } }

        #region 私有方法

        /// <summary>
        /// 返回图片
        /// </summary>
        /// <param name="context">当前上下文</param>
        /// <param name="filePath">图片路径</param>
        private void CreateImageResponse(HttpContext context, string filePath)
        {
            context.Response.Cache.SetLastModified(DateTime.Now);
            context.Response.ContentType = "image/JPEG";
            context.Response.WriteFile(filePath);
            context.Response.End();
        }

        /// <summary>
        /// 返回默认图片
        /// </summary>
        /// <param name="context">当前上下文</param>
        private void CreateNotFoundResponse(HttpContext context)
        {
            var path = context.Server.MapPath("/upload/image/404.png");
            CreateImageResponse(context, path);
        }

        /// <summary>
        /// 返回缓存的内容,HttpCode等于304
        /// </summary>
        /// <param name="context"></param>
        private void CreateCacheResponse(HttpContext context)
        {
            context.Response.StatusCode = 304;
            context.Response.End();
        }

        #endregion

    }
}

 

 
2、在webconfig中添加handler处理配置:
<add name="getImage" path="/upload/image/*" verb="GET" type="cczcrv.Web.File.Filters.GetImageHandler" />

运行效果可以通过下面两个链接查看:

原图:http://file.cczcrv.com/upload/image/201612/15/w_1903359727.png

缩略图:http://file.cczcrv.com/upload/image/201612/15/w_1903359727.png?width=100

唉,受园友启发,不能偷懒,加了width参数验证,合法的参数包括[100,255,320]。

 

这个Handler中还可以做很多事情,比如图片防盗链,ip黑名单等等,不过说到底原理只是一个IHttpHandler的应用而已。

 
posted @ 2016-12-20 09:24  _liuxx  阅读(3025)  评论(27编辑  收藏  举报