聊聊WEB项目中的图片

13 篇文章 0 订阅
12 篇文章 0 订阅

最近一直在鼓捣图片相关的代码,今天抽时间写篇总结。此文没有什么高深的知识点,不汲及第三方的OSS相关点,更不汲及分布式文件存储框架,算是一篇关于WEB项目中图片相关功能的扫盲文; 同时与大家分享码字时的心得。文章中的服务器开发语言使用的是java。文中代码汲及到一个工具子模块(util)在文章最后提供下载连接,放心不需要您有下载积分,防止资源若审核过不去同时提供百度网盘地址。

A. 客户端:

A1. html表单,客户端预览

有同事认为这像输入数字时调出数字键盘一样是高级功能。其实不然,现代浏览器提供多种方案可以实现,代码也不复杂。先看图:
在这里插入图片描述
在这里插入图片描述
以下代码使用FileReader生成图片的BASE64值:

<div class="form-group row">
  <label for="imageAddr" class="col-12 col-sm-3 col-form-label text-sm-right">图标</label>
  <div class="col-12 col-sm-8 col-lg-6" id="upload-section">
    <label class="custom-control custom-checkbox">
      <input type="text" class="form-control" value="" tabindex="2" readonly="readonly" id="inputfile-names"/>
      <span class="form-text text-muted">可选项,不填写将使用默认值</span>
    </label>
    <label class="custom-control custom-checkbox">
      <input type="file" name="file" id="inputfile" accept="image/*" multiple="multiple" data-multiple-caption="{count} 文件被选中"/>
      <div class="images"><div class="pic">选择图片</div></div>
    </label>
  </div>
</div>

201491026增补

input type=file的accept属性
可以粗略的划分:accept=“image/*”, 也可用Content-type来细分: accept=“image/gif, image/jpeg”,还可以用扩展名过滤:accept=".gif,.jpeg,.png,.jpg"
需要注意不同浏览器对上面的三种情况的待遇也不同,但无一例外都有选择所有文件的选项,问题来了:怎么删除它或让不可用呢?

function uploadImage() {
    var uploader = $('#inputfile');
    var images = $('.images');
    
    uploader.on('change', function () {
        var reader = new FileReader();
        reader.onload = function(event) {
            images.prepend('<div class="img" style="background-image: url(\'' + event.target.result + '\');" rel="'+ event.target.result  +'"><span>删除</span></div>');
            images.find('.pic').hide();
        };
        reader.readAsDataURL(uploader[0].files[0]);
     });
    
    images.on('click', '.img', function () {
        $(this).remove();
        images.find('.pic').show();
    });
}

图片BASE64更多讨论:how-to-convert-image-into-base64-string-using-javascript

A2. 富文本编辑,CKEditor中的图片上传

CKEditor的版本号:4.11.4; 若使用其它富文本编辑器请自行参阅官方文档。以下代码用一个函数来封装了CKEditor的实例

//v:4.11.4
function initCkEditor(config){
    var options=$.extend({
        textarea: 'content', 
        plugs: 'uploadimage', 
        width: 600, 
        height: 150, 
        UploadUrl: '/upload/ckeditor'}, 
        config);
    CKEDITOR.config.pasteFilter = null;
    CKEDITOR.config.height = options.height;
    CKEDITOR.config.width = options.width;
    CKEDITOR.replace( options.textarea ,{
        extraPlugins: options.plugs
    });
    if(options.plugs.indexOf('uploadimage') != -1){
        CKEDITOR.config.filebrowserUploadUrl = options.UploadUrl+'?type=Files';
        CKEDITOR.config.filebrowserImageUploadUrl = options.UploadUrl+'?type=Images';
    }
};

这里提到CKEditor是因为后面的服务器端上传时需要。因为不同版本的CKEditor的图片上传响应格式不同。此版本的响应格式为:

{
    "uploaded": 1,
    "fileName": "foo(2).jpg",
    "url": "/files/foo(2).jpg",
    "error": {
        "message": "A file with the same name already exists. The uploaded file was renamed to \"foo(2).jpg\"."
    }
}

CKEditor的参考文档: Uploading Dropped or Pasted Files

B. 本地存储(服务器端)

如果项目中的图片不是很多。像一般企业CMS,本地存储也是可以的,具体情况具体对待。为了在需求变动时不改动代码,服务器端对图片的地址进行编码和解码,在数据库中存储的图片地址都是编码后的. 例 :<img src="image://xxx/yy.z"/>。这种格式没有域名部分,没有具体的目录信息,只有图片的文件名(yy.z)及上一级目录的名称(xxx)。根据具体的配置信息浏览器中html的图片src解码为具体的可访问地址。

B1. 外部化上传需要的参数信息, 以下是资源文件中代码:

img.bucket.domain=http://www.test.com
img.bucket.upload.direct=imagestore

为了在代码中使用方便,而不是到处使用@Value.需要在spring bean工厂中实例一个单例的值对象,例:

	<!-- 站内本地存储 -->
	<bean id="imageStorageExecutor" class="com.apobates.forum.trident.fileupload.LocalImageStorage">
		<constructor-arg name="imageBucketDomain" value="${img.bucket.domain}"/>
		<constructor-arg name="uploadImageDirectName" value="${img.bucket.upload.direct}"/>
		<constructor-arg name="localRootPath" value="C:\apache-tomcat-8.5.37\webapps\ROOT\"/> 
	</bean>

localRootPath值等于:servletContext.getRealPath("/");。为了以后站外存储或第三方存储图片,ImageStorageExecutor设计成一个接口。相关代码如下:

public interface ImageStorageExecutor extends ImageStorage{
	/**
	 * 存储图片
	 * 
	 * @param file 图片文件
	 * @return 成功返回图片的访问连接
	 * @throws IOException
	 */
	Result<String> store(MultipartFile file) throws IOException;
}

Result是一个类似Optional的一个工具类,同时提供Empty,Success,Fail三种情况; 代码在工具子模块(util)中。 ImageStorage为一获取配置信息的接口:

public interface ImageStorage {
	/**
	 * 图片存储的域名,例:http://x.com
	 * 
	 * @return
	 */
	String imageBucketDomain();
	
	/**
	 * 图片存储的目录名
	 * 
	 * @return
	 */
	String uploadImageDirectName();
}

B2. spring mvc获取表单中的图片。并上传图片

formbean中的代码大致如下:

public class BoardForm extends ActionForm{
	private String title;
	private String imageAddr;
	private MultipartFile file;
              //ETC
	public MultipartFile getFile() {
		return file;
	}
	public void setFile(MultipartFile file) {
		this.file = file;
	}
	
	public String getEncodeIcoAddr(ImageStorageExecutor executor){
		return super.uploadAndEncodeFile(getFile(), executor);
	}
}

方法getEncodeIcoAddr使用imageStorageExecutor(注入到控制器类中的)来获得上传后的图片访问地址并对其进行编码。具体上传工作ActionForm.uploadAndEncodeFile方法来作。代码如下:

public abstract class ActionForm implements Serializable{
	private String record = "0";
	private String token; // (message="错误代码:1051;抽像的操作标识丢失")
	private String status = "0";
	// 来源地址
	// 只允许是本站地址,例:/x
	private String refer;
	private final static Logger logger = LoggerFactory.getLogger(ActionForm.class);
              //ETC
	/**
	 * 
	 * @param file     上传的文件
	 * @param executor 上传的执行器
	 * @return
	 */
	protected String uploadAndEncodeFile(MultipartFile file, ImageStorageExecutor executor)throws FileUploadFailException{
		Result<String> data = Result.empty();
		try{
			data = executor.store(file);
		}catch(IOException e){
			//只关心上传产生的错误
			throw new FileUploadFailException(e.getMessage()); //只关心上传产生的错误
		}
		if(data.isFailure()){ //上传过程中的错误
			throw new FileUploadFailException(data.failureValue().getMessage());
		}
		//
		String defaultValue = "image://defat/ico/default_icon.png";
		if(data.isEmpty()){ 
			//新增返回默认值
			//编辑返回null
			return (isUpdate())?null:defaultValue;
		}
		//编码
		ImagePathCoverter ipc = new ImagePathCoverter(data.successValue());
		return ipc.encodeUploadImageFilePath(executor.imageBucketDomain(), executor.uploadImageDirectName()).getOrElse(defaultValue);
	}
}

ImagePathCoverter类的构造参数即为图片的可访问地址,接下来上传工作交给ImageStorageExecutor接口的实现类:LocalImageStorage,代码如下:

public class LocalImageStorage implements ImageStorageExecutor{
	private final String imageBucketDomain;
	private final String uploadImageDirectName;
	//servletContext.getRealPath("/")
	private final String localRootPath;
	
	/**
	 * 
	 * @param imageBucketDomain     本站的域名,例:http://x.com
	 * @param uploadImageDirectName 存储图片的目录名称
	 * @param localRootPath          servletContext.getRealPath("/")的结果
	 */
	public LocalImageStorage(String imageBucketDomain, String uploadImageDirectName, String localRootPath) {
		super();
		this.imageBucketDomain = imageBucketDomain;
		this.uploadImageDirectName = uploadImageDirectName;
		this.localRootPath = localRootPath;
	}

	@Override
	public Result<String> store(MultipartFile file) throws IOException {
		//空白
		if(file==null || file.isEmpty()){
			return Result.failure("arg file is null or empty");
		}
		//子目录
		String childDirect = DateTimeUtils.getYMD();
		// 项目路径
		final String realPath = localRootPath + File.separator + uploadImageDirectName + File.separator +childDirect;
		// 前台访问路径
		final String frontVisitPath = imageBucketDomain + "/" + uploadImageDirectName + "/" + childDirect + "/";
		CommonInitParamers cip = new CommonInitParamers() {
			@Override
			public String getFileSaveDir() {
				return realPath;
			}
			@Override
			public String getCallbackFunctionName() {
				// 没有回调函数
				return null;
			}
		};
		
		// 使用fileupload
		SpringFileUploadConnector connector = new SpringFileUploadConnector(new CKEditorHightHandler(frontVisitPath));
		try{
			return Result.ofNullable(connector.upload(cip, file)); //返回文件的访问地址
		}catch(ServletException e){
			return Result.failure("upload has exception: "+ e.getMessage());
		}
	}
	@Override
	public String imageBucketDomain() {
		return imageBucketDomain;
	}
	@Override
	public String uploadImageDirectName() {
		return uploadImageDirectName;
	}
}

CKEditorHightHandler负责处理CKEditor需要的响应,上面提到过因为版本不同CKEditor需要不同的响应。SpringFileUploadConnector类负责具体的上传工作,代码如下:

public class SpringFileUploadConnector extends ApacheFileUploadConnector{
	
    public SpringFileUploadConnector(UploadHandler handler) {
        super(handler);
    }
    
    public String upload(CommonInitParamers params, MultipartFile file) throws IOException, ServletException{
        if(!(file instanceof CommonsMultipartFile)){
            return "500";
        }
        CommonsMultipartFile cmf=(CommonsMultipartFile)file;
        //保存的路径
        File uploadDir=new File(params.getFileSaveDir());
        if (!uploadDir.exists()) {
            uploadDir.mkdirs();
        }
        
        String callbackFun=params.getCallbackFunctionName();
        
        return execute(cmf.getFileItem(), uploadDir, callbackFun);
    }
}

ApacheFileUploadConnector类的代码在工具子模块(util)中。这里都不继续贴了。

B3. CKEditor的图片上传

上面的代码中有写过CKEditor图片上传的连接:/upload/ckeditor; 此连接是一个单独的控制器,代码如下:

@Controller
@RequestMapping("/upload")
public class UploadController {
	@Autowired
	private ServletContext servletContext;
	@Autowired
	private ImageIOInfo imageIOInfo;
	private final static Logger logger = LoggerFactory.getLogger(UploadController.class);

	// CK4:V4.11.4[LocalStorage|本地存储]
	@RequestMapping(value = "/ckeditor", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
	@ResponseBody
	public String ckeditorUploadAction(@RequestParam("upload") MultipartFile file, @RequestParam("type") final String fileType) {
		String localReal = servletContext.getRealPath("/");
		ImageStorageExecutor ise = new LocalImageStorage(imageIOInfo.getImageBucketDomain(), imageIOInfo.getUploadImageDirectName(), localReal);
		Result<String> data = Result.empty();
		try{
			data = ise.store(file);
		}catch(Exception e){
			data = Result.failure(e.getMessage(), e);
		}
		if(!data.isSuccess()){
			return buildCkeditorResponse(false, "上传失败", null, null);
		}
		String responsebody = data.successValue();
		return buildCkeditorResponse(true, "上传成功", getFileName(responsebody), responsebody);
	}

	// CK4:V4.11.4的响应格式
	private String buildCkeditorResponse(boolean isCompleted, String message, String fileName, String imageUrl) {
		Map<String, Object> data = new HashMap<>();
		data.put("uploaded", isCompleted ? "1" : "0");
		if (fileName != null) {
			data.put("fileName", fileName);
		}
		if (imageUrl != null) {
			data.put("url", imageUrl);
		}
		if (!isCompleted) {
			Map<String,String> tmp = new HashMap<>();
			tmp.put("message", Commons.optional(message, "未知的网络错误"));
			data.put("error", tmp);
		}
		return new Gson().toJson(data);
	}
	
	//获得图片地址的文件名
	private String getFileName(String imageURL){
		return imageURL.substring(imageURL.lastIndexOf("/") + 1);
	}
}

C. 站外存储

本地开发的时候会频繁的改动代码,若图片随项目走,真是太烦人的。
同时也是检验图片灵活存储的一个机会,开始新拉一个小项目:bucket,这个小项目目前只有一项工作都是保存图片。

C1. 接受上传工作, UploadEditorFileServlet

public class UploadEditorFileServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	private String uploadImageDirectName; 
	private String siteDomain; 
	private final static Logger logger = LoggerFactory.getLogger(UploadEditorFileServlet.class);

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
		response.setContentType("application/json");
		response.setCharacterEncoding("UTF-8");
		//子目录
		String childDirect = DateTimeUtils.getYMD();
		// 项目路径
		final String realPath = request.getServletContext().getRealPath("/") + File.separator + uploadImageDirectName + File.separator + childDirect;
		// 前台访问路径
		final String frontVisitPath = siteDomain + uploadImageDirectName + "/"+childDirect+"/";
		UploadInitParamers cip = new UploadInitParamers() {
			@Override
			public String getFileSaveDir() {
				return realPath;
			}

			@Override
			public String getCallbackFunctionName() {
				// 没有回调函数
				return null;
			}

			@Override
			public String getUploadFileInputName() {
				return "upload";
			}

			@Override
			public HttpServletRequest getRequest() {
				return request;
			}
		};
		try (PrintWriter out = response.getWriter()) {
			try {
				ServletPartUploadConnector connector = new ServletPartUploadConnector(new CKEditorHightHandler(frontVisitPath));
				String responsebody = connector.upload(cip);
				out.println(buildCkeditorResponse(true, "上传成功", responsebody.replace(frontVisitPath, ""), responsebody));
			} catch (Exception e) {
				if (logger.isDebugEnabled()) {
					logger.debug(e.getMessage(), e);
				}
				out.println(buildCkeditorResponse(false, e.getMessage(), null, null));
			}
			out.flush();
		}
	}

	// CK4:V4.11.4的响应格式
	// https://ckeditor.com/docs/ckeditor4/latest/guide/dev_file_upload.html
	private String buildCkeditorResponse(boolean isCompleted, String message, String fileName, String imageUrl) {
		Map<String, Object> data = new HashMap<>();
		data.put("uploaded", isCompleted ? "1" : "0");
		if (fileName != null) {
			data.put("fileName", fileName);
		}
		if (imageUrl != null) {
			data.put("url", imageUrl);
		}
		if (!isCompleted) {
			Map<String,String> tmp = new HashMap<>();
			tmp.put("message", Commons.optional(message, "未知的网络错误"));
			data.put("error", tmp);
		}
		return new Gson().toJson(data);
	}

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		resp.setContentType("text/html");
		resp.setCharacterEncoding("UTF-8");
		try (PrintWriter out = resp.getWriter()) {
			out.println("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>Access Denied</title></head><body><p>暂不支持目录的功能</p></body></html>");
			out.flush();
		}
	}
	/**
	 * 获得初始化值
	 */
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		this.siteDomain = config.getInitParameter("siteDomain"); //站点URL, http://xx.com
		this.uploadImageDirectName = config.getInitParameter("uploadImageDirectName"); //图片保存的目录
	}
}

为了减小依赖这里使用ServletPartUploadConnector来保存图片,代码在工具子模块(util)中。同时也将配置外部化方便更改。web.xml如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	version="3.1">
	<display-name>bucket</display-name>
	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
	</welcome-file-list>
	<servlet>
		<servlet-name>UploadEditorFileServlet</servlet-name>
		<servlet-class>com.apobates.forum.bucket.servlet.UploadEditorFileServlet</servlet-class>
		<init-param>
			<param-name>siteDomain</param-name>
			<param-value>http://pic.test.com/</param-value>
		</init-param>
		<init-param>
			<param-name>uploadImageDirectName</param-name>
			<param-value>imagestore</param-value>
		</init-param>
		<multipart-config>
			<max-file-size>10485760</max-file-size>
			<max-request-size>20971520</max-request-size>
			<file-size-threshold>5242880</file-size-threshold>
		</multipart-config>
	</servlet>
	<servlet-mapping>
		<servlet-name>UploadEditorFileServlet</servlet-name>
		<url-pattern>/upload/ckeditor</url-pattern>
	</servlet-mapping>
</web-app>

这里可以看到访问图片的域名为:pic.test.com, 图片保存在imagestore目录中,上传的地址为: http://pic.test.com/upload/ckeditor。

下面来在tomcat中配置这个域名, <tomcat安装目录>/conf/server.xml

      <Host name="www.test.com"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
      <Host name="pic.test.com" appBase="bucket" unpackWARs="true" autoDeploy="true">
	     <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="bucket_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
	     <Context path="" docBase="ROOT" reloadable="true" useHttpOnly="true"/>
      </Host>

因为跨域了所以还需要在web.xml配置一个CORS 过滤器。这里都不贴了,请参考: http://tomcat.apache.org/tomcat-8.5-doc/config/filter.html#CORS_Filter

C2. 更改CKEditor编辑图片上传的地址

这个比较容易,找到项目的js文件中的initCkEditor函数,将UploadUrl: '/upload/ckeditor' 变成 : UploadUrl: 'http://pic.test.com/upload/ckeditor'

C3. formbean中的图片上传

C3.1先将资源文件中的配置修改如下:

img.bucket.domain=http://pic.test.com
img.bucket.upload.direct=imagestore
img.bucket.upload.uri=/upload/ckeditor
img.bucket.upload.input=upload

C3.2 将spring配置文件中的 imageStorageExecutor Bean换一个新实现:

	<!-- 站外存储图片 -->
	<bean id="imageStorageExecutor" class="com.apobates.forum.trident.fileupload.OutsideImageStorage">
		<constructor-arg name="imageBucketDomain" value="${img.bucket.domain}"/>
		<constructor-arg name="uploadImageDirectName" value="${img.bucket.upload.direct}"/>
		<constructor-arg name="imageBucketUploadURL" value="${img.bucket.domain}${img.bucket.upload.uri}"/>
		<constructor-arg name="imageBucketUploadInputFileName" value="${img.bucket.upload.input}"/>
	</bean>

OutsideImageStorage代码如下:

public class OutsideImageStorage implements ImageStorageExecutor{
	private final String imageBucketDomain;
	private final String uploadImageDirectName;
	private final String imageBucketUploadURL;
	private final String imageBucketUploadInputFileName;
	private final static Logger logger = LoggerFactory.getLogger(OutsideImageStorage.class);
	
	/**
	 * 
	 * @param imageBucketDomain              站外的访问域名,例:http://x.com
	 * @param uploadImageDirectName          站外保存图片的目录
	 * @param imageBucketUploadURL           站外上传程序的访问地址,例:http://x.com/y
	 * @param imageBucketUploadInputFileName 站外上传程序接受的type=file输入项的名称
	 */
	public OutsideImageStorage(String imageBucketDomain, String uploadImageDirectName, String imageBucketUploadURL, String imageBucketUploadInputFileName) {
		super();
		this.imageBucketDomain = imageBucketDomain;
		this.uploadImageDirectName = uploadImageDirectName;
		this.imageBucketUploadURL = imageBucketUploadURL;
		this.imageBucketUploadInputFileName = imageBucketUploadInputFileName;
	}

	@Override
	public String imageBucketDomain() {
		return imageBucketDomain;
	}

	@Override
	public String uploadImageDirectName() {
		return uploadImageDirectName;
	}

	public String getImageBucketUploadURL() {
		return imageBucketUploadURL;
	}

	public String getImageBucketUploadInputFileName() {
		return imageBucketUploadInputFileName;
	}

	@Override
	public Result<String> store(MultipartFile file) throws IOException {
		if(file==null || file.isEmpty()){
			logger.info("[AFU][OU] file is null or empty");
			return Result.failure("arg file is null or empty");
		}
		HttpHeaders parts = new HttpHeaders();  
		parts.setContentType(MediaType.TEXT_PLAIN);
		final ByteArrayResource byteArrayResource = new ByteArrayResource(file.getBytes()) {
			@Override
			public String getFilename() {
				return file.getOriginalFilename();
			}
		};
		final HttpEntity<ByteArrayResource> partsEntity = new HttpEntity<>(byteArrayResource, parts);
		//
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.MULTIPART_FORM_DATA);
		
		MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
		body.add(imageBucketUploadInputFileName, partsEntity);
		HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
		
		RestTemplate restTemplate = new RestTemplate();
		String responseBody = restTemplate.postForEntity(imageBucketUploadURL, requestEntity, String.class).getBody();
		return parseFileURL(responseBody);
	}

	private Result<String> parseFileURL(String responseBody){
		if(responseBody == null){
			logger.info("[AFU][AF]3> upload fail used default value");
			return Result.failure("upload fail used default value");
		}
		Map<String, Object> rbMap = new Gson().fromJson(responseBody, new TypeToken<Map<String, Object>>() {}.getType());
		if(rbMap==null || rbMap.isEmpty()){
			logger.info("[AFU][AF]4> from json map is not used");
			return Result.failure("from json map is not used");
		}
		if(rbMap.containsKey("error")){
			@SuppressWarnings("unchecked")
			Map<String,String> tmp = (Map<String,String>)rbMap.get("error");
			logger.info("[AFU][AF]5> uploaded response error: "+ tmp.get("message"));
			return Result.failure("uploaded response error: "+ tmp.get("message"));
		}
		//String fileVisitPath=null;
		try{
			String fileVisitPath = rbMap.get("url").toString();
			return Result.success(fileVisitPath);
		}catch(NullPointerException | ClassCastException e){
			return Result.failure("parse image url for response has exception: "+ e.getMessage());
		}
	}
}

这里使用Spring的RestTemplate将MultipartFile交给bucket项目的UploadEditorFileServlet,工作结束。

D. 生成缩略图

小可也曾用过img标签上加width和height属性来控制图片,不让它破坏CSS布局; 也有各种hack让图片自适应父div。但这都不治本,因为服务器还是要输出哪么大的图片,试想如果用oss或第三方的解决方案都是按流量收费的。现代app的图片大多采用webp, 因为它有高压缩比,经过优化,可以在网络上实现更快,更小的图像。再者看看你日常去的哪些大网站,哪个对图片原样输出了。

D1 新模块:thumbnail

对外服务的 ThumbBuilderServlet,它接受这几个参数:dir目录名,file文件名,scale支持:auto|widthheight.代码如下:

public class ThumbBuilderServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	private static final Logger logger = LoggerFactory.getLogger(ThumbBuilderServlet.class);
	private String originalDir;
	private String thumbDir;
	private int maxWidth;
	
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		String scaleParameter = req.getParameter("scale"); //接受:auto|width<x>height
		String imageFileName = req.getParameter("file");
		String directory = req.getParameter("dir");
		//
		if (scaleParameter==null || scaleParameter.isEmpty()) {
			resp.sendError(400, "scale parameter lost");
			return;
		}
		if (imageFileName.isEmpty() || imageFileName.indexOf(".") == -1) {
			resp.sendError(400, "image file is not exist");
			return;
		}
		try {
			String originalImageDirect = getServletContext().getRealPath("/") + originalDir + ThumbConstant.FS;
			String originalImagePath;
			if(!"/".equals(directory)){
				originalImagePath = directory + ThumbConstant.FS + imageFileName;
			}else{
				originalImagePath = imageFileName;
			}
			//是否裁剪
			int imageWidth = getOriginalImageWidth(originalImageDirect+originalImagePath);
			if(imageWidth <= maxWidth && "auto".equals(scaleParameter)){ 
				//
				RequestDispatcher rd = getServletContext().getRequestDispatcher("/"+ originalDir + ThumbConstant.WEB_FS + originalImagePath);
				rd.forward(req, resp);
				return;
			}
			//裁剪开始
			ImagePath ip = new ImagePath(scaleParameter, (originalImageDirect+originalImagePath), imageWidth, new File(getServletContext().getRealPath("/") + thumbDir + ThumbConstant.FS));
			if (!ip.getImagePhysicalPath().exists()) {
				//
				ip.makeThumb();
			}
			//
			redirectThumbImage(req, resp, ip);
		} catch (IOException e) {
			if(logger.isDebugEnabled()){
				logger.debug("[Thumb][TBS]image thumb create fail");
			}
		}
	}
	
	/**
	 * 获得初始化值
	 */
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		this.originalDir = config.getInitParameter("original"); //原目录
		this.thumbDir = config.getInitParameter("thumb"); //封面目录
		this.maxWidth = 900;
		try{
			maxWidth = Integer.valueOf(config.getInitParameter("maxWidth"));
		}catch(NullPointerException | NumberFormatException e){}
	}
	
	/**
	 * 跳转到缩略图地址
	 * 
	 * @param req
	 * @param resp
	 * @param ip
	 * @throws ServletException
	 * @throws IOException
	 */
	protected void redirectThumbImage(HttpServletRequest req, HttpServletResponse resp, ImagePath ip)throws ServletException, IOException {
		RequestDispatcher rd = getServletContext().getRequestDispatcher(ip.getThumbWebHref(thumbDir));
		rd.forward(req, resp);
	}
	
	/**
	 * 计算原始图片的宽度
	 * 
	 * @param originalImagePath 原始图片的物理地址
	 * @return
	 */
	private int getOriginalImageWidth(String originalImagePath){
		int imageWidth = 0;
		try{
			BufferedImage bimg = ImageIO.read(new File(originalImagePath));
			imageWidth = bimg.getWidth();
		}catch(IOException e){}
		return imageWidth;
	}
}

对于图片宽度小于maxWidth设置的不允以裁剪,原样输出。thumbDir值为裁剪后的目录名。originalDir值为原始图片的存放目录,因此只要将thumbnail模加加入到图片存储项目的依赖中并为servlet配置一个地址即可。ImagePath为裁剪的入口类,代码如下:

public class ImagePath {
	//接受:auto|width<x>height
	private final String scaleParameter;
	//图片的缩放比例(1-100)
	private final int imageScale;
	//裁剪保存的目录
	private final File thumbDirectory;
	//被裁剪的图片文件
	private final File sourceImageFile;
	
	public ImagePath(String scale, String sourceFilePath, int imageWidthSize, File thumbDir) {
		this.scaleParameter = scale;
		this.imageScale = getScaleNumber(imageWidthSize);
		this.thumbDirectory = thumbDir;
		this.sourceImageFile = new File(sourceFilePath);
	}
	
	/**
	 * 裁剪后的WEB访问地址
	 * @param thumbDir
	 * @return
	 */
	public String getThumbWebHref(String thumbDir) {
		return ThumbConstant.WEB_FS + thumbDir + ThumbConstant.WEB_FS + getCropDirect() + ThumbConstant.WEB_FS + sourceImageFile.getName();
	}
	
	/**
	 * 裁剪后的物理地址
	 * @return
	 */
	public File getImagePhysicalPath() {
		File ipp = new File(thumbDirectory, ThumbConstant.FS + getCropDirect() + ThumbConstant.FS);
		if (!ipp.exists()) {
			//
			ipp.mkdirs();
		}
		return new File(ipp, ThumbConstant.FS + sourceImageFile.getName());
	}
	
	/**
	 * 参数中的宽度和高度信息
	 * @return
	 */
	private Map<String, Integer> getImageWidthAndHeight() {
		Map<String, Integer> data = new HashMap<String, Integer>();
		String[] sas = scaleParameter.split(ThumbConstant.WIDTH_HEIGHT_SPLITER);
		if(sas.length != 2){
			return Collections.EMPTY_MAP;
		}
		data.put(ThumbConstant.IMAGE_WIDTH, Integer.valueOf(sas[0]));
		data.put(ThumbConstant.IMAGE_HEIGHT, Integer.valueOf(sas[1]));
		return data;
	}

	public void makeThumb() throws IOException {
		Map<String, Integer> d = getImageWidthAndHeight();
		int cropWidth=0, cropHeight=0;
		if(!d.isEmpty()){
			cropWidth = d.get(ThumbConstant.IMAGE_WIDTH);
			cropHeight = d.get(ThumbConstant.IMAGE_HEIGHT);
		}
		//
		new ThumbnailsScaleHandler(sourceImageFile, imageScale).cropImage(getImagePhysicalPath(), cropWidth, cropHeight);
	}
	
	/**
	 * 根据图片的宽度返回缩放的值
	 * 
	 * @param originalImageWidth 原始图片的宽度
	 * @return
	 */
	private int getScaleNumber(int originalImageWidth){
		if(originalImageWidth > 1440){
			return 25;
		}
		if(originalImageWidth > 992){
			return 50;
		}
		return 75;
	}
	
	/**
	 * 返回图片裁剪后的存储目录名
	 * @return
	 */
	private String getCropDirect(){
		if(scaleParameter.indexOf(ThumbConstant.WIDTH_HEIGHT_SPLITER) == -1){
			return imageScale+"";
		}
		return scaleParameter; //width<x>height
	}
}

ThumbnailsScaleHandler类使用了Thumbnails框架来缩放图片。更多关于Thumbnails访问: https://github.com/coobird/thumbnailator.
如果scale=auto,根据图片的宽度来适当的裁剪,getScaleNumber方法来计算得出。ThumbnailsScaleHandler类的代码如下:

public class ThumbnailsScaleHandler extends AbstractCropImageHandler{
	//图片的缩放比例(1-100)
	private final int scale;
	private final static Logger logger = LoggerFactory.getLogger(ThumbnailsScaleHandler.class);
	
	public ThumbnailsScaleHandler(File sourceImagePath, int scale) {
		super(sourceImagePath);
		this.scale = scale;
	}
	
	@Override
	public void cropImage(File cropSaveFile, int width, int height) throws IOException{
		String fileName = getFileName(cropSaveFile); 
		String fileExt = getFileExtension(fileName);
		File sourceImageFile = getOrginalImageFile();
		
		//("[Thumb][TSH]Thumbnails.cropImage 参数: {w:"+width+", h:"+height+", scale:"+scale+", ext:"+fileExt+", name:"+fileName+"}");
		if(width > 0 && height > 0){ //固定大小
			if(logger.isDebugEnabled()){
				String descrip = "[Thumbnails]" + ThumbConstant.NEWLINE;
						descrip += "/*----------------------------------------------------------------------*/" +ThumbConstant.NEWLINE;
						descrip += "width: " + width + ", height: " + height + ThumbConstant.NEWLINE;
						descrip += "ext: " + fileExt + ThumbConstant.NEWLINE;
						descrip += "source: " + sourceImageFile.getAbsolutePath() + ThumbConstant.NEWLINE;
						descrip += "thumb: " + cropSaveFile.getAbsolutePath() + ThumbConstant.NEWLINE;
						descrip += "/*----------------------------------------------------------------------*/" + ThumbConstant.NEWLINE;
				logger.debug(descrip);
			}
			//
			Thumbnails.of(sourceImageFile)
						.size(width, height)
						.outputFormat(fileExt)
						.toFile(cropSaveFile);
		}else{
			if(logger.isDebugEnabled()){
				String descrip = "[Thumbnails]" + ThumbConstant.NEWLINE;
				descrip += "/*----------------------------------------------------------------------*/" + ThumbConstant.NEWLINE;
				descrip += "scale: " +  scale + ThumbConstant.NEWLINE;
				descrip += "ext: " + fileExt + ThumbConstant.NEWLINE;
				descrip += "source: " + sourceImageFile.getAbsolutePath() + ThumbConstant.NEWLINE;
				descrip += "thumb: " + cropSaveFile.getAbsolutePath() + ThumbConstant.NEWLINE;
				descrip += "/*----------------------------------------------------------------------*/" + ThumbConstant.NEWLINE;
				logger.debug(descrip);
			}
			Thumbnails.of(sourceImageFile)
						.scale(Math.abs(scale) / 100.00D)
						.outputFormat(fileExt)
						.toFile(cropSaveFile);
		}
	}
}

这里需要注意一下即使Thumbnails有调用size也不会裁剪成固定的宽和高,它还是缩放到一个宽和高。裁剪出固定的宽和高的图片其实并不难,这里给出一个从中心点开始裁剪的思路

在这里插入图片描述
实现类代码如下:

public class FixedCenterCropHandler extends AbstractCropImageHandler{
	public FixedCenterCropHandler(File orginalImageFile) {
		super(orginalImageFile);
	}
	
	@Override
	public void cropImage(File cropSaveFile, int width, int height) throws IOException{
		BufferedImage originalImage = ImageIO.read(getOrginalImageFile());
		int[] xyPointers = getCropXYPointer(originalImage.getWidth(), originalImage.getHeight(), width, height);
		if(xyPointers.length == 0){
			return;
		}
		BufferedImage croppedImage = originalImage.getSubimage(xyPointers[0], xyPointers[0], width, height);
		String fileExt = getFileExtension(getFileName(cropSaveFile));
		ImageIO.write(croppedImage, fileExt, cropSaveFile);
	}
	
	/**
	 * 获取裁剪的起始点坐标
	 * 
	 * @param originalImageWidth  原始图片的宽度
	 * @param originalImageHeight 原始图片的高度
	 * @param width               裁剪的宽度
	 * @param height              裁剪的高度
	 * @return
	 */
	private int[] getCropXYPointer(int originalImageWidth, int originalImageHeight, int width, int height){
		if(originalImageWidth <= width && originalImageHeight <= height){
			return new int[]{};
		}
		
		int startX=0;
		if(originalImageWidth > width){
			int centerX = originalImageWidth / 2; //宽度中心点X
			startX = centerX - width / 2;
		}
		int startY=0;
		if(originalImageHeight > height){
			int centerY = originalImageHeight / 2; //高度中心点Y
			startY = centerY - height / 2;
		}
		return new int[]{startX, startY};
	}
}

这并不是一个很美的思路,因为不是所有图片中心物都出现在中心点左右,例如:一些风景画往往在右下或右上有动物。

E. 图片URL地址重写

这里介绍使用: urlrewritefilter 框架,官方网站: http://www.tuckey.org/urlrewrite/
若裁剪的ThumbBuilderServlet配置的地址为: /thumb, 加上参数后可能是这样的: /thumb?dir=20191025&scale=auto&file=xxxx.png,我希望地址重写后地址更自然,例:http://pic.test.com/thumbs/20191025/auto/xxxx.png,这样更直观一些。

        <dependency>
			<groupId>org.tuckey</groupId>
			<artifactId>urlrewritefilter</artifactId>
			<version>4.0.3</version>
		</dependency>

像thumbnail模块的使用一样,它也随是图片存储走,加载入图片绑定的项目中。在绑定的项目的web.xml中加入以下:

    <filter>
        <filter-name>UrlRewriteFilter</filter-name>
        <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>UrlRewriteFilter</filter-name>
        <url-pattern>/thumbs/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>

框架还需要在WEB-INF下有一个配置文件:urlrewrite.xml,示例代码如下:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 4.0//EN"
        "http://www.tuckey.org/res/dtds/urlrewrite4.0.dtd">
    <urlrewrite>
        <rule match-type="regex">
           <name>imageServlet</name>
           <from>/thumbs/(.*)/(.*)/(.*)\.(png|jpg|jpeg|gif)$</from>
           <to type="forward">/thumb?dir=$1&amp;file=$3.$4&amp;scale=$2</to>
        </rule>
    </urlrewrite>

F. 浏览器中图片的懒加载

这里介绍使用: Lazy Load Js框架,官方网站:https://github.com/tuupola/lazyload. 先看滚动加载效果图:
在这里插入图片描述
在这里插入图片描述
注意滚动条的位置,及网络加载的图片。使用方法也及其简单,这里都不贴了。

G 总结

G1. 文章中提到的util模块下载地址

白渡网盘
CSDN资源

G2. 项目的依赖及版本

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
		<project.version>0.0.1-SNAPSHOT</project.version>
		<spring-framework.version>5.0.7.RELEASE</spring-framework.version>
		<jackson.version>2.9.8</jackson.version>
		<junit.version>4.12</junit.version>
		<log4j2.version>2.11.2</log4j2.version>
		<servlet.version>3.1.0</servlet.version>
		<fileupload.version>1.4</fileupload.version>
		<slf4j.version>1.7.25</slf4j.version>
	</properties>

G3. 20191026补充

修正:

R1: 上传时的文件扩展名检查.

ApacheFileUploadConnector|ServletPartUploadConnector
AbstractHandler(allowFileExtension方法默认实现只允许图片文件)|UploadHandler(接口新增方法:String[] allowFileExtension())

R2: 上抛上传时产生的错误信息

HandleOnUploadException(接口方法增加IO异常声明)|AbstractHandler.forceException

R3: ActionForm在上传失败时上抛错误信息. 不上传时使用默认文件名

com.apobates.forum.trident.controller.form.ActionForm.uploadAndEncodeFile
新增FileUploadFailException,类继承了IllegalStateException
控制器方法若需要知道上传错误可以捕获FileUploadFailException异常

R4: CKEditor的图片类型设置

研究了一天文档没找到配置参数。只能手写代码:

//v:4.x.x
function initCkEditor(config){
    //ETC
    if(options.plugs.indexOf('uploadimage') != -1){
        CKEDITOR.config.filebrowserUploadUrl = BASE+options.UploadUrl+'?type=Files';
        CKEDITOR.config.filebrowserImageUploadUrl = BASE+options.UploadUrl+'?type=Images';
        //ADD 20191026
        CKEDITOR.on('dialogDefinition', function( evt ) {
          var dialogName = evt.data.name;
          if (dialogName == 'image') {
            var uploadTab =  evt.data.definition.getContents('Upload'); // get tab of the dialog
            var browse = uploadTab.get('upload'); //get browse server button

            browse.onClick = function() {
              var input = this.getInputElement();
              input.$.accept = 'image/*'
            };

            browse.onChange = function(){
              var input = this.getInputElement();
              var fn = input.$.value; // 文件路径
              var imageReg = /\.(gif|jpg|jpeg|png)$/i;
              if(!imageReg.test(fn)){
                alert('非法的文件类型');
                input.$.value='';
              }
            };
          }
        });
    }
 }

R5: 修复CKEditor的错误响应格式

com.apobates.forum.trident.controller.UploadController.buildCkeditorResponse
com.apobates.forum.bucket.servlet.UploadEditorFileServlet.buildCkeditorResponse
com.apobates.forum.trident.fileupload.OutsideImageStorage.parseFileURL

R6: 上传保存的文件名更改,原来是写死的

CKEditorHandler|CKEditorHightHandler,两者的父类:AbstractHandler默认方法实现输出null
ApacheFileUploadConnector|ServletPartUploadConnector
未写包名的即为util模块中的修改,反之即为本文提及的代码。 修正后的util模块在白渡网盘中更新

H. 有关MaxUploadSizeExceededException

不论是使用Servlet 3 Part还是ASF commons fileupload框架都有可能在设置文件大小(除了不设置,默认是-1不限制)时出来这个异常,这时连接会被重置。

Spring MultipartFile时出现的异常:

nested exception is org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size of 5242880 bytes exceeded; 
nested exception is org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (8718571) exceeds the configured maximum (5242880)] with root cause
 org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (8718571) exceeds the configured maximum (5242880)
        at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:977)
        at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:309)
        at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:333)
        at org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:113)
        at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:158)
        at org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:142)
        at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1128)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:960)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)

只使用fileupload时出现的异常:

java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (12119773) exceeds the configured maximum (1048576)
        at org.apache.catalina.connector.Request.parseParts(Request.java:2947)
        at org.apache.catalina.connector.Request.parseParameters(Request.java:3242)
        at org.apache.catalina.connector.Request.getParameter(Request.java:1136)
        at org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:381)
        at com.apobates.forum.utils.fileupload.ServletPartUploadConnector.upload(ServletPartUploadConnector.java:81)
        at com.apobates.forum.bucket.servlet.UploadEditorFileServlet.doPost(UploadEditorFileServlet.java:71)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)

这时正常的错误提示不会由response输出,网络有一堆如下的解答:

H1. 1兆不行10兆,通过提升允许的上限

个人不建议,这是不是有点妥协的意味呢?

H2. 使用ExceptionHandler

    @ExceptionHandler(MultipartException.class)
    public String handleError1(MultipartException e, RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("message", e.getCause().getMessage());
        return "redirect:/uploadStatus";
    }

源文地址: How to handle max upload size exceeded exception, 我试了一下电脑里的浏览器只有win10 自带Edge会正常工作, Firefox, Chrome都没效果。

H3. 继承CommonsMultipartResolver 法

public class DropOversizeFilesMultipartResolver extends CommonsMultipartResolver {
    /**
     * Parse the given servlet request, resolving its multipart elements.
     * 
     * Thanks Alexander Semenov @ http://forum.springsource.org/showthread.php?62586
     * 
     * @param request
     *            the request to parse
     * @return the parsing result
     */
    @Override
    protected MultipartParsingResult parseRequest(final HttpServletRequest request) {
        String encoding = determineEncoding(request);
        FileUpload fileUpload = prepareFileUpload(encoding);
        List fileItems;
        try {
            fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
        } catch (FileUploadBase.SizeLimitExceededException ex) {
            request.setAttribute(EXCEPTION_KEY, ex);
            fileItems = Collections.EMPTY_LIST;
        } catch (FileUploadException ex) {
            throw new MultipartException("Could not parse multipart servlet request", ex);
        }
        return parseFileItems(fileItems, encoding);
    }
}

源文地址: Using Spring 3 @ExceptionHandler with commons FileUpload and SizeLimitExceededException/MaxUploadSizeExceededException, 我用的Spring5上不好使,所有浏览器都会提示连接重置,我试着在调用parseRequest之前判断上传文件的大小,若超出设置的上限上抛MultipartException,还是提示连接被重置。

H4. 继承OncePerRequestFilter 法

public class MultipartExceptionHandler extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (MaxUploadSizeExceededException e) {
            handle(request, response, e);
        } catch (ServletException e) {
            if(e.getRootCause() instanceof MaxUploadSizeExceededException) {
                handle(request, response, (MaxUploadSizeExceededException) e.getRootCause());
            } else {
                throw e;
            }
        }
    }
    private void handle(HttpServletRequest request,
            HttpServletResponse response, MaxUploadSizeExceededException e) throws ServletException, IOException {
        String redirect = UrlUtils.buildFullRequestUrl(request) + "?error";
        response.sendRedirect(redirect);
    }
}

源文地址: How to nicely handle file upload MaxUploadSizeExceededException with Spring Security

H5. 个人方案

即然不论Part,ASF common fileupload或Spring MultipartResolver在超出允许的上限时都会重置连接,说明这可能是上佳的可行方案,出于什么原因这样作不是我辈菜鸟可想到的。但又需要在超出上限时给用户一个提示,操作得以继续进行而不是让用户看浏览器的错误页:
在这里插入图片描述
在这里插入图片描述
== 这里说明一下:我试着用http watch之类的工具看一看网络情况。发现只要开了代理都可以一遍跑过。错误提示原本不会响应的也出来了。==

即然服务器上无法作文章,又需要给用户一个提示,又不要影响操作继续只能选用客户端异步上传。即使上传的连接被重置,用户当前的操作也不会因此打断,像CKEditor哪样:
在这里插入图片描述
这里推荐一下: bootstrap-fileinput, 它可以在客户端对文件大小检测,只不过单位是字节,示例如下

在这里插入图片描述
JS代码示例:

    $('.bootCoverImage').bind('initEvent', function(){
        var self=$(this); var token=new Date().getTime();
        var option={
                uploadUrl: BASE+"upload/fileinput",
                uploadExtraData: {'uploadToken':token, 'id':token},
                maxFileCount: 5,
                maxFileSize: 1024,
                maxFilesNum: 1,
                allowedFileTypes: ['image'],
                allowedFileExtensions: ['jpg', 'png', 'gif', 'jpeg'],
                overwriteInitial: true,
                theme: 'fa',
                uploadAsync: true,
                showPreview: true
        };
        if(self.attr('data-bind')){
            option.initialPreviewAsData=true;
            option.initialPreview=[self.attr('data-bind')];
        }
        if(self.attr('data-element')){
            hiddenElement=self.attr('data-element');
        }
        var coverCmp=self.fileinput(option);
        coverCmp.on('fileuploaded', function(event, data, id, index) {
            var response=data.response,
            currentImage=response.data[0].location;
            //currentImage即为上传成功后图片的访问地址;
        }).on('fileuploaderror', function(event, data, msg){
            console.log('File Upload Error', 'ID: ' + data.fileId + ', Thumb ID: ' + data.previewId);
        }).on('filebatchuploadcomplete', function(event, preview, config, tags, extraData) {
            console.log('File Batch Uploaded', preview, config, tags, extraData);
        });
    }).trigger('initEvent');

上传成功后服务器站的响应格式如下:

{
"data":[
  {
    "id":"101",
    "name":"snapshot-20191101000913773.png",
    "location":"http://www.test.com/imagestore/20191101/snapshot-20191101000913773.png"
}],
"success":true
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值