GithubHelp home page GithubHelp logo

articles's Introduction

经验文章

工作生活点滴,记录总结经验留存,力求完善,在路上~~

前端开发工作流

计算机基础

计算机操作系统64位和32位的区别及原理

汇编语言入门

TCP 协议简介

什么是MTU?为什么MTU值普遍都是1500?

字符编码 ASCII、URI 、UNICODE、Base64

理解 WebSocket 原理

HTTPS 传输协议加密原理分析

HTTP3

算法

十大排序排序算法

算法(algorithms)

JS 简单实现(FIFO 、LRU、LFU)缓存淘汰算法

浏览器相关

HSTS详解

聚焦 Web 性能指标 TTI

前端性能监控 Performance

COOKIE与SESSION知识

浏览器缓存知识

浏览器的基本工作原理

H5直播起航

CSR、SSR、NSR、ESR 理清前端渲染方案

图形变换

CSS 中矩阵变换 matrix()、matrix3d()

聊聊 SVG 基本形状转换那些事

SVG坐标系和变换

Cairo 二维矢量图形库

游戏开发 - 物理引擎 & 渲染引擎

WebGL 框架选型

Matter.js 2D 物理引擎介绍

多端开发

Sketch 插件开发实践

React Native 跨平台思考

React Native 与小程序运行机制

Javascript

| 基础知识

Fetch 实现 Abort

如何避免 JavaScript 长递归导致的堆栈溢出?

前端大文件上传

JavaScript 错误处理机制

[转载] 2018 来谈谈 Web Component

JavaScript 浮点数运算的精度问题

JavaScript 大数运算精度问题,如何实现两个大数相加?

前端数据缓存方案

HTTP请求中的Form Data与Request Payload的区别

Service Worker理解使用

EventEmitter 理解

译文: Prefetching, preloading, prebrowsing

JavaScript 中 for in 循环和数组的问题

Blob对象

移动端 Scroll Event 思考

防抖动(Debounce)和节流阀(Throttle)

代码复用之继承 duplicate

stopImmediatePropagation、Event.initEvent()、element.dispatchEvent(event)简单记录

正则replace方法

JavaScript 事件委托代码片段 question

eval()与new Function()

JavaScript 模块的循环加载 -- 转载

| Javascript 编译

代码编译 - Babel Compiler

| 内存机制

Node.js 内存管理和 V8 垃圾回收机制

JavaScript的垃圾回收机制

JavaScript 内存机制

| 模块框架

React 核心知识点 -- Virtual Dom 与 Diff

React Hooks

React学习:状态(State) 和 属性(Props)

React 错误边界(Error Boundaries)

Vue实现原理

vue2.0 render函数介绍中 Array.apply(null, { length: 20 }) 引起思考

CommonJS, AMD, CMD 和 原生 JS 一些感悟

前端测试框架 Jest

数据库

数据库的最简单实现

数据库表连接的简单解释

Mongodb 对内嵌数组的增删改查操作

MongoDB 提升性能的18原则

服务端

Node.js 同步异步、阻塞与非阻塞

理解Node.js 中的进程与线程 -- 转载

Node.js 事件循环,定时器和 process.nextTick()

Node服务性能监控

前端工程化 GraphQL

SSO、OAuth2.0、JWT 登录与授权理解

学习 Restful HTTP API 设计

用 GitLab CI 进行持续集成

开篇 Serverless(无服务)基础知识

Koa2原理详解

Web服务高并发与性能调试

CDN 回源与CDN 多级缓存原理

CDN 带宽与上传下载速率关系?

| RPC & 序列化与反序列化

JSON-RPC 2.0 规范(中文版转载)

RPC 框架介绍

高效的数据压缩编码方式 Protobuf

思维导图 & 流程图 & 架构图

OmniGraffle 绘制流程图

九种常用的UML图总结

方法论与工具

lerna 和 yarn workspace 的 monorepo 工作流

自定义 Eslint 开发

iOS 模拟器调试

VS Code 项目配置路径别名跳转

npx 是什么

Git 几个特殊命令

git commit message 中使用 emoji

Mac 实用技巧

Sublime Snippet 代码段

chrome使用技巧集锦

Python 版本管理

Mac 端环境变量配置

VAGRANT 使用

articles's People

Contributors

dependabot[bot] avatar pfan123 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

articles's Issues

EventEmitter 理解

EventEmitter 事件发射器是一种发布,订阅模式,是event 模块提供的一个对象,用来注册事件可触发事件。Node 中的异步操作都是基于EventEmitter 来实现的。

EventEmitter主要API

  • emitter.on(event, listener) 注册一个事件
  • emitter.once(event, listener) 注册一个一次性的事件,触发后就被抹掉
  • emitter.removeListener(event, listener) 在时间队列中剔除某一个事件
  • emitter.removeAllListeners([event]) 删除整个事件队列,或多个事件
  • emitter.listeners(event) 返回某些事件 emitter.emit(event, [arg1], [arg2], […]) 触发事件,可传入具体参数

EventEmitter使用方式

  • 实例化获取EventEmitter
const events = require('events');
 
const emitter = new events.EventEmitter();
 
// 绑定sayHi事件,可以绑定多个同名事件,触发时会顺序触发
emitter.on('sayHi', function(someone){
    console.log("我是", someone)
})
emitter.on('sayHi', function(someone){
    console.log("我就是", someone)
})
 
// 触发sayHi事件
emitter.emit('sayHi', 'pfan');
 
// 我是pfan
// 我就是pfan

继承获取事件对象的方法

const util = require('util');
 
const events = require('events');
 
// 创建自定义对象
let Cat = function (name) {
    this.name = name;
}
 
// 继承events.EventEmitter
util.inherits(Cat, events.EventEmitter);
 
// 创建自定义对象实例
let Tom = new Cat('Tom');
 
// 绑定sayHiTo事件
Tom.on('sayHi', function(someone){
    // this指向实例Tom
    console.log(this.name," sayHiTo ", someone)
})
 
Tom.emit('sayHiTo', 'pfan')
 
// 输出
 
// Tom sayHiTo pfan

Node.js中大部分的模块,都继承自Event模块,来异步操作处理如request、stream

常见Event Emitter工具库实现

可以采用现有的成熟框架,通过继承即可食用:

Component: component/emitter
Bower or standalone: Wolfy87/EventEmitter

EventEmitter的实现原理

EventEmitter实现并不难,可以实现一个简单的版本,加深理解。具备以下基本功能:

  • on: 为特定事件添加监听器
  • off: 为特定事件移除监听器
  • emit: 触发特定事件
  • once: 注册只执行一次的监听器
function Emitter(){
  this.events = {}
}

Emitter.prototype.on = function(type, listener){
  // 在事件对象中加入新的属性
  // 确保新的属性以数组的形式保存
  this.events[type] = this.events[type] || [];
  // 在数组中加入事件处理函数
  this.events[type].push(listener);
}

Emitter.prototype.off = function(type, listener){
	if(this.events && this.events[type]){
		delete this.events[type]
		listener(...arguments)
	}
}

Emitter.prototype.once = function(type, listener){
	let self = this
	this.on(type, function(){
		self.off(type)
		listener(...arguments)
	})
}

Emitter.prototype.emit = function(type, arg){
  if(this.events[type]) {// 如果事件对象中含有该属性
    this.events[type].forEach(function(listener){
      listener(arg)
    })
  }
}
module.exports = Emitter;

component/emitter 源码分析

/**
 * 使用 `Emitter` 方式
 */
var Emitter = require('emitter');
var emitter = new Emitter;
emitter.emit('something');

on(event, listener)
为指定事件注册一个监听器,接受一个字符串 event 和一个回调函数。

emit(event, [arg1], [arg2], [...])
按参数的顺序执行每个监听器,如果事件有注册监听返回 true,否则返回 false。

/**
 * 使用 `Emitter` 方式
 */


/**
 * 设置导出 `Emitter` 类
 */

if (typeof module !== 'undefined') {
  module.exports = Emitter;
}

/**
 * Initialize a new `Emitter`.
 *
 * @api public
 */

function Emitter(obj) {
  if (obj) return mixin(obj);
};

/**
 * Mixin the emitter properties.
 * 浅拷贝 Emitter.prototype
 * @param {Object} obj
 * @return {Object}
 * @api private
 */

function mixin(obj) {
  for (var key in Emitter.prototype) {
    obj[key] = Emitter.prototype[key];
  }
  return obj;
}

/**
 * Listen on the given `event` with `fn`.
 * 事件只执行一次
 * @param {String} event
 * @param {Function} fn
 * @return {Emitter}
 * @api public
 */

Emitter.prototype.on =
Emitter.prototype.addEventListener = function(event, fn){
  this._callbacks = this._callbacks || {};
  (this._callbacks['$' + event] = this._callbacks['$' + event] || [])
    .push(fn);
  return this;
};

/**
 * Adds an `event` listener that will be invoked a single
 * time then automatically removed.
 *
 * @param {String} event
 * @param {Function} fn
 * @return {Emitter}
 * @api public
 */

Emitter.prototype.once = function(event, fn){
  function on() {
    this.off(event, on);
    fn.apply(this, arguments);
  }

  on.fn = fn;
  this.on(event, on);
  return this;
};

/**
 * Remove the given callback for `event` or all
 * registered callbacks.
 * 移除注册的监听器
 * @param {String} event
 * @param {Function} fn
 * @return {Emitter}
 * @api public
 */

Emitter.prototype.off =
Emitter.prototype.removeListener =
Emitter.prototype.removeAllListeners =
Emitter.prototype.removeEventListener = function(event, fn){
  this._callbacks = this._callbacks || {};

  // all
  if (0 == arguments.length) {
    this._callbacks = {};
    return this;
  }

  // specific event
  var callbacks = this._callbacks['$' + event];
  if (!callbacks) return this;

  // remove all handlers
  if (1 == arguments.length) {
    delete this._callbacks['$' + event];
    return this;
  }

  // remove specific handler
  var cb;
  for (var i = 0; i < callbacks.length; i++) {
    cb = callbacks[i];
    if (cb === fn || cb.fn === fn) {
      callbacks.splice(i, 1);
      break;
    }
  }
  return this;
};

/**
 * Emit `event` with the given args.
 * 事件发射告诉监听器开始执行
 * @param {String} event
 * @param {Mixed} ...
 * @return {Emitter}
 */

Emitter.prototype.emit = function(event){
  this._callbacks = this._callbacks || {};
  var args = [].slice.call(arguments, 1)
    , callbacks = this._callbacks['$' + event];

  if (callbacks) {
    callbacks = callbacks.slice(0);
    for (var i = 0, len = callbacks.length; i < len; ++i) {
      callbacks[i].apply(this, args);
    }
  }

  return this;
};

/**
 * Return array of callbacks for `event`.
 * 返回监听
 * @param {String} event
 * @return {Array}
 * @api public
 */

Emitter.prototype.listeners = function(event){
  this._callbacks = this._callbacks || {};
  return this._callbacks['$' + event] || [];
};

/**
 * Check if this emitter has `event` handlers.
 * 检测是否含有监听器
 * @param {String} event
 * @return {Boolean}
 * @api public
 */

Emitter.prototype.hasListeners = function(event){
  return !! this.listeners(event).length;
};

Component: component/emitter
Bower or standalone: Wolfy87/EventEmitter
Node.js EventEmitter
理解Event Emitter
深入浅出Node.js(四):Node.js的事件机制

前端数据缓存方案

缓存一直以来作为性能优化的一环被广泛使用,

  • 数据库缓存
  • 代理服务器缓存
  • CDN 缓存
  • 浏览器缓存

等等,几乎在每一层,都有缓存存在。本篇博客讨论的不是上面这些缓存,而是由我们自己控制的缓存,具体来说是「请求」的缓存,如何优化请求的缓存让我们的应用更好。

一、需要缓存吗?

1、减少不必要的请求

在我们的应用中,会存在一些很少改动的数据,但这些数据又要从后端获取。
典型的如下拉框的内容,可以是行业、职业、角色等等,这类数据在很长一段时间内都是不会改变的,至少在应用的使用过程中是不会改变的,而我们却在每次打开页面或者是切换页面时,就重新向后端请求了一次,这类请求完全是没有必要的,通过对类请求的数据进行缓存,可以大大减小对服务器的压力。

2、更快的访问速度

在访问一些数据时,不再重新向后端请求,而是直接使用缓存中的数据,访问速度毫无疑问会更加快,用户体验也必然会更好。

二、哪些数据可以缓存?

在单页面应用中,所有数据来源都是接口请求,但并不是所有数据都需要,或者说能被缓存。

讨论的都是GET请求,PUTPOST等绝对是不能被缓存的。

判断标准是根据请求的频次,这里给出不同请求频次的定义,「高频」、「中频」、「低频」。

  • 高频:通过交互就可以请求的数据,如查询接口
  • 中频:页面切换时才会请求的数据,如页面数据
  • 低频:只有在应用初始化时才会请求,如获取当前登录用户信息

具体的可以根据自己项目进行调整。

举个例子,页面展示「图书列表」,可以对该列表进行查询、切换页码、删除。根据上面的定义得出如下判断

  • 1、类别下拉框,只在访问页面时请求,用户交互不会触发重新请求,所以属于「中频」。
  • 2、获取图书列表,可以通过切换分页请求,所以属于「高频」。
  • 3、查询功能,同样可以通过点击请求,所以属于「高频」。
  • 4、当前登录用户名,在单页面应用中切换页面也不会重新请求,所以属于「低频」。
  • 5、删除功能,不缓存。

在确定请求频次后,还要判断「该数据是否会被修改」,假设我们认为「获取图书列表」是高频所以缓存,确实能解决用户切换分页时频繁请求数据的问题,但如果用户删除了某条记录,在切换分页后会发现该数据还存在

所以当数据是可以被新增、删除、修改时,就不能缓存该数据了。

或许可以设置一种机制,当接口存在这三种请求,缓存即过期,将重新请求。

三、内存还是 localstorage ?

在确定了哪些数据需要缓存后,如何将数据缓存呢?
由于要使用,当然保存在一个全局变量会方便很多,也就是保存在内存中。如果说保存在localStorage,每次使用时还是要读取到内存中,那干脆都保存到内存,localStorage作为持久化方案,保存一些低频、不会变化的数据,如「用户信息」,如果有的话。

四、具体代码如何写

能否实现对已有系统改造量最小,甚至做到无需修改呢?
在目前普通使用redux的情况下,在「请求数据」这里做比较好,一开始想到,一般我们使用axios请求数据,当请求需要缓存的接口时,就直接返回缓存好的,通过拦截器可以轻松做到。
但问题来了,如何标志哪些接口是需要缓存,哪些又是不需要的呢,通过method判断可以解决一部分,但还是不够完善。

要解决这个问题,确实应该在请求前就处理好,如果不使用拦截器,我们可以自己做一次处理,以具体代码来说:

// api.js
export function fetch(params) {
    return axios.get('/api/books', { params });
}

export function fetchCategories() {
    return axios.get('/api/categories');
}

export function delete(id) {
    return axios.delete(`/api/books/${id}`);
}

简单粗暴的做法,直接加一个缓存对象,一旦请求,就加入缓存,否则就请求。

const cache = {};
export function fetchCategories() {
    const url = '/api/categories';
    if (cache[url]) {
        return cache[url];
    }
    const res = axios.get(url);
    cache[url] = res;
    return res;
}

虽然简单粗暴,但这是核心逻辑,能够优化的就是如何优雅的写代码了。

1、现成的缓存库

这种需求肯定早有人想过,先来看一个已有的缓存库 mem(适合单页面使用,缓存cache是保存在内存里面,而不是sessionStorage、localStorage),

const mem = require('mem');

let i = 0;
const counter = async () => ++i;
const memoized = mem(counter);

(async () => {
	console.log(await memoized());
	//=> 1

	// The return value didn't increase as it's cached
	console.log(await memoized());
	//=> 1
})();

counter就是要被缓存的请求,第二次调用时,会返回之前的值,而不会再次调用该请求。

换成我们的代码,就是这样:

const mem = require('mem');
export const fetchCategories = mem(function() {
    return axios.get('/api/categories');
});

2、mem 在实际项目中的拓展

如果需求比较简单,目前应该就能够满足需求了。

但其实还可以更一步优化,举例来说,虽然上面提到「图书列表」会被删除修改,所以不应该缓存,但如果用户在页码之间来回切换,请求的频率还是很高的,而且这种情况下是完全可以缓存的,所以,判断两次请求的时间间隔,如果小于 5s,就返回缓存的结果,否则就不缓存。

当然这种需求mem的作者也考虑到了,就是过期时间,

const mem = require('mem');
export const fetchCategories = mem(function() {
    return axios.get('/api/categories');
}, {
    maxAge: 5000,
});

表示设置缓存有效期是 5s,5s 内多次请求,都会返回缓存,5s 后会重新请求。

上面写不太直观,我们可以使用修饰器来简化,但这种方式对原有代码调整很大,因为装饰器只能用于类与类的方法,所以代码变成这样:

import mem from 'mem';

/**
 * @param {MemOption} - mem 配置项
 * @return {Function} - 装饰器
 */
function m(options) {
  return (target, name, descriptor) => {
    const oldValue = descriptor.value;
    descriptor.value = mem(oldValue, options);
    return descriptor;
  };
}
class Api {
    @m({ maxAge: 5000 })
    fetchCategories() {
        return axios.get('/api/categories');
    }
}

五、参考

JavaScript 错误处理机制

Error 实例对象

JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。JavaScript 原生提供Error构造函数,所有抛出的错误都是这个构造函数的实例。

var err = new Error('出错了');
err.message // "出错了"

上面代码中,我们调用Error构造函数,生成一个实例对象errError构造函数接受一个参数,表示错误提示,可以从实例的message属性读到这个参数。抛出Error实例对象以后,整个程序就中断在发生错误的地方,不再往下执行。

JavaScript 语言标准只提到,Error实例对象必须有message属性,表示出错时的提示信息,没有提到其他属性。大多数 JavaScript 引擎,对Error实例还提供namestack属性,分别表示错误的名称和错误的堆栈,但它们是非标准的,不是每种实现都有。

  • message:错误提示信息
  • name:错误名称(非标准属性)
  • stack:错误的堆栈(非标准属性)

使用namemessage这两个属性,可以对发生什么错误有一个大概的了解。

if (error.name) {
  console.log(error.name + ': ' + error.message);
}

stack属性用来查看错误发生时的堆栈。

function throwit() {
  throw new Error('');
}

function catchit() {
  try {
    throwit();
  } catch(e) {
    console.log(e.stack); // print stack trace
  }
}

catchit()
// Error
//    at throwit (~/examples/throwcatch.js:9:11)
//    at catchit (~/examples/throwcatch.js:3:9)
//    at repl:1:5

上面代码中,错误堆栈的最内层是throwit函数,然后是catchit函数,最后是函数的运行环境。

原生错误类型

Error实例对象是最一般的错误类型,在它的基础上,JavaScript 还定义了其他6种错误对象。也就是说,存在Error的6个派生对象。

SyntaxError 对象

SyntaxError对象是解析代码时发生的语法错误。

// 变量名错误
var 1a;
// Uncaught SyntaxError: Invalid or unexpected token

// 缺少括号
console.log 'hello');
// Uncaught SyntaxError: Unexpected string

上面代码的错误,都是在语法解析阶段就可以发现,所以会抛出SyntaxError。第一个错误提示是“token 非法”,第二个错误提示是“字符串不符合要求”。

ReferenceError 对象

ReferenceError对象是引用一个不存在的变量时发生的错误。

// 使用一个不存在的变量
unknownVariable
// Uncaught ReferenceError: unknownVariable is not defined

另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果或者this赋值。

// 等号左侧不是变量
console.log() = 1
// Uncaught ReferenceError: Invalid left-hand side in assignment

// this 对象不能手动赋值
this = 1
// ReferenceError: Invalid left-hand side in assignment

上面代码对函数console.log的运行结果和this赋值,结果都引发了ReferenceError错误。

RangeError 对象

RangeError对象是一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number对象的方法参数超出范围,以及函数堆栈超过最大值。

// 数组长度不得为负数
new Array(-1)
// Uncaught RangeError: Invalid array length

TypeError 对象

TypeError对象是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用new命令,就会抛出这种错误,因为new命令的参数应该是一个构造函数。

new 123
// Uncaught TypeError: number is not a func

var obj = {};
obj.unknownMethod()
// Uncaught TypeError: obj.unknownMethod is not a function

上面代码的第二种情况,调用对象不存在的方法,也会抛出TypeError错误,因为obj.unknownMethod的值是undefined,而不是一个函数。

URIError 对象

URIError对象是 URI 相关函数的参数不正确时抛出的错误,主要涉及encodeURI()decodeURI()encodeURIComponent()decodeURIComponent()escape()unescape()这六个函数。

decodeURI('%2')
// URIError: URI malformed

EvalError 对象

eval函数没有被正确执行时,会抛出EvalError错误。该错误类型已经不再使用了,只是为了保证与以前代码兼容,才继续保留。

总结

以上这6种派生错误,连同原始的Error对象,都是构造函数。开发者可以使用它们,手动生成错误对象的实例。这些构造函数都接受一个函数,代表错误提示信息(message)。

var err1 = new Error('出错了!');
var err2 = new RangeError('出错了,变量超出有效范围!');
var err3 = new TypeError('出错了,变量类型无效!');

err1.message // "出错了!"
err2.message // "出错了,变量超出有效范围!"
err3.message // "出错了,变量类型无效!"

自定义错误

除了 JavaScript 原生提供的七种错误对象,还可以定义自己的错误对象。

function UserError(message) {
  this.message = message || '默认信息';
  this.name = 'UserError';
}

UserError.prototype = new Error();
UserError.prototype.constructor = UserError;

上面代码自定义一个错误对象UserError,让它继承Error对象。然后,就可以生成这种自定义类型的错误了。

new UserError('这是自定义的错误!');

throw 语句

throw语句的作用是手动中断程序执行,抛出一个错误。

if (x < 0) {
  throw new Error('x 必须为正数');
}
// Uncaught ReferenceError: x is not defined

上面代码中,如果变量x小于0,就手动抛出一个错误,告诉用户x的值不正确,整个程序就会在这里中断执行。可以看到,throw抛出的错误就是它的参数,这里是一个Error实例。

throw也可以抛出自定义错误。

function UserError(message) {
  this.message = message || '默认信息';
  this.name = 'UserError';
}

throw new UserError('出错了!');
// Uncaught UserError {message: "出错了!", name: "UserError"}

上面代码中,throw抛出的是一个UserError实例。

实际上,throw可以抛出任何类型的值。也就是说,它的参数可以是任何值。

// 抛出一个字符串
throw 'Error!';
// Uncaught Error!

// 抛出一个数值
throw 42;
// Uncaught 42

// 抛出一个布尔值
throw true;
// Uncaught true

// 抛出一个对象
throw {
  toString: function () {
    return 'Error!';
  }
};
// Uncaught {toString: ƒ}

对于 JavaScript 引擎来说,遇到throw语句,程序就中止了。引擎会接收到throw抛出的信息,可能是一个错误实例,也可能是其他类型的值。

try…catch 结构

一旦发生错误,程序就中止执行了。JavaScript 提供了try...catch结构,允许对错误进行处理,选择是否往下执行。

try {
  throw new Error('出错了!');
} catch (e) {
  console.log(e.name + ": " + e.message);
  console.log(e.stack);
}
// Error: 出错了!
//   at <anonymous>:3:9
//   ...

上面代码中,try代码块抛出错误(上例用的是throw语句),JavaScript 引擎就立即把代码的执行,转到catch代码块,或者说错误被catch代码块捕获了。catch接受一个参数,表示try代码块抛出的值。

如果你不确定某些代码是否会报错,就可以把它们放在try...catch代码块之中,便于进一步对错误进行处理。

try {
  f();
} catch(e) {
  // 处理错误
}

上面代码中,如果函数f执行报错,就会进行catch代码块,接着对错误进行处理。

catch代码块捕获错误之后,程序不会中断,会按照正常流程继续执行下去。

try {
  throw "出错了";
} catch (e) {
  console.log(111);
}
console.log(222);
// 111
// 222

上面代码中,try代码块抛出的错误,被catch代码块捕获后,程序会继续向下执行。

catch代码块之中,还可以再抛出错误,甚至使用嵌套的try...catch结构。

var n = 100;

try {
  throw n;
} catch (e) {
  if (e <= 50) {
    // ...
  } else {
    throw e;
  }
}
// Uncaught 100

上面代码中,catch代码之中又抛出了一个错误。

为了捕捉不同类型的错误,catch代码块之中可以加入判断语句。

try {
  foo.bar();
} catch (e) {
  if (e instanceof EvalError) {
    console.log(e.name + ": " + e.message);
  } else if (e instanceof RangeError) {
    console.log(e.name + ": " + e.message);
  }
  // ...
}

上面代码中,catch捕获错误之后,会判断错误类型(EvalError还是RangeError),进行不同的处理。

finally 代码块

try...catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句。

function cleansUp() {
  try {
    throw new Error('出错了……');
    console.log('此行不会执行');
  } finally {
    console.log('完成清理工作');
  }
}

cleansUp()
// 完成清理工作
// Error: 出错了……

上面代码中,由于没有catch语句块,所以错误没有捕获。执行finally代码块以后,程序就中断在错误抛出的地方。

function idle(x) {
  try {
    console.log(x);
    return 'result';
  } finally {
    console.log("FINALLY");
  }
}

idle('hello')
// hello
// FINALLY
// "result"

上面代码说明,try代码块没有发生错误,而且里面还包括return语句,但是finally代码块依然会执行。注意,只有在其执行完毕后,才会显示return语句的值。

下面的例子说明,return语句的执行是排在finally代码之前,只是等finally代码执行完毕后才返回。

var count = 0;
function countUp() {
  try {
    return count;
  } finally {
    count++;
  }
}

countUp()
// 0
count
// 1

上面代码说明,return语句的count的值,是在finally代码块运行之前就获取了。

下面是finally代码块用法的典型场景。

openFile();

try {
  writeFile(Data);
} catch(e) {
  handleError(e);
} finally {
  closeFile();
}

上面代码首先打开一个文件,然后在try代码块中写入文件,如果没有发生错误,则运行finally代码块关闭文件;一旦发生错误,则先使用catch代码块处理错误,再使用finally代码块关闭文件。

下面的例子充分反映了try...catch...finally这三者之间的执行顺序。

function f() {
  try {
    console.log(0);
    throw 'bug';
  } catch(e) {
    console.log(1);
    return true; // 这句原本会延迟到 finally 代码块结束再执行
    console.log(2); // 不会运行
  } finally {
    console.log(3);
    return false; // 这句会覆盖掉前面那句 return
    console.log(4); // 不会运行
  }

  console.log(5); // 不会运行
}

var result = f();
// 0
// 1
// 3

result
// false

上面代码中,catch代码块结束执行之前,会先执行finally代码块。

catch代码块之中,触发转入finally代码块的标志,不仅有return语句,还有throw语句。

function f() {
  try {
    throw '出错了!';
  } catch(e) {
    console.log('捕捉到内部错误');
    throw e; // 这句原本会等到finally结束再执行
  } finally {
    return false; // 直接返回
  }
}

try {
  f();
} catch(e) {
  // 此处不会执行
  console.log('caught outer "bogus"');
}

//  捕捉到内部错误

上面代码中,进入catch代码块之后,一遇到throw语句,就会去执行finally代码块,其中有return false语句,因此就直接返回了,不再会回去执行catch代码块剩下的部分了。

参考连接

COOKIE与SESSION知识

什么是Cookie

Cookie 是一种发送到客户浏览器的文本串句柄,并保存在客户机硬盘上,可以用来在某个WEB站点会话间持久的保持数据,用来跟踪浏览器用户身份的会话方式。

怎么使用Cookie?

通常我们有两种方式给浏览器设置或获取Cookie,分别是HTTP Response Headers中的Set-Cookie Header和HTTP Request Headers中的Cookie Header,以及通过JavaScript对document.cookie进行赋值或取值。

rfc6265第5.2节定义的Set-Cookie Header,除了必须包含Cookie正文,还可以选择性包含6个属性path、domain、max-age、expires、secure、httponly,它们之间用英文分号和空格("; ")连接。

Cookie的正文部分,是由&连接的key=value键值对字符串,类似于url中的查询字符串。下面是一个标准的Set-Cookie Header:

Set-Cookie: key=value; path=path; domain=domain; max-age=max-age-in-seconds; expires=date-in-GMTString-format; secure; httponly

在浏览器端,通过document.cookie也可以设置Cookie,以MDC文档为例,Cookie的内容除了必须包含正文之外,还可选5个属性:path、domain、max-age、expires、secure。下面是简单的示例:

document.cookie = "key=value; path=path; domain=domain; max-age=max-age-in-seconds; expires=date-in-GMTString-format; secure";

有两点需要说明:

  • max-age作为对expires的补充,现阶段有兼容性问题(IE低版本不支持),所以一般不单独使用;
  • JS中设置Cookie和HTTP方式相比较,少了对HttpOnly的控制,是因为JS不能读写HttpOnly Cookie;

Cookie 字段

rfc6265第5.3节定义了浏览器存放每个Cookie时应该包括这些字段:name、value、expiry-time、domain、path、creation-time、last-access-time、persistent-flag,、host-only-flag、secure-only-flag和http-only-flag。

  • name、value:由Cookie正文指定;
  • expiry-time:根据Cookie中的expires和max-age产生;
  • domain、path:分别由Cookie中的domain和path指定;
  • creation-time、last-access-time:由浏览器自行获得;
  • persistent-flag:持久化标记,在expiry-time未知的情况下为false,表示这是个session cookie;
  • secure-only-flag:在Cookie中包含secure属性时为true,表示这个cookie仅在https环境下才能使用;
  • http-only-flag:在Cookie中包含httponly属性时为true,表示这个cookie不允许通过JS来读写;
  • host-only-flag:在Cookie中不包含Domain属性,或者Domain属性为空,或者Domain属性不合法(不等于页面url中的Domain部分、也不是页面Domain的大域)时为true。此时,我们把这个Cookie称之为HostOnly Cookie;

什么是 Session

Session 指的就是访问者从到达某个特定主页到离开为止的那段时间。 Session 其实是利用 Cookie 进行信息处理的,当用户首先进行了请求后,服务端就在用户浏览器上创建了一个Cookie,当这个Session结束时,其实就是意味着这个Cookie就过期了。

ps: 为这个用户创建的Cookie的名称是aspsessionid。这个Cookie的唯一目的就是为每一个用户提供不同的身份认证

session

cookie 和session 的区别

  • 1.session 在服务器端,cookie 在客户端(浏览器)
  • 2.各大浏览器对 cookie 个数的限制通常是 20~50 个,大小限制通常在 4095~4097 字节之间,建议页面cookie操作时,应尽量保证cookie个数小于20个,总大小 小于4KB
  • 3.session 默认被存在在服务器的一个文件里(不是内存)
  • 4.session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie ,同时 session 也会失效(但是可以通过其它方式实现,比如在 url 中传递 session_id)
  • 5.session 可以放在 文件、数据库、或内存中都可以。
  • 6.用户验证这种场合一般会用 session

Q&A

1.服务端如何识别特定的客户?

每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。

2.设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?

登录信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。

参考资料:

你所不知道的HostOnly Cookie

COOKIE和SESSION有什么区别?

session与cookie的区别

Cookie个数限制及大小

深入理解 Session 与 Cookie

会话管理,cookie-parser 和 express-session

开篇 Serverless(无服务)基础知识

Serverless 架构即“无服务器”架构,它是一种全新的架构方式,是云计算时代一种革命性的架构模式。与云计算、容器和人工智能一样,Serverless 是这两年IT行业的一个热门词汇,它在各种技术文章和论坛上都有很高的曝光度。

目前行业可能更多处在容器 Docker+Kubernetes, 利用 IaaS、PaaS和SaaS 来快速搭建部署应用

什么是Serverless

Serverless 圈内俗称为“无服务器架构”,Serverless 不是具体的一个编程框架、类库或者工具。简单来说,Serverless 是一种软件系统架构**和方法,它的核心**是用户无须关注支撑应用服务运行的底层主机。这种架构的**和方法将对未来软件应用的设计、开发和运营产生深远的影响。

所谓“无服务器”,并不是说基于 Serverless 架构的软件应用不需要服务器就可以运行,其指的是用户无须关心软件应用运行涉及的底层服务器的状态、资源(比如 CPU、内存、磁盘及网络)及数量。软件应用正常运行所需要的计算资源由底层的云计算平台动态提供。

Serverless的技术实现

Serverless 的核心**是让作为计算资源的服务器不再成为用户所关注的一种资源。其目的是提高应用交付的效率,降低应用运营的工作量和成本。以 Serverless 的**作为基础实现的各种框架、工具及平台,是各种 Serverless 的实现(Implementation)。Serverless不是一个简单的工具或框架。用户不可能简单地通过实施某个产品或工具就能实现 Serverless 的落地。但是,要实现 Serverless 架构的落地,需要一些实实在在的工具和框架作为有力的技术支撑和基础。

随着 Serverless 的日益流行,这几年业界已经出现了多种平台和工具帮助用户进行 Serverless 架构的转型和落地。目前市场上比较流行的 Serverless 工具、框架和平台有:

  • AWS Lambda,最早被大众所认可的 Serverless 实现。
  • Azure Functions,来自微软公有云的 Serverless 实现。
  • OpenWhisk,Apache 社区的开源 Serverless 框架。
  • Kubeless,基于 Kubernetes 架构实现的开源 Serverless 框架。
  • Fission,Platform9 推出的开源 Serverless 框架。
  • OpenFaaS,以容器技术为核心的开源 Serverless 框架。
  • Fn,来自 Oracle 的开源 Serverless 框架,由原 Iron Functions 团队开发。

列举的 Serverless 实现有的是公有云的服务,有的则是框架工具,可以被部署在私有数据中心的私有云中(私有云 Serverless 框架 OpenWhisk、Fission 及 OpenFaaS)。每个 Serverless 服务或框架的实现都不尽相同,都有各自的特点。

FaaS与BaaS

IT是一个永远都不消停的行业,在这个行业里不断有各种各样新的名词和技术诞生,云计算(Cloud Computing)的出现是21世纪IT业界最重大的一次变革。云计算的发展从基础架构即服务(Infrastructure as a Service, IaaS),平台即服务(Platform as a Service,PaaS),软件即服务(Software as a Service,SaaS),慢慢开始演变到函数即服务(Function as a Service,FaaS)以及后台即服务(Backend as a Service,BaaS),Serverless 无服务化。

云计算演变

目前业界的各类 Serverless 实现按功能而言,主要为应用服务提供了两个方面的支持:函数即服务(Function as a Service,FaaS)以及后台即服务(Backend as a Service,BaaS)。

serverless结构

1.FaaS

FaaS 提供了一个计算平台,在这个平台上,应用以一个或多个函数的形式开发、运行和管理。FaaS 平台提供了函数式应用的运行环境,一般支持多种主流的编程语言,如 Java、PHP 及 Python 等。FaaS 可以根据实际的访问量进行应用的自动化动态加载和资源的自动化动态分配。大多数 FaaS 平台基于事件驱动(Event Driven)的**,可以根据预定义的事件触发指定的函数应用逻辑。

目前业界 FaaS 平台非常成功的一个代表就是AWS Lambda平台。AWS Lambda 是 AWS 公有云服务的函数式计算平台。通过 AWS Lambda,AWS 用户可以快速地在 AWS 公有云上构建基于函数的应用服务。

2.BaaS

为了实现应用后台服务的 Serverless 化,BaaS(后台即服务)也应该被纳入一个完整的 Serverless 实现的范畴内。通过 BaaS 平台将应用所依赖的第三方服务,如数据库、消息队列及存储等服务化并发布出来,用户通过向 BaaS 平台申请所需要的服务进行消费,而不需要关心这些服务的具体运维。

BaaS 涵盖的范围很广泛,包含任何应用所依赖的服务。一个比较典型的例子是数据库即服务(Database as a Service,DBaaS)。许多应用都有存储数据的需求,大部分应用会将数据存储在数据库中。传统情况下,数据库都是运行在数据中心里,由用户运维团队负责运维。在DBaaS的场景下,用户向 DBaaS 平台申请数据库资源,而不需要关心数据库的安装部署及运维。

Serverless的技术特点

为了实现解耦应用和服务器资源,实现服务器资源对用户透明,与传统架构相比,Serverless 架构在技术上有许多不同的特点。

  • 1.按需加载

在 Serverless 架构下,应用的加载(load)和卸载(unload)由 Serverless 云计算平台控制。这意味着应用不总是一直在线的。只有当有请求到达或者有事件发生时才会被部署和启动。当应用空闲至一定时长时,应用会到达或者有事件发生时才会被部署和启动。当应用空闲至一定时长时,应用会被自动停止和卸载。因此应用并不会持续在线,不会持续占用计算资源。

  • 2.事件驱动

Serverless 架构的应用并不总是一直在线,而是按需加载执行。应用的加载和执行由事件驱动,比如HTTP请求到达、消息队列接收到新的信息或存储服务的文件被修改了等。通过将不同事件来源(Event Source)的事件(Event)与特定的函数进行关联,实现对不同事件采取不同的反应动作,这样可以非常容易地实现事件驱动(Event Driven)架构。

  • 3.状态非本地持久化

云计算平台自动控制应用实例的加载和卸载,且应用和服务器完全解耦,应用不再与特定的服务器关联。因此应用的状态不能,也不会保存在其运行的服务器之上,不能做到传统意义上的状态本地持久化。

  • 4.非会话保持

应用不再与特定的服务器关联。每次处理请求的应用实例可能是相同服务器上的应用实例,也可能是新生成的服务器上的应用实例。因此,用户无法保证同一客户端的两次请求由同一个服务器上的同一个应用实例来处理。也就是说,无法做到传统意义上的会话保持(Sticky Session)。因此,Serverless架构更适合无状态的应用。

  • 5.自动弹性伸缩

Serverless 应用原生可以支持高可用,可以应对突发的高访问量。应用实例数量根据实际的访问量由云计算平台进行弹性的自动扩展或收缩,云计算平台动态地保证有足够的计算资源和足够数量的应用实例对请求进行处理。

  • 6.应用函数化

每一个调用完成一个业务动作,应用会被分解成多个细颗粒度的操作。由于状态无法本地持久化,这些细颗粒度的操作是无状态的,类似于传统编程里无状态的函数。Serverless 架构下的应用会被函数化,但不能说 Serverless 就是 Function as a Service(FaaS)。Serverless 涵盖了 FaaS 的一些特性,可以说 FaaS 是 Serverless 架构实现的一个重要手段。

Serverless的应用场景

通过将 Serverless 的理念与当前 Serverless 实现的技术特点相结合,Serverless 架构可以适用于各种业务场景。

  • 1.Web应用

Serverless 架构可以很好地支持各类静态和动态Web应用。如 RESTful API 的各类请求动作(GET、POST、PUT及DELETE等)可以很好地映射成 FaaS 的一个个函数,功能和函数之间能建立良好的对应关系。通过 FaaS 的自动弹性扩展功能,Serverless Web 应用可以很快速地构建出能承载高访问量的站点。

  • 2.移动互联网

Serverless 应用通过 BaaS 对接后端不同的服务而满足业务需求,提高应用开发的效率。前端通过FaaS提供的自动弹性扩展对接移动端的流量,开发者可以更轻松地应对突发的流量增长。在 FaaS 的架构下,应用以函数的形式存在。各个函数逻辑之间相对独立,应用更新变得更容易,使新功能的开发、测试和上线的时间更短。

  • 3.物联网(Internet of Things,IoT)

物联网(Internet of Things,IoT)应用需要对接各种不同的数量庞大的设备。不同的设备需要持续采集并传送数据至服务端。Serverless 架构可以帮助物联网应用对接不同的数据输入源。

  • 4.多媒体处理

视频和图片网站需要对用户上传的图片和视频信息进行加工和转换。但是这种多媒体转换的工作并不是无时无刻都在进行的,只有在一些特定事件发生时才需要被执行,比如用户上传或编辑图片和视频时。通过 Serverless 的事件驱动机制,用户可以在特定事件发生时触发处理逻辑,从而节省了空闲时段计算资源的开销,最终降低了运维的成本。

  • 5.数据及事件流处理

Serverless 可以用于对一些持续不断的事件流和数据流进行实时分析和处理,对事件和数据进行实时的过滤、转换和分析,进而触发下一步的处理。比如,对各类系统的日志或社交媒体信息进行实时分析,针对符合特定特征的关键信息进行记录和告警。

  • 6.系统集成

Serverless 应用的函数式架构非常适合用于实现系统集成。用户无须像过去一样为了某些简单的集成逻辑而开发和运维一个完整的应用,用户可以更专注于所需的集成逻辑,只编写和集成相关的代码逻辑,而不是一个完整的应用。函数应用的分散式的架构,使得集成逻辑的新增和变更更加灵活。

Serverless的局限

世界上没有能解决所有问题的万能解决方案和架构理念。Serverless 有它的特点和优势,但是同时也有它的局限。有的局限是由其架构特点决定的,有的是目前技术的成熟度决定的,毕竟 Serverless 还是一个起步时间不长的新兴技术领域,在许多方面还需要逐步完善。

  • 1.控制力

Serverless 的一个突出优点是用户无须关注底层的计算资源,但是这个优点的反面是用户对底层的计算资源没有控制力。对于一些希望掌控底层计算资源的应用场景,Serverless 架构并不是最合适的选择。

  • 2.可移植性

Serverless 应用的实现在很大程度上依赖于 Serverless 平台及该平台上的 FaaS 和 BaaS 服务。不同IT厂商的 Serverless 平台和解决方案的具体实现并不相同。而且,目前 Serverless 领域尚没有形成有关的行业标准,这意味着用户将一个平台上的 Serverless 应用移植到另一个平台时所需要付出的成本会比较高。较低的可移植性将造成厂商锁定(Vendor Lock-in)。这对希望发展 Serverless 技术,但是又不希望过度依赖特定供应商的企业而言是一个挑战。

  • 3.安全性

在 Serverless 架构下,用户不能直接控制应用实际所运行的主机。不同用户的应用,或者同一用户的不同应用在运行时可能共用底层的主机资源。对于一些安全性要求较高的应用,这将带来潜在的安全风险。

  • 4.性能

当一个 Serverless 应用长时间空闲时将会被从主机上卸载。当请求再次到达时,平台需要重新加载应用。应用的首次加载及重新加载的过程将产生一定的延时。对于一些对延时敏感的应用,需要通过预先加载或延长空闲超时时间等手段进行处理。

  • 5.执行时长

Serverless 的一个重要特点是应用按需加载执行,而不是长时间持续部署在主机上。目前,大部分 Serverless 平台对 FaaS 函数的执行时长存在限制。因此 Serverless 应用更适合一些执行时长较短的作业。

  • 6.技术成熟度

虽然 Serverless 技术的发展很快,但是毕竟它还是一门起步时间不长的新兴技术。因此,目前 Serverless 相关平台、工具和框架还处在一个不断变化和演进的阶段,开发和调试的用户体验还需要进一步提升。Serverless 相关的文档和资料相对比较少,深入了解 Serverless 架构的架构师、开发人员和运维人员也相对较少。

Other Resources

精读《Serverless 给前端带来了什么》
Docker — 从入门到实践
serverless-chrome
怎么理解 IaaS、SaaS 和 PaaS 的区别?

正则replace方法

replace 本身是JavaScript字符串对象的一个方法,它允许接收两个参数:

replace([RegExp|String],[String|Function])
  • 第1个参数可以是一个普通的字符串或是一个正则表达式

  • 第2个参数可以是一个普通的字符串或是一个回调函数

如果第1个参数是RegExp, JS会先提取RegExp匹配出的结果,然后用第2个参数逐一替换匹配出的结果

第2个参数是字符串,对于正则replace约定了一个特殊标记符$:

字符 替换文本
$1、$2、...、$99 与 regexp 中的第 1 到第 99 个子表达式相匹配的文本。
$& 与 regexp 相匹配的子串。
$` 位于匹配子串左侧的文本。
$' 位于匹配子串右侧的文本。
$$ 直接量符号。
'**人民'.replace(/(**)/g,'($1)')
//"(**)人民"
例外
'**人民**人民'.replace(/(**)/g,'($2)')  
//"($2)人民($2)人民"   由于没有组$2, ()代表一个组,此处只有一个组

'cdab'.replace(/(ab)/g,'$`')
//"cdcd"
'abcd'.replace(/(ab)/g,"$'")
//"cdcd"
'abcdabcd'.replace(/(ab)/g,"[$&]")
//"[ab]cd[ab]cd"
'$1$2wa,test'.replace(/[a-zA-z]/g,'$$');
//"$1$2$$,$$$$"

如果第2个参数是回调函数,每匹配到一个结果就回调一次,每次回调都会传递以下参数:

函数参数的规定:

  • 1.第一个参数为每次匹配的全文本($&)
  • 2.中间参数为子表达式匹配字符串,个数不限.( $i (i:1-99))
  • 3.倒数第二个参数为匹配文本字符串的匹配下标位置
  • 4.最后一个参数表示字符串本身

函数的匹配返回值,作为每次的匹配替换值。

'abcdedddabc12323abc'.replace(/[abc]/g,function(matched,index,originalText){
  return matched+'~'
})
//"a~b~c~deddda~b~c~12323a~b~c~"
'abcdedddabc12323abc'.replace(/[abc]/g,function(matched,index,originalText){
  return '~'
})
//"~~~deddd~~~12323~~~"
'abcdeabc'.replace(/[ab]/g,function(matched,index,originalText){
  return '['+index+']';
})
//"[0][1]cde[5][6]c"


var k = "abc123ac222".replace(/(\d+)/g, function(matched, $1, index, originalText){
	return Number(matched)+1
})
//abc124ac223

var k1 = "abc123ac222".replace(/(\d+)/g, function(matched, $1, index, originalText){
	return Number($1)+1
})
//abc124ac223

题目:要求对一个串的重复部分进行替换处理,比如:abcabcaabbbdd,该串中abc紧接着abc,a后紧接着a,无论重复多少次,我们都用#替换掉

//因为有一个捕获组,所以,需要再传递1参数 $1
'abcaabbccccdddabcabcef'.replace(/(\w+)\1+/g,function(matched,$1,index,originalText){
   return '#';
})
//"abc#####ef"

等同于
'abcaabbccccdddabcabcef'.replace(/(\w+)\1+/g, "#")

另外:

'abcaabbccccdddabcabcef'.replace(/(\w+)\1/g,function(matched,$1,index,originalText){
   console.log(matched);return '#';
})
//
"abc####d#ef"

如果只是保留非重复部分,也就是紧连接的aaa,只需要保留a即可。那么我们可以这么改:

'abcaabbccccdddabcabcef'.replace(/(\w+)\1+/g,function(matched,$1,index,originalText){
   return $1;
})
//"abcabccdabcef"

正则表达式
RegExp 代码中的代码
正则表达式30分钟入门教程

JSON-RPC 2.0 规范(中文版转载)

1.概述

JSON-RPC是一个无状态且轻量级的远程过程调用(RPC)协议。 本规范主要定义了一些数据结构及其相关的处理规则。它允许运行在基于socket,http等诸多不同消息传输环境的同一进程中。其使用JSONRFC 4627)作为数据格式。

它为简单而生!

2.约定

文档中关键字"MUST"、"MUST NOT"、"REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、"RECOMMENDED"、"MAY"和 "OPTIONAL" 将在RFC 2119 中得到详细的解释及描述。

由于JSON-RPC使用JSON,它具有与其相同的类型系统(见http://www.json.orgRFC 4627)。JSON可以表示四个基本类型(String、Numbers、Booleans和Null)和两个结构化类型(Objects和Arrays)。 规范中,术语“Primitive”标记那4种原始类型,“Structured”标记两种结构化类型。任何时候文档涉及JSON数据类型,第一个字母都必须大写:Object,Array,String,Number,Boolean,Null。包括True和False也要大写。

在客户端与任何被匹配到的服务端之间交换的所有成员名字应是区分大小写的。 函数、方法、过程都可以认为是可以互换的。

客户端被定义为请求对象的来源及响应对象的处理程序。

服务端被定义为响应对象的起源和请求对象的处理程序。

该规范的一种实现为可以轻而易举的填补这两个角色,即使是在同一时间,同一客户端或其他不相同的客户端。 该规范不涉及复杂层。

3.兼容性

JSON-RPC 2.0 的请求对象和响应对象可能无法在现用的JSON-RPC 1.0 客户端或服务端工作,然而我们可以很容易在两个版本间区分出2.0,总会包含一个成员命名为 “jsonrpc” 且值为“2.0”, 而1.0版本是不包含的。大部分的2.0实现应该考虑尝试处理1.0的对象,即使不是对等的也应给其相关提示。

4.请求对象

发送一个请求对象至服务端代表一个rpc调用, 一个请求对象包含下列成员:

jsonrpc

指定JSON-RPC协议版本的字符串,必须准确写为“2.0”

method

包含所要调用方法名称的字符串,以rpc开头的方法名,用英文句号(U+002E or ASCII 46)连接的为预留给rpc内部的方法名及扩展名,且不能在其他地方使用。

params

调用方法所需要的结构化参数值,该成员参数可以被省略。

id

已建立客户端的唯一标识id,值必须包含一个字符串、数值或NULL空值。如果不包含该成员则被认定为是一个通知。该值一般不为NULL[1],若为数值则不应该包含小数[2]

服务端必须回答相同的值如果包含在响应对象。 这个成员用来两个对象之间的关联上下文。

[1] 在请求对象中不建议使用NULL作为id值,因为该规范将使用空值认定为未知id的请求。另外,由于JSON-RPC 1.0 的通知使用了空值,这可能引起处理上的混淆。

[2] 使用小数是不确定性的,因为许多十进制小数不能精准的表达为二进制小数。

4.1通知

没有包含“id”成员的请求对象为通知, 作为通知的请求对象表明客户端对相应的响应对象并不感兴趣,本身也没有响应对象需要返回给客户端。服务端必须不回复一个通知,包含那些批量请求中的。

由于通知没有返回的响应对象,所以通知不确定是否被定义。同样,客户端不会意识到任何错误(例如参数缺省,内部错误)。

4.2参数结构

rpc调用如果存在参数则必须为基本类型或结构化类型的参数值,要么为索引数组,要么为关联数组对象。

  • 索引:参数必须为数组,并包含与服务端预期顺序一致的参数值。
  • 关联名称:参数必须为对象,并包含与服务端相匹配的参数成员名称。没有在预期中的成员名称可能会引起错误。名称必须完全匹配,包括方法的预期参数名以及大小写。

5.响应对象

当发起一个rpc调用时,除通知之外,服务端都必须回复响应。响应表示为一个JSON对象,使用以下成员:

jsonrpc

指定JSON-RPC协议版本的字符串,必须准确写为“2.0”

result

该成员在成功时必须包含。

当调用方法引起错误时必须不包含该成员。

服务端中的被调用方法决定了该成员的值。

error

该成员在失败是必须包含。

当没有引起错误的时必须不包含该成员。

该成员参数值必须为5.1中定义的对象。

id

该成员必须包含。

该成员值必须于请求对象中的id成员值一致。

若在检查请求对象id时错误(例如参数错误或无效请求),则该值必须为空值。

响应对象必须包含result或error成员,但两个成员必须不能同时包含。

5.1错误对象

当一个rpc调用遇到错误时,返回的响应对象必须包含错误成员参数,并且为带有下列成员参数的对象:

code

使用数值表示该异常的错误类型。 必须为整数。

message

对该错误的简单描述字符串。 该描述应尽量限定在简短的一句话。

data

包含关于错误附加信息的基本类型或结构化类型。该成员可忽略。 该成员值由服务端定义(例如详细的错误信息,嵌套的错误等)。

-32768至-32000为保留的预定义错误代码。在该范围内的错误代码不能被明确定义,保留下列以供将来使用。错误代码基本与XML-RPC建议的一样,url: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php

code message meaning
-32700 Parse error语法解析错误 服务端接收到无效的json。该错误发送于服务器尝试解析json文本
-32600 Invalid Request无效请求 发送的json不是一个有效的请求对象。
-32601 Method not found找不到方法 该方法不存在或无效
-32602 Invalid params无效的参数 无效的方法参数。
-32603 Internal error内部错误 JSON-RPC内部错误。
-32000 to -32099 Server error服务端错误 预留用于自定义的服务器错误。

除此之外剩余的错误类型代码可供应用程序作为自定义错误。

6.批量调用

当需要同时发送多个请求对象时,客户端可以发送一个包含所有请求对象的数组。

当批量调用的所有请求对象处理完成时,服务端则需要返回一个包含相对应的响应对象数组。每个响应对象都应对应每个请求对象,除非是通知的请求对象。服务端可以并发的,以任意顺序和任意宽度的并行性来处理这些批量调用。

这些相应的响应对象可以任意顺序的包含在返回的数组中,而客户端应该是基于各个响应对象中的id成员来匹配对应的请求对象。

若批量调用的rpc操作本身非一个有效json或一个至少包含一个值的数组,则服务端返回的将单单是一个响应对象而非数组。若批量调用没有需要返回的响应对象,则服务端不需要返回任何结果且必须不能返回一个空数组给客户端。

7.示例

Syntax:

--> data sent to Server
<-- data sent to Client

带索引数组参数的rpc调用:

--> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

--> {"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}
<-- {"jsonrpc": "2.0", "result": -19, "id": 2}

带关联数组参数的rpc调用:

--> {"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}
<-- {"jsonrpc": "2.0", "result": 19, "id": 3}

--> {"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}
<-- {"jsonrpc": "2.0", "result": 19, "id": 4}

通知:

--> {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}
--> {"jsonrpc": "2.0", "method": "foobar"}

不包含调用方法的rpc调用:

--> {"jsonrpc": "2.0", "method": "foobar", "id": "1"}
<-- {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}

包含无效json的rpc调用:

--> {"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]
<-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}

包含无效请求对象的rpc调用:

--> {"jsonrpc": "2.0", "method": 1, "params": "bar"}
<-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}

包含无效json的rpc批量调用:

--> [
        {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
        {"jsonrpc": "2.0", "method"
    ]
<-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}

包含空数组的rpc调用:

--> []
<-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}

非空且无效的rpc批量调用:

--> [1]
<-- [
    {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
    ]

无效的rpc批量调用:

--> [1,2,3]
<-- [
    {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
    {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
    {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
    ]

rpc批量调用:

--> [
    {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
    {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
    {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
    {"foo": "boo"},
    {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
    {"jsonrpc": "2.0", "method": "get_data", "id": "9"}
    ]
<-- [
    {"jsonrpc": "2.0", "result": 7, "id": "1"},
    {"jsonrpc": "2.0", "result": 19, "id": "2"},
    {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
    {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
    {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
    ]

所有都为通知的rpc批量调用:

--> [
    {"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]},
    {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}
]

<-- //Nothing is returned for all notification batches

7.扩展

以rpc开头的方法名预留作为系统扩展,且必须不能用于其他地方。每个系统扩展都应该有相关规范文档,所有系统扩展都应是可选的。


Copyright (C) 2007-2010 by the JSON-RPC Working Group

This document and translations of it may be used to implement JSON-RPC, it may be copied and furnished to others, and derivative works that comment on or otherwise explain it or assist in its implementation may be prepared, copied, published and distributed, in whole or in part, without restriction of any kind, provided that the above copyright notice and this paragraph are included on all such copies and derivative works. However, this document itself may not bemodified in any way.

代码复用之继承

代码复用之继承

js继承通常分两类:原型链的继承,实例对象继承(浅拷贝,深拷贝)

原型链的继承

  • 1.借用构造函数:
	function Parent(name){
		this.name= name || "adam",
		this.age="24";
	}
	Parent.prototype.say = function () {
		return this.name
	}	

	function Child(name){
		Parent.apply(this, arguments)
	}

	var child = new Child()

缺点:子类无法继承父类 prototype 共享属性,原型链断裂。

  • 2.借用和设置原型:
	function Parent(){
		this.name = "adam"
	}
	Parent.prototype.say = function () {
		return this.name
	}	

	function Child(){
		Parent.call(this)
	}

	Child.prototype = new Parent()

	Child.prototype.constructor = Child

	var child = new Child()

缺点:实现继承原型,同时继承了两个对象的属性,即添加到this的属性以及原型属性。在绝大多数的时候,并不需要这些自身的属性,因为它们很可能是指向一个特定的实例,而不是复用。

  • 3.共享原型:
	function Parent(){
		this.name = "adam"
	}
	Parent.prototype.say = function () {
		return this.name
	}	

	function Child(){
		Parent.call(this)
	}

	Child.prototype = Parent.prototype;

	var child = new Child()

优点: 效率比较高(不用执行和建立父类Parent的实例了),比较省内存。
缺点: 子类Child.prototype和父类Parent.prototype现在指向了同一个对象,那么任何对Child.prototype的修改,都会反映到Parent.prototype,影响作用域。

如这样操作Child.prototype.constructor = Child; 则 alert(Parent.prototype.constructor); // Child

  • 4.利用空对象作为中介:
function Parent(name){
	this.name=name || "gaolu",
	this.age="24";
}
Parent.prototype.sayName=function(){
	return this.name;
}

function Child(name){
   this.sex = “male”,
   Parent.apply(this, arguments)
}
function inhert(C,P){
	var F=function(){}; //F是空对象,所以几乎不占内存
	F.protototype = P.prototype;
	C.prototype = new F();
	C.prototype.constructor = C;
	C.uber = P;
}

inhert(Child, Parent)
var child = new Child()

此种方式最优,也被大多数框架库所采用,节省内存效率较高。

  • 5.利用 Object.create 实现继承

Object.create()

//通过Object.create()继承
function Parent(){
	this.name = "adam"
}

Parent.prototype.say = function () {
	return this.name
}

function Child (name){
	Parent.call(this)
}

Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
console.log(new Child())
  • 6.利用 ES6 中 extends 实现继承
    使用 extends 实现继承,更简单,不会破坏 instanceof 运算的危险。
class Queue{
	consturctor(contents = []) {
		this._queue = [...contents]
	}
	pop() {
		const value = this._queue[0];
		this._queue.splice(0, i);
		return value;
	}
}

//继承
class PeekableQueue extends Queue{
        constructor(...args) {
             super(...args);
       }
	peek() {
		return this._queue[0];
	}
}

实例对象继承(非构造函数的继承)

  • 浅拷贝

简单的把父对象的属性,全部拷贝给子对象,也能实现继承

	function extend(p){
		var c = {}
		for (var i in p){
			c[i] = p[i];
		}
		c.uber = p;

		return c;
	}

  • 深拷贝

如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。所谓"深拷贝",就是能够实现真正意义上的数组和对象的拷贝。它的实现只要递归调用"浅拷贝"就行了。

	function deepExtend(p, c){
		var c = c || {};
		
		for(var i in p){
			if(typeof p[i] === 'object'){
				c[i] = (p[i].constructor === Array) ? [] : {};
				//c[i] = (Object.prototype.toString.call(p[i]) === '[object Array]') ? [] : {};
				deepExtend(p[i], c[i])
			}else{
				c[i] = p[i]
			}
		}

		c.uber = p

		return c;
	}

引伸数组的常用方法拷贝

  • 1.slice
arrayObject.slice(start,end)

返回一个新的数组,不影响父数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。

parent = [1,2,3,4]
//slice() 与 slice(0),开始参数默认为0
child = parent.slice()
child.push(22)
console.log(parent, child) // [1, 2, 3, 4], [1, 2, 3, 4, 22]

  • 2.concat
arrayObject.concat(arrayX,arrayX,......,arrayX)

该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本

parent = [1,2,3,4]
child = parent.concat([])
child.push(22)
console.log(parent, child) // [1, 2, 3, 4], [1, 2, 3, 4, 22]
  • 3.for in 实现拷贝

ps: 三者效率,slice 与 concat 相当,而for in 最差

  • 4.扩展运算符(...)

ES6 中,扩展运算符也能很方便的拷贝数组

const items = [1,2,3,4] 
const itemCopy = [...items]

注意 splice 不能实现数组拷贝

arrayObject.splice(index,howmany,item1,.....,itemX)

splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目,该方法会改变原始数组

//使用splice()
var x = [14, 3, 77]
var y = x.splice(1, 2)
console.log(x)           // [14]
console.log(y)           // [3, 77]

//使用slice()
var x = [14, 3, 77];
var y = x.slice(1, 3);
console.log(x);          // [14, 3, 77]
console.log(y);          // [3,77]

由此,也发现 spliceslice 区别,splice会改变原生数组,而slice返回一个新的数组。

参考:

从__proto__和prototype来深入理解JS对象和原型链

Javascript 面向对象编程(一):封装

Javascript面向对象编程(二):构造函数的继承

Javascript面向对象编程(三):非构造函数的继承

Object.create()

HTTP请求中的Form Data与Request Payload的区别

HTTP请求中的Form Data与Request Payload的区别

前端开发中经常会用到AJAX发送异步请求,对于POST类型的请求会附带请求数据。而常用的两种传参方式为:Form Data 和 Request Payload。

121212

334343

GET请求

使用get请求时,参数会以key=value的形式拼接在请求的url后面。例如:

http://m.baidu.com/address/getlist.html?limit=50&offset=0&t=1502345139870

但是受限于请求URL的长度限制,一般参数较少时会使用get请求。

POST请求

当参数数量较多,且对数据有一定安全性要求时,会考虑用post请求传递参数数据。POST请求的参数数据是在请求体中。

方式一: Form Data形式

当POST请求的请求头里设置Content-Type: application/x-www-form-urlencoded(默认), 参数在请求体以标准的Form Data的形式提交,以&符号拼接,参数格式为key=value&key=value&key=value…

3333

121212

前端代码设置:

xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send('a=1&b=2&c=3');

在servlet中,后端可以通过request.getParameter(name)的形式来获取表单参数。

方式二:Request Payload形式

如果使用AJAX原生POST请求,请求头里设置Content-Type:application/json,请求的参数会显示在Request Payload中,参数格式为JSON格式:{“key”:”value”,”key”:”value”…},这种方式可读性会更好。

444

334343

后端可以使用getRequestPayload方法来获取。

Form Data 和 Request Payload 区别

  1. 如果请求头里设置Content-Type: application/x-www-form-urlencoded,那么这个请求被认为是表单请求,参数出现在Form Data里,格式为key=value&key=value&key=value…
  2. 原生的AJAX请求头里设置Content-Type:application/json,或者使用默认的请求头Content-Type:text/plain;参数会显示在Request payload块里提交。

防抖动(Debounce)和节流阀(Throttle)

防抖动(Debounce)和节流阀(Throttle)

函数节流和函数防抖,两者都是优化高频率执行js代码的一种手段。常用于优化DOM 上有些事件是会频繁触发,比如mouseover、scroll、resize...。把 js 代码的执行次数控制在合理的范围,既能节省浏览器CPU资源,又能让页面浏览更加顺畅,不会因为js的执行而发生卡顿,这就是函数节流和函数防抖要做的事。

函数节流是指一定时间内js方法只跑一次。比如人的眨眼睛,就是一定时间内眨一次。这是函数节流最形象的解释。
函数防抖是指频繁触发的情况下,只有足够的空闲时间,才执行代码一次。比如生活中的坐公交,就是一定时间内,如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。

函数节流

函数节流应用的实际场景,多数在监听页面元素滚动事件的时候会用到。因为滚动事件,是一个高频触发的事件。以下是监听页面元素滚动的示例代码:

var timer , canRun = true;
function throttle(delay){
   if(!canRun)return;

   canRun = false;
   clearTimeout(timer)
   timer = setTimeout(function(){
   		console.log(222)
   		canRun = true;
   }, delay || 200)
}

window.addEventListener("scroll", function(){
	console.log(1111)
	throttle(1000)
})

函数节流有两种形式:① 时间间隔内,起点执行,时间间隔中最后一次执行 ②时间间隔内,终点执行,时间间隔中最后一次执行

封装 throttle

/**
*
* @param fn {Function}   实际要执行的函数
* @param threshhold {Number}  执行间隔,单位是毫秒(ms)
* @param type {String}  是否第一次执行
* @return {Function}     返回一个“节流”函数
*/

function throttle(fn, threshhold, type) {

  // 记录是否可执行
  var isRun = true;

  // 定时器
  var timer;

  type = type || true;

  // 默认间隔为 200ms
  threshhold || (threshhold = 200)

  // 返回的函数,每过 threshhold 毫秒就执行一次 fn 函数
  return function () {

    // 保存函数调用时的上下文和参数,传递给 fn
    var context = this;
    var args = arguments;

    //第一次执行
    if(type && 'undefined' == typeof timer){
    	fn()  
    }

    if(!isRun)return;

    isRun = false;

    //保证间隔时间内执行
	timer = setTimeout(function () {
	   fn.apply(context, args)
	   isRun = true;
	}, threshhold)    

  }
}

//使用
document.addEventListener('mousemove', throttle(() => console.log(new Date().getTime()), 1000), false);

三、函数防抖

函数防抖的应用场景,最常见的就是用户注册时候的手机号码验证和邮箱验证了。只有等用户输入完毕后,前端才需要检查格式是否正确,如果不正确,再弹出提示语。例:

var timer
function debounce(fn, delay){
	clearTimeout(timer)
	timer= setTimeout(fn, delay || 200)
}

input.addEventListener("keyup", function(){
	debounce(function(){console.log(22)}, 2000)
})

封装 debounce

/**
 *
 * @param fn {Function}   实际要执行的函数
 * @param delay {Number}  延迟时间,单位是毫秒(ms)
 *
 * @return {Function}     返回一个“防反跳 debounce”了的函数
 */

function debounce(fn, delay) {

  // 定时器,用来 setTimeout
  var timer

  // 返回一个函数,这个函数会在一个时间区间结束后的 delay 毫秒时执行 fn 函数
  return function () {

    // 保存函数调用时的上下文和参数,传递给 fn
    var context = this
    var args = arguments

    // 每次这个返回的函数被调用,就清除定时器,以保证不执行 fn
    clearTimeout(timer)

    // 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作),
    // 再过 delay 毫秒就执行 fn
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay || 0)
  }
}

//使用
document.addEventListener('mousemove', debounce(() => console.log(new Date().getTime()), 1000), false);

ps: 此处精妙的地方就是,debounce()返回一个函数作为事件监听回调方法,实现 timer 共享,而不需全局定义 timer,减少污染全局变量

参考资料:
Javascript 的 Debounce 和 Throttle 的原理及实现
JavaScript函数节流和函数防抖之间的区别

字符编码 ASCII、URI 、UNICODE、Base64

ASCII

ASCII(American Standard Code for Information Interchange:美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言,由美国国家标准学会 ANSI(American National Standard Institude)于1968年正式制定。它是现今最通用的信息交换标准,并等同于国际标准ISO/IEC 646。

ASCII 码使用指定的7 位或8 位二进制数组合来表示128 或256 种可能的字符。标准ASCII 码也叫基础ASCII码,使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号, 以及在美式英语中使用的特殊控制字符

  • 0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符)

  • 32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字。

  • 65~90为26个大写英文字母,97~122号为26个小写英文字母,其余为一些标点符号、运算符号等。

在标准ASCII中,其最高位(b7)用作奇偶校验位。所谓奇偶校验,是指在代码传送过程中用来检验是否出现错误的一种方法,一般分奇校验和偶校验两种。奇校验规定:正确的代码一个字节中1的个数必须是奇数,若非奇数,则在最高位b7添1;偶校验规定:正确的代码一个字节中1的个数必须是偶数,若非偶数,则在最高位b7添1。

后128个称为扩展ASCII码。许多基于x86的系统都支持使用扩展(或“高”)ASCII。扩展ASCII 码允许将每个字符的第8 位用于确定附加的128 个特殊符号字符、外来语字母和图形符号。

ASCII编码查询表知识

URI

统一资源标识符(英语:Uniform Resource Identifier,缩写:URI)是一个用于标识某一互联网资源名称的字符串。URI 是一个通用的概率,由两个主要的子集 URL (统一资源定位符,又称 百分号编码 ) 和 URN (统一资源名) 构成,URL 是通过描述资源的位置来标识资源的,URN 则是通过名字来识别资源,与它们当前所处的位置无关。

  • URI:RFC1630,发布于 1994 年 6 月,被称为“Universal Resource Identifiers in WWW: A Unifying Syntax for the Expression of Names and Addresses of Objects on the Network as used in the World-Wide Web”。它是一个Informational RFC —— 也就是说,它没有获得社区的任何认可。
  • URL:RFC1738,发布于 1994 年 12 月, 被称为“Uniform Resource Locators”。它是一个 Proposed Standard —— 也就是说,它是一个共识过程的结果,虽然它还没有经过测试,并成熟到足以成为一个完整的 Internet Standard。
  • URN:RFC1737,发布于 1994 年 12 月,被称为“Functional Requirements for Uniform Resource Names”。

URI编码

URI的字符类型

URI所允许的字符分作保留未保留保留字符是那些具有特殊含义的字符,例如:斜线字符用于URL(或URI)不同部分的分界符;未保留字符没有这些特殊含义。百分号编码把保留字符表示为特殊字符序列。上述情形随URI与URI的不同版本规格会有轻微的变化。

RFC 3986 section 2.2 保留字符 (2005年1月)

! * ' ( ) ; : @ & = + $ , / ? # [ ]

RFC 3986 section 2.3 未保留字符 (2005年1月)

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
0 1 2 3 4 5 6 7 8 9 - _ . ~

URI中的其它字符必须用百分号编码。

保留字符的百分号编码

如果一个保留字符在特定上下文中具有特殊含义(称作"reserved purpose") , 且URI中必须使用该字符用于其它目的, 那么该字符必须百分号编码。百分号编码一个保留字符,首先需要把该字符的ASCII的值表示为两个16进制的数字,然后在其前面放置转义字符("%"),置入URI中的相应位置。(对于非ASCII字符, 需要转换为UTF-8字节序, 然后每个字节按照上述方式表示.)

例如,"/", 如果用作URI的路径成分的分界符, 则是具有特殊含义的保留字符. 如果该字符需要出现在URI一个路径成分的内部, 则三字符序列"%2F"或"%2f"就用于代替原本的"/"出现在该URI路径成分的内部.

! # $ & ' ( ) * + , / : ; = ? @ [ ]
%21 %23 %24 %26 %27 %28 %29 %2A %2B %2C %2F %3A %3B %3D %3F %40 %5B %5D

在特定上下文中没有特殊含义的保留字符也可以被百分号编码,在语义上与不百分号编码的该字符没有差别.

在URI的"查询"成分(?字符后的部分)中, 例如"/"仍然是保留字符但是没有特殊含义,除非一个特定的URI有其它规定. 该/字符在没有特殊含义时不需要百分号编码.

如果保留字符具有特殊含义,那么该保留字符用百分号编码的URI与该保留字符仅用其自身表示的URI具有不同的语义。

受限字符或不安全字符

受限字符或不安全字符,直接放在Url中的时候,可能会引起解析程序的歧义,也需要百分号编码。

受限字符 为何受限 例子
% 作为编码字符的转义标志,因此本身需要编码 encodeURI('%') // "%25"
空格 Url在传输的过程,或者用户在排版的过程,或者文本处理程序在处理Url的过程,都有可能引入无关紧要的空格,或者将那些有意义的空格给去掉。 encodeURI(' ') // "%20"
<>" 尖括号和引号通常用于在普通文本中起到分隔Url的作用,所以应该对其进行编码 encodeURI('<>"') // "%3C%3E%22"
{} \^~[]' 某一些网关或者传输代理会篡改这些字符。你可能会感到奇怪,为什么使用一些不安全字符的时候并没有发生什么不好的事情,比如无需对~字符进行编码,前面也说了,对某些传输协议来说不是问题。
0x00-0x1F, 0x7F 受限,这些十六进制范围内的字符都在US-ASCII字符集的不可打印区间内 比如换行键是0x0A
>0x7F 受限,十六进制值在此范围内的字符都不在US-ASCII字符集的7比特范围内 encodeURI('京东') // "%E4%BA%AC%E4%B8%9C"

javascript 转义字符

Javascript中提供六个方法来处理特殊保留字符、受限字符、不安全字符,如下

escape(已废弃) 针对 ASCII字母、数字、标点符号"@ * _ + - . /"以外,其他所有字符进行编码 unicode 字符
unescape(已废弃)

encodeURI 对整个URL进行编码,除了常见的符号以外,对其他一些在网址中有特殊含义的符号"; / ? : @ & = + $ , #",也不进行编码。编码后,它输出符号的utf-8形式,并且在每个字节前加上%。
decodeURI

encodeURIComponent 与encodeURI()的区别是,它用于对URL的组成部分进行个别编码,而不用于对整个URL进行编码。
因此,"; / ? : @ & = + $ , #",这些在encodeURI()中不被编码的符号,在encodeURIComponent()中统统会被编码
decodeURIComponent

Unicode

Unicode(统一码、万国码、单一码,简称UCS)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。1990年开始研发,1994年正式公布。

Unicode 字符集(简称为ucs),国际标准组织于1984年4月成立ISO/IEC JTCI/SC2/WG2工作组,针对各国文字、符号进行统一性编码。1991年美国跨国公司成立 Unicode Consortium ,并于1991年10月与WG2达成协议,采用统一编码字集。

大概来说,Unicode编码系统可分为编码方式和实现方式两个层次。

Unicode 编码规则

统一码的编码方式与ISO 10646通用字符集概念相对应。当前实际应用的统一码版本对应于UCS-2,使用16的编码空间。也就是每个字符占用2个字节。这样理论上一共最多可以表示2^16(即65536)个字符。

采用16位编码体系基本满足各种语言的使用,内容包含符号6811个,汉字20902个,韩文拼音11172个,造字区6400个,保留20249个,共计65534个。Unicode 编码后的大小是一样的,例如一个英文字母 “a” 和 一个汉字 “好”,编码后占用的空间大小是一样的都是两个字节。

随着中文,日文和韩文引入,原有的 Unicode 定义的字符集无法满足。Unicode 定义的字符集已经超过16位所能表达的范围,把所有这些 CodePoint 分成17个平面 (Code Plane): U+0000 ~ U+FFFF 划入基本多语言平面(Basic MultilingualPlane, 简记为BMP),其余划入16个辅助平面(Supplementary Plane), 代码点范围U+10000(2^16) ~ U+10FFFF(2^20+2^16).

img

平面 始末字符值 中文名称 英文名称
0号平面 U+0000 - U+FFFF 基本多文种平面 Basic Multilingual Plane,简称BMP
1号平面 U+10000 - U+1FFFF 多文种补充平面 Supplementary Multilingual Plane,简称SMP
2号平面 U+20000 - U+2FFFF 表意文字补充平面 Supplementary Ideographic Plane,简称SIP
3号平面 U+30000 - U+3FFFF 表意文字第三平面(未正式使用[1] Tertiary Ideographic Plane,简称TIP
4号平面 至 13号平面 U+40000 - U+DFFFF (尚未使用)
14号平面 U+E0000 - U+EFFFF 特别用途补充平面 Supplementary Special-purpose Plane,简称SSP
15号平面 U+F0000 - U+FFFFF 保留作为私人使用区(A区)[2] Private Use Area-A,简称PUA-A
16号平面 U+100000 - U+10FFFF 保留作为私人使用区(B区)[2] Private Use Area-B,简称PUA-B

在Unicode中,私人使用区 (Private Use Areas) 指其解释未在Unicode标准中指定,而是由合作用户之间的私人协议决定其用途的一系列码位。 当前定义了三个私人使用区:一个在基本多语言平面(U+E000-U+F8FF)中,另外两个几乎包含了整个第15和第16平面(分别为U+F0000-U+FFFFD,U+100000-U+10FFFD)。

基本多语言平面的字符的编码为U+hhhh,其中每个h代表一个十六进制数字,与UCS-2编码完全相同。而其对应的4字节UCS-4编码后两个字节一致,前两个字节则所有位均为0。

关于统一码和ISO 10646及UCS的详细关系,见通用字符集

img

\u则代表unicode编码

Unicode 编码实现方式

Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的。但是在实际存储传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF),Unicode的实现方式有UTF-7、UTF-8、UTF-16、UTF-32、Punycode、CESU-8、SCSU、UTF-32、GB18030等, 其中 UTF-8、UTF-16、UTF-32 使用比较广泛。

UTF-8 编码

UTF-8 是使用互联网上使用最广泛的 unicode 编码方式,目前已经占有整个互联网 92% 的份额。

UTF-8 是一种变长的编码方法,字符长度从1个字节到4个字节不等。越是常用的字符,字节越短,最前面的128个字符,只使用1个字节表示,与ASCII码完全相同(Unicode 中的前 128 个字符和 ASCII 码都是一一对应的)。

编号范围 字节
0x0000 - 0x007F 1
0x0080 - 0x07FF 2
0x0800 - 0xFFFF 3
0x010000 - 0x10FFFF 4

0x 开头代表十六进制

1个字节是8位,二进制8位:xxxxxxxx 范围从00000000-11111111,表示0到255。一位16进制数(用二进制表示是xxxx)最多只表示到15(即对应16进制的F 1111),要表示到255,就还需要第二位。所以1个字节=2个16进制字符,一个16进制位=0.5个字节。

UTF-16 编码

UTF-16 编码介于 UTF-32 与 UTF-8 之间,同时结合了定长和变长两种编码方法的特点。

它的编码规则很简单:基本平面的字符占用2个字节,辅助平面的字符占用4个字节。也就是说,UTF-16的编码长度要么是2个字节(U+0000到U+FFFF),要么是4个字节(U+010000到U+10FFFF)。

UTF-32 编码

UTF-32 对 Unicode 中的每个字符都用 4 个字节来表示。UTF-32 的优点在于,转换规则简单直观,查找效率高。缺点在于浪费空间,同样内容的英语文本,它会比ASCII编码大四倍。这个缺点很致命,导致实际上没有人使用这种编码方法,HTML 5标准就明文规定,网页不得编码成UTF-32。

截自网友 Unicode 的思维导图:
Unicode 思维导图

javascript Unicode 字符转义

Javascript中提供了相关方法来处理 Unicode 转义,如下:

charAt() 方法可返回指定位置的字符。

charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。这个返回值是 0 - 65535 之间的整数。

fromCharCode() 可接受一个指定的 Unicode 值,然后返回一个字符串。String 的静态方法,字符串中的每个字符都由单独的数字 Unicode 编码指定.

示例

str = "中文";
// 获取字符
char0 = str.charAt(0); // "中"

// 对应字符 Unicode 编码值,根据 Unicode 表寻找对应字符
code = str.charCodeAt(0); // 20013

// Unicode 编码转换为字符串
str0 = String.fromCharCode(code); // "中"

// 转为16进制数组
code16 = code.toString(16); // "4e2d"

// 变成字面量表示法
ustr = "\\u"+code16; // "\u4e2d"

'\u4e2d' === '中' // true

// 包装为JSON
jsonstr = '{"ustr": "'+ ustr +'"}'; //'{"ustr": "\u4e2d"}'

// 使用JSON工具转换
obj = JSON.parse(jsonstr); // Object {ustr: "中"}
//
ustr_n = obj.ustr; // "中"

小知识:计算机喜欢用16进制
字节(byte)在计算机内部出现的频率较高,使用一种简洁的方式将内在含义准确表达出来,会带来很多方便。选择十六进制,因为8位二进制的数字可以方便的转换为2个十六进制的数字。一个字节能且只能由一对十六进制来表示,比如10110110可以表示为B6。

Base64

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于{\displaystyle 2^{6}=64},所以每6个比特为一个单元,对应某个可打印字符。3个字节有24个比特,对应于4个Base64单元,即3个字节可由4个可打印字符来表示。它可用来作为电子邮件的传输编码。在Base64中的可打印字符包括字母A-Za-z数字0-9,这样共有62个字符,此外两个可打印符号在不同的系统中而不同。一些如uuencode的其他编码方法,和之后BinHex的版本使用不同的64字符集来代表6个二进制数字,但是不被称为Base64。

Base64常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据,包括MIME电子邮件XML的一些复杂数据。

Base64编码转换方式

Base64,选出64个字符----小写字母a-z、大写字母A-Z、数字0-9、符号"+"、"/"(再加上作为垫字的"=",实际上是65个字符)----作为一个基本字符集。然后,其他所有符号都转换成这个字符集中的字符。具体来说,转换方式可以分为四步。

  • 第一步,将每三个字节作为一组,一共是24个二进制位。

  • 第二步,将这24个二进制位分为四组,每个组有6个二进制位。

  • 第三步,在每组前面加两个00,扩展成32个二进制位,即四个字节。

  • 第四步,根据 Base64 索引表,得到扩展后的每个字节的对应符号,这就是Base64的编码值。

Base64索引表:

数值 字符 数值 字符 数值 字符
0 A 16 Q 32 g 48 w
1 B 17 R 33 h 49 x
2 C 18 S 34 i 50 y
3 D 19 T 35 j 51 z
4 E 20 U 36 k 52 0
5 F 21 V 37 l 53 1
6 G 22 W 38 m 54 2
7 H 23 X 39 n 55 3
8 I 24 Y 40 o 56 4
9 J 25 Z 41 p 57 5
10 K 26 a 42 q 58 6
11 L 27 b 43 r 59 7
12 M 28 c 44 s 60 8
13 N 29 d 45 t 61 9
14 O 30 e 46 u 62 +
15 P 31 f 47 v 63 /

如果要编码的字节数不能被3整除,最后会多出1个或2个字节,那么可以使用下面的方法进行处理:先使用0字节值在末尾补足,使其能够被3整除,然后再进行Base64的编码。在编码后的Base64文本后加上一个或两个=号,代表补足的字节数。也就是说,当最后剩余两个八位字节(2个byte)时,最后一个6位的Base64字节块有四位是0值,最后附加上两个等号;如果最后剩余一个八位字节(1个byte)时,最后一个6位的base字节块有两位是0值,最后附加一个等号。 参考下表:

文本(1 Byte) A
二进制位 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
二进制位(补0) 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Base64编码 Q Q = =
文本(2 Byte) B C
二进制位 0 1 0 0 0 0 1 0 0 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0
二进制位(补0) 0 1 0 0 0 0 1 0 0 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0
Base64编码 Q k M =

注意:Base64将三个字节转化成四个字节,因此Base64编码后的文本,会比原文本大出三分之一左右。

Base64编码转换示例

  • 编码“Man”
文本 M a n
ASCII编码 77 97 110
二进制位 0 1 0 0 1 1 0 1 0 1 1 0 0 0 0 1 0 1 1 0 1 1 1 0
索引 19 22 5 46
Base64编码 T W F u

在此例中,Base64算法将3个字节编码为4个字符。

  • 编码汉字"严"

汉字本身可以有多种编码,比如gb2312、utf-8、gbk等等,每一种编码的Base64对应值都不一样。下面的例子以utf-8为例。

文本
utf-8 编码 E4B8A5
二进制位(24位) 1 1 1 0 0 1 0 0 1 0 1 1 1 0 0 0 1 0 1 0 0 1 0 1
二进制位(32位) 0 0 1 1 1 0 0 1 0 0 0 0 1 0 1 1 0 0 1 0 0 0 1 0 0 0 1 0 0 1 0 1
索引 57 11 34 37
Base64编码 5 L i l

扩展进制转换(-)

// 十进制转其他进制
const  x = 110;
x.toString(2)//转为2进制
x.toString(8)//转为8进制
x.toString(16)//转为16进制

// 其他进制转十进制

const x = "110" // 二进制的字符串表示
parseInt(x, 2) // 二进制, 转为十进制

const x = "70" // 八进制的字符串表示
parseInt(x, 8) // 八进制, 转为十进制

const x = "ff" // 十六进制的字符串表示
parseInt(x, 16) // 十六进制, 转为十进制

扩展进制转换(二)

形如——

&name;   // html 转义字符,类似前面学到 uri 使用 % 转义字符
&#dddd;  // 十进制数字
&#xhhhh; // 十六进制数字

——的一串字符是 HTML、XML 等 SGML 类语言的转义序列(escape sequence)。它们不是「编码」。

以 HTML 为例,这三种转义序列都称作 character reference:

1.第一种是 character entity reference,后接预先定义的 entity 名称,而 entity 声明了自身指代的字符。

2.后两种是 numeric character reference(NCR),数字取值为目标字符的 Unicode code point;以「&#」开头的后接十进制数字,以「&#x」开头的后接十六进制数字。

从 HTML 4 开始,NCR 以 Unicode 为准,与文档编码无关。

「**」二字分别是 Unicode 字符 U+4E2D 和 U+56FD,十六进制表示的 code point 数值「4E2D」和「56FD」就是十进制的「20013」和「22269」。所以——

&#x4e2d;&#x56fd;
&#20013;&#22269;

参考阅读:
Unicode®字符百科
ASCII
Unicode
Unicode字符列表
UTF-8
百分号编码
字符,字节和编码
字符编码笔记:ASCII,Unicode 和 UTF-8
阮一峰 - Unicode与JavaScript详解
阮一峰 - 关于URL编码
阮一峰 - Base64笔记
探究 dataURI 中使用 SVG 正确姿势
escape,encodeURI,encodeURIComponent有什么区别?
URL编码的奥秘
你真的了解 Unicode 和 UTF-8 吗?
彻底弄懂 Unicode 编码
二进制
计算机的血肉:数据
Javascript 与字符编码
Unicode® 6.0.0

MongoDB 提升性能的18原则 — 转载

MongoDB 是高性能数据,使用的过程中,偶尔还会碰到一些性能问题。MongoDB和其它关系型数据库相比,例如 SQL Server 、MySQL 、Oracle 相比来说,相对较新,很多人对其不是很熟悉,所以很多开发、DBA往往是注重功能的实现,而忽视了性能的要求。其实,MongoDB和 SQL Server 、MySQL 、Oracle 一样,一个数据库对象的设计调整、索引的创建、语句的优化,都会对性能产生巨大的影响。

为了充分挖掘MongoDB性能,现简单总计了以下18条,欢迎大家一起来持续总结完善。

  • 文档中的_id键推荐使用默认值,禁止向_id中保存自定义的值。

MongoDB文档中都会有一个_id键,默认是个ObjectID对象(标识符中包含时间戳、机器ID、进程ID和计数器)。MongoDB在指定_id与不指定_id插入时 速度相差很大,指定_id会减慢插入的速率。

  • 推荐使用短字段名。

与关系型数据库不同,MongoDB 集合中的每一个文档都需要存储字段名,长字段名会需要更多的存储空间。

  • MongoDB 索引可以提高文档的查询、更新、删除、排序操作,所以结合业务需求,适当创建索引。

  • 每个索引都会占用一些空间,并且导致插入操作的资源消耗,因此,建议每个集合的索引数尽量控制在5个以内。

  • 对于包含多个键的查询,创建包含这些键的复合索引是个不错的解决方案。复合索引的键值顺序很重要,理解索引最左前缀原则。

例如在test集合上创建组合索引{a:1,b:1,c:1}。执行以下7个查询语句:

db.test.find({a:”hello”})
db.test.find({b:”sogo”, a:”hello”})
db.test.find({a:”hello”,b:”sogo”, c:”666”})
db.test.find({c:”666”, a:”hello”})
db.test.find({b:”sogo”, c:”666”})
db.test.find({b:”sogo” })
db.test.find({c:”666”})
  • 以上查询语句可能走索引的是1、2、3、4
  • 查询应包含最左索引字段,以索引创建顺序为准,与查询字段顺序无关。
  • 最少索引覆盖最多查询。
  • TTL 索引(time-to-live index,具有生命周期的索引),使用TTL索引可以将超时时间的文档老化,一个文档到达老化的程度之后就会被删除。

创建TTL的索引必须是日期类型。TTL索引是一种单字段索引,不能是复合索引。TTL删除文档后台线程每60s移除失效文档。不支持定长集合。

  • 需要在集合中某字段创建索引,但集合中大量的文档不包含此键值时,建议创建稀疏索引。

索引默认是密集型的,这意味着,即使文档的索引字段缺失,在索引中也存在着一个对应关系。在稀疏索引中,只有包含了索引键值的文档才会出现。

  • 创建文本索引时字段指定 text,而不是1或者-1。每个集合只有一个文本索引,但是它可以为任意多个字段建立索引。

文本搜索速度快很多,推荐使用文本索引替代对集合文档的多字段的低效查询。

  • 使用 findOne 在数据库中查询匹配多个项目,它就会在自然排序文件集合中返回第一个项目。如果需要返回多个文档,则使用 find方法。

  • 如果查询无需返回整个文档或只是用来判断键值是否存在,可以通过投影(映射)来限制返回字段,减少网络流量和客户端的内存使用。

既可以通过设置{key:1}来显式指定返回的字段,也可以设置{key:0}指定需要排除的字段。

  • 除了前缀样式查询,正则表达式查询不能使用索引,执行的时间比大多数选择器更长,应节制性地使用它们。

  • 在聚合运算中,$match 要在 $ group前面,通过 $match 前置,可以减少$ group 操作符要处理的文档数量。

  • 通过操作符对文档进行修改,通常可以获得更好的性能,因为,不需要往返服务器来获取并修改文档数据,可以在序列化和传输数据上花费更少的时间。

  • 批量插入(batchInsert)可以减少数据向服务器的提交次数,提高性能。但是批量提交的 BSON Size 不超过48MB。

  • 禁止一次取出太多的数据进行排序,MongoDB 目前支持对 32M 以内的结果集进行排序。如果需要排序,请尽量限制结果集中的数据量。

  • 查询中的某些$操作符可能会导致性能低下,如$ne$not$exists$nin$or 尽量在业务中不要使用。

a) $exist: 因为松散的文档结构导致查询必须遍历每一个文档;

b) $ne: 如果当取反的值为大多数,则会扫描整个索引;

c) $not: 可能会导致查询优化器不知道应当使用哪个索引,所以会经常退化为全表扫描;

d) $nin: 全表扫描;

e) $or: 有多个条件就会查询多少次,最后合并结果集,应该考虑装换为 $in

  • 固定集合可以用于记录日志,其插入数据更快,可以实现在插入数据时,淘汰最早的数据。需求分析和设计时,可考虑此特性,即提高了性能,有省去了删除动作。

固定集合需要显式创建,指定Size的大小,还能够指定文档的数量。集合不管先达到哪一个限制,之后插入的新文档都会把最老的文档移出。

  • 集合中文档的数据量会影响查询性能,为保持适量,需要定期归档。

MongoDB 单条文档大小的限制 ---- Document 文档是构成 MongoDB 数据存储的最小单元, Document 表现形式犹如JSON一般,采用 K-V 对形式展开,类型要比 JSON 丰富。鉴于 JSON 只有6种数据类型 (字符串(string)、数值(number)、布尔(true、false)、 null、对象(object)、数组(array)),MongoDB在数据类型上并未采用简单的JSON进行数据的存储,而是使用了BSON (Binary Javascript Object Notation)。BSON 为了避免无端的大数据写入(类似二进制的图片、音频等),把内存全部吃满,而特意设置了单条文档的上限,因此一条文档上限若超过 16MB,则直接报错。

Other source

MongoDB优化查询性能

MongoDB 管理

MongoDB更需要好的模式设计 及 案例赏析

Mongodb 对内嵌数组的增删改查操作

先定义一份初始化,设置一个User类,其初始数据如下:

{ 
  arr: [ 1, 2 ],
  _id: 5ac5ee12a79131259413c40f,
  name: 'scy',
  __v: 0 
}

随后以初始数据为基,进行操作。

1、向内嵌数组添加数据

使用操作符 $push,向数组末尾添加数据 ,可重复

//第一个参数是匹配条件 第二个参数是具体操作向user里面的arr末尾追加元素3
User.update({name:"scy"},{$push:{"arr":3}});

执行结果

{ 
  arr: [ 1, 2, 3 ],
  _id: 5ac5f0d3db343b1888a8969d, 
  name: 'scy',
  __v: 0 
}

一次添加多个数据:

//两种方式都可以
User.update({name:"scy"},{$push:{"arr": [2,3]}});
User.update({name:"scy"},{$push:{"arr":{$each:[2,3]}}});

2、删除内嵌数组指定数据

使用操作符 $pull

//删除arr所有数据为2的元素*
User.update({name:"scy"},{$pull:{"arr":2}});

执行结果

{ arr: [ 1 ], _id: 5ac5f39fdad94e23e8de9aee, name: 'scy', __v: 0 }

如果数组元素是对象,可以根据对象属性操作:

//User数据结构
{
  name:"scy",
  mArray:[{age:13,weight:50},{age:13,weight:30}]
}
//删除所有weight属性值为30的对象
User.update({name:"scy"},{$pull:{"mArray":{"weight":30}}});

3、修改内嵌数组指定数据

//将数组里面的第一个元素1修改为2
User.update({"arr":{$all:[1]}},{$set:{"arr.$":2}});

//根据下标 将arr下标为1的元素修改为22
User.update({$set:{"arr.1":22}});

//将数组里面的元素1批量修改为2
User.updateMany({"arr":{$all:[1]}},{$set:{"arr.$":2}});

如果数组的元素是对象,如下:

{
  name:"scy",
  mArray:[{age:13,weight:50},{age:13,weight:30}]
}

修改操作如下:

//将第一个age为13的值修改为22
User.update({"mArray.age":13},{$set:{"mArray.$.age":22}});

//还可以这样 mArray.1.age 其中1是下标 
User.update({$set:{"mArray.1.age":22}});//将arr第二个元素对象的age改为22

//将第age为13的值修改为22
User.updateMany({"mArray.age":13},{$set:{"mArray.$.age":22}});

4、查询内嵌数组并返回指定的数据

使用 $size 返回指定数组长度的数据:

//$size限制比较大 下面表示查询数组长度为2的数据
User.find({arr:{$size:2}})

$slice 操作符比较强大:

//匹配到的user 将其数组截取第一个返回 如[1,1,2]返回[1]
User.findOne({name:"scy"},{arr:{$slice:1}});
//将匹配到的user的数组 截取返回后面两个元素  如[1,1,2]返回[1,2]
User.findOne({name:"scy"},{arr:{$slice:-2}});
//从数组的下表为1的元素开始 返回两个 如[1,3,2]返回[3,2]
User.findOne({name:"scy"},{arr:{$slice:[1,2]}});

5、mongodb 按照字段模糊查询方法

直接查询:

//值查询
db.student.find({name:{$regex:'jack', $options:'i'}})
db.student.find({name:{$regex:/jack.*/i}})
db.student.find({name:/jack/i})

//数组元素查询, 与值查询类似
db.school.find({students:/jack/i})

// 对数组对象查询
contacts:{
    [
        {
            address: "address1",
            name: "张三"
        },
        {
            address: "address2",
            name: "李四"
        },
        .....
    ]
}
db.collection.find({'contacts.name':{$regex:'张'}})

对比:

MySQL MongoDB
select * from student where name like ‘%jack%’ db.student.find({name:{$regex:/jack/}})
select * from student where name regexp ‘jack’ db.student.find({name:/jack/})

6、MongoDB对AND、OR、IN的操作

AND操作:

在MongoDB中向查询文档中加入多个键值对,将多个查询条件组合在一起,这样的条件会被解释成AND操作。例如,要想查询用户名为ickes而且年龄为25岁的用户。查询语句如下:

db.users.find({"name":"ickes","age":25})  

OR查询:

查询用户名为user1 或者 age为24的用户。查询语句如下:

db.users.find({"$or":[{"name":"user1"},{"age":24}]})  

IN和NOT IN操作:

其实IN就是对单个字段的OR的一种简写。

查询年龄等于16、24、32的用户。查询语句如下:

db.users.find({"age":{"$in":[16,24,32]}})  

查询年龄不等于13、17、21的用户。查询语句如下:

db.users.find({"age":{"$nin":[13,17,21]}})  

结合 $or and $in 复合操作

// 查询根据 tag|name 模糊查询图标库图标 满足endisable: true
db.repoicons.find({"$or":[{tags: {$regex: tag, $options:'i'} }, {name: {$regex: tag, $options:'i'}}], endisable: true}).populate('userInfo', {username: 1, nickname: 1, _id: 0})

MongoDB 操作命令

1、json 格式数据导入(mongoimport)

mongoimport --db database_name --collection collection_name --file path\file_name.json

其中 "database_name" 代表数据库的名字,"collection_name" 代表集合的名字,"path" 是要导入的 json 格式数据的路径。

2、bson 格式数据导入(mongorestore)

mongoimport --db database_name --collection collection_name path\file_name.bson 

其中 "database_name" 代表数据库的名字,"collection_name" 代表集合的名字,"path" 是要导入的 bson 格式数据的路径。

3、导出数据 json(mongoexport)

mongoexport --db database_name --collection collection_name --output path\file_name.json

其中 "database_name" 代表数据库的名字,"collection_name" 代表集合的名字,"path" 是要导入的 json 格式数据的路径。

参考资料:
mongoosejs
Mongoose 之 Population 使用
mongodb doc

浏览器缓存知识

浏览器缓存知识

web缓存

缓存是指存储指定资源的一份拷贝,并在下次请求该资源时提供该拷贝的技术。当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。

  • 1.减少网络延迟,加快页面响应速度,增强用户体验
  • 2.减少网络带宽消耗
  • 3.缓解服务器端压力

Web缓存类型

在Web应用领域,Web缓存大致可以分为以下几种类型:

数据库缓存

Web应用,特别是SNS类型的应用关系比较复杂、数据库表繁多,需频繁进行数据库查询,很容易导致数据库不堪重荷。为了提高查询的性能,将查询后的数据放到内存中进行缓存,方便下次查询直接从内存缓存返回,快速响应。比如常用的缓存方案 memcachedredis

服务器端缓存

  • 代理服务器缓存

代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大。可以把它理解为一个共享缓存,不只为一个用户服务,一般为大量用户提供服务,因此在减少相应时间和带宽使用方面很有效,同一个副本会被重用多次。常见代理服务器缓存解决方案有 Squid 等。

  • CDN缓存

CDN(Content delivery networks)缓存,也叫网关缓存、反向代理缓存。CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能。浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。虽然这种架构负载均衡源服务器之间的缓存没法共享,但却拥有更好的处扩展性。从浏览器角度来看,整个CDN就是一个源服务器。

浏览器缓存

浏览器缓存根据一套与服务器约定的规则进行工作,在同一个会话过程中会检查一次并确定缓存的副本足够新。如果你浏览过程中,比如前进或后退,访问到同一个图片,这些图片可以从浏览器缓存中调出而即时显现。

浏览器缓存有那些

1、HTTP 缓存是基于 HTTP 协议的浏览器文件级缓存机制

2、HTML5 Web SQL 这种方式部分主流浏览器支持,从2010年11月18日W3C宣布舍弃 Web SQL database 草案开始

3、HTML5 indexedDB 是一个为了能够在客户端存储可观数量的结构化数据,并且在这些数据上使用索引进行高性能检索的 API

4、Cookie 一般网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密)

5、localStorage 是 HTML5 的一种新的本地缓存方案,目前用的比较多,一般用来存储ajax返回的数据,加快下次页面打开时的渲染速度

6、sessionStorage 和 localStorage 类似,但是浏览器关闭则会全部删除,api和localstorage相同,实际项目中使用较少。

7、Application Cache (AppCache) 接口设定浏览器应该缓存的资源放在 manifest 配置文件中并使得离线用户可用,不过目前已从 Web 标准中删除

8、cacheStorage是在 ServiceWorker 的规范中定义的,可以保存每个serverWorker申明的cache对象

浏览器缓存存放位置

1、memory cache

按照操作系统的常理:先读内存,再读硬盘。几乎所有的网络请求资源都会被浏览器自动加入到 memory cache 中。但是也正因为数量很大但是浏览器占用的内存不能无限扩大这样两个因素,memory cache 注定只能是个“短期存储”。常规情况下,浏览器的 TAB 关闭后该次浏览的 memory cache 便告失效 (为了给其他 TAB 腾出位置)。而如果极端情况下 (例如一个页面的缓存就占用了超级多的内存),那可能在 TAB 没关闭之前,排在前面的缓存就已经失效了。 memory cache 机制保证了一个页面中如果有两个相同的请求 (例如两个 src 相同的 ,两个 href 相同的 )都实际只会被请求最多一次,避免浪费。 在从 memory cache 获取缓存内容时,浏览器会忽视例如 max-age=0, no-cache 等头部配置。 如果站长是真心不想让一个资源进入缓存,就连短期也不行,那就需要使用 no-store。 思考:刚才提到 几乎所有的网络请求资源都会被浏览器自动加入到 memory cache 中,为什么? preloader,在2007年之前,许多情况下,浏览器某些元素的解析和执行可能会影响紧随其后的资源,浏览器停止并等待资源完成下载,然后再获取下一个资源。这意味着如果一个页面包含多个JavaScript资源或外部CSS资源,在它们之后的元素,解析器将暂停并等待每个外部资源完成下载,然后再获取下一行内容。随着页面越来越繁琐的JavaScript,对性能的影响是巨大的,并且是灾难性的。所以在资源解析执行时候,网络状态是空闲的,能不能边解边请求资源呢,在2008年,ie/谷歌等浏览器便有了相关的机制(预加载器机制),预加载的资源都是放在memory cache中的。

2、disk cache

存储在硬盘上的缓存会严格根据 HTTP 头信息中的各类字段来判定哪些资源可以缓存,哪些资源不可以缓存;哪些资源是仍然可用的,哪些资源是过时需要重新请求的。当命中缓存之后,浏览器会从硬盘中读取资源,虽然比起从内存中读取慢了一些,但比起网络请求还是快了不少的。绝大部分的缓存都来自 disk cache。 凡是持久性存储都会面临容量增长的问题,disk cache 也不例外。在浏览器自动清理时,每个浏览器都会识别“最老的”和“最可能过时的”资源。

3、Service Worker

Service Worker 是 Chrome 团队提出和力推的一个 WEB API,用于给 web 应用提供高级的可持续的后台处理能力。 service worker 能够操作的缓存是有别于浏览器内部的 memory cache 或者 disk cache。它是独立于当前页面的一段运行在浏览器后台进程里的脚本。大家可以把 Service Worker 理解为一个介于客户端和服务器之间的一个代理服务器。在 Service Worker 中我们可以做很多事情,比如拦截客户端的请求、向客户端发送消息、向服务器发起请求等等,离线资源缓存只是它的作用之一。 这个缓存是永久性的,即关闭 TAB 或者浏览器,下次打开依然还在(而 memory cache 不是)。有两种情况会导致这个缓存中的资源被清除:手动调用 API cache.delete(resource) 或者容量超过限制,被浏览器全部清空。 Service Worker 特点 网站必须使用 HTTPS。除了使用本地开发环境调试时(如域名使用 localhost) 运行于浏览器后台,可以控制打开的作用域范围下所有的页面请求 单独的作用域范围,单独的运行环境和执行线程不能操作页面 DOM,但可以通过事件机制来处理。

HTTP 缓存分类

1.非验证性缓存,或者称为强缓存,用 Cache-Control 、 Expires 、 Pragma(比较老的版本兼容写法) 来控制,其特点是一旦有效就在有效期内不会发任何请求到服务器

2.验证性缓存,也叫协商缓存,用 ETag 、 Last-Modified 、 If-None-Match 、 If-Modified-Since 来控制,其特点是会发一个请求给服务器来确认缓存是否有效,如果有效就返回 304 ,省去传输内容的时间。

从描述上很容易看出来,非验证性缓存的优先级是高于验证性缓存的,因为有验证性缓存存在就根本不会发请求,自然也没有什么 If-None-Match 之类的东西出现的机会了。

目前,浏览器功能越来越强大,我们可以在chrome中输入 chrome://view-http-cache/,查看浏览器缓存情况,方便理解。

非验证性缓存

HTTP 1.0: 基于Pragma和Expires的缓存实现

在 http1.0 时代,给客户端设定缓存方式可通过两个字段——“Pragma”和“Expires”来规范。目前这两个字段早可抛弃,但为了做http协议的向下兼容,我们还是可以看到很多网站依旧会带上这两个字段。

Pragma

当该字段值为“no-cache”的时候(事实上现在RFC中也仅标明该可选值),会知会客户端不要对该资源读缓存,即每次都得向服务器发一次请求才行。

注意,Pragma优先级比较高,使用HTTP/1.0的缓存将忽略Expires和Cache-Control头

Expires

ExpiresRFC 2616(HTTP/1.0)协议中和网页缓存相关字段,用来控制缓存的失效日期。
Expires 头指定了一个日期/时间, 在这个日期/时间之后,HTTP响应被认为是过时的;
无效的日期,比如 0, 代表着一个过去的事件,即该资源已经过期了。

注意: Expires因为是对时间设定的,且时间是Greenwich Mean Time (GMT),而不是本地时间,所以对时间要求较高。

Cache-Control

HTTP1.1新增了 Cache-Control 来定义缓存过期时间。Cache-Control在 HTTP 响应头中(通用首部字段),用于指示代理和 UA 使用何种缓存策略。 比如 no-cache 为不可缓存、private 为仅 UA 可缓存,public 为大家都可以缓存。报文中同时出现了Expires 和 Cache-Control,会以 Cache-Control 为准。在RFC中规范了 Cache-Control 的格式为:

"Cache-Control" ":" cache-directive

常用 cache-directive 值

cache-directive 说明
public 所有内容都将被缓存(客户端和代理服务器都可缓存)
private 内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)
no-cache 必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载。
no-store 所有内容都不会被缓存到缓存或 Internet 临时文件中
must-revalidation/proxy-revalidation 如果缓存的内容失效,请求必须发送到服务器/代理以进行重新验证
max-age=xxx (xxx is numeric) 缓存的内容将在 xxx 秒后失效, 这个选项只在HTTP 1.1可用, 并如果和Last-Modified一起使用时, 优先级较高

常见 Response 内的 Cache-Control

# 允许客户端进行缓存,并且,需要立即进行请求服务器验证资源是否过期
cache-control: no-cache

#直接禁止浏览器以及所有中间缓存存储任何版本的返回响应,每次用户请求该资产时,都会向服务器发送请求
cache-control: no-store

# 表示 1000 秒内,浏览器需要这资源时,直接从缓存区内拿,而不用重新发请求
# 当过期时,发送「条件 GET 请求」
cache-control: max-age: 1000

# 经常会看到这样写
cache-control: max-age: 0, must-revalidate

# 其实等同于
cache-control: no-cache

验证性缓存

协商缓存会根据[last-modified/if-modified-since]或者[etag/if-none-match]来进行判断缓存是否过期。

Last-Modified

服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端。

客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回304状态码即可。

缺点:1.保存的时间是以秒为单位的,1秒内多次修改是无法准确捕捉到;2.各机器读取到的时间不一致,导致出现误差的可能性。

传递标记的最终修改时间的请求报文首部字段一共有两个:

  • 1.If-Modified-Since: Last-Modified-value
If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT

该请求首部告诉服务器如果客户端传来的最后修改时间与服务器上的一致,则直接返回304 和响应报头即可。

当前各浏览器均是使用的该请求首部来向服务器传递保存的 Last-Modified 值。

  • 2.If-Unmodified-Since: Last-Modified-value

告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应当返回412(Precondition Failed) 状态码给客户端。

ETag

ETag HTTP响应头是资源的特定版本的标识符。ETag是http协议提供的若干机制中的一种Web缓存验证机制,并且允许客户端进行缓存协商。生成ETag常用的方法包括对资源内容使用抗碰撞散列函数,使用最近修改的时间戳的哈希值,甚至只是一个版本号。

ETag能够解决last-modified的一些缺点,但是etag每次服务端生成都需要进行读写操作,而last-modified只需要读取操作,从这方面来看,etag的消耗是更大的。

和last-modified一样,浏览器会先发送一个请求得到ETag的值,然后再下一次请求在request header中带上if-none-match:[保存的etag的值]。通过发送的etag的值和服务端重新生成的etag的值进行比对,如果一致代表资源没有改变,服务端返回正文为空的响应,告诉浏览器从缓存中读取资源。

If-None-Match: ETag-value

If-None-Match: "56fcccc8-1699"

告诉服务端如果 ETag 没匹配上需要重发资源数据,否则直接回送304 和响应报头即可。当前各浏览器均是使用的该请求首部来向服务器传递保存的 ETag 值。

If-Match: ETag-value

告诉服务器如果没有匹配到ETag,或者收到了“*”值而当前并没有该资源实体,则应当返回412(Precondition Failed) 状态码给客户端。否则服务器直接忽略该字段。

缓存头部对比

头部 优势和特点 劣势和问题
Expires 1、HTTP 1.0 产物,可以在HTTP 1.0和1.1中使用,简单易用。
2、以时刻标识失效时间。
1、时间是由服务器发送的(UTC),如果服务器时间和客户端时间存在不一致,可能会出现问题。
2、存在版本问题,到期之前的修改客户端是不可知的。
Cache-Control 1、HTTP 1.1 产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题。
2、比Expires多了很多选项设置。
1、HTTP 1.1 才有的内容,不适用于HTTP 1.0 。
2、存在版本问题,到期之前的修改客户端是不可知的。
Last-Modified 1、不存在版本问题,每次请求都会去服务器进行校验。服务器对比最后修改时间如果相同则返回304,不同返回200以及资源内容。 1、只要资源修改,无论内容是否发生实质性的变化,都会将该资源返回客户端。例如周期性重写,这种情况下该资源包含的数据实际上一样的。
2、以时刻作为标识,无法识别一秒内进行多次修改的情况。
3、某些服务器不能精确的得到文件的最后修改时间。
ETag 1、可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况。
2、不存在版本问题,每次请求都会去服务器进行校验。
1、计算ETag值需要性能损耗。
2、分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时发现ETag不匹配的情况。

缓存头部对比

从浏览器请求到展示资源的过程:

计算过期时间

用户行为对浏览器缓存的控制

地址栏访问重载

地址栏重新输入当前页面地址并按下回车也会当做刷新处理, 这意味着只有从新标签页或超链接打开时,才能观察到直接使用硬盘缓存的情况。

正常重新加载

按下刷新按钮或快捷键(在 MacOS 中是 Cmd+R)会触发浏览器的“正常重新加载”(normal reload), 此时浏览器会执行一次 Conditional GETCache-Control等缓存头字段会被忽略,并且带If-None-Match, If-Modified-Since等头字段。 此时服务器总会收到一次 HTTP GET 请求。 在 Chrome 中按下刷新,浏览器还会带如下请求头:

Cache-Control:max-age=0

强制重新加载

在 Chrome 中按下 Cmd+Shift+R (MacOS)可以触发强制重新加载(Hard Reload), 此时包括页面本身在内的所有资源都不会使用缓存。 浏览器直接发送 HTTP 请求且不带任何条件请求字段。

缓存运用情况

目前,各大公司对静态资源基本都采用 CDN 强缓存处理,通过加大 Cache-Control 的 max-age 有效值(例如京东设置的max-age为315360000一年),那输入URL按回车,我们会始终看到200 OK (form cache)缓存信息,知乎解释

强缓存的更新文件方式,主要是通过改变文件名,来更新文件缓存。常用手段,文件路径添加时间戳(?=xxx),文件名用md5,hash代替等。

但同时也会对一些不经常变更的静态资源做 ETag 协商缓存处理,以达到优化作用。

缓存配置实践

Nginx 缓存配置

location ~ .*\.(ico|svg|ttf|eot|woff)(.*) {
  proxy_cache               pnc;
  proxy_cache_valid         200 304 1y;
  proxy_cache_valid         any 1m;
  proxy_cache_lock          on;
  proxy_cache_lock_timeout  5s;
  proxy_cache_use_stale     updating error timeout invalid_header http_500 http_502;
  etag                      on;
  expires                   1y;
}

nginx proxy_cache

NodeJS 缓存配置

  • 对于使用 Express Static 中间件,max-age 设置的单位是毫秒。
var oneYear = 60 * 1000 * 60 * 24 * 365;
app.use(express.static('staticFile', { maxAge: oneYear }));
  • 原生NodeJS可以使用 setHeader 方法来添加 Cache-Control 。
response.setHeader('Cache-Control', 'max-age=31536000');

QA:遇到的缓存问题

1.from disk cache 和 from memory cache 区别

Chrome在高版本中缓存策略之后,
当看到的 200 from memory cache 就是验证性缓存 ,而 200 from disk cache 就是非验证性缓存,可通过两个浏览器访问对比得出

2.只设置Etag,那么为什么在 Chrome 下会有非验证性缓存呢?

没有设置 Cache-Control 这个头,其默认值是 Private ,在标准中明确说了:

Unless specifically constrained by a cache-control
directive, a caching system MAY always store a successful response

如果没有 Cache-Control 进行限制,缓存系统可以对一个成功的响应进行存储

很显然, Chrome 是遵守标准的,它在没有检查到 Cache-Control 的时候对响应做了非验证性缓存,所以你看到了 200 from memory cache
同时 Safari 也是遵守标准的,因为标准只说了可以进行存储,而非应当或者必须,所以 Safari 不进行缓存也是合理的

我们可以理解为,没有 Cache-Control 的情况下,缓存不缓存就看浏览器高兴,你也没什么好说的。那么你如今的需求是“明确不要非验证性缓存”,则从标准的角度来说,你必须指定相应的 Cache-Control 头

查看浏览器缓存文件: chrome://view-http-cache/

3.常见Cache-Control 的 max-age 有效值设置

365天

Cache-Control:max-age = 315360000

30天

Cache-Control:max-age = 25920000

4.Response Header 中 Age 与 Date

Age表示命中代理服务器的缓存. 它指的是代理服务器对于请求资源的已缓存时间, 单位为秒.

Date指的是响应生成的时间,请求经过代理服务器时, 返回的Date未必是最新的, 通常这个时候, 代理服务器将增加一个Age字段告知该资源已缓存了多久。

5.Cache-Control设置强缓存计算过期时间

计算过期时间

过期日期 = Date + max-age ,所得出的日期

6.CAUTION: Provisional headers are shown

出现这个警告,是因为去获取该资源的请求其实并(还)没有真的发生,所以 Header 里显示的是伪信息,直到服务器真的有响应返回,这里的 Header 信息才会被更新为真实的。

参考资料:

Hypertext Transfer Protocol rfc2616 -- HTTP/1.1

IndexedDB 浏览器存储限制和清理标准

Caching Tutorial 缓存指南

Cache-control

Web缓存机制系列

浅谈 web 缓存

浅谈浏览器http的缓存机制

HTTP 缓存

使用 HTTP 缓存:Etag, Last-Modified 与 Cache-Control

合理使用 HTTP 缓存

NGINX缓存使用官方指南

ASP.NET Core 缓存技术 及 Nginx 缓存配置

网页性能优化:设置永久缓存

HTTP缓存控制小结

你应该知道的浏览器缓存知识

Blob对象

一直以来,JS都没有比较好的可以直接处理二进制的方法。而Blob的存在,允许我们可以通过JS直接操作二进制数据。

Blob对象

一个 Blob对象表示一个不可变的, 原始数据的类似文件对象。Blob表示的数据不一定是一个JavaScript原生格式。 File 接口基于Blob,继承 blob功能并将其扩展为支持用户系统上的文件。

数据类型 Blob 对象是在HTML5中,新增了File API。

构造Blob对象

生成Blob对象有两种方法:一种是使用Blob构造函数,另一种是对已有的Blob对象使用slice()方法切出一段。

Blob构造函数

var blob = new Blob(data[, options]))

返回一个新创建的 Blob 对象,其内容由参数中给定的数组串联组成。

Blob构造函数接受两个参数:

参数data是一组数据,所以必须是数组,即使只有一个字符串也必须用数组装起来.

参数options是对这一Blob对象的配置属性,目前也只有一个type也就是相关的MIME需要设置 type的值:
'text/csv,charset=UTF-8' 设置为csv格式,并设置编码为UTF-8,'text/html'设置成html格式。

注意:任何浏览器支持的类型都可以这么用

var blob = new Blob(['我是Blob'],{type: 'text/html'});

Blob属性

blob.size   //Blob大小(以字节为单位)
blob.type   //Blob的MIME类型,如果是未知,则是“ ”(空字符串)

slice() 创建

slice()返回一个新的Blob对象,包含了源Blob对象中指定范围内的数据。

Blob.slice([start[, end[, contentType]]])

参数说明:

  • start 可选,开始索引,可以为负数,语法类似于数组的slice方法.默认值为0.

  • end 可选,结束索引,可以为负数,语法类似于数组的slice方法.默认值为最后一个索引.

  • contentType可选 ,新的Blob对象的MIME类型,这个值将会成为新的Blob对象的type属性的值,默认为一个空字符串.

URL.createObjectURL()

URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。

objectURL = URL.createObjectURL(blob);

使用URL.createObjectURL()函数可以创建一个Blob URL,参数blob是用来创建URL的File对象或者Blob对象,返回值格式是:blob://URL。

在每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法传入创建的URL为参数,用来释放它。浏览器会在文档退出的时候自动释放它们,但是为了获得最佳性能和内存使用状况,应该在安全的时机主动释放掉它们。

URL.revokeObjectURL()

URL.revokeObjectURL() 静态方法用来释放一个之前通过调用 URL.createObjectURL() 创建的已经存在的 URL 对象。当你结束使用某个 URL 对象时,应该通过调用这个方法来让浏览器知道不再需要保持这个文件的引用了。

window.URL.revokeObjectURL(objectURL);

参数: objectURL 是一个通过URL.createObjectURL()方法创建的对象URL.

Blob的使用

  1. 使用Blob最简单的方法就是创建一个URL来指向Blob:
<a download="data.txt" id="getData">下载</a>   

var data= 'Hello world!';  
var blob = new Blob([data], {   
  type: 'text/html,charset=UTF-8'   
});
window.URL = window.URL || window.webkitURL; 
document.querySelector("#getData").href = URL.createObjectURL(blob);

上面的代码将Blob URL赋值给a,点击后提示下载文本文件data.txt,文件内容为“Hello World”。

2.Blob 响应

window.URL = window.URL || window.webkitURL;  // Take care of vendor prefixes.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';
xhr.send()

xhr.onload = function(e) {
  if (this.status == 200) {
    var blob = this.response;

    var img = document.createElement('img');
    var URL = window.URL || window.webkitURL;  //兼容处理
    var objectUrl = URL.createObjectURL(blob);
    img.onload = function(e) {
      window.URL.revokeObjectURL(img.src); // 释放 url.
    };

    img.src = objectUrl;
    document.body.appendChild(img);
    ...
  }
};

xhr.send();

总结

目前,Blob对象大多是运用在,处理大文件分割上传(利用Blob中slice方法),处理图片canvas跨域(避免增加crossOrigin = "Anonymous",生成当前域名的url,然后 URL.revokeObjectURL()释放,createjs有用到),以及隐藏视频源路径等等。

① 大文件分割上传

function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };
  xhr.send(blobOrFile);
}

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
  var blob = this.files[0];

  const BYTES_PER_CHUNK = 1024 * 1024; // 1MB chunk sizes.
  const SIZE = blob.size;

  var start = 0;
  var end = BYTES_PER_CHUNK;

  while(start < SIZE) {
    upload(blob.slice(start, end));

    start = end;
    end = start + BYTES_PER_CHUNK;
  }
}, false);

② 图片跨域请求,处理跨域问题,参考 createjs ImageLoader.js

在使用 preloadJS处理加载问题时,我们可以绕过其他方式跨域Queue = new createjs.LoadQueue(true, '', "Anonymous"); 语法 LoadQueue (preferXHR, basePath, crossOrigin)则可以达到 image.crossOrigin = "Anonymous"设置跨域的效果,这个文档里面没有说明,需要阅读源码才能知道。

③ 隐藏视频源路径

var video = document.getElementById('video');
var obj_url = window.URL.createObjectURL(blob);
video.src = obj_url;
video.play()
window.URL.revokeObjectURL(obj_url);

④ Web Worker 串行加载优化

一般形式 :

main.js:

const worker = new Worker('worker.js');

worker.addEventListener('message', function(evt) {
    console.log(`[main] result is: ${evt.data.result}.`);
}, false);

worker.postMessage({num1: 20, num2: 10});

console.log('[main] Main is initialized.');

worker.js:

self.addEventListener('message', function (evt) {
    const num1 = evt.data.num1;
    const num2 = evt.data.num2;
    const result = num1 + num2;
    console.log('[worker] num1=' + num1 + ', num2=' + num2);
    self.postMessage({result: result});
}, false);

console.log(`[worker] Worker is initialized.`);

使用 Blob、URL.createObjectURL 优化后:

const workerFileContent = `self.addEventListener('message', function (evt) {
    const num1 = evt.data.num1;
    const num2 = evt.data.num2;
    const result = num1 + num2;
    console.log('[worker] num1=' + num1 + ', num2=' + num2);
    self.postMessage({result: result});
}, false);`;

const workerBlob = new Blob([workerFileContent], { type:'text/javascript' });
const workerUrl = window.URL.createObjectURL(workerBlob);
const worker = new Worker(workerUrl);
window.URL.revokeObjectURL(workerUrl);

worker.addEventListener('message', function(evt) {
    console.log(`[main] result is: ${evt.data.result}.`);
}, false);

worker.postMessage({num1: 20, num2: 10});

⑤ 使用 createObjectURL(blob) 输出页面,移动端长按保存,转发

测试demo

实例发现,经过 revokeObjectURL(url) 将丧失长按保存转发所有功能,不释放兼容性不是特别好,目前ios全部支持,andriod部分机型不支持

参考资料:
文件和二进制数据的操作
XMLHttpRequest2 新技巧
URL.createObjectURL()
Sending and Receiving Binary Data
createjs ImageLoader.js
URL.createObjectURL和URL.revokeObjectURL
JavaScript多线程编程
Web Workers 资源跨域问题
worker-loader

chrome使用技巧集锦

Chrome调试工具介绍

Elements 板块可以看到整个页面的Dom结构。

Console 调试日志控制台。

Sources 所有资源看到页面加载的资源,图片,css,js等,它们会按照资源的来源分类。

Network 查看页面所加载的所有资源响应情况,响应时间,浏览器等待时间,状态码,MINE Type,资源大小等。

Perfomance 查看浏览器的渲染流程:解析代码,布局,绘制,合并渲染层。

Memory 含Profiles选项主要是用来检测CPU占用程度,堆栈申请的内存。

Application 显示资源,跟Sources不同的是,它会对文档类型分类。并且可以查看,增加,删除,修改页面LocalStorage,SessionStorage,Cookies等。

Security 安全检测提醒。

Chrome快捷使用调试技巧

快速切换文件

按Ctrl+P(cmd+p on mac),就能快速搜寻和打开你项目的文件。

快速切换文件

源代码中搜索

在要在Elements查看源码,只要定位到Elements面板,然后按 ctrl+f (cmd+f on mac)就可以

快速切换文件

源代码快速跳转到指定行

在Sources标签中打开一个文件之后,按Ctrl + G,(or Cmd + L for Mac),然后输入行号,chrome控制台就会跳转到你输入的行号所在的行。

或者ctrl+p后输入 :行号;

源代码快速跳转到指定行

使用多个插入符进行选择

当编辑一个文件的时候,你可以按住Ctrl(cmd on mac)在你要编辑的地方点击鼠标,可以设置多个插入符,这样可以一次在多个地方编辑。

使用多个插入符进行选择

格式化凌乱的js源码

当看到压缩好的一团糟的js,都不知道从何着手去看。chrome控制台有内建的美化代码功能,可以返回一段最小化且格式易读的代码。Pretty Print的在Sources标签的左下角。

格式化凌乱的js源码

选择下一个匹配项

当在Sources下编辑文件时,按下Ctrl + D (Cmd + D) ,当前选中的单词的下一个匹配也会被选中,有利于你同时对它们进行编辑。

选择下一个匹配项

##Chrome Dev开发者选项

万能的Chrome地址栏功能,很多人不知道,作为一个Chrome用户,必须懂的。以下我要介绍的这些指令在Chrome地址栏输入即可,可以输入 chrome://about/ (记住此命令使用about:about同理)查看所以指令

Chrome的about指令

chrome://version/ (about:version) – 显示当前版本

chrome://histograms/ (about:histograms) – 显示历史记录

chrome://dns/ (about:dns) – 显示DNS状态

chrome://cache/ (about:cache) - 重定向到 chrome://cache/ 显示缓存页面

chrome://flags/ (about:flags) – Chrome高级设置

chrome://accessibility/ – 查看浏览器当前访问的标签

chrome://appcache-internals/ - 对HTML5应用的离线存储进行管理

chrome://apps/ - Chrome网上应用商店

chrome://bookmarks/ - Chrome书签管理

chrome://cache/ - Chrome缓存, 查看浏览器缓存的文件,会用16进制的方法显示缓存文件

chrome://components/ - 查看相关组件

chrome://crashes/ - 停用启用崩溃报告

chrome://credits/ - 查看第三方软件许可证

chrome://devices/ - 查看设备,比如链接的打印机

chrome://dns/ - 查看DNS记录

chrome://extensions/ - 查看扩展程序

chrome://flags/ - 实验性功能列表

chrome://history/ - 查看历史记录

chrome://net-internals/ - Chrome的抓包工具,基本把dns、prefetch、cache等功能集成

注意,从使用经验来说 chrome://flags/ 开启实验功能列表、 chrome://net-internals/抓包工具是非常重要

如何禁止chrome自动跳转https

在chrome的地址栏输入:chrome://net-internals/#hsts

在打开的页面中, Delete domain 栏的输入框中输入:xx.xx.com(注意这里是二级域名),然后点击“delete”按钮,即可完成配置。
然后你可以在 Query domain 栏中搜索刚才输入的域名,点击“query”按钮后如果提示“Not found”,那么现在就可以使用http来访问了。

参考资料:

史上最全的Chrome使用技巧集锦

Chrome使用技巧

关于 Chrome DevTools 的 25 个实用技巧

Chrome控制台使用详解
chrome://flags/ 中有哪些值得调整的选项?

Sublime Snippet 代码段

依次找到:Tools -> Developer -> New Snippet,默认代码段配置文件如下:

<snippet><content><![CDATA[
Hello, ${1:this} is a ${2:snippet}.
]]></content><!-- Optional: Set a tabTrigger to define how to trigger the snippet --><!-- <tabTrigger>hello</tabTrigger> --><!-- Optional: Set a scope to limit where the snippet will trigger --><!-- <scope>source.python</scope> --></snippet>

配置文件是 XML 格式,含义如下:

  • snippet: 父节点,必须
  • content: 内容节点,将要定义的代码段写到该节点下
  • tabTrigger: 触发节点,设置触发源
  • scope: 代码段生效作用域
  • ${number:}: Tab 按键切换的光标

下面是一个 Demo:

<snippet><content><![CDATA[
Hello, ${1:this} is a ${2:snippet}.
]]></content><!-- Optional: Set a tabTrigger to define how to trigger the snippet --><tabTrigger>hello</tabTrigger><!-- Optional: Set a scope to limit where the snippet will trigger --><!-- <scope>source.python</scope> --></snippet>

接下来 Ctrl + s 保存该文件,写入文件名:hello.sublime-snippet , 然后到文件中通过输入 hello 触发给代码段。

注意:最后保存文件的时候,必须是 xxx.sublime-snippet 的名字保存,其中 xxx 替换成你自己定义的名字。

[转载] 2018 来谈谈 Web Component

对很多人来说,组件已经成为他们开发工作中的核心概念。组件提供了一种健壮的模型,允许我们用一个个更小的更简单的封装好的部件来搭建出复杂的应用程序。组件的概念在 Web 上已经存在一段时间了,比如在 JavaScript 生态的早期,Dojo Toolkit 已经在它的 Dijit 插件系统里面应用了组件这个概念。

现代框架比如说 React、Angular、Vue 和 Dojo 进一步把组件放在开发的前列,并作为核心要素用在它们自己的框架结构上。然而,虽说组件结构变得越来越普遍,但是各种各样的框架和库也衍生出一个纷繁复杂、四分五裂的组件生态。这种分裂常常将一些团队钉死在某个特定的框架上,哪怕时间、技术的更迭也不会轻易地改变。

解决这种割裂的形势,让 Web 组件模型统一化,这项工作已经在努力推进中。最早的努力当数 “Web Component” 规范说明 circa 2011 的出现,并在同年的 Fronteers Conference 大会上由 Alex Russell 将之宣之于众。该 Web Component 规范的产生和发展,旨在提供一种权威的、浏览器能理解的方式来创建组件。在做出跨浏览器支持的组件方案这件事上我们还有很多事情要做,但已经比以往任何时候更接近目标了。理论上讲,这些规范和实践铺平了组件间相互作用相互结合的道路,即使这些组件出自不同的供应方(比如 React,比如 Vue)。下面我们开始探索 Web Component 规范的组成。

组成部分

Web Component 并非单一的技术,而是由一系列 W3C 定义的浏览器标准组成,使得开发者可以构建出浏览器原生支持的组件。这些标准包括:

  • HTML Templates(译者注:模板)and Slots(译者注:插槽) — 可复用的 HTML 标签,提供了和用户自定义标签相结合的接口
  • Shadow DOM(译者注:影子节点) — 对标签和样式的一层 DOM 包装
  • Custom Elements(译者注:自定义元素) — 带有特定行为且用户自命名的 HTML 元素

这里还有另一个 Web Component 规范,HTML Imports,用于将 HTML 代码及 Web Component 导入到网页中。然而,在交叉参考 ES Module 规范后,Firefox 团队认为这不是一种最佳实践,该规范也就没多少人在推动了。

Shadow DOM 和 Custom Element 规范经历了一些迭代,现在都已经是第二个版本(v1)。在 2016 年 2 月,有人推动将 Shadow DOM 和 Custom Element 并入 DOM 标准规范里面,而不再作为独立的规范存在。

template 标签和 slot 标签

HTML 模板是支持度最高的特性,可以说是 Web Component 规范最直观的体现。它允许开发者定义一个直到被复制使用时才会进行渲染的 HTML 标签块。你可以参考下面的简单示例来定义一个模板:

<template id="custom-template>
     <h1>HTML Templates are rad</h1>
</template>
复制代码

一旦 DOM 里面定义了这样的一个模板,就可以在 JavaScript 里面引用了:

const template = document.getElementById("custom-template");
const templateContent = template.content;
const container = document.getElementById("container");
const templateInstance = templateContent.cloneNode(true);
container.appendChild(templateInstance);
复制代码

像上面那样写,就可以借助 cloneNode 函数来复用这个模板。提到 <template> 标签就不得不提 <slot> 标签。slot 标签允许开发者通过特定接入点来动态替换模板中的 HTML 内容。它用 name 属性来作为唯一识别标志(译者注,就类似普通 DOM 节点的 id 属性):

<template id="custom-template">
    <p><slot name="custom-text">We can put whatever we want here!</slot></p>
</template>

slot 标签在 Custom Element 的注入中非常有用。它允许开发者在写好的 Custom Element 里面设置标记。当 Custom Element 里面的节点用到了 slot 属性作为标记,那这个节点就会替换掉模板里面对应的 slot 标签。

Shadow DOM

在页面上定位具体的节点这是 web 开发的一个基本能力。CSS 选择器不仅可以用来给节点加样式,还可以用来查询特定的 DOM 集合。这通常发生在根据一个标识符选择特定节点,比方说使用 document.querySelectorAll 就可以找到整个 DOM 树中匹配指定选择器的节点数组。然而,如果应用程序非常庞大,有很多节点有冲突的 class 属性,那又该怎么办?此时,程序就不知道哪个节点是想被选中的,bug 也就随之产生。如果可能的话,将部分 DOM 节点抽象出来,隔离开来,让它们不会被 DOM 选择器选择到,那岂不是很好?Shadow DOM 就能做到,它允许开发者将一些节点放到独立的子树上来实现隔离。根本上说 Shadow DOM 提供了一种健壮的封装方式来做到页面节点的隔离,这也是 Web Component 的核心优势。

与此相似,CSS 的类和 ID 应用于全局样式时也会出现类似的问题。冲突的命名标示会导致样式的相互覆盖。那参考上面 DOM 树选择节点的思路,如果能将 CSS 样式限制在某个 DOM 的子树上,不就可以避免全局样式冲突,解决问题?比较有名的样式设置技术比如 CSS ModulesStyled Components、以及CSS BEM 命名规范,它们的核心出发点之一就是为了解决这个问题。举个例子,CSS 模块技术通过对类名和模块名进行哈希处理,赋予每个 CSS 样式唯一的标识符从而避免冲突。Shadow DOM 跟它们不同之处在于它并不对类名做处理,而是直接就把这个作为原生特性来支持。它将部分 DOM 节点隔离开来使得我们的网站和程序少了不可预知的变化,更加稳定。

tips: emotion-jslinaria 都是基于 Styled Components 的实现

那在代码层面上该怎么操作?可以这样将 Shadow DOM 附加到一个节点上:

element.attachShadow({mode: 'open'});
复制代码

这里 attachShadow 函数接受一个含 mode 属性的对象作为参数。Shadow DOM 可以打开关闭打开时使用 element.shadowRoot 就可以拿到 DOM 子树,反之如果关闭了则会拿到 null。接着创建一个 Shadow DOM 就会创建一个阴影的边界,在封装节点的同时封装样式。默认情况下该节点内部的所有样式会被限制仅在这个影子树里生效,于是样式选择器写起来就短得多了。Shadow DOM 通常可以和 HTML 模板结合使用:

const shadowRoot = element.attachShadow({mode: 'open'});
shadowRoot.appendChild(templateContent.cloneNode(true));

现在这个 element 就有一个影子树,影子树的内容是模板的一个复制。Shadow DOM、 <template> 标签、<slot> 标签在这里和谐地应用在一起,构造出了可复用、封装良好的组件。

通过 Custom Element 进一步封装

HTML 的 template 和 slot 标签提供了复用性和灵活性,Shadow DOM 提供了封装方法。而 Custom Element 再进一步,将所有这些特性打包在一起成为有自己名字的可反复使用的节点,让它可以像常规 HTML 节点一样用起来。

定义一个 Custom Element

定义 Custom Element 要用到 JavaScript。Custom Element 依赖 ES2015+ 的 Class 特性,用 Class 作为其声明模式,通常是从 HTMLElement 或它的子类继承而来。这里有一个 Custom Element 的例子,使用 ES2015+ 语法创建,用于计数:

// 我们定义一个 ES6 的类,拓展于 HTMLElement
class CounterElement extends HTMLElement {
    constructor() {
        super();
 
        // 初始化计数器的值
        this.counter = 0;
 
        // 我们在当前 custom element 上附加上一个打开的影子根节点
        const shadowRoot= this.attachShadow({mode: 'open'});
 
        // 我们使用模板字符串来定义一些内嵌样式
        const styles=`
            :host {
                position: relative;
                font-family: sans-serif;
            }
 
            #counter-increment, #counter-decrement {
                width: 60px;
                height: 30px;
                margin: 20px;
                background: none;
                border: 1px solid black;
            }
 
            #counter-value {
                font-weight: bold;
            }
        `;
 
        // 我们给影子根节点提供一些 HTML
        shadowRoot.innerHTML = `
            <style>${styles}</style>
            <h3>Counter</h3>
            <slot name='counter-content'>Button</slot>
            <button id='counter-increment'> - </button>
            <span id='counter-value'>; 0 </span>;
            <button id='counter-decrement'> + </button>
        `;
 
        // 我们可以通过影子根节点查询内部节点
        // 就比如这里的按钮
        this.incrementButton = this.shadowRoot.querySelector('#counter-increment');
        this.decrementButton = this.shadowRoot.querySelector('#counter-decrement');
        this.counterValue = this.shadowRoot.querySelector('#counter-value');
 
        // 我们可以绑定事件,用类方法来响应
        this.incrementButton.addEventListener("click", this.decrement.bind(this));
        this.decrementButton.addEventListener("click", this.increment.bind(this));
 
    }
 
    increment() {
        this.counter++
        this.invalidate();
    }
 
    decrement() {
        this.counter--
        this.invalidate();
    }
 
    // 当计数器的值发生变化时调用
    invalidate() {
        this.counterValue.innerHTML = this.counter;
    }
}
 
// 这里定义了可以在 DOM 树上直接使用的真实节点
customElements.define('counter-element', CounterElement);

特别注意最后一行,那里注册了可以用在 DOM 里面的 Custom Element。

Custom Element 的种类

上面代码展示了如何从 HTMLElement 接口做拓展,然而我们还可以从更具体的节点上拓展,比如 HTMLButtonElement。Web Component 规范提供了一个完整的可供继承的接口列表

Custom Element 可分为两种主要类型:独立自定义元素(Autonomous custom elements)内置自定义元素(Customized built-in elements)。独立自定义元素和那些早已定义且不继承自特定接口的节点类似(译者注:就是我们平常使用的 DOM 节点)。一个独立自定义元素只要在页面一定义上,就可以像常规 HTML 节点那样使用。举个例子,上面定义的计数节点,既可以在 HTML 中通过 <counter-element></counter-element> 定义,也可以在 JavaScript 中用 document.createElement('counter-element') 来创建。

内置自定义元素在使用上略有不同,当 HTML 定义节点时可以传一个 is 属性到标准节点上(比如 <button is='special-button'>),又或者使用 document.createElement 时传一个 is 属性作为参数(比如 document.createElement("button", { is: "special-button" })。

Custom Element 的生命周期

Custom Element 也有一系列的生命周期事件,用于管理组件连接和脱离 DOM :

  • connectedCallback:连接到 DOM
  • disconnectedCallback: 从 DOM 上脱离
  • adoptedCallback: 跨文档移动

一种常见错误是将 connectedCallback 用做一次性的初始化事件,然而实际上你每次将节点连接到 DOM 时都会被调用。取而代之的,在 constructor 这个 API 接口调用时做一次性初始化工作会更加合适。

此处还有一个 attributeChangedCallback 事件可以用来监听节点(译者注:使用 Custom Element 定义的节点)属性的变化,然后通过这个变化来更新内部状态。不过,要想用上这个能力,必须先在节点类里面定义一个名为 observedAttributes 的 getter:

constructor() {
    super();
    // ...
    this.observedAttributes();
}
 
get observedAttributes() {return ['someAttribute']; } 
// 其他方法

从这里起就可以通过 attributeChangedCallback 来处理节点属性的变化:

attributeChangedCallback(attributeName, oldValue, newValue) {
    if (attributeName==="someAttribute") {
        console.log(oldValue, newValue)
        // 根据属性变化做一些事情
    }
}

支持度如何?

截至 2018 年 6 月,Shadow DOM 第二版和 Custom Element 第二版在 Chrome、Safari、三星浏览器上已经支持,还被 Firefox 列为要支持的特性,希望很大。而 Edge 依然在考虑是否支持。在这个时间点,Github 仓库 webcomponents 上已经有了一系列的 polyfill。这些 polyfill 使得包括 IE11 在内的所有当下活跃的浏览器上都能运转 Web Component。该 webcomponents 库包含多种形态,既提供了一个包含所有必要 polyfill 的脚本(webcomponents-bundle.js),也提供了一个通过特性检测来只加载必要 polyfill 的版本(webcomponents-loader.js)。如果使用第二种,你还是必须将各个 polyfill 文件都放到服务器上来保证加载器可以加载到。

对于那些代码中只能用 ES5 的情况,还必须加载一个 custom-elements-es5-adapter.js 文件,而且它必须首先加载,不能跟组件代码打包在一起。之所以需要这个适配文件是因为 Custom Element 必须 继承自 HTMLElement 类,且构造函数中必须以 ES2015 的方式调用 super()(这在 ES5 代码里看起来会很困惑!)。在 IE11 中还是会由于不支持 ES2015 的类特性而抛出错误,不过可以忽略之

Web Component 和框架

历史上,Web Component 最大的支持者之一是 Polymer 库。Polymer 针对 Web Component API 添加了一些语法糖使得定义和传递组件变得更加容易。在最新版本 Polymer3 中,它与时俱进用上了 ES2015 的模块特性并且使用 npm 作为标准的包管理工具,跟上了其他的现代框架。Web Component 编码工具的另一种形态则更像是编译器而非框架。StencilSvelte 这两个框架就是这样。它们使用各自的工具 API 来书写组件,然后编译成原生的 Web Component。一些框架比如 Dojo 2, 则选择允许开发者编写特定框架的组件,不过也允许编译成原生 Web Component 就是了。在 Dojo2 中这是用 @dojo/cli tools 来实现的。

努力实现原生的 Web Component 的一个愿景,是希望跨越不同团队不同项目来共用组件,即使它们用的是不同的框架。当下不同的框架和 Web Component 规范有不同的关系,有些更贴近规范有些则不然。已经有一些指引告诉我们怎么在诸如 ReactAngular 这样的框架中用上原生的 Web Component ,但它们的实现上还是带着浓浓的框架特色。有一个很好的资源可以帮你理解这些关系,那就是 Rod Dodson 的 Custom Elements Everywhere,它通过测试用例测出不同框架想和 Custom Element(Web组件规范的核心) 结合的难易程度。

最后的想法

围绕 Web Component 的使用和炒作不断持续此起彼伏。这意味着,随着 Web Component 得到越来越好的支持,polyfill 将逐渐淡出我们的视野,组件书写将更加简洁和快速。Shadow DOM 允许开发者写一些简单的限定区域有效的 CSS,这无疑更加容易管理,通常性能也会更好。Custom Element 提供了一种统一的方法来定义组件,这些组件可以(理论上)跨代码库和团队来使用。目前有一些额外的规范建议,开发者可以根据基本规范加以利用:

这些补充规范可以为原生 web 平台增加更多功能,让开发者不用再去理解那么多抽象概念,释放更多的潜力。

该基本规范毫无疑问是一套强大的工具,但最终它是否能发挥最大的效用还是要取决于用到它的框架、开发者和团队。目前如 React、Vue、Angular 这样的框架已经大大占据了开发者的大脑,它们会因为这些原生态的技术和工具而逐渐败下阵来吗?只能让时间来见证了。


VAGRANT 使用

Vagrant 是基于Ruby的工具,依赖 VirtualBox,VMware,AWS 或其他虚拟机管理软件 (provider) 的接口来创建虚拟机, 用于创建和部署虚拟化开发环境。标准的 配置管理工具(provisioning tools (例如 shell 脚本, Chef, 或 Puppet)都可以在 Vagrant 创建的虚拟机上使用,用来进行自动安装及配置管理。我们可以使用它来干如下这些事:

  • 建立和删除虚拟机
  • 配置虚拟机运行参数
  • 管理虚拟机运行状态
  • 自动配置和安装开发环境
  • 打包和分发虚拟机运行环境

注意版本兼容问题目前 2.0 版本不兼容前面的版本。

Install VirtualBox

Download VirtualBox: https://www.virtualbox.org/wiki/Downloads

Install Vagrant

Download Vagrant: https://www.vagrantup.com/downloads.html

Uninstall Vagrant

On Windows

Uninstall using the add/remove programs section of the control panel

On Mac OS X

rm -rf /opt/vagrant
rm -f /usr/local/bin/vagrant
sudo pkgutil --forget com.vagrant.vagrant

On Linux

rm -rf /opt/vagrant
rm -f /usr/bin/vagrant

Vagrantfile 配置

创建使用 Vagrant 的第一步就是对 Vagrantfile 配置文件进行配置。Vagrantfile 配置文件的作用有两个方面:

  1. 设置项目的根目录,很多 Vagrant 的配置都是与根目录有紧密关系。
  2. 定义项目中所需的虚拟机类型、资源、插件。
# Setting the Language using environment variable LC_ALL and LANG in UNIX OS in EngageOne Generate
ENV["LC_ALL"] = "en_US.UTF-8"
$Script = <<-SHELL
    apt-get update
    apt-get install -y nginx
  SHELL
# Vagrantfile 配置 基本格式
Vagrant.configure("2") do |config|
	# 指定镜像
	config.vm.box = "centos/7"  
	# 分配磁盘大小
	config.disksize.size = '50GB'
	# 定义转发端口
	config.vm.network "forwarded_port", guest: 80, host: 8080
	config.vm.boot_timeout = 5000
	config.ssh.username = 'Vagrant'	
	
	# 定义依赖虚拟机内存、CPU等
	config.vm.provider "virtualbox" do |vb|
  	vb.name = "my_vm"
  	vb.memory = "1024"
  	vb.cpus = 2
	end
	
	# Vagrant 中 Provisioner 允许你自动安装软件、更改配置
	# 通常在 vagrant up、vagrant reload执行,可以--provision(强制执行)、--no-provision (取消执行)
	# 变量换行模式
	config.vm.provision "shell" do |s|
    s.inline = "echo hello"
  end
  # inline 模式
	config.vm.provision "shell", inline: $Script
  # ...
end

初始化 Vagrant 项目

$ mkdir vagrant_getting_started  # 建立 Vagrant 项目目录
$ cd vagrant_getting_started
$ vagrant init # 初始化 vagrant 项目,生成 Vagrantfile, 也可直接 vagrant init centos/7 生成对应 Vagrantfile

BOXES 镜像

基础镜像包在 Vagrant 中被称为 boxes ,类似于docker体系中的image(镜像)。

查找更多 Vagrant Boxes https://app.vagrantup.com/boxes/search

$ vagrant box list  # 列出本地环境中所有的box

$ vagrant box add centos/7 # 添加 box 到本地vagrant环境

# 选择 provider,依赖虚拟技术提供方,如我本地安装 virtualbox 则选择 virtualbox
1) hyperv
2) libvirt
3) virtualbox
4) vmware_desktop

$ vagrant box update box-name  # 更新本地指定 box
$ vagrant box remove box-name  # 删除本地指定 box
$ vagrant box repackage box-name	# 重新打包本地指定 box

Vagrant 基本命令

# 在空文件夹初始化虚拟机
$ vagrant init [box-name]

# 在初始化完的文件夹内启动虚拟机
$ vagrant up

# ssh登录启动的虚拟机
$ vagrant ssh

# 挂起本地启动的虚拟机
$ vagrant suspend

# 恢复本地启动的虚拟机
$ vagrant suspend

# 重启虚拟机
$ vagrant reload

# 关闭虚拟机
$ vagrant halt

# 查找虚拟机的运行状态
$ vagrant status

# 销毁当前虚拟机
$ vagrant destroy

Vagrant 使用

1.vagrant centos7 创建 root用户并使用ssh连接

# 先使用ragrant 用户登录
config.ssh.username = "vagrant"

# 按照提示设置 root 密码
$ sudo passwd root

# 切换至root用户
$ su root  

2.vagrant centos7 安装 node.js

# 切换到 root, epel 是社区强烈打造的免费开源发行软件包版本库
$ yum install epel-release

# 新版的nodejs已集成了npm
$ yum install nodejs

Other Resources

Vagrant Documentation

VAGRANT 中文文档

征服诱人的Vagrant!

vagrant ubuntu 创建 root用户并使用ssh连接

Vagrant (二) - 日常操作

Sketch 插件开发实践

sketch

Sketch 是非常流行的 UI 设计工具,2014年随着 Sketch V43 版本增加 Symbols 功能、开放开发者权限,吸引了大批开发者的关注。

目前 Sketch 开发有两大热门课题:① React 组件渲染成 sketch 由 airbnb 团队发起,② 使用 skpm 构建开发 Sketch 插件。

Sketch 插件开发相关资料较少且不太完善,我们开发插件过程中可以重点参考官方文档,只是有些陈旧。官方有提供 JavaScript API 借助 CocoaScript bridge 访问内部 Sketch API 和 macOS 框架进行开发插件(Sketch 53~56 版 JS API 在 native MacOS 和 Sketch API 暴露的特殊环境中运行),提供的底层 API 功能有些薄弱,更深入的就需要了解掌握 Objective-CCocoaScriptAppKitSketch-Headers

Sketch 插件结构

Sketch Plugin 是一个或多个 scripts 的集合,每个 script 定义一个或多个 commands。Sketch Plugin 是以 .sketchplugin 扩展名的文件夹,包含文件和子文件夹。严格来说,Plugin 实际上是 OS X package,用作为 OS X bundle

Bundle 具有标准化分层结构的目录,其保存可执行代码和该代码使用的资源。

Plugin Bundle 文件夹结构

Bundles 包含一个 manifest.json 文件,一个或多个 scripts 文件(包含用 CocoaScript 或 JavaScript 编写的脚本),它实现了 Plugins 菜单中显示的命令,以及任意数量的共享库脚本和资源文件。

mrwalker.sketchplugin
  Contents/
    Sketch/
      manifest.json
      shared.js
      Select Circles.cocoascript
      Select Rectangles.cocoascript
    Resources/
      Screenshot.png
      Icon.png

最关键的文件是 manifest.json 文件,提供有关插件的信息。

小贴士:

Sketch 插件包可以使用 skpm 在构建过程中生成,skpm 提供 Sketch 官方插件模版:

💁 Tip: Any Github repo with a 'template' folder can be used as a custom template:

skpm create <project-name> --template=<username>/<repository>

Manifest

manifest.json 文件提供有关插件的信息,例如作者,描述,图标、从何处获取最新更新、定义的命令 (commands)、调用菜单项 (menu) 以及资源的元数据。

{
  "name": "Select Shapes",
  "description": "Plugins to select and deselect shapes",
  "author": "Joe Bloggs",
  "homepage": "https://github.com/example/sketchplugins",
  "version": "1.0",
  "identifier": "com.example.sketch.shape-plugins",
  "appcast": "https://excellent.sketchplugin.com/excellent-plugin-appcast.xml",
  "compatibleVersion": "3",
  "bundleVersion": 1,
  "commands": [
    {
      "name": "All",
      "identifier": "all",
      "shortcut": "ctrl shift a",
      "script": "shared.js",
      "handler": "selectAll"
    },
    {
      "name": "Circles",
      "identifier": "circles",
      "script": "Select Circles.cocoascript"
    },
    {
      "name": "Rectangles",
      "identifier": "rectangles",
      "script": "Select Rectangles.cocoascript"
    }
  ],
  "menu": {
    "items": ["all", "circles", "rectangles"]
  }
}

Commands

声明一组 command 的信息,每个 command 以 Dictionary 数据结构形式存在。

  • script : 实现命令功能的函数所在的脚本
  • handler : 函数名,该函数实现命令的功能。Sketch 在调用该函数时,会传入 context 上下文参数。若未指定 handler,Sketch 会默认调用对应 script 中 onRun 函数
  • shortcut:命令的快捷键
  • name:显示在 Sketch Plugin 菜单中
  • identifier : 唯一标识,建议用 com.xxxx.xxx 格式,不要过长

Menu

Sketch 加载插件会根据指定的信息,在菜单栏中有序显示命令名。

在了解了 Sketch 插件结构之后,我们再来了解一下,sketch提供的官方 API: Actions API, Javascript API。

Sketch Actions API

Sketch Actions API 用于监听用户操作行为而触发事件,例如 OpenDocumen(打开文档)、CloseDocument(关闭文档)、Shutdown(关闭插件)、TextChanged(文本变化)等,具体详见官网:https://developer.sketch.com/reference/action/

  • register Actions

manifest.json 文件,配置相应 handlers。

示例:当 OpenDocument 事件被触发时调用 onOpenDocument handler 。

"commands" : [
  ...
  {
    "script" : "my-action-listener.js",
    "name" : "My Action Listener",
    "handlers" : {
      "actions": {
        "OpenDocument": "onOpenDocument"
      }
    },
    "identifier" : "my-action-listener-identifier"
  }
  ...
],

**my-action-listener.js **

export function onOpenDocument(context) {  	  		
  context.actionContext.document.showMessage('Document Opened')
}
  • Action Context

Action 事件触发时会将 context.actionContext 传递给相应 handler。注意有些 Action 包含两个状态beginfinish,例如 SelectionChanged,需分别订阅 SelectionChanged.beginSelectionChanged.finish,否则会触发两次事件。

Sketch JS API

Sketch 插件开发大概有如下三种方式:① 纯使用 CocoaScript 脚本进行开发,② 通过 Javascript + CocoaScript 的混合开发模式, ③ 通过 AppKit + Objective-C 进行开发。Sketch 官方建议使用 JavaScript API 编写 Sketch 插件,且官方针对 Sketch Native API 封装了一套 JS API,目前还未涵盖所有场景, 若需要更丰富的底层 API 需结合 CocoaScript 进行实现。通过 JS API 可以很方便的对 Sketch 中 DocumentArtboardGroupLayer 进行相关操作以及导入导出等,可能需要考虑兼容性, JS API 原理图如下:

api-reference

CocoaScript

CocoaScript 实现 JavaScript 运行环境到 Objective-C 运行时的桥接功能,可通过桥接器编写 JavaScript 外部脚本访问内部 Sketch API 和 macOS 框架底层丰富的 API 功能。

小贴士:

Mocha 实现提供 JavaScript 运行环境到 Objective-C 运行时的桥接功能已包含在CocoaScript中。

CocoaScript 建立在 Apple 的 JavaScriptCore 之上,而 JavaScriptCore 是为 Safari 提供支持的 JavaScript 引擎,使用 CocoaScript 编写代码实际上就是在编写 JavaScript。CocoaScript 包括桥接器,可以从 JavaScript 访问 Apple 的 Cocoa 框架。

借助 CocoaScript 使用 JavaScript 调 Objective-C 语法:

  • 方法调用用 ‘.’ 语法
  • Objective-C 属性设置
    • Getter: object.name()
    • Setter: object.setName('Sketch')object.name='sketch'
  • 参数都放在 ‘ ( ) ’ 里
  • Objective-C 中 ' : '(参数与函数名分割符) 转换为 ' _ ',最后一个下划线是可选的
  • 返回值,JavaScript 统一用 var/const/let 设置类型

注意:详细 Objective-C to JavaScript 请参考 Mocha 文档

示例:

// oc: MSPlugin 的接口 valueForKey:onLayer:
NSString * value = [command valueForKey:kAutoresizingMask onLayer:currentLayer];

// cocoascript:
const value = command.valueForKey_onLayer(kAutoresizingMask, currentLayer);

// oc:
const app = [NSApplication sharedApplication];
[app displayDialog:msg withTitle:title];

// cocoascript:
const app = NSApplication.sharedApplication();
app.displayDialog_withTitle(msg, title)

// oc:
const openPanel = [NSOpenPanel openPanel]
[openPanel setTitle: "Choose a location…"]
[openPanel setPrompt: "Export"];

// cocoascript:
const openPanel = NSOpenPanel.openPanel
openPanel.setTitle("Choose a location…")
openPanel.setPrompt("Export")

Objective-C Classes

Sketch 插件系统可以完全访问应用程序的内部结构和 macOS 中的核心框架。Sketch 是用 Objective-C 构建的,其 Objective-C 类通过 Bridge (CocoaScript/mocha) 提供 Javascript API 调用,简单的了解 Sketch 暴露的相关类以及类方法,对我们开发插件非常有帮助。

使用 Bridge 定义的一些内省方法来访问以下信息:

String(context.document.class()) // MSDocument

const mocha = context.document.class().mocha()

mocha.properties() // array of MSDocument specific properties defined on a MSDocument instance
mocha.propertiesWithAncestors() // array of all the properties defined on a MSDocument instance

mocha.instanceMethods() // array of methods defined on a MSDocument instance
mocha.instanceMethodsWithAncestors()

mocha.classMethods() // array of methods defined on the MSDocument class
mocha.classMethodsWithAncestors()

mocha.protocols() // array of protocols the MSDocument class inherits from
mocha.protocolsWithAncestors()

Context

当输入插件定制的命令时,Sketch 会去寻找改命令对应的实现函数, 并传入 context 变量。context 包含以下变量:

  • command: MSPluginCommand 对象,当前执行命令
  • document: MSDocument 对象 ,当前文档
  • plugin: MSPluginBundle 对象,当前的插件 bundle,包含当前运行的脚本
  • scriptPath: NSString 当前执行脚本的绝对路径
  • scriptURL: 当前执行脚本的绝对路径,跟 **scriptPath **不同的是它是个 NSURL 对象
  • selection: 一个 NSArray 对象,包含了当前选择的所有图层。数组中的每一个元素都是 MSLayer 对象

小贴士:MS 打头类名为 Sketch 封装类如图层基类 MSLayer、文本层基类 MSTextLayer 、位图层基类 MSBitmapLayer,NS 打头为 AppKit 中含有的类

const app = NSApplication.sharedApplication()

function initContext(context) {
		context.document.showMessage('初始执行脚本')
		
    const doc = context.document
    const page = doc.currentPage()
    const artboards = page.artboards()
    const selectedArtboard = page.currentArtboard() // 当前被选择的画板
    
    const plugin = context.plugin
    const command = context.command
    const scriptPath = context.scriptPath
    const scriptURL = context.scriptURL
    const selection = context.selection // 被选择的图层
}

Sketch 插件开发上手

前面我们了解了许多 Sketch 插件开发知识,那接下来实际上手两个小例子: ① 创建辅助内容面板窗口, ② 侧边栏导航。为了方便开发,我们在开发前需先进行如下操作:

崩溃保护

当 Sketch 运行发生崩溃,它会停用所有插件以避免循环崩溃。对于使用者,每次崩溃重启后手动在菜单栏启用所需插件非常繁琐。因此可以通过如下命令禁用该特性。

defaults write com.bohemiancoding.sketch3 disableAutomaticSafeMode true

插件缓存

通过配置启用或禁用缓存机制:

defaults write com.bohemiancoding.sketch3 AlwaysReloadScript -bool YES

该方法对于某些场景并不适用,如设置 COScript.currentCOScript().setShouldKeepAround(true) 区块会保持常驻在内存,那么则需要通过 coscript.setShouldKeepAround(false) 进行释放。

WebView 调试

如果插件实现方案使用 WebView 做界面,可通过以下配置开启调试功能。

defaults write com.bohemiancoding.sketch3 WebKitDeveloperExtras -bool YES

创建辅助内容面板窗口

首先我们先熟悉一下 macOS 下的辅助内容面板, 如下图最左侧 NSPanel 样例, 它是有展示区域,可设置样式效果,左上角有可操作按钮的辅助窗口。

Sketch 中要创建如下内容面板,需要使用 macOS 下 AppKit 框架中 NSPanel 类,它是 NSWindow 的子类,用于创建辅助窗口。内容面板外观样式设置,可通过 NSPanel 类相关属性进行设置, 也可通过 AppKitNSVisualEffectView 类添加模糊的背景效果。内容区域则可通过 AppKitWKWebView 类,单开 webview 渲染网页内容展示。

console

  • 创建 Panel
const panelWidth = 80;
const panelHeight = 240;

// Create the panel and set its appearance
const panel = NSPanel.alloc().init();
panel.setFrame_display(NSMakeRect(0, 0, panelWidth, panelHeight), true);
panel.setStyleMask(NSTexturedBackgroundWindowMask | NSTitledWindowMask | NSClosableWindowMask | NSFullSizeContentViewWindowMask);
panel.setBackgroundColor(NSColor.whiteColor());

// Set the panel's title and title bar appearance
panel.title = "";
panel.titlebarAppearsTransparent = true;

// Center and focus the panel
panel.center();
panel.makeKeyAndOrderFront(null);
panel.setLevel(NSFloatingWindowLevel);

// Make the plugin's code stick around (since it's a floating panel)
COScript.currentCOScript().setShouldKeepAround(true);

// Hide the Minimize and Zoom button
panel.standardWindowButton(NSWindowMiniaturizeButton).setHidden(true);
panel.standardWindowButton(NSWindowZoomButton).setHidden(true);
  • Panel 添加模糊的背景
// Create the blurred background
const vibrancy = NSVisualEffectView.alloc().initWithFrame(NSMakeRect(0, 0, panelWidth, panelHeight));
vibrancy.setAppearance(NSAppearance.appearanceNamed(NSAppearanceNameVibrantLight));
vibrancy.setBlendingMode(NSVisualEffectBlendingModeBehindWindow);

// Add it to the panel
panel.contentView().addSubview(vibrancy);
  • Panel 插入 webview 渲染
  const wkwebviewConfig = WKWebViewConfiguration.alloc().init()
  const webView = WKWebView.alloc().initWithFrame_configuration(
    CGRectMake(0, 0, panelWidth, panelWidth),
    wkwebviewConfig
  )
  
  // Add it to the panel
  panel.contentView().addSubview(webView);
  
  // load file URL
  webview.loadFileURL_allowingReadAccessToURL(
    NSURL.URLWithString(url),
    NSURL.URLWithString('file:///')
  )

侧边栏导航开发

我们开发复杂的 Sketch 插件,一般都要开发侧边栏导航展示插件功能按钮,点击触发相关操作。那开发侧边栏导航,我们主要使用 AppKit 中的那些类呢,有 NSStackViewNSBoxNSImageNSImageViewNSButton 等,大致核心代码如下:

  // create toolbar
  const toolbar = NSStackView.alloc().initWithFrame(NSMakeRect(0, 0, 40, 400))
  threadDictionary[SidePanelIdentifier] = toolbar
  toolbar.identifier = SidePanelIdentifier
  toolbar.setSpacing(8)
  toolbar.setFlipped(true)
  toolbar.setBackgroundColor(NSColor.windowBackgroundColor())
  toolbar.orientation = 1
	
  // add element
  toolbar.addView_inGravity(createImageView(NSMakeRect(0, 0, 40, 22), 'transparent', NSMakeSize(40, 22)), 1)
  const Logo = createImageView(NSMakeRect(0, 0, 40, 30), 'logo', NSMakeSize(40, 28))
  toolbar.addSubview(Logo)

  const contentView = context.document.documentWindow().contentView()
  const stageView = contentView.subviews().objectAtIndex(0)

  const views = stageView.subviews()
  const existId = views.find(d => ''.concat(d.identifier()) === identifier)

  const finalViews = []

  for (let i = 0; i < views.count(); i++) {
    const view = views[i]
    if (existId) {
      if (''.concat(view.identifier()) !== identifier) finalViews.push(view)
    } else {
      finalViews.push(view)
      if (''.concat(view.identifier()) === 'view_canvas') {
        finalViews.push(toolbar)
      }
    }
  }

	// add to main Window
  stageView.subviews = finalViews
  stageView.adjustSubviews()

详细见开源代码: https://github.com/o2team/sketch-plugin-boilerplate (欢迎 star 交流)

调试

当插件运行时,Sketch 将会创建一个与其关联的 JavaScript 上下文,可以使用 Safari 来调试该上下文。

在 Safari 中, 打开 Developer > 你的机器名称 > Automatically Show Web Inspector for JSContexts,同时启用选项 Automatically Pause Connecting to JSContext,否则检查器将在可以交互之前关闭(当脚本运行完时上下文会被销毁)。

现在就可以在代码中使用断点了,也可以在运行时检查变量的值等等。

日志

JavaScriptCore 运行 Sketch 插件的环境 也有提供类似调试 JavaScript 代码打 log 的方式,我们可以在关键步骤处放入一堆 console.log/console.error 等进行落点日志查看。

有以下几种选择可以查看日志:

  • 打开 Console.app 并查找 Sketch 日志
  • 查看 ~/Library/Logs/com.bohemiancoding.sketch3/Plugin Output.log 文件
  • 运行 skpm log 命令,该命令可以输出上面的文件(执行 skpm log -f 可以流式地输出日志)
  • 使用 skpm 开发的插件,安装 sketch-dev-tools,使用 console.log 打日志查看。

console

SketchTool

SketchTool 包含在 Sketch 中的 CLI 工具,通过 SketchTool 可对 Sketch 文档执行相关操作:

sketchtool 二进制文件位于 Sketch 应用程序包中:

Sketch.app/Contents/Resources/sketchtool/bin/sketchtool

设置 alias

alias sketchtool="/Applications/Sketch.app/Contents/Resources/sketchtool/bin/sketchtool"

使用:

sketchtool -h  # 查看帮助
sketchtool export artboards path/to/document.sketch  # 导出画板
sketchtool dump path/to/document.sketch # 导出 Sketch 文档 JSON data
sketchtool metadata path/to/document.sketch # 查看 Sketch 文档元数据
sketchtool run [Plugin path] # 运行插件

注意:SketchTool 需要 OSX 10.11或更高版本。

Other Resources

sketch Plugin 开发官方文档

sketch插件开发中文文档

sketch 使用文档

sketch-utils

sketch reference api

Github SketchAPI

react-sketchapp

Sketch-Plugins-Cookbook

iOS开发60分钟入门

AppKit, 构建 Sketch 的一个主要 Apple 框架

Foundation(基础), 更重要的 Apple 课程和服务

Chromeless-window

CommonJS, AMD, CMD 和 原生 JS 一些感悟

CommonJS, AMD, CMD 和 原生 JS 一些感悟

模块标准

CommonJS

Node应用由模块组成,采用CommonJS模块规范。

根据这个规范,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

如果想要对外提供接口的话,可以将接口绑定到 exports (即 module.exports) 上,推荐使用 module.exports(一个页面代表一个模块)。

CommandJS写法示例:

//先创建一个 check_commonjs.js 的文件
var flag = true;

function check(){
    return flag;
}

module.exports = check


//在我们需要用到的页面加载模块

var check = require('./check_commonjs');

check()

CommonJS是用在Node应用中的模块规范,在做web开发中并不被前端开发人员所追逐,由于Node作为服务端应用,加载一个文件,速度就是真的是可以忽略不计的,然而浏览器作为一个客户端,在这个大框框下面,想要加载完一个js文件,再执行下面的js语句,加载时间速度真没那么快,所以就有了我们常用的AMD和CMD

AMD

随着RequireJS成为最流行的实现方式,异步模块规范(AMD)在前端界已经被广泛认同。

RequireJS的例子:

// 先创建一个 check_amd.js 的文件

define(['check'], function(){
    var flag = true;
    function check(){
        return flag;
    }

    return {
        check: check
    };
});

// 在我们需要用到的页面加载模块

require(['check_amd'], function (check){
    if(check.check()){
        console.log("哈哈哈");
    }
});

从代码的整洁性和可读性来讲, CommonJS 要好很多, 但AMD定义下的RequireJS 解决了上述同步加载文件导致的问题

CMD

CMD 通用模块定义最常见的应用例子就是SeaJS, 有些人把RequireJS 与 SeaJS做比较的时候, 会简单的认为异步与同步的区别
这是不太对, web端加载文件的时候一定是异步的

// 先创建一个 check_cmd.js 的文件

define(function(require, exports, module) {
    var a = require('a');//这里就不举例再创建a文件了
    function check(){
       return a.flag;
    }
    exports.check = check;
});

// 在我们需要用到的页面加载模块
seajs.use(['check_cmd.js'], function(check){
    if(check.check()){
        console.log("哈哈哈");
    }
});

CMD与AMD比较可以发现,AMD用户体验好由于没有延迟,依赖模块提前执行了,CMD性能好,因为只有用户需要的时候才执行。

在我们使用过程中,发现其实RequireJS和Sea.js在资源加载的时间点都是一样的,所以论“懒”的程度都是一样的。差别仅仅在于加载的脚本什么时候执行。RequireJS的依赖模块在回调函数执行前执行完毕,而Sea.js的依赖模块在回调函数执行require时执行。

ps: Sea.js原理
1.seajs中通过回调函数的Function.toString函数(真实使用parseDependencies(factory.toString())),使用正则表达式来捕捉内部的require字段,找到require('xx')内部依赖的模块xx
2.根据配置文件,找到jquery的js文件的实际路径
3.在dom中插入script标签,载入模块指定的js,绑定加载完成的事件,使得加载完成后将js文件绑定到require模块指定的id(这里就是jquery这个字符串)上回调函数内部依赖的js全部加载(暂不调用)完后,调用回调函数

兼容AMD、CMD、CommonJS,同时还支持老式的“全局”变量规范:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define([], factory);
    } else if (typeof define === 'function' && define.cmd) {
        define(function(require, exports, module){
            module.exports = factory()
        });
    } else if (typeof exports === 'object') {
        module.exports = factory();
    } else {
        root.LocalStorage = factory();
    }
}(this, function () {
    //    方法
    function myFunc(){};
 
 	myFunc.prototype = {
 		constructor: "myFunc"
 	}

    //    暴露公共方法
    return myFunc;	
})

CommonJS规范
seajs
requirejs
LABjs、RequireJS、SeaJS 哪个最好用?为什么?

JavaScript 中 for in 循环和数组的问题

for...in 语句以任意顺序遍历一个对象的可枚举(enumerable)属性。对于每个不同的属性,语句都会被执行。

for...in 遍历对象:

let tMinus = {
    two: "Two",
    one: "One",
    zero: "zero"
};
 
let countdown = "";
 
for (let step in tMinus) {
    countdown += tMinus[step] + " ";
}
 
console.log(countdown);  // Two  One  zero

因为for…in循环支持所有的JavaScript对象,所以它同样可用于数组对象之中:

let tMinus = [
    "Two",
    "One",
    "zero"
];
 
let countdown = "";
 
for (let step in tMinus) {
    countdown += tMinus[step] + " ";
}
 
console.log(countdown);   // Two  One  zero

然而,以这样的方式遍历数组存在部分问题。

为内置原型添加属性/方法,for in时也是可遍历

Array.prototype.voice = "voice";
 
let tMinus = [
    "Two",
    "One",
    "zero"
];
 
let countdown = "";
 
for (let step in tMinus) {
    countdown += tMinus[step] + " ";
}
 
console.log(countdown);  // Two  One  zero  voice

可借助getOwnPropertyNames() 或执行 hasOwnProperty() 函数来避免这一问题

Array.prototype.voice = "voice";
 
let tMinus = [
    "Two",
    "One",
    "zero"
];
 
let countdown = "";
 
for (let step in tMinus) {
	if( tMinus.hasOwnProperty(step) ){
		countdown += tMinus[step] + " ";
	}
}
console.log(countdown);  // Two  One  zero 

迭代的顺序是依赖于执行环境的,数组遍历不一定按次序访问元素

可能出现这样的结果

console.log(countdown);  //One  zero  Two

向数组变量添加额外的属性,会导致不可预知的结果

let tMinus = [
    "Two",
    "One",
    "zero"
];
 
tMinus.vioce = "vioce";
 
let countdown = "";
 
for (let step in tMinus) {
    if (tMinus.hasOwnProperty(step)) {
        countdown += tMinus[step] + " ";
    }
}
 
console.log(countdown);  // Two  One  zero  vioce

由此可见,当你需要遍历数组元素的时候,应使用for循环或者数组对象的内置迭代函数(如forEach、map等),而不是for…in循环。

javascript检测对象中是否存在某个属性

检测对象中属性的存在与否可以通过几种方法来判断。

1.使用in关键字

该方法可以判断对象的自有属性和继承来的属性是否存在。

let obj = { x: 1 }
"x" in obj;            //true,自有属性存在
"y" in obj;            //false
"toString" in obj;     //true,是一个继承属性

2.使用hasOwnProperty()方法

该方法只能判断自有属性是否存在,对于继承属性会返回false。

let obj = { x: 1 }

obj.hasOwnProperty("x")   //true,自有属性中有x
obj.hasOwnProperty("y")	  //false,自有属性中不存在y
obj.hasOwnProperty("toString")   //false,这是一个继承属性,但不是自有属性

vue2.0 render函数介绍中 Array.apply(null, { length: 20 }) 引起思考

Function.prototype.apply()

apply() 方法在指定 this 值和参数(参数以数组或类数组对象的形式存在)的情况下调用某个函数。

注意:该函数的语法与 call() 方法的语法几乎完全相同,唯一的区别在于,call()方法接受的是一个参数列表,而 apply()方法接受的是一个包含多个参数的数组(或类数组对象)。
语法

fun.apply(thisArg[, argsArray])

参数

thisArg
在 fun 函数运行时指定的 this 值。需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。

function fun() {
    alert(this);
}

//非严格模式下
fun.call(null); // window or global
fun.call(undefined); // window or global

ES5严格模式(ie6/7/8/9除外)则不再揣测,给call/apply传入的任何参数不再转换:

'use strict'
function fun() {
    alert(this);
}
fun.call(null)      // null
fun.call(undefined) // undefined

argsArray
一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 fun 函数。如果该参数的值为null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。

ps: 类似 Array.call()、Array.slice.call()却别,Array.call()是执行Array构造函数, Array.slice.call()执行Array对象的slice方法

类数组

一个类数组对象:

具有:指向对象元素的数字索引下标以及 length 属性告诉我们对象的元素个数
不具有:诸如 push 、 forEach 以及 indexOf 等数组对象具有的方法

ps:这是我参考的定义,实际上,只要有length属性,且它的属性值为number类型就行了。请围观评论。

类数组示例:

DOM方法 document.getElementsByClassName() 

var a = {'1':'gg','2':'love','4':'meimei',length:5};
Array.prototype.join.call(a,'+');//'+gg+love++meimei'

非类数组示例:

var c = {'1':2};

没有length属性,所以就不是类数组。

javascript中常见的类数组有arguments对象和DOM方法的返回结果。
比如 document.getElementsByTagName()。

类数组判断

function isArrayLike(o) {
    if (o &&                                
        typeof o === 'object' &&            
        isFinite(o.length) &&               
        o.length >= 0 &&                    
        o.length===Math.floor(o.length) &&  
        o.length < 4294967296)              
        return true;                        
    else
        return false;                     
}

类数组对象转化为数组

Array.prototype.slice.call(arguments)

JavaScript 的怪癖 8:“类数组对象”
如何理解和熟练运用js中的call及apply?

Service Worker理解使用

什么是 Service Worker

W3C 组织早在 2014 年 5 月就提出过 Service Worker 这样的一个 HTML5 API ,主要用来做持久的离线缓存。

浏览器中的 javaScript 都是运行在一个单一主线程上的,在同一时间内只能做一件事情。随着 Web 业务不断复杂,我们逐渐在 js 中加了很多耗资源、耗时间的复杂运算过程,这些过程导致的性能问题在 WebApp 的复杂化过程中更加凸显出来。

Service Worker 是在新开 Web Worker进程脱离在主线程之外, 将缓存拦截处理完成后通过 postMessage 方法告诉主线程,而主线程通过 onMessage 方法得到 Web Worker 的结果反馈。Service Worker 在 Web Worker 的基础上加上了持久离线缓存能力。

Service Worker 之前也有在 HTML5 上做离线缓存的 API 叫 AppCache, 但存在很多缺点。W3C 决定 AppCache 仍然保留在 HTML 5.0 Recommendation 中,在 HTML 后续版本中移除。
Issue: https://github.com/w3c/html/issues/40open_in_new
Mailing list: https://lists.w3.org/Archives/Public/public-html/2016May/0005.htmlopen_in_new

Service Worker 功能和特性

Service Worker 的伟大使命,就是让缓存做到优雅和极致,让 Web App 相对于 Native App 的缺点更加弱化,也为开发者提供了对性能和体验的无限遐想,其含有很多特性和功能点

  • 一个独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context。
  • 一旦被 install,就永远存在,除非被 uninstall
  • 需要的时候可以直接唤醒,不需要的时候自动睡眠(有效利用资源,此处有坑)
  • 可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态)
  • 离线内容开发者可控
  • 能向客户端推送消息
  • 不能直接操作 DOM
  • 出于安全的考虑,必须在 HTTPS 环境下才能工作(允许在开发调试的 localhost 使用)
  • 异步实现,内部大都是通过 Promise 实现

Service Worker 浏览器支持情况

Service Worker 浏览器支持情况怎么样呢?参考 Can I use 可得下图:
Service Worker 浏览器支持情况

前支持的浏览器不多,而且支持的浏览器也是在试验阶段,Chrome、Firefox、Opera 支持性较好,同时 x5 andriod 支持 Service Worker

Service Worker 使用限制

if('serviceWorker' in navigator) {
  navigator.serviceWorker.register(scriptURL, options)
  .then(registration => {
    console.error('registration', registration)
  })
  .catch(err => {
    console.error('catch', err)
  })
}

Service Worker 除了 work 线程的限制外,由于可拦截页面请求,为了保证页面安全,浏览器端对 Service Worker 的使用限制也不少。

1)service worker 脚本的 URL, 不支持跨域,不允许缓存 service worker 脚本如 service-worker.js。

2)service worker 脚本的 URL, 不支持 Blob/String URLCreate service worker from Blob/String URL

3)无法直接操作 DOM 对象,也无法访问 window、document、parent 对象。可以访问 location、navigator;

4)可代理的页面作用域限制 (scope) 。默认是 service-worker.js 所在文件目录及子目录的请求可代理,可在注册时手动设置作用域范围;

5)必须在 https 中使用,允许在开发调试的 localhost 使用。

Service Worker 生命周期

Service Worker 的工作原理是基于注册、安装、激活等步骤在浏览器 js 主线程中独立分担缓存任务的,那么我们如何在这些 API 自身一系列的操作中进行一些我们自己想让 worker 干的事情呢?

这里我们需要了解一下 Service Worker 的生命周期的概念,这有利于我们学会在各个生命周期的阶段进行有目的性的回调,让我们自定义的工作在 Service Worker 中正确有效的开展下去。MDN 给出了详细的 Service Worker 生命周期图:

Service Worker 生命周期

我们可以看到生命周期分为这么几个状态 安装中, 安装后, 激活中, 激活后, 废弃

  • 安装( installing ):这个状态发生在 Service Worker 注册之后,表示开始安装,触发 install 事件回调指定一些静态资源进行离线缓存。
    install 事件回调中有两个方法:

  • event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。

  • self.skipWaiting():self 是当前 context 的 global 变量,执行该方法表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态。

  • 安装后( installed ):Service Worker 已经完成了安装,并且等待其他的 Service Worker 线程被关闭。

  • 激活( activating ):在这个状态下没有被其他的 Service Worker 控制的客户端,允许当前的 worker 完成安装,并且清除了其他的 worker 以及关联缓存的旧缓存资源,等待新的 Service Worker 线程被激活。

Service Worker 文件

对于浏览器来说,Service Worker 是一个独立于 js 主线程的一种 Web Worker 线程,一个独立于主线程的 Context,但是面向开发者来说 Service Worker 的形态其实就是一个需要开发者自己维护的文件,我们假设这个文件叫做 service-worker.js,此文件的内容就是定制 Service Worker 生命周期中每个阶段所处理的定制化的细节逻辑,比如缓存 Cache 的读写,更新的策略,推送的策略等等,通常 service-worker.js 文件是处于项目的根目录,并且需要保证能直接通过 https: //yourhost/service-worker.js 这种形式直接被访问到才行。

service-worker.js基本结构代码,如下:

// 安装
self.addEventListener('install', function (e) {
    // 缓存 App Shell 等关键静态资源和 html (保证能缓存的内容能在离线状态跑起来)
});

// 激活
self.addEventListener('activate', function (e) {
    // 激活的状态,这里就做一做老的缓存的清理工作
});

// 缓存请求和返回(这是个简单的缓存优先的例子)
self.addEventListener('fetch', function (e) {
    e.respondWith(caches.match(e.request)
        .then(function (response) {
            if (response) {
                return response;
            }
            // fetchAndCache 方法并不存在,需要自己定义,这里只是示意代码
            return fetchAndCache(e.request);
        })
    );
});

快速注册 Service Worker

注册 Service Worker 还是蛮简单的,只要小段代码。只要在工程中的 html 文档的 <script> 标签里或者随便在页面的哪个 javaScript 模块中添加如下代码即可

navigator.serviceWorker && navigator.serviceWorker.register('/service-worker.js', {scope: '/'})
	.then( registration => {
		// 注册成功
		console.log('ServiceWorker registration successful with scope: ', registration.scope);
	})
	.catch( err => {
		// 注册失败:(
		console.log('ServiceWorker registration failed: ', err);
	})

sw-register-webpack-plugin 与 sw-precache-webpack-plugin

无论是 Service Worker 作用域问题,还是 Service Worker 的更新问题,都与 Service Worker 的注册息息相关,一个看似简单的 Service Worker 的注册还是有很多地方需要注意,但是如果这些都需要在每个项目中都要自己完全实现一遍,还是非常繁琐的。而sw-precache-webpack-pluginsw-register-webpack-plugin 作为一个 Webpack Plugin 很好的帮助我们解决了优雅的注册 Service Worker 的问题

参考资料:

workbox

Service Worker 简介

如何优雅的为 PWA 注册 Service Worker

Service Worker最佳实践

Progressive Web App 的离线存储

Service Workers 与离线缓存

PWA与service worker工作原理探析

service worker 实现离线缓存

使用service worker对静态资源进行全缓存

PWA 应用实战

node-gyp 知识来龙去脉

在我们写node addon时,需要使用node-gyp命令行工具,大部分同学会用configue生成配置文件,然后使用build进行构建。但是node-gyp到底是什么?底层有什么呢?下面我们来刨根问底。

本文的线索是自底向上的讲解node-gyp的各层次依赖,主要有以下几个部分:

1. make
2. make install
3. cmake
4. gyp
5. node-gyp

层次结构如下图所示:

img

make

从源文件到可执行文件叫做编译(包括预编译、编译、链接),而make作为构建工具掌握着编译的过程,也就是如何去编译、文件编译的顺序等。

make是最常用的构建工具,针对用户制定的构建规则(makefile)去执行响应的任务。make会根据构建规则去查找依赖,决定编译顺序等。大致了解可参考Make 命令教程

Makefile(makefile)中定义了make的构建规则,当然也可以自己指定规则文件。例如:

$ make -f rules.txt
# 或者
$ make --file=rules.txt

Makefile由一条条的规则组成,每条规则由target(目标)、source(前置条件/依赖)、command(指令)三者组成。

形式如下:

<target> : <prerequisites> 
[tab]  <commands>

make target时,主要做了以下几件事:

1.检查目标是否存在
2.如果不存在目标
	· 检查目标的依赖是否存在
	· 不存在则调用`make source`;存在并且没有变化(修改时间戳小于target),不操作
	· 执行target中的command指令
2.如果存在目标
	· 检查依赖是否发生变化
	· 没有变化则不需要执行,有变化则执行`make source`后执行command

以编译一个C++文件的规则为例:

hellomake: hellomake.c hellofunc.c
     gcc -o hellomake hellomake.c hellofunc.c -I.

当我们执行make hellomake,会使用gcc编译器编译产出hellomake。如果make不带有参数,则执行makefile中的第一条指令。

make也允许我们定义一些纯指令(伪指令)去执行一些操作,相当于把上面的target写成指令名称,只不过在command中不生成文件,所以每次执行该规则时都会执行command。为了和真实的目标文件做区分,make中使用了.PHONY关键字,关键字.PHONY可以解决这问题,告诉make该目标是“假的”(磁盘上其实没有这个目标文件)。例如

.PHONY: clean
clean:
        rm *.o temp

由于makefile目标只能写一个,所以我们可以使用all来将多个目标组合起来。例如:

all: executable1 executable2

一般情况下可以把all放在makefile的第一行,这样不带参数执行make就会找到all。

make install

make install用来安装文件,它从Makefile中读取指令,安装到系统目录中。

cmake

上面提到了make,似乎已经够了,如果我是一个开发者,我定义了makefile,让使用者执行make编译就好了。但是不同平台的编译器、动态链接库的路径都有可能不同,如果想让你的软件能够跨平台编译、运行,必须要保证能够在不同平台编译。如果使用上面的Make工具,就得为每一种标准写一次Makefile,这是很繁琐并且容易出错的地方。

cmake的出现就是为了解决上述问题,它首先允许开发者编写一种平台无关的 CMakeList.txt文件来定制整个编译流程,cmake会根据操作系统选择不同编译器,当然也可以在CMakeList.txt中去指定,执行cmake时会目标用户的平台和自定义的配置生成所需的Makefile或工程文件,如Unix的Makefile、Windows的Visual Studio。

CMake是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的makefile或者project文件,能测试编译器所支持的C++特性,类似UNIX下的automake。

在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:

1.编写 CMake 配置文件 CMakeLists.txt 。
2.执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile。其中,PATH是CMakeLists.txt 所在的目录。
3.使用 make 命令进行编译。

CMakeList.txt中由面向过程的一条条指令组成,例如:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (Demo3)
# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
# 添加 math 子目录
add_subdirectory(math)
# 指定生成目标 
add_executable(Demo main.cc)
# 添加链接库
target_link_libraries(Demo MathFunctions)

具体可参考cmake文档

GYP

Gyp是一个类似CMake的项目生成工具, 用于管理你的源代码, 在google code主页上唯一的一句slogan是”GYP can Generate Your Projects.”。GYP是由 Chromium 团队开发的跨平台自动化项目构建工具,Chromium便是通过GYP进行项目构建管理。

首先看GYP与cmake类似,那为什要有GYP呢?GYP和cmake有哪些相同点、不同点呢?

GYP vs cmake

相同点:

支持跨平台项目工程文件输出,Windows 平台默认是 Visual Studio,Linux 平台默认是 Makefile,Mac 平台默认是 Xcode,这个功能 CMake 也同样支持,只是缺少了 Xcode。

不同点:

配置文件形式不同,GYP的配置文件更像一个“配置文件”,而Cmake的上述所言更像一个面向过程的一个脚本,也就是说在项目设置的层次上进行抽象;同时GYP支持交叉编译。

具体比较可参考GYP vs. CMake

GYP配置

GYP的配置文件以.gyp结尾,一个典型的.gyp文件如下所示:

{
    'variables': {
      .
      .
      .
    },
    'includes': [
      '../build/common.gypi',
    ],
    'target_defaults': {
      .
      .
      .
    },
    'targets': [
      {
        'target_name': 'target_1',
          .
          .
          .
      },
      {
        'target_name': 'target_2',
          .
          .
          .
      },
    ],
    'conditions': [
      ['OS=="linux"', {
        'targets': [
          {
            'target_name': 'linux_target_3',
              .
              .
              .
          },
        ],
      }],
      ['OS=="win"', {
        'targets': [
          {
            'target_name': 'windows_target_4',
              .
              .
              .
          },
        ],
      }, { # OS != "win"
        'targets': [
          {
            'target_name': 'non_windows_target_5',
              .
              .
              .
          },
      }],
    ],
  }

variables : 定义可以在文件其他地方访问的变量;

includes : 将要被引入到该文件中的文件列表,通常是以.gypi结尾的文件

target_defaults : 将作用域所有目标的默认配置;

targets: 构建的目标列表,每个target中包含构建此目标的所有配置;

conditions: 条件列表,会根据不同条件选择不同的配置项。在最顶级的配置中,通常是平台特定的目标配置。

具体可参考GYP文档

node-gyp

node-gyp是一个跨平台的命令行工具,目的是编译node addon模块。

常用的命令有configurebuildconfigure 原理就是利用gyp生成不同的编译配置文件,build则根据不同平台、不同构建配置进行编译。

configure

我们分步骤看下configure的代码:

findPython(python, function (err, found) {
    if (err) {
      callback(err)
    } else {
      python = found
      getNodeDir()
    }
})

由于GYP是python写的,所以这里首先找当前系统下的python,内部利用的是which这个第三方库。

function getNodeDir () {

    // 'python' should be set by now
    process.env.PYTHON = python

    if (gyp.opts.nodedir) {
      // --nodedir was specified. use that for the dev files
      nodeDir = gyp.opts.nodedir.replace(/^~/, osenv.home())

      log.verbose('get node dir', 'compiling against specified --nodedir dev files: %s', nodeDir)
      createBuildDir()

    } else {
      gyp.commands.install([ release.version ], function (err, version) {
        if (err) return callback(err)
        log.verbose('get node dir', 'target node version installed:', release.versionDir)
        nodeDir = path.resolve(gyp.devDir, release.versionDir)
        createBuildDir()
      })
    }
  }

找到node所在目录,如果没有,则下载node压缩包并解压。

function createBuildDir () {
    log.verbose('build dir', 'attempting to create "build" dir: %s', buildDir)
    mkdirp(buildDir, function (err, isNew) {
      if (err) return callback(err)
      log.verbose('build dir', '"build" dir needed to be created?', isNew)
      if (win && (!gyp.opts.msvs_version || gyp.opts.msvs_version === '2017')) {
        findVS2017(function (err, vsSetup) {
          if (err) {
            log.verbose('Not using VS2017:', err.message)
            createConfigFile()
          } else {
            createConfigFile(null, vsSetup)
          }
        })
      } else {
        createConfigFile()
      }
    })
  }

创建build目录,这里区分了是否有vs,查找vs的方法是打开powershell(windows),试图打开vs。

function createConfigFile (err, vsSetup) {
    if (err) return callback(err)

    var configFilename = 'config.gypi'
    var configPath = path.resolve(buildDir, configFilename)

    if (vsSetup) {
      // GYP doesn't (yet) have support for VS2017, so we force it to VS2015
      // to avoid pulling a floating patch that has not landed upstream.
      // Ref: https://chromium-review.googlesource.com/#/c/433540/
      gyp.opts.msvs_version = '2015'
      process.env['GYP_MSVS_VERSION'] = 2015
      process.env['GYP_MSVS_OVERRIDE_PATH'] = vsSetup.path
      defaults['msbuild_toolset'] = 'v141'
      defaults['msvs_windows_target_platform_version'] = vsSetup.sdk
      variables['msbuild_path'] = path.join(vsSetup.path, 'MSBuild', '15.0',
                                            'Bin', 'MSBuild.exe')
    }

    // loop through the rest of the opts and add the unknown ones as variables.
    // this allows for module-specific configure flags like:
    //
    //   $ node-gyp configure --shared-libxml2
    Object.keys(gyp.opts).forEach(function (opt) {
      if (opt === 'argv') return
      if (opt in gyp.configDefs) return
      variables[opt.replace(/-/g, '_')] = gyp.opts[opt]
    })

    configs.push(configPath)
    fs.writeFile(configPath, [prefix, json, ''].join('\n'), findConfigs)
}

这里创建config.gypi文件,主要包含target_defaultsvariables

// config = ['config.gypi']
  function runGyp (err) {
    if (err) return callback(err)

    if (!~argv.indexOf('-f') && !~argv.indexOf('--format')) {
      if (win) {
        log.verbose('gyp', 'gyp format was not specified; forcing "msvs"')
        // force the 'make' target for non-Windows
        argv.push('-f', 'msvs')
      } else {
        log.verbose('gyp', 'gyp format was not specified; forcing "make"')
        // force the 'make' target for non-Windows
        argv.push('-f', 'make')
      }
    }

    if (win && !hasMsvsVersion()) {
      if ('msvs_version' in gyp.opts) {
        argv.push('-G', 'msvs_version=' + gyp.opts.msvs_version)
      } else {
        argv.push('-G', 'msvs_version=auto')
      }
    }

    // include all the ".gypi" files that were found
    configs.forEach(function (config) {
      argv.push('-I', config)
    })

    // For AIX and z/OS we need to set up the path to the exports file
    // which contains the symbols needed for linking. 
    var node_exp_file = undefined
    if (process.platform === 'aix' || process.platform === 'os390') {
      var ext = process.platform === 'aix' ? 'exp' : 'x'
      var node_root_dir = findNodeDirectory()
      var candidates = undefined 
      if (process.platform === 'aix') {
        candidates = ['include/node/node',
                      'out/Release/node',
                      'out/Debug/node',
                      'node'
                     ].map(function(file) {
                       return file + '.' + ext
                     })
      } else {
        candidates = ['out/Release/obj.target/libnode',
                      'out/Debug/obj.target/libnode',
                      'lib/libnode'
                     ].map(function(file) {
                       return file + '.' + ext
                     })
      }
      var logprefix = 'find exports file'
      node_exp_file = findAccessibleSync(logprefix, node_root_dir, candidates)
      if (node_exp_file !== undefined) {
        log.verbose(logprefix, 'Found exports file: %s', node_exp_file)
      } else {
        var msg = msgFormat('Could not find node.%s file in %s', ext, node_root_dir)
        log.error(logprefix, 'Could not find exports file')
        return callback(new Error(msg))
      }
    }

    // this logic ported from the old `gyp_addon` python file
    var gyp_script = path.resolve(__dirname, '..', 'gyp', 'gyp_main.py')
    var addon_gypi = path.resolve(__dirname, '..', 'addon.gypi')
    var common_gypi = path.resolve(nodeDir, 'include/node/common.gypi')
    fs.stat(common_gypi, function (err, stat) {
      if (err)
        common_gypi = path.resolve(nodeDir, 'common.gypi')

      var output_dir = 'build'
      if (win) {
        // Windows expects an absolute path
        output_dir = buildDir
      }
      var nodeGypDir = path.resolve(__dirname, '..')
      var nodeLibFile = path.join(nodeDir,
        !gyp.opts.nodedir ? '<(target_arch)' : '$(Configuration)',
        release.name + '.lib')

      argv.push('-I', addon_gypi)
      argv.push('-I', common_gypi)
      argv.push('-Dlibrary=shared_library')
      argv.push('-Dvisibility=default')
      argv.push('-Dnode_root_dir=' + nodeDir)
      if (process.platform === 'aix' || process.platform === 'os390') {
        argv.push('-Dnode_exp_file=' + node_exp_file)
      }
      argv.push('-Dnode_gyp_dir=' + nodeGypDir)
      argv.push('-Dnode_lib_file=' + nodeLibFile)
      argv.push('-Dmodule_root_dir=' + process.cwd())
      argv.push('-Dnode_engine=' +
        (gyp.opts.node_engine || process.jsEngine || 'v8'))
      argv.push('--depth=.')
      argv.push('--no-parallel')

      // tell gyp to write the Makefile/Solution files into output_dir
      argv.push('--generator-output', output_dir)

      // tell make to write its output into the same dir
      argv.push('-Goutput_dir=.')

      // enforce use of the "binding.gyp" file
      argv.unshift('binding.gyp')

      // execute `gyp` from the current target nodedir
      argv.unshift(gyp_script)

      // make sure python uses files that came with this particular node package
      var pypath = [path.join(__dirname, '..', 'gyp', 'pylib')]
      if (process.env.PYTHONPATH) {
        pypath.push(process.env.PYTHONPATH)
      }
      process.env.PYTHONPATH = pypath.join(win ? ';' : ':')

      var cp = gyp.spawn(python, argv)
      cp.on('exit', onCpExit)
    })
}

这里主要是区分了不同平台,给GYP命令加入各种参数,其中-I代表include,最后执行gyp脚本生成构建配置文件,比如unix下生成makefile。

build

build比较简单,言简意赅就是就是区分不同平台,收集不同参数,利用不同编译工具进行编译。

command = win ? 'msbuild' : makeCommand

区分编译工具。

function loadConfigGypi () {
    fs.readFile(configPath, 'utf8', function (err, data) {
      if (err) {
        if (err.code == 'ENOENT') {
          callback(new Error('You must run `node-gyp configure` first!'))
        } else {
          callback(err)
        }
        return
      }
      config = JSON.parse(data.replace(/\#.+\n/, ''))

      // get the 'arch', 'buildType', and 'nodeDir' vars from the config
      buildType = config.target_defaults.default_configuration
      arch = config.variables.target_arch
      nodeDir = config.variables.nodedir

      if ('debug' in gyp.opts) {
        buildType = gyp.opts.debug ? 'Debug' : 'Release'
      }
      if (!buildType) {
        buildType = 'Release'
      }

      log.verbose('build type', buildType)
      log.verbose('architecture', arch)
      log.verbose('node dev dir', nodeDir)

      if (win) {
        findSolutionFile()
      } else {
        doWhich()
      }
    })
}

加载config.gypi,为构建收集一波参数。如果在windows下,收集build/*.sln

  function doBuild () {

    // Enable Verbose build
    var verbose = log.levels[log.level] <= log.levels.verbose
    if (!win && verbose) {
      argv.push('V=1')
    }
    if (win && !verbose) {
      argv.push('/clp:Verbosity=minimal')
    }

    if (win) {
      // Turn off the Microsoft logo on Windows
      argv.push('/nologo')
    }

    // Specify the build type, Release by default
    if (win) {
      var archLower = arch.toLowerCase()
      var p = archLower === 'x64' ? 'x64' :
              (archLower === 'arm' ? 'ARM' : 'Win32')
      argv.push('/p:Configuration=' + buildType + ';Platform=' + p)
      if (jobs) {
        var j = parseInt(jobs, 10)
        if (!isNaN(j) && j > 0) {
          argv.push('/m:' + j)
        } else if (jobs.toUpperCase() === 'MAX') {
          argv.push('/m:' + require('os').cpus().length)
        }
      }
    } else {
      argv.push('BUILDTYPE=' + buildType)
      // Invoke the Makefile in the 'build' dir.
      argv.push('-C')
      argv.push('build')
      if (jobs) {
        var j = parseInt(jobs, 10)
        if (!isNaN(j) && j > 0) {
          argv.push('--jobs')
          argv.push(j)
        } else if (jobs.toUpperCase() === 'MAX') {
          argv.push('--jobs')
          argv.push(require('os').cpus().length)
        }
      }
    }

    if (win) {
      // did the user specify their own .sln file?
      var hasSln = argv.some(function (arg) {
        return path.extname(arg) == '.sln'
      })
      if (!hasSln) {
        argv.unshift(gyp.opts.solution || guessedSolution)
      }
    }

    var proc = gyp.spawn(command, argv)
    proc.on('exit', onExit)
}

执行编译命令。

JavaScript的垃圾回收机制

前言

无论高级语言,还是低级语言。内存的管理都是:

  1. 内存分配:申明变量、函数、对象,系统会自动分配内存
  2. 内存使用:读写内存,使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存

释放内存处理方式,各种语言都有自己的垃圾回收(garbage collection, 简称GC)机制。做GC的第一步是判断堆中存的是数据还是指针,是指针的话,说明它被指向活跃的对象。有3种判断方法:

  1. Conservative:存储格式是地址,C/C++有用到这种算法。
  2. Compiler hints:对于静态语言,比如Java,编译器是知道它是不是指针的,所以可以用这种。
  3. Tagged pointersJavaScript用的是这种,在字末位进行标识,1为指针。

JavaScript 内存问题

内存泄漏

什么情况下会内存泄漏 memory leak ?可以这么理解,就是有些代码本来应该要被回收的,但是没有被回收,所以一直占用着操作系统的内存,从而越积越多。一般的内存泄漏其实无关紧要,可怕的是内存泄漏引起的堆积,导致GC一直没办法使用所占用的内存给其他程序使用。

内存溢出

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现 out of memory;比如申请了一个 integer, 但给它存了 long 才能存下的数,那就是内存溢出。

注意: memory leak 会最终会导致 out of memory

JavaScript 垃圾管理

JavaScript 有自动垃圾收集机制,找出不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。 在 JavaScript 中,最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,因此 a = null 其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。

  • 在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量,以确保性能问题。
  • 以 Google 的 V8 引擎为例,在 V8 引擎中所有的 JAVASCRIPT 对象都是通过堆来进行内存分配的。当我们在代码中声明变量并赋值时,V8 引擎就会在堆内存中分配一部分给这个变量。如果已申请的内存不足以存储这个变量时,V8 引擎就会继续申请内存,直到堆的大小达到了 V8 引擎的内存上限为止(默认情况下,V8引擎的堆内存的大小上限在 64 位系统中为 1464MB,在32位系统中则为 732MB)。
  • 另外,V8 引擎对堆内存中的 JAVASCRIPT 对象进行分代管理。新生代:新生代即存活周期较短的 JAVASCRIPT 对象,如临时变量、字符串等; 老生代:老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

javascript回收方法

V8 引擎中使用两种优化方法:
  1. 分代回收;
  2. 增量 GC
  3. 目的是通过对象的使用频率、存在时长区分新生代与老生代对象。多回收新生代区(young generation),少回收老生代区(tenured generation),减少每次需遍历的对象,从而减少每次 GC 的耗时。
  4. 把需要长耗时的遍历、回收操作拆分运行,减少中断时间,但是会增大上下文切换开销.
回收算法:

大部分垃圾回收语言用的算法称之为 Mark-and-sweep

(1) 引用计数 (reference counting)

在内存管理环境中,对象 A 如果有访问对象 B 的权限,叫做对象 A 引用对象 B。引用计数的策略是将“对象是否不再需要 ”简化成“ 对象有没有其他对象引用到它,如果没有对象引用这个对象,那么这个对象将会被回收。

let obj1 = { a: 1 }; // 一个对象(称之为 A)被创建,赋值给 obj1,A 的引用个数为 1 ,很显然引用次数不为0, 无法垃圾收集
let obj2 = obj1; // A 的引用个数变为 2

obj1 = 0; // A 的引用个数变为 1
obj2 = 0; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了

mozilla 文档 中很形象的一个例子:

var o = { 
  a: {
    b:2
  }
}; 
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集


var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1;      // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有

var oa = o2.a; // 引用“这个对象”的a属性
               // 现在,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
           // 但是它的属性a的对象还在被oa引用,所以还不能回收

oa = null; // a属性的那个对象现在也是零引用了
           // 它可以被垃圾回收了

当对象被引用次数为 0 时,就被回收。潜在的一个问题是:循环引用时,两个对象都至少被引用了一次,将不能自动被回收,导致内存泄露。

function func() {
    let obj1 = {};
    let obj2 = {};

    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。

要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:

obj1 = null;
obj2 = null;

(2)标记清除 (mark and sweep)
这是当前主流的 GC 算法,从2012年起,所有现代浏览器都使用了** 标记-清除(Mark-Sweep)**垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于 标记-清除算法 的改进,并没有改进 标记-清除算法 本身和它对“对象是否不再需要”的简化定义。

Mark-Sweep(标记清除)分为标记清除两个阶段,在标记阶段会遍历堆中的所有对象,然后标记活着的对象,在清除阶段中,会将死亡的对象进行清除。

当对象,无法从根对象沿着引用遍历到,即不可达(unreachable),进行清除。JAVASCRIPT 中有个全局对象,浏览器中是 window。定期的,垃圾回收期将从这个全局对象开始,找所有从这个全局对象开始引用的对象,再找这些对象引用的对象...对这些活着的对象进行标记,这是标记阶段清除阶段就是清除那些没有被标记的对象。

有了标记清除法,循环引用不再是问题了。在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。

常见 JavaScript 内存泄漏

1.意外的全局变量(全局变量不会被标记清除法清除)

JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是 window

function foo(arg) {
    bar = "this is a hidden global variable";  // 意外挂在在 window 全局变量,导致内存泄漏
}

解决方法:

delete window.bar

2.被遗忘的计时器或回调函数

  • 计数器函数,一直占用内存
// 计数器一直存在会一直占用内存,计数器结束需要做释放处理
var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);
  • 事件回调函数(老式浏览器如 IE 6 无法做回收,新版浏览器已经可以处理)
var element = document.getElementById('button');
function onClick(event) {
    element.innerHTML = 'text';
}
element.addEventListener('click', onClick);  
//针对老式浏览器,回收需要移除事件回调
element.removeEventListener('click', onClick);  

3.闭包递归

闭包的特性:1.函数嵌套函数,2.函数内部可以引用外部的参数和变量,3.参数和变量不会被垃圾回收机制回收

闭包由于存在变量引用其返回的匿名函数,导致作用域无法得到释放。 最新版本的浏览器中,可以通过标记清除的方式处理掉闭包的作用域导致的内存泄漏,但闭包的变量(匿名函数、参数变量)会常驻内存,会造成一定的性能问题

let index = 0
function readData() {
  let buf = new Buffer(1024 * 1024 * 100)
  buf.fill('g')  

  return function fn() { // 此处会把 return 出来的函数挂在在 window 下,作用域无法清除
    index++   // 引入局外变量,内存无法清除
    if (index < buf.length) { 
      return buf[index-1]   // buf 不会被清除,需要手动清除
    } else {
      return ''
    } 
  }
}

const data = readData()
const next = data()

内存剖析工具方法

Chrome浏览器方法

Chrome 提供了一套很棒的检测 JavaScript 内存占用的工具 Memory , 提供 Heap snapshot (堆内存截图)、allocation instrumentation on timeline ( 内存timeline上的分配检测)、allocation sampling (内存分配抽样)

Node 命令行查看内存状态方法

console.log(process.memoryUsage());
// { rss: 27709440,
//  heapTotal: 5685248,
//  heapUsed: 3449392,
//  external: 8772 }

process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,含义如下。

  • rss(resident set size)`:所有内存占用,包括指令区和堆栈。
  • heapTotal:"堆"占用的内存,包括用到的和没用到的。
  • heapUsed:用到的堆的部分。
  • external: V8 引擎内部的 C++ 对象占用的内存。

判断内存泄漏,以heapUsed字段为准。

WeakSet 和 WeakMap

通过前几个内存泄漏示例我们会发现如果我们一旦疏忽,就会容易地引发内存泄漏的问题。及时清除引用,回收内存非常重要。但是实际生产过程中,有可能不清楚上下文,导致内存泄漏。最好能有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用可以忽略不计,当其他引用消失以后,垃圾回收机制就可以释放内存。这样就能大大减轻程序员的负担,只要清除主要引用就可以了。

ES6 考虑到了这一点,推出了两种新的数据结构:WeakSetWeakMap。它们对于值的引用都是不计入垃圾回收机制的,弱引用。

const wm = new WeakMap();

const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"

上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。

也就是说,DOM 节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。

基本上,如果要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap

参考阅读:

JavaScript深入理解之垃圾收集

阮一峰-JavaScript 内存泄漏教程

阮一峰-JavaScript 运行机制详解:再谈Event Loop

4类 JavaScript 内存泄漏及如何避免

10分钟了解JS堆、栈以及事件循环的概念

(js队列,堆栈) (FIFO,LIFO)

从Promise到Event Loop

Node.js 内存管理和 V8 垃圾回收机制

Koa2原理详解

Koa vs Express

Koa是继Express之后,Node的又一主流Web开发框架。相比于Express,Koa只保留了核心的中间件处理逻辑,去掉了路由,模板,以及其他一些功能。详细的比较可以参考Koa vs Express

另一方面,在中间件的处理过程中,Koa和Express也有着一定区别,看下面例子:

// http style
http.createServer((req, res) => {
  // ...
})

// express style
app.use((req, res, next) => {
  // ...
})

// koa style
app.use( async (ctx, next) => {
  // ...
})

Node自带的http模块处理请求的时候,参数是一个req和res,分别为http.IncomingMessage和http.ServerResponse的实例。

Express对请求参数req和res的原型链进行了扩展,增强了req和res的行为。

而Koa并没有改变req和res,而是通过req和res封装了一个ctx (context)对象,进行后面的逻辑处理。

Koa基本组成

Koa源码非常精简,只有四个文件:

  • application.js:Application(或Koa)负责管理中间件,以及处理请求
  • context.js:Context维护了一个请求的上下文环境
  • request.js:Request对req做了抽象和封装
  • response.js:Response对res做了抽象和封装

Application

Application主要维护了中间件以及其它一些环境:

// application.js
module.exports = class Application extends Emitter {
  constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  // ...
}
通过app.use(fn)可以将fn添加到中间件列表this.middleware中。

app.listen方法源码如下:

// application.js
listen() {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen.apply(server, arguments);
}

首先会通过this.callback方法来返回一个函数作为http.createServer的回调函数,然后进行监听。我们已经知道,http.createServer的回调函数接收两个参数:req和res,下面来看this.callback的实现:

// application.js
callback() {
  const fn = compose(this.middleware);

  if (!this.listeners('error').length) this.on('error', this.onerror);

  return (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    onFinished(res, ctx.onerror);
    fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);
  };
}

首先是将所有的中间件通过 compose 组合成一个函数fn,然后返回http.createServer所需要的回调函数。于是我们可以看到,当服务器收到一个请求的时候,会使用req和res通过this.createContext方法来创建一个上下文环境ctx,然后使用fn来进行中间件的逻辑处理。

Context

通过上面的分析,我们已经可以大概得知Koa处理请求的过程:当请求到来的时候,会通过req和res来创建一个context (ctx),然后执行中间件。

事实上,在创建context的时候,还会同时创建request和response,通过下图可以比较直观地看到所有这些对象之间的关系。

context

图中:

  • 最左边一列表示每个文件的导出对象
  • 中间一列表示每个Koa应用及其维护的属性
  • 右边两列表示对应每个请求所维护的一些对象
  • 黑色的线表示实例化
  • 红色的线表示原型链
  • 蓝色的线表示属性

实际上,ctx主要的功能是代理request和response的功能,提供了对request和response对象的便捷访问能力。在源码中,我们可以看到:

// context.js
delegate(proto, 'response')
  .method('attachment')
  // ...
  .access('status')
  // ...
  .getter('writable');

delegate(proto, 'request')
  .method('acceptsLanguages')
  // ...
  .access('querystring')
  // ...
  .getter('ip');

这里使用了 delegates 模块来实现属性访问的代理。简单来说,通过delegate(proto, 'response'),当访问proto的代理属性的时候,实际上是在访问proto.response的对应属性。

Request & Response

Request对req进行了抽象和封装,其中对于请求的url相关的处理如图:

┌────────────────────────────────────────────────────────┐
│ href │
├────────────────────────────┬───────────────────────────┤
│ origin │ url / originalurl │
├──────────┬─────────────────┼──────────┬────────────────┤
│ protocol │ host │ path │ search │
├──────────├──────────┬──────┼──────────┼─┬──────────────┤
│ │ hostname │ port │ │?│ querystring │
│ ├──────────┼──────┤ ├─┼──────────────┤
│ │ │ │ │ │ │
" http: │ host.com : 8080 /p/a/t/h ? query=string │
│ │ │ │ │ │ │
└──────────┴──────────┴──────┴──────────┴─┴──────────────┘

Response对res进行了封装和抽象,这里不做赘述。

中间件的执行

在上面已经提到,所有的中间件会经过 compose 处理,返回一个新的函数。该模块源码如下:

function compose(middleware) {
  // 错误处理
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function(context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)

    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      // 当前执行第 i 个中间件
      index = i
      let fn = middleware[i]
      // 所有的中间件执行完毕
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()

      try {
        // 执行当前的中间件
        // 这里的fn也就是app.use(fn)中的fn
        return Promise.resolve(fn(context, function next() {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

Koa的中间件支持普通函数,返回一个Promise的函数,以及async函数。由于generator函数中间件在新的版本中将不再支持,因此不建议使用。

参考资料:

koa2-note

koa-guide

koa-0.0.2

带你走进koa2的世界(koa2源码浅谈)

深入浅出 Koa

Koa2源码初读

HSTS详解

HSTS详解

1. 缘起:启用HTTPS也不够安全

有不少网站只通过HTTPS对外提供服务,但用户在访问某个网站的时候,在浏览器里却往往直接输入网站域名(例如 www.example.com),而不是输入完整的URL(例如 https://www.example.com),不过浏览器依然能正确的使用HTTPS发起请求。这背后多亏了服务器和浏览器的协作,如下图所示。

服务器和浏览器

图1:服务器和浏览器在背后帮用户做了很多工作

简单来讲就是,浏览器向网站发起一次HTTP请求,在得到一个重定向响应后(服务器发起301、302),发起一次HTTPS请求并得到最终的响应内容。所有的这一切对用户而言是完全透明的,所以在用户眼里看来,在浏览器里直接输入域名却依然可以用HTTPS协议和网站进行安全的通信,是个不错的用户体验。

一切看上去都是那么的完美,但其实不然,由于在建立起HTTPS连接之前存在一次明文的HTTP请求和重定向(上图中的第1、2步),使得攻击者可以以中间人的方式劫持这次请求,从而进行后续的攻击,例如窃听数据,篡改请求和响应,跳转到钓鱼网站等。

以劫持请求并跳转到钓鱼网站为例,其大致做法如下图所示:

劫持HTTP请求,阻止HTTPS连接,并进行钓鱼攻击

图2:劫持HTTP请求,阻止HTTPS连接,并进行钓鱼攻击
  • 第1步:浏览器发起一次明文HTTP请求,但实际上会被攻击者拦截下来
  • 第2步:攻击者作为代理,把当前请求转发给钓鱼网站
  • 第3步:钓鱼网站返回假冒的网页内容
  • 第4步:攻击者把假冒的网页内容返回给浏览器

这个攻击的精妙之处在于,攻击者直接劫持了HTTP请求,并返回了内容给浏览器,根本不给浏览器同真实网站建立HTTPS连接的机会,因此浏览器会误以为真实网站通过HTTP对外提供服务,自然也就不会向用户报告当前的连接不安全。于是乎攻击者几乎可以神不知鬼不觉的对请求和响应动手脚。

2. 解决之道:使用HSTS

既然建立HTTPS连接之前的这一次HTTP明文请求和重定向有可能被攻击者劫持,那么解决这一问题的思路自然就变成了如何避免出现这样的HTTP请求。我们期望的浏览器行为是,当用户让浏览器发起HTTP请求的时候,浏览器将其转换为HTTPS请求,直接略过上述的HTTP请求和重定向,从而使得中间人攻击失效,规避风险。其大致流程如下:

服务器和浏览器

图3:略过HTTP请求和重定向,直接发送HTTPS请求
  • 第1步:用户在浏览器地址栏里输入网站域名,浏览器得知该域名应该使用HTTPS进行通信
  • 第2步:浏览器直接向网站发起HTTPS请求
  • 第3步:网站返回相应的内容

那么问题来了,浏览器是如何做到这一点的呢?它怎么知道那个网站应该发HTTPS请求,那个网站应该用HTTP请求呢?此时就该HSTS粉墨登场了。

2.1 HSTS

HSTS的全称是HTTP Strict-Transport-Security,它是一个Web安全策略机制(web security policy mechanism)。

HSTS最早于2015年被纳入到ThoughtWorks技术雷达,并且在2016年的最新一期技术雷达里,它直接从“评估(Trial)”阶段进入到了“采用(Adopt)“阶段,这意味着ThoughtWorks强烈主张业界积极采用这项安全防御措施,并且ThoughtWorks已经将其应用于自己的项目。

HSTS最为核心的是一个HTTP响应头(HTTP Response Header)。正是它可以让浏览器得知,在接下来的一段时间内,当前域名只能通过HTTPS进行访问,并且在浏览器发现当前连接不安全的情况下,强制拒绝用户的后续访问要求。

HSTS Header的语法如下:

Strict-Transport-Security: <max-age=>[; includeSubDomains][; preload]

其中:

  • max-age是必选参数,是一个以秒为单位的数值,它代表着HSTS Header的过期时间,通常设置为1年,即31536000秒。
  • includeSubDomains是可选参数,如果包含它,则意味着当前域名及其子域名均开启HSTS保护。
  • preload是可选参数,只有当你申请将自己的域名加入到浏览器内置列表的时候才需要使用到它。关于浏览器内置列表,下文有详细介绍。

2.2 让浏览器直接发起HTTPS请求

只要在服务器返回给浏览器的响应头中,增加 Strict-Transport-Security 这个HTTP Header(下文简称HSTS Header),例如:

Strict-Transport-Security: max-age=31536000; includeSubDomains

就可以告诉浏览器,在接下来的31536000秒内(1年),对于当前域名及其子域名的后续通信应该强制性的只使用HTTPS,直到超过有效期为止。

浏览器获取到 HSTS 信息后,会将所有HTTP访问请求在内部做307跳转到HTTPS,无需服务器做任何跳转。

完整的流程如下图所示:

图4:完整的HSTS流程

图4:完整的HSTS流程

只要是在有效期内,浏览器都将直接强制性的发起HTTPS请求,但是问题又来了,有效期过了怎么办?其实不用为此过多担心,因为HSTS Header存在于每个响应中,随着用户和网站的交互,这个有效时间时刻都在刷新,再加上有效期通常都被设置成了1年,所以只要用户的前后两次请求之间的时间间隔没有超过1年,则基本上不会出现安全风险。更何况,就算超过了有效期,但是只要用户和网站再进行一次新的交互,用户的浏览器又将开启有效期为1年的HSTS保护。

2.3 让浏览器强制拒绝不安全的链接,不给用户选择的机会

在没有HSTS保护的情况下,当浏览器发现当前网站的证书出现错误,或者浏览器和服务器之间的通信不安全,无法建立HTTPS连接的时候,浏览器通常会警告用户,但是却又允许用户继续不安全的访问。如下图所示,用户可以点击图中红色方框中的链接,继续在不安全的连接下进行访问。

图5:浏览器依然允许用户进行不安全的访问

图5:浏览器依然允许用户进行不安全的访问

理论上而言,用户看到这个警告之后就应该提高警惕,意识到自己和网站之间的通信不安全,可能被劫持也可能被窃听,如果访问的恰好是银行、金融类网站的话后果更是不堪设想,理应终止后续操作。然而现实很残酷,就我的实际观察来看,有不少用户在遇到这样的警告之后依然选择了继续访问。

不过随着HSTS的出现,事情有了转机。对于启用了浏览器HSTS保护的网站,如果浏览器发现当前连接不安全,它将仅仅警告用户,而不再给用户提供是否继续访问的选择,从而避免后续安全问题的发生。例如,当访问Google搜索引擎的时候,如果当前通信连接存在安全问题,浏览器将会彻底阻止用户继续访问Google,如下图所示。

图6:浏览器彻底阻止用户继续进行不安全的访问

图6:浏览器彻底阻止用户继续进行不安全的访问

3. 道高一尺魔高一丈:攻击者依然有可乘之机

细心的你可能发现了,HSTS存在一个比较薄弱的环节,那就是浏览器没有当前网站的HSTS信息的时候,或者第一次访问网站的时候,依然需要一次明文的HTTP请求和重定向才能切换到HTTPS,以及刷新HSTS信息。而就是这么一瞬间却给攻击者留下了可乘之机,使得他们可以把这一次的HTTP请求劫持下来,继续中间人攻击。

4. Preload List:让防御更加彻底

针对上面的攻击,HSTS也有应对办法,那就是在浏览器里内置一个列表,只要是在这个列表里的域名,无论何时、何种情况,浏览器都只使用HTTPS发起连接。这个列表由Google Chromium维护,FireFox、Safari、IE等主流浏览器均在使用。

5. 一些Tips

Tips 1:如何配置HSTS

很多地方都可以进行HSTS的配置,例如反向代理服务器、应用服务器、应用程序框架,以及应用程序中自定义Header。你可以根据实际情况进行选择。

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

不过需要特别注意的是,在生产环境下使用HSTS应当特别谨慎,因为一旦浏览器接收到HSTS Header(假如有效期是1年),但是网站的证书又恰好出了问题,那么用户将在接下来的1年时间内都无法访问到你的网站,直到证书错误被修复,或者用户主动清除浏览器缓存。因此,建议在生产环境开启HSTS的时候,先将max-age的值设置小一些,例如5分钟,然后检查HSTS是否能正常工作,网站能否正常访问,之后再逐步将时间延长,例如1周、1个月,并在这个时间范围内继续检查HSTS是否正常工作,最后才改到1年。

Tips 2:如何加入到HSTS Preload List

根据官方说明,你的网站在具备以下几个条件后,可以提出申请加入到这个列表里。

  • 具备一个有效的证书
  • 在同一台主机上提供重定向响应,以及接收重定向过来的HTTPS请求
  • 所有子域名均使用HTTPS
  • 在根域名的HTTP响应头中,加入HSTS Header,并满足下列条件:

①过期时间最短不得少于18周(10886400秒)

②必须包含 includeSubDomains 参数

③必须包含 preload 参数

④当你准好这些之后,可以在HSTS Preload List的官网上(https://hstspreload.org)提交申请,或者了解更多详细的内容。

Tips 3:如何查询域名是否加入到了Preload List

从提交申请到完成审核,成功加入到内置列表 ,中间可能需要等待几天到几周不等的时间。可通过官网 https://hstspreload.org 或在Chrome地址栏里输入 chrome://net-internals/#hsts 查询状态。

总结

随着越来越多的网站开始使用HTTPS,甚至是开启全站HTTPS,数据在传输过程中的安全性能够得到极大的保障,与此同时,通过HSTS的帮助,避免SSL Stripping或者中间人攻击,能够使得数据通信变得更加安全。希望本篇文章通过对HSTS的解析,能使得更多的开发团队将HSTS运用到自己的项目中。

原文链接来源:http://www.jianshu.com/p/caa80c7ad45c

Other Resources

浏览器发起 http 请求时候,如何知道服务器支持什么 http 版本?

解决缺陷,让HSTS变得完美

前端大文件上传--转载

在某些业务中,大文件上传是一个比较重要的交互场景,如上传入库比较大的Excel表格数据、上传影音文件等。如果文件体积比较大,或者网络条件不好时,上传的时间会比较长(要传输更多的报文,丢包重传的概率也更大),用户不能刷新页面,只能耐心等待请求完成。

下面从文件上传方式入手,整理大文件上传的思路,并给出了相关实例代码,由于PHP内置了比较方便的文件拆分和拼接方法,因此服务端代码使用PHP进行示例编写。

本文相关示例代码位于github上,主要参考

文件上传的几种方式

首先我们来看看文件上传的几种方式。

普通表单上传

使用PHP来展示常规的表单上传是一个不错的选择。首先构建文件上传的表单,并指定表单的提交内容类型为enctype="multipart/form-data",表明表单需要上传二进制数据。

<form action="/index.php" method="POST" enctype="multipart/form-data">
  <input type="file" name="myfile">
  <input type="submit">
</form>
复制代码

然后编写index.php上传文件接收代码,使用move_uploaded_file方法即可(php大法好...)

$imgName = 'IMG'.time().'.'.str_replace('image/','',$_FILES["myfile"]['type']);
$fileName =  'upload/'.$imgName;
// 移动上传文件至指定upload文件夹下,并根据返回值判断操作是否成功
if (move_uploaded_file($_FILES['myfile']['tmp_name'], $fileName)){
    echo $fileName;
}else {
    echo "nonn";
}
复制代码

form表单上传大文件时,很容易遇见服务器超时的问题。通过xhr,前端也可以进行异步上传文件的操作,一般由两个思路。

文件编码上传

第一个思路是将文件进行编码,然后在服务端进行解码,之前写过一篇在前端实现图片压缩上传的博客,其主要实现原理就是将图片转换成base64进行传递

var imgURL = URL.createObjectURL(file);
ctx.drawImage(imgURL, 0, 0);
// 获取图片的编码,然后将图片当做是一个很长的字符串进行传递
var data = canvas.toDataURL("image/jpeg", 0.5); 
复制代码

在服务端需要做的事情也比较简单,首先解码base64,然后保存图片即可

$imgData = $_REQUEST['imgData'];
$base64 = explode(',', $imgData)[1];
$img = base64_decode($base64);
$url = './test.jpg';
if (file_put_contents($url, $img)) {
    exit(json_encode(array(
        url => $url
    )));
}
复制代码

base64编码的缺点在于其体积比原图片更大(因为Base64将三个字节转化成四个字节,因此编码后的文本,会比原文本大出三分之一左右),对于体积很大的文件来说,上传和解析的时间会明显增加。

更多关于base64的知识,可以参考Base64笔记。

除了进行base64编码,还可以在前端直接读取文件内容后以二进制格式上传

// 读取二进制文件
function readBinary(text){
   var data = new ArrayBuffer(text.length);
   var ui8a = new Uint8Array(data, 0);
   for (var i = 0; i < text.length; i++){ 
     ui8a[i] = (text.charCodeAt(i) & 0xff);
   }
   console.log(ui8a)
}

var reader = new FileReader();
reader.onload = function(){
	  readBinary(this.result) // 读取result或直接上传
}
// 把从input里读取的文件内容,放到fileReader的result字段里
reader.readAsBinaryString(file);
复制代码

formData异步上传

FormData对象主要用来组装一组用 XMLHttpRequest发送请求的键/值对,可以更加灵活地发送Ajax请求。可以使用FormData来模拟表单提交。

let files = e.target.files // 获取input的file对象
let formData = new FormData();
formData.append('file', file);
axios.post(url, formData);
复制代码

服务端处理方式与直接form表单请求基本相同。

iframe无刷新页面

在低版本的浏览器(如IE)上,xhr是不支持直接上传formdata的,因此只能用form来上传文件,而form提交本身会进行页面跳转,这是因为form表单的target属性导致的,其取值有

  • _self,默认值,在相同的窗口中打开响应页面
  • _blank,在新窗口打开
  • _parent,在父窗口打开
  • _top,在最顶层的窗口打开
  • framename,在指定名字的iframe中打开

如果需要让用户体验异步上传文件的感觉,可以通过framename指定iframe来实现。把form的target属性设置为一个看不见的iframe,那么返回的数据就会被这个iframe接受,因此只有该iframe会被刷新,至于返回结果,也可以通过解析这个iframe内的文本来获取。

function upload(){
    var now = +new Date()
    var id = 'frame' + now
    $("body").append(`<iframe style="display:none;" name="${id}" id="${id}" />`);

    var $form = $("#myForm")
    $form.attr({
        "action": '/index.php',
        "method": "post",
        "enctype": "multipart/form-data",
        "encoding": "multipart/form-data",
        "target": id
    }).submit()

    $("#"+id).on("load", function(){
        var content = $(this).contents().find("body").text()
        try{
            var data = JSON.parse(content)
        }catch(e){
            console.log(e)
        }
    })
}
复制代码

大文件上传

现在来看看在上面提到的几种上传方式中实现大文件上传会遇见的超时问题,

  • 表单上传和iframe无刷新页面上传,实际上都是通过form标签进行上传文件,这种方式将整个请求完全交给浏览器处理,当上传大文件时,可能会遇见请求超时的情形
  • 通过fromData,其实际也是在xhr中封装一组请求参数,用来模拟表单请求,无法避免大文件上传超时的问题
  • 编码上传,我们可以比较灵活地控制上传的内容

大文件上传最主要的问题就在于:在同一个请求中,要上传大量的数据,导致整个过程会比较漫长,且失败后需要重头开始上传。试想,如果我们将这个请求拆分成多个请求,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始,这样是否可以解决大文件上传的问题呢?

综合上面的问题,看来大文件上传需要实现下面几个需求

  • 支持拆分上传请求(即切片)
  • 支持断点续传
  • 支持显示上传进度和暂停上传

接下来让我们依次实现这些功能,看起来最主要的功能应该就是切片了。

文件切片

参考: 大文件切割上传

编码方式上传中,在前端我们只要先获取文件的二进制内容,然后对其内容进行拆分,最后将每个切片上传到服务端即可。

在JavaScript中,文件FIle对象是Blob对象的子类,Blob对象包含一个重要的方法slice,通过这个方法,我们就可以对二进制文件进行拆分。

下面是一个拆分文件的示例

function slice(file, piece = 1024 * 1024 * 5) {
  let totalSize = file.size; // 文件总大小
  let start = 0; // 每次上传的开始字节
  let end = start + piece; // 每次上传的结尾字节
  let chunks = []
  while (start < totalSize) {
    // 根据长度截取每次需要上传的数据
    // File对象继承自Blob对象,因此包含slice方法
    let blob = file.slice(start, end); 
    chunks.push(blob)

    start = end;
    end = start + piece;
  }
  return chunks
}
复制代码

将文件拆分成piece大小的分块,然后每次请求只需要上传这一个部分的分块即可

let file =  document.querySelector("[name=file]").files[0];

const LENGTH = 1024 * 1024 * 0.1;
let chunks = slice(file, LENGTH); // 首先拆分切片

chunks.forEach(chunk=>{
  let fd = new FormData();
  fd.append("file", chunk);
  post('/mkblk.php', fd)
})
复制代码

服务器接收到这些切片后,再将他们拼接起来就可以了,下面是PHP拼接切片的示例代码

$filename = './upload/' . $_POST['filename'];//确定上传的文件名
//第一次上传时没有文件,就创建文件,此后上传只需要把数据追加到此文件中
if(!file_exists($filename)){
    move_uploaded_file($_FILES['file']['tmp_name'],$filename);
}else{
    file_put_contents($filename,file_get_contents($_FILES['file']['tmp_name']),FILE_APPEND);
    echo $filename;
}
复制代码

测试时记得修改nginx的server配置,否则大文件可能会提示413 Request Entity Too Large的错误。

server {
	// ...
	client_max_body_size 50m;
}
复制代码

上面这种方式来存在一些问题

  • 无法识别一个切片是属于哪一个切片的,当同时发生多个请求时,追加的文件内容会出错
  • 切片上传接口是异步的,无法保证服务器接收到的切片是按照请求顺序拼接的

因此接下来我们来看看应该如何在服务端还原切片。

还原切片

在后端需要将多个相同文件的切片还原成一个文件,上面这种处理切片的做法存在下面几个问题

  • 如何识别多个切片是来自于同一个文件的,这个可以在每个切片请求上传递一个相同文件的context参数
  • 如何将多个切片还原成一个文件
    • 确认所有切片都已上传,这个可以通过客户端在切片全部上传后调用mkfile接口来通知服务端进行拼接
    • 找到同一个context下的所有切片,确认每个切片的顺序,这个可以在每个切片上标记一个位置索引值
    • 按顺序拼接切片,还原成文件

上面有一个重要的参数,即context,我们需要获取为一个文件的唯一标识,可以通过下面两种方式获取

  • 根据文件名、文件长度等基本信息进行拼接,为了避免多个用户上传相同的文件,可以再额外拼接用户信息如uid等保证唯一性
  • 根据文件的二进制内容计算文件的hash,这样只要文件内容不一样,则标识也会不一样,缺点在于计算量比较大.

修改上传代码,增加相关参数

// 获取context,同一个文件会返回相同的值
function createContext(file) {
 	return file.name + file.length
}

let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = slice(file, LENGTH);

// 获取对于同一个文件,获取其的context
let context = createContext(file);

let tasks = [];
chunks.forEach((chunk, index) => {
  let fd = new FormData();
  fd.append("file", chunk);
  // 传递context
  fd.append("context", context);
  // 传递切片索引值
  fd.append("chunk", index + 1);
	
  tasks.push(post("/mkblk.php", fd));
});
// 所有切片上传完毕后,调用mkfile接口
Promise.all(tasks).then(res => {
  let fd = new FormData();
  fd.append("context", context);
  fd.append("chunks", chunks.length);
  post("/mkfile.php", fd).then(res => {
    console.log(res);
  });
});
复制代码

mkblk.php接口中,我们通过context来保存同一个文件相关的切片

// mkblk.php
$context = $_POST['context'];
$path = './upload/' . $context;
if(!is_dir($path)){
    mkdir($path);
}
// 把同一个文件的切片放在相同的目录下
$filename = $path .'/'. $_POST['chunk'];
$res = move_uploaded_file($_FILES['file']['tmp_name'],$filename);
复制代码

除了上面这种简单通过目录区分切片的方法之外,还可以将切片信息保存在数据库来进行索引。接下来是mkfile.php接口的实现,这个接口会在所有切片上传后调用

// mkfile.php
$context = $_POST['context'];
$chunks = (int)$_POST['chunks'];

//合并后的文件名
$filename = './upload/' . $context . '/file.jpg'; 
for($i = 1; $i <= $chunks; ++$i){
    $file = './upload/'.$context. '/' .$i; // 读取单个切块
    $content = file_get_contents($file);
    if(!file_exists($filename)){
        $fd = fopen($filename, "w+");
    }else{
        $fd = fopen($filename, "a");
    }
    fwrite($fd, $content); // 将切块合并到一个文件上
}
echo $filename;
复制代码

这样就解决了上面的两个问题:

  • 识别切片来源
  • 保证切片拼接顺序

断点续传

即使将大文件拆分成切片上传,我们仍需等待所有切片上传完毕,在等待过程中,可能发生一系列导致部分切片上传失败的情形,如网络故障、页面关闭等。由于切片未全部上传,因此无法通知服务端合成文件。这种情况下可以通过断点续传来进行处理。

断点续传指的是:可以从已经上传部分开始继续上传未完成的部分,而没有必要从头开始上传,节省上传时间。

由于整个上传过程是按切片维度进行的,且mkfile接口是在所有切片上传完成后由客户端主动调用的,因此断点续传的实现也十分简单:

  • 在切片上传成功后,保存已上传的切片信息
  • 当下次传输相同文件时,遍历切片列表,只选择未上传的切片进行上传
  • 所有切片上传完毕后,再调用mkfile接口通知服务端进行文件合并

因此问题就落在了如何保存已上传切片的信息了,保存一般有两种策略

  • 可以通过locaStorage等方式保存在前端浏览器中,这种方式不依赖于服务端,实现起来也比较方便,缺点在于如果用户清除了本地文件,会导致上传记录丢失
  • 服务端本身知道哪些切片已经上传,因此可以由服务端额外提供一个根据文件context查询已上传切片的接口,在上传文件前调用该文件的历史上传记录

下面让我们通过在本地保存已上传切片记录,来实现断点上传的功能

 // 获取已上传切片记录
function getUploadSliceRecord(context){
  let record = localStorage.getItem(context)
  if(!record){
    return []
  }else {
    try{
      return JSON.parse(record)
    }catch(e){}
  }
}
// 保存已上传切片
function saveUploadSliceRecord(context, sliceIndex){
  let list = getUploadSliceRecord(context)
  list.push(sliceIndex)
  localStorage.setItem(context, JSON.stringify(list))
}
复制代码

然后对上传逻辑稍作修改,主要是增加上传前检测是已经上传、上传后保存记录的逻辑

let context = createContext(file);
// 获取上传记录
let record = getUploadSliceRecord(context);
let tasks = [];
chunks.forEach((chunk, index) => {
  // 已上传的切片则不再重新上传
  if(record.includes(index)){
    return
  }
	
  let fd = new FormData();
  fd.append("file", chunk);
  fd.append("context", context);
  fd.append("chunk", index + 1);

  let task = post("/mkblk.php", fd).then(res=>{
    // 上传成功后保存已上传切片记录
    saveUploadSliceRecord(context, index)
    record.push(index)
  })
  tasks.push(task);
});
复制代码

此时上传时刷新页面或者关闭浏览器,再次上传相同文件时,之前已经上传成功的切片就不会再重新上传了。

服务端实现断点续传的逻辑基本相似,只要在getUploadSliceRecord内部调用服务端的查询接口获取已上传切片的记录即可,因此这里不再展开。

此外断点续传还需要考虑切片过期的情况:如果调用了mkfile接口,则磁盘上的切片内容就可以清除掉了,如果客户端一直不调用mkfile的接口,放任这些切片一直保存在磁盘显然是不可靠的,一般情况下,切片上传都有一段时间的有效期,超过该有效期,就会被清除掉。基于上述原因,断点续传也必须同步切片过期的实现逻辑。

上传进度和暂停

通过xhr.upload中的progress方法可以实现监控每一个切片上传进度。

上传暂停的实现也比较简单,通过xhr.abort可以取消当前未完成上传切片的上传,实现上传暂停的效果,恢复上传就跟断点续传类似,先获取已上传的切片列表,然后重新发送未上传的切片。

由于篇幅关系,上传进度和暂停的功能这里就先不实现了。

小结

目前社区已经存在一些成熟的大文件上传解决方案,如七牛SDK腾讯云SDK等,也许并不需要我们手动去实现一个简陋的大文件上传库,但是了解其原理还是十分有必要的。

本文首先整理了前端文件上传的几种方式,然后讨论了大文件上传的几种场景,以及大文件上传需要实现的几个功能

  • 通过Blob对象的slice方法将文件拆分成切片
  • 整理了服务端还原文件所需条件和参数,演示了PHP将切片还原成文件
  • 通过保存已上传切片的记录来实现断点续传

Other resource

浏览器端js有如何为本机生成固定的uuid

Anonymous browser fingerprint

从 Fetch 到 Streams —— 以流的角度处理网络请求

大规格文件的上传优化

SparkMD5

前端测试框架 Jest

前端测试工具一览

前端测试工具也和前端的框架一样纷繁复杂,其中常见的测试工具,大致可分为测试框架、断言库、测试覆盖率工具等几类。在正式开始本文之前,我们先来大致了解下它们:

测试框架

测试框架的作用是提供一些方便的语法来描述测试用例,以及对用例进行分组。

测试框架可分为两种: TDD (测试驱动开发)和 BDD (行为驱动开发),我理解两者间的区别主要是一些语法上的不同,其中 BDD 提供了提供了可读性更好的用例语法,至于详细的区别可参见 The Difference Between TDD and BDD 一文。

常见的测试框架有 Jasmine, Mocha 以及本文要介绍的 Jest

断言库

断言库主要提供语义化方法,用于对参与测试的值做各种各样的判断。这些语义化方法会返回测试的结果,要么成功、要么失败。常见的断言库有 Should.js, Chai.js 等。

测试覆盖率工具

用于统计测试用例对代码的测试情况,生成相应的报表,比如 istanbul

Jest

为什么选择 Jest

Jest 是 Facebook 出品的一个测试框架,相对其他测试框架,其一大特点就是就是内置了常用的测试工具,比如自带断言、测试覆盖率工具,实现了开箱即用。

而作为一个面向前端的测试框架, Jest 可以利用其特有的快照测试功能,通过比对 UI 代码生成的快照文件,实现对 React 等常见框架的自动测试。

此外, Jest 的测试用例是并行执行的,而且只执行发生改变的文件所对应的测试,提升了测试速度。目前在 Github 上其 star 数已经破万;而除了 Facebook 外,业内其他公司也开始从其它测试框架转向 Jest ,比如 Airbnb 的尝试 ,相信未来 Jest 的发展趋势仍会比较迅猛。

jest_process

安装

Jest 可以通过 npm 或 yarn 进行安装。以 npm 为例,既可用npm install -g jest进行全局安装;也可以只局部安装、并在 package.json 中指定 test 脚本:

{
  "scripts": {
    "test": "jest"
  }
}

Jest 的测试脚本名形如*.test.js,不论 Jest 是全局运行还是通过npm test运行,它都会执行当前目录下所有的*.test.js*.spec.js 文件、完成测试。

基本使用

用例的表示

表示测试用例是一个测试框架提供的最基本的 API , Jest 内部使用了 Jasmine 2 来进行测试,故其用例语法与 Jasmine 相同。test()函数来描述一个测试用例,举个简单的例子:

// hello.js
module.exports = () => 'Hello world'
// hello.test.js
let hello = require('hello.js')

test('should get "Hello world"', () => {
    expect(hello()).toBe('Hello world') // 测试成功
    // expect(hello()).toBe('Hello') // 测试失败
})

其中toBe('Hello world')便是一句断言( Jest 管它叫 “matcher” ,想了解更多 matcher 请参考文档)。写完了用例,运行在项目目录下执行npm test,即可看到测试结果:

img

若测试失败,会标识出失败的断言位置,结果如下:

img

用例的预处理或后处理

有时我们想在测试开始之前进行下环境的检查、或者在测试结束之后作一些清理操作,这就需要对用例进行预处理或后处理。对测试文件中所有的用例进行统一的预处理,可以使用 beforeAll() 函数;而如果想在每个用例开始前进行都预处理,则可使用 beforeEach() 函数。至于后处理,也有对应的 afterAll()afterEach() 函数。

如果只是想对某几个用例进行同样的预处理或后处理,可以将先将这几个用例归为一组。使用 describe() 函数即可表示一组用例,再将上面提到的四个处理函数置于 describe() 的处理回调内,就实现了对一组用例的预处理或后处理:

describe('test testObject', () => {
    beforeAll(() => {
        // 预处理操作
    })

    test('is foo', () => {
       expect(testObject.foo).toBeTruthy()
    })

    test('is not bar', () => {
        expect(testObject.bar).toBeFalsy()
    })

    afterAll(() => {
        // 后处理操作
    })
})

测试异步代码

异步代码的测试,关键点在于告知测试框架测试何时完成,让其在恰当的时机进行断言。针对几种常见的异步代码形式, Jest 也提供了相应的异步测试语法。首先对于异步回调,向其传入并执行 done 函数, Jest 会等 done 回调执行结束后,结束测试:

// asyncHello.js
module.exports = (name, cb) => setTimeout(() => cb(`Hello ${name}`), 1000)
// asyncHello.test.js
let asyncHello = require('asyncHello.js')

test('should get "Hello world"', (done) => {
    asyncHello('world', (result) => {
        expect(result).toBe('Hello world')
        done()
    })
})

此外,对于 Promise 控制的异步代码,可以直接在 then 回调中进行断言,只要保证在用例中返回该 Promise 对象即可:

// promiseHello.js
module.exports = (name) => {
    return new Promise((resolve) => {
        setTimeout(() => resolve(`Hello ${name}`), 1000)
    })
}
// promiseHello.test.js
let promiseHello = require('promiseHello.js')

it('should get "Hello world"', () => {
    expect.assertions(1); // 确保至少有一个断言被调用,否则测试失败
    return promiseHello('world').then((data) => {
        expect(data).toBe('Hello world')
    })
})

Jest 也支持 async/await 语法的测试,无需多余的操作,只要在 await 后进行断言即可,和同步测试的写法一致。

测试覆盖率

Jest 内置了测试覆盖率工具istanbul,要开启,可以直接在命令中添加 --coverage 参数,或者在 package.json 文件进行更详细的配置

运行 istanbul 除了会再终端展示测试覆盖率情况,还会在项目下生产一个 coverage 目录,内附一个测试覆盖率的报告,让我们可以清晰看到分支的代码的测试情况。比如下面这个例子:

// branches.js
module.exports = (name) => {
    if (name === 'Levon') {
        return `Hello Levon`
    } else {
        return `Hello ${name}`
    }
}
// branches.test.js
let branches = require('../branches.js')

describe('Multiple branches test', ()=> {
    test('should get Hello Levon', ()=> {
          expect(branches('Levon')).toBe('Hello Levon')
    });
    // test('should get Hello World', ()=> {
    //       expect(branches('World')).toBe('Hello World')
    // });  
})

运行 jest --coverage 可看到产生的报告里展示了代码的覆盖率和未测试的行数:

img

如果我们把branches.test.js中的注释去掉,跑遍测试对象中的所有分支,测试覆盖率就是100%了:

img

在前端项目中使用

搭配React和其它框架

针对前端框架的测试, Jest 的一大特色就是提供了快照测试功能。首次运行快照测试,会让 UI 框架生产一个可读的快照,再次测试时便会通过比对快照文件和新 UI 框架产生的快照判断测试是否通过。对于 React ,我们可以通过下面的方法生产一个快照:

import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';

it('renders correctly', () => {
    const tree = renderer.create(
        <Link page="http://www.facebook.com">Facebook</Link>
    ).toJSON();
    expect(tree).toMatchSnapshot();
});

运行测试,我们可以看到生成一个快照文件如下:

exports[`renders correctly 1`] = `
<a
    className="normal"
    href="http://www.facebook.com"
    onMouseEnter={[Function]}
    onMouseLeave={[Function]}
>
    Facebook
</a>
`;

这个可读的快照文件以可读的形式展示了 React 渲染出的 DOM 结构。相比于肉眼观察效果的 UI 测试,快照测试直接由Jest进行比对、速度更快;而且由于直接展示了 DOM 结构,也能让我们在检查快照的时候,快速、准确地发现问题。

除了 React ,Jest 文档中也提供了针对其他框架进行测试的指南

无缝迁移

如果你的项目中已经使用了别的测试框架,比如 Mocha,有一个第三方工具jest-codemods可以自动把用例迁移成 Jest 的用例,降低了迁移成本。

后记:前端自动化测试,值不值得?

近几年前端工程化的发展风起云涌,但是前端自动化测试这块内容大家却似乎不太重视。虽然项目迭代过程中会有专门的测试人员进行测试,但等他们来进行测试时,代码已经开发完成的状态。与之相比,如果我们在开发过程中就进行了测试(直接采用 TDD 开发模式、或者针对既有的模块写用例),会有如下的好处:

  • 保障代码质量和功能的实现的完整度
  • 提升开发效率,在开发过程中进行测试能让我们提前发现 bug ,此时进行问题定位和修复的速度自然比开发完再被叫去修 bug 要快许多
  • 便于项目维护,后续任何代码更新也必须跑通测试用例,即使进行重构或开发人员发生变化也能保障预期功能的实现

当然,凡事都有两面性,好处虽然明显,却并不是所有的项目都值得引入测试框架,毕竟维护测试用例也是需要成本的。对于一些需求频繁变更、复用性较低的内容,比如活动页面,让开发专门抽出人力来写测试用例确实得不偿失。

而那些适合引入测试场景大概有这么几个:

  • 需要长期维护的项目。它们需要测试来保障代码可维护性、功能的稳定性
  • 较为稳定的项目、或项目中较为稳定的部分。给它们写测试用例,维护成本低
  • 被多次复用的部分,比如一些通用组件和库函数。因为多处复用,更要保障质量

扩展 Cypress

Cypress 是专为现代网络打造的下一代前端测试工具,解决了开发人员和质量检查工程师在测试现代应用程序时面临的主要痛点。

  • 端到端测试
  • 整合测试
  • 单元测试

UI测试 - 前端页面交互
单元测试 - 公共组件逻辑测试

Other Resources

用 GitLab CI 进行持续集成

简介

从 GitLab 8.0 开始,GitLab CI 就已经集成在 GitLab 中,我们只要在项目中添加一个 .gitlab-ci.yml 文件,然后添加一个 Runner,即可进行持续集成。 而且随着 GitLab 的升级,GitLab CI 变得越来越强大,本文将介绍如何使用 GitLab CI 进行持续集成。

一些概念

在介绍 GitLab CI 之前,我们先看看一些持续集成相关的概念。

Pipeline

一次 Pipeline 其实相当于一次构建任务,里面可以包含多个流程,如安装依赖、运行测试、编译、部署测试服务器、部署生产服务器等流程。
任何提交或者 Merge Request 的合并都可以触发 Pipeline,如下图所示:

+------------------+           +----------------+
|                  |  trigger  |                |
|   Commit / MR    +---------->+    Pipeline    |
|                  |           |                |
+------------------+           +----------------+

Stages

Stages 表示构建阶段,说白了就是上面提到的流程。
我们可以在一次 Pipeline 中定义多个 Stages,这些 Stages 会有以下特点:

  • 所有 Stages 会按照顺序运行,即当一个 Stage 完成后,下一个 Stage 才会开始
  • 只有当所有 Stages 完成后,该构建任务 (Pipeline) 才会成功
  • 如果任何一个 Stage 失败,那么后面的 Stages 不会执行,该构建任务 (Pipeline) 失败

因此,Stages 和 Pipeline 的关系就是:

+--------------------------------------------------------+
|                                                        |
|  Pipeline                                              |
|                                                        |
|  +-----------+     +------------+      +------------+  |
|  |  Stage 1  |---->|   Stage 2  |----->|   Stage 3  |  |
|  +-----------+     +------------+      +------------+  |
|                                                        |
+--------------------------------------------------------+

Jobs

Jobs 表示构建工作,表示某个 Stage 里面执行的工作。
我们可以在 Stages 里面定义多个 Jobs,这些 Jobs 会有以下特点:

  • 相同 Stage 中的 Jobs 会并行执行
  • 相同 Stage 中的 Jobs 都执行成功时,该 Stage 才会成功
  • 如果任何一个 Job 失败,那么该 Stage 失败,即该构建任务 (Pipeline) 失败

所以,Jobs 和 Stage 的关系图就是:

+------------------------------------------+
|                                          |
|  Stage 1                                 |
|                                          |
|  +---------+  +---------+  +---------+   |
|  |  Job 1  |  |  Job 2  |  |  Job 3  |   |
|  +---------+  +---------+  +---------+   |
|                                          |
+------------------------------------------+

GitLab Runner

简介

理解了上面的基本概念之后,有没有觉得少了些什么东西 —— 由谁来执行这些构建任务呢?
答案就是 GitLab Runner 了!

想问为什么不是 GitLab CI 来运行那些构建任务?
一般来说,构建任务都会占用很多的系统资源 (譬如编译代码),而 GitLab CI 又是 GitLab 的一部分,如果由 GitLab CI 来运行构建任务的话,在执行构建任务的时候,GitLab 的性能会大幅下降。

GitLab CI 最大的作用是管理各个项目的构建状态,因此,运行构建任务这种浪费资源的事情就交给 GitLab Runner 来做拉!
因为 GitLab Runner 可以安装到不同的机器上,所以在构建任务运行期间并不会影响到 GitLab 的性能~

安装

安装 GitLab Runner 太简单了,按照着 官方文档 的教程来就好拉!
下面是 Debian/Ubuntu/CentOS 的安装方法,其他系统去参考官方文档:

# For Debian/Ubuntu
$ curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | sudo bash
$ sudo apt-get install gitlab-ci-multi-runner

# For CentOS
$ curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh | sudo bash
$ sudo yum install gitlab-ci-multi-runner

注册 Runner

安装好 GitLab Runner 之后,我们只要启动 Runner 然后和 CI 绑定就可以了:

  • 打开你 GitLab 中的项目页面,在项目设置中找到 runners
  • 运行 sudo gitlab-ci-multi-runner register
  • 输入 CI URL
  • 输入 Token
  • 输入 Runner 的名字
  • 选择 Runner 的类型,简单起见还是选 Shell 吧
  • 完成

当注册好 Runner 之后,可以用 sudo gitlab-ci-multi-runner list 命令来查看各个 Runner 的状态:

$ sudo gitlab-runner list
Listing configured runners          ConfigFile=/etc/gitlab-runner/config.toml
my-runner                           Executor=shell Token=cd1cd7cf243afb47094677855aacd3 URL=http://mygitlab.com/ci

.gitlab-ci.yml

简介

配置好 Runner 之后,我们要做的事情就是在项目根目录中添加 .gitlab-ci.yml 文件了。
当我们添加了 .gitlab-ci.yml 文件后,每次提交代码或者合并 MR 都会自动运行构建任务了。

还记得 Pipeline 是怎么触发的吗?Pipeline 也是通过提交代码或者合并 MR 来触发的!
那么 Pipeline 和 .gitlab-ci.yml 有什么关系呢?
其实 .gitlab-ci.yml 就是在定义 Pipeline 而已拉!

基本写法

我们先来看看 .gitlab-ci.yml 是怎么写的:

# 定义 stages
stages:
  - build
  - test

# 定义 job
job1:
  stage: test
  script:
    - echo "I am job1"
    - echo "I am in test stage"

# 定义 job
job2:
  stage: build
  script:
    - echo "I am job2"
    - echo "I am in build stage"

写起来很简单吧!用 stages 关键字来定义 Pipeline 中的各个构建阶段,然后用一些非关键字来定义 jobs。
每个 job 中可以可以再用 stage 关键字来指定该 job 对应哪个 stage。
job 里面的 script 关键字是最关键的地方了,也是每个 job 中必须要包含的,它表示每个 job 要执行的命令。

回想一下我们之前提到的 Stages 和 Jobs 的关系,然后猜猜上面例子的运行结果?

I am job2
I am in build stage
I am job1
I am in test stage

根据我们在 stages 中的定义,build 阶段要在 test 阶段之前运行,所以 stage:build 的 jobs 会先运行,之后才会运行 stage:test 的 jobs。

常用的关键字

下面介绍一些常用的关键字,想要更加详尽的内容请前往 官方文档

stages

定义 Stages,默认有三个 Stages,分别是 build, test, deploy

types

stages 的别名。

before_script

定义任何 Jobs 运行前都会执行的命令。

after_script

要求 GitLab 8.7+ 和 GitLab Runner 1.2+

定义任何 Jobs 运行完后都会执行的命令。

variables && Job.variables

要求 GitLab Runner 0.5.0+

定义环境变量。
如果定义了 Job 级别的环境变量的话,该 Job 会优先使用 Job 级别的环境变量。

cache && Job.cache

要求 GitLab Runner 0.7.0+

定义需要缓存的文件。
每个 Job 开始的时候,Runner 都会删掉 .gitignore 里面的文件。
如果有些文件 (如 node_modules/) 需要多个 Jobs 共用的话,我们只能让每个 Job 都先执行一遍 npm install
这样很不方便,因此我们需要对这些文件进行缓存。缓存了的文件除了可以跨 Jobs 使用外,还可以跨 Pipeline 使用。

具体用法请查看 官方文档

Job.script

定义 Job 要运行的命令,必填项。

Job.stage

定义 Job 的 stage,默认为 test

Job.artifacts

定义 Job 中生成的附件。
当该 Job 运行成功后,生成的文件可以作为附件 (如生成的二进制文件) 保留下来,打包发送到 GitLab,之后我们可以在 GitLab 的项目页面下下载该附件。
注意,不要把 artifactscache 混淆了。

实用例子

下面给出一个我自己在用的例子:

stages:
  - install_deps
  - test
  - build
  - deploy_test
  - deploy_production

cache:
  key: ${CI_BUILD_REF_NAME}
  paths:
    - node_modules/
    - dist/


# 安装依赖
install_deps:
  stage: install_deps
  only:
    - develop
    - master
  script:
    - npm install


# 运行测试用例
test:
  stage: test
  tags:  # 标识使用共享 runner
    - shared
  only:
    - develop
    - master
  script:
    - npm run test


# 编译
build:
  stage: build
  only:
    - develop
    - master
  script:
    - npm run clean
    - npm run build:client
    - npm run build:server


# 部署测试服务器
deploy_test:
  stage: deploy_test
  only:
    - develop
  script:
    - pm2 delete app || true
    - pm2 start app.js --name app


# 部署生产服务器
deploy_production:
  stage: deploy_production
  only:
    - master
  script:
    - bash scripts/deploy/deploy.sh

上面的配置把一次 Pipeline 分成五个阶段:

  • 安装依赖(install_deps)
  • 运行测试(test)
  • 编译(build)
  • 部署测试服务器(deploy_test)
  • 部署生产服务器(deploy_production)

设置 Job.only 后,只有当 develop 分支和 master 分支有提交的时候才会触发相关的 Jobs。
注意,我这里用 GitLab Runner 所在的服务器作为测试服务器。

参考资料

https://about.gitlab.com/gitlab-ci/
http://docs.gitlab.com/ce/ci/yaml/README.html
http://docs.gitlab.com/ce/ci/variables/README.html
https://gitlab.com/gitlab-org/gitlab-ci-multi-runner
https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/issues/1232
<http://stackbox.cn/2016-02-gitlab-ci-conf/

JavaScript 浮点数运算的精度问题

JavaScript 浮点数运算的精度问题

在使用JS发现某些浮点数运算的时候,得到的结果存在精度问题:比如0.1 + 0.2 = 0.30000000000000004以及7 * 0.8 = 5.6000000000000005等等。

什么原因造成了这个问题?实际上是因为计算机内部的信息都是由二进制方式表示的,即0和1组成的各种编码,但由于某些浮点数没办法用二进制准确的表示出来,也就带来了一系列精度问题。当然这也不是JS独有的问题

以0.1+0.2为例,理解浮点数的运算方法,如何规避这个问题。

计算机的运算方式

如何将小数转成二进制

**① 整数部分:**除2取余数,若商不为0则继续对它除2,当商为0时则将所有余数逆序排列;

**② 小数部分:**乘2取整数部分,若小数不为0则继续乘2,直至小数部分为0将取出的整数位正序排列。(若小数部分无法为零,根据有效位数要求取得相应数值,位数后一位0舍1入进行取舍)

利用上述方法,我们尝试一下将0.1转成二进制:

0.1 * 2 = 0.2 - - - - - - - - - - 取0

0.2 * 2 = 0.4 - - - - - - - - - - 取0

0.4 * 2 = 0.8 - - - - - - - - - - 取0

0.8 * 2 = 1.6 - - - - - - - - - - 取1

0.6 * 2 = 1.2 - - - - - - - - - - 取1

0.2 * 2 = 0.4 - - - - - - - - - - 取0

......

算到这就会发现小数部分再怎么继续乘都不会等于0,所以二进制是没办法精确表示0.1的

那么0.1的二进制表示是:0.000110011......0011...... (0011无限循环)

而0.2的二进制表示则是:0.00110011......0011...... (0011无限循环)

而具体应该保存多少位数,则需要根据使用的是什么标准来确定,也就是下一节所要讲到的内容。

IEEE 754 标准

IEEE 754 标准是IEEE二进位浮点数算术标准(IEEE Standard for Floating-Point Arithmetic)的标准编号。IEEE 754 标准规定了计算机程序设计环境中的二进制和十进制的浮点数自述的交换、算术格式以及方法。

根据IEEE 754标准,任意一个二进制浮点数都可以表示成以下形式:

img

S为数符,它表示浮点数的正负(0正1负);M为有效位(尾数);E为阶码,用移码表示,阶码的真值都被加上一个常数(偏移量)。

尾数部分M通常都是规格化表示的,即非"0"的尾数其第一位总是"1",而这一位也称隐藏位,因为存储时候这一位是会被省略的。比如保存1.0011时,只保存0011,等读取的时候才把第一位的1加上去,这样做相当于多保存了1位有效数字

常用的浮点格式有:

① 单精度:

img单精度浮点格式

这是32位的浮点数,最高的1位是符号位S,后面的8位是指数E,剩下的23位为尾数(有效数字)M;

真值为:

img

② 双精度:

img双精度浮点格式

这是64位的浮点数,最高的1位是符号位S,后面的11位是指数E,剩下的52位为尾数(有效数字)M;

真值为:

img

JavaScript只有一种数字类型number,而number使用的就是IEEE 754双精度浮点格式。依据上述规则,接下来我们就来看看JS是如何存储0.1和0.2的:

0.1是正数,所以符号位是0;

而其二进制位是0.000110011......0011...... (0011无限循环),进行规格化后为1.10011001......1001(1)*2^-4,根据0舍1入的规则,最后的值

2^-4 * 1.1001100110011001100110011001100110011001100110011010

指数E = -4 + 1023 = 1019

由此可得,JS中0.1的二进制存储格式为**(符号位用逗号分隔,指数位用分号分隔)**:

0,01111111011;1001100110011001100110011001100110011001100110011010

0.2则为:

0,01111111100;1001100110011001100110011001100110011001100110011010

***Q1:*指数位E(阶码)为何用移码表示?

***A1:*为了便于判断其大小。

浮点数运算

0.1 => 0,01111111011;1001100110011001100110011001100110011001100110011010

0.2 => 0,01111111100;1001100110011001100110011001100110011001100110011010

浮点数的加减运算按以下几步进行:

① 对阶,使两数的小数点位置对齐(也就是使两数的阶码相等)。

所以要先求阶差,阶小的尾数要根据阶差来右移**(尾数位移时可能会发生数丢失的情况,影响精度)**

因为0.1和0.2的阶码和尾数均为正数,所以它们的原码、反码及补码都是一样的。(使用补码进行运算,计算过程中使用双符号)

△阶差(补码) = 00,01111111011 - 00,01111111100 = 00,01111111011 + 11,10000000100 = 11,11111111111

由上可知△阶差为-1,也就是0.1的阶码比0.2的小,所以要把0.1的尾数右移1位,阶码加1(使0.1的阶码和0.2的一致)

最后0.1 => 0,01111111100;1100110011001100110011001100110011001100110011001101**(0)**

注:要注意0舍1入的原则。之所以右移一位,尾数补的是1,是因为隐藏位的数值为1(默认是不存储的,只有读取的时候才加上)

② 尾数求和

0.1100110011001100110011001100110011001100110011001101

+ 1.1001100110011001100110011001100110011001100110011010

——————————————————————————————

10.0110011001100110011001100110011001100110011001100111

③ 规格化

针对步骤②的结果,需要右规(即尾数右移1位,阶码加1)

sum = 0.1 + 0.2 = 0,01111111101;1.0011001100110011001100110011001100110011001100110011**(1)**

注:右规操作,可能会导致低位丢失,引起误差,造成精度问题。所以就需要步骤④的舍入操作

④ 舍入(0舍1入)

sum = 0,01111111101;1.0011001100110011001100110011001100110011001100110100

⑤ 溢出判断

根据阶码判断浮点运算是否溢出。而我们的阶码01111111101即不上溢,也不下溢。

至此,0.1+0.2的运算就已经结束了。接下来,我们一起来看看上面计算得到的结果,它的十进制数是多少。

<1> 先将它非规格化,得到二进制形式:

sum = 0.010011001100110011001100110011001100110011001100110100

<2> 再将其转成十进制

sum = 2^2 + 2^5 + 2^6 + ... + 2^52 = 0.30000000000000004440892098500626

现在你应该明白JS中 0.30000000000000004 这个结果怎么来的吧。

***Q2:*计算机运算为何要使用补码?

***A2:*可以简化计算机的运算步骤,且只用设加法器,如做减法时若能找到与负数等价的正数来代替该负数,就可以把减法操作用加法代替。而采用补码,就能达到这个效果。

浮点数精度问题的解决方法

  • 调用round() 方法四舍五入或者toFixed() 方法保留指定的位数(对精度要求不高,可用这种方法)
  • 将小数转为整数再做计算,即前文提到的那个简单的解决方案
  • 使用特殊的进制数据类型,如前文提到的bignumber(对精度要求很高,可借助这些相关的类库)

CSS 中矩阵变换 matrix()、matrix3d()

图形变换与线性代数息息相关(坐标系空间转换), 坐标变换与矩阵变换。在笛卡尔坐标系中,每个 欧氏空间 里的点都由横坐标和纵坐标这两个值来确定。在 CSS(和大部分的计算机图形学)中,原点 (0, 0) 在元素的左上角。每个点都使用数学上的向量符号 (x, y) 来描述。

每个线性函数使用 2 × 2 矩阵描述,如:

     [a c]
     [b d]

将矩阵乘法用于上述坐标系中的每个点,一个变换就形成了:

可以在一行中进行多次矩阵乘法进行变换:
图片描述

有了这种方法,就可以描述大部分常见的变换,并因此可以将他们组合起来,如:旋转、缩放或拉伸。事实上,所有线性函数的变换都可以被描述,组合的变换是从右到左生效的。然而,有一种常见的变换并不是线性的,所以当这种变换要用这种方法来表示时,应该被单独列出来:位移。位移的向量 (tx, ty) 必须单独表示,作为两个附加参数, 那我们描述矩阵会变成如下展示:

| a  c  tx |
| b  d  tx |
| 0  0   1 |

显而易见 transform 的属性是由 Matrix 矩阵通过参数计算出来

2D变换 matrix()

在 2D变换中,矩阵变换函数 matrix() 接受 6 个值,语法形式如下:

matrix(a, b, c, d, e, f)

齐次坐标 下相当于变换矩阵:

| a c e |
| b d f |
| 0 0 1 |

变换矩阵,如何进行线性坐标变换呢?设元素所呈现出来的几何图形中一点的坐标是 (x, y),那么所谓的根据变换矩阵进行变换就是使用这个点的坐标 (x, y) 的向量矩阵:

[x y 1 ]

与变换矩阵相乘:

坐标变换

在 2D变换中,变换总共有以下几种操作:

  • 平移:transform: translate(X, Y)
  • 旋转:transform: rotate(θ)
  • 倾斜:transform: skew(α, β)
  • 缩放:transform: scale(scaleX, scaleY)

这些对应的变换矩阵分别如下:

平移 translate

平移变换 translate(X, Y),相当于对其应用如下变换矩阵:

| 1 0 X |
| 0 1 Y |
| 0 0 1 |

即等价于使用矩阵变换函数 matrix(1, 0, 0, 1, X, Y)

旋转 rotate

旋转变换 rotate(θ),相当于对其应用如下变换矩阵:

| cosθ −sinθ 0 |
| sinθ cosθ  0 |
| 0 		0 	 1 |

即等价于矩阵变换函数 matrix(cosθ, sinθ, -sinθ, cosθ, 0, 0)

倾斜 skew

倾斜变换 skew(α, β),相当于对其应用如下变换矩阵:

| 1  tanα 0 |
| tanβ 1  0 |
| 0 	 0 	1 |

即等价于使用矩阵变换函数 matrix(1, tanβ, tanα, 1, 0, 0)

缩放 scale

缩放变换 scale(scaleX, scaleY),相当于对其应用如下变换矩阵:

| scaleX  0 0 |
| 0 scaleY  0 |
| 0 	    0 1 |

即等价于使用矩阵变换函数 matrix(scaleX, 0, 0, scaleY, 0, 0)

3D变换 matrix3d()

在 3D变换中,矩阵变换函数 matrix3d() 接受 16 个值,语法形式如下:

matrix3d(a1, b1, c1, d1, a2, b2, c2, d2, a3, b3, c3, d3, a4, b4, c4, d4)

注意: matrix(a, b, c, d, e, f)matrix3d(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, e, f, 0, 1) 的一个简写

齐次坐标 下相当于变换矩阵:

| a1	a2	a3	a4 |
| b1	b2	b3	b4 |
| c1	c2	c3	c4 |
| d1	d2	d3	d4 |

3D 矩阵坐标变换
3d矩阵坐标变换

在 3D变换中,变换总共有以下几种操作:

  • 平移:transform: translate3d(X, Y, Z)
  • 旋转:transform: rotate3d(X, Y, Z, θ)
  • 缩放:transform: scale3d(scaleX, scaleY, scaleZ)

这些对应的变换矩阵分别如下:

平移 translate3d

平移变换 translate3d(X, Y, Z),相当于对其应用如下变换矩阵:

| 1	0	0	X |
| 0	1	0	Y |
| 0	0	1	Z |
| 0	0	0	1 |

即等价于使用矩阵变换函数 matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, X, Y, Z, 1)

旋转 rotate3d

旋转变换 rotate3d(x, y, z, α),相当于对其应用如下变换矩阵:

旋转 rotate3d

缩放 scale3d

缩放变换 scale3d(scaleX, scaleY, scaleZ),相当于对其应用如下变换矩阵:

| scaleX	0			           0		0 |
| 0			scaleY	     0	            0 |
| 0				0	scaleZ	    0 |
| 0				0		  0	         1 |

即等价于使用矩阵变换函数 matrix3d(scaleX, 0, 0, 0, 0, scaleY, 0, 0, 0, 0, scaleZ, 0, 0, 0, 0, 1)

Other Resources

理解矩阵乘法

matrix()

matrix3d()

Transform Functions

笛卡尔坐标系

3D数学基础-向量运算基础和矩阵变换

Mac 端环境变量配置

Mac 使用 bash 做为默认的 shell,MAC OS 环境配置文件如下:

# 系统级别
/etc/profile
/etc/paths 
/etc/bashrc

# 用户级别
~/.bash_profile 
~/.bash_login 
~/.profile 

~/.bashrc

前三个是系统级别的环境变量针对所有用户,后面四个带有 ~/ 用户级别的环境变量。

  • 前三个系统级别环境配置会在系统启动时加载。
  • ~/.bash_profile~/.bash_login~/.profile 依次加载,若 ~/.bash_profile 不存在,依次加载后面几个文件;若 ~/.bash_profile 文件存在,后面几个文件不会加载
  • ~/.bashrc 在 bash shell 打开时加载

全局环境变量设置

修改全局环境变量时候参考系统默认的环境变量配置格式。

修改全局环境变量需要 root 权限。

  • /etc/paths 全局建议修改这个文件
  • /etc/profile 不建议修改这个文件,全局共有配置,用户登录时候都会加载该文件
  • /etc/bashrc 一般在这个文件中添加系统级别的环境变量,全局共有配置,bash shell 执行时候都会加载

用户级别环境变量设置

~/.bash_profile 中配置环境,格式如下:

# 使用冒号隔开 export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
export PATH=$PATH:<PATH 1>:<PATH 2>:<PATH 3>:------:<PATH N> 

# 或者 
export PATH=${PATH}:<PATH 1>
export PATH=${PATH}:<PATH 2>

# 第一种将路径合并在一起,不方便删除,建议使用第二种,换行挨个设置

查看 PATH

echo $PATH

命令行添加 PATH

# 覆盖是添加
echo 'export PATH=$HOME/openwhisk/bin:$PATH' > "$HOME/.bash_profile" 

# 增量式添加
echo 'eval "$(register-python-argcomplete wskadmin)"' >> "$HOME/.bash_profile"
echo 'export PKG_CONFIG_PATH="/usr/local/opt/libffi/lib/pkgconfig'>> "$HOME/.bash_profile"

重新载入配置文件

在环境配置完毕后,重载入配置文件生效,执行以下指令:

source <相应文件配置文件>

#示例
source .bash_profile

Other Resources

Mac 每次都要执行source ~/.bash_profile 配置的环境变量才生效

译文: Prefetching, preloading, prebrowsing

当提到前端性能优化时,我们首先会联想到文件的合并、压缩,文件缓存和开启服务器端的 gzip 压缩等,这使得页面加载更快,用户可以尽快使用我们的 Web 应用来达到他们的目标。
资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。

引用 Patrick Hamann解释

预加载是浏览器对将来可能被使用资源的一种暗示,一些资源可以在当前页面使用到,一些可能在将来的某些页面中被使用。作为开发人员,我们比浏览器更加了解我们的应用,所以我们可以对我们的核心资源使用该技术。

这种做法曾经被称为 prebrowsing,但这并不是一项单一的技术,可以细分为几个不同的技术:DNS-prefetchsubresource 和标准的 prefetchpreconnectprerender

DNS 预解析 DNS-Prefetch

通过 DNS 预解析来告诉浏览器未来我们可能从某个特定的 URL 获取资源,当浏览器真正使用到该域中的某个资源时就可以尽快地完成 DNS 解析。例如,我们将来可能从 example.com 获取图片或音频资源,那么可以在文档顶部的 <head> 标签中加入以下内容:

<link rel="dns-prefetch" href="//example.com">

当我们从该 URL 请求一个资源时,就不再需要等待 DNS 的解析过程。该技术对使用第三方资源特别有用。

Harry Roberts 的文章中提到:

通过简单的一行代码就可以告知那些兼容的浏览器进行 DNS 预解析,这意味着当浏览器真正请求该域中的某个资源时,DNS 的解析就已经完成了。

这似乎是一个非常微小的性能优化,显得也并非那么重要,但事实并非如此 – Chrome 一直都做了类似的优化。当在浏览器的地址栏中输入 URL 的一小段时,Chrome 就自动完成了 DNS 预解析(甚至页面预渲染),从而为每个请求节省了至关重要的时间。

预连接 Preconnect

与 DNS 预解析类似,preconnect 不仅完成 DNS 预解析,同时还将进行 TCP 握手和建立传输层协议。可以这样使用:

<link rel="preconnect" href="http://example.com">

在 Ilya Grigorik 的文章中有更详细的介绍:

现代浏览器都试着预测网站将来需要哪些连接,然后预先建立 socket 连接,从而消除昂贵的 DNS 查找、TCP 握手和 TLS 往返开销。然而,浏览器还不够聪明,并不能准确预测每个网站的所有预链接目标。好在,在 Firefox 39 和 Chrome 46 中我们可以使用 preconnect 告诉浏览器我们需要进行哪些预连接。

预获取 Prefetching

如果我们确定某个资源将来一定会被使用到,我们可以让浏览器预先请求该资源并放入浏览器缓存中。例如,一个图片和脚本或任何可以被浏览器缓存的资源:

<link rel="prefetch" href="image.png">

与 DNS 预解析不同,预获取真正请求并下载了资源,并储存在缓存中。但预获取还依赖于一些条件,某些预获取可能会被浏览器忽略,例如从一个非常缓慢的网络中获取一个庞大的字体文件。并且,Firefox 只会在浏览器闲置时进行资源预获取。

在 Bram Stein 的帖子中说到,这对 webfonts 性能提升非常明显。目前,字体文件必须等到 DOM 和 CSS 构建完成之后才开始下载,使用预获取就可以轻松绕过该瓶颈。

注意:要测试资源的预获取有点困难,但在 Chrome 和 Firefox 的网络面板中都有资源预获取的记录。还需要记住,预获取的资源没有同源策略的限制。

Subresources

这是另一个预获取方式,这种方式指定的预获取资源具有最高的优先级,在所有 prefetch 项之前进行:

<link rel="subresource" href="styles.css">

根据 Chrome 文档

rel=prefetch 为将来的页面提供了一种低优先级的资源预加载方式,而 rel=subresource 为当前页面提供了一种高优先级的资源预加载。

所以,如果资源是当前页面必须的,或者资源需要尽快可用,那么最好使用 subresource 而不是 prefetch

预渲染 Prerender

这是一个核武器,因为 prerender 可以预先加载文档的所有资源:

<link rel="prerender" href="http://example.com">

Steve Souders 在他的一篇文章中写到:

这类似于在一个隐藏的 tab 页中打开了某个链接 – 将下载所有资源、创建 DOM 结构、完成页面布局、应用 CSS 样式和执行 JavaScript 脚本等。当用户真正访问该链接时,隐藏的页面就切换为可见,使页面看起来就是瞬间加载完成一样。Google 搜索在其即时搜索页面中已经应用该技术多年了,微软也宣称将在 IE11 中支持该特性。

需要注意的是不要滥用该特性,当你知道用户一定会点击某个链接时才可以进行预渲染,否则浏览器将无条件地下载所有预渲染需要的资源。

更多相关讨论:

所有预加载技术都存在一个潜在的风险:对资源预测错误,而预加载的开销(抢占 CPU 资源,消耗电池,浪费带宽等)是高昂的,所以必须谨慎行事。虽然很难确定用户下一步将访问哪些资源,但高可信的场景确实存在:

  • 如果用户完成一个带有明显结果的搜索,那么结果页面很可能会被加载
  • 如果用户进入到登陆页面,那么登陆成功的页面很可能会被加载
  • 如果用户阅读一个多页的文章或访问一个分页的结果集,那么下一页很可能会被加载

最后,使用 Page Visibility API 可以防止页面真正可见前被执行。

Preload

preload 是一个新规范,与 prefetch 不同(可能被忽略)的是,浏览器一定会预加载该资源:

<link rel="preload" href="image.png">

虽然该规范还没有被所有浏览器兼容,但其背后的**还是非常有意思的。

总结

预测用户下一步将访问哪些资源是困难的,需要进行大量的测试,但是这带来的性能提升是明显的。如果我们愿意尝试这些预获取技术,一定会显著提升用户的体验。

参考资料

Slides from a talk by Ilya Grigorik called Preconnect, prerender, prefetch
MDN link prefetching FAQ
W3C preload spec
Harry Roberts on DNS prefetching
HTML5 prefetch
Preload hints for webfonts
w3c Resource Hints

十大排序排序算法

冒泡排序

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

export const bubbleSort = (arr = []) => {
    let len = arr.length
    for(let i = 0; i < len - 1; i++){
        for(let j = 0; j < len - 1 -i; j++ ){
            if(arr[j] > arr[j+1]){
                let temp = arr[j+1]
                arr[j+1] = arr[j]
                arr[j] = temp
            }
        }
    }
    return arr
}

选择排序

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

export const selectionSort = (arr = []) => {
    let len = arr.length
    let minIndex, temp
    for(let i = 0; i < len -1; i++){
        minIndex = i
        for(let j = i + 1; j < len; j++){
            if(arr[j] < arr[minIndex]){
                minIndex = j
            }
        }
        temp = arr[i]
        arr[i] = arr[minIndex]
        arr[minIndex] = temp
    }    
    return arr 
}

插入排序

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。+

插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

export const insertSort = (arr = []) => {
    let len = arr.length
    let preIndex, current
    for(let i = 1; i < len; i++){
        preIndex = i - 1
        current = arr[i]
        while(preIndex >= 0 && arr[preIndex] > current){
            arr[preIndex+1] = arr[preIndex]
            preIndex--
        }
        arr[preIndex+1] = current
    }
    return arr
}

希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;

  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

希尔排序的基本**是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

export const shellSort = (arr = []) => {
    let len = arr.length,
        temp,
        gap = 1;
    while(gap < len/3){
        gap = gap*3 + 1
    }
    for(gap; gap > 0; gap = Math.floor(gap/3)){
        for(let i = gap; i < len; i++){
            temp = arr[i]
            for(let j = i - gap; j >= 0 && arr[j] > temp; j -= gap){
                arr[j+gap] = arr[j]
            }
            arr[j+gap] = temp
        }
    }
    return arr
}

归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。+

作为一种典型的分而治之**的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;
const merge = (left, right) => {
    let result = []
    while (left.length && right.length) {
        if(left[0] <= right[0]){
            result.push(left.shift())
        }else{
            result.push(right.shift())
        }
    }

    return result.concat(left).concat(right)
}

export const mergeSort = (arr = []) => {
    let len = arr.length
    if(len <= 1)return arr 

    let middle = Math.floor(len / 2),
        left = arr(0, middle),
        right = arr(middle)
    return merge(mergeSort(left), mergeSort(right))
}

快速排序

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。+

快速排序又是一种分而治之**在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

export const quickSort = (arr = []) => {
    if(arr.length <= 1)return arr
    let pivotIndex = Math.floor(arr.length / 2) //选择中间数
    let pivot = arr.splice(pivotIndex, 1)[0]  //中间数从原数组删除并保存
    let left = [], right = []
    for(let i = 0; i < arr.length; i++){
        if(arr[i] < pivot){
            left.push(arr[i])
        }else{
            right.push(arr[i])
        }
    }
    return quickSort(left).concat([pivot],quickSort(right))
}

堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

1.大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
2.小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

let len

function buildMaxHeap(arr) {   // 建立大顶堆
    len = arr.length
    for (let i = Math.floor(len/2); i >= 0; i--) {
        heapify(arr, i)
    }
}

function heapify(arr, i) {     // 堆调整
    let left = 2 * i + 1,
        right = 2 * i + 2,
        largest = i

    if (left < len && arr[left] > arr[largest]) {
        largest = left
    }

    if (right < len && arr[right] > arr[largest]) {
        largest = right
    }

    if (largest != i) {
        swap(arr, i, largest)
        heapify(arr, largest)
    }
}

function swap(arr, i, j){
    let temp = arr [i]
    arr[i] = arr[j]
    arr[j] = temp  
}

export const heapSort = (arr=[]) => {
    buildMaxHead(arr)
    for(let i = arr.length-1; i > 0; i++){
        swap(arr, 0, i)
        len--
        heapify(arr, 0);
    }
    return arr
}

计数排序

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

export const countingSort = (arr = [], maxValue) => {
    let bucket = new Array(maxValue+1),
        sortedIndex = 0;
        arrLen = arr.length,
        bucketLen = maxValue + 1

    for (let i = 0; i < arrLen; i++) {
        if (!bucket[arr[i]]) {
            bucket[arr[i]] = 0
        }
        bucket[arr[i]]++
    }

    for (let j = 0; j < bucketLen; j++) {
        while(bucket[j] > 0) {
            arr[sortedIndex++] = j
            bucket[j]--
        }
    }

    return arr 
}

桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  • 在额外空间充足的情况下,尽量增大桶的数量

  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

export const bucketSort = (arr = [], bucketSize) => {
    if (arr.length === 0) {
      return arr
    }

    let i
    let minValue = arr[0]
    let maxValue = arr[0]
    for (i = 1; i < arr.length; i++) {
      if (arr[i] < minValue) {
          minValue = arr[i]               // 输入数据的最小值
      } else if (arr[i] > maxValue) {
          maxValue = arr[i]              // 输入数据的最大值
      }
    }

    //桶的初始化
    let DEFAULT_BUCKET_SIZE = 5            // 设置桶的默认数量为5
    bucketSize = bucketSize || DEFAULT_BUCKET_SIZE
    let bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1   
    let buckets = new Array(bucketCount)
    for (i = 0; i < buckets.length; i++) {
        buckets[i] = []
    }

    //利用映射函数将数据分配到各个桶中
    for (i = 0; i < arr.length; i++) {
        buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i])
    }

    arr.length = 0
    for (i = 0; i < buckets.length; i++) {
        insertionSort(buckets[i])                      // 对每个桶进行排序,这里使用了插入排序
        for (let j = 0; j < buckets[i].length; j++) {
            arr.push(buckets[i][j])                      
        }
    }

    return arr
}

基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

let counter = []
export const radixSort = (arr = [], maxDigit) => {
    let mod = 10
    let dev = 1
    for (let i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for(let j = 0; j < arr.length; j++) {
            let bucket = parseInt((arr[j] % mod) / dev);
            if(counter[bucket]==null) {
                counter[bucket] = []
            }
            counter[bucket].push(arr[j])
        }
        let pos = 0
        for(let j = 0; j < counter.length; j++) {
            let value = null
            if(counter[j]!=null) {
                while ((value = counter[j].shift()) != null) {
                      arr[pos++] = value
                }
          }
        }
    }
    return arr
}

参考资料

十大经典排序算法
js排序算法总结—冒泡,快速,选择,插入,希尔,归并

移动端 Scroll Event 思考

移动端 Scroll Event 思考

在移动端,我们遇到页面滚动 CSS3 animation 停止/ JS 挂起的情况,其实主要是移动端出于对性能的考虑,相当于自带防抖的功能,scroll 事件是在滚动停止之后才触发。

Android scroll

Android 4.0 及以下(主要是Android <= 2.3)的scroll事件不会实时滚动结束之后才执行,但Android 4.1之后scroll事件和PC几乎没什么区别

The Android browser in Ice Cream Sandwich fires the event but doesn’t feel very responsive and only sporadically re-paints the DOM to move the blue box. Luckily, Jelly Bean’s Android browser handles this example perfectly; everything is updated and rendered smoothly as the user scrolls.

IOS scroll

iOS < 8 滚动事件暂停了 CSS3 animation / JS scroll 事件,结束之后才会执行。

And keep in mind that we’re not just talking about the scroll event. iOS < 8 pauses all JavaScript execution during scrolling. Therefore, any intervals you create with setInterval() are also paused.

总结

通常使用scroll事件,场景常见于吸顶效果、滚动动画等。滚动吸顶由于目前 Android 与 iOS 对 scroll 事件改变滑动时实时触发,那么我们可以通过实时判断dom滚动高度做是否吸顶,不会出现抖动,而出于性能考虑,减少重新渲染重排和重绘,建议 iOS 使用 sticky 实现兼容性 iOS 以上。滚动动画这块,scroll事件变更之后,滚动动画不会挂起。

参考资料:

onscroll Event Issues on Mobile Browsers

Why the Scroll Event Change in iOS 8 is a Big Deal

React学习:状态(State) 和 属性(Props)

React :元素构成组件,组件又构成应用。
React 核心**是组件化,其中组件通过属性 (props) 和 状态 (state) 传递数据。

State 与 Props 区别

props 是组件对外的接口,state 是组件对内的接口。组件内可以引用其他组件,组件之间的引用形成了一个树状结构(组件树),如果下层组件需要使用上层组件的数据或方法,上层组件就可以通过下层组件的 props 属性进行传递,因此 props 是组件对外的接口。组件除了使用上层组件传递的数据外,自身也可能需要维护管理数据,这就是组件对内的接口 state。根据对外接口 props 和对内接口state,组件计算出对应界面的 UI。

主要区别:

  • State 是可变的,是一组用于反映组件UI变化的状态集合;
  • 而 Props 对于使用它的组件来说,是只读的,要想修改 Props,只能通过该组件的父组件修改。
    在组件状态上移的场景中,父组件正是通过子组件的 Props,传递给子组件其所需要的状态。

Props的使用

当一个组件被注入一些属性(Props )值时,属性值来源于它的父级元素,所以人们常说,属性在 React 中是单向流动的:从父级到子元素。

1.props (属性) 默认为 “true”

如果你没给 prop(属性) 传值,那么他默认为 true 。下面两个 JSX 表达式是等价的:

<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />

通常情况下,我们不建议使用这种类型,因为这会与 ES6 中的对象 shorthand 混淆 。ES6 shorthand 中 { foo } 指的是 { foo: foo } 的简写,而不是 { foo: true } 。这种行为只是为了与 HTML 的行为相匹配。
(举个例子,在 HTML 中,< input type=“radio” value=“1” disabled />< input type=“radio” value=“1” disabled=“true” /> 是等价的。JSX 中的这种行为就是为了匹配 HTML 的行为。)

2.props扩展

如果你已经有一个 object 类型的 props,并且希望在 JSX 中传入,你可以使用扩展操作符 … (JSX spread 用法) 传入整个 props 对象。这两个组件是等效的:

function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

显然下面的方法更方便:因为它将数据进行了包装,而且还简化了赋值的书写

State

State 是什么

React 的核心**是组件化,而组件中最重要的概念是 State,State 是一个组件的UI数据模型,是组件渲染时的数据依据。

状态(state) 和 属性(props) 类似,都是一个组件所需要的一些数据集合,但是state是私有的,可以认为state是组件的“私有属性(或者是局部属性)”。

如何判断是否为 State ?

组件中用到的一个变量是不是应该作为组件 State,可以通过下面的 4 条依据进行判断:

  • 这个变量是否是通过 Props 从父组件中获取 ?如果是,那么它不是一个状态。
  • 这个变量是否在组件的整个生命周期中都保持不变 ?如果是,那么它不是一个状态。
  • 这个变量是否可以通过其他状态(State)或者属性 (Props) 计算得到 ?如果是,那么它不是一个状态。
  • 这个变量是否在组件的render方法中使用?如果不是,那么它不是一个状态。这种情况下,这个变量更适合定义为组件的一个普通属性,例如组件中用到的定时器,就应该直接定义为 this.timer,而不是 this.state.timer。

并不是组件中用到的所有变量都是组件的状态!当存在多个组件共同依赖一个状态时,一般的做法是状态上移,将这个状态放到这几个组件的公共父组件中。

如何正确使用 State

1.用setState 修改State

直接修改 state,组件并不会重新触发 render()
// 错误
this.state.comment = 'Hello';

正确的修改方式是使用setState()

// 正确
this.setState({comment: 'Hello'});

2.State 的更新是异步的

  • 调用 setState 后,setState 会把要修改的状态放入一个队列中(因而组件的 state 并不会立即改变);
  • 之后 React 会优化真正的执行时机,来优化性能,所以优化过程中有可能会将多个 setState 的状态修改合并为一次状态修改,因而 State 更新可能是异步的。
  • 所以不要依赖当前的 State,计算下个 State。当真正执行状态修改时,依赖的this.state并不能保证是最新的State,因为 React 会把多次 State 的修改合并成一次,这时,this.state 将还是这几次 State 修改前的State。
    另外需要注意的事,同样不能依赖当前的 Props 计算下个状态,因为 Props 一般也是从父组件的 State 中获取,依然无法确定在组件状态更新时的值。

综上所述:
this.props 和 this.state 可能是异步更新的,你不能依赖他们的值计算下一个 state (状态)

例:这样 counter (计数器) 会更新失败

// 错误
this.setState({
  counter: this.state.counter + this.props.increment,
});

要弥补这个问题,使用 setState() 的另一种形式,它接受一个函数而不是一个对象。这个函数有两个参数:
(1)第一个参数: 是当前最新状态的前一个状态(本次组件状态修改前的状态)
(2)第二个参数:是当前最新的属性props

// 正确
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

//注意:下面这样是错的
this.setState((prevState, props) => { //没将{}用()括起来,所以会解析成代码块
  counter: prevState.counter + props.increment
});

如果你还不懂没关系,看下面例子:
我们现在渲染出一个button,想每点击一下,counter就+3
看下面代码:

class App extends React.Component {
  state = {
    counter: 0,
  }
  handleClick = () => {
    const { counter } = this.state;
    //或者 const counter = this.state.counter;
    this.setState({ counter: counter + 1 });
    this.setState({ counter: counter + 1 });
    this.setState({ counter: counter + 1 });
  }
  render() {
    return (
      <div>
        counter is: {this.state.counter}
        <button onClick={this.handleClick} >点我</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

每点击一下,加 +1,并不是 +3

之所以+1,不是 +3,是因为 state 的更新可能是异步的,React 会把传入多个 setState 的多个 Object “batch” 起来合并成一个。合并成一个就相当于把传入 setState 的多个 Object 进行 shallow merge,像这样:

const update = {
    counter: counter + 1,
    counter: counter + 1,
    counter: counter + 1
    //因为上面三句话都一样,所以会当一句话执行
 }

我们可以这么做就会成功:看下面

class App extends React.Component {
  state = {
    counter: 0,
  }
  handleClick = () => {
    this.setState(prev => ({ counter: prev.counter + 1 }));
    this.setState(prev => ({ counter: prev.counter + 1 }));
    this.setState(prev => ({ counter: prev.counter + 1 }));
    //这样是错的 this.setState(prev => {counter: prev.counter + 1});
    //这样是错的 this.setState(prev => {counter:++prev.counter});
    //这样是错的 this.setState(prev => {counter:prev.counter++});
  }
  render() {
    return (
      <div>
        counter is: {this.state.counter}
        <button onClick={this.handleClick} >点我</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

之所以成功是因为:传入多个 setState 的多个 Object 会被 shallow Merge,而传入多个 setState 的多个 function 会被 "queue" 起来,queue 里的 function 接收到的 state(上面是 prev )都是前一个 function 操作过的 state。

3.State更新会被合并

官方文档看不懂不要紧,直接举个例子你就懂了。

例如一个组件的状态为:

this.state = {
  title : 'React',
  content : 'React is an wonderful JS library!'
}

当只需要修改状态 title 时,只需要将修改后的 title 传给 setState:

this.setState({title: 'Reactjs'});

React 会合并新的 title 到原来的组件状态中,同时保留原有的状态 content,合并后的 State 为:

{
  title : 'Reactjs',
  content : 'React is an wonderful JS library!'
}

4.setState里顺序更新

  // history 为数组
   this.setState({
       history: history.concat([1]),  //(1)
       current: history.length,       //(2)
       nextPlayer: !nextPlayer,       //(3)
  });

执行 setState 时:先更新 history,然后再用更新改变后的 history 计算 current 的值,最后再更新 nextPlayer

根据 State 类型 更新

当状态发生变化时,如何创建新的状态?根据状态的类型,可以分成三种情况:

1.状态的类型是不可变类型(数字,字符串,布尔值,null, undefined)

这种情况最简单,直接给要修改的状态赋一个新值即可

// 原state
this.state = {
  count: 0,
  title : 'React',
  success:false
}

// 改变state
this.setState({
  count: 1,
  title: 'bty',
  success: true
})

2.状态的类型是数组

数组是一个引用,React 执行 diff 算法时比较的是两个引用,而不是引用的对象。所以直接修改原对象,引用值不发生改变的话,React 不会重新渲染。因此,修改状态的数组或对象时,要返回一个新的数组或对象。
(1)增加
如有一个数组类型的状态 books,当向 books 中增加一本书 (chinese) 时,使用数组的 concat 方法或 ES6 的数组扩展语法

// 方法一:将 state 先赋值给另外的变量,然后使用 concat 创建新数组
let books = this.state.books; 
this.setState({
  books: books.concat(['chinese'])
})

// 方法二:使用preState、concat创建新数组
this.setState(preState => ({
  books: preState.books.concat(['chinese'])
}))

// 方法三:ES6 spread syntax
this.setState(preState => ({
  books: [...preState.books, 'chinese']
}))

(2)截取
当从 books 中截取部分元素作为新状态时,使用数组的 slice 方法:

// 方法一:将state先赋值给另外的变量,然后使用slice创建新数组
let books = this.state.books; 
this.setState({
  books: books.slice(1,3)
})

// 方法二:使用preState、slice创建新数组
this.setState(preState => ({
  books: preState.books.slice(1,3)
}))

(3)条件过滤
当从 books 中过滤部分元素后,作为新状态时,使用数组的 filter 方法:

// 方法一:将state先赋值给另外的变量,然后使用filter创建新数组
var books = this.state.books; 
this.setState({
  books: books.filter(item => {
    return item != 'React'; 
  })
})

// 方法二:使用preState、filter创建新数组
this.setState(preState => ({
  books: preState.books.filter(item => {
    return item != 'React'; 
  })
}))

注意:不要使用 push、pop、shift、unshift、splice 等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而 concat、slice、filter 会返回一个新的数组。

3.状态的类型是普通对象(不包含字符串、数组)
对象是一个引用,React 执行 diff 算法时比较的是两个引用,而不是引用的对象。所以直接修改原对象,引用值不发生改变的话,React 不会重新渲染。因此,修改状态的数组或对象时,要返回一个新的对象。
使用 ES6 的 Object.assgin 方法

// 方法一:将state先赋值给另外的变量,然后使用Object.assign创建新对象
var owner = this.state.owner;
this.setState({
  owner: Object.assign({}, owner, {name: 'Jason'})
})

// 方法二:使用preState、Object.assign创建新对象
this.setState(preState => ({
  owner: Object.assign({}, preState.owner, {name: 'Jason'})
}))

使用对象扩展语法(object spread properties)

// 方法一:将state先赋值给另外的变量,然后使用对象扩展语法创建新对象
var owner = this.state.owner;
this.setState({
  owner: {...owner, name: 'Jason'}
})

// 方法二:使用preState、对象扩展语法创建新对象
this.setState(preState => ({
  owner: {...preState.owner, name: 'Jason'}
}))

综上所述: 创建新的状态对象的关键是,避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。

State 向下流动

我们说 props 是组件对外的接口,state 是组件对内的接口。
一个组件可以选择将 state(状态) 向下传递,作为其子组件的 props(属性):

<MyComponent title={this.state.title}/>

这通常称为一个“从上到下”,或者“单向”的数据流。任何 state(状态) 始终由某个特定组件所有,并且从该 state(状态) 导出的任何数据 或 UI 只能影响树中 “下方” 的组件。

如果把组件树想像为 props(属性) 的瀑布,所有组件的 state(状态) 就如同一个额外的水源汇入主流,且只能随着主流的方向向下流动。

嵌套组件树生命周期

父组件:

class Parent extends PureComponent {
  constructor(props) {
    super(props);
    console.log('Parent constructor');
  }
  
  getDerivedStateFromProps() {
    console.log('Parent shouldComponentUpdate');
  }
  
  shouldComponentUpdate() {
    console.log('Parent shouldComponentUpdate');
  }

  componentDidMount() {
    console.log('Parent componentDidMount');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('Parent componentDidUpdate(prevProps, prevState)');
  }

  componentWillUnmount() {
    console.log('Parent componentWillUnmount');
  }

  render() {
    console.log('Parent render');
    return (
      <div className="root">
          <h3>This is Parent</h3>
           <Child />
      </div>
    );
  }
}

子组件:

class Child extends PureComponent {
  constructor(props) {
    super(props);
    console.log('Child constructor');
  }

  getDerivedStateFromProps() {
    console.log('Child shouldComponentUpdate');
  }

  shouldComponentUpdate() {
    console.log('Child shouldComponentUpdate');
  }

  componentDidMount() {
    console.log('Child componentDidMount');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('Child componentDidUpdate(prevProps, prevState)');
  }

  componentWillUnmount() {
    console.log('Child componentWillUnmount');
  }

  render() {
    console.log('Child render');
    return (
      <div className="child">
        <h4>I am a Child</h4>
      </div>
    );
  }
}

运行后结果如下:

Parent constructor

Parent getDerivedStateFromProps

Parent shouldComponentUpdate

Parent render

Child constructor

Child getDerivedStateFromProps

Child shouldComponentUpdate

Child render

Child componentDidMount

Parent componentDidMount

此时可以分析出,当父组建 render 时遇到子组件,然后进入子组件的生命周期,当执行完子组件生命周期中的componentDidMount 时会回到父组建继续执行父组建未完成的生命周期。

由上面父子嵌套组件的生命周期流程,可以推断继续验证多级组件嵌套的流程。

stopImmediatePropagation、Event.initEvent()、element.dispatchEvent(event)简单记录

stopImmediatePropagation、Event.initEvent()、element.dispatchEvent(event)简单记录

创建和触发 events

Events 可以使用 Event构造函数 创建如下:

var event = new Event('build');

// Listen for the event.
elem.addEventListener('build', function (e) { ... }, false);

// Dispatch the event.
elem.dispatchEvent(event);

添加自定义数据 – CustomEvent()

CustomEvent 接口可以为 event 对象添加更多的数据。例如,event 可以创建如下:

var event = new CustomEvent('build', { 'detail': elem.dataset.time });

老式的方式: document.createEvent()、event.initEvent()已废弃

  var event = new MouseEvent('click', {
    'view': window,
    'bubbles': true,
    'cancelable': true
  });
  var cb = document.getElementById('checkbox');
  var cancelled = !cb.dispatchEvent(event);

stopImmediatePropagation

阻止当前事件的冒泡行为并且阻止当前事件所在元素上的所有相同类型事件的事件处理函数的继续执行.

fastclick解决点击延迟300ms的原理,首先,通常将事件绑定到 document.body(委托代理原理),其次,在代理过程中,通过stopImmediatePropagation移除目标节点绑定的事件, 最后是在touchend时,创建了一个鼠标事件,然后dispatchEvent事件(通过一系列的判断排除如长按、位置偏移,第二次点击等不触发click事件,反之出发sendClick()),使点击事件是在touchend时触发。

ps: dom节点多次绑定事件,当事件触发时会按事件绑定顺序依次执行。

//自定义事件,触发目标节点点击事件
FastClick.prototype.sendClick = function(targetElement, event) {
	var clickEvent, touch;

	// On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
	if (document.activeElement && document.activeElement !== targetElement) {
		document.activeElement.blur();
	}

	touch = event.changedTouches[0];

	// Synthesise a click event, with an extra attribute so it can be tracked
	clickEvent = document.createEvent('MouseEvents');
	clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
	clickEvent.forwardedTouchEvent = true;
	targetElement.dispatchEvent(clickEvent);
};

移动端300ms延迟由来及解决方案
事件模型
fastclick
event.stopImmediatePropagation
【读fastclick源码有感】彻底解决tap“点透”,提升移动端点击响应速度
创建和触发 events
创建和触发 events
点击穿透

MouseEvent.initMouseEvent() -- 已废弃
Event.initEvent() -- 已废弃

Python 版本管理

由于 Python 拥有众多的版本,以及不同模块也有不同的版本。同一模块不同版本有时需要的 Python 版本是不相同的,所以 Python 的版本控制显得尤为重要。

目前,常用的有以下三种工具进行 Python 版本管理:

virtualenv

virtualenv 用来为一个应用创建一套“隔离”的 Python 运行环境。

Install

pip3 install virtualenv

Create virtualenv

 # 创建一个名为ENV的目录 参数--no-site-packages 不复制已经安装到系统Python环境中的第三方包
virtualenv --no-site-packages ENV 

Activate virtualenv

source ENV/bin/activate

Exit virtualenv

deactivate

pyenv

pyenv 可以改变全局的 Python 版本,安装多个版本的 Python, 设置目录级别的 Python 版本,还能创建和管理 virtual python environments

pyenv项目是参考 rbenvruby-build 演变过来的。

Install

$ brew update
$ brew install pyenv

Common command

使用 pyenv commands 显示所有可用命令

pyenv versions # 查看本机安装版本
pyenv --version # 查看当前版本
pyenv install -l # 查看可安装 Python 版本
pyenv install 3.6.8 # 安装 python 3.6.8 版本
pyenv uninstall 3.6.8 # 卸载 python 3.6.8 版本

# python 版本切换 shell > local > global
pyenv global 3.6.8 # 设置全局的 Python 版本,版本号写入 ~/.pyenv/version 文件
pyenv local 3.6.8 # 设置 Python 本地版本,版本号写入当前目录下 .python-version 文件

pyenv-virtualenv

pyenv 插件:pyenv-virtualenv

Install

brew install pyenv-virtualenv

Create virtualenv

# 指定 Python 版本创建 virtualenv
pyenv virtualenv 2.7.10 my-virtual-env-2.7.10

# 当前 Python 版本创建 virtualenv
pyenv virtualenv venv34

List existing virtualenvs

pyenv shell venv34
pyenv virtualenvs

Activate virtualenv

pyenv activate <name>
pyenv deactivate

Delete existing virtualenv

# 删除 virtualenv 工作目录,或者运行以下方式
pyenv uninstall my-virtual-env
pyenv virtualenv-delete my-virtual-env

Anaconda

Anaconda 在英文中是“蟒蛇”,包管理器和环境管理器。Anaconda 附带了一大批常用数据科学包,附带了condanumpyscipyPython 在内的超过180个科学包及其依赖项。

Anaconda 是在 conda(一个包管理器和环境管理器)上发展出来的,拥有1,000+开源库(若不必要使用1,000多个库,那么可以考虑安装 Miniconda), Jupyter notebook 可以将数据分析的代码、图像和文档全部组合到一个web文档中

Install

Anaconda 可用于多个平台( Windows、Mac OS X 和 Linux)。可以在下面地址上找到安装程序和安装说明,根据你的操作系统是32位还是64位选择对应的版本下载。

官网地址:https://www.anaconda.com/distribution/

# 卸载 anaconda3
rm -rf ~/anaconda3

Common command

# 更新conda至最新版本
conda update conda

# 查看 conda 安装版本
conda --version 
conda -V

# 列出环境
conda env list
conda info -e
conda info --envs

# 当前环境中安装包
conda install <package_name>

# 指定环境中安装包
conda install -n <env_name> <package_name>

# 更新所有包
conda update --all
conda upgrade --all

# 更新指定包
conda update <package_name>
conda upgrade <package_name>

# 卸载当前环境中的包
conda remove <package_name>

# 卸载指定环境中的包
conda remove -n <env_name> <package_name>

conda install 无法进行安装时,可以使用pip进行安装。

pip只是包管理器,无法对环境进行管理,需先切换到指定环境,再使用pip命令安装包。pip无法更新Python,因为pip并不将 Python 视为包

Create env

conda create --n <env_name> <package_names>
# 例 创建环境名称为py3,并安装最新版本的Python3
conda create -n py3 python=3 

# 例 创建环境名称为py3,并安装最新版本的Python3.6,以及anaconda基础数据包
conda create -n py36 python=3.6 anaconda

Activate env

conda activate <env_name> 

'source activate' is deprecated. Use 'conda activate'

Exit env

conda deactivate

'source deactivate' is deprecated. Use 'conda deactivate'

Share env

# save environment
conda env export > /path/to/environment.yaml

# update environment
conda env update -f=/path/to/environment.yml

# install environment
pip install -r /path/to/environment.yml

Remove env

conda remove --n <env_name> --all

Install Jupyter

Jupyter Notebook 是基于网页的用于交互计算的应用程序。其可被应用于全过程计算:开发、文档编写、运行代码和展示结果

conda install jupyter notebook

Other Resources

Virtualenv Document

Jupyter Notebook

Anaconda

jupyter notebook 可以做哪些事情?

为什么现在更多需要用的是 GPU 而不是 CPU

学习 Restful HTTP API 设计

学习 Restful HTTP API 设计

近几年提供 HTTP API 服务的公司越来越多,许多公司都把 API 作为产品重要的一部分,作为服务提供出去。而微服务的兴起,也让企业内部开始重视和频繁使用 HTTP API 。好的 HTTP API设计容易理解、符合 RFC 标准、提供使用者便利的功能,其中经常被拿来作为教科书典范的当属 Github API。这篇文章就通过 Github API 总结了一些非常好的设计原则,可以作为以后要编写 HTTP API 的参考。

注意:这篇文章只讨论设计原则,不是强制要求(API 设计者可以根据实际情况实现部分内容,甚至实现出和某些原则相反的内容),也不会给出实现的思路和细节。

1. 使用 HTTPS

这个和 Restful API 本身没有很大的关系,但是对于增加网站的安全是非常重要的。特别如果你提供的是公开 API,用户的信息泄露或者被攻击会严重影响网站的信誉。

NOTE:不要让非SSL的url访问重定向到SSL的url。

2. API 地址和版本

url 中指定 API 的版本是个很好地做法。如果 API 变化比较大,可以把 API 设计为子域名,比如 https://api.github.com/v4;也可以简单地把版本放在路径中,比如 https://example.com/api/v1

3. schema

对于响应返回的格式,JSON 因为它的可读性、紧凑性以及多种语言支持等优点,成为了 HTTP API 最常用的返回格式。因此,最好采用 JSON 作为返回内容的格式。如果用户需要其他格式,比如 xml,应该在请求头部 Accept 中指定。对于不支持的格式,服务端需要返回正确的 status code,并给出详细的说明。

4. 以资源为中心的 URL 设计

资源是 Restful API 的核心元素,所有的操作都是针对特定资源进行的。而资源就是 URL(Uniform Resoure Locator)表示的,所以简洁、清晰、结构化的 URL 设计是至关重要的。Github 可以说是这方面的典范,下面我们就拿 repository 来说明。

/users/:username/repos
/users/:org/repos
/repos/:owner/:repo
/repos/:owner/:repo/tags
/repos/:owner/:repo/branches/:branch

我们可以看到几个特性:

  • 资源分为单个文档和集合,尽量使用复数来表示资源,单个资源通过添加 id 或者 name 等来表示
  • 一个资源可以有多个不同的 URL
  • 资源可以嵌套,通过类似目录路径的方式来表示,以体现它们之间的关系

NOTE: 根据RFC3986定义,URL是大小写敏感的。所以为了避免歧义,尽量使用小写字母。

5. 使用正确的 Method

有了资源的 URL 设计,所有针对资源的操作都是使用 HTTP 方法指定的。比较常用的方法有:

VERB 描述
HEAD 只获取某个资源的头部信息。比如只想了解某个文件的大小,某个资源的修改日期等
GET 获取资源
POST 创建资源
PATCH 更新资源的部分属性。因为 PATCH 比较新,而且规范比较复杂,所以真正实现的比较少,一般都是用 POST 替代
PUT 替换资源,客户端需要提供新建资源的所有属性。如果新内容为空,要设置 Content-Length 为 0,以区别错误信息
DELETE 删除资源

比如:

GET /repos/:owner/:repo/issues
GET /repos/:owner/:repo/issues/:number
POST /repos/:owner/:repo/issues
PATCH /repos/:owner/:repo/issues/:number
DELETE /repos/:owner/:repo

NOTE:更新和创建操作应该返回最新的资源,来通知用户资源的情况;删除资源一般不会返回内容。

不符合 CRUD 的情况

在实际资源操作中,总会有一些不符合 CRUD(Create-Read-Update-Delete) 的情况,一般有几种处理方法。

使用 POST

为需要的动作增加一个 endpoint,使用 POST 来执行动作,比如 POST /resend 重新发送邮件。

增加控制参数

添加动作相关的参数,通过修改参数来控制动作。比如一个博客网站,会有把写好的文章“发布”的功能,可以用上面的 POST /articles/{:id}/publish 方法,也可以在文章中增加 published:boolean 字段,发布的时候就是更新该字段 PUT /articles/{:id}?published=true

把动作转换成资源

把动作转换成可以执行 CRUD 操作的资源, github 就是用了这种方法。

比如“喜欢”一个 gist,就增加一个 /gists/:id/star 子资源,然后对其进行操作:“喜欢”使用 PUT /gists/:id/star,“取消喜欢”使用 DELETE /gists/:id/star

另外一个例子是 Fork,这也是一个动作,但是在 gist 下面增加 forks资源,就能把动作变成 CRUD 兼容的:POST /gists/:id/forks 可以执行用户 fork 的动作。

6. Query 让查询更自由

比如查询某个 repo 下面 issues 的时候,可以通过以下参数来控制返回哪些结果:

  • state:issue 的状态,可以是 openclosedall
  • since:在指定时间点之后更新过的才会返回
  • assignee:被 assign 给某个 user 的 issues
  • sort:选择排序的值,可以是 createdupdatedcomments
  • direction:排序的方向,升序(asc)还是降序(desc)
  • ……

7. 分页 Pagination

当返回某个资源的列表时,如果要返回的数目特别多,比如 github 的 /users,就需要使用分页分批次按照需要来返回特定数量的结果。

分页的实现会用到上面提到的 url query,通过两个参数来控制要返回的资源结果:

  • per_page:每页返回多少资源,如果没提供会使用预设的默认值;这个数量也是有一个最大值,不然用户把它设置成一个非常大的值(比如 99999999)也失去了设计的初衷
  • page:要获取哪一页的资源,默认是第一页

返回的资源列表为 [(page-1)*per_page, page*per_page)。github API 文档中还提到一个很好的点,相关的分页信息还可以存放到 Link 头部,这样客户端可以直接得到诸如下一页最后一页上一页等内容的 url 地址,而不是自己手动去计算和拼接。

8. 选择合适的状态码

HTTP 应答中,需要带一个很重要的字段:status code。它说明了请求的大致情况,是否正常完成、需要进一步处理、出现了什么错误,对于客户端非常重要。状态码都是三位的整数,大概分成了几个区间:

  • 2XX:请求正常处理并返回
  • 3XX:重定向,请求的资源位置发生变化
  • 4XX:客户端发送的请求有错误
  • 5XX:服务器端错误

在 HTTP API 设计中,经常用到的状态码以及它们的意义如下表:

状态码 LABEL 解释
200 OK 请求成功接收并处理,一般响应中都会有 body
201 Created 请求已完成,并导致了一个或者多个资源被创建,最常用在 POST 创建资源的时候
202 Accepted 请求已经接收并开始处理,但是处理还没有完成。一般用在异步处理的情况,响应 body 中应该告诉客户端去哪里查看任务的状态
204 No Content 请求已经处理完成,但是没有信息要返回,经常用在 PUT 更新资源的时候(客户端提供资源的所有属性,因此不需要服务端返回)。如果有重要的 metadata,可以放到头部返回
301 Moved Permanently 请求的资源已经永久性地移动到另外一个地方,后续所有的请求都应该直接访问新地址。服务端会把新地址写在 Location 头部字段,方便客户端使用。允许客户端把 POST 请求修改为 GET。
304 Not Modified 请求的资源和之前的版本一样,没有发生改变。用来缓存资源,和条件性请求(conditional request)一起出现
307 Temporary Redirect 目标资源暂时性地移动到新的地址,客户端需要去新地址进行操作,但是不能修改请求的方法。
308 Permanent Redirect 和 301 类似,除了客户端不能修改原请求的方法
400 Bad Request 客户端发送的请求有错误(请求语法错误,body 数据格式有误,body 缺少必须的字段等),导致服务端无法处理
401 Unauthorized 请求的资源需要认证,客户端没有提供认证信息或者认证信息不正确
403 Forbidden 服务器端接收到并理解客户端的请求,但是客户端的权限不足。比如,普通用户想操作只有管理员才有权限的资源。
404 Not Found 客户端要访问的资源不存在,链接失效或者客户端伪造 URL 的时候回遇到这个情况
405 Method Not Allowed 服务端接收到了请求,而且要访问的资源也存在,但是不支持对应的方法。服务端必须返回 Allow 头部,告诉客户端哪些方法是允许的
415 Unsupported Media Type 服务端不支持客户端请求的资源格式,一般是因为客户端在 Content-Type 或者 Content-Encoding 中申明了希望的返回格式,但是服务端没有实现。比如,客户端希望收到 xml返回,但是服务端支持 Json
429 Too Many Requests 客户端在规定的时间里发送了太多请求,在进行限流的时候会用到
500 Internal Server Error 服务器内部错误,导致无法完成请求的内容
503 Service Unavailable 服务器因为负载过高或者维护,暂时无法提供服务。服务器端应该返回 Retry-After 头部,告诉客户端过一段时间再来重试

上面这些状态码覆盖了 API 设计中大部分的情况,如果对某个状态码不清楚或者希望查看更完整的列表,可以参考 HTTP Status Code 这个网站,或者 RFC7231 Response Status Codes 的内容。

9. 错误处理:给出详细的信息

如果出错的话,在 response body 中通过 message 给出明确的信息。

比如客户端发送的请求有错误,一般会返回 4XX Bad Request 结果。这个结果很模糊,给出错误 message 的话,能更好地让客户端知道具体哪里有问题,进行快速修改。

  • 如果请求的 JSON 数据无法解析,会返回 Problems parsing JSON
  • 如果缺少必要的 filed,会返回 422 Unprocessable Entity,除了 message 之外,还通过 errors 给出了哪些 field 缺少了,能够方便调用方快速排错

基本的思路就是尽可能提供更准确的错误信息:比如数据不是正确的 json,缺少必要的字段,字段的值不符合规定…… 而不是直接说“请求错误”之类的信息。

10. 验证和授权

一般来说,让任何人随意访问公开的 API 是不好的做法。验证和授权是两件事情:

  • 验证(Authentication)是为了确定用户是其申明的身份,比如提供账户的密码。不然的话,任何人伪造成其他身份(比如其他用户或者管理员)是非常危险的
  • 授权(Authorization)是为了保证用户有对请求资源特定操作的权限。比如用户的私人信息只能自己能访问,其他人无法看到;有些特殊的操作只能管理员可以操作,其他用户有只读的权限等等

如果没有通过验证(提供的用户名和密码不匹配,token 不正确等),需要返回 401 Unauthorized状态码,并在 body 中说明具体的错误信息;而没有被授权访问的资源操作,需要返回 403 Forbidden 状态码,还有详细的错误信息。

NOTE:Github API 对某些用户未被授权访问的资源操作返回 404 Not Found,目的是为了防止私有资源的泄露(比如黑客可以自动化试探用户的私有资源,返回 403 的话,就等于告诉黑客用户有这些私有的资源)。

11. 限流 rate limit

如果对访问的次数不加控制,很可能会造成 API 被滥用,甚至被 DDos 攻击。根据使用者不同的身份对其进行限流,可以防止这些情况,减少服务器的压力。

对用户的请求限流之后,要有方法告诉用户它的请求使用情况,Github API 使用的三个相关的头部:

  • X-RateLimit-Limit: 用户每个小时允许发送请求的最大值
  • X-RateLimit-Remaining:当前时间窗口剩下的可用请求数目
  • X-RateLimit-Rest: 时间窗口重置的时候,到这个时间点可用的请求数量就会变成 X-RateLimit-Limit 的值

如果允许没有登录的用户使用 API(可以让用户试用),可以把 X-RateLimit-Limit 的值设置得很小,比如 Github 使用的 60。没有登录的用户是按照请求的 IP 来确定的,而登录的用户按照认证后的信息来确定身份。

对于超过流量的请求,可以返回 429 Too many requests 状态码,并附带错误信息。而 Github API 返回的是 403 Forbidden,虽然没有 429 更准确,也是可以理解的。

Github 更进一步,提供了不影响当然 RateLimit 的请求查看当前 RateLimit 的接口 GET /rate_limit

12. Hypermedia API

Restful API 的设计最好做到 Hypermedia:在返回结果中提供相关资源的链接。这种设计也被称为 HATEOAS。这样做的好处是,用户可以根据返回结果就能得到后续操作需要访问的地址。

比如访问 api.github.com,就可以看到 Github API 支持的资源操作。

13. 编写优秀的文档

API 最终是给人使用的,不管是公司内部,还是公开的 API 都是一样。即使我们遵循了上面提到的所有规范,设计的 API 非常优雅,用户还是不知道怎么使用我们的 API。最后一步,但非常重要的一步是:为你的 API 编写优秀的文档。

对每个请求以及返回的参数给出说明,最好给出一个详细而完整地示例,提醒用户需要注意的地方……反正目标就是用户可以根据你的文档就能直接使用 API,而不是要发邮件给你,或者跑到你的座位上问你一堆问题。

参考资料

JavaScript 内存机制

JavaScript 内存机制

简介

每种编程语言都有它的内存管理机制,比如简单的C有低级的内存管理基元,像malloc(),free()。同样我们在学习JavaScript的时候,很有必要了解JavaScript的内存管理机制。 JavaScript的内存管理机制是:内存基元在变量(对象,字符串等等)创建时分配,然后在他们不再被使用时“自动”释放。后者被称为垃圾回收。这个“自动”是混淆并给JavaScript(和其他高级语言)开发者一个错觉:他们可以不用考虑内存管理。 对于前端开发来说,内存空间并不是一个经常被提及的概念,很容易被大家忽视。当然也包括我自己。在很长一段时间里认为内存空间的概念在JS的学习中并不是那么重要。可是后我当我回过头来重新整理JS基础时,发现由于对它们的模糊认知,导致了很多东西我都理解得并不明白。比如最基本的引用数据类型和引用传递到底是怎么回事儿?比如浅复制与深复制有什么不同?还有闭包,原型等等。 但其实在使用JavaScript进行开发的过程中,了解JavaScript内存机制有助于开发人员能够清晰的认识到自己写的代码在执行的过程中发生过什么,也能够提高项目的代码质量。

内存模型

JS内存空间分为栈(stack)堆(heap)池(一般也会归类为栈中)。 其中存放变量,存放复杂对象,存放常量。

基础数据类型与栈内存

JS中的基础数据类型,这些值都有固定的大小,往往都保存在栈内存中(闭包除外),由系统自动分配存储空间。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问 数据在栈内存中的存储与使用方式类似于数据结构中的堆栈数据结构,遵循后进先出的原则。 基础数据类型: Number String Null Undefined Boolean 复习一下,此问题常常在面试中问到,然而答不出来的人大有人在 ~ ~ 要简单理解栈内存空间的存储方式,我们可以通过类比乒乓球盒子来分析。

乒乓球盒子
5
4
3
2
1

这种乒乓球的存放方式与栈中存取数据的方式如出一辙。处于盒子中最顶层的乒乓球5,它一定是最后被放进去,但可以最先被使用。而我们想要使用底层的乒乓球1,就必须将上面的4个乒乓球取出来,让乒乓球1处于盒子顶层。这就是栈空间先进后出,后进先出的特点。

引用数据类型与堆内存

与其他语言不同,JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JS不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以粗浅地理解为保存在栈内存中的一个地址,该地址与堆内存的实际值相关联。 堆存取数据的方式,则与书架与书非常相似。 书虽然也有序的存放在书架上,但是我们只要知道书的名字,我们就可以很方便的取出我们想要的书,而不用像从乒乓球盒子里取乒乓一样,非得将上面的所有乒乓球拿出来才能取到中间的某一个乒乓球。好比在JSON格式的数据中,我们存储的key-value是可以无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书的名字。

为了更好的搞懂栈内存与堆内存,我们可以结合以下例子与图解进行理解。
var a1 = 0; // 栈
var a2 = 'this is string'; // 栈
var a3 = null; // 栈
var b = { m: 20 }; // 变量b存在于栈中,{m: 20} 作为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于栈中,[1, 2, 3] 作为对象存在于堆内存中

变量名 具体值
c 0x0012ff7d
b 0x0012ff7c
a3 null
a2 this is string
a1 0

[栈内存空间] ------->

        堆内存空间
        [1,2,3]           
                    {m:20}           

因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从栈中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。 理解了JS的内存空间,我们就可以借助内存空间的特性来验证一下引用类型的一些特点了。 在前端面试中我们常常会遇到这样一个类似的题目

// demo01.js
var a = 20;
var b = a;
b = 30;
// 这时a的值是多少?

// demo02.js
var m = { a: 10, b: 20 };
var n = m;
n.a = 15;
// 这时m.a的值是多少

在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a执行之后,ab虽然值都等于20,但是他们其实已经是相互独立互不影响的值了。具体如图。所以我们修改了b的值以后,a的值并不会发生变化。

栈内存空间
a 20

[复制前]

栈内存空间
b 20
a 20

[复制后]

栈内存空间
b 30
a 20

[b值修改后]
这是 demo1 的图解

在demo02中,我们通过var n = m执行一次复制引用类型的操作。引用类型的复制同样也会为新的变量自动分配一个新的值保存在栈内存中,但不同的是,这个新的值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管他们相互独立,但是在堆内存中访问到的具体对象实际上是同一个。 |栈内存空间|| |变量名|具体值|

m 0x0012ff7d

[复制前]

堆内存空间
{a:10,b:20}

[复制前]

栈内存空间
变量名
m
n

[复制后]

堆内存空间
{a:10,b:20}

[复制后]

这是demo2图解

除此之外,我们还可以以此为基础,一步一步的理解JavaScript的执行上下文,作用域链,闭包,原型链等重要概念。其他的以后再说,光做这个就累死了。

内存的生命周期

JS环境中分配的内存一般有如下生命周期:

  1. 内存分配:当我们申明变量、函数、对象的时候,系统会自动为他 们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存

为了便于理解,我们使用一个简单的例子来解释这个周期。

var a = 20;  // 在内存中给数值变量分配空间
alert(a + 100);  // 使用内存
var a = null; // 使用完毕之后,释放内存空间

第一步和第二步我们都很好理解,JavaScript在定义变量时就完成了内存分配。第三步释放内存空间则是我们需要重点理解的一个点。

现在想想,从内存来看 nullundefined 本质的区别是什么?

为什么typeof(null) //object typeof(undefined) //undefined

现在再想想,构造函数和立即执行函数的声明周期是什么?

对了,ES6语法中的 const 声明一个只读的常量。一旦声明,常量的值就不能改变。但是下面的代码可以改变 const 的值,这是为什么?

const foo = {}; 
foo.prop = 123;
foo.prop // 123
foo = {}; // TypeError: "foo" is read-only

内存回收

JavaScript有自动垃圾收集机制,那么这个自动垃圾收集机制的原理是什么呢?其实很简单,就是找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。 在JavaScript中,最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,因此 a = null 其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。而在适当的时候解除引用,是为页面获得更好性能的一个重要方式。

  • 在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量,以确保性能问题。
  • 以Google的V8引擎为例,在V8引擎中所有的JAVASCRIPT对象都是通过堆来进行内存分配的。当我们在代码中声明变量并赋值时,V8引擎就会在堆内存中分配一部分给这个变量。如果已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止(默认情况下,V8引擎的堆内存的大小上限在64位系统中为1464MB,在32位系统中则为732MB)。
  • 另外,V8引擎对堆内存中的JAVASCRIPT对象进行分代管理。新生代:新生代即存活周期较短的JAVASCRIPT对象,如临时变量、字符串等; 老生代:老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

请各位老铁see一下以下的代码,来分析一下垃圾回收。

function fun1() {
    var obj = {name: 'csa', age: 24};
}
 
function fun2() {
    var obj = {name: 'coder', age: 2}
    return obj;
}
 
var f1 = fun1();
var f2 = fun2();

在上述代码中,当执行var f1 = fun1();的时候,执行环境会创建一个{name:'csa', age:24}这个对象,当执行var f2 = fun2();的时候,执行环境会创建一个{name:'coder', age=2}这个对象,然后在下一次垃圾回收来临的时候,会释放{name:'csa', age:24}这个对象的内存,但并不会释放{name:'coder', age:2}这个对象的内存。这就是因为在fun2()函数中将{name:'coder, age:2'}这个对象返回,并且将其引用赋值给了f2变量,又由于f2这个对象属于全局变量,所以在页面没有卸载的情况下,f2所指向的对象{name:'coder', age:2}是不会被回收的。 由于JavaScript语言的特殊性(闭包...),导致如何判断一个对象是否会被回收的问题上变的异常艰难,各位老铁看看就行。

垃圾回收算法

对垃圾回收算法来说,核心**就是如何判断内存已经不再使用了。

引用计数算法

熟悉或者用C语言搞过事的同学的都明白,引用无非就是指向某一物体的指针。对不熟悉这个语言的同学来说,可简单将引用视为一个对象访问另一个对象的路径。(这里的对象是一个宽泛的概念,泛指JS环境中的实体)。

引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。如果没有其他对象指向它了,说明该对象已经不再需了。

老铁们来看一个例子:

// 创建一个对象person,他有两个指向属性age和name的引用
var person = {
    age: 12,
    name: 'aaaa'
};

person.name = null; // 虽然设置为null,但因为person对象还有指向name的引用,因此name不会回收

var p = person; 
person = 1;         //原来的person对象被赋值为1,但因为有新引用p指向原person对象,因此它不会被回收

p = null;           //原person对象已经没有引用,很快会被回收

由上面可以看出,引用计数算法是个简单有效的算法。但它却存在一个致命的问题:循环引用。如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。

老铁们再来看一个例子:

function cycle() {
    var o1 = {};
    var o2 = {};
    o1.a = o2;
    o2.a = o1; 

    return "Cycle reference!"
}

cycle();

上面我们申明了一个cycle方程,其中包含两个相互引用的对象。在调用函数结束后,对象o1和o2实际上已离开函数范围,因此不再需要了。但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收,内存泄露不可避免了。 正是因为有这个严重的缺点,这个算法在现代浏览器中已经被下面要介绍的标记清除算法所取代了。但绝不可认为该问题已经不再存在了,因为还占有大量市场的IE老祖宗们使用的正是这一算法。在需要照顾兼容性的时候,某些看起来非常普通的写法也可能造成意想不到的问题:

var div = document.createElement("div");
div.onclick = function() {
    console.log("click");
};

上面这种JS写法再普通不过了,创建一个DOM元素并绑定一个点击事件。那么这里有什么问题呢?请注意,变量div有事件处理函数的引用,同时事件处理函数也有div的引用!(div变量可在函数内被访问)。一个循序引用出现了,按上面所讲的算法,该部分内存无可避免地泄露哦了。 现在你明白为啥前端程序员都讨厌IE了吧?拥有超多BUG并依然占有大量市场的IE是前端开发一生之敌!亲,没有买卖就没有杀害。

标记清除算法

上面说过,现代的浏览器已经不再使用引用计数算法了。现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体**都是一致的。

标记清除算法将“不再使用的对象”定义为“无法达到的对象”。简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。但反之未必成立。

根据这个概念,上面的例子可以正确被垃圾回收处理了(亲,想想为什么?)。

当div与其时间处理函数不能再从全局对象出发触及的时候,垃圾回收器就会标记并回收这两个对象。

如何写出对内存管理友好的JS代码?

如果还需要兼容老旧浏览器,那么就需要注意代码中的循环引用问题。或者直接采用保证兼容性的库来帮助优化代码。

对现代浏览器来说,唯一要注意的就是明确切断需要回收的对象与根部的联系。有时候这种联系并不明显,且因为标记清除算法的强壮性,这个问题较少出现。最常见的内存泄露一般都与DOM元素绑定有关:

email.message = document.createElement(“div”);
displayList.appendChild(email.message);

// 稍后从displayList中清除DOM元素
displayList.removeAllChildren();

div元素已经从DOM树中清除,也就是说从DOM树的根部无法触及该div元素了。但是请注意,div元素同时也绑定了email对象。所以只要email对象还存在,该div元素将一直保存在内存中。

小结

如果你的引用只包含少量JS交互,那么内存管理不会对你造成太多困扰。一旦你开始构建中大规模的 SPA 或是服务器和桌面端的应用,那么就应当将内存泄露提上日程了。不要满足于写出能运行的程序,也不要认为机器的升级就能解决一切。

内存泄露

什么是内存泄露

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。 有些语言(比如 C 语言)必须手动释放内存,程序员负责内存管理。

char * buffer;
buffer = (char*) malloc(42);

// Do something with buffer

free(buffer);

看不懂没关系,上面是 C 语言代码,malloc方法用来申请内存,使用完毕之后,必须自己用free方法释放内存。 这很麻烦,所以大多数语言提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制"(garbage collector),已经提过,不再多讲。

内存泄漏的识别方法

怎样可以观察到内存泄漏呢? 经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。这要我们实时查看内存占用。

浏览器方法

  1. 打开开发者工具,选择 Timeline 面板
  2. 在顶部的Capture字段里面勾选 Memory
  3. 点击左上角的录制按钮。
  4. 在页面上进行各种操作,模拟用户的使用情况。
  5. 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。

如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。 反之,就是内存泄漏了。

命令行方法

命令行可以使用 Node 提供的 process.memoryUsage 方法。

console.log(process.memoryUsage());
// { rss: 27709440,
//  heapTotal: 5685248,
//  heapUsed: 3449392,
//  external: 8772 }

process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,含义如下。

Resident Set(常驻内存)
Code Segment(代码区)
Stack(Local Variables, Pointers)
Heap(Objects, Closures)
Used Heap
  • rss(resident set size):所有内存占用,包括指令区和堆栈。
  • heapTotal:"堆"占用的内存,包括用到的和没用到的。
  • heapUsed:用到的堆的部分。
  • external: V8 引擎内部的 C++ 对象占用的内存。

判断内存泄漏,以heapUsed字段为准。

WeakMap

前面说过,及时清除引用非常重要。但是,你不可能记得那么多,有时候一疏忽就忘了,所以才有那么多内存泄漏。

最好能有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用可以忽略不计,当其他引用消失以后,垃圾回收机制就可以释放内存。这样就能大大减轻程序员的负担,你只要清除主要引用就可以了。

ES6 考虑到了这一点,推出了两种新的数据结构:WeakSetWeakMap。它们对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个"Weak",表示这是弱引用。

下面以 WeakMap 为例,看看它是怎么解决内存泄漏的。

const wm = new WeakMap();

const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"

上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。

也就是说,DOM 节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。

基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap

WeakMap 示例

WeakMap 的例子很难演示,因为无法观察它里面的引用会自动消失。此时,其他引用都解除了,已经没有引用指向 WeakMap 的键名了,导致无法证实那个键名是不是存在。 (具体可以去看阮一峰老师的内存泄露文章)。

特别感谢:

npx 是什么

npm v5.2.0引入的一条命令(npx),引入这个命令的目的是为了提升开发者使用包内提供的命令行工具的体验。

举例:使用create-react-app创建一个react项目。

老方法:

npm install -g create-react-app
create-react-app my-app

npx方式:

npx create-react-app my-app

这条命令会临时安装 create-react-app 包,命令完成后create-react-app 会删掉,不会出现在 global 中。下次再执行,还是会重新临时安装。

npx 会帮你执行依赖包里的二进制文件。

举例来说,之前我们可能会写这样的命令:

npm i -D webpack
./node_modules/.bin/webpack -v

如果你对 bash 比较熟,可能会写成这样:

npm i -D webpack
`npm bin`/webpack -v

有了 npx,你只需要这样:

npm i -D webpack
npx webpack -v

也就是说 npx 会自动查找当前依赖包中的可执行文件,如果找不到,就会去 PATH 里找。如果依然找不到,就会帮你安装!

npx 甚至支持运行远程仓库的可执行文件:

npx github:piuccio/cowsay hello

再比如 npx http-server 可以一句话帮你开启一个静态服务器!(第一次运行会稍微慢一些)

npx http-server

指定node版本来运行npm scripts

npx -p node@8 npm run build

主要特点:

1、临时安装可执行依赖包,不用全局安装,不用担心长期的污染。
2、可以执行依赖包中的命令,安装完成自动运行。
3、自动加载node_modules中依赖包,不用指定$PATH。
4、可以指定node版本、命令的版本,解决了不同项目使用不同版本的命令的问题。

npx 使用教程

eval()与new Function()

eval()与new Function()

eval

eval 接受字符串参数,可将任意字符串当做一个JavaScript代码来执行。使用 eval存在一些安全隐患,可能执行被篡改过的代码,严格模式是不允许的。

function f(){
	var foo = 1
    eval( 'foo++' );  //不是一种沙盒状态,会影响上下级作用域,可读取上级作用域,影响下层作用域
    console.log( foo ); // 1
}

eval实际使用转换JSON

var jsonStr = '{ "age": 20, "name": "jack" }';
eval('(' + jsonStr + ')');
//{age: 20, name: "jack"}

function getData(data){
	return data
}
//注意这里用了上面的作用域 getData方法
eval('try{getData({ "age": 20, "name": "jack" })}catch(e){}')

//{age: 20, name: "jack"}

为什么要加括号呢?
因为js中{}通常是表示一个语句块,eval只会计算语句块内的值进行返回。加上括号就变成一个整体的表达式。

console.log( eval('{}') );      // undefind
console.log( eval('({})') );    // Object {}

Function 构造函数

Function 构造函数 创建一个新的Function对象。 在 JavaScript 中, 每个函数实际上都是一个Function对象。

语法:new Function ([arg1[, arg2[, ...argN]],] functionBody)

  • arg1, arg2, ... argN,被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的JavaScript标识符的字符串,或者一个用逗号分隔的有效字符串的列表;例如“×”,“theValue”,或“A,B”。
  • functionBody,一个含有包括函数定义的JavaScript语句的字符串。
var add = new Function('a', 'b', 'return a+b;');
console.log( add(2, 3) );    // 5

使用Function构造器生成的Function对象是在函数创建时解析的。这比你使用函数声明或者函数表达式(function)并在你的代码中调用更为低效,因为使用后者创建的函数是跟其他代码一起解析的。

注意: 使用Function构造器生成的函数,并不会在创建它们的上下文中创建闭包;它们一般在全局作用域中被创建。当运行这些函数的时候,它们只能访问自己的本地变量和全局变量,不能访问Function构造器被调用生成的上下文的作用域。

var jsonStr = '{ "age": 20, "name": "jack" }',
    json = (new Function('return ' + jsonStr))();

//Function构造器被调用生成的上下文的作用域,只能访问自己的本地变量和全局变量  
var jsonStr = 'getData({ "age": 20, "name": "jack" })'
new Function('function getData(data){return data};return ' + jsonStr)()   
// Object {age: 20, name: "jack"}, 记住jsonStr不能含try{}catch(e){}

new Function()三种运行形势

以下三种形式输出结果是等价的

new Function()()
(new Function())()
Function()()

示例:

//第一种
var jsonStr = 'getData({ "age": 20, "name": "jack" })'
(new Function('function getData(data){return data};return ' + jsonStr))() 

//第二种
var jsonStr = 'getData({ "age": 20, "name": "jack" })'
new Function('function getData(data){return data};return ' + jsonStr)()  

//第三种
var jsonStr = 'getData({ "age": 20, "name": "jack" })'
Function('function getData(data){return data};return ' + jsonStr)() 

eval 与 new Function 区别

1.eval会影响到作用域(访问和修改它外部作用域的变化),而 Function 更多地类似于一个沙盒。无论在哪里执行 Function,它都仅仅能全局作用域,因此对局部变量影响较小
2.使用eval经常会自动生成为全局变量,因封装即时函数

ps: jQuery.parseJSON的兼容实现用的是new Function()

引起小聊jsonp(数据解析)

jsonp的原理:

  • 1、浏览器的同源策略把跨域请求禁止(Ajax直接请求普通文件存在跨域无权限访问的问题,无论是静态页面、动态网页、web服务、WCF,只要是跨域请求,一律不允许)
  • 2、HTML的<script>标签是例外,可以突破同源策略从其他来源获取数据(bug级的存在,然后拥有"src"这个属性的标签都拥有跨域的能力,比如<script>、、<iframe>)
  • 3、为了便于客户端使用数据,逐渐形成了一种非正式传输协议,称作JSONP,该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据

Function
【译】以 eval() 和 new Function() 执行JavaScript代码
神奇的eval()与new Function()
说说JSON和JSONP,也许你会豁然开朗,含jQuery用例

SVG坐标系和变换

SVG 指可伸缩矢量图形 (Scalable Vector Graphics),是使用 XML 来描述二维图形和绘图程序的语言。在深入了解 SVG 过程中 , 若能很好的理解 SVG 坐标系统和坐标变换,有助于我们更好的使用 SVG 做相关项目。

坐标系统

网格

SVG 使用的坐标系统或者说网格系统,和Canvas用的差不多(所有计算机绘图都差不多)。这种坐标系统是:以页面的左上角为(0,0)坐标点,坐标以像素为单位,x轴正方向是向右,y轴正方向是向下。

网格系统

<rect x="0" y="0" width="100" height="100" />

定义一个矩形,即从左上角开始,向右延展100px,向下延展100px,形成一个100*100大的矩形。

viewport 、 viewBox、preserveAspectRatio属性

  • 视口(viewport):文档使用的画布区域,表示SVG可见区域的大小,通常可以在 <svg>元素 上使用 widthheight 属性确定视口的大小。
  • viewBox:允许指定一个给定的一组图形伸展以适应特定的容器元素。这个属性值由4个数值组成,viewBox = <min-x> <min-y> <width> <height>, 分别代表想要叠加在视口上的用户坐标系统的最小x坐标、最小y坐标、宽度和高度。(可以理解为 SVG 内元素定位的真实坐标系统)
<svg width="1024px" height="1024px" viewBox="0 0 80 80">
  <rect x="10" y="20" width="40" height="40" style="stroke: black; fill: none"/>
</svg>
  • preserveAspectRatio:可以指定被缩放的图像相对视口的对齐方式,以及是希望它适配边缘还是要裁减。该属性的模型为:
preserveAspectRatio = "alignment [meet | slice]"

alignment :指定轴和位置,由一个x对齐方式和一个y对齐方式(min, mid, max)组合而成。默认为xMidYMid

y对齐 xMin xMid xMax
yMin xMinYmin 视口左侧边缘、顶部边缘对齐 xMidYmin 视口水平中心、顶部边缘对齐 xMaxYmin 视口右侧边缘、顶部边缘对齐
yMid xMinYmid 视口左侧边缘、垂直中心 xMidYmid 视口水平中心、垂直中心 xMaxYmid 视口右侧边缘、垂直中心
yMax xMinYmax 视口左侧边缘、底部边缘对齐 xMidYmax 视口水平中心、底部边缘对齐 xMaxYmax 视口右侧边缘、底部边缘对齐

meet :缩小图像以适配可用的空间。
slice :裁减图像不适合视口的部分。

坐标变换

SVG 元素可以通过缩放,移动,倾斜和旋转来变换。类似 HTML 元素使用 CSS transform 来变换,但也有些差异与复杂度,比如 SVG 中使用 <g> 标签创建分组也可以进行组嵌套,组内的标签继承属性,使用transform属性定义坐标变换,可以使组内的元素进行整体变换。

transform属性

tranform 属性用来对一个元素声明一个或多个变换。它输入一个带有顺序的变换定义列表的<transform-list>值,每个变换定义由空格或逗号隔开。

<rect width="50" height="50" x="10" y="10" transform="translate(10, 20) scale(2)" style="stroke: black; fill: none" />
变换 描述
translate(x, y) 按照指定的 x 和 y 值移动用户坐标系统。如果没有指定 y 值,默认 0。
scale(factor1, factor2) 使用指定的 factor1 和 factor2 乘以所有的用户坐标系统。比例值可以是小数或者负数。
scale(factor) 和 scale(factor, factor) 相同。
rotate(angle) 旋转用户坐标,中心点为 (0, 0)。
rotate(angle, x, y) 旋转用户坐标,中心点为 (x, y)。
skewX(angle) 根据指定的 angle 倾斜所有 x 坐标。
skewY(angle) 根据指定的 angle 倾斜所有 y 坐标。
matrix(a b c d e f) 设置 6 个值变换矩阵。

变换矩阵

SVG 元素缩放,移动,倾斜和旋转的变换方式,其实原理都是通过 matrix(a b c d e f) 矩阵变换达到的自定义效果。可以通过一个简单线性方程获得变换后的新坐标 x2 和 y2:

x2 = ax + cy + e
y2 = bx + dy + f

矩阵图:

| a c e |
| b d f |
| 0 0 1 |

矩阵变换坐标变换

Other Resources

matrix.js

线性代数拾遗(一 ):线性方程组、向量方程和矩阵方程

svg transfrom

Cairo 二维矢量图形库

二维矢量图形

计算机图形可分为两类,矢量图形与光栅图形。光栅图形是将图像表示为像素点集。矢量图形则是使用一些几何图元(点、直线、曲线、多边形等)表示图像,这些图元是使用数学公式生成的。

Cairo

Cairo 是用于绘制二维矢量图形的库,采用 C 语言实现,又被许多其它计算机语言所绑定,譬如 Python、PERL、C++、C#、Java。Cairo 是跨平台库,可运行于 Linux、BSD、OSX 等操作系统。

Cairo 力求在各种后端上产生相同的输出,但是每种后端各有优势。例如,PDF 后端会尽可能使用矢量计算(只在必要时生成图像),而 PostScript 后端实际上会为每个页面生成一个大图像。

Cairo 的呈现模型受到许多原有技术的影响。Cairo 采用了 PostScript 中的路径、笔画(stroke)和填充(fill)概念,还实现了 PDF 和现代 X 服务器实现的呈现扩展中的 Porter-Duff 图像组合技术。另外,cairo 还实现了剪切、蒙板和渐变等补充特性。

Cairo 支持多种后端 (backend):

  • X Window 系统
  • Win32 GDI
  • Mac OS X Quartz
  • PNG
  • PDF
  • PostScript(Encapsulated PostScript)
  • SVG

这些后端意味着可使用 Cairo 库在 Windows、Linux/BSD、OSX 等平台的窗口中绘图,也可以用于生成 PNG 图片、PDF/PostScript/SVG 文件。

与 Windows 操作系统的 GDI+ 以及 Mac OS 的 Quartz 2D 库相比,Cairo 是自由软件库。自 GTK+ 2.8 版本开始,Cairo 成为 GTK+ 库的一部分。

Cairo 基本绘图模块

Cairo API 分为三大模块:核心绘图类(Drawing)、外表类(Surfaces)和与字体(Fonts)相关的类(更多细节见 参考资料)。

核心绘图类(Drawing)

Cairo 有一个绘图上下文(drawing context),相当于画布。上下文是 Context 是 Cairo 的核心结构,在 Cairo 中使用 cairo_t 来表示,呈现图形。在绘图上下文上通过 Paths,绘制可以绘制一系列曲线和相关数据的路径,还可设置笔画宽度或填充。Cairo 笔画路径含有 5 种基本绘图操作:

  cairo_stroke
  cairo_fill
  cairo_show_text/cairo_show_glyphs
  cairo_paint
  cairo_mask

Cairo 还提供类似 OpenGL 的坐标变换操作。变换操作包括:平移 cairo_translate ,伸缩 cairo_scale,旋转 cairo_rotate。我们也可以通过 cairo_transform 函数来指定一个复杂的变换。

外表类(Surfaces)

Cairo 外表类型,对应各种输出目标。Cairo 外表(surface)是执行绘图的位置。具体地说,有用于图像(内存缓冲区)的外表、用于 Open GL 的 glitz 外表、用于呈现文档的 PDF 和 PostScript 外表以及用于直接执行绘图的 XLib 和 Win32 外表。这些外表类型都派生自外表基类型 cairo_surface_t

字体(Fonts)

Cairo 为字体提供了一个基类 cairo_font_face_t。Cairo 支持可缩放字体,其中包含给定字体大小的缓存标准。另外,可以用各种字体选项控制如何显示给定的字体。在使用 Cairo 时,在 UNIX 上常用的字体是 Freetype 字体,在 Windows 平台上使用 Win32 字体。

绘制一个矩形到 rectangle.png 图片上,示例:

#include <cairo.h>
#include <png.h>

int
main (int argc, char *argv[])
{
    cairo_surface_t *surface;
    cairo_t *cr;
 
    int width = 640;
    int height = 480;
    surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
    cr = cairo_create (surface);
 
    /* Drawing code goes here */
    cairo_set_line_width (cr, 10);
    cairo_set_source_rgb (cr, 0, 0, 0);
    cairo_rectangle (cr, width/4, height/4, width/2, height/2);
    cairo_stroke (cr);
 
    /* Write output and clean up */
    cairo_surface_write_to_png (surface, "./rectangle.png");
    cairo_destroy (cr);
    cairo_surface_destroy (surface);
 
    return 0;
}

Mac 环境下编译安装 Cairo

1.安装

  • 使用 MacPorts 安装
  sudo port install cairo
  • 使用 fink 安装
 sudo apt-get install cairo
  • 使用 Homebrew 安装
 brew install cairo

2.设置环境变量

安装完 cairo, 需设置环境变量:

For compilers to find libffi you may need to set:
  export LDFLAGS="-L/usr/local/opt/libffi/lib"

For pkg-config to find libffi you may need to set:
  export PKG_CONFIG_PATH="/usr/local/opt/libffi/lib/pkgconfig"

3.安装 gcc 编译器

由于 cairo 采用 C 语言实现,mac环境需使用 gcc 编译

 brew install gcc

gcc 编译,可以把前面讲的例子命名为 cairotest.c 文件进行编译,会生成 rectangle.png 文件

gcc -o cairotest $(pkg-config --cflags --libs cairo) cairotest.c
或
gcc -o cairotest `pkg-config --cflags --libs cairo` cairotest.c

小知识: .cpp和.c 文件区别

.cpp是c++的源文件, c++语言兼容c语言, 编写c语言代码可以用c++的源文件.cpp
.c是纯粹的c语言文件, 不可以有c++语言的代码, 默认自带一些库文件
c++语言兼容c语言, c语言是面向过程, c++语言既能面向过程也可以面向对象

Cairo 实际应用

许多有影响力的开放源码项目已经采用了 Cairo,Cairo 已经成为 Linux 图形领域的重要软件。已经采用 Cairo 的重要项目包括:

  • node-canvas,Web Canvas API的一个实现,并尽可能地实现该API
  • CairoSVG,是SVG 1.1到PNG,PDF,PS和SVG转换器。j
  • librsvg,SVG渲染库
  • Gtk+,一个广受喜爱的跨平台图形工具集
  • Pango,一个用于布置和呈现文本的免费软件库,它主要用于实现国际化
  • Gnome,一个免费的桌面环境
  • Mozilla,一个跨平台的 Web 浏览器基础结构,Firefox 就是在这个基础结构上构建的
  • OpenOffice.org,一个可以与 Microsoft Office 匹敌的免费办公套件

参考资料:
Cairo
cairo repository
Cairo 图形指南
cairo-demo
CairoSVG
Ghostscript
postscript
Encapsulated PostScript (EPS) File Format, Version 3.x
Encapsulated PostScript
C语言中.h和.c文件解析
C++/C/JAVA/Python之间的区别?

如何避免 JavaScript 长递归导致的堆栈溢出?

递归 (recursion) 是很多算法都使用的一种编程方法。

理解递归

假设我们要实现数学里面计算阶乘的例子, 如:

1
2x1
3x2x1
4x3x2x1
5x4x3x2x1
6x5x4x3x2x1

第一种方法我们采用是 while 循环,累乘代码下去:

function factorial (number) {
  let result = 1
  while (number > 1) {
    result = result * number * (number - 1)
    number = number - 2
  }
  return result
}

第二种方法使用递归——函数调用自己,这种方法的伪代码如下.

function factorial (number) {
  if (number < 2) {
    return 1
  } else {
    return number * factorial(number - 1)
  }
}

这两种方法的作用相同,但第二种方法更清晰。递归只是让解决方案更清晰,并没有性能上的优势。实际上,在有些情况下,使用循环的性能更好。在 Stack Overflow 上说的一句话: “如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要。”

递归引起堆栈溢出

JavaScript 代码运行时,函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。

img

计算当前使用的JavaScript引擎可以支持多深的调用:

function computeMaxCallStackSize () {
  try {
    return 1 + computeMaxCallStackSize()
  } catch (e) {
    // Call stack overflow
    return 1
  }
}

由于递归函数的特点,导致如果边界检查存在缺陷,那么就可能导致超过这个最大深度,从而超出堆栈的存储能力,也就是所说的“内存溢出”,那遇到这种情况,则需求对递归进行优化,前面例子循环代替递归是一种解决方案,但除了这个方法之外也还有其他许多方法。

尾调用优化

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。

还是前面的例子,计算 number 的阶乘,最多需要保存 n 个调用记录,复杂度 O(n) 。

function factorial (number) {
  if (number < 2) {
    return 1
  } else {
    return number * factorial(number - 1)
  }
}

如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

function factorial (number, result = 1) {
  if (number === 1) return result
  return factorial(number - 1, number * result)
}

由此可见,"尾调用优化"对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6也是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署"尾调用优化"。这就是说,在 ES6 中,只要使用尾递归,就不会发生栈溢出,相对节省内存。

注意:

尾递归写法的函数在 Chrome 浏览器的控制台下依旧出现了调用栈溢出的异常,是因为 chrome 对尾递归调用(proper tail calls)不支持,可查看 ES6在各大平台上的兼容性

Node V8 引擎实际上已经实现了尾调用优化,但是默认是关闭该功能的。执行 node --v8-options 可以找到一个启用尾调用优化的参数--harmony_tailcalls

事件驱动” (Event-Driven)的特性

在 JavaScript 中,由于其 “事件驱动” (Event-Driven)的特性,使用 "setTimeout"、 “nextTick” 等方式对指定函数的调用,实际上是将该函数的引用(指针)储存起来,并在适当的时候调用。
换句话说,JavaScript 中, setTimeoutnextTick 等方式调用的函数,并不会形成类似于递归那样, “一层套一层” 的调用链。下一次函数调用时,上一个 “父” 函数的调用已经执行完毕,就不会存在堆栈溢出的风险。

function factorial (number, result = 1) {
  if (number === 1) {
    console.log('result', result)
    return result
  }
  setTimeout(() => {
    factorial(number - 1, number * result)
  }, 0)
}

factorial(10000000)  // result Infinity

小结

最后,我们总结罗列下递归优化方案:

  • 循环代替递归
  • 尾递归优化
  • 事件驱动 的特性,使用 "setTimeout"、 “nextTick”

Other Resouces:

JS的函数调用栈有多深?

怎样避免JavaScript中过长递归导致的堆栈溢出?

ES6尾调用优化

为什么要用setTimeout模拟setInterval ?

尾递归的后续探究

JavaScript 事件委托代码片段

event.currentTarget与event.target区别

event.currentTarget,当事件遍历 DOM 时,识别事件的当前目标对象(Identifies the current target for the event, as the event traverses the DOM.)。 该属性总是指向被绑定事件句柄(event handler)的元素。而与之对比的event.target ,则是指向触发该事件的元素。

Element.matches API 的基本使用方法:

Element.matches(selectorString),selectorString 既是 CSS 那样的选择器规则,比如本例中可以使用 target.matches('li.class-1'),他会返回一个布尔值,如果 target 元素是标签 li 并且它的类是 class-1 ,那么就会返回 true,否则返回 false;

if (!Element.prototype.matches) {
  Element.prototype.matches =
    Element.prototype.matchesSelector ||
    Element.prototype.mozMatchesSelector ||
    Element.prototype.msMatchesSelector ||
    Element.prototype.oMatchesSelector ||
    Element.prototype.webkitMatchesSelector ||
    function(s) {
      var matches = (this.document || this.ownerDocument).querySelectorAll(s),
      i = matches.length;
      while (--i >= 0 && matches.item(i) !== this) {}
      return i > -1;
    };
}
document.getElementById('list').addEventListener('click', function (e) {
  // 兼容性处理
  var event = e || window.event;
  var target = event.target || event.srcElement;
  if (target.matches('li.class-1')) {
    console.log('the content is: ', target.innerHTML);
  }
});

封装写法:

function eventDelegate (parentSelector, targetSelector, events, foo) {

  // 触发执行的函数
  function triFunction (e) {
    // 兼容性处理
    var event = e || window.event;

    // 获取到目标阶段指向的元素
    var target = event.target || event.srcElement;

    // 获取到代理事件的函数
    var currentTarget = event.currentTarget;

    // 处理 matches 的兼容性
    if (!Element.prototype.matches) {
      Element.prototype.matches =
        Element.prototype.matchesSelector ||
        Element.prototype.mozMatchesSelector ||
        Element.prototype.msMatchesSelector ||
        Element.prototype.oMatchesSelector ||
        Element.prototype.webkitMatchesSelector ||
        function(s) {
          var matches = (this.document || this.ownerDocument).querySelectorAll(s),
            i = matches.length;
          while (--i >= 0 && matches.item(i) !== this) {}
          return i > -1;            
        };
    }

    // 遍历外层并且匹配
    while (target !== currentTarget) {
      // 判断是否匹配到我们所需要的元素上
      if (target.matches(targetSelector)) {
        var sTarget = target;
        // 执行绑定的函数,注意 this
        foo.call(sTarget, Array.prototype.slice.call(arguments))
      }

      target = target.parentNode;
    }
  }

  // 如果有多个事件的话需要全部一一绑定事件
  events.split('.').forEach(function (evt) {
    // 多个父层元素的话也需要一一绑定
    Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(function ($p) {
      $p.addEventListener(evt, triFunction);
    });
  });
}

JavaScript 事件委托详解
JavaScript 和事件

git commit message 中使用 emoji

git commit message 中使用 emoji

🎨 :art: Improving structure / format of the code. 改进目录或代码结构 / 格式化代码

⚡️ :zap: Improving performance. 提升性能

🔥 :fire: Removing code or files. 移除代码或文件

🐛 :bug: Fixing a bug. 修复 bug

🚑 :ambulance: Critical hotfix. 紧急修复

:sparkles: Introducing new features. 新 feature

📝 :memo: Writing docs. 书写文档

🚀 :rocket: Deploying stuff. 部署相关

💄 :lipstick: Updating the UI and style files. 更新 UI 和 样式文件

🎉 :tada: Initial commit. 首次提交

:white_check_mark: Adding tests. 新增测试用例

🔒 :lock: Fixing security issues. 修复安全性问题

🍎 :apple: Fixing something on macOS. 修复 macOS 平台上的缺陷

🐧 :penguin: Fixing something on Linux. 修复 Linux 平台上的缺陷

🏁 :checkered_flag: Fixing something on Windows. 修复 Windows 平台上的缺陷

🤖 :robot: Fixing something on Android. 修复 Android 上的缺陷

🍏 :green_apple: Fixing something on iOS. 修复 iOS 上的缺陷

🔖 :bookmark: Releasing / Version tags. 发布 / 给代码打版本化的 tag

🚨 :rotating_light: Removing linter warnings. 移除 linter 的警告

🚧 :construction: Work in progress. 开发进行时

💚 :green_heart: Fixing CI Build. 修复 CI 问题

⬇️ :arrow_down: Downgrading dependencies. 降级依赖版本

⬆️ :arrow_up: Upgrading dependencies. 升级依赖版本

📌 :pushpin: Pinning dependencies to specific versions. 锁死依赖版本

👷 :construction_worker: Adding CI build system. 添加 CI

📈 :chart_with_upwards_trend: Adding analytics or tracking code. 添加分析或埋点代码

♻️ :recycle: Refactoring code. 代码重构

:heavy_minus_sign: Removing a dependency. 移除依赖

🐳 :whale: Work about Docker. Docker 相关事由

:heavy_plus_sign: Adding a dependency. 添加一个依赖

🔧 :wrench: Changing configuration files. 修改一个配置文件

🌐 :globe_with_meridians: Internationalization and localization. 国际化和本地化

✏️ :pencil2: Fixing typos. 修正拼写错误

💩 :hankey: Writing bad code that needs to be improved. 需要改进的代码,先上后续再重构

:rewind: Reverting changes. 回滚变更

🔀 :twisted_rightwards_arrows: Merging branches. 分支合并

📦 :package: Updating compiled files or packages. 更新打包后的文件或者包

👽 :alien: Updating code due to external API changes. 外部依赖 API 变更导致的代码变更

🚚 :truck: Moving or renaming files. 移动或重命名文件

📄 :page_facing_up: Adding or updating license. 添加或者更新许可

💥 :boom: Introducing breaking changes. 不兼容变更

🍱 :bento: Adding or updating assets. 新增或更新 assets 资源

👌 :ok_hand: Updating code due to code review changes. 更新由 CR 引起的代码变更

♿️ :wheelchair: Improving accessibility. 提升无障碍体验

💡 :bulb: Documenting source code. 书写源码文档

🍻 :beers: Writing code drunkenly.

💬 :speech_balloon: Updating text and literals. 更新文案以及字面量

🗃 :card_file_box: Performing database related changes. 执行数据库相关变更

🔊 :loud_sound: Adding logs. 增加日志

🔇 :mute: Removing logs. 移除日志

👥 :busts_in_silhouette: Adding contributor(s). 新增贡献者

🚸 :children_crossing: Improving user experience / usability. 提升用户体验 / 可用性

🏗 :building_construction: Making architectural changes. 架构变更

📱 :iphone: Working on responsive design. 真在进展响应式设计的相关事由

🤡 :clown_face: Mocking things. Mock 相关

🥚 :egg: Adding an easter egg. 彩蛋

🙈 :see_no_evil: Adding or updating a .gitignore file 新增或者更新 .gitignore 文件

Mac 实用技巧

Mac 每次都要执行source ~/.bash_profile 配置的环境变量才生效

~/.bash_profile 中配置环境变量, 可是每次重启终端后配置的不生效.需要重新执行 : $source ~/.bash_profile

例如设置 cairo 环境变量

For compilers to find libffi you may need to set:
  export LDFLAGS="-L/usr/local/opt/libffi/lib"

For pkg-config to find libffi you may need to set:
  export PKG_CONFIG_PATH="/usr/local/opt/libffi/lib/pkgconfig"

# 编译c语言
gcc -o cairotest $(pkg-config --cflags --libs cairo) cairotest.c

发现zsh加载的是 ~/.zshrc 文件,而 .zshrc 文件中并没有定义任务环境变量。

解决办法

~/.zshrc 文件最后,增加一行:

ssh source ~/.bash_profile

Mac 系统调整 Launchpad 应用程序图标大小

运行“终端”程序,执行以下命令:

1.调整每一列显示图标数量,7 表示每一列显示7个,在我的电脑上,7个个人觉得比较不错

defaults write com.apple.dock springboard-rows -int 7

2.调整每一行显示图标数量,这里我用的是8

defaults write com.apple.dock springboard-columns -int 8

3.由于修改了每一页显示图标数量,可能需要重置Launchpad

defaults write com.apple.dock ResetLaunchPad -bool TRUE;killall Dock

macOS Catalina 10.15 第三方软件文件提示已损坏解决办法

sudo xattr -r -d com.apple.quarantine  [Application path]
// 例如 Sketch
sudo xattr -r -d com.apple.quarantine /Applications/Sketch.app/

使用【Finder】的【显示】设置显示完整路径

打开【Finder】,找到菜单栏中的【显示】->【显示路径栏】,或者使用快捷键【option+command+p】显示路径栏。

设置【VS code】alias 快捷键

设置 alias

alias code="/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code"

vi ~/.zshrc 添加

source ~/.bash_profile

解决 vscode 使用 Powerline 乱码问题

settings.json 设置:

{
    "terminal.integrated.fontFamily": "Source Code Pro for Powerline"
}
// or
{
    "terminal.external.osxExec": "iTerm.app",
    "terminal.integrated.shell.osx": "/bin/zsh",
    "terminal.integrated.fontFamily": "Menlo for Powerline"
}

解决 vscode 重置 ESLint 对话选择判定

ESLint Extension 升级后,启动新 workspace 会弹窗提示选择是否选择本地 node_modules/eslint,但有时候我们选择 NO 之后,如何重置选择呢?

"The eslint extension will use the eslint library node_modules/eslint installed locally to the workspace folder 'reponame' for validation. Do you allow this?"

我们可以 command+shift+p 打开命令窗口,输入:

ESLint: Reset Library Decisions  // 重置选择判定
ESLint: Create ESLint configuration 
ESLint: Disable ESLint
ESLint: Enable ESLint
ESLint: Fix all auto-fixable Problems
ESLint: Migrate Settings
ESLint: Show Output Channel

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.