我常使用的一些前端编码技巧 ⚔️

1,057 阅读3分钟

image.png

持续更新中,欢迎关注……

  • 2025-12-3 增加“单元测试如何断言有颜色的输出”
  • 2025-5-27 增加第十一条技巧
  • 2025-3-10 initial

如果能有所收获欢迎点赞支持! 公众号:JavaScript与编程艺术

1. 巧妙替换部分 URL 参数 #浏览器 🌐

答案:URLSearchParams;兼容性:Chrome ≥ 49;Safari on iOS 10.3

比如想将 q 改成 '你好 world'

http://foo.ai/c?q=Who+is+on+the+new+initial+board+of+OpenAI%3F&source=hot

输出

http://foo.ai/c?q=%E4%BD%A0%E5%A5%BD%20world&source=hot

这样一个看起来简单的需求有几个注意点:

  1. 其他 query 不能受影响,比如 source=hot
  2. 需要对新加入的 query encode。你好 => %E4%BD%A0%E5%A5%BD%20(比较容易忘记)。

一般的做法使用正则表达式精确替换(正则表达式难写难维护),这里提供一个巧妙的办法:

function updateQuery(key, value) {
  const params = new URLSearchParams(location.search);

  // 更新
  params.set(key, value);

  // 返回更新后参数(可选)
  return params.toString();
}

既能精准替换又能自动 encode 🎉!存在相同的 key 还能自动替换而不会出现重复。

2. URL 参数获取和拼接 query #浏览器 🌐

答案还是:URLSearchParams

如果不用需自行 split + reduce + decodeURIComponent 一套组合拳下来才能获取所有参数。

使用 URLSearchParams

  • 获取所有
// location.search = '?q=Who+is+on+the+new+initial+board+of+OpenAI%3F&source=hot'
const params = new URLSearchParams(location.search)

const paramsInObject = Object.fromEntries(params)
// => {q: 'Who is on the new initial board of OpenAI?', source: 'hot'}

const paramsInEntryArray = [...params]
// => [ [ "q", "Who is on the new initial board of OpenAI?" ], [ "source", "hot" ] ]
  • 获取某个
params.get('q') // 'Who is on the new initial board of OpenAI?'
  • 拼接参数
const params = new URLSearchParams()

params.set('q', 'Who is on the new initial board of OpenAI?')
params.set('source', 'hot')

const url = 'http://foo.ai/c?' + params.toString()
// 或
const url = 'http://foo.ai/c?' + params // `http://foo.ai/c?${params}`
// => 'http://foo.ai/c?q=Who+is+on+the+new+initial+board+of+OpenAI%3F&source=hot'

现在有 url https://example.com?a=b 现在想往里面添加 { foo: 'bar', foz: 'baz' } 这个对象非嵌套,但是内容未知,请问如何做到:

/**
 * ### 将对象参数附加到 url 参数中:
 * - 如果 url 存在参数则二者 merge,
 * - 如果参数相同则覆盖已有参数
 * @param url 请求地址
 * @param params
 * @returns
 * @example
 * // 使用示例
 * const originalUrl = 'https://example.com?a=b';
 * const newParams = { foo: 'bar', foz: 'baz', arr: ['x', 'y'], empty: null };
 *
 * const newUrl = addParamsToUrl(originalUrl, newParams);
 * console.log(newUrl);
 * // 输出: https://example.com?a=b&foo=bar&foz=baz&arr=x&arr=y
 */
function addParamsToUrl(url: string, params: IFlatObject) {
  // 创建 URL 对象
  const urlObj = new URL(url);

  // 获取现有的查询参数
  const searchParams = new URLSearchParams(urlObj.search);

  // 添加新的参数
  Object.entries(params).forEach(([key, value]) => {
    if (value !== null && value !== undefined) {
      if (Array.isArray(value)) {
        // 如果是数组,添加多个相同键的参数
        value.forEach((item) => {
          if (item !== null && item !== undefined) {
            searchParams.append(key, String(item));
          }
        });
      } else {
        // 如果是单个值,直接设置或覆盖
        searchParams.set(key, String(value));
      }
    }
  });

  // 更新 URL 的查询字符串
  urlObj.search = searchParams.toString();

  // 返回完整的 URL
  return urlObj.toString();
}

3. 获取 HTML 标签内所有文字(仅文字,嵌套标签无需)#浏览器 🌐

  • 输入 <ul>你好<li><strong>hello world</li></strong>世界</ul>
  • 输出 你好hello world世界

使用正则表达式很麻烦且易出错。浏览器环境有一种取巧方式,几行代码搞定,而且可读性很好:

/**
 * @param {string} html
 */
function extractText(html) {
  const div = document.createElement('div');
  
  div.innerHTML = html;
  
  return div.textContent;
}

4. top level `await` #node.js 🟢

Node.js 文件后缀改成 .mjs 即可。

Before:

(async function () {
  await doSth()
})()

After:

await doSth()

5. `@ts-check` 让 JS 也支持 TS 类型错误检查 #通用 👨‍💻

开启 ts 类型检查有两种方式:

  1. 头部注释增加 // @ts-check
// @ts-check

2 配置文件:jsconfig.json

{
  "compilerOptions": {
    "checkJs": true,
    "strict": true
  }
}

如果只是单个或少量文件,方式一简单又可满足诉求。还有个小技巧可通过 // @ts-nocheck 忽略检查某个文件。

6. 优先 `// @ts-expect-error` 而非 `// @ts-ignore` #通用 👨‍💻

当我们觉得某行代码报 TS error 是合理的时候,绝大部分情况应该通过 @ts-expect-error 抑制错误。很常见的一例情况是在单测中我们需要测试函数接收不同类型的参数的表现:

// src/utils.ts

export function foo(bar: string) {
    ...
}

上述函数接受 string 类型,但我们想测试在传入数字时其正确性,如果这么写会报 TS 类型错误:

// tests/utils.test.ts

import { foo } from 'src/utils'

// 此行会报错
expect(foo(123)).toEqual(...) 

可以用 @ts-expect-error:

// tests/utils.test.js

// @ts-expect-error 测试非预期类型参数报错符合预期
expect(foo(123)).toEqual(...)

那么和 @ts-ignore 的区别是什么以及为什么推荐使用 @ts-expect-error

  • 可读性更好,因为意图更明显。
  • 后续如果此处被修复了,不报错了,ts 还会提醒我们可以删除该注释了。

更多解释详见 How to use @ts-expect-error

注意: 社区有一种声音,在某些情况下用 any// @ts-expect-error 更好,因为后者的抑制范围太大了。比如 下面代码只是因为入参不符合要求(当时实际上运行时是符合的)但我们又不想对 json 写太精确的类型“耗时费力”,如果直接对整行使用 // @ts-expect-error 会导致无论是哪部分有类型问题都会被“遮盖”:可能是 modelUtils.parse 错写成了 modelUtil.parse,类似我们 try-catch 了整个导致该暴露的错误被“淹没”。

// @ts-expect-error
const { id: modelId } = modelUtils.parse(json);

故社区更好的写法是用 any

const { id: modelId } = modelUtils.parse(json as any);

但我不敢苟同,虽然精确性达到了,但是可读性相比还是差一些,能否鱼与熊掌二者兼得?

答案是可以的:

const { id: modelId } = modelUtils.parse(
  // @ts-expect-error <此处写错误抑制原因>
  json,
);

7. 获取用户环境语言 #node.js 🟢

const lang = Intl.DateTimeFormat().resolvedOptions().locale;

8. 大对象断言 #单测 🧪

小技巧,对大对象一般我们会手动复制,vitest 其实可以先用 toMatchInlineSnapshot 自动补充,然后改成 toEqual(方便高亮和做局部修改)。

9. 模糊断言 #单测 🧪

Vitest

expect.

  • anything
  • any
  • string
  • closeTo
  • arrayContaining
  • objectContaining
  • stringContaining
  • stringMatching

RTL - React Testing Library

testing-library.com/docs/dom-te…

  • 断言异步出现的节点:await findByText(string)
  • 断言跨多元素的文本 - 函数:await findByText((text, node) => { ... })
  • 断言跨多元素的文本 - 正则表达式 + timeout 避免手动 waitFor / sleepawait findByText(/正则表达式/, {}, { timeout }) timeout 默认 1s。

Bad

await waitFor(() => {
  return utils.findByText(/深度思考完成/)
}, { timeout: 4e3 })

Good

await utils.findByText(/深度思考完成/, {}, { timeout: 4e3 })
  • screen.debug():打印渲染结果方便调试单测

我们举几个常见使用场景来说如何使用。

示例 1:只想断言类型是字符串

非常适合测试生成随机字符串的场景。

test('"id" is a string', () => {
  expect({ id: generateId() }).toEqual({ id: expect.any(String) })
})
示例 2:只想断言字段是日期格式(2024-06-28 16:54:26)
test('"id" is a string', () => {
  expect({ time: generateTime() }).toEqual({
    time: expect.stringMatching(/\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}/)
  })
})
示例 3:断言文本跨多个元素,且支持 React.ReactNode #rtl

如果文本跨多个元素,经常会遇到这种报错 “Unable to find an element with the text: Hello world. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

我们可以使用函数或正则表达式解决。

比如我们想断言 确定要删除“<b>XXXYYYZZZ</b>”模板吗?

函数断言

更直观一些

import React from 'react'
import { expect, test } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'

test('should show the correct text when btn clicked', async () => {
   const Hello = () => (
    <div>
      确定要删除“<b>XXXYYYZZZ</b>”模板吗?
    </div>
  );
  
  render(<Hello />);

  // 断言 `确定要删除“<b>XXXYYYZZZ</b>”模板吗?`
  // 因为被多个元素截断所以使用正则表达式
  const nodes = await screen.findAllByText((text, node) => {
    // 注意 text 是 '确定要删除“”算力券模板吗?' 所以用 innerHTML 做完整断言更合适
    // 断言能传入 React.ReactNode
    return node?.innerHTML === '确定要删除“<b>XXXYYYZZZ</b>”模板吗?'
  })
})

正则表达式断言

更繁琐一些

import React from 'react'
import { expect, test } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'

test('should show the correct text when btn clicked', async () => {
   const Hello = () => (
    <div>
      确定要删除“<b>XXXYYYZZZ</b>”模板吗?
    </div>
  );
  
  render(<Hello />);

  // 因为被多个元素截断所以使用正则表达式
  const nodes = await screen.findAllByText(/确定要删除“/)
  
  expect(nodes.length).toBe(1)
  
  // 断言能传入 React.ReactNode
  expect(nodes[0].innerHTML).toBe('确定要删除“<b>XXXYYYZZZ</b>”模板吗?')
})

10. 对含颜色字符串做断言:巧用 stripVTControlCharacters 去除 ansi color code #单测 🧪

在 CLI 工具中我们经常会使用颜色来加强对比突出意图。 比如 github.com/legend80s/s…

export const WHITE = '\x1b[37m'
export const RESET = '\x1b[0m'
export const RED = '\x1b[31m'
export const GREEN = '\x1b[32m'
export const YELLOW = '\x1b[33m'
// const UNDERLINE = '\x1b[4m'
const BOLD = '\x1b[1m'

/**
 * @param {`\x1b[${string}m`} color
 * @returns
 */
function colorize(color) {
  /** @param {string} str */
  return (str) => `${color}${str}${RESET}`
}

export const green = colorize(GREEN)
export const bold = colorize(BOLD)

但这样的输出会夹杂类似 \x1b[\dmANSI color codes,如果要对其对断言将很麻烦,有两种办法:

import { beforeEach, it } from 'node:test'
 
beforeEach(() => {
  process.env.FORCE_COLOR = '0'
})

然后并可如对普通字符串断言:

// https://github.com/legend80s/stoc/blob/master/tests/bin.test.mjs

it('Use `interface`: in body', () => {
  const input = `node bin.mjs -i assets/openapi-3.0.1.json --api fox/list --use-interface --no-request --no-grouped --no-return-type`
  const actual = execSync(input).toString('utf8')
  const expected = `...`
  deepStrictEqual(actual, expected)
})

但是这样会让单元测试的所有输出如 actual expected + - 都“黯然失色”,也就是其范围太大了,第二种办法:

+ import { stripVTControlCharacters } from 'node:util'

- const actual = execSync(input).toString('utf8')
+ const actual = stripVTControlCharacters(execSync(input).toString('utf8'))

巧用 Node.js v16 就有的 stripVTControlCharacters 去除 ansi color code!

console.log(util.stripVTControlCharacters('\u001B[4mvalue\u001B[0m'));
// Prints "value"

大家还可关注 v22.13.0 变稳定的 API styleText,这样可以无需自己写 ansi color 或引入 chalk 这样的包了:

console.log(
  util.styleText(['underline', 'italic'], 'My italic underlined message'),
);

11. setTimeout 的 promise 版本 #node.js 🟢

import { setTimeout } from 'node:timers/promises';

await setTimeout(1e3);

12. 巧用剩余参数运算符 `...`(rest operator)删除元素

利用 ... 我们可以删除对象的某个 key 同时避免了使用 delete 性能差,会修改元对象的缺点。

const params1 = { name: 'foo', type: 'bar' }

// `_` 表示忽略或丢弃
const { name: _name, ...rest } = params1

console.log(rest) // { type: 'bar' }

参考 📚

如果觉得本文对你有帮助 🤝,希望能够给我点赞支持一下哦 💖。

也欢迎关注公众号『JavaScript与编程艺术』。

image.png