0%

由于七牛提供的 js sdk 依赖较多,所以对于单纯上传文件的需求,采用表单上传会是一种更简便的方法。

function upload(file, token, params) {
  const formData = new FormData();
  formData.append('token', token);
  formData.append('file', file);
  Object.keys(params).forEach(key => {
    formData.append(`x:${key}`, params[key]);
  });

  return post('//upload.qiniup.com', formData);
}

表单上传-七牛开发者中心

步骤

  1. input 输入文件
  2. FileReader 将文件读作 DataURL
  3. img 元素载入 URI
  4. 使用 CanvasRenderingContext2D.drawImage() 在 canvas 中绘制指定图片
  5. 使用 HTMLCanvasElement.toDataURL() 将图片转换成 data URI
  6. a 元素 download 下载图片

代码

<input id="file-input" type="file" accept="image/*">

<script>
  var doc = document;

  var input = doc.getElementById('file-input');

  function exportImageFileFromCanvas(canvas) {
    var dataURL = canvas.toDataURL('image/webp', .8);
    var a = doc.createElement('a');
    a.href = dataURL;
    a.setAttribute('download', 'img.webp');
    doc.body.appendChild(a);
    a.click();
    doc.body.removeChild(a);
    a = null;
  }

  function url2canvas(url) {
    return new Promise((resolve, reject) => {
      const image = document.createElement('img');
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');

      image.onload = event => {
        const img = event.target;
        const height = image.naturalHeight;
        const width = image.naturalWidth;
        canvas.height = height;
        canvas.width = width;
        ctx.drawImage(img, 0, 0, width, height);
        resolve(canvas);
      };
      image.onerror = err => {
        reject(err);
      };
      image.src = url;
    });

  }

  function readFileAsDataURL(file) {
    return new Promise((resolve, reject) => {
      const fr = new FileReader();
      fr.onload = event => {
        resolve(event.target.result);
      };
      fr.onerror = err => {
        reject(err);
      }
      fr.readAsDataURL(file);
    });
  }

  function handleFileChange(e) {
    var files = e.target.files;
    if (!files || !files.length) return;
    var file = e.target.files[0];

    readFileAsDataURL(file)
      .then(url2canvas)
      .then(exportImageFileFromCanvas);
  }

  input.addEventListener('change', handleFileChange);
</script>

const cluster = require('cluster')
const http = require('http')
const numCPUs = require('os').cpus().length

const rssWarn = (50 * 1024 * 1024),
  heapWarn = (50 * 1024 * 1024)

const workers = {}

if (cluster.isMaster) {
  for (let i = 0; i < numCPUs; i++) {
    createWorker()
  }

  setInterval(() => {
    const time = Date.now()
    for (pid in workers) {
      if (workers.hasOwnProperty(pid) &&
        workers[pid].lastCb + 5000 < time) {
        console.log('Long runing worker ' + pid + ' killed')
        workers[pid].worker.kill()
        delete workers[pid]
        createWorker()
      }
    }
  }, 1000)
} else {
  http.Server((req, res) => {
    if (Math.floor(Math.random() * 200) === 4) {
      console.log(`Stopped ${process.pid} from ever finishing`)
      while (true) {
        continue
      }
    }
    res.writeHead(200)
    res.end(`hello world from ${process.pid}\n`)
  }).listen(8000)

  setInterval(function report() {
    process.send({
      cmd: 'reportMem',
      memory: process.memoryUsage(),
      process: process.pid
    })
  }, 1000)
}

function createWorker() {
  const worker = cluster.fork()
  const pid = worker.process.pid
  console.log(`Created worker: ${pid}`)

  workers[pid] = {
    worker: worker,
    lastCb: Date.now() - 1000
  }

  worker.on('message', m => {
    if (m.cmd === 'reportMem') {
      workers[m.process].lastCb = Date.now()

      if (m.memory.rss > rssWarn) {
        console.log(`Worker ${m.process} using too much memory.`)
      }
    }
  })
}

M3U8

M3U8 是 Unicode 版本的 M3U,用 UTF-8 编码。

M3UM3U8 文件都是苹果公司使用的 HTTP Live Streaming (HLS) 格式的基础,这种格式可以在 iPhone 和 Macbook 等设备播放。

HLS

HTTP Live Streaming (HLS) 是一个由苹果公司提出的基于 HTTP 的流媒体网络传输协议。它的工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些。当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。在开始一个流媒体会话时,客户端会下载一个包含元数据的 extended M3U (m3u8) playlist 文件,用于寻找可用的媒体流。

HLS 只请求基本的 HTTP 报文,与实时传输协议 (RTP) 不同,HLS 可以穿过任何允许 HTTP 数据通过的防火墙或者代理服务器。它也很容易使用内容分发网络来传输媒体流。

由于 Android 和 iOS 原生支持 HLSHLS 已成为移动设备流媒体的事实标准。有很多无关平台的原因来推荐这个格式:

  • 支持(客户端驱动)自适应比特率选择
  • 通过标准的 HTTP 端口传输
  • 简单、基于文本的 manifest 格式
  • 不需要专有流服务器

然而除了 Safari,主流的桌面浏览器都不支持 HLS。这使得 web 开发人员要维护多个格式的同一视频文件,或者放弃使用 html 的 video 来提供最好的桌面浏览体验。

在这里,我们在支持 Media Source Extensions 的浏览器使用 polyfill 来使用 HLS。

Media Source Extensions

Media Source Extensions (MSE) 为支持无插件的基于 Web 的流媒体提供功能。使用 MSE,流媒体可以通过 JavaScript 创建,并用 <audio><video> 元素播放。

播放方法

采用 videojs + videojs-contrib-hls


# .bashrc
export SASS_BINARY_SITE="https://npm.taobao.org/mirrors/node-sass"
export ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/"
export PHANTOMJS_CDNURL="https://npm.taobao.org/mirrors/phantomjs"
export CHROMEDRIVER_CDNURL="https://npm.taobao.org/mirrors/chromedriver"

更多:Taobao Mirrors

Underscore.js 1.8.3

// https://github.com/jashkenas/underscore/blob/1.8.3/underscore.js#L6

(function() {
}.call(this));

立即执行函数,避免污染外部环境。

// https://github.com/jashkenas/underscore/blob/1.8.3/underscore.js#L12

var root = this;

获取根对象,selfglobal

// https://github.com/jashkenas/underscore/blob/1.8.3/underscore.js#L18

var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;

压缩变量名长度,节省字节。

// https://github.com/jashkenas/underscore/blob/1.8.3/underscore.js#L21

var
  push             = ArrayProto.push,
  slice            = ArrayProto.slice,
  toString         = ObjProto.toString,
  hasOwnProperty   = ObjProto.hasOwnProperty;

为一些方法创建引用,方便访问。

// https://github.com/jashkenas/underscore/blob/1.8.3/underscore.js#L29

var
  nativeIsArray      = Array.isArray,
  nativeKeys         = Object.keys,
  nativeBind         = FuncProto.bind,
  nativeCreate       = Object.create;

ES5 中原生支持的一些方法。

// https://github.com/jashkenas/underscore/blob/1.8.3/underscore.js#L36

var Ctor = function(){};

一个空的构造函数。


阅读全文 »

代码: https://github.com/hanai/js-d3

坐标系

3d cube

渲染

投影

将三维空间中的坐标 <x, y, z> 投影到平面上,得到投影的坐标 <x', y'>

渲染函数

遍历每一个要渲染的对象,遍历对象的各个平面,对平面各个顶点由函数 project() 计算出投影在平面上的二维坐标,依次连接平面各顶点。

function render(objects, ctx, dx, dy) {
  // Clear the previous frame
  ctx.clearRect(0, 0, 2 * dx, 2 * dy);

  // for each object
  objects.forEach(object => {
    // for each face
    object.faces.forEach(face => {
      face.forEach((vertex, vertexIdx) => {
        var P = project(vertex);

        if (vertexIdx === 0) {
          // draw the first vertex
          ctx.beginPath();
          ctx.moveTo(P.x + dx, -P.y + dy);
        } else {
          // draw the other vertices
          ctx.lineTo(P.x + dx, -P.y + dy);
        }
      });

      // close the path and draw the face
      ctx.closePath();
      ctx.stroke();
      ctx.fill();
    });
  });
}

正视图

function project(M) {
    return new Vertex2D(M.x, M.z);
}

透视图

cube project

point project

由三维空间中的顶点 M(x, y, z),我们希望的到平面上的投影 M' 的坐标 (x', z')

point in plane

如图,由截线定理可得 x' = d / y * x,故:

function project(M) {
    // Distance between the camera and the plane
    var d = 200;
    var r = d / M.y;

    return new Vertex2D(r * M.x, r * M.z);
}

其它

旋转鼠标旋转立方体

rotate cube

绑定事件 mousedownmousemove,由 event.clientXevent.clientY 可得 Δx, Δy,换算得水平旋转角度 theta 与竖直旋转角度 phi


参考

hash

监听 hashchange 事件。

window.addEventListener('hashchange', function() {
  console.log(window.location.hash);
});

history

使用 History API

window.addEventListener('popstate', function(event) {
  console.log(event.state);
});

popstate 事件在 history 改变时触发,调用 history.pushState() 创建新的历史项,或调用 history.replaceState() 替换新的历史项,那么 popstate 事件的 state 属性会包含历史项状态对象(state object)的拷贝(event.state)。

需要注意的是调用 history.pushState()history.replaceState() 不会触发 popstate 事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在 Javascript 代码中调用history.back())。

2D 图形的坐标

笛卡尔坐标系

笛卡尔坐标系

原点 (0, 0) 在元素的左上角,每一个点由向量 (x, y) 标记。

进行变换的时候,对每个点做矩阵乘法:

$$ \left\lgroup \matrix{a & c\cr b & d} \right\rgroup \left\lgroup \matrix{x \cr y} \right\rgroup = \left\lgroup \matrix{ax + cy\cr bx + dy} \right\rgroup $$

可以在一行中应用多次变换:

$$ \left\lgroup \matrix{a_1 & c_1\cr b_1 & d_1} \right\rgroup \left\lgroup \matrix{a_2 & c_2\cr b_2 & d_2} \right\rgroup = \left\lgroup \matrix{a_1a_2 + c_1b_2 & a_1c_2 + c_1d_2\cr b_1a_2 + d_1b_2 & b_1c_2 + d_1d_2} \right\rgroup $$

使用这种标记可以描述多种常用的变换:旋转、缩放、倾斜。事实上所有这些变换都是线性函数可以描述的。使用这种标记时有一种非线性的变换需要当做特例,就是平移。平移向量 (tx, ty) 必须作为两个附加的参数来表述。

函数定义的变换 matrix()

语法

matrix(a, b, c, d, tx, ty)
  • a, b, c, d 描述线性变换的数字
  • tx, ty 描述平移的数字

示例

.pane {
  background: green;
  width: 200px;
  height: 200px;
}

.box {
  float: left;
  background: blue;
  width: 50px;
  height: 50px;
  transform-origin: 0 0;
}

.more {
  float: left;
  width: 50px;
  height: 50px;
  background: red;
}
上下左右各平移 4px
.box {
  transform: matrix(1, 0, 0, 1, 4, 4);
}
放大两倍
.box {
  transform: matrix(2, 0, 0, 2, 0, 0);
}
旋转
.box {
  transform: matrix(2, 1, -1, 2, 0, 0);
}

Tip: 为什么平移不是线性变换?

一个线性变换 f,会使 f(x + y) = f(x) + f(y) 成立

而平移:y = ax + b

用 WeakMap 实现

let _counter = new WeakMap();
let _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    let counter = _counter.get(this);
    if (counter < 1) return;
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}

WeakMap 的键名是对象的弱引用,其所对应的对象可能会被自动回收,只要不暴露 WeakMap,私有数据就是安全的。

Pros.

  • 私有属性命名不会冲突

Cons.

  • 代码不够优雅

用 Symbol 实现

const _counter = Symbol('counter');
const _action = Symbol('action');

class Countdown {
  constructor(counter, action) {
    this[_counter] = counter;
    this[_action] = action;
  }
  dec() {
    if (this[_counter] < 1) return;
    this[_counter]--;
    if (this[_counter] === 0) {
      this[_action]();
    }
  }
}

每一个 Symbol 都是唯一的,这就是为什么使用 Symbol 的属性键名之间不会冲突的原因。并且,Symbol 某种程度上来说是隐式的,但也并不完全是:

let c = new Countdown(2, () => console.log('DONE'));

console.log(Object.keys(c));
// []
console.log(Reflect.ownKeys(c));
// [Symbol(counter), Symbol(action)]

Pros.

  • 私有属性命名不会冲突

Cons.

  • 代码不够优雅
  • 不太安全,可以通过 Reflect.ownKeys() 列表一个对象所以的属性键名