Skip to main content

· 8 min read
加菲猫

📒 以 pnpm 为例谈谈如何调试大型项目

📒 如何实现双向链表

查看详情

在项目中遇到一个问题,源码中使用数组模拟队列,添加使用 unshift,移除使用 pop,导致添加元素的时间复杂度为 O(n)。这里使用双向链表模拟队列,两端均可添加、删除元素,且时间复杂度均为 O(1)

/**
* 链表节点
*/
class ListNode<T> {
public next: ListNode<T> = null;
public prev: ListNode<T> = null;
public val: T = undefined;

constructor(val: T) {
this.val = val;
}
}

/**
* 实现双向链表
*/
class LinkedList<T> {
private head: ListNode<T> = null;
private end: ListNode<T> = null;
private _size: number = 0;

/**
* add() 相当于 addLast()
* @param val
* @returns
*/
public add(val: T): boolean {
const node = new ListNode<T>(val);
if (this.head == null) {
// 初始化 head 指针
this.head = node;
}
if (this.end == null) {
// 初始化 end 指针
this.end = node;
} else {
// 把新节点挂到链表最后
this.end.next = node;
// 新节点 prev 指向前一节点
node.prev = this.end;
// end 指针后移一位
this.end = node;
}
// 维护 size
this._size++;
return true;
}

/**
* addFirst() 在链表头部添加
* @param val
* @returns
*/
public addFirst(val: T): boolean {
const node = new ListNode<T>(val);
if (this.head == null) {
// 初始化 head 指针
this.head = node;
} else {
// 把新节点挂到链表头部
this.head.prev = node;
// 新节点 next 指向下一节点
node.next = this.head;
// head 指针前移一位
this.head = node;
}
if (this.end == null) {
// 初始化 end 指针
this.end = node;
}
// 维护 size
this._size++;
return true;
}

/**
* poll() 相当于 pollFirst()
* @returns
*/
public poll(): T {
// 缓存需要删除的节点
const node = this.head;
// head 指向下一节点
this.head = this.head.next;
// 切断与前一节点的联系
this.head.prev = null;
// 维护 size
this._size--;
return node.val;
}

/**
* pollLast() 移除链表尾部元素
* @returns
*/
public pollLast(): T {
// 缓存需要删除的节点
const node = this.end;
// end 指向前一节点
this.end = this.end.prev;
// 切断与后一节点的联系
this.end.next = null;
// 维护 size
this._size--;
return node.val;
}

/**
* 获取链表长度
* @returns
*/
public size(): number {
return this._size;
}

/**
* 序列化为字符串
* @returns
*/
public toString(): string {
let res: T[] = [];
let list = this.head;
while (list != null) {
res.push(list.val);
list = list.next;
}
return `[ ${res.join(" ")} ]`;
}
}

📒 Nest.js 的 AOP 架构的好处,你感受到了么?

📒 React Hooks 源码分析

React 函数组件通过 renderWithHooks 函数进行渲染,里面有个 workingInProgress 的对象就是当前的 fiber 节点,fiber 节点的 memorizedState 就是保存 hooks 数据的地方。它是一个通过 next 串联的链表。

这个 memorizedState 链表是什么时候创建的呢?确实有个链表创建的过程,也就是 mountXxx。链表只需要创建一次,后面只需要 update。所以第一次调用 useState 会执行 mountState,后面再调用 useState 会执行 updateState

每个 Hook 的 memorizedState 链表节点是通过 mountWorkInProgressHook 函数创建的:

function mountWorkInProgressHook(): Hook {
const hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};

if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}

函数组件本身是没有挂载、更新的概念的,每次 rerender 就是执行这个函数,但是挂载、更新的逻辑体现在 Hooks 里面,首次执行的时候调用 mountWorkInProgressHook 创建链表节点,后续执行的时候调用 updateWorkInProgressHook 访问并更新链表节点

React Hooks 的原理,有的简单有的不简单

React 进阶实战指南 - 原理篇:Hooks 原理

📒 前端工程师如何快速使用一个NLP模型

2017年谷歌提出了Transformer架构模型,2018年底,基于Transformer架构,谷歌推出了bert模型,bert模型一诞生,便在各大11项NLP基础任务中展现出了卓越的性能,现在很多模型都是基于或参考Bert模型进行改造。

tip

如果想了解 Transformer 和 bert,可以看这个视频

https://www.bilibili.com/video/BV1P4411F77q

https://www.bilibili.com/video/BV1Mt411J734

前端工程师如何快速使用一个NLP模型

📒 Lerna 运行流程剖析

⭐️ Git不要只会pull和push,试试这5条提高效率的命令

📒 React内部的性能优化没有达到极致?

📒 reduce 方法注意事项

初始值非空初始值为空
数组非空首次执行回调,accu 为初始值,cur 为数组第一项首次执行回调,accu 为数组第一项,cur 为数组第二项
数组为空不执行回调,直接返回初始值报错(建议任何情况下都传递初始值)

📒 npm 安装依赖默认添加 ^ 前缀,当再次执行 npm install 命令时,会自动安装这个包在此大版本下的最新版本。如果想要修改这个功能,可以执行以下命令:

$ npm config set save-prefix='~'

执行完该命令之后,就会把 ^ 符号改为 ~ 符号。当再次安装新模块时,就从只允许小版本的升级变成了只允许补丁包的升级

如果想要锁定当前的版本,可以执行以下命令:

$ npm config set save-exact true

这样每次 npm install xxx --save 时就会锁定依赖的版本号,相当于加了 --save-exact 参数。建议线上的应用都采用这种锁定版本号的方式

既然可以锁定依赖版本,为什么还需要 lcok-file 呢,个人理解锁定依赖只能锁定当前项目中的依赖版本,但是还存在间接依赖,即依赖还有依赖,直接锁定依赖版本无法解决间接依赖的问题,间接依赖版本还是不受控制,需要借助 lock-file 锁定间接依赖的版本。

📒 函数式编程三种形式:

  • 函数赋值给变量
    • 可作为数组的元素,进而实现 compose 函数组合,或者管道操作
  • 函数作为参数
    • 常见的有 forEachPromisesetTimeout 等,React 技术栈也有很多 API
  • 函数作为返回值

📒 GitLab CI 打造一条自己的流水线

📒 type-challenges

type-challenges 是一个 TypeScript 类型体操姿势合集。本项目意在于让你更好的了解 TS 的类型系统,编写你自己的类型工具,或者只是单纯的享受挑战的乐趣!

https://github.com/type-challenges/type-challenges

· 11 min read
加菲猫

📒 React 18 RC 版本发布啦,生产环境用起来!

安装最新的 React 18 RC 版本(Release Candidate候选版本):

$ yarn add react@rc react-dom@rc

注意在 React 18 中新增了 concurrent Mode 模式,通过新增的 createRoot API 开启:

import ReactDOM from 'react-dom'

// 通过 createRoot 创建 root
const root = ReactDOM.createRoot(document.getElementById('app'))
// 调用 root 的 render 方法
root.render(<App/>)

startTransition 特性依赖 concurrent Mode 模式运行

如果使用传统 legacy 模式,会按 React 17 的方式运行:

import ReactDOM from 'react-dom'

// 通过 ReactDOM.render
ReactDOM.render(
<App />,
document.getElementById('app')
)

React 18 主要是对自动批处理进行优化。在 React 18 之前实际上已经有批处理机制,但是只针对同步代码,如果放在 PromisesetTimeout 等异步回调中,自动批处理会失效。

class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}

componentDidMount() {
// 自动批处理更新
// 注意此时 setState 是异步的
this.setState({val: this.state.val + 1});
console.log(this.state.val);
this.setState({val: this.state.val + 1});
console.log(this.state.val);

setTimeout(() => {
// 自动批处理失效
// 此时 setState 是同步的
this.setState({val: this.state.val + 1});
console.log(this.state.val);
this.setState({val: this.state.val + 1};
console.log(this.state.val);
}, 0);
}
};

在 React 18 版本之前,上面代码的打印顺序是 0、0、2、3

React 18 版本解决了这个问题,在异步回调中更新状态也能触发自动批处理,打印的顺序是 0、0、1、1

总结一下主要有以下几个新特性:

  • 新的 ReactDOM.createRoot() API(替换 ReactDOM.render()
  • 新的 startTransition API(用于非紧急状态更新)
  • 渲染的自动批处理优化(主要解决异步回调中无法批处理的问题)
  • 支持 React.lazy 的 全新 SSR 架构(支持 <Suspense> 组件)

React 18 RC 版本发布啦,生产环境用起来!

📒 CSS TreeShaking 原理揭秘: 手写一个 PurgeCss

📒 「源码解析」一文吃透react-redux源码(useMemo经典源码级案例)

📒 Recoil实现原理浅析-异步请求

📒 WebSocket 基础与应用系列(一)—— 抓个 WebSocket 的包

HTTP 和 WebSocket 都属于应用层协议,都是基于 TCP 来传输数据的,可以理解为对 TCP 的封装,都要遵循 TCP 的三次握手和四次挥手,只是在连接之后发送的内容(报文格式)不同,或者是断开的时间不同。

如何使用 Wireshark 抓包:

  • 在 Capture 中选择本机回环网络
  • 在 filter 中写入过滤条件 tcp.port == 3000

WebSocket 基础与应用系列(一)—— 抓个 WebSocket 的包

📒 CSS 代码优化的12个小技巧

📒 JS 框架解决了什么问题

📒 反向操作,用 Object.defineProperty 重写 @vue/reactivity

📒 antfu 大佬的 eslint 配置

https://github.com/antfu/eslint-config

📒 antfu 大佬的 vscode 配置

https://github.com/antfu/vscode-settings

📒 使用 tsdoc 编写规范的注释

https://tsdoc.org/

📒 npm 包发布工具

https://github.com/JS-DevTools/version-bump-prompt

📒 使用 pnpm 作为包管理工具

基本用法:

  • pnpm add <pkg>:安装依赖
  • pnpm add -D <pkg>:安装依赖到 devDependencies
  • pnpm install:安装所有依赖
  • pnpm -r update:递归更新每个包的依赖
  • pnpm -r update typescript@latest:将每个包的 typescript 更新为最新版本
  • pnpm remove:移除依赖

如何支持 monorepo 项目:https://pnpm.io/zh/workspaces

pnpm -r 带一个参数 -r 表示进行递归操作。

pnpm 官方文档

为什么 vue 源码以及生态仓库要迁移 pnpm?

📒 推荐两个打包工具

📒 seedrandom:JS 种子随机数生成器

种子随机数生成器,生成是随机的,但是每次调用生成的值是固定的:

const seedrandom = require('seedrandom');
const rng = seedrandom('hello.');

console.log(rng()); // 第一次调用总是 0.9282578795792454
console.log(rng()); // 第二次调用总是 0.3752569768646784

https://github.com/davidbau/seedrandom

📒 深入Node.js的模块加载机制,手写require函数

📒 require加载器实现原理

📒 聊一聊前端算法面试——递归

📒 589. N 叉树的前序遍历 :「递归」&「非递归」&「通用非递归」

📒 Million v1.5:一种快速虚拟 DOM 的实现

专注于性能和大小,压缩后小于 1KB,如果您想要一个抽象的 VDOM 实现,Million 是你构建自己的框架或库时理想的选择

https://millionjs.org/

📒 200 行代码使用 React 实现俄罗斯方块

https://blog.ag-grid.com/tetris-to-learn-react/

📒 真实案例说明 TypeScript 类型体操的意义

📒 「React 进阶」 学好这些 React 设计模式,能让你的 React 项目飞起来🛫️

📒 「1.9W字总结」一份通俗易懂的 TS 教程,入门 + 实战!

📒 Oclif v2.5:Heroku 开源的 CLI 框架

一个用于构建 CLI 脚手架的成熟框架,无论是简单的参数解析还是很多功能指令都可以驾驭。

https://github.com/oclif/oclif

📒 使用 Rust 与 WebAssembly 重新实现了 Node 的 URL 解析器

https://www.yagiz.co/implementing-node-js-url-parser-in-webassembly-with-rust/

📒 PDF:从 JavaScript 到 Rust:新书免费发布

https://github.com/vinodotdev/node-to-rust/releases/download/v1/from-javascript-to-rust.pdf

📒 Red Hat 和 IBM 团队的 Node.js “架构参考”

https://github.com/nodeshift/nodejs-reference-architecture

📒 在 Node 环境下使用 execa 运行命令

https://blog.logrocket.com/running-commands-with-execa-in-node-js/

📒 万字长文详解从零搭建企业级 vue3 + vite2+ ts4 框架全过程

📒 从 Linux 源码的角度解释进程

📒 10 React Antipatterns to Avoid - Code This, Not That!

https://www.youtube.com/watch?v=b0IZo2Aho9Y

📒 markdown 编辑器滚动如何实现联动

const ScrollTarget = {
NONE: "NONE",
EDITOR: "EDITOR",
RENDER: "RENDER",
};

let curTarget = ScrollTarget.NONE;
let timer = null;

const scrollManager = (handler) => (target) => {
if ((curTarget = ScrollTarget.NONE)) {
curTarget = target;
}
if (curTarget === target) {
handler(target);
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
curTarget = ScrollTarget.NONE;
}, 100);
}
};

const scrollFn = scrollManager(handleScroll);

📒 Webpack 的模块规范

Webpack 基于 CJS 和 ESM 规范实现了模块机制,但也不是完全基于,而是在这些模块规范基础上扩展了一套自己的 api,用于增强功能,例如:

  • require.context
  • 使用 import 加载 .json 模块

在 ESM 规范中 import 只能用于加载 JS 模块,只有 require 可以加载 json 模块

📒 如何将对象序列化为查询字符串

const aaa = {a: 1, b: 2, c: "2333"}

第一种手动拼接,简单直接,一行代码搞定:

const _stringify =
(obj) => Object.entries(obj).map(([key, val]) => `${key}=${val}`).join("&");

还可以使用 URLSearchParams 对象进行拼接:

const _stringify = obj => Object.entries(obj).reduce((accu, [key, val]) => {
accu.append(key, val);
return accu;
}, new URLSearchParams)

📒 「深入浅出」主流前端框架更新批处理方式

浏览器环境下,宏任务的执行并不会影响到浏览器的渲染和响应,即宏任务优先级低于页面渲染。

function run(){
setTimeout(() => {
console.log('----宏任务执行----')
run()
}, 0)
}
// 通过递归调用 run 函数,让 setTimeout 宏任务反复执行
// 这种情况下 setTimeout 执行并不影响页面渲染和交互事件
run()

微任务会在当前 event loop 中执行完毕,会阻塞浏览器的渲染和响应,即微任务优先级高于页面渲染。

function run(){
Promise.resolve().then(() => {
run()
})
}
// 在这种情况下,页面直接卡死了,没有响应
run()

这里主要就是理解关键渲染路径,即浏览器渲染一帧会先执行脚本,再页面布局,绘制渲染。如果是宏任务,浏览器会把每一次事件回调放在下一帧渲染前执行,这样可以确保浏览器每一帧都能正常渲染。如果是微任务,浏览器在执行渲染之前会清空微任务队列,会导致一直停留在当前 event loop,相当于脚本一直在执行,如果长时间不把控制权交还给浏览器,就会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。

「深入浅出」主流前端框架更新批处理方式

· 19 min read
加菲猫

📒 通过Vue自定义指令实现前端埋点

📒 Deno 简介:它比 Node.js 更好吗?

📒 快来玩转这 19 个 css 技巧

📒 解决了 Redis 大 key 问题,同事们都说牛皮!

📒 图解|Linux内存背后的那些神秘往事

📒 深入理解Go Json.Unmarshal精度丢失之谜

📒 如何理解快速排序和归并排序

快速排序实际就是二叉树的前序遍历,归并排序实际就是二叉树的后序遍历。

快速排序的逻辑是,若要对 nums[lo..hi] 进行排序,我们先找一个分界点 p,通过交换元素使得 nums[lo..p-1] 都小于等于 nums[p],且 nums[p+1..hi] 都大于 nums[p],然后递归地去 nums[lo..p-1]nums[p+1..hi] 中寻找新的分界点,最后整个数组就被排序了。

快速排序的代码框架如下:

void sort(int[] nums, int lo, int hi) {
/****** 前序遍历位置 ******/
// 通过交换元素构建分界点 p
int p = partition(nums, lo, hi);
/************************/

sort(nums, lo, p - 1);
sort(nums, p + 1, hi);
}

先构造分界点,然后去左右子数组构造分界点,你看这不就是一个二叉树的前序遍历吗

再说说归并排序的逻辑,若要对 nums[lo..hi] 进行排序,我们先对 nums[lo..mid] 排序,再对 nums[mid+1..hi] 排序,最后把这两个有序的子数组合并,整个数组就排好序了。

归并排序的代码框架如下:

void sort(int[] nums, int lo, int hi) {
int mid = (lo + hi) / 2;
// 排序 nums[lo..mid]
sort(nums, lo, mid);
// 排序 nums[mid+1..hi]
sort(nums, mid + 1, hi);

/****** 后序位置 ******/
// 合并 nums[lo..mid] 和 nums[mid+1..hi]
merge(nums, lo, mid, hi);
/*********************/
}

先对左右子数组排序,然后合并(类似合并有序链表的逻辑),你看这是不是二叉树的后序遍历框架?另外,这不就是传说中的分治算法嘛,不过如此呀

说了这么多,旨在说明,二叉树的算法思想的运用广泛,甚至可以说,只要涉及递归,都可以抽象成二叉树的问题。

📒 Leetcode 236 二叉树最近公共祖先

lowestCommonAncestor 方法的定义:给该函数输入三个参数 rootpq,它会返回一个节点。

  • 情况 1,如果 pq 都在以 root 为根的树中,函数返回的即 pq 的最近公共祖先节点。
  • 情况 2,如果 pq 都不在以 root 为根的树中,则理所当然地返回 null 呗。
  • 情况 3,如果 pq 只有一个存在于 root 为根的树中,函数就返回那个节点。
tip

题目说了输入的 pq 一定存在于以 root 为根的树中,但是递归过程中,以上三种情况都有可能发生,所以说这里要定义清楚,后续这些定义都会在代码中体现。

函数参数中的变量是 root,因为根据框架,lowestCommonAncestor(root) 会递归调用 root.leftroot.right;至于 pq,我们要求它俩的公共祖先,它俩肯定不会变化的。你也可以理解这是「状态转移」,每次递归在做什么?不就是在把「以root为根」转移成「以root的子节点为根」,不断缩小问题规模嘛

class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 两个 base case
// 1.如果 root 为空,直接返回 null
if (root == null) return null;
// 2.如果 root 本身就是 p 或者 q
// 例如 root 是 p 节点,如果 q 存在于以 root 为根的树中,显然 root 就是最近公共祖先
// 即使 q 不存在于以 root 为根的树中,按照情况 3 的定义,也应该返回 root 节点
if (root == p || root == q) return root;

TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);

// 在后序位置分情况讨论
// 情况 1,如果 p 和 q 都在以 root 为根的树中
// 那么 left 和 right 一定分别是 p 和 q(从 base case 看出)
// 由于后序位置是从下往上,就好比从 p 和 q 出发往上走
// 第一次相交的节点就是这个 root,显然就是最近公共祖先
if (left != null && right != null) {
return root;
}
// 情况 2,如果 p 和 q 都不在以 root 为根的树中,直接返回 null
if (left == null && right == null) {
return null;
}
// 情况 3,如果 p 和 q 只有一个存在于 root 为根的树中,函数返回该节点
return left == null ? right : left;
}
}

在前序位置搜索节点,如果是空节点直接返回,如果搜索到 p 或者 q 返回该节点,否则继续递归

在后序位置接收前序的返回值,如果 leftright 都不为空,说明分别是 pq,当前 root 就是最近公共祖先,直接返回 root 节点。如果一个为空另一个不为空,说明找到一个节点,把这个节点向上传递,查找另一个节点,直到出现两个都不为空,此时 root 就是最近公共祖先,直接返回 root 节点

📒 如何写对二分查找

  • 不要使用 else,而是把所有情况用 else if 写清楚
  • 计算 mid 时需要防止溢出,使用 left + (right - left) / 2 先减后加这样的写法
  • while 循环的条件 <= 对应 right 初始值为 nums.length - 1,此时终止条件是 left == right + 1,例如 [3, 2]
  • 如果 while 循环的条件 <,需要把 right 初始值改为 nums.length,此时终止条件是 left == right,例如 [2, 2],这样会漏掉最后一个区间的元素,需要单独判断下
  • mid 不是要找的 target 时,下一步应该搜索 [left, mid-1] 或者 [mid+1, right],对应 left = mid + 1 或者 right = mid - 1
  • 二分查找时间复杂度 O(logn)
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意

while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1; // 注意
} else if (nums[mid] > target) {
right = mid - 1; // 注意
}
}
return -1;
}
}

📒 前端三种 Content-Type

application/json:这种应该是接口请求用到最多的,可以使用 JSON.stringify() 序列化得到,实际传递的内容类似于:

{"a": "111", "b": "222"}

application/x-www-form-urlencoded:这是表单提交对应的 Content-Type,实际上就是通过 body 传递 query 参数,如使用 HTML 的表单元素,浏览器会自动进行拼接,也可通过 URLSearchParams 拼接得到,实际传递的内容类似于:

a=111&b=222

multipart/form-data:是通过 FormData 对象构造出来的表单格式,通常用于文件上传,实际传递的报文内容类似于:

POST /test.html HTTP/1.1
Host: example.org
Content-Type: multipart/form-data;boundary="boundary"

--boundary
Content-Disposition: form-data; name="field1"

value1
--boundary
Content-Disposition: form-data; name="field2"; filename="example.txt"

value2
--boundary--

顺便提一下,文件下载对应的 Content-Type 是 application/octet-stream

📒 如何理解 Node.js 模块

一个模块实际上可以看做一个 once 函数,头部的 require 命令可以看做入参,module.exports 可以看做返回值。

当首次加载一个模块的时候,就会运行这个模块代码,可以看做是调用一个函数,执行结束后得到导出的内容并被缓存,可以看做函数返回一个值。当再次加载这个模块,不再执行这个模块代码,而是直接从缓存中取值。

在一个函数中,我们知道可以使用 return 语句提前结束运行,那么在模块中如何实现呢,答案是使用 process.exit(1)

const fs = require("node:fs");
const path = require("node:path");
const webpack = require("webpack");

const workDir = process.cwd();
const envFilePath = path.resolve(workDir, "./.env.local");
const hasEnvFile = fs.existsSync(envFilePath);

if (!hasEnvFile) {
process.exit(1);
}

module.exports = {
mode: "development",
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: '[chunkhash].bundle.js',
clean: true
},
}

这里注意下,fs.exists() 方法已经废弃了,但是 fs.existsSync() 仍然可用。此外还可使用 fs.stat() 或者 fs.access() 检查文件是否存在

📒 在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么?

📒 一键部署 K8S 环境,10分钟玩转,这款开源神器实在太香了!

📒 charles 如何连接手机抓包

  • 确保手机和电脑连接的是同一个网络
  • 首先打开 charles,会启动一个服务,查看端口:proxy -> proxy setting
  • 勾选 Enable transparent HTTP proxying
  • 查看本机 IP
  • 在手机上设置 http 代理服务器,输入 IP 和端口
  • 此时 charles 会弹出提示,有新的连接,点击 allow

📒 前端项目的 .env 文件是如何生效的,一句话总结

通过 dotenv 这个包解析 .env 文件,加载到 process.ENV 里面,这时候可以通过 process.ENV.xxx 访问到环境变量,适用于 Node.js 项目,但是由于浏览器环境访问不到 process 对象,所以对于前端项目,还需要使用 Webpack 的 DefinePlugin 在打包构建阶段将变量替换为对应的值。

📒 如何防止用户篡改 url 参数

http://localhost:8080/codepc/live?codeTime=1646038261531&liveId=5e24dd3cf03a&sign=e8fe282676f584ceab7e35f84cbc52ff&keyFrom=youdao

前端的直播链接带有 codeTimeliveId,如何防止用户篡改。只需要后端在返回 codeTimeliveId 的时候,同时计算一个签名 sign 返回给前端,前端提交给后端的时候,同时传递三个参数,后端计算一个新的签名,与前端传过来的 sign 进行比对,如果一样就说明没有篡改。

但是计算签名用的 md5 是一个公开的算法,假如有人篡改了 codeTimeliveId ,只要他使用 md5 计算一个新的签名 sign ,这样传给后端校验必然可以通过。这就需要后端签名的时候拼接一个加密串进去,验签的时候也用这个加密串。这样由于别人不知道加密串,即便生成新的签名,后端校验也不会通过。

📒 了解下Rust 模块使用方式

🌛 一文秒杀排列组合问题的 9 种题型

📒 Screenshot: 不依赖浏览器原生能力的截屏库

该库基于 MediaDevice API 来提供了易于截屏的抽象。如果你有兴趣可以来看看 GitHub 仓库

https://github.com/xataio/screenshot

📒 enum-xyz:使用 Proxy 实现 JavaScript 中的枚举

一个 js-weekly 的读者,分享的有趣实现思路。源码很短,推荐看一下

https://github.com/chasefleming/enum-xyz

📒 使用 React 和 Tailwind 创建阅读进度条

📒 React内部让人迷惑的性能优化策略

📒 Nest.js 基于 Express 但也不是完全基于

📒 如何使用代理模式优化代码

开发环境下打印日志:

const dev = process.env.NODE_ENV === 'development';
const createDevFn = (cb) => {
return (...args) => dev && cb(...args);
};

const log = createDevFn(console.log);
log("23333"); // "2333"

异常捕获:

class ExceptionsZone {
static handle(exception) {
console.log('Error:',exception.message, exception.stack);
}

static run(callback) {
try {
callback();
} catch (e) {
this.handle(e);
}
}
}

function createExceptionZone(target) {
return (...args) => {
let result;
ExceptionsZone.run(() => {
result = target(...args);
});
return result;
};
}

const request = () => new Promise((resolve) => setTimeout(resolve, 2000));
const requestWithHandler = createExceptionZone(request);
requestWithHandler().then(res => console.log("请求结果:", res));

如何用 Proxy 更优雅地处理异常

📒 VuePress 博客优化之开启 Algolia 全文搜索

📒 Git 分支操作流程

在 Git Flow 中,有两个长期存在且不会被删除的分支:masterdevelop

  • master 主要用于对外发布稳定的新版本,该分支时常保持着软件可以正常运行的状态,不允许开发者直接对 master 分支的代码进行修改和提交。当需要发布新版本时,将会与 master 分支进行合并,发布时将会附加版本编号的 Git 标签
  • develop 则用来存放我们最新开发的代码,这个分支是我们开发过程中代码中心分支,这个分支也不允许开发者直接进行修改和提交。程序员要以 develop 分支为起点新建 feature 分支,在 feature 分支中进行新功能的开发或者代码的修正

注意 develop 合并的时候,不要使用 fast-farward merge,建议加上 --no-ff 参数,这样在 master 上就会有合并记录

除了这两个永久分支,还有三个临时分支:feature brancheshotfixes 以及 release branches

  • feature branches 是特性分支,也叫功能分支。当你需要开发一个新的功能的时候,可以新建一个 feature-xxx 的分支,在里边开发新功能,开发完成后,将之并入 develop 分支中
  • hotfixes 就是用来修复 BUG 的。当我们的项目上线后,发现有 BUG 需要修复,那么就从 master 上拉一个名为 fixbug-xxx 的分支,然后进行 BUG 修复,修复完成后,再将代码合并到 masterdevelop 两个分支中,然后删除 hotfix 分支
  • release branches 是发版的时候拉的分支。当我们所有的功能做完之后,准备要将代码合并到 master 的时候,从 develop 上拉一个 release-xxx 分支出来,这个分支一般处理发版前的一些提交以及客户体验之后小 BUG 的修复(BUG 修复后也可以将之合并进 develop),不要在这个里边去开发功能,在预发布结束后,将该分支合并进 develop 以及 master,然后删除 release

image

📒 大厂动态规划面试汇总,重量级干货,彻夜整理

⭐️ 通过几行 JS 就可以读取电脑上的所有数据?

📒 百行代码带你实现通过872条Promise/A+用例的Promise

📒 颜值爆表!Redis 官方可视化工具来啦,功能真心强大!

📒 程序员开源月刊《HelloGitHub》第 71 期

C 项目

chibicc:迷你 C 编译器。虽然它只是一个玩具级的编译器,但是实现了大多数 C11 特性,而且能够成功编译几十万行的 C 语言项目,其中包括 Git、SQLite 等知名项目。而且它项目结构清晰、每次提交都是精心设计、代码容易理解,对编译器感兴趣的同学可以从第一个提交开始学习

https://github.com/rui314/chibicc

Go 项目

nali:离线查询 IP 地理信息和 CDN 服务提供商的命令行工具

https://github.com/zu1k/nali

revive:快速且易扩展的 Go 代码检查工具。它比 golint 更快、更灵活,深受广大 Go 开发者的喜爱

https://github.com/mgechev/revive

go-chart:Go 原生图表库。支持折线图、柱状图、饼图等

https://github.com/wcharczuk/go-chart

Java 项目

thingsboard:完全开源的物联网 IoT 平台。它使用行业的标准物联网协议 MQTT、CoAP 和 HTTP 连接设备,支持数据收集、处理、可视化和设备管理等功能。通过该项目可快速实现物联网平台搭建,从而成为众多大型企业的首选,行业覆盖电信、智慧城市、环境监测等

https://github.com/thingsboard/thingsboard

from-java-to-kotlin:展示 Java 和 Kotlin 语法上差别的项目。让有 Java 基础的程序员可以快速上手 Kotlin

https://github.com/MindorksOpenSource/from-java-to-kotlin

⭐️ ⭐️ 「React进阶」react-router v6 通关指南

· 17 min read
加菲猫

📒 Vue diff 算法

Vue2 diff 算法核心流程如下:

  • diff 的入口函数为 patch,使用 sameVnode 比较节点是否相同,如相同则使用 patchVnode 继续进行深层比较,否则就使用 createEle 方法渲染出真实 DOM 节点,然后替换旧元素节点
  • sameVnode 通过比较 key 值是否一样、标签名是否一样、是否都为注释节点、是否都定义 data、当标签为 input 时,type 是否相同来判断两个节点是否相同
  • patchVnode 方法如何对节点深层比较
    • 拿到真实 DOM 的节点 el(即 oldVnode.el
    • 判断当前 newVnodeoldVnode 是否指向同一对象,如果是直接 return
    • 如果新旧虚拟节点是文本节点,且文本不一样,则直接将真实 DOM 中文本更新为新虚拟节点的文本;若文本没有变化,则继续对比新旧节点的 children
    • 如果 oldVnode 有子节点而 newVnode 没有,则删除 el 的子节点
    • 如果 oldVnode 没有子节点而 newVnode 有,则将 newVnode 的子节点渲染出真实 DOM 添加到 el(Vue 源码中会判断是否有 key 重复)
    • 如果两者都有子节点,则执行 updateChildren 函数比较子节点
  • updateChildren 是 diff 算法核心部分,当发现新旧虚拟节点的子节点都存在时,需要判断哪些节点是需要移动的,哪些节点是可以直接复用的,进而提高 diff 的效率
    • 通过 首尾指针法,在新旧子节点的首位定义四个指针,然后不断对比找到可复用的节点,同时判断需要移动的节点
    • 非理想状态下只能通过节点映射的方式去找可复用节点,时间复杂度为 O(n^2)
    • Vue3 的 diff 算法在非理想状态下的节点对比使用了最长递增子序列来处理,时间复杂度为 O(nlgn)~O(n^2)

image

图解Diff算法——Vue篇

浅析 Snabbdom 中 vnode 和 diff 算法

📒 Leetcode 300 最长递增子序列

常规方式是使用动态规划,时间复杂度 O(n^2)。这里注意 dp[i] 的定义是 nums[i] 这个数结尾的最长递增子序列长度

class Solution {
public int lengthOfLIS(int[] nums) {
// 定义 dp 数组
// dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列长度
int[] dp = new int[nums.length];
// 初始值填充 1(子序列至少包含当前元素自己)
Arrays.fill(dp, 1);
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
// 假设 dp[0...i-1] 都已知,需要求出 dp[i]
// 只需要遍历 nums[0...i-1],找到结尾比 nums[i] 小的子序列长度 dp[j]
// 然后把 nums[i] 接到最后,就可以形成一个新的递增子序列,长度为 dp[j] + 1
// 显然,可能形成很多种新的子序列,只需要选择最长的,作为 dp[i] 的值即可
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
// 遍历 dp 数组,找出最大值
int res = 0;
for (int i = 0; i < dp.length; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
}

📒 CSS 中的 object-fit 属性用法

在项目中有一个需求,图片尺寸较小时,需要保存图片原有大小,图片尺寸大于容器大小时,需要缩放以适合容器大小,同时保持原有比例。

查阅 MDN 文档可知,在 <img><video> 等替换元素上可以使用 object-fit 属性,用于设置替换元素该如何适配容器,可以取以下几个值:

  • object-fit: fill:图片被拉伸以适应容器,这种方式不会保持长宽比
  • object-fit: contain:图片被缩放以适应容器,同时保持长宽比,如果图片与容器长宽比不匹配,较短边会留出空白
  • object-fit: cover:图片被缩放以适应容器,同时保持长宽比,如果图片与容器长宽比不匹配,较长边会被剪裁
  • object-fit: none:图片不会调整大小
  • object-fit: scale-down:图片较小时使用 none,图片较大时使用 contain

综上,使用 object-fit: scale-down 就可以实现项目需求。

注意 IE 11 不支持 object-fit

object-fit - MDN

📒 理解归并排序

归并排序就是对数组的左半边和右半边分别排序,然后再合并两个有序数组。

  • 归并排序的过程可以在逻辑上抽象成一棵二叉树,树上的每个节点的值可以认为是 nums[lo..hi],叶子节点的值就是数组中的单个元素
  • 然后,在每个节点的后序位置(左右子节点已经被排好序)的时候执行 merge 函数,合并两个子节点上的子数组
  • 这个 merge 操作会在二叉树的每个节点上都执行一遍,执行顺序是二叉树后序遍历的顺序

一句话总结,归并排序实际上就是先对数组不断进行二分,分到只有一个元素为止,此时 merge 方法开始发挥作用,将两个元素为一组,合并为长度为 2 的有序数组,再将两个长度为 2 的有序数组为一组,合并为长度为 4 的有序数组,以此类推

class Merge {

// 用于辅助合并有序数组(不能原地合并,需要借助额外空间)
private static int[] temp;

public static void sort(int[] nums) {
// 避免递归中频繁分配和释放内存可能产生的性能问题
// 提前给辅助数组开辟内存空间
temp = new int[nums.length];
// 原地修改的方式对整个数组进行排序
sort(nums, 0, nums.length - 1);
}

// 定义:将子数组 nums[lo..hi] 进行排序
private static void sort(int[] nums, int lo, int hi) {
if (lo == hi) {
// 单个元素不用排序
return;
}
// 这样写是为了防止溢出,效果等同于 (hi + lo) / 2
// 注意:对于无法整除的情况,Java 中 int 类型会自动向下取整
int mid = lo + (hi - lo) / 2;
// 先对左半部分数组 nums[lo..mid] 排序
sort(nums, lo, mid);
// 再对右半部分数组 nums[mid+1..hi] 排序
sort(nums, mid + 1, hi);
// 将两部分有序数组合并成一个有序数组
merge(nums, lo, mid, hi);
}

// 将 nums[lo..mid] 和 nums[mid+1..hi] 这两个有序数组合并成一个有序数组
private static void merge(int[] nums, int lo, int mid, int hi) {
// 先把 nums[lo..hi] 复制到辅助数组中
// 以便合并后的结果能够直接存入 nums
for (int i = lo; i <= hi; i++) {
temp[i] = nums[i];
}

// 数组双指针技巧,合并两个有序数组
// i => 左半边数组起始下标
// j => 右半边数组起始下标
int i = lo, j = mid + 1;
for (int p = lo; p <= hi; p++) {
if (i == mid + 1) {
// 左半边数组已全部被合并,只需把右半边数组合并过来即可
nums[p] = temp[j++];
} else if (j == hi + 1) {
// 右半边数组已全部被合并,只需把左半边数组合并过来即可
nums[p] = temp[i++];
} else if (temp[i] > temp[j]) {
// 将较小的元素合入,同时下标前进一位,此时是升序
// 只要将 > 改为 < 就可以把结果改为降序
nums[p] = temp[j++];
} else {
nums[p] = temp[i++];
}
}
}
}

归并排序时间复杂度为 O(nlogn)

归并排序的正确理解方式及运用

🌛 Leetcode 112 路径总和

判断是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false

class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false;
if (root.left == null && root.right == null) {
return (targetSum - root.val) == 0;
}
boolean leftResult = hasPathSum(root.left, targetSum - root.val);
boolean rightResult = hasPathSum(root.right, targetSum - root.val);
return leftResult || rightResult;
}
}

📒 Podman 已成 Linux 官方标配!Docker 没戏了?

⭐️ 不懂动态规划?21道 LeetCode题目带你学会动态规划!

⭐️ 浅析 Snabbdom 中 vnode 和 diff 算法

📒 HTTP 缓存最佳实践

在配置 nginx 的时候,可以配置合理的缓存策略,例如:

  • html 文件配置协商缓存
  • js、css、图片、字体等文件由于带有哈希,可以配置一年强缓存
tip

这里的缓存更新逻辑:

当 js、css 等静态资源文件修改后,文件哈希发生变化,对应引入 html 的文件地址也发生变化,等于 html 文件也被修改。因此浏览器端会获取到最新的 html 文件,然后根据带有哈希的路径加载最新的静态资源文件。

这样配置缓存之后,可以极大提升资源二次加载速度,进而提升用户体验。以上这些是从性能角度考虑的,从安全角度考虑,推荐如下配置:

  • 为了防止中介缓存,建议设置 Cache-Control: private,这可以禁用掉所有 Public Cache(比如代理),这就减少了攻击者跨界访问到公共内存的可能性
  • 默认情况下,浏览器使用 URL请求方法 作为缓存 key,这意味着,如果一个网站需要登录,不同用户的请求由于它们的请求URL和方法相同,数据会被缓存到一块内存里。如果我们请求的响应是跟请求的 Cookie 相关的,建议设置 Vary: Cookie 作为二级缓存 key

HTTP 缓存别再乱用了!推荐一个缓存设置的最佳姿势!

📒 跨域,不止CORS

通常提到跨域问题的时候,相信大家首先会想到的是 CORS (Cross Origin Resource Sharing),其实 CORS 只是众多跨域访问场景中安全策略的一种,类似的策略还有:

  • COEP (Cross Origin Embedder Policy):跨源嵌入程序策略
  • COOP (Cross Origin Opener Policy):跨源开放者政策
  • CORP (Cross Origin Resource Policy):跨源资源策略
  • CORB (Cross Origin Read Blocking):跨源读取阻止

为何有时候服务端没有给响应头设置 Content-Type,浏览器还能正确识别资源类型

当服务端没有设置 Content-Type 或者浏览器认为类型不正确时,浏览器会读取资源的字节流,进行 MIME 类型嗅探。这就可能导致一些敏感数据被提交到内存,攻击者随后可以利用 Spectre 之类的漏洞来潜在地读取该内存块。

https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing

为了使我们的网站更加安全,建议所有网站都开启 CORB,只需要下面的操作:

  • 配置正确的 Content-Type(例如,HTML 资源设置 text/html
  • 开启 X-Content-Type-Options: nosniff 来禁止客户端进行自动 MIME 嗅探

跨域,不止CORS

新的跨域策略:使用COOP、COEP为浏览器创建更安全的环境

📒 如何监听系统黑暗模式

在 CSS 中可以通过 prefers-color-scheme 媒体查询实现:

body {
color: black;
background: white;
}
@media (prefers-color-scheme: dark) {
body {
color: white;
background: black;
}
}

在 JS 中可以使用 window.matchMedia 媒体查询:

import React from "react";

export type ThemeName = "light" | "dark";

function useTheme() {
const [themeName, setThemeName] = React.useState<ThemeName>("light");

React.useEffect(() => {
// 设置初始皮肤
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
setThemeName("dark");
} else {
setThemeName("light");
}

// 监听系统颜色切换
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (event) => {
if (event.matches) {
setThemeName("dark");
} else {
setThemeName("light");
}
});
}, []);

return {
themeName,
isDarkMode: themeName === "dark",
isLightMode: themeName === "light",
}
}

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/matchMedia

自定义 hook 实际上就是 mixin,把一段可复用的逻辑抽离出来

📒 搜索 JS、Go、Java、Python 的第三方库

https://openbase.com/

例如搜索 Redux 的替代方案:

https://openbase.com/js/redux/alternatives

⭐️ React hooks 状态管理方案解析

📒 深入理解 React Native 的新架构

照 React Native 团队去年发表的一篇 博客 的说法,他们会在今年发布新的架构。本文将会详细介绍新架构的相关内容。

https://medium.com/coox-tech/deep-dive-into-react-natives-new-architecture-fb67ae615ccd

📒 QUIC——快速UDP网络连接协议

  • QUIC 的Stream流基于Stream ID+Offset进行包确认,流量控制需要保证所发送的所有包offset小于 最大绝对字节偏移量 ( maximum absolute byte offset ), 该值是基于当前 已经提交的字节偏移量(offset of data consumed) 而进行确定的,QUIC会把连续的已确认的offset数据向上层应用提交。QUIC支持乱序确认,但本身也是按序(offset顺序)发送数据包
  • QUIC利用ack frame来进行数据包的确认,来保证可靠传输。一个ack frame只包含多个确认信息,没有正文
  • 如果数据包N超时,发送端将超时数据包N重新设置编号M(即下一个顺序的数据包编号) 后发送给接收端
  • 在一个数据包发生超时后,其余的已经发送的数据包依旧可以基于Offset得到确认,避免了TCP利用SACK才能解决的重传问题
tip

其实QUIC的乱序确认设计思想并不新鲜,大量网络视频流就是通过类似的基于UDP的RUDP、RTP、UDT等协议来实现快速可靠传输的。他们同样支持乱序确认,所以就会导致这样的观看体验:明明进度条显示还有一段缓存,但是画面就是卡着不动了,如果跳过的话视频又能够播放了

QUIC——快速UDP网络连接协议

· 12 min read
加菲猫

📒 一致性哈希算法解决的问题

  • 大多数网站都是 多节点部署,需要根据不同场景使用不同的 负载均衡策略
  • 最简单的算法就是使用 加权轮询,这种场景建立在每个节点存储的数据都是相同的前提下,访问任意一个节点都能得到结果
  • 当我们想提高系统的容量,就会将数据水平切分到不同的节点来存储,也就是将数据分布到了不同的节点。加权轮询算法是无法应对「分布式系统」的,因为分布式系统中,每个节点存储的数据是不同的,不是说任意访问一个节点都可以得到缓存结果的
  • 这种场景可以使用 哈希算法,对同一个关键字进行哈希计算,每次计算都是相同的值,这样就可以将某个 key 映射到一个节点了,可以满足分布式系统的负载均衡需求
  • 哈希算法最简单的做法就是进行取模运算,比如分布式系统中有 3 个节点,基于 hash(key) % 3 公式对数据进行了映射,如果计算后得到的值是 0,就说明该 key 需要去第一个节点获取
  • 但是哈希算法存在一个问题,如果 节点数量发生了变化,也就是在对系统做扩容或者缩容时,意味取模哈希函数中基数的变化,这样会导致 大部分映射关系改变,必须迁移改变了映射关系的数据,否则会出现查询不到数据的问题
  • 假设总数据条数为 M,哈希算法在面对节点数量变化时,最坏情况下所有数据都需要迁移,所以它的数据迁移规模是 O(M),这样数据的迁移成本太高了
  • 一致性哈希算法就很好地解决了分布式系统在扩容或者缩容时,发生过多的数据迁移的问题

微信一面:什么是一致性哈希?用在什么场景?解决了什么问题?

📒 前端项目 nginx 配置总结

有段时间没搞项目部署了,结果最近有同事在部署前端项目的时候,访问页面路由,响应都是 404,排查了半天,这里再总结一下。

前端单页应用路由分两种:哈希模式和历史模式。

哈希模式部署不会遇到啥问题,但是一般只用于本地调试,没人直接部署到生产环境。历史模式的路由跳转通过 pushStatereplaceState 实现,不会触发浏览器刷新页面,不会给服务器发送请求,且会触发 popState 事件,因此可以实现纯前端路由。

需要注意,使用历史模式的时候,还是有两种情况会导致浏览器发送请求给服务器:

  • 输入地址直接访问
  • 刷新页面

在这两种情况下,如果当前地址不是根路径,因为都是前端路由,服务器端根本不存在对应的文件,则会直接导致服务器直接响应 404。因此需要在服务器端进行配置:

server {
listen 80;
server_name www.bili98.com;
location / {
root /root/workspace/ruoyi-ui/dist;

# history 模式重点就是这里
try_files $uri $uri/ /index.html;
}
}
tip

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 的匹配优先顺序按照上面的顺序进行优先匹配,而且 只要某一个匹配命中直接退出,不再进行往下的匹配

剩下的普通匹配会按照 最长匹配长度优先级来匹配,就是谁匹配的越多就用谁。

前端到底用nginx来做啥

一份简单够用的 Nginx Location 配置讲解

📒 零基础理解 PostCSS 的主流程

📒 Jest + React Testing Library 单测总结

📒 使用lerna管理monorepo及发npm包实战教程

📒 从源码中来,到业务中去,React性能优化终极指南

📒 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 和私有路由 (也称作保护路由)

https://www.robinwieruch.de/react-router-private-routes/

📒 React Router v6 的身份验证简介

在一个简单的示例应用程序中,通过 React Router v6 实现身份验证的实用演练。

https://www.robinwieruch.de/react-router-authentication/

📒 Etsy 从 React 15.6 迁移到了 Preact (而不是 React 16)

在这篇 关于在 Etsy 更新 React 的文章中,对这个决定有一个完整的解释。但事实证明,拥有相同 API 的小型 React 替代品 Preact 是他们的正确选择。

https://twitter.com/sangster/status/1486382892326563845

📒 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 配置

前端组件/库打包利器rollup使用与配置实战

📒 Docker 使用,Gitlab CI 实践

GitLab CI 从入门到实践

📒 总结一下 Babel 插件开发基本操作

https://github.com/BoBoooooo/AST-Learning

📒 记一次 Vue2 迁移 Vue3 的实践总结

· 8 min read
加菲猫

📒 浏览器技术架构的演进过程和背景

📒 从chromium源码来窥探浏览器的渲染

📒 从 0 开始手把手带你搭建一套规范的 Vue3.x 项目工程环境

📒 为什么 React 中要使用 immutable 数据流

PureComponentmemo 中会将新旧 props 进行 浅层比对,逻辑非常简单:

function shallowEqual (objA: mixed, objB: mixed): boolean {
// 下面的 is 相当于 === 的功能
// 只是对 + 0 和 - 0,以及 NaN 和 NaN 的情况进行了特殊处理
// 第一关:基础数据类型直接比较出结果
if (is (objA, objB)) {
return true;
}
// 第二关:只要有一个不是对象数据类型就返回 false
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}

// 第三关:在这里已经可以保证两个都是对象数据类型,比较两者的属性数量
const keysA = Object.keys (objA);
const keysB = Object.keys (objB);

if (keysA.length !== keysB.length) {
return false;
}

// 第四关:比较两者的属性是否相等,值是否相等
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call (objB, keysA [i]) ||
!is (objA [keysA [i]], objB [keysA [i]])
) {
return false;
}
}

return true;
}

但浅层比较相当于只是比较第一层,还是会存在一些问题,如果修改深层嵌套的对象,浅层比较会认为相等。

为解决这个问题,可以手动在 shouldComponentUpdate 钩子中实现深层比对,但缺点就是浪费性能。最好的解决方案就是使用 immutable 数据流。immutable 对象内部采用的是多叉树的结构,只要有节点被修改,那么该节点和与之相关的所有父节点会直接拷贝到一个新的对象中(创建一个新的引用)。也就是说,修改任意一个子节点,改动都会冒泡到根节点,这样浅比较就能感知到数据改变了。

React Hooks 与 Immutable 数据流实战

📒 操作 JavaScript 的 AST

acornEspree@babel/parser 三种解析器用法说明

操作 JavaScript 的 AST

📒 React fiber 架构浅析

在 React 16 之前,vdom 以递归的方式进行 patch 和渲染,一个 vdom 节点可以表示如下:

class VNode {
type: string;
props: Record<string, any>;
children: VNode[];
}

在 React 16 之后引入了 fiber 架构,vdom 不再直接渲染,而是先转成 fiber,一个 fiber 节点可以表示如下:

class FiberNode {
type: string;
props: Record<string, any>;
dom: HTMLElement; // 提前创建 dom 节点
child?: FiberNode;
sibling?: FiberNode;
return?: FiberNode;
effectTag: string; // 做 diff,确定是增、删还是改
}

在 fiber 架构中,将 vdom 树结构转成了链表,每个 fiber 节点的 child 关联第一个子节点,然后通过 sibling 串联同一层级的节点,所有的节点可以 return 到父节点:

image

先把 vdom 转 fiber,也就是 reconcile 的过程,因为 fiber 是链表,就可以打断,用 schedule 来空闲时调度(requestIdleCallback)就行,最后全部转完之后,再一次性 render,这个过程叫做 commit。

schedule 就是通过空闲调度每个 fiber 节点的 reconcile(vdom 转 fiber),全部 reconcile 完了就执行 commit。

reconcile 除了将 vdom 转 fiber 外,还会做两件事:一个是 提前创建对应的 dom 节点,另一个是 做 diff,确定是增、删还是改,通过 schdule 的调度,最终把整个 vdom 树转成了 fiber 链表。

commit 就是对 dom 的增删改,把 reconcile 产生的 fiber 链表一次性添加到 dom 中,因为 dom 节点都提前创建好了、是增是删还是改也都知道了,所以这个阶段很快。每个 fiber 节点的渲染就是按照 child、sibling 的顺序以此插入到 dom 中,这里每个 fiber 节点都要往上找它的父节点(之前保存的 return 指针),因为我们只是新增,那么只需要 appendChild 就行。

手写简易版 React 来彻底搞懂 fiber 架构

📒 Chrome 99新特性:@layers 规则浅析

📒 WebVM.io:基于 Web 的“无服务端”虚拟 Linux 环境

浏览器端运行的 Linux 环境,基于 JavaScript 和 WebAssembly 的 CheerpX x86 虚拟化引擎驱动。虽然它不是一个完全基于 JavaScript 的项目,但它很好地展示了 Web 技术的发展程度。它已经内置了 Node v10.24.0,但要注意它首次加载速度可能会有点慢。

https://webvm.io/

这里有一篇关于它如何工作的文章。

https://leaningtech.com/webvm-server-less-x86-virtual-machines-in-the-browser/

📒 如何使用 Vue 3、Vite、Pinia 开发应用程序

非常完善的开发、测试、部署指南。

https://labs.pineview.io/learn-how-to-build-test-and-deploy-a-single-page-app-with-vue-3-vite-and-pinia/

📒 用代码分割来提高打包 JavaScript 时的性能

https://www.smashingmagazine.com/2022/02/javascript-bundle-performance-code-splitting/

📒 提升 VSCode 扩展插件的运行速度

插件开发者必读

image

https://jason-williams.co.uk/speeding-up-vscode-extensions-in-2022

📒 Babel 发布 v7.17.0

该版本对 装饰器提案 的支持已稳定,还对装饰器的解析和转换进行了支持。

https://babeljs.io/blog/2022/02/02/7.17.0

📒 使用 Streams 模块构建高性能的 Node 应用

https://blog.appsignal.com/2022/02/02/use-streams-to-build-high-performing-nodejs-applications.html

📒 Node.js 新增 Fetch API

对 Fetch API (一般是浏览器端用来获取资源)的支持已经合并到 Node.js,将在提供 ‑‑experimental‑fetch 标志后可以开启,Node v18 或者更高版本会默认启用。

https://fusebit.io/blog/node-fetch/

⭐️ 来自未来,2022 年的前端人都在做什么?

⭐️ 最全的前端性能定位总结

📒 接近天花板的TS类型体操,看懂你就能玩转TS了

📒 2022年必会Vue3.0学习 (强烈建议)

📒 如何利用 SCSS 实现一键换肤

📒 手写 JS 引擎来解释一道赋值面试题

📒 10 分钟讲述 React 的故事

https://www.youtube.com/watch?v=Wm_xI7KntDs

📒 2022 年值得关注的 React 趋势

https://www.chakshunyu.com/blog/what-you-should-definitely-look-out-for-in-react-in-2022/

📒 React 18 中的自动批处理(Automatic Batching)

https://blog.bitsrc.io/automatic-batching-in-react-18-what-you-should-know-d50141dc096e?gi=aa52794e9a07

📒 React Mentions:在 Textarea 中提及某人

· 8 min read
加菲猫

📒 很多人上来就删除的 package.json,还有这么多你不知道的

📒 React hooks 使用注意事项

惰性初始化 State:

// React hook 会在每次组件重新渲染的时候调用
const [count, setCount] = React.useState(ExpensiveCal());

// 如果 useState 的初始值需要通过复杂计算获取,可以传入一个函数惰性初始化
// 这个函数只会在组件挂载的时候执行一次,后续更新都不会执行
const [count, setCount] = React.useState(() => ExpensiveCal());

不需要视图渲染的变量,不要用 useState

const App: React.FC<{}> = () => {
// count 与视图渲染无关
// 如果使用 useState,每次 count 变化都会触发组件重新渲染
const [count, setCount] = React.useState(0);
// 这里推荐使用 useRef
const count = React.useRef(0);

const handleClick = () => setCount(c => c + 1);

return (
<button onClick={handleClick}>Counter</button>
)
}

函数式更新:

const [count, setCount] = React.useState(0);

// 下面这样虽然调用了两次
// 但由于一次渲染中获取的 count 都是闭包中老的值
// 因此最终 count 还是 1
setCount(count + 1);
setCount(count + 1);

// 如果要获取到上一次更新的值,可以使用函数式更新
// 最终 count 为 2
setCount(c => c + 1);
setCount(c => c + 1);

useEffect 清除副作用:

React.useEffect(() => {
// ...
return () => {
// useEffect 的回调函数中可以返回一个函数
// 这个函数会在组件卸载的时候执行
// 用于清理各种事件监听器、定时器等
}
}, []);

React.useCallback 需要配合 React.memo 使用,其中任意一个单独使用是没用的。

tip

React.useCallback 使用的一个场景是:

  • 一个父组件中有一个复杂的自定义组件,需要传入事件处理函数作为 prop,为避免父组件渲染导致该子组件重新渲染,使用 React.memo 包裹一下;
  • 包裹之后发现父组件重新渲染,该子组件还是会重新渲染,这是因为事件处理函数在父组件每次渲染的时候都重新生成,因而传入子组件的 prop 变化导致 React.memo 失效;
  • 将事件处理函数用 React.useCallback 包裹一下,对事件处理函数进行缓存,避免每次父组件渲染都重新生成,这样父组件重新渲染就不会导致子组件重新渲染;
  • 需要注意 React.useCallback 缓存本身也是有性能开销的,因此只有在子组件渲染比较昂贵的时候,才进行缓存处理;

📒 Golang 中的包管理机制

Golang 中的包管理使用 go.mod 文件,可以使用下面的命令在项目根目录初始化一个 go.mod

# 初始化一个 v0 或者 v1 的包
$ go mod init example.com/m
# 初始化指定版本的包
$ go mod init example.com/m/v2

安装依赖:

$ go get -u github.com/gin-gonic/gin

-u 安装全局变量类似 npm i -g cobra

如果直接下载请求超时,可以设置镜像源:

$ go env -w GO111MODULE=on
$ go env -w GOPROXY=https://goproxy.cn,https://goproxy.io,direct

类似 npm config set registry

安装之后就可以看到 go.mod 里面多了些东西:

module github.com/Jiacheng787/goexample

go 1.17

require (
github.com/gin-gonic/gin v1.7.7
)

下载项目依赖:

$ go get ./...

三分钟掌握Go mod常用与高级操作

📒 如何解决 CSS 兼容性问题

对于 JS 的兼容性,我们可以使用 Babel 进行语法转换以及对 API 进行 polyfill。那么对于 CSS 的兼容性如何适配呢?可以使用 PostCSS,最完善的 CSS 工程化解决方案:

  • autoprefixer:根据 Can I Use 的数据给 CSS 属性添加厂商前缀
  • postcss-preset-env:允许使用一些提案阶段的特性

此外还提供各种插件:

  • postcss-modules:CSS 模块化
  • postcss-initial:重置默认样式
  • sugarss:支持缩进语法编写 CSS 样式

https://github.com/postcss/postcss

📒 How To Process Images in Node.js With Sharp

📒 字节跳动开源项目

📒 前端项目 babel 配置

编译一个前端项目,一般需要安装如下依赖:

  • @babel/core:核心库
  • babel-loader:配合 Webpack 打包场景使用
  • @babel/preset-env:语法转换的预设插件集,同时支持 api 兼容
  • @babel/preset-react:编译 React 的 JSX 语法
  • @babel/preset-typescript:可选,编译 TypeScript 语法
tip

@babel/core 是核心库,里面包含:

  • @babel/parser:一个 ast 解析器,之前叫 Babylon,基于 acorn 魔改而来,负责解析生成 ast
  • @babel/traverse:负责通过访问者模式遍历并操作 ast 节点
  • @babel/generator:负责根据 ast 生成代码

babel-loader 用于配合 Webpack 打包场景使用,如果想通过命令行的方式使用,则需要安装 @babel/cli

@babel/preset-env 的 api 兼容是通过引入 core-js polyfill 实现的。core-js 引入有多种方式,可以配置 entry,即在入口文件处根据根据 browserslist 配置需要适配的目标环境全量引入 polyfill,也可以配置 usage,根据 browserslist 配置和实际用的 api 按需引入 polyfill。@babel/preset-env 是通过全局污染的形式引入的,一般在前端项目中没问题,但是作为第三方库就不合适了,这时候需要使用 @babel/plugin-transform-runtime 通过沙箱机制引入 polyfill,这种引入方式有个缺点,无法根据 browserslist 配置动态调整引入的 polyfill。

@babel/preset-typescript 实际上就是简单删除掉类型注解。因为 Babel 是单文件处理,不可能进行类型检查,类型检查可以交给 VSCode 插件,或者 ForkTsCheckerWebpackPlugin 单独起一个进程进行类型检查,这时候 tsc 的作用就是类型检查器,需要配置 "noEmit": true

📒 写文章集合

  • Redux 在完善下,增加 UI-binding
  • 深入源码分析 Koa 中间件与洋葱圈模型
  • 前端项目的 env 文件是如何被加载的
  • Webpack 打包的图片和字体的哈希是如何生成的 - file-loader 源码分析

· 8 min read
加菲猫

📒 推荐使用 stylus

推荐使用 stylus,所有的 {}: 以及 ; 都是可省略的:

.page
padding-bottom 2rem
display block

.content-lock
display none
text-align center
padding 2rem
font-size 1em

这就类似为什么建议使用 yaml 替代 json,在 yaml 中不需要引号,简单省事

📒 页面性能优化技巧

分析代码执行耗时可以通过 火焰图,分析内存占用情况可以通过 堆快照

⭐️ react-use - 一个 React Hooks 库

📒 Next.js 提供的渲染方式

  • SSR: Server-side rendering (服务端渲染)
  • SSG: Static-site generation (静态站点生成)
  • CSR: Client-side rendering (客户端渲染)
  • Dynamic routing (动态路由)
  • ISR: Incremental Static Regeneration (增量静态再生)
tip

CSR、SSR、SSG 的区别?

CSR 是在用户浏览器端调接口请求数据进行渲染;SSR 是在用户请求页面的时候,服务器端请求数据并进行渲染;SSG 是直接在构建阶段就进行渲染,一般用于文档网站。

📒 Node 案发现场揭秘 —— 未定义 “window” 对象引发的 SSR 内存泄露

📒 从头开始,彻底理解服务端渲染原理(8千字汇总长文)

📒 【7000字】一晚上爆肝浏览器从输入到渲染完毕原理

📒 爆肝三天,学习Scss-看这篇就够了

⭐️ 编译技术在前端的实践(二)—— Antlr 及其应用

⭐️ 编译技术在前端的实践(一)—— 编译原理基础

📒 如何实从零实现 husky

看下如何做 测试驱动开发

从零实现husky

📒 如何让一个构造函数只能用 new 调用

使用 ES6 class 会检查是否通过 new 调用,而普通构造函数不会检查是否通过 new 调用,这种情况下需要手动进行判断,通常都会这样做:

function MyClass() {
if (!(this instanceof MyClass)) {
throw new Error("MyClass must call with new");
}
// ...
}

这样的话,如果不通过 new 调用,就会抛出异常。其实更好的方案是进行兼容处理,即不使用 new 调用,自动改用 new 调用:

function MyClass() {
if (!(this instanceof MyClass)) {
// 如果没有使用 `new` 调用,自动改用 `new` 调用
// 通过 `return` 中断函数执行,并返回创建的实例
return new MyClass();
}
// ...
}

📒 为什么 React Hook 底层使用链表而不是数组

React Hooks 核心实现

深入浅出 React

React 技术揭秘

📒 React 17 架构

图解 React 原理系列

React16架构

📒 数组的 flatMap 方法

数组的 [].map() 可以实现一对一的映射,映射后的数组长度与原数组相同。有时候需要过滤掉一些元素,或者实现一对多的映射,这时候只用 map 就无法实现了。这种情况下就可以使用 flatMap

// 需要过滤掉 0,并且使其余各元素的值翻倍
const numbers = [0, 3, 6];

// 常规方法是 map 和 filter 搭配
const doubled = numbers
.filter(n => n !== 0)
.map(n => n * 2)

// 使用 flatMap 实现
const doubled = numbers.flatMap(number => {
return number === 0 ? [] : [2 * number];
})

此外还可以实现一对多的映射:

const numbers = [1, 4];
const trippled = numbers.flatMap(number => {
return [number, 2 * number, 3 * number];
})
console.log(trippled); // [1, 2, 3, 4, 8, 12]
tip

flatMap 实际上是先 mapflat,理解了这一点就能掌握了

📒 如何用 TypeScript 配置一个 Node 项目

📒 Remix vs Next.js

📒 你应该知道的三个 React 组件设计模式

📒 V8 Promise源码全面解读,其实你对Promise一无所知

⭐️ 60+ 实用 React 工具库,助力你高效开发!

⭐️ 如何编写更好的 JSX 语句

查看详情

列表不为空的时候进行渲染:

// 注意这种写法有 bug
// 如果 data 数组为空,则会直接渲染 `0` 到页面上
{data.length && <div>{data.map((d) => d)}</div>}

// 使用 && 的时候需要手动转换布尔值
data.length > 0 && jsx
!!data.length && jsx
Boolean(data.length) && jsx

不要使用 props 传递的 React 元素作为判断条件:

// 这样的判断不准确
// props.children 可能是一个空数组 []
// 使用 children.length 也不严谨,因为 children 也可能是单个元素
// 使用 React.Children.count(props.children) 支持单个和多个 children
// 但是对于存在多个无效节点,例如 false 无法准确判断
// 使用 React.Children.toArray(props.children) 可以删除无效节点
// 但是对于一个空片段,例如 <></> 又会被识别为有效的元素
// 所以为了避免出错,建议不要这样判断
const Wrap = (props) => {
if (!props.children) return null;
return <div>{props.children}</div>
};

重新挂载还是更新:

// 使用三元运算符分支编写的 JSX 看上去就像完全独立的代码
{hasItem ? <Item id={1} /> : <Item id={2} />}

// 但实际上 hasItem 切换时,React 仍然会保留挂载的实例,然后更新 props
// 因此上面的代码实际上等价于下面这样
<Item id={hasItem ? 1 : 2} />

// 一般来讲不会有什么问题,但是对于非受控组件,就可能导致 bug
// 例如 mode 属性变化,会发现之前输入的信息还在
{mode === 'name'
? <input placeholder="name" />
: <input placeholder="phone" />}

// 由于 React 会尽可能复用组件实例
// 因此我们可以传递 key,告诉 React 这是两个完全不一样的元素,让 React 强制重新渲染
{mode === 'name'
? <input placeholder="name" key="name" />
: <input placeholder="phone" key="phone" />}

// 或者使用 && 替代三元运算符
{mode === 'name' && < input placeholder = "name" /> }
{mode !== 'name' && < input placeholder = "phone" /> }

// 相反,如果在同一个元素上的逻辑条件不太一样
// 可以试着将条件拆分为两个单独的 JSX 提高可读性
<Button
aria-busy={loading}
onClick={loading ? null : submit}
>
{loading ? <Spinner /> : 'submit'}
</Button>

// 可以改为下面这样
{loading
? <Button aria-busy><Spinner /></Button>
: <Button onClick={submit}>submit</Button>}

// 或者使用 &&
{loading && <Button key="submit" aria-busy><Spinner /></Button>}
{!loading && <Button key="submit" onClick={submit}>submit</Button>}

写好 JSX 条件语句的几个建议

📒 Node.js 十大设计缺陷 - Ryan Dahl - JSConf EU

📒 为什么说 WebAssembly 是 Web 的未来?

📒 浅析TypeScript Compiler 原理

📒 TypeScript 4.6 beta 发布:递归类型检查增强、参数的控制流分析支持、索引访问的类型推导

· 7 min read
加菲猫

📒 Golang 如何根据指针访问对应的值

原始类型需要手动使用 * 操作符,复杂对象会自动解除指针引用:

num := &42
fmt.Println(num) // 打印的是内存地址
fmt.Println(*num) // 42

ms := &myStruct{foo: 42}
(*ms).foo = 17
fmt.Println((*ms).foo) // 17
// 对于复杂对象,直接操作就行
ms.foo = 17
fmt.Println(ms.foo) // 17

📒 Golang 创建对象指针的三种方式

Golang 中所有的赋值操作都是 copy,例如原始类型、arraystruct,有两种例外:mapslice,它们具有内部指针,在赋值的时候传递指针类型。

// 第一种:对已有的值类型使用 `&` 操作符
ms := myStruct{foo: 42}
p := &ms

// 第二种:在初始化的时候使用 `&` 操作符
p := &myStruct{foo: 42}

// 第三种:使用 `new` 关键字,这种方法不能在初始化的时候进行赋值
var ms *myStruct = new(myStruct)

📒 如何渲染虚拟 DOM

所谓虚拟 DOM 其实就是一棵多叉树,可以使用下面的结构表示:

class VDOM {
type: ElementTagName;
props: ElementProps;
children: VDOM[];
}

渲染虚拟 DOM,很明显要用递归,对不同的类型做不同的处理:

  • 如果是文本类型,就要用 document.createTextNode 创建文本节点;
  • 如果是元素类型,就要用 document.createElement 创建元素节点,元素节点还有属性要处理,并且要递归渲染子节点;

实现 render 函数如下:

const render = (vdom, parent = null) => {
const mount = (el) => {
if (!parent) return el;
// 如有父节点则挂载到父节点,组装为 DOM 树
return parent.appendChild(el);
}
if (isTextVdom(vdom)) {
// 创建文本节点
return mount(document.createTextNode(vdom));
} else if (isElementVdom(vdom)) {
// 创建元素节点
const dom = mount(document.createElement(vdom.type));
// 递归渲染子节点,这里使用深度优先遍历
for (const child of vdom.children) {
render(child, dom);
}
// 给元素添加属性
for (const prop in vdom.props) {
setAttribute(dom, prop, vdom.props[prop]);
}
return dom;
}
};

如何判断文本节点:

function isTextVdom(vdom) {
return typeof vdom == 'string' || typeof vdom == 'number';
}

如何判断元素节点:

function isElementVdom(vdom) {
return typeof vdom == 'object' && typeof vdom.type == 'string';
}

如何处理样式、事件、属性:

const setAttribute = (dom, key, value) => {
if (typeof value == 'function' && key.startsWith('on')) {
// 事件处理,使用 `addEventListener` 设置
const eventType = key.slice(2).toLowerCase();
dom.addEventListener(eventType, value);
} else if (key == 'style' && typeof value == 'object') {
// 样式处理,合并样式
Object.assign(dom.style, value);
} else if (typeof value != 'object' && typeof value != 'function') {
// 属性处理,使用 `setAttribute` 设置
dom.setAttribute(key, value);
}
}

📒 能用js实现的最终用js实现,Shell脚本也不例外

📒 heapify:最快的 JavaScript 优先级队列库

📒 easyjson:Golang 中的序列化库,比 encoding/json 快 4-5 倍

📒 fast-json-stringify:比 JSON.stringify 快两倍

📒 六千字详解!vue3 响应式是如何实现的?

📒 Nodejs 如何将图片转为 base64

使用 Buffer 对象:

import fs from "node:fs";
import path from "node:path";

const raw = fs.readFileSync(path.join(__dirname, './2333.png'), 'binary');
const buf = Buffer.from(raw, 'binary');
const string = buf.toString('base64');

同理可以将 base64 转回图片:

const raw =  Buffer.from(string, 'base64').toString('binary');

📒 Nodejs 如何实现图片处理

推荐使用 sharp 这个库,可以实现图片压缩,转 JPEG、PNG、WebP 等格式:

https://github.com/lovell/sharp

📒 如何打印 26 个字母的字符串

一行代码搞定:

String.fromCharCode(...Array.from({ length: 26 }, (_, index) => 97 + index));
// 'abcdefghijklmnopqrstuvwxyz'

📒 如何用 Map 实现 Set

关于 Map 和 Set 是两个抽象数据结构,Map 存储一个键值对集合,其中键不重复,Set 存储一个不重复的元素集合。本质上 Set 可以视为一种特殊的 Map,Set 其实就是 Map 中的键:

class NewSet<T extends unknown> {
private collection: Map<T, undefined>;

constructor(iterable: T[] = []) {
this.collection = new Map(
iterable.map(it => [it, undefined])
);
}
}

📒 方法重载与参数默认值

为了支持可变参数,在 Java 中通过 方法重载 实现,通过定义多个方法签名,根据实际调用传递的参数去匹配签名。在 TypeScript 中也提供了方法重载特性,但在开发中很少用到,一般都通过 参数默认值 实现可变参数:

type NewSet<T> = (iterable: T[] = []) => void

注意使用参数默认值之后,TS 会自动将这个参数推导为可变参数,例如上面这个会推导为 NewSet<T>(iterable?: T[]): void

📒 项目常用工具库

  • dayjs:与 moment 的 API 设计保持一样,但体积仅有 2KB;
  • qs:解析 URL query 参数的库;
  • js-cookie:简单、轻量的处理 cookie 的库;
  • flv.js:bilibili 开源的 HTML5 flash 播放器,使浏览器在不借助 flash 插件的情况下可以播放 flv;
  • vConsole:一个轻量、可拓展、针对手机网页的前端开发者调试面板;
  • animate.css:一个跨浏览器的 css3 动画库,内置了很多典型的 css3 动画,兼容性好,使用方便;
  • lodash:一个一致性、模块化、高性能的 JavaScript 实用工具库;

⭐️ elf: 使用 RxJs 的响应式状态管理

📒 如何防止 CSS 样式污染

  • 使用命名约定
  • CSS Modules
  • CSS in JS

其中命名约定最流行的方式是 BEM 101。它代表了 BlockElementModifier 方法。

[block]__[element]--[modifier]
/* Example */
.menu__link--blue {
...
}

📒 现代配置指南——YAML 比 JSON 高级在哪?

📒 前端架构师神技,三招统一团队代码风格

📒 前端架构师的 git 功力,你有几成火候?

· 9 min read
加菲猫

📒 实现一个 WebAssembily 版本的 Python 解释器

  • wasm 可以把代码编译出来,但是能否执行
  • 如果 Python 代码涉及系统调用,例如代码中经常需要进行文件 IO,这种情况下 wasm 能否实现

https://github.com/pyodide/pyodide

📒 Webpack5 配置了 devServer.hot = true 是否会自动配置 HotModuleReplacementPlugin

📒 看下 axios 源码,响应拦截中第一个回调 reject 能否进入第二个回调

📒 webpack-dev-server 如何配置代理

查看详情

在 CRA 搭建的项目中,我们知道可以在 src/setupProxy.js 文件中写入代理配置:

const proxy = require('http-proxy-middleware');

module.exports = function(app) {
app.use(
proxy(
'/course',
{
target: 'https://ke.study.163.com',
changeOrigin: true,
},
),
)
}

那么手动搭建的项目该如何配置代理呢?我们看一下 CRA 源码:

// react-scripts/config/paths.js:87

module.exports = {
// ...
proxySetup: resolveApp('src/setupProxy.js'),
// ...
}

然后去找哪里用到了 proxySetup

// react-scripts/config/webpackDevServer.config.js:112

onBeforeSetupMiddleware(devServer) {
// Keep `evalSourceMapMiddleware`
// middlewares before `redirectServedPath` otherwise will not have any effect
// This lets us fetch source contents from webpack for the error overlay
devServer.app.use(evalSourceMapMiddleware(devServer));

if (fs.existsSync(paths.proxySetup)) {
// This registers user provided middleware for proxy reasons
require(paths.proxySetup)(devServer.app);
}
},

看了下上面的配置,说明应该是这么用的:

const compiler = webpack(config);
const devServer = new WebpackDevServer(options, compiler);

devServer.app.use(
proxy(
'/course',
{
target: 'https://ke.study.163.com',
changeOrigin: true,
},
),
)

📒 不优雅的 React Hooks

📒 为什么可以用函数模拟一个模块

在一个模块中,有一些属性和方法是私有的,另外一些是对外暴露的:

// main.js
let foo = 1;
let bar = 2;

export const getFoo = () => foo;
export const getBar = () => bar;
const defaultExport = () => foo + bar;
export default defaultExport;

// index.js
import main, { getFoo, getBar } from "./main";

这种行为就可以通过函数模拟出来,其中私有变量、方法以闭包的形式实现,这样只有模块内部才能访问:

const main = (function() {
let foo = 1;
let bar = 2;
const getFoo = () => foo;
const getBar = () => bar;
const defaultExport = () => foo + bar;

return {
getFoo,
getBar,
default: defaultExport
}
})();
tip

可以看到给默认导出加了一个 deafult 属性。

另外推荐看看 browserify 这个库,如何在浏览器端实现 CommonJS 模块机制:

https://browserify.org/

📒 Webpack 中 loader 处理流程

有点像责任链模式,上一个函数的返回值会作为参数传入下一个函数。需要注意使用 call 方法让每个 loader 内部可以获取到 loaderAPI:

import { readFileSync } from 'node:fs';

const loaders = [];
const raw = readFileSync('xxx');

const loaderAPI = {
emitFile: () => {},
}

const parsed = loaders.reduce(
(accu, cur) => cur.call(loaderAPI, accu),
raw
);

📒 字体文件的 hash 是如何生成的,file-loader 中如何处理的

写一篇文章:《你不知道的 Webpack loader —— file-loader 源码探秘》

webpack 源码解析:file-loader 和 url-loader

file-loader - GitHub

loader-utils - GitHub

📒 Golang 编译为 WebAssembly

在 Golang 中可以使用 syscall/js 这个库与 JS 环境进行交互,可以调用 JS 的 API,以及传递 JSON 数据:

package main

import (
"encoding/json"
"fmt"
"syscall/js"
)

type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}

func main() {
// Work around for passing structs to JS
frank := &Person{Name: "Frank", Age: 28}
p, err := json.Marshal(frank)
if err != nil {
fmt.Println(err)
return
}
obj := js.Global().Get("JSON").Call("parse", string(p))
js.Global().Set("aObject", obj)
}

Compiling Go to WebAssembly

📒 Golang 中的指针

对于原始类型来说,赋值就等于 copy,相当于在内存中创建一个一模一样的值,具有不同的内存地址:

func main() {
a := 42
b := a
fmt.Println(a, b) // 42 42
a = 27
fmt.Println(a, b) // 27 42
}

可以通过 & 操作符取到内存地址:

func main() {
var a int = 42
var b *int = &a
fmt.Println(a, b) // 42 0×1040a124
}

还可以通过 * 操作符根据内存地址访问对应的值:

func main() {
var a int = 42
var b *int = &a
fmt.Println(a, *b) // 42 42
}

由于 b 实际持有的是 a 的指针引用,因此修改 a 会导致 b 指向的值发生变化:

func main() {
var a int = 42
var b *int = &a
fmt.Println(a, *b) // 42 42
a = 27
fmt.Println(a, *b) // 27 27
*b = 14
fmt.Println(a, *b) // 14 14
}

📒 Golang 中的 struct

注意 structslicemap 不同,下面这个操作实际上是完整 copy 了一个对象,内存开销较大:

package main

import (
"fmt"
)

type Doctor struct {
name string
}

func main() {
aDoctor := Doctor{
name: "John Pertwee"
}
anotherDoctor := aDoctor
anotherDoctor.name = "Tom Baker"
fmt.Println(aDoctor) // {John Pertwee}
fmt.Println(anotherDoctor) // {Tom Baker}
}

可以使用 & 操作符拿到对象的指针进行赋值,这时候两边就是联动的:

func main() {
aDoctor := Doctor{
name: "John Pertwee"
}
anotherDoctor := &aDoctor
anotherDoctor.name = "Tom Baker"
fmt.Println(aDoctor) // {Tom Baker}
fmt.Println(anotherDoctor) // &{Tom Baker}
}

注意 array 进行赋值也会 copy:

func main() {
a := [3]int{1, 2, 3}
b := a
fmt.Println(a, b) // [1, 2, 3] [1, 2, 3]
a[1] = 42
fmt.Println(a, b) // [1, 42, 3] [1, 2, 3]
}

但如果将 array 改为 slice,赋值传递的就是指针:

func main() {
a := []int{1, 2, 3}
b := a
fmt.Println(a, b) // [1, 2, 3] [1, 2, 3]
a[1] = 42
fmt.Println(a, b) // [1, 2, 3] [1, 2, 3]
}

📒 年终盘点:2022基于Monorepo的首个大趋势-TurboRepo

⭐️ 2022年如何成为一名优秀的大前端Leader?

📒 GitHub 定时任务

下面的代码中,on 字段指定了两种触发条件,一个是代码 push 进仓库,另一种是定时任务,每天在国际标准时间21点(北京时间早上5点)运行。

on:
push:
schedule:
- cron: '0 21 * * *'

定时任务配置参考:

https://github.com/lxchuan12/juejin-actions

另外推荐一个项目,可以使用 curl wttr.in 命令获取天气预报:

https://github.com/chubin/wttr.in

📒 如何开发一个 CLI 工具

参考下尤大的项目:

const templateDir = path.join(__dirname, `template-${template}`)

const write = (file, content) => {
const targetPath = renameFiles[file]
? path.join(root, renameFiles[file])
: path.join(root, file)
if (content) {
fs.writeFileSync(targetPath, content)
} else {
copy(path.join(templateDir, file), targetPath)
}
}

const files = fs.readdirSync(templateDir)
for (const file of files.filter((f) => f !== 'package.json')) {
write(file)
}

注意这里有两个文件要处理下,一个是给 package.json 修改包名:

const pkg = require(path.join(templateDir, `package.json`))

pkg.name = packageName || targetDir

write('package.json', JSON.stringify(pkg, null, 2))

还有是 .gitignore 修改文件名:

const renameFiles = {
_gitignore: '.gitignore'
}

https://github.com/vitejs/vite/blob/main/packages/create-vite/index.js

📒 命令行工具开发技术栈

  • chalk/kolorist
  • inquirer/prompts
  • ora
  • semver
  • pkg-install
  • ncp
  • commander/yargs
  • execa(个人觉得 Node 原生 child_processexec 就够用了)
  • minimist
tip

网上一些文章也都实现了递归拷贝文件,但是是否考虑到了跨平台,可以看下 ncp 的实现

https://github.com/AvianFlu/ncp

Node.js 原生的 child_process.exec 也可以执行命令,看下 execa 是如何支持 Promise 的

https://github.com/sindresorhus/execa

现在开发已经不需要自己组装 pick 了,common-binoclif 这两个,约定式路由。

另外脚手架工具,可以看看 plopyeoman,一个是基于 actioninquirer 的生态,一个是内核加自定义模板项目。

其实最简单的脚手架,不是通过cli界面选择模板,然后到 github 上去下载对应的模板文件,而是 start-kit

https://github.com/digipolisantwerp/starter-kit-ui_app_nodejs

📒 「前端基建」探索不同项目场景下Babel最佳实践方案

📒 说不清rollup能输出哪6种格式😥差点被鄙视

📒 【手把手】学会VS Code"任务"神技,成为项目组最靓的崽!