2025-02-17【Vue/Vite】语法细节

1 各种各样的数据绑定

1-1. 单向数据绑定

  • 文本插值
    • 使用双大括号 {{ }} 语法,将数据插入到HTML标签之间。它会把数据解析为纯文本并插入到DOM中。例如:
<div>
    {{ message }}
</div>
<script>
export default {
    data() {
        return {
            message: 'Hello, Vue!'
        };
    }
};
</script>
  • 也支持在JavaScript表达式中使用,例如:
<div>
    {{ number + 1 }}
</div>
<script>
export default {
    data() {
        return {
            number: 5
        };
    }
};
</script>
  • v - text指令
    • 功能与双大括号文本插值类似,也是将数据作为纯文本插入到元素中。但它可以避免在插值表达式尚未解析时,页面上出现闪烁的Mustache语法({{ }})。例如:
<div v-text="message"></div>
<script>
export default {
    data() {
        return {
            message: 'This is v - text binding'
        };
    }
};
</script>
  • v - html指令
    • 用于将数据作为HTML代码插入到元素中。这在需要显示富文本内容(如从服务器获取的HTML片段)时很有用,但需要注意安全问题,防止XSS攻击。例如:
<div v-html="htmlContent"></div>
<script>
export default {
    data() {
        return {
            htmlContent: '<p>这是一段 <strong>HTML</strong> 内容</p>'
        };
    }
};
</script>

1-2. 属性绑定

  • v - bind指令: 可简写为:属性名
    • 用于动态绑定HTML元素的属性。可以缩写为 :。例如,动态绑定图片的src属性:
<img :src="imageUrl" alt="示例图片">
<script>
export default {
    data() {
        return {
            imageUrl: 'https://example.com/image.jpg'
        };
    }
};
</script>
  • 绑定class属性时,可以通过对象语法动态切换类名。例如:
<div :class="{ active: isActive, 'text - red': hasError }"></div>
<script>
export default {
    data() {
        return {
            isActive: true,
            hasError: false
        };
    }
};
</script>
  • 也支持数组语法,例如:
<div :class="[activeClass, errorClass? 'text - red' : '']"></div>
<script>
export default {
    data() {
        return {
            activeClass: 'active',
            errorClass: false
        };
    }
};
</script>
  • 对于style属性,同样支持对象语法和数组语法来动态设置样式。例如对象语法:
<div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div>
<script>
export default {
    data() {
        return {
            textColor: 'blue',
            fontSize: 16
        };
    }
};
</script>

1-3. 事件绑定

  • v - on指令
    • 用于绑定DOM事件监听器,可以缩写为@。例如,绑定一个按钮的点击事件:
<button @click="handleClick">点击我</button>
<script>
export default {
    methods: {
        handleClick() {
            console.log('按钮被点击了');
        }
    }
};
</script>
  • 可以传递参数,例如:
<button @click="handleClick('参数值')">点击我</button>
<script>
export default {
    methods: {
        handleClick(param) {
            console.log('接收到的参数:', param);
        }
    }
};
</script>
  • 还支持事件修饰符,如.prevent(阻止默认事件,例如提交表单时阻止页面刷新)、.stop(阻止事件冒泡)等。例如:
<a href="#" @click.prevent="handleClick">链接,点击不跳转</a>

1-4. 双向数据绑定(除v - model的特殊场景)

  • 在自定义组件中,可以通过props接收父组件数据,通过$emit触发自定义事件来实现双向数据绑定效果。例如,父组件:
<template>
    <div>
        <child - component :value="parentValue" @input="updateParentValue"></child - component>
    </div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
    components: {
        ChildComponent
    },
    data() {
        return {
            parentValue: '初始值'
        };
    },
    methods: {
        updateParentValue(newValue) {
            this.parentValue = newValue;
        }
    }
};
</script>
  • 子组件ChildComponent.vue
<template>
    <input :value="value" @input="$emit('input', $event.target.value)">
</template>
<script>
export default {
    props: ['value']
};
</script>

这种方式模拟了类似v - model在自定义组件中的双向绑定行为。

1-5. v-model双向数据绑定

v-model 是Vue.js中一个非常重要的指令,用于在表单元素(如 inputselecttextarea 等)上创建双向数据绑定。它使得视图与数据之间的同步变得更加便捷,开发者无需手动处理表单元素值的变化和数据更新。

  • input 元素 - 文本输入框
    • 对于文本输入框,v-model 绑定到一个数据属性,输入框的值会实时同步到该属性,同时该属性的变化也会实时反映在输入框中。例如:
<template>
  <div>
    <input type="text" v-model="message">
    <p>你输入的是: {{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    };
  }
};
</script>
  • input 元素 - 复选框(checkbox)
    • 单个复选框v-model 绑定到一个布尔值,用于表示复选框是否被选中。例如:
<template>
  <div>
    <input type="checkbox" v-model="isChecked">
    <p>复选框状态: {{ isChecked }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isChecked: false
    };
  }
};
</script>
  • 多个复选框:当有多个复选框且绑定到同一个数组时,每个复选框通过 value 属性设置其对应的值,v - model 绑定到一个数组。例如:
<template>
  <div>
    <input type="checkbox" value="apple" v-model="selectedFruits"> 苹果
    <input type="checkbox" value="banana" v-model="selectedFruits"> 香蕉
    <input type="checkbox" value="cherry" v-model="selectedFruits"> 樱桃
    <p>你选择的水果: {{ selectedFruits }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedFruits: []
    };
  }
};
</script>
  • input 元素 - 单选框(radio)
    • 多个单选框通过 v - model 绑定到同一个数据属性,每个单选框通过 value 属性设置其对应的值。例如:
<template>
  <div>
    <input type="radio" value="male" v-model="gender"> 男
    <input type="radio" value="female" v-model="gender"> 女
    <p>你的性别: {{ gender }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      gender: 'male'
    };
  }
};
</script>
  • select 元素 - 选择框
    • 单选选择框v - model 绑定到一个数据属性,<option> 标签的 value 属性值与之匹配。例如:
<template>
  <div>
    <select v-model="selectedOption">
      <option value="option1">选项1</option>
      <option value="option2">选项2</option>
      <option value="option3">选项3</option>
    </select>
    <p>你选择的选项: {{ selectedOption }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedOption: 'option1'
    };
  }
};
</script>
  • 多选选择框(设置 multiple 属性)v - model 绑定到一个数组。例如:
<template>
  <div>
    <select multiple v-model="selectedOptions">
      <option value="option1">选项1</option>
      <option value="option2">选项2</option>
      <option value="option3">选项3</option>
    </select>
    <p>你选择的选项: {{ selectedOptions }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedOptions: []
    };
  }
};
</script>
  • textarea 元素
    • v - model 绑定到一个数据属性,用于同步文本区域的内容。例如:
<template>
  <div>
    <textarea v-model="textContent"></textarea>
    <p>你输入的内容: {{ textContent }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      textContent: ''
    };
  }
};
</script>

2. <script setup>语法

  • 传统Vue单文件组件(SFC)中,编写组合式API
<script>
import { ref, computed } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const doubleCount = computed(() => return count.value * 2);

    function increment() {
      count.value++;
    }

    return {
      count,
      doubleCount,
      increment
    };
  }
}
</script>

<template>
  <div>
    <p>{{ count }}</p>
    <p>{{ doubleCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>
  • 使用<script setup>编写
<script setup>
import { ref, computed } from 'vue';

const count = ref(0);
const doubleCount = computed(() =>  return count.value * 2);

function increment() {
  count.value++;
}
</script>

<template>
  <div>
    <p>{{ count }}</p>
    <p>{{ doubleCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

语法更简洁,自动注册插件,自动返回调用对象

3. 带双向绑定的计算属性

import { ref, computed } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`; // 必须有 return 语句
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ');
  }
});

console.log(fullName.value); // 输出: "John Doe"
// 设置 fullName 的值,这会触发 set 方法
fullName.value = 'Jane Smith';
console.log(firstName.value); // 输出: "Jane"
console.log(lastName.value);  // 输出: "Smith"

get()监听firstNamelastName的变化
set()监听fullName的变化

4. 根据ref和监听事件变更HTMLCSS

  • 变更html
<template>
  <div>
    <p ref="textElement">初始文本</p>
    <button @click="updateText">更新文本</button>
  </div>
</template>

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

const textElement = ref(null);

function updateText() {
  if (textElement.value) {
    textElement.value.textContent = '更新后的文本';
  }
}
</script>
  • 变更CSS
<template>
  <div>
    <p ref="styledElement" :style="elementStyle">这是一个段落</p>
    <button @click="toggleStyle">切换样式</button>
  </div>
</template>

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

const styledElement = ref(null);
const elementStyle = reactive({
  color: 'black',
  fontSize: '16px'
});

function toggleStyle() {
  if (styledElement.value) {
    elementStyle.color = elementStyle.color === 'black' ? 'red' : 'black';
    elementStyle.fontSize = elementStyle.fontSize === '16px' ? '24px' : '16px';
  }
}
</script>
  • 变更类名,进而变更CSS 更直观快捷
<template>
  <div>
    <p :class="dynamicClass">这是一个段落</p>
    <button @click="toggleClass">切换样式</button>
  </div>
</template>

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

const dynamicClass = ref('default-style');

function toggleClass() {
  dynamicClass.value = dynamicClass.value === 'default-style' ? 'highlight-style' : 'default-style';
}
</script>

<style scoped>
.default-style {
  color: black;
  font-size: 16px;
}

.highlight-style {
  color: red;
  font-size: 24px;
}
</style>

scoped限制样式仅在本文件生效

5. 响应式引用的运行规则

const width=ref(),所有与width相关的

  • 模板
<template>
    <div>
        <!-- 模板中使用响应式引用,当 width.value 变化时,这里会自动更新 -->
        <textarea id="markdown" :style="{width: width + 'px'}">show</textarea>
    </div>
</template>
  • 监听器
// 当 width.value 变化时,这个 watch 会触发
import { watch } from 'vue';
watch(width, (newValue, oldValue) => {
    console.log(`Width changed from ${oldValue} to ${newValue}`);
});
  • 计算属性
const widthDouble = computed(() => {
    return width.value * 2;
});

会被触发更新

6. Vite缓存机制

为加速程序启动,Vite会缓存一些依赖库的编译结果,虽然Vite支持热更新,但仅限于对Vue组件main.jsCSS的修改。当对依赖库的js文件进行更改后,更改后的结果不会及时反映到新的Vite项目中,因此需要清除缓存,且强制刷新界面。操作如下

  • 清除Vite缓存目录 确保在项目根目录
rm -rf node_modules/.vite
  • 强制刷新界面
先运行项目
npm run dev

再打开开发者工具


点击Force Reload重新加载,然后就能在2区看到更新后的依赖库代码。否则,即便依赖库的文件被更改,但当断点设置在依赖库内时,依赖的代码仍然保持着上次运行的状态。
务必两个操作都要做,只删除缓存目录会失败的。

7 Vue的组件之间的消息传递

<template>
    <div>
        <A></A>
        <B></B>
    </div>
</template>
  • B组件从某处获取一个消息,传给A
    整体流程
  1. B向父组件发送一个事件消息
  2. 父组件监听事件,获得B传出的消息内容。
  3. A组件内部定义一个props,接受父组件消息。
  4. 最后,父组件通过props将来自B的消息内容传递到A。

具体实现

  1. 在B内定义事件消息和发送函数
const emit = defineEmits(['消息名1', '消息名2']);
设置一个发送条件
const sendMessage = () => {
    emit('消息1', 消息1的内容);
}

当B内定义了一个消息后,B就自动多了一个可监听事件消息名1消息名2

  1. 父组件监听事件
<template>
    <div>
        <A></A>
        <B @消息名1=“触发函数1” @消息名2=“触发函数2”></B>
    </div>
</template>

还需要在父组件内设置对应的响应式引用变量,用以接受消息内容

<script setup>
import { ref } from "vue"
const refValue1 = ref(0);  //存储数字
const refValue2 = ref("");  //存储字符串
还可以是ref({})  //空对象,可以存储更复杂的内容

const 触发函数1 = (任意变量名A用来接受消息内容) => {
    refValue1.value = A;
}
<script>
  1. 在A内定义接受父组件消息的props量 ,A中相应多出了可用于绑定的props属性
    如果不需要复杂操作,直接绑定(会同步响应新传入的值),需要额外操作就监听props变化,写触发函数
const props = defineProps({
    prop1: {
    type:Number,
    default: 40,
  },
  prop2: {
    type:String,
    default: "hello",
}
});
直接绑定
<div  :style="{width: prop1 + 'px'}"></div>
需要复杂操作  不能简写为prop2
watch( () => props.prop2, (newVal, oldVal) => {

});
  1. 父组件绑定属性到响应式引用
<A :props1="refValue" :props2="refValue2"></A>

总结
B将要传出的消息定位为事件消息,父组件监听B的事件消息,并获得消息内容。
消息内容被赋值到响应式引用,响应式引用被刷新。
响应式引用又被绑定到A的props属性上,从而传入A组件

8 深度理解Vue3的双向绑定v-model

v-model是一种语法糖,也就是它是一套语法方案,不是一个独立函数或者变量,需要遵循既定的规则,在代码的多个部位写下这套方案。

v-model常用于表单绑定,直接能绑定到表单的value,可直接使用v-model绑定value的标签:

<template>
  <div>
    <!-- 文本输入 -->
    <input v-model="text" type="text" placeholder="请输入文本">

    <!-- 复选框 -->
    <input v-model="checked" type="checkbox"> 是否同意

    <!-- 单选按钮 -->
    <input v-model="picked" type="radio" value="option1"> 选项1
    <input v-model="picked" type="radio" value="option2"> 选项2

    <!-- 下拉选择框 -->
    <select v-model="selected">
      <option value="option1">选项1</option>
      <option value="option2">选项2</option>
    </select>

    <!-- 多行文本输入 -->
    <textarea v-model="message" placeholder="请输入多行文本"></textarea>

    <!-- 自定义组件 -->
    <custom-input v-model="customValue"></custom-input>
  </div>
</template>

v-model是属性绑定:modelValue和监听事件@update:modelValue的组合。
v-model一般用于两个DOM元素或两个组件之间的数据同步,当然也可以自定义属性、监听事件和响应式应用实现双向绑定,但v-model的益处就在于用更少的代码实现同样的功能
下面详细拆解这套语法方案

  • 单组件内,两个DOM元素之间的数据双向绑定
    完整代码:
<template>
  <div class="app">
    <input type="text" class="input" v-model="inputText">
    <textarea class="textarea">{{ inputText }}</textarea>
  </div>
</template>

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

const inputText = ref('');
</script>

注意,数据绑定时,格式是:绑定属性=“绑定变量”,可以把:换成v-bind或其他写法。绑定变量务必在JS处定义绑定变量,否则会成为undefined量,会有⚠:


且必须定义为响应式引用,否则无法实现数据同步

这样一个单组件内,两个DOM元素之间的数据单向绑定就实现了。在input中输入的文字会同步更新到textareatextarea的文字更改不会同步到input
注意

  1. {{xxx}}是绑定一个纯文本变量进行插值,和v-model没有关系,只能接收inputText变量,将
<textarea class="textarea">{{ inputText }}</textarea>

改为

<textarea class="textarea" v-model="inputText"></textarea>

才能实现双向绑定textarea的文字更改才会同步到input

  • 父组件与子组件之间的数据双向绑定
    在子组件与父组件的双向绑定中,直观思维是子组件内部使用v-model绑定一个变量,然后父组件在子组件的标签里使用v-model绑定获取子组件的绑定变量。只针对一个v-model讨论
    例如,此处省略响应式引用的创建
//子组件
<div class="child">
        <input type="text" v-model="msg">
</div>
//父组件
<child v-model="inputText"></child>

以上代码是错误的
因为不同于同组件内两个DOM元素之间的双向绑定,父子组件的数据传输不再遵循v-model包办一切数据传递相关的事件发送与监听,本质上还是开发手动使用父子之间的数据流动规则手动实现了双向绑定,但其中却有包含一些v-model的规则,非常不直观,建议成为高手后再使用。

具体实现为,子组件代码:

<template>
    <div class="child">
        <input type="text" v-model="msg" @input="sendMsg">
        <div class="msg" contenteditable="true">{{modelValue}}</div>
    </div>
</template>

<script setup>
    import { ref, watch } from 'vue'
    const msg = ref('');

    const emit = defineEmits(['update:modelValue'])
   // 定义props来接收v-model绑定的值
    const props = defineProps({
          modelValue: {
          type: String,
          default: ''
        }
      });
    const sendMsg = () => {
        emit('update:modelValue', msg.value)
    }
    watch( () => props.modelValue, (newVal, oldVal) => {
        msg.value = newVal;
    } );
</script>

在子组件中,使用v-model="msg"绑定input的输入内容到msg后,还需要监听输入事件@input=“sendMsg”msg从子组件以事件消息的形式发送出去
因此需要指定emit,而且要按规则指定事件名称
针对单v-model,子组件发送事件名必须为update:modelValue

同时,为了实现双向的数据传递,子组件还要指定一个props用以接收父组件处对绑定变量的更改,且名称必须为modelValue

这也符合前文提到的,v-model的本质是属性绑定:modelValue和监听事件@update:modelValue的组合。

另外父组件代码为:

<template>
  <div class="app">
    <child v-model="inputText"></child>
    <textarea class="textarea" v-model="inputText"></textarea>
  </div> 
</template>

<script setup>
    import child from './components/child.vue';
   import { ref } from 'vue';
   const inputText = ref('');
<script>

父组件使用v-model绑定了子组件外传的update:modelValue对应的值,还将其绑定到了一个文本框。
除了定义对应的响应式引用,父组件没有事件监听代码@update:modelValue和属性绑定:modelValue,因为只要按照命名规则写子组件,父组件就能自动进行监听、绑定和回传。

同样元素textarea内的更改也会反应到子组件上,这便是父子组件之间的数据双向绑定。

父子组件之间多v-model的交互太不直观了,还是不研究了

9 如何使用文件路径引入静态资源

以图片举例,静态资源只能放在两个文件夹下publicassets
1.静态资源在public
使用相对路径:

<template>
  <ChildComponent imageSrc="/images/example.jpg" />
</template>
  1. 静态资源存放在assets
    script中引入
import image from '@/assets/images/example.jpg'; // 假设图片放在src/assets/images目录下

在组件或元素中使用

<img :src="image" alt="Example Image">

总结
@/:这是Vue CLI项目中的别名,通常指向src目录。
/:如果图片放在public目录下,可以直接使用根路径/来引用图片。
注意:务必使用./images/example.jpg,而不是/images/example.jpg,否则在开发阶段图片能正常显示,但生产阶段就出错。
相对路径:如果图片放在src/assets目录下,可以使用相对路径@/assets/images/example.jpg来引入。

10 ref变量的模板自动解包

如果在script中创建了响应式变量ref,而且在模板中使用该变量时,不要使用.value解包
因为模板自动解包ref变量

<template>
  <ul class="indicator">
    <!-- 遍历 images 数组 -->
    <li class="indicator-item"
        v-for="(image, index) in images"
        :key="index"
        <!-- 根据 index 是否等于 currentIndex 来设置背景颜色 -->
        :style="{
          backgroundColor: index === currentIndex ? 'white' : 'gray',
        }"
        <!-- 点击时更新 currentIndex 的值 -->
        @click="currentIndex = index"
        ></li>
  </ul>
</template>

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

// 定义 images 数组
const images = ref([
  { src: 'image1.jpg' },
  { src: 'image2.jpg' },
  { src: 'image3.jpg' },
]);

// 定义当前索引,初始值为 0
const currentIndex = ref(0);
</script>

现在发现props响应式变量也是自动解包的,不需要使用props.name,直接使用name

11 Less的加载与modifyVars

  • 如果要覆盖某个less文件中的某些变量
  1. 加载该less文件
import '@arco-design/web-vue/dist/arco.less';

2.编写新的less文件,覆写变量的值

// modifyVarsTheme.less
@olivine-450: #6d8e53;
@color-ivory-light: #faf9f5;
@color-ivory-shoadow: #e4e4e2;
@text-black: #141413;

@color-bg-1:@color-ivory-light;
@color-bg-2:@color-ivory-shoadow;
@color-bg-3:#f2c037;
@color-bg-4:#f2c037;
@color-bg-5:#f2c037;
  1. 在vite.config.js中加载
    先安装less 依赖
npm add -D less

改写配置

css:{
    preprocessorOptions: {
        less: {
          modifyVars:{
             hack: `true; @import (reference) "${path.resolve('./src/modifyVarsTheme.less')}";`
          },
          // 在这里引入你的新 Less 文件
          // additionalData: `@import "${path.resolve('./src/theme.less')}";`,
          javascriptEnabled: true,
      }
    }
  }
  • 如果要添加自己的less 且在style中用var()方式引用
    1.安装依赖(略过)
    2.编写less
@olivine-450: #6d8e53;
@color-ivory-light: #faf9f5;
@color-ivory-shoadow: #e4e4e2;
@text-black: #141413;
@button-bg-color:@olivine-450;          // 按钮背景颜色
:root {
    --button-bg-color: @button-bg-color;
  }

2.直接引入

<script setup>
import './theme.less'
<script >

3.使用

.slider-btn {
  color: var(--button-bg-color);
  background-color: var(--button-bg-color);
}

也可以将新加的less变量直接放入modifyVarsTheme.less,也能被应用,形式上更简洁,但App.vue中的import和vite.config.js中的modifyVars都要做

12 在Vite中配置@路径别名

Vite中,@指向项目的src目录
vue单组件中,图片引用时也用@,但仅限于.vue文件,在纯script,如Vite的main.js中使用时,要提前在vite.config.js中配置

resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }

使用:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import './style.css'
import App from './App.vue'
import router from '@/router' // 自动识别 router/index.js

createApp(App)
    .use(router)
    .use(createPinia())
    .mount('#app')

特别注意,如果@出现在模板的src属性或者在脚本的import中,它会被正确解析
但如果包装在变量里,再把这个变量和模板的src绑定,@就不能被正确解析
同样,脚本中的public/img的简写/img也不会被正确解析

综上,为了统一规范和编写方便。把所有静态资源放到public/xx下。
引用静态变量时,在script中写./xx.,在import其他组件时,写@/
这样开发环境的表现就和生产环境一致了
但是如果使用@引入其他vue或模块,就会丧失编辑器的变量索引定位能力,所以我又用回了使用相对路径引入模块

又发现一条新规则
script脚本中,./的两种用法都能被Vue3识别

  • /public/ 即/public/img 在模板中可写为/img 在脚本中写为./img/img都能被识别
  • public其他同级的上级目录 即有目录

    ./config也能被Vue3识别
    原因:
    http://localhost:5173指向项目根目录 脚本中.//指向此目录
    因为public目录在生产中视作不存在,其子文件夹直接暴露于外,使用public/img时,./img /img最终等价于 http://localhost:5173/img
    但同级config目录不会被无视,即./config等价于http://localhost:5173/config
    总结
    开发环境下,
  • 无论在模板还是脚本中,.//都等价http://localhost:5173/
  • public目录被无视,其子文件夹或子文件直接暴露于http://localhost:5173/

新的痛点
config文件夹是我在开发环境下因业务需要额外生成的,不属于Vite的自带目录 如何生产环境下使用自己生产的文件夹呢?
首先确定生产环境下.//的指向路径
生产环境下,主程序valki.exe所在目录为electron-builder-output/win-unpacked/

  • 开发环境下,在electronmain.js中直接创建一个文件夹,如
let prefix='config';
// 检查config目录是否存在,如果不存在则创建
if (!fs.existsSync(prefix)) {
    fs.mkdirSync(prefix, { recursive: true }); // recursive: true 表示递归创建目录
}

config文件夹会直接创建在Vite工程的根目录,即和public同级

  • 生产环境下,该代码会在valki.exe的同级生产config文件夹
    使用以下三种路径,它们在打包后的生产环境中的显示效果如下:

    应用内被file协议解析后的完整路径

    由第2条可见/路径完全失效,盘符都没有,无法定位到任何文件,无论开发还是生产环境都不要使用
    分析1、3可知,./指向生产环境中的路径为file:///F:/Zlab/JavaScript/electron-builder-output/win-unpacked/resources/app.asar/dist/
    这个dist就是开发环境下,所有静态资源被Vite整合后的目录,生产环境下,它被放在win-unpacked/resources下,注意app.asar在文件系统内显示是一个文件,但它是一种特殊的封装类型,有子目录。

回到正题,如何保证开发和生产环境下都能访问到自己额外生成的文件夹config

  • 开发环境:文件夹由electron的main.js生成在Vite的publlic同级位置,使用./config访问
  • 生产环境:文件夹由valki.exe在其同级目录生成,./被解析为file:///F:/Zlab/JavaScript/electron-builder-output/win-unpacked/resources/app.asar/dist/,但实际要访问file:///F:/Zlab/JavaScript/electron-builder-output/win-unpacked/
    二者差三个目录,只需要在开发环境使用./config 生产环境使用./../../../config

如何轻松实现这一点呢?
在Vite工程根目录内新建

  • .env.development文件 (不是后缀 就是文件名)
    内部可定义全局变量,Vite自动检测,开发环境生效
VITE_EXE_DIR='./'
  • .env.production文件
    生产环境生效
VITE_EXE_DIR='./../../../'

使用方法

<script setup>
import { ref } from 'vue'
const exeDir = import.meta.env.VITE_EXE_DIR
const Imgsrc1 = ref(exeDir + 'config/roleName/pic3.png')
const Imgsrc2 = ref('./img/pic1.jpg')
const Imgsrc3 = ref('./img/pic1.jpg')
</script>
  
<style scoped>
</style>

如此,三个图片无论在开发还是生产环境都能正常使用。public/xx下的资源仍然使用./xx访问,因为它是Vite维护的静态资源,使用./xx无论在开发还是生产环境都能正常访问

13 如何找到打包好的electron应用的工作目录

如果我们在vue的脚本写一个路径:

<script setup>
imgSrc = './config'
<script>

./img在会被打包好的.exe解析为

所以要找到工作目录,只需要./../../../就指向了.exe的工作目录,就和主进程的工作目录对齐了

14 总结Vue3计算属性和监听器的响应式更新

<template>
<div>
    <p>count: {{ count }}</p>
    <p>doubleCount: {{ doubleCount }}</p>
</div>
<button @click="increment">Increment</button>
</template>

<script setup>
import { ref, computed, watchEffect} from 'vue'
const count = ref(0)

//计算属性
const doubleCount = computed(() => count.value * 2)
//监听器
watchEffect(() => {
    console.log('count:', count.value) 
    console.log('doubleCount:', doubleCount.value)
    count.value = 0 
})
//方法
const increment = () => {
    count.value++
}
</script>

点击增加按钮时,会导致count变化。然后

  • 触发侦听器副作用函数,打印信息 改变count的value
  • 触发计算属性
    但是
  1. 监听器不会因为ount的value改变而循环触发,这是Vue3的设计机制决定的
  2. 监听器(watchEffect)和计算属性都会在组件挂载时执行一次
  3. watch不会再组件挂载时执行一次

watch监听器的其他规则

watch 第一个参数必须是:

  • 一个 getter 函数 () => value
  • 一个 ref 对象
  • 一个 reactive 对象
  • 包含前三种类型的数组
    假设有一个reactive对象,如:
const form = reactive({                      // 表单数据对象
    name: '',
    gender: '',
    genderOther: '',
    area: '',
    flag: '',
    presupposition: '',
    voiceEngine: '',
})

监听form.voiceEngine时,一定要用

watch(() => form.voiceEngine, callback)

或者用深度递归监听

watch(form, () => {
  console.log('form 的任何属性变化都会触发')
})

watch(() => form, callback)仅限顶层监听、只能在form被整个替换的时候触发,不会监听内部属性

  • 什么时候加.value
  1. 不需要加 .value 的情况:
watch(selectedChat, (newVal) => {
  // 这里会自动解包 ref,newVal 已经是值本身
  console.log(newVal) 
})

这是因为 watch 会自动解包 ref,你直接传入 ref 对象即可。

  1. 需要加 .value 的情况:
watch(() => selectedChat.value, (newVal) => {
  // 使用 getter 函数形式时需要显式加 .value
  console.log(newVal)
})

当你使用 getter 函数形式时,需要显式访问 .value

  1. 特殊情况
    如果 selectedChat 本身是一个 reactive 对象(不是 ref),那么永远不需要加 .value

最佳实践建议:

  • 如果是 ref 对象,直接传入 watch(selectedChat) 即可
  • 如果需要深度监听或复杂表达式,使用 getter 函数形式并显式加 .value

计算属性和监听器的设计模式

  • 什么是副作用
    在编程中,副作用(Side Effect)指的是函数或表达式在执行时,除了返回一个值之外,还对程序的其他部分产生了影响,例如:
  • 修改外部变量(如chatList.value
  • 发送网络请求(如 window.ipcRenderer.invoke()
  • 操作 DOM
  • 触发其他计算或状态更新
    watch 的设计目的就是监听响应式数据的变化并执行副作用
  • Vue 的 计算属性(Computed) 的设计初衷是:
  • 纯函数:只依赖响应式数据,返回一个计算后的值,不应该有副作用。
  • 高效缓存:只有当依赖项变化时才会重新计算,避免重复执行。
  • 声明式:只描述"数据如何计算",而不是"数据如何更新"。
const chatListShow = computed(() => {
    if (selectedChat.value) {
        updateChatList() // ❌ 副作用:发送 IPC 请求、修改 store
    }
    return chatList.value.map(/* ... */)
})

15 延迟加载的妙用

  • 加载图片不显示
    当把一个img标签的src使用ref绑定到一个src时,如果src地址不变,但其对应图片已经发生更改(比如同名替换、裁剪),因为ref.value没有变更,对应组件不会重新渲染,显示内容自然也不会随图片更改而变化,此时可以使用延迟赋值,强制刷新一下组件。
// 图片路径不变,但内容变更的操作
const path = changeImg()
setTimeout(() => {
refImgPath = path
}, 100)

其实针对图片缓存导致的图片不更新setTimeout偶尔也会失灵
终极的解决方案是:添加时间戳参数
模板:

<img v-if="avatarSrc" :src="avatarSrc+ '?t=' + Date.now()" alt="avatar" 
      :style="{
      width: '100%',
      height: '100%',
      objectFit: 'cover',
}" />

脚本:

const handleImgName = (name) => {            // 图片裁剪组件回调函数
    avatarSrc.value = ''
    avatarSrc.value = exeDir + 'config/roleName/' + name
}
  • 子窗口渲染闪烁
    使用一下逻辑渲染子窗口时
<template>
  <!-- 根据 isSubWindow 决定渲染布局 -->
  <div class="app">
    <template v-if="!isSubWindow">
      <!-- 原始布局结构 -->
      <a-layout class="layout-mother">
        <!-- 侧边栏、header 等原有代码 -->
      </a-layout>
    </template>
    
    <template v-else>
      <!-- 子窗口专用简约布局 -->
      <router-view v-slot="{ Component }">
        <component :is="Component" />
      </router-view>
    </template>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const isSubWindow = computed(() => route.meta.isSubWindow)
</script>

子窗口会先加载主布局,再切换到子窗口的路由布局,这种瞬时的切换就像是闪烁,用户体验极差,只需在主进程中显示子窗口的代码处添加一个100ms的延迟即可隐藏闪烁阶段

subWindow.once('ready-to-show', () => {
        setTimeout(() => {
            subWindow.show();
        }, 100)
    });  // 窗口准备好后再显示
  • 避免动画合并
transform.value = `translateX(-${(props.cards.length + 1) * (100 +  carouselMotherGapRate.value)}%)`
        setTimeout( () => {
            onRight = false
            transform.value = defaultTranslateX.value
            transition.value = defaultTransiton
        }, 0)

上面代码中,两次设置了transform.value,理想是完成两次动画
可以把setTimeout换成

requestAnimationFrame(() => {
            onLeft = false
             transform.value = defaultTranslateX.value
            transition.value = defaultTransiton
         })

手动触发时,两种渲染动画帧的方法都可用。
但如果设计了autoPlay()这种自动执行动画的函数,且使用requestAnimationFrame,则两次 transform.value会被合并成最后一次,这会导致动画不符合预期,但使用setTimeout就能顺利执行两次transform动画
但首次加载时,使用setTimeout仍会偶尔出现第一个动画消失的问题
transition.value = '' "transform.value = 'translateX(0%)'在同一帧内更新,然后马上就 setTimeout(..., 0)设回去了,浏览器可能都还没来得及应用第一组样式变化。

还有终极一招 都亲测有用

  • nextTick()等待DOM更新
await nextTick()  // 等待 DOM 更新生效

setTimeout(() => {
    onLeft = false
    transform.value = defaultTranslateX.value
    transition.value = defaultTransiton
}, 0)
  • 双重 requestAnimationFrame
transform.value = `translateX(0%)`
transition.value = ''
requestAnimationFrame(() => {
    requestAnimationFrame(() => {
        onLeft = false
        transform.value = defaultTranslateX.value
        transition.value = defaultTransiton
    })
})

16 模板中,监听事件执行多个函数

@click="activateButton"如果要执行多个函数,使用"fun1(),fun2()"还是"fun1,fun2"

在Vue模板中,如果要执行多个函数,正确的语法是:

  1. 使用分号分隔的多个语句:
@click="fun1(); fun2()"
  1. 或者使用逗号操作符(会返回最后一个表达式的结果):
@click="(fun1(), fun2())"

不推荐直接写"fun1,fun2",因为这样只会执行第二个函数(逗号操作符的特性)。

最佳实践是使用方法1,清晰明确:

@click="activateButton(); anotherFunction()"

或者更好的做法是在脚本中创建一个新方法来组合这些调用:

@click="handleButtonClick"

然后在script中:

function handleButtonClick() {
  activateButton();
  anotherFunction();
}

17 在父组件中,对子组件添加类名,无需:deep()穿透,直接进行CSS控制

父组件使用子组件时,对子组件添加的非props属性自动挂载到子组件根元素,class属于非proos属性,使父组件的 scoped 样式可以直接生效,而无需使用 :deep()穿透
前提是子组件有一个根元素

  • 父组件代码:
<template>
    <Child class="test-class xxx"></Child>
</template>
<style scoped>
.test-class {
    color: blue;
}
.xxx {
    background-color: yellow;
}
</style>
  • 子组件代码
<template>
    <div>
        <h1>This is the child component</h1>
    </div>
</template>

<script setup>

</script>

达成效果:


并且在一个组件内,Vue会为组件内的每个根元素都添加同一的data-v值 但还是不能借此直接使用后代选择器控制其子元素,因为在style scoped中书写时,会被vue转化为

.Child[data-v-parentxxxxxx] .inner-element[data-v-parentxxxxxx] {
  color: red;
}

18 一般使用v-model="XX",v-model:visiable是什么用法

在 Vue.js 里,v-model 是一个语法糖,主要用于实现双向数据绑定。一般状况下,v-model="XX" 把组件里的 value 特性和 input 事件结合起来,从而达成双向数据绑定。

v-model:visible 是 Vue 3 支持的自定义 v-model 参数的用法。借助这种方式,你能够指定不同的属性和事件来实现双向数据绑定,而并非局限于默认的 valueinput

用法解释

  • v-model 后面的冒号加上属性名(像 visible),意味着要对这个属性实现双向数据绑定。
  • 组件内部要通过 definePropsdefineEmits 来处理这个自定义的 v-model 属性和对应的更新事件。

示例代码

下面是一个简单的模态框组件示例,展示了 v-model:visible 的使用方法:

<template>
  <div>
    <!-- 父组件 -->
    <button @click="showModal = true">打开模态框</button>
    <Modal v-model:visible="showModal" />
  </div>
</template>

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

const showModal = ref(false);
</script>
<template>
  <!-- 子组件 -->
  <div v-if="visible" class="modal">
    <div class="modal-content">
      <h2>模态框标题</h2>
      <p>这是模态框的内容。</p>
      <button @click="closeModal">关闭</button>
    </div>
  </div>
</template>

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

const props = defineProps({
  visible: Boolean
});

const emits = defineEmits(['update:visible']);

const closeModal = () => {
  emits('update:visible', false);
};
</script>

<style scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-content {
  background-color: white;
  padding: 20px;
  border-radius: 5px;
}
</style>

代码解释

  • 父组件:借助 v-model:visible="showModal"showModal 变量和 Modal 组件的 visible 属性绑定在一起。
  • 子组件
    • defineProps 接收 visible 属性。
    • defineEmits 定义 update:visible 事件,当模态框关闭时触发该事件,以此更新父组件里的 showModal 变量。

通过这种方式,你能够自定义 v-model 绑定的属性和事件,达成更灵活的双向数据绑定。

19 ref绑定DOM元素

<audio id="audio" :src="songSrc" ref="audioPlayer"></audio>
const audioPlayer = ref(null)  

在 Vue 3 的 Composition API 中,audioPlayer 通过 ref(null) 初始化为 null,但最终能正确绑定到 <audio> DOM 元素,这是由 Vue 的模板引用(Template Refs)机制实现的。具体绑定过程如下:


1. 初始化阶段

const audioPlayer = ref(null) // 创建一个响应式引用,初始值为 null
  • 此时 audioPlayer.valuenull,尚未绑定任何 DOM 元素。

2. 模板绑定阶段

<audio id="audio" :src="songSrc" ref="audioPlayer"></audio>
  • 当 Vue 渲染模板时,检测到 ref="audioPlayer" 属性:
    • 自动将 audioPlayer 引用的 .value 更新为对应的 DOM 元素
    • 绑定发生在组件挂载后(afterMount)

3. 绑定时机(关键)

生命周期阶段 audioPlayer.value 的值
组件创建时 null
模板编译完成后 仍为 null
DOM 挂载后 自动变为 <audio> 元素引用

4. 验证绑定的正确性

你可以在 onMounted 钩子中验证:

import { onMounted } from 'vue'

onMounted(() => {
  console.log(audioPlayer.value) // 输出:<audio id="audio">...</audio>
  console.log(audioPlayer.value === document.getElementById('audio')) // true
})

5. 为什么能安全使用?

即使初始化时为 null,你也可以直接操作 audioPlayer,因为:

  1. 模板中的 ref 是响应式的
    Vue 会在 DOM 渲染完成后自动更新引用
  2. 实际使用时通常发生在交互事件(如点击)
    此时组件早已挂载,audioPlayer.value 必然已是有效的 DOM 元素:
    const playMusic = () => {
      audioPlayer.value.play() // 安全调用,因为点击时绑定已完成
    }
    

对比:如果手动绑定(非模板 ref)

// 不推荐!仅用于理解原理
const audioPlayer = ref(null)
onMounted(() => {
  audioPlayer.value = document.getElementById('audio')
})

Vue 的模板 ref 机制本质上帮你自动完成了这个过程。


20 总结闭合标签写法

环境 推荐写法 是否必须闭合
原生 HTML5 <img src="..." alt=""> ❌ 可选
XHTML <img src="..." alt=""/> ✅ 必须
Vue 模板 <img src="..." alt="" /> ✅ 必须
JSX <img src="..." alt="" /> ✅ 必须
  • 在 Vue/JSX 中:始终自闭合<img />),保持语法一致性
  • 在纯 HTML 中:省略斜杠<img>),符合 HTML5 标准
  • 在混合环境中:统一采用 <img /> 写法(兼容性最好
场景 正确写法 说明
带子内容 <MicMonitor>内容</MicMonitor> 传统 HTML 风格(需显式闭合)
无子内容 <MicMonitor /> 自闭合风格(推荐在 Vue/JSX 中使用)
HTML5 原生 <mic-monitor></mic-monitor> 需用 kebab-case(浏览器原生解析)

21 默认提交表单事件的触发规则

  • 表单父元素内有type=submit的button,如
<a-form class="role-profile-form" :model="form" @submit="handleSubmit">
    <a-form-item field="presupposition" tooltip="你想要什么样的ta?" label="预设">
        <a-textarea class="presupposition-textarea" v-model="form.presupposition" placeholder="请输入预设" spellcheck="false"/>
    </a-form-item>
    <div class="handle-btns" :style="{
        display: 'flex',
        justifyContent: 'flex-end',
        width: '100%',
        marginBottom: '30px'
        }">
        <a-button type="secondary" shape="round" :style="{
            marginRight: '20px'
            }">取消
        </a-button>
        <!-- 触发表单提交事件的按钮 -->
         <a-button type="primary" shape="round" html-type="submit">保存</a-button> 
    </div>
</a-form>

如果没有这个按钮,那在表单内按回车就不会触发默认表单提交事件@submit

22 Vue3模板的自动解包

模板中不需要加.value直接访问的响应式变量

  • ref 创建的响应式变量
  • reactive 创建的响应式对象
  • toReftoRefs 创建的响应式引用
  • 计算属性

模板中可以不加props.访问props.xxx
但为了避免重名混乱,还是加上props比较好
脚本中访问计算属性的value要加.value

22 尽量使用ref

  • backgroundImageUrl.value = defaultPostUrl(常量)
    → 直接赋值静态字符串,触发同步更新 → 浏览器会立即重新渲染背景图
  • backgroundImageUrl.value = test.value(ref)
    → 通过响应式代理赋值 → Vue 会将其纳入下一个tick的更新队列 → 渲染更平滑

23 ref绑定DOM元素和Vue组件

  • 绑定DOM元素
<div ref="divRef"></div>

可以通过divRef访问原生DOM元素的所有属性,比如
const contentHeight = divRef.value.clientHeght

  • 绑定Vue组件
<a-div ref="divRef"></a-div>

divRef.value.$el才是组件对应的原生 DOM 元素
访问DOM属性需要写成:const contentHeight = divRef.value..$el.clientHeght

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容