前端开发常用技巧
1. css实现多行文本展开效果
css多行文本展开收起 = 控制按钮文字环绕效果 + 多行文本溢出省略 + 展开、收起状态切换
难点
-
如何实现展开和收起切换按钮的文字环绕效果
-
如何实现多行文本溢出省略效果
-
如何实现展开和收起的状态or文字切换
按钮文字环绕
居右:float: right;
居下:.text::before
浮动后高度塌陷(解决方法:在元素外边包一层具有包裹性又具有定位特性的标签)
<div class="content">
<div class="text">
<label class="btn">展开</label>
<span>
但听得蹄声如雷,十余乘马疾风般卷上山来。马上乘客一色都是玄色薄毡大氅,
里面玄色布衣,但见人似虎,马如龙,人既矫捷,马亦雄骏,每一匹马都是高头
长腿,通体黑毛,奔到近处,群雄眼前一亮,金光闪闪,却见每匹马的蹄铁竟然
是黄金打就。来者一共是一十九骑,人数虽不甚多,气势之壮,却似有如千军万
马一般,前面一十八骑奔到近处,拉马向两旁一分,最后一骑从中驰出</span>
</div>
</div>
<style>
.content {
display: flex;
}
.text {
width: 475px;
border: aqua solid 1px;
color: #333;
font-size: 14px;
}
.text::before {
content: '';
float: right;
height: 100%;
margin-bottom: -20px;
}
.btn {
color: dodgerblue;
cursor: pointer;
/* 控制环绕 */
float: right;
clear: both;
margin-right: 8px;
}
</style>
多行文本溢出省略
-webkit-line-clamp: 3
(用来限制在一个块元素显示的文本的行数, 2 表示最多显示 2 行。为了实现该效果,它需要组合其他的WebKit属性)
display: -webkit-box
(和 1 结合使用,将对象作为弹性伸缩盒子模型显示)
-webkit-box-orient: vertical
(和 1 结合使用 ,设置或检索伸缩盒对象的子元素的排列方式)
overflow: hidden
(文本溢出限定的宽度就隐藏内容)
text-overflow: ellipsis
(多行文本的情况下,用省略号“…”隐藏溢出范围的文本)
.text {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
如何实现展开和收起状态切换
使用input type=checkbox
控制展开与收起效果
<input type="checkbox" id="exp" />
<style>
#exp {
visibility: hidden;
}
#exp:checked+.text {
-webkit-line-clamp: 999; /*设置一个足够大的行数就可以了*/
}
.btn::after {
content: '展开';
}
#exp:checked+.text .btn::after {
content: '收起';
}
</style>
最终效果
<style>
.content {
display: flex;
}
.text {
width: 475px;
border: aqua solid 1px;
color: #333;
font-size: 14px;
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.text::before {
content: "";
float: right;
height: 100%;
margin-bottom: -20px;
}
.btn {
color: dodgerblue;
cursor: pointer;
/* 控制环绕*/
float: right;
clear: both;
margin-right: 8px;
}
#exp {
visibility: hidden;
}
#exp:checked + .text {
-webkit-line-clamp: 999; /*设置一个足够大的行数就可以了*/
}
.btn::after {
content: "展开";
}
#exp:checked + .text .btn::after {
content: "收起";
}
</style>
</head>
<body>
<div class="content">
<input type="checkbox" id="exp" />
<div class="text">
<label class="btn" for="exp"></label>
<span>
但听得蹄声如雷,十余乘马疾风般卷上山来。马上乘客一色都是玄色薄毡大氅,
里面玄色布衣,但见人似虎,马如龙,人既矫捷,马亦雄骏,每一匹马都是高头
长腿,通体黑毛,奔到近处,群雄眼前一亮,金光闪闪,却见每匹马的蹄铁竟然
是黄金打就。来者一共是一十九骑,人数虽不甚多,气势之壮,却似有如千军万
马一般,前面一十八骑奔到近处,拉马向两旁一分,最后一骑从中驰出</span
>
</div>
</div>
</body>
2. css绘制三角形
实心三角形 + 带边缘色三角形 + 圆角三角形
实心三角形
正方形 + 加大border值 + 设置宽高为0 + 保留唯一颜色
<div class="filled-triangle"></div>
<style>
.filled-triangle {
width: 0;
height: 0;
border: 50px solid transparent;
border-bottom: 50px solid cyan;
}
</style>
气泡聊天框
圆角矩形 + 加尖脚三角形
<div class="margin-triangle">hi, sister</div>
<style>
.margin-triangle {
position: relative;
width: 300px;
height: 60px;
padding: 10px;
border: 1px solid cyan;
border-radius: 8px;
}
/* 深色三角形 */
.margin-triangle::before {
position: absolute;
top: 34px;
left: -10px;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 10px solid cyan;
content: '';
}
/* 画一个白色的三角形盖上去,错位 2 个像素 */
.margin-triangle::after{
position: absolute;
top: 34px;
left: -8px;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 10px solid #fff;
content: '';
}
</style>
3. new Map()的使用
在开发过程中,涉及到数据结构,能使用Map不适应Array,尤其是复杂的数据结构。
如果对于数组的存储考虑唯一性,使用Set。
Map和Array的对比
let map = new Map()
let arr = new Array()
// 增
map.set('a', 1)
arr.push({'a': 1})
// 查
map.has('a')
arr.find(item => item.a)
// 改
map.set('a', 2);
arr.forEach(item=> item.a?item.a=2:'')
// 删
map.delete('a')
arr.splice(arr.findIndex(item=>item.a),1)
set map object 对比
let item = {a: 1};
let set = new Set()
let map = new Map()
let obj = new Object()
// 增
set.add(item)
map.set('a', 1)
obj['a'] = 1;
// 查
set.has(item)
map.has('a')
'a' in obj
// 改
item.a = 2;
map.set('a', 2)
obj['a'] = 2;
// 删
set.delete(item)
map.delete('a')
delete obj['a']
4. css实现自适应正方形
padding-top使用%比数值时,是相对于父容器进行百分比计算数值的
aspect-ratio:
CSS 属性为box容器规定了一个期待的纵横比,这个纵横比可以用来计算自动尺寸以及为其他布局函数服务
<style>
.box {
width: 50%;
background: blue;
padding-top: 50%;
/* 或者使用
若为16/9 则为 width / height = 16/ 9
aspect-ratio: 1/1;
*/
}
</style>
<body>
<div class="box"></div>
</body>
兼容性
5. 自定义封装防抖
支持立即执行和非立即执行
立即执行:即多次触发事件,第一次会立即执行函数,之后在设定wait事件内触犯的事件无效,不会执行
非立即执行函数: 多次触发事件,只会在最后一次触发事件后等待设定的wait时间结束时执行一次
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
/**
* @desc 函数防抖---“立即执行版本” 和 “非立即执行版本” 的组合版本
* @param func 需要执行的函数
* @param wait 延迟执行时间(毫秒)
* @param immediate---true 表立即执行,false 表非立即执行
**/
function debounce(func, wait, immediate) {
let timer;
return function () {
//this指向debounce
let context = this;
//即参数,func,wait
let args = arguments;
//如果timer不为null, 清除定时器
if (timer) clearTimeout(timer);
//如果是立即执行
if (immediate) {
//定义callNow = !timer
var callNow = !timer;
//定义wait时间后把timer变为null
//即在wait时间之后事件才会有效
timer = setTimeout(() => {
timer = null;
}, wait)
//如果callNow为true,即原本timer为null
//那么执行func函数
if (callNow) func.apply(context, args)
} else {
//如果是不立即执行
//那就是每次重新定时
timer = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener("mousemove",debounce(handle,1000,true)); // 调用立即执行版本
// window.addEventListener("mousemove", debounce(handle, 1000, false)); // 调用非立即执行版本
</script>
</html>
6. 数据分组
将list数据通过指定key分组
// 对数据进行分组处理
handleDepartData(data, key = 'category') {
let obj = {};
data.forEach(c => {
let value = c[key];
obj[value] = obj[value] || []
obj[value].push(c);
});
return obj;
}
7. 将树形结构数据扁平化
/**
* 树形结构转为数组
* @param node {NodeList} 树形结构数据
* @returns {*[]}
*/
deep(node){
let stack = node
let data = []
while(stack.length){
let pop = stack.pop();
data.push(pop)
let children = pop.children
if(children){
for(let i = children.length-1; i >=0; i--){
stack.push(children[i])
}
}
}
return data
}
8. 数组对象去重
/**
* 数组对象去重
* @param {Array} arr 目标数组对象
* @param {String} key 通过key进行去重
* @returns {Array} 返回去重后的数组
*/
export const filterSameElement = (arr, key = 'value') => {
const res = new Map();
return arr.filter((item) => !res.has(item[key]) && res.set(item[key], 1));
}
9. 多条件筛选数据
使用场景:如果有多个条件同时满足的情况下,进行筛选列表数据
// 通过多个条件筛选班次
let obj = {
type: 'cate',
date: '2022-01-02',
disabled: 1
}
const list = [
{type: 'cate', date: '2022-01-02', disabled: 1, value: 2},
{type: 'cate', date: '2022-01-03', disabled: 1, value: 2},
{type: 'test', date: '2022-01-07', disabled: 1, value: 2},
]
let keys = Object.keys(obj)
// tempList即为筛选后的数据
let tempList = list.filter((item) => keys.every(key => obj[key] === item[key]))
//
function filterDataByInditions(obj, list){
let keys = Object.keys(obj)
return list.filter((item) => keys.every(key => obj[key] === item[key]))
}
10. 解决element datepicker报错
使用elementui 时间选择器时,浏览器一直报错,错误提示如下, Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated:"placement".
解答
查询源码发现报错原因在于placement赋值问题, this.placement = PLACEMENT_MAP[this.align] || PLACEMENT_MAP.left,由于未设置align,导致一直报警告⚠️
<!-- 添加align="center"配置,可以解决找到不值的问题 -->
<el-date-picker
v-model="value2"
type="date"
align="center"
placeholder="时间选择">
</el-date-picker>
11. 处理平铺数据分级分组
给一个list数据,需要先通过a字符分组后,再通过b字段进行进一步分组
/**
* 将数据多级分组
* @param array {Array} 需要分组的数据
* @param keys {Array} 需要进行多级分组的字段值
* @param index {number}
* @returns {*[]|*}
*/
function recursiveGrouping(array, keys = ['planDate'], index = 0) {
let temp = []
if (keys[index]) {
array.forEach((item) => {
let data = temp.find((x) => x[keys[index]] === item[keys[index]])
if (data) {
data.list.push(item)
} else {
temp.push({
[keys[index]]: item[keys[index]],
list: [item],
})
}
})
index++
temp.forEach((item) => {
item.list = recursiveGrouping(item.list, keys, index)
})
} else {
return array
}
return temp
}
12. vue provide, inject响应式数据
默认情况下provide注入的数据不支持响应式,需要进行处理
// 父组件中
export default {
data () {
return {
// 这里需要注入的数据要定义为对象类型,因为基础类型无法触发defineReactive手动绑定响应式的监听
for: {}
}
},
provide() {
return {
provObj: this.for
};
},
mounted() {
setTimeout(() => {
this.for.fp= 'demo';
}, 2000);
}
}
// 子组件
export default {
inject:['provObj'],
data(){
return{
demo: this.provObj.fp
}
}
13. vue发布订阅方式实现数据
利用的是elment-ui的发布订阅的方式,实现外部组件监听到内部组件数据的变化
原理:vue中通过parent或parent或parent或children调用多级嵌套的父或子组件的方法
/*
@description 遍历寻找所有子孙组件,假如子孙组件和componentName组件名称相同的话,则触发$emit的事件方法,数据为 params.
如果没有找到 则使用递归的方式 继续查找孙组件,直到找到为止,否则继续递归查找,直到找到最后一个都没有找到为止
*/
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
/**
* @method dispatch 查找所有父级,直到找到要找到的父组件,并在身上触发指定的事件
* @param componentName 组件名
* @param eventName 事件名
* @param params 触发时带入的参数
*/
dispatch(componentName, eventName, params) {
// 查找到当前元素的父组件,如果没有就使用根节点,并取父组件的组件名用于后期匹配
let parent = this.$parent || this.$root;
let name = parent.$options.name;
// while循环,用于循环父组件,直到找到或者到达根元素,匹配不到
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
// 当有匹配到的父组件时,就去触发父组件的对应的事件函数
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
/**
* @method broadcast 事件广播
* @param componentName 组件名
* @param eventName 事件名
* @param params 触发时带入的参数
*/
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
调用子组件方法
/* 父组件 */
export default {
name: 'eleComponents',
methods: {
testClick() {
console.log(document.getElementById('testBtn'))
}
},
mounted(){
// 调用dispatch的获取元素方法
this.$on('getTestBtn', this.testClick);
}
}
/* 子组件 */
methods:{
// 需要调用父组件的方法
handleChange(){
this.dispatch('eleComponents', 'getTestBtn');
}
}
调用父组件方法
/* 子组件 */
export default {
name: 'verifyCountBtn',
methods: {
sendVerifyCode() {
console.log(document.getElementById('testBtn'))
}
},
mounted(){
// 调用dispatch的获取元素方法
this.$on('sendVerifyCode', this.sendVerifyCode);
}
}
/* 父组件 */
methods:{
// 需要调用子组件的方法
handleChange(){
this.broadcast('verifyCountBtn', 'sendVerifyCode');
}
}
14. 前端主题切换
css变量+类名切换
提前引入样式文件,切换时将指定的根元素类名替换
/* 滚动条设置为黑色 */
html.dark {
color-scheme: dark;
}
/*方案实现*/
/* 定义根作用域下的变量 */
:root {
--theme-color: #333;
--theme-background: #eee;
}
/* 更改dark类名下变量的取值 */
.dark{
--theme-color: #eee;
--theme-background: #333;
}
/* 更改pink类名下变量的取值 */
.pink{
--theme-color: #fff;
--theme-background: pink;
}
.box {
transition: all .2s;
width: 100px;
height: 100px;
border: 1px solid #000;
/* 使用变量 */
color: var(--theme-color);
background: var(--theme-background);
}
15. 大屏项目适配全屏幕
若设计稿是1920*1080可以直接复用下面方法,其它尺寸可修改配置数据
适配方案
// drawMixin.js封装
// 屏幕适配 mixin 函数
// * 默认缩放值
const scale = {
width: '1',
height: '1',
}
// * 设计稿尺寸(px)
const baseWidth = 1920
const baseHeight = 1080
// * 需保持的比例(默认1.77778)
const baseProportion = parseFloat((baseWidth / baseHeight).toFixed(5))
export default {
data() {
return {
// * 定时函数
drawTiming: null
}
},
mounted () {
this.calcRate()
window.addEventListener('resize', this.resize)
},
beforeDestroy () {
window.removeEventListener('resize', this.resize)
},
methods: {
calcRate () {
const appRef = this.$refs["appRef"]
if (!appRef) return
// 当前宽高比
const currentRate = parseFloat((window.innerWidth / window.innerHeight).toFixed(5))
if (appRef) {
if (currentRate > baseProportion) {
// 表示更宽
scale.width = ((window.innerHeight * baseProportion) / baseWidth).toFixed(5)
scale.height = (window.innerHeight / baseHeight).toFixed(5)
appRef.style.transform = `scale(${scale.width}, ${scale.height}) translate(-50%, -50%)`
} else {
// 表示更高
scale.height = ((window.innerWidth / baseProportion) / baseHeight).toFixed(5)
scale.width = (window.innerWidth / baseWidth).toFixed(5)
appRef.style.transform = `scale(${scale.width}, ${scale.height}) translate(-50%, -50%)`
}
}
},
resize () {
clearTimeout(this.drawTiming)
this.drawTiming = setTimeout(() => {
this.calcRate()
}, 200)
}
},
}
css样式添加
<template>
<div class="container" ref="appRef" id="index">
</div>
</template>
<script>
import drawMixin from "@/mixins/drawMixin";
export default {
mixins: [drawMixin],
}
</script>
<style lang="scss">
#index {
color: #d3d6dd;
width: 1920px;
height: 1080px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: left top;
overflow: hidden;
.bg {
width: 100%;
height: 100%;
padding: 16px 16px 0 16px;
//background-image: url("../assets/pageBg.png");
background-size: cover;
background-position: center center;
}
}
</style>
16. 将列表数据转为Tree
提供一个包含父id对应关系的list数据,要求转为树形结构
数据结构
// 扁平数据
let list = [
{ id: 1, pid: null, name: 'M1部门' },
{ id: 11, pid: 1, name: '张三' },
{ id: 12, pid: 1, name: '李四' },
{ id: 13, pid: 1, name: '王五' },
{ id: 2, pid: null, name: 'M2部门' },
{ id: 21, pid: 2, name: '赵六' },
{ id: 22, pid: 2, name: '周七' },
{ id: 23, pid: 2, name: '吴八' }
]
// 树形结构
let tree = [
{
id: 1, pid: null, name: 'M1部门',
children: [
{ id: 11, pid: 1, name: '张三' },
{ id: 12, pid: 1, name: '李四' },
{ id: 13, pid: 1, name: '王五' }
]
},
{
id: 2, pid: null, name: 'M2部门',
children: [
{ id: 21, pid: 2, name: '赵六' },
{ id: 22, pid: 2, name: '周七' },
{ id: 23, pid: 2, name: '吴八' }
]
}
]
方法1:递归转换
- 根据pid和id的对应关系筛选出根节点
- 遍历根节点调用自身匹配子节点
const listToTree = (list, {rootid = null, id = 'id', pid = 'pid'}) => {
return list.filter(item => item[pid] === rootid)
.map(item => ({...item, children: listToTree(list, {rootid: item[id]})}))
}
【缺点】递归容易造成堆栈的溢出,且性能消耗巨大
方法2:遍历转换
核心思想:利用引用数据类型浅拷贝的特征,直接从
Map
中找对应的数据进行存储
- 通过id给列表的每一项做一个映射
- 通过pid从映射中取父节点
/**
* 扁平数据结构转Tree
*
* @param {Array} list 源数据
* @param {String} id 唯一的自增ID名称
* @param {String} pid 父ID名称
* @param {String} branch 树杈字段名称
* @return {Array} roots 目标数据
* @example
*
* listToTree(data)
* listToTree(data, { branch: 'children' })
*/
const listToTree = (list = [], {id = 'id', pid = 'pid', branch = 'children'} = {}) => {
const hash = new Map(), roots = []
list.forEach(item => {
hash.set(item[id], item)
const parent = hash.get(item[pid])
if (!parent) {
roots.push(item)
} else {
!parent[branch] && (parent[branch] = [])
parent[branch].push(item)
}
})
return roots
}
转载自:https://juejin.cn/post/7226970948182884410