Quantcast
Channel: CodeSky 代码之空
Viewing all 195 articles
Browse latest View live

JavaScript 纯前端实现图片的上传、下载与复制

$
0
0

这次在写一个画布应用,由于是纯前端的项目(我也希望加入后端啊可是后端在公司里申请机器要走架构评审),所以和普通的上传到服务器不太一样,下载倒是比较常见的函数,复制也是新增研究对象……当然大家懂得,博客第一段通常是用来吐槽的……所以这是一篇吐槽文。

从最简单的开始——下载

下载之所以说是最简单的,是因为它是在太常用了!创造一个 a 标签并且模拟点击,使用 canvas.toDataURL 把 URI 写入,然后在需要的位置调用这个函数就能起到下载的效果,完全没毛病(大概就是用 a 标签引用文件地址点击能下载那么简单的原理)。原理说起来挺简单的,实际上用到的比如模拟点击,平时确实不是很常用,不过还好,需求比较特殊,当个 copy-paste 工程师也没什么不好。

const downloadImage = function(canvas) {
  const image = canvas.toDataURL('image/png')
  const dLink = document.createElement('a')
  const evt = new MouseEvent('click')
  dLink.download = 'image.png'
  dLink.href = image
  dLink.dispatchEvent(evt)
}

上传图片

正常我们的上传都是对应的一个接口,然后用 form-data 的形式把内容传入,无论是普通的上传方式还是流式上传(大文件上传时比较实用,当然最好还是断点),实际上都对应了服务器端——Emmm,从来没有做过一个纯前端还有上传需求的业务呢。

不过只要 so 一下,就能发现还是可以做的。

在这里我们主要用到的是 FileReader 对象,它可以读取你的文件并且处理成数据流的形式:https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader

可以读取为 DATAURL,那么就能载入到 canvas 中了:

const uploadImage = function(upload, cb) {
  const reader = new FileReader()
  if (upload) {
    reader.readAsDataURL(upload.files[0])
  }

  reader.addEventListener('load', () => {
    const img = new Image()
    img.src = reader.result
    img.onload = () => {
      cb(reader, img)
    }
  })
}

复制图片

复制图片一开始其实我也有点怀疑,真的可以做到吗,从原理上就是把数据写入剪贴板,唯一的问题是:到底给不给你写图片的权限,之前实际上做过写文字,第一是使用开源库,第二是使用一个 execCommand 方法。

后来其实找到了这样一个方法:https://developer.mozilla.org/zh-CN/docs/Web/API/ClipboardEvent/clipboardData

const copyToClipboard = function(canvas, e) {
  const image = canvas.toDataURL('image/png')
  e.clipboardData.setData('image/png', image)
  e.preventDefault()
}

实现大概是以上,但是会发现并不能生效。

主要是因为 image/png 并不在 Chrome 的支持范围内,其实这是一个 Chrome 的 bug(或许也许也成为了 feature?)依旧是很多年都没有修:

https://bugs.chromium.org/p/chromium/issues/detail?id=150835

如果使用 Firefox 之类的浏览器,是可以这么做的。


React div 实现一个 textarea

$
0
0

看到标题,老爷们肯定不满意,切,用 div 实现一个 textarea 有啥难度,不就是 contenteditable 吗?

看到 React,又要多加一句:切,跟用什么库有什么关系。实际上在使用的时候,我还是遇到了一点微小的麻烦。

全选

在 textarea 中,全选只需要使用 element.select() 就可以,它的作用是:

The HTMLInputElement.select() method selects all the text in a

然而在 div 中,没有这样的函数,所以需要自己划定范围并且选中:

const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(this.textInput) // DOM
selection.removeAllRanges()
selection.addRange(range)

纯文字的切换

textarea 中,所有内容都会变成文本,但是在 div 中,我们很容易受到转义的影响。

尽管 user-modify: read-write-plaintext-only; 这个可以免于标签注入的烦恼,但是依旧会遇到换行的问题,换行操作在 div 中是 <br />,对于文本来说是 \n,于是,我们还需要在转换时做一遍替代:

this.initInput = input.replace(/\n/g, '<br />')

无警告渲染的方式

在 React 中,如果你用 div 取代输入,会有一个 warning 警告,虽然不影响使用,但是 warning 总是让人浑身难受,这种时候以下两个属性可以避免这个烦恼:

suppressContentEditableWarning={true}
dangerouslySetInnerHTML={{ __html: this.initInput }}

剩下的都不是什么大问题,相信大家是能搞定的!

Eggjs 从放弃到开始使用

$
0
0

咦,这篇文章标题为什么反了?

实际上这是个人走过的心路历程,最初看到 eggjs 的时候,我就觉得 Egg 很明显不符合我的审美——我选择 koa 的理由就是小巧精致,all in middleware. 而 eggjs 不是画蛇添足吗?

这次新项目用到了公司自改版 egg,不过其实也就是 egg 多封装了几个 service。

——一开始我是拒绝的。

egg 与 koa

egg 底层用了 koa,从开发体验上来说,有种求同存异的感觉,因为 koa 是自己一个个包找来的,所以我仿佛从 0 开始知道了为什么世界这么转,而封装好的世界就没有这种快感,同时,它扩展了一些概念:service / model / middleware / controller 都会进行自动注册,在 koa 的世界里,你可能需要自己写一段代码来实现自动注册。

此外,在 koa 的基础上,它免于了一些选择困难症,也就是说,只要开启它的插件,你就遵守 egg 的规定就可以了。

当然,这种时候也带来了另一种纠结,这类企业开发的框架规定了一种开发的标准语法和规范,你没法按照自己喜欢的方式来,只能遵守它的规定,没有 koa 那种爱怎么写怎么写的自由感——不过从另一个角度来看,可能是为了长期维护的可行性做出的牺牲。

配置

和我们平时用的配置库差不多,都是根据 env 区分文件名,值得一提的是,单元测试时,环境变量为变更为 unittest,所以可以定制测试环境时的配置。如果没有找到的配置会降级到 config.default.js 中取寻找。

测试

如果我这篇文章只是简单的把官方文档压缩压缩再灌输给大家,大家肯定也读的不(hen)开心。主要想说的还是 egg 给我们在测试环节带来的便利。

测试时往往我会思考以下问题:测些什么,mock 些什么,选择啥库,这三个问题往往会阻碍我行进的步伐,尤其是 mock 步骤太多的时候——SSO 要不要 mock,某些服务要不要 mock。调用了其他外部服务要不要 mock。这样一来一去可能就更不想测试了。

在 egg 中封装好的 Service 或者是 context 的属性是可以直接 mock 配置的,使得整个过程非常的流畅,流畅的另一个原因当然是不用想如同「今天吃什么」一样的问题——「今天挑什么库用」。

文档

剩下的就是 koa 和 egg 的文档了,koa 概念很少,基本是用到什么查什么就行了,而 egg 相比之下引入的新概念和内置的 API 就比较多了,按照我们的尿性,字太长不看,很有可能会错过什么,这里已经把某些我曾经错过的部分抽出来介绍了(逃)。

在 egg API 文档的阅读时,请记住,如果写着 the same as 或者 alias,请到指定位置查看接口信息;点击源代码也可能有意外之喜。

总之

如果你期待被规范,egg 还是一个值得选择的框架,于此同时,也可以定制自己的 egg 版本,封装一些常用 Service 给自己用,不过另一方面,由于封装的太齐全,我也遇到了:egg 是照着这个来的 -> 点击进入这个东西的文档 -> 这一部分请看这个文档 -> 又进入了另一个文档的深层窘境,这种深化带来的问题是,出了 Bug,如果定位到不是你——接下来甩锅给谁好呢?

总的来说,虽然有些蛋疼,还不算太惨不忍睹,某些场景下还是比较棒的。

使用 Docker 来进行测试

$
0
0

上一篇 egg 的介绍中我们说到测试的问题,测试一直是个让人很头大的问题,不过有了 Docker 以后就解决了很多问题,本文用数据库来抛砖引玉,举个🌰,更重要的是一种思路。

要进行 mock,其实本质思路是一样的,我们需要一个用完就可以卸载的数据库,以便下一次测试使用,他必须对外暴露一个端口,可以读写数据。

为了便于与未来其他服务进行链接,我们使用 docker-compose 来配置,如果还没有相关概念并且想要了解的同学建议阅读一下 docker 相关科普以及官方文档。

version: '3'
services:
  db:
    build: ./db/
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test
      MYSQL_USER: root
    ports:
      - "0.0.0.0:3306:3306"

这里设置了 docker 的用户名密码和创建的数据库,并且暴露了 3306 端口。build 的目录选择的是 db 目录下的 Dockerfile,也可以直接定义 docker 仓库中的 MySQL 数据库。

Dockerfile 中很简单的写一下,我们之所以不直接用 MySQL image 是因为需要导入数据库表初始化的 SQL:

FROM mysql:5.6

COPY db.sql /docker-entrypoint-initdb.d/

这东西大概长这样,放在 docker-entrypoint-initdb.d 文件夹中就会自动在启动好之后自动导入。

如果到这里文章就能结束,那么自然是全体欢呼,但是现实却是挺残酷的。docker-compose up -d 的结束只代表容器启动,不代表服务启动,在刚开始的阶段如果直接执行 test 非常容易失败。

没办法,叹了口气,写一个 shell 脚本……写 bash 的时候真是痛恨自己读书少,老是忘了怎么写,于是好不容易找到了能跑起来的,照抄一波:


(
  docker-compose up -d
  while ! mysqladmin --user=root --password=root --host "127.0.0.1" ping --silent &> /dev/null ; do
    sleep 2
  done
  echo "success for database connection..."
  npm run lint -- --fix
  npm run test-local
  docker-compose down
)

其实也就是写一个循环……阻塞下一个命令的执行而已……

之后 docker-compose down 会自动卸载创建的容器,整个流程结束。

参考链接:

https://docs.docker.com/compose/compose-file/

闲话 CDN

$
0
0

开头

这篇文章通过 FCC 上海线下和成都微信的分享,整理成文字稿顺便凑一下更新,考虑到吃瓜读者们不知道都了解到啥程度,以及我科普作者的身份(自己定的),我决定从入门到放弃的介绍一下,大致涉及:

  • 什么是 CDN
  • 为什么我们要用 CDN
  • 访问原理
  • 架构
  • 应用与踩坑
  • 现实世界的 CDN

由于每个地方都事无巨细讲起来非常费劲,费劲就容易跳票,而且会导致篇幅过长,所以其实都是科普向的,如果想要深入,在每个地方都会给出链接,可以进行针对性的深入阅读。

什么是 CDN

从一个简单的栗子说起:

15307128774572.jpg

「非洲农业不发达,人人都要金坷垃」——相信大家基本上都看过来自美国圣地亚哥的视频,美国人、非洲人和日本人在一起抢来抢去。如果金坷垃只在一个地方生产,那么非洲的运输成本和生产者的产能压力都很大。

那么很简单,我们在世界的每个需要金坷垃的国家都开代工厂,都生产金坷垃——我们的 CDN 就是生产金坷垃的公司,而一个个「节点」就是代工厂。

CDN,中文名叫做「内容分发网络」,它的作用是减少传播时延,找最近的节点,实际上,尽管互联网帮助我们实现了地球村,但是从中国到日本和从中国到台湾的时延仍旧是不一样的,这一点可以从 pingtraceroute 中看出。

CDN 的优点

访问加速

CDN 作为前端性能经典手段,相信大家已经无脑使用了,正如前面所说的,减少了时延,从很大程度上就能作为加速手段了。实际上,真正的 CDN 并不是前面举例的一个国家一个节点,甚至是一个运营商,一个省份乃至地区都会有节点。

15307134926298.jpg

减轻源站(服务器)负载

一个非常简单就能想明白的问题,如果 CDN 已经能帮我返回数据了,那么请求就不会到达源站,源站(服务器)的负载就减轻了。

抗住攻击

既然源站的负载被减轻了,那么在受到 DDOS 攻击的时候,也能谈笑风生。

15307135839639.jpg

当年阮老师被 DDOS 闹的满城风雨,后来阮老师就把内容开始迁移到 GayHub……

然后本来我不用更新内容,就在昨天,阮老师发布了一篇 DDOS 防御指南,然后接着被攻击,又瘫痪了,防御指南中说自己受到了 CC,然后迁移到了腾讯云,啪啪啪打了我的脸……当然,其实 CC 并没有那么难防御,但是不在分享主题内容中,感兴趣的可以之后聊……

受到阮老师启发,于是我糊了一个架构图,一个博客系统的思路图,基本上和市面上的 Jekyll 和 Hexo 一样,当时我的设想是,把评论之类的全部抽出来,这样打的时候最多打挂评论之类的,对于博文本身不会有任何影响。后来我回去查了一下,发现这不就和市面上的一样嘛——不错!

15307136375685.jpg

那么既然静态资源能上 CDN,我们的 API,我们的 MVC 页面能不能也上 CDN?答案是可以的,关键就是这个叫全站加速的东西。

啥玩样儿?其实 CDN 就是一个缓存,区别只是这个缓存是放在网络服务提供商节点的。

最简单的模型像这张图,这张图来自《大型网站技术架构 核心原理与案例分析》一书。

15307138006738.jpg

访问原理

从我们发起请求,到到达 CDN 节点,到底经过了哪些东西,CDN 是怎么加速我们的请求的呢?

15307138483713.jpg

这张图也是网上找来的。

首先我们在地址栏键入一个网址,浏览器发现本地没有关于这个网址的 DNS 缓存,所以向网站的 DNS 服务器发起请求。

网站的 DNS 服务器设置了 CNAME,指向了某个 CDN 服务器,也就是我们常见的阿里云、腾讯云、Cloudflare 之类的,去请求 CDN 中的智能 DNS 均衡负载系统。

均衡负载系统解析域名,把对用户响应最快的节点返回给用户,然后用户向该节点发出请求。

如果是第一次访问该内容,CDN 服务器会向源站请求数据并缓存,否则的话,直接在缓存节点中找到该数据,将请求结果发给用户。

对于最简单的 CDN 系统而言,只要一台 DNS 调度服务器和一个节点服务器即可,但在复杂的应用中,会存在多级缓存,多台 Cache 来协同工作。

这里我之前在博客里写过类似的内容(实际上就是摘抄的)

架设原理

如果你要架设一个 CDN,大概需要怎么做?记住我们刚才介绍的内容,重点是,CDN 仍然是一个缓存。

15307154326400.jpg

拿来阿里云 CDN 的架构图口胡一下,我也没有搭建过,如果解释的不对请大家指出。

既然是缓存,那么很明显,也就是均衡负载加上缓存调度的搭配,根据我们刚才所说的访问原理,其实主要的重点除了均衡负载与缓存外,就是一个中央的 DNS 调度器。

实际上,和计算机的多级缓存设计以及后端的多级缓存设计一样,每一层的 cache 一级比一级大,可以存储更多资源,但是响应一个比一个耗时,如果在 L1 中无法命中,那么我们就会去 L2 找,L2 无法命中才会回到源站,这样可以有效的避免回到源站过于频繁的问题。

15307155006059.jpg

接下来这张图,我就有点编不下去了,在阿里,主要使用 LVS + Tengine 做负载均衡,然后用 Swift 做 HTTP 缓存。这是他们自己说的,但是和我没什么关系,我主要讲的是左边那个花括号的一致性 Hash。

一致性 Hash 就是把对象映射在 2^32 个桶空间里,像一个闭合的圆环,当然,实际上,我们将机器也映射到这个圆环中,比如利用别名或者 IP,顺时针将对象将内容存储到离自己最近的机器中,删除和添加节点也一样,添加和删除节点之后,根据顺时针迁移,原来的对象会进行重新计算。

关于这点需要详细了解可以看这篇:https://blog.csdn.net/cywosp/article/details/23397179

15307155521962.jpg

然后说完这个关键性算法,我们就差不多消化了——隔壁阿里云 CDN 是怎么搞出来的。

当然这依旧和我们没啥关系……

应用与踩坑

最常见的应用,就是用于前端静态资源的加速,实际上,利用 CDN,我们甚至可以做出一套属于自己的 jsDelivr

不过,使用 CDN 的时候,有一些基本法需要我们了解。

缓存设置

第一,缓存的设置,max-age 我们都用过,在 Cache-Control 中经常用于缓存的控制,可是 max-age 设置的缓存会应用于一个请求经过的每一级,如果我们希望 CDN 不缓存那么久,要怎么办呢?那就是 s-maxage,它用于设置代理服务器的缓存时间,会覆盖 max-age 的设置,这样我们可以把 max-age 用于本地缓存,把 s-maxage 用于 CDN 缓存时间,避免脏数据的产生。

缓存命中率

对于一个缓存而言,还有一点很重要,就是你的缓存到底有没有用,衡量这个东西的就是缓存命中率。如果只是静态资源,在刷新缓存之后,可能会导致命中率下降,因此 CDN 的资源不适合经常刷新,换句话说,如果一个请求结果会经常进行变更,那么 CDN 基本就没什么存在的意义。

判断是否命中缓存

无论是我们自己在开发过程中,还是帮客户 debug 的过程中,我们都会考虑一件事——这个资源是否命中了CDN,是否是因为CDN导致的问题,这个时候就要秀一波操作了。

15307158048853.jpg

在各大厂商的 CDN 返回的数据中都会有一个 X-Cache,上面内容是 Hit 或者 Miss,还会加上诸如 Memory 或者 Disk 的缩写表示内存还是磁盘,如果出现 Upstream 或者 Miss 则表示没有命中。

资源预热

缓存设计中,预热是很重要的环节,在最初刚开始启动 CDN 的时候,CDN 上并没有缓存数据,此刻大量的请求全部打向源站,肯定会把源站打挂,预热就是实现缓存好热门数据,这样在业务上线时,CDN 上已经有所需的数据了。

Vary

此外很多 CDN 都不支持 Vary 头,这样 CORS 需要的 Vary: Origin 就没法保证了,遇到这种情况,比如你发现 Origin 头被缓存了,就只能把跨域头改为 * 去匹配。

Range

另外如果是很大的文件,往往是用 RANGE 头分片载入的,但如果 CDN 没有进行分片,就会重复向源站请求完整资源,CDN 就白搞了,启用 RANGE 回源,就可以减少流量的损耗,正确的设置 RANGE 回源,就能够正确的命中 CDN 缓存。

无私钥 HTTPS

另外重点说一下:这年头上 HTTPS 已经是常规基本法了,而不上 HTTPS 才是个喷点,CDN 为了避免篡改和劫持,当然也得上 HTTPS,但这样就导致我们必须要将证书和私钥传输到 CDN 的平台中去,对于安全性是一个隐患。

所以就有了无私钥解决方案,用户搭建私钥服务器,由 CDN 方去请求签名。图为阿里云的实现,只要在自己的服务器上部署 KeyServer 和配置就可以用了。

15307163956761.jpg

这里我在上海 FCC 分享的时候有同学想要具体了解一下,具体来说的话涉及到很多 HTTPS 加密的事情,暂时不做展开,可以看一下 CloudFlare 发的一篇文章,我正好找到了翻译版:https://www.zcfy.cc/article/keyless-ssl-the-nitty-gritty-technical-details-967.html

现实世界的 CDN

15307162764700.jpg
比如,节点挂了,直接导致的是用户的损失,尤其是体量大的公司依赖 CDN 进行静态资源管理的时候,发生这样的事情后果会非常严重。

其次,便宜没好货:本来在只有网宿而没有阿里云的时代,CDN 是很昂贵的,阿里腾讯在拉低 CDN 价格的同时,也拉低了 CDN 的质量,部分节点的访问质量不太高会导致有些用户访问的网络质量非常差。

然后,一个微小的科普:什么是混合 CDN——混合 CDN 这个名词看着很高端,实际上就是,我们用了多家厂商的 CDN,可能也包括自己建的,然后谁好的选谁,但是有的时候反而会造成服务不可控,进一步劣化 CDN 的质量。

总结

CDN 这个东西本质就是一个缓存,只是这个缓存离你特别特别的近,作为用户还是开发都能从中享受到一点福利,但作为一个服务于企业的开发人员,不仅要考虑 CDN 的优点,也要知道 CDN 给我们带来的坑,这样才能靠谱的作为 CDN 的使用者。

Grafana 插件开发从零到一

$
0
0
阅前高能提醒:这篇文章总的来说并不是在教你怎么开发,而是告诉你怎么去学习开发一个 Grafana 的插件,说是从零到一,MAX_VALUE 其实是 100,望珍重。

引子

前一阵子突然接到了一个新任务:开发一个 Grafana 的 Datasource——差不多是这个表情。

15311431794605.jpg
作为一个新时代的好码畜,上一次配置 Grafana 面板的时候,含着热泪抱着大佬的大腿,在几乎完全是大佬输入的情况下完成了面板——我连面板都不会配你竟然让我开发?What,我没听错吧。

在此之前,我们为服务添加监控除了已经注入到满足条件的数据库后使用已有数据源配置,还有的就是 mock 请求映射到数据源,但是这样有太多不可变操作,如果我们能自己写个 datasource 岂不是美滋滋,一把梭。

开车

当然,我相信我艰苦卓绝的学习能力,比如这篇文章屁都没讲我已经水了 300 字了。

我们先了解到了相似的产品 azure-monitor-datasource,证明了我们方向的可行性,为了明白整个 datasource 应该是怎么运作的,以及配置界面大概长什么样,于是我特地注册了一个 Azure 账号并且配置了一波,幸运的是,大家只要看我的截图就行了:

2018-07-09 at 21.51.png
15311443644720.jpg

但是相比其他 datasource 或者 plugin 的代码,Azure 的显得相当体积庞大,因此我接触了大概一周的时间之后最终放弃了从 Azure 的角度出发去修改成一个我需要的 Datasource,因为代码实在忒复杂了……而且更主要的是,我对于 Azure 的 API 并不熟悉,就更增加了阅读成本。

所以很快的,我换了一个思路,当然,在换思路前,先熟悉一下项目结构:

用 karma 进行测试,使用 TypeScript 和 Grunt,Template 语法为 AngularJS 1.x,同时需要一个 grafana-sdk-mocks。

大概是这样,除了测试的部分,之后都会做一一的说明——总之我们看了这些上个时代的道具,掐指一算,这车大概是开不起来了,只能换换单车。

此外,还有一个问题:

Grunt 能不能换成别的?TypeScript 能不能换成 JavaScript?AngularJS 能不能换成更高版本的 Angular 或者使用其他框架?

前两个的答案是可以,不过作为非配置工程师,懒得折腾这么多,照抄配置一把梭,至于 AngularJS 1.x,涉及到了整个 Grafana 的 View 层问题,在某个论坛看到关于能不能升级的提议,大致是:「你们能不能升级到 Angular 6 啊?」「我们还在升级 Angular 4 的路上」——总之,路漫漫其修远兮,一句话就是,我们现在还得用 AngularJS 来搞定 Template。

单车

截至目前,我们已经知道「我们要使用哪些技术」、「完成品大概会有哪些配置可供选择」两个问题了,下一个问题就是:到底如何开发呢?

为了方便阅读,这回换了两个简单的项目:

这两个项目唯一的区别,是它们一个使用了 TypeScript,一个使用了 JavaScript。

接下来,我们就要蹬着我们的脚踏车开始崭新的开发之旅了。

环境配置与概览

水了几百字,你才猛然发现,雾草,环境都没搭起来——现在开始我们的表演:Installing on Mac

如果是 brew 安装的,默认的配置在 /usr/local/var/lib/grafana/plugins,之后我们为了便于预览,将项目的文件夹直接创建在这里,根据规范命名为 grafana-[yourname]-datasource。

照抄 typescript-template-datasource 初始化你的项目,并且 npm run watch 尝试一下,如果能跑起来,那么证明抄的没啥问题。

最简单的 Datasource 主要有以下模块,这里引用一下掘金的文章中的一张图:

15312298736748.jpg

从中我们可以非常清楚的看出各模块的作用,方便之后的开发。

Plugin.json

mainfest.jsonpackage.jsonconfig.json 之类的配置文件作用一样,Plugin.json 用来配置一些基本属性,基本上,typescript-template-datasource 已经填完了,你需要的只是按需修改和删减。官方文档描述的非常详细。

对于 datasource 而言,在 Plugin JSON 中还多了一项:

"metrics": true,
"annotations": false

用来选定是否支持 metrics 和 annotations,其中至少有一项需要为 true

其他文件

module.ts:可以看做是一个插件的入口文件,规定了 Datasource 和 Controllers 的值。

datasource.ts:一个插件的核心,用来系统会调用 query() 方法发起请求。

datasource 需要实现以下函数,具体之后会做介绍:

query(options) //used by panels to get data
testDatasource() //used by datasource configuration page to make sure the connection is working
annotationQuery(options) // used by dashboards to get annotations
metricFindQuery(options) // used by query editor to get metric suggestions.

config_ctrl.ts:负责配置一些额外的信息,基本上不填也可以。我们第一张截图的 settings 由此开始,是 settings 中的 Controller。

query_ctrl.ts:负责规定查询内容,也就是我们第二张截图中的内容。

在 partials 目录中有对应的各个 Controller 的 View 文件。在 Controller 内部用 static templateUrl = 'partials/query.editor.html' 来指定。

扬帆起航

终于,差不多可以开始一把梭了。

之后,除了上面两个 demo,你可能还需要寻找一些别的 datasource 作为备选答案方便「抄」。

配置页面

我建议首先从最简单粗暴的 Config Controller 入手,它即使一个 DataSource 的原点,同时又比较简单。

Controller 配合 View 使用,主要负责变量的注入,这些变量日后可以在 datasource.ts 中获取到,实际上,你在每个 Controller 中注入的变量,最终都会汇聚于 datasource.ts 的类中。

尽管现在我们还是不会 AngularJS,但我们可以抄,很快就可以抄出 dropdown 和 input 框了(此处建议抄 azure-monitor-datasource)。因为他的配置页使用了丰富的控件类型:

<gf-form-dropdown model="ctrl.target.target"
  allow-custom="true"
  lookup-text="true"
  get-options="ctrl.getOptions($query)"
  on-change="ctrl.onChangeInternal()">
</gf-form-dropdown>
<input class="gf-form-input width-30" type="text" ng-model='ctrl.current.jsonData.subscriptionId' placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>

Controller 中没有什么需要做多余的处理,不过在保存时,会触发 datasource.ts 中的 testDatasource() 方法,如果你需要校验配置是否合法,那么需要对这个部分进行配置,否则返回 true 即可,比如在 simple-json-datasource 中,利用 请求来校验是否正确。

注意,testDatasource 的返回值可以是 any 或者 Promise<any>

testDatasource() {
    return this.doRequest({
      url: this.url + '/',
      method: 'GET',
    }).then(response => {
      if (response.status === 200) {
        return { status: "success", message: "Data source is working", title: "Success" };
      }
});

返回值中的 messagetitle 自然不用说,status 其实是个枚举值,除了 success 以外,还有 warningerror——别问我怎么知道的,碰巧猜出来的。

就这样,我们实际上已经完成了一个 datasource 的设置页面。

Metrics 配置信息栏

之后,轮到 Query Controller 来解决配置「我需要怎么样的图表」的问题了,这着实比配置 Datasource 要复杂了不少,不过没关系,我们抄起来,依旧是一把梭。

首先,和 Config Controller 一样,从 View 开始抄起,简单的话我们只需要一个 Dropdown 或者 input,复杂的时候我们可能需要用到 ng-if 之类的,这个就看抄法了,实在不行你也只能拿起 AngularJS 1.x 的文档啃起来——不过这实在太可怕了。

在 Controller 中基本和 Config 的一致,如果你需要在变更的同时对资源进行更新,在 Config 中可能没什么必要,但在 Panel 中就比较有用了,可以使用以下方法来更新你的 Panel:

onChangeInternal() {
  this.panelCtrl.refresh(); // Asks the panel to refresh data.
}

datasource.ts 中,每次获取数据,都会调用一次 query() 方法,options 中会有我们传入的内容,比如:

{
  "range": { "from": "2015-12-22T03:06:13.851Z", "to": "2015-12-22T06:48:24.137Z" },
  "interval": "5s",
  "targets": [
    { "refId": "B", "target": "upper_75" },
    { "refId": "A", "target": "upper_90" }
  ],
  "format": "json",
  "maxDataPoints": 2495 //decided by the panel
}

这里我们传入的时间跨度在 range 中,时间间隔为 5s,传入的参数在 targets 数组中(这里用 dropdown 选择了 upper_75,又创建了一个选择了 upper_90),注意,即使是只有一个 target,依旧会创建一个数组,所以我们要用遍历数组的形式来处理输入。

我们通常都使用 series 作为显示屏(也就是折线图),另一种是 table,目前只有 InfluxDB 支持。

对于 Time Series,我们的标准返回值是:

[
  {
    "target":"upper_75",
    "datapoints":[
      [622, 1450754160000],
      [365, 1450754220000]
    ]
  },
  {
    "target":"upper_90",
    "datapoints":[
      [861, 1450754160000],
      [767, 1450754220000]
    ]
  }
]

对于 table,我们的标准返回值是:

[
  {
    "columns": [
      {
        "text": "Time",
        "type": "time",
        "sort": true,
        "desc": true,
      },
      {
        "text": "mean",
      },
      {
        "text": "sum",
      }
    ],
    "rows": [
      [
        1457425380000,
        null,
        null
      ],
      [
        1457425370000,
        1002.76215352,
        1002.76215352
      ],
    ],
    "type": "table"
  }
]

也就是说,query() 函数必须要满足所对应的格式才可以被看做是正确的输出,图像才会出现在图表中。

对于通过不同的条件查询不同的内容并且格式化,相信大家各有各的方法,这里就不在多做介绍。

针对变量的查询

我们在很多项目中都会看到左上角有个下拉框,这是怎么做的——答案是 Config - Variables。可是要变量随我们的心意创建,也需要做一番工作,这涉及到了 datasource.ts 中的 metricFindQuery()

在变量的查询中,会执行这个函数,和 query() 一样,我们只要根据需要返回正确的结果就行了。

QA 死在沙滩上

看完上面的,大概你觉得毫无难度,全靠一个抄字——不,骚气的现在才开始。

第一,面对 npm 编程惯了,为什么一使用 npm 包就花式报错,而 lodash / moment / q / systemjs 就跑的安然无恙——因为这是人家底层内置的几个库,其他统统没有,如果要引入,简单,把 JS 文件拷过来啊。实际上阿里云 Log Datasource 就是这么做的,看了让我不禁恶从胆边生,当然,如果是开发时使用的 dev-dependenies,请放心食用。

第二,为什么明明用了 TypeScript,我想要的特性还是不支持——使用 TypeScript,请遵循基本法,grunt-typescript 支持哪个版本就用哪个版本,如果不支持,建议更换到 grunt-ts 或者自己撸配置,这样可以更快的解决你的问题。

第三,我还有一些加密数据需要处理,最简单粗暴的方法当然是把这些数据转移到后端封装好,当然在写作时我发现了一个新方法,可见文档的 Password Security 一节,如果使用 secureJsonData,你的数据将被妥善的处理,否则的话就会凉凉的被暴露于 window 的 Grafana 配置信息中。

总结

事实证明,一个好的文档非常的重要,Grafana 的开发文档实在写的太玄学了,加上前端也忒老旧了,官网的链接竟然还有 404……开发门槛完全堆砌在「学会抄」这一步上,在搜了很多资料都找不到想要的内容,排障困难的情况下,终于还是写了一篇来告诉大家怎么抄——每个开发 Grafana Plugin 的上辈子的都是折翼的天使。

抄袭一把梭的项目仓库总结

GitHub 搜索 grafana datasource,可以找到更多可以抄的仓库。

衷心感谢

以下文章可以说是指路的明灯了,具有举足轻重的地位:

JavaScript Note.我想插入一个动态的脚本

$
0
0
发现各位观众老爷们要求太高吓得我都不敢更新了……喵喵喵,我明明只是一个笔记本顺便分享一下……的说。

还是安静的当个笔记本吧……

最近两次跌到在同一个坑里,问题其实非常简单,也非常基础,就是——我需要动态加载并执行脚本。

一般来说,我喜欢用 innerHTML 这种简单明快的方式添加内容,但是却发现,innerHTML 竟然无法执行我添加的 <script> 脚本,很明显,这不符合我的预期——

先来说说解决方案吧:

简单粗暴:eval

eval 是一种非常简单粗暴的方式,但是确实会带来很明显的安全问题,「eval is evil」。

引用自 MDN:

eval() 是一个危险的函数, 他执行的代码拥有着执行者的权利。如果你用 eval() 运行的字符串代码被恶意方(不怀好意的人)操控修改,您最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码。更重要的是,第三方代码可以看到某一个 eval() 被调用时的作用域,这也有可能导致一些不同方式的攻击。相似的 Function 就不容易被攻击。
eval() 通常比替代方法慢,因为它必须调用 JS 解释器,而许多其他结构则由现代 JS 引擎进行优化。

很明显,最简单粗暴的方法也最不可取。

常见策略:appendChild

三行代码也可以插入并执行一段代码——

const s = document.createElement('script');
s.textContent = text;
document.body.appendChild(s);

这种方法肯定比前一种靠谱多了。如果你只想要运行,可以在完成添加后移除掉这个标签。

骚操作:document.write

document.write 当然是可以的,不过问题是——顺手把页面也给刷新了,一般情况下肯定是不能够的,不过这点正好被用在了需要复写的 restc-chrome-extension

https://github.com/csvwolf/restc-chrome-extension/blob/master/src/index.js#L35

为什么 innerHTML 无效

刚开始很郁闷,同样是插入标签,为什么只有 innerHTML 不行,直到我在标准中看到这样一句:

script elements get marked unexecutable and the contents of noscript get parsed as markup.

好的吧,标准就是这么定的!事实上,在不同的浏览器上可能还是会有略微的表现形式差异,但是我们仍然应该基于标准开发。

参考资料

水了一篇,美滋滋。

别闹!自签名证书!

$
0
0

程序员英语这本书虽然事实证明确实写的不怎么样,但是开头的一些内容还是值得参考的,比如其中的一道思考题:自签名证书会带来哪些危害。

既然说起这个,当然要从最简单的 HTTPS 说起。

为什么我们需要 HTTPS

理论上而言,HTTPS 即通信加密,可以预防窃听和中间人攻击,当然,对于大多数用户和网站而言,最重要的是避免了各层的劫持。

Google 有总结曰:

  • 善意的或恶意的入侵者会利用您的网站和用户之间传输的每个未受保护的资源。
  • 许多入侵者都会查看汇总的行为以识别您的用户。
  • HTTPS 不仅可阻止您的网站被滥用,也是许多先进功能不可或缺的一部分,可作为类似应用功能(如服务工作线程)的实现技术。

HTTPS 是怎么工作的

说了半天,一定还是会好奇,HTTPS 到底是怎么工作的?(如果不好奇的请直接跳至下一节 QvQ)

简单的来说 HTTPS 的 S,就是 SSL/ TLS,SSL 是 TLS 的前身,基本上我们当成同一个 (S)看就行,换言之,HTTPS = HTTP + SSL / TLS,在 TCP 层到 HTTP 层之间加了一层 SSL。

如图所示,在 HTTP 网站通讯中,由于没有一个三方认证的过程:

15336438755229.jpg

你完全无法得知,你收到的结果是否是被篡改后的结果,当然,更无法保证,即使没动过的数据,是不是会被窃听。

我们叫图中那位黑客「中间人」,也就会造成上文所说的中间人攻击。

因此后来,大家就想到了,只要手握一份密钥,是否就天下我有了?

常见的加密有两种:对称加密和非对称加密——但是我们实际上发现,无论用对称加密还是非对称加密,密钥一泄露,还是没办法(在非对称加密中,公钥本身是公开的,只要截取私钥返回,需要公钥解密的部分,照样的截获数据)。

于是乎,我们又想到了一种升级的加密方式:对称与非对称结合。

15336441891453.jpg

然后就变成了上图中的加密方式,我先用非对称加密的方式传输密钥,然后用对称加密传输数据,这样就可以避免了传输过程中对称加密密钥泄露和公钥公开导致的截获数据两个问题了——但是,怎么证明你是个好人呢?

于是,终于找到了一个可信的三方机构:证书发布机构。

15336386568738.jpg
浏览器会在发起请求时帮你检验这个网站证书中的信息是否合法,是否是由授信机构发布的,如果不合法,那么就会报错,如果合法,那么就用对应颁发者的 CA 公钥解密,对服务器证书的签名解密。

之后使用 Hash 算法计算得到的证书和服务器发来的证书对应 Hash 是否一致,如果一致,代表没有被冒充。

之后读取证书中的密钥,用于后续加密通讯。

最终,我们的整个过程都不怕窃听篡改了——不过,可能有个让人困惑的问题,我的操作系统是怎么拥有这么多 CA 公钥的。

实际上,顶级的 CA 屈指可数,那些其他签发商,其实是被间接信任的,正好在前几天,Let's Encrypt 发布了一条消息,说明他们未来会被所有主要的程序直接信任,而过去,他们一直是被间接信任的,是由于浏览器和操作系统信任 IdenTrust,而 IdenTrust 信任他们,因此他们才被信任:

Browsers and operating systems have not, by default, directly trusted Let’s Encrypt certificates, but they trust IdenTrust, and IdenTrust trusts us, so we are trusted indirectly. IdenTrust is a critical partner in our effort to secure the Web, as they have allowed us to provide widely trusted certificates from day one.

如何为你的网站升级 HTTPS

说到升级,当然要说到 Let's Encrypt,他们提供了免费可靠的 SSL 证书,同时前一阵开始提供免费的泛域名证书(意思是之前我们只提供针对 www.codesky.me 的证书,但是之后他们提供 *.codesky.me 的证书)。

实际上,使用 certbot 可以帮助你最轻松的部署 Let's Encrypt 的证书:https://certbot.eff.org/lets-encrypt/

当然,你可以自己使用自带的证书和私钥配置证书,这里只以 Nginx 为例:

server {
    listen              443 ssl;
    server_name         www.example.com;
    ssl_certificate     www.example.com.crt;
    ssl_certificate_key www.example.com.key;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ...
}

简单几步,配置完成。

我可以使用自签名证书吗?

当然,你自己可以使用 openssl 生成一个证书,但是按照我们所讲的过程,这个证书是不受信任的,所以会以下提示:

15336442309491.jpg

图中是 12306 网站对于 https 的处理,当然,他非常的有觉悟,给大家提供了根证书的下载:

15336443217053.jpg

当然,你也可以选择信任他,绕过这一限制,但是与此同时,就意味着你承担了 HTTPS 所想要规避的所有问题。如果是有软件或者应用希望你信任他们的证书,也需要权衡再三:

作为一个可信的中间人,他们依旧是一个中间人,如果这个中间人变得不再可靠,那么传输也就不再可靠了,另一方面,即使他们并不想做什么,不完善的服务器管理制度也会导致密钥泄露,让你的通讯无处遁形。

HTTPS 对现有网站会有影响吗?

HTTPS 如果对网站有影响,那么很多网站可能就会开始考虑上不上了,尽管答案很简单——利大于弊。

目前笔者所关注到的弊端无非就是:企业证书的昂贵,以及 HTTPS 需要校验证书,这多了一步 SSL,因此也就拖延了一些速度,另外 HTTP -> HTTPS 转换的时候可能也会增加耗时(直接访问 HTTPS 就不会有这个烦恼啦)。

另外,HTTPS 可能对你的 SEO 有意想不到的效果,目前 Google 和百度都有书面文字表示他们会对 HTTPS 的站点优先收录:https://webmasters.googleblog.com/2014/08/https-as-ranking-signal.html

但是比起其他问题来说,HTTPS 更给人一种可靠,靠谱的感觉,所以为什么不来一个 HTTPS 呢?

当然,万一你的用户还在使用 IE6 呢——

15336376176759.jpg

(注:IE6 并不是不支持 HTTPS,而是不支持 SNI,不支持意味着只能一个 IP 对应一个域名一个证书,很明显在现代网站构建中这是一个不合理的设计,详情可以阅读:https和SNI

参考资料


CSS 在按钮上做个涟漪效果(Ripple Animation)

$
0
0

作为一个 CSS 渣,这次在看到一个 Vue 组件库的按钮里有个点按之后的效果之后跃跃欲试。

效果大概长这样:

2018-08-26 13.20.35.gif

首先先观察了一番,大概得到以下特征:

  1. 以鼠标按下的位置为原点,以某个值为半径进行扩散
  2. 在长宽、以及透明度上,均有渐变

原本是 Vue 实现的,但剥离框架实现,就还得在 JavaScript 上哲学一番,为此,我实现了一个较为简单的效果,这篇文章基本上是逐行总结解读:

HTML 结构:

<button>
  <span class="background"></span>
  <span class="content">涟漪效果</span>
</button>

勿用说明,这个按钮是由内容和涟漪效果的背景组成的,内容在 Vue 中也就是 <slot> 进来的文本。

之后,大致写一些基本样式,和动画核心代码没有太大关系:

button {
  overflow: hidden;
  border: 1px solid #000;
  padding: 12px;
  cursor: pointer;
  position: relative;
  margin: 10px;
}

.content {
  position: relative;
  display: inline-block;
}

要实现这个效果,我们主要考虑的就是上述总结的两点,首先,如何以一个地方为中心点,向两边扩散。

为了获取鼠标位置,我们使用的是 event.offsetXevent.offsetY,但是如果光这样对 background 进行绝对定位,达到的效果是以起始点为左上角开始的动画,为了变成中心点,我们自然想到了 transform: translate(-50%, -50%),成功把左上角转成了中心点。

接下来,我们需要确定动画中圆的半径,考虑到 button 通常都是宽 > 长的,所以取宽(button.clientWidth)为半径做圆。

另外,我们要保证鼠标点击在任一按钮区域都可以填充满,所以宽度至少需要为本身的 3 倍。

最终,我们的样式是:

{
    left: event.offsetX + 'px',
    top: event.offsetY + 'px',
    width: button.clientWidth * 3 + 'px',
    height: button.clientWidth * 3 + 'px'
}

全部准备完成之后,使用 transition 对我们的动画进行补间:

transition: width 3s ease, height 3s ease, opacity 1s ease;

全部的代码和效果见下方 jsfiddle 的演示:

但是,如果我们使用 CSS Houdini,整个动画的实现将会是非常容易的:

.ripple {
  --gradient: linear-gradient(to bottom right, deeppink, orangered);
  background: var(--gradient);
}
.ripple.animating {
  background: paint(ripple), var(--gradient);
}

效果可见:https://css-houdini.rocks/ripple/

脏读、幻读与不可重复读

$
0
0

最近在读 《MySQL 技术内幕 InnoDB 存储引擎》,里面提到的各种概念都很新鲜,以前听说过脏读、幻读、不可重复读,但是对于概念不甚了解,于是查了一下,这里做个笔记。

数据库事务特征

数据库事务特征,即 ACID:

A Atomicity 原子性

事务是一个原子性质的操作单元,事务里面的对数据库的操作要么都执行,要么都不执行,

C Consistent 一致性

在事务开始之前和完成之后,数据都必须保持一致状态,必须保证数据库的完整性。也就是说,数据必须符合数据库的规则。

I Isolation 隔离性

数据库允许多个并发事务同事对数据进行操作,隔离性保证各个事务相互独立,事务处理时的中间状态对其它事务是不可见的,以此防止出现数据不一致状态。可通过事务隔离级别设置:包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)

D Durable 持久性

一个事务处理结束后,其对数据库的修改就是永久性的,即使系统故障也不会丢失。

MySQL 数据隔离级别

首先 MySQL 里有四个隔离级别:Read uncommttied(可以读取未提交数据)、Read committed(可以读取已提交数据)、Repeatable read(可重复读)、Serializable(可串行化)。

在 InnoDB 中,默认为 Repeatable 级别,InnoDB 中使用一种被称为 next-key locking 的策略来避免幻读(phantom)现象的产生。

使用 select @@tx_isolation; 可以查看 MySQL 默认的事务隔离级别。

不同的事务隔离级别会导致不同的问题:

15352624354970.jpg

脏读、幻读、不可重复读的概念

脏读

所谓脏读是指一个事务中访问到了另外一个事务未提交的数据,如下图:
15352625974682.png
如果会话 2 更新 age 为 10,但是在 commit 之前,会话 1 希望得到 age,那么会获得的值就是更新前的值。或者如果会话 2 更新了值但是执行了 rollback,而会话 1 拿到的仍是 10。这就是脏读。

幻读

一个事务读取2次,得到的记录条数不一致:
15352627898696.png
上图很明显的表示了这个情况,由于在会话 1 之间插入了一个新的值,所以得到的两次数据就不一样了。

不可重复读

一个事务读取同一条记录2次,得到的结果不一致:

15352628823137.png
由于在读取中间变更了数据,所以会话 1 事务查询期间的得到的结果就不一样了。

解决方案

解决方案也就是上文提到的四种隔离级别,他们可以最大程度避免以上三种情况的发生:

未授权读取

也称为读未提交(Read Uncommitted):允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。

授权读取

也称为读提交(Read Committed):允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。

可重复读取(Repeatable Read)

可重复读取(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻读数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。

序列化(Serializable)

序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

参考

[翻译]小心你复制的内容:使用零宽字符将用户名不可见的插入文本中

$
0
0

翻译源:Be careful what you copy: Invisibly inserting usernames into text with Zero-Width Characters

不想读?试试这个 demo

零宽字符都是不可见的「非打印」字符,大多数应用程序中都不会显示这些字符。比如,我在这句话中添加了十个零宽字符​​​​​​​​​​,你能分辨出来吗?(提示:将句子粘贴到 Diff Checker 来查看这些字符的位置。这些字符可以被用于做某些用户的「指纹」字符。)

它当然可以,只是你不知道。

为什么?

好吧,开始的原因并不是那么让人兴奋的。几年前我是一个电竞团队的成员。这个团队有一个私人留言板,用于发布重要的公告。最终这些公告会出现在网络中的其他地方,来假冒我们团队。当然更重要的是,确保留言板冗余以便共享机密信息和策略。

网络的安全性似乎非常让人紧张,因此理论上来说,任一登录用户就可以简单地拷贝和发布公告到任何地方。我创建了一个脚本来允许团队在每个公告中带有含用户名的隐形指纹。

我看到 Zach Aysan 最近的一篇文章中大家对于零宽字符很感兴趣,所以我想用一个交互式的 demo 来公开这个方法。这个代码示例已经更新为了最新的 JavaScript,但是整体的逻辑是相同的。

怎么做?

确切的步骤和逻辑详述如下,但是简单的来说:用户名会被转成二进制,二进制会被转为一系列零宽字符来表示每个二进制数。零宽字符将被插入到文本当中。如果所述文本被发布到其他地方,则可以提出宽度字符串,并在该过程中反向的找出复制他的人的用户名。

将文本带上指纹

1: 得到已登录用户的用户名并将其转换为二进制

在这里我们将用户名的每个字符转换为同等的二进制码:

const zeroPad = num => ‘00000000’.slice(String(num).length) + num;
const textToBinary = username => (
  username.split('').map(char =>
    zeroPad(char.charCodeAt(0).toString(2))).join(' ')
);

2: 得到用户名转换而来的二进制码然后转换为零宽字符

遍历二进制字符串,并将每个 1 转换成零宽字符空间,将 0 转换为零宽非连接字符。一旦我们转换了字母,我们在移动到下一个字符前插入一个零宽链接字符。

const binaryToZeroWidth = binary => (
  binary.split('').map((binaryNum) => {
    const num = parseInt(binaryNum, 10);
    if (num === 1) {
      return '​'; // zero-width space
    } else if (num === 0) {
      return '‌'; // zero-width non-joiner
    }
    return '‍'; // zero-width joiner
  }).join('') // zero-width no-break space
);

3: 在机密文本中插入零宽「用户名」

这步只是将零宽字符块插入到机密文本中。

从指纹文本中提取出用户名

将逻辑反过来。

1: 从机密文本中提取零宽「用户名」
从字符串中删除预期的机密文本,只留下零宽字符。

2: 把零宽「用户名」转回二进制

在这里我们根据之前添加的零宽不间断空格来分割字符串。这将为我们提供等效于用户名字母的二进制编码的零宽字符,我们遍历零宽字符并返回 1 或 0 以重新创建二进制字符串。如果我们没有找到相应的 0 或者 1,我们必须命中零宽连接符,从而完成了一个字符的二进制的转换。然后我们可以在字符串中附加一个空格并移动到下一个字符中。

const zeroWidthToBinary = string => (
  string.split('').map((char) => { // zero-width no-break space
    if (char === '​') { // zero-width space
      return '1';
    } else if (char === '‌') {  // zero-width non-joiner
      return '0';
    }
    return ' '; // add single space
  }).join('')
);

3: 将二进制用户名转换成文本

最后,我们解析二进制字符串并将每个 1 和 0 序列转换成相应的字符。

const binaryToText = string => (
  string.split(' ').map(num =>
    String.fromCharCode(parseInt(num, 2))).join('')
);

总结

公司会比任何时候做的都多来避免信息泄露和阻止泄密者,这个技巧只是众多可以使用的技巧之一。根据你的工作,了解复制文本的相关的风险可能至关重要。很少有应用会尝试渲染零宽字符。比如,你希望你的终端渲染他们(我的终端并没有)。

回到留言板方案,这个计划按照预期工作了,在脚本部署后不久就发布了新的声明。几个小时内,文本已经在其他地方被共享了,并附有零宽字符串。罪魁祸首的用户名并正确识别并且封号。一个成功的项目!

当然,这种方法有一些注意事项。比如,用户知道脚本,理论上他们可以插入自己的零宽字符串并陷害其他人。更好的解决方案是插入一个不公开的唯一用户 ID 而不是用户名。

要尝试一下?请查看演示或者查看源代码

我只想让一堆 Promise 跑起来:promise-foreach

$
0
0

最近在写项目的时候有个需求:我的 Promise 即使失败了也没关系——更进一步的,当且仅当失败率大于某一值,这才是一个失败的请求。

于是我查看了包括 ES6 Promise 和 Bluebird 的实现,发现都没有我想要的效果:

  1. 异步无序
  2. 每个方法使用相同处理函数
  3. 失败不影响结果

在完成项目之后把函数抽出来作为一个单独的 utils 模块发布,今天总算把 lint 和 test 补齐了,于是顺手写一篇宣传文。

他没什么大作用,函数实现还是表现形式都比较 Low:

const foreach = require('sky-promise-foreach')

foreach([...promises], (result) => {
  // success handler for each promise
}, (err) => {
  // error handler for each promise
})

在同事的建议下,之后有空可能会出一个改版,把这个库做的更通用一点。

Repo 地址:https://github.com/csvwolf/promise-foreach

NAS 入门篇

$
0
0

由于 Time Machine 先后救我于水火之中两次,但是我的路由器实在不方便做,仗着有一点微小的闲钱(2000 块……),我就开始研究 NAS 了。

货比三家

在购买之前,首先先查了一波 NAS 的户口:群晖、威联通、玩客云——玩客云首先由于功能薄弱先 Pass,与其说玩客,不如说是视频+,具体是在什么值得买看到的……Emmmm,一点都不 Server。

威联通的价格比群晖低好多,不过相对的软件生态也不太一样,刚开始苏老师跟我疯狂案例威联通,看了一下价格还算可以,同样的价格群晖的配置比威联通低一点,而且还只有两个盘位,隔壁威联通能买到四个盘位,配置可扩展性也更高一点——但是在 DIY 的黑群的对比下,完全是碾压级别的……便宜。

无偿打广告:迷你NAS迷你整机 DIY 的NAS 企业级存储文件服务器

换了一个静音电源,再加上两块希捷 4T 的硬盘,加起来大概 2800 左右,这个价格如果买群晖只能买到一半内存的二盘口 NAS。

安装群晖系统

启动盘 U 盘内置,下完安装固件和群晖助手之后,安装过程很流畅,大概也就磕磕瓜子看看进度条,大概十几二十分钟的时间,安装就成功了,唯一需要注意的,黑群不能随便升级……不过算了,如果没有什么重大安全漏洞的话应该也不会有人来打我,没有人打我的话地球还是很和平的,功能已经够用了,手贱升级把启动盘搞坏的话又要重新刷一遍启动盘……所以最好还是不要手贱更新,并且把自动更新给关了(不要问我怎么知道的!)——而且更新也更新不上,会提示启动盘空间不足。

15372625519124.jpg

常用软件

15372620874474.jpg

进去之后,把要用的都人工安装一波,依赖项会自动安装上,自带的 Docker 不够好用,没有命令行所以我不太玩得来,因此装了一个 Ubuntu 16.04 的 VM 用来管理,Docker 上跑一些不需要配置直接使用的比如 aria2 之类的服务,而 VM 上跑一些自定义的开发者服务。

群晖的几大 Station 和 Drive,Sync 都是卖点,之前嫌弃 Download Station 下载不够快,功能也没有 aria2 强大,最后发现 BT 下载大家都没有种子,同样下不动,而 Cloud Sync + 百度云简直是神器,如果不加限制完全可以满速跑,也不用一次性获取一堆 aria2 的链接,直接挪到同步对应的文件夹就会下载,而且还可以安排定时任务。

Video Station 也是另一个非常好的功能,Download Station 下载下来之后可以直接看视频,iOS 版支持外挂字幕,Android 由于调用的三方播放器,所以外挂字幕很困难,目前没什么好的解决方案——用 iOS 或者下载内置字幕的资源吧。

Photo Station 和 Drive 主要做版本同步,可以保存你的多个版本,一般当做普通的同步也没啥关系。

值得一提的是,在控制面板中可以直接打开 AFP 协议,这样就可以创建 Time Machine 的对应文件夹进行 Time Machine 同步了,备份流程比较稳定,效果也比较客观。

绑定域名

在鼓捣完内置软件之后,第一个安排的就是 Jenkins 和 Git:

15372626354790.jpg

用 Docker 轻松一把梭——并且买了一个新域名 var.moe 绑定了一下,非常稳。

不过,光记一个端口肯定会一脸绝望,更何况绑定域名的时候我们还是动态 ip,甚至还不能走路由器暴露端口——

作为一个中国电信玩家,第一步,打电话给电信,说要开桥接模式,电信服务还不错,直接预约好时间来改,改之前还提醒说 IPTV 这样不能用了,几个网口也不能用了,只能从路由器走线——这点当然没问题。

然后改为拨号上网,就能愉快的开始折腾了。

极路由直接安装端口映射,会得到外网 IP,如果外网 IP 和 WAN 口 IP 不一致,取 WAN 口 IP 或者进行 Mac 地址克隆,就会得到一个外网的 IP。

在虚拟机内做反代,把所有服务暴露到同一个端口下——当然顺手上了证书,所以是 ip:80 和 ip:443,暴露到需要的端口下。保存,这一步就搞定了。

但是目前我们还只能 A 记录下来,必须要用 DDNS 动态解析才能根据 ip 地址更改解析,不过极路由提供了一个一键 DDNS,给出的域名拿来做底层直接 CNAME 过去就行,如果不想这么操作,也可以利用 DNSPOD 之类的 API。同时,群晖也提供了 DDAS 的功能,可以直接填写用户名密码和 key 操作。不过我直接 CNAME 一把梭了。

完结

当然,NAS 的玩法不仅于此,我也只是刚刚玩了一个礼拜(就玩坏了一次)……之后有钱了再买两个盘做 RAID5……

脏读、幻读与不可重复读

$
0
0

最近在读 《MySQL 技术内幕 InnoDB 存储引擎》,里面提到的各种概念都很新鲜,以前听说过脏读、幻读、不可重复读,但是对于概念不甚了解,于是查了一下,这里做个笔记。

数据库事务特征

数据库事务特征,即 ACID:

A Atomicity 原子性

事务是一个原子性质的操作单元,事务里面的对数据库的操作要么都执行,要么都不执行,

C Consistent 一致性

在事务开始之前和完成之后,数据都必须保持一致状态,必须保证数据库的完整性。也就是说,数据必须符合数据库的规则。

I Isolation 隔离性

数据库允许多个并发事务同事对数据进行操作,隔离性保证各个事务相互独立,事务处理时的中间状态对其它事务是不可见的,以此防止出现数据不一致状态。可通过事务隔离级别设置:包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)

D Durable 持久性

一个事务处理结束后,其对数据库的修改就是永久性的,即使系统故障也不会丢失。

MySQL 数据隔离级别

首先 MySQL 里有四个隔离级别:Read uncommttied(可以读取未提交数据)、Read committed(可以读取已提交数据)、Repeatable read(可重复读)、Serializable(可串行化)。

在 InnoDB 中,默认为 Repeatable 级别,InnoDB 中使用一种被称为 next-key locking 的策略来避免幻读(phantom)现象的产生。

使用 select @@tx_isolation; 可以查看 MySQL 默认的事务隔离级别。

不同的事务隔离级别会导致不同的问题:

15352624354970.jpg

脏读、幻读、不可重复读的概念

脏读

所谓脏读是指一个事务中访问到了另外一个事务未提交的数据,如下图:
15352625974682.png
如果会话 2 更新 age 为 10,但是在 commit 之前,会话 1 希望得到 age,那么会获得的值就是更新前的值。或者如果会话 2 更新了值但是执行了 rollback,而会话 1 拿到的仍是 10。这就是脏读。

幻读

一个事务读取2次,得到的记录条数不一致:
15352627898696.png
上图很明显的表示了这个情况,由于在会话 1 之间插入了一个新的值,所以得到的两次数据就不一样了。

不可重复读

一个事务读取同一条记录2次,得到的结果不一致:

15352628823137.png
由于在读取中间变更了数据,所以会话 1 事务查询期间的得到的结果就不一样了。

解决方案

解决方案也就是上文提到的四种隔离级别,他们可以最大程度避免以上三种情况的发生:

未授权读取

也称为读未提交(Read Uncommitted):允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。

授权读取

也称为读提交(Read Committed):允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。

可重复读取(Repeatable Read)

可重复读取(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻读数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。

序列化(Serializable)

序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

参考

[翻译]小心你复制的内容:使用零宽字符将用户名不可见的插入文本中

$
0
0

翻译源:Be careful what you copy: Invisibly inserting usernames into text with Zero-Width Characters

不想读?试试这个 demo

零宽字符都是不可见的「非打印」字符,大多数应用程序中都不会显示这些字符。比如,我在这句话中添加了十个零宽字符​​​​​​​​​​,你能分辨出来吗?(提示:将句子粘贴到 Diff Checker 来查看这些字符的位置。这些字符可以被用于做某些用户的「指纹」字符。)

它当然可以,只是你不知道。

为什么?

好吧,开始的原因并不是那么让人兴奋的。几年前我是一个电竞团队的成员。这个团队有一个私人留言板,用于发布重要的公告。最终这些公告会出现在网络中的其他地方,来假冒我们团队。当然更重要的是,确保留言板冗余以便共享机密信息和策略。

网络的安全性似乎非常让人紧张,因此理论上来说,任一登录用户就可以简单地拷贝和发布公告到任何地方。我创建了一个脚本来允许团队在每个公告中带有含用户名的隐形指纹。

我看到 Zach Aysan 最近的一篇文章中大家对于零宽字符很感兴趣,所以我想用一个交互式的 demo 来公开这个方法。这个代码示例已经更新为了最新的 JavaScript,但是整体的逻辑是相同的。

怎么做?

确切的步骤和逻辑详述如下,但是简单的来说:用户名会被转成二进制,二进制会被转为一系列零宽字符来表示每个二进制数。零宽字符将被插入到文本当中。如果所述文本被发布到其他地方,则可以提出宽度字符串,并在该过程中反向的找出复制他的人的用户名。

将文本带上指纹

1: 得到已登录用户的用户名并将其转换为二进制

在这里我们将用户名的每个字符转换为同等的二进制码:

const zeroPad = num => ‘00000000’.slice(String(num).length) + num;
const textToBinary = username => (
  username.split('').map(char =>
    zeroPad(char.charCodeAt(0).toString(2))).join(' ')
);

2: 得到用户名转换而来的二进制码然后转换为零宽字符

遍历二进制字符串,并将每个 1 转换成零宽字符空间,将 0 转换为零宽非连接字符。一旦我们转换了字母,我们在移动到下一个字符前插入一个零宽链接字符。

const binaryToZeroWidth = binary => (
  binary.split('').map((binaryNum) => {
    const num = parseInt(binaryNum, 10);
    if (num === 1) {
      return '​'; // zero-width space
    } else if (num === 0) {
      return '‌'; // zero-width non-joiner
    }
    return '‍'; // zero-width joiner
  }).join('') // zero-width no-break space
);

3: 在机密文本中插入零宽「用户名」

这步只是将零宽字符块插入到机密文本中。

从指纹文本中提取出用户名

将逻辑反过来。

1: 从机密文本中提取零宽「用户名」
从字符串中删除预期的机密文本,只留下零宽字符。

2: 把零宽「用户名」转回二进制

在这里我们根据之前添加的零宽不间断空格来分割字符串。这将为我们提供等效于用户名字母的二进制编码的零宽字符,我们遍历零宽字符并返回 1 或 0 以重新创建二进制字符串。如果我们没有找到相应的 0 或者 1,我们必须命中零宽连接符,从而完成了一个字符的二进制的转换。然后我们可以在字符串中附加一个空格并移动到下一个字符中。

const zeroWidthToBinary = string => (
  string.split('').map((char) => { // zero-width no-break space
    if (char === '​') { // zero-width space
      return '1';
    } else if (char === '‌') {  // zero-width non-joiner
      return '0';
    }
    return ' '; // add single space
  }).join('')
);

3: 将二进制用户名转换成文本

最后,我们解析二进制字符串并将每个 1 和 0 序列转换成相应的字符。

const binaryToText = string => (
  string.split(' ').map(num =>
    String.fromCharCode(parseInt(num, 2))).join('')
);

总结

公司会比任何时候做的都多来避免信息泄露和阻止泄密者,这个技巧只是众多可以使用的技巧之一。根据你的工作,了解复制文本的相关的风险可能至关重要。很少有应用会尝试渲染零宽字符。比如,你希望你的终端渲染他们(我的终端并没有)。

回到留言板方案,这个计划按照预期工作了,在脚本部署后不久就发布了新的声明。几个小时内,文本已经在其他地方被共享了,并附有零宽字符串。罪魁祸首的用户名并正确识别并且封号。一个成功的项目!

当然,这种方法有一些注意事项。比如,用户知道脚本,理论上他们可以插入自己的零宽字符串并陷害其他人。更好的解决方案是插入一个不公开的唯一用户 ID 而不是用户名。

要尝试一下?请查看演示或者查看源代码


我只想让一堆 Promise 跑起来:promise-foreach

$
0
0

最近在写项目的时候有个需求:我的 Promise 即使失败了也没关系——更进一步的,当且仅当失败率大于某一值,这才是一个失败的请求。

于是我查看了包括 ES6 Promise 和 Bluebird 的实现,发现都没有我想要的效果:

  1. 异步无序
  2. 每个方法使用相同处理函数
  3. 失败不影响结果

在完成项目之后把函数抽出来作为一个单独的 utils 模块发布,今天总算把 lint 和 test 补齐了,于是顺手写一篇宣传文。

他没什么大作用,函数实现还是表现形式都比较 Low:

const foreach = require('sky-promise-foreach')

foreach([...promises], (result) => {
  // success handler for each promise
}, (err) => {
  // error handler for each promise
})

在同事的建议下,之后有空可能会出一个改版,把这个库做的更通用一点。

Repo 地址:https://github.com/csvwolf/promise-foreach

NAS 入门篇

$
0
0

由于 Time Machine 先后救我于水火之中两次,但是我的路由器实在不方便做,仗着有一点微小的闲钱(2000 块……),我就开始研究 NAS 了。

货比三家

在购买之前,首先先查了一波 NAS 的户口:群晖、威联通、玩客云——玩客云首先由于功能薄弱先 Pass,与其说玩客,不如说是视频+,具体是在什么值得买看到的……Emmmm,一点都不 Server。

威联通的价格比群晖低好多,不过相对的软件生态也不太一样,刚开始苏老师跟我疯狂案例威联通,看了一下价格还算可以,同样的价格群晖的配置比威联通低一点,而且还只有两个盘位,隔壁威联通能买到四个盘位,配置可扩展性也更高一点——但是在 DIY 的黑群的对比下,完全是碾压级别的……便宜。

无偿打广告:迷你NAS迷你整机 DIY 的NAS 企业级存储文件服务器

换了一个静音电源,再加上两块希捷 4T 的硬盘,加起来大概 2800 左右,这个价格如果买群晖只能买到一半内存的二盘口 NAS。

安装群晖系统

启动盘 U 盘内置,下完安装固件和群晖助手之后,安装过程很流畅,大概也就磕磕瓜子看看进度条,大概十几二十分钟的时间,安装就成功了,唯一需要注意的,黑群不能随便升级……不过算了,如果没有什么重大安全漏洞的话应该也不会有人来打我,没有人打我的话地球还是很和平的,功能已经够用了,手贱升级把启动盘搞坏的话又要重新刷一遍启动盘……所以最好还是不要手贱更新,并且把自动更新给关了(不要问我怎么知道的!)——而且更新也更新不上,会提示启动盘空间不足。

15372625519124.jpg

常用软件

15372620874474.jpg

进去之后,把要用的都人工安装一波,依赖项会自动安装上,自带的 Docker 不够好用,没有命令行所以我不太玩得来,因此装了一个 Ubuntu 16.04 的 VM 用来管理,Docker 上跑一些不需要配置直接使用的比如 aria2 之类的服务,而 VM 上跑一些自定义的开发者服务。

群晖的几大 Station 和 Drive,Sync 都是卖点,之前嫌弃 Download Station 下载不够快,功能也没有 aria2 强大,最后发现 BT 下载大家都没有种子,同样下不动,而 Cloud Sync + 百度云简直是神器,如果不加限制完全可以满速跑,也不用一次性获取一堆 aria2 的链接,直接挪到同步对应的文件夹就会下载,而且还可以安排定时任务。

Video Station 也是另一个非常好的功能,Download Station 下载下来之后可以直接看视频,iOS 版支持外挂字幕,Android 由于调用的三方播放器,所以外挂字幕很困难,目前没什么好的解决方案——用 iOS 或者下载内置字幕的资源吧。

Photo Station 和 Drive 主要做版本同步,可以保存你的多个版本,一般当做普通的同步也没啥关系。

值得一提的是,在控制面板中可以直接打开 AFP 协议,这样就可以创建 Time Machine 的对应文件夹进行 Time Machine 同步了,备份流程比较稳定,效果也比较客观。

绑定域名

在鼓捣完内置软件之后,第一个安排的就是 Jenkins 和 Git:

15372626354790.jpg

用 Docker 轻松一把梭——并且买了一个新域名 var.moe 绑定了一下,非常稳。

不过,光记一个端口肯定会一脸绝望,更何况绑定域名的时候我们还是动态 ip,甚至还不能走路由器暴露端口——

作为一个中国电信玩家,第一步,打电话给电信,说要开桥接模式,电信服务还不错,直接预约好时间来改,改之前还提醒说 IPTV 这样不能用了,几个网口也不能用了,只能从路由器走线——这点当然没问题。

然后改为拨号上网,就能愉快的开始折腾了。

极路由直接安装端口映射,会得到外网 IP,如果外网 IP 和 WAN 口 IP 不一致,取 WAN 口 IP 或者进行 Mac 地址克隆,就会得到一个外网的 IP。

在虚拟机内做反代,把所有服务暴露到同一个端口下——当然顺手上了证书,所以是 ip:80 和 ip:443,暴露到需要的端口下。保存,这一步就搞定了。

但是目前我们还只能 A 记录下来,必须要用 DDNS 动态解析才能根据 ip 地址更改解析,不过极路由提供了一个一键 DDNS,给出的域名拿来做底层直接 CNAME 过去就行,如果不想这么操作,也可以利用 DNSPOD 之类的 API。同时,群晖也提供了 DDAS 的功能,可以直接填写用户名密码和 key 操作。不过我直接 CNAME 一把梭了。

完结

当然,NAS 的玩法不仅于此,我也只是刚刚玩了一个礼拜(就玩坏了一次)……之后有钱了再买两个盘做 RAID5……

npm shrinkwrap 与 package-lock

$
0
0
从开搞到文章结束过了一个月……所以……看看就行

前两天在一个群里看到一个问题,有人问「yarn 能不能人工锁版本号」,刚开始我们都没有理解,觉得明明直接用 yarn 或者 yarn install 就能生成 lock 文件,为什么要人工生成。

于是被介绍了一个 npm shrinkwrap,这是在 package-lock 出现之前就有的项目依赖锁定工具,实际上效果和 package-lock 差不多,唯一的不同是:npm shrinkwrap 是手动生成的。同样的,在更新包、删除包时也需要在完成后手动使用 npm shrinkwrap

在现代的版本中,如果两者同时存在,那么会优先取 shrinkwrap:

This command installs a package, and any packages that it depends on. If the package has a package-lock or shrinkwrap file, the installation of dependencies will be driven by that, with an npm-shrinkwrap.json taking precedence if both files exist. See package-lock.json and npm-shrinkwrap.

但是,yarn 玩家就没有这么幸运了:

yarn install 会默认创建 yarn.lock,但是同时,却并没有给一个 shrinkwrap 一样的命令创建更高优先级的手动锁定。

如果需要这一效果,可能你就需要配合 npm script 自己进行维护:

yarn install --pure-lockfile # 不生成配置文件
yarn install # 生成配置文件

粗体的玄学:谈谈 b 与 strong

$
0
0

之前遇到了在一段提示中需要加粗的问题,我们都知道,加粗有几种写法:

  • font-weight
  • <b>
  • <strong>

但是,这三者到底有什么区别——

在大多数场景下,我都会选择使用 font-weight,众所周知的是,HTML 应该与语义结合,如果是一般的加粗,那么使用 font-weight 刚刚好。

那么 <b><strong> 的区别呢?

在过去 <b> 不包含语义,只代表加粗,等于 font-weight,因此刚开始,我以为这应该是一个被废弃的标签,但是看了一下 MDN,发现并没有被废弃,但是 <b> 的含义已经变化了,它表示:吸引读者的注意到该元素的内容上(如果没有另加特别强调)。

也就是说 <strong><b> 的区别主要在于:是否强调上。

说到了这里,可能要一脸绝望了,语文不好的人根本搞不清楚什么叫吸引注意力,什么叫特别强调——作为语文好的人(喂),给大家举个例子:

特别强调:

不管你怎么看,JavaScript 也好,Golang 也好,我都觉得:PHP 是世界上最好的语言

这里我并不是希望大家特别把目光聚集到到这句话的后半段,实际上,我只是想要强调一下世界上最好的语言(为此我还额外用了感叹号)。

那么如果是吸引别人的注意力呢:

请在下方输入富强民主文明和谐

在这一段中,富强民主文明和谐本身并没有什么重要性,只是为了方便引起大家关注而使用了粗体。

同样的我们可以经常在论坛注册的回答型问题中应用:

请输入:1+1 = ?

当然,并不仅仅限于加粗,斜体也一样,具体用什么,就需要大家仔细斟酌了。

聊聊阿里云 OSS 的转义设计问题

$
0
0

今天同事向我反馈,当我们的静态资源有中文名混杂 + 和空格 时,得到的链接并不能打开,返回的是 404.

举个例子,假定我们的资源为:

http://example-resource.oss-cn-shanghai.aliyuncs.com/23232323/中文 + 文件-file.ec296f20-d67b-11e8-a623-7b28809dc3c9.js

那么在浏览器中访问的文件名应该被转义为:

%E4%B8%AD%E6%96%87%20+%20%E6%96%87%E4%BB%B6-file.ec296f20-d67b-11e8-a623-7b28809dc3c9.js

注意,在这里,空格被转义为 %20,而 + 没有进行任何处理。

同时,JavaScript 中的 encodeURInew URL() 都是符合这一规则的。

但是在阿里云 OSS 中,你可以在 404 页面看到他们后端请求的文件:

15402962480160.jpg

此时的一个问题:到底是存错了还是取错了——其实在这里我们基本可以看出取的是有问题的,因为对于这个 path 的语义应该是+,却被错误的处理成了空格,不过考虑到严谨性,还是确认了一下存储的 Object,确定是取的问题。

接下来的问题是:

  1. SDK 给我们返回的阿里云链接是可以直接访问的,而我们自定义拼接的链接是不可访问的,他们做了什么特殊处理
  2. 到底是哪里出了问题

因此,不得已的只能把 Node SDK 源码看了一遍……终于发现了他们的骚操作:

15402967203618.jpg

https://github.com/ali-sdk/ali-oss/blob/master/lib/client.js#L366

在 encode 之后人工 2B,谁都比不过——

第二个问题,到底是谁的错?

于是正好上 so 搜了一下,发现了一波标准:https://stackoverflow.com/questions/2678551/when-to-encode-space-to-plus-or-20

+ means a space only in application/x-www-form-urlencoded content, such as the query part of a URL
Now, the HTML 2.0 Specification (RFC1866) explicitly said, in section 8.2.2, that the Query part of a GET request's URL string should be encoded as ·application/x-www-form-urlencoded·. This, in theory, suggests that it's legal to use a '+' in the URL in the query string (after the '?').

也就是说,只有 application/x-www-form-urlencodedquerystring 中的空格 = ++ = %2B,其他情况下 + 不应该被特殊处理,很显然阿里云 OSS 的实现是错误的。

得到问题的答案之后,基本定位到是 OSS 后端的转义问题,但是去怼 OSS 的时候,他们用「这是 Feature」、「为了复杂规则更好的兼容」、「设计就是这样」、「用户最好自己 encode」来答复,这里有涉及到内容存储的另一个结构了:

对于内容存储而言,会将路径也作为文件名的一部分,即上例中的文件名是:

23232323/中文 + 文件-file.ec296f20-d67b-11e8-a623-7b28809dc3c9.js

那么我们处理的时候,如果使用 encodeURI,则不会转义 +,如果使用 encodeURIComponent,则会把 / 一起转义,如果在保存前转义,就无法保留正确的文件名——非常矛盾,这大概也就是为什么 SDK 用了 + => 2B 的黑魔法吧。

不过我也有个大胆的猜测,因为阿里云这段代码非常年长而稳定了,而我们这样的考据用户其实并不多,凑合用黑魔法也能用,所以所谓的「Feature」,就是——改了万一挂了我可背不起锅,即使是加一个兼容 Feature,也是有可能挂的,所以最好的方法还是让用户继续用黑魔法,但是建议大家以后在做类似的实现的时候不要理所当然的以为 url 中的 + 必然等于空格,在标准中是有明确规定的。

小插曲:最开始以为是 JavaScript encode 库有问题,怎么都不能和阿里 encode 的结果一致,还骂了一声 JavaScript 辣鸡,如果是 PHP 估计就没问题(喂。定位了一天并且和阿里云撕逼之后只能说——阿里云辣鸡(喂。

Viewing all 195 articles
Browse latest View live


Latest Images