Skip to main content

11 posts tagged with "git"

View All Tags

· 13 min read
加菲猫

📒 rollup 配置优化方案

// 打包引用主入口文件
const appIndex = ["ESM", "CJS"].map(format => ({
input: path.resolve(__dirname, 'index.js'),
format,
external: ["./pyodide.worker.js"],
output: {
file: path.resolve(__dirname, 'dist/client', 'env.mjs'),
sourcemap: true
}
}));

// 打包 worker 文件
// 目的是让 rollup 也处理下这个文件
const worker = [
{
input: path.resolve(__dirname, 'pyodide.worker.js'),
output: {
file: path.resolve(__dirname, 'dist/client', 'pyodide.worker.js'),
sourcemap: true
}
}
]

export default [...appIndex, ...worker];

📒 Git merge 三种策略

  • git merge:默认使用 fast-forward 方式,git 直接把 HEAD 指针指向合并分支的头,完成合并。属于“快进方式”,不过这种情况如果删除分支,则会丢失分支信息。因为在这个过程中没有创建 commit
  • git merge --no-ff:强行关闭 fast-forward 方式,可以保存之前的分支历史。能够更好的查看 merge 历史,以及 branch 状态
  • git merge --squash:用来把一些不必要 commit 进行压缩,比如说,你的 feature 在开发的时候写的 commit 很乱,那么我们合并的时候不希望把这些历史 commit 带过来,于是使用 --squash 进行合并,需要进行一次额外的 commit 来“总结”一下,完成最终的合并

📒 Git 如何变基拉取代码

在本地 commit 之后,下一步一般会执行 git pull 合并远程分支代码。我们知道 git pull 相当于 git fetch && git merge,通过 merge 方式合并代码,缺点就是会导致时间线比较混乱,出现大量没用的 commit 记录,给 Code Review 带来不便。另一种方式是变基拉取:

$ git pull --rebase

在变基操作的时候,我们不去合并别人的代码,而是直接把我们原先的基础变掉,变成以别人修改过后的新代码为基础,把我们的修改在这个新的基础之上重新进行。变基的好处之一是可以使我们的时间线变得非常干净。

变基操作的时候,会创建一个临时的 rebasing branch,如有冲突,合并完冲突的文件,添加到暂存区后,执行:

$ git rebase --continue

此时会进入 commit message 编辑界面,输入 :q 就会提交 commit,后续只要推送远程仓库即可。

如果不想继续变基操作,执行:

$ git rebase --abort

📒 Git 操作之 git push -f

在开发一个项目的时候,本人将自己的 feature 分支合并到公共 test 分支,并且在测试环境部署成功。

几天后再去看的时候,发现测试环境提交的代码都不见了,本人在 test 分支的提交记录也都没了,只有另外一个同事留下的提交记录。最后重新将 feature 分支合到 test,再次部署到测试环境。

这个事情虽然影响不是很大,毕竟只是部署测试环境的分支,没有影响到 feature 分支,但是后来一直在想,究竟什么操作可以覆盖别人的提交记录。想来想去,应该只有下面几种情况:

  • git reset:回退版本,实际上就是向后移动 HEAD 指针,该操作不会产生 commit 记录
  • git revert:撤销某次操作,用一次新的 commit 来回滚之前的 commit,HEAD 继续前进,该操作之前和之后的 commit 和 history 都会保留
  • git push -f:将自己本地的代码强制推送到远程仓库。当使用 git push 推送报错时,除了耐心解决冲突再提交之外,还可以使用这个命令强制推送,但通常会造成严重后果,例如覆盖别人的提交记录

由于开发一般都在自己的 feature 分支上,只有在需要测试的时候才会合并 test 分支,因此使用 git reset 可能性不大。git revert 更不可能,不仅不会修改 history,同时还会创建一条新的 commit 记录。因此可能性最大的就是 git push -f 了。

一般我们推送代码之前都会习惯性执行 git pull,就算不执行 git pull,直接推送,只要有人在你之前推送过也会报错:

$ git push -u origin main

error: failed to push some refs to 'https://github.com/Jiacheng787/git-operate-demo.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

在这种情况下,常规做法是执行 git pull 更新本地提交记录,如有冲突则解决冲突,然后再次推送。另一种做法就是强制推送:

$ git push -f origin main

可以看到就算没有事先 git pull 也不会报错,但是这样会导致远程仓库的提交记录被覆盖,远程仓库的提交记录变成了你本地的记录,你上次同步代码之后别人的提交记录都丢失了。

如何删除所有 commit 记录

初始化一个仓库:

$ git init

本地提交:

$ git add .
$ git commit -m "Initial commit"

下一步强制推送到远程仓库即可:

$ git branch -m main
$ git remote add origin <REPO_TARGET>
$ git push -f origin main

📒 Docker 容器如何实现持久化

Docker 容器本身是无状态的,无法持久化存储,在 Docker 容器中构建前端项目,如何缓存 node_modules 从而提升构建效率?可以给 Docker 容器挂载外部数据卷,映射到本地文件系统,就可以实现持久化存储。

📒 复盘 Node 项目中遇到的13+常见问题和解决方案

📒 GitHub 最受欢迎的Top 20 JavaScript 项目

GitHub 最受欢迎的Top 20 JavaScript 项目

📒 保护自己 - 深入链路探究网络安全

📒 50 多个提高前端人效率的工具、网站和书籍整理

📒 如何成为一个优秀的复制粘贴工程师

📒 原创精选荟萃(2022.03.14)

📒 只会用传统开发模式?10分钟教你玩转敏捷!

📒 如何提升 GitHub Page 访问速度

打包构建

使用 GitHub Action 作为 CI 环境,使用 Docker 进行构建,充分利用缓存,如 package.json 没变就不重复装包。

部署

打包之后将静态资源上传至阿里云 OSS(需要配置 Webpack 的 output.publicPath),提升页面加载速度。

HTML 页面暂时可以不上传,使用 GitHub Page 托管,这样访问速度可以保证,但是不能解决 GitHub Page 偶尔会挂的问题。还是要将 HTML 页面上传(Cache-Control:no-cache),此时整个网站完全托管在阿里云 OSS 上面,需要域名备案。

tip

如果页面需要后端服务,也可以不用服务器,直接使用 云数据库 + 云存储 + Serverless 云函数,免去运维成本。

📒 Golang 算法

https://github.com/fangbinwei/algorithm-practice

📒 Golang 项目参考

https://github.com/fangbinwei/aliyun-oss-website-action

📒 你知道的前端优化手段

📒 函数式编程(FP)

lodash 中的 FP

在lodash的官网上,我们很容易找到一个 function program guide 。在 lodash / fp 模块中提供了实用的对函数式编程友好的方法。里面的方式有以下的特性:

  • 不可变
  • 已柯里化(auto-curried)
  • 迭代前置(iteratee-first)
  • 数据后置(data-last)

假如需要将字符串进行如下转换,该如何实现呢?

例如:CAN YOU FEEL MY WORLD -> can-you-feel-my-world

import _ from 'lodash';

const str = "CAN YOU FEEL MY WORLD";

const split = _.curry((sep, str) => _.split(str, sep));
const join = _.curry((sep, arr) => _.join(arr, sep));
const map = _.curry((fn, arr) => _.map(arr, fn));

const f = _.flow(split(' '), map(_.toLower), join('-'));

f(str); // 'can-you-feel-my-world'

我们在使用 lodash 时,做能很多额外的转化动作,那我们试试 fp 模块吧。

import fp from 'lodash/fp';

const str = "CAN YOU FEEL MY WORLD";
const f = fp.flow(fp.split(' '), fp.map(fp.toLower), fp.join('-'));

f(str); // 'can-you-feel-my-world'

这种编程方式我们称之为 PointFree,它有 3 个特点:

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数

注意:FP 中的 map 方法和 lodash 中的 map 方法参数的个数是不同的,FP 中的 map 方法回调函数只接受一个参数

函数式编程(FP)

📒 一文颠覆大众对闭包的认知

📒 React v18 正式版发布

📒 答好这5个问题,就入门Docker了

📒 手写 Webpack

手写webpack核心原理,再也不怕面试官问我webpack原理

100行代码实现一个组件引用次数统计插件

📒 Golang 指针几点注意

  • Golang 中赋值操作、函数参数、函数返回值都是 copy
  • 基本类型、slice、map 直接传递就行,对于 struct、array 需要特别注意,建议一律传递指针类型

📒 Dum:Rust 编写的 npm 脚本运行器

延续了使用不是 JavaScript 来构建 JavaScript 工具的趋势。这个奇怪的名字 “Dum”,旨在取代 npm runnpx 来减少任务启动时间的毫秒数。

https://github.com/egoist/dum

📒 Node 之道:关于设计、架构与最佳实践

https://alexkondov.com/tao-of-node/

📒 Hooks 的 ”危害性“

作者声称“每周都能找到十几个与 hooks 相关的问题”,并利用这段经历给出了一些例子和解决方法,以避免“API 的不足之处”。

https://labs.factorialhr.com/posts/hooks-considered-harmful

📒 Dockerfile 配置

# 两段式构建
# 第一段构建源码镜像
ARG PROJECT_DIR=/project
ARG BB_ENV=prod
FROM harbor.hiktest.com/public/vue:2.5-node10 as src
ARG PROJECT_DIR
ARG BB_ENV


COPY . ${PROJECT_DIR}/
WORKDIR ${PROJECT_DIR}/

RUN npm install && npm run build:${BB_ENV}


# 第二段从源码镜像中拷贝出编译的dist,做成目标镜像
FROM harbor.hiktest.com/hikvision/nginx:1.12
ARG PROJECT_DIR

ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

COPY --from=src ${PROJECT_DIR}/dist /usr/share/nginx/html/
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./default.conf /etc/nginx/conf.d/default.conf

📒 万字长文助你上手软件领域驱动设计 DDD

📒 TypeScript 终极初学者指南

· 12 min read
加菲猫

📒 从 React 源码的类型定义中,我学到了什么?

📒 前端单测为什么不要测代码实现细节?

📒 React+Ts,这样学起来确实简单!!!

📒 高频 LeetCode 面试题分类

📒 如何在常数时间插入、删除数组中的元素

之前看到过一个用数组实现队列的方案,移除元素使用 shift,导致时间复杂度为 O(n),后来改为使用双向链表实现队列,插入删除时间复杂度都为 O(1)。如果使用链表的话,如何在常数时间内查找元素呢,可以使用 Map 存储链表节点指针,从而实现哈希链表的结构。

话说回来,如果使用数组的方案,如何实现常数时间插入、删除数组元素呢?可以做到!对数组尾部进行插入和删除操作不会涉及数据搬移,时间复杂度是 O(1)所以,如果我们想在 O(1) 的时间删除数组中的某一个元素 val,可以先把这个元素交换到数组的尾部,然后再 pop

📒 Typeit:轻量级代码录制回放

📒 React 18 超全升级指南

📒 「多图详解」NodeJs中EventLoop与浏览器下的差异性

📒 超爽!VSCode 实现自动原子化 CSS 样式

📒 云计算时代,你还不会 Docker ? 一万字总结(建议收藏)

📒 腾讯云后端15连问

三数之和,可以先对数组进行排序,然后使用左右指针

腾讯云后端15连问!

📒 十道腾讯算法真题解析!

📒 [科普文] Vue3 到底更新了什么?

📒 基于 TypeScript 理解程序设计的 SOLID 原则

📒 晋升,如何减少 50%+ 的答辩材料准备时间、调整心态(个人经验总结)

📒 【Anthony Fu】写个聪明的打字机!直播录像

📒 https://github.com/unjs

📒 如何理解 partition 函数

利用左右指针,其实有点类似反转数组,只不过反转数组对每个元素都交换一下,而 partition 只有在特定条件下进行交换:

  • 左指针向右移动,直到 nums[i] > pivot 停止移动,此时再移动右指针,接下来会有两种情况
    • 右指针遇到 nums[j] <= pivot 时停止移动,此时进行元素交换
    • 左指针右侧的元素都大于 pivot,没有元素需要交换,最终两个指针重合,停止操作
  • 不断重复上述步骤,直到交换结束,此时 nums[j] 为较小值,将 pivotnums[j] 交换
const partition = (nums: number[], lo: number, hi: number) => {
const pivot = nums[lo];
let i = lo + 1,
j = hi;
while (true) {
while (i < hi && nums[i] <= pivot) {
i++;
}
while (j > lo && nums[j] > pivot) {
j--;
}
if (i >= j) {
break;
}
swap(nums, i, j);
}
swap(nums, lo, j);
return j;
};

const swap = (nums: number[], i: number, j: number) => {
const temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
};

📒 在项目中用ts封装axios,一次封装整个团队受益😁

📒 阿里三面:灵魂拷问——有react fiber,为什么不需要vue fiber呢

为什么递归遍历 vdom 树不能中断

这种遍历有一个特点,必须一次性完成。假设遍历发生了中断,虽然可以保留当下进行中节点的索引,下次继续时,我们的确可以继续遍历该节点下面的所有子节点,但是没有办法找到其父节点——因为每个节点只有其子节点的指向。断点没有办法恢复,只能从头再来一遍。

在新的架构中,每个节点有三个指针:分别指向第一个子节点、下一个兄弟节点、父节点。这种数据结构就是fiber,它的遍历规则如下:

  1. 从根节点开始,依次遍历该节点的子节点、兄弟节点,如果两者都遍历了,则回到它的父节点;
  2. 当一个节点的所有子节点遍历完成,才认为该节点遍历完成;

提出问题:render 阶段 vdom 转 fiber 还是通过递归的方式,那么 fiber 链表可中断遍历是在哪一步

阿里三面:灵魂拷问——有react fiber,为什么不需要vue fiber呢

React 技术揭秘

📒 Vue组件库设计 | Vue3组件在线交互解释器

📒 React 类组件注意事项

1. 为了避免组件不必要的 rerender,建议继承 PureComponent

class MyCompoment extends React.PureComponent {
// ...
}

PureComponent 相当于函数组件使用 React.memo

2. 在构造方法中如要使用 this,则必须先调用 super()

class MyCompoment extends React.Component {
constructor(props) {
// 如果想在构造方法中使用 this,则必须先调用 super()
// super 实际上就是父类构造方法,类似盗用构造函数继承
// 下面是一个声明 state 的例子
super(props);
this.state = {
// ...
}
}
}

如果使用 ES2022 Class Properties 语法,则可以直接干掉构造方法,更加简洁:

class MyCompoment extends React.Component {
// 使用 ES2022 Class Properties 语法
state = {
// ...
}
}

3. 状态更新可能是异步的

React 可能会对多次 setState() 调用进行批处理,使组件只更新一次,因此 this.propsthis.state 可能会异步更新。所以不能依赖 this.state 计算下一个状态,这种情况下,可以使用函数式更新:

this.setState((prevState, prevProps) => ({
counter: prevState.counter + prevProps.increment
}));

4 类组件中需要注意事件处理函数的 this 绑定问题

class MyCompoment extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
console.log('===点击事件');
}

render() {
return (
<button onClick={this.handleClick}>点击</button>
)
}
}

如果使用 ES2022 Class Properties 语法,也可以让语法更简洁:

class MyCompoment extends React.Component {
handleClick = () => {
console.log('===点击事件');
}

render() {
return (
<button onClick={this.handleClick}>点击</button>
)
}
}

📒 箭头函数两个注意点

查看详情

1. 箭头函数中 this 指向能否改变

以下引用阮一峰 ES6 教程:

箭头函数没有自己的 this 对象,内部的 this 就是定义时上层作用域中的 this。也就是说,箭头函数内部的 this 指向是固定的,相比之下,普通函数的 this 指向是可变的

看了上面这段描述,很多同学可能都认为,箭头函数的 this 是无法改变的,但实际上箭头函数的 this 是跟着上层作用域走的,只要上层作用域的 this 改变,箭头函数中的 this 也会相应改变:

function foo() {
const bar = () => {
// 箭头函数的 this 来自 foo 函数
console.log(this.name);
}
bar();
}

const o1 = { name: "2333" };
const o2 = { name: "666" };

foo.bind(o1)(); // 2333
foo.bind(o2)(); // 666

如果将上述代码编译为 ES5 就能很容易理解上述过程:

function foo() {
var _this = this;
var bar = function() {
console.log(_this.name);
}
bar();
}

2. 为什么“类方法”可以使用箭头函数

在博客中看到有这样的代码:

class Person {
constructor(name) {
this.name = name;
}

getName = () => {
console.log(this.name);
}
}

咋一看好像没有问题,但是仔细一想发现不对,原型对象在所有实例之间是共享的,因此类方法的 this 必须要动态绑定,而箭头函数的 this 是静态的,这样不就有 bug 了,但是试验发现并没有问题:

const p1 = new Person("2333");
p1.getName(); // 2333
const p2 = new Person("666");
p2.getName(); // 666

这是因为,getName 实际并不是类方法,而是 ES2022 中类属性的写法,getName 实际上是一个对象的自有属性,可以使用下面的代码证明:

Object.prototype.hasOwnProperty.call(p1, "getName"); // true

这一点在 React 文档事件处理函数 this 绑定中也有说明:

class Foo extends Component {
// Note: this syntax is experimental and not standardized yet.
handleClick = () => {
console.log('Click happened');
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}

https://reactjs.org/docs/faq-functions.html#how-do-i-bind-a-function-to-a-component-instance

而类方法有且仅有下面这种写法:

class Person {
constructor(name) {
this.name = name;
}

getName() {
console.log(this.name);
}
}

使用箭头函数作为类属性时,绑定 this 的过程如下:

function Person(name) {
this.name = name;
this.getName = () => {
console.log(this.name);
}
}

const o = {};
Person.bind(o)("2333");
o.getName(); // 2333

new 调用过程中,Person 函数的 this 会绑定到实例对象上,箭头函数的 this 就是 Person 函数的 this,因此箭头函数的 this 会指向实例对象,并且由于箭头函数作为类的自有属性,会在每次 new 的时候重新生成,因此不同实例之间不会影响

📒 我的第一次webpack优化,首屏渲染从9s到1s

📒 几个一看就会的 TypeScript 小技巧

📒 Next.js 官方发布全新教程

· 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 通关指南

· 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 功力,你有几成火候?