按需导入
环境
1 | "@vue/cli": "5.0.8", |
element-plus
vue.config.js
1 | const { defineConfig } = require('@vue/cli-service') |
main.js(导入样式)
1 | import { createApp } from 'vue' |
App.vue (导入语言配置)
1 | <template> |
重新启动 npm run serve
后,就可以直接使用 element-plus 的组件了。但是图标还需要特殊处理下,改成下面形式,即加上 iEp
或者 i-ep-
,详见:https://icon-sets.iconify.design/ep/
1 | <el-icon><iEpUser/></el-icon>或者<el-icon><i-ep-user/></el-icon> |
vue-echarts
其官方有提供一个自动生成按需引入的生成器,地址后面补上,应该在官方文档里面可以找到:https://vue-echarts.dev/
1 | <template> |
setup组件命名
1 | const { default: vueSetupExtendPlus } = require('unplugin-vue-setup-extend-plus/webpack') |
使用
1 | <template> |
忽略报错提示
1 | module.exports = defineConfig({ |
深度选择器不生效问题
1 | <template> |
上面样式不生效,查看控制台发现其生成代码如下
1 | .el-dialog[data-v-07a48dde] .el-dialog__headerbtn { ... } |
data-v-07a48dde 并没有生成到DOM里面,导致无法选中,给template下面加一个根元素问题就可以解决了。造成该问题是v-for生成了样式作用域,但是vue3的template最外层是不带data-v-hash的。
打包后部分scoped样式不生效
https://www.zhihu.com/question/608472633
在一个vue组件内,存在不同的样式作用域(data-v-hash),目前通过禁用多线程解决,具体原因未知
1 | // vue.config.js |
element-plus 问题
element-plus的v-loading中element-loading-spinner使用el-icon里面的图标不生效?
该问题注释掉 element-loading-spinner 即可正常,暂未深究是否是element-plus的bug
element-plus el-pagination 如果 :total 是 null 会提示使用了废弃语法
我的组件封装
svg图标组件
由于之前遇到过 Iconfont 好几天无法访问,且每次添加图标都需要下载一堆的字体和样式文件提交Git导致不容易看出更新了哪个图标。因此考虑研究下 Iconfont 是如何实现图标展示的,根据下载下来的demo.index.html文件可以看到主要有3种使用方式,其中 Font class(兼容性好) 和 Symbol(支持彩色图标),关于这2种方式其实现原理在官网也有介绍:iconfont 字体生成原理及使用技巧 、 未来必热:SVG Sprites技术介绍。
看完之后通过字体的方式本地化图标库感觉没有啥解决方法,但是通过 SVG Sprites 则可以完美达到我希望图标本地化和Git提交干净整洁的目标,因此通过webpack和vue进行组件封装,以便达到更贴切的Iconfont的图标使用方式。(本质上就是在页面中直接放SVG,注意:兼容性较差,支持 IE9+,及现代浏览器)
创建
svgIcon
组件在
src/components/svgIcon
目录下创建一个名为svgIcon.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<template>
<svg class="svg-icon" aria-hidden="true">
<use :xlink:href="iconName"></use>
</svg>
</template>
<script setup>
import { computed, defineProps } from 'vue'
const props = defineProps({
name: {
type: String,
required: true
}
})
const iconName = computed(_ => `#icon-${props.name}`)
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>加载所有 svg 文件
在 ``src/components/svgIcon/index.js
svgIcon
组件,并自动加载./icons
目录下的所有 svg 文件1
2
3
4
5
6
7
8import svgIcon from './svgIcon.vue'
const requireContext = require.context('./icons', true, /\.svg$/)
requireContext.keys().forEach(requireContext)
export default (app) => {
return app.component(svgIcon.name, svgIcon)
}全局注册
svgIcon
组件在 ``src/main.js
svgIcon
组件1
2
3
4
5
6import { createApp } from 'vue'
import useSvgIcon from '@/components/svgIcon'
const app = createApp(App)
useSvgIcon(app).mount('#app')使用 svg-sprite-loader 解析 svg 文件
安装
svg-sprite-loader
后,在vue.config.js
中添加 svg loader 解析1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// svg-sprite-loader@6.0.11
// npm install -D svg-sprite-loader
module.exports = defineConfig({
...
chainWebpack(config) {
config.module.rules.delete('svg')
config.module.rule('svg')
.test(/\.svg$/)
.include.add(resolve('src/components/svgIcon/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({ symbolId: 'icon-[name]' })
.end()
},
...
})在 ``src/components/svgIcon/icons` 目录添加一个 sentry.svg 文件后,页面即可使用了
sentry.svg
1
<svg t="1719649375124" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1580" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1009.685333 920.405333c19.157333-31.914667 19.157333-63.829333 0-95.744l-414.72-720.938666c-19.072-31.872-50.986667-44.714667-82.901333-44.714667s-63.786667 19.157333-82.901333 44.714667L295.146667 339.754667l31.914666 19.157333a632.064 632.064 0 0 1 242.432 242.432c51.072 89.344 82.944 185.002667 89.344 287.146667h-95.701333a568.106667 568.106667 0 0 0-76.544-242.517334c-44.672-89.301333-114.858667-159.488-204.16-210.56l-31.957333-19.072-127.573334 216.917334 31.872 19.157333c82.944 50.986667 140.373333 133.930667 153.130667 229.674667H97.344c-6.357333 0-12.8-6.4-12.8-6.4s-6.314667-6.4 0-12.757334l57.514667-102.101333c-19.157333-19.072-44.714667-31.914667-70.229334-38.229333L14.4 824.704c-19.157333 31.914667-19.157333 63.829333 0 95.744 19.114667 31.829333 44.629333 44.586667 82.901333 44.586667h293.504v-38.229334c0-70.186667-19.157333-134.016-51.029333-197.845333-25.557333-50.986667-63.829333-89.301333-108.458667-121.173333L275.989333 524.8c57.472 44.629333 108.501333 95.701333 146.730667 159.530667 44.672 76.501333 63.829333 159.488 63.829333 242.432v38.229333h248.789334v-38.229333c0-127.573333-31.872-255.232-102.058667-370.048-51.072-102.144-134.016-185.045333-229.76-248.874667L499.264 141.994667c6.442667-6.4 12.8-6.4 12.8-6.4 6.4 0 6.4 0 12.757333 6.4l414.762667 720.896c6.314667 6.314667 0 12.757333 0 12.757333s-6.4 6.4-12.8 6.4h-95.701333v76.586667h95.701333c38.229333 6.357333 63.786667-6.4 82.901333-38.229334z" p-id="1581"></path></svg>
index.vue
1
<svg-icon name="sentry" style="color: red; font-size: 20px;"></svg-icon>
函数式弹窗
vue3
1 | <!-- src\components\DynamicDialog\DynamicDialog.vue --> |
1 | // src\components\DynamicDialog\index.js |
1 | <!-- src\views\screen_layer_1\FsdaMark\index.vue --> |
vue2
1 | <!-- src\components\DynamicDialog\DynamicDialog.vue --> |
1 | // src\components\DynamicDialog\index.js |
1 | <!-- src\views\TableCurd\index.vue --> |
组件级权限控制
1 | <template> |
使用
1 | <Authority permissions="user.signup"> |
我的最佳实践
页面拆分
通过在 views 文件夹下目录作为 路由页面,同级目录下的其他文件作为该页面的模块,达到减少页面代码同时不暴露到component公共模块文件夹的目的
如存在一个登录页面,其有登录、注册、隐私协议、忘记密码、找回密码等功能,目录结构如下
1 | ---views |
样式拆分
通过组件拆分把样式拆分出去
通过 @import 拆分
@import 只能放到style的最外层,同时因为其是由浏览器加载因此不能scoped(scoped依赖webpack的postcss进行处理)
如果需要scoped则可如下处理(缺点是不能统一给文件里面的样式增加父级)
1
<style scoped src="../static/css/user.css"></style>
BEM命名规范
B代表Block,E代表Element,M代表Modifier
其代表是 element-plus 框架,如 el-dialog、el-dialog__header、el-dialog__body、el-dialog__close
简单来说,就是元素通过 __ 连接,修饰符通过 – 连接。为什么用2个符号是为了方便自己编写连字符或者下划线的元素/块。
1 | .block {} |
块(Block):是一个独立的功能块,代表一个高层级的组件。
其命名是描述块的用途(是什么,如
menu、button、el-menu
),而不是它的状态(怎么样,red、big、top
)命名方式:
block-name
示例:
.button
,.header
元素(Element):是块的组成部分,不能单独存在。
命名方式:
block-name__element-name
示例:
.button__icon
,.header__logo
一个元素应始终是其块的组成部分,而不是其他元素的。这意味着元素命名不应被定义成类似层级如
block-elm1-elm2
修饰符(Modifier):是块或元素的变体,用于表示不同的状态或外观。
命名方式:
block-name--modifier-name
或block-name__element-name--modifier-name
示例:
.button--primary
,.header__logo--small
在当前流行的 Vue.js
/ React
/ Angular
等前端框架中,都有 CSS 组件级作用域的编译实现,当你选择了这种局部作用域的写法时,在较小的组件中,BEM 格式可能显得没那么重要。但对于公共的、全局性的模块样式定义,还是推荐使用 BEM 格式。
另外,对于对外发布的公共组件来说,一般为了风格的可定制性,都不会使用这种局部作用域方式来定义组件样式。此时使用 BEM 格式也会大显其彩。
简单来说,如果不是局部作用域样式推荐使用BEM。如果需要使用BEM可以借助CSS预处理器使书写简单化
1 | .article { |
vue2切换到vue3遇到问题
<router-view> can no longer be used directly inside <transition> or <keep-alive>.
1
2
3
4
5
6
7
8
9<Transition>
<router-view/>
</Transition>
<!-- 在vue-router最新版本不支持上面写法,需要改成(如果有keep-alive可放到component标签上层) -->
<router-view v-slot="{ Component }">
<transition>
<component :is="Component" />
</transition>
</router-view>Extraneous non-emits event listeners (close) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. If the listener is intended to be a component custom event listener only, declare it using the "emits" option.
vue3里面如果父组件监听了一个事件(
close
),子组件必须声明它,即使没有使用到,否则就会出现上面警告
遇到问题
在2个页面中引用了同名的vue组件出现冲突问题
1
2
3
4
5
6
7
8
9
10
11// views/a.vue
import TestItem from '@/components/a/TestItem.vue'
export default {
components: { TestItem },
}
// views/b.vue
import TestItem from '@/components/b/TestItem.vue'
export default {
components: { TestItem },
}
代码如上,在 vue-cli 的 vue3 环境中就会出现一个组件覆盖另外一个组件的情况(如果两个都使用 script setup 则不会,如果一个script setup,另外一个和示例一样还是会存在覆盖),目前未测试在 vue2 环境是否存在该问题。
目前是通过文件重命名解决,希望有更好的解决方法。
keep-alive无法缓存一个公共组件问题
场景:有的时候我们的路由是动态路由,其页面组件相同,只有页面参数的差异,正常会通过 vue-router 的动态路由实现。但是由于历史原因,前端并没有引入vue-router,仅仅通过 component 动态加载组件实现切换页面(代码如下)。但是在复用组件时,keep-alive则无法缓存复用的组件状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<template>
<el-main>
<keep-alive :max="10">
<component :is="currentComponent" v-bind="currentProps"></component>
</keep-alive>
</el-main>
</template>
<script>
export default {
data() {
return {
currentComponent: "",
currentProps: {}
}
},
methods: {
routerTo(name, params = {}) {
this.currentComponent = () => import("@/views/" + name)
this.currentProps = params
}
}
}
</script>由于翻阅官方文档并没有找到解决方案,但是之前使用 vue-router 的确是支持这种场景的(上面的 component 组件换成 router-view 组件),因此这里记录下解决方案(解决方案是查看keep-alive源码实现发现的,详见:https://github.com/vuejs/vue/blob/73486cb5f5862a443b42c2aff68b82320218cbcd/src/core/components/keep-alive.ts#L148)
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<template>
<el-main>
<keep-alive :max="10">
<component :is="currentComponent" :key="currentKey" v-bind="currentProps"></component>
</keep-alive>
</el-main>
</template>
<script>
export default {
data() {
return {
currentComponent: "",
currentKey: "",
currentProps: {}
}
},
methods: {
routerTo(name, params = {}) {
this.currentComponent = () => import("@/views/" + name)
this.currentProps = params
this.currentKey = `${name}-${JSON.stringify(params)}` // 这里只是举例,实际情况应该取params的“主键”
}
}
}
</script>PS:值得注意的是,根据源码看,vnode.key 只在 include 和 exinclude 不匹配的时候才会生效,不太清楚为什么要如此设计,因此要注意如果配置了 include 不能包含 currentKey, 如果设置了 exinclude 必须包含 currentKey。
ref对象中动态添加的对象没有响应式
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
43
44<template>
<el-tree :data="treeNodeData" show-checkbox node-key="id"
:default-expanded-keys="expandedKeys" :default-checked-keys="checkedKeys"
ref="treeRef" :expand-on-click-node="false">
<template #default="{ data }">
<span class="custom-tree-node">
<div style="display: inline-block">{{ data.layerCn }}</div>
</span>
</template>
</el-tree>
</template>
<script>
import { onMounted, ref, nextTick, reactive } from 'vue'
const warningExceedNode = reactive({ id: -999, layerName: 'warning', layerCn: '超阈值提示', layerFormat: "warningMap", children: [] })
function _drayWarningInfor(map, warnVal, layerCn) {
// drayWarningInfor(map, warnVal)
if (warningExceedNode.children.some(item => item.layerCn === layerCn)) return // 添加过了不再添加
const layerName = Object.keys(warnVal).join(',')
const id = -999 + warningExceedNode.children.length + 1
warningExceedNode.children.push({ id, layerName, layerCn, layerFormat: "warningMap" })
checkedKeys.value.push(id)
nextTick(() => {
treeRef.value.setCheckedKeys(checkedKeys.value);
})
}
const treeNodeData = ref([])
const checkedKeys = ref([])
const treeRef = ref(null)
treeNodeData.value.push(warningExceedNode)
const map = null
setTimeout(_ => {
_drayWarningInfor(map, { riverAlert: [] }, '河道')
}, 1000)
setTimeout(_ => {
_drayWarningInfor(map, { rvsrAlert: [] }, '水库')
}, 1500)
setTimeout(_ => {
_drayWarningInfor(map, { difangAlert: [] }, '提防')
}, 2000)
</script>上面代码如果不添加 nextTick 则 checked 只能选中第一个,如果不添加 reactive 则不能绑定成功
参考文章
本文链接: http://www.ionluo.cn/blog/posts/90a7d65a.html
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!