学习

定义全局变量

main.ts 里定义全局变量

1
2
3
4
5
app.config.globalProperties.globalInfo = {

  bodyWidth: 1300,

}

在组件中引用全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>

  <div class="header">

    <div class="header-content"

    :style="{width: proxy.globalInfo.bodyWidth + 'px'}" >

  </div>
</div>

</template>

<script setup>

import {ref, getCurrentInstance} from 'vue'

const {proxy} = getCurrentInstance()

TypeScript 编译器无法理解 .vue 文件和 .js 文件

原因 : 缺少了必要的 类型声明

解决方案

要解决这个问题,你需要为 .vue.js 文件创建或配置类型声明文件。


1. 解决 .vue 文件报错

对于 .vue 文件,你需要在你的项目中创建一个 .d.ts 文件来声明其类型。通常,一个名为 env.d.ts 的文件会包含这个声明。

  • 步骤

    1. 在你的 src 目录下,创建一个名为 env.d.ts 的文件(如果它不存在)。

    2. 在文件中添加以下内容:

      TypeScript

      1
      2
      3
      4
      5
      6
      7
      /// <reference types="vite/client" />

      declare module '*.vue' {
      import type { DefineComponent } from 'vue'
      const component: DefineComponent<{}, {}, any>
      export default component
      }

    这个声明告诉 TypeScript,任何以 .vue 结尾的文件都应该被视为一个 DefineComponent,从而让导入 .vue 文件不再报错。


2. 解决 .js 文件报错

对于 .js 文件,你需要确保 TypeScript 知道如何处理它。通常,Vite + Vue 3 项目的 TypeScript 配置已经处理了这个问题,但如果你的 tsconfig.json 配置不正确,就会出现这个错误。

  • 步骤

    1. 检查你的 tsconfig.jsontsconfig.app.json 文件。

    2. 确保 compilerOptions 中包含了 allowJs: true。这个选项允许 TypeScript 编译器处理 JavaScript 文件。

      JSON

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      {
      "extends": "@vue/tsconfig/tsconfig.dom.json",
      "compilerOptions": {
      "allowJs": true, // 确保有这一行
      "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
      "composite": true,
      "baseUrl": ".",
      // ... 其他配置
      },
      // ...
      }

3. 重启 TypeScript 服务

完成以上更改后,请执行以下操作:

监听滚动条

功能: 实时监听网页滚动条的位置, 实现当滚动条下滑且下滑到一定位置时, 隐藏组件; 当上滑时, 显示组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//导航栏的显示效果
const showHeader = ref(true)
//获取滚动条的高度
const getScrollTop = () => {
let scrollTop = //不同浏览器获取的方式不同
document.documentElement.scrollTop
|| window.pageYOffset
|| document.body.scrollTop

return scrollTop
}

const initScroll =() => {

let initScrollTop = getScrollTop()
let scrollType = 0

window.addEventListener('scroll', () => {
let currentScrollTop = getScrollTop()
if (currentScrollTop > initScrollTop) {
scrollType = 1
} else {
scrollType = 0
}
initScrollTop = currentScrollTop
// console.log(scrollType === 1 ? 'shang' : 'xia')
if (scrollType === 1 && currentScrollTop > 150) {
showHeader.value = false
} else showHeader.value = true
})
}

onMounted(() => {
initScroll()
})

隐藏密码

效果: 初始默认密码为隐藏状态(眼睛图标闭合)
点击后密码为显示状态(眼睛图标睁开)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<tepmlate>
<!-- 登录密码 -->
<el-form-item prop="registerPassword">
<el-input
:type="passwordEyeType.registerPasswordEye?'text':'password'"
size="large"
clearable
placeholder="请输入密码"
v-model="formData.registerPassward"
>
<template #prefix>
<span class="iconfont icon-password"></span>
</template>
<template #suffix>
<span
@click="eyeChange('registerPasswordEye')"
:class="[
'iconfont',
passwordEyeType.registerPasswordEye
?'icon-eye'
: 'icon-close-eye'
]"></span>
</template>
</el-input>
</el-form-item>
</template>

<script>

//密码显示隐藏操作

const passwordEyeType = reactive({

    passwordEye:false,
    registerPasswordEye:false,
    reRegisterPasswordEye:false,
})

const eyeChange = (type)=>{
  passwordEyeType[type] = !passwordEyeType[type]
}
</script>

合并两个仓库

在合作的过程中, 我终于知道为什么说合并两个仓库很常见了

应用场景 :
多人写一个项目, 需要一个人把主体仓库建好, 其余人去fork这个仓库
每个人写好自己的功能部分, 写完后发送请求
然后建立这个仓库的人需要新建一个分支

  • 首先需要remote其他人复刻的仓库(contirbutor)
1
2
3
4
5
6
7
8
9
10
11
# 1. 确认已在正确的本地分支
git checkout main

# 2. 从贡献者的远程仓库获取最新代码
git fetch contributor

# 3. 将贡献者的分支合并到你的本地 main 分支
git merge contributor/main

# 4. 将合并后的代码推送到你的主仓库
git push origin main

两个弹窗同时出现时的闪退问题

组件: element中的# MessageBox 消息弹框
描述: 点击登录时界面出现一个弹出框, 第二个弹出框是是隐藏状态.
输入正确格式的邮箱后, 点击按钮[[前端了解/仿知乎, 掘金vue3项目/_resources/学习/e7181b6a66f4121a31efc96c0b9b8db2_MD5.jpeg|Open: Pasted image 20250828151143.png]]
![[前端了解/仿知乎, 掘金vue3项目/_resources/学习/e7181b6a66f4121a31efc96c0b9b8db2_MD5.jpeg]]
第二个弹出框出现后, 两个弹出框立马闪退

问题一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const showSendEmailDialog = ()=>{
formDataRef.value.validateField("email", (valid) => {
if(!valid) {
return
}
dialogConfig4SendEmailCode.value.show = true

nextTick(() => {
//进行表单初始化
formData4SendEmailCode.value = {
email: formData.value.email,
checkCode: ''
}
//先判断这个表单是否为空
if (formData4SendEmailCodeRef.value) {
formData4SendEmailCodeRef.value.resetFields();
}
//存在表单后再生成验证码
changeCheckCode(1);
})
})
}

访问本地图片

1
2
3
  
const localImage = new URL('@/assets/img/default.jpg', import.meta.url).href
<img src="localImage"> </img>

这是在现代 JavaScript 模块(ES Module)中**动态导入(dynamic import)**本地资源的常见写法,尤其是在使用像 Vue CLI 或 Vite 这样的构建工具时。


代码分解

为了理解这行代码,我们把它拆成两部分:

  1. import.meta.url

  2. new URL(..., ...)

1. import.meta.url

这是一个内置的 JavaScript 属性,它在 ES Module 中提供了当前模块的 URL

  • 简单来说,它告诉你当前正在运行的 JavaScript 文件在文件系统中的完整路径。

  • 例如,如果你的文件路径是 src/components/MyComponent.vue,那么 import.meta.url 可能会是 file:///C:/.../src/components/MyComponent.vue

2. new URL(resource, base)

这是一个标准的浏览器 API,用于创建和处理 URL。它接收两个参数:

  • resource:你想要引用的相对路径,这里是 '@/assets/img/default.jpg'

  • base:一个基础 URL,相对路径会基于它进行解析,这里是 import.meta.url

当这两者结合在一起时,new URL() 就会根据当前模块的 URL (import.meta.url) 解析出 '@/assets/img/default.jpg' 的完整绝对路径。

3. .href

最后,.href 属性获取的是解析后 URL 对象的完整字符串表示。


为什么需要这样写?

在过去,我们通常使用 require() 或直接的相对路径来引用图片。但在现代前端项目中,由于构建工具(如 Vite)的优化,图片的路径通常会在构建时被处理和压缩,其最终路径会改变。

  • 直接使用相对路径./assets/img/default.jpg 在不同组件中可能会有不同的解析结果,容易出错。

  • 使用 new URL():这种方法能确保路径解析始终是相对于当前模块的,并且生成的 URL 是一个可被构建工具识别和处理的静态资源路径。

简单来说,这行代码的作用是:安全地、动态地获取本地图片的 URL,以便在运行时可以正确地引用它。

这使得你可以将图片路径作为变量传递给组件或在 JavaScript 中使用,同时确保图片在构建后能够被正确加载。

板块部分高亮状态延迟出现

问题描述: 导航栏的板块部分在切换板块时, 字体的active并不会立即生效, 但是在点击第二次时却会生效;

原思路

在pinia中定义两个数

1
2
const activePboardId = ref(0) //一级板块
    const activeBoardId = ref(0) //二级板块

表示高亮状态的板块编号

当点击某一栏时, click函数改变高亮的板块编号, 同时跳转路由
watch监听pinia中高亮编号的变化, 一旦发生改变立刻给对应的板块添加active属性样式

原先代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
	<!-- 板块信息 -->
<div class="menu-pannel">
<router-link class="menu-item home" to="/"
:class="{'active': activePboardId === 0}"
>首页</router-link>
<template
v-for="item in boardList"
:key="item.boardId">
<el-popover
v-if="item.children.length >= 1"
:width="250"
title="Title"
placement="bottom-start"
trigger="hover">
<template #reference>
<span class="menu-item"
@click=boardClickHandler(item)
:class="{'active': activePboardId === item.boardId}">
{{ item.boardName }}
</span>
</template>
<span class="sub-board-list">
<span class="sub-board"
v-for="subBoard in item.children"
@click="subBoardClickHandler(subBoard)"
:class="{'active': activeBoardId === subBoard.boardId}">
{{ subBoard.boardName }}
</span>
</span>
</el-popover>
<span v-else class="menu-item"
@click="boardClickHandler(item)"
:class="{'active': activePboardId === item.boardId}">{{ item.boardName }}</span>
</template>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

//板块点击
const boardClickHandler = (board) => {
userStore.activePboardId = board.boardId
userStore.activeBoardId = null; // 清空二级板块的高亮状态
router.push(`/forum/${board.boardId}`)
}
//二级板块
const subBoardClickHandler = (subBoard) => {
userStore.activePboardId = subBoard.pBoardId
userStore.activeBoardId = subBoard.boardId
router.push(`/forum/${subBoard.pBoardId}/${subBoard.boardId}`)
}

//板块字体颜色变化
const activePboardId = ref(0)
const activeBoardId = ref(0)
watch(
() => userStore.activePboardId,
(newValue,oldValue) => {
activePboardId.value = newValue
}
)
watch(
() => userStore.activeBoardId,
(newValue,oldValue) => {
if (newValue !== undefined) {
activeBoardId.value = newValue
}
}
)

问题

在学长(shuangbin)的帮助下, 终于成功找到问题所在!

数据类型错误
在切换板块时, 调用 boardClickHandlersubBoardClickHandler函数 , 路由跳转

注意 : 在另一个vue文件(面包屑导航)中, 我会监听路由变化, 对userStore.activePboardactiveBoard 进行再次赋值, 因为值是从路由当中直接获取的, 所以他们的类型是 String,
然而, active样式生效的判断确实 Number 类型, 所以会导致高亮显示在第一时间不能生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//监听路由变化

watch(
    () => route.params,
    (newValue, oldValue) => {
       pBoardId.value = newValue.pBoardId || 0
       boardId.value = newValue.boardId || 0
       userStore.activeBoardId = boardId.value
       userStore.activePboardId = pBoardId.value

       setSubBoard()
       loadArticle() //更新页面
    },
    {immediate: true, deep: true})

解决方案

不再监听pinia中 activeboard数据的变化, 当路由信息改变时, 直接改变active信息, 相当于取消了监听中间值了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    //板块点击
const boardClickHandler = (board) => {
router.push(`/forum/${board.boardId}`)
}
//二级板块
const subBoardClickHandler = (subBoard) => {

router.push(`/forum/${subBoard.pBoardId}/${subBoard.boardId}`)
}

//板块字体颜色变化
const activePboardId = ref(0)
const activeBoardId = ref(0)

watch(
() => route.params,
(newValue) => {
// 从路由参数中获取板块 ID,并确保是数字类型
userStore.activePboardId = newValue.pBoardId ? parseInt(newValue.pBoardId) : 0;
userStore.activeBoardId = newValue.boardId ? parseInt(newValue.boardId) : null;
activePboardId.value = userStore.activePboardId
activeBoardId.value = userStore.activeBoardId
},
{ immediate: true, deep: true }
);

数据加载不完全, 导致画面闪现

在加载数据时, 加载不完全会显示数据为空时的数据, 加载后又会出现正常数据, 画面连续切换, 造成不好的用户体验.
解决方法 : 增加 v-if, 当数据存在内容时, 才会才是渲染

css样式不生效

问题描述:
后端返回整个帖子的内容(html格式), 前端要做的就是把它以html的格式挂到对象的位置

但, 当前端想要对这个html里面的内容样式做一些修改 , 却发现某些嵌套的比较深的盒子不受前端规定样式的控制.
例如 :
后端返回

1
<div> <p>  <img src="" alt="" />  </p> </div>

前端代码

1
2
3
4
5
6
7
8
9
10
11
<div class="detail" id="detail" v-html="articleInfo.content"></div>

.detail {
letter-spacing: 1.5px;
line-height: 22px;
img {
max-width: 90%;
height: auto;
cursor: pointer;
}
}

可以很明显的看到, 前端想对帖子的图片大小进行控制(最大宽度不超过父组件宽度的90%, 添加鼠标样式), 但真实操作时, 发现这个css代码完全没作用

经过不断的寻找, 我终于发现了:

v-html 的“影子 DOM”行为

  • 问题v-html 的内容被插入到 DOM 中,但它不会被 scoped 样式所影响。这是 Vue scoped CSS 的一个重要特性。scoped 样式只对当前组件模板中的元素生效,而 v-html 渲染的内容被视为“外部”内容。

  • 解决方案: 要解决这个问题,你需要移除 <style scoped> 属性,或者使用一个深度选择器来穿透作用域。

    • 方法一:移除 scoped (不推荐) 移除 <style scoped> 后,你的样式会成为全局样式,可能会影响到其他组件。

    • 方法二:使用深度选择器 (推荐) 在你的 CSS 中使用 ::v-deep>>> 组合子,让样式规则能够影响 v-html 渲染的内容。

::v-deep>>>(有时也写作 /deep/)是 CSS 深度选择器,它们的作用是让你在 Vue 组件中,能够穿透 scoped 样式的限制,去修改子组件或由 v-html 渲染出来的 HTML 元素的样式。


scoped 样式的问题

在 Vue 组件中,当我们使用 <style scoped> 时,Vue 会为组件内的所有 HTML 标签添加一个唯一的属性,比如 data-v-f3f3eg9

这样,你的 CSS 规则就会被自动加上这个属性选择器,比如:

CSS

1
2
3
4
5
6
7
8
9
/* 你写的 */
.title {
color: blue;
}

/* 编译后 */
.title[data-v-f3f3eg9] {
color: blue;
}

这保证了你的样式只对当前组件有效,不会污染全局。

但这也带来一个问题:如果你想修改一个子组件的内部样式(比如一个第三方 UI 库的按钮),或者修改通过 v-html 动态渲染出来的内容的样式,scoped 样式就无能为力了,因为它不会给这些“外部”元素添加 data-v 属性。


::v-deep>>> 的作用

深度选择器就是为了解决这个问题而生的。它会强制 Vue 的样式编译器,让其规则跳过 scoped 样式的限制,直接作用于目标元素。

语法示例:

假设你的组件里有一个子组件 <MyButton />,你想改变它的颜色。

HTML

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<MyButton />
</div>
</template>

<style scoped>
.my-container ::v-deep .button-text {
color: red;
}
</style>

经过编译后,上面的样式规则会变成:

CSS

1
2
3
4
/* 编译后 */
.my-container[data-v-f3f3eg9] .button-text {
color: red;
}

这样,你的样式就可以成功地应用到子组件内部的 .button-text 元素上了。

它们之间的区别

  • >>>/deep/:这是旧版的 Vue CLI 2.x 和一些早期的 Webpack 配置中使用的语法。

  • ::v-deep:这是 Vue 官方推荐的深度选择器语法,它在 Vue CLI 3.x+ 和 Vite 构建的项目中得到了广泛支持。

Vue 3 引入了新的 :deep() 语法,作为旧版 ::v-deep 的替代品,目的是让 CSS 深度选择器的语法更规范、更易于理解。虽然 ::v-deep 目前仍然有效,但 Vue 官方鼓励开发者转向新语法,以便在未来的版本中保持兼容性。

总结::v-deep>>> 就像是 Vue scoped 样式的“通行证”,允许你跨越组件边界,对子组件或 v-html 渲染的内容进行样式修改,从而实现更灵活的样式控制。

原版提供的编辑器与最新版的vue3不兼容

如何让让页面内的某个组件出现滚动条

  1. 给想要内部出现滚动条的组件设置一个最大高度 / 宽度 (或者固定高度 / 宽度)
    max-width: calc(100vh - 100px);
  2. 添加CSS属性 overflow: auto;

解释
calc() 函数用一个表达式作为它的参数,用这个表达式的结果作为值
+ 和 - 运算符的两边必须要有空白字符。 比如,calc(50% -8px) 会被解析成为一个无效的表达式,解析结果是:一个百分比 后跟一个负数长度值。而加有空白字符的、有效的表达式

overflow: auto; 是一个 CSS 属性,它的作用是控制当元素内容超出其边界时,如何处理溢出的部分。

auto 这个值是最常用、最智能的选项。它告诉浏览器:

  • 如果内容没有超出容器,就不显示滚动条。

  • 如果内容超出了容器,就自动显示滚动条,让用户可以滚动查看所有内容。

点击按钮时页面自动刷新

在 HTML 中,button 元素如果没有指定 type 属性,它的默认类型是 submit。当一个 submit 类型的按钮被点击时,它会尝试提交最接近它的父级 <form> 元素,这会导致页面刷新。

如果不给button设置 type属性的话, 可能会出现一些意想不到的错误

自定义样式不生效

描述: 在使用element组件库时, 给他加上class名称, 在自定义书写样式时, 发现样式不生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
   <div class="message-info">
<el-dropdown>
<el-badge
:value="messageCountInfo.total"
class="item"
:hidden="
messageCountInfo.total === 0 ||
messageCountInfo.total === null
">
<div class="iconfont icon-message"></div>
</el-badge>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
class="message-item" //想要修改的样式
@click="gotoMessage('reply')">
<span class="text">回复我的</span>
<span
v-if="messageCountInfo.likeComment > 0"
class="count"> {{
messageCountInfo.likeComment > 99
? '99+'
: messageCountInfo.likeComment
}}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>

当把 message-item 的css代码写在嵌套里面, 发现一俺家的样式不生效

但当把 message-item 的css代码直接卸载 <style> 的子级下, 外面没有任何嵌套时, 却能生效

问题出在 Element UI 的渲染方式

  • el-dropdown-menu(以及其内部的 el-dropdown-item)在页面上渲染时,它们不会被放在 message-info 或您的根组件 <div class="header"> 内部。

  • Element UI 通常会使用 Portal(或类似的机制)将这些下拉菜单浮层元素渲染到 <body> 元素的直接子节点

  • 因此,当 Vue 为您的 .message-item 样式加上作用域属性后,例如变成 .[data-v-xxxxxx] .message-item[data-v-xxxxxx],它就无法匹配到那个被渲染在 <body> 下的 <li class="el-dropdown-menu__item message-item"> 元素,样式自然就失效了。

Portal

  • Portal(传送门):在 Vue 3 中,它是一个内置组件,名为 <Teleport>

  • 作用:它允许您在组件内部编写代码(保留组件的逻辑和状态),但将这些代码渲染到 DOM 树中的其他位置,通常是 <body-content> 的最末尾或一个特定的顶级容器中。

Portal/Teleport 主要用于解决以下几类组件的 层叠(z-index)样式作用域 问题:

  1. 模态框/弹窗 (Modal):如果模态框嵌套在很深的组件层级中,其父组件的 overflow: hiddentransform 样式可能会裁剪或影响模态框的显示。将模态框“传送”到 <body> 根部,可以确保它位于所有元素的顶层。

  2. 下拉菜单/气泡提示 (Popover/Tooltip/Dropdown):就像您遇到的 Element UI 组件一样,将浮层元素渲染到 DOM 根部,可以确保它不会被任何父组件的布局或 CSS 属性(如 z-index)限制,从而保证浮层能正确覆盖所有内容。

配置404界面

直接在route的子级下面配置

1
2
3
4
5
6
7
8
9
10
routes: [
{
//项目的基本功能页面
},
{
      path: '/:pathMatch(.*)*',
      name: '404',
      component: ()=> import("@/views/ucenter/Error404.vue")
    },
]

Vue 的scoped原理

这个项目中运用了大量的element插件, 但它提供的基本样式往往不能满足项目需求

在重新定义element组件样式的过程中, 我发现 给有些组件加上class后, 子级写的样式不能生效, 很多时候需要加上深度选择器 :deep()后, 才能起作用.
在网上看到了一篇 帖子 , 终于知道原因了

CSS常见模块化方案

  1. BEM方案:BEM全称是Block Element Modifier,通过.block__element--modifier.模块名__元素名--修饰符名这种CSS命名方式实现样式隔离和模块化;
  2. CSS Modules:将CSS文件进行编译后,使之具备模块化的能力;
  3. CSS-IN-JS:使用 js 来编写CSS规则;

而Vue设置样式的方法则是通过单文件组件中的style标签进行样式,你只要在style标签上添加一个scoped属性,就能轻松实现样式隔离,而且还可以支持lesssass等预处理器,甚至还深度集成了CSS Modules。当然我们这里主要介绍是scoped

scoped的使用

1
2
3
4
5
 <style scoped>
   .container {
       background: red;
  }
 </style>

style标签上增加scoped属性后,最终编译出来的结果会在选择器上增加一个唯一的attribute(比如data-v-mlxsojjm),每个.vue文件编译出来的attribute都不一样,从而实现了样式隔离

1
2
3
4
5
 <style scoped>
   .container[data-v-mlxsojjm] {
       background: red;
  }
 </style>

.vue文件的css编译

1
2
3
4
5
6
7
8
9
10
11
 <template>
  <div class="container"></div>
 </template>
 ​
 <style scoped>
 .container {
  width: 100px;
  height: 100px;
  background-color: red;
 }
 </style>

我们可以用vue提供的解析单文件组件的编译包@vue/compiler-sfc,来解析我们在.vue文件中编写的css。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 const { compileStyle } = require("@vue/compiler-sfc");
 const css = `
 .container {
     width: 100px;
     height: 100px;
     background-color: red;
 }
 `;
 const { code } = compileStyle({
   source: css, // css源代码
   scoped: true, // 是否要启用scoped
   id: `data-v-${Math.random().toString(36).substring(2, 10)}`, // scoped的id
 });
 console.log(code);

编译结果如下:

1
2
3
4
5
 .container[data-v-mlxsojjm] {
     width: 100px;
     height: 100px;
     background-color: red;
 }

可以看到,带了scoped的style标签中的css,编译后会被加上一个属性选择器名字以data-v开头,后面跟的是一个字符串,这个其实可以自己定义,只要保证全局唯一就行了,比如可以取当前文件的路径,然后用摘要函数md5或者sha256去生成一个哈希,取这个哈希值就行了。

而template经过编译后,结果如下:

1
2
3
 <template>
  <div class="container" data-v-mlxsojjm></div>
 </template>

这就是scoped的原理了,通过给组件中DOM元素和CSS各自都添加一个相同且唯一的属性选择器,让当前的css文件的样式只对当前组件生效

注意点

1. 子组件的根节点会同时被自己以及父组件的样式所影响

在vue官网中有这么一段话: “使用 scoped 后,父组件的样式将不会渗透到子组件中。不过,子组件的根节点会同时被父组件的作用域样式和子组件的作用域样式影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式”

啥意思呢?比如你定义一个父组件parent.vue和子组件child.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 // Child.vue
 <template>
     <div class="child-container">
       child
     </div>
 </template>
 ​
 <style scoped>
 .child-container {
   color: red;
 }
 </style>
 ​
 // parent.vue
 <template>
   <div class="container">
     <Child />
   </div>
 </template>
 <script>
 import Child from "./Child.vue";
 export default {
   name: "Parent",
   components: {
     Child,
  },
 }
 </script>
 ​
 <style scoped>
 .container {
   width: 100px;
   height: 100px;
   background-color: red;
 }
 .child-container {
   color: blue !important;
 }
 </style>

最终渲染出来的子组件里面显示的字体颜色是蓝色。

![[前端了解/仿知乎, 掘金vue3项目/_resources/学习/9edce1d34e35ae9d5e76f67d572ea7ab_MD5.webp]]

怎么会这样呢?看vue最终渲染出来DOM的样子就能看出来了。 ![[前端了解/仿知乎, 掘金vue3项目/_resources/学习/183f7e5887f10e4325a2669f84078831_MD5.webp]]

子组件Child的根节点上既有自己声明scoped后的属性选择器,又有父级的声明scoped后的属性选择器,所以在父组件中,就可以修改子组件根节点的样式了。

我之前不知道这个知识点的时候,被这个坑了一把,不知道为啥自己组件的样式被改了,当时找了半天才看到是父组件改的,所以我之后定义组件根节点的class名字的时候,尽量定义成一个独一无二的,免得无意中被父组件的同名类名的样式污染了。

2. scoped对插槽slot的影响

我们把提供插槽的组件叫Child,使用插槽的组件叫Parent,slot中的内容最终编译出来会同时含有ParentChildscopedId,所以会同时受ParentChild两个组件的的样式影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 // Parent.vue
 <template>
   <div class="container">
     <Child >
       <div class="c1">c</div>
     </Child>
   </div>
 </template>
 <script>
 import Child from "./Child.vue";
 export default {
   name: "Parent",
   components: {
     Child,
  },
 }
 </script>
 <style scoped>
 </style>
 // Child.vue
 <template>
   <div class="child-container">
     child
    <div>
     <slot></slot>
    </div>
   </div>
 </template>
 <style scoped>
 </style>

最终渲染的DOM如下:

![[前端了解/仿知乎, 掘金vue3项目/_resources/学习/6f23925f44d56a3f18c8defd2c6c5480_MD5.webp]]

如果遇到相同权重的样式,比如元素<div class="text">a</div>,在Parent组件中写的样式是.text{ color: red },在Child组件中写的样式是.text{ color: blue },由于在vue父子组件的渲染过程中,子组件会先于父组件渲染完成,所以最终父组件样式会覆盖子组件相同权重的样式,最终渲染color颜色会是red

深度选择器

在实际开发中,我们常常需要在父组件修改子组件的样式,比如在用三方组件库的时候,组件库里的样式往往不能100%满足我们的需求,这时候就要用到深度选择器做样式穿透了。

深度选择器有4种语法:

  1. 三个大于号 >>>
  2. /deep/
  3. ::deep{}
  4. :deep()

比如你这样写了一段样式:

1
2
3
 .a :deep(.b) {
  color: green;
 }

上面的代码会被编译成:

1
2
3
4
 .a[data-v-9ea40744] .b {
    color: green;
 }

在编译后,在对应css样式上会带上该组件scoped对应的属性选择器,所以自然就能影响子组件的样式了。