Skip to main content

· 11 min read
加菲猫

📒 如何将数组转为对象

之前在业务中遇到一个场景,配置 Webpack alias 的时候,会出现很多模板代码:

module.exports = {
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
"@foo": path.resolve(__dirname, "src/foo"),
"@bar": path.resolve(__dirname, "src/bar"),
}
}
}

那么其实是可以通过数组的方式干掉模板代码:

function constructAlias(arr: string[]): Record<string, string> {
return Object.fromEntries(
arr.map(item => [
item,
path.resolve(cwd, item.replace(/^\@(.*?)$/, '$1'))
])
);
}

const config = ['@', '@foo', '@bar'];

const res = constructAlias(config);
console.log(res);

使用数组的 map 方法映射出一个 entry 数组,可以表示为形如 [key, value][] 的结构,然后使用 Object.fromEntries 将 entry 数组转为对象

这里需要注意,Object.fromEntries 是 ES2019 语法,支持 Chrome >= 73 和 Node.js >= 12.0.0。浏览器环境问题不大,一般都会配置 Babel polyfill 兼容,但是 Node.js 环境就会出一些问题,例如一些 CI 环境的 Node.js 版本很老,就会报错进而导致构建失败。因此通常开发的话,我们应该尽量用数组的 reduce 替代:

function constructAlias(arr: string[]): Record<string, string> {
return arr.reduce((accu, cur) => {
accu[cur] = path.resolve(cwd, cur.replace(/^\@(.*?)$/, '$1'));
return accu;
}, {});
}

📒 An introductory guide to Contiuous Integration and Delivery/Deployment (CI/CD) for Frontend Developers

https://blog.tegadev.xyz/an-introductory-guide-to-ci-cd-for-frontend-developers

📒 基于设计稿识别的可视化低代码系统实践

📒 被diss性能差,Dan连夜优化React新文档

📒 Node.js 调试一路走来经历了什么

📒 如何解决组件库打包条件引入

由于 import 语句必须放在顶层,不能放在条件判断中。如果同时保留两个 import 语句则会导致两个包都被打包进去。所以解决的方案就是在构建阶段动态修改 import 语句,但是需要注意两个问题:

  • 要注意修改时机,假如打包工具依赖分析已经完成,这时候再修改就太迟了
  • 另外还要注意不同打包工具的兼容性,如果开发 rollup 插件,可能导致 webpack、vite 等工具不兼容

因此选择开发 babel 插件,可以兼容各种打包工具。

📒 UMI3源码解析系列之运行时插件机制

📒 推荐一个前端技术选型神器

📒 Webpack 模块构建缓存

模块构建缓存,推荐使用 Webpack5 的 filesystem cache,技术更成熟,可以参考 CRA 的 Webpack 配置:

module.exports = {
cache: {
type: 'filesystem',
version: createEnvironmentHash(env.raw),
cacheDirectory: paths.appWebpackCache,
store: 'pack',
buildDependencies: {
defaultWebpack: ['webpack/lib/'],
config: [__filename],
tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
fs.existsSync(f)
),
},
},
}

关于持久化缓存,有两个地方需要注意:

  • 默认缓存的路径是 node_modules/.cache/webpack,也就是说,只要删除 node_modules,相当于缓存也被清空了
  • 本地和 CI 环境的缓存是相互独立的,本地的缓存无法在 CI 环境使用。在 CI 环境中需要使用 CI 的缓存机制

https://github.com/facebook/create-react-app/blob/main/packages/react-scripts/config/webpack.config.js

📒 最高性能的包管理器-pnpm

📒 【第2624期】Fastify 如何实现更快的 JSON 序列化

📒 如何设置 npm 私有源

在项目根目录建一个 .yarnrc 文件,配置如下:

# 淘宝源
registry=https://registry.npmmirror.com
# 私有源
@myscope:registry=https://mycustomregistry.example.org

这样的话,package.json 中带有 @myscope 前缀的依赖,例如 @myscope/design-system 都会从私有源下载。

📒 前端多线程编程探索

📒 精妙的配合!文字轮播与图片轮播?CSS 不在话下

📒 并发渲染优化:让文件树的渲染又快又稳

📒 UMI3源码解析系列之构建原理

⭐️ 看了9个开源的 Vue3 组件库,发现了这些前端的流行趋势

对包管理器的总结非常好,推荐看一下。

npm@v3 之前

  • 嵌套结构(nest),会出现大量重复装包的问题
  • 因为是树型结构,node_modules 嵌套层级过深,导致文件路径过长
  • 模块实例不能共享,例如在两个不同包引入的 React 不是同一个模块实例

npm@v3 / yarn

  • 分身依赖:npm@v3 使用扁平化(flat)的方式安装依赖,一定程度上解决了重复装包的问题,但是注意并没有完全解决,例如 A 和 B 依赖了不同版本的 C,会导致 C 被安装两次
  • 幽灵依赖:由于使用扁平化方式安装,package.json 里并没有写入的包竟然也可以在项目中使用了
  • 平铺减少安装没有减省时间,因为扁平化算法比较复杂,时间居然还增加了

npm@v5 / yarn

该版本引入了一个 lock 文件,以解决 node_modules 安装中的不确定因素。这使得无论你安装多少次,都能有一个一样结构的node_modules

然而,平铺式的算法的复杂性,幽灵依赖之类的问题还是没有解决。

看了9个开源的 Vue3 组件库,发现了这些前端的流行趋势

📒 Node.js Web 框架 Midway 入门实战

📒 肝了一个月的 DDD,一文带你掌握

📒 使用 rollup 构建第三方库包括哪些过程

  • 浏览器不兼容的语法转换
    • Vue 文件处理:rollup-plugin-vue
    • JSX、TS 语法编译:rollup-plugin-babel
    • 支持 CSS 加载、添加前缀、压缩、scss/less 预编译:rollup-plugin-postcss
  • 编译兼容
    • 仅限语法转换,不建议 polyfill:rollup-plugin-babel
  • 混淆压缩
    • 对应:rollup-plugin-terser
  • 打包为一份文件(注意 peerDependencies 外部化),多种打包格式,生成类型声明文件等
  • 工程质量保障,例如 ESLint、TS 类型检查、单元测试等
tip

前面两步可以避免业务项目的 babel-loader 处理 node_modules 下的模块,提升构建效率。

📒 如何实现主题切换

关键看场景,如果需要在运行环境动态切换,就需要打包两套样式,然后通过媒体查询之类的方式进行切换。如果不需要动态切换,可以在构建的时候进行变量注入。

自己开发的组件库是否有必要设置主题?样式都不打包,less 变量注入肯定没用的;如果搞个 theme-reset.less,肯定会污染到全局。最好还是在业务工程里面设置主题。

📒 如何做首屏性能优化

1. 路由懒加载

首先想到的就是解决资源冗余问题,我们可以按需投喂 JS 资源,只把渲染当前页面需要的资源投喂给浏览器,对应的方案是路由懒加载。

2. 分包优化

在按需投喂 JS 资源的基础上,对于一些不需频繁修改、体积又很大的依赖进行拆包处理,例如 reactreact-dom,单独分包设置强缓存。

3. 服务端渲染

如果按需投喂 JS 资源还是太慢,可以考虑服务端渲染(SSR),在服务端直接把当前页面的 HTML 丢给浏览器,可以理解为按需投喂 HTML 页面。

4. 静态生成 && 混合渲染

服务端渲染可以理解为在服务端调接口渲染出 HTML 丢给浏览器,但是这个过程还是存在性能开销。对于一些不需要动态数据的页面,例如文档、博客等,可以考虑静态生成(SSG),即在构建的时候就渲染出 HTML,可以极大提升首屏性能,当然更多时候是 SSG 和 SSR 混合渲染。

📒 深入理解 Linux CPU 上下文切换

📒 中后台 CSS Modules 最佳实践

📒 在 React 中实现条件渲染的 7 种方法

⭐️ 2022年值得使用的 Node.js 框架

📒 解决 Vite 无法全局启用 css module 的问题

在这里打个断点看看:

packages/vite/src/node/plugins/css.ts:688
const {
modules: modulesOptions,
preprocessorOptions,
devSourcemap
} = config.css || {}
const isModule = modulesOptions !== false && cssModuleRE.test(id)

📒 useRef 在列表渲染场景需要特别注意

在列表渲染的时候,不能对列表的每一项使用 ref,否则会出现 bug。这种情况下,应该将列表的每一项封装为组件,在组件内部使用 ref

type IProps = {
questionList: string[];
}

const App: React.FC<IProps> = ({ questionList }) => {
const ref = React.useRef();

return (
<>
{questionList.map((item, index) => (
<div
classNames="list-item"
key={index}
ref={ref}
>
{item}
</div>
))}
</>
)
}

📒 100 行代码实现 React 路由

https://github.com/ashok-khanna/react-snippets/blob/main/Router.js

精读《react-snippets - Router 源码》

📒 如何实现多行文本省略

这个功能不需要自己实现,自己实现还可能存在兼容性问题。只需要使用 antd 的 Typography 组件就可以了:

import * as React from "react";
import { Typography } from "antd";

const { Paragraph } = Typography;

const App: React.FC<{}> = () => {
return (
<Paragraph
ellipsis={{ rows: 2, expandable: true, symbol: 'more' }}
>
...
</Paragraph>
)
}

📒 HTTP 的缓存为什么这么设计

📒 vscode插件原理浅析与实战

· 10 min read
加菲猫

📒 PNPM 源码结构 - 前端包管理工具具有哪些功能

首先 pnpm 整个项目的主入口包文件为 packages/pnpm 这个包里面,这个包名称也直接叫做 pnpm ,其中 main.ts 文件是其入口文件,这个文件会处理掉用户传进来的一些参数,然后根据处理后的不同的参数对各命令做一个下发执行工作,下发后的命令参数再到各个包里面去,从而执行里面对应的逻辑。

处理参数用到的包为 @pnpm/parse-cli-args ,它会接收到用户传递进来的命令行参数,然后将其处理成一个 pnpm 内部的统一格式,例如用户输入如下命令:

$ pnpm add -D axios

这里传进来的一些参数都会被 parseCliArgs 这个方法处理:

例如 add 会被处理给 cmd 字段,一些裸的参数例如 axios 会被放进 cliParams 这个数组中,-D 这个参数在 cliOptions 里面去。处理后的这些变量以及参数用于主入口文件后续代码执行逻辑的判断。具体的判断逻辑可以在调试的时候遇到了,再去看对应的入口逻辑判断调试即可,这里不做具体的介绍。

main.ts 中会通过调用当前包下面的 cmd 目录下面的方法(pnpmCmds),来完成各命令的分发。

  • 依赖管理:如果 cmd 值为 addinstallupdate 等这些涉及和依赖安装相关的包,则会走 @pnpm/plugin-commands-installation 这个包里面对应的子命令逻辑(基本上 pnpm 所有的核心模块都围绕依赖安装这一块展开)
  • 打包发布:如果 cmd 值为 packpublish 这一类涉及到打包发布的包,则会走 @pnpm/plugin-commands-publishing 这个包的逻辑
  • 命令执行:如果 cmd 值为 runexecdlx 等这些和命令执行相关的方法,则会走 @pnpm/plugin-commands-script-runners 这个包的逻辑

📒 学习 swr 获取数据的思路

最近遇到很多列表渲染的场景,例如根据筛选项和分页参数获取列表数据。在代码中看到虽然用了 React Hooks,但是获取数据依旧是 jQuery 时代的 命令式 写法。

我们知道,前端框架都是数据驱动、声明式渲染的,即渲染视图不需要命令式地操作 DOM,而是声明式地修改数据就行。因此,获取数据也可以使用 声明式 写法,这样代码更容易维护。

import useSWR from 'swr'

function Profile() {
const { data, error } = useSWR('/api/user', fetcher)

if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}

https://swr.vercel.app/docs/getting-started

📒 【第2618期】手把手教你定制一套适合团队的微前端体系

📒 Vite 相关 issue 梳理

https://github.com/vitejs/vite/discussions/8232

📒 Chrome debugger 小技巧

在 Chrome 浏览器中打断点调试,此时切到控制台,可以访问断点位置上下文信息,也就是说可以访问、甚至修改变量。

⭐️ 前端必学的动画实现思路,高逼格的效果老板看了都会大声称赞

📒 【第2617期】React 组件库 CSS 样式方案分析

📒 【第2616期】解释JavaScript的内存管理

📒 UMI3源码解析系列之插件化架构核心

插件机制实现的方式:

  • umi:基于 tapable 的发布订阅模式,在路由、生成文件、构建打包、HTML 操作、命令等方面提供能力
  • babel:基于 visitor 的访问者模式,对于 AST 的操作等
  • rollup:基于 hook 的回调模式,定制构建和打包阶段的能力
  • webpack:基于 tapable 的发布订阅模式,loader 不能实现的都靠它
  • vue-cli:基于 hook 的回调模式,在生成项目、项目运行和 vue ui 阶段提供能力

UMI3源码解析系列之插件化架构核心

📒 写了一个基于 MacOS + iTerm2 自动执行化执行工具

📒 介绍全新的 JSX 转换

由于浏览器无法识别 JSX 语法,所以我们需要通过 Babel、TypeScript 等工具将 JSX 编译为浏览器能识别的 render 函数。在 React 17 之前,JSX 会转换为 React.createElement(...) 调用:

import React from 'react';

function App() {
return React.createElement('h1', null, 'Hello world');
}

正是因为 JSX 会转换为 React.createElement(...),所以每个组件顶部必须导入 React

在 React 17 版本,React 的 package 中引入了两个新入口,这些入口只会被 Babel 和 TypeScript 等编译器使用。新的 JSX 转换不会将 JSX 转换为 React.createElement,而是自动从 React 的 package 中引入新的入口函数并调用。下方是新 JSX 被转换编译后的结果:

// 由编译器引入(禁止自己引入!)
import { jsx as _jsx } from 'react/jsx-runtime';

function App() {
return _jsx('h1', { children: 'Hello world' });
}

注意,此时源代码无需引入 React 即可使用 JSX 了!(但是如果使用 React 提供的 Hook 或其他导出,这种情况下仍需引入 React)

新的 JSX 转换对应的配置是 runtime: "automatic"

// babel.config.js

module.exports = {
presets: [
[
"@babel/preset-react",
{
// 新的 JSX 转换 -> automatic
// 旧的 JSX 转换 -> classic
runtime: "automatic"
}
]
]
}
tip

可以直接在 Babel Playground 看编译结果:

https://babeljs.io/repl

官方文档表示,新的 JSX 转换会略微优化包体积,个人认为优化还是比较有限。虽说 React.createElement() 变成了更短的调用,但是又多出来一段运行时代码。

https://react.docschina.org/blog/2020/09/22/introducing-the-new-jsx-transform.html

📒 从 Turborepo 看 Monorepo 工具的任务编排能力

📒 大牛书单 | 学习 Golang 资料

📒 解决前端常见问题:竞态条件

📒 React Router v6 新手指南

https://www.youtube.com/watch?v=59IXY5IDrBA

📒 [调研报告] 新一代前端构建工具汇总

📒 Google 最新的性能优化方案,LCP 提升30%!

📒 React useEvent:砖家说的没问题

useEvent 会将一个函数「持久化」,同时可以保证函数内部的变量引用永远是最新的。如果你用过 ahooks 的 useMemoizedFn,实现的效果是几乎一致的。再强调下 useEvent 的两个特性:

  • 函数地址永远是不变的
  • 函数内引用的变量永远是最新的

通过 useEvent 代替 useCallback 后,不用写 deps 函数了,并且函数地址永远是固定的,内部的 state 变量也永远是最新的。

useEvent 的实现原理比较简单:

function useEvent(handler) {
const handlerRef = useRef(null);

// 用于确保函数内引用的变量永远是最新的
useLayoutEffect(() => {
handlerRef.current = handler;
});

// 用于确保返回的函数地址永远不变
return useCallback((...args) => {
const fn = handlerRef.current;
return fn(...args);
}, []);
}

React useEvent:砖家说的没问题

📒 为什么用 Vite 打包 React 组件库

  • 生产环境 rollup 打包 + 开发环境 devServer
  • 开发环境可以通过 @vitejs/plugin-react 插件支持 fast-refresh
  • 生产环境默认使用 esbuild 代码压缩,效率是 terser 的 20-40 倍
  • esbuild 在语法转换这块尚不完善,但是组件库打包不用考虑兼容性问题,兼容性问题交给业务项目解决
  • Vite 提供了很多 esbuild 尚不支持的特性(例如 CSS 模块化等)
  • 开发环境和生产环境几乎可以复用一套配置(Vite 抹平了 esbulid 和 rollup 配置差异)

· 18 min read
加菲猫

📒 解决 Vite 打包 React 组件库无法排除 peerDependencies 的问题

轮子系列:使用vite从零开发React组件库

如何使用Rollup打包React组件库

使用 dumi 实现组件库文档自动化

https://d.umijs.org/zh-CN/guide/advanced#%E7%BB%84%E4%BB%B6-api-%E8%87%AA%E5%8A%A8%E7%94%9F%E6%88%90

📒 NodeJs进阶开发、性能优化指南

📒 使用 URLSearchParams 注意事项

很多同学都会用 URLSearchParams 解析、拼接 query 参数,非常方便,而且还能自动进行参数编码、解码。但是在使用的时候,有几个注意事项:

const p = new URLSearchParams();

// 1. 当某个 value 为 undefined 时,会直接转为字符串拼接到 URL 上
p.set("dby", undefined);
p.set("dm", 2333);
p.toString(); // 'dby=undefined&dm=2333'

// 解决方案,使用逻辑或操作
p.set("dby", undefined || "");
p.toString(); // 'dby=&dm=2333'

// 2. get 一个不存在的值,返回的是 null,因此 TS 会推导为联合类型
const foo = p.get("foo"); // string | null

// 解决方案,使用逻辑或操作,实现类型守卫
const foo = p.get("foo") || ""; // string

📒 写给前端的手动内存管理基础入门(一)返璞归真:从引用类型到裸指针

📒 React如何原生实现防抖

📒 你想知道vite核心原理吗,我来手写告诉你(80行源代码)

📒 Rust 入门 - 资源与生命周期

📒 The Ultimate Guide To Software Engineering

📒 Tree shaking问题排查指南来啦

Tree shaking在不同工具里的意义不太统一,为了统一后续讨论,我们规范各个术语:

  • minify:编译优化手段,指在不影响代码语义的情况下,尽可能的减小程序的体积,常见的 minify 工具如 terser、uglify,swc 和 esbuid 也自带 minify 功能
  • Dead code elimination(DCE):即死代码优化,一种编译器优化手段,用于移除不影响程序结果的代码,实现DCE的手段有很多种,如 const folding (常量折叠)、Control flow analysis、也包括下面的 LTO
  • Link Time Optimization:指 link 期优化的手段,可以进行跨模块的分析优化,如可以分析模块之间的引用关系,删掉其他模块未使用的导出变量,也可以进行跨模块对符号进行 mangle http://johanengelen.github.io/ldc/2016/11/10/Link-Time-Optimization-LDC.html
  • Tree shaking:一种在 Javascript 社区流行的一个术语,是一种死代码优化手段,其依赖于 ES2015 的模块语法,由 rollup 引入。这里的 tree shaking 通常指的是基于 module 的跨模块死代码删除技术,即基于 LTO 的 DCE,其区别于一般的 DCE 在于,其只进行 top-level 和跨模块引用分析,并不会去尝试优化如函数里的实现的 DCE

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module syntax, i.e. import and export. The name and concept have been popularized by the ES2015 module bundler rollup.

https://webpack.js.org/guides/tree-shaking/

  • mangle:即符号压缩,将变量名以更短的变量名进行替换
  • 副作用:对程序状态造成影响,死代码优化一般不能删除副作用代码,即使副作用代码的结果在其他地方没用到
  • 模块内部副作用:副作用影响范围仅限于当前模块,如果外部模块不依赖当前模块,那么该副作用代码可以跟随当前模块一起被删除,如果外部模块依赖了当前模块,则该副作用代码不能被删除

image

因此我们的后续讨论,所说的 tree shaking 均是指基于 LTO 的 DCE,而 DCE 指的是不包含 tree shaking 的其他 DCE 部分。

简单来说即是,tree shaking 负责移除未引用的 top-level 语句,而 DCE 删除无用的语句

Tree shaking问题排查指南来啦

📒 [科普] JS中Object的keys是无序的吗

1. Object.keys() 返回类型始终为 string[]

因为 JS 对象 key 的类型只有三种:numberstringSymbol,需要注意 number 类型底层也是按 string 进行存储,而 Symbol 类型不可枚举。

2. ES2015 之后 Object.keys() 输出顺序是可以预测的

我们说普通对象的 Key 是无序的,不可靠的,指的是不能正确维护插入顺序,与之相对的是 Map 实例会维护键值对的插入顺序。

在 ES2015 之后,普通对象 Key 顺序是可预测的。先按照自然数升序进行排序,然后按照非数字的 String 的加入时间排序,然后按照 Symbol 的时间顺序进行排序。也就是说他们会先按照上述的分类进行拆分,先按照自然数、非自然数、Symbol 的顺序进行排序,然后根据上述三种类型下内部的顺序进行排序。

使用 Object.keys() 只会输出对象自身可枚举属性的 key,不含 Symbol 类型的 key。如果要输出 Symbol 类型 key,可以使用 Reflect.ownKeys()

[科普] JS中Object的keys是无序的吗

📒 如何设计更优雅的 React 组件

📒 代码覆盖率在性能优化上的一种可行应用

由于 JS 资源需要通过网络加载,代码的体积直接影响页面加载性能。很多时候我们“喂”给浏览器的代码,并不会全部执行,因此我们可以做分包优化,即 code-spliting,只“喂”给浏览器渲染当前页面所需的资源。

注意区分以下两个概念:

  • Dead code

    也叫无用代码,这个概念应是在编译时静态分析出的对执行无影响的代码。通常我们用 Tree Shaking 在编译时移除这些 dead code 以减小代码体积。

  • 冗余代码

    代码覆盖率中的概念,适用于运行时,而 Dead code 适用于编译时。Dead code 是任何情况下都不会执行的代码,所以可以在编译阶段将其剔除。冗余代码是某些特定的业务逻辑之下并不会执行到这些代码逻辑(比如:在首屏加载时,某个前端组件完全不会加载,那么对于“首屏”这个业务逻辑用例来讲,该前端代码就是冗余的)。

如何进行合理分包呢?这就需要统计代码覆盖率。代码覆盖率(Code coverage)是软件测试中的一种度量指标。即描述测试过程中(运行时)被执行的源代码占全部源代码的比例。

如何统计代码覆盖率:

1. Chrome 浏览器 Dev Tools

chrome 浏览器的 DevTools 给我们提供了度量页面代码(JS、CSS)覆盖率的工具 Coverage。使用方式:Dev tools —— More tools —— Coverage

由于一般都会对 JS、CSS 资源进行混淆压缩,因此建议导入 Source Map 以便查看源代码的覆盖率。

2. Istanbul(NYC)

Istanbul或者 NYC(New York City,基于 istanbul 实现) 是度量 JavaScript 程序的代码覆盖率工具,目前绝大多数的node代码测试框架使用该工具来获得测试报告,其有四个测量维度:

  • line coverage(行覆盖率):每一行是否都执行了 【一般我们关注这个信息】
  • function coverage(函数覆盖率):每个函数是否都调用了
  • branch coverage(分支覆盖率):是否每个 if 代码块都执行了
  • statement coverage(语句覆盖率):是否每个语句都执行了

缺点:目前使用 istanbul 度量网页前端JS代码覆盖率没有非侵入的方案,采用的是在编译构建时修改构建结果的方式埋入统计代码,再在运行时进行统计展示。

我们可以使用 babel-plugin-istanbul 插件在对源代码在 AST 级别进行包装重写,这种编译方式也叫 代码插桩 / 插桩构建(instrument)。

代码覆盖率在性能优化上的一种可行应用

📒 关于“环境变量”需要注意的问题

1. 为什么使用 cross-env 设置环境变量

有时候我们需要用 npm scripts 设置 Node.js 的环境变量,通常都会使用 cross-env 这个库。其实设置环境变量,在 MacOS 和 linux 系统直接通过 shell 命令就可以了,例如 PORT=8066,但是 Win 设置的方式不太一样,所以 cross-env 实际上是实现了跨平台设置环境变量。

2. .env 文件是如何生效的

可以使用 dotenv 这个库,可以将 .env 文件下的内容加载到 Node.js 的 process.env 对象中,注意 key 和 value 都是 string 类型。

3. 前端项目的环境变量是如何生效的

前端项目的环境变量,实际上不是真正的环境变量,因为浏览器环境下是访问不到 process 对象的,需要通过 DefinePlugin 在打包构建的时候,将变量替换为对应的值。

注意这里有个坑,DefinePlugin 默认直接进行文本替换,如果想要替换为字符串字面量,则需要在字符串中再加一个引号,或者用 JSON.stringify 包裹:

// webpack.config.js
new webpack.DefinePlugin({
__DEV__: "true", // 替换为布尔值
"process.env.NODE_ENV": JSON.stringify("development"), // 替换为字符串字面量
})

// 源码
if (__DEV__) {
// ...
}

if (process.env.NODE_ENV === "development") {
// ...
}

// 替换得到的结果
if (true) {
// ...
}

if ("development" === "development") {
// ...
}

使用 DefinePlugin 遇到的问题

在开发一个组件库,需要区分运行环境,根据环境打包相应的模块代码。根据 Webpack 代码优化(生产环境默认启用)的时候,terser 会做 DCE(无用代码移除)处理,进而优化打包体积:

// 在 Webpack 代码优化的时候
// terser 会识别出“业务2”的代码为无用代码,进而移除掉
// 只保留“业务1”的代码

if (true) {
// 业务 1
}

if (false) {
// 业务 2
}

原先的方式在一个模块中定义常量,然后其他模块引入常量进行判断。这里要注意一个问题,在 Webpack 代码优化的时候,terser 并不会做程序流分析,也就是说访问不到模块的上下文信息。这种情况下,terser 可能还是会将模块导出的常量当做变量处理,从而导致 DCE 失效。这种情况下,我们不能通过模块方式引入常量,而是要用 DefinePlugin 直接把变量替换为对应的字面量。

📒 治理项目模块依赖关系,试试这艘「依赖巡洋舰」

📒 【前端部署十二篇】使用 CI 中的缓存进行 Pipeline 优化

当我们使用 webpack 5 进行构建时,如果使用了 filesystem cache,因为在磁盘中含有缓存 (node_modules/.cache),二次构建往往比一次构建快速十几倍。

而在 CICD 中,这些都失去了意义,因为 CICD 每次 Job 都相当于新建了一个目录,「每次构建都相当于是首次构建」。

但是,CI 提供了一些缓存机制,可以将一些资源进行缓存。如果每次可以将缓存取出来,则大大加速了前端部署的速度。

【前端部署十二篇】使用 CI 中的缓存进行 Pipeline 优化

📒 UMI3源码解析系列之核心service类初始化

📒 【第2610期】JavaScript Containers

📒 【前端部署十一篇】通过 CICD 实践 Lint、Test、Performance 等前端质量保障工程

在 CI 操作保障代码质量的环节中,可确定以下时机:

# 1. 当功能分支代码 push 到远程仓库后,进行 CI
on:
push:
branches:
- 'feature/**'

# 2. 当功能分支代码 push 到远程仓库以及是 Pull Request 后,进行 CI
on:
pull_request:
types:
# 当新建了一个 PR 时
- opened
# 当提交 PR 的分支,未合并前并拥有新的 Commit 时
- synchronize
branches:
- 'feature/**'

CRA 内部使用 ESLint Plugin 进行代码检查,而非命令的方式。当 ESLint 存在问题时,CRA 如果判断当前是 CI 环境,则直接报错并退出进程,导致打包失败:

new ESLintPlugin({
// Plugin options
// ...
failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
})

Lint 和 Test 仅是 CI 中最常见的阶段。为了保障我们的前端代码质量,还可以添加以下阶段:

  • Audit: 使用 npm audit 或者 snyk 检查依赖的安全风险
  • Quality: 使用 SonarQube 检查代码质量
  • Container Image: 使用 trivy 扫描容器镜像安全风险
  • End to End: 使用 Playwright 进行 UI 自动化测试
  • Bundle Chunk Size Limit: 使用 size-limit 限制打包体积,打包体积过大则无法通过合并
  • Performance (Lighthouse CI): 使用 lighthouse CI 为每次 PR 通过 Lighthouse 打分,如打分过低则无法通过合并

有些细心并知识面广泛的同学可能注意到了,某些 CI 工作也可在 Git Hooks 完成,确实如此。

它们的最大的区别在于一个是客户端检查,一个是服务端检查。而客户端检查是天生不可信任的。

而针对 git hooks 而言,很容易通过 git commit --no-verify 而跳过。

【前端部署十一篇】通过 CICD 实践 Lint、Test、Performance 等前端质量保障工程

📒 【第2609期】Javascript之迪米特法则

📒 React 并发渲染的前世今生

📒 如何优雅实现轮询

  • 初级:使用定时器(setInterval
  • 中级:使用基于事件循环的递归(setTimeout 递归调用)
  • 高级:使用轮询调度器

📒 npm 包的入口点

注意 exports 字段优先级最高,当提供 exports 字段后,mainmodule 字段会被覆盖。

exports 可以更容易地控制子目录的访问路径,不在 exports 字段中的模块,即使直接访问路径,也无法引用!

工程化知识卡片 014: 发包篇之 package.json 中 main、export、module 的区别何在

http://nodejs.cn/api/packages.html#main-entry-point-export

📒 使用 Next.js 和 MDX 构建你自己的博客

https://www.freecodecamp.org/news/how-to-build-your-own-blog-with-next-js-and-mdx/

📒 React Concurrent 的故事

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

⭐️ TCP 重传、滑动窗口、流量控制、拥塞控好难?看完图解就不愁了

⭐️ TCP 就没什么缺陷吗?

📒 React Server Components:我们即将和 API 告别?

· 16 min read
加菲猫

📒 从零开始构建 JavaScript Bundler

Jest 作者的最新系列文章,并且配套视频,内容绝对硬核。

https://cpojer.net/posts/building-a-javascript-bundler

📒 JavaScript 框架的四个时代

这篇文章作者以自身多年的开发经历还原了 JavaScript 框架的发展历程,并划分出了四个时代。

  • 远古时代:无框架
  • 框架初期:Backbone.js、Angular 1、Knockout.js、SproutCore、Ember.js、Meteor.js
  • 以组件为中心的时代:React.js、Vue.js、Svelte、Polymer.js
  • 全栈框架:Next.js、Nuxt.js、Remix、SvelteKit、Gastby 和 Astro

https://www.pzuraq.com/blog/four-eras-of-javascript-frameworks

📒 pnpm v7.0.0

pnpm 发布了 v7.0.0,带来了大量的更新。如:不再支持 Node.js 12、pnpm run <script> 脚本名称后的所有命令行参数都会传递给 argv 等等。

https://github.com/pnpm/pnpm/releases/tag/v7.0.0

⭐️ 2022 年的前端行业,咋样啦

ESR(Edge Side Rendering,边缘渲染)是最近的一大热门趋势,可以直接在 CDN 级别实现按需渲染。Nuxt 3、Remix 以及 Sveltekit 等框架都在朝着这个方向发展,目测会在未来的一到两年会成为一大焦点。

2022 年的前端行业,咋样啦

📒 docker-node - Node.js 官方 Docker 镜像

📒 JS 新的日期 API:Temporal

这项特性提案时间为 2021 年 7 月,不到一年的时间已经进展到 stage-3 阶段,目前组委会已经在在做它的功能实现,有望在下个版本推出。

tip

该项提案的初衷来自这篇文章,因为 JavaScript 最初关于日期的实现是照搬的 Java 方案,但由于各种限制和问题,Java 早在 1997 年就实现 Calendar 做了功能改进,而 JavaScript 时至今日用的还是老旧方案,改进优化实在是迫在眉睫。

https://maggiepint.com/2017/04/09/fixing-javascript-date-getting-started/

官方文档(打开控制台就可以体验 Polyfill):

https://tc39.es/proposal-temporal/docs/

或者在 RunKit 上体验(浏览器端运行 node 模块):

https://npm.runkit.com/proposal-temporal

📒 【工程化】探索webpack5中的Module Federation

📒 我们如何使用 Webpack 将启动时间减少 80%

📒 React官方团队出手,补齐原生Hook短板

📒 你可能并没有理解的 babel 配置的原理

📒 前端抢饭碗系列之Docker进阶部署

📒 前端抢饭碗系列之初识Docker容器化部署

📒 从零开始发布自己的NPM包

⭐️ Umi 4 特性合集,比 Vite 还要快?

📒 HTTP分块传输 如何在 React18 中应用

📒 下集」React性能优化,你需要知道的一切

📒 htmlparser2 8.0:快速且高容错的 HTML 和 XML 解析器

https://github.com/fb55/htmlparser2

📒 Node v18 test 模块

注意 Node v18 test 模块是第一个 Prefix-Only Core Modules,也就是说加载该模块必须带上 node: 前缀:

import test from 'node:test';  // Uses the node: prefix. Loads from core.
import assert from 'assert'; // Does not use the node: prefix. Loads from core.
tip

假如没有带上 node: 前缀,则会尝试从用户空间加载 test 模块。但是对于 Node 其他内置模块来说,加不加 node: 前缀都是一样的。

https://fusebit.io/blog/node-18-prefix-only-modules/

📒 Node v16.15.0 (LTS) 发布

现在 Node v16 可以使用实验性支持的 Fetch API 了

https://nodejs.org/en/blog/release/v16.15.0/

📒 升级到 React 18 所对应的 TypeScript 类型定义的改动

https://blog.logrocket.com/upgrading-react-18-typescript/

📒 如何理解 React Hooks 的闭包陷阱

函数组件更新,实际上就是函数重新执行,生成一个新的执行上下文,所有变量、函数重新创建,hooks 重新执行。

一般来说,当函数执行完毕,内部的变量就会销毁、被垃圾回收机制回收。当然也有例外情况,在下面的代码中,函数 baz 依赖了 bar 内部的变量 a,并且 baz 作为返回值传递给了 foo,因此 a 并不会被垃圾回收机制回收,而是会作为闭包缓存下来。只要 foo 的引用不解除,a 就会一直缓存:

function bar() {
const a = 1;
return function baz() {
console.log(a);
}
}

const foo = bar();

再来看这个场景:useEffect 的回调函数依赖了 state 变量,而我们知道这个回调函数在下次 rerender 之前都是缓存在 fiber 节点上的,这样一来就创建了闭包,即使函数组件已经执行完毕,但是 state 变量仍会被缓存下来。

当组件更新的时候,会生成一个新的执行上下文,state 变量也会重新生成,但是 useEffect 回调函数仍然引用了旧的闭包。但是为什么 useEffect 依赖项变化、回调函数执行的时候,总是可以获取到新的值呢?这是因为每次函数组件重新渲染,useEffect 都会重新执行,回调函数也会重新生成(但不一定都会执行),在 updateEffectImpl 内部用重新生成的函数替换了 fiber 节点缓存的函数,这样一来,回调函数执行的时候,始终都能获取到最新的值了。

你可能会觉得这样没什么问题,但是如果在 useEffect 中使用定时器,大概率都会遇到闭包陷阱。

另一个会遇到闭包陷阱的是 useCallback。很多同学觉得 useCallback 依赖项似乎没什么用,习惯性传递空数组,这就会导致函数一直被缓存,假如内部依赖了 state 变量,则始终会缓存旧的闭包。正确做法应该是把 state 变量添加到依赖项数组中,在 state 改变的时候重新生成函数,这样就可以获取到最新的值。

tip

函数组件 rerender 过程中,缓存状态的 fiber 节点(相当于组件实例)并不会销毁,但函数组件是重新执行了,会生成一个新的上下文环境,如果 useEffect 回调依赖了 state 变量,则会一直缓存旧的闭包。所以要避免闭包陷阱,只需要 保证每次渲染的时候,函数都重新生成 就行。

📒 TypeScript 小技巧:常量断言

在讲常量断言之前,先提一下,TS 会区别对待可修改和不可修改的值的类型推断:

// 推断成单值类型 'dbydm'
const immutable = 'dbydm';

// 推断成通用的 string 类型
let mutable = 'dn';

// 由于对象的属性都具有可修改性,TS 都会对它们「从宽」类型推断
// 例如下面的 prop 的类型被推断为 string
const obj = { prop: 'foo' }

再来看下面的代码,例如我们实现了一个用 ref 维护状态的 hook:

import * as React from "react";

const useRenderlessState = <T>(initialState: T) => {
const stateRef = React.useRef(initialState);

const setState = (nextState: T) => stateRef.current = nextState;

return [stateRef.current, setState];
}

此时我们会发现上面 hook 的返回值的类型被推导成了如下的数组类型:

(T | ((nextState: T) => T))[]

这就导致我们在使用的时候无法对它进行准确的解构:

const [value, setValue] = useRenderlessState(0);

一般来说我们可以 显示声明返回类型 或者 对返回值做类型断言,告诉 TS 返回值类型是元组而不是数组:

// 显示声明返回类型
const useRenderlessState = <T>(initialState: T): [T, (nextValue: T) => T] => {/*...*/}

// 对返回值对类型断言
const useRenderlessState = <T>(initialState: T) => {
// ...
return [state, setState] as [typeof value, typeof setValue];
}

上面的两种写法都各有冗余成分,算不上优雅。

其实从语义层面来分析,TS 之所以没能将返回值推断为元组类型是因为它认为该返回值仍有可能被 push 值,被修改。所以我们真正需要做的是告诉 TS,这个返回值是一个 final,其本身和属性都是不可篡改的,而这正是常量断言所做的事。

常量断言可以把一个值标记为一个不可篡改的常量,从而让 TS 以最严格的策略来进行类型推断:

const useRenderlessState = <T>(initialState: T) => {
// ...
return [state, setState] as const
}

这下 useRenderlessState 的返回类型就被推断成了如下的 readonly 值:

readonly [T, (nextState: T) => T]
tip

as const 与 ES6 const 常量声明的区别:

  • const 常量声明是 ES6 的语法,对 TS 而言,它只能反映该常量本身是不可被重新赋值的,它的子属性仍然可以被修改,故 TS 只会对它们做松散的类型推断
  • as const 是 TS 的语法,它告诉 TS 它所断言的值以及该值的所有层级的子属性都是不可篡改的,故对每一级子属性都会做最严格的类型推断(所有的字面量都会被推断为单值类型)

常量断言可以让我们不需要 enum 关键字就能定义枚举对象:

const EnvEnum = {
DEV: "development",
PROD: "production",
TEST: "test",
} as const;

TypeScript 夜点心:常量断言

📒 了解 Symbol.toStringTag 的用法吗

Symbol.toStringTag 是一个内置 symbol,它通常作为对象的属性键使用,对应的值是字符串类型,用来表示该对象的自定义类型标签。通常只有内置的 Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里。

const foo = {};
const bar = {
[Symbol.toStringTag]: "测试内容"
}

foo.toString(); // '[object Object]'
bar.toString(); // '[object 测试内容]'

Symbol.toStringTag - MDN 文档

📒 函数组合中的 composeflowpipe

compose 实现如下,注意调用顺序是反过来的:

const compose = (...fns) => x0 => fns.reduceRight(
(x, f) => f(x),
x0
);

// 接受参数后,返回一个待执行函数
// 需要再接受一个初始值才开始执行
const processComment = compose(
linkify,
imagify,
emphasize,
headalize
);

flow 实现如下,注意这里调用顺序是从左到右:

const flow = (...fns) => x0 => fns.reduce(
(x, f) => f(x),
x0
);

// 注意这里仍然是返回一个待执行函数
const processComment = flow(
headalize,
emphasize,
imagify,
linkify,
codify
);

pipe 实现如下,调用顺序也是从左到右:

// 注意 pipe 直接执行所有的函数,返回一个值
// 而 flow 返回一个待执行函数,需要再接受一个初始值才开始执行
const pipe = (x0, ...fns) => fns.reduce(
(x, f) => f(x),
x0
);

const map = f => arr => arr.map(f);
const filter = p => arr => arr.filter(p);
const take = n => arr => arr.slice(0, n);
const join = s => arr => arr.join(s);

const comments = pipe(commentStrs,
filter(noNazi),
take(10),
map(emphasize),
map(itemize),
join('\n'),
);

什么是 JavaScript 的函数组合

📒 基于依赖倒置原则实现插件机制

依赖倒置原则(DIP)

核心思想:依赖一个抽象的服务接口,而不是去依赖一个具体的服务执行者,从依赖具体实现转向到依赖抽象接口,倒置过来

例如在 Webpack 中包含一套插件机制:

module.exports = {
// ...
plugins: [
new WebpackBar(),
new webpack.HotModuleReplacementPlugin(),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: '[id].css'
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, '../public/index.html'),
title: "React App",
filename: "index.html",
})
]
}

Webpack 插件需要实现一个统一的接口,即:

interface IPlugin {
apply(compiler: ICompiler): void;
}

class MyPlugin implements IPlugin {
constructor() {
// 构造器可以在初始化的时候接受配置参数
}

@Override
apply(compiler) {
// ...
}
}

这样 Webpack 只需要遍历 plugins 数组,顺次调用每个插件上的 apply 方法,传入 compiler 对象即可:

plugins.forEach(plugin => plugin.apply(compiler));

顺便提一下,有同学会问,为啥插件要写成 class 的形式,直接用一个对象可以吗,例如:

const MyPlugin = {
apply(compiler) {
// ...
}
}

直接用一个对象也是可以的,但是用 class 显然更灵活,可以在初始化的时候接受配置参数

📒 浏览器 JavaScript 和 Node.js 的区别

  • API 区别:浏览器 JavaScript 是面向浏览器编程,调用浏览器的 API,例如 documentwindow。而 Node.js 是面向操作系统编程,没有浏览器 API,相反可以调用 Node 提供的标准库,与操作系统进行交互
  • 运行环境区别:浏览器 JavaScript 的特殊性(JS 代码需要经过网络请求,在客户端下载并执行),因此无法选择运行环境,需要考虑语法、API 兼容性问题,需要使用 Babel 处理。而 Node.js 通常在本地开发环境、CI 环境、服务端运行,可以控制运行环境,无需考虑兼容性问题
  • 模块规范区别:浏览器原本没有模块机制,但可以自行实现模块命名空间机制(例如 browserifywebpack),从 Chrome 61 开始支持 <script type="module">,即浏览器原生支持 import 命令加载模块(需要注意这种方式也是要经过网络请求)。而 Node.js 自带了一套 CommonJS 模块机制,在 Node 14 之后支持 ES Module 规范(注意 CommonJS 仍然是默认启用的模块规范)

· 18 min read
加菲猫

📒 实现一个 Code Pen:(三)10 行代码实现代码格式化

📒 为什么用链表实现队列

很多时候用数组也能实现队列,我们知道数组尾部操作(例如 pushpop)时间复杂度都是 O(1),但如果在数组头部增删元素(例如 shiftunshift),需要移动其他元素的下标,因此时间复杂度为 O(n)

而链表增删元素实际上都是修改指针指向,不需要移动下标,因此时间复杂度都是 O(1)。链表只有查找元素需要遍历,时间复杂度为 O(n),但是队列并不需要查找,而且链表的 size 属性可以在增删操作的时候进行维护,所以用链表实现队列非常合适。

思考题:如何在常数时间内删除数组中的元素。我们知道 splice 删除元素也需要移动其他元素下标,时间复杂度为 O(n),但是在数组尾部操作时间复杂度都是 O(1),因此可以先把要删除的元素交换到数组尾部,然后直接删除尾部元素即可。

📒 写给前端的 K8S 上手指南

📒 Web页面全链路性能优化指南

📒 系统困境与软件复杂度,为什么我们的系统会如此复杂

📒 【第2602期】设置 NPM Registry 的 4 种姿势

📒 前端算法系统练习: 栈和队列篇

📒 怎么解决MySQL死锁问题的

📒 【第2598期】ServiceWorker 缓存与 HTTP 缓存

📒 Monorepo 的过去、现在、和未来

📒 【第2597期】如何用JavaScript实现一门编程语言 - AST

📒 TS 类型体操:索引类型的映射再映射

📒 ESBuild & SWC浅谈: 新一代构建工具

📒 InnoDB原理篇:如何用好索引

我们都知道 InnoDB 索引结构是 B+ 树组织的,但是根据 数据存储形式不同 可以分为两类,分别是 聚簇索引二级索引

其实聚簇索引的本质就是主键索引。因为每张表只能拥有一个主键字段,所以每张表只有一个聚簇索引。另外聚簇索引还有一个特点,表的数据和主键是一起存储的,它的叶子节点存放的是整张表的行数据(树的最后一层),叶子节点又称为数据页。

很简单记住一句话:找到了索引就找到了行数据,那么这个索引就是聚簇索引

知道了聚簇索引,再来看看二级索引是什么,简单概括,除主键索引以外的索引,都是二级索引,像我们平时建立的联合索引、前缀索引、唯一索引等。

二级索引的叶子节点存储的是索引值 + 主键 id。所以二级索引与聚簇索引的区别在于 叶子节点是否存放整行记录

也就意味着,仅仅靠二级索引无法拿到完整行数据,只能拿到 id 信息

假设,我们有一个主键列为 id 的表,表中有字段 kk 上有索引。

我们执行一条主键查询语句 select * from T where id = 100,只需要搜索 id 聚簇索引树就能查询整行数据。

接着再执行一条 select * from T where k = 1,此时要搜索 k 的二级索引树,具体过程如下:

  • k 索引树上找 k = 1 的记录,取得 id = 100
  • 再到聚簇索引树查 id = 100 对应的行数据
  • 回到 k 索引树取下一个值 k = 2,不满足条件,循环结束

上述过程中,回到聚簇索引树搜索的过程,我们称为 回表

也就是说,基于二级索引的查询需要多扫描一棵聚簇索引树,因此在开发中尽量使用主键查询

可是有时候我们确实需要使用二级索引查询,有没有办法避免回表呢?

办法是有的,但需要结合业务场景来使用,比如本次查询只返回 id 值,查询语句可以这样写 select id from T where k = 1,过程如下

  • k 索引树上找 k = 1 的记录,取得 id = 100
  • 返回 id
  • 回到 k 索引树取下一个值 k = 2,不满足条件,循环结束

在这个查询中,索引 k 已经覆盖了我们的查询需求,不需要回表,这个操作称为覆盖索引

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段

假设现在有一个高频的业务场景,根据 k 查询,返回 name,我们可以把 k 索引变更成 kname 的联合索引。

InnoDB原理篇:如何用好索引

InnoDB原理篇:聊聊数据页变成索引这件事

📒 一分钟看懂TCP粘包拆包

TCP 是一个面向「流」的协议,所谓流就是没有界限的一长串二进制数据。在实际的传输过程中,TCP 会根据网络情况将数据包进行拆分或者拼装,如果业务没有定义一个明确的界限规则,在应用层的业务上就会出现粘包拆包的现象。

针对 TCP 粘包拆包的现象,常见的解决思路如下:

  1. 发送端给每个数据包 添加包首部
  2. 发送端将每个数据包 封装为固定长度
  3. 可以在数据包之间 设置边界

为了解决粘包拆包,Netty 框架也提供了很多开箱即用的编解码器,极大简化网络编程解决此类问题的难度。

一分钟看懂TCP粘包拆包

📒 什么是有区分度的好的面试问题,来看看字节跳动这道实现异步 sum 的问题

📒 【前端部署第三篇】通过 docker 学习 nginx 配置,及基于 nginx 部署最简前端项目

📒 使用 CRA 搭建 React + TS 项目

都 2022 年了,手动搭建 React 开发环境很难吗

会写 TypeScript 但你真的会 TS 编译配置吗

用 Redux 做状态管理,真的很简单🦆

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

📒 Usage With TypeScript - Redux Toolkit

https://redux-toolkit.js.org/usage/usage-with-typescript

📒 全局状态和状态管理的区别

全局状态可以很简单,例如只要一个 JS 对象 {} 就可以实现,但是如果尝试修改全局状态的值,无法触发组件更新。

状态管理,除了具有全局状态的功能,还提供了一套发布订阅机制,即状态改变的时候通知对应组件更新。

Redux 本身其实就是全局状态,为了实现状态改变通知组件更新,还需要一个 UI-binding,即 React-redux。

📒 浅谈V8垃圾回收机制

📒 打造 Go 语言最快的排序算法

📒 实现一个 Codepen:(二)在 Next.js 中使用 Monaco Editor

📒 【架构师(第十八篇)】脚手架之项目模板的安装

📒 【前端部署第二篇】基于 docker/compose 部署一个最简单的前端项目

📒 7 段小代码,玩转Java程序常见的崩溃场景

如何排查 CPU 飙升问题,获取问题代码通常可以使用下面的方法:

  1. 使用 top 命令,查找到使用 CPU 最多的某个进程,记录它的 pid。使用 Shift + P 快捷键可以按 CPU 的使用率进行排序
  2. 再次使用 top 命令,加 -H 参数,查看某个进程中使用 CPU 最多的某个线程,记录线程的 ID
  3. 使用 printf 函数,将十进制的 tid 转化成十六进制
  4. 使用 jstack 命令,查看 Java 进程的线程栈
  5. 使用 less 命令查看生成的文件,并查找刚才转化的十六进制 tid,找到发生问题的线程上下文

7 段小代码,玩转Java程序常见的崩溃场景

📒 看完这篇你一定能掌握Linux

📒 ObjectMapper,别再像个二货一样一直new了

📒 [科普] Service Worker 入门指南

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

📒 前端历史项目的 Vite 迁移实践总结

📒 手写 css-modules 来深入理解它的原理

📒 在 Webpack 5 中开启懒编译(Lazy Compilation)

Webpack 5 的实验特性,可以针对多入口(Initial Chunk)和动态加载(Async Chunk)进行懒编译。开启懒编译之后,可以实现按需编译,提升启动速度,若再配合 Webpack 5 持久化缓存,则可以直接秒杀 Vite。

module.exports = {
// …
experiments: {
lazyCompilation: {
imports: true,
entries: true,
},
},
};

由于实验特性具有相对宽松的语义版本,可能会有重大的变更,所以你需要锁定 Webpack 的小版本号,例如 "webpack": "~5.4.3",或者锁定版本号

在 Webpack 5 中开启懒编译(Lazy Compilation)

📒 浅谈文档的实时协同编辑

📒 腾讯一面:CORS为什么能保障安全?为什么只对复杂请求做预检

首先为什么要有同源策略?浏览器需要记住用户的登录状态(即登录凭证),这样用户下次访问页面就无需重复登录。这样的话,就需要有一些安全策略,否则很容易出现 CSRF 攻击等问题。如果是其他的 http client 则没有同源策略。

CORS策略的心智模型是:所有跨域请求都是不安全的,浏览器要带上来源给服务器检验

同源策略会限制哪些行为:

  • 跨域情况下获取 DOM 元素(例如跨域的 iframe)、localStorage、Cookie 等
  • 跨域情况下发送 ajax 请求,浏览器会拒绝解析响应报文

注意,浏览器默认的表单提交不受同源策略限制

CORS 即跨域资源共享,这里注意 CORS 的目的不是拦截请求,反倒是为了让其能正常请求。CORS 的诞生背景就是同源策略,这是一个相当严苛的规定,它禁止了跨域的AJAX请求。但实际的开发中又有这样的需求,于是开一个口子——只要配置了CORS的对应规则,跨域请求就能正常进行。

如何配置 CORS?前端在发送请求的时候,浏览器会在请求头添加 Origin 字段,这样后端就能知道请求的来源,然后后端在响应头添加 Access-Control-Allow-Origin,这个值就是前端发送的来源地址(或者直接加 * 表示允许所有地址)。

跨域请求的流程,CORS把请求分成简单请求和复杂请求,划分的依据是“是否会产生副作用”。同时满足下面这两个条件的是 简单请求,否则就是 非简单请求

  • 请求方法是 HEAD/GET/POST
  • 请求体的 Conent-Type 只能是 form-urlencodedform-datatext/plain

对于简单请求,流程如下:

  1. 浏览器发起请求,并且自动加上请求的来源 origin 给服务器检查;
  2. 服务器返回数据,并返回检查结果,配置CORS响应头;
  3. 浏览器检查CORS响应头,如果包含了当前的源则放行,反之拦截;

这里需要注意,浏览器是拦截响应,而不是拦截请求,跨域请求是发出去的,并且服务端做了响应,只是浏览器拦截了下来

对于复杂请求,流程如下:

  1. 浏览器发起预检请求,带上请求的来源 origin,不包含请求体;
  2. 服务器返回检查结果,配置CORS头;
  3. 浏览器发起真正请求;
  4. 浏览器返回数据;

浏览器会检查第2步中拿到的CORS头,如果没有包含当前的源,后续的第3、4步都不会进行,也就是不会发起真正请求

为什么只对复杂请求做预检?上文提到,划分简单请求和复杂请求的依据是“是否产生副作用”。这里的副作用指对 数据库做出修改:使用GET请求获取新闻列表,数据库中的记录不会做出改变,而使用PUT请求去修改一条记录,数据库中的记录就发生了改变。

假设网站被CSRF攻击了——黑客网站向银行的服务器发起跨域请求,并且这个银行的安全意识很弱,只要有登录凭证cookie就可以成功响应,考虑下面两种情况:

  • 黑客网站发起一个GET请求,目的是查看受害用户本月的账单。银行的服务器会返回正确的数据,不过影响并不大,而且由于浏览器的拦截,最后黑客也没有拿到这份数据;
  • 黑客网站发起一个PUT请求,目的是把受害用户的账户余额清零。浏览器会首先做一次预检,发现收到的响应并没有带上CORS响应头,于是真正的PUT请求不会发出;

幸好有预检机制,否则PUT请求一旦发出,黑客的攻击就成功了。

这种情况下,后端也需要遵循 RESTful 规范,否则要么面临攻击风险,要么会多发一次预检请求

腾讯一面:CORS为什么能保障安全?为什么只对复杂请求做预检

腾讯三面:Cookie的SameSite了解吧,那SameParty呢

📒 Axios 三个优点

  • Promisify
  • 责任链(拦截器机制)
  • 适配器(同时支持浏览器和 node 环境)

📒 深入理解 Promise 之手把手教你写一版

📒 2022 年 JavaScript 开发工具的生态

📒 自动化生成骨架屏的技术方案设计与落地

· 14 min read
加菲猫

📒 Ubuntu 22.04 LTS 安装

https://phoenixnap.com/kb/ubuntu-22-04-lts

https://releases.ubuntu.com/jammy/

📒 计算机程序的构造和解释 — JavaScript 版

这本由麻省理工学院出版的著作终于有了 JavaScript 语言版本,可以帮助你建立对计算机程序的心智模型。

https://github.com/source-academy/sicp

📒 为什么要使用 Redux Toolkit

Redux 官方发布的这篇博客讲解了 Redux Toolkit 的 Why 和 How,并强烈推荐使用。

一句话总结:Redux Toolkit 是使用 Redux 的最佳实践。

https://redux.js.org/introduction/why-rtk-is-redux-today

⭐️ Node.js 18 新特性解读

📒 那些你应该说再见的 npm 祖传老库

📒 如何实现数组转对象

传入一个 paramKeys 数组,获取 query 参数的值,然后以对象形式返回,使用 reduce 方法:

function getSearchParams(paramKeys: string[]): Record<string, string> {
const searchParams = new URLSearchParams(window.location.search);
return paramKeys.reduce<Record<string, string>>((accu, cur) => {
accu[cur] = searchParams.get(cur) || '';
return accu;
}, {});
}

// 使用
const searchParams = getSearchParams(['name', 'age']);

以上流程还可以封装成自定义 hook:

import * as React from 'react';

function useSearchParams(paramKeys: string[]): Record<string, string> {
const query = window.location.search;
return React.useMemo(() => {
const searchParams = new URLSearchParams(query);
return paramKeys.reduce<Record<string, string>>((accu, cur) => {
accu[cur] = searchParams.get(cur) || '';
return accu;
}, {});
}, [paramKeys, query]);
}

看了 antfu 大佬的代码,还可以使用 Object.fromEntries() 方法:

function useSearchParams(paramKeys: string[]): Record<string, string> {
const searchParams = new URLSearchParams(window.location.search);
return Object.fromEntries(
paramKeys.map((key) => [key, searchParams.get(key)])
);
}

注意:Object.fromEntries() 是 ES2019 中的语法,存在兼容性问题(Chrome >= 73),不过只要正确配置 polyfill 就可以放心使用

📒 使用 defineConfig 约束配置对象

在项目中经常需要用到配置对象,例如 Webpack、rollup 的配置,我们可以使用 TS 来约束配置对象的 API schema,告知用户应该传哪些字段以及对应的类型,这样有两个好处:

  • 对用户更加友好,不需要看文档就能直接上手
  • 在开发阶段就能提前检查出配置项错误,不用到运行阶段再去校验了

一般来说我们需要导出一个接口类型:

export type IConfig = {
name: string;
age: number;
sex?: boolean;
};

用户在使用的时候需要导入类型,然后自己添加注解,这样编写配置对象就能得到类型提示了:

import type { IConfig } from "xxx";

const config: IConfig[] = [
{
name: "dbydm",
age: 23
}
]

但是这样对用户来说还是太麻烦了,我们可以定义一个 defineConfig 函数,这个函数做的事情很简单,就是把接收到的参数原封不动地返回,但在这个过程中,就可以实现参数类型的校验:

type IConfig = {
name: string;
age: number;
sex?: boolean;
};

export function defineConfig(config: IConfig[]) {
return config;
}

用户只需导入 defineConfig 编写配置就可以实现参数类型的校验:

import { defineConfig } from "xxx";

export default defineConfig([
{
name: "dbydm",
age: 23
}
])

📒 Elasticsearch 基础入门详文

📒 如何把前端项目写成一座屎山

📒 浅谈JS内存机制

📒 深入理解 scheduler 原理

📒 前端框架如何实现预渲染

首先预渲染根据渲染时机分为以下两种:

  • 静态站点生成(SSG),构建的时候获取数据进行渲染,数据不一定是最新的
  • 服务端渲染(SSR),用户访问的时候服务端获取数据进行渲染,数据实时获取

两种渲染方案都可以实现 首屏性能优化SEO 优化,不同的是 SSR 需要在服务端运行 JS,并且每次用户请求的时候都会进行渲染;SSG 已经将每个页面渲染成静态 html,因此可以将资源托管到 CDN 上

获取数据又可以分为以下几种方式:

  • 本地文件系统读取
  • 调接口获取
  • 查询数据库获取

实际上,React 本身已经提供了服务端渲染和静态生成相关的 API。在前端项目中,我们一般会使用下面的 API 挂载 React 组件:

ReactDOM.render(element, container[, callback])

为了实现 SSR 渲染,我们可以使用下面的 API 将 React 组件直接渲染为 HTML 字符串:

ReactDOMServer.renderToString(element)

使用 renderToString 方法渲染出的 HTML 字符串会带有特定标记,我们可以使用下面的 API 在客户端进行激活,对标记的节点挂载相应的事件监听器:

ReactDOM.hydrate(element, container[, callback])

在 SSG 渲染中,我们不需要在客户端进行激活,因此不用在 HTML 字符串中添加标记,只需渲染出纯的 HTML 字符串:

ReactDOMServer.renderToStaticMarkup(element)

📒 2万字系统总结,带你实现 Linux 命令自由

📒 还在手撸 Nginx 配置?试试这款可视化配置工具吧,真心强大

📒 esno,基于 Esbuild 的神器

📒 「React进阶」换个姿势看 hooks ! 灵感来源组合和HOC 模式下逻辑视图分离新创意

useMemo 类似 Vue 中的计算属性,当依赖项发生变化,会重新计算。但实际上 useMemo 比计算属性更强大,除了缓存值之外,还能缓存组件:

function Index({ value }){
const [number, setNumber] = React.useState(0);
const element = React.useMemo(() => <Test />, [value]);

return (
<div>
{element}
<button onClick={() => setNumber(number + 1)}>点击 {number}</button>
</div>
)
}

有时候在父组件定义的事件处理函数,需要作为 prop 传入子组件。如果父组件重新渲染,会导致函数重新生成,相当于 prop 发生变化,即使子组件内部使用 React.memo() 包裹也会导致重新渲染。常规做法是使用 React.useCallback() 包裹事件处理函数,但实际上用 React.useRef() 包裹也是可以的,都是把事件处理函数缓存到 Fiber 节点上。

function MyApp() {
const onClickRef = React.useRef(() => {
console.log("666");
});

// const onClick = React.useCallback(() => {}, []);

return (
<div>
<h1>Welcome to my app</h1>
<MyButton onClick={onClickRef.current} />
</div>
);
}

「React进阶」换个姿势看 hooks ! 灵感来源组合和HOC 模式下逻辑视图分离新创意

📒 React 18 升级踩坑汇总

1. React.StrictMode 导致所有组件重复挂载两次

使用 CRA 5.0.1 搭建 React 项目,默认的项目模板中,根组件使用了 React.StrictMode 包裹,结果出现了所有组件都重复挂载的情况,导致组件中接口调了两次。看了下文档,确实是 React 18 中引入的 Breaking Change,启用严格模式,会导致所有组件重复挂载两次(即使用了 React.memo 也会重复挂载):

Stricter Strict Mode: In the future, React will provide a feature that lets components preserve state between unmounts. To prepare for it, React 18 introduces a new development-only check to Strict Mode. React will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount. If this breaks your app, consider removing Strict Mode until you can fix the components to be resilient to remounting with existing state

tip

使用 CRA 创建的 React 18 项目,建议移除 React.StrictMode

2. React 18 中使用了 antd 的 message 组件控制台打印警告信息

React 18 使用了新的 ReactDOM.createRoot() API 挂载根节点,Concurrent Mode 需要通过此 API 开启,但是 antd 中的 message 等组件内部仍使用 ReactDOM.render() 挂载根节点,此时在控制台会打印警告,注意这并不是报错,仅仅只是 fallback 到 legacy mode 而已。

tip

升级前最好仔细看一遍官方的说明,特别是 Breaking Change:

https://github.com/facebook/react/releases/tag/v18.0.0

📒 为什么需要 peerDependencies

例如开发一个 React 组件库的时候,有三个诉求:

  • 该组件库开发的时候需要安装 React;
  • 用户引入该组件库的时候不能重复安装 React;
  • 组件库的 React 版本与目标环境不一致的时候需要被包管理器发现并打印警告;

如果安装到 dependencies 下,显然会导致重复安装;如果安装到 devDependencies 下虽然不会导致重复安装,但包管理器不会检查版本,当版本不一致的时候不会打印警告。所以 peerDependencies 是最优选择。

在老版本 React 项目中引入某些依赖库(例如 antdreact-transition-group),一般不能直接安装最新的版本(大概率会报错),此时应该根据依赖库的 package.json 中指定的 peerDependencies 字段选择合适的依赖库版本

⭐️ 什么是 JavaScript 的函数组合

本篇文章以一个简略的 Markdown 的例子为主线,讲述了什么是函数组合,以及如何使用函数组合的思想编写代码,是一篇非常不错的编程思想类文章。

https://jrsinclair.com/articles/2022/javascript-function-composition-whats-the-big-deal/

📒 一些关于react的keep-alive功能相关知识在这里(下)

📒 一些关于react的keep-alive功能相关知识在这里(上)

📒 理清 HTTP 下的 TCP 流程,让你的 HTTP 水平更上一层

📒 React 18 系列

React 18 全览

React 18 对 Hooks 的影响

React 的心智模型

你不知道的 React v18 的任务调度机制

📒 React 几个小技巧

1. React 内置工具类型

// 使用 React.ComponentType 同时表示类组件和函数组件
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;

// 使用 React.Key 来表示列表渲染 key 的类型
type Key = string | number;

参考:

精读《@types react 值得注意的 TS 技巧》

从 @types/react 的类型定义中,我学到了什么

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/v17/index.d.ts

2. 自定义组件如何绑定 className

className 作为 prop 传入,内部使用 classnames 这个库进行拼接:

import cx from "classnames";
import s from "./style.module.less";

type IProps = {
className: string;
}

const App: React.FC<IProps> = ({ className }) => {
return (
<div className={cx(s.wrapper, className)}>
...
</div>
)
}

更进一步,把以上流程封装成高阶组件(HOC):

function withClassName<T>(Component: React.ComponentType<T>): React.FC<T & { className: string }> {
return ({ className, ...restProps }) => {
return (
<div className={className}>
<Component {...restProps} />
</div>
)
}
}

3. Input 如何变为受控组件

Antd 中的 Input 默认是非受控组件,可以绑定 value,然后监听 onChange 修改 value 实现受控(v-model 的原理):

const App: React.FC<{}> = () => {
const [num, setNum] = React.useState(1);

return (
<InputNumber value={num} onChange={setNum} />
)
}

📒 我帮一朋友重构了点代码,他直呼牛批,但基操勿六

📒 React + TypeScript:如何处理常见事件

📒 单例模式 4 种经典实现方法

📒 如何实现 useClickAway

如何监听元素外的点击,类似 Vue 的 ClickOutSide 指令

官方文档:https://ahooks.js.org/hooks/use-click-away

源码:https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useClickAway/index.ts

📒 快速理解 TypeScript 的逆变、协变

📒 都 2022 年了,手动搭建 React 开发环境很难吗

📒 这篇手写 Promise 你一定要康康

📒 超全面的前端新一代构建工具对比: esbuild、Snowpack、Vite、wmr

· 5 min read
加菲猫

📒 JS 相关技巧

// 1. 数组转对象
const dict: Record<number, boolean> = Object.fromEntries(
array.map(i => [i, true])
)

// 2. 使用 Array.from 初始化数组
const digits = Array.from({ length: 10 }, (_, i) => i);

// 3. 字符串转数组,然后用数组方法遍历
// 常规方法是用 split()
String(num).split("").reduce(...)
// 由于字符串实现了 iterator 接口,因此可以使用扩展运算符展开到数组中
[...String(num)].reduce(...)

📒 用Rust锈化Vue Compiler

📒 TS 类型体操性能分析

$ tsc index.ts –-diagnostics

📒 前端动画实现方案

  • CSS 方案:transitionanimation
  • JS 方案:setTimeoutrequestAnimationFrame

一个实验性 API Element.animate(),可以在渲染进程的时候就执行,性能更好。

https://developer.mozilla.org/zh-CN/docs/Web/API/Element/animate

📒 我是如何带领团队从零到一建立前端规范的

📒 血泪教训之请不要再轻视Git —— 我在工作中是如何使用 Git 的

📒 https://nextjs.org/learn/basics/create-nextjs-app

📒 https://nextjs.org/docs

📒 手把手教你用神器nextjs一键导出你的github博客文章生成静态html

📒 Golang 三数之和

package algorithm

import (
"sort"
)

func ThreeSum(nums []int) [][]int {
ans := make([][]int, 0)
// 数组元素个数小于 3,直接返回
if len(nums) < 3 {
return ans
}
// 排序
sort.Ints(nums)
// 遍历到倒数第二个,因为是三个数总和
for i := 0; i < len(nums) - 2; i++ {
// 规定 nums[i] < nums[left] < nums[right]
// 如果 nums[i] > 0 则不存在另外两个值使得相加等于 0
// 大于 0 可以直接跳出循环了
if nums[i] > 0 {
break
}
// 过滤 nums[i] 重复
if i > 0 && nums[i] == nums[i-1] {
continue
}
// 先确定一个值 nums[i]
// 再去找另外两个值 nums[left] 和 nums[right]
// 需要满足 nums[i] < nums[left] < nums[right]
target := -nums[i]
left, right := i + 1, len(nums) - 1

// 使用双指针法确定剩下两个值
for left < right {
sum := nums[left] + nums[right]
if sum < target {
left++
} else if sum > target {
right--
} else if sum == target {
ans = append(ans, []int{nums[i], nums[left], nums[right]})
// 找到目标值,左右指针分别移动一位
left++
right--
// 过滤 nums[left] 重复
for left < right && nums[left] == nums[left-1] {
left++
}
// 过滤 nums[right] 重复
for left < right && nums[right] == nums[right+1] {
right--
}
}
}
}
return ans
}

📒 Golang 手写数组方法

package main

import (
"fmt"
)

func ForEach(nums []int, fn func(int, int)) {
for index, item := range nums {
fn(item, index)
}
}

func Map(nums []int, fn func(int, int) int) []int {
res := make([]int, 0)
for index, item := range nums {
res = append(res, fn(item, index))
}
return res;
}

func Filter(nums []int, fn func(int, int) bool) []int {
res := make([]int, 0)
for index, item := range nums {
if fn(item, index) {
res = append(res, item)
}
}
return res
}

func Reduce(nums []int, fn func(int, int, int) int, initValue int) int {
res := initValue
for index, item := range nums {
res = fn(res, item, index)
}
return res
}

func main() {
s := []int{1, 2, 3, 4}
ForEach(s, func(item, index int) {
fmt.Println("===forEach", item, index)
})
mapped := Map(s, func(item, index int) int {
return item * 2
})
fmt.Println(mapped)
filtered := Filter(s, func(item, index int) bool {
return item % 2 == 0
})
fmt.Println(filtered)
reduced := Reduce(s, func(accu, cur, index int) int {
return accu + cur
}, 0)
fmt.Println(reduced)
}

📒 选择第三方 NPM 包时的 5 条最佳实践

📒 Vue3.2 vDOM diff流程分析之一:diff算法

📒 从零到一,我们来一起造个 JS 的编译器

⭐️ 2022 年的 React 生态

📒 linux后台开发具备能力集锦

📒 Linux下C++后台服务器开发

📒 Go 语言与并发编程

📒 打造轻量级 WebIDE,看这一篇文章就够啦

📒 developer-roadmap

developer-roadmap 是一个开发人员路线图,包含了前端路线图、后端路线图、DevOps 路线图、React 路线图、Angular 路线图、Android 路线图、Python 路线图、Go 路线图、Java 路线图、DBA 路线图。

https://github.com/kamranahmedse/developer-roadmap

📒 pkg: 把 Node.js 项目打包为可执行文件

· 7 min read
加菲猫

📒 编译 ts 代码用 tsc 还是 babel

📒 第三方库是否应该提交 lockfile

在业务项目中,每次依赖安装的版本号都从 lock 文件中进行获取,锁定依赖和依赖的依赖,避免了不可测的依赖风险。但是仍然存在间接依赖不可控的问题,例如 React 依赖 object-assign,在 React 中 lockfile 锁定的版本为 object-assign@4.1.0,但是前端项目实际安装的版本为 object-assign@4.99.99

  • 第三方库的 devDependencies 必须锁定,这样 Contributor 可根据 lockfile 很容易将项目跑起来
  • 第三方库的 dependencies 虽然有可能存在不可控问题,但是可通过锁死依赖或者勤加更新的方式来解决

如果没有 package-lock.json,那将如何

📒 工程化知识卡片 023:node_modules 版本重复的困境

npmv3 之后 node_modules 为平铺结构,但是仍然存在依赖重复安装的问题。

工程化知识卡片 023:node_modules 版本重复的困境

📒 Vue 开发小技巧

分享 15 个 Vue3 全家桶开发的避坑经验

vue3中可以帮助你早点下班的9个开发技巧

📒 【混淆系列】三问:npx、npm、cnpm、pnpm区别你搞清楚了吗?

📒 Webpack组件库打包超详细指南

📒 Node.js 技术架构

Node 是怎么实现的?简言之:用 V8 运行 JS、用 bindings 实现 JS 与 C/C++ 沟通、用 C/C++ 库高效处理 IO、用 Node.js 标准库简化 JS 代码、用 Event Loop 管理事件处理顺序、用 libuv 实现异步 I/O 操作。

Node.js 技术架构

📒 Web 框架的替代方案

📒 写好 JavaScript 异步代码的几个推荐做法

📒 Node.js 进阶 - 多文件 Stream 合并,串行和并发两种模式实现

重要的事情再说一遍,pipe 方法默认情况下会自动关闭可写流,但是如果可读流期间发生错误,则写入的目标流将不会关闭,所以如果使用 pipe 需要监听错误事件,手动关闭可写流,防止文件句柄泄露。

Node.js 进阶 - 多文件 Stream 合并,串行和并发两种模式实现

📒 服务端渲染SSR及实现原理

📒 手摸手服务端渲染-react

📒 如何在项目中用好 TypeScript

📒 Golang 和 JS 创建对象方式对比

Golang 与 JS 创建对象非常类似,Golang 在创建对象的时候需要定义 schema 进行类型约束:

type Person struct {
Name string
Age int
Sex bool
}

person := Person{
Name: "dbydm",
Age: 12,
Sex: true,
}

Golang 创建对象数组:

list := []*Person{
&Person{
Name: "dbydm",
Age: 12,
Sex: true,
},
&Person{
Name: "dm",
Age: 2333,
Sex: false,
},
}

📒 Node.js 常见的系统信号

  • SIGHUP:不通过 ctrl+c 停止进程,而是直接关闭命令行终端,会触发该信号
  • SIGINT:按下 ctrl+c 停止进程时触发;pm2 重启或者停止子进程时,也会向子进程发送该信号
  • SIGTERM:一般用于通知进程优雅退出,如 k8s 删除 pod 时,就会向 pod 发送 SIGTERM 信号,pod 可以在超时时间内(默认 30s)做一些退出清理动作
  • SIGBREAK:在 window 系统上,按下 ctrl+break 会触发该信号
  • SIGKILL:强制退出进程,进程无法做任何清理动作,执行命令 kill -9 pid,进程会收到该信号。k8s 删除 pod 时,如果超过 30s,pod 还没退出,k8s 会向 pod 发送 SIGKILL 信号,立即退出 pod 进程;pm2 在重启或者停止进程时,如果超过 1.6s,进程还没退出,也会发送 SIGKILL 信号

📒 2022 年,Babel vs TypeScript,谁更适合代码编译

📒 React 常用状态管理库

  • Redux
  • Mobx
  • Recoil
  • Hookstate
  • Rematch
  • Jotai
  • Zustand

📒 从源码理清 useEffect 第二个参数是怎么处理的

📒 腾讯一面:CORS为什么能保障安全?为什么只对复杂请求做预检?

📒 如何在 Node 环境使用 ESM 模块规范

首先明确一点,Node 环境并非不支持 ESM 规范,只是没有启用而已,默认使用 CJS 规范,可通过如下方式启用:

  • 单文件使用 ESM 规范,可以将该文件后缀改为 .mjs
  • 整个工程使用 ESM 规范,可以在 package.json 中配置 "type": "module"

假如不想通过上述方式启用,还有一些方法:

  • 通过 Webpack 等打包工具支持 ESM 模块(Webpack 默认使用 web 环境构建,需要配置 target: "node" 避免打包 Node 内置模块);
  • 还可以使用 ts-nodejiti 等 runtime 支持 ESM 模块(内部使用 tsc 或者 babel 进行编译);

📒 如何生成随机 ID

一种是直接使用 Math.random()

const randomId = () => Math.random().toString().slice(2, 8);

另一种是使用查表的方式:

// 生成 [0..9] 的数组
const nums = Array.from({ length: 10 }, (_, index) => index);

// 从 nums 数组中随机选取元素
const sample = (arr) => arr[Math.floor(Math.random() * arr.length)];
const randomId = () => Array.from({ length: 6 }, () => sample(nums)).join("");

📒 跨域如何携带 Cookie

  • 如果通过网关层代理(例如 nginx)则不用担心,对于浏览器来说实际上并没有跨域,可正常携带 Cookie
  • 如果通过 CORS 跨域,浏览器默认不会携带 Cookie,此时有两种方案:
    • 在请求头中添加 Authorization 字段发送 Cookie(在 axios 中配置请求拦截添加)
    • 后端响应头添加 Access-Control-Allow-Credentials,前端发送请求时配置 xhr.withCredentials = true

· 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 官方发布全新教程