注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

说一说css的font-size: 0?

web
平常我们说的font-size:0;就是设置字体大小为0对吧,但是它的用处不仅仅如此哦,它还可以消除子行内元素间额外多余的空白! 问题描述? 是否出现过当多个img标签平铺的时候,会出现几个像素的间距?就像这样👇(为了醒目加了个红色的框框) 是什么原因造成...
继续阅读 »

平常我们说的font-size:0;就是设置字体大小为0对吧,但是它的用处不仅仅如此哦,它还可以消除子行内元素间额外多余的空白



问题描述?


是否出现过当多个img标签平铺的时候,会出现几个像素的间距?就像这样👇(为了醒目加了个红色的框框)


image.png


是什么原因造成的呢?


大家都知道img是行内元素,比如当我们的标签换行的时候,回车符会解析一个空白符,所以这是造成会有间距的原因之一。


当然喽,不仅仅是img,包括其他的一些常见的行内元素,比如span👇标签回车换行的效果,同样也会间隙,当然如果是缩进、空格等字符同样也会产生空白间隙,导致元素间产生多余的间距


image.png


    <span>背景图</span>
<span>背景图</span>
<span>背景图</span>
<span>背景图</span>
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >

如何解决呢?


那我们首先想到取消换行、空格...


既然是因为标签换行了引起的,那么我们就取消换行、空格等试一试。


image.png


<span>背景图</span><span>背景图</span><span>背景图</span><span>背景图</span>
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >

证明方法还是有用的~ 那还有没有其他的方法解决呢,那这个时候可以借助font-size:0来用一用。


如何使用font-size: 0 解决呢?


利用font-size:0消除子行内元素间额外多余的空白,需要在父元素上添加font-size:0


image.png


是不是就解决了呀?


看一个完整的完整demo效果


image.png
当然需要注意一下



设置font-size: 0时,子元素必须指定一个font-size大小,否则文本内容不会显示哦



示例代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
<style>
/*************************css代码👇***********************/
ul {
margin: 20px;
display: flex;
gap: 20px;
}
.item {
width: 300px;
height: 200px;
padding: 20px;
border-radius: 10px;
background: #fff;
overflow: hidden;
font-size: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)
}
.item-img {
width: 100%;
height: 175px;
object-fit: cover;
border-radius: 5px;
}
.item-text {
color: #333;
font-size: 14px;
}
span {
background-color: red;
padding: 10px;
}
</style>
</head>
<body>


<ul>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
</ul>
</body>
</html>


作者:是小西瓜吖
来源:juejin.cn/post/7260752483055878204
收起阅读 »

一个左侧导航栏的选中状态把我迷得颠三倒四

web
事情是这样的 👇 前段时间我用arco.design这个组件库默认的模板项目开放,刚开始需求是这样的:总共八个页面,其中四个显示在导航栏,四个不在导航栏显示,可以导航的页面里又有俩个需要登录才能访问,俩个不登录就能访问。听着是有些绕不过开发起来不是SO EAS...
继续阅读 »

事情是这样的 👇


前段时间我用arco.design这个组件库默认的模板项目开放,刚开始需求是这样的:总共八个页面,其中四个显示在导航栏,四个不在导航栏显示,可以导航的页面里又有俩个需要登录才能访问,俩个不登录就能访问。听着是有些绕不过开发起来不是SO EASY吗😎


我用的是vue版本的,说白了就一个知识点——路由和菜单。凭借我的聪明才智,肯定一看就会。


路由


首先,需要先了解一下路由表的配置。基本的路由配置请参阅 Vue-Router 官方文档


// 在本例子中,页面最终路径为 /dashboard/workplace
export default {
path: 'dashboard',
name: 'dashboard', // 路由名称
component: () => import('@/views/dashboard/index.vue'),
meta: {
locale: 'menu.dashboard',
requiresAuth: true,
icon: 'icon-dashboard',
},
children: [
{
path: 'workplace',
name: 'workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: {
locale: 'menu.dashboard.workplace',
requiresAuth: true,
roles: ['admin'],
hideInMenu: false,
},
},
],
};

路由 Meta 元信息


参数名说明类型默认值
roles配置能访问该页面的角色,如果不匹配,则会被禁止访问该路由页面string[]-
requiresAuth是否需要登录鉴权booleanfalse
icon菜单配置iconstring-
locale一级菜单名(语言包键名)string-
hideInMenu是否在左侧菜单中隐藏该项boolean-
hideChildrenInMenu强制在左侧菜单中显示单项boolean-
activeMenu高亮设置的菜单项string-
order排序路由菜单项。如果设置该值,值越高,越靠前number-
noAffix如果设置为true,标签将不会添加到tab-bar中boolean-
ignoreCache如果设置为true页面将不会被缓存boolean-

hideInMenu 控制菜单显示, requiresAuth 控制是否需要登录,没错,以上知识点完全够我用了🤏🤏🤏🤏


三下五除二就开发完成了,正当我沉浸在成功的喜悦时,测试给我提了个bug。


说导航栏目切换,选中状态有俩个正常,俩个不正常,切换页面导航没选中,再点一次才会选中。我擦👏👏👏👏👏,我傻眼了😧😧😧


到底哪里出了问题,我哪里写错了呢,人家官方肯定没问题,于是我开始寻找。是路由名称重复了,还是那个组件写的有问题,还好有俩个正常的,我仔细比对一下不就好了吗,我可真是个小机灵鬼。


就这样,我又一次为我的自大付出了汗水,对比了一天,我感觉好像真不是我写的有问题,不过还是有收获的,我发现requiresAuth 设置为true的导航正常,requiresAuth设置为false的不正常。抱着怀疑的态度我开始找原来模板项目中处理requiresAuth的代码。最后在components》menu》index.vue文件下发现一个方法:


listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if (requiresAuth && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);

const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];

selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);

这... ... 我人麻了,这不是只有requiresAuth 为true的时候才会有效吗?他为啥这么写呢?还是我复制项目的时候复制错了?然后我改成了


listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if ((requiresAuth === true || requiresAuth === false) && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);

const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];

selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);

🙈🙈🙈🙈🙈看出来我改哪里了吗,考考你们的眼力,我改的方法是不是很nice。
反正好使了,下班!!!!


作者:一路向北京
来源:juejin.cn/post/7317277887567151145
收起阅读 »

【日常总结】解决el-select数据量过大的3种方法

web
背景 最近做完一个小的后台管理系统,快上线了,发现一个问题,有2个select的选项框线上的数据量是1w+。。而测试环境都是几百的,所以导致页面直接卡住了,over了。 想了一下,如果接口支持搜索和分页,那直接通过上拉加载就可以了。但后端不太愿意改😄。行吧,...
继续阅读 »

背景


最近做完一个小的后台管理系统,快上线了,发现一个问题,有2个select的选项框线上的数据量是1w+。。而测试环境都是几百的,所以导致页面直接卡住了,over了。


image.png


想了一下,如果接口支持搜索和分页,那直接通过上拉加载就可以了。但后端不太愿意改😄。行吧,前端搞也是可以的。


这个故事还有个后续


image.png


过了一周上线后,发现有一个下拉框的数据有30w+!!!加载都加载不出来,哈哈哈哈,接口直接超时报错了,所以又cuocuocuo改了一遍,最后改成了:



  1. 接口翻页请求

  2. 前端使用自定义指令实现上拉加载更多,搜索直接走的后端接口


方案


通过一顿搜索加联想总结了3种方法,以下方法都需要支持开启filterable支持搜索。


标题具体问题
方案1只展示前100条数据,这个的话配合filter-method每次只返回前100条数据。限制展示的条数可能不全,搜索需要多搜索点内容
方案2分页方式,通过指令实现上拉加载,不断上拉数据展示数据。仅过滤加载出来的数据,需要配合filterMethod过滤数据
方案3options列表采用虚拟列表实现。成本高,需要引入虚拟列表组件或者自己手写。经掘友指点,发现element-plus提供了对应的实现,如果是plus,则可以直接使用select-v2

方案一(青铜段位) filterMethod直接过滤数据量


<template>
<el-select
v-model="value"
clearable filterable
:filter-method="filterMethod">

<el-option
v-for="(item, index) in options.slice(0, 100)"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>

</template>
export default {
name: 'Demo',
data() {
return {
options: [],
value: ''
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
for (let i = 0; i < 25000; i++) {
this.options.push({label: "选择"+i,value:"选择"+i});
}
},
filterMethod(val) {
console.log('filterMethod', val);
this.options = this.options.filter(item => item.value.indexOf(val) > -1).slice(0, 100);
},
visibleChange() {
console.log('visibleChange');
}
}
}

方案二(白银段位) 自定义滚动指令,实现翻页加载


写自定义滚动指令,options列表滚动到底部后,再加载下一页。但这时候筛选出来的是已经滚动出来的值。


这里如果直接使用filterable来搜索,搜索出来的内容是已经滑动出来的内容。如果想筛选全部的,就需要重写filterMethod方法来自定义过滤功能。可以根据情况选择是否要重写filterMethod。


image.png


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable
v-el-select-loadmore="loadMore"
:filter-method="filterMethod">

<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>
</div>

</template>
<script>
export default {
name: 'Demo',
data() {
return {
options: [],
value: '',
pageNo: 0
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
const data = [];
for (let i = 0; i < 25000; i++) {
data.push({label: "选择"+i,value:"选择"+i});
}
this.allData = data;
this.data = data;
this.getPageList()
},
getPageList(pageSize = 10) {
this.pageNo++;
const list = this.data.slice(0, pageSize * (this.pageNo));
this.options = list;
},
loadMore() {
this.getPageList();
},
filterMethod(val) {
this.data = val ? this.allData.filter(item => item.label.indexOf(val) > -1) : this.allData;
this.getPageList();
}
},
directives:{
'el-select-loadmore':(el, binding) => {
// 获取element-ui定义好的scroll父元素
const wrapEl = el.querySelector(".el-select-dropdown .el-select-dropdown__wrap");
if(wrapEl){
wrapEl.addEventListener("scroll", function () {
/**
* scrollHeight 获取元素内容高度(只读)
* scrollTop 获取或者设置元素的偏移值,
* 常用于:计算滚动条的位置, 当一个元素的容器没有产生垂直方向的滚动条, 那它的scrollTop的值默认为0.
* clientHeight 读取元素的可见高度(只读)
* 如果元素滚动到底, 下面等式返回true, 没有则返回false:
* ele.scrollHeight - ele.scrollTop === ele.clientHeight;
*/

if (this.scrollTop + this.clientHeight >= this.scrollHeight) {
// binding的value就是绑定的loadmore函数
binding.value();
}
});
}
},
},
}
</script>

</script>

方案三(黄金段位) 虚拟列表


引入社区的vue-virtual-scroll-list 支持虚拟列表。但这里想的自己再实现一遍虚拟列表,后续再写吧。


另外,element-plus提供了对应的实现,如果是使用的是plus,则可以直接使用 select-v2组件


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable >

<virtual-list
class="list"
style="height: 360px; overflow-y: auto;"
:data-key="'value'"
:data-sources="data"
:data-component="item"
:estimate-size="50"
/>

</el-select>
</div>

</template>
<script>
import VirtualList from 'vue-virtual-scroll-list';
import Item from './item';
export default {
name: 'Demo',
components: {VirtualList, Item},
data() {
return {
options: [],
data: [],
value: '',
pageNo: 0,
item: Item,
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
const data = [];
for (let i = 0; i < 25000; i++) {
data.push({label: "选择"+i,value:"选择"+i});
}
this.allData = data;
this.data = data;
this.getPageList()
},
getPageList(pageSize = 10) {
this.pageNo++;
const list = this.data.slice(0, pageSize * (this.pageNo));
this.options = list;
},
loadMore() {
this.getPageList();
}
}
}
</script>


// item组件
<template>
<el-option :label="source.label" :value="source.value"></el-option>
</template>


<script>
export default {
name: 'item',
props: {
source: {
type: Object,
default() {
return {}
}
}
}
}
</script>


<style scoped>
</style>



总结


最后我们项目中使用的虚拟列表,为啥,因为忽然发现组件库支持select是虚拟列表,那就直接使用这个啦。


最后的最后


没有用虚拟列表,因为接口数据量过大(你见过返回30w+的接口吗🙄。。),后端接口改成分页,前端支持自定义指令上拉加载,引用的参数增加了remote、remote-method设置为远端的方法。


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable
v-el-select-loadmore="loadMore"
remote
:remote-method="remoteMethod">

<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>
</div>

</template>

参考文章:



作者:searchop
来源:juejin.cn/post/7278238985448341544
收起阅读 »

从 Vue 2 迁移到 Svelte

web
大家好,这里是大家的林语冰。 本周之后,Vue 2 将停止开源维护。所以本期《前端翻译计划》共享的是某企业去年从 Vue 2 迁移到 Svelte 的现实测评,以及 Vue 3 和 Svelte 的“零和博弈”。 在使用 Vue 2 作为我们的前端框架近 2 ...
继续阅读 »

大家好,这里是大家的林语冰。


本周之后,Vue 2 将停止开源维护。所以本期《前端翻译计划》共享的是某企业去年从 Vue 2 迁移到 Svelte 的现实测评,以及 Vue 3 和 Svelte 的“零和博弈”。


在使用 Vue 2 作为我们的前端框架近 2 年后,我们宣布此支持将不再维护,因此我们决定迁移到新框架。但该选谁呢:Vue 3 还是 Svelte 呢


粉丝请注意,我们迁移后的目标也是改善 DX(开发体验),尤其是类型检查、性能和构建时间。我们没有考虑 React,因为它需要投资一大坨时间成本来学习,而且与 Vue 和 Svelte 不同,它没有提供开箱即用的解决方案。此外,后者共享相同的 SFC(单文件组件)概念:同一文件中的逻辑(JS)、结构(HTML)和样式(CSS)。


Svelte vs Vue 3


Svelte 的留存率更高。对于我们的新前端,我们必须从市场上可用的 2 个框架权衡,即 Svelte 和 Vue 3。下面是过去 5 年不同框架留存率的图示(留存率 = 会再次使用/(会再次使用 + 不会再次使用))。JS 现状调查汇编了该领域开发者的数据,如你所见,Svelte 排名第二,而 Vue 3 排名第四。


01-state.jpg


这启示我们,过去使用过 Svelte 的开发者愿意再次使用它的数量比不愿使用它的要多。


Svelte 的类型体验更棒


Vue 2Vue 3Svelte
组件级类型YesYesNo
跨组件类型NoYesYes
类型事件NoNoYes

Svelte 通过更简单的组件设计流程和内置类型事件提供了更好的类型体验,对我们而言十分用户友好。


全局变量访问限制。使用 Svelte,可以从其他文件导入枚举,并在模板中使用它们,而 Vue 3 则达咩。


02-benchmark.png


语法。就我个人而言,私以为 Svelte 语法比 Vue 更优雅和用户友好。您可以瞄一下下面的代码块,并亲自查看它们。


Svelte:


03-svelte.png


Vue:


04-vue.png


没有额外的 HTML div <template>。在 Svelte 中您可以直接编写自己的 HTML。


样式在 Svelte 中会自动确定作用域,这对于可维护性而言是一个优点,且有助于避免 CSS 副作用。每个组件的样式独立,能且仅能影响该组件,而不影响其父/子组件。


更新数据无需计算属性。在 Svelte 中,感觉更像在用纯 JS 撸码。您只需要专注于编写一个箭头函数:


const reset = () => {
firstName = ''
lastName = ''
}

Svelte 中只需单个括号:


//Svelte
{fullName}

//Vue
{{fullName}}

添加纯 JS 插件更简单。此乃使用 Svelte 和 Prism.js 的语法高亮集成用例,如下所示:


05-prism.png


无需虚拟 DOM 即可编译代码。Svelte 和 Vue 之间的主要区别是,减少了浏览器和 App 之间的层数,实现更优化、更快的任务成果。


自动更新。诉诸声明变量的辅助,Svelte 可以自动更新您的数据。这样,您就不必等待变更反映在虚拟结构中,获得更棒的 UX(用户体验)。


Svelte 也有短板


理所当然,Svelte 也有短板,比如社区相对较小,因为它是 2019 年才诞生的。但随着越来越多的开发者可能会认识到其质量和用户友好的内容,支持以及社区未来可能会不断发展壮大。


因此,在审查了此分析的结果后,尽管 SvelteKit 在迁移时仍处于积极开发阶段,我们决定使用 Svelte 和 Svelte Kit 砥砺前行。


06cons.png


如何处理迁移呢?


时间:我们选择在 8 月份处理迁移,当时该 App 用户较少。


时间长度:我们花了 2 周时间将所有文件从 Vue 迁移到 Svelte。


开发者数量:2 名前端开发者全职打工 2 周,另一名开发者全职打工 1 周,因此涉及 3 名开发人员。


工作流:首先,我们使用 Notion 工具将我们的凭据归属于团队的开发者。然后,我们开始在 Storybook 中创建新组件,最后,每个开发者都会奖励若干需要在 Svelte 中重写的页面。


作为一家初创公司,迁移更简单,因为我们没有 1_000 个文件需要重写,因此我们可以快速执行。虽然但是,当 SvelteKit 仍处于积极开发阶段时,我们就冒着风险开始迁移到 SvelteKit,这导致我们在迁移仅 1 个月后就不得不做出破坏性更新。但 SvelteKit 专业且博大精深的团队为我们提供了一个命令(npx svelte-migrate routes),以及一个解释清晰的迁移指南,真正帮助我们快速适应新的更新。


此外,9 月份,SvelteKit 团队宣布该框架终于进入候选版本阶段,这意味着,它的稳定性现在得到了保证!


文件和组件组织


SvelteKit 的“文件夹筑基路由”给我们带来了很多。我们可以将页面拆分为子页面,复用标准变量名,比如 loading/submit 等等。此外,布局直接集成到相关路由中,由于树内组织的增加,访问起来更简单。


那么我们得到了什么?


除了上述好处之外,还值得探讨其他某些关键因素:


性能提高且更流畅。编译完成后,我们可以体会到该 App 的轻量级。与其他框架相比,这提高了加载速度,其他框架在 App 的逻辑代码旁嵌入了“运行时”。


DX 更棒。SvelteKit 使用 Vite 打包器,此乃新一代 JS 构建工具,它利用浏览器中 ES 模块的可用性和编译为原生(compile-to-native)的打包器,为您带来最新 JS 技术的最佳 DX。


代码执行更快。它没有虚拟 DOM,因此在页面上变更时,需要执行的层数少了一层。


启动并运行 SSR(服务器端渲染)。如果最终用户没有良好的互联网连接或启用 JS,平台仍将在 SSR 的帮助下高效运行,因为用户仍能加载网页,同时失去交互性。


代码简洁易懂。Svelte 通过将逻辑(JS)、结构(HTML)和样式(CSS)分组到同一文件中,可以使用更具可读性和可维护性的面向组件的代码。黑科技在于所有这些元素都编译在 .svelte 文件中。


固定类型检查。自从我们迁移到 Svelte 以来,我们已经成功解决了类型检查的最初问题。事实上,我们以前必须处理周期性的通知,而如今时过境迁。不再出现头大的哨兵错误。(见下文)


07-error.jpg


粉丝请注意,此博客乃之前的迁移测评,其中某些基准测试见仁见智,尤大还亲自码字撰写博客布道分享,我们之后会继续翻译 Vue 官方博客详细说明。



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Migrating from Vue 2 to Svelte



您现在收看的是《前端翻译计划》,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎关注地球猫猫教。谢谢大家的点赞,掰掰~


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7317222425384714294
收起阅读 »

看我如何用JDBC数据库连接池,轻松解决大量并发请求问题!

我们已经知道JDBC是Java语言中用来规范客户端程序如何访问数据库的应用程序接口,也是大多数Java开发者与数据库打交道的必备工具。但是,你是否知道,JDBC在处理大量并发请求时,可能会遇到一些问题?这就是我们今天要讨论的主题——JDBC数据库连接池。首先,...
继续阅读 »

我们已经知道JDBC是Java语言中用来规范客户端程序如何访问数据库的应用程序接口,也是大多数Java开发者与数据库打交道的必备工具。

但是,你是否知道,JDBC在处理大量并发请求时,可能会遇到一些问题?这就是我们今天要讨论的主题——JDBC数据库连接池。

首先,让我们来了解一下什么是数据库连接池。

一、数据库连接池简介

JDBC连接池,全称为Java多线程数据库连接池,是一种用于管理数据库连接的技术。其主要作用是减少每次请求时创建和释放数据库连接的开销,以此提高系统性能。

在应用程序和数据库之间,JDBC连接池会建立一个连接池,当需要访问数据库时,无需每次都重新创建连接,而是直接从池中获取已有的连接。

Description

总结一下就是:

  • 数据库连接池是个容器,负责分配、管理数据库连接(Connection)

  • 它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。

  • 释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。

那么,为什么我们需要JDBC数据库连接池呢?

这主要有以下几个原因:

1.提高性能: 频繁地创建和销毁数据库连接会消耗大量的系统资源,而通过使用连接池,可以大大减少这部分开销,提高系统的性能。

2.提高稳定性: 在高并发的情况下,如果直接使用JDBC创建数据库连接,可能会出现系统无法创建更多的数据库连接的情况,导致系统崩溃。而通过使用连接池,可以有效地控制并发请求的数量,保证系统的稳定性。

3.提高数据库的响应速度: 通过使用连接池,可以减少等待数据库连接的时间,从而提高系统的响应速度。

之前我们代码中使用连接是没有使用都创建一个Connection对象,使用完毕就会将其销毁。这样重复创建销毁的过程是特别耗费计算机的性能的及消耗时间的。

而数据库使用了数据库连接池后,就能达到Connection对象的复用,如下图:

Description

  • 连接池是在一开始就创建好了一些连接(Connection)对象存储起来。用户需要连接数据库时,不需要自己创建连接;

  • 而只需要从连接池中获取一个连接进行使用,使用完毕后再将连接对象归还给连接池。

这样就可以起到资源重用,也节省了频繁创建连接销毁连接所花费的时间,从而提升了系统响应的速度。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看


二、数据库连接池实现

1、标准接口:

javax.sql.DataSource。

官方(SUN公司)为数据库连接池提供了一套标准接口,由第三方组织实现此接口。

  • 核心方法:Connection getConnection(),获取连接。

Description

2、常见的数据库连接池:

JDBC的数据库连接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由第三方来实现。

市面上有很多开源的JDBC数据库连接池,如C3P0、DBCP、Druid等,它们都有各自的特点和优势。

C3P0数据库连接池: 速度相对较慢(只是慢一丢丢),但是稳定性很好,Hibernate,Spring底层用的就是C3P0。

DBCP数据库连接池: 速度比C3P0快,但是稳定性差。

Proxool数据库连接池: 有监控连接池状态的功能,但稳定性仍然比C3P0差一些。

BoneCP数据库连接池: 速度较快。

Druid数据库连接池(德鲁伊连接池): 由阿里提供,集DBCP,Proxool,C3P0连接池的优点于一身,是日常项目开发中使用频率最高的数据库连接池。

三、Durid(德鲁伊连接池)的使用

Druid使用步骤:

  • 导入jar包 druid-1.1.12.jar。

  • 定义配置文件。

  • 加载配置文件。

  • 获取数据库连接池对象。

  • 获取连接。

druid.properties配置文件:

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc_test?useSSL=false&useServerPrepStmts=true
username=root
password=123456
# 初始化连接数量
initialSize=5
# 最大连接数
maxActive=10
# 最大等待时间
maxWait=3000

代码示例:


package com.green.druid;


import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.FileInputStream;
import java.sql.Connection;
import java.util.Properties;

public class DruidDemo {

public static void main(String[] args) throws Exception {
//1、导入jar包


//2、定义配置文件


//3、加载配置文件
Properties prop = new Properties();
prop.load(new FileInputStream("jdbc-demo/src/druid.properties"));
//System.out.println(System.getProperty("user.dir")); //当前文件目录 D:\code\JDBC


//4、获取连接池对象
DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);


//5、获取数据库连接 Connection
Connection conn = dataSource.getConnection();


System.out.println(conn);


}
}

以上就是JDBC数据连接池的简介与常见连接池的基本使用,希望对你有所帮助。在未来的开发过程中,不妨尝试使用JDBC数据库连接池,让你的应用性能更上一层楼!

收起阅读 »

js跨标签页通信

web
一、为什么要跨标签页通信 在web开发中,有时会有这样的情况,A页面中打开了B页面,B页面中操作了一些内容,再回到A页面时,需要看到更新后的内容。这种场景在电商、支付、社交等领域经常出现。 二、实现跨标签页通信的几种方式 2.1 localStorage 打...
继续阅读 »

一、为什么要跨标签页通信


在web开发中,有时会有这样的情况,A页面中打开了B页面,B页面中操作了一些内容,再回到A页面时,需要看到更新后的内容。这种场景在电商、支付、社交等领域经常出现。


二、实现跨标签页通信的几种方式


2.1 localStorage


image.png


打开A页面,可以看到localStorage和sessionStorage中都存储了testA:


image.png
image.png

B页面中,可以获取到localStorage,但是无法获取到sessionStorage:


image.png

2.2 BroadcastChannel


BroadcastChannel允许同源下浏览器不同窗口订阅它,postMessage方法用于发送消息,message事件用于接收消息。


A页面:


      const bc = new BroadcastChannel('test')

bc.postMessage('不去上班行吗?')

B页面:


      const bc = new BroadcastChannel('test')

bc.onmessage = (e) => {
console.log(e)
}

动画.gif


2.3 postMessage(跨源通信)


image.png


2.3.1 iframe跨域数据传递


parent.html


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<h1>主页面</h1>
<iframe id="child" src="http://10.7.9.69:8080"></iframe>
<div>
<h2>主页面跨域接收消息区域</h2>
<div id="message"></div>
</div>
</body>
<script>
// 传递数据到子页面
window.onload = function () {
// 第二个参数表示哪个窗口可以接收到消息
document.getElementById('child').contentWindow.postMessage('不上班行不行', 'http://10.7.9.69:8080')
}
// 接受子页面传递过来的数据
window.addEventListener('message', function (event) {
document.getElementById('message').innerHTML = '收到' + event.origin + '消息:' + event.data
})
</script>
</html>


App.vue


<template>
<div class="app">
<div id="message"></div>
</div>

</template>
<script>
export default {
created() {
// 接收父页面传过来的数据
window.addEventListener('message', function (event) {
// 处理addEventListener执行两次的情况,避免获取不到data
// 因此判断接收的域是否是父页面
console.log('event', event)
if (event.origin.includes('http://127.0.0.1:5501')) {
document.getElementById('message').innerHTML = '收到' + event.origin + '消息:' + event.data
// 把数据传递给父页面 window.parent === top
window.parent.postMessage('不上班你养我啊', 'http://127.0.0.1:5501')
}
})
},
}
</script>


image.png


注:



  1. http://127.0.0.1:5501/ 是使用 Open with Live Server打开后的地址

  2. http://10.7.9.69:8080/ 是启动vue后的地址


2.3.2 postMessage在window.open()中的使用


作者:蓝色海岛
来源:juejin.cn/post/7315354087829536803
收起阅读 »

【前端考古】没有await,如何处理“回调地狱”

web
太长不看 不要嵌套使用函数。给每个函数命名并把他们放在你代码的顶层 利用函数提升。先使用后声明。 处理每一个异常 编写可以复用的函数,并把他们封装成一个模块 什么是“回调地狱”? 异步Javascript代码,或者说使用callback的Javascrip...
继续阅读 »

太长不看



  • 不要嵌套使用函数。给每个函数命名并把他们放在你代码的顶层

  • 利用函数提升。先使用后声明。

  • 处理每一个异常

  • 编写可以复用的函数,并把他们封装成一个模块


什么是“回调地狱”?


异步Javascript代码,或者说使用callback的Javascript代码,很难符合我们的直观理解。很多代码最终会写成这样:


fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})

看到上面金字塔形状的代码和那些末尾参差不齐的 }) 了吗?这就是广为人知的回调地狱了。

人们在编写JavaScript代码时,误认为代码是按照我们看到的代码顺序从上到下执行的,这就是造成回调地狱的原因。在其他语言中,例如C,Ruby或者Python,第一行代码执行结束后,才会开始执行第二行代码,按照这种模式一直到执行到当前文件中最后一行代码。随着你学习深入,你会发现JavaScript跟他们是不一样的。


什么是回调(callback)?


某种使用JavaScript函数的惯例用法的名字叫做回调。JavaScript语言中没有一个叫“回调”的东西,它仅仅是一个惯例用法的名字。大多数函数会立刻返回执行结果,使用回调的函数通常会经过一段时间后才输出结果。名词“异步”,简称“async”,只是意味着“这将花费一点时间”或者说“在将来某个时间发生而不是现在”。通常回调只使用在I/O操作中,例如下载文件,读取文件,连接数据库等等。


当你调用一个正常的函数时,你可以向下面的代码那样使用它的返回值:


var result = multiplyTwoNumbers(5, 10)
console.log(result)
// 50 gets printed out

然而使用回调的异步函数不会立刻返回任何结果。


var photo = downloadPhoto('http://coolcats.com/cat.gif')
// photo is 'undefined'!

在这种情况下,上面那张gif图片可能需要很长的时间才能下载完成,但你不想你的程序在等待下载完成的过程中中止(也叫阻塞)。


于是你把需要下载完成后运行的代码存放到一个函数中(等待下载完成后再运行它)。这就是回调!你把回调传递给downloadPhoto函数,当下载结束,回调会被调用。如果下载成功,传入photo给回调;下载失败,传入error给回调。


downloadPhoto('http://coolcats.com/cat.gif', handlePhoto)

function handlePhoto (error, photo) {
if (error) console.error('Download error!', error)
else console.log('Download finished', photo)
}

console.log('Download started')

人们理解回调的最大障碍在于理解一个程序的执行顺序。在上面的例子中,发生了三件事情。



  1. 声明handlePhoto函数

  2. downloadPhoto函数被调用并且传入了handlePhoto最为它的回调

  3. 打印出Download started


请大家注意,起初handlePhoto函数仅仅是被创建并被作为回调传递给了downloadPhoto,它还没有被调用。它会等待downloadPhoto函数完成了它的任务才会执行。这可能需要很长一段时间(取决于网速的快慢)。


这个例子意在阐明两个重要的概念:



  1. handlePhoto回调只是一个存放将来进行的操作的方式

  2. 事情发生的顺序并不是直观上看到的从上到下,它会当某些事情完成后再跳回来执行。


怎样解决“回调地狱”问题?


糟糕的编码习惯造成了回调地狱。幸运的是,编写优雅的代码不是那么难!


你只需要遵循三大原则


1. 减少嵌套层数(Keep your code shallow)


下面是一堆乱糟糟的代码,使用browser-request做AJAX请求。


**


var form = document.querySelector('form')
form.onsubmit = function (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, function (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
})
}

这段代码包含两个匿名函数,我们来给他们命名。


var form = document.querySelector('form')
form.onsubmit = function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
})
}

如你所见,给匿名函数一个名字是多么简单,而且好处立竿见影:



  • 起一个一望便知其函数功能的名字让代码更易读

  • 当抛出异常时,你可以在stacktrace里看到实际出异常的函数名字,而不是"anonymous"

  • 允许你合理安排函数的位置,并通过函数名字调用它


现在我们可以把这些函数放在我们程序的顶层。


document.querySelector('form').onsubmit = formSubmit

function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, postResponse)
}

function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
}

请大家注意,函数声明在程序的底部,但是我们在函数声明之前就可以调用它。这是函数提升的作用。


2.模块化(Modularize)


任何人都有有能力创建模块,这点非常重要。写出一些小模块,每个模块只做一件事情,然后把他们组合起来放入其他的模块做一个复杂的事情。只要你不想陷入回调地狱,你就不会。让我们把上面的例子修改一下,改为一个模块。


下面是一个名为formuploader.js的新文件,包含了我们之前使用过的两个函数。


module.exports.submit = formSubmit

function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, postResponse)
}

function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
}

module.exports是node.js模块化的用法。现在已经有了 formuploader.js 文件,我们只需要引入它并使用它。请看下面的代码:


var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit

我们的应用只有两行代码并且还有以下好处:



  1. 方便新开发人员理解你的代码 -- 他们不需要费尽力气读完formuploader函数的全部代码

  2. formuploader可以在其他地方复用


3.处理每一个异常(Handle every single error)


有三种不同类型的异常:语法异常,运行时异常和平台异常。语法异常通常由开发人员在第一次解释代码时捕获,运行时异常通常在代码运行过程中因为bug触发,平台异常通常由于没有文件的权限,硬盘错误,无网络链接等问题造成。这一部分主要来处理最后一种异常:平台异常。


前两个大原则意在提高代码可读性,但是第三个原则意在提高代码的稳定性。在你与回调打交道的时候,你通常要处理发送请求,等待返回或者放弃请求等任务。任何有经验的开发人员都会告诉你,你从来不知道哪里回出现问题。所以你有必要提前准备好,异常总是会发生。


把回调函数的第一个参数设置为error对象,是Node.js中处理异常最流行的方式。


var fs = require('fs')

fs.readFile('/Does/not/exist', handleFile)

function handleFile (error, file) {
if (error) return console.error('Uhoh, there was an error', error)
// otherwise, continue on and use `file` in your code
}

把第一个参数设为error对象是一个约定俗成的惯例,提醒你记得去处理异常。如果它是第二个参数,你更容易把它忽略掉。


作者:Max力出奇迹
来源:juejin.cn/post/7294166986195533843
收起阅读 »

做了几年前端,别跟我说没配置过webpack

web
引言 webpack中文官网:webpack.docschina.org/concepts/ webpack是一个javascript常用的静态模块打包工具,通过配置webpack,可以实现包括压缩文件,解析不同文件格式等强大的功能,作者发现很多同学对webp...
继续阅读 »

引言


webpack中文官网:webpack.docschina.org/concepts/


webpack是一个javascript常用的静态模块打包工具,通过配置webpack,可以实现包括压缩文件,解析不同文件格式等强大的功能,作者发现很多同学对webpack的认知都停留在入口出口以及简单的loader和plugin配置上,对webpack的核心原理都一知半解。本文期望通过更深层的解读,让读者能更彻底地理解这个打包工具的来龙去脉。


为什么要用webpack


在webpack等打包工具出世之前,我们普通的H5项目是怎么处理错综复杂的脚本呢?
第一种方式:引用不同的脚本去使用不同的功能,但脚本太多的时候会导致网络瓶颈
第二种方式:使用一个大型js文件去引入所有代码,但这样会严重影响可读性,可维护性,作用域。


举个栗子:
由于浏览器不能直接解析less文件,我们可通过引入转换的插件(file watcher)把less实时转换为css并引入,但项目里面会多出一个map跟css文件,造成项目文件的臃肿。


官方文档的说法:


node.js诞生可以让Javasrcipt在浏览器环境之外使用,而webpack运行在node.js中。CommonJS的require机制允许在文件中引用某个模块,如此一来就可以解决作用域的问题。


    const HtmlWebpackPlugin = require('html-webpack-plugin')

webpack 关心性能和加载时间;它始终在改进或添加新功能,例如:异步地加载 chunk 和预取,以便为你的项目和用户提供最佳体验


核心概念


webpack有7个核心概念:



  1. 入口(entry)

  2. 输出(output)

  3. loader

  4. 插件(plugin)

  5. 模式(mode)

  6. 浏览器兼容性(brower compatibility)

  7. 环境(environment)


新建一个build文件夹,里面新建一个webpack.config.js


入口entry


这是打包的入口文件,所有的脚本将从这个入口文件开始


单入口


    const path = require('path')
module.exports = {
entry: path.resolve(__dirname, '../src/main.js')
}

多入口


使用对象语法配置,更好扩展,例如一个项目有前台网页跟后台管理两个项目可用多入口管理。


    entry: {
app: path.resolve(__dirname, '../src/main.js'),
admin: path.resolve(__dirname, '../src/admin.js'),
},

输出output


打包后输出的文件,[name]跟[hash:8]表示文件名跟入口的保持一致但后面加上了hash的后缀让每次生成的文件名是唯一的。


单入口


module.exports = {
output: {
filename: '[name].[hash:8].js',
path: path.resolve(__dirname, '../dist'),
},
}

多入口


module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js',
},
output: {
filename: '\[name].js',
path: \_\_dirname + '/dist',
},
};
// 写入到硬盘:./dist/app.js, ./dist/admin.js

loader转化器


用于模块的源码的转换,也是同学们在配置webpack的时候频繁接触的一个配置项。


举个例子,加载typescript跟css文件需要用到ts-loader跟css-loader、style-loader,
如果没有对应的loader,打包会直接error掉。


image.png


我们可以这么配置:先 npm i css-loader style-loader


module: {
rules: [
{
test: /\.css\$/,
use: ['style-loader','css-loader']
},
{
test: /\.ts\$/,
use: 'ts-loader'
}

必须留意的是,loader的执行是从右到左,就是css-loader执行完,再交给style-loader执行,


plugin插件


这是webpack配置的核心,有一些loader无法实现的功能,就通过plugin去扩展,建立一个规范的插件系统,能让你每次搭建项目的时候省去很多成本。


举个例子,我们会使用HtmlWebpackPlugin这个插件去生成一个html,其中会引入入口文件main.js。
假设不用这个插件,会发生什么?


当然是不会生成这个html,因此HtmlWebpackPlugin插件也是webpack的必备配置之一


const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html') // 同时引入main.js的hash名字
}),
],

模式mode


mode一共有production,development,node三种,如果没有设置,会默认为production


不同的mode对于默认优化的选项有所不同,环境变量也不同,具体需要了解每个插件的具体使用


选项描述
development会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development
production会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPlugin 和 TerserPlugin
none没优化选项

module.exports = {
mode: 'production'
}

source-map 的解读


Sourcemap 本质上是一个信息文件,里面储存着代码转换前后的对应位置信息。它记录了转换压缩后的代码所对应的转换前的源代码位置,是源代码和生产代码的映射。 Sourcemap 解决了在打包过程中,代码经过压缩,去空格以及 babel 编译转化后,由于代码之间差异性过大,造成无法debug的问题


当mode为development时,devtool默认为‘eval’,当mode为production时,devtool默认为false。


sourceMap的分类



  • source-map:外部。可以查看错误代码准确信息和源代码的错误位置。

  • inline-source-map:内联。只生成一个内联 Source Map,可以查看错误代码准确信息和源代码的错误位置

  • hidden-source-map:外部用于生产环境。可以查看错误代码准确信息,但不能追踪源代码错误,只能提示到构建后代码的错误位置。

  • eval-source-map:内联。每一个文件都生成对应的 Source Map,都在 eval 中,可以查看错误代码准确信息 和 源代码的错误位置。

  • nosources-source-map:外部。可以查看错误代码错误原因,但不能查看错误代码准确信息,并且没有任何源代码信息。

  • cheap-source-map:外部。可以查看错误代码准确信息和源代码的错误位置,只能把错误精确到整行,忽略列。

  • cheap-module-source-map:外部用于生产环境。可以错误代码准确信息和源代码的错误位置,module 会加入 loader 的 Source Map。

  • eval-cheap-module-source-map: 内联,用于开发环境,构建跟热更新比较快


内联和外部的区别: 外部生成了文件(.map),内联没有。内联构建速度更快。


笔者用的两种配置分为是


// webpack.dev.js
devtool: 'eval-cheap-module-source-map',

// webpack.prod.js
devtool: 'cheap-module-source-map'

浏览器兼容性 brower compatibility


Webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)。webpack 的 import() 和 require.ensure() 需要 Promise。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要提前加载 polyfill


环境 enviroment


本文使用的是webpack5 ,要求Node.js V在10.13.0+


Loader的汇总


笔者汇总了一部分常用的Loader以及其配置事项



  1. 浏览器兼容性:babel-loader

  2. css相关: css/style/less/postcss-loader

  3. vue: vue-loader



在配置loader前,先了解一下基本的配置



  • test: 匹配的文件,多用正则匹配

  • use: 使用loader,多用数组

  • exclude: 调整Loader解析的范围,包括某个路径下的文件,不如node_modules

  • include: 调整Loader解析的范围,包括某个路径下的文件


解决浏览器兼容性:babel


转义语法的babel-loader


譬如把const转为浏览器认识的var,虽然现在大部分主流浏览器都认识ES5之后的语法。


npm i babel-loader @babel/preset-env @babel/core

在rules配置:


{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
exclude: /node_modules/
}

转义ES API的babel/polyfill


如果只有babel-loader,浏览器并不能识别出新的API(promise,proxy,includes),如图:


image.png


因此还需要配置一个babel/polyfill,在入口里面:


// npm i @babel/polyfill

entry: ["@babel/polyfill",path.resolve(__dirname, '../src/main.js')],

解析vue的vue-loader


vue-loader: 解析vue
vue-template-compiler: 编译vue模板


npm i vue-loader vue-template-compiler vue-style-loader
npm i vue

在rules跟plugins配置:


const { VueLoaderPlugin } = require('vue-loader') // vue3的引入跟vue2路径不同

rules:{
{
test: /\.vue$/,
use: ['vue-loader']
}
},
plugins:[
...
new VueLoaderPlugin()
...
]

配置完成后,vue文件就可以正常解析了


image.png


解析CSS文件


需要引入的Loader不止一个



  • 引入的基本Loader: style-loader,css-loader,如有less还需要less-loader

  • postcss-loader 添加不同浏览器的css前缀: 解决部分css语法在不同浏览器的写法不同的弊端


modules.exports = {
modules: {
rules: [{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
] // 解析css文件必须的style-loader,css-loader
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env' // 解决css不同浏览器兼容性
],
],
},
}
}, 'less-loader'
]
},
}
}

拆分css


mini-css-extract-plugin: 把css拆分出来用外链的形式引入css文件,然后会在dist生成css文件,为每一个包含了 CSS 的 JS 文件创建一个 CSS 文件,
ps: 使用该插件不能重复使用style-loader


··· 
plugins: [
...
new MiniCssExtractPlugin({
filename: devMode ? "[name].css" : "[name].[hash].css",
chunkFilename: devMode ? "[id].css" : "[id].[hash].css",
}),
...
],
module:{
rules: [{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader',
'postcss-loader'
] // 解析css文件必须的style-loader,css-loader
}]
}

打包图片,字体,媒体等文件


file-loader: 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件url),并将文件移动到输出的目录中


url-loader 一般与file-loader搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中



{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
use: [{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/image')],
exclude: /node_modules/
},
{
test: /\.(jpg|png|gif)$/i,
use: [{
loader: 'url-loader',
options: {
limit: 10240, // KB
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/image')],
exclude: /node_modules/
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
use: [{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/icon')],
exclude: /node_modules/
},

使用多线程提升构建速度


js是单线程的工程,在构建工程的过程中,要消耗大量的时间在Loader的转换过程中,为了提升构建速度,这里使用了thread-loader将任务拆分为多个线程去处理。 其原理是把任务分配到各个worker线程中,之前多数人会使用happyPack,但webpack官方使用了thread-loader取代happypack。


...
{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', ],
cacheDirectory: true,
}
},
{
loader: 'thread-loader', // 多线程解析模块
options: {
workers: 3, // 开启几个 worker 进程来处理打包,默认是 os.cpus().length - 1
}
}
],
exclude: /node_modules/
}
...

必须使用的插件Plugins


配置plugins必须注意的是,由于我们的模式(mode)区分为development跟production,因此plugins也需要按照实际需要,在config(公用),dev,prod三个配置文件分开加入。


首先先配置公用部分的plugins


公用plugins


清除打包残留文件


每次执行npm run build 会发现dist文件夹里会残留上次打包的文件,在打包输出前清空文件夹


const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')



用外链的形式引入css


当一个html文件里面的css太多,全部把css添加到html中会显得很臃肿,那我们可以用mini-css-extract-plugin 把css拆分成外链引入,为每一个包含了 CSS 的 JS 文件创建一个 CSS 文件。


需要留意的是不能跟style-loader同时使用,下面用了hash


const MiniCssExtractPlugin = require("mini-css-extract-plugin");
....
plugins: [
new MiniCssExtractPlugin({
filename: devMode ? "[name].css" : "[name].[hash].css",
chunkFilename: devMode ? "[id].css" : "[id].[hash].css",
}, {
filename: devMode ? "[name].css" : "[name].[hash].less",
chunkFilename: devMode ? "[id].css" : "[id].[hash].less",
}),
]
....

module:{
rules:[{
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader',
'postcss-loader'
]
},
}]
}
....

生产打包后的html


wepback必备的插件之一,上述举例也有提到。
主要是生产打包后的html, 同时由于main.js文件会随机生成新的hash名字,html在引入main.js文件时频繁改名字会很浪费时间,此插件会自动同步改文件名


const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html') // 同时引入main.js的hash名字
}),
],

开发环境Dev


热更新: webpack-dev-server


当我们修改文件的内容时,要重新build一次才能看到变化,这样对开发的效率不友好。


需要留意的是,webpack-dev-server只是在开发环境搭建一个服务帮助开发人员提高开发效率,实现了实时更新的功能,在生产环境并不会用到这一个插件.


同时注意在plugins中加入webpack自带的HotModuleReplacementPlugin。


webpack-dev-server这个插件功能十分强大,官方文档有详细的记录
(webpack.docschina.org/configurati…)


npm i webpack-dev-server --save-dev

module.exports = {
devServer: {
// 基本目录
static: {
directory: path.join(__dirname, 'dist'),
},
// 自动压缩代码
compress: true,
port: 9000,
// 自动打开浏览器
open: true,
// 热加载,默认是true
hot: true,
},
plugins: [
new Webpack.HotModuleReplacementPlugin()
]
}

生产环境Prod


由于生产环境对性能的要求跟开发不同,需要引入的插件比较丰富,也更需要对项目构建有更高的熟悉程度


压缩Js文件


webpack mode设置production的时候会自动压缩js代码。原则上不需要引入terser-webpack-plugin进行重复工作。但是optimize-css-assets-webpack-plugin压缩css的同时会破坏原有的js压缩,所以这里我们引入terser-webpack-plugin进行压缩


option很多,使用了dropconsole去除打印的内容


const TerserPlugin = require("terser-webpack-plugin");

...

optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress:{
drop_console: true
}
}
}),
],
},
...


压缩CSS


前面有使用mini-css-extract-plugin的插件去拆分css,但这个插件并不能压缩CSS体积,
使用css-minimizer-webpack-plugin 可以压缩css的体积,但不同的是它是被加入到optimization的minimizer中,跟上述的js压缩插件共同作用


const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
...
optimization: {
minimizer: [
new CssMinimizerPlugin(),
new TerserPlugin({
terserOptions: {
compress:{
drop_console: true
}
}
}),
],
},
...


抽离第三方模块


使用DllReferencePlugin把不需要经常变更的静态文件抽离出来,譬如element-ui组件,这样每次打包的时候就不会再去重新打包选中的静态文件了。


如此一来,当我们修改代码后,webpack只需要打包项目的代码而不需要重复去编译没有发生改变的第三方库。这样当我们没有升级第三方库时,webpack就不会再对这些库进行打包,从而提升项目构建的速度。


首先我们在同级目录下新建文件webpack.dll.config.js,在entry的vendor里面配置了vue跟element-ui。


// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
// 每次执行npm run build 会发现dist文件夹里会残留上次打包的文件,在打包输出前清空文件夹
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')

module.exports = {
mode: 'production',
// 你想要打包的模块的数组
entry: {
vendor: ['vue','element-plus']
},
output: {
path: path.resolve(__dirname, '../public/vendor'), // 打包后文件输出的位置,要在静态资源里面避免被打包转义
filename: '[name].dll.js',
library: 'vendor_library'
// 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: 'vendor_library',
context: __dirname
})
]
};



同时在packake.json里面配置dll的命令


"scripts":{
"dll": "webpack --config build/webpack.dll.config.js",
}

最后在webpack.prod.js 加入配置项


···
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
···

随后执行命令npm run dll
在public/vendor会出现一个vendor.dll.js文件,我们需要在html文件引入这个文件.


<body>
<!-- dll插件的配置路径,注意是打包后的 -->
<script src="./vendor/vendor.dll.js"></script>
<!-- <img src="../assets/image/logo192.png" alt=""> -->
<!-- <img src="../assets/image/loginbg.png" alt=""> -->

<div id="app"></div>
</body>

配置完毕,这样我们在不需要更新第三方包的时候可以不用执行npm run dll,然后直接执行npm run build/dev的时候就会发现构建速度有所提高。


分析打包后的文件


使用webpack-bundle-analyzer,启动项目后会打开一个展示各个包的大小。从图中可以看出来,es6.promise.js这个包



  • stat size: webpack 从入口文件打包递归到的所有模块体积

  • parsed size: 解析与代码压缩后输出到dist目录的体积

  • gzipped size: 开启Gzip之后的体积


image.png


总结


webpack身为前端必备的一项技能,各位在学会基础的配置之后,千万别忘了因地制宜,看看哪些插件更适合自己的项目哦


作者:广州交租公
来源:juejin.cn/post/7277490138518159379
收起阅读 »

亲测实战大屏项目的适配方法

web
背景 想必在平常开发中,遇到要适配各种不同尺寸的屏幕、分辨率,甚至一套代码要兼容PC端到手机端对前端来说是比较头疼和麻烦的事情。 如果要在时间很紧迫的情况下去一套代码,要在大屏、PC和移动端都可运行,且界面不乱,也不超出范围;确实挺头疼。 我这里倒是有一个亲测...
继续阅读 »

背景


想必在平常开发中,遇到要适配各种不同尺寸的屏幕、分辨率,甚至一套代码要兼容PC端到手机端对前端来说是比较头疼和麻烦的事情。

如果要在时间很紧迫的情况下去一套代码,要在大屏、PC和移动端都可运行,且界面不乱,也不超出范围;确实挺头疼。

我这里倒是有一个亲测且实践的一个特别简单的方法,不用换算,设计给多少px前端就写几px,照着UI设计无头脑的搬就可,而且在大屏、电脑pc端、手机移动端,一套代码,就可运行,保证界面不乱,不超出屏幕范围。

这个方法是在我第一次写大屏项目时,用上的,当时时间也是很紧迫,但是项目上线后,对甲方来说很完美无缺了,一次过。

最后发现它在手机端也能看。

如果好奇想知道究竟是什么方法可以这么的丝滑去兼容大屏乃至任何尺寸任何分辨率的设备的小伙伴们,不妨接着往下看。


常见兼容各尺寸的方法


css媒体查询


@media screen

eg:


@media screen and (max-width: 1700px){
//屏幕尺寸小于1700的样式
}

这种方式,太麻烦,如果要求高,就要分的越细,几乎每个尺寸都要兼顾,写进去,实在实在是太太麻烦了。


viewport


<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no">

网页头部加入这段代码,网页宽度自动适应屏幕的宽度。

这种方式大屏中不太适合,这种更加适合移动端。

亲测,大屏不太适合。


rem


要换算,而且每次要根据屏幕尺寸的变化而去重新给一个新的根节点字体大小值。

还是麻烦,嗯,也是不能完美适合大屏。


vw和vh


把整个屏幕宽高分成一百分,一个vw就是1%的宽,一个vh就是10的高。

也是麻烦,而且也不能完美适配大屏。


百分百%


同vw,vh一样也不能完美适配大屏。


重点来了


最终我用了缩放,完美解决了大屏适配问题。

它不仅可以在任何尺寸任何分辨率下,去相对完美的展示,在PC甚至移动端也是可以看的。


如何?


jsx页面里:


 useEffect(() => {
//全屏模式
fullScreens();
//首次加载应用,设置一次
setScale();
//调试比例
window.addEventListener('resize', Listen(setScale, 100));
}, []);
//调试比例
// 监听手机窗口变化,重新设置
function Listen(fn, t) {
const delay = t || 100;
let timer;
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
const context = this;
timer = setTimeout(() => {
timer = null;
fn.apply(context, args);
}, delay);
};
}

// 获取放大缩小比例
function getScale() {
const w = window.innerWidth / 1920;
const h = window.innerHeight / 1080;
return w < h ? w : h;
}

// 设置比例
function setScale() {
const bigMainDom = document.getElementById('bigMain');
bigMainDom && (bigMainDom.style.transform = 'scale(' + getScale() + ') translate(-50%, -50%)');
}

css:


 width:1920px;
height:1080px;
transform-origin: 0 0;
position: absolute;
left: 50%;
top: 50%;

想要哪个页面去做这种兼容,就可在哪个页面去这样设置;

利用查找id的方式找到对应的页面元素(这里只要能查找到元素就行,怎么写随自己),然后给对应的元素添加样式 transform ,去设置比例


getScale方法里面:

先求一下宽高的比例,因为设计稿一般使用的分辨率是1920和1080,所以这里使用这个取比例;

最后返回比例的时候,取比例比较小的,这样可以保证都显示出来;

只不过相对小的尺寸会出现空白,大的尺寸那个占满全屏;

js这里的translate(-50%, -50%) 和css里面的position: absolute; left:50%;top: 50%;transform-origin:0 0可保证画面在最中间


注意点:transform-origin:0 0这个很重要哦,用来设置动画的基点,没有这个,怎么都不会居中正常显示的哦。


否则就会出现以下这几个情况:


image.png


image.png


而:


// 设置比例
function setScale() {
const bigMainDom = document.getElementById('bigMain');
bigMainDom && (bigMainDom.style.transform = 'scale(' + getScale() + ') translate(0, 0)');
}

则会:


image.png
Listen方式,是专门设备窗口大小变化时,就会自动调用setScale方法,我设置的是每100秒监听一次,相当于做了一个自适应的处理;

setScale在页面加载时调用了一次,然后也加了一个resize方法;

宽度、长度也设置一样,我写的是1920 * 1080


出现问题,两边有空白?


屏幕没有铺满,左右有空白,这是因为设备上面有地址栏,而导致的;

因为地址栏占了一定的高度,但是设计稿把这块高度没有算进去,因此,实际的整体高度会比设计稿的大,而宽度是一样的,为了能够全部展示,就以大的为主,因此,宽度相对比就会出现空白啦;

image.png


解决


设置成全屏就好啦;


image.png

完美;

因为我的笔记本电脑分辨率高,所以,电脑默认缩放设置的是125%,而不是100%,默认设置为100%整体字体什么都都会变小,观看都不高,所以电脑默认设置为125%啦, 无论电脑默认是多少百分比,只要设置成全屏,都是全部铺满的。


image.png


设置全屏的代码


进入全屏、退出全屏、当前是否全屏模式的代码:

commonMethod.js:


//进入全屏
static fullScreen = (element) => {
if (window.ActiveXObject) {
var WsShell = new ActiveXObject('WScript.Shell');
WsShell.SendKeys('{F11}');
}
//HTML W3C 提议
else if (element.requestFullScreen) {
element.requestFullScreen();
}
//IE11
else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
// Webkit (works in Safari5.1 and Chrome 15)
else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
// setTimeout(()=>{
// console.log(isFullScreen());
// },100)
}
// Firefox (works in nightly)
else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
}
};
//退出全屏
static fullExit = () => {
//IE ActiveXObject
if (window.ActiveXObject) {
var WsShell = new ActiveXObject('WScript.Shell');
WsShell.SendKeys('{F11}');
}
//HTML5 W3C 提议
else if (element.requestFullScreen) {
document.exitFullscreen();
}
//IE 11
else if (element.msRequestFullscreen) {
document.msExitFullscreen();
}
// Webkit (works in Safari5.1 and Chrome 15)
else if (element.webkitRequestFullScreen) {
document.webkitCancelFullScreen();
}
// Firefox (works in nightly)
else if (element.mozRequestFullScreen) {
document.mozCancelFullScreen();
}
};
//当前是否全屏模式
static isFullScreen = () =>{
return document.fullScreen ||
document.mozFullScreen ||
document.webkitIsFullScreen ||
document.msFullscreenElement;
}

这是react项目页面里引用的封装好的方法:


 //全屏
const fullScreens = () => {
if (!CommonMethod.isFullScreen()) {
Modal.info({
content: '开启全屏模式,体验更佳',
okText: '确定',
maskClosable: true,
mask: false,
// centered:true,
width: '200px',
height: '100px',
className: 'fullScreenModal',
onOk() {
CommonMethod.fullScreen(document.getElementsByTagName('html')[0]);
},
onCancel() {
CommonMethod.fullScreen(document.getElementsByTagName('html')[0]);
},
});
}
};

然后大屏页面第一次加载时,调用一次全屏模式方法即可,fullScreens


useEffect(() => {
//全屏模式
fullScreens();
}


注意点


如果在使用transform,缩放的地方,就不能使用其他定位,你会发现使用其他定位(绝对定位、相对定位、固定定位等)会失效;

使用antd里的模态框,就会失效哦。


完结


设置缩放的方法可以试试,并且在不同设备尺寸和分辨率下,大屏、pc、手机端都可以试一波。

回头我把自己上面那个项目在不同设备下运行的截图整理一下,补发出来。

对移动端有点要求的其实还会需要重新写一套代码的,我上面这种方式移动端只是样式没有乱也没有超出屏幕而已,毕竟这个只是专门给大屏做的代码,那么大的尺寸怎么的在手机端看也不会很符合的


作者:浅唱_那一缕阳光
来源:juejin.cn/post/7232229178278903865
收起阅读 »

Vue 2 最后之舞“天鹅挽歌”

web
大家好,这里是大家的林语冰。老粉都知道,我们之前已经在《Vue 2 将死》中已经提及 Vue 2 今年年底将全面停止维护,且最终版即将发布,只打补丁,不再增量更新任何新功能。圣诞节前夕,平安夜之际,Vue 团队正式官宣 —— Vue 2 最后一个补丁版本&nb...
继续阅读 »

大家好,这里是大家的林语冰。老粉都知道,我们之前已经在《Vue 2 将死》中已经提及 Vue 2 今年年底将全面停止维护,且最终版即将发布,只打补丁,不再增量更新任何新功能。

圣诞节前夕,平安夜之际,Vue 团队正式官宣 —— Vue 2 最后一个补丁版本 Vue@2.7.16 正式发布,版本代号“Swan Song”(天鹅挽歌)。

01-swan.png

地球人都知道,去年 Vue 2 官宣了最后一个次版本 Vue@2.7.x,如今 Vue 2 官宣最后一个补丁版本 Vue@2.7.16,也算是为 Vue 2 的最后之舞画上惊叹号!此去经年,再无 Vue 2。

虽然但是,前端踏足之地,Vue 亦生生不息,此乃“Vue 之意志”。故本期《前端翻译计划》一起来重温 Vue@2.7 的官方博客,为 Vue 生态的未来规划未雨绸缪。

00-wall.png

今天我们十分鸡冻地官宣,Vue 2.7(版本代号“火影忍者”)正式发布!

尽管 Vue 3 现在已经是默认版本,但我们特别理解,由于依赖兼容性、浏览器支持的要求或带宽不足无法升级,仍然有一大坨用户被迫滞留在 Vue 2。在 Vue 2.7 中,我们向后移植了 Vue 3 中的某些特色功能,Vue 2 爱好者也可以有福同享。

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Vue 2.7 "Naruto" Released

向后移植的功能

  • 组合式 API
  • SFC 
收起阅读 »

前端部署真的不简单

web
现在大部分的中小型公司部署前端代码都是比较简单的,主要步骤如下: 首先,通过脚手架提供的命令npm run build打包前端代码,生成dist文件夹; 最后,将dist文件夹丢给后台开发人员放在他们的工程里面,随后台一起部署;现在普遍是前后端分开部署,因此,...
继续阅读 »

现在大部分的中小型公司部署前端代码都是比较简单的,主要步骤如下:


首先,通过脚手架提供的命令npm run build打包前端代码,生成dist文件夹;


最后,将dist文件夹丢给后台开发人员放在他们的工程里面,随后台一起部署;现在普遍是前后端分开部署,因此,利用nginx起一个web服务器,将dist文件夹放到指定的路径下,配置下nginx访问路径,对于请求接口使用proxy_pass进行转发,解决跨域的问题。


更加高端一点的操作,是利用CI/CD + Docker进行自动化部署。


但是,你是否真的想过前端部署真的就这么简单吗?


这其实是一个非常严肃且复杂的问题,因为这关系到线上生产环境的稳定


有一天,从自知乎上看到一篇张云龙大佬在2014年写的文章,非常有启发,即使这篇文章距离现在有快10年了,但是其中的思想仍然熠熠生辉。


因为写的真的是太好了,为了让更多的人看到,所以大部分内容直接就照搬过来,为了让自己加深印象。如果想看原文,原文网址在这里。


那让我们从原始的前端开发讲起。


下图是一个 index.html 页面和它的样式文件 a.css,无需编译,本地预览,丢到服务器,等待用户访问。


image.png


哇,前端这么简单,门槛好低啊。这也是前端有太多人涌入进来的原因。


接着,我们访问页面,看到效果,再查看一下网络请求,200!不错,太完美了!


image.png


那么,研发完成。。。。了么?


等等,这还没完呢!


对于像 BAT 这种公司来说,那些变态的访问量和性能指标,将会让前端一点也不好玩。


看看那个 a.css 的请求,如果每次用户访问页面都要加载,是不是很影响性能,很浪费带宽啊,我们希望最好这样:


image.png


利用304,让浏览器使用本地缓存。


但,这样也就够了吗?


不够!


304叫协商缓存,这玩意还是要和服务器通信一次,我们的优化级别是变态级,所以必须彻底灭掉这个请求,要变成这样:


image.png


强制浏览器使用本地缓存(cache-control/expires),不要和服务器通信。


好了,请求方面的优化已经达到变态级别,那问题来了:你都不让浏览器发资源请求了,这缓存咋更新


很好,相信有人想到了办法:通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新资源


像这样:


image.png


下次上线,把链接地址改成新的版本,这就更新资源了。


问题解决了么?当然没有,思考这种情况:


image.png


页面引用了3个 css 文件,而某次上线只改了其中的a.css,如果所有链接都更新版本,就会导致b.cssc.css的缓存也失效,那岂不是又有浪费了?


不难发现,要解决这种问题,必须让url的修改与文件内容关联,也就是说,只有文件内容变化,才会导致相应url的变更,从而实现文件级别的精确缓存控制


什么东西与文件内容相关呢?


我们会很自然的联想到利用数据摘要要算法对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。


OK,那我们把 url 改成带摘要信息的:


image.png


这回再有文件修改,就只更新那个文件对应的 url 了,想到这里貌似很完美了。你觉得这就够了么?


图样图森破!


现代互联网企业,为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径:


image.png


好了,当我要更新静态资源的时候,同时也会更新 html 中的引用吧,就好像这样:


image.png


这次发布,同时改了页面结构和样式,也更新了静态资源对应的url地址。现在重点来了,现在要发布代码上线,亲爱的前端研发同学,你来告诉我,咱们是先上线页面,还是先上线静态资源



这里的静态资源不仅仅包括css文件,也包括图片,以及不怎么经常变的资源。




  1. 先部署动态页面,再部署静态资源:在二者部署的时间间隔内,如果有用户访问页面,就会在新的页面结构中加载旧的资源,并且把这个旧版本的资源当做新版本缓存起来,其结果就是:用户访问到了一个样式错乱的页面,除非手动刷新,否则在资源缓存过期之前,页面会一直执行错误。

  2. 先部署静态资源,再部署动态页面:在部署时间间隔之内,有旧版本资源本地缓存的用户访问网站,由于请求的页面是旧版本的,资源引用没有改变,浏览器将直接使用本地缓存,这种情况下页面展现正常;但没有本地缓存或者缓存过期的用户访问网站,就会出现旧版本页面加载新版本资源的情况,导致页面执行错误,但当页面完成部署,这部分用户再次访问页面又会恢复正常了。


好的,上面一坨分析想说的就是:先部署谁都不成!都会导致部署过程中发生页面错乱的问题。


所以,访问量不大的项目,可以让研发同学苦逼一把,等到半夜偷偷上线,先上静态资源,再部署页面,看起来问题少一些。这也是很多公司的部署方案。


但是,大公司超变态,没有这样的绝对低峰期,只有相对低峰期。


所以,为了稳定的服务,还得继续追求极致啊!


这个奇葩问题,起源于资源的 覆盖式发布,用待发布资源覆盖已发布资源,就有这种问题。


解决它也好办,就是实现 非覆盖式发布


image.png


看上图,用文件的摘要信息来对资源文件进行重命名,把摘要信息放到资源文件发布路径中,这样,内容有修改的资源就变成了一个新的文件发布到线上,不会覆盖已有的资源文件。上线过程中,先全量部署静态资源,再灰度部署页面,整个问题就比较完美的解决了。


因为很多前端开发同学不怎么接触部署,对灰度部署不太熟悉,下面将介绍下什么是灰度部署。


软件开发一般都是一个版本一个版本的迭代。新版本上线前都会经过测试,但就算这样,也不能保证上线了不出问题。


所以,在公司里上线新版本代码一般都是通过灰度系统。灰度系统可以把流量划分成多份,一份走新版本代码,一份走老版本代码。


image.png


而且灰度系统支持设置流量的比例,比如可以把走新版本代码的流程设置为 5%,没啥问题了再放到 10%,50%,最后放到 100% 全量。这样可以把出现问题的影响降到最低。


不然一上来就全量,万一出了线上问题,那就是大事故。


另外,灰度系统不止这一个用途,比如,产品不确定某些改动是不是有效的,就要做 AB 实验,也就是要把流量分成两份,一份走 A 版本代码,一份走 B 版本代码。


那这样的灰度系统是怎么实现的呢?其实很多都是用 nginx 实现的。


nginx 是一个反向代理的服务,用户请求发给它,由它转发给具体的应用服务器。


image.png


它的过程如下图所示:


image.png


首先,需要对流量进行染色,即对这个用户进行标注,让这个用户访问服务1,另外的用户访问服务2。染色的方式有很多,可以通过cookie来完成。不同的用户携带的cookie是不同的。第一染色的时候,所有的用户都访问服务1。


然后,第二次访问的时候,nginx根据用户携带的cookie进行转发到不同的服务,这样就完成了灰度访问。


好了,灰度部署就介绍到这里,回到原文讲的先全量部署静态资源,再灰度部署页面,这是什么意思呢?


首先,部署静态资源的时候,不要删除原来的静态资源,而是把新的静态资源发复制过去,因为文件名用摘要算法重命名的,所以不会发生重名的问题。


其次,灰度部署动态页面,也就是一部分用户访问老的页面,一部分用户访问新的页面。访问老页面的用户请求的还是老资源,直接使用缓存。访问新页面的用户访问新资源,此时新资源已经部署完成,所以不会访问老的资源,导致页面出现错误。


最后,根据访问情况,利用灰度系统,逐渐把访问老页面的用户过渡到访问新页面上。


所以,大公司的静态资源优化方案,基本上要实现这么几个东西:



  1. 配置超长时间的本地缓存:节省带宽,提高性能

  2. 采用内容摘要作为缓存更新依据:精确的缓存控制

  3. 静态资源CDN部署:优化网络请求

  4. 更资源发布路径实现非覆盖式发布:平滑升级


全套做下来,就是相对比较完整的静态资源缓存控制方案了,而且,还要注意的是,静态资源的缓存控制要求在前端所有静态资源加载的位置都要做这样的处理


是的,所有!


什么js、css自不必说,还要包括js、css文件中引用的资源路径,由于涉及到摘要信息,引用资源的摘要信息也会引起引用文件本身的内容改变,从而形成级联的摘要变化,大概就是:


image.png


到这里本文结束了,我们已经了解了前端部署中关于静态资源缓存要面临的优化和部署问题,新的问题又来了:这™让工程师怎么写码啊!!!


这又会扯出一堆有关模块化开发、资源加载、请求合并、前端框架等等的工程问题。


妈妈,我再也不玩前端了。。。。


作者:小p
来源:juejin.cn/post/7316202725330796571
收起阅读 »

封装一个丝滑的评分组件

web
效果 实现 找到喜欢的图标 http://www.iconfont.cn/ 打开 iconfont,随便搜个点赞,仅需一个点赞即可,节省内存,另一个用CSS完成 下载解压 取出如下文件,在项目入口导入 iconfont.css,或者统一放入一个文件导入也...
继续阅读 »

效果


score.gif


实现


找到喜欢的图标


http://www.iconfont.cn/


打开 iconfont,随便搜个点赞,仅需一个点赞即可,节省内存,另一个用CSS完成


image.png


下载解压


image.png


取出如下文件,在项目入口导入 iconfont.css,或者统一放入一个文件导入也行


image.png


编写组件


一个点赞,就叫Like


一个点踩,就叫Hate


Hate点踩组件


先写属性,就几个,不用注释也明白


分别是



  • 绑定值 modelValue

  • 默认颜色 defaultColor

  • 激活颜色 activeColor

  • 大小 size


image.png


HTML给个容器,再加个固定格式(xxx-container)的容器类名,防止样式穿透


再把iconfont样式加上,图标就出来了


这些用VSCode自定义代码片段都能自动生成


image.png


现在是这样的图标,也就是点赞


image.png


那么怎样让它变成点踩手势呢?答案是rotate旋转


加上下图类名有用吗? 答案是没用,他并不会旋转


image.png


为什么呢?因为i标签是行盒模型,必须变成块盒才行


方式有很多,不过我就爱flex,优点很多,这里就不赘述了


image.png


这不就转起来了吗


image.png


动效


加个鼠标移入变色效果,考虑到一会还有另一个点赞组件,那就写个通用的sass


接收一个激活颜色和默认颜色参数


image.png


导入并使用,下面用了个v-bind绑定当前被点击的颜色,下面来实现逻辑


image.png


上面接收了一个modelValue,类型是布尔值,当他为真时就把颜色改为激活颜色


于是就理所当然的使用计算属性


image.png


事件


当被点击或hover时,就激活图标颜色,现在还差点击


点击事件要做 3 件事



  1. 改变父组件的值

  2. 改变颜色

  3. 实现开头的丝滑动画


image.png


这里用一个showAnimation变量控制动画展示


再改变父组件的值,父组件的值一变,自己的颜色也会跟着变


还差个动画,只要让动画在其中一段不停反复横跳,即可实现国际友好手势


image.png


但这样有问题,只有第一次触发动画才有动效,后续需要改变showAnimation的值


那么怎么知道动画什么时候结束呢?


答案是事件onanimationend,只要在这个事件把动画关掉,点击时开启即可


image.png


ok,实现最主要的组件了,另一个点赞就是复制改东改西,大家都会


score.gif


组合组件


接下来需要把点赞和点踩组合一起,那就叫Gr0up


需要实现逻辑



  1. 根据父组件的值,动态展示

  2. 点击时传递一个事件,告诉父组件,究竟是点赞还是点踩

  3. 提供一个值,锁定按钮,因为点完之后一般都不能反悔

  4. 提供样式设置,传递到子组件


类型


传递一个固定类型的值,才能分辨是什么操作


image.png


null就是初始状态,无操作,另外俩见名知义


那么现在需要编写自身的状态


image.png



  • wasChoose是否有按钮被选中

  • disabled当被选中,同时父组件完成(done),就禁用


样式


image.png


初始化


建议写代码都提供一个入口,后面有空出一期如何像写诗一般写代码


image.png


事件


当被点击时,改变两个状态,没什么好说的,完成


image.png


测试


test.gif



源码: gitee.com/cjl2385/dig…



作者:寅时码
来源:juejin.cn/post/7316321509857034280
收起阅读 »

“来同学在用户点击后退的时候加个弹窗做个引导”

web
文章起因这篇文章的起因来源于一个需求,产品:“我需要在浏览器点击后退的时候,加一个弹框引导用户做其他的操作”,老实讲这一块之前研究了很多次通过popstate去监听,但是始终有问题没有达到理想的状态或者说并没有完全研究透彻,本来想直接回复做不了砍掉这里吧,后来...
继续阅读 »

文章起因

这篇文章的起因来源于一个需求,产品:“我需要在浏览器点击后退的时候,加一个弹框引导用户做其他的操作”,老实讲这一块之前研究了很多次通过popstate去监听,但是始终有问题没有达到理想的状态或者说并没有完全研究透彻,本来想直接回复做不了砍掉这里吧,后来出于职业道德的遵守还是说试试看吧!

后来

之后在网上遨游了一段时间找了很多实现方案最后发现有一个Api叫做prompt,他来自于react-router,正好 项目中目前使用的路由就是react-router

import { Prompt } from 'react-router' //v5.2.0版本

我不太清楚看到这篇文章的同学有没有用到过这个Api,我大致介绍一下用法

const App = () =>{
const [isBlocking, setIsBlocking] = useState(true)

return <>
<Prompt
//这里是个Boolean 控制是否监听浏览器的后退 默认监听
when={isBlocking}
message={(_, action) =>
{
if (action === 'POP') {
Dialog.show({ //普普通通的弹框而已,,,,
title: '提示',
actions: [
{
key: 'back',
text: '回到浪浪山',
onClick() {
history.go(-1)
//用户选择按钮之后关闭掉监听
setIsBlocking(false)
},
},
{
key: 'saveAndBack',
text: '去往光明顶',
onClick: async () => null
},
{
key: 'cancle',
text: intl.t('取消'),
},
],
})
return false // 返回false通知该组件需要拦截到后退的操作 将执行权交给用户
}
return true //返回true 正常后退不做拦截
}}
>Prompt>

{/* ...内容部分 */}

}
export default App

上面这样可以实现我的需求,但是因为之前研究过这好一阵子那会并没找到这个Api,现在找到了本着一种知其然知其所以然的态度,深究一下内部到底是怎么实现可以禁止浏览器后退的,如果你不知道就跟着一起寻找一下吧,可能需要占用一杯咖啡的时间☕️

| Prompt

最初的想法就是直接去看Prompt实现的源码就好了,看看是怎么实现的这里的逻辑 其实在看之前内心是有一些猜测的觉得可能是下面这样做的

  • 可能是有一些浏览器提供的api但是我不清楚可以直接做到禁止后退,然后Prompt内部有调用
  • 或者是先添加了浏览器记录然后在后退的时候监听又删除

git上找react-router源码,注意要切换到对应的版本V5.2.0,免得对不上号

react-router5.2.0版本对应的链接🔽

github.com/remix-run/r…

从这个链接点击去之后我们可以看到Prompt方法,主要是下面这一段我们捡重点解析一下

  1. 获取history上面的block
  2. 在初始化阶段将我们的message传递到block中执行,并且获取到当前的unblock
  3. 离开的时候执行self.release()执行卸载操作
/**
* The public API for prompting the user before navigating away from a screen.
*/

function Prompt({ message, when = true }) {
return (

{context => {
invariant(context, "You should not use outside a ");

if (!when || context.staticContext) return null;

// 这个context是当前使用的环境上下文我们内部路由跳转用的history的包
const method = context.history.block;

return (
{
//初始化的阶段执行该方法
self.release = method(message);
}}
onUpdate={(self, prevProps) => {
if (prevProps.message !== message) {
self.release();
self.release = method(message);
}
}}
onUnmount={self => {
self.release();
}}
message={message}
/>
);
}}

);
}

既然看到了这里再继续看下 Lifecycle 方法其实这个里面就是执行了一些生命周期,这里就不解析了贴个图大家自己看下吧,都能看懂

github.com/remix-run/r…

image.png

到了这里可能有些疑惑,这里好像并没有什么其他操作就是调用了一个block,起初我以为block是原生history上面提供的方法后来去查了一下api发现上面并没有这个方法,此刻就更加好奇block内部的实现方式了

因为我们项目的上下文里面使用的history是来自于一个npm包,后面就继续看看这个history内部怎么实现的

import { createBrowserHistory } from 'history'
const history = createBrowserHistory()

createBrowserHistory

传送门在这里👇🏻感兴趣的同学直接去看源码

github.com/remix-run/h…

直接看里面的block方法

let isBlocked = false

const block = (prompt = false) => {
const unblock = transitionManager.setPrompt(prompt)

if (!isBlocked) {
checkDOMListeners(1)
isBlocked = true
}

return () => {
if (isBlocked) {
isBlocked = false
checkDOMListeners(-1)
}

return unblock()
}
}

现在来分析一下上面的代码

  1. prompt是我们传进来的弹框组件或者普通字符串信息
  2. transitionManager是一个工厂函数将prompt存到了函数内部以便后面触发的时候使用
  3. checkDOMListeners去做挂载操作就是监听popstate那一步
  4. 返回出去的函数是在外面在离开的时候做销毁popstate监听的

现在按照上面的步骤在逐步做代码分析,下面会看具体的部分有些不重要的地方会做删减

| transitionManager.setPrompt

  • 可以看到工厂函数里面存储了prompt
  • 销毁的时机是在上面unblock的时候执行重置prompt
  const createTransitionManager = () => {
let prompt = null
const setPrompt = (nextPrompt) => {
prompt = nextPrompt
return () => {
if (prompt === nextPrompt)
prompt = null
}
}
return {
setPrompt,
}
}

| checkDOMListeners

  • 上面默认传了1初始化的时候会进行popstate监听
  • 离开的时候传了-1移除监听
let listenerCount = 0
const checkDOMListeners = (delta) => {
listenerCount += delta
if (listenerCount === 1) {
addEventListener(window, PopStateEvent, handlePopState)
} else if (listenerCount === 0) {
removeEventListener(window, PopStateEvent, handlePopState)
}
}

| handlePopState

  • 调用getDOMLocation获取到一个location
const handlePopState = (event) => {
handlePop(getDOMLocation(event.state))
}
  • getDOMLocation 内部调用createLocation创建了一个
  • createLocation内部大家感兴趣可以自己去看一下,没有什么可讲的就是创建一些常规的属性
  • 比如state、pathname之类的

const getDOMLocation = (historyState) => {
const { key, state } = (historyState || {})
const { pathname, search, hash } = window.location

let path = pathname + search + hash

if (basename)
path = stripBasename(path, basename)

return createLocation(path, state, key)
}

那我们现在知道getDOMLocation是创建一个location并且传递到了handlePop方法内部现在去看看这个内部都干了啥

| handlePop

  • 我们要看的主要在else里面
  • confirmTransitionTo是我们上面提到的工厂函数里面的一个方法
  • 该方法内部执行了Prompt并返回了Prompt执行后的结果

let forceNextPop = false

const handlePop = (location) => {
if (forceNextPop) {
forceNextPop = false
setState()
} else {
const action = 'POP'

transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({ action, location })
} else {
revertPop(location)
}
})
}
}

敲黑板 重点来了!!!

现在来看下confirmTransitionTo内部的代码

const confirmTransitionTo = (location, action, getUserConfirmation, callback) => {
if (prompt != null) {
const result = typeof prompt === 'function' ? prompt(location, action) : prompt

if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
getUserConfirmation(result, callback)
} else {
callback(true)
}
} else {
// 重点在这里,result是我们调用block时候的返回参数 true or false
// 如果返回false 那浏览器回退将被禁止 反之则正常
callback(result !== false)
}
} else {
callback(true)
}
}

所以现在回到上面的handlePop函数我们就能推测出如果我们回调中返回的false,说明我们想阻止浏览器的回退操作,那么执行的就是revertPop方法(其实名字大家可能也能猜出来 恢复 pop操作😂)

| revertPop

  • delta的逻辑是计算从开始到目前为止走过的路径做个差值计算
  • 这个时候正常来讲delta应该是1
  • 我们看最后一个逻辑就好这里是禁止撤回的重点
  • 当delta为1的时候就执行了go(1)
  • go方法内部实际调用了window.history.go(n)
const revertPop = (fromLocation) => {
const toLocation = history.location

let toIndex = allKeys.indexOf(toLocation.key)

if (toIndex === -1)
toIndex = 0

let fromIndex = allKeys.indexOf(fromLocation.key)

if (fromIndex === -1)
fromIndex = 0

const delta = toIndex - fromIndex

if (delta) {
forceNextPop = true
//window.history.go
go(delta)
}
}

之前我看到这里有个疑问就是如果最后的结果只是调用了go的话,那这个好像我们自己监听也可以实现一下于是就有了以下代码

function History() {
this.handelState = function (event) {
history.go(1)
}

this.block = function (Prompt) {
window.addEventListener('popstate', this.handelState)
return () => {
window.removeEventListener('popstate', this.handelState)
}
}
}

const newHistory = new History()

等到我实验的时候发现页面回退确实阻止住了,但是会闪一下上一个页面,给大家举个例子

Step1
我从PageA页面一路push到PageC
PageA -> PageB -> PageC

Step2
从PageC页面点击返回,之后页面的过程是这样的
PageC -> PageB -> PageC

就是说我本应该在PageC点击撤回,理想的效果是就停留在了PageC页面,但是目前效果是先回到了PageB因为我使用了go(1)就又回到了PageC,相当于在点击回退的时候多加载了PageB页面

这使我又陷入了沉思,其实研究到这里如果不把这个弄懂之前的努力就白费了,抱着这种想法又扎到了history代码中遨游一番

之后光看代码捋逻辑对这确实有些迷茫,没有办法只能开始调试history的源码了,这里比较简单,history源码下载下来之后做几个步骤

  1. 安装history相关依赖package
  2. 启动服务会有一个本地域名

image.png

  1. 之后在你真实项目中引入这个资源开始做调试

后面其实就一直打log和断点不断调试history源码查看执行路径,发现了问题所在

刚才上面提到的handlePop方法内部有一段代码那会忽略掉了,就是ok为true的时候,因为之前一直关注false的情况忽略了这里,后面把这个里面就研究了一下才明白其中的原委

if (ok) {
setState({ action, location })
} else {
revertPop(location)
}

| setState

这个方法做了几件事

  • 更新本地history状态,nextState可以理解为下一个目标地址其中包含location和action
  • 更新本地history的长度这里没有完全搞懂为什么要更新一下长度,但是猜测可能是为了和原生history状态一直保持同步吧防止出现意外情况
  • 这里又看到了transitionManager工厂函数,此时调用的notifyListeners这个就是解决我们上面的谜团所在
const setState = (nextState) => {

// 1.更新本地history状态
Object.assign(history, nextState)

history.length = globalHistory.length

//2.更新依赖history相关的订阅方法
transitionManager.notifyListeners(
history.location,
history.action
)
}

| notifyListeners

notifyListeners更新订阅的方法,我直接把这块代码贴出来了,一个发布订阅模式没什么好讲的

  let listeners = []

const appendListener = (fn) => {
let isActive = true

const listener = (...args) => {
if (isActive)
fn(...args)
}

listeners.push(listener)

return () => {
isActive = false
listeners = listeners.filter(item => item !== listener)
}
}

const notifyListeners = (...args) => {
listeners.forEach(listener => listener(...args))
}

重点的地方是react-router内部会调用history中的listen,这个listen方法会调用上面的appendListener进行存储,之后在合适的时间点执行react-router中传递的方法

这些方法的入参是目标页面的history属性(location,action),在接收到参数的时候根据参数中的location更新当前的页面

现在可以得出结论我们上面的例子不能成功的原因,是因为我们在执行的过程中没有绕过setState(因为此刻没有能让ok返回false的操作)所以当我们页面路径变更的时候自然页面也会更新

最后整体捋一下这个流程吧

到这里其实细心的同学会发现浏览器的回退我们确实是控制不了的只要点击了就一定会执行后退的操作。在history中针对block方法来说做的事情其实就下面这几步

  1. 封装了一个自己本地的history,当然跳转能力等还是依赖原生的history
  2. 在URL路径变更的时候history可以决定是否通知单页面应用的路由
  3. 如果通知了就相当于我们的ok是true,需要页面也更新一下
  4. 如果未通知相当于ok是false,就不需要页面更新

这就是为什么history里面的block为什么可以用go就能实现当前页面回退,本质上浏览器历史记录确实回退了,但是history并没有通知应用页面更新,并且继续执行go(1)这样给用户看到的视角就是页面没有回退并且url也没有变化

结论

其实在使用的时候history还是有一些问题如果当前页面reload了,那么revertPop里面的go就不会执行,因为此时的delta是0,这样就会导致即使页面没有变化但是url更新成了上一个记录

说一下我的看法这可能是这个history遗留的bug也可能是有意而为之但是也不重要了我们搞懂了原理就好。

其实浏览器后退加监听的行为感觉还是一个比较高频的需求操作,后面我打算自己写一个插件专门就做后退拦截的操作~

到底了------

今天圣诞节了 祝你们圣诞节快乐 Merry Christmas🎄🎄🎄 !!!希望都能收到喜欢的礼物🎁


作者:零狐冲
来源:juejin.cn/post/7316202778790477834

收起阅读 »

浅谈Vue3的逻辑复用

web
Vue3的逻辑复用 使用了 Vue3 Composables 之后,逻辑复用比之前要顺手很多,这里说说前端开发最常用的场景,管理页面的列表查询和增删改查的逻辑复用,欢迎大家共同探讨。 用免费的 render 服务搭建了个在线的预览地址,源码在这里,用了免费的 ...
继续阅读 »

Vue3的逻辑复用


使用了 Vue3 Composables 之后,逻辑复用比之前要顺手很多,这里说说前端开发最常用的场景,管理页面的列表查询和增删改查的逻辑复用,欢迎大家共同探讨。


用免费的 render 服务搭建了个在线的预览地址源码在这里,用了免费的 node 环境和免费的 pg 数据库,对这部分有兴趣的可以看看我以前的分享,我写了个部署 spring-boot 的分享,使用 node 就更简单了。


可能每个人的具体工作内容不一致,但是应该都完成过这样的工作内容:



  1. 列表查询,带分页和过滤条件

  2. 新增,修改,查看,删除

  3. 进行一些快捷操作,比如:激活、通过


这些最基础的工作可能占用了我们很大的时间和精力,下面来讨论下如何逻辑复用,提高工作效率


需求分析


一个后台管理中心,绝大部分都是这种管理页面,那么需要:



  • 首先是封装一些通用的组件,这样代码量最低也容易保持操作逻辑和 UI 的一致性

  • 其次要封装一些逻辑复用,比如进入页面就要进行一次列表查询,翻页的时候需要重新查询

  • 最后需要有一些定制化的能力,最基本的列需要自定义,页面的过滤条件和操作也不一样


统一复用



  1. 发起 http 请求

  2. 展示后端接口返回的信息,有成功或者参数校验失败等信息


列表的查询过程



  1. 页面加载后的首次列表查询

  2. 页面 loading 状态的维护

  3. 翻页的逻辑和翻页后的列表重新查询

  4. 过滤条件和模糊搜索的逻辑,还有对应的列表重新查询


新增 Item、查询 Item、修改 Item



  1. form 在提交过程的禁用状态

  2. 发起网络请求

  3. 后端接口返回的信息提示

  4. 列表重新查询


删除 Item



  1. 删除按钮状态的维护(需要至少一个选中删除按钮才可用)

  2. 发起网络请求

  3. 后端接口返回的信息提示

  4. 列表重新查询


定制化的内容



  1. table 的列数据

  2. item 的属性,也就是具体的表单

  3. 快捷操作:改变 user 激活状态

  4. 列表的过滤条件


成果展示


img



  1. 打开页面会进行一次列表查询

  2. 翻页或者调整页面数量,会进行一次列表查询

  3. 右上角的是否激活筛选状态变更会进行一次列表查询

  4. 右上角模糊搜索,输入内容点击搜索按钮会进行一次列表查询

  5. 点击左上角的新增,弹出表单对话框,进行 item 的新增

  6. 点击操作的“编辑”按钮,弹出表单对话框,对点击的 item 进行编辑

  7. 点击“改变状态”按钮,弹出确认框,改变 user 的激活状态

  8. 选中列表的 checkbox,可以进行删除


代码直接贴在下面了,使用逻辑复用完成以上的内容一共 200 多行,大部分是各种缩进,可以轻松阅读,还写了一个 Work 的管理,也很简单,证明这套东西复用起来没有任何难度。


<template>
<div class="user-mgmt">
<biz-table
:operations="operations"
:filters="filters"
:loading="loading"
:columns="columns"
:data="tableData"
:pagination="pagination"
:row-key="rowKey"
:checked-row-keys="checkedRowKeys"
@operate="onOperate"
@filter-change="onFilterChange"
@update:checked-row-keys="onCheckedRow"
@update:page="onUpdatePage"
@update:page-size="onUpdatePageSize"
/>

<user-item :show="showModel" :item-id="itemId" @model-show-change="onModelShowChange" @refresh-list="queryList" />
</div>
</template>

<script setup name="user-mgmt">
import { h, ref, computed } from 'vue';
import urls from '@/common/urls';
import dayjs from 'dayjs';
import { NButton } from 'naive-ui';
import BizTable from '@/components/table/BizTable.vue';
import UserItem from './UserItem.vue';
import useQueryList from '@/composables/useQueryList';
import useDeleteList from '@/composables/useDeleteList';
import useChangeUserActiveState from './useChangeUserActiveState';

// 自定义列数据
const columns = [
{
type: 'selection'
},
{
title: '姓',
key: 'firstName'
},
{
title: '名',
key: 'lastName'
},
{
title: '是否激活',
key: 'isActive',
render(row) {
return row.isActive ? '已激活' : '未激活';
}
},
{
title: '创建时间',
key: 'createdAt'
},
{
title: '更新时间',
key: 'updatedAt'
},
{
title: '操作',
key: 'actions',
render(row) {
return [
h(
NButton,
{
size: 'small',
onClick: () => onEdit(row),
style: { marginRight: '5px' }
},
{ default: () => '编辑' }
),
h(
NButton,
{
size: 'small',
onClick: () => onChangeUserActiveState(row),
style: { marginRight: '5px' }
},
{ default: () => '改变状态' }
)
];
}
}
];

// 自定义右上角筛选
const filters = ref([
{
label: '是否激活',
type: 'select',
value: '0',
class: 'filter-select',
options: [
{
label: '全部',
value: '0'
},
{
label: '已激活',
value: '1'
},
{
label: '未激活',
value: '2'
}
]
},
{
label: '',
type: 'input',
placeholder: '请输入姓氏',
value: ''
}
]);

// 筛选变化,需要重新查询列表
const onFilterChange = ({ index, type, value }) => {
filters.value[index].value = value;
queryList();
};

// 自定义查询列表参数
const parmas = computed(() => {
return {
isActive: filters.value[0].value,
like: filters.value[1].value
};
});

// 封装好的查询列表方法和返回的数据
const { data, loading, pagination, onUpdatePage, onUpdatePageSize, queryList } = useQueryList(urls.user.user, parmas);

// 经过处理的列表数据,用于在 table 中展示
const tableData = computed(() =>
data.value.list.map(item => {
return {
...item,
createdAt: dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss'),
updatedAt: dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss')
};
})
);

// 删除列表相关逻辑封装
const { checkedRowKeys, onCheckedRow, deleteList } = useDeleteList({
content: '确定删除选中的用户?',
url: urls.user.userDelete,
callback: () => {
queryList();
}
});

// 列表中的快捷操作
const operations = computed(() => {
return [
{
name: 'create',
label: '新增',
type: 'primary'
},
{
name: 'delete',
label: '删除',
disabled: checkedRowKeys.value.length === 0
}
];
});

// 触发操作函数
const onOperate = function (name) {
operationFucs.get(name)();
};

// 新创建 item
const create = () => {
showModel.value = true;
itemId.value = 0;
};

const onModelShowChange = () => {
showModel.value = !showModel.value;
};

const itemId = ref(0);

// 控制模态对话框
const showModel = ref(false);

// 编辑 item
const onEdit = row => {
itemId.value = row.id;
showModel.value = true;
};

const { changeUserActiveState } = useChangeUserActiveState();

// 改变激活状态
const onChangeUserActiveState = row => {
changeUserActiveState({
id: row.id,
isActive: !row.isActive,
loading,
callback: () => {
queryList();
}
});
};

// 指定 table 的 rowKey
const rowKey = row => row['id'];

// operation 函数集合
const operationFucs = new Map();
operationFucs.set('create', create);
operationFucs.set('delete', deleteList);
</script>

<style lang="scss">
.user-mgmt {
height: 100%;
.filter-select {
.biz-select {
width: 100px;
}
}
}
</style>



作者:hezf
来源:juejin.cn/post/7316349124600315940
收起阅读 »

h5端调用手机摄像头实现扫一扫功能

web
一、前言 最近有遇到一个需求,在h5浏览器中实现扫码功能,其本质便是打开手机摄像头定时拍照,特此做一个记录。主要技术栈采用的是vue2,使用的开发工具是hbuilderX。 经过测试发现部分浏览器并不支持打开摄像头,测试了果子,华子和米,发现夸克浏览器无法打...
继续阅读 »

一、前言



最近有遇到一个需求,在h5浏览器中实现扫码功能,其本质便是打开手机摄像头定时拍照,特此做一个记录。主要技术栈采用的是vue2,使用的开发工具是hbuilderX。


经过测试发现部分浏览器并不支持打开摄像头,测试了果子,华子和米,发现夸克浏览器无法打开摄像头实现功能。



h5调用摄像头实现扫一扫只能在https环境下,亦或者是本地调试环境!!


image.png


二、技术方案



经过一番了解之后,找到了两个方案


1.使用html5-qrcode(对二维码的精度要求较高,胜在使用比较方便,公司用的是vue2,因此最终采用此方案)


2.使用vue-qrcode-reader(对vue版本和node有一定要求,推荐vue3使用,这里就不展开说了)



三、使用方式


image.png


当点击中间的扫码时,设置isScanning属性为true,即可打开扫码功能,代码复制粘贴即可放心‘食用’。


使用之前做的准备



通过npm install html5-qrcode 下载包


引入 import { Html5Qrcode } from 'html5-qrcode';



html结构
<view class="reader-box" v-if="isScaning">
<view class="reader" id="reader"></view>
</view>

所用数据
data(){
return{
html5Qrcode: null,
isScaning: false,
}
}


methods方法
openQrcode() {
this.isScaning = true;
Html5Qrcode.getCameras().then((devices) => {
if (devices && devices.length) {
this.html5Qrcode = new Html5Qrcode('reader');
this.html5Qrcode.start(
{
facingMode: 'environment'
},
{
focusMode: 'continuous', //设置连续聚焦模式
fps: 5, //设置扫码识别速度
qrbox: 280 //设置二维码扫描框大小
},
(decodeText, decodeResult) => {
if (decodeText) { //这里decodeText就是通过扫描二维码得到的内容
this.action(decodeText) //对二维码逻辑处理
this.stopScan(); //关闭扫码功能
}
},
(err) => {
// console.log(err); //错误信息
}
);
}
});
},

stopScan() {
console.log('停止扫码')
this.isScaning = false;
if(this.html5Qrcode){
this.html5Qrcode.stop();
}
}

css样式
.reader-box {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
}

.reader {
width:100%;
// width: 540rpx;
// height: 540rpx;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

四、最终效果


image.png


如有问题,欢迎指正,若此文对您有帮助,不要忘记收藏+关注!


作者:极客转
来源:juejin.cn/post/7316795553798815783
收起阅读 »

关于晚上十点和男生朋友打电话调试vue源码那些事

web
简介朋友昨晚让我帮忙看怎么调试vue源码,我当时就不困了啊。兴致勃勃搞了半小时以失败告终。小白我可是永不言败的,早上鼓捣了俩小时总算写了点东西出来。想调试vue源码的小伙伴可以试试我这个思路fork源码首先肯定是要把vue/core代码fork一份到自己的仓库...
继续阅读 »

简介

朋友昨晚让我帮忙看怎么调试vue源码,我当时就不困了啊。兴致勃勃搞了半小时以失败告终。小白我可是永不言败的,早上鼓捣了俩小时总算写了点东西出来。想调试vue源码的小伙伴可以试试我这个思路

fork源码

首先肯定是要把vue/core代码fork一份到自己的仓库 这样后续有改动可以提交一下 也可以从源码一键同步

ps:github.com/baicie/vuej… 我的代码在这里可以参考一下

装包

pnpm i @pnpm/find-workspace-packages @pnpm/types -wD

ps:可以先看看pnpm与monorepo

在根目录执行上述命令装一下依赖-wD含义是在workspace的根安装开发依赖

脚本编写

首先在packages下执行pnpm creata vite创建一个vue项目

然后在scripts文件夹下创建dev.ts

import type { Project as PnpmProject } from '@pnpm/find-workspace-packages'
import { findWorkspacePackages } from '@pnpm/find-workspace-packages'
import type { ProjectManifest } from '@pnpm/types'
import { execa } from 'execa'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import color from 'picocolors'
import { scanEnums } from './const-enum'

export type Manifest = ProjectManifest & {
buildOptions: {
name?: string
compat?: boolean
env?: string
formats: ('global' | 'cjs' | 'esm-bundler' | 'esm-browser')[]
}
}

interface Project extends PnpmProject {
manifest: Manifest
}

const pkgsPath = path.resolve(process.cwd(), 'packages')
const getWorkspacePackages = () => findWorkspacePackages(pkgsPath)

async function main() {
scanEnums()
// 获取所有的包 除了private与没有buildOptions的包
const pkgs = (
(await getWorkspacePackages()).filter(
item => !item.manifest.private
) as Project[]
).filter(item => item.manifest.buildOptions)

await buildAll(pkgs)
}

async function buildAll(target: Project[]) {
// 并行打包
return runParallel(Number.MAX_SAFE_INTEGER, target, build)
}

async function runParallel(
maxConcurrent:
number,
source: Project[],
buildFn: (project: Project) =>
void
) {
const ret: Promise<void>[] = []
const executing: Promise<void>[] = []
for (const item of source) {
const p = Promise.resolve().then(() => buildFn(item))
// 封装所有打包任务
ret.push(p)

//
if (maxConcurrent <= source.length) {
const e: any = p.then(() => executing.splice(executing.indexOf(e), 1))
executing.push(e)
if (executing.length >= maxConcurrent) await Promise.race(executing)
}
}

return Promise.all(ret)
}

async function build(project: Project) {
const pkg = project.manifest
// 获取相对路径 包名
const target = path.relative(pkgsPath, project.dir)
if (pkg.private) {
return
}

const env = (pkg.buildOptions && pkg.buildOptions.env) || 'development'
await execa(
'rollup',
[
`-c`,
// 给rollup配置文件传递参数 watch 监听文件变化
'--watch',
'--environment',
[`NODE_ENV:${env}`, `TARGET:${target}`, `SOURCE_MAP:true`]
.filter(Boolean)
.join(',')
],
{ stdio: 'inherit' }
)
}

main().catch(err => {
console.error(color.red(err))
})

然后在根目录的package.json scripts 添加如下

"my-dev": "tsx scripts/dev.ts"

上述脚本主要是为了扫描工作目录下所有有意义的包并执行rollup打包命令(主要也就为了加一下watch没毛病)

验证

终端打开上吗创建的vite项目然后修改package.json里面的vue

 "vue": "workspace:*"

修改后根目录执行pnpm i建立软连接

1.根目录终端执行pnpm run my-dev

2.vite-project执行pnpm run dev

3.去runtime-core/src/apiCreateApp.ts createAppAPI 的 createApp 方法加一句打印

4.等待根目录终端打包完毕

5.去看看浏览器控制台有没有打印

按理说走完上述流出应该有打印出来哈哈 

优化更快?

然后就是想要快点因为我电脑不太行每次修改要等1.5~2s,然后我就想到了turbo,看了官网发现可行就试了试

修改如下

1. pnpm i turbo -wD

  1. 修改上述的my-dev "my-dev": "tsx scripts/dev.ts --turbo"
  2. 启动并验证

快了一点但不多

最后

新人多多关照哈哈 如果你想变强 b站 掘金搜索小满zs!


作者:白cie
来源:juejin.cn/post/7316539952475996194

收起阅读 »

前端要对用户的电池负责!

web
前言 我有一个坏习惯:就是下班之后不关电脑,但是电脑一般来说第二天电量不会有什么损失。但是后来突然有一天它开不起来了,上午要罢工?那也不好吧,也不能在光天化日之下划水啊;聪明的我还是找到了原因:电池耗尽了;于是赶紧来查一查原因,到底是什么程序把电池耗尽了呢? ...
继续阅读 »

前言


我有一个坏习惯:就是下班之后不关电脑,但是电脑一般来说第二天电量不会有什么损失。但是后来突然有一天它开不起来了,上午要罢工?那也不好吧,也不能在光天化日之下划水啊;聪明的我还是找到了原因:电池耗尽了;于是赶紧来查一查原因,到底是什么程序把电池耗尽了呢?


这里我直接揭晓答案:是一个Web应用,看来这个锅必须要前端来背了;我们来梳理一下js中有哪些耗电的操作。


js中有哪些耗电的操作


js中有哪些耗电的操作?我们不如问问浏览器有哪些耗电的操作,浏览器在渲染一个页面的时候经历了GPU进程、渲染进程、网络进程,由此可见持续的GPU绘制、持续的网络请求和页面刷新都会导致持续耗电,那么对应到js中有哪些操作呢?


Ajax、Fetch等网络请求


单只是一个AjaxFetch请求不会消耗多少电量,但是如果有一个定时器一直轮询地向服务器发送请求,那么CPU一直持续运转,并且网络进程持续工作,它甚至有可能会阻止电脑进行休眠,就这样它会一直轮询到电脑电池不足而关机;
所以我们应该尽量少地去做轮询查询;


持续的动画


持续的动画会不断地触发GPU重新渲染,如果没有进行优化的话,甚至会导致主线程不断地进行重排重绘等操作,这样会加速电池的消耗,但是js动画与css动画又不相同,这里我们埋下伏笔,等到后文再讲;


定时器


持续的定时器也可能会唤醒CPU,从而导致电池消耗加速;


上面都是一些加速电池消耗的操作,其实大部分场景都是由于定时器导致的电池消耗,那么下面来看看怎么优化定时器的场景;


针对定时器的优化


先试一试定时器,当我们关闭屏幕时,看定时器回调是否还会执行呢?


let num = 0;
let timer = null;
function poll() {
clearTimeout(timer);
timer = setTimeout(()=>{
console.log("测试后台时是否打印",num++);
poll();
},1000*10)
}

结果如下图,即使暂时关闭屏幕,它仍然会不断地执行:


未命名.png


如果把定时器换成requestAnimationFrame呢?


let num = 0;
let lastCallTime = 0;
function poll() {
requestAnimationFrame(() =>{
const now = Date.now();
if(now - lastCallTime > 1000*10){
console.log("测试raf后台时是否打印",num++);
lastCallTime = now;
}
poll();
});
}

屏幕关闭之前打印到了1,屏幕唤醒之后才开始打印2,真神奇!


未命名.png


当屏幕关闭时回调执行停止了,而且当唤醒屏幕时又继续执行。屏幕关闭时我们不断地去轮询请求,刷新页面,执行动画,有什么意义呢?因此才出现了requestAnimationFrame这个API,浏览器对它进行了优化,用它来代替定时器能减少很多不必要的能耗;


requestAnimationFrame的好处还不止这一点:它还能节省CPU,只要当页面处于未激活状态,它就会停止执行,包括屏幕关闭、标签页切换;对于防抖节流函数,由于频繁触发的回调即使执行次数再多,它的结果在一帧的时间内也只会更新一次,因此不如在一帧的时间内只触发一次;它还能优化DOM更新,将多次的重排放在一次完成,提高DOM更新的性能;


但是如果浏览器不支持,我们必须用定时器,那该怎么办呢?


这个时候可以监听页面是否被隐藏,如果隐藏了那么清除定时器,如果重新展示出来再创建定时器:


let num = 0;
let timer = null;
function poll() {
clearTimeout(timer);
timer = setTimeout(()=>{
console.log("测试后台时是否打印",num++);
poll();
},1000*10)
}

document.addEventListener('visibilitychange',()=>{
if(document.visibilityState==="visible"){
console.log("visible");
poll();
} else {
clearTimeout(timer);
}
})

针对动画优化


首先动画有js动画、css动画,它们有什么区别呢?


打开《我的世界》这款游戏官网,可以看到有一个keyframes定义的动画:


未命名.png


切换到其他标签页再切回来,这个扫描二维码的线会突然跳转;


因此可以得出一个结论:css动画在屏幕隐藏时仍然会执行,但是这一点我们不好控制。


js动画又分为三种种:canvas动画、SVG动画、使用js直接操作css的动画,我们今天不讨论SVG动画,先来看一看canvas动画(MuMu官网):


未命名.png


canvas动画在页面切换之后再切回来能够完美衔接,看起来动画在页面隐藏时也并没有执行;


那么js直接操作css的动画呢?动画还是按照原来的方式一直执行,例如大话西游这个官网的”获奖公示“;针对这种情况我们可以将动画放在requestAnimationFrame中执行,这样就能在用户离开屏幕时停止动画执行


上面我们对大部分情况已经进行优化,那么其他情况我们没办法考虑周到,所以可以考虑判断当前用户电池电量来兼容;


Battery Status API兜底


浏览器给我们提供了获取电池电量的API,我们可以用上去,先看看怎么用这个API:


调用navigator.getBattery方法,该方法返回一个promise,在这个promise中返回了一个电池对象,我们可以监听电池剩余量、电池是否在充电;


navigator.getBattery().then((battery) => {
function updateAllBatteryInfo() {
updateChargeInfo();
updateLevelInfo();
updateChargingInfo();
updateDischargingInfo();
}
updateAllBatteryInfo();

battery.addEventListener("chargingchange", () => {
updateChargeInfo();
});
function updateChargeInfo() {
console.log(`Battery charging? ${battery.charging ? "Yes" : "No"}`);
}

battery.addEventListener("levelchange", () => {
updateLevelInfo();
});
function updateLevelInfo() {
console.log(`Battery level: ${battery.level * 100}%`);
}

battery.addEventListener("chargingtimechange", () => {
updateChargingInfo();
});
function updateChargingInfo() {
console.log(`Battery charging time: ${battery.chargingTime} seconds`);
}

battery.addEventListener("dischargingtimechange", () => {
updateDischargingInfo();
});
function updateDischargingInfo() {
console.log(`Battery discharging time: ${battery.dischargingTime} seconds`);
}
});


当电池处于充电状态,那么我们就什么也不做;当电池不在充电状态并且电池电量已经到达一个危险的值得时候我们需要暂时取消我们的轮询,等到电池开始充电我们再恢复操作


后记


电池如果经常放电到0,这会影响电池的使用寿命,我就是亲身经历者;由于Web应用的轮询,又没有充电,于是一晚上就耗完了所有的电,等到再次使用时电池使用时间下降了好多,真是一次痛心的体验;于是后来我每次下班前将Chrome关闭,并关闭所有聊天软件,但是这样做很不方便,而且很有可能遗忘;


如果每一个前端都能关注到自己的Web程序对于用户电池的影响,然后尝试从定时器和动画这两个方面去优化自己的代码,那么我想应该就不会发生这种事情了。注意:桌面端程序也有可能是披着羊皮的狼,就是披着原生程序的Web应用;


参考:
MDN


作者:蚂小蚁
来源:juejin.cn/post/7206331674296746043
收起阅读 »

为什么我的页面鼠标一滑过,布局就错乱了?

web
前言 这天刚到公司,测试同事又在群里@我: 为什么页面鼠标一滑过,布局就错乱了? 以前是正常的啊? 刷新后也是一样 快看看怎么回事 同时还给发了一段bug复现视频,我本地跑个例子模拟下 可以看到,鼠标没滑过是正常的,鼠标一滑过图片就换行了,鼠标移出又正常了。...
继续阅读 »

前言


这天刚到公司,测试同事又在群里@我:

为什么页面鼠标一滑过,布局就错乱了?

以前是正常的啊?

刷新后也是一样

快看看怎么回事


同时还给发了一段bug复现视频,我本地跑个例子模拟下


GIF 2023-8-28 11-23-25.gif


可以看到,鼠标没滑过是正常的,鼠标一滑过图片就换行了,鼠标移出又正常了。


正文


首先说下我们的产品逻辑,我们产品的需求是:要滚动的页面,滚动条不应该占据空间,而是悬浮在滚动页面上面,而且滚动条只在滚动的时候展示。


我们的代码是这样写:


  <style>
.box {
width: 630px;
display: flex;
flex-wrap: wrap;
overflow: hidden; /* 注意⚠️ */
height: 50vh;
box-shadow: 0 0 5px rgba(93, 96, 93, 0.5);
}
.box:hover {
overflow: overlay; /* 注意⚠️ */
}
.box .item {
width: 200px;
height: 200px;
margin-right: 10px;
margin-bottom: 10px;
}
img {
width: 100%;
height: 100%;
}
</style>
<div class="box">
<div class="item">
<img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
</div>
<div class="item">
<img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
</div>
<div class="item">
<img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
</div>
<div class="item">
<img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
</div>
</div>

我们使用了overflow属性的overlay属性,滚动条不会占据空间,而是悬浮在页面之上,刚好吻合了产品的需求。


image.png


然后我们在滚动的区域默认是overflow:hidden,正常时候不会产生滚动条,hover的时候把overflow改成overlay,滚动区域可以滚动,并且不占据空间。


简写代码如下:


  .box {
overflow: hidden;
}
.box:hover {
overflow: overlay;
}

然后我们是把它封装成sass的mixins,滚动区域使用这个mixins即可。


上线后没什么问题,符合预期,获得产品们的一致好评。


直接这次bug的出现。


排查


我先看看我本地是不是正常的,我打开一看,咦,我的布局不会错乱。


然后我看了我的chrome的版本,是113版本


然后我问了测试的chrome版本,她是114版本


然后我把我的chrome升级到最新114版本,咦,滑过页面之后我的布局也乱了。


初步判断,那就有可能是chrome版本的问题。


去网上看看chrome的升级日志,看看有没有什么信息。


image.png


具体说明:


image.png


可以看到chrome114有了一个升级,就是把overflow:overlay当作overflow:auto的别名,也就是说,overlay的表现形式和auto是一致的。


实锤了,auto是占据空间的,所以导致鼠标一滑过,布局就乱了。


其实我们从mdn上也能看到,它旁边有个删除按钮,然后滑过有个tips:


image.png


解决方案


第一种方式


既然overflow:overlay表现形式和auto一致,那么我们得事先预留出滚动条的位置,然后设置滚动条的颜色是透明的,滑过才显示颜色,基本上也能符合产品的需求。


代码如下:


  // 滚动条
::-webkit-scrollbar {
background: transparent;
width: 6px;
height: 6px;
}
// 滚动条上的块
::-webkit-scrollbar-thumb {
background-clip: padding-box;
background-color: #d6d6d6;
border: 1px solid transparent;
border-radius: 10px;
}
.box {
overflow: auto;
}
.box::-webkit-scrollbar-thumb {
background-color: transparent;
}
.box:hover::-webkit-scrollbar-thumb {
background-color: #d6d6d6;
}

第二种方式


如果有用element-ui,它内部封装了el-scrollbar组件,模拟原生的滚动条,这个也不占据空间,而是悬浮,并且默认不显示,滑过才显示。



element-ui没有el-scrollbar组件的文档说明,element-plus才有,不过都可以用。



总结


这次算是踩了一个坑,当mdn上面提示有些属性已经要废弃⚠️了,我们就要小心了,尽量不要使用这些属性,就算当前浏览器还是支持的,但是不能保证未来某个时候浏览器会不会废弃这个属性。(比如这次问题,改这些问题挺费时间的,因为用的地方挺多的。)


因为一旦这些属性被废弃了,它预留的bug就等着你去解决,谨记!


作者:答案cp3
来源:juejin.cn/post/7273875079658209319
收起阅读 »

工作踩坑之在浏览器关闭/刷新前发送请求

web
丑话说在前 丑话说在前:当前的浏览器完全没有任何一个可靠、通用、准确区分用户关闭和刷新操作的API。 因此,如果你是单纯想在用户关闭时发送请求,那么没有任何完美答案。如果你只是想在谷歌Chrome、360浏览器的急速模式等一众基于谷歌Chrome浏览器套皮浏览...
继续阅读 »

丑话说在前


丑话说在前:当前的浏览器完全没有任何一个可靠、通用、准确区分用户关闭和刷新操作的API


因此,如果你是单纯想在用户关闭时发送请求,那么没有任何完美答案。如果你只是想在谷歌Chrome360浏览器的急速模式等一众基于谷歌Chrome浏览器套皮浏览器上实现,那么在下面我会提供一个简单的方法,但是Edge并不支持该方法。Edge是真牛啊,青出于蓝胜于蓝?


先来看看浏览器在刷新/关闭时的顺序


为了帮助理解我区分浏览器关闭和刷新操作的方法,先来看看浏览器在关闭/刷新时的执行顺序吧~


在浏览器关闭或刷新页面时,onbeforeunloadonunload 事件的执行顺序是固定的。



  1. 当用户关闭浏览器标签、窗口或者输入新的 URL 地址时,首先会触发 onbeforeunload 事件。

  2. onbeforeunload 事件处理完成后,如果用户选择离开页面(关闭或刷新),则会触发 onunload 事件。


因此,onbeforeunload 事件在用户决定离开页面之前执行,而 onunload 事件在用户离开页面之后执行。这两个事件提供了在用户离开页面前后执行代码的机会,可以用于执行清理操作或者提示用户确认离开等操作。通过对比两个事件的执行时间差,我们就可以简单判断浏览器的关闭或刷新行为啦。


简易判断Chrome浏览器关闭或刷新行为的方法


let beforeTime = 0,
leaveTime = 0;
// 获取浏览器onbeforeunload时期的时间戳
window.onbeforeunload = () => {
beforeTime = new Date().getTime();
};
window.onunload = () => {
// 对比onunload时期和onbeforeunload时期的时间差值
leaveTime = new Date().getTime() - beforeTime;
if (leaveTime < 5) {
// 如果小于5就是关闭
// 你可以在这发送请求
} else {
// 如果大于5就是刷新
// 你可以在这发送请求
}
};

注意:经过本人的测试,该方法仅支持Chrome浏览器等,Edge浏览器无论是关闭还是刷新,时间戳差均小于5ms,而谷歌Chrome浏览器的时间戳差均大于5ms,为7ms-8ms左右。环境不同亦有可能导致结果不同。


详见他人的测试结果图:


1703417937476.jpg


如何发送请求


既然已经区分了Chrome浏览器的关闭和刷新行为,那么该如果发送请求呢?


发送请求的方式主要有以下几种:


1. 使用 Navigator.sendBeacon()


该方法主要用于将统计数据发送到 Web 服务器,同时避免了用传统技术,如XMLHttpRequest所导致的各种问题。


他的使用方法也很简单:


navigator.sendBeacon(url, data);

// url参数表明 data 将要被发送到的网络地址
// data (可选) 参数是将要发送的 ArrayBuffer、ArrayBufferView、Blob、DOMString、FormData 或 URLSearchParams 类型的数据。
// 当用户代理成功把数据加入传输队列时,sendBeacon() 方法将会返回 true,否则返回 false。

怎么样?简简单单一行代码即可实现发送可靠的异步请求,同时不会延迟页面的卸载或影响下一导航的载入性能。但是别忽略的他很重要的一个特点数据是通过 POST 请求发送的。


2. 使用 fetch + keepalive


该方法用于发起获取资源的请求。它返回的是一个 promise。他支持 POST 和 GET 方法,配合 keepalive 参数,可以实现浏览器关闭/刷新行为前发送请求。keepalive可用于超过页面的请求。可以说keepalive就是 Navigator.sendBeacon() 的替代品。


fetch('url',{
method:'GET',
keepalive:true
})

3. 直接发送异步请求


由于从Chrome83开始,onunload里面不允许执行同步的XHR,所以同步请求自然是无法实现的,但是一部请求是可以实现的。但是异步请求发送到设备的成功率并非百分之百,因此并不推荐,也不在此赘述。


总结


以上便是浏览器关闭/刷新前发送请求的几种方法,而我是采用了 fetch + alive 尝试简单实现浏览器仅关闭时发送请求,具体实现代码如下:


let beforeTime = 0,
leaveTime = 0;
window.onbeforeunload = () => {
beforeTime = new Date().getTime();
};
window.onunload = () => {
leaveTime = new Date().getTime() - beforeTime;
if (leaveTime <= 5) {
fetch('/logout.do',{
method:'GET',
keepalive:true
})
}
};

经测试,使用效果如下


使用该方法对于各浏览器的测试结果


浏览器/测试方法关闭tab页关闭浏览器任务管理器关闭刷新
Chrome登出登出登出未登出
Edge登出未登出登出登出
360急速模式登出登出登出未登出
360兼容模式白屏白屏白屏白屏
IE白屏白屏白屏白屏

浏览器/测试方法关闭tab页关闭浏览器任务管理器关闭刷新
Chrome
Edge××
360急速模式
360兼容模式××××
IE××××

小小的吐槽:


后端感知web退出本就不推荐由前端来处理,更优解为 持续ping 或者后端 心跳机制发包 来检测。


既然设备那边提出了这个请求,我们web这也就努力挣扎一下,把测试结果发给评审人员评审一下吧~


作者:bachelor98
来源:juejin.cn/post/7315846825344647194
收起阅读 »

妙用 CSS counters 实现逐层缩进

web
妙用 CSS counters 实现逐层缩进 之前使用纯 CSS 实现了一个树形结构,效果如下 其中,展开收起是用到了原生标签details和summary,有兴趣的可以回顾之前这篇文章 CSS 实现树状结构目录 还有一点,树形结构是逐层缩进的,是使用...
继续阅读 »

妙用 CSS counters 实现逐层缩进



之前使用纯 CSS 实现了一个树形结构,效果如下


image-20231221201613974


其中,展开收起是用到了原生标签detailssummary,有兴趣的可以回顾之前这篇文章



CSS 实现树状结构目录



还有一点,树形结构是逐层缩进的,是使用内边距实现的,但是这样会有点击范围的问题,层级越深,点击范围越小,如下


image-20231221201953463


之前的方案是用绝对定位实现的,比较巧妙,但也有点难以理解,不过现在发现了另一种方式也能很好的实现缩进效果,一起看看吧


一、counter() 与 counters()


我们平时使用的一般都是counter,也就是计数器,比如


<ul>
<li>li>
<li>li>
<li>li>
ul>

加上计数器,通常用伪元素来显示这个计数器


ul {
counter-reset: listCounter; /*初始化计数器*/
}
li {
counter-increment: listCounter; /*计数器增长*/
}
li::before {
content: counter(listCounter); /*计数器显示*/
}

这就是一个最简单的计数器了,效果如下


image-20231221203258255


我们还可以改变计数器的形态,比如改成大写罗马数字(upper-roman


li::before {
content: counter(listCounter, upper-roman);
}

效果如下


image-20231221203158970


有关计数器,网上的教程非常多,大家可以自行搜索


然后我们再来看counters(),比前面的counter()多了一个s,叫做嵌套计数器,有什么区别呢?下面来看一个例子,还是和上面一样,只是结构上复杂一些


<ul>
<li>
<ul>
<li>li>
<li>li>
<li>li>
ul>
li>
<li>li>
<li>li>
<li>
<ul>
<li>li>
<li>
<ul>
<li>li>
<li>li>
<li>li>
ul>
li>
ul>
li>
ul>


效果如下


image-20231221204007978


看着好像也不错?但是好像从计数器上看不出层级效果,我们把counter()换成counters(),注意,counters()要多一个参数,表示连接字符,也就是嵌套时的分隔符,如下


li::before {
content: counters(listCounter, '-');
}

效果如下


image-20231221204311891


是不是可以非常清楚的看出每个列表的层级?下次碰到类似的需求就不需要用 JS 去递归生成了,直接用 CSS 渲染,简单高效,也不会出错。


默认ul是有padding的,我们把这个去除看看,变成了这样


image-20231221204528126


嗯,看着这些长短不一的序号,是不是刚好可以实现树形结构的缩进呢?


二、树形结构的逐层缩进


回到文章开头,我们先去除之前的padding-left,会变成这样


image-20231221224113570


完全看不清结构关系,现在我们加上嵌套计数器


.tree details{
counter-reset: count;
}
.tree summary{
counter-increment: count;
}
.tree summary::before{
content: counters(count,"-");
color: red;
}

由于结构关系,目前序号都是1,没关系,只需要有嵌套关系就行,效果如下


image-20231221224810497


**是不是刚好把每个标题都挤过去了?**然后我们把中间的连接线去除,这样可以更方便的控制缩进的宽度


.tree summary::before{
content: counters(count,"");
color: red;
}

效果如下


image-20231221225225369


最后,我们只需要设置这个计数器的颜色为透明就行了


.tree summary::before{
content: counters(count,"");
color: transparent;
}

最终效果如下


image-20231221225607078


这样做的好处是,每个树形节点都是完整的宽度,所以 可以很轻易的实现hover效果,而无需借助伪元素去扩大点击范围


.tree summary:hover{
background-color: #EEF2FF;
}

效果如下


image-20231221225732065


还可以通过修改计数器的字号来调整缩进,完整代码可以访问以下链接:




三、总结一下


以上就是本文的全部内容了,主要介绍了计数器的两种形态,以及想到的一个应用场景,下面总结一下



  1. 逐层缩进用内边距比较容易实现,但是会造成子元素点击区域过小的问题

  2. counter 表示计数器,比较常规的单层计数器,形如 1、2、3

  3. counters 表示嵌套计数器,在有层级嵌套时,会自动和上一层的计数器相叠加,形如1、1-1、1-2、1-2-1

  4. 嵌套计数器会逐层叠加,计数器的字符会逐层增加,计数器所占据的位置也会越来越大

  5. 嵌套计数器所占据的空间刚好可以用作树形结构的缩进,将计数器的颜色设置为透明就可以了

  6. 用计数器的好处是,每个树形节点都是完整的宽度,而无需借助伪元素去扩大点击范围


一个还算实用的小技巧,你学到了吗?


作者:XboxYan
来源:juejin.cn/post/7315850963343671335
收起阅读 »

🔥图片懒加载🔥三种实现方案

web
一、前言 图片懒加载,当图片出现在可视区域再进行加载,提升用户的体验。因为有些用户不会看完图片,全部加载会浪费流量。在网上查阅资料,总结了三种办法,有各自的利弊,下文一一介绍。 方法优点缺点推荐指数设置img loadingh5的属性,没有兼容问题需要已知图片...
继续阅读 »

一、前言


图片懒加载,当图片出现在可视区域再进行加载,提升用户的体验。因为有些用户不会看完图片,全部加载会浪费流量。在网上查阅资料,总结了三种办法,有各自的利弊,下文一一介绍。


方法优点缺点推荐指数
设置img loadingh5的属性,没有兼容问题需要已知图片高度、宽高比⭐️⭐️
IntersectionObserver API无需知道图片高度低版本需引入polyfill⭐️⭐️⭐️
vue-lazyload 自定义指令无需知道图片高度github现存issues较多,没有解决⭐️⭐️

output.gif


二、实现方式及Demo


1. 设置img标签loading属性


loading属性允许两个值:eager立即加载图像(默认值);lazy延迟加载图像。在使用lazy属性的时候,需要设置<img>标签的高度,否则无法懒加载。


注意: 适用于两种场景,图片高度已知、图片宽高比已知。



  • 已知图片高度


<style>
.img-box img {
width: 100%;
height: 700px; /*设置为图片的真实高度*/
}
</style>

<div class="img-box">
<img src="https://i.postimg.cc/GtN3Cs02/1.jpg" loading="lazy" />
<img src="https://i.postimg.cc/hGdKLGdW/2.jpg" loading="lazy" />
<img src="https://i.postimg.cc/T1SkJTbF/3.jpg" loading="lazy" />
<img src="https://i.postimg.cc/wxPFPTtb/4.jpg" loading="lazy" />
<img src="https://i.postimg.cc/FRkGF28x/5.jpg" loading="lazy" />
<img src="https://i.postimg.cc/05JH9wqq/6.jpg" loading="lazy" />
</div>



  • 已知图片宽高比


 <style>
.img-box div {
position: relative;
padding-top: 66%; /* (你的图片的高度/宽度值) */
overflow: hidden;
}
.img-box img {
position: absolute;
top:0;
right:0;
width:100%;
}
</style>

<div class="img-box">
<div>
<img src="https://i.postimg.cc/GtN3Cs02/1.jpg" loading="lazy" />
</div>
<div>
<img src="https://i.postimg.cc/hGdKLGdW/2.jpg" loading="lazy" />
</div>
<div>
<img src="https://i.postimg.cc/T1SkJTbF/3.jpg" loading="lazy" />
</div>
<div>
<img src="https://i.postimg.cc/wxPFPTtb/4.jpg" loading="lazy" />
</div>
<div>
<img src="https://i.postimg.cc/FRkGF28x/5.jpg" loading="lazy" />
</div>
<div>
<img src="https://i.postimg.cc/05JH9wqq/6.jpg" loading="lazy" />
</div>
</div>


2. 使用 IntersectionObserver


IntersectionObserver接口,可以观察DOM节点是否出现在视口,当DOM节点出现在视口中才加载图片。img必须有高度,否则图片默认都在视口中,会将图片全部加载。可以设置img的src为base64白色图片,然后在替换为真实的图片地址。


注意: 不需要预先知道图片的高度,但是有兼容性问题,低版本需要引入intersection-observer polyfill



  • 已知图片高度


<style>
.img-box .lazy-img {
width: 100%;
height: 600px; /*如果已知图片高度可以设置*/
}
</style>

<div class="img-box">
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/d48aed7c991b43d850d011f2299d852e.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/a588b152c79ac60162ecbdf82b060061.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/eacbc2cd4b6ca636077378182bdfcc88.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/751470f4b478450e8556f78cd7dd3d96.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/e4a531bee5694a4a01dee74b18bbfd8b.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/7d8f107e827a7beaa0b9d231bfa4187f.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/4f7586f6b74f2bd0b94004fcbae69856.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/863849e14e7e8903ed4b27fcbdafe8b0.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/d8bb17fe9a7223f35075014ef250e2fa.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
</div>

<script>
function Observer() {
let images = document.querySelectorAll(".lazy-img");
let observer = new IntersectionObserver(entries => {
entries.forEach(item => {
if (item.isIntersecting) {
item.target.src = item.target.dataset.origin; // 开始加载图片,把data-origin的值放到src
observer.unobserve(item.target); // 停止监听已开始加载的图片
}
});
});
images.forEach(img => observer.observe(img));
}
Observer()
</script>

3. 使用vue-lazyload


在vue2中使用时,建议安装npm i vue-lazyload@1.3.3 -s,使用高版本在main.js中全局自定义指令后依然无法使用指令。在vue3中可以使用 npm i vue3-lazy -s



  • 全局注册自定义指令,在页面就可以使用了


// 全局自定义指令
import Vue from 'vue'
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
preLoad: 1,
observer: true // 设置为true,内部使用IntersectionObserver。默认使用
})

/* 在页面中直接使用 */
<div>
<img v-lazy="https://images.djtest.cn/pic/test/d48aed7c991b43d850d011f2299d852e.jpg">
<img v-lazy="https://images.djtest.cn/pic/test/a588b152c79ac60162ecbdf82b060061.jpg">
<img v-lazy="https://images.djtest.cn/pic/test/eacbc2cd4b6ca636077378182bdfcc88.jpg">
<img v-lazy="https://images.djtest.cn/pic/test/751470f4b478450e8556f78cd7dd3d96.jpg">
</div>

作者:起风了啰
来源:juejin.cn/post/7316349850854752294
收起阅读 »

这一年遇到的奇怪bug

web
position sticky 失效 在 Iphone6 plus 上使用 position sticky 不生效 解决办法: position: sticky; position: -webkit-sticky; // 兼容写法需要写在下面 参...
继续阅读 »

position sticky 失效



在 Iphone6 plus 上使用 position sticky 不生效




解决办法:



position: sticky;  
position: -webkit-sticky; // 兼容写法需要写在下面

参考 position sticky 失效 – 有点另类的写法


new Date().toLocaleDateString() 获取当前的日期字符串无效



当系统语言是新加坡英语的时候,使用这个方法获取当前的日期字符串会出现 Invalid Date,toLocaleDateString 是有两个参数的,不指定语言就会出现这个问题,而且只在手机上出现,不太好排查,new Date().toLocaleDateString('en-Us') 调用的时候指定语言就没问题了;



参考 Date.prototype.toLocaleDateString()


两行溢出显示省略号但是部分手机上出现第三行截断痕迹


image.png



例如设置了高度为36px,line-height 18px,但是出现了第三行截断痕迹,应该是文字 baseline 的对其方式问题,试着设置 vertical-align 也不行。解决办法就是不给文字的盒子设置高度,如果一定要个高度兜底,可以在文字的盒子再套一个盒子,在套的那个盒子设置高度。



泰文字体文本溢出隐藏,但是第二行出现截断痕迹



原因,应该是泰语的字体行高要求比较高,暂时的解决办法:加高文本行高



useEffect 首次获取 dom 的 clientHeight 不对



初步感觉是因为 css 样式加载慢了,导致第一次获取到的高度是没有样式的高度,而且又是偶现的;所以在这个组件或者 hooks 重新 render 的时候去获取高度,如果获取到最新的高度发生变化,去同步修改 state 保存的高度。



import { useEffect, useState } from "react";
export default function useTop(){
const [top, setTop]=useState(0);
const [bodyHeight, setBodyHeight] = useState(document.body.clientHeight);
const newestTop = (document.getElementById('nav-header')?.clientHeight || 0) - 1;

if (newestTop !== top) { // nav header height may change
setTop(newestTop);
setBodyHeight(document.body.clientHeight - (newestTop + 1));
}

useEffect(() => {
const nav = document.getElementById('nav-header');
const navHeight = nav?.clientHeight ?? 0;
setTop(navHeight-1);
setBodyHeight(document.body.clientHeight - navHeight);
}, []);
return {top, bodyHeight}
}

一个页面中有两个滚动条,两个滚动条几乎同时触发滚动条的滚动方法,后执行的不生效



两个滚动条,一个使用 scrollBy 方法,另一个使用 scrollIntoView 方法,behavior 属性都为 smooth,这个属性会让滚动条平滑移动,导致滚动条事件一直在触发状态,另一个滚动方法就执行不了了。解决方法:让先执行的方法 behavior 属性为 auto;或者在第一个滚动条结束之后再执行第二个滚动条的方法,可以让第二个方法 setTimeout 100ms 左右,不能超过 300ms,否则用户会感觉卡顿。



iphone6 手机上横向或者纵向滑动不了



原因,可能是dom结构问题,导致低端ios机型没有识别到生成滚动条,导致不能滚动,android 和其他 ios 机型正常;



<div className="list-tabs-wrap">
<div
className="list-tabs"
>

<div className="tab-item">tab1</div>
<div className="tab-item">tab2</div>
<div className="tab-item">tab3</div>
</div>
</div>

.list-tabs-wrap {
width: 100%;
background-color: #fff;
overflow: hidden;
}
.list-tabs {
overflow-x: scroll;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
height: 50px;
background-color: #fff;
}
.list-tabs::-webkit-scrollbar {
display: none;
background-color: transparent;
color: transparent;
width: 0;
height: 0;
}
.tab-item {
width: 50vw;
}


解决办法,新增一个 container 结构,container dom 宽度为 max-content,overflow 拆开写



<div className="list-tabs-wrap">
<div
className="list-tabs"
>

<div className="list-tabs-container">
<div className="tab-item">tab1</div>
<div className="tab-item">tab2</div>
<div className="tab-item">tab3</div>
</div>
</div>
</div>

.list-tabs-container {
overflow-x: scroll; // overflow 拆开写
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
width: max-content; // 纵向设置 height
}


用上面方法解决 iPhone6 滚动条问题后,又出现一个滚动条隐藏样式不生效的问题;解决办法,设置一个外层的盒子,固定高度然后 overflow: hidden,需要滚动的盒子加一个 padding-bottom: 10px,padding 大小看着改,能放下一个滚动条就可以,这样滚动条会出现在padding里,然后又因为外层盒子overflow: hidden 了,所以滚动条和padding都看不到了;愿世界再无 iphone6.



在 Android webview 中,window.location.reload 和 replace 失效


const reload = () => {
const timeStamp = new Date().getTime();
const oldUrl = window.location.href;
const url = `${oldUrl}${oldUrl.includes('?') ? '&' : '?'}timeStamp=${timeStamp}`;
window.location.href = url;
};
const locationReplace = (url) => {
if(history.replaceState) {
history.replaceState(null, document.title, url);
history.go(0);
} else {
location.replace(url);
}
};


部分安卓手机把请求参数的字符串中间的空格转义成+号



发现在 谷歌 Pixel 3 XL 手机上,会把请求参数的字符串中间的空格转义成+号,比如 '[{"filterField":"accommodationType","value":"Hotel,Entire apartment"}]' => '[{"filterField":"accommodationType","value":"Hotel,Entire+apartment"}]'。调试了下,发现在发起请求前参数打印是正常的,是浏览器在请求的时候在请求体中字段转义的。不过好像对后端的搜索结果并不影响,所以这里就没有改动。




解决办法,对字符串encode下,后端收到参数后再decode。



ios 17 input 聚焦页面出现抖动


解决办法: input focus 给 body 添加 height: 100vh; overflow: hidden; 样式。input blur 取消 focus 添加的样式。


作者:wait
来源:juejin.cn/post/7309040097936343103
收起阅读 »

产品经理:实现一个微信输入框

web
近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。 初期认为这应该改动不大,就是把input换...
继续阅读 »


近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。


初期认为这应该改动不大,就是把input换成textarea吧。但是实际开发过程发现并没有这么简单,本文仅作为开发过程的记录,因为是基于uniapp开发,相关实现代码都是基于uniapp


简单分析我们大概需要实现以下几个功能点:



  • 默认单行输入

  • 可多行输入,但有最大行数限制

  • 超过限制行术后内容在内部滚动

  • 支持回车发送内容

  • 支持常见组合键在输入框内换行输入

  • 多行输入时高度自适应 & 页面整体自适应


单行输入


默认单行输入比较简单直接使用input输入框即可,使用textarea的时候目前的实现方式是通过设置行内样式的高度控制,如我们的行内高度是36px,那么就设置其高度为36px。为什么要通过这种方式设置呢?因为要考虑后续多行输入超出最大行数的限制,需要通过高度来控制textarea的最大高度。


<textarea style="{ height: 36px }" />


多行输入


多行输入核心要注意的就是控制元素的高度,因为不能随着用户的输入一直增加高度,我们需要设置一个最大的行数限制,超出限制后就不再增加高度,内容可以继续输入,可以在输入框内上下滚动查看内容。


这里需要借助于uniapp内置在textarea@linechange事件,输入框行数变化时调用并回传高度和行数。如果不使用uniapp则需要对输入文字的长度及当前行高计算出对应的行数,这种情况还需要考虑单行文本没有满一行且换行的情况。


代码如下,在linechange事件中获取到最新的高度设置为textarea的高度,当超出最大的行数限制后则不处理。


linechange(event) {
const { height, lineCount } = event.detail
if (lineCount < maxLine) {
this.textareaHeight = height
}
}

这是正常的输入,还有一种情况是用户直接粘贴内容输入的场景,这种时候不会触发@linechange事件,需要手动处理,根据粘贴文本后的textarea的滚动高度进行计算出对应的行数,如超出限制行数则设置为最大高度,否则就设置为实际的行数所对应的高度。代码如下:


const paddingTop = parseInt(getComputedStyle(textarea).paddingTop);
const paddingBottom = parseInt(getComputedStyle(textarea).paddingBottom);
const textHeight = textarea.scrollHeight - paddingTop - paddingBottom;
const numberOfLines = Math.floor(textHeight / lineHeight);

if (numberOfLines > 1 && this.lineCount === 1) {
const lineCount = numberOfLines < maxLine ? numberOfLines : maxLine
this.textareaHeight = lineCount * lineHeight
}

键盘发送内容


正常我们使用电脑聊天时发送内容都是使用回车键发送内容,使用ctrlshiftalt等和回车键的组合键将输入框的文本进行换行处理。所以接下来要实现的就是对键盘事件的监听,基于事件进行发送内容和内容换行输入处理。


首先是事件的监听,uniapp不支持keydown的事件监听,所以这里使用了原生JS做监听处理,为了避免重复监听,对每次开始监听前先进行移除事件的监听,代码如下:


this.$refs.textarea.$el.removeEventListener('keydown', this.textareaKeydownHandle)
this.$refs.textarea.$el.addEventListener('keydown', this.textareaKeydownHandle)

然后是对textareaKeydownHandle方法的实现,这里需要注意的是组合键对内容换行的处理,需要获取到当前光标的位置,使用textarea.selectionStart可获取,基于光标位置增加一个换行\n的输入即可实现换行,核心代码如下:


const cursorPosition = textarea.selectionStart;
if(
(e.keyCode == 13 && e.ctrlKey) ||
(e.keyCode == 13 && e.metaKey) ||
(e.keyCode == 13 && e.shiftKey) ||
(e.keyCode == 13 && e.altKey)
){
// 换行
this.content = `${this.content.substring(0, cursorPosition)}\n${this.content.substring(cursorPosition)}`
}else if(e.keyCode == 13){
// 发送
this.onSend();
e.preventDefault();
}

高度自适应


当多行输入内容时输入框所占据的高度增加,导致页面实际内容区域的高度减小,如果不进行动态处理会导致实际内容会被遮挡。如下图所示,红色区域就是需要动态处理的高度。



主要需要处理的场景就是输入内容行数变化的时候和用户粘贴文本的时候,这两种情况都会基于当前的可视行数计算输入框的高度,那么内容区域的高度就好计算了,使用整个窗口的高度减去输入框的高度和其他固定的高度如导航高度和底部安全距离高度即是真实内容的高度。


this.contentHeight = this.windowHeight - this.navBarHeight - this.fixedBottomHeight - this.textareaHeight;

最后


到此整个输入框的体验优化核心实现过程就结束了,增加了多行输入,组合键换行输入内容,键盘发送内容,整体内容高度自适应等。整体实现过程的细节功能点还是比较多,有实现过类似需求的同学欢迎留言交流~


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


作者:南城FE
来源:juejin.cn/post/7267791228872753167
收起阅读 »

面试官问我按钮级别权限怎么控制,我说v-if,面试官说再见

web
最近的面试中有一个面试官问我按钮级别的权限怎么控制,我说直接v-if啊,他说不够好,我说我们项目中按钮级别的权限控制情况不多,所以v-if就够了,他说不够通用,最后他对我的评价是做过很多东西,但是都不够深入,好吧,那今天我们就来深入深入。 因为我自己没有相关实...
继续阅读 »

最近的面试中有一个面试官问我按钮级别的权限怎么控制,我说直接v-if啊,他说不够好,我说我们项目中按钮级别的权限控制情况不多,所以v-if就够了,他说不够通用,最后他对我的评价是做过很多东西,但是都不够深入,好吧,那今天我们就来深入深入。


因为我自己没有相关实践,所以接下来就从这个有16.2k星星的后台管理系统项目Vue vben admin中看看它是如何做的。


获取权限码


要做权限控制,肯定需要一个code,无论是权限码还是角色码都可以,一般后端会一次性返回,然后全局存储起来就可以了,Vue vben admin是在登录成功以后获取并保存到全局的store中:


import { defineStore } from 'pinia';
export const usePermissionStore = defineStore({
state: () => ({
// 权限代码列表
permCodeList: [],
}),
getters: {
// 获取
getPermCodeList(){
return this.permCodeList;
},
},
actions: {
// 存储
setPermCodeList(codeList) {
this.permCodeList = codeList;
},

// 请求权限码
async changePermissionCode() {
const codeList = await getPermCode();
this.setPermCodeList(codeList);
}
}
})

接下来它提供了三种按钮级别的权限控制方式,一一来看。


函数方式


使用示例如下:


<template>
<a-button v-if="hasPermission(['20000', '2000010'])" color="error" class="mx-4">
拥有[20000,2000010]code可见
</a-button>
</template>

<script lang="ts">
import { usePermission } from '/@/hooks/web/usePermission';

export default defineComponent({
setup() {
const { hasPermission } = usePermission();
return { hasPermission };
},
});
</script>

本质上就是通过v-if,只不过是通过一个统一的权限判断方法hasPermission


export function usePermission() {
function hasPermission(value, def = true) {
// 默认视为有权限
if (!value) {
return def;
}

const allCodeList = permissionStore.getPermCodeList;
if (!isArray(value)) {
return allCodeList.includes(value);
}
// intersection是lodash提供的一个方法,用于返回一个所有给定数组都存在的元素组成的数组
return (intersection(value, allCodeList)).length > 0;

return true;
}
}

很简单,从全局store中获取当前用户的权限码列表,然后判断其中是否存在当前按钮需要的权限码,如果有多个权限码,只要满足其中一个就可以。


组件方式


除了通过函数方式使用,也可以使用组件方式,Vue vben admin提供了一个Authority组件,使用示例如下:


<template>
<div>
<Authority :value="RoleEnum.ADMIN">
<a-button type="primary" block> 只有admin角色可见 </a-button>
</Authority>
</div>
</template>
<script>
import { Authority } from '/@/components/Authority';
import { defineComponent } from 'vue';
export default defineComponent({
components: { Authority },
});
</script>

使用Authority包裹需要权限控制的按钮即可,该按钮需要的权限码通过value属性传入,接下来看看Authority组件的实现。


<script lang="ts">
import { defineComponent } from 'vue';
import { usePermission } from '/@/hooks/web/usePermission';
import { getSlot } from '/@/utils/helper/tsxHelper';

export default defineComponent({
name: 'Authority',
props: {
value: {
type: [Number, Array, String],
default: '',
},
},
setup(props, { slots }) {
const { hasPermission } = usePermission();

function renderAuth() {
const { value } = props;
if (!value) {
return getSlot(slots);
}
return hasPermission(value) ? getSlot(slots) : null;
}

return () => {
return renderAuth();
};
},
});
</script>

同样还是使用hasPermission方法,如果当前用户存在按钮需要的权限码时就原封不动渲染Authority包裹的内容,否则就啥也不渲染。


指令方式


最后一种就是指令方式,使用示例如下:


<a-button v-auth="'1000'" type="primary" class="mx-4"> 拥有code ['1000']权限可见 </a-button>

实现如下:


import { usePermission } from '/@/hooks/web/usePermission';

function isAuth(el, binding) {
const { hasPermission } = usePermission();

const value = binding.value;
if (!value) return;
if (!hasPermission(value)) {
el.parentNode?.removeChild(el);
}
}

const mounted = (el, binding) => {
isAuth(el, binding);
};

const authDirective = {
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted,
};

// 注册全局指令
export function setupPermissionDirective(app) {
app.directive('auth', authDirective);
}

只定义了一个mounted钩子,也就是在绑定元素挂载后调用,依旧是使用hasPermission方法,判断当前用户是否存在通过指令插入的按钮需要的权限码,如果不存在,直接移除绑定的元素。


很明显,Vue vben admin的实现有两个问题,一是不能动态更改按钮的权限,二是动态更改当前用户的权限也不会生效。


解决第一个问题很简单,因为上述只有删除元素的逻辑,没有加回来的逻辑,那么增加一个updated钩子:


app.directive("auth", {
mounted: (el, binding) => {
const value = binding.value
if (!value) return
if (!hasPermission(value)) {
// 挂载的时候没有权限把元素删除
removeEl(el)
}
},
updated(el, binding) {
// 按钮权限码没有变化,不做处理
if (binding.value === binding.oldValue) return
// 判断用户本次和上次权限状态是否一样,一样也不用做处理
let oldHasPermission = hasPermission(binding.oldValue)
let newHasPermission = hasPermission(binding.value)
if (oldHasPermission === newHasPermission) return
// 如果变成有权限,那么把元素添加回来
if (newHasPermission) {
addEl(el)
} else {
// 如果变成没有权限,则把元素删除
removeEl(el)
}
},
})

const hasPermission = (value) => {
return [1, 2, 3].includes(value)
}

const removeEl = (el) => {
// 在绑定元素上存储父级元素
el._parentNode = el.parentNode
// 在绑定元素上存储一个注释节点
el._placeholderNode = document.createComment("auth")
// 使用注释节点来占位
el.parentNode?.replaceChild(el._placeholderNode, el)
}

const addEl = (el) => {
// 替换掉给自己占位的注释节点
el._parentNode?.replaceChild(el, el._placeholderNode)
}

主要就是要把父节点保存起来,不然想再添加回去的时候获取不到原来的父节点,另外删除的时候创建一个注释节点给自己占位,这样下次想要回去能知道自己原来在哪。


第二个问题的原因是修改了用户权限数据,但是不会触发按钮的重新渲染,那么我们就需要想办法能让它触发,这个可以使用watchEffect方法,我们可以在updated钩子里通过这个方法将用户权限数据和按钮的更新方法关联起来,这样当用户权限数据改变了,可以自动触发按钮的重新渲染:


import { createApp, reactive, watchEffect } from "vue"
const codeList = reactive([1, 2, 3])

const hasPermission = (value) => {
return codeList.includes(value)
}

app.directive("auth", {
updated(el, binding) {
let update = () => {
let valueNotChange = binding.value === binding.oldValue
let oldHasPermission = hasPermission(binding.oldValue)
let newHasPermission = hasPermission(binding.value)
let permissionNotChange = oldHasPermission === newHasPermission
if (valueNotChange && permissionNotChange) return
if (newHasPermission) {
addEl(el)
} else {
removeEl(el)
}
};
if (el._watchEffect) {
update()
} else {
el._watchEffect = watchEffect(() => {
update()
})
}
},
})

updated钩子里更新的逻辑提取成一个update方法,然后第一次更新在watchEffect中执行,这样用户权限的响应式数据就可以和update方法关联起来,后续用户权限数据改变了,可以自动触发update方法的重新运行。


好了,深入完了,看着似乎也挺简单的,我不确定这些是不是面试官想要的,或者还有其他更高级更优雅的实现呢,知道的朋友能否指点一二,在下感激不尽。


作者:街角小林
来源:juejin.cn/post/7209648356530896953
收起阅读 »

el-table表格大数据卡顿?试试umy-ui

web
最近公司项目表格数据量过大导致页面加载时间非常长,而且还使用了keep-alive缓存,关闭页面时需要卸载大量dom,导致页面卡死、cpu飙到100%的问题 后来在网上查找了很多方法,发现了umy-ui(目前只支持v2),这个表格库是针对element-u...
继续阅读 »

最近公司项目表格数据量过大导致页面加载时间非常长,而且还使用了keep-alive缓存,关闭页面时需要卸载大量dom,导致页面卡死、cpu飙到100%的问题



image.png

后来在网上查找了很多方法,发现了umy-ui(目前只支持v2),这个表格库是针对element-ui的表格做了二次优化,支持el-table的所有方法


image.png

这个表格可以基于可视区域做dom渲染,这样就大大的减少了页面初次渲染的压力。


首先第一步


 npm install umy-ui

或者使用CDN的方式引入


  <!--引入表格样式-->
<link rel="stylesheet" href="https://unpkg.com/umy-ui/lib/theme-chalk/index.css">

<!-- import Vue -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>

<script src="https://unpkg.com/umy-ui/lib/index.js"></script>
<!-- 真实项目不建议你直接引入 <script src="https://unpkg.com/umy-ui/lib/index.js"></script>-->

<!-- 这样去引如会直接下最新版本,如果你的项目打包发布了,然后遇见umy-ui大更新 你可能项目会报错。-->

<!--推荐你这样引入: https://unpkg.com/umy-ui$1.0.1/lib/index.js 加入版本号!-->
<!-- 这样去引如会直接下最新版本,如果你的项目打包发布了,然后遇见umy-ui大更新 你可能项目会报错。-->

<!--推荐你这样引入: https://unpkg.com/umy-ui@1.0.1/lib/index.js 加入版本号!-->

第二步 main.js中全局引入


  import UmyUi from 'umy-ui'
import 'umy-ui/lib/theme-chalk/index.css';// 引入样式
Vue.use(UmyUi)

或按需引入


import { UTable, UTableColumn } from 'umy-ui';

Vue.component(UTable.name, UTable);
Vue.component(UTableColumn.name, UTableColumn);

修改起来也很方便
直接吧 el-table 改成 u-table, el-table-column改成u-table-column,最后添加属性use-virtual这样就可以使用了


示例


<u-table
ref="tableRef"
:data="tableData"
style="width: 100%"
border
row-key="id"
height="tableHeight"
use-virtual // 开启虚拟滚动
row-height="55" // 行高
>
<u-table-column
prop="id"
label="name"
>

...
</u-table-column>

</u-table>

其中的u-table是基础虚拟表格,u-grid是解决冲向列多卡顿的问题、或单元格合并。(这里注意u-grid的没有prop字段!!而是field)


具体详细属性请看umy-ui官网


问题

用完这个表格页面性能虽然提升不少但是当我开启多个keep-alive缓存之后全部关闭时还是会有卡顿


image.png

目前用的是 vue-element-admin 的模板,希望有大佬指点一二


最后

如果文章有帮助到你,帮作者点个赞就好啦


作者:凤栖夜落
来源:juejin.cn/post/7315681269702688779
收起阅读 »

js需要同时发起百条接口请求怎么办?--通过Promise实现分批处理接口请求

web
如何通过 Promise 实现百条接口请求? 实际项目中遇到需要批量发起上百条接口请求怎么办? 最新案例代码在此!点击看看 前言 不知你项目中有没有遇到过这样的情况,反正我的实际工作项目中真的遇到了这种玩意,一个接口获取一份列表,列表中的每一项都有一个属性需...
继续阅读 »
如何通过 Promise 实现百条接口请求?

实际项目中遇到需要批量发起上百条接口请求怎么办?

最新案例代码在此!点击看看


前言



  • 不知你项目中有没有遇到过这样的情况,反正我的实际工作项目中真的遇到了这种玩意,一个接口获取一份列表,列表中的每一项都有一个属性需要通过另一个请求来逐一赋值,然后就有了这份封装

  • 真的是很多功能都是被逼出来的

  • 这份功能中要提醒一下:批量请求最关键的除了分批功能之外,适当得取消任务和继续任务也很重要,比如用户到了这个页面后,正在发起百条数据请求,但是这些批量请求还没完全执行完,用户离开了这个页面,此时就需要取消剩下正在发起的请求了,而且如果你像我的遇到的项目一样,页面还会被缓存,那么为了避免用户回到这个页面,所有请求又重新发起一遍的话,就需要实现继续任务的功能,其实这个继续任务比断点续传简单多了,就是过滤到那些已经赋值的数据项就行了

  • 如果看我啰啰嗦嗦一堆烂东西没看明白的话,就直接看下面的源码吧


源码在此!



  • 【注】:这里的 httpRequest 请根据自己项目而定,比如我的项目是uniapp,里面的http请求是 uni.request,若你的项目是 axios 或者 ajax,那就根据它们来对 BatchHttp 中的某些部分进行相应的修改



    • 比如:其中的 cancelAll() 函数,若你的 http 取消请求的方式不同,那么这里取消请求的功能就需要相应的修改,若你使用的是 fetch 请求,那除了修改 cancelAll 功能之外,singleRequest 中收集请求任务的方式也要修改,因为 fetch 是不可取消的,需要借助 AbortController 来实现取消请求的功能,





    • 提示一下,不管你用的是什么请求框架,你都可以自己二次封装一个 request.js,功能就仿照 axios 这种,返回的对象中包含一个 abort() 函数即可,那么这份 BatchHttp 也就能适用啦



  • BatchHttp.js


// 注:这里的 httpRequest 请根据自己项目而定,比如我的项目是uniapp,里面的http请求是 uni.request,若你的项目是 axios 或者 ajax,那就根据它们来对 BatchHttp 中的某些部分
import httpRequest from './httpRequest.js'

/**
* 批量请求封装
*/

export class BatchHttp {

/**
* 构造函数
* @param {Object} http - http请求对象(该http请求拦截器里切勿带有任何有关ui的功能,比如加载对话框、弹窗提示框之类),用于发起请求,该http请求对象必须满足:返回一个包含取消请求函数的对象,因为在 this.cancelAll() 函数中会使用到
* @param {string} [passFlagProp=null] - 用于识别是否忽略某些数据项的字段名(借此可实现“继续上一次完成的批量请求”);如:passFlagProp='url' 时,在执行 exec 时,会过滤掉 items['url'] 不为空的数据,借此可以实现“继续上一次完成的批量请求”,避免每次都重复所有请求
*/

constructor(http=httpRequest, passFlagProp=null) {
/** @private @type {Object[]} 请求任务数组 */
this.resTasks = []
/** @private @type {Object} uni.request对象 */
this.http = http
/** @private @type {boolean} 取消请求标志 */
this.canceled = false
/** @private @type {string|null} 识别跳过数据的属性 */
this.passFlagProp = passFlagProp
}


/**
* 将数组拆分成多个 size 长度的小数组
* 常用于批量处理控制并发等场景
* @param {Array} array - 需要拆分的数组
* @param {number} size - 每个小数组的长度
* @returns {Array} - 拆分后的小数组组成的二维数组
*/

#chunk(array, size) {
const chunks = []
let index = 0

while(index < array.length) {
chunks.push(array.slice(index, size + index))
index += size;
}

return chunks
}

/**
* 单个数据项请求
* @private
* @param {Object} reqOptions - 请求配置
* @param {Object} item - 数据项
* @returns {Promise} 请求Promise
*/

#singleRequest(reqOptions, item) {
return new Promise((resolve, _reject) => {
const task = this.http({
url: reqOptions.url,
method: reqOptions.method || 'GET',
data: reqOptions.data,
success: res => {
resolve({sourceItem:item, res})
}
})
this.resTasks.push(task)
})
}

/**
* 批量请求控制
* @private
* @async
* @param {Object} options - 函数参数项
* @param {Array} options.items - 数据项数组
* @param {Object} options.reqOptions - 请求配置
* @param {number} [options.concurrentNum=10] - 并发数
* @param {Function} [options.chunkCallback] - 分块回调
* @returns {Promise}
*/

async #batchRequest({items, reqOptions, concurrentNum = 10, chunkCallback=(ress)=>{}}) {
const promiseArray = []
let data = []
const passFlagProp = this.passFlagProp
if(!passFlagProp) {
data = items
} else {
// 若设置独立 passFlagProp 值,则筛选出对应属性值为空的数据(避免每次都重复请求所有数据,实现“继续未完成的批量请求任务”)
data = items.filter(d => !Object.hasOwnProperty.call(d, passFlagProp) || !d[passFlagProp])
}
// --
if(data.length === 0) return

data.forEach(item => {
const requestPromise = this.#singleRequest(reqOptions, item)
promiseArray.push(requestPromise)
})

const promiseChunks = this.#chunk(promiseArray, concurrentNum) // 切分成 n 个请求为一组

for (let ck of promiseChunks) {
// 若当前处于取消请求状态,则直接跳出
if(this.canceled) break
// 发起一组请求
const ckRess = await Promise.all(ck) // 控制并发数
chunkCallback(ckRess) // 每完成组请求,都进行回调
}
}

/**
* 设置用于识别忽略数据项的字段名
* (借此参数可实现“继续上一次完成的批量请求”);
* 如:passFlagProp='url' 时,在执行 exec 时,会过滤掉 items['url'] 不为空的数据,借此可以实现“继续上一次完成的批量请求”,避免每次都重复所有请求
* @param {string} val
*/

setPassFlagProp(val) {
this.passFlagProp = val
}

/**
* 执行批量请求操作
* @param {Object} options - 函数参数项
* @param {Array} options.items - 数据项数组
* @param {Object} options.reqOptions - 请求配置
* @param {number} [options.concurrentNum=10] - 并发数
* @param {Function} [options.chunkCallback] - 分块回调
*/

exec(options) {
this.canceled = false
this.#batchRequest(options)
}

/**
* 取消所有请求任务
*/

cancelAll() {
this.canceled = true
for(const task of this.resTasks) {
task.abort()
}
this.resTasks = []
}
}

调用案例在此!



  • 由于我的项目是uni-app这种,方便起见,我就直接贴上在 uni-app 的页面 vue 组件中的使用案例

  • 案例代码仅展示关键部分,所以比较粗糙,看懂参考即可


<template>
<view v-for="item of list" :key="item.key">
<image :src="item.url"></image>
</view>
</template>
<script>
import { BatchHttp } from '@/utils/BatchHttp.js'

export default {
data() {
return {
isLoaded: false,
batchHttpInstance: null,
list:[]
}
},
onLoad(options) {
this.queryList()
},
onShow() {
// 第一次进页面时,onLoad 和 onShow 都会执行,onLoad 中 getList 已调用 batchQueryUrl,这里仅对缓存页面后再次进入该页面有效
if(this.isLoaded) {
// 为了实现继续请求上一次可能未完成的批量请求,再次进入该页面时,会检查是否存在未完成的任务,若存在则继续发起批量请求
this.batchQueryUrl(this.dataList)
}
this.isLoaded = true
},
onHide() {
// 页面隐藏时,会直接取消所有批量请求任务,避免占用资源(下次进入该页面会检查未完成的批量请求任务并执行继续功能)
this.cancelBatchQueryUrl()
},
onUnload() {
// 页面销毁时,直接取消批量请求任务
this.cancelBatchQueryUrl()
},
onBackPress() {
// 路由返回时,直接取消批量请求任务(虽然路由返回也会执行onHide事件,但是无所胃都写上,会判断当前有没有任务的)
this.cancelBatchQueryUrl()
},
methods: {
async queryList() {
// 接口不方法直接贴的,这里是模拟的列表接口
const res = await mockHttpRequest()
this.list = res.data

// 发起批量请求
// 用 nextTick 也行,只要确保批量任务在列表dom已挂载完成之后执行即可
setTimeout(()=>{this.batchQueryUrl(resData)},0)
},
/**
* 批量处理图片url的接口请求
* @param {*} data
*/
batchQueryUrl(items) {
let batchHttpInstance = this.batchHttpInstance
// 判定当前是否有正在执行的批量请求任务,有则直接全部取消即可
if(!!batchHttpInstance) {
batchHttpInstance.cancelAll()
this.batchHttpInstance = null
batchHttpInstance = null
}
// 实例化对象
batchHttpInstance = new BatchHttp()
// 设置过滤数据的属性名(用于实现继续任务功能)
batchHttpInstance.setPassFlagProp('url') // 实现回到该缓存页面是能够继续批量任务的关键一步 <-----
const reqOptions = { url: '/api/product/url' }
batchHttpInstance.exec({items, reqOptions, chunkCallback:(ress)=>{
let newDataList = this.dataList
for(const r of ress) {
newDataList = newDataList.map(d => d.feId === r['sourceItem'].feId ? {...d,url:r['res'].msg} : d)
}

this.dataList = newDataList
}})

this.batchHttpInstance = batchHttpInstance
},
/**
* 取消批量请求
*/
cancelBatchQueryUrl() {
if(!!this.batchHttpInstance) {
this.batchHttpInstance.cancelAll()
this.batchHttpInstance = null
}
},
}
}
</script>

作者:FE_C_P小麦
来源:juejin.cn/post/7306331039843270667
收起阅读 »

前端中 JS 发起的请求可以暂停吗

web
在前端中,JavaScript(JS)可以使用XMLHttpRequest对象或fetch API来发起网络请求。然而,JavaScript本身并没有提供直接的方法来暂停请求的执行。一旦请求被发送,它会继续执行并等待响应。 尽管如此,你可以通过一些技巧或库来模...
继续阅读 »

在前端中,JavaScript(JS)可以使用XMLHttpRequest对象或fetch API来发起网络请求。然而,JavaScript本身并没有提供直接的方法来暂停请求的执行。一旦请求被发送,它会继续执行并等待响应。


尽管如此,你可以通过一些技巧或库来模拟请求的暂停和继续执行。下面是一种常见的方法:


1. 使用XMLHttpRequest对象


你可以在发送请求前创建一个XMLHttpRequest对象,并将其保存在变量中。然后,在需要暂停请求时,调用该对象的abort()方法来中止请求。当需要继续执行请求时,可以重新创建一个新的XMLHttpRequest对象并发起请求。


var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/api', true);
xhr.send();

// 暂停请求
xhr.abort();

// 继续请求
xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/api', true);
xhr.send();

2. 使用fetch API和AbortController


fetch API与AbortController一起使用可以更方便地控制请求的暂停和继续执行。AbortController提供了一个abort()方法,可以用于中止fetch请求。


var controller = new AbortController();

fetch('https://example.com/api', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));

// 暂停请求
controller.abort();

// 继续请求
controller = new AbortController();

fetch('https://example.com/api', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));

请注意,这些方法实际上是通过中止请求并重新发起新的请求来模拟暂停和继续执行的效果,并不能真正暂停正在进行的请求。


3. 曲线救国


模拟一个假暂停的功能,在前端的业务场景上,需要对这些数据进行处理之后渲染在界面上,如果我们能在请求发起之前增加一个控制器,在请求回来时,如果控制器为暂停状态则不处理数据,等待控制器恢复后再进行处理,也可以达到暂停的效果。


// 创建一个暂停控制器 Promise
function createPauseControllerPromise() {
const result = {
isPause: false, // 标记控制器是否处于暂停状态
resolveWhenResume: false, // 表示在恢复时是否解析 Promise
resolve(value) {}, // 解析 Promise 的占位函数
pause() { // 暂停控制器的函数
this.isPause = true;
},
resume() { // 恢复控制器的函数
if (!this.isPause) return;
this.isPause = false;
if (this.resolveWhenResume) {
this.resolve();
}
},
promise: Promise.resolve(), // 初始为已解决状态的 Promise
};

const promise = new Promise((res) => {
result.resolve = res; // 将解析函数与 Promise 关联
});
result.promise = promise; // 更新控制器中的 Promise 对象

return result; // 返回控制器对象
}

function requestWithPauseControl(request) {
const controller = createPauseControllerPromise(); // 创建暂停控制器对象

const controlRequest = request() // 执行请求函数
.then((data) => { // 请求成功回调
if (!controller.isPause) controller.resolve(); // 如果控制器未暂停,则解析 Promise
return data; // 返回请求结果
})
.finally(() => {
controller.resolveWhenResume = true; // 标记在恢复时解析 Promise
});

const result = Promise.all([controlRequest, controller.promise]).then(
(data) => {
controller.resolve(); // 解析控制器的 Promise
return data[0]; // 返回请求处理结果
}
);

result.pause = controller.pause.bind(controller); // 将暂停函数绑定到结果 Promise 对象
result.resume = controller.resume.bind(controller); // 将恢复函数绑定到结果 Promise 对象

return result; // 返回添加了暂停控制功能的结果 Promise 对象
}

为什么需要创建两个promise


在requestWithPauseControl函数中,需要等待两个Promise对象解析:一个是请求处理的Promise,另一个是控制器的Promise。通过使用Promise.all方法,可以将这两个Promise对象组合成一个新的Promise,该新的Promise会在两个原始Promise都解析后才会解析。这样做的目的是确保在处理请求结果之前,暂停控制器的resolve方法被调用,以便在恢复时解析Promise。


因此,将请求处理的Promise和控制器的Promise放入一个Promise数组,并使用Promise.all等待它们都解析完成,可以确保在两个Promise都解析后再进行下一步操作,以实现预期的功能。


使用


const result = requestWithPauseControl(/*request fn*/).then((data) => {
console.log(data)
})

if (Math.random() > 0.5) { result.pause() }

setTimeout(() => {
result.resume()
}, 4000)

作者:来点vc
来源:juejin.cn/post/7310786521082560562
收起阅读 »

更改官方demo的登录方式—web端

项目场景:在环信官网下载Demo,本地运行只有手机+验证码的方式登录?怎么更改为自己项目的appkey和用户去进行登录呢?往下看👇👇👇VUE2 DEMOvue2 demo源码下载vue2 demo线上体验第一步:更改appkeywebim-vue-demo==...
继续阅读 »

项目场景:
环信官网下载Demo,本地运行只有手机+验证码的方式登录?怎么更改为自己项目的appkey和用户去进行登录呢?往下看👇👇👇

VUE2 DEMO

vue2 demo源码下载

vue2 demo线上体验

第一步:更改appkey
webim-vue-demo===>src===>utils===>WebIMConfig.js


第二步:更改代码
webim-vue-demo===>src===>pages===>login===>index.vue

<template>
<a-layout>
<div class="login">
<div class="login-panel">
<div class="logo">Web IM</div>
<a-input v-model="username" :maxLength="64" placeholder="用户名" />
<a-input v-model="password" :maxLength="64" v-on:keyup.13="toLogin" type="password" placeholder="密码" />
<a-input v-model="nickname" :maxLength="64" placeholder="昵称" v-show="isRegister == true" />

<a-button type="primary" @click="toRegister" v-if="isRegister == true">注册</a-button>
<a-button type="primary" @click="toLogin" v-else>登录</a-button>
</div>
<p class="tip" v-if="isRegister == true">
已有账号?
<span class="green" v-on:click="changeType">去登录</span>
</p>
<p class="tip" v-else>
没有账号?
<span class="green" v-on:click="changeType">注册</span>
</p>

<!-- <div class="login-panel">
<div class="logo">Web IM</div>
<a-form :form="form" >
<a-form-item has-feedback>
<a-input
placeholder="手机号码"
v-decorator="[
'phone',
{
rules: [{ required: true, message: 'Please input your phone number!' }],
},
]"
style="width: 100%"
>
<a-select
initialValue="86"
slot="addonBefore"
v-decorator="['prefix', { initialValue: '86' }]"
style="width: 70px"
>
<a-select-option value="86">
+86
</a-select-option>
</a-select>
</a-input>
</a-form-item>

<a-form-item>
<a-row :gutter="8">
<a-col :span="14">
<a-input
placeholder="短信验证码"
v-decorator="[
'captcha',
{ rules: [{ required: true, message: 'Please input the captcha you got!' }] },
]"
/>
</a-col>
<a-col :span="10">
<a-button v-on:click="getSmsCode" class="getSmsCodeBtn">{{btnTxt}}</a-button>
</a-col>
</a-row>
</a-form-item>
<a-button style="width: 100%" type="primary" @click="toLogin" class="login-rigester-btn">登录</a-button>

</a-form> -->
<!-- </div> -->
</div>
</a-layout>
</template>

<script>
import './index.less';
import { mapState, mapActions } from 'vuex';
import axios from 'axios'
import { Message } from 'ant-design-vue';
const domain = window.location.protocol+'//a1.easemob.com'
const userInfo = localStorage.getItem('userInfo') && JSON.parse(localStorage.getItem('userInfo'));
let times = 60;
let timer
export default{
data(){
return {
username: userInfo && userInfo.userId || '',
password: userInfo && userInfo.password || '',
nickname: '',
btnTxt: '获取验证码'
};
},
beforeCreate() {
this.form = this.$form.createForm(this, { name: 'register' });
},
mounted: function(){
const path = this.isRegister ? '/register' : '/login';

if(path !== location.pathname){
this.$router.push(path);
}
if(this.isRegister){
this.getImageVerification()
}
},
watch: {
isRegister(result){
if(result){
this.getImageVerification()
}
}
},
components: {},
computed: {
isRegister(){
return this.$store.state.login.isRegister;
},
imageUrl(){
return this.$store.state.login.imageUrl
},
imageId(){
return this.$store.state.login.imageId
}
},
methods: {
...mapActions(['onLogin', 'setRegisterFlag', 'onRegister', 'getImageVerification', 'registerUser', 'loginWithToken']),
toLogin(){
this.onLogin({
username: this.username.toLowerCase(),
password: this.password
});
// const form = this.form;
// form.validateFields(['phone', 'captcha'], { force: true }, (err, value) => {
// if(!err){
// const {phone, captcha} = value
// this.loginWithToken({phone, captcha})
// }
// });
},
toReset(){
this.$router.push('/resetpassword')
},
toRegister(e){
e.preventDefault(e);
// this.form.validateFieldsAndScroll((err, values) => {
// if (!err) {
// this.registerUser({
// userId: values.username,
// userPassword: values.password,
// phoneNumber: values.phone,
// smsCode: values.captcha,
// })
// }
// });

this.onRegister({
username: this.username.toLowerCase(),
password: this.password,
nickname: this.nickname.toLowerCase(),
});
},
changeType(){
this.setRegisterFlag(!this.isRegister);
},
getSmsCode(){
if(this.$data.btnTxt != '获取验证码') return
const form = this.form;
form.validateFields(['phone'], { force: true }, (err, value) => {
if(!err){
const {phone, imageCode} = value
this.getCaptcha({phoneNumber: phone, imageCode})
}
});
},
getCaptcha(payload){
const self = this
const imageId = this.imageId
axios.post(domain+`/inside/app/sms/send/${payload.phoneNumber}`, {
phoneNumber: payload.phoneNumber,
})
.then(function (response) {
Message.success('短信已发送')
self.countDown()
})
.catch(function (error) {
if(error.response && error.response.status == 400){
if(error.response.data.errorInfo == 'Image verification code error.'){
self.getImageVerification()
}
if(error.response.data.errorInfo == 'phone number illegal'){
Message.error('请输入正确的手机号!')
}else if(error.response.data.errorInfo == 'Please wait a moment while trying to send.'){
Message.error('你的操作过于频繁,请稍后再试!')
}else if(error.response.data.errorInfo.includes('exceed the limit')){
Message.error('获取已达上限!')
}else{
Message.error(error.response.data.errorInfo)
}
}
});
},
countDown(){
this.$data.btnTxt = times
timer = setTimeout(() => {
this.$data.btnTxt--
times--
if(this.$data.btnTxt === 0){
times = 60
this.$data.btnTxt = '获取验证码'
return clearTimeout(timer)
}
this.countDown()
}, 1000)
}
}
};
</script>


webim-vue-demo===>src===>store===>login.js
只用更改actions下的onLogin,其余不用动

onLogin: function(context, payload){
context.commit('setUserName', payload.username);
let options = {
user: payload.username,
pwd: payload.password,
appKey: WebIM.config.appkey,
apiUrl: 'https://a1.easecdn.com'
};
WebIM.conn.open(options).then((res)=>{
localStorage.setItem('userInfo', JSON.stringify({ userId: payload.username, password: payload.password,accessToken:res.accessToken}));
});

},



VUE3 DEMO:

vue3 demo源码下载

vue3 demo线上体验

第一步:更改appkey
webim-vue-demo===>src===>IM===>config===>index.js


第二步:更改代码
webim-vue-demo===>src===>views===>Login===>components===>LoginInput===>index.vue

<script setup>
import { ref, reactive, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { EaseChatClient } from '@/IM/initwebsdk'
import { handleSDKErrorNotifi } from '@/utils/handleSomeData'
import { fetchUserLoginSmsCode, fetchUserLoginToken } from '@/api/login'
import { useStore } from 'vuex'
import { usePlayRing } from '@/hooks'
const store = useStore()
const loginValue = reactive({
phoneNumber: '',
smsCode: ''
})
const buttonLoading = ref(false)
//根据登陆初始化一部分状态
const loginState = computed(() => store.state.loginState)
watch(loginState, (newVal) => {
if (newVal) {
buttonLoading.value = false
loginValue.phoneNumber = ''
loginValue.smsCode = ''
}
})
const rules = reactive({
phoneNumber: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号',
trigger: ['blur', 'change']
}
],
smsCode: [
{
required: true,
message: '请输入短信验证码',
trigger: ['blur', 'change']
}
]
})
//登陆接口调用
const loginIM = async () => {
const { clickRing } = usePlayRing()
clickRing()
buttonLoading.value = true
/* SDK 登陆的方式 */
try {
let { accessToken } = await EaseChatClient.open({
user: loginValue.phoneNumber.toLowerCase(),
pwd: loginValue.smsCode.toLowerCase(),
});
window.localStorage.setItem(`EASEIM_loginUser`, JSON.stringify({ user: loginValue.phoneNumber, accessToken: accessToken }))
} catch (error) {
console.log('>>>>登陆失败', error);
const { data: { extraInfo } } = error
handleSDKErrorNotifi(error.type, extraInfo.errDesc);
loginValue.phoneNumber = '';
loginValue.smsCode = '';
}
finally {
buttonLoading.value = false;
}
/* !环信后台接口登陆(仅供环信线上demo使用!) */
// const params = {
// phoneNumber: loginValue.phoneNumber.toString(),
// smsCode: loginValue.smsCode.toString()
// }
// try {
// const res = await fetchUserLoginToken(params)
// if (res?.code === 200) {
// console.log('>>>>>>登陆token获取成功', res.token)
// EaseChatClient.open({
// user: res.chatUserName.toLowerCase(),
// accessToken: res.token
// })
// window.localStorage.setItem(
// 'EASEIM_loginUser',
// JSON.stringify({
// user: res.chatUserName.toLowerCase(),
// accessToken: res.token
// })
// )
// }
// } catch (error) {
// console.log('>>>>登陆失败', error)
// if (error.response?.data) {
// const { code, errorInfo } = error.response.data
// if (errorInfo.includes('does not exist.')) {
// ElMessage({
// center: true,
// message: `用户${loginValue.username}不存在!`,
// type: 'error'
// })
// } else {
// handleSDKErrorNotifi(code, errorInfo)
// }
// }
// } finally {
// buttonLoading.value = false
// }
}
/* 短信验证码相关 */
const isSenedAuthCode = ref(false)
const authCodeNextCansendTime = ref(60)
const sendMessageAuthCode = async () => {
const phoneNumber = loginValue.phoneNumber
try {
await fetchUserLoginSmsCode(phoneNumber)
ElMessage({
type: 'success',
message: '验证码获取成功!',
center: true
})
startCountDown()
} catch (error) {
ElMessage({ type: 'error', message: '验证码获取失败!', center: true })
}
}
const startCountDown = () => {
isSenedAuthCode.value = true
let timer = null
timer = setInterval(() => {
if (
authCodeNextCansendTime.value <= 60 &&
authCodeNextCansendTime.value > 0
) {
authCodeNextCansendTime.value--
} else {
clearInterval(timer)
timer = null
authCodeNextCansendTime.value = 60
isSenedAuthCode.value = false
}
}, 1000)
}
</script>

<template>
<el-form :model="loginValue" :rules="rules">
<el-form-item prop="phoneNumber">
<el-input
class="login_input_style"
v-model="loginValue.phoneNumber"
placeholder="手机号"
clearable
/>
</el-form-item>
<el-form-item prop="smsCode">
<el-input
class="login_input_style"
v-model="loginValue.smsCode"
placeholder="请输入短信验证码"
>
<template #append>
<el-button
type="primary"
:disabled="loginValue.phoneNumber && isSenedAuthCode"
@click="sendMessageAuthCode"
v-text="
isSenedAuthCode
? `${authCodeNextCansendTime}S`
: '获取验证码'
"
></el-button>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="function_button_box">
<el-button
v-if="loginValue.phoneNumber && loginValue.smsCode"
class="haveValueBtn"
:loading="buttonLoading"
@click="loginIM"
>登录</el-button
>
<el-button v-else class="notValueBtn">登录</el-button>
</div>
</el-form-item>
</el-form>
</template>

<style lang="scss" scoped>
.login_input_style {
margin: 10px 0;
width: 400px;
height: 50px;
padding: 0 16px;
}

::v-deep .el-input__inner {
padding: 0 20px;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 1.75px;
color: #3a3a3a;

&::placeholder {
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
/* identical to box height */
letter-spacing: 1.75px;
color: #cccccc;
}
}

::v-deep .el-input__suffix-inner {
font-size: 20px;
margin-right: 15px;
}

::v-deep .el-form-item__error {
margin-left: 16px;
}

::v-deep .el-input-group__append {
background: linear-gradient(90deg, #04aef0 0%, #5a5dd0 100%);
width: 60px;
color: #fff;
border: none;
font-weight: 400;

button {
font-weight: 300;
}
}

.login_text {
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 17px;
text-align: right;

.login_text_isuserid {
display: inline-block;
// width: 100px;
color: #f9f9f9;
}

.login_text_tologin {
margin-right: 20px;
width: 80px;
color: #05b5f1;
cursor: pointer;

&:hover {
text-decoration: underline;
}
}
}

.function_button_box {
margin-top: 10px;
width: 400px;

button {
margin: 10px;
width: 380px;
height: 50px;
border-radius: 57px;
}

.haveValueBtn {
background: linear-gradient(90deg, #04aef0 0%, #5a5dd0 100%);
border: none;
font-weight: 300;
font-size: 17px;
color: #f4f4f4;

&:active {
background: linear-gradient(90deg, #0b83b2 0%, #363df4 100%);
}
}

.notValueBtn {
border: none;
font-weight: 300;
font-size: 17px;
background: #000000;
mix-blend-mode: normal;
opacity: 0.3;
color: #ffffff;
cursor: not-allowed;
}
}
</style>


REACT DEMO:

react demo源码下载

react demo线上体验

 第一步:更改appkey
webim-dev===>demo===>src===>config===>WebIMConfig.js


第二步:更改代码
webim-dev===>demo===>src===>config===>WebIMConfig.js
将usePassword改为true



UNIAPP DEMO:

uniapp vue2 demo源码下载

uniapp vue3 demo源码下载

第一步:更改appkey

uniapp vue2 demo
webim-uniapp-demo===>utils===>WebIMConfig.js


uniapp vue3 demo
webim-uniapp-demo===>EaseIM===>config===>index.js


第二步:更改代码

webim-uniapp-demo===>pages===>login===>login.vue



微信小程序 DEMO:

微信小程序源码下载

第一步:更改appkey
webim-weixin-demo===>src===>utils===>WebIMConfig.js


第二步:更改代码
webim-weixin-demo===>src===>pages===>login===>login.wxml

<import src="../../comps/toast/toast.wxml" />
<view class="login">
<view class="login_title">
<text bindlongpress="longpress">登录</text>
</view>

<!-- 测试用 请忽略 -->
<view class="config" wx:if="{{ show_config }}">
<view>
<text>使用沙箱环境</text>
<switch class="config_swich" checked="{{isSandBox? true: false}}" color="#0873DE" bindchange="changeConfig" />
</view>
</view>

<view class="login_user {{nameFocus}}">
<input type="text" placeholder="请输入用户名" placeholder-style="color:rgb(173,185,193)" bindinput="bindUsername" bindfocus="onFocusName" bindblur="onBlurName" />
</view>
<view class="login_pwd {{psdFocus}}">
<input type="text" password placeholder="用户密码" placeholder-style="color:rgb(173,185,193)" bindinput="bindPassword" bindfocus="onFocusPsd" bindblur="onBlurPsd"/>
</view>
<view class="login_btn">
<button hover-class="btn_hover" bind:tap="login">登录</button>
</view>
<template is="toast" data="{{ ..._toast_ }}"></template>
</view>

webim-weixin-demo===>src===>pages===>login===>login.js

let WebIM = require("../../utils/WebIM")["default"];
let __test_account__, __test_psword__;
let disp = require("../../utils/broadcast");

let runAnimation = true
Page({
data: {
name: "",
psd: "",
grant_type: "password",
rtcUrl: '',
show_config: false,
isSandBox: false
},

statechange(e) {
console.log('live-player code:', e.detail.code)
},

error(e) {
console.error('live-player error:', e.detail.errMsg)
},

onLoad: function(option){
const me = this;
const app = getApp();
new app.ToastPannel.ToastPannel();

disp.on("em.xmpp.error.passwordErr", function(){
me.toastFilled('用户名或密码错误');
});
disp.on("em.xmpp.error.activatedErr", function(){
me.toastFilled('用户被封禁');
});

wx.getStorage({
key: 'isSandBox',
success (res) {
console.log(res.data)
me.setData({
isSandBox: !!res.data
})
}
})

if (option.username && option.password != '') {
this.setData({
name: option.username,
psd: option.password
})
}
},

bindUsername: function(e){
this.setData({
name: e.detail.value
});
},

bindPassword: function(e){
this.setData({
psd: e.detail.value
});
},
onFocusPsd: function(){
this.setData({
psdFocus: 'psdFocus'
})
},
onBlurPsd: function(){
this.setData({
psdFocus: ''
})
},
onFocusName: function(){
this.setData({
nameFocus: 'nameFocus'
})
},
onBlurName: function(){
this.setData({
nameFocus: ''
})
},

login: function(){
runAnimation = !runAnimation
if(!__test_account__ && this.data.name == ""){
this.toastFilled('请输入用户名!')
return;
}
else if(!__test_account__ && this.data.psd == ""){
this.toastFilled('请输入密码!')
return;
}
wx.setStorage({
key: "myUsername",
data: __test_account__ || this.data.name.toLowerCase()
});

getApp().conn.open({
user: __test_account__ || this.data.name.toLowerCase(),
pwd: __test_psword__ || this.data.psd,
grant_type: this.data.grant_type,
appKey: WebIM.config.appkey
});
},

longpress: function(){
console.log('长按')
this.setData({
show_config: !this.data.show_config
})
},

changeConfig: function(){
this.setData({
isSandBox: !this.data.isSandBox
}, ()=>{
wx.setStorage({
key: "isSandBox",
data: this.data.isSandBox
});
})

}

});



收起阅读 »

写一个万用RecyclerView分隔线,支持linear grid staggered

web
前言 2023已过半,才发现我已经大半年没写博客了,痛定思痛决定水一篇。 不知道大家平时干活的时候有没有被RecyclerView列表的分隔线困扰过,app里一般都会有各种各样的列表,横的竖的、网格、瀑布流样式的,每次要给列表设分隔线时都要自己写一个特化的It...
继续阅读 »

前言


2023已过半,才发现我已经大半年没写博客了,痛定思痛决定水一篇。


不知道大家平时干活的时候有没有被RecyclerView列表的分隔线困扰过,app里一般都会有各种各样的列表,横的竖的、网格、瀑布流样式的,每次要给列表设分隔线时都要自己写一个特化的ItemDecoration,既麻烦又难以复用,那能不能写一个适用大多数场景的ItemDecoration来减轻这类负担呢?


别急,本篇文章就给大家带来一个我自用的通用ItemDecoration,支持linear grid staggered LayoutManager,支持横竖向、跨列等情况;支持边缘、横纵向分隔线不同宽度,使用也非常简单。


效果图和代码


代码


单个类可以直接使用,仓库包含demo


效果图 网格样式


20230621_173920.gif


瀑布流


20230621_174043.gif


这个ItemDecoration暂时没有实现分隔线上色,因为我觉得这种场景其实很少就把相关代码删掉了,要加的话建议通过继承实现。


实现和注意点


首先,由于要支持横竖向,所以定义两个轴,主轴代表可滑动的那个轴,交叉轴代表另一个轴,这样无论是横向还是竖向都能保持语义一致


// 主轴方向分割线宽度
protected var mainWidth = 0

// 交叉轴方向分割线宽度
protected var crossWidth = 0

// 边缘宽度
protected var mainPadding = 0
protected var crossPadding = 0

主轴的间隔


主轴的分隔线很简单,第一行的item和最后一行的item设置边缘间隔,其他每个item在主轴同一方向上设置分隔线间隔,关键点在于首行和末行的判断。


LinearLayoutManager情况下最简单,判断position是首个或者最后一个就ok了,但是GridLayoutManager和StaggeredGridLayoutManager都存在跨列问题。比如说列表有5列,但是第一个item就占满了整行,那么本该在第一行的2-5个item实际上就不在第一行了;末行判断同理。


GridLayoutManager通过它的SpanSizeLookup来判断,groupIndex==0在首行,groupIndex==lastGr0upIndex在最后一行


// 当前item在哪一行
val groupIndex = manager.spanSizeLookup.getSpanGr0upIndex(position, spanCount)
// 最后一个item在哪一行
val lastGr0upIndex = manager.spanSizeLookup.getSpanGr0upIndex(size - 1, spanCount)

StaggeredGridLayoutManager相对麻烦一些,看下面的注释,spanIndex代表当前item在本行内的下标


val lp = view.layoutParams
if (lp is StaggeredGridLayoutManager.LayoutParams) {
val spanCount = manager.spanCount
// 前面没有跨列item时当前item的期望下标
val exceptSpanIndex = position % spanCount
// 真实的item下标
val spanIndex = lp.spanIndex
// position原属于第一行并且此item之前没有跨列的情况,当前item才属于第一行
val isFirstGr0up = position < spanCount && exceptSpanIndex == spanIndex
var isLastGr0up = false
if (size - position <= spanCount) {
// position原属于最后一行
val lastItemView = manager.findViewByPosition(size - 1)
if (lastItemView != null) {
val lastLp = lastItemView.layoutParams
if (lastLp is StaggeredGridLayoutManager.LayoutParams) {
// 列表最后一个item和当前item的spanIndex差等于position之差说明它们之间没有跨列的情况,当前item属于最后一行
if (lastLp.spanIndex - spanIndex == size - 1 - position) {
isLastGr0up = true
}
}
}
}
}

接下来就很简单了,设置主轴上的间隔


if (isFirstGr0up) {
// 是第一行
if (isVertical) {
outRect.top = mainPadding
} else {
outRect.left = mainPadding
}
} else if (isLastGr0up) {
// 是最后一行要加边缘
if (isVertical) {
outRect.top = mainWidth
outRect.bottom = mainPadding
} else {
outRect.left = mainWidth
outRect.right = mainPadding
}
} else {
if (isVertical) {
outRect.top = mainWidth
} else {
outRect.left = mainWidth
}
}

交叉轴的间隔


交叉轴的分隔线最简单的是LinearLayoutManager,由于不存在多列直接设置为边缘间隔就可以了


if (isVertical) {
outRect.left = crossPadding
outRect.right = crossPadding
} else {
outRect.top = crossPadding
outRect.bottom = crossPadding
}

GridLayoutManager和StaggeredGridLayoutManager的交叉轴分隔线计算方法是一样的,可以统一处理,需要遵循的规则有两个



  1. 每列占用的左右间隔之和相等

  2. 每个item占用的右间隔和它相邻item占用的左间隔之和等于给定的间隔宽度


以下图为例,列表共4列,边缘间隔是15,item间隔是10,第二个item跨两列,每列应该占用的空间为15。


image.png


以第3个item为例,如何计算出它的左间隔和右间隔,公式如下


左间隔:到当前item的左边为止的总间隔(crossWidth * spanIndex + crossPadding)减去 到上一个item为止需要使用的总间隔(spanUsedWidth * spanIndex),这个例子中这两个值相等


同理右间隔:到当前item为止需要使用的总间隔(spanUsedWidth * (spanIndex + spanSize)) 减去 到当前item右边为止的总间隔(crossWidth * (spanIndex + spanSize - 1) + crossPadding);当然也可以用 当前item需要使用的总间隔(
spanUsedWidth * spanSize) - 当前item已经使用的总间隔(
crossWidth * (spanSize - 1) + lt)


这样通过归纳只使用两行代码就统合了所有情况


/**
* 交叉轴间隔
* [spanIndex] 当前item的以第几列开始
* [spanSize] 当前item占用的列数
*/

private fun getItemCrossOffsets(outRect: Rect, isVertical: Boolean, spanCount: Int, spanIndex: Int, spanSize: Int) {
// 每列占用的间隔
val spanUsedWidth = (crossPadding * 2 + crossWidth * (spanCount - 1)) / spanCount
// 到当前item的左边为止的总间隔 - 到上一个item为止需要使用的总间隔
val lt = crossWidth * spanIndex + crossPadding - spanUsedWidth * spanIndex
// 到当前item为止需要使用的总间隔 - 到当前item右边为止的总间隔
// val rb = spanUsedWidth * (spanIndex + spanSize) - crossWidth * (spanIndex + spanSize - 1) - crossPadding
// 当前item需要使用的总间隔 - 当前item已经使用的总间隔
val rb = spanUsedWidth * spanSize - crossWidth * (spanSize - 1) - lt
if (isVertical) {
outRect.left = lt
outRect.right = rb
} else {
outRect.top = lt
outRect.bottom = rb
}
}

作者:北野青阳
来源:juejin.cn/post/7248811984749527101
收起阅读 »

什么?要给localStorage加上过期时间

web
localStorage 是 HTML5 引入的本地存储机制,可以在浏览器端保存键值对数据。 特点 数据存储在浏览器端,页面关闭后数据不丢失 储存空间较大,不同浏览器支持至少 5MB 存储 API简单,可以直接像操作对象一样使用 数据格式为字符串类型,需要自...
继续阅读 »

localStorage 是 HTML5 引入的本地存储机制,可以在浏览器端保存键值对数据。


特点



  • 数据存储在浏览器端,页面关闭后数据不丢失

  • 储存空间较大,不同浏览器支持至少 5MB 存储

  • API简单,可以直接像操作对象一样使用

  • 数据格式为字符串类型,需要自行序列化和反序列化

  • 同源的页面间可以共享 localStorage 数据

  • 数据有更好的安全性和生命周期,相比cookie更适合存储重要信息


使用



  • 存储数据:


localStorage.setItem('key', 'value');


  • 获取数据:


let value = localStorage.getItem('key'); 


  • 移除数据:


localStorage.removeItem('key');


  • 清空所有数据:


localStorage.clear();


  • 遍历所有键值:


for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i);
let value = localStorage.getItem(key);
}

应用场景


localStorage 适合保存应用程序需要记住的少量数据,如用户设置、表单自动填充等。

不适合存储敏感信息,因为数据可以被查看和修改。

大量数据也不适合存入 localStorage,可以考虑 IndexedDB 或服务器端存储。

总之,明智地使用 localStorage 可以在一定程度增强 Web 应用程序的用户体验。



那么,如何给localStorage加上有效期呢



export default class Storage {
constructor(expiryTime) {
this.expiryTime = expiryTime;
}
set(key, value, expiryTime) {
let obj = {
data: value,
expiryTime: Date.now()+(expiryTime || this.expiryTime)
};
localStorage.setItem(key, JSON.stringify(obj));
}
get(key) {
let item = localStorage.getItem(key);
if (!item) {
return null;
}
item = JSON.parse(item);
let nowTime = Date.now();
if (item.expiryTime && nowTime > item.expiryTime) {
console.log('已过期');
this.remove(key);
return null;
} else {
return item.data;
}
}
remove(key) {
localStorage.removeItem(key);
}
clear() {
localStorage.clear();
}
}

使用


import Storage from 'xx/storage.js'
const storage1 = new Storage(24*60*60*1000); // 设置全局默认过期时间为24小时
storage1.set('name', 'nan'); // 使用全局默认过期时间
storage1.set('age', 18, 60*1000); // 设置独立的过期时间为1分钟

作者:IMyself
来源:juejin.cn/post/7296414016326713355
收起阅读 »

学到了!Figma 原来是这样表示矩形的

web
大家好,我是前端西瓜哥。 今天我们来研究一下 Figma 是如何表示图形的,这里以矩形为切入点进行研究。 明白最简单的矩形的表示后,研究其他的图形就可以举一反三。 矩形的一般表达 如果让我设计一个矩形图形的物理属性,我会怎么设计? 我张口就来:x、y、widt...
继续阅读 »

大家好,我是前端西瓜哥。


今天我们来研究一下 Figma 是如何表示图形的,这里以矩形为切入点进行研究。


明白最简单的矩形的表示后,研究其他的图形就可以举一反三。


矩形的一般表达


如果让我设计一个矩形图形的物理属性,我会怎么设计?


我张口就来:x、y、width、height、rotation。


对一些简单的图形编辑操作,这些属性基本上是够用的,比如白板工具,如果你不考虑或者不希望图形可以翻转(flip) 的话。


Figma 需要考虑翻转的情况的,此外还有斜切的情况。


翻转的场景:


图片


还有斜切的场景,在选中多个图形然后缩放时有发生。


图片


这些表达光靠上面的几个属性是不够的,我们看看 Figma为了表达这些效果,是怎么去设计矩形的。


Figma 矩形物理属性


上篇文章我们用 Figma-To-JSON 成功解析了 fig 文件,借助这个工具,我们得到了矩形图形的属性。


与物理信息相关的属性如下:


{
  "size": {
    "x"100,
    "y"100
  },
  "transform": {
    "m00"1,
    "m01"3,
    "m02"5,
    "m10"2,
    "m11"4,
    "m12"6
  },
  // 省略其他无关属性
}


没有位置属性,这个属性默认是 (0, 0),实际它转移到 transform 的矩阵的位移子矩阵上了。


size 表示宽高,理论上 width 和 height 语义更好,这样应该是用了平面矢量类型的结构体,所以是 x 和 y。


transform 表示一个 3x3 的变换矩阵。


m00 | m01 | m02
m10 | m11 | m12
0 | 0 | 1


上面的 transform 属性的值所对应的矩阵为:


1 | 3 | 5
2 | 4 | 6
0 | 0 | 1


属性面板


再看看这些属性对应的右侧属性面板。


图片


x、y 分别是 5 和 6,它是 (0, 0) 进行 transform 后的结果,这个直接对应 transform.m02tansfrom.m12


import { Matrix } from "pixi.js";

const matrix = new Matrix(123456);
const topLeft = matrix.apply({ x0y0 }); // { x: 5, y: 6 }

// 或直接点
const topLeft = { x5y6 }


这里引入了 pixi.js 的 matrix 类,该类使用列向量方式进行表达。


文末有 demo 源码以及线上 demo,可打开控制台查看结果验证正确性。



然后这里的 width 和 height,是 223.61 和 500, 怎么来的?


它们对应的是矩形的两条边变形后的长度,如下:


图片


uiWidth 为 (0, 0)(width, 0)  进行矩阵变换后坐标点之间的距离。


const distance = (p1, p2) => {
  const a = p1.x - p2.x;
  const b = p1.y - p2.y;
  return Math.sqrt(a * a + b * b);
};

const matrix = new Matrix(123456);
const topLeft = { x5y6 }

const topRight = matrix.apply({ x100y0 });
distance(topRight, topLeft); // 223.60679774997897

最后计算出 223.60679774997897,四舍五入得到 223.61。


高度计算同理。


uiHeight 为 (0, 0)(0, height)  进行矩阵变换后坐标点之间的距离。


const matrix = new Matrix(123456);
const topLeft = { x5y6 }

const bottomLeft = matrix.apply({ x0y100 });
distance(bottomLeft, topLeft); // 500

旋转角度


最后是旋转角度,它是宽度对应的矩形边向量,逆时针旋转 90 度的向量所对应的角度。


图片


先计算宽边向量,然后逆时针旋转 90 度得到旋转向量,最后计算旋转向量对应的角度。


const wSideVec = { x: topRight.x - topLeft.xy: topRight.y - topLeft.y };
// 逆时针旋转 90 度,得到旋转向量
const rotationMatrix = new Matrix(0, -11000);
const rotationVec = rotationMatrix.apply(wSideVec);
const rad = calcVectorRadian(rotationVec);
const deg = rad2Deg(rad); // -63.43494882292201

这里用了几个工具函数。


// 计算和 (0, -1) 的夹角
const calcVectorRadian = (vec) => {
  const a = [vec.x, vec.y];
  const b = [0, -1]; // 这个是基准角度

  // 使用点积公式计算夹脚
  const dotProduct = a[0] * b[0] + a[1] * b[1];
  const d =
    Math.sqrt(a[0] * a[0] + a[1] * a[1]) * Math.sqrt(b[0] * b[0] + b[1] * b[1]);
  let rad = Math.acos(dotProduct / d);

  if (vec.x > 0) {
    // 如果 x > 0, 则 rad 转为 (-PI, 0) 之间的值
    rad = -rad;
  }
  return rad;
}

// 弧度转角度
const rad2Deg = (rad) => (rad * 180) / Math.PI;

Figma 的角度表示比较别扭。


特征为:基准角度朝上,对应向量为 (0, -1),角度方向为逆时针,角度范围限定为 (-180, 180],计算向量角度时要注意这个特征进行调整。


图片


完整代码实现


线上 demo:


codepen.io/F-star/pen/…


代码实现:


import { Matrix } from "pixi.js";

// 计算和 (0, -1) 的夹角
const calcVectorRadian = (vec) => {
  const a = [vec.x, vec.y];
  const b = [0, -1];

  const dotProduct = a[0] * b[0] + a[1] * b[1];
  const d =
    Math.sqrt(a[0] * a[0] + a[1] * a[1]) * Math.sqrt(b[0] * b[0] + b[1] * b[1]);
  let rad = Math.acos(dotProduct / d);

  if (vec.x > 0) {
    // 如果 x > 0, 则 rad 为 (-PI, 0) 之间的值
    rad = -rad;
  }
  return rad;
}

// 弧度转角度
const rad2Deg = (rad) => (rad * 180) / Math.PI;

const distance = (p1, p2) => {
  const a = p1.x - p2.x;
  const b = p1.y - p2.y;
  return Math.sqrt(a * a + b * b);
};

const getAttrs = (size, transform) => {
  const width = size.x;
  const height = size.y;
  const matrix = new Matrix(
    transform.m00// 1
    transform.m10// 2
    transform.m01// 3
    transform.m11// 4
    transform.m02// 5
    transform.m12 // 6
  );

  const topLeft = { x: transform.m02y: transform.m12 };
  console.log("x:", topLeft.x)
  console.log("y:", topLeft.y)

  const topRight = matrix.apply({ x: width, y0 });
  console.log("width:"distance(topRight, topLeft)); // 223.60679774997897

  const bottomLeft = matrix.apply({ x0y: height });
  console.log("height:"distance(bottomLeft, topLeft)); // 500

  const wSideVec = { x: topRight.x - topLeft.xy: topRight.y - topLeft.y };
  // 逆时针旋转 90 度,得到旋转向量
  const rotationMatrix = new Matrix(0, -11000);
  const rotationVec = rotationMatrix.apply(wSideVec);

  const rad = calcVectorRadian(rotationVec);
  const deg = rad2Deg(rad);
  console.log("rotation:", deg); // -63.43494882292201
};

getAttrs(
  // 宽高
  { x100y100 },
  // 变换矩阵
  {
    m001,
    m013,
    m025,
    m102,
    m114,
    m126,
  }
);


运行一下,结果和属性面板一致。


图片


结尾


Figma 只用宽高和变换矩阵来表达矩形,在数据层可以用精简的数据表达丰富的变形,此外在渲染的时候也能将矩阵运算交给 GPU 进行并行运算,是不错的做法。


我是前端西瓜哥,欢迎关注我,学习更多图形编辑器知识。




相关阅读,


计算机图形学:变换矩阵


什么?Figma 的 fig 文件格式居然解析出来了


求向量的角度


图形编辑器开发:属性显示与格式转换


作者:前端西瓜哥
来源:juejin.cn/post/7314488568969478154
收起阅读 »

天天看到有人抵触ref,有人抵触reactive,把我整笑了

web
背景 这几天看到好多文章标题都是类似于: 不用 ref 的 xx 个理由 不用 reactive 的 xx 个理由 历数 ref 的 xx 宗罪 我就很不解,到底是什么原因导致有这两批人: 抵触 ref 的人 抵触 reactive 的人 看了这些文章...
继续阅读 »

背景


这几天看到好多文章标题都是类似于:



  • 不用 ref 的 xx 个理由

  • 不用 reactive 的 xx 个理由

  • 历数 ref 的 xx 宗罪


我就很不解,到底是什么原因导致有这两批人:



  • 抵触 ref 的人

  • 抵触 reactive 的人


看了这些文章,我可以总结出他们的想法


抵触 reactive 的人


抵触 reactive 的人,他们的想法大概就是:



  • 1、Vue 官方推荐 ref

  • 2、reactive 有类型限制,ref 没有

  • 3、reactive 使用不当会丢失响应式,比如解构

  • 4、reactive 无法修改整个对象的值


抵触 ref 的人


抵触 ref 的人,他们的想法大概就是:



  • 1、ref 的底层其实就是 reactive,用 ref 相当于多了一层,耗费性能

  • 2、ref 的 .value 用起来很麻烦,增加使用者心里负担

  • 3、ref 到模板的时候会解掉 value 这一层,这时候也会耗费性能


把我整笑了~


说实话,看到这些文章,有点把我整笑了,其实你要用 ref 或者 reactive 都没错,但是没比必要那么抵触,编程很多时候并不是非黑即白啊。。。


既然 Vue3 推出了 ref 和 reactive,那就说明他们都有存在的必要,在项目中不同的场景去运用他们,我觉得才是最好的,而不是用一个不用另一个,不止这两个,还有很多其他好用的 Vue3 API


我想针对这两批人的想法做一个回应:


回应 -> 抵触 reactive 的人



  • 1、官方是推荐,不是抵触

  • 2、reactive 既然有类型限制,那就在特定时候用 reactive 就行

  • 3、使用不当会丢失响应式?那就是开发者对于 Vue3 API 的使用还不熟

  • 4、用 Object.assign 就可以修改整个对象的值


回应 -> 抵触 ref 的人



  • 1、耗费性能的话,这么久了,也没人贴出到底耗费了多少性能?

  • 2、.value 不麻烦,我觉得 .value 可以起到辨别响应式和非响应式数据的效果,而且现在编辑器都有插件提供的代码补全了,多个 .value 也花不了多少时间吧?


灵活使用 Vue3 API 才是王道


其实在平时开发中,我觉得基本数据类型和数组,都可以用 ref 来管理,而对象的话可以使用 reactive 来管理,比如表单对象、状态对象


其实 Vue3 不止有这两个 API ,还有很多其他 API ,也很好用,大家只要去灵活使用它们,能让你的Vue3 项目上一个层次


readonly


顾名思义,就是只读的意思,如果你的数据被这个 API 包裹住的话,那么修改之后并不会触发响应式,并且会提示警告




readonly 的用途一般用于一些 hooks 暴露出来的变量,不想外界去修改,比如我封装一个 hooks,这样去做的话,那么外界只能用变量,但是不能修改变量,这样大大保护了 hooks 内部的逻辑~



shallowRef


shallowRef 用来包住一个基础类型或者引用类型,如果是基础类型那么跟 ref 基本没区别,如果是引用类型的话,那么直接改深层属性是不能触发响应式的,除非直接修改引用地址,如下:




注意:改深层属性能改数据,只是没触发响应式,所以当下一次响应式触发的时候,你修改的深层数据会渲染到页面上~



shallowRef 的用处主要用于一些比较大的但又变化不大的数据,比如我有一个表格数据,通过接口直接获取,并且主要用在前端展示,需要修改一些深层的属性,但是这些属性并不需要立即表现在页面上,比如以下例子,我只需要展示 name、age 字段,至于 isOld 字段并不需要展示,我想要计算 isOld 但是又不想触发响应式更新,所以可以用 shallowRef 包起来,进而减少响应式更新,优化性能



shallowReactive


shallowReactive 用来包住一个引用类型,被包住后,修改第一层才会触发响应式更新,也就是浅层的属性,修改深层的属性并不会触发响应式更新



注意:改深层属性能改数据,只是没触发响应式,所以当下一次响应式触发的时候,你修改的深层数据会渲染到页面上~




shallowReactive 用的比较少,shallowReactive 的用处跟 shallowRef 比较像,都是为了让一些比较大的数据能减少响应式更新,进而优化性能


toRef & toRefs


先说说 toRef 吧,我们平时在使用 reactive 的时候会有一个苦恼,那就是解构,比如看以下例子,我们为了少些一些代码,解构出来了 name 并放到模板里渲染,但是当我们想改原数据的时候,发现 name 并不会更新,这就是解构出来基础类型的苦恼




这时我们可以使用 toRef,这个时候我们直接修改 name 也会触发原数据的修改,修改原数据也会触发 name 的修改




但是如果是属性太多了,我们想一个一个去用 toRef 的话会写很多代码



所以我们可以使用 toRefs 一次性解构



toRaw & markRaw & unref


toRaw 可以把一个响应式 reactive 转成普通对象,也就是把响应式对象转成非响应式对象



toRaw 主要用在回调传参中,比如我封装一个 hooks,我想要把 hooks 内维护的响应式变量转成普通数据,当做参数传给回调函数,可以用 toRaw



markRaw 可以用来标记响应式对象里的某个属性不被追踪,如果你的响应式对象里有某个属性数据量比较大,但又不想被追踪,你可以使用 markRaw



unref 相当于返回 ref 的 value



effectScope & onScopeDispose


effectScope 可以有两个作用:



  • 收集副作用

  • 全局状态管理


收集副作用


比如我们封装一个共用的 hooks,为了减少页面隐患,肯定会统一收集副作用,并且在组件销毁的时候去统一消除,比如以下代码:



但是这么收集很麻烦, effectScope 能帮我们做到统一收集,并且通过 stop 方法来进行清除,且 stop 执行的时候会触发 effectScope 内部的 onScopeDispose



我们可以利用 effectScope & onScopeDispose 来做一些性能优化,比如下面这个例子,我们封装一个鼠标监听的 hooks



但是如果在页面里调用多次的话,那么势必会往 window 身上监听很多多余的事件,造成性能负担,所以解决方案就是,无论页面里调用再多次 useMouse,我们只往 window 身上加一个鼠标监听事件



全局状态管理


现在 Vue3 最火的全局状态管理工具肯定是 Pinia 了,那么你们知道 Pinia 的原理是什么吗?原理就是依赖了 effectScope



所以我们完全可以自己使用 effectScope 来实现自己的局部状态管理,比如我们封装一个通用组件,这个组件层级比较多,并且需要共享一些数据,那么这个时候肯定不会用 Pinia 这种全局状态管理,而是会自己写一个局部的状态管理,这个时候 effectScope 就可以排上用场了


vueuse 中的 createGlobalState 就是为了这个而生




provide & inject


Vue3 用来提供注入的 API,主要是用在组件的封装,比如那种层级较多的组件,且子组件需要依赖父组件甚至爷爷组件的数据,那么可以使用 provide & inject,最典型的例子就是 Form 表单组件,可以去看看各个组件库的源码,表单组件大部分都是用 provide & inject 来实现的,比如 Form、Form-Item、Input这三个需要互相依赖对方的规则、字段名、字段值,所以用 provide & inject 会更好。具体用法看文档吧~cn.vuejs.org/guide/compo…



结语 & 加学习群 & 摸鱼群


我是林三心



  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;

  • 一个偏前端的全干工程师;

  • 一个不正经的掘金作者;

  • 一个逗比的B站up主;

  • 一个不帅的小红书博主;

  • 一个喜欢打铁的篮球菜鸟;

  • 一个喜欢历史的乏味少年;

  • 一个喜欢rap的五音不全弱鸡


如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点


作者:Sunshine_Lin
来源:juejin.cn/post/7311226497111916563
收起阅读 »

CSS整洁之道——:is()、:where()和:has()的用法

web
让我们写出优雅界面的CSS,它也总是把自己进化得更加优雅。 今天我们花5分钟时间学习三个优雅的CSS伪类::is()、:where() 和 :has()。 :is() - 取代组合选择器 :is() 允许你在一个规则中包含多个选择器。它接受一组选择器作为参数,...
继续阅读 »

让我们写出优雅界面的CSS,它也总是把自己进化得更加优雅。


今天我们花5分钟时间学习三个优雅的CSS伪类::is():where():has()


:is() - 取代组合选择器


:is() 允许你在一个规则中包含多个选择器。它接受一组选择器作为参数,并应用样式到匹配的元素上。


/* 传统方法 */
ul > li > a,
ol > li > a,
nav > ul > li > a,
nav > ol > li > a {
color: blue;
}

/* 使用 :is() */
:is(ul, ol, nav > ul, nav > ol) > li > a {
color: blue;
}

:is() 可以简化多层嵌套和多种选择器组合的写法,让你维护样式更方便。


:is() 优先级依然遵循CSS选择器的优先级规则,即 ID -> 类 -> 元素 的顺序。


:is(.class1) a {
color: blue;
}

:is(#id1) a {
color: red;
}


这段代码里两条规则如果命中相同的元素,那么第二条会优先应用。


:is() 的参数也可以传一个匹配规则


:is([class^="is-styling"]) a {
color: yellow;
}

这样的写法会匹配所有 class 开头是 is-styling 的选择器。


:where() - 拥有最低优先级


:where():is() 相似,都可以传入选择器或者匹配规则来简化你的CSS代码。


:where([class^="where-styling"]) a {
color: yellow;
}

但和 :is() 不同的是,:where() 拥有最低优先级,这样的好处是它定义的样式规则不会影响其他样式规则,避免了样式冲突。


/* <footer class="where-styling">……</footer> */

footer a {
color: green;
}

:where([class^="where-styling"]) a {
color: red
}


当有其他规则和 :where() 同时被命中时,:where() 一定是失效的。所以上面这个例子实际效果是链接显示绿色。


:has() - 基于其他元素进行匹配


:has() 可以根据直接后代元素的存在来匹配元素


/* 选择直接包含 p 元素的 div */
div:has(> p) {
border: 1px solid black;
}

也可以根据紧邻的下一个兄弟元素来匹配元素


/* 选择后面跟着 p 元素的 div */
div:has(+ p) {
border: 1px solid black;
}

你还可以把它跟其他伪类一起使用,比如 :has():is() 一起使用



:has() 使用场景很多,只要是强互动的页面都可能用到,以后有机会单独分享一篇~


总结


大部分浏览器的新版本都已支持 :is():where():has() 这三个伪类了,如果你的项目跑在低版本的浏览器中,那么需要考虑一下回退策略。


专栏资源


专栏介绍:分享CSS新特性和好看的样式设计

专栏地址:👉# CSS之美


作者:BigYe程普
来源:juejin.cn/post/7314841908169850891
收起阅读 »

js中?.、??、??=的用法及使用场景

web
上面这个错误,相信前端开发工程师应该经常遇到吧,要么是自己考虑不全造成的,要么是后端开发人员丢失数据或者传输错误数据类型造成的。因此对数据访问时的非空判断就变成了一件很繁琐且重要的事情,下面就介绍ES6一些新的语法来方便我们开发。 1. 可选链操作符 (Opt...
继续阅读 »

image.png


上面这个错误,相信前端开发工程师应该经常遇到吧,要么是自己考虑不全造成的,要么是后端开发人员丢失数据或者传输错误数据类型造成的。因此对数据访问时的非空判断就变成了一件很繁琐且重要的事情,下面就介绍ES6一些新的语法来方便我们开发。


1. 可选链操作符 (Optional Chaining Operator - ?.):


可选链操作符允许您在访问对象属性或调用函数时,检查中间的属性是否存在或为 null/undefined。如果中间的属性不存在或为空,表达式将短路返回 undefined,而不会引发错误。


1.1 用法示例:


const obj = {
foo: {
bar: {
baz: 42
}
},
xyz: []
};


// 使用可选链操作符
const value1 = obj?.foo?.bar?.baz; // 如果任何中间属性不存在或为空,value 将为 undefined
//除了对属性的检查,还可以用于对数组下标及函数的检查
const value2 = obj?.xyz?.[0]?.fn?.();

// 传统写法
const value1 = obj && obj.foo && obj.foo.bar && obj.foo.bar.baz; // 需要手动检查每个属性
const value2 = obj && obj.xyz && obj.xyz[0] && obj.xyz[0].fn && obj.xyz[0].fn();

1.2 使用场景:



  • 链式访问对象属性,而不必手动检查每个属性是否存在。

  • 调用可能不存在的函数。


2. 空值合并操作符 (Nullish Coalescing Operator - ??):


空值合并操作符用于选择性地提供默认值,仅当变量的值为 null 或 undefined 时,才返回提供的默认值。否则,它将返回变量的实际值。


2.1 用法示例:


const foo = null;
const bar = undefined;
const baz = 0;
const qux = '';
cosnt xyz = false;

const value1 = foo ?? 'default'; // 'default',因为 foo 是 null
const value2 = bar ?? 'default'; // 'default',因为 bar 是 undefined
const value3 = baz ?? 'default'; // 0,因为 baz 不是 null 或 undefined
const value4 = qux ?? 'default'; // '',因为 qux 不是 null 或 undefined
const value5 = xyz ?? 'default'; // false,因为 xyz 不是 null 或 undefined

//可能存在的传统写法,除了null,undefined, 无法兼容0、''、false的情况,使用时要特别小心
const value1 = foo || 'default'; // 'default'
const value2 = bar || 'default'; // 'default'
const value3 = baz || 'default'; // 'default',因为 0 转布尔类型是 false
const value4 = qux || 'default'; // 'default',因为 '' 转布尔类型是 false
const value5 = xyz || 'default'; // 'default'

2.2 使用场景:



  • 提供默认值,而不使用 falsy 值(如空字符串、0 等)。

  • 在处理可能为 null 或 undefined 的变量时,选择性地提供备用值。


3. 空值合并赋值操作符 (Nullish Coalescing Assignment Operator - ??=):


空值合并赋值操作符结合了空值合并操作符和赋值操作符。它用于将默认值分配给变量,仅当变量的值为 null 或 undefined 时。


3.1 用法示例:


let foo = null;
let bar = undefined;
let baz = 0;

foo ??= 'default'; // 'default',因为 foo 是 null
bar ??= 'default'; // 'default',因为 bar 是 undefined
baz ??= 'default'; // 0,因为 baz 的初始值不是 null 或 undefined

3.2 使用场景:



  • 在变量没有被赋值或被赋值为 null 或 undefined 时,将默认值分配给变量。


4. 注意:


这些运算符在处理可能为 null 或 undefined 的值时非常有用,可以简化代码并提高可读性。然而,需要注意的是,它们是在 ECMAScript 2020 标准中引入的,因此在旧版本的 JavaScript 中可能不被支持。


作者:阿虎儿
来源:juejin.cn/post/7270900584466513974
收起阅读 »

争论不休:金额用Long还是BigDecimal?

问题 今天在网上看到一个有意思的问题,金额的数据类型用Long还是BigDecimal? 具体问题大概是这样的:关于金额的数据类型,组长认为使用BigDecimal比较稳妥,总监认为使用Long才不会出问题,然后开发认为Long用起来比较爽。 从这两个数据类...
继续阅读 »

问题


今天在网上看到一个有意思的问题,金额的数据类型用Long还是BigDecimal?


具体问题大概是这样的:关于金额的数据类型,组长认为使用BigDecimal比较稳妥,总监认为使用Long才不会出问题,然后开发认为Long用起来比较爽。



从这两个数据类型来看,这家公司使用的开发语言应该是Java,不过换成其它开发语言,也有类似的数据类型选择问题,这是一个广泛存在的问题,所以可以和大家好好聊聊。


网友方案


针对这个问题,热情的网友们从各自的经历出发,提供了很多方案。我大概总结了下,居然有十种之多,虽然有的像调侃,但都有一定的道理。相信大家也很好奇,所以这里我先分享下网友们的方案。


Long




解读:单位到分,没有小数点,也就没有小数精度的问题。而且Long取值范围也足够了。


BigDecimal




解读:大家都这么用,BigDecimal就是为精确计算而生的。用long不专业,适应性不好。


Long和BigDecimal




解读:成年人不做选择,成年人什么都要。金额、价格这些用Long,汇率、费率这些要求的小数点比较多,那就用BigDecimal。


String




解读:万物皆可string。只是处理规则需要全部自己写,高手必备的技能。


Protobuf



解读:脱离框架讲方案都是耍流氓。Protobuf里边根本就没有BigDecimal,虽然可以用string或者自定义类型来代表Java中的BigDecimal,不过性能可能要差那么一点点。


自定义




解读:架构师的好苗子。程序不是能跑起来、不出错就行了,要考虑设计能不能自然体现业务需求,好不好理解、扩展和维护。


听领导的




解读:霍金来了中国也得站起来敬酒。这根本不是技术问题,一切听领导指示,但是也要做好自我保护。


问AI



解读:紧跟时代风口。作为有追求的技术人,就应该想着怎么偷懒怎么最快,先进的生产力工具要用起来,大语言模型回答这个问题滴水不漏、手到擒来,不信你试试!


节省型




解读:节俭是美德。就几百块钱的货,又不是航母和火箭,根本用不着Long,用int、short,甚至byte就能满足。


莫名其妙



解读:这个特定芯片是说CPU做不了浮点数运算吗?还是说不同的CPU浮点数运算的算法不同?那编程语言不能直接处理这个问题吗?还需要开发者关心。不懂,真不懂,完全不懂,请有经验的大神帮解答下。


根本问题


俗话说,结局问题先得明确问题。那么这到底是个什么问题呢?归根到底还是小数的精度问题。


有时候是根本除不尽,比如10除以3;有时候是因为小数的表达问题,编程语言中带小数的数据类型一般是float和double,它们内部使用科学计数法,转换二进制的时候有可能出现无限小数位的问题,比如Javascript中的0.1+0.2算出来就不是0.3。


所以为了避免此类问题,大家想出来了各种各样的方法。


其实使用Long和BigDecimal的本质都是一样的,他们内部都是通过整数记录值,只是Long属于隐式设定小数点,BigDecimal属于显示设定小数点。


比如,使用Long表示价格时,系统约定单位是分,那么9999就代表99.99元;而使用BigDecimal表示价格时,则需要明确小数位 new BigDecimal("99.99")。


另外不管是Long还是BigDecaimal只要发生除不尽,就存在精度问题。


解决方案


这里我做个总结。


在程序中处理金额时,最佳实践通常是使用类似BigDecimal的数据类型,因为它提供了精确的小数运算能力,这对于财务计算来说非常重要。使用BigDecimal可以避免因浮点数的精度问题导致的计算误差,这些误差在金融应用中可能会导致严重的问题。


BigDecimal可以精确地表示和计算小数,它允许你定义小数点后的精度,并且提供了一系列的舍入模式。这意味着当你需要执行加减乘除时,可以控制舍入行为以符合金融计算的要求。


另一方面,虽然使用Long类型来表示金额(通常以分为单位)也是一种选择,因为它避免了小数的使用,从而也能保证精确性。但是,这种方法在表示和处理小数时就不那么直观,而且在需要进行货币转换或者涉及到小数的计算时,你必须自己管理小数点的位置。


例如,如果使用Long表示金额,你需要记住金额是以分为单位还是以元为单位,而且在报告或用户界面中显示金额时,通常需要将金额转换为以元为单位的格式,这就需要额外的计算步骤。


所以,虽然Long类型也可以用来精确地表示金额,但是为了代码的可读性、易用性和减少手动处理小数点的错误,推荐使用BigDecimal来处理金额。这是一种更安全、更灵活的方法,尤其是在需要精确计算小数时。


其它使用string或者自定义类的方案,当然也可以,只是需要更多的工作来完善数据处理的各种规则,容易出错,也不规范,为什么不使用现成的BigDecimal呢?




以上就是本文的主要内容。


关注萤火架构,提升技术不迷路!


作者:萤火架构
来源:juejin.cn/post/7314928953193578505
收起阅读 »

用lodash开发前端,真香!

web
前言 在日常的前端开发中,总是涉及到对数据的处理,比如后端返给你一坨数据,你需要进行处理并回显到页面上,又或者提交form表单到服务端时,你需要将数据处理成后端接口定义的数据结构,而这些都离不开数据处理。 那数据处理有什么好用的工具库吗?lodash当之无愧。...
继续阅读 »

前言


在日常的前端开发中,总是涉及到对数据的处理,比如后端返给你一坨数据,你需要进行处理并回显到页面上,又或者提交form表单到服务端时,你需要将数据处理成后端接口定义的数据结构,而这些都离不开数据处理。


那数据处理有什么好用的工具库吗?lodash当之无愧。


lodash使用


使用:


// 浏览器环境
<script src="lodash.js"></script>
// npm
npm i --save lodash

接下来给大家介绍下我平时开发用lodash最最最常用的一些方法。


一、数组类


1、_.compact(array)


作用:剔除掉数组中的假值假值包括falsenull,0""undefined, 和 NaN这5个)元素,并返回一个新数组。


使用示例


const _ = require('lodash')
console.log(_.compact([0, 1, false, 2, '', 3, undefined, 4, null, 5]));
// 输出 [ 1, 2, 3, 4, 5 ]

项目中的应用:剔除数组中的一些脏数据。


2、_.difference(array, [values])


作用:过滤掉数组中的指定元素,并返回一个新数组


使用示例


const _ = require('lodash')
console.log(_.difference([1, 2, 3], [2, 4]))
// 输出 [ 1, 3 ]
const arr = [1, 2], obj = { a: 1 }
console.log(_.difference([1, arr, [3, 4], obj, { a: 2 }], [1, arr, obj]))
// 输出 [ 1, 3 ]

类似方法



  • _.pull(array, [values]),与_.difference不同之处在于_.pull会改变原数组。

  • _.without(array, [values]): 剔除所有给定值,并返回一个新数组,这个方法的作用和_.difference相同。


项目中的应用:这个可以在某些场景代替掉Array.prototype.filter


3、_.last(array)


作用:返回数组的最后一个元素


console.log(_.last([1, 2, 3, 4, 5]))
// 输出 5

项目中的应用:有了这个方法,就不需要通过arr[arr.length - 1]这样去取数组的最后一项了,比如一个省市区级联选择器Cascader,但传给后端的时候只需要最后一级的id,所以直接用_.last取最后一项给后端。


类似方法



  • _.head(aray)方法,返回数组的第一项

  • _.tail(array)方法,返回除了数组第一项以外的全部元素


顺便一提,我在实际开发项目中还遇到过用数组的pop方法去取最后一项的,然后由于取了两次调用了两次pop,造成了一个bug,让人哭笑不得。


4、_.chunk(array, [size=1])


作用:将数组按给定的size进行区块拆分,多余的元素会被拆分到最后一个区块当中,返回值是一个二维数组。


使用示例


console.log(_.chunk([1, 2, 3, 4, 5], 2))
// 输出: [ [ 1, 2 ], [ 3, 4 ], [ 5 ] ]

项目中的应用:比如你需要渲染出一个xx行xx列的布局,你就可以用这个方法将数据变变成一个二维数组arrarr.length代表行数,arr[0].length代表列数


二、对象类


1、_.get(object, path, [defaultValue])


作用:从对象中获取路径path的值,如果获取值为undefined,则用defaultValue代替。


使用示例


const _ = require('lodash')
const object = { a: { b: [{ c: 1 }, null] }, d: 3 };

console.log(_.get(object, 'a.b[0].c'));
// 输出 1
console.log(_.get(object, ['a', 'b', 1], 4));
// 输出 null
console.log(_.get(object, 'e', 5));
// 输出 5

项目中的应用:这个是获取数据的神器,再也不用写出if(a && a.b && a.b.c)的这种代码了,直接用_.get(a, 'b.c')搞定,_.get里面会帮你做判断,绝对省事!


2、_.has(object, path)


作用:判断对象上是否有路径path的值,不包括原型


使用示例


const _ = require('lodash')
const obj = { a: 1 };
const obj1 = { b: 1 }

const obj2 = Object.create(obj1)

console.log(_.has(obj, 'a'));
// 输出 true
console.log(_.has(obj2, 'b'));
// 输出 false

项目中的应用:这个可以代替Object.prototype.hasOwnProperty,判断对象上有没有某个属性。


3、.mapKeys(object, [iteratee=.identity])


作用:遍历并修改对象的key值,并返回一个新对象。


使用示例


const _ = require('lodash')
const obj = { a: 1, b: 1 };

const res = _.mapKeys(obj, (value, key) => {
return key + value;
})
console.log(res)
// 输出 { a1: 1, b1: 1 }


项目中的应用:调接口传递给后端数据时,如果定义的key和后端接口数据结构定义的key不匹配,可以用_.mapKeys进行适配。


4、.mapValues(object, [iteratee=.identity])


作用:遍历并修改对象的value值,并返回一个新对象。


使用示例


const _ = require('lodash')
const obj = { a: { age: 1 }, b: { age: 2 } };

const res = _.mapValues(obj, (value) => {
return value.age;
})
console.log(res)
// 输出 { a: 1, b: 2 }

项目中的应用:依次对对象values值进行处理,进行数据格式化,以适配后端接口。


5、_.pick(object, [props])


作用:从object中挑出对应的属性,并组成一个新对象


使用示例


const _ = require('lodash')
const obj = { a: 1, b: 2, c: 3 };

const res = _.pick(obj, ['a', 'b'])
console.log(res)
// 输出 { a: 1, b: 2 }

项目中的应用:从后端接口中,pick出对应你需要用的值,然后进行逻辑处理和页面渲染,或者pick对应的值,传给后端。


6、.pickBy(object, [predicate=.identity])


作用:与_.pick类似,只是第二个参数是一个函数,当返回为真时才会被pick


使用示例


const _ = require('lodash')
const obj = { a: 1, b: 2, c: 3 };

const res = _.pickBy(obj, (val, key) => val === 2)
console.log(res)
// { b: 2 }

项目中的应用:是_.pick的增强版,可以实现动态pick


7、_.omit(object, [props])


作用:_.pick的反向版,忽略掉某些属性后,剩下的属性组成一个新对象。


使用示例


const _ = require('lodash')
const obj = { a: 1, b: 2, c: 3 };

const res = _.omit(obj, ['b'])
console.log(res)
// { a: 1, c: 3 }

项目中的应用:代替delete obj.xx,剔除某些属性。


8、.omitBy(object, [predicate=.identity])


作用:_.omit的增强版,第二个参数是一个函数,当返回为真时才会被omit


使用示例


const _ = require('lodash')
const obj = { a: 1, b: 2, c: 3, cc: 4 };

const res = _.omitBy(obj, (val, key) => {
return key.includes('c');
} )
console.log(res)
// { a: 1, b: 2 }

项目中的应用:与_.omit类似。


9、_.set(object, path, value)


作用:给object上对应的path设置值,路径不存在会自动创建,索引创建成数组,其它创建为对象。


使用示例


const _ = require('lodash')
const obj = { };

const res = _.set(obj, ['a', '0', 'b'], 1)
console.log(res)
// 输出:{ a: [ { b: 1 } ] }

const res1 = _.set(obj, 'a.1.c', 2)
console.log(res1)
// 输出:{ a: [ { b: 1 }, { c: 2 } ] }

项目中的应用:给对象设置值,再也不用设置的时候一层层判断了。


10、_.unset(object, path)


作用:与_.set相反,删除object上对应的path上的值,删除成功返回true,否则返回false


使用示例


const _ = require('lodash')
const obj = { a: [{ b: 2 }] }

const res = _.unset(obj, ['a', '0', 'b'])
console.log(res)
// 输出:true
const res1 = _.unset(obj, ['a', '1', 'c'])
console.log(res1)
// 输出:true

项目中的应用:给对象删除值,替换delete a.b.c。使用delete如果在访问a.b.c的时候,发现没有b属性就会报错,而_.unset不会报错,有更加好的容错处理。


三、实用函数


1、_.cloneDeep(value)


作用:标准的深拷贝函数,这个无须多言,用过的人都说好


使用示例


const _ = require('lodash')
const obj = { a: [{ b: 2 }] }

const res = _.cloneDeep(obj)
console.log(res)
// 输出:{ a: [ { b: 2 } ] }

项目中的应用:代替JSON.parse(JSON.string(obj))等深拷贝方法,能处理循环引用,有更好的兼容性。


2、_.isEqual(value, other)


作用:深度比较两者的值是否相等


使用示例


const _ = require('lodash')
const obj = { a: [{ b: 2 }] }
const obj1 = { a: [{ b: 2 }] }

const res = _.isEqual(obj, obj1)
console.log(res)
// 输出:true

项目中的应用:比较form表单前后的数据是否发生了变化,再也不用自己循环两次+递归去手动比较了。


3、_.isNil(value)


作用:某个值是null或者undefined


使用示例


const _ = require('lodash')
let a = null;

const res = _.isNil(a)
console.log(res)
// 输出:true

项目中的应用:有时候我们并不想用if(obj.xx)判断是否有值,因为0也是算有值的,而且可能在后端定义中还有含义,但它转成boolean去判断却是false,所以我们用_.isNil去判断更为准确。


4、_.debounce(func, [wait=0], [options=])


作用:标准的防抖函数,简单理解就是,函数被触发多次,只有最后一次会被触发


使用示例


const _ = require('lodash')

const fn = () => ({
fetch('https://xxx.cn/api')
})
const res = _.debounce(fn, 3000)

项目中的应用input输入框的实时搜索,减少接口调用,节约服务器资源。


5、_.throttle(func, [wait=0], [options=])


作用:标准的节流函数,简单理解就是,函数被触发多次,在指定时间范围内只会调用一次


使用示例


const _ = require('lodash')

const fn = () => ({
fetch('https://xxx.cn/api')
})
const res = _.throttle(fn, 300)

项目中的应用:监听页面scroll事件滚动加载,监听页面的resize事件等。


6、_.isEmpty(value)


作用:判断一个对象/数组/map/set是否为空


使用示例


const _ = require('lodash')

const obj = {}
const res = _.isEmpty(obj);
console.log(res)
// 输出 true

项目中的应用:对传入的数据做非空校验。


7、_.flow([funcs])


作用:传入一个函数数组,并返回一个新函数。_.flow内部从左到右依次调用数组中的函数,上一次函数的返回的结果,会作为下个函数调用的入参


使用示例


const _ = require('lodash')

const add = (a, b) => a + b;
const multi = (a) => a * a;
const computerFn = _.flow([add, multi]);
console.log(computerFn(1, 2))
// 输出 9

项目中的应用:我们可以把各种工具方法进行抽离,然后用_.flow自由组装成新的工具函数,帮助我们流式处理数据,有点函数式编程那味儿了。


8、_.flowRight([funcs])


作用:与_.flow相反,函数会从右到左执行,相当于React中的compose函数


使用示例


const _ = require('lodash')

const add = (a) => a + 3;
const multi = (a) => a * a;
const computerFn = _.flowRight([add, multi]);
console.log(computerFn(4))
// 输出 19

项目中的应用:与_.flow类似,遇到相关场景,用flow还是flowRight都行,看个人习惯。


小结


以上就是我个人在项目中常用的lodash方法了,使用体验是非常好的,节约了不少处理数据的时间,所以想分享给大家。


大家熟练用起来,摸鱼时间这不就有了么!


作者:han_
来源:juejin.cn/post/7277799790296416290
收起阅读 »

写 Vue 我建议非必要别用 watch

web
场景 代码大概如下,删除了很多无关内容。 <template> <div> <SearchBar @search="handleSearch" /> <Pagination v-mode...
继续阅读 »

场景


代码大概如下,删除了很多无关内容。


<template>
<div>
<SearchBar @search="handleSearch" />
<Pagination
v-model:page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
/>

</div>
</template>
<script setup lang="ts">
import { reactive, ref, watch, inject, computed } from 'vue'
import SearchBar from '@/components/SearchBar.vue'

const route = useRoute()
const pagination = reactive({
page: 1,
pageSize: isPublic.value ? 10 : 9,
total: 0,
})
const keyword = ref('')
const fetchList = async () => {
loading.value = true
const res = await connect.get(`/api/${route.params.type}`, {
params: {
pageSize: pagination.pageSize,
page: pagination.page,
name: keyword.value,
},
})
pagination.total = res.total
loading.value = false
}
watch(
() => route.params.type,
async () => {
pagination.page = 1
fetchList()
},
{ immediate: true }
)
watch(
() => pagination.page,
async () => {
fetchList()
}
)
watch(
() => keyword.value,
async () => {
if (pagination.page === 1) fetchList()
else {
pagination.page = 1
}
}
)
const handleSearch = (val: string) => {
keyword.value = val
}
const handleDelete = async (item: MindMapItem) => {
await confirmModal.value?.confirm()
await connect.delete('/api/map/' + item._id)
fetchList()
}
</script>

本来只有 2 个 watch,今天新功能加了个关键词搜索,又得多 watch 一个 keyword.value


于是这里变成了 3 个 watch,而且里面有逻辑,甚至是相互依赖的逻辑。


上面的代码没写完,但是整理一下,最终目标是这样的:



  • 请求参数有三个变量:route.params.type、keyword 和 pagination

  • route.params.type 改变时需要重置 pagination 和 keyword,然后重新请求

  • keyword 改变时需要重制 pagination,然后重新请求

  • pagination 改变时需要重新请求


watch 真的好?


如果继续用 watch,因为需要重置 pagination 和 keyword,硬生生把三个 watch 写成了个像是任务委托一样的效果,例如 keyword.value 修改时如果 page 是 1 就直接请求,否则修改 page 再让 page 的 watch 触发请求。


watch(
() => keyword.value,
async () => {
if (pagination.page === 1) fetchList()
else {
pagination.page = 1
}
}
)

这么耦合真的好吗?这不好。我劝自己耗子尾汁,好好反思。


得出的结论是:watch 不是好文明,能不用 watch,就别用 watch


这不是我第一次对 watch 有意见,在工作中我就见过很多复杂组件动则 5 个以上的 watch,有的里面还有复杂逻辑。


重点是啥,还没注释……watch 天然就容易让人不写注释,给人一种“啊,这个值变了,运行下面的逻辑是理所当然的吧。”,那你问问两个月后的自己,是不是真的这样?你自己写的 watch 你自己看得懂吗?一个值变了就触发逻辑,但问题是,它变的原因可多了。


所以 watch 生而在语义上不明确,它只解释了对值的依赖,没有解释依赖的原因。


watchEffect 呢?


上面的例子,假如把 fetchList 写成 watchEffect,其实还是一样的问题,需要在里面额外加 if else 处理重置逻辑。不过逻辑集中在一个 watchEffect 大概还是比分散在 N 个 watch 里好。


总结


总结一下,watch 或者 watchEffect 有其用武之地,但最好满足以下的条件:



  • 变动触发点大于 2 个才考虑 watch(只有一个触发机会的话,什么时候用,什么时候跑就好了)

  • 所有场景全都适用同一个处理逻辑

  • 与其他 watch 没耦合


不过如果没有事件机制来触发的话,那就只能 watch 了。


优化后


<template>
<div>
<SearchBar @search="handleSearch" />
<Pagination
v-model:page="pagination.page"
@update:page="fetchList"
:page-size="pagination.pageSize"
:total="pagination.total"
/>

</div>
</template>
<script setup lang="ts">
import { reactive, ref, watch, inject, computed } from 'vue'
import SearchBar from '@/components/SearchBar.vue'

const route = useRoute()
const pagination = reactive({
page: 1,
pageSize: isPublic.value ? 10 : 9,
total: 0,
})
const keyword = ref('')
const fetchList = async () => {
// 省略
}
watch(
() => route.params.type,
async () => {
keyword.value = ''
pagination.page = 1
fetchList()
},
{ immediate: true }
)
const handleSearch = (val: string) => {
keyword.value = val
pagination.page = 1
fetchList()
}
const handleDelete = async (item: MindMapItem) => {
await confirmModal.value?.confirm()
await connect.delete('/api/map/' + item._id)
fetchList()
}
</script>

修改后,只保留 route.params.typewatch,不会发生冲突,另外两个通过事件触发。至于触发事件也不用额外写 @change,直接用 @update:xxx 就可以了。


这样只有易读的重置逻辑,没有 if else!清爽!


原文传送门:ssshooter.com/vue-watch/


作者:ssshooter
来源:juejin.cn/post/7314860085931065359
收起阅读 »

小公司-小前端团队,如何一步步走向成熟?

web
现状 去年下半年,加入了一家小公司,前端团队也是刚刚成立没多久,虽然自己心里上已经提前预设了团队可能存在的种种问题,但是,进入以后,还是发现了一系列比较明显的问题,这里列举其中一些典型问题: 前后端代码没有分离,发布上线等没有分离 前端技术栈单一,所有项目都...
继续阅读 »

image.png


现状


去年下半年,加入了一家小公司,前端团队也是刚刚成立没多久,虽然自己心里上已经提前预设了团队可能存在的种种问题,但是,进入以后,还是发现了一系列比较明显的问题,这里列举其中一些典型问题:



  • 前后端代码没有分离,发布上线等没有分离

  • 前端技术栈单一,所有项目都是直接采用vue3-vben-admin

  • 代码规范,commit规范等等没有统一,CI/CD流程不规范。

  • 没有设计,产品等,导致整体开发流程不规范。

  • ...其他


之所以,列举以上这些问题,并不是对我司有任何的不满啊,哈哈哈,更多的是希望能够给遇到类似问题的小伙伴儿一些方案或者方向,同时,也能够帮助大家的前端团队逐渐走向规范与成熟。


写作不易,如果这篇文章对您有所帮助,欢迎 关注♥️ + 点赞👍 鼓励一下作者,感恩~


成熟的前端团队是什么样子?


前端团队刚刚成立不久,如何一步步走向规范与成熟呢?很显然,我们需要知道一个成熟的前端团队是什么样子的(当然一般谈论这种话题,很有可能被喷),其实呢?没有一个明确的标准,而且公司业务不同,前端的技术栈,基建等都会不同,这里只是列举出了建设前端团队比较常见的一些方向供大家参考:


image.png


前端规范


这里,总结出了一些常见的规范,直接上图:


image.png


如果大家所在团队中,对这些规范没有进行统一,可以参考上面这些方向在团队中进行推广。


前端项目模版


大家肯定都知道vue,react的官方脚手架工具:vue-cli, create-react-app,通过这些基础脚手架,就可以帮助我们创建最基础的前端项目代码,但是随着业务的迭代,各个公司的业务场景不同,技术栈不同,往往需要在vue-cli, create-react-app创建的项目模版基础上,逐渐沉淀出符合公司的自定义项目模版代码,具体可以参考下图:


image.png


前端脚手架


上面,我们讲了前端项目模版,那如何更好的去管理模版呢?答案就是脚手架,加入没有脚手架,我们很可能是直接将模版代码放在一个单独的仓库中,每次开启一个新项目,就clone到本地,然后在copy一份出来,这样虽然也可以做,但是脚手架可以通过命令更好更快捷的帮助我们去管理项目模版,以及进行项目初始化等等操作。


image.png


当然,脚手架的技术栈和传统的前端项目的技术栈有所不同,上面图中也有说到,底层依赖NodeLerna,Yargs等,感兴趣的可以学习一些,是否要维护公司自己的脚手架,就要评估人力成本,收益等,结合团队的实际情况进行考量了。


前端自动化构建部署(CI/CD)


这部分就是大家常说的CI/CD,即前端项目如何持续集成与部署,这里就不额外展开说啦,具体可以参考我自己写的一篇文章:基于Docker + Nginx + Gitlab-runner 构建前端CI/CD


有一点说明一下:可能早期,工作经验不多的前端小伙伴儿,会遇到这种情况,每次项目发布上线,可能都是直接使用公司现成的发布系统,直接在页面点点就可以成功,但是往往遇到问题的时候,就不太知道怎么去排查问题,还得请教运维等相关同学,遇到好交流的同事可能还帮你解决一下,遇到一些不友好的同事,你自己内心也会一万个....


因此,随着前端的不断发展,对于前端的要求也会越来越高,我们也有必要知道前端项目底层到底是如何进行CI/CD,如何去发布上线,这里就会涉及到Docker,K8s,Nginx,CI工具等技术栈,感兴趣的同学也可以去写一些demo,了解了解。


前端全链路监控体系


其实就是随着项目的迭代,功能越来越复杂,尤其是一些C端的项目,我们需要去掌握用户的行为,从而,根据用户的行为,去进一步更好的迭代我们的项目, 那么,这就需要我们对这些行为进行监控,也就是大家常说的埋点,


一个完整的监控体系,通常包含如下内容:
image.png


如果有的小伙伴儿所在团队有这样的需求,那么就要考虑如何去做啦,目前市场上也有一些开源的方案可以参考,例如:Sentry,当然,也要结合团队实际情况,看是否需要自己去实现一套完整的监控体系,因为实现成本也不低,尤其小公司,我们就需要调研调研,是否可以使用一些开源的方案去实现啦。


前端物料库


什么是前端物料?其实就是大家常说的组件库,工具库等可以复用的代码,具体可以参考下图:


image.png


一般大厂都会有类似的物料平台,那么,我们小公司呢?就要考虑其实现成本和收益啦,也不一定非要建立物料平台,因为小公司能够沉淀的物料也不会有那么多,比如:一般有沉淀一些组件库,工具库,我们也可以发布到npm上,这样团队内部也可以使用。


怎么做?


那具体怎么做呢?主要从以下几方面考虑:



  1. 明确要解决的问题:结合公司团队当前情况,按照优先级明确现有问题

  2. 明确要解决的问题的具体实现方案:通过调研,团队讨论等方式明确各个方案利弊,选择最优方案

  3. 明确具体的执行步骤:从团队实际情况出发,最好是渐进式开启,在对现有业务不影响的前提下做增量式基建工作


于是,我进一步结合我司的情况,明确了以下几点是要优先去实现的:



  • 确定前端技术栈

  • 明确前端规范

  • 前后端代码分离,打造独立的前端CI/CD


确定前端技术栈


由于我司目前主要中后台项目居多,这里确认的技术栈也主要基于此方向展开的。


首先,传统的中后台项目,前端一般会包含以下这些内容:


image.png


那以上这些内容,如何实现呢?可以从三个方向展开:



  • 自己团队手动封装,形成团队自己的一套最佳实践(其实就是结合公司业务场景,逐渐沉淀出一套初始化项目的项目模版)

  • 借助社区开源方案:这里推荐:蚂蚁开源的UmiJS
    image.png


    image.png
    简单来说,该框架就是以插件的形式集成了传统中后台解决方案常见的内容,例如常见的路由管理,权限管理等等,我们只需要引入相应的插件即可。


  • 使用现成的开箱即用的中台前端解决方案框架,这里推荐以下几个框架:





那我司是如何选择的呢?历史项目使用了一部分Vue3 Vben Admin,新项目统一采用Ant Design Pro


image.png


这里重点对比一下Vue-vben-adminAnt design pro



  • Vue3 Vben Admin



    • 优势



      • 当前使用技术栈,且用了两到三年,积累了一定的经验,趟过了一些坑。

      • 整体功能相对比较齐全,不需要从零开发。



    • 劣势



      • 本地版本迭代更新机制不太友好,需要开发者每次手动clone最新版本的模版仓库,然后还需要将原来的业务代码进行copy,而且如果对源码代码有更新的话,会更加麻烦,例如:一个组件,我们可能在项目中进行了二次修改,然后,我们更新vben版本的时候,作者很有可能也对该组件进行了代码更新,这个时候,就需要比对新旧组件代码,容易出现问题。

        • 底层原因一:项目架构整体相对比较简单,没有采用monorepo架构,模版项目代码中封装的hook,组件等内容没有单独发npm包,没有版本管理。

        • 底层原因二:封装的组件,自定义render能力有限,需要我们手动修改组件源码。





    • 部分源码嵌套层级较深,新手上手成本较大,随着业务的迭代,源码和业务代码容易混淆,导致后期版本升级较难。

    • 目前我们项目首屏渲染过慢(后期可能会成为一个比较明显的问题)



  • Ant Design Pro



    • 优势

      • 整体社区生态更加完善,基于 umi + ant-design pro component,二次封装的组件等内容都有版本管理,作为开源项目,更利于开发者去通过npm包的形式去按需引入,同时提供了一下自定义入口,可以帮助我们二次开发。



    • 劣势

      • 新技术栈,初期需要一些学习成本,需要淌一些坑。






综合考虑了几点,团队计划采用 React + Umijs + And-design-pro 来作为目前复杂项目的核心技术栈,同时,一些简单项目,我们也可以直接使用creat-react-app脚手架去初始化项目,同时,一些官网,也采用了WordPress去快速建站(国外使用的较多)这样可以节省前端的开发成本。



前端中台解决方案 之 umijs + ant-design-pro 调研踩坑全记录
如果小伙伴对上面这些技术栈,尤其是React + Umijs + And-design-pro这一套有经验,也欢迎评论区分享,看有没有哪些问题或者坑可以避免。



明确前端规范


确认技术栈以后,接下来,就是要明确前端规范,保证团队开发统一,再次贴出之前总结的图:


image.png


这里,再把其中一些关键点说明一下:



  • 编码规范:除了确认规范标准之后,在项目中还需要借助工具化:Eslint + Prettier + Stylelint 要确保项目中引入这些工具,并且进行有效检测。

  • Git规范:常见两点就是:Commit Message 规范以及Branch命名规范,这些也都可以借助工具:husky + lingstaged来进行约束。

  • UI规范:对于一部分中后台项目,可能没有专门设计参与,这个时候就需要前端对于整个页面的设计交互有一个更好的认识和把握,推荐大家可以参考:Ant Design设计规范,里面列出了常见页面场景的交互规范,可以帮助我们更好的提升项目的用户体验。


前后端代码分离,打造独立的前端CI/CD


CI/CD这部分,通常也需要前端整体有一个认识和把握,这样可以帮助我们了解前端项目在整个集成部署过程中,内部的实现流程,也可以帮助我们更快的去定位问题,解决问题。


这里就不额外展开了,有类似需求的同学可以参考我之前总结的:# 基于Docker + Nginx + Gitlab-runner 构建前端CI/CD


文档建设


文档建设,其实也是必不可少的一个环节,包括我们上提到的这些前端规范,项目等内容,都可以逐步去沉淀到我们团队的文档中,随着内容的积累,沉淀的文档也会成为团队必不可缺的财富。


如果想推动团队进行文档建设的小伙伴儿,可以从以下几方面展开:



  • 新人报到(VPN配置、项目启动)

  • 规范类

    • 流程规范:git分支、commit规范

    • 编码规范:eslint、文件名、代码复杂度



  • 项目类

    • 需求文档

    • 研发方案

    • 项目总结



  • 技术类

    • 常见的技术分享

    • 项目中的典型的技术点总结




总结


以上内容虽然相对基础,也很简单,但其实都是前端团队必不可少的,相信做完以上这些,我们的前端团队会变得更加规范和专业,当然,距离一个成熟的前端团队,大厂前端团队来说,我司的前端团队才刚刚起步,同时不同的团队,不同的业务,也会有不同的基建工作,这篇文档也会伴随着我司前端团队的成长去逐步的更新,相信会越来越好,前端不易,尤其这两年,前端已死,裁员,降薪等等负面消息在不断冲击着每一个程序猿,相信大家都可以熬过去的,祝大家越来越好。


写作不易,欢迎小伙伴儿们点赞收藏+关注,感恩。


参考文档



作者:寻觅人间美好
来源:juejin.cn/post/7221359467618517052
收起阅读 »

一个30岁前端老社畜的人生经历

web
前言 在掘金多年,我一直是一个读者,从事前端快8年了,每天都在看一些视频和资料以及别人的日记,零零碎碎我也做过一些笔记,但是都不成体系。这些笔记至今留存在各种应用上,写了就再也没打开过,还是没有养成习惯,我希望能坚持下去,为自己的人生添加一点历史,等以后老了,...
继续阅读 »

前言


在掘金多年,我一直是一个读者,从事前端快8年了,每天都在看一些视频和资料以及别人的日记,零零碎碎我也做过一些笔记,但是都不成体系。这些笔记至今留存在各种应用上,写了就再也没打开过,还是没有养成习惯,我希望能坚持下去,为自己的人生添加一点历史,等以后老了,我还能证明我的青春有过一些记录,偶尔回味也会是一件比较幸福的事情。


近些年,感觉社会戾气挺重的,特别是疫情的时候,抖音里面的那些评论很让人糟心,现在年轻人也逐渐选择躺平,也是对社会的卷系妥协,随着经济的下滑,一般学校的研究生可能都很难找到一个比较ok的工作,更别提本科或者大专,作为学历真的拿不出手的我,更加焦虑。


从业前端快8年了,做过很多类型的项目,小到一般的H5展示网页,大到区块链应用、智能能源项目;其实回过头来看,没有什么大的成就感,我的从业经验只获得过一次奖杯,就是吃苦耐劳奖一个镀金的大手指,那还是我4年前在一家外包公司连续工作48小时做一个小程序上线后,老板看我确实辛苦,于是发了一个这个奖杯给我,后面被我娃摔坏了,就啥也没有了。


2023


2023年其实回头来看,收获并不是很大,归纳下来也没有几条:


  1. 今年非全研究生在读了

  2. 今年提交了入党申请

  3. 第二套房子装修完成

  4. 小孩来到了身边读书(之前在农村读幼儿园)

  5. 工作中学会了Vue3,能用Java做开发,同时更了解了业务方面的知识

  6. 开始了写作的习惯

  7. 跑了5场马拉松


2024 展望


2024年还有几天就到了,我希望每一年都能有点收获吧,立几个flag:


  1. 带妈妈旅游一次

  2. 成为党员积极分子

  3. 提高Java方面的基础知识,以及three 3D方面的能力

  4. 看不低于5本技术书籍,至少写30篇技术文章

  5. 还清自己的个人债务,当然不包含房贷

  6. 跑5场马拉松


行业展望


目前行业有些自媒体在唱衰,说前端已死,但是我觉得没这么悲观,国家多次强调往智能方向发展,各行业的智能得依托计算机才能智能,像什么智慧制造,智慧能源,智慧农村等等都需要计算机技术来运算和展示。前端只是比以前的要求会高一些了,在5,6年前对前端技术要求没有那么高,大家0基础都可以参与,但如今可能不行了,我觉得这是一个好事,要求高一点,薪资也会高一些。淘汰的,就是一开始就不适合这个行业的人。我目前在一家大型央企工作,还是算较为稳定,但同时也需要不断学习,因为或许某一天的淘汰人选就会是我,社会是残酷的,混日子终究不是一个好的方式。


个人真实经历分享


给大家分享一下我的个人真实经历,与君共勉。


我出生在一个普通的农村家庭,初中开始接触老虎机,高中接触网吧,17岁前没有出过县城,是个十足的井底之蛙,父亲几十年一直在外地工作,一年就回家两三次,从小就我妈妈一个人带我,她做装修的,每天早上7点就骑车外出上班了,下午5点回家,我在家做好饭菜等她回家吃了后,她就马上去田里土里种庄稼,喂了很多牲畜,高中毕业前一直都是这样(不过高中我住校,就我妈妈和我妹在家),我妈妈非常节约,从我记事起,每年只有过年她才会舍得给自己买一两件衣服,因为她觉得过年要穿新的衣服,寓意者新的一年有好的开始,从来没给自己买过首饰,也从来没有烫染过头发,也从来没有赌博过,但同时我父亲其实并不是有责任心的人,基本从不过问家里,以及我的学习。


我的学习打小从小就不好,学习生涯当了两个月的劳动委员,这就是我的荣耀,因为我觉得我小时就是sb,在干啥完全不知道,在学校就是为了吃那一顿饭,和同学天天玩,初中要毕业就被学校各种“”“好言相劝” 去中专学技术,学会拿高薪,实际上是为了赚中专学校的回扣。中专后面又把我们送去富士康,天天12个小时流水线,学校也是为了赚富士康的回扣,我的学业就是这样被卖来卖去,突然觉得有些可悲。这也是普遍读书不行的农村孩子的现状。


我的第一份工作是从2013年开始的,到现在已经差不多10年了,那就做一个时间线看看我的悲催往事吧,这也是我第一次对外讲


2013-2014.02


毕业季,和同学们坐着学校包的几辆大巴车,开到了成都郫县的富士康厂区,哪个时候富士康才在这里建厂,每天的工作就是搬东西,从另外一个地方往厂区里面搬,后面正式开工就开始了每天12个小时的白夜班交替,本来身体从小就弱,经常生病,在富士康就是上班,生病,加上富士康十三跳以及厂区经常出事,我和同学晚上提着东西,连夜翻墙走的,对,真是翻墙走的,后面线长给我打电话,我说我已经不在成都了。不过线长是我老乡,还是跟我没算旷工,算正常离职。 这里不得不说一句,在厂里,一个芝麻官都的官威都不得了,我实在看不惯,加上没前途,才走的。那时候天天12小时到手工资3500,自己上班赚钱还挺潇洒的,下班就去麻辣烫,一人吃饱全家不饿,和同学们啤酒小菜吃着,真是潇洒,厂区还有来自五湖四海的同龄妹子,都是中专生,还是挺快乐的,因为大家年龄相同,就是吃喝玩乐。自然半年才没存什么钱,灰溜溜回了老家,被我妈骂了一顿。


总结:富士康收获: 吃喝玩乐,此刻我的人生规划一无所知


2014.2-2015.07


回了老家,每天早上我妈6点就起来做饭、洗衣服、扫地等等,我起来烧柴,跟猪熬糠羹,喂猪,经常都是公鸡还没叫,我们都忙活一阵了,坚持了十天我就受不了了,因为我得承认,我出去工作后我变懒了,但是每天晚上很晚才睡,因为我在成都买了一个山寨的洛基亚手机,我开始在QQ聊天了,枯燥的生活我受不了,我要出去上班,我就去了重庆。就我妈和我妹两个人在家,这里我的说一下,我去重庆了,我妹才读幼儿园,我妈每天已经非常忙了,我妹从小就是邻居照看,她是位留守的老人,她每天给我妹妹吃好喝好的,比我奶奶好了太多,因为我妈性格很强势,和奶奶性格不合,我奶奶从来没照顾过我和我妹,都在伯伯家带他们的孩子,我妈妈经常晚上8.9点才从田里回家,我妹都是在邻居婆婆那儿吃饭睡觉,前几年她去世了,我妈妈哭了好久,因为她是我家的大贵人,现在每次走到她的坟前,我们都会去跪拜她。现在想起来,我妈太伟大了,她一生都是这么勤劳,吃苦。


到重庆了,上了一年多的厂,其实也是浑浑噩噩的,没有学历,只有在厂里做检验员,一个月2400的工资,入不敷出,因为当时听说主管也是中专学习,干了10多年才当主管,主管才5200一月工资,我觉得没前途,加上厂里玩的好的同事也走了,我也就走了


收获:C1驾-照, 成人高考专科录取通知书, iSO9O001,iSO14001 两个体系证书


2015.7-2016.7


这一年我就像做梦一样,2015年3月去学校报到,认识了班花我老婆,然后就开始交往,然后10月的时候,检查到怀孕了,过年就去了她的老家,因为怀孕了,也就准备结婚的事情了,同学们简直惊叹,纷纷问我怎么办到的?我才23岁,当父亲完全没概念,不过这也满足了我家人的愿望,穷人家里早当家,就在这一年,我妈妈存了一辈子的钱就被我花完了。10月检查怀孕,11月孩子她妈跟我父母說了要了买房买车的事情,我妈妈非常反对,后面我外婆对我妈说:你就这一个儿子,你都不帮他,以后他不恨你吗? 我妈妈想了几晚她咬咬牙还是同意了,过年去了女方家,她父母挺喜欢我的,我妈妈第二年年初付了房子首付26万,后面装修8万,买车8万,结婚7万,对没听错,全是我妈出的,她平时在农村做装修,有的时候包工,一个月7,8000有的时候包工一万多一个月工资,省吃俭用,全部存下的,都被我全部榨干了,好在岳父岳母没有要我一分彩礼,还给我2万块钱装修,他们也是农村人,也是吃了很多苦,2万得他们在工地干很久了,他们在老家为我们办了十多桌,请了一村的人来吃饭。


我妈后面才跟我说,这么多钱,我爸只出了一万块钱,我现在都不可思议,他在外面这么多年的钱去哪儿了? 但我也不恨他,毕竟每个人想法不同,他没有义务要给我出这些费用,不过好在之前房子一个平方8000,算是重庆比较贵的房子了,现在26000一个平方,算是赚了一些,有了一个家庭的财产保障,之前还要贵一点,现在房地产不行降了一些。


2016年7月后我出来也是误打误撞的进入了计算机这个行业,我之前压根就不了解这个行业,是看的招聘网站,招聘信息写的5000的工资,我那时候才3000多,在做销售,简直是高薪了,结果去了才知道,原来是计算机培训学校,耐不住那个美女姐姐各种软磨硬泡,我还是去学了计算机,当然,钱还是我妈跟我出的,因为也是孩子她妈跟我妈说这个行业好,比上厂强,我妈才听了她的,要是我说,那根本不管用。


收获:买房,结婚,买车,装修,好像所有的大事这段时间都基本完成了,虽然都是我妈出的钱


2016.7-2018.7


2016年10月孩子出生了,我也从培训学校出来工作了一段时间,培训机构学了4个月,时间都忙家里的事情去了,所以一毕业面试了20多家公司,都被打击了,每次都想放弃,但是回到家,看到家人,我都心里说不出的滋味,为此也哭了好多次,孩子她妈跟着我这些年,没买过一件超过300的衣服,全是淘宝的几十块一件的,我妈妈为了我在农村不管工作有多远,天气有多冷,都要去工作,我觉得我就是累赘,那时我24岁,我压力可能已经超出了我的极限,房贷3000,孩子每个月2000,车子和物业1000,还有生活费,每个月花销都要8000,有的时候孩子一生病就可能要一万以上,我后面找到一份工作4500,是切图仔,每天就jQuery,才稍微帮家里分担了一下,其实压力全部都在我妈妈哪儿,我妈妈为了我,操碎了不少心。


2018年我拿到了大专学历,然后随即开始了报名成人高考本科,孩子她妈就没有报名了,她觉得女孩子大专就够了,加上家里也没钱


收获:
1.当父亲了,压力更大了。我必须得成熟一点了,在前端行业算是正式入行了,通过自己每天工作之外,在各种QQ群里聊天拉业务,我的外快收入也逐渐多了起来,虽然很多时候工作到2点,但是总算是跟家庭减少一点压力,虽然期间换了3家工作,但是我的工资也高了一些,月薪到手9000了,加上外快时多时不多,一个月平均有个1.3的收入了;我也有一点点经济带家人去自驾游了(不过只有两次)
2.成人高考本科录取通知书


2018.7-2020.7


这一年通过我经常在QQ群聊天的好友介绍,我到了一家外包公司(他当时也拿了回扣,但是我也很感激他。因为他教我怎么面试,跟我出面试题),因为通过了客户的面试,我厚着脸皮开到了1.6一个月的工资。到手14k,我当时说我拿这么多,家里人都以为骗他们,不过等发第一个月工资的时候,他们觉得我以前选择计算机是对的,我妈妈也多了很多笑容,这个时候小孩也是大了,妈妈一个人带着孩子读幼儿园,我和孩子他妈在重庆上班,我妈也在上班,家庭算是好了起来,大家笑容也多了起来。当时加上的我的外快业务,一年也能赚个6,7万,因为大家知道我在做这块,后面一些朋友陆续的给我介绍,我也会给他们相对满足的回扣。平均一个月收入已经超过2万了,不过有点不厚道的是,我上班没事也在做外包。


收获:自己随着年龄的增加,人的心态也在发生变化,随着收入多了起来,脸上的笑容也多了,家庭矛盾也少了,日子也越来越有奔头了


2020.7-2023.10


2021年因为公司被客户从人力资源池给移除了,我们没有资格做客户的业务了,我随即也面临着失业,我28岁了,其实我还是很恐慌的,因为家庭开支这么大,加上我长期做外包,技术底子很薄弱,可能失业找不到这么高的工资,所以我很担忧,工作随便都是全日制本科起,我一个半罐子学历,能干点啥,但是后面客户对我的技术能力还有做事能力还算比较认可的,给我推荐了另外还在资源池的外包公司,但是我都不去,我觉得外包没有前途,同时他们也开不起16k的工资(虽然技术不咋的,但是现在是这工资,让我转到其他外包公司才13k,我也心有不甘啊),最后客户他们把我转进了客户内部,于是我一个中专生进入了体制内,不过在进入之前,各部长对我的学历还是有一定质疑,不过我的直系领导以自身名誉担保,我还是通过他们的几轮面试,最终成功进入。进入到体制内,身边的同事都是985的博士,研究生,还有都是留学回来的,也有一些清华北大北航的研究生,其实还是很自卑的,大家学历这么高,有的时候不得不承认,他们的专业素养,学术知识,脑回路都比较灵活,他们的英语都非常厉害,有的同事28岁都上中央台了,太强了,我妈妈非常担心我的学历,怕我一在公司一犯错就被开除,其实有的时候她还是多虑了,我也在尽力 的追赶他们,希望差距尽量小一点点。所以在2023年我拿到了非全的研究生录取通知书,继续读软件工程。在公司也申请入了党,因为他们全是党员,我在公司负责两个部门的前端管理工作,也在带一些校招的研究生,同时我也在2022年5月买了第二套房,首付42万,其他非要加上差不多47万,为此把车卖了一万五,凑点首付钱,我妈妈又出了26万,再一次吧我妈给榨干了,这次我爸也没有出一分钱。不过还在我是组合贷,每个月只出商业贷,每个月出差不多2000的房贷,第一套房贷也还有20多万就还完了,2023年我孩子读小学了,我妈妈也来重庆带孩子了,为此她没有继续在做装修了,每天接送小孩子上学放学,中间有两个小时去一家店里打扫卫生,每个月2500的收入。我在每个月给他2000多生活费,虽然她不太适应城市的生活,觉得城市的人会看不起她是农村人,走哪儿都不会用导航,但是她慢慢的还是习惯了,城市的人并没有觉得自己高人一等,她还算是过得比较快乐,现在我的收入在重庆来说还算OK,外快也有,但是我也不想太累了,我想把时间利用在学习上,因为同事们都很强,我尽量向他们看齐,


收获:本科毕-业-证,非全研究生录取通知书,稳定的工作,第二套房子首付+装修(因为旁边学校更好一点)


最后


今年30了,孩子已经7岁了,我已经开始享受十天的年假了。其实我已经算是走了很多路,深夜哭了很多次,第二天依旧怀揣着斗志,我数次回想我这30年的发展,其实都过得不是很灿烂,或许平凡的就是这样,一无所有的农村人,只能靠父母,如果父母靠不住,那自己也开心点,随着父母的年龄越来越大,我的压力也变大了,他们很多时候会征求我的意见,我也要拿钱支持他们了,也有一些感悟:


1.每年还是得有一个目标,细分到每个月,每一周去完成它,如果没有目标,那就认真的把每一件事情尽量做好,贵人每个人都会遇到,只是看能否抓住,可能会是工作中,生活中的某一个,他愿意提携你一下,真的能少走很多弯路,我的经验告诉我,我有两三次都有贵人帮助我,只是我没有把握住,就像之前一起做区块链,一起做电商的公司老板就很喜欢我,因为我比较踏实,没攻击性,人老实。但是我还是太年轻,很多时候做事不够成熟,就这样和机会擦肩而过,他们现在已经是财富自由了


2.与人交流,说话适可而止,充分尊重他人,聊天中尽量带点幽默,学习一下话题的扩展


3.没事多扩展一下人脉,我才开始培训机构出来,基本没学,全靠在QQ群聊天的人带的我,怎么学,我每次遇到问题我都会问他们,他们在远程帮我改bug,这样我才能保住工作


4.多学习,我看了下我现在的同事,他们没事不会在网上划水,而是都在学习,最敬佩的是旁边那位,一年了从小白,到一名技术骨干,技术成长太快了,他除了学习,每天还不断在看书,我只能说佩服,我很多时候都在刷抖音,我自愧不如,我有罪


5.想办法融入更好的圈子,我之前待得公司都不大,都是外包公司,大家学历都很低,没有一个是985或者211的本科生,大家上班都在聊吃喝嫖赌,主要是聊女人那点事。但是现在我发现身边的同事几年了,没有一个人说过一句脏话,说话总会特别舒服,因为你能感受到他非常尊重你,说话也非常温柔,绝不会听到SEX,tm 这种言词。


6.接受自己的平凡。我以前有很多想法,内心很浮躁,后面发现读书越少的人想法越多,到最后越来越差,债务缠身,本来都是资本的牟利工具,平凡开心更好,把家里的事情处理好,生活上逐渐改善品质,就已经很不错了,在平凡的生活多点浪漫,对未来有一点期待,但不浮夸,我觉得就已经很不错了


7.多多提高自己的综合素质吧,我是一个比较随心的人,但是后面发现,穿的邋遢,说话幼稚,身形不行,走在外面都没自信,何况别人会怎么看你呢,这一点我也在慢慢提高


8.最后我的技术其实很一般,node,vue,react,java,python,php,微信小程序,three.js 这些都有做过,有的都是为了外包业务减少点成本才去学的,但是要说哪一个比较深入,可能就前端的这几个框架,因为天天都在做,偶尔看看掘金的技术文档,但是要说特别深入的,抱歉没有,因为我从误打误撞开始进入这个行业,我的目的不是因为喜欢,而是因为工资高一点,我没有想给要为这个行业带来些什么,我只想活着,我现在觉得我没有特别喜欢做的行业,我不清楚我能在这个行业做多少年,但是只要做,我就把它做好,因为做工作的态度跟自己的喜欢没有关系,做事是做人,自己的工作做好了,下个同事才会很轻松。同时也在尽可能的弥补一些自己的软实力。希望在某一天,有更好的机会,自己能抓得住,自己不会为了自己的能力而自卑!


9.2023-12-21 17:28:53 下班了


作者:超级666
来源:juejin.cn/post/7314877697996947482
收起阅读 »

什么,你还不会 vue 表格跨页多选?

web
前言看背景就知道,国服第一薇恩,欢迎组队! 言归正传在我们日常项目开发中,经常会有表格跨页多选的需求,接下来让我们用 el-table 示例一步步来实现这个需求。动手开发在线体验codesandbox.io/s/priceless…常规版本...
继续阅读 »

前言

看背景就知道,国服第一薇恩,欢迎组队! 言归正传

在我们日常项目开发中,经常会有表格跨页多选的需求,接下来让我们用 el-table 示例一步步来实现这个需求。

动手开发

在线体验

codesandbox.io/s/priceless…

常规版本

本部分只写了一些重点代码,心急的彦祖可以直接看 性能进阶版

  1. 首先我们需要初始化一个选中的数组 checkedRows
this.checkedRows = []
  1. 在触发选中的时候,我们就需要把当前行数据 push 到 checkedRows,否则就需要剔除对应行
"multipleTable" @select="handleSelectChange">
handleSelectChange (val, row) {
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if (checkedIndex > -1) {
// 选中剔除
this.checkedRows.splice(checkedIndex, 1)
} else {
// 未选中压入
this.checkedRows.push(row)
}
}
  1. 实现换页的时候的回显逻辑
this.data.forEach(row=>{
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if(checkedIndex>-1) this.$refs.multipleTable.toggleRowSelection(row,true)
})

效果预览

让我们看下此时的效果

2023-08-08 20.03.52.gif

完整代码



<script>
export default {
data () {
return {
currentPage: 1,
checkedRows: [],
pageSize: 10,
totalData: Array.from({ length: 1000 }, (_, index) => {
return {
date: '2016-05-03',
id: index,
name: '王小虎' + index
}
})
}
},
computed: {
tableData () {
const { currentPage, totalData, pageSize } = this
return totalData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
}
},
methods: {
currentChange (page) {
this.currentPage = page
this.tableData.forEach(row => {
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if (checkedIndex > -1) this.$refs.multipleTable.toggleRowSelection(row, true)
})
},
handleSelectChange (val, row) {
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if (checkedIndex > -1) {
this.checkedRows.splice(checkedIndex, 1)
} else {
this.checkedRows.push(row)
}
},
handleSelectAllChange (val) {
this.tableData.forEach(row => {
this.handleSelectChange(null, row)
})
}
}
}
script>

性能进阶版

性能缺陷分析

优秀的彦祖们,应该发现以上代码的性能缺陷了

1.handleSelectChange 需要执行一个 O(n) 复杂度的循环

2.currentChange 的回显逻辑内部, 有一个 O(n^2) 复杂度的循环

想象一下 如果场景中勾选的行数达到了 10000 行, 每页显示 100 条

那么我们每次点击换页 最坏情况就要执行 10000 * 100 次循环,这是件可怕的事...

重新设计数据结构

其实我们没必要把 checkedRows 设计成一个数组

我们可以设计成一个 map,这样读取值就只需要 O(1)复杂度

Object 和 Map 的选择

此时应该有 彦祖会好奇,为什么要搞一个 Map 而不是 Object呢?

其实要弄清楚这个问题,我们必须要知道他们之间的区别,网上的文章非常多,也介绍的非常详细

但有一点,是很多文章没有提及的,那就是 Map 是有序的,Object 是无序的

比如有个需求要获取 第一个选中行,最后一个选中行,那么我们利用 Map 实现就非常简单。

其次 我们可以用 size 方法轻松获取 选中行数量

改造代码

1.改造 checkedRows

this.crossPageMap = new Map()

2.修改选中逻辑(核心代码)

handleSelectChange (val, row) {
// 实现了 O(n) 到 O(1) 的提升
const checked = this.crossPageMap.has(row.id)
if (checked) {
this.crossPageMap.delete(row.id)
} else {
this.crossPageMap.set(row.id, row)
}
}

3.修改换页回显逻辑

currentChange (page) {
this.currentPage = page
// 实现了 O(n^2) 到 O(n) 的提升
this.tableData.forEach(row => {
const checked = this.crossPageMap.has(row.id)
if (checked) this.$refs.multipleTable.toggleRowSelection(row, true)
})
}

完整代码



<script>
export default {
data () {
return {
currentPage: 1,
crossPageMap: new Map(),
pageSize: 10,
totalData: Array.from({ length: 1000 }, (_, index) => {
return {
date: '2016-05-03',
id: index,
name: '王小虎' + index
}
})
}
},
computed: {
tableData () {
const { currentPage, totalData, pageSize } = this
return totalData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
}
},
methods: {
currentChange (page) {
this.currentPage = page
this.tableData.forEach(row => {
const checked = this.crossPageMap.has(row.id)
if (checked) this.$refs.multipleTable.toggleRowSelection(row, true)
})
},
handleSelectChange (val, row) {
const checked = this.crossPageMap.has(row.id)
if (checked) {
this.crossPageMap.delete(row.id)
} else {
this.crossPageMap.set(row.id, row)
}
},
handleSelectAllChange (val) {
this.tableData.forEach(row => {
const isChecked = this.crossPageIns.isChecked(row)
if (val.length === 0) {
// 取消全选 只有选中的需要改变状态
if (isChecked) this.crossPageIns.onRowSelectChange(row)
} else {
// 全选 只有未选中的才需要改变状态
if (!isChecked) this.crossPageIns.onRowSelectChange(row)
}
})
}
}
}
script>

抽象业务逻辑

以上就是完整的业务代码部分,但是为了复用性。

我们考虑可以把其中的逻辑抽象成一个CrossPage

设计 CrossPage 类

接收以下参数

`data` - 行数据
`key` - 行数据唯一值
`max` - 最大选中行数
`toggleRowSelection` - 切换行数据选中/取消选中的方法

提供以下方法

`onRowSelectChange` - 外部点行数据点击的时候调用此方法
`onDataChange` - 外部数据变化的时候调用此方法
`clear` - 清空所有选中行
`isChecked` - 判断当前行是否选中

构造器大致代码 如下

constructor (options={}) {
this.crossPageMap = new Map()
this.key = options.key || 'id'
this.data = options.data || []
this.max = options.max || Number.MAX_SAFE_INTEGER
this.toggleRowSelection = options.toggleRowSelection
if(typeof this.toggleRowSelection !== 'function') throw new Error('toggleRowSelection is not function')
}

设置私有crossPageMap

彦祖们,问题来了,我们把crossPageMap挂载到实例上,那么外部就可以直接访问修改这个变量。

这可能导致我们内部的数据逻辑错乱,所以必须禁止外部访问。

我们可以使用 # 修饰符来实现私有属性,具体参考

developer.mozilla.org/zh-CN/docs/…

完整代码

  • CrossPage.js
/**
* @description 跨页选择
* @param {Object} options
* @param {String} options.key 行数据唯一标识
* @param {Function} options.toggleRowSelection 设置行数据选中/取消选中的方法,必传
*/

export const CrossPage = class {
#crossPageMap = new Map();
constructor (options={}) {
this.key = options.key || 'id'
this.data = options.data || []
this.max = options.max || Number.MAX_SAFE_INTEGER
this.toggleRowSelection = options.toggleRowSelection
if(typeof this.toggleRowSelection !== 'function') throw new Error('toggleRowSelection is not function')
}
get keys(){
return Array.from(this.#crossPageMap.keys())
}
get values(){
return Array.from(this.#crossPageMap.values())
}
get size(){
return this.#crossPageMap.size
}
clear(){
this.#crossPageMap.clear()
this.updateViews()
}
isChecked(row){
return this.#crossPageMap.has(row[this.key])
}
onRowSelectChange (row) {
if(typeof row !== 'object') return console.error('row is not object')
const {key,toggleRowSelection} = this
if(this.isChecked(row)) this.#crossPageMap.delete(row[key])
else {
this.#crossPageMap.set(row[key],row)
if(this.size>this.max){
this.#crossPageMap.delete(row[key])
toggleRowSelection(row,false)
}
}
}
onDataChange(list){
this.data = list
this.updateViews()
}
updateViews(){
const {data,toggleRowSelection,key} = this
data.forEach(row=>{
toggleRowSelection(row,this.isChecked(row))
})
}
}

写在最后

未来想做的还有很多

  •  利用requestIdleCallback 提升单页大量数据的 toggleRowSelection 渲染效率
  •  提供默认选中项的配置
  •  ...

欢迎彦祖们 贡献宝贵代码

个人能力有限 如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟

彩蛋

宁波团队还有一个hc, 带你海鲜自助。 欢迎彦祖们私信😚


作者:前端手术刀
来源:juejin.cn/post/7264898713646153780
收起阅读 »

20行js就能实现逐字显示效果???-打字机效果

web
效果演示 横版 竖版 思路分析 可以看到文字是一段一段的并且独占一行,使用段落标签p表示一行 一段文字内,字是一个一个显示的,所以这里每一个字都用一个span标签装起来 每一个字都是从透明到不透明的过渡效果,使用css3的过渡属性transition让每...
继续阅读 »

效果演示


横版


原生JavaScript实现逐字显示效果(打字机效果)插图


竖版


原生JavaScript实现逐字显示效果(打字机效果)插图1


思路分析



  1. 可以看到文字是一段一段的并且独占一行,使用段落标签p表示一行

  2. 一段文字内,字是一个一个显示的,所以这里每一个字都用一个span标签装起来

  3. 每一个字都是从透明到不透明的过渡效果,使用css3的过渡属性transition让每个字都从透明过渡到不透明


基本结构


HTML基本结构


<div id="container"></div>

这里只需要一个容器,其他的结构通过js动态生成


CSS


#container {
/* 添加这行样式=>文字纵向从右往左显示 */
/* 目前先不设置,后面可以取消注释 */
/* writing-mode: vertical-rl; */
}
#container span {
/* 这里opacity先设置为1,让其不透明,可以看到每一步的效果 */
/* 写完js之后到回来改为0 */
opacity: 1;
transition: opacity 0.5s;
}

文本数据


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']

使用数组存放文本数据,一个元素代表一段文字


创建p标签


使用for/of循环遍历数组创建对应个数的p标签,添加到html页面中


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']
// 获取dom元素
const container = document.querySelector('#container')
// for/of循环遍历数组
for (const item of data) {
// 打印每一个item => 数组的每一个元素
console.log(item)
// 创建p标签
const p = document.createElement('p')
// 将p标签插入到container
container.append(p)
}

item代表数组的每一个元素,也就是每一段文字,所以会创建4个p标签


原生JavaScript实现逐字显示效果(打字机效果)插图2


原生JavaScript实现逐字显示效果(打字机效果)插图3


与数组元素数量对应的p标签就生成好了


接下来就是将每一个元素里面的文本添加到span标签中


创建span标签


为每一个字创建一个span标签,然后让span标签的内容等于对应的字,再将每一个生成的span插入到p标签


本节代码


// 遍历item的每一个字
for (let i = 0; i < item.length; i++) {
// 创建span
let span = document.createElement('span')
// span的内容等于item的每一个字
span.innerHTML = item[i]
// 将span插入到p标签中
p.append(span)
}

合并后代码


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']
// 获取dom元素
const container = document.querySelector('#container')
// for/of循环遍历数组
for (const item of data) {
// 打印每一个item => 数组的每一个元素
console.log(item)
// 创建p标签
const p = document.createElement('p')
// 遍历item的每一个字
for (let i = 0; i < item.length; i++) {
// 创建span
let span = document.createElement('span')
// span的内容等于item的每一个字
span.innerHTML = item[i]
// 将span插入到p标签中
p.append(span)
}
// 将p标签插入到container
container.append(p)
}

原生JavaScript实现逐字显示效果(打字机效果)插图4


此时已经完成了渲染数组,并将数组的每一个元素的文字渲染到单独的span中


接下来就要让每一个文字做到从看不见到看的见的效果


添加透明度过渡效果


将css样式中的opacity由1改为0


因为每个字的出现时间不一样,所以不能直接在循环的时候直接添加过渡效果,添加以下代码,让span标签在添加到p标签前也添加到新数组中


const arr = []
// 将span也添加到新数组中
arr.push(span)

最后遍历arr数组,为每一个元素添加一个过渡延迟效果


// 延时1毫秒等待上方循环渲染完成
setTimeout(() => {
// 遍历arr数组的每一个元素
arr.forEach((item, index) => {
// 给每一个元素添加过渡延迟属性
item.style.transitionDelay = `${index * 0.2}s`
// 将透明度设置为不透明
item.style.opacity = 1
})
}, 1)

最后的最后将css样式中的opacity改为0,让所有的字透明


#container span {
opacity: 0;
transition: opacity 0.5s;
}

完整js代码


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']
const arr = []
// 获取dom元素
const container = document.querySelector('#container')
// for/of循环遍历数组
for (const item of data) {
// 打印每一个item => 数组的每一个元素
console.log(item)
// 创建p标签
const p = document.createElement('p')
// 遍历item的每一个字
for (let i = 0; i < item.length; i++) {
// 创建span
let span = document.createElement('span')
// span的内容等于item的每一个字
span.innerHTML = item[i]
// 将span插入到p标签中
p.append(span)
// 将span也添加到新数组中
arr.push(span)
}
// 将p标签插入到container
container.append(p)
}
// 延时1毫秒等待上方循环渲染完成
setTimeout(() => {
// 遍历arr数组的每一个元素
arr.forEach((item, index) => {
// 给每一个元素添加过渡延迟属性
// 让每一个字都比前一个字延时0.2秒的时间
item.style.transitionDelay = `${index * 0.2}s`
// 将透明度设置为不透明
item.style.opacity = 1
})
}, 1)

至此,已经完成了逐字显示的效果,最后介绍一个css属性


writing-mode


使用这个属性可以改变文字方向,实现纵向从左往右或从右往左显示


以下摘自mdn文档


writing-mode 属性定义了文本水平或垂直排布以及在块级元素中文本的行进方向。为整个文档设置该属性时,应在根元素上设置它(对于 HTML 文档,应该在 html 元素上设置)


horizontal-tb

对于左对齐(ltr)文本,内容从左到右水平流动。对于右对齐(rtl)文本,内容从右到左水平流动。下一水平行位于上一行下方。


vertical-rl

对于左对齐(ltr)文本,内容从上到下垂直流动,下一垂直行位于上一行左侧。对于右对齐(rtl)文本,内容从下到上垂直流动,下一垂直行位于上一行右侧。


vertical-lr

对于左对齐(ltr)文本,内容从上到下垂直流动,下一垂直行位于上一行右侧。对于右对齐(rtl)文本,内容从下到上垂直流动,下一垂直行位于上一行左侧。


作者:AiYu
来源:juejin.cn/post/7271165389692960828
收起阅读 »

前端使用a链接下载内容增加loading效果

web
问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。代码如下:// utils.js const XLSX = require('xlsx') // 将一个sheet转成最终的ex...
继续阅读 »
  1. 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。
  2. 代码如下:
// utils.js
const XLSX = require('xlsx')
// 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
export const sheet2blob = (sheet, sheetName) => {
sheetName = sheetName || 'sheet1'
var workbook = {
SheetNames: [sheetName],
Sheets: {}
}
workbook.Sheets[sheetName] = sheet
// 生成excel的配置项
var wopts = {
bookType: 'xlsx', // 要生成的文件类型
bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
type: 'binary'
}
var wbout = XLSX.write(workbook, wopts)
var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
// 字符串转ArrayBuffer
function s2ab(s) {
var buf = new ArrayBuffer(s.length)
var view = new Uint8Array(buf)
for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
return buf
}
return blob
}

/**
* 通用的打开下载对话框方法,没有测试过具体兼容性
* @param url 下载地址,也可以是一个blob对象,必选
* @param saveName 保存文件名,可选
*/

export const openDownloadDialog = (url, saveName) => {
if (typeof url === 'object' && url instanceof Blob) {
url = URL.createObjectURL(url) // 创建blob地址
}
var aLink = document.createElement('a')
aLink.href = url
aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
var event
if (window.MouseEvent) event = new MouseEvent('click')
else {
event = document.createEvent('MouseEvents')
event.initMouseEvent(
'click',
true,
false,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
)
}
aLink.dispatchEvent(event)
}

"clickExportBtn"
>
<i class="el-icon-download">i>下载数据

<div class="mongolia" v-if="loadingSummaryData">
<el-icon class="el-icon-loading loading-icon">
<Loading />
el-icon>
<p>loading...p>
div>

clickExportBtn: _.throttle(async function() {
const downloadDatas = []
const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
summaryDataForDownloads.map(summaryItem =>
downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
)
// donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
this.loadingSummaryData = true
const downloadBlob = aoa2sheet(downloadDatas.flat(1))
openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
this.loadingSummaryData = false
}, 2000),

// css
.mongolia {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
color: #409eff;
z-index: 9999;
}
.loading-icon {
color: #409eff;
font-size: 32px;
}
  1. 解决方案探究:
  • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:

    • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。
    • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。
    • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。
    • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。
  • 查阅资料后找到了如下几种方案:

      1. 使用 setTimeout 使 openDownloadDialog 异步执行
      clickExport() {
      this.loadingSummaryData = true;

      setTimeout(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }
      1. 对 openDownloadDialog 内部进行优化
      • 避免大循环或递归逻辑
      • 将计算工作分批进行
      • 使用 Web Worker 隔离耗时任务
        • 在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。

            1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。
            1. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。
            1. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。
            1. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。
            1. 代码应该是自包含的,不依赖外部变量或状态。
            1. 可以导入其他脚本依赖,比如用 import 引入工具函数等。
            1. 避免修改或依赖全局作用域,比如定义全局变量等。
        • 总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。

          // 创建 Worker
          const worker = new Worker('downloadWorker.js');

          // 点击下载时向 Worker 发送消息
          function clickDownload() {

          showLoading();

          worker.postMessage({
          url: fileURL,
          filename: 'report.xlsx'
          });

          worker.onmessage = function(e) {
          // 收到下载完成的消息
          hideLoading();
          }

          }

          // 显示 loading
          function showLoading() {
          loading.style.display = 'block';
          }

          // 隐藏 loading
          function hideLoading() {
          loading.style.display = 'none';
          }

          // downloadWorker.js

          onmessage = function(e) {
          const url = e.data.url;
          const filename = e.data.filename;

          // 创建并点击链接触发下载
          const a = document.createElement('a');
          a.href = url;
          a.download = filename;
          a.click();

          postMessage('下载完成');
          }

          id="loading" style="display:none;">
          Downloading...





      1. 使用 requestIdleCallback 进行调度
      clickExport() {
      this.loadingSummaryData = true;

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }
    • 当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:

      使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。

      因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。

      requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。

      但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。

      所以需要权衡执行速度和避免阻塞之间的关系:

      • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。
      • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。

      偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。

      此外,可以结合两者试试:

      clickExport() {

      this.loadingSummaryData = true;

      setTimeout(() => {

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob);
      });

      this.loadingSummaryData = false;

      }, 200);

      }

      setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。

      1. 分析线程模型,找到具体的阻塞点
      • 使用 Performance 工具分析线程
      • debugger 及 console.log 打印关键函数的执行时间
      • 检查是否有非主线程的任务阻塞了主线程
      1. 调整 vue 的批量更新策略
      new Vue({
      config: {
      // 修改批量更新的队列长度
      batchUpdateDuration: 100
      }
      })


作者:李卫泽
来源:juejin.cn/post/7268050036474609683
收起阅读 »

实现丝滑的无缝滚动轮播图

web
一. 目标效果 二. 实现思路 使用Animate API或者CSS动画的方式都可以,我选择的是Animate API。 实现无缝滚动的一般思路 Translate位移+无限循环动画。但是这样会有一个小问题,就是在动画结束又开始的一瞬间会闪烁一下,不是很完美...
继续阅读 »

一. 目标效果


2023-12-15-13-59-18.gif


二. 实现思路


使用Animate API或者CSS动画的方式都可以,我选择的是Animate API。


实现无缝滚动的一般思路


Translate位移+无限循环动画。但是这样会有一个小问题,就是在动画结束又开始的一瞬间会闪烁一下,不是很完美。


解决方法


复制一份数据, 原来的1份数据变成2份数据。然后动画的关键帧设置位移的终点为50%,这样每次动画的结束帧就在数据的中间位置, 注意如果数据之间有间距的话,还要加上间距的一半。这样即可实现无限滚动,并且足够丝滑。3


三. 实现


核心代码


以下代码示例都使用React框架。


  // 使用Web animate Api 添加动画
useEffect(() => {
if (!container.current) return;
// 获取gap值
const gap = getComputedStyle(container.current).gap.split('px')[0] ?? 0;

// 滚动容器(container)的50%宽度 + 滚动容器的50%gap值
// 如果不加滚动容器的50%gap值, 在动画结束又开始的瞬间会跳一下
const translateX = container.current.clientWidth / 2 + Number(gap) / 2;
if (isNaN(translateX)) {
throw new Error('translateX is NaN!');
}

// 定义关键帧, 执行动画
let keyframes: Keyframe[] = [];
if (type === 'rtl') {
keyframes = [
{
transform: 'translateX(0)',
},
{
transform: `translateX(-${translateX}px)`,
},
];
} else if (type === 'ltr') {
keyframes = [
{
transform: `translateX(-${translateX}px)`,
},
{
transform: 'translateX(0)',
},
];
}

animation.current = container.current.animate(keyframes, {
duration,
easing: 'linear',
iterations: Infinity,
});
}, []);

return (
// 使用context传递store和dispatch
<SwiperContext.Provider value={{ store, dispatch }}>
<div className={classNames(['w-full overflow-x-hidden', wrapperClassName])}>
{/* 使用inline-flex代替flex,让ul的宽度被子元素撑开 */}
<ul
className={classNames(['inline-flex flex-nowrap gap-5', className])}
style={style}
ref={container}
>

{/* 类似于HOC的效果, 为Item组件添加_key */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}

{/* 实现无缝滚动, 复制一组子元素进行占位 */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}
</ul>
</div>
</SwiperContext.Provider>

);

其中type只是为了区分在x轴上的滚动方向而已,根据方向应用不同的动画。动画的实例使用useRef()去保存。
方便后续调用此动画实例进行动画的暂停和播放。


完整代码


SwiperBox.tsx


import { useHover } from 'ahooks';
import classNames from 'classnames';
import { isNaN, isUndefined } from 'lodash-es';
import {
cloneElement,
CSSProperties,
ReactElement,
ReactNode,
useContext,
useEffect,
useRef,
} from 'react';

import { SwiperContext } from './swiper-context';
import useSwiperReducer, { SwiperActions } from './use-swiper-reducer';

interface SwiperBoxProp {
/**
* 轮播方向
*
* @type {('ltr' | 'rtl')}
* @memberOf SwiperBoxProp
*/

type: 'ltr' | 'rtl';

/**
* 子节点
*
* @type {ReactNode[]}
* @memberOf SwiperBoxProp
*/

children: ReactNode[];

/**
* 类名
*
* @type {string}
* @memberOf SwiperBoxProp
*/

className?: string;

/**
* 外层节点类名
*
* @type {string}
* @memberOf SwiperBoxProp
*/

wrapperClassName?: string;

/**
* 节点样式
*
* @type {CSSProperties}
* @memberOf SwiperBoxProp
*/

style?: CSSProperties;

/**
* 动画持续时间
*
* @type {EffectTiming['duration']}
* @memberOf SwiperBoxProp
*/

duration?: EffectTiming['duration'];

/**
* 鼠标悬停时触发
* @type {boolean} isHovering 是否悬停
* @type {string} key 节点key
*
* @memberOf SwiperBoxProp
*/

hoverOnChange?: (isHovering: boolean, key: string) => void;
}

/**
* 无限循环、无缝轮播组件
* 使用这个组件必须通过gap的方式(eg: gap-4)来设置滚动项之间的距离, 不能使用margin的方式, 不然无缝滚动会有问题
*/

function SwiperBox(prop: SwiperBoxProp) {
const {
type,
className,
wrapperClassName,
style,
children,
duration = 3000,
hoverOnChange,
} = prop;
const [store, dispatch] = useSwiperReducer();
const { activeKey } = store;
// 滚动容器
const container = useRef<HTMLUListElement>(null);
// 动画实例
const animation = useRef<Animation | null>(null);

// activeKey改变时通知外部组件
useEffect(() => {
hoverOnChange &&
!!Object.keys(activeKey).length &&
hoverOnChange(activeKey.isHovering, activeKey.key);
}, [activeKey]);

// 获取所有的key值并存储
useEffect(() => {
dispatch(
SwiperActions.updateKeys(children.map((child) => (child as ReactElement).key ?? ''))
);
}, []);

// 使用Web animate Api 添加动画
useEffect(() => {
if (!container.current) return;
// 获取gap值
const gap = getComputedStyle(container.current).gap.split('px')[0] ?? 0;

// 滚动容器(container)的50%宽度 + 滚动容器的50%gap值
// 如果不加滚动容器的50%gap值, 在动画结束又开始的瞬间会跳一下
const translateX = container.current.clientWidth / 2 + Number(gap) / 2;
if (isNaN(translateX)) {
throw new Error('translateX is NaN!');
}

// 定义关键帧, 执行动画
let keyframes: Keyframe[] = [];
if (type === 'rtl') {
keyframes = [
{
transform: 'translateX(0)',
},
{
transform: `translateX(-${translateX}px)`,
},
];
} else if (type === 'ltr') {
keyframes = [
{
transform: `translateX(-${translateX}px)`,
},
{
transform: 'translateX(0)',
},
];
}

animation.current = container.current.animate(keyframes, {
duration,
easing: 'linear',
iterations: Infinity,
});
}, []);

// 鼠标移入动画暂停/播放
useEffect(() => {
if (!animation.current) return;
if (isUndefined(activeKey.isHovering)) return;

if (activeKey.isHovering) {
animation.current.pause();
} else {
animation.current.play();
}
}, [activeKey]);

return (
// 使用context传递store和dispatch
<SwiperContext.Provider value={{ store, dispatch }}>
<div className={classNames(['w-full overflow-x-hidden', wrapperClassName])}>
{/* 使用inline-flex代替flex,让ul的宽度被子元素撑开 */}
<ul
className={classNames(['inline-flex flex-nowrap gap-5', className])}
style={style}
ref={container}
>

{/* 类似于HOC的效果, 为Item组件添加_key */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}

{/* 实现无缝滚动, 复制一组子元素进行占位 */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}
</ul>
</div>
</SwiperContext.Provider>

);
}

interface SwiperBoxItemProp {
children: ReactNode;
// 唯一标识, React不会将key转发到组件中, 因此自定义一个唯一的_key
_key?: string;
}

function SwiperBoxItem(prop: SwiperBoxItemProp) {
const { children, _key } = prop;

const container = useRef<HTMLLIElement>(null);
const context = useContext(SwiperContext);

// 鼠标hover
const onEnter = () => {
context && _key && context.dispatch(SwiperActions.onEnter(true, _key));
};

// 鼠标退出hover
const onLeave = () => {
context && _key && context.dispatch(SwiperActions.onLeave(false, _key));
};

useHover(container, {
onEnter,
onLeave,
});

return (
<li
ref={container}
className="transition-transform duration-500 ease-out hover:scale-105"
>

{children}
</li>

);
}

const SwiperWithAnimation = {
Box: SwiperBox,
Item: SwiperBoxItem,
};

export default SwiperWithAnimation;


swiper-context.ts


import { createContext, Dispatch } from 'react';

import { SwiperAction, SwiperState } from './use-swiper-reducer';

export type SwiperContextType = {
store: SwiperState;
dispatch: Dispatch<SwiperAction>;
};
export const SwiperContext = createContext<SwiperContextType | null>(null);


useSwiperReducer.ts


import { useReducer } from 'react';

export interface SwiperState {
activeKey: { isHovering: boolean; key: string };
totalKeys: string[];
}

export type SwiperAction<T = any> = {
type: string;
payload: T;
};

export const SwiperActions = {
onEnter: (isHovering: boolean, key: string) => ({
type: 'onEnter',
payload: { isHovering, key },
}),
onLeave: (isHovering: boolean, key: string) => ({
type: 'onLeave',
payload: { isHovering, key },
}),
updateKeys: (keys: string[]) => ({
type: 'update_keys',
payload: keys,
}),
};

export default function useSwiperReducer() {
const initialState: SwiperState = {
activeKey: {} as SwiperState['activeKey'],
totalKeys: [] as SwiperState['totalKeys'],
};

const reducer = (store: SwiperState, { type, payload }: SwiperAction): SwiperState => {
switch (type) {
case 'onEnter':
return {
...store,
activeKey: payload,
};
case 'onLeave':
return {
...store,
activeKey: payload,
};
case 'update_keys':
return {
...store,
totalKeys: payload,
};
default:
return store;
}
};

const [store, dispatch] = useReducer(reducer, initialState);

return [store, dispatch] as const;
}


四、如何使用


import SwiperWithAnimation from '@/components/swiper-box/SwiperBox';
import { uniqueId } from 'lodash-es';


const DATA = new Array(2).fill(0).map(() => uniqueId('data'));
/**
* 测试页面
*/

export default function TestPage() {
// 鼠标hover事件
const hoverOnChange = (isHovering: boolean, key: string) => {
console.log('isHovering: ', isHovering);
console.log('key: ', key);
};

return (
<div>
<SwiperWithAnimation.Box
type="ltr"
wrapperClassName="py-9 m-auto !w-[600px] border border-red-200"
className="gap-8"
hoverOnChange={hoverOnChange}
>

{DATA.map((data) => (
<SwiperWithAnimation.Item key={data}>
<div className="f-c h-[300px] w-[300px] rounded-lg bg-theme-primary">
<div className="text-2xl">{data}</div>
</div>
</SwiperWithAnimation.Item>
))}
</SwiperWithAnimation.Box>
</div>

);
}


作者:In74
来源:juejin.cn/post/7312421872414818331
收起阅读 »

css 实现 'X' 号的显示(close关闭 icon), 并支持动画效果

web
最近项目上要实现一个小 'x' 的关闭样式, 今天记录一下处理过程 先看效果 HTML DOM 元素说明 要渲染内容必须有 dom 节点, 这里我们使用 span 作为容器, 然后所有的处理都基于它进行处理 <span class="close-x"&...
继续阅读 »

最近项目上要实现一个小 'x' 的关闭样式, 今天记录一下处理过程


先看效果


HTML DOM 元素说明


要渲染内容必须有 dom 节点, 这里我们使用 span 作为容器, 然后所有的处理都基于它进行处理


<span class="close-x">span>

第一步, 设置 close-x 的样式


@closeXSize: 20px; // 大小/尺寸
@closeXLine: 2px; // 线条宽度
.close-x {
position: relative;
display: inline-block;
width: @closeXSize;
height: @closeXSize;
cursor: pointer;
}


  • 通过使用 closeXSize closeXLine, 方便对尺寸进行调整

    渲染出来大概是这样的
    image.png


第二步, 通过伪元素 before after 画两条线


.close-x {
// ...

&::before, &::after {
position: absolute;
left: 50%;
width: @closeXLine;
height: 100%;
margin-left: (@closeXLine / -2);
content: '';
background: #000;
}
}


  • margin-left 的设置是为了处理'线'的自身宽度

    渲染出来大概是这样的
    image.png


第三步, 分别设置旋转角度


.close-x {
// ...

&::before {
transform: rotate(-45deg);
}

&::after {
transform: rotate(45deg);
}
}

渲染出来大概是这样的, 基本上就完成了
image.png


继续优化, 锦上添花



  • 先来定义一个动画, 动画的意思是这样的:

    • 当为 0% 时旋转角为 0 度,

    • 当为 100% 时旋转角为 360 度




@keyframes rotating {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}

持续旋转


.rotate-infinite {
animation: rotating .3s infinite linear;
}

// 使用方式 增加类 rotate-infinite
//

加载时旋转一次


.rotate-one {
animation: rotating .3s linear;
}

// 使用方式 增加类 rotate-one
//

hover 时旋转一次


.rotate-hover:hover {
.rotate-one();
}

// 使用方式 增加类 rotate-hover
//

选中时旋转


.rotate-active:active {
.rotate-infinite();
}

// 使用方式 增加类 rotate-active
//

纯JS实现


function addCloseX(content) {
const closeXSize = 20;
const closeXLine = 2;

const closeXWrap = document.createElement('div');
closeXWrap.style.cssText = `
position: relative;
display: inline-block;
width:
${closeXSize}px;
height:
${closeXSize}px;
cursor: pointer;
`
;

const baseStyle = `
display: block;
height: 100%;
width:
${closeXLine}px;
margin: auto;
background: #000;
`
;
const xLineOne = document.createElement('i');
xLineOne.style.cssText = baseStyle + `
transform: rotate(45deg);
`
;
const xLineTwo = document.createElement('i');
xLineTwo.style.cssText = baseStyle + `
margin-top: -100%;
transform: rotate(-45deg);
`
;
closeXWrap.appendChild(xLineOne);
closeXWrap.appendChild(xLineTwo);

content.appendChild(closeXWrap);
}

addCloseX(document.getElementById('close'))

需要提供一下注入的位置, 以上示例需要我们提供这样的 dmo 节点:


<div id="close">div>


  • 这种方式没有使用样式表, 所有的样式都使用了行内样式的方式实现的

  • 因为只用到了行内样式, 所以没办法使用伪元素, 故引入了两个 i 标签代替


结束


相关文档


CSS 实现圆(环)形进度条


作者:洲_
来源:juejin.cn/post/7263069805254197307
收起阅读 »

一次面试让我真正认识了 “:visible.sync”

web
面试官提出了一个很实际的问题:如何封装一个不需要在每个父组件中都定义关闭方法的全局对话框组件,类似于 Element UI 的 el-dialog。这篇技术文章将带你了解如何实现这样的组件,并解释 :visible.sync 这个 Vue 2 的语法糖。 如何...
继续阅读 »

面试官提出了一个很实际的问题:如何封装一个不需要在每个父组件中都定义关闭方法的全局对话框组件,类似于 Element UI 的 el-dialog。这篇技术文章将带你了解如何实现这样的组件,并解释 :visible.sync 这个 Vue 2 的语法糖。


如何封装一个类似 el-dialog 的全局对话框组件


el-dialog 是 Element UI 中的一个常用对话框组件,它提供了一种简洁的方式来展示模态框。在我们自己的项目中,我们可能也会需要封装一个自定义的、功能类似的对话框组件。


步骤一:创建 MyDialog 组件


在 src/components 目录下创建 MyDialog.vue 文件:


<template>
<!-- 对话框的 HTML 代码结构如上所示 -->
</template>

<script>
export default {
// 组件逻辑如上所示
};
</script>

<style scoped>
/* 对话框的样式如上所示 */
</style>

步骤二:在 main.js 中全局注册


在 main.js 文件中,导入 MyDialog 并全局注册这个组件:


import Vue from 'vue';
import MyDialog from './components/MyDialog.vue';

Vue.component('my-dialog', MyDialog);

步骤三:在父组件中使用 .sync 修饰符


要想让 MyDialog 组件的显示状态能够通过父组件控制,但不需要在每个父组件中定义方法来关闭对话框,我们可以使用 Vue 的 .sync 修饰符。


在父组件中,你可以这样使用 MyDialog 组件:


<template>
<my-dialog :title="'自定义对话框'" :visible.sync="dialogVisible">
<!-- 对话框的内容 -->
</my-dialog>
</template>

<script>
export default {
data() {
return {
dialogVisible: false
};
}
// 无需定义关闭对话框的方法
};
</script>

理解 .sync 修饰符


.sync 是 Vue 2 中的一个定制的语法糖,用于创建双向绑定。通常来说,Vue 使用单向数据流,即数据的变化是单向传播的。通过 .sync,子组件可以通过事件更新父组件的状态,使数据的变更成为双向的。


当你在父组件的一个属性上加了 .sync 时,实际上 Vue 会自动更新父组件的状态,当子组件触发了一个特定命名形式的事件(update:xxx)时。


示例:MyDialog 组件


在 MyDialog 组件中,当用户点击关闭按钮时,组件需要通知父组件更新visible属性。这可以通过在子组件内部触发 update:visible 事件来实现:


methods: {
handleClose() {
this.$emit('update:visible', false);
}
}

结论


通过正确使用 Vue 的 .sync 修饰符和事件系统,我们可以轻松地封装和使用类似于 el-dialog 的全局对话框组件,而无需在每个使用它的父组件中定义关闭方法。这种方式使代码更加干净、可维护,并遵循 Vue 的设计原则。


以这种方法,你可以增强组件的可重用性与可维护性,同时学习到 Vue.js 高级组件通信的实用技巧。


注意:在 Vue 3 中,.sync 已经被废弃。相同功能可以通过 v-model 或其它定制的事件和属性实现。所以,确保你的实现与你使用的 Vue 版本相一致。


作者:超级vip管理员
来源:juejin.cn/post/7314493016497635368
收起阅读 »

主管让我说说 qiankun 是咋回事😶

web
前言 最近乙方要移交给我们开发的一个项目的代码,其中前端用到了 qiankun 微前端技术,因为第一版代码之前让我看过,写过基础开发文档,然后主管昨天就找我问了一下,本来以为就是问下具体概念和开发,没想到问起了是怎么实现的🥲,之前了解 qiankun 也就是看...
继续阅读 »

前言


最近乙方要移交给我们开发的一个项目的代码,其中前端用到了 qiankun 微前端技术,因为第一版代码之前让我看过,写过基础开发文档,然后主管昨天就找我问了一下,本来以为就是问下具体概念和开发,没想到问起了是怎么实现的🥲,之前了解 qiankun 也就是看了下开发配置,并没有去关注具体实现,一下子给我难住了。后面又给我留下了几个问题,让我去了解了解,琢磨琢磨,这篇文章就是记一下自己 search 到的一些知识和自己的理解,可能有很多问题,期待JY们指正。


QA


Q:父应用和子应用可以在不同的nginx上吗?


A:可以,父子应用既可以在同一个nginx也可以在不同的nginx上。


Q:从SLB过来的请求是先到父应用再路由到子应用?


A:不是,父应用在运行时,通过 fetch 拿到子应用的 html 文件上的 js、css 依赖(import-html-entry),划出一个独立容器(sandbox)运行子应用,所有子应用都是运行在父应用这个基座上的“应用级组件”,子应用成为了父应用的一部分,子应用中配置的代理不会生效,父子应用共享同一个网络环境,都运行在同一个IP上,请求都从同一个IP发出,子应用的所有网络请求都通过父应用配置的代理转发。


Q:父应用和子应用通信?(是不是通过网络通信)


A:qiankun的父子应用通信不是通过网络通信。


父子应用通信是直接通过浏览器存储或者内存等,例如路由的 query、localStorage、eventBus 或者qiankun提供的全局状态管理工具都可以管理。


子应用挂载时,也可以类似React组件通过props传递具体数据和父应用中改变数据的函数,也可以传递一个全局状态,其包含变量修改和监听变化的函数,父子应用都可以监听变量的变化和修改变量。


Nginx配置


父应用上的 nginx 配置类似本地文件中的 proxy 代理配置,在父应用上分别配置每个子应用的 html 文件所在的地址(资源代理),和子应用的后端接口地址(请求代理)。


export default {
"/root-app": {
target: "https://xxx.xxx.com:xxxx/",
changeOrigin: true,
},

// child1
// 资源代理
"/child1/": {
target: "https://xxx.xxx.com:xxxx/",
changeOrigin: true,
},
// 接口代理
"/child1-api/": {
target: "https://xxx.xxx.com:xxxx/",
changeOrigin: true,
},
// ......
};

不允许主应用跨域访问微应用,做法就是将主应用服务器上一个特殊路径的请求全部转发到微应用的服务器上,即通过代理实现“微应用部署在主应用服务器上”的效果。


例如,主应用在 A 服务器,微应用在 B 服务器,使用路径 /app1 来区分微应用,即 A 服务器上所有 /app1 开头的请求都转发到 B 服务器上。此时主应用的 Nginx 代理配置为:


/app1/ {
proxy_pass http://www.b.com/app1/;
proxy_set_header Host $host:$server_port;
}

演示图


资源文件


从子应用 html 上解析出 js 和 css 加载到父应用基座未命名文件 (2).png


网络请求


未命名文件 (1).png


核心


应用的加载


qiankun 的一个重要的依赖库 import-html-entry ,其功能是主应用拉取子应用 html 中的 js 和 css 文件并加载到父应用基座,css 嵌入到 html,js放在内存中在适当时机 eval 运行


应用的隔离与通信


通过 sandbox 进行 js 和 css 隔离。


js 隔离

js 隔离通过给全局 window 一个 proxy 包裹传递进来,子应用的 js 运行在 proxy 上,子应用卸载时,proxy 跟着清除,这样避免了污染真正的 window,另外对于不支持 proxy 的浏览器,没有 polyfill 方案,qiankun 采用 snapshot 快照方案,保存子应用挂载前的 window 状态,在子应用卸载时,恢复到挂载前的状态,但这种解决方案无法处理基座上同时挂载多个子应用的情景;


css 隔离

css 隔离通过 shadowdom,将子应用的根节点挂载到 shadowdom 中,shadowdom 内部的样式并不会影响全局样式,但是有个缺点,很多组件库的类似弹窗提醒组件会把 dom 提升到顶层,这样注定会污染到全局的样式;


qiankun 的一个实验性解决方案,类似 vue 的 scoped 方案/css-module,给子应用的 css 变量装饰一下(一般是hash),这样避免来避免子应用的样式污染到全局。


彻底解决:约定主子应用完全使用不同的 css 命名; react 的 css-in-js 方案;使用 postcss 全局加变量;全部写 tailwindcss ......


通信

父子应用通信是直接通过浏览器存储或者内存等,例如路由的 query、localStorage、eventBus 或者qiankun提供的全局状态管理工具都可以管理,简单来说就是全局变量。


子应用挂载时,也可以类似React组件通过props传递具体数据和父应用中改变数据的函数,也可以传递一个全局状态,其包含变量修改和监听变化的函数,父子应用都可以监听变量的变化和修改变量。


理解


子应用是可以独立开发、独立部署、独立运行的应用,但在父应用上并不是“独立”运行,而是父应用通过网络动态 fetch 到子应用的 html 文件,然后解析出 html 上的 js 和 css 依赖,处理后加载到父应用基座,将子应用作为自己的一个特殊组件加载渲染到一个“独立沙箱容器”中。


问题



  • 多应用模块共享、代码复用问题没有解决。父子应用如果存在相同依赖,在子应用加载时,是不是还是会去重新加载一遍?

  • 子应用 css 隔离仍存在问题,不支持 proxy 的浏览器无法支持多个子应用同时加载的情形;

  • 当前项目是否真的大到需要使用微前端来增加开发和维护复杂度;

  • 根据我的搜索,qiankun 对于 vite 构建的项目支持度貌似不够,而我们最新项目基本都是通过 vite 构建,可能会有问题。


作者:HyaCinth
来源:juejin.cn/post/7314196310647423039
收起阅读 »

数据库连接神器:JDBC的基本概述、组成及工作原理全解析!

JDBC(Java DataBase Connectivity)是一种用于执行SQL语句的 Java API,是Java和数据库之间的一个桥梁,是一个规范而不是一个实现,能够交给数据库执行SQL语句。在信息化时代,数据库已经成为了存储和管理数据的重要工具。而J...
继续阅读 »

JDBC(Java DataBase Connectivity)是一种用于执行SQL语句的 Java API,是Java和数据库之间的一个桥梁,是一个规范而不是一个实现,能够交给数据库执行SQL语句。

在信息化时代,数据库已经成为了存储和管理数据的重要工具。而Java作为一种广泛使用的编程语言,其与数据库的交互就显得尤为重要。JDBC就是为了解决这个问题而生的。通过JDBC,我们可以在Java程序中轻松地执行SQL语句,实现对数据库的增删改查操作。今天我们就来聊一聊JDBC的相关概念。

一、JDBC简介

概念:

JDBC(Java DataBase Connectivity) :Java数据库连接技术。
具体讲就是通过Java连接广泛的数据库,并对表中数据执行增、删、改、查等操作的技术。如图所示:

Description

本质上,JDBC的作用和图形化客户端的作用相同,都是发送SQL操作数据库。差别在图形化界面的操作是图形化、傻瓜化的,而JDBC则需要通过编码(这时候不要思考JDBC代码怎么写,也不要觉得它有多难)完成图形操作时的效果。

也就是说,JDBC本质上也是一种发送SQL操作数据库的client技术,只不过需要通过Java编码完成。

作用:

  • 通过JDBC技术与数据库进行交互,使用Java语言发送SQL语句到数据库中,可以实现对数据的增、删、改、查等功能,可以更高效、安全的管理数据。
  • JDBC是数据库与Java代码的桥梁(链接)。

二、JDBC的组成

JDBC是由一组用Java语言编写的类和接口组成,主要有驱动管理、Connection接口、Statement接口、ResultSet接口这几个部分。

Description

Connection 接口

定义:在 JDBC 程序中用于代表数据库的连接,是数据库编程中最重要的一个对象,客户端与数据库所有的交互都是通过connection 对象完成的。

Connection conn = DriverManager.getConnection(url,user,password);

常见方法:

  • createStatement() :创建向数据库发送的sql的statement对象。

  • prepareStatement(sql) :创建向数据库发送预编译sql的PrepareSatement对象。

  • prepareCall(sql) :创建执行存储过程的callableStatement对象。(常用)

  • setAutoCommit(boolean autoCommit):设置事务是否自动提交。

//关闭自动提交事务  
setAutoCommit(false);
//关闭后需要手动打开提交事务
  • commit() : 在链接上提交事务。

  • rollback() : 在此链接上回滚事务。

Statement 接口

  • statement:由createStatement创建,用于发送简单的SQL语句(不带参数)。
Statement st = conn.createStatement();
  • PreparedStatement :继承自Statement接口,是Statement的子类,可发送含有参数的SQL语句。效率更高,并且可以防止SQL注入,建议使用。

  • PreparedStatement ps = conn.prepareStatement(sql语句);

PreparedStatement 的优势:
Statement会使数据库频繁编译SQL,可能造成数据库缓冲区溢出。PreparedStatement 可对SQL进行预编译,从而提高数据库的执行效率。
并且PreperedStatement对于sql中的参数,允许使用占位符的形式进行替换,简化sql语句的编写,可以避免SQL注入的问题。

  • CallableStatement:继承自PreparedStatement接口,由方法 prepareCall创建,用于调用存储过程。

常见方法:

  • executeQuery(String sql) :用于向数据发送查询语句。

  • executeUpdate(String sql) :用于向数据库发送insert、update或delete语句。

  • execute(String sql):用于向数据库发送任意sql语句。

  • addBatch(String sql):把多条sql语句放到一个批处理中。

  • executeBatch():向数据库发送一批sql语句执行。

ResultSet 接口

ResultSet:用于代表Sql语句的执行结果。

Resultset封装执行结果时,采用的类似于表格的方式,ResultSet 对象维护了一个指向表格数据行的游标,初始的时候,游标在第一行之前,调用ResultSet.next() 方法,可以使游标指向具体的数据行,进行调用方法获取该行的数据。

常用方法:

  • ResultSet.next() :移动到下一行;

  • ResultSet.Previous() :移动到前一行

  • ResultSet.absolute(int row):移动到指定行

  • ResultSet.beforeFirst():移动resultSet的最前面

  • ResultSet.afterLast():移动resultSet的最后面

在这里给大家分享一下【云端源想】学习平台,无论你是初学者还是有经验的开发者,这里都有你需要的一切。包含课程视频、在线书籍、在线编程、一对一咨询等等,现在功能全部是免费的,

点击这里,立即开始你的学习之旅!

三、JDBC的工作原理

JDBC的工作原理可以分为以下几个步骤:

Description

1、加载并注册JDBC驱动:
这是建立数据库连接的第一步,我们需要先加载JDBC驱动,然后通过DriverManager的registerDriver方法进行注册。

2、建立数据库连接:
通过DriverManager的getConnection方法,我们可以建立与数据库的连接。

3、创建Statement对象:
通过Connection对象的createStatement方法,我们可以创建一个Statement对象,用于执行SQL语句。

4、执行SQL语句:
通过Statement对象的executeQuery或executeUpdate方法,我们可以执行SQL语句,获取结果或者更新数据库。

5、处理结果:
对于查询操作,我们需要处理ResultSet结果集;对于更新操作,我们不需要处理结果。

6、关闭资源:
最后,我们需要关闭打开的资源,包括ResultSet、Statement和Connection。

下面,我们来看一个简单的JDBC使用示例。假设我们要查询为"students"的表中的所有数据:

Description

四、面向过程的实现过程

1.在pom.xml中引入mysql的驱动文件

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
  1. 加载驱动类
Class.forName("com.mysql.cj.jdbc.Driver");
  1. 建立Java同数据库中间的连接通道
String url = "jdbc:mydql://locallhost:3306/test";//test是数据库名称
String user = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url,user,password);
  1. 产生负责’传递命令’的‘传令官’对象
String sql ="insert into emp values(null,"苏醒","歌手",7956,now(),79429,6799,30,1)";
PrepareStement ps = conn.prepareStement(sql);
  1. 接收结果集(只有查询有结果集)
int row = ps.excuteUpdate();//交由MySQL执行命令
System.out.println(row + "行受到影响!");
  1. 关闭连接通道
ps.close();
conn.close();

参数的传递方式

//键盘赋值
private static Scanner scan;
static{
scan = new Scanner(System.in);
}

拼接字符串方式

Description

占位符方式 ‘?’

Description

使用占位符的好处:

  • 可以有效避免SQL注入问题

  • 可以自动根据复制时的数据类型来决定是否引入"

删除

  • 物理删除

Description

  • 逻辑删除

Description

查询操作

  • 全查询

Description

  • 按ID查询

Description

五、面向对象(JDBC)的实现方式

面向对象是指将多个功能查分成多个包进行对数据库的增删改查(CRUD)操作。

db包作用

db包中只需要一个类–>DBManager类,这个类的主要作用就是负责管理数据的的连接。

Description

bean包作用

一般和数据库中的表对应,bean包中的类一般都和表名相同,首字母大写,驼峰命名。

Description

dao包作用

DAO是Data Access Object数据访问接口,一般以bean包的类名为前缀,以DAO结尾,负责执行CRUD操作,一个dao类负责一个表的CRUD,也可以说成是对一个bean类的CRUD(增删改查)。

public class EmpDAO(){


// 一般对于删除操作,都是进行更新状态将之隐藏
public void delete(int id){
try{
conn = DBManager.getConnection();
String sql = "update emp set state = 0 where empNo = "+ id;
ps = conn.prepareStatement(sql);
ps.executeUpdate();
}catch(ClassNotFoundException e){
e.printStackTrace();
}catch(SQLException e){
e.printStackTrace();
}finally{
DBManager.closeConn(conn, ps);
}
}

//存储
public void save(Emp emp) {


try {
conn = DBManager.getConnection();
String sql = "insert into emp values(null,?,?,?,now(),?,?,?,1)";
ps = conn.prepareStatement(sql);


ps.setString(1, emp.getEname());
ps.setString(2, emp.getJob());
ps.setInt(3, emp.getMgr());
ps.setDouble(4, emp.getSal());
ps.setDouble(5, emp.getComm());
ps.setInt(6, emp.getDeptNo());
ps.executeUpdate();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {


DBManager.closeConn(conn, ps);
}
}

//更新--修改
public void update(Emp emp) {
try {
conn = DBManager.getConnection();
String sql = "update emp set ename=?,job=?,mgr=?,sal=?,comm=?,deptNo=? where empNo=?";
ps = conn.prepareStatement(sql);
ps.setString(1, emp.getEname());
ps.setString(2, emp.getJob());
ps.setInt(3, emp.getMgr());
ps.setDouble(4, emp.getSal());
ps.setDouble(5, emp.getComm());
ps.setInt(6, emp.getDeptNo());
ps.setInt(7, emp.getEmpNo());
ps.executeUpdate();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBManager.closeConn(conn, ps);
}
}
//单条信息查询--按ID查询--将填写的信息填写在emp属性里中,然后将emp
public Emp findEmpByNo(int id) {
Emp emp = new Emp();


try {
conn = DBManager.getConnection();
String sql = "select * from emp where empno=? and state = 1";
ps = conn.prepareStatement(sql);
ps.setInt(1, id);
rs = ps.executeQuery();
if (rs.next()) {
//取出第一列的值赋给empNO
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {


DBManager.closeConn(conn, ps);
}
return emp;
}


//全表查询--集合
public List<Emp> findAllEmp() {
List<Emp> list = new ArrayList<>();


try {
conn = DBManager.getConnection();
String sql = "select * from emp where state = 1";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
Emp emp = new Emp();//每循环一次new一个新对象,给对象付一次值
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
list.add(emp);//循环一次在集合中增加一条数据
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBManager.closeConn(conn, ps);
}


return list;


}
}

多表联查


//多表联查
//方法一:
public List<Emp> findAllEmp2(){
List<Emp> list = new ArrayList<>();
try {
conn = DBManager.getConnection();
String sql = "select * from emp e left join Dept d on e.deptNo = d.deptNo where state =1";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()){
Emp emp = new Emp();
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
emp.setDname(rs.getString(11));
emp.setLoc(rs.getString(12));
list.add(emp);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBManager.closeConn(conn, ps,rs);
}
return list;
}


//方法二:
public List<Emp> findAllEmp3(){
List<Emp> list = new ArrayList<>();


try {
conn = DBManager.getConnection();
String sql = "select * from emp e left join Dept d on e.deptNo = d.deptNo where state =1";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()){
Emp emp = new Emp();
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
Dept dept = new Dept();
dept.setDeptNo(rs.getInt(10));
dept.setDname(rs.getString(11));
dept.setLoc(rs.getString(12));
emp.setDept(dept);//引入dept表
list.add(emp);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBManager.closeConn(conn, ps,rs);
}


return list;
}
}

以上就是JDBC的基本概述了,JDBC这个看似复杂的技术,其实是一个非常实用的工具。

只要你掌握了它,就可以轻松地处理和管理你的数据。无论你是一位经验丰富的程序员,还是一位刚刚入门的新手,我都强烈推荐你学习和使用JDBC。相信我,当你掌握了JDBC,你会发现它为你的工作和学习带来了极大的便利。

收起阅读 »

Vue实现一个textarea幽灵建议功能

web
不知道你有没有发现Bing AI聊天有个输入提示功能,在用户输入部分内容时后面会给出灰色提示文案。用户只要按下tab键就可以快速添加提示的后续内容。我将这个功能称为幽灵建议。接下来我将用Vue框架来实现这个功能。 布局样式 布局使用label标签作为容器,这...
继续阅读 »

不知道你有没有发现Bing AI聊天有个输入提示功能,在用户输入部分内容时后面会给出灰色提示文案。用户只要按下tab键就可以快速添加提示的后续内容。我将这个功能称为幽灵建议。接下来我将用Vue框架来实现这个功能。



布局样式


布局使用label标签作为容器,这样即使建议内容在上层,也不会影响输入框的输入。


<label class="container">
<textarea></textarea>
<div class="ghost-content"></div>
</label>

样式需要确保输入框与建议内容容器除了颜色外都要一致。建议内容可以通过z-index: -1置于输入框底部,但要注意输入框必须是透明背景。


.container {
position: relative;
display: block;
width: 300px;
height: 200px;
font-size: 14px;
line-height: 21px;
}
.container textarea {
width: 100%;
height: 100%;
padding: 0;
border: 0;
font: inherit;
color: #212121;
background-color: #fff;
outline: none;
}
.ghost-content {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #212121;
opacity: 0.3;
}

显示逻辑


显示逻辑比较简单,当输入框中显示输入内容时,找到匹配的内容后将其显示在建议容器中。以下是代码示例:


import { ref } from 'vue'

const content = ref('') // 输入框内容
const ghostContent = ref('') // 建议内容
const suggestions = ['你好啊', '怎么学编程'] // 建议列表

const handleInput = () => {
ghostContent.value = '' // 内容变化时,清空建议
// 如果为空或者建议内容改变,则不进行后续匹配
if (content.value === '') {
return
}
const suggestion = suggestions.find((item) => item.startsWith(content.value))
if (suggestion) {
ghostContent.value = suggestion
}
}

const handleTabKeydown = () => {
// 监听tab键按下,将输入框内容设置为建议内容,同时清空建议内容
content.value = ghostContent.value
ghostContent.value = ''
}

按照以上代码的写法,已经可以实现幽灵建议的功能了。但还存在一个小问题,输入框内容和建议内容的重叠部分会显得比较粗。因此,最好将重叠部分的文字颜色设置为透明。我的解决方法是使用span标签来包裹重叠部分的内容,然后将span的文字样式设置为透明。此外,为了表示可以使用tab键,我在末尾添加了符号。改进后的代码如下:


// 重复部分省略
// ...
const ghostHTML = ref('') // 建议内容HTML
const handleInput = () => {
ghostContent.value = ''
ghostHTML.value = ''
if (content.value === '' || fromSuggestion) {
fromSuggestion && (fromSuggestion = false)
return
}
const suggestion = suggestions.find((item) => item.startsWith(content.value))
if (suggestion) {
ghostContent.value = suggestion
ghostHTML.value = suggestion.replace(content.value, `<span>${content.value}</span>`) + ' →' // 显示内容替换
}
}

const handleTabKeydown = () => {
content.value = ghostContent.value
ghostContent.value = ''
ghostHTML.value = ''
}

最后,补充一下HTML代码。


<label class="container">
<textarea v-model="content" @input="handleInput" @keydown.tab.prevent="handleTabKeydown"></textarea>
<div class="ghost-content" v-html="ghostHTML"></div>
</label>


  • 我们需要阻止tab按下的默认事件,按下tab键会导致切换到其他元素,使输入框失去焦点;

  • 使用v-html来绑定HTML内容。


作者:60岁咯
来源:juejin.cn/post/7273674732448120895
收起阅读 »

谈谈国内前端的三大怪啖

web
因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。今天聊三个事情:小程序微前端模块加载小程序每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。“我们...
继续阅读 »

因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。

今天聊三个事情:

  • 小程序
  • 微前端
  • 模块加载

小程序

每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。

“我们为什么需要小程序?”

第一次被问到这个问题,是因为一个法国的同事。他被派去做一个移动端业务,刚好那个业务是采用小程序在做。于是一个法国小哥就在被痛苦的中文文档和黑盒逻辑中来回折磨着 🤦。

于是,当我们在有一次交流中,他问出了我这个问题:我们为什么需要小程序?

说实话,我试图解释了 19 年国内的现状,以及微信小程序推出时候所带来的便利和体验等等。总之,在我看来并不够深刻的见解。

即便到现在为止,每次当我使用小程序的时候,依旧会复现这个问题。在 ChatGPT 11 月份出来的时候,我也问了它这个很有国内特色的问题:

看起来它回答的还算不错,至少我想如果它来糊弄那些老外,应该会比我做的更好些。

但如果扪心自问,单从技术上来讲。以上这些事情,一定是有其他方案能解决的。

所以从某种程度上来看,这更像是一场截胡的商业案例:

应用市场

全世界的互联网人都知道应用市场是非常有价值的事情,可以说操作系统最值钱的部分就在于他们构建了自己的应用市场。

只要应用在这里注册发行,雁过拔毛,这家公司就是互联网世界里的统治阶级,规则的制定者。

反之则需要受制于人,APP 做的再大,也只是应用市场里的一个应用,做的好与坏还得让应用商店的评判。

另外,不得不承认的是,一个庞大如苹果谷歌这样的公司,他们的应用商店对于普通国内开发者来说,确实是有门槛的。

在国内海量的 APP 需求来临之前,能否提供一个更低成本的解决方案,来消化这些公司的投资?

毕竟不是所有的小企业都需要 APP,其实他们大部分需求 Web 就可以解决,但是 Web 没牌面啊,做 Web 还得砸搜索的钱才有流量。(某度搜索又做成那样...)

那做操作系统?太不容易,那么多人都溺死在水里了,这水太深。

那有没有一种办法可以既能构建生态,又有 APP 的心智,还能给入驻企业提供流量?

于是,在 19 年夏天,滨海大厦下的软件展业基地里,每天都在轮番播放着,做 XX小程序,拥抱下一个风口...

全新体验心智

小程序用起来挺方便的。

你有没有想过,这些美妙感觉的具体都来自哪些?以及这些真的是 Web 技术本身无法提供的吗?

  1. 靠谱感,每个小程序都有约束和规范,于是你可以将他们整整齐齐的陈放在你的列表里,仿佛你已经拥有了这一个个精心雕琢的作品,相对于一条条记不住的网页地址和鱼龙混杂的网页内容来说,这让你觉得小程序更加的有分量和靠谱。
  2. 安全感,沉浸式的头部,没有一闪而过的加载条,这一切无打扰的设计,都让你觉得这是一个在你本地的 APP,而不是随时可能丢失网页。你不会因为网速白屏而感到焦虑,尽管网络差的时候,你的 KFC 依旧下不了单 😂
  3. 沉浸感,我不知道是否打开网页时顶部黑黑的状态栏是故意留下的,还是不小心的... 这些限制都让用户非常强烈的意识到这是一个网页而不是 APP,而小程序中虽然上面也存在一个空间的空白,但是却可以被更加沉浸的主题色和氛围图替代。网页这个需求做不了?我不信。
H5小程序
  1. 顺滑感,得益于 Native 的容器实现,小程序在所有的视图切换时,都可以表现出于原生应用一样的顺滑感。其实这个问题才是在很多 Hybrid 应用中,主要想借助客户端来处理和解决的问题。类似容器预开、容器切换等技术是可以解决相关问题的,只是还没有一个标准。

我这里没有提性能,说实话我不认为性能在小程序中是有优势的(Native 调用除外,如地图等,不是一个讨论范畴)。作为普通用户,我们感受到的只是离线加载下带来的顺滑而已。

而上述提到的许多优势,这对于一个高品质的 Web 应用来说是可以做得到的,但注意这里是高品质的 Web 应用。而这种“高品质”在小程序这里,只是入驻门槛而已。

心智,这个词,听起来很黑话,但却很恰当。当小程序通过长期这样的筛选,所沉淀出来一批这样品质的应用时。就会让每个用户即便在还没打开一个新的小程序之前,也有不错体验的心理预期。这就是心智,一种感觉跟 Web 不一样,跟 APP 有点像的心智。

打造心智,这件事情好像就是国内互联网企业最擅长做的事情,总是能从一些细微的差别中开辟一条独立的领域,然后不断强化灌输本来就不大的差异,等流量起来再去捞钱变现。


我总是感觉现在的互利网充斥着如何赚钱的想法,好像永远赚不够。“赚钱”这个事情,在这些公司眼里就是圈人圈地抢资源,看看谁先占得先机,别人有的我也得有,这好像是最重要的事情。

很少有企业在思考如何创造些没有的市场,创造些真正对技术发展有贡献,对社会发展有推动作用的事情。所以在国内互联网圈也充斥着一种奇怪的价值观,有技术的不一定比赚过钱的受待见。

管你是 PHP 还是 GO,管你是在做游戏还是直播带货,只要赚到钱就是高人。并且端的是理所应当、理直气壮,有些老板甚至把拍摄满屋子的程序员为自己打工作为一种乐趣。什么情怀、什么优雅、什么愿景,人生就俩字:搞钱。

不是故意高雅,赚钱这件事情本身不寒碜,只是在已经赚到盆满钵满、一家独大的时候还在只是想着赚更多的钱,好像赚钱的目的就是为了赚钱一样,这就有点不合适。企业到一定程度是要有社会责任的,龙头企业每一个决定和举措,都有会影响接下来的几年里这个行业的价值观走向。

当然也不是完全没有好格局的企业,我非常珍惜每一个值得尊重的中国企业,来自一个蔚来车主。

小程序在商业上固然是成功的,但吃的红利可以说还是来自 网页 到 应用 的心智变革。将本来流向 APP 的红利,截在了小程序生态里。

但对于技术生态的发展却是带来了无数条新的岔路,小程序的玩法就决定了它必须生长在某个巨型应用里面,不论是用户数据获取、还是 API 的调用,其实都是取决于应用容器的标准规范。

不同公司和应用之间则必然会产生差异,并且这种差异是墒增式的差异,只会随着时间的推移越变越大。如果每个企业都只关注到自己业务的增长,无外部约束的话,企业必然会根据自己的业务发展和政策需要,选择成本较低的调整 API,甚至会故意制造一些壁垒来增加这种差异。

小程序,应该是 浏览器 与 操作系统 的融合,这本应该是推动这两项技术操刀解决的事情。

微前端

qiankun、wujie、single-spa 是近两年火遍前端的技术方案,同样一个问题:我们为什么需要微前端?

我不确定是否每个在使用这项技术的前端都想清楚了这个问题,但至少在我面试过的候选人中,我很少遇到对自己项目中已经在使用的微前端,有很深的思考和理解的人。

先说下我的看法:

  1. 微前端,重在解决项目管理而不在用户体验。
  2. 微前端,解决不了该优化和需要规范的问题。
  3. 微前端,在挽救没想清楚 MPA 的 SPA 项目。

没有万能银弹

银色子弹(英文:Silver Bullet),或者称“银弹”“银质子弹”,指由纯银质或镀银的子弹。在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银色子弹往往被描绘成具有驱魔功效的武器,是针对狼人、吸血鬼等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。

所有技术的发展都是建立在前一项技术的基础之上,但技术依赖的选择过程中一定需要保留第一性原理的意识。

当 React、Vue 兴起,当 打包技术(Webpack) 兴起,当 网页应用(SPA) 兴起,这些杰出的技术突破都在不同场景和领域中给行业提供了新的思路、新的方案。

不知从何时开始,前端除了 div 竟说不出其他的标签(还有说 View 的),项目中再也不会考虑给一个通用的 class 解决通用样式问题。

不知从何时开始,有没有权限需要等到 API 请求过后才知道,没有权限的话再把页面跳转过去申请。

不知从何时开始,大家的页面都放在了一个项目里,两个这样的巨石应用融合竟然变成了一件困难的事。

上面这些不合理的现状,都是在不同的场景下,不思考适不适合,单一信奉 “一招吃遍天” 下演化出的问题。

B 端应用,是否应该使用 SPA? 这其实是一个需要思考的问题。

微前端从某种程度上来讲,是认准 SPA 了必须是互联网下一代应用标准的产物,好像有了 SPA 以后,MPA 就变得一文不值。甭管页面是移动端的还是PC的;甭管页面是面对 C 端的还是 B 端的;甭管一个系统是有 20 个页面还是 200 个页面,一律行这套逻辑。

SPA 不是万能银弹,React 不是万能银弹,Tailwind 不是万能银弹。在新技术出现的时候,保持热情也要保持克制。

ps. 我也十分痛恨 React 带来的这种垄断式的生态,一个 React 组件将 HTML 和 Style 都吃进去,让你即使在做一个移动端的纯展示页面时,也需要背上这些称重的负担。

质疑 “墨守成规”,打开视野,深度把玩,理性消费。

分而治之

分治法,一个很基本的工程思维。

在我看来在一个正常商业迭代项目中的主要维护者,最好不要超过 3 个人,注意是主要维护者(Maintainer) 。

你应该让每个项目都有清晰的责任人,而不是某行代码,某个模块。责任人的理解是有归属感,有边界感的那种,不是口头意义上的责任人。(一些公司喜欢搞这种虚头巴脑的事情,什么连坐…)

我想大部分想引入微前端的需求都是类似 如何更好的划分项目边界,同时保留更好的团队协同。

比如 导航菜单 应该是独立收口独立管理的,并且当其更新时,应该同时应用于所有页面中。类似的还有 环境变量、时区、主题、监控及埋点。微前端将这些归纳在主应用中。

而具体的页面内容,则由对应的业务进行开发子应用,最后想办法将路由关系注册进主应用即可。

当然这样纯前端的应用切换,还会出现不同应用之间的全局变量差异、样式污染等问题,需要提供完善的沙箱容器、隔离环境、应用之间通信等一系列问题,这里不展开。

当微前端做到这一部分的时候,我不禁在想,这好像是在用 JavaScript 实现一个浏览器的运行容器。这种本应该浏览器本身该做的事情,难道 JS 可以做的更好?

只是做到更好的项目拆分,组织协同的话,引入后端服务,由后端管控路由表和页面规则,将页面直接做成 MPA,这个方案或许并不比引入微前端成本高多少。

体验差异

从 SPA 再回 MPA,说了半天不又回去了么。

所以不防想想:在 B端 业务中使用 SPA 的优势在哪里?

流畅的用户体验:

这个话题其实涵盖范围很广,对于 SPA 能带来的 “流畅体验”,对于大多数情况下是指:导航菜单不变,内容变化 发生变化,页面切换中间不出现白屏

但要做到这个点,其实对于 MPA 其实并没有那么困难,你只需要保证你的 FCP 在 500ms 以内就行。

以上的页面切换全部都是 MPA 的多页面切换,我们只是简单做了导航菜单的 拆分 和 SWR,并没有什么特殊的 preload、prefetch 处理,就得到了这样的效果。

因为浏览器本身在页面切换时会在 unload 之前先 hold 当前页面的视图不变,发起一下一个 document 的请求,当页面的视图渲染做到足够快和界面结构稳定就可以得到这样的效果。

这项浏览器的优化手段我找了很久,想找一篇关于它的博文介绍,但确实没找到相关内容,所以 500ms 也是我的一个大致测算,如果你知道相关的内容,可以在评论区补充,不胜感激。

所以从这个角度来看,浏览器本身就在尽最大的努力做这些优化,并且他们的优化会更底层、更有效的。

离线访问 (PWA)

SPA 确实会有更好的 PWA 组织能力,一个完整的 SPA 应用甚至可以只针对编译层做改动就可以支持 PWA 能力。

但如果看微前端下的 SPA 应用,需要支持 PWA 那就同样需要分析各子应用之间的元数据,定制 Service Worker。这种组织关系和定制 SW,对于元数据对于数据是来自前端还是后端,并不在意。

也就是说微前端模式下的 PWA,同样的投入成本,把页面都管理在后端服务中的 MPA 应用也是可以做到相同效果的。

项目协同、代码复用

有人说 SPA 项目下,项目中的组件、代码片段是可以相互之间复用的,在 MPA 下就相对麻烦。

这其实涉及到项目划分的领域,还是要看具体的需求也业务复杂度来定。如果说整个系统就是二三十个页面,这做成 SPA 使用前端接管路由高效简单,无可厚非。

但如果你本身在面对的是一个服务于复杂业务的 B 端系统,比如类似 阿里云、教务系统、ERP 系统或者一些大型内部系统,这种往往需要多团队合作开发。这种时候就需要跟完善的项目划分、组织协同和系统整合的方案。

这个时候 SPA 所体现出的优势在这样的诉求下就会相对较弱,在同等投入的情况下 MPA 的方案反而会有更少的执行成本。

也不是所有项目一开始就会想的那么清楚,或许一开始的时候就是个简单的 SPA 项目,但是随着项目的不断迭代,才变成了一个个复杂的巨石应用,现在如果再拆出来也会有许多迁移成本。引入微前端,则可以...

这大概是许多微前端项目启动的背景介绍,我想说的是:对于屎山,我从来不信奉“四两拨千斤”

如果没有想好当下的核心问题,就引入新的“银弹”解决问题,只会是屎山雕花。

项目协同,抽象和复用这些本身不是微前端该解决的问题,这是综合因素影响下的历史背景问题。也是需要一个个资深工程师来把控和解决的核心问题,就是需要面对不同的场景给出不同的治理方案。

这个道理跟防沙治沙一样,哪有那么多一蹴而就、立竿见影的好事。

模块加载

模块加载这件事情,从玉伯大佬的成名作 sea.js 开始就是一个非常值得探讨的问题。在当时 jQuery 的时代里,这是一个绝对超前的项目,我也在实际业务中体会过在无编译的环境下 sea.js 的便捷。

实际上,不论是微前端、低代码、会场搭建等热门话题离不开这项技术基础。

import * from * 我们每天都在用,但最终的产物往往是一个自运行的 JS Bundle,这来自于 Webpack、Vite 等编译技术的发展。让我们可以更好的组织项目结构,以构建更复杂的前端应用。

模块的概念用久了,就会自然而然的在遇到浏览器环境中,遇到动态模块加载的需求时,想到这种类似模块加载的能力。

比如在遇到会场千奇百怪的个性化营销需求时,能否将模块的 Props 开放出来,给到非技术人员,以更加灵活的方式让他们去做自由组合。

比如在低代码平台中,让开发者自定义扩展组件,动态的将开发好的组件注册进低代码平台中,以支持更加个性的需求。

在万物皆组件的思想影响下,把一整个完整页面都看做一个组件也不是不可以。于是在一些团队中,甚至提倡所有页面都可以搭建、搭建不了的就做一个大的页面组件。这样及可以减少维护运行时的成本,又可以统一管控和优化,岂不美哉。

当这样大一统的“天才方案”逐渐发展成为标准后,也一定会出现一些特殊场景无法使用,但没关系,这些天才设计者肯定会提供一种更加天才的扩展方案出来。比如插件,比如扩展,比如 IF ELSE。再后来,就会有性能优化了,而优化的 追赶对象 就是用原来那种简单直出的方案。

有没有发现,这好像是在轮回式的做着自己出的数学题,一道接一道,仿佛将 1 + 1的结论重新演化了一遍。

题外话,我曾经自己实现过一套通过 JSON Schema 描述 React 结构的 “库” ,用于一个低代码核心层的渲染。在我的实现过程中,我越发觉得我在做一件把 JSX 翻译成 JS 的事情,但 JSX 或者 HTML 本身不就是一种 DSL 么。为什么一定要把它翻译成 JSON 来传输呢?或者说这样的封装其本身有意义么?这不就是在做 PHP、.Net 直接返回带有数据的 HTML Ajax 一样的事情么。

传统的浏览器运行环境下要实现一个模块加载器,无非是在全局挂载一个注册器,通过 Script 插入一段新的 JS,该 JS 通过特殊的头尾协议,将运行时的代码声明成一个函数,注册进事先挂载好的注册器。

但实际的实现场景往往要比这复杂的多,也有一些问题是这种非原生方式无法攻克的问题。比如全局注册器的命名冲突;同模块不同版本的加载冲突;并发加载下的时序问题;多次模块加载的缓存问题 等等等等等...

到最后发现,这些事情好像又是在用 JS 做浏览器该做的事情。然而浏览器果然就做了,,Vite 就主要采用这种模式实现了它 1 年之内让各大知名项目切换到 Vite 的壮举。

“但我们用不了,有兼容性问题。”

哇哦,当我看着大家随意写出的 display: grid 样式定义,不禁再次感叹人们对未知的恐惧。

import.meta 的兼容性是另外一个版本,是需要 iOS12 以上,详情参考:caniuse.com/?search=imp…

试想一下,现在的低代码、会场搭建等等各类场景的模块加载部分,如果都直接采用 ESM 的形式处理,这对于整个前端生态和开发体验来说会有多么大的提升。

模块加载,时至今日,本来就已经不再需要 loader。 正如 seajs 中写的:前端模块化开发那点历史

历史不是过去,历史正在上演。随着 W3C 等规范、以及浏览器的飞速发展,前端的模块化开发会逐步成为基础设施。一切终究都会成为历史,未来会更好。

结语

文章的结尾,我想感叹另外一件事,国人为什么一定要有自己的操作系统?为什么一定需要参与到一些规范的制定中?

因为我们的智慧需要有开花的土壤,国内这千千万开发者的抱负需要有地方释放。

如果没有自己掌握核心技术,就是只能在问题出现的时候用另类的方式来解决。最后在一番折腾后,发现更底层的技术只要稍稍一改就可以实现的更好。这就像三体中提到的 “智子” 一样,不断在影响着我们前进的动力和方向。

不论是小程序、微前端还是模块加载。试想一下,如果我们有自己的互联网底蕴,能决定或者影响操作系统和浏览器的底层能力。这些 “怪啖” 要么不会出现,要么就是人类的科技创新。

希望未来技术人不用再追逐 Write Once, Run Everywhere 的事情...


作者:YeeWang
来源:juejin.cn/post/7267091810366488632
收起阅读 »

QR码是怎么工作的?

web
原文链接: typefully.com/DanHollick/… 作者:Dan Hollick 你有想过QR码是如何工作的吗? 我也没有想过,但是它真的很低调很迷人~ 【警告】这里有一些非常书呆子的东西👇 ) QR码是由丰田的一个子公司发明的,目的是为了在整...
继续阅读 »

原文链接: typefully.com/DanHollick/…


作者:Dan Hollick


你有想过QR码是如何工作的吗?


我也没有想过,但是它真的很低调很迷人~


【警告】这里有一些非常书呆子的东西👇 )


image.png


QR码是由丰田的一个子公司发明的,目的是为了在整个制造过程中跟踪零件信息。


之前出现的条形码被证明是不足够的 - 它们只能从特定的角度读取,并且相对于他们的大小来说,并不能储存很多的数据。


那么 QR 码不只解决了这些问题


image.png


QR 码最独一无二的地方在于这些正方体,这些正方体被称为“查找器”,这些正方体帮助了你的阅读器检测到码的存在。


第四个小的正方体,被称作对齐模式,它是用来定向代码的,使它可以在任何角度呈现,阅读器仍然哪个方向是向上的。


image.png


你可能从来都没有注意过,但是每个 QR 码都有这些叫做定时模式的黑白相间的点。


这些黑白相间的点告诉阅读器单个模块有多大以及整个 QR 码有多大 -- 也就是版本。


image.png


版本一:最小的。
版本四十:最大的。


image.png


关于格式的信息被存在查找器旁边的两个条纹中。


它被存储了两次,所以即使QR码被部分遮挡,它也是可读的。(你会注意到这是一个反复出现的主题。)


image.png


它存储了三个重要的信息片段



  1. 掩码(Mask)

  2. 纠错级别

  3. 纠错格式


我知道这听起来很无聊,但是实际上,他还是很有意思的(doge


image.png


首先,纠错 - 这是什么玩意?


从本质上讲,它规定了在 QR 码中存储多少冗余信息,以确保即使部分信息丢失也能保持可读。


image.png


这真的很酷好吗 - 如果您的代码在户外,您可以选择更高的冗余级别,以确保它在模糊的时候也能正常工作。


试试下面这个二维码


image.png


第二个,这个 mask,这个是个什么东西?


首先你需要知道,QR 阅读器在黑色区域和白色区域的数量一样的时候工作的最好。


但是数据可能无法发挥作用,因此使用掩码来平衡。


image.png


当掩版应用于QR码时,任何落在掩码暗部的东西都会被反转。


白色区域变成黑色,黑色区域变成白色。


image.png


有8种标准模式,一个接一个地应用。使用达到最佳结果的模式,并存储该信息,以便读者可以不应用掩码。


最后呢,就到了我们的实际数据的部分。


奇怪的是,数据从右下角开始,然后像图中那样蜿蜒而上。


从哪开始几乎不重要了,因为它可以从每个角度读取。


image.png


这里的第一个信息块告诉读者数据是以什么模式编码的,第二个告诉它长度。


在我们的例子中,每个字符占用8位块,或者称为字节,共有24个字节。


image.png


在我们的数据之后还有一些剩余的空间。这是存储纠错信息的地方,以便在部分模糊的情况下可以读取。它的工作方式实际上非常非常复杂,所以我把它省略了。基本上就是这样!


image.png


对于制作 QR 码的书呆子来说,一个有意思的事实是: QR码最酷的事情是发明QR码的Denso Wave公司从未行使他们的专利,并且免费发布这项技术!


作者:阳树阳树
来源:juejin.cn/post/7311142182810992703
收起阅读 »