今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受。这么好的光阴怎么浪费,睡觉、吃饭、打豆豆这怎么可能(耍多了也烦),完全不符合我们程序员的作风,赶紧起来把文章写完。
这篇文章比较基础,在国庆期间的业余时间写的,这几天又完善了下,力求把更多的前端所涉及到的关于文件上传的各种场景和应用都涵盖了,若有疏漏和问题还请留言斧正和补充。
以下是本文所涉及到的知识点,break or continue ?
原理很简单,就是根据 http 协议的规范和定义,完成请求消息体的封装和消息体的解析,然后将二进制内容保存到文件。
我们都知道如果要上传一个文件,需要把 form 标签的enctype设置为multipart/form-data,同时method必须为post方法。
那么multipart/form-data表示什么呢?
multipart互联网上的混合资源,就是资源由多种元素组成,form-data表示可以使用HTML Forms 和 POST 方法上传文件,具体的定义可以参考RFC 7578。
multipart/form-data 结构
看下 http 请求的消息体
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN 表示本次请求要上传文件,其中boundary表示分隔符,如果要上传多个表单项,就要使用boundary分割,每个表单项由———XXX开始,以———XXX结尾。
每一个表单项又由Content-Type和Content-Disposition组成。
Content-Disposition: form-data 为固定值,表示一个表单元素,name 表示表单元素的 名称,回车换行后面就是name的值,如果是上传文件就是文件的二进制内容。
Content-Type:表示当前的内容的 MIME 类型,是图片还是文本还是二进制数据。
解析
客户端发送请求到服务器后,服务器会收到请求的消息体,然后对消息体进行解析,解析出哪是普通表单哪些是附件。
可能大家马上能想到通过正则或者字符串处理分割出内容,不过这样是行不通的,二进制buffer转化为string,对字符串进行截取后,其索引和字符串是不一致的,所以结果就不会正确,除非上传的就是字符串。
不过一般情况下不需要自行解析,目前已经有很成熟的三方库可以使用。
至于如何解析,这个也会占用很大篇幅,后面的文章在详细说。
使用 form 表单上传文件
在 ie时代,如果实现一个无刷新的文件上传那可是费老劲了,大部分都是用 iframe 来实现局部刷新或者使用 flash 插件来搞定,在那个时代 ie 就是最好用的浏览器(别无选择)。
DEMO
这种方式上传文件,不需要 js ,而且没有兼容问题,所有浏览器都支持,就是体验很差,导致页面刷新,页面其他数据丢失。
HTML
<form method="post" action="http://localhost:8100" enctype="multipart/form-data">
选择文件:
<input type="file" name="f1"/> input 必须设置 name 属性,否则数据无法发送<br/>
<br/>
标题:<input type="text" name="title"/><br/><br/><br/>
<button type="submit" id="btn-0">上 传</button>
</form>
复制代码
服务端文件的保存基于现有的库koa-body结合 koa2实现服务端文件的保存和数据的返回。
在项目开发中,文件上传本身和业务无关,代码基本上都可通用。
在这里我们使用koa-body库来实现解析和文件的保存。
koa-body 会自动保存文件到系统临时目录下,也可以指定保存的文件路径。
然后在后续中间件内得到已保存的文件的信息,再做二次处理。
NODE
/**
* 服务入口
*/
var http = require('http');
var koaStatic = require('koa-static');
var path = require('path');
var koaBody = require('koa-body');//文件保存库
var fs = require('fs');
var Koa = require('koa2');
var app = new Koa();
var port = process.env.PORT || '8100';
var uploadHost= `http://localhost:${port}/uploads/`;
app.use(koaBody({
formidable: {
//设置文件的默认保存目录,不设置则保存在系统临时目录下 os
uploadDir: path.resolve(__dirname, '../static/uploads')
},
multipart: true // 开启文件上传,默认是关闭
}));
//开启静态文件访问
app.use(koaStatic(
path.resolve(__dirname, '../static')
));
//文件二次处理,修改名称
app.use((ctx) => {
var file = ctx.request.files.f1;//得道文件对象
var path = file.path;
var fname = file.name;//原文件名称
var nextPath = path+fname;
if(file.size>0 && path){
//得到扩展名
var extArr = fname.split('.');
var ext = extArr[extArr.length-1];
var nextPath = path+'.'+ext;
//重命名文件
fs.renameSync(path, nextPath);
}
//以 json 形式输出上传文件地址
ctx.body = `{
"fileUrl":"${uploadHost}${nextPath.slice(nextPath.lastIndexOf('/')+1)}"
}`;
});
/**
* http server
*/
var server = http.createServer(app.callback());
server.listen(port);
console.log('demo1 server start ...... ');
复制代码
CODE
https://github.com/Bigerfe/fe-learn-code/
了执行Javascript,需要在HTML文件内以特定的方式书写JavaScript的代码,JavaScript的书写方法有多种,其执行的流程也各不相同:
此种嵌入方法无法操作<script>之后的DOM元素。因为<script>之后的DOM元素还未构造,因此在<script>标签内就无法取得位于其后的DOM元素。
此种嵌入方法可以指定defer、async属性。defer可以推迟执行,async可以异步执行。
此种嵌入方法在页面读取完后再对其执行,所以可以对所有的DOM元素操作。
<body onload="alert('hello')">
window.onload = function(){alert('hello');};
当window.onload事件触发时,页面上所有的DOM、样式表、脚本、图片、flash都已经加载完成了。
//window.onload不能同时编写多个。
//以下代码无法正确执行,结果只输出第二个。
window.onload = function(){
alert("test1");
};
window.onload = function(){
alert("test2");
};
//$(document).ready()能同时编写多个
//结果两次都输出
$(document).ready(function(){
alert("Hello World");
});
$(document).ready(function(){
alert("Hello again");
});
window.onload和body中onload也有些许区别:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.9.0.js"></script>
<script language="javascript">
window.onload = haha;
function haha(){console.log("window.onload");}
if(document.addEventListener){
function DOMContentLoaded(){
console.log("DOMContentLoaded");
}
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
}</script>
</head>
<body onload="console.log('bodyonload');">
<div id="div1">a</div>
</body>
</html>
在IE10和FireFox下,结果为 :
"DOMContentLoaded"
"bodyonload"
说明body中的onload会覆盖window.onload
在chrome下,结果为:
DOMContentLoaded
window.onload
bodyonload
然后,如果把javascript代码移到最下面,结果又会是什么样呢?
chrome和IE10、FireFox的结果竟然是一样的:
DOMContentLoaded
window.onload
IE 10、Fire Fox可以理解,window.on load和body中的 on load 谁在下面就是谁覆盖谁,只会执行后面的那个。
onload方法可能需要等待时间,而本方法可以在完成HTML解析后发生的事件,减少等待时间。
在chrome、IE10和FireFox中,执行结果是:DOMContentLoaded然后才是onload的输出。所以说一般情况下,DOMContentLoaded事件要在window.onload之前执行,当DOM树构建完成的时候就会执行DOMContentLoaded事件。
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<script type="text/javascript" src="jquery2.js"></script>
<script language="javascript">
window.onload = haha;
function haha(){console.log(document.getElementById("div1"));}
if(document.addEventListener){
function DOMContentLoaded(){
console.log("DOMContentLoaded");
}
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
}
</script>
</head>
<body>
<div id="div1">a</div>
</body>
</html>
如果你是个jQuery使用者,你可能会经常使用$(document).ready();或者$(function(){}),这都是使用了DOMContentLoaded事件
5.1 使用原生js方法
动态创建script标签,并指定script的src属性
function loadJs(url, callback) {
var script = document.createElement('script');
script.type = "text/javascript";
if (typeof(callback) != "undefined") {
if (script.readyState) {
script.onreadystatechange = function() {
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
callback();
}
}
} else {
script.onload = function() {
callback();
}
}
}
script.src = url;
document.body.appendChild(script);
}
loadJs("test.js", function() {
alert('done');
});
还可以使用同样的原理动态加载css文件,只不过插入的的父节点是head标签。
5.2 使用document.write/writeln()方式
该种方式可以实现js文件的动态加载,原理就是在重写文档流,这种方式会导致整个页面重绘。
document.writeln("<script src=\"http://lib.sinaapp.com/js/jquery/1.6/jquery.min.js\"></script>");
需要注意的是特殊字符的转义。
5.3 使用jQuery
使用getScript(url,callback)方法实现动态加载js文件
$.getScript('test.js',function(){
alert('done');
});
-End-
ello~各位亲爱的看官老爷们大家好。估计大家都听过,尽量将 CSS 放头部, JS 放底部,这样可以提高页面的性能。然而,为什么呢?大家有考虑过么?很长一段时间,我都是知其然而不知其所以然,强行背下来应付考核当然可以,但实际应用中必然一塌糊涂。因此洗(wang)心(yang)革(bu)面(lao),小结一下最近玩出来的成果。
友情提示,本文也是小白向为主,如果直接想看结论可以拉到最下面看的~
推荐下我的前端群:524262608,不定期分享干货,包括我自己整理的一份2017最新的前端资料和零基础入门教程,欢迎初学和进阶中的小伙伴。
由于关系到文件的读取,那是肯定需要服务器的,我会把全部的文件放在 github 上,给我点个 star 我会开心!掘金上再给我点个 赞 我就更开心了~
node 端唯一需要解释一下的是这个函数:
function sleep(time) { return new Promise(function(res) { setTimeout(() => { res() }, time); }) }
嗯!其实就延时啦。如果 CSS 或者 JS 文件名有 sleep3000 之类的前缀时,意思就是延迟3000毫秒才会返回这文件。
下文使用的 HTML 文件是长这样的:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> div { width: 100px; height: 100px; background: lightgreen; } </style> </head> <body> <div></div> </body> </html>
我会在其中插入不同的 JS 和 CSS 。
而使用的 common.css ,不论有没有前缀,内容都是这样的:
div { background: red; }
好了,话不多数,开始正文!
关于 CSS ,大家肯定都知道的是 <link> 标签放在头部性能会高一点,少一点人知道如果 <script> 与 <link> 同时在头部的话, <script> 在上可能会更好。这是为什么呢?下面我们一起来看一下 CSS 对 DOM 的影响是什么。
CSS 不会阻塞 DOM 的解析
注意哦!这里说的是 DOM 解析,证明的例子如下,首先在头部插入 <script defer src="/js/logDiv.js"></script> , JS 文件的内容是:
const div = document.querySelecot('div'); console.log(div);
defer 属性相信大家也很熟悉了,MDN对此的描述是用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。设置这个属性,能保证 DOM 解析后马上打印出 div 。
之后将 <link rel="stylesheet" href="/css/sleep3000-common.css"> 插入 HTML 文件的任一位置,打开浏览器,可以看到是首先打印出 div 这个 DOM 节点,过3s左右之后才渲染出一个浅蓝色的 div 。这就证明了 CSS 是不会阻塞 DOM 的解析的,尽管 CSS 下载需要3s,但这个过程中,浏览器不会傻等着 CSS 下载完,而是会解析 DOM 的。
这里简单说一下,浏览器是解析 DOM 生成 DOM Tree ,结合 CSS 生成的 CSS Tree ,最终组成 render tree ,再渲染页面。由此可见,在此过程中 CSS 完全无法影响 DOM Tree ,因而无需阻塞 DOM 解析。然而, DOM Tree 和 CSS Tree 会组合成 render tree ,那 CSS 会不会页面阻塞渲染呢?
其实这一点,刚才的例子已经说明了,如果 CSS 不会阻塞页面阻塞渲染,那么 CSS 文件下载之前,浏览器就会渲染出一个浅绿色的 div ,之后再变成浅蓝色。浏览器的这个策略其实很明智的,想象一下,如果没有这个策略,页面首先会呈现出一个原始的模样,待 CSS 下载完之后又突然变了一个模样。用户体验可谓极差,而且渲染是有成本的。
因此,基于性能与用户体验的考虑,浏览器会尽量减少渲染的次数, CSS 顺理成章地阻塞页面渲染。
然而,事情总有奇怪的,请看这例子, HTML 头部结构如下:
<header> <link rel="stylesheet" href="/css/sleep3000-common.css"> <script src="/js/logDiv.js"></script></header>
但思考一下这会产生什么结果呢?
答案是浏览器会转圈圈三秒,但此过程中不会打印任何东西,之后呈现出一个浅蓝色的 div ,再打印出 null 。结果好像是 CSS 不单阻塞了页面渲染,还阻塞了 DOM 的解析啊!稍等,在你打算掀桌子疯狂吐槽我之前,请先思考一下是什么阻塞了 DOM 的解析,刚才已经证明了 CSS 是不会阻塞的,那么阻塞了页面解析其实是 JS !但明明 JS 的代码如此简单,肯定不会阻塞这么久,那就是 JS 在等待 CSS 的下载,这是为什么呢?
仔细思考一下,其实这样做是有道理的,如果脚本的内容是获取元素的样式,宽高等 CSS 控制的属性,浏览器是需要计算的,也就是依赖于 CSS 。浏览器也无法感知脚本内容到底是什么,为避免样式获取,因而只好等前面所有的样式下载完后,再执行 JS 。因而造成了之前例子的情况。
所以,看官大人明白为何 <script> 与 <link> 同时在头部的话, <script> 在上可能会更好了么?之所以是可能,是因为如果 <link> 的内容下载更快的话,是没影响的,但反过来的话, JS 就要等待了,然而这些等待的时间是完全不必要的。
JS ,也就是 <script> 标签,估计大家都很熟悉了,不就是阻塞 DOM 解析和渲染么。然而,其中其实还是有一点细节可以考究一下的,我们一起来好好看看。
JS 阻塞 DOM 解析
首先我们需要一个新的 JS 文件名为 blok.js ,内容如下:
<body> <div></div> <script src="/js/sleep3000-logDiv.js"></script> <style> div { background: lightgrey; } </style> <script src="/js/sleep5000-logDiv.js"></script> <link rel="stylesheet" href="/css/common.css"></body>
这个例子也是很极端的例子,但不妨碍它透露给我们很多重要的信息。想象一下,页面会怎样呢?
答案是先浅绿色,再浅灰色,最后浅蓝色。由此可见,每次碰到 <script> 标签时,浏览器都会渲染一次页面。这是基于同样的理由,浏览器不知道脚本的内容,因而碰到脚本时,只好先渲染页面,确保脚本能获取到最新的 DOM 元素信息,尽管脚本可能不需要这些信息。
综上所述,我们得出这样的结论:
CSS 不会阻塞 DOM 的解析,但会阻塞 DOM 渲染。
JS 阻塞 DOM 解析,但浏览器会"偷看" DOM ,预先下载相关资源。
最后还是要推荐下我的前端群:524262608,不定期分享干货,包括我自己整理的一份2017最新的前端资料和零基础入门教程,欢迎初学和进阶中的小伙伴。
浏览器遇到 <script> 且没有 defer 或 async 属性的 标签时,会触发页面渲染,因而如果前面 CSS 资源尚未加载完毕时,浏览器会等待它加载完毕在执行脚本。
所以,你现在明白为何 <script> 最好放底部, <link> 最好放头部,如果头部同时有 <script> 与 <link> 的情况下,最好将 <script> 放在 <link> 上面了吗?
*请认真填写需求信息,我们会在24小时内与您取得联系。