_John

_John 查看完整档案

深圳编辑深圳信息职业技术学院  |  软件技术 编辑蚂蚁集团  |  前端开发 编辑 github.com/Ricbet 编辑
编辑

这家伙很懒~啥都不愿意留下

个人动态

_John 赞了文章 · 2020-06-15

VS Code、ATOM这些开源文本编辑器的代码实现中有哪些奇技淫巧?

这篇是我在知乎的回答,原文在这里:justjavac: VS Code、ATOM这些开源文本编辑器的代码实现中有哪些奇技淫巧?

研究 V8 比较多,也关注了一下 vscode 和 atom 的性能,每次 vscode、atom 的 change log 我都会看一遍。印象最深的是 vscode 1.14 的一次更新日志,doApplyEdits Lines inserted using splice · Issue #351 · Microsoft/monaco-editor不要在循环中使用 splice

下图是我一年前跑的测试结果:Inserting an array within an array

300+倍的差距。

在之前 vscode 还有一次很大的性能提升,在版本 1.9 的时候,改进了语法高亮的算法。

语法高亮的过程通常分为 2 个阶段(tokenization 和 render):先将源码分割为 token,然后使用不同的主题对分割后的 token 进行着色。

tokenization 的过程是:从上到下逐行运行。tokenizer 在行的末尾存储一些状态,在 tokenize 下一行时会用到这些状态。这样,在用户进行编辑时仅需要重新 tokenize 行的一小部分,而不需要扫描整个文件内容。

还有一种情况是当前行的输入会影响到后面(甚至是前面)的行,这时会用到结束状态:

在 1.9 之前的版本,vscode 如何 tokenization 呢?

比如上面的代码:

在 vscode 种这样存储:

tokens = [
    { startIndex:  0, type: 'keyword.js' },
    { startIndex:  8, type: '' },
    { startIndex:  9, type: 'identifier.js' },
    { startIndex: 11, type: 'delimiter.paren.js' },
    { startIndex: 12, type: 'delimiter.paren.js' },
    { startIndex: 13, type: '' },
    { startIndex: 14, type: 'delimiter.curly.js' },
]

{ startIndex: 0, type: 'keyword.js' } 表示从 0 开始的 token 是一个 keyword。

VSCode 团队在博客种指出这在 Chrome 中占据 648 个字节,因此存储这样的对象在内存方面的代价非常高(每个对象实例必须保留指向其原型的空间,以及其属性列表等)。为 15 个字符存储 648 字节是不可接受的。

所以,vscode 使用二进制来存储token:

//     0        1               2                  3                      4
map = ['', 'keyword.js', 'identifier.js', 'delimiter.paren.js', 'delimiter.curly.js'];
tokens = [
    { startIndex:  0, type: 1 },
    { startIndex:  8, type: 0 },
    { startIndex:  9, type: 2 },
    { startIndex: 11, type: 3 },
    { startIndex: 12, type: 3 },
    { startIndex: 13, type: 0 },
    { startIndex: 14, type: 4 },
]

和上面的表示法相比,只是把 type 由字符串变成了数字,本质上并没有节约太多的内存。但是别着急,vscode 还有黑科技。

我们都知道 JavaScript 使用 IEEE-754 标准存储双精度浮点数,尾数为 53bit。能够在不丢失精度的情况下处理的最大整数为 2^53-1。因此 vscode 使用其中的 48big 进行编码:使用 32bit 来存储 startIndex,16bit 来存储type。 于是上面的对象在 vscode 种被存储为:

tokens = [
                 //       type                 startIndex
     4294967296, // 0000000000000001 00000000000000000000000000000000
              8, // 0000000000000000 00000000000000000000000000001000
     8589934601, // 0000000000000010 00000000000000000000000000001001
    12884901899, // 0000000000000011 00000000000000000000000000001011
    12884901900, // 0000000000000011 00000000000000000000000000001100
             13, // 0000000000000000 00000000000000000000000000001101
    17179869198, // 0000000000000100 00000000000000000000000000001110
]

每个数字是 64bit(8字节),一共是 7 个数字,存储这些元素一共需要 7*8 = 56 字节,再加上数组的额外开销共需要 104 个字节,只有之前的 648 字节的 1/6。

而主题的渲染则用到了 Trie 数据结构。

这个学过《数据结构》的都懂,算不上奇技淫巧,就不展开了。

这一切都是 2017 年 3 月发布的 vscode 1.9。

而今年 3 月,vscode 又重写了 Text Buffer。用户使用编辑器,大部分时间就是写新代码,改旧代码,说到底还是对 text 进行编辑。

对于高性能的文本操作,vscode 最初尝试使用 C++ 进行编写,毕竟 C++ 的性能要比 JavaScript 高出不少,但是事实却不够理想,使用 C++ 确实节约了内存,但是在使用 C++ 模块时,需要在 JavaScript 和 C++ 之间往返数次,这大大减慢了 vscode 的性能。

vscode 团队从 Vyacheslav Egorov 的一篇文章 Maybe you don't need Rust and WASM to speed up your JS 收到了启发,如何充分压榨 V8 引擎的性能。mrale.ph 的博客我几乎每篇都看,非常经典,也非常难懂 。

大多编辑器都是基于行的。程序员逐行编写代码,编译器提供基于行的反馈信息,堆栈跟踪包含行号,tokenization 引擎逐行运行…… 在 vscode 的早期版本中也是直接把每行代码作为字符串存储在数组中。

但是这种方式存在一些问题:

  • 无法打开大文件,因为把所有内容读入数组中可能导致内存不足。
  • 即使文件不大,但是行数太多也无法打开。例如,一个用户无法打开一个 35 MB 的文件。根本原因是该文件的行数太多,1370 万行。引擎将为ModelLine每行和每个对象使用大约 40-60 个字节,因此整个数组使用大约 600MB 内存来存储文档。也就是说打开这个 35M 的文件需要 600M 的内容,20 倍啊!!!
  • 另一个问题就是速度。为了构建这个数组,必须通过换行符分割内容,以便每行获得一个字符串对象。

于是 vscode 开始寻找新的数据结果,最终选择了 Piece table。不知道为什么这么晚才选择 piece table,要知道在微软的 office word 中早就已经使用了 piece table。我也是在一次 Java 读取 word 的 jar 包源码中第一次知道的 piece table 数据结构。

推荐几篇延伸阅读的文章:

目前主要的三种编辑方式有 gap buffer, rope, piece table。


最近用 Atom 少了。

上一次让我兴奋的地方是:The State of Atom's Performance。在2017年6月 Atom 使用了 piece table 数据结构,使用 C++ 重新实现了 text buffer:Atom's new concurrency-friendly buffer implementation。比 vscode 还要早半年,但是为什么还是这么慢呢???

Atom 使用 V8 的自定义快照(snapshot)提升启动性能,最终删除了影响性能的 jQuery 和自定义 element。就连 V8 的

Atom 还更新了 DOM 渲染的方式:A new approach to text rendering,而这个新算法包括一个类似 React 的 vdom,从 issue 来看这是一个大工程啊,包含了近 100 个 task

图片描述

经过一系列优化,官方说道:

we made loading Atom almost 50% faster and snapshots were a crucial tool that enabled some otherwise impossible optimizations.

我们使 Atom 快了 50%,snapshot 功不可没。(PS:我一定是使用了假的 Atom)

不过 snapshot 确实是 V8 的神器,Nodejs 也看到了 Atom 的成果,于 2017-11-16 开了 issue :speeding up Node.js startup using V8 snapshot · Issue #17058 · nodejs/node。这在我之前的专栏里面有介绍:Node.js 新计划:使用 V8 snapshot 将启动速度提升 8 倍


最近一次关注 Atom 是 atom/xray。知乎上也有相关的讨论,atom 开发的下一代编辑器(莫非已经定义 atom 为上一代编辑器了吗)。大概就是一种“大号废了,开小号重练”的感觉。

值得学习的地方是 text 处理使用 copy-on-write CRDT:

如果一直关注 Atom,对于 CRDT 应该不会陌生。Atom 的多人实时共同编辑插件 https://teletype.atom.io/ 就是使用的 CRDT。

CRDT 全称:Conflict-Free Replicated Data Types,强行翻译过来就是“无冲突可复制数据类型”。

CAP定理:在分布式系统中,最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

很多分布式系统都舍弃了C(一致性):允许可以在某些时刻不一致,转而求其次要求系统满足最终一致性。这也是目前很多 nosql 数据库追求的方式(另一种是传统的符合 ACID 特性的数据库系统,放弃了A(可用性),这种系统称为强一致性)。

而在最终一致性分布式系统中,一个最基本的问题就是,应该采用什么样的数据结构来保证最终一致性? 答案就是 CRDT。


这篇文章只是一个提纲,里面的每个知识点都可以展开了讲上三天三夜。

查看原文

赞 62 收藏 68 评论 5

_John 关注了用户 · 2020-06-01

justjavac @justjavac

会写点 js 代码

关注 14506

_John 赞了文章 · 2019-12-12

一篇文章搞定Github API 调用 (v3)

对于常用Github的用户来说,经常有一些自动化的需求。比如我的需求是定时备份Github的issues和comments到本地。以下为Github的API的使用参考。

v3版API的文档链接
v3版API的官方教程

基本访问路径 (Root Endpoints)

一开始读文档的时候,照着它的事例直接在命令行里curl,或者在InSomnia或Postman软件里访问,都完美显示200状态。可是一旦把链接里改写成自己的用户名就各种显示404无页面。还以为是授权问题,然后在页头HEADER中按照各种方式试了username和token密钥,都没用还是404。结果发现,原来不是方法的问题,纯粹是链接地址没写对!
实际上只是读取的话,完全不用任何授权,可以在命令行、Insomnia、网页等各种情况下直接输入链接访问任何人的所有公开信息。
然后对照官方路径列表Root Endpoints得到的链接,好像怎么访问都不对。反而在Stackoverflow中看到的一个链接,顺藤摸瓜自己发现了各种正确的访问路径,总结如下:

  • 首先!访问的链接最后不能有/。如https://api.github.com/users/solomonxie是可以访问到我个人信息的,但是https://api.github.com/users/solomonxie/就不行了,唯一不同是多了一个/.
  • 其次!不同于一般URL访问,GIthub的API访问链接是区分大小写的!
  • 个人主要信息。 https://api.github.com/users/用户名,得到数据如下图:

image

  • 个人所有repo。https://api.github.com/users/用户名/repos。会得到一个repo的JSON格式列表。
  • repo详细信息。https://api.github.com/repos/用户名/仓库名。repo的路径就开始和个人信息不同了。
  • 获取某个repo的内容列表。https://api.github.com/repos/solomonxie/gists/contents,注意这只会返回根目录的内容。
  • 获取repo中子目录的内容列表。https://api.github.com/repos/solomonxie/gists/contents/目录名。一定要注意这里一定要完全遵循原文件名的大小写,否则无法获得信息。如果是更深层的内容,则在链接列按照顺序逐级写上目录名称。
  • 获取repo中某文件信息(不包括内容)。https://api.github.com/repos/solomonxie/gists/contents/文件路径。文件路径是文件的完整路径,区分大小写。只会返回文件基本信息。
  • 获取某文件的原始内容(Raw)。1. 通过上面的文件信息中提取download_url这条链接,就能获取它的原始内容了。2. 或者直接访问:https://raw.githubusercontent.com/用户名/仓库名/分支名/文件路径
  • repo中所有的commits列表。https://api.github.com/repos/用户名/仓库名/commits
  • 某一条commit详情。https://api.github.com/repos/用户名/仓库名/commits/某一条commit的SHA
  • issues列表。https://api.github.com/repos/用户名/仓库名/issues
  • 某条issue详情。https://api.github.com/repos/用户名/仓库名/issues/序号。issues都是以1,2,3这样的序列排号的。
  • 某issue中的comments列表。https://api.github.com/repos/用户名/仓库名/issues/序号/comments
  • 某comment详情。https://api.github.com/repos/用户名/仓库名/issues/comments/评论详情的ID。其中评论ID是从issues列表中获得的。

查询参数 (Parameters)

如果在上面基本链接中加入查询条件,那么返回的数据就是filtered,过滤了的。比如要求只返回正在开放的issues,或者让列表数据分页显示。常用如下:

  • 分页功能。格式是?page=页数&per_page=每页包含数量

https://api.github.com/users/solomonxie/repos?page=2&per_page=3

  • issues状态。格式是?state=状态

https://api.github.com/repos/solomonxie/solomonxie.github.io/issues?state=closed

权限认证 Authentication

首先需要知道都是,到此为止之前所有都查询都是不需要任何权限的,给个地址就返回数据,全公开。
但是创建文件、更新、删除等就是必须用自己的账号"登录"才能实现的。所以为了下面的增删改做准备,需要先看一下权限问题。
官网虽然写的很简答,不过如果不熟悉API的话还是不能马上就理解。

常用的认证方法有三种,Basic authentication, OAuth2 token, OAuth2 key/secret
三种方法效果一样,但是各有其特点和方便之处。选哪种就要看自己哪种方便了。

认证方法一:Basic authentication

这种最简单,如果是用curl的话,就:

curl -u "用户名:密码" https://api.github.com

如果是用Insomnia等api调试工具的话,直接在Auth选项栏里选Basic Auth,然后填上用户名密码即可。

认证方法二:OAuth2 token

关于token

这种token方式,说实话如果不是操作过API或深度了解REST的话,是很难理解的东西。
说白了就是第二个密码,你既不用到处泄露自己的用户名密码,又可以专门给这个"第二密码"设置不同需要的权限,如有的只可读有的还可以写等。而且这个“第二密码”是既包括用户名又包括密码功能的,全站只此一个绝对不会和别人重复。初次之外,你还可以设置很多个token,也就是第三、第四、第五...密码。很方便。

设置token方法

就位于github个人账号设置->开发者设置->个人token里。创建一个新token时,可以选择具体的权限,创建成功时一定要复制到本地哪里保存,只会让你看见一次,如果忘记的话就需要重新生成(其实丢了也不算麻烦)。
image

另外!注意:

token字符串不能存储在github的repo中,经过测试,一旦提交的文件中包含这个token字符串,那么github就会自动删除这个token -_-! 我用了很久才明白过来,创建的Personal Access Token总是自动消失,还以为是有时限的。

用token通过权限认证

有两种传送方法,哪种都可以:

  1. 作为url中的参数明文传输:
curl https://api.github.com/?access_token=OAUTH-TOKEN
  1. 作为header中的参数传输:
curl -H "Authorization: token OAUTH-TOKEN" https://api.github.com

如果不是用curl而是Insomnia测试的话,和上面basic auth是大同小异的,很容易操作就不复述了。
到此为止,权限认证就算搞清了,而且也实际验证过有效了。强烈建议用insomnia工具操作,有GUI界面方便理解,成功后再转为curl或python等程序语言。

认证方法三:OAuth2 key/secret

这个是除了Personal Access Token之外的另一种好用的方法,即创建自己的OAuth app,然后得到一对client_idclient_secret。如下:
image
image
得到这两个值之后,直接在访问任何api的url连接后面显性加上这两个参数即可完成认证,如:
https://api.github.com/users/yourusername?client_id=YOUR-CLIENT-ID&client_secret=YOUR-CLIENT-SECRET
但是:

目前这种认证方式不支持查询以外的操作,也就是只能GET获取某些api信息,不能执行request里的任何PUT/PATCH/DELETE操作。

创建新文件 Create content

Contents操作 官方文档
  • 传输方法:PUT
  • 访问路径:https://api.github.com/repos/用户名/仓库名/contents/文件路径
  • JSON格式:
{
  "message": "commit from INSOMNIA",
  "content": "bXkgbmV3IGZpbGUgY29udGVudHM="
}

JSON填写如下图:
image

  • 注意:1.必须添加权限验证(上面有写) 2. 数据传送格式选择JSON 3. 文件内容必须是把文件整体转为Base64字符串再存到JSON变量中 4. 文件路径中如果有不存在的文件夹,则会自动创建

起初不管怎么尝试都一直报同样都错误,400 Invalid JSON,如下图:
[图片上传失败...(image-884e71-1527903120996)]

最后发现原来是犯了很小很小都错误才导致如此:
image
原来,我的token看似是正常的,唯独错误的是,多了一个空行!也就是说,标明都是invalid JSON,结果没注意竟然是invalid Token!

增加文件成功后返回的消息:
image

更新文件 Update content

主要这几点: 1. 传送方式用PUT 和创建文件一样 2. 需要权限验证,3. 传输内容数据用JSON 4. 需要指定该文件的SHA码 4. 路径和访问content时一样 5. 文件内容必须是把文件整体转为Base64字符串再存到JSON变量中
  • 传输方法:PUT
  • 访问路径:https://api.github.com/repos/用户名/仓库名/contents/文件路径
  • JSON格式:
{
  "message": "update from INSOMNIA",
  "content": "Y3JlYXRlIGZpbGUgZnJvbSBJTlNPTU5JQQoKSXQncyB1cGRhdGVkISEhCgpJdCdzIHVwZGF0ZWQgYWdhaW4hIQ==",
  "sha": "57642f5283c98f6ffa75d65e2bf49d05042b4a6d"
}
  • 注意:必须指定该文件的SHA码,相当于文件的ID。

SHA虽然是对文件的唯一识别码,相当于ID,但是它是会随着文件内容变化而变化的!所以必须每次都重新获取才行。

至于获取方式,验证后发现,目前最靠谱的是用前面的get content获取到该文件的信息,然后里面找到sha

对上传时的JSON格式另有要求,如果没有按照要求把必填项输入,则会出现422错误信息:
image

或者如果用错了SHA,会出现409错误消息:
image

如果正确传送,就会显示200完成更新:
image

删除文件 Delete content

  • 传输方法:DELETE
  • 访问路径:https://api.github.com/repos/用户名/仓库名/contents/文件路径
  • JSON格式:
{
  "message": "delete a file",
  "sha": "46d2b1f2ef54669a974165d0b37979e9adba1ab2"
}

删除成功后,会返回200消息:
image

增删改issues

如果做过了上面文件的增删改,这里大同小异,不同的访问路径和JSON的格式而已。唯一不同的是,issues是不用把内容转为Base64码的。

参考链接:github官方文档

增加一条issue

  • 传输方法:POST
  • 访问路径:https://api.github.com/repos/用户名/仓库名/issues
  • JSON格式:
{
  "title": "Creating issue from API",
  "body": "Posting a issue from Insomnia"
}
  • 注意:issue的数据里面是可以加label,milestone和assignees的。但是必须注意milestone和assignees必须是已有的名次完全对应才行,否则无法完成创建。

更改某条issue

  • 传输方法:PATCH
  • 访问路径:https://api.github.com/repos/用户名/仓库名/issues/序号
  • JSON格式:
{
  "title": "Creating issue from API ---updated",
  "body": "Posting a issue from Insomnia \n\n Updated from insomnia.",
  "state": "open"
}
  • 注意:如果JSON中加入空白的labels或assignees,如"labels": [],作用就是清空所有的标签和相关人。

锁住某条issue

不允许别人评论(自己可以)
image

  • 传输方法:PUT
  • 访问路径:https://api.github.com/repos/用户名/仓库名/issues/序号/lock
  • JSON格式:
{
  "locked": true,
  "active_lock_reason": "too heated"
}
  • 注意:active_lock_reason只能有4种值可选:off-topic, too heated, resolved, spam,否则报错。

另外,成功锁住,会返回204 No Content信息。

解锁某条issue

  • 传输方法:DELETE
  • 访问路径:https://api.github.com/repos/用户名/仓库名/issues/序号/lock
  • 无JSON传输

增删改comments

参考官方文档

增加comment

  • 传输方法:POST
  • 访问路径:https://api.github.com/repos/用户名/仓库名/issues/序号/comments
  • JSON格式:
{
  "body": "Create a comment from API"
}

更改comment

  • 传输方法:PATCH
  • 访问路径:https://api.github.com/repos/用户名/仓库名/issues/comments/评论ID
  • JSON格式:
{
  "body": "Create a comment from API \n\n----Updated"
}
  • 注意:地址中,issues后不用序号了,因为可以通过唯一的评论ID追查到。查看评论ID的方法,直接在上面查询链接中找。

删除comment

  • 传输方法:DELETE
  • 访问路径:https://api.github.com/repos/用户名/仓库名/issues/comments/评论ID
  • 无传输数据
查看原文

赞 78 收藏 57 评论 23

_John 赞了文章 · 2019-11-06

CodeSandbox 浏览器端的webpack是如何工作的? 上篇

这期来关注一下CodeSandbox, 这是一个浏览器端的沙盒运行环境,支持多种流行的构建模板,例如 create-react-appvue-cliparcel等等。 可以用于快速原型开发、DEMO 展示、Bug 还原等等.

相似的产品有很多,例如codepenJSFiddleWebpackBin(已废弃).

CodeSandbox 则更加强大,可以视作是浏览器端的 Webpack 运行环境, 甚至在 V3 版本已经支持 VsCode 模式,支持 Vscode 的插件和 Vim 模式、还有主题.

另外 CodeSandbox 支持离线运行(PWA)。基本上可以接近本地 VSCode 的编程体验. 有 iPad 的同学,也可以尝试基于它来进行开发。所以快速的原型开发我一般会直接使用 CodeSandbox

目录

笔者对 CodeSandbox 的第一印象是这玩意是运行在服务器的吧? 比如 create-react-app 要运行起来需要 node 环境,需要通过 npm 安装一大堆依赖,然后通过 Webpack 进行打包,最后运行一个开发服务器才能在浏览器跑起来.

实际上 CodeSandbox 打包和运行并不依赖于服务器, 它是完全在浏览器进行的. 大概的结构如下:

  • Editor: 编辑器。主要用于修改文件,CodeSandbox这里集成了 VsCode, 文件变动后会通知 Sandbox 进行转译. 计划会有文章专门介绍CodeSandbox的编辑器实现
  • Sandbox: 代码运行器。Sandbox 在一个单独的 iframe 中运行, 负责代码的转译(Transpiler)和运行(Evalation). 如最上面的图,左边是Editor,右边是Sandbox
  • Packager 包管理器。类似于yarn和npm,负责拉取和缓存 npm 依赖

CodeSandbox 的作者 Ives van Hoorne 也尝试过将 Webpack 移植到浏览器上运行,因为现在几乎所有的 CLI 都是使用 Webpack 进行构建的,如果能将 Webpack 移植到浏览器上, 可以利用 Webpack 强大的生态系统和转译机制(loader/plugin),低成本兼容各种 CLI.

然而 Webpack 太重了😱,压缩过后的大小就得 3.5MB,这还算勉强可以接受吧;更大的问题是要在浏览器端模拟 Node 运行环境,这个成本太高了,得不偿失。

所以 CodeSandbox 决定自己造个打包器,这个打包器更轻量,并且针对 CodeSandbox 平台进行优化. 比如 CodeSandbox 只关心开发环境的代码构建, 目标就是能跑起来就行了, 跟 Webpack 相比裁剪掉了以下特性:

  • 生产模式. CodeSandbox 只考虑 development 模式,不需要考虑 production一些特性,比如

    • 代码压缩,优化
    • Tree-shaking
    • 性能优化
    • 代码分割
  • 文件输出. 不需要打包成chunk
  • 服务器通信. Sandbox直接原地转译和运行, 而Webpack 需要和开发服务器建立一个长连接用于接收指令,例如 HMR.
  • 静态文件处理(如图片). 这些图片需要上传到 CodeSandbox 的服务器
  • 插件机制等等.

所以可以认为CodeSandbox是一个简化版的Webpack, 且针对浏览器环境进行了优化,比如使用worker来进行并行转译

CodeSandbox 的打包器使用了接近 Webpack Loader 的 API, 这样可以很容易地将 Webpack 的一些 loader 移植过来. 举个例子,下面是 create-react-app 的实现(查看源码):

import stylesTranspiler from "../../transpilers/style";
import babelTranspiler from "../../transpilers/babe";
// ...
import sassTranspiler from "../../transpilers/sass";
// ...

const preset = new Preset(
  "create-react-app",
  ["web.js", "js", "json", "web.jsx", "jsx", "ts", "tsx"],
  {
    hasDotEnv: true,
    setup: manager => {
      const babelOptions = {
        /*..*/
      };
      preset.registerTranspiler(
        module =>
          /\.(t|j)sx?$/.test(module.path) && !module.path.endsWith(".d.ts"),
        [
          {
            transpiler: babelTranspiler,
            options: babelOptions
          }
        ],
        true
      );
      preset.registerTranspiler(
        module => /\.svg$/.test(module.path),
        [
          { transpiler: svgrTranspiler },
          {
            transpiler: babelTranspiler,
            options: babelOptions
          }
        ],
        true
      );
      // ...
    }
  }
);

可以看出, CodeSandbox的Preset和Webpack的配置长的差不多. 不过, 目前你只能使用 CodeSandbox 预定义的 Preset, 不支持像 Webpack 一样进行配置, 个人觉得这个是符合 CodeSandbox 定位的,这是一个快速的原型开发工具,你还折腾 Webpack 干嘛?

目前支持这些Preset:


基本目录结构

CodeSandbox 的客户端是开源的,不然就没有本文了,它的基本目录结构如下:

  • packages

    • app CodeSandbox应用

      • app 编辑器实现
      • embed 网页内嵌运行 codesandbox
      • sandbox 运行沙盒,在这里执行代码构建和预览,相当于一个缩略版的 Webpack. 运行在单独的 iframe 中

        • eval

          • preset

            • create-react-app
            • parcel
            • vue-cli
            • ...
          • transpiler

            • babel
            • sass
            • vue
            • ...
        • compile.ts 编译器
    • common 放置通用的组件、工具方法、资源
    • codesandbox-api: 封装了统一的协议,用于 sandbox 和 editor 之间通信(基于postmessage)
    • codesandbox-browserfs: 这是一个浏览器端的‘文件系统’,模拟了 NodeJS 的文件系统 API,支持在本地或从多个后端服务中存储或获取文件.
    • react-sandpack: codesandbox公开的SDK,可以用于自定义自己的codesandbox

源码在这


项目构建过程

packager -> transpilation -> evaluation

Sandbox 构建分为三个阶段:

  • Packager 包加载阶段,下载和处理所有npm模块依赖
  • Transpilation 转译阶段,转译所有变动的代码, 构建模块依赖图
  • Evaluation 执行阶段,使用 eval 运行模块代码进行预览

下面会按照上述的步骤来描述其中的技术点

Packager

尽管 npm 是个'黑洞',我们还是离不开它。 其实大概分析一下前端项目的 node_modules,80%是各种开发依赖组成的.

由于 CodeSandbox 已经包揽了代码构建的部分,所以我们并不需要devDependencies, 也就是说 在CodeSandbox 中我们只需要安装所有实际代码运行需要的依赖,这可以减少成百上千的依赖下载. 所以暂且不用担心浏览器会扛不住.

WebpackDllPlugin

CodeSandbox 的依赖打包方式受 WebpackDllPlugin 启发,DllPlugin 会将所有依赖都打包到一个dll文件中,并创建一个 manifest 文件来描述dll的元数据(如下图).

Webpack 转译时或者 运行时可以根据 manifest 中的模块索引(例如__webpack_require__('../node_modules/react/index.js'))来加载 dll 中的模块。 因为WebpackDllPlugin是在运行或转译之前预先对依赖的进行转译,所以在项目代码转译阶段可以忽略掉这部分依赖代码,这样可以提高构建的速度(真实场景对npm依赖进行Dll打包提速效果并不大):

manifest文件

在线打包服务

基于这个思想, CodeSandbox 构建了自己的在线打包服务, 和WebpackDllPlugin不一样的是,CodeSandbox是在服务端预先构建Manifest文件的, 而且不区分Dll和manifest文件。 具体思路如下:

简而言之,CodeSandbox 客户端拿到package.json之后,将dependencies转换为一个由依赖和版本号组成的Combination(标识符, 例如 v1/combinations/babel-runtime@7.3.1&csbbust@1.0.0&react@16.8.4&react-dom@16.8.4&react-router@5.0.1&react-router-dom@5.0.1&react-split-pane@0.1.87.json), 再拿这个 Combination 到服务器请求。服务器会根据 Combination 作为缓存键来缓存打包结果,如果没有命中缓存,则进行打包.

打包实际上还是使用yarn来下载所有依赖,只不过这里为了剔除 npm 模块中多余的文件,服务端还遍历了所有依赖的入口文件(package.json#main), 解析 AST 中的 require 语句,递归解析被 require 模块. 最终形成一个依赖图, 只保留必要的文件.

最终输出 Manifest 文件,它的结构大概如下, 他就相当于WebpackDllPlugin的dll.js+manifest.json的结合体:

{
  // 模块内容
  "contents": {
    "/node_modules/react/index.js": {
      "content": "'use strict';↵↵if ....", // 代码内容
      "requires": [                        // 依赖的其他模块
        "./cjs/react.development.js",
      ],
    },
    "/node_modules/react-dom/index.js": {/*..*/},
    "/node_modules/react/package.json": {/*...*/},
    //...
  },
  // 模块具体安装版本号
  "dependencies": [{name: "@babel/runtime", version: "7.3.1"}, {name: "csbbust", version: "1.0.0"},/*…*/],
  // 模块别名, 比如将react作为preact-compat的别名
  "dependencyAliases": {},
  // 依赖的依赖, 即间接依赖信息. 这些信息可以从yarn.lock获取
  "dependencyDependencies": {
    "object-assign": {
      "entries": ["object-assign"], // 模块入口
      "parents": ["react", "prop-types", "scheduler", "react-dom"], // 父模块
      "resolved": "4.1.1",
      "semver": "^4.1.1",
    }
    //...
  }
}
Serverless 思想


值得一提的是 CodeSandbox 的 Packager 后端使用了 Serverless(基于 AWS Lambda),基于 Serverless 的架构让 Packager 服务更具伸缩性,可以灵活地应付高并发的场景。使用 Serverless 之后 Packager 的响应时间显著提高,而且费用也下去了。

Packager 也是开源的, 围观

回退方案

AWS Lambda函数是有局限性的, 比如/tmp最多只能有 500MB 的空间. 尽管大部分依赖打包场景不会超过这个限额, 为了增强可靠性(比如上述的方案可能出错,也可能漏掉一些模块), Packager还有回退方案.

后来CodeSanbox作者开发了新的Sandbox,支持把包管理的步骤放置到浏览器端, 和上面的打包方式结合着使用。原理也比较简单: 在转译一个模块时,如果发现模块依赖的npm模块未找到,则惰性从远程下载回来. 来看看它是怎么处理的:

在回退方案中CodeSandbox 并不会将 package.json 中所有的包都下载下来,而是在模块查找失败时,惰性的去加载。比如在转译入口文件时,发现 react 这个模块没有在本地缓存模块队列中,这时候就会到远程将它下载回来,然后接着转译。

也就是说,因为在转译阶段会静态分析模块的依赖,只需要将真正依赖的文件下载回来,而不需要将整个npm包下载回来,节省了网络传输的成本.

CodeSandbox 通过 unpkg.comcdn.jsdelivr.net 来获取模块的信息以及下载文件, 例如

  • 获取 package.json: https://unpkg.com/react@latest/package.json
  • 包目录结构获取: https://unpkg.com/antd@3.17.0/?meta 这个会递归返回该包的所有目录信息
  • 具体文件下载: https://unpkg.com/react@16.8.6/cjs/react.production.min.js 或者 https://cdn.jsdelivr.net/npm/@babel/runtime@7.3.1/helpers/interopRequireDefault.js

Transpilation

讲完 Packager 现在来看一下 Transpilation, 这个阶段从应用的入口文件开始, 对源代码进行转译, 解析AST,找出下级依赖模块,然后递归转译,最终形成一个'依赖图':

CodeSandbox 的整个转译器是在一个单独的 iframe 中运行的:

Editor 负责变更源代码,源代码变更会通过 postmessage 传递给 Compiler,这里面会携带 Module+template

  • Module 中包含所有源代码内容和模块路径,其中还包含 package.json, Compiler 会根据 package.json 来读取 npm 依赖;
  • template 表示 Compiler 的 Preset,例如create-react-appvue-cli, 定义了一些 loader 规则,用来转译不同类型的文件, 另外preset也决定了应用的模板和入口文件。 通过上文我们知道, 这些 template 目前的预定义的.

基本对象

在详细介绍 Transpilation 之前先大概看一些基本对象,了解这些对象之间的关系:

  • Manager 这是 Sandbox 的核心对象,负责管理配置信息(Preset)、项目依赖(Manifest)、以及维护项目所有模块(TranspilerModule)
  • Manifest 通过上文的 Packager 我们知道,Manifest 维护所有依赖的 npm 模块信息
  • TranspiledModule 表示模块本身。这里面维护转译的结果、代码执行的结果、依赖的模块信息,负责驱动具体模块的转译(调用 Transpiler)和执行
  • Preset 一个项目构建模板,例如 vue-clicreate-react-app. 配置了项目文件的转译规则, 以及应用的目录结构(入口文件)
  • Transpiler 等价于 Webpack 的 loader,负责对指定类型的文件进行转译。例如 babel、typescript、pug、sass 等等
  • WorkerTranspiler 这是 Transpiler 的子类,调度一个 Worker池来执行转译任务,从而提高转译的性能

Manager

Manager是一个管理者的角色,从大局上把控整个转译和执行的流程. 现在来看看整体的转译流程:

大局上基本上可以划分为以下四个阶段:

  • 配置阶段:配置阶段会创建 Preset 对象,确定入口文件等等. CodeSandbox 目前只支持限定的几种应用模板,例如 vue-cli、create-react-app。不同模板之间目录结构的约定是不一样的,例如入口文件和 html 模板文件。另外文件处理的规则也不一样,比如 vue-cli 需要处理.vue文件。
  • 依赖下载阶段: 即 Packager 阶段,下载项目的所有依赖,生成 Manifest 对象
  • 变动计算阶段:根据 Editor 传递过来的源代码,计算新增、更新、移除的模块。
  • 转译阶段:真正开始转译了,首先重新转译上个阶段计算出来的需要更新的模块。接着从入口文件作为出发点,转译和构建新的依赖图。这里不会重复转译没有变化的模块以及其子模块

TranspiledModule

TranspiledModule用于管理某个具体的模块,这里面会维护转译和运行的结果、模块的依赖信息,并驱动模块的转译和执行:

TranspiledModule 会从Preset中获取匹配当前模块的Transpiler列表的,遍历Transpiler对源代码进行转译,转译的过程中会解析AST,分析模块导入语句, 收集新的依赖; 当模块转译完成后,会递归转译依赖列表。 来看看大概的代码:

  async transpile(manager: Manager) {
    // 已转译
    if (this.source)  return this
    // 避免重复转译, 一个模块只转译一次
    if (manager.transpileJobs[this.getId()]) return this;
    manager.transpileJobs[this.getId()] = true;

    // ...重置状态 

    // 🔴从Preset获取Transpiler列表
    const transpilers = manager.preset.getLoaders(this.module, this.query);

    // 🔴 链式调用Transpiler
    for (let i = 0; i < transpilers.length; i += 1) {
      const transpilerConfig = transpilers[i];
      // 🔴构建LoaderContext,见下文
      const loaderContext = this.getLoaderContext(
        manager,
        transpilerConfig.options || {}
      );

      // 🔴调用Transpiler转译源代码
      const {
        transpiledCode,
        sourceMap,
      } = await transpilerConfig.transpiler.transpile(code, loaderContext); // eslint-disable-line no-await-in-loop

      if (this.errors.length) {
        throw this.errors[0];
      }
    }

    this.logWarnings();

    // ...

    await Promise.all(
      this.asyncDependencies.map(async p => {
        try {
          const tModule = await p;
          this.dependencies.add(tModule);
          tModule.initiators.add(this);
        } catch (e) {
          /* let this handle at evaluation */
        }
      })
    );
    this.asyncDependencies = [];

    // 🔴递归转译依赖的模块
    await Promise.all(
      flattenDeep([
        ...Array.from(this.transpilationInitiators).map(t =>
          t.transpile(manager)
        ),
        ...Array.from(this.dependencies).map(t => t.transpile(manager)),
      ])
    );

    return this;
  }

Transpiler

Transpiler等价于webpack的loader,它配置方式以及基本API也和webpack(查看webpack的loader API)大概保持一致,比如链式转译和loader-context. 来看一下Transpiler的基本定义:

export default abstract class Transpiler {
  initialize() {}

  dispose() {}

  cleanModule(loaderContext: LoaderContext) {}

  // 🔴 代码转换
  transpile(
    code: string,
    loaderContext: LoaderContext
  ): Promise<TranspilerResult> {
    return this.doTranspilation(code, loaderContext);
  }

  // 🔴 抽象方法,由具体子类实现
  abstract doTranspilation(
    code: string,
    loaderContext: LoaderContext
  ): Promise<TranspilerResult>;

  // ...
}

Transpiler的接口很简单,transpile接受两个参数:

  • code即源代码.
  • loaderContext 由TranspiledModule提供, 可以用来访问一下转译上下文信息,比如Transpiler的配置、 模块查找、注册依赖等等。大概外形如下:
export type LoaderContext = {
  // 🔴 信息报告
  emitWarning: (warning: WarningStructure) => void;
  emitError: (error: Error) => void;
  emitModule: (title: string, code: string, currentPath?: string, overwrite?: boolean, isChild?: boolean) => TranspiledModule;
  emitFile: (name: string, content: string, sourceMap: SourceMap) => void;
  // 🔴 配置信息
  options: {
    context: string;
    config?: object;
    [key: string]: any;
  };
  sourceMap: boolean;
  target: string;
  path: string;
  addTranspilationDependency: (depPath: string, options?: { isAbsolute?: boolean; isEntry?: boolean; }) => void;
  resolveTranspiledModule: ( depPath: string, options?: { isAbsolute?: boolean; ignoredExtensions?: Array<string>; }) => TranspiledModule;
  resolveTranspiledModuleAsync: ( depPath: string, options?: { isAbsolute?: boolean; ignoredExtensions?: Array<string>; }) => Promise<TranspiledModule>;
    // 🔴 依赖收集
  addDependency: ( depPath: string, options?: { isAbsolute?: boolean; isEntry?: boolean; }) => void;
  addDependenciesInDirectory: ( depPath: string, options?: { isAbsolute?: boolean; isEntry?: boolean; }) => void;
  _module: TranspiledModule;
};

先从简单的开始,来看看JSON模块的Transpiler实现, 每个Transpiler子类需要实现doTranspilation,接收源代码,并异步返回处理结果:

class JSONTranspiler extends Transpiler {
  doTranspilation(code: string) {
    const result = `
      module.exports = JSON.parse(${JSON.stringify(code || '')})
    `;

    return Promise.resolve({
      transpiledCode: result,
    });
  }
}

BabelTranspiler

并不是所有模块都像JSON这么简单,比如Typescript和Babel。 为了提高转译的效率,Codesandbox会利用Worker来进行多进程转译,多Worker的调度工作由WorkerTranspiler完成,这是Transpiler的子类,维护了一个Worker池。Babel、Typescript、Sass这类复杂的转译任务都是基于WorkerTranspiler实现的:

其中比较典型的实现是BabelTranspiler, 在Sandbox启动时就会预先fork三个worker,来提高转译启动的速度, BabelTranspiler会优先使用这三个worker来初始化Worker池:

// 使用worker-loader fork三个loader,用于处理babel编译
import BabelWorker from 'worker-loader?publicPath=/&name=babel-transpiler.[hash:8].worker.js!./eval/transpilers/babel/worker/index.js';

window.babelworkers = [];
for (let i = 0; i < 3; i++) {
  window.babelworkers.push(new BabelWorker());
}

这里面使用到了webpack的worker-loader, 将指定模块封装为 Worker 对象。让 Worker 更容易使用:

// App.js
import Worker from "./file.worker.js";

const worker = new Worker();

worker.postMessage({ a: 1 });
worker.onmessage = function(event) {};

worker.addEventListener("message", function(event) {});

BabelTranpiler具体的流程如下:

WorkerTranspiler会维护空闲的Worker队列和一个任务队列, 它的工作就是驱动Worker来消费任务队列。具体的转译工作在Worker中进行:


Evaluation

虽然称为打包器(bundler), 但是 CodeSandbox 并不会进行打包,也就是说他不会像 Webpack 一样,将所有的模块都打包合并成 chunks 文件.

Transpilation入口文件开始转译, 再分析文件的模块导入规则,递归转译依赖的模块. 到Evaluation阶段,CodeSandbox 已经构建出了一个完整的依赖图. 现在要把应用跑起来了🏃

Evaluation 的原理也比较简单,和 Transpilation 一样,也是从入口文件开始: 使用eval执行入口文件,如果执行过程中调用了require,则递归 eval 被依赖的模块

如果你了解过 Node 的模块导入原理,你可以很容易理解这个过程:

  • ① 首先要初始化 html,找到index.html文件,将 document.body.innerHTML 设置为 html 模板的 body 内容.
  • ② 注入外部资源。用户可以自定义一些外部静态文件,例如 css 和 js,这些需要 append 到 head 中
  • ③ evaluate 入口模块
  • ④ 所有模块都会被转译成 CommonJS 模块规范。所以需要模拟这个模块环境。大概看一下代码:

    // 实现require方法
    function require(path: string) {
      // ... 拦截一些特殊模块
    
      // 在Manager对象中查找模块
      const requiredTranspiledModule = manager.resolveTranspiledModule(
        path,
        localModule.path
      );
    
      // 模块缓存, 如果存在缓存则说明不需要重新执行
      const cache = requiredTranspiledModule.compilation;
    
      return cache
        ? cache.exports
        : // 🔴递归evaluate
          manager.evaluateTranspiledModule(
            requiredTranspiledModule,
            transpiledModule
          );
    }
    
    // 实现require.resolve
    require.resolve = function resolve(path: string) {
      return manager.resolveModule(path, localModule.path).path;
    };
    
    // 模拟一些全局变量
    const globals = {};
    globals.__dirname = pathUtils.dirname(this.module.path);
    globals.__filename = this.module.path;
    
    // 🔴放置执行结果,即CommonJS的module对象
    this.compilation = {
      id: this.getId(),
      exports: {}
    };
    
    // 🔴eval
    const exports = evaluate(
      this.source.compiledCode,
      require,
      this.compilation,
      manager.envVariables,
      globals
    );
  • ⑤ 使用 eval 来执行模块。同样看看代码:

    export default function(code, require, module, env = {}, globals = {}) {
      const exports = module.exports;
      const global = g;
      const process = buildProcess(env);
      g.global = global;
      const allGlobals = {
        require,
        module,
        exports,
        process,
        setImmediate: requestFrame,
        global,
        ...globals
      };
    
      const allGlobalKeys = Object.keys(allGlobals);
      const globalsCode = allGlobalKeys.length ? allGlobalKeys.join(", ") : "";
      const globalsValues = allGlobalKeys.map(k => allGlobals[k]);
      // 🔴将代码封装到一个函数下面,全局变量以函数形式传入
      const newCode = `(function evaluate(` + globalsCode + `) {` + code + `\n})`;
      (0, eval)(newCode).apply(this, globalsValues);
    
      return module.exports;
    }

Ok!到这里 Evaluation 就解释完了,实际的代码比这里要复杂得多,比如 HMR(hot module replacement)支持, 有兴趣的读者,可以自己去看 CodeSandbox 的源码.


技术地图

一不小心又写了一篇长文,要把这么复杂代码讲清楚真是一个挑战, 我还做的不够好,按照以往的经验,这又是一篇无人问津的文章, 别说是你们, 我自己都不怎么有耐心看这类文章, 后面还是尽量避免吧!

  • worker-loader: 将指定模块封装为Worker
  • babel: JavaScript代码转译,支持ES, Flow, Typescript
  • browserfs: 在浏览器中模拟Node环境
  • localForage: 客户端存储库,优先使用(IndexedDB or WebSQL)这些异步存储方案,提供类LocalStorage的接口
  • lru-cache: least-recently-used缓存

扩展

查看原文

赞 76 收藏 33 评论 17

_John 回答了问题 · 2019-08-16

解决typescript find() 怎么写类型接口

如果 cart 一定找得到就试试这样

cart.checked = !cart!.checked;

否则就放在 if 里

if (cart) {
    ...
}

关注 6 回答 6

_John 赞了文章 · 2019-06-17

经验分享:微信小程序外包接单常见问题及流程

从九月底内测到现在已经三个半月.凌晨一点睡觉已经习以为常,也正是这样,才让无前端经验的我做微信小程序开发并不感到费劲.
最近才开始接微信小程序的外包项目,目前已经签下了五份合同,成品出了两个.加上转给朋友做的,正在谈的,算起来有十来个项目.
距离微信正式开放的时间越来越近,接外包的同学也越来越多.@天下雪 让分享下接外包的经验,作为微信小程序联盟 外包版的版主,自然是当仁不让.

经常遇到的几个情况:

1.客户:开发一个微信小程序多少钱?

这是客户最常问到的问题,就跟我问你吃一顿饭多少钱一样.很难回答上来.至少不是一两句话能说完的.在哪里吃?吃快餐还是满汉全席?

地域不同,吃的东西不一样,价格自然也就不一样.

有句话说得好,不谈需求的议价都是耍流氓.

这时候就直接点,你有没有需求文档?把需求说详细点就行.没有需求文档?有没有类似的APP?都没有?那基本没戏.

这样的客户连自己想要什么都不知道,写代码的你能知道?那你岂不是他肚子里的蛔虫.哈哈

2.多久能上线?一天够不够?

开发周期一定要实话实说,能多久做完就说多久.不然后期扯皮就不好了.

3.我要做个像淘宝那样的商城,你能做吗?

这样的客户确实知道自己要什么,但是你确定是淘宝?

淘宝一秒可以成交17万笔,你确定要这么牛逼?淘宝一年的经费可不是几万块,而且现在我们所见到的版本是经过多年迭代的.

4.我要做个蓝牙连接XXX设备,我要做个直播的微信小程序;我要做一款小游戏....

很明显.微信小程序现在不支持蓝牙连接.不让做直播,不让做小游戏...

开发人员需要对微信官方的设计指南和运营规范有大致的了解.

运营规范

5.我要做个论坛

微信小程序的项目中比较火的算是论坛,有做本地的,有做专业性比较强的小分类的.我会多问几句,有没有数据支持.比如说客户要做个美女图片的社区.我会问问,他有没有图片资源.我只是关心客户而已,没有别的意思.哈哈哈哈

如果没有数据支持,那真做不了.

类似的很多,需要用到品牌库,文献库的模块,先问问,不然后面你没办法帮客户解决,那就是你的锅.

6.听说微信小程序开发很简单,成本大幅降低.两千块你能不能帮我做个有赞那样的商城?

长久来看,微信小程序开发的成本确实比android + ios +H5 开发的成本要低,但是目前做不到.别听那些人说一周做出了一个微信小程序,就觉得成本低了.首先别人有成型的UI,现成的后台,你有啥?前端,后台,UI,产品设计....

7.你们是团队开发还是个人,合同跟谁签?

我一般会如实回答,大学室友一起兼职做外包,android,ios,h5,微信小程序都能做(也算是打个广告,说不定客户有其他需求呢),如果是全职接外包,价格也不会这么低(我都是给良心价);

合同是个公司签,我是法人.

然后贴上营业执照.有很多客户不在乎这些,不问起就别说了.多一事不如少一事.

目前就想到了这么多情况.以后想到或者遇到再来补充.

接外包的条件:

1.微信小程序前端开发

2.后台开发

3.UI设计

4.产品设计

接外包的时候为了节约成本,增加自身竞争力,通常是一人身兼数职.

再说说接外包的流程:

1)有没有公司资质?

目前微信小程序不对个人开放.如果做完了,不能上架,问题会比较严重.

2)服务器.域名备案

腾讯云有全套解决方案.尽快.也是个费时间的事情.

3)如果12都行,那就细化需求,画个产品原型图看看.(产品经理干的活,如今程序员想接外包,那就得干)

之前有个客户说他的需求很简单,就三五个页面.后来细化了需求.活生生干到了二十多个页面.你想想看,如果我傻不拉几先把合同签了,那就呵呵了.

4)估算开发周期

这个因人而异,熟手一看就知道开发周期,但是目前微信小程序开发都是新手,很少有做过两个以上商业项目的人,所以很难估算.尽量多预留时间吧,看这来不及了就加加班.

5)估算开发费用

这个是大家最关心的.

我一般是按照开发时间来计算开发费用,比如说一个微信小程序需要一个月做完,前后台各一个,UI一个,产品的活我已经干了,如果UI要求不高,我自己也能切几张图(只不过比较丑,哈哈),测试就别说了,自己测试.

估算:前端一个,后台一个,UI只需要半个月就能做完,算半个,然后乘上月薪,按照10K的标准来(我自然不止10K啦,呵呵呵.先做个梦)

费用 24K = 110K + 110K + 0.5*8K

6)45都谈好了.那就签合同吧.

这个很有必要.遇到经常反悔的客户还需要录音,时不时给你丢个需求让你开发,或者觉得之前的设计不够好,需要改改.

不签合同最后会死的很惨.

(我们写代码的人实诚,绝对不坑别人,但也不能让别人坑啊)

我的合同是直接在百度下载的,最下载数最多的那一个肯定没错.拿回来自己修改下.我本来打算贴一份合同在上面的,考虑到涉及客户的商业机密,还是算了.百度软件外包合同,一堆一堆的.作为程序员,这不算事.

7)打款

这个其实应该算在合同里.但是关系重大.拿出来说说.

我这边是按照开发周期或者开发费用来分,大于三个月,那就每个月一次打款,比例自己去谈,30% 30% 40%,20% 30% 50%...

开发费用大于五万.....

也有遇到费用只有五千,还要分三期四期的.心累.

8)准备好了.开始干吧.

我以前都是签小单,毕竟微信有个1M的限制在那里摆着,不可能做特别大.

同学们看到有不妥的地方记得告诉我.

如需发外包或接外包,欢迎在这里发布相关信息
http://www.wxapp-union.com/fo...

来自:微信小程序联盟

查看原文

赞 5 收藏 17 评论 0

_John 赞了文章 · 2019-05-30

mac电脑终端怎么显示项目树:tree命令的使用

tree-command

how to use tree command on mac

Mac中tree命令使用

tree是一个能列出递归目录的命令,以图形显示驱动器或路径的文件夹结构,可以生成命令树。Windows和Linux都有,mac没有原生支持,需要安装tree或者在home目录中添加.bashrc文件里面加入:
alias tree="find . -print | sed -e 's;1*/;|____;g;s;____|; |;g'" 。保存退出后,使用source .bashrc命令更新一下脚本资源,这是再在终端下试一下tree命令.
第二种没有试过,我们来试下比较简单的第一种吧。


# 安装Homebrew 在终端输入下面指令

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

# 安装 tree
brew install tree

在某个项目下执行tree命令即可

panzhiqiangdeMacBook-Pro:webpack2-in-action panzhiqiang$ tree -L 1

# -L 1 指只显示一级目录

.
├── README.md
├── index.html
├── index.js
├── node_modules
└── package.json

tree命令后面的参数有其他可选,具体我们可以用tree --help来查看

panzhiqiangdeMacBook-Pro:webpack2-in-action panzhiqiang$ tree --help
usage: tree [-acdfghilnpqrstuvxACDFJQNSUX] [-H baseHREF] [-T title ]
        [-L level [-R]] [-P pattern] [-I pattern] [-o filename] [--version]
        [--help] [--inodes] [--device] [--noreport] [--nolinks] [--dirsfirst]
        [--charset charset] [--filelimit[=]#] [--si] [--timefmt[=]<f>]
        [--sort[=]<name>] [--matchdirs] [--ignore-case] [--] [<directory list>]
  ------- Listing options -------
  -a            All files are listed.
  -d            List directories only.
  -l            Follow symbolic links like directories.
  -f            Print the full path prefix for each file.
  -x            Stay on current filesystem only.
  -L level      Descend only level directories deep.
  -R            Rerun tree when max dir level reached.
  -P pattern    List only those files that match the pattern given.
  -I pattern    Do not list files that match the given pattern.
  --ignore-case Ignore case when pattern matching.
  --matchdirs   Include directory names in -P pattern matching.
  --noreport    Turn off file/directory count at end of tree listing.
  --charset X   Use charset X for terminal/HTML and indentation line output.
  --filelimit # Do not descend dirs with more than # files in them.
  --timefmt <f> Print and format time according to the format <f>.
  -o filename   Output to file instead of stdout.
  -------- File options ---------
  -q            Print non-printable characters as '?'.
  -N            Print non-printable characters as is.
  -Q            Quote filenames with double quotes.
  -p            Print the protections for each file.
  -u            Displays file owner or UID number.
  -g            Displays file group owner or GID number.
  -s            Print the size in bytes of each file.
  -h            Print the size in a more human readable way.
  --si          Like -h, but use in SI units (powers of 1000).
  -D            Print the date of last modification or (-c) status change.
  -F            Appends '/', '=', '*', '@', '|' or '>' as per ls -F.
  --inodes      Print inode number of each file.
  --device      Print device ID number to which each file belongs.
  ------- Sorting options -------
  -v            Sort files alphanumerically by version.
  -t            Sort files by last modification time.
  -c            Sort files by last status change time.
  -U            Leave files unsorted.
  -r            Reverse the order of the sort.
  --dirsfirst   List directories before files (-U disables).
  --sort X      Select sort: name,version,size,mtime,ctime.
  ------- Graphics options ------
  -i            Don't print indentation lines.
  -A            Print ANSI lines graphic indentation lines.
  -S            Print with CP437 (console) graphics indentation lines.
  -n            Turn colorization off always (-C overrides).
  -C            Turn colorization on always.
  ------- XML/HTML/JSON options -------
  -X            Prints out an XML representation of the tree.
  -J            Prints out an JSON representation of the tree.
  -H baseHREF   Prints out HTML format with baseHREF as top directory.
  -T string     Replace the default HTML title and H1 header with string.
  --nolinks     Turn off hyperlinks in HTML output.
  ---- Miscellaneous options ----
  --version     Print version and exit.
  --help        Print usage and this help message and exit.
  --            Options processing terminator.

这样我们可以愉快的玩tree命令了,感谢


  1. /
查看原文

赞 6 收藏 1 评论 0

_John 赞了文章 · 2019-05-16

CSS实现两个球相交的粘粘效果

这是一个纯粹利用CSS所做出来的效果,这个效果说穿了就是一个图像处理的原理,做法跟Photoshop里头的几乎一模一样,只是一个用图层和色版来制作,一个则是用CSS(把div当成图层思考就好了)。

从PhotoShop开始

一开始我们来玩Photoshop,会比直接写CSS来得容易理解(没有Photoshop的人也没关系,看完说明也就懂了),首先我们新增两个图层,一个里头放上红色的小球,另外一个里头放上黑色的大球。

clipboard.png

接着我们使用高斯模糊的滤镜,分别把小红球和大黑球模糊。

clipboard.png

clipboard.png

再来我们新增一个“亮度与对比”的调整图层,勾选使用旧版,然后把对比数值往上拉,你就会看到神奇的现象。

clipboard.png

拉到边缘不再模糊之后,你就可以用鼠标尝试着把红球移动,就会发现红球与黑球交界的地方变成粘粘的效果心,这就是我们要做的效果!

图片描述

就这样,你已经知道如何用Photoshop来制作,同理,CSS也是用同样的方法,只是把图层换成了div而已,就这么简单。

CSS效果

首先我在HTML里头,放上一个class为redball的div当作红球,class为blackball的是黑球,这就是刚刚在PhotoShop里头的两个图层,接着在最外围放上一个class为effect的div,这就是我们的调整图层,完成后HTML代码的长相应该是这样:

<div class="effect">
  <div class="blackball"></div>
  <div class="redball"></div>
</div>

只要对blackball和redball加入模糊的滤镜,对effect加入对比的滤镜,就能够达到Photoshop里面的特效,而模糊的滤镜必须使用filter:blur(数值),对比则使用filter:contrast(数值)。

图片描述

CSS的长相会长这样:

.effect{
  width:100%;
  height:100%;
  padding-top:50px;
  filter:contrast(10);
  background:#fff;
}
.blackball{
  width:100px;
  height:100px;
  background:black;
  padding:10px;
  border-radius:50%;
  margin:0 auto;
  z-index:1;
  filter:blur(5px);
}
.redball{
  width:60px;
  height:60px;
  background:#f00;
  padding:10px;
  border-radius:50%;
  position:absolute;
  top:70px;
  left:50px;
  z-index:2;
  filter:blur(5px) ;
  animation:rball 6s infinite;
}

忽略CSS里头那些定位数值,里头blur的数值设为5px,contrast的数值设为10,就可以看到红求黑球粘在一起了,至于该怎么让他们动起来呢?就要使用CSS3的animation,animation的程序如下:

@keyframes rball{
  0%,100%{
    left:35px;
    width:60px;
    height:60px;
  }
  4%,54%{
    width:60px;
    height:60px;
  }
  10%,60%{
    width:50px;
    height:70px;
  }
  20%,70%{
    width:60px;
    height:60px;
  }
  34%,90%{
    width:70px;
    height:50px;
  }
  41%{
    width:60px;
    height:60px;
  }
  50%{
    left:calc(100% - 95px);
    width:60px;
    height:60px;
  }
}

这里的keyframe写了很多,因为要让红球进入黑球时,水平方向会被压缩一下,然后再离开黑球的时候,水平方向会被拉长,如此一来才会更像有粘性的感觉,为了测试这个效果,可真是煞费我的苦心呀!(不过这里有个要注意的地方,由于位置上会自动去计算,所以要测试的话,最外层的effect宽度记得设为320px)

完成红球之后,要让两颗蓝色球合在一起再分开,也是同样的原理,下方列出两颗蓝色球的CSS,比较需要注意的地方是我让蓝色球合体之后会变大一些,分开的时候也会拉长。

图片描述

.blueball1{
  width:80px;
  height:80px;
  background:#00f;
  padding:10px;
  border-radius:50%;
  position:absolute;
  top:230px;
  left:0;
  z-index:2;
  filter:blur(8px) ;
  animation:bball1 6s infinite;
}
.blueball2{
  width:80px;
  height:80px;
  background:#00f;
  padding:10px;
  border-radius:50%;
  position:absolute;
  top:230px;
  left:240px;
  z-index:2;
  filter:blur(8px) ;
  animation:bball2 6s infinite;
}
@keyframes bball1{
  0%,100%{
    left:0;
    top:230px;
    width:80px;
    height:80px;
  }
  20%{
    top:230px;
    width:80px;
    height:80px;
  }
  85%{
    top:230px;
    left:75px;
    width:90px;
    height:70px;
  }
  90%{
    top:228px;
    width:75px;
    height:85px;
  }
  50%{
    top:215px;
    left:110px;
    width:110px;
    height:110px;
  }
}
@keyframes bball2{
  0%,100%{
    left:240px;
    top:230px;
    width:80px;
    height:80px;
  }
  20%{
    top:230px;
    width:80px;
    height:80px;
  }
  85%{
    top:230px;
    left:165px;
    width:90px;
    height:70px;
  }
  90%{
    top:228px;
    width:75px;
    height:85px;
  }
  50%{
    left:110px;
    top:215px;
    width:110px;
    height:110px;
  }
}

就这样,单纯利用CSS就完成了一个粘粘的效果,坦白说我也不太清楚这个效果可以用在哪里,不过如果用在水底世界或一些loading的特效,应该是相当不赖的!

获取粘粘球完整动画代码可以:
图片描述

查看原文

赞 97 收藏 66 评论 8

_John 赞了文章 · 2019-03-31

Manacher 算法

原文:https://ethsonliu.com/2018/04...

给定一个字符串,求出其最长回文子串。例如:

  1. s="abcd",最长回文长度为 1;
  2. s="ababa",最长回文长度为 5;
  3. s="abccb",最长回文长度为 4,即 bccb。

以上问题的传统思路大概是,遍历每一个字符,以该字符为中心向两边查找。其时间复杂度为 $O(n^2)$,效率很差。

1975 年,一个叫 Manacher 的人发明了一个算法,Manacher 算法(中文名:马拉车算法),该算法可以把时间复杂度提升到 $O(n)$。下面来看看马拉车算法是如何工作的。

算法过程分析

由于回文分为偶回文(比如 bccb)和奇回文(比如 bcacb),而在处理奇偶问题上会比较繁琐,所以这里我们使用一个技巧,具体做法是:

  1. 在字符串首尾及每个字符间都插入一个 "#",这样可以使得原先的奇偶回文都变为奇回文;
  2. 接着再在首尾两端各插入 "$" 和 "^",这样中心扩展寻找回文的时候会自动退出循环,不需每次判断是否越界,可参见下面代码。
  3. 上述新插入的三个字符,即 "#"、 "$" 和 "^",必须各异,且不可以与原字符串中的字符相同。

举个例子:s="abbahopxpo",转换为 s_new="$#a#b#b#a#h#o#p#x#p#o#^"。如此,s 里起初有一个偶回文 abba 和一个奇回文 opxpo,被转换为 #a#b#b#a##o#p#x#p#o#,长度都转换成了奇数。

定义一个辅助数组 int p[],其中 p[i] 表示以 i 为中心的最长回文的半径,例如:

i012345678910111213141516171819202122
s_new[i]$#a#b#b#a#h#o#p#x#p#o#^
p[i]11212521212121216121211

可以看出,p[i] - 1 正好是原字符串中最长回文串的长度

接下来的重点就是求解 p 数组,如下图:

设置两个变量,mx 和 id 。mx 代表以 id 为中心的最长回文的右边界,也就是 mx = id + p[id]

假设我们现在求 p[i],也就是以 i 为中心的最长回文半径,如果 i < mx,如上图,那么:

if (i < mx)  
    p[i] = min(p[2 * id - i], mx - i);

2 * id - i 为 i 关于 id 的对称点,即上图的 j 点,而 p[j]表示以 j 为中心的最长回文半径,因此我们可以利用 p[j] 来加快查找。

代码

#include <iostream>  
#include <cstring>
#include <algorithm>  

using namespace std;

char s[1000];
char s_new[2000];
int p[2000];

int Init()
{
    int len = strlen(s);
    s_new[0] = '$';
    s_new[1] = '#';
    int j = 2;

    for (int i = 0; i < len; i++)
    {
        s_new[j++] = s[i];
        s_new[j++] = '#';
    }

    s_new[j++] = '^';  // 别忘了哦
    s_new[j] = '\0';   // 这是一个好习惯
    
    return j;  // 返回 s_new 的长度
}

int Manacher()
{
    int len = Init();  // 取得新字符串长度并完成向 s_new 的转换
    int max_len = -1;  // 最长回文长度

    int id;
    int mx = 0;

    for (int i = 1; i < len; i++)
    {
        if (i < mx)
            p[i] = min(p[2 * id - i], mx - i);  // 需搞清楚上面那张图含义,mx 和 2*id-i 的含义
        else
            p[i] = 1;

        while (s_new[i - p[i]] == s_new[i + p[i]])  // 不需边界判断,因为左有 $,右有 ^
            p[i]++;

        // 我们每走一步 i,都要和 mx 比较,我们希望 mx 尽可能的远,
        // 这样才能更有机会执行 if (i < mx)这句代码,从而提高效率
        if (mx < i + p[i])
        {
            id = i;
            mx = i + p[i];
        }

        max_len = max(max_len, p[i] - 1);
    }

    return max_len;
}

int main()
{
    while (printf("请输入字符串:"))
    {
        scanf("%s", s);
        printf("最长回文长度为 %d\n\n", Manacher());
    }
    return 0;
}

测试如下:

请输入字符串:abbahopxpo
最长回文长度为 5

请输入字符串:a
最长回文长度为 1

请输入字符串:aa
最长回文长度为 2

请输入字符串:abax
最长回文长度为 3

算法复杂度分析

文章开头已经提及,Manacher 算法为线性算法,即使最差情况下其时间复杂度亦为 $O(n)$,在进行证明之前,我们还需要更加深入地理解上述算法过程。

根据回文的性质,p[i] 的值基于以下三种情况得出:

(1):j 的回文串有一部分在 id 的之外,如下图:

上图中,黑线为 id 的回文,i 与 j 关于 id 对称,红线为 j 的回文。那么根据代码此时 p[i] = mx - i,即紫线。那么 p[i] 还可以更大么?答案是不可能!见下图:

假设右侧新增的紫色部分是 p[i] 可以增加的部分,那么根据回文的性质,a 等于 d ,也就是说 id 的回文不仅仅是黑线,而是黑线+两条紫线,矛盾,所以假设不成立,故 p[i] = mx - i,不可以再增加一分。

(2):j 回文串全部在 id 的内部,如下图:

根据代码,此时 p[i] = p[j],那么 p[i] 还可以更大么?答案亦是不可能!见下图:

假设右侧新增的红色部分是 p[i] 可以增加的部分,那么根据回文的性质,a 等于 b ,也就是说 j 的回文应该再加上 a 和 b ,矛盾,所以假设不成立,故 p[i] = p[j],也不可以再增加一分。

(3):j 回文串左端正好与 id 的回文串左端重合,见下图:

根据代码,此时 p[i] = p[j]p[i] = mx - i,并且 p[i] 还可以继续增加,所以需要

while (s_new[i - p[i]] == s_new[i + p[i]]) 
    p[i]++;

根据(1)(2)(3),很容易推出 Manacher 算法的最坏情况,即为字符串内全是相同字符的时候。在这里我们重点研究 Manacher() 中的 for 语句,推算发现 for 语句内平均访问每个字符 5 次,即时间复杂度为:$T_{worst}(n)=O(n)$。

同理,我们也很容易知道最佳情况下的时间复杂度,即字符串内字符各不相同的时候。推算得平均访问每个字符 4 次,即时间复杂度为:$T_{best}(n)=O(n)$。

综上,Manacher 算法的时间复杂度为 $O(n)$

查看原文

赞 46 收藏 25 评论 22

_John 赞了文章 · 2019-03-07

掌握Angular2的依赖注入

更好阅读体验,请看原文

在读这篇文章之前,你要先了解一下什么是依赖注入,网上关于这个的解释很多,大家可以自行Google.

article-cover

我们这一篇文章还是以QuickStart项目为基础,从头开始讲解怎么在Angular2中使用依赖注入,如果你按照本篇文章中讲解的示例亲自走一遍的话,你一定能够掌握如何在Angular2中使用依赖注入.好,废话不多说,开始我们今天的旅行吧!

我们首先将项目中的内联模板替换为一个模板文件,使用templateUrl替换template:

@Component({
    selector: 'my-app',
    //template: '<h1>My First Angular2 Travel</h1>',
    templateUrl: 'app/templates/app.html'
})

接下来我们给自己的页面添加一些展示的数据,我们首先新建一个文件app/classes/User.ts,用来创建我们的User实例:

export class User{
    constructor(
        private name:string,
        private age:number,
        private email:string
    ){}
}

然后我们在组件中引入这个类,然后创建我们的显示数据:

import {User} from "./classes/User";
// ...
export class AppComponent {
    users: User[] = [
        new User('dreamapple', 22, '2312674832@qq.com'),
        new User('dreamapplehappy', 18, '2313334832@qq.com')
    ]
}

别忘了在模板中添加一些展示数据使用的html代码:

<h1>依赖注入</h1>
<ul>
    <li *ngFor="let user of users">
        用户的姓名: {{user.name}}; 用户的年龄: {{user.age}}; 用户的邮箱: {{user.email}}
    </li>
</ul>

然后我们就会看到,在页面中显示出来了我们想要的那些数据:
ng2-di-1

Angular2的依赖注入

一般情况下在Web应用中,我们要展示的数据都是从后台服务器动态获取的,所以我们来模拟一下这个过程;我们在这里就要使用服务的依赖注入了,我们首先创建文件user-data.mock.ts,路径是app/mock/user-data.mock.ts;

import {User} from "../classes/User";

export var Users:User[] = [
    new User('dreamapple1', 21, '2451731631@qq.com'),
    new User('dreamapple2', 22, '2451731632@qq.com'),
    new User('dreamapple3', 23, '2451731633@qq.com'),
    new User('dreamapple4', 24, '2451731634@qq.com'),
    new User('dreamapple5', 25, '2451731635@qq.com'),
    new User('dreamapple6', 26, '2451731636@qq.com')
]

我们使用了User类来创建我们的数据,然后把创建的数据导出.

接下来我们要创建一个获取用户数据的服务,我们创建一个新的文件user.service.ts,路径app/services/user.service.ts:

import {Injectable} from '@angular/core';
import {Users} from "../mock/user-data.mock";


@Injectable()
export class UserService {
    getUsers() {
        return Users;
    }
}

大家关于上面的代码部分会有一些疑问,我们来给大家解释一下:首先我们使用了刚才我们创造的模拟数据Users;然后我们从@angular/core中导出了Injectable,就像我们从中导出Component一样;@Injectable()标志着一个类可以被一个注入器实例化;通常来讲,在试图实例化一个没有被标识为@Injectable()的类时候,注入器将会报告错误.上面的解释现在不明白不要紧,我们先学会如何使用;就像你不懂计算机原理一样可以把计算机玩得很溜一样.

我们接下来要在AppComponent组件中使用UserService了,需要注意的地方是:我们要在@Component的元数据中使用providers声明我们所需要的依赖,还要引入User类来帮助我们声明数据的类型.

import {UserService} from "./services/user.service";
import {User} from "./classes/User";
//...
@Component({
    selector: 'my-app',
    //template: '<h1>My First Angular2 Travel</h1>',
    templateUrl: 'app/templates/app.html',
    providers: [
        UserService
    ]
})
export class AppComponent {
    users: User[];

    constructor(private userService: UserService) {
        this.users = userService.getUsers();
    }
}

对上面代码的一些解释:我们使用providers: [UserService]来声明我们这个组件的依赖,如果没有这个选项,我们的程序会报错;然后我们给这个类添加一个属性users,同时声明这个属性的类型是一个含有User类实例的数组;最后我们在构造函数中又声明了一个私有的属性userService,它是UserService服务类的一个实例,我们可以用这个实例来获取users数据.

运行一下,然后我们就会看到下面的页面,表示一切成功.
ng2-di-2

如果这个时候你试图把user.service.ts@Injectable注释掉的话,整个程序是没有报错的,但是我们建议为每个服务类都添加@Injectable(),包括那些没有依赖所以技术上不需要它的.因为:(1)面向未来,没有必要记得在后来添加了一个依赖的时候添加@Injectable().(2)一致性,所有的服务都遵循同样的规则,并且我们不需要考虑为什么少一个装饰器.

这是因为,我们的UserService服务现在还没有什么依赖,如果我们给UserService添加一个依赖的话,如果这时候把@Injectable()注释掉的话,程序就会报错;我们来试试看吧.

很多Web程序都会需要一个日志服务,所以我们来新建一个服务Logger,路径如下:app/services/logger.service.ts:

import {Injectable} from '@angular/core';

@Injectable()
export class Logger{
    logs: string[] = [];

    log(msg) {
        this.logs.push(msg);
        console.warn('From logger class: ' + msg);
    }
}

然后我们在UserService服务中使用这个服务:

import {Injectable} from '@angular/core';
import {Users} from "../mock/user-data.mock";
import {Logger} from "./logger.service";


@Injectable()
export class UserService {
    constructor(private logger: Logger) {

    }

    getUsers() {
        this.logger.log('get users');
        return Users;
    }
}

可以看到,我们把Logger当做UserService服务的一个依赖,因为我们在UserService类的构造函数中声明了一个logger属性,它是Logger类的一个实例;还有别忘了在AppComponent中添加这个Logger依赖:

@Component({
    selector: 'my-app',
    //template: '<h1>My First Angular2 Travel</h1>',
    templateUrl: 'app/templates/app.html',
    providers: [
        Logger, // 添加Logger依赖
        UserService
    ]
})

然后我们可以在页面中看到:
ng2-di-3

如果这个时候,我们注释掉UserService@Injectable()的话,程序就会报错:
ng2-di-4

所以,就像上面所说的;我们还是给每一个服务类添加@Injectable(),以免出现不必要的麻烦.

接下来我们来讨论一下在Angular2中服务的提供商们,如果你对所谓的提供商不理解的话,没关系;可以这样理解,每当我们使用一个服务的时候,Angular2都会通过提供商来创建或者获取我们想要的服务的实例.

我们上面所说的那种提供服务的方式其实是最简单的一种方式,接下来我们讨论注册不同的服务提供商的方法;首先第一种就是我们上面所说的那种了,其实它是一种简写的方式;详细的方式应该是这样的:

[{ provide: Logger, useClass: Logger }]

其中provide作为键值key使用,用于定位依赖,用于注册这个提供商;这个其实就是在后面的程序中使用的服务的名字;useClass表示我们使用哪一个服务类去创建实例,当然我们可以使用不同的服务类,只要这些服务的类满足我们相应的需求就行.

我们可以试着替换Logger类为BetterLogger类,我们首先创建BetterLogger类:

import {Injectable} from '@angular/core';

@Injectable()
export class BetterLogger{
    logs: string[] = [];

    log(msg) {
        this.logs.push(msg);
        console.warn('From better logger class: ' + msg);
    }
}

然后在AppComponent中使用这个BetterLogger类:

@Component({
    selector: 'my-app',
    //template: '<h1>My First Angular2 Travel</h1>',
    templateUrl: 'app/templates/app.html',
    providers: [
        //Logger,
        [{provide: Logger, useClass: BetterLogger}],
        UserService
    ]
})

我们可以看到,控制台的输出是:
ng2-di-5

从中可以看到,我们使用了BetterLogger类替换了Logger类.如果我们的提供商需要一些依赖,我们应该怎么办呢?不用怕,我们可以使用下面这种形式:

[ LoggerHelper,{ provide: Logger, useClass: Logger }]

接下来我们来创建一个LoggerHelper类,它的路径是app/services/logger-helper.service.ts:

import {Injectable} from '@angular/core';

@Injectable()
export class LoggerHelper {
    constructor() {
        console.warn('Just a logger helper!');
    }
}

我们在AppComponent中注册提供商:

@Component({
    selector: 'my-app',
    //template: '<h1>My First Angular2 Travel</h1>',
    templateUrl: 'app/templates/app.html',
    providers: [
        //Logger,
        //[{provide: Logger, useClass: BetterLogger}],
        [LoggerHelper, {provide: Logger, useClass: BetterLogger}], // 带有依赖的注册商
        UserService
    ]
})

然后我们在BetterLogger服务中使用这个依赖:

import {Injectable} from '@angular/core';
import {LoggerHelper} from "./logger-helper.service";

@Injectable()
export class BetterLogger{
    logs: string[] = [];

    constructor(private loggerHelper: LoggerHelper) {
    }

    log(msg) {
        this.logs.push(msg);
        console.warn('From better logger class: ' + msg);
    }
}

然后可以看到我们的控制台的输出结果是:
ng-di-6

说明我们正确的使用了依赖;还有我们可以使用别名来使用相同的提供商,这种方式可以解决一些问题;尤其是当我们想让某个老的组件使用一个新的服务,就好比我们想让AppComponent使用BetterLogger类来打印日志,而不是使用Logger类,假如我们不能够改变AppComponent类,并且我们还想让其他的组件也是用新的BetterLogger类的话,那么我们就可以像下面这样注册这个提供商

[{ provide: BetterLogger, useClass: BetterLogger}],
[{ provide: Logger, useExisting: BetterLogger}]

看到了吗,我们使用useExisting而不是useClass;因为使用useClass或导致我们的应用中出现两个BetterLogger类的实例.我们可以试验一下,在AppComponent中:

@Component({
    selector: 'my-app',
    //template: '<h1>My First Angular2 Travel</h1>',
    templateUrl: 'app/templates/app.html',
    providers: [
        //Logger,
        //[{provide: Logger, useClass: BetterLogger}],
        [LoggerHelper, {provide: BetterLogger, useClass: BetterLogger}],
        [LoggerHelper, {provide: Logger, useExisting: BetterLogger}],
        UserService
    ]
})

然后我们在BetterLogger类的构造函数中添加一个打印语句:

console.warn('BetterLogger Constructor');

我们还要在UserService类的构造函数中声明一个属性betterLogger,它是BetterLogger类的一个实例:

constructor(private logger: Logger, private betterLogger: BetterLogger) {

    }

最后我们可以看到控制台的打印结果是:

Just a logger helper!
BetterLogger Constructor
From better logger class: get users 

但是一旦我们使用了useClass而不是useExisting,那么控制台的打印结果就变成了:

Just a logger helper!
BetterLogger Constructor
BetterLogger Constructor
From better logger class: get users

说明我们创建了两个BetterLogger的实例.所以当我们的多个服务想使用同一个提供商的时候,我们应该使用useExisting,而不是useClass.

[2016-8-20:续写]

值提供商:我们可以使用更简便的方法来注册一个提供商,那就是使用,所谓的值可以是任何一种有效的TypeScript的基本的数据类型.我们来首先使用一个对象吧.首先我们新创建一个文件logger.value.ts,路径是app/values/logger.value.ts;我们写一个基本的loggerValue对象如下:

let loggerValue = {
    logs: ['Hello', 'World'],
    log: (msg) => {
        console.warn('From values: ' + msg);
    },
    hello: () => {
        console.log('Just say hello!');
    }
};

export {loggerValue};

那我们如何注册这个提供商呢?我们使用useValue选项来注册我们这种提供商;如下所示:

// ...
providers: [
        //Logger,
        //[{provide: Logger, useClass: BetterLogger}],
        [LoggerHelper, {provide: BetterLogger, useClass: BetterLogger}],
        //[LoggerHelper, {provide: Logger, useClass: BetterLogger}],
        {provide: Logger, useValue: loggerValue},
        //{provide: Logger, useValue: loggerValue1}, // 我们使用了useValue选项
        UserService
    ]
// ...    

还要记住把loggerValue导入进来;然后我们稍微修改一下user.service.ts的代码:

// ...
getUsers() {
        this.logger.log('get users');
        //noinspection TypeScriptUnresolvedFunction
        this.logger.hello();
        return Users;
    }
// ...

然后我们会看到控制台的输出是:

// ...
From values: get users
Just say hello!
// ...

表明我们这种方式注册提供商成功了. 当然我们也可以使用一个字符串了,这些读者可以自行尝试;或者观看这个示例.

工厂提供商:有时我们需要动态创建这个依赖值,因为它所需要的信息我们直到最后一刻才能确定;我们如何注册一个工厂提供商呢?不着急,我们一步一步来:我们首先来创建一个验证权限的文件,authorize.ts,路径是:app/services/authorize.ts,我们暂且在里面放置一些简单的逻辑,来判定当前用户有没有获取Users的权限:

import {Injectable} from '@angular/core';

@Injectable()
export class Authorize {
    isAuthorized: boolean;
    constructor(){
        this.isAuthorized = Math.random() > 0.5 ? true: false;
    }
    getIsAuthorized() {
        return this.isAuthorized;
    }
}

好吧,我承认这样写有点随意,暂时先这样吧;我们的目的是为了告诉大家如何使用工厂提供商,暂时简化权限验证这一块;从上面的代码我们可以大概了解到,这个服务就是为了获取当前用户的权限情况;然后我们来配置我们的UserService2Provider,为了方便我们暂时直接在app.component.ts中书写我们的配置:

// ...
let UserService2Provider = (logger: Logger, authorize: Authorize) => {
    return new UserService2(logger, authorize.getIsAuthorized());
};
// ...

可以看到,我们的UserService2Provider其实就是一个返回了类的实例的一个函数;我们给这个函数传递了两个参数,分别是LoggerAuthorize类的实例,然后我们根据这两个实例,创建出了我们新的服务实例;奥,忘了告诉大家,我们还要创建一个新的UserService2类,路径:app/services/user-service2.ts:

import {Injectable} from '@angular/core';
import {Users} from "../mock/user-data.mock";
import {Logger} from "./logger.service";


@Injectable()
export class UserService2 {
    isAuthorized: boolean;
    constructor(private logger: Logger, isAuthorized: boolean) {
        this.isAuthorized = isAuthorized;
    }

    getUsers() {
        if(this.isAuthorized){
            this.logger.log('get users');
            return Users;
        }
        else {
            this.logger.log('not isAuthorized!');
            return [];
        }
    }
}

可以看到这个服务类和UserService差不多,就是多了一个条件验证,如果当前用户有获取Users的权限,我们就会返回这些Users;如果没有,我们就返回一个空数组.接下来就是很重要的一步了,我们需要在app.component.tsproviders中使用UserService2Provider:

// ...
providers: [
        //Logger,
        //[{provide: Logger, useClass: BetterLogger}],
        [LoggerHelper, {provide: BetterLogger, useClass: BetterLogger}],
        //[LoggerHelper, {provide: Logger, useClass: BetterLogger}],
        {provide: Logger, useValue: loggerValue},
        //{provide: Logger, useValue: loggerValue1},
        UserService,
        Authorize, // 不可缺少
        {
            provide: UserService2,
            useFactory: UserService2Provider,
            deps: [Logger, Authorize]
        }
    ]
// ...    

还要记住,要添加Authorize依赖;我们在app.component.ts中使用新的服务:

// ...
export class AppComponent {
    users: User[];

    constructor(private userService: UserService, private userService2: UserService2) {
        this.users = userService.getUsers();

        console.log(this.userService2.getUsers());
    }
}

刷新浏览器,你会看到有时它会输出:

From values: not isAuthorized!
[]

有时它会输出:

From values: get users
[User, User, User, User, User, User]

那么说明,我们这种方式注册工厂提供商的方式也成功了.

也许大家会有一些疑问,我们在类的构造函数中使用private userService2: UserService2怎么就获取了这个服务的一个示例,Angular2是怎么知道我们要的是UserService2类,它又是如何获取这个类的实例的呢?在Angular2中我们通过注射器来进行依赖注入,其实上面的形式只是一种简写;详细一点的写法是这样的:

import {Component, Injector} from '@angular/core';

我们先从Ng2中获取Injector注射器,然后使用这个注射器来进行我们所需服务的实例化:

// ...
export class AppComponent {
    users: User[];
    private userService2: UserService2;
    
    //constructor(private userService: UserService, private userService2: UserService2) {
    //    this.users = userService.getUsers();
    //
    //    console.log(this.userService2.getUsers());
    //}

    constructor(private userService: UserService, private injector: Injector) {
        this.users = userService.getUsers();
        this.userService2 = this.injector.get(UserService2);
        console.log(this.userService2.getUsers());
    }
}

所以可以看出来,这些繁琐的活我们全部都让injector去做了,只需要我们提供一些简单的说明,聪明的Ng2就知道如何进行依赖注入.

非类依赖

我们上面的讲解全部都是把一个类作为一个依赖来进行服务的依赖注入,但是假如我们想要的不是一个类,而是一些值,或者对象;我们应该怎么办?我们先来写出这么一个文件app-config.ts,路径是:app/config/app-config.ts:

export interface AppConfig {
    title: string,
    apiEndPoint: string
}

export const AppConf: AppConfig = {
    title: 'Dreamapple',
    apiEndPoint: 'https://hacking-with-angular.github.io/'
};

按照上面的使用的方法,我们应该可以这样做:

// ...
{provide: AppConfig, useValue: AppConf}
// ...
constructor(private userService: UserService, private userService2: UserService2, private appConf: AppConfig) {
        this.users = userService.getUsers();

        console.log(this.userService2.getUsers());

        console.log(this.appConf);
    }
// ...    

但是我们这样做却没有达到我们想要的效果;控制台报错:

Error: ReferenceError: AppConfig is not defined(…)

因为接口interface不能够被当做一个类class来处理,所以我们需要换一种新的方式,使用OpaqueToken(不透明的令牌):

// 首先导入 OpaqueToken和Inject
import {Component, Injector, OpaqueToken, Inject} from '@angular/core';
// 引入AppConf,并且使用OpaqueToken
import {AppConf} from "./config/app-config";
let APP_CONFIG = new OpaqueToken('./config/app-config');
// 在providers中进行配置
{provide: APP_CONFIG, useValue: AppConf}
// 在类中使用
constructor(private userService: UserService, private userService2: UserService2, @Inject(APP_CONFIG) appConf: AppConfig) {
        this.users = userService.getUsers();

        console.log(this.userService2.getUsers());

        console.log(appConf);
    }

对上面的代码的一些解释首先我们使用OpaqueTokean对象注册依赖的提供商,然后我们在@Inject的帮助下,我们把这个配置对象注入到需要它的构造函数中,最后我们就可以使用最初的那个对象了.虽然AppConfig接口在依赖注入过程中没有任何作用,但它为该类中的配置对象提供了强类型信息.

最后我们来讲解一下可选依赖(Optional),有些服务的依赖也许不是必须的;我们就可以使用@Optional()来对这些参数做标记;记住,当使用@Optional()时,如果我们想标记logger这个服务是可选的;那么如果我们不在组件或父级组件中注册一个logger的话,注入器会设置该logger的值为空null;我们的代码必须要为一个空值做准备.来看一下例子吧,我们在user.service.ts中做一些改动:

// 导入 Optonal
import {Injectable, Optional} from '@angular/core';
import {Users} from "../mock/user-data.mock";
import {Logger} from "./logger.service";
import {BetterLogger} from "./better-logger.service";


@Injectable()
export class UserService {
    constructor(private logger: Logger,
                // 使用@Optional标记
                @Optional()private betterLogger: BetterLogger) {
    }

    getUsers() {
        this.logger.log('get users');
        //noinspection TypeScriptUnresolvedFunction
        this.logger.hello();

        // 存在betterLogger时的处理
        if(this.betterLogger) {
            this.betterLogger.log('optional');
        }

        //console.log(this.logger);
        return Users;
    }
}

至此,整篇文章已经结束了;如果你坚持读到了这里,那说明你也是一个很有耐心的人;如果你有什么问题可以在这里提出.

查看原文

赞 28 收藏 58 评论 11

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-11-20
个人主页被 1.4k 人浏览