项目技术栈
vue3 js OA 后台管理类型的项目
场景
代码发布到生产环境后,测试反馈某些页面的图标样式大小错乱,具体表现为:
- 首次加载进入到某个使用 SVG 组件的页面内,样式一切正常显示
- 点击左侧菜单跳转进入其他页面后,然后点击回退返回前一个页面发现 SVG 子组件的大小错乱
- 所有应用 svg 组件的页面都有这种问题
- 仅生产环境有此问题,测试环境和本地代码无问题
问题代码展示
- 有问题的子组件 SvgIcon
xml
<template>
<svg :class="svgClass">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
export default {
name: "SvgIcon",
props: {
iconClass: {
type: String,
required: true,
},
className: {
type: String,
default: "",
},
size: {
type: Number,
default: 16,
},
},
computed: {
iconName() {
return `#icon-${this.iconClass}`;
},
svgClass() {
if (this.className) {
return "svg-icon " + this.className;
} else {
return "svg-icon";
}
},
},
};
</script>
<style lang="less" scoped>
@icon-bg: #1890ff;
.svg-icon {
width: 1.5em;
height: 1.5em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
margin: 0 5px;
cursor: pointer;
}
</style>
- 使用子组件的父组件(关键代码节选,并非完整页面)
DOM:
xml
<template>
<div class="mainpage">
...
<SvgIcon v-if="record.selfStatus === 0" className="statusIcon lineMiddle" icon-class="submit-waitSubmit"></SvgIcon>
<SvgIcon v-if="record.selfStatus === 1" className="statusIcon lineMiddle" icon-class="submit-success"></SvgIcon>
<SvgIcon v-if="record.selfStatus === 2" className="statusIcon lineMiddle" icon-class="submit-waitEdit"></SvgIcon>
...
</div>
</template>
CSS:
xml
<style scoped>
.mainpage {
width: 100%;
height: 100%;
}
.statusIcon {
width: 12px;
height: 12px;
display: inline-block;
vertical-align: top;
margin-right: 6px;
margin-left: 0;
}
</style>
问题排查
- 关于样式错乱,直接体现为父组件定义了12像素宽高,但是在错乱之后,控制台显示12像素被子组件原本定义的宽高1.5em覆盖。
- 最开始我是直接通过class 绑定在 SvgIcon标签上,怀疑父子组件样式权重问题,因此通过className props传入父组件的class,保证是处于子组件绑定的class后面,但无效。
- 为什么生产环境才有这种毛病,为什么子组件的样式始终在父组件后面
- 在本地设置环境为production,然后打包代码后发现,打包会有多个chunk文件,而非production环境的打包只有一个chunk
- 在生产环境下切换页面,然后查看控制台发现,当进入且仅第一次某个页面后,会在html头部的尾部添加其样式引入文件,而非production环境下没有这种引入的过程
- scoped本来会添加唯一标识的,但SvgIcon具有两个data-v标识,一个是它自己在所有页面共用的一种标识,一个是父组件渗透下来的标识。而子组件选择器样式,和父组件定义的样式层级同级,造成二者权重相冲,也就是说,谁后声明谁就牛逼。而切换页面后,这个子组件定义的宽高样式跑到了父组件定义的样式之后,冲掉了当前页面本该展示的模样。
反思和知识点
- 关于样式scoped
- scoped样式的data-v会给页面的所有dom节点加上,而子组件自身由于也使用了scoped,导入的过程中又会有一个在各父组件里固定的data-v,因此在这个意义上,子组件data-v并不是"唯一"的,会有两个。而只有页面的根节点(即我例子所示最外层容器mainpage),它只具备一个data-v且唯一,也就是说,如果我们页面的所有选择器都挂在这个页面选择器之后,可以保证其样式选择器是有唯一性的,不会被其他data-v选到同一个元素
- 在本次踩坑之前,我认为scoped绑定的style全都是唯一的,样式也就写的比较随意,没有考虑非要放在mainpage里
- 关于css打包 本次踩坑,包括现在我都有一些疑问,关于production环境下的打包会生成多个chunk文件,而其他我自己定义的环境只有一个;每次切换不同的页面,production环境会加载对应的样式,而其他环境则像是打包完一开始全部加载完毕。
- 在vue.config.js里有一个相关的css配置,此前没有去了解
css
css: {
loaderOptions: {
less: {
lessOptions: {
modifyVars: {
'primary-color': '#3C53FF',
},
javascriptEnabled: true,
},
},
},
// extract: true
},
这里的extract是我后来配置的,默认情况下这个配置在判断当前env为production的情况下,它是true,其他环境是false.因此产生差异。如果自己主动配置为true,那么任何环境都会打包出css,而不是内联打包到js里面
最终解决方案
按照反思1中所提,将父组件内所有样式全部写在mainpage,即最外层容器class下面.可保证样式选择器前额外带上父组件的classname,权重会大于子组件选择器
xml
<style lang='less' scoped>
.mainpage {
width: 100%;
height: 100%;
.statusIcon {
width: 12px;
height: 12px;
display: inline-block;
vertical-align: top;
margin-right: 6px;
margin-left: 0;
}
}
</style>
如果不想改动css写法,也可以考虑按照反思2的点将vueconfig.js里extract设置为false,这样他就不会打包出css,直接内联打包到js脚本里,这样的话也可以保证顺序,子组件样式先生成,而后父组件样式覆盖子组件。
但这种方式感觉是破坏了他逐步加载css的初衷,我最后没有采用
css
css: {
loaderOptions: {
less: {
lessOptions: {
modifyVars: {
'primary-color': '#3C53FF',
},
javascriptEnabled: true,
},
},
},
extract: false
},