Coder Social home page Coder Social logo

blog's People

Contributors

renaesop 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

Watchers

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

blog's Issues

webpack的主要对象

webpack里面的对象,只要是单例(或者实例数目只跟option挂钩),都可以定义plugin。

compiler

一般而言,webpack的配置文件是一个对象。但是,webpack的配置文件是可以给出数组的,并且可以嵌套。当给出数组的时候,就定义了多个构建任务了。每个webpack的构建任务对应一个compiler实例,互相之间独立,其执行方式是async.map

自身将处理如下流程:

  • 结合用户和默认配置初始化option
  • 先加载option中定义的插件,再加载内建插件
  • emit过程,负责输出文件

compilation

webpack的编译任务的真正执行者。

自身会处理如下流程:

  • addEntry: 调用方法将entry转化为Dependency并转化为module
  • seal: 实现 modules ==> chunks ==> assets, 过程中会调度plugin执行优化,包括给出hash

xxModule(不支持插件,但compilation上某些插件会在某些方法中被触发)

webpack会有不同的module,不过他们不是根据文件类型分的,而是根据模块所属依赖类型(是否是dll,multiple,normal等)定的。

webpack把 require(ModulePath)中的ModulePath叫做request
自身会处理如下流程:

  • 根据options.module定义的规则和module自身的性质,匹配其loader
  • 调用resovler,基于context和request、loader的路径,获取loader和request的绝对路径
  • 加载loader, 读取request的内容,称为source,并以waterfall的形式让loader处理
  • 如果需要,将loader处理完的source,交给parser(后面有说)处理
  • 将dependencies转化为module
  • module的id为0,则为entry

parser

解析JS文件,并遍历。

自身会这么做:

  • 调用acorn生成ast
  • 遍历ast,其中每个语句都会触发插件,例如 parse.applyPluginBailResult('call commonjs:require:xxx', xxx, xx)。就连require函数添加依赖的功能也是插件完成的。

resolver

顾名思义,将路径转化为绝对路径。
有normalResolver、contextResolver、loaderResolver三种。
contextResolver用于解析contextModule(形如require('a/' + b + '/c')这种)

chunk (不支持插件,但compilation上某些插件会在某些方法中被触发)

可以认为chunk是entry和entry依赖的合集,或者是按需加载的module及其依赖。(如果不考虑优化)

chunkHash是根据chunk的id,name以及包含的module内容生成。

xxTemplate

将chunk生成assets的模板。

做法:

  • 根据chunk类型选择生成的assets的头部(比如说写入commonjs的runtime)
  • 将module当做依赖,结合其id,用函数包裹,添加到到{}

source

包含源码以及其sourceMap。如果xxModule并没有执行parse阶段的话,只会在emit的时候才会去调用source.source方法获取源码。也就是说,source可以是根据compilation、module的元数据生成的,非常强大的特性。

HTTP缓存

首先,HTTPS的缓存和HTTP的缓存策略是相同的,都是由HTTP响应头决定。

Cache-Control

请求头和响应头都可以设定该字段,用于控制缓存链的行为;这是个单向指令,也就是请求和响应头中的Cache-Control独立控制请求和响应;不管代理能不能理解这些指令都应该原封不动地传递。Age是与之紧密相关的字段,表示响应由源站生成或确认的时间与当前时间的差值。指令如下:

请求

1. max-age

简而言之,指定保质期的指令。max-age指令标示了客户端不愿意接收一个age大于设定时间的响应。除非还指定了max-stale,否则客户端是不会接收过期的响应的。

某些浏览器(比如Firefox)中如果设定为永不缓存,那么其发出的请求中,请求头会包含max-age=0

2.max-stale

字面意思,能容忍的最大过期时间。max-stale指令标示了客户端愿意接收一个已经过期了的响应。如果指定了max-stale的值,则最大容忍时间为对应的秒数。如果没有指定,那么说明浏览器愿意接收任何age的响应。

暂时没有发现比较常见的使用场景。

3.min-fresh

设定能够容忍的最小新鲜度min-fresh标示了客户端不愿意接受新鲜度不多于当前的age加上min-fresh设定的时间之和的响应。

4.no-cache

no-cache指令标示了缓存链禁止返回一个未经源站验证的缓存响应。

已知的是Chrome勾掉disable cache之后,所用的请求都会带有这个字段。可惜的是这个指令对于CDN来说,往往没有什么用处,并不会回源,可能是CDN并不认为自己是个代理。

5.no-store

cache禁止存储请求和响应中的任何部分。这个指令被用于privateshared的cache中。“禁止存储”在这个上下文中是指cache禁止在非易失存储设备中存储信息,并且必须尽最大努力在转发请求/响应之后移除这些信息。

这个指令并不是一个可靠和有效保证隐私的措施。特别是,恶意的或者妥协的cache并不会识别或者遵守这个指令,并且互联网通信是很可能被窃听。

啊哈,如果运营商啥的遵守这个指令,就不会有信息贩卖这回事了。

6.no-transform

禁止中间人更改payload(请求体)。

7.only-if-cached

只要缓存的内容。only-if-cached请求指定指示了客户端指向获取一个缓存的响应。如果接收到这个指定,cache应该要么用缓存的内容给出响应,要么给出一个504(GateWay Timeou)响应码。如果一组cache被作为一个内部相连的系统,那么其中的某个成员可以向这个缓存组里请求响应。

响应

1.must-revalidate

cache在响应不新鲜,必须回源。must-revalidate响应指定,一旦内容过期,cache禁止在没有和源站确认情况下,使用响应来返回给后续请求。

这个指令对于支持某些协议的可靠操作是必要的。所有情况下,cache必须遵守这个指令;特别地,如果cache因为某些原因无法到达源站,它必须产生一个504响应。

这个指令当且仅当被服务器用于,如果未能验证一个呈现的请求会导致错误操作的场合,例如一个一个沉默地失败的金融交易。

2.no-cache

禁止返回未被源站验证的内容。这个指令允许源站阻止cache在不回源的情况下响应请求,甚至是在cache设定发送过期响应的情况下。

如果no-cache响应指令指定了一个或者多个字段名,那么cache可能会使用这个响应来处理后续请求。然而,任意出现在列表中的头部字段在没有回源的情况下是禁止发给客户端的。

这个指令使得在利用缓存的情况下,也可以使得头部字段有所不同,例如Set-Cookie头部。

3.no-store

跟请求头中的no-store一致

4.no-transform

跟请求头中一致

5.public

参考文献:

  1. HTTP/1.1规范 RFC7234

证书小记

虽然证书常常被挂在嘴边,但是证书到底有哪些类型,又分别有哪些特点,哪些坑,还是比较杂乱的,望山也找不到什么相关总结文章。那这篇文章是要总结一下吗?=。=既然你诚心地发问了,我就大发慈悲地告诉你,这篇文章只管大胆胡说,绝不小心求证,(逃

首先,安利一个站点检测HTTPS配置

坑点1:证书在PC上、IOS上一切正常,但是到Android上就不信任

具体表现:

实际原因:

解决方案:

坑点2:泛域名(wildcard, SAN)证书

待续……

浅探webpack的module

本文主要说的是webpack编译中的make阶段,或者说webpack生成modules的整个流程。

webpack的make过程还是比较复杂的,主要流程如下

webpack-make

1.核心

我认为make阶段的核心是依赖,实际上,make阶段的一切模块都由依赖构建出来。

首先是entry。我们在webpack的配置文件中可以指定entry,entry中指定的文件可以是单个、也可以是多个(字符串),举个entry给多个文件的例子(伪代码):

指定配置::

{
     entry: {
        main: ['./src/a', './src/b'],
    }
}


webpack::

const entryMainModule = {
    dependencies: {
            './src/a': Dep('./src/a'),
            './src/b':Dep('./src/b')
    },
    // webpack的source是在make结束之后才被调用的
    source() {
        return `
                     __webpack_require__(${this.dependencies['./src/a'].id});
                     module.exports =  __webpack_require__(${this.dependencies['./src/b'].id});
                    `;
    }
}

可以看出,webpack会把entry转换成一个新增的MultipleModule,entry数组里的模块被指定为依赖。当然如果是指定的单个文件,则entry直接转化为
NormalModule。不过还是会有转化为Dependency的过程。总结如下:

entry --> 是否单文件 --是--> entry设定为为SingleModuleDependency --> 生成NormalModule(会执行loader)
             |
             否--> entry设定为MultipleModuleDependency --> 生成MultipleModule(无loader, parse)

2. 对引用路径的处理

造过构建工具轮子的人都知道,引用路径的处理是一块很麻烦的事情。webpack没有像babel他们,使用node的核心模块module, path等来简化一些处理,而是自己造了一个类Resolver, 这个类是支持插件的。不管是loader还是contextModule还是normalModule都是他处理,最后处理的结果都是把相对路径转化为绝对路径。(比较常规)

由于loader也是Resolver解析的,所以loader的路径也就可以很灵活了,不一定非要放到node_modules里面(好像没人提这点)。

另外,resolve是比较蠢的,比如引用./a, 这货会同时找文件夹和文件,文件还会带上extension找。

此外,每个module都会独立地匹配loader,并不存在什么缓存。

3. 对loader的处理

webpack的loader有pitch特性, 所以

['style', 'picth-enabled', 'css']

加载loader的顺序是从左到右的,虽然执行的顺序是相反的。

loader可以指定raw属性,选择传过来的source是否是buffer。

loader具有一些很实用的方法可以调用,尤其是emitFileemitFile的作用是给当前module附加一个assets(正常情况下assets其实是很后面的阶段才生成的),有没有一种钦定的赶脚。在我们常用的file-loader中就是用了这个特性。

4. loader之后的parse

很少有文章会提到webpack的parse,实际上这个才是真正意义上的“编译”嘛。在loader执行完之后,webpack会使用acorn将模块解析成ast,然后去遍历ast。

Parser模块支持插件,在遍历大多数语句的时候,都存在钩子。实际上给模块添加依赖就是遍历到"call commonjs:require"这种语句的时候,插件完成的,并且也将依赖模块的名字改为动态的了。

webpack的Parser相比babel还是很弱的。模块自身处理完了之后,都会去处理自身的依赖,也就是把依赖转化为模块。

5. module的大致结构

module: {
    request: './src/a',
    context: 'xxxx',
    loaders: [],
    id: 0, // entry的id为0
    source() {return xxxx},
    dependencies: {
          './b': {
               request: './src/a',
               context: 'xxxx',
               module: module,
          }
    }
}

React(16.2)源码阅读笔记

副标题:在onClick中调用setState会发生什么?

现在版本的React(16.2)用了fiber,网上也说的很多,但实质上React就是把对树的遍历由递归改成了循环,把数组换成了链表。而所谓的fiber--也就是所谓的virtual stack frame则是把栈帧的组织方式由栈变成了链表而已。递归被撤掉,加上引入的一系列新特性(call, return, 甚至fragment)让React源代码显得比较碎片化,以至于只能自己动手去观赏源码。

下面是一个很简单(无聊)的React应用:

import React, { Component } from 'react';
import { render } from 'react-dom';
class Comp1 extends Component {
    constructor(props) {
        super(props);
        this.state = { 
            data: new Array(100).fill(0).map((_, index) => index + 1),
        };
    }
    onClickCb() {
        this.setState(prevState => ({
              data: [...prevState.data.slice(0, 1), ...prevState.data.slice(2)],
        }));
    }
    render() {
         return <div>
             <button onClick={() => this.onClickCb()}>click to remove 1 </button>
             {this.state.data.map(val => <div key={val}>{val}</div>)}
         </div>;
    }
}

const Comp2 = () => <div>A placeholder</div>;

render(<div>
    <Comp1 />
    <Comp2 />
</div>, document.getElementById('container'));

如果点击了button会发生什么呢?嘿,肯定是一通操作,把✈️摔了(误……),是把Comp1里面的div给更新掉了。下面当然会提那个已经被说到耳朵起茧子的diff算法,但不仅仅是说diff,且diff算法本身也是有一些变化的。

首先是大致的流程图,流程较长就分了几张

  1. 在render阶段以前的调用步骤
    在render阶段之前

然后是文字解说,唔。

Step1. click事件的回调函数式如何触发的

首先,click事件是绑到哪里的? 我们可以用Chrome的工具很容易的看到(查看元素--> Elements面板 --> 右侧Event Listener这个tab页),是绑到document上的,这是绑定事件的代码, listenTo(registrationName, contentDocumentHandle)被调用时contentDocumentHandle传的是挂载点(root)的ownerDocument。

其次,事件的回调函数呢?在document上绑的回调函数是dispatchEvent。在这个函数里面,我们可以看到,在冒泡情况下,React会找到target(事件有个target,而React创建的DOM节点都有俩property, __reactEventHandlers[随机数] 与 __reactInternalInstance[随机数], 分别用来存传入的property以及对应的fiber)所有的祖先fiber(由React创建的节点),这样就获得了需要冒泡的节点。

最后,React的事件是支持插件的,所以还要有机会合成事件。也就是说,在冒泡的路径上,每个节点都可能会有多个事件要处理。而每个事件都会去检查DOM节点上存的__reactEventHandlers中是否有对应回调,在这个地方不用fiber本身而舍近求远,是因为在异步模式下,fiber的属性和DOM上挂载的可能不一致,按语义将,事件回调是要依DOM上的。

Step2. setState会做什么事

首先,在dispatchEvent被调用的时候,调用了batchedUpdates , 而它定义在reconciler中, 简单来说,他接收一个函数fn,以及函数的参数,他会将isBatchingUpdates 设置为true,之后他会调用函数fn。isBatchingUpdates会影响到React更新视图的策略,如果它为true, 那么无论如何(不管React是不是使用了同步模式),只有在fn执行完之后才会去更改State、更新视图(也就是所谓“异步”setState)。

其次,setState实际调的是setState实现,也就是:

 enqueueSetState(instance, partialState, callback) {
      const fiber = ReactInstanceMap.get(instance);
      callback = callback === undefined ? null : callback;
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
     // 计算需要更新的ddl,如果是Sync模式则是Sync/1, Async则按照优先级处理
      const expirationTime = computeExpirationForFiber(fiber);
      ******
      insertUpdateIntoFiber(fiber, update);
      // 实际上做的是标记可能需要更新的节点
      scheduleWork(fiber, expirationTime);
    }

紧接着,执行scheduleWork,主要做的工作是,标记setState所在的节点以及其祖先节点的expiration,也就是标记“可能需要更新”。之后会调用requestWork, 会找出最需要需要更新的root(也就是render时的挂载点), 在batching模式下,requestWork会不做任何事情,非batching模式下则依次开始更新root。

下一步,执行performWorkOnRoot这个函数主要有两个工作,分别是renderRootcompleteRoot,render对应于构建新的virtual DOM树,而complete则对应于让真实DOM同步virtual DOM的修改。由于在异步模式下,render可能不会一口气做完,所以renderRoot可能没有完成更新整个virtual DOM树的工作,这种情况下便不会调用complete。异步模式还可能存在render已经完成但不剩时间片的情况,这时候就可能会把complete(commit)工作留到下一个时间片里面做。

Step3. renderRoot会做什么事

在Fiber架构中,有三种树(不太严格的“树”),分别是ReactElement,fiber,instance/DOM树,对应者主要的三种对象。我们常说的virtual DOM树应该指的是ReactElement树,但现在来说可能fiber树可能更贴切,三者的节点之间接近一一对应。我们所写的JSX对应的就是ReactElement,比如说<Component1 />就相当于{$$typeof: 'xxxxx', type: Component1, props: ..., key: ..., children: [...],...},一般而言ReactElement由render方法(class组件)或者函数(函数式组件)返回,最终在diff时会转化成(或者更新已有的)fiber对象,既然fiber是任务单位自然也会记录要做的更新,这些更新会在commit阶段的时候被消化掉。

renderRoot除了会做一些簿记工作和错误处理以外,主要还是调用workLoop函数

  function workLoop(expirationTime: ExpirationTime) {
    if (capturedErrors !== null) {
      ******
      return;
    }
    if (nextRenderExpirationTime === NoWork || nextRenderExpirationTime > expirationTime) {
      return;
    }
    // 可能会被打断,所以
    if (nextRenderExpirationTime <= mostRecentCurrentTime) {
      // Flush all expired work.
      while (nextUnitOfWork !== null) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
      }
    } else {
      // Flush asynchronous work until the deadline runs out of time.
      while (nextUnitOfWork !== null && !shouldYield()) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
      }
    }
  }

这个workLoop,实际上是reactor模式的标配,比如说node的workLoop。reactor模式下需要做的事情先丢给队列,在这里就是个链表(nextUnitOfWork链),然后让workLoop决定是否处理或者何时处理。这个函数的nextUnitOfWork实际上就是一个fiber,而如果是同步模式下,相当于是遍历树,一边遍历一遍更新。

接下来就是performUnitOfWork:

  function performUnitOfWork(workInProgress: Fiber): Fiber | null {
    const current = workInProgress.alternate;
   //  ** some dev code**
    let next = beginWork(current, workInProgress, nextRenderExpirationTime);
     // **some dev code**
    if (next === null) {
      next = completeUnitOfWork(workInProgress);
    }
   // **some bookkeeping code**
    return next;
  }

这个函数很有意思,会调用beginWorkcompleteUnitOfWork,这俩函数的名字很令人迷糊。什么叫开始和完成?在深度优先遍历树的时,分先序和后序遍历,或者用我们更熟悉的话说就是捕获和冒泡,也就是说树的每个节点有两次调用函数的机会,在这里,beginWork是在捕获阶段执行一些工作,而completeUnitOfWork则是在冒泡阶段做一些工作(其实这里这么说并不准确,对于call&return组件而言略有差异)。从返回值来说,beginWork会返回节点的child,而completeUnitOfWork往往返回的是sibling节点(也可能返回child,在call&return组件的情况下),这样树就能被完全遍历了。

接着看beginWork(ReactFiberBeginWork.js)

function beginWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderExpirationTime: ExpirationTime,
  ): Fiber | null {
    if (workInProgress.expirationTime === NoWork ||workInProgress.expirationTime > renderExpirationTime) {
      return bailoutOnLowPriority(current, workInProgress);
    }
    switch (workInProgress.tag) {
      // ** some other cases**
      case ClassComponent:
        return updateClassComponent(
          current,
          workInProgress,
          renderExpirationTime,
        );
      // ** some other cases**
    }
  }

这个函数首先判断这个节点是否需要render,而这个expirationTime这个标记呢是在前面scheduleWork的时候做的,如果没有标记那么他的整个子树也都跳过更新了。接下来是根据fiber(element)的所属类型选择更新的策略,由于最典型最复杂的是class组件,这里就把他拿出来做例子。

updateClassComponent

function updateClassComponent( current: Fiber | null, workInProgress: Fiber,
     renderExpirationTime: ExpirationTime) {
   // class component 可能有context,以栈形式组织
    const hasContext = pushContextProvider(workInProgress);

    let shouldUpdate;
    if (current === null) {
      if (!workInProgress.stateNode) {
        constructClassInstance(workInProgress, workInProgress.pendingProps);
        mountClassInstance(workInProgress, renderExpirationTime);
        shouldUpdate = true;
      } else {
        invariant(false, 'Resuming work not yet implemented.');
      }
    } else {
      shouldUpdate = updateClassInstance(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
    return finishClassComponent(
      current,
      workInProgress,
      shouldUpdate,
      hasContext,
    );
  }

第一个判断处理的是初次渲染以及更新时新建组件的状况,此时,首先需要构建instance并与fiber挂钩,之后执行mount(也就是willMount, didMount以及的update那套,因为willMount可能会调用setState之类的)。而else分支中的updateClassInstance则是负责去调用componentWillReceivePropsshouldComponentUpdatecomponentWillUpdate这些生命周期函数,以及应用setState存入队列的更新。很多文章已经强调过,在render阶段调用的生命周期方法可能会在commit之前调用多次,所以不应该有副作用,而如果细看相关代码也可以发现React有很多帮助检测副作用的工作(搜索debugRenderPhaseSideEffects)。总的来说前面就是判断组件是否需要更新以及让组件有机会做一些数据的处理工作,最后的finishClassComponent则会真正做“计算和标记更新”的工作。

finishClassComponent

 function finishClassComponent(current: Fiber | null, workInProgress: Fiber,
    shouldUpdate: boolean, hasContext: boolean) {
    // Refs should update even if shouldComponentUpdate returns false
    markRef(current, workInProgress);
    if (!shouldUpdate) {
      // **ctx code**
      return bailoutOnAlreadyFinishedWork(current, workInProgress);
    }
    const instance = workInProgress.stateNode;
    let nextChildren;
    // **some dev code**
     nextChildren = instance.render();
    reconcileChildren(current, workInProgress, nextChildren);
    memoizeState(workInProgress, instance.state);
    memoizeProps(workInProgress, instance.props);
    // **ctx code**
    return workInProgress.child;
  }

函数首先去标定需要更新ref,这里为什么跟shouldUpdate无关呢?考虑如下情况,

<Container>
   {data.map((_, index) => <Child ref={'_' + index} />)}
</Container>

也就是说以index为ref,如果Child实现了shouldComponentUpdate,当对Child更改排顺序的时候,实际上只需要做移动操作。从语义上讲,这时候ref是需要更改的,但是shouldUpdate却是false,因此不应把
shouldUpdate作为ref更新的判据。接下来是调用组件的render方法,获取新的element,再传入reconcileChildren做我们常提起的diff操作。diff操作完之后的两步memoize应该是方便打断render之后的恢复操作,最后返回第一个child交给workLoop

由于diff相关的代码比较繁杂,在此先跳回之后(不严格地说“冒泡阶段”)会调用的completeUnitOfWork

function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
    while (true) {
      const current = workInProgress.alternate;
      const next = completeWork(
        current,
        workInProgress,
        nextRenderExpirationTime,
      );
      // ** dev code **
      const returnFiber = workInProgress.return;
      const siblingFiber = workInProgress.sibling;
      if (next !== null) {
        return next;
      }
      if (returnFiber !== null) {
        // **将子树的effect(commit要做的事情)链表并入上层链表**
      }
      if (siblingFiber !== null) {
        return siblingFiber;
      } else if (returnFiber !== null) {
        workInProgress = returnFiber;
        continue;
      } else {
        const root: FiberRoot = workInProgress.stateNode;
        root.isReadyForCommit = true;
        return null;
      }
    }
  }

我们可以先观察一下用循环遍历树的代码:

let node = root;
label: while(true) {
   fnEnter(node);
   if (node.child) {
      node = node.child;
      continue;
   }
   while (!node.sibling) {
      fnExit(node);
      node = node.parent;
      if (!node) break label;
   }
   node = node.sibling;
}

大致结构是先进入子节点,当到达叶子节点时退回最有邻居节点的祖先节点,然后再做循环,如此便能将整个树遍历完。
completeUnitOfWork做的便是 while (!node.sibling)及此行以下的工作。首先会调用completeWork,对于HostComponent(div啥的)会去计算更新需要做的工作然后存入effect,这些工作在beginWork阶段也是能做的,放到这里的缘故应该是考虑到render阶段中可能有嵌套的更新,做的工作可能会“浪费”,所以越晚做浪费的可能性就越小;对于自定义的组件(class、functional),基本上什么都不做;而对于Call而言,会在这一步render出element,然后……去做diff工作,所以返回值不为null的情况也就是Call组件会出现了。后续的代码也就是遍历树而已。

浅探webpack的plugin

众所周知,webpack可以定义很多plugin; 大家也都知道webpack的plugin相关文档约等于0,能够讲清楚webpack的plugin的,笔者还没有见到。在这里,笔者就人肉去探查一番。

此文基于webpack1.13.2

webpack开发者指南(山寨)

webpack的整个打包流程可以大致分为初始化打包器、解析和加载模块、生成和优化chunk、生成和输出资源文件这几个过程,然而webpack的官方文档对开发者很不友好。在此,笔者按照专有名词在整个打包流程中出现的顺序人肉总结了一下,希望能有所帮助:

options

  • 顾名思义,webpack的配置信息,由用户定义的配置和webpack提供的默认配置合并之后生成。webpack的处理非常丰富。例如, defaulter中存储 output.path的方式为this.set('output.path', ''); 此外,options的每个配置项,实际上是有对应的写入规则来控制其行为的,包括call、append、undefined三种, 如果是undefined类型的规则,则是直接取字面量。
  • options的标准形式是对象。但是options可以给出数组,这样相当于定义了多份打包任务,并且数组可以嵌套。

Tapable

  • webpack定义的一个可以定义插件,以及拥有丰富的插件执行方法的类,继承此类就可以拥有插件功能。
  • webpack中定义的插件一般而言是生命周期钩子。
  • 有人给了个Tapable中文文档

Compiler < Tapable

  • 可以认为是打包的任务单位。数量由options决定,如果有多个互相之间是独立的。compiler把一次打包任务划分成了几个部分: make, seal, emit
  • compiler持有下述对象:
    • resolvers { normal, loader, context }
    • parser
    • compilation
  • compiler在如下阶段有插件点 (this = compiler):
    • entry-option == 用户定义的插件加载完后,供修改entry: (options.context, options.entry)
    • after-plugins == 内建插件初始化结束:(compiler)
    • after-resolvers == resolver加载结束: (resolvers)
    • environment == 将封装过的node fs模块传递给compiler对象之后: (void)
    • after-environment == 紧跟上一个:(void)
    • run == 开始执行,实际上就是记录了一个startTime: async, (compiler)
    • compile ==

      NormalModuleFactory, ContextModuleFactory < Tapable

      plugin

  • 只要继承了Tapable,就有插件功能
  • webpack的插件在加载时分内建和用户定义的, 用户的先加载。

D3学习笔记(一)

d3.js 的4.0版本已经拆分成了模块, 这篇issue主要记录的是 d3.js 的一些语言加强(主要是数组方面),以及一些像scale一样的基础模块。

几个比较难记/易混的DOM接口

1. width、height、left、top系列

DOM接口涉及样式就很多样

1.1 client系列

client这个名字看起来很莫名,但是注意搜索的话还是可以找到一些端倪。

client系列来自IE的DTHML模型, IE的DHTML出现在IE4.0, IE4.0发布于1997年,也就是说client这个名字很可能跟windows 图形编程有关。经查,win32的paint接口,把整个窗口中除了边框,各种菜单、工具栏的部分就叫 client area,实际上的意思就是呈现主要内容的部分,这个是出处

这样也就很明朗了,从window推广到每个DOM节点,除去边框和滚动条部分就叫client area了。(不过有意思的是,window反而没有client系列接口,取而代之是IE4.0时的 document.body.clientxx和IE6之后的document.documentElement.clientxx

接口 说明
clientTop 获取border-top的值,原意是border-top和上滚动条之和,好像并不存在上滚动条,所以就等同于上边框高度
clientWidth 元素的内容与padding的宽度和,也就是w3c中的width + padding
clientHeight 元素的内容与padding的高度和,也就是w3c中的width + padding
clientLeft 如果direction是从左到右,那么值就是border-left,如果是从右向左(ar),那么就是左边框加上滚动条
document.documentElement. clientTop/Left 理论上说是浏览器的地址栏以下和整个文档中间的间距,不过现代浏览器看起来都取消了这个
document.documentElement .clientHeight/Width 相当于整个视口的宽高,不过现代浏览器用window.innerWidth/Height更合适

我们可以给出一个结论,client系列表达的是元素自身的内部布局。具体讲,就是对应于CSS的padding、border、width/height系列。

1.2 offset系列

值得注意的是,这个系列的接口属于HTMLElement部分,其他属于Element,也就是说SVG等是不包括这些接口的

从字面意思上看,就是偏移,既然是样式接口,那么一定有对应的CSS属性。CSS中的偏移,也就是top、left、bottom、right,并且只有当position属性不为static时才生效。

接口 说明
offsetParent 按照W3C,只有当元素没有layout-box(display:none,或者不可见元素)或者根节点或者body节点 或者position:fixed时为null,否则定为最近的position属性为非static的祖先元素,或者position 属性为static且为td、th、table的祖先元素,直到找到body,其他情况是null
offsetTop a.body或没有layout-box的元素返回0 b.不满足a时,如果offsetParent为null返回在浏览器中的Y坐标,transform不计入 c.返回和offsetParent的距离 (元素的border顶部和offsetParent的padding顶部的距离)
offsetLeft 与offsetTop同理,只不过变成了左边缘
offsetWidth 简单粗暴地说,就是元素的左右border之间的距离
offsetTop 简单粗暴,元素左右border间距

总结一下,offset系列是元素的框在整个布局上下文中的相对位置、宽高信息。值得注意的是,float元素并没有特殊的offsetParent,因为他不是定位元素。

1.3 scroll

字面看挺好理解的,就是跟滚动相关的接口。

接口 说明
scrollTop 元素滚动距离,实际上是元素的padding顶部和border底部的距离, 如果不可滚动则为0
scrollLeft 元素的padding左边界到border右边界的距离 如果direction是rtl,那么最右是0,向左则负数增长
scrollWidth 元素内容的总宽度,包含可滚动部分
scrollHeight 元素内容总高度,包含可滚动部分

总结一下,scroll主要影响的是后代元素,scrollTop ~ height + scrollTop是子元素的可见部分。

1.4其他

getBoundingClientRect(): 从其接口名字上的构造来说,是获取 client Area的边界矩形的参数,也就是client Area + bounding,最终获取到的也就是该边界相对浏览器视口左上角的坐标,注意返回值是浮点数。更深一步说,这个是绘制时rect的位置。

《Cocoa编程开发者手册》阅读笔记(一)

该书有点年代,讲的OC都是不带ARC的。用Xcode9的话,书中的例子甚至都不能通过编译,网上也鲜有人提及修改的方法,在此把折腾的东西做个记录。

P87 中提及了“利用该机消息枚举”,实质是给容器生成一个代理对象,而向代理对象发送消息的话会将消息转发到容器中的每一个对象,并将调用的结果存入数组中返回。

修改后可通过编译的代码如下:

NSArray+map.h

#import <Foundation/Foundation.h>
@interface NSArray (AllElements)
- (id) map;
@end

@interface NSArrayMapProxy: NSProxy {
    NSArray* array;
}
-  (instancetype) initWithArray: (NSArray*) anArray;
@end

NSArray+map.m

#import "NSArray+map.h"
@implemetation NSArray (AllElements) 
- (id) map {
    return [[NSArrayMapProxy alloc] initWithArray: self];
 }

@end



@implemetation NSArrayMapProxy: NSProxy 
-  (instancetype) initWithArray: (NSArray*) anArray {
    array = anArray;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    for (id obj in array) {
        if ([obj respondsToSelector:sel]) {
            return [obj methodSignatureForSelector:sel];
        }
    }
    return [super methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL selector = [invocation selector];
    NSMutableArray* mutableArray = [NSMutableArray arrayWithCapacity:[array count]];
    for (id obj in array) {
        if ([obj respondsToSelector:selector]) {
            [invocation invokeWithTarget:obj];
            id mapped;
            [invocation getReturnValue:&mapped];
            [mutableArray addObject: mapped];
        }
    }
   // 重要,实质上这里是进入了C field,如果不手动retain的话,数组就会被自动释放了
    CFBridgingRetain(mutableArray);
    [invocation setReturnValue: &mutableArray];
}
@end

cookie规范(RFC 6265)翻译

RFC 6265 要点翻译

1.简介

本文档定义了HTTP Cookie以及HTTP头的Set-Cookie字段。通过使用Set-Cookie头,一个HTTP服务器可以传递name/value键值对以及相对应的元数据(所谓的cookies)到user agent。当user agent向服务器发送后续请求时,user agent会根据元数据和其他信息来决定是否要在Cookie头中返回name/value键值对。

虽然表面上看起来很简单, 但时cookies有很多复杂的地方。例如,服务器在向user agent发送cookie时,对每个cookie会设定一个作用域。 作用域制定了user agent回传cookie的规则:cookie需要回传的最大期限,需要回传cookie到哪些服务器,以及需要应用到哪些模式的URI上。

由于历史原因,cookie包含许多在安全性和隐私上不恰当的地方。例如,服务器可以指定一个给出的cookie字段需要“安全的”连接,但是安全属性并没有保证在存在网络中间人攻击时cookie的完整性。相似的是,给定host的cookies将会被这个host上的所有端口共享,尽管通常来说,浏览器所用的“同源策略”会将从不同端口上取回的东西孤立开来。

这份标准有两类受众:会生产cookie的web服务器的开发者,以及会消费cookie的user agent的开发者。

为了最大化在user agent中的通用性,web服务器在生成cookie时应该把他们自身限制为一个在第4章定义的良好的实现者。

User agent必须实现比第5章中定义的更加宽松的规则,以达到最大化和现有的不符合第4章定义的良好实现者的服务器的互通性。

这份文档说明了在互联网上经常被使用的头的句法和语义。特别地,这份文档并没有创造新的句法和语义。对cookie生成的推荐标准在第4章提供,表述了一些现有服务器行为的子集,在第5章中表述了一些今天并不被推荐的句法和语义,更加宽松的cookie处理算法。某些现存软件的实现和推荐的协议有一些重大的不同,这份文档也包含了一份解释这些不同的内容。

在这份文档之前,至少存在着三份不同的cookie描述:所谓的“Netscape cookie 标准”,RFC 2109, RFC2965。然而,这些文档都没有描述Cookie和Set-Cookie头是如何在互联网上被使用的。根据之前IETF的HTTP状态管理机制的标准,这份文档请求下列操作:

[]将RFC2109的状态改为Historic(已经被RFC2965废止)
[]将RFC2965的状态改为Historic
[]指定RFC2965已经被这份文档废止

特别的是,通过将RFC2965移到Historic并且将其废止,这份文档反对使用Cookie2和Set-Cookie2头。

2.约定

2.1. 一致标准

此处略去几百字

2.2. 句法注解

本文档使用扩充巴科斯范式(ABNF),在RFC5234中有注释。

下列在RFC5234中定义的核心规则被引用,ALPHA(字母),CR(回车),CRLF(CR LF),CTLs(控制字符),DIGIT(数字0-9),DQUOTE(双引号),HEXDIG(十六进制元素 0-9/A-F/a-f),LF(换行符),NUL(空八比特),OCTET(除了NUL以外的所有八比特串),SP(空格),HTAB(水平制表符),CHAR(任意ascii码字符),VCHAR(任意可见的ascii码字符),以及WSP(空白符)。

OWS(可选空白符)规则被用在0个或更多的线性空白符可能出现的场合:

OWS = *([obs-fold]WSP)

obs-fold = CRLF

OWS应该要么不产生要么就产生为一个单独的SP字符。

2.3. 术语

下列术语:user agent,client,server,proxy,origin server(源服务器)和HTTP/1.1标准(RFC2616,1.3节)中的含义相同。

术语request-host是指host的名字,也就是已经被user agent所知的,对user agent来说发送HTTP请求的目的地,或者接收HTTP响应的来源。(也就是发送相应的HTTP请求的host名字)。

术语request-uri已经在RFC2616的5.1.2节中定义。

两个八比特的序列被称为大小写不敏感地相同,当且仅当他们在RFC4790中定义的大小写映射关系被满足时成立。

术语字符串意思是一个非NUL的八位bit序列。

3.概述

这一节概括了一种源服务器将状态信息传递给user agent的方式,也包含了一种user agent将状态信息回传服务器的方式。

为了存储状态,源服务器在HTTP响应中包含了一个Set-Cookie头。在后续的请求中,user agent将回传一个Cookie请求头到源服务器。Cookie头包含了user agent在前面Set-Cookie头中包含的cookie。源服务器可以选择忽略Cookie头或将Cookie用于应用所定义的目的。

源服务器可以在任何响应中发送Set-Cookie响应头。user agent可以在响应码为1xx的请求中忽略Set-Cookie,但必须在除此以外的任何种类响应中处理Set-Cookie(包括响应码为4xx和5xx的响应)。源服务器可以在单个请求的响应中包含多个Set-Cookie字段。Cookie或者Set-Cookie的出现不会阻止存储和复用HTTP请求的缓存。

源服务器不应该把多个Set-Cookie字段打包到单个HTTP头中。通常打包HTTP头的字段可能会更改Set-Cookie字段的语义,因为%x2c(",")字符被Set-Cookie使用,从而在这种打包方式中存在冲突。

3.1. 示例

使用Set-Cookie头,服务器可以向在一个HTTP响应中user agent发送一条短字符串,这条字符串会在未来符合cookie作用域的HTTP请求中回传给服务器。例如,服务器可以给user agent发送一个名叫“SID”的“session标识符”,值为 31d4d96e407aad42。user agent会在后续的请求中回传这个session标识符以及其值。

== Server -> User Agent ==

Set-Cookie: SID=31d4d96e407aad42

== User Agent -> Server ==

Cookie: SID=31d4d96e407aad42

服务器可以使用Path和Domain属性变更cookie的作用域。例如,服务器可以委托user agent在每个path每个example.com的子域都返回cookie。

 == Server -> User Agent ==

Set-Cookie:SID=31d4d96e407aad42;Path=/;Domain=example.com

 == User Agent -> Server ==

Cookie: SID=31d4d96e407aad42

就如下一个例子中展示的那样,服务器可以在user agent中存储多个cookie。例如,服务器可以通过返回两个Set-Cookie字段,实现既存储一个session标识符,又存储用户的偏好语言。值得注意的是,服务器用Secure和HttpOnly属性来对更加敏感的session标识符提供额外的安全保护(见4.1.2.)

 == Server -> User Agent ==

Set-Cookie: SID=31d4d96e407aad42; Path=/; Secure; HttpOnly
Set-Cookie: lang=en-US; Path=/; Domain=example.com

 == User Agent -> Server ==

Cookie: SID=31d4d96e407aad42; lang=en-US

注意上面的Cookie头包含了两个cookie,一个名叫SID,另一个为lang。如果服务器希望cookie在user agent的多个“会话“(sessions,例如,user agent重启之后)中持续存在,服务器可以在Expires属性中指定一个过期时间。注意,如果user agent的cookie存储超过它的定额或者用户手动删除了cookie的话,user agent可能会在过期时间到达之前删除cookie。

 == Server -> User Agent ==

Set-Cookie: lang=en-US; Expires=Wed, 09 Jun 2021 10:18:14 GMT

 == User Agent -> Server ==

Cookie: SID=31d4d96e407aad42; lang=en-US

最后,为了移除一个cookie,服务器要返回一个把过期时间设置在过去的Set-Cookie字段。服务器只有在Set-Cookie头中Path和Domain属性与创建cookie时相符时,才能成功删除cookie。

== Server -> User Agent ==

Set-Cookie: lang=; Expires=Sun, 06 Nov 1994 08:49:37 GMT

== User Agent -> Server ==

Cookie: SID=31d4d96e407aad42

4. 服务端的要求

本节描述了“表现良好”的Cookie和Set-Cookie头的句法和语义。

4.1. Set-Cookie

HTTP响应头中的Set-Cookie被用于从服务器向user agent发送cookie。

4.1.1 句法

不正式地说,Set-Cookie响应头包含了名字叫做“Set-Cookie”并跟着的一个“:”以及一个cookie。每个cookie由一个name-value键值对打头,后面跟着0个或者多个attribute-value键值对。服务器不应该发送未能遵从下列语法的Set-Cookie头。

 set-cookie-header = "Set-Cookie:" SP set-cookie-string  
                    ;Set-Cookie: 之后必须有空格,空格之后才是  
                    ;具体的set-cookie-string    

 set-cookie-string = cookie-pair *( ";" SP cookie-av )  
                    ;cookie-pair以及每个cookie-av  
                    ;之间分隔符都是";" SP(也就是分号加空格)  

 cookie-pair       = cookie-name "=" cookie-value   

 cookie-name       = token  
                    ;token表示的是除了分隔符和CTLs以外的ASCII字符  
                    ;分隔符包括:  
                    ;小中大尖括号 "("|")"|"[]"|"]"|"{"|"}"|"<"|">"  
                    ;空格和水平制表符 SP | HT  
                    ;逗号分号冒号引号问号等号 ","|";"|":"|"?"|"="|"\""  
                    ;斜线: "\" | "/"  
                    ;@: "@"  

 cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )

 cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E  
                       ; ASCII字符中除了CTRL(控制字符, 空白符  
                       ; 双引号, 逗号, 分号, 反斜线(\)  

 token             = <token, defined in [RFC2616], Section 2.2>  

 cookie-av         = expires-av / max-age-av / domain-av /  
                     path-av / secure-av / httponly-av /  
                     extension-av  

 expires-av        = "Expires=" sane-cookie-date  

 sane-cookie-date  = <rfc1123-date, defined in [RFC2616], Section 3.3.1>  

 max-age-av        = "Max-Age=" non-zero-digit *DIGIT  
                       ; In practice, both expires-av and max-age-av  
                       ; are limited to dates representable by the  
                       ; user agent.  

 non-zero-digit    = %x31-39  
                       ; digits 1 through 9  

 domain-av         = "Domain=" domain-value  

 domain-value      = <subdomain>  
                       ; defined in [RFC1034], Section 3.5, as  
                       ; enhanced by [RFC1123], Section 2.1  

 path-av           = "Path=" path-value  

 path-value        = <any CHAR except CTLs or ";">  

 secure-av         = "Secure"  

 httponly-av       = "HttpOnly"  

 extension-av      = <any CHAR except CTLs or ";">  

注意上面的参考文档中某些条目使用了一些和这份文档(ABNF,RFC5234)不同的语法标记。

这份文档没有定义任何关于cookie-value的语义。

为了最大化和user agent的兼容性,服务器在要使用一些任意的数据作为cookie-value时,应该将数据编码,例如使用Base64(RFC4648)。

set-cookie-string中由cookie-av项贡献的部分是被大家熟知的属性。为了最大化和user agent的兼容性,服务器不应该产生在set-cookie-string中有两个属性相同的名字的cookie。(关于user agent如何处理这种情况,见5.3节)

服务器不应该在同一个响应中包含超过一个具有相同cookie-name的Set-Cookie字段。(关于如何处理这种情况,见5.2节)

如果一个服务器向user agent并发地发送了多条包含Set-Cookie头的响应(例如,当在多个sockets上和user agent通信时),这些响应将会创造一个“竞争条件”,最终会导致不可预期的后果。

注意:一些现有的user agent对两位数的年份有不同的处理。为了避免兼容性问题,服务器应该使用要求四位数年份的RFC1123定义的日期格式。

注意:一些user agent会用32位的UNIX time_t来存储和处理日期。time_t相关的库的bug可能会导致这些user agent在2038年之后错误地处理日期。

4.1.2. 句法(非正式)

本节描述了简化的关于Set-Cookie头的语义。这些语义对于要理解最常见的服务器上cookie用法已经足够详细。全部地语义在第5节描述。

当user agent接收到一个Set-Cookie头时,user agent会将cookie及其属性一起存储。随后,当user agent发起HTTP请求时,user agent会在Cookie头中包含合适的并且没有过期的cookie。

如果user agent接收到了一个和某个现有cookie的cookie-name、domain-value和path-value都相同的新cookie,现有的那个cookie将会被驱逐,取而代之的是那个新cookie。注意服务器可以通过向user agent发送一个拥有值为过去某一时刻的Expires属性的新cookie,来删除一个cookie。

除非cookie的属性额外指定,cookie将只会回传到源服务器(例如,不会回传到任何子域上),并且cookie将会在当前会话结束时过期(会话由user agent自己定义)。user agent会忽略未被识别的cookie属性(但不会忽略整个cookie)。

4.1.2.1. Expires属性

Expires属性指明了cookie的最大生命周期,形式为cookie过期的时刻。user agent并不被要求在设定的时间之前保留cookie。实际上,user agent经常由于存储压力或者隐私上的考虑驱逐了cookie。

4.1.2.2. Max-Age属性

Max-Age属性指明了cookie的最大生命周期,形式为cookie过期之前的具体秒数。user agent并不被要求在这段指定的时长内保留cookie。实际上,user agent经常由于存储压力或者隐私上的考虑驱逐了cookie。

注意:某些现有的user agent并不支持Max-Age属性。不支持Max-Age属性的user agent将会直接忽略。

如果cookie既有Max-Age也有Expires属性,Max-Age属性将会有更高的优先级,并且控制cookie的过期时间。如果一个cookie既没有Max-Age也没有Expires属性,user agent将会在本次会话(会话由user agent定义)结束之前保留这个cookie。

4.1.2.3. Domain属性

Domain属性指明了cookie会被发送到哪些host。例如,如果某cookie的Domain属性的值为"example.com",user agent将会在向example.com,www.example.com以及www.corp.example.com(注意,最前面的%x2E("."), 如果出现,将会被忽略,尽管它除了出现在末尾以外都是非法的)发送HTTP请求时,在Cookie头中包含该cookie。如果服务器漏掉了这个Domain属性,user agent只会向源服务器返回cookie。

警告:某些现存的user agent会将不存在Domain属性时,错误地假设为Domain属性存在,并且值为当前的host name。例如,如果example.com返回了一个没有Domain属性的Set-Cookie头,这些user agent将会错误地也向www.example.com发送cookie。

user agent将会拒绝cookie,除非Domain属性为cookie指定的作用域会包含源服务器。例如,user agent将会接受一段来自“foo.example.com”的Domain属性为"example.com"或者"foo.example.com"的cookie,但是user agent不会接受Domain属性为"bas.example.com"或者"baz.foo.example.com"的cookie。

注意:出于安全的原因,许多user agent被设定为拒绝Domain属性对应为"公共结尾"的cookie。例如,一些user agent将会拒绝Domain属性为"com"或"co.uk"等。(详见5.3节)

4.1.2.4. Path属性

每个cookie的作用域被限定到了由path组成集合中,由Path属性控制。如果服务器没有提供Path属性,user agent将会使用当前的require-uri中path元素的“目录”作为默认值(更多细节详见5.1.4节)

user agent会在一次HTTP请求中包含该cookie,条件是require-uri中路径的部分匹配Path属性(或者是Path的子目录),其中%x2F("/")被解释为路径分隔符。

虽然这看起来对分隔同一host中不同路径的cookie十分实用,但是Path属性不能作安全的凭据。(见第8节)

4.1.2.5.Secure属性

Secure属性将cookie的作用域限定到“安全的”传输途径(“安全的”是有user agent所定义的)。当一个cookie拥有Secure属性时,user agent只有在请求是从一个安全的传输途径(典型的是以TLS方式传输HTTP,也就是HTTPS,RFC2818)传输时,才会发送该cookie。

尽管这看起来对保护cookie以免受中间人攻击很有用,但是Secure属性只在机密性上保护了cookie。一个网络中间攻击者可以通过非安全的传输途径覆盖该cookie,从而破坏其完整性。(详见8.6节)

4.1.2.6. HttpOnly属性

HttpOnly属性将cookie的作用域限制到HTTP请求中。尤其是,这个属性委托user agent在提供非HTTP方式访问cookie时(例如,浏览器提供给脚本访问cookie的接口),忽略该cookie。

4.2. Cookie

4.2.1. 句法

user agent会将存储的cookie放在Cookie头中发送给源服务器。如果服务器遵从4.1中的要求(并且user agent遵从第5节的要求),user agent将会发送符合下述语法的Cookie头部:

cookie-header = "Cookie:" OWS cookie-string OWS  

cookie-string = cookie-pair *( ";" SP cookie-pair )  

4.2.2. 语义

每个cookie键值对都表述了一个被user agent保存的cookie。cookie键值对包括从Set-Cookie头中接收到的的cookie-name和cookie-value。

注意cookie的属性没有被返回。尤其是,服务器不能单靠Cookie头部就能确定,什么时候cookie会过期,cookie对哪些host有效,对什么路径有效,还有cookie是否设置了Secure或者HttpOnly属性。

每个单独的cookie的语义没有在该文档中定义。服务器被期望以应用相关地语义来填充cookie。

虽然cookie在Cookie头中被线性地序列化,但是服务器不应该依赖序列化的顺序。尤其是,当Cookie头中包含了两个具有相同名字的cookie时(例如,被设置成不同Path或者Domain属性但拥有相同名字的cookie),服务器不应该依赖这些cookie在头部中出现的顺序。

5. User Agent的要求

本节用具体地细节说明了Cookie和Set-Cookie头部,使得实现这些要求的user agent可以和现存的服务器(即使那些不满足第4节中要求的)进行互操作。

5.1. 子元素算法

本节定义了user agent所用的用于处理Cookie和Set-Cookie头部子元素得一些算法。

5.1.1. 日期

user agent必须使用一个和下述算法等价的算法来实现解析cookie-date。注意下述的各种被定义为算法一部分的boolean-flag(例如,found-time,found-day-of-month, found-month,found-year)最初状态是“没有设定”)。

1.使用下述算法,将cookie-date分割成date-token

cookie-date     = *delimiter date-token-list *delimiter  
date-token-list = date-token *( 1*delimiter date-token )  
                  ;date-token由终结符分隔  
date-token      = 1*non-delimiter  

delimiter       = %x09 / %x20-2F / %x3B-40 / %x5B-60 / %x7B-7E  

non-delimiter   = %x00-08 / %x0A-1F / DIGIT / ":" / ALPHA /%x7F-FF  
                  ;此处的非终结符指的就是非ascii,以及ascii码中的不可见字符  
                  ;以及字母和数字,以及:  
                  ;也就是说非终结符在可见ascii字符中只包括数字字母和冒号  

non-digit       = %x00-2F / %x3A-FF  

day-of-month    = 1*2DIGIT ( non-digit *OCTET )  
month           = ( "jan" / "feb" / "mar" / "apr" /  
                       "may" / "jun" / "jul" / "aug" /  
                       "sep" / "oct" / "nov" / "dec" ) *OCTET  

year            = 2*4DIGIT ( non-digit *OCTET )  
time            = hms-time ( non-digit *OCTET )   
                ;可以看出时间之后必须要有一个数字来分隔  
                ;但是再往后是什么都无所谓了  
hms-time        = time-field ":" time-field ":" time-field  
time-field      = 1*2DIGIT  

2.按照下述顺序出列每个在cookie-date中出现的token:

1. 如果found-time的flag还没有设定并且token匹配了time产生式,设定found-time的flag,并且相应地  设定hour、minute、second为token所表示的值。跳过下面剩余的步骤,并继续处理下一个  date-token。  
2. 如果found-day-of-month的flag还没有被设定,并且token匹配了day-of-month的产生式,设定  found-day-of-month的flag,并且相应地设定day-of-month为token所表示的值。跳过下面剩余的步骤,  并继续处理下一个date-token。
3. 如果found-month的flag还没有被设定,并且token匹配了month的产生式,设定found-month的flag,  并且相应地设定month为token所表示的值。跳过下面剩余的步骤,并继续处理下一个date-token。  
4. 如果found-year的flag还没有被设定,并且token匹配了year的产生式,设定found-year的flag,  并且相应地设定year为token所表示的值。跳过下面剩余的步骤,并继续处理下一个date-token。

3.如果year的值大于70且小于等于99,在year的值的基础上增加1900.

4.如果year的值大于0且小于等于69,在year值得基础上增加2000.

  1. 注意:某些现有的user agent会用不同的方式解析两位数的年份。

5.在下列情况中,终止这些步骤并且解析cookie-date以失败告终:

[*] 至少在found-day-of-month,found-month,found-year,或者found-time的flag中有一个没有设定

[*] day-of-month的值小于1或者大于31

[*] year的值小于1601

[*] hour的值大于23

[*] minute的值大于59

[*] second的值大于59

(注意在这个句法中闰秒不能被体现)

6.将解析出的cookie-date转换为UTC时间。如果这个日期不存在,终止算法并且以失败告终。

7.将转换出的时间作为算法的结果返回。

5.1.1. 规范化host名

一个规范的host名是通过下述算法生成的字符串:

1. 将host名转换为独立域名标签序列.  
2. 将每个不属于LDH非保留字标签的标签,转换为A标签详见[RFC5890] 2.3.2.1),或者转换为“punycode 标签”(RFC3490的第四节所定义的“TOASCII”方法).  
3. 将最终的标签序列拼接,中间以%x2E(".")分隔.  

5.1.3. Domain匹配

只有在满足下列条件之一时:

A. domain 字符串和给定的字符串相同。(注意两者会被规范化为
小写之后进行比较)  
B. 当满足下列所有条件时:  
    [*] domain字符串是给定字符串的一个尾部子串  
    [*] 给定字符串不包含在domain字符串中的最后一位字符是%X2E(".")  
    [*] 该字符串是一个host name(也就是说,不是一个ip地址)

给定的字符串才匹配给定的domain字符串的domain。

注意此处Domain是指hostname,是不包含端口号的

5.1.4. 路径和路径匹配

user agent必须使用和下述算法等价的算法来计算cookie的默认路径:

1. 将uri-path设定为require-uri中的path部分,如果path不存在, 则设为空。  
例如,如果request-uri刚好包含了path(以及可选的query string),那么  
uri-path就是该path(不包括%X3F("?")字符或者query string),而当request-uri  
包含一个完整的绝对路径时,uri-path就是URI的path元素。  

2. 如果uri-path为空,或者uri-path的第一位不是%X2F("/")字符,输出%x2F并且  
跳过剩余的步骤。  

3. 如果uri-path只包含不超过一个%x2F("/"),输出%x2F并且  
跳过剩余的步骤。  

4. 输出uri-path从左边数第一位到最后一个%x2F("/")之间的字符串,但是结果不  
包含最后一个%x2F("/")

至少满足下面的一个条件时,request-path才匹配cookie-path的路径:

[*] cookie-path和request-path相等  
[*] cookie-path是request-path的前缀,并且cookie-path的最后一位是%x2F("/")  
[*] cookie-path是request-path的前缀,并且第一个不包含在cookie-path的
字符是%x2F("/")

5.2. Set-Cookie头

当user agent在一次HTTP响应中接收到一个Set-Cookie字段时,user agent可能会完全会略Set-Cookie字段。例如,user agent可能希望禁止“第三方”cookie(见7.3节)。

如果user agent没有完全忽略Set-Cookie字段,那么user agent必须解析将Set-Cookie头中每个字段的值作为cookie-string解析。(下面将定义)

注意:下述算法比4.1节中的语法更加宽松。例如,该算法会将cookie的name-value的头尾空白符
移除(但保留中间的空白符),不过4.1节中的语法禁止了这些地方的空白符。user agent使用这个算法来和哪些不符合第四节中的推荐的服务器进行互操作。

user agent必须使用和下面所述的算法等价的算法来解析set-cookie-string:

1. 如果set-cookie-string包含%x3B(";"), 那么name-value-pair由set-cookie-string  
开头到第一个%x3B(“;”)组成,但不包含%x3B,并且unparsed-attributes由剩下的set-cookie-string  组成(包括%x3B); 否则,name-value-pair就是set-cookie-string,并且  
unparsed-attributes时空串。  

2. 如果name-value-pair缺少%x3D("=")字符,那么完全忽略set-cookie-string。      
3. name字符串(可能为空)由name-value-pair的开头,到第一个%x3D("=")组成,但不包含%x3D,  同时value(可能为空)由第一个%x3D之后的所有字符组成。    

4. 移除name和value的开头和结尾处的所有WSP(空白符)。    

5. 如果name为空,完全忽略set-cookie-string。    

6. cookie-name就是name字符串,cookie-value就是value字符串。  

user agent必须使用和下面所述的算法等价的算法来解析unparsed-attributes:

1. 如果unparsed-attributes为空,跳过剩下的这些步骤。    

2. 丢弃unparsed-attributes的第一个字符串(也就是%x3B)。    

3. 如果剩下的unparsed-attributes中包含%x3B(“;”),那么消耗从开头到第一个%x3B  
的字符串(但不包含);否则,消耗剩下的所有unparsed-attributes。之后,令  
这个步骤中消耗的字符串为cookie-av。  
4. 如果cookie-av包含一个%x3D(“=”),那么attribute-name(可能为空)由开头  
到第一个%x3D组成,并且不包含%x3D,同时attribute-name(可能为空)为第一个  
%x3D之后的字符串;否则,attribute-name的值为整个cookie-av,并且attribute-value  
为空。  

5. 移除attribute-name和attribute-value的首尾空格。    

6. 根据下面子章节所述的要求解析attribute-name和attribute-value(注意,未被  
识别的attribute-name将会被忽略)。  

7. 回到算法的第一步。

当user agent完成对set-cookie-string的解析时,就说user agent从require-uri中获取到了
名字为cookie-name,值为cookie-value,以及属性为cookie-attribute-list的cookie。(由接收到cookie所触发的额外的要求详见5.3节)

5.2.1. Expires属性

如果attribute-name大小写不敏感地匹配了字符串“Expires”,user agent必须按照下列步骤
处理cookie-av。

令expiry-time的值为将attribute-value按cookie-date(见5.1.1节)解析后的值。

如果attribute-value没有成功地解析成一个cookie date,那么忽略这个cookie-av。

如果expiry-time晚于user agent可以表达的最晚的时间,user agent可以将expiry-time
替换为user agent可以表达的最晚的时间。

如果expiry-time早于user agent可以表达的最早的时间,user agent可以将expiry-time
替换为能表达的最早的时间。

向cookie-attribute-list追加一个属性名为Expires,属性值为expiry-time
的属性。

5.2.2. Max-Age属性

如果attribute-name大小写不敏感地匹配了字符串“Max-Age”,user agent必须按照下列步骤
处理cookie-av。

如果attribute-value的第一个字符不是DIGIT(数字)或者“-”,那么忽略cookie-av。

如果attribute-value剩余的字符中包含一个非DIGIT(非数字)字符,那么忽略这个cookie-av。

令delta-seconds为attribute-value转换为整数之后的值。

如果delta-seconds小于或等于0,令expiry-time为最早可以表达的日期和时间。否则,
令expiry-time为当前的日期和时间加上delta-seconds的秒数。

向cookie-attribute-list追加一个属性名为Max-Age,属性值为expiry-time
的属性。

5.2.3. Domain属性

如果attribute-name大小写不敏感地匹配了字符串“Domain”,user agent必须按照下列步骤
处理cookie-av。

如果attribute-value是空值,那么行为是未知的。然而,user agent应该忽略整条cookie-av。

如果attribute-value的第一个字符是%x2E("."),令cookie-domain为除去attribute-value第一个%x2E(".")之后的值;否则,令cookie-domain的值为整个attribute-value。

将cookie-domain转换为小写。

向cookie-attribute-list追加一个属性名为Domain,属性值为cookie-domain
的属性。

5.2.4. Path属性

如果attribute-name大小写不敏感地匹配了字符串“Path”,user agent必须按照下列步骤
处理cookie-av。

如果attribute-value时空值或者attribute-value的第一个字符不是%x2F(“/”),
那么令cookie-path为默认路径;否则,令cookie-path为整个attribute-value。

向cookie-attribute-list追加一个属性名为Path,属性值为cookie-path
的属性。

5.2.5. Secure属性

如果attribute-name大小写不敏感地匹配了字符串“Secure”,user agent必须向cookie-attribute-list追加一个属性名为Secure,属性值为空的属性。

5.2.5. HttpOnly属性

如果attribute-name大小写不敏感地匹配了字符串“HttpOnly”,user agent必须向cookie-attribute-list追加一个属性名为HttpOnly,属性值为空的属性。

5.3. 存储模型

user agent的每个cookie会存储下列所述的字段:name,value,expiry-time,domain,
path,creation-time,last-access-time,persistent-flag,host-only-flag,
secure-only-flag,以及http-only-flag。

当user agent从一个request-uri接受了一个拥有名叫cookie-name,值为cookie-value,
以及属性为cookie-attribute-list的cookie时,user agent必须像下面这样处理cookie:

1. user agent可能完全忽略某个接收到的cookie。例如,user agent可能希望禁止掉来自  
第三方的cookie,或者user agent不希望存储超过某个大小的cookie。   

2. 新建一个名字为cookie-name,值为cookie-value的新cookie。将creation-time和  
last-access-time设定为当前日期和时间。  

3. 如果cookie-attribute-list包含一个属性名为"Max-Age"的属性,那么将cookie  
的presistent-flag设为true,将cookie的expiry-time设定为cookie-attribute-list中  
最后一个属性名为Max-Age的属性的属性值; 

否则,如果cookie-attribute-list中包含一个属性名为“Expires”的属性  (并且不包含“Max-Age”属性) ,那么将cookie的presistent-flag设定为true,并且将cookie的expiry-time  
设定为cookie-attribute-list中最后一个属性名为Expires的属性值;    

否则,将cookie的presistent-flag属性设为false,并且将cookie的expiry-time设定为  
能表达的最远的日期。  

4. 如果cookie-attribute-list包含一个属性名为“Domain”的属性,令domain-attribute  
为cookie-attribute-list中最后一个属性名为Domain的属性值;否则,令domain-attribute为空串。    

5. 如果user agent被配置为拒绝“公共后缀”,并且domain-attribute的值为某个  
公共后缀时:如果domain-attribute和规范化后的request-host相同的话,令domain-  
attribute属性为空串;否则,忽略整个cookie病跳过这些步骤。  

> 注意: “公共后缀”是一个由公开注册机构控制的域名,比如说“com”,"co.uk",以及
> "pvt.k12.wy.us"。一个步骤对于预防从attacker.com,通过设置一个Domain属性
> 为"com"的cookie,来破坏example.com的cookie完整性很重要。不幸的是,
> 公共后缀(著称的注册商控制的域名)的集合随时间发生着变化。如果可能的话,user agent
> 应该用一份最新的公共后缀列表,例如有Mozillas维护的一份列表(https://publicsuffix.org/)。  

6. 如果domain-attribute非空:如果规范化之后的request-host不匹配domain-attribute  
中的域名,那么完全忽略掉cookie并且终止这些步骤;否则,将cookie的host-only-flag  
设定为false,并且将cookie的domain设定为domain-attribute。    
否则:将cookie的host-only-flag设定为true,并且将domain设定为规范化之后的request-host。    

7. 如果cookie-attribute-list包含一个属性名为“Path”的属性,将cookie的path  
属性设定为cookie-attribute-list中最后一个属性名为“path”的属性值;否则,将  
cookie的path设定为request-uri的默认路径。(5.1.4节)    

8. 如果cookie-attribute-list包含一个属性名为“Secure”的属性时,将cookie的  
Secure-only-flag设定为true;否则,设为false。    

9. 如果cookie-attribute-list包含一个属性名为“HttpOnly”的属性时,将cookie的  
http-only-flag设定为true;否则,设为false。  

10. 如果cookie是从非HTTP的API传入的,并且设定了cookie的http-only-flag,那么  
终止这些步骤并且完全忽略该cookie。  

11. 如果cookie的存储中已经包含了一个和新建的cookie的name、domain、path  
都相同的cookie,那么:a. 令old-cookie为现存的domain、name、path都相同的  
cookie(注意这个算法保持了至多只有一个这样cookie的不变性);b. 如果新创建的  
cookie是从一个非HTTP的API接收到的并且old-cookie的http-only-flag已经设定,  
那么终止这些步骤并且完全忽略这个新创建的cookie;c. 将新创建的cookie的creation-time  
更新为old-cookie的creation-time;d. 将old-cookie从cookie 存储中移除。    

12. 将新创建的cookie插入到cookie存储中。

当cookie的过期时间是在过去时,这个cookie就是“过期的”。

user agent必须从cookie存储中清除掉所有的过期cookie,条件为在任何时刻,cookie
存储中存在一个过期的cookie时。

在任何时刻,user agent都可能会从cookie存储中“移除过量的cookie”,条件为共享
同一个domain字段的cookie的数量超过了预设的边界值(例如50个cookie)。

在任何时刻,user agent都可能会从cookie存储中“移除过量的cookie”,条件为所有cookie的总数量超过了预设的边界值(例如3000个cookie)。

当user agent从cookie存储中移除过量的cookie时,user agent必须按下面的优先级
清除cookie:

  1. 过期的cookie
  2. 共享一个Domain的cookie数目超过其他cookie预设的一个数量(?)时
  3. 所有的cookie

如果两个cookie具有相同的移除优先级,那么user agent必须先移除last-access
更早的那个cookie。

当“当前会话结束”(由user agent定义)时,user agent必须从cookie存储中
移除所有persisitent-flag属性为false的cookie。

5.4. Cookie头

user agent将会在HTTP请求的Cookie头中包含存储的cookie。

当user agent产生一个HTTP请求时,user agent禁止产生多余一个的Cookie头字段。

user agent可能会在HTTP中删除Cookie头部。例如,user agent可能希望禁止
发送从第三方请求中获取到的cookie。(见7.1节)

如果user agnet已经把一个Cookie头部字段添加到HTTP头部中,user agent必须把cookie-string(由下面定义)当做这个头部字段的值发送。

user agent必须用一个等价于下面所述的算法来从cookie存储中结合request-uri,来
计算“cookie-string”:

1. 令cookie-list为cookie存储中符合下面所有要求的cookie的集合:
    [*] 要么,cookie的host-only-flag为true,并且规范化后的request-host和
        cookie的域名相同;或者,cookie的host-only-flag为false,并且规范化后
        的request-host和cookie的domain匹配;  
    [*] request-uri的path部分和cookie的path部分相匹配;  
    [*] 如果cookie的secure-only-flag为true, 那么request-uri的scheme
    必须表示一个“安全的”协议(由user agent定义)。  
    > 注意:“安全的”协议不是由本文档定义的。典型的情况是,user agent会将使用
    安全传输的协议认为是安全的,比如SSL或者TLS。例如,大多user agent会将“https”
    看作是表示安全协议的scheme。  
    [*] 如果cookie的http-only-flag被设定为true,那么这个cookie将会在为“非HTTP”
    接口(由user agent定义)生产cookie-string时被忽略。   

2. user agent应该按照下列顺序对cookie-list进行排序:
    [*] 拥有更长的path的cookie将会排在拥有更短的path的cookie前面。  
    [*] 当cookie拥有相同的path字段长度时,拥有更早creation-time的cookie将会
    被排在更晚creation-time的cookie前面。  
    > 注意:不是所有的user agent都按这个吮吸排列cookie-list,但是这个顺序
    反应了在撰写本文档时以及历史上,现有的服务器(错误地)所依赖的顺序的经验。    

 3. 将cookie-list中每个cookie的last-access-time更新为当前的时刻。  

 4. 将cookie-list序列化为一个cookie-string,需要按照下列顺序处理每个cookie:
    1. 输出cookie的名字,%x3D("=")字符,以及cookie的值.
    2. 如果在cookie-list中还存在未处理的cookie,输出字符%x3B以及%x20("; ").
 >注意:除了它的名字,cookie-string其实是一个八bit序列,而不是字符序列。为了将
 cookie-string(或者其中的元素)转换为字符序列(例如,为了展示给用户),user agent
 可能希望尝试使用UTF-8编码(RFC3629)来解码这个八bit序列。但是,这个解码可能会失败,因为
 不是所有的八bit序列都是合法的UTF-8.

6. 实现上的考虑

6.1. 限制

实际的user agtn实现在他们能存储的cookie的数目和大小上有限制。常见的user agent
应该提供下述的最小容量:
[] 每个cookie至少4096bytes(以cookie的name,value和attribute之和衡量)
[
] 每个domain至少50个cookie
[*] 至少总共3000个cookie

服务器应该使用尽可能小和尽可能小的cookie来防止达到这些实现上的限制,以及最小化
网络带宽的需求,因为Cookie头会在每一个请求中都被包含。

如果user agent未能在Cookie头中返回一个或者多个cookie,服务器应该优雅地处理
这种情况,因为user agent随时可能会按照用户的要求移除任何cookie。

6.2. API

Cookie和Set-Cookie头使用一个这么难懂的句法的原因是,许多平台(包括服务器和
user agent)都提供了一个基于字符串的有关cookie的API,这就要求应用层的开发者
自己生成和解析Cookie和Set-Cookie头,这导致许多程序员错误地实现了,最终导致
不兼容问题。

为了替代提供基于字符串的cookie相关API的做法,平台最好提供更加语义化的API。推荐
详细的API设计超过了本文档的范畴,但是显然接收一个抽象的"Date"对象而不是一段序列化的
date string会有清晰的好处。

6.3. IDNA依赖与迁移

IDNA2008(RFC5890)取代了IDNA2003(RFC3490)。然而,在两份说明中存在差异,
因此处理(比如,转换)从一个标准下注册的域名标签到另一个存在差异。IDNA2003存在的
过渡时期会有一定的时常。user agent应该实现IDNA2008(RFC5890)并且可能会实现
UTS46或者RFC5895以加快IDNA的过渡。如果user agent没有实现IDNA2008,那它应该
实现IDNA2003(RFC3490)。

7. 隐私的考虑

cookie因为允许服务器追踪用户饱受批评。例如,很多“web分析”公司使用cookie来识别
用户是否回到了网站或者访问了另一个站点。虽然cookie不是服务器唯一可以用来追踪跨
HTTP请求的手段,但是cookie促进了追踪,因为他们在user agent的会话之间可以一直存在,
并且可以被多个host共享。

7.1. 第三方cookie

特别令人担忧的是所谓的“第三方”cookie。在渲染一个HMTL文档时,user agent经常从其他
服务器请求资源(例如广告网络)。这些第三方服务器可以使用cookie来跟踪用户,尽管用户
没有直接访问他们的服务器。例如,如果一个用户访问了一个带有第三方内容的网站,之后用户
浏览另一个包含这个内容的网站时,第三方可以跨域两个站点追踪用户。

一些user agent限制了第三方cookie的行为。例如,其中一些user agent拒绝在向第三方的请求中
发送Cookie头部。另外一些则是拒绝处理第三方请求的响应中的Set-Cookie头部。user agent在
处理的第三方cookie策略方面有很多不同。本文档允许user agent在很大范围内尝试第三方cookie的策略,来满足用户对隐私和兼容性的需求。然而,本文档并不偏向于任何特别的第三方cookie策略。

禁止第三方cookie的策略,当服务器尝试绕过这个追踪用户的限制来实现他们隐私追踪时,是无效的。
尤其是,两个协作的服务器经常通过动态url而不是cookie添加识别信息,来追踪用户的时候。

7.2. 用户控制

user agent应该提供一种给用户管理存储在cookie存储中cookie的机制。例如,user agent可能
让用户删除一段时间内的所有cookie或者和某个domain相关的所有cookie另外,许多user agent
都包含了一个让用户检查存储在cookie store中的cookie的界面。

user agent应该提供一种可以让用户禁用cookie的机制。当cookie被禁用时,user agent禁止在
发出的HTTP请求中包含Cookie头部,并且user agent处理收到的HTTP响应中的Set-Cookie头部。

一些user agent提供了阻止cookie跨session存储的选项。当这个被配置到的时候,user agent
必须将所有收到的cookie当做persistent-flag设定为false进行对待。一些流行的user agent
通过“匿名浏览模式”来开放这个功能。

某些user agent提供了允许用户自己写入cookie的能力。在大多数常见的场景中,这会产生大量的
对话框。然而,尽管如此,某些看中隐私的用户认为这个功能很有用。

7.3. 过期时间

虽然服务器可以将cookie的过期时间设定到一个遥远的未来,但是绝大多数user agent实际上并不会
将cookie保留几十年。与其选择无厘头的很长的国务时间,服务器应该,基于实际目的来选择一个合适的cookie过期时间,以提高用户的隐私性。例如,一个典型的cookie标识符应该设置成过期时间为
两周比较合理。

8. 安全考虑

8.1. 概述

cookie可能有很多安全陷阱。本节概述了几个比较显著的问题。

尤其是,cookie鼓励开发者依赖Ambient Authority做认证,往往提供了被攻击的弱点,比如说跨站请求伪造(CSRF)。同时,当在cookie中存储session标识符时,开发者往往也留下了session fixation的隐患。

传输层加密,例如使用HTTPS,并不足以防御网络攻击者获得或者更改受攻击者的cookie,因为cookie协议本身有很多脆弱性(见下面的“弱机密性”和“弱完整性”)。另外,默认情况下,cookie不能从网络攻击者那儿获得完整性和机密性,甚至在使用HTTPS时。

8.2. 环境授权(Ambient Authority)

使用cookie来认证用户的服务器可能会承受安全上的脆弱性,因为一些user agent允许远程组织从
user agent发送HTTP请求(例如,通过HTTP重定向或者HTML表单)。当处理这些请求时,user agent会附上这些cookie,尽管远程组织不知道cookie的内容,但是也潜在地允许远程组织利用在
没有防备性的服务器的授权。

尽管这个安全担忧有过许多名字(比如,跨站请求伪造,confused deputy),这个问题起源于将cookie作为一种环境授权(Ambient Authority)。cookie鼓励服务器的管理员将名称(用URL的形式)和授权(cookie)中分离。结果是,user agent可能会向攻击者指定的地址进行授权,可能导致服务器和客户端承受由攻击者指定的动作,因为他们曾经被用户授权。

服务器管理员可能会考虑通过将URL作为授权表,将名字(URL)和授权绑在一起,来作为取代使用cookie作为授权的手段。取代将secret存在cookie的是,这个方法将secret存在URL中,要求远程
实体自己提供授权。虽然这个方法不是万能药,审慎的运用这个原则可以获得更好的安全性。

8.3. 明文

除非以安全的途径传输(例如TLS),在Cookie和Set-Cookie字头是以明文传输的。

  1. 这些头部中所有传输的敏感信息都曝光给了窃听者。
  2. 一个怀有恶意的中间人可以更改任一方向中的头部,带来不可预知的后果。
  3. 一个怀有恶意的客户端可以在传输之前更改Cookie头部,带来不可预知的后果。

当传输到user agent时(就算在安全的隧道发送cookie也是),服务器应该加密并签名cookie的内容(使用任何服务器想使用的格式)。然而,加密并签名的cookie内容并没有预防攻击者将cookie从一个user agent转移到另一个,或者之后重放cookie进行攻击。

签名和加密每个cookie的内容之外,要求一个更高安全等级的服务器应该只在一个安全的隧道中
使用Cookie和Set-Cookie头。当使用安全渠道的cookie时,服务器应该设定每个cookie的Secure
属性。如果服务器没有设置Secure属性,由安全隧道提供的保护将会很大程度上的没有意义。

例如,考虑一个webmail服务器,将session标识符存在一个cookie钟,并且通过HTTPS访问。如果
服务器没有在它的cookie上设定安全属性,一个活跃的网络攻击者将可以拦截任何由user agent发出的HTTP请求并且将其请求重定向到向HTTP上的webmail服务器。就算webmail服务器没有监听HTTP连接,user agent也会在该请求中包含cookie。这个活跃的网络攻击者拦截这些cookie,并且向服务器重放攻击,之后获取到用户的邮件内容。如果,取而代之,服务器在cookie中设定了Secure属性,
user agent将会在明文请求中包含这个cookie。

8.4. session标识符

服务器一般在cookie中存储一个nonce(或者session标识符)来代替,直接在cookie中存储session信息(可能会被攻击者获得或者重放)。当服务器收到一个拥有nonce的HTTP请求时,服务器会把nonce当做key查找和cookie相关联的状态信息。

使用session标识符限制了攻击者可以造成的危害,如果攻击者拿到了cookie的内容,因为nonce是唯一一个和服务器交互的有用信息(不像非nonce得cookie内容,其本身就是敏感的)。而且,使用单个nonce避免了攻击者将两个交互中的内容混杂起来,造成服务器不可预期的行为。

使用session标识符也不是完全没有风险。例如,服务器应该避免“session fixation”造成的易受攻击性。固化session攻击以下面三个步骤进行。首先,攻击者将session标识符从他的user agent中
移植到受害者的user agent中。第二,受害者使用这个session和服务器交互,可能会用用户信息填充这个session标识符。第三,攻击者直接使用这个session标识符和服务器交互,可能会获得授权信息或者机密信息。

8.5. 弱机密性

cookie没有提供端口隔离。如果一个cookie在一个端口上是可读的,那么这个cookie对另一个运行在同一个服务器上不同端口的服务来说也是可读的。如果一个cookie在一个端口上是可写的,那么这个cookie对另一个运行在同一个服务器上不同端口的服务来说也是可写的。出于这个原因,服务器不应该
在具有相同host的不同端口上运行相互不信任的服务,并且用cookie来存储安全敏感信息。

cookie没有提供协议带来的隔离性。虽然绝大多数通常都使用http和HTTPS协议,给定host的cookie也可能被其他协议获取到,例如ftp和gopher。虽然缺少协议隔离性,对从非HTTP的API获取cookie的权限是显著的,但是实际上由协议导致的隔离性缺失表现在了他们需要自己处理cookie(例如,考虑通过HTTP取回一个使用gopher协议的URI)。

cookie往往也没有提供基于path的隔离性。虽然网络层的协议并没有将存储在一个path的协议发到另一个,但是某些user agent通过非HTTP的api暴露了这些cookie,例如HTML的document.cookie API。因为其中的某些user agent(例如,浏览器)并没有将从不同path获得的资源隔离起来,从一个path取回的某个资源也可能可以拿到存储到另一个path的cookie。

8.6. 弱完整性

cookie没有为兄弟域名(极其子域名)提供完整性保障。例如,考虑foo.example.com和bar.example.com。foo.example.com服务器可以设置一个Domain属性为"example.com"的cookie(可能覆盖一个现有的由bar.example.com设置的"example.com"的cookie)。并且user agent会在想bar.example.com的HTTP请求中包含这个cookie。在最坏的情况下,bar.example.com将不能把这个cookie和一个自己设定cookie区分开来。foo.example.com服务器可能会利用这个能力来发起对bar.example.com的攻击。

尽管Set-Cookie头支持Path属性,path属性也没有提供任何完整性保证,因为user agent将会接受一个Set-Cookie头部的任意路径。例如,一个对 http://example.com/foo/bar 的HTTP响应,可以设置一个Path属性为"/qux"的cookie。因此,服务器不应该在相同host上的不同path运行两个相互不信任的服务,并且使用cookie来存储安全敏感信息。

某个活动的网络攻击者也可以向发送到 https://example.com/ 的请求中的Cookie头部注入cookie,方法是仿造一个 http://example.com/ 的响应并且注入一个Set-Cookie头。位于example.com的HTTPS服务器将不能分辨gaicooie是否是他自己在HTTPS响应中设置的cookie。一个活跃的网络攻击者可能会通过这个机制来攻击example.com,即使example.com只使用了HTTPS。

译者注:例如,用户直接敲了一个HTTP站点,www.baidu.com,在重完成定向到HTTPS之前,中间人可以伪造跳转,比如返回一个302跳转到 https://www.baidu.com 同时中间人设定一个cookie

服务器可以通过加密和签名他们的cookie来缓和这些攻击。然而,使用密码学并没有完全缓解这个问题,因为一个攻击者可以重放TA从真实的example.com服务器中的sessuib获取到的cookie,以导致不可预期的后果。

最后,一个攻击者可以通过存储大量cookie来强迫user agent删除掉cookie。一旦user agent达到了它的存储限制,user agent会被迫驱除掉某些cookie。服务器不应该依赖user agent对cookie的保留。

8.7. 依赖DNS

cookie依赖DNS系统来提供安全性。如果DNS部分或全部受损,cookie协议可能会无法提供应用所要求的安全属性

9. IANA考虑

永久的消息头部注册(RFC3864)已经被下列登记项目更新。

9.1. Cookie

头部名称: Cookie

应用协议: http

状态: 标准

作者/变化控制者: IETF

标准文档: 本文档(5.4节)

9.2. Set-Cookie

头部名称: Set-Cookie

应用协议: http

状态: 标准

作者/变化控制者: IETF

标准文档: 本文档(5.2节)

9.3. Cookie2

头部名称: Cookie2

应用协议: http

状态: 已淘汰

作者/变化控制者: IETF

标准文档: RFC2965

9.4. Set-Cookie2

头部名称: Set-Cookie2

应用协议: http

状态: 已淘汰

作者/变化控制者: IETF

标准文档: RFC2965

互联网免费服务集锦

网上有很多免费又好用的服务,为了使用查找方便,在此备忘

1.持续集成、代码质量

travis-ci 基本上github上标配的持续集成平台,支持多种语言,但是貌似没有windows平台的ci

appveyor 比较好用的windows平台ci,开源项目免费

codecov 很好的代码覆盖率展示平台

coveralls很多人推荐,不过笔者遇到过好几次服务挂掉的情况,而且感觉这个站点很丑

codeclimate检查代码的坏味道,会给代码打GPA,4.0满分

semaphoreci据TJ说是最好用的CI

gitlabci除了无缝跟gitlab集成以外,看不出任何优势,持续集成是在docker里面跑的

2.互联网基础设施系列

cloudflare提供免费的DDOS防护和CDN加速服务,提供SSL,应用非常广泛,使用很方便

letsencrypt质量高、免费、火爆的证书

使用React + baas搭建blog实录(一)

使用webpack,babel搭建React基本开发环境,完成Hello,world。

如果下述命令不方便执行,可以直接执行git clone https://github.com/renaesop/react-leancloud-blog.git && cd react-leancloud-blog && git reset --hard 56a9c26 && npm i, 然后全局安装webpack之后即可构建

1. 初始化npm项目,添加.gitignore等

  • git init 初始化git项目,将会产生.git文件夹
  • echo ".idea \n node_modules \n logs \n *.log \n" > .gitignore 用于设定被git Track的文件列表
  • npm init并按照提示操作,将产生package.json,也就是npm项目的元描述文件

2. 添加React,以及相应的babel插件

  • npm i --save react react-dom 引入react, react-dom
  • npm i --save babel-core 引入babel
  • echo "{\"presets\": [\"es2015\", \"react\"]}" > .babelrc 设定babel的插件
  • npm i --save-dev babel-preset-es2015 babel-preset-react 安装babel所需的插件集,注意preset相当于一系列的plugin

3. 添加webpack,完成Hello, world!

  • npm i --save-dev webpack 安装webpack
  • npm i --save-dev babel-loader 增加webpack的一个loader,babel-loader
  • touch webpack.config.js 添加webpack配置文件
  • 编辑webpack.config.js, 其中内容见文件链接
  • 新增html入口文件index.html, 其中内容见文件链接
  • 新增文件夹src,并在src下新建文件,entry.jsx, 其中的内容如文件链接

4. 构建并运行

  • npm i -g webpack 全局安装个webpack(其实并不一定要全局安装,不过这里为了简单,暂时不描述不全局安装)
  • webpack 执行构建
  • 打开index.html 应该能够看到Hello,world!

5. 增加代码风格检测

  • echo "{\"extends\": \"airbnb\"}" > .eslintrc 新增eslint的配置,继承自airbnb
  • echo "node_modules/**/* \n dist/**/*" > .eslintignore 新增eslint忽略文件的配置
  • npm i --save-dev eslint-config-airbnb eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y eslint
  • 更新webpack.config.js的配置, 执行npm run lint
  • 如果使用webstorm, 可以在preference中搜索到eslint,并启用

call/apply漫谈

在JavaScript中,call/apply是函数原型上的方法,作用是指定函数的context也就是所谓的this变量。JavaScript中this的指向“不明”饱受诟病然而,实际上this的指向十分清晰,this永远指向函数的调用者,而call/apply的作用可以简单地说成了强行更改调用者。

不过,为什么好像其他常用的语言中没有出现this的混淆和call/apply这种函数呢?实际上,门门语言都有这个问题,因为从根本上说,this本身就只能通过当做参数来传入,我们的计算机底层只能调用函数/过程,只是,其他语言都是会有隐式绑定,也就是类似于箭头函数的行为的。

下面是几个语言this实现举例,其中大都涉及到面向对象的实现机制:

Java的实现

嗯,Java是个纯粹的面向对象语言,基于class。使用Java的时候,要显式使用this的场景,只有函数形参或者局部变量和类的成员变量名冲突的时候。
假设我们有这样一个类:

class Hello {
    Hello() {
    }
    public void sayHello () {
        say("Hello, world!");
    }
    private void say(String str) {
        System.out.println(str);
    }
    public static void main(String[] args) {
        Hello hello = new Hello();
        hello.sayHello();
    }
}

在这个简单的java程序中,由静态方法实例化了Hello类,之后调用了实例方法sayHello, 而实例方法sayHello又调用了另一个实例方法say。根据Java虚拟机规范(Java SE第八版本),大致调用过程如下:

  1. 静态方法main被传入第一个参数 args,此参数是一个数组的引用;
  2. main中构造Hello;
  3. main中调用了hello的sayHello,在jvm中实际进行的操作为,向sayHello传入hello作为第一个参数,也就是传入this,注意,this是被传入的~;
  4. 在sayHello中调用了另一个实例方法say,jvm中进行的操作为,向say传入sayHello接收的实参中的第一个参数作为say的this参数;

我们可以得出一个结论,Java语言中,一般(除了奇葩的构造器方法)只有实例方法拥有this,而且,这个this还是作为参数传递进来的。在实例方法中调用静态方法的时候,并没有向静态方法传入this,因此不能在静态方法中调用实例=。=

Objective-C的实现

OC是一个神奇的语言,完全与C兼容,实际上最终的编译也是转化成C的。OC里面等价于this的东西是self。

OC可以动态地添加方法:

#import <objc/runtime.h>
// 中間省略
void myMethodIMP(id self, SEL _cmd) {
    doSomething();
}
class_addMethod([MyClass class], @selector(myMethod), (IMP)myMethodIMP, "v@:");

可以清晰地看到,OC的runtime里面,真正干事儿的方法的C代码表示,接受的参数更加明显,第一个就是self,第二个参数甚至是SEL。

从上可以得知,OC的this也是通过传参实现的。

C++的实现

=。=我并不懂cpp。

但是,从查到的资料看,C++中,所有的class或者struct在编译完成之后,成员函数,都会丢失所有信息,其this指针作为函数首个参数传入。

从上述几个语言看来,this实际上在底层都是作为参数传入的,也就是说js中的call/apply只是把底层暴露出来了而已!!!令人奇怪的是,另外几个语言都不能自定义this,只能由编译器隐式传入。

引申一点,虽然this对于面向对象来说极其重要,但是最终编译的结果中,保留的信息只有成员变量。根本没有什么成员函数,只是一堆能够额外接受一个指针的普通函数。怪不得有人说面向对象是骗局……

对JS原型的一些思考

我们常常听说JavaScript是一门基于原型的面向对象语言,然后也有一堆说“原型”的文章,不过大都是列举一下用法,说说原型链,就完了。至于究竟“原型”是什么,语义是什么,甚少有文章会提到。

原型这个词其实是非常中肯的。

简述

写程序的都知道“原型”这个东西,就是万恶的产品经理经常搞的那个嘛~假设产品经理是用的Sketch做的原型,那么设计会从产品经理那儿拷贝一份产品经理做好的原型,然后在上面修改(当然不一定这么干,不过可以这么干就是了),最后的苦逼的小前端就会基于设计给出的设计稿生成页面。

我们可以这么说,设计做的事情是把原型改成设计稿, 而小前端做的则是把设计稿作为参考生成html/css/js。而JavaScript中的原型,也差不是这个理,也就是用来复制一个副本 & 在副本的基础上修改。所以说原型这个词是比较中肯的。

起源

从网上都能找到JavaScript里基于原型的面向对象(Prototype-based oo)的起源,Self语言(真正的起源其实应该是actor,不过这货资料极其稀有)。这个小众的Self语言,网上没什么资料。

在只言片语中,可以找出一些Self语言中的重要信息:

  • Self中一切都是对象,对象有数个slot(slot是一个方法或数据,跟js的属性有点类似)
  • Self中新建对象的方法为复制现有对象,再酌情增删改slot oldObj copy newProp: 'Hello, World!'.
  • Self中被复制的对象可以叫做prototype,因为原对象并不完善,要复制一份出来修修补补
  • Self中可以明确地选取slot指明parent属性,以供delegation

另外有一篇paper讨论了delegation(委托)和inheritance(继承)分别实现prototypal 和 class版本的面向对象编程的“行为共享”。[1]

语义

原型,顾名思义,本身就拥有一定的功能,只需要改改就能用。JavaScript中每个可构造的函数(非箭头函数)都有个prototype字段,这货就是所谓的“原型”。

按照基于原型的OO语言的惯例,新建对象相当于对原有对象的扩展或者复制。对应到JS,就是prototype,而JavaScript有一个new关键字,在语言的spec中的语义是将构造函数F的prototype设定为新对象的proto, 并执行F。我们可以换一种等价的说法,new操作符创建了一个新对象,并将F.prototype以某种形式复制(实际上是给了个引用,但是可以理解为写时复制),而F本身则是对新对象的一些修饰。这样的,原型就真的实至名归了。

既然new是对原型的拷贝,那么很自然的就有:obj instanceof F的判别方式是obj是否具有F.prototype的所有属性;Function.prototype 是function(){}。这样是很符合直觉的。

delegation

网上流行一个所谓的“原型继承”的说法。其实经过考证,这个词应该是Douglas Crockford创造的(不能说他坏话),“原型继承”想要表达的意思就是implicit delegation。

implicit delegation就是,向对象A请求属性prop时,A会首先在自身查找,如果不存在,则查找其proto,此过程会持续到proto为null。也就是我们常说的原型链。我们可以发现,其实这个是复制属性的语义的超集(如果运行时proto都不变就跟复制一样)。

虽然我们常说继承是为了多态,但是从效果上看继承是共享代码。也就是说,继承只是一种共享代码的方式,delegation也是共享代码的方法,而且都知道delegation可以实现继承的效果,反之则不成立。

prototype对比class

  • 类比编程语言,编译语言的编译阶段就好像是class到instance的过程,而执行阶段则像是原型被复制并修饰(由程序加载器把程序加载到内存的指定位置)
  • docker的image可以说是运行时容器的prototype
  • 人类认知事物的时候往往是通过某个具体的单位,比如某一头大象开始的,而不是通过class(也是抽象的属性们)

一些事实

  • JS中判断对象的类型,都是根据一些slot(或者说属性),这些属性的访问可能不完全符合原型链的规则。比如说继承基本类型如Array,原生extends写法和由babel转译出来的extends并不是完全等价的(虽然按理说语义上等价)。

总的来说,原型式面向对象就是通过复制(都明白,不一定真的把每个属性都弄一份副本,写时复制也等价)旧的对象来生成新对象,并在此之上进行修改的编程方式。“复制”则就有了复用代码的语义,只是JS的“复制”恰好使用的是软复制(就像symlink……)。

参考:
[1] .Lieberman H. Using prototypical objects to implement shared behavior in object-oriented systems[C]//ACM Sigplan Notices. ACM, 1986, 21(11): 214-223.

突破JS的作用域规则

会JS的同学都知道JS的作用域规则,也就是在函数和块的内部,可以访问外部的变量,比如全局变量。看起来这是一条语言铁律,但是实际上我们可以突破这一点,可以让函数无法访问到外部的变量甚至全局变量。

首先给出一段典型的代码:

var a = 1;
function factory() {
  console.log(a);
}
factory();

毫无疑问,这段代码会输出1,因为factory可以访问到外部的变量。那我们可不可以通过一定的手段让factory无法访问到a呢,答案是肯定的,只是需要一定的技巧。

首先,我们需要明确一点,JS函数中变量的引用是在函数定义点决定的,也就是说,只要在定义函数的地方访问不到外部的变量,函数也就访问不到了(废话︿( ̄︶ ̄)︿)。不过这听起来还是没啥用。

其次,我们知道,在JS中有个被批的很惨的关键字——with,而且还被严格模式给驱逐出境了。with被批的原因,一在于他可以改变作用域规则,让局部变量的优先级甚至更低;二则是因为浏览器厂商痛恨他,因为有他不利于性能提升。由此可以看出,我们可以利用with来改变一下作用域,比如说想要屏蔽到外部的变量a,就可以给个对象里面含有同名变量名。结合第一点,我们可以写出下面的代码:

var a = 1;
var shadow = {
  a: 2,
};
function factory() {
  console.log(a);
}
function sandbox(factory) {
  with (shadow) {
    return eval('((' + factory.toString() + ')())');
  }
}
sandbox(factory);

上述代码会输出2,不过你肯定想说,这有什么卵用,我只能指定某个变量,然后拿个假的值来保护原值。嘿,这是没什么卵用,但是如果配合上ES6的一个新特性——Proxy就很有用了。

ES6的Proxy应该是大家很少接触的一个特性,这个特性使得JS可以元编程了。嘿,结合Proxy可以把我们的sanbox函数改变得很有用处:

var a = 1;
function sandbox(factory) {
  var shadow = new Proxy({
    window: {},
    factory,
    eval,
    console,
  }, {
    get(target, key, reveiver) {
      return Reflect.get(target, key, reveiver);
    },
    has() {
      return true;
    }
  });
  with (shadow) {
    return eval('((' + factory.toString() + ')())');
  }
}
function factory() {
  console.log(a);
  console.log(this.a);
  !function () {
    var b = 3;
    console.log(b);
  }();
  console.log(b);
}
sandbox(factory);

执行这个函数可以发现,外部变量甚至全局变量a无法被访问到了。而且由于函数的缘故,内部的作用域并没有被打乱,也就是说我们实际上获得了一个真·沙盒!!!

通过使用sandbox,我们完完全全突破了JS的作用域规则。对外部变量的访问受到我们控制了,只能访问到我们所设置的“白名单”里的对象,想怎么玩就怎么玩。

使用cloudflare和nginx加速任意站点(水)

往往有很多站点,虽然后端速度还可以,但是由于服务器在美帝,导致速度慢的抠脚,用的时候简直崩溃。众所周知,CDN是互联网重要的基础设施,然而,某些站长可能并没有上CDN这艘大船,在这时候,我们就可以用自己的手段来加速自己上网了。

下面将以建立一个简单小站,并进行CDN加速为例子描述。

A. 选一个免费空间,建站

随手搜了一个,选了个免费PHP共享空间的站的,不造是什么烂站

然后,常规操作,开免费主机,选一个免费的二级域名(这个一看就是基于vhost的那种共享主机),我选了http://519.96.lt/ (感觉我需要治疗)。然后,随意选择一个建站模板,这里我选择了一个买衣服的电商,因为图片很多,势必会比较慢,比较好试出效果。

这样第一步就差不多了。

B. 买一个顶级域名

不乱安利了,现在pw、top之类的域名是辣条价,假设买的是es2016.top。然后去把DNS服务器(权威服务器)改成cloudflare提供的DNS服务器,也就是matt.ns.cloudflare.comwanda.ns.cloudflare.com

C. 弄一台自己的服务器,比如digitalocean

装上nginx,然后配上反向代理:

upstream shop {
  server 519.96.lt;
}
server {
  listen 80;
  proxy_set_header Host 519.96.lt;
  proxy_set_header Accept-Encoding "";
  server_name www.es2016.top;
  location / {
      proxy_pass  http://shop;
      sub_filter      '519.96.lt' 'www.es2016.top';
  }
}

然后reload nginx

D. 在cloudflare配置加速

cloudflare添加好站点之后,选择DNS,然后在DNS面板新增一条A记录,比如说www, ip指向之前所配置的服务器,再选中面板上面的加速。

之后可以选择配置html、css、js的自动压缩。

cloudflare加速之后,会自动压缩可以压缩的内容,并且自动支持了https.

面向前端的CDN原理介绍

内容分发网络(Content delivery network或Content distribution network,缩写:CDN)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

为什么需要CDN

根本上的原因是,访问速度对互联网应用的用户体验、口碑、甚至说直接的营收都有巨大的影响,任何的企业都渴望自己站点有更快的访问速度。而HTTP传输时延对web的访问速度的影响很大,在绝大多数情况下是起决定性作用的,这是由TCP/IP协议的一些特点决定的。物理层上的原因是光速有限、信道有限,协议上的原因有丢包、慢启动、拥塞控制等。

要提高访问速度,最简单的做法当然就是多设置几个服务器,让终端用户离服务器“更近”。典型的例子是各类下载网站在不同地域不同运营商设置镜像站,或者是像Google那样设置多个数据中心。但是多设几个服务器的问题也不少,一是多地部署时的困难,二是一致性没法保障,三则是管理困难、成本很高。实际上,在排除多地容灾等特殊需求的情况下,对大多数公司这种做法是不太可取的。当然,这种方案真正做好了,甚至是比后续所说的使用CDN要好的。

CDN是一种公共服务,他本身有很多台位于不同地域、接入不同运营商的服务器,而所谓的使用CDN实质上就是让CDN作为网站的门面,用户访问到的是CDN服务器,而不是直接访问到网站。由于CDN内部对TCP的优化、对静态资源的缓存、预取,加上用户访问CDN时,会被智能地分配到最近的节点,降低大量延迟,让访问速度可以得到很大提升。

CDN的原理

CDN做了两件事,一是让用户访问最近的节点,二是从缓存或者源站获取资源

CDN有个源站的概念,源站就是提供内容的站点(网站的真实服务器), 从源站取内容的过程叫做回源。

每次访问的具体流程如图(以最普通的CDN为例)

流程图

具体举个例子:

用户在首次访问 https://assets-cdn.github.com/pinned-octocat.svg , 假设不委托local DNS服务器递归查询,会经历以下几个过程

  1. 浏览器检查本地有没有这个东东的有效缓存,有则使用缓存,没有有效缓存则进行对assets-cdn.github.com的DNS查询,获得一个 CNAME记录, igithub.map.fastly.net,值得注意的是,多个加速域名可以解析到同一个CNAME,CDN回源和缓存的时候考虑到了hostname,👍;
  2. 进行对github.map.fastly.net的DNS查询,获得一个A/AAAA记录,给出地址103.245.222.133(视网站不同返回的不一样,可以有多个), 这一步对CDN来说时十分重要的,它给出了离用户最近的边缘节点;
  3. 浏览器选一个返回的地址,然后进行真正的http请求,开始向103.245.222.133握手,握手完了把http请求头也发给了该边缘服务器;
  4. 边缘服务器检查自己的cache里面有没有https://assets-cdn.github.com/pinned-octocat.svg这个资源,有则返回给用户,如果没有,向CDN中心服务器发起请求;
  5. CDN中心服务器检查自己的cache里面有没有这个资源,有则返回给边缘服务器,没有则回源;
  6. 中心服务器发现客户配置了github.map.fastly.net的回源地址(这个只有cdn会知道,假设是xxx.xxx.xxx.xxx),就把http请求发到源站地址上,源站返回后返回给请求方;

可以看出CDN加速的原理很大部分是跟DNS挂钩在一起的,CDN供应商几乎一定需要一个智能DNS服务器。CDN可以拿到所有的明文数据,所以对数据安全性、保密性要求比较高的企业会选择自建CDN或者设置NS记录,指向自建的智能DNS服务器。

上述步骤每一步都可以缓存,注意是每一步! 所以CDN要清除缓存很难,因为有很多服务器上的缓存要清除。无论是用户对边缘服务器的请求,还是CDN服务器的回源都可以使用https。

注意,实际环境中图中每个服务器都可以是集群,甚至CDN分区域中心和总中心。

使用karma实现mock

karma是一个著名的浏览器测试框架,他的特色是启动一个nodejs服务器,在浏览器端通过http下载脚本,并通过websocket实现karma和浏览器实现双向通信。

网上甚少有使用karma做mock的教程。但是因为karma使用了connect模块,有中间件的能力,因此值得一试。

引入karma

karma官方支持的是jasmine,所以省事一点就直接用jasmine(不过这货返回promise没有官方支持)。

  • 在项目中新建文件karma.conf.js
module.exports = config => {
    config.set({
        basePath: './test',
        files: ['**/*.spec.js'],
        browsers : [
          'Chrome',
        ],
        frameworks: ['jasmine'],
        singleRun: true,
    });
}
  • 新建 test/entry.spec.js
describe('Hello, test', () => {
    it('should work', () => {
        const data = 0;
        expect(data).toEqual(0);
    });
});
  • 安装依赖
    npm i karma karma-jasmine karma-chrome-launcher jasmine-core -D

  • 执行测试
    node_modules/.bin/karma start

执行之后,可以看到浏览器被自动打开然后关闭,控制台出现一片绿色提示,表示测试成功了。

实现mock

吐槽一下,karma深受Java/AngularJS的影响,什么都搞依赖注入,怪恶心的。

karma支持插件,插件的结构应该如此:

.......
// optional
fn.$injector = [xxx, xxx ,xxx]

module.exports = {
    `${plugin type}:${plugin name}`: [`${fn type, factory or value}`, fn]
}

插件主要有4种类型分别是 frameworks, reporters, launchers and preprocessors, 另外还有我们要用的middleware。

插件默认会接收到config作为参数(就是我们写的配置文件),也可以通过对fn执行$injector属性指定fn被注入的参数。

如此我们就开始实现一个我们的middleware。

  • 新建 test/mock/middleware.js 文件
// 比较简单,当路径匹配到指定的mockUriStart时
// 则加载processors下的模块处理请求

function mockFactory(config) {
  const mockUrl = config.mockUriStart || '/mock-api/';
  return function (req, res, next) {
    if (req.url.indexOf(mockUrl) === 0) {
      const path = req.url.slice(mockUrl.length);
      try {
        const processor = require(`./processors/${path}`);
        processor(req, res);
      }
      catch (e) {
        next();
      }
    }
    else {
      next();
    }
  };
};
// 我们只需要config,因此就不指定$injector属性了
module.exports = {
  'middleware:mock': ['factory', mockFactory]
};
  • 新建 test/mock/processors/simple.js文件
module.exports = function (req, res) {
  res.end('Simple data');
};
  • 修改karma.conf.js, 这里有个大坑点,如果你想加载不处于node_modules的插件,则需要手动在plugins中添加。但是一旦有了plugins选项,那么所有的插件加载都要手动添加,包括在node_modules中的插件。
module.exports = config => {
    config.set({
        basePath: './test',
        files: ['**/*.spec.js'],
        browsers : [
          'Chrome',
        ],
        frameworks: ['jasmine'],
        middleware: ['mock'],
        plugins: [
            require('./test/mock/middleware'),
            require('karma-jasmine'),
            require('karma-chrome-launcher'),
        ],
        singleRun: true,
    });
}
  • 修改 test/entry.spec.js
describe('Hello, test', () => {
    it('should work', (done) => {
       const xhr = new XMLHttpRequest();
       xhr.open('GET', '/mock-api/simple', true);
       xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
               console.log(xhr.response);
               done();
             }
      };
    xhr.send(null);
    });
});
  • 执行测试
    node_modules/.bin/karma start

将会看到karma的控制台有LOG: 'Simple data'的字样。

至此,就完成了一个简单的mock server,只要扩充mock中间件对路由的处理逻辑或者让mock中间件做其他mock服务器的代理,就可以拥有更加完整的功能。

Web Workers 草案翻译

基于11.27日的草案版本,在线地址

摘要

这份Spec定义了一个API,这个API使web应用开发者能够生成与他们的主页面并行的、在后台运行的worker脚本。这也使得,只要将消息传递作为协作机制,就能实现类线程的操作。

1. 简介

1.1. 范畴

本节是非正式的

这份Spec定义了一个用于运行与用户界面脚本独立的后台脚本的API。

这使得持续运行一些脚本,而不被响应点击或者其他用户交互的脚本打断,成为可能。而且,也使得长时任务可以被持续运行,而不用让出调度权来使得页面保持响应性。

Worker(这些后台脚本就叫这个)是相对而言比较重的,并且也不想被大规模地创建出来。例如,为一个4M大小的图片的每个像素创建一个worker是不合适的。下面的例子将展示一些worker恰当的用法。

一般而言,worker具有较长的存活期,有一个比较大的启动性能开销并且每个实例有略高的内存消耗。

1.2. 示例

本节是非正式的

worker有许多用途。下述子章节展示了几种用途。

1.2.1. 进行后台数值运算的worker

本节是非正式的

worker最简单的用途是,运行计算密集型任务而不打断用户的交互。

在这个例子中,主文档派生一个worker(naively)来计算素数,并展示最近被找到的素数。

住文档页面如下所述:

示例1 main.html

<!DOCTYPE HTML>
<html>
 <head>
  <title>Worker example: One-core computation</title>
 </head>
 <body>
  <p>The highest prime number discovered so far is: <output id="result"></output></p>
  <script>
   var worker = new Worker('worker.js');
   worker.onmessage = function (event) {
     document.getElementById('result').textContent = event.data;
   };
  </script>
 </body>
</html>

调用Worker()构造器将会创建一个worker,并返回一个Worker对象用于表示worker,该对象被用于和worker通信。返回对象的onmessage事件handler将使得代码可以从worker接收消息。

worker本身的代码如下所述:

示例2 worker.js

var n = 1;
search: while (true) {
  n += 1;
  for (var i = 2; i <= Math.sqrt(n); i += 1)
    if (n % i == 0)
     continue search;
  //  找到素数
  postMessage(n);
}

The bulk of this code is simply an unoptimised search for a prime number. The postMessage() method is used to send a message back to the page when a prime is found.
这份代码中的大部分都只是未经优化的素数搜索。postMessage() 方法被用于找到新素数后向页面回传数据。

1.2.2. 用于后台I/O的worker

本节是非正式的

在这个示例中,主文档使用了两个worker,一个用于以固定地频率获取股票更新,而另一个用于处理用户请求的搜索查询。

主页面代码如下

示例3 main.html

<!DOCTYPE HTML>
<html>
 <head>
  <title>Worker example: Stock ticker</title>
  <script>
   // Ticker
   var symbol = 'GOOG'; // default symbol to watch
   var ticker = new Worker('ticker.js');

   // Searcher
   var searcher = new Worker('searcher.js');
   function search(query) {
     searcher.postMessage(query);
   }

   // Symbol selection UI
   function select(newSymbol) {
     symbol = newSymbol;
     ticker.postMessage(symbol);
   }
  </script>
 </head>
 <body onload="search('')">
  <p><output id="symbol"></output> <output id="value"></output></p>
  <script>
   ticker.onmessage = function (event) {
     var data = event.data.split(' ');
     document.getElementById('symbol').textContent = data[0];
     document.getElementById('value').textContent = data[1];
   };
   ticker.postMessage(symbol);
  </script>
  <p><label>Search: <input type="text" autofocus oninput="search(this.value)"></label></p>
  <ul id="results"></ul>
  <script>
   searcher.onmessage = function (event) {
     var data = event.data.split(' ');
     var results = document.getElementById('results');
     while (results.hasChildNodes()) // Clear previous results
       results.removeChild(results.firstChild);
     for (var i = 0; i < data.length; i += 1) {
       // Add a list item with a button for each result
       var li = document.createElement('li');
       var button = document.createElement('button');
       button.value = data[i];
       button.type = 'button';
       button.onclick = function () { select(this.value); };
       button.textContent = data[i];
       li.appendChild(button);
       results.appendChild(li);
     }
   };
  </script>
  <p>(The data in this example is not real. Try searching for "Google" or "Apple".)</p>
 </body>
</html>

这两个worker使用了一个公共的库来进行实际的网络请求。这个库如下所示:

示例4 io.js

function get(url) {
  try {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, false);
    xhr.send();
    return xhr.responseText;
  } catch (e) {
    return ''; // Turn all errors into empty results
  }
}

股票更新的worker如下所述:

示例5 ticker.js

importScripts('io.js');
var timer;
var symbol;
function update() {
  postMessage(symbol + ' ' + get('stock.cgi?' + symbol));
  timer = setTimeout(update, 10000);
}
onmessage = function (event) {
  if (timer)
    clearTimeout(timer);
  symbol = event.data;
  update();
};

搜索查询的worker代码如下所示:

示例6 search.js

importScripts('io.js');
onmessage = function (event) {
  postMessage(get('search.cgi?' + event.data));
};

1.2.3. shared worker简介

本节是非正式的

本节用一个Hello World的示例介绍了shared worker。shared worker使用一些稍微不同的API,因为每个woker都可能有多个连接。

第一个示例展示了你该怎样连接一个worker,以及一个worker如何在页面连接上它的时候回传信息。接收道德消息将在log中展示。

下面是HTML页面:

示例7 main.html

<!DOCTYPE HTML>
<title>Shared workers: demo 1</title>
<pre id="log">Log:</pre>
<script>
  var worker = new SharedWorker('test.js');
  var log = document.getElementById('log');
  worker.port.onmessage = function(e) { // Note: Not worker.onmessage!
    log.textContent += '\n' + e.data;
  }
</script>

下面是js代码

示例8 test.js

onconnect = function(e) {
  var port = e.ports[0];
  port.postMessage('Hello World!');
}

通过改变两件事,第二个例子扩展了第一个示例。首先,消息用addEventListener()接收而不是用event handler 的IDL属性;其次,一个消息被发送给worker,使得worker发回另一个消息。接收道德消息也将在log中展示。

下面是HTML代码:

示例9 main.html

<!DOCTYPE HTML>
<title>Shared workers: demo 2</title>
<pre id="log">Log:</pre>
<script>
  var worker = new SharedWorker('test.js');
  var log = document.getElementById('log');
  worker.port.addEventListener('message', function(e) {
    log.textContent += '\n' + e.data;
  }, false);
  worker.port.start(); // Note: Needed when using addEventListener
  worker.port.postMessage('ping');
</script>

这是worker的代码

示例10 test.js

onconnect = function(e) {
  var port = e.ports[0];
  port.postMessage('Hello World!');
  port.onmessage = function(e) {
    port.postMessage('pong'); // Not e.ports[0].postMessage!
    // e.target.postMessage('pong'); // Also works
  }
}

最后这个例子被扩展到展示两个页面如何与同一个worker通信;在这个例子中下,第二个页面仅仅是第一个页面的一个iframe,但是在一个完全独立的独立定级浏览上下文(就是浏览器的不同窗口 / 标签页)中也有相同的原理。

这是外层的html页面:

示例11

<!DOCTYPE HTML>
<title>Shared workers: demo 3</title>
<pre id="log">Log:</pre>
<script>
  var worker = new SharedWorker('test.js');
  var log = document.getElementById('log');
  worker.port.addEventListener('message', function(e) {
    log.textContent += '\n' + e.data;
  }, false);
  worker.port.start();
  worker.port.postMessage('ping');
</script>
<iframe src="inner.html"></iframe>

这是内层的html页面:

示例12

<!DOCTYPE HTML>
<title>Shared workers: demo 3 inner frame</title>
<pre id=log>Inner log:</pre>
<script>
  var worker = new SharedWorker('test.js');
  var log = document.getElementById('log');
  worker.port.onmessage = function(e) {
   log.textContent += '\n' + e.data;
  }
</script>

这是worker的代码:

示例13

var count = 0;
onconnect = function(e) {
  count += 1;
  var port = e.ports[0];
  port.postMessage('Hello World! You are connection #' + count);
  port.onmessage = function(e) {
    port.postMessage('pong');
  }
}

1.2.4. 用shared worker共享状态

本节是非正式的

Node 原生模块杂谈

网上谈Node C++扩展的文章种类比较单一,基本上都是在说怎么去写扩展,而对模块本身的解读相当少,笔者恰巧拜读了相关代码,在此做个记录。

注: 文中的“原生模块”均是指代C++模块

Node如何加载原生模块

朴灵老师的《深入浅出Node.js》一书其实有谈过这个问题,但是随着Node项目的演进,已经发生了一些微妙的变化。

原生模块被存在链表中,原生模块的定义为:

struct node_module {
// 表示node的ABI版本号,node本身导出的符号极少,所以变更基本上由v8、libuv等依赖引起
// 引入模块时,node会检查ABI版本号
// 这货基本跟v8对应的Chrome版本号一样
  int nm_version; 
// 暂时只有NM_F_BUILTIN和0俩玩意
  unsigned int nm_flags;
// 存动态链接库的句柄
  void* nm_dso_handle;
  const char* nm_filename;
// 下面俩函数指针,一个模块只会有一个,用于初始化模块
  node::addon_register_func nm_register_func;
// 这货是那种支持多实例的原生模块,不过扩展写成这个也无法支持原生模块
  node::addon_context_register_func nm_context_register_func;
  const char* nm_modname;
  void* nm_priv;
  struct node_module* nm_link;
};

原生模块被分为了三种,内建(builtint)扩展(addon)已链接的扩展(linked),分别含义为:

  • 内建:Node.js的原生C++模块,
  • 扩展: 用require来进行引入的模块
  • 已链接的扩展:非Node原生模块,但是链接到了node可执行文件上(这货几乎没用)

所有原生模块的加载均使用的是extern "C" void node_module_register(void* mod)函数,而mod这个参数实际上就是上面的node_module,不过node_module被放在了node这个namespace中,所以只能设置为void*, 函数的实现很简单:

extern "C" void node_module_register(void* m) {
  struct node_module* mp = reinterpret_cast<struct node_module*>(m);
  // node实例创建之前注册的模块挂对应链表上
  if (mp->nm_flags & NM_F_BUILTIN) {
    mp->nm_link = modlist_builtin;
    modlist_builtin = mp;
  } else if (!node_is_initialized) {
    // "Linked" modules are included as part of the node project.
    // Like builtins they are registered *before* node::Init runs.
    mp->nm_flags = NM_F_LINKED;
    mp->nm_link = modlist_linked;
    modlist_linked = mp;
  } else {
// 这货是调用`process.dlopen`时出现
    modpending = mp;
  }
}

不过代码里面并不会直接去调用node_module_register,而是通过宏来生成调用这个函数的代码:

  • NODE_MODULE: 普通的原生模块
  • NODE_MODULE_CONTEXT_AWARE: 支持单进程多node实例的原生模块
  • NODE_MODULE_CONTEXT_AWARE_BUILTIN: 内建模块均支持多实例,跟上个宏只是多一个flag

这些宏的作用都是使得模块的注册在main函数之前发生(如果模块被链接到了node上),或者在uv_dlopen返回前完成。值得注意的是,真正的模块初始化是要执行nm_**_register_func的。

内存**有四个存储node_module的链表,均是static变量(所以并不是线程安全的……),分别为:

  • modpending: 主要用于加载C++ addon时传递当前加载的模块
  • modlist_builtin: 存储内建模块的链表,process.binding函数会查找这个链表来获取模块并初始化
  • modlist_linked: 存储已链接模块, process._linkedBinding函数查此表
  • modlist_addon: 存储C++ addon,可能会问为啥有了modpending还会要这货,实际上当单进程有多个node实例时,都依赖C++ addon时第二次加载动态链接库时,不会设定modpending,但是现在node并没有解决这个问题,这个变量应该是准备用来辅助解决这个问题的。

模块在被实际使用时(也就是require时),才会被初始化(执行nm_**_register_func)好,初始化完当然大家都知道会缓存起来。大多数内建模块并不会一开始就被初始化,所以node启动时的开销相当小。内建模块都会被包装一下,这些包装模块会去调用process.binding获取到原生模块,而启动node时对包装模块的引用在lib/internal/bootstrap_node.js中可以找到(主要是fs等)。

模块加载的细节到这里基本上就差不多, 因为我们更可能接触扩展模块的编写,所以详细说说扩展模块。

C++ addon的加载

我们知道,引用一个原生扩展的方式是require('./xxx/xxx.node'),而Node.js的require支持所谓的“扩展”,也就是针对不同的后缀可以实现不同的加载方式(这就是所谓的loader,babel-register就是利用了这货),具体代码是:

// 位置: lib/module.js
//Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  return process.dlopen(module, path._makeLong(filename));
};

这货就是仅仅调用了process.dlopen嘛,而既然是要跟C++模块通信,那么肯定process.dlopen也是C++的比较合适咯,的确,这个函数就是用C++写的~,这个函数有点长,主要的逻辑如下:

......
  uv_lib_t lib;
  CHECK_EQ(modpending, nullptr);
......
  const bool is_dlopen_error = uv_dlopen(*filename, &lib);

  node_module* const mp = modpending;
  modpending = nullptr;
......
  mp->nm_dso_handle = lib.handle;
  mp->nm_link = modlist_addon;
  modlist_addon = mp;
......
if (mp->nm_context_register_func != nullptr) {
    mp->nm_context_register_func(exports, module, env->context(), mp->nm_priv);
  } else if (mp->nm_register_func != nullptr) {
    mp->nm_register_func(exports, module, mp->nm_priv);
  } else {
    uv_dlclose(&lib);
    env->ThrowError("Module has no declared entry point.");
    return;
  }
......

上述代码中mp->nm_priv可以直接忽略,以为都被设置成了NULL

主要逻辑是:

  1. 确定modpending为空,非空直接crash
  2. 使用uv_dlopen加载动态链接库(也就是编译好的扩展),这个函数执行过程是会运行node_module_register
  3. 通过modpending获取到当前模块(很久以前使用uv_dlsym
  4. 置空modpending, 将handler存储起来,在多实例环境中可能是有用的,可以帮助实例的销毁
  5. 真正初始化module,然后返回给调用方

node.gyp工具

小前端看Haskell的一些感想

  • Haskell实现算法都是递归,一切都可递归
  • 一切都是函数,就连list也只是对某种类型的构造函数
  • 函数都只能接受一个参数,“多个参数”只是多出了curry的可能性

从JS中的valueOf谈开

ValueOf是JavaScript中Object原型上少数几个方法之一,应该不能算是很偏门的函数,但是只有《JavaScript权威指南》上面有只言片语的描述,而对ES2015中相应的Symbol.toPrimitive更是鲜有提及。

首先,给出一个结论,valueOftoString几乎都是在出现操作符(+-*/==><)时被调用, 并且valueOf几乎没有什么用。

那么,valueOf的作用是什么呢?按照语言标准上的说法就是,用于toPrimitive需要Number时,而toPrimitive出现的时机,说得简单一点就是,当需要一个Number或者String,但是被传入了一个对象时,就会执行这个操作。有一个常见的用法实际上是使用了valueOf:

将Date对象转换为时间戳时,会很自然的用+ new Date(), 实际上是悄悄地调用了new Date().valueOf()

详细地说,有如下场景,会出现偏好结果是Number的toPrimitive,也就是说valueOf可能被调用(如果没有valueOf的话,可以被toString等替代):

  • ToNumber

具体而言,当obj前后操作符是加法,以及减法、乘法、除法,以及调用Number(obj)以及new Number(obj)时。值得注意的是,parseInt, parseFloat等方法实际上不会调用ToNumber, 他们调用的是偏好String的toPrimitive。举个代码的例子:

class Test {
  valueOf() {
    return 1;
  }
  toString() {
    return '2';
  }
}

const a = new Test();

console.log(parseInt(a, 10)); //  打印出2,也就是toString被调用了
console.log(Number(a));//  打印出1,也就是valueOf被调用了

const obj = {};
obj[a] = 1;
console.log(obj); // 打印出 {‘2’: 1},也就是toString被调用了

const b = 0;
const c = '0';
console.log(a + b); // 打印出 1, 也就是valueOf被调用了
console.log(a + c);// 打印出 10, 也就是还是ValueOf被调用
  • 比较大小

直接上代码:

class Test {
  constructor(val) {
    this.__val = val;
  }
  valueOf() {
    return this.__val;
  }
}

const a = new Test('12');
const b = new Test(2);
console.log(a < b); // 打印出false,说明valueOf被调用,且两者不都是string时,转换为Number
                                // 且如果不能转为Number,则值为NaN

嗯,我们可以得出如下结论,在应该使用“值”,也就是数值的地方,如果出现了对象,就会调用valueOf。

再给出一个例子,证明valueOf不存在时,可以用toString作为替代品

class Test {
  toString() {
    return '2';
  }
}

const a = new Test();

console.log(parseInt(a, 10)); //  打印出2,也就是toString被调用了
console.log(Number(a));//  打印出2,也就是toString被调用了

const obj = {};
obj[a] = 1;
console.log(obj); // 打印出 {‘2’: 1},也就是toString被调用了

const b = 0;
const c = '0';
console.log(a + b); // 打印出 1, 也就是toString被调用了
console.log(a + c);// 打印出 10, 也就是还是toString被调用

但是反过来是不成立的,有的地方必须使用toString, 比如说把对象作为对象的key使用时,以及向parseInt中传入对象时。

到这里,可以得出一个结论,一个对象想要在希望转化为数字的地方,通过给出特殊的valueOf来给出不同于期望转化为字符串的地方的值,最好的例子就是Date对象。

另外,我们频繁提到的toPrimitive这个操作,在ES2015标准中,已经真的添加了这个方法,并且这个方法会比toStringvalueOf的优先级都高,并且嘛,几乎都可以替代这俩货了, 给个例子:

class Test {
  valueOf() {
    return 1;
  }
  toString() {
    return '2';
  }
  [Symbol.toPrimitive](hint) {
    console.log(hint);
    return 3;
  }
}

let a = new Test();

const obj = {};
obj[a] = 1;
console.log(obj); // => string  { '3': 1 }

const b = 0;
const c= '0';
console.log(a + b); // => default 3, default相当于number
console.log(a + c); // => default 30

console.log(parseInt(a, 10)); // => string 3
console.log(Number(a)); // => number 3

上述代码中Symbol.toPrimitive方法可以接收参数,表示期望的类型,就像我们提到的,如果是string,就是期望获得字符串(也就是之前说的调用toString),如果是default或者number则希望获取一个数字(也就是之前说的优先调用valueOf,否则调用toString)。

可以看出,Symbol.toPrimitive是完完全全可以取代掉valueOf,甚至toString

另外,++运算符也可以触发偏好Number的toPrimitive,而且很有意思的是toPrimitive系是内建函数,可以返回左值,所以可以用到对象上:

class Test {
  constructor(val) {
    this.__val = val;
  }
  [Symbol.toPrimitive](hint) {
    return this.__val;
  }
}

let a = new Test(1);
console.log(++a); // => 2

let b = new Test('2');
b++;
console.log(b); // => 3

let c = '1';
console.log(typeof c) // => 'string'
console.log(++c)
console.log(typeof c) // => 'numer', ++ 确实有转型的作用

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.