阅读视图

发现新文章,点击刷新页面。

Vue 3的新特性解析与开发实践指南

1、使用defineCustomElement定义组件

在 Vue 3 中,defineCustomElement 是用来创建自定义元素的 API,它允许开发者将 Vue 组件封装成自定义元素(Custom Element),自定义元素是 Web Components 的一部分,可以在其他框架或者原生 HTML、JavaScript 环境中使用。这对于构建可复用的 UI 组件非常有用,以下是defineCustomElement 的解析和使用方法

1-1. 什么是自定义元素(Custom Element)?

自定义元素是浏览器原生支持的组件化技术,具有以下特点:

  • 跨框架:可以在 Vue、React、Angular 或原生 HTML 中使用
  • 封装性:样式和行为封装在组件内部,不会影响外部
  • 生命周期:支持自定义生命周期钩子(如 connectedCallbackdisconnectedCallback 等)

1-2. defineCustomElement 的作用

defineCustomElement 是 Vue 3 提供的 API,用于将 Vue 组件转换为自定义元素。转换后的组件可以像原生 HTML 标签一样使用

1-3. 使用 defineCustomElement 的步骤

(1) 定义 Vue 组件

首先,定义一个普通的 MyComponent.vue 组件

// MyComponent.vue
<template>
  <div>
    <h1>{{ message }}</h1>
    <button @click="handleClick">Click Me</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const message = ref('Hello, Custom Element!');

const handleClick = () => {
  alert('Button clicked!');
};
</script>

(2) 将MyComponent.vue组件转换为自定义元素

使用 defineCustomElement 将 Vue 组件转换为自定义元素

customElements对象是Web Components API的一部分,用于定义自定义元素。 ‌在JavaScript中,customElements对象是由浏览器提供的,用于创建和管理自定义元素。你可以通过调用customElements.define()方法来定义一个新的自定义元素。这个方法接受两个参数:元素的名称和元素的构造函数

// main.js
import { defineCustomElement } from 'vue';
import MyComponent from './MyComponent.vue';

// 将 Vue 组件转换为自定义元素
const MyCustomElement = defineCustomElement(MyComponent);

// 注册自定义元素
customElements.define('my-component', MyCustomElement);

(3) 在 HTML 中使用

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Vue Custom Element</title>
</head>
<body>
  <!-- 在 HTML 中使用 MyComponent.vue 组件-->
  <my-component></my-component>
  <script src="./main.js"></script>
</body>
</html>

  1. 传递 Props

可以通过属性(Attributes)或属性(Properties)向自定义元素传递数据

(1) 通过属性传递

属性值只能是字符串,需要通过 props 接收

<my-component message="Hello from Attribute!"></my-component>

通过 JavaScript 设置属性值,可以传递任意类型的数据

const element = document.querySelector('my-component');
element.message = 'Hello from Property!';

在组件中接收:

// MyComponent.vue
<template>
  <div>
    <h1>{{ message }}</h1>
    <button @click="handleClick">Click Me</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
// 接收父组件传递过来的属性
defineProps({
  message: String,
});

const message = ref('Hello, Custom Element!');

const handleClick = () => {
  alert('Button clicked!');
};
</script>

  1. 监听事件

自定义元素可以触发自定义事件,父组件可以通过 addEventListener 监听

// MyComponent.vue
<template>
  <div>
    <h1>{{ message }}</h1>
    <button @click="handleClick">Click Me</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
// 接收父组件传递过来的属性
defineProps({
  message: String,
});

const message = ref('Hello, Custom Element!');

// 自定义元素可以触发自定义事件,父组件可以通过 `addEventListener` 监听
const handleClick = () => {
  const event = new CustomEvent('custom-click', {
    detail: { message: 'Button clicked!' },
  });
  dispatchEvent(event);
};
</script>

在父组件中监听:

const element = document.querySelector('my-component');
element.addEventListener('custom-click', (event) => {
  console.log(event.detail.message); // 输出: Button clicked!
});

  1. 生命周期钩子
class MyCustomElement extends HTMLElement {
  connectedCallback() {
    console.log('Custom element added to the DOM');
  }

  disconnectedCallback() {
    console.log('Custom element removed from the DOM');
  }
}

customElements.define('my-custom-element', MyCustomElement);

  1. 样式封装 自定义元素支持 Shadow DOM,可以将样式封装在组件内部
class MyCustomElement extends HTMLElement {
  connectedCallback() {
    console.log('Custom element added to the DOM');
  }

  disconnectedCallback() {
    console.log('Custom element removed from the DOM');
  }
}

customElements.define('my-custom-element', MyCustomElement);

Vue 3.5 对自定义元素defineCustomElement的支持进一步增强,增加了多个实用的 API 和功能:

  • 通过 configureApp 选项支持自定义元素的应用程序配置,允许在自定义元素初始化时配置 Vue 应用实例,例如全局插件、混入或依赖注入

  • 添加 useHost()、useShadowRoot() 和 this.$host API,用于访问自定义元素的宿主元素和影子根

  • 支持通过传递 shadowRoot: false 来在没有 Shadow DOM 的情况下挂载自定义元素,默认情况下, Vue 自定义元素会使用 Shadow DOM 进行封装,以提高样式隔离性

  • 支持提供 nonce 选项,该选项将附加到自定义元素注入的 <style>标签, 用于指定 CSP(内容安全策略)的 nonce 值,确保内联样式和脚本符合安全策略

这些新的仅自定义元素选项可以通过第二个参数传递给 defineCustomElement

useHost() 是一个在 Vue 3 的自定义渲染器中使用的方法,它允许你访问宿主(host)元素。这在创建自定义渲染器或在封装原生组件时非常有用。例如,如果你正在创建一个封装了原生 <canvas> 元素的 Vue 组件,你可能需要访问底层的 DOM 元素来操作它

import { defineComponent, useHost } from 'vue';
 
export default defineComponent({
  setup() {
    const host = useHost();
    // 你可以在这里操作 host 元素
    console.log(host);
    return {};
  }
});

useShadowRoot() 同样是在自定义渲染器中使用的,它允许你访问 Shadow DOM 的根节点。这对于创建封装了 Shadow DOM 的组件特别有用,例如 Web Components

import { defineComponent, useShadowRoot } from 'vue';
 
export default defineComponent({
  setup() {
    const shadowRoot = useShadowRoot();
    // 你可以在这里操作 Shadow DOM
    console.log(shadowRoot);
    return {};
  }
});

在 Vue 2 中,this.$host 是用来访问宿主元素的。但在 Vue 3 中,特别是在使用标准的 Vue API 和不涉及自定义渲染器的情况下,并没有直接暴露 this.$host

如果你想在 Vue 3 中访问宿主元素,通常的做法是使用 ref 和 onMounted 钩子来获取对 DOM 元素的引用

import { defineComponent, onMounted, ref } from 'vue';
 
export default defineComponent({
  setup() {
    const hostRef = ref(null);
    onMounted(() => {
      if (hostRef.value) {
        // 访问宿主元素
        console.log(hostRef.value);
      }
    });
    return { hostRef };
  }
});

shadowRoot| nonce| configureApp

import { defineCustomElement } from 'vue'
import MyElement from './MyElement.ce.vue';
 
const MyElement = defineCustomElement(MyElement,{
  configureApp(app) {
    // 配置 Vue 应用实例,例如添加插件或全局变量
    app.provide('someGlobal', 'value') // 全局依赖注入
    app.directive('focus', {
        mounted(el) {
            el.focus(); 
        }
    }); // 注册指令
    app.config.errorHandler = function() {
      // 错误处理逻辑
    };

  },
  
  shadowRoot: true, // 使用 Shadow DOM
  
  nonce: generateNonce(), // 设置 CSP nonce值
  
  styles: [`
    :host {
      display: block;
      color: v-bind(color);
    }
  `],
})

// 使用浏览器原生 Crypto API 生成安全随机值(推荐)
// 生成示例:f85a7c4e3b1d9a02
function generateNonce() {
  const buffer = new Uint8Array(16);
  window.crypto.getRandomValues(buffer);
  return Array.from(buffer, byte => byte.toString(16).padStart(2, '0')).join('');
}


 
customElements.define('my-element', MyElement)

在 defineCustomElement中的configureApp配置注册指令

const MyElement = defineCustomElement({
  template: `<input v-auto-slug />`
}, {
  configureApp(app) {
    app.directive('auto-slug', {
      mounted(el) {
        el.addEventListener('input', (e) => {
          e.target.value = e.target.value
            .toLowerCase()
            .replace(/\s+/g, '-');
        });
      }
    });
  }
});

在 defineCustomElement中的configureApp配置依赖注入

defineCustomElement({
  template: `<child-component />`
}, {
  configureApp(app) {
    // 注入全局数据,依赖注入
    app.provide('apiKey', '123-456-789');
    // 注入全局方法,依赖注入
    app.provide('formatDate', (date) => new Date(date).toLocaleString());
  }
});

子组件通过 inject 使用:

const apiKey = inject('apiKey');
const formatDate = inject('formatDate');

总结

defineCustomElement 是 Vue 3 中用于将 Vue 组件转换为自定义元素的 API,具有以下优势:

  • 跨框架:可以在任何框架或原生 HTML 中使用
  • 封装性:支持 Shadow DOM 和样式封装
  • 灵活性:支持 Props、事件和生命周期钩子

通过 defineCustomElement,可以将 Vue 组件无缝集成到现有项目中,或将其发布为独立的 Web Components

2、Lazy Hydration(懒加载水合)

Vue 3.5 引入了懒加载水合策略,通过 hydrateOnVisible() 选项,仅在组件可见时进行水合。这一功能可以减少不必要的资源消耗,提高页面初次加载的性能

<script setup>
import { hydrateOnVisible } from 'vue';

const lazyComponent = hydrateOnVisible(() => import('./MyComponent.vue'));
</script>

此特性对于使用 SSR 的项目,尤其是在大规模应用中,能够显著提升用户体验

3、useId:一致的唯一 ID 生成

Vue 3.5新增的 useId() API 可以生成在 SSR 和客户端之间一致的唯一 ID,解决了在 SSR 中可能出现的 ID 不一致问题

<script setup>
import { useId } from 'vue';

const id = useId();
</script>

<template>
  <label :for="id">Name:</label>
  <input :id="id" type="text" />
</template>

这对于生成表单元素和无障碍属性的 ID,确保 SSR 应用中不会导致水合不匹配

4、 useTemplateRef:动态模板引用

Vue 3.5 之前获取dom元素的方法:

<template>
  // 步骤1 ref="myElement"
  <div ref="myElement">目标元素</div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
const myElement = ref(null); // 步骤2

onMounted(() => {
  console.log(myElement.value); // 输出 DOM 元素
});
</script>

Vue 3.5 引入了一种通过 useTemplateRef() API 获取模板引用的新方法,支持动态 ref 绑定到变化的 ID

<script setup>
import { useTemplateRef } from 'vue';

const inputRef = useTemplateRef('input');
</script>

<template>
  <input ref="input">
</template>

相比之前仅限于静态 ref 属性的方法,useTemplateRef() 提供了更灵活的引用方式,也更容易理解

5、 onWatcherCleanup、onEffectCleanup:观察者清理回调,

Vue 3.5 引入了一个全局导入的 API onWatcherCleanup(),用于在观察者中注册清理回调,避免内存泄漏

类似 onEffectCleanup,专为 watch 设计的清理钩子

import { watch, onWatcherCleanup } from 'vue';

watch(id, (newId) => {
  //1这里
  const controller = new AbortController();
  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // 回调逻辑
  });
  //2这里
  onWatcherCleanup(() => {
    // 中止陈旧请求
    controller.abort();
  });
});

这对于在组件卸载之前或者下一次 watch 回调执行之前进行资源清理非常有用

onEffectCleanup

在响应式 effect 清理前执行自定义逻辑(如取消定时器/请求)

专为 watchEffect 设计的清理钩子

watchEffect(() => {
  const timer = setInterval(doSomething, 1000);
  onEffectCleanup(() => clearInterval(timer));
});

6、watch、watchEffect的暂停和恢复

Vue 3.5 中对watch进行的扩展,在一些场景中,可能需要暂停 watch 或者 watchEffect 中的回调,满足业务条件后再恢复执行。Vue 3.5 为此提供了 pause() 和 resume() 方法

const { stop, pause, resume } = watchEffect(() => {
  // 观察逻辑
});

// 暂时暂停观察者
pause();

// 恢复观察者
resume();

// 停止观察
stop();

示例,使用 pause() 和 resume()

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="toggleWatch">Toggle Watch</button>
  </div>
</template>
 
<script setup>
import { ref, watch } from 'vue';
 
const count = ref(0);
let watcher; // 用于存储 watcher 的引用
 
function setupWatcher() {
  watcher = watch(count, (newVal, oldVal) => {
    console.log(`Count changed from ${oldVal} to ${newVal}`);
  });
}
 
function toggleWatch() {
  if (watcher.active) {
    watcher.pause(); // 暂停 watcher
    console.log('Watcher paused');
  } else {
    watcher.resume(); // 恢复 watcher
    console.log('Watcher resumed');
  }
}
 
setupWatcher(); // 初始化 watcher
</script>

这使得对观察者的控制更加灵活,满足复杂的业务需求

7、 Teleport 和过渡(Teleport & Transition)增强,延迟挂载

内置 <Teleport> 组件的一个已知限制是,传送目标元素必须在传送组件挂载时存在。这阻止了用户将内容传送到 Vue 渲染的其他元素中

在 Vue 3.5 中,引入了一个 defer 属性,使得 <Teleport> 组件可以延迟挂载目标元素,这对于控制复杂 UI 布局非常有帮助

// defer 的使用
<Teleport defer target="#container">...</Teleport>
<div id="container"></div>

8、响应式 Props 解构

直接解构 defineProps 保留响应性,无需 toRefs

const { count = 0 } = defineProps(['count']); // 自动追踪 count 变化

你不知道的Javascript(上卷) | 第三章难点与细节解读(函数作用域和块作用域)

作为《你不知道的Javascript》忠实读者,多次拜读该著作,本专栏用来分享我对该书的解读,适合希望深入了解这本书的读者阅读 电子书下载网址:zh.101-c.online

第三章——函数作用域和块作用域

一、全局命名空间与模块管理的区别

先看原文表述

image.png

image.png 我相信,对于全局命名空间大家都可以理解,所以我们把重点放在第二部分模块管理,其中有一些名词我们需要理解一下,什么是模块管理器,什么又是依赖管理器

模块管理器(Module Manager)

模块管理器是一种用于组织和管理代码模块的工具或系统。它的主要功能包括:

  1. 模块定义与封装:允许开发者将代码划分为独立的模块,每个模块具有自己的作用域,避免全局作用域的污染。
  2. 模块加载:负责按需加载模块,可能是同步或异步的(如动态加载)。
  3. 依赖管理:确保模块之间的依赖关系被正确处理,按正确的顺序加载和执行模块。
  4. 作用域隔离:通过机制(如闭包、函数作用域或专用命名空间)确保模块的标识符(变量、函数等)不会泄漏到全局作用域,从而避免命名冲突。

常见的模块管理器:

  • CommonJS:主要用于Node.js,通过requiremodule.exports实现模块化。
  • AMD(Asynchronous Module Definition):如RequireJS,支持异步加载模块。
  • ES Modules(ESM):JavaScript的原生模块系统,通过importexport语法实现。
例子:ES Modules(ESM)
// math.js(模块定义)
export function add(a, b) {
  return a + b;
}

// app.js(模块导入)
import { add } from './math.js';
console.log(add(2, 3)); // 5

依赖管理器(Dependency Manager)

依赖管理器是一种专门用于管理代码库或模块之间依赖关系的工具。它的核心功能是:

  1. 依赖解析:根据模块的依赖声明(如“模块A需要模块B”),自动解析并获取所有依赖项。
  2. 依赖安装:通常与包管理器(如npm、yarn)结合,从远程仓库下载依赖的库。
  3. 版本控制:确保依赖的版本兼容性,避免冲突(如通过package.json或锁文件)。
  4. 依赖注入:将依赖的模块显式导入到特定作用域中,而非全局作用域。
例子1:npm(Node.js 包管理器)
// package.json(声明依赖)
{
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

依赖管理器与模块管理器的关系:

  • 依赖管理器更侧重于解决“如何获取和组合依赖”,而模块管理器更侧重于“如何定义和隔离模块”。
  • 许多工具兼具两者功能(如Webpack、Rollup等打包工具,既管理模块也处理依赖)。

相信大家不难看出全局命名空间与模块管理的区别,全局命名空间的所有变量归根结底都在全局作用域之中,但是模块管理通过export,import等手段避免了变量在全局作用域中的暴露,只在使用的地方暴露,并不污染全局作用域,下面我们举一些例子来说明

🌰 示例分析

情况1:全局命名空间(冲突)

假设有两个文件:

lib.js
// 定义了一个全局变量 `utils`
var utils = { version: 1 };
app.js
// 不小心也定义了一个全局变量 `utils`
var utils = { version: 2 };
index.html
<script src="lib.js"></script>
<script src="app.js"></script>

运行结果

  • lib.js 先执行,window.utils = { version: 1 }
  • app.js 后执行,window.utils = { version: 2 },覆盖了 lib.js 的 utils
  • 最终 utils 的值是 { version: 2 }lib.js 的变量被覆盖!

情况2:模块管理(无冲突)
lib.js(使用 ES Modules)
// 导出 utils,不会污染全局
export const utils = { version: 1 };
app.js(使用 ES Modules)
// 导入 lib.js 的 utils
import { utils } from './lib.js';

// 即使这里再定义一个 utils,也不会冲突
const utils = { version: 2 }; // 这是 app.js 的局部变量
console.log(utils); // { version: 2 }(优先用局部变量)
console.log(window.utils); // undefined(全局没有被污染)

运行结果

  • lib.js 的 utils 只在模块内部作用域。
  • app.js 的 utils 是局部变量,不会影响 lib.js 的 utils
  • 两个 utils 互不干扰!

二、函数声明与函数表达式的区别

先看原文表述

image.png 毫无疑问,原文给出了一种区分函数声明和函数表达式的方法,但是其中有一句耐人寻味的话,(不仅仅是一行代码,而是整个声明中的位置) 这句话的核心意思是:
判断 function 是否为“第一个词”,不能只看它是不是在一行的开头,而要看它在整个语句的语法结构中的位置。因为代码的换行和格式化(如括号、缩进)可能会干扰直观判断。

下面我举个例子

假设你看到这样的代码:

function foo() {}
  • 这里 function 是一行的第一个词,显然是 函数声明

但如果代码被换行或括号包裹:

const bar = function
foo() {};
  • 如果只看第一行 const bar = function,可能误以为 function 是第一个词。
  • 但实际上,function 属于赋值语句的一部分,所以 整体是一个函数表达式

知道如何判断何为函数声明与函数表达式之后我们聚焦于函数声明与函数表达式的区别这里主要提两点

1. 作用域提升(Hoisting)

(1)函数声明
  • 会被 JavaScript 引擎提升(Hoisting),可以在声明前调用。

    console.log(greet("Alice")); // 正常执行(输出 "Hello, Alice!")
    function greet(name) {
      return `Hello, ${name}!`;
    }
    
(2)函数表达式
  • 不会被提升,必须在定义后才能调用。

    console.log(greet("Alice")); // 报错!greet is not a function
    const greet = function(name) {
      return `Hello, ${name}!`;
    };
    

2. 函数名的作用域

(1)函数声明
  • 函数名在 当前作用域 中直接可用。

    function foo() {}
    console.log(foo); // 正常输出函数
    
(2)函数表达式
  • 匿名函数表达式:没有函数名,只能通过变量调用。

    const bar = function() {};
    console.log(bar); // 正常输出函数
    
  • 具名函数表达式:函数名仅在函数内部可用,外部无法访问。

    const baz = function qux() {};
    console.log(baz); // 正常输出函数
    console.log(qux); // 报错!qux is not defined
    

三、IIFE倒置代码运行顺序的意义

先看原文表述

image.png

相信看完这段表述之后,不止我在内的读者会思考一个问题,IIFE倒置代码运行顺序的意义在哪里,就简单的倒置吗?我相信意义不止于此

IIFE 倒置代码运行顺序的实际意义

这种 IIFE(立即执行函数表达式)参数化模式,确实可以通过“倒置代码顺序”改变执行逻辑。它的核心意义在于 控制作用域、依赖注入和模块化封装,尤其在 UMD(Universal Module Definition) 这类兼容性模块化方案中非常有用。


1. 代码示例分析

var a = 2;

(function IIFE(def) {
  def(window); // 将 window 作为参数传递给 def
})(function def(global) {
  var a = 3;
  console.log(a); // 3(当前作用域的 a)
  console.log(global.a); // 2(外部的 a)
});
执行顺序
  1. 声明全局变量 a = 2

  2. 定义并立即执行 IIFE,传入函数 def 作为参数。

  3. IIFE 内部调用 def(window),将 window 作为 global 参数传递。

  4. 执行 def 函数:

    • 定义局部变量 a = 3(不影响外部的 a)。
    • 打印 a(局部)和 global.a(全局)。

2. 这种模式的实际意义

(1)作用域隔离
  • 避免污染全局作用域
    通过 IIFE 包裹代码,所有内部变量(如 a = 3)不会泄漏到全局,避免命名冲突。
  • UMD 模块化的核心思想
    在兼容 CommonJS、AMD 和浏览器的模块化方案中,这种模式能确保模块代码在独立作用域中运行。
(2)依赖注入(Dependency Injection)
  • 显式传递全局对象
    window 作为参数 global 传入,明确依赖关系,而非隐式依赖全局变量。
    优点

    • 代码更易测试(可模拟 global 参数)。
    • 兼容非浏览器环境(如 Node.js 的 global 对象)。
(3)代码逻辑解耦
  • 分离“定义”和“执行”
    函数 def 的定义和执行被拆分为两部分,便于动态调整依赖或配置。
    应用场景

    • 模块化库(如 jQuery、Lodash)的兼容性封装。
    • 需要运行时注入依赖的插件系统。
(4)更清晰的执行流程
  • 逻辑分层
    将核心逻辑(def)放在代码后半部分,而初始化逻辑(IIFE)在前,符合“先配置后执行”的设计模式。

3. 对比传统 IIFE

传统 IIFE(直接执行)
var a = 2;
(function() {
  var a = 3;
  console.log(a); // 3
  console.log(window.a); // 2
})();

缺点

  • 隐式依赖 window,难以测试或替换依赖。
  • 逻辑全部嵌套在 IIFE 内,可读性较差。
参数化 IIFE(倒置顺序)
var a = 2;
(function IIFE(def) {
  def(window); // 显式传递依赖
})(function def(global) {
  var a = 3;
  console.log(a); // 3
  console.log(global.a); // 2
});

优点

  • 依赖通过参数传递,灵活性高。
  • 模块化代码更易维护和扩展。

4. 实际应用场景

(1)UMD 模块化方案
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD 环境(如 RequireJS)
    define(['jquery'], factory);
  } else if (typeof exports === 'object') {
    // CommonJS 环境(如 Node.js)
    module.exports = factory(require('jquery'));
  } else {
    // 浏览器全局环境
    root.MyLib = factory(root.jQuery);
  }
}(typeof self !== 'undefined' ? self : this, function ($) {
  // 模块核心逻辑
  return {
    version: '1.0.0'
  };
}));

说明

  • 通过 IIFE 的倒置顺序,动态适配不同模块化规范。
  • factory 函数是核心逻辑,root 和依赖(如 jQuery)通过参数注入。
(2)依赖配置化
// 配置依赖后执行
(function init(appConfig) {
  appConfig.start();
})({
  debug: true,
  start: function() {
    console.log('App started with debug:', this.debug);
  }
});

5. 总结

为什么这样设计?
  1. 明确依赖关系:通过参数传递依赖,而非隐式依赖全局变量。
  2. 作用域安全:避免变量污染,兼容多种环境。
  3. 代码分层:分离“初始化”和“逻辑”,提升可读性。
何时使用?
  • 需要兼容多种模块化规范的库(如 UMD)。
  • 需要注入依赖或动态配置的插件系统。
  • 希望代码更易测试和维护的场景。

这种模式虽然略显冗长,但在复杂项目中能显著提升代码的健壮性和可维护性

四、块级作用域的垃圾回收机制

先来看原文的表述

image.pngimage.png

这里有两个问题需要思考

  • 为何someReallyBigData不会被回收
  • 为何块级作用域可以让其回收

为何someReallyBigData不会被回收

原因:闭包的作用域链保留

在 JavaScript 中,闭包(Closure) 会让函数持有其定义时所处的作用域链。即使函数没有显式使用某些变量,引擎仍然会保留整个作用域链,因为:

  1. 词法作用域规则:函数在定义时就确定了它能访问哪些变量。
  2. 保守的垃圾回收机制:引擎无法静态分析函数是否会动态访问变量(例如通过 eval 或 with),因此会默认保留所有可能的引用。

代码示例分析

var someReallyBigData = { /* 大对象 */ }; // (1) 定义在全局作用域
process(someReallyBigData); // (2) 使用后理论上可回收

var btn = document.getElementById("my_button");
btn.addEventListener("click", function click() { // (3) 闭包形成
  console.log("button clicked");
});
  • 问题click 回调函数的作用域链包含全局作用域(即 someReallyBigData)。
  • 结果:只要 click 函数存在(按钮未移除),someReallyBigData 就被视为“可达”,无法被回收。

1. 为什么 someReallyBigData 不会被回收?

原因:闭包的作用域链保留

在 JavaScript 中,闭包(Closure) 会让函数持有其定义时所处的作用域链。即使函数没有显式使用某些变量,引擎仍然会保留整个作用域链,因为:

  1. 词法作用域规则:函数在定义时就确定了它能访问哪些变量。
  2. 保守的垃圾回收机制:引擎无法静态分析函数是否会动态访问变量(例如通过 eval 或 with),因此会默认保留所有可能的引用。

代码示例分析

javascript
复制
var someReallyBigData = { /* 大对象 */ }; // (1) 定义在全局作用域
process(someReallyBigData); // (2) 使用后理论上可回收

var btn = document.getElementById("my_button");
btn.addEventListener("click", function click() { // (3) 闭包形成
  console.log("button clicked");
});
  • 问题click 回调函数的作用域链包含全局作用域(即 someReallyBigData)。
  • 结果:只要 click 函数存在(按钮未移除),someReallyBigData 就被视为“可达”,无法被回收。

2. 为什么块级作用域可以让 someReallyBigData 被回收?

原因:作用域隔离

通过 let/const + {} 块级作用域,可以显式限制变量的生命周期

  1. 块级作用域内的变量:仅在 {} 内有效,外部无法访问。
  2. 闭包的作用域链不包含块级变量:如果回调函数定义在块外,就不会引用块内的变量。

优化后的代码

function process(data) { /* ... */ }

{ // (1) 使用块级作用域
  let someReallyBigData = { /* 大对象 */ }; // 仅在块内有效
  process(someReallyBigData);
} // (2) 块结束,someReallyBigData 超出作用域

var btn = document.getElementById("my_button");
btn.addEventListener("click", function click() { // (3) 闭包不引用块级变量
  console.log("button clicked");
});
  • 关键点

    • someReallyBigData 被限制在 {} 内,click 函数未在块内定义,因此不引用它。
    • 块执行完毕后,someReallyBigData 不再被任何闭包引用,可安全回收。

五、结语

通过《你不知道的JavaScript》第三章的深度解析,我们系统性地梳理了函数作用域与块作用域的核心机制。从全局命名空间的污染问题到模块管理的封装艺术,从函数声明的提升特性到IIFE的依赖注入模式,再到块作用域对垃圾回收的精准控制——这些看似独立的特性,实则构成了JavaScript作用域体系的完整拼图。理解这些概念不仅能帮助我们写出更健壮的代码,更能培养对语言设计哲学的深刻认知。

正如Kyle Simpson在书中强调的,作用域的本质是代码可维护性与执行效率的平衡艺术。无论是模块化的依赖管理,还是闭包的内存优化,都需要开发者既掌握底层原理,又能灵活运用设计模式。建议读者将本文作为引子,结合书中示例代码进行实践,真正让这些知识融入日常开发思维。当你能清晰地预见每一行代码对作用域链和内存管理的影响时,就离JavaScript高手更近了一步

独立开发者工具 • 半月刊 第 010 期

alt text

Indie Tools - 专注于分享独立开发:出海、精选、最新、最实用的工具和资源

独立开发者必备网站:www.indietools.work 订阅半月刊:点击订阅

如果本文能给你提供启发和帮助,感谢各位小伙伴动动小手指,一键三连 (点赞评论转发),给我一些支持和鼓励,谢谢。

Ahrefs SEO optimization guidelin

总结

Ahrefs 的 7 篇 SEO 入门文章是一系列针对初学者的搜索引擎优化指南,涵盖关键词研究、内容优化、链接建设等核心内容,帮助用户快速掌握 SEO 基础知识并提升网站排名。

Ahrefs SEO optimization guidelin

链接: https://www.indietools.work/product/fd832c85-9994-421c-8b0b-8b1207142202

特性

  1. 全面的 SEO 基础知识:涵盖关键词研究、内容优化、链接建设等核心主题,适合初学者系统学习 SEO 的基础概念和实操技巧。
  2. 实操性强:每篇文章提供具体的操作步骤和工具推荐(如 Ahrefs 工具的使用),帮助用户快速上手并应用所学知识。
  3. 结构化学习路径:7 篇文章按逻辑顺序排列,从基础到进阶,帮助用户循序渐进地掌握 SEO 的核心技能。

使用场景

  1. SEO 初学者入门:适合没有任何 SEO 经验的用户,通过系列文章快速了解 SEO 的基本原理和操作方法。
  2. 网站管理员优化内容:帮助网站管理员学习如何优化网站内容和结构,提升搜索引擎排名和流量。

缺点

  1. 内容可能过于基础,不适合已经具备 SEO 经验的用户。
  2. 依赖 Ahrefs 工具,部分内容可能需要付费工具支持才能完全实践。

usermaven

总结

usermaven 是一款简单但功能强大的网站和产品分析工具,专注于帮助企业和开发者通过数据驱动决策。它提供直观的用户行为追踪、转化分析和产品优化功能,适合中小企业和初创公司快速获取关键洞察,无需复杂配置即可实现高效数据分析。

usermaven

链接: https://www.indietools.work/product/eee3ce95-aa5c-48b8-bf06-2fc03cef1850

特性

  1. 用户行为追踪与分析:提供详细的用户行为路径分析,帮助理解用户如何与网站或产品互动,识别关键转化点和流失环节。
  2. 无需代码的集成方式:支持无代码嵌入,只需简单配置即可开始收集数据,降低技术门槛,适合非技术人员使用。
  3. 实时数据仪表盘:内置可视化仪表盘,实时展示关键指标如访问量、留存率和转化率,便于快速监控和决策。

使用场景

  1. 初创公司产品迭代:初创团队可通过 usermaven 快速获取用户反馈和行为数据,优化产品功能和用户体验。
  2. 电商网站转化优化:电商企业可利用其分析工具追踪用户购物路径,优化页面设计和促销策略以提高转化率。

缺点

  1. 功能相对基础,可能无法满足大型企业或复杂分析需求。
  2. 免费版功能有限,高级功能需订阅付费计划。

beamanalytics

总结

beamanalytics 是一款专注于隐私保护的网站分析工具,提供比 Google Analytics 更友好的免费套餐,适合中小企业和个人开发者。它以简洁的界面、实时数据和无需 Cookie 的特点,成为注重用户隐私的网站首选分析工具。

beamanalytics

链接: https://www.indietools.work/product/a974daf6-3ac2-418c-bac3-27448771371c

特性

  1. 隐私优先设计:无需使用 Cookie 或收集个人数据,完全符合 GDPR 和 CCPA 等隐私法规,避免法律风险。
  2. 实时数据分析:提供即时访问数据统计,包括页面浏览量、用户来源和停留时间,帮助快速优化网站。
  3. 免费套餐慷慨:免费计划包含每月 10,000 次页面浏览量,远超多数竞品,适合初创团队或个人博客。

使用场景

  1. 中小企业和个人网站:无需复杂配置即可获得核心数据,低成本满足基本分析需求。
  2. 注重隐私的开发者:适用于不希望追踪用户行为或面临合规压力的项目,提供干净的数据解决方案。

缺点

  1. 功能相对基础,缺乏高级细分或漏斗分析等深度功能。
  2. 品牌知名度较低,可能影响团队协作或客户信任度。

featurebase

总结

Featurebase 是一款集成反馈收集、客户支持与产品更新发布的一体化工具,旨在帮助团队高效管理用户反馈、降低支持成本,并通过透明化的产品路线图提升用户参与度。适用于初创公司至中大型企业,简化产品迭代流程。

featurebase

链接: https://www.indietools.work/product/d0bad3de-1749-4e7a-9e0f-eb17a3eb5a94

特性

  1. 集中式反馈管理:用户可通过投票、评论等方式提交反馈,团队可优先处理高需求功能,减少重复性支持请求。
  2. 可视化产品路线图:动态展示产品开发计划,增强用户信任感,同时减少“何时发布”类咨询。
  3. 自动化分类与分析:利用 AI 自动归类反馈建议,生成可操作洞察,节省人工整理时间。
  4. 多平台集成:支持与 Slack、Intercom 等工具连接,确保反馈实时同步至现有工作流。

使用场景

  1. 敏捷产品团队:快速验证用户需求优先级,避免开发资源浪费在低价值功能上。
  2. 客户支持繁重的企业:通过公开路线图减少重复询问,将支持工单量降低 30%-50%。
  3. SaaS 公司:增强用户参与感,通过反馈闭环提升订阅留存率与口碑传播。

缺点

  1. 学习曲线较陡:高级功能(如自定义工作流)需时间配置,对非技术用户不友好。
  2. 定价偏高:中小企业可能难以承担高阶版本的订阅费用。
  3. 移动端功能受限:部分管理操作需在桌面端完成。

chatwoot

总结

Chatwoot 是一款开源客户沟通平台,帮助企业通过多种渠道(如电子邮件、网站实时聊天、社交媒体等)统一管理客户互动,提升服务效率与客户满意度,适用于中小企业和客服团队。

chatwoot

链接: https://www.indietools.work/product/d8ae0b56-4b3a-4bb9-87bf-a2a0510f5bc1

特性

  1. 多渠道集成:支持电子邮件、网站聊天、Facebook、Twitter、WhatsApp 等主流渠道,实现客户沟通的统一管理,避免信息分散。
  2. 自动化与机器人:提供自动化工作流和聊天机器人功能,可快速响应常见问题,减轻人工客服负担。
  3. 开源与可定制:作为开源软件,用户可自由部署和定制功能,满足特定业务需求,适合技术团队深度开发。

使用场景

  1. 电商客服:通过集中管理订单咨询、退货请求等,提升响应速度,优化客户购物体验。
  2. 中小企业支持:低成本搭建专业客服系统,整合社交媒体与邮件,高效处理客户问题。

缺点

  1. 学习成本较高:开源版本需要一定的技术能力部署和配置。
  2. 高级功能需付费:部分高级功能(如高级分析)仅在付费版本中提供。

supahub

总结

Supahub 是一个专注于客户反馈管理和产品更新的平台,帮助团队集中收集用户意见、优先处理功能请求,并高效发布产品动态。它简化了反馈流程,提升团队与用户的互动效率,适合产品经理和开发团队使用。

supahub

链接: https://www.indietools.work/product/649f4557-d325-4b75-837a-18d4aafdd4fb

特性

  1. 集中反馈管理:提供一个统一平台收集、分类和跟踪用户反馈,避免信息分散在不同渠道,提升处理效率。
  2. 功能优先级排序:通过投票或分析工具帮助团队识别用户最需求的功能,确保资源投入在关键改进上。
  3. 产品更新发布:支持团队发布产品动态和更新日志,保持用户知情并增强透明度,提升用户参与感。

使用场景

  1. 产品迭代优化:适合需要持续收集用户反馈并快速迭代产品的团队,确保开发方向符合市场需求。
  2. 客户支持与沟通:帮助客服团队高效整理用户建议,并与开发团队协作解决问题,提升客户满意度。

缺点

  1. 可能缺乏高级数据分析功能,难以深度挖掘反馈数据。
  2. 免费版功能有限,部分高级特性需付费解锁。

featureos

总结

featureos 是一款专为产品团队设计的产品生命周期操作系统,整合了产品开发所需的工具和用户反馈分析功能,帮助团队高效打造优秀产品。通过集中管理反馈、任务和数据分析,提升团队协作效率,缩短产品迭代周期。

featureos

链接: https://www.indietools.work/product/6d9fb3bf-fd3a-4807-b0d9-b9060d137840

特性

  1. 集成化产品管理工具:提供从用户反馈收集到任务分配的全流程管理,减少工具切换带来的效率损耗,确保团队协作无缝衔接。
  2. 实时用户反馈分析:通过自动化工具快速收集并分析用户反馈,识别关键需求,帮助团队优先处理高价值功能。
  3. 可视化数据看板:内置数据分析功能,直观展示产品性能指标和用户行为数据,辅助团队做出数据驱动的决策。

使用场景

  1. 敏捷产品开发团队:适合需要快速迭代产品的团队,通过集中管理反馈和任务,加速开发周期并提高响应速度。
  2. 用户反馈驱动的优化:适用于重视用户意见的产品团队,通过高效分析反馈数据,持续优化产品体验。

缺点

  1. 学习曲线较陡:功能集成度高,新用户可能需要时间熟悉操作流程。
  2. 定价可能较高:针对中小团队,高级功能的订阅费用可能成为负担。

canny

总结

Canny.io 是一个专注于收集和管理客户反馈的平台,帮助企业和团队通过用户意见优化产品。它提供直观的看板和自动化工具,简化反馈处理流程,使产品决策更高效、数据驱动。适合初创公司到大型企业使用。

canny

链接: https://www.indietools.work/product/ba0e8d12-31ad-4a03-8403-dbdc63ef916d

特性

  1. 集中化反馈管理:用户可以通过看板提交和投票反馈,团队能快速识别高优先级需求,减少信息碎片化问题。
  2. 自动化分类与追踪:利用标签和状态标记自动分类反馈,支持与开发工具(如 Jira、Slack)集成,提升团队协作效率。
  3. 数据驱动的决策支持:提供分析仪表盘,展示用户需求趋势和投票数据,帮助团队基于真实反馈制定产品路线图。

使用场景

  1. 产品迭代优化:团队可通过收集用户反馈,快速定位产品痛点,优先开发高需求功能,提升用户满意度。
  2. 客户需求优先级排序:企业利用投票和分类功能,筛选出最具价值的客户建议,避免资源浪费在低优先级需求上。

缺点

  1. 学习曲线较陡:部分高级功能(如自定义工作流)需要时间熟悉,可能影响初期使用效率。
  2. 定价较高:对于小型团队或初创公司,高级功能订阅费用可能超出预算。

往期回顾

不规范 JSON 怎么办?三种修复思路+代码实现

最近在处理一批第三方返回的数据时,我遇到了一个令人头疼的问题:部分 JSON 字符串是破损的,导致在用 JSON.parse() 解析时直接抛出异常。比如有些缺少引号,有些多了逗号,甚至还有嵌套结构不完整的情况。手动修复显然不现实,于是我开始寻找是否有自动修复 JSON 的方案或工具,比如能否在解析前做一些预处理,或者使用更智能的解析库来容错。那面对这些结构混乱的 JSON 数据,我们到底该怎么安全地修复和解析它们呢?

在JavaScript中,正常来说我们都是使用JSON.parse方法将JSON字符串转换为JavaScript对象。以下是一个简单的示例:

// JSON字符串
const jsonString = '{"name":"Alice","age":25,"city":"Wonderland"}';

// 使用JSON.parse将JSON字符串转换为JavaScript对象
const jsonObject = JSON.parse(jsonString);

// 打印转换后的对象
console.log(jsonObject);

// 访问对象的属性
console.log('Name:', jsonObject.name);
console.log('Age:', jsonObject.age);
console.log('City:', jsonObject.city);

这世界就像一个草台班子,总有些奇奇怪怪的内容,有时候,拿到一段别人写的 JSON,就像接手一锅没煮熟、还撒了一地的乱粥

  • 少了引号
  • 多了逗号
  • 布尔值写成大写True 而不是 true
  • 结尾少了大括号……

你拿这种东西直接 JSON.parse()?对不起,JS 直接报错扔锅!💥 那怎么办?当然是「修粥大法」上场!

我们需要从一个老旧的系统获取数据,而这个系统就有意思了,他的接口返回的是一个纯字符串,如果是纯正json字符串,我转成对象处理也可以,结果他这个返回字符串在转json的时候还报错了,但有时候会出现各种奇怪的错误——单引号代替了双引号,属性名没有加引号,或者多了一些奇怪的逗号或注释……

原生JavaScript方法:手动调锅,自己加水加火加料

我首先尝试了最简单的方法——使用try/catch和一些基本的字符串替换:

function tryParseJSON(jsonString) {
  try {
    return JSON.parse(jsonString);
  } catch (e) {
    // 尝试简单修复:去除可能的多余字符
    const cleaned = jsonString.replace(/[^\x20-\x7E]/g, '').trim();
    try {
      return JSON.parse(cleaned);
    } catch (e2) {
      console.error("无法解析JSON:", e2);
      return null;
    }
  }
}

这种方法对于简单的问题有时能奏效,但对于更复杂的错误,比如混合了单引号和双引号的情况,就力不从心了。我接着尝试了一个更针对性的修复方法:

function fixQuotes(jsonString) {
  // 将单引号替换为双引号(简单情况)
  return jsonString.replace(/'/g, '"');
}

但问题是,这样的替换太过简单粗暴,如果字符串内容中本身包含单引号,就会导致新的解析错误。难道真的没有更好的解决方案吗?


在搜索了大量资料并尝试了各种方法后,我找到了两个非常有用的库:jsonrepair 和 JSON5。这两个库在处理破损的JSON字符串时各有所长。

jsonrepair: 智能修锅机器人!

jsonrepair 是我发现的一个专门用于修复损坏JSON的强大工具。它能处理多种常见的JSON格式错误:

npm install jsonrepair

示例代码

const { jsonrepair } = require('jsonrepair');

const damagedJson = "{'name': 'John', age: 30}";
const fixedJson = jsonrepair(damagedJson);
const obj = JSON.parse(fixedJson);

console.log(obj); // { name: 'John', age: 30 }

这个库能修复什么类型的问题呢?实际上相当广泛:

  • 引号问题:将单引号替换为双引号,为未加引号的属性名和字符串值添加引号
  • 多余字符:移除注释、尾部逗号和JSON前后的无关字符
  • 结构问题:修复缺少闭合的括号或引号
  • 编码问题:修复转义字符和Unicode字符

但是,即使是这样强大的工具也有其局限性。对于严重损坏的JSON(如完全无效的语法、深层嵌套的结构错误),jsonrepair 也无能为力。这时候,我们需要另一种解决方案。

JSON5:允许你“不按食谱炒菜”的宽松厨房

在某些情况下,我们需要的不是修复现有的JSON字符串,而是使用一个更宽松的解析器。这就是 JSON5 派上用场的时候:

npm install json5

示例代码

const JSON5 = require('json5');

const looseJson = `{
  // 这是一个注释
  name: 'Tom',
  age: 30,
  traits: ["smart", 'funny'],
}`;

const obj = JSON5.parse(looseJson);
console.log(obj);

JSON5 允许:

  • 单引号字符串
  • 未加引号的属性名
  • 尾随逗号
  • 注释(单行和多行)
  • 更灵活的数字格式、十六进制、科学计数法等数字格式
  • 多行字符串

但它也不是万能的。JSON5 不能处理完全不相关的格式或严重结构损坏的数据。

组合使用多种工具

在经过多次尝试后,我发现最有效的方法是根据情况组合使用不同的工具。下面是我最终采用的解决方案:

function parseJson(jsonString) {
  // 第一步:尝试标准JSON解析
  try {
    return JSON.parse(jsonString);
  } catch (e) {
    console.log("标准JSON解析失败,尝试修复...");
    
    // 第二步:尝试使用jsonrepair修复
    try {
      const { jsonrepair } = require('jsonrepair');
      const fixedJson = jsonrepair(jsonString);
      return JSON.parse(fixedJson);
    } catch (e2) {
      console.log("修复失败,尝试使用JSON5解析...");
      
      // 第三步:尝试使用JSON5解析
      try {
        const JSON5 = require('json5');
        return JSON5.parse(jsonString);
      } catch (e3) {
        // 最后:如果所有方法都失败,返回错误信息
        console.error("所有解析方法都失败了:", e3);
        throw new Error("无法解析JSON数据");
      }
    }
  }
}

虽然有了 jsonrepair 和 JSON5 这样的“补救工具”,但它们永远只是权宜之计。我们应该追本溯源,确保数据的生成过程符合 JSON 标准。如果你需要处理来自用户或第三方的非标准数据,建议在入口阶段就做格式校验和错误报告。

HTML中是后如何解决移动设备上显示问题的?

<meta name="viewport" content="width=device-width, initial-scale=1.0"> 是 HTML 中用于控制网页在 移动设备 上显示效果的关键标签。它的作用是确保网页能够适配不同尺寸的移动设备屏幕,避免出现文字过小、需要手动缩放等问题。


一、核心参数解释

  1. width=device-width

    • 作用:将网页的布局视口宽度设置为当前设备的屏幕宽度(即“设备宽度”)。
    • 默认行为:如果没有设置 width=device-width,移动设备会默认以桌面浏览器的宽度(通常是 980px)渲染页面,然后将整个页面缩小到屏幕内。这会导致内容过小,用户需要手动缩放才能正常阅读。
    • 示例
      • 如果设备屏幕宽度是 750px(如 iPhone SE),设置 width=device-width 后,网页的布局视口宽度会被设置为 750px,CSS 中的 width: 100% 会基于这个宽度计算。
  2. initial-scale=1.0

    • 作用:设置页面初始加载时的缩放比例为 1.0(即 100% 缩放)。
    • 默认行为:如果不设置 initial-scale,移动设备可能会根据默认的布局视口宽度自动缩放页面(如将 980px 的页面缩小到屏幕内),导致内容显示异常。
    • 示例
      • 如果不设置 initial-scale=1.0,页面可能被浏览器自动缩放,导致文字过小或布局错乱。

二、完整标签的作用

  • 适配移动端屏幕:确保网页宽度与设备屏幕宽度一致,避免内容被压缩或需要手动缩放。
  • 响应式设计基础:为媒体查询(@media)提供准确的尺寸基准,方便实现不同设备的布局适配。
  • 提升用户体验:避免用户需要手动缩放才能查看内容,减少横向滚动条的出现。

三、未设置 viewport 时的现象

假设未添加 <meta name="viewport"> 标签:

<!DOCTYPE html>
<html>
  <head>
    <!-- 未设置 viewport -->
  </head>
  <body>
    <div style="width: 300px">内容</div>
  </body>
</html>

在 375px 宽的手机上,这个 div 实际显示可能只有约 115px(因为浏览器默认以 980px 的桌面宽度渲染,然后缩小到 375px 屏幕内)。用户需要手动缩放才能看清内容。


四、扩展参数(可选)

可以添加更多参数来进一步控制视口行为:

<meta name="viewport" content="width=device-width, 
                               initial-scale=1.0, 
                               maximum-scale=1.0, 
                               user-scalable=no">
  • maximum-scale:限制用户可以缩放的最大比例(如 maximum-scale=1.0 禁止放大)。
  • user-scalable:控制用户是否可以手动缩放页面(user-scalable=no 禁止缩放,yes 允许缩放,默认为 yes)。

五、PC 端和移动端的差异

  • PC 端initial-scale 通常会被浏览器忽略,因为 PC 浏览器有完善的缩放功能。
  • 移动端initial-scale 会生效,直接影响页面的初始显示效果。

六、实际应用场景

  1. 移动优先的响应式设计:确保网页在移动端优先适配,再通过媒体查询适配桌面端。
  2. 避免横向滚动条:通过设置 width=device-width,防止页面内容超出屏幕宽度。
  3. 图片和文本清晰显示:确保图片和文字不会因缩放而模糊。

七、总结

  • 必加标签:在所有移动端网页中,建议始终添加 <meta name="viewport" content="width=device-width, initial-scale=1.0">
  • 关键作用:它是响应式网页设计的基础,能显著提升移动端用户体验。
  • 兼容性:现代移动浏览器(如 Safari、Chrome)均支持此标签,且能兼容大部分旧版设备。

React + Vite 项目使用 PM2 部署测试环境全流程指南(附踩坑指南)

🛠 React + Vite 项目使用 PM2 部署测试环境全流程指南(附 Docker 对比)

🧭 背景介绍

在之前的测试环境部署中,我们使用的是 Docker 容器,部署流程如下:

  1. 手动登录服务器;
  2. 拉取 Git 仓库最新代码;
  3. 执行 runtest.sh 脚本进行打包并构建镜像;
  4. 停止旧容器,启动新容器;

虽然这种方式有较强的隔离性与可控性,但对轻量前端项目来说显得流程稍重。因此本次我们采用更灵活的 PM2 + Node 方式来部署前端打包产物,减少部署链路复杂度。

🧱 环境准备(服务器)

1. 安装 Node.js(推荐使用 nvm)

// 注意根据服务器类型替换命令
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 18
nvm use 18

2. 安装 PM2

npm install -g pm2

🧳 本地打包流程

1. 打测试包

npm run build:test

这会生成一个 dist/ 文件夹,里面包含打包好的静态资源。

2. 将 dist 上传到你的服务器目录下

scp -r dist/ xx@xx.xx.xx.xx:/home/xx/web-server/

或者写个脚本upload.sh以后每次更新执行脚本即可

npm run build:test && scp -r dist/ xx@xx.xx.xx.xx:/home/xx/web-server/

🧩 服务器配置

1.新建 app.js(PM2 启动入口)

const express = require('express');
const path = require('path');
const app = express();

// 配置跨域(可选)
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://your-frontend-domain');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  next();
});

// 静态资源配置
app.use('/', express.static(path.join(__dirname, 'dist')));

// 动态路由配置
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});

const port = yourport;
app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

🚀 使用 PM2 启动服务

pm2 start app.js --name web-server
pm2 logs web-server
pm2 startup
pm2 save

📎 初次部署踩坑记录

❌ 路由 404 报错

项目中使用了动态路由,需要在appjs中需添加 app.get('*') 映射到 index.html

❌ CORS 报错

res.header('Access-Control-Allow-Origin', 'http://your-frontend-domain');
res.header('Access-Control-Allow-Credentials', 'true');

❌ # TypeError: Missing parameter name at 2: git.new/pathToRegex…

控制台报错[pathToRegexpError],页面始终加载不出来,以为是路由配置的问题,就去检索了所有的routerkey配置发现没啥问题,google了一下发现github上有类似问题的做法是给express降级,express5版本可能会遇见这个问题,Downgrade to 4.21.2之后就没有问题了,可以正常加载 (参考链接:github.com/expressjs/e…

🔁 与 Docker 部署方式对比

项目 Docker 部署 PM2 + Node 部署
操作流程 拉取代码 + 构建镜像 本地打包上传
资源占用 高,包含镜像+容器 低,仅 Node 环境
运行隔离性 高,运行于独立容器 中,运行在主机上
可维护性 容器可快速替换 静态文件需手动更新
热更新 重启容器 pm2 reload web-server
适用场景 后端服务、隔离环境 静态站点测试环境

✅ 小结

PM2 部署更轻便、灵活,适合测试环境;Docker 更适合正式环境部署,保障一致性与隔离性。

🧠 面试官:你知道 Vue3 的响应式为什么比 Vue2 更强吗?

“听说你研究过 Vue 的响应式?”

“是的,Vue2 的 Object.defineProperty 和 Vue3 的 Proxy 都了解过。”

面试官点点头:“那你觉得 Vue3 的响应式,为什么比 Vue2 更强?”

我沉思了一下,脑子里浮现出 definePropertyProxyEffecttracktrigger……

但忽然意识到:我虽然知道底层实现细节,但宏观上,Vue3 响应式到底‘强’在哪?我好像说不太清……

今天,我们就不卷源码,来从设计思想实际开发体验出发,看看 Vue3 的响应式系统,到底强在哪里?


🏛️ Vue2 的局限

Vue2 的响应式系统用的是 Object.defineProperty(),这也是大家老生常谈的一个方法了。

Object.defineProperty(obj, 'key', {
  get() { ... },
  set(val) { ... }
})

它能做到监听对象属性的访问与变更,但问题也不少:

  • 新增/删除属性无法触发视图更新,必须通过 Vue.setthis.$set 处理。
  • 数组变更监听不完全,比如直接用索引赋值或修改 length,Vue2 是无法感知的。
  • 只能追踪到对象最外层,嵌套对象需要递归劫持,每一层对象都得遍历并手动设置 getter/setter,性能瓶颈明显。
  • 只支持对象,不能劫持 Map、Set、WeakMap 等结构

总体而言:

Vue2 响应式的核心是属性劫持,它解决了早期前端双向绑定的刚需,但随着需求复杂化,这套系统逐渐显得力不从心。


🚀 Vue3 的响应式系统做了什么?

Vue3 基于 ES6 的 Proxy 重写了整个响应式系统,解决了 Vue2 中 Object.defineProperty 只能监听对象最外层的问题。

const proxy = new Proxy(target, {
  get(target, key, receiver) {
    // track
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    // trigger
    return Reflect.set(target, key, value, receiver)
  }
})

相比 Vue2,它做了:

  • get 读取数据时,执行 track 追踪数据。
  • set 改变数据时,通过 trigger 触发数据相应逻辑执行。
  • 使用 Reflect 更好的配合 proxy 去使用。

它不是 劫持属性,而是 代理对象 本身。

Vue3 响应式甚至能追踪 Map.set()Set.add(),这是 Vue2 完全做不到的。

这套新系统相比 Vue2,有哪些本质优势?


🧩 Vue2 vs Vue3 响应式设计对比

维度 Vue2 Vue3
实现方式 Object.defineProperty Proxy
监听粒度 每个属性 整个对象
动态属性支持 不支持(需 Vue.set 原生支持
数组监听 部分操作监听不到 全部操作可监听
数据结构支持 仅普通对象 Map、Set、WeakMap 等
嵌套监听 递归遍历所有层级 懒监听,性能更优
响应式解耦 响应逻辑耦合在组件内部 Effect 模块集中处理

可以发现,Vue3 并不仅仅是换了个底层 API,同时还系统性重构了整个响应式模型,它可扩展、性能更好,并且为 Composition API 奠定了基础。


🧠 Vue3 响应式更强,背后的三个关键改变

Vue3 的响应式厉害,不只是因为用了 Proxy。更重要的是它相比 Vue2 做出的逻辑变化。

你可以把它理解为三大升级:

从“逐个监听”到“一次打包监听”

在 Vue2 里,它是怎么监听数据变化的?

是一层层地“劫持”每个属性,每个字段都得手动处理。对象一多、嵌套一深,性能就跟不上了。

而 Vue3 用了 Proxy,只要一层代理就能搞定整棵对象树:

  • 不用再递归监听每个属性。
  • 动态新增属性也能自动追踪。
  • 还能轻松做“只读”、“浅层监听”等拓展配置。

更高效、更灵活,也更容易维护。

从“到处埋点”到“集中处理”

Vue2 的依赖追踪,埋得很分散:

组件里读数据的时候,会收集依赖;更新数据时,再触发更新。这种逻辑藏得比较深,不好统一管理。

Vue3 就清爽多了:

effect(() => {
  console.log(state.count) // 自动追踪依赖
})

数据一变化,自动触发 effect 里的逻辑。追踪、触发都交给了 track / trigger 两个模块去做。

响应式逻辑 集中起来、模块化了,更好调试、更易扩展。

为组合式 API 打好底层地基

Vue3 响应式系统的设计初衷之一,就是为组合式 API 打好地基。

过去 Vue2 是 选项式 API:你要在组件里写 datamethodscomputed,功能分散在不同配置里。

而 Vue3 推出的组合式 API,比如:

const count = ref(0)
const doubled = computed(() => count.value * 2)
watchEffect(() => {
  console.log(doubled.value)
})

是不是更灵活、更自由?

这些写法能成立,就是因为 Vue3 的响应式系统更强大了

  • refreactive 可以包裹任何值,变成响应式对象。
  • watchEffect 能自动追踪你访问的值。
  • 不用依赖组件生命周期,逻辑可以拆分封装,更容易复用。

关于响应式的具体实现可以参考我之前的 Vue 源码解析 系列文章 😊。


💡 我为什么更喜欢 Vue3 的响应式?

  • 代码更直观,不用担心数据更新视图不渲染的问题。
  • 调试友好,依赖追踪逻辑清晰。
  • 组合式 API 配合响应式更优雅。
  • TypeScript 支持更完善。

一个明显的例子:

const state = reactive({ count: 0 })

watchEffect(() => {
  console.log(state.count) // 自动追踪
})

更新 state.count++,就会触发 watchEffect

无需手动声明依赖、无需担心深层嵌套属性监听不到。


❓你真的理解 Proxy 比 defineProperty 更强吗?

很多人一提起 Vue3 响应式为什么强,第一反应是:

“因为 Proxy 可以监听整个对象啊!”

确实没错,但如果你只停留在这个理解层面,那你对 Proxy 的认识还远远不够深。

有一个更容易被忽视、却非常关键的底层细节是 —— Vue3 为什么还要配合使用 Reflect

来看下面这段 Proxy 的代码实现:

const proxy = new Proxy(target, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  }
})

你可能会问:为啥不直接写 target[key] 呢?

get(target, key) {
  return target[key] // 这样不行吗?
}

还真的不行

为什么必须用 Reflect?

Reflect 是 ES6 新增的内置对象,一般与 Proxy 组合使用。使用 Reflect.get 有几个好处:

  • 保证 this 指向正确(特别是在 class 中访问 getter)
  • 更准确地返回属性值,避免默认行为丢失
  • 和 Proxy 内部行为保持一致

来看个示例对比:

class A {
  get value() {
    return this === proxy ? 'proxy called' : 'raw called'
  }
}

const a = new A()
const proxy = new Proxy(a, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
    // return target[key] 会让 this 变成 target,而不是 proxy
  }
})

console.log(proxy.value) // 'proxy called'

你会发现,如果不使用 Reflect.get,那么 this 会指向原始对象,而不是代理对象,导致 getter、setter 行为不一致。

所以说,Proxy 是 Vue3 响应式的关键核心,但是配合 Reflect 使用才会更安全稳定。


🧯 Vue3 响应式也有一些注意点

当然,Vue3 的响应式系统虽然强大,但也不是无敌的。在实际使用中,仍然有一些需要特别注意的坑和限制。

无法 Polyfill

Vue3 使用了 Proxy,这是 ES6 才引入的高级特性,而它 无法被降级或 Polyfill

意味着如果你想支持 IE11 或更老的浏览器 —— Vue3 本身就无法运行。这也是 Vue 团队决定 Vue3 完全放弃 IE 支持的一个重要原因。

响应式过度追踪问题

在使用 watchEffect 时,Vue 会自动收集你访问的所有响应式数据作为依赖。

这听起来方便好用,但如果你在一个 effect 里访问了太多变量,每一个变量变化都会触发整个副作用函数重新执行

watchEffect(() => {
  console.log(obj.a, obj.b, obj.c) // 都是依赖
})

假如这些变量变化频繁,或者你在其中做了复杂逻辑操作,性能就可能拉胯。

解决办法是更精准使用 watch,或者拆分多个 watchEffect,控制依赖粒度。

写过 React 的小伙伴可以说是颇有经验了 🙃。

响应式丢失陷阱:结构赋值后就不是响应式了!

这是很多人会踩的坑,尤其是习惯了结构赋值的写法:

const state = reactive({ count: 0 })

const { count } = state // 断开响应式

console.log(count) // 这只是普通数值,变化不会响应式更新

为啥?

因为响应式代理的是对象本身,结构赋值相当于把值 复制 出来了,脱离了响应式系统。

正确做法:

  • toRefs() 保留响应式:

    const { count } = toRefs(state)
    
  • 或者干脆用 storeToRefs()(如果你在 Pinia 中)

  • 或者不要结构赋值,直接用 state.count


🎯 小总结

Vue3 的响应式系统,远不止于 把 defineProperty 换成了 Proxy 这么简单。

真正让它 更强 的地方是:

  • 响应式能力更强:对象、数组、Map、Set 统统支持;
  • 代码逻辑层级清晰可控:track/trigger/effect 模块解耦;
  • 支持更多编程范式:支持组合式、TS、服务端渲染等场景;
  • 源码阅读体验感更好:语义清晰,调试方便,扩展性强。

所以,如果你在面试被问到:

“Vue3 的响应式到底强在哪?”

你就该开始自信吟唱了!😄

如果你觉得这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 让我知道你在看~ 我会持续更新 前端打怪笔记系列文章,👉 记得关注我,不错过每一篇干货更新!❤️

🚀 超越JSON.parse:解锁JavaScript超速反序列化的秘密武器

=================================================================================================================================================================================================================================================================================================================================================================================================

(传统JSON解析 vs 高效反序列化技术对比)

🔍 为什么我们需要超越JSON.parse?

**JSON.parse()** 是JavaScript中最常用的反序列化方法,但在处理大数据量高频解析场景时,它存在明显瓶颈:

  • 性能问题:解析10MB JSON需要200-400ms(主流浏览器)

  • 内存压力:生成庞大的中间对象

  • 功能局限:无法直接处理特殊数据类型(如Date、BigInt)

    // 传统方式 const data = JSON.parse(largeJsonString); // 阻塞主线程!

⚡ 高性能替代方案全景图

(现代JavaScript反序列化技术生态)

1. 流式JSON解析(Streaming JSON Parser)

适用场景:网络传输中的大JSON分块处理

import { JSONParser } from '@streamparser/json';

const parser = new JSONParser();
parser.onValue = (value) => {
  // 逐值处理,不占用完整内存
};

fetch('/large-data.json')
  .then(res => res.body.pipeThrough(parser));

优势
✅ 内存占用恒定(O(1))
✅ 支持提前终止解析

2. 结构化克隆(Structured Clone)

适用场景:Web Worker通信、IndexedDB存储

// 比JSON.parse更快且支持更多类型
const original = { date: new Date(), bigInt: 123n };
const cloned = structuredClone(original); 

console.log(cloned.date instanceof Date); // true!

支持类型对比

类型

JSON.parse

structuredClone

Date

Map/Set

RegExp

BigInt

3. 二进制序列化(MessagePack/BSON)

适用场景:WebSocket通信、游戏数据同步

import { encode, decode } from '@msgpack/msgpack';

// 比JSON小30%-50%,解析快2-3倍
const binary = encode({ foo: 'bar', count: 42 });
const data = decode(binary);

性能基准(1MB数据):

格式

编码时间

解码时间

大小

JSON

12ms

28ms

1.0MB

MessagePack

8ms

10ms

0.6MB

🛠️ 进阶技巧:定制化解析器

1. 按需解析(Lazy Parsing)

// 使用JSON Pointer延迟解析
const lazyParse = (jsonStr) => {
  const raw = JSON.parse(jsonStr);
  return new Proxy(raw, {
    get(target, prop) {
      if (prop === 'heavyField') {
        return JSON.parse(target[prop]); // 使用时才解析
      }
      return target[prop];
    }
  });
};

2. WebAssembly加速

// 使用Rust编写的WASM解析器
import init, { parse_json } from './pkg/json_parser.js';

await init();
const data = parse_json(largeJsonStr); // 比原生快3-5倍

📊 性能优化决策树

🚀 未来趋势:JSON解析的下一站

JSON.parse异步化

// 提案中的异步JSON.parse
const data = await JSON.parseAsync(bigJson);
  1. SIMD加速

    (Chrome V8引擎正在实验SIMD优化)

  2. GPU加速解析
    WebGL/WebGPU通用计算应用于JSON解析

💡 实战建议

  1. 关键路径:首屏数据优先使用结构化克隆

监控指标

// 测量解析时间
const start = performance.now();
JSON.parse(data);
console.log(`耗时: ${performance.now() - start}ms`);
  1. 混合策略:对大JSON的关键字段单独提取

"在速度至上的时代,解析性能就是用户体验的第一道门槛" —— 《高性能JavaScript》作者Nicholas Zakas

📢 互动话题
你在项目中遇到过JSON解析的性能瓶颈吗?用了哪些优化方案?欢迎分享! 💬

(如果觉得有收获,请点赞/收藏支持!❤️)

什么年代了还在手动执行npm?

背景

在前端多包项目开发中,每次打开项目都需要手动启动多个服务是一件繁琐的事情。通过VSCode的tasks任务系统,我们可以实现打开项目文件夹时自动执行多个npm命令,提高开发效率和流畅度。

功能实现

1、在项目根目录创建.vscode/tasks.json文件(官方文档),然后根据自身项目实际情况配置任务,配置完重启即可。本示例项目采用的是lerna架构存在多个分包,我希望一打开项目就跑多个项目的环境,因此需要按顺序执行多个子任务,具体的配置如下:

{
  "version": "2.0.0",
    "tasks": [
    {
      "label": "Open Bash Terminal",
      "type": "shell",
      "command": "echo '打开Bash终端,可以输入命令' && bash",
      "options": {
        "cwd": "${workspaceFolder}",
        "shell": {
          "executable": "C:\Program Files\Git\bin\bash.exe", //我希望用bash打开,所以要指定bash的安装目录
          "args": ["-c"]
        }
      },
      "presentation": {
        "reveal": "always",
        "panel": "shared",
        "group": "devServer"
      },
      "isBackground": true,
      "problemMatcher": []
    },
    {
      "label": "Run broadband-flow debug",
      "type": "shell",
      "command": "npm run debug",
      "options": {
        "cwd": "${workspaceFolder}/packages/broadband-flow",
        "shell": {
          "executable": "C:\Program Files\Git\bin\bash.exe",
          "args": ["-c"]
        }
      },
      "presentation": {
        "reveal": "always",
        "panel": "shared",
        "group": "devServer"
      },
      "isBackground": true,
      "problemMatcher": []
    },
    {
      "label": "Run business debug",
      "type": "shell",
      "command": "sleep 2 &&  npm run debug",
      "options": {
        "cwd": "${workspaceFolder}/packages/business",
        "shell": {
          "executable": "C:\Program Files\Git\bin\bash.exe",
          "args": ["-c"]
        }
      },
      "presentation": {
        "reveal": "always",
        "panel": "shared",
        "group": "devServer"
      },
      "isBackground": true,
      "problemMatcher": []
    },
    {
      "label": "Run group-customer debug",
      "type": "shell",
      "command": "sleep 4 && npm run debug",
      "options": {
        "cwd": "${workspaceFolder}/packages/group-customer",
        "shell": {
          "executable": "C:\Program Files\Git\bin\bash.exe",
          "args": ["-c"]
        }
      },
      "presentation": {
        "reveal": "always",
        "panel": "shared",
        "group": "devServer"
      },
      "isBackground": true,
      "problemMatcher": []
    },
    {
      "label": "Run mtapp-cli proxy",
      "type": "shell",
      "command": "mtapp-cli proxy",
      "options": {
        "cwd": "${workspaceFolder}",
        "shell": {
          "executable": "C:\Program Files\Git\bin\bash.exe",
          "args": ["-c"]
        }
      },
      "presentation": {
        "reveal": "always",
        "panel": "new"
      },
      "isBackground": true,
      "problemMatcher": []
    },
    {
      "label": "Run All Packages",
      "dependsOn": [
        "Run mtapp-cli proxy",
        "Open Bash Terminal",
        "Run broadband-flow debug",
        "Run business debug",
        "Run group-customer debug"
      ],
      "dependsOrder": "parallel",
      "runOptions": {
        "runOn": "folderOpen"
      } // 打开文件夹时自动运行
    }
  ]
}

效果如下

2、如果你不想要一打开项目就立即执行任务,你还可以通过设置快捷键来执行。点击 文件 -> 首选项 -> 键盘快捷键下右上角的(打开键盘快捷键json文件)的按钮,将以下代码添加进json文件保存重启编辑器即可,快捷键可以设置自己熟悉的。

{
  "key": "alt+numpad1",
    "command": "workbench.action.tasks.runTask",
    "args": { "task": "Run All Packages" }
},
{
  "key": "alt+numpad2",
    "command": "workbench.action.tasks.runTask",
    "args": { "task": "Run broadband-flow debug" }
},
{
  "key": "alt+numpad3",
    "command": "workbench.action.tasks.runTask",
    "args": { "task": "Run business debug" }
},
{
  "key": "alt+numpad4",
    "command": "workbench.action.tasks.runTask",
    "args": { "task": "Run group-customer debug" }
},
{
  "key": "alt+numpad5",
    "command": "workbench.action.tasks.runTask",
    "args": { "task": "Run mtapp-cli proxy" }
},
{
  "key": "alt+numpad6",
    "command": "workbench.action.tasks.runTask",
    "args": { "task": "Open Bash Terminal" }
}

结语

tasks可以自动化地执行各种脚本,是个不错的功能,各位小伙伴可以用起来~

HarmonyOS 路由跳转示例

HarmonyOS 路由跳转指南

1. 基础路由跳转(Router)

1.1 使用 router.pushUrl 进行跳转

最基本的页面跳转方式,支持传递参数:

router.pushUrl({
  url: 'pages/router/RouterOnePage',
  params: { message: '普通跳转' }
})

1.2 跨包跳转,使用命名路由跳转

通过路由名称进行跳转:支持跳转到其他模块的页面:

router.pushNamedRoute({
  name: 'TestRouterPage',
  params: {
    data1: 'message',
    data2: {
      data3: [123, 456, 789]
    }
  }
})

1.3 返回上一页

Button('回到上一页')
  .onClick(() => {
    router.back()
  })

1.4 完整代码

1.4.1 主页面(RouterJumpPage)
@Entry
@Component
struct RouterJumpPage {
  @State message: string = 'Router跳转示例';

  build() {

    Column() {
      Text(this.message)
        .fontSize(36)
        .margin({ top: 20, left: 20 })
        .fontWeight(FontWeight.Bold)

      Button('跳转同模块页面RouterOnePage')
        .height(50)
        .margin({ top: 20, left: 20 })
        .onClick(() => {
          router.pushUrl({ url: 'pages/router/RouterOnePage' })
        })
      Button('跨包跳转到子模块页面')
        .height(50)
        .margin({ top: 20, left: 20 })
        .onClick(() => {
          try {
            router.pushNamedRoute({
              name: 'TestRouterPage',
              params: {
                data1: 'message',
                data2: {}
              }
            })
          } catch (err) {
            let message = (err as BusinessError).message
            let code = (err as BusinessError).code
            console.error(`pushNamedRoute failed, code is ${code}, message is ${message}`);
          }
        })
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Start)
    .justifyContent(FlexAlign.Start)
  }
}
1.4.2 同模块子页面(RouterOnePage)
@Entry
@Component
export struct NavigationOnePage {
  @State message: string = 'Navigation 普通跳转的二级界面';
  derivedStack: NavPathStack = new NavPathStack();
  curStringifyParam: string = "NA";

  build() {
    NavDestination() {
      Column() {
        Column() {
          Text('current page param info:')
            .margin(10)
            .fontSize(25)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Start)
          Text(this.curStringifyParam)
            .margin(20)
            .fontSize(20)
            .textAlign(TextAlign.Start)
        }.backgroundColor(Color.Pink)

        Button('回到上一页', { stateEffect: true, type: ButtonType.Capsule })
          .width('40%')
          .height(50)
          .margin(20)
          .onClick(() => {
            // 返回到上一页
            this.derivedStack.pop();
          })
      }
      .height('100%')
      .width('100%')
      .alignItems(HorizontalAlign.Start)
      .justifyContent(FlexAlign.Start)
    }
    .title(this.message)
    .onReady((context: NavDestinationContext) => {
      console.log('[derive-test] reached PageOne's onReady');
      // get derived stack from navdestinationContext
      this.derivedStack = context.pathStack as NavPathStack;
      // console.log('[derive-test] -- got derivedStack: ' + this.derivedStack.id);
      this.curStringifyParam = JSON.stringify(context.pathInfo.param);
      console.log('[derive-test] -- got param: ' + this.curStringifyParam);
    })
  }
}
1.4.3 跨包子页面(TestRouterPage)
import { router } from "@kit.ArkUI";

@Entry({ routeName: 'TestRouterPage' })
@Component
export struct TestRouterPage {
  @State message: string = '子模块的界面';

  build() {
    Column() {
      Text(this.message)
        .fontSize(36)
        .margin({ top: 20, left: 20 })
        .fontWeight(FontWeight.Bold)

      Button('回到首页', { stateEffect: true, type: ButtonType.Capsule })
        .width('40%')
        .height(50)
        .margin(20)
        .onClick(() => {
          router.back()
        })
    }.width('100%').height('100%')
    .alignItems(HorizontalAlign.Start)
    .justifyContent(FlexAlign.Start)
  }
}

2. Navigation 导航跳转

2.1 基础 Navigation 跳转

使用 Navigation 组件进行页面导航:

Navigation(this.derivedStack)
{
  Button('Navigation 普通跳转')
    .onClick(() => {
      this.derivedStack.pushPath({
        name: 'NavigationOnePage',
        param: new param('push NavigationOnePage when stack size: ' + this.derivedStack.size())
      });
    })
}

2.2 跨包 Navigation 跳转

支持跨包使用 Navigation 进行跳转:

this.derivedStack.pushPath({
  name: 'LoginPage',
  param: new param('push LoginPage when stack size: ' + this.derivedStack.size())
});

2.3 使用 Navigation 返回

Button('回到上一页')
  .onClick(() => {
    this.derivedStack.pop();
  })

2.4 完整示例

2.4.1 主页面(NavigationJumpPage)
@Entry
@Component
struct NavigationJumpPage{
  @State message: string = '使用NavDestination跳转';
  // 创建一个页面栈对象并传入Navigation
  derivedStack: NavPathStack = new NavPathStack();

  aboutToAppear():void {
    // this.derivedStack.setId('origin stack');
  }

  @Builder
  pageMap(name:string){
    if (name === 'NavigationOnePage') {
      NavigationOnePage();
    } else if (name === 'LoginPage') {
      TestNavigationPage();
    }
  }

  build(){
    Navigation(this.derivedStack){
      Column(){
        Button('Navigation 普通跳转')
          .height(50)
          .margin({ top: 20, left: 20 })
          .onClick(() => {
            this.derivedStack.pushPath({
              name: 'NavigationOnePage',
              param: new param('push NavigationOnePage when stack size: ' + this.derivedStack.size())
            });
          })
        Button('TestNavigationPage 跨包跳转')
          .height(50)
          .margin({ top: 20, left: 20 })
          .onClick(() => {
            this.derivedStack.pushPath({
              name: 'LoginPage',
              param: new param('push TestNavigationPage when stack size: ' + this.derivedStack.size())
            });
          })
      }
      .
      width('100%')
        .height('100%')
        .alignItems(HorizontalAlign.Start)
        .justifyContent(FlexAlign.Start)
    }
    .
    title(this.message)
      .navDestination(this.pageMap)
  }
}
2.4.2 同模块的子页面(NavigationOnePage)
@Entry
@Component
export struct NavigationOnePage{
  @State message: string = 'Navigation 普通跳转的二级界面';
  derivedStack: NavPathStack = new NavPathStack();
  curStringifyParam: string = "NA";

  build(){
    NavDestination(){
      Column(){
        Column(){
          Text('current page param info:')
            .margin(10)
            .fontSize(25)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Start)
          Text(this.curStringifyParam)
            .margin(20)
            .fontSize(20)
            .textAlign(TextAlign.Start)
        }
        .
        backgroundColor(Color.Pink)

        Button('回到上一页', { stateEffect: true, type: ButtonType.Capsule })
          .width('40%')
          .height(50)
          .margin(20)
          .onClick(() => {
            // 返回到上一页
            this.derivedStack.pop();
          })
      }
      .
      height('100%')
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .justifyContent(FlexAlign.Start)
    }
    .
    title(this.message)
      .onReady((context: NavDestinationContext) => {
        console.log('[derive-test] reached PageOne\'s onReady');
        // get derived stack from navdestinationContext
        this.derivedStack = context.pathStack as NavPathStack;
        // console.log('[derive-test] -- got derivedStack: ' + this.derivedStack.id);
        this.curStringifyParam = JSON.stringify(context.pathInfo.param);
        console.log('[derive-test] -- got param: ' + this.curStringifyParam);
      })
  }
}

2.4.3 跨包的子页面(TestNavigationPage)
@Entry
@Component
export struct TestNavigationPage{
  @State message: string = 'NavigationPage跨包跳转的二级界面';
  derivedStack: NavPathStack = new NavPathStack();
  curStringifyParam: string = "NA";
  build(){
    NavDestination(){
      Column(){
        Text('current page param info:')
          .margin(10)
          .fontSize(25)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Start)
        Text(this.curStringifyParam)
          .margin(20)
          .fontSize(20)
          .textAlign(TextAlign.Start)

        Button('回到上一页', { stateEffect: true, type: ButtonType.Capsule })
          .width('40%')
          .height(50)
          .margin(20)
          .onClick(() => {
            this.derivedStack.pop();
          })
      }
      .
      width('100%')
        .height('100%')
        .alignItems(HorizontalAlign.Start)
        .justifyContent(FlexAlign.Start)
    }.title(this.message)
     .onReady((context: NavDestinationContext) => {
        console.log('[derive-test] reached PageOne\'s onReady');
        // get derived stack from navdestinationContext
        this.derivedStack = context.pathStack as NavPathStack;
        // console.log('[derive-test] -- got derivedStack: ' + this.derivedStack.id);
        this.curStringifyParam = JSON.stringify(context.pathInfo.param);
        console.log('[derive-test] -- got param: ' + this.curStringifyParam);
      })
  }
}

3. 注意事项

  1. 路由跳转时需要注意处理异常情况
  2. 跨包跳转时需要确保目标页面已经注册
  3. Navigation 跳转需要正确配置 NavDestination
  4. 参数传递时注意类型安全
  5. 返回操作时注意页面栈的状态

面试官最爱问的 const 问题:‘不可变’到底是值不变还是地址不变?一篇搞懂

作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反复推敲,才终于理清其中的底层逻辑。今天就结合实际代码和学习笔记,和大家聊聊这个让新手又爱又恨的const


一、为什么需要const?从JS的"成长烦恼"说起

时间回到2015年之前,那时候JS开发者只能用var声明变量。这种"上古语法"有两个让人头疼的问题:

  1. 全局污染:用var声明的变量会默认挂在window对象上。想象一下,你在全局作用域声明了一个name变量,结果引入的第三方库也声明了同名变量——这种"变量打架"的情况在大型项目中简直是灾难。

  2. 变量提升的歧义var存在"变量提升"机制,比如这段代码:

    console.log(a); // 输出undefined,而不是报错
    var a = 10;
    

    代码的实际执行顺序是先声明a再赋值,但阅读时很容易产生"变量未声明就使用"的误解,尤其在复杂逻辑中非常影响代码可读性。

ES6(2015年发布)的出现,正是为了解决这些问题。作为ES6的第一个新特性,const(常量声明)和let(块级变量声明)的加入,让JS终于具备了现代编程语言的特性——这也标志着JS从"网页脚本语言"向"企业级开发语言"的转型。


二、const的基础规则:先记住这三个关键点

学习const,首先要明确它的核心规则:

  1. 必须初始化:声明时必须赋值,否则会报错:

    const age; // 报错:Missing initializer in const declaration
    
  2. 块级作用域:和let一样,const声明的变量只在所在的块级作用域({}内)有效。比如:

    if (true) {
      const name = '张三';
    }
    console.log(name); // 报错:name is not defined
    
  3. "不可变"的本质:这是const最容易误解的点——它限制的是变量绑定(即变量指向的内存地址不可变),而不是变量的值不可变。


三、简单类型vs复杂类型:const的"双标"行为

理解const的关键,是搞清楚JS中简单数据类型复杂数据类型的内存存储机制。

1. 简单数据类型:值不可变

JS的简单数据类型包括:NumberStringBooleanUndefinedNullSymbol(ES6新增)、BigInt(ES2020新增)。它们的特点是:直接存储在内存栈中

内存栈的空间小但读取快,像酒店的"小格子储物柜",每个变量对应一个独立的格子。用const声明简单类型时,相当于给这个"格子"上了锁:

const age = 18;
age = 19; // 报错:Assignment to constant variable

因为age指向的栈内存地址被锁定,无法修改存储的值。

2. 复杂数据类型:地址不可变,内容可变

复杂数据类型(如ObjectArrayFunction)的存储方式不同:变量在栈中存储堆内存地址,实际数据存放在内存堆中

内存堆像酒店的"大仓库",空间大但需要通过"地址牌"(栈中的引用)访问。const对复杂类型的限制是:栈中的地址牌不能换,但仓库里的东西可以改

看一段实际代码(来自我的学习示例):

<script>
  const friends = [
    { name: 'hh', home: '江西' },
    { name: 'xx', home: '河南' }
  ];

  // 可以向数组中添加元素(修改堆内存中的内容)
  friends.push({ name: 'zz', home: '湖南' });

  // 可以修改对象属性(同样是修改堆内存)
  friends[0].name = 'HH'; 

  // 但不能重新赋值(更换栈中的地址牌)
  friends = ['新数组']; // 报错:Assignment to constant variable
</script>

这段代码完美展示了const对复杂类型的特性:数组的堆内存地址被锁定,但堆内存中的具体内容(数组元素、对象属性)可以自由修改。

3. 为什么会有这种差异?

根本原因在于内存管理效率:

  • 简单类型体积小(通常8字节以内),直接存栈中能快速访问;
  • 复杂类型体积大(可能包含成百上千个属性),存堆中可以灵活扩展空间,栈中只存地址能节省空间。

const通过"锁定栈内存"的方式,既保证了简单类型的常量特性,又允许复杂类型在合理范围内修改内容——这种设计完美平衡了"数据安全"和"开发灵活性"。


四、从一段代码看const的"兄弟"let:块级作用域的重要性

提到const就不能不提let,它们都是ES6块级作用域的"践行者"。看一个经典的循环示例(来自我的测试代码):

<script>
  // 使用let声明循环变量
  for (let i = 0; i < 10; i++) {
    setTimeout(function () {
      console.log(i); // 依次输出0-9
    }, 1000);
  }
</script>

如果把let换成var,结果会变成输出10次10——因为var没有块级作用域,循环中的i共享同一个全局变量。而let为每次循环创建独立的块级作用域,setTimeout中的回调能正确捕获当前循环的i值。

<script>
//闭包也一样解决问题
for(var i=0;i<10;i++){
        (function(i){
            setTimeout(function(){
                console.log(i)
            },1000)
        })(i)
    }
</script>

闭包能够解决这个问题的原因在于它能够捕获并保留外部函数的变量状态,即使外部函数已经执行完毕。具体来说:

  1. 变量作用域
    for 循环中使用 var 声明的变量 i 是函数作用域的,而不是块级作用域。因此,i 的值在循环结束后会变成 10

  2. 闭包的作用
    通过立即执行函数(IIFE)创建一个闭包,每次循环时都会创建一个新的函数作用域,并将当前的 i 值传递给这个作用域。这样,setTimeout 回调函数中捕获的 i 值就是闭包创建时的值,而不是循环结束后的值。

  3. 代码执行过程

    • 每次循环时,立即执行函数会捕获当前的 i 值。
    • setTimeout 回调函数在 1 秒后执行时,会使用闭包中捕获的 i 值,而不是全局的 i 值。

因此,闭包通过保留每次循环时的 i 值,解决了 var 变量作用域带来的问题,确保 setTimeout 回调函数能够正确输出预期的值。

这种特性对大型应用至关重要:在React组件、Vue的v-for指令中,块级作用域能避免变量污染,让代码更可控。


五、常见误区:const的"不可变"是绝对的吗?

新手最容易犯的错误,是认为const声明的对象"完全不可变"。实际上:

  • 对于数组,可以push/pop/splice,但不能重新赋值为[]
  • 对于对象,可以修改属性值(obj.key = newVal),但不能重新赋值为{}
  • 对于函数,可以修改原型方法,但不能重新赋值为function() {}

如果需要彻底禁止修改对象内容,可以使用ES5的Object.freeze(): 冻结对象,使其不可修改。最高级别不可变性,禁止对象本身及任意直接属性的修改(但不影响它引用的其他对象)

const obj = Object.freeze({ name: '张三' });
obj.name = '李四'; // 赋值无效(非严格模式下静默失败,严格模式报错)

但要注意,Object.freeze()只能冻结对象的第一层属性,嵌套对象仍可修改——这属于更高级的"深度冻结"范畴了。

ps:“深度冻结”:在这个对象调用Object.freeze(),然后遍历它引用的所有对象并在这些对象上调用Object.freeze()。一定要小心无意冻结其他共享对象


六、总结:const的"进化"背后是JS的成长

varconst,看似只是一个关键字的变化,背后却是JS从"玩具语言"到"企业级语言"的蜕变:

  • 开发者友好:块级作用域、常量声明让代码更易读、更安全;
  • 内存管理优化:通过栈/堆分离设计,平衡了性能与灵活性;
  • 生态扩展:ES6之后,TS、React、Vue等工具链的崛起,让JS能驾驭更复杂的业务场景。

下次再遇到const的"善变"行为,不妨打开浏览器的开发者工具,在Memory面板里观察内存地址的变化——你会更深刻地理解:所谓"不可变",不过是JS在内存世界里玩的一场"地址保卫战"。

b04be3ec3168ebbfa6f88c2088384e12.jpg

《深入理解 JavaScript 变量声明、作用域与内存分配》

在当今的现代前端开发领域,JavaScript 无疑占据着举足轻重的地位。自 ES6 标准问世以来,JavaScript 的功能和特性得到了极大的丰富与,尤其是在变量声明和作用域管理方面,发生了显著的变革。本文将深入剖析 const、let 和 var 这三种变量声明方式的区别,以及它们在内存分配过程中所扮演角色。

一、const 与 let 的引入

在 ES5 时代,JavaScript 仅支持使用 var 来声明变量,这种单一的声明方式引发了一系列问题。,存在变量提升现象,即 var 声明的变量会被提升到当前作用域的顶部;其二,使用 var 声明的变量会被挂载到全局对象 window 上,从而导致全局变量污染问题。为了解决这些弊端,ES6 引入了 const 和 let 这两种新的变量声明方式。

1. const

const 主要用于声明常量,一旦声明,其值通常不可重新赋值。

  • 简单数据类型:对于像 Number、String 这类简单数据类型,使用 const 声明后,其值无法被改变。例如:
const num = 10;
// 以下代码会报错,因为不能对 const 声明的简单数据类型重新赋值
// num = 20; 
  • 复杂数据类型:当使用 const 声明 Object、Array 等复杂数据类型时,变量所指向的内存地址不可改变,但对象的属性或数组的元素是可以修改的。示例如下:
const obj = { name: 'John' };
obj.name = 'Jane'; // 可以修改对象的属性
// 以下代码会报错,因为不能改变 const 声明的变量指向的内存地址
// obj = { age: 25 }; 
2. let
  • 块级作用域:let 声明的变量具有块级作用域,仅在 {} 块内有效。这一特性避免了变量污染和意外修改的问题。
  • 暂时性死区:在使用 let 声明变量时,在声明之前访问该变量会抛出 ReferenceError,一现象被称为“暂时性死区”(Temporal Dead Zone, TDZ)。示例如下:
// 以下代码会抛出 ReferenceError,因为处于暂时性死区
// console.log(x); 
let x = 5;

二、内存分配

JavaScript 中的变量存储主要分为栈内存和堆内存,它们各自具有不同的特点和适用场景。

1. 栈内存

栈内存具有连续的空间,但空间相对较小,不过访问速度较快。简单数据类型的值会直接存储在栈内存中,这种存储方式使得对简单数据类型的访问非常高效,适合存储那些需要频繁操作的小数据。例如:

let a = 10; // 变量 a 的值 10 直接存储在栈内存中
2. 堆内存

堆内存的空间较大,但不连续,访问速度相对较慢。复杂数据类型的值会存储在堆内存中,而栈内存中则存储着指向堆内存的引用(指针)。这种存储方式使得堆内存能够容纳复杂的数据结构。示例如下:

let arr = [1, 2, 3]; // 数组 [1, 2, 3] 存储在堆内存中,变量 arr 存储的是指向堆内存的引用

三、var 的问题

在 ES5 中,使用 var 声明的变量会挂载到全局对象 上,这可能会导致全局变量污染和命名冲突的问题。而 ES6 引入的 let 和 const 通过块级作用域和暂时性死区的特性,有效地解决了这些问题。

四、块级作用域

块级作用域是 ES6 引入的一项重要特性,它使得变量仅在 {} 块内有效,避免了变量污染和意外修改。下面通过两个示例来对比 let 和 var 在块级作用域方面的差异。

1. 使用 let 的情况
for (let i = 0; i < 10; i++) {
    setTimeout(() => {
        console.log(i);
    }, 1000);
}

由于 let 声明的变量具有块级作用域,每次循环都会创建一个新的块级作用域,变量 i 的值只在该作用域下有效。因此,setTimeout 回调函数会保留 i 的所有值,最终结果是 1 秒后依次输出 0 - 9,每个数字输出间隔 1 秒。

2. 使用 var 的情况
for (var i = 0; i < 10; i++) {
    setTimeout(() => {
        console.log(i);
    }, 1000);
}

由于 var 不支持块级作用域,在整个函数作用域中,var 声明的变量 i 共享同一个作用域。在 setTimeout 回调函数中的所有 i 都指向同一个变量,而循环结束后 i 的值已经是 10,并没有像 let 声明的块级作用域那样被依次保留下来。所以,这段代码执行结果是 1 秒后,输出 10 个 10。

五、总结

ES6 的 const 和 let 通过块级作用域和暂时性死区,显著提升了 JavaScript 的变量管理能力。理解这些特性及其在内存分配中的作用,对于编写高效、可维护的代码至关重要。希望本文能帮助你更好地理解 JavaScript 的变量声明和作用域管理!

nest.js项目表字段规范及如何实现

先说结论

  • 数据库层面:推荐使用snake_case。
  • NestJS代码层面(Entity属性):推荐使用camelCase
  • ORM(TypeORM):会帮助您在两者之间进行映射。
  1. 在nest里直接声明camelCase格式的字段名,在数据库里存储snake_case格式的字段,ORM会帮我们自动映射到正确的字段名 image.png
  2. 但是在设置外键的时候,我们我们必须使用snake_case格式,否则数据库迁移的时候会创建一个同名的camelCase字段,此外我们还需要再次声明一下这个字段但是不要用@Column装饰器,这样就可以正常互相映射了 image.png
  3. 执行数据库迁移的时候需要使用SnakeNamingStrategy,用来根据实体的camelCase格式的字段名在数据库生成snake_case格式的字段 image.png

enum-plus 全面实战指南:让你的 TypeScript 枚举焕然一新

enum-plus 是一个增强型的枚举库,完全兼容原生的 TypeScript enum,并扩展了显示文本、本地化、UI 控件绑定、枚举项遍历等强大功能。这是一个轻量级、零依赖的 TypeScript 库,可与任何前端框架配合使用。

特性

  • 完全兼容原生 enum 行为:支持 numberstring 等多种数据类型。
  • 增强的枚举项:可自定义显示文本,支持本地化,方便与任何 i18n 库集成。
  • 丰富的 API:提供便捷的方法,如获取枚举项的显示文本、遍历枚举项、判断值是否存在等。
  • UI 集成:可与 Ant Design、ElementPlus、Material-UI 等 UI 库无缝集成,仅需一行代码即可生成下拉菜单、复选框等组件。
  • TypeScript 支持:提供全面的类型推断,增强开发体验。
  • 轻量级:仅 2KB+,无任何依赖。

安装

使用 npm 安装:

npm install enum-plus

使用 yarn 安装:

yarn add enum-plus

枚举初始化

enum-plus 提供多种方式初始化枚举,可根据具体需求选择最适合的方式。

1. 简单的 key-value 格式

直接映射键和值,类似于原生枚举格式。

import { Enum } from 'enum-plus';

const Week = Enum({
  Sunday: 0,
  Monday: 1,
} as const);

console.log(Week.Monday); // 输出: 1

注意:在 TypeScript 中使用 as const 类型断言,确保枚举值被视为字面量类型。如果使用 JavaScript,请移除 as const

2. 带字符串值的 key-value 格式

允许使用字符串作为值。

import { Enum } from 'enum-plus';

const Week = Enum({
  Sunday: 'Sun',
  Monday: 'Mon',
} as const);

console.log(Week.Monday); // 输出: 'Mon'

3. 标准格式(推荐)

包含 valuelabel,可为每个枚举项指定显示文本,适用于 UI 组件。

import { Enum } from 'enum-plus';

const Week = Enum({
  Sunday: { value: 0, label: '我爱星期天' },
  Monday: { value: 1, label: '我讨厌星期一' },
} as const);

console.log(Week.Sunday); // 输出: 0
console.log(Week.label(0)); // 输出: '我爱星期天'

4. 仅 label 格式

使用键作为值。

import { Enum } from 'enum-plus';

const Week = Enum({
  Sunday: { label: '我爱星期天' },
  Monday: { label: '我讨厌星期一' },
} as const);

console.log(Week.Sunday); // 输出: 'Sunday'
console.log(Week.label('Sunday')); // 输出: '我爱星期天'

5. 数组格式

适用于需要动态创建枚举的场景,例如从 API 数据生成枚举。

import { Enum } from 'enum-plus';

// 假设 getPetsData() 返回类似以下的数据
const petTypes = await getPetsData();
// [
//   { value: 1, key: 'dog', label: '狗' },
//   { value: 2, key: 'cat', label: '猫' },
//   { value: 3, key: 'rabbit', label: '兔子' }
// ];
const PetTypes = Enum(petTypes);

6. 原生枚举格式

可从原生枚举创建,利用原生枚举的自增行为。

import { Enum } from 'enum-plus';

enum Init {
  Sunday = 0,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
}
const Week = Enum(Init);

console.log(Week.Sunday); // 输出: 0
console.log(Week.Monday); // 输出: 1
console.log(Week.Saturday); // 输出: 6
console.log(Week.label('Sunday')); // 输出: 'Sunday'

API

enum-plus 提供丰富的 API,方便开发者操作和扩展枚举。

  • Enum.XXX:直接访问枚举值,类似于原生 enum

    console.log(Week.Sunday); // 输出: 0
    console.log(Week.Monday); // 输出: 1
    
  • items:返回所有枚举项的只读数组,结构符合 Ant Design 组件规范,可用于生成下拉菜单、复选框等 UI 控件。

  • keys:返回所有枚举项的键名数组。

  • label(keyOrValue?: string | number): string | undefined:根据值或键获取枚举项的显示文本,支持本地化。

    console.log(Week.label(1)); // 输出: 'Monday'
    console.log(Week.label('Monday')); // 输出: 'Monday'
    
  • key(value?: string | number): string | undefined:根据值获取枚举项的键名。

    console.log(Week.key(1)); // 输出: 'Monday'
    
  • has(keyOrValue?: string | number): boolean:判断某个枚举项(值或键)是否存在。

    console.log(Week.has(1)); // 输出: true
    console.log(Week.has('Sunday')); // 输出: true
    console.log(Week.has(9)); // 输出: false
    console.log(Week.has('Birthday')); // 输出: false
    
  • toSelect(config?: OptionsConfig): {value, label}[] :返回仅包含 labelvalue 字段的枚举项数组,可用于生成

功能测试完全指南:从理论到实践

引言:为什么功能测试如此重要?

在软件开发的生命周期中,功能测试是确保产品质量的关键环节。根据业界统计,修复生产环境中发现的缺陷成本是需求阶段发现缺陷的100倍。功能测试作为质量保障的第一道防线,能够有效降低项目风险,提升用户体验。

一、功能测试基础概念

1.1 什么是功能测试?

功能测试(Functional Testing)是一种黑盒测试方法,主要验证软件系统的功能是否符合需求规格说明书的要求。它关注的是"系统应该做什么",而不是"系统如何实现"。

1.2 功能测试的主要类型

测试类型 描述 适用场景
冒烟测试 验证基本功能是否可用 每日构建后
回归测试 确保修改没有引入新缺陷 每次代码变更后
端到端测试 验证完整业务流程 系统测试阶段
用户验收测试 验证是否符合用户需求 发布前

二、功能测试完整流程

2.1 测试准备阶段

  1. 需求分析

    • 阅读需求文档
    • 标记模糊需求并与产品经理确认
    • 示例:电商平台的"购物车"功能需求分析
  2. 测试计划制定

    • 确定测试范围
    • 评估工作量
    • 资源分配
# 测试计划示例

**项目名称**:电商平台V2.0  
**测试目标**:验证核心购物流程  
**测试范围**- 商品搜索
- 购物车管理
- 订单结算
- 支付流程

2.2 测试设计阶段

  1. 测试用例设计方法

    • 等价类划分法
    • 边界值分析法
    • 因果图法
    • 场景法
    • 错误推测法
  2. 测试数据准备

    • 生产数据脱敏
    • 自动化生成测试数据
    • 考虑边界条件数据

三、测试用例设计实战

3.1 测试用例模板

| 用例ID | 模块 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|-------|------|---------|---------|---------|------|
| TC001 | 登录 | 1. 输入正确用户名和密码<br>2. 点击登录按钮 | 跳转到首页 | - | - |

3.2 电商平台测试用例示例

场景:购物车功能测试

  1. 添加商品到购物车

    • 用例ID:TC_EC_001
    • 前置条件:用户已登录,商品有库存
    • 测试步骤:
      1. 浏览商品详情页
      2. 点击"加入购物车"按钮
    • 预期结果:
      • 商品成功添加到购物车
      • 购物车图标显示数量+1
      • 系统提示"添加成功"
  2. 购物车商品数量修改

    • 用例ID:TC_EC_002
    • 测试步骤:
      1. 进入购物车页面
      2. 将商品数量从1修改为5
      3. 点击"更新"按钮
    • 预期结果:
      • 商品数量更新为5
      • 小计金额正确计算

3.3 边界值分析案例

测试用户年龄输入框(允许范围18-99岁):

测试数据 预期结果
17 提示"年龄不足"
18 验证通过
50 验证通过
99 验证通过
100 提示"年龄超出范围"

四、测试执行与报告

4.1 测试执行策略

  1. 优先级排序

    • P0:核心业务流程(如支付)
    • P1:主要功能(如搜索)
    • P2:次要功能(如个人资料)
  2. 缺陷管理流程

    • 发现 → 记录 → 分配 → 修复 → 验证 → 关闭

4.2 缺陷报告示例

**缺陷标题**:购物车页面-修改商品数量为0时未正确校验  
**严重程度**:Major  
**优先级**:P1  
**重现步骤**1. 添加任意商品到购物车
2. 进入购物车页面
3. 将数量修改为0
4. 点击"更新"按钮  
**实际结果**:商品被删除  
**预期结果**:应提示"数量不能少于1"  
**环境**:Chrome 89/Win10  
**截图**:[附件]

五、自动化功能测试入门

5.1 主流自动化测试工具对比

工具 语言 特点 适用场景
Selenium 多语言 浏览器自动化 Web UI测试
Cypress JavaScript 全栈测试框架 现代Web应用
Appium 多语言 移动端测试 iOS/Android应用
Postman JavaScript API测试 接口自动化

5.2 Selenium示例(Python)

from selenium import webdriver

def test_login():
    driver = webdriver.Chrome()
    driver.get("https://example.com/login")
    
    # 输入用户名密码
    driver.find_element("id", "username").send_keys("testuser")
    driver.find_element("id", "password").send_keys("password123")
    
    # 点击登录按钮
    driver.find_element("xpath", "//button[text()='登录']").click()
    
    # 验证登录成功
    assert "欢迎页" in driver.title
    driver.quit()

六、功能测试进阶技巧

6.1 提高测试覆盖率的方法

  1. 需求覆盖矩阵:确保每个需求都有对应测试用例
  2. 代码覆盖分析:使用工具如JaCoCo、Istanbul
  3. 探索性测试:基于经验的创造性测试

6.2 常见功能测试陷阱与规避

  1. 过度依赖UI自动化

    • 问题:UI变更导致大量用例失败
    • 解决方案:分层测试(UI+API+单元)
  2. 测试数据污染

    • 问题:测试数据互相影响
    • 解决方案:每次测试前初始化环境

七、现代功能测试趋势

  1. AI在测试中的应用

    • 自动生成测试用例
    • 视觉回归测试
    • 智能缺陷预测
  2. 持续测试

    • CI/CD流水线集成
    • 自动化测试即代码
  3. 无代码测试工具

    • Katalon
    • TestComplete

结语:优秀功能测试工程师的素养

  1. 需求理解能力:比开发更懂业务
  2. 批判性思维:善于发现边缘情况
  3. 自动化意识:能自动化的不手工
  4. 沟通技巧:清晰表达缺陷和风险

资源推荐

  • 书籍:《Google软件测试之道》
  • 工具:Selenium官方文档
  • 社区:掘金"测试"专栏、TesterHome

互动问题

  1. 你在功能测试中遇到的最大挑战是什么?
  2. 如何看待"测试工程师会被AI取代"的观点?

欢迎在评论区分享你的测试经验和见解!

【CodeBuddy】三分钟开发一个实用小功能之:弹跳球物理实验

探索CodeBuddy AI编程的无限魅力

在当今数字化飞速发展的时代,编程已然成为推动科技进步的核心力量。而随着人工智能技术的不断演进,AI编程工具如雨后春笋般涌现,其中CodeBuddy凭借其卓越的性能和强大的功能,成为了众多开发者眼中的璀璨明星。下面,就让我们一同走进CodeBuddy的世界,领略其AI编程的独特魅力。


以下是实际操作中的开发界面与最终呈现效果(文末附完整代码):


高效与精准的完美结合

CodeBuddy展现出了令人惊叹的编程效率。以往,开发者在创建一个完整的项目时,需要花费大量的时间进行代码编写、调试和优化。就拿这个弹跳球物理小游戏项目来说,从搭建HTML页面结构,到设计CSS样式,再到实现JavaScript的复杂交互逻辑,每一个环节都可能充满挑战和变数。然而,CodeBuddy能够快速理解开发者的需求,在短时间内生成高质量的代码。它精准地把握每个细节,无论是页面布局的合理性,还是物理效果的模拟,都能达到近乎完美的程度。这不仅大大缩短了开发周期,还让开发者能够将更多的精力投入到创新和功能拓展上。

智能的逻辑构建能力

在编程过程中,逻辑构建是最为关键的部分之一。CodeBuddy具备强大的智能逻辑构建能力,能够深入理解项目的需求和目标,并运用先进的算法和模型进行合理的逻辑设计。在弹跳球物理小游戏中,涉及到球体的运动轨迹、碰撞检测、重力模拟等复杂的物理逻辑。CodeBuddy能够清晰地梳理这些逻辑关系,将其转化为可行的代码实现。它考虑到了各种可能出现的情况,如球体之间的碰撞角度、碰撞后的速度变化等,并通过精确的计算和模拟,让游戏中的物理效果更加真实和流畅。这种智能的逻辑构建能力,让开发者无需为复杂的算法和逻辑而烦恼,轻松实现自己的创意。

丰富的代码生成多样性

CodeBuddy的代码生成具有丰富的多样性,能够根据不同的需求和场景生成合适的代码。在HTML页面的搭建中,它可以根据项目的风格和定位,生成简洁明了或功能丰富的页面结构;在CSS样式设计方面,它能够运用各种布局和特效技巧,打造出美观、舒适的用户界面;在JavaScript逻辑实现上,它可以根据不同的交互需求,采用不同的编程模式和算法,实现多样化的功能。这种多样性使得开发者能够根据自己的喜好和项目的特点,灵活选择合适的代码实现方式,为项目赋予独特的个性。

持续学习与进化

CodeBuddy并非一成不变的工具,它具有持续学习和进化的能力。随着编程技术的不断发展和更新,CodeBuddy能够不断学习新的知识和技能,提升自己的编程水平。它会分析大量的优秀代码案例,总结经验教训,不断优化自己的代码生成策略。同时,它还能根据用户的反馈和使用情况,不断改进自己的功能和性能。这种持续学习和进化的能力,让CodeBuddy始终保持在编程技术的前沿,为开发者提供更加优质、高效的编程服务。

降低编程门槛,激发创新活力

对于初学者来说,编程往往是一件充满挑战和困难的事情。复杂的语法规则、繁琐的逻辑结构常常让他们望而却步。而CodeBuddy的出现,大大降低了编程的门槛。它以简单易懂的方式生成代码,让初学者能够快速上手,实现自己的编程想法。同时,CodeBuddy的强大功能也为有经验的开发者提供了更多的创新空间。他们可以借助CodeBuddy的智能辅助,尝试新的编程思路和方法,创造出更加优秀的项目。这种降低门槛和激发创新活力的特点,让更多的人能够参与到编程的世界中来,推动编程技术的普及和发展。

附:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>弹跳球物理小游戏</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <canvas id="gameCanvas"></canvas>
    </div>
    <script src="game.js"></script>
</body>
</html>

style.css

body {
    margin: 0;
    padding: 0;
    overflow: hidden; /* 禁止滚动 */
    background: linear-gradient(135deg, #1e5799 0%, #2989d8 50%, #207cca 51%, #7db9e8 100%);
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    font-family: Arial, sans-serif;
}

.container {
    position: relative;
}

#gameCanvas {
    background-color: rgba(255, 255, 255, 0.1);
    box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
    border-radius: 8px;
    display: block;
    width: 800px;
    height: 600px;
}

script.js

// 初始化Canvas
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

// 设置Canvas实际大小
canvas.width = 800;
canvas.height = 600;

// 球体数组
const balls = [];
const colors = ['#FF5252', '#FF4081', '#E040FB', '#7C4DFF', '#536DFE', '#448AFF', '#40C4FF', '#18FFFF', '#64FFDA', '#69F0AE', '#B2FF59', '#EEFF41', '#FFFF00', '#FFD740', '#FFAB40', '#FF6E40'];

// 球体类
class Ball {
    constructor(x, y, radius) {
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.dx = (Math.random() - 0.5) * 8;
        this.dy = (Math.random() - 0.5) * 8;
        this.color = colors[Math.floor(Math.random() * colors.length)];
        this.mass = radius * 0.1;
        this.friction = 0.99;
        this.trail = [];
        this.maxTrailLength = 10;
    }

    // 绘制球体
    draw() {
        // 绘制轨迹
        for (let i = 0; i < this.trail.length; i++) {
            const alpha = i / this.trail.length * 0.6;
            ctx.beginPath();
            ctx.arc(this.trail[i].x, this.trail[i].y, this.radius, 0, Math.PI * 2);
            ctx.fillStyle = this.color.replace(')', `, ${alpha})`).replace('rgb', 'rgba');
            ctx.fill();
        }

        // 绘制球体
        const gradient = ctx.createRadialGradient(
            this.x - this.radius * 0.3, 
            this.y - this.radius * 0.3, 
            this.radius * 0.1,
            this.x, 
            this.y, 
            this.radius
        );
        gradient.addColorStop(0, 'white');
        gradient.addColorStop(1, this.color);

        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
        ctx.fillStyle = gradient;
        ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
        ctx.shadowBlur = 10;
        ctx.shadowOffsetX = 3;
        ctx.shadowOffsetY = 3;
        ctx.fill();
        ctx.shadowColor = 'transparent';
    }

    // 更新球体位置
    update() {
        // 添加重力
        this.dy += 0.2;

        // 应用摩擦力
        this.dx *= this.friction;
        this.dy *= this.friction;

        // 更新位置
        this.x += this.dx;
        this.y += this.dy;

        // 记录轨迹
        this.trail.push({x: this.x, y: this.y});
        if (this.trail.length > this.maxTrailLength) {
            this.trail.shift();
        }

        // 边界碰撞检测
        if (this.x + this.radius > canvas.width) {
            this.x = canvas.width - this.radius;
            this.dx = -this.dx * 0.8;
        } else if (this.x - this.radius < 0) {
            this.x = this.radius;
            this.dx = -this.dx * 0.8;
        }

        if (this.y + this.radius > canvas.height) {
            this.y = canvas.height - this.radius;
            this.dy = -this.dy * 0.8;
        } else if (this.y - this.radius < 0) {
            this.y = this.radius;
            this.dy = -this.dy * 0.8;
        }
    }
}

// 球体碰撞检测
function checkCollision(ball1, ball2) {
    const dx = ball2.x - ball1.x;
    const dy = ball2.y - ball1.y;
    const distance = Math.sqrt(dx * dx + dy * dy);

    if (distance < ball1.radius + ball2.radius) {
        // 碰撞发生
        const angle = Math.atan2(dy, dx);
        const sin = Math.sin(angle);
        const cos = Math.cos(angle);

        // 旋转坐标系
        const x1 = 0;
        const y1 = 0;
        const x2 = dx * cos + dy * sin;
        const y2 = dy * cos - dx * sin;

        // 旋转速度
        const vx1 = ball1.dx * cos + ball1.dy * sin;
        const vy1 = ball1.dy * cos - ball1.dx * sin;
        const vx2 = ball2.dx * cos + ball2.dy * sin;
        const vy2 = ball2.dy * cos - ball2.dx * sin;

        // 碰撞后速度
        const vx1Final = ((ball1.mass - ball2.mass) * vx1 + 2 * ball2.mass * vx2) / (ball1.mass + ball2.mass);
        const vx2Final = ((ball2.mass - ball1.mass) * vx2 + 2 * ball1.mass * vx1) / (ball1.mass + ball2.mass);

        // 更新位置防止重叠
        const overlap = ball1.radius + ball2.radius - distance;
        ball1.x -= overlap * cos * 0.5;
        ball1.y -= overlap * sin * 0.5;
        ball2.x += overlap * cos * 0.5;
        ball2.y += overlap * sin * 0.5;

        // 旋转回原坐标系
        ball1.dx = vx1Final * cos - vy1 * sin;
        ball1.dy = vy1 * cos + vx1Final * sin;
        ball2.dx = vx2Final * cos - vy2 * sin;
        ball2.dy = vy2 * cos + vx2Final * sin;
    }
}

// 初始化球体
function initBalls(count) {
    for (let i = 0; i < count; i++) {
        const radius = Math.random() * 20 + 10;
        const x = Math.random() * (canvas.width - radius * 2) + radius;
        const y = Math.random() * (canvas.height - radius * 2) + radius;
        balls.push(new Ball(x, y, radius));
    }
}

// 动画循环
function animate() {
    requestAnimationFrame(animate);
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 更新和绘制所有球体
    for (let i = 0; i < balls.length; i++) {
        balls[i].update();
        balls[i].draw();
    }

    // 检测球体间碰撞
    for (let i = 0; i < balls.length; i++) {
        for (let j = i + 1; j < balls.length; j++) {
            checkCollision(balls[i], balls[j]);
        }
    }
}

// 鼠标交互
let isDragging = false;
let draggedBall = null;
let offsetX = 0;
let offsetY = 0;

canvas.addEventListener('mousedown', (e) => {
    const rect = canvas.getBoundingClientRect();
    const mouseX = e.clientX - rect.left;
    const mouseY = e.clientY - rect.top;

    for (let i = 0; i < balls.length; i++) {
        const ball = balls[i];
        const distance = Math.sqrt((mouseX - ball.x) ** 2 + (mouseY - ball.y) ** 2);
        if (distance < ball.radius) {
            isDragging = true;
            draggedBall = ball;
            offsetX = mouseX - ball.x;
            offsetY = mouseY - ball.y;
            ball.trail = []; // 清除拖动时的轨迹
            break;
        }
    }
});

canvas.addEventListener('mousemove', (e) => {
    if (isDragging && draggedBall) {
        const rect = canvas.getBoundingClientRect();
        draggedBall.x = e.clientX - rect.left - offsetX;
        draggedBall.y = e.clientY - rect.top - offsetY;
        draggedBall.dx = (e.movementX || 0) * 0.5;
        draggedBall.dy = (e.movementY || 0) * 0.5;
    }
});

canvas.addEventListener('mouseup', () => {
    isDragging = false;
    draggedBall = null;
});

canvas.addEventListener('click', (e) => {
    if (!isDragging) {
        const rect = canvas.getBoundingClientRect();
        const mouseX = e.clientX - rect.left;
        const mouseY = e.clientY - rect.top;
        const radius = Math.random() * 20 + 10;
        balls.push(new Ball(mouseX, mouseY, radius));
    }
});

// 启动游戏
initBalls(10);
animate();



🌟 让技术经验流动起来

▌▍▎▏ 你的每个互动都在为技术社区蓄能 ▏▎▍▌
点赞 → 让优质经验被更多人看见
📥 收藏 → 构建你的专属知识库
🔄 转发 → 与技术伙伴共享避坑指南

点赞 ➕ 收藏 ➕ 转发,助力更多小伙伴一起成长!💪

💌 深度连接
点击 「头像」→「+关注」
每周解锁:
🔥 一线架构实录 | 💡 故障排查手册 | 🚀 效能提升秘籍

从零实现React函数组件支持:核心原理与实现过程

背景与目标

在React开发中,函数组件因其简洁性广受欢迎。本文将手把手实现React框架对函数组件的支持,揭示其核心运行原理。通过实现函数组件渲染、props传递和多组件支持,深入理解虚拟DOM和协调机制。


一、函数组件的初步支持

1.1 问题现象

当我们尝试渲染函数组件时,控制台报错:

function Counter() {
  return <div>count</div>
}
// 报错:无法将函数作为DOM类型创建

1.2 核心解决思路

  • 类型判断:在虚拟DOM处理阶段识别函数组件类型
  • 组件展开:通过执行函数获取实际子节点
function performWorkOfUnit(fiber) {
  const isFunctionComponent = typeof fiber.type === 'function'
  const children = isFunctionComponent 
    ? [fiber.type(fiber.props)] 
    : fiber.props.children
}

二、DOM树处理机制

2.1 无DOM节点的处理

函数组件本身没有DOM节点,需要在提交阶段向上查找有效父节点:

function commitWork(fiber) {
  let fiberParent = fiber.parent
  // 循环查找有效DOM父节点
  while(!fiberParent.dom) {
    fiberParent = fiberParent.parent
  }
  if(fiber.dom) {
    fiberParent.dom.append(fiber.dom)
  }
}

2.2 多级嵌套组件支持

通过循环向上查找机制,可支持任意层级的组件嵌套:

function Parent() {
  return <Child />
}

三、Props处理与内容渲染

3.1 Props传递实现

通过函数参数传递props对象:

const children = fiber.type(fiber.props)

3.2 文本节点增强

支持数字类型子节点的渲染处理:

function createElement(type, props, ...children) {
  children.map(child => {
    const isTextNode = typeof child === "string" || typeof child === "number"
    return isTextNode ? createTextNode(child) : child
  })
}

四、同级多组件渲染支持

4.1 兄弟节点遍历算法

通过循环查找兄弟节点和父级节点,实现完整DOM树构建:

function getNextFiber(fiber) {
  let nextFiber = fiber
  while(nextFiber) {
    if(nextFiber.sibling) return nextFiber.sibling
    nextFiber = nextFiber.parent
  }
}

实现效果验证

function App() {
  return (
    <div>
      <Counter num={10} />
      <Counter num={20} />
    </div>
  )
}

成功渲染两个计数器组件,分别显示"count: 10"和"count: 20"。


总结与启示

通过四个关键步骤实现函数组件支持:1)类型判断执行组件函数获取子节点;2)DOM查找机制处理无容器组件;3)完善props传递和文本处理;4)兄弟节点遍历算法。核心启示:组件本质是返回虚拟DOM的函数,框架需要递归处理组件树,通过协调算法构建完整DOM结构。该实现方案仅需200行代码,却完整呈现了React核心设计思想,对理解现代前端框架原理具有重要参考价值。

快接龙 | 一款简单高效微信小程序的设计与实现

#1 背景故事

某公司工会贴心地为员工安排了理发与洗头服务💐,时间为指定日期的上午十点至下午六点,当日服务人次上限为 15 人,每人的服务时长为 30 分钟。员工需在微信群中以文字编辑的形式进行接龙报名,但是这种报名方式存在一些弊端😔。

代码已开源:GITHUB,欢迎 star 😄。

#2 程序设计

#2.1 🧠功能设定

#2.2 🚀技术选型

类别 技术栈 备注
后端 Node.js + Fastify
数据库 SQLite3 轻量级的嵌入式关系型数据库,简单稳定,ORM 框架为objection.js,理论能无缝对接 MySQL8
组件库 Vant 小程序版本
包管理 pnpm 速度快、节省磁盘空间的软件包管理器⚡
构建工具 rsbuild 支持前后端构建(速度优于 webpack,而 vite 构建后端应用存在缺陷),支持国产✊
IDE VSCode + 微信开发者工具 微软出品,必属精品🙂

#2.3 🔢数据字典

#3 📃 功能页面

此处仅介绍小程序端页面

未来会将代码开源😄。

小小副业:个人微信小程序_如何实现收益?

上一篇讲到我用cursor如何从0到1开发了一款个人小程序,# Cursor实战:从0到1快速打造你的第一个微信小程序。今天讲一下个人小程序如何实现盈利?收益可以覆盖开发成本吗?

开发成本
  1. 个人微信小程序认证:¥30/年;
  2. cursor充值了会员:$20/月;(这部分公司报销,可以忽略)
  3. 后台用的微信云开发,每月19.9,本来一开始已经准备为爱发电了,因为19.9也就是一杯奶茶的钱,但当用户量达到500时,我还是心动了(要是除去成本还能有19.9的收益,那岂不是可以再多喝一杯奶茶了...)
1. 积累用户阶段:

在这里不得不说一句,微信是真的苟,我的小程序不能被模糊搜索到,只能通过全称搜索,更要命的是我刚开始起了一个老长的名字:【50天重启人生-计划打卡】,少一个符号都搜不出的那种,所以一点自然流量都没得,当时就觉得靠自然流量算是废了,于是我连夜改名,朋友圈发了一波,然后又在友商平台推广了一下,就这样在上线10天左右,用户量终于达到500+。

image.png

2. 开通流量主:

开通流量主后,有2种接入广告的模式,一种是智能接入,由微信官方在合适的页面自动接入,无需自己开发;第二种是自主接入,需手动插入广告组件代码,优点是可以自定义样式。我采用了后者,虽然当时用户量不多,但还是不想影响页面的美观以及影响用户体验。由于刚开始只接入2个广告,所以前期的收益少的可以直接忽略。

image.png

后期接入了激励视频,收益渐渐开始有了上升趋势(大家如果怕接入激励视频太过生硬,引起用户反感,可以采用这种形式,静静地放在那里,赌一把用户会主动点击)。

WechatIMG1264_副本.jpg

再之后的收益就还算平稳的维持着,截止到昨天,40天左右,总收益大概是107(我能说比我的基金强嘛🐶)

image.png

以上就是关于个人小程序的收益情况,正好也回答下之前在另一个帖子中的一条评论,我觉得AI提效不等于卷, 拥抱AI,只会让你做更多自己想做的事,实现更多之前做不到,但现在可以做的事~

image.png

❌