用文件模型绑定接口:IFormFile (小文件上传)


当你使用IFormFile接口来上传文件的时候,一定要注意,IFormFile会将一个Http请求中的所有文件都读取到服务器内存后,才会触发ASP.NET Core MVC的Controller中的Action方法。这种情况下,如果上传一些小文件是没问题的,但是如果上传大文件,势必会造成服务器内存大量被占用甚至溢出,所以IFormFile接口只适合小文件上传。

一个文件上传页面的Html代码一般如下所示:

<form method="post" enctype="multipart/form-data" action="/Upload">
<div>
<p>Upload one or more files using this form:</p>
<input type="file" name="files" />
</div>
<div>
<input type="submit" value="Upload" />
</div>
</form>

为了支持文件上传,form标签上一定要记得声明属性enctype="multipart/form-data",否则你会发现ASP.NET Core MVC的Controller中死活都读不到任何文件。Input type="file"标签在html 5中支持上传多个文件,加上属性multiple即可。

使用IFormFile接口上传文件非常简单,将其声明为Controller中Action的集合参数即可:

[HttpPost]
public async Task<IActionResult> Post(List<IFormFile> files)
{
long size = files.Sum(f => f.Length); foreach (var formFile in files)
{
var filePath = @"F:\UploadingFiles\" + formFile.FileName.Substring(formFile.FileName.LastIndexOf("\\") + );//注意formFile.FileName包含上传文件的文件路径,所以要进行Substring只取出最后的文件名 if (formFile.Length > )
{
using (var stream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(stream);
}
}
} return Ok(new { count = files.Count, size });
}

注意上面Action方法Post的参数名files,必须要和上传页面中的Input type="file"标签的name属性值一样。

不要直接用Request.Form.Files

上面例子是我们知道Input type="file"标签的name属性值时的情况,如果你不知道Input type="file"标签的name属性值(例如前端用javascript动态生成的Input type="file"标签),有什么办法可以获取所有的上传文件吗?

也许有同学会想到可以用Request.Form.Files来获取当前Http请求中,所有的上传文件,如下所示:

[HttpPost]
public async Task<IActionResult> Post()
{
IFormFileCollection files = Request.Form.Files;
long size = files.Sum(f => f.Length); foreach (var formFile in files)
{
var filePath = @"F:\UploadingFiles\" + formFile.FileName.Substring(formFile.FileName.LastIndexOf("\\") + ); if (formFile.Length > )
{
using (var stream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(stream);
}
}
} return Ok(new { count = files.Count, size });
}

然后执行上面的代码你会发现,代码执行到Request.Form.Files的时候,就一直卡住了,如下所示:

然后现在我们给Post方法随便加一个参数string parameter,如下所示:

[HttpPost]
public async Task<IActionResult> Post(string parameter)
{
IFormFileCollection files = Request.Form.Files;
long size = files.Sum(f => f.Length); foreach (var formFile in files)
{
var filePath = @"F:\UploadingFiles\" + formFile.FileName.Substring(formFile.FileName.LastIndexOf("\\") + ); if (formFile.Length > )
{
using (var stream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(stream);
}
}
} return Ok(new { count = files.Count, size });
}

运行代码,你会发现虽然Post方法的参数string parameter没得到任何值为null,但是这次Post方法却没有卡在Request.Form.Files,文件上传成功,Post方法成功执行完毕:

这是因为当ASP.NET Core MVC中Controller的Action方法没有定义参数的时候,Request.Form不会做数据绑定,也就是说当我们在上面Post方法没有定义参数的时候,Request.Form根本就没有被ASP.NET Core初始化,所以只要一访问Request.Form代码就会被卡住,所以当我们随便给Post方法定义一个string parameter参数后,Request.Form就被初始化了,这时就可以访问Request.Form中的数据了。

既然必须要给Post方法定义参数,那我们就定义有意义的参数,而不是胡乱定义一个没有用的。我们将Post方法的代码改为如下:

[HttpPost]
public async Task<IActionResult> Post([FromForm]IFormCollection formData)
{
IFormFileCollection files = formData.Files;//等价于Request.Form.Files long size = files.Sum(f => f.Length); foreach (var formFile in files)
{
var inputName = formFile.Name;//可以通过IFormFile.Name属性获得每个上传文件,在页面上所属Input type="file"标签的name属性值
var filePath = @"F:\UploadingFiles\" + formFile.FileName.Substring(formFile.FileName.LastIndexOf("\\") + ); if (formFile.Length > )
{
using (var stream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(stream);
}
}
} return Ok(new { count = files.Count, size });
}

我们给Post方法定义了一个IFormCollection类型的参数formData,并且标记了[FromForm]特性标签,表示IFormCollection formData参数使用Http请求中的表单(Form)数据进行初始化,所以这下formData其实就等价于Request.Form了。

我们可以从formData中访问表单(Form)提交的任何数据,获得所有的上传文件。其实Post方法的参数名字叫什么并不重要(本例中我们取名为formData),但是其参数必须是IFormCollection类型才会绑定Http请求中的表单(Form)数据,这才是关键。

执行上面的代码,文件成功上传,代码成功执行完毕:

用文件流 (大文件上传)


在介绍这个方法之前我们先来看看一个包含上传文件的Http请求是什么样子的:

Content-Type=multipart/form-data; boundary=---------------------------99614912995
-----------------------------99614912995
Content-Disposition: form-data; name="SOMENAME" Formulaire de Quota
-----------------------------99614912995
Content-Disposition: form-data; name="OTHERNAME" SOMEDATA
-----------------------------99614912995
Content-Disposition: form-data; name="files"; filename="Misc 001.jpg" SDFESDSDSDJXCK+DSDSDSSDSFDFDF423232DASDSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995
Content-Disposition: form-data; name="files"; filename="Misc 002.jpg" ASAADSDSDJXCKDSDSDSHAUSAUASAASSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995
Content-Disposition: form-data; name="files"; filename="Misc 003.jpg" TGUHGSDSDJXCK+DSDSDSSDSFDFDSAOJDIOASSAADDASDASDASSADASDSDSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995--

这就是一个multipart/form-data格式的Http请求,我们可以看到第一行信息是Http header,这里我们只列出了Content-Type这一行Http header信息,这和我们在html页面中form标签上的enctype属性值一致,第一行中接着有一个boundary=---------------------------99614912995,boundary=后面的值是随机生成的,这个其实是在声明Http请求中表单数据的分隔符是什么,其代表的是在Http请求中每读到一行 ---------------------------99614912995,表示一个section数据,一个section有可能是一个表单的键值数据,也有可能是一个上传文件的文件数据。每个section的第一行是section header,其中Content-Disposition属性都为form-data,表示这个section来自form标签提交的表单数据,如果section header拥有filename或filenamestar属性,那么表示这个section是一个上传文件的文件数据,否则这个section是一个表单的键值数据,section header之后的行就是这个section真正的数据行。例如我们上面的例子中,前两个section就是表单键值对,后面三个section是三个上传的图片文件。

那么接下来,我们来看看怎么用文件流来上传大文件,避免一次性将所有上传的文件都加载到服务器内存中。用文件流来上传比较麻烦的地方在于你无法使用ASP.NET Core MVC的模型绑定器来将上传文件反序列化为C#对象(如同前面介绍的IFormFile接口那样)。首先我们需要定义类MultipartRequestHelper,用于识别Http请求中的各个section类型(是表单键值对section,还是上传文件section)

using System;
using System.IO;
using Microsoft.Net.Http.Headers; namespace AspNetCore.MultipartRequest
{
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec says 70 characters is a reasonable limit.
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
//var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary);// .NET Core <2.0
var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; //.NET Core 2.0
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
} //注意这里的boundary.Length指的是boundary=---------------------------99614912995中等号后面---------------------------99614912995字符串的长度,也就是section分隔符的长度,上面也说了这个长度一般不会超过70个字符是比较合理的
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
} return boundary;
} public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= ;
} //如果section是表单键值对section,那么本方法返回true
public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value"
&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); // For .NET Core <2.0 remove ".Value"
} //如果section是上传文件section,那么本方法返回true
public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value"
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); // For .NET Core <2.0 remove ".Value"
} // 如果一个section的Header是: Content-Disposition: form-data; name="files"; filename="F:\Misc 002.jpg"
// 那么本方法返回: files
public static string GetFileContentInputName(ContentDispositionHeaderValue contentDisposition)
{
return contentDisposition.Name.Value;
} // 如果一个section的Header是: Content-Disposition: form-data; name="myfile1"; filename="F:\Misc 002.jpg"
// 那么本方法返回: Misc 002.jpg
public static string GetFileName(ContentDispositionHeaderValue contentDisposition)
{
return Path.GetFileName(contentDisposition.FileName.Value);
}
}
}

然后我们需要定义一个扩展类叫FileStreamingHelper,其中的StreamFiles扩展方法用于读取上传文件的文件流数据,并且将数据写入到服务器的硬盘上,其接受一个参数targetDirectory,用于声明将上传文件存储到服务器的哪个文件夹下。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks; namespace AspNetCore.MultipartRequest
{
public static class FileStreamingHelper
{
private static readonly FormOptions _defaultFormOptions = new FormOptions(); public static async Task<FormValueProvider> StreamFiles(this HttpRequest request, string targetDirectory)
{
if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))
{
throw new Exception($"Expected a multipart request, but got {request.ContentType}");
} // Used to accumulate all the form url encoded key value pairs in the
// request.
var formAccumulator = new KeyValueAccumulator(); var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, request.Body); var section = await reader.ReadNextSectionAsync();//用于读取Http请求中的第一个section数据
while (section != null)
{
ContentDispositionHeaderValue contentDisposition;
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition); if (hasContentDispositionHeader)
{
/*
用于处理上传文件类型的的section
-----------------------------99614912995
Content - Disposition: form - data; name = "files"; filename = "Misc 002.jpg" ASAADSDSDJXCKDSDSDSHAUSAUASAASSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995
*/
if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
{
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
} var fileName = MultipartRequestHelper.GetFileName(contentDisposition); var loadBufferBytes = ;//这个是每一次从Http请求的section中读出文件数据的大小,单位是Byte即字节,这里设置为1024的意思是,每次从Http请求的section数据流中读取出1024字节的数据到服务器内存中,然后写入下面targetFileStream的文件流中,可以根据服务器的内存大小调整这个值。这样就避免了一次加载所有上传文件的数据到服务器内存中,导致服务器崩溃。 using (var targetFileStream = System.IO.File.Create(targetDirectory + "\\" + fileName))
{
//section.Body是System.IO.Stream类型,表示的是Http请求中一个section的数据流,从该数据流中可以读出每一个section的全部数据,所以我们下面也可以不用section.Body.CopyToAsync方法,而是在一个循环中用section.Body.Read方法自己读出数据(如果section.Body.Read方法返回0,表示数据流已经到末尾,数据已经全部都读取完了),再将数据写入到targetFileStream
await section.Body.CopyToAsync(targetFileStream, loadBufferBytes);
} }
/*
用于处理表单键值数据的section
-----------------------------99614912995
Content - Disposition: form - data; name = "SOMENAME" Formulaire de Quota
-----------------------------99614912995
*/
else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
{
// Content-Disposition: form-data; name="key"
//
// value // Do not limit the key name length here because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
var encoding = GetEncoding(section);
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: ,
leaveOpen: true))
{
// The value length limit is enforced by MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
{
value = String.Empty;
}
formAccumulator.Append(key.Value, value); // For .NET Core <2.0 remove ".Value" from key if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
{
throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
}
}
}
} // Drains any remaining section body that has not been consumed and
// reads the headers for the next section.
section = await reader.ReadNextSectionAsync();//用于读取Http请求中的下一个section数据
} // Bind form data to a model
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture); return formValueProvider;
} private static Encoding GetEncoding(MultipartSection section)
{
MediaTypeHeaderValue mediaType;
var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
// UTF-7 is insecure and should not be honored. UTF-8 will succeed in
// most cases.
if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
{
return Encoding.UTF8;
}
return mediaType.Encoding;
}
}
}

现在我们还需要创建一个ASP.NET Core MVC的自定义拦截器DisableFormValueModelBindingAttribute,该拦截器实现接口IResourceFilter,用来禁用ASP.NET Core MVC的模型绑定器,这样当一个Http请求到达服务器后,ASP.NET Core MVC就不会在将请求的所有上传文件数据都加载到服务器内存后,才执行Controller的Action方法,而是当Http请求到达服务器时,就立刻执行Controller的Action方法。

ASP.NET Core 2.X使用下面的代码:

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Linq; namespace AspNetCore.MultipartRequest
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var formValueProviderFactory = context.ValueProviderFactories
.OfType<FormValueProviderFactory>()
.FirstOrDefault();
if (formValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(formValueProviderFactory);
} var jqueryFormValueProviderFactory = context.ValueProviderFactories
.OfType<JQueryFormValueProviderFactory>()
.FirstOrDefault();
if (jqueryFormValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory);
}
} public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
}

ASP.NET Core 3.X使用下面的代码:

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Linq; namespace AspNetCore.MultipartRequest
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
} public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
}

最后我们在Controller中定义一个叫Index的Action方法,并注册我们定义的DisableFormValueModelBindingAttribute拦截器,来禁用Action的模型绑定。Index方法会调用我们前面定义的FileStreamingHelper类中的StreamFiles方法,其参数为用来存储上传文件的文件夹路径。StreamFiles方法会返回一个FormValueProvider,用来存储Http请求中的表单键值数据,之后我们会将其绑定到MVC的视图模型viewModel上,然后将viewModel传回给客户端浏览器,来告述客户端浏览器文件上传成功。

[HttpPost]
[DisableFormValueModelBinding]
public async Task<IActionResult> Index()
{
FormValueProvider formModel;
formModel = await Request.StreamFiles(@"F:\UploadingFiles"); var viewModel = new MyViewModel(); var bindingSuccessful = await TryUpdateModelAsync(viewModel, prefix: "",
valueProvider: formModel); if (!bindingSuccessful)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
} return Ok(viewModel);
}

视图模型viewModel的定义如下:

public class MyViewModel
{
public string Username { get; set; }
}

最后我们用于上传文件的html页面和前面几乎一样:

<form method="post" enctype="multipart/form-data" action="/Home/Index">
<div>
<p>Upload one or more files using this form:</p>
<input type="file" name="files" multiple />
</div>
<div>
<p>Your Username</p>
<input type="text" name="username" />
</div>
<div>
<input type="submit" value="Upload" />
</div>
</form>

这就是所有的代码,希望对大家有所帮助!

参考文献:

File uploads in ASP.NET Core

Uploading Files In ASP.net Core

What is the boundary parameter in an HTTP multi-part (POST) Request?

ASP.NET Core MVC如何上传文件及处理大文件上传的更多相关文章

  1. ASP.NET Core MVC压缩样式、脚本及总是复制文件到输出目录

    前言 在.NET Core之前对于压缩样式文件和脚本我们可能需要借助第三方工具来进行压缩,但在ASP.NET MVC Core中则无需借助第三方工具来完成,本节我们来看看ASP.NET Core MV ...

  2. 002.Create a web API with ASP.NET Core MVC and Visual Studio for Windows -- 【在windows上用vs与asp.net core mvc 创建一个 web api 程序】

    Create a web API with ASP.NET Core MVC and Visual Studio for Windows 在windows上用vs与asp.net core mvc 创 ...

  3. 004.Create a web app with ASP.NET Core MVC using Visual Studio on Windows --【在 windows上用VS创建mvc web app】

    Create a web app with ASP.NET Core MVC using Visual Studio on Windows 在 windows上用VS创建mvc web app 201 ...

  4. .NET Core 如何上传文件及处理大文件上传

    当你使用IFormFile接口来上传文件的时候,一定要注意,IFormFile会将一个Http请求中的所有文件都读取到服务器内存后,才会触发ASP.NET Core MVC的Controller中的A ...

  5. ASP.NET Core MVC 模型绑定 (转载)

    ASP.NET Core MVC的Model Binding会将HTTP Request数据,以映射的方式对应到参数中.基本上跟ASP.NET MVC差不多,但能Binding的来源更多了一些.本篇将 ...

  6. asp.net core mvc 读取配置文件appsettings.json

    上一篇我们将了读取自定义配置文件.这篇我们讲一下asp.net core mvc里读取自带的配置文件 appsettings.json 首先创建个asp.net core mvc项目,项目里有Prog ...

  7. ASP.NET Core MVC的Razor视图中,使用Html.Raw方法输出原生的html

    我们在ASP.NET Core MVC项目中,有一个Razor视图文件Index.cshtml,如下: @{ Layout = null; } <!DOCTYPE html> <ht ...

  8. 解决ASP.NET Core Mvc文件上传限制问题

    一.简介 在ASP.NET Core MVC中,文件上传的最大上传文件默认为20MB,如果我们想上传一些比较大的文件,就不知道怎么去设置了,没有了Web.Config我们应该如何下手呢? 二.设置上传 ...

  9. ASP.NET Core MVC上传、导入、导出知多少

    前言 本君已成夜猫子,本节我们来讲讲ASP.NET Core MVC中的上传,这两天才研究批量导入功能,本节顺便简单搞搞导入.导出,等博主弄妥当了再来和大家一并分享. .NET Core MVC上传 ...

随机推荐

  1. Java并发—–深入分析synchronized的实现原理

    记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字“同步”,也成为了我们解决多线 ...

  2. 查询多表集合(union)、查询时建临时字段、查询时给字段设置默认值

    () UNION () UNION (select i.create_time as time,i.investment_amount as amount,i.invest_state as stat ...

  3. 虽然我们可能不想对元素应用3D变换,可我们一样可以开启3D引擎

    例如我们可以用transform: translateZ(0); 来开启硬件加速 ..cube {-webkit-transform: translateZ(0);-moz-transform: tr ...

  4. 弹性布局(flex)

    一.Flex 布局是什么? Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性. 任何一个容器都可以指定为 Flex 布局.但在使用时 ...

  5. 004声明式服务调用Feign & 断路器Hystrix

    1.POM配置 和普通Spring Boot工程相比,添加了Eureka Client.Feign.Hystrix依赖和Spring Cloud依赖管理 <dependencies> &l ...

  6. JavaScript中实现DI的原理

    什么是依赖注入 按照上面图的流程中我们可以知道我们需要实现这么几件事: 提供一个服务容器 为目标函数注册需要的依赖 获取目标函数注册的依赖项 通过依赖项来查询对应服务 将获取的依赖项传入目标函数 提供 ...

  7. 为TextView增加onclick

    必须添加上 android:clickable="true"

  8. Tesseract-OCR-03-图片文字识别

    Tesseract-OCR-03-图片文字识别 本篇介绍使用 Tesseract-OCR 做图片文字识别,识别手写文字的时候,正确率能达到 90%,当训练后正确率是极高的.这里介绍的图片文字识别,可以 ...

  9. Unity导出APk出错解决方法二

    错误提示(需得打开编辑器log文件才能看到全部log,Unity3d只显示一部分): Error building Player: CommandInvokationFailure: Unable t ...

  10. 六、使用media实现响应式布局

    常见写法: 下面总结常见的响应式布局的分类: @media screen and (max-width:320px){ #talkFooter .editArea{…… } } @media scre ...