[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"list-\u002Farticles\u002Fweb":3},{"items":4,"total":131},[5,22,33,42,51,66,74,84,92,101,112,121],{"id":6,"type":7,"slug":8,"title":9,"date":10,"category":11,"tags":12,"body_markdown":16,"permalink":17,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":21},193,"article","use-npm-mirror-on-pnpm","在pnpm中使用NPM镜像","2024-11-23 14:00","web",[13,14,15],"NPM","代理","pnpm","\n在[上一篇文章](\u002Farticle\u002Fweb\u002F2024\u002Fsetup-npm-proxy)中，我为了解决网络阻断的问题，搭建了一个NPM代理服务器。文章发布后，收到了不少反馈，其中有一条是关于是否可以在pnpm中直接使用国内镜像。\n\n首先明确一下问题背景与结论：\n\n- 在使用NPM安装包的时候，如果遇到网络阻断问题，可以通过配置NPM镜像来下载依赖，但是有可能影响CI环境以及与他人的协作\n- 在pnpm中，可以直接配置使用任意NPM镜像，而不会影响CI环境和他人协作\n\n下面我们来详细看看来龙去脉。\n\n\u003C!-- more -->\n\n## npm pnpm和它们的lock文件\n\nnpm在安装包的时候，会生成一个`package-lock.json`文件，里面记录了每个包的版本号和下载地址，除此之外还记录了一个`integrity`字段，这个字段是一个哈希值，用来校验包的完整性。我们摘录`package-lock.json`文件中的一段内容：\n\n```json\n\"node_modules\u002Fis-number\": {\n    \"version\": \"6.0.0\",\n    \"resolved\": \"https:\u002F\u002Fregistry.npmjs.org\u002Fis-number\u002F-\u002Fis-number-6.0.0.tgz\",\n    \"integrity\": \"sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6\u002F538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==\",\n    \"engines\": {\n        \"node\": \">=0.10.0\"\n    }\n},\n```\n\n其中`resolved`字段就是包的下载地址，`integrity`字段是包的哈希值。\n\npnpm也有一个类似的文件，叫做`pnpm-lock.yaml`，它的内容和`package-lock.json`类似，但是有一点不同：pnpm的lock文件中，并不记录包的下载地址，而是只记录了包的哈希值。我们摘录`pnpm-lock.yaml`文件中的一段内容：\n\n```yaml\n\u002Fis-number@6.0.0:\n  resolution: {integrity: sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6\u002F538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==}\n  engines: {node: '>=0.10.0'}\n  dev: false\n```\n\n可以看到，`resolved`字段被省略了，只有`integrity`字段，而且`integrity`字段的值和`package-lock.json`中的一样。\n\n## lock文件的差异导致的registry镜像问题\n\n首先，因为pnpm的lock文件中没有记录包的下载地址，所以更换registry镜像并不会影响lock文件的内容。这意味着，如果我们在pnpm中使用了国内镜像，那么lock文件中的所有内容都和不使用镜像时一致，不会发生改变。\n\n但npm的表现就不一样了。如果我们在NPM中使用了registry镜像，那么lock文件中的`resolved`字段就**可能**被替换成镜像地址。注意这里我说的是**可能**，因为npm客户端记录的lock文件的`resolved`字段除了受配置的registry镜像影响，还会受到`node_modules`目录中已经存在的包以及本机npm缓存的影响。\n\n下面是一个真实的例子：\n\n```json\n\"node_modules\u002Fis-number\": {\n    \"version\": \"6.0.0\",\n    \"resolved\": \"https:\u002F\u002Fregistry.npmjs.org\u002Fis-number\u002F-\u002Fis-number-6.0.0.tgz\",\n    \"integrity\": \"sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6\u002F538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==\",\n    \"engines\": {\n    \"node\": \">=0.10.0\"\n    }\n},\n\"node_modules\u002Fis-odd\": {\n    \"version\": \"3.0.1\",\n    \"resolved\": \"https:\u002F\u002Fregistry.npmmirror.com\u002Fis-odd\u002F-\u002Fis-odd-3.0.1.tgz\",\n    \"integrity\": \"sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==\",\n    \"dependencies\": {\n    \"is-number\": \"^6.0.0\"\n    },\n    \"engines\": {\n    \"node\": \">=4\"\n    }\n}\n```\n\n虽然我只配置了`registry.npmmirror.com`一个源，但在这个lock文件中同时存在`registry.npmjs.org`和`registry.npmmirror.com`两个地址。也就是说，安装同一个包时，不同的机器可能会有不同的`resolved`字段，无法保证所有机器上生成的lock文件都是一致的。\n\n如果我们将npm的lock文件提交上去，就有可能导致两个问题：\n\n1. CI服务器会尝试访问这个镜像地址，而不是原始的npm registry地址，这可能会导致CI环境无法正常安装依赖（企业中CI机器一般有严格的网络策略，不会允许访问任意registry镜像）。\n2. 因为不同机器上的lock文件可能不一致，所以在多人协作的情况下，可能会导致不同人生成的lock文件不一致，进而导致冲突。\n\n## 一些无用小资料\n\n在翻查相关资料的过程中，还发现一些有意思的小细节。\n\n### npm的隐藏lock文件\n\n首先是npm的lock文件有3个不同的版本，其中v1是比较老的版本使用，而v2和v3的格式则完全一样，那为什么会有一个v3版本呢？\n\n```json\n{\n  \"name\": \"npm-test\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 3,\n  ...\n}\n```\n\n这实际上是npm v7带来的一个新东西，叫“隐藏lock文件”。在npm v7以上的版本中，如果你使用npm安装依赖，它会生成一个`node_modules\u002F.package-lock.json`文件，注意不是根目录下的`package-lock.json`文件。这个文件的内容和根目录下的lock文件几乎一样，只是位置不同。这样做的目的是为了减少对`node_modules`目录的扫描，以提高性能。当满足以下条件时，npm会直接读取隐藏lock文件中的信息，而不对`node_modules`进行扫描：\n\n- 隐藏lock文件中所有的包对应的目录都存在\n- 所有包目录中的包都在隐藏lock文件中列出\n- 隐藏lock文件的修改时间不早于所有包目录的修改时间\n\n这3个条件的意思也就是“隐藏lock文件”是在最近一次安装\u002F更新包依赖时被更新的。\n\n因此`lockfileVersion: 3`的意思也就是“这里有一个隐藏lock文件”。\n\n### pnpm对“换源”问题的解决\n\n通过前面的介绍，我们可以知道pnpm对于更换registry镜像是可以无感的，因为它的lock文件中并不记录包的下载地址。但实际上可能是出于对完整性的考虑，pnpm在换源后重新安装包的时候会报错：\n\n```shell\nERROR  This modules directory was created using the following registries configuration: {\"default\":\"https:\u002F\u002Fregistry.npmjs.org\u002F\"}. The current configuration is {\"default\":\"https:\u002F\u002Fregistry.npmmirror.com\u002F\"}. To recreate the modules directory using the new settings, run \"pnpm install\".\n```\n\n也就是说换源后需要重新使用`pnpm install`来安装一次依赖。\n\n但是既然pnpm的lock文件并没有记录包的下载地址，它是怎么知道之前这些包是从哪下载的呢？通过查看pnpm的源码，最终找到了答案：pnpm会在`node_modules\u002F.modules.yaml`中记录一些信息：\n\n```yaml\nhoistPattern:\n  - '*'\nhoistedDependencies:\n  \u002Fis-number\u002F6.0.0:\n    is-number: private\nincluded:\n  dependencies: true\n  devDependencies: true\n  optionalDependencies: true\ninjectedDeps: {}\nlayoutVersion: 5\nnodeLinker: isolated\npackageManager: pnpm@8.10.2\npendingBuilds: []\nprunedAt: Sat, 23 Nov 2024 04:56:52 GMT\npublicHoistPattern:\n  - '*eslint*'\n  - '*prettier*'\nregistries:\n  default: https:\u002F\u002Fregistry.npmjs.org\u002F\nskipped: []\nstoreDir: \u002FUsers\u002Ftoobug\u002F.pnpm-global\u002Fstore\u002Fv3\nvirtualStoreDir: .pnpm\n```\n\n这是不是又和npm的隐藏lock文件有点像呢？\n\n## 结语\n\npnpm真香！用pnpm解千愁！以及需要认真对维护npm镜像的同学们表示感谢！\n",null,0,"2026-08-28 04:37:17","published","",{"id":23,"type":7,"slug":24,"title":25,"date":26,"category":11,"tags":27,"body_markdown":32,"permalink":17,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":21},192,"setup-npm-proxy","NPM代理搭建指南","2024-11-16 16:00",[13,14,28,29,30,31],"反向代理","SSL","证书","VPN","\n最近在家里开发项目的时候，时常碰到npm安装超时。想了一下手上有好多服务器，应该能解决这个问题，于是我搭建了一个代理服务器。\n\n## 技术方案概述\n\nnpm registry有两个主要地址：\n\n- https:\u002F\u002Fregistry.npmjs.com\n- https:\u002F\u002Fregistry.npmjs.org\n\n需要让npm客户端访问这两个域名的时候走自己的服务器，并突破封锁。\n\n\u003C!-- more -->\n\n本方案概要：\n\n- 使用Nginx搭建反向代理服务器\n- 自签名SSL证书并设置信任\n- 使用TailScale VPN实现网络互通\n- hosts文件配置域名解析\n\n其中TailScale是一个基于WireGuard的现代VPN解决方案，它是本方案中不可或缺的组件，因为它能够帮助我们：\n\n- 通过零配置组网快速建立安全通道\n- 利用 WireGuard 的高性能加密通信确保数据安全\n- 实现 NAT 穿透，突破网络封锁\n- 支持多平台部署\n\n> 为什么不直接使用梯子：一方面是因为梯子流量有限，安装npm包会消耗大量流量；另一方面是我主要使用浏览器插件来分流，因此梯子没有配置按域名分流，无法针对npm的请求进行分流。\n\n> 为什么不使用npm镜像：因为通过npm镜像安装的话，lock文件也会使用镜像地址，这样在CI\u002FCD环境中可能会出现新的问题。\n\n## SSL证书配置与信任\n\n为了确保npm客户端信任我们自己搭的反向代理，我们需要配置并信任自签名证书。整个过程分为三步：生成根证书（CA）、生成服务器证书、添加证书信任。\n\n### 生成根证书（CA）\n\n这个脚本完成以下工作：\n- 生成 2048 位的 CA 私钥\n- 创建证书签名请求（CSR）\n- 设置基本约束和密钥用途\n- 生成有效期为10年的CA证书\n\n```sh\n#!\u002Fbin\u002Fbash\nset -o errexit\n\n# Generate CA private key\nopenssl genrsa -out ca.key 2048\n\n# Generate CA certificate signing request\nopenssl req -new -key ca.key -out ca.csr -sha256 \\\n    -subj \"\u002FC=CN\u002FST=GuangDong\u002FL=Shenzhen\u002FO=Tinkink\u002FOU=Tinkink\u002FCN=tinkink.net\"\n\n# Create ca.ext file\necho \"basicConstraints=CA:TRUE\nkeyUsage=keyCertSign,cRLSign\" > ca.ext\n\n# Generate CA certificate\nopenssl x509 -req -in ca.csr -signkey ca.key -out ca.crt \\\n    -extfile ca.ext -sha256 -days 3650\n```\n\n### 生成服务器证书\n\n这个脚本完成以下工作：\n\n- 生成服务器私钥\n- 创建服务器证书签名请求\n- 配置证书扩展信息，包括多域名支持\n- 使用之前创建的 CA 证书签发服务器证书\n\n```sh\n#!\u002Fbin\u002Fbash\n\n# Generate private key for npm\nopenssl genrsa -out npm.key 2048\n\n# Generate certificate signing request\nopenssl req -new -key npm.key -out npm.csr -sha256 \\\n    -subj \"\u002FC=CN\u002FST=GuangDong\u002FL=Shenzhen\u002FO=Tinkink\u002FOU=Tinkink\u002FCN=registry.npmjs.org\"\n\n# Create npm.ext file\necho \"authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment\nsubjectAltName=@alt_names\n\n[alt_names]\nDNS.1=registry.npmjs.org\nDNS.2=registry.npmjs.com\" > npm.ext\n\n# Generate certificate\nopenssl x509 -req -in npm.csr -CA ca.crt -CAkey ca.key \\\n    -CAcreateserial -out npm.crt -extfile npm.ext -sha256 -days 720\n```\n\n### 证书信任配置\n\n生成证书后，需要将 CA 证书（ca.crt）添加到系统的信任存储中：\n\n**Windows**:\n\n1. 双击证书文件\n2. 选择\"安装证书\" -> \"本地计算机\"（需要管理员权限）\n3. 选择\"受信任的根证书颁发机构\"\n4. 也可以使用管理员权限运行 PowerShell 命令：\n\n```powershell\nImport-Certificate -FilePath \"ca.crt\" -CertStoreLocation Cert:\\LocalMachine\\Root\n```\n\n**Mac**:\n\n1. 双击证书文件，添加到钥匙串访问\n2. 在钥匙串访问中找到证书，双击展开\n3. 展开\"信任\"选项，将\"使用此证书时\"设置为\"始终信任\"\n\n**Linux**:\n\n```bash\nsudo cp ca.crt \u002Fusr\u002Flocal\u002Fshare\u002Fca-certificates\u002F\nsudo update-ca-certificates\n```\n\n## Nginx 反向代理配置\n\n在服务器上先配置好TailScale VPN，确保本地和服务器在同一个网络中。然后使用以下 Nginx 配置文件：\n\n```nginx\nserver {\n    listen 80;\n    server_name registry.npmjs.com registry.npmjs.org;\n\n    # Redirect all HTTP requests to HTTPS\n    return 301 https:\u002F\u002F$host$request_uri;\n}\n\nserver {\n    listen 443 ssl;\n    server_name registry.npmjs.com registry.npmjs.org;\n\n    ssl_certificate \u002Fetc\u002Fnginx\u002Fssl\u002Fnpm.crt;\n    ssl_certificate_key \u002Fetc\u002Fnginx\u002Fssl\u002Fnpm.key;\n\n    ssl_protocols TLSv1.2 TLSv1.3;\n\n    # Define the DNS resolver\n    resolver 8.8.8.8 8.8.4.4 valid=30s; # You can use Google's public DNS or any resolver you prefer\n    resolver_timeout 5s;\n\n    # Proxy requests to npm registry\n    location \u002F {\n        proxy_pass https:\u002F\u002F$host;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n\n        # Proxy settings to handle large requests and timeouts\n        proxy_read_timeout 90;\n        proxy_connect_timeout 90;\n        proxy_redirect off;\n\n        # Optionally, if you need to handle large response or request sizes\n        client_max_body_size 50M;\n    }\n}\n```\n\n这里重点说明两个关键配置：\n\n1. `resolver`配置用来指定当nginx访问npm registry时的DNS解析服务器，这里使用了Google的公共DNS服务器，如果不设置的话会报错\n2. `proxy_pass https:\u002F\u002F$host`：使用 `$host` 变量而不是硬编码域名，这样可以支持多个npm registry域名\n\n## hosts 文件配置\n\n要让本地请求指向代理服务器，需要修改 hosts 文件。推荐使用[SwitchHosts](https:\u002F\u002Fgithub.com\u002Foldj\u002FSwitchHosts)。但需要注意，大部分系统下，修改Hosts都需要管理员权限，有些系统还需要专门添加可写权限才能修改成功。\n\n```plaintext\n# NPM registry proxy\n10.x.x.x    registry.npmjs.com registry.npmjs.org\n```\n\n## 结语\n\n通过以上配置，我们就可以使用自己的服务器来代理npm包的下载，目前我已经使用了一段时间，下载速度明显提升，推荐有这个问题且不方便用梯子的也尝试一下。\n\n> 本文部分内容由Cursor（AI驱动的代码编辑器）协助编写和润色。\n",{"id":34,"type":7,"slug":35,"title":36,"date":37,"category":11,"tags":38,"body_markdown":40,"permalink":17,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":41},191,"add-twitter-card-for-blog","为博客添加Twitter卡片","2022-08-29 00:00:00",[39],"Twitter","\n## 什么是Twitter卡片\n\nTwitter中可以引用各个网站的链接，如果网站不做特殊处理的话，在推文中只会显示一条普通的蓝色文字链接。\n\n![普通链接](\u002Fassets\u002Fweb\u002F2022\u002Fadd-twitter-card-for-blog\u002F01.png)\n\n但是我们时不时会看到一些引用的链接，在推文中有另外的表现，像这样：\n\n![Twitter卡片](\u002Fassets\u002Fweb\u002F2022\u002Fadd-twitter-card-for-blog\u002F02.png)\n\n这就是Twitter卡片。\n\n## 原理\n\nTwitter卡片需要网站主动进行适配，Twitter会在适当的时候访问网址进行抓取，如果能抓取到指定的结构，就会显示成卡片。\n\n按照[官方文档](https:\u002F\u002Fdeveloper.twitter.com\u002Fen\u002Fdocs\u002Ftwitter-for-websites\u002Fcards\u002Fguides\u002Fgetting-started)，卡片可以有好几种类型：\n\n- `summary` 图文摘要\n- `summary_large_image` 大图展示的图文摘要\n- `app` APP下载链接\n- `player` 视频播放器\n\n针对博客，比较适合的类型是`summary`，如果图片更重要的话，用`summary_large_image`也可以。\n\n具体而言，如果需要使用`summary`类型的卡片，需要在页面的`head`中放置以下标记：\n\n```html\n\u003C!--指定卡片类型-->\n\u003Cmeta name=\"twitter:card\" content=\"summary\" \u002F>\n\u003C!--可选：站点的twitter账号-->\n\u003Cmeta name=\"twitter:site\" content=\"@nytimesbits\" \u002F>\n\u003C!--可选：作者的twitter账号-->\n\u003Cmeta name=\"twitter:creator\" content=\"@nickbilton\" \u002F>\n\u003C!--文章URL-->\n\u003Cmeta property=\"og:url\" content=\"http:\u002F\u002Fbits.blogs.nytimes.com\u002F2011\u002F12\u002F08\u002Fa-twitter-for-my-sister\u002F\" \u002F>\n\u003C!--文章标题-->\n\u003Cmeta property=\"og:title\" content=\"A Twitter for My Sister\" \u002F>\n\u003C!--文章摘要-->\n\u003Cmeta property=\"og:description\" content=\"In the early days, Twitter grew so quickly that it was almost impossible to add new features because engineers spent their time trying to keep the rocket ship from stalling.\" \u002F>\n\u003C!--文章配图-->\n\u003Cmeta property=\"og:image\" content=\"http:\u002F\u002Fgraphics8.nytimes.com\u002Fimages\u002F2011\u002F12\u002F08\u002Ftechnology\u002Fbits-newtwitter\u002Fbits-newtwitter-tmagArticle.jpg\" \u002F>\n```\n\n\u003C!-- more -->\n\n事实上下面这些`og:`开头的标签，使用Twitter专属的标签也可以生效。\n\n```html\n\u003Cmeta name=\"twitter:title\" content=\"In the early days, Twitter grew so quickly that it was almost impossible to add new features because engineers spent their time trying to keep the rocket ship from stalling.\" \u002F>\n\u003Cmeta name=\"twitter:description\" content=\"In the early days, Twitter grew so quickly that it was almost impossible to add new features because engineers spent their time trying to keep the rocket ship from stalling.\" \u002F>\n\u003Cmeta name=\"twitter:image:src\" content=\"http:\u002F\u002Fgraphics8.nytimes.com\u002Fimages\u002F2011\u002F12\u002F08\u002Ftechnology\u002Fbits-newtwitter\u002Fbits-newtwitter-tmagArticle.jpg\" \u002F>\n```\n\n## 实战\n\n在Hexo中，只需要修改一下模板，为文章页添加一下上述标记，并填上对应的内容即可（模板是jade\u002Fpug）：\n\n```pug\nmeta(name=\"description\", content=desc)\nmeta(name=\"twitter:card\", content=\"summary\")\nmeta(name=\"twitter:creator\", content=\"@TooooooBug\")\nmeta(name=\"og:url\", content=config.url + url_for(page.path))\nmeta(name=\"og:title\", content=page.title)\nmeta(name=\"og:description\", content=page.raw.replace(\u002F^[\\s\\S]*---\\n\\n\u002F, '').replace(\u002F\\s+\u002Fg, ' ').slice(0, 100), '')\n- var getPhoto = (raw) => {\n-   const regex = \u002F\\!\\[.*?\\]\\((.*?)\\)\u002F;\n-   const matches = regex.exec(raw);\n-   if (matches && matches[1]) return matches[1];\n-   return '\u002Flogo.png';\n- }\nmeta(name=\"og:image\", content=config.url + getPhoto(page.raw))\n```\n\n中间加了一个`getPhoto()`方法，从原始的Markdown文本中解析出第一张图片的url，如果没有找到图片，则使用logo。\n\n## 小结\n\n挺简单的配置就能实现Twitter卡片，如果配置成功了，在发送推文的时候也可能会出现卡片预览。\n\n![卡片预览](\u002Fassets\u002Fweb\u002F2022\u002Fadd-twitter-card-for-blog\u002F03.png)\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2022\u002Fadd-twitter-card-for-blog\u002F01.png",{"id":43,"type":7,"slug":44,"title":45,"date":46,"category":11,"tags":47,"body_markdown":50,"permalink":17,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":21},188,"coding-is-a-sure-thing","写代码是一件与确定性为伍的事情","2020-09-19 13:39",[48,49],"stream","node.js","\n我们所处的世界充满了各种各样的不确定性。但有一件事是不存在不确定性的，即写代码。\n\n多年以前，在我刚入行不久的时候，有一位前辈和我说过“出现问题的时候，先怀疑是自己的原因，因为机器是不会出错的，错的永远是人。”这句话我记了很久，也时不时就会翻出来回想一番，也会冒出很多更细的想法：那机器不也是人造的？机器的程序不也是人写的？就一定是自己的原因，不能是别人的原因吗？但反复想了很多年，还是觉得这句话相当有道理，即使是别人的原因，那错的也是人而不是机器。\n\n这其实就是写代码时的确定性，我们写的代码会被怎么运行，是非常确定的。即便它要依赖更多的底层软硬件机制，但仍然是确定的，只是找出这个确定性的过程更加复杂而已。\n\n## 一个例子\n\n> 如果你看不懂例子，跳过就好。\n\n### 背景\n\n项目中需要上传下载文件，使用的是某云服务的存储服务。在下载的部分，为了方便，使用Node.js封装了一个下载方法，返回一个`Stream`，而这个`Stream`本质上是由http请求库request.js请求后返回的。最后由koa框架返回这个`Stream`给浏览器。\n\n请求下载 -> 下载方法 -> request.js请求云服务 -> 返回`Stream`\n\n代码大致如下：\n\n```javascript\nrouter.get('\u002Fapi\u002Fdownload-file', async (ctx) => {\n    ctx.body = Download.getPrivateStream(ctx.query.fileId);\n});\n```\n\n然而，同样的代码，在不同的项目下，表现却大不一样，A项目访问图片时是直接在浏览器中显示图片，B项目访问同样的图片却变成了下载。调试工具一查看，发现它们有不一样的HTTP Header返回：\n\n- A项目`Content-Type: image\u002Fpng`\n- B项目`Content-Type: application\u002Foctet-stream`\n\n\u003C!-- more -->\n\n### 解决\n\n经过初步排查，A B两个项目中都没有手工设置过这个Header值，可以基本确认这个差异并不是由于下载部分的写法造成的。\n\n虽然原因不是很明朗，但这个问题却很好解决：手工加一个设置`Content-Type`值的代码，一行代码就能解决。\n\n```javascript\nrouter.get('\u002Fapi\u002Fdownload-file', async (ctx) => {\n    ctx.type = mime.getType(fileExt);\n    ctx.body = Download.getPrivateStream(ctx.query.fileId);\n});\n```\n\n### 寻找确定性\n\n虽然上面的代码解决了这个应用场景下的问题，但却并没有找到真正的原因。也就是说，这里遗留了一段具有不确定性的代码。\n\n为了找到真正确定的原因，我在接下来的两天内花了一个晚上+一个上午的时间，从下载的封装到request.js的源码都一一做了排查，最终找到了原因。\n\nrequest.js在发现`response`（`Stream`）被`pipe`到一个新的`Stream`的时候，会尝试使用新`Stream`的`setHeader`方法，将源响应中的HTTP header都设置到新的`Stream`上。\n\n```javascript\nif (dest.headers && !dest.headersSent) {\n    if (response.caseless.has('content-type')) {\n        var ctname = response.caseless.has('content-type')\n        if (dest.setHeader) {\n            dest.setHeader(ctname, response.headers[ctname])\n        } else {\n            dest.headers[ctname] = response.headers[ctname]\n        }\n    }\n\n    if (response.caseless.has( 'content-length')) {\n        var clname = response.caseless.has( 'content-length')\n        if (dest.setHeader) {\n            dest.setHeader(clname, response.headers[clname])\n        } else {\n            dest.headers[clname] = response.headers[clname]\n        }\n    }\n}\n```\n\n然而调试到这里的时候会发现A B两个项目走到了不同的逻辑。B项目的新`Stream`（代码中的`dest`）并不存在`setHeader`方法。\n\n通过查看koa的源码，会发现这个`dest`其实就是`ctx.body`。按理说，`ctx.body`是一个http response stream，肯定是有`setHeader`方法的。那么，唯一的解释就是：有别的代码动过`ctx.body`了。\n\n最后经过一番排查，找到了一个万万想不到的事实：`koa-logger`会替换`ctx.body`\n\n```javascript\n\u002F\u002F calculate the length of a streaming response\n\u002F\u002F by intercepting the stream with a counter.\n\u002F\u002F only necessary if a content-length header is currently not set\nconst length = ctx.response.length\nconst body = ctx.body\nlet counter\nif (length == null && body && body.readable) {\n    ctx.body = body\n        .pipe(counter = Counter())\n        .on('error', ctx. onerror)\n}\n```\n\n原来，`koa-logger`为了记录响应体的大小，粗暴地将`ctx.body` `pipe`到了一个`Counter`实例上，并就此替换了`ctx.body`。刚好，A项目没有使用`koa-logger`，而B项目使用了。\n\n\n## 结\n\n因为出于对不确定性的不放心，所以尽管有现成的解决办法，但仍然没有放弃对它的追查。谁能想得到，最终的问题出在一个看起来人畜无害的代码库中呢？好在，花费了一番工夫，总算把一个不确定的事情变成了确定的事情。\n在这个例子中，可能我们会再慎重地评估这个代码库，即使不能第一时间进行替换和修复，也可以给出足够的文档，而这将为后续代码的稳定运行打下坚实基础。\n\n写代码是一件与确定性为伍的事情，如果你觉得你的代码有诸多不确定性，那只能说明一件事情，就是你又欠工夫了。\n\n> 2022-08更新：2年过去了，koa-logger仍然没有修复这个问题。\n>\n> [issue](https:\u002F\u002Fgithub.com\u002Fkoajs\u002Flogger\u002Fpull\u002F81) [Pull Request](https:\u002F\u002Fgithub.com\u002Fkoajs\u002Flogger\u002Fpull\u002F85)\n",{"id":52,"type":7,"slug":53,"title":54,"date":55,"category":11,"tags":56,"body_markdown":63,"permalink":64,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":65},190,"work-with-urlencode","Urlencode踩坑日记","2020-03-31 13:37",[57,58,59,60,61,62],"JavaScript","PHP","Python","Urlencode","编码","签名","\nUrlencode又称百分号编码，是一种很常用的编码方式，作为前端工程师，少不了与它要打交道。不管是GET请求发送参数，还是POST请求发送body，都少不了要使用Urlencode来编码。\n\n而Urlencode的编码规则又特别简单：取出字符的ASCII码，转成16进制，然后前面加上百分号即可。如果是多字节的字符，则取出每一字节，按照同样的规则进行转换即可。例如问号`?`的ASCII码为`63`，转换为16进制为`3F`，所以`%3F`即为`?`进行Urlencode编码的结果。\n\n![urlencode](\u002Fassets\u002Fwork-with-urlencode\u002F1.png)\n\n## 背景\n\n项目需要对外提供HTTP API接口，因此接口鉴权成为一个很重要的内容。为了确保安全，防止中间人篡改数据或进行重放攻击，双方约定的私钥不可以直接出现在请求中，因此采用请求签名的方式来鉴权。\n\n双方约定appKey和appSecret，其中appKey用于识别请求对象，appSecret用于请求签名。具体的方案如下：\n\n1. 客户端按照当前时间生成时间戳`timestamp`和随机数`nonce`\n2. 客户端按照指定的规则将HTTP请求的queryString和POST的body进行编码，得到一个字符串`data`\n3. 将`timestamp`、`nonce`和`data`按规则拼接，然后使用`appSecret`计算签名\n4. 将`appKey`、`timestamp`、`nonce`和签名一起随请求发出\n\n服务端在接到请求后将使用获得的数据和`appSecret`重新计算签名，然后判断与客户端给出的签名值是否一致，如果不一致则鉴权不通过。\n\n这一套鉴权机制可以有效防御一些攻击手段：\n\n- 使用了时间戳，可以避免过期请求被重发\n- 使用了随机数，可以避免请求被短时间重复发送\n- 签名数据包含了完整的时间戳、随机数和请求数据，保证服务端收到的确实是客户端发送的数据，避免被拦截修改\n- 签名的密钥是双方协商好的，避免请求伪造\n\n## 踩坑\n\n在上面的鉴权过程中，一个非常重要的点就是第2点，即将请求的queryString和POST的body进行编码，得到一个字符串。\n\n因为GET和POST请求中，数据都会被Urlencode编码，因此很容易想到，我们也使用Urlencode来进行这个鉴权前的编码过程。\n\n于是坑就这么不期而遇了。\n\n\u003C!-- more -->\n\n由于服务端和客户端都使用JavaScript编写，因此都使用了`encodeURIComponent`来进行Urlencode编码，并且过程相当愉快。\n\n但既然是开放接口，就早晚会面临各种各样的客户端。于是在我自己编写的PHP客户端上，踩坑了：有时候请求一切正常，有时候却鉴权无法通过。经过反复的调试分析，最终发现，PHP获取的待签名的字符串和JS获取的不一样，而问题就出在对`*`的转义上。\n\n在JS中，`encodeURIComponent`并不会对`*`进行转义，而PHP中`rawurlencode`却会将`*`转义为`%2A`。因此，同样的数据在不同的客户端中就产生了不同的字符串，最终导致计算出的签名值不同，鉴权失败。\n\n## 爬坑失败\n\n如果一个接口一直使用都没有问题，突然来了一个新的客户端就鉴权失败，那么必然是这个客户端有问题了。在这种想法的驱使下，对PHP这个世界上最好的语言好感度再次-1，然后硬着头皮去查资料。发现确实有很多人碰到了PHP在使用Urlencode编码的时候星号被编码的问题。还有人给出了解决问题的代码，即在`rawurlencode`之后再将`%2A`替换成`*`。\n\n于是就这么更新上线了，一切又恢复了正常。\n\n然而好景不长，才刚正常几天，又出现了诡异的鉴权失败的问题。而这一次，请求的内容是`[链接](https:\u002F\u002Fwww.qq.com)`。再次对比后，发现括号`(`、`)`在Urlencode后又不一致了：JS没有对括号进行转义，而PHP对它们进行了转义，于是再次出现签名不一致的问题。\n\n直觉告诉我，当一个问题第一次出现时，也许可以绕得过去，但是当它再一次出现的时候，就必须得挖到底了，否则未来一定会有更严重的问题出现。\n\n## 认真审视Urlencode\n\n回到问题本质：兼容性问题。这可是前端工程师最擅长的领域，于是很自然地想到——规范。urlencode的规范是[RFC3986](https:\u002F\u002Ftools.ietf.org\u002Fhtml\u002Frfc3986)，但是看完规范之后，并没能解决这个兼容性的问题，反而解释了兼容性的来源：很多字符是否进行编辑取决于具体的场景和实现……\n\n> 下面这一段有点烧脑，如果不是特别有兴趣，建议跳过。\n\n规范将保留字符分为`gen-delims`和`sub-delims`两部分：\n\n- `gen-delims = \":\" \u002F \"\u002F\" \u002F \"?\" \u002F \"#\" \u002F \"[\" \u002F \"]\" \u002F \"@\"`\n- `sub-delims = \"!\" \u002F \"$\" \u002F \"&\" \u002F \"'\" \u002F \"(\" \u002F \")\" \u002F \"*\" \u002F \"+\" \u002F \",\" \u002F \";\" \u002F \"=\"`\n\n然后定义了`pchar`（`unreserved`指除了保留字符之外的字符）\n\n- `pchar = unreserved \u002F pct-encoded \u002F sub-delims \u002F \":\" \u002F \"@\"`\n\n以URL中出现的`path`和`query`为例，它们的规则分别是\n\n```\npath          = path-abempty    ; begins with \"\u002F\" or is empty\n                    \u002F path-absolute   ; begins with \"\u002F\" but not \"\u002F\u002F\"\n                    \u002F path-noscheme   ; begins with a non-colon segment\n                    \u002F path-rootless   ; begins with a segment\n                    \u002F path-empty      ; zero characters\n\n      path-abempty  = *( \"\u002F\" segment )\n      path-absolute = \"\u002F\" [ segment-nz *( \"\u002F\" segment ) ]\n      path-noscheme = segment-nz-nc *( \"\u002F\" segment )\n      path-rootless = segment-nz *( \"\u002F\" segment )\n      path-empty    = 0\u003Cpchar>\n\n      segment       = *pchar\n      segment-nz    = 1*pchar\n      segment-nz-nc = 1*( unreserved \u002F pct-encoded \u002F sub-delims \u002F \"@\" )\n                    ; non-zero-length segment without any colon \":\"\n```\n\n```\nquery       = *( pchar \u002F \"\u002F\" \u002F \"?\" )\n```\n\n可以看到，它们都有引用`pchar`作为规则（或规则的一部分），除此之外，还有各自允许的字符。这中间的细节要弄明白需要花非常多的时间，我们也可以先不纠结，虽然规范中写了每个部分可以包含哪些字符，却并没有明确写出这些字符是否需要进行`urlencode`（例如`sub-delims`）。\n\n规范中唯一能给我们一些比较明确指引的只有对`unreserved`非保留字符的描述，明确定义了它们是`字母 \u002F 数字 \u002F \"-\" \u002F \".\" \u002F \"_\" \u002F \"~\"`这几个字符。\n\n## 回到现实\n\n既然规范无法给出足够明确的指引，就只能看看现实世界是怎么运作的了。在搜索urlencode规范的时候，发现有很多文档都是这么写：\n\n> 按照rfc3986，除字母、数字、`-`、`.`、`_`、`~`字符外，其它字符均需要进行百分号编码。\n\n也即，大家在实际应用时，会把除非保留字符之外的其他字符全部进行编码。\n\n那编程语言又是如何处理的呢？于是拿JavaScript、PHP、Python分别跑了一下。由于JS中有`encodeURI`\u002F`encodeURIComponent`两个方法，PHP有`urlencode`\u002F`rawurlencode`两个方法，因此一共有5组结果。\n\n|ASCII|char              |python|js encodeURI|js encodeURIComponent|php urlencode|php rawurlencode|\n|-----|------------------|------|------------|---------------------|-------------|----------------|\n|0    |NUT 空字符（Null）|%00   |%00          |%00                   |%00           |%00              |\n|1    |SOH 标题开始      |%01   |%01          |%01                   |%01           |%01              |\n|2    |STX 本文开始      |%02   |%02          |%02                   |%02           |%02              |\n|3    |ETX 本文结束      |%03   |%03          |%03                   |%03           |%03              |\n|4    |EOT 传输结束      |%04   |%04          |%04                   |%04           |%04              |\n|5    |ENQ 请求          |%05   |%05          |%05                   |%05           |%05              |\n|6    |ACK 确认回应      |%06   |%06          |%06                   |%06           |%06              |\n|7    |BEL 响铃          |%07   |%07          |%07                   |%07           |%07              |\n|8    |BS 退格           |%08   |%08          |%08                   |%08           |%08              |\n|9    |HT 水平定位符号TAB|%09   |%09          |%09                   |%09           |%09              |\n|10   |LF 换行键         |%0A   |%0A          |%0A                   |%0A           |%0A              |\n|11   |VT 垂直定位符号   |%0B   |%0B          |%0B                   |%0B           |%0B              |\n|12   |FF 换页键         |%0C   |%0C          |%0C                   |%0C           |%0C              |\n|13   |CR Enter回车键    |%0D   |%0D          |%0D                   |%0D           |%0D              |\n|14   |SO 取消变换       |%0E   |%0E          |%0E                   |%0E           |%0E              |\n|15   |SI 启用变换       |%0F   |%0F          |%0F                   |%0F           |%0F              |\n|16   |DLE 跳出数据通讯  |%10   |%10          |%10                   |%10           |%10              |\n|17   |DC1 设备控制一    |%11   |%11          |%11                   |%11           |%11              |\n|18   |DC2 设备控制二    |%12   |%12          |%12                   |%12           |%12              |\n|19   |DC3 设备控制三    |%13   |%13          |%13                   |%13           |%13              |\n|20   |DC4 设备控制四    |%14   |%14          |%14                   |%14           |%14              |\n|21   |NAK 确认失败回应  |%15   |%15          |%15                   |%15           |%15              |\n|22   |SYN 同步用暂停    |%16   |%16          |%16                   |%16           |%16              |\n|23   |TB 区块传输结束   |%17   |%17          |%17                   |%17           |%17              |\n|24   |CAN 取消          |%18   |%18          |%18                   |%18           |%18              |\n|25   |EM 连接介质中断   |%19   |%19          |%19                   |%19           |%19              |\n|26   |SUB 替换          |%1A   |%1A          |%1A                   |%1A           |%1A              |\n|27   |ESC 退出键        |%1B   |%1B          |%1B                   |%1B           |%1B              |\n|28   |FS 文件分区符     |%1C   |%1C          |%1C                   |%1C           |%1C              |\n|29   |GS 组群分隔符     |%1D   |%1D          |%1D                   |%1D           |%1D              |\n|30   |RS 记录分隔符     |%1E   |%1E          |%1E                   |%1E           |%1E              |\n|31   |US 单元分隔符     |%1F   |%1F          |%1F                   |%1F           |%1F              |\n|32   |空格              |%20   |%20          |%20                   |+             |%20              |\n|33   |!                 |%21   |!            |!                     |%21           |%21              |\n|34   |\"                 |%22   |%22          |%22                   |%22           |%22              |\n|35   |#                 |%23   |#            |%23                   |%23           |%23              |\n|36   |$                 |%24   |$            |%24                   |%24           |%24              |\n|37   |%                 |%25   |%25          |%25                   |%25           |%25              |\n|38   |&                 |%26   |&            |%26                   |%26           |%26              |\n|39   |'                 |%27   |'            |'                     |%27           |%27              |\n|40   |(                 |%28   |(            |(                     |%28           |%28              |\n|41   |)                 |%29   |)            |)                     |%29           |%29              |\n|42   |\\*                 |%2A   |\\*            |*                     |%2A           |%2A              |\n|43   |+                 |%2B   |+            |%2B                   |%2B           |%2B              |\n|44   |,                 |%2C   |,            |%2C                   |%2C           |%2C              |\n|45   |-                 |-     |-            |-                     |-             |-                |\n|46   |.                 |.     |.            |.                     |.             |.                |\n|47   |\u002F                 |\u002F     |\u002F            |%2F                   |%2F           |%2F              |\n|48   |0                 |0     |0            |0                     |0             |0                |\n|49   |1                 |1     |1            |1                     |1             |1                |\n|50   |2                 |2     |2            |2                     |2             |2                |\n|51   |3                 |3     |3            |3                     |3             |3                |\n|52   |4                 |4     |4            |4                     |4             |4                |\n|53   |5                 |5     |5            |5                     |5             |5                |\n|54   |6                 |6     |6            |6                     |6             |6                |\n|55   |7                 |7     |7            |7                     |7             |7                |\n|56   |8                 |8     |8            |8                     |8             |8                |\n|57   |9                 |9     |9            |9                     |9             |9                |\n|58   |:                 |%3A   |:            |%3A                   |%3A           |%3A              |\n|59   |;                 |%3B   |;            |%3B                   |%3B           |%3B              |\n|60   |\u003C                 |%3C   |%3C          |%3C                   |%3C           |%3C              |\n|61   |=                 |%3D   |=            |%3D                   |%3D           |%3D              |\n|62   |>                 |%3E   |%3E          |%3E                   |%3E           |%3E              |\n|63   |?                 |%3F   |?            |%3F                   |%3F           |%3F              |\n|64   |@                 |%40   |@            |%40                   |%40           |%40              |\n|65   |A                 |A     |A            |A                     |A             |A                |\n|66   |B                 |B     |B            |B                     |B             |B                |\n|67   |C                 |C     |C            |C                     |C             |C                |\n|68   |D                 |D     |D            |D                     |D             |D                |\n|69   |E                 |E     |E            |E                     |E             |E                |\n|70   |F                 |F     |F            |F                     |F             |F                |\n|71   |G                 |G     |G            |G                     |G             |G                |\n|72   |H                 |H     |H            |H                     |H             |H                |\n|73   |I                 |I     |I            |I                     |I             |I                |\n|74   |J                 |J     |J            |J                     |J             |J                |\n|75   |K                 |K     |K            |K                     |K             |K                |\n|76   |L                 |L     |L            |L                     |L             |L                |\n|77   |M                 |M     |M            |M                     |M             |M                |\n|78   |N                 |N     |N            |N                     |N             |N                |\n|79   |O                 |O     |O            |O                     |O             |O                |\n|80   |P                 |P     |P            |P                     |P             |P                |\n|81   |Q                 |Q     |Q            |Q                     |Q             |Q                |\n|82   |R                 |R     |R            |R                     |R             |R                |\n|83   |S                 |S     |S            |S                     |S             |S                |\n|84   |T                 |T     |T            |T                     |T             |T                |\n|85   |U                 |U     |U            |U                     |U             |U                |\n|86   |V                 |V     |V            |V                     |V             |V                |\n|87   |W                 |W     |W            |W                     |W             |W                |\n|88   |X                 |X     |X            |X                     |X             |X                |\n|89   |Y                 |Y     |Y            |Y                     |Y             |Y                |\n|90   |Z                 |Z     |Z            |Z                     |Z             |Z                |\n|91   |[                 |%5B   |%5B          |%5B                   |%5B           |%5B              |\n|92   |\\                 |%5C   |%5C          |%5C                   |%5C           |%5C              |\n|93   |]                 |%5D   |%5D          |%5D                   |%5D           |%5D              |\n|94   |^                 |%5E   |%5E          |%5E                   |%5E           |%5E              |\n|95   |_                 |_     |_            |_                     |_             |_                |\n|96   |`                 |%60   |%60          |%60                   |%60           |%60              |\n|97   |a                 |a     |a            |a                     |a             |a                |\n|98   |b                 |b     |b            |b                     |b             |b                |\n|99   |c                 |c     |c            |c                     |c             |c                |\n|100  |d                 |d     |d            |d                     |d             |d                |\n|101  |e                 |e     |e            |e                     |e             |e                |\n|102  |f                 |f     |f            |f                     |f             |f                |\n|103  |g                 |g     |g            |g                     |g             |g                |\n|104  |h                 |h     |h            |h                     |h             |h                |\n|105  |i                 |i     |i            |i                     |i             |i                |\n|106  |j                 |j     |j            |j                     |j             |j                |\n|107  |k                 |k     |k            |k                     |k             |k                |\n|108  |l                 |l     |l            |l                     |l             |l                |\n|109  |m                 |m     |m            |m                     |m             |m                |\n|110  |n                 |n     |n            |n                     |n             |n                |\n|111  |o                 |o     |o            |o                     |o             |o                |\n|112  |p                 |p     |p            |p                     |p             |p                |\n|113  |q                 |q     |q            |q                     |q             |q                |\n|114  |r                 |r     |r            |r                     |r             |r                |\n|115  |s                 |s     |s            |s                     |s             |s                |\n|116  |t                 |t     |t            |t                     |t             |t                |\n|117  |u                 |u     |u            |u                     |u             |u                |\n|118  |v                 |v     |v            |v                     |v             |v                |\n|119  |w                 |w     |w            |w                     |w             |w                |\n|120  |x                 |x     |x            |x                     |x             |x                |\n|121  |y                 |y     |y            |y                     |y             |y                |\n|122  |z                 |z     |z            |z                     |z             |z                |\n|123  |{                 |%7B   |%7B          |%7B                   |%7B           |%7B              |\n|124  |&#124;            |%7C   |%7C          |%7C                   |%7C           |%7C              |\n|125  |}                 |%7D   |%7D          |%7D                   |%7D           |%7D              |\n|126  |~                 |~     |~            |~                     |%7E           |~                |\n|127  |删除              |%7F   |%7F          |%7F                   |%7F           |%7F              |\n\n总结一下：\n\n- 非保留字符的处理上，非常一致（除了`~`）\n- JS的`encodeURI`方法保留了很多符号，这些符号没有进行编码\n- JS的`encodeURIComponent`方法相比PHP和Python，少了`!`、`'`、`（`、`）`、`*`这5个字符的编码\n- PHP的`urlencode`方法将空格编码成了加号（`+`），且对`~`进行不必要的编码\n- Python默认没有对`\u002F`进行编码，需要显式指定`safe=''`才会进行编码（`urllib.parse.quote(str, safe='')`）\n\n如果按照业界“非保留字符一律进行编码”的实践规则来看，那么Python（指定`safe=''`）和PHP（`rawurlencode`）是符合要求的，而JS的`encodeURIComponent`则需要针对额外的5个字符打补丁。\n\n```javascript\nfunction fixedEncodeURIComponent (str) {\n  return encodeURIComponent(str).replace(\u002F[!'()*]\u002Fg, function(c) {\n    return '%' + c.charCodeAt(0).toString(16);\n  });\n}\n```\n\n## 善后\n\n因为结论比较明显，业界和主流语言都采用了比较一致的规则，因此最终这个项目的鉴权部分也进行了修改，除了非保留字符外，其他的字符都需要进行百分号编码。\n\nurlencode由于规范中没有规定得非常细致，将很多细节交给了实现，因此导致各语言的处理并不一致。当然如果能提前预知有这样的问题，有可能在选择方案的时候就不会选择urlencode这么一种“不太确定”的编码规则。\n\n> 如果你认为JSON大法好，恭喜你即将进行另一个坑：不同语言在JSON编码的处理上也不一致，例如中文要不要变成unicode格式、斜杠要不要编码等等。\n\n大概一位前端工程师也没想到有一天需要在后端处理“兼容性问题”。好在处理兼容性问题的原则是一致的：找到差异点、抹平它。\n","\u002Farticle\u002Fwork-with-urlencode.html","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fwork-with-urlencode\u002F1.png",{"id":67,"type":7,"slug":68,"title":69,"date":70,"category":11,"tags":71,"body_markdown":73,"permalink":17,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":21},189,"nodejs-in-frontend-dev","【问答】为什么前端越来越复杂？Node.js有什么作用？","2020-03-18 23:47",[72],"Node.js","\n本文来自知乎问题：[为什么要把前端搞的这么复杂，UI 组件不是很好用吗， 难道就是为了推广 nodejs 和 npm 吗?](https:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F378533373\u002Fanswer\u002F1088512439)\n\n这个题目中有无数槽点：\n\n画个界面只要快就好了吗？JS类库能帮你解决渲染慢的问题？类库和组件能帮你解决所有的兼容问题？不需要高并发所以就不需要架构？\n\n这里的每一条都值得展开来反驳，但鉴于题主的主要疑问不在这里，就不跑题。\n\n这个问题的核心，抽取一下就是两个：\n\n1. 为什么前端越来越复杂了\n2. Node.js在前端开发中是干什么的\n\n\u003C!-- more -->\n\n先回答第2个问题。在前端开发中，现在的确基本上都会使用Node.js，但事实上，绝大部分前端工程师只是需要安装一下Node.js而已，一行Node.js代码都不用写。因此Node.js在这里只是一个工具平台，它为前端各种工程化的工具提供一个可执行的环境，仅此而已。打个不是特别恰当的比喻，很多软件在安装的时候都需要编译，编译过程中需要python，这个时候你要做的仅仅是安装一下python，并不需要写python代码。\n\n第1个问题，前端为什么越来越复杂了。\n\n比较欣慰的是，题主好歹说了“代码分布合理，清晰”，要不然实在是聊不下去。JS作为一个20多年的语言，在语言特性上是缺失很多现代软件工程的特性的，典型的就是模块化。你写Java，一个文件就是一个文件，一个包就是一个包，跨文件跨包的代码不会有互相影响吧？要引用只需要import一下就好了吧？那JS呢？\n\nJS的模块化从AMD诞生开始，一直到Node.js的CommonJS规范，中间有十年左右的时间是没有官方方案的，全靠社区想各种办法，从一开始的命名空间，到AMD\u002FCommonJS，全都不是语言层面的官方支持。而这些规范中，只有AMD可以在不编译的情况下运行，CommonJS直到今天都是不可以直接在浏览器中使用的，所以“编译”的第一个用处，在这里——解决JS模块化的问题。\n\n即使AMD可以不编译直接在浏览器中运行，也会面临另一个问题：文件碎片化。大家都是喜欢组织代码的程序员，一旦模块可以不互相影响了，那一定会导致原来在同一个文件中的代码被拆分成更多的子模块。于是有可能你一个页面就要加载几十上百个JS文件（一个文件一个模块），于是你仍然要想办法把它们进行合并打包，于是“编译”的第二个用处出现——合并JS文件，减少HTTP请求。\n\n好了，再回到题主题到的UI组件库和JS类库。这些东西基本上都不是你自己开发的对吗？意味着你要使用别人的代码。那么问题来了，你要如何引入别人的代码？在java中，你可以用marven，在前端代码中呢？你自然可以直接复制代码到项目中，但你要如何管理呢？难道包管理是只有java程序员可以用，并不是呀，写windows的写mac的写android的写ios的都可以用包管理来引入第三方的代码，凭什么写前端的不能这么干呢？于是有了bower，但是bower死了，只剩下了npm。所以npm就成了前端御用的包管理软件。那你下载这么多包了，在代码中要怎么找到这些包？require('jquery')怎么就能找到node_modules\u002Fjquery\u002Fdist\u002Fjquery.min.js呢，又要怎么变成浏览器可访问的呢？于是“编译”的第三个用处出现——可复用软件包的寻址。\n\n“编译”当然还有第四个用处，那就是处理兼容性。这里的兼容性和题主理解的UI组件\u002FJS类库处理的兼容器不是一回事，主要是指语言的兼容性。ES5之后ES6 7 8 9一直在不断出现，JSX、TypeScript等新玩意也在不断出现，但问题是浏览器并不支持或并不完全支持这些玩意，那就不能玩了吗？要等所有浏览器都支持再玩吗？能通过编译一下让大家都愉快地玩起来，何乐而不为？\n\n还有第五个用处吗？有的呢，有了编译，意味着代码的组织有更多可能性，你会看到Vue单文件组件，你会看到Web Components的代码，你会看到在JS中引入CSS的用法，你会看到styled components \u002F scoped css \u002F css modules等等各种神奇的玩法。然而归根结底，他们在做什么，无非是在追求题主所说的“代码分布合理、清晰”。\n\n代码这个世界之所以好玩，就是因为你可以玩出花，而不是被别人限制，对不起，你只能这么玩。\n\n题主觉得用UI组件库\u002FJS类库对前端来说就足够了，但是对前端工程师来说，对不起，这不是我们想玩的玩法，我们就想让前端也可以更好玩一些，不要天天去羡慕做别的语言\u002F平台的工程师。作为一个局外人，你可以不理解，你也可以选择你认为舒服的方式，但是请不要随意质疑别人觉得舒服的方式。\n\n写个HTML，放个jQuery，放个bootstrap，$.ajax()写起来，没有人会鄙视你的。但是我vue-cli \u002F npm install \u002F npm run dev，也不要鄙视我好吗？大家各自开心。\n",{"id":75,"type":7,"slug":76,"title":77,"date":78,"category":11,"tags":79,"body_markdown":82,"permalink":83,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":21},186,"sequelize-tricks","Sequelize的一些小技巧","2019-11-10 21:36",[72,80,81],"Sequelize","ORM","\n[Sequelize.js](https:\u002F\u002Fsequelize.org\u002F)是一个用于Node.js的数据库ORM库，支持Postgres、MySQL\u002FMariaDB、SQLite、SQL Server等引擎。\n\n本文记录一些团队在使用Sequelize过程中积累的经验教训。\n\n## 介绍\n\nORM即Object Relational Mapping，中文叫“对象关系映射”。简单地说就是可以将数据库的各种对象（表、字段）及关系映射为程序语言的对象和关系，从而使开发者不需要直接操作数据库，转而操作对象即可。\n\n例如，将表`user`映射为模型`User`后，从数据库中查询`id`为`1`的用户就可以直接调用`findOne()`方法：\n\n```javascript\nconst user = await User.findOne({\n    where: {\n        id: 1\n    }\n});\n```\n\n这样做会带来几个明显的好处：\n\n1. 降低开发难度：ORM都有完善的文档，几乎所有的操作只需要按文档调用指定方法即可，不需要自己拼接SQL\n2. 提升安全性：ORM会处理好SQL注入问题，不需要开发者关注\n3. 降低封装复杂度：公共逻辑可以基于ORM封装，非常方便\n\n> 下文不区分“模型”和“Model”，均指Sequelize中与数据表对应的数据模型。\n\n\u003C!-- more -->\n\n## 命名\n\n团队合作中统一大家的命名规则是很重要的事情，因此一般稍微规范一些的团队都会有比较详尽的命名规范。但是不同地方的命名规则却不一定完全一致，例如：\n\n- 数据库规范：表名及字段名使用小写字母，单词间以下划线分隔\n- JS编码规范：变量命名使用驼峰式命名（即首字母小写，后续单词的首字母大写）\n\n这种情况可以通过Sequelize模型定义来解决，直接指定表名和字段名即可：\n\n```javascript\nsequelize.define('targetInfo', {\n    targetId: {\n        type: DataTypes.INTEGER(11),\n        allowNull: true,\n        field: 'target_id',\n    },\n}, {\n    tableName: 'target_info',\n});\n```\n\n上例中的`target_id`字段，在使用Sequelize的Model时就可以使用`targetId`属性来访问`target_id`字段，完全遵守JS编码规则。\n\n## 软删除 & 自动管理时间戳\n\n很多时候，因为保留痕迹、灾难恢复等各种原因，在设计技术方案时，我们都会使用一个字段来标记数据是否被删除。当业务需要删除数据时，只需要改变这个标记即可，而不是真的删除数据库记录。\n\n但是选择这种方案的同时，却会为业务带来一些复杂性，即每一个查询都需要考虑删除标记的状态。作为一个合格的ORM库，Sequelize也很贴心地提供了软删除的支持。在开启这个特性后，开发者不需要关注数据记录是否已被删除，只需要正常地使用查询、删除等操作即可，Sequelize会在执行对应的SQL查询前自动加上软删除的条件。\n\n具体的操作非常简单：\n\n1. 数据库和模型文件添加`deleted_at`字段\n2. 在模型定义的选项中加上`paranoid: true`选项\n\n此后，被删除的数据记录的`deleted_at`会记录被删除的时间，而没被删除的记录`deleted_at`为`NULL`。\n\n除了软删除外，记录的建立和更新时间也可以交给Sequelize来管理，操作同样简单：\n\n1. 数据库和模型文件添加`created_at`和`updated_at`字段\n2. 在模型定义的选项中加上`timestamps: true`选项\n\n这样定义之后，数据建立时会自动记录创建时间到`created_at`字段中，而当数据发生修改时，`updated_at`会自动记录更新时间。\n\n## 关联\n\n多表的查询在数据操作中也是一个比较常见的操作。Sequelize也可以让我们指定模型之间的关联（且有完善的1:1、1:n、m:n关联）。一旦指定完成，则可以在查询数据时直接带出关联数据。\n\n例如每一个会议（`Meeting`）有多个参会者（`Participator`），在查询会议时可以直接拿出参会者信息：\n\n```javascript\n\u002F\u002F 指定1:n关联\nMeeting.hasMany(Participator);\n\n\u002F\u002F 查询会议\nconst meeting = Meeting.findOne({\n    where: {\n        id: 1,\n    },\n    include: [Participator],\n});\n```\n\n接下来访问`meeting.participators`即可获得会议参会者列表。\n\n如果希望关联数据进行排序，则可以直接在查询中指定`order`排序规则。但是这个排序和直观想法不太一样，从SQL的角度来讲，关联数据的查询无论是用多表查询还是`join`，都没有办法单独对关联表单独排序，因此不管怎么排序会影响主表的排序。所以如果要对关联数据排序，最好将主表的排序依据写在前面：\n\n```javascript\n\u002F\u002F 查询会议\nconst meeting = Meeting.findAll({\n    where: {\n        id: 1,\n        \u002F\u002F 排序\n        order: [\n            \u002F\u002F 先对主表排序\n            ['id', 'asc'],\n            \u002F\u002F 再对关联表排序\n            [Participator, 'id', 'asc'],\n        ],\n    },\n    include: [Participator],\n});\n```\n\n## Model Diff\n\n之前团队碰到了一个需求：在同步数据的同时记录数据发生的变化情况。一开始的想法是在同步前先读取一次数据，等数据同步完之后，再读取一次数据，然后对两次数据进行对比和记录。但是在深入了解Sequelize之后，发现这个事情有更简单的解法。\n\nSequelize的Model在结构上是有记录两个值的，内部分别用`_previousDataValues`和`dataValues`记录。其中`_previousDataValues`表示从数据库中读出来的原始记录，而`dataValues`则记录Model经过一些操作之后的新值。例如`.set()`方法会改变`dataValues`的值，但不会改变`_previousDataValues`的值。但是如果调用`.save()`方法，则新值会写入数据库，`_previousDataValues`也会改变。\n\n因此，我们可以将数据保存的过程分为设置新值和保存到数据库两步，并且从中获取数据的变更：\n\n1. 通过`findOne()\u002FfindAll()`读取原值获取Model\n2. 通过`.set()`方法设置Model的新值\n3. 读取原值和新值的变化\n4. 通过`.save()`方法保存数据\n\n而具体的第3步，获取变化，Sequelize也有提供一些帮助：`.changed()`方法可以返回所有发生变更的字段名，`.previous()`方法在不传参数的情况下，会返回仅包含变化字段的原数据，可以直接作为记录变化的原值。而新值则只要拿到发生变更的字段名列表，然后新值即可。\n\n```javascript\n\u002F\u002F 获取一个模型发生变化的值\nconst getChanges = function(model){\n    const changedFields = model.changed();\n    if(!changedFields){\n        return false;\n    }\n    const oldValue = model.previous();\n    const newValue = {};\n    changedFields.forEach((field) => {\n        newValue[field] = model[field];\n    });\n    return {\n        oldValue,\n        newValue,\n    };\n};\n```\n\n## 事务\n\n事务是数据库的一个很重要的特性，它的最重要的一个应用场景即是将一系列的数据库操作原子化——要么全部成功，要么全部失败。上一节提到的场景，在提交数据变更本身的同时记录数据变更情况即是一种典型的适合使用事务的场景。\n\nSequelize也提供了事务的支持，在使用时先初始化一个事务对象，然后在进行数据操作时传入事务对象即可，没有很特别的地方，仅仅是作为一个记录，在适当的场景下记得使用它即可。\n\n下面的例子删除了一堆数据，并且新增了一堆与之对应的日志：\n\n```javascript\nawait sequelize.transaction((t) => {\n    return Promise.all([\n        \u002F\u002F 创建删除日志\n        Event.bulkCreate(models.map((item)=>{\n            return {\n                type: 'delete',\n                targetId: item.id,\n                oldValue: JSON.stringify(item),\n                newValue: null,\n            };\n        }), {transaction: t}),\n        \u002F\u002F 删除数据\n        Model.destroy({\n            where: {\n                id: {\n                    [Op.in]: deleteIdList\n                }\n            }\n        }, {transaction: t})\n    ]);\n});\n```\n\n## Model扩展\n\nSequelize的Model有很丰富的内部结构，但在进行JSON输出（`JSON.stringify`）的时候，却只会输出模型的数据，不需要进行其他的额外处理。在大部分情况下这种处理是合适的，但是在某些情况下，我们仍然需要对数据进行一些处理，例如字段扩展或者字段裁剪。\n\n在这种情况下，我们可以对Model进行扩展，添加一些最终输出前进行整理的代码：\n\n```javascript\nModel.protoype.output = function(){\n    \u002F\u002F 只输出指定的字段，且根据需要格式化\n    return {\n        foo: this.foo,\n        bar: this.bar + '@toobug.net'\n    }\n}\n```\n\n> 在Sequelize 4中，我们使用的Model的原型并不是Model本身，而是Model.Instance，所以需要在`Model.Instance.prototype`上定义方法才有效。\n\n在实际使用的过程中，我们还可以将这个过程整理得更加工程化：\n\n1. 定义一个`extend`目录，专门定义对每个Model的扩展，且命名与Model定义一一对应\n2. 在初始化的时候读取所有的Model（`sequelize.models`），然后一一读取对应的`extend`，并扩展到原型上\n\n这样在以后需要扩展模型的时候，只要在`extend`目录下定义对模型的扩展方法即可，不用再手工操作Model。\n\n## Model生成\n\nSequelize Model的使用非常方便，但是手写模型的过程并不太愉快。一个字段就要定义类型、默认值、是否为NULL、字段名等，如果一个表有30个字段，则光写模型定义就需要写100多行。\n\n事实上，如果已经建好了数据库表，则这些模型定义的内容基本上都可以从数据库中读取出来。`sequelize-auto`正是做这件事情的库，它会连接数据库，并从数据库中读出所有表，生成对应的模型文件。\n\n它的使用很简单，首先作为工具安装`sequelize-auto`模块（`npm i sequelize-auto -g`或者不加`-g`安装到项目中），然后在命令行中调用它即可，例如：\n\n```sh\nsequelize-auto -h 127.0.0.1 -d database -u username -x password -p 3306 -C -a .modelConfig.json -o server\u002Fmodels\u002Fmodels\n```\n\n参数\n\n- `-h`\u002F`-p`\u002F`-d`\u002F`-u`\u002F`-x`，数据库的IP、端口、数据库、用户名、密码\n- `-C`，使用驼峰式命名规则\n- `-a`，模型的配置项\n\n其中模型的配置项是一个JSON文件，这些配置项会出现在生成的Model文件中，作为模型的配置，例如：\n\n```json\n{\n    \"paranoid\": true,\n    \"timestamps\": true\n}\n```\n\n生成模型后，使用`sequelize.import(modelFilePath)`即可引入模型，然后愉快地使用。\n\n> sequelize-auto略有些年久失修，比如生成的模型中还有jshint的注释，且缩进是2空格。如果与你的项目规范不符，可以在生成后再加一个`eslint --fix`或者`prettier`之类的工具进行格式化即可。\n","\u002Farticle\u002Fsequelize-tricks.html",{"id":85,"type":7,"slug":86,"title":87,"date":88,"category":11,"tags":89,"body_markdown":91,"permalink":17,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":21},187,"why-babel-exists","【问答】为什么会允许babel这种解析工具的存在？","2019-03-28 18:21",[57,90],"babel","\n本文来自知乎问题[为什么会允许babel这种解析工具的存在？](https:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F315934143\u002Fanswer\u002F634975427)\n\n希望提问者真的没有在调侃……因为在我看来，这有点像“何不食肉糜”的提问了。\n\nES6又名ES2015，也就是在2015年定稿的，在定稿之前其实大家已经讨论了很久了。但是光讨论有什么用呢？没有任何一个环境是支持ES6运行的。所以就讨论讨论再讨论，然后大家一拍桌子，好，定稿？\n\n事实上在ES2015之前，ES5可能就是这么定下来的，ES4可能也是这么废弃的。\n\n这时候，就有个神奇的东西，叫6to5出现了，它的第一次提交出现在2012年9月。Initial import · babel\u002Fbabel@aedcd4e 它的作用就是把ES6的代码编译成ES5的代码，它的神奇之处就在于，虽然一个能支持ES6的环境都没有，但是我们仍然可以使用ES6来编写代码。这是一种前所未有的模式，甚至在其它语言中都没有出现过这种模式。（希望不是孤陋寡闻，至少py3 -> py2是没有见到类似工具的。）\n\n于是，我们可以在规范还没有定稿的时候就先用用看，用着觉得不爽了再回去修改规范。这样是不是比拍桌子要科学得多？事实上现在的ES规范制定过程就是这么干的，定了stage 0到stage 4等几个级别，而且规定了需要在多少个环境中先验证，验证完之后才可以定稿发布。基本上可以毫不客气地说，这个东东就是由6to5开创的新局面。\n\n\u003C!-- more -->\n\n当然，在规范发展过程中，浏览器、Node.js等环境也在不断实现ES6规范的特性，6to5的作者自然也清楚，等环境都支持ES6了，可能就没我什么事了。（像题主这样，认为大家都支持就好，不需要编译了。）所以这个作者又开了个脑洞，首先将6to5做了个改名，以便让它的适应范围更广，要不然以后ES 7\u002F8\u002F9出来的时候玩得名不正言不顺的嘛。然后，将所有的转译过程全部插件化，你可以自由选择某个ES6\u002F7\u002F8特性是否要做转换。这样作为一个转译插件，它就可以更好、更长久地生存下去，也能持续为JS社区带来价值。不得不说，作者真是个天才。（据说写6to5的时候还是个高中生……）\n\n这个神奇的6to5后来改的名字，就叫babel。可以说，没有babel就没有JS社区今天在语言规范上的高度繁荣。也就没有提主提问的前提存在了。\n\n（当然，也要承认，在6to5同期也有其它的转译插件，例如来自Google的traceur，但babel一开始在转译后的代码可读性上就有优势，后期改名和插件化又是神来之笔，因此很快胜出。）\n\n再来说今天仍然使用babel的重要性。\n\n首先一个很重要的点，用户用什么鬼玩意你是无法决定的。虽然现在主流浏览器对ES6的支持都能到99%以上，但是如果用户用的safari 8呢？用的IE9呢？用的Android 4.0呢？这一点很多答主都已经解释得很清楚了，不再多说。\n\n第二个点其实在看完上面一大段后也容易理解了，babel != ES6，并不是只有ES6才可以用babel呀。事实上你用的ESM模块化规范import\u002Fexport，并不属于ES6规范，你用的async\u002Fawait也不属于ES6规范，你用的666的解构，也有一部分不属于ES6规范……但是非常幸运的是，babel并不苛求你知道它属不属于ES6，而是默默都帮你处理了。因此，后ES6时代，babel仍然可能长期存在。（这也再次印证作者眼光很毒！）\n\n第三个点，babel除了做ES规范的转译以外，也可以做做别的转译，比如JSX\u002FTS。但这个在我看来不属于babel的主业，而是人们发现babel的转译是万能的，既然在工具链中已经无法缺少了，顺手让它也帮帮忙也无可厚非。\n\n以上。均是个人理解，如有错漏，欢迎评论指正。\n",{"id":93,"type":7,"slug":94,"title":95,"date":96,"category":11,"tags":97,"body_markdown":100,"permalink":17,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":21},185,"work-with-npm-package-lock","如何与NPM package-lock.json愉快地玩耍","2018-07-26 19:40",[98,99],"npm","package-lock","\n## 背景\n\n对的，最近写文章都会交代一下背景。因为按标题的套路，这本应该是一篇教程类的文章，但这种文章其实挺无趣的。之所以想写这篇，是因为确实碰到了一些很麻烦的事情。闲言少叙，我们进入正题。\n\n最近我们前端代码打包正在接入Gitlab CI，使用Docker来作为Executor，也就是在Docker中进行前端代码打包，然后收集打包结果，以备发布时使用。打包时Docker镜像很自然地就选择了官方Node镜像，最新版本（Node 10）。\n\n一开始我们尝试性地接入了几个项目，有使用NPM scripts进行打包的，也有使用Gulp进行打包的，一切都很正常。但是昨天在接入一个新项目，使用Gulp打包的时候，却突然碰到了报错：\n\n```sh\n$ gulp gitlab-ci\ngulp[85]: ..\u002Fsrc\u002Fnode_contextify.cc:631:static void node::contextify::ContextifyScript::New(const v8::FunctionCallbackInfo\u003Cv8::Value>&): Assertion `args[1]->IsString()' failed.\nAborted\nERROR: Job failed: exit code 134\n```\n\n看了一眼这个错误信息，一下子就发现，这并不是来自JS层的错误，而是来自Node原生层，这就超出了我的理解范围了。\n\n\u003C!-- more -->\n\n## 模块版本兼容问题\n\n通过搜索，首先找到一篇中文的解决方案：[升级node10后gulp报错的解决办法](https:\u002F\u002Fliyang.pro\u002Fafter-upgrading-node10-gulp-error-solution\u002F)。文中给出了如下解决办法：\n\n```sh\nrm -fr node_modules\nrm -fr package-lock.json\nnpm cache clean --force\nnpm install\n```\n\n所以这是什么原理呢？没看到文章有解释，另外这文章本身也写得不是很清楚。抱着半信半疑的态度，在CI的脚本中加上了`npm cache clean --force`，结果问题依旧。\n\n接着搜索了一下，很快发现一些和这个问题有关的issue：\n\n- nodejs\u002Fnode: [node 10.0.0安装模块时core dump](https:\u002F\u002Fgithub.com\u002Fnodejs\u002Fnode\u002Fissues\u002F20281)\n- nodejs\u002Fnode: [新发布的版本与Gulp ^3.9.0不兼容](https:\u002F\u002Fgithub.com\u002Fnodejs\u002Fnode\u002Fissues\u002F20285)\n- gulpjs\u002Fgulp: [将graceful-fs的升级合并回Gulp 3](https:\u002F\u002Fgithub.com\u002Fgulpjs\u002Fgulp\u002Fissues\u002F2146)\n- gulpjs\u002Fgulp: [node 10中崩溃](https:\u002F\u002Fgithub.com\u002Fgulpjs\u002Fgulp\u002Fissues\u002F2162)\n- isaacs\u002Fnode-graceful-fs [v3.x分支与Node.js master不兼容](https:\u002F\u002Fgithub.com\u002Fisaacs\u002Fnode-graceful-fs\u002Fissues\u002F120)\n- isaacs\u002Fnode-graceful-fs [v3.0.11依赖natives 1.1.1，后者与node 10不兼容](https:\u002F\u002Fgithub.com\u002Fisaacs\u002Fnode-graceful-fs\u002Fissues\u002F130)\n\n通过`npm ls`，一下子就看到了`natives`模块的版本是1.1.1，于是定位到真正的问题：`gulp`依赖`vinyl-fs`，`vinyl-fs`依赖`graceful-fs`，`graceful-fs`依赖`natives`，而`natives`在1.1.3之前都不兼容Node 10。\n\n那么解决方案就简单了，想办法升级一下模块不就好了？\n\n## 如何升级间接依赖\n\n首先想到的是Gulp是否有解决这个问题，`npm info gulp`看了一下，发现最新版本是`3.9.1`，然后`npm ls`一下，发现本机装的已经是`3.9.1`了，也就是说，Gulp根本没有升级的可能。那要如何升级这个间接的依赖呢？\n\n这里我绕了个弯子，`package.json`中直接依赖的模块无法升级，`npm update`也不能搞定。于是想到，如果显式安装一下这个模块，是不是能解决问题？\n\n```sh\nnpm install natives@1.1.3\n```\n\n安装完之后还要在`package.json`中将直接依赖`natives`删除（因为我并不直接依赖它，留着没用）。然后再次查看依赖：\n\n```sh\n▶ npm ls natives\nseed@1.0.0 \u002FUsers\u002FTooBug\u002Fwork\u002Foa\u002Flearn\u002Ffrontend\n└─┬ gulp@3.9.1\n  └─┬ vinyl-fs@0.3.14\n    └─┬ graceful-fs@3.0.11\n      └── natives@1.1.3\n```\n\n这次对了。\n\n但是，这个操作我是在本机进行的，这个操作到底改了什么呢？如果去CI上再次执行`npm install`，会不会依然安装到旧版本呢？毕竟`package.json`并没有任何改动。\n\n于是翻看了一下`git diff`，发现了本文的主角`package-lock.json`被修改了，其中的`natives`由1.1.1变成了1.1.3。\n\n```json\n\"natives\": {\n    \"version\": \"1.1.3\",\n    \"resolved\": \"http:\u002F\u002Fregistry.npm.oa.com\u002Fnatives\u002Fdownload\u002Fnatives-1.1.3.tgz\",\n    \"integrity\": \"sha1-RKV5vmRQfqLW7RygSpQVkVz3VVg=\"\n},\n```\n\n于是怦然大悟：本来npm在安装模块是是按语义化版本安装最新版的，但是在CI上却没有安装`natives` 1.1.3版本，而是安装了`natives` 1.1.1版本，这正是因为`package-lock.json`装版本锁定了，从而导致了与Node 10的不兼容问题。\n\n这也能很好地解释，为什么其它项目没有这样的问题，因为其它的项目在代码仓库中没有包含`package-lock.json`，在安装时自然就安装了`natives` 1.1.3版本。\n\n于是再回头看一开始搜到的文章提出的解决办法：\n\n```sh\nrm -fr node_modules\nrm -fr package-lock.json\nnpm cache clean --force\nnpm install\n```\n\n其实是非常有效的，因为它将`package-lock.json`直接删除了，然后重新安装一遍最新版本，生成新的`package-lock.json`，从而解决问题。\n\n## 延伸：如何正确地在项目中使用`package-lock.json`\n\n最后，也补充说明一下`package-lock.json`的正确用法。（虽然我知道，但还是不小心踩坑了。如果不清楚它的用法的话，可能会在坑里待更长时间爬不出来。）\n\n首先，需要确保`npm`的版本在5.6以上，因为在5.0 - 5.6中间，对`package-lock.json`的处理逻辑更新过几个版本。5.6以上的才是符合认知的逻辑。\n\n然后，在项目中如果没有锁版本的必要，可以不使用`package-lock.json`，在安装模块时指定`--no-lock`即可。\n\n如果项目中有`package-lock.json`，则安装模块时会以这个文件中指定的版本和地址为准，直接下载安装。（除非它和`package.json`中指定的版本不相符。）\n\n最后，如果已经锁定了版本的情况下，需要更新直接依赖，则直接安装指定版本，`package.json`和`package-lock.json`都会同步更新。如果需要更新间接依赖的话，则需要像本文这样，手工安装一下，保证`package-lock.json`被更新到。或者如果其它模块的锁定并不是那么重要时，也可以直接删除`package-lock.json`，然后重新安装一遍，相当于把所有（直接依赖+间接依赖）模块全部更新一遍。\n",{"id":102,"type":7,"slug":103,"title":104,"date":105,"category":11,"tags":106,"body_markdown":110,"permalink":17,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":111},182,"a-bug-in-wechat-work","记一次企业微信webview bug排查","2018-07-24 10:40",[107,108,109],"bug","debug","企业微信","\n## 背景\n\n咱们在企业微信上开发了一个简单的选课系统，用于让公司内的同学在企业微信上进行选课。昨天下午HR的同学很开心地推送了8月份的课程列表，让大家报名。然后就炸锅了：企业微信中点击课程没反应，报不了名！\n\n收到消息第一时间感觉确认了一下问题，发现Mac下使用完全没有任何问题。于是打听了一下，出问题的都是Windows电脑的企业微信。直接使用Chrome浏览器访问或者在手机企业微信上访问都是没有问题的。\n\n那就简单啦，赶紧引导大家在浏览器或者手机企业微信上访问。问题解决！顺便感慨一下：狡兔三窟真重要啊，还好我们及早打通了OA登录，在各种环境下都可以登录使用。\n\n当然，夜深人静的时候，作为一名靠谱的前端工程师，还是要老老实实查问题的。于是诞生此文，流水账，记录bug排查的全程并揭秘原因。\n\n\u003C!-- more -->\n\n## TLDR\n\n> Too Long, Didn't Read\n>\n> 太长了，不想读\n\n好的，说重点：\n\n1. Windows企业微信某个版本的webview有bug，导致`'ontouchstart' in windows`为true，事实上它并不支持触控事件。\n2. 这会导致第三方库`iscroll`产生误判，认为这是一个触控设备，从而错误地将`disableMouse`设为`true`，进而忽略`mousedown\u002Fmouseup`事件，导致内部不向外发出`click`事件。\n3. 最终导致外层绑定的`click`事件处理函数不触发，点击无效。\n\n## 排查过程\n\n### 重现\n\n首先，想办法重现。\n\n打开虚拟机，下载企业微信，登录、开应用，一顿操作猛如虎，然后……没发现问题。\n\n重现失败。\n\n接下来找反馈方。您的系统是什么版本？您的企业微信是什么版本？您是什么网络？得知是旧版企业微信。于是赶紧让组里小伙伴确认一下，是不是有问题的升级一下就没有问题了。\n\n是。\n\n锁定问题范围：旧版本Windows企业微信下页面出现点击无效。\n\n虚拟机下载了一个旧版本企业微信，覆盖安装，再次登录、开应用，一顿操作猛如虎，然后果然看到了问题所在。\n\n接下来，想办法在开发环境重现问题。开启本机环境，配置hosts访问本机，问题依旧，重现成功。\n\n### 确认问题\n\n再接下来，确认问题来源。\n\n因为企业微信没有调试工具或者调试接口，只能先引入`vconsole`模块，确保日志能被看到。\n\n```sh\nnpm install vconsole\n```\n\n```javascript\nvar VConsole = require('vconsole');\nnew VConsole();\n```\n\n![VConsole界面](\u002Fassets\u002Fweb\u002F2018\u002Fa-bug-in-wechat-work\u002F1.png)\n\n展示一下项目的Vue代码：\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"content\">\n        \u003Cdiv class=\"listWrapper\" ref=\"wrapper\">\n            \u003Cul>\n                \u003Cli v-for=\"course in courseList\" @click=\"chooseCourse\">{{course.name}}\u003C\u002Fli>\n            \u003C\u002Ful>\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport IScroll from 'iscroll';\n\nexport default {\n    created(){\n        this.scroll = new IScroll(this.$refs.wrapper, {\n            click: true,\n            tap: true,\n            wheel: true,\n            mouseWheel: true,\n        })\n    },\n    methods:{\n        chooseCourse(){\n\n        }\n    }\n}\n\u003C\u002Fscript>\n```\n\n核心逻辑是，`.listWrapper`部分会被`iscroll`模块处理，以便能够方便地使用无限滚动、滚动加载等功能。\n\n于是将`@click`分别绑定到`.listWrapper`内部和外部，发现外部的点击可以响应，内部的点击无法响应。\n\n问题被进一步缩小范围：`iscroll`导致的元素点击事件不触发。\n\n接下来就想到将代码中初始化过的`iscroll`实例对象的`options`属性打印出来进行对比，看看是否初始化的时候环境检测结果有些不一样。果然发现了一点区别：\n\n没有问题的浏览器上：\n\n```\ndisableMouse: true\ndisablePointer: false\ndisableTouch: true\n```\n\n有问题的浏览器上：\n\n```\ndisableMouse: true\ndisablePointer: true\ndisableTouch: false\n```\n\n可以看到`disablePointer`和`disableTouch`的值是不一样的。开始翻看`iscroll`的源码\u003Chttps:\u002F\u002Fgithub.com\u002Fcubiq\u002Fiscroll\u002Fblob\u002Fmaster\u002Fbuild\u002Fiscroll.js>，发现它的基本逻辑：\n\n构造函数`IScroll()`从第317行开始，首先处理了选项的合并。\n\n第331行有三个关键的选项：\n\n```javascript\ndisablePointer : !utils.hasPointer,\ndisableTouch : utils.hasPointer || !utils.hasTouch,\ndisableMouse : utils.hasPointer || utils.hasTouch,\n```\n\n于是，往回翻，找到`utils`中这几个东东的定义：\n\n```javascript\nhasTouch: 'ontouchstart' in window,\nhasPointer: !!(window.PointerEvent || window.MSPointerEvent), \u002F\u002F IE10 is prefixed\n```\n\n因此`hasTouch`和`hasPointer`的值不同会导致上述选项`disableTouch`和`disableMouse`的不同。再接下来就简单了，将这两个值分别打印出来，很快就能发现，正是旧版企业微信的`hasTouch`判断失误，导致了后续`disableMouse`为`true`，导致鼠标`mousedown`\u002F`mouseup`事件相关处理函数没有被调用。\n\n值得注意的是，没有问题的浏览器，`disableMouse`也为`true`。这里经过调试跟踪，发现这部分浏览器也没有走鼠标事件`mousedown`\u002F`mouseup`，而是走了`pointerdown`\u002F`pointerup`事件。于是caniuse了一下，发现`Pointer`事件从Chrome 55开始支持的，而出问题的企业微信的webview使用的是Chrome 49。因此这个最后的保险，`Pointer`事件也失效了。\n\n![Pointer事件兼容性](\u002Fassets\u002Fweb\u002F2018\u002Fa-bug-in-wechat-work\u002F2.png)\n\n### 修复\n\n超级简单，只需要在调用的时候传入`disableMouse:false`即可。\n\n```javascript\nthis.scroll = new IScroll(this.$refs.wrapper, {\n    disableMouse: false,\n    click: true,\n    tap: true,\n    wheel: true,\n    mouseWheel: true,\n})\n```\n\n### 复盘\n\n问题解决了，自然要回想一下，是什么导致了这个问题，我还能做点什么。\n\n导致问题的直接原因是：企业微信的webview有一个bug，明明不支持触控，但是在`'ontouchstart' in window`时却返回了`true`。又因为新版本的企业微信已经没有了这个问题，所以要反馈给企业微信也是一件无意义的事情了。\n\n另一个有一些关系的原因是：引入了一些没有把握的第三方库。事实上我们对于第三方库的引入一直非常谨慎，但这个系统因为早期并不是一个正式的项目，因此有些随意了。\n\n再接下来就是记录一下这整个过程，整理成本文，虽然并不知道对其他人是否有一些思路上的借鉴意义。\n\n值得一提的是，在修复的过程中，发现`iscroll`的源码早已不是我很多年前看的时候的样子了，整个模块划分非常清晰，所以代码读起来很容易。接下来有空的时候我应该会再仔细读一下它的代码，关于事件的检测、分发还是做得挺有意思的。\n\n修复的过程让我想到了知乎上看过的一个问题：我去修电脑，别人只飞了一根线，却要了我100元，合理吗？下面有个回答：线值0.5元，知道怎么飞这根线值99.5元。\n\n对工程师来说也一样，修复的过程只有一行代码，但找到这个方案的过程却并不容易。比如我，因为这个问题的出现，半夜11点半才下班。\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2018\u002Fa-bug-in-wechat-work\u002F1.png",{"id":113,"type":7,"slug":114,"title":115,"date":116,"category":11,"tags":117,"body_markdown":120,"permalink":17,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":21},184,"what-does-frontend-architect-do","【问答】2018年的前端是否有『架构』可言?","2018-05-28 21:52",[118,119],"前端","架构","\n本文来自知乎问题[2018年的前端是否有『架构』可言?](https:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F278925914\u002Fanswer\u002F403803226)\n\n## 牛角尖\n\n一，明确下架构的定义，按题主的说法，“整个后端的架构是非常复杂和庞大的，一个好的架构师需要在数不清的方案组合中进行架构选择”。\n\n所以，回答前端的方案组合很多，是否就可以回应这个问题了。那么，前端架构的方案多吗？多如牛毛吧，比后端多很多很多吧。\n\n-------------------以上是牛角尖，以下是正经回答------------------\n\n## 一，关注点\n\n后端架构的目标，题主也说到了，高性能、高可用、可扩展、安全。\n\n至于后面说的那么多知识点，虽然都是有用的，但是不免有些掉书袋了。你说高可用有各种难点，有各个方面，但事实上这些问题的解决方案也都非常明确了。如果不明确的也是业界公认暂时无解的问题，例如分布式CAP等。至于其中的具体策略，如果还需要自己去想，那这个后端工程师（还不叫架构师）应该是不合格的。做后端的架构其实基本上是选择。\n\n那么前端呢？目前是高性能、高可用、可扩展、安全？所以数据库不用选，语言不用选，框架三选一，架构就简单了？我认为这是对前端的关注点理解不到位的体现。\n\n## 二，前端的核心关注点——用户体验\n\n作为前端工程师，关注点是什么呢？首先就是用户体验。\n\n用户访问我的网站快不快，用起来爽不爽，会不会觉得卡，会不会觉得low。这才是前端最该关注的东西。\n\n所以我做的架构一点都不高大上。如何让网站加载更快，如何不引入不必要的代码，把不必要的代码干掉，如何让用户加载最少的内容，如果让用户最快看到效果，前端渲染还是后端渲染，同步输出还是异步输出。\n\n是不是觉得一点都不是架构师干的事？但前端构架师确实就是在干这些事。\n\n诸如此类的事情非常多，比如报错是红色文字还是输入框变红？Loading放顶上还是页面中间？转菊花还是骨架屏？先显示占位图还是先白屏？\n\n所以，你要说前端技术架构师，还不如说是用户体验架构师。跟后端不在一个维度上。\n\n## 三，工程难度\n\n题主说，“最多算上错误监控、埋点方案、缓存策略等偏运维的决策”，仿佛这些是一句话可以搞定的很简单的事情。\n\n是，后端错误监控不难，无非try..catch嘛，再不挤进程挂了还有运维工具可以救人于水火。但是前端呢？一个ajax加载失败了怎么监控，一个css加载失败了怎么监控，再极端一点，浏览器卡死了怎么监控，页面崩溃了怎么监控？\n\n至于埋点，能做得好的我都非常佩服。连用户关闭了你的页面都监控不到，还想通过埋点来获取业务和技术数据？\n\n至于缓存就更奇葩了，到底有没有缓存，到底是200还是304还是没有请求了，到底版本对不对，到底有没有被代理瞎搞，到底这个神奇的浏览器是怎么处理缓存的……别以为说一句“加个缓存”，就这么轻轻松松地搞定了。每一个把缓存玩好的前端工程师都值得尊敬。就不说更多的什么localStorage\u002FserviceWorker之类的了。\n\n说这一大段，并不是要说前端架构师搞这些很苦很累，你们要尊重我们。而是想说，这些不起眼的事情，不如想象的那么容易，很多时候架构师是在帮助工程师探路或者踩坑，把这些东西的技术方案搞定。\n\n## 四，面向团队\n\n前端工程化还不成熟，这是个切实的情况。架构师在项目选型的同时，有相当部分的精力会放到工程化方案的选型上。是否用webpack，是否用npm，是否用typescript，是否用ES6\u002F7\u002F8，是否要babel，是否用postcss。是否要CI，是否要自动打包发布。是否要分包加载，是否要抽取CSS文件……\n\n这些事，在后端可能不存在，在客户端可能也不存在，或者即使存在，也被IDE悄悄地代劳了。作为项目的选型者，写几个依赖，install一下就开工了。但是前端不行，前端这里有大量的工程化选型或者配置\u002F开发的任务。\n\n作为架构师，如何选择一套好用的、没有坑的语言\u002F构建工具，也不容易。\n\n我举个例子吧，大家可能觉得typescript很好，直接用不就好了吗？殊不知，typescript需要配置tsc或者webpack的ts-loader（现在babel也可以了），还有tsconfig文件，以及各种.d.ts文件。当然，即使这样，也有可能在写代码的时候报一堆无法解决的错误，需要你花大量的时间去研究为什么有这个做，怎样可以不报错（也可能根本就不能不报错）。当你在Vue中使用typescript时，你需要研究怎样让它识别Vue Component的类型，怎样做自动提示和类型检查。当你在ajax使用时会发现你要研究怎样将服务端返回的数据与某个类型映射起来（然后发现在runtime时毛用都没有）。\n\n前端架构师，也有相当多的精力在这上面。\n\n这是一个好的现状吗？未必，但现状确实如此，这个锅架构师不背谁来背？\n\n## 五，小结\n\n所谓架构，我理解是综合考虑目标、业界和团队，作为合理的方案选择，既能支撑业务的发展，又能令团队满意。如果能达到这个目标，自然就是一个好的架构。目前来看，前端要做到一个好的架构不容易，做的事情并不比后端少。\n\n至于说前端架构师做的所谓的这些架构的事情是否高大上，那是另一个没有答案的问题。\n",{"id":122,"type":7,"slug":123,"title":124,"date":125,"category":11,"tags":126,"body_markdown":129,"permalink":17,"excerpt_src":17,"media_type":17,"media_title":17,"media_author":17,"media_url":17,"rating":17,"layout":17,"pv":18,"admin_only":18,"created_at":19,"updated_at":19,"deleted_at":17,"status":20,"cover":130},183,"nginx-koa-use-http2-server-push","Nginx + Koa 开启http\u002F2 server push","2018-05-15 09:25",[49,127,128],"nginx","http2","\n一看这标题就是不准备好好写的。对的，最近特别忙，只能简单记录一下折腾的东西。\n\n## Nginx开启server push\n\n1. 升级nginx到1.13.9或以上版本（注意1.13.6修改http\u002F2的实现，与一些旧版本客户端不兼容，比如旧版Android okhttp）\n2. nginx配置中加上`http2_push_preload on`，表示使用preload header来作为server push标识\n\n## Node开启server push\n\nNode处理文件内容时加上preload header即可，例如：\n\n```\nlink: \u003C\u002Fmain.37d69167.css>; as=style; rel=preload, \u003C\u002Fmain.f06ad8b3.css>; as=style; rel=preload\n```\n\n此处比较科学的做法应该是使用一个中间件，在返回内容之前，根据要返回的HTML内容来处理preload header。\n\n因为我主要处理静态html文件，又用的koa，所以将主要逻辑放在了koa-static的`setHeaders`函数中。`setHeaders`主要用于在返回静态文件前设置自定义的header，刚好和server push的场景相符。\n\n\u003C!-- more -->\n\n主要逻辑：\n\n1. 读取html文件，使用正则表达式匹配出css和js文件的路径（如果有图片也可以一起）\n2. 将这些资源拼接成preload header的值\n\n```javascript\nconst htmlContent = fs.readFileSync(path, 'utf8');\nconst styleRegExp = \u002F\u003Clink(?:.*?)href=['\"]?([\\w\\.\u002F]+\\.css)['\"]?\u002Fg\nlet currentStyle;\nconst styleList = [];\nwhile(currentStyle = styleRegExp.exec(htmlContent)){\n    styleList.push(currentStyle[1]);\n\n}\nconst scriptRegExp = \u002F\u003Cscript(?:.*?)src=['\"]?([\\w\\.\u002F]+\\.js)['\"]?\u002Fg\nlet currentScript;\nconst scriptList = [];\nwhile(currentScript = scriptRegExp.exec(htmlContent)){\n    scriptList.push(currentScript[1]);\n}\nlet link = styleList.map((styleFile) => {\n    return `\u003C${styleFile}>; as=style; rel=preload`;\n}).join(', ');\nlink += ', ' + scriptList.map((scriptFile) => {\n    return `\u003C${scriptFile}>; as=script; rel=preload`;\n}).join(', ');\ncache[path] = link;\nres.setHeader('Link', link);\n```\n\n这样就可以实现http\u002F2 server push了。\n\n## 有缓存不再推送\n\n上面两步都超简单，网上的教程满天飞。这一个标题“有缓存不再推送”内容才是促成本文的原因。\n\n回到http\u002F2 server push的原理，浏览器访问`index.html`时，server除了返回html，还将css \u002F js \u002F image也一并推送回来，这样浏览器接受完之后，就不用再单独请求一次，从而加快页面的加载。\n\n但是这里有一个矛盾，如果我们的静态资源是有长缓存的，下一次请求的时候该推送还是不该推送呢？如果推送，则相当于是忽略了缓存，白白浪费带宽。\n\n到目前为止，这些仍然是网上文章中的主要结论。于是我就验证了一下，有缓存时是否真的会浪费带宽。于是我打开了`chrome:\u002F\u002Fnet-internals\u002F#http2`，然后找到了活跃的http\u002F2连接。在`Source Type`为`HTTP2 SESSION`那一栏中，可以看到详细的HTTP\u002F2通信过程。我截了一些图：\n\n首先是没有缓存的情况下，server push开启：\n\n1. 浏览器请求完html之后，发现了`PUSH_PROMISE`，按字面意思理解，也就是server承诺推荐这些资源\n    ![push promise](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F01.jpg)\n2. 接下来浏览器接受了这些推送的stream，把资源弄下来了\n    ![accept push](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F02.jpg)\n\n到这里一切正常。但是当资源有缓存时，再次请求，server push仍然开启的情况下：\n\n1. 浏览器收到PUSH_PROMISE，注意最后一行，`main.6e607578.js`的`stream_id`为`12`\n    ![push prmise stream id 12](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F03.jpg)\n2. 接下来搜索这个`stream_id=12`，找到一串看不懂的东西，看起来跟TCP窗口调整的逻辑很类似？\n    ![stream id 12](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F04.jpg)\n3. 再接着，罢工了……清楚地写着`Abandoned.`已放弃，应该是浏览器拒绝了这次推送，注意`stream_id`仍然是`12`\n    ![abandoned](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F05.jpg)\n\n也就是说，在有缓存的情况下，浏览器并不会傻乎乎地再接受推送。试验到这里后有点不可思议，于是又从两个方面做了验证。\n\n1. 网络带宽\n    这是`chrome:\u002F\u002Fnet-internals\u002F#timeline`的时间线，比较明显的有15个峰，分别是我发起的15次请求，前5次缓存有效，中间5次没有缓存，后5次缓存有效。可以明显看到，有缓存时带宽是比没缓存时低的，证实有缓存时push并不会真的发生。\n\n    ![abandoned](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F05.jpg)\n2. 服务端网络IO\n    在有缓存的情况下，服务端网络IO大约每个请求0.2-0.4M，在无缓存的情况下，服务端网络IO大约每个请求2.2M-2.4M。同样证实有缓存的情况下，push并不会发生。\n\n## 有缓存不再推送\n\n其实有上面的结论后，不需要再做什么了。但是仍然怀疑这是不是哪一端实现的bug，要不然为什么大家都把这当作一个不可解决的缺陷呢？\n\n那么就假装我不知道上面这一段吧，还是需要自己根据是否有缓存控制是否开启server push。\n\n最容易想到的办法就是cookies了，将已推送过的资源url放入cookies中，下次再请求时进行对比，已有的资源就不再推送。\n\n但是这样的话，cookies会非常庞大。于是有人提出了使用BloomFilter来存放推送信息。\n\nBloomFilter是一个空间和时间复杂度都比较小的算法，主要用于快速进行“有损”存在性判定。所谓存在性判定就是给定一个key，确定它是否存在。而“有损”的意思则是指它并不100%精准。\n\n它的原理可以简单这么理解：首先放一个数组，接下来每一个需要检查的key都做一个hash，映射到这个数组中的某几个位置，如果这几个位置全部为`1`，则认为这个key存在，否则认为这个key不存在。\n\n考虑到server push的场景，即使不精准也不影响页面打开使用，因此它是适用的。HTTP server软件H2O就是使用了类似的算法。\n\n本来我也打算这么实现一版，但是后来转念一想，我就一个页面，3个资源，好像没必要这么麻烦。不如直接全部hash一把，hash匹配就认为有缓存，hash不匹配就认为没缓存，全部重新推送一遍。\n\n于是就有了类似这样的代码：\n\n```javascript\nlet pushHash = md5(cache[path]);\nif(!cookie || cookie !== pushHash){\n    res.setHeader('Link', cache[path]);\n    res.setHeader('Set-Cookie', 'push=' + pushHash);\n}\n```\n\n简单粗暴有效。\n\n参考：\n\n- https:\u002F\u002Fimququ.com\u002Fpost\u002Fcache-aware-server-push-in-h2o.html\n- https:\u002F\u002Fwww.keakon.net\u002F2018\u002F03\u002F07\u002FNGINX%E6%94%AF%E6%8C%81HTTP\u002F2serverpush%E4%BA%86\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F01.jpg",46]