作为一个喜欢折腾的人,个人搞了很多东西放在自己的服务器上,但是为了方便,能够在世界各地随时随地的打开查看和使用,我将服务器暴露到了公网中,当然了有些在公有云上的本来就暴露出来的。

那么这里就有一个问题,我如何保护我的信息只能我来查看呢?

  • 最简单的方法就是通过 HTTP Basic Auth + HTTPS。记住一定要上 https,否则你的密码也是会泄漏的。为什么说简单呢?因为只需要在 Nginx 或 Traefik 上配置下就可以了。但是这个方案有一个非常麻烦的问题,就是过一段时间之后就要输入用户名和密码。时间短了,到无所谓,时间一长就会觉得很烦。

  • 构建一套 token 验证体系,不管是使用 oauth 也好还是 jwt 也好,都是可以的。安全性也是可以保证的,而且设置好 token 的时间长度,也能保证避免频繁的输入密码。但是这有一个问题就是实现起来太过于复杂,都快赶上公司的一套系统了。而且还要有各种登录页面,想想都烦。

  • 与上面类似,不过验证方式使用 Two Auth,也就是基于时间的 6 位数组。但是依旧比较复杂。

  • 使用 OpenVPN 的方式。这在一定程度上也能使用,但是对于我来说,OpenVPN 的限制还是比较大的。首先安卓手机无法开启两个 VPN,而且我也不能一直连着 VPN,因为我会部署一些经常用的服务。而且我不是为了能够连接到内网,而是想对外网使用的服务添加验证。

我想了许久,有没有一种不需要输入密码,就可以验证安全的呢?因为是我一个人使用的,所以我根本不需要多用户系统,也就是说验证方式只需要一个密码就可以了。这我突然想起了之前在写 gRPC 的时候有一个双向验证的参数,也可以验证客户端可以不可以。当时觉得只是他们基于 h2 改的协议,结果我一查发现这原来就包含在 https 里面,准确说是 SSL 规范里面。(怪自己当初上计算机网络的时候没好好学这部分,竟然连这个都不知道)

那么至此,思路就很清晰了,给我的所有个人服务都添加 https 客户端校验。只要我的证书够安全,我的页面就是安全的(反正都是我个人的东西,直接拿着 U 盘到处拷贝,手机 Pad 用数据线发送,我就不信这样谁还能盗走我的证书,傲娇脸)

关于 SSL 证书的一些知识

  • 生成证书我们主要采用 openssl 具体的安装教程我就不讲解了,有兴趣的小伙伴自行查阅,主要有下面几个步骤:

    • openssl genrsa:生成 Private Key,用于生成请求文件使用,这里用 .key 后缀。

    • openssl req:依赖上面生成的 Key 去生成 CSR,也就是证书请求文件。使用 .csr 后缀。这期间要填写一些信息,前面的几个大写字母是缩写,后面在命令行使用的时候会用到。

      • C(Country) 国家

      • ST(State/Province) 州或者省

      • L(Locality) 地区,国内写区即可

      • O(Organization) 组织

      • OU(Organization) 组织单位

      • CN(Common Name) 通用名,这个是非常重要的,影响了证书的显示名称和 HTTPS 的域名。

    • openssl x509:根据 x509 规范,利用 CA 的证书和私钥将 CSR 文件加密成真正可以使用到的证书。使用 .crt 后缀

  • SSL 证书必须要采用 sha-2 加密算法。2015 年 12 月 31 日前,CA 机构还会颁发 SHA-1 签名的证书,但是之后只会签发 SHA-2 签名的证书了。Chrome 也会对 SHA-1 签名的证书提示不安全。在 openssl 中用的是 -sha-256 参数。

  • CRTPEM 的关系,大家可以简单的认为 PEM 是将证书 base64 之后的文件,而 CRT 是既能 base64 也能 binary 的一种文件格式。但是通常 openssl 产出的是 base64 的文件,你可以通过 -outform 参数控制产出的类型。

CA 的生成

有了 CA 我们才能去给其他的证书签名,生成 CA 的过程很简单

创建根钥

💡 这个秘钥非常重要,任何获得了这个秘钥的人在知道密码的情况下都可以生成证书。所以请小心保存

1
openssl genrsa -des3 -out root.key 4096
  • -des3 标明了私钥的加密方式,也就是带有密码。建议添加密码保护,这样即使私钥被窃取了,依旧无法对其他证书签名。你也可以更换其他的加密方式,具体的请自行 help。

  • 4096 表示秘钥的长度。

创建自签名证书

因为是 CA 证书,所以没法让别人去签名,只能自签名。这里可以认为是生成 CSR 和签名两部合成一步走。

1
openssl req -x509 -sha256 -new -key root.key -sha256 -days 1024 -out root.crt

服务端证书生成

生成证书私钥

1
openssl genrsa -out your-domain.com.key 2048

和 CA 证书不同,这个私钥一般不需要加密,长度也可以短一些。

生成证书请求文件

1
openssl req -new -key your-domain.com.key -out your-domain.com.csr

这期间要填入一些信息,注意 CN 的名字一定要是你的域名。

使用 CA 对 CSR 签名

在 Chrome 58 之前,Chrome 会根据 CN 来检查访问的域名是不是和证书的域名一致,但是在 Chrome 58 之后,改为使用 SAN(Subject Alternative Name) 而不是 CN 检查域名的一致性。

而 SAN 属于 x509 扩展里面的内容,所以我们需要通过 -extfile 参数来指定存放扩展内容的文件。

所以我们需要额外创建一个 your-domain.com.ext 文件用来保存 SAN 信息,通过指定多个 DNS 从而可以实现多域名证书。

1
2
3
4
5
6
subjectAltName = @alt_names

[alt_names]
DNS.1 = your-domain.com
DNS.2 = *.your-domain.com
DNS.3 = *.api.your-domain.com

以此类推,如果域名较少,还可以用另外一种简写方案。

1
subjectAltName = DNS: your-domain.com, DNS: *.your-domain.com

关于语法的更多内容请查看官方文档。在有了 ext 文件之后就直接可以开始签名了。

1
openssl x509 -req -sha256 -in your-domain.com.csr -CA root.crt -CAkey root.key -CAcreateserial -out your-domain.com.crt -days 365 -extfile your-domain.com.ext

CAcreateserial 这个参数是比较有意思的,意思是如果证书没有 serial number 就创建一个,因为我们是签名,所以肯定会创建一个。序列号在这里的作用就是唯一标识一个证书,当有两个证书的时候,只有给这两个证书签名的 CA 和序列号都一样的情况下,我们才认为这两个证书是一致的。除了自定生成,还可以通过 -set_serial 手动指定一个序列号。

当使用 -CAcreateserial 参数之后,会自动创建一个和 CA 文件名相同的,但是后缀是 .srl 的文件。这里存储了上一次生成的序列号,每次调用的时候都会读取并 +1 。也就是说每一次生成的证书的序列号都比上一次的加了一。

现在,只需要将 your-domain.com.crtyour-domain.com.key 放到服务端就可以使用了。别忘了将 CA 添加系统当中,要不然浏览器访问会出现问题。

客户端证书生成

服务端有了之后,就需要生成客户端的证书,步骤和服务端基本一致,但是不需要 SAN 信息了。

1
2
3
4
5
6
7
openssl genrsa -out client.key 2048
# 这里也可以采用非交互形式,方便制作成命令行工具
openssl req -new \
-key client.key \
-subj "/C=CN/ST=Zhejiang/O=X/CN=*.your-domain.com" \ # 这里的缩写就是文章一开始所说的那些缩写
-out client.csr
openssl x509 -req -in client.csr -CA root.crt -CAkey root.key -out client.crt -days 365

只不过客户端验证需要的是 PKCS#12 格式,这种格式是将证书和私钥打包在了一起。因为系统需要知道一个证书的私钥和公钥,而证书只包含公钥和签名,不包含私钥,所以需要这种格式的温江将私钥和公钥都包含进来。

1
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12

这期间会提示你输入密码,用于安装的时候使用。也就是说不是什么人都可以安装客户端证书的,要有密码才行,这无疑又增加了一定的安全性。当然了,我试过不输入密码,但是好像有点问题,有兴趣的同学可以自己尝试下。

客户端校验证书的使用

这里以 Node.js 举例。使用 https 模块,在创建的时候和普通的创建方式基本一致,但是需要额外指定 requestCertca 参数来开启客户端校验。

1
2
3
4
5
6
7
8
https.createServer({
key: fs.readFileSync('your-domain.com.key'),
cert: fs.readFileSync('your-domain.com.crt'),
requestCert: true,
ca: [fs.readFileSync('root.crt')], // 校验客户端证书的 CA
}, (req, resp) => {
// blahblah
})

这样只要客户端没有安装合法的证书,那么整个请求就是失败的。而且根本不会进入请求处理的回调函数中,这也意味着显示的错误是浏览器的默认错误。那么这对用户来讲其实不太友好。

那么我们可以通过在参数中添加 rejectUnauthorized: false 来关闭这个功能,也就是说不管客户端证书校验是正确还是失败,都可以进入正常的回调流程。此时我们只需要通过 req.client.authorized 来判断这个请求是否通过了客户端证书的校验,可以给予用户更详尽的错误提示。

另外我们还可以通过 resp.connection.getPeerCertificate() 获取客户端证书的信息,甚至可以根据不同的信息选择给予不同的用户权限。

这里有一个 DEMO: https://www.xgheaven.net.cn:3443,大家打开之后应该会看到一个证书校验失败的提示。这里要说下,我这里的 DEMO 没有使用自签名的服务端证书,只是使用了自签名的 CA 去检查客户端证书。因为用自己签名的服务端证书的话,浏览器会提示不安全,因为用户么有安装自签名的 CA。

可以点击下载客户端证书按钮,安装客户端证书。因为客户端证书是有密码保护的,请输入页面上提示的密码。

再次刷新,如果是 Mac 系统,会提示你要使用哪个客户端证书登录,此时就说明安装成功了。

点击确认,可能还要输入一个系统密码允许 Chrome 访问 Keychain,一劳永逸的话在输入密码之后选择 Always Allow,从此就不需要再输入密码了。

按照道理,你就可以看到这个页面了。

结语

有了这个功能,我就可以将我的所有内容全盘私有化而且还能直接暴露在公网中。配合之前毕设搞的微服务化,简直不要美滋滋。如果之前是使用账号密码登录的,也可以接入这个方案。就是将登录页面替换成证书校验就可以了。

Refs

注释和共享

当你看到这个标题的时候,一定很好奇,React 不是很快么?为啥会变慢呢?在写这篇文章之前,我也是这么认为的,但是当我去看了一下 React 有关 Array 的 Diff 之后,我才认识到其实 React 如果你用的不正确,那么是会变慢的。

React Diff 算法

React Diff 算法相信大家不是很陌生吧,这里就不具体展开讲了。不过有一点要补充下,Diff 算法针对的是整个 React 组件树,而不仅仅是 DOM 树,虽然这样性能会比较低一些,但是实现起来却很方便。

而在 Diff 算法中,针对数组的 diff 其实是比较有意思的一个地方。在开始讲解方面,我希望你能对 React 有一定的了解和使用。

试一试有什么区别?

首先我们创建 3 个组件,分别渲染 10000 个 DOM 元素,从 [1...10000] ,渲染成如下。

1
2
const e10000 = new Array(10000).fill(0).map((_, i) => i + 1)
element10000.map(i => <div key={`${i}`}>{i}</div>)

每个组件有两个状态,会切换数据的顺序

  • 组件 A 在 [1...10000][2,1,3...10000] 之间切换。

  • 组件 B 在 [1...10000][10000,1...9999] 之间切换

  • 组件 C 在 [1...10000][10000...1] 之间切换,也就是正序和倒序之间切换。

我们简单命名下,默认的初始状态为 S1 而切换之后的状态为 S2 。大家可以思考一下,同一个组件状态切换的时候,所耗费的时间是不是都是一样的?可以直接使用这个 DEMO

可以直接点击上方的 toggle 来切换两者之间的状态,并在控制台中查看渲染的时间。因为每次时间都不是绝对准确的,所以取了多次平均值,直接揭晓答案:

组件 S2 ⇒ S1 S1 ⇒ S2
A 102ms 103ms
B 129ms 546ms
C 556ms 585ms

有么有觉得很奇怪,为什么同样是 S1 ⇒ S2 ,同样是只改变了一个元素的位置,为什么 A 和 B 的时间差距有这么多的差距。这个具体原理就要从 Diff 算法开始讲起了。

Array Diff 的原理

在讲 React 的实现之前,我们先来抛开 React 的实现独立思考一下。但是如果直接从 React 的组件角度下手会比较麻烦,首先简化一下问题。

存在两个数组 A 和 B,数组中每一个值必须要保证在对应数组内是唯一的,类型可以是字符串或者数字。那么这个问题就转变成了如何从数组 A 通过最少的变换步骤到数组 B。

其实每个元素的值对应的就是 React 当中的 key。如果一个元素没有 key 的话,index 就是那个元素默认的 key。为什么要强调最少?因为我们希望的是能够用最少的步数完成,但是实际上这会造成计算量的加大,而 React 的实现并没有计算出最优解,而是一个较快解。

顺便定义一下操作的类型有:删除元素插入元素移动元素

这里又要引申一个特殊点,React 充分利用了 DOM 的特性,在 DOM 操作中,你是可以不使用 index 来索引数据的。简单来讲,如果用数组表示,删除需要指定删除元素的索引,插入需要指定插入的位置,而移动元素需要指定从哪个索引移动到另一个索引。而利用 DOM,我们就可以简化这些操作,可以直接删除某个元素的实例,在某个元素前插入或者移动到这里(利用 insertBefore API,如果是要在添加或者移动到最后,可以利用 append )。这样最大的好处是我们不需要记录下移动到的位置,只需要记录下那些元素移动了即可,而且这部分操作正好可以由 Fiber 来承担。

举个例子说,从 A=[1,2,3] 变化到 B=[2,3,4,1],那么只需要记录如下操作即可:

有人好奇,不需要记录移动插入到那个元素前面么?其实不需要的,这是因为你有了操作列表和 B 数组之后,就可以知道目标元素在哪里了。而且采用这种方式就根本不需要关心每次操作之后索引的变化。

回到上面的简化后的问题,首先通过对比 A、B 数组,可以得到哪些元素是删除的,哪些元素是添加的,而不管采用什么样子的策略,添加删除元素的操作次数是无法减少的。因为你不能凭空产生或者消失一个元素。那么我们问题就可以再简化一下,把所有的添加删除的元素剔除后分别得到数组 A’ 和 B’,也就是 A’ 中不包含被删除的元素,B’ 中不包含被添加的元素,此时 A’ 和 B’ 的长度一定是一样长的。也就是求解出最少移动次数使得数组 A’ 能够转化成数组 B’。

如果只是简单的求解一下最少移动步数的话,答案很简单,就是最长上升子序列(LIS,Longest Increasing Subsequence)。关于如何证明为什么是最长不下降子序列这个算法,可以通过简单的反证法得到。关于这个算法的内容我就不具体讲解了,有兴趣的可以自行 Google。在这里我们只需要知道这个算法的时间复杂度是 O(n^2)

但是现在我们还无法直接应用这个算法,因为每个元素的类型可能是字符串或者数字,无法比较大小。定义数组 T 为 B’ 内元素在 A’ 的位置。举个例子,如果 A' = ['a', 'b', 'c'] B' = ['b', 'c', 'a'],那么 T = [2, 3, 1]。本文约定位置是从 1 开始,索引从 0 开始。

此时便可以对 T 求解 LIS,可以得到 [2, 3],我们将剩下不在 LIS 中的元素标记为移动元素,在这里就是 1,最后补上被剔除的删除和插入的元素的操作动作。这样 Diff 算法就可以结束了。

上面讲解的是一个个人认为完整的 Array Diff 算法,但是还是可以在保证正确性上继续优化。但是不管优化,这个复杂度对于 React 来讲还是偏高的,而如何平衡效率和最优解成为了最头疼的问题,好在 React 采用了一个混合算法,在牺牲掉一定正确性的前提下,将复杂度降低为 O(n)。下面我们来讲解下。

React 简化之后的 Array Diff

大家有过 React 开发经验的人很清楚,大部分情况下,我们通常是这样使用的:

  • 情形1:一个标签的的直接子子标签数量类型顺序不变,通常用于静态内容或者对子组件的更新

    1
    2
    3
    4
    5
    6
    7
    // 比如每次渲染都是这样的,里面的直接子元素的类型和数量是不变的,在这种情况下,其实是可以省略 key
    <div>
    <div key="header">header</div>
    <div key="content">content</div>
    <div key="footer">footer</div>
    <SubCmp time={Date.now()}/>
    </div>
  • 情形2:一个标签有多个子标签,但是一般只改变其中的少数几个子标签。最常见的场景就是规则编辑器,每次只在最后添加新规则,或者删除其中某个规则。当然了,滚动加载也算是这种。

  • 情形3:交换某几个子标签之间的顺序

  • 情形4:翻页操作,几乎重置了整个子元素

上面只是简单举了几个常见的例子,大家可以发现,大部分情况下子标签变动的其实并不多,React 利用了这个,所以将 LIS 简化成以第一个元素开始,找到最近上升子序列。简单来来讲就是从头开始遍历,只要这个元素不小于前的元素,那么就加入队列。

1
2
3
4
5
Q = [4, 1, 5, 2, 3]
// 标准算法
LIS = [1, 2, 3]
// 简化后的算法,从第一个开始,找到最近的不下降子序列即可。
LIS_React = [4, 5]

我们乍一看,这个算法不对呀,随便就能举出一个例子让这个算法错成狗,但是我们要结合实际情况来看。如果我们套回前面说的几种情况,可以看到对于情况 1,2,3 来讲,几乎和简化前效果是一样。而这样做之后,时间复杂度降低为 O(n) ,空间复杂度降低为 O(1)。我们给简化后的算法叫做 LIS' 方便后面区分。

我们将 LIS 算法简化后,配合上之前一样的流程就可以得出 React 的 Array Diff 算法的核心流程了。(为什么叫核心流程,因为还有很多优化的地方没有讲)

变慢的原因?

当我们在了解了 React 的实现之后,我们再回过来头来看看前面给出的三个例子为啥会有这么大的时间差距?

  • 组件 A 从 [1...10000] 变化到 [2,1,3...10000] 。此时我们先求解一下 LIS' 可以得到 [2,3,4...10000],那么我们只需要移动 1 这个元素就可以了,将移动到元素 3 前面。同理反过来也是如此,也就是说 S1 ⇒ S2 和 S2 ⇒ S1 的所需要移动的次数是一致的,理论上时间上也就是相同的。

  • 组件 B 从 [1...10000] 变化到 [10000,1,2...9999] 。同理,先计算 LIS' 可以得到 [10000],没错,你没看错,就是只有一次元素,那么我需要将剩下的所有元素全都移动到 10000 的后面去,换句话要进行 9999 次移动。这也就是为啥 S1 => S2 的时间会这么慢。但是反过来却不需要这个样子,将状态反过来,并重新计算索引,那么也就是从 [1...10000][2,3....10000,1],在计算一次 LIS' 得到 [2,3...10000] ,此时只需要移动一次即可,S2 ⇒ S1 的时间也就自然恢复的和组件 A 一致。

  • 组件 C 是完全倒序操作,所以只分析其中一个过程即可。首先计算 LIS' 可以得到,[10000] ,也就是说要移动 9999 次,反过来也是要 9999 次,所以时间状态是一致的。

经过这样的分析大家是不是就明白为啥会变慢了吧?

优化细节

降低 Map 的生成操作次数

上面有一点没有讲到,不知道大家有没有思考到,我怎么知道某个元素是该添加函数删除呢?大家第一反应就是构建一个 Set,将数组元素全放进去,然后进行判断就可以了。但是在 React 中,其实用的是 Map,因为要存储对应的 Fiber,具体细节大家可以不用关注,只需要知道这里用 Map 实现了这个功能。

不管怎么样,根据算法,一开始肯定要构建一遍 Map,但是我们来看下上面的 情形1。发现内容是根本不会发生变化的,而且对于 情形2 来讲,有很大的概率前面的大部分是相同的。

于是 React 一开始不构建 Map,而是假设前面的内容都是一致的,对这些元素直接执行普通的更新 Fiber 操作,直到碰到第一个 key 不相同的元素才开始构建 Map 走正常的 Diff 流程。按照这个方式,情形1根本不会创建 Map,而且对于情形2、3来讲也会减少很多 Map 元素的操作(set、get、has)。

降低循环次数

按照上面的算法,我们需要至少 3 遍循环:第一遍构建 Map,第二遍剔除添加删除的元素生成 A’ 和 B’,第三遍计算 LIS 并得到哪些元素需要移动或者删除。而我们发现第二遍和第三遍是可以合并在一起的。也即是说我们在有了 Map 的情况下,不需要剔除元素,当遍历发现这个元素是新增的时候,直接记录下来。

总结

关于 Diff 算法其实还有很多的细节,我这边没有过多讲解,因为比较简单,比较符合直觉。大家有兴趣的可以自己去看下。另外有人应该会注意到,上面的例子中,为什么切换同样的次数,有的时间长,有的时间短了。日后有时间再分析下补充了。

注释和共享

在平时的开发中,我发现大家特别喜欢将很多自己常用或者公司常用的脚本封装成一个 cli,这虽然无可厚非,但是作为一个有强迫症的患者来说,我认为这其实做不好会导致 cli 非常不好用的。所以这里总结了下平时写 cli 的经验希望和大家分享下。

写之前请思考

  1. 我是不是为了 cli 而去写 cli,换句话我觉得这样比较 cool。如果是,那么请放弃这种想法。
  2. 这个 cli 一定会减轻工作量么?
    • 有很多公司喜欢将各种 webpack/eslint/babel 工具封装成一个 cli,那么这真的会降低使用者的工作量么?如果你封装的不好,相反会增加很大的工作量。最神奇的是,封装了这么多工具,配置文件却一个都没少,babelrc/eslintrc/prettierrc,那封装了有何用。
    • cli 的不透明性就会导致使用者永远都是有使用成本的,不论这个 cli 有多简单。他不会知道你是干了什么。所以能避免写一个 cli 就避免写一个 cli。那如果真的要写,有其他方案么?请看第三条
  3. 除了写 node cli 真的没有其他方案了么?
    • 大部分情况下,写 shell 的效率远远高于写 cli,将一个命令拆分成多个 shell,然后配合简单的帮助文档即可。如果用户不知道是做什么的,那么直接就可以去看源码。
    • 而且使用 git 管理 shell 也同样高效,比如将个人的一些脚本放到 private github 上,然后直接 clone 下来就可以用了。这样不需要每次都 npm publish,进而污染 npm。
    • 大部分情况下,去写 Makefile/Rakefile 同样可行。当然,Node 生态也有 Jake,不过不推荐用,因为要装 jake 包。
    • 如果你这个 cli 是作为一个项目的脚手架工具,那么是不是用 yeoman 或者 degit 这类工具更好?除非你的项目非常热门,功能自定义程度高,否则完全不需要自己去写一个脚手架。如果你只是觉得好玩,想写一个脚手架的话,那么请去看第一条,问问自己。
    • 如果是一个团队使用,那么除非有很大的普世性,那么用 git 管理同样比 npm 管理要强的多。
  4. 最后如果决定一定要写 cli 的话,有必要发布到 npm 么?
    • 是不是发布到自己的 scope 下也是一个不错的选择?
    • 是不是直接让别人通过 npm install [github.com/user/package](http://github.com/user/package) 也是一个不错的选择?
    • 是不是上传到公司 or 个人的私有 npm 更好?

开发时请遵循以下几点

  1. 请使用 npm
  2. 不要写死版本号,优先使用 ^
    • 这是因为有可能你的 cli 会被直接当做依赖安装到项目中,如果你写死了版本号,那么可能会装多分依赖。比如项目依赖了 yargs@^13.2.0 ,但是你锁死了 `yargs@13.1.0`,就会导致安装两个 yargs。
  3. 避免引入一些功能很大,而自己只用其中一部分的包
    • 因为没有 webpack 工具,无法进行 tree shaking,所以导致安装了很多比较大的包。比如你只是用了 lodashcloneDepp 却安装了整个包。优先使用 [lodash.xxx](http://lodash.xxx) 子包。
  4. 如果你使用了某些构建工具(babel,webpack,typescript,flow),那么请将构建之后的文件也加入代码仓库。
    • 比如有一个 src 目录存放了 ts 源文件,而 dist 存放了构建之后的 js 文件。那么很多人的选择往往是将这 dist 文件夹加入 gitignore。这样其实是不太好的,原因如下:
    • 第一,方便追踪变化,比如你自己添加了一些 debug 代码,这个时候构建之后发现没有问题。又把 debug 代码删除,当你提交的时候就可以很清楚的看到自己修改了之后没有构建代码
    • 第二,方便通过 unpkg.com 等工具访问
    • 第三,在版本开发依赖升级之后,可以很方便的看到改变的内容。一般使用者会放心升级 cli 之类的开发工具,所以这部分的质量需要我们自己来保证。

交互设计上请遵循以下几点

  1. Throw as possible,将可能的所有报错向上抛出,是一个好系统所必备的能力。但是在错误展示的时候,可以向用户隐藏部分调用栈信息等,只保留关键信息即可。
  2. 尽可能遵循 linux 下的 cli 规范。
  3. 不要让用户产生无畏的等待,通过添加进度条或者输出完成列表等告诉用户你依旧是在工作中的。
  4. 给予用户想查看所有日志的能力。可以通过 -v -vv -vvv 或者 --log-level debug 来控制显示级别
  5. 对所有命令的描述都不要超过一行。不论屏幕宽度如何(一般默认 80),最好不要超过 70。如果需要大量描述,请尝试通过 man 或者单独的页面。
  6. 帮助是最好的文档,写好 cli 的 help 远比去写好一个文档网站要关键

注释和共享

思来想去,我决定还是要写一篇文章分享一下我在网易的经历和生活,我对网易的观点或者想法以及评论也许并不是客观公正的,我只是想从一个校招生的角度来讲述。

2018 年 7 月 01 日,我从杭电毕业,进入了网易,从事大数据前端管理页面开发;
2019 年 3 月 15 日,我主动离开了网易,一共 257 天、6168 小时、370080 分、22204800 秒。

心路历程

进入网易后,我的心态也一点一点的发生着变化,这其中有一些是因为自己的原因,也有一部分是网易的原因,如果你愿意,我愿和你慢慢阐述

意外 —— 校招季拿到了网易 Offer

我原本没想给网易投递 Offer,那个时候我心里只有阿里,至于原因么,有句话说的好,『所有的 Node 大佬不是在阿里,就去在去阿里的路上』,虽然我不是大佬,但是我也有一颗想成为技术大佬的心。那个时候我只投递了阿里的 Offer。

可是事与愿违,18 届的校招可谓说是及其严格,基本都是社招要求,而且听说就招了 300 人(只是听说)。投了两次阿里云,都以失败告终(毕竟我的朴神来阿里云)。这个时候正好有同学在网易实习,我就看了下,有前端岗位,那就去下试试吧。

说实话,那个时候对网易的前端、Node 没有抱有任何希望。网易在前端的开源社区几乎没有任何动静,也几乎从来没参加过任何技术论坛,也从来没听说他们有用任何前端框架,可以说对网易前端没有任何的概念。

面试的时候我也就啥都没准备,纯裸考,只是觉得这公司进不进无所谓吧,甚至 HR 面的时候,稍微顶撞了一下 HR,HR 问了一个问题,我回答了,结果他说我回答的不是他问的问题,我思考了一下,坚定的回了他一句『我回答的就是你的问题,那您不是想问这个?』。结果意外的,我竟然进了,我当时自己都蒙了。直到入职之后才回过神来,原来是他们太缺人了。。。

但是毕竟校招,怎么样还是要试一下么,就安慰自己说可能他们内部用的技术挺好的,只不过按照丁磊的作风,可能不喜欢招摇吧。但后来结果实力打脸,事与愿违~

说实话,当时也找过其他的公司,可是杭州这边,真正既有技术实力、有大佬助阵、面向开源的公司几乎少的可怜,除了阿里几乎找不到几个,于是当时在好朋友的推荐下,投了有赞的前端、七牛的后端。

七牛的后端笔试直接跪了,有赞的拿到了 offer,但是当时脑抽觉得有赞技术不好(另外是我和室友都拿到了有赞的 offer,但是前端的老大跟我的室友说带着他一起搞波大事情,和我却啥都没说,就加了个微信,我觉得受到了某种歧视),而且觉得有赞的平台有点小,于是给推了。

后来真正进入到网易之后,觉得网易还不如有赞呢。或许大家认识有赞是因为 996,都认为有赞 996 不好,可是有赞和普通的互联网企业一样,并没有 996,相反比其他公司还有活力,有各种兴趣组织,甚至还有官方的游戏比赛,Dota/王者,比赛还有专人解说呢。而且钱多福利好。而且杭州除了阿里能数上的有赞也是接入企业滴滴。

反正说了这么多,最终我算是说服自己,现在网易学习 1 年,然后去我喜欢的公司。

期待 —— 嗨,猪行动

期待着进入网易的生活会是什么样子;期待着会不会遇见很多大佬;期待着网易的食堂有多么好吃;期待着能不能和同事很好的相处;期待能不能用到自己喜欢的技术;期待着别人能不能承认自己的能力。

一切的期待,都在拿到校招 Offer 的那一刻开始。

这期间参加了 『嗨,猪行动』,第一次以校招的身份进入网易,我们在 HR 小哥哥小姐姐们的安排下,一起参加了一些有趣有意思的活动。大家分成各个小队,以小队的身份参加比赛,每完成一个比赛,都会有一定的积分奖励,在活动结束之后,可以去换取很多严选的礼物,比如有行李箱、抱枕、牙刷、拖鞋、杯子、笔等等。

我觉得这场活动最成功的便是在最后,来了一场上百人吃鸡游戏,在园区散落着很多盒子,每个盒子里面武器或者,然后找到其他小队成员消灭之。虽然我们的枪只是简单的水枪,子弹也是可消除墨水,命只是一个简单的丝带绑在头上。而且我不得不承认这个游戏 Bug 太多,不过不得不说真的是很好玩的。

很快,虽然我很轻松的活到了前 10 名的样子,但是我是用某些不正常的方法(在某个门后躺着,假装自己已经死掉了,悠闲的等着;而且我们在这个游戏开始前就已经偷偷攒了好多武器和命,就是在玩吃鸡之前的游戏的时候),感觉不是太很公平,于是我就主动退出了。

静静的看着他们战斗,很快场上就只剩下 2 名妹子,一枚来自其他的组,还有一枚便是我们组的。于是最精彩的决赛来了,我们给他们腾出了场地,每人补足武器。

piupiupiu,piupiupiu,真的是太精彩了,我无法描述,但是结果有点遗憾,我们组的妹子惜败第二名。

一天如此开心,感谢 HR 小哥哥小姐姐们的辛勤付出,也感谢队友的努力奋斗,或许在此刻,我对网易的印象有了那么一些改变,感觉让人期待网易的生活会是怎么样子。

满足 —— Mini 项目 『来人吖』

2018 年 7 月 1 日,这一天终于到来了。我真正的不如了网易的大门,发现一切都是如想象的那么美好。

免费的四餐(包含夜宵喽),除了人太多以外味道还是很不错的;各种严选考拉的内购。有各种各样的折扣,而且有些还会直接在食堂门口摆摊。免费的班车简直不要太舒服,就是要每天 7 点半起床赶班车,真的是难受。园区免费停车、新能源免费充电,虽然这个时候我还没有买车,不过我已经在考虑中了。

入职之后,参加了 2 天的素质拓展,终于圆梦了我一直想去尝试的高空断桥,刺激、爽,还想再来一次。哈哈哈。

很快,最令我欣喜激动的事情来了,『Mini 项目』,简单点来讲,就是网易花了 100w 做了 8 个没啥用的产品。当然了,其实不是这样,重新讲一下,就是大家在一个月的时间里面,从开始,做出一个可以上线的产品,从中体验整个产品的开发流程,学习工作中用到的技术栈,培养同学之间的合作意识。这其实是一个非常有意义的事情,从中你可以体会到创业的过程,需求分析、竞品分析、优势分析、未来产品规划、UI 设计、技术架构、团队合作、测试保证、产品推广,每一步都可谓非常有价值。

当然了,这其中最让我满足的便是我在 Mini 项目中担任了技术负责人的身份,终于可以满足我想玩各种技术的心,把各种想用的技术都集成在一起,话不多说,先申请 10 台机器走起,Docker Swarm;Kafka;Node.js;NestJs;小程序;mpvue;Redis;TiDB;ES;CI/CD;Spring Cloud,简直不要太爽。

让我最欣慰满足的是虽然我这个技术负责人当的不是很称职,但是大家不嫌弃,而且一起推动团队的进步,而且对我定下的技术方向没有太多的异议,尤其是后端龙哥定的微服务化的 Spring Cloud 框架简直不要太符合我的胃口。感觉大家都是有技术追求,热爱技术。

另外最爽的就是入职一个月的便天天加班,甚至有几次我都是凌晨 12 点下班,到家都 1 点了,第二天还要 7 点半爬起来赶班车,谢天谢地那个时候还有顺风车。即便如此,我并没有觉得加班有多累,因为我们都有一个共同的目标——产品上线,拿到第一。

哦顺便说一句,那个时候我们就是想做顺风车匹配,只不过我们是做拼滴滴车,简单来讲就是大家都是乘客,匹配到一起了,由其中一个人打滴滴,然后大家一起走。这样做最大的好处便是省钱,比如说我回家快车 80,拼车 60,如果找齐 4 人,便只要 20 快,甚至比顺风车还便宜,而且有快车的服务。但是即便如此,开始产品评审的时候,评委一直无法绕开滴滴顺风车这个大对手,也对我们的产品产生了质疑,而且我们在这期间也质疑过很多次,直到我们做完了之后没多久,2018 年 8 月,顺风车下架了,顺风车下架了,顺风车下架了,重要事情说三遍,我们这才意识到我们的产品是有多么正确,多么正确,多么正确。

向往 —— 就要步入真正的业务线去了

一个月的 Mini 项目结束之后,虽然我们并没有取得第一名,但是我们在这期间收获了更多的东西。

小组里面的每个人就要到对应的业务线去了,有去云信的,有去严选的,有去云音乐的,也有去 AI、安全部门的。虽然这些部门无一例外经历了裁员。

有了 Mini 项目的经历之后,我对接下来步入真正的工作充满了无比的向往,会不会他们也和我们的组员一样有着技术追求,我能不能发挥我自己最大的力量给项目贡献代码,能不能维护以为开源项目,能不能听到很多大佬们的讲座。

以前这些问题我是有所担心的,但是有了 Mini 项目的经历之后,我更相信我所说的。可是事实却是……

愁 —— 为什么和我想象的不一样,甚至说是非常非常非常不一样

可是事实却是事与愿违,进入部门之后,我才发现是我想多了。

有的时候我一直在吐槽说我们项目技术差,比如什么只能用 ES5;没有 webpack;没有测试;没有 CI;虽然是前后端分离,但是代码却和后端放在一个仓库里面;Git 提交没有规范,各种 pull merge;Git Message 也没有规范;……这是一个已经三年的项目了,而且最有意思的是原本开始的时候是孟导定的用 Regular,想兼容 IE。但是我问现在不是只兼容 Chrome 么,这是为啥呢?他们和我说原因就是因为 IE 实在是兼容不了。这个时候我真想说一句,这个理由真的很棒。

这个问题不仅仅体现在前端,后端也是有如此的问题,觉得只要能解决问题就好了,要什么格式,要什么技术追求。这碗面条(代码)我只要保证计算机能吃就行,乱成啥样关我啥事。我只说一句,看了后端 Java 代码,我是第一次见识到 Space 和 Tab 缩进同时使用的,牛啤。

我甚至还开过一个玩笑说,前端越来越追求规范,开始越来越多的使用 TS 强类型的语言,而后端却想着如果绕开 Java 类型限定,内部几乎都是 JSONObject。当然这是一句玩笑,毕竟有些框架的东西你是没法直接用 JSONObject 的,但也能反映一些问题。

愁啊愁啊,当初的期待全部破灭,想推重构,可是我一个刚入职的又如何推的动?

愁啊愁啊,不仅仅是因为技术,而且内部其他的问题也很多,比如说 QA 测试问题、产品乱接需求、文档书写不全。。。夸张点说,我的大学技术社团除了干活没钱、人力不足以外,算是吊打我们部门,至少从技术上是这么讲的。至少社团的服务器环境是全 Docker 化的。都 9102 年了,还不会用 Docker 是来搞笑的么……

更愁的是,网易似乎不接受内部的合作,我曾经给网易云写了一个更好用对象存储 Node.js SDK,nos-node-sdk,想合并到他们的代码库里面,结果他们一直不给我回复。问他们为啥不改进代码,他说人手不够……我 go……我帮你写了,我帮你维护还不乐意啊,再说我就不吐槽你们的文档写的有多差了,错误一片一片的,也就我愿意认认真真的看下来了。

迷茫 —— 完全看不到方向

时间一久,我就陷入了迷茫期,找不到未来发展的方向,就像是我周围的同学都在踏步前进,有研究 Node 内核的,有使用 Egg 开发后台的,有用 React+Antd 的,还有 Typescript 写 IOS 组件的。而我,现在只能看着 Regular,对未来几乎没有任何帮助。换句话说,他们所在的环境至少给他们拓宽了不少眼界,而网易却没给我拓宽任何的眼界。

我也在安慰自己,毕竟学习是自己的事情,公司不给力,只能靠我们自己啦。确实我也在不停的努力,尝试学习其他的东西,可是这种学习毕竟是没有方向性的,我现在缺的是项目经历,而不是基础。基础就像是地基,只要地基够用,如何快速搭建起高楼是最重要的,而不是说连高楼都没造起来的时候想着拓宽地基。

另外,你见过那个专家是可以脱离他们的环境的?工作不用 React,却想成为 React 专家;工作不用 Java,却想成为 Java 专家;工作不接触 Node 内核,怎么成为 Node 专家;工作不接触浏览器内核;怎么成为浏览器专家。一个公司的天花板牵扯着你能早就多高的喽,也能让你知道你现在的基础够不够用,如何更好的提高自己。自我驱动的学习最大的用处是帮你补足基础,而只有真正的项目经历,才能真正让你有质的成长。

但是,这些东西在网易,很难寻得。

也许有人会出来说,网易里面技术栈也是很多的,React/Vue 都是有的。确实,有的确有,可能这又能如何?况且使用这部分技术的人也是少之又少,最关键的还是人!

离开 —— 却没有任何留念

最终我选择了离开网易,去寻找新的家园。

当我真正离开的时候,我才发现原来网易并不是那么的不堪。有的地方还是有许多值得学习借鉴的地方,可惜对于我们这些下层人民来讲,上层优秀的想法无法渗透到我们这里,在我们眼里,网易只是一个完成需求就可以下班的地方,不需要你有太多的想法,只需要按照上面的指示一步一步来做就可以了。

难得说网易的福利好,可惜我并没有沾上多少,食堂 Mini 项目之后我就搬离本部园区,在外面写字楼包了一块办公区,所以食堂基本吃不上了;免费停车就更不用说了,只能停本部,然后走一公里到我的工作地点;内部的技术分享全在园区,根本没时间赶过去,只能在线上看看了;各种摆摊更别说看见了;网易二期别人都搬过去了,为啥我们还不搬。简单点来讲,我培养不了我对网易的感情,对我来我,我就像是给大公司做外包,那我为啥不去一个钱多技术好的公司呢。

离开的时候,最大的愧疚便是对 HR 们,愧对了你们对我们的培养,真的真的真的非常抱歉,路过 HR 办公区的时候,害怕你看到我,虽然你还是看到了我。不过我们是主动离职的,比被裁的要好一点点,可惜就是没钱拿。

但是,这并不能阻止我离开网易。

感谢

  • 感谢陪伴和我一起走过 mini 项目的同学们
  • 感谢导师的帮助
  • 感谢身边的同事们
  • 最最感谢校招 HR 小姐姐小哥哥们的努力,为你们实力打 Call。感谢你们对我们的照顾和指导,让我们更快的融入到了网易的大世界中。谢谢,谢谢,尤其感谢森森和 6 姐。

回首

现在我入职了字节跳动,现在回过头来看了看,对我来说,这真的算是一个非常正确的决定,我也就不吐槽网易跟字节跳动相比的基础设施有多差了,我也不吐槽网易有多扣了。

如果你现在问我推不推荐网易,我觉得还是值得推荐的,但是一定一定一定要看部门,一定一定一定要看 Leader,一定一定一定要看环境。看部门有没有发展前景,看 Leader 愿不愿意培养自己,看环境会不会限制到你的天花板。

另外这里实力打 Call 考拉前端,足够开放,也有大佬,如果真的去可以去这个部门。当然严选的就是有点封闭了,搞了一套很不错的 npm 私有仓库,权限系统也做了,却不尝试在公司内部推广,搞不懂。云音乐和严选差不多,这里又要吐槽一下内部的 nei 是好难用,不仅仅是 ui 丑,而且这么久了,才上线在线 mock 功能,还不支持 swagger 导入,还不如之前我实习的公司大搜车推出的 easy-mock 呢。

过去就是过去,迎接未来,至少字节跳动能给我保证多劳多得,这就足够了!!!

另外说一句,有想要内推字节跳动的学弟学妹么,社招也可以哦,请用简历砸我吧,xutianyang.bradley@bytedance.com,如果想要和我交流的话,可以 Telegram 找我,@xgheaven。

注释和共享

  • 第 1 页 共 1 页

XGHeaven

一个弱弱的码农


杭州电子科技大学学生一枚


Weifang Shandong, China