element
popover 单例实现
vue
<template>
<el-popover :ref="`popover-${item.id}`" trigger="click">
<el-link type="primary" slot="reference" @click="getOrderItemDict(item)">cs</el-link>
</el-popover>
<template>
<script>
export default{
methods:{
getOrderItemDict(item){
Object.keys(this.$refs).forEach((key) => {
if (key.startsWith('popover-')) {
const currentRef=this.$refs[key];
if(Array.isArray(currentRef)){
currentRef.forEach((ref)=>{
ref.doClose();
})
}else{
currentRef.doClose();
}
}
});
}
}
}
</script>
ElementPlus
自定义命名空间
将原本的 el-xxx
类名全换成 命名空间-xxx
用处: 可以避免与其他应用冲突, 当例如微应用那种多应用存在与一个页面时, 格外好用 参考网站elementplus 官方
js
/**自定义变量*/
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': green,
),
),
);
js
// styles/element/index.scss
// 使用自定义命名空间ep代替el前缀
@forward 'element-plus/theme-chalk/src/mixins/config.scss' with (
$namespace: 'ep'
);
@use "~/styles/element/index.scss" as *;
// ...
js
<!-- App.vue -->
<template>
//使用el-config-provider包裹根组件,namespace属性的值需要与$namespace保持一致
<el-config-provider namespace="命名空间的名字,例如wl">
<!-- ... -->
</el-config-provider>
</template>
js
-->使项目中的el前缀替换为命名空间的插件
function changeHtmlClassPrefix(htmlString, oldPrefix, newPrefix) {
const regex = new RegExp(
`(class|style)\\s*:\\s*((["']((${oldPrefix}\\b)-).*["'])|((_normalizeClass|_normalizeStyle)\\(.*(${oldPrefix}\\b)-.*\\)))`,
'g'
)
return htmlString.replace(regex, (match, p1, offset, string) => {
return match.replace(oldPrefix, newPrefix)
})
}
function changeSelectorPrefix(cssString, oldPrefix, newPrefix) {
const regex = new RegExp(`(\\.${oldPrefix}\\b|\#${oldPrefix}\\b|\--${oldPrefix}\\b)`, 'g')
return cssString.replace(regex, (match, p1, offset, string) => {
return match.replace(oldPrefix, newPrefix)
})
}
export default function addScopedAndReplacePrefixPlugin({ prefixScoped, oldPrefix, newPrefix }) {
return {
name: 'addScopedAndReplacePrefix',
transform(code, id) {
if (!oldPrefix || !newPrefix) return
if (id.includes('node_modules')) return
const cssLangs = ['css', 'scss', 'less', 'stylus', 'styl']
let newCode = code
if (id.endsWith('.vue')) {
newCode = changeHtmlClassPrefix(newCode, oldPrefix, newPrefix)
}
// else if (id.includes('.vue') && id.includes('scoped')) {
else if (cssLangs.some((lang) => id.endsWith(`.${lang}`))) {
if (oldPrefix && newPrefix) {
newCode = changeSelectorPrefix(newCode, oldPrefix, newPrefix)
}
if (prefixScoped) {
newCode = `${newCode}${prefixScoped}{${newCode}}`
}
return newCode
}
return newCode
}
}
}
js
-->使用插件
import addScopedAndReplacePrefixPlugin from './plugins/addScopedAndReplacePrefixPlugin.js'
export default defineConfig((mode)=>{
plugins:[
vue(),
addScopedAndReplacePrefixPlugin({
prefixScoped: `div[data-qiankun='${QIANKUN_APP_NAME}']`,
oldPrefix: 'el',
newPrefix: '命名空间的名字,例如wl'
})
]
...,
})
全局注入第三方库 (例如 element-plus 的消息框)
js
//Element
-->使用方式
-->1.书写插件
function addHtmlCode (htmlString, code) {
return `${htmlString}${code}`
}
export default function addElementGlobalMacro () {
return {
transform (code, id) {
if (id.includes('node_modules')) return
let newCode = code
if (id.endsWith('.vue')) {
newCode = addHtmlCode('import {ElMessage,ElMessageBox,ElNotification} from \'element-plus\';', newCode)
return newCode
}
}
}
}
-->2.全局定义,以防报错
//global.d.ts文件中
declare global{
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const ElNotification: typeof import('element-plus/es')['ElNotification']
}
-->3.使用插件
import addElementGlobalMacro from './plugins/addElementGlobalMacro.js'
export default defineConfig((mode)=>{
plugins:[
vue(),
addElementGlobalMacro()
]
...,
})
ElMessage 样式失效
参考文章 当使用自动按需引入, 并且手动在组件内部 import 了 ElMesssge 时, ElMessage 样式会失效, 需要在组件内部不要引入,但会导致 eslint 报错, 此时需要将 ElMessage 声明为全局变量
js
//.eslintrc.js or eslintrc.cjs
module.exports = {
...,
//声明全局变量,避免ElMessage警告,defineEmits等也是同理
globals: {
ElMessage: true,
ElMessageBox: true
}
}
遇到的一些坑
超过 16 位的数值, 在转换为 number 时会有精度问题
css 错误
设置 flex-1 的元素被子元素撑开
当子元素超出flex-1的元素时,flex-1的元素会被撑开,需要给设置flex-1的元素设置overflow:hidden
其他
text
架构组件的思路和坑,怎么做的,有什么坑,怎么发布,文档怎么写
-->控制并发
控制并发就是将所有请求维护到一个queue中,当当前请求数量小于并发量时,
-->文件上传
拿到文件对象,通过它的size属性,slice分割为多块,一块块的加密上传,最终输出md5值用于比较这个文件上传过没,然后调用合并接口让后端把文件合并,
-->断点续传
断点续传就是请求失败后后端返回当前下标,下次继续从这里传和加密
-->权限管理
删除一个页面权限后,退出登录重新登录会报错,需要判断当前用户登录的路由是否在动态路由权限中,做法是将当前登录的路由,拿去跟动态权限路由递归比较
每次动态添加路由的时候,都需要router.matcher=new VueRouter().matcher
主题切换
定义 css 变量
所有使用 color 和 background-color 的地方用自定义变量定义
切换主题, 修改 css 变量的值
定义 css 变量, 并全局使用
javascript
:root {
--sw-green: #70c877;
--sw-orange: #e6a23c;
--sw-topo-animation: topo-dash 0.3s linear infinite;
}
html {
--el-color-primary: #409eff;
--theme-background: #fff;
--font-color: #3d444f;
--disabled-color: #ccc;
--dashboard-tool-bg: rgb(240 242 245);
--text-color-placeholder: #666;
--border-color: #dcdfe6;
--border-color-primary: #eee;
--layout-background: #f7f9fa;
--box-shadow-color: #ccc;
--sw-bg-color-overlay: #fff;
}
定义主题切换动画
javascript
<span class="ml-5" ref="themeSwitchRef">
<el-switch
v-model="theme"
:active-icon="Moon"
:inactive-icon="Sunny"
inline-prompt
@change="handleChangeTheme"
/>
</span>
function handleChangeTheme() {
const x = themeSwitchRef.value?.offsetLeft ?? 0;
const y = themeSwitchRef.value?.offsetTop ?? 0;
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
// 兼容处理
if (!document.startViewTransition) {
changeTheme();
return;
}
// api: https://developer.chrome.com/docs/web-platform/view-transitions
const transition = document.startViewTransition(() => {
changeTheme();
});
transition.ready.then(() => {
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`];
document.documentElement.animate(
{
clipPath: !theme.value ? clipPath.reverse() : clipPath,
},
{
duration: 500,
easing: "ease-in",
pseudoElement: !theme.value ? "::view-transition-old(root)" : "::view-transition-new(root)",
},
);
});
}
定义切换主题方法
javascript
enum Themes {
Dark = "dark",
Light = "light",
}
function changeTheme() {
//获取根节点,这里是html
const root = document.documentElement;
if (theme.value) {
root.classList.add(Themes.Dark);
root.classList.remove(Themes.Light);
} else {
root.classList.add(Themes.Light);
root.classList.remove(Themes.Dark);
}
window.localStorage.setItem("theme-is-dark", String(theme.value));
}
定义切换主题样式
javascript
html.dark {
--el-color-primary: #409eff;
--theme-background: #212224;
--font-color: #fafbfc;
--disabled-color: #999;
--dashboard-tool-bg: #000;
--text-color-placeholder: #ccc;
--border-color: #262629;
--border-color-primary: #4b4b52;
--layout-background: #000;
--box-shadow-color: #606266;
--sw-bg-color-overlay: #1d1e1f;
--sw-border-color-light: #414243;
--popper-hover-bg: rgb(64, 158, 255, 0.1);
--sw-icon-btn-bg: #222;
--sw-icon-btn-color: #ccc;
--sw-icon-btn-border: #999;
}
复制&大文件上传/下载/控制并发
- [[代码实现#复制函数]]
- [[代码实现#文件下载]]
- [[代码实现#大文件上传]]
- [[代码实现#控制请求并发]]
jsx 错误
h is not define
正常 jsx 写在组件内, 如 data, methods, render 中, 都不会报 h is not define,
但是当 jsx 语法放到外部 js 中引入会报错, 因为 jsx 需要关联组件上下文,否则无法解析 ( 编译的时候 h 函数在 vue 组件内, 如果放到外部 js, 外部文件 h 函数不存在, 会直接报错)
此时需要手动传入正确的 h 函数 (当前组件的 h 函数)
javascript
//参考config-form
//options.js 所有报错都因为jsx内的内容是从上下文中取的
// h is not define
//因为报h is not define是因为上下文中有h函数,因此,只需要上下文中有h函数即可
//方案1: import {h} form 'vue';
//方案2: 写mixins
//方案3: 传递h,在render中接收
// 提示组件未注册,因为上下文中没有显示这个组件
//方案1:全局注册,vue.component
//方案2:在configFrom实例上局部注册,实例.component
export const render= (formData,h) => {
return [<el-input v-model={formData.code} placeholder="请输入缓存编码" />];
}
//业务组件内
<template>
<confingForm :render="render" />
</template>
<script>
import {render} from './options';
export default{
render:render
}
</script>
//接收jsx render函数的组件内,假设config-form
export default{
render(h){
return [<div>
//代表这一行自定义渲染
const {render,formData} = this.$props;
if (render) {
return [render(formData,h)];
}else{
return [];
}
</div>];
}
}
el-select 无法回显
原因未知, 组件是真的垃圾
javascript
//在change事件中重新将options的arr覆盖就行
<el-select
v-model={formData.hospCode}
style={{ width: '100%' }}
onchange={() => this.bindOrgYuanqusArr.splice(0, 0)}>
{this.bindOrgYuanqusArr.map(item => {
return [<el-option key={item.hospCode} label={item.hospName} value={item.hospCode} />];
})}
</el-select>
vite 引入 svg
js
// vite.config.js
import path from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import svgLoader from 'vite-svg-loader'
export default defineConfig((mode) => {
return {
plugins:[
...,
/** 将 SVG 静态图转化为 Vue 组件 */
svgLoader({ defaultImport: 'url' }),
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(),'src/assets/svgs/svg')],
symbolId: 'icon-[dir]-[name]'
})
]
}
})
js
//svgIcon.vue
<script lang="ts" setup>
import { computed } from "vue"
interface Props {
prefix?: string
name: string
}
const props = withDefaults(defineProps<Props>(), {
prefix: "icon"
})
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
</script>
<template>
<svg class="svg-icon" aria-hidden="true">
<use :href="symbolId" />
</svg>
</template>
js
//main.js
import { type App } from "vue"
import SvgIcon from "@/components/SvgIcon.vue" // Svg Component
import "virtual:svg-icons-register"
app.component("SvgIcon", SvgIcon)
store 改造
js
//针对mutation重复代码问题,重构部分store,使state具有默认mutation
//传统mutation大部分都长这样
// setRouteData(state, newData) {
// state.routeData = newData;
// },
//最多就进行一下简单操作,比如 state.count+=1;
//针对这种问题,写如下代码
//首字母大写功能函数
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
//使所有state都具有默认修改mutation
function nomarlMutationHandler(names) {
const res = {};
for (const name of names) {
res['set' + capitalizeFirstLetter(name)] = function(state, newData) {
if (typeof newData === 'function') {
newData(state);
return;
}
state[name] = newData;
};
}
return res;
}
export default function() {
return {
...nomarlMutationHandler(Object.keys(state))
};
}
权限管理
js
//router.options一般是VueRouter(options)的options
//缓存静态路由
const staticRoutes = router.options.routes;
const addRoutes=(routes=[],parentPath = '')=>{
//还原初始化的静态路由
router.options.routes = staticRoutes;
routes.forEach(item => {
if(item.children){
addRoutes(item.children,route.path + '/');
}else{
item.path=parentPath+item.path
router.addRoute(item);
}
});
}
const initRoutes=(router)=>{
const newRouter = new VueRouter();
router.matcher = newRouter.matcher;
}
//例如现在退出登录
initRoutes(router);
//例如现在登录拿到了动态路由asyncRoutes
addRoutes(asyncRoutes);
//route的来源来自于路由守卫,路由守卫中判断,在跳转登录页时,缓存from路由信息作为route
let route={};
router.beforeEach(form,to,next)=>{
if(to.path==='\login'){
route=form||{};
}
return hasPermission(router,to)?next():next('/login');
}
//登录完毕后如果需要重定向到之前的页面,需要判断是否还存在权限
const hasPermission=(router,route)=>{
const routes=router.matcher.options.routes; //3.0x
//const routes=router.getRoutes() //4.0x
const hasRoute=(item,route)=>{
//路由匹配规则
const matchRule=item.path===route.path||item.name===route.name;
//如果有子路由,则匹配子路由和当前路由,否则只匹配当前路由
return item.children?item.children.find(el=>hasRoute(el,route)||matchRule:matchRule;
}
return routes.find(el=>hasRoute(el,route));
}
hasPermission(router,route);
//可以在路由守卫中通过next跳转,也可以通过router.replace跳转
登录流程
javascript
//密码加密
前端将用户名和md5加密后的密码传给后端,
后端与数据库的账号密码对比(后端存的加密后的密码),通过后返回token,
前端将token存在本地,每次请求时在请求头携带token,
//token失效
过期后后端返回401或者前端本地存储时存个时间戳,一定时间后算过期,过期了前端路由守卫跳转登录页,登录请求新的token
//无感刷新
基于登录流程,在token的基础上加一个refreshToken,当token过期返回401时,在响应拦截器中用数组将未请求到新token过程中过期的请求存储起来,用refreshToken请求到新token和新refreshToken后,重新请求
注意,这会导致无限刷新token。除非用户长时间未登录,refreshToken过期了
token比refreshToken过期的早
[[代码实现#文件预览(pdf/图片等)]]
性能优化
(具体看前端详细点的性能优化)
javascript
代码优化
尽量减少 data 中的数据,data 中的数据都会增加 getter 和 setter,会收集对应的 watcher
v-if 和 v-for 不能连用
如果需要使用 v-for 给每项元素绑定事件时使用事件代理
SPA 页面采用 keep-alive 缓存组件
在更多的情况下,使用 v-if 替代 v-show
key 保证唯一
requestAnimationFrame处理频繁渲染,避免卡顿
webWorker 开启单独线程处理时间很长的代码
SEO 优化
采用http2代替http1.1 (可管道复用,请求头压缩,响应时额外推送信息)
预渲染 (使用async/defer script提前加载js)
服务端渲染 SSR
使用缓存(客户端缓存、服务端缓存)
使用路由懒加载、异步组件
防抖、节流
第三方模块按需导入
长列表滚动到可视区域动态加载
图片懒加载
打包优化
压缩代码,服务端开启 gzip 压缩等。
Tree Shaking/Scope Hoisting
使用 cdn 加载第三方模块
多线程打包 HappyPack开启多线程打包
splitChunks 抽离公共文件
sourceMap 优化
用户体验
骨架屏
PWA
动态注册组件
动态注册的缺点是点击组件跳转的功能将失效 官方文档对import.glob的解释
js
import { deepClone } from '@/utils';
export function getComponents (componentFiles, isToUpperCase = false) {
const components = {}
Object.keys(componentFiles).forEach(modulePath => {
const paths = modulePath.split('/')
if (isToUpperCase) {
let name = paths.pop().split('.')[0].toUpperCase()
if (name == 'INDEX') name = paths.pop().split('.')[0].toUpperCase()
components[name] = componentFiles[modulePath]
} else {
let name = paths.pop().split('.')[0]
if (name == 'index') name = paths.pop().split('.')[0]
components[name] = componentFiles[modulePath]
}
})
const componentNames = Object.keys(components)
components.install = function (app) {
componentNames.forEach(name => {
app.component(name, components[name])
})
}
return components
}
/**
* 获取./component目录下的所有组件,命名需要小驼峰
* componentFiles.keys() 获取../components/ 路径下所有文件的文件路径组成的数组,eg:["./wlCheckbox/index.vue","./wlCheckboxGroup/index.vue"]
*///动态注册的缺点是 点击组件跳转的功能将失效
//webpack下
const componentFiles = require.context('./components/', true, /\.vue$/);
const components = componentFiles.keys().reduce((modules, modulePath) => {
const value = componentFiles(modulePath).default;
if (value) modules[modulePath] = value;
return modules;
}, {});
//vite下
// 根据官方文档说明,import.meta.glob方式匹配到的文件默认是懒加载的,通过动态导入实现,并会在构建时分离为独立的chunk。
// 如果直接import.meta.glob("./components/**/*.vue")获取结构为:
// const modulesFiles = { // './src/foo.vue': () => import('./src/foo.vue'), // './src/bar.vue': () => import('./src/bar.vue') // }
// 如果要直接引入所有的模块,传入{ eager: true }
// {import:'default'} value取值为 模块 default空间的内容,即export default内容
// const components = import.meta.glob("./components/**/*.vue", { import: 'default', eager: true});
//!用来排除某些文件
// const componentFiles = import.meta.glob(["./*.vue",'./**/index.vue', '!./index.vue'], { import:'default', eager: true });
export default getComponents(components);
组件发布流程
- 为组件指定入口文件:在每个包下的 package. Json 文件中添加 main (require 命令的入口) 和 module (import 命令的入口) 属性,指定组件的入口文件。
- 设置环境变量:利用工具(如 cross-env)设置环境变量,区分开发环境和生产环境。
- 添加打包配置文件:在项目的根目录下添加构建配置文件(如 rollup. Config. Js),配置构建和打包的相关参数。
- Npm config set registry=私库地址/
- 开发组件
- Npm publish npm 会根据 package. Json 文件中的 files 字段来决定哪些文件应该被包含在发布的包中。如果
files
字段不存在,npm 会默认包含除了被.npmignore
或.gitignore
文件排除之外的所有文件。
优化
javascript
//造成首屏缓慢的点分为I/O阻塞,DOM渲染和性能
//I/O阻塞(分为减少包体积,请求优化,资源懒加载)
//减少包体积
JavaScript:UglifyjsPlugin
CSS :MiniCssExtractPlugin(提取css为单独文件) OptimizeCSSAssetsPlugin(压缩css)
HTML:HtmlWebpackPlugin
gzip:开启 gzip 压缩(一种压缩算法,减少传输的资源量),通常开启 gzip 压缩能够有效的缩小传输资源的大小。
压缩图片:可以使用 image-webpack-loader,在用户肉眼分辨不清的情况下一定程度上压缩图片
树摇
使用 svg 图标:相对于用一张图片来表示图标,svg 拥有更好的图片质量,体积更小,并且不需要开启额外的 http 请求
合理使用第三方库:对于一些第三方 ui 框架、类库,尽量使用按需加载,减少打包体积
//请求优化
请求优化:将第三方的类库放到 CDN 上,能够大幅度减少生产环境中的项目体积,另外 CDN 能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
缓存:将长时间不会改变的第三方类库或者静态资源设置为强缓存,将 max-age 设置为一 个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源,好的缓存策略有助于减轻服务器的压力,并且显著的提升用户的体验
http2:如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同域名的 tcp 连接数量是有限制的(chrome 为 6 个)超过规定数量的 tcp 连接,则必须要等到之前的请求收到响应后才能继续发送,而 http2 则可以在多个 tcp 连接中并发多个请求没有限制,在一些网络较差的环境开启 http2 性能提升尤为明显。
//资源懒加载
懒加载:当 url 匹配到相应的路径时,通过 import 动态加载页面组件,这样首屏的代码量会大幅减少,webpack 会把动态加载的页面组件分离成单独的一个 chunk.js 文件
图片懒加载:使用图片懒加载可以优化同一时间减少 http 请求开销,避免显示图片导致的画面抖动,提高用户体验
//DOM渲染
虚拟列表,图片懒加载等各种懒加载
//代码性能优化
预渲染:由于浏览器在渲染出页面之前,需要先加载和解析相应的 html、css 和 js 文件,为此会有一段白屏的时间,可以添加loading,或者骨架屏幕尽可能的减少白屏对用户的影响体积优化
使用可视化工具分析打包后的模块体积:webpack-bundle- analyzer 这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化
提高代码使用率:利用代码分割,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程
封装:构建良好的项目架构,按照项目需求就行全局组件,插件,过滤器,指令,utils 等做一 些公共封装,可以有效减少我们的代码量,而且更容易维护资源优化