Skip to content

a

🕒 Published at:

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);

组件发布流程

  1. 为组件指定入口文件:在每个包下的 package. Json 文件中添加 main (require 命令的入口) 和 module (import 命令的入口) 属性,指定组件的入口文件。
  2. 设置环境变量:利用工具(如 cross-env)设置环境变量,区分开发环境和生产环境。
  3. 添加打包配置文件:在项目的根目录下添加构建配置文件(如 rollup. Config. Js),配置构建和打包的相关参数。
  4. Npm config set registry=私库地址/
  5. 开发组件
  6. Npm publish npm 会根据 package. Json 文件中的 files 字段来决定哪些文件应该被包含在发布的包中。如果 files 字段不存在,npm 会默认包含除了被 .npmignore.gitignore 文件排除之外的所有文件。

优化

javascript
//造成首屏缓慢的点分为I/O阻塞,DOM渲染和性能

//I/O阻塞(分为减少包体积,请求优化,资源懒加载)
//减少包体积
    JavaScriptUglifyjsPlugin 
    CSSMiniCssExtractPlugin(提取css为单独文件) OptimizeCSSAssetsPlugin(压缩css)
    HTMLHtmlWebpackPlugin
    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渲染
    虚拟列表,图片懒加载等各种懒加载

//代码性能优化
    预渲染由于浏览器在渲染出页面之前需要先加载和解析相应的 htmlcss js 文件为此会有一段白屏的时间可以添加loading或者骨架屏幕尽可能的减少白屏对用户的影响体积优化
    使用可视化工具分析打包后的模块体积webpack-bundle- analyzer 这个插件在每次打包后能够更加直观的分析打包后模块的体积再对其中比较大的模块进行优化
    提高代码使用率利用代码分割将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程
    封装构建良好的项目架构按照项目需求就行全局组件插件过滤器指令utils 等做一 些公共封装可以有效减少我们的代码量而且更容易维护资源优化