hellorayza

hellorayza 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

hellorayza 关注了标签 · 1月4日

svn

svn(subversion)是近年来崛起的版本管理工具,是cvs的接班人。目前,绝大多数开源软件都使用svn作为代码版本管理软件。

关注 745

hellorayza 赞了文章 · 2020-11-16

【编译篇】AST实现函数错误的自动上报

前言

之前有身边有人问我在错误监控中,如何能实现自动为函数自动添加错误捕获。今天我们来聊一聊技术如何实现。先讲原理:在代码编译时,利用 babel 的 loader,劫持所有函数表达。然后利用 AST(抽象语法树) 修改函数节点,在函数外层包裹 try/catch。然后在 catch 中使用 sdk 将错误信息在运行时捕获上报。如果你对编译打包感兴趣,那么本文就是为你准备的。

本文涉及以下知识点:

  • [x] AST
  • [x] npm 包开发
  • [x] Babel
  • [x] Babel plugin
  • [x] Webpack loader

实现效果

Before 开发环境:

var fn = function(){
  console.log('hello');
}

After 线上环境:

var fn = function(){
+  try {
    console.log('hello');
+  } catch (error) {
+    // sdk 错误上报
+    ErrorCapture(error);
+  }
}

Babel 是什么?

Babel 是JS编译器,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
简单说就是从一种源码到另一种源码的编辑器!下面列出的是 Babel 能为你做的事情:

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)
  • 源码转换 (codemods)
  • 其它

Babel 的运行主要分三个阶段,请牢记:解析->转换->生成,后面会用到。

本文我们将会写一个 Babel plugin 的 npm 包,用于编译时将代码进行改造。

babel-plugin 环境搭建

这里我们使用 yeomangenerator-babel-plugin 来构建插件的脚手架代码。安装:

$ npm i -g yo
$ npm i -g generator-babel-plugin

然后新建文件夹:

$ mkdir babel-plugin-function-try-actch
$ cd babel-plugin-function-try-actch

生成npm包的开发工程:

$ yo babel-plugin


此时项目结构为:

babel-plugin-function-try-catch
├─.babelrc
├─.gitignore
├─.npmignore
├─.travis.yml
├─README.md
├─package-lock.json
├─package.json
├─test
|  ├─index.js
|  ├─fixtures
|  |    ├─example
|  |    |    ├─.babelrc
|  |    |    ├─actual.js
|  |    |    └expected.js
├─src
|  └index.js
├─lib
|  └index.js

这就是我们的 Babel plugin,取名为 babel-loader-function-try-catch为方便文章阅读,以下我们统一简称为plugin)。

至此,npm 包环境搭建完毕,代码地址

调试 plugin 的 ast

开发工具

本文前面说过 Babel 的运行主要分三个阶段:解析->转换->生成,每个阶段 babel 官方提供了核心的 lib:

  • babel-core。Babel 的核心库,提供了将代码编译转化的能力。
  • babel-types。提供 AST 树节点的类型。
  • babel-template。可以将普通字符串转化成 AST,提供更便捷的使用

plugin 根目录安装需要用到的工具包:

npm i @babel/core @babel/parser babel-traverse @babel/template babel-types -S

打开 plugin 的 src/index.js 编辑:

const parser = require("@babel/parser");

// 先来定义一个简单的函数
let source = `var fn = function (n) {
  console.log(111)
}`;

// 解析为 ast
let ast = parser.parse(source, {
  sourceType: "module",
  plugins: ["dynamicImport"]
});

// 打印一下看看,是否正常
console.log(ast);

终端执行 node src/index.js 后将会打印如下结果:

这就是 fn 函数对应的 ast,第一步解析完成!

获取当前节点的 AST

然后我们使用 babel-traverse 去遍历对应的 AST 节点,我们想要寻找所有的 function 表达可以写在 FunctionExpression 中:

打开 plugin 的 src/index.js 编辑:

const parser = require("@babel/parser");
const traverse = require("babel-traverse").default;

// mock 待改造的源码
let source = `var fn = function() {
  console.log(111)
}`;

// 1、解析
let ast = parser.parse(source, {
  sourceType: "module",
  plugins: ["dynamicImport"]
});

// 2、遍历
+ traverse(ast, {
+   FunctionExpression(path, state) { // Function 节点
+     // do some stuff
+   },
+ });

所有函数表达都会走到 FunctionExpression 中,然后我们可以在里面对其进行修改。
其中参数 path 用于访问到当前的节点信息 path.node,也可以像 DOM 树访问到父节点的方法 path.parent

修改当前节点的 AST

好了,接下来要做的是在 FunctionExpression 中去劫持函数的内部代码,然后将其放入 try 函数内,并且在 catch 内加入错误上报 sdk 的代码段。

获取函数体内部代码

上面定义的函数是

var fn = function() {
  console.log(111)
}

那么函数内部的代码块就是 console.log(111),可以使用 path 拿到这段代码的 AST 信息,如下:

const parser = require("@babel/parser");
const traverse = require("babel-traverse").default;

// mock 待改造的源码
let source = `var fn = function(n) {
  console.log(111)
}`;

// 1、解析
let ast = parser.parse(source, {
  sourceType: "module",
  plugins: ["dynamicImport"]
});

// 2、遍历
traverse(ast, {
  FunctionExpression(path, state) { // 函数表达式会进入当前方法
+    // 获取函数当前节点信息
+    var node = path.node,
+        params = node.params,
+        blockStatement = node.body,
+        isGenerator = node.generator,
+        isAsync = node.async;

+    // 可以尝试打印看看结果
+    console.log(node, params, blockStatement);
  },
});

终端执行 node src/index.js,可以打印看到当前函数的 AST 节点信息。

创建 try/catch 节点(两步骤)

创建一个新的节点可能会稍微陌(fu)生(za)一点,不过我已经为大家总结了我个人的经验(仅供参考)。首先需要知道当前新增代码段它的声明是什么,然后使用 @babel-types 去创建即可。

第一步:

那么我们如何知道它的表达声明type是什么呢?这里我们可以 使用 astexplorer 查找它在 AST 中 type 的表达

如上截图得知,try/catch 在 AST 中的 type 就是 TryStatement

第二步:

然后去 @babel-types 官方文档查找对应方法,根据 API 文档来创建即可。

如文档所示,创建一个 try/catch 的方式使用 t.tryStatement(block, handler, finalizer)

创建新的ast节点一句话总结:使用 astexplorer 查找你要生成的代码的 type,再根据 type 在 @babel-types 文档查找对应的使用方法使用即可!

那么创建 try/catch 只需要使用 t.tryStatement(try代码块, catch代码块) 即可。

  • try代码块 表示 try 中的函数代码块,即原先函数 body 内的代码 console.log(111),可以直接用 path.node.body 获取;
  • catch代码块 表示 catch 代码块,即我们想要去改造进行错误收集上报的 sdk 的代码 ErrorCapture(error),可以使用 @babel/template 去生成。

代码如下所示:

const parser = require("@babel/parser");
const traverse = require("babel-traverse").default;
const t = require("babel-types");
const template = require("@babel/template");

// 0、定义一个待处理的函数(mock)
let source = `var fn = function() {
  console.log(111)
}`;

// 1、解析
let ast = parser.parse(source, {
  sourceType: "module",
  plugins: ["dynamicImport"]
});

// 2、遍历
traverse(ast, {
  FunctionExpression(path, state) { // Function 节点
    var node = path.node,
        params = node.params,
        blockStatement = node.body, // 函数function内部代码,将函数内部代码块放入 try 节点
        isGenerator = node.generator,
        isAsync = node.async;

+    // 创建 catch 节点中的代码
+    var catchStatement = template.statement(`ErrorCapture(error)`)();
+    var catchClause = t.catchClause(t.identifier('error'),
+          t.blockStatement(
+            [catchStatement] //  catchBody
+          )
+        );
+    // 创建 try/catch 的 ast
+    var tryStatement = t.tryStatement(blockStatement, catchClause);
  }
});

创建新函数节点,并将上面定义好的 try/catch 塞入函数体:

const parser = require("@babel/parser");
const traverse = require("babel-traverse").default;
const t = require("babel-types");
const template = require("@babel/template");

// 0、定义一个待处理的函数(mock)
let source = `var fn = function() {
  console.log(111)
}`;

// 1、解析
let ast = parser.parse(source, {
  sourceType: "module",
  plugins: ["dynamicImport"]
});

// 2、遍历
traverse(ast, {
  FunctionExpression(path, state) { // Function 节点
      var node = path.node,
          params = node.params,
          blockStatement = node.body, // 函数function内部代码,将函数内部代码块放入 try 节点
          isGenerator = node.generator,
          isAsync = node.async;

      // 创建 catch 节点中的代码
      var catchStatement = template.statement(`ErrorCapture(error)`)();
      var catchClause = t.catchClause(t.identifier('error'),
            t.blockStatement(
              [catchStatement] //  catchBody
            )
          );
      // 创建 try/catch 的 ast
      var tryStatement = t.tryStatement(blockStatement, catchClause);

+    // 创建新节点
+    var func = t.functionExpression(node.id, params, t.BlockStatement([tryStatement]), isGenerator, isAsync);
+    // 打印看看是否成功
+    console.log('当前节点是:', func);
+    console.log('当前节点下的自节点是:', func.body);
  }
});

此时将上述代码在终端执行 node src/index.js

可以看到此时我们在一个函数表达式 body 中创建了一个 try 函数(TryStatement)。
最后我们需要将原函数节点进行替换:

const parser = require("@babel/parser");
const traverse = require("babel-traverse").default;
const t = require("babel-types");
const template = require("@babel/template");

// 0、定义一个待处理的函数(mock)
let source = `var fn = function() {...

// 1、解析
let ast = parser.parse(source, {...

// 2、遍历
traverse(ast, {
  FunctionExpression(path, state) { // Function 节点
      var node = path.node,
          params = node.params,
          blockStatement = node.body, // 函数function内部代码,将函数内部代码块放入 try 节点
          isGenerator = node.generator,
          isAsync = node.async;

      // 创建 catch 节点中的代码
      var catchStatement = template.statement(`ErrorCapture(error)`)();
      var catchClause = t.catchClause(t.identifier('error'),...

      // 创建 try/catch 的 ast
      var tryStatement = t.tryStatement(blockStatement, catchClause);
      // 创建新节点
      var func = t.functionExpression(node.id, params, t.BlockStatement([tryStatement]), isGenerator, isAsync);
      
+    // 替换原节点
+    path.replaceWith(func);
  }
});

+ // 将新生成的 AST,转为 Source 源码:
+ return core.transformFromAstSync(ast, null, {
+  configFile: false // 屏蔽 babel.config.js,否则会注入 polyfill 使得调试变得困难
+ }).code;

“A loader is a node module exporting a function”,也就是说一个 loader 就是一个暴露出去的 node 模块,既然是一个node module,也就基本可以写成下面的样子:

module.exports = function() {
    //  ...
};

再编辑 src/index.js 为如下截图:

边界条件处理

我们并不需要为所有的函数都增加 try/catch,所有我们还得处理一些边界条件。

  • 1、如果有 try catch 包裹了
  • 2、防止 circle loops
  • 3、需要 try catch 的只能是语句,像 () => 0 这种的 body
  • 4、如果函数内容小于多少行数

满足以上条件就 return 掉!

代码如下:

if (blockStatement.body && t.isTryStatement(blockStatement.body[0])
  || !t.isBlockStatement(blockStatement) && !t.isExpressionStatement(blockStatement)
  || blockStatement.body && blockStatement.body.length <= LIMIT_LINE) {
  return;
}

最后我们发布到 npm 平台 使用。

由于篇幅过长不易阅读,本文特别的省略了本地调试过程,所以需要调试请移步 [【利用AST自动为函数增加错误上报-续集】有关 npm 包的本地开发和调试]()。

如何使用

npm install babel-plugin-function-try-catch

webpack 配置

rules: [{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
+   "babel-plugin-function-try-catch",
    "babel-loader",
  ]
}]

效果见如下图所示:

最后

有关 npm 包的本地调试见下篇: 有关 npm 包的本地开发和调试

更多 AST 相关请关注后面分享,谢谢。

Reference:

完整代码地址请点击

Babel 插件手册点击

查看原文

赞 18 收藏 9 评论 5

hellorayza 赞了文章 · 2020-11-11

Eventloop不可怕,可怕的是遇上Promise

有关Eventloop+Promise的面试题大约分以下几个版本——得心应手版、游刃有余版、炉火纯青版、登峰造极版和究极变态版。假设小伙伴们战到最后一题,以后遇到此类问题,都是所向披靡。当然如果面试官们还能想出更变态的版本,算我输。

版本一:得心应手版

考点:eventloop中的执行顺序,宏任务微任务的区别。

吐槽:这个不懂,没得救了,回家重新学习吧。

setTimeout(()=>{
   console.log(1) 
},0)
Promise.resolve().then(()=>{
   console.log(2) 
})
console.log(3) 

这个版本的面试官们就特别友善,仅仅考你一个概念理解,了解宏任务(marcotask)微任务(microtask),这题就是送分题。

笔者答案:这个是属于Eventloop的问题。main script运行结束后,会有微任务队列和宏任务队列。微任务先执行,之后是宏任务。

PS:概念问题

有时候会有版本是宏任务>微任务>宏任务,在这里笔者需要讲清楚一个概念,以免混淆。这里有个main script的概念,就是一开始执行的代码(代码总要有开始执行的时候对吧,不然宏任务和微任务的队列哪里来的),这里被定义为了宏任务(笔者喜欢将main script的概念单独拎出来,不和两个任务队列混在一起),然后根据main script中产生的微任务队列和宏任务队列,分别清空,这个时候是先清空微任务的队列,再去清空宏任务的队列。

版本二:游刃有余版

这一个版本,面试官们为了考验一下对于Promise的理解,会给题目加点料:

考点:Promise的executor以及then的执行方式

吐槽:这是个小坑,promise掌握的熟练的,这就是人生的小插曲。

setTimeout(()=>{
   console.log(1) 
},0)
let a=new Promise((resolve)=>{
    console.log(2)
    resolve()
}).then(()=>{
   console.log(3) 
}).then(()=>{
   console.log(4) 
})
console.log(4) 

此题看似在考Eventloop,实则考的是对于Promise的掌握程度。Promise的then是微任务大家都懂,但是这个then的执行方式是如何的呢,以及Promise的executor是异步的还是同步的?

错误示范:Promise的then是一个异步的过程,每个then执行完毕之后,就是一个新的循环的,所以第二个then会在setTimeout之后执行。(没错,这就是某年某月某日笔者的一个回答。请给我一把枪,真想打死当时的自己。)

正确示范:这个要从Promise的实现来说,Promise的executor是一个同步函数,即非异步,立即执行的一个函数,因此他应该是和当前的任务一起执行的。而Promise的链式调用then,每次都会在内部生成一个新的Promise,然后执行then,在执行的过程中不断向微任务(microtask)推入新的函数,因此直至微任务(microtask)的队列清空后才会执行下一波的macrotask。

详细解析

(如果大家不嫌弃,可以参考我的另一篇文章,从零实现一个Promise,里面的解释浅显易懂。)
我们以babel的core-js中的promise实现为例,看一眼promise的执行规范:

代码位置:promise-polyfill

PromiseConstructor = function Promise(executor) {
    //...
    try {
      executor(bind(internalResolve, this, state), bind(internalReject, this, state));
    } catch (err) {
      internalReject(this, state, err);
    }
};

这里可以很清除地看到Promise中的executor是一个立即执行的函数。

then: function then(onFulfilled, onRejected) {
    var state = getInternalPromiseState(this);
    var reaction = newPromiseCapability(speciesConstructor(this, PromiseConstructor));
    reaction.ok = typeof onFulfilled == 'function' ? onFulfilled : true;
    reaction.fail = typeof onRejected == 'function' && onRejected;
    reaction.domain = IS_NODE ? process.domain : undefined;
    state.parent = true;
    state.reactions.push(reaction);
    if (state.state != PENDING) notify(this, state, false);
    return reaction.promise;
},

接着是Promise的then函数,很清晰地看到reaction.promise,也就是每次then执行完毕后会返回一个新的Promise。也就是当前的微任务(microtask)队列清空了,但是之后又开始添加了,直至微任务(microtask)队列清空才会执行下一波宏任务(marcotask)。

//state.reactions就是每次then传入的函数
 var chain = state.reactions;
  microtask(function () {
    var value = state.value;
    var ok = state.state == FULFILLED;
    var i = 0;
    var run = function (reaction) {
        //...
    };
    while (chain.length > i) run(chain[i++]);
    //...
  });

最后是Promise的任务resolve之后,开始执行then,可以看到此时会批量执行then中的函数,而且还给这些then中回调函数放入了一个microtask这个很显眼的函数之中,表示这些回调函数是在微任务中执行的。

那么在没有Promise的浏览器中,微任务这个队列是如何实现的呢?

小知识:babel中对于微任务的polyfill,如果是拥有setImmediate函数平台,则使用之,若没有则自定义则利用各种比如nodejs中的process.nextTick,浏览器中支持postMessage的,或者是通过create一个script来实现微任务(microtask)。最终的最终,是使用setTimeout,不过这个就和微任务无关了,promise变成了宏任务的一员。

拓展思考:

为什么有时候,then中的函数是一个数组?有时候就是一个函数?

我们稍稍修改一下上述题目,将链式调用的函数,变成下方的,分别调用then。且不说这和链式调用之间的不同用法,这边只从实践角度辨别两者的不同。链式调用是每次都生成一个新的Promise,也就是说每个then中回调方法属于一个microtask,而这种分别调用,会将then中的回调函数push到一个数组之中,然后批量执行。再换句话说,链式调用可能会被Evenloop中其他的函数插队,而分别调用则不会(仅针对最普通的情况,then中无其他异步操作。)。

let a=new Promise((resolve)=>{
     console.log(2)
     resolve()
})
a.then(()=>{
    console.log(3) 
})
a.then(()=>{
    console.log(4) 
})
 

下一模块会对此微任务(microtask)中的“插队”行为进行详解。

版本三:炉火纯青版

这一个版本是上一个版本的进化版本,上一个版本的promise的then函数并未返回一个promise,如果在promise的then中创建一个promise,那么结果该如何呢?

考点:promise的进阶用法,对于then中return一个promise的掌握

吐槽:promise也可以是地狱……

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})
按照上一节最后一个microtask的实现过程,也就是说一个Promise所有的then的回调函数是在一个microtask函数中执行的,但是每一个回调函数的执行,又按照情况分为立即执行,微任务(microtask)和宏任务(macrotask)。

遇到这种嵌套式的Promise不要慌,首先要心中有一个队列,能够将这些函数放到相对应的队列之中。

Ready GO

第一轮

  • current task: promise1是当之无愧的立即执行的一个函数,参考上一章节的executor,立即执行输出[promise1]
  • micro task queue: [promise1的第一个then]

第二轮

  • current task: then1执行中,立即输出了then11以及新promise2的promise2
  • micro task queue: [新promise2的then函数,以及promise1的第二个then函数]

第三轮

  • current task: 新promise2的then函数输出then21和promise1的第二个then函数输出then12
  • micro task queue: [新promise2的第二then函数]

第四轮

  • current task: 新promise2的第二then函数输出then23
  • micro task queue: []

END

最终结果[promise1,then11,promise2,then21,then12,then23]

变异版本1:如果说这边的Promise中then返回一个Promise呢??

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    return new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})

这里就是Promise中的then返回一个promise的状况了,这个考的重点在于Promise而非Eventloop了。这里就很好理解为何then12会在then23之后执行,这里Promise的第二个then相当于是挂在新Promise的最后一个then的返回值上。

变异版本2:如果说这边不止一个Promise呢,再加一个new Promise是否会影响结果??

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})
new Promise((resolve,reject)=>{
    console.log("promise3")
    resolve()
}).then(()=>{
    console.log("then31")
})

笑容逐渐变态,同样这个我们可以自己心中排一个队列:

第一轮

  • current task: promise1,promise3
  • micro task queue: [promise2的第一个thenpromise3的第一个then]

第二轮

  • current task: then11,promise2,then31
  • micro task queue: [promise2的第一个thenpromise1的第二个then]

第三轮

  • current task: then21,then12
  • micro task queue: [promise2的第二个then]

第四轮

  • current task: then23
  • micro task queue: []

最终输出:[promise1,promise3,then11,promise2,then31,then21,then12,then23]

版本四:登峰造极版

考点:在async/await之下,对Eventloop的影响。

槽点:别被async/await给骗了,这题不难。

相信大家也看到过此类的题目,我这里有个相当简易的解释,不知大家是否有兴趣。

async function async1() {
    console.log("async1 start");
    await  async2();
    console.log("async1 end");
}

async  function async2() {
    console.log( 'async2');
}

console.log("script start");

setTimeout(function () {
    console.log("settimeout");
},0);

async1();

new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
console.log('script end'); 

async/await仅仅影响的是函数内的执行,而不会影响到函数体外的执行顺序。也就是说async1()并不会阻塞后续程序的执行,await async2()相当于一个Promise,console.log("async1 end");相当于前方Promise的then之后执行的函数。

按照上章节的解法,最终输出结果:[script start,async1 start,async2,promise1,script end,async1 end,promise2,settimeout]

如果了解async/await的用法,则并不会觉得这题是困难的,但若是不了解或者一知半解,那么这题就是灾难啊。

  • 此处唯一有争议的就是async的then和promise的then的优先级的问题,请看下方详解。*

async/await与promise的优先级详解

async function async1() {
    console.log("async1 start");
    await  async2();
    console.log("async1 end");
}
async  function async2() {
    console.log( 'async2');
}
// 用于test的promise,看看await究竟在何时执行
new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
}).then(function () {
    console.log("promise3");
}).then(function () {
    console.log("promise4");
}).then(function () {
    console.log("promise5");
});

先给大家出个题,如果让你polyfill一下async/await,大家会怎么polyfill上述代码?下方先给出笔者的版本:

function promise1(){
    return new Promise((resolve)=>{
        console.log("async1 start");
        promise2().then(()=>{
            console.log("async1 end");
            resolve()
        })
    })
}
function promise2(){
    return new Promise((resolve)=>{
        console.log( 'async2'); 
        resolve() 
    })
}

在笔者看来,async本身是一个Promise,然后await肯定也跟着一个Promise,那么新建两个function,各自返回一个Promise。接着function promise1中需要等待function promise2中Promise完成后才执行,那么就then一下咯~。

根据这个版本得出的结果:[async1 start,async2,promise1,async1 end,promise2,...],async的await在test的promise.then之前,其实也能够从笔者的polifill中得出这个结果。

然后让笔者惊讶的是用原生的async/await,得出的结果与上述polyfill不一致!得出的结果是:[async1 start,async2,promise1,promise2,promise3,async1 end,...],由于promise.then每次都是一轮新的microtask,所以async是在2轮microtask之后,第三轮microtask才得以输出(关于then请看版本三的解释)。

/ 突如其来的沉默 /

这里插播一条,async/await因为要经过3轮的microtask才能完成await,被认为开销很大,因此之后V8和Nodejs12开始对此进行了修复,详情可以看github上面这一条pull

那么,笔者换一种方式来polyfill,相信大家都已经充分了解await后面是一个Promise,但是假设这个Promise不是好Promise怎么办?异步是好异步,Promise不是好Promise。V8就很凶残,加了额外两个Promise用于解决这个问题,简化了下源码,大概是下面这个样子:

// 不太准确的一个描述
function promise1(){
    console.log("async1 start");
    // 暗中存在的promise,笔者认为是为了保证async返回的是一个promise
    const implicit_promise=Promise.resolve()
    // 包含了await的promise,这里直接执行promise2,为了保证promise2的executor是同步的感觉
    const promise=promise2()
    // https://tc39.github.io/ecma262/#sec-performpromisethen
    // 25.6.5.4.1
    // throwaway,为了规范而存在的,为了保证执行的promise是一个promise
    const throwaway= Promise.resolve()
    //console.log(throwaway.then((d)=>{console.log(d)}))
    return implicit_promise.then(()=>{
        throwaway.then(()=>{
            promise.then(()=>{
                console.log('async1 end');
            })
        }) 
    })
}

ps:为了强行推迟两个microtask执行,笔者也是煞费苦心。

总结一下:async/await有时候会推迟两轮microtask,在第三轮microtask执行,主要原因是浏览器对于此方法的一个解析,由于为了解析一个await,要额外创建两个promise,因此消耗很大。后来V8为了降低损耗,所以剔除了一个Promise,并且减少了2轮microtask,所以现在最新版本的应该是“零成本”的一个异步。

版本五:究极变态版

饕餮大餐,什么变态的内容都往里面加,想想就很丰盛。能考到这份上,只能说面试官人狠话也多。

考点:nodejs事件+Promise+async/await+佛系setImmediate

槽点:笔者都不知道那个可能先出现

async function async1() {
    console.log("async1 start");
    await  async2();
    console.log("async1 end");
}
async  function async2() {
    console.log( 'async2');
}
console.log("script start");
setTimeout(function () {
    console.log("settimeout");
});
async1()
new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
setImmediate(()=>{
    console.log("setImmediate")
})
process.nextTick(()=>{
    console.log("process")
})
console.log('script end'); 

队列执行start

第一轮:

  • current task:"script start","async1 start",'async2',"promise1",“script end”
  • micro task queue:[async,promise.then,process]
  • macro task queue:[setTimeout,setImmediate]

第二轮

  • current task:process,async1 end ,promise.then
  • micro task queue:[]
  • macro task queue:[setTimeout,setImmediate]

第三轮

  • current task:setTimeout,setImmediate
  • micro task queue:[]
  • macro task queue:[]

最终结果:[script startasync1 startasync2promise1script end,process,async1 end,promise2,setTimeout,setImmediate]

同样"async1 end","promise2"之间的优先级,因平台而异。

笔者干货总结

在处理一段evenloop执行顺序的时候:

  • 第一步确认宏任务,微任务

    • 宏任务:script,setTimeout,setImmediate,promise中的executor
    • 微任务:promise.then,process.nextTick
  • 第二步解析“拦路虎”,出现async/await不要慌,他们只在标记的函数中能够作威作福,出了这个函数还是跟着大部队的潮流。
  • 第三步,根据Promise中then使用方式的不同做出不同的判断,是链式还是分别调用。
  • 最后一步记住一些特别事件

    • 比如,process.nextTick优先级高于Promise.then

参考网址,推荐阅读:

有关V8中如何实现async/await的,更快的异步函数和 Promise

有关async/await规范的,ecma262

还有babel-polyfill的源码,promise

后记

Hello~Anybody here?

本来笔者是不想写这篇文章的,因为有种5年高考3年模拟的既视感,奈何面试官们都太凶残了,为了“折磨”面试者无所不用其极,怎么变态怎么来。不过因此笔者算是彻底掌握了Eventloop的用法,因祸得福吧~

有小伙伴看到最后嘛?来和笔者聊聊你遇到过的的Eventloop+Promise的变态题目。

欢迎转载~但请注明出处~首发于掘金~Eventloop不可怕,可怕的是遇上Promise

题外话:来segmentfault试水~啊哈哈哈啊哈哈

查看原文

赞 30 收藏 22 评论 2

hellorayza 提出了问题 · 2020-09-26

nodejs图片转为bse64后压缩问题

我想在读取图片的时候输出压缩后的base64内容
目前是这样读取读片转base64的,没有被压缩

let im = fs.readFileSync(picPath);
let img_base64 = 'data:image/png;base64,' + im.toString('base64');

我不想先读取图片然后输出压缩图片再转base64,那样比较慢。有没有办法在读取的时候就输出压缩后的base64

看了下nodejszlip模块,里面zlib.gzip之类写的非常简略。另外不知道在stream等模块能不能实现以上目标。求助。

在这个issue中,虽然图片被压缩了,base64大小并没有被减少

node-images

关注 2 回答 1

hellorayza 赞了回答 · 2020-09-04

解决markdown公式中如何插入空格

$$ \vec{a}\cdot\vec{b} = ||\ a\ || \quad ||\text{ b }||\ cos\theta \\ \vec{a}\cdot\vec{b} = a_x b_x + a_y b_y $$

问答区不支持,文章笔记没问题

image

关注 2 回答 1

hellorayza 提出了问题 · 2020-09-03

解决markdown公式中如何插入空格

$$ 
\vec{a}\cdot\vec{b} = || a ||       ||b|| cos\theta
$$

image.png
P.S. : SegmentFault对公式居然不支持
Reference : https://www.cnblogs.com/klchang/p/10203404.html

关注 2 回答 1

hellorayza 关注了专栏 · 2020-09-02

前端巅峰

注重前端性能优化和前沿技术,重型跨平台开发,即时通讯技术等。 欢迎关注微信公众号:前端巅峰

关注 20109

hellorayza 赞了文章 · 2020-08-31

Vue 原生右键菜单组件

vue-contextmenujs

Vue 原生实现右键菜单组件, 零依赖

FireShot Capture 003 - Vue CLI App - localhost.png

项目地址

https://github.com/GitHub-Laziji/menujs

在线演示

快速安装

npm 安装

npm install vue-contextmenujs

CDN

<script data-original="https://unpkg.com/vue-contextmenujs/dist/contextmenu.umd.js">

使用

测试中使用的是element-ui图标
import Contextmenu from "vue-contextmenujs"
Vue.use(Contextmenu);
<template>
  <div id="app" style="width:100vw;height:100vh" @contextmenu.prevent="onContextmenu"></div>
</template>

<script>
export default {
  methods: {
    onContextmenu(event) {
      this.$contextmenu({
        items: [
          {
            label: "返回(B)",
            onClick: () => {
              this.message = "返回(B)";
              console.log("返回(B)");
            }
          },
          { label: "前进(F)", disabled: true },
          { label: "重新加载(R)", divided: true, icon: "el-icon-refresh" },
          { label: "另存为(A)..." },
          { label: "打印(P)...", icon: "el-icon-printer" },
          { label: "投射(C)...", divided: true },
          {
            label: "使用网页翻译(T)",
            divided: true,
            minWidth: 0,
            children: [{ label: "翻译成简体中文" }, { label: "翻译成繁体中文" }]
          },
          {
            label: "截取网页(R)",
            minWidth: 0,
            children: [
              {
                label: "截取可视化区域",
                onClick: () => {
                  this.message = "截取可视化区域";
                  console.log("截取可视化区域");
                }
              },
              { label: "截取全屏" }
            ]
          },
          { label: "查看网页源代码(V)", icon: "el-icon-view" },
          { label: "检查(N)" }
        ],
        event,
        //x: event.clientX,
        //y: event.clientY,
        customClass: "class-a",
        zIndex: 3,
        minWidth: 230
      });
      return false;
    }
  }
};
</script>

参数说明

Menu

属性描述类型可选值默认值
items菜单结构信息MenuItem[]
event鼠标事件信息Event
x菜单显示X坐标, 存在event则失效number0
y菜单显示Y坐标, 存在event则失效number0
zIndex菜单样式z-indexnumber2
customClass自定义菜单classstring
minWidth主菜单最小宽度number150

MenuItem

属性描述类型可选值默认值
label菜单项名称string
icon菜单项图标, 生成<i class="icon"></i>元素string
disabled是否禁用菜单项booleanfalse
divided是否显示分割线booleanfalse
customClass自定义子菜单classstring
minWidth子菜单最小宽度number150
onClick菜单项点击事件Function()
children子菜单结构信息MenuItem[]
欢迎关注我的博客公众号
2018_11_16_0048241709.png
查看原文

赞 21 收藏 11 评论 2

hellorayza 赞了文章 · 2020-08-28

VUE-多文件断点续传、秒传、分片上传

本文为:多文件断点续传、分片上传、秒传、重试机制 的更新版,若想看初始版本的实现,请查看该文章。

凡是要知其然知其所以然

文件上传相信很多朋友都有遇到过,那或许你也遇到过当上传大文件时,上传时间较长,且经常失败的困扰,并且失败后,又得重新上传很是烦人。那我们先了解下失败的原因吧!

据我了解大概有以下原因:

  1. 服务器配置:例如在PHP中默认的文件上传大小为8M【post_max_size = 8m】,若你在一个请求体中放入8M以上的内容时,便会出现异常
  2. 请求超时:当你设置了接口的超时时间为10s,那么上传大文件时,一个接口响应时间超过10s,那么便会被Faild掉。
  3. 网络波动:这个就属于不可控因素,也是较常见的问题。
基于以上原因,聪明的人们就想到了,将文件拆分多个小文件,依次上传,不就解决以上1,2问题嘛,这便是分片上传。 网络波动这个实在不可控,也许一阵大风刮来,就断网了呢。那这样好了,既然断网无法控制,那我可以控制只上传已经上传的文件内容,不就好了,这样大大加快了重新上传的速度。所以便有了“断点续传”一说。此时,人群中有人插了一嘴,有些文件我已经上传一遍了,为啥还要在上传,能不能不浪费我流量和时间。喔...这个嘛,简单,每次上传时判断下是否存在这个文件,若存在就不重新上传便可,于是又有了“秒传”一说。从此这"三兄弟" 便自行CP,统治了整个文件界。”

注意文中的代码并非实际代码,请移步至github查看最新代码
https://github.com/pseudo-god...


分片上传

HTML

原生INPUT样式较丑,这里通过样式叠加的方式,放一个Button.
  <div class="btns">
    <el-button-group>
      <el-button :disabled="changeDisabled">
        <i class="el-icon-upload2 el-icon--left" size="mini"></i>选择文件
        <input
          v-if="!changeDisabled"
          type="file"
          :multiple="multiple"
          class="select-file-input"
          :accept="accept"
          @change="handleFileChange"
        />
      </el-button>
      <el-button :disabled="uploadDisabled" @click="handleUpload()"><i class="el-icon-upload el-icon--left" size="mini"></i>上传</el-button>
      <el-button :disabled="pauseDisabled" @click="handlePause"><i class="el-icon-video-pause el-icon--left" size="mini"></i>暂停</el-button>
      <el-button :disabled="resumeDisabled" @click="handleResume"><i class="el-icon-video-play el-icon--left" size="mini"></i>恢复</el-button>
      <el-button :disabled="clearDisabled" @click="clearFiles"><i class="el-icon-video-play el-icon--left" size="mini"></i>清空</el-button>
    </el-button-group>
    <slot 
    
 //data 数据
 
var chunkSize = 10 * 1024 * 1024; // 切片大小
var fileIndex = 0; // 当前正在被遍历的文件下标

 data: () => ({
    container: {
      files: null
    },
    tempFilesArr: [], // 存储files信息
    cancels: [], // 存储要取消的请求
    tempThreads: 3,
    // 默认状态
    status: Status.wait
  }),
    

一个稍微好看的UI就出来了。

选择文件

选择文件过程中,需要对外暴露出几个钩子,熟悉elementUi的同学应该很眼熟,这几个钩子基本与其一致。onExceed:文件超出个数限制时的钩子、beforeUpload:文件上传之前

fileIndex 这个很重要,因为是多文件上传,所以定位当前正在被上传的文件就很重要,基本都靠它

handleFileChange(e) {
  const files = e.target.files;
  if (!files) return;
  Object.assign(this.$data, this.$options.data()); // 重置data所有数据

  fileIndex = 0; // 重置文件下标
  this.container.files = files;
  // 判断文件选择的个数
  if (this.limit && this.container.files.length > this.limit) {
    this.onExceed && this.onExceed(files);
    return;
  }

  // 因filelist不可编辑,故拷贝filelist 对象
  var index = 0; // 所选文件的下标,主要用于剔除文件后,原文件list与临时文件list不对应的情况
  for (const key in this.container.files) {
    if (this.container.files.hasOwnProperty(key)) {
      const file = this.container.files[key];

      if (this.beforeUpload) {
        const before = this.beforeUpload(file);
        if (before) {
          this.pushTempFile(file, index);
        }
      }

      if (!this.beforeUpload) {
        this.pushTempFile(file, index);
      }

      index++;
    }
  }
},
// 存入 tempFilesArr,为了上面的钩子,所以将代码做了拆分
pushTempFile(file, index) {
  // 额外的初始值
  const obj = {
    status: fileStatus.wait,
    chunkList: [],
    uploadProgress: 0,
    hashProgress: 0,
    index
  };
  for (const k in file) {
    obj[k] = file[k];
  }
  console.log('pushTempFile -> obj', obj);
  this.tempFilesArr.push(obj);
}

分片上传

  • 创建切片,循环分解文件即可

      createFileChunk(file, size = chunkSize) {
        const fileChunkList = [];
        var count = 0;
        while (count < file.size) {
          fileChunkList.push({
            file: file.slice(count, count + size)
          });
          count += size;
        }
        return fileChunkList;
      }
  • 循环创建切片,既然咱们做的是多文件,所以这里就有循环去处理,依次创建文件切片,及切片的上传。
async handleUpload(resume) {
  if (!this.container.files) return;
  this.status = Status.uploading;
  const filesArr = this.container.files;
  var tempFilesArr = this.tempFilesArr;

  for (let i = 0; i < tempFilesArr.length; i++) {
    fileIndex = i;
    //创建切片
    const fileChunkList = this.createFileChunk(
      filesArr[tempFilesArr[i].index]
    );
      
    tempFilesArr[i].fileHash ='xxxx'; // 先不用看这个,后面会讲,占个位置
    tempFilesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({
      fileHash: tempFilesArr[i].hash,
      fileName: tempFilesArr[i].name,
      index,
      hash: tempFilesArr[i].hash + '-' + index,
      chunk: file,
      size: file.size,
      uploaded: false,
      progress: 0, // 每个块的上传进度
      status: 'wait' // 上传状态,用作进度状态显示
    }));
    
    //上传切片
    await this.uploadChunks(this.tempFilesArr[i]);
  }
}
  • 上传切片,这个里需要考虑的问题较多,也算是核心吧,uploadChunks方法只负责构造传递给后端的数据,核心上传功能放到sendRequest方法中
 async uploadChunks(data) {
  var chunkData = data.chunkList;
  const requestDataList = chunkData
    .map(({ fileHash, chunk, fileName, index }) => {
      const formData = new FormData();
      formData.append('md5', fileHash);
      formData.append('file', chunk);
      formData.append('fileName', index); // 文件名使用切片的下标
      return { formData, index, fileName };
    });

  try {
    await this.sendRequest(requestDataList, chunkData);
  } catch (error) {
    // 上传有被reject的
    this.$message.error('亲 上传失败了,考虑重试下呦' + error);
    return;
  }

  // 合并切片
  const isUpload = chunkData.some(item => item.uploaded === false);
  console.log('created -> isUpload', isUpload);
  if (isUpload) {
    alert('存在失败的切片');
  } else {
    // 执行合并
    await this.mergeRequest(data);
  }
}
  • sendReques。上传这是最重要的地方,也是容易失败的地方,假设有10个分片,那我们若是直接发10个请求的话,很容易达到浏览器的瓶颈,所以需要对请求进行并发处理。

    • 并发处理:这里我使用for循环控制并发的初始并发数,然后在 handler 函数里调用自己,这样就控制了并发。在handler中,通过数组API.shift模拟队列的效果,来上传切片。
    • 重试: retryArr 数组存储每个切片文件请求的重试次数,做累加。比如[1,0,2],就是第0个文件切片报错1次,第2个报错2次。为保证能与文件做对应,const index = formInfo.index; 我们直接从数据中拿之前定义好的index。 若失败后,将失败的请求重新加入队列即可。

      • 关于并发及重试我写了一个小Demo,若不理解可以自己在研究下,文件地址:https://github.com/pseudo-god... , 重试代码好像被我弄丢了,大家要是有需求,我再补吧!
    // 并发处理
sendRequest(forms, chunkData) {
  var finished = 0;
  const total = forms.length;
  const that = this;
  const retryArr = []; // 数组存储每个文件hash请求的重试次数,做累加 比如[1,0,2],就是第0个文件切片报错1次,第2个报错2次

  return new Promise((resolve, reject) => {
    const handler = () => {
      if (forms.length) {
        // 出栈
        const formInfo = forms.shift();

        const formData = formInfo.formData;
        const index = formInfo.index;
        
        instance.post('fileChunk', formData, {
          onUploadProgress: that.createProgresshandler(chunkData[index]),
          cancelToken: new CancelToken(c => this.cancels.push(c)),
          timeout: 0
        }).then(res => {
          console.log('handler -> res', res);
          // 更改状态
          chunkData[index].uploaded = true;
          chunkData[index].status = 'success';
          
          finished++;
          handler();
        })
          .catch(e => {
            // 若暂停,则禁止重试
            if (this.status === Status.pause) return;
            if (typeof retryArr[index] !== 'number') {
              retryArr[index] = 0;
            }

            // 更新状态
            chunkData[index].status = 'warning';

            // 累加错误次数
            retryArr[index]++;

            // 重试3次
            if (retryArr[index] >= this.chunkRetry) {
              return reject('重试失败', retryArr);
            }

            this.tempThreads++; // 释放当前占用的通道

            // 将失败的重新加入队列
            forms.push(formInfo);
            handler();
          });
      }

      if (finished >= total) {
        resolve('done');
      }
    };

    // 控制并发
    for (let i = 0; i < this.tempThreads; i++) {
      handler();
    }
  });
}
  • 切片的上传进度,通过axios的onUploadProgress事件,结合createProgresshandler方法进行维护
// 切片上传进度
createProgresshandler(item) {
  return p => {
    item.progress = parseInt(String((p.loaded / p.total) * 100));
    this.fileProgress();
  };
}

Hash计算

其实就是算一个文件的MD5值,MD5在整个项目中用到的地方也就几点。
  • 秒传,需要通过MD5值判断文件是否已存在。
  • 续传:需要用到MD5作为key值,当唯一值使用。
本项目主要使用worker处理,性能及速度都会有很大提升.
由于是多文件,所以HASH的计算进度也要体现在每个文件上,所以这里使用全局变量fileIndex来定位当前正在被上传的文件

执行计算hash

正在上传文件

// 生成文件 hash(web-worker)
calculateHash(fileChunkList) {
  return new Promise(resolve => {
    this.container.worker = new Worker('./hash.js');
    this.container.worker.postMessage({ fileChunkList });
    this.container.worker.onmessage = e => {
      const { percentage, hash } = e.data;
      if (this.tempFilesArr[fileIndex]) {
        this.tempFilesArr[fileIndex].hashProgress = Number(
          percentage.toFixed(0)
        );
      }

      if (hash) {
        resolve(hash);
      }
    };
  });
}

因使用worker,所以我们不能直接使用NPM包方式使用MD5。需要单独去下载spark-md5.js文件,并引入

//hash.js

self.importScripts("/spark-md5.min.js"); // 导入脚本
// 生成文件 hash
self.onmessage = e => {
  const { fileChunkList } = e.data;
  const spark = new self.SparkMD5.ArrayBuffer();
  let percentage = 0;
  let count = 0;
  const loadNext = index => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(fileChunkList[index].file);
    reader.onload = e => {
      count++;
      spark.append(e.target.result);
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          hash: spark.end()
        });
        self.close();
      } else {
        percentage += 100 / fileChunkList.length;
        self.postMessage({
          percentage
        });
        loadNext(count);
      }
    };
  };
  loadNext(0);
};

文件合并

当我们的切片全部上传完毕后,就需要进行文件的合并,这里我们只需要请求接口即可
mergeRequest(data) {
   const obj = {
     md5: data.fileHash,
     fileName: data.name,
     fileChunkNum: data.chunkList.length
   };

   instance.post('fileChunk/merge', obj, 
     {
       timeout: 0
     })
     .then((res) => {
       this.$message.success('上传成功');
     });
 }
Done: 至此一个分片上传的功能便已完成

断点续传

顾名思义,就是从那断的就从那开始,明确思路就很简单了。一般有2种方式,一种为服务器端返回,告知我从那开始,还有一种是浏览器端自行处理。2种方案各有优缺点。本项目使用第二种。

思路:已文件HASH为key值,每个切片上传成功后,记录下来便可。若需要续传时,直接跳过记录中已存在的便可。本项目将使用Localstorage进行存储,这里我已提前封装好addChunkStorage、getChunkStorage方法。

存储在Stroage的数据

缓存处理

在切片上传的axios成功回调中,存储已上传成功的切片

 instance.post('fileChunk', formData, )
  .then(res => {
    // 存储已上传的切片下标
+ this.addChunkStorage(chunkData[index].fileHash, index);
    handler();
  })

在切片上传前,先看下localstorage中是否存在已上传的切片,并修改uploaded

    async handleUpload(resume) {
+      const getChunkStorage = this.getChunkStorage(tempFilesArr[i].hash);
      tempFilesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({
+        uploaded: getChunkStorage && getChunkStorage.includes(index), // 标识:是否已完成上传
+        progress: getChunkStorage && getChunkStorage.includes(index) ? 100 : 0,
+        status: getChunkStorage && getChunkStorage.includes(index)? 'success'
+              : 'wait' // 上传状态,用作进度状态显示
      }));

    }

构造切片数据时,过滤掉uploaded为true的

 async uploadChunks(data) {
  var chunkData = data.chunkList;
  const requestDataList = chunkData
+    .filter(({ uploaded }) => !uploaded)
    .map(({ fileHash, chunk, fileName, index }) => {
      const formData = new FormData();
      formData.append('md5', fileHash);
      formData.append('file', chunk);
      formData.append('fileName', index); // 文件名使用切片的下标
      return { formData, index, fileName };
    })
}

垃圾文件清理

随着上传文件的增多,相应的垃圾文件也会增多,比如有些时候上传一半就不再继续,或上传失败,碎片文件就会增多。解决方案我目前想了2种
  • 前端在localstorage设置缓存时间,超过时间就发送请求通知后端清理碎片文件,同时前端也要清理缓存。
  • 前后端都约定好,每个缓存从生成开始,只能存储12小时,12小时后自动清理
以上2中方案似乎都有点问题,极有可能造成前后端因时间差,引发切片上传异常的问题,后面想到合适的解决方案再来更新吧。

Done: 续传到这里也就完成了。


秒传

这算是最简单的,只是听起来很厉害的样子。原理:计算整个文件的HASH,在执行上传操作前,向服务端发送请求,传递MD5值,后端进行文件检索。若服务器中已存在该文件,便不进行后续的任何操作,上传也便直接结束。大家一看就明白
async handleUpload(resume) {
    if (!this.container.files) return;
    const filesArr = this.container.files;
    var tempFilesArr = this.tempFilesArr;

    for (let i = 0; i < tempFilesArr.length; i++) {
      const fileChunkList = this.createFileChunk(
        filesArr[tempFilesArr[i].index]
      );

      // hash校验,是否为秒传
+      tempFilesArr[i].hash = await this.calculateHash(fileChunkList);
+      const verifyRes = await this.verifyUpload(
+        tempFilesArr[i].name,
+        tempFilesArr[i].hash
+      );
+      if (verifyRes.data.presence) {
+       tempFilesArr[i].status = fileStatus.secondPass;
+       tempFilesArr[i].uploadProgress = 100;
+      } else {
        console.log('开始上传切片文件----》', tempFilesArr[i].name);
        await this.uploadChunks(this.tempFilesArr[i]);
      }
    }
  }
  // 文件上传之前的校验: 校验文件是否已存在
  verifyUpload(fileName, fileHash) {
    return new Promise(resolve => {
      const obj = {
        md5: fileHash,
        fileName,
        ...this.uploadArguments //传递其他参数
      };
      instance
        .post('fileChunk/presence', obj)
        .then(res => {
          resolve(res.data);
        })
        .catch(err => {
          console.log('verifyUpload -> err', err);
        });
    });
  }
Done: 秒传到这里也就完成了。

后端处理

文章好像有点长了,具体代码逻辑就先不贴了,除非有人留言要求,嘻嘻,有时间再更新

Node版

请前往 https://github.com/pseudo-god... 查看

JAVA版

下周应该会更新处理

PHP版

1年多没写PHP了,抽空我会慢慢补上来

待完善

  • 切片的大小:这个后面会做出动态计算的。需要根据当前所上传文件的大小,自动计算合适的切片大小。避免出现切片过多的情况。
  • 文件追加:目前上传文件过程中,不能继续选择文件加入队列。(这个没想好应该怎么处理。)

更新记录

组件已经运行一段时间了,期间也测试出几个问题,本来以为没BUG的,看起来BUG都挺严重

BUG-1:当同时上传多个内容相同但是文件名称不同的文件时,出现上传失败的问题。

预期结果:第一个上传成功后,后面相同的问文件应该直接秒传

实际结果:第一个上传成功后,其余相同的文件都失败,错误信息,块数不对。

原因:当第一个文件块上传完毕后,便立即进行了下一个文件的循环,导致无法及时获取文件是否已秒传的状态,从而导致失败。

解决方案:在当前文件分片上传完毕并且请求合并接口完毕后,再进行下一次循环。

将子方法都改为同步方式,mergeRequest 和 uploadChunks 方法


BUG-2: 当每次选择相同的文件并触发beforeUpload方法时,若第二次也选择了相同的文件,beforeUpload方法失效,从而导致整个流程失效。

原因:之前每次选择文件时,没有清空上次所选input文件的数据,相同数据的情况下,是不会触发input的change事件。

解决方案:每次点击input时,清空数据即可。我顺带优化了下其他的代码,具体看提交记录吧。

<input
  v-if="!changeDisabled"
  type="file"
  :multiple="multiple"
  class="select-file-input"
  :accept="accept"
+  οnclick="f.outerHTML=f.outerHTML"
  @change="handleFileChange"/>
重写了暂停和恢复的功能,实际上,主要是增加了暂停和恢复的状态

之前的处理逻辑太简单粗暴,存在诸多问题。现在将状态定位在每一个文件之上,这样恢复上传时,直接跳过即可

封装组件

写了一大堆,其实以上代码你直接复制也无法使用,这里我将此封装了一个组件。大家可以去github下载文件,里面有使用案例 ,若有用记得随手给个star,谢谢!

偷个懒,具体封装组件的代码就不列出来了,大家直接去下载文件查看,若有不明白的,可留言。

组件文档

Attribute

参数类型说明默认备注
headersObject设置请求头
before-uploadFunction上传文件前的钩子,返回false则停止上传
acceptString接受上传的文件类型
upload-argumentsObject上传文件时携带的参数
with-credentialsBoolean是否传递Cookiefalse
limitNumber最大允许上传个数00为不限制
on-exceedFunction文件超出个数限制时的钩子
multipleBoolean是否为多选模式true
base-urlString由于本组件为内置的AXIOS,若你需要走代理,可以直接在这里配置你的基础路径
chunk-sizeNumber每个切片的大小10M
threadsNumber请求的并发数3并发数越高,对服务器的性能要求越高,尽可能用默认值即可
chunk-retryNumber错误重试次数3分片请求的错误重试次数

Slot

方法名说明参数备注
header按钮区域
tip提示说明文字

后端接口文档:按文档实现即可



代码地址:https://github.com/pseudo-god...

接口文档地址 https://docs.apipost.cn/view/...

查看原文

赞 80 收藏 56 评论 13

hellorayza 赞了文章 · 2020-08-28

前端常用插件utils汇总

工具库 || 数据处理

underscore - JavaScript的实用程序带库

lodash - 是一个一致性、模块化、高性能的 JavaScript 实用工具库。

表单验证

async-validator – 验证表单异步

---jquery

jquery-validation - 您现有的表单提供了插入式验证,同时使各种自定义适合您的应用程序变得非常容易

图片懒加载

---JavaScript

lazyload - 用于延迟加载图像

lazyload - LazyLoad是一种快速,轻巧和灵活的脚本,通过仅在内容图像,视频和iframe进入视口时加载它们来加快Web应用程序的速度

---vue

vue-lazyload - 一个Vue.js插件,用于将图像或组件延迟加载到应用程序中。

---react

react-lazyload - 延迟加载组件,图像或任何与性能有关的内容

图片预览

类似朋友圈

PhotoSwipe- 适用于移动和pc,模块化,框架独立的JavaScript图像库

满足聊天递增图片的需求

viewerjs - JavaScript图像查看器

fancybox - jQuery lightbox脚本,用于显示图像,视频等。触摸启用,响应迅速且可完全自定义

---vue

vue-picture-preview - 移动端、PC 端 Vue.js 图片预览插件

文件上传

---JavaScript

DropzoneJS - 提供带有图像预览的拖放文件上传

Web Uploader - 以HTML5为主的现代文件上传组件(百度)

jQuery-File-Upload - 具有多个文件选择,拖放支持,进度条,验证和预览图像,jQuery的音频和视频。

---vue

vue-dropzone - Dropzone.js的Vue.js组件-带有图像预览的拖放文件上传实用程序

单选框/复选框相关

---jquery

iCheck – 增强复选框和单选按钮

选择框

Choices - 轻量级库,用于制作高度可自定义的选择框、文本区域和其他输入表单。类似于select2和selectize,但没有依赖jquery。

Chosen - 一个使较长的笨拙的选择框更友好的库。

Select2 - 基于jQuery的选择框的替代品。它支持搜索,远程数据集和结果的无限滚动。

bootstrap-select - jQuery插件通过直观的多选,搜索等功能将选择元素带入21世纪

tree树形

---jquery

Bootstrap Tree View - Tree View for Twitter Bootstrap

zTree - 一个依靠 jQuery 实现的多功能 “树插件”

无限滚动

Infinite Scroll - 自动添加下一页

---vue

vue-infinite-scroll - vue.js的无限滚动指令

列表拖拽

Sortable - 是一个JavaScript库,用于在现代浏览器和触摸设备上对拖放列表进行重新排序

MDN拖拽文档

---vue

Vue.Draggable - 基于Sortable.js的Vue拖放组件

---react

react-sortablejs - 在成熟的拖放库Sortable之上构建的React组件

元素拖曳

draggabilly - 使该shiz可拖动

自定义滚动条

jScrollPane-跨浏览器自定义滚动条

perfect-scrollbar - 简约但完美的自定义滚动条插件

进度条

nprogress - 对于细长的进度条,例如YouTube,Medium等上的进度条

---github类似页面进度条

vue-progressbar - vue的轻量级进度条

cookie管理

js-cookie - 一个简单,轻巧的JavaScript API,用于处理浏览器cookie

WebSocket

ws - 简单易用,为Node.js开创了经过快速且经过全面测试的WebSocket客户端和服务器

socket.io - 实时应用程序框架

---vue

Vue-Socket.io - Vuejs和Vuex的Socket.io实现

JavaScript 动画效果库

Vue官网推荐

Velocity - 是一个简单易用、高性能、功能丰富的轻量级JS动画库

Animate.css - 一款强大的预设css3动画库

tween.js - JavaScript补间引擎可简化动画

额外效果

transformjs - 腾讯使CSS3转换超级容易

scenejs

midnight.js - 文字颜色随着背景变

vue-countTo - 在指定的持续时间内计入目标数量

CSS shake - 抖动动画

轮播效果

swiper5 - 纯javascript打造的滑动特效插件,面向手机、平板电脑等移动终端

iSlider - 适用于Mobile WebApp,HTML5 App,Hybrid App的平滑移动触摸滑块

---vue

vue-awesome-swiper - 基于Swiper4,适用于Vue的轮播组件

瀑布流展示

scrollreveal - 在元素滚动到视图时对其进行动画处理

scrollreveal 的演示地址

pc桌面级通知

push.js - 世界上最通用的桌面通知框架

响应式媒体查询

react中antd框架grid组件使用

enquire.js - JavaScript中的媒体查询

操作引导js工具

Intro.js - 为您的网站和项目提供新功能介绍和逐步用户指南的更好方法

driver.js —— 前端操作引导js工具

编辑器工具

editor.js - 具有干净JSON输出的块样式编辑器

UEditor - 由百度web前端研发部开发所见即所得富文本web编辑器

Markdown编辑器

SimpleMDE-Markdown - 一个简单,美观,可嵌入的JavaScript Markdown编辑器

simditor - 方便快捷的所见即所得编辑器

---想掘金一样的markdown解析器(双栏)

marked – markdown解析器

视频

flv.js - 用纯JavaScript编写的HTML5 Flash Video(FLV)播放器(bilibili)

video.js - Video.js是专为HTML5世界打造的网络视频播放器。它支持HTML5和Flash视频

html5media - 简单的h5player,轻量级

jwplayer - 被大量网站使用

日期选择 || 日历

Pikaday - 令人耳目一新的JavaScript Datepicker —轻巧,没有依赖关系,模块化CSS

bootstrap-datepicker - Twitter引导程序(@twbs)的 日期选择器

时间选择

有vue的版本,也支持日期选择

Flatpickr - 轻巧,功能强大的javascript datetimepicker,无依赖项

时间处理

Moment.js - 在JavaScript中解析,验证,操作和显示日期和时间。

模拟数据

Mock - 模拟数据生成器

代码注释

JSDoc - 是JavaScript的API文档生成器

触摸方面 || 手势

PhyTouch - 腾讯丝般顺滑的触摸运动方案

hammer.js - 一个用于多点触摸手势的javascript库

better-scroll - 受iscroll的启发,它支持更多功能并具有更好的滚动性能

loading加载动画

loaders.css - 令人愉悦且注重性能的纯CSS加载动画

css-spinners - 使用CSS和最少的HTML标记制作的简单CSS旋转器和控件

progressbar.js - 反应灵敏的进度条

通知组件

notie - 干净,简单的javascript通知,输入和选择套件,没有依赖项

全屏滚动

fullPage.js - 轻松创建全屏滚动网站

pagePiling.js - Alvaro Trigo的pagePiling插件。创建滚动显示的部分

fullPage.js - 专注于移动端

---vue

Vue-fullpage.js - fullPage.js的官方Vue.js包装器

模板引擎

sodajs - 腾讯重量轻但功能强大的JavaScript模板引擎

list || table组件

vue-easytable - vue table 组件,支持 单元格合并、单元格编辑、多表头固定、多列固定、列拖动、排序、自定义列、条件过滤、分页

list.js - 完美的库,可为表,列表和各种HTML元素添加搜索,排序,过滤器和灵活性

浏览器属性

bowser - 浏览器属性,例如名称,版本,渲染引擎

excel处理

sheetjs - 电子表格数据工具包

exceljs - 读取,操作并将电子表格数据和样式写入XLSX和JSON。

网格布局库

Masonry - 级联网格布局库

FFmpeg(c模块 视频帧预览)

ffmpeg

WebAssembly | MDN

地图

Leaflet - 适用于移动设备的交互式地图的JavaScript库

表情包扩展

twemoji - 一个简单的库,可在所有平台上提供标准Unicode 表情符号支持。

浏览器测试

cypress - 对浏览器中运行的所有内容进行快速,轻松和可靠的测试

浏览器代码编辑器

[]monaco-editor - 基于浏览器的代码编辑器](https://github.com/microsoft/...

滑动拼图验证

vue-puzzle-verification
vue-puzzle-vcode

值得学习库

weui

额外推荐

turn.js - 做一本书,带漂亮的翻页的效果

查看原文

赞 50 收藏 44 评论 0

认证与成就

  • 获得 0 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-07-24
个人主页被 574 人浏览