📒 解决 Vite 打包 React 组件库无法排除 peerDependencies 的问题
使用 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
📒 使用 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
📒 写给前端的手动内存管理基础入门(一)返璞归真:从引用类型到裸指针
📒 你想知道vite核心原理吗,我来手写告诉你(80行源代码)
📒 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.
- mangle:即符号压缩,将变量名以更短的变量名进行替换
- 副作用:对程序状态造成影响,死代码优化一般不能删除副作用代码,即使副作用代码的结果在其他地方没用到
- 模块内部副作用:副作用影响范围仅限于当前模块,如果外部模块不依赖当前模块,那么该副作用代码可以跟随当前模块一起被删除,如果外部模块依赖了当前模块,则该副作用代码不能被删除
因此我们的后续讨论,所说的 tree shaking 均是指基于 LTO 的 DCE,而 DCE 指的是不包含 tree shaking 的其他 DCE 部分。
简单来说即是,tree shaking 负责移除未引用的 top-level 语句,而 DCE 删除无用的语句
📒 [科普] JS中Object的keys是无序的吗
1. Object.keys()
返回类型始终为 string[]
因为 JS 对象 key 的类型只有三种:number
、string
、Symbol
,需要注意 number
类型底层也是按 string
进行存储,而 Symbol
类型不可枚举。
2. ES2015 之后 Object.keys()
输出顺序是可以预测的
我们说普通对象的 Key 是无序的,不可靠的,指的是不能正确维护插入顺序,与之相对的是 Map 实例会维护键值对的插入顺序。
在 ES2015 之后,普通对象 Key 顺序是可预测的。先按照自然数升序进行排序,然后按照非数字的 String 的加入时间排序,然后按照 Symbol 的时间顺序进行排序。也就是说他们会先按照上述的分类进行拆分,先按照自然数、非自然数、Symbol 的顺序进行排序,然后根据上述三种类型下内部的顺序进行排序。
使用
Object.keys()
只会输出对象自身可枚举属性的 key,不含Symbol
类型的 key。如果要输出Symbol
类型 key,可以使用Reflect.ownKeys()
📒 代码覆盖率在性能优化上的一种可行应用
由于 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 优化
📒 【第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 等前端质量保障工程
📒 如何优雅实现轮询
- 初级:使用定时器(
setInterval
) - 中级:使用基于事件循环的递归(
setTimeout
递归调用) - 高级:使用轮询调度器
📒 npm 包的入口点
注意 exports
字段优先级最高,当提供 exports
字段后,main
、module
字段会被覆盖。
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 的故事
⭐️ TCP 重传、滑动窗口、流量控制、拥塞控好难?看完图解就不愁了
⭐️ TCP 就没什么缺陷吗?