注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

记一次修改一行代码导致的线上BUG

web
背景介绍 先描述一下需求,要在一个老项目里根据type类型,给一个试题题干组件新增一个class样式,type是在url地址栏上面携带的。简单,一行代码搞定,五分钟部署,十分钟留给测试,然后跟车上线,打卡下班! 《凉凉》送给自己 看标题就知道结果了,第二天下午...
继续阅读 »

1920_1200_20100319011154682575.jpg


背景介绍


先描述一下需求,要在一个老项目里根据type类型,给一个试题题干组件新增一个class样式type是在url地址栏上面携带的。简单,一行代码搞定,五分钟部署,十分钟留给测试,然后跟车上线,打卡下班!


《凉凉》送给自己


看标题就知道结果了,第二天下午现网问题来了,一线反馈某个页面题干不展示了,值班同事排查一圈,找到我说我昨天加的代码报错了!


006Cmetyly1ff16b3zxvxj308408caa8.jpg


惊了,就加了一行业务代码,其他都是样式,测试也通过了,这也能有问题?绩效C打底稳了(为方便写文章,实际判断用变量代替):


<div :class="{'addClass': $route.query.type === 'xx'}">
...
</div>

temp.png
问题其实很简单,$route为undefined了,导致query获取有问题,这让我点怀疑自己,难道这写错了?管不了太多,只能先兼容上线了。


$route && $route.query && $route.query.type

其实是可以用?.简写的,但是这个项目实在不“感动”了,保险写法,解决问题优先。提申请,拉评审,走流程,上线,问题解决,松口气,C是保住了。


问题分析


解决完问题,还要写线上问题分析报告,那只能来扒一扒代码来看看了。首先,这个项目使用的是多页应用,每个页面都是一个新的SPA,我改的页面先叫组件A吧,组件A在页面A里被使用,没问题;组件A同样被页面B使用,报错了。那接下来简单了,看代码:


// 2022-09-26 新增
import App from '@/components/pages/页面A'
import router from '@/config/router.js'
// initApp 为封装的 new Vue
import { initApp, Vue } from '../base-import'
initApp(App, router)

// 2020-10-18 新增
import App from '@/components/pages/页面b'
new Vue({
el: '#app',
components: { App },
template: '<App/>'
})

两个页面的index.js文件,两种写法,一个引用了router,一个没有引用,被这个神仙代码整懵了。然后再看了一下其他页面,也都是两种写法掺着写的,心态崩了。这分析报告只能含着泪写了...


最后总结



  1. 问题不是关键,关键的是代码规范;

  2. 修改新项目之前,最好看一下代码逻辑,有熟悉的同事最好,可以沟通了解一下业务(可以避免部分问题);

  3. 当想优化之前代码的时候,要全面评估,统一优化,上面的写法我也找同事了解了,因为之前写法不满足当时的需求,他就封装了新方法,但是老的没有修改,所以就留了坑;


作者:追风筝的呆子
来源:juejin.cn/post/7252198762625089596
收起阅读 »

什么!一个项目给了8个字体包???

web
🙋 遇到的问题 在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。 首先,字体包的使用分为了以下几种情况: 无特殊要求的语言使用字体A,阿拉伯语言使用字体B; 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用...
继续阅读 »

🙋 遇到的问题


在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。


首先,字体包的使用分为了以下几种情况:



  1. 无特殊要求的语言使用字体A,阿拉伯语言使用字体B;

  2. 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用 BoldMediumRegularThin 四种字体包;


所以,我现在桌面上摆着 8 个字体包:



  • A-Bold.tff

  • A-Medium.tff

  • A-Regular.tff

  • A-Thin.tff

  • B-Bold.tff

  • B-Medium.tff

  • B-Regular.tff

  • B-Thin.tff


image.png
不同语言要使用不同的字体包,不同粗细也要使用不同的字体包!


还有一个前提是,设计给的设计图都是以字体A为准,所以在 Figma 中复制出来的 CSS 代码中字体名称都是A。


刚接到这个需求时还是比较懵的,一时想不出来怎么样才能以最少的逻辑判断最少的文件下载最少的代码改动去实现在不同情况下自动的去选择对应的字体包。


因为要涉及到语言的判断,最先想到的还是通过 JS,然后去添加相应的类名。但这样也只能判断语言使用A或B,粗细还是解决不了。


image.png


看来还是要用 CSS 解决。


首先我将所有的8个字体先定义好:


@font-face {
font-family: A-Bold;
src: url('./fonts/A-Bold.ttf');
}

/* ... */

@font-face {
font-family: B-Thin;
src: url('./fonts/B-Thin.ttf');
}

image.png


🤲🏼 如何根据粗细程度自动选择对应字体包


有同学可能会问,为什么不直接使用 font-weight 来控制粗细而是用不同的字体包呢?


我们来看下面这个例子,我们使用同一个字体, font-weight 分别设置为900、500、100,结果我们看到的字体粗细是一样的。


对的,很多字体不支持 font-weight 所以我们需要用不同粗细的字体包。


image.png


所以,我们可以通过 @font-face 中的 font-weight 属性来设置字体的宽度:


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}
@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

注意,这里我们把字体名字都设为相同的,如下图所示,这样我们就成功的解决了第一个问题:不同粗细也要使用不同的字体包;


image.png


并且,如果我们只是定义而未真正使用时,不会去下载未使用的字体包,再加上字体包的缓存策略,就可以最大程度节省带宽:


image.png


🔤 如何根据不同语言自动选择字体包?


通过张鑫旭的博客找到了解决办法,使用 unicode-range 设置字符 unicode 范围,从而自定义字体包。


unicode-range 是一个 CSS 属性,用于指定字体文件所支持的 Unicode 字符范围,以便在显示文本时选择适合的字体。


它的语法如下:


@font-face {
font-family: "Font Name";
src: url("font.woff2") format("woff2");
unicode-range: U+0020-007E, U+4E00-9FFF;
}

在上述例子中,unicode-range 属性指定了字体文件支持的字符范围。使用逗号分隔不同的范围,并使用 U+XXXX-XXXX 的形式表示 Unicode 字符代码的范围。


通过设置 unicode-range 属性,可以优化字体加载和页面渲染性能,只加载所需的字符范围,减少不必要的网络请求和资源占用。


通过查表得知阿拉伯语的 unicode 的范围为:U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F 这么几个区间。所以我们设置字体如下,因为设计以 A 字体为准,所以在 Figma 中给出的样式代码字体名均为 A,所以我们把 B 字体的字体名也设置为 A:


image.png


当使用字体的字符中命中 unicode-rang 的范围时,自动下载相应的字体包。


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}

@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}

@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}

@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

:root {
--ARABIC_UNICODE_RANGE: U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F;
}
@font-face {
font-family: A;
src: url('./fonts/B-Bold.ttf');
font-weight: 600;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Medium.ttf');
font-weight: 500;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Regular.ttf');
font-weight: 400;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Thin.ttf');
font-weight: 300;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
p {
font-family: A;
}

总结


遇到的问题:



  1. 两种字体,B 字体为阿拉伯语使用,A 字体其他语言使用。根据语言自动选择。

  2. 根据字宽自动选择相应的字体包。

  3. 可以直接使用 Figma 中生成的样式而不必每次手动改动。

  4. 尽可能节省带宽。


我们通过 font-weight 解决了问题2,并通过 unicode-range 解决了问题1。


并且实现了按需下载相应字体包,不使用时不下载。


Figma 中的代码可以直接复制粘贴,无需任何修改即可根据语言和自宽自动使用相应字体包。




参考资料:http://www.zhangxinxu.com/wordpr

作者:Mengke
来源:juejin.cn/post/7251884086536781880
ess/2…

收起阅读 »

用 node 实战一下 CSRF

web
前言 之前面试经常被问到 CSRF, 跨站请求伪造 大概流程比较简单, 大概就是用户登录了A页面,存下来登录凭证(cookie), 攻击者有诱导受害者打开了B页面, B页面中正好像A发送了一个跨域请求,并把cookie进行了携带, 欺骗浏览器以为是用户的行为...
继续阅读 »

前言


之前面试经常被问到 CSRF, 跨站请求伪造



大概流程比较简单, 大概就是用户登录了A页面,存下来登录凭证(cookie), 攻击者有诱导受害者打开了B页面, B页面中正好像A发送了一个跨域请求,并把cookie进行了携带, 欺骗浏览器以为是用户的行为,进而达到执行危险行为的目的,完成攻击



上面就是面试时,我们通常的回答, 但是到底是不是真是这样呢? 难道这么容易伪造吗?于是我就打算试一下能不能实现


接下来,我们就通过node起两个服务 A服务(端口3000)和B服务(端口4000), 然后通过两个页面 A页面、和B页面模拟一下CSRF。


我们先约定一下 B页面是正常的页面, 起一个 4000 的服务, 然后 A页面为伪造者的网站, 服务为3000


先看B页面的代码, B页面有一个登录,和一个获取数据的按钮, 模拟正常网站,需要登录后才可以获取数据


<body>
<div>
正常 页面 B
<button onclick="login()">登录</button>
<button onclick="getList()">拿数据</button>
<ul class="box"></ul>
<div class="tip"></div>
</div>
</body>
<script>
async function login() {
const response = await fetch("http://localhost:4000/login", {
method: "POST",
});
const res = await response.json();
console.log(res, "writeCookie");
if (res.data === "success") {
document.querySelector(".tip").innerHTML = "登录成功, 可以拿数据";
}
}

async function getList() {
const response = await fetch("http://localhost:4000/list", {
method: "GET",
});

if (response.status === 500) {
document.querySelector(".tip").innerHTML = "cookie失效,请先登录!";
document.querySelector(".box").innerHTML = "";
} else {
document.querySelector(".tip").innerHTML = "";
const data = await response.json();
let html = "";
data.map((el) => {
html += `<div>${el.id} - ${el.name}</div>`;
});
document.querySelector(".box").innerHTML = html;
}
}
</script>

在看B页面的服务端代码如下:


const express = require("express");
const app = express();

app.use(express.json()); // json
app.use(express.urlencoded({ extends: true })); // x-www-form-urlencoded

app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
// 允许客户端跨域传递的请求头
res.header("Access-Control-Allow-Headers", "Content-Type");
next();
});

app.use(express.static("public"));

app.get("/list", (req, res) => {
const cookie = req.headers.cookie;
if (cookie !== "user=allow") {
res.sendStatus("500");
} else {
res.json([
{ id: 1, name: "zhangsan" },
{ id: 2, name: "lisi" },
]);
}
});

app.post("/login", (req, res) => {
res.cookie("user", "allow", {
expires: new Date(Date.now() + 86400 * 1000),
});
res.send({ data: "success" });
});

app.post("/delete", (req, res) => {
const cookie = req.headers.cookie;
if (req.headers.referer !== req.headers.host) {
console.log("should ban!");
}
if (cookie !== "user=allow") {
res.sendStatus("500");
} else {
res.json({
data: "delete success",
});
}
});

app.listen(4000, () => {
console.log("sever 4000");
});

B 服务有三个接口, 登录、获取列表、删除。 再触发登录接口的时候,会像浏览器写入cookie, 再删除或者获取列表的时候,都先检测有没有将指定的cookie传回,如果有就认为有权限


然后我们打开 http://localhost:4000/B.html 先看看B页面功能是否都正常


image.png


我们看到此时 B 页面功能和接口都是正常的, cookie 也正常进行了设置,每次获取数据的时候,都是会携带cookie到服务端校验的


那么接下来我们就通过A页面,起一个3000端口的服务,来模拟一下跨域情况下,能否完成获取 B服务器数据,调用 B 服务器删除接口的功能


A页面代码


  <body>
<div>
伪造者页面 A
<form action="http://localhost:4000/delete" method="POST">
<input type="hidden" name="account" value="xiaoming" />
</form>
<script>
// 这行可以放到控制台执行,便于观察效果
// document.forms[0].submit();
</script>
</div>
<ul class="box"></ul>
<div class="tip"></div>
</body>

A页面服务端代码


  <body>
<div>
伪造者页面 A
<form action="http://localhost:4000/delete" method="POST">
<input type="hidden" name="account" value="xiaoming" />
</form>
<script>
// 这行可以放到控制台输入
// document.forms[0].submit();
</script>
<script src="http://localhost:4000/list"></script>
</div>

</body>

于是在我们 访问 http://localhost:3000/A.html 页面的时候发现, 发现list列表确实,请求到了, 控制台输入 document.forms[0].submit() 时发现,确实删除也发送成功了, 是不是说明csrf就成功了呢, 但是其实还不是, 关键的一点是, 我们在B页面设置cookie的时候, domain设置的是 localhost 那么其实在A页面, 发送请求的时候cookie是共享的状态, 真实情况下,肯定不会是这样, 那么为了模拟真实情况, 我们把 http://localhost:3000/A.html 改为 http://127.0.0.1:3000/A.html, 这时发现,以及无法访问了, 那么这是怎么回事呢, 说好的,cookie 会在获取过登录凭证下, 再次访问时可以携带呢。


image.png


于是,想了半天也没有想明白, 难道是浏览器限制严格进行了限制, 限制规避了这个问题? 难道我们背的面试题是错误的?


有知道的

作者:重阳微噪
来源:juejin.cn/post/7250374485567340603
小伙伴,欢迎下方讨论

收起阅读 »

前端流程图插件对比选型

web
前言 前端领域有多种流程库可供选择,包括但不限于vue-flow、butterfly、JointJS、AntV G6、jsPlumb和Flowchart.js。这些库都提供了用于创建流程图、图形编辑和交互的功能。然而,它们在特性、易用性和生态系统方面存在一些差...
继续阅读 »

Snipaste_2023-07-04_15-49-12.png


前言


前端领域有多种流程库可供选择,包括但不限于vue-flow、butterfly、JointJS、AntV G6、jsPlumb和Flowchart.js。这些库都提供了用于创建流程图、图形编辑和交互的功能。然而,它们在特性、易用性和生态系统方面存在一些差异。


流程图插件汇总


序号名称地址
1vue-flowgithub.com/bcakmakoglu…
2butterflygithub.com/alibaba/but…
3JointJShttp://www.jointjs.com/
4AntV G6antv-2018.alipay.com/zh-cn/g6/3.…
5jsPlumbgithub.com/jsplumb/jsp…
6Flowchart.jsgithub.com/adrai/flowc…

流程图插件分析


vue-flow


简介


vue-flowReactFlow 的 Vue 版本,目前只支持 在Vue3中使用,对Vue2不兼容,目前国内使用较少。包含四个功能组件 core、background、controls、minimap,可按需使用。


使用


Vue FlowVue下流程绘制库。安装:
npm i --save @vue-flow/core 安装核心组件
npm i --save @vue-flow/background 安装背景组件
npm i --save @vue-flow/controls 安装控件(放大,缩小等)组件
npm i --save @vue-flow/minimap 安装缩略图组件

引入组件:
import { Panel, PanelPosition, VueFlow, isNode, useVueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'

引入样式:
@import '@vue-flow/core/dist/style.css';
@import '@vue-flow/core/dist/theme-default.css';

优缺点分析


优点:



  1. 轻松上手:内置缩放和平移功能、元素拖动、选择等等。

  2. 可定制:使用自定义节点、边缘和连接线并扩展Vue Flow的功能。

  3. 快速:链路被动更改,仅重新渲染适当的元素。

  4. 工具和组合:带有图形助手和状态可组合函数,用于高级用途。

  5. 附加组件:背景(内置模式、高度、宽度或颜色),小地图(右下角)、控件(左下角)。


缺点:



  1. 仓库迭代版本较少,2022年进入首次迭代。

  2. 国内使用人数少,没有相关技术博客介绍,通过官网学习。


butterfly


简介


Butterfly是由阿里云-数字产业产研部孵化出来的的图编辑器引擎,具有使用自由、定制性高的优势,已支持上百张画布。号称 “杭州余杭区最自由的图编辑器引擎”。


使用



  • 安装


//
npm install butterfly-dag --save


  • 在 Vue3 中使用


<script lang="ts" setup>
import {TreeCanvas, Canvas} from 'butterfly-dag';
const root = document.getElementById('chart')
const canvas = new Canvas({
root: root,
disLinkable: true, // 可删除连线
linkable: true, // 可连线
draggable: true, // 可拖动
zoomable: true, // 可放大
moveable: true, // 可平移
theme: {
edge: {
shapeType: "AdvancedBezier",
arrow: true,
arrowPosition: 0.5, //箭头位置(0 ~ 1)
arrowOffset: 0.0, //箭头偏移
},
},
});
canvas.draw(mockData, () => {
//mockData为从mock中获取的数据
canvas.setGridMode(true, {
isAdsorb: false, // 是否自动吸附,默认关闭
theme: {
shapeType: "circle", // 展示的类型,支持line & circle
gap: 20, // 网格间隙
background: "rgba(0, 0, 0, 0.65)", // 网格背景颜色
circleRadiu: 1.5, // 圆点半径
circleColor: "rgba(255, 255, 255, 0.8)", // 圆点颜色
},
});
});
</script>

<template>
<div class="litegraph-canvas" id="chart"></div>
</template>

优缺点分析


优点:



  1. 轻松上手:基于dom的设计模型大大方便了用户的入门门槛,提供自定义节点,锚点的模式大大降低了用户的定制性。

  2. 多技术栈支持:支持 jquery 基于 dom 的设计,也包含 butterfly-react、butterfly-vue 两种设计。

  3. 核心概念少而精:提供 画布(Canvas)、节点(Node)、线(Edge)等核心概念。

  4. 优秀的组件库支持:对于当前使用组件库来说,可以大量复用现有的组件。


缺点:



  1. butterfly 对 Vue的支持不是特别友好,这跟阿里的前端技术主栈为React有关,butterfly-vue库只支持 Vue2版本。在Vue3上使用需要对 butterfly-drag 进行封装。


JointJS


简介


创建静态图表或完全交互式图表工具,例如工作流编辑器、流程管理工具、IVR 系统、API 集成器、演示应用程序等等。


属于闭源收费项目,暂不考虑。


AntV G6


简介


AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便、专业可靠、无限可能的数据可视化最佳实践。G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。G6可以实现很多d3才能实现的可视化图表。


使用



  • 安装


npm install --save @antv/g6	//安装


  • 在所需要的文件中引入


<template>
/* 图的画布容器 */
<div id="mountNode"></div>
</template>

<script lang="ts" setup>
import G6 from '@antv/g6';
// 定义数据源
const data = {
// 点集
nodes: [
{
id: 'node1',
x: 100,
y: 200,
},
{
id: 'node2',
x: 300,
y: 200,
},
],
// 边集
edges: [
// 表示一条从 node1 节点连接到 node2 节点的边
{
source: 'node1',
target: 'node2',
},
],
};

// 创建 G6 图实例
const graph = new G6.Graph({
container: 'mountNode', // 指定图画布的容器 id
// 画布宽高
width: 800,
height: 500,
});
// 读取数据
graph.data(data);
// 渲染图
graph.render();
</script>



优缺点分析


优点:



  1. 强大的可定制性:G6 提供丰富的图形表示和交互组件,可以通过自定义配置和样式来实现各种复杂的图表需求。

  2. 全面的图表类型支持:G6 支持多种常见图表类型,如关系图、流程图、树图等,可满足不同领域的数据可视化需求。

  3. 高性能:G6 在底层图渲染和交互方面做了优化,能够处理大规模数据的展示,并提供流畅的交互体验。


缺点:



  1. 上手难度较高:G6 的学习曲线相对较陡峭,需要对图形语法和相关概念有一定的理解和掌握。

  2. 文档相对不完善:相比其他成熟的图表库,G6 目前的文档相对较简单,部分功能和使用方法的描述可能不够详尽,需要进行更深入的了解与实践。


jsPlumb


简介


一个用于创建交互式、可拖拽的连接线和流程图的 JavaScript 库。它在 Web 应用开发中广泛应用于构建流程图编辑器、拓扑图、组织结构图等可视化操作界面。


使用


<template>
<div ref="container">
<div ref="sourceElement">Source</div>
<div ref="targetElement">Target</div>
</div>

</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { jsPlumb } from 'jsplumb';

const container = ref<HTMLElement | null>(null);
const sourceElement = ref<HTMLElement | null>(null);
const targetElement = ref<HTMLElement | null>(null);

onMounted(() => {
// 创建 jsPlumb 实例
const jsPlumbInstance = jsPlumb.getInstance();

// 初始化 jsPlumb 实例设置
if (container.value) {
jsPlumbInstance.setContainer(container.value);
}

// 创建连接线
if (sourceElement.value && targetElement.value) {
jsPlumbInstance.connect({
source: sourceElement.value,
target: targetElement.value,
});
}
});
</script>

优缺点分析


优点:



  1. 简单易用:jsPlumb 提供了直观的 API 和丰富的文档,比较容易上手和使用。

  2. 可拓展性:允许开发人员根据自己的需求进行定制和扩展,使其适应不同的应用场景。

  3. 强大的连接功能:jsPlumb 允许创建各种连接类型,包括直线、曲线和箭头等,满足了复杂交互需求的连接效果。
    缺点:

  4. 文档更新不及时:有时候,jsPlumb 的官方文档并没有及时更新其最新版本的特性和用法。

  5. 性能考虑:在处理大量节点、连接线或复杂布局时,jsPlumb 的性能可能受到影响,需要进行优化。


Flowchart.js


简介


Flowchart.js 是一款开源的JavaScript流程图库,可以使用最短的语法来实现在页面上展示一个流程图,目前大部分都是用在各大主流 markdown 编辑器中,如掘金、csdn、语雀等等。


使用


flowchat
start=>start: 开始
end=>end: 结束
input=>inputoutput: 我的输入
output=>inputoutput: 我的输出
operation=>operation: 我的操作
condition=>condition: 确认
start->input->operation->output->condition
condition(yes)->end
condition(no)->operation

优缺点


优点:



  1. 使用方便快捷,使用几行代码就可以生成一个简单的流程图。

  2. 可移植:在多平台上只需要写相同的代码就可以实现同样的效果。


缺点:



  1. 可定制化限制:对于拥有丰富需求的情况下,flowchartjs只能完成相对简单的需求,没有高级的定制化功能。

  2. 需要花费一定时间来学习他的语法和规则,但是flowchartjs的社区也相对不太活跃。


对比分析




  1. 功能和灵活性:



    • Butterfly、G6 和 JointJS 是功能较为丰富和灵活的库。它们提供了多种节点类型、连接线样式、布局算法等,并支持拖拽、缩放、动画等交互特性。

    • Vue-Flow 来源于 ReactFlow 基于 D3和vueuse等库,提供了 Vue 组件化的方式来创建流程图,并集成了一些常见功能。

    • jsPlumb 专注于提供强大的连接线功能,具有丰富的自定义选项和功能。

    • Flowchart.js 则相对基础,提供了构建简单流程图的基本功能。




  2. 技术栈和生态系统:



    • Vue-Flow 是基于 Vue.js 的流程图库,与 Vue.js 生态系统无缝集成。

    • Butterfly 是一个基于 TypeScript 的框架,适用于现代 Web 开发。

    • JointJS、AntV G6 和 jsPlumb 可以与多种前端框架(如Vue、React、Angular等)结合使用。

    • AntV G6 是 AntV 团队开发的库,其背后有强大的社区和文档支持。




  3. 文档和学习曲线:



    • Butterfly、G6 和 AntV G6 都有完善的文档和示例,提供了丰富的使用指南和教程。

    • JointJS 和 jsPlumb 也有较好的文档和示例资源,但相对于前三者较少。

    • Flowchart.js 的文档相对较少。




  4. 兼容性:



    • Butterfly、JointJS 和 G6 库在现代浏览器中表现良好,并提供了兼容低版本浏览器

    • 作者:WayneX
      来源:juejin.cn/post/7251835247595110457
      l>

收起阅读 »

为什么选择 Next.js 框架?

web
前言 Next.js 框架作为一种强大而受欢迎的工具,为开发人员提供了许多优势和便利。本文将探讨 Next.js 框架的优点,并解释为什么选择 Next.js 是一个明智的决策。 文档:nextjs.org/docs 强大的服务端渲染和静态生成能力: Ne...
继续阅读 »

前言


Next.js 框架作为一种强大而受欢迎的工具,为开发人员提供了许多优势和便利。本文将探讨 Next.js 框架的优点,并解释为什么选择 Next.js 是一个明智的决策。



文档:nextjs.org/docs



强大的服务端渲染和静态生成能力:


Next.js 框架提供了先进的服务端渲染(SSR)和静态生成(SSG)能力,使得我们能够在服务器上生成动态内容并将其直接发送给客户端,从而大大减少首次加载的等待时间。这样可以提高网站的性能、搜索引擎优化(SEO)以及用户体验。


简化的数据获取:


Next.js 提供了简单易用的数据获取方法,例如 getServerSidePropsgetStaticProps,使得从后端获取数据并将其注入到组件中变得非常容易。这种无缝的数据获取流程,可以让开发人员专注于业务逻辑而不用过多关注数据获取的细节。


优化的路由系统:


Next.js 内置了灵活而强大的路由功能,使得页面之间的导航变得简单直观。通过自动化的路由管理,我们可以轻松地构建复杂的应用程序,并实现更好的用户导航体验。


支持现代前端技术栈:


Next.js 是建立在 React 生态系统之上的,因此可以充分利用 React 的强大功能和丰富的社区资源。同时,Next.js 也支持最新的 JavaScript(ES6+)特性,如箭头函数、模块化导入导出、解构赋值等,让开发人员可以使用最新的前端技术来构建现代化的应用。


简化的部署和扩展:


Next.js 提供了轻松部署和扩展应用程序的工具和解决方案。借助 Vercel、Netlify 等平台,我们可以快速将应用程序部署到生产环境,并享受高性能、弹性扩展的好处。Next.js 还支持构建静态站点,可以轻松地将应用部署到 CDN 上,提供更快的加载速度和更好的全球可访问性。


大型社区支持:


Next.js 拥有庞大的开发者社区,其中有许多优秀的开源项目和库。这意味着你可以从社区中获取到大量的学习资源、文档和支持。无论是在 Stack Overflow 上寻求帮助,还是参与讨论,你都能够从其他开发人员的经验中获益。


什么环境下需要选择nextjs框架?


需要服务端渲染或静态生成:


如果你的应用程序需要在服务器端生成动态内容,并将其直接发送给客户端,以提高性能和搜索引擎优化,那么 Next.js 是一个很好的选择。它提供了强大的服务端渲染和静态生成能力,使得构建高性能的应用变得更加简单。


需要快速开发和部署:


Next.js 提供了简化的开发流程和快速部署的解决方案。它具有自动化的路由管理、数据获取和构建工具,可以提高开发效率。借助 Vercel、Netlify 等平台,你可以轻松地将 Next.js 应用部署到生产环境,享受高性能和弹性扩展的好处。


基于 React 的应用程序:


如果你已经熟悉 React,并且正在构建一个基于 React 的应用程序,那么选择 Next.js 是自然而然的。Next.js 是建立在 React 生态系统之上的,提供了与 React 紧密集成的功能和工具。


需要良好的 SEO 和页面性能:


如果你的应用程序对搜索引擎优化和良好的页面性能有较高的要求,Next.js 可以帮助你实现这些目标。通过服务端渲染和静态生成,Next.js 可以在初始加载时提供完整的 HTML 内容,有利于搜索引擎索引和页面的快速呈现。


需要构建现代化的单页应用(SPA):


尽管 Next.js 可以支持传统的多页面应用(MPA),但它也非常适合构建现代化的单页应用(SPA)。你可以使用 Next.js 的路由系统、数据获取和状态管理功能,构建出功能丰富且响应快速的 SPA。


与nextjs相似的框架?


Nuxt.js:


Nuxt.js 是一个基于 Vue.js 的应用框架,提供了类似于 Next.js 的服务端渲染和静态生成功能。它通过使用 Vue.js 的生态系统,使得构建高性能、可扩展的 Vue.js 应用变得更加简单。


Gatsby:


Gatsby 是一个基于 React 的静态网站生成器,具有类似于 Next.js 的静态生成功能。它使用 GraphQL 来获取数据,并通过预先生成静态页面来提供快速的加载速度和良好的SEO。


Angular Universal:


Angular Universal 是 Angular 框架的一部分,提供了服务端渲染的能力。它可以生成动态的 HTML 内容,从而加快首次加载速度,并提供更好的 SEO 和用户体验。


Sapper:


Sapper 是一个基于 Svelte 的应用框架,支持服务端渲染和静态生成。它提供了简单易用的工具和流畅的开发体验,帮助开发者构建高性能的 Sv

作者:嚣张农民
来源:juejin.cn/post/7251875626906599485
elte 应用程序。

收起阅读 »

为什么你非常不适应 TypeScript

web
前言 在群里看到一些问题和言论:为什么你们这么喜欢“类型体操”?为什么我根本学不下去 TypeScript?我最讨厌那些做类型体操的了;为什么我学了没过多久马上又忘了? 有感于这些问题,我想从最简单的一个角度来切入介绍一下 TypeScript,并向大家介绍并...
继续阅读 »

前言


在群里看到一些问题和言论:为什么你们这么喜欢“类型体操”?为什么我根本学不下去 TypeScript?我最讨厌那些做类型体操的了;为什么我学了没过多久马上又忘了?


有感于这些问题,我想从最简单的一个角度来切入介绍一下 TypeScript,并向大家介绍并不是只要是个类型运算就是体操。并在文中介绍一种基本思想作为你使用类型系统的基本指引。


引子


我将从一个相对简单的 API 的设计过程中阐述关于类型的故事。在这里我们可以假设我们现在是一个工具的开发者,然后我们需要设计一个 API 用于从对象中拿取指定的一些 key 作为一个新的对象返回给外面使用。


垃圾 TypeScript


一个人说:我才不用什么破类型,我写代码就是要没有类型,我就是要随心所欲的写。然后写下了这段代码。


declare function pick(target: any, ...keys: any): any

他的用户默默的写下了这段代码:


pick(undefined, 'a', 1).b

写完运行,发现问题大条了,控制台一堆报错,接口数据也提交不上去了,怎么办呢?


刚学 TypeScript


一个人说:稍微检查一下传入类型就好了,别让人给我乱传参数就行。


declare function pick(target: Record<string, unknown>, ...keys: string[]): unknown

很好,上面的问题便不复存在了,API 也是基本可用的了。但是!当对象复杂的时候,以及字段并不是短单词长度的时候就会发现了一个没解决的问题。


pick({ abcdefghijkl: '123' }, 'abcdefghikjl')

从肉眼角度上,我们很难发现这前后的不一致,所以我们为什么要让调用方的用户自己去 check 自己的字段有没有写对呢?


不就 TypeScript


一个人说:这还不简单,用个泛型加 keyof 不就行了。


declare function pick<
T extends Record<string, unknown>
>(target: T, ...keys: keyof T[]): unknown

我们又进一步解决的上面的问题,但是!还是有着相似的问题,虽然我们不用检查 keys 是不是传入的是一个正确的值了,但是我们实际上对返回的值也存在一个类似的问题。


pick({ abcdefghijkl: '123' }, 'abcdefghijkl').abcdefghikjl



  • 一点小小的拓展


    在这里我们看起来似乎是一个很简单的功能,但实际上蕴含着一个比较重要的信息。


    为什么我们之前的方式都拿不到用户传入进来的类型信息呢?是有原因的,当我们设计的 API 的时候,前面的角度是从,如何校验类型方向进行的思考。


    而这里是尝试去通过约定好的一种规则,通过 TypeScript 的隐式类型推断获得到传入的类型,再通过约定的规则转化出一种新的类型约束来对用户的输入进行限制。




算算 TypeScript


一个人说:好办,算出来一个新的类型就好了。


declare function pick<
T extends Record<string, unknown>,
Keys extends keyof T
>(target: T, ...keys: Keys[]): {
[K in Keys]: T[K]
}

到这里已经是对类型的作用有了基础的了解了,能写出来符合开发者所能接受的类型相对友好的代码了。我们可以再来思考一些更特殊的情况:


// 输入了重复的 key
pick({ a: '' }, 'a', 'a')

完美 TypeScript


到这里,我们便是初步开始了类型“体操”。但是在本篇里,我们不去分析它。


export type L2T<L, LAlias = L, LAlias2 = L> = [L] extends [never]
? []
: L extends infer LItem
? [LItem?, ...L2T<Exclude<LAlias2, LItem>, LAlias>]
: never

declare function pick<
T extends Record<string, unknown>,
Keys extends L2T<keyof T>
>(target: T, ...keys: Keys): Pick<T, Keys[number] & keyof T>

const x0 = pick({ a: '1', b: '2' }, 'a')
console.log(x0.a)
// @ts-expect-error
console.log(x0.b)

const x1 = pick({ a: '1', b: '2' }, 'a', 'a')
// ^^^^^^^^
// TS2345: Argument of type '["a", "a"]' is not assignable to parameter of type '["a"?, "b"?] | ["b"?, "a"?]'.
//   Type '["a", "a"]' is not assignable to type '["a"?, "b"?]'.
//     Type at position 1 in source is not compatible with type at position 1 in target.
//       Type '"a"' is not assignable to type '"b"'.

一个相对来说比较完美的 pick 函数便完成了。


总结


我们再来回到我们的标题吧,从我对大多数人的观察来说,很多的人开始来使用 TypeScript 有几种原因:



  • 看到大佬们都在玩,所以自己也想来“玩”,然后为了过类型校验而去写

  • 看到一些成熟的项目在使用 TypeScript ,想参与贡献,参与过程中为了让类型通过而想办法去解决类型报错

  • 公司整体技术栈采用的是 TypeScript ,要用 TypeScript 进行业务编写,从而为了过类型检查和 review 而去解决类型问题


诸如此类的问题还有很多,我将这种都划分为「为了解决类型检查的问题」而进行的类型编程,这也是大多数人为什么非常不适应 TypeScript,甚至不喜欢他的一个原因。这其实对学习 TypeScript 并不是一个很好的思路,在这里我觉得我们需要站在设计者的角度去对类型系统进行思考。我觉得有以下几个角度:



  • 类型检查到位

  • 类型提示友好

  • 类型检查严格

  • 扩展性十足


我们如果站在这几个角度对我们的 API 进行设计,我们可以发现,开发者能够很轻松的将他们需要的代码编写出来,而尽量不用去翻阅文档,查找 example。


希望通过我的这篇分享,大家能对 TypeScript 多一些理解,并参与到生态中来,守护我们的 JavaScript。




2023/06/27 更新



理性探讨,在评论区说什么屎不是屎的,嘴巴臭可以不说话的。


没谁逼着你一定要写最后一种层次的代码,能力不足可以学啊,不喜欢可以不学啊,能达到倒数第二个就已经很棒啊。


最后一种只是给大家看看 TypeScript 的一种可能,而不是说你应该这么做的。


作者:一介4188
来源:juejin.cn/post/7248599585751515173

收起阅读 »

次世代前端视图框架都在卷啥?

web
上图是 State of JavaScript 2022 前端框架满意度排名。前三名分别是 Solid、Svelte、Qwik。我们可以称他们为次世代前端框架的三大代表,前辈是 React/Angular/Vue。 目前 React/Augular/Vue 还...
继续阅读 »

state of JavaScript 2022 满意度排名


上图是 State of JavaScript 2022 前端框架满意度排名。前三名分别是 SolidSvelteQwik。我们可以称他们为次世代前端框架的三大代表,前辈是 React/Angular/Vue
目前 React/Augular/Vue 还占据的主流的市场地位, 现在我们还不知道下一个五年、十年谁会成为主流,有可能前辈会被后浪拍死在沙滩上, 也有可能你大爷还是你大爷。


就像编程语言一样,尽管每年都有新的语言诞生,但是撼动主流编程语言的地位谈何容易。在企业级项目中,我们的态度会趋于保守,选型会偏向稳定、可靠、生态完善的技术,因此留给新技术的生存空间并不多。除非是革命性的技术,或者有大厂支撑,否则这些技术或框架只会停留小众圈子内。



比如有一点革命性、又有大厂支撑的 Flutter。





那么从更高的角度看,这些次时代的前端视图框架在卷哪些方向呢?有哪些是革命性的呢?


先说一下本文的结论:



  • 整体上视图编程范式已经固化

  • 局部上体验上内卷






视图编程范式固化


从 JQuery 退出历史舞台,再到 React 等占据主流市场。视图的编程范式基本已经稳定下来,不管你在学习什么视图框架,我们接触的概念模型是趋同的,无非是实现的手段、开发体验上各有特色:



  • 数据驱动视图。数据是现代前端框架的核心,视图是数据的映射, View=f(State) 这个公式基本成立。

  • 声明式视图。相较于上一代的 jQuery,现代前端框架使用声明式描述视图的结构,即描述结果而不是描述过程。

  • 组件化视图。组件是现代前端框架的第一公民。组件涉及的概念无非是 props、slots、events、ref、Context…






局部体验内卷


回顾一下 4 年前写的 浅谈 React 性能优化的方向,现在看来依旧不过时,各大框架无非也是围绕着这些「方向」来改善。


当然,在「框架内卷」、「既要又要还要」时代,新的框架要脱颖而出并不容易,它既要服务好开发者(开发体验),又要服务好客户(用户体验) , 性能不再是我们选择框架的首要因素。




以下是笔者总结的,次世代视图框架的内卷方向:



  • 用户体验

    • 性能优化

      • 精细化渲染:这是次世代框架内卷的主要战场,它们的首要目的基本是实现低成本的精细化渲染

        • 预编译方案:代表有 Svelte、Solid

        • 响应式数据:代表有 Svelte、Solid、Vue、Signal(不是框架)

        • 动静分离





    • 并发(Concurrent):React 在这个方向独枳一树。

    • 去 JavaScript:为了获得更好的首屏体验,各大框架开始「抛弃」JavaScript,都在比拼谁能更快到达用户的眼前,并且是完整可交互的形态。



  • 开发体验

    • Typescript 友好:不支持 Typescript 基本就是 ca

    • 开发工具链/构建体验: Vite、Turbopack… 开发的工具链直接决定了开发体验

    • 开发者工具:框架少不了开发者工具,从 Vue Devtools 再到 Nuxt Devtools,酷炫的开发者工具未来可能都是标配

    • 元框架: 毛坯房不再流行,从前到后、大而全的元框架称为新欢,内卷时代我们只应该关注业务本身。代表有 Nextjs、Nuxtjs










精细化渲染






预编译方案


React、Vue 这些以 Virtual DOM 为主的渲染方式,通常只能做到组件级别的精细化渲染。而次世代的 Svelte、Solidjs 不约而同地抛弃了 Virtual DOM,采用静态编译的手段,将「声明式」的视图定义,转译为「命令式」的 DOM 操作


Svelte


<script>
let count = 0

function handleClick() {
count += 1
}
</script>

<button on:click="{handleClick}">Clicked {count} {count === 1 ? 'time' : 'times'}</button>

编译结果:


// ....
function create_fragment(ctx) {
let button
let t0
let t1
let t2
let t3_value = /*count*/ (ctx[0] === 1 ? 'time' : 'times') + ''
let t3
let mounted
let dispose

return {
c() {
button = element('button')
t0 = text('Clicked ')
t1 = text(/*count*/ ctx[0])
t2 = space()
t3 = text(t3_value)
},
m(target, anchor) {
insert(target, button, anchor)
append(button, t0)
append(button, t1)
append(button, t2)
append(button, t3)

if (!mounted) {
dispose = listen(button, 'click', /*handleClick*/ ctx[1])
mounted = true
}
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t1, /*count*/ ctx[0])
if (
dirty & /*count*/ 1 &&
t3_value !== (t3_value = /*count*/ (ctx[0] === 1 ? 'time' : 'times') + '')
)
set_data(t3, t3_value)
},
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(button)
}

mounted = false
dispose()
},
}
}

function instance($$self, $$props, $$invalidate) {
let count = 0

function handleClick() {
$$invalidate(0, (count += 1))
}

return [count, handleClick]
}

class App extends SvelteComponent {
constructor(options) {
super()
init(this, options, instance, create_fragment, safe_not_equal, {})
}
}

export default App

我们看到,简洁的模板最终被转移成了底层 DOM 操作的命令序列。


我写文章比较喜欢比喻,这种场景让我想到,编程语言对内存的操作,DOM 就是浏览器里面的「内存」:



  • Virtual DOM 就是那些那些带 GC 的语言,使用运行时的方案来屏蔽 DOM 的操作细节,这个抽象是有代价的

  • 预编译方案则更像 Rust,没有引入运行时 GC, 使用了一套严格的所有权和对象生命周期管理机制,让编译器帮你转换出安全的内存操作代码。

  • 手动操作 DOM, 就像 C、C++ 这类底层语言,需要开发者手动管理内存


使用 Svelte/SolidJS 这些方案,可以做到修改某个数据,精细定位并修改 DOM 节点,犹如我们当年手动操作 DOM 这么精细。而 Virtual DOM 方案,只能到组件这一层级,除非你的组件粒度非常细。








响应式数据


和精细化渲染脱不开身的还有响应式数据


React 一直被诟病的一点是当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树,如果要避免不必要的子组件的重渲染,需要开发者手动进行优化(比如 shouldComponentUpdatePureComponentmemouseMemo/useCallback)  。同时你可能会需要使用不可变的数据结构来使得你的组件更容易被优化。


在 Vue 应用中,组件的依赖是在渲染过程中自动追踪的,所以系统能精确知晓哪个组件确实需要被重渲染。


近期比较火热的 signal (信号,Angular、Preact、Qwik、Solid 等框架都引入了该概念),如果读者是 Vue 或者 MobX 之类的用户, Signal 并不是新的概念。


按 Vue 官方文档的话说:从根本上说,信号是与 Vue 中的 ref 相同的响应性基础类型。它是一个在访问时跟踪依赖、在变更时触发副作用的值容器。


不管怎样,响应式数据不过是观察者模式的一种实现。相比 React 主导的通过不可变数据的比对来标记重新渲染的范围,响应式数据可以实现更细粒度的绑定;而且响应式的另一项优势是它的可传递性(有些地方称为 Props 下钻(Props Drilling))。






动静分离


Vue 3 就是动静结合的典型代表。在我看来 Vue 深谙中庸之道,在它身上我们很难找出短板。


Vue 的模板是需要静态编译的,这使得它可以像 Svelte 等框架一样,有较大的优化空间;同时保留了 Virtual DOM 和运行时 Reactivity,让它兼顾了灵活和普适性。


基于静态的模板,Vue 3 做了很多优化,笔者将它总结为动静分离吧。比如静态提升、更新类型标记、树结构打平,无非都是将模板中的静态部分和动态部分作一些分离,避免一些无意义的更新操作。


更长远的看,受 SolidJS 的启发, Vue 未来可能也会退出 Vapor 模式,不依赖 Virtual DOM 来实现更加精细的渲染。








再谈编译时和运行时


编译时和运行时没有优劣之分, 也不能说纯编译的方案就必定是未来的趋势。


这几年除了新的编译时的方案冒出来,宣传自己是未来;也有从编译时的焦油坑里爬出来, 转到运行时方案的,这里面的典型代表就是 Taro。


Taro 2.0 之前采用的是静态编译的方案,即将 ’React‘ 组件转译为小程序原生的代码:


Untitled


但是这个转译工作量非常庞大,JSX 的写法千变万化,非常灵活。Taro 只能采用 穷举 的方式对 JSX 可能的写法进行了一 一适配,这一部分工作量很大,实际上 Taro 有大量的 Commit 都是为了更完善的支持 JSX 的各种写法。这也是 Taro 官方放弃这种架构的原因。


也就是说 Taro 也只能覆盖我们常见的 JSX 用法,而且我们必须严格遵循 Taro 规范才能正常通过。


有非常多的局限:



  • 静态的 JSX

  • 不支持高阶组件

  • 不支持动态组件

  • 不支持操作 JSX 的结果

  • 不支持 render function

  • 不能重新导出组件

  • 需要遵循 on*、render* 约束

  • 不支持 Context、Fragment、props 展开、forwardRef

  • ….


有太多太多的约束,这已经不是带着镣铐跳舞了,是被五花大绑了。




使用编译的方案不可避免的和实际运行的代码有较大的 Gap,源码和实际运行的代码存在较大的差别会导致什么?



  • 比较差的 Debug 体验。

  • 比较黑盒。


我们在歌颂编译式的方案,能给我们带来多大的性能提升、带来多么简洁的语法的同时。另一方面,一旦我们进行调试/优化,我们不得不跨越这层 Gap,去了解它转换的逻辑和底层实现。


这是一件挺矛盾的事情,当我们「精通」这些框架的时候,估计我们已经是一个人肉编译器了。


Taro 2.x 配合小程序, 这对卧龙凤雏, 可以将整个开发体验拉到地平线以下。




回到这些『次世代』框架。React/Vue/Angular 这些框架先入为主, 在它们的教育下,我们对前端视图开发的概念和编程范式的认知已经固化。


Untitled


比如在笔者看来 Svelte 是违法直觉的。因为 JavaScript 本身并不支持这种语义。Svelte 要支持这种语义需要一个编译器,而作为一个 JavaScript 开发者,我也需要进行心智上的转换。


而 SolidJS 则好很多,目之所及都是我们熟知的东西。尽管编译后可能是一个完全不一样的东西。



💡 Vue 曾经也过一个名为**响应性语法糖的实验性功能来探索这个方向,但最后由于这个原因**,废弃了。这是一次明智的决定



当然,年轻的次世代的前端开发者可能不这么认为,他们毕竟没有经过旧世代框架的先入为主和洗礼,他们更能接受新的开发范式,然后扛起这些旗帜,让它们成为未来主流。


总结。纯编译的方能可以带来更简洁的语法、更多性能优化的空间,甚至也可以隐藏一些跨平台/兼容性的细节。另一方面,源码和实际编译结果之间的 Gap,可能会逼迫开发者成为人肉编译器,尤其在复杂的场景,对开发者的心智负担可能是翻倍的。


对于框架开发者来说,纯编译的方案实现复杂度会更高,这也意味着,会有较高贡献门槛,间接也会影响生态。








去 JavaScript


除了精细化渲染,Web 应用的首屏体验也是框架内卷的重要方向,这个主要的发展脉络,笔者在 现代前端框架的渲染模式 一文已经详细介绍,推荐大家读一下:


Untitled


这个方向的强有力的代表主要有 Astro(Island Architecture 岛屿架构)、Next.js(React Server Component)、Qwik(Resumable 去 Hydration)。


这些框架基本都是秉承 SSR 优先,在首屏的场景,JavaScript 是「有害」的,为了尽量更少地向浏览器传递 JavaScript,他们绞尽脑汁 :



  • Astro:’静态 HTML‘优先,如果想要 SPA 一样实现复杂的交互,可以申请开启一个岛屿,这个岛屿支持在客户端进行水合和渲染。你可以把岛屿想象成一个 iframe 一样的玩意。

  • React Server Component: 划分服务端组件和客户端组件,服务端组件仅在服务端运行,客户端只会看到它的渲染结果,JavaScript 执行代码自然也仅存于服务端。

  • Qwik:我要直接革了水合(Hydration)的命,我不需要水合,需要交互的时候,我惰性从服务端拉取事件处理器不就可以了…


不得不说,「去 JavaScript」的各种脑洞要有意思多了。






总结


本文主要讲了次世代前端框架的内卷方向,目前来看还处于量变的阶段,并没有脱离现在主流框架的心智模型,因此我们上手起来基本不会有障碍。


作为普通开发者,我们可以站在更高的角度去审视这些框架的发展,避免随波逐流和无意义的内卷。






扩展阅读



作者:荒山
来源:juejin.cn/post/7251763342954512440
收起阅读 »

为了娃的暑期课,老父亲竟然用上了阿里云高大上的 Serverless FaaS!!!

web
起因 事件的起因是,最近家里的俩娃马上要放暑假了,家里的老母亲早早的就规划好了姐姐弟弟的暑期少年宫课程,奈何有些想上个课程一直没有”抢“到课程。平时带娃在少年宫上课的父母可能懂的,一般少年宫的课程都是提前预报名,然后会为了公平起见进行摇号,中者缴费。本来是一件...
继续阅读 »

起因


事件的起因是,最近家里的俩娃马上要放暑假了,家里的老母亲早早的就规划好了姐姐弟弟的暑期少年宫课程,奈何有些想上个课程一直没有”抢“到课程。平时带娃在少年宫上课的父母可能懂的,一般少年宫的课程都是提前预报名,然后会为了公平起见进行摇号,中者缴费。本来是一件比较合理的处理方式,但奈何不住各位鸡娃的父母们的上有政策下有对策的路子。



第一阶段:靠数量提高命中率 ,大家都各自报了很多不同课程,防止因为摇号没摇上,导致落空。我们家也是一样操作~~~。但是这里也会出现另一种状况,当摇号结束,大家缴费期间,有的摇中家长,发现课程多了或者有些课程和课外兴趣班冲突,或者种种其他原因,不想再上暑期课程了,就会取消这门课程。 即时你缴费了,后面也是可以取消的,只是会扣除一些费用。
第二阶段:捡漏,有报多的家长,就有没有抢到合适课程的家长。没错,说的正是我们家 哈哈。在我老婆规划中,我们还有几门课程没有摇中,那这个时候怎么办呢?只能蹲守,人工不定时的登录查课,寄期望于有些家长退课了,我们好第一时间补上去。


当当当,作为一个程序员老父亲,这个时候终于排上用场了~~~,花了一个晚上,写了个定时查询脚本+通知,当有课放出,咱们就通知一下领导(老婆大人)定夺,话说这个小查课定时任务深受领导的高度表扬。
好了起因就是这样,下面我们回到正题,给大家实操下如何使用阿里云的Serverless 函数,来构建这个小定时脚本。


架构


很简单的架构图,只用到了这么几个组件,



  • Serverless FC 无服务器函数,承载逻辑主体

  • OSS 存储中间结果数据

  • RAM是对计算函数赋予角色使其有对应权限。

  • 企业微信机器人,企业微信本身可以随便注册,拉个企业微信群,加入一个群机器人,就可以作为消息触达端。



实践


函数计算FC



本次实操中,我们需要先了解阿里云的函数计算FC几个概念,方便我们后面操作理解:



相关官方资料:基本概念

下面我只列了本次操作涉及到的概念,更详细资料,建议参考官方文档。




  • 服务:服务是函数计算资源管理的单位,是符合微服务理念的概念。从业务场景出发,一个应用可以拆分为多个服务。从资源使用维度出发,一个服务可以由多个函数组成。

  • FC函数:函数计算的资源调度与运行是以函数为单位。FC函数由函数代码和函数配置构成。FC函数 必须从属于服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。函数的相关操作

  • 层:层可以为您提供自定义的公共依赖库、运行时环境及函数扩展等发布与部署能力。您可以将函数依赖的公共库提炼到层,以减少部署、更新时的代码包体积,也可以将自定义的运行时,以层部署在多个函数间共享。

  • 触发器:触发器是触发函数执行的方式。在事件驱动的计算模型中,事件源是事件的生产者,函数是事件的处理者,而触发器提供了一种集中、统一的方式来管理不同的事件源


创建函数



  1. 函数计算FC--> 任务--> 选择创建函数




  1. 配置函数


这里我截了个长屏,来给大家逐个解释



tips: 如果大家也有截长屏需求,推荐chrome 中的插件:Take Webpage Screenshots Entirely - FireShot




  • 函数方式:我的小脚本是python 代码,我直接使用自定义运行环境,如果你想了解这三种方式区别,建议详细阅读这篇文章:函数计算支持的多语言运行时信息

  • 服务名称:我们如果初次创建,选择创建一个服务,然后填入自己设定的服务名字即可

  • 函数代码:这里我选择运行时python 3.9 , 示例代码(代码等我们创建完成之后,再填充自己的代码逻辑)

  • 高级配置: 这里如果是初学者,个人建议尽量选最小配置,因为函数计算是按你使用的资源*次数 收费的, 这里我改成了资源粒度,0.05vCpu 128MB,并发度 1

  • 函数变量:我暂时不需要,就没有设置,如果你需要外部配置一些账号密码,可以使用这种方式来配置

  • 触发器:这里展示出了函数计算的协同作用,可以通过多种云服务产品来进行事件通知触发,我们这里的样例只需要一个定时轮询调度,所以这里我使用了定时触发器,5分钟调用一次。



配置依赖



函数整体创建成功之后,点击函数名称,进入函数详情页



函数代码模块填充本地已经调试好的代码, 测试函数,发现相关依赖并没有,这里我们需要编辑层,来将python相关依赖文件引入, 点击上图中编辑层



我选择的是在线构建依赖层,按照requirements.txt的格式书写,然后就可以在线安装了,很方便。创建成功之后,回到编辑层位置,选择刚开始创建的层,点击确定,既可,这样就不会再报相关依赖缺失了。


配置OSS映射


我的小脚本里,需要存储中间数据,因为函数计算FC本身是无状态的,所以需要借助外部存储,很自然的就会想到用OSS来存储。但是如何正确的使用OSS桶来存储中间数据呢?
官方关于python操作OSS的教程:python 操作 OSS



# -*- coding: utf-8 -*-
import oss2
# 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
auth = oss2.Auth('<yourAccessKeyId>', '<yourAccessKeySecret>')
# Endpoint以杭州为例,其它Region请按实际情况填写。
bucket = oss2.Bucket(auth, 'http://oss-cn-hangzhou.aliyuncs.com', '<yourBucketName>')

这里操作基本都会使用到 AK,SK。 但是基于云上的安全的实践操作以及要定期更换ak,sk来保证安全,尽量不要直接在代码中使用ak, sk来调用api 。那是否有其他更合理的操作方式?
我找到了这篇文章 配置OSS文件系统



函数计算支持与OSS无缝集成。您可以为函数计算的服务配置OSS挂载,配置成功后,该服务下的函数可以像使用本地文件系统一样使用OSS存储服务。



个人推荐这种解决方案



  • 只需要配置对应函数所授权的角色策略中,加上对相应的挂载OSS桶的读写权限

  • 这个操作符合最小粒度的赋予权限,同时也减少代码开发量,python可以像操作本地磁盘一样,操作oss,简直不要太方便~~~

  • 同时也不需要担心所谓的ak sk泄漏风险以及需要定期更换密钥的麻烦,因为就不存在使用ak sk


我最后也是用这种方式,配置了oss文件系统映射到函数运行时的环境磁盘上。


企业微信机器人


企业微信可以直接注册,不需要任何费用,之后两个人拉一个群,添加一个群机器人即可。
可以参考官方文档:如何使用群机器人 来用python 发送群消息,很简单的一段代码既可完成发送消息通知。


wx_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx-xxxxx-xxxxx"
def sendWechatBotMsg(msg,collagues):
"""艾特指定同事,并发送指定信息"""
data = json.dumps({"msgtype": "text", "text": {"content": msg, "mentioned_list":collagues}})
r = requests.post(wx_url, data, auth=('Content-Type', 'application/json'))

最后效果如图所示:



总结


通过日常生活中的一个小场景,实践了下阿里云的高大上的Serverless FC 服务。个人选择这种无服务器函数计算,也是结合了成本的因素。
给大家对比下两种方案价格:



  • 传统云主机方式:


阿里云官方ECS主机的定价:实例价格信息
最便宜的一档: 1vCPU 1GB 实例, 每个月也要34.2 RMB 还没有包括挂载的磁盘价格 ,以及公网带宽费用




  • Serverless FC


而使用无服务器函数计算服务, 按使用时长和资源计费,像我这种最小资源粒度就可以满足同时调度次数是周期性的,大大消减了费用, 我跑了大概一周的时间 大概花费了 0.16 RMB,哈哈 简直是不能再便宜了。大家感兴趣的也可以动手实践下自己的需求场景。




云计算已经是当下技术人员的必学的一门课程,如果有时间也鼓励大家可以多了解学习,提升自己的专业能力。感兴趣的朋友,如果有任何问题,需要沟通交流也可以添加我的个人微信 coder_wukong,备注:云计算,或者关注我的公众号 WuKongCoder日常也会不定期写一些文章和思考。




如果觉得文章不错,欢迎大家点赞,留言,转发,收藏 谢谢大家,我们下篇文章再会~~~



参考资料


中国唯一入选 Forrester 领导者象限,阿里云 Serverless 产品能力全球第一

函数计算支持的多语言运行时信息

阿里云OSS文档:python 操作 OSS

阿里云函数计算文档:配置OSS文件系统

企业微信文档:如何使用群机器人

让 Serverless 更普惠

Serverless 在阿里云函数计算中的实践


作者:WuKongCoder
来源:juejin.cn/post/7251786717652107301
收起阅读 »

你还在用传统轮播组件吗?来看看遮罩轮播组件

web
背景 最近有一个页面改版的需求,在UI走查阶段,设计师说原来的轮播组件和新版页面UI整体风格不搭,所以要换掉。 这里就涉及到两种轮播组件,一种是传统的轮播组件,一种是设计师要的那种。 传统的轮播组件,大家都见过,原理也清楚,就是把要轮播的图片横向排成一个队列,...
继续阅读 »

背景


最近有一个页面改版的需求,在UI走查阶段,设计师说原来的轮播组件和新版页面UI整体风格不搭,所以要换掉。


这里就涉及到两种轮播组件,一种是传统的轮播组件,一种是设计师要的那种。


传统的轮播组件,大家都见过,原理也清楚,就是把要轮播的图片横向排成一个队列,把他们当成一个整体,每次轮换,其实是把这个队列整体往左平移X像素,这里的X通常就是一个图片的宽度。
这种效果可以参见vant组件库里的swipe组件


而我们设计师要的轮播效果是另外一种,因为我利用端午假期已经做好了一个雏形,所以大家可以直接看Demo


当然你也可以直接打开 腾讯视频APP 首页,顶部的轮播,就是我们设计师要的效果。


需求分析


新式轮播,涉及要两个知识点:



  • 图片层叠

  • 揭开效果


与传统轮播效果一个最明显的不同是,新的轮播效果需要把N张待轮播的图片在Z轴上重叠放置,每次揭开其中的一张,下一张是自然漏出来的。这里的实现方式也有多种,但最先想到的还是用zindex的方案。


第二个问题是如何实现揭开的效果。这里就要使用到css3的新属性mask。
mask是一系列css的简化属性。包括mask-image, mask-position等。
因为mask的系列属性还有一定的兼容性,所以一部分浏览器需要带上-webkit-前缀才能生效。


还有少数浏览器不支持mask属性,退化的情况是轮播必须有效,但是没有轮换的动效。


实现


有了以上的分析,就可以把效果做出来了。核心代码如下:


<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
// 定义属性
const props = defineProps([
'imgList',
'duration',
'transitionDuration',
'maskPositionFrom',
'maskPositionTo',
'maskImageUrl'
]);
// 定义响应式变量
const currentIndex = ref(0);
const oldCurrentIndex = ref(0);
const imgList = ref([...props.imgList, props.imgList[0]]);
const getInitZindex = () => {
const arr = [1];
for (let i = imgList.value.length - 1; i >= 1; i--) {
arr.unshift(arr[0] + 1);
}
return arr;
}
const zIndexArr = ref([...getInitZindex()]);
const maskPosition = ref(props.maskPositionFrom || 'left');
const transition = ref(`all ${props.transitionDuration || 1}s`);
// 设置动画参数
const transitionDuration = props.transitionDuration || 1000;
const duration = props.duration || 3000;

// 监听currentIndex变化
watch(currentIndex, () => {
if (currentIndex.value === 0) {
zIndexArr.value = [...getInitZindex()];
}
maskPosition.value = props.maskPositionFrom || 'left';
transition.value = 'none';
})
// 执行动画
const execAnimation = () => {
transition.value = `all ${props.transitionDuration || 1}s`;
maskPosition.value = props.maskPositionFrom || 'left';
maskPosition.value = props.maskPositionTo || 'right';
oldCurrentIndex.value = (currentIndex.value + 1) % (imgList.value.length - 1);
setTimeout(() => {
zIndexArr.value[currentIndex.value] = 1;
currentIndex.value = (currentIndex.value + 1) % (imgList.value.length - 1);
}, 1000)
}
// 挂载时执行动画
onMounted(() => {
const firstDelay = duration - transitionDuration;
function animate() {
execAnimation();
setTimeout(animate, duration);
}
setTimeout(animate, firstDelay);
})
</script>
<template>
<div class="fly-swipe-container">
<div class="swipe-item"
:class="{'swipe-item-mask': index === currentIndex}"
v-for="(url, index) in imgList"
:key="index"
:style="{ zIndex: zIndexArr[index],
'transition': index === currentIndex ? transition : 'none',
'mask-image': index === currentIndex ? `url(${maskImageUrl})` : '',
'-webkit-mask-image': index === currentIndex ? `url(${maskImageUrl})`: '',
'mask-position': index === currentIndex ? maskPosition: '',
'-webkit-mask-position': index === currentIndex ? maskPosition: '' }"
>

<img :src="url" alt="">
</div>
<div class="fly-indicator">
<div class="fly-indicator-item"
:class="{'fly-indicator-item-active': index === oldCurrentIndex}"
v-for="(_, index) in imgList.slice(0, imgList.length - 1)"
:key="index">
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.fly-swipe-container {
position: relative;
overflow: hidden;
width: 100%;
height: inherit;
.swipe-item:first-child {
position: relative;
}
.swipe-item {
position: absolute;
width: 100%;
top: 0;
left: 0;
img {
display: block;
width: 100%;
object-fit: cover;
}
}
.swipe-item-mask {
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: cover;
-webkit-mask-size: cover;
}
.fly-indicator {
display: flex;
justify-content: center;
align-items: center;
z-index: 666;
position: relative;
top: -20px;
.fly-indicator-item {
margin: 0 5px;
width: 10px;
height: 10px;
border-radius: 50%;
background: gray;
}
.fly-indicator-item-active {
background: #fff;
}
}
}
</style>

这是一个使用 Vue 3 构建的图片轮播组件。在这个组件中,我们可以通过传入一组图片列表、切换动画的持续时间、过渡动画的持续时间、遮罩层的起始位置、遮罩层的结束位置以及遮罩层的图片 URL 来自定义轮播效果。


组件首先通过 defineProps 定义了一系列的属性,并使用 ref 创建了一些响应式变量,如 currentIndexoldCurrentIndeximgListzIndexArr 等。


onMounted 钩子函数中,我们设置了一个定时器,用于每隔一段时间执行一次轮播动画。
在模板部分,我们使用了一个 v-for 指令来遍历图片列表,并根据当前图片的索引值为每个图片元素设置相应的样式。同时,我们还为每个图片元素添加了遮罩层,以实现轮播动画的效果。


在样式部分,我们定义了一些基本的样式,如轮播容器的大小、图片元素的位置等。此外,我们还为遮罩层设置了一些样式,包括遮罩图片的 URL、遮罩层的位置等。


总之,这是一个功能丰富的图片轮播组件,可以根据传入的参数自定义轮播效果。


后续


因为mask可以做的效果还有很多,后续该组件可以封装更多轮播效果,比如从多个方向的揭开效果,各种渐变方式揭开效果。欢迎使用和提建议。


仓库地址:github.com/cunzai

zhuyi…

收起阅读 »

你们公司的官网被搜索引擎收录了吗?

web
前言 前段时间,我司的官网要改版。老板们手一挥,提出了以下几点需求 网站要大气,炫酷,有科技感 图片文字要高大上 注重SEA、SEO优化,用户查找关键字后,我们公司的网站排名要显示在前列 为此,我们还专门买了一个SEO优化的课程,大张旗鼓的学习了一通。至于...
继续阅读 »

1.jpg


前言


前段时间,我司的官网要改版。老板们手一挥,提出了以下几点需求



  • 网站要大气,炫酷,有科技感

  • 图片文字要高大上

  • 注重SEA、SEO优化,用户查找关键字后,我们公司的网站排名要显示在前列


为此,我们还专门买了一个SEO优化的课程,大张旗鼓的学习了一通。至于效果如何,1个月见分晓


那么如何编写 JavaScript 代码以有利于 SEO 和 SEA 呢?


下面仅展示被被谷歌搜索引擎收录的


SEA、SEO优化


保持好的网页结构



  1. 使用语义化的 HTML结构


HTML语义化是指使用恰当的HTML标签来描述网页内容的结构和含义,以提高网页的可读性、可访问性和搜索引擎优化。



  • header: 网站的页眉部分

  • nav: 定义网站的主要导航链接

  • main: 定义页面的主要内容区域,每个页面应该只有一个<main>标签

  • section: 定义页面中的独立区块, 例如文章、产品列表等

  • article: 定义独立的文章内容,通常包含标题、作者、发布日期等信息

  • aside: 定义页面的侧边栏或附属信息区域

  • footer: 网站的页脚部分


<header>
<h1>官网</h1>
<nav>
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">关于我们</a></li>
<li><a href="#">联系我们</a></li>
</ul>
</nav>
</header>

<main>
<div>欢迎来到我们的网站</div>
<p>这里是网站的主要内容。</p>
</main>


<section>
<h2>最新文章</h2>
<article>
<h3>文章标题</h3>
<p>文章内容...</p>
</article>
<article>
...
</article>
</section>


<aside>
<h3>最新消息</h3>
<ul>
<li><a href="#">链接1</a></li>
...
</ul>
</aside>

<article>
<h2>消息1</h2>
<p>文章内容...</p>
</article>


<footer>
<p>版权所有 &copy; 2023</p>
<p>联系我们:info@example.com</p>
</footer>



  1. 提供准确且吸引人的页面标题和描述


准确且简洁的标题和描述,有利于吸引访问者和搜索引擎的注意



  • 页面标题: Title

  • 页面描述: Meta Description


<head>
<title>精美手工艺品——手工制作的独特艺术品</title>
<meta name="description" content="我们提供精美手工艺品的设计与制作,包括陶瓷、木雕、织物等。每件艺术品都是独一无二的,以精湛的工艺和创造力打动您的心灵。欢迎浏览我们的作品集。">
</head>


标题要小于50个字符,描述要小于150字符





  1. 在关键位置使用关键字: 包括标题、段落文本、链接文本和图片的 alt 属性。



    • 段落文本: 自然的使用关键字,有助于搜索引擎收录

    • 链接文本: 使用描述性的链接文本,并在其中包含关键字,这有助于搜索引擎理解链接指向的内容

    • 图片的 alt 属性: 对于每个图像,使用描述性的 alt 属性来说明图像内容,并在其中包含关键字。这不仅有助于视力障碍用户理解图像,还可以提供关键字相关的图像描述给搜索引擎。




<h1>欢迎来到精美手工艺品网店</h1>
<p>我们提供各种精美手工艺品,包括陶瓷、木雕、织物等。每个艺术品都是由我们经验丰富的工匠手工制作而成,展现了精湛的工艺和创造力。</p>
<p>浏览我们的<a href="/products" title="手工艺品产品列表">产品列表</a>,您将发现独特的艺术品,适合作为礼物或收藏。</p>
<img src="product.jpg" alt="陶瓷花瓶 - 手工制作的精美艺术品" />


一个页面要保证有且只有h1标签



使用友好的 URL 结构


使用友好的URL结构是一个重要的优化策略,它可以提升网站的可读性、可维护性和用户体验



  • 使用关键字: 在URL中使用关键字,以便用户和搜索引擎可以更好地理解页面的主题和内容, URL中多个关键词使用连字符字符 "-"进行分隔。

  • 结构层次化: 层次化的URL结构来反映内容的结构和关系

  • 避免使用参数: 尽量避免在URL中使用过多的参数,特别是使用随机字符串或数字作为参数

  • 尽量使用永久链接: 尽可能使用永久链接,避免频繁更改URL

  • 尽量保持URL简洁: 避免过长的URL。短连接更易于分享和记忆


<!-- 不友好的URL -->
https://example.com/index.html?category=7&product=12345
https://example.com/qinghua/porcelain

<!-- 友好的URL -->
https://example.com/porcelain/qinghua
https://example.com/blog/friendly-urls


  1. 重要链接不要用JS


搜索引擎爬虫通常不会执行 JavaScript,并且依赖 JavaScript 的链接可能无法被爬虫正确解析和索引



使用标准的 <a> 标签进行跳转,避免使用 JavaScript 跳转




  1. 使用W3C规范


使用W3C规范是确保你的网页符合Web标准并具有良好可访问性的重要方式


不符合W3C的规范:



  • 未闭合的标签

  • 未正确嵌套的元素

  • 行内元素包裹块状元素


<!-- 未闭合的标签 -->
<p>This is a paragraph with no closing tag.
<!-- 未正确嵌套的元素 -->
<div><p>This paragraph is inside a div but not closed properly.</div></p>
<!-- 行内元素包裹块状元素 -->
<span><p>This paragraph is inside a div but not closed properly.</p></span>

响应式设计和移动优化


Google 现在使用了移动优先索引, 搜索引擎更倾向于优先索引和显示移动友好的网页


使用响应式设计,使你的网页在各种设备上都能正确显示。



  1. 响应式设计:确保网页具有响应式设计,能够适应不同设备的屏幕尺寸

  2. 关注移动友好性:确保网页在移动设备上加载和显示良好


JavaScript使用和加载优化


搜索引擎爬虫通常不会执行 JavaScript,并且在抓取和索引页面时可能会忽略其中的动态内容




  1. 加载时间优化: 通过压缩和合并 JavaScript文件,减小文件大小,以及使用异步加载和延迟加载的方式,可以提高网页的加载速度




  2. 避免使用AJAX技术加载核心内容: 对于核心内容,避免使用 AJAX 或动态加载方式,而是在初始页面加载时就呈现。这样可以确保搜索引擎能够正确抓取和索引核心内容,提高网页的可见性和相关性。




  3. 减少懒加载、瀑布流、上拉刷新、下载加载、点击更多等互动加载: 这些常见的页面优化方式虽然有利于用户体验。但搜索引擎爬虫不会执行 JavaScript,并且在抓取和索引页面时可能会忽略其中的动态内容。




  4. js阻塞后保证页面正常运行: 确保网站在没有 JavaScript 的情况下仍然能够正常运行。这有助于搜索引擎爬虫能够正确索引你的网页内容。




性能和体验优化



  1. 提高网站加载速度: 搜索引擎和用户都更喜欢快速加载的网页,提高页面的转加载速度,会对搜索引擎排名产生积极影响。

  2. 优化移动体验: 在移动设备上,用户的粘性和耐心被放大,优化移动体验,减少用户的流失率,会对移动搜索排名产生积极影响。

  3. 无障碍: 在 Web 开发无障碍性意味着使尽可能多的人能够使用 Web 站点, 增加用户人群的受众,会提高搜索引擎排名


内容更新



  1. 内容持续更新: 搜索引擎比较喜欢新鲜的内容,如果网站内容长期不更新的话,搜索引擎就会厌烦我们的网站。反之,我们频繁的更新新闻、博客等内容,会大大的提高

  2. 网页数量尽可能的多: 尽可能的让网页超过15个,



频繁修改或调整网站结构的话就相当于修改了搜索引擎爬取网站的路径,导致网站即使更新再多的内容也难以得到收录



监测


索引


在浏览器中输入 site:你的地址(此方法仅适合谷歌,百度则直接搜索URL地址)


查看是否被索引



  1. 进入Google Search Console

  2. 进入URL检测工具。

  3. 将需要索引的URL粘贴到搜索框中。

  4. 等待谷歌检测URL。

  5. 点击“请求编入索引”按钮。


image.png


收录


点击网址检查: 如果页面被索引,那么会显示“URL is on Google(URL在谷歌中)”。


image.png


如何去收录


image.png


但是,请求编入收录索引不太可能解决旧页面的索引问题,并且这只是一个最原始的方式,提交链接不能确保你的URL一定被收录,尤其是百度。


参考11个让百度快速收录网站的奇思淫技


总结


持续的优化和监测是关键,以确保你的策略和实践符合不断变化的搜索引擎算法和用户需求。


期待一个月后见分晓啦!


参考文献



  1. 11个让百度快速收录网站的奇思淫技

  2. search

  3. JavaScript与SEO之间的藕断丝连关系<
    作者:高志小鹏鹏
    来源:juejin.cn/post/7251786985535275067
    /a>

收起阅读 »

一次微前端的改造记录

web
前言 由于公司的一些需求,需要去了解 iframe 和 qiankun 两种微前端方案,特此记录一下。 微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独...
继续阅读 »

前言


由于公司的一些需求,需要去了解 iframe 和 qiankun 两种微前端方案,特此记录一下。


微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。


iframe


HTML 内联框架元素,能够将另一个 HTML 页面嵌入到当前页面


<iframe src="文件路径"></iframe>

postMessage




  • window.postMessage() 方法可以安全地实现跨源通信:postMessage 讲解




  • 通常来说,对于两个不同页面的脚本,只有当他们页面位于具有相同的协议(通常为 https),端口号(443 为 https 的默认值),以及主机时,这两个脚本才能互相通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全


    otherWindow.postMessage(message, targetOrigin, [transfer]);



  • postMessage 的兼容性




实现思路


整体架构


以我们公司的真实项目为例:


1687167610369.png


一个父站点,很多子站点,不同的子站点即为完全独立的不同的项目
父站点包括:



  1. 公共部分:header 部分、左侧菜单部分都是公共的组件

  2. 主区域部分(子站点区域):需要展示不同子站点的业务部分


父子站点通信(如何展示不同的站点页面)


上面已经介绍过 iframe 和 postMessage:我们通过 iframe 去加载不同项目的线上地址



  1. 我们新建一个通用组件,无论菜单路由指向何处都指向这个组件,渲染这个组件

  2. 在这个组件中监听路由的变化,返回不同的线上地址,让 iframe 去加载对应的内容(公司项目比较老,还是使用的 vue2)


<template>
<div class="container">
<iframe :src="src"></iframe>
</div>
</template>

<script>
export default {
data() {
return {
src: '',
};
},
mounted() {
this.updateIframe(); // 生命周期加载一次,否则页面空白,第一次监听不到
},
watch: {
$route() {
this.updateIframe();
},
},
methods: {
updateIframe() {
// 更新 src
},
},
};
</script>


  1. 菜单以及子站点线上地址怎么来:目前我们的做法是单独的菜单配置,通过接口去拿,配置菜单的时候同事配置好 iframe 线上地址,这样就可以一起拿到了
    image.png

  2. 那我们究竟如何通信呢?
    父站点(补充上面的代码):


<template>
<div class="container">
<iframe :src="src" id="iframe"></iframe>
</div>
</template>
<script>
export default {
data() {
return {
src: '',
};
},
mounted() {
this.getMessage();
this.updateIframe(); // 生命周期加载一次,否则页面空白,第一次监听不到,同样的也可以使用 iframe 的 onload 方法
},
watch: {
$route() {
this.updateIframe();
},
},
methods: {
updateIframe() {
// 更新 src
},
messageCallBack(data) {
// 这边可以接收一些子站点的数据,去做一些逻辑判断,比如要在iframe加载完之后,父站点再去发消息给子站点,不然肯定存在问题
// 可以传递一些信息给子站点
this.postMessage({
data: {},
});
},
postMessage(data) {
document.getElementById('iframe').contentWindow.postMessage(JSON.stringify(data), '*');
},
getMessage() {
window.addEventListener('message', this.messageCallBack);
},
},
};
</script>


  1. 子站点也一样在需要的地方通过 postMessage 去发送或者接受数据(比如我们子站点每次都加载首页,然后接收到路由信息,再在子项目中跳转到对应页面)


需要干掉 iframe 滚动条吗


当然需要,不然多丑,加入以下代码即可去掉:


#app {
-ms-overflow-style: none; /* IE 和 Edge 浏览器隐藏滚动条 */
scrollbar-width: none; /* FireFox隐藏浏览器滚动条 */
}
/* Chrome浏览器隐藏滚动条 */
#app::-webkit-scrollbar {
display: none;
}

弹窗是否能覆盖整个屏幕


UI 不同步,DOM 结构不共享。 iframe 里来一个带遮罩层的弹框,只会在 iframe 区域内,为了好看,我们需要让它在整个屏幕的中间


解决:



  • 使得 iframe 区域的宽高本身就和屏幕宽高相等,子站点内部添加 padding ,使内容区缩小到原本子站点 content 区域;

  • 正常情况下,父站点 header、左侧菜单部分的层级需要高于 iframe 的层级(iframe 不会阻止这些区域的点击);

  • 当用户点了新建按钮,对话框出现的时候,给父项目发送一条消息,让父项目调高 iframe 的层级,遮罩便可以覆盖全屏。


这样的解决的缺点:每次打开弹窗,都得先发送 postMessage 数据,逻辑显得多余,对于新手不友好;可是为了好看,只能这样了。


iframe 方案总结


好用的地方:



  • 业务解耦

  • 技术隔离,vue、react 互不影响

  • 项目拆分,上线快速,对其他项目无影响

  • iframe 的硬隔离使得各个项目的 JS 和 CSS 完全独立,不会产生样式污染和变量冲突


存在的缺点:



  • 布局约束:不给定高度,会塌陷;iframe 内的 div 无法全屏(iframe 标签设置 allow="fullscreen" 属性即可)

  • 不利于 seo,会当成 2 个页面,破坏了语义化 ,对无障碍可访问性支持不好

  • url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用

  • 性能开销,慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程


还需要解决的问题:



  • 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果;至于怎么传,可以根据每个公司得实际情况去定


qiankun


接下来在 iframe 的基础下扩展下 qiankun,在使用方面还是简单的
qiankun 使用指南


父项目中


值得注意的是,我们需要增加一个类型,如果是 qiankun 的项目,需要全部指向新增的 qiankun 组件


<template>
<div>
<div id="microContainer" class="microContainer"></div>
</div>
</template>

<script>
import fetchData from './fetchData'; // 一些逻辑处理
import { loadMicroApp } from 'qiankun';
import { getToken } from '@/...';

export default {
data() {
return {
microRef: null,
};
},
methods: {
fetch(route) {
fetchData(route).then(({ data }) => {
const { name, entry } = data;

this.microRef = loadMicroApp({
name,
entry,
container: '#yourContainer',
props: {
router: this.$router,
data: {
// 一些参数
},
token: getToken(),
},
});
});
},
unregisterApplication() {
this.microAppRef.mountPromise.then(() => this.microAppRef.unmount());
},
},
mounted() {
this.fetch(this.$route);
},
beforeDestroy() {
this.unregisterApplication();
},
};
</script>

子项目中


在 src 目录新增文件 public-path.js


iif (window.__POWERED_BY_QIANKUN__) {
// 动态设置子应用的基础路径
// 使用 window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ 变量来获取主应用传递的基础路径
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

子项目中需要导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用


const render = (parent = {}) => {
if (window.__POWERED_BY_QIANKUN__) {
// 渲染 qiankun 的路由
} else {
// 渲染正常的路由
}
};

//全局变量来判断环境,独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/

export async function bootstrap() {
console.log('react app bootstraped');
}

/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/

export async function mount(props) {
render(props.data);
}

/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/

export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root')
);
}

/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/

export async function update(props) {
console.log('update props', props);
}

子站点 webpack 配置


const packageName = require('./package.json').name;

module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd', // 把子应用打包成 umd 库格式,让 qiankun 拿到其 export 的生命周期函数
jsonpFunction: `webpackJsonp_${packageName}`,
},
};

路由方面:qiankun 使用父站点的路由,在子站点获取到路由信息,然后加载不同的组件,如果单独运行子站点,则需要适配自己的路由组件,做一些差异化处理就好了


qiankun 总结



  • qiankun 自带 js/css 沙箱功能

    • js 隔离:Proxy 沙箱,它将 window 上的所有属性遍历拷贝生成一个新的 fakeWindow 对象,紧接着使用 proxy 代理这个 fakeWindow,用户对 window 操作全部被拦截下来,只作用于在这个 fakeWindow 之上

    • css 隔离:ShadowDOM 样式沙箱会被开启。在这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响;



  • qiankun 支持子项目预请求功能

  • 支持复用公共依赖

    • webpack 配置 externals:子项目独立运行时,这些依赖的来源有且仅有 index.html 中的外链 script 标签

    • 可以给子项目 index.html 中公共依赖的 script 和 link 标签加上 ignore 属性;有了这个属性,qiankun 便不会再去加载这个 js/css,而子项目独立运行,这些 js/css 仍能被加载




存在的问题:



  • css 污染问题

    • 全局 CSS:如果子站点中使用了全局 CSS 样式(如直接写在 HTML 中的 标签或通过 引入的外部样式表),这些全局样式可能会影响整个页面,包括父站点和其他子站点。
    • CSS 命名冲突:如果子站点和父站点使用相同的 CSS 类名或样式选择器,它们的样式规则可能会相互覆盖或产生不可预料的效果。为避免冲突,可以采用命名约定或 CSS 模块化来隔离样式。




总结


目前只是初步接入了 qiankun,后续还会基于 qiankun 做一些优化,当然不考虑一些因素的情况下,个人觉得 iframe 依旧是最完美的沙箱隔离,当然目前在我们的项目中,他们是共存的,各有优劣。微前端也是前端工

作者:Breeze
来源:juejin.cn/post/7251495270800752700
程化一个重要的方案。

收起阅读 »

再学http-为什么文件上传要转成Base64?

web
1 前言 最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本...
继续阅读 »

1 前言


最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本质上都是一样的,那么为什么还要先转成Base64呢?这两种方式有什么区别?带着这样的疑问我们一起来分析下。


2 multipart/form-data上传


先来看看multipart/form-data的方式,我在本地通过一个简单的例子来查看http multipart/form-data方式的文件上传,html代码如下


<!DOCTYPE html>
<html>
<head>
<title>上传文件示例</title>
<meta charset="UTF-8">
<body>
<h1>上传文件示例</h1>
<form action="/upload" method="POST" enctype="multipart/form-data">
<label for="file">选择文件:</label>
<input type="file" id="file" name="file"><br>
<label for="tx">说明:</label>
<input type="text" id="tx" name="remark"><br><br>
<input type="submit" value="上传">
</form>
</body>
</html>

页面展示也比较简单


image.png


选择文件点击上传后,通过edge浏览器f12进入调试模式查看到的请求信息。

请求头如下
image.png
在请求头里Content-Type 为 multipart/form-data; boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo,刚开始看肯定有点懵,不过其实也不复杂,可以简单理解为在请求体里要传递的参数被分为多部份,每一部分通过分解符boundary分割,就比如在这个例子,表单里有file和remark两个字段,则在请求体里就被分为两部分,每一部分通过boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo来分隔(实际上还要加上CRLF回车换行符,回车表示将光标移动到当前行的开头,换行表示一行文本的结束,也就是新文本行的开始)。需要注意下当最后一部分结尾时需要加多两个"-"结尾。

我们继续来看请求体


image.png
第一部分是file字段部分,它的Content-Type为image/png,第二部分为remark字段部分,它没有声明Content-Type,则默认为text/plain纯文本类型,也就是在例子中输入的“测试”,到这里大家肯定会有个疑问,上传的图片是放在哪里的,这里怎么没看到呢?别急,我猜测是浏览器做了特殊处理,请求体里不显示二进制流,我们通过Filder抓包工具来验证下。


image.png
可以看到在第一部分有一串乱码显示,这是因为图片是二进制文件,显示成文本格式自然就乱码了,这也证实了二进制文件也是放在请求体里。后端使用框架springboot通过MultipartFile接受文件也是解析请求体的每一部分最终拿到二进制流。


@RestController
public class FileController {
// @RequestParam可接收Content-Type 类型为:multipart/form-data 
// 或 application/x-www-form-urlencoded 请求体的内容
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
return "test";
}
}

到此multipart/form-data方式上传文件就分析完了,关于multipart/form-data官方说明可参考 RFC 7578 - Returning Values from Forms: multipart/form-data (ietf.org)


3 Base64上传


在http的请求方式中,文件上传只能通过multipart/form-data的方式上传,这样一来就会有比较大的限制,那有没其他方式可以突破这一限制,也就是说我可以通过其他的请求方式上传,比如application/json?当然有,把文件当成一个字符串,和其他普通参数没什么两样,我们可以通过其他任意请求方式上传。如果转成了字符串,那上传文件就比较简单了,但问题是我们怎么把二进制流转成字符串,因为这里面可能会有很多“坑”,业界一般的做法是通过Base64编码把二进制流转成字符串,那为什么不直接转成字符串而要先通过Base64来转呢?我们下面来分析下。


3.1 Base64编码原理


在分析原理之前,我们先来回答什么是Base64编码?首先我们要知道Base64只是一种编码方式,并不是加解密算法,因此Base64可以编码,那也可以解码,它只是按照某种编码规则把一些不可显示字符转成可显示字符。这种规则的原理是把要编码字符的二进制数每6位分为一组,每一组二进制数可对应Base64编码的可打印字符,因为一个字符要用一个字节显示,那么每一组6位Base64编码都要在前面补充两个0,因此总长度比编码前多了(2/6) = 1/3,因为6和8最小公倍数是24,所以要编码成Base64对字节数的要求是3的倍数(24/8=3字节),对于不足字节的需要在后面补充字节数,补充多少个字节就用多少个"="表示(一个或两个),这么说有点抽象,我们通过下面的例子来说明。

我们对ASCII码字符串"AB\nC"(\n和LF都代表换行)进行Base64编码,因为一共4字节,为了满足是3的倍数需要扩展到6个字节,后面补充了2个字节。


image.png


表3.1


转成二级制后每6位一组对应不同颜色,每6位前面补充两个0组成一个字节,最终Base64编码字符是QUIKQw==,Base64编码表大家可以自行网上搜索查看。


image.png
我们通过运行程序来验证下


image.png
最终得出的结果与我们上面推理的一样。


3.2 Base64编码的作用


在聊完原理之后,我们继续来探讨文件上传为什么要先通过Base64编码转成字符串而不直接转成字符串?一些系统对特殊的字符可能存在限制或者说会被当做特殊含义来处理,直接转成普通字符串可能会失真,因此上传文件要先转成Base64编码字符,不能把二进制流直接字符串。


另外,相比较multipart/form-data Base64编码文件上传比较灵活,它不受请求类型的限制,可以是任何请求类型,因为最终就是一串字符串,相当于请求的一个参数字段,它不像二进制流只能限定multipart/form-data的请求方式,日常开发中,我们用的比较多的是通过apllication/json的格式把文件字段放到请求体,这种方式提供了比较便利的可操作性。


4 总结


本文最后再来总结对比下这两种文件上传的方式优缺点。

(1)multipart/form-data可以传输二进制流,效率较高,Base64需要编码解码,会耗费一定的性能,效率较低。

(2)Base64不受请求方式的限制,灵活度高,http文件二进制流方式传输只能通过multipart/form-data的方式,灵活度低。

因为随着机器性能的提升,小文件通过二进制流传输和字符串传输,我们对这两种方式时间延迟的感知差异并不那么明显,因此大部分情况下我们更多考虑的是灵活性,所以采用Base64编码的情况也就比较多。


作者:初心不改_1
来源:juejin.cn/post/7251131990438264889
收起阅读 »

为啥你的tree的checkbox隐藏的这么艰难

web
场景: 近期在实现一个基于element-ui 的 Tree 组件的场景, 产品要求, 部门的数据,都不要checkbox, 只有节点值为 员工 才显示,而且还要部分员工的checkbox 禁用 element-ui 的 tree 还不支持特定节点的check...
继续阅读 »

场景:


近期在实现一个基于element-ui 的 Tree 组件的场景, 产品要求, 部门的数据,都不要checkbox, 只有节点值为 员工 才显示,而且还要部分员工的checkbox 禁用


element-ui 的 tree 还不支持特定节点的checkbox隐藏功能, 网上大多采用 class 的方式,将第一层的checkbox进行了隐藏, 但是不满足我们的要求


规则:



  • 第一层节点不显示checkbox

  • 后续任意子节点,如果数据为部门 则也不显示 checkbox

  • 后端返回的部分数据,如果人员符合特定规则(根据自己场景来即可),则表现为 禁用 checkbox


实现


数据
treeData.js


export default [
{
"id":1,
"label":"一级 1-是部门",
"depType":1,
"disabled":false,
"children":[
{
"id":4,
"label":"二级 1-1-是部门",
"depType":1,
"disabled":false,
"children":[
{
"id":9,
"label":"三级 1-1-9",
"disabled":false
},
{
"id":25,
"label":"三级 1-1-25",
"disabled":false
},
{
"id":27,
"label":"三级 1-1-27",
"disabled":false
},
{
"id":30,
"label":"三级 1-30",
"disabled":false
},
{
"id":10,
"label":"三级 1-1-2 是部门",
"depType":5,
"disabled":false
}
]
}
]
},
{
"id":2,
"label":"一级 2 部门",
"depType":1,
"disabled":false,
"children":[
{
"id":5,
"label":"二级 2-1 张三",
"disabled":false
},
{
"id":6,
"label":"二级 2-2 李四",
"disabled":false
}
]
},
{
"id":3,
"label":"一级 3 部门",
"depType":1,
"disabled":false,
"children":[
{
"id":7,
"depType":1,
"label":"二级 3-1 王武",
"disabled":false
},
{
"id":8,
"label":"二级 3-2 赵柳",
"disabled":false
}
]
}
]

上述数据,有的有 deptType字段 ,有的节点没有, 这其实是业务场景的特殊规则,有deptType的认为这个节点为部门节点,没有的则为 员工


<template>
<div>
<el-tree
node-key="id"
show-checkbox
:data="treeData"
:render-content="renderContent"
class="tree-box"
@node-expand='onNodeExpand'
>
</el-tree>
<div>

<ul>
<li>一开始的数据结构必须都有 disabled字段, 默认不禁用,设置为 false 否则会出现视图的响应式延迟问题</li>
<li>是否禁用某个节点,根据renderContent 里面的规则来的, 规则是, 只要是部门的维度,就禁用 设置 data.disabled= true</li>
<li>tree的第一层节点隐藏,是通过js控制的</li>
</ul>
</div>
</div>

</template>

<script>
import treeData from './treeData.js'

export default {
name: 'render-content-tree',
data() {
return {
treeData
}
},
mounted() {
let nodes = document.querySelector('.tree-box')
let children = nodes.querySelectorAll('.el-tree-node')

for(let i=0; i< children.length; i++) {
children[i].querySelector('.el-checkbox').style.display = 'none'
}

// 第一层不要checkbox
// 后续根据规则来
},

methods: {
renderContent(h, { node, data, store }) {
// console.log(node, data)

// 如果不是一级节点,并且符合数据的特定要求,比如这里是 id 大于27 的数据,禁用掉
if (node.level !== 1 && data.id > 27) {
data.disabled = true
}

return h('div',
{
// 如果是部门,就将所有的 checkbox 都隐藏
class: data.depType === undefined ? '' : 'dept-node'
},
data.label)
},

setDeptNodeHide() {
let deptNodes = document.querySelectorAll('.dept-node')

for(let i=0; i<deptNodes.length; i++) {
let checkbox = deptNodes[i].parentNode.querySelector('.el-checkbox')

checkbox.style.display = 'none'
}
},

onNodeExpand(data, node, com) {
// console.log(data);
// console.log(node);
// console.log(com);

this.$nextTick(() => {
this.setDeptNodeHide()
})
}
}
}
</script>

image.png


节点初次渲染的效果.png




展开后的效果


image.png


部门节点没有checkbox, 符合特定规则的c

作者:知了清语
来源:juejin.cn/post/7250040492162433081
heckbox 禁用

收起阅读 »

手撸一个私信功能

web
前言 几年前的项目里写了一pc版的私信功能,使用的版本和代码比较老了, 这篇文章就直接粘了之前的代码简单的改了改,说明一下问题; 主要就是写一下这个功能如何下手,思想,以及界面如何整,消息怎么发等; 也只是截取了当时项目里私信的一部分功能,这个完全可以说明问题...
继续阅读 »

前言


几年前的项目里写了一pc版的私信功能,使用的版本和代码比较老了,

这篇文章就直接粘了之前的代码简单的改了改,说明一下问题;

主要就是写一下这个功能如何下手,思想,以及界面如何整,消息怎么发等;

也只是截取了当时项目里私信的一部分功能,这个完全可以说明问题了;


效果


界面大概是这样的
image.png


整体动态效果是这样的


test 00_00_00-00_00_30~1.gif


test1 00_00_00-00_00_30.gif


说下大致思路吧


首先是把界面分成左边和右边,左边占少一部分,是朋友目录界面;

右边占多一点,右边是聊天的详情界面;

点击左边对应的那个人,右边就会出现本人跟点击的那个人的聊天详情;


左边人员目录的思路


左边的人员目录和显示的头像,最新的一条消息还有时间,这些都是后端返给前端的;

前端把数据展现出来就行,

时间那里可以根据公司需求以及后端返回的格式转成前天,刚刚等根据需求而定;

我这块时间项目中是有分开前天,昨天,刚刚的,

只不过这里就自己造的数据时间随便写的;

当然这里数据多的时候,可做成虚拟滚动效果;
每个人头像那个红色是消息数量,当读完消息时,就恢复成剩下的消息数量;


右边聊天详情的思路


右边是左边点击对应的聊天人员时,

拿这个人的id之类的数据去请求后端,拿对应的聊天详情数据;

最下面的显示的是最新的聊天信息,后端给的排序不对,可自己反转去排序;

这里也做成虚拟滚动;

最上面显示的那个名称是当前和谁聊天的那个人的昵称;


image.png


聊天界面里也显示的是时间,昵称,头像,聊天信息内容,

时间也需要分昨天,前天,刚刚等。。。


发送消息的思路


我这里也做了按键和点击按钮两种方式;

按键就是在代码里添加一个键盘的监听事件就可;


    var footerTarget = document.getElementById('footer');
footerTarget.addEventListener('keydown', this.footerKeydown);

Enter按键是13;



//底部keydown监听事件
footerKeydown = (e) => {
if (e?.keyCode === 13) {
this.handleSubmit();
}
};

发送消息界面其实就是个表单,做成那个样子就可以啦;

发送消息时,调用后端接口,把这条消息添加在消息数据后面就可;


结尾


只是简单写下思路就已经写这么多了;

代码后面有空给粘上;

由于我是临时把几年前的代码拿出来粘的,

为了显示效果,数据也是自己造的,

一些时间呀以及显示,已读信息的数量呀以及其他一些细节都没有管,

实际项目中直接对应接口嘛,

所以这里就只是随便

作者:浅唱_那一缕阳光
来源:juejin.cn/post/7250029035744149541
改改说明一下问题哈;

收起阅读 »

我工作中用到的性能优化全面指南

web
在Web开发中,Web的性能优化是一个重要的话题。无论是页面加载速度,用户体验,或者是程序运行效率,都与Web的性能优化息息相关。 最小化和压缩代码 在构建过程中,为了减少文件的大小和加载时间,通常会对JavaScript代码进行最小化和压缩处理。这包括移除...
继续阅读 »

在Web开发中,Web的性能优化是一个重要的话题。无论是页面加载速度,用户体验,或者是程序运行效率,都与Web的性能优化息息相关。



最小化和压缩代码


在构建过程中,为了减少文件的大小和加载时间,通常会对JavaScript代码进行最小化和压缩处理。这包括移除不必要的空格、换行、注释,以及缩短变量和函数名。工具如UglifyJS和Terser等可以帮助我们完成这个任务。


// 原始代码
function hello(name) {
let message = 'Hello, ' + name;
console.log(message);
}

// 压缩后的代码
function hello(n){var e='Hello, '+n;console.log(e)}

利用浏览器缓存


浏览器缓存是提升Web应用性能的一个重要手段。我们可以将一些经常用到的、变化不大的数据存储在本地,以减少对服务器的请求。例如,可以使用localStorage或sessionStorage来存储这些数据。


// 存储数据
localStorage.setItem('name', 'John');

// 获取数据
var name = localStorage.getItem('name');

// 移除数据
localStorage.removeItem('name');

// 清空所有数据
localStorage.clear();

避免过度使用全局变量


全局变量会占用更多的内存,并且容易导致命名冲突,从而降低程序的运行效率。我们应尽量减少全局变量的使用。


// 不好的写法
var name = 'John';

function greet() {
console.log('Hello, ' + name);
}

// 好的写法
function greet(name) {
console.log('Hello, ' + name);
}

greet('John');

使用事件委托减少事件处理器的数量


事件委托是将事件监听器添加到父元素,而不是每个子元素,以此来减少事件处理器的数量,并且提升性能。


document.getElementById('parent').addEventListener('click', function (event) {
if (event.target.classList.contains('child')) {
// 处理点击事件...
}
});

好的,下面我会详细解释一下这些概念以及相关的示例:


async 和 defer


asyncdefer 是用于控制 JavaScript 脚本加载和执行的 HTML 属性。



  • async 使浏览器在下载脚本的同时,继续解析 HTML。一旦脚本下载完毕,浏览器将中断 HTML 解析,执行脚本,然后继续解析 HTML。


<script async src="script.js"></script>


  • defer 也使浏览器在下载脚本的同时,继续解析 HTML。但是,脚本的执行会等到 HTML 解析完毕后再进行。


<script defer src="script.js"></script>

在需要控制脚本加载和执行的时机以优化性能的场景中,这两个属性是非常有用的。


防抖和节流


throttle(节流)和 debounce(防抖)。



  • throttle 保证函数在一定时间内只被执行一次。例如,一个常见的使用场景是滚动事件的监听函数:


function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) return;
lastCall = now;
return func(...args);
};
}

window.addEventListener('scroll', throttle(() => console.log('Scrolling'), 100));


  • debounce 保证在一定时间内无新的触发后再执行函数。例如,实时搜索输入的监听函数:


function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}

searchInput.addEventListener('input', debounce(() => console.log('Input'), 300));

利用虚拟DOM和Diff算法进行高效的DOM更新


当我们频繁地更新DOM时,可能会导致浏览器不断地进行重绘和回流,从而降低程序的性能。因此,我们可以使用虚拟DOM和Diff算法来进行高效的DOM更新。例如,React和Vue等框架就使用了这种技术。


// React示例
class Hello extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

ReactDOM.render(<Hello name="John" />, document.getElementById('root'));

避免长时间运行的任务


浏览器单线程的运行方式决定了JavaScript长时间运行的任务可能会阻塞UI渲染和用户交互,从而影响性能。对于这类任务,可以考虑将其分解为一系列较小的任务,并在空闲时执行,这就是“分片”或者“时间切片”的策略。


function chunk(taskList, iteration, context) {
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && taskList.length > 0) {
iteration.call(context, taskList.shift());
}

if (taskList.length > 0) {
chunk(taskList, iteration, context);
}
});
}

chunk(longTasks, (task) => {
task.execute();
}, this);

虚拟列表(Virtual List)


当我们在页面上渲染大量的元素时,这可能会导致明显的性能问题。虚拟列表是一种技术,可以通过只渲染当前可见的元素,来优化这种情况。



虚拟列表的等高方式实现:



// 列表项高度
const ITEM_HEIGHT = 20;

class VirtualList {
constructor(container, items, renderItem) {
this.container = container;
this.items = items;
this.renderItem = renderItem;

this.startIndex = 0;
this.endIndex = 0;
this.visibleItems = [];

this.update();

this.container.addEventListener('scroll', () => this.update());
}

update() {
const viewportHeight = this.container.clientHeight;
const scrollY = this.container.scrollTop;

this.startIndex = Math.floor(scrollY / ITEM_HEIGHT);
this.endIndex = Math.min(
this.startIndex + Math.ceil(viewportHeight / ITEM_HEIGHT),
this.items.length
);

this.render();
}

render() {
// 移除所有的可见元素
this.visibleItems.forEach((item) => this.container.removeChild(item));
this.visibleItems = [];

// 渲染新的可见元素
for (let i = this.startIndex; i < this.endIndex; i++) {
const item = this.renderItem(this.items[i]);
item.style.position = 'absolute';
item.style.top = `${i * ITEM_HEIGHT}px`;
this.visibleItems.push(item);
this.container.appendChild(item);
}
}
}

// 使用虚拟列表
new VirtualList(
document.getElementById('list'),
Array.from({ length: 10000 }, (_, i) => `Item ${i}`),
(item) => {
const div = document.createElement('div');
div.textContent = item;
return div;
}
);


优化循环


在处理大量数据时,循环的效率是非常重要的。我们可以通过一些方法来优化循环,例如:避免在循环中进行不必要的计算,使用倒序循环,使用forEach或map等函数。


// 不好的写法
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

// 好的写法
let length = arr.length;
for (let i = 0; i < length; i++) {
console.log(arr[i]);
}

// 更好的写法
arr.forEach(function (item) {
console.log(item);
});

避免阻塞UI


JavaScript的运行是阻塞UI的,当我们在进行一些耗时的操作时,应尽量使用setTimeout或Promise等异步方法,以避免阻塞UI。


setTimeout(function () {
// 执行耗时的操作...
}, 0);

使用合适的数据结构和算法


使用合适的数据结构和算法是优化程序性能的基础。例如,当我们需要查找数据时,可以使用对象或Map,而不是数组;当我们需要频繁地添加或移除数据时,可以使用链表,而不是数组。


// 使用对象进行查找
var obj = { 'John': 1, 'Emma': 2, 'Tom': 3 };
console.log(obj['John']);

// 使用Map进行查找
var map = new Map();
map.set('John', 1);
map.set('Emma', 2);
map.set('Tom', 3);
console.log(map.get('John'));

避免不必要的闭包


虽然闭包在某些情况下很有用,但是它们也会增加额外的内存消耗,因此我们应该避免不必要的闭包。


// 不必要的闭包
function createFunction() {
var name = 'John';
return function () {
return name;
}
}

// 更好的方式
function createFunction() {
var name = 'John';
return name;
}

避免使用with语句


with语句会改变代码的作用域,这可能会导致性能问题,因此我们应该避免使用它。


// 不好的写法
with (document.getElementById('myDiv').style) {
color = 'red';
backgroundColor = 'black';
}

// 好的写法
var style = document.getElementById('myDiv').style;
style.color = 'red';
style.backgroundColor = 'black';

避免在for-in循环中使用hasOwnProperty


hasOwnProperty方法会查询对象的整个原型链,这可能会影响性能。在for-in循环中,我们应该直接访问对象的属性。


// 不好的写法
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key + ': ' + obj[key]);
}
}

// 好的写法
for (var key in obj) {
console.log(key + ': ' + obj[key]);
}

使用位操作进行整数运算


在进行整数运算时,我们可以使用位操作符,它比传统的算术运算符更快。


// 不好的写法
var half = n / 2;

// 好的写法
var half = n >> 1;

避免在循环中创建函数


在循环中创建函数会导致性能问题,因为每次迭代都会创建一个新的函数实例。我们应该在循环外部创建函数。


// 不好的写法
for (var i = 0; i < 10; i++) {
arr[i] = function () {
return i;
}
}

// 好的写法
function createFunction(i) {
return function () {
return i;
}
}

for (var i = 0; i < 10; i++) {
arr[i] = createFunction(i);
}

使用Web Worker进行多线程处理


JavaScript默认是单线程运行的,但我们可以使用Web Worker来进行多线程处理,以提升程序的运行效率。


// 主线程
var worker = new Worker('worker.js');

worker.onmessage = function (event) {
console.log('Received message ' + event.data);
}

worker.postMessage('Hello Worker');

// worker.js
self.onmessage = function(event) {
console.log('Received message ' + event.data);
self.postMessage('You said: ' + event.data);
};

使用WebAssembly进行性能关键部分的开发


WebAssembly是一种新的编程语言,它的代码运行速度接近原生代码,非常适合于进行性能关键部分的开发。例如,我们可以用WebAssembly来开发图形渲染、物理模拟等复杂任务。


// 加载WebAssembly模块
WebAssembly.instantiateStreaming(fetch('module.wasm'))
.then(result => {
// 调用WebAssembly函数
result.instance.exports.myFunction();
});

使用内存池来管理对象


当我们频繁地创建和销毁对象时,可以使用内存池来管理这些对象,以避免频繁地进行内存分配和垃圾回收,从而提升性能。


class MemoryPool {
constructor(createObject, resetObject) {
this.createObject = createObject;
this.resetObject = resetObject;
this.pool = [];
}

acquire() {
return this.pool.length > 0 ? this.resetObject(this.pool.pop()) : this.createObject();
}

release(obj) {
this.pool.push(obj);
}
}

var pool = new MemoryPool(
() => { return {}; },
obj => { for (var key in obj) { delete obj[key]; } return obj; }
);

使用双缓冲技术进行绘图


当我们需要进行频繁的绘图操作时,可以使用双缓冲技术,即先在离屏画布上进行绘图,然后一次性将离屏画布的内容复制到屏幕上,这样可以避免屏幕闪烁,并且提升绘图性能。


var offscreenCanvas = document.createElement('canvas');
var offscreenContext = offscreenCanvas.getContext('2d');

// 在离屏画布上进行绘图...
offscreenContext.fillRect(0, 0, 100, 100);

// 将离屏画布的内容复制到屏幕上
context.drawImage(offscreenCanvas, 0, 0);

使用WebGL进行3D渲染


WebGL是一种用于进行3D渲染的Web标准,它提供了底层的图形API,并且能够利用GPU进行加速,非常适合于进行复杂的3D渲染。


var canvas = document.getElementById('myCanvas');
var gl = canvas.getContext('webgl');

// 设置清空颜色缓冲区的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);

// 清空颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

使用Service Workers进行资源缓存


Service Workers可以让你控制网页的缓存策略,进一步减少HTTP请求,提升网页的加载速度。例如,你可以将一些不常变化的资源文件预先缓存起来。


// 注册一个service worker
navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(error) {
console.log('ServiceWorker registration failed: ', error);
});

// service-worker.js
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll([
'/style.css',
'/script.js',
// 更多资源...
]);
})
);
});

self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});

使用内容分发网络(CDN)


你可以将静态资源(如JavaScript、CSS、图片等)上传到CDN,这样用户可以从离他们最近的服务器下载资源,从而提高下载速度。


<!-- 从CDN加载jQuery库 -->
<script src="https://cdn.example.com/jquery.min.js"></script>

使用HTTP/2进行资源加载


HTTP/2支持头部压缩和多路复用,可以更高效地加载资源。如果你的服务器和用户的浏览器都支持HTTP/2,那么你可以使用它来提高性能。


// 假设我们有一个HTTP/2库
var client = new Http2Client('https://example.com');

client.get('/resource1');
client.get('/resource2');

使用Web Socket进行数据通信


如果你需要频繁地与服务器进行数据交换,可以使用Web Socket,它比HTTP有更低的开销。


var socket = new WebSocket('ws://example.com/socket');

socket.addEventListener('open', function() {
socket.send('Hello, server');
});

socket.addEventListener('message', function(event) {
console.log('Received message from server: ' + event.data);
});

使用Progressive Web Apps(PWA)技术


PWA可以让你的网站在离线时仍然可用,并且可以被添加到用户的主屏幕,提供类似于原生应用的体验。PWA需要使用Service Workers和Manifest等技术。


// 注册Service Worker
navigator.serviceWorker.register('/service-worker.js');

// 检测是否支持Manifest
if ('manifest' in document.createElement('link')) {
var link = document.createElement('link');
link.rel = 'manifest';
link.href = '/manifest.json';
document.head.appendChild(link);
}

使用WebRTC进行实时通信


WebRTC是一种提供实时通信(RTC)能力的技术,允许数据直接在浏览器之间传输,对于需要实时交互的应用,如视频聊天、实时游戏等,可以使用WebRTC来提高性能。


var pc = new RTCPeerConnection();

// 发送offer
pc.createOffer().then(function(offer) {
return pc.setLocalDescription(offer);
}).then(function() {
// 发送offer给其他浏览器...
});

// 收到answer
pc.setRemoteDescription(answer);

使用IndexedDB存储大量数据


如果你需要在客户端存储大量数据,可以使用IndexedDB。与localStorage相比,IndexedDB可以存储更大量的数据,并且支持事务和索引。


var db;
var request = indexedDB.open('myDatabase', 1);
request.onupgradeneeded = function(event) {
db = event.target.result;
var store = db.createObjectStore('myStore', { keyPath: 'id' });
store.createIndex('nameIndex', 'name');
};
request.onsuccess = function(event) {
db = event.target.result;
};
request.onerror = function(event) {
// 错误处理...
};

使用Web Push进行后台消息推送


Web Push允许服务器在后台向浏览器推送消息,即使网页已经关闭。这需要在Service Worker中使用Push API和Notification API。


// 请求推送通知的权限
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
console.log('Push notification permission granted');
}
});

// 订阅推送服务
navigator.serviceWorker.ready.then(function(registration) {
registration.pushManager.subscribe({ userVisibleOnly: true }).then(function(subscription) {
console.log('Push subscription: ', subscription);
});
});

// 在Service Worker中接收和显示推送通知
self.addEventListener('push', function(event) {
var data = event.data.json();
self.registration.showNotification(data.title, data);
});

通过服务器端渲染(SSR)改善首次页面加载性能


服务器端渲染意味着在服务器上生成HTML,然后将其发送到客户端。这可以加快首次页面加载速度,因为用户可以直接看到渲染好的页面,而不必等待JavaScript下载并执行。这对于性能要求很高的应用来说,是一种有效的优化手段。


// 服务器端
app.get('/', function(req, res) {
const html = ReactDOMServer.renderToString(<MyApp />);
res.send(`<!DOCTYPE html><html><body>${html}</body></html>`);
});

利用HTTP3/QUIC协议进行资源传输


HTTP3/QUIC协议是HTTP/2的后续版本,采用了全新的底层传输协议(即QUIC),以解决HTTP/2中存在的队头阻塞(Head-of-line Blocking)问题,从而进一步提高传输性能。如果你的服务器和用户的浏览器都支持HTTP3/QUIC,那么可以考虑使用它进行资源传输。


使用Service Worker与Background Sync实现离线体验


通过Service Worker,我们可以将网络请求与页面渲染解耦,从而实现离线体验。并且,结合Background Sync,我们可以在用户离线时提交表单或同步数据,并在用户重新联网时自动重试。


// 注册Service Worker
navigator.serviceWorker.register('/sw.js');

// 提交表单
fetch('/api/submit', {
method: 'POST',
body: new FormData(form)
}).catch(() => {
// 如果请求失败,使用Background Sync重试
navigator.serviceWorker.ready.then(reg => {
return reg.sync.register('sync-submit');
});
});

// 在Service Worker中监听sync事件
self.addEventListener('sync', event => {
if (event.tag === 'sync-submit') {
event.waitUntil(submitForm());
}
});

使用PostMessage进行跨文档通信


如果你的应用涉及到多个窗口或者iframe,你可能需要在他们之间进行通信。使用postMessage方法可以进行跨文档通信,而不用担心同源策略的问题。


// 父窗口向子iframe发送消息
iframeElement.contentWindow.postMessage('Hello, child', 'https://child.example.com');

// 子iframe接收消息
window.addEventListener('message', function(event) {
if (event.origin !== 'https://parent.example.com') return;
console.log('Received message: ' + event.data);
});

使用Intersection Observer进行懒加载


Intersection Observer API可以让你知道一个元素何时进入或离开视口,这对于实现图片或者其他资源的懒加载来说非常有用。


var images = document.querySelectorAll('img.lazy');

var observer = new IntersectionObserver(function(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
var img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});

images.forEach(img => {
observer.observe(img);
});

利用OffscreenCanvas进行后台渲染


OffscreenCanvas API使得开发者可以在Web Worker线程中进行Canvas渲染,这可以提高渲染性能,尤其是在进行大量或者复杂的Canvas操作时。


var offscreen = new OffscreenCanvas(256, 256);
var ctx = offscreen.getContext('2d');

// 在后台线程中进行渲染...

利用Broadcast Channel进行跨标签页通信


Broadcast Channel API提供了一种在同源的不同浏览器上下文之间进行通信的方法,这对于需要在多个标签页之间同步数据的应用来说非常有用。


var channel = new BroadcastChannel('my_channel');

// 发送消息
channel.postMessage('Hello, other tabs');

// 接收消息
channel.onmessage = function(event) {
console.log('Received message: ' + event.data);
};

使用Web Cryptography API进行安全操作


Web Cryptography API 提供了一组底层的加密API,使得开发者可以在Web环境中进行安全的密码学操作,例如哈希、签名、加密、解密等。


window.crypto.subtle.digest('SHA-256', new TextEncoder().encode('Hello, world')).then(function(hash) {
console.log(new Uint8Array(hash));
});

使用Blob对象进行大型数据操作


Blob对象代表了一段二进制数据,可以用来处理大量的数据,比如文件。它们可以直接从服务端获取,或者由客户端生成,这对于处理大型数据或者二进制数据很有用。


var fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', function(event) {
var file = event.target.files[0];
var reader = new FileReader();
reader.onload = function(event) {
var contents = event.target.result;
processContents(contents);
};
reader.readAsArrayBuffer(file);
});

使用Page Visibility API进行页面可见性调整


Page Visibility API提供了一种方式来判断页面是否对用户可见。利用这个API,你可以在页面不可见时停止或减慢某些操作,例如动画或视频,从而节省CPU和电池使用。


document.addEventListener('visibilitychange', function() {
if (document.hidden) {
pauseAnimation();
} else {
resumeAnimation();
}
});

使用WeakMap和WeakSet进行高效的内存管理


在处理大量数据时,如果不小心可能会产生内存泄漏。WeakMap和WeakSet可以用来保存对对象的引用,而不会阻止这些对象被垃圾回收。这在一些特定的应用场景中,例如缓存、记录对象状态等,可能非常有用。


let cache = new WeakMap();

function process(obj) {
if (!cache.has(obj)) {
let result = /* 对obj进行一些复杂的处理... */
cache.set(obj, result);
}

return cache.get(obj);
}

使用requestAnimationFrame进行动画处理


requestAnimationFrame能够让浏览器在下一次重绘之前调用指定的函数进行更新动画,这样可以保证动画的流畅性,并且减少CPU的使用。


function animate() {


// 更新动画...
requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

使用CSS3动画替代JavaScript动画


CSS3动画不仅可以提供更好的性能,还可以在主线程之外运行,从而避免阻塞UI。因此,我们应该尽可能地使用CSS3动画替代JavaScript动画。


@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

.myDiv {
animation: fadeIn 2s ease

-in-out;
}

避免回流和重绘


回流和重绘是浏览器渲染过程中的两个步骤,它们对性能影响很大。优化的关键在于尽可能减少触发回流和重绘的操作,例如一次性修改样式,避免布局抖动等。


var el = document.getElementById('my-el');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
// 尽量避免上面的写法,以下为优化后的写法
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';

使用CSS3硬件加速提高渲染性能


使用 CSS3 的 transform 属性做动画效果,可以触发硬件加速,从而提高渲染性能。


element.style.transform = 'translate3d(0, 0, 0)';

避免使用同步布局


同步布局(或强制布局)是指浏览器强制在 DOM 修改和计算样式之后,立即进行布局。这会中断浏览器的优化过程,导致性能下降。一般出现在连续的样式修改和读取操作之间。


let div = document.querySelector('div');

// 写样式
div.style.width = '100px';
// 读样式,导致同步布局
let width = div.offsetWidth;
// 再写样式
div.style.height = width + 'px'; // 强制布局

为避免这个问题,可以将读操作移到所有写操作之后:


let div = document.querySelector('div');

// 写样式
div.style.width = '100px';
// 写样式
div.style.height = '100px';

// 读样式
let width = div.offsetWidth;

使用ArrayBuffer处理二进制数据


ArrayBuffer 提供了一种处理二进制数据的高效方式,例如图像,声音等。


var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
for (var i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}

利用ImageBitmap提高图像处理性能


ImageBitmap对象提供了一种在图像处理中避免内存拷贝的方法,可以提高图像处理的性能。


var img = new Image();
img.src = 'image.jpg';
img.onload = function() {
createImageBitmap(img).then(function(imageBitmap) {
// 在这里使用 imageBitmap
});
};
作者:linwu
来源:juejin.cn/post/7249991926307864613

收起阅读 »

我看UI小姐姐就是在为难我这个切图仔

web
前言 改成这个样子 咱也不懂啊,这样更好看了吗,只能照着改了,谁让我只是个卑微的切图仔呢. 实现过程 刚开始我觉得很简单嘛,封装一个组件,用它包裹表单元素,比如Input、 Select、DatePicker等,然后修改css样式,把表单元素的bord...
继续阅读 »

前言



image.png


改成这个样子


image.png


咱也不懂啊,这样更好看了吗,只能照着改了,谁让我只是个卑微的切图仔呢.


image.png


实现过程


刚开始我觉得很简单嘛,封装一个组件,用它包裹表单元素,比如Input、 Select、DatePicker等,然后修改css样式,把表单元素的border干掉,给外面的组件加上border不就搞定了,看起来也不是很复杂的样子.第一版长这样


image.png


发现问题了嘛,select下拉选项的宽度和表单元素不一样长,当然我觉得问题不大能用就行,但是在ui眼里那可不行,必须要一样长,不然不好看.
好吧,在我的据理力争下,我妥协啦,开始研究下一版.


image.png


在第一版的基础上我发现只有Select有这个问题,那就好办了,针对它单独处理就行了,解决方法思考了3种:



  • 第一种就是antd的Select可以设置dropdownStyle,通过获取父元素的宽度来设置下拉菜单的宽度,以此达到等长的目的

  • 第二种就是通过设置label元素为绝对定位,同时设置Select的paddingLeft

  • 还有一种就是通过在Select里添加css伪元素(注意这种方法需要把content里的中文转成unicode编码,不然可能会乱码)


最终我采用的是第二种方法,具体代码如下


import React, { CSSProperties, PropsWithChildren, useMemo } from 'react';
import { Form, FormItemProps, Col } from 'antd';
import styles from './index.module.less';

interface IProps extends FormItemProps {
label?: string;
style?: CSSProperties;
className?: string;
isSelect?: boolean;
noMargin?: boolean;
col?: number;
}
export const WrapFormComponent = ({ children, className, isSelect, style, col, noMargin = true, ...props }: PropsWithChildren<IProps>) => {
const labelWidth = useMemo(() => {
if (!isSelect || !props.label) return 11;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context!.font = '12px PingFang SC';
const metrics = context!.measureText(props.label);
return metrics.width + (props.colon === undefined || props.colon ? 10 : 0) + 11;
}, [isSelect, props.label, props.colon]);
return (
<Col span={col}>
<Form.Item
style={{ '--label-length': labelWidth + 'px', marginBottom: noMargin ? 0 : '16px', ...style } as CSSProperties}
className={`${styles['wrap-form']} ${isSelect ? styles['wrap-form-select'] : ''} ${className || ''}`}
{...props}
>

{children}
</Form.Item>
</Col>

);
};


less代码


.wrap-form {
padding: 0 !important;
padding-left: 11px !important;
border: 1px solid #c1c7cd;
border-radius: 4px;

:global {
.ant-form-item-label {
display: inline-flex !important;
align-items: center !important;
}

.ant-form-item-label > label {
height: auto;
color: #919399;
font-weight: 400;
}

.ant-picker {
width: 100%;
}

.ant-input,
.ant-select-selector,
.ant-picker,
.ant-input-number {
border: none;
border-color: transparent !important;
}

.ant-input-affix-wrapper {
background: none;
border: none;
}
}
}

.wrap-form-select {
position: relative;
padding: 0 !important;

:global {
.ant-form-item-label {
position: absolute;
top: 50%;
left: 11px;
z-index: 1;
text-align: left;
transform: translateY(-50%);
}

.ant-select-selector {
padding-left: var(--label-length) !important;
}

.ant-select-selection-search {
left: var(--label-length) !important;
}

.ant-select-multiple .ant-select-selection-search {
top: -2px;
left: 0 !important;
margin-left: 0 !important;
}

.ant-select-multiple .ant-select-selection-placeholder {
left: var(--label-length) !important;
height: 28px;
line-height: 28px;
}
}
}

最后就变成这样了,完美解决,这下ui总不能挑刺儿了吧.


image.png

收起阅读 »

记两次优化导致的Bug

web
人云,过早的优化不如不优化。个人的理解,还是要具体情况具体分析。一般这里认为的是,开发过程的变动会导致之前做出的优化失灵。 如果没有,那说明你赌对了,不,说明你眼光真好。 废话到此结束。 本文记录了两次巧合。优化本身一般不会导致Bug,但是可能会有其它没预料到...
继续阅读 »

人云,过早的优化不如不优化。个人的理解,还是要具体情况具体分析。一般这里认为的是,开发过程的变动会导致之前做出的优化失灵。 如果没有,那说明你赌对了,不,说明你眼光真好。


废话到此结束。 本文记录了两次巧合。优化本身一般不会导致Bug,但是可能会有其它没预料到的问题。Bug本天成,菜鸡偶遇之。


和requestFrameAnimation有关。


和requestFrameAnimation就是在下一帧的时候调用, 调用这个方法后会返回一个id,凭借此id可以取消下一帧的调用cancelAnimationFrame(id)


在图形渲染中,经常会使用连续渲染的方式来不断更新视图,使用此api可以保证每一帧只绘制一次,一帧绘制一次就够了,两次没有意义。 当然,这里说的一次,是指一整个绘制,包含了后处理(比如three的postprocess)的一系列操作。


大致情况是这样的, 现在已经封装好了一个3d渲染器,然后在更新场景数据的,会有比较多的循环,这个时候,3d渲染就可以停掉了,这里就是做了这个优化。


来看主要代码,渲染器部分。只要调用animate方法就会循环渲染, pause停止,resume恢复渲染。 看上去好像没啥问题。


// 渲染 
animate = () => {
if (this.paused ) return ;
this.animateId = requestAnimationFrame(this.animate);
};
pause() {
this.paused = true;
}
resume() {
if (this.paused) {
this.paused = false;
this.animate()
}
}

再看,更新数据的部分。 问题就出在这里,更新数据这个操作,是没有任何异步的,也就是说在当前帧的宏任务里,先暂停后恢复, 结果就是,下一帧执行animate的时候, paused仍为true, 这个优化毫无意义。


view.current.pause() ;

//更新数据完成
view.current.resume()


无意义倒也没啥,但是 resume方法执行了之后,会新开一个requestAnimationFrame, 上一个requestAnimationFrame的循环又没有取消,


所以现在, 一帧里会执行两次render方法。 这个卡顿啊,就十分明显了,也就是当时的项目模型还不是特别大,只有在某些场景模型较大的时候才会卡,所以没急着处理。


过了几个月,临近更新了,不得不解决。排查的时候,是靠git记录二分才找出来的。


其次,要说明的是,使用了requestAnimationFrame连续渲染,这种在一帧里先暂停再继续的操作肯定是无意义的,因为下一帧才执行,只看执行前的结果。


当然,前面的暂停和继续的逻辑,也是一个隐患。 于是,就改成了我惯用的那种方式。 那就是暂停的时候,只是不渲染,循环继续空跑,如此而已。


  // 渲染
animate = () => {
!this.paused && this.renderer.render(this.scene, this.camera);
this.animateId = requestAnimationFrame(this.animate);
};

pause() {
this.paused = true;
}

resume() {
this.paused = false;
}

和 URL.create 有关


上面的那个代码,还可以说是写的人不熟悉requestAnimationFrame的特性造成的。 这一次的这个,真的是因为,引用关系比较多。


这次是一个纯2d项目,这里有一个将(后端)处理后的图片在canvas2d编辑器上显示出来的操作。这个图,叫产品图, 产品图是用户上传的, 也可以不经过后端处理,就直接进入到2d编辑器里。 后端处理之后,返回的是一个url。 所以有了下面的函数。


  function afterImgMatter(img:File|string) {
setShowMatter(false);
if (img instanceof File) {
tempImg.src = URL.createObjectURL(img);
} else {
tempImg.src = img
}

console.log(tempImg.src);
if (productImg) {
let url = (productImg.image as HTMLImageElement)?.src
url.startsWith('blob') && URL.revokeObjectURL(url)
}
if (refCanvas.current) {
tempImg.onload = () => {

// 产品图层不可手动删除
productImg = refCanvas.current!.addImg(tempImg, false, null, 'product');
if (img instanceof File) {
productImg.id = globalData.productId;
} else {
productImg.id = globalData.matteredId;
}
setCanvasEditState(true);
!curScene && changeSene(scenes[0]);

}
}
}


如果是没经过后端处理的,那个图片就是文件转URL,就用到了这个方法URL.createObjectURL, 这个方法为二进制文件生成一个临时路径,mdn强调了一定要手动释放它。 浏览器在 document 卸载的时候,会自动释放它们,也就是说在这之前GC不会自动释放他们,即便找不到引用了。


那这个优化,我必然不能不做。 所以,我就判断了一下,如果之前的src是这个方法生成的,我就去释放它。 于是,在重新上传图片,也就是二次触发这个方法的时候出问题了, 画布直接白屏了,原来是报错了。


Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state.


说我提供的图像源是破碎的,所以这个drawImage方法不能执行,其实上面还有一个报错,我之前一直没在意, 说得是一个url路径失效了,图片加载失败了,因为我释放了路径,所以我觉得出现这个,应该是正常的。


但是,现在结合drawImage的执行失败,这里还是有问题的。 我发现,确实就是因为我释放了要用做图像源的那个路径。 因为这里的productImg和 tempImg其实是通一个引用,只不过语义不同。


解决办法也很简单,那就是把释放的这一段代码,放到onload的回调里执行即可,图片加载完成之后,释放这个url也能正常工作


    tempImg.onload = () => {
if (productImg) {
let url = (productImg.image as HTMLImageElement)?.src
url.startsWith('blob') && URL.revokeObjectURL(url)
}
}

这里之所以会有 productImg和 tempImg通一个引用,语义不同, 也是因为我想优化一下。之前是每次加载图片的时候,都new Image,实际上这个可以用同一个对象,所以就有了tempImg


结束


本文记录了两个bug,顺带说了一下requestAnimationFrame URL.createObjectURL的部分用法。


没有直接阐述他们的用法,有兴趣了解的可以直接看文档。


requestAnimationFrame


URL.createObj

ectURL

收起阅读 »

如何实现比 setTimeout 快 80 倍的定时器?

web
很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说: 在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这...
继续阅读 »

很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说:



在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。



HTML Standard 规范中也有提到更具体的:



Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.



简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。


用如下代码做个测试:


let a = performance.now();
setTimeout(() => {
let b = performance.now();
console.log(b - a);
setTimeout(() => {
let c = performance.now();
console.log(c - b);
setTimeout(() => {
let d = performance.now();
console.log(d - c);
setTimeout(() => {
let e = performance.now();
console.log(e - d);
setTimeout(() => {
let f = performance.now();
console.log(f - e);
setTimeout(() => {
let g = performance.now();
console.log(g - f);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);

在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。



更详细的原因,可以参考 为什么 setTimeout 有最小时延 4ms ?


探索


假设我们就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,上面那篇 MDN 文档的角落里有一些线索:



如果想在浏览器中实现 0ms 延时的定时器,你可以参考这里所说的 window.postMessage()



这篇文章里的作者给出了这样一段代码,用 postMessage 来实现真正 0 延迟的定时器:


(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';

// 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}

function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}

window.addEventListener('message', handleMessage, true);

// 把 API 添加到 window 对象上
window.setZeroTimeout = setZeroTimeout;
})();

由于 postMessage 的回调函数的执行时机和 setTimeout 类似,都属于宏任务,所以可以简单利用 postMessageaddEventListener('message') 的消息通知组合,来实现模拟定时器的功能。


这样,执行时机类似,但是延迟更小的定时器就完成了。


再利用上面的嵌套定时器的例子来跑一下测试:



全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。


测试


从理论上来说,由于 postMessage 的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。但空口无凭,咱们用数据说话。


作者设计了一个实验方法,就是分别用 postMessage 版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。读者也可以在这里自己跑一下测试


实验代码:


function runtest() {
var output = document.getElementById('output');
var outputText = document.createTextNode('');
output.appendChild(outputText);
function printOutput(line) {
outputText.data += line + '\n';
}

var i = 0;
var startTime = Date.now();
// 通过递归 setZeroTimeout 达到 100 计数
// 达到 100 后切换成 setTimeout 来实验
function test1() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setZeroTimeout took ' +
(endTime - startTime) +
' milliseconds.'
);
i = 0;
startTime = Date.now();
setTimeout(test2, 0);
} else {
setZeroTimeout(test1);
}
}

setZeroTimeout(test1);

// 通过递归 setTimeout 达到 100 计数
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' +
(endTime - startTime) +
' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}

实验代码很简单,先通过 setZeroTimeout 也就是 postMessage 版本来递归计数到 100,然后切换成 setTimeout 计数到 100。


直接放结论,这个差距不固定,在我的 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在我硬件更好的台式机上,甚至能到 200 倍以上。



Performance 面板


只是看冷冰冰的数字还不够过瘾,我们打开 Performance 面板,看看更直观的可视化界面中,postMessage 版的定时器和 setTimeout 版的定时器是如何分布的。



这张分布图非常直观的体现出了我们上面所说的所有现象,左边的 postMessage 版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。


而右边的 setTimeout 版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。


作用


也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。


借用 React Scheduler 为什么使用 MessageChannel 实现 这篇文章中的一段伪代码:


const channel = new MessageChannel();
const port = channel.port2;

// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;

const scheduler = {
scheduleTask() {
// 挑选一个任务并执行
const task = pickTask();
const continuousTask = task();

// 如果当前任务未完成,则在下个宏任务继续执行
if (continuousTask) {
port.postMessage(null);
}
},
};

React 把任务切分成很多片段,这样就可以通过把任务交给 postMessage 的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。


为什么不用执行时机更靠前的微任务呢?参考我的这篇对 EventLoop 规范的解读 深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系,关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。


总结


通过本文,你大概可以了解如下几个知识点:



  1. setTimeout 的 4ms 延迟历史原因,具体表现。

  2. 如何通过 postMessage 实现一个真正 0 延迟的定时器。

  3. postMessage 定时器在 React 时间切片中的运用。

  4. 为什么时间切片需要用宏任务,而不是微任务。

作者:ssh_晨曦时梦见兮
来源:juejin.cn/post/7249633061440749628

收起阅读 »

项目提交按钮没防抖,差点影响了验收

web
前言 一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交了,由于数据错误...
继续阅读 »

前言


一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交了,由于数据错误个别单据流程给弄不正常了,一些报表的数据统计也不对了,客户相关人员很不满意,马上该交付了,出这问题可还了得,项目款不按时给了,这责任谁都担不起🤣


QQ图片20230627163527.jpg


领导紧急组织相关技术人员开会分析原因


初步分析原因


发生这个情况前端选手应该会很清楚这是怎么回事,明显是项目里的按钮没加防抖导致的,按钮点击触发接口,接口响应慢,用户多点了几次,可能查询接口还没什么问题,如果业务复杂的地方,部分按钮的操作涉及到一些数据计算和后端多次交互更新数据的情况,就会出现错误。


看下项目情况


用到的框架和技术


项目使用 angular8 ts devextreme 组合。对!这就是之前文章提到的那个屎山项目(试用期改祖传屎山是一种怎么样的体验


项目规模


业务单据页面大约几百个,项目里面的按钮几千个,项目里面的按钮由于场景复杂,分别用了如下几种写法:



  • dx-button

  • div

  • dx-icon

  • input type=button

  • svg


由于面临交付,领导希望越快越好,最好一两天之内解决问题


还好我们领导没有说这问题当天就要解决 😁


解决方案


1. 添加防抖函数


按钮点击添加防抖函数,设置合理的时间


function debounce(func, wait) {
let timeout;
return function () {
if(timeout) clearTimeout(timeout);
timeout = setTimeout(func, wait)
}
}

优点


封装一个公共函数,往每个按钮的点击事件里加就行了


缺点


这种情况有个问题就是在业务复杂的场景下,时间设置会比较棘手,如果时间设置短了,接口请求慢,用户多次点击还会出现问题,如果时间设置长了,体验变差了


2. 设置按钮禁用


设置按钮的 disabled 相关属性,按钮点击后设置禁用效果,业务代码执行结束后取消禁用


this.disabled = true
this.disabled = false

优点


原生按钮和使用的UI库的按钮设置简单


缺点


div, icon, svg 这种自定义的按钮的需要单独处理效果,比较麻烦


3. 请求拦截器中添加loading


在请求拦截器中根据请求类型显示 loading,请求结束后隐藏


优点


直接在一个地方设置就行了,不用去业务代码里一个个加


缺点


由于我们的技术栈使用的 angular8 内置的请求,无法实现类似 axios 拦截器那种效果,还有就是项目中的接口涉及多个部门的接口,不同部门的规范命名不一样,没有统一的标准,在实际的业务场景中,一个按钮的行为可能触发了多个请求,因此这个方案不适合当前的项目


4. 添加 loading 组件(项目中使用此方案)


新增一个 loading 组件,绑定到全局变量中,按钮点击触发显示 loading,业务执行结束后隐藏。


loading 组件核心代码


import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
  providedIn: 'root'
})
export class LoadingService {
  private isLoading$ = new BehaviorSubject<boolean>(false);
  private message$ = new BehaviorSubject<string>('正在加载中...');
  constructor() {}
  show(): void {
    this.isLoading$.next(true);
  }
  hide(): void {
    this.isLoading$.next(false);
  }
}

主要是 show()hide() 函数,将 loading 组件绑定到 app.components.ts 中,绑定组件到window 对象上,


window['loading'] = this.loadingService

在按钮点击时触发 show() 函数,业务代码执行结束后触发 hide() 函数


window['loading'].show();
window['loading'].hide();

优点


这种方式很好的解决了问题,由于 loading 有遮罩层还避免了用户点击某提交按钮后,接口响应慢,这时候去点击了别的操作按钮的情况。


缺点


需要在业务单据的按钮提交的地方一个个加


问题来了,一两天解决所有问题了吗?


QQ图片20230627165837.png


这么大的项目一两天不管哪种方案,把所有按钮都处理好是不现实的,经过分析讨论,最终选择了折中处理,先把客户提出来的几个业务单据页面,以及相关的业务单据页面添加上提交 loading 处理,然后再一边改 bug 一边完善剩余的地方,优先保证客户正常使用



还有更好的解决思路吗?欢迎JYM讨论交流


作者:草帽lufei
来源:juejin.cn/post/7249288087820861499

收起阅读 »

面试官问:如何实现 H5 秒开?

web
我在简历上写了精通 H5,结果面试官上来就问: 同学,你说你精通 H5 ,那你能不能说一下怎么实现 H5 秒开? 由于没怎么做过性能优化,我只能凭着印象,断断续续地罗列了几点: 网络优化:http2、dns 预解析、使用 CDN 图片优化:压缩、懒加...
继续阅读 »

我在简历上写了精通 H5,结果面试官上来就问:



同学,你说你精通 H5 ,那你能不能说一下怎么实现 H5 秒开?



image.png


由于没怎么做过性能优化,我只能凭着印象,断断续续地罗列了几点:




  • 网络优化:http2、dns 预解析、使用 CDN

  • 图片优化:压缩、懒加载、雪碧图

  • 体积优化:分包、tree shaking、压缩、模块外置

  • 加载优化:延迟加载、骨架屏

  • ...



看得出来面试官不太满意,最后面试也挂了。于是我请教了我的好友 Gahing ,问问他的观点。



Gahing:


你列的这些优化手段本身没啥问题,如果是一个工作一两年的我会觉得还可以。但你已经五年以上工作经验了,需要有一些系统性思考了。



好像有点 PUA 的味道,于是我追问道:什么是系统性的思考?



Gahing:


我们先说回答方式,你有没有发现,你回答时容易遗漏和重复。


比如说「图片懒加载」,你归到了「图片优化」,但其实也可以归到「加载优化」。同时你还漏了很多重要的优化手段,比如资源缓存、服务端渲染等等。


究其原因应该是缺少抽象分类方法。



那针对这个问题,应该如何分类回答?



Gahing:


分类并非唯一,可以有不同角度,但都需遵从 MECE 原则(相互独立、完全穷尽) ,即做到不重不漏




  • 按页面加载链路分类:容器启动、资源加载、代码执行、数据获取、绘制渲染。




  • 按资源性能分类:CPU、内存、本地 I/O、网络。该分类方法又被叫做 USE 方法(Utilization Saturation and Errors Method)




  • 按协作方分类:前端、客户端、数据后台、图片服务、浏览器引擎等。




  • 按流程优化分类前置、简化、拆分



    • 前置即调整流程,效果上可能是高优模块前置或并行,低优模块后置;

    • 简化即缩减或取消流程,体积优化是简化,执行加速也是简化;

    • 拆分即细粒度拆解流程,本身没有优化效果,是为了更好的进行前置和简化。

    • 这个角度抽象层次较高,通常能回答出来的都是高手。




  • 多级分类:使用多个层级的分类方法。比如先按页面加载链路分类,再将链路中的每一项用协作方或者流程优化等角度再次分类。突出的是一个系统性思维。




选择好分类角度,也便于梳理优化方案的目标。



现在,尝试使用「页面加载链路+流程优化+协作方」的多级分类思维,对常见的首屏性能优化手段进行分类。


image.png


PS: 可以打开飞书文档原文查看思维导图


好像有点东西,但是我并没有做过性能优化,面试官会觉得我在背八股么?



Gahing:


可以没有实操经验,但是得深入理解。随便追问一下,比如「页面预渲染效果如何?有什么弊端?什么情况下适用?」,如果纯背不加理解的话很容易露馅。


另外,就我个人认为,候选人拥有抽象思维比实操经验更重要,更何况有些人的实操仅仅是知道怎么做,而不知道为什么做。



那我按上面的方式回答了,能顺利通过面试么 🌝 ?



Gahing:


如果能按上面的抽象思维回答,并顶住追问,在以前应该是能顺利通过面试的(就这个问题)。


但如今行业寒冬,大厂降本增效,对候选人提出了更高的要求,即系统性思考业务理解能力


从这个问题出发,如果想高分通过,不仅需要了解优化方案,还要关注研发流程、数据指标、项目协作等等,有沉淀自己的方法论和指导性原则,能实施可执行的 SOP。。




最后,我还是忍不住问了 Gahing :如果是你来回答这个问题,你会怎么回答?



Gahing:


H5 秒开是一个系统性问题,可以从深度和广度两个方向来回答。


深度关注的是技术解决方案,可以从页面加载链路进行方案拆解,得到容器启动、资源加载、代码执行、数据获取、绘制渲染各个环节。其中每个环节还可以从协作方和流程优化的角度进一步拆解。


广度关注的是整个需求流程,可以用 5W2H 进行拆解,包括:



  • 优化目标(What):了解优化目标,即前端首屏加载速度

  • 需求价值(Why):关注需求收益,从技术指标(FMP、TTI)和业务指标(跳失率、DAU、LT)进行分析

  • 研发周期(When):从开发前到上线后,各个环节都需要介入

  • 项目协作(Who):确定优化专项的主导方和协作方

  • 优化范围(Where):关注核心业务链路,确定性能卡点

  • 技术方案(How):制定具体的优化策略和行动计划

  • 成本评估(How much):评估优化方案的成本和效益。考虑时间、资源和预期收益,确保优化方案的可行性和可持续性。


通过 5W2H 分析法,可以建立系统性思维,全面了解如何实现 H5 秒开,并制定相应的行动计划来改进用户体验和页面性能。





限于篇幅,后面会单独整理两篇文章来聊聊关于前端首屏优化的系统性思考以及可实施的解决方案。


👋🏻 Respect!欢迎一键三连 ~


作者:francecil
来源:juejin.cn/post/7249665163242307640
收起阅读 »

websocket 实时通信实现

web
轮询和websocket对比 开发过程中,有些场景,如弹幕、聊天、统计实时在线人数、实时获取服务端最新数据等,就需要实现”实时通讯“,一般有如下两种方式: 轮询:定义一个定时器,不停请求数据并更新,近似地实现“实时通信”的效果 这种方式比较古老,但是兼容性...
继续阅读 »

轮询和websocket对比


开发过程中,有些场景,如弹幕、聊天、统计实时在线人数、实时获取服务端最新数据等,就需要实现”实时通讯“,一般有如下两种方式:




  1. 轮询:定义一个定时器,不停请求数据并更新,近似地实现“实时通信”的效果


    这种方式比较古老,但是兼容性强。


    缺点就是不断请求,耗费了大量的带宽和 CPU 资源,而且存在一定的延迟性




  2. websocket 长连接:全双工通信,客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,更加方便




websocket 实现


创建 websocket 连接


建立ws连接,有如下两种形式:


ws 代表明文,默认端口号为 80,例如ws://http://www.example.com:80, 类似http


wss 代表密文,默认端口号为 443,例如wss://http://www.example.com:443, 使用SSL/TLS加密,类似https


const useWebSocket = (params: wsItem) => {
// 定义传参 url地址 phone手机号
let { url = "", phone = "" } = params;
const ws = (useRef < WebSocket) | (null > null);
// ws数据
const [wsData, setMessage] = (useState < wsDataItem) | (null > null);
// ws状态
const [readyState, setReadyState] =
useState < any > { key: 0, value: "正在连接中" };
// 是否在当前页
const [isLocalPage, setIsLocalPage] = useState(true);

// 创建Websocket
const createWebSocket = () => {
try {
window.slWs = ws.current = new WebSocket(
`wss://${url}/ws/message/${phone}`
);
// todo 全局定义发送函数
window.slWs.sendMessage = sendMessage;
// todo 准备初始化
initWebSocket();
} catch (error) {
// 创建失败需要进行异常捕获
slLog.error("ws创建失败", error);
// todo 准备重连
reconnect();
}
};

return { isLocalPage, wsData, closeWebSocket, sendMessage };
};

初始化 websocket


当前的连接状态定义如下,使用常量数组控制:


const stateArr = [
{ key: 0, value: "正在连接中" },
{ key: 1, value: "已经连接并且可以通讯" },
{ key: 2, value: "连接正在关闭" },
{ key: 3, value: "连接已关闭或者没有连接成功" },
];

主要有四个事件,连接成功的回调函数(onopen)、连接关闭的回调函数(onclose)、连接失败的回调函数(onerror)、收到消息的回调函数(onmessage)


const initWebSocket = () => {
ws.current.onopen = (evt) => {
slLog.log("ws建立链接", evt);
setReadyState(stateArr[ws.current?.readyState ?? 0]);
// todo 心跳检查重置
keepHeartbeat();
};
ws.current.onclose = (evt) => {
slLog.log("ws链接已关闭", evt);
};
ws.current.onerror = (evt) => {
slLog.log("ws链接错误", evt);
setReadyState(stateArr[ws.current?.readyState ?? 0]);
// todo 重连
reconnect();
};
ws.current.onmessage = (evt) => {
slLog.log("ws接受消息", evt.data);
if (evt && evt.data) {
setMessage({ ...JSON.parse(evt.data) });
}
};
};

ws_1.png


websocket 心跳机制


在使用 ws 过程中,可能因为网络异常或者网络比较差,导致 ws 断开链接了,此时 onclose 事件未执行,无法知道 ws 连接情况。就需要有一个心跳机制,监控 ws 连接情况,断开后,可以进行重连操作。


目前的实现方案就是:前端每隔 5s 发送一次心跳消息,服务端连续 1 分钟没收到心跳消息,就可以进行后续异常处理了


const timeout = 5000; // 心跳时间间隔
let timer = null; // 心跳定时器

// 保持心跳
const keepHeartbeat = () => {
timer && clearInterval(timer);
timer = setInterval(() => {
if (ws.current?.readyState == 1) {
// 发送心跳 消息接口可以自己定义
sendMessage({
cmd: "SL602",
content: { type: "heartbeat", desc: "发送心跳维持" },
});
}
}, timeout);
};

如下图所示,为浏览器控制台中的截图,可以查看ws连接请求及消息详情。


注意:正常情况下,是需要对消息进行加密的,最好不要明文传输。


ws_2.png


websocket 重连处理


let lockFlag = false; // 避免重复连接
// 重连
const reconnect = () => {
try {
if (lockFlag) {
// 是否已经执行重连
return;
}
lockFlag = true;
// 没连接上会一直重连
// 设置延迟避免请求过多
lockTimer && clearTimeout(lockTimer);
var lockTimer = setTimeout(() => {
closeWebSocket();
ws.current = null;
createWebSocket();
lockFlag = false;
}, timer);
} catch (err) {
slLog.error("ws重连失败", err);
}
};

websocket 关闭事件


关闭事件需要暴露出去,给外界控制


// 关闭 WebSocket
const closeWebSocket = () => {
ws.current?.close();
};

websocket 发送数据


发送数据时,数据格式定义为对象形式,如{ cmd: '', content: '' }


// 发送数据
const sendMessage = (message) => {
if (ws.current?.readyState === 1) {
// 需要转一下处理
ws.current?.send(JSON.stringify(message));
}
};

页面可见性


监听页面切换到前台,还是后台,可以通过visibilitychange事件处理。


当页面长时间处于后台时,可以进行关闭或者异常的逻辑处理。


// 判断用户是否切换到后台
function visibleChange() {
// 页面变为不可见时触发
if (document.visibilityState === "hidden") {
setIsLocalPage(false);
}
// 页面变为可见时触发
if (document.visibilityState === "visible") {
setIsLocalPage(true);
}
}

useEffect(() => {
// 监听事件
document.addEventListener("visibilitychange", visibleChange);
return () => {
// 监听销毁事件
document.removeEventListener("visibilitychange", visibleChange);
};
}, []);

页面关闭


页面刷新或者是页面窗口关闭时,需要做一些销毁、清除的操作,可以通过如下事件执行:


beforeunload:当浏览器窗口关闭或者刷新时会触发该事件。当前页面不会直接关闭,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新。


onunload:当文档或一个子资源正在被卸载时,触发该事件。beforeunload在其前面执行,如果点的浏览器取消按钮,不会执行到该处。


function beforeunload(ev) {
const e = ev || window.event;
// 阻止默认事件
e.preventDefault();
if (e) {
e.returnValue = "关闭提示";
}
return "关闭提示";
}
function onunload() {
// 执行关闭事件
ws.current?.close();
}

useEffect(() => {
// 初始化
window.addEventListener("beforeunload", beforeunload);
window.addEventListener("unload", onunload);
return () => {
// 销毁
window.removeEventListener("beforeunload", beforeunload);
window.removeEventListener("unload", onunload);
};
}, []);

执行 beforeunload 事件时,会有如下取消、确认弹框


ws_3.png


参考文档:



作者:时光足迹
来源:juejin.cn/post/7249204284180086842
收起阅读 »

IM 聊天组件

web
IM 消息通常分为文本、图片、文件等 3 类,会对应不同的展示 传入参数 自定义内容:标题(title)、内容(children)、底部(footer) 弹框组件显隐控制: 一般通过一个变量控制显示或隐藏(visible); 并且暴露出一个事件,控制该变量(...
继续阅读 »

IM 消息通常分为文本、图片、文件等 3 类,会对应不同的展示


im_3.png


传入参数


自定义内容:标题(title)、内容(children)、底部(footer)


弹框组件显隐控制:


一般通过一个变量控制显示或隐藏(visible);


并且暴露出一个事件,控制该变量(setVisible)


interface iProps {
title?: string // 标题
maskClose?: boolean // 点击 x 或 mask 回调
visible?: boolean // 是否显示
setVisible: (args) => void // 设置是否显示
children?: React.ReactNode | Array<React.ReactNode> // 自定义内容
footer?: React.ReactNode | Array<React.ReactNode> // 自定义底部
}

基础结构


IM 聊天组件基础结构包含:头部、内容区、尾部


function wsDialog(prop: iProps) {
const wsContentRef = useRef(null); // 消息区
const { title = "消息", maskClose, visible, setVisible } = prop; // 传入参数
const [message, setMessage] = useState(""); // 当前消息
const imMessage = useSelector(
(state: rootState) => state.mediaReducer.imMessage
); // 消息列表 全局管理

return (
<Modal
className={styles.ws_modal}
visible={visible}
transparent
onClose={handleMaskClose}
popup
animationType="slide-up"
>

<div className={styles.ws_modal_widget}>
{/* 头部 */}
<div className={styles.ws_header}></div>
{/* 内容区 */}
<div ref={wsContentRef} className={styles.ws_content}></div>
{/* 尾部区域 */}
<div className={styles.ws_footer}></div>
</div>
</Modal>

);
}

头部区


头部区域主要展示标题和关闭图标


标题内容可以自定义


不仅可以点击“右上角关闭图标”进行关闭


也可以通过点击“遮罩”进行关闭


// 头部关闭事件
function handleClose() {
slLog.log("[wsDialog]点击了关闭按钮");
setVisible(false);
}

// 弹框遮罩关闭事件
function handleMaskClose() {
if (maskClose) {
slLog.log("[wsDialog]点击了遮罩关闭");
setVisible(false);
}
}

// 头部区域
<div className={styles.ws_header}>
<div>{title}</div>
<div className={styles.ws_header_close} onClick={handleClose}>
<Icon type="cross" color="#999" size="lg" />
</div>

</div>;

内容区


消息内容分类展示:



  1. 文本:直接展示内容

  2. 图片:通过 a 标签包裹展示,可以在新标签页中打开,通过target="_blank"控制

  3. 文件:不同类型文件展示不同的图标,包括 zip、rar、doc、docx、xls、xlsx、pdf、txt 等;文件还可以进行下载


<div ref={wsContentRef} className={styles.ws_content}>
{imMessage &&
imMessage.length &&
imMessage.map((o, index) => {
return (
<div
key={index}
className={`${styles.item} ${
o.category === "send" ? styles.self_item : ""
}`}
>

<div className={styles.title}>{o.showName + " " + o.showNum}</div>
{/* 消息为图片 */}
{o.desc === "img" ? (
<a
className={`${styles.desc} ${styles.desc_image}`}
href={o.fileUrl}
title={o.fileName}
target="_blank"
>

<img src={o.fileUrl} />
</a>
) : o.desc === "file" ? (
// 消息为文件
<div className={`${styles.desc} ${styles.desc_file}`}>
<img
className={styles.file_icon}
src={handleSuffix(o.fileSuffix)}
/>

<div className={styles.file_content}>
<a title={o.fileName}>{o.fileName}</a>
<div>{o.fileSize}</div>
</div>
<img
className={styles.down_icon}
src={downIcon}
onClick={() =>
handleDownload(o)}
/>
</div>
) : (
// 消息为文本
<div className={`${styles.desc} ${styles.desc_message}`}>
{o.message}
</div>
)}
</div>

);
})}
</div>

文件下载通过 a 标签模拟实现


// 下载文件
function handleDownload(o) {
slLog.log("[SLIM]下载消息文件", o.fileUrl);
const a = document.createElement("a");
a.href = o.fileUrl;
a.download = o.fileName;
document.body.appendChild(a);
a.target = "_blank";
a.click();
a.remove();
}

监听消息内容,自动滚动到最底部处理


useEffect(() => {
if (visible && imMessage && imMessage.length) {
// 滚动到底部
wsContentRef.current.scrollTop = wsContentRef.current.scrollHeight;
}
}, [visible, imMessage]);

尾部区


主要是操作区,用于展示和发送文本、图片、文件等消息。


图片和文件通过原生input实现,通过accept属性控制文件类型


<div className={styles.ws_footer}>
<div className={styles.tools_panel}>
{/* 上传图片 */}
<div className={styles.tool}>
<img src={imageIcon} />
<input type="file" accept="image/*" onChange={handleChange("img")} />
</div>
{/* 上传文件 */}
<div className={styles.tool}>
<img src={fileIcon} />
<input
type="file"
accept=".doc,.docx,.pdf,.txt,.xls,.xlsx,.zip,.rar"
onChange={handleChange("file")}
/>

</div>
</div>

<div className={styles.input_panel}>
{/* 输入框,上传文本 */}
<input
placeholder="输入文本"
value={message}
onChange={handleInputChange}
className={`${styles.message} ${styles.mMessage}`}
onKeyUp={handleKeyUp}
/>

{/* 消息发送按钮 */}
<div onClick={handleMessage} className={styles.btn}>
发送
</div>
</div>

</div>

获取图片、文件信息:


// 消息处理
function handleChange(type) {
return (ev) => {
switch (type) {
case "img":
case "file":
msgObj.type = type === "img" ? 4 : 7;
const e = window.event || ev;
const files = e.target.files || e.dataTransfer.files;
const file = files[0];
msgObj.content = file;
break;
}
};
}

实现回车键发送消息:


通过输入框,发送文本消息时,一般需要监听回车事件(onKeyUp 事件中的 event.keyCode 为 13),也能发送消息


// 回车事件
function handleKeyUp(event) {
const value = event.target.value;
if (event.keyCode === 13) {
slLog.log("[wsDialog]onKeyUp", value, event.keyCode);
handleInputChange(event);
handleMessage();
}
}

组件封装


组件级别:公司级、系统级、业务级


组件封装优势:



  1. 提升开发效率,组件化、统一化管理

  2. 考虑发布成 npm 形式,远程发布通用


组件封装考虑点:



  1. 组件的分层和分治

  2. 设置扩展性(合理预留插槽)

  3. 兼容性考虑(向下兼容)

  4. 使用对象考虑

  5. 适用范围考虑


组件封装步骤:



  1. 建立组件的模板:基础架子,UI 样式,基本逻辑

  2. 定义数据输入:分析逻辑,定义 props 里面的数据、类型

  3. 定义数据输出:根据组件逻辑,定义要暴露出来的方法,$emit 实现等

  4. 完成组件内部的逻辑,考虑扩展性和维护性

  5. 编写详细的说明文档


作者:时光足迹
来源:juejin.cn/post/7249286405025022009
收起阅读 »

关于正则表达式,小黄人有话要说!!!

web
引言(关于正则表达式,小黄人有话要说!!!) 掌握 JavaScript 正则表达式:从基础到高级,十个实用示例带你提升编程效率! 本文将带你逐步学习正则表达式的基础知识和高级技巧,从基本的元字符到实用的正则表达式示例,让你轻松掌握这一重要的编程技能。无论你是...
继续阅读 »

38dbb6fd5266d016a9ef9caf912bd40734fa3546.jpeg


引言(关于正则表达式,小黄人有话要说!!!)


掌握 JavaScript 正则表达式:从基础到高级,十个实用示例带你提升编程效率!


本文将带你逐步学习正则表达式的基础知识和高级技巧,从基本的元字符到实用的正则表达式示例,让你轻松掌握这一重要的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都能帮助你更好地理解和应用正则表达式。


如果您认为这篇文章对您有帮助或有价值,请不吝点个赞支持一下。如果您有任何疑问、建议或意见,欢迎在评论区留言。


image.png


如果你想快速入门 JavaScript 正则表达式,不妨点击这里阅读文章 "点燃你的前端技能!五分钟掌握JavaScript正则表达式"


字面量和构造函数


在 JavaScript 中,我们可以使用正则表达式字面量构造函数来创建正则表达式对象。


// 使用字面量
let regexLiteral = /pattern/;
// 使用构造函数
let regexConstructor = new RegExp('pattern');

正则表达式的方法


在 JavaScript 中,你可以使用正则表达式的方法进行模式匹配和替换。以下是一些常用的方法:




  • test():测试一个字符串是否匹配正则表达式。


    const regex = /pattern/;
    regex.test('string'); // 返回 true 或 false



  • exec():在字符串中执行正则表达式匹配,返回匹配结果的数组。


    const regex = /pattern/;
    regex.exec('string'); // 返回匹配结果的数组或 null



  • match():在字符串中查找匹配正则表达式的结果,并返回匹配结果的数组。


    const regex = /pattern/;
    'string'.match(regex); // 返回匹配结果的数组或 null



  • search():在字符串中搜索匹配正则表达式的结果,并返回匹配的起始位置。


    const regex = /pattern/;
    'string'.search(regex); // 返回匹配的起始位置或 -1



  • replace():在字符串中替换匹配正则表达式的内容。


    const regex = /pattern/;
    'string'.replace(regex, 'replacement'); // 返回替换后的新字符串



  • split():将字符串根据匹配正则表达式的位置分割成数组。


    const regex = /pattern/;
    'string'.split(regex); // 返回分割后的数组



u=1690536536,1627515251&fm=253&fmt=auto&app=138&f=JPEG.webp


基本元字符


正则表达式由字母、数字和特殊字符组成。其中,特殊字符被称为元字符,具有特殊的意义和功能。以下是一些常见的基本元字符及其作用:


元字符及其作用




  • 字符类 []



    • [abc]:匹配任意一个字符 a、b 或 c。

    • [^abc]:匹配除了 a、b 或 c 之外的任意字符。

    • [0-9]:匹配任意一个数字。

    • [a-zA-Z]:匹配任意一个字母(大小写不限)。




  • 转义字符 \



    • \d:匹配任意一个数字字符。

    • \w:匹配任意一个字母、数字或下划线字符。

    • \s:匹配任意一个空白字符。




  • 量词 {}



    • {n}:匹配前一个元素恰好出现 n 次。

    • {n,}:匹配前一个元素至少出现 n 次。

    • {n,m}:匹配前一个元素出现 n 到 m 次。




  • 边界字符 ^



    • ^pattern:匹配以 pattern 开头的字符串。

    • pattern$:匹配以 pattern 结尾的字符串。

    • \b:匹配一个单词边界。




  • 其他元字符



    • .:匹配任意一个字符,除了换行符。

    • |:用于模式的分组和逻辑 OR。

    • ():捕获分组,用于提取匹配的子字符串。

    • ?::非捕获分组,用于匹配但不捕获子字符串。




实例演示


现在,让我们通过一些实例来演示正则表达式中元字符的实际作用:


u=3528014621,1838675307&fm=253&fmt=auto&app=138&f=JPEG.webp



  • 字符类 []


let regex = /[abc]/;
console.log(regex.test("apple")); // true
console.log(regex.test("banana")); // false


  • 转义字符 \


let regex = /\d{3}-\d{4}/;
console.log(regex.test("123-4567")); // true
console.log(regex.test("abc-1234")); // false


  • 量词 {}


let regex = /\d{2,4}/;
console.log(regex.test("123")); // true
console.log(regex.test("12345")); // false
console.log(regex.test("12")); // true


  • 边界字符 ^


// 以什么开头
let regex = /^hello/;
console.log(regex.test("hello world")); // true
console.log(regex.test("world hello")); // false

// 单词边界
const pattern = /\bcat\b/;
console.log(pattern.test("The cat is black.")); // 输出:true
console.log(pattern.test("A cat is running.")); // 输出:true
console.log(pattern.test("The caterpillar is cute.")); // 输出:false


  • 其他元字符


// 捕获分组与模式分组
let regex = /(red|blue) car/;
console.log(regex.test("I have a red car.")); // true
console.log(regex.test("I have a blue car.")); // true
console.log(regex.test("I have a green car.")); // false

// 点号元字符
const pattern = /a.b/;
console.log(pattern.test("acb")); // 输出:true
console.log(pattern.test("a1b")); // 输出:true
console.log(pattern.test("a@b")); // 输出:true
console.log(pattern.test("ab")); // 输出:false

修饰符的使用


修饰符用于改变正则表达式的匹配行为,常见的修饰符包括 g(全局)、i(不区分大小写)和 m(多行)。


// 使用 `g` 修饰符全局匹配
const regex = /a/g;
const str = "abracadabra";
console.log(str.match(regex)); // 输出:['a', 'a', 'a', 'a']

// 使用 `i` 修饰符进行不区分大小写匹配
const pattern = /abc/i;
console.log(pattern.test("AbcDef")); // 输出:true
console.log(pattern.test("XYZ")); // 输出:false

十个高度实用的正则表达式示例


u=4075901265,1581553886&fm=253&fmt=auto&app=120&f=JPEG.webp



  1. 验证电子邮件地址:


const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
console.log(emailPattern.test("example@example.com")); // 输出:true
console.log(emailPattern.test("invalid.email@com")); // 输出:false


  1. 验证手机号码:


const phonePattern = /^\d{11}$/;
console.log(phonePattern.test("12345678901")); // 输出:true
console.log(phonePattern.test("98765432")); // 输出:false


  1. 提取 URL 中的域名:


const url = "https://www.example.com";
const domainPattern = /^https?://([^/?#]+)(?:[/?#]|$)/i;
const domain = url.match(domainPattern)[1];
console.log(domain); // 输出:"www.example.com"


  1. 验证日期格式(YYYY-MM-DD):


const datePattern = /^\d{4}-\d{2}-\d{2}$/;
console.log(datePattern.test("2023-05-12")); // 输出:true
console.log(datePattern.test("12/05/2023")); // 输出:false


  1. 验证密码强度(至少包含一个大写字母、一个小写字母和一个数字):


const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
console.log(passwordPattern.test("Password123")); // 输出:true
console.log(passwordPattern.test("weakpassword")); // 输出:false


  1. 提取文本中的所有链接:


const text = "Visit my website at https://www.example.com. For more information, check out http://www.example.com/docs.";
const linkPattern = /https?://\S+/g;
const links = text.match(linkPattern);
console.log(links); // 输出:["[https://www.example.com](https://www.example.com/)", "<http://www.example.com/docs>"]


  1. 替换字符串中的所有数字为特定字符:


const text = "I have 3 apples and 5 oranges.";
const digitPattern = /\d/g;
const modifiedText = text.replace(digitPattern, "*");
console.log(modifiedText); // 输出:"I have * apples and * oranges."


  1. 匹配 HTML 标签中的内容:


const html = "<p>Hello, <strong>world</strong>!</p>";
const tagPattern = /<[^>]+>/g;
const content = html.replace(tagPattern, "");
console.log(content); // 输出:"Hello, world!"


  1. 检查字符串是否以特定后缀结尾:


const filename = "example.txt";
const suffixPattern = /.txt$/;
console.log(suffixPattern.test(filename)); // 输出:true


  1. 验证邮政编码(5 位或 5+4 位数字):


const zipCodePattern = /^\d{5}(?:-\d{4})?$/;
console.log(zipCodePattern.test("12345")); // 输出:true
console.log(zipCodePattern.test("98765-4321")); // 输出:true
console.log(zipCodePattern.test("1234")); // 输出:false

u=3763318279,485967013&fm=253&fmt=auto&app=138&f=JPEG.webp


通过正则表达式的核心概念和用法,结合实例和讲解。在实际开发中,不难发现正则表达式是一个强大的工具,可用于字符串处理、模式匹配和验证输入等方面。掌握正则表达式的技巧,可以大大提升 JavaScript 编程的效率和灵活性。


结语


感谢您的阅读!希望本文带给您有价值的信息。


如果对您有帮助,请「点赞」支持,并「关注」我的主页获取更多后续相关文章。同时,也欢迎「收藏」本文,方便以后查阅。


写作不易,我会继续努力,提供有意义的内容。感谢您的支持和关注!


290be963d171f8b42f347d7e97b62252.jpg.source.jpg


作者:Sailing
来源:juejin.cn/post/7249231231967232037
收起阅读 »

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的而且消息已读后需要多次刷新会话列表才会清空未读消息

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的
而且消息已读后需要多次刷新会话列表才会清空未读消息

你不常用的 FileReader 能干什么?

web
前言 欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群! 本文灵感源于上周小伙伴遇到一个问题: "一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!" 这会有什么问题呢? 按原逻辑就是调用该接口后,就会一股脑...
继续阅读 »

前言



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



本文灵感源于上周小伙伴遇到一个问题:


"一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!"


1C306E8E.jpg


这会有什么问题呢?


按原逻辑就是调用该接口后,就会一股脑把该接口接返回过来的内容,直接经过 Blob 对象 转换后再通过隐藏的 <a> 标签实现下载。


但是有一个问题,那就是接口也是需要进行各种逻辑处理、判断等等,然后再决定是给前端响应一个正常的 Blob 格式的文件流,还是返回相应 JSon 格式的异常信息 等等。


如果返回了 JSon 格式的异常信息,那前端应该给用户展示信息内容,而不是将其作为下载的内容!


1C3802FC.gif


FileReader 实现 Blob 从 String 到 JSON


复现问题


为了更直观看到对应的效果,我们这里来简单模拟一下前后端的交互过程吧!


前端


由于小伙伴发送请求时使用的是 Axios,并且设置了其对应的 responsetype:blob | arraybuffer,所以这里我们也使用 Axios 即可,具体如下:


    // 发起请求
const request = () => {
axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then((res) => {

// 转换为 bloc 对象
const blob = new Blob([res.data])

// 获取导出文件名,decodeURIComponent为中文解码方法
const fileName = decodeURIComponent(res.headers["content-disposition"].split("filename=")[1])

// 通过a标签进行下载
let downloadElement = document.createElement('a');
let href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = fileName;
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
});
}

后端


这里我们就简单通过 koa 来实现将一个表格文件响应给前端,具体如下:


    const xlsx = require("node-xlsx");

const Koa = require("koa");
const app = new Koa();

const cors = require("koa2-cors");

// 处理跨域
app.use(
cors({
origin: "*", // 允许来自指定域名请求
maxAge: 5, // 本次预检请求的有效期,单位为秒
methods: ["GET", "POST"], // 所允许的 HTTP 请求方法
credentials: true, // 是否允许发送 Cookie
})
);

// 响应
app.use(async (ctx) => {
// 文件名字
const filename = "人员信息";

// 数据
const data = [
{ name: "赵", age: 16 },
{ name: "钱", age: 20 },
{ name: "孙", age: 17 },
{ name: "李", age: 19 },
{ name: "吴", age: 18 },
];

// 表格样式
const oprions = {
"!cols": [{ wch: 24 }, { wch: 20 }, { wch: 100 }, { wch: 20 }, { wch: 10 }],
};

// JSON -> Buffer
const buffer = JSONToBuffer(data, oprions);

// 设置 content-type
ctx.set("Content-Type", "application/vnd.openxmlformats");

// 设置文件名,中文必须用 encodeURIComponent 包裹,否则会报异常
ctx.set(
"Content-Disposition",
"attachment; filename=" + encodeURIComponent(filename) + ".xlsx"
);

// 文件必须设置该请求头,否则前端拿不到 Content-Disposition 响应头信息
ctx.set("Access-Control-Expose-Headers", "Content-Disposition");

// 将 buffer 返回给前端
ctx.body = buffer;
});

// 将数据转成 Buffer
const JSONToBuffer = (data, options = {}) => {
let xlsxObj = [
{
name: "sheet",
data: [],
},
];

data.forEach((item, idx) => {
// 处理 excel 表头
if (idx === 0) {
xlsxObj[0].data.push(Object.keys(item));
}

// 处理其他 excel 数据
xlsxObj[0].data.push(Object.values(item));
});

// 返回 buffer 对象
return xlsx.build(xlsxObj, options);
};

// 启动服务
app.listen(3000);

正常效果展示


1.gif


异常效果展示


可以看到当返回的内容为 JSON 格式 的内容时,原本逻辑在获取 filename 处就发生异常了,即使这一块没有发生异常,被正常下载下来也是不对的,因为这种情况应该要进行提示。


1.gif


并且此时直接去访问 res.data 得到的也不是一个 JSON 格式 的内容,而是一个 ArrayBuffer


image.png


返回的明明是 JSON ,但是拿到的却是 ArrayBuffer?


responseType 惹的祸


还记得我们在通过 Axios 去发起请求时设置的 responseType:'arraybuffer' 吗?


没错,就是因为这个配置的问题,它会把得到的结果给转成设置的类型,所以看起是一个 JSON 数据,但实际上拿到的是 Arraybuffer



这个 responseType 实际上就是 XMLHttpRequest.responseType,可点击该链接自行查看。



不设置 responseType 行不行?


那么既然是这个配置的问题,那么我们不设置不就好了!


确实可行,如下是未设置 responseType 获取到的结果:


image.png


但也不行,如果不设置 responseType 或者设置的类型不对,那么在 正常情况 下(即 文件被下载)时 会导致文件格式被损坏,无法正常打开,如下:


image.png


FileReader 来救场


实际上还有个比较直接的解决方案,那就是把接收到的 Arraybuffer 转成 JSON 格式不就行了吗?


1CB04D6B.jpg


没错,我们只需要通过 FileReader 来完成这一步即可,请看如下示例:


// json -> blob
const obj = { hello: "world" };

const blob = new Blob([JSON.stringify(obj, null, 2)], {
type: "application/json",
});

console.log(blob) // Blob {size: 22, type: 'application/json'}

// blob -> json
const reader = new FileReader()

reader.onload = () => {
console.log(JSON.parse(reader.result)) // { hello: "world" }
}

reader.readAsText(blob, 'utf-8')

是不是很简单啊!


值得注意的是,并不是任何时候都需要转成 JSON 数据,就像并不是任何时候都要下载一样,我们需要判断什么时候该走下载逻辑,什么时候该走转换成 JSON 数据。


怎么判断当前是该下载?还是该转成 JSON?


这个还是比较简单的,换个说法就是判断当前返回的是不是文件流,下面列举较常见的两种方式。


根据 filename 判断


正常情况来讲,再返回文件流的同时会在 Content-Disposition 响应头中添加和 filename 相关的信息,换句话说,如果当前没有返回 filename 相关的内容,那么就可以将其当做异常情况,此时就应该走转 JSON 的逻辑。


不过需要注意,有时候后端返回的某些文件流并不会设置 filename 的值,此时虽然符合异常情况,但是实际上返回的是一个正常的文件流,因此不太推荐这种方式


208EA3E8.gif


根据 Content-Type 判断


这种方式更合理,毕竟后端无论是返回 文件流 或是 JSON 格式的内容,其响应头中对应的 Content-Type,必然不同,这里的判断更简单,我们直接判断其是不是 JSON 类型即可。


更改后的代码,如下:


axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then(({headers, data}) => {
console.log("FileReader 处理前:", data)

const IsJson = headers['content-type'].indexOf('application/json') > -1;

if(IsJson){
const reader = new FileReader()

// readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob
// 若后端直接返回的就是 blob 类型,则直接使用即可
reader.readAsText(new Blob([data], {type: 'application/json'}), 'utf-8')

reader.onload = () => {
// 将字符内容转为 JSON 格式
console.log("FileReader 处理后:", JSON.parse(reader.result))
}
return
}

// 下载逻辑
download(data)
});

值得注意的是,readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob,若后端直接返回的就是 blob 类型,则直接使用即可。


image.png


FileReader 还能干什么?


以上是使用 FileReader 解决一个实际问题的例子,那么除此之外它还有什么应用场景呢?


不过我们还是先来了解一下 FileReader 的一些相关内容吧!!!


FileReader 是什么?


FileReader 对象允许 Web 应用程序 异步读取 存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。


不过还要注意如下两条规则:



  • FileReader 仅用于以安全的方式从用户(远程)系统读取文件内容,它不能用于从文件系统中按路径名简单地读取文件

  • 要在 JavaScript 中按路径名读取文件,应使用标准 Ajax 解决方案进行 服务器端文件读取


总结起来就是,FileReader 只能读取 FileBlob 类型的文件内容,并且不能直接按路径的方式读取文件,如果需要以路径方式读取,最好要通过 服务端 返回流的形式。


四种读取方式


FileReader 可以如下四种方式读取目标文件:




  • FileReader.readAsArrayBuffer()



    • 开始读取指定的 Blob中的内容,读取完成后,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象




  • FileReader.readAsBinaryString() (非标准



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含所读取文件的 原始二进制数据




  • FileReader.readAsDataURL()



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 data: URL 格式的 Base64 字符串以表示所读取文件的内容




  • FileReader.readAsText()



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 字符串 以表示所读取的文件内容




如上对应的方法命名十分符合顾名思义的特点,因此可以很容易看出来在不同场景下应该选择什么方法,并且如上方法一般都会配合 FileReader.onload 事件FileReader.result 属性 一起使用。


FileReader 的其他应用场景


预览本地文件


通常情况下,前端选择了相应的本地文件(图片、音/视频 等)后,需要通过接口发送到服务端,接着服务端在返回一个相应的预览地址,前端在实现支持预览的操作。


如果说现在有一个需要省略掉中间过程的需求,那么你就可以通过 FileReader.readAsDataURL() 方法来实现,但是要考虑文件大小带来转换时间快慢的问题。


这一部分比较简单,就不贴代码占篇幅了,效果如下:


1.gif


传输二进制格式数据


通常在上传文件时,前端直接将接收到的 File 对象以 FormData 发送给后端,但如果后端需要的是二进制的数据内容怎么办?


此时我们就可以使用 FileReader.readAsArrayBuffer() 来配合,为啥不用 FileReader.readAsBinaryString(),因为它是非标准的,而且 ArrayBuffer 也是原始的 二进制数据


具体代码如下:


// 文件变化
const fileChange = (e: any) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.readAsArrayBuffer(file)

reader.onload = () => {
upload(reader.result, 'http://xxx')
}
}

// 上传
const upload = (binary, url) => {
var xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.overrideMimeType("application/octet-stream");

//直接发送二进制数据
xhr.send(binary);

// 监听变化
xhr.onreadystatechange = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 响应成功
}
}
}
}

最后



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



上面我们通过 FileReader 解决了一个实际问题,同时也简单介绍了其相应的使用场景,但这个场景具体是否是用于你的需求还要具体分析,不能盲目使用。


以上就是本文的全部内容了,希望本文对你有所帮助!!!


21E0754A.jpg

收起阅读 »

数组去重你想到几种办法呢?

web
前言 你是否在面试的过程中被考到过给你一个数组让你去掉重复项呢?当时你的脑海里除了用Set实现之外,你还与面试官讲了什么去重的方法呢?你能否封装来一个可复用的数组去重api呢?依稀记得当时我被问到这个问题的时候,我也没回答出很多种解决办法。那下面我来总结一下对...
继续阅读 »

前言


你是否在面试的过程中被考到过给你一个数组让你去掉重复项呢?当时你的脑海里除了用Set实现之外,你还与面试官讲了什么去重的方法呢?你能否封装来一个可复用的数组去重api呢?依稀记得当时我被问到这个问题的时候,我也没回答出很多种解决办法。那下面我来总结一下对于数组去重这道简单的面试题时,我们可以回答的方法有什么吧。


数组去重


1. 不使用数组API方法


首先我来介绍一种不是用数组身上的API的去重解法,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = []
for(let i = 0; i < array.length; i++){
for( var j = 0; j < res.length; j++){
if(array[i] === res[j]){
break;
}
}
if(j === res.length){
res.push(array[i])
}
}
return res
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]


既然不使用数组自带的API方法,那我们首先考虑的就是用双重for循环了,如上述代码:



  1. 我们准备了一个空的结果数组

  2. 我们对需要去重的数组进行循环

  3. 在第一层数据中再套一层循环,根据下标判断结果数组内是否有重复项。


我们调用该方法,打印结构如上述代码的注解处,成功的实现了对数组的去重。


2. 使用 indexOf


既然有不使用数组API的,那就肯定有使用数组API的,下面看我使用indexOf完成数组的去重,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = []
for (let i = 0; i < array.length; i++) {
if (res.indexOf(array[i]) === -1) { // 返回找到的第一个值得下标
res.push(array[i])
}
}
return res
}
console.log(unique(array))// [ '1', 1, '2', 2 ]


如上述代码, 我们巧妙了使用了indexOf查找结果数组中是否已经存在,如果不存在才向结果数组中添加,实现了数组去重。


在上述代码的基础上,我们还可以转变一下,将for循环内的语句改为


if (array.indexOf((array[i])) == array.lastIndexOf(array[i])) {
i++
} else {
array.splice(array.lastIndexOf(array[i]), 1)
}

不新增其他变量,直接通过indexOf和lastIndexOf判断该值是否在原数组内为唯一值,从而直接修改原数组,实现数组的去重。


3. 使用 sort


对于数组去重,我们除了通过下标找出是否有重复项之外,我们还可以先排序,然后在判断前后项是否相同来实现去重,代码如下:


var  array = [1, 3, 5, 4, 2, 1, 2, 4, 4, 4]
function unique(array) {
let res = []
let sortedArray = array.concat().sort() //concat() 返回新的数组
let seen;
for (let i = 0; i < sortedArray.length; i++) {
if (!i || seen !== sortedArray[i]) {
res.push(sortedArray[i])
}
seen = sortedArray[i]
}
return res
}
console.log(unique(array)); // [ 1, 2, 3, 4, 5 ]

如上述代码, 我们先获取一个排好序的新数组,再对新数组进行循环,判断保存前一个值的seen与当前值是否相同来实现数组去重。


温馨小提示: 由于数组的排序方法不能区分数组和字符串,所以想要使用此方法必须要保证数组的值的类型相同,不然会出bug


4. 使用 filter


既然都用到了sort排序了,那我直接抬出ES6数组新增的filter过滤器API也不过分吧,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = array.filter((item, index, array) => {
return array.indexOf(item) === index
})
return res
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]

如上述代码,filter直接使用array.indexOf(item) === index作为过滤条件返回出一个新的数组,实现数组去重。


如上述代码,我们结合了 indexOf方法作为过滤条件,那我们也可以结合一下sort方法吧,直接使用一行代码就解决了数组的去重。代码如下:


function unique(array) {
return array.concat().sort().filter((item, index, array) => !index || item !== array[item - 1])
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]

5. 使用Set、Map、或者对象


除了上述的通过数组API和不使用数组API的方法外,我们还能想到的就是借助对象来实现数组的去重。使用Set数据结构是我们最容易想到的办法,使用Map与对象方法的相似,都是以数组的值作为key,再将所有的可以取出来组成一个数组。 我就不给小伙伴们演示代码了,感兴趣的小伙伴可以自己动手试试。


(对于对象的key只能为字符串这个问题,我们可以换个思路,将下标存为key,值存为value,判断不同key的值相不相同来实现数组去重。我们还可以在存key时加上其类型,然后进行一次转换。)


自己封装一个去重API


在介绍上述数组去重的方法后,我们再来总结一下,将其融合成一个有复用性,而且还可以适用不同情况的API方法。


我来介绍一下如下我封装的一个数组去重的API方法,



  1. 该方法可接受三个参数,第一个参数为需要去重的数组,第二个参数为该数组是否为排好序的数组,第三个参数为一个回调函数

  2. 该回调函数也有三个参数,分别为值,下标,需要去重数组。该回调函数的作用是方便用户对数组进行一些额外的处理(例如将大写转为小写)

  3. 第二,三参数可不传递。


var array = [1, 2, '1', 'a', 'A', 2, 1]
var array2 = [1, 1, '1', 2, 2]
function uniq(array, isSorted, iteratee) {
let seen = []
let res = []
for(let i = 0; i < array.length; i++){
let computed = iteratee ? iteratee(array[i], i,array) : array[i]
if(isSorted) {
if(!i || seen !== array[i]){
res.push(array[i])
}
seen = array[i]
}else if(iteratee) {
if(seen.indexOf(computed) === -1){
seen.push(computed)
res.push(computed)
}
}
else {
if(res.indexOf(array[i]) === -1) {
res.push(array[i])
}
}
}
return res
}
let result = uniq(array, false, function(item, index, arr){
return typeof item == 'string' ? item.toLowerCase() : item
})
console.log(result); // [ 1, 2, '1', 'a' ]
console.log(uniq(array2, true)); // [ 1, 2 ]

总结


对于数组的去重,当我们能在面试中说到这个多方法的话,这道面试题也就过了,虽然这道面试不难,但如果我们想要想到这个多方法的话,还是

作者:潘小七
来源:juejin.cn/post/7248835844659970105
需要许多知识储备的。

收起阅读 »

在高德地图实现卷帘效果

web
介绍 今天介绍一个非常简单的入门级小案例,就是地图的卷帘效果实现,各大地图引擎供应商都有相关示例,很奇怪高德居然没有,我看了下文档发现其实也是可以简单实现的,演示代码放到文末。本文用到了图层掩模,即图层遮罩,让图层只在指定范围内显示。 实现思路 1.创建目标图...
继续阅读 »

介绍


今天介绍一个非常简单的入门级小案例,就是地图的卷帘效果实现,各大地图引擎供应商都有相关示例,很奇怪高德居然没有,我看了下文档发现其实也是可以简单实现的,演示代码放到文末。本文用到了图层掩模,即图层遮罩,让图层只在指定范围内显示。


实现思路


1.创建目标图层,这里除了有一个默认的底图,还增加了卫星影像图和路网图层,后两者是可以被掩模的。因此在创建图层时通过设置rejectMapMask(默认值false)让图层是否允许被掩模。


2.提供实时设置掩模的方法renderMask,核心代码只需要map.setMask(mask)。


3.实现拖拽交互逻辑,监听拖拽过程,实时触发 renderMask


实现代码


1.创建目标图层


// 基础底图
const baseLayer = new AMap.TileLayer({
zIndex: 1,
//拒绝被掩模
rejectMapMask: true,
})

map = new AMap.Map('container', {
center:[116.472804,39.995725],
viewMode:'3D',
labelzIndex:130,
zoom: 5,
cursor:'pointer',
layers:[
// 底图,不掩模
baseLayer,
// 路网图层
new AMap.TileLayer.RoadNet({
zIndex:7
}),
// 卫星影像图层
new AMap.TileLayer.Satellite()
]
});

2.提供实时设置掩模的方法


function renderMask(){
// 当前地图范围
const {northEast, southWest} = map.getBounds()
// 地理横向跨度
const width = northEast.lng - southWest.lng
// 拖拽条位置占比例
const dom = document.querySelector('#dragBar')
const ratio = Math.ceil(parseInt(dom.style.left) + 5) / map.getSize().width

let mask = [[
[northEast.lng, northEast.lat],
[southWest.lng+ width * ratio, northEast.lat],
[southWest.lng+ width * ratio, southWest.lat],
[northEast.lng, southWest.lat]
]]

map.setMask(mask)
}

3.实现拖拽交互逻辑


// 拖拽交互
function initDrag(){

const dom = document.querySelector('#dragBar')
dom.style.left = `${map.getSize().width/2}px`

// const position = {x:0, y:0}
interact('#dragBar').draggable({
listeners: {
start (event) {
// console.log(event.type, event.target)
},
move (event) {
// 改变拖拽条位置
const left = parseFloat(dom.style.left)
const targetLeft = Math.min(Math.max(left + event.dx, 0), map.getSize().width - 10)
dom.style.left = `${targetLeft}px`

if(event.dx !== 0){
renderMask()
//必须!强制地图重新渲染
map.render()
}
},
end(event){
// console.log(event.type, event.target)
}
}
})
}


  1. 启动相关方法,完善交互逻辑


initDrag()
renderMask()
map.on('mapmove', renderMask)
map.on('zoomchange', renderMask)
window.addEventListener('resize', renderMask)

相关链接


本文代码演示


jsfiddle.net/gyratesky/z…


maptalks 图层卷帘效果


maptalks.org/examples/cn…


卫星+区域掩模


lbs.amap.com/demo/j

avasc…

收起阅读 »

正则别光想着抄,看懂用法下次你也会写

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 日常开发中,应该很多人都经常会使用正则表达式去校验字符串。但是总是遇到复杂的表达式就从网上抄了就结束了,下次写还是不会,今天我们就来看两个稍微复杂一点的案例,从案例中学会一...
继续阅读 »

前言


大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题


日常开发中,应该很多人都经常会使用正则表达式去校验字符串。但是总是遇到复杂的表达式就从网上抄了就结束了,下次写还是不会,今天我们就来看两个稍微复杂一点的案例,从案例中学会一些高级的正则表达式用法。


校验字符串是否包含大小写字母+数字+特殊字符,并且长度为8-12。


如果想要使用单个正则表达式就解决上述问题,就需要稍微学习一下正则的一些高级用法了。


^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[a-zA-Z\d!@#$%^&*()_+]{8,12}$

先行断言(预搜索)


先行断言中不会获取任何内容,只是做一次筛查



  • 正向先行 指在某个位置向右看,该位置必须能匹配该表达式(?=表达式)。

  • 反向先行 指在某个位置往右看,该位置保证不能出现的表达式。

  • 正向后行 指在某个位置向左看,该位置必须能匹配该表达式,但不会获取表达式的内容(?<=表达式)

  • 反向后行 指在某个位置往左看,该位置保证不能出现的表达式(?<!表达式)


这个正则表达式使用了正向先行断言来同时检查字符串中是否包含大小写字母、数字和特殊符号。它的含义如下:



  • ^:匹配字符串的开头。

  • (?=.*[a-z]):正向先行断言,要求字符串中至少包含一个小写字母。

  • (?=.*[A-Z]):正向先行断言,要求字符串中至少包含一个大写字母。

  • (?=.*\d):正向先行断言,要求字符串中至少包含一个数字。

  • (?=.*[!@#$%^&*()_+]):正向先行断言,要求字符串中至少包含一个特殊符号(这里列出了一些常见的特殊符号,你可以根据需要添加或修改)。

  • [a-zA-Z\d!@#$%^&*()_+]:匹配允许的字符集合,包括大小写字母、数字和特殊符号。

  • {8,12}:限定字符串的长度在 8 到 12 位之间。

  • $:匹配字符串的结尾。


使用这个正则表达式可以对目标字符串进行检查,判断是否满足包含大小写、数字和特殊符号,并且长度为 8 到 12 位的要求。例如:


let regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[a-zA-Z\d!@#$%^&*()_+]{8,12}$/;
let str = "Password123!";
let isMatch = regex.test(str);
console.log(isMatch); // 输出: true

获取ip地址


当处理日志文件时,有时需要从日志文本中提取特定的信息。一个常见的场景是提取日志中的 IP 地址。


假设我们有一个日志文件,其中包含了多行日志记录,每行记录的格式如下:


[2023-06-26 10:15:25] [INFO] Access from IP: 192.168.0.1 to URL: /home

在上述示例中,我们使用 match 方法来执行正则表达式匹配,并将匹配的结果存储在 match 变量中。如果有匹配结果,我们可以从数组中取得第一个元素 match[0],即提取到的 IP 地址。


let logText = "[2023-06-26 10:15:25] [INFO] Access from IP: 192.168.0.1 to URL: /home";
let regex = /\b(?:\d{1,3}\.){3}\d{1,3}\b/;
let match = logText.match(regex);
if (match) {
let ipAddress = match[0];
console.log(ipAddress); // 输出: 192.168.0.1
} else {
console.log("No IP address found.");
}

非捕获型分组


非捕获型分组是正则表达式中的一种分组语法,用于对一组子表达式进行逻辑组合,但不会捕获匹配的结果。它以 (?: 开始,并以 ) 结束。


/\b(?:\d{1,3}\.){3}\d{1,3}\b/

解释一下这个正则表达式:



  • \b:单词边界,用于确保 IP 地址被完整匹配。

  • (?:\d{1,3}\.){3}:非捕获型分组,匹配由 1 到 3 个数字和一个点组成的序列,重复匹配 3 次,用于匹配 IP 地址的前三个数字和点的部分。

  • \d{1,3}:匹配由 1 到 3 个数字组成的序列,用于匹配 IP 地址的最后一个数字。

  • \b:单词边界,用于确保 IP 地址被完整
    作者:simple_lau
    来源:juejin.cn/post/7248832185808617509
    匹配。

收起阅读 »

从张鑫旭大佬文章中发现了我前端知识的匮乏

web
最近翻看张鑫旭大佬的博客,发现了一篇叫《前端原生API实现条形码二维码的JS解析识别》的文章,觉得很不错,于是就把大佬的代码拷贝下来学习了下,结果就是看的我一脸懵,自信息大大受打击了。痛定思痛,于是把其中觉得有意思的地方记录下,整理成此文。 我们先看下页面是怎...
继续阅读 »

最近翻看张鑫旭大佬的博客,发现了一篇叫《前端原生API实现条形码二维码的JS解析识别》的文章,觉得很不错,于是就把大佬的代码拷贝下来学习了下,结果就是看的我一脸懵,自信息大大受打击了。痛定思痛,于是把其中觉得有意思的地方记录下,整理成此文。


我们先看下页面是怎么样的:


chrome-capture-2023-5-26.gif


功能很简单,就是复制下面的二维码图片,然后粘贴到文本框中,最后点击识别按钮,把识别二维码的结果展示到下面。


源代码:


<!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>qrcode</title>
<style>
.area {
height: 200px;
border: 1px dashed skyblue;
background-color: #fff;
display: grid;
place-items: center;
margin-top: 20px;
}
.area:focus {
border-style: solid;
}
.area:empty::before {
content: '或粘贴图片到这里';
color: gray;
}
.button {
margin: 1rem auto;
width: 160px;
height: 40px;
font-size: 112.5%;
background-color: #eb4646;
color: #fff;
border: 0;
border-radius: 0.25rem;
margin-top: 1.5rem;
}
</style>
</head>
<body>
<div class="container">
<input id="file" class="file" type="file" accept="image/png" />
<div id="area" class="area" tabindex="-1"></div>
</div>
<p align="center">
<button id="button" class="button">识别</button>
</p>

<p id="result" align="center"></p>

<p align="center">
方便大家复制的示意图:<br /><img
src="./qrcode.png"
style="margin-top: 10px"
/>

</p>

<script>
var reader = new FileReader()
reader.onload = function (event) {
area.innerHTML = '<img src="' + event.target.result + '">'
}
document.addEventListener('paste', function (event) {
var items = event.clipboardData && event.clipboardData.items
var file = null
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
// 此时file就是剪切板中的图片文件
if (file) {
reader.readAsDataURL(file)
}
})

file.addEventListener('change', function (event) {
const file = event.target.files && event.target.files[0]
if (file) {
reader.readAsDataURL(file)
}
})

button.addEventListener('click', function () {
if ('BarcodeDetector' in window) {
// 创建检测器
const barcodeDetector = new BarcodeDetector({
formats: ['qr_code']
})

const eleImg = document.querySelector('#area img')
if (eleImg) {
barcodeDetector
.detect(eleImg)
.then(barcodes => {
console.log('barcodes', barcodes)
barcodes.forEach(barcode => {
result.innerHTML = `<span class="success">解析成功,结果是:</span>${barcode.rawValue}`
})
})
.catch(err => {
result.innerHTML = `<span class="error">解析出错:${err}</span>`
})
} else {
result.innerHTML = `<span class="error">请先粘贴二维码图片</span>`
}
} else {
result.innerHTML = `<span class="error">当前浏览器不支持二维码识别</span>`
}
})
</script>
</body>
</html>

背景交代完成,现在就一点一点的来分析其中代码的精妙之处。


CSS部分


tabindex = -1


<div id="area" class="area" tabindex="-1"></div>

当我看到tabindex这个属性时,完全不知道它的用法,于是我继续在张鑫旭大佬的博客中搜索,找到一篇叫《HTML tabindex属性与web网页键盘无障碍访问》的文章,这里简要说下这个属性的用法和作用。


tabindex属性是一个全局属性,也就是所有 HTML 标签都可以用的属性,比方说idclass属性等。所以,可以在div上使用。同时,这个属性是一个非常老的属性,没有兼容性问题,放心使用。


tabindex属性是一个与键盘访问行为息息相关的属性。平常可能感觉不到它的价值,但是一旦我们的鼠标坏掉了或者没电了,我们就只能使用键盘。亦或者在电视机上,或者投影设备上访问我们的网页的时候,我们只能使用遥控器。就算设备都完全正常,对于资深用户而言,键盘访问可以大大提高我们的使用效率。


当一个元素设置tabindex属性值为-1的时候,元素会变得focusable,所谓focusable指的是元素可以被鼠标或者JS focus,在 Chrome 浏览器下表现为会有outline发光效果,IE浏览器下是虚框,同时能够响应focus事件。默认的focusable元素有<a>, <area>, <button>, <input>, <object>, <select> 以及 <textarea>


但是,tabindex = -1不能被键盘的tab键进行focus。这种鼠标可以focus,但是键盘却不能focus的状态,只要tabindex属性值为负值就可以了。


因此,我们可以设置divfocus的样式,当鼠标点击div时,我们可以改变它的边框,如下:


.area:focus {
border-style: solid;
}

tabindex属性值是一个整数,它来决定被tabfocus的顺序,顺序越小越先被focus,但是 0除外,如下divfocus的顺序依次是:1,2,3。


<div id="area" class="area" tabindex="1"></div>
<div class="area" tabindex="3"></div>
<div class="area" tabindex="2"></div>

tabindex="0"又是怎么回事呢?


元素设置tabindex="-1",可以鼠标和JS可以focus,但键盘不能focus


tabindex="0"tabindex="-1"的唯一区别就是键盘也能focus,但是被focus的顺序是最后的。或者你可以这么理解,<div>设置了tabindex="0",从键盘访问的角度来讲,相对于<div>元素变成了<button>元素。


垂直居中


垂直居中是一个常用的需求了,我经常使用flex来完成:


display: flex;
align-items: center;
justify-content: center;

在大佬的文章中使用了一个新的用法:


display: grid;
place-items: center;

place-items 属性是以下属性的简写:align-itemsjustify-items


:empty::before


div元素没有内容时,.area:empty样式会生效,同时为了显示一段提示内容,使用了伪元素::before,在content写入提示内容。


.area:empty::before {
content: '或粘贴图片到这里';
color: gray;
}

JS部分


copy paste 事件


document.addEventListener('paste', function (event) {
var items = event.clipboardData && event.clipboardData.items
var file = null
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
// 此时file就是剪切板中的图片文件
if (file) {
reader.readAsDataURL(file)
}
})

这两个事件都属于ClipboardEvent事件(剪切板事件),还有一个cut剪切事件。


wrap.oncopy = function(event){}
wrap.oncut = function(event){}
wrap.onpaste = function(event) {}

任何软件上的内容,可以被复制粘贴,是因为软件对操作系统复制粘贴操作的实现,软件都会把复制剪切的内容存入操作系统的剪切板上。同样,浏览器也对操作系统的剪切板进行了实现,属于浏览器的自身的实现。


浏览器复制操作的默认行为是触发浏览器的 copy 事件,将 copy 的内容存入操作系统的剪切板中。


那如何干预浏览器的这种默认的复制粘贴操作呢?


可以通过event.preventDefault阻止事件的默认行为,即当触发这三个事件时,阻止对系统剪切板的数据操作。然后,我们对数据进行加工后,重新写入到剪贴板。


比如,当用户复制我们网站的内容时,可以在数据后面加一个版权的相关信息。


<div id="wrap">这是复制的复制内容</div>
<script>
var wrap = document.getElementById('wrap')
wrap.oncopy = function (event) {
// 通过copy事件监听,阻止将选中内容复制到系统剪切板上
event.preventDefault()
// 获取选中内容对象
const selection = document.getSelection()
// selection对象重构了toSring()方法,获取selection对象的选中内容
var selectContent = selection.toString()
var dealContent =
selectContent +
'转载请联系作者,内容地址:xxxxx'
// 把重写后的内容写入到剪贴板
event.clipboardData.setData('text/plain', dealContent)
}
</script>


ClipboardEvent 事件有个最重要的属性clipboardData,该属性值是DataTransfer对象,这个对象在拖拽场景中经常使用,后面会专门写一篇文章来说说这个对象。


new BarcodeDetector解析二维码


// 创建检测器
const barcodeDetector = new BarcodeDetector({
formats: ['qr_code']
})
barcodeDetector.detect(eleImg)
.then(barcodes => {
console.log('barcodes', barcodes)
barcodes.forEach(barcode => {
result.innerHTML = `<span class="success">解析成功,结果是:</span>${barcode.rawValue}`
})
})
.catch(err => {
result.innerHTML = `<span class="error">解析出错:${err}</span>`
})

浏览器提供了原生的API来解析二维码和条形码,即 Barcode Detection API


formats表示要解析那种码,如下图所示:


image.png


总结


通过学习上面的代码,可以发现自己在 css,js 方面上的不足,原因是缺乏探索性,老是用已有的知识来解决问题,或者直接去 github 上找第三方库,其实可以使

作者:小p
来源:juejin.cn/post/7248874230862233655
用最简单的方式实现。

收起阅读 »

从 0 到 1 实现一个 Terminal 终端

web
前言 之前在我自己的项目中 打造属于你自己的 Mac(Next.js+Nest.js TS全栈项目)有同学问Terminal 组件是怎么实现的呢,现在我们就用 React+TS 写一个支持多种命令的 Terminal 终端吧。 每一步骤后都有对应的 com...
继续阅读 »

前言



之前在我自己的项目中 打造属于你自己的 Mac(Next.js+Nest.js TS全栈项目)有同学问Terminal 组件是怎么实现的呢,现在我们就用 React+TS 写一个支持多种命令的 Terminal 终端吧。



每一步骤后都有对应的 commit 记录;


源码地址:github.com/ljq0226/my-… 欢迎 Star ⭐️⭐️⭐️


体验地址: my-terminal.netlify.app/



搭建环境


我们使用 vite 构建项目,安装所需要的依赖库:



  • @neodrag/react (拖拽)

  • tailwindcss

  • lucide-react (图标)
    步骤:

  • pnpm create vite

  • 选择 React+TS 模版

  • 安装依赖:pnpm install @neodrag/react lucide-react && pnpm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p
    配置 tailwind.config.js:


/** @type {import('tailwindcss').Config} */
export default {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
}


仓库代码:commit1


开发流程


搭建页面


image.png


以上是终端的静态页面,样式这里就不在详细展开了,此次代码仓库 commit2 。 接下来我们为该终端添加拖拽效果:


//App.tsx
···
import type { DragOptions } from '@neodrag/react'
import { useRef, useState } from 'react'

function APP(){
const [position, setPosition] = useState({ x: 0, y: 0 })
const draggableRef = useRef(null)
// 初始化 dragable 拖拽设置
const options: DragOptions = {
position,
onDrag: ({ offsetX, offsetY }) => setPosition({ x: offsetX, y: offsetY }),
bounds: { bottom: -500, top: 32, left: -600, right: -600 },
handle: '.window-header',
cancel: '.traffic-lights',
}
useDraggable(draggableRef, options)

}

return (
<div ref={draggableRef}> //将 draggableRef 挂在到节点上

</div>

)
···

这样我们的 Terminal 终端就有了拖拽效果,其它 API 方法在@neodrag/react 官网中,代码仓库 commit3


terminal2.gif


输入命令


一个终端最重要的当然是输入命令了,在这我们使用 input 框来收集收集输入命令的内容。
由于我们每次执行完一次命令之后,都会生成新的行,所以我们将新行封装成一个组件,Row 组件接收两个参数(id:当前 Row 的唯一标识;onkeydown:监听 input 框的操作):


// components.tsx
interface RowProps {
id: number
onkeydown: (e: React.KeyboardEvent<HTMLInputElement>) => void
}
const Row: React.FC<RowProps> = ({ id, onkeydown }) => {

return (
<div className='flex flex-col w-full h-12'>
<div>
<span className="mr-2 text-yellow-400">funnycoder</span>
<span className="mr-2 text-green-400">@macbook-pro</span>
<span className="mr-2 text-blue-400">~{dir}</span>
<span id={`terminal-currentDirectory-${id}`} className="mr-2 text-blue-400"></span>
</div>
<div className='flex'>
<span className="mr-2 text-pink-400">$</span>
<input
type="text"
id={`terminal-input-${id}`}
autoComplete="off"
autoFocus={true}
className="flex-1 px-1 text-white bg-transparent outline-none"
onKeyDown={onkeydown}
/>

</div>

</div>

)
}

一开始的时候,我们通过初始化一个 Row 进行操作,我们所有生成的 Row 通过


//app.tsx
const [content, setContent] = useState<JSX.Element[]>(
[<Row
id={0}
key={key()} // React 渲染列表时需要key
onkeydown={(e: React.KeyboardEvent<HTMLInputElement>
) => executeCommand(e, 0)}
/>,
])

content 变量来存储,在后续我们经常要修改 content 的值,为了简化代码我们为 setContent 封装成 generateRow 方法:


// 生成内容
const generateRow = (row: JSX.Element) => {
setContent(s => [...s, row])
}

问题来了,当我们获取到了输入的命令时,怎么执行对应的方法呢?


每一个 Row 组件都有 onKeyDown事件监听,当按下按键时就调用 executeCommand 方法,通过 input 框的 id 获取该 input 框 dom 节点, const [cmd, args] = input.value.trim().split(' ') 获取执行命令 cmd 和 参数 args,此时根据 event.key 按键操作执行对应的方法:


 // 执行方法
function executeCommand(event: React.KeyboardEvent<HTMLInputElement>, id: number) {
const input = document.querySelector(`#terminal-input-${id}`) as HTMLInputElement
const [cmd, args] = input.value.trim().split(' ')
if (event.key === 'ArrowUp')
alert(`ArrowUp,Command is ${cmd} Args is ${args}`)

else if (event.key === 'ArrowDown')
alert(`ArrowDown,Command is ${cmd} Args is ${args}`)

else if (event.key === 'Tab')
alert(`Tab,Command is ${cmd} Args is ${args}`)

else if (event.key === 'Enter')
alert(`Enter,Command is ${cmd} Args is ${args}`)
}

接下来我们测试一下,输入cd desktop,按下 Enter 键:
terminal3.gif


代码仓库 commit3


构建文件夹系统


终端的最常用的功能就是操作文件,所以我们需要构建一个文件夹系统,起初,在我的项目中使用的是一个数组嵌套,类似下面这种


image.png


这种数据结构的话,每次寻找子项的都需要递归计算,非常麻烦。在这我们采用 map 进行存储,将数据扁平化:


image.png


代码仓库 commit4


执行命令


准备工作


我们先介绍一下几个变量:



  • currentFolderId :当前文件夹的 id,默认为 0 也就是最顶层的文件夹

  • currentDirectory : 当前路径

  • currentId : input 输入框的 id 标识


  const [currentId, setCurrentId] = useState<number>(0)
const [currentFolderId, setCurrentFolderId] = useState(0)
const [currentDirectory, setCurrentDirectory] = useState<string>('')

并把一些静态组件封装在 components.tsx 文件中:


image.png


核心介绍


我们用一个对象来存储需要执行对应的方法:


  const commandList: CommandList = {
cat,
cd,
clear,
ls,
help,
mkdir,
touch,
}

executeCommand 方法中,如果用户按下的是'Enter' 键,我们首先判断下输入的 cmd 是否在 commandlist 中,如果存在,就直接执行该方法,如果不存在,就生成一个 CommandNotFound
行:


//app.js 
function executeCommand(){
//...
else if (event.key === 'Enter') {
// 将新输入 command 加入 commandHistory 中
const newArr = commandHistory
newArr.push(input.value.trim())
setCommandHistory(newArr)
// 如果输入 command 符合就执行 ⭐️⭐️⭐️
if (cmd && Object.keys(commandList).includes(cmd))
commandList[cmd](args)
else if (cmd !== '')
generateRow(<CommandNotFound key={key()} command={input.value.trim()} />)
// 每次无论 command 符不符合,都需要生成一行新的 Row,并且 curentId++
setCurrentId(id => id + 1)
setTimeout(() => {
generateRow(
<Row
key={key()}
id={commandHistory.length}
onkeydown={(e: React.KeyboardEvent<HTMLInputElement>
) => executeCommand(e, commandHistory.length)}
/>,
)
}, 100)
}
//...
}

help


当输入的 cmd 识别为'help'时就会调用该方法,生成在 components.tsx 里 Help()中定义好的静态数据:


  // help 命令
const help = () => {
generateRow(<Help key={key()} />)
}

代码仓库:commit5


cd


首先,默认的currentFolderId为 0,也就是指向我们的根文件夹,我们可以通过 folderSysteam.get(currentFolderId) 来获取当前文件夹下的信息,包括该文件夹的 title,子文件的 id 数组 childIds
当我们获取到了参数 arg 时,首先要判断 是否为空或者'..',若是的话,即返回上一层目录,
如果是正常参数的话,通过 folderSysteam.get(currentFolderId) 获取子目录的 childIds 数组,遍历当前目录下的子目录,找到子目录中 title 和 arg 一样的目录并返回该子目录 id,将 currentFolderId 设置为该子目录 id 并且拼接文件路径:


  // cd 命令
const cd = (arg = '') => {
const dir: string = localStorage.getItem(CURRENTDIRECTORY) as string
//判断是否返回上一层目录
if (!arg || arg === '..') {
// 处理文件路径
const dirArr = dir.split('/')
dirArr.length = Math.max(0, dirArr.length - 2)
//区分是否是root层
if (!dirArr.length)
setCurrentDirectory(`${dirArr.join('')}`)
else
setCurrentDirectory(`${dirArr.join('')}/`)
// 将当前目录设置为上一层目录
setCurrentFolderId(folderSysteam.get(`${currentFolderId}`)?.parentId as number)
return
}
//若是正常的跳转子目录
//根据 arg 参数获取需跳转目录的 id
const id = searchFile(arg)
// 如果子目录存在,设置路径、更新当前目录id
if (id) {
const res = `${dir + folderSysteam.get(`${id}`)?.title}/`
setCurrentFolderId(id)
setCurrentDirectory(res)
}
// 否则返回 NoSuchFileOrDirectory
else { generateRow(<NoSuchFileOrDirectory key={key()} command={arg}/>) }
}
const searchFile = (arg: string) => {
// 对输入做一个优化,例如文件夹名为 Desktop,只要我们输入'Desktop'|'desktop'|'DESKTOP'都行
const args = [arg, arg.toUpperCase(), arg.toLowerCase(), arg.charAt(0).toUpperCase() + arg.slice(1)]
// 获取当前目录下子目录
const childIds = getStorage(CURRENTCHILDIDS)
// 遍历子目录,找到title 为 arg 的目录
for (const item of folderSysteam.entries()) {
if (childIds.includes(item[1].id) && args.includes(item[1].title))
return item[1].id
}
}


ls


  // ls 命令
const ls = () => {
let res = ''
// 获取当前目录下所有子目录 id
const ids = getStorage(CURRENTCHILDIDS)
// 遍历 id 进行拼接
for (const id of ids)
res = `${res + folderSysteam.get(`${id}`)?.title} `
if (!res) {
generateRow(<div key={key()} >There are no other folders or files in the current directory.</div>)
}
else {
res.split(' ').map((item: string) =>
generateRow(<div key={key()} className={item.includes('.') ? 'text-blue-500' : ''}>{item}</div>),
)
}
}

terminal6.gif


代码仓库:commit6| commit6.1


mkdir、touch


创建文件或文件夹,我们只需要创建该文件或文件夹对象,新对象的 parentId 指向当前目录,其新 id 加入到当前目录的 childIds 数组中,最后再更新一下 folderSysteam 变量:


  // mkdir 命令
const mkdir = (arg = '') => {
const currentFolderId = getStorage(CURRENTFOLDERID)
const size = folderSysteam.size.toString()
// 创建新对象
const newFolderSysteam = folderSysteam.set(`${size}`, {
id: +size,
title: arg,
childIds: [],
parentId: currentFolderId,
})
// 更新 当前文件夹下的 childIds
const childIds = (folderSysteam.get(`${currentFolderId}`) as FolderSysteamType).childIds as number[]
childIds && childIds.push(+size)
setStorage(CURRENTCHILDIDS, childIds)
setFolderSysteam(newFolderSysteam)
}
// touch 命令
const touch = (arg = '') => {
const currentFolderId = getStorage(CURRENTFOLDERID)
const size = folderSysteam.size.toString()
// 创建新对象
const newFolderSysteam = folderSysteam.set(`${size}`, {
id: +size,
title: arg,
content: <div ><h1>
This is <span className='text-red-400 underline'>{arg}</span> file!
</h1>
<p>Imagine there's a lot of content here...</p>
</div>
,
parentId: currentFolderId,
})
// 更新 当前文件夹下的 childIds
const childIds = (folderSysteam.get(`${currentFolderId}`) as FolderSysteamType).childIds as number[]
childIds && childIds.push(+size)
setStorage(CURRENTCHILDIDS, childIds)
setFolderSysteam(newFolderSysteam)
}


terminal7.gif


代码仓库:commit7


cat、clear


cat 命令只需要展示子文件的 content 属性值即可:


  // cat 命令
const cat = (arg = '') => {
//获取当前目录下 childIds 进行遍历
const ids = getStorage(CURRENTCHILDIDS)
ids.map((id: number) => {
const item = folderSysteam.get(`${id}`) as FolderSysteamType
//生成 title 为 arg 文件的 content Row 行
return item.title === arg ? generateRow(<div key={key()}>{item.content}</div> as JSX.Element) : ''
})
}

clear 命令只需要调用 setContent():


  // clear 命令
const clear = () => {
setContent([])
//清空 input 框内容
const input = document.querySelector('#terminal-input-0') as HTMLInputElement
input.value = ''
}

terminal8.gif
代码仓库:commit8


其它操作


准备工作


我们先介绍一下几个变量:



  • commandHistory : 用于存储输入过的 command数组

  • changeCount : 用来切换 command 计数


  const [changeCount, setChangeCount] = useState<number>(0)
const [commandHistory, setCommandHistory] = useState<string[]>([])

上下键切换 command


上面定义的 changeCount 变量默认为 0,当我们按上🔼键时,changeCount-1,当我们按下🔽键时,changeCount+1。
而当 changeCount 变量变化时,获取当前 input dom 节点,设置其值为commandHistory[commandHistory.length + changeCount],这样我们的上下键切换 command 就实现了:


    // 当按下上下键时 获取历史 command
useEffect(() => {
const input = document.querySelector(`#terminal-input-${commandHistory.length}`) as HTMLInputElement
if (commandHistory.length)
input.value = commandHistory[commandHistory.length + changeCount]
if (!changeCount) {
input.value = ''
setChangeCount(0)
}
}, [changeCount])

// 按向上🔼键
function handleArrowUp() {
setChangeCount(prev => Math.max(prev - 1, -commandHistory.length))
}
// 按向下🔽键
function handleArrowDown() {
setChangeCount(prev => Math.min(prev + 1, 0))
}
// 执行方法
function executeCommand(...) {
//...
if (event.key === 'ArrowUp') {
handleArrowUp()
}
else if (event.key === 'ArrowDown') {
handleArrowDown()
}
//...

Tab 键补全 command


根据历史记录补全 command ,利用 Array.filter() 和 String.startsWith() 就行:


  // 匹配历史 command 并补充
const matchCommand = (inputValue: string): string | null => {
// 遍历历史command 返回以当前输入 command 值开头(startsWith)的 command
const matchedCommands = commandHistory.filter(command => command.startsWith(inputValue))
return matchedCommands.length > 0 ? matchedCommands[matchedCommands.length - 1] : null
}


代码仓库:commit9


最后


大家有兴趣的话可以自己再去二次改造或添加一些新玩法,此组件已通过 Netlify 部署上线,地址为 my-terminal.netlify.app/
项目源代码:github.com/ljq0226/my-… 欢迎 S

作者:Aphelios_
来源:juejin.cn/post/7248599585735098405
tar ⭐️⭐️⭐️

收起阅读 »

前端面试题 - 96. hash 和 history 的区别?

web
hash和history是Web开发中常用的两个概念,它们都与浏览器URL相关。 Hash(哈希) URL中以#符号开始的部分被称为哈希部分。在Web开发中,通常使用哈希来实现页面内的导航或锚点定位。当浏览器的哈希发生变化时,页面不会重新加载,而是触发一个ha...
继续阅读 »

hashhistory是Web开发中常用的两个概念,它们都与浏览器URL相关。


Hash(哈希)


URL中以#符号开始的部分被称为哈希部分。在Web开发中,通常使用哈希来实现页面内的导航或锚点定位。当浏览器的哈希发生变化时,页面不会重新加载,而是触发一个hashchange事件。


// 监听 hashchange 事件
window.addEventListener('hashchange', function() {
var currentHash = window.location.hash;

// 根据不同的哈希值执行相应的操作
if (currentHash === '#section1') {
console.log('显示第一部分的内容')
} else if (currentHash === '#section2') {
console.log('显示第二部分的内容')
} else {
console.log('其他操作')
}
});

通过监听此事件,你可以根据哈希的变化来执行相应的操作,例如显示不同的内容或调用特定的函数。哈希可以直接通过JavaScript进行修改,例如window.location.hash = "section2",URL将变为(此时hashchange事件也会触发):


https://example.com/page.html#section2
// 输出 显示第二部分的内容

History(历史记录)


历史记录是浏览器跟踪用户访问过的URL的一种机制。通过history对象,你可以在JavaScript中操作浏览器的历史记录。一些常用的方法包括history.pushState()history.replaceState()history.back()。这些方法允许你添加、替换和移动浏览器的历史记录,并且不会导致页面的实际刷新。当历史记录发生变化时,浏览器不会重新加载页面,但可以通过popstate事件来捕获这些变化并做出响应。


示例:


// 添加新的历史记录
history.pushState({ page: "page2" }, "Page 2", "page2.html");

// 监听 popstate 事件
window.addEventListener('popstate', function(event) {
var state = event.state;
console.log(state)
// 根据历史记录的变化执行相应的操作
if (state.page === "page1") {
console.log('显示第一页的内容')
} else if (state.page === "page2") {
console.log('显示第二页的内容')
} else {
console.log('其他操作')
}
});

需要注意的是,使用pushState()方法修改历史记录并不会触发popstate事件。只有在用户点击浏览器的前进或后退按钮时,或者通过JavaScript代码调用history.back()history.forward()history.go()方法导致历史记录变化时,popstate

作者:总瓢把子
来源:juejin.cn/post/7248608019851755575
e>事件才会被触发。

收起阅读 »

面试官: 既然有了 cookie 为什么还要 localStorage?😕😕😕

web
Web Storage Web Storage 最终是网页超文本应用技术工作组在 Web Applications 1.0 规范中提出的。这个规范中的草案最终成为了 HTML5 的一部分,后来有独立称为自己的规范。Web Storage 的目的是解决通过客户端...
继续阅读 »

Web Storage


Web Storage 最终是网页超文本应用技术工作组在 Web Applications 1.0 规范中提出的。这个规范中的草案最终成为了 HTML5 的一部分,后来有独立称为自己的规范。Web Storage 的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用 cookie 的问题。


Web Storage 规范最新的版本是第 2 版,这一版规范主要有两个目标:



  1. 提供在 cookie 之外的存储会话数据的途径;

  2. 提供跨会话持久化存储大量数据的机制;


Web Storage 定义了两个对象: localStoragesessionStorage。前者是永久存储机制,而后者是跨会话的存储机制。这两个浏览器存储 API 提供了在浏览器中不收页面刷新影响而存储数据的两种方式。


Storage 类型


Storage 类型用于保存 名/值 对数据,直至存储空间上限(由浏览器决定)。Storage 的实例与其他对象一样,但增加了以下方法:



  1. clear(): 删除所有值;

  2. getItem(name): 取得给定 name 值;

  3. key(index): 取得给定数值位置的名称;

  4. removeItem(name): 删除给定 name名/值 对;

  5. setItem(name,value): 设置给定 name 的值;


getItem()removeItem(name)setItem() 方法可以直接或间接通过 Storage 对象调用。因为每个数据项都作为属性存储在该对象上,所以可以使用点或括号操作符访问这些属性,统统同样的操作来设置值,也可以使用 delete 操作符来删除属性。即便如此,通常还是建议使用方法而非属性来执行这些操作,以免意外重写某个已存在的对象成员。


localStorage 对象


在修订的 HTML5 规范里,localStorage 对象取代了 globalStorage,作为在客户端持久存储数据的机制,要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在想用的端口上使用相同的协议。


因为 localStorageStorage 的实例,所以可以像使用 sessionStorage 一样使用 localStorage。具体实例请看下面几个例子:


// 使用方法存储数据
localStorage.setItem("moment", 777);

// 使用属性存储数据
localStorage.nickname = "moment";

// 使用方法获取数据
const name = localStorage.getItem("moment");

// 使用属性获得数据
const nickname = localStorage.nickname;

两种存储方法的区别在于,存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存。localStorage 数据不受页面刷新影响,也不会因关闭窗口,标签也或重新启动浏览器而丢失。


存储事件


每当 Storage 对象发生变化时,都会在文档上触发 storage 事件,使用属性或者 setItem() 设置值、使用 deleteremoveItem() 删除值,以及每次调用 clean() 时都会触发这个事件,这个事件的事件对象有如下四个属性:



  1. domain: 存储变化对应的域;

  2. key: 被设置或删除的键;

  3. newValue: 键被设置的新值,若键被删除则为 null;

  4. oldValue: 键变化之前的值。


我们可以使用如下代码监听 storage 事件:


window.addEventListener("storage", function (e) {
document.querySelector(".my-key").textContent = e.key;
});

对于 sessionStoragelocalStorage 上的任何更改都会触发 storage 事件,但 storage 事件不会区分这两者。


这是一道面试题


在不久前,被问到这样一个问题,我们通过后端返回来的 token 为什么是存储在 localStorage 而不是存储在 cookie 中?


考虑这个问题的首先我们应该知道,token 就是一个字符串,而使用 cookie 的话,大小是满足的,所以考察的点就不在这个内存上面了。


之所以使用 localStorage 存储 token,而不是使用 cookie,这可能基于以下几个方面考虑:



  1. 前后端分离架构: 在一些现代的 Web 应用程序中,前端和后端通常是通过 API 进行通信的,而不是使用传统的服务器端渲染。在这种情况下,前端可能是一个独立的应用程序,如基于 JavaScript 的单页应用或移动应用程序。由于前端和后端是分离的,Cookie 在这种架构中不太容易管理,因为跨域请求可能会遇到一些限制。localStorage 提供了一种更方便的解决方案,前端应用程序可以直接访问和管理存储在本地的令牌;

  2. 安全性需求: 在某些情况下,开发者可能认为将令牌存储在 Cookie 中存在一些安全风险,尤其是在面对跨站脚本攻击 XSS 时。使用 localStorage 可以减少某些安全风险,因为 LocalStorage 中的数据不会自动发送到服务器,且可以通过一些安全措施(如加密)来增强数据的安全性;

  3. 令牌过期处理: 使用 localStorage 存储令牌可以让令牌在浏览器关闭后仍然保持有效,这在某些应用场景下是有用的。例如,用户可能关闭了浏览器,然后再次打开时仍然保持登录状态,而不需要重新输入凭据;


值得注意的是,使用 localStorage 存储 token 也不是说百分百安全的,依然会存在一些问题和风险,如容易收到 XSS 攻击、不支持跨域贡献等。因此,在使用 localStorage 存储令牌时,开发者需要采取适当的安全措施,如加密存储数据、定期更新令牌等,以确保令牌的安全性和有效性。


localStorage 如何实现跨域


localStorage 是一直域限制的存储机制,通常只能在同一域名下的页面中访问。这意味着默认情况下,localStorage 的数据在不同域名或跨域的情况下是无法直接访问的。然而,有几种方法可以实现跨域访问 localStorage 中的数据:



  1. 域名映射(Domain Mapping): 将不同域名都指向同一个服务器 IP 地址。这样不同域名下的页面就可以共享同一个 localStorage 中的数据;

  2. postMessage API: postMessage 是一种浏览器提供的 API,用于在不同窗口或跨域的 iframe 之间进行安全的消息传递。你可以在不同域名的页面中使用 postMessage 将数据从一个窗口传递到另一个窗口,并在目标窗口中将数据存储到 localStorage 中;


使用 postMessage 将数据从一个窗口传递到另一个窗口,并在目标窗口中将数据存储到 localStorage 中,实例代码如下:


// 发送消息到目标窗口
window.postMessage(
{ key: "token", value: "1233211234567" },
"https://liangzai.com"
);

在接收消息的窗口中:


// 监听消息事件
window.addEventListener("message", function (event) {
if (event.origin === "https://sourcedomain.com") {
// 存储数据到 LocalStorage
localStorage.setItem(event.data.key, event.data.value);
}
});

这些方法提供了一些途径来实现跨域访问 localStorage 中的数据。具体选择哪种方法取决于你的需求和应用场景,以及你对目标域名的控制程度。需要注意的是,安全性是非常重要。


cookie 和 localStorage 的区别


CookieLocalStorage 是两种用于在浏览器中存储数据的机制,它们在以下方面有一些区别:



  1. 存储容量: Cookie 的存储容量通常较小,每个 Cookie 的大小限制在几 KB 左右。而 LocalStorage 的存储容量通常较大,一般限制在几 MB 左右。因此,如果需要存储大量数据,LocalStorage 通常更适合;

  2. 数据发送: Cookie 在每次 HTTP 请求中都会自动发送到服务器,这使得 Cookie 适合用于在客户端和服务器之间传递数据。而 localStorage 的数据不会自动发送到服务器,它仅在浏览器端存储数据,因此 LocalStorage 适合用于在同一域名下的不同页面之间共享数据;

  3. 生命周期:Cookie 可以设置一个过期时间,使得数据在指定时间后自动过期。而 LocalStorage 的数据将永久存储在浏览器中,除非通过 JavaScript 代码手动删除;

  4. 安全性:Cookie 的安全性较低,因为 Cookie 在每次 HTTP 请求中都会自动发送到服务器,存在被窃取或篡改的风险。而 LocalStorage 的数据仅在浏览器端存储,不会自动发送到服务器,相对而言更安全一些;


总结


Cookie 适合用于在客户端和服务器之间传递数据、跨域访问和设置过期时间,而 LocalStorage 适合用于在同一域名下的不同页面之间共享数据、存储大量数据和永久存储数据。选择使用哪种机制应根据具体的需

作者:Moment
来源:juejin.cn/post/7248623545219825723
求和使用场景来决定。

收起阅读 »

在高德地图中实现降雨图层

web
前言 有一天老板跑过来跟我说,我们接到一个水利局的项目,需要做一些天气效果,比如说降雨、河流汛期、洪涝灾害影响啥的,你怎么看。欸,我觉得很有意思,马上开整。 需求说明 在地图上实现降雨效果,画面尽量真实,比如天空、风云的变化与降雨场景契合; 可以结合当地天气预...
继续阅读 »

前言


有一天老板跑过来跟我说,我们接到一个水利局的项目,需要做一些天气效果,比如说降雨、河流汛期、洪涝灾害影响啥的,你怎么看。欸,我觉得很有意思,马上开整。


需求说明


在地图上实现降雨效果,画面尽量真实,比如天空、风云的变化与降雨场景契合;


可以结合当地天气预报情况,自动调节风速、风向、降雨量等参数。


需求分析


方案一:全局降雨


在用户视口面前加一层二维的降雨平面层。


优点: 只管二维图层就行了,不需要与地图同步坐标,实现起来比较简单,界面是全局的一劳永逸。


缺点:只适合从某些角度观看,没法再做更多定制了。


Honeycam_2023-06-16_11-10-37.gif


方案二:局部地区降雨


指定降雨范围,即一个三维空间,坐标与地图底图同步,仅在空间内实现降雨。


优点:降落的雨滴有远近关系,比较符合现实场景;可适用各种地图缩放程度。


缺点:需要考虑的参数比较多,比如降雨范围一项就必须考虑这个三维空间是什么形状,可能是立方体、圆柱体或者多边形挤压体;需要外部图层的配合,比如说下雨了,那么天空盒子的云层、建筑图层的明度是否跟着调整。


Honeycam_2023-06-16_11-20-08.gif


实现思路


根据上面利弊权衡,我选择了方案二进行开发,并尽量减少输入参数,降雨影响范围初步定为以地图中心为坐标中心的立方体,忽略风力影响,雨滴采用自由落体方式运动。


降雨采用自定义着色器的方式实现,充分利用GPU并行计算能力,刚好在网上搜到一位大佬写的three演示代码,改一下坐标轴(threejs空间坐标轴y轴朝上,高德GLCustomLayer空间坐标z轴朝上)就可以直接实现最基础的效果。这里为了演示方便增加坐标轴和影响范围的辅助线。


1.创建影响范围,并在该范围内创建降雨层的几何体Geometry,该几何体的构成就是在影响范围内随机位置的1000个平面,这些平面与地图底面垂直;


Honeycam_2023-06-24_15-40-31.gif


2.创建雨滴材质,雨滴不受光照影响,这里使用最基础的MeshBasicMaterial材质即可,半透明化且加上一张图片作为纹理;


Honeycam_2023-06-24_15-50-32.gif


3.为实现雨滴随着时间轴降落的动画效果,需要调整几何体的形状尺寸,并对MeshBasicMaterial材质进行改造,使其可以根据当前时间time改变顶点位置;


Honeycam_2023-06-24_16-01-39.gif



  1. 调整顶点和材质,使其可以根据风力风向改变面的倾斜角度和移动轨迹;


Honeycam_2023-06-24_16-16-52.gif



  1. 将图层叠加到地图3D场景中


Honeycam_2023-06-24_16-28-46.gif


基础代码实现


为降低学习难度,本模块只讲解最基础版本的降雨效果,雨滴做自由落体,忽略风力影响。这里的示例以高德地图上的空间坐标轴为例,即z轴朝上,three.js默认空间坐标系是y轴朝上。我把three.js示例代码演示放到文末链接中。


1.创建影响范围,并在该范围内创建降雨层的几何体Geometry


createGeometry () {
// 影响范围:只需要设定好立方体的size [width/2, depth/2, height/2]
//
const { count, scale, ratio } = this._conf.particleStyle
// 立方体的size [width/2, depth/2, height/2]
const { size } = this._conf.bound
const box = new THREE.Box3(
new THREE.Vector3(-size[0], -size[1], 0),
new THREE.Vector3(size[0], size[1], size[2])
)

const geometry = new THREE.BufferGeometry()
// 设置几何体的顶点、法线、UV
const vertices = []
const normals = []
const uvs = []
const indices = []

// 在影响范围内随机位置创建粒子
for (let i = 0; i < count; i++) {
const pos = new THREE.Vector3()
pos.x = Math.random() * (box.max.x - box.min.x) + box.min.x
pos.y = Math.random() * (box.max.y - box.min.y) + box.min.y
pos.z = Math.random() * (box.max.z - box.min.z) + box.min.z

const height = (box.max.z - box.min.z) * scale / 15
const width = height * ratio

// 创建当前粒子的顶点坐标
const rect = [
pos.x + width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z - height / 2,
pos.x + width,
pos.y,
pos.z - height / 2
]

vertices.push(...rect)

normals.push(
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z
)

uvs.push(1, 1, 0, 1, 0, 0, 1, 0)

indices.push(
i * 4 + 0,
i * 4 + 1,
i * 4 + 2,
i * 4 + 0,
i * 4 + 2,
i * 4 + 3
)
}

// 所有顶点的位置
geometry.setAttribute(
'position',
new THREE.BufferAttribute(new Float32Array(vertices), 3)
)
// 法线信息
geometry.setAttribute(
'normal',
new THREE.BufferAttribute(new Float32Array(normals), 3)
)
// 设置UV属性与顶点顺序一致
geometry.setAttribute(
'uv',
new THREE.BufferAttribute(new Float32Array(uvs), 2)
)
// 设置基本单元的顶点顺序
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1))

return geometry
}

2.创建材质


createMaterial () {
// 粒子透明度、贴图地址
const { opacity, textureUrl } = this._conf.particleStyle
// 实例化基础材质
const material = new THREE.MeshBasicMaterial({
transparent: true,
opacity,
alphaMap: new THREE.TextureLoader().load(textureUrl),
map: new THREE.TextureLoader().load(textureUrl),
depthWrite: false,
side: THREE.DoubleSide
})

// 降落起点高度
const top = this._conf.bound.size[2]

material.onBeforeCompile = function (shader, renderer) {
const getFoot = `
uniform float top; // 天花板高度
uniform float bottom; // 地面高度
uniform float time; // 时间轴进度[0,1]
#include <common>
float angle(float x, float y){
return atan(y, x);
}
// 让所有面始终朝向相机
vec2 getFoot(vec2 camera,vec2 normal,vec2 pos){
vec2 position;
// 计算法向量到点的距离
float distanceLen = distance(pos, normal);
// 计算相机位置与法向量之间的夹角
float a = angle(camera.x - normal.x, camera.y - normal.y);
// 根据点的位置和法向量的位置调整90度
pos.x > normal.x ? a -= 0.785 : a += 0.785;
// 计算投影值
position.x = cos(a) * distanceLen;
position.y = sin(a) * distanceLen;

return position + normal;
}
`

const begin_vertex = `
vec2 foot = getFoot(vec2(cameraPosition.x, cameraPosition.y), vec2(normal.x, normal.y), vec2(position.x, position.y));
float height = top - bottom;
// 计算目标当前高度
float z = normal.z - bottom - height * time;
// 落地后重新开始,保持运动循环
z = z + (z < 0.0 ? height : 0.0);
// 利用自由落体公式计算目标高度
float ratio = (1.0 - z / height) * (1.0 - z / height);
z = height * (1.0 - ratio);
// 调整坐标参考值
z += bottom;
z += position.z - normal.z;
// 生成变换矩阵
vec3 transformed = vec3( foot.x, foot.y, z );
`

shader.vertexShader = shader.vertexShader.replace(
'#include <common>',
getFoot
)
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
begin_vertex
)
// 设置着色器参数的初始值
shader.uniforms.cameraPosition = { value: new THREE.Vector3(0, 0, 0) }
shader.uniforms.top = { value: top }
shader.uniforms.bottom = { value: 0 }
shader.uniforms.time = { value: 0 }
material.uniforms = shader.uniforms
}

this._material = material

return material
}

3.创建模型



createScope () {
const material = this.createMaterial()
const geometry = this.createGeometry()

const mesh = new THREE.Mesh(geometry, material)

this.scene.add(mesh)

// 便于调试,显示轮廓
// const box1 = new THREE.BoxHelper(mesh, 0xffff00)
// this.scene.add(box1)
}

4.更新参数


// 该对象用于跟踪时间
_clock = new THREE.Clock()

update () {
const { _conf, _time, _clock, _material, camera } = this

// 调整时间轴进度,_time都值在[0,1]内不断递增循环
// particleStyle.speed为降落速度倍率,默认值1
// _clock.getElapsedTime() 为获取自时钟启动后的秒数
this._time = _clock.getElapsedTime() * _conf.particleStyle.speed / 2 % 1

if (_material.uniforms) {
// 更新镜头位置
_material.uniforms.cameraPosition.value = camera.position
// 更新进度
_material.uniforms.time.value = _time
}
}

animate (time) {
if (this.update) {
this.update(time)
}
if (this.map) {
// 叠加地图时才需要
this.map.render()
}
requestAnimationFrame(() => {
this.animate()
})
}

优化调整


修改场景效果


通过对图层粒子、风力等参数进行封装,只需简单地调整配置就可以实现额外的天气效果,比如让场景下雪也是可以的,广州下雪这种场景,估计有生之年只能在虚拟世界里看到了。


Honeycam_2023-06-24_17-00-11.gif


以下是配置数据结构,可供参考


const layer = new ParticleLayer({
map: getMap(),
center: mapConf.center,
zooms: [4, 30],
bound: {
type: 'cube',
size: [500, 500, 500]
},
particleStyle: {
textureUrl: './static/texture/snowflake.png', //粒子贴图
ratio: 0.9, //粒子宽高比,雨滴是长条形,雪花接近方形
speed: 0.04, // 直线降落速度倍率,默认值1
scale: 0.2, // 粒子尺寸倍率,默认1
opacity: 0.5, // 粒子透明度,默认0.5
count: 1000 // 粒子数量,默认值10000
}
})

添加风力影响


要实现该效果需要添加2个参数:风向和风力,这两个参数决定了粒子在降落过程中水平面上移动的方向和速度。



  1. 首先调整一下代码实际那一节步骤2运动的相关代码


const begin_vertex = `
...
// 利用自由落体公式计算目标高度
float ratio = (1.0 - z / height) * (1.0 - z / height);
z = height * (1.0 - ratio);
// 增加了下面这几行
float x = foot.x+ 200.0 * ratio; // 粒子最终在x轴的位移距离是200
float y = foot.y + 200.0 * ratio; // 粒子最终在y轴的位移距离是200
...
// 生成变换矩阵
vec3 transformed = vec3( foot.x, y, z );


  1. 如果粒子是长条形的雨滴,那么它在有风力影响的运动时,粒子就不是垂直地面的平面了,而是与地面有一定倾斜角度的平面,如图所示。


Untitled.png


我们调整调整一下代码实际那一节步骤1的代码,实现方式就是让每个粒子平面在创建之后,所有顶点绕着平面的法线中心轴旋转a角度。


本示例旋转轴(x, y, 1)与z轴(0,0,1)平行,这里有个技巧,我们在做平面绕轴旋转的时候先把平面从初始位置orgPos移到坐标原点,绕着z轴旋转后再移回orgPos,会让计算过程简单很多。


// 创建当前粒子的顶点坐标
const rect = [
pos.x + width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z - height / 2,
pos.x + width,
pos.y,
pos.z - height / 2
]

// 定义旋转轴
const axis = new THREE.Vector3(0, 0, 1).normalize();
//定义旋转角度
const angle = Math.PI / 6;
// 创建旋转矩阵
const rotationMatrix = new THREE.Matrix4().makeRotationAxis(axis, angle);

for(let index =0; index< rect.length; index +=3 ){
const vec = new THREE.Vector3(rect[index], rect[index + 1], rect[index + 2]);
//移动到中心点
vec.sub(new THREE.Vector3(pos.x, pos.y,pos.z))
//绕轴旋转
vec.applyMatrix4(rotationMatrix);
//移动到原位
vec.add(new THREE.Vector3(pos.x, pos.y, pos.z))
rect[index] = vec.x;
rect[index + 1] = vec.y;
rect[index + 2] = vec.z;
}

待改进的地方


本示例中有个需要完善的地方,就是加入了风力影响之后,如果绕垂直轴旋转一定的角度,会看到如下图的异常,雨点的倾斜角度和运动倾斜角度是水平相反的。


Honeycam_2023-06-24_21-06-51.gif


问题的原因是材质着色器中的“让所有面始终朝向相机”方法会一直维持粒子的倾斜状态不变,解决这个问题应该是调整这个方法就可以了。然而作为学渣的我还没摸索出来,果然可视化工程的尽头全是数学Orz。


相关链接


1.THREE.JS下雨进阶版,面只旋转Y轴朝向相机


http://www.wjceo.com/blog/threej…


2.演示代码在线DEMO


jsfiddle.net/gyrate

sky/5…

收起阅读 »

我有个气人的同事......

web
前段时间看到掘金上好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。 曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 ...
继续阅读 »

前段时间看到掘金上好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。




曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 console-custom。沉寂在个人仓库很久,前段时间看到别人也有类似仓库,也就想着把自己的也发出来。




其实,我个人不是很推荐在代码里 写 console.log 之类的来调试代码,更推荐去浏览器控制台去打断点来调试,更好的理清数据的流转,事件的先后顺序等。



背景


官方背景:



  • 方便大家调试代码的时候,在浏览器控制台输出自定义个性化日志。

  • 防止控制台输出密密麻麻的 console.log,一眼看不到想看的。

  • 防止某个气人的小伙伴老是使用 console.error,强迫症不允许。

  • ......


真实背景:


其实,是我之前有个小伙伴同事——“小白菜”(也是为啥函数名叫 blog 的原因之一,下边会看到),他调试代码,打印输出总是喜欢 console.error(),用完了还不自己清理,大家协同开发的时候,git pull 他的代码后,总是让人就很难受!看着一堆报错,一时半会看不清是程序自己的报错,还是调试的输出!强迫症就犯了!想骂街......


不......不......要冷静!



  • 编码千万行

  • 调试要输出

  • log不规范

  • 同事两行泪


效果


浏览器页面 page


tu1.jpg


浏览器控制台 console


image.png



有个重点、痛点是这个, console.log(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333); 打印的数据多的时候不换行,需要找半天,我专门给处理成 分行, 一行一行展示了,这样好看清数据。



这个工具库有以下几个特点:



  1. 支持输入多个数据,并分行打印出来,并能看出是个整体

  2. 支持自己修改自己的默认样式部分,配置自己的默认部分

  3. 支持额外的自定义部分,拓展用户更多的玩法

  4. ......


其实 console 由于可以自定义,其实会有很多玩法,我个人在此主要的思路是



  1. 一定要简单,因为 console.log 本身就很简单,尽量不要造成使用者心智负担。

  2. 就简单的默认定制一个彩色个性化的部分,能区分出来,解决那个气人同事所谓的痛点就好。

  3. 代码要少,不要侵入,不要影响用户的业务代码


源码


此处源码有借鉴 github 开源代码:github.com/Redstone-1/…


大家如有更多、更丰富的需求场景可去参考使用。


// src/utils/console-custom.js
const GourdBabyColorMap = new Map([
["1", "#FF0000"],
["2", "#FFA500"],
["3", "#FFFF00"],
["4", "#008000"],
["5", "#00FFFF"],
["6", "#0000FF"],
["7", "#800080"],
]);

const createBLog = (config) => {
const logType = config.logType || "default";
const username = config.username || "";
const logName = config.logName || "";
const usernameColor = config.usernameColor || "#41b883";
const logNameColor = config.logNameColor || "#35495e";
const padding = config.padding || 6;
const borderRadius = config.borderRadius || 6;
const fontColor = config.fontColor || "#FFFFFF";
const usernameStyle = config.usernameStyle || "";
const logNameStyle = config.logNameStyle || "";

const logTemplate = (username = "myLog", logName = "") =>
`${username ? '%c' + username : ''} ${logName ? '%c' + logName : ''} `;

const customLog = (...data) => {
console.log(
logTemplate(username, logName),
usernameStyle ? usernameStyle : `background: ${usernameColor}; padding: 6px; border-radius: 6px 0 0 6px; color: #fff`,
logNameStyle ? logNameStyle : `background: ${logNameColor}; padding: 6px; border-radius: 0 6px 6px 0; color: #fff`,
...data
);
};

const defaultLog = (...data) => {
const len = data.length;
if (len > 1) {
data.map((item, index) => {
let firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`
;
let secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`
;
if (index === 0) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: ${borderRadius}px 0 0 0;
color: ${fontColor}
`
;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: 0 ${borderRadius}px 0 0;
color: ${fontColor}
`
;
} else if (index === len -1) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 0 ${borderRadius}px;
color: ${fontColor}
`
;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 ${borderRadius}px 0;
color: ${fontColor}
`
;
}
console.log(
logTemplate(username, `数据${index+1}`),
firstStyle,
secondStyle,
item
);
});
} else {
const firstStyle = `
background: ${usernameColor};
padding: ${padding}px;
border-radius: ${borderRadius}px 0 0 ${borderRadius}px;
color: ${fontColor}
`
;

const secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 ${borderRadius}px ${borderRadius}px 0;
color: ${fontColor}
`
;

console.log(
logTemplate(username, logName),
firstStyle,
secondStyle,
...data
);
}
};

const log = (...data) => {
switch(logType) {
case 'custom':
customLog(...data)
break;
default:
defaultLog(...data)
}
};

return {
log,
};
};

export default createBLog

API


唯一API createBLog(对!简单!易用!用起来没有负担!)


import createBLog from '@/utils/console-custom'

const myLog = createBLog(config)

配置 config: Object


一次配置,全局使用。(该部分是借鉴开源代码重构了配置内容)


配置项说明类型默认值
logTypelog 日志类型default、customdefault
usernamelog 的主人,也就是谁打的日志string-
logNamelog 的名字,也就是打的谁的日志string-
usernameColorusername 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#41b883
logNameColorlogName 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#35495e
paddingusername 和 logName 内边距,单位 pxnumber6
borderRadiususername 和 logName 圆角边框,单位 pxnumber6
fontColorusername 和 logName 字体颜色string#FFFFFF
usernameStyleusername 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-
logNameStylelogName 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-

基本用法 default



也是默认用法(default),同时也是最推荐大家用的一种方法。



vue2 版本


// main.js
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

// 不需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$blog = myLog.log;

// 需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$nlog = (logName, ...data) => {
myLog.logName = logName;
myLog.log(...data);
};

// vue2 组件里边使用
// 同时输入多个日志数据,可帮用户按照行的形式分开,好一一对应看清 log
this.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
this.$blog(111231231231231);

this.$nlog("logName", 2212121212122);

vue3 版本


// main.ts
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

app.config.globalProperties.$blog = myLog.log;

// vue3 组件里边使用
import { getCurrentInstance } from 'vue'

export default {
setup () {
const { proxy } = getCurrentInstance()

proxy.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
proxy.$blog(111231231231231);

proxy.$nlog("logName", 2212121212122);
}
}

自定义用法 custom



这部分我没有很多玩法,下边的例子也是借鉴别人的,主要全靠用户自己扩展 css 样式了。做一套自己喜欢的样式。



// main.js

// ....
Vue.prototype.$clog = (logName, ...data) => {
myLog.logType = "custom";
myLog.logName = logName;
myLog.usernameStyle = `text-align: center;
padding: 10px;
background-image: -webkit-linear-gradient(left, blue,
#66ffff 10%, #cc00ff 20%,
#CC00CC 30%, #CCCCFF 40%,
#00FFFF 50%, #CCCCFF 60%,
#CC00CC 70%, #CC00FF 80%,
#66FFFF 90%, blue 100%);`
;
myLog.logNameStyle = `background-color: #d2d500;
padding: 10px;
text-shadow: -1px -1px 0px #e6e600,-2px -2px 0px #e6e600,
-3px -3px 0px #e6e600,1px 1px 0px #bfbf00,2px 2px 0px #bfbf00,3px 3px 0px #bfbf00;`
;
myLog.log(...data);
};

// 提供的其他 css 样式
myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 0px 0px 15px #00FFFF,0px 0px 15px #00FFFF,0px 0px 15px #00FFFF;`
;
myLog.logNameStyle = `background-color: gray;
color: #eee;
padding: 10px;
text-shadow: 5px 5px 0 #666, 7px 7px 0 #eee;`
;

myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 1px 1px 0px #0000FF,2px 2px 0px #0000FF,-1px -1px 0px #E31B4E,-2px -2px 0px #E31B4E;`
;
myLog.logNameStyle = `font-family: "Arial Rounded MT Bold", "Helvetica Rounded", Arial, sans-serif;
text-transform: uppercase;/* 全开大写 */
padding: 10px;
color: #f1ebe5;
text-shadow: 0 8px 9px #c4b59d, 0px -2px 1px #fff;
font-weight: bold;
letter-spacing: -4px;
background: linear-gradient(to bottom, #ece4d9 0%,#e9dfd1 100%);`
;
// ....

其中渐变色的玩法


myLog.usernameStyle = `background-image: linear-gradient(to right, #ff0000, #ff00ff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;
myLog.logNameStyle = `background-image: linear-gradient(to right, #66ff00 , #66ffff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;

其中输出 emoji 字符


this.$nlog("😭", 2212121212122);
this.$nlog("🤡", 2212121212122);
this.$nlog("💩", 2212121212122);
this.$nlog("🚀", 2212121212122);
this.$nlog("🎉", 2212121212122);
this.$nlog("🐷", 2212121212122);

小伙伴们你肯定还有什么好玩的玩法!尽情发挥吧!


最后


还是想极力劝阻那些用 console.error() 调试代码的人,同时也能尽量少用 console 来调试,可以选择控制台断点、编译器断点等。还是不是很推荐使用 console 来调试,不过本文也可以让大家知道,其实 console 还有这种玩法。如果写 JS 库的时候也可以使用,让自己

作者:Bigger
来源:juejin.cn/post/7248448028297855035
的库极具自己的特色。

收起阅读 »

常见 Node.js 版本管理器比较:nvm、Volta 和 asdf

web
常见 Node.js 版本管理器比较:nvm、Volta 和 asdf 随着 Node.js 的发展,能够管理不同版本的运行时以确保兼容性和稳定性非常重要。这就是 Node.js 版本管理器的用武之地!在本文中,我们将比较和对比三种流行的 Node.js 版本...
继续阅读 »

常见 Node.js 版本管理器比较:nvm、Volta 和 asdf


随着 Node.js 的发展,能够管理不同版本的运行时以确保兼容性和稳定性非常重要。这就是 Node.js 版本管理器的用武之地!在本文中,我们将比较和对比三种流行的 Node.js 版本管理器:nvm、voltaasdf 来帮助你为你的开发环境选择合适的版本管理器。


nvm


nvm(Node Version Manager)是最古老和最受欢迎的 Node.js 版本管理器之一,至今仍在积极维护。nvm 允许开发人员在一台机器上安装和管理多个版本的 Node.js。它还提供了一个方便的命令行界面,用于在可用版本之间切换。


nvm 的工作原理是将 Node.js 的每个版本安装到下的独立目录中。使用 nvm use 在版本之间切换时,它会更新环境变量以指向相应的目录。所以可以并行安装多个版本的 Node.js,并且每个版本都有自己的一组全局安装的软件包 ~/.nvm/versions/node/$PATH


nvm 的一个缺点是它只支持 Node.js。如果需要管理其他编程语言或工具,则需要使用单独的版本管理器。另外,nvm 需要手动安装和配置,这对于初学者来说可能有点难受。


要安装 nvm,可以使用以下命令:


curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

# or

wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

这将从官方 NVM GitHub 仓库下载并运行 NVM 安装脚本,该仓库将在你的系统上安装 NVM。安装完成后,你可以通过运行以下命令来验证是否已安装 NVM:


nvm --version

1.1 如何使用特定版本的 Node.js


要将特定版本的 Node.js 与 nvm 配合使用,你需要执行以下步骤:



  1. 列出可用的 Node.js 版本:要查看可以使用 nvm 安装的所有可用 Node.js 版本的列表,请运行以下命令:


nvm ls-remote


  1. 安装所需版本:要安装特定版本的 Node.js,例如版本 18,请使用以下命令:


nvm install 18


  1. 使用已安装的版本:安装所需的 Node.js 版本后,你可以通过运行以下命令来使用它:


nvm use 16

设置默认版本:如果要使用特定版本的 Node.js 默认情况下,可以使用以下命令将其设置为默认版本:


nvm alias default 18

Volta


volta 是一个较新的 Node.js 版本管理器,旨在简化 Node.js 和其他工具的安装和管理,在 2019 年出世,仍在积极开发中。Volta 采用了与 nvm 不同的方法:它不是管理 Node.js 的多个版本,而是管理项目及其依赖项。当你创建新项目时,volta 会自动检测所需的 Node.js 版本并为你安装它。


volta 还支持其他工具,如 Yarn 和 Rust,开箱即用(不仅仅是 Node.js!对于使用多种编程语言并需要单个工具来管理它们的开发人员来说,这使其成为一个不错的选择。与 nvm 一样,volta 提供了一个命令行界面,用于在 Node.js 版本之间切换,但它通过使用拦截对 node 可执行文件的调用的全局填充程序来实现。


要安装 volta,你可以使用以下命令:


curl https://get.volta.sh | bash

此命令将下载并执行安装 volta 的脚本。


如何使用特定版本的 Node.js



  1. 安装所需版本:安装 volta 后,你可以使用它创建一个新项目并使用 volta install 命令设置所需的 Node.js 版本,以下命令创建一个新项目并将所需的 Node.js 版本设置为 16.0.0:


volta install node@16.0.0


  1. 在该项目的上下文中运行命令:此命令使用所需版本的 Node.js 在项目的上下文中运行 app.js 文件。


volta run node app.js


  1. 切换版本:你也可以使用 Volta 在不同版本的 Node.js 之间切换。例如,要切换到版本 10.0.0,可以使用以下命令:


volta pin node@10.0.0


  1. 设置默认版本:最后,以下命令将你的环境切换为 Node.js 版本 16.0.0 并设置为 Node.js 的默认版本:


nvm alias default 16.0.0

volta 的一个潜在缺点是它仍然是一个相对较新的工具,因此它可能不像 nvm 那样经过实战测试,而且它的社区支持有限,插件和集成也较少。


ASDF


ASDF 是一个版本管理器,旨在成为“通用语言版本管理器”。在 2015 年出世,支持广泛的编程语言和工具,包括 Node.js。ASDF 设计为可扩展,因此可以轻松地添加新语言和工具的支持。


asdf 支持几种流行的编程语言,包括 Node.js,Ruby,Python,Elixir,Java,Rust,PHP,Perl,Haskell,R,Lua 和 Earlang。这意味着你可以在一个地方管理不同的语言版本!如果要在不同语言的项目之间切换,使 asdf 成为一个不错的选择。


与 volta 一样,ASDF 管理项目及其依赖项,而不是同一工具的多个版本。创建新项目时,asdf 会自动检测所需的 Node.js 版本并为你安装。asdf 提供了一个命令行界面,用于在 Node.js 版本以及其他工具之间切换。


asdf 的一个潜在缺点是它的设置可能比 nvm 或 volta 复杂一些。你需要安装多个插件来添加对不同语言和工具的支持,并且可能需要修改 shell 配置以正确使用 asdf。


下面是如何使用 ASDF 安装和使用特定 Node.js 版本的示例:



  1. 安装 ASDF:可以使用以下命令安装 ASDF:


brew install asdf


  1. 将 Node.js 插件添加到 ASDF:你必须安装插件才能将 Node.js 添加到你的项目中


asdf plugin add nodejs


  1. 安装 Node.js 版本 18:使用以下命令使用特定版本的 Node.js:


asdf install nodejs 18


  1. 使用特定版本:


asdf global nodejs 18

nvm,volta 和 asdf 之间的差异



  1. 目的: NVM,Volta 和 ASDF 有不同的用途。NVM 专注于管理多个版本的 Node.js。而 Volta 将 Node.js 版本管理和包管理结合在一个工具中。ASDF 是一个版本管理器,支持多种编程语言,包括 Node.js。

  2. 安装: NVM、Volta 和 ASDF 的安装过程不同。NVM 可以使用 curl 命令安装,而 Volta 要求你手动下载并安装它。ASDF 可以使用 Homebrew 等包管理器安装,也可以直接从 GitHub 下载。

  3. 配置: NVM、Volta 和 ASDF 的配置过程是不同的。NVM 要求你手动更新 shell 配置文件。Volta 不需要任何手动配置。ASDF 要求你手动设置所需的插件。

  4. 自动版本检测: Volta 是唯一通过读取项目的 package.json 文件自动检测项目所需的 Node.js 版本的版本管理器。

  5. 包管理: Volta 是唯一将 Node.js 版本管理和包管理结合在一个工具中的版本管理器。NVM 和 ASDF 仅管理 Node.js 版本。


相似之处



  1. 多节点.js版本: NVM、Volta 和 ASDF 都允许你在同一台机器上管理多个版本的 Node.js。

  2. 全局和本地 node.js 版本: 所有三个版本管理器都允许你全局或本地安装 Node.js 版本。

  3. 简单命令: NVM、Volta 和 ASDF 有简单的命令来管理 Node.js 版本。

  4. 兼容性: 所有三个版本管理器都与 macOS,Linux 和 Windows 操作系统兼容。

  5. 开源: NVM、Volta 和 ASDF 都是开源项目,这意味着它们可以免费使用,并且可以由社区贡献。

  6. 版本锁定: 所有三个版本管理器都允许你锁定特定项目的 Node.js 版本,确保所有团队成员使用相同的版本。


小结


总之,nvmVoltaasdf 都是很棒的 Node.js 版本管理器,可以帮助你更改,管理和更新 Node.js 的多个版本,还可以与新版本保持同步,包括 LTS 版本。Nvm 是最古老和最受欢迎的版本管理器之一,Volta 有不同的方法,它不是管理多个版本的 Node.js,而是管理项目及其依赖项,最后 asdf 管理不同的语言版本,如果你在使用不同语言的项目之间切换,这使得 asdf

作者:夏安君
来源:juejin.cn/post/7247543825535270968
是一个不错的选择。

收起阅读 »

强制缓存这么暴力,为什么不使用协商缓存😡😡😡

web
前段时间在看面经的时候,发现很多份面经中都被问到了 强缓存 和 协商缓存。因此我觉得有必要写一篇文章来好好聊聊这两者。 强缓存和协商缓存 浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档...
继续阅读 »

前段时间在看面经的时候,发现很多份面经中都被问到了 强缓存协商缓存。因此我觉得有必要写一篇文章来好好聊聊这两者。


强缓存和协商缓存


浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档,其中浏览器缓存就分为 强缓存协商缓存:



  1. 强缓存: 当浏览器在请求资源时,根据响应头中的缓存策略信息,判断是否使用本地缓存副本而无需发送请求到服务器。如果资源被标记为强缓存,浏览器会直接从本地缓存中加载资源,而不发送请求到服务器,从而提高页面加载速度并减轻服务器负载;

  2. 协商缓存: 协商缓存是一种缓存策略,用于在资源未过期但可能已经发生变化时,通过与服务器进行协商确定是否使用本地缓存。协商缓存通过在请求中发送特定的条件信息,让服务器判断资源是否有更新,并返回相应的状态码和响应头信息,以指示浏览器是否可以使用本地缓存;


所以根据以上所聊到的特点,浏览器缓存有以下几个方面的优点:



  1. 减少冗余的数据传输;

  2. 减少服务器负担;

  3. 加快客户端加载网页的速度;


20230624082050


浏览器会首先获取该资源缓存的 header 信息,然后根据 Cache-Controlexpires 来判断是否过期。


如图,在浏览器第一次发送请求后,需要再次发送请求时,它会经过以下几个步骤:




  1. 首先,浏览器发送请求到服务器,请求的资源可能是一个网页、css 文件、JavaScript 文件或者其他类型的文件;




  2. 当服务器接收到请求后,首先检查请求中的缓存策略,例如请求头中的 Cache-Controlexpires 字段;




  3. 如果资源被标记为强缓存,服务器会进行以下判断:



    • 如果缓存有效,即资源的过期时间未到达或过期时间在当前时间之后,服务器返回状态码为 200 ok,并在响应头中设置适当的缓存策略,例如设置 Cache-ControlExpires 字段,告诉浏览器可以使用本地缓存;

    • 如果缓存无效,即资源的过期时间已过或过期时间在当前时间之前,服务器返回新的资源,状态码为 200 ok,并在响应头中设置适当的缓存策略;




  4. 如果资源未被标记为强缓存或缓存验证失败,服务器进行协商缓存的判断:



    • 如果请求头中包含 If-Modified-Since 字段,表示浏览器之前缓存了该组员并记录了最后修改时间,服务器会根据资源的最后修改时间进行判断;

      • 如果资源的最后修改时间与 If-Modified-Since 字段的值相同或更早,服务器返回状态码 304 Not Modified,并在响应头中清除实际的响应头;

      • 如果资源的最后修改时间晚于 If-Modified-Since 字段的值,表示资源已经发生了变化,服务器返回新的资源,状态码为 200 ok,并在响应头中设置新的最后修改时间;



    • 如果请求头中包含 If--Match 字段,表示浏览器之前缓存了该资源并记录资源的 ETag 值,服务器会根据资源的 ETag 进行判断:

      • 如果资源的 ETagIf--Match 字段的值相同,服务器返回状态码 304 Not Modified,并在响应头中清除实际的响应体;

      • 如果资源的 ETagIf--Match 字段的值不同,表示资源已经发生了变化,服务器返回新的资源,状态码为 200 OK,并在响应头中设置新的 ETag;






  5. 浏览器接收到服务器的响应之后,根据状态码和响应头信息进行相应的处理:



    • 如果状态码为 200 OK,表示服务器返回了新的资源,浏览器使用新的资源并更新本地缓存;

    • 如果状态码为 304 Not Modified,表示资源未发生变化,浏览器使用本地缓存的副本;

    • 浏览器根据响应头中的缓存策略进行进一步处理:

      • 如果响应头中包含 Cache-Control 字段,浏览器根据其指令执行缓存策略。例如,如果响应头中的 Cache-Control 包含 no-cache,浏览器将不使用本地缓存,而是向服务器发送请求获得最新的资源;

      • 如果响应头中包含 Expires 字段,浏览器将与当前时间比较,判断资源的过期时间。如果过期时间已过,浏览器将不使用本地缓存,而是向服务器发送请求获取最新的资源;

      • 如果响应头中包含其他相关的缓存控制字段(如 ETag),浏览器可以根据这些字段进行更精确的缓存控制和验证;






其中,在上面的流程中,又有几个令人难懂的字段,主要有以下几个:



  1. ETag: 它是通过对比浏览器和服务器资源的特征值来决定是否要发送文件内容,如果一样就只发送 304 Not Modified;

  2. Expires: 设置过期时间,是绝对时间;

  3. Last-Modified: 以时刻作为标识,无法识别一秒内进行多次修改的情况,只要资源修改,无论内容是否发生实质性变化,都会将该资源返回客户端;

  4. If--Match: 当客户端发送 GET 请求时,如果之前已经键相用资源的请求时,并且服务器返回了 ETag,那么客户端可以将 ETag 的值添加到 If--Match 头中,当再次请求该资源时,客户端会将 If--Match 头发送给服务器,服务器收到请求之后,会检查 If--Match 投中的值是否与当前资源的 ETag 值匹配:

    • 如果匹配,则表示客户端所请求的资源没有发生变化,服务器会返回状态码 304 Not Modified,并且不返回实际的资源内容;

    • 如果 If--Match 头中的值与服务器上资源的 ETag 值不匹配,说明资源发生了变化,服务器会正常返回资源,并返回状态码 200 OK;




图解强缓存和协商缓存


在上面的内容中讲了这么多的理论, 你是否还是不太理解什么是 强缓存协商缓存 啊,那么接下来我们就用几张图片来弄清楚这两者的区别。


强缓存


强缓存就是文件直接从本地缓存中获取,不需要发送请求。


首次请求


20230624103449


当浏览器发送初次请求时,浏览器会向服务器发起请求,服务器接收到浏览器的请求后,返回资源并返回一个 Cache-Control 字段给客户端,在该字段中设置一些缓存相关的信息,例如最大过期时间。


再次请求


20230624103906


在前面的基础上,浏览器再次发送请求,浏览器一节接收到 Cache-Control 的值,那么这个时候浏览器它会首先检查它的 Cache-Control 是否过期,如果没有过期则直接从本地缓存中拉取资源,返回割到客户端,则无需再经过服务器。


缓存失效


20230624104233


强缓存有过期时间,那么就意味着总有一天缓存会失效,如果客户端的 Cache-Control 失效了,那么它就会像首次请求中一样,重新向服务器发起请求,之后服务器会再次返回资源和 Cache-Control 的值。


协商缓存


协商缓存也叫做对比缓存,服务端判断客户端的资源是否和服务端的一样,如果一样则返回 304,反之返回 200 和最新的资源。


初次请求


20230624112243


如果客户端是第一次向服务器发出请求,则服务器返回资源和对应的资源标识给浏览器,该资源标识就是对当前所返回资源的唯一标识,可以是 ETag 或者是 Last-Modified


之后如果浏览器再次发送请求是,浏览器就会带上这个资源表,此时服务端就会通过这个资源标识,可以判断出浏览器的资源跟服务器此时的资源是否一致,如果一致则返回 304 Not Modified,如果不一致,则返回 200,并返回资源以及新的资源标识。


不同刷新操作方式,对强制缓存和协商缓存的影响


不同的刷新操作方式对于强制缓存和写上缓存的影响如下:




  1. 普通刷新(F5刷新按钮):



    • 强制缓存的影响: 浏览器忽略强制缓存,直接向服务器发送请求,获取最新的资源,也就是强制缓存失效;

    • 协商缓存的影响: 浏览器放带有缓存验证的字段的请求,浏览器会根据验证结果返回新的资源或者 304 Not Modified;




  2. 强制刷新(Ctrl+F5Shift+刷新按钮):



    • 强制缓存的影响: 同上,强制缓存失效;

    • 协商缓存的影响: 浏览器发送不带缓存验证字段的请求,服务器返回新的资源,不进行验证,也就是协商缓存失效;




  3. 禁用缓存刷新(DevTools 中的 Disable cacheNetwork 勾选 Disable cache):



    • 强制缓存的影响: 同上,强制缓存失效;

    • 协商缓存的影响: 浏览器会发送带有缓存验证字段的请求,服务器会根据验证结果返回新的资源或 304 Not Modified;




这玩意也就图一乐,一般出现了问题我都是直接重启......


总结


总的来说,强制缓存是通过在请求中添加缓存策略,判断缓存是否有效,避免发送请求到服务器。而协商缓存是通过条件请求与服务器进行通信,验证缓存是否仍然有效,并在服务器返回适当的响应状态码和缓存策略。


强制缓存可以减少对服务器的请求,加快资源加载速度,但可能无法获取到最新的资源。协商缓存能够验证资源的有效性,并在需要时获取最新的资源,但会增加对服务器的请求。选择使用哪种缓存策略取决于具体的应用场景和资源的特性。


参考资料


你知道 304 吗?图解强缓存和协商缓存


作者:Moment
来源:juejin.cn/post/7248235392284721209
收起阅读 »

你真的会用<a>标签下载文件吗?

web
最近和后端联调下载时忽然发现屡试不爽的 <a> 标签下载失灵了?这才感觉自己对文件下载一直处在一知半解的模糊状态中,趁端午前夕有空赶紧总结了一下,和大家一起讨论讨论。 <a> 标签 download 这应该是最常见,最受广大人民群众喜闻...
继续阅读 »

最近和后端联调下载时忽然发现屡试不爽的 <a> 标签下载失灵了?这才感觉自己对文件下载一直处在一知半解的模糊状态中,趁端午前夕有空赶紧总结了一下,和大家一起讨论讨论。


<a> 标签 download


这应该是最常见,最受广大人民群众喜闻乐见的一种下载方式了,搭配上 download 属性, 就能让浏览器将链接的 URL 视为下载资源,而不是导航到该资源。


如果 download 再指定个 filename ,那么就可以在下载文件时,将其作为预填充的文件名。不过名字中的 /\ 会被转化为下划线 _,而且文件系统可能会阻止文件名中的一些字符,因此浏览器会在必要时适当调整文件名。


封装下载方法


贴份儿我常用的下载方法:


const downloadByUrl = (url: string, filename: string) => {
if (!url) throw new Error('当前没有下载链接');

const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = filename;
// 使用target="_blank"时,添加rel="noopener noreferrer" 堵住钓鱼安全漏洞 防止新页面window指向之前的页面
a.rel = "noopener noreferrer";
document.body.append(a);
a.click();

setTimeout(() => {
a.remove();
}, 1000);
};

Firefox 不能一次点击多次下载


这里有个兼容性问题:在火狐浏览器中,当一个按钮同时下载多个文件(调用多次)时,只能下载第一个文件。所以,我们可以利用 <a> 标签的 target 属性,将其设置成 _blank 让火狐在一个新标签页中继续下载。


// 检查浏览器型号和版本
const useBrowser = () => {
const ua = navigator.userAgent.toLowerCase();
const re = /(msie|firefox|chrome|opera|version).*?([\d.]+)/;
const m = ua.match(re);
const Sys = {
browser: m[1].replace(/version/, "'safari"),
version: m[2]
};

return Sys;
};

添加一个浏览器判断:


const downloadByUrl = (url: string, filename: string) => {
// 略......

// 火狐兼容
if (useBrowser().browser === "firefox") {
a.target = "_blank";
}

document.body.append(a);
}

download 使用注意点


<a> 标签虽好,但还有一些值得注意的点:


1. 同源 URL 的限制



download 只在同源 URL 或 blob:data: 协议起作用



也就是说跨域是下载不了的......


首先,非同源 URL 会进行导航操作。其次,如果非要下载,那么正如上面的文档所说,可以先将其转换为 blob:data: 再进行下载,至于如何转换会在 Blob 章节中详细介绍。


2. 无法鉴权


使用 <a> 标签下载是带不了 Header 的,因此也不能携带登录态,所以无法进行鉴权。这里我们给出一个解决方案:



  1. 先发送请求获取 blob 文件流,这样就能在请求时进行鉴权;

  2. 鉴权通过后再执行下载操作。


这样是不是就能很好的同时解决问题1和问题2带来的两个痛点了呢😃


顺便提一下,location.hrefwindow.open 也存在同样的问题。


3. download 与 Content-Disposition 的优先级


这里需要关注一个响应标头 Content-Disposition,它会影响 <a>的 download 从而可能产生不同的下载行为,先看一个真实下载链接的 Response Headers


Snipaste_2023-06-20_18-19-21.png


如图所示,Content-Disposition 的 value 值为 attachment;filename=aaaa.bb。请记住,此时Content-Disposition 的 filename 优先级会大于 <a> download 的优先级。也就是说,当两者都指定了 filename 时,会优先使用 Content-Disposition 中的文件名。


接下来我们看看这个响应标头到底是什么。


Content-Disposition



在常规的 HTTP 应答中,Content-Disposition 响应标头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。



Content-Type 不同,后者用来指示资源的 MIME 类型,比如资源是图片(image/png)还是一段 JSON(application/json),而 Content-Disposition 则是用来指明该资源是直接展示在页面上的,还是应该当成附件下载保存到本地的。


当它作为 HTTP 消息主题的标头时,有以下三种写法:


Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"

inline


默认值,即指明资源是直接展示在页面上的。
但是在同源 URL 情况下,<a> 元素的 download 属性优先级比 inline 大,浏览器优先使用 download 属性来处理下载(Firefox 早期版本除外)。


attachment


即指明资源应该被下载到本地,大多数浏览器会呈现一个 “保存为” 的对话框,如果此时有 filename,那么它将其优于 download 属性成为下载的预填充文件名。


<a>标签 VS Content-Disposition


介绍完 Content-Disposition,我们做一个横向比对的归纳一下:




  • download VS inline/attachment


    优先级:attachment > download > inline




  • download 的值 VS filename


    优先级:filename > download 的值




Blob 转换


前文介绍到,在非同源请情况下可以将资源当成二进制的 blob 先拿到手,再进行 <a> 的下载处理。接下来,我们介绍两种 blob 的操作:


方法1. 用作 URL(blob:)


URL.createObjectURL 可以给 FileBlob 生成一个URL,形式为 blob:<origin>/<uuid>,此时浏览器内部就会为每个这样的 URL 存储一个 URL → Blob 的映射。因此,此类 URL 很短,但可以访问 Blob。


那这就好办多了,写成代码就三行:


import downloadByUrl from "@/utils/download";

const download = async () => {
const blob = await fetchFile();

// 生成访问 blob 的 URL
const url = URL.createObjectURL(blob);

// 调用刚刚封装的 a 标签下载方法
downloadByUrl(url, "表格文件.xlsx");

// 删除映射,释放内存
URL.revokeObjectURL(url);
};

不过它有个副作用。虽然这里有 Blob 的映射,但 Blob 本身只保存在内存中的。浏览器无法释放它。


在文档退出时(unload),该映射会被自动清除,因此 Blob 也相应被释放了。但是,如果应用程序寿命很长,那这个释放就不会很快发生。


因此,如果我们创建一个 URL,那么即使我们不再需要该 Blob 了,它也会被挂在内存中。


不过,URL.revokeObjectURL 可以从内部映射中移除引用,允许 Blob 被删除并释放内存。所以,在即时下载完资源后,不要忘记立即调用 URL.revokeObjectURL。


方法2. 转换为 base64(data:)


作为 URL.createObjectURL 的一个替代方法,我们也可以将 Blob 转换为 base64-编码的字符串。这种编码将二进制数据表示为一个由 0 到 64 的 ASCII 码组成的字符串,非常安全且“可读”。


更重要的是 —— 我们可以在 “data-url” 中使用此编码。“data-url” 的形式为 data:[<mediatype>][;base64],<data>。我们可以在任何地方使用这种 url,和使用“常规” url 一样。


FileReader 是一个对象,其唯一目的就是从 Blob 对象中读取数据,我们可以使用它的 readAsDataURL 方法将 Blob 读取为 base64。请看以下示例:


import downloadByUrl from "@/utils/download";

const download = async () => {
const blob = await fetchFile();

// 声明一个 fileReader
const fileReader = new FileReader();

// 将 blob 读取成 base64
fileReader.readAsDataURL(blob);

// 读取成功后 下载资源
fileReader.onload = function () {
downloadByUrl(fileReader.result);
};
};

在上述例子中,我们先实例化了一个 fileReader,用它来读取 blob。


一旦读取完成,就可以从 fileReader 的 result 属性中拿到一个data: URL 格式的 Base64 字符串。


最后,我们给 fileReader 注册了一个 onload 事件,在读取操作完成后开始下载。


两种方法总结与对比


URL.createObjectURL(blob) 可以直接访问,无需“编码/解码”,但需要记得撤销(revoke);


Data URL 无需撤销(revoke)任何操作,但对大的 Blob 进行编码时,性能和内存会有损耗。


总而言之,这两种从 Blob 创建 URL 的方法都可以用。但通常 URL.createObjectURL(blob) 更简单快捷。


responseType


最后,我们回头说一下请求的注意点:如果你的项目使用的是 XHR (比如 axios)而不是 fetch, 那么请记得在请求时添加上 responseType 为 'blob'。


export const fetchFile = async (params) => {
return axios.get(api, {
params,
responseType: "blob"
});
};

responseType 不是 axios 中的属性,而是 XMLHttpRequest 中的属性,它用于指定响应中包含的数据类型,当为 "blob" 时,表明 Response 是一个包含二进制数据的 Blob 对象。


除了 blob 之外,responseType 还有 arraybufferjsontext等其他枚举字符串值。


总结


一言以蔽之,同源就直接使用 <a> download 下载,跨域就先获取 blob,用 createObjectURLreadAsDataURL 读取链接,再用 <a> download 下载。


参考资料


收起阅读 »

那年毕业前,我花了一整个上午的时间走遍整个校园

web
又逢毕业季,最近看了很多伤感的分别视频,在感叹年轻真好的同时,不禁想起来自己之前在离校前几天也做了一系列的... 所谓的告别的事情... 其中最令我印象深刻的就是拿着ipad,起很早,几乎走遍了整个校园,拍了一些有趣但更有意义的照片。 那些照片 🔼 这张照片...
继续阅读 »

又逢毕业季,最近看了很多伤感的分别视频,在感叹年轻真好的同时,不禁想起来自己之前在离校前几天也做了一系列的... 所谓的告别的事情...


其中最令我印象深刻的就是拿着ipad,起很早,几乎走遍了整个校园,拍了一些有趣但更有意义的照片


那些照片


木头 2023-06-06 19.29.22.jpeg


🔼 这张照片左边黑白的是一天前拍的,右边是今天拍的,当时毕业典礼刚结束,我们要从体育馆走回计算机学院,人很多,大家边走边笑着,感觉就像... 对,就像是刚入学报到的那一天,那天也像这天一样热闹。今天的路上没有人,天气很好,只有静悄悄的阳光撒在地面上。


木头 2023-06-06 19.35.20.jpeg


🔼 这张照片是宿舍楼旁边的一条小路,路中间是校医院。七天前吃饭的时候路过这里,随手拍了一张照片,今天有风,落了一地小花,还挺好看。


木头 2023-06-06 19.40.15.jpeg


🔼 这是学校操场,28天前正好是在做毕业设计的阶段,在宿舍一坐就是一天,没思路的时候,我就喜欢一个人来看台上坐一会儿,戴着耳机灯歌,看下面跑步的人,等操场上的灯关了,我就回去。当时是一个傍晚,夕阳挺好看的。今天的天气很好,有很多人在跑步。


木头 2023-06-06 19.44.53.jpeg


🔼 老照片拍摄于596天前,快两年了吧... 那时候应该大三,中午刚下课,大家走在南广场上,一块去食堂,那天的云很好看。当时在拍新照片的时候,心里还是有点伤感,明天就要离校了,很多人很可能就再也见不到了...(现在来看,的确是这样)


木头 2023-06-06 19.49.36.jpeg


🔼 上学的时候,每天早晨我都起的很早,六点多就从宿舍出来,其实也不是为了学习或者什么,我就喜欢走在安静的校园里,阳光洒在草丛间,偶尔有鸟叫声,我觉得这种感觉很美好。拍左边图的时候应该是个深秋了吧,树叶落了一地,而右边又正好是一个盛夏,树木生长的正好!这张照片的对比感让我感到无比惊喜!


拍摄心得


其实当时拍的比这些成品照片要多得多,总共拍了20多个地方吧,最终只合成出了78张可以用的,废片率相当高


因为什么呢?


原因是当时的我没有一个准确的参照物,在拍新照片时我一般会历经以下步骤:


1、先拿出手机看看老照片的角度和位置


2、举起ipad,凭感觉走到自己认为准确的位置上


3、拍一张看一下效果


4、满意就再拍两张当备份,不满意就继续重复以上步骤,直到排除满意的


好像一个递归方法!用伪代码实现一下就是:


const takePhoto = () => {
// 1、拿出老照片看角度 + 位置
const { position } = showOldPhoto();

// 2、拿出ipad,走到对应的位置上
walkToPosition(position);

// 3、拍一张看看效果
const { isOK } = takeSomePhoto();

// 4、判断是否满意,满意就结束,不满意就继续递归
!isOk && takePhoto();
}

当然,我也没那么工匠精神,我可能还得再加一个结束条件


const takePhoto = () => {
// 如果拍5次还不满意,就
if(reTryTime > 5) {
return;
}

// others
}

这个过程是比较重复且枯燥的,当然可以适当优化一下比如我可以在ipad上看照片,这样就省掉手机这一步了,另外可以在拍摄时不断地切换照片和相机app,这样就可以稍微快点看到当前位置对不对了...


em... 当时的我真的希望有一个工具能来辅助我拍这些照片!


噢噢噢!


现在的我可以很开心的跟那时候的我说,有了!现在有了!


你可以去微信小程序里搜:历旧弥新


你就可以搜到一个看起来还蛮专业的一个小程序,UI做的也不错,不丑!


它好像提供了一个你非常需要的功能:和旧照片来一场对话


你可以非常轻松的用它来拍一张新旧照片合成的照片,


就像下图:


WechatIMG301.jpeg


你可以将你的旧照片半透明的状态覆盖到相机上(就像左边的图),可以缩放平移,把它放在准确的位置上之后,然后你就可以非常轻易的去拍摄相同角度的照片了!


嗯... 听到这里是不是感觉出来这是一个广告了哈哈哈,没错,那就是了!


打广告!


对,这就是我基于四年前的想法,最近花了几个周末开发的一个小程序,历旧弥新


名字取自 历久弥新 => ,代表一种新旧交替的含义


来看下小程序首页


木头 2023-06-06 21.31.46.jpeg


它一共包含四个功能:


1、与旧照片来一次对话


2、已有关联的照片拼接


3、快速找一个相同的拍照姿势


4、异地也可以来合照


我们也提供了比较好的一些用户拍摄过的照片,放在首页的下半部分:


木头 2023-06-06 21.54.15.jpeg


木头 2023-06-06 21.57.31.jpeg


你可以快速的进行 拍同款 !就可以拍摄类似的照片啦!


当然它或许也存在一些问题希望大家不要吝啬自己的建议,可以评论在下方哈!

作者:木头就是我呀
来源:juejin.cn/post/7242247549511663672

收起阅读 »

Vue KeepAlive 为什么不能缓存 iframe

web
最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下: <router-view v-slot="{ Component ...
继续阅读 »

最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下:


  <router-view v-slot="{ Component }">
<keep-alive :include="keepAliveList">
<component :is="Component"></component>
</keep-alive>
</router-view>

看起来并没有什么问题,并且其他非 iframe 实现的页面都是可以被缓存的,因此可以推断问题出在 iframe 的实现上。


我们先了解下 KeepAlive


KeepAlive (熟悉的可跳过本节)


被 KeepAlive 包裹的组件不是真的卸载,而是从原来的容器搬运到另外一个隐藏容器中,实现“假卸载”, 当被搬运的容器需要再次挂载时,应该把组件从隐藏容器再搬运到原容器,这个过程对应到组件的生命周期就是 activated 和 deactivated


keepAlive 是需要渲染器支持的,在执行 mountComponent 时,如果发现是 __isKeepAlive 组件,那么会在上下文注入 move 方法。


function mountComponent(vnode, container, anchor) {
/**... */
const instance = {
/** ... */
state,
props: shallowReactive(props),
// KeepAlive 实例独有
keepAliveCtx: null
};

const isKeepAlive = vnode.__isKeepAlive;
if (isKeepAlive) {
instance.keepAliveCtx = {
move(vnode, container, anchor) {
insert(vnode.component.subTree.el, container, anchor);
},
createElement
};
}
}

实现一个最基本的 KeepAlive,需要注意几个点



  1. KeepAlive 组件会创建一个隐藏的容器 storageContainer

  2. KeepAlive 组件的实例增加两个方法 _deActive_active

  3. KeepAlive 组件存在一个缓存的 Map,并且缓存的值是 vnode


const KeepAlive = {
// KeepAlive 特有的属性,用来标识
__isKeepAlive: true,
setup() {
/**
* 创建一个缓存对象
* key: vnode.type
* value: vnode
*/

const cache = new Map();
// 当前 keepAlive 组件的实例
const instance = currentInstance;
const { move, createElement } = instance.keepAliveCtx;
// 创建隐藏容器
const storageContainer = createElement('div');

// 为 KeepAlive 组件的实例增加两个方法
instance._deActive = vnode => {
move(vnode, storageContainer);
};
instance._active = (vnode, container, anchor) => {
move(vnode, container, anchor);
};

return () => {
// keepAlive 的默认插槽就是要被缓存的组件
let rawVNode = slot.default();
// 不是组件类型的直接返回,因为其无法被缓存
if (typeof rawVNode !== 'object') {
return rawVNode;
}

// 挂载时,优先去获取被缓存组件的 vnode
const catchVNode = cache.get(rawVNode.type);
if (catchVNode) {
rawVNode.component = catchVNode.component;
// 避免渲染器重新挂载它
rawVNode.keptAlive = true;
} else {
// 如果没有缓存,就将其加入到缓存,一般是组件第一次挂载
cache.set(rawVNode.type, rawVNode);
}
// 避免渲染器真的把组件卸载,方便特殊处理
rawVNode.shouldKeepAlive = true;
rawVNode.keepAliveInstance = instance;
return rawVNode;
};
}
};

从上可以看到,KeepAlive 组件不会渲染额外的内容,它的 render 函数最终只返回了要被缓存的组件(我们称要被缓存的组件为“内部组件”)。KeepAlive 会对“内部组件”操作,主要是在其 vnode 上添加一些特殊标记,从而使渲染器能够据此执行特殊的逻辑。


function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
const { type } = n2;
if (type === 'string') {
/** 执行普通的标签 patch */
} else if (type === Text) {
/** 处理文本节点 */
} else if (type === Fragment) {
/** 处理Fragment节点 */
} else if (typeof type === 'object') {
if (!n1) {
if (n2.keptAlive) {
n2.keepAliveInstance._activate(n2, container, anchor);
} else {
mountComponent(n2, container, anchor);
}
} else {
patchComponent(n1, n2, anchor);
}
}
}

function unmount(vnode) {
const { type } = vnode;
if (type === Fragment) {
/**... */
} else if (typeof type === 'object') {
if (vnode.shouldKeepAlive) {
vnode.keepAliveInstance._deActivate(vnode);
} else {
unmount(vnode.component.subTree);
}
return;
}
}

从上面的代码我们可以看出,vue 在渲染 KeepAlive 包裹的组件时,如果有缓存过将执行 keepAliveInstance._activate,在卸载时将执行 keepAliveInstance._deActivate


原因


通过上面的了解,我们知道,KeepAlive 缓存的是 vnode 节点,vnode 上面会有对应的真实DOM。组件“销毁”时,会将真实 DOM 移动到“隐藏容器”中,组件重新“渲染”时会从 vnode 上取到真实 DOM,再重新插入到页面中。这样对普通元素是没有影响的,但是 iframe 很特别,当其插入到页面时会重新加载,这是浏览器特性,与 Vue 无关。


解决方案


思路:路由第一次加载时将 iframe 渲染到页面中,路由切换时通过 v-show 改变显/隐。



  1. 在路由注册时,将 component 赋值为一个空组件


  {
path: "/chathub",
name: "chathub",
component: { render() {} }, // 这里写 null 时控制台会出 warning,提示缺少 render 函数
},


  1. 在 router-view 处,渲染 iframe,通过 v-show 来控制显示隐藏


  <ChatHub v-if="chatHubVisited" v-show="isChatHubPage"></ChatHub>
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveList">
<component :is="Component"></component>
</keep-alive>
</router-view>


  1. 监听路由的变化,改变 iframe 的显/隐


const isChatHubPage = ref(false)
// 这里是个优化,想的是只有页面访问过该路由才渲染,没访问过就不渲染该组件
const chatHubVisited = ref(false)

watch(
() => routes.path,
(value) => {
if (value === '/chathub') {
chatHubVisited.value = true
isChatHubPage.value = true
} else {
isChatHubPage.value = false
}
},
{
immediate: true
}
)
作者:莱米
来源:juejin.cn/post/7246310077233659941

收起阅读 »

uni-app实现微信小程序蓝牙打印

web
打印流程 小程序连接蓝牙打印大致可以分为九步:1.初始化蓝牙模块、2.开始搜索附近的蓝牙设备、3.获取搜索到的蓝牙列表、4.监听寻找到新设备的事件、5.连接蓝牙设备、6.关闭搜索蓝牙设备事件、7.获取蓝牙设备的所有服务、8.获取服务的所有特征值、9.向蓝牙设备...
继续阅读 »

打印流程


小程序连接蓝牙打印大致可以分为九步:1.初始化蓝牙模块、2.开始搜索附近的蓝牙设备、3.获取搜索到的蓝牙列表、4.监听寻找到新设备的事件、5.连接蓝牙设备、6.关闭搜索蓝牙设备事件、7.获取蓝牙设备的所有服务、8.获取服务的所有特征值、9.向蓝牙设备写入数据


1.初始化蓝牙模块 uni.openBluetoothAdapter


注意:其他蓝牙相关 API 必须在 uni.openBluetoothAdapter 调用之后使用。


uni.openBluetoothAdapter({
success(res) {
console.log(res)
}
})

2.开始搜索附近的蓝牙设备 uni.startBluetoothDevicesDiscovery


此操作比较耗费系统资源,请在搜索并连接到设备后调用 uni.stopBluetoothDevicesDiscovery 方法停止搜索。


uni.startBluetoothDevicesDiscovery({
success(res) {
console.log(res)
}
})

3.获取搜索到的蓝牙列表 uni.getBluetoothDevices


获取在蓝牙模块生效期间所有已发现的蓝牙设备。包括已经和本机处于连接状态的设备(不是很准确,有时会获取不到)。


uni.getBluetoothDevices({
success(res) {
console.log(res)
}
})

4.监听寻找到新设备的事件 uni.onBluetoothDeviceFound


监听寻找到新设备的事件,跟第三步一起使用,确保能获取附近所有蓝牙设备。


uni.onBluetoothDeviceFound(function (devices) {
console.log(devices)
})

5.连接蓝牙设备 uni.createBLEConnection


若APP在之前已有搜索过某个蓝牙设备,并成功建立连接,可直接传入之前搜索获取的 deviceId 直接尝试连接该设备,避免用户每次都要连接才能打印,省略二三四步减少资源浪费。


uni.createBLEConnection({
deviceId:获取到蓝牙的deviceId,
success(res) {
console.log(res)
}
})

6.关闭搜索蓝牙设备事件 uni.stopBluetoothDevicesDiscovery


停止搜寻附近的蓝牙外围设备。若已经找到需要的蓝牙设备并不需要继续搜索时,建议调用该接口停止蓝牙搜索。


uni.stopBluetoothDevicesDiscovery({
success(res) {
console.log(res)
}
})

7.获取蓝牙设备的所有服务 uni.getBLEDeviceServices


uni.getBLEDeviceServices({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
success(res) {
console.log('device services:', res.services)
}
})

8.获取服务的所有特征值 uni.getBLEDeviceCharacteristics


uni.getBLEDeviceCharacteristics({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
success(res) {
console.log('device getBLEDeviceCharacteristics:', res.characteristics)
}
})
三种不同特征值的id
for (var i = 0; i < res.characteristics.length; i++) {
if (!notify) {
notify = res.characteristics[i].properties.notify;
if (notify) readId = res.characteristics[i].uuid;
}
if (!indicate) {
indicate = res.characteristics[i].properties.indicate;
if (indicate) readId = res.characteristics[i].uuid;
}
if (!write) {
write = res.characteristics[i].properties.write;
writeId = res.characteristics[i].uuid;
}
if ((notify || indicate) && write) {
/* 获取蓝牙特征值uuid */
success &&
success({
serviceId,
writeId: writeId,
readId: readId,
});
finished = true;
break;
}

9.向蓝牙设备写入数据 uni.writeBLECharacteristicValue


向低功耗蓝牙设备特征值中写入二进制数据。注意:必须设备的特征值支持 write 才可以成功调用。


并行调用多次会存在写失败的可能性。


APP不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。


若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。


安卓平台上,在调用 notifyBLECharacteristicValueChange 成功后立即调用 writeBLECharacteristicValue 接口,在部分机型上会发生 10008 系统错误


// 向蓝牙设备发送一个0x00的16进制数据
const buffer = new ArrayBuffer(1)
const dataView = new DataView(buffer)
dataView.setUint8(0, 0)
uni.writeBLECharacteristicValue({
// 这里的 deviceId 需要在 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId,
// 这里的value是ArrayBuffer类型
value: buffer,
success(res) {
console.log('writeBLECharacteristicValue success', res.errMsg)
}
})

写在最后


DEMO地址:gitee.com/zhou_xuhui/… (plus可能会报错,demo中注释掉就好,不影响流程)


打印机CPCL编程参考手册(CPCL 语言):http://www.docin.com/p-2160

作者:我是真的菜呀
来源:juejin.cn/post/7246264754141773885
10502…

收起阅读 »

悟了两星期终于悟了,移动端适配核心思想——没讲懂揍我

web
移动端开发与pc端适配的不同 pc端布局常用方案 所谓适配,就是指我们的项目开发出来用户在不同虚拟环境、不同硬件环境等等各种情况下有一个稳定、正常的展示(简单理解就是不会排版混乱) 先来回顾一下pc端的项目我们如何写页面做到适配的,大部分页面都是采用版心布局的...
继续阅读 »

移动端开发与pc端适配的不同


pc端布局常用方案


所谓适配,就是指我们的项目开发出来用户在不同虚拟环境、不同硬件环境等等各种情况下有一个稳定、正常的展示(简单理解就是不会排版混乱)


先来回顾一下pc端的项目我们如何写页面做到适配的,大部分页面都是采用版心布局的形式。也就是说所有的内容都写在版心容器盒子里,这个容器盒子设置:margin: 0 auto; & min-width: <版心宽度> & width: <版心宽度>就可以保证:




  • 当用户的屏幕(浏览器)宽度很大,或者缩放浏览器到很小比例时,此时浏览器的宽度大于版心盒子的width,版心容器会自动生成margin-left & margin-right,总会保证版心容器处于页面的正中心。


    这里可以提一嘴pc端浏览器缩放的原理:页面所有元素的css宽高都不会改变,只是css像素的在屏幕上展示的大小缩水了,具体点来说,原本700px * 700px的盒子在浏览器上用10cm * 10cm面积(的物理像素)渲染,但现在用原本<浏览器缩放比率> * 10cm * 10cm面积(的物理像素)渲染。




  • 当用户的屏幕小于版心盒子的width,出现横向滚动条,版心盒子的左右margin为0,width内的内容可滑动滚动条完整查看。




可以参考大淘宝pc端官网就是版心布局的实践。


好了,那么问题来了,移动端为啥不能照搬pc端的这种适配方案呢?


我们有必要先梳理一下移动端对页面进行渲染展示的逻辑:


移动端页面渲染的逻辑


<meta name="viewport">的情况


在html文档里没有<meta name="viewport">标签配置的情况下(通过对比即可理解<meta>标签的意义):


plus:如下整个流程篇口语话主要是梳理核心思路,没有一字一板的细节考究



  1. 我们项目中布局写的所有dom元素的css大小都正常(完全按照css大小的预期)在一个非常大的空间进行渲染,这个空间可能不是无限大,但是为了帮助理解,因为这个空间的大小一般不影响我们项目的正常布局,所以我们可以理解为无限大,这是第一步,即项目页面就像在pc端一样完全按照css写的大小以及布局进行渲染。



  1. 因为我们的移动端设备没有电脑屏幕那么大,所以会把第一步在“很大空间”渲染的页面进行缩小,直至缩小到我们的大页面宽度正好与手机屏幕的宽度一样即可。所以第二步相当于为了让用户把页面看全,手机自动把页面缩小至屏幕内。


为了帮助大家理解,也验证我上面的说法,我写了如下的pc端的版心布局的页面:


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Document</title>
</head>
<style>
.container {
   width: 1200px;
   min-width: 1200px;
   margin: 0 auto;
   border: 2px solid red;
   background: yellow;
}
.container .content {
   height: 3000px;
}
</style>
<body>
   <div class="container">
       <div class="content">内容</div>
   </div>
</body>
</html>

我把上面的页面部署到osrc(一个国内的免费部署网站,类似于vercel)上了(不可用chrome浏览器的移动端去模拟移动端访问的场景,chrome浏览器只是模拟了屏幕大小,而并没有模拟移动端环境,也就是说根本不会处理<meta>标签,所以这里我们需要部署),大家可以自行用pc端和移动端访问体验(实践一下绝对秒懂我上面的文字),为了照顾没有双端设备的读者,我截一下图(直接喂饭到胃哈哈)


pc端访问:


pc端访问版心布局.png


移动端访问:


移动端访问版心布局.jpg


清晰了吧兄弟们,我们写死的1200px宽的container盒子因为手机本身没这么大,所以缩小之后塞进了手机屏幕中,仔细看手机中的文字,已经被缩小的看不清了。


配置<meta name="viewport">的情况


暂时只给我们的index.html<meta name="viewport">添加一个content="width=device-width, initial-scale=1.0"


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Document</title>
</head>
<style>
.container {
   width: 1200px;
   min-width: 1200px;
   margin: 0 auto;
   border: 2px solid red;
   background: yellow;
}
.container .content {
   height: 3000px;
}
</style>
<body>
   <div class="container">
       <div class="content">内容</div>
   </div>
</body>
</html>

重新部署后访问查看效果,有meta的页面,部署地址<meta>标签是针对移动端的,所以pc端完全没影响,跟上面一样。现在我们访问移动端效果如下,我没有缩小图片,注意观察页面底部出现滚动条了(纵向滚动条有滚动所以文字没展示,不重要):


设置meta后移动端访问效果.jpg


解释一下content="width=device-width, initial-scale=1.0"的作用。


解读<meta> & dip & 布局视口(自认为最精华,全网少数不带偏新人的解释)


其实相当于我们在content字段中进行了两项配置,第一项是width=device-width,第一个width是指布局视口的宽度,引出概念,何为布局视口?还记得我们上面说的在没有<meta>时的那个非常大的布局空间嘛,就是它!我们让这个空间的宽度等于device-widthdevice-width就是指dip即设备独立像素,第二个概念,何为dip(device independent piexl设备独立像素)呢?听我的,完全不要被网上各种乱七八糟的解释弄迷糊了,什么dpr,什么物理像素,我只能说那些东西与我们开发者并没有直接关系,笔者读了几户所有能搜到的各种移动端入门文章,一言难尽... ,我来给出对于dip的理解,每一个型号的移动设备都具有的一个大小,这个大小是用设备独立像素dip来描述的,所以它仅仅是一个描述大小的单位,但是这个单位究竟是多大呢,换句话说dip有何特殊性呢?


在移动端不缩放的情况下,一个css像素等于一个设备独立像素dip

(chrome浏览器的移动端开发工具里显示的就是dip大小)也就是说,我们让布局视口的宽度等于设备的dip宽度,这里注意:布局视口由原来的”无限大“现在改为一个具体的数值,并不会影响页面的正常布局,页面还是会完整渲染, 只是最后不用缩小放进屏幕了,因为我们缩小的目的就是让布局视口完整的展现在屏幕中。因为屏幕不能展示完整整个页面的布局,所以底部出现滚动条。用户可以滚动访问页面全部内容。


其实这里initial-scale=1.0的作用就是让移动端浏览器不自行缩放,不然的话浏览器会把如上页面再缩小,然后放到手机屏幕里去。


关于<meta name="viewport">的最佳实践


简简单单如下:


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

为什么说这是最佳实践,论证它其实还缺少一个关键点,也就是移动端css单位的选取,概括来说,width=device-width配置与移动端css单位的选取两者相辅相成,共同构成了“最佳实践”。


先说一下css单位选取——以vw为例:vw是相对屏幕大小的一个长度单位,1vw等于移动设备屏幕宽度的1%


如何理解“最佳实践”?


首先width=device-width保证了无论何种机型的移动设备,我们开发时写页面的布局视口始终等于屏幕宽度,但看这一点,确实没啥用。如果再来一个条件:页面中所有长度单位用vw来表达。细品!


如何细品?别忘了初心,我们的目标是在不同的移动设备上都能有统一的展示效果,开品:我们用不同dip宽度的设备打开网页,首先布局视口的大小会随着设备dip的不同而不同,也就是始终等于dip宽度:


布局视口宽度 === 设备dip宽度,

并且我们的所有元素大小单位都是vw,也就是说页面中所有元素大小都以屏幕宽度为参照物。最终的效果就是,一个dip宽度超级小的设备打开网页,与一个dip宽度非常大的设备打开网页,看到的页面内容是完全相似的,也就是每个元素在页面中所占的比例不同设备都一样(不同点就在于屏幕本身的大小不一样)!


一般<meta>标签的content中还会设置initial-scale=1.0, maximum-scale=1.0, user-scalable=no,即不让页面进行缩放,感觉这个看需求吧,不让缩小应该是必须的,因为可以想一想,用户缩小完全没有意义呐!(需要大家自己去理解,属于只可意会),至于让不让放大,应该是看情况吧,反正移动端淘宝官网是允许放大的。


移动端适配方案理解


主流的有vw方案、flexible + rem方案,总而言之,把元素的大小用rem来表示或者vw表示,本质都是以手机屏幕宽度为参考,vw比较直接,表达的意思就是1vw等于手机屏幕宽度的百分之一;rem比较间接,通过flexible.js先把得知屏幕宽度是多少px,然后设置<html>font-size,进而所有元素的rem其实还是表达占屏幕宽度的百分之多少。


当然两种方案都有一些技术细节问题需要解决,比如1px问题、安全区域问题等等。这里就不多说了。


相信能一步一步走到这里的同志,对移动端适配绝对有了一个清晰的把握。


2023.6.19,3: 59。

作者:荣达
来源:juejin.cn/post/7246001188448731196
更文不易,点个赞吧!

收起阅读 »

值得学习的JavaScript调试技巧

web
引言 最近老大在cr我代码时,我就静静坐在他旁边看他装逼。他一顿断点+抓包+各种骚操作给我看楞了,我没忍住就让他手把手教我,他就和我在一个小屋子里xxxx了几个小时,他手法太快了俺看一遍就忘了,于是乎就靠回忆和资料查找整理了哈高频的js调试技巧,希望能帮助到各...
继续阅读 »

引言


最近老大在cr我代码时,我就静静坐在他旁边看他装逼。他一顿断点+抓包+各种骚操作给我看楞了,我没忍住就让他手把手教我,他就和我在一个小屋子里xxxx了几个小时,他手法太快了俺看一遍就忘了,于是乎就靠回忆和资料查找整理了哈高频的js调试技巧,希望能帮助到各位。


一:console.dir


在打印dom节点时,普通的console.log是纯文本格式,而dir的打印是以对象的方式。因此在输出dom节点时,务必使用dir打印


<div id="main">
<div class="box1">
<p>p1</p>
</div>

</div>

let oD = document.querySelector('.box1')
console.log(oD)//普通的log输出
console.dir(oD)//dir输出方式

image.png


二:二次发起请求


在调试接口时,通常我们会刷新页面然后观察network的接口信息,如果项目加载时间过长,刷新页面查看接口的效率是十分低的。



  1. 对接口请求右键

  2. 选择Relpy xhr发送请求


image.png


三:接口请求参数修改


借助浏览器控制台可以不用修改代码就可以发送不同参数的新请求了。具体操作如下



  1. 对接口请求右键

  2. 选择copy。

  3. 再选择copy as fetch。

  4. 在console区域粘贴上面的请求信息,然后修改请求体参数。

  5. 然后切换到networkl查看最新请求的结果


效果展示


24.gif


四:css查看伪类hover,active样式


在控制台右侧选择:hov可以选择对应dom各种伪类状态下的css样式,十分的便捷


image.png


五:css样式跳转到对应文件查看


选择css样式,按住alt点击就可以跳到对应文件查看具体代码


25.gif


六:控制台输出选择的dom


首先在页面选择指定的位置dom,然后在在控制台使用$0就表示当前选中的dom了


26.gif


七:展开全部dom


有时候我们在页面查找一个dom时,它嵌套层级特别深。这巨他妈蛋疼一层层展开,这个时候我们就需要找到一键全部展开来帮助我们解决这个问题了。


27.gif


右键选择expand就可以展开选择的dom了。


八:断点调试


断点调试是本节最后一个内容了,它也是最核心的内容了,玩的6的是真的6,老大说我搞懂断点调试和对应的堆栈上下文就可以毕业了。(毕业=辞退?还是。。。)下面我列举的仅仅是入门级别的断点调试,只是说明如何上手操作,里面许多东西还望大家多多探索。


1. 打断点方式


代码中:debugger


在需要断点的地方写入debugger,此时程序运行后代码就会卡在这里,等待主人的安排


let a = 10
debugger
a++

浏览器中:



  1. 选择sources

  2. 在指定代码行左侧单击


image.png


2. 断点间调试


第一种断点调试是十分常用的方式,代码会从当前断点直接运行到下一个断点处执行,中间经过代码都默认被执行且跳过。如下图红色按钮就是断点间调试。


image.png


例子演示


28.gif


我们在上图中打了3个断点,逐个点击,首先从断点15行直接跳到断点17行,最后跳到19行。由于异步最后执行,所以最后又跳到断点15行结束。断点经过的地方鼠标移动到变量上可以查看其内部数据。


3. 逐步调试


逐步调试很明显就是字面意思,从当前断点位置开始一行一行的运行代码,稍微注意的是,遇到函数不进入函数的内部,而是直接执行完函数。


image.png


例子演示


29.gif


4. 进入与进出函数调试


逐步调试遇到函数是不进入函数内部的,因此需要借助进入和进出调试方式控制函数的访问


image.png


例子演示


30.gif


5. 逐步调试详细版


上面讲述了第一种逐步调试方式,其遇到函数是不进入函数内部的,而是直接执行函数。因此下面这种方式是逐步调试的详细版,它也是从断点位置逐步的调试运行,遇到函数也会进入函数的内部进行逐步执行。


image.png


九:React/Vue中尝试


有吊毛说react和vue咋调试?嗯,那个吊毛其实就是我,其实也很简单滴。



  1. 在需要调试的代码位置插入debugger

  2. 在浏览器控制台需要查看变量的地方插入断点

  3. 使用各种调试连招一顿操作就行。


代码例子


例如下面的例子,页面最后显示的num是多少?最后是101,不了解批量setState的开始肯定蒙,我们调试看看


import React,{useEffect, useState} from "react";
const Home = () => {
const [num,setNum] = useState(1)
useEffect(()=>{
debugger
setNum(100)
setTimeout(() => {
setNum(num+100)
}, 0);
},[])
return (
<div>num:{num}</div>
)
}
export default Home;

调试演示
根据调试发现,进入定时器的时候num还未更新,还是1。


31.gif


作者:前端兰博
来源:juejin.cn/post/7246376735838060603
收起阅读 »

前端时钟翻页效果,一看就会,一写就fei

web
最近阅读了不少实现翻页效果的文章,受益匪浅,在此写个学习笔记。 一、元素拆解 从侧面来观察这个翻页过程,能看到承载文字内容的主要有三个面板:一个静止在上半部分的面板,显示旧文字的上半部分;一个静止在下半部分的面板,显示新文字的下半部分;第三个是旋转面板,一...
继续阅读 »

最近阅读了不少实现翻页效果的文章,受益匪浅,在此写个学习笔记。


22.gif


一、元素拆解


动画拆解.png


从侧面来观察这个翻页过程,能看到承载文字内容的主要有三个面板:一个静止在上半部分的面板,显示旧文字的上半部分;一个静止在下半部分的面板,显示新文字的下半部分;第三个是旋转面板,一面显示旧文字的下半部分,另一面显示新文字的上半部分。翻转的动画,我们考虑采用FLIP的思想:



  1. 先实现【动画结束帧】的样式;

  2. 再从【动画开始帧】播放。


二、实现结束帧样式


准备工作:用vue脚手架创建一个模板项目,并添加一个div容器:


image.png


<!-- App.vue -->
<template>
<div id="app">
<test-comp/>
</div>
</template>


<!-- Test.vue -->
<template>
<div class="card"></div>
</template>


<style lang="less" scoped>
.card {
position: relative;
border: solid 4px black;
width: 400px;
height: 400px;
perspective: 1000px;
}
</style>


2.1 实现静止的上半面板


image.png


<template>
<div class="card">
<div class="half-card top-half"></div>
<!-- <div class="half-card bottom-half">财</div> -->
</div>

</template>

<style lang="less" scoped>
/* ... */
.half-card {
position: absolute;
width: 100%;
height: 50%;
overflow: hidden;
background-color: #2c292c;
color: white;
font-size: 320px;
}
.top-half {
line-height: 400px;
}
</style>


我们知道line-height配合font-size可以控制文字在垂直方向的位置,大多数情况下,文字顶部与容器顶部的距离公式为(line-height - font-size) / 2。


记容器高度h,文字大小f,容器只显示文字上半部分的情况下,上述距离的值为h - f / 2,即(line-height - f) / 2 = h - f / 2,所以line-height为2h(400px)。


2.2 实现静止的下半面板


image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<div class="half-card bottom-half"></div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.bottom-half {
top: 50%;
line-height: 0;
}
</style>


在容器只显示文字下半部分的情况下,完整的文字顶部距离容器顶部的距离是-f / 2,那么就有(line-height - f) / 2 = - f / 2,即line-height = 0;


2.3 实现旋转面板


2.3.1 旋转面板的正面————新文字的上半部分


image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<!-- <div class="half-card bottom-half">财</div> -->
<div class="rotating-half">
<div class="half-card front-side"></div>
<!-- <div class="half-card back-side">发</div> -->
</div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.rotating-half {
position: absolute;
width: 100%;
height: 50%;
.half-card {
height: 100%;
}
}
.front-side {
line-height: 400px;
}
</style>


2.3.2 旋转面板的背面————旧文字的下半部分


怎么让一个div背对我们?只要让它绕着自己的腰部横线翻转180度即可(翻跟斗)。


image.png
image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<!-- <div class="half-card bottom-half">财</div> -->
<div class="rotating-half">
<!-- <div class="half-card front-side">财</div> -->
<div class="half-card back-side"></div>
</div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.back-side {
line-height: 0;
transform: rotateX(180deg); // !!!!!!!!!!!
}
</style>


现在,如果把正面也加上,会发现这样一个问题:两个面的位置是重叠的,在模板中后声明的背面元素(即使它是背对着我们)会覆盖正面元素。我们想让这两个面在背对我们的状态下都不显示,这就需要到如下的css属性:backface-visibility: hidden。


此外,现在一个旋转面板中带有两个“面”,我们想要这两个面随着父元素面板的3d旋转一起旋转,也就是保持相对静止,这就需要设置旋转面板【将子元素纳入自己的3d变换空间】:transform-style: preserve-3d。


加上css后,让旋转面板简单地旋转一下,看看效果(效果图有点慢):


<style lang="less" scoped>
/* ... */
.rotating-half {
/* ... */
transform-style: preserve-3d;
.half-card {
/* ... */
backface-visibility: hidden;
}
/* to delete */
transition: transform 1s;
&:hover { transform: rotateX(-180deg); }
}
/* ... */
</style>

2.gif


至此,三个面板静态效果已经完成:


image.png


三、播放动画


在第二节已经得到了动画结束时的状态。接下来需要从动画开始的状态进行播放。


3.1 设置好旋转轴


在目标动画中,旋转面板应该是绕着底边进行旋转的。把【变换原点】设置为底边的中点,这样,经过这个点的X轴就和底边所在的直线重合,绕X轴旋转就等价于绕底边旋转:


<style lang="less" scoped>
/* ... */
.rotating-half {
/* ... */
transform-origin: center bottom;
}
/* ... */
</style>

3.2 找到动画开始帧,使用animate播放动画


动画开始时,旋转面板在主面板的下半区域。要从上半区域(无变换状态)到达下半区域,需要绕着底边逆时针旋转180度,因此开始帧所处于的变换状态就是rotateX(-180deg),从而得到动画的关键帧:


【transform: rotateX(-180deg)】->【transform: none】。


我们给旋转面板加上ref,然后在组件挂载完毕时播放即可:


<script>
export default{
mounted() {
this.$refs.rotate?.animate?.(
[
{ offset: 0, transform: 'rotateX(-180deg)' },
// { offset: 1, transform: 'none' },
],
{
duration: 1000,
easing: 'ease-in-out',
},
);
},
};
</script>

2.gif


四、应用


这样的UI组件可能会用于记录时间、比赛分数变化啥的,自然是不能把值写死。考虑如下的应用场景:


<!-- App.vue -->
<template>
<div id="app" class="flex-row">
<test-comp :value="scoreLGD"/>
<h1>VS</h1>
<test-comp :value="scoreLiquid"/>
</div>
</template>


<script>
import TestComp from './Test';
export default {
components: { TestComp },
data() { return {
scoreLGD : 15,
scoreLiquid: 13,
};
},
mounted() {
setInterval(() => {
this.scoreLGD = this.randomInt(99);
this.scoreLiquid = this.randomInt(99);
}, 5000);
},
/* ... */
};

在该场景下,翻页组件需要在更新时而不是挂载时执行动画(因为没有上一个值)。因此我们在组件内部维护一个记录上一个值的状态,然后把动画从挂载阶段移动到更新阶段:


<template>
<div class="card">
<!-- 旧文字上 -->
<div
v-if="staleValue !== undefined"
class="half-card top-half">

{{ staleValue }}
</div>
<!-- 新文字下 -->
<div class="half-card bottom-half">{{ value }}</div>
<!-- 旋转面板 -->
<div ref="rotate" class="rotating-half">
<!-- 新文字上 -->
<div class="half-card front-side">{{ value }}</div>
<!-- 旧文字下 -->
<div
v-if="staleValue !== undefined"
class="half-card back-side">

{{ staleValue }}
</div>
</div>
</div>

</template>

<script>
export default {
props: ['value'],
data() { return { staleValue: undefined }; },
watch: {
value(_, old) { this.staleValue = old; },
},
updated() {
this.$refs.rotate?.animate?.(
[{ offset: 0, transform: 'rotateX(-180deg)' }],
{ duration: 1000, easing: 'ease-in-out' },
);
},
};
</script>


基本完成:


22.gif


总结一下


实现翻页效果 = 实现两块静态面板 + 实现一块双面旋转面板 + 播放旋转动画。


这里用vue写了demo, react应该也差不多,将updated换成layoutEffect等等。


另外,动画也可以用类名加css实现,当元素不在视口可以不播放,一些样式可以改成props配置。总之应该有不少地方还可以迭代优化下。


参考文章如下,分析思路基本一致,代码实现上有差异:

【1】优雅的时钟翻页效果,让你的网页时钟与众不同!

【2】原生JS实现

一个翻页时钟

收起阅读 »

用js脚本下载某书的所有文章

web
前言 在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。 想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是...
继续阅读 »

前言


在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。


想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是下载功能根据日期什么判断了,还是bug了,试了好几次都这样,官方渠道只能放弃了。


手动一篇一篇粘贴的成本太高了,不仅有发布的文章,还有各种没有发布的笔记在里面,各种文章笔记加起来好几百篇呢,既然是工程师,就用工程师思维解决实际问题,能用脚本下载个人账号的下的所有文章吗?


思考.gif


思路梳理


由于是下载个人账号下的所有文章,包含发布的和未发布的,来看下个人账号的文章管理后台


文集模式.png


根据操作以及分析浏览器控制台 网络 请求得知,文章管理后台逻辑是这样的,默认查询所有文集(文章分类列表), 默认选中第一个文集,查询第一个文集下的所有文章,默认展示第一篇文章的内容,点击文集,获取当前文集下的所有文章,默认展示文集中的第一篇文章,点击文章获取当前文章数据,来分析一下相关的接口请求


获取所有文集


https://www.jianshu.com/author/notebooks 这个 Get 请求是获取所有 文集,用户信息是放在 cookie


分析请求模式.png


来看下返回结果


[
    {
        "id": 51802858,
        "name": "思考,工具,痛点",
        "seq": -4
    },
    {
        "id": 51783763,
        "name": "安全",
        "seq": -3
    },
    {
        "id": 51634011,
        "name": "数据结构",
        "seq": -2
    },
    ...
]

接口返回内容很简单,一个 json 数据,分别是:id、文集名称、排序字段。


获取文集中的所有文章


https://www.jianshu.com/author/notebooks/51802858/notes 这个 Get 请求是根据 文集id 获取所有文章,51802858"思考,工具,痛点" 文集的id, 返回数据如下


[
    {
        "id": 103888430, // 文章id
        "slug": "984db49de2c0",
        "shared": false,
        "notebook_id": 51802858, // 文集id
        "seq_in_nb": -4,
        "note_type": 2,
        "autosave_control": 0,
        "title": "2022-07-18", // 文章名称
        "content_updated_at": 1658111410,
        "last_compiled_at": 0,
        "paid": false,
        "in_book": false,
        "is_top": false,
        "reprintable": true,
        "schedule_publish_at": null
    },
    {
        "id": 98082442,
        "slug": "6595bc249952",
        "shared": false,
        "notebook_id": 51802858,
        "seq_in_nb": -3,
        "note_type": 2,
        "autosave_control": 3,
        "title": "架构图",
        "content_updated_at": 1644215292,
        "last_compiled_at": 0,
        "paid": false,
        "in_book": false,
        "is_top": false,
        "reprintable": true,
        "schedule_publish_at": null
    },
    ...
]

接口返回的 json 数据里包含 文章id文集名称,这是接下来需要的字段,其他字段暂时忽略。


获取文章内容


https://www.jianshu.com/author/notes/98082442/content 这个 Get 请求是根据 文章id 获取文章 Markdown 格式内容, 98082442《架构图》 文章的id, 接口返回为 Markdown 格式的字符串


{"content":"![微服务架构图 (3).jpg](https://upload-images.jianshu.io/upload_images/6264414-fa0a7893516725ff.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"}

现在,我们了解清楚了文集,文章,以及文档内容的获取方式,接下来开始脚本实现。


代码实现


由于我是前端攻城狮,优先考虑使用 js 来实现下载,还有一个考虑因素是,接口请求里面的用户信息是通过读取 cookie 来实现的,js 脚本在浏览器的控制台执行发起请求时,会自动读取 cookie,很方便。


如果要下载个人账号下所有文章的话,根据梳理出来的思路编写代码就行


获取所有文集id


fetch("https://www.jianshu.com/author/notebooks")
  .then((res) => res.json())
  .then((data) => {
    // 输出所有文集
    console.log(data);
  })

使用fetch函数进行请求,得到返回结果,上面的代码直接在浏览器控制台执行即可,控制台输出效果如下


输出所有文集.png


根据文集数据获取所有文章


上一步得到了所有文集,使用 forEach 循环所有文集,再根据 文集id 获取对应文集下的所有文章,依然使用 fetch 进行请求


...
let wenjiArr = [];
wenjiArr = data; // 文集json数据
let articleLength = 0;
wenjiArr.forEach((item, index) => {
  // 根据文集获取文章
  fetch(`https://www.jianshu.com/author/notebooks/${item.id}/notes`)
    .then((res2) => res2.json())
    .then((data2) => {
      console.log("输出文集下的所有文章:", data2);
    });
});

根据文章id获取文章内容,并下载 Markdown 文件


有了文章 id, 根据 id 获取内容,得到的内容是一个对象,对象中的 content 属性是文章的 Markdown 字符串,使用 Blob 对象和 a 标签,通过 click() 事件实现下载。


在这里的代码中使用 articleLength 变量记录了一下文章数量,使用循环中的文集名称和文章名称拼成 Markdown 文件名 item.name - 《item2.title》.md


...
console.log(item.name + " 文集中的文章数量: " + data2.length);
articleLength = articleLength + data2.length;
console.log("articleLength: ", articleLength);
data2.forEach(async (item2, i) => {
// 根据文章id获取Markdown内容
fetch(`https://www.jianshu.com/author/notes/${item2.id}/content`)
.then((res3) => res3.json())
.then((data3) => {
console.log(data3);
const blob = new Blob([data.content], {
type: "text/markdown",
});
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = item.name + " - 《" + item2.title + `》.md`;
link.click();
});
});

代码基本完成,运行


在浏览器控制台中运行写好的代码,浏览器下方的下载提示嗖嗖的显示,由于没有做任何处理,当前脚本执行过程中报错了,文章下载了几十个以后就停止了,提示 429



HTTP 请求码 429 表示客户端发送了太多的请求,服务器无法处理。这种错误通常表示服务器被攻击或过载。



文章内容太多了,意料之中的情况,需要改进代码


思路改进分析


根据问题分析,脚本里的代码是循环套循环发请求的,这部分改造一下试试效果。


把每个循环里面发送 fetch 请求的外面是加个 setTimeout, 第一个循环里面的 setTimeout 延迟参数设置为 1000 * indexindex 为当前循环的索引,第一个请求0秒后执行,后面每一次都加1秒后执行,由于文集的数量不多,大约20个,这一步这样实现是没问题的。


重点是第二个循环,根据文集获取所有文章,每个文集里多的文章超过50篇,少的可能2,3篇,这里面的 setTimeout 延迟参数这样设置 2000 * (i + index)i 为第二个循环的索引,这样保证在后面的请求中避免了某个时间段发送大量请求,导致丢包的问题。


再次执行代码,对比控制台输出的文章数量和下载目录中的文章(项目)数量,如果一致,说明文章都下载好了


390.png


下载.png


改造后的完整代码地址


github.com/gywgithub/F…


思考


整体来看,文章下载脚本的逻辑并不复杂,接口参数也简单明确,两个 forEach 循环,三个 fetch 请求,把获取到的文章内容实用 a 标签下载下来就行了。关于大量请求发送导致 429 或者请求丢失的问题,脚本中使用了一种方案,当时还想到了另外两种方案:


请求同步执行


通过同步的方式先得到所有文集下的所有文章,再根据文章列表数组同步发请求下载,请求一个个发,文章一篇篇下载


Promise.all


使用 Promise.all() 分批发送请求,避免一次请求发送太多



也可能还有其他的解决方案,欢迎大家评论区讨论交流,一起学习共同进步 ^-^



作者:草帽lufei
来源:juejin.cn/post/7245184987531018300

收起阅读 »

这道面试题真的很变态吗?😱

web
最近帮公司招聘,主要负责一面,所以基本上问的基础多一点。但是我在问这样一道面试题的时候,很少有人答对。不少人觉得我问这道题多少有点过分了😭,当然了面试还是奔着相互沟通相互学习的目的,并不是说这道题不会就被刷掉,单纯的觉得这道题有意思。话不多说,我们直接上题 题...
继续阅读 »

最近帮公司招聘,主要负责一面,所以基本上问的基础多一点。但是我在问这样一道面试题的时候,很少有人答对。不少人觉得我问这道题多少有点过分了😭,当然了面试还是奔着相互沟通相互学习的目的,并不是说这道题不会就被刷掉,单纯的觉得这道题有意思。话不多说,我们直接上题


题目


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()


function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()

这里我会要求面试者从上到下依次说出执行结果。普遍多的面试者给出的答案是:foo1、foo2、foo1、foo2。虽然在我看来这是一道简单的面试题,但是也不至于这么简单吧😱~~~


当然面试本来就是一个相互讨论的过程,那就和面试者沟通下这道题我的理解,万一我理解错了呢😂


解答


拆分函数表达式


首先我会让面试者先看前面两个函数


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()

这时候大部分人基本上都可以答对了,是:foo1、foo2。再有很少数的人答不对那就只能”施主,出门右转“😌。接着根据我当时的心情可能会稍作追问(美女除外🙂):


foo()
var foo = function () {
console.log("foo1")
}

这时候又有一部分的人答不上来了。这毫无疑问是肯定会报错的啊


image.png


我们都知道用var定义的变量会变量提升,所以声明会被拿到函数或全局作用域的顶部,并且输出undefined。所以当执行foo()的时候,foo还是undefined,所以会报错。由于js从按照顺序从上往下执行,所以当执行foo = function(){}的时候,才对foo进行赋值为一个函数。我们重新看拆分之后的代码


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()

foo首先会变量提升,然后进行赋值为function。所以当执行第一个foo的时候,此时foo就是我们赋值的这个函数。接着执行第二个foo的赋值操作,由于函数作用域的特性,后面定义的函数将覆盖前面定义的函数。
由于在调用函数之前就进行了函数的重新定义,所以在调用函数时,实际执行的是最后定义的那个函数。所以上面的代码会打印:foo1、foo2。


这种定义函数的方式,我们称为函数表达式。函数表达式是将函数作为一个值赋给一个变量或属性


函数表达式我们拆分完了,下面就看看函数声明吧。


拆分函数声明


function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()

大部分人其实都卡在了这里。函数声明会在任何代码执行之前先被读取并添加到执行上下文,也就是函数声明提升。说到这里其实大多数人就已经明白了。这里使用了函数声明定义了两个foo函数,由于函数声明提升,第二个foo会覆盖第一个foo,所以当调用第一个foo的时候,其实已经被第二个foo覆盖了,所以这两个打印的都是foo2。


当两段代码结合


当开始解析的时候,函数声明就已经提升了,第四个foo会覆盖第三个foo。然后js开始从上往下执行,第一个赋值操作之后执行foo()后,打印了”foo1“,第二个赋值之后执行foo(),打印了"foo2"。下面两个foo的执行其实是第二个赋值了的foo,因为函数声明开始从刚开始就被提升了,而下面的赋值会覆盖foo。


总结


我们整体分析代码的执行过程



  1. 通过函数表达式定义变量foo并赋值为一个匿名函数,该函数在被调用时打印"foo1"。

  2. 接着,通过函数表达式重新定义变量foo,赋值为另一个匿名函数,该函数在被调用时打印"foo2"。

  3. 使用函数声明定义了两个名为foo的函数。函数声明会在作用域中进行提升。后面的会覆盖前面的,由于声明从一开始就提升了,而又执行了两个赋值操作,所以此时foo是第二个赋值的函数。

  4. 然后调用foo(),输出"foo2"。

  5. 再调用foo(),也输出"foo2"。


其实就一个点: 函数表达式相对于函数声明的一个重要区别是函数声明在代码解析阶段就会被提升(函数声明提升),而函数表达式则需要在赋值语句执行到达时才会创建函数对象


小伙伴们,以上是我的理解,欢迎在评论区留言,大家相互讨论相互学习。


之前的描述确实有点不妥,所以做了改动,望大家谅解,还

作者:翰玥
来源:juejin.cn/post/7237051958993469496
是本着相互学习的态度

收起阅读 »

别再无聊地显示隐藏了,Vue 中使用过渡动画让你的网页更有活力

web
Vue 是一款流行的前端框架,支持过渡动画的实现是其中的一项重要特性。在 Vue 中,使用过渡动画可以为用户提供更加友好的用户体验。下面我将为大家介绍一下子如何在 Vue 中实现过渡动画。 1. 你知道什么是过渡动画吗 过渡动画是指在 DOM 元素从一个状态到...
继续阅读 »

Vue 是一款流行的前端框架,支持过渡动画的实现是其中的一项重要特性。在 Vue 中,使用过渡动画可以为用户提供更加友好的用户体验。下面我将为大家介绍一下子如何在 Vue 中实现过渡动画。


1. 你知道什么是过渡动画吗


过渡动画是指在 DOM 元素从一个状态到另一个状态发生变化时,通过添加过渡效果使得这个变化看起来更加平滑自然的动画效果。在 Vue 中,过渡动画可以应用到以下几个场景中:



  • 显示和隐藏元素

  • 动态添加或删除元素

  • 元素位置的变化


2. Vue 过渡动画的实现方法


2.1 CSS 过渡


Vue 提供了 transition 组件来支持过渡动画。我们可以在需要应用过渡动画的元素外层包裹一个 transition 组件,并通过设置 CSS 样式或绑定动态 class 来实现过渡动画的效果。


Vue 的过渡动画通过添加 CSS 类名来实现。我们可以通过为需要过渡的元素添加 v-ifv-show 指令来控制元素的显示和隐藏,然后使用 transition 组件进行动画效果的设置。


下面我写个示例给大家参考一下,我将给按钮添加过渡动画效果:


<template>
<button @click="show=!show">Toggle</button>
<transition name="fade">
<div v-if="show">Hello, World!</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false
};
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

在上面的代码思路中,我们在 transition 包裹的 div 元素上使用了 v-if 指令来控制元素的显示和隐藏。同时,我们给 transition 组件添加了一个 name 属性,并使用 CSS 样式来定义过渡动画的效果。其中,.fade-enter-active.fade-leave-active 分别表示进入和离开时的过渡动画,而 .fade-enter.fade-leave-to 则分别表示进入和离开时元素的样式。


2.2 JS 过渡


除了使用 CSS 过渡外,在 Vue 中也可以使用 JavaScript 过渡来实现动画效果。JS 过渡相比于 CSS 过渡的优势在于它可以更加灵活地控制过渡动画。


它与 CSS 过渡不同,Javascript 过渡可以更加灵活地控制过渡动画,可以实现更加丰富的效果。Vue 提供了事件钩子函数,使得我们可以自定义过渡动画的效果。


image.png


Vue 中提供了以下事件钩子函数:



  • before-enter

  • enter

  • after-enter

  • enter-cancelled

  • before-leave

  • leave

  • after-leave

  • leave-cancelled


我们可以使用 transition 组件的 mode 属性来设置过渡的模式,如果使用了 mode 属性,Vue 将会自动调用对应的钩子函数,我们可以通过这些钩子函数来自定义过渡效果。


下面是我写的一个基于 JS 过渡的演示Demo,我们将为按钮添加自定义的过渡动画:


<template>
<button @click="show=!show">Toggle</button>
<transition :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave">
<div v-if="show">Hello, World!</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false
};
},
methods: {
beforeEnter(el) {
el.style.opacity = 0;
el.style.transformOrigin = 'left';
},
enter(el, done) {
anime({
targets: el,
opacity: 1,
translateX: [20, 0],
easing: 'easeInOutQuad',
duration: 500,
complete: done
});
},
leave(el, done) {
anime({
targets: el,
opacity: 0,
translateX: [-20, 0],
easing: 'easeInOutQuad',
duration: 500,
complete: done
});
}
}
};
</script>

在上面的前端页面中,我们通过设置 transition 组件的 css 属性为 false 来禁用 CSS 过渡,然后我们使用了 before-enterenterleave 等钩子函数来自定义过渡动画。在这个示例代码中,我们使用了第三方动画库 Anime.js 来实现元素进入和离开时的动画效果,同时在 anime 动画完成后,我们还需要手动调用 done 函数来告知 Vue 过渡动画已经完成。


3. 小结一下


通过我写的这篇文章的介绍,可以让大家多了解了 Vue 过渡动画的基本概念,并且掌握了如何在 Vue 中实现过渡动画。不论是使用 CSS 过渡还是 JavaScript 过渡,都可以帮助我们为用户提供更加友好的用户体验。我希望本文对您有所帮助,如果您有任何疑问或建议,欢迎在评论区留言。


作者:Cosolar
来源:juejin.cn/post/7241874482574114875
收起阅读 »

for循环的代价

web
for循环的可控性比forEach好,但是它的代价却鲜为人知,这篇文章就是简单的探讨下这背后的代价。 先定一个基调,我们从作用域的角度去分析,也会按需讲一些作用域的知识。 作用域是什么? 要解释这个问题,先从程序的角度来看,几乎所有编程语言上来就会介绍自己的变...
继续阅读 »

for循环的可控性比forEach好,但是它的代价却鲜为人知,这篇文章就是简单的探讨下这背后的代价。 先定一个基调,我们从作用域的角度去分析,也会按需讲一些作用域的知识。


作用域是什么?


要解释这个问题,先从程序的角度来看,几乎所有编程语言上来就会介绍自己的变量类型,因为如果没有变量程序只能执行一些简单的任务。但是引入变量之后程序怎么才能准确的找到自己需要的变量。这就需要建立一套规则让程序能够准确的找到需要的变量,这样的规则被称为作用域。


块级作用域


块级作用域如同全局作用域和函数作用域一样,只不过块级作用域由花括号({})包裹的代码块创建的。在块级作用域内声明的变量只能在该作用域内访问,可以使用 let 或 const 关键字声明变量,可以在块级作用域内创建变量。
所以引擎在编译时是通过花括号({})包裹和声明关键字判断是否创建块级作用域,因此绝大多数的语句是没有作用域的,同时从语言设计的角度来说越少作用域的执行环境调度效率也就越高,执行时的性能也就越好。
基于这个原则,switch语句被设计为有且仅有一个作用域,无论它有多少个case语句,其实都是运行在一个块级作用域环境中的。
一些简单的、显而易见的块级作用域包括:


// 例1
try {
// 作用域1
}
catch (e) { // 表达式e位于作用域2
// 作用域2
}
finally {
// 作用域3
}

// 例2
//(注:没有使用大括号)
with (x) /* 作用域1 */; // <- 这里存在一个块级作用域

// 例3, 块语句
{
// 作用域1

除此之外,按上述理解,for语句也可以满足上述的条件。


for循环作用域


并不是所有的for循环都有自己的作用域,有且仅有


for ( <let/const> ...) ...

这个语法有自己的块级作用域。当然,这也包括相同设计的for await和for .. of/in ..。例如:


for await ( <let/const> x of ...) ...
for ( <let/const> x ... in ...)
for ( <let/const> x ... of ...) ...

已经注意到了,这里并没有按照惯例那样列出“var”关键字。简单理解就是不满足创建的条件。Js引擎在编译时,会对标识符进行登记,而为了兼容,将标识符分为了两类varNames 和 lexicalNames。以前 var 声明、函数声明将会登记在varNames,为了兼容varNames只有全局作用域和函数作用域两种,所以编译时会就近登记在全局作用域和函数作用域中且变量有“提升”效果。Es6新增的声明关键词将登记在lexicalNames,编译时会就近创建块级作用或就近登记在函数作用域中。



varNames 和 lexicalNames属性只是一个用于记录标识符的列表,是通过词法作用域分析,在当前作用域中做登记的。它们记录了当前作用域中的变量和函数的名称,以及它们的作用域信息,帮助 JavaScript 引擎在代码执行时正确地解析标识符的作用域。



关于作用域还有一点要说明,JavaScript采用词法作用域,这意味着变量的作用域在代码编写时就确定了,而不是在运行时确定。这与动态作用域不同,动态作用域是根据函数的调用栈来确定变量的作用域。
举个例子:


function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();

词法作用域下log的结果是2,动态作用域下log的是3。



词法作用域是指 JavaScript 引擎在编译时如何确定变量的作用域。在 JavaScript 中,词法作用域是指变量的作用域是由代码的位置(词法)决定的,而不是由运行时的作用域链决定的。



变量的作用域和可见性是由词法作用域和作用域链来决定的,作用域链是基于词法作用域和函数作用域来确定的,这也证明了 JavaScript 采用的是词法作用域。


for循环隐藏作用域


首先,必须要拥有至少一个块级作用域。如之前讲到的,满足引擎创建的条件。但是这一个作用域貌似无法解释下面这段代码


for(let i=0;i<10;i++){
let i=1;
console.log(i) // 1
}

这段代码时可以正常运行的,而我们知道let语句的变量不能重复声明的,所以对for循环来说一个作用域是满足了这个场景的。
但是这段代码依然可以执行,那JS引擎是如何处理的呢?
只能说明循环体又创建了一个块级作用域,事实如你所见,JS引擎确实对for循环的每个循环体都创建了一个块级作用域。
举个栗子,以下代码中使用 let 声明变量 i


for (let i = 0; i < 5; i++) {
console.log(i);
}

在编译时,JavaScript 引擎会将循环体包裹在一个块级作用域中,类似于以下代码:


{
let i;
for (i = 0; i < 5; i++) {
console.log(i);
}
}

每次循环都会创建一个新的块级作用域,因此,在循环中声明的变量 i 只能在当前块级作用域中访问,不会污染外部作用域的变量。而通过作用域链每个循环体内都可以访问外层变量i。
而我们知道从语言设计的角度来说越少作用域的执行环境调度效率也就越高,执行时的性能也就越好,所以这也算是代价之一吧。
就算如此设计还是无法解释下面这段代码。


for (let i = 0; i < 5; i++) {
setTimeout(()=>{console.log(i)})
}

如果按上述的理解,那最后log时访问的都是外层的变量i,最后的结果应该都是4,可事实却并非如此。当定时器被触发时,函数会通过它的闭包来回溯,并试图再次找到那个标识符i。然而,当定时器触发时,整个for迭代都已经结束了。这种情况下,访问i,获取到的也是上层作用域中的i,此刻i的值应该是最后一次赋。
之所能按我们预想的输出1,2,3,4,那是因为JavaScript 引擎在创建循环体作用域的时候,会在该作用域中声明一个新的变量 i,并将其初始化为当前的迭代次数,这个新的变量 i 会覆盖外层的变量 i。这个过程是由 JavaScript 引擎自动完成的,我们并不需要手动

作者:chtty
来源:juejin.cn/post/7245641209913360445
创建或赋值这个变量。

收起阅读 »

前端如何破解 CRUD 的循环

web
据说,西西弗斯是一个非常聪明的国王,但他也非常自负和狂妄。他甚至敢欺骗神灵,并把死者带回人间。为此,他被宙斯(Zeus)惩罚,被迫每天推着一块巨石上山,但在接近山顶时,巨石总是会滚落下来,他不得不重新开始推石头,永远困在这个循环中… 很多开发工作也如此单调而乏...
继续阅读 »

据说,西西弗斯是一个非常聪明的国王,但他也非常自负和狂妄。他甚至敢欺骗神灵,并把死者带回人间。为此,他被宙斯(Zeus)惩罚,被迫每天推着一块巨石上山,但在接近山顶时,巨石总是会滚落下来,他不得不重新开始推石头,永远困在这个循环中…


很多开发工作也如此单调而乏味,比如今天要讲的中后台开发的场景。中后台业务基本上就是一些数据的增删改查、图表,技术含量不高,比较容易范式化。


前端如何破除这种 CRUD 的单调循环呢?








低代码


过去几年前端的低代码很火,这些低代码平台通常支持创建数据模型后,一键生成对应的增删改查页面:




模型驱动生成页面





💡 本文提及的低代码是狭义低代码,你可以认为就是可视化搭建平台





低代码在过去几年就是 「雷声大,雨点小」,跟现在的 AI 颇为相似。


不管是大厂还是小厂都在搞低代码,包括笔者也参与过几个低代码项目,但是小厂支撑不起来这样的资源投入,最后都胎死腹中。我相信很多读者也经历过这种情况。
大部分公司只是尾随市场营销噱头,盲目跟风,压根就没有做这种低代码平台资源准备和沉淀。


作为前端,能参与到低代码项目的开发是一件非常兴奋的事情,毕竟是少数前端能主导的项目,架构、组件设计、编辑器的实现可玩性很高,可以跟同行吹很久。


作为用户(开发者)呢?可能会排斥和质疑,不管怎么说,它并没有发挥市场所期望的价值。




最主要的原因是:它解决不了复杂的问题




低代码直观、门槛低, 前期开发确实很爽,可视化数据建模、拖拉拽生成页面、流程编排,很快就可以把一些简单的业务开发出来。


然而软件编码本身占用研发流程的比例,据 ChatGPT 估算大约只有 20% ~ 30%。而且业务持续变化,代码也需要持续迭代。试想一下如何在这些低代码平台上进行重构和检索?






总的来说,有一些缺点:




  • 复杂的业务逻辑用低代码可能会更加复杂。低代码应该是特定领域问题的简化和抽象,如果只是单纯将原有的编码工作转换为 GUI 的模式,并没有多大意义。


    例如流程编排,若要用它从零搭建一个复杂的流程,如果照搬技术语言去表达它,那有可能是个地狱:


    流程编排


    理想的流程编排的节点应该是抽象程度更高的、内聚的业务节点,来表达业务流程的流转。然而这些节点的设计和开发其实是一件非常有挑战性的事情。




  • 软件工程是持续演进的,在可维护性方面,目前市面上的低代码平台并不能提供可靠的辅助和验证。因此企业很难将核心的稳态业务交给这些平台。




  • 还有很多… 平台锁定,缺乏标准,性能问题、复用、扩展性、安全问题、黑盒,可迁移性,研发成本高,可预测性/可调试性差,高可用,版本管理,不能自动化…








当然,低代码有低代码的适用场景,比如解决特定领域问题(营销活动页面,海报,数据大屏,表单引擎、商城装修、主页),POC 验证。即一些临时的/非核心的敏态业务



💡 目前有些低代码平台也有「出码能力」,让二开有了一定的可行性。




💡 AI 增强后的低代码可能会更加强大。但笔者依旧保持观望的态度,毕竟准确地描述软件需求,本身就是就是软件研发的难题之一,不然我们也不需要 DDD中的各种方法论,开各种拉通会,或许也不需要需求分析师,产品…


非专业用户直接描述需求来产出软件,大多是不切实际的臆想









中间形态


有没有介于可视化低代码平台和专业代码之间的中间形态?既能保持像低代码平台易用性,同时维持代码的灵活性和可维护性。


我想那就是 DSL(domain-specific language) 吧? DSL 背后体现的是对特定领域问题的抽象,其形式和语法倒是次要的。



💡 DSL 的形式有很多,可以创建一门新的微语言(比如 SQL, GraphQL);可以是一个 JSON 或者 YAML 形式;也可以基于一门现有的元语言(比如 Ruby、Groovy,Rust…)来创建,这些元语言,提供的元编程能力,可以简洁优雅地表达领域问题,同时能够复用元语言 本身的语言能力和基础设施。



严格上可视化低代码平台也是一种‘可视化’ 的 DSL,笔者认为它的局限性更多还是来源‘可视化’,相对的,它优点也大多来源’可视化‘



这又牵扯到了持续了半个多世纪的: GUI vs CLI(程序化/文本化) 之争。这个在《UNIX 编程艺术》中有深入的探讨。命令行和命令语言比起可视化接口来说,更具表达力,尤其是针对复杂的任务。另外命令行接口具有高度脚本化的能力。缺点就是需要费劲地记忆,易用性差,透明度低。当问题规模变大、程序的行为日趋单一、过程化和重复时, CLI 也常能发挥作用。

如果按照友好度和问题域的复杂度/规模两个维度来划分,可以拉出以下曲线:

友好曲线


中间会出现一个交叉点,在这个交叉点之后,命令行的简要行和表达力变得要比避免记忆负担更有价值。


《反 Mac 接口》一书中也进行了总结:可视化接口在处理小数量物体简单行为的情况下,工作的很好,但是当行为或物体的数量增加是,直接操作很快就编程机械重复的苦差…



也就是说,DSL 的形式会约束 DSL 本身的表达能力。




正如前文说的,如果‘低代码’仅仅是将原本的编码工作转换为 GUI 形式,其实并没有多大意义,因为没有抽象。


反例:JSON GUI vs JSON


JSON GUI vs JSON






正例: VSCode 案例


setting in json


setting in gui


充分利用 GUI 的优势,提供更好的目录组织、文本提示、数据录入的约束和校验。






我们可能会说 GUI 形式用户体验更好,门槛低更低,不用关心底层的细节。其实并不一定是 GUI 带来的,而是抽象后的结果。GUI 只不过是一种接口形式




回到正题,为了摆脱管理后台 CRUD 的 「西西弗斯之石」: 我们可以创建一个 DSL,这个 DSL 抽象了管理端的各种场景,将繁琐的实现细节、重复的工作封装起来,暴露简洁而优雅的用户接口(User Interface)。



💡 小结。DSL 是可视化低代码与 pro code 之间的中间中间形态,权衡了易用性/灵活性和实现成本。DSL 的形式会直接影响它的表达能力,但比形式更重要的是 DSL 对特定问题域的抽象。


我们不必重新发明一门语言,而是复用元语言的能力和生态,这基本上是零成本。











抽象过程


典型的增删改查页面:


CRUD


分析过程:



  1. 后端增删改查主要由两大组件组成: 表单表格

  2. 而表单和表格又由更原子的’字段’组成。字段的类型决定了存储类型、录入方式、和展示方式

  3. 字段有两种形态:编辑态预览态。表格列、详情页通常是预览态,而表单和表格筛选则使用编辑态。




预览态和编辑态


借鉴低代码平台的组件库/节点库,我们可以将这些‘字段’ 提取出来, 作为表单和表格的‘原子’单位, 这里我们给它取个名字,就叫原件(Atomic)吧。


低代码平台


原件将取代组件库里面的表单组件,作为我们 CRUD 页面的最小组成单位。它有且只有职责:


原件



  • 数据类型和校验。原件代表的是一种数据类型,可以是基础类型,比如数字、字符串、布尔值、枚举;也可以是基础类型上加了一些约束和交互,比如邮件、手机号码、链接;甚至可能有业务属性,比如用户,商品,订单,二维码。

  • 数据的预览。

  • 数据的录入,严格约束为 value/onChange 协议。好处是方便进行状态管理,可能保证原件实现的统一性。






接着组合原件来实现表单和表格组件,满足 CRUD 场景:


CRUD


理想状态下,我们仅需声明式地指定表格的列和原件类型,其余的技术细节应该隐藏起来。表格伪代码示例:


# 创建包含 名称、创建时间、状态三列的表格,其中可以搜索名称和创建时间
Table(
columns(
column(名称,name, queryable=true)
column(创建时间, created, data-range, queryable=true)
column(状态, status, select, options=[{label: 启用,value: 1, {label: 禁用, value: 0}}])
)
)



表单伪代码示例:


# 创建包含 名称、状态、地址的表单
Form(
item(名称,name, required=true)
item(状态,status, select, options=[{label: 启用,value: 1, {label: 禁用, value: 0}}])
item(地址, address, address)
)



如上所示,本质上,开发者就应该只关注业务数据本身,而应该忽略掉前端技术实现的噪音(比如状态管理、展示风格、分页、异常处理等等)。






表格和表单为了适应不同的需求,还会衍生出不同的展现形式:


概览图


原件 + 核心的表单/表格能力 + 场景/展示形式,一套「组合拳」下来,基本就可以满足常见的后台 CRUD 需求了。








约定大于配置


前端的在研发流程中相对下游,如果上游的产品定义,UI 设计,后端协议没有保持一致性,就会苦于应付各种混乱的差异,复用性将无从谈起。


为了最小化样板代码和沟通成本,实现开箱即用的效果。我们最好拉通上下游,将相关的规范确定下来,前端开发者应该扮演好串联的角色。




这些规范包含但不限于:



  • 页面的布局

  • UI 风格

  • 提示语

  • 验证规则

  • 数据的存储格式

  • 通用的接口(比如文件上传,导入导出)



概览图


组件库可以内置这些约定,或者提供全局的配置方式。这些规范固化后,我们就享受开箱即用的快感了。








实现示例


基于上述思想,我们开发了一套组件库(基于 Vue 和 element-ui),配合一套简洁的 DSL,来快速开发 CRUD 页面。





💡 这套组件库耦合了我们自己的约定。因此可能不适用于外部通用的场景。本文的意义更多是想启发读者,去构建适合自己的一套解决方案。



列表页定义:


表格示例


import { defineFatTable } from '@wakeadmin/components'

/**
* 表格项类型
*/

export interface Item {
id: number
name: string
createDate: number
}

export const MyTable = defineFatTable<Item>(({ column }) => {
// 可以在这里放置 Vue hooks
return () => ({
async request(params) {
/* 数据获取,自动处理异常和加载状态 */
},
// 删除操作
async remove(list, ids) {
/*列删除*/
},
// 表格列
columns: [
// queryable 标记为查询字段
column({ prop: 'name', label: '名称', queryable: true }),
column({ prop: 'createDate', valueType: 'date-range', label: '创建时间', queryable: true }),
column({
type: 'actions',
label: '操作',
actions: [{ name: '编辑' }, { name: '删除', onClick: (table, row) => table.remove(row) }],
}),
],
})
})

语法类似于 Vue defineComponent,传入一个’setup’, 。这个 setup 中可以放置一些逻辑和状态或者 Vue hooks,就和 Vue defineComponent 定义一样灵活。


返回关于表格结构的”声明”。最优的情况下,开发者只需要定义表格结构和后端接口,其余的交由组件库处理。


当然复杂的定制场景也能满足,这里可以使用 JSX,监听事件,传递组件支持的任意 props 和 slots。






表单页示例:


表单示例


import { defineFatForm } from '@wakeadmin/components'
import { ElMessageBox } from 'element-plus'

export default defineFatForm<{
// 🔴 这里的泛型变量可以定义表单数据结构
name: string
nickName: string
}>(({ item, form, consumer, group }) => {
// 🔴 这里可以放置 Vue Hooks

// 返回表单定义
return () => ({
// FatForm props 定义
initialValue: {
name: 'ivan',
nickName: '狗蛋',
},

submit: async (values) => {
await ElMessageBox.confirm('确认保存')
console.log('保存成功', values)
},

// 🔴 子节点
children: [
item({ prop: 'name', label: '账号名' }),
item({
prop: 'nickName',
label: '昵称',
}),
],
})
})


💡 和 tailwind 配合食用更香。我们假设整体的页面是符合UI规范的,细微的调整使用 tw 会很方便







全局配置:


import { provideFatConfigurable } from '@wakeadmin/components'
import { Message } from 'element-ui'

export function injectFatConfigurations() {
provideFatConfigurable({
// ...
// 统一处理 images 原件上传
aImagesProps: {
action: '/upload',
},
// 统一 date-range 原件属性
aDateRangeProps: {
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'yyyy-MM-dd',
shortcuts: [
{
text: '最近一周',
onClick(picker: any) {
picker.$emit('pick', getTime(7))
},
},
{
text: '最近一个月',
onClick(picker: any) {
picker.$emit('pick', getTime(30))
},
},
{
text: '最近三个月',
onClick(picker: any) {
picker.$emit('pick', getTime(90))
},
},
],
},
})
}





更多示例和深入讲解见这里








更多实现


前端社区有很多类似的产品,比如:



  • XRender。中后台「表单/表格/图表」开箱即用解决方案

  • Antd ProComponents。ProComponents 是基于 Ant Design 而开发的模板组件,提供了更高级别的抽象支持,开箱即用。可以显著的提升制作 CRUD 页面的效率,更加专注于页面

  • 百度 Amis 。 用 JSON 作为 DSL,来描述界面


读者不妨多参考参考。








总结


简单来说,我们就是从提供「毛坯房」升级到了「精装房」,精装房的设计基于我们对市场需求的充分调研和预判。目的是对于 80% 的用户场景,可以实现拎包入住,当然也允许用户在约束的范围内改装。


本文主要阐述的观点:



  • 低代码平台的高效和易用大多来源于抽象,而不一定是 GUI,GUI ≠ 低代码。

  • 摆脱「西西弗斯之石」 考验的是开发者的抽象能力,识别代码中固化/重复的逻辑。将模式提取出来,同时封装掉底层的实现细节。最终的目的是让开发者将注意力关注到业务本身,而不是技术实现细节。

  • 用声明式、精简、高度抽象 DSL 描述业务 。DSL 的形式会约束他的表达能力,我们并不一定要创建一门新的语言,最简单的是复用元语言的生态和能力。

  • 约定大于配置。设计风格、交互流程、数据存储等保持一致性,才能保证抽象收益的最大化。因此规范很重要。这需要我们和设计、产品、后端深入沟通,达成一致。

  • 沉淀原件。低代码平台的效率取决于平台提供的组件能力、数量和粒度。比如前端的组件库,亦或者流程引擎的节点,都属于原件的范畴。

  • 要求不要太高,没有万精油方案,我们期望能满足 80% 常见的场景,这已经是一个很好的成绩。至于那 20% 的个性需求,还是从毛坯房搞起吧。








扩展阅读


收起阅读 »

你的代码着色好看吗?来这里看看吧!

web
如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。 那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。 你只需要使用一个叫做 highlight.js 的第三方...
继续阅读 »

如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。


那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。


你只需要使用一个叫做 highlight.js 的第三方库,就可以轻松实现代码着色的效果。



highlight.js 是一个非常强大和流行的库,它可以自动识别和着色超过 190 种编程语言。


它支持多种主题和样式,让你可以根据自己的喜好选择合适的配色方案。


在本文中,子辰将向你介绍如何使用 highlight.js 来为你的代码着色,以及它的基本原理和优势。


让我们开始吧!


如何使用 highlight.js


使用 highlight.js 的方法有两种:一种是通过 npm 下载并安装到你的项目中,另一种是通过 CDN 引入到你的网页中。


这里我们以 CDN 的方式为例,如果你想使用 npm 的方式,可以参考官方文档。


首先,我们需要在网页中引入 highlight.js 的 JS 文件和 CSS 文件。


JS 文件是核心文件,负责识别和着色代码,CSS 文件是样式文件,负责定义代码的颜色和格式。



我们可以从 CDN 中选择一个合适的 JS 文件和 CSS 文件。


highlight.js 提供了多个 CDN 服务商,你可以根据自己的需求选择一个,这里我们以 jsDelivr 为例。


JS 文件的链接如下:


<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js"></script>

CSS 文件的链接则需要根据你想要的主题来选择。


highlight.js 提供了很多主题,你可以在官网上预览每个主题的效果,并找到对应的 CSS 文件名,这里我们以 github-dark 为例。


CSS 文件的链接如下:


<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github-dark.min.css">

将上面两个链接分别放到网页的 head 标签中,就完成了引入 highlight.js 的步骤。


接下来,我们需要在网页中写一些代码,并用 pre 标签和 code 标签包裹起来。


pre 标签用于保留代码的格式,code 标签用于标识代码内容。例如:


<pre>
<code id="code-area">
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
height: 100vh;
width: 100vw;
}
</code>
</pre>

注意,我们给 code 标签添加了一个 id 属性,方便后面通过 JS 获取它。


最后,我们需要在网页中添加一些 JS 代码,来调用 highlight.js 的方法,实现代码着色的功能。


highlight.js 提供了两个主要的方法:highlightElement 和 highlight。


这两个方法都可以实现代码着色的效果,但是适用于不同的场景。


highlightElement


highlightElement 方法适用于当你的代码是直接写在网页中的情况。


这个方法接受一个元素作为参数,并将该元素内部的文本内容进行着色处理。例如:


// 获取 code 元素
const codeEle = document.getElementById("code-area");
// 调用 highlightElement 方法,传入 code 元素
hljs.highlightElement(codeEle);

如果一切顺利,你应该能看到类似下图的效果:



代码已经被着色了,并且你可以看到代码被替换成了一个个标签,标签被加上了样式。


在最后的原理里我们在详细的说一下。


highlight


highlight 方法适用于当你的代码是通过 Ajax 请求获取到的纯文本数据的情况。


这个方法接受一个字符串作为参数,并返回一个对象,包含着色后的文本内容和代码的语言。例如:


<script>
const codeEle = document.getElementById('code-area')
// 比如说现在 code 就是 Ajax 返回的数据,lang 就是代码语言,content 就是代码内容
const code = {
lang: 'css',
content: `
* {
margin: 0;
padding: 0;
}`

}
// 我们接下来可以使用 hljs.highlight,将代码内容与代码语言传入进去
const result = hljs.highlight(code.content, {
language: code.lang
})
// 它会返回一个结果,我们打印到控制台看看
console.log('result >>> ', result)
</script>


我们可以看到,打印出来的是一个对象,code 是它原始的代码,language 是它的语言,而 value 就是它着色后的代码。


那么现在要做的就是将 value 添加到 code 元素里边去。


<script>
const code = {
lang: 'css',
content: `
* {
margin: 0;
padding: 0;
}`

}
const result = hljs.highlight(code.content, {
language: code.lang
})
const codeEle = document.getElementById('code-area')
codeEle.innerHTML = result.value
</script>


我们可以看到,代码确实被着色了,但是和之前的有所差别,我们看一下是什么原因。



打开控制后我们发现,用这种方式 code 元素就没有办法被自动加上类样式了,所以说我们就需要手动给 code 加上类样式才可以。


// 通过 className 为 code 手动添加类样式,并添加类的语言
codeEle.className = `hljs language-${code.lang}`

highlight.js 的语言支持


无论使用哪种方法,都需要注意指定代码所属的语言。


如果不指定语言,highlight.js 会尝试自动识别语言,并可能出现错误或不准确的结。


指定语言可以通过两种方式:



  • 在 code 标签中添加 class 属性,并设置为 language-xxx 的形式,其中 xxx 是语言名称。

  • 在调用 highlightElement 或 highlight 方法时,在第二个参数中传入一个对象,并设置 language 属性为语言名称。



上图是 highlight.js 支持的语言,可以看到有很多种,需要用其他语言的时候,language 设置成指定的语言名称就可以了。


原理


它的原理你可能已经猜到了,在 highlightElement 里我们简单说了一下,现在再看下图:



之所以可以实现着色,其实就是查找和替换的过程,将原来的纯文本替换为元素标签包裹文本,元素是可以加上样式的,而样式就是我们引入的 css 文件。


这就是它的基本原理了。


总结


其实有时候我们去设置 Mackdown 的自定义样式呢,在代码区域设置的时候也是这样设置的,当然类样式的名字呢,基本上都是标准的格式。


好了,这个库分享介绍给你了,库的原理也为你做了简单的科普,希望对你有所帮助。


如果你有什么问题或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收

作者:子辰Web草庐
来源:juejin.cn/post/7245584147456507965
藏或分享给你的朋友!

收起阅读 »

优雅的使用位运算,省老多事了!!!

web
你好,我是泰罗凹凸曼,今天我们来一篇JS中的位运算科普,经常在源码中看到的位运算符,和用其定义的一系列状态到底有什么优势? 位运算符号的基本了解 首先,我们应该要简单了解位运算符,常用的位运算符大概有以下几种,我们可以在JS中使用 toString 将数字转换...
继续阅读 »

你好,我是泰罗凹凸曼,今天我们来一篇JS中的位运算科普,经常在源码中看到的位运算符,和用其定义的一系列状态到底有什么优势?


位运算符号的基本了解


首先,我们应该要简单了解位运算符,常用的位运算符大概有以下几种,我们可以在JS中使用 toString 将数字转换为二进制查看,也可以通过 0b 开头来手动创建一个二进制数字:


(3).toString(2) // 11
0b00000011 // 3 前面的几位0可以省略,可以简写为 0b11

1. 与 &


按位对比两个二进制数,如果对应的位都为 1,则结果为 1,否则为 0


console.log((1 & 3) == 1) // true

对比图例如下所示:



2. 或 |


按位对比两个二进制数,如果对应的位有一个 1,则结果为 1,否则为 0


console.log((1 | 3) == 3) // true

对比图例如下所示:



3. 异或 ^


按位对比两个二进制数,如果对应的位有且只有一个 1,则结果为 1,否则为 0


console.log((1 ^ 3) == 2) // true

对比图例如下所示:



4. 非 ~


按位对操作的二进制数取反,即 1 变 0,0 变 1,任何数的非运算符计算结果都是 -(x + 1)


const a = -1 // ~a = -(-1 + 1) = 0
console.log(~a) // 0
const b = 5 // ~b = -(5 + 1) = -6
console.log(~b) // -6

一个数和它的取反数相加的结果总为 -1


5. 左移 <<


左移会将二进制值的有效位数全部左移指定位数,被移出的高位(最左边的数字)丢弃,但符号会保留,低位(最右边的数字)会自动补0


console.log(1 << 2) // 4

图例如下所示:



6. 右移 >>


和左移相反的操作,将二进制的操作数右移指定位数,高位补0,低位丢弃!


console.log(4 >> 2) // 1

参考资料均来自 MDN,除了这些常用的符号之外,文档还标注了所有的JS操作符号,感兴趣的同学可以看一看!


有什么用?


说了这么多符号,对于操作符的影响是加深了,但是有什么用呢?二进制数字难理解,位操作符也难理解,二进制和十进制的互转不写个代码都心算不了,相信各位同学肯定有如此费解,我们先来看一段 Vue 的源代码,其中定义了很多状态类的字段!


源码位置戳这里



以及 Vue 中对其的使用,源码位置戳这里



我们可以看到,Vue 定义了一系列状态列标识一个 Dom 是属于什么类型,并用 VNode 中的一个字段 shapeFlag 来完成存储和判断,对状态的存储只用到了一个字段一个数字,就可以进行多种状态的判断!


我们尝试着设计一种类似的判断结构出来如何?


我有N个权限


假设系统中的用户我们规定其有增删改查四个权限,我们可以设计一个枚举类来标识拥有的四个权限:


enum UserPerm {
CREATE = 1 << 0,
DELETE = 1 << 1,
UPDATE = 1 << 2,
SELECT = 1 << 3,
}

我们设计的时候,完全不必在意上述的二进制的十进制值是什么,只需要清楚的是,上述枚举的 1 在二进制位的哪个位置,如 1 的 二进制为 00000001,将其左移 1(1 << 1), 就变成了 00000010, 依次类推,我们用一个二进制串中的每一位来标识一个权限,这样一个字符串中只要出现对应位置的 1, 则该用户就拥有对应位置的权限,如图:



有什么好处呢?


我们知道二进制是可以转换为十进制的,这样子我们就可以用一个数字来表示多个权限,如一个用户完整的拥有四个权限,那他的二进制为 0b1111, 那么其状态为数字 15


如果一个用户只有 CREATESELECT 的权限,那么二进制表达为 0b1001,十进制数字为 9


后端数据库中,前端用户信息中,接口返回都只有一列一个字段就可以表示,那么用户信息应该是下面的形式:


const userInfo = {
name: '泰罗凹凸曼',
phone: '15888888888',
perm: 9, // 代表其只有 CREATE 和 SELECT 两种权限
}

权限的判断


如何判断这个用户是否具备某一个权限呢?那就需要请出我们的 与运算符(&),参考 Vue 的做法:


console.log(userInfo.perm & UserPerm.CREATE) // 9 & (1 << 0) = 1

console.log(userInfo.perm & UserPerm.UPDATE) // 返回 0, 0代表不通过

如果 userInfo.perm 中包含 CREATE,就会返回 CREATE 的值,否则返回 0,在JS中,任何非0的数字都可以通过 if 判断,所以我们只需要一个判断就足够了!


if (userInfo.perm & UserPerm.CREATE) {
console.log('有创建权限')
} else {
console.log('没有创建权限')
}

什么原理?我们之前给过与运算符的图例,接下来我们看一下如上两句代码的图例所示:



我们看到,上下的符号位如果对不上的话,返回的结果都是 0,这样子我们就轻松实现了权限的判断


权限的增删


那么我们如何实现对一个用户的权限更新呢,比如给上面的用户新增一个 UPDATE 权限,这个时候我们就需要 或运算符(|)


比如:


userInfo.perm | UserPerm.UPDATE // 1001 | 0100 = 1101 = 13

这样子我们就对一个用户权限进行了增加,或的规则我们上面也给过图例,这里大家可以自己尝试理解一下,无非是两个二进制数 10010100 之间的或运算,只有其中一位为 1 则为 1,这两个数字计算的结果自然是 1101


那么如何实现权限删除呢?异或运算符(^)给你答案!有且只有一个 1,返回 1,否则为 0,删除对我们刚刚添加的 UPDATE 权限的方法:


userInfo.perm ^ UserPerm.UPDATE // 1101 ^ 0100 = 1001

非常简单是吧?看到这里,相信你已经完全理解位运算符在权限系统的妙用了,如果我这个时候需要添加一个新的权限,如分享权限,那么我只有用第五位的1来表示这个权限就可以啦


enum UserPerm {
SHARE = 1 << 5
}

// 添加分享权限
userInfo.perm | UserPerm.SHARE

以前的方案


我们以前在做用户标识的时候,通常会定义一个数组来表示,然后执行数组判断来进行权限的判断


const userPerm = ['CREATE', 'UPDATE', 'DELETE', 'SELECT']

// 判断有无权限
if (userPerm.includes('CREATE')) {
// ...
}

// 增加权限
user.perm.push('UPDATE')

// 删除权限
user.perm.splice(user.perm.indexOf('UPDATE'), 1)

相信大家也可以看出来,无论是从内存占用,效率,便捷程度来说位运算符的形式都是完胜,这也是会被各大项目使用的原因之一!快去你的项目中实践吧,记得写好注释哦!


结语


今天带大家认识了位运算符在权限系统的妙用,小伙伴们还有什么使用位运算符的巧妙思路,可以在评论中给出来哦!继续加油吧,快去实践少年!


祝大家越来越牛逼!


去探索,不知道的东西还多着呢,我是泰罗凹凸曼,M78星云最爱写代码的,我们下一篇再会!


作者:泰罗凹凸曼
来源:juejin.cn/post/7244809939838844984
收起阅读 »