阅读视图

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

精简之道:TypeScript 参数属性 (Parameter Properties) 详解

一、什么是参数属性?

参数属性是一种简洁的语法,是TypeScript独特的语法糖,它允许你在构造函数的参数列表中,通过添加访问修饰符(public, private, protected)或 readonly 关键字,来一次性完成属性的声明和初始化

示例:

class User {
    constructor(public name: string, private age: number, readonly id: number) {
        // 构造函数体可以是空的,因为声明和赋值已经自动完成了!
        // TypeScript 在幕后为你做了三件事:
        // 1. 声明了一个 public 的 name 属性。
        // 2. 声明了一个 private 的 age 属性。
        // 3. 声明了一个 readonly 的 id 属性。
        // 4. 自动完成了 this.name = name, this.age = age, this.id = id。
    }

    public getAge(): number {
        return this.age; // age 是 private 的,但可以在类内部访问
    }
}

const user = new User('Alice', 30, 123);
console.log(user.name); // "Alice" (public, 可访问)
// console.log(user.age); // Error: 属性'age'是私有的,只能在类'User'中访问。
console.log(user.getAge()); // 30 (通过公共方法访问)
console.log(user.id); // 123 (readonly, 可访问但不可修改)
// user.id = 456; // Error: 无法分配到 'id' ,因为它是只读属性。


二、参数属性的规则与组合

参数属性不仅仅是 private 的专利,它可以与所有访问修饰符以及 readonly 组合使用:

  • public:成员在任何地方都可见。(如果省略修饰符,参数默认不会成为属性)。
  • private:成员只能在声明它的类的内部访问。
  • protected:成员可以在声明它的类及其子类的内部访问。
  • readonly:成员在初始化后不能被再次赋值,有助于创建不可变(immutable)对象。

你可以自由组合它们(readonly 通常与访问修饰符一起使用)。

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

vue3入门-v-model、ref和reactive讲解

组件上v-model用法

在 Vue 2.0 发布后,开发者使用 v-model 指令必须使用为 valueprop。如果开发者出于不同的目的需要使用其他的 prop,他们就不得不使用 v-bind:propName.sync。此外,由于 v-modelvalue 之间的这种硬编码关系的原因,产生了如何处理原生元素和自定义元素的问题。

在 Vue 2.2 中,我们引入了 model 组件选项,允许组件自定义用于 v-modelprop事件。但是,这仍然只允许在组件上使用一个 model

在 Vue 3 中,双向数据绑定的 API 已经标准化,减少了开发者在使用 v-model 指令时的混淆并且在使用 v-model 指令时可以更加灵活。

首先让我们回忆一下 v-model原生元素上的用法:

<input v-model="text" />

在代码背后,模板编译器会对 v-model 进行更冗长的等价展开。因此上面的代码其实等价于下面这段:

<input :value="text" @input="text = $event.target.value" />

接下来我们看下在自定义组件上的用法。

2.x语法

<ChildComponent v-model="text" />
<!-- 去除 v-model 语法糖后的写法 -->
<ChildComponent :value="text" @input="text = $event" />

ChildComponent.vue

<template>
  <input :value="value" @input="($event) => $emit('input', $event.target.value)" />
</template>
<script>
  export default {
    props: ['value'],
  }
</script>

如果要将属性或事件名称更改为其他名称,则需要在 ChildComponent 组件中添加 model 选项:

ParentComponent.vue

<myComponent v-model="isChecked" />

ChildComponent.vue

<template>
  <input type="checkbox" :checked="checked" @change="($event) => $emit('change', $event.target.checked)" />
</template>
<script>
  export default {
    model: {
      // 使用 `checked` 代替 `value` 作为 model 的 prop
      prop: 'checked',
      // 使用 `change` 代替 `input` 作为 model 的 event
      event: 'change'
    },
    props: {
      checked: Boolean
    }
  }
</script>

在这个例子中,父组件 v-model 的实际内部处理如下:

<ChildComponent :value="text" @change="text = $event" />

使用 v-bind.sync

在某些情况下,我们可能需要对某一个 prop 进行“双向绑定”(除了前面用 v-model 绑定 prop 的情况)。为此,我们建议使用 update:myPropName 抛出事件。

假设 ChildComponent 带有 title prop ,我们可以通过下面的方式将分配新 value 的意图传达给父级:

this.$emit('update:title', newValue)

如果需要的话,父级可以监听该事件并更新本地 data property。例如:

<ChildComponent :title="text" @update:title="text = $event" />

为了方便起见,我们可以使用 .sync 修饰符来缩写,如下所示:

<ChildComponent :title.sync="text" />

3.x语法

当使用在一个组件上时,v-model 会被展开为如下的形式:

<template> 
  <myComponent model-value="text" @update:model-value="($event) => text = $event" />
  <div>{{ text }}</div>
</template>
<script setup>
import { ref } from 'vue'
import myComponent from './components/myComponent.vue'
const text = ref('')
</script>

要让这个例子实际工作起来,<myComponent> 组件内部需要做两件事:

  • 将内部原生 <input> 元素的 value attribute 绑定到 modelValue prop
  • 当原生的 input 事件触发时,触发一个携带了新值的 update:modelValue 自定义事件

这里是相应的代码:

myComponent.vue

<template>
  <input :value="modelValue" @input="(e) => $emit('update:modelValue', e.target.value)" />
</template>
<script setup>
const props = defineProps(['modelValue'])
const emits = defineEmits(['update:modelValue'])
</script>

现在 v-model 可以在这个组件上正常工作了:

<myComponent v-model="text" />

另一种在组件内实现 v-model 的方式是使用一个可写的,同时具有 gettersettercomputed 属性。get 方法需返回 modelValue prop,而 set 方法需触发相应的事件:

myComponent.vue

<template>
  <input v-model="value" />
</template>
<script setup>
import { computed } from 'vue'
const { modelValue } = defineProps(['modelValue'])
const emits = defineEmits(['update:modelValue'])
const value = computed({
  get() {
    return modelValue
  },
  set(newVal) {
    return emits('update:modelValue', newVal)
  }
})
</script>

v-model 的参数

组件上的 v-model 也可以接受一个参数:

<MyComponent v-model:title="bookTitle" />

在这种情况下,子组件应该使用 title prop 和 update:title 事件来更新父组件的值,而非默认的 modelValue prop 和 update:modelValue 事件:

myComponent.vue

<template>
  <input :value="title" @input="(e) => $emit('update:title', e.target.value)" />
</template>
<script setup>
const { title } = defineProps(['title'])
const emits = defineEmits(['update:title'])
</script>

多个 v-model 绑定

利用刚才在 v-model 的参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个 v-model 双向绑定。

组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:

<template> 
  <myComponent v-model:book-name="bookName" v-model:book-auther="bookAuther" />
  <div>bookName:{{ bookName }}、bookAuther:{{ bookAuther }}</div>
</template>
<script setup>
import { ref } from 'vue'
import myComponent from './components/myComponent.vue'
const bookName = ref('')
const bookAuther = ref('')
</script>

在这种情况下,子组件应该使用 bookNamebookAuther prop,以及 update:bookNameupdate:bookAuther 事件来更新父组件的值:

myComponent.vue

<template>
  <input :value="bookName" @input="(e) => $emit('update:bookName', e.target.value)" />
  <input :value="bookAuther" @input="(e) => $emit('update:bookAuther', e.target.value)" />
</template>
<script setup>
const { bookName, bookAuther } = defineProps(['bookName', 'bookAuther'])
const emits = defineEmits(['update:bookName', 'update:bookAuther'])
</script>

ref与reactive

在 Vue 3 中,响应式数据的创建主要依赖于 refreactive 两个 API。它们各自有不同的用途和适用场景。

ref 用于创建基本数据类型的响应式数据,而 reactive 用于创建复杂数据结构(如对象和数组)的响应式数据。

ref

这个方法需要在顶部引入:import { ref } from 'vue'。通常使用它定义响应式数据,不限数据类型。

let xxx = ref(初始值)

返回值: 传入的是基本数据类型,则返回 RefImpl 实例对象(简称ref)。如果传的是引用数据类型,则内部是通过 reactive 方法处理,最后形成了一个 Proxy 类型的对象。ref 对象的 value 属性是响应式的。

ref 创建的数据,js 中需要 .valuetemplate 中可省略(自动解包)。

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

  const text = ref('')
  console.log('ref text', text) // ref text RefImpl {dep: Dep, __v_isRef: true, __v_isShallow: false, _rawValue: '', _value: ''}

  const obj = reactive({
    name: 'caoyuan'
  })
  console.log('reactive obj', obj) // reactive obj Proxy(Object) {name: 'caoyuan'}

</script>

我们打印 obj,你会发现,它不再是 RefImpl 实例对象,变成了 Proxy 实例对象,vue3 底层把对象都变成了 Proxy 实例对象,对于基本数据类型就是按照 Object.defineProperty 里面的 getset 进行数据劫持然后进行响应式,但是如果是对象类型的话,是用到的 Proxy。vue3把它封装在新函数 reactive 里,就相当于,ref 中是对象,自动会调用 reactive

那为什么定义一个响应式数据,偏偏要用 .value 去操作呢,满篇的 .value 有什么必要吗,像 Vue2 里直接拿着变量名称处理不是很好?

在官网中得到了解答:

将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,NumberString 等基本类型是通过值而非引用传递的。

按引用传递与按值传递

在任何值周围都有一个封装对象,这样我们就可以在整个应用中安全地传递它,而不必担心在某个地方失去它的响应性。

提示: 换句话说,ref 为我们的值创建了一个响应式引用。在整个组合式 API 中会经常使用引用的概念。

注:安装 Vue - Official 插件后,搜索 Dot Value,勾选对应选项,插件会在使用 ref 创建的变量时自动添加 .value

reactive

前边提到,ref 可以返回任意类型的变量的响应式副本,那 reactive 还有什么必要吗?

当然是有必要的。上一段对 ref 了解,他在操作该响应式变量的时候,需要 .value 去取值,那有没有一个方法,可以避开 NumberString 等基本类型,操作时候无需 .value 呢?答案是有的。也就是 reactive 函数。

作用:定义一个对象类型的响应式数据,不能定义基本数据类型。

语法:const 代理对象 = reactive(源对象)

  • 接收一个对象(或数组),返回一个代理对象(Proxy 的实例对象,简称 proxy 对象)
  • reactive 定义的响应式数据是深层次的,意思是不管对象嵌套多少层,整个对象都是响应式的
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作
<template>
  <div>{{  arr.toString()  }}</div>
  <div>{{ obj.info.schoolInfo.location }}</div>
</template>
<script setup>
import { reactive } from 'vue';

  const arr = reactive([1,2,3])
  // 3秒后值变化
  setTimeout(() => {
    arr.push(4)
  }, 3000);

  const obj = reactive({
    info: {
      name: 'caoyuan',
      schoolInfo: {
        location: 'henan'
      }
    }
  })
  // 6秒后值变化
  setTimeout(() => {
    obj.info.schoolInfo.location = 'shanghai'
  }, 6000);
</script>

ref与reactive的区别

从定义数据角度:

  • ref 用来定义基本类型数据、引用类型数据。定义引用数据类型时,内部会调用 reactive 转为代理对象
  • reactive 用来定义引用类型数据,不支持基本数据类型

从原理角度:

  • ref 通过 Object.defineProperty()getset 来实现响应式(数据劫持)
  • reactive 通过使用 Proxy 来实现响应式(数据劫持), 并通过 Reflect 操作源对象内部的数据
  • ref 遇到引用数据类型时,它的内部会自动通过 reactive 转为代理对象

从使用角度:

  • ref 定义的数据:操作数据需要 .value,读取数据时模板中直接读取不需要 .value
  • reactive 定义的数据:操作数据与读取数据:均不需要 .value

使用原则:

  • 若需要一个基本类型的响应式数据,必须使用 ref
  • 若需要一个响应式对象,层级不深,refreactive都可以
  • 若需要一个响应式对象,且层级较深,推荐使用 reactive

ref模板引用

在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref attribute:

<input ref="input">

ref 是一个特殊的 attribute,它允许我们在一个特定的 DOM 元素子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。

访问模板引用

要在组合式 API 中获取引用,我们可以使用辅助函数 useTemplateRef()

<template>
  <input ref="my-input" />
</template>
<script setup>
import { useTemplateRef, onMounted } from 'vue'

// 第一个参数必须与模板中的 ref 值匹配
const inputRef = useTemplateRef('my-input')

onMounted(() => {
  inputRef.value.focus()
})
</script>

注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input,在初次渲染时会是 null。这是因为在初次渲染前这个元素还不存在呢!

如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为 null 的情况:

watchEffect(() => {
  if (inputRef.value) {
    inputRef.value.focus()
  } else {
    // 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
  }
})

组件上的 ref

模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:

<template>
  <Child ref="child" />
</template>
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'

const childRef = useTemplateRef('child')

onMounted(() => {
  // childRef.value 将持有 <Child /> 的实例
})
</script>

如果一个子组件使用的是选项式 API 或没有使用 <script setup>,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 propsemit 接口来实现父子组件交互。

当使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:

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

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
  a,
  b
})
</script>

当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number } (ref 都会自动解包,和一般的实例一样)。

请注意,defineExpose 必须在任何顶层 await 操作之前调用。否则,在 await 操作后暴露的属性和方法将无法访问。

v-for 中的模板引用

当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:

<template>
  <ul>
    <li v-for="item in list" ref="items">
      {{ item }}
    </li>
  </ul>
</template>
<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'

const list = ref([
  /* ... */
])

const itemRefs = useTemplateRef('items')

onMounted(() => console.log(itemRefs.value))
</script>

应该注意的是,ref 数组并不保证与源数组相同的顺序。

函数模板引用

除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:

<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">

注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。

react-router里的两种路由方式有什么不同

在React Router中,createBrowserRoutercreateHashRouter的主要区别在于路由模式的选择:

兼容性差异

  • ‌**createBrowserRouter**‌(基于HTML5 History API)不兼容IE9及以下版本,但支持现代主流浏览器。 ‌
  • ‌**createHashRouter**‌(基于URL哈希值)兼容性更好,可适配更多浏览器,包括低版本IE。 ‌

地址栏表现形式

  • ‌**createBrowserRouter**‌的URL路径中不含#(如localhost:3000/demo/test)。 ‌
  • ‌**createHashRouter**‌的URL路径包含#(如localhost:3000/#/demo/test)。 ‌

刷新页面影响

  • ‌**createBrowserRouter**‌刷新页面后,路由状态(如参数)不受影响,因状态保存在浏览器历史记录中。 ‌
  • ‌**createHashRouter**‌刷新页面会导致路由状态丢失,需重新加载数据。 ‌

使用场景建议

  • ‌**createBrowserRouter**‌适用于需要优雅URL且兼容性要求不高的场景(如公网项目)。
  • ‌**createHashRouter**‌适用于兼容性优先的场景(如内网项目或老旧浏览器环境)。 ‌

异步任务并发控制

JavaScript 异步任务并发控制

🤔 问题背景

常见面试题 批量并发任务

📋 需求分析

一个完整的并发控制方案需要满足以下要求:

  • 并发限制:同时执行的任务数量不能超过指定上限
  • 任务队列:待执行的任务需要有序排队
  • 结果保序:无论任务何时完成,最终结果要按原始顺序返回
  • 错误处理:任何一个任务失败时,能够优雅地处理错误
  • 动态调度:任务完成后自动开始下一个待执行任务

💡 核心实现

让我们来看一个优雅的实现方案:

const runTask = async (tasks, maxTaskNum) => {
  // 参数校验和初始化
  const total = Array.isArray(tasks) ? tasks.length : 0;
  if (total === 0) return [];

  const limit = Math.max(1, Math.min(maxTaskNum, total));
  const result = new Array(total);

  // 使用Promise.withResolvers()创建可控制的Promise
  const { promise, resolve, reject } = Promise.withResolvers();
  let nextIndex = 0; // 下一个要执行的任务索引
  let finished = 0; // 已完成的任务计数

  const runNext = () => {
    const i = nextIndex++;
    if (i >= total) return; // 没有更多任务

    Promise.resolve()
      .then(() => tasks[i]())
      .then((res) => {
        result[i] = res; // 按索引存储,保证顺序
      })
      .catch((err) => {
        reject(err); // 任何一个任务失败,整体失败
      })
      .finally(() => {
        finished++;
        if (finished === total) {
          resolve(result); // 所有任务完成
        } else {
          runNext(); // 继续执行下一个任务
        }
      });
  };

  // 启动初始的并发任务
  for (let i = 0; i < limit; i++) {
    runNext();
  }

  await promise;
  return result;
};

🔍 代码详解

1. 参数处理与初始化

const total = Array.isArray(tasks) ? tasks.length : 0;
if (total === 0) return [];

const limit = Math.max(1, Math.min(maxTaskNum, total));
const result = new Array(total);

这部分代码确保了参数的合法性:

  • 验证任务数组的有效性
  • 计算实际并发数(不能超过总任务数,至少为 1)
  • 预先创建结果数组,确保索引对应关系

2. Promise 控制器

const { promise, resolve, reject } = Promise.withResolvers();

Promise.withResolvers()是 ES2024 的新特性,它返回一个包含 promise 及其控制函数的对象,让我们可以在外部控制 Promise 的状态。

3. 任务调度核心

const runNext = () => {
  const i = nextIndex++;
  if (i >= total) return;

  Promise.resolve()
    .then(() => tasks[i]())
    .then((res) => (result[i] = res))
    .catch((err) => reject(err))
    .finally(() => {
      finished++;
      if (finished === total) {
        resolve(result);
      } else {
        runNext();
      }
    });
};

这是整个调度器的核心逻辑:

  • nextIndex++:原子性地获取下一个任务索引
  • Promise.resolve().then():确保任务异步执行
  • result[i] = res:按原始索引存储结果
  • finally块:无论成功失败都要更新计数和调度

🎯 执行流程演示

让我们通过一个具体例子来理解执行流程:

runTask(
  [
    () => new Promise((resolve) => setTimeout(() => resolve(1), 6000)), // 6秒
    () => new Promise((resolve) => setTimeout(() => resolve(2), 1000)), // 1秒
    () => new Promise((resolve) => setTimeout(() => resolve(3), 100)), // 0.1秒
    () => new Promise((resolve) => setTimeout(() => resolve(4), 2000)), // 2秒
    () => new Promise((resolve) => setTimeout(() => resolve(5), 100)), // 0.1秒
  ],
  2
).then((res) => {
  console.log(res); // [1, 2, 3, 4, 5]
});

执行时间线(并发数=2):

0ms:     启动任务0(6s) 和 任务1(1s)      [执行中: 0,1]
1000ms:  任务1完成,启动任务2(0.1s)      [执行中: 0,2]
1100ms:  任务2完成,启动任务3(2s)        [执行中: 0,3]
3100ms:  任务3完成,启动任务4(0.1s)      [执行中: 0,4]
3200ms:  任务4完成,等待任务0            [执行中: 0]
6000ms:  任务0完成,所有任务结束         [完成]

总耗时: 6秒(相比串行执行的9.3秒,节省了3.3秒)

🚀 优化和扩展

1. 添加进度回调

const runTaskWithProgress = async (tasks, maxTaskNum, onProgress) => {
    // ... 原有代码

    .finally(() => {
        finished++;
        onProgress && onProgress({
            finished,
            total,
            percent: (finished / total * 100).toFixed(2)
        });
        // ... 后续逻辑
    });
};

2. 支持任务优先级

const runTaskWithPriority = async (tasks, maxTaskNum) => {
  // 按优先级排序任务
  const sortedTasks = tasks
    .map((task, index) => ({ task, index, priority: task.priority || 0 }))
    .sort((a, b) => b.priority - a.priority);

  // ... 使用排序后的任务执行
};

3. 失败重试机制

const executeWithRetry = async (task, retries = 3) => {
  for (let i = 0; i < retries; i++) {
    try {
      return await task();
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise((resolve) => setTimeout(resolve, 1000 * i));
    }
  }
};

⚡ 性能考量

  1. 内存使用:预先创建结果数组会占用内存,对于大量任务需要考虑分批处理
  2. 错误处理:当前实现遇到错误会立即终止,可以考虑支持部分失败
  3. 取消机制:长时间运行的任务可能需要支持取消操作

🎉 总结

异步任务并发控制是前端开发中的重要技能,它能够:

  • 提升性能:合理利用并发,减少总执行时间
  • 保护资源:避免过度并发造成的资源浪费
  • 增强体验:提供可控的执行进度和错误处理

通过理解其核心原理和实现细节,我们可以根据具体场景进行定制和优化,构建出更加 robust 和高效的异步任务处理方案。

这种模式在现代前端框架中也有广泛应用,比如 Vue 的异步组件加载、React 的 Suspense 机制等,都体现了类似的并发控制思想。掌握这种技术,将让你在处理复杂异步场景时更加得心应手。


如果这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐ 和分享 📤!

关注我,获取更多前端技术干货和面试题解析 🚀

前端居中九种方式血泪史:面试官最爱问的送命题,我一次性整明白!

“你能说一下有哪几种方式实现居中吗?” —— 这句话堪称前端面试的经典开场白。无数面试者在这道看似简单的问题上折戟沉沙,今天我就带你彻底攻克这个“送命题”,用九种实用方案征服面试官!


🧱 一、经典基础方案(传统布局)

  1. 文本居中:text-align + line-height

    .parent { text-align: center; }
    .child { 
      display: inline-block; 
      line-height: 200px; /* 等于父级高度 */
    }
    

    适用场景:单行文本或行内元素垂直居中。

  2. 绝对定位 + margin:auto

    .child {
      position: absolute;
      top: 0; right: 0; bottom: 0; left: 0;
      margin: auto;
      width: 100px; height: 100px;
    }
    

    优势:兼容性好(IE8+),需指定宽高。

  3. 负边距偏移(经典居中)

    .child {
      position: absolute;
      top: 50%; left: 50%;
      margin-top: -50px; /* 高度一半 */
      margin-left: -50px; /* 宽度一半 */
    }
    

    痛点:需精确计算尺寸,响应式不友好。


⚡ 二、现代布局方案(Flex/Grid)

  1. Flex 布局(面试官最爱!)

    .parent {
      display: flex;
      justify-content: center; /* 水平居中 */
      align-items: center;     /* 垂直居中 */
    }
    

    适用场景:99%的居中需求,移动端首选。

  2. Grid 布局(降维打击)

    .parent {
      display: grid;
      place-items: center; /* 一行搞定水平和垂直居中 */
    }
    

    优势:代码极简,适合复杂布局。


🧪 三、黑科技方案(展示技术深度)

  1. transform 位移法(不依赖固定尺寸)

    .child {
      position: absolute;
      top: 50%; left: 50%;
      transform: translate(-50%, -50%);
    }
    

    适用场景:未知尺寸元素居中(IE9+)。

  2. Table-Cell 魔法(兼容老项目)

    .parent {
      display: table-cell;
      vertical-align: middle; /* 垂直居中 */
      text-align: center;      /* 水平居中 */
    }
    .child { display: inline-block; }
    
  3. 伪元素撑高法(垂直居中神器)

    .parent::before {
      content: "";
      display: inline-block;
      height: 100%;
      vertical-align: middle;
    }
    .child { display: inline-block; vertical-align: middle; }
    
  4. Writing-mode 文字流旋转

    .parent {
      writing-mode: vertical-lr; /* 改变流方向 */
      text-align: center;
    }
    .child {
      writing-mode: horizontal-tb;
      display: inline-block;
    }
    

    慎用:炫技专用,实际项目慎用!


💡 面试满分话术模板

“居中方案需根据场景选择

  • 移动端首选 Flex,代码简洁兼容好;
  • 未知尺寸用 Transform 位移;
  • 老项目可用 Table-Cell负边距
  • 文本居中优先 text-alignline-height
    现代开发中,Flex/Grid 是更优雅的解决方案。

📊 方案对比总结

方案 兼容性 是否需要定宽高 适用场景
Flex IE10+ 通用布局
Grid IE11+ 二维布局
Transform IE9+ 未知尺寸元素
绝对定位 + margin IE6+ ✔️ 传统固定尺寸元素
Table-Cell IE8+ 老项目兼容

下次面试官再问居中,直接甩出九连击:“从传统到现代,从兼容到黑科技,您想听哪种?” 技术深度与幽默感并存,offer拿到手软!

推荐一个三维导航库:three-pathfinding-3d

three-pathfinding-3d

介绍

PS: 由于 three-pathfinding 作者目前没有维护,提交的pr也没有得到反馈,所以自己新建了一个库,其中大部分代码从 three-pathfinding 拷贝而来,解决此库未解决的几个问题。

如有侵权请联系我:email:526838933@qq.com

three-pathfingding-3d 由 three-pathfinding 优化而来,修复了该库存在的几个已知问题:

  1. funnel 算法缺失第一通道,该问题导致最终路径错误
  2. funnel 算法没有针对3d场景进行优化,该问题导致在特殊情况下,实际路径与算法生成路径差距过大,很多情况会异常中断算法执行。

该库解决了第一个问题,将缺失的第一通道补充。 该库解决了第二个问题,优化 funnel => funnel3d 使其在 3d 情况下能够生成合适的路径。

使用说明
git clone https://gitee.com/yjsdszz/three-pathfinding-3d

cd demo

npm i 

npm run dev
npm i three-pathfinding-3d

技术原理:

  1. navigation mesh : 导航网格,根据此mesh的几何数据构建图,A*算法基于此图搜索。
  2. 漏斗算法:基于A*搜索得到的结果,生成通道,根据漏斗算法得到最终路径。
  3. 三维场景需要考虑垂直差,在传统的2d算法情况下,需要进行优化部分场景。

有需要了解更多细节的,可以私信我详细讨论。

我差点失去了巴顿(我的狗狗) - 肘子的 Swift 周报 #98

巴顿已经 13 岁了。尽管大多数时候他都表现出远超同龄狗狗的活力和状态,但随着年龄增长,各种健康问题也随之而来。不久前,巴顿被检查出肺动脉高压,医生给出了针对性的治疗方案。就在我为治疗似乎初见成效而欣慰时,上周一下午,巴顿突然无法站立,大量流口水,表现出明显的心脏不适。

async/await 的优雅外衣下:Generator 的核心原理与 JavaScript 执行引擎的精细管理

async/await 的优雅外衣下:Generator 的核心原理与 JavaScript 引擎的精细管理

在现代 JavaScript 的异步编程中,async/await 几乎成了主流。开发者们喜欢用它来编写逻辑清晰、易于维护的异步代码。然而,很少有人深入探究 async/await 背后强大的技术支撑——Generator(生成器)机制,以及 JavaScript 引擎在编译和运行阶段是如何巧妙管理这些复杂流程的。本文将系统性地揭开这层神秘面纱,带你从语法、原理一直深入到引擎内部的运作机制。


1. async/await:让异步世界感觉像同步

async/await 是 ES2017 引入的语法糖,它为基于 Promise 的异步操作带来了同步代码般的编写体验。典型的写法如下:

async function getData() {
  const user = await fetchUser();
  const posts = await fetchPosts(user.id);
  return { user, posts };
}

await 关键字遇到 Promise 时会暂停当前函数的执行,等待这个 Promise 完成(resolved 或 rejected),然后再继续向下执行。开发者可以用近乎同步的顺序来表达异步逻辑,不再需要繁琐的 .then()/.catch() 链或者嵌套的回调函数。


2. async/await 的底层基石:Generator 的自动调度

2.1 async 函数与 Promise 的本质

每个 async 函数本质上都会返回一个 Promise。函数内部任何未被捕获的异常都会导致这个返回的 Promise 变为 rejected 状态。因此,async/await 本质上是一种语法上的便捷包装:

// async/await 写法
async function foo() {
  const res = await bar();
  return res;
}

// 转换后的等效 Promise 写法
function foo() {
  return bar().then(res => res);
}

2.2 Generator:支撑 async/await 的核心机制

Generator(生成器)是 ES6 引入的一种特殊函数类型,它可以暂停执行,之后又能从暂停的地方恢复。使用 function* 声明,yield 关键字用于“暂停”函数的执行,并保留函数当前的执行状态(包括局部变量、上下文等)。

function* sequence() {
  yield 1;
  yield 2;
  return 3;
}

const it = sequence();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: true }

每次在 yield 处暂停时,所有状态都被完整保存。当通过 .next() 方法恢复时,函数会从上次暂停的位置继续执行。这种“暂停与恢复”的能力,正是 async/await 实现顺序化异步操作的技术基础。

2.3 Generator 自动化控制流程

在 async/await 成为标准之前,社区库(如 co.js)就利用 Generator 实现了自动化的异步流程控制:

function* asyncFlow() {
  const user = yield fetchUser(); // 暂停,等待 fetchUser 结果
  const posts = yield fetchPosts(user.id); // 暂停,等待 fetchPosts 结果
  return { user, posts };
}

// 自动执行 Generator 的函数
function run(gen) {
  const iterator = gen();
  function step(prev) {
    const { value, done } = iterator.next(prev); // 恢复执行,传入上一个结果
    if (done) return Promise.resolve(value); // 如果结束,返回最终值
    return Promise.resolve(value).then(step); // 等待 Promise 完成,然后继续下一步
  }
  return step(); // 开始执行
}

// 使用
run(asyncFlow).then(result => console.log(result));

async/await 在底层本质上就是引擎自动帮你实现了类似 run 函数的功能,将 Generator 和 Promise 完美结合,只是语法上更加简洁直观。

2.4 Babel / 引擎的编译转换

现代的 JavaScript 引擎(或 Babel 这样的转译器)在内部会将 async/await 代码编译转换。转换的目标通常是类似上面 run 函数的逻辑(基于 Generator)或者是纯粹的 Promise 链。关键点在于:

  • 每当遇到 await,引擎会在运行时暂停函数的执行(相当于 Generator 的 yield),等待后面的 Promise 完成。
  • 编译阶段会生成管理函数执行状态(比如当前执行到哪里了)的代码,并确保函数恢复执行时,局部变量和作用域都能正确还原。

3. Generator 的本质:状态机与作用域快照

Generator 的技术核心是一个自带状态的迭代器

  • 每个 yield 语句对应函数执行中的一个特定状态点。
  • 当执行到 yield 暂停时,函数当前的所有局部变量、执行上下文状态都会被完整保存下来
  • 通过调用 .next()(传入值恢复)或 .throw()(抛出异常恢复),可以从暂停点恢复执行,并可以传入新的值或异常。

伪代码模拟底层状态机:

function* taskFlow() {
  const a = yield step1(); // 状态 0: 开始执行,调用 step1
  const b = yield step2(a); // 状态 1: 接收到 step1 结果 a,调用 step2(a)
  return b; // 状态 2: 接收到 step2 结果 b,结束
}

// 编译后可能类似于 (概念性伪代码):
function compiledTaskFlow() {
  let state = 0;
  let a, b;
  return {
    next: function (value) {
      switch (state) {
        case 0:
          state = 1;
          return { value: step1(), done: false }; // 启动 step1
        case 1:
          a = value; // 接收 step1 的结果
          state = 2;
          return { value: step2(a), done: false }; // 启动 step2(a)
        case 2:
          b = value; // 接收 step2 的结果
          state = -1;
          return { value: b, done: true }; // 结束
        default:
          return { value: undefined, done: true };
      }
    }
  };
}

Generator 的强大之处在于它高效地保存和恢复了函数执行环境的“快照”,特别是在处理并发异步逻辑时,为复杂的控制流提供了坚实基础。


4. JavaScript 引擎的编译与运行管理

4.1 编译期(准备阶段)

  • 分析代码: 引擎识别出 async/awaitfunction*/yield 语法。
  • 代码转换: 将这些语法结构转换为底层可执行的代码,通常是基于状态机的实现(如上文的伪代码概念)或 Promise 链。
  • 生成管理代码: 为每个暂停点(await/yield)生成管理执行状态(当前进行到哪一步)、保存/恢复局部变量和作用域链的代码。
  • 处理异常与外部控制: 设置好处理异常传播的路径以及外部控制(如 .next(), .throw())的接入点。

4.2 运行期(执行阶段)

  • 暂停与恢复: 当执行到 awaityield 时,引擎会挂起当前函数的整个执行上下文(包括变量、作用域链等)
  • 事件循环集成: 引擎将等待的 Promise 纳入事件循环的微任务队列管理。当 Promise 完成(resolved/rejected)时,对应的恢复操作(继续执行 Generator 或 async 函数)会被安排到微任务队列中。
  • 状态恢复: 引擎从微任务队列取出恢复任务,利用编译期生成的管理代码,精准地还原之前保存的执行上下文和状态,并从暂停点继续执行。
  • 异常处理: 如果等待的 Promise 被拒绝(rejected),引擎会将异常注入到暂停点,使其能被 async 函数内部的 try/catch 或 Generator 的 .catch / try/catch 捕获。
  • 性能与体验: 这套机制实现了“用同步语法写异步代码”的效果(非阻塞),在保证开发者良好体验的同时,也尽可能提升了性能。

5. 总结

Generator 是 JavaScript 异步编程能力实现飞跃的关键技术内核。 async/await 作为其上层封装,提供了一层优雅易用的语法糖衣。其底层核心依赖于 Generator 的暂停/恢复机制和 Promise 的异步状态管理。

在这个过程中,JavaScript 引擎扮演着至关重要的角色:在编译期,它进行复杂的代码分析和转换,生成状态管理逻辑;在运行期,它通过事件循环和微任务队列,精确地调度函数的暂停与恢复,并确保执行环境(作用域、变量)的正确保存与还原。这套精巧的协作机制,不仅让开发者能够编写出清晰、易维护的异步代码,也为构建高性能的现代 Web 应用提供了强大的底层支撑。


关键点回顾:

  • 理解 Generator 的工作原理(暂停、恢复、状态保存)是深入掌握 JavaScript 高级异步编程本质的关键。
  • async/await 的简洁性 得益于 JavaScript 引擎在幕后高效地实现了状态机管理和执行环境的保存/恢复。
  • 了解引擎在编译期和运行期如何协作管理异步流程,有助于开发者编写出性能更好、结构更优的复杂异步代码。

希望这篇解析能帮你真正看透 JavaScript 异步编程背后的“魔法”,从优雅的语法表面,深入到 Generator 的核心原理,再到引擎的精密运作机制,全方位提升你的技术洞察力!

Vue2实践(3)之用component做一个动态表单(二)

前言

在上一篇中,我们已经通过<component>实现了组件的动态渲染,为这个动态表单功能定下框架。

在这篇中,我们将着重实现功能。

功能实现

在上一篇中,我们定下了由设置组件来制作具体组件的方案,我们先来完善这一功能——从设置组件中获取完整的具体组件信息。

在这里我选用SelectInputSetting来做例子

<template>
    <div>
        <TextInput :field="labelField" v-model="setting.name" />
        <div class="option" v-for="(item, index) in setting.options" :key="item.key"> 
            <TextInput class="option-content" :field="item" v-model="setting.options[index].value" />
            <button @click="deleteOption(index)">删除</button>
        </div>
        <button @click="addOption">添加选项</button>
    </div>
</template>
<script>
import TextInput from './TextInput.vue';
export default {
    components: {
        TextInput
    },
    data: () => ({
        labelField: {
            name: '选项名称',
            placeholder: '请输入选项名称',
            value: '', // 通过之前的源码文档,我们得知初始的object其中的属性是响应式的
        },
        setting: {
            id: '',
            editor: 'SelectInputSetting',
            type: 'SelectInput',
            name: '',
            value: '',
            options:[],
            optionCount: 0, // 内部自增标识
        },
    }),
    methods: {
        addOption() {
            this.setting.options.push({
                name: '选项内容',
                placeholder: '请输入选项名称',
                value: '',
                key: this.setting.optionCount++
            })
        },
        deleteOption(index) {            
            this.setting.options.splice(index, 1); // 通过之前的源码文档,我们得知vue通过劫持数组原型方法实现数组响应式,splice就是其中之一
        },
    }
}
</script>

设置组件与预览表单的数据交互

分析

目前在设置组件SelectInputSetting中,通过setting收集用户输入,已经能够得到一份“下拉框组件定义”数据;

接下来,只要把这份数据传递到“表单预览”中,即可。所以我们需要实现它们之间的数据交互,通常来说有许多方案,但是考虑到用户操作性,数据交互可以通过:点击、拖拽等交互实现。在这里我们选用“拖拽”交互。

实现拖拽交互

实现拖拽交互,需要使用浏览器提供的一些API。

SelectInputSetting.vue
<template>
    <div>
        <TextInput draggable="true" :field="labelField" v-model="setting.name" @dragstart="dragstart" />
        <div class="option" v-for="(item, index) in setting.options" :key="item.key"> 
            <TextInput class="option-content" :field="item" v-model="setting.options[index].value" />
            <button @click="deleteOption(index)">删除</button>
        </div>
        <button @click="addOption">添加选项</button>
    </div>
</template>
<script>
import TextInput from './TextInput.vue';
export default {
    components: {
        TextInput
    },
    data: () => ({
        labelField: {
            name: '选项名称',
            placeholder: '请输入选项名称',
            value: '',
        },
        setting: {
            id: '',
            editor: 'SelectInputSetting',
            type: 'SelectInput',
            name: '',
            value: '',
            options:[],
            optionCount: 0,
        },
    }),
    methods: {
        addOption() {
            this.setting.options.push({
                name: '选项内容',
                placeholder: '请输入选项名称',
                value: '',
                key: this.setting.optionCount++
            })
        },
        deleteOption(index) {            
            this.setting.options.splice(index, 1);
        },
        // 开始拖动事件
        dragstart(e) {
            const dataStr = JSON.stringify(this.setting);
            e.dataTransfer.setData('application/json', dataStr);
        }
    }
}
</script>

draggable标识: 应用于HTML元素,用于标识元素是否允许使用浏览器原生行为或HTML 拖放操作 API拖动。true时元素可以被拖动。

dragstart事件: dragstart 事件在用户开始拖动元素或被选择的文本时调用。

通过HTML的拖放API,我们将数据传递通过event进行传递。

DynamicForm.vue
<template>
    <div class="container">
        <div class="main-area" @drop="addComponent" @dragover.prevent>
            <!-- 表单预览域 -->
            <div class="form-title">
                <TextInput :field="titleField" />
            </div>
            <div class="form-content" v-for="(item) in fields" :key="item.id">
                <component class="form-component" :is="item.type" :field="item" />
            </div>
        </div>
        <div class="sidebar">
            <!-- 表单组件域 -->
            <SelectInput v-model="componentValue" :field="createField" />
            <div>
                <component class="form-component" :is="componentValue" />
            </div>
        </div>
    </div>
</template>

<script>
import TextInput from './FieldTypes/TextInput.vue';
import TextInputSetting from './FieldTypes/TextInputSetting.vue';
import SelectInput from './FieldTypes/SelectInput.vue';
import SelectInputSetting from './FieldTypes/SelectInputSetting.vue';
export default {
    components: {
        TextInput,
        TextInputSetting,
        SelectInput,
        SelectInputSetting
    },
    data: () => ({
        titleField: {
            name: '表单名称',
            placeholder: '请输入表单名称',
            value: ''
        },
        componentValue: '',
        createField: {
            name: '选择要创建的组件',
            placeholder: '',
            value: '',
            options: [
                { 'value': 'TextInputSetting', 'name': '文本框' },
                { 'value': 'SelectInputSetting', 'name': '下拉单选框' },
            ]
        },
        fields: [],
    }),
    methods: {
        addComponent(e) {
            e.preventDefault(); // drop事件必须阻止默认行为

            const dataStr = e.dataTransfer.getData('application/json');
            const data = JSON.parse(dataStr);
            data.id = Date.now().toString(); // 添加一个唯一标识用于diff
            this.fields.push(data);
        }
    }
}
</script>

<style lang="scss" scoped>
.container {
    display: flex;
    border: 2px solid #000;
    padding: 10px;
}

.main-area {
    flex-grow: 4;
    margin-right: 10px;
    padding: 0 10px;
    border: 2px solid #000;
    border-radius: 10px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;

    .form-title {
        width: auto;
        text-align: center;
        margin-bottom: 8px;
    }

    .form-content {
        border-radius: 10px;
        padding: 8px;
        width: 90%;
        border: 1px solid #ccc; // 默认边框

        .form-component {
            width: 300px;
        }

        &:hover {
            border: 1px solid #ccc;
            cursor: all-scroll;
        }
    }
}

.sidebar {
    display: flex;
    flex-direction: column;
    border: 2px solid #000;
    border-radius: 10px;
    padding: 10px;
    flex-grow: 1;

    .form-component {
        border: 1px solid #555;
        border-radius: 10px;
        padding: 8px;
        margin-bottom: 10px;
    }

    label {
        margin-bottom: 5px;
    }

    input {
        margin-top: 10px;
        padding: 5px;
        border: 1px solid #000;
        border-radius: 5px;
    }
}
</style>

drop事件: 事件在元素或文本选择被放置到有效的放置目标上时触发。为确保 drop 事件始终按预期触发,应当在处理 dragover 事件的代码部分始终包含 preventDefault() 调用。

dragover.prevent: 只有在 dragenterdragover 事件中调用了 event.preventDefault(),浏览器才会允许元素的拖放操作。

通过HTML的拖放API,我们将数据传递通过event进行接接收。

功能完善

  • SelectInputSetting中,选项删除按钮在当前选项hover时才出现
<style lang="scss" scoped>
    .option {
        button {
            visibility: hidden;
        }
        &:hover {
            button {
                visibility: visible;
            }
        }
    }
</style>

使用visibility属性的修改只会触发重绘,使用display实现的话会触发重排。这个跟使用v-show还是v-if的问题相似;

使用css元素实现而不是vue指令,在于css更好控制:hover

  • 组件设置完成后应该有保存选项来进行锁定,避免误操作
<!-- TextInput -->
<template>
  <div class="text-input-container">
    <label :for="field.name" class="text-input-label">{{ field.name }}:</label>
    <input class="text-input" type="text" :placeholder="field.placeholder" :value="value"
      @input="$emit('input', $event.target.value)" :required="field.required" :readonly="disabled" />
  </div>
</template>

<script>
export default {
  props: ['field', 'disabled', 'value'],
}
</script>
<!-- SelectInputSetting -->
<template>
    <div :class="{'drag':isFinished}" :draggable="isFinished" @dragstart="dragstart">
        <TextInput :disabled="isFinished" :field="labelField" v-model="setting.name" />
        <!-- 这里如果不使用item.key而是使用index,会因为节点复用导致显示错误 -->
        <div class="option" v-for="(item, index) in setting.options" :key="item.key"> 
            <TextInput :disabled="isFinished" class="option-content" :field="item" v-model="setting.options[index].value" />
            <button v-show="!isFinished" @click="deleteOption(index)">删除</button>
        </div>
        <button v-show="!isFinished" @click="addOption">添加选项</button>
        <button v-show="!isFinished" @click="finish">完成</button>
    </div>
</template>
<script>
import TextInput from './TextInput.vue';
export default {
    components: {
        TextInput
    },
    data: () => ({
        labelField: {
            name: '选项名称',
            placeholder: '请输入选项名称',
            value: '',
        },
        
        setting: {
            id: '',
            editor: 'SelectInputSetting',
            type: 'SelectInput',
            name: '',
            value: '',
            options:[],
            optionCount: 0
        },
        isFinished: false,
    }),
    methods: {
        addOption() {
            this.setting.options.push({
                name: '选项内容',
                placeholder: '请输入选项名称',
                value: '',
                key: this.setting.optionCount++
            })
        },
        deleteOption(index) {            
            this.setting.options.splice(index, 1);
        },
        dragstart(e) {
            const dataStr = JSON.stringify(this.setting);
            e.dataTransfer.setData('application/json', dataStr);
        },
        finish() {
            this.isFinished = true;
        },
    }
}
</script>
<style lang="scss" scoped>
.drag {
    &:hover {
        cursor: all-scroll; // 修改鼠标样式,更符合移动组件的暗示
    }
}
</style>

小结

至此我们已经完成了一个相对简单的动态表单组件。能从中体会组件的设计思想、代码组织,并且了解到一些具体实现需要调用的API。

接下来我们还将继续实现类似的有趣实践——导航栏

微信闪照小程序实现

已经有一年半没有写文章了,今天给掘友们写一个闪照实现的demo,纯前端开发技术栈为uniapp+uni云开发;先贴出代码

首先是闪照的几个要点(小程序申请注册啥的就不说了,只说功能)

  1. 上传图片到uni云存储空间
  2. 上传图片需要做违规检测
  3. 闪照需要分享出去,微信分享功能
  4. 查看闪照时需要限时查看和防止手机截屏

image.png

image.png

<view wx:if="{{!isBlackScreen}}" class="page-container {{isBlackScreen ? 'black-screen' : ''}} container">
<view class="upload-area">
<up-upload :fileList="fileList" @afterRead="afterRead" @delete="deletePic" name="1" multiple :maxCount="1"
width="400" height="500">
</up-upload>
</view>
<!-- 按钮区域 -->
<view class="button-group">
<!-- 隐藏的上传组件 -->
<up-upload ref="uploadRef" :fileList="fileList" @afterRead="afterRead" @delete="deletePic" name="1" multiple
:maxCount="1" style="display: none;"></up-upload>
<u-button class="action-button" shape="circle" icon="photo" text="选择照片" @click="handleSelectPhoto" />
<u-button class="action-button share-button" shape="circle" icon="share" text="分享" open-type="share" :disabled="!canShare" />
</view>
<custom-tabbar :current="currentTab"></custom-tabbar>
</view>
<view wx:if="{{isBlackScreen}}" class="black-screen-overlay">
<text>禁止截图或录屏</text>
</view>
</template>

<script setup>
import {
ref
} from 'vue'
import {
onLoad,
onShow,
onNavigationBarButtonTap,
onPullDownRefresh,
onReachBottom,
onUnload,
onShareAppMessage
} from '@dcloudio/uni-app';
import CustomTabbar from '../components/custom-tabber.vue'
const currentTab = ref(0) //tabbar
const fileList = ref([]);
const subscribeNotify = ref(false);
const allowForward = ref(false);
const uploadRef = ref(null);
const canShare = ref(false); // 新增:控制是否允许分享
const handleSelectPhoto = () => {
// 手动触发上传组件的选择文件
uploadRef.value?.chooseFile();
};
const isBlackScreen = ref(false) // 是否显示黑屏
onLoad(() => {
wx.showShareMenu({
menus: ['shareAppMessage', 'shareTimeline'],
success() {
console.log('分享功能已启用')
}
})
wx.onUserCaptureScreen(() => {
this.setData({
isBlackScreen: true
}); // 触发黑屏

// 3秒后恢复(可选)
setTimeout(() => {
this.setData({
isBlackScreen: false
});
}, 3000);
});

})
onLoad(() => {

})
onUnload(() => {
wx.offUserCaptureScreen(); // 移除监听
});
onShareAppMessage(() => {
if (!canShare.value || !fileID.value) {
uni.showToast({
title: '请先上传图片',
icon: 'none'
});
return {};
}
console.log(fileID.value); // 查看 fileID 是否正常
return {
title: '查看闪照',
path: '/pages/viewImg/viewImg?fileID=' + fileID.value, // 带参数的分享路径
imageUrl: '/static/sz.png', // 分享图片
success(res) {
uni.showToast({
title: '分享成功'
})
},
fail(err) {
console.log('分享失败', err)
}
}
})
// 删除图片
const deletePic = (event) => {
fileList.value.splice(event.index, 1);
canShare.value = false; // 删除图片后禁止分享
};
const toview = () => {
uni.navigateTo({
url: '/pages/viewImg/viewImg?fileID=' + fileID.value, // 带参数的分享路径
})
}
const handleToTop = () => {
uni.navigateTo({
url: '/pages/wgbtop/wgbtop',
})
}
const afterRead = async (event) => {
fileList.value = []
canShare.value = false; // 开始上传时先禁止分享
let lists = [].concat(event.file);
console.log('选择的文件:', lists);
let fileListLen = fileList.value.length;

// 更新UI状态
lists.map((item) => {
fileList.value.push({
...item,
status: 'checking',
message: '安全检测中',
});
});

// 显示加载中状态
uni.showLoading({
title: '正在加载中...',
mask: true
});

// 读取文件的辅助函数
const readFileContent = async (fileItem) => {
// H5环境
if (fileItem.file && fileItem.file instanceof File) {
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = reject;
reader.readAsArrayBuffer(fileItem.file);
});
}
// 小程序环境
else if (fileItem.url) {
return await new Promise((resolve, reject) => {
uni.getFileSystemManager().readFile({
filePath: fileItem.url,
encoding: 'binary',
success: res => resolve(res.data),
fail: reject
});
});
}
throw new Error('不支持的文件类型');
};

for (let i = 0; i < lists.length; i++) {
let uploadResult = null;
try {
// 更新状态为上传中
fileList.value[i].status = 'uploading';
fileList.value[i].message = '正在加载...';

// 读取文件内容
const fileContent = await readFileContent(lists[i]);

// 更新状态为检测中
fileList.value[i].status = 'checking';
fileList.value[i].message = '安全检测中...';
// 调用云函数进行安全检测
const checkResult = await uniCloud.callFunction({
name: 'imgSecCheck',
data: {
fileContent: fileContent
}
});
if (checkResult.result.code !== 0) {
throw new Error(checkResult.result.message || '图片安全检测未通过');
}
console.log('安全检测通过', checkResult);
// 更新状态为上传中
fileList.value[i].status = 'uploading';
// fileList.value[i].message = '正在上传...';
// 安全检测通过后再上传到uniCloud
uploadResult = await uploadToUniCloud(lists[i]);

let item = fileList.value[fileListLen];
fileList.value.splice(fileListLen, 1, {
...item,
status: 'success',
message: '加载成功',
url: uploadResult.fileID,
});
fileListLen++;

// 上传成功,允许分享
canShare.value = true;

// 隐藏加载中
uni.hideLoading();
uni.showToast({
title: '加载成功',
icon: 'success',
duration: 2000
});
} catch (error) {
console.error('检测或上传失败:', error);
let item = fileList.value[fileListLen];

let message = '上传失败,图片可能包含违规内容';
if (error.message && error.message.includes('违规')) {
message = '图片包含违规内容';
} else if (error.message && error.message.includes('大小')) {
message = '图片大小超过限制(10MB)';
} else if (error.errMsg && error.errMsg.includes('fail')) {
message = '安全检测服务异常';
}
fileList.value.splice(fileListLen, 1, {
...item,
status: 'failed',
message: message,
});
fileListLen++;
// 上传失败,禁止分享
canShare.value = false;
// 隐藏加载中并显示错误
uni.hideLoading();
uni.showToast({
title: message,
icon: 'none',
duration: 3000
});
// 如果上传了文件但检测失败,删除已上传的文件
if (uploadResult && uploadResult.fileID) {
try {
await uniCloud.deleteFile({
fileList: [uploadResult.fileID]
});
console.log('已删除未通过检测的文件');
} catch (deleteError) {
console.error('删除文件失败:', deleteError);
}
}
}
}
};
// 上传到uniCloud云存储
const fileID = ref()
const uploadToUniCloud = async (fileItem) => {
// 如果是H5环境且有原始File对象
if (fileItem.file && process.env.VUE_APP_PLATFORM === 'h5') {
// H5方式上传
const cloudPath = 'uploads/' + Date.now() + '-' + fileItem.file.name + '.png';
const res = await uniCloud.uploadFile({
filePath: fileItem.file,
cloudPath: cloudPath
});
fileID.value = res.fileID
return res;
} else {
// 小程序/APP方式上传
const cloudPath = 'uploads/' + Date.now() + '-' + Math.random().toString(36).substring(2) + '.png';
const res = await uniCloud.uploadFile({
filePath: fileItem.url,
cloudPath: cloudPath
});
fileID.value = res.fileID
console.log(fileID.value)
return res;
}
};
onShow(() => {
uni.hideTabBar()
// 根据当前页面设置currentTab
const pages = getCurrentPages()
const page = pages[pages.length - 1]
const route = page.route
if (route === 'pages/index/index') {
currentTab.value = 0
} else if (route === 'pages/wgbtop/wgbtop') {
currentTab.value = 1
} else if (route === 'pages/user/user') {
currentTab.value = 2
}
})
</script>

<style lang="scss" scoped>
.container {
// padding: 24rpx;
padding: 20rpx;
box-sizing: border-box;
background-color: #f8f8f8;
min-height: 100vh;
}

.black-screen-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: black;
color: white;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}

.upload-area {
height: 1000rpx;
// background-color: #fff;
border-radius: 16rpx;
margin-bottom: 32rpx;
display: flex;
align-items: center;
justify-content: center;
// box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}

.button-group {
display: flex;
justify-content: space-between;
margin-bottom: 32rpx;

.action-button {
flex: 1;
height: 80rpx;
font-size: 28rpx;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
border: none;
color: #333;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);

&:active {
opacity: 0.9;
}

&.share-button {
margin-left: 24rpx;
background: linear-gradient(135deg, #3c9cff 0%, #2b85e4 100%);
color: #fff;

&.u-button--disabled {
opacity: 0.6;
}
}
}
}

.settings-section {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);

:deep(.u-cell) {
padding: 28rpx 32rpx;
}

:deep(.u-cell_title) {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
}
</style>
1.上传图片到uni云存储空间

首先上传图片用两个地方,一个是上传组件,一个是点击上传的按钮,所以我写了两个up-upload组件,一个是显示的,一个是隐藏的,隐藏的组件用于实现按钮点击上传,使用uploadRef.value?.chooseFile();来手动触发;我使用的是uni的云存储方法为uniCloud.uploadFile(),需要这个uniapp账号开通了云存储空间,可以免费开通看不懂的话可以点击这段去uni的云存储板块看教程

2.上传图片需要做违规检测

第二点就是上传时需要做违规检测,如果用户上传了色情恐怖等等就不让检测上传分享了,这一块微信有提供检测的api————api.weixin.qq.com/wxa/img_sec… ;检测的api有两个一个只需要token就可以了;我使用的就是这个。另一个需要用户的openid,由于我没有做登录所以要openid的我就没有使用,这一块主要是获取token去调用这个检测接口,我使用的是uni的云函数代码如下

exports.main = async (event, context) => {
  // 获取微信access_token
  const getAccessToken = async () => {
    const res = await uniCloud.httpclient.request(
      'https://api.weixin.qq.com/cgi-bin/token', 
      {
        method: 'GET',
        data: {
          grant_type: 'client_credential',
          appid: 替换为你的小程序AppID
          secret:替换为你的小程序AppSecret
        },
        dataType: 'json'
      }
    )
    return res.data.access_token
  }

  try {
    const access_token = await getAccessToken()
    
    // 阿里云不支持downloadFile,直接从event中获取文件内容
    const fileContent = event.fileContent
    
    // 调用微信安全检测接口
    const result = await uniCloud.httpclient.request(
      `https://api.weixin.qq.com/wxa/img_sec_check?access_token=${access_token}`,
      {
        method: 'POST',
        content: fileContent,
        headers: {
          'Content-Type': 'application/octet-stream'
        },
        dataType: 'json'
      }
    )
    
    if (result.data.errcode === 0) {
      return {
        code: 0,
        message: '检测成功',
        data: result.data
      }
    } else {
      return {
        code: result.data.errcode || -1,
        message: result.data.errmsg || '检测失败',
        data: result.data
      }
    }
  } catch (error) {
    return {
      code: -2,
      message: error.message || '检测异常',
      data: error
    }
  }
}
3.闪照需要分享出去,微信分享功能

微信的分享功能这一块没有啥好说的很简单,给按钮加上open-type="share",然后吧分享功能打开通过onShareAppMessage方法就可以分享了 主要代码如下

<u-button class="action-button share-button" shape="circle" icon="share" text="分享" open-type="share" :disabled="!canShare" />


wx.showShareMenu({
menus: ['shareAppMessage', 'shareTimeline'],
success() {
console.log('分享功能已启用')
}
})
                
                onShareAppMessage(() => {
if (!canShare.value || !fileID.value) {
uni.showToast({
title: '请先上传图片',
icon: 'none'
});
return {};
}
return {
title: '查看闪照',
path: '/pages/viewImg/viewImg?fileID=' + fileID.value, // 带参数的分享径
imageUrl: '/static/sz.png', // 分享图片
success(res) {
uni.showToast({
title: '分享成功'
})
},
fail(err) {
console.log('分享失败', err)
}
}
})
4.查看闪照时需要限时查看和防止手机截屏

第四点主要是通过css模糊效果结合定时器来实现;判断是否看过的字段我存储在了本地存储中,防君子不防小人。防截屏使用的是微信提供的wx.setVisualEffectOnCapture方法;具体代码如下

<template>
<view class="image-container">
<!-- 使用两层图片结构,一层模糊层,一层清晰层 -->
<image v-if="isBlurred" :src="imageSrc" mode="widthFix" class="blur-layer" 
@touchstart="handleTouchStart" @touchend="handleTouchEnd" @touchcancel="handleTouchEnd" />
<image :src="imageSrc" mode="widthFix" :class="['sharp-layer', { 'visible': !isBlurred }]" 
@touchstart="handleTouchStart" @touchend="handleTouchEnd" @touchcancel="handleTouchEnd" />
<view v-if="hasViewed" class="hint-text">
<up-button :plain="true" class="" style="margin-top: 40rpx;width: 180rpx;" size='mini'
@click="toIndex">我也要发照片</up-button>
</view>
<up-modal :show="show" :title="title" :content='content' @confirm="confirm" :closeOnClickOverlay="true"
showCancelButton='true' @cancel='cancel'></up-modal>
<view v-if="showBlackScreen" class="black-screen">
<text class="hint-text">禁止截屏</text>
</view>
</view>
</template>

<script setup>
import {
ref
} from 'vue'
import {
onLoad,
onShow,
onNavigationBarButtonTap,
onPullDownRefresh,
onUnload,
onHide
} from '@dcloudio/uni-app';
import {
onUnmounted
} from 'vue';
const imageSrc = ref(
'https://mp-57911374-353d-4222-b8c2-1a8948d61be7.cdn.bspapp.com/cloudstorage/4e16e15d-6660-4c24-af36-d6886d1e3a7e.'
)
const isBlurred = ref(true)
const hasViewed = ref(false) // 是否已经查看过
let timer = null
const show = ref(false);
const title = ref('提示');
const content = ref('您已经查看过该图片');
const imgArray = ref([])

onLoad((options) => {
if (uni.getStorageSync('imgArray')) {
imgArray.value = uni.getStorageSync('imgArray')
}
if (options) {
imageSrc.value = options.fileID
const isExist = imgArray.value.some(item => item === imageSrc.value);
if (isExist) {
hasViewed.value = true
isBlurred.value = true // 修改这里:已经查看过的图片保持模糊状态
} else {
hasViewed.value = false
isBlurred.value = true
}
}
wx.setVisualEffectOnCapture({
visualEffect: 'hidden',
});
})

onHide(() => {
wx.setVisualEffectOnCapture({
visualEffect: 'none',
});
})

onUnload(() => {
wx.setVisualEffectOnCapture({
visualEffect: 'none',
});
})

const handleTouchStart = () => {
// 已经查看过,直接显示提示
if (hasViewed.value) {
show.value = true
return
}

// 清除之前的定时器
clearTimeout(timer)
// 立即显示清晰图片
isBlurred.value = false

// 设置2秒后自动恢复模糊
timer = setTimeout(() => {
isBlurred.value = true
hasViewed.value = true // 标记为已查看
imgArray.value.push(imageSrc.value)
uni.setStorageSync('imgArray', imgArray.value); //存本地
}, 2000)
}

const handleTouchEnd = () => {
// 已经查看过的不处理
if (hasViewed.value) return
// 如果触摸时间不足2秒就松手,也恢复模糊并标记为已查看
clearTimeout(timer)
isBlurred.value = true
hasViewed.value = true
imgArray.value.push(imageSrc.value)
uni.setStorageSync('imgArray', imgArray.value);
}

// 去看广告
const confirm = () => {
show.value = false
};
// 不看
const cancel = () => {
show.value = false
};

onShow(() => {
wx.setVisualEffectOnCapture({
visualEffect: 'hidden',
});
})

const toIndex = () => {
uni.switchTab({
url: '/pages/index/index'
})
}

onUnmounted(() => {
clearTimeout(timer)
});
</script>

<style scoped>
/* 容器确保图片比例不变形 */
.image-container {
width: 100%;
height: 80vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}

.black-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}

/* 模糊层 */
.blur-layer {
width: 100%;
display: block;
position: absolute;
filter: blur(22px);
transform: scale(1.02);
transition: opacity 0.5s ease;
}

/* 清晰层 */
.sharp-layer {
width: 100%;
display: block;
position: absolute;
opacity: 0;
transition: opacity 0.5s ease;
}

.sharp-layer.visible {
opacity: 1;
}

.hint-text {
position: absolute;
bottom: 50%;
left: 0;
right: 0;
text-align: center;
color: white;
padding: 10rpx 20rpx;
border-radius: 10rpx;
margin: 0 auto;
width: max-content;
z-index: 10;
}

/* 性能优化 */
@media (prefers-reduced-motion: reduce) {
.blur-layer, .sharp-layer {
transition: none;
}
}
</style>

到这里整个功能就已经实现完了,主要两个页面一个是上传图片和分享的页面;一个是查看闪照的页面。整篇文章都是干货无划水;喜欢的朋友可以点赞收藏一下,感谢了。下一篇我会分享纯前端实现的情侣互动点餐小程序。

页面点击跳转源代码?——element-jumper插件实现

前言

在开发公司或个人大型项目时,很多人都会碰到这样的困扰:

明明是简单的功能需求,比如在页面底部加个按钮,却不知道该从代码里的哪个组件入手。我们往往要花大量时间去寻找页面内容和源代码的对应关系,这种耗时在简单功能开发时显得尤为突出。

有没有插件能解决这个问题呢?答案就在这篇文章里。本系列将带大家从 0 开始,深入原理,一步步实现一个能从页面直接跳转至对应源代码的实用插件——element-jumper

本项目已经开源以及发布npm包,欢迎小伙伴们自行测试:gitbub传送门点点star谢谢喵

系列文章(WIP)

  1. 页面点击跳转源代码?——element-jumper插件实现(本文)
  2. element-jumper插件实现之BabelPlugin
  3. element-jumper插件实现之WebpackPlugin

通过这篇文章能学到什么?

  1. element-jumper的基本概念以及功能
  2. element-jumper整体功能拆解思路
  3. element-jumper各部分原理概述

一、基本概念

相信经过前文的简单介绍依然有不少同学对此插件的功能存在困惑,所以本章节我们来讲讲本插件的基本概念以及这个想法是如何产生的等等。

  • 想法诞生——从 “找代码困境” 到 “自制插件”

主播刚结束为期三个月的第一段实习,刚进公司的时候由于新人文档还不是特别完善以及本人有一点小小的社恐,导致landing期间就只是配置了环境,而对于很多提效的插件了解的很少。

于是在接到第一个需求准备大展身手时,陷入了找不到代码的困境,这个时候mt向我介绍了公司研发的代码定位插件,处于开发模式下,按住快捷键(Ctrl+Shift+某字母),再点击页面上对应的组件,就直接跳转到了vscode对应的代码中。从此,我不用在浩如烟海的代码里苦苦搜寻,能愉快地投入需求开发了。

这个插件给当时的我带来了极大震撼,于是我决定花时间研究其原理,复刻一个属于自己的代码定位插件 ——element-jumper(虽然翻译不算专业,但 “jumper” 一词自带灵动感,便沿用了这个名字)。经过两个月的学习和编码,终于成功做出了一个能通过自测的插件。

  • 功能概述——页面与代码的 “一键直达”

通过刚刚不清不楚的描述相信大家对此插件的功能已经有了初步的理解,下面对element—jumper的概念和功能做一个小小的总结:

该插件专为解决开发中的 “代码定位难题” 设计,在开发模式下,用户只需按住特定快捷键并点击页面上的目标组件(如按钮、文本框等),插件就能自动定位到该组件在源代码中的位置,并直接跳转至 VS Code对应的代码区域,帮助开发者跳过 “找代码” 的耗时环节,快速进入功能开发阶段。

二、需求拆解

对功能有了了解之后,正在看文章的你或许很激动的想要投入开发。但是在这之前,我们需要对功能进行“拆解”,这个步骤不管是在平时项目的练习,还是公司需求的开发中都显得尤为关键,能够帮助开发者评估工作量以及为后续开发过程奠定思路。

对此我总结出了一套流程来快速的拆解一个比较大的需求,大家可以对比学习:

  1. 明确功能:即用一段文字去准确的描述需求的功能,这个步骤我们在前文已经完成了;

  2. 提炼关键词:提取刚才那段文字中最核心的词语,这里比如:点击页面组件、源代码位置、跳转vscode等等;

  3. 逻辑连接:将提炼到的关键词进行逻辑组合(比如时间顺序,先A后B等等),那么针对代码定位插件,逻辑连接如下:首先需要点击页面上的元素或者组件,其次获取它在源代码中的位置,最后根据位置进行vscode的跳转。(时间顺序)

  4. 针对每一个分句进行提问和回答:这一步可以借助工具和查阅文档,针对本插件,提问和回答如下,

    • 怎么确定点击的是哪一个组件?——可以在每一个组件外部都包裹一个自定义的透明深色组件,在hover的时候予以显示,类似浏览器开发者工具,如图就代表你的点击覆盖的范围。

    123.png

    • 怎么获取组件的位置?—— 可以通过在打包等时候遍历代码的AST,并对对应的行列信息进行存储。
    • 怎么跳转VSCode?——VSCode 支持通过特殊协议链接被外部调用,格式为vscode://,比如vscode://file/{文件路径}:{行号}:{列号}这个链接能直接让 VS Code 打开指定文件,并定位到具体行列位置。

至此,我们便完成了基本的需求拆解,可以通过画图来加深理解,后续也方便参考。

456.png

三、功能概述

由于本插件的实现涉及 Webpack 插件、Babel 插件、AST 等较多复杂技术知识点,因此作为系列文章的开篇,本文更侧重于从整体视角展开介绍,仅对核心功能的实现原理进行关键提示,暂不做过于细致的技术阐述。

后续章节将针对前文拆解的各个步骤(如组件与源代码的关联机制、协议链接的生成逻辑等),分别进行深入的技术分析和具体代码实现的详细讲解。

3.1 遮罩组件

该环节的实现逻辑相对清晰,核心可拆解为两大步骤:遮罩组件的本体开发组件的自动化注入机制。简单来说,需先自定义一个无实际内容、只有颜色遮罩组件,随后在开发环境模式下,为应用中所有渲染的组件自动包裹一层该自定义遮罩组件。

在遮罩组件的实现层面,需根据项目所采用的技术栈选择对应的开发方式。如 React 或者 Vue 组件,值得注意的是,若需实现跨框架兼容 —— 即让遮罩组件能在 ReactVue 等不同框架构建的项目中通用,WebComponent 技术方案会是更优选择。采用纯 JavaScript 编写的 WebComponent 组件,具备原生HTML标签的使用特性,可直接通过<组件标签名>的形式在页面中引用,无需依赖特定框架的编译或运行时环境,从而有效降低跨框架适配的复杂度。

这里用react组件做示例(element—jumper中使用的是webcomponent

const MaskOverlay = ({ children }) => {
    return (
     // 遮罩容器:通过定位覆盖子内容,不影响原始布局
     <div className="mask-overlay-container">
       <div className="mask-overlay-content">
           {children}
       </div>
     </div>
    );
};
export default MaskOverlay;

//css
.mask-overlay-container {
    /* 半透明背景*/
    background-color: rgba(230, 230, 230, 0.3);
    /* 继承父元素尺寸,确保完全覆盖子内容 */
    width: 100%;
    height: 100%;
    /* 相对定位:避免影响页面布局流 */
    position: relative;
}

.mask-overlay-content {
    /* 子内容容器:保持原始内容布局 */
    width: 100%;
    height: 100%;
}

在组件注入的实现上,核心逻辑与前文提及的 AST(抽象语法树) 密切相关,而与遮罩组件自身的业务代码关联度较低。这部分内容将在后续小节中,结合 AST 的具体操作进行简要说明。

3.2 babel—pluginAST的遍历与操作

(不清楚的同学可以先学习babelAST相关知识)

结合前文内容,想必你已清晰这个 Babel 插件的核心目标 ——精准获取组件在源代码中的行列位置信息,并自动完成遮罩组件的注入操作。

这里有个值得思考的细节:行列信息获取后该如何存储?既要保证每个组件的信息独立不混淆,又不能对页面其他功能产生干扰。此时我们会发现,即将注入的遮罩组件恰好是理想的存储载体:每个组件外层都有独立的遮罩组件包裹,且不会影响原始内容的展示与交互。因此,将行列信息以属性形式挂载到遮罩组件上,无疑是巧妙且合理的解决方案,这也是整个项目实现中的一个关键亮点。

这部分功能的逻辑框架并不复杂,但需要扎实掌握 Babel 插件开发和 AST 处理的相关知识,例如通过path对象获取节点位置信息等操作。下面为获取组件行列信息的实现思路举例说明:

module.exports = function({ types: t }) {
  return {
    visitor: {
      JSXElement(path, state) {
        //通过this.file获取当前文件信息
        const filename = this.file.opts.filename;
        // 跳过特定文件(如开发覆盖层组件本身)
        if (filename && filename.endsWith('devOverlay.jsx')) return;
        
        //通过path获取JSX元素的位置信息
        const loc = path.node.openingElement.loc;
        if (!loc || !loc.start) return;
        
        // 获取行列信息并生成唯一的debugId
        const { line, column } = loc.start;
        const debugId = `cmp-${line}-${column}`;
        //...后续代码
      }
    }
  };
};
3.3 webpack-plugin:跳转实现

由于webpack插件hooks的多样化,这里的跳转实现思路有很多,由于是复刻的项目,所以我选择了直接向项目资产html文件(即emit钩子)注入全局点击事件监听以及跳转逻辑。

前文提到我们已经把行列以及文件信息注入到了遮罩组件的属性中,现在直接对应取出并补充完整vscode协议即可,以下是关键代码实现:

 apply(compiler) {
    // 使用Webpack的emit钩子(资产输出前触发)
    compiler.hooks.emit.tapAsync('VscodeJumpPlugin', (compilation, callback) => {
      try {
        // 找到所有HTML资产(通常是index.html)
        const htmlAssets = Object.keys(compilation.assets).filter(filename => 
          filename.endsWith('.html')
        );
        // 处理每个HTML文件
        htmlAssets.forEach(filename => {
          // 获取原始HTML内容
          const originalHtml = compilation.assets[filename].source();

          // 注入点击监听脚本
          const injectScript = `
            <script>
              document.addEventListener('click', (e) => {
                // 查找带目标属性的元素
                const attrNames = ['${this.attrs.file}', '${this.attrs.line}', '${this.attrs.column}'];
                //处理内容
                const targetEl = e.target.closest(
                  attrNames.map(attr => \`[\${attr}]\`).join('')
                );
                if (!targetEl) return;
                // 提取属性信息
                const file = targetEl.getAttribute('${this.attrs.file}');
                const line = targetEl.getAttribute('${this.attrs.line}');
                const column = targetEl.getAttribute('${this.attrs.column}');

                if (!file || !line || !column) return;

                // 处理Windows路径并跳转
                const normalizedFile = file.replace(/\\\\/g, '/');
                const encodedFile = encodeURIComponent(normalizedFile);
                const vscodeUrl = \`vscode://file/\${encodedFile}:\${line}:\${column}\`;
                window.open(vscodeUrl, '_blank');
              });
            </script>
          `;

          // 将脚本插入到</body>前
          const modifiedHtml = originalHtml.replace('</body>', `${injectScript}</body>`);

          // 更新资产内容
          compilation.assets[filename] = {
            source: () => modifiedHtml,
            size: () => modifiedHtml.length
          };
        });
      } catch (e) {
        console.error('插件处理失败:', e);
      }
      callback();
    });
  }
}

至此,代码定位功能的核心实现步骤已为大家梳理完毕,相信你对整体开发思路已有了初步框架。但正如前文所说,本文作为系列开篇更侧重思路概述,对技术细节的展开较为有限。这里提前抛出几个关键细节问题(后续文章会逐一深入解答并实战演示):

  1. 怎么手动实现代码定位模式的开关控制?(即快捷键功能)
  2. 怎么实现开发模式(dev mode)的判断和注入?
  3. babel-plugin中如何对组件进行“筛选”?(<div> <p>等原生标签怎么排除?)

四、总结

  • 再次强调文章定位,本文遵循 “问题→目标→拆解→方案→展望” 的技术分享逻辑,分享了代码定位插件的相关内容,侧重逻辑思路而略写了技术性和知识性的相关内容,这些隐去的内容也会在后续文章进行补充。感兴趣的同学可以在评论区写下问题,后面会发文章解答。
  • github传送门(Zestia-l (Juicetone) · GitHub) ~
  • element-jumper传送门(GitHub - Zestia-l/element-jumper) ~

TypeScript:联合类型可以转化为元组类型吗?数组如何用联合类型逐项约束?

如何用联合类型约束数组类型

联合类型能转化为元组类型吗.png

TypeScript 类型体操中,我希望能用联合类型约束数组(或元组)类型,实现更强的类型安全。本文将围绕这个主题展开,介绍常见的思路、局限与解决方案。

1. 联合类型与元组类型的关系

我们知道,TypeScript 可以很容易地将元组类型转化为联合类型:

const tuple = ['a', 'b', 'c'] as const; // as const 不能遗漏哦
type Union = (typeof tuple)[number]; // 'a' | 'b' | 'c'

但反过来,在进行了一些徒劳的尝试和资料查询后发现:

无法直接将联合类型转化为元组类型(;´д`)ゞ。

因为联合类型本质上是无序的集合,而元组类型是有序的列表,TypeScript 类型系统无法保证顺序和长度。

2. 如何实现精准的类型约束

虽然不能直接将联合类型转为元组,但我们依然可以做到:

  • 让数组的元素类型受联合类型约束
  • 任何一方缺失都会在类型检查阶段暴露

3. 开始操作

假设这样一个场景,我们在做参数校验:

有一个如下的联合类型的入参。现在,我们要设定一个数组,使用includes方法来确定入参的有效性,但是入参类型可能会随着版本变化而变化,我们希望类型提示能帮助我们快速发现、更新。

type InputVersion = 'latest' | 1 | 2;
  1. 先给出想要的数组,以as const约束它并提取元组类型
const v = ['latest', 1, 2] as const;
type ArrVersion = (typeof v)[number]; // 'latest' | 1 | 2
  1. 对比类型ArrVersionInputVersion是否相同
type What = IsSameType<ArrVersion, InputVersion>;
  1. 给一个变量显式声明此类型并赋值
const what: What = 1; // 如果ArrVersion和InputVersion不一致,这行代码会因类型不匹配而报错。
// 严谨版本这里可以赋值为true,具体什么值都没有关系,选择自己喜欢的就好

Note: 这个赋值语句将会在编译期间被打包工具terser消除,因其未使用过。所以无需担心最终结果多出赋值语句。 (o゜▽゜)o

4. 实现 IsSameType 工具类型

直观版本

从数学理论讲,关键字extends类似于“偏序关系”,因此对于偏序关系而言,只要a ≤ bb ≤ a同时成立,那么就可以得到a = b。因此,我们以三元运算符来做到这件事,一个直接的想法是这样:

// 这是直观版本,但不是最优,最优请见下方的“严谨版本”
type IsSameTypeIntuitionistic<A, B> = A extends B ? (B extends A ? 1 : 2) : 2;

Note: 如果用truefalse,那么此泛型工具会永远返回boolean类型从而失去判断能力。

Note: 不使用10是因为0作为falsy的值性质略有区别,可能使得推断结果和约束行为不如预期。

严谨版本

TypeScript 存在“分布式展开”行为,当类型入参存在 never 时,分布式展开会直接返回 never,不会进入分支。以下是社区体操之神( (#°Д°)?)提供的严谨版本:

// 严谨版本
type IsSameType<A, B> =
  (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;

此写法阻止了“分布式展开”行为,边界情况都可以照顾到,适用范围更广。

总结

  • 元组类型可以转为联合类型,但联合类型无法直接转为元组类型。
  • 可以用联合类型约束数组元素类型,实现基本的类型安全。
  • 进一步约束时,可以用 IsSameType 工具类型判断类型集合是否完全一致。

(❁´◡`❁) 感谢你读到这里!

【前端特效系列】css+js实现聚光灯效果

✨ 前言

源码地址:leixq1024/FrontEndSnippetHub: ✨html+css+js的前端特效合集

本次灵感来源:codepen.io/zorgos/pen/…

这个系列主要分享一些用css+html+js制作的前端特效或动画的代码实现思路和解析。如果对你有帮助请给仓库点一个✨

🎬 效果演示

聚光灯演示效果

🧰 前期准备

这里我准备了两个图片一个是地图,一个是火把gif,并且创建了index.htmlstyle.cssindex.js三个文件

image.png

🗺️ 初始化场景

index.html

<!DOCTYPE html>
<html lang="zh-cn">
  <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="map">
      <!-- 黑色遮罩 -->
      <div class="mask" id="mask"></div>
      <!-- 火把gif,随光圈移动 -->
      <img id="torch" src="./img/torch.gif" alt="火把" />
    </div>
    <script src="index.js"></script>
  </body>
</html>

刚开始没有设置样式效果就如下

image-20250817113125370

🎨 编写样式

先把地图放上去

html,
body {
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: none;
}
/* 地图 */
.map {
  position: relative;
  width: 100vw;
  height: 100vh;
  background: url('./img/map.png') no-repeat;
  background-size: 100% 100%;
}

效果如下

image-20250817113413388

接下来做一个黑色的背景,并且用mask-image做一个蒙版

.mask {
  position: absolute;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 1);
  mask-image: radial-gradient(
    circle var(--r, 110px) at var(--x, 50%) var(--y, 50%),
    transparent 0%,
    transparent 50%,
    black 100%
  );
  transition: -webkit-mask-position 0.06s linear;
  transition: mask-position 0.06s linear;
  pointer-events: auto;
}
  • 通过mask-image的径向渐变创建圆形透明区域
  • transparent 0%transparent 50%:中心区域完全透明(显示底层内容)
  • black 100%:边缘黑色区域遮挡内容
  • 最终效果:黑色背景中有一个圆形"窗口"

其中transition: mask-position 0.06s linear;中的mask-position是指遮罩位置,这样遮罩位置变化就会有一个线性的过渡

这里蒙版的一些值用css变量来控制,方便等下用js动态的更新蒙版的位置

效果如下

image-20250817114340987

🔥 火把样式

/* 火把样式 */
#torch {
  position: absolute;
  width: 100px;
  height: 100px;
  pointer-events: none;
  z-index: 10;
  left: var(--x, 50%);
  top: var(--y, 50%);
  transform: translate(-50%, -50%);
}

效果如下

image-20250817114625669

🖱️ 鼠标和滚轮事件

index.js

let radius = 110 // 光照半径
// 设置css变量
const setStyleVar = (el, key, val) => el && el.style.setProperty(key, val)
// 遮罩元素
const mask = document.getElementById('mask')
// 火把元素
const torch = document.getElementById('torch')
// 修改遮罩层光圈位置
const setPos = (clientX, clientY) => {
  const { left, top } = mask.getBoundingClientRect()
  setStyleVar(mask, '--x', clientX - left + 'px')
  setStyleVar(mask, '--y', clientY - top + 'px')
  // 火把居中显示在光圈圆心
  setStyleVar(torch, '--x', clientX - left + 'px')
  setStyleVar(torch, '--y', clientY - top + 'px')
}
// 鼠标移动时,更新遮罩层光圈位置
mask.addEventListener('mousemove', (e) => {
  setPos(e.clientX, e.clientY)
})
// 滚轮滚动时,更新光照半径
mask.addEventListener('wheel', (e) => {
  radius = Math.max(50, Math.min(200, radius + e.deltaY * 0.1))
  setStyleVar(mask, '--r', radius + 'px')
})
let flickerTime = 0
// 遮罩层呼吸效果
const maskBreathe = () => {
  flickerTime += 0.05
  setStyleVar(mask, '--r', radius + Math.sin(flickerTime * 3) * 3 + 'px')
  requestAnimationFrame(maskBreathe)
}
maskBreathe()

其中 setStyleVar(mask, '--r', radius + Math.sin(flickerTime * 3) * 3 + 'px')是通过正弦函数拟火把的自然闪烁效果

🌟 最终效果

聚光灯演示效果

栗子前端技术周刊第 94 期 - React Native 0.81、jQuery 4.0.0 RC1、Bun v1.2.20...

🌰栗子前端技术周刊第 94 期 (2025.08.11 - 2025.08.17):浏览前端一周最新消息,学习国内外优秀文章视频,让我们保持对前端的好奇心。

📰 技术资讯

  1. React Native 0.81:React Native 0.81 版本新增了对 Android 16 的支持,提升了 iOS 构建速度,并进行了一系列稳定性改进。

  2. jQuery 4.0.0 RC1:jQuery 4.0.0 首个候选版本已发布,团队认为它已接近完成,希望开发者进行广泛测试,若未发现需修复的问题,将直接推出正式版本 jQuery 4.0.0。新版本主要变化与亮点包括:不再支持 IE 11 以下版本、删减遗留代码与弃用 API、引入精简版本等等。

  3. Bun v1.2.20:Bun v1.2.20 修复了 141 个问题,并带来了显著的性能提升,包括降低空闲状态下的 CPU 使用率,以及将 AbortSignal.timeout 的速度提升 40 倍。

📒 技术文章

  1. What we learned from creating PostCSS:那些从创建 PostCSS 中所学到的经验 - 12 年前,作者创建了 PostCSS —— 一款 CSS 自动化工具,其月下载量达 4 亿次,被谷歌、维基百科、Tailwind 以及 38% 的开发者所使用。在本文中,作者将分享在维护这个热门开源项目的漫长历程中所学到的经验。

  2. How to Use innerHTML, innerText, and textContent Correctly in JavaScript:如何在 JavaScript 中正确使用 innerHTMLinnerTexttextContent - 本文将解释 JavaScript 中三个 DOM 属性的区别:innerHTML 返回包含标签的完整 HTML 内容,innerText 仅返回受 CSS 规则影响的可见样式文本,而 textContent 则返回所有文本内容,无论其在 CSS 中是否可见。这三个属性在 DOM 操作中适用于不同的使用场景。

  3. 前端必学-完美组件封装原则:此文总结了作者多年组件封装经验,以及拜读 antdelement-plusvantfusion 等多个知名组件库所提炼的完美组件封装的经验;是一个开发者在封装项目组件,公共组件等场景时非常有必要遵循的一些原则。

🔧 开发工具

  1. ReactJS Cheatsheet:一份简洁且对开发者友好的 ReactJS 速查表,汇总了核心概念、必备 Hooks、路由、性能优化技巧以及 React 18 的新特性。
image-20250816142106838
  1. vue-scan:让你的组件在每次更新时都闪现红色边框,帮助你排查性能问题。
image-20250817090008862
  1. react-json-view:react-json-view(简称 rjv)是一个用于展示和编辑 JavaScript 数组及 JSON 对象的 React 组件。
image-20250817090054910

🚀🚀🚀 以上资讯文章选自常见周刊,如 JavaScript Weekly 等,周刊内容也会不断优化改进,希望你们能够喜欢。

💖 欢迎关注微信公众号:栗子前端

React状态更新踩坑记:我是这样优雅修改参数的

大家好,我是小杨,一名有6年经验的前端开发工程师。在React开发中,状态(State)和参数(Props)的修改是最基础但也最容易踩坑的部分。今天我就来分享几种常见的React参数修改方法,以及我在项目中总结的最佳实践,避免大家走弯路。


1. 直接修改State?大忌!

新手常犯的一个错误是直接修改state,比如:

// ❌ 错误示范:直接修改state
this.state.count = 10;  

React的state不可变(Immutable) 的,直接修改不会触发重新渲染。正确的做法是使用setState(类组件)或useState的更新函数(函数组件)。


2. 类组件:setState的正确姿势

在类组件里,修改状态必须用setState

class Counter extends React.Component {
  state = { count: 0 };

  increment = () => {
    // ✅ 正确方式:使用setState
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return <button onClick={this.increment}>Count: {this.state.count}</button>;
  }
}

注意setState异步的,如果依赖前一个状态,应该用函数式更新:

this.setState((prevState) => ({ count: prevState.count + 1 }));

3. 函数组件:useState + 不可变更新

在函数组件里,我们使用useState,同样要遵循不可变原则:

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    // ✅ 正确方式:使用useState的更新函数
    setCount(count + 1);
  };

  return <button onClick={increment}>Count: {count}</button>;
}

如果新状态依赖旧状态,推荐使用函数式更新:

setCount((prevCount) => prevCount + 1);

4. 修改对象或数组:避免引用突变

React要求状态更新必须是不可变的,所以直接修改对象或数组的属性是不行的:

const [user, setUser] = useState({ name: 'Alice', age: 25 });

// ❌ 错误:直接修改对象
user.age = 26;  
setUser(user); // 不会触发更新!

// ✅ 正确:创建新对象
setUser({ ...user, age: 26 });

数组的更新也要遵循不可变原则:

const [todos, setTodos] = useState(['Learn React', 'Write Blog']);

// ✅ 正确:使用展开运算符或map/filter
setTodos([...todos, 'New Task']); // 添加
setTodos(todos.filter((todo) => todo !== 'Learn React')); // 删除

5. 性能优化:useState vs useReducer

如果状态逻辑较复杂,useState可能会变得臃肿,这时可以用useReducer

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error('Unknown action');
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </div>
  );
}

useReducer适合管理复杂状态逻辑,比如表单、全局状态等。


6. 常见坑点 & 解决方案

① 连续setState不会立即更新

// ❌ 连续调用setState,count只会+1
setCount(count + 1);
setCount(count + 1);

// ✅ 使用函数式更新
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 现在会+2

② useEffect依赖问题

如果useEffect依赖state,但忘记加进依赖数组,可能导致闭包问题:

useEffect(() => {
  console.log(count); // 可能拿到旧值
}, []); // ❌ 缺少依赖

useEffect(() => {
  console.log(count); // ✅ 正确
}, [count]); // 依赖正确

总结

  • 不要直接修改state,使用setStateuseState的更新函数
  • 对象/数组更新时,创建新引用
  • 复杂状态逻辑用useReducer
  • 连续更新用函数式setState
  • useEffect依赖要写全

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

vite和webpack打包结构控制

概述

在工程化项目中,Vite 和 Webpack 作为当前最流行的两大构建工具,它们在打包输出目录结构的配置上各有特点,webpack和vite默认打包构建的输出目录结构可能不满足我们的需求,因此需要根据实际情况进行控制。

默认输出结构对比

webpack

默认情况下,基本上都不会根据情况进行分块,所有资源都是默认被打包到了一个文件中。

dist/
  ├── main.js
  ├── index.html
  |—— .....

vite

dist/
  ├── assets/
  │   ├── index.[hash].js
  │   ├── vendor.[hash].js
  │   └── style.[hash].css
  └── index.html

Vite目录结构精细控制

文件指纹策略

Webpack 提供了多种 hash 类型:

  • [hash]: 项目级hash
  • [chunkhash]: chunk级hash
  • [contenthash]: 内容级hash

基础配置方案

// vite.config.js
export default {
  build: {
    outDir: 'dist',
    assetsDir: 'static',
    emptyOutDir: true
  }
}

Rollup 输出配置

由于vite内部打包使用rollup,因此打包输出相关配置需参考rollup的配置

export default {
  build: {
    rollupOptions: {
      output: {
          //资源块输出目录配置
        chunkFileNames: 'static/js/[name]-[hash].js',
        //入口文件输出目录配置
        entryFileNames: 'static/js/[name]-[hash].js',
        //静态输出目录配置(图片、音频、字体)
        assetFileNames: ({ name }) => {
          const ext = name.split('.').pop()
          //函数形式动态返回文件输出名及其位置
          return `static/${ext}/[name]-[hash].[ext]`
        }
      }
    }
  }
}

webpack 目录结构精细控制

基础输出配置

// webpack.config.js
module.exports = {
  output: {
    path: path.resolve(__dirname, 'build'), // 修改输出目录
    filename: 'js/[name].[contenthash:8].js', // JS文件输出路径
    chunkFilename: 'js/[name].[contenthash:8].chunk.js', // 异步chunk
    assetModuleFilename: 'media/[name].[hash:8][ext]', // 静态资源
    clean: true // 构建前清空目录
  }
}

高级资源管理

使用 mini-css-extract-plugin 控制 CSS 输出:

  module: {
    rules: [
      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
     
    ],
  },

Webpack 5 引入了资源模块类型,取代了传统的 file-loader/url-loader,用来处理之前繁琐的配置

 module: {
   rules: [
{
       test: /.(png|jpe?g|gif|svg)$/i,
       type: 'asset/resource' // 替换 file-loader
     },
     {
       test: /.(mp4|webm|ogg)$/i,
       type: 'asset/resource'
     }
   ],
 },

四种资源模块类型

类型 作用 等价 loader
asset/resource 导出单独文件并生成 URL file-loader
asset/inline 导出资源的 Data URI url-loader
asset/source 导出资源的源代码 raw-loader
asset 自动选择 resource 或 inline url-loader + 限制

总结

上面列举的部分配置,更多的详细配置,可以查阅官网解析。

前端必会:如何创建一个可随时取消的定时器

一、原生的取消方式

JavaScript 原生就提供了取消定时器的方法。setTimeoutsetInterval 在调用时都会返回一个数字类型的 ID,我们可以将这个 ID 传递给 clearTimeoutclearInterval 来取消它。

// 1. 设置一个定时器
const timerId: number = setTimeout(() => {
  console.log("这个消息可能永远不会被打印");
}, 2000);

// 2. 在它触发前取消它
clearTimeout(timerId);

常见痛点:

  • timerId 变量需要被保留在组件或模块的作用域中,状态分散。
  • 启动、暂停、取消的逻辑是割裂的,代码可读性和可维护性差。

二、封装一个可取消的定时器类

我们可以简单的封装一个 CancellableTimer 类,将定时器的状态和行为内聚在一起。后续可以扩展,把项目中的所有定时器进行统一管理。

// 定义定时器ID类型
type TimeoutId = ReturnType<typeof setTimeout>;

class CancellableTimer {
    private timerId: TimeoutId | null = null;

    constructor(private callback: () => void, private delay: number) {}

    public start(): void {
        // 防止重复启动
        if (this.timerId !== null) {
            this.cancel();
        }

        this.timerId = setTimeout(() => {
            this.callback();
            // 执行完毕后重置 timerId
            this.timerId = null;
        }, this.delay);
    }

    public cancel(): void {
        if (this.timerId !== null) {
            clearTimeout(this.timerId);
            this.timerId = null;
        }
    }
}

// 使用示例
console.log('定时器将在3秒后触发...');
const myTimer = new CancellableTimer(() => {
    console.log('定时器任务执行!');
}, 3000);

myTimer.start();

// 模拟在1秒后取消
setTimeout(() => {
    console.log('用户取消了定时器。');
    myTimer.cancel();
}, 1000);

三、实现可暂停和恢复的定时器

在很多场景下,我们需要的不仅仅是取消,还有暂停恢复

要实现这个功能,我们需要在暂停时记录剩余时间

type TimeoutId = ReturnType<typeof setTimeout>;

class AdvancedTimer {
    private timerId: TimeoutId | null = null;
    private startTime: number = 0;
    private remainingTime: number;
    private callback: () => void;
    private delay: number;


    constructor(callback: () => void, delay: number) {
        this.remainingTime = delay;
        this.callback = callback;
        this.delay = delay;
    }

    public resume(): void {
        if (this.timerId) {
            return; // 已经在运行
        }

        this.startTime = Date.now();
        this.timerId = setTimeout(() => {
            this.callback();
            // 任务完成,重置
            this.remainingTime = this.delay;
            this.timerId = null;
        }, this.remainingTime);
    }

    public pause(): void {
        if (!this.timerId) {
            return;
        }

        clearTimeout(this.timerId);
        this.timerId = null;
        // 计算并更新剩余时间
        const timePassed = Date.now() - this.startTime;
        this.remainingTime -= timePassed;
    }

    public cancel(): void {
        if (this.timerId) {
            clearTimeout(this.timerId);
        }
        this.timerId = null;
        this.remainingTime = this.delay; // 重置
    }
}

// 使用示例
console.log('定时器启动,5秒后执行...');
const advancedTimer = new AdvancedTimer(() => console.log('Done!'), 5000);
advancedTimer.resume();

setTimeout(() => {
    console.log('2秒后暂停定时器');
    advancedTimer.pause();
}, 2000);

setTimeout(() => {
    console.log('4秒后恢复定时器 , 应该还剩3秒');
    advancedTimer.resume();
}, 4000);

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

Oxc 最新 Transformer Alpha 功能速览! 🚀🚀🚀

前言

刚刚看到尤雨溪推特转发了 OXC 团队的最新成果,并介绍了该成果背后的一些故事!

尤雨溪推特

今天介绍下这些详细成果!

往期精彩推荐

正文

Oxc Transformer Alpha 内置 React Refresh,以及无需 TypeScript 编译器的独立 .d.ts 文件生成。相较于 SWC 和 Babel,Oxc 在性能、内存占用和包体积上表现出色,堪称前端构建的实用利器。

以下是其核心特性的详细解析。

1. TypeScript 和 JSX 到 ESNext 转换

Oxc 支持将 TypeScript 和 React JSX 代码转换为 ESNext,性能显著优于传统工具:

  • 3-5 倍于 SWC:处理 100 到 10,000 行代码,Oxc 耗时仅 0.14ms 至 14.9ms,而 SWC 为 0.7ms 至 35.9ms。
  • 20-50 倍于 Babel:Babel 处理同样代码耗时 11.5ms 至 492ms,Oxc 效率遥遥领先。

2. 内置 React Refresh

Oxc 集成了 React Refresh,支持开发中的热重载,速度比 SWC 快 5 倍,比 Babel 快 50 倍。这让 React 开发更流畅,减少等待时间。

3. TypeScript 独立声明生成

Oxc 提供无需 TypeScript 编译器的 .d.ts 文件生成,性能惊人:

  • 40 倍于 TSC:处理 100 行代码仅需 0.1ms(TSC 为 23.1ms)。
  • 20 倍于大文件:10,000 行代码耗时 3.5ms(TSC 为 115.2ms)。

示例

import { transform } from 'oxc-transform';
const transformed = transform('file.ts', sourceCode, {
  typescript: {
    onlyRemoveTypeImports: true,
    declaration: { stripInternal: true },
  },
});
await fs.writeFile('out.js', transformed.code);
await fs.writeFile('out.d.ts', transformed.declaration);

4. 轻量级与低内存占用

Oxc 仅需 2 个 npm 包(总计 2MB),对比 SWC 的 37.5MB 和 Babel 的 21MB(170 个包)。内存占用上,Oxc 处理 10,777 行代码仅用 51MB 内存,SWC 用 67MB,Babel 高达 172MB。

5. 实际应用案例

  • Vue.js:实验性使用 oxc-transform 优化构建流程。
  • vue-macros:通过 unplugin-isolated-decl.d.ts 生成时间从 76s 降至 16s。
  • Airtable:在 Bazel 构建中集成 Oxc 的 .d.ts 生成。
  • Rolldown:直接使用 Rust oxc_transformer crate。

最后

Oxc Transformer Alpha 以 Rust 的高性能和轻量级设计,为 JavaScript 编译带来新可能。无论是加速 TypeScript 转换还是优化 React 开发体验,它都展现了朴实无华的实用力量!

今天的分享就这些了,感谢大家的阅读!如果文章中存在错误的地方欢迎指正!

往期精彩推荐

❌