高级前端程序员的 10 个实战技巧 ⚔️

843 阅读5分钟

image.png

持续更新中(2025-3-10),欢迎关注……

如果能有所收获欢迎点赞支持!

公众号:JavaScript与编程艺术

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

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

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

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

输出

http://prophetes.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://prophetes.ai/c?' + params.toString()
// => 'http://prophetes.ai/c?q=Who+is+on+the+new+initial+board+of+OpenAI%3F&source=hot'

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

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

  • 断言异步出现的节点:await findByText(string)
  • 断言跨多元素的文本 - 函数:await findByText((text, node) => { ... })
  • 断言跨多元素的文本 - 正则表达式 + timeout 避免手动 waitFor / sleep:await findByText(/正则表达式/, {}, { timeout })
  • 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. setTimeout 的 promise 版本 #node.js 🟢

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

await setTimeout(1e3);

参考 📚

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

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

image.png