NikoManiac

NikoManiac 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 www.lanyixing.com 编辑
编辑

2018,厚积才能爆发。

个人动态

NikoManiac 关注了标签 · 2018-07-08

java

Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaSE, JavaEE, JavaME)的总称。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

Java编程语言的风格十分接近 C++ 语言。继承了 C++ 语言面向对象技术的核心,Java舍弃了 C++ 语言中容易引起错误的指針,改以引用取代,同时卸载原 C++ 与原来运算符重载,也卸载多重继承特性,改用接口取代,增加垃圾回收器功能。在 Java SE 1.5 版本中引入了泛型编程、类型安全的枚举、不定长参数和自动装/拆箱特性。太阳微系统对 Java 语言的解释是:“Java编程语言是个简单、面向对象、分布式、解释性、健壮、安全与系统无关、可移植、高性能、多线程和动态的语言”。

版本历史

重要版本号版本代号发布日期
JDK 1.01996 年 1 月 23 日
JDK 1.11997 年 2 月 19 日
J2SE 1.2Playground1998 年 12 月 8 日
J2SE 1.3Kestrel2000 年 5 月 8 日
J2SE 1.4Merlin2002 年 2 月 6 日
J2SE 5.0 (1.5.0)Tiger2004 年 9 月 30 日
Java SE 6Mustang2006 年 11 月 11 日
Java SE 7Dolphin2011 年 7 月 28 日
Java SE 8JSR 3372014 年 3 月 18 日
最新发布的稳定版本:
Java Standard Edition 8 Update 11 (1.8.0_11) - (July 15, 2014)
Java Standard Edition 7 Update 65 (1.7.0_65) - (July 15, 2014)

更详细的版本更新查看 J2SE Code NamesJava version history 维基页面

新手帮助

不知道如何开始写你的第一个 Java 程序?查看 Oracle 的 Java 上手文档

在你遇到问题提问之前,可以先在站内搜索一下关键词,看是否已经存在你想提问的内容。

命名规范

Java 程序应遵循以下的 命名规则,以增加可读性,同时降低偶然误差的概率。遵循这些命名规范,可以让别人更容易理解你的代码。

  • 类型名(类,接口,枚举等)应以大写字母开始,同时大写化后续每个单词的首字母。例如:StringThreadLocaland NullPointerException。这就是著名的帕斯卡命名法。
  • 方法名 应该是驼峰式,即以小写字母开头,同时大写化后续每个单词的首字母。例如:indexOfprintStackTraceinterrupt
  • 字段名 同样是驼峰式,和方法名一样。
  • 常量表达式的名称static final 不可变对象)应该全大写,同时用下划线分隔每个单词。例如:YELLOWDO_NOTHING_ON_CLOSE。这个规范也适用于一个枚举类的值。然而,static final 引用的非不可变对象应该是驼峰式。

Hello World

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

编译并调用:

javac -d . HelloWorld.java
java -cp . HelloWorld

Java 的源代码会被编译成可被 Java 命令执行的中间形式(用于 Java 虚拟机的字节代码指令)。

可用的 IDE

学习资源

常见的问题

下面是一些 SegmentFault 上在 Java 方面经常被人问到的问题:

(待补充)

关注 105554

NikoManiac 关注了标签 · 2018-07-08

react.js

React (sometimes styled React.js or ReactJS) is an open-source JavaScript library for creating user interfaces that aims to address challenges encountered in developing single-page applications. It is maintained by Facebook, Instagram and a community of individual developers and corporations.

关注 36366

NikoManiac 关注了收藏夹 · 2018-07-08

Redis设计与实现

关注 263

NikoManiac 赞了文章 · 2018-01-26

PWA之Workbox缓存策略分析

作者:陈达孚

香港中文大学研究生,《移动Web前端高效开发实战》作者之一,《前端开发者指南2017》译者之一,在中国前端开发者大会,中生代技术大会等技术会议发表过主题演讲, 专注于新技术的调研和使用.

本文为原创文章,转载请注明作者及出处

PWA之Workbox缓存策略分析

本文主要分析通过workbox(基于1.x和2.x版本,未来3.x版本会有新的结构)生成Service-Worker的缓存策略,workbox是GoogleChrome团队对原来sw-precache和sw-toolbox的封装,并且提供了Webpack和Gulp插件方便开发者快速生成sw.js文件。

precache(预缓存)

首先看一下 workbox 提供的 Webpack 插件 workboxPlugin 的三个最主要参数:

  • globDirectory
  • staticFileGlobs
  • swDest

其中 globDirectorystaticFileGlobs 会决定需要缓存的静态文件,这两个参数也存在默认值,插件会从compilation参数中获取开发者在 Webpack 配置的 output.path 作为 globDirectory 的默认值,staticFileGlobs 的默认配置是 html,js,css 文件,如果需要缓存一些界面必须的图片,这个地方需要自己配置。

之后 Webpack 插件会将配置作为参数传递给 workbox-build 模块,workbox-build 模块中会根据 globDirectory 和 staticFileGlobs 读取文件生成一份配置信息,交给 precache 处理。需要注意的是,precache里不要存太多的文件,workbox-build 对文件会有一个过滤, 该模块会读取利用 node 的 fs 模块读取文件,如果文件大于2M则不会加入配置中(可以通过配置 maximumFileSize 修改),同时会根据文件的 buffer 生成一个 hash 值,也就是说就算开发者不改变文件名,只要文件内容修改了,也会生成一个新的配置内容,让浏览器更新缓存。

那么说了那么多,precache 到底干了什么,看一下生成的sw文件:

const fileManifest = [
  {
    'url': 'main.js',
    'revision': '0e438282dc400829497725a6931f66e3'
  },
  {
    'url': 'main.css',
    'revision': '02ba19bb320adb687e08dded3e71408d'
  }
];

const workboxSW = new self.WorkboxSW();
workboxSW.precache(fileManifest);

那还是需要看一下 precache 的代码:

precache(revisionedFiles) {
  this._revisionedCacheManager.addToCacheList({
    revisionedFiles,
  })
}

是的,workbox会提供一个对象 revisionedCacheManager 来管理所有的缓存,先不管里面具体怎么处理的,往下看有个 registerInstallActivateEvents

_registerInstallActivateEvents(skipWating, clientsClaim) {
  self.addEventListener('install', (event) => {
    const cachedUrls = this._revisionedCacheManager.getCachedUrls();
    event.waitUntil(
      this._revisionedCacheManager.install().then(() => {
        if (skipWaiting) {
          return self.skipWaiting();
        }
      })
    )
}

这里可以看出,所有的 precache 都会在 service worker 的 install 事件中完成。event.waitUntil 会根据内部promise的结果来确定安装是否完成。如果安装失败,则会舍弃这个ServiceWorker。

现在看一下 _revisionedCacheManager.install 里干了什么,首先 revisionedFiles 会被放在一个 Map 中,当然这个 revisionedFiles 是已经被处理过了, 在经过 addToCacheList -> _addEntries -> _parseEntry 的过程后,会返回:

{
  entryID,
  revision,
  request: new Request(url),
  cacheBust
}

entryID 不主动传入可以视为用户传入的url,将用来作为IndexDB中的key存储revision,而request则用来提供给之后的fetch请求,cacheBust默认为true,功能等会再分析。

Map 的set 过程在 _addEntries_addEntryToInstallList 函数中,这里只需注意因为 fileManifest 中不能存放具有相同 url (或者说entryID)的值,不然会被警告。

现在回来看install,install是一个async函数,返回一个包含一系列Promise请求的Promise.all,符合waitUntil的要求。每一个需要缓存的文件会到 cacheEntry 函数中处理:

async _cacheEntry(precacheEntry) {
  const isCached = await this._isAlreadyCached(precacheEntry);
  const precacheDetails = {
    url: precacheEntry.request.url,
    revision: precacheEntry.revision,
    wasUpdated: !isCached,
  };
  if (isCached) {
    return precacheDetails;
  }

  try {
    await this._requestWrapper.fetchAndCache({
      request: precacheEntry.getNetworkRequest(),
      waitOnCache: true,
      cacheKey: precacheEntry.request,
      cleanRedirects: true,
    });

    await this._onEntryCached(precacheEntry);
    return precacheDetails;
  } catch (err) {
    throw new WorkboxError('request-not-cached', {
      url: precacheEntry.request.url,
      error: err,
    });
  }
}

对于每一个请求会去通过 _isAlreadyCached 方法访问indexDB 得知是否被缓存过。这里可能有读者会疑惑,我们不是不能在 fileManifest 中不允许存储同样的url,为什么还要查是否缓存过,这是因为当你sw文件更新后,原来的缓存还是存在的,它们或许持有相同的url,如果它们的revision也相同,就不用获取了。

在 _cacheEntry 内部,还有两个异步操作,一个是通过包装后的 requestWrapperfetchAndCache 请求并缓存数据,一个是通过 _onEntryCached 方法更新indexDB,可以看到虽然catch了错误,但依旧会throw出来,意味着任何一个precache的文件请求失败,都会终止此次install。

这里另一个需要注意的地方是 _requestWrapper.fetchAndCache,所有请求最后都会在 requestWrapper中处理,这里调用的实例方法是 fetchAndCache ,说明这次请求会涉及到网络请求和缓存处理两部分。在发出请求后,首先会判断请求结果是否需要加入缓存中:

const effectiveCacheableResponsePlugin =
  this._userSpecifiedCachableResponsePlugin ||
  cacheResponsePlugin ||
  this.getDefaultCacheableResponsePlugin();

如果没有插件配置,会使用 getDefaultCacheableResponsePlugin() 来取得默认配置,即缓存返回状态为200的请求。

在上面的代码中可以看到在 precache 环境下,会有两个参数为 true, 一个是 waitOnCache,另一个是cleanRedirects。waitOnCache保证在需要缓存的情况下返回网络结果时必须完成缓存的处理,cleanRedirects则会重新包装一下请求重定向的结果。

最后用_onEntryCached把缓存的路径凭证信息存在indexDB中。

在activate阶段,会对precache在cache里的内容进行clean,因为前面只做了更新,如果是新的precache没有的资源地址,在这里会删除。

所以 precache 就是在 service-worker 的 install 事件下完成一次对配置资源的网络请求,并在请求结果返回时完成对结果的缓存。

runtimecache(运行时缓存)

在了解 runtimecache 前,先看下 workbox-sw 的实例化过程中比较重要的部分:

this._runtimeCacheName = getDefaultCacheName({cacheId});
this._revisionedCacheManager = new RevisionedCacheManager({
  cacheId,
  plugins,
});
this._strategies = new Strategies({
  cacheId,
});

this._router = new Router(
  this._revisionedCacheManager.getCacheName(),
  handleFetch
);
this._registerInstallActivateEvents(skipWaiting, clientsClaim);
this._registerDefaultRoutes(ignoreUrlParametersMatching, directoryIndex);

所以看出 workbox-sw 实例化的过程主要有生成缓存对应空间名,缓存空间,挂载缓存策略,挂载路由方法(用于处理对应路径的缓存策略),注册安装激活方法,注册默认路由。

precache 对应的就是 runtimecache,runtimecache 顾名思义就是处理所有运行时的缓存,runtimecache 往往应对着各种类型的资源,对于不同类型的资源往往也有不同的缓存策略,所以在 workbox 中使用 runtimecache 需要调用方法,workbox.router.registerRoute 也是说明 runtimecache 需要路由层面的细致划分。

看到最后一步的 _registerDefaultRoutes ,看一下其中的代码,可以发现 workbox 有一个最基本的cache,这个 cache 其实处理的就是前面的 precache,这个 cache 遵从着 cacheFirst 原则:

const cacheFirstHandler = this.strategies.cacheFirst({
  cacheName: this._revisionedCacheManager.getCacheName(),
  plugins,
  excludeCacheId: true,
});

const capture = ({url}) => {
  url.hash = '';

  const cachedUrls = this._revisionedCacheManager.getCachedUrls();
  if (cachedUrls.indexOf(url.href) !== -1) {
    return true;
  }

  let strippedUrl =
    this._removeIgnoreUrlParams(url.href, ignoreUrlParametersMatching);
  if (cachedUrls.indexOf(strippedUrl.href) !== -1) {
    return true;
  }

  if (directoryIndex && strippedUrl.pathname.endsWith('/')) {
    strippedUrl.pathname += directoryIndex;
    return cachedUrls.indexOf(strippedUrl.href) !== -1;
  }

    return false;
  };

  this._precacheRouter.registerRoute(capture, cacheFirstHandler);

简单的说,如果你一个路径能直接在 precache 中可以找到,或者在去除了部分查询参数后符合,或者去处部分查询参数添加后缀后符合,就会直接返回缓存,至于请求过来怎么处理的,稍后再看。

我们可以这么认为 precache 就是添加了 cache,至于真实请求时如何处理还是和 runtimecache 在一个地方处理,现在看来,在 workbox 初始化的时候就有了第一个 router.registerRoute(),之后的就需要手动注册了。

在写自己注册的策略之前,考虑下,注册了 route 后,又怎么处理呢?在实例化 Router 的时候,我们就会添加一个 self.addEventListener('fetch', (event) => {...}),除非你手动传入一个handleFetch参数为false。

在注册路由的时候,registerRoute(capture, handler, method)在类中接受一个捕获条件和一个句柄函数,这个捕获条件可以是字符串,正则表达式或者是直接的Route对象,当然最终都会变成 Route 对象(分别通过 ExpressRoute 和 RegExpRoute),Route对象包含匹配,处理方法,和方法(默认为 GET)。然后在注册时会使用一个 Map,以每个使用到的方法为 Key,值为包含所有Route对象的数组,在遍历时也只会遍历相应方法的值。所以你也可以给不同的方法定义同样的捕获路径。

这里使用了 unshift 操作,所以每个新的配置会被压入堆栈的顶部,在遍历时则会被优先遍历到。因为 workbox 实例化是在 registerRoute 之前,所以默认配置优先级最低,配置后面的注册会优先于前面的。

所以最终在页面上,你的每次请求都会被监听,到相应的请求方法数组里找有没有匹配的,如果没有匹配的话,也可以使用 setDefaultHandlersetDefaultHandler不是前面的 _registerDefaultRoutes,它需要开发者自己定义,并决定策略,如果定义了,所有没被匹配的请求就会被这个策略处理。请求还支持设置在,在请求被匹配却没有正确被方法处理情况下的错误处理,最终 event 会用处理方法(策略)处理这个请求,否则就正常请求。这些请求就是 workbox下的 runtimecache。

缓存策略

现在来看看 Workbox 提供的缓存策略,主要有这几种:cache-first,cache-only,network-first,network-only,stale-while-revalidate

在前面看到,实例化的时候会给 workbox 挂载一个 Strategies 的实例。提供上面一系列的缓存策略,但在实际调用中,使用的是 _getCachingMechanism,然后把整个策略类放到一参中,二参则提供了配置项,在每个策略类中都有 handle 方法的实现,最终也会调用 handle方法。那既然如此还搞个 _getCachingMechanism干嘛,直接返回策略类就得了,这个等下看。

先看下各个策略,这里就简单说下,可以参考离线指南,虽然会有一点不一样。

第一个 Cache-First, 它的 handle 方法:

const cachedResponse = await this.requestWrapper.match({
  request: event.request,
});

return cachedResponse || await this.requestWrapper.fetchAndCache({
  request: event.request,
  waitOnCache: this.waitOnCache,
});

Cache-First策略会在有缓存的时候返回缓存,没有缓存才会去请求并且把请求结果缓存,这也是我们对于precache的策略。

然后是 Cache-only,它只会去缓存里拿数据,没有就失败了。

network-first 是一个比较复杂的策略,它接受 networkTimeoutSeconds 参数,如果没有传这个参数,请求将会发出,成功的话就返回结果添加到缓存中,如果失败则返回立即缓存。这种网络回退到缓存的方式虽然利于那些频繁更新的资源,但是在网络情况比较差的情况(无网会直接返回缓存)下,等待会比较久,这时候 networkTimeoutSeconds 就提供了作用,如果设置了,会生成一个setTimeout后被resolve的缓存调用,再把它和请求放倒一个 Promise.race 中,那么请求超时后就会返回缓存。

network-only,也比较简单,只请求,不读写缓存。

最后提供的策略是 StaleWhileRevalidate,这种策略比较接近 cache-first,代码如下:

const fetchAndCacheResponse = this.requestWrapper.fetchAndCache({
  request: event.request,
  waitOnCache: this.waitOnCache,
  cacheResponsePlugin: this._cacheablePlugin,
}).catch(() => Response.error());

const cachedResponse = await this.requestWrapper.match({
  request: event.request,
});

return cachedResponse || await fetchAndCacheResponse;
  

他们的区别在于就算有缓存,它仍然会发出请求,请求的结果会用来更新缓存,也就是说你的下一次访问的如果时间足够请求返回的话,你就能拿到最新的数据了。

可以看到离线指南中还提供了缓存然后访问网络再更新页面的方法,但这种需要配合主进程代码的修改,WorkBox 没有提供这种模式。

自定义缓存配置

回到在缓存策略里提到的,讲讲 _getCachingMechanism和缓存策略的参数。默认支持5个参数:'cacheExpiration', 'broadcastCacheUpdate', 'cacheableResponse', 'cacheName', 'plugins',(当然你会发现还有几个参数不在这里处理,比如你可以传一个自定义的 requestWrapper, 前面提到的 waitOnCache 和 NetworkFirst 支持的 networkTimeoutSeconds),先看一个完整的示例:

const workboxSW = new WorkboxSW();
const cacheFirstStrategy = workboxSW.strategies.cacheFirst({
  cacheName: 'example-cache',
  cacheExpiration: {
    maxEntries: 10,
    maxAgeSeconds: 7 * 24 * 60 * 60
  },
  broadcastCacheUpdate: {
    channelName: 'example-channel-name'
  },
  cacheableResponse: {
    stses: [0, 200, 404],
    headers: {
      'Example-Header-1': 'Header-Value-1',
      'Example-Header-2': 'Header-Value-2'
    }
  }
  plugins: [
    // Additional Plugins
  ]
});

大致可以认定的是 cacheExpiration 会用来处理缓存失效,cacheName 决定了 cache 的索引名,cacheableResponse 则决定了什么请求返回可以被缓存。

那么插件到底是怎么被处理,现在可以看_getCachingMechanism函数了,_getCachingMechanism函数处理了什么,它其实就是把 cacheExpirationbroadcastCacheUpdate,cacheabelResponse里的参数找到对应方法,传入参数实例化,然后挂在在封装后的wrapperOptions的plugins参数里,但是只是实例化了有什么用呢?这里有关键的一步:

options.requestWrapper = new RequestWrapper(wrapperOptions);

所以最终这些插件还是会在 RequestWrapper 里处理,这里的一些操作是我们之前没有提到的,来看下 RequestWrapper 里怎么处理的。

看下 RequestWrapper 的构造函数,取其中涉及到 plugins 的部分:

constructor({cacheName, cacheId, plugins, fetchOptions, matchOptions} = {}) {

  this.plugins = new Map();

  if (plugins) {
    isArrayOfType({plugins}, 'object');

    plugins.forEach((plugin) => {
      for (let callbackName of pluginCallbacks) {
        if (typeof plugin[callbackName] === 'function') {
          if (!this.plugins.has(callbackName)) {
            this.plugins.set(callbackName, []);
          } else if (callbackName === 'cacheWillUpdate') {
            throw ErrorFactory.createError(
              'multiple-cache-will-update-plugins');
          } else if (callbackName === 'cachedResponseWillBeUsed') {
            throw ErrorFactory.createError(
              'multiple-cached-response-will-be-used-plugins');
          }
          this.plugins.get(callbackName).push(plugin);
          }
        }
    });
  }
}

plugins是一个Map,默认支持以下几种Key:cacheDidUpdate, cacheWillUpdate, fetchDidFail, requestWillFetch, cachedResponseWillBeUsed。可以理解为 requestWrapper 提供了一些hooks或者生命周期,而插件就是在 hook 上进行一些处理。

这里举个缓存失效的例子看看怎么处理:

首先我们需要实例化CacheExpirationPlugin,CacheExpirationPlugin没有构造函数,实例化的是CacheExpiration,然后在this上添加maxEntries,maxAgeSeconds。所有的 hook 方法实现都放在了 CacheExpirationPlugin,提供了两个 hook: cachedResponseWillBeUsed 和 cacheDidUpdate,cachedResponseWillBeUsed 会在 RequestWrapper的match中执行,cacheDidUpdate 在 fetchAndCache中 执行。

这里可以看出,每个plugin其实就是对hook或者生命周期调用的具体实现,在把response扔到cache里之后,调用了插件的cacheDidUpdate方法,看下CacheExpirationPlugin中的cacheDidUpdate:

async cacheDidUpdate({cacheName, newResponse, url, now} = {}) {
  isType({cacheName}, 'string');
  isInstance({newResponse}, Response);

  if (typeof now === 'undefined') {
    now = Date.now();
  }

  await this.updateTimestamp({cacheName, url, now});
  await this.expireEntries({cacheName, now});
}

那么关键就是更新时间戳和失效条数,如果设置了更新时间戳会怎么样呢,在请求的时候,runtimecache也会添加到IndexedDB,值存入的是一个对象,包含了一个url和时间戳。

这个时间戳怎么生效,CacheExpirationPlugin提供了另外一个方法,cachedResponseWillBeUsed:

cachedResponseWillBeUsed({cachedResponse, cachedResponse, now} = {}) {
  if (this.isResponseFresh({cachedResponse, now})) {
    return cachedResponse;
  }

  return null;
}

RequestWrapper中的match方法会默认从cache里取,取到的是当时的完整 response, 在cache的 response 里的 headers 里取到 date,然后把当时的date加上 maxAgeSecond 和 现在的时间比, 如果小于了就返回 false,那么自然会去发起请求了。

CacheableResponsePlugin用来控制 fetchAndCache 里的 cacheable,它设置了一个 cacheWillUpdate,可以设置哪些 http status 或者 headers 的 response 要缓存,做到更精细的缓存操作。

如何配置我的缓存

离线指南已经提供了一些缓存方式,在 workbox 中,可以大致认为,有一些资源会直接影响整个应用的框架能否显示的(开发应用的 JS,CSS 和部分图片)可以做 precache,这些资源一般不存在“异步”的加载,它们如果不显示整个页面无法正常加载。

那他们的更新策略也很简单,一般这些资源的更新需要发版,而在这里用更新sw文件更新。

对于大部分无状态(注意无状态)数据请求,推荐StaleWhileRevalidate方式或者缓存回退,在某些后端数据变化比较快的情况下,添加失效时间也是可以的,对于其它(业务图片)需求,cache-first比较适用。

最后需要讨论的是页面和有状态的请求,页面是一个比较复杂的情况,页面如果是纯静态的,那么可以放入precache。但要注意,如果我们的页面不是打包工具生成的,页面文件很可能不在dist目录下,那么怎么追踪变化呢,这里推荐一种方式,我们的页面往往有一个模版,和一个json串配置hash变量,那么你可以添加这种模式:

templatedUrls: {
  path: [
    '.../xxx.html',
    '.../xxx.json'
  ]
}

如果没有json,就需要关联所有可能影响生成页面的数据了,那么这些文件的变化都会改变最后生成的sw文件。

如果你在页面上有一些动态信息(比如用户信息等等),那就比较麻烦了,推荐使用 network-first 配合一个合适的失败时间,毕竟大家都不希望用户登录了另一个账号,显示的还是上一个账号,这同样适用于那些使用cookie(有状态)的请求,这些请求也推荐你添加失效策略,和失败状态。

永远记住你的目标,让用户能够更快的看到页面,但不要给用户一个错误的页面。

总结

在目前的网络环境下,service worker 的推送服务并不能得到很好的利用,所以使用 service worker 很大程度就是利用其强大的缓存能力给用户在弱网和无网环境的优化,甚至可以通过判断网络环境进行一些预下载,丰富页面的交互。但是一个错误的缓存策略可能会使用户得不到最新的内容,每一个致力于使用 service worker 或者 PWA 的开发者都需要了解其缓存的处理。Google 提供了一系列的工具能够快速生成优质的sw文件,但是配套文档过分简单和无本地化让这些配置如同一个黑盒,使开发者很难确定正确的配置方案。希望能够阅读本文,解决读者这方面的困惑。

查看原文

赞 6 收藏 14 评论 4

NikoManiac 赞了文章 · 2018-01-13

Koa-jwt 说明文档(机翻润色)

KOA-JWT (机翻润色)

node>=7.6.0

npm v3.2.2

这个模块可以让你在你的KOA应用中通过使用JSON WEB TOKEN(以下简称JWT)认证HTTP请求

这个文档做了一个很好的介绍.

  • 如果你使用KOA版本2.+,同时你有一个低于7.6版本的node. 安装koa-jwt@2

  • 主分支(Koajwt git仓库上的.译者注)上的koa-jwt版本3+使用async/await,所以必须是node 7.6以上

  • 如果你使用koa1,你需要从npm安装koa-jwt@1.这个代码在koa-v1分支上

安装

$ npm install koa-jwt

用例

JWT认证中间件的认证通过使用JWT令牌(token,译者注).如果令牌有效,ctx.state.user(默认情况下)会被设置成解码的JSON对象,以便于在稍后的中间件中进行认证或访问控制.

检索令牌

令牌通常被包产在一个名为Authorization的HTTP头中,但也能通过cookie来进行,但是也可以使用Cookie来提供令牌,只要在opts.cookie选项上设置为包含令牌的cookie的名称.也可以通过opts.getToken选项完成自定义的令牌检索.返回的函数应该匹配以下接口:

/**
 * 自定义令牌解析器
 * @this 传入中间件的CTX对象
 *
 * @param  {object} opts     中间件选项
 * @return {String|null}    返回被解析的令牌,如果没有找到则返回null
 */

令牌解析顺序如下所示.第一个被解析的非空令牌将被用于验证

  • 运行opts.getToken 函数

  • 检查cookies(如果cookies被设置了)

  • 检查承载令牌的认证头(header)

传递密钥

通常你在opts.secret中提供了一个单独的开放密钥,但是另一个替代方案是在一个更靠前的中间件中设置ctx.state.secret,通常是每个请求中.如果这个属性存在,它将用来替换opts.secret中的密钥.

检查销毁密钥

你可以提供一个异步的函数来让koa-jwt检查令牌是否被撤销.这个函数应该被设置再opts.isRevoked属性中.你提供的函数应该匹配以下接口:

/**
 * 你自定义的检索撤销解析器
 *
 * @param  {object}      ctx     传递给中间件的CTX对象
 * @param  {object}      token     令牌
 * @param  {object}      user     令牌的内容
 * @return {Promise}     如果令牌没有被销毁, Promise必须被解析为false,其他情况下(Promise解析为true或erro)令牌被销毁.
 */

例子

var Koa = require('koa');
var jwt = require('koa-jwt');

var app = new Koa();

// Custom 401 handling if you don't want to expose koa-jwt errors to users
app.use(function(ctx, next){
  return next().catch((err) => {
    if (401 == err.status) {
      ctx.status = 401;
      ctx.body = 'Protected resource, use Authorization header to get access\n';
    } else {
      throw err;
    }
  });
});

// Unprotected middleware
app.use(function(ctx, next){
  if (ctx.url.match(/^\/public/)) {
    ctx.body = 'unprotected\n';
  } else {
    return next();
  }
});

// Middleware below this line is only reached if JWT token is valid
app.use(jwt({ secret: 'shared-secret' }));

// Protected middleware
app.use(function(ctx){
  if (ctx.url.match(/^\/api/)) {
    ctx.body = 'protected\n';
  }
});

app.listen(3000);

你也可以在某些条件下有条件的运行koa-jwt中间件:

var koa = require('koa');
var jwt = require('koa-jwt');

var app = new Koa();

// Middleware below this line is only reached if JWT token is valid
// unless the URL starts with '/public'
app.use(jwt({ secret: 'shared-secret' }).unless({ path: [/^\/public/] }));

// Unprotected middleware
app.use(function(ctx, next){
  if (ctx.url.match(/^\/public/)) {
    ctx.body = 'unprotected\n';
  } else {
    return next();
  }
});

// Protected middleware
app.use(function(ctx){
  if (ctx.url.match(/^\/api/)) {
    ctx.body = 'protected\n';
  }
});

app.listen(3000);

更多关于unless的例子,请点击koa-unless

即使没有找到认证头,你也可以通过添加一个passthrough选项来保证始终传递到下一个(中间件)

app.use(jwt( { secret: 'shared-secret', passthrough:true }))

通过这个选项,使得下游中间件可以基于ctx.state.user是否设置做出决定

如果你更喜欢使用另外一个ctx key来表示解码数据,只需要传入key属性,如下:

app.use(jwt({ secret: 'shared-secret', key: 'jwtdata' }));

此时解码数据可以通过ctx.state.jwtdata得到(替换掉默认的ctx.state.user)

你同样可以指定audience和/或issuer

app.use(jwt({ secret:   'shared-secret',
              audience: 'http://myapi/protected',
              issuer:   'http://issuer' }));

如果koa-jwt有设置一个expiration(exp),它将会被检查到.

如果存在tokenkey 选项,并且找到有效的令牌,原始令牌数据可以从随后的中间件中的ctx.state[opts.tokenKey]属性中得到.

这个模块也支持令牌标记为公钥/私钥对. 作为秘钥的替代,你可以通过Buffer指定一个Public key

var publicKey = fs.readFileSync('/path/to/public.pub');
app.use(jwt({ secret: publicKey }));

sercret选项是一个函数时,这个函数将会被每个koa-jwt接受到,用于决定哪个密钥会被用于验证JWT

这个方法的签名应该是这样的: (header) => [Promise(secret)], header表示令牌头, 如果想作为支持JWKS的令牌头的实例, 应当包含algkid:算法和密钥ID字段

通过使用 node-jwks-rsa 组件,这个选项也可以用于支持JWKS(JSON Web Set),如下:

const { koaJwtSecret } = require('jwks-rsa');

app.use(jwt({ secret: koaJwtSecret({
                        jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json',
                        cache: true,
                        cacheMaxEntries: 5,
                        cacheMaxAge: ms('10h') }),
              audience: 'http://myapi/protected',
              issuer:   'http://issuer' }));

关联模块

jsonwebtoken — JSON Web Token signing and verification

注意koa-v2版本的koa-jwt不再支持从jsonwebtoken中导出sign,verify和decode函数


原文地址: https://github.com/koajs/jwt

查看原文

赞 6 收藏 10 评论 0

NikoManiac 关注了用户 · 2017-10-30

清蒸不是水煮 @evaaz

2017.02.22 - 2018.03.09
我不只是一个 SFer
从 2018.03.10 开始
我只是一个 SFer
(^o^)/
已离职

关注 817

NikoManiac 回答了问题 · 2017-10-26

关于webpack的url-loader

能把webpack的配置文件贴一下吗,根据错误提示,是没有找到file-loader模块,而不是编译错误。

关注 3 回答 2

NikoManiac 关注了用户 · 2017-10-25

有赞前端 @youzan_fe

有赞前端团队官方专栏

关注 173

NikoManiac 赞了文章 · 2017-10-23

剖析Vue原理&实现双向绑定MVVM

本文能帮你做什么?
1、了解vue的双向数据绑定原理以及核心代码模块
2、缓解好奇心的同时了解如何实现双向绑定
为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环依赖等,也难免存在一些问题,欢迎大家指正。不过这些并不会影响大家的阅读和理解,相信看完本文后对大家在阅读vue源码的时候会更有帮助<
本文所有相关代码均在github上面可找到 https://github.com/DMQ/mvvm

相信大家对mvvm双向绑定应该都不陌生了,一言不合上代码,下面先看一个本文最终实现的效果吧,和vue一样的语法,如果还不了解双向绑定,猛戳Google

<div id="mvvm-app">
    <input type="text" v-model="word">
    <p>{{word}}</p>
    <button v-on:click="sayHi">change model</button>
</div>

<script data-original="./js/observer.js"></script>
<script data-original="./js/watcher.js"></script>
<script data-original="./js/compile.js"></script>
<script data-original="./js/mvvm.js"></script>
<script>
    var vm = new MVVM({
        el: '#mvvm-app',
        data: {
            word: 'Hello World!'
        },
        methods: {
            sayHi: function() {
                this.word = 'Hi, everybody!';
            }
        }
    });
</script>

效果:
图片描述

几种实现双向绑定的做法

目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。

实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)

脏值检查(angular.js)

数据劫持(vue.js)

发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value),这里有篇文章讲的比较详细,有兴趣可点这里

这种方式现在毕竟太low了,我们更希望通过 vm.property = value 这种方式更新数据,同时自动更新视图,于是有了下面两种方式

脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:

  • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
  • XHR响应事件 ( $http )
  • 浏览器Location变更事件 ( $location )
  • Timer事件( $timeout , $interval )
  • 执行 $digest() 或 $apply()

数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

思路整理

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一,如果不熟悉defineProperty,猛戳这里
整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者

上述流程如图所示:
图片描述

1、实现Observer

ok, 思路已经整理完毕,也已经比较明确相关逻辑和模块功能了,let's do it
我们知道可以利用Obeject.defineProperty()来监听属性变动
那么将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 settergetter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。。相关代码可以是这样:

var data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // 哈哈哈,监听到值变化了 kindeng --> dmq

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // 取出所有属性遍历
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};

function defineReactive(data, key, val) {
    observe(val); // 监听子属性
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再define
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
        }
    });
}

这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法,代码改善之后是这样:

// ... 省略
function defineReactive(data, key, val) {
    var dep = new Dep();
    observe(val); // 监听子属性

    Object.defineProperty(data, key, {
        // ... 省略
        set: function(newVal) {
            if (val === newVal) return;
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
            dep.notify(); // 通知所有订阅者
        }
    });
}

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

那么问题来了,谁是订阅者?怎么往订阅器添加订阅者?
没错,上面的思路整理中我们已经明确订阅者应该是Watcher, 而且var dep = new Dep();是在 defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在 getter里面动手脚:

// Observer.js
// ...省略
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addSub(Dep.target);
        return val;
    }
    // ... 省略
});

// Watcher.js
Watcher.prototype = {
    get: function(key) {
        Dep.target = this;
        this.value = data[key];    // 这里会触发属性的getter,从而添加订阅者
        Dep.target = null;
    }
}

这里已经实现了一个Observer了,已经具备了监听数据和数据变化通知订阅者的功能,完整代码。那么接下来就是实现Compile了

2、实现Compile

compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图,如图所示:
图片描述

因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中

function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}
Compile.prototype = {
    init: function() { this.compileElement(this.$fragment); },
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(), child;
        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    }
};

compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明:

Compile.prototype = {
    // ... 省略
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;    // 表达式文本
            // 按元素节点方式编译
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍历编译子节点
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 规定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令为 v-text
            var attrName = attr.name;    // v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if (me.isEventDirective(dir)) {
                    // 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    }
};

// 指令处理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // ...省略
    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化视图
        updaterFn && updaterFn(node, vm[exp]);
        // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
        new Watcher(vm, exp, function(value, oldValue) {
            // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
};

// 更新函数
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }
    // ...省略
};

这里通过递归遍历保证了每个节点及子节点都会解析编译到,包括了{{}}表达式声明的文本节点。指令的声明规定是通过特定前缀的节点属性来标记,如<span v-text="content" other-attrv-text便是指令,而other-attr不是指令,只是普通的属性。
监听数据、绑定更新函数的处理是在compileUtil.bind()这个方法中,通过new Watcher()添加回调来接收数据变化的通知

至此,一个简单的Compile就完成了,完整代码。接下来要看看Watcher这个订阅者的具体实现了

3、实现Watcher

Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
如果有点乱,可以回顾下前面的思路整理

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run();    // 属性值变化收到通知
    },
    run: function() {
        var value = this.get(); // 取到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
        }
    },
    get: function() {
        Dep.target = this;    // 将当前订阅者指向自己
        var value = this.vm[exp];    // 触发getter,添加自己到属性订阅器中
        Dep.target = null;    // 添加完毕,重置
        return value;
    }
};
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // 调用订阅者的update方法,通知变化
        });
    }
};

实例化Watcher的时候,调用get()方法,通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。

ok, Watcher也已经实现了,完整代码
基本上vue中数据绑定相关比较核心的几个模块也是这几个,猛戳这里 , 在src 目录可找到vue源码。

最后来讲讲MVVM入口文件的相关逻辑和实现吧,相对就比较简单了~

4、实现MVVM

MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

一个简单的MVVM构造器是这样子:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data;
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

但是这里有个问题,从代码中可看出监听的数据对象是options.data,每次需要更新视图,则必须通过var vm = new MVVM({data:{name: 'kindeng'}}); vm._data.name = 'dmq'; 这样的方式来改变数据。

显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:
var vm = new MVVM({data: {name: 'kindeng'}}); vm.name = 'dmq';

所以这里需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理为访问vm._data的属性,改造后的代码如下:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data, me = this;
    // 属性代理,实现 vm.xxx -> vm._data.xxx
    Object.keys(data).forEach(function(key) {
        me._proxy(key);
    });
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
    _proxy: function(key) {
        var me = this;
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];
            },
            set: function proxySetter(newVal) {
                me._data[key] = newVal;
            }
        });
    }
};

这里主要还是利用了Object.defineProperty()这个方法来劫持了vm实例对象的属性的读写权,使读写vm实例的属性转成读写了vm._data的属性值,达到鱼目混珠的效果,哈哈

至此,全部模块和功能已经完成了,如本文开头所承诺的两点。一个简单的MVVM模块已经实现,其思想和原理大部分来自经过简化改造的vue源码,猛戳这里可以看到本文的所有相关代码。
由于本文内容偏实践,所以代码量较多,且不宜列出大篇幅代码,所以建议想深入了解的童鞋可以再次结合本文源代码来进行阅读,这样会更加容易理解和掌握。

总结

本文主要围绕“几种实现双向绑定的做法”、“实现Observer”、“实现Compile”、“实现Watcher”、“实现MVVM”这几个模块来阐述了双向绑定的原理和实现。并根据思路流程渐进梳理讲解了一些细节思路和比较关键的内容点,以及通过展示部分关键代码讲述了怎样一步步实现一个双向绑定MVVM。文中肯定会有一些不够严谨的思考和错误,欢迎大家指正,有兴趣欢迎一起探讨和改进~

最后,感谢您的阅读!

查看原文

赞 1286 收藏 1758 评论 152

认证与成就

  • 获得 37 次点赞
  • 获得 12 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 11 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-06-28
个人主页被 465 人浏览