📒 一致性哈希算法解决的问题
- 大多数网站都是 多节点部署,需要根据不同场景使用不同的 负载均衡策略
- 最简单的算法就是使用 加权轮询,这种场景建立在每个节点存储的数据都是相同的前提下,访问任意一个节点都能得到结果
- 当我们想提高系统的容量,就会将数据水平切分到不同的节点来存储,也就是将数据分布到了不同的节点。加权轮询算法是无法应对「分布式系统」的,因为分布式系统中,每个节点存储的数据是不同的,不是说任意访问一个节点都可以得到缓存结果的
- 这种场景可以使用 哈希算法,对同一个关键字进行哈希计算,每次计算都是相同的值,这样就可以将某个 key 映射到一个节点了,可以满足分布式系统的负载均衡需求
- 哈希算法最简单的做法就是进行取模运算,比如分布式系统中有 3 个节点,基于 hash(key) % 3公式对数据进行了映射,如果计算后得到的值是 0,就说明该 key 需要去第一个节点获取
- 但是哈希算法存在一个问题,如果 节点数量发生了变化,也就是在对系统做扩容或者缩容时,意味取模哈希函数中基数的变化,这样会导致 大部分映射关系改变,必须迁移改变了映射关系的数据,否则会出现查询不到数据的问题
- 假设总数据条数为 M,哈希算法在面对节点数量变化时,最坏情况下所有数据都需要迁移,所以它的数据迁移规模是 O(M),这样数据的迁移成本太高了
- 一致性哈希算法就很好地解决了分布式系统在扩容或者缩容时,发生过多的数据迁移的问题
📒 前端项目 nginx 配置总结
有段时间没搞项目部署了,结果最近有同事在部署前端项目的时候,访问页面路由,响应都是 404,排查了半天,这里再总结一下。
前端单页应用路由分两种:哈希模式和历史模式。
哈希模式部署不会遇到啥问题,但是一般只用于本地调试,没人直接部署到生产环境。历史模式的路由跳转通过 pushState 和 replaceState 实现,不会触发浏览器刷新页面,不会给服务器发送请求,且会触发 popState 事件,因此可以实现纯前端路由。
需要注意,使用历史模式的时候,还是有两种情况会导致浏览器发送请求给服务器:
- 输入地址直接访问
- 刷新页面
在这两种情况下,如果当前地址不是根路径,因为都是前端路由,服务器端根本不存在对应的文件,则会直接导致服务器直接响应 404。因此需要在服务器端进行配置:
server {
  listen 80;
  server_name www.bili98.com;
  location / {
    root /root/workspace/ruoyi-ui/dist;
    # history 模式重点就是这里
    try_files $uri $uri/ /index.html;
  }
}
try_files 的作用就是按顺序检查文件是否存在,返回第一个找到的文件。$uri 是 nginx 提供的变量,指当前请求的 URI,不包括任何参数
当请求静态资源文件的时候,命中 $uri 规则;当请求页面路由的时候,命中 /index.html 规则
此外,在部署的时候不使用根路径,例如希望通过这样的路径去访问 /i/top.gif,如果直接修改 location 发现还会响应 404:
location /i/ {
  root /data/w3;
  try_files $uri $uri/ /index.html;
}
这是因为
root是直接拼接root+location,访问/i/top.gif,实际会查找/data/w3/i/top.gif文件
这种情况下推荐使用 alias:
location /i/ {
  alias /data/w3;
  try_files $uri $uri/ /index.html;
}
alias是用alias替换location中的路径,访问/i/top.gif,实际会查找/data/w3/top.gif文件
现在页面部署成功了,但是接口请求会出错,这是因为还没有对接口请求进行代理,下面配置一下:
location ^~ /prod-api/ {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://192.168.31.101:8080/;
}
完整的 nginx 配置如下:
server {
  listen 80;
  server_name www.bili98.com;
  location /ruoyi/ {
    # 支持 /ruoyi 子路径访问
    alias /root/workspace/ruoyi-ui/dist;
    # history 模式重点就是这里
    try_files $uri $uri/ /index.html;
    # html 文件不可设置强缓存,设置协商缓存即可
    add_header Cache-Control 'no-cache, must-revalidate, proxy-revalidate, max-age=0';
  }
  # 接口请求代理
  location ^~ /prod-api/ {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://192.168.31.101:8080/;
  }
  location ~* \.(?:css(\.map)?|js(\.map)?|gif|svg|jfif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
    # 静态资源设置一年强缓存
    add_header Cache-Control 'public, max-age=31536000';
  }
}
location 的匹配规则:
- =表示精确匹配。只有请求的url路径与后面的字符串完全相等时,才会命中。
- ^~表示如果该符号后面的字符是最佳匹配,采用该规则,不再进行后续的查找。
- ~表示该规则是使用正则定义的,区分大小写。
- ~*表示该规则是使用正则定义的,不区分大小写。
nginx 的匹配优先顺序按照上面的顺序进行优先匹配,而且 只要某一个匹配命中直接退出,不再进行往下的匹配。
剩下的普通匹配会按照 最长匹配长度优先级来匹配,就是谁匹配的越多就用谁。
📒 Jest + React Testing Library 单测总结
📒 React核心设计原理--(React Fiber)异步执行调度
📒 如何在浏览器使用后端语言进行编程
你可能会认为这是关于使用 WebAssembly 在浏览器中运行 Python 之类的代码,但这并不是作者想分享的。作者提到的是通过服务端的 WebSocket 连接浏览器平台,由服务端处理 HTML 渲染更新到浏览器,这种方案日益流行,并且已经在 Elixir 和 Rails 全栈框架中支持。
https://github.com/readme/featured/server-side-languages-for-front-end
📒 正则表达式如何实现千分位分隔符
实现如下的需求:
- 从后往前每三个数字前加一个逗号
- 开头不能加逗号
这样看起来非常符合 (?=p) 的规律,p 可以表示每三个数字,要添加逗号所处的位置正好是 (?=p) 匹配出来的位置。
第一步,先尝试把最后一个逗号弄出来:
"300000000".replace(/(?=\d{3}$)/, ",")
// '300000,000'
第二步,把所有逗号都弄出来:
"300000000".replace(/(?=(\d{3})+$)/g, ",")
// ',300,000,000'
使用括号把一个
p模式变成一个整体
第三步,去掉首位的逗号:
"300000000".replace(/(?!^)(?=(\d{3})+$)/g, ",")
// '300,000,000'
📒 React Router v6 和私有路由 (也称作保护路由)
📒 React Router v6 的身份验证简介
在一个简单的示例应用程序中,通过 React Router v6 实现身份验证的实用演练。
📒 Etsy 从 React 15.6 迁移到了 Preact (而不是 React 16)
在这篇 关于在 Etsy 更新 React 的文章中,对这个决定有一个完整的解释。但事实证明,拥有相同 API 的小型 React 替代品 Preact 是他们的正确选择。
📒 Promise 两点总结
不建议在 Promise 里面使用 try...catch,这样即使 Promise 内部报错,状态仍然是 fullfilled,会进入 then 方法回调,不会进入 catch 方法回调。
function request() {
  return new Promise((resolve, reject) => {
    try {
      // ...
      resolve("ok");
    } catch(e) {
      console.log(e);
    }
  })
}
request()
  .then(res => {
    console.log("请求结果:", res);
  })
  .catch(err => {
    // 由于在 Promise 中使用了 try...catch
    // 因此即使 Promise 内部报错,也不会被 catch 捕捉到
    console.log(err);
  })
Promise内部的异常,老老实实往外抛就行,让catch方法来处理,符合单一职责原则
不建议在 async 函数中,既不使用 await,也不使用 return,这样就算内部的 Promise reject 也无法捕捉到:
async function handleFetchUser(userList) {
  // 这里既没有使用 await,也没有使用 return
  Promise.all(userList.map(u => request(u)));
}
handleFetchUser(userList)
  .then(res => {
    // 由于没有返回值,这里拿到的是 undefined
    console.log(res);
  })
  .catch(err => {
    // 即使 handleFetchUser 内部的 Promise reject
    // async 函数返回的 Promise 仍然是 fullfilled
    // 此时仍然会进入 then 方法回调,无法被 catch 捕捉到
    console.log(err);
  })
如果确实有这种需求,建议不要使用
async函数,直接改用普通函数即可
📒 Rollup 配置
📒 Docker 使用,Gitlab CI 实践
📒 总结一下 Babel 插件开发基本操作