Skip to content

首页

快速入门脚手架(Node.js CLI)开发

开发一个 Node.js CLI 脚手架,是创建高效开发工具的重要一步。本文将从基础概念入手,逐步深入开发过程,并以最小依赖实现第一个 CLI 工具。 一、基础概念与预备知识 1. 什么是 Shell 和 Bash? Shell:操作系统的命令解释器,允许用户与系统进行交互。常见的 Shell 包括 Bash、zsh、fish 等。 Bash (Bourne Again Shell):是 Linux 和 macOS 默认的 Shell,提供了丰富的命令和脚本支持。 Shell 脚本允许自动化任务,许多 CLI 工具的核心逻辑往往需要与 Shell 交互。 2. 什么是 CLI 与 GUI? CLI(Command Line Interface,命令行接口):用户通过命令输入与程序交互,如 git 或 npm。 GUI(Graphical User Interface,图形用户界面):用户通过窗口和按钮与系统交互,如 VSCode。 CLI 工具的优势在于: 自动化程度高、支持脚本化操作。 高效处理批量任务,开发时可远程执行。 二、脚手架开发的预备知识 1. 了解 Node.js 的能力 Node.js 提供内置模块(如 fs、child_process 等),适合开发 CLI。 Node.js 支持同步与异步 API,CLI 通常选择同步执行关键任务,确保顺序执行。 2. package.json 的 bin 字段 在 package.json 中配置 bin 字段,让你的脚手架可以全局执行。 "bin": { "my-cli": "./index.js" } 三、开发第一个脚手架 1. 初始化项目 mkdir my-cli && cd my-cli npm init -y touch index.js 2. 在 index.js 中实现基础 CLI #!/usr/bin/env node const [,, command, ...args] = process.argv; switch (command) { case 'greet': console.log(`Hello, ${args[0] || 'World'}!`); break; default: console.log('Usage: my-cli greet <name>'); } 解释 const [,, command, ...args] = process.argv; 这行代码利用了 解构赋值,用于从 process.argv 中提取命令和参数。我们逐步解析这一部分代码,并解释为什么 command 前面有两个逗号。 process.argv 是一个数组,包含运行 Node.js 脚本时传入的所有命令行参数。其中: 第 0 个元素:Node.js 可执行文件的路径(如 /usr/local/bin/node)。 第 1 个元素:正在执行的脚本路径(如 /Users/alice/my-cli/index.js)。 第 2 个元素及之后:用户传入的命令和参数。 示例: 如果用户运行如下命令: node index.js greet Alice 那么 process.argv 的值将是: [ '/usr/local/bin/node', // 第 0 个元素 '/Users/alice/my-cli/index.js', // 第 1 个元素 'greet', // 第 2 个元素(命令) 'Alice' // 第 3 个元素(参数) ] 在 Node.js 中运行脚本时,第 0 个和第 1 个元素分别是 Node.js 可执行文件和脚本的路径。 对于命令行工具,我们通常不需要这两个路径,因此直接跳过。 3. 本地测试 ./index.js greet Alice 4. 配置全局运行 在 package.json 中添加 bin 字段: "bin": { "my-cli": "./index.js" } 使用 npm link 进行本地测试: npm link my-cli greet Bob 四、核心原生实现 1. 读取用户输入(process.argv) Node.js 的 process.argv 提供命令行参数的访问能力: const args = process.argv.slice(2); console.log(`Arguments: ${args.join(' ')}`); 2. 文件系统操作(fs 模块) 创建和写入文件: const fs = require('fs'); fs.writeFileSync('hello.txt', 'Hello, Node.js CLI!'); console.log('File created: hello.txt'); 3. 执行系统命令(child_process 模块) 通过 Shell 执行命令: const { execSync } = require('child_process'); const result = execSync('ls').toString(); console.log(result); 4. 处理用户交互(readline 模块) 简单实现用户输入: const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('What is your name? ', (answer) => { console.log(`Hello, ${answer}!`); rl.close(); }); 五、第三方库与框架介绍 1. commander:简化命令和参数处理。 npm install commander const { Command } = require('commander'); const program = new Command(); program .version('1.0.0') .command('greet <name>') .description('Say hello to someone') .action((name) => console.log(`Hello, ${name}!`)); program.parse(process.argv); 2. inquirer:增强用户交互体验。 npm install inquirer const inquirer = require('inquirer'); inquirer.prompt([ { type: 'input', name: 'username', message: 'What is your name?' } ]).then(answers => console.log(`Hello, ${answers.username}!`)); 3. chalk:为终端输出添加颜色。 npm install chalk const chalk = require('chalk'); console.log(chalk.green('Success!')); 六、总结 开发 CLI 脚手架并不复杂,只需掌握 Node.js 基础模块即可实现很多功能。在此基础上,你可以根据项目需求引入第三方库来增强用户体验。以下是脚手架开发的流程回顾: 基础概念:理解 Shell、Bash、CLI 与 GUI 的区别。 项目初始化:配置 package.json 的 bin 字段。 核心实现:使用 process.argv、fs、child_process 等原生模块。 扩展功能:根据需要引入第三方库(如 commander 和 inquirer)。 最后,分享一个自己开发的 Node.js CLI 工具: douban-scanner —— 豆瓣扫描器,一个用来抓取豆瓣 “书影音” 数据的 CLI 工具

2024-05-04

从 Dark 看编程语言空安全特性

前段时间想做一个小工具 APP,考虑想用 Flutter 实现,于是去看了看 Dart 语言,发现一个比较有意思的特性 —— “空安全”,这里和大家聊一聊。 什么是空安全 空安全是指编程语言的类型系统能够区分可为空的类型和不能为空的类型,这种区别可以防止空引用错误(Null Reference Errors)。 Dart 的空安全 Dart 在 2.12 版本引入了空安全。 可空和非空类型 在 Dart 中,所有类型默认都是非空的。例如,如果你声明一个 String 类型的变量,Dart 会假设它永远不会为空: 且不能将它赋予 null 值: 如果你想声明一个可以为空的 String,你需要在类型后面加上 ?: 处理可空值 与 JS 类似,Dart 也提供了几种方式来处理可能为空的值。 例如,可以使用 ?? 操作符来提供一个默认值: 可以使用 ?. 操作符,在对象为空时,跳过方法调用或属性访问,避免抛出空引用错误: 针对上面这种情况,如果编译器解析出你去访问一个可能为空的对象的属性或方法时,没有使用 ?. 操作符,这会在编译时就会报错,避免了运行时才暴露问题: 空值断言操作符 ! 的使用 当我们排除变量或参数的可空的可能后,可以通过 ! 来告诉编译器这个可空的变量或者参数不可空,这对我们进行方法传参或将可空的参数传递给一个不可空的入参时特别有用。 引用 Dart 文档中的一个例子⬇️:(From:Dart| 空值断言操作符) 由于 error 属性是可空的,在返回结果成功时,它不会有值。我们通过仔细观察类可以看出,当消息为空时,我们永远不会访问 error。但为了知晓这个行为,必须要理解 code 的值与 error 的可空性之间的联系。类型检查器看不出这种联系。 换句话说,作为代码的人类维护者,我们知道在使用 error 时,它的值不会是 null,并且我们需要对其进行断言。通常你可以通过使用 as 转换来断言类型,这里你也可以这样做: 编译时的报错消失了!⬆️ 如果在运行时,将 error 转换为非空的 String 类型出现了无法转换的错误,会抛出一个异常。若转换成功,一个非空的字符串就会回到我们的手上,让我们可以进行方法调用。 “排除可空性的转换”的场景频繁出现,这促使了 Dart 带来了新的短小精悍的语法。一个作为后缀的感叹号标记 (!) 会让左侧的表达式转换成其对应的非空类型。所以上面的函数等效于: 当原有的类型非常繁琐的时候,这个只有一个字符的 “! 操作符” 就会非常顺手。如果仅仅是为了将一个类型转换为非空,就需要写出类似于 as Map<TransactionProviderFactory, List<Set<ResponseFilter>>> 这样的代码,会让这个过程变得非常烦人。 TypeScript 开启严格空值检查 目前很多主流编程语言都对空安全有自己的支持,比如:Kotlink、Swift、Rust,包括我们熟悉的 TypeScript,其实也可以手动开启严格空值检查。 TypeScript 里,JS 中的基本数据类型 undefined 和 null 两者各自有自己的类型分别叫做 undefined 和 null。 默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给 number 类型的变量。 例如下面的代码,在 TS 中是完全可以执行的: strictNullChecks TypeScript 2.0 增加了对 不可为空类型 的支持。有一种新的 严格空值检查 模式,他提供了 strictNullChecks 来限制对空值的检查,可以通过在命令行上添加 --strictNullChecks 参数来启功严格空值检查,也可以在项目的 tsconfig.json **文件中启用 strictNullChecks 编译器选项。 在TS中,为了各版本的兼容,strictNullChecks 的默认值是 false,我们需要手动设置为 true: { "compilerOptions": { "strictNullChecks": true // ... } } 此时,我们刚刚的代码,就无法通过编译器检查了: 变量如何可以为空 开启严格空值检查后,如果我们想要一个变量可以接受空值,我们该怎么办呢? 使用联合类型 比如下面的代码,username 可以接受 null 类型的值,但是无法接受 undefined 的值: 在 Object 中使用 ? 可选属性 首先,联合类型可以在 Object 中使用: 这里我们设置 age 的类型为 number 和 undefined,下面的两种用法都是正确的: 如果我们想要下面的效果,不需要手动给 age 赋值: 此时我们就需要用到 ? 来使属性成为可选,这样我们就可以完全省略 age 属性的定义。 在这种情况下:undefined 类型会自动添加到联合类型中。因此,以下所有赋值都是正确的: 引入空安全的好处 可以将原本运行时的空值引用错误,变为编译时的分析错误 增强程序的健壮性,有效避免由Null而导致的崩溃 由于空安全特性的存在,编译层面可以做很多优化

2023-11-09

客户端与服务器即时通信的几种实现方式

1. 轮询(Polling)—— 简单直接的老方法 轮询是一种最基础的通信方式,客户端定期向服务器发送 HTTP 请求,询问是否有新数据。 实现示例 // 每隔5秒轮询一次服务器是否有新消息 setInterval(async () => { const response = await fetch('/api/messages'); const data = await response.json(); console.log('新消息:', data); }, 5000); 优缺点分析 优点:简单易用,所有浏览器都支持。 缺点:响应不及时,浪费带宽,服务器压力大。 适用场景:对实时性要求不高的项目,如低频更新的系统状态检查。 2. 长轮询(Long Polling)—— 聊天系统的好搭档 长轮询是轮询的升级版:客户端发起请求后,如果服务器暂时没有数据,不会立即返回响应,而是等到有新数据时才返回。 实现示例 // 长轮询请求示例 async function longPoll() { try { const response = await fetch('/api/messages'); const data = await response.json(); console.log('收到消息:', data); } catch (error) { console.error('连接错误:', error); } finally { // 收到消息或连接断开后,立即发起下一次请求 longPoll(); } } longPoll(); 优缺点分析 优点:比传统轮询更高效,减少了不必要的请求。 缺点:服务器需要长时间维护连接,对资源占用较高。 适用场景:适用于需要准实时响应的聊天系统或通知系统。 3. WebSocket —— 全双工通信的利器 WebSocket 是一种基于 TCP 的全双工通信协议,允许客户端和服务器之间建立持久连接,双方可以互相主动发送数据。 实现示例 客户端代码: const socket = new WebSocket('wss://example.com/socket'); // 监听服务器消息 socket.onmessage = (event) => { console.log('收到消息:', event.data); }; // 发送消息给服务器 socket.onopen = () => { socket.send('Hello, Server!'); }; // 处理错误 socket.onerror = (error) => { console.error('WebSocket 错误:', error); }; 优缺点分析 优点:双向通信,实时性极高,适合高并发应用。 缺点:需要服务器支持 WebSocket,客户端实现也较复杂。 适用场景:在线游戏、实时协作工具、股票交易系统等对实时性要求极高的应用。 4. 服务器推送事件(Server-Sent Events, SSE)—— 简单的单向推送 SSE 允许服务器主动推送数据给客户端,客户端只需建立一次连接。 这是一个基于 HTTP 协议的单向通信方式。 实现示例 服务器端(Node.js 示例): const http = require('http'); http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); setInterval(() => { res.write(`data: ${new Date().toLocaleTimeString()}\n\n`); }, 1000); }).listen(3000); console.log('SSE 服务器已启动,监听端口 3000'); 客户端代码: const eventSource = new EventSource('/sse'); eventSource.onmessage = (event) => { console.log('收到服务器推送:', event.data); }; 优缺点分析 优点:实现简单,支持自动重连,节省带宽。 缺点:只支持单向通信,客户端无法主动发送数据。 适用场景:实时新闻推送、数据监控面板。 5. HTTP/2 Push —— 优化资源加载的新选择 HTTP/2 Push 是 HTTP/2 协议中的一项功能,允许服务器在客户端请求前主动推送资源。 优缺点分析 优点:减少延迟,提高页面加载速度。 缺点:浏览器和服务器必须都支持 HTTP/2。 适用场景:主要用于优化前端资源加载,例如预加载 CSS 或 JavaScript 文件。 总结:如何选择合适的即时通信方案? 方案 优点 缺点 适用场景 轮询 简单直接 浪费带宽,响应不及时 低频数据刷新 长轮询 相对高效 服务器资源占用较高 聊天系统、通知系统 WebSocket 双向通信,实时性高 实现复杂 在线游戏、协作工具 SSE 实现简单,支持自动重连 单向通信 数据监控、实时新闻 HTTP/2 Push 优化资源加载 需要 HTTP/2 支持 前端性能优化

2023-10-19

JavaScript 浮点数与 0 按位或,实现舍弃小数位取整

今天工作时,在项目中发现一处有趣的 JavaScript 代码,它将一个浮点数与 0,进行了一个 按位或 运算。开始还不清楚所谓何意,后来通过测试和查证才知道。 原来,使用 按位或运算符 (|) 可以将浮点数转为整数,并舍弃小数部分。这是因为按位操作会将数字隐式转换为 32 位整数,从而舍弃小数部分。示例代码如下: let num = 5.67; let result = num | 0; // 按位或运算 console.log(result); // 输出: 5 原理: 按位或 (|) 运算符会将操作数转换为32位有符号整数。 浮点型的小数部分在转换过程中会被截断,而不是四舍五入。 优点: 性能好:比 Math.floor()、parseInt() 等函数更快。 简洁:代码简单,易于书写。 注意事项: 只能处理 32位整数范围内的数值 (即:-2^31 到 2^31 - 1)。对于超出此范围的数,结果可能不准确。 如果需要 向下取整 或处理 负数,可以改用 Math.floor(): console.log(-5.67 | 0); // 输出: -5 console.log(Math.floor(-5.67)); // 输出: -6 结论: 所以,num | 0 确实是一种快速、简便的方式来舍弃小数部分取整,但要注意其处理负数的行为和范围限制。

2023-06-07

macOS - 给 Terminal(终端)配置网络代理

与浏览器不同,mac 的终端默认并没有开启代理模式,也就是说即使我们电脑安装了代理客户端,在终端中也是无法使用代理的。下面通过样例演示如何对终端配置网络代理。 1. 确定客户端端口 首先我们打开使用的代理客户端设置界面,查看其开放的 HTTP 端口,比如我这里是 1087 2. 配置代理 macOS Catalina 之后,Mac 使用 zsh 作为默认的 Shell 终端,我们这里就以 zsh 配置为例展示配置方法。 修改 ~/.zshrc 配置文件 vim ~/.zshrc 向其中添加如下内容: # Turn on and off all_proxy alias proxy="export all_proxy=http://127.0.0.1:1087 && echo 'Already turn on all_proxy to http://127.0.0.1:1087'" alias unproxy="unset all_proxy && echo 'Already turn off all_proxy'" 保存退出 vim 后,执行如下命令,使配置生效 source ~/.zshrc 3. 测试 首先我们使用 curl 命令查看终端目前的 IP: curl ipinfo.io 得到类似下面的结果,可以看出确实使用的是国内的 IP 地址: { "ip": "...", "region": "Guangdong", "country": "CN", "timezone": "Asia/Shanghai", // ... } 接着我们执行 proxy 命令开启终端代理模式(仅针对当前会话生效): proxy 再次使用 curl 命令查看可以发现变成了国外的 IP(代理服务器的 IP): { "ip": "...", "region": "Tokyo", "country": "JP", "timezone": "Asia/Tokyo", // ... } 如果需要关闭代理模式,执行如下命令即可: unproxy

2023-01-02

如何减少关键路径渲染(Critical Rendering Path)

关键路径渲染(Critical Rendering Path,CRP) 是指浏览器将 HTML、CSS 和 JavaScript 解析为可视页面的过程。CRP 的优化直接影响 页面的首次内容呈现时间 和用户的 加载体验。在现代前端开发中,减少关键路径渲染的时间已成为提升网页性能的关键环节。本文将逐步讲解 CRP 的工作原理,并提供实用的优化策略。 一、什么是关键路径渲染(CRP)? 浏览器渲染页面的过程大致如下: HTML 解析为 DOM 树:浏览器逐行解析 HTML 代码,构建 DOM(Document Object Model)树。 CSS 解析为 CSSOM 树:同时解析 CSS,构建 CSSOM(CSS Object Model)树。 合并 DOM 和 CSSOM 树:生成渲染树(Render Tree),决定哪些元素可见。 布局和绘制:浏览器计算元素的位置和大小(布局),然后将其绘制到屏幕。 如果某些资源(如 JS 或 CSS 文件)未及时加载并阻塞了这个过程,就会延长页面的首屏呈现时间,导致用户体验下降。 二、如何减少关键路径渲染的时间? 减少 CRP 的时间,主要依赖减少资源体积、缩短加载时间和优化资源的解析顺序。以下是一些行之有效的优化策略。 1. 减少阻塞资源 1.1 延迟加载 JavaScript JavaScript 文件会阻塞 HTML 的解析,因此可以使用 defer 或 async 来延迟脚本加载。 <!-- 使用 async 加载,脚本下载完成后立即执行 --> <script src="script.js" async></script> <!-- 使用 defer 加载,脚本在 HTML 解析完成后执行 --> <script src="script.js" defer></script> 推荐:将不影响页面首屏展示的 JS 脚本标记为 defer。 1.2 内联关键 CSS 将首屏展示所需的 CSS内联到 HTML 中,减少 CSS 文件的加载延迟。 <style> body { margin: 0; font-family: sans-serif; } .hero { background-color: #4CAF50; height: 100vh; } </style> 2. 压缩和优化资源 2.1 使用 Gzip 或 Brotli 压缩 启用服务器端压缩,减少 HTML、CSS 和 JavaScript 文件的传输体积。 # 在 nginx 配置中启用 Gzip gzip on; gzip_types text/plain text/css application/javascript; 2.2 使用 Tree Shaking 和代码拆分 通过 Tree Shaking 移除未使用的代码,并利用代码拆分减少首次加载的资源量。 // Tree Shaking 示例,只打包实际使用的函数 import { usedFunction } from './utils'; usedFunction(); 3. 优化 CSS 和字体加载 3.1 减少 CSS 文件体积 删除未使用的 CSS。 使用 CSS 压缩工具(如 cssnano)。 3.2 使用字体加载优化 使用 font-display: swap 避免字体阻塞渲染。 @font-face { font-family: 'CustomFont'; src: url('custom-font.woff2') format('woff2'); font-display: swap; } 4. 使用缓存提升加载速度 启用浏览器缓存:设置 HTTP 头,如 Cache-Control 和 ETag,确保静态资源被缓存。 使用 Service Worker:利用 PWA 技术,将资源缓存到本地,提高页面的离线访问能力。 5. 优先加载关键资源 5.1 使用 <link rel="preload"> 预加载关键资源,确保它们尽快开始下载。 <link rel="preload" href="styles.css" as="style"> <link rel="preload" href="script.js" as="script"> 5.2 使用 <link rel="prefetch"> 为非关键资源使用预获取策略,提高后续页面加载速度。 <link rel="prefetch" href="next-page.html"> 6. 减少重排与重绘 避免频繁操作 DOM:合并多次 DOM 修改,减少重排开销。 使用 CSS 动画代替 JavaScript 动画:CSS 动画通常性能更好。 避免触发 Layout Thrashing:避免在 JS 中频繁读取和写入 DOM 属性。 三、常见问题与解决方案 问题 1:如何判断哪些资源属于关键资源? 解决方案:使用 Chrome DevTools 的“性能面板”查看加载的资源,并标记延迟页面渲染的关键资源。 问题 2:如何避免字体加载造成的闪烁(FOIT)? 解决方案:为字体添加 font-display: swap,让浏览器使用系统字体进行替代,直至自定义字体加载完成。 问题 3:如何减少第三方库对渲染的影响? 解决方案: 使用 async 或 defer 延迟加载第三方 JS 脚本,如广告、分析工具等。 利用动态导入按需加载部分第三方依赖。 使用轻量替代库,如用 day.js 替代 moment.js。 问题 4:如何避免图片加载影响页面首屏渲染? 解决方案: 使用 lazyload 延迟加载首屏外的图片。 将小图片转换为 Base64 格式内联到 HTML 中,减少 HTTP 请求。 使用 srcset 和 sizes 优化响应式图片加载。 问题 5:如何优化首次访问与返回访问的性能差异? 解决方案: 配置 Service Worker 将关键资源缓存到本地,缩短后续访问时间。 使用 HTTP/2 或 HTTP/3 多路复用技术,提升首次访问时的资源加载速度。 问题 6:如何监控并持续优化 CRP? 解决方案: 使用 Lighthouse 或 Web Vitals 插件定期生成性能报告。 集成 Google Analytics 的 First Input Delay (FID) 指标,实时分析页面交互性能。 持续关注 CLS(Cumulative Layout Shift),减少布局偏移问题。 四、总结 减少关键路径渲染时间对于提升网页性能和用户体验至关重要。通过减少阻塞资源、压缩和优化资源、优先加载关键内容和减少重排与重绘,我们可以有效缩短页面的首次内容呈现时间。 在实际项目中,优化 CRP 是一个持续的过程。建议借助 Chrome DevTools 等工具,实时监控性能瓶颈,并逐步优化。优化后的页面不仅能显著提升加载速度,也能提高用户的访问体验,带来更高的转化率。

2022-11-22

手动实现 JavaScript 迭代器

在 JavaScript 中,迭代器是一个非常强大的工具。它允许我们逐步遍历集合中的元素,如数组、字符串、Map 等。然而,除了使用内置的迭代器(如 for...of、Array.prototype.entries() 等),我们还可以手动实现自己的迭代器来满足特殊需求。今天我们就从零开始,逐步带你了解如何实现一个 JavaScript 迭代器。 一、什么是迭代器? 迭代器的本质是一个对象,它提供了一种标准化的访问数据的方法。它需要具备两个关键要素: next() 方法: 每次调用时,返回一个包含 value 和 done 属性的对象。 状态跟踪: 用于记录当前迭代的进度。 返回的对象结构为: { value: <当前值>, done: <是否结束> } value 表示当前迭代项的值。 done 是一个布尔值,用于表示迭代是否结束。 二、内置迭代器的简单例子 在继续之前,我们先来看一个数组的内置迭代器: const arr = [1, 2, 3]; const iterator = arr[Symbol.iterator](); console.log(iterator.next()); // { value: 1, done: false } console.log(iterator.next()); // { value: 2, done: false } console.log(iterator.next()); // { value: 3, done: false } console.log(iterator.next()); // { value: undefined, done: true } 这里我们使用了数组的内置 Symbol.iterator 方法。每次调用 next() 都会依次获取数组的下一个元素,直到所有元素遍历完成。 三、手动实现一个简单的迭代器 我们现在尝试手动实现一个基本的迭代器。目标是实现一个能遍历数组的自定义迭代器。 function createArrayIterator(arr) { let index = 0; return { next() { if (index < arr.length) { return { value: arr[index++], done: false }; } else { return { value: undefined, done: true }; } } }; } const iterator = createArrayIterator([10, 20, 30]); console.log(iterator.next()); // { value: 10, done: false } console.log(iterator.next()); // { value: 20, done: false } console.log(iterator.next()); // { value: 30, done: false } console.log(iterator.next()); // { value: undefined, done: true } 代码说明: createArrayIterator 函数接收一个数组作为参数。 每次调用 next(),都会返回当前数组元素并将索引前进一位。 当数组遍历完成后,返回 { value: undefined, done: true }。 四、为对象添加迭代器 我们可以通过 Symbol.iterator 为任何对象定义自定义的迭代逻辑。这样,我们的对象也能在 for...of 循环中使用。下面,我们为一个自定义对象实现迭代器。 const range = { start: 1, end: 5, [Symbol.iterator]() { let current = this.start; const end = this.end; return { next() { if (current <= end) { return { value: current++, done: false }; } else { return { value: undefined, done: true }; } } }; } }; for (const num of range) { console.log(num); // 输出 1 2 3 4 5 } 代码说明: range 对象表示一个范围,从 start 到 end。 在对象内部使用 Symbol.iterator 定义了一个迭代器。 for...of 会自动调用对象的 Symbol.iterator 方法来获取迭代器。 五、实现无限迭代器 有时候,我们需要创建一个无限序列,比如生成斐波那契数列。下面是一个无限斐波那契迭代器的实现: function fibonacciIterator() { let [prev, curr] = [0, 1]; return { next() { [prev, curr] = [curr, prev + curr]; return { value: prev, done: false }; } }; } const fib = fibonacciIterator(); console.log(fib.next().value); // 1 console.log(fib.next().value); // 1 console.log(fib.next().value); // 2 console.log(fib.next().value); // 3 console.log(fib.next().value); // 5 console.log(fib.next().value); // 8 代码说明: 每次调用 next(),斐波那契序列都会前进一位,并返回当前的值。 无限序列的迭代器没有终止条件,因此 done 始终为 false。 六、可迭代对象与迭代器的区别 可迭代对象:具有 Symbol.iterator 方法的对象,可以在 for...of 中使用,如数组、字符串等。 迭代器:实现了 next() 方法的对象,每次调用 next() 返回一个 { value, done } 对象。 总结: 可迭代对象内部使用迭代器来实现元素的逐个访问。for...of 只是对迭代器的一种封装。

2022-11-21

基于 axios 手写文件上传,实现进度监听、上传中断

要基于 axios 手写一个文件上传组件,并实现以下功能: 选择文件并上传 监听上传进度 支持上传中断(取消请求) 处理成功和失败的回调 下面是一个完整的实现代码示例: 1. 安装依赖 npm install axios 2. 代码实现 HTML 模板示例: <div id="app"> <input type="file" id="fileInput" /> <button id="uploadButton">上传</button> <button id="cancelButton">取消上传</button> <div id="progress">上传进度: 0%</div> </div> JavaScript 代码实现: import axios from 'axios'; let cancelTokenSource = null; // 用于取消上传 // 监听文件选择并上传 document.getElementById('uploadButton').addEventListener('click', () => { const fileInput = document.getElementById('fileInput'); if (fileInput.files.length === 0) { alert('请选择文件'); return; } const file = fileInput.files[0]; uploadFile(file); }); // 取消上传 document.getElementById('cancelButton').addEventListener('click', () => { if (cancelTokenSource) { cancelTokenSource.cancel('上传已取消'); } }); // 文件上传逻辑 function uploadFile(file) { const url = 'https://your-upload-endpoint.com/upload'; // 替换为实际上传地址 // 创建 Axios 的取消令牌 cancelTokenSource = axios.CancelToken.source(); // 创建表单数据 const formData = new FormData(); formData.append('file', file); axios.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data', }, cancelToken: cancelTokenSource.token, onUploadProgress: (progressEvent) => { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); document.getElementById('progress').innerText = `上传进度: ${percentCompleted}%`; }, }) .then((response) => { alert('上传成功'); console.log(response.data); }) .catch((error) => { if (axios.isCancel(error)) { console.log('上传被取消:', error.message); } else { console.error('上传失败:', error); alert('上传失败'); } }); } 3. 代码说明 选择文件并上传:用户点击上传按钮时,调用 uploadFile 函数,将文件上传到指定的服务器端。 监听上传进度:使用 onUploadProgress 回调函数来实时更新进度。 支持上传中断:使用 axios.CancelToken 创建可取消的请求,并在用户点击“取消上传”按钮时触发中断。 处理错误:区分普通错误和用户主动取消上传的情况。 4. 效果预览 进度监听:上传时,页面上会实时显示百分比。 取消上传:上传过程中的任何时间都可以中断请求。 5. 服务器端(可选参考) 后端需要支持接收 multipart/form-data 请求。如果你使用的是 Node.js,可以用 express 搭配 multer 处理文件上传。 const express = require('express'); const multer = require('multer'); const upload = multer({ dest: 'uploads/' }); const app = express(); app.post('/upload', upload.single('file'), (req, res) => { console.log('文件信息:', req.file); res.send({ status: '上传成功' }); }); app.listen(3000, () => { console.log('服务器启动在 http://localhost:3000'); }); 这个组件实现了完整的文件上传流程,你还可以根据需要进一步定制,比如支持多个文件、添加更多的上传校验等。

2022-05-12

在 PowerShell 中使用 Git

前言 在 macOS 下 git 命令行工具默认有着很好的 tab 补全功能,而在 Windows 下通过 exe 安装的 git 程序,看起来就有些简陋。 其自带的 Unix Shell 环境模拟窗口 Git Bash,有着丑陋的外观,即便可以通过配置字体、颜色等手段稍加改善,但其一会儿类 Unix 工具链环境的反馈,一会儿 Windows cmd 工具链混搭的集成环境,着实容易让人精神分裂。 为了更好的自始至终统一使用体验,我们通常会将 git 程序添加到 全局 path 中(引导安装程序即可选配),然后在 cmd 或 PowerShell(通常是功能更强大的 PowerShell)中调用 git。 然而在 PowerShell 中调用 git 时,我们丧失了 tab 补全功能。 这里,我们介绍使用 Posh-Git 这个扩展包,从而在 PowerShell 中应用 git 的 tab 补全。 安装 Post-Git 配置脚本执行权限 在可以运行 PowerShell 脚本之前,你需要将本地的 ExecutionPolicy 设置为 RemoteSigned 在 PowerShell 中执行下面的命令,更精细化的配置参见 微软文档 Set-ExecutionPolicy Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine 使用包管理器安装 posh-git 在 PowerShell 中执行下面的命令,通过包管理器来安装 posh-git Install-Module posh-git -Scope CurrentUser -Force Install-Module posh-git -Scope CurrentUser -AllowPrerelease -Force # 带有 PowerShell Core 支持的更新的 beta 版 如果你想为所有的用户安装 posh-git,请使用 -Scope AllUsers 并在管理员权限启动的 PowerShell 控制台中执行。 更新 PowerShell 提示符 要在你的提示符中包含 Git 信息,那么需要导入 Posh-Git 模块。 要让 PowerShell 在每次启动时都导入 Posh-Git,请执行 Add-PoshGitToProfile 命令, 它会在你的 $profile 脚本中添加导入语句。此脚本会在每次打开新的 PowerShell 终端时执行。 Import-Module posh-git Add-PoshGitToProfile -AllHosts 更多详细内容,参见:Git 文档:A1.9 附录 A: 在其它环境中使用 Git - Git 在 PowerShell 中使用 Git 自定义 posh-git 提示符 当您导入 posh-git 模块时,它将用新的提示功能替换 PowerShell 的默认提示功能。当当前目录位于 Git 存储库中时,posh-git 提示功能将显示 Git 状态摘要信息。如果 posh-git 检测到您有自己的自定义提示功能,则不会替换提示功能。 这里可以通过编辑当前用户 ps1 文件,实现自定义提示符: echo $Profile 得到形如下方的绝对路径,表示当前用户 ps1 配置文件默认加载位置 C:\Users\liang\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 打开并编辑这个文件,加入下面的内容: $GitPromptSettings.DefaultPromptPrefix.Text = 'PS [$(Get-Date -f "HH:mm:ss")] ' $GitPromptSettings.DefaultPromptPrefix.ForegroundColor = [ConsoleColor]::Magenta $GitPromptSettings.DefaultPromptAbbreviateHomeDirectory = $true $GitPromptSettings.BeforePath = '{' $GitPromptSettings.AfterPath = '}' $GitPromptSettings.DefaultPromptPath.ForegroundColor = 0xFFA500 $GitPromptSettings.BeforePath.ForegroundColor = 0xFFA500 $GitPromptSettings.AfterPath.ForegroundColor = 0xFFA500 $GitPromptSettings.DefaultPromptBeforeSuffix.Text = '`n' 你将获得一个同时拥有 时间、家目录缩写、git 状态的提示符,它会像下面这样: 更多详细内容,参见:GitHub - dahlbyk/posh-git - Customizing the posh-git prompt 顺便说下中文乱码问题 PowerShell 下 git log 中文、git status 文件名等,可能存在中文乱码的问题。 可以向下面一样配置 git: git config --global core.quotepath false git config --global gui.encoding utf-8 git config --global i18n.commit.encoding utf-8 git config --global i18n.logoutputencoding utf-8 一劳永逸的方式: 系统设置 -> 管理语言设置,打开旧版的区域设置窗口 更改系统区域设置 -> Beta 版:使用 Unicode UTF-8 提供全球语言支持,勾选它,重启✅

2022-04-22

PowerShell 修改提示符的文字内容和样式

前言 默认的 PowerShell 提示符丑? 其实我们可以通过编辑 PowerShell 的启动脚本,来自定义提示符的文字内容和样式 配置脚本执行权限 在可以运行 PowerShell 脚本之前,你需要将本地的 ExecutionPolicy 设置为 RemoteSigned 在 PowerShell 中执行下面的命令,更精细化的配置参见 微软文档 Set-ExecutionPolicy Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine 查看当前用户 ps1 文件位置 echo $Profile 得到形如下方的绝对路径,表示当前用户 ps1 配置文件默认加载位置 C:\Users\liang\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 编辑 ps1 配置文件 编辑这个 ps1 配置文件,可以改变 PowerShell 初始化时的一些行为 如果是第一次使用 PowerShell,这个文件可能不存在,需要我们手动创建 如果当前系统安装有 VScode,可以执行一下命令,快速创建或打开 code $Profile 下方是我的自定义配置文件内容,可以参考 function prompt { Write-Host ("`nPS [") -nonewline Write-Host ($(get-date -Format "HH:mm:ss")) -nonewline -foregroundcolor Red Write-Host ("] {") -nonewline Write-Host ($(pwd)) -nonewline -foregroundcolor Blue Write-Host ("}") return "$ " } 使自定义配置文件生效 一种方式是: 关闭并重新打开当前 PowerShell 会话,修改的配置文件效果便会生效 另一种即时生效的方式是: 执行重新加载命令: . $Profile 最终效果

2022-04-22