普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月15日首页

【vue篇】Vue 模板编译原理:从 Template 到 DOM 的翻译官

作者 LuckySusu
2025年10月15日 18:18

在 Vue 项目中,你写的:

<template>
  <div class="user" v-if="loggedIn">
    Hello, {{ name }}!
  </div>
</template>

最终变成了浏览器能执行的 JavaScript 函数。
这背后,就是 Vue 模板编译器 在默默工作。

本文将深入解析 Vue 模板编译的三大核心阶段parseoptimizegenerate,带你揭开 .vue 文件如何变成可执行代码的神秘面纱。


一、为什么需要模板编译?

🎯 浏览器不认识 <template>

<!-- 你写的 -->
<template>
  <div v-if="user.loggedIn">{{ user.name }}</div>
</template>

<!-- 浏览器看到的 -->
Unknown tag: template → 忽略 or 报错

✅ 解决方案:编译成 render 函数

// 编译后生成的 render 函数
render(h) {
  return this.user.loggedIn 
    ? h('div', { class: 'user' }, `Hello, ${this.user.name}!`)
    : null;
}

💡 render 函数返回的是 虚拟 DOM (VNode),Vue 拿它来高效更新真实 DOM。


二、模板编译三部曲

Template String 
     ↓ parse
   AST (抽象语法树)
     ↓ optimize
   优化后的 AST
     ↓ generate
   Render Function

第一步:🔍 解析(Parse)—— 构建 AST

目标:将 HTML 字符串转为 AST(Abstract Syntax Tree)

示例输入:

<div id="app" class="container">
  <p v-if="show">Hello {{ name }}</p>
</div>

输出 AST 结构:

{
  "type": 1,
  "tag": "div",
  "attrsList": [...],
  "children": [
    {
      "type": 1,
      "tag": "p",
      "if": "show",           // 指令被解析
      "children": [
        {
          "type": 3,
          "text": "Hello ",
          "static": false
        },
        {
          "type": 2,
          "expression": "_s(name)",  // {{ name }} 被编译
          "text": "{{ name }}"
        }
      ]
    }
  ]
}

🛠️ 如何实现?正则 + 状态机

编译器使用多个正则表达式匹配:

匹配内容 正则示例
标签开始 /<([^\s>/]+)/
属性 /(\w+)(?:=)(?:"([^"]*)")/
插值表达式 /{{\s*([\s\S]*?)\s*}}/
指令 /v-(\w+):?(\w*)/?

⚠️ 注意:Vue 的 parser 是一个递归下降解析器,比简单正则复杂得多,但原理类似。


第二步:⚡ 优化(Optimize)—— 标记静态节点

目标:提升运行时性能,跳过不必要的 diff

什么是静态节点?

  • 不包含动态绑定;
  • 内容不会改变;
  • 如:<p>纯文本</p><img src="/logo.png">

优化过程:

  1. 遍历 AST,标记静态根节点和静态子节点;
  2. 添加 static: truestaticRoot: true 标志。
{
  "tag": "p",
  "static": true,
  "staticRoot": true,
  "children": [
    { "type": 3, "text": "这是静态文本", "static": true }
  ]
}

运行时收益:

// patch 过程中
if (vnode.static && oldVnode.static) {
  // 直接复用,跳过 diff!
  vnode.componentInstance = oldVnode.componentInstance;
  return;
}

💥 对于大量静态内容(如文档页面),性能提升可达 30%+


第三步:🎯 生成(Generate)—— 输出 render 函数

目标:将优化后的 AST 转为可执行的 render 函数字符串

输入:优化后的 AST

输出:JavaScript 代码字符串

with(this) {
  return _c('div',
    { attrs: { "id": "app", "class": "container" } },
    [ (show) ?
      _c('p', [_v("Hello "+_s(name))]) :
      _e()
    ]
  )
}

🔤 代码生成规则

AST 节点 生成代码
元素标签 _c(tag, data, children)
文本节点 _v(text)
表达式 {{ }} _s(expression)
条件渲染 v-if (condition) ? renderTrue : renderFalse
静态节点 _m(index)(从 $options.staticRenderFns 中取)

💡 _c = createElement, _v = createTextVNode, _s = toString


三、完整流程图解

          Template
             │
             ▼
       [ HTML Parser ]
             │
             ▼
         AST (未优化)
             │
             ▼
      [ 静态节点检测与标记 ]
             │
             ▼
         AST (已优化)
             │
             ▼
     [ Codegen (生成器) ]
             │
             ▼
     Render Function String
             │
             ▼
     new Function(renderStr)
             │
             ▼
       可执行的 render()
             │
             ▼
        Virtual DOM
             │
             ▼
        Real DOM (渲染)

四、Vue 2 vs Vue 3 编译器对比

特性 Vue 2 Vue 3
编译目标 render 函数 render 函数
模板语法限制 较多(如必须单根) 更灵活(Fragment 支持多根)
静态提升 ✅✅ 更强的 hoist 静态节点
Patch Flag 动态节点标记,diff 更快
编译时优化 基础静态标记 Tree-shaking 友好,死代码消除
源码位置 src/compiler/ @vue/compiler-dom

💥 Vue 3 的编译器更智能,生成的代码更小、更快。


五、手写一个极简模板编译器(玩具版)

function compile(template) {
  // Step 1: Parse (简化版)
  const tags = template.match(/<(\w+)[^>]*>(.*?)<\/\1>/);
  if (!tags) return;

  const tag = tags[1];
  const content = tags[2];

  // Step 2: Optimize (判断是否静态)
  const isStatic = !content.includes('{{');

  // Step 3: Generate
  const renderCode = `
    function render() {
      return ${isStatic 
        ? `_v("${content}")` 
        : `_c("${tag}", {}, [ _v( _s(${content.slice(2,-2)})) ])`
      };
    }
  `;

  return renderCode;
}

// 使用
const code = compile('<p>{{ msg }}</p>');
console.log(code);
// 输出:function render() { return _c("p", {}, [ _v( _s(msg)) ]); }

🎉 这就是一个最简化的“编译器”雏形!


💡 结语

“Vue 模板编译器,是连接声明式模板与命令式 DOM 操作的桥梁。”

阶段 作用 输出
Parse 解析 HTML 字符串 AST
Optimize 标记静态节点 优化后的 AST
Generate 生成 JS 代码 render 函数

掌握编译原理,你就能:

✅ 理解 Vue 模板的底层机制;
✅ 写出更高效的模板(减少动态绑定);
✅ 调试编译错误更得心应手;
✅ 为学习其他框架(React JSX)打下基础。

【vue篇】Vue Mixin:可复用功能的“乐高积木”

作者 LuckySusu
2025年10月15日 18:18

在开发多个 Vue 组件时,你是否遇到过这样的问题:

“这几个组件都有相同的 loading 逻辑,要复制粘贴?” “如何共享通用的错误处理方法?” “有没有像‘插件’一样的功能可以注入?”

答案就是:Mixin(混入)

本文将全面解析 Vue Mixin 的核心概念使用场景潜在风险


一、什么是 Mixin?

Mixin 是一个包含 Vue 组件选项的对象,可以被“混入”到多个组件中,实现功能复用。

🎯 核心价值

  • 代码复用:避免重复编写相同逻辑;
  • 逻辑分离:将通用功能(如 loading、权限)抽离;
  • 渐进增强:为组件动态添加功能。

二、快速上手:一个 Loading Mixin 示例

场景:多个组件需要“加载中”状态

Step 1:创建 loading.mixin.js

// mixins/loading.mixin.js
export const loadingMixin = {
  data() {
    return {
      loading: false,
      errorMessage: null
    };
  },

  methods: {
    async withLoading(asyncFn) {
      this.loading = true;
      this.errorMessage = null;
      try {
        await asyncFn();
      } catch (err) {
        this.errorMessage = err.message;
      } finally {
        this.loading = false;
      }
    }
  },

  // 生命周期钩子
  created() {
    console.log('【Mixin】组件创建,初始化 loading 状态');
  }
};

Step 2:在组件中使用

<!-- UserProfile.vue -->
<script>
import { loadingMixin } from '@/mixins/loading.mixin';

export default {
  mixins: [loadingMixin],

  async created() {
    // 使用 mixin 提供的方法
    await this.withLoading(() => this.fetchUser());
  },

  methods: {
    async fetchUser() {
      // 模拟 API 调用
      await new Promise(r => setTimeout(r, 1000));
      this.user = { name: 'Alice' };
    }
  }
};
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="errorMessage">错误:{{ errorMessage }}</div>
  <div v-else>用户:{{ user.name }}</div>
</template>

✅ 效果:UserProfile 组件自动拥有了 loadingerrorMessagewithLoading 方法。


三、Mixin 合并规则:当名字冲突了怎么办?

当 Mixin 和组件定义了同名选项,Vue 会按规则合并:

选项类型 合并策略
data 函数返回对象合并(浅合并)
methods / computed / props 组件优先,Mixin 的会被覆盖
生命周期钩子 两者都执行,Mixin 的先执行
watch 同名 watcher 都会执行
computed 组件优先

🎯 生命周期执行顺序

const myMixin = {
  created() {
    console.log('1. Mixin created');
  }
};

export default {
  mixins: [myMixin],
  created() {
    console.log('2. Component created'); // 后执行
  }
}

输出:

1. Mixin created
2. Component created

💥 Mixin 的生命周期永远先于组件自身执行


四、实战应用场景

✅ 场景 1:表单验证逻辑复用

// mixins/validation.mixin.js
export const validationMixin = {
  data() {
    return {
      errors: {}
    };
  },
  methods: {
    validateEmail(email) {
      const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!re.test(email)) {
        this.errors.email = '邮箱格式不正确';
      } else {
        delete this.errors.email;
      }
    }
  }
};

✅ 场景 2:权限控制

// mixins/permission.mixin.js
export const permissionMixin = {
  mounted() {
    if (!this.$store.getters.hasPermission(this.requiredPermission)) {
      this.$router.push('/403');
    }
  }
};

// 组件中
export default {
  mixins: [permissionMixin],
  data() {
    return {
      requiredPermission: 'user:edit'
    };
  }
};

✅ 场景 3:第三方 SDK 集成

// mixins/analytics.mixin.js
export const analyticsMixin = {
  mounted() {
    this.$analytics.pageView(); // 记录页面访问
  },
  methods: {
    trackEvent(event, props) {
      this.$analytics.track(event, props);
    }
  }
};

五、Mixin 的“黑暗面”:潜在问题

❌ 问题 1:命名冲突(Name Collision)

// mixin 定义了 fetchData
const apiMixin = {
  methods: {
    fetchData() { /* ... */ }
  }
};

// 组件也定义了 fetchData
export default {
  mixins: [apiMixin],
  methods: {
    fetchData() { /* 覆盖了 mixin 的方法!*/ }
  }
}

⚠️ 组件的方法会覆盖 Mixin 的,可能导致逻辑丢失。


❌ 问题 2:隐式依赖(Implicit Dependency)

// mixin 依赖组件必须提供 `userId`
const userMixin = {
  async created() {
    this.userData = await fetch(`/api/users/${this.userId}`);
  }
};

如果组件没有定义 userId,就会报错,但没有明显提示


❌ 问题 3:来源不清晰(Source Ambiguity)

<template>
  <!-- 这个 `loading` 是哪来的? -->
  <div v-if="loading">加载中...</div>
</template>

🔍 开发者无法从模板直接看出 loading 是来自 Mixin 还是组件自身。


六、Vue 3 的替代方案:Composition API

// composables/useLoading.js
import { ref } from 'vue';

export function useLoading() {
  const loading = ref(false);
  const errorMessage = ref(null);

  const withLoading = async (asyncFn) => {
    loading.value = true;
    errorMessage.value = null;
    try {
      await asyncFn();
    } catch (err) {
      errorMessage.value = err.message;
    } finally {
      loading.value = false;
    }
  };

  return { loading, errorMessage, withLoading };
}
<!-- UserProfile.vue -->
<script setup>
import { useLoading } from '@/composables/useLoading';

const { loading, withLoading } = useLoading();

async function loadUser() {
  await withLoading(fetchUser);
}
</script>

✅ Composition API 的优势:

特性 Mixin Composition API
命名冲突 ❌ 易发生 ✅ 通过解构重命名
源头追踪 ❌ 困难 useXxx() 清晰可见
类型推导 ❌ 弱 ✅ TypeScript 友好
逻辑复用 ✅ 更灵活

💡 结语

“Mixin 是一把双刃剑:用得好,提升效率;用不好,制造混乱。”

方案 适用场景
Mixin Vue 2 项目、简单逻辑复用
Composition API Vue 3 项目、复杂逻辑、TypeScript

🚀 最佳实践建议:

  1. 优先使用 Composition API(Vue 3);
  2. ✅ 如果用 Mixin,命名清晰(如 useLoadingMixin);
  3. ✅ 避免在 Mixin 中引入隐式依赖
  4. ✅ 文档化 Mixin 的输入/输出

掌握 Mixin,你就能写出更 DRY(Don't Repeat Yourself)的代码。

【vue篇】Vue 2 响应式“盲区”破解:如何监听对象/数组属性变化

作者 LuckySusu
2025年10月15日 18:18

在 Vue 开发中,你是否遇到过这样的诡异问题:

“我明明改了 this.user.name,为什么页面没更新?” “this.arr[0] = 'new',视图怎么不动?” “Vue 不是响应式的吗?”

本文将彻底解析 Vue 2 的响应式限制,并提供五种解决方案,让你彻底告别“数据变了,视图没变”的坑。


一、核心问题:Vue 2 的响应式“盲区”

🎯 为什么直接赋值不触发更新?

// ❌ 无效:视图不更新
this.user.name = 'John';      // 对象新增属性
this.users[0] = 'Alice';      // 数组索引赋值

🔍 根本原因:Object.defineProperty 的限制

Vue 2 使用 Object.defineProperty 拦截:

  • ✅ 能监听 已有属性的修改
  • 不能监听
    • 对象新增属性
    • 数组索引直接赋值arr[0] = x);
    • 数组长度修改arr.length = 0)。

💥 Vue 无法“感知”这些操作,所以不会触发视图更新。


二、解决方案:五种正确姿势

✅ 方案 1:this.$set() —— Vue 官方推荐

// ✅ 对象新增属性
this.$set(this.user, 'name', 'John');

// ✅ 数组索引赋值
this.$set(this.users, 0, 'Alice');

// ✅ 等价于
Vue.set(this.user, 'name', 'John');

🎯 this.$set 的内部原理

function $set(target, key, val) {
  // 1. 如果是数组 → 用 splice 触发响应式
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1, val);
    return val;
  }
  
  // 2. 如果是对象
  const ob = target.__ob__;
  if (key in target) {
    // 已有属性 → 直接赋值(已有 getter/setter)
    target[key] = val;
  } else {
    // 新增属性 → 动态添加响应式
    defineReactive(target, key, val);
    ob.dep.notify(); // 手动派发更新
  }
  return val;
}

💡 $set = 智能判断 + 自动响应式处理


✅ 方案 2:数组专用方法 —— splice

// ✅ 修改数组某一项
this.users.splice(0, 1, 'Alice'); // 索引0,删除1个,插入'Alice'

// ✅ 新增元素
this.users.splice(1, 0, 'Bob'); // 在索引1前插入

// ✅ 删除元素
this.users.splice(0, 1); // 删除第一项

🎯 为什么 splice 可以?

Vue 2 重写了数组的 7 个方法

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];

// 重写后,调用这些方法时会:
// 1. 执行原生方法
// 2. dep.notify() → 触发视图更新

✅ 这些方法是“响应式安全”的。


✅ 方案 3:对象整体替换

// ✅ 对象新增属性
this.user = { ...this.user, name: 'John' };

// ✅ 或
this.user = Object.assign({}, this.user, { name: 'John' });
  • ✅ 原理:重新赋值 → 触发 setter → 视图更新;
  • ❌ 缺点:失去响应式连接(如果 user 被深层嵌套)。

✅ 方案 4:初始化时声明属性

data() {
  return {
    user: {
      name: '',    // 提前声明
      age: null,
      email: ''    // 避免运行时新增
    }
  };
}

💡 最佳实践:data 中定义所有可能用到的属性


✅ 方案 5:使用 Vue.observable + computed

const state = Vue.observable({
  user: { name: 'Tom' }
});

// 在组件中
computed: {
  userName() {
    return state.user.name; // 自动依赖收集
  }
}
  • ✅ 适合全局状态;
  • ❌ 不推荐用于组件局部状态。

三、Vue 3 的革命性改进:Proxy 无所不能

import { reactive } from 'vue';

const state = reactive({
  user: {},
  users: []
});

// ✅ Vue 3 中,以下操作全部响应式!
state.user.name = 'John';        // 新增属性
state.users[0] = 'Alice';        // 索引赋值
state.users.length = 0;          // 修改长度
delete state.user.name;          // 删除属性

💥 Vue 3 使用 Proxy,能拦截 getsetdeleteProperty 等所有操作,彻底解决 Vue 2 的响应式盲区。


四、最佳实践清单

场景 推荐方案
Vue 2:对象新增属性 this.$set(obj, key, val)
Vue 2:数组索引赋值 this.$set(arr, index, val)arr.splice(index, 1, val)
Vue 2:批量更新数组 splice / push / pop
Vue 2:避免问题 初始化时声明所有属性
Vue 3:任何操作 直接赋值,Proxy 全部拦截

五、常见误区

❌ 误区 1:this.$set 只用于对象

// ❌ 错误:认为数组不需要 $set
this.users[0] = 'new'; // 不响应

// ✅ 正确
this.$set(this.users, 0, 'new');

❌ 误区 2:pushsplice 更好

// ✅ `splice` 更通用
this.users.splice(1, 0, 'Bob'); // 在中间插入
this.users.push('Bob');         // 只能在末尾

✅ 推荐:splice 是数组操作的“瑞士军刀”


💡 结语

“在 Vue 2 中,永远不要直接操作数组索引或对象新增属性。”

方法 是否响应式 适用场景
obj.key = val (已有) 修改已有属性
obj.newKey = val $set
arr[i] = val $setsplice
this.$set() 通用解决方案
splice() 数组操作首选

掌握这些技巧,你就能:

✅ 避免响应式失效的 bug;
✅ 写出更可靠的 Vue 代码;
✅ 理解 Vue 响应式的核心原理。

【vue篇】Vue.delete vs delete:数组删除的“陷阱”与正确姿势

作者 LuckySusu
2025年10月15日 18:17

在 Vue 开发中,你是否遇到过这样的问题:

“用 delete 删除数组项,视图为什么没更新?” “Vue.delete 和原生 delete 有什么区别?” “如何安全地删除数组元素?”

本文将彻底解析 deleteVue.delete删除数组时的根本差异。


一、核心结论:一个“打洞”,一个“重排”

操作 结果 响应式 视图更新
delete arr[index] 元素变 empty长度不变 ❌ 不响应 ❌ 不更新
Vue.delete(arr, index) 直接删除,长度改变 ✅ 响应式 ✅ 自动更新

💥 delete 只是“打了个洞”,而 Vue.delete 是真正的“移除”。


二、实战演示:同一个操作,两种结果

场景:删除数组第二项

const vm = new Vue({
  data: {
    users: ['Alice', 'Bob', 'Charlie']
  }
});

方式一:delete(错误方式)

delete vm.users[1];
console.log(vm.users); 
// ['Alice', empty, 'Charlie'] → 长度仍为 3!
  • 内存中users[1] 变为 empty slot
  • DOM 中:视图不会更新

⚠️ 控制台警告:

[Vue warn]: A value is trying to be set on a non-existent property...

方式二:Vue.delete(正确方式)

Vue.delete(vm.users, 1);
// 或 this.$delete(vm.users, 1)
console.log(vm.users); 
// ['Alice', 'Charlie'] → 长度变为 2!

✅ 视图自动更新,完美!


三、深入原理:为什么 delete 不行?

🔍 1. delete 的本质

let arr = ['a', 'b', 'c'];
delete arr[1];

// 等价于
arr[1] = undefined; // ❌ 错误理解
// 实际是:
Object.defineProperty(arr, 1, { configurable: true });
delete arr[1]; // 移除属性,但保留索引“空位”
索引:   0     1     2
值:   'a'   empty  'c'
  • 数组长度 不变
  • for...in 会跳过 empty 项;
  • Array.prototype 方法(如 map, filter)会跳过 empty

🔍 2. Vue 响应式的限制

Vue 2 使用 Object.defineProperty 拦截:

  • ✅ 能监听 arr[1] = newValue(赋值);
  • 不能监听 delete arr[1](删除属性)

💡 Vue 无法检测到“属性被删除”,所以不会触发视图更新。


🔍 3. Vue.delete 的内部实现

Vue.delete = function (target, key) {
  // 1. 执行原生 delete
  delete target[key];
  
  // 2. 手动触发依赖更新
  if (target.__ob__) {
    target.__ob__.dep.notify(); // 强制通知 watcher
  }
}

Vue.delete = delete + 手动派发更新


四、其他删除数组的方法(推荐)

✅ 1. splice() —— 最常用

vm.users.splice(1, 1); // 从索引1开始,删除1个
// ['Alice', 'Charlie']
  • ✅ 响应式(Vue 重写了 splice);
  • ✅ 支持删除多个元素;
  • ✅ 返回被删除的元素。

✅ 2. filter() —— 函数式编程

vm.users = vm.users.filter((user, index) => index !== 1);
// 或根据条件删除
vm.users = vm.users.filter(user => user !== 'Bob');
  • ✅ 不修改原数组,返回新数组;
  • ✅ 适合复杂条件删除;
  • ✅ 响应式(因为重新赋值)。

✅ 3. slice() + 解构

vm.users = [
  ...vm.users.slice(0, 1),
  ...vm.users.slice(2)
]; // 删除索引1
  • ✅ 函数式,不可变数据;
  • ✅ 适合组合多个片段。

五、Vue 3 的改进:Proxy 无所不能

import { reactive } from 'vue';

const state = reactive({
  users: ['Alice', 'Bob', 'Charlie']
});

// Vue 3 中,delete 也能触发更新!
delete state.users[1]; // ✅ 视图自动更新

💥 Vue 3 使用 Proxy,能拦截 deleteProperty,因此原生 delete 也响应式!


六、最佳实践清单

场景 推荐方法
删除指定索引 splice(index, 1)
删除满足条件的元素 filter(condition)
需要兼容 Vue 2 Vue.delete(array, index)
Vue 3 项目 delete array[index]
性能敏感场景 splice(原地修改)

💡 结语

“在 Vue 2 中,永远不要用 delete 删除数组!”

方法 是否响应式 是否推荐
delete arr[i] ❌ 绝对避免
Vue.delete(arr, i) ✅ Vue 2 推荐
arr.splice(i, 1) ✅ 首选
arr.filter(...) ✅ 函数式首选

掌握这些删除技巧,你就能:

✅ 避免视图不更新的 bug;
✅ 写出更健壮的 Vue 代码;
✅ 顺利过渡到 Vue 3 的响应式系统。

【vue篇】Vue 项目中的静态资源管理:assets vs static 终极指南

作者 LuckySusu
2025年10月15日 18:17

在 Vue 项目中,你是否遇到过这样的困惑:

assetsstatic 文件夹有什么区别?” “图片到底该放哪个文件夹?” “为什么有的资源路径变了,有的没变?”

本文将彻底解析 assetsstatic核心差异使用场景最佳实践


一、核心结论:一句话总结

assets 走构建流程(可处理),static 直接拷贝(不处理)。

维度 assets static
是否参与构建 ✅ 是 ❌ 否
是否被 webpack 处理 ✅ 是 ❌ 否
是否支持模块化导入 ✅ 是 ❌ 否
是否会被重命名(hash) ✅ 是 ❌ 否
是否支持 Tree-shaking ✅ 是 ❌ 否

二、详细对比:从构建流程说起

🔄 1. assets:构建流程的“参与者”

src/assets/logo.png
     ↓
  webpack 处理
     ↓
  压缩、转 base64、生成 hash 名
     ↓
dist/static/img/logo.2f1f87g.png

assets 的特点:

  • 参与构建:被 webpack 处理;
  • 优化处理
    • 图片压缩(image-webpack-loader);
    • 小图转 base64(减少 HTTP 请求);
    • 文件名加 hash(缓存优化);
  • 支持模块化导入
import logo from '@/assets/logo.png';
console.log(logo); // /static/img/logo.abc123.png
  • 路径动态化:路径由构建工具生成,不可预测

🔄 2. static:构建流程的“旁观者”

static/favicon.ico
     ↓
  直接拷贝
     ↓
dist/favicon.ico

static 的特点:

  • 不参与构建:原封不动拷贝到 dist
  • 无优化:不压缩、不转码、不加 hash;
  • 路径固定:访问路径 = / + 文件名
  • 适合“即插即用”资源
<!-- 直接通过绝对路径访问 -->
<link rel="icon" href="/favicon.ico">
<script src="/js/third-party.js"></script>

三、实战演示:同一个图片的不同命运

场景:项目中使用 logo.png

方式一:放在 assets

<template>
  <img :src="logo" alt="Logo">
</template>

<script>
import logo from '@/assets/logo.png';
// logo = "/static/img/logo.abc123.png"
</script>

优势

  • 图片被压缩,体积更小;
  • 文件名加 hash,缓存友好;
  • 支持按需加载。

方式二:放在 static

<template>
  <img src="/static/logo.png" alt="Logo">
</template>

优势

  • 构建速度快(跳过处理);
  • 路径固定,适合第三方脚本引用。

劣势

  • 图片未压缩,体积大;
  • 无 hash,缓存更新困难。

四、何时使用 assets?何时使用 static

✅ 推荐使用 assets 的场景:

资源类型 示例
项目自用图片 logo、banner、icon
CSS/SCSS 文件 @import '@/assets/styles/main.scss'
字体文件 .woff, .ttf(可被 hash)
SVG 图标 可被 svg-sprite-loader 处理
需要按需引入的 JS 工具函数、配置文件

💡 原则:项目源码中直接引用的资源 → 放 assets


✅ 推荐使用 static 的场景:

资源类型 示例
第三方库 static/js/jquery.min.js
Favicon favicon.ico
Robots.txt SEO 爬虫规则
大型静态文件 PDF、视频(避免 webpack 处理)
CND 回退文件 当 CDN 失败时本地加载
<!-- 第三方库回退 -->
<script src="https://cdn.example.com/vue.js"></script>
<script>window.Vue || document.write('<script src="/static/js/vue.min.js"><\/script>')</script>

💡 原则:不希望被构建工具处理的资源 → 放 static


五、Vue CLI 项目结构示例

my-project/
├── public/               # Vue CLI 中 static 的新名字
│   ├── favicon.ico
│   ├── robots.txt
│   └── static/
│       └── js/
│           └── analytics.js
├── src/
│   ├── assets/           # 所有需要构建的资源
│   │   ├── images/
│   │   ├── fonts/
│   │   └── styles/
│   └── components/
└── package.json

⚠️ 注意:在 Vue CLI 3+ 中,static 文件夹已更名为 public


六、常见误区与最佳实践

❌ 误区 1:所有图片都放 static

<!-- 错误:大图未压缩,无 hash -->
<img src="/static/banner.jpg">

✅ 正确做法:

import banner from '@/assets/banner.jpg';
<img :src="banner">

❌ 误区 2:在 assets 中放第三方库

// ❌ 错误:第三方库应放 public
import 'jquery'; // 来自 node_modules 或 assets

✅ 正确做法:

<!-- 放 public,通过 script 标签引入 -->
<script src="/static/js/jquery.min.js"></script>

✅ 最佳实践清单

实践 说明
✅ 小图放 assets 转 base64,减少请求
✅ 大图放 assets 压缩,但不转 base64
✅ 第三方库放 public 避免重复打包
✅ 使用 require 动态加载 :src="require('@/assets/dynamic.png')"
✅ 配置 publicPath 部署到子目录时设置

💡 结语

assets 是你的‘智能资源库’,staticpublic)是你的‘原始文件仓库’。”

选择 使用场景
assets 项目源码引用、需要优化、支持 hash
static / public 第三方资源、固定路径、避免构建

掌握这一原则,你就能:

✅ 优化项目性能;
✅ 减少打包体积;
✅ 提升缓存效率;
✅ 避免资源加载错误。

她问我::is-logged 是啥?我说:前面加冒号,就是 Vue 在发暗号

作者 洛小豆
2025年10月15日 13:04

深夜代码系列 · 第4期

关注我,和小豆一起在掘金看小说

🔥 开篇引爆

周五下午,我刚想摸鱼打开掘金,水篇小说,她突然走过来,一脸困惑地指着我屏幕上的代码。

“豆子,你看看这个,冒号和 @ 都是啥意思?我知道它们是 Vue 的语法糖,但具体怎么理解?我 Vue2 写到吐,Vue3 一升级全不会了!”

我一看,正是我们项目里最常见的 Header 组件调用:

<Header
  :is-logged-in="isLoggedIn"
  :username="username"
  @logout="handleLogout"
  @login-success="handleLoginSuccess"
/>

我放下鼠标,给她倒了杯水,笑眯眯地说:“这三个符号,就像是父子组件之间的三条秘密通道,它们分别负责传递数据接收信号。”


🎯 初步分析:父子组件通信的“传声筒”原理

父组件需要向子组件传递数据(如登录状态),子组件需要向父组件发送事件(如用户点击登出),实现双向通信。

核心概念:

  1. props(父 → 子):父组件通过属性向子组件传递数据。
  2. emit(子 → 父):子组件通过事件向父组件发送消息。

:is-logged-in:它负责“传递数据

我指着代码中的冒号,开始解释:

“你看这个 :,它是 v-bind 的简写。你可以把它想象成一个单向快递。”

<!-- 动态绑定 prop -->
:is-logged-in="isLoggedIn"  // 等价于 v-bind:is-logged-in="isLoggedIn"

“父组件(我们现在所在的这个页面)是快递公司,isLoggedIn 是一个包裹,里面装着‘用户是否登录’这个信息。我们用 :is-logged-in 这个‘快递单’,把这个包裹寄给了子组件 Header。”

“所以,当父组件里的 isLoggedIn 变量从 false 变成 true 时,这个包裹里的内容也会自动更新,子组件就会立刻收到最新的状态。”

小汐若有所思地点点头:“我懂了,这个冒号就是把父组件的数据动态地‘喂’给子组件,对吧?”

“没错,”我打了个响指,“这就是 props 传值 的过程。父组件通过 props 把数据传递给子组件,让子组件知道‘现在是什么情况’。”

Prop 命名规范

  • 父组件模板中使用 kebab-case:is-logged-in
  • 子组件中使用 camelCaseisLoggedin

类型安全

defineProps({
  isLoggedin: Boolean,
  username: {
    type: String,
    required: true,
    default: '游客'
  }
})

@logout@login-success:它们负责“接收信号

我继续指着 @ 符号,解释道:

“如果说冒号是快递,那么 @ 就是一个对讲机。”

<!-- 监听自定义事件 -->
@logout="handleLogout"       // 等价于 v-on:logout="handleLogout"

“当用户在 Header 组件里点击了‘登出’按钮,子组件会对着对讲机喊一声:‘logout’!而父组件这边一直开着对讲机,听到这个信号后,就会立即调用 handleLogout 方法,把 isLoggedIn 设为 false,清空 username。”

@login-success 也是同理,当子组件完成登录操作后,它会对着对讲机喊:‘login-success’,甚至还会顺便把用户信息作为‘暗号’一起发送过来。父组件接收到信号和暗号后,就能调用 handleLoginSuccess 方法来更新用户信息了。”

小汐听完,露出了恍然大悟的表情:“所以,@ 就是 v-on 的简写,用来监听子组件发出的自定义事件。这就像是子组件在告诉父组件:‘我干完活了,你来处理一下吧!’”

事件命名规范

  • 使用 kebab-case@login-success
  • 事件名要有动词:login-successupdate-userdelete-item

事件声明

defineEmits(['logout', 'login-success'])
// 或带验证
defineEmits({
  logout: null,
  'login-success': (user) => {
    return user && typeof user.name === 'string'
  }
})

三兄弟身份档案(必背)

符号 长写 身份 方向 场景
: v-bind: 动态绑定 父 → 子(prop) 变量塞给子组件
@ v-on: 事件监听 子 → 父(emit) 子组件喊"爸,有人点我!"
. 修饰符 语法糖plus —— 如 @click.stop

记住口诀:

"有冒号传变量,无冒号传字面量;有 @ 等孩子喊妈。 "


示例代码

父组件 (App.vue):状态的“总指挥官”

<template>
  <div>
    <Header
      :is-logged-in="isLoggedIn"
      :username="username"
      @logout="handleLogout"
      @login-success="handleLoginSuccess"
    />
    <p>当前登录状态: {{ isLoggedIn ? '已登录' : '未登录' }}</p>
  </div>
</template>

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

// 定义父组件的状态
const isLoggedIn = ref(false);
const username = ref('');

// 定义处理子组件发出的事件的方法
const handleLogout = () => {
  isLoggedIn.value = false;
  username.value = '';
  console.log('✅ 父组件收到登出信号,状态已更新!');
};

const handleLoginSuccess = (user) => {
  isLoggedIn.value = true;
  username.value = user.name;
  console.log(`✅ 父组件收到登录成功信号,用户:${user.name}!`);
};
</script>

子组件 (Header.vue):事件的“执行者”

<template>
  <header>
    <div v-if="isLoggedIn">
      <span>欢迎,{{ username }}</span>
      <button @click="logout">登出</button>
    </div>
    <div v-else>
      <button @click="login">登录</button>
    </div>
  </header>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

// 接收父组件传递的props
defineProps({
  isLoggedIn: Boolean,
  username: String,
});

// 声明子组件将要发出的事件
const emit = defineEmits(['logout', 'login-success']);

// 触发登出事件
const logout = () => {
  console.log('➡️ 子组件发出登出信号...');
  emit('logout');
};

// 触发登录成功事件,并传递参数
const login = () => {
  const user = { name: '小明' };
  console.log('➡️ 子组件发出登录成功信号,并附带用户信息...');
  emit('login-success', user);
};
</script>

流程图

image.png


⚠️ 常见坑点:

  • 坑1:在子组件中直接修改 prop

    // ❌ 错误做法
    props.isLoggedin = false // 会报警告
    
    // ✅ 正确做法
    emit('update:isLoggedin', false) // 或使用 v-model
    
  • 坑2:忘记声明 emits

    // ❌ 未声明的事件在 strict 模式下会报警告
    emit('logout')
    
    // ✅ 正确做法
    const emit = defineEmits(['logout'])
    
  • 坑3:事件名大小写错误

    <!-- ❌ 模板中不能用 camelCase -->
    @loginSuccess="handleLoginSuccess"
    
    <!-- ✅ 必须用 kebab-case -->
    @login-success="handleLoginSuccess"
    
  • 坑4:静态字符串导致布尔值失效

    <!-- ❌ 恒为真,变量失效 -->
    is-logged-in="true"
    
    <!-- ✅ 使用绑定,让 Vue 知道这是 JS 表达式 -->
    :is-logged-in="true"
    
  • 坑5:emit 名称与声明大小写不一致

    // ❌ 与声明不符,控制台警告
    emit('loginSuccess')
    
    // ✅ 与模板保持一致
    emit('login-success')
    
  • 坑6:prop 类型对不上,dev 爆红

    // ❌ 类型对不上,dev 直接爆红
    defineProps({ isLoggedIn: String })
    
    // ✅ 类型保持一致
    defineProps({ isLoggedIn: Boolean })
    

🌙 温馨收尾:凌晨两点的顿悟

小汐兴奋地拍了拍我的肩膀:“原来如此!这样一讲,我感觉整个组件的通信逻辑都清晰了。怪不得你总是说,理解了 propsemit,就掌握了 Vue 的精髓!”

我看着她远去的背影,心里默默想道:今天下午的摸鱼时间没了,掘金我都还没看呢,这波真是亏大了

昨天 — 2025年10月14日首页

【vue篇】Vue.js 2025:为何全球开发者都在拥抱这个前端框架?

作者 LuckySusu
2025年10月14日 23:03

在 React、Angular、Svelte 等众多前端框架中,Vue.js 凭借其独特的设计理念,持续赢得开发者青睐。

“Vue 到底强在哪?” “为什么中小企业首选 Vue?” “它的性能真的比 React 快吗?”

本文将从 轻量易学响应式生态,全面解析 Vue 的六大核心优势。


一、🔥 优势 1:极致轻量,启动飞快

Vue 3 (gzip): ~22KB
React 18 (gzip): ~40KB + react-dom

✅ 轻量带来的好处:

优势 说明
快速加载 移动端、低网速环境体验更佳
首屏更快 TTI(可交互时间)提前
Bundle 更小 减少用户流量消耗
// CDN 引入,5 秒上手
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

💡 Vue 是“渐进式框架”,你可以从 <script> 开始,逐步升级到 Vue CLI / Vite。


二、📚 优势 2:简单易学,中文友好

🌍 国人开发,文档贴心

  • 中文文档:官方文档翻译精准,无语言障碍;
  • 渐进式学习:从模板 → Options API → Composition API,平滑过渡;
  • 开发者友好:错误提示清晰,调试工具强大。

🎯 学习曲线对比

阶段 Vue React
第一天 能写 v-model 需理解 JSX、state
第一周 掌握组件通信 理解 Hooks、不可变性
第一个月 上线项目 仍在优化性能

Vue 是前端新手的“最佳第一课”


三、🔁 优势 3:双向数据绑定,开发更高效

<template>
  <!-- v-model:自动同步 -->
  <input v-model="message" />
  <p>{{ message }}</p>
</template>

<script>
export default {
  data() {
    return { message: 'Hello' }
  }
}
</script>

🆚 对比 React

// React:手动同步
function Input() {
  const [message, setMessage] = useState('');
  return (
    <input 
      value={message} 
      onChange={e => setMessage(e.target.value)} 
    />
  );
}

💥 Vue 的 v-model 让表单操作减少 50% 代码量


四、🧩 优势 4:组件化,复用无处不在

<!-- Button.vue -->
<template>
  <button :class="`btn-${type}`" @click="$emit('click')">
    <slot></slot>
  </button>
</template>
<!-- 使用 -->
<Btn type="primary" @click="save">保存</Btn>
<Btn type="danger">删除</Btn>

✅ 组件化优势:

优势 说明
UI 一致性 全站按钮风格统一
开发效率 修改一处,全局生效
团队协作 设计师 + 前端可共建组件库

📌 Vue 的单文件组件(.vue)将 模板、逻辑、样式 封装在一起,清晰易维护。


五、🧱 优势 5:关注点分离,结构清晰

视图 (template)    ←→    数据 (data)
       ↑                    ↑
   用户操作           状态管理 (Vuex/Pinia)

✅ 三大分离:

  1. 视图与数据分离

    • 修改 data,视图自动更新;
    • 无需手动操作 DOM。
  2. 结构与样式分离

    • <style scoped> 避免样式污染;
    • 支持 CSS Modules、PostCSS。
  3. 逻辑与模板分离

    • setup() / methods 集中处理业务逻辑;
    • 模板只负责展示。

💡 这种分离让维护成本大幅降低


六、⚡ 优势 6:虚拟 DOM + 响应式 = 性能王者

🎯 Vue 的性能优势在哪?

机制 说明
自动依赖追踪 渲染时自动收集依赖,只更新相关组件
细粒度更新 不像 React 默认全量 diff
编译优化 Vue 3 的 PatchFlag 标记动态节点,跳过静态节点
Tree-shaking 按需引入,减少打包体积

📊 性能对比(同场景)

操作 Vue 3 React 18
列表更新(1000项) ✅ 60fps ⚠️ 需 React.memo 优化
首次渲染 ✅ 更快 ❌ Bundle 更大
内存占用 ✅ 更低 ⚠️ 较高

💥 Vue 的响应式系统是“智能的”,它知道谁依赖谁,无需手动优化。


七、🚀 2025 Vue 生态全景

工具 说明
Vite 下一代构建工具,秒级启动
Pinia Vue 3 官方状态管理,TypeScript 友好
Vue Router 官方路由,支持懒加载
Nuxt.js SSR / SSG 框架,SEO 友好
UnoCSS 原子化 CSS,极速样式开发
# 5 秒创建项目
npm create vue@latest

💡 结语

“Vue 不是最快的框架,但可能是最平衡的。”

优势 说明
轻量 22KB,CDN 可用
易学 中文文档,渐进式学习
高效 v-model、组件化减少代码量
清晰 关注点分离,维护简单
性能 响应式 + 虚拟 DOM,自动优化
生态 Vite + Pinia + Nuxt,现代开发闭环

【vue篇】React vs Vue:2025 前端双雄终极对比

作者 LuckySusu
2025年10月14日 23:03

在选择前端框架时,你是否在 React 和 Vue 之间犹豫不决?

“React 和 Vue 到底有什么区别?” “哪个更适合我的团队?” “它们的未来趋势如何?”

本文将从 数据流模板响应式生态,全面解析 React 与 Vue 的异同。


一、相似之处:现代前端的共同基石

特性 React Vue
核心库 聚焦 UI 渲染 聚焦 UI 渲染
虚拟 DOM ✅ 支持 ✅ 支持(Vue 2+)
组件化 ✅ 鼓励 ✅ 鼓励
构建工具 Create React App Vue CLI / Vite
状态管理 Redux / MobX / Zustand Vuex / Pinia
路由 React Router Vue Router

✅ 两者都遵循 现代前端最佳实践:组件化、虚拟 DOM、单向数据流。


二、核心差异:哲学与设计

🎯 1. 数据流:双向 vs 单向

框架 数据流 示例
Vue 默认支持双向绑定v-model <input v-model="msg" />
React 严格单向数据流 <input value={msg} onChange={e => setMsg(e.target.value)} />

💡 Vue 更“贴心”,React 更“可控”。


🎯 2. 模板 vs JSX:声明式 UI 的两种范式

Vue:HTML 扩展式模板

<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <ChildComponent 
      :msg="message" 
      @update="handleUpdate" 
    />
  </div>
</template>
  • ✅ 语法接近 HTML,设计师友好;
  • ✅ 指令系统(v-if, v-for)简洁;
  • ❌ 逻辑能力有限,复杂逻辑需写在 script

React:JSX(JavaScript XML)

function App() {
  const [msg, setMsg] = useState('');
  
  return (
    <div className="container">
      <h1>{title}</h1>
      <ChildComponent 
        msg={msg} 
        onUpdate={handleUpdate} 
      />
    </div>
  );
}
  • ✅ 逻辑与 UI 在同一文件,更灵活;
  • ✅ 可用完整 JavaScript 表达式;
  • ❌ 学习成本略高(JSX 语法)。

🎯 3. 响应式系统:谁更高效?

框架 实现原理 性能特点
Vue getter/setter 拦截(Vue 2)
Proxy(Vue 3)
自动依赖追踪
无需手动优化,更新粒度更细
React 手动触发更新setState
默认全量 diff
❌ 可能导致不必要的渲染
✅ 可通过 useMemo/useCallback/React.memo 优化

💥 Vue 的响应式是“自动挡”,React 是“手动挡”。


🎯 4. 组件通信与复用

React:高阶组件(HOC)与 Hooks

// HOC
const withLogger = (Component) => {
  return (props) => {
    console.log('Render:', props);
    return <Component {...props} />;
  };
};

// Hooks
function useCounter() {
  const [count, setCount] = useState(0);
  return { count, increment: () => setCount(c => c + 1) };
}
  • ✅ 函数式,组合能力强;
  • ✅ Hooks 解决了 mixin 的问题。

Vue:Mixins 与 Composition API

// Mixin(Vue 2)
const logMixin = {
  created() {
    console.log('Component created');
  }
};

// Composition API(Vue 3)
function useCounter() {
  const count = ref(0);
  const increment = () => count.value++;
  return { count, increment };
}

💡 Vue 3 的 Composition API 已向 React Hooks 靠拢。


🎯 5. 监听数据变化的实现

框架 实现方式 特点
Vue Object.defineProperty / Proxy 精确追踪
知道哪个属性变了
React 引用比较(shallowEqual) ❌ 不比较值,只比较引用
✅ 鼓励不可变数据(Immutability)

📌 Vue:可变数据 + 精确更新
📌 React:不可变数据 + 手动优化


🎯 6. 跨平台能力

平台 React Vue
Web
移动端 React Native(成熟) Weex(已停止维护)
UniApp(第三方)
桌面端 Electron + React Electron + Vue
小程序 Taro / Remax UniApp / Taro

✅ React 在跨平台(尤其是移动端)生态更强大。


🎯 7. 学习曲线

框架 学习难度 适合人群
Vue ⭐⭐☆ 初学者、HTML 开发者
React ⭐⭐⭐ 有 JavaScript 基础的开发者
  • Vue:渐进式,从模板开始;
  • React:需理解 JSX、状态、不可变性。

三、实战对比:同一个功能

需求:计数器组件

Vue 3(Composition API)

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

const count = ref(0);
const increment = () => count.value++;
</script>

<template>
  <button @click="increment">Count: {{ count }}</button>
</template>

React 18(Hooks)

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(c => c + 1);
  
  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  );
}

💡 代码结构高度相似!Vue 3 的 Composition API 明显受 React Hooks 启发。


四、如何选择?

你的需求 推荐框架
快速上手,团队有 HTML 经验 Vue
复杂应用,需要强大状态管理 React
移动端开发(React Native) React
小程序(UniApp) Vue
喜欢函数式编程 React
喜欢模板语法 Vue

💡 结语

“React 和 Vue 不是敌人,而是共同推动前端进步的力量。”

维度 React Vue
哲学 “Just JavaScript” “渐进式框架”
模板 JSX(JavaScript) 模板(HTML 扩展)
响应式 手动触发 自动追踪
复用 Hooks / HOC Composition API / Mixins
生态 更大(尤其移动端) 更聚焦 Web
学习曲线 较陡 较平缓

🚀 2025 趋势

  • Vue 3 + Composition API:向 React Hooks 学习,提升逻辑复用;
  • React Server Components:服务端渲染新范式;
  • Vite:取代 Webpack,成为新一代构建工具(Vue 和 React 都支持)。

选择建议

  • 团队新手多?→ Vue
  • 需要跨平台?→ React
  • 追求最新技术?→ 两者都支持 Reactivity、SSR、Micro Frontends。

【vue篇】Vue 响应式核心:依赖收集机制深度解密

作者 LuckySusu
2025年10月14日 23:02

在 Vue 应用中,你是否好奇:

“当我修改 this.message 时,DOM 为何能自动更新?” “为什么只有被模板用到的数据才会触发更新?” “Vue 是如何知道哪个组件依赖哪个数据的?”

这一切的背后,是 Vue 依赖收集(Dependency Collection) 的精妙设计。

本文将从 Object.definePropertyDep-Watcher 模型,彻底解析 Vue 2 的响应式原理。


一、核心结论:依赖收集 = 数据 ↔ 视图 的双向绑定

数据变化 → 通知视图更新
     ↑          ↓
   收集      触发 getter
  • 谁收集? Dep(依赖中心)
  • 被谁收集? Watcher(观察者)
  • 何时收集? 组件渲染时读取数据(触发 getter)

二、三大核心角色

🎯 1. defineReactive:让数据“响应式”

function defineReactive(obj, key, val) {
  // 每个属性都有一个独立的依赖中心
  const dep = new Dep();
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // ✅ 依赖收集:谁在读我?
      if (Dep.target) {
        dep.depend(); // 通知 dep:当前 watcher 依赖我
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      // ✅ 派发更新:通知所有依赖者
      dep.notify();
    }
  });
}

💥 dep 是每个属性的“私人秘书”,记录谁依赖它。


🎯 2. Dep:依赖管理中心

class Dep {
  static target = null; // 🌟 全局唯一,指向当前正在计算的 Watcher
  subs = []; // 存储所有依赖此数据的 Watcher

  // 被收集:当前 Watcher 依赖我
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this); // 告诉 Watcher:你依赖我
    }
  }

  // 添加订阅者
  addSub(watcher) {
    this.subs.push(watcher);
  }

  // 派发更新:数据变了!
  notify() {
    // 避免在 notify 时修改数组
    const subs = this.subs.slice();
    for (let i = 0; i < subs.length; i++) {
      subs[i].update(); // 通知每个 Watcher 更新
    }
  }
}

🔑 Dep.target 是关键:它确保同一时间只有一个 Watcher 在收集依赖。


🎯 3. Watcher:观察者(组件/计算属性)

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    this.getter = expOrFn; // 如 vm._update(vm._render())
    this.cb = cb;
    this.deps = [];      // 记录依赖了哪些 dep
    this.depIds = new Set(); // 去重
    this.value = this.get(); // 🚀 首次执行,触发依赖收集
  }

  // 读取数据,触发 getter
  get() {
    pushTarget(this); // 设置当前 Watcher
    const value = this.getter.call(this.vm, this.vm);
    popTarget(); // 清除
    return value;
  }

  // 被 dep 收集
  addDep(dep) {
    const id = dep.id;
    if (!this.depIds.has(id)) {
      this.depIds.add(id);
      this.deps.push(dep);
      dep.addSub(this); // dep 记录我
    }
  }

  // 更新:数据变化后调用
  update() {
    queueWatcher(this); // 异步更新
  }

  run() {
    const value = this.get(); // 重新计算
    this.cb(value, this.value); // 执行回调(如更新 DOM)
    this.value = value;
  }
}

💡 Watcher 是“消费者”,它知道自己依赖哪些数据。


三、依赖收集全过程(图文解析)

🔄 阶段 1:初始化响应式数据

// data: { message: 'Hello' }
defineReactive(data, 'message', 'Hello');
// → 为 message 创建 dep 实例
data.message
     ↓
   dep (subs: [])

🔄 阶段 2:组件挂载,创建 Watcher

new Watcher(vm, () => {
  vm._update(vm._render());
});
  • Watcher.get() 被调用;
  • pushTarget(this)Dep.target = watcher
Dep.target → Watcher实例

🔄 阶段 3:渲染触发 getter,完成收集

vm._render(); // 生成 VNode
// 模板中:{{ message }}
// → 读取 this.message → 触发 getter
// getter 执行
get() {
  if (Dep.target) {
    dep.depend(); // dep.depend()
  }
  return val;
}
// dep.depend()
depend() {
  Dep.target.addDep(this); // watcher.addDep(dep)
}
// watcher.addDep(dep)
addDep(dep) {
  this.deps.push(dep);
  dep.addSub(this); // dep.subs.push(watcher)
}

收集完成!

data.message
     ↓
   dep (subs: [watcher])
     ↑
Watcher (deps: [dep])

🔄 阶段 4:数据变化,派发更新

this.message = 'World'; // 触发 setter
// setter 执行
set(newVal) {
  val = newVal;
  dep.notify(); // 通知所有 subs
}
// dep.notify()
notify() {
  this.subs.forEach(watcher => watcher.update());
}

queueWatcher(watcher) → 异步更新 DOM。


四、实战演示:一个简单的响应式系统

// 1. 数据
const data = { count: 0 };

// 2. 响应式化
defineReactive(data, 'count', 0);

// 3. 创建 Watcher(模拟组件)
new Watcher(null, () => {
  console.log('Render:', data.count);
}, null);
// → 触发 getter → 依赖收集完成

// 4. 修改数据
data.count = 1;
// → 触发 setter → dep.notify() → Watcher.update()
// → 输出:Render: 1

五、Vue 3 的改进:Proxy + effect

// Vue 3 使用 Proxy
const reactiveData = reactive({ count: 0 });

effect(() => {
  console.log(reactiveData.count); // 收集依赖
});

reactiveData.count++; // 触发更新
  • 优势
    • 支持动态新增属性;
    • 性能更好(无需递归 defineProperty);
    • 代码更简洁。

💡 结语

“依赖收集是 Vue 响应式的灵魂。”

角色 职责
defineReactive 拦截 getter/setter
Dep 管理订阅者(Watcher)
Watcher 观察数据变化,执行更新
过程 关键操作
初始化 defineReactive
收集 Dep.target = watcher + dep.depend()
更新 dep.notify()watcher.update()

掌握依赖收集机制,你就能:

✅ 理解 Vue 响应式原理;
✅ 调试响应式问题;
✅ 设计自己的响应式系统;
✅ 顺利过渡到 Vue 3 的 reactiveeffect

【vue篇】Vue 单向数据流铁律:子组件为何不能直接修改父组件数据?

作者 LuckySusu
2025年10月14日 23:02

在 Vue 开发中,你是否写过这样的代码:

<!-- ChildComponent.vue -->
<template>
  <button @click="changeParentData">修改父数据</button>
</template>

<script>
export default {
  methods: {
    changeParentData() {
      // ❌ 危险操作!
      this.$parent.formData.name = 'Hacker';
    }
  }
}
</script>

“为什么 Vue 要禁止子组件修改父数据?” “直接改不是更方便吗?” “如果必须改,该怎么办?”

本文将从 设计哲学实战模式,彻底解析 Vue 的单向数据流原则。


一、核心结论:绝对禁止!

子组件绝不能直接修改父组件的数据。

<!-- Parent.vue -->
<template>
  <Child :user="user" />
</template>

<script>
export default {
  data() {
    return {
      user: { name: 'Alice', age: 20 }
    }
  }
}
</script>
<!-- Child.vue -->
<script>
export default {
  props: ['user'],
  methods: {
    // ❌ 错误:直接修改 prop
    badWay() {
      this.user.name = 'Bob'; // ⚠️ Vue 会警告!
    },
    
    // ✅ 正确:通过事件通知父组件
    goodWay() {
      this.$emit('update:user', { ...this.user, name: 'Bob' });
    }
  }
}
</script>

二、为什么禁止?三大核心原因

🚫 1. 破坏单向数据流

父组件 → (props) → 子组件
   ↑
   └── (events) ← 子组件
  • 单向:数据流动清晰可预测;
  • 双向:数据可能从任意子组件修改,形成“意大利面条式”数据流。

💥 复杂应用中,你将无法追踪数据变化来源。


🚫 2. 导致难以调试

// 10 个子组件都可能修改 user.name
// 问题:name 何时、何地、被谁修改?
  • 控制台警告:

    [Vue warn]: Avoid mutating a prop directly...
    
  • 调试时需检查 所有子组件$emit$parent 调用。


🚫 3. 组件复用性降低

<!-- 假设 Child 可以直接修改 user -->
<Child :user="user1" />
<Child :user="user2" />

<!-- 如果 Child 修改了 user1,user2 也会被意外修改(引用传递) -->

✅ 组件应是“纯”的:相同输入 → 相同输出。


三、正确修改父数据的 4 种方式

✅ 1. v-model / .sync 修饰符(Vue 2)

方式一:v-model(默认 value / input

<!-- Parent -->
<Child v-model="userName" />

<!-- Child -->
<input 
  :value="value" 
  @input="$emit('input', $event.target.value)" 
/>

方式二:.sync 修饰符

<!-- Parent -->
<Child :user.sync="user" />

<!-- Child -->
<button @click="$emit('update:user', { ...user, name: 'New' })">
  更新
</button>

💡 .sync 本质是 :user + @update:user 的语法糖。


✅ 2. 自定义事件($emit

<!-- Parent -->
<Child 
  :config="config" 
  @change-config="updateConfig" 
/>

<!-- Child -->
<button @click="$emit('change-config', newConfig)">
  修改配置
</button>
// Parent method
updateConfig(newConfig) {
  this.config = newConfig;
}

✅ 3. 作用域插槽(传递方法)

<!-- Parent -->
<Child>
  <template #default="{ updateUser }">
    <button @click="updateUser({ name: 'New' })">
      通过插槽修改
    </button>
  </template>
</Child>
<!-- Child -->
<template>
  <div>
    <slot :updateUser="updateUser" />
  </div>
</template>

<script>
export default {
  methods: {
    updateUser(newData) {
      this.$emit('update:user', newData);
    }
  }
}
</script>

✅ 4. 状态管理(Vuex / Pinia)

// store.js
const userStore = defineStore('user', {
  state: () => ({ user: { name: 'Alice' } }),
  actions: {
    updateUser(payload) {
      this.user = { ...this.user, ...payload };
    }
  }
});

// Child.vue
import { useUserStore } from '@/stores/user';

export default {
  setup() {
    const userStore = useUserStore();
    return {
      updateUser: () => userStore.updateUser({ name: 'Bob' })
    }
  }
}

✅ 适合跨层级、复杂状态


四、特殊情况:如何“安全”地修改?

⚠️ 仅当 prop 是“配置对象”时

<!-- Parent -->
<Child :options="chartOptions" />

<!-- Child -->
<script>
export default {
  props: ['options'],
  mounted() {
    // ✅ 安全:只读取,不修改
    const chart = new Chart(this.$el, this.options);
  }
}
</script>

❌ 即使是配置对象,也不应修改其属性。


五、Vue 3 中的 definePropsdefineEmits

<script setup>
const props = defineProps(['user']);
const emit = defineEmits(['update:user']);

function changeName() {
  emit('update:user', { ...props.user, name: 'Charlie' });
}
</script>

definePropsdefineEmits 是 Vue 3 <script setup> 的推荐方式。


💡 结语

“单向数据流不是限制,而是自由的保障。”

方式 适用场景
$emit 简单父子通信
.sync / v-model 双向绑定场景
作用域插槽 需要传递方法
Vuex/Pinia 复杂全局状态
反模式 正确做法
this.$parent.xxx = value $emit('update:xxx', value)
直接修改 prop 对象属性 通过事件通知父组件

记住:

“子组件只应通过事件告诉父组件‘我想改变’,而非直接动手。”

掌握这一原则,你就能:

✅ 构建可维护的大型应用;
✅ 快速定位数据变更问题;
✅ 提升组件复用性;
✅ 为迁移到 Pinia 打下基础。

【vue篇】Vue 自定义指令完全指南:从入门到高级实战

作者 LuckySusu
2025年10月14日 23:02

在 Vue 开发中,你是否遇到过:

“如何让输入框自动聚焦?” “如何实现图片懒加载?” “如何集成 Chart.js 到 Vue 组件?”

数据驱动 无法满足需求时,自定义指令(Custom Directives)就是你的终极武器。

本文将从 基础语法高级实战,全面解析 Vue 自定义指令的用法与原理。


一、为什么需要自定义指令?

✅ Vue 的哲学

数据驱动视图” —— 大部分情况下,你只需修改数据,Vue 自动更新 DOM。

❌ 但有些场景例外

场景 数据驱动不足
输入框聚焦 无数据变化
图片懒加载 需监听 scroll 事件
集成第三方库(如 DatePicker 需直接操作 DOM
按钮权限控制(v-permission) 需动态显示/隐藏

💥 这些场景需要直接操作 DOM,此时自定义指令是最佳选择。


二、基础语法:钩子函数详解

📌 钩子函数执行时机

bind → inserted → update → componentUpdated → unbind
钩子 触发时机 典型用途
bind 指令第一次绑定到元素 初始化设置(如添加事件监听)
inserted 元素插入父节点 访问 DOM 尺寸、位置
update 组件 VNode 更新时 值变化时更新 DOM
componentUpdated 组件及其子组件更新后 执行依赖完整 DOM 的操作
unbind 指令解绑时 清理事件、定时器

🎯 钩子函数参数

function myDirective(el, binding, vnode, prevVnode) {
  // el: 绑定的 DOM 元素
  // binding: 指令对象
  // vnode: 虚拟节点
  // prevVnode: 上一个 VNode(仅 update/componentUpdated)
}

binding 对象详解

属性 示例 说明
value v-my-dir="msg"msg 的值 指令绑定的值
oldValue 更新前的值 仅在 update/componentUpdated 中可用
arg v-my-dir:arg'arg' 传入的参数
modifiers v-my-dir.mod1.mod2{ mod1: true, mod2: true } 修饰符对象
expression v-my-dir="a + b"'a + b' 绑定的表达式字符串

三、定义方式

✅ 1. 全局指令

Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
});

✅ 2. 局部指令

<template>
  <input v-focus />
</template>

<script>
export default {
  directives: {
    focus: {
      inserted(el) {
        el.focus();
      }
    }
  }
}
</script>

四、初级应用:5 个经典案例

🎯 1. 自动聚焦(v-focus

Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
});
<input v-focus />

🎯 2. 点击外部关闭(v-click-outside

Vue.directive('click-outside', {
  bind(el, binding) {
    const handler = (e) => {
      if (!el.contains(e.target)) {
        binding.value(e); // 执行传入的函数
      }
    };
    document.addEventListener('click', handler);
    el._clickOutside = handler;
  },
  unbind(el) {
    document.removeEventListener('click', el._clickOutside);
  }
});
<div v-click-outside="closeMenu">菜单</div>

🎯 3. 相对时间(v-timeago

Vue.directive('timeago', {
  bind(el, binding) {
    const date = new Date(binding.value);
    el.textContent = `${Math.floor((Date.now() - date) / 60000)}分钟前`;
  },
  update(el, binding) {
    // 值变化时更新
    if (binding.value !== binding.oldValue) {
      const date = new Date(binding.value);
      el.textContent = `${Math.floor((Date.now() - date) / 60000)}分钟前`;
    }
  }
});
<span v-timeago="post.createdAt"></span>

🎯 4. 按钮权限(v-permission

Vue.directive('permission', {
  bind(el, binding) {
    const userRoles = this.$store.getters.roles;
    if (!userRoles.includes(binding.value)) {
      el.parentNode.removeChild(el); // 移除无权限的按钮
    }
  }
});
<button v-permission="'admin'">删除</button>

🎯 5. 滚动动画(v-scroll

Vue.directive('scroll', {
  inserted(el, binding) {
    const onScroll = () => {
      if (window.scrollY > 100) {
        el.classList.add('scrolled');
      } else {
        el.classList.remove('scrolled');
      }
    };
    window.addEventListener('scroll', onScroll);
    el._scrollHandler = onScroll;
  },
  unbind(el) {
    window.removeEventListener('scroll', el._scrollHandler);
  }
});
<header v-scroll></header>

五、高级应用:2 个深度实战

🚀 1. 图片懒加载(v-lazy

const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.classList.remove('lazy');
      imageObserver.unobserve(img);
    }
  });
});

Vue.directive('lazy', {
  bind(el, binding) {
    el.dataset.src = binding.value;
    el.classList.add('lazy');
    imageObserver.observe(el);
  },
  update(el, binding) {
    if (binding.value !== binding.oldValue) {
      el.dataset.src = binding.value;
      // 如果已进入视口,立即加载
      if (el.getBoundingClientRect().top < window.innerHeight * 1.5) {
        el.src = binding.value;
      }
    }
  },
  unbind(el) {
    imageObserver.unobserve(el);
  }
});
<img v-lazy="imageUrl" />

🚀 2. 集成 ECharts(v-chart

Vue.directive('chart', {
  bind(el) {
    el._chart = echarts.init(el);
  },
  update(el, binding) {
    const chart = el._chart;
    if (binding.value) {
      chart.setOption(binding.value, true);
    }
  },
  unbind(el) {
    el._chart.dispose();
  }
});
<div v-chart="chartOption" style="width: 400px; height: 300px;"></div>

六、重要注意事项

⚠️ 1. 不要修改 v-model 绑定的值

<input v-model="msg" v-my-directive />
  • ❌ 在指令中直接 el.value = 'new'msg 不会更新;
  • ✅ 正确做法:触发 inputchange 事件。
el.value = 'new';
el.dispatchEvent(new Event('input'));

⚠️ 2. 清理副作用

  • unbind 中移除事件监听;
  • 清除定时器;
  • 销毁第三方实例(如 ECharts)。

⚠️ 3. 性能优化

  • 避免在 update 中做昂贵操作;
  • 使用 binding.valuebinding.oldValue 判断是否需要更新。

💡 结语

“自定义指令是 Vue 的‘最后一公里’解决方案。”

场景 推荐方案
简单 DOM 操作 自定义指令
复杂逻辑复用 Mixin / Composition API
UI 组件 普通组件
钩子 使用场景
bind 初始化
inserted 访问布局
update 值变化
unbind 清理资源

掌握自定义指令,你就能:

✅ 实现原生 DOM 操作;
✅ 集成第三方库;
✅ 创建可复用的 DOM 行为;
✅ 补充数据驱动的不足。

vue3事件总线与emit

作者 九十一
2025年10月14日 17:55

ccd29c6c-26b6-4e79-8b92-45e14ce4a24a.png

1.vue3为什么去掉了$on$off?

1.设计理念的调整

Vue 3 更加注重组件间通信的明确性和可维护性。$on 这类事件 API 本质上是一种 "发布 - 订阅" 模式,容易导致组件间关系模糊(多个组件可能监听同一个事件,难以追踪事件来源和流向)。Vue 3 推荐使用更明确的通信方式,如: - 父子组件通过 props 和 emit 通信 - 跨组件通信使用 provide/inject 或 Pinia/Vuex 等状态管理库 - 复杂场景可使用专门的事件总线库(如 mitt

2.与 Composition API 的适配

Vue 3 主推的 Composition API 强调逻辑的封装和复用,而 $on 基于选项式 API 的实例方法,与 Composition API 的函数式思维不太契合。移除后,开发者可以更自然地使用响应式变量或第三方事件库来实现类似功能。

3.减少潜在问题

  • $on 容易导致内存泄漏(忘记解绑事件)
  • 事件名称可能冲突(全局事件总线尤其明显)
  • 不利于 TypeScript 类型推断,难以实现类型安全

vue3中如何使用事件总线实现跨级组件之间的通信?

1.可以通过第三方库(如 mitt 或 tiny-emitter)替代,示例如下:

// 安装 mitt:npm install mitt
import mitt from 'mitt'

// 创建事件总线实例
const emitter = mitt()

// 监听事件
emitter.on('event-name', (data) => {
  console.log('收到事件:', data)
})

// 触发事件
emitter.emit('event-name', { message: 'hello' })

// 移除事件监听
emitter.off('event-name', handler)

2.使用 Vue3 提供的 provide/inject

// 父组件提供事件总线
import { provide, ref } from 'vue'

export default {
  setup() {
    const events = ref({})
    
    const on = (name, callback) => {
      events.value[name] = callback
    }
    
    const emit = (name, data) => {
      if (events.value[name]) {
        events.value[name](data)
      }
    }
    
    provide('eventBus', { on, emit })
  }
}

// 子组件使用
import { inject } from 'vue'

export default {
  setup() {
    const eventBus = inject('eventBus')
    
    // 监听事件
    eventBus.on('event-name', (data) => {
      // 处理逻辑
    })
    
    // 发送事件
    eventBus.emit('event-name', data)
  }
}

3.利用 Vue 实例的自定义事件 虽然 Vue3 移除了 $on$off 等方法,但可以创建一个空的 Vue 实例作为事件总线,利用其自定义事件 API:

// eventBus.js
import { createApp } from 'vue'
const app = createApp({})
export default app



// 发送事件
import bus from './eventBus'
bus.config.globalProperties.$emit('event-name', data)

// 监听事件(在组件中)
import { getCurrentInstance } from 'vue'
export default {
  mounted() {
    const instance = getCurrentInstance()
    instance.appContext.config.globalProperties.$on('event-name', (data) => {
      // 处理逻辑
    })
  }
}

4.使用 reactive 创建事件总线

// 组件A中发送事件
import eventBus from './eventBus'
eventBus.emit('user-updated', { name: '张三' })

// 组件B中监听事件
import eventBus from './eventBus'
export default {
  mounted() {
    this.handleUserUpdate = (user) => {
      console.log('用户更新了', user)
    }
    eventBus.on('user-updated', this.handleUserUpdate)
  },
  beforeUnmount() {
    // 组件卸载时移除监听,避免内存泄漏
  1. 使用 Pinia/Vuex 状态管理

对于复杂应用,更推荐使用状态管理库来处理组件间通信,通过修改共享状态来实现组件间的数据传递。

总结

  • 在 Vue3 中实现事件总线,最推荐的方式是使用 mitt 库,它轻量高效且 API 简洁,能够很好地替代 Vue2 中的事件总线功能。对于简单场景也可以使用 provide/inject 方案,但对于大型应用,状态管理库会是更优选.择。
  • 手动实现的事件总线需要注意在组件卸载时移除事件监听,避免内存泄漏; 注意考虑 “同一事件绑定多个回调” 的去重逻辑;避免没有事件触发时的异常捕获,单个回调报错可能阻断整个事件流程。

2.vue3中的defineEmits $emit又是什么关系?

Vue3 并没有完全移除 $emit(它仍然用于子组件向父组件传递事件)。

defineEmits是 Vue3 提供的编译时宏命令(Compiler Macro),用于在 <script setup> 语法中声明组件可以触发的事件,主要作用是:

  1. 明确组件对外暴露的事件,提升代码可读性和可维护性
  2. 提供 TypeScript 类型校验(如果使用 TS)
  3. 在开发环境下对未声明的事件触发给出警告

使用方式(在 <script setup> 中)

<template>
  <button @click="handleClick">点击触发事件</button>
</template>

<script setup>
// 声明组件可以触发的事件
const emit = defineEmits(['change', 'update'])

const handleClick = () => {
  // 触发事件并传递数据
  emit('change', 'hello')
  emit('update', { id: 1, name: 'test' })
}
</script>

带类型校验的用法(TypeScript)

<script setup lang="ts">
// 用类型标注事件名称和参数类型
const emit = defineEmits<{
  (e: 'change', value: string): void
  (e: 'update', data: { id: number; name: string }): void
}>()

// 错误示例:参数类型不匹配会报错
emit('change', 123) // TS 类型错误
</script>

注意点

  1. 仅在 <script setup> 中可用defineEmits 是编译时宏,不需要导入,直接使用(Vue 编译器会处理)

  2. 替代 Vue2 的 emits 选项:在非 <script setup> 语法中,仍可以用 emits 选项声明事件:

    export default {
      emits: ['change', 'update'], // 等价于 defineEmits
      setup(props, { emit }) {
        // ...
      }
    }
    
  3. 与 $emit 的关系defineEmits 返回的 emit 函数与 this.$emit 功能一致,但在 <script setup> 中推荐使用前者(更符合组合式 API 风格)

解决 Vite 代理中的 E RR_CONTENT_DECODING_FAILED 错误:禁用自动压缩的实践

作者 Neil鹏
2025年10月14日 08:18

最近在使用 Vite 开发一个 Vue3 项目时,遇到了一个颇为棘手的网络错误。项目配置了代理 ( serv er.proxy ) 将特定前缀(比如 /api )的请求转发到后端服务。大部分接口工作正常,但部分接口在浏览器控制台会抛出 ERR_ CONTENT_DECODING_FAILED 错误。这个错误通常意味着浏览器接收到了经过压缩(如 gzip, br)的响应内容,但无法正确解码。

排查过程

  1. 检查后端服务: 首先确认后端服务本身是正常的,直接访问后端接口 URL(不通过 Vite 代理)可以成功返回预期的 JSON 数据或其它内容,且响应头 Content-Encoding 显示后端确实返回了压缩内容(如 gzip )。

  2. 检查 Vite 代理配置: 基础的代理配置看起来没有问题:

    // vite.config.jsexport default defineConfig({ server: { proxy: { '/api': { target: ' your-backend-server.com', // 后端地址 changeOrigin: true, // 通常建议开启 rewrite: (path) => path.replace(/^/api/, ''), // 可选,重写路径 // ... 其他配置 ... } } }, // ... 其他配置 ...});

JavaScript

配置了 changeOrigin: true 确保请求头中的 Host 和 Origin 被正确修改以应对跨域问题。

  1. 对比请求差异: 使用浏览器开发者工具对比了通过 Vite 代理的请求和直接请求后端的请求/响应头信息。发现关键差异在于 Accept-Encoding 请求头:
  • 直接请求后端: 浏览器发送的 Accept-Encoding 通常包含 gzip, deflate, br 等,表明浏览器可以接受这些压缩格式。后端据此返回压缩内容并设置 Content-Encoding: gzip 。

  • 通过 Vite 代理请求: Vite 开发服务器在转发请求给后端时,默认也会带上 Accept-Encoding: gzip, deflate, br (或类似)的请求头。后端同样识别到这个头,并返回了压缩内容 ( Content-Encoding: gzip )。

  1. 问题定位: 问题出在 Vite 开发服务器对代理响应的处理上。当后端返回压缩内容时:
  • Vite 开发服务器(基于 http-proxy-middleware )接收到了这个压缩的响应体。

  • 试图将这个压缩的响应体原样转发给浏览器

  • 然而,浏览器在接收到这个响应时,发现响应头 Content-Encoding: gzip 存在,表明内容需要解压。

  • 浏览器尝试解压这个响应体,但失败了,导致 ERR_CONTENT_DECODING_FAILED 错误。

核心原因

Vite 代理默认行为是“透明”转发请求和响应。它不会主动解压后端返回的压缩内容,而是直接将其传递给前端浏览器。浏览器看到 Content-Encoding 头,就会尝试解压,但如果这个压缩流在传输或处理过程中出现任何不兼容或损坏(即使后端压缩本身是正确的,代理的传递过程也可能引入微妙的不兼容),或者浏览器对特定压缩算法的实现有细微差异,解压就可能失败。

解决方案:强制后端返回未压缩内容

既然问题源于浏览器无法正确处理代理转发的压缩响应,最直接的思路就是阻止后端返回压缩内容。我们可以在代理请求中明确告诉后端:“我不接受任何压缩格式,请给我原始(identity)内容”。

这就是通过设置 headers 选项中的 Accept-Encoding 来实现的:

// vite.config.jsexport default defineConfig({  server: {    proxy: {      '/api': {        target: ' https://your-backend-server.com',        changeOrigin: true,        rewrite: (path) => path.replace(/^\/api/, ''), // 可选        // 关键解决方案:添加 headers 配置        headers: {          'Accept-Encoding': 'identity', // 明确要求后端不要压缩响应体        },      }    }  },  // ... 其他配置 ...});

TypeScript

解释

  • headers 选项允许我们在 Vite 代理将请求转发给目标服务器(后端)之前,修改或添加请求头。

  • 设置 'Accept-Encoding': 'identity' :

  • Accept-Encoding 是 HTTP 请求头,用于告知服务器客户端能够理解的内容编码(压缩)方式。

  • identity 是一个特殊值,表示“不压缩”、“无编码”、“原样”。它明确告诉服务器:“请直接返回原始数据,不要进行任何压缩”。

  • 效果: 后端服务器收到这个请求头后,知道客户端(此时是 Vite 代理服务器,它代表浏览器)不接受压缩,因此会返回未经压缩的原始响应体,并且响应头中通常不会包含 Content-Encoding ,或者其值为 identity 。

  • 结果: Vite 代理将这个未压缩的响应体转发给浏览器。浏览器没有看到 Content-Encoding 头,或者看到 identity ,就知道内容不需要解压,直接使用即可。 ERR_CONTENT_DECODING_FAILED 错误消失。

总结与启示

  1. 问题本质: ERR_CONTENT_DECODING_FAILED 在 Vite 代理场景下,通常是由于代理直接转发了后端的压缩响应,而浏览器解压该响应时失败。

  2. 解决方案: 在 Vite 的代理配置 ( server.proxy[xxx].headers ) 中设置 'Accept-Encoding': 'identity' ,强制要求后端返回未压缩的原始内容。这消除了浏览器解压环节,从而避免了解压失败的错误。

  3. 权衡: 此方案的代价是牺牲了网络传输的压缩效率。未压缩的内容体积更大,可能会略微增加加载时间。但在开发环境或部分特定接口遇到此问题时,稳定性优先于那一点传输效率通常是更合理的选择。对于生产环境,静态资源应使用构建时预压缩(如 vite-plugin-compression ),并由服务器(如 Nginx)根据请求头 Accept-Encoding 动态提供正确的压缩版本或原始版本给浏览器。

  4. 排查技巧: 遇到代理相关问题时,仔细对比代理前后请求/响应头的差异是至关重要的第一步。开发者工具的网络面板是解决此类问题的利器。

Vue 3 组合式函数(Composables)全面解析:从原理到实战

作者 90后晨仔
2025年10月13日 22:11

一、前言

当 Vue 3 发布时,组合式 API(Composition API) 带来了一个革命性的变化:

我们不再需要依赖 data、methods、computed 这些分散的选项,而是能用函数的方式,灵活组织逻辑。

这套函数化逻辑复用方案,就叫做 组合式函数(Composables)

简单来说:

  • Options API 更像是“配置式”;

  • Composition API 则让我们“像写逻辑一样组织组件”。

组合式函数(Composables) ,就是在这个新体系下,用于封装和复用有状态逻辑的函数。


二、什么是组合式函数?

先来看一句官方定义:

“组合式函数是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。”

也就是说,它不仅可以处理计算逻辑、请求接口、事件监听,还能和组件生命周期绑定,并且是响应式的。

按照惯例,我们命名时一般以 use 开头

// useXxx 组合式函数命名惯例
export function useMouse() { ... }
export function useFetch() { ... }
export function useEventListener() { ... }

三、基础示例:从组件逻辑到组合式函数

假设我们要做一个“鼠标追踪器”,实时显示鼠标位置。

如果直接写在组件里,可能是这样 👇

<!-- MouseComponent.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

// 定义响应式状态
const x = ref(0)
const y = ref(0)

// 事件处理函数:更新坐标
function update(e) {
  x.value = e.pageX
  y.value = e.pageY
}

// 生命周期绑定
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>
  鼠标坐标:{{ x }}, {{ y }}
</template>

很好,但如果我们多个页面都要复用这个逻辑呢?

那就应该把它抽出来!


四、封装成组合式函数

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// 约定:组合式函数以 use 开头
export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  // 内部逻辑:跟踪鼠标移动
  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }

  // 生命周期钩子
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 返回需要暴露的状态
  return { x, y }
}

使用起来非常简单:

<!-- MouseComponent.vue -->
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>

<template>
  鼠标坐标:{{ x }}, {{ y }}
</template>

✅ 这样写的好处是:

  • 组件逻辑更清晰;
  • 多处可复用;
  • 生命周期自动关联;
  • 每个组件都拥有独立的状态(互不干扰)。

五、进阶封装:useEventListener

假如我们还想监听滚动、键盘等事件,可以进一步抽象出一个事件监听函数 👇

// event.js
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}

接着 useMouse 就能进一步简化:

// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  useEventListener(window, 'mousemove', (e) => {
    x.value = e.pageX
    y.value = e.pageY
  })

  return { x, y }
}

💡 这样我们不仅复用了逻辑,还建立了逻辑的“组合关系” ——

组合式函数可以嵌套调用另一个组合式函数


六、异步场景:useFetch 示例

除了事件逻辑,我们常常需要封装“异步请求逻辑”,比如:

// fetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  fetch(url)
    .then((res) => res.json())
    .then((json) => (data.value = json))
    .catch((err) => (error.value = err))

  return { data, error }
}

使用方式:

<script setup>
import { useFetch } from './fetch.js'

const { data, error } = useFetch('https://api.example.com/posts')
</script>

<template>
  <div v-if="error">❌ 出错:{{ error.message }}</div>
  <div v-else-if="data">✅ 数据:{{ data }}</div>
  <div v-else>⏳ 加载中...</div>
</template>

七、响应式参数:动态请求的 useFetch

上面 useFetch 只会执行一次,

但如果我们希望在 URL 改变时自动重新请求呢?

就可以用 watchEffect() + toValue():

// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  const fetchData = () => {
    data.value = null
    error.value = null

    fetch(toValue(url)) // 兼容 ref / getter / 普通字符串
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  watchEffect(() => {
    fetchData() // url 改变时会自动重新执行
  })

  return { data, error }
}

使用示例:

<script setup>
import { ref } from 'vue'
import { useFetch } from './fetch.js'

const postId = ref(1)
const { data, error } = useFetch(() => `/api/posts/${postId.value}`)

// 模拟切换文章
function nextPost() {
  postId.value++
}
</script>

<template>
  <button @click="nextPost">下一篇</button>
  <div v-if="error">❌ 出错:{{ error.message }}</div>
  <div v-else-if="data">📰 文章:{{ data.title }}</div>
  <div v-else>⏳ 加载中...</div>
</template>

✅ 这就让你的 useFetch 成为了真正“响应式的请求函数”。


八、组合式函数的使用规范

项目 推荐做法 原因
🧩 命名 useXxx() 一目了然,符合惯例
📦 返回值 返回多个 ref,不要直接返回 reactive 对象 防止解构时丢失响应性
🔁 生命周期 必须在 Vue 需要绑定当前组件实例
⚙️ 参数 建议使用 toValue() 规范化输入 兼容 ref、getter、普通值
🧹 清理 要在 onUnmounted() 清理副作用 避免内存泄漏

九、与其他模式的比较

模式 优点 缺点
Mixins 逻辑复用简单 来源不清晰、命名冲突、隐式依赖
无渲染组件 (Renderless) 可复用逻辑 + UI 会额外创建组件实例,性能差
组合式函数 (Composables) 无实例开销、逻辑清晰、依赖显式 不直接提供模板复用

✅ 结论:

纯逻辑复用 → 用组合式函数

逻辑 + UI 复用 → 用无渲染组件


十、总结

概念 说明
组合式函数 利用 Vue 组合式 API 封装可复用逻辑的函数
核心特性 可使用 ref / reactive / 生命周期钩子 / watch
优势 灵活组合、逻辑清晰、性能优秀、类型友好
常见应用 请求封装、事件监听、滚动追踪、权限控制、表单管理等
开发建议 命名统一、输入规范化、注意生命周期上下文

✨ 最后

Composables 就像是 Vue 世界里的「逻辑积木」——

你可以自由拼接、拆解、组合它们,构建出任何复杂的交互逻辑。

如果你曾觉得逻辑在组件里越堆越乱,

那是时候开始用 组合式函数 让代码“呼吸”了。

Vue 3 中 Provide / Inject 在异步时不起作用原因分析(二)?

作者 90后晨仔
2025年10月13日 21:33

本文是继续上一篇文章《Vue 3 中 Provide / Inject 在异步时不起作用原因分析(一)》

在线查看示例(需要科学上网)

示例源码下载地址:分析demo

🧩 一、核心原理(简单讲人话)

在 Vue3 中:

  • provide 是父组件提供一个依赖值

  • inject 是子组件接收这个依赖值

  • 默认情况下,provide 提供的是一个「普通的引用值」,而不是响应式的。

👉 这意味着:

如果你在父组件中 later(异步)修改了 provide 的值,而这个值不是响应式对象,那么子组件不会自动更新。


🧠 二、最简单示例:静态 provide(不响应)

<!-- App.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <button @click="changeName">修改名字</button>
    <Child />
  </div>
</template>

<script setup>
import { provide } from 'vue'
import Child from './Child.vue'

let username = '小明'

// 向子组件提供 username
provide('username', username)

function changeName() {
  username = '小红'
  console.log('父组件修改了 username =', username)
}
</script>

<!-- Child.vue -->
<template>
  <div>
    <h3>子组件</h3>
    <p>用户名:{{ username }}</p>
  </div>
</template>

<script setup>
import { inject } from 'vue'
const username = inject('username')
</script>

🧩 运行结果:

  • 初始显示:用户名:小明

  • 点击“修改名字”按钮后,子组件界面不会更新

📖 原因:

因为 provide('username', username) 提供的是普通字符串,不具备响应式特性。


✅ 三、扩展版:让 provide 变成响应式的(推荐写法)

要让子组件能「自动响应父组件异步变化」,只需要用 ref 或 reactive 包装即可。

<!-- App.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <button @click="changeName">异步修改名字(2秒后)</button>
    <Child />
  </div>
</template>

<script setup>
import { ref, provide } from 'vue'
import Child from './Child.vue'

const username = ref('小明')

// ✅ 提供响应式的值
provide('username', username)

function changeName() {
  setTimeout(() => {
    username.value = '小红'
    console.log('父组件异步修改 username = 小红')
  }, 2000)
}
</script>

<!-- Child.vue -->
<template>
  <div>
    <h3>子组件</h3>
    <p>用户名:{{ username }}</p>
  </div>
</template>

<script setup>
import { inject } from 'vue'
const username = inject('username') // 自动响应
</script>

🧩 运行结果:

  • 初始显示:用户名:小明

  • 点击按钮后 2 秒 → 自动更新为:用户名:小红

✅ 因为我们注入的是 ref,Vue3 会自动处理 .value 的响应式绑定。


❌ 四、错误示例:异步 provide 失效的情况(常见坑)

有时新手会这么写:

<!-- App.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <button @click="loadData">异步加载 provide 值</button>
    <Child />
  </div>
</template>

<script setup>
import { provide, ref } from 'vue'
import Child from './Child.vue'

let user = null

function loadData() {
  setTimeout(() => {
    user = { name: '异步用户' }
    provide('user', user) // ❌ 错误!在 setup 外部、异步中调用 provide 无效
    console.log('异步 provide 完成')
  }, 2000)
}

provide('user', user)
</script>

<!-- Child.vue -->
<template>
  <div>
    <p>子组件:{{ user }}</p>
  </div>
</template>

<script setup>
import { inject } from 'vue'
const user = inject('user')
</script>

🧩 现象:

  • 初始显示:子组件:null

  • 点击“异步加载”后,依然不变!

📖 原因:

provide 只能在组件 setup() 执行时建立依赖关系,

异步调用 provide() 没有效果,Vue 根本不会重新建立依赖注入。


🔍 五、正确的异步写法总结

场景 错误示例 正确写法
父组件 setup 后再异步修改 普通变量 ✅ 使用 ref 或 reactive
异步中重新调用 provide() ❌ 无效 ✅ 一次 provide 响应式引用即可
想实时共享对象状态 ❌ 普通对象 ✅ 用 reactive() 或 Pinia

🧱 六、总结

类型 响应式 子组件会更新? 推荐
provide('a', 普通变量) ❌ 否 ❌ 否
provide('a', ref()) ✅ 是 ✅ 是
provide('a', reactive()) ✅ 是 ✅ 是
异步重新调用 provide() ❌ 无效 ❌ 否

Vue 3 中 Provide / Inject 在异步时不起作用原因分析(一)?

作者 90后晨仔
2025年10月13日 21:29

一、先搞清楚:Provide / Inject 是什么机制

provide 和 inject 是 Vue 组件之间 祖孙通信的一种机制

它允许上层组件提供数据,而下层组件直接获取,不需要层层 props 传递。

简单关系图:

App.vue (provide)
   └── ChildA.vue
         └── ChildB.vue (inject)

App 通过 provide 提供,ChildB 直接拿到。

在 Vue 3 中:

// 父组件
import { provide } from 'vue'

setup() {
  provide('theme', 'dark')
}
// 孙组件
import { inject } from 'vue'

setup() {
  const theme = inject('theme')
  console.log(theme) // 'dark'
}

这本质上是 Vue 在「组件初始化时」建立的一种依赖注入映射关系(依赖树)


二、误区:为什么“异步”时会失效?

很多人说“在异步组件里 inject 不到值”,其实问题出在「加载时机」上。

❌ 错误理解:

以为 inject 是“运行时全局取值”,随时都能拿到。

✅ 实际原理:

inject() 的查找是在 组件创建阶段(setup 执行时) 完成的。

也就是说:

只有当父组件已经被挂载并执行了 provide() 后,子组件在 setup 时才能拿到。

如果异步加载的子组件在 provide 之前被初始化,或者在懒加载时「上下文丢失」,那它当然拿不到值。


三、可复现测试案例(你可以直接复制运行)

我们写一个最常见的「异步子组件注入」示例。

你可以用 Vite 新建项目,然后建这三个文件:


🟢App.vue(父组件)

<template>
  <div>
    <h2>父组件</h2>
    <p>当前主题:{{ theme }}</p>
    <button @click="loadAsync">加载异步子组件</button>

    <!-- 当点击后才加载 -->
    <component :is="childComp" />
  </div>
</template>

<script setup>
import { ref, provide, defineAsyncComponent } from 'vue'

// 1️⃣ 提供一个响应式值
const theme = ref('🌙 暗黑模式')
provide('theme', theme)

// 2️⃣ 模拟异步组件加载
const childComp = ref(null)
function loadAsync() {
  // 模拟异步加载组件(1 秒后返回)
  const AsyncChild = defineAsyncComponent(() =>
    new Promise(resolve => {
      setTimeout(() => resolve(import('./Child.vue')), 1000)
    })
  )
  childComp.value = AsyncChild
}
</script>

🟡Child.vue(中间组件)

<template>
  <div class="child">
    <h3>中间组件</h3>
    <GrandChild />
  </div>
</template>

<script setup>
import GrandChild from './GrandChild.vue'
</script>

<style scoped>
.child {
  border: 1px solid #aaa;
  margin: 8px;
  padding: 8px;
}
</style>

🔵GrandChild.vue(孙组件)

<template>
  <div class="grand">
    <h4>孙组件</h4>
    <p>从 provide 注入的主题:{{ theme }}</p>
  </div>
</template>

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

// 1️⃣ 注入父级 provide 的数据
const theme = inject('theme', '默认主题')

// 2️⃣ 打印验证
console.log('孙组件注入的 theme 值是:', theme)
</script>

<style scoped>
.grand {
  border: 1px dashed #666;
  margin-top: 8px;
  padding: 6px;
}
</style>

✅ 运行结果验证:

1️⃣ 页面初始只显示父组件。

2️⃣ 点击「加载异步子组件」。

3️⃣ 一秒后加载完成,控制台输出:

孙组件注入的 theme 值是:RefImpl {value: '🌙 暗黑模式'}

页面上显示:

从 provide 注入的主题:🌙 暗黑模式

👉 说明:即使是 异步组件,也能正确拿到 provide 的值。


四、那为什么有时真的“不起作用”?

有三种常见原因:

原因 说明 解决方案
1️⃣ 在 setup 外使用 inject() Vue 只能在组件初始化(setup 阶段)内建立依赖 一定要在 setup() 中调用
2️⃣ 异步组件创建时父组件上下文丢失 如果异步加载组件时没有挂在已有的上下文中(比如 createApp 动态 mount) 保证异步组件是作为「现有组件树」的子节点被渲染
3️⃣ SSR 场景中 hydration 时机问题 如果在服务器端渲染中,provide 未在客户端同步恢复 SSR 需保证 provide/inject 在同一上下文实例中执行

五、底层原理小科普(可选理解)

Vue 内部维护了一棵「依赖注入树」,

每个组件实例在初始化时会记录自己的 provides 对象:

instance.provides = Object.create(parent.provides)

所以当 inject('theme') 时,它会:

  1. 向上查找父组件的 provides;

  2. 找到对应 key;

  3. 返回对应的值(引用)。

这就是为什么:

  • 父子必须在「同一组件树上下文」中;
  • 异步不会破坏注入关系(除非脱离这棵树)。

✅ 总结重点

概念 说明
Provide / Inject 用于祖孙通信的依赖注入机制
异步组件能否注入? ✅ 能,只要仍在同一组件树中
什么时候会失效? 父未先 provide、或异步 mount 独立实例
验证方法 使用 defineAsyncComponent 懒加载组件
推荐做法 始终在 setup 内使用 provide/inject

Vue 异步组件(defineAsyncComponent)全指南:写给新手的小白实战笔记

作者 90后晨仔
2025年10月13日 21:18

在现代前端应用中,性能优化几乎是每个开发者都要面对的课题。

尤其是使用 Vue 构建大型单页应用(SPA)时,首屏加载慢、包体积大 成了常见的痛点。

这时,“异步组件”就登场了。

它能让你把页面拆成小块按需加载,只在用户真正需要时才下载对应的模块,显著减少首屏压力。

这篇文章是写给 刚入门 Vue 3 的开发者 的异步组件实战指南,

我会用简单的语言、可运行的代码和图景化的思维带你彻底搞懂——

defineAsyncComponent 到底做了什么、怎么用、有哪些坑。


一、为什么需要异步组件

🚀 核心动机:提升首屏速度,减少无用资源加载。

想象一个后台系统,首屏只展示“仪表盘”,但你的 bundle 里却打包了“用户管理”、“统计分析”、“设置中心”……

即使用户一天都没点进去,这些模块也会白白加载。

异步组件正是用来解决这种浪费的:

  • 不会被打进主包
  • 只有在组件首次渲染时,才会异步加载真实实现;
  • 这就是所谓的 按需加载 (lazy load)代码分割 (code-splitting)

二、最简单的异步加载:

defineAsyncComponent+import()

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

使用方式完全与普通组件一致:

<template>
  <AsyncComp some-prop="Hello Vue!" />
</template>

解释一下背后的机制:

  • import() 会返回一个 Promise;

  • 打包工具(Vite / Webpack)会自动把它拆成独立的 chunk 文件

  • defineAsyncComponent() 会创建一个“外壳组件”,在内部完成加载逻辑;

  • 一旦加载完成,它会自动渲染内部真正的 MyComponent.vue;

  • 所有 props、插槽、事件 都会被自动透传。

简单来说,它是 Vue 帮你封装好的“懒加载包装器”。


三、加载中 & 加载失败状态:更友好的配置写法

网络总是有延迟或失败的时候,Vue 官方提供了更完善的配置:

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Foo.vue'),
  loadingComponent: LoadingComponent, // 加载中占位
  delay: 200,                          // 多少 ms 后显示 loading
  errorComponent: ErrorComponent,      // 失败时的提示
  timeout: 3000                        // 超时视为失败
})

🧠 要点:

  • delay:默认 200ms,如果加载太快就不显示 loading,防止闪烁;
  • timeout:超过指定时间自动触发错误;
  • loadingComponent / errorComponent 都是普通组件,可以是骨架屏或重试按钮;
  • Vue 会自动处理 Promise 的状态变化。

四、SSR 场景下的新玩法:Hydration 策略(Vue 3.5+)

在服务器端渲染(SSR)场景下,HTML 首屏已经输出,但 JS 模块还没激活。

Vue 3.5 开始支持为异步组件设置「延迟激活策略」:

import { defineAsyncComponent, hydrateOnVisible } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Comp.vue'),
  hydrate: hydrateOnVisible({ rootMargin: '100px' })
})

这意味着:

  • 组件只在滚动到可视区时才激活;

  • SSR 首屏照常渲染,但 hydration(激活)被延后;

  • 从而减少初始脚本执行量,提高 TTI(可交互时间)。

其他常见策略:

策略函数 行为
hydrateOnIdle() 浏览器空闲时激活
hydrateOnVisible() 元素进入视口时激活
hydrateOnMediaQuery() 媒体查询匹配时激活
hydrateOnInteraction('click') 用户交互后激活

你甚至可以自定义策略,在合适时机调用 hydrate() 完成手动激活。


五、搭配

使用,构建优雅的异步界面

是 Vue 专门为异步组件设计的辅助标签,它可以集中控制加载状态与回退界面。

<Suspense>
  <template #default>
    <AsyncComp />
  </template>
  <template #fallback>
    <div>正在努力加载中...</div>
  </template>
</Suspense>

的工作原理:

  • 会等待内部所有异步依赖(包括 defineAsyncComponent)加载完成;
  • 如果有 delay 或网络延迟,会自动显示 fallback 内容;
  • 当所有异步都 resolve 后,才一次性切换到真实内容;
  • 适合并行加载多个异步子组件时使用。

六、实战建议与最佳实践

1. 优先按路由懒加载:

const routes = [
  { path: '/admin', component: () => import('./views/Admin.vue') }
]

这能最大化地减少首包体积。

2. 小组件不建议懒加载:

懒加载有 HTTP 开销,过度拆包反而拖慢渲染。

3. 善用 loadingComponent 做骨架屏:

用灰色框或占位元素代替 spinner,更自然。

4. 设置合理 delay / timeout:

避免闪烁,也要能及时处理网络异常。

5. 支持重试:

function retryImport(path, retries = 3, interval = 500) {
  return new Promise((resolve, reject) => {
    const attempt = () => {
      import(path).then(resolve).catch(err => {
        if (retries-- <= 0) reject(err)
        else setTimeout(attempt, interval)
      })
    }
    attempt()
  })
}

const AsyncComp = defineAsyncComponent(() => retryImport('./Foo.vue', 2))

6. SSR 优化:

配合 hydrateOnVisible / hydrateOnIdle 让页面更快可交互。


七、常见陷阱 Q&A

Q1:defineAsyncComponent 会影响 props 或 slot 吗?

👉 不会,Vue 内部会自动透传所有 props / slot。

Q2:可以全局注册异步组件吗?

👉 可以:

app.component('MyComp', defineAsyncComponent(() => import('./MyComp.vue')))

Q3:delay=0 会怎样?

👉 loading 组件会立刻显示,建议保留短延迟防闪烁。

Q4:如何在 errorComponent 里实现重试?

👉 通过 emit 通知父组件重新渲染异步组件实例即可。


八、完整实战示例

<script setup>
import { defineAsyncComponent } from 'vue'
import LoadingSkeleton from './LoadingSkeleton.vue'
import ErrorBox from './ErrorBox.vue'

const AsyncWidget = defineAsyncComponent({
  loader: () => import('./HeavyWidget.vue'),
  loadingComponent: LoadingSkeleton,
  errorComponent: ErrorBox,
  delay: 200,
  timeout: 5000
})
</script>

<template>
  <section class="dashboard">
    <h2>📊 仪表盘</h2>
    <AsyncWidget />
  </section>
</template>

📌 ErrorBox 可加上「重试」按钮,点击后 emit 事件让父组件重新创建 AsyncWidget 实例即可。


九、总结回顾

要点 说明
defineAsyncComponent() 创建懒加载包装组件
import() 触发动态分包
loadingComponent / errorComponent 优化加载与失败体验
SSR Hydration 策略 控制何时激活异步组件
统一处理异步加载状态
实战建议 只懒加载页面级或大型组件,合理延迟与重试

尤雨溪强烈推荐的这个库你一定要知道 ⚡️⚡️⚡️

2025年10月13日 19:38

前言

今天带大家看看尤雨溪在推特墙裂推荐的 Nitro v3 这个库!

尤雨溪推特

往期精彩推荐

正文

Nitro 是一个全栈框架,兼容任何运行时。

Nitro v3 可以通过 Vite 插件形式集成,扩展 dev 服务器为生产服务器,支持 vite build 统一输出前后端代码。

快速启动与渐进集成

Nitro 支持 npx create-nitro-app 一键创建项目,或在现有 Vite 项目中安装 nitro 包并添加插件:

import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [nitro()]
});

这样配置后 Vite 的 HMR 无缝延伸至后端路由和 API,可以在单一项目中处理前后端逻辑,无需切换工具链。

路由与生命周期优化

Nitro 的文件系统路由(routes/目录)与 Vite 的模块解析结合,自动映射API端点,支持动态参数、方法特定(如.get.ts)和中间件。

routes/
  api/
    test.ts      <-- /api/test
  hello.get.ts   <-- /hello (GET only)
  hello.post.ts  <-- /hello (POST only)

文件中只需要通过函数暴露对象即可:

import { defineHandler } from "nitro/h3";

export default defineHandler(() => {
  return { hello: "API" };
});

生命周期从路由规则到全局中间件,再到自定义服务器入口和渲染器,确保请求高效处理。

渲染器与SSR支持

Nitro 的渲染器(renderer)捕捉未匹配路由,支持自动 index.html 或自定义模板,使用 Rendu 预处理器注入动态内容。自定义渲染器通过defineRenderHandler生成响应!

插件与扩展性

Nitro 插件(plugins/目录)在启动时运行,支持钩子(如closeerrorrequest)扩展行为。

// nitro.config.ts
export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook("close", async () => {
    // Will run when nitro is being closed
  });
})

支持 hook 时机如下:

"close", () => {}
"error", (error, { event? }) => {}
"render:response", (response, { event }) => {}
"request", (event) => {}
"beforeResponse", (event, { body }) => {}
"afterResponse", (event, { body }) => {}

最后

Nitro v3 与 Vite 搭配,提供高效的全栈方案,从快速启动到路由渲染,再到插件扩展,可以全方位的优化工作流!

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

往期精彩推荐

弃用 uni-app!Vue3 的原生 App 开发框架来了!

2025年10月13日 17:47

长久以来,"用 Vue 3 写真正的原生 App" 一直是块短板。

uni-app 虽然"一套代码多端运行",但性能瓶颈厂商锁仓原生能力羸弱的问题常被开发者诟病。

整个 Vue 生态始终缺少一个能与 React Native 并肩的"真·原生"跨平台方案

直到 NativeScript-Vue 3 的横空出世,并被 尤雨溪 亲自点赞。

为什么是时候说 goodbye 了?

uni-app 现状 开发者痛点
渲染层基于 WebView 或弱原生混合 启动慢、掉帧、长列表卡顿
自定义原生 SDK 需写大量 renderjs / plus 桥接 维护成本高,升级易断裂
锁定 DCloud 生态 工程化、VitePinia 等新工具跟进慢
Vue 3 支持姗姗来迟,Composition API 兼容碎裂 类型推断、生态插件处处踩坑

"我们只是想要一个 Vue 语法 + 真原生渲染 + 社区插件开箱即用 的解决方案。"
—— 这,正是 NativeScript-Vue 给出的答案。

尤雨溪推特背书

2025-10-08Evan You 转发 NativeScript 官方推文:

"Try Vite + NativeScript-Vue today — HMR, native APIs, live reload."

配图是一段 <script setup> + TypeScript 的实战 Demo,意味着:

  • 真正的 Vue 3 语法Composition API
  • Vite 秒级热重载
  • 直接调用 iOS / Android 原生 API

获创始人的公开推荐,无疑给社区打了一剂强心针。

NativeScript-Vue 是什么?

一句话:Vue 的自定义渲染器 + NativeScript 原生引擎

  • 运行时 没有 WebView,JS 在 V8 / JavaScriptCore 中执行
  • <template> 标签 → 原生 UILabel / android.widget.TextView
  • 支持 NPM、CocoaPods、Maven/Gradle 全部原生依赖
  • React Native 同级别的性能,却拥有 Vue 完整开发体验

5 分钟极速上手

1. 环境配置(一次过)

# Node ≥ 18
npm i -g nativescript
ns doctor                # 按提示安装 JDK / Android Studio / Xcode
# 全部绿灯即可

2. 创建项目

ns create myApp \
  --template @nativescript-vue/template-blank-vue3@latest
cd myApp

模板已集成 Vite + Vue3 + TS + ESLint

3. 运行 & 调试

# 真机 / 模拟器随你选
ns run ios
ns run android

保存文件 → 毫秒级 HMRconsole.log 直接输出到终端。

4. 目录速览

myApp/
├─ app/
│  ├─ components/          // 单文件 .vue
│  ├─ app.ts               // createApp()
│  └─ stores/              // Pinia 状态库
├─ App_Resources/
└─ vite.config.ts          // 已配置 nativescript-vue-vite-plugin

5. 打包上线

ns build android --release   # 生成 .aab / .apk
ns build ios --release       # 生成 .ipa

签名渠道自动版本号——标准原生流程,CI 友好。

Vue 3 生态插件兼容性一览

插件 是否可用 说明
Pinia 零改动,app.use(createPinia())
VueUse ⚠️ 无 DOM 的 Utilities 可用
vue-i18n 9.x 实测正常
Vue Router 官方推荐用 NativeScript 帧导航$navigateTo(Page)
Vuetify / Element Plus 依赖 CSS & DOM,无法渲染

检测小技巧:

npm i xxx
grep -r "document\|window\|HTMLElement" node_modules/xxx || echo "大概率安全"

调试神器:Vue DevTools 支持

NativeScript-Vue 3 已提供 官方 DevTools 插件

  • 组件树PropsEventsPinia 状态 实时查看
  • 沿用桌面端调试习惯,无需额外学习成本

👉 配置指南:https://nativescript-vue.org/docs/essentials/vue-devtools

插件生态 & 原生能力

  • 700+ NativeScript 官方插件
    ns plugin add @nativescript/camera | bluetooth | sqlite...

  • iOS/Android SDK 直接引入
    CocoaPods / Maven 一行配置即可:

 // 调用原生 CoreBluetooth
 import { CBCentralManager } from '@nativescript/core'
  • 自定义 View & 动画
    注册即可在 <template> 使用,与 React Native 造组件体验一致

结语:这一次,Vue 开发者不再低人一等

React NativeFacebook 撑腰,FlutterGoogle 背书,

现在 Vue 3 也有了自己的 真·原生跨平台答案 —— NativeScript-Vue

它让 Vue 语法第一次 完整、无损、高性能 地跑在 iOS & Android 上,
并获得 尤雨溪 公开点赞与 Vite 官方生态加持。

弃用 uni-app,拥抱 NativeScript-Vue
性能、原生能力、工程化 三者兼得,
用你最爱的 .vue 文件,写最硬核的移动应用!

🔖 一键直达资源

  • 官网 & 文档https://nativescript-vue.org
  • 插件兼容列表https://nativescript-vue.org/docs/essentials/vue-plugins
  • DevTools 配置https://nativescript-vue.org/docs/essentials/vue-devtools
  • 环境安装指南https://docs.nativescript.org/setup/
❌
❌