注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

前端如何生成临时链接?

web
前言 前端基于文件上传需要有生成临时可访问链接的能力,我们可以通过URL.createObjectURL和FileReader.readAsDataURAPI来实现。 URL.createObjectURL() URL.createObjectURL() 静态...
继续阅读 »



前言


前端基于文件上传需要有生成临时可访问链接的能力,我们可以通过URL.createObjectURLFileReader.readAsDataURAPI来实现。


URL.createObjectURL()


URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。


1. 语法


let objectURL = URL.createObjectURL(object);

2. 参数


用于创建 URL 的 File 对象、Blob 对象或者 MediaSource 对象。


3. 返回值


一个DOMString包含了一个对象URL,该URL可用于指定源 object的内容。


4. 示例


"file" id="file">

document.querySelector('#file').onchange = function (e) {
console.log(e.target.files[0])
console.log(URL.createObjectURL(e.target.files[0]))
}

0f40e1fff9674142889f8bacc6d455b9.png


将上方console控制台打印的blob文件资源地址粘贴到浏览器中


blob:http://localhost:8080/1ece2bb1-b426-4261-89e8-c3bec43a4020

5cc4d088c5c941b7950f6f930cb9a1bc.png


URL.revokeObjectURL()


在每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。


浏览器在 document 卸载的时候,会自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。


1. 语法


window.URL.revokeObjectURL(objectURL);

2. 参数 objectURL


一个 DOMString,表示通过调用 URL.createObjectURL() 方法返回的 URL 对象。


3. 返回值


undefined


4. 示例


"file" id="file">
<img id="img1" style="width: 200px;height: auto" />
<img id="img2" style="width: 200px;height: auto" />

document.querySelector('#file').onchange = function (e) {
const file = e.target.files[0]

const URL1 = URL.createObjectURL(file)
console.log(URL1)
document.querySelector('#img1').src = URL1
URL.revokeObjectURL(URL1)

const URL2 = URL.createObjectURL(file)
console.log(URL2)
document.querySelector('#img2').src = URL2
}

ecba01284f034c42a2bf4200054b0e9f.png


与FileReader.readAsDataURL(file)区别


1. 主要区别



  • 通过FileReader.readAsDataURL(file)可以获取一段data:base64的字符串

  • 通过URL.createObjectURL(blob)可以获取当前文件的一个内存URL


2. 执行时机



  • createObjectURL是同步执行(立即的)

  • FileReader.readAsDataURL是异步执行(过一段时间)


3. 内存使用



  • createObjectURL返回一段带hashurl,并且一直存储在内存中,直到document触发了unload事件(例如:document close)或者执行revokeObjectURL来释放。

  • FileReader.readAsDataURL则返回包含很多字符的base64,并会比blob url消耗更多内存,但是在不用的时候会自动从内存中清除(通过垃圾回收机制)


4. 优劣对比



  • 使用createObjectURL可以节省性能并更快速,只不过需要在不使用的情况下手动释放内存

  • 如果不在意设备性能问题,并想获取图片的base64,则推荐使用FileReader.readAsDataURL




作者:sorryhc
来源:juejin.cn/post/7333236033038778409
收起阅读 »

Vue3 实现最近很火的酷炫功能:卡片悬浮发光

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 有趣的动画效果 前几天在网上看到了一个很有趣的动画效果,如下,光会跟随鼠标在卡片上进行移动,并且卡片会有视差的效果 那么在 Vue3 中应该如何去实现这个效果呢...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


有趣的动画效果


前几天在网上看到了一个很有趣的动画效果,如下,光会跟随鼠标在卡片上进行移动,并且卡片会有视差的效果


那么在 Vue3 中应该如何去实现这个效果呢?



基本实现思路


其实实现思路很简单,无非就是分几步:



  • 首先,卡片是相对定位,光是绝对定位

  • 监听卡片的鼠标移入事件mouseenter,当鼠标进入时显示光

  • 监听卡片的鼠标移动事件mouseover,鼠标移动时修改光的left、top,让光跟随鼠标移动

  • 监听卡片的鼠标移出事件mouseleave,鼠标移出时,隐藏光


我们先在 Index.vue 中准备一个卡片页面,光的CSS效果可以使用filter: blur() 来实现



可以看到现在的效果是这样



实现光源跟随鼠标


在实现之前我们需要注意几点:



  • 1、鼠标移入时需要设置卡片 overflow: hidden,否则光会溢出,而鼠标移出时记得还原

  • 2、获取鼠标坐标时需要用clientX/Y而不是pageX/Y,因为前者会把页面滚动距离也算进去,比较严谨


刚刚说到实现思路时我们说到了mouseenter、mousemove、mouseleave,其实mouseenter、mouseleave 这二者的逻辑比较简单,重点是 mouseover 这个监听函数


而在 mouseover 这个函数中,最重要的逻辑就是:光怎么跟随鼠标移动呢?


或者也可以这么说:怎么计算光相对于卡片盒子的 left 和 top


对此我专门画了一张图,相信大家一看就懂怎么算了




  • left = clientX - x - width/2

  • height = clientY - y - height/2


知道了怎么计算,那么逻辑的实现也很明了了~封装一个use-light-card.ts



接着在页面中去使用



这样就能实现基本的效果啦~



卡片视差效果


卡片的视差效果需要用到样式中 transform 样式,主要是配置四个东西:



  • perspective:定义元素在 3D 变换时的透视效果

  • rotateX:X 轴旋转角度

  • rotateY:Y 轴旋转角度

  • scale3d:X/Y/Z 轴上的缩放比例



现在就有了卡片视差的效果啦~



给所有卡片添加光源


上面只是给一个卡片增加光源,接下来可以给每一个卡片都增加光源啦!!!




让光源变成可配置


上面的代码,总感觉这个 hooks 耦合度太高不太通用,所以我们可以让光源变成可配置化,这样每个卡片就可以展示不同大小、颜色的光源了~像下面一样



既然是配置化,那我们希望是这么去使用 hooks 的,我们并不需要自己在页面中去写光源的dom节点,也不需要自己去写光源的样式,而是通过配置传入 hooks 中



所以 hooks 内部要自己通过操作 DOM 的方式,去添加、删除光源,可以使用createElement、appendChild、removeChild 去做这些事~



完整源码


<!-- Index.vue -->

<template>
<div class="container">
<!-- 方块盒子 -->
<div class="item" ref="cardRef1"></div>
<!-- 方块盒子 -->
<div class="item" ref="cardRef2"></div>
<!-- 方块盒子 -->
<div class="item" ref="cardRef3"></div>
</div>
</template>

<script setup lang="ts">
import { useLightCard } from './use-light-card';

const { cardRef: cardRef1 } = useLightCard();
const { cardRef: cardRef2 } = useLightCard({
light: {
color: '#ffffff',
width: 100,
},
});
const { cardRef: cardRef3 } = useLightCard({
light: {
color: 'yellow',
},
});
</script>

<style scoped lang="less">
.container {
background: black;
width: 100%;
height: 100%;
padding: 200px;
display: flex;
justify-content: space-between;

.item {
position: relative;
width: 125px;
height: 125px;
background: #1c1c1f;
border: 1px solid rgba(255, 255, 255, 0.1);
}
}
</style>


// use-light-card.ts

import { onMounted, onUnmounted, ref } from 'vue';

interface IOptions {
light?: {
width?: number; // 宽
height?: number; // 高
color?: string; // 颜色
blur?: number; // filter: blur()
};
}

export const useLightCard = (option: IOptions = {}) => {
// 获取卡片的dom节点
const cardRef = ref<HTMLDivElement | null>(null);
let cardOverflow = '';
// 光的dom节点
const lightRef = ref<HTMLDivElement>(document.createElement('div'));
// 设置光源的样式

const setLightStyle = () => {
const { width = 60, height = 60, color = '#ff4132', blur = 40 } = option.light ?? {};
const lightDom = lightRef.value;
lightDom.style.position = 'absolute';
lightDom.style.width = `${width}px`;
lightDom.style.height = `${height}px`;
lightDom.style.background = color;
lightDom.style.filter = `blur(${blur}px)`;
};

// 设置卡片的 overflow 为 hidden
const setCardOverflowHidden = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardOverflow = cardDom.style.overflow;
cardDom.style.overflow = 'hidden';
}
};
// 还原卡片的 overflow
const restoreCardOverflow = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.style.overflow = cardOverflow;
}
};

// 往卡片添加光源
const addLight = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.appendChild(lightRef.value);
}
};
// 删除光源
const removeLight = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.removeChild(lightRef.value);
}
};

// 监听卡片的鼠标移入
const onMouseEnter = () => {
// 添加光源
addLight();
setCardOverflowHidden();
};

// use-light-card.ts

// 监听卡片的鼠标移动
const onMouseMove = (e: MouseEvent) => {
// 获取鼠标的坐标
const { clientX, clientY } = e;
// 让光跟随鼠标
const cardDom = cardRef.value;
const lightDom = lightRef.value;
if (cardDom) {
// 获取卡片相对于窗口的x和y坐标
const { x, y } = cardDom.getBoundingClientRect();
// 获取光的宽高
const { width, height } = lightDom.getBoundingClientRect();
lightDom.style.left = `${clientX - x - width / 2}px`;
lightDom.style.top = `${clientY - y - height / 2}px`;

// 设置动画效果
const maxXRotation = 10; // X 轴旋转角度
const maxYRotation = 10; // Y 轴旋转角度

const rangeX = 200 / 2; // X 轴旋转的范围
const rangeY = 200 / 2; // Y 轴旋转的范围

const rotateX = ((clientX - x - rangeY) / rangeY) * maxXRotation; // 根据鼠标在 Y 轴上的位置计算绕 X 轴的旋转角度
const rotateY = -1 * ((clientY - y - rangeX) / rangeX) * maxYRotation; // 根据鼠标在 X 轴上的位置计算绕 Y 轴的旋转角度

cardDom.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; //设置 3D 透视
}
};
// 监听卡片鼠标移出
const onMouseLeave = () => {
// 鼠标离开移出光源
removeLight();
restoreCardOverflow();
};

onMounted(() => {
// 设置光源样式
setLightStyle();
// 绑定事件
cardRef.value?.addEventListener('mouseenter', onMouseEnter);
cardRef.value?.addEventListener('mousemove', onMouseMove);
cardRef.value?.addEventListener('mouseleave', onMouseLeave);
});

onUnmounted(() => {
// 解绑事件
cardRef.value?.removeEventListener('mouseenter', onMouseEnter);
cardRef.value?.removeEventListener('mousemove', onMouseMove);
cardRef.value?.removeEventListener('mouseleave', onMouseLeave);
});

return {
cardRef,
};
};


结语 & 加学习群 & 摸鱼群


我是林三心



  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;

  • 一个偏前端的全干工程师;

  • 一个不正经的掘金作者;

  • 一个逗比的B站up主;

  • 一个不帅的小红书博主;

  • 一个喜欢打铁的篮球菜鸟;

  • 一个喜欢历史的乏味少年;

  • 一个喜欢rap的五音不全弱鸡


如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点


作者:Sunshine_Lin
来源:juejin.cn/post/7373867360019742758
收起阅读 »

扫码出入库与web worker

web
我为什么会用到这个呢,那还得从最近项目的一个扫码出入库的需求说起,之前客户的扫码出入库都是c端的,在效率方面没有明显的问题,但是后面这个项目的升级,就把c端的扫码部分摞到了B端了 大体需求是这样,客户用无线扫码枪扫回运单上的条码,然后扫码枪使用HID键盘模式(...
继续阅读 »

我为什么会用到这个呢,那还得从最近项目的一个扫码出入库的需求说起,之前客户的扫码出入库都是c端的,在效率方面没有明显的问题,但是后面这个项目的升级,就把c端的扫码部分摞到了B端了


大体需求是这样,客户用无线扫码枪扫回运单上的条码,然后扫码枪使用HID键盘模式(扫码枪相当于一个键盘),在一个一直聚焦的输入框输入扫到的条码,然后我这边监听到条码调接口录入库,成功后再语音播报扫码结果,同时刷新结果,刷新统计信息。


听上去很简单是不是,想象是美好的,可现实就残酷了,在初始版的时候,功能是做出来了,本地出入库都没问题,但是发到生产就悲催了,乱七八糟的问题


比如



  • 1.扫码枪精度的问题,扫码识别率低下,扫10次才能正确识别1次

  • 2.扫出来的码,断码,原本以为扫码枪扫一次就等同于我复制一个条码进输入框,可结果是扫码枪一次扫入,输入框接受的条码就像一个字符串流一样,一个字符一个字符进入的,这就导致中间间隔稍微长一点,就被错误的识别为另外一个条码(扫码是多个码连续扫入的)

  • 3.语音播报延迟,经常会有语音播放不出或者播放一半,这个..


这个就很让人无语,明明本地啥问题也没有


第一个问题,扫码枪精度,确实是有,因为我做的时候拿的扫码枪是一个有线的扫码枪,那识别率才叫一个高,准确率差不多95%,几乎没遇到解码啥的问题,可换成无线的扫码枪就傻眼,第一个问题就很烦,想到几千个客户没办法统一更换扫码枪,于是就想想优化一下条码编码呢,
我这边条码是用的jsBarcode组件,默认的编码类型CODE128,嗯~~问题会不会出在条码规范上呢


我去查了一下,条码的编码规范大致有以下几种


条码类型类别描述常见应用编码长度
UPC-A1D通用产品代码,常见于零售业零售商品12位数字
UPC-E1DUPC-A的压缩版本小型零售商品6位数字
EAN-131D欧洲商品编号,国际通用图书、零售商品13位数字
EAN-81DEAN-13的压缩版本小型商品8位数字
Code 391D可变长度,包含字母、数字和特殊字符工业、政府可变长度
Code 1281D高密度条码,表示所有128个ASCII字符物流、运输可变长度
Interleaved 2 of 5 (ITF)1D数字条码,每两个数字组成一对交错编码分销、仓储偶数位数字
QR Code2D可存储大量数据,包括文字、数字、二进制数据和汉字支付、信息分享、广告可变长度
Data Matrix2D高密度编码,适用于小型物品标识电子元器件、医疗设备可变长度
PDF4172D可编码大量数据身-份-正件、运输标签可变长度
Aztec Code2D高容错性,适用于票务和登机牌票务、登机牌可变长度

我这里着重说说CODE39和CODE128;我发现CODE39生成的条码比CODE128生成的长很多,我这把无线扫码枪扫很久都扫不出来,识别超慢,这个很奇怪,之前客户C端系统找技术查了一下,编码规范是CODE39,我就懵逼了,都是CODE39,为啥我们生成的码就识别这么慢,捣鼓了很久也没个结果,如果有哪位知道的可以给我说一下,就索性放弃这种编码模式,改用CODE128吧,查了一下,这是一种效率更高的编码方式,CODE39条码较长的主要原因在于它的编码效率较低,每个字符占用的空间较大,而CODE128通过更加紧凑和高效的编码方式,能够在同样的内容下生成更短的条码,于是撺掇同事把所有的条码都用CODE128生成,至此,扫码枪识别效率低的问题算事过去了


然后就是第二点,扫出来的码,断码问题,这个也因为换了短码好那么一点,可扫出来也经常有解码内容变长,的问题,暂时还在想办法优化


最后就是语音播报延迟,卡壳,甚至没有语音的情况,这个问题比较恼火,我这边组件是使用的开源库howler.js,这个库的优点就是兼容性好,可以播放包括mp3, opus, ogg, wav, aac, m4a, m4b, mp4, webm, 等多种格式,而且还支持分轨sprite播放,这个是我的最初的代码


import config from "./config";
import "./lib/howler.min";
const ENV = import.meta.env;

class VoiceReport {
public list = [];
constructor() {
this.initVoice();
}
// 目录放在@/assets/voice/ 下面
public voiceList: any = import.meta.globEager("@/assets/voice/*.mp3");
public voiceNameList = Object.keys(this.voiceList);
// 初始化语音播报器列表
initVoice = () => {
config.forEach((v) => {
const item = {
name: `Ref${v.codeType}${v.codeKey}`,
code: v.codeKey,
codeName: v.codeName,
voice: "",
};
const voiceIndex = this.voiceNameList.findIndex((voice) =>
String(voice).includes(v.codeKey)
);
if (voiceIndex > -1) {
item.path = this.voiceNameList[voiceIndex];
item.voice = this.voiceList[this.voiceNameList[voiceIndex]];
}
this.list.push(item);
});
};
// 播放
play = (code: string) => {
const Stream = this.list.find((v) => v.code == code);
let StreamVoide = null;
if (ENV?.DEV) {
StreamVoide = Stream?.path;
} else {
StreamVoide = Stream?.voice?.default;
}
// 提供的条码不在列表中
if (!StreamVoide) return;
try {
const sound = new Howl({
src: [StreamVoide],
volume: 1.0,
html5: true,
onplayerror: (e) => {
console.log("error", e);
},
});
sound.play();
} catch (e) {
console.log(e);
}
};
}

export default VoiceReport;


这个倒是能放,可能不能优化呢


我首先想到的是就从播放器本身优化呢,我想着会不会是加载的延迟或者加载文件过多,想着将所有的文件进行合并,再生成sprite信息,弄是弄了,可是不论如何就是load报错,我再把这个多个mp3合并成一个文件@/assets/voice/fullStack.mp3,进行生成sprite,来加载,加载是加载上来了,可同样遇到播放错误,播放的track根本不是我期望的那个


这个是错误代码:



import config from "./config";
import "./lib/howler.min";
import fullVoice from "@/assets/voice/fullStack.mp3";
const ENV = import.meta.env;

class player {
public list: any = [];
public player: any = {};
constructor() {
this.initVoice();
}
// 目录放在@/assets/voice/ 下面
public voiceList: any = import.meta.globEager("@/assets/voice/*.mp3");
public fullVoice: any = fullVoice;
public voiceNameList = Object.keys(this.voiceList);
public sprite: any = {};
public streamVoide: any = [];
// 时间戳转换为秒
timeStringToSeconds = (timeStr: string) => {
const parts = timeStr.split(":");
const hours = parseInt(parts[0]);
const minutes = parseInt(parts[1]);
const seconds = parseInt(parts[2]);

return hours * 3600 + minutes * 60 + seconds;
}
// 初始化语音播报器列表
initVoice = () => {
config.forEach((v, index) => {
const item = {
name: `Ref${v.codeType}${v.codeKey}`,
code: v.codeKey,
codeName: v.codeName,
voice: {},
path: "",
duration: this.timeStringToSeconds(v.duration ?? 0) * 1000,
durationStart: 0,
durationEnd: 0,
};
item.durationStart = !index ? 0 : this.list[index - 1].durationEnd;
item.durationEnd = item.durationStart + item.duration;
this.sprite[v.codeKey] = [item.durationStart, item.durationEnd];
const voiceIndex = this.voiceNameList.findIndex((voice) =>
String(voice).includes(v.codeKey)
);
if (voiceIndex > -1) {
item.path = this.voiceNameList[voiceIndex];
item.voice = this.voiceList[this.voiceNameList[voiceIndex]];
}
this.list.push(item);
/* eslint-disable */
// @ts-ignore
this.streamVoide.push(ENV?.DEV ? item.path : item.voice?.default);
});
/* eslint-disable */
// @ts-ignore
this.player = new Howl({
src: this.streamVoide,
volume: 1.0,
html5: true,
sprite: this.sprite,
onplayerror: (e: any) => {
console.log("play error", e);
},
onload: (e: any) => {
console.log("error", e);
}
});
window.player = this.player;
console.log(this.sprite, this.player, fullVoice)
};
// 播放
play = (code: string) => {
try {
this.player.play(code);
} catch (e) {
console.log(e);
}
};
}

export default player;

到现在还在持续找解决方案中,
最后,不得不把希望寄托在异步任务请求导致阻塞主线程这个猜想上,因为每完成一次扫码,会发起三个请求



  • 入库请求

  • 刷新结果列表请求

  • 刷新统计请求


这么多请求一起,接口稍微一慢就有可能导致播放卡顿的问题
这个在我经过一段时间的搜索之后发现,发现webworker可以处理这个问题


web worker

根据MDN的说法

Web Workers 是 Web 内容在后台线程中运行脚本的一种简单方法。工作线程可以在不干扰用户界面的情况下执行任务。此外,他们还可以使用 fetch() 或 XMLHttpRequest API 发出网络请求。创建后,工作人员可以通过将消息发布到该代码指定的事件处理程序来向创建它的 JavaScript 代码发送消息(反之亦然)。


既然是独立于主线程之外的一个,那就不可避免的会遇到身份验证和通信的问题,对于发起的请求没有携带身份信息,这个好办,就自己在封装一个axios方法fetch,将身份信息传过去ok,这里主要贴一下worker的内容,也很简单


import type { WorkerMessageDataType } from "../types/types";
import fetch from "@/utils/fetch";
import { throttle } from "lodash";
let Ajax: any = null;

// 从主线程接受数据
self.onmessage = function (e: WorkerMessageDataType) {
console.log("Worker: 收到请求", e);
const type = e.data?.type || "";
const data = e.data?.data || {};
// 一定要初始化
if (type == "init") {
const headers: any = e.data?.headers;
Ajax = fetch(headers);
}
// 请求刷新统计数据
if (type == "refreshScanCountData") refreshScanCountData();
// 请求刷新列表扫码结果
if (type == "refreshDataList") refreshDataList();
// 请求入库
if (type == "checkAddIntoStock") checkAddIntoStock(data);
};

// 向主线程发送数据
const sedData = (type: string, data: object) => {
const param = {
type,
data: data || {},
};
self.postMessage(param);
};

// 刷新统计数据,查询统计信息api
const refreshScanCountData = throttle(() => {
Ajax({
method: "post",
url: `/api/CountStatistics`,
data: {},
}).then((res: any) => {
sedData("refreshScanCountData", res);
});
}, 500);

// 刷新扫码结果数据
const refreshDataList = throttle(() => {
Ajax({
method: "post",
url: `/api/scanToStorage/page`,
data: {},
}).then((res: any) => {
sedData("refreshDataList", res);
});
}, 500);

// 请求入库
const checkAddIntoStock = (data: { barcode: string; [x: string]: any }) => {
Ajax({
method: "post",
url: `/api/scanToStorage`,
data,
})
.then((res: any) => {
// 刷新统计数据
refreshScanCountData();
// 刷新列表
refreshDataList();
sedData("checkAddIntoStock", {
barcode: data.barcode,
...res,
status: true,
});
})
.catch(() => {
sedData("checkAddIntoStock", {
barcode: data.barcode,
status: false,
});
});
};

在主线程页面写一个方法,初始化一下这个worker


// 加载worker
const initWorker = () => {
const headers = {
Authorization: "bearer " + sessionStorage.getItem("token"),
token: sessionStorage.getItem("token"),
currRoleId: sessionStorage.getItem("roleId"),
};
// 初始化,加入身份信息
WebWorker.postMessage({ type: "init", headers });
// 从worker接受消息
WebWorker.onmessage = (e) => {
console.log("Main script: Received result", e.data);
const type = e.data?.type || "";
const data = e.data?.data || {};

// 异步更新统计信息
if (type == "refreshScanCountData") {
ScanCountData.value = data;
}
// 刷新表格数据
if (type == "refreshDataList") {
dataTable.value.updateData(data);
}
};
};


这样就可以了,即便是这样,依然还有好多问题没解决,这个是我的第一篇文章,难免有错误疏漏,这个需求并没结束,我还会持续跟进更新的


作者:kiohang
来源:juejin.cn/post/7380342160581492747
收起阅读 »

用空闲时间做了一个小程序-二维码生成器

web
一直在摸鱼中赚钱的大家好呀~ 先向各位鱼友们汇报一下情况,目前小程序已经有900+的鱼友注册使用过。虽然每天都有新的鱼友注册,但是鱼友增长的还很缓慢。自从国庆前的文字转语音的工具上线到现在已经将近有1个月没有更新小程序了。但是今天终终终终终于又有个小工具上线了...
继续阅读 »

一直在摸鱼中赚钱的大家好呀~


先向各位鱼友们汇报一下情况,目前小程序已经有900+的鱼友注册使用过。虽然每天都有新的鱼友注册,但是鱼友增长的还很缓慢。自从国庆前的文字转语音的工具上线到现在已经将近有1个月没有更新小程序了。但是今天终终终终终于又有个小工具上线了,希望这个小工具可以帮助到更多的鱼友们(没错就是你们)


这次更新的工具是一个二维码生成器,虽然很多小程序存在这个工具,但是本人也是想尝试一下实现这个工具。老规矩,先来看下知名UI设计师设计的页面。







同样在工具tab页中增加了二维码生成器模块。从UI图中可以看出第一个表单页面不是很难,就是一个文本框、两个颜色选择、一个图片上传。这个页面我在开发中也是很快就完成了,没有什么技术含量。


当我做到颜色选择弹窗的时候是想从网上找一个现成的插件。但是找了半天没有找到合适的,只能自己手动开发一个。既然要做颜色选择器的功能就要先了解一下颜色的两种格式 (我这边的实现就这两种格式)


颜色的HEX格式

颜色的HEX格式是#+六位数字/字母,其中六位数字/字母是一种十六进制的表达方式。这六位分别两个一组,从左到右分别表示绿00表示最小,十进制是0FF表示最大,十进制是255。通俗点讲,某个颜色的数值越大,包含这个颜色就越多。如:#000000-黑色、#FFFFFF-白色、#FF0000-红色、#00FF00-绿色、#0000FF-蓝色。


颜色的RGB格式

颜色的RGB格式是rgb(0-255,0-255,0-255), 其中0-255就是HEX格式的十进制表达方式。这三个数值从左到右分别表示绿0表示最小;255表示最大。通俗点讲,某个颜色的数值越大,包含这个颜色就越多。如:rgb(0,0,0)-黑色、rgb(255,255,255)-白色、rgb(255,0,0)-红色、rgb(0,255,0)-绿色、rgb(0,0,255)-蓝色。


有了上面的概念,我的思路也就出来了。让用户分别选择这三种颜色的数值,然后通过用户选择的三种颜色的数值转成目标颜色,就可以完成颜色选择的功能。思路出来了之后就告知了UI,然后按照我的思路将效果图出了出来 (没错,就是先实现后出图)。实现中主要使用了vant-ui组件库的popupslider两个组件 (聪明人都喜欢用现成的)。贴一下部分实现代码:


show="{{ show }}" 
title="展示弹出层"
position="bottom"
bind:close="cancelHandle"
custom-style="background-color: #F3F3F9;border-radius: 40rpx 40rpx 0rpx 0rpx;"
root-portal>
class="color-popup">
class="popup-header flex flex_j_c--space-between flex_a_i--center">
class="flex-item_f-1">
class="title flex-item_f-1">{{ title }}
class="flex-item_f-1 flex flex_j_c--flex-end">
name="cross" size="32rpx" bind:tap="cancelHandle" />


class="color-picker" wx:for="{{ pickers }}" wx:key="index" wx:if="{{ index !== 3 }}">
class="color-picker-label">{{ item.label }}
class="flex flex_a_i--center">
class="slider-wrap flex-item_f-1 {{ item.field }}">
value="{{ item.value }}" min="{{ 0 }}" max="{{ 255 }}" data-index="{{ index }}" bind:change="changeHandle" bind:drag="changeHandle" custom-class="slider" bar-height="60rpx" active-color="transparent" use-button-slot>
class="slider-button" slot="button">


class="slider-value">{{ item.value }}


class="color-preview-box flex flex_a_i--center">
class="preview-box-wrap">
class="preview-box" style="background-color: {{ rgbaStyle }};">
class="preview-label">颜色预览

class="presets-box-wrap flex-item_f-1 flex flex_j_c--space-between">
class="presets-box flex flex_j_c--center flex_a_i--center {{ rgbaStyle === item.rgbaStyle ? 'active' : '' }}" wx:for="{{ presets }}" wx:key="index" style="background-color: {{ item.rgbaStyle }};" data-row="{{ item }}" bind:tap="chooseHandle">
class="active-box">



class="confirm-wrap flex">
class="hex-box flex flex_a_i--center flex_j_c--space-between">
#
{{ hex }}

class="confirm-button-box flex-item_f-1">
type="primary" custom-class="confirm-button" bind:click="confirmHandle" round>确定





import { rgb2Hex } from '../../utils/util'

const presets = [
[0, 0, 0, 255], [102, 102, 102, 255],
[0, 95, 244, 255], [100, 196, 102, 255],
[247, 206, 70, 255], [235, 77, 61, 255],
]

Component({
options: {
addGlobalClass: true
},
properties: {
show: {
type: Boolean,
value: false
},
title: {
type: String,
value: ''
},
value: {
type: Array,
value: [0, 0, 0, 255],
observer: function(val) {
const { pickers } = this.data
if(val.length) {
this.setData({
pickers: pickers.map((item, index) => {
return {...item, value: val[index]}
}),
})
this.setColor(val)
} else {
this.setData({
pickers: pickers.map((item, index) => {
return {...item, value: index === 3 ? 255 : 0}
}),
})
const rgba = [0, 0, 0, 255]
this.setColor(rgba)
}
}
}
},
data: {
pickers: [
{ field: 'r', label: '红色', value: 0 },
{ field: 'g', label: '绿色', value: 0 },
{ field: 'b', label: '蓝色', value: 0 },
{ field: 'a', label: '透明度', value: 255 },
],
rgba: [],
hex: '',
rgbaStyle: '',
presets: [
...presets.map(rgba => {
return {
rgba,
rgbaStyle: `rgba(${ rgba.join(',') })`
}
})
]
},
methods: {
changeHandle(e) {
const { detail, currentTarget: { dataset: { index } } } = e
const key = `pickers[${ index }].value`
this.setData({
[key]: typeof detail === 'object' ? detail.value : detail
})
const rgba = this.data.pickers.map(item => item.value)
this.setColor(rgba)
},
chooseHandle(e) {
const { rgba } = e.currentTarget.dataset.row
this.setData({
pickers: this.data.pickers.map((item, index) => {
return {...item, value: rgba[index]}
}),
})
this.setColor(rgba)
},
// 设置颜色
setColor(rgba) {
const hex = rgb2Hex(...rgba)
const rgbaStyle = `rgba(${ rgba.join(',') })`
this.setData({ rgba, hex: hex.replace('#', ''), rgbaStyle })
},
confirmHandle(e) {
this.triggerEvent('confirm', { rgba: this.data.rgba, rgbaStyle: this.data.rgbaStyle })
},
cancelHandle() {
this.triggerEvent('cancel')
},
}
})

到此颜色选择器的组件已经实现了,还剩下一个预览下载的页面。我这边的实现并不是直接页面跳转,因为这边预览之后返回是希望还保留预览之前的数据的。如果直接离开当前页面并清除了数据,不符合用户预期的。所以使用了一个假页。微信小程序提供了一个 page-container 的页面容器,效果类似于 popup 弹出层,页面内存在该容器时,当用户进行返回操作,关闭该容器不关闭页面。


如果二维码中含中文的静态码使用微信扫描后是无法正常展示内容的(后期安排上二维码解析的功能)


感谢大家观看我今日的水文,文笔实在是不行,欢迎鱼友们给小程序提提意见,或者有什么有趣的想法也可以与楼主提一提。最后希望大家到我的小程序来多坐坐。





作者:拖孩
来源:juejin.cn/post/7384350475736989731
收起阅读 »

dockerhub国内镜像站集体下线?别慌,教你丝滑拉取镜像~

web
前言想必大家都听说了,国内镜像站几乎都用不了,对于开发者来说,无疑是个不好的消息。在docker pull时直接超时失败,拉取不下来镜像。那么有没有什么办法解决呢?有!还不止一种。通过docker配置文件配置可用的国内镜像源设置代理自建镜像仓库方法1已经不太好...
继续阅读 »

前言

想必大家都听说了,国内镜像站几乎都用不了,对于开发者来说,无疑是个不好的消息。在docker pull时直接超时失败,拉取不下来镜像。那么有没有什么办法解决呢?有!还不止一种。

  1. 通过docker配置文件配置可用的国内镜像源
  2. 设置代理
  3. 自建镜像仓库

方法1已经不太好使了,能找到可用的不多,有的还存在没有最新的镜像问题。

方法2可行,不过得要有科学上网的工具,再会一点配置代理的知识,操作起来稍稍复杂。

本文主要介绍第三种方法,上手快,简单,关键还0成本!

准备工作

  1. 登录阿里云,找到容器镜像服务,创建一个个人版实例。(第一次使用的话,会让设置访问密码。记住,后面会用)
  2. 找到仓库管理-命名空间,新建一个命名空间且设置为公开

微信截图_20240626174632.png 3.不要创建镜像仓库,回到访问凭证

可以看到,如下2个信息,一个是你的阿里云用户名,一个是你的仓库地址(后面有用)

sudo docker login --username=阿里云用户名 registry.cn-beijing.aliyuncs.com

github配置

  1. fork项目,地址: docker_image_pusher

(感谢tech-shrimp提供的工具)

  1. 在fork后的项目中通过Settings-Secret and variables-Actions-New Repository secret路径,配置4个环境变量
  • ALIYUN_NAME_SPACE-命名空间
  • ALIYUN_REGISTRY_USER-阿里云用户名
  • ALIYUN_REGISTRY_PASSWORD-访问密码
  • ALIYUN_REGISTRY-仓库地址

企业微信截图_20240626203514.png

3.配置要拉取的镜像 打开项目images.txt,每一行配置一个镜像,格式:name:tag 比如

企业微信截图_20240626213138.png

提交修改的文件,则会自动在Actions中创建一个workflow。等待片刻即可(1分钟左右)

企业微信截图_20240626212730.png

5.回到阿里云容器镜像服务控制台-镜像仓库

企业微信截图_20240626213555.png

可以看到镜像已成功拉取并同步到你自己的仓库中。

测试效果

我自己操作了下把nginx的镜像给拉了过来,找台服务器测试一下速度

演示.gif 哈哈!这速度杠杠的吧! 用这个方式的好处是,借助github的action机制,直接从dockerhub上拉取任何你想要的镜像,也不用担心国内镜像站版本更新不及时的问题。再从自建的仓库中pull下来就可以啦! 如果有小伙伴没捣鼓成功的,可以留言给我。


作者:临时工
来源:juejin.cn/post/7384623060199473171
收起阅读 »

微信小程序全新渲染引擎Skyline(入门篇)

web
前言 最近看小程序文档的时候发现了 swiper 组件新增了 Skyline 特有的属性,直接使用竟然没有效果。 不信邪的我打算来研究研究究竟什么是 Skyline!经过一系列文档阅读与实践,长时间闭门造车的我打开了新世界的大门,我惊讶的发现 Skyline...
继续阅读 »

前言


最近看小程序文档的时候发现了 swiper 组件新增了 Skyline 特有的属性,直接使用竟然没有效果。



不信邪的我打算来研究研究究竟什么是 Skyline!经过一系列文档阅读与实践,长时间闭门造车的我打开了新世界的大门,我惊讶的发现 Skyline 引擎很可能是微信小程序未来发展的重点方向,有着更类似原生的交互体验,新增的特性让人连连称叹,特以此文来总结性地介绍一下 Skyline。


双线程模型


了解 Skyline 之前,我们有必要重新复习一下什么是小程序的双线程模型。


如官方文档所言,小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。小程序的渲染层和逻辑层分别由2个线程管理:



  • 渲染层的界面使用了WebView 进行渲染,一个小程序存在多个界面,所以渲染层存在多个WebView线程;

  • 逻辑层采用JsCore线程运行JS脚本。


这两个线程的通信会经由微信客户端(原生) 做中转,逻辑层发送网络请求也经由微信客户端 (原生) 转发,有了微信小程序客户端 (原生) 作为媒介系统,使得我们开发者能够专注于数据与逻辑。


如上所述,小程序的通信模型如下图所示。



什么是 Skyline 引擎


前文提到,基于 WebView 和原生控件混合渲染的方式,小程序优化扩展了 Web 的基础能力,所以小程序相对于普通的Web页面有着更为良好的性能与体验。


由于 Web 在移动端的表现与原生应用仍有一定差距,亦或许是 Web 的优化遇到了瓶颈,为了进一步优化小程序性能,微信在 WebView 渲染之外新增了一个渲染引擎,也就是我们本文的重磅主角: Skyline,它使用更精简高效的渲染管线,并带来诸多增强特性,让 Skyline 拥有更接近原生渲染的性能体验。


Skyline 引擎 vs Webview 引擎


我们知道:WebView 的 JS 逻辑、DOM 树创建、CSS 解析、样式计算、Layout、Paint (Composite) 都发生在同一线程,在 WebView 上执行过多的 JS 逻辑可能阻塞渲染,导致界面卡顿,大致流程如下图所示。



但是,在 Skyline 环境下改变了这个情况,它创建了一条渲染线程来负责计算图层布局,图层的绘制以及整合图层页面等渲染任务,并在 AppService 中划出一个独立的上下文,来运行之前 WebView 承担的 JS 逻辑、DOM 树创建等逻辑。



据官方统计数据表明,Skyline 与 WebView 性能相比,具有如下优势:


Skyline 的首屏时间比 WebView 快约 66%



单个页面 Skyline 的占用比 WebView 减少约 35%


单个页面 Skyline 的占用比 WebView 减少 35%,两个页面 Skyline 的内存占用比 WebView 减少 50%,随着打开的页面变多,内存差距越明显。



Skyline 引擎的优点



  • 界面更不容易被逻辑阻塞,进一步减少卡顿

  • 无需为每个页面新建一个 JS 引擎实例(WebView),减少了内存、时间开销

  • 框架可以在页面之间共享更多的资源,进一步减少运行时内存、时间开销

  • 框架的代码之间无需再通过 JSBridge 进行数据交换,减少了大量通信时间开销

  • 保持和原有架构的兼容性,基于 WebView 环境的小程序代码基本上无需任何改动即可直接在新的架构下运行


更多Skyline的特性更新请详见Skyline 渲染引擎 / 概览 / 特性 | 微信开放文档


Skyline 引擎的缺点



  • WXS效率可能有所下降 (WXS 由于被移到 AppService 中,虽然逻辑本身无需改动,但询问页面信息等接口会变为异步,效率也可能有所下降)


但是,也不必过多的担心,微信推出了新的 Worklet 机制,它比原有的 WXS 更靠近渲染流程,用以高性能地构建各种复杂的动画效果。


Skyline 引擎的使用


前文提到,我想使用 swiper 组件新增的 Skyline 特有属性无果,是因为我没有完成 Skyline的配置。如果想要使用 Skyline引擎,我们可以按页面级别来选择性的配置是走 Skyline 引擎或是 Webview 引擎来渲染。


// page.json
{
"renderer": "skyline"
}

// page.json
{
"renderer": "webview"
}

配置完成之后,我们就可以愉快的使用 Skyline 专有的新特性了。


Skyline 引擎的兼容性


我们可能会担心开启了 Skyline 的渲染模式会不会带来兼容性问题。官方表示:



所以我们完全可以放下对兼容性的顾虑,拥抱新的 Skyline 引擎,让大部分的用户优先体验到新一代微信小程序的渲染技术,做第一批吃螃蟹的人!对于我们开发者而言,有必要深入了解一下Skyline引擎的更新带来了哪些开发层面的变化与创新,毕竟,吃螃蟹的人会越来越多嘛。


后记


感谢您的阅读,本文仅为微信小程序 Skyline 引擎的入门介绍篇,后续会持续更新有关 Skyline 引擎相关实际操作及使用的文章,如有兴趣,欢迎持续关注。


作者:阿李贝斯
来源:juejin.cn/post/7298927261210361882
收起阅读 »

2024年令人眼前一亮的Web框架

web
本文翻译自 dev.to/wasp/web-fr… 感谢您的阅读! 介绍 2024年正向我们走来,我们怀着满腔热情为新的一年制定计划,探索未来一年可以学习或实现的目标。此时此刻,正是探寻来年值得学习的框架、理解其功能和特色的最佳时刻。我们以2023年JS 新...
继续阅读 »

本文翻译自 dev.to/wasp/web-fr…

感谢您的阅读!



介绍


2024年正向我们走来,我们怀着满腔热情为新的一年制定计划,探索未来一年可以学习或实现的目标。此时此刻,正是探寻来年值得学习的框架、理解其功能和特色的最佳时刻。我们以2023年JS 新星名单为指引,力求保持客观公正的态度。对于每一个特色框架,我们都将突出其最大的优势,使您能够全面理解它们的优点,从而选择适合自己的框架进行尝试!


HTMX - 返璞归真🚲


htmx-演示


为谁而设:



  • 你希望减少JavaScript的编写量

  • 你希望代码更简单,以超媒体为中心


HTMX在2023年迅速走红,过去一年间在GitHub上赢得了大量星标。HTMX并非普通的JS框架。如果你使用HTMX,你将大部分时间都花在超媒体的世界中,以与我们通常对现代Web开发的JS密集型视角完全不同的视角看待Web开发。HTMX利用HATEOAS(Hypermedia作为应用程序状态的引擎)的概念,使开发人员能够直接从HTML访问浏览器功能,而不是使用Javascript。


此外,它还证明了通过发布令人惊叹的表情符号并以口碑作为主要营销手段,你可以获得人气和认可。不仅如此,你还可能成为HTMX的CEO!它吸引了许多开发人员尝试这种构建网站的方法,并重新思考他们当前的实践。所有这些都使2024年对于这个库的未来发展充满了激动人心的可能性。


Wasp - 全栈,开箱即用🚀


开放SaaS


为谁而设:



  • 你希望快速构建全栈应用

  • 你希望在一个出色的一体化解决方案中继续使用React和Node.js,而无需手动挑选堆栈的每一部分

  • 你希望获得一个为React和Node.js预配置的免费SaaS模板—— Open SaaS


对于希望简单轻松地全面控制其堆栈的工具的用户,无需再寻找!Wasp是一个有主见的全栈框架,利用其编译器以快速简便的方式为你的应用创建数据库、后端和前端。它使用React、Node.js和Prisma,这些都是全栈Web开发人员正在使用的一些最著名的工具。


Wasp的核心是main.wasp文件,它作为你大部分需求的一站式服务。在其中,你可以定义:



  • 全栈身份验证

  • 数据库架构

  • 异步作业,无需额外的基础设施

  • 简单且灵活的部署

  • 全栈类型安全

  • 发送电子邮件(Sendgrid、MailGun、SMTP服务器等)

  • 等等……


最酷的事情是?经过编译器步骤后,你的Wasp应用程序的输出是一个标准的React + Vite前端、Node.js后端和PostgreSQL数据库。从那里,你可以使用单个命令轻松将一切部署到Fly.io等平台。


尽管有些人可能会认为Wasp的有主见立场是负面的,但它却是Wasp众多全栈功能的驱动力。使用Wasp,单个开发人员或小型团队启动全栈项目变得更加容易,尤其是如果你使用预制的模板或OpenSaaS作为你的SaaS起点。由于项目的核心是定义明确的,因此开始一个项目并可能在几天内创建自己的全栈SaaS变得非常容易!


此外,还有一点很酷的是,大多数Web开发人员对大多数现有技术的预先存在的知识仍然在这里适用,因为Wasp使用的技术已经成熟。


Solid.js - 一流的reactivity库 ↔️


扎实的例子


适合人群:



  • 如果你希望代码具有高响应性

  • 现有的React开发人员,希望尝试一种对他们来说学习曲线较低的高性能工具


Solid.js是一个性能很高的Web框架,与React有一些相似之处。例如,两者都使用JSX,采用基于函数的组件方法,但Solid.js不使用虚拟DOM,而是将你的代码转换为纯JavaScript。然而,Solid.js因其利用信号、备忘录和效果实现细粒度响应性的方法而更加出名。信号是Solid.js中最简单、最知名的基本元素。它们包含值及其获取和设置函数,使框架能够观察并在DOM中的确切位置按需更新更改,这与React重新渲染整个组件的方式不同。


Solid.js不仅使用JSX,还对其进行了增强。它提供了一些很酷的新功能,例如Show组件,它可以启用JSX元素的条件渲染,以及For组件,它使在JSX中更轻松地遍历集合变得更容易。另一个重要的是,它还有一个名为Solid Start的元框架(目前处于测试版),它使用户能够根据自己的喜好,使用基于文件的路由、操作、API路由和中间件等功能,以不同的方式渲染应用程序。


Astro - 静态网站之王👑


天文示例


适合人群:



  • 如果您需要一款优秀的博客、CMS重型网站工具

  • 需要一个能够集成其他库和框架的框架


如果您在2023年构建了一个内容驱动的网站,那么很有可能您选择了Astro作为首选框架来实现这一目标!Astro是另一个使用不同架构概念来脱颖而出的框架。对于Astro来说,这是岛屿架构。在Astro的上下文中,岛屿是页面上的任何交互式UI组件,与静态内容的大海形成鲜明对比。由于这些岛屿彼此独立运行,因此页面可以有任意数量的岛屿,但它们也可以共享状态并相互通信,这非常有用。


关于Astro的另一个有趣的事情是,他们的方法使用户能够使用不同的前端框架,如React、Vue、Solid来构建他们的网站。因此,开发人员可以轻松地在其当前知识的基础上构建网站,并利用可以集成到Astro网站中的现有组件。


Svelte - 简单而有效🎯


精简演示


适合人群:



  • 您希望学习一个简单易上手的框架

  • 追求简洁且代码执行速度快的开发体验


Svelte是另一个尝试通过尽可能直接和初学者友好的方式来简化和加速Web开发的框架。它是一个很容易学习的框架,因为要使一个属性具有响应性,您只需声明它并在HTML模板中使用它。 每当在JavaScript中程序化地更新值时(例如,通过触发onClick事件按钮),它将在UI上反映出来,反之亦然。


Svelte的下一步将是引入runes。runes将是Svelte处理响应性的方式,使处理大型应用程序变得更加容易。类似于Solid.js的信号,符文通过使用类似函数的语句提供了一种直接访问应用程序响应性状态的方式。与Svelte当前的工作方式相比,它们将允许用户精确定义整个脚本中哪些部分是响应性的,从而使组件更加高效。类似于Solid和Solid Start,Svelte也有其自己的框架,称为SvelteKit。SvelteKit为用户提供了一种快速启动其由Vite驱动的Svelte应用程序的方式。它提供了路由器、构建优化、不同的渲染和预渲染方式、图像优化等功能。


Qwik - 非常快🚤


qwik演示


适合人群:



  • 如果您想要一个高性能的Web应用

  • 现有的React开发人员,希望尝试一种高性能且学习曲线平缓的框架


最后一个但同样重要的框架是Qwik。Qwik是另一个利用JSX和函数组件的框架,类似于Solid.js,为基于React的开发人员提供了一个熟悉的环境,以便尽快上手。正如其名字所表达的,Qwik的主要目标是实现您应用程序的最高性能和最快执行速度。


Qwik通过利用可恢复性(resumability)的概念来实现其速度。简而言之,可恢复性基于在服务器上暂停执行并在客户端上恢复执行而无需重新播放和下载全部应用程序逻辑的想法。这是通过延迟JavaScript代码的执行和下载来实现的,除非有必要处理用户交互,这是一件非常棒的事情。它使整体速度提高,并将带宽降低到绝对最小值,从而实现近乎瞬间的加载。


结论


在我们所提及的所有框架和库中,最大的共同点是它们的熟悉度。每个框架和库都试图以构建在当前知识基础上的方式吸引潜在的新开发者,而不是做一些全新的事情,这是一个非常棒的理念。


当然,还有许多我们未在整篇文章中提及但值得一提的库和框架。例如,Angular 除了新的标志和文档外,还包括信号和新的控制流。还有 Remix,它增加了对 Vite、React Server Components 和新的 Remix SPA 模式的支持。最后,我们不能忘记 Next.js,它在过去几年中已成为 React 开发者的默认选择,为新的 React 功能铺平了道路。


作者:腾讯TNTWeb前端团队
来源:juejin.cn/post/7339830464000213027
收起阅读 »

Node.js 正在衰退吗?通过一些关键指标告诉你事实如何!

web
关于 “Node.js 凉了吗?” 类似话题大家平常在某乎上也有看到过。 近日 Node.js 官方 Twitter 上转载了一则帖子,看来国外也有此讨论。Node.js TSC 成员 & fastifyjs 首席维护者 @Matteo Collin...
继续阅读 »

关于 “Node.js 凉了吗?” 类似话题大家平常在某乎上也有看到过。



近日 Node.js 官方 Twitter 上转载了一则帖子,看来国外也有此讨论。Node.js TSC 成员 & fastifyjs 首席维护者 @Matteo Collina 对此进行了回复,表示关于 Node.js 衰退的传言被大大夸大了。Node.js 不仅不会消失,而且正在积极进化以满足现代 Web 开发的需求



以下内容翻译自 @Matteo Collina 的博文


在过去的 15 年里,Node.js 一直是 Web 开发的基石。自 2009 年发布以来,它从一个简单的小众技术,发展到如今支持超过 630 万个网站、无数的 API,并被财富 500 强中的 98% 所使用。


作为一个强大的开源运行时环境,Node.js 非常适合数字化转型的挑战。基于熟悉的 JavaScript 基础,Node.js 拥有轻量且事件驱动的架构,这使其非常适合构建可扩展的实时应用程序,能够处理大量并发请求——这是当今 API 驱动世界的关键需求。


结合其活跃且不断增长的开源社区以及 OpenJS 基金会的强力支持,Node.js 已成为当代 Web 开发的支柱。


但最近,有关 Node.js 衰落的传言开始流传。这些说法有多少可信度呢?


在这篇博客中,我们将深入探讨一些关键指标,这些指标描绘了一个繁荣的 Node.js 生态系统,并展现了其光明的未来。我们还将看看已经发布并即将在 Node.js 上推出的主要功能。


技术是永无止境的循环


有些人可能认为新技术不可避免地会使旧技术过时。但事实上,进步往往是建立在现有基础之上的。以 COBOL 为例,这种编程语言创建于 1959 年,今天仍在积极使用。虽然它可能不是前沿 Web 开发的首选,但 COBOL 在银行、金融和政府机构的核心业务系统维护中仍然至关重要。根据最新的 Tiobe 指数,COBOL 正在上升,其受欢迎程度在 Ruby 和 Rust 之间。其持久的相关性突显了一个关键点:技术进步并不总是意味着抛弃过去。


COBOL 正在崛起(来源: tiobe.com/tiobe-index)


让我们考虑另一个 Web 开发领域的老将:jQuery。这款 JavaScript 库比 Node.js 早三年发布,拥有令人印象深刻的使用统计数据——超过 95% 的 JavaScript 网站和 77% 的所有网站都在使用它。jQuery 的持久受欢迎程度表明,技术的年龄并不一定决定其相关性。就像 jQuery 一样,Node.js 尽管更年轻,但也有潜力保持其作为 Web 开发人员宝贵工具的地位。


94.4% 支持 JS 的网站都使用了 jQuery -(来源: w3techs.com/technologies/overview/javascrip..)


Node.js 目前的势头


根据 StackOverflow 的调查,Node.js 是最受欢迎的技术。这种成功依赖于 Node.js 和 npm 注册表的强大组合。这个创新的二人组解决了大规模软件复用的挑战,这是以前无法实现的。


来源:StackOverflow


因此,预先编写的代码模块的使用激增,巩固了 Node.js 作为开发强国的地位。



Readable-stream 的下载量从 2022 年的略高于 30 亿增长到 2023 年的接近 70 亿,意味着使用量在三年内翻了一番。


Node.js 的总下载量:Node.js 每月有高达 1.3 亿的下载量。


然而,理解这一数字包含什么很重要。这些下载量中的很大一部分实际上是头文件。在 npm i 命令期间,这些头文件是临时下载的,用于编译二进制插件。编译完成后,插件会存储在系统上供以后使用。


来源:nodedownloads.nodeland.dev


按操作系统划分的下载量中,Linux 位居榜首。这是有道理的,因为 Linux 通常是持续集成(CI)的首选——软件在开发过程中经过的自动化测试过程。虽然 Linux 主导 CI,但开源项目(OSS)通常在 Windows 上进行额外测试以确保万无一失。


这种高下载量的趋势转化为实际使用。在 2021 年,Node.js 二进制文件的下载量为 3000 万到 2024 年这一数字跃升至 5000 万。在 2023 年,Docker Hub 上的 Node.js 镜像获得了超过 8 亿次下载,提供了 Node.js 在生产环境中使用情况的宝贵洞察。


保持应用程序安全:更新你的 Node.js 版本


许多开发人员和团队无意中让他们的应用程序面临风险,因为他们没有更新 Node.js。以下是保持最新版本的重要性。


Node.js 提供了长期支持(LTS)计划,以确保关键应用程序的稳定性和安全性。然而,版本最终会到达其生命周期的终点,这意味着它们不再接收安全补丁。使用这些过时版本构建的应用程序将面临攻击风险。


例如,Node.js 版本 14 和 16 现在已经被弃用。尽管如此,这些版本每月仍有数百万次下载 —— Node 16 在 2 月份被下载了 2500 万次,而 Node 14 则约为 1000 万次*。令人震惊的是,一些开发人员甚至在使用更旧的版本,如 Node 10 和 12。


LTS 计划


好消息是:更新 Node.js 很容易。推荐的方法是每隔两个 LTS 版本进行升级。例如,如果你当前使用的是 Node.js 16(已不再支持),你应该迁移到最新的 LTS 版本,即目前的 Node.js 20。不要让过时的软件使你的应用程序暴露于安全威胁中。


Node.js 努力确保你的安全


Node.js 非常重视安全性。安全提交会由 Node 技术指导委员会(TSC)进行彻底评估,以确定其有效性。该团队努力确保快速响应时间,目标是在提交报告后 5 天内做出初步响应,通常在 24 小时内实现。


初次响应平均时间


安全修复每季度批量发布。去年,TSC 总共收到了 80 个提交。


Node.js 安全提交


没有 Open Source Security Foundation(OpenSSF)的支持,这种对安全性的承诺是不可能实现的。通过 OpenSSF 领导的 Alpha-Omega 项目,由微软、谷歌和亚马逊资助,Node.js 获得了专门用于提高其安全态势的拨款。该项目于 2022 年启动,旨在通过促进更快的漏洞识别和解决,使关键的开源项目更加安全。这一合作以及 Node.js 对安全工作的专门资金,展示了其保护用户安全的强烈承诺。


安全工作总资金


近年来发布的主要功能


让我们来看看过去几年引入的一些功能。


ESM


Node.js 已经采用了 ECMAScript 模块(ESM)。ESM 提供了一种现代的代码结构方式,使其更清晰和易于维护。


ESM 的一个关键优势是能够在 import 语句中显式声明依赖项。这改善了代码的可读性,并帮助你跟踪项目的依赖关系。因此,ESM 正迅速成为新 Node.js 项目的首选模块格式。


以下是如何在 Node 中使用 ESM 模块的演示:


// addTwo.mjs
function addTwo(num) {
return num + 2;
}

export { addTwo };

// app.mjs
import { addTwo } from './addTwo.mjs';

// 打印:6
console.log(addTwo(4));

线程


Node 还推出了工作线程,允许用户将复杂的计算任务卸载到独立的线程。这释放了主线程来处理用户请求,从而带来更流畅和响应更快的用户体验。


const {
Worker,
isMainThread,
setEnvironmentData,
getEnvironmentData,
} = require('node:worker_threads');

if (isMainThread) {
setEnvironmentData('Hello', 'World!');
const worker = new Worker(__filename);
} else {
console.log(getEnvironmentData('Hello')); // 打印“World!”。
}

Fetch


Node.js 现在内置了 Fetch API 的实现,这是一种现代且符合规范的方式来通过网络获取资源。这意味着你可以编写更清晰和一致的代码,而不必依赖外部库。


Node.js 还引入了几个与 Fetch 一起的新功能,以增强 Web 平台的兼容性。这些功能包括:



  • Web Streams:高效处理大数据流,而不会使应用程序不堪重负。

  • FormData:轻松构建和发送表单数据用于 Web 请求。

  • StructuredClone():创建复杂数据结构的深拷贝。

  • textEncoder() 和 textDecoder():无缝处理文本编码和解码任务。

  • Blob:表示各种用途的原始二进制数据。


结合 Fetch,这些新增功能使你能够在 Node.js 环境中完全构建现代 Web 应用程序。


const res = await fetch('https://example.com');
const json = await res.json();
console.log(json);

Promises


Node.js 提供了内置的 Promise 功能,提供了一种更清晰和结构化的方式来处理异步任务的结果(成功或失败)。


与回调地狱相比,使用 Promises 可以编写更自然、更易于理解的代码。


以下是使用 fs/promises 模块中的 readFile 方法的实际示例,展示了 Promises 如何简化异步文件读取:


import { readFile } from 'node:fs/promises';

try {
const filePath = new URL('./package.json', import.meta.url);
const contents = await readFile(filePath, { encoding: 'utf8' });
console.log(contents);
} catch (err) {
console.error(err.message);
}

Node 独有的核心模块


Node.js 引入了核心模块和用户引入模块的明确区分,使用 "node:" 前缀来标识核心模块


这个前缀像是一个标签,立即将模块标识为 Node.js 的核心构建块。这种区分有几个好处:



  • 减少混淆:不再将核心模块误认为是用户创建的模块。

  • 简化选择:使用 "node:" 前缀轻松选择所需的特定核心模块。


这种变化还防止用户使用可能与未来核心模块冲突的名称注册到 npm 注册表中,如下所示:


import test from 'node:test';
import assert from 'node:assert';

Watch


在引入此功能之前,nodemon 是文件更改监视中最流行的包。


现在,--watch 标志提供了:



  • 自动文件监视:它监视您导入的文件,准备在发生任何更改时立即采取行动。

  • 即时重启:每当修改监视的文件时,Node.js 自动重启,确保您的应用程序反映最新更新。

  • 测试协同作用:--watch 标志与测试运行器友好地协作,在文件更改后自动重新运行测试。这使得开发工作流程变得流畅,提供持续反馈。

  • 为了更精细的控制,--watch-path 标志允许您指定要监视的确切文件。


AsyncLocalStorage


AsyncLocalStorage 允许在 Web 请求或任何其他异步持续时间内存储数据。它类似于其他语言中的线程本地存储。


AsyncLocalStorage 增强了开发人员创建像 React 服务器组件这样的功能,并作为 Next.js 请求存储的基础。这些组件简化了 React 应用程序的服务器端渲染,最终提高了开发者体验。


import http from 'node:http';
import { AsyncLocalStorage } from 'node:async_hooks';

const asyncLocalStorage = new AsyncLocalStorage();

function logWithId(msg) {
const id = asyncLocalStorage.getStore();
console.log(`${id !== undefined ? id : '-'}:`, msg);
}

let idSeq = 0;
http.createServer((req, res) => {
asyncLocalStorage.run(idSeq++, () => {
logWithId('start');
// Imagine any chain of async operations here
setImmediate(() => {
logWithId('finish');
res.end();
});
});
}).listen(8080);

http.get('http://localhost:8080');
http.get('http://localhost:8080');
// 输出:
// 0: start
// 1: start
// 0: finish
// 1: finish

WebCrypto


这个标准化的 API 在 Node.js 环境中直接提供了强大的加密工具集。


使用 WebCrypto,您可以利用以下功能:



  • 密钥生成:创建强大的加密密钥以保护您的数据。

  • 加密和解密:对敏感信息进行加密,以安全存储和传输,并在需要时解密。

  • 数字签名:签署数据以确保真实性并防止篡改。

  • 哈希:生成数据的唯一指纹以进行验证和完整性检查。


通过将 WebCrypto 集成到您的 Node.js 应用程序中,您可以显著增强其安全性,并保护用户数据。


const { subtle } = require('node:crypto').webcrypto;

(async function () {
const key = await subtle.generateKey({
name: 'HMAC',
hash: 'SHA-256',
length: 256
}, true, ['sign', 'verify']);

const enc = new TextEncoder();
const message = enc.encode('I love cupcakes');

const digest = await subtle.sign({
name: 'HMAC'
}, key, message);
})();

实用工具


Node 开始提供了许多实用工具。其核心团队认为用户不应该安装新模块来执行基本实用程序。其中一些实用程序包括以下内容。


Utils.ParseArgs()


Node.js 提供了一个名为 Utils.ParseArgs() 的内置实用程序(或来自 node 模块的 parseArgs 函数),简化了解析应用程序中的命令行参数的任务。这消除了对外部模块的需求,使您的代码库更精简。


那么,Utils.ParseArgs() 如何帮助?它接受传递给您的 Node.js 脚本的命令行参数,并将它们转换为更可用的格式,通常是一个对象。这个对象使得在代码中访问和利用这些参数变得容易。


import { parseArgs } from 'node:util';

const args = ['-f', '--bar', 'b'];
const options = {
foo: {
type: 'boolean',
short: 'f',
},
bar: {
type: 'string',
},
};

const {
values,
positionals,
} = parseArgs({ args, options });

console.log(values, positionals);
// 输出:[Object: null prototype] { foo: true, bar: 'b' } []

单一可执行应用程序


单个可执行应用程序使得通过 Node 分发应用程序成为可能。这在构建和分发 CLI 到用户时非常强大。


这个功能将应用程序代码注入到 Node 二进制文件中。可以分发二进制文件而不必安装 Node/npm。目前仅支持单个 CommonJS 文件。


为了简化创建单个可执行文件,Node.js 提供了一个由 Postman Labs 开发的辅助模块 postject。


权限系统


Node.js 进程对系统资源的访问以及可以执行的操作可以通过权限来管理。还可以通过权限管理其他模块可以访问的模块。


process.permission.has('fs.write');
// true
process.permission.deny('fs.write', '/home/user');

process.permission.has('fs.write');
// true
process.permission.has('fs.write', '/home/user');
// false

测试运行器


它使用 node:test、--test 标志和 npm test。它支持子测试、skip/only 和生命周期钩子。它还支持函数和计时器模拟;模块模拟即将推出。


它还通过 --experimental-test-coverage 提供代码覆盖率和通过 -test-reporter 和 -test-reporter-destination 提供报告器。基于 TTY,默认为 spec、TAP 或 stdout。


import test from 'node:test';
import test from 'test';

test('synchronous passing test', (t) => {
// This test passes because it does not throw an exception.
assert.strictEqual(1, 1);
});

test('synchronous failing test', (t) => {
// This test fails because it throws an exception.
assert.strictEqual(1, 2);
});

test('asynchronous passing test', async (t) => {
// This test passes because the Promise returned by the async
// function is settled and not rejected.
assert.strictEqual(1, 1);
});

test('asynchronous failing test', async (t) => {
// This test fails because the Promise returned by the async
// function is rejected.
assert.strictEqual(1, 2);
});

test('failing test using Promises', (t) => {
// Promises can be used directly as well.
return new Promise((resolve, reject) => {
setImmediate(() => {
reject(new Error('this will cause the test to fail'));
});
});
});

test('callback passing test', (t, done) => {
// done() is the callback function. When the setImmediate() runs, it invokes
// done() with no arguments.
setImmediate(done);
});

test('callback failing test', (t, done) => {
// When the setImmediate() runs, done() is invoked with an Error object and
// the test fails.
setImmediate(() => {
done(new Error('callback failure'));
});
});

require(esm)


一个新的标志已经发布,允许开发者同步地引入 ESM 模块。


'use strict';

const { answer } = require('./esm.mjs');
console.log(answer);


另外,一个新的标志 --experimental-detect-module 允许 Node.js 检测模块是 commonJS 还是 esm。这个新标志简化了在 JavaScript 中编写 Bash 脚本。


WebSocket


WebSocket 是 Node.js 最受欢迎的功能请求之一。这个功能也是符合规范的。



为 Node.js 做贡献


作为一种开源技术,Node.js 主要由志愿者和协作者维护。由于 Node.js 的受欢迎程度不断提高,维护工作也越来越具有挑战性,需要更多的帮助。


Node.js 核心协作者维护 nodejs/node GitHub 仓库。Node.js 核心协作者的 GitHub 团队是 @nodejs/collaborators。协作者具有:



  • 对 nodejs/node 仓库的提交访问权限

  • 对 Node.js 持续集成(CI)作业的访问权限


无论是协作者还是非协作者都可以对 Node.js 源代码提出修改建议。提出修改建议的机制是 GitHub 拉取请求(pull request)。协作者审查并合并(land)拉取请求。


在拉取请求能够合并之前,必须得到两个协作者的批准。(如果拉取请求已经开放超过 7 天,一个协作者的批准就足够了。)批准拉取请求表示协作者对变更负责。批准必须来自不是变更作者的协作者。


如果协作者反对提出的变更,则该变更不能合并。例外情况是,如果 TSC 投票批准变更,尽管存在反对意见。通常,不需要涉及 TSC。


通常,讨论或进一步的更改会导致协作者取消他们的反对。


从根本上说,如果您想对 Node.js 的未来有发言权,请开始贡献!



总结


关于 Node.js 衰退的传言被大大夸大了。深入研究这些指标后,可以清楚地看到:Node.js 不仅不会消失,而且正在积极进化以满足现代 Web 开发的需求


凭借庞大的用户基础、繁荣的开源社区和不断创新的功能,Node.js 仍然是一个强大而多功能的平台。最近增加的 ESM、工作线程、Fetch API 和内置模块表明了它在技术前沿保持领先的承诺。


此外,Node.js 通过专门的团队和严格的流程优先考虑安全性。它的开放协作模式欢迎像您这样的开发人员的贡献,确保平台的光明未来。


因此,无论您是经验丰富的开发人员还是刚刚起步,Node.js 都为构建可扩展和高效的 Web 应用程序提供了一个有力的选择。丰富的资源、活跃的社区和对持续改进的承诺使其成为您下一个项目的坚实基础


参考:



作者:五月君
来源:juejin.cn/post/7379667550505304075
收起阅读 »

uni-app利用renderjs实现截取视频第一帧画面作为封面图

web
需求背景 如下图,使用 uni-app 做 app 时,要上传图片和视频,这里选择图片和视频分别使用的 uni.chooseImage 和 uni.chooseVideo,上传使用的 uni.uploadFile,问题就是这些 API 还有上传成功后服务器返回...
继续阅读 »



需求背景


如下图,使用 uni-app 做 app 时,要上传图片和视频,这里选择图片和视频分别使用的 uni.chooseImageuni.chooseVideo,上传使用的 uni.uploadFile,问题就是这些 API 还有上传成功后服务器返回内容中都没有提供视频封面图,于是只能使用一个固定的图片来充当视频封面,但是这样用户体验很不好


image.png


解决思路


在获取到视频链接后,如果我们可以让视频在后台自动播放,出现第一帧画面后再将它给停掉,在这个过程中利用 canvas 截取到视频播放的第一帧画面保存起来,那不就可以作为视频封面了吗?没那么容易,平时在 H5 环境中,到目前为止就行了,但问题是,现在我这里是 App,然后 uni-app 自带的 video 组件没法截取画面,而 App 环境又没法用 H5 环境的 video 标签,它甚至没有 document 对象, 技术框架上不兼容, 那怎么办?


这时候就需要用到 renderjs 了,毕竟它的核心作用之一就是 “在视图层操作dom,运行 for webjs库”。


那思路就有了,在 renderjs 模块中监听原始模块中的文件列表,当更改时(新增、删除),在 renderjs 中动态创建 video 元素,让它自动静音播放,使用 canvas 截取第一帧画面后销毁 video 元素并将图片传递给原始模块,原始模块将其设置为对应视频的封面


代码逻辑


<template>
<view :prop="canvasList" :change:prop="canvas.getVideoCanvas">
<view v-for="(item,index) in fileList" :key="index">
<image v-if="item.type===0" :src="item.url" @click="previewImage(item.url)">image>
<view v-else @click="previewVideoSrc = item.url">

<image mode="widthFix" :src="item.cover">image>

<u-icon class="play-icon" name="play-right-fill" size="30" color="#fff">u-icon>
view>
view>
<view class="preview-full" v-if="previewVideoSrc!=''">
<video :autoplay="true" :src="previewVideoSrc" :show-fullscreen-btn="false">
<cover-view class="preview-full-close" @click="previewVideoSrc=''"> ×
cover-view>
video>
view>
view>
template>

<script>
import { deepClone } from '@/utils'
// 原始模块
export default {
data() {
return {
previewVideoSrc: '', // 预览视频url
fileList: [
{ url: '', type: 0 },
{ url: '', type: 1 },
{ url: '', type: 1 },
] // 真正用来展示和传递的文件列表,type: 0代表图片,1代表视频
}
},
computed: {
// 用于 renderjs 模块监听,不用 fileList 是因为后续还有更改它(为其内部元素添加 cover )属性
// 监听 fileList 然后又更改它会导致循环递归,这里使用 deepClone 也是为了让 canvasList 不与
// fileList 产生关联
canvasList() {
return deepClone(this.fileList)
}
},
methods: {
// 预览图片
previewImage(url) {
uni.previewImage({
urls: [url]
});
},
// 生成视频封面
getVideoPoster({ index, cover }) {
this.$set(this.fileList[index], 'cover', cover)
},
}
}
script>
<script module="canvas" lang="renderjs">
// renderjs 模块
export default {
methods: {
getVideoCanvas(nV, oV, ownerInstance) {
if(oV !== undefined && Array.isArray(nV) && nV.length > 0) {
nV.forEach((item, index) => {
// 如果是视频
if(item.type == 1) {
// 防止一次性执行过多逻辑导致卡顿
setTimeout(() => {
// 创建video标签
let video = document.createElement("video")
// 设置为自动播放和静音
video.setAttribute('autoplay', 'autoplay')
video.setAttribute('muted', 'muted')
// 设置播放源
video.innerHTML = ''
// 创建 canvas 元素和 2d 画布
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
// 监听 video 的 canplay 事件
video.addEventListener('canplay', function () {
// 设置宽高
let anw = document.createAttribute("width");
anw.nodeValue = 80;
let anh = document.createAttribute("height");
anh.nodeValue = 80;
canvas.setAttributeNode(anw);
canvas.setAttributeNode(anh);
// 画布渲染
ctx.drawImage(video, 0, 0, 80, 80);
// 生成 base64 图片
let base64 = canvas.toDataURL('image/png')
// 暂停并销毁 video 元素
video.pause()
video.remove();
// 传递数据给逻辑层
ownerInstance.callMethod('getVideoPoster', {
index,
cover: base64
})
}, false)
}, index * 120)
}
})
}
}
}
}
script>

成果展示


image.png


还有另一个地方,之前就是这样的,都是用的默认图片当作封面:


image.png


经过处理后就是这样啦:


image.png


7.gif


作者:鹏北海
来源:juejin.cn/post/7322762833690066981
收起阅读 »

微信小程序区分环境开发 and 合理绕过官方上线审核

web
前言:首先说明一点,虽然绕过官方审核,是不推荐的行为,但是实际的项目开发中,难免会有一些需求或功能在发布上线时,会被官方拒绝。例如类目不对的情况,由于企业性质或其他原因,无法申请相关类目要求的资质;或者申请资质办理难度大、所需时间漫长,无法在上线节点前申请完成...
继续阅读 »

前言:

首先说明一点,虽然绕过官方审核,是不推荐的行为,但是实际的项目开发中,难免会有一些需求或功能在发布上线时,会被官方拒绝。

例如类目不对的情况,由于企业性质或其他原因,无法申请相关类目要求的资质;或者申请资质办理难度大、所需时间漫长,无法在上线节点前申请完成,但是实际业务中确实有此需求。

这就需要在上线时先合理绕过官方审核,以期能顺利发布成功,不耽误业务使用。

一、背景和问题描述

很多开发者在开发项目的时候发现,上线微信小程序最难的不是开发阶段,而是微信审核机制。因为微信为了自身平台规避法律风险,开发的很多功能需要提供相关的正件或者资质,就像前面所说,相关的资质办理难度大,或者一般的公司根本办不下来。那么绕过审核就是一个很重要的上线技巧。
我们之前开发的一个微信小程序,涉及一些视频,发布审核时,被官方认定需要补充“教育服务-在线视频课程类目”。如下图所示:

image.png

但是我们项目中的视频内容是关于“用车知识的介绍和使用须知”,并不属于教育类视频或直播课程,而且我们也拥有“教育服务 > 在线教育”的服务类目,可能跟“在线视频课程”类目不一样。

image.png

可是实际业务中确实需要此功能,那么该如何顺利上线呢?

二、解决思路

因为需要此功能,那么:

  1. 体验版环境下必须能正常展示,才能让测试同事正常测试。
  2. 在提交审核时,即在开发版环境下,此模块需要隐藏,才能绕过官方审核,使审核通过。
  3. 在发布审核成功后,即在正式版环境下,此模块需正常展示,可供用户使用。

三、解决方案

我这边实现了两种解决方法,供大家参考:

方案一

核心: 使用 wx.getAccountInfoSync()

功能描述: 获取当前账号信息。线上小程序版本号仅支持在正式版小程序中获取,开发版和体验版中无法获取。

可参考微信小程序官方文档: 获取当前账号信息:Object wx.getAccountInfoSync()

image.png

具体使用方法如下:

  1. 在小程序项目的app.js文件中的onLaunch中获取小程序账号信息:

image.png

onLaunch: function () {
//启动时动态获取小程序的 appid
const accountinfo = wx.getAccountInfoSync()

wx.setStorageSync('miniProgram', accountinfo.miniProgram)
},
  1. 然后在需要做判断的模块的页面获取miniProgram,我这边是在展示视频模块入口页面获取:
  • js文件中获取账号信息的值:

image.png

data: {
miniProgram: wx.getStorageSync('miniProgram'),
},
  • html文件中进行判断:

image.png

注:我是使用miniProgram.version的值进行判断的。
因为此值是线上小程序版本号,只有在线上环境中才会有值,所以只会在线上环境中展示,提交审核的开发环境中看不到此模块。
而在体验版环境下,我不会加wx:if="{{miniProgram.version}}"这个代码,只有在提交审核时加上。缺点就是需要改动代码,但是能完美避开审核,使审核顺利通过。

方案二

核心: 使用小程序视频插件。
优点: 完美继承完美继承小程序原生的所有特性和事件。不用改代码。

后期我们开发了一个小程序的视频插件,在展示视频的页面中,使用视频插件代替。这样也能完美通过审核。

image.png

这个小程序视频插件作用是,专门为没有视频播放资质的小程序提供视频播放功能,解决视频播放资质问题。

思路来源于官方解答:

image.png

涉小程序插件功能介绍: developers.weixin.qq.com/miniprogram…

涉小程序类目资质、适用范围参考:developers.weixin.qq.com/miniprogram…

以上,希望对大家有帮助!


作者:小蹦跶儿
来源:juejin.cn/post/7340154170234552370
收起阅读 »

微信小程序:uniapp解决上传小程序体积过大的问题

web
概述 在昨天的工作中遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,首先介绍一下,技术栈是使用uniapp框架+HBuilderX的开发环境。 错误提示 真机调试,提示包提交过大,不能正常生成二维码,后续上传代码更是不可能了,减少包中的...
继续阅读 »

概述


在昨天的工作中遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,首先介绍一下,技术栈是使用uniapp框架+HBuilderX的开发环境。


错误提示


图4.png


真机调试,提示包提交过大,不能正常生成二维码,后续上传代码更是不可能了,减少包中的体积顺着这条思路去解决问题。


1.静态图片资源改变成网络请求的方式


问题5.png


我们使用的初衷是,把图片加载在static本地,缓存在本地,以便提升更快的响应速度,第一步剥离大的图片更换成网络请求,顺着编辑器提示去处理。


2.对小程序进行分包


小程序主包最大可以加载到1.5M,加载所有的依赖和插件不能大于2M,小程序中有个解决办法是对小程序进行分包处理,使每个包保持在2M的大小,主包和分包之间直接进行跳转,分包和分包不能跳转。


"optimization" : {
"subPackages" : true
},

进行了拆包还是没有解决问题,分包的作用主要运行的是代码,也就是说代码要尽量的小,多了需要进行分解。


3.压缩vendor.js


昨天真正的定位问题是vendor.js 1.88M ,小程序开发代码工具-详情-代码依赖分析中查看,解决vendor.js才是根本的解决之道。


使用HBuilderX打包上传来解决问题,HBuilderX -> 发行 -> 小程序(微信),操作的过程失败了一次,是因为需要注意的是需要绑定开发者后台的地方,开发管理->开发设置->小程序代码上传下载小程序代码上传密钥和绑定IP白名单,这个需要管理员同意。


问题6.png


最后包的体积从12.88M压缩到了4.16M,问题得以解决。


作者:stark张宇
来源:juejin.cn/post/7282363816020508733
收起阅读 »

uni-app app端 人脸识别

web
在听到人脸识别,连忙去看看,去阿里 腾讯 看他们的人脸识别方法,官方sdk什么的。 到后来,需求确定了,拍照(照片)上传,后台去识别是不是本人,这一瞬间从天堂到地狱,放着官方那么好的方法。 用照片,还的自己去写,去实现。 下面为大家提供一个 uni-app 自...
继续阅读 »

在听到人脸识别,连忙去看看,去阿里 腾讯 看他们的人脸识别方法,官方sdk什么的。


到后来,需求确定了,拍照(照片)上传,后台去识别是不是本人,这一瞬间从天堂到地狱,放着官方那么好的方法。


用照片,还的自己去写,去实现。


下面为大家提供一个 uni-app 自动拍照 上传照片 后端做匹配处理。


参考插件市场的 ext.dcloud.net.cn/plugin?id=4…


在使用前 先去manifest.json 选择APP模块配置, 勾选直播推流



直接采用nvue开发,直接使用live-pusher组件进行直播推流,如果是vue开发,则需要使用h5+的plus.video.LivePusher对象来获取


nuve js注意事项


注意nuve 页面 main.js 的封装函数 。无法直接调用(小程序其他的端没有测试)


在APP端 this.api报错,显示是undefined,难道nvue页面,要重新引入api文件


在APP端,main.js中挂载Vuex在nvue页面无法使用this.$store.state.xxx


简单粗暴点直接用uni.getStorageSync 重新获取一遍


//获取用户数据 userInfo在Data里定义


this``.userInfo = uni.getStorageSync(``'userInfo'``)


nuve css注意事项


单位只支持px


其他的em,rem,pt,%,upx 都不支持


需要重新引入外部css


不支持使用 import 的方式引入外部 css


<``style src="@/common/test.css"></``style``>


 默认flex布


display: flex; //不需要写
//直接用下面的标签
flex-direction: column;
align-items: center;
justify-content: space-between;

页面样式


<view class="live-camera" :style="{ width: windowWidth, height: windowHeight }">
<view class="title">
{{second}}秒之后开始识别
</view>
<view class="preview" :style="{ width: windowWidth, height: windowHeight-80 }">
<live-pusher id="livePusher" ref="livePusher" class="livePusher" mode="FHD" beauty="1" whiteness="0"
aspect="2:3" min-bitrate="1000" audio-quality="16KHz" :auto-focus="true" :muted="true"
:enable-camera="true" :enable-mic="false" :zoom="false" @statechange="statechange"
:style="{ width: cameraWidth, height: cameraHeight }">
</live-pusher>

<!--提示语-->
<cover-view class="remind">
<text class="remind-text" style="">{{ message }}</text>
</cover-view>

<!--辅助线-->
<cover-view class="outline-box" :style="{ width: windowWidth, height: windowHeight-80 }">
<cover-image class="outline-img" src="../../static/idphotoskin.png"></cover-image>
</cover-view>
</view>
</view>

JS部分


<script>
import operate from '../../common/operate.js'
import api from '../../common/api.js'
export default {
data() {
return {
//提示
message: '',
//相机画面宽度
cameraWidth: '',
//相机画面宽度
cameraHeight: '',
//屏幕可用宽度
windowWidth: '',
//屏幕可用高度
windowHeight: '',
//流视频对象
livePusher: null,
//照片
snapshotsrc: null,
//倒计时
second: 0,
ifPhoto: false,
// 用户信息
userInfo: []
};
},
onLoad() {
//获取屏幕高度
this.initCamera();
//获取用户数据
this.userInfo = uni.getStorageSync('userInfo')
setTimeout(() => {
//倒计时
this.getCount()
}, 500)
},
onReady() {
// console.log('初始化 直播组件');
this.livePusher = uni.createLivePusherContext('livePusher', this);
},
onShow() {
//开启预览并设置摄像头
/*
* 2023年12月28日
* 在最新的APP上面这个周期 比onReady 直播初始要早执行
* 故而第二次进入页面 相机启动失败
* 把该方法 移步到 onReady 即可
*/


this.startPreview();

},
methods: {
//获取屏幕高度
initCamera() {
let that = this
uni.getSystemInfo({
success: function(res) {
that.windowWidth = res.windowWidth;
that.windowHeight = res.windowHeight;
that.cameraWidth = res.windowWidth;
that.cameraHeight = res.windowWidth * 1.5;
}
});
},
//启动相机
startPreview() {
this.livePusher.startPreview({
success(res) {
console.log('启动相机', res)
}
});
},
//停止相机
stopPreview() {
let that = this
this.livePusher.stopPreview({
success(res) {
console.log('停止相机', res)
}
});
},
//摄像头 状态
statechange(e) {
console.log('摄像头', e);
if (this.ifPhoto == true) {
//拍照
this.snapshot()
}
},
//抓拍
snapshot() {
let that = this
this.livePusher.snapshot({
success(res) {
that.snapshotsrc = res.message.tempImagePath;
that.uploadingImg(res.message.tempImagePath)
}
});
},
// 倒计时
getCount() {
this.second = 5
let timer = setInterval(() => {
this.second--;
if (this.second < 1) {
clearInterval(timer);
this.second = 0
this.ifPhoto = true
this.statechange()
}
}, 1000)
},
// 图片上传
uploadingImg(e) {
let url = e
// console.log(url);
let that = this
uni.uploadFile({
url: operate.api + 'api/common/upload',
filePath: url,
name: 'file',
formData: {
token: that.userInfo.token
},
success(res) {
// console.log(res);
let list = JSON.parse(res.data)
// console.log(list);
that.request(list.data.fullurl)
}
})
},
//验证请求
request(url) {
let data = {
token: this.userInfo.token,
photo: url
}
api.renzheng(data).then((res) => {
// console.log(res);
operate.toast({
title: res.data.msg
})
if (res.data.code == 1) {
setTimeout(() => {
operate.redirectTo('/pages/details/details')
}, 500)
}
if (res.data.code == 0) {
setTimeout(() => {
this.anew(res.data.msg)
}, 500)
}
})
},
// 认证失败,重新认证
anew(msg) {
let that = this
uni.showModal({
content: msg,
confirmText: '重新审核',
success(res) {
if (res.confirm) {
// console.log('用户点击确定');
that.getCount()
} else if (res.cancel) {
// console.log('用户点击取消');
uni.navigateBack({
delta: 1
})
}
}
})
},
}
};
</script>

css 样式


<style lang="scss">
// 标题
.title {
font-size: 35rpx;
align-items: center;
justify-content: center;
}

.live-camera {
.preview {
justify-content: center;
align-items: center;

.outline-box {
position: absolute;
top: 0;
left: 0;
bottom: 0;
z-index: 99;
align-items: center;
justify-content: center;

.outline-img {
width: 750rpx;
height: 1125rpx;
}
}

.remind {
position: absolute;
top: 880rpx;
width: 750rpx;
z-index: 100;
align-items: center;
justify-content: center;

.remind-text {
color: #dddddd;
font-weight: bold;
}
}
}
}
</style>


作者:虚乄
来源:juejin.cn/post/7273126566459719741
收起阅读 »

Jquery4.0发布!下载量依旧是 Vue 的两倍!

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 其实在去年,Jquery 就宣布了要发布 4 版本 可以看到,Jquery 在五天前发布了 4 版本 Jquery4.0 更新了啥? 接下来说一下到...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


背景


其实在去年,Jquery 就宣布了要发布 4 版本



可以看到,Jquery 在五天前发布了 4 版本




Jquery4.0 更新了啥?


接下来说一下到底更新了啥?


弃用了 1x 和 2x 版本,废弃一些方法


这意味着不再去兼容低版本了,未来 Jquery 将着力于发展新的版本,弃用了一些方法



  • jQuery.cssNumber

  • jQuery.cssProps

  • jQuery.isArray

  • jQuery.parseJSON

  • jQuery.nodeName

  • jQuery.isFunction

  • jQuery.isWindow

  • jQuery.camelCase

  • jQuery.type

  • jQuery.now

  • jQuery.isNumeric

  • jQuery.trim

  • jQuery.fx.interval


Typescript 重构


看过 Jquery 源码的都知道,以前 Jquery 是用 JavaScript 写的,现在新版本是采用 Typescript 重构的,提高整体代码的可维护性


对新特性的支持


jQuery 4.0 将添加对新的 JavaScript 特性的支持,包括:



  • async/await

  • Promise

  • Optional Chaining

  • Nullish Coalescing


优化性能



  • 优化 DOM 操作

  • 改进事件处理

  • 优化 Ajax 请求

  • 增强兼容性


增强兼容性



  • 支持 Internet Explorer 11 和更高版本

  • 支持 Edge 浏览器

  • 支持 Safari 浏览器


FormData 支持


jQuery.ajax 添加了对二进制数据的支持,包括 FormData。


此外,jQuery 4.0 还删除了自动 JSONP 升级、将 jQuery source 迁移至 ES 模块;以及添加了对 Trusted Types 的支持,确保以 TrustedHTML 封装的 HTML 能以不违反 require-trusted-types-for 内容安全策略指令的方式用作 jQuery 操作方法的输入。


由于删除了 Deferreds 和 Callbacks(现在压缩后不到 20k 字节),jQuery 4.0.0 的 slim build 变得更加小巧。


还有人用 Jquery 吗?


随着现在前端发展的迅速,越来越多人投入了 React、Vue 的怀抱,这意味着越来越少人用 Jquery 了,而且用 Jquery 的基本都是老项目,老项目都是求稳的,所以也不会去升级 Jquery


所以我不太看好 Jquery 后续的发展趋势,虽然曾经它真的帮助了我们很多


虽然如此,现阶段 NPM 上,Jquery 的下载量依旧是 Vue 的两倍



作者:Sunshine_Lin
来源:juejin.cn/post/7362727170039070771
收起阅读 »

适配最新微信小程序隐私协议开发指南,兼容uniapp版本

web
前一阵微信小程序官方发布了一个用户隐私保护指引填写说明,说是为了规范开发者的用户个人信息处理行为,保障用户合法权益,小程序、插件中涉及处理用户个人信息的开发者,均需补充相应用户隐私保护指引。 估计是有啥政策强制要求,微信自己也还没想好要咋实现就匆匆发布了第一版...
继续阅读 »

前一阵微信小程序官方发布了一个用户隐私保护指引填写说明,说是为了规范开发者的用户个人信息处理行为,保障用户合法权益,小程序、插件中涉及处理用户个人信息的开发者,均需补充相应用户隐私保护指引。


估计是有啥政策强制要求,微信自己也还没想好要咋实现就匆匆发布了第一版,然后不出意外各种问题,终于在2023年8月22发布了可以正常接入调试的版本。


逛开发者社区很多人在吐槽这个东西,按照现在的实现方式微信完全可以自己在它的框架层实现,非得让开发者多此一举搞个弹窗再去调它的接口通知它。


吐槽归吐槽,代码还是要改的不是,毕竟不改9月15号之后相关功能就直接挂了!时间紧任务重下面直说干货。


准备工作



  • 小程序后台设置用户隐私保护指引,需要等待审核通过:设置-基本设置-服务内容声明-用户隐私保护指引

  • 小程序的基础库版本从 2.32.3 开始支持,所以要选这之后的版本

  • 在 app.json 中加上这个设置 " usePrivacyCheck" : true,在2023年9月15号之前需要自己手动加这个设置,15号之后平台就强制了


具体步骤可以参考官方给的开发文档,里面也有官方提供的 demo 文件。



原生小程序适配代码


直接参考的官方给的 demo3 和 demo4 综合修改出的版本,通过组件的方式引用,所有相关处理逻辑全部放到了 privacy 组件内部,其他涉及到隐私接口的页面只需在 wxml 里引用一下就行了,其他任何操作都不需要,组件内部已经全部处理了。


网上有其他人分享的,要在页面 onLoad、onShow 里获取是否有授权这些,用下面的代码这些都不需要,只要页面里需要隐私授权,引入 privacy 组件后,用户触发了隐私接口时会自动弹出此隐私授权弹窗。



长话短说这一步你总共只需做2个步骤:



  1. 新建一个 privacy 组件:privacy.wxml、privacy.wxss、privacy.js、privacy.json,完整代码在下方

  2. 在涉及隐私接口的页面引入 privacy 组件,如果使用的页面比较多,可以直接在 app.json 文件里通过 usingComponents 全局引入


privacy.wxml


<view wx:if="{{innerShow}}" class="privacy">
<view class="privacy-mask" />
<view class="privacy-dialog-wrap">
<view class="privacy-dialog">
<view class="privacy-dialog-header">用户隐私保护提示</view>
<view class="privacy-dialog-content">感谢您使用本小程序,在使用前您应当阅读井同意<text class="privacy-link" bindtap="openPrivacyContract">《用户隐私保护指引》</text>,当点击同意并继续时,即表示您已理解并同意该条款内容,该条款将对您产生法律约束力;如您不同意,将无法继续使用小程序相关功能。</view>
<view class="privacy-dialog-footer">
<button
id="btn-disagree"
type="default"
class="btn btn-disagree"
bindtap="handleDisagree"
>不同意</button>
<button
id="agree-btn"
type="default"
open-type="agreePrivacyAuthorization"
class="btn btn-agree"
bindagreeprivacyauthorization="handleAgree"
>同意并继续</button>
</view>
</view>
</view>
</view>

privacy.wxss


.privacy-mask {
position: fixed;
z-index: 5000;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.2);
}

.privacy-dialog-wrap {
position: fixed;
z-index: 5000;
top: 16px;
bottom: 16px;
left: 80rpx;
right: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}

.privacy-dialog {
background-color: #fff;
border-radius: 32rpx;
}

.privacy-dialog-header {
padding: 60rpx 40rpx 30rpx;
font-weight: 700;
font-size: 36rpx;
text-align: center;
}

.privacy-dialog-content {
font-size: 30rpx;
color: #555;
line-height: 2;
text-align: left;
padding: 0 40rpx;
}

.privacy-dialog-content .privacy-link {
color: #2f80ed;
}

.privacy-dialog-footer {
display: flex;
padding: 20rpx 40rpx 60rpx;
}

.privacy-dialog-footer .btn {
color: #FFF;
font-size: 30rpx;
font-weight: 500;
line-height: 100rpx;
text-align: center;
height: 100rpx;
border-radius: 20rpx;
border: none;
background: #07c160;
flex: 1;
margin-left: 30rpx;
justify-content: center;
}

.privacy-dialog-footer .btn::after {
border: none;
}

.privacy-dialog-footer .btn-disagree {
color: #07c160;
background: #f2f2f2;
margin-left: 0;
}

privacy.js


let privacyHandler
let privacyResolves = new Set()
let closeOtherPagePopUpHooks = new Set()

if (wx.onNeedPrivacyAuthorization) {
wx.onNeedPrivacyAuthorization(resolve => {
if (typeof privacyHandler === 'function') {
privacyHandler(resolve)
}
})
}

const closeOtherPagePopUp = (closePopUp) => {
closeOtherPagePopUpHooks.forEach(hook => {
if (closePopUp !== hook) {
hook()
}
})
}

Component({
data: {
innerShow: false,
},
lifetimes: {
attached: function() {
const closePopUp = () => {
this.disPopUp()
}
privacyHandler = resolve => {
privacyResolves.add(resolve)
this.popUp()
// 额外逻辑:当前页面的隐私弹窗弹起的时候,关掉其他页面的隐私弹窗
closeOtherPagePopUp(closePopUp)
}

closeOtherPagePopUpHooks.add(closePopUp)

this.closePopUp = closePopUp
},
detached: function() {
closeOtherPagePopUpHooks.delete(this.closePopUp)
}
},
pageLifetimes: {
show: function() {
this.curPageShow()
}
},
methods: {
handleAgree(e) {
this.disPopUp()
privacyResolves.forEach(resolve => {
resolve({
event: 'agree',
buttonId: 'agree-btn'
})
})
privacyResolves.clear()
},
handleDisagree(e) {
this.disPopUp()
privacyResolves.forEach(resolve => {
resolve({
event: 'disagree',
})
})
privacyResolves.clear()
},
popUp() {
if (this.data.innerShow === false) {
this.setData({
innerShow: true
})
}
},
disPopUp() {
if (this.data.innerShow === true) {
this.setData({
innerShow: false
})
}
},
openPrivacyContract() {
wx.openPrivacyContract({
success: res => {
console.log('openPrivacyContract success')
},
fail: res => {
console.error('openPrivacyContract fail', res)
}
})
},
curPageShow() {
if (this.closePopUp) {
privacyHandler = resolve => {
privacyResolves.add(resolve)
this.popUp()
// 额外逻辑:当前页面的隐私弹窗弹起的时候,关掉其他页面的隐私弹窗
closeOtherPagePopUp(this.closePopUp)
}
}
}
}
})

privacy.json


{
"component": true,
"usingComponents": {}
}

uniapp版本


uniapp 版本也可以直接用上面的代码,新建的 privacy 组件放到微信小程序对应的 wxcompnents 目录下,这个目录下是可以直接放微信小程序原生的组件代码的,因为目前只有微信小程序有这个东西,后期还可能随时会更改,所以没必要再额外去封装成 vue 组件了。


页面引用组件的时候直接用条件编译去引用:


{
// #ifdef MP-WEIXIN
"usingComponents": {
"privacy": "/wxcomponents/privacy/privacy"
}
// #endif
}

在 vue 页面里使用组件也要用条件编译:


<template>
<view>
<!-- #ifdef MP-WEIXIN -->
<privacy />
<!-- #endif -->
</view>
</template>

注意uniapp官方目前还没有来适配微信这,目前开发调试 usePrivacyCheck 这个设置放到 page.json 文件里无效的,要放到 manifest.json 文件的 mp-weixin 下面:


{
"name" : "uni-plus",
"appid" : "__UNI__3C6F1BF",
"mp-weixin" : {
"appid" : "wx123456789",
"__usePrivacyCheck__" : true
}
}

作者:cafehaus
来源:juejin.cn/post/7272276908381175819
收起阅读 »

在微信小程序里使用rpx,被坑了😕 | 不同设备表现不同

web
小小需求 实现一个 Tooltip,就是那种很简单有一个按钮,按一下就会出现或消失气泡的组件,就是下面这个效果。 放个按钮、加个点击事件、做好气泡的定位好像就搞定了。然鹅,事情没有发展的这么顺利。 开发 组件结构 按照上面说的过程,放好按钮,添加好点击事...
继续阅读 »

小小需求


实现一个 Tooltip,就是那种很简单有一个按钮,按一下就会出现或消失气泡的组件,就是下面这个效果。


gif


放个按钮、加个点击事件、做好气泡的定位好像就搞定了。然鹅,事情没有发展的这么顺利。


开发


组件结构


按照上面说的过程,放好按钮,添加好点击事件。比较复杂的地方就是处理气泡的定位,气泡需要进行绝对定位,让它脱离文档流,不能在隐藏或偏移的时候还占个坑(需要实现那种浮动的效果)。还有气泡有个小三角,这个三角也是需要额外处理定位的。于是设计了组件的结构如下:
old.png
Tooltip 包着 Button 显示在界面上,设置定位属性,让它可以成为子元素定位的基准元素。然后创建 Prompt,它会相对于 Tooltip 进行定位,Prompt 中的小三角形则相对于 Prompt 进行定位。定位的具体数值则根据元素的尺寸和想放置的位置进行定位。在这个例子中就是实现气泡在按钮下方居中显示, Prompt 偏移数值计算如下:

水平偏移需要考虑 Tooltip 和 Prompt 的宽度,偏移的距离就是两者宽度之差的一半。


move


left=width(Tooltip)/2width(Prompt)/2left = width(Tooltip) / 2 - width(Prompt) / 2

垂直偏移则要考虑 Tooltip、Prompt 和 小三角的高度,计算方法类似。(偷懒不做动图了)

top=height(Tooltip)+hegiht(::before_border)+height(gap)top = height(Tooltip) + hegiht(::before\_border) + height(gap)

小三角相对于气泡的偏移也是类似的计算方法,总之能够根据元素的尺寸让偏移刚好能够居中。


代码


代码如下:(wxml 和 wxss 没有高亮,用 html 和 css 格式代替了)


<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>

.tooltip{
display: flex;
align-items: center;
position:relative;
width:350rpx;
height: 64rpx;
}
.prompt-container{
background-color: rgba(0,0,0,0.7);
border-radius: 16rpx;
color: #fff;
width: 200rpx;

position:absolute;
padding: 20rpx;
top: 80rpx;
left: 55rpx;
}
.prompt-container::before{
position:absolute;
content:'';
width: 0rpx;
height: 0rpx;
border: 14rpx solid transparent;
border-bottom-color:rgba(0,0,0,0.7);
top: -28rpx;
left: 103rpx;
}
.prompt-title{
font-size: 26rpx;
font-weight: bold;
}
.prompt-content{
font-size: 24rpx;
}

踩坑


美滋滋呀,这就做完了,在 iPhone 12 mini 模拟器上看起来丝毫没有问题,整个气泡内容看起来是那么完美~


12mini


换个最豪(ang)华(gui)的机型(iPhone 14 Pro Max)看看,大事不妙,怎么小三角和气泡之间出现了一条缝,再看看代码按按计算器,算的尺寸没有任何问题呀!但是展示就是变成了这样:


14pm


爬坑


打开调试器一顿猛调试,发现了一些不对劲,下面慢慢说。


关于 rpx


微信小程序提供了个特殊的单位 rpx,代码中也都是使用这个单位进行开发,据说是能够方便开发者在不同尺寸设备上实现自适应布局。
放一张官方文档截图:
wxwd.png
它的意思就是,它把所有的屏幕宽度都设置为 750rpx,不管这个设备真实的宽度有几个设备独立像素(就是宽度有多少 px)。开发者只需要使用 rpx 为单位,小程序会帮你把 rpx 转成 px,听起来是不是很方便很友好~(但并不是🌚)


试试这个公式是不是真的


根据图中提供的转换方式 1rpx=(screenWidth/750)×px1rpx = (screenWidth / 750) \times px
用上面那个例子中的 Tooltip 组件来进行验证,手动算一下在设备上得到的 px 值是不是真的能用上面的公式计算出来。


设备屏幕尺寸 / 750组件理论尺寸真实尺寸
iPhone 12 mini375 / 750 = 0.5Tooltip175 × 32175 × 32
iPhone 14 Pro Max428 / 750 = 0.57Tooltip199.5 × 36.48199× 36

看起来真实的计算会直接省略小数点后的值,直接进行取整。这可能是导致我们的预期和真实展示有偏差的原因。再看看其他组件的尺寸计算:


设备屏幕尺寸 / 750组件理论尺寸真实尺寸
iPhone 12 mini375 / 750 = 0.5Prompt120120
::before14 × 1414 × 14
iPhone 14 Pro Max428 / 750 = 0.57Prompt136.8136
::before15.96 × 15.9614 × 14

因为高度是根据文字自适应的,所以这里没有计算 Prompt 的高度。但依然可以从表中看出,rpx 到 px 的转换并不是简单的直接取整,不然 iPhone 14 Pro Max 中的 ::before 应该尺寸为 15 × 15。至于到底是所有 rpx 到 px 转换都有隐藏的规则,还是伪元素的尺寸转换和其他元素不统一,还是 border 尺寸计算比较特殊,我们也无从得知,官方也没有相关说明。


小三角相对于气泡的偏移


其实在这个例子中,我们最关心的就是这个小三角相对于气泡的偏移是不是符合预期,整体气泡居中与否那么小的差别我们几乎看不出来,但是这个小三角偏离气泡这段距离,搁谁都无法接受。
那着重看下这个小三角的偏移我们是怎么做的,小三角的尺寸完全是由边构成的,完整的矩形尺寸是 28rpx×28rpx28rpx × 28rpx,我们向上的偏移需要设置成整个矩形的尺寸,也就是 top:28rpxtop: -28rpx,这样才能让下半部分的小三角完全展示出来。理论上来说,尺寸和偏移都设置 rpx 为单位,如果使用统一的转换规则,那肯定也是没问题的,既然出现了问题肯定是两者的计算不是那么的统一。我们看到实际的结果,尺寸计算是不符合我们的预期的,那么就猜测偏移可能是按照公式计算的。可以来验证一下,计算得到的值 1515 和真实值 1414 相差 1px1px,我打算放个高度为 1px1px 的长条在这个缝隙里,看看是不是刚好塞进去。
test.png

竟然真的刚好塞进去了,这说明我的猜测应该没有错,偏移的计算在我们预期中,小三角向上移动了 15px。但不能进行更多的验证了,再猜我就要把这个规律猜出来了(手动狗头🤪)。
总之就是,盒子尺寸的计算和偏移距离的计算用的不是一个规律,这就是坑之所在。


解决方案


针对当前需求


我们可以避开这个坑,让小三角相对于气泡不要产生偏移,而是能死死的贴在气泡上。想要达到这样的效果,我们需要修改一下布局结构,改成下面这个样子:


new.png
让气泡整体和小三角形成兄弟关系,那他俩就不会分离了,然后整体的偏移让他们的父节点 Prompt 来决定。


代码如下:


<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-triangle"></view>
<view class="prompt-text-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>
</view>

.tooltip{
display: flex;
align-items: center;
position:relative;
width:350rpx;
height: 64rpx;
}
.prompt-container{
position:absolute;
top: 80rpx;
left: 55rpx;
}
.prompt-triangle{
position:relative;
content:'';
width: 0rpx;
height: 0rpx;
border: 14rpx solid transparent;
border-bottom-color:rgba(0,0,0,0.7);
left: 103rpx;
}
.prompt-text-container{
background-color: rgba(0,0,0,0.7);
border-radius: 16rpx;
color: #fff;
width: 200rpx;
padding: 20rpx;
}
.prompt-title{
font-size: 26rpx;
font-weight: bold;
}
.prompt-content{
font-size: 24rpx;
}


通用方法


除了上面避坑的方法,还有一个方法就是进行一个填坑。自定义实现一个 rpx2px 方法,动态的根据设备来进行 px 值的计算,再通过内联样式传递给元素。


function rpx2px(rpx){
return ( wx.getSystemInfoSync().windowWidth / 750) * rpx
}
Page({
data: {
top: rpx2px(100), // 类似这样定义一个状态,通过内联样式传入
},
})

<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-triangle" style="top:{{top}}px"></view>
<!-- 注意上面👆这里添加了 style 内联样式 -->
<view class="prompt-text-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>
</view>

这样子不管是什么尺寸什么偏移都按照统一的规则进行换算,妈妈再也不同担心我被坑啦~~~


总结


虽然解法看起来如此简单,但是爬坑的过程真是无比艰难,各种猜测和假设,虽然一些得到了验证,但最终也是无法猜透小程序的心~~ 只能自己避坑和填坑,按需选择吧。


作者:用户9787521254131
来源:juejin.cn/post/7257516901843763257
收起阅读 »

写html页面没意思,来挑战chrome插件开发

web
谷歌浏览器插件开发是指开发可以在谷歌浏览器中运行的扩展程序,可以为用户提供额外的功能和定制化的体验。谷歌浏览器插件通常由HTML、CSS和JavaScript组成,非常利于前端开发者。 开发者可以利用这些技术在浏览器中添加新的功能、修改现有功能或者与网页进行交...
继续阅读 »

谷歌浏览器插件开发是指开发可以在谷歌浏览器中运行的扩展程序,可以为用户提供额外的功能和定制化的体验。谷歌浏览器插件通常由HTML、CSS和JavaScript组成,非常利于前端开发者。
开发者可以利用这些技术在浏览器中添加新的功能、修改现有功能或者与网页进行交互。


要开发谷歌浏览器插件,开发者通常需要创建一个包含*清单文件(manifest.json)、背景脚本(background script)、内容脚本(content script)*等文件的项目结构。清单文件是插件的配置文件,包含插件的名称、版本、描述、权限以及其他相关信息。背景脚本用于处理插件的后台逻辑,而内容脚本则用于在网页中执行JavaScript代码。


谷歌浏览器插件可以实现各种功能,例如添加新的工具栏按钮、修改网页内容、捕获用户输入、与后台服务器进行通信等。开发者可以通过谷歌浏览器插件API来访问浏览器的各种功能和数据,实现各种定制化的需求。
插件开发涉及的要点:


image.png


基础配置


开发谷歌浏览器插件,最重要的文件 manifest.json


{
"name": "Getting Started Example", // 插件名称
"description": "Build an Extension!", // 插件描述
"version": "1.0", // 版本
"manifest_version": 3, // 指定插件版本,这个很重要,指定什么版本就用什么样的api,不能用错了
"background": {
"service_worker": "background.js" // 指定background脚本的路径
},
"action": {
"default_popup": "popup.html", // 指定popup的路径
"default_icon": { // 指定popup的图标,不同尺寸
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
}
},
"icons": { // 指定插件的图标,不同尺寸
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
},
"permissions": [],// 指定应该在脚本中注入那些变量方法,后文再详细说
"options_page": "options.html",
"content_scripts": [ // 指定content脚本配置
{
"js": [ "content.js"], // content脚本路径
"css":[ "content.css" ],// content的css
"matches": ["<all_urls>"] // 对匹配到的tab起作用。all_urls就是全部都起作用
}
]
}


  • name: 插件名称


manifest_version:对应chrome API插件版本,浏览器插件采用的版本,目前共2种版本,是2和最新版3



  • version: 本插件的版本,和发布相关

  • action:点击图标时,设置一些交互

    • default_icon:展示图标

      • 16、32、48、128



    • default_popup:popup.html,一个弹窗页面

    • default_title:显示的标题



  • permissions:拥有的权限

    • tabs:监听浏览器tab切换事件



  • options_ui

  • background:

    • service_worker:设置打开独立页面




官方实例


官方教程


打开pop弹窗页面


设置action的default_popup属性


{
"name": "Hello world",
"description": "show 'hello world'!",
"version": "1.0",
"manifest_version": 3,
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
}
},
"permissions":["tabs", "storage", "activeTab", "idle"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"js": [ "content.js"],
"css":[ "content.css" ],
"matches": ["<all_urls>"]
}
]
}

创建popup.html


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>显示出hello world</title>
<link rel="stylesheet" type="text/css" href="popup.css">
</head>

<body>
<h1>显示出hello world</h1>
<button id="clickBtn">点击按钮</button>
<script src="popup.js"></script>
</body>
</html>

文件可以通过链接引入css、js。


body {
width: 600px;
height: 300px;
}
h1 {
background-color: antiquewhite;
font-weight: 100;
}


console.log(document.getElementById('clickBtn'));
document.getElementById('clickBtn').addEventListener('click', function () {
console.log('clicked');
});

点击插件图标


点击图标可以看到如下的popup的页面。


image.png


调试popup.js的方法



  • 通过弹窗,在弹窗内部点击右键,选择审查内容
    image.png

  • 通过插件图标,进行点击鼠标右键,选择审查弹出内容
    image.png


通过background打开独立页面


基于backgroundservice_workerAPI可以打开一个独立后台运行脚本。此脚本会随着插件安装,初始化执行一次,然后一直在后台运行。可以用来存储浏览器的全局状态数据。
background脚本是长时间运行在后台,随着浏览器打开就运行,直到浏览器关闭而结束运行。通常把需要一直运行的、启动就运行的、全局公用的数据放到background脚本。


chrome.action.onClicked.addListener(function () {
chrome.tabs.create({
url: chrome.runtime.getURL('newPage.html')
});
});


为了打开独立页面,需要修改manifest.json


{
"name": "newPage",
"description": "Demonstrates the chrome.tabs API and the chrome.windows API by providing a user interface to manage tabs and windows.",
"version": "0.1",
"permissions": ["tabs"],
"background": {
"service_worker": "service-worker.js"
},
"action": {
"default_title": "Show tab inspector"
},
"manifest_version": 3
}

为了实现打开独立页面,在manifest.json中就不能在配置 action:default_popup
newPage.js文件中可以使用*chrome.tabs*和chrome.windowsAPI;
可以使用 chrome.runtime.getUrl 跳转一个页面。


chrome.runtime.onInstalled.addListener(async () => {
chrome.tabs.create(
{
url: chrome.runtime.getURL('newPage.html'),
}
);
});

content内容脚本


content-scripts(内容脚本)是在网页上下文中运行的文件。通过使用标准的文档对象模型(DOM),它能够读取浏览器访问的网页的详细信息,可以对打开的页面进行更改,还可以将DOM信息传递给其父级插件。内容脚本相对于background还是有一些访问API上的限制,它可以直接访问以下chrome的API



  • i18n

  • storage

  • runtime:

    • connect

    • getManifest

    • getURL

    • id

    • onConnect

    • onMessage

    • sendMessage




content.js运行于一个独立、隔离的环境,它不会和主页面的脚本或者其他插件的内容脚本发生冲突
有2种方式添加content脚本


在配置中设置


"content_scripts": [
{
"js": [ "content.js"],
"css":[ "content.css" ],
"matches": ["<all_urls>"]
}
]

content_scripts属性除了配置js,还可以设置css样式,来实现修改页面的样式。
matches表示需要匹配的页面;
除了这3个属性,还有



  • run_at: 脚本运行时刻,有以下3个选项

    • document_idle,默认;浏览器会选择一个合适的时间注入,并是在dom完成加载

    • document_start;css加载完成,dom和脚本加载之前注入。

    • document_end:dom加载完成之后



  • exclude_matches:排除匹配到的url地址。作用和matches相反。


动态配置注入


在特定时刻才进行注入,比如点击了某个按钮,或者指定的时刻
需要在popup.jsbackground.js中执行注入的代码。


chrome.tabs.executeScript(tabs[0].id, {
code: 'document.body.style.backgroundColor = "red";',
});

也可以将整个content.js进行注入


chrome.tabs.executeScript(tabs[0].id, {
file: "content.js",
});

利用content制作一个弹窗工具


某天不小心让你的女神生气了,为了能够道歉争取到原谅,你是否可以写一个道歉信贴到每一个页面上,当女神打开网站,看到每个页面都会有道歉内容。


image.png


道歉信内容自己写哈,这个具体看你的诚意。
下面设置2个按钮,原谅和不原谅。 点击原谅,就可以关闭弹窗。 点击不原谅,这个弹窗调整css布局位置继续显示。(有点像恶意贴片广告了)


下面设置content.js的内容


let newDiv = document.createElement('div');
newDiv.innerHTML = `<div id="wrapper">
<h3>小仙女~消消气</h3>
<div><button id="cancel">已消气</button>
<button id="reject">不原谅</button></div>
</div>`
;
newDiv.id = 'newDiv';
document.body.appendChild(newDiv);
const cancelBtn = document.querySelector('#cancel');
const rejectBtn = document.querySelector('#reject');
cancelBtn.onclick = function() {
document.body.removeChild(newDiv);
chrome.storage.sync.set({ state: 'cancel' }, (data) => {
});
}
rejectBtn.onclick = function() {
newDiv.style.bottom = Math.random() * 200 + 10 + "px";
newDiv.style.right = Math.random() * 800 + 10 + "px";
}
// chrome.storage.sync.get({ state: '' }, (data) => {
// if (data.state === 'cancel') {
// document.body.removeChild(newDiv);
// }
// });

content.css布局样式


#newDiv {
font-size: 36px;
color: burlywood;
position: fixed;
bottom: 20px;
right: 0;
width: 300px;
height: 200px;
background-color: rgb(237, 229, 216);
text-align: center;
z-index: 9999;
}

打开option页面


options页,就是插件的设置页面,有2个入口



  • 1:点击插件详情,找到扩展程序选项入口


image.png



  • 2插件图标,点击右键,选择 ‘选项’ 菜单


image.png


可以看到设置的option.html页面


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>插件的option配置</title>
</head>
<body>
<h3>插件的option配置</h3>
</body>
</html>

此页面也可以进行js、css的引入。


替换浏览器默认页面


override功能,是可以替换掉浏览器默认功能的页面,可以替换newtab、history、bookmark三个功能,将新开页面、历史记录页面、书签页面设置为自定义的内容。
修改manifest.json配置


{
"chrome_url_overrides": {
"newtab": "newtab.html",
"history": "history.html",
"bookmarks": "bookmarks.html"
}
}

创建一个newtab的html页面


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>new tab</h1>
</body>
</html>

插件更新后,点开新的tab,就会出现我们自定义的页面。第一次的情况会让用户进行选择,是进行更换还是保留原来的配置。


image.png
很多插件都是使用newtab进行自定义打开的tab页,比如掘金的浏览器插件,打开新页面就是掘金网站插件


页面之间进行数据通信


image.png
如需将单条消息发送到扩展程序的其他部分并选择性地接收响应,请调用 runtime.sendMessage()tabs.sendMessage()。通过这些方法,您可以从内容脚本向扩展程序发送一次性 JSON 可序列化消息,或者从扩展程序向内容脚本发送。如需处理响应,请使用返回的 promise。
来源地址:developer.chrome.com/docs/extens…


content中脚本发送消息


chrome.runtime.sendMessage只能放在content的脚本中。


(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();

其他页面发送消息


其他页面需向内容脚本发送请求,请指定请求应用于哪个标签页,如下所示。此示例适用于 Service Worker、弹出式窗口和作为标签页打开的 chrome-extension:// 页面


(async () => {
const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();

接收消息使用onMessage


在扩展程序和内容脚本中使用相同的代码


chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
if (request.greeting === "hello")
sendResponse({farewell: "goodbye"});
}
);

添加右键菜单


创建菜单


首先在manifest.json的权限中添加配置


{
"permissions": ["contextMenus"]
}


background.js中添加创建菜单的代码


let menu1 = chrome.contextMenus.create({
type: 'radio', // 可以是 【normal、checkbox、radio】,默认是normal
title: 'click me',
id: "myMenu1Id",
contexts:['image'] // 只有是图片时,菜显示
}, function(){

})

let menu2 = chrome.contextMenus.create({
type: 'normal', // 可以是 【normal、checkbox、radio】,默认是normal
title: 'click me222',
id: "myMenu222Id",
contexts:['all'] //所有类型都显示
}, function(){

})

let menu3 = chrome.contextMenus.create({
id: 'baidusearch1',
title: '使用百度搜索:%s',
contexts: ['selection'], //选择页面上的文字
});

// 删除一个菜单
chrome.contextMenus.remove('myMenu222Id'); // 被删除菜单的id menuItemId
// 删除所有菜单
chrome.contextMenus.removeAll();

// 绑定菜单点击事件
chrome.contextMenus.onClicked.addListener(function(info, tab){
if(info.menuItemId == 'myMenu222Id'){
console.log('xxx')
}
})

以下是其他可以使用的api


// 删除某一个菜单项
chrome.contextMenus.remove(menuItemId);
// 删除所有自定义右键菜单
chrome.contextMenus.removeAll();
// 更新某一个菜单项
chrome.contextMenus.update(menuItemId, updateProperties);
// 监听菜单项点击事件, 这里使用的是 onClicked
chrome.contextMenus.onClicked.addListener(function(info, tab)) {
//...
});

绑定点击事件,发送接口请求


首先需要在manifest.jsonhosts_permissions中添加配置


{
"host_permissions": ["http://*/*", "https://*/*"]
}

创建node服务器,返回json数据


// server.mjs
const { createServer } = require('node:http');
const url = require('url');

const server = createServer((req, res) => {
var pathname = url.parse(req.url).pathname;

if (pathname.includes('api')) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write(
JSON.stringify({
name: 'John Doe',
age: 30,
})
);
res.end();
} else {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World!\n' + pathname);
}
});

server.listen(8080, '127.0.0.1', () => {
console.log('Listening on 127.0.0.1:8080');
});

编辑background.js文件


// 插件右键快捷键
// 点击右键进行选择
chrome.contextMenus.onClicked.addListener(function (info, tab) {
if (info.menuItemId === 'group1') {
console.log('分组文字1', info);
}
if (info.menuItemId === 'group2') {
console.log('分组文字2');
}
// 点击获取到数据
if (info.menuItemId === 'fetch') {
console.log('fetch 获取数据');
const res = fetch('http://localhost:8080/api', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}).then((res) => {
console.log(res, '获取到http://localhost:8080/api接口数据');
chrome.storage.sync.set({ color: 'red' }, function (err, data) {
console.log('store success!');
});
});
}
// 创建百度搜索,并跳转到搜索结果页
if (info.menuItemId === 'baidusearch1') {
// console.log(info, tab, "baidusearch1")
// 创建一个新的tab页面
chrome.tabs.create({
url:
'https://www.baidu.com/s?ie=utf-8&wd=' + encodeURI(info.selectionText),
});
}
});

// 创建右键快捷键
chrome.runtime.onInstalled.addListener(function () {
// Create one test item for each context type.
let contexts = [
'page',
'selection',
'link',
'editable',
'image',
'video',
'audio',
];
// for (let i = 0; i < contexts.length; i++) {
// let context = contexts[i];
// let title = "Test '" + context + "' menu item";
// chrome.contextMenus.create({
// title: title,
// contexts: [context],
// id: context,
// });
// }

// Create a parent item and two children.
let parent = chrome.contextMenus.create({
title: '操作数据分组',
id: 'parent',
});
chrome.contextMenus.create({
title: '分组1',
parentId: parent,
id: 'group1',
});
chrome.contextMenus.create({
title: '分组2',
parentId: parent,
id: 'group2',
});
chrome.contextMenus.create({
title: '获取远程数据',
parentId: parent,
id: 'fetch',
});

// Create a radio item.
chrome.contextMenus.create({
title: '创建单选按钮1',
type: 'radio',
id: 'radio1',
});
chrome.contextMenus.create({
title: '创建单选按钮2',
type: 'radio',
id: 'radio2',
});

// Create a checkbox item.
chrome.contextMenus.create({
title: '可以多选的复选框1',
type: 'checkbox',
id: 'checkbox',
});
chrome.contextMenus.create({
title: '可以多选的复选框2',
type: 'checkbox',
id: 'checkbox2',
});

// 在title属性中有一个%s的标识符,当contexts为selection,使用%s来表示选中的文字
chrome.contextMenus.create({
id: 'baidusearch1',
title: '使用百度搜索:%s',
contexts: ['selection'],
});

// Intentionally create an invalid item, to show off error checking in the
// create callback.
chrome.contextMenus.create(
{ title: 'Oops', parentId: 999, id: 'errorItem' },
function () {
if (chrome.runtime.lastError) {
console.log('Got expected error: ' + chrome.runtime.lastError.message);
}
}
);
});

点击鼠标右键,效果如下


image.png


image.png


如果在页面选择几个文字,那么就显示出百度搜索快捷键,


image.png


缓存,数据存储


首先在manifest.json的权限中添加storage配置


{
"permissions": ["storage"]
}

chrome.storage.sync.set({color: 'red'}, function(){
console.log('background js storage set data ok!')
})

然后就可以在content.js或popup.js中获取到数据


// 这里的参数是,获取不到数据时的默认参数
chrome.storage.sync.get({color: 'yellow'}, function(){
console.log('background js storage set data ok!')
})

tabs创建页签


首先在manifest.json的权限中添加tabs配置


{
"permissions": ["tabs"]
}

添加tabs的相关操作


chrome.tabs.query({}, function(tabs){
console.log(tabs)
})
function getCurrentTab(){
let [tab] = chrome.tabs.query({active: true, lastFocusedWindow: true});
return tab;
}

notifications消息通知


Chrome提供chrome.notifications的API来推送桌面通知;首先在manifest.json中配置权限


{
"permissions": [
"notifications"
],
}

然后在background.js脚本中进行创建


// background.js
chrome.notifications.create(null, {
type: "basic",
iconUrl: "drink.png",
title: "喝水小助手",
message: "看到此消息的人可以和我一起来喝一杯水",
});


devtools开发扩展工具


在manifest中配置一个devtools.html


{
"devtools_page": "devtools.html",
}

devtools.html中只引用了devtools.js,如果写了其他内容也不会展示


<!DOCTYPE html>
<html lang="en">
<head> </head>
<body>
<script type="text/javascript" src="./devtools.js"></script>
</body>
</html>

创建devtools.js文件


// devtools.js
// 创建扩展面板
chrome.devtools.panels.create(
// 扩展面板显示名称
"DevPanel",
// 扩展面板icon,并不展示
"panel.png",
// 扩展面板页面
"Panel.html",
function (panel) {
console.log("自定义面板创建成功!");
}
);

// 创建自定义侧边栏
chrome.devtools.panels.elements.createSidebarPane(
"Sidebar",
function (sidebar) {
sidebar.setPage("sidebar.html");
}
);

然后在创建自定的Panel.html和sidebar.html页面。


相关代码下载


作者:北鸟南游
来源:juejin.cn/post/7350571075548397618
收起阅读 »

到底怎样配色才能降低图表的可读性?

web
点赞 + 关注 + 收藏 = 学会了 本文简介 在数据可视化的世界里,图表是我们最常用的语言。但你是否曾被一张图表的配色误导? 配色方案的选择往往被看作是一种艺术,但其实它更是一门科学。 文章将带你一探究竟,哪些配色选择实际上会削弱图表的表达力,甚至误导读者。...
继续阅读 »

点赞 + 关注 + 收藏 = 学会了


本文简介


在数据可视化的世界里,图表是我们最常用的语言。但你是否曾被一张图表的配色误导?


配色方案的选择往往被看作是一种艺术,但其实它更是一门科学。


文章将带你一探究竟,哪些配色选择实际上会削弱图表的表达力,甚至误导读者。


过于丰富的颜色


我管理着10家酒店。以下是这10家酒店在2023年里的收入数据。


1月2月3月4月5月6月7月8月9月10月11月12月
酒店A134501360013200135001370013350136501340013800132001360013700
酒店B680071007300690072007400700073007500760071007200
酒店C152001490015100148001500014700152001490015100148001500014700
酒店D830085008100840086008200850083008600840085008600
酒店E118001200012200121001190012100122001200011800121001190012000
酒店F790081007700830078008400800082008100830084008200
酒店G146501440014700145001480014400146001470014500144001460014800
酒店H550057005800560059005750580059505600590057005800
酒店I143001400014200141001430014200140001410014300142001410014300
酒店J960094009800950097009600990094009800950097009600

我想按月对比酒店G酒店I的收入,并且能直观的知道这两家酒店在所有酒店中的收入属于什么水平。


如果按下图这样展示,对吗?


01.png


粗略一看,这图的数据还挺丰富的,色彩也挺吸引眼球。但你花了多久才找到酒店G酒店I


我们使用 Echarts 等图表库时,通常都会在页面中展示图例。如果想看酒店G酒店I的数据,那我们把其他酒店的数据隐藏掉就行了。


02.png


这样确实能很直观的看到酒店G酒店I的收入趋势和对比。


但把其他酒店的数据隐藏了,又观察不到这两家酒店在所有酒店中的收入水平。


更好的做法是将其他酒店的颜色设置为灰色。


03.png


灰色是一个不起眼的颜色,非常适合用来展示“背景信息”,它不像其他颜色那样吸引眼球。


在上面这个例子中,灰色的主要作用是描述“大环境”,用来凸显想要强调的信息。


但在实际项目中,如果页面的背景色不是白色,又想做到上面这个例子的效果,那可以在页面背景色的基础上往“白色”或者“黑色”方向调色。


04.png


比如,圆点是页面的背景色,红框部分就是可以选择的“背景信息”的颜色。


现在回过头来看看为什么会出现色彩丰富的图表。


我猜有两种可能。


一是项目需求,比如做To G的大屏项目,通常需要炫酷的特效和丰富的色彩去吸引甲方眼球。


二是设计工具或者前端的图表库默认提供了丰富的颜色,开发者只管把数据丢给图表库使用默认的配色去渲染。


配色始终不如一


同一个数据,在不同页中使用了不同的配色方案。用户会觉得你的产品很不专业,也很难培养用户习惯和对品牌的认知。


举个例子,在下方这个图中,顶部的柱状图和下方3个折线图的配色完全不一样。


05.png


反传统的配色


我们的产品支持微信和支付宝这两个支付方式,我们都知道支付宝的主色是蓝色,微信的主色是绿色。


在统计支付来源的数据时,如果出现反传统的配色就会影响用户对数据的理解。


06.png


再错得离谱点的话,可能会将支付宝和微信的主色对掉。


07.png




IMG_0393.GIF


点赞 + 关注 + 收藏 = 学会了


作者:德育处主任
来源:juejin.cn/post/7383268946819792911
收起阅读 »

解决小程序web-view两个恶心问题

web
1.web-view覆盖层问题 问题由来 web-view 是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面。 所以这得多恶心。。。不仅铺满,还覆盖了普通的标签,调z-index都无解。 解决办法 web-view内部使用cover-...
继续阅读 »

1.web-view覆盖层问题


问题由来


web-view 是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面。



所以这得多恶心。。。不仅铺满,还覆盖了普通的标签,调z-index都无解。



解决办法


web-view内部使用cover-view,调整cover-view的样式即可覆盖在web-view上。


cover-view


覆盖在原生组件上的文本视图。


app-vue和小程序框架,渲染引擎是webview的。但为了优化体验,部分组件如map、video、textarea、canvas通过原生控件实现,原生组件层级高于前端组件(类似flash层级高于div)。为了能正常覆盖原生组件,设计了cover-view。


支持的平台:


AppH5微信小程序支付宝小程序百度小程序

具体实现


<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
</view>

</template>

.close-view{
position: fixed;
z-index: 99999;
top: 30rpx;
left: 45vw;

.close-icon{
width: 100rpx;
height: 80rpx;
}
}

代码说明:这里的案例是一个关闭按钮图标悬浮在webview上,点击图标可以关闭当前预览的webview。


注意


仅仅真机上才生效,开发者工具上是看不到效果的,如果要调整覆盖层的样式,可以先把web-view标签注释了,写完样式没问题再释放web-view标签。


2.web-view导航栏返回


问题由来



  • 小程序端 web-view 组件一定有原生导航栏,下面一定是全屏的 web-view 组件,navigationStyle: custom 对 web-view 组件无效。


场景


用户在嵌套的webview里填写表单,不小心按到导航栏的返回了,就全没了。


解决办法


使用page-container容器,点击到返回的时候,给个提示。


page-container


页面容器。


小程序如果在页面内进行复杂的界面设计(如在页面内弹出半屏的弹窗、在页面内加载一个全屏的子页面等),用户进行返回操作会直接离开当前页面,不符合用户预期,预期应为关闭当前弹出的组件。 为此提供“假页”容器组件,效果类似于 popup 弹出层,页面内存在该容器时,当用户进行返回操作,关闭该容器不关闭页面。返回操作包括三种情形,右滑手势、安卓物理返回键和调用 navigateBack 接口。


具体实现


<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
<!--这里这里,就这一句-->
<page-container :show="isShow" :overlay="false" @beforeleave="beforeleave"></page-container>
</view>

</template>

export default {
data() {
return {
isShow: true
}
},
methods: {
beforeleave(){
this.isShow = false
uni.showToast({
title: '别点这里',
icon: 'none',
duration: 3000
})
}
}
}


结语


算是小完美的解决了吧,这里记录一下,看看就行,勿喷。


作者:世界哪有真情
来源:juejin.cn/post/7379960023407198220
收起阅读 »

React Native新架构:恐怖的性能提升

web
自2018年以来,React Native团队一直在重构其核心架构,以便开发者能够创建更高质量更好性能的体验。最近在 React Native 的官网看到他们在安利他们的新的架构,本文将我所了解到的一些皮毛带给大家。以浅薄的见解来揭示其所带来的显著的性能改进,...
继续阅读 »

新架构


自2018年以来,React Native团队一直在重构其核心架构,以便开发者能够创建更高质量更好性能的体验。最近在 React Native 的官网看到他们在安利他们的新的架构,本文将我所了解到的一些皮毛带给大家。以浅薄的见解来揭示其所带来的显著的性能改进,并探讨为何以及如何过渡到这一新架构。


为什么需要新的架构?


多年来,使用React Native构建应用遇到了一些不可避免的限制。比如:React Native的布局和动画效果可能不如原生应用流畅,JavaScript和原生代码之间的通信效率低下,序列化和反序列化开销大,以及无法利用新的React特性等。这些限制在现有架构下无法解决,因此新的架构应运而生。新的架构提升了React Native在数个方面的能力,使得一些之前无法实现的特性和优化成为可能。


同步布局和效果


对比下老的架构(左边)和新的架构(右边)的效果:


React


构建自适应的UI体验通常需要测量视图的大小和位置并进行调整。在现有架构中,使用onLayout事件获取布局信息可能导致用户看到中间状态或视觉跳跃。而在新架构下,useLayoutEffect可以同步获取布局信息并更新,让这些中间状态彻底消失。可以明显看到不会存在跟不上的情况。


function ViewWithTooltip() {
const targetRef = React.useRef(null);
const [targetRect, setTargetRect] = React.useState(null);

useLayoutEffect(() => {
targetRef.current?.measureInWindow((x, y, width, height) => {
setTargetRect({ x, y, width, height });
});
}, [setTargetRect]);

return (
<>
<View ref={targetRef}>
<Text>一些内容,显示一个悬浮提示Text>

View>
<Tooltip targetRect={targetRect} />

);
}

支持并发渲染和新特性


可以看到新架构支持了并发渲染的效果对比,左边是老架构,右边是新架构:


并发渲染特性


新架构支持React 18及之后版本的并发渲染和新特性,例如Suspense数据获取和Transitions。这使得web和原生React开发之间的代码库和概念更加一致。同时,自动批处理减少了重绘的次数,提升了UI的流畅性。


function TileSlider({ value, onValueChange }) {
const [isPending, startTransition] = useTransition();

return (
<>
<View>
<Text>渲染 {value} 瓷砖Text>

<ActivityIndicator animating={isPending} />
View>
<Slider
value={value}
minimumValue={1}
maximumValue={1000}
step={1}
onValueChange={newValue =>
{
startTransition(() => {
onValueChange(newValue);
});
}}
/>

);
}

快速的JavaScript/Native接口


新架构移除了JavaScript和原生代码之间的异步桥接,取而代之的是JavaScript接口(JSI)。JSI允许JavaScript直接持有C++对象的引用,从而大大提高了调用效率。这使得像VisionCamera这样处理实时帧的库能够高效运行,消除大量序列化的开销。


JSI


VisionCamera 的地址是:github.com/mrousavy/re…


目前多达6K+的star,这个在 React Native 上的份量还是响当当的,可以看到它明显是用上了 JSI 了,向先驱们致敬。
VisionCamera


启用新架构的期望


尽管新架构提供了显著的改进,启用新架构并不一定会立即提升应用的性能。你的代码可能需要重构以利用新的功能,如同步布局效果或并发特性。或许,我认为,React Native 可能会同步出一些工具来帮助我们更好的迁移。比如配套的 eslint 插件,提示更优的建议写法等等。


现在是否应该使用新架构?


目前新架构仍被视为实验性,在2024年末发布的React Native版本中将成为默认设置。对于大多数生产环境应用,建议等待正式发布。库维护者则可以尝试启用并确认其用例被覆盖。另外看到react-native-vision-camera 这个库的 issue 下面反馈,JSI 目前还是存在一些坑需要爬的,所以要尝鲜的话,还是要有心理准备。
还是有坑在


通过详细介绍新架构的一系列优势和实际应用,我们可以看到React Native的未来发展前景。尽早了解和适应这些变化,一旦新架构正式发布,我们就能更好地利用React Native的潜力,为用户提供更好的体验。更好的产品体验,意味着产品的竞争力也会更强。


作者:brzhang
来源:juejin.cn/post/7377277576651898899
收起阅读 »

Jenkins 自动化部署微信小程序

web
近期一直参与微信小程序的开发工作,这段时间让我受益匪浅。在整个过程中,学到了很多关于小程序开发的知识和技能,比如如何优化小程序的性能、如何设计更好的用户界面、如何提高小程序的安全性,以及在小程序展示统计图表,层级渲染问题等等。同时,我也深刻认识到了小程序开发中...
继续阅读 »

近期一直参与微信小程序的开发工作,这段时间让我受益匪浅。在整个过程中,学到了很多关于小程序开发的知识和技能,比如如何优化小程序的性能、如何设计更好的用户界面、如何提高小程序的安全性,以及在小程序展示统计图表,层级渲染问题等等。同时,我也深刻认识到了小程序开发中的一些痛点,比如提测和修改bug需要被测试催着在 测试、uat、生产 环境中频繁发版,很是难受,于是想把这些繁琐的步骤交给机器处理,最终确定技术方案,利用Jenkinsuniapp() 还有 官方打包部署预览脚手架(miniprogram-ci) 配置了一套自动化部署的流程


准备


安装jenkins


服务器(本文 服务器系统是Ubuntu 22.04) 安装好jenkins,具体的步骤可以参考这篇文章


jenkins 自动化部署前端项目


配置项目


开发项目git仓库,项目搭建 具体请查看这篇


用Vue打造微信小程序,让你的开发效率翻倍!


打包部署预览原理和脚本编写请移步这篇文章


命令行秒传:一键上传微信小程序和生成二维码预览


上传脚本沿用了这篇文章的中脚本:命令行秒传:一键上传微信小程序和生成二维码预览,只需要略微改动, 改动支持了 设置版本号和备注,且先生成预览二维码和上传到微信小程序后台平台体验版


下面的代码中 appid 和 私钥(小程序后台的私钥 具体配置获取方法请参考上面文章链接)的路径 请自行更改


// 小程序发版
const ci = require('miniprogram-ci');
const path = require('path');
const argv = require('yargs').argv;

const appid = '*******'
let versions = '1.0.1'
let descs = '备注'
let projectPath = './dist/build/mp-weixin'
return (async () => {
if (argv.version) {
versions = argv.version
}
if (argv.descs) {
versions = argv.version
}
// 注意: new ci.Project 调用时,请确保项目代码已经是完整的,避免编译过程出现找不到文件的报错。
const project = new ci.Project({
appid: appid,
type: 'miniProgram',
projectPath: path.join(__dirname, projectPath), // 项目路径
privateKeyPath: path.join(__dirname, './private.*******.key'), // 私钥的路径
ignores: ['node_modules/**/*'],
})
// 生成二维码
const previewResult = await ci.preview({
project,
desc: 'hello', // 此备注将显示在“小程序助手”开发版列表中
setting: {
es6: true,
},
qrcodeFormat: 'image',
qrcodeOutputDest: path.join(__dirname, '/qrcode/destination.jpg'),
onProgressUpdate: console.log,
pagePath: 'pages/index/index', // 预览页面
searchQuery: 'a=1&b=2', // 预览参数 [注意!]这里的`&`字符在命令行中应写成转义字符`\&`
})
console.log('previewResult', previewResult)
console.log('等待5秒后 开始上传')
// 开始上传
let s = 5
let timer = setInterval(async () => {
--s
console.log(`${s}秒`)
if (s == 0) {
clearInterval(timer);
timer = undefined
const uploadResult = await ci.upload({
project,
version: versions,
desc: descs,
setting: {
es6: false, // es6 转 es5
disableUseStrict: true,
autoPrefixWXSS: true, // 上传时样式自动补全
minifyJS: true,
minifyWXML: true,
minifyWXSS: true,
}
})
console.log('uploadResult', uploadResult)
}
}, 1000);
})()

服务器配置 ssh配置


root 用户登录服务器 执行以下命令 切换为jenkins用户


sudo su jenkins

执行生成sshkey命令


 ssh-keygen -t rsa -C "你的邮箱"
// 然后一路回车

image.png


输出ssh私钥 和 公钥 保存备用


 cat ~/.ssh/id_rsa
cat ~/.ssh/id_rsa.pub

image.png


jenkins配置


全局工具配置


配置Git installations


image.png
配置NodeJS版本


image.png


安装jenkins插件


Git Parameter: git分支参数插件


description setter 根据构建日志文件的正则表达式设置每个构建的描述


Version Number 修改版本号


在 Manage Jenkins->插件管理中 搜索 Git Parameter 并且安装重启生效


image.png


image.png


3.jpg


image.png


image.png


配置 ssh


jenkins 全局安全配置


系统管理->全局安全配置->Git Host Key Verification Configuration,选则Manually provided keys


Approved Host Keys中填写上方 服务器的jenkins用户生成的私钥内容


image.png


image.png


git仓库配置 ssh


如果你的项目是私人隐藏的,则需要在项目 配置 SSH 公钥(从上文服务器jenkins用户生成公钥获取内容)


image.png


修改标记格式器


这一步是为了 在构建记录中输出二维码和备注准备


在全局安全配置中 找到标记格式器,改为Safe HTML 保存


image.png


创建任务


首页新建任务


构建一个多配置项目
1.jpg
填写描述、选择github项目写入地址


设置参数


勾选参数化构建过程,添加git参数,输入名称、描述、默认分支
参数类型选择 分支


image.png
继续新增 字符参数 version和remark(这里名字可以自定义,随便起,与shell 脚本中变量名称相匹配就好 )


image.png


配置源码管理


源码管理选择Git,填写 Repository URL,Branches to build 指定分支 ${branch}


image.png


构建环境


勾选 Create a formatted version number


依次填写


Environment Variable Name:BUILD_VERSION


Version Number Format String: ${branch}


Project Start Date: 2023-06-30(项目开始日期)


image.png


Provide Node & npm bin/ folder to PATH 选择 18.16.1


image.png


Build Steps shell脚本


点击 增加构建步骤,选择 执行 shell 输入以下命令(可根据自己的实际情况进行改写)


下方的图片(destination.jpg)存放目录,记得配置文件访问服务,或者自行 编写上传图片逻辑,保证能访问到图片即可;



cd /var/lib/jenkins/workspace/wechart;
npm config set registry https://registry.npmmirror.com/;
npm install -g yarn;
yarn config set registry https://registry.npmmirror.com/;
yarn;
npm run build:mp-weixin;
node upload.js --version=$version --remark=$remark;
mv qrcode/destination.jpg /var/www/html;
chmod -R 777 /var/www/html/destination.jpg;
echo DESC_INFO:http://服务器域名/destination.jpg,$remark;
exit 0;

继续新增构建步骤 选择 Set build description


Regular expression 填写 DESC_INFO:(.*),(.*)


Description 填写 <img src="\1" height:"200" width="200" /><div style="color:blue;">\2</div>


image.png


构建后操作


选择Git Publisher


勾选Push Only If Build Succeeds


点击 Add Tag


Tag to push: wechart-$BUILD_NUMBER


勾选 Create new tag
image.png


点击保存


打包运行构建


选择 Build with Parameters,设置分支(这里会默认显示 git仓库的所有分支)


image.png


打包构建完成后,选择 wechart 点击 配置default


image.png


image.png


image.png


作者:iwhao
来源:juejin.cn/post/7250374485567750203
收起阅读 »

深入理解前端缓存

web
前端缓存是所有前端程序员在成长历程中必须要面临的问题,它会让我们的项目得到非常大的优化提升,同样也会带来一些其它方面的困扰。大部分前端程序员也了解一些缓存相关的知识,比如:强缓存、协商缓存、cookie等,但是我相信大部分的前端程序员不了解它们的缓存机制。接下...
继续阅读 »

前端缓存是所有前端程序员在成长历程中必须要面临的问题,它会让我们的项目得到非常大的优化提升,同样也会带来一些其它方面的困扰。大部分前端程序员也了解一些缓存相关的知识,比如:强缓存协商缓存cookie等,但是我相信大部分的前端程序员不了解它们的缓存机制。接下来我将带你们深入理解缓存的机制以及缓存时间的判断公式,如何合理的使用缓存机制来更好的提升优化。我将会把前端缓存分成HTTP缓存和浏览器缓存两个部分来和大家一起聊聊。


HTTP 缓存


HTTP是一种超文本传输协议,它通常运行在TCP之上,从浏览器Network中可以看到,它分为Respnse Headers(响应头)Request Headers(请求头)两部分组成。


image.png


接下来介绍一下与缓存相关的头部字段:


image.png


expires


我们先来看一下MDN对于expires的介绍



响应标头包含响应应被视为过期的日期/时间。


备注:  如果响应中有指令为 max-age 或 s-maxage 的 Cache-Control 标头,则 Expires 标头会被忽略。



Expires: Wed, 24 Apr 2024 14:27:26 GMT

Cache-Control


Cache-ControlHTTP/1.1中定义的缓存字段,它可以由多种组合使用,分开列如:max-age、s-maxage、public/private、no-cache/no-store等


Cache-Control: max-age=3600, s-maxage=3600, public

max-age是相对当前时间,单位是秒,当设置max-age时则expires就会失效,max-age的优先级更高。


而 s-maxage 与 max-age 不同之处在于,其只适用于公共缓存服务器,比如资源从源服务器发出后又被中间的代理服务器接收并缓存。


public是指该资源可以被任何节点缓存,而private只能提供给客户端缓存。当设置了private之后,s-maxage则会无效。


使用no-store表示不进行资源缓存。使用no-cache表示告知(代理)服务器不直接使用缓存,要求向源服务器发起请求,而当在响应首部中被返回时,表示客户端可以缓存资源,但每次使用缓存资源前都必须先向服务器确认其有效性,这对每次访问都需要确认身份的应用来说很有用。


当然,我们也可以在代码里加入 meta 标签的方式来修改资源的请求首部:


<meta http-equiv="Cache-Control" content="no-cache" />

示例


这里我起了一个nestjs的服务,该getdata接口缓存10s的时间,Ï代码如下:


  @Get('/getdata')
getData(@Response() res: Res) {
return res.set({ 'Expires': new Date(Date.now() + 10).toUTCString() }).json({
list: new Array(1000000).fill(1).map((item, index) => ({ index, item: 'index' + index }))
});Ï
}

第一次请求,花费了334ms的时间。


image.png


第二次请求花费了163ms的时间,走的是磁盘缓存,快了近50%的速度


image.png


接下来我们来验证使用Cache-Control是否可以覆盖Exprie,我们将getdata接口修改如下,Cache-Control设置了1s。Ï我们刷新页面可以看到getdata接口并没有缓存,每次都会想服务器发送请求。


  @Get('/getdata')
getData(@Response() res: Res) {
return res.set({ 'Expires': new Date(Date.now() + 10).toUTCString(), 'Cache-Control': 1 }).json({
list: new Array(1000000).fill(1).map((item, index) => ({ index, item: 'index' + index }))
});
}

仔细的同学应该会发现一个问题,清除缓存后的第一次请求和第二次请求Size的大小不一样,这是为什么呢?


打开f12右键刷新按钮,点击清空缓存并硬性重新加载。


image.png


我们开启Big request rows更方便查看Size的大小,开启时Size显示两行,第一行就是请求内容的大小,第二行则是实际的大小。


image.png


刷新一下,可以看到Size变成了283B大小了。


image.png


带着这个问题我们来深入研究一下浏览器的压缩。HTTP2和HTTP3的压缩算法是大致相同,我们就拿HTTP2的压缩算法(HPACK)来了解一下。


HTTP2 HPACK压缩算法


HPACK压缩算法大致分为:静态Huffman(哈夫曼)压缩和动态Huffman哈夫曼压缩,所谓静态压缩是指根据HTTP提供的静态字典表来查找对应的请求头字段从而存储对应的index值,可以极大的减少内催空间。


动态压缩它是在同一个会话级的,第一个请求的响应里包含了一个比如 {list: [1, 2, 3]},那么就会把它存进表里面,后续的其它请求的响应,就可以只返回这个 header 在动态表里的索引,实现压缩的目的


需要详细了解哈夫曼算法原理的可以去这个博客看一看。


Last-Modified 与 If-Modified-Since


Last-Modified代表资源的最后修改时间,其属于响应首部字段。当浏览器第一次接收到服务器返回资源的 Last-Modified 值后,其会把这个值存储起来,并下次访问该资源时通过携带If-Modified-Since请求首部发送给服务器验证该资源是否过期。


yaml
复制代码
Last-Modified: Fri , 14 May 2021 17:23:13 GMT
If-Modified-Since: Fri , 14 May 2021 17:23:13 GMT

如果在If-Modified-Since字段指定的时间之后资源都没有发生更新,那么服务器会返回状态码 304 Not Modified 的响应。


Etag 与 If--Match


Etag代码该资源的唯一标识,它会根据资源的变化而变化着,同样浏览器第一次收到服务器返回的Etag值后,会把它存储起来,并下次访问该资源通过携带If--Match请求首部发送给服务器验证资源是否过期


Etag: "29322-09SpAhH3nXWd8KIVqB10hSSz66" 
If--Match: "29322-09SpAhH3nXWd8KIVqB10hSSz66"

如果两者不相同则代表服务器资源已经更新,服务器会返回该资源最新的Etag值。


强缓存


强缓存的具体流程如下:


image.png


上面我们介绍了expires设置的是绝对的时间,它会根据客户端的时间来判断,所以会造成expires不准确,如果我有一个资源缓存到到期时间是2024年4月31日我将客户端时间修改成过期的时间,则在一次访问该资产会重新请求服务器获取最新的数据。


max-age则是相对的时间,它的值是以秒为单位的时间,但是max-age也会不准确。


那么到底浏览器是怎么判断该资源的缓存是否有效的呢?这里就来介绍一下资源新鲜度的公式。


我们来用生活中的食品新鲜度来举例:


食品是否新鲜 = (生产日期 + 保质期) > 当前日期

那么缓存是否新鲜也可以借助这个公式来判断


缓存是否新鲜 = (创建时间 + expire || max-age) > 缓存使用期

这里的创建时间可以理解为服务器返回资源的时间,它和expires一样是一个绝对时间。


缓存使用期 = 响应使用期 + 传输延迟时间 + 停留缓存时间

响应使用期


响应使用期有两种获取方式:



  • max(0, responseTime - dateTime)

  • age


responseTime: 是指客户端收到响应的时间

dateTime: 是指服务器创建资源的时间

age:是响应头部的字段,通常是秒为单位


传输延迟时间


传输延迟的时间 = 客户端收到响应的时间 - 请求时间

停留时间


停留时间 = 当前客户端时间 - 客户端收到响应的时间

所以max-age也会失效的问题就是它也使用到了客户端的时间


协商缓存


协商缓存的具体流程如下:


image.png


从上文可以知道,协商缓存就是通过EtagLast-Modified这两个字段来判断。那么这个Etag的标识是如何生成的呢?


我们可以看node中etag第三方库。


该库会通过isState方法来判断文件的类型,如果是文件形式的话就会使用第一种方法:通过文件内容和修改时间来生成Etag


image.png


第二种方法:通过文件内容和hash值和内容长度来生成Etag


image.png


浏览器缓存


我们访问掘金的网站,查看Network可以看到有Size列有些没有大小的,而是disk cachememory cache这样的标识。


image.png


memory cache翻译就是内存缓存,顾名思义,它是存储在内存中的,优点就是速度非常快,可以看到Time列是0ms,缺点就是当网页关闭则缓存也就清空了,而且内存大小是非常有限的,如果要存储大量的资源的话还是使用磁盘缓存。


disk cache翻译就是磁盘缓存,它是存储在计算机磁盘中的一种缓存,它的优缺点和memory cache相反,它的读取是需要时间的,可以看到上方的图片Time列用了1ms的时间。


缓存获取顺序



  1. 浏览器会先查找内存缓存,如果存在则直接获取内存缓存中的资源

  2. 内存缓存没有,就回去磁盘缓存中查找,如果存在就返回磁盘缓存中的资源

  3. 磁盘缓存没有,那么就会进行网络请求,获取最新的资源然后存入到内存缓存或磁盘缓存


缓存存储优先级


浏览器是如何判断该资源要存储在内存缓存还是磁盘缓存的呢?


打开掘金网站可以看到,发现除了base64图片会从内存中获取,其它大部分资源会从磁盘中获取。


image.png


js文件是一个需要注意的地方,可以看到下面的有些js文件会被磁盘缓存有些则会被内存缓存,这是为什么呢?


image.png


Initiator列表示资源加载的位置,我们点击从内存获取资源的该列发现资源在HTML渲染阶段就被加载了,列入一下代码


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>DocumentÏ</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
</head>
<body>
<div id="root">这样加载的js资源大概率会存储到内存中</div>
</body>
</html>

而被内存抛弃的可以发现就是异步资源,这些资源不会被缓存到内存中。


上图我们可以看到有一个Initiator列的值是(index):50但是它还是被内存缓存了,我们可以点击进去看到他的代码如下:


image.png


这个js文件还是通过动态创建script标签来动态引入的。


Preload 与 Prefetch


PreloadPrefetch也会影响浏览器缓存的资源加载。


Preload称为预加载,用在link标签中,是指哪些资源需要页面加载完成后立刻需要的,浏览器会在渲染机制介入前加载这些资源。


<link rel="preload" href="//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/0358ea0.js" as="script">

当使用preload预加载资源时,这些资源一直会从磁盘缓存中读取。


prefetch表示预提取,告诉浏览器下一个页面可能会用到该资源,浏览器会利用空闲时间进行下载并存储到缓存中。


<link rel="prefretch" href="//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/0358ea0.js"Ï>

使用 prefetch 加载的资源,刷新页面时大概率会从磁盘缓存中读取,如果跳转到使用它的页面,则直接会从磁盘中加载该资源。


作者:sorryhc
来源:juejin.cn/post/7382891974942179354
收起阅读 »

Strapi让纯前端成为全栈不再是口号!🚀🚀🚀

web
序言很早以前就知道strap的存在,一直没有机会使用到。很早以前就想找一个类似strapi的框架,来帮我快速搭建后台服务。如果你只懂前端、那么它将非常适合你用来快速构建自己的api服务,以此实现全栈项目开发。strapi是什么?Strapi在国内鲜为人知,但它...
继续阅读 »

序言

很早以前就知道strap的存在,一直没有机会使用到。

很早以前就想找一个类似strapi的框架,来帮我快速搭建后台服务。

如果你只懂前端、那么它将非常适合你用来快速构建自己的api服务,以此实现全栈项目开发。

image.png

strapi是什么?

Strapi在国内鲜为人知,但它在国外的使用情况真的很Nice!

image.png 其仓库也是一直在维护、更新的。

Strapi 是一个开源的 Headless CMS(无头内容管理系统)。它允许开发者通过自定义的方式快速构建、管理和分发内容。Strapi 提供了一个强大的后端 API,支持 RESTful  GraphQL 两种方式,使得开发者可以方便地将内容分发到任何设备或服务,无论是网站、移动应用还是 IoT 设备。

Strapi 的主要特点包括:

  • 灵活性和可扩展性:通过自定义模型、API、插件等,Strapi 提供了极高的灵活性,可以满足各种业务需求。
  • 易于使用的 API:Strapi 提供了一个简洁、直观的 API,使得开发者可以轻松地与数据库进行交互。
  • 内容管理界面:Strapi 提供了一个易于使用的管理界面,使得用户可以轻松地创建、编辑和发布内容。
  • 多语言支持:Strapi 支持多种语言,包括中文、英语、法语、德语等。
  • 可扩展性:Strapi 具有高度的可扩展性,可以通过插件和自定义模块、插件来扩展其功能。
  • 社区支持:Strapi 拥有一个活跃的社区,提供了大量的文档、示例和插件,使得开发人员可以轻松地解决问题和扩展功能。

主要适用场景:

  • 多平台内容分发( 将内容分发到不同web、h5等不同平台 
  • 定制化 CMS 需求( 通过插件等扩展性高度定制 
  • 快速开发api(API管理界面能够大大加快开发速度,尤其是MVP(最小可行产品)阶段 

strapi实战

光看官网界面原官网地址),还是相当漂亮的🙈:

安装Strapi

超级简单,执行下面的命令后、坐等服务启动

(安装完后,自动执行了strapi start,其mysql、语言切换、权限配置等都内置到了@strapi包中)

yarn create strapi-app my-strapi --quickstart

浏览器访问:http://localhost:1337/admin/

第一步就完成了、是不是so easy😍,不过默认是英文的,虽然英语还凑合,但使用起来还是多有不便。strapi原本就支持国际化,我们来切换成中文再继续操作。

语言切换

  1. 设置国际化

  1. 个人设置中配置语言即可:

如果看不到"中文(简体)"选项,就在项目根目录下执行build并重启:npm run build && npm start,再刷新页面应该就能看到了。注意npm start默认是生产环境的启动(只能使用表,无法创建表)、开发环境启动用"npm run develop"

strapi的基础使用

在第一步完成的时候,其实数据库就已经搭建好了,我们只管建表、增加curd的接口即可

1. 建表

设置字段、可以选择需要的类型:

在保存左边的按钮可以继续添加字段

blog字段、建模完成后,进入内容管理器给表插入数据

2. curd

上面只是可视化的查看、插入数据,怎样才能变成api来进行curd了。

  • 设置API令牌,跟进提示操作

  • 权限说明

find GET请求 /api/blogs 查找所有数据

findone GET请求 /api/blogs/:id 查找单条数据

create POST请求 /api/blogs 创建数据

update PUT请求 /api/blogs/:id 更新数据

delete DELETE请求 /api/blogs/:id 删除数据

  • postman调试

先给blog公共权限,以便调试:

  1. 查找所有数据(find)

  1. 查找单条数据(findone)

  1. 更新修改数据(update)

  1. 删除数据(delete),返回被删除的数据

再次查看:

好了,恭喜你。 到这一步,你已经掌握了strapi curd的使用方法。


strapi数据可视化、Navicat辅助数据处理

Strapi 支持多种数据库,包括 MySQL、PostgreSQL、MongoDB 和 SQLite,并且具有高度的可扩展性和自定义性,可以满足不同项目的需求。(默认使用的是SQLite数据库)

我们也可以借助Navicat等第三个工具来实现可视化数据操作:

其用户名、密码默认都是strapi

strapi数据迁移

SQLite数据库

如果你只是需要将SQLite数据库从一个环境迁移到另一个环境(比如从一个服务器迁移到另一个服务器),操作相对简单:

  1. 备份SQLite数据库文件:找到你的SQLite数据库文件(默认位置是项目根目录下的 .tmp/data.db)并将其复制到安全的位置。
  2. 迁移文件:将备份的数据库文件移动到新环境的相同位置。
  3. 更新配置(如有必要) :如果新环境中数据库文件的位置有变化,确保更新Strapi的数据库配置文件(./config/database.js)以反映新的文件路径。

SQLite到其他数据库系统

如果你需要将SQLite数据库迁移到其他类型的数据库系统,比如PostgreSQL或MySQL,流程会更复杂一些:

  1. 导出SQLite数据:首先,你需要导出SQLite数据库中的数据。这可以通过多种工具完成,例如使用sqlite3命令行工具或一个图形界面工具(如DB Browser for SQLite)来导出数据为SQL文件。
  2. 准备目标数据库:在目标数据库系统中创建一个新的数据库,为Strapi项目准备使用。
  3. 修改Strapi的数据库配置:根据目标数据库类型,修改Strapi的数据库配置文件(./config/database.js)。你需要根据目标数据库系统的要求配置连接参数。
  4. 导入数据到目标数据库:使用目标数据库系统的工具导入之前导出的数据。不同数据库系统的导入工具和命令会有所不同。例如,对于PostgreSQL,你可能会使用psql工具,对于MySQL,则可能使用mysql命令行工具。
  5. 处理数据类型和结构差异:不同的数据库系统在数据类型和结构上可能会有所差异。在导入过程中,你可能需要手动调整SQL文件或在导入后调整数据库结构,尤其是对于关系和外键约束。
  6. 测试:迁移完成后,彻底测试你的Strapi项目,确保数据正确无误,所有功能正常工作。

注意事项

  • 数据兼容性:在不同数据库系统之间迁移时,可能会遇到数据类型不兼容的问题,需要仔细处理。
  • 性能调优:迁移到新的数据库系统后,可能需要根据新的数据库特性进行调优以确保性能。
  • 备份:在进行任何迁移操作之前,总是确保已经备份了所有数据和配置。

具体步骤可能会因你的具体需求和所使用的数据库系统而异。根据你的目标数据库系统,可能有特定的迁移工具和服务可以帮助简化迁移过程。

小结

好了,梳理一下。现在我要建一套博客系统的API该怎么做了?

  1. 安装启动(已安装可忽略)yarn create strapi-app my-strapi --quickstart
  2. 在后台建表建模、设置字段
  3. 设置表的API调用权限
  4. 在需要用到的地方使用即可
    是不是超级简单了!

总结

上面我们了解了strapi的后台使用、curd操作、数据迁移等。相信大家都能快速掌握使用。我们无需基于ORM框架去搭建数据模型,也无需使用python、nestjs等后台框架去创建后台服务了。 这势必能大大提升我们的开发效率。

后续会再继续讲解strapi富文本插件的使用,有了富文本的加持、我们都能省去搭建管理后台了,如果用来做博客、高度定制化的文档系统将是非常不错的选择。


作者:tager
来源:juejin.cn/post/7340152660224819212

收起阅读 »

未登录也能知道你是谁?浏览器指纹了解一下!

web
引言 大多数人都遇到过这种场景,我在某个网站上浏览过的信息,但我并未登录,可是到了另一个网站发现被推送了类似的广告,这是为什么呢? 本文将介绍一种浏览器指纹的概念,以及如何利用它来判断浏览者身份。 浏览器指纹 浏览器指纹是指通过浏览器的特征来唯一标识用户身份的...
继续阅读 »

引言


大多数人都遇到过这种场景,我在某个网站上浏览过的信息,但我并未登录,可是到了另一个网站发现被推送了类似的广告,这是为什么呢?


本文将介绍一种浏览器指纹的概念,以及如何利用它来判断浏览者身份。


浏览器指纹


浏览器指纹是指通过浏览器的特征来唯一标识用户身份的一种技术。


它通过记录用户浏览器的一些基本信息,包括操作系统、浏览器类型、浏览器版本、屏幕分辨率、字体、颜色深度、插件、时间戳等,通过这些信息,可以唯一标识用户身份。


应用场景


其实浏览器指纹这类的技术已经被运用的很广泛了,通常都是用在一些网站用途上,比如:



  • 资讯等网站:精准推送一些你感兴趣的资讯给你看

  • 购物网站: 精确推送一些你近期浏览量比较多的商品展示给你看

  • 广告投放: 有一些网站是会有根据你的喜好,去投放不同的广告给你看的,大家在一些网站上经常会看到广告投放吧?

  • 网站防刷: 有了浏览器指纹,就可以防止一些恶意用户的恶意刷浏览量,因为后端可以通过浏览器指纹认得这些恶意用户,所以可以防止这些用户的恶意行为

  • 网站统计: 通过浏览器指纹,网站可以统计用户的访问信息,比如用户的地理位置、访问时间、访问频率等,从而更好的为用户提供服务


如何获取浏览器指纹


指纹算法有很多,这里介绍一个网站 https://browserleaks.com/ 上面介绍了很多种指纹,可以根据自己的需要选择。



这里我们看一看canvas,可以看到光靠一个canvas的信息区分,就可以做到15万用户只有7个是重复的,如果结合其他信息,那么就可以做到更精准的识别。


canvas指纹


canvas指纹的原理就是通过 canvas 生成一张图片,然后将图片的像素点信息记录下来,作为指纹信息。


不同的浏览器、操作系统、cpu、显卡等等,画出来的 canvas 是不一样的,甚至可能是唯一的。


具体步骤如下:



  1. 用canvas 绘制一个图像,在画布上渲染图像的方式可能因web浏览器、操作系统、图形卡和其他因素而异,从而生成可用于创建指纹的唯一图像。在画布上呈现文本的方式也可能因不同web浏览器和操作系统使用的字体渲染设置和抗锯齿算法而异。




  1. 要从画布生成签名,我们需要通过调用toDataURL() 函数从应用程序的内存中提取像素。此函数返回表示二进制图像文件的 base64 编码字符串。然后,我们可以计算该字符串的MD5哈希来获得画布指纹。或者,我们可以从IDAT块中提取 CRC校验和IDAT块 位于每个 PNG 文件末尾的16到12个字节处,并将其用作画布指纹。


我们来看看结果,可以知道,无论是否在无痕模式下,都可以生成相同的 canvas 指纹。
在这里插入图片描述


换台设备试试



其他浏览器指纹


除了canvas,还有很多其他的浏览器指纹,比如:


WebGL 指纹


WebGL(Web图形库)是一个 JavaScript API,可在任何兼容的 Web 浏览器中渲染高性能的交互式 3D2D 图形,而无需使用插件。


WebGL 通过引入一个与 OpenGL ES 2.0 非常一致的 API 来做到这一点,该 API 可以在 HTML5 元素中使用。


这种一致性使 API 可以利用用户设备提供的硬件图形加速。


网站可以利用 WebGL 来识别设备指纹,一般可以用两种方式来做到指纹生产:


WebGL 报告——完整的 WebGL 浏览器报告表是可获取、可被检测的。在一些情况下,它会被转换成为哈希值以便更快地进行分析。


WebGL 图像 ——渲染和转换为哈希值的隐藏 3D 图像。由于最终结果取决于进行计算的硬件设备,因此此方法会为设备及其驱动程序的不同组合生成唯一值。这种方式为不同的设备组合和驱动程序生成了唯一值。


可以通过 Browserleaks test 检测网站来查看网站可以通过该 API 获取哪些信息。


产生 WebGL 指纹原理是首先需要用着色器(shaders)绘制一个梯度对象,并将这个图片转换为Base64 字符串。


然后枚举 WebGL 所有的拓展和功能,并将他们添加到 Base64 字符串上,从而产生一个巨大的字符串,这个字符串在每台设备上可能是非常独特的。


例如 fingerprint2js 库的 WebGL 指纹生产方式:


HTTP标头


每当浏览器向服务器发送请求时,它会附带一个HTTP标头,其中包含了诸如浏览器类型、操作系统、语言偏好等信息。


这些信息可以帮助网站优化用户体验,但同时也能用来识别和追踪用户。


屏幕分辨率


屏幕分辨率指的是浏览器窗口的大小和设备屏幕的能力,这个参数因用户设备的不同而有所差异,为浏览器指纹提供了又一个独特的数据点。


时区


用户设备的本地时间和日期设置可以透露其地理位置信息,这对于需要提供地区特定内容的服务来说是很有价值的。


浏览器插件


用户安装的插件列表是非常独特的,可以帮助形成识别个体的浏览器指纹。


音频和视频指纹


通过分析浏览器处理音频和视频的方式,网站可以获取关于用户设备音频和视频硬件的信息,这也可以用来构建用户的浏览器指纹。


webgl指纹案例


那么如何防止浏览器指纹呢?


先讲结论,成本比较高,一般人不会使用。


现在开始实践,根据上述的原理,我们知道了如何生成一个浏览器指纹,我们只需要它在获取toDataURL时,修改其中的内容,那么结果就回产生差异,从而无法通过浏览器指纹进行识别。


那么,我们如何修改toDataURL的内容呢?


我们不知道它会在哪里调用,所以我们只能通过修改它的原型链来修改。


又或者使用专门的指纹浏览,该浏览器可以随意切换js版本等信息来造成无序的随机值。


修改 toDataURL


第三方指纹库


FingerprintJS



FingerprintJS是一个源代码可用的客户端浏览器指纹库,用于查询浏览器属性并从中计算散列访问者标识符。


cookie和本地存储不同,指纹在匿名/私人模式下保持不变,即使浏览器数据被清除。


ClientJS Library



ClientJS 是另一个常用的JavaScript库,它通过检测浏览器的多个属性来生成指纹。


该库提供了易于使用的接口,适用于多种浏览器指纹应用场景。




作者:我码玄黄
来源:juejin.cn/post/7382344353069088803
收起阅读 »

h5 如何跳转微信小程序(uni-app)?

web
前几天组员遇到h5跳转微信小程序的功能,由于时间关系,我这边接手实现这个功能。 遇到问题点 微信内部跳转怎么实现? 外部浏览器或者app打开链接如何跳转? 微信内部跳转怎么实现 使用 wx-open-launch-weapp 这边标签进行跳转,使用这个标签...
继续阅读 »

前几天组员遇到h5跳转微信小程序的功能,由于时间关系,我这边接手实现这个功能。


遇到问题点



  • 微信内部跳转怎么实现?

  • 外部浏览器或者app打开链接如何跳转?


微信内部跳转怎么实现

使用 wx-open-launch-weapp 这边标签进行跳转,使用这个标签跳转小程序,需要满足3个条件



  • 首先 wx.config 授权,公众号设置 > 功能设置 > js安全域名设置

  • 域名设置,静态资源托管

  • 小程序需要关联这个公众号 设置 > 基本设置 > 相关公众号


h5上代码源码



<html>
<head>
<title>打开小程序</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<script>
window.onerror = e => {
console.error(e)
alert('发生错误' + e)
}
</script>
<!-- 引入jQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<!-- weui 样式 -->
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.4.1/weui.min.css"></link>
<!-- 调试用的移动端 console -->
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
<!-- 公众号 JSSDK -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- 云开发 Web SDK -->
<script src="https://res.wx.qq.com/open/js/cloudbase/1.1.0/cloud.js"></script>
<script>
const baseUrl ="/pages/biddingCenter/index"
function getQueryVariable(variable)
{
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}

function docReady(fn) {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
fn()
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
var version = 'trial'
var fetchData = new Promise((resolve, reject) => {
// 获取签名,timestamp、nonceStr、signature
$.ajax({
url: "xxxxxxx",
dataType: "json",
method: 'get',
data: { xxx },

success: function (res) {
console.log("WeChatConfig", res);
if (res.data) {
var data = res.data; // 根据实际情况返还的数据进行赋值
console.log(999, data)
resolve(data);
}
},
error: function (error) {
console.error('error-->', error)
return reject(error)
}
})
});



docReady(async function() {
var ua = navigator.userAgent.toLowerCase()
var isWXWork = ua.match(/wxwork/i) == 'wxwork'
var isWeixin = !isWXWork && ua.match(/MicroMessenger/i) == 'micromessenger'
var isMobile = false
var isDesktop = false
if (navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|IEMobile)/i)) {
isMobile = true
} else {
isDesktop = true
}
console.warn('ua', ua)
console.warn(ua.match(/MicroMessenger/i) == 'micromessenger')
var m = ua.match(/MicroMessenger/i)
console.warn(m && m[0] === 'micromessenger')

if (isWeixin) {
var containerEl = document.getElementById('wechat-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'wechat-web-container')

var launchBtn = document.getElementById('launch-btn')
launchBtn.addEventListener('ready', function (e) {

launchBtn.setAttribute('env-version',version)
launchBtn.setAttribute('path', baseUrl)

console.log('开放标签 ready')
})
launchBtn.addEventListener('launch', function (e) {
console.log('开放标签 success')
})
launchBtn.addEventListener('error', function (e) {
console.log('开放标签 fail', e.detail)
})

await fetchData.then(res=> {
wx.config({
// debug: true, // 调试时可开启
appId: res.appId,
timestamp: res.timestamp, // 必填,填任意数字即可
nonceStr: res.nonceStr, // 必填,填任意非空字符串即可
signature: res.signature, // 必填,填任意非空字符串即可
jsApiList: ['chooseImage'], // 安卓上必填一个,随机即可
openTagList:['wx-open-launch-weapp'], // 填入打开小程序的开放标签名
})
})

} else if (isDesktop) {
// 在 pc 上则给提示引导到手机端打开
var containerEl = document.getElementById('desktop-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'desktop-web-container')
} else {

var containerEl = document.getElementById('public-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'public-web-container')
var c = new cloud.Cloud({
// 必填,表示是未登录模式
identityless: true,
// 资源方 AppID
resourceAppid: 'wxaabe7e5d652fc1d8',
// 资源方环境 ID
resourceEnv: 'cloud1-7gnlxfema33074ec',
})
await c.init()
console.log(c)
window.c = c

var buttonEl = document.getElementById('public-web-jump-button')
var buttonLoadingEl = document.getElementById('public-web-jump-button-loading')
try {
await openWeapp(() => {
console.log('dui')
buttonEl.classList.remove('weui-btn_loading')
buttonLoadingEl.classList.add('hidden')
})
} catch (e) {
console.log(e,'cuo')
buttonEl.classList.remove('weui-btn_loading')
buttonLoadingEl.classList.add('hidden')
throw e
}
}
})

async function openWeapp(onBeforeJump) {
var c = window.c
const res = await c.callFunction({
name: 'public',
data: {
action: 'getUrlScheme',

},
})

if (onBeforeJump) {
onBeforeJump()
}
location.href = res.result.openlink
}
</script>
<style>
.hidden {
display: none;
}

.full {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}

.public-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.public-web-container p {
position: absolute;
top: 40%;
}

.public-web-container a {
position: absolute;
bottom: 40%;
}

.wechat-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.wechat-web-container p {
position: absolute;
top: 40%;
}

.wechat-web-container wx-open-launch-weapp {
position: absolute;
bottom: 40%;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
}

.desktop-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.desktop-web-container p {
position: absolute;
top: 40%;
}
</style>
</head>
<body>
<div class="page full">
<div id="public-web-container" class="hidden">
<p class="">正在打开 “ 小程序”...</p>
<a id="public-web-jump-button" href="javascript:" class="weui-btn weui-btn_primary weui-btn_loading" onclick="openWeapp()">
<span id="public-web-jump-button-loading" class="weui-primary-loading weui-primary-loading_transparent"><i class="weui-primary-loading__dot"></i></span>
小程序
</a>
</div>
<div id="wechat-web-container" class="hidden">
<p class="">点击以下按钮打开 “小程序”</p>
<!-- 跳转小程序的开放标签。文档 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html -->
<wx-open-launch-weapp id="launch-btn" username="gh_36a8dca79910" path="/pages/biddingCenter/index" env-version="trial">
<template>
<button style="width: 200px; height: 45px; text-align: center; font-size: 17px; display: block; margin: 0 auto; padding: 8px 24px; border: none; border-radius: 4px; background-color: #07c160; color:#fff;">打开小程序</button>
</template>
</wx-open-launch-weapp>
</div>
<div id="desktop-web-container" class="hidden">
<p class="">请在手机打开网页链接</p>
</div>
</div>
</body>
</html>

以上第二点后面会讲到
以上功能满足基本可以在微信内部打开小程序了,但是实际上,我们url可能是其他app,短信,或者浏览器里面打开的


外部app或者短信跳转怎么实现

首先我这边是uni-app实现的



  • 第一步在项目根目录项目新建functions这个文件夹,然后到


image.png
点击下载放入刚刚的functions下面,然后再 manifest.json 下面


 "mp-weixin" : {
"appid" : "xxxxx",
"cloudfunctionRoot": "./functions/", // 这一行就是标记云函数目录的字段
"setting" : {
"urlCheck" : false
},
"usingComponents" : true,
"permission" : {}
}

这个时候编译项目在编译之后的小程序项目是没有functions 这个文件夹的,我们这个时候需要在vue.config.js


const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, 'functions'),
to: path.join(__dirname, 'unpackage/dist', process.env.NODE_ENV === 'production' ? 'build' : 'dev', process.env.UNI_PLATFORM, 'functions')
}
]
})
]
}
}

在终端上面npm install 才行,需要node环境
设置以上代码重新启动编译,在微信工具里面就会出现


image.png


image.png


右击public上传并部署(云端安装依赖),成功之后打开微信开发工具云开发


image.png


image.png


会出现刚刚的云函数文件
然后再次点击云函数权限


image.png
点击所有用户访问,保存


image.png


然后再点击右上角设置>权限设置> 未登录用户访问云资源权限设置打开


image.png


以上搞定也只是函数环境搞定,然后还要配置刚刚的函数方法api


image.png


只有配置这个才能在index方法里面调用


// 云函数入口文件
const cloud = require('wx-server-sdk')

cloud.init()

// 云函数入口函数
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()

switch (event.action) {
case 'getUrlScheme': {
return getUrlScheme(event)
}
}

return 'action not found'
}

async function getUrlScheme(event) {

return cloud.openapi.urlscheme.generate({
jumpWxa: {
path:'/pages/index/index', // <!-- replace -->
env_version:'trial',
},
// 如果想不过期则置为 false,并可以存到数据库
isExpire: false,
// 一分钟有效期
expireTime: parseInt(Date.now() / 1000 + 60),
})
}


代码讲到这边会发现之前h5里面调用的 c.callFunction 方法其实调用的就是exports.main 这个导出得方法
然后还需要把h5代码放入


image.png


image.png
以上就是整个h5跳转微信小程序的过程,需要注意的点事在h5里面 方法返回 res.result.openlink 这个参数需要跳转的url在生产上线过,要不然报错 openapi.urlscheme.generate pagepath 获取不到


作者:成书平
来源:juejin.cn/post/7264772939773526075
收起阅读 »

【小程序分包】小程序包大于2M,来这教你分包啊

web
前言🍊缘由该大的不大,小程序包超出2M,无法上传发布前段时间项目迭代时,因版本大升级,导致uniapp打包后小程序后,包体积大于2M。虽然将图片等静态资源压缩,体积大的资源放置cdn,在不懈的努力下,治标不治本,包体积还是不听话的长到2M以上。憋的实在没办法,...
继续阅读 »

前言

🍊缘由

该大的不大,小程序包超出2M,无法上传发布

前段时间项目迭代时,因版本大升级,导致uniapp打包后小程序后,包体积大于2M。虽然将图片等静态资源压缩,体积大的资源放置cdn,在不懈的努力下,治标不治本,包体积还是不听话的长到2M以上。憋的实在没办法,遂将小程序分包,彻底解除封印,特来跟大家分享下如何将小程序分包,减小主包大小


🎯主要目标

实现2大重点

  1. 如何进行小程序分包
  2. 如个根据分包调整配置文件

🍈猜你想问

如何与狗哥联系进行探讨

关注公众号【JavaDog程序狗】

公众号回复【入群】或者【加入】,便可成为【程序员学习交流摸鱼群】的一员,问题随便问,牛逼随便吹。

此群优势:

  1. 技术交流随时沟通
  2. 任何私活资源免费分享
  3. 实时科技动态抢先知晓
  4. CSDN资源免费下载
  5. 本人一切源码均群内开源,可免费使用

2.踩踩狗哥博客

javadog.net

大家可以在里面留言,随意发挥,有问必答


🍯猜你喜欢

文章推荐

正文

🍵三个问题

  1. 为什么小程序会有2M的限制?
  1. 用户体验:小程序要求在用户进入小程序前能够快速加载,以提供良好的用户体验。限制小程序的体积可以确保小程序能够在较短的时间内下载和启动,避免用户长时间的等待。
  2. 网络条件:考虑到不同地区和网络条件的差异,限制小程序的体积可以确保在低速网络环境下也能够较快地加载和打开小程序,提供更广泛的用户覆盖。
  3. 设备存储:一些用户使用的设备可能存储空间有限,限制小程序的体积可以确保小程序可以在这些设备上正常安装和运行。
  1. 如何解决包过大问题?
  1. 优化代码,删除掉不用的代码
  2. 图片压缩或者上传服务器
  3. 分包加载
  1. 什么是分包加载?

小程序一般都是由某几个功能组成,通常这几个功能之间是独立的,但会依赖一些公共的逻辑,且这些功能一般会对应某几个独立的页面。那么小程序代码的打包,可以按照功能的划分,拆分成几个分包,当需要用到某个功能时,才加载这个功能对应的分包。


🧀实操分包步骤

1.查看项目结构

通过上方三个问题,我们开始具体分包流程,首先看一下分包前项目结构pages.json配置文件

pages.json
{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/card/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/device/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/order/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/product/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

2.分析主包大小

微信开发者工具中,查看【详情】进行分析,此处本地代码只有一个主包大小399.8KB

3.参考文档

本文以uniapp为实操介绍案例

小程序官方文档:

developers.weixin.qq.com/miniprogram…

uniapp 分包文档:

uniapp.dcloud.net.cn/collocation…

4. 结构调整

将咱们项目结构按照如下图所示进行拆分

新建subPages_A 和 subPages_B,将pages下不同页面移入进新增的两个包,此处subPages_A的名字只做示例,实际要按照标准命名!

比较下之前项目结构,此处项目会报错,不用担心,稍后修改pages.json


5. 修改pages.json

根据上一步拆分的包路径,进行配置文件的调整,此处注意"subPackages" 要和 "pages" 同级

{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
],
"subPackages": [{
"root": "pages/subPages_A",
"pages": [{
"path": "card/index",
"style": {
"navigationBarTitleText": "card"
}
},
{
"path": "device/index",
"style": {
"navigationBarTitleText": "device"
}
}
]
}, {
"root": "pages/subPages_B",
"pages": [{
"path": "order/index",
"style": {
"navigationBarTitleText": "order"
}
},
{
"path": "product/index",
"style": {
"navigationBarTitleText": "product"
}
}
]
}],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

这里的意思是将主包拆成subPages_A和subPages_B两个子包,对比下之前的配置


6. 启动测试

启动后查看微信开发者工具,查看【详情】可看到主包大小降为326.0kb,并且下方还有subPages_A和subPages_B两个子包

比较之前包大小,分包成功!


7. 特别注意

🎯 如果设计代码中路径问题,需要调成最新包结构路径。例如

拆包前跳转到对应设备页面

uni.navigateTo({
url:'/pages/device/index'
})

拆包后跳转到对应设备页面

uni.navigateTo({
url:'/pages/subPages_A/device/index'
})

切记如果拆包后所有路径问题需要统一修改,否则则会报错!!!


总结

本文通过实际demo进行uniapp小程序拆包,通过分析项目主包大小,查看官方文档,按照功能划分进行子包拆分,如果还有博友存在疑问或者不理解可以在上方与本狗联系,或者查看本狗发布在上方的代码,希望可以帮到大家。



作者:JavaDog程序狗
来源:juejin.cn/post/7270445774324367394

收起阅读 »

产品经理:优惠金额只入不舍,支付金额只舍不入...

web
前言 当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。 优惠券分为:折扣券(n折)、抵扣券(减x元) 需求 优惠金额、支付金额都需要保留两位小数。 优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些 支付金...
继续阅读 »

前言


当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。

优惠券分为:折扣券(n折)、抵扣券(减x元)


需求


优惠金额、支付金额都需要保留两位小数。

优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些

支付金额只舍不入,比如18.888元,显示为:18.88元。

从产品角度来讲,这个设计相当人性化。


实现


/**
* 金额计算
* @param {number} a sku原始价格
* @param {number} b 优惠券金额/折扣
* @param {string} mathFunc 四舍五入:round/ceil/floor
* @param {string} type 计算方式默认减法
* @param {digits} type 精度,默认两位小数
* */

export function numbleCalc(a, b,mathFunc='round', type = '-',digits=2) {

var getDecimalLen = num =>{
return num.toString().split('.')[1] ? num.toString().split('.')[1].length : 0;
}
//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}


var c;
//获取2个数字中,最长的小数位数
var aLen = getDecimalLen(a);
var bLen = getDecimalLen(b);
var decimalLen = aLen>bLen?aLen:bLen;
var mul = decimalLen>0?(10 ** decimalLen):1;

//转换成整数
var aInteger = floatToInt(a,aLen,decimalLen)
var bInteger = floatToInt(b,bLen,decimalLen)


if(type=='-'){
c = (aInteger - bInteger)/mul;
}else if(type=='*'){
c = aInteger * bInteger/mul/mul;
}

c = digits==0?c : Math.round(c * (10 ** digits));

if(mathFunc=='floor'){
c= Math.floor(c);
}else if(mathFunc=='ceil'){
c= Math.ceil(c);
}else {
c= Math.round(c);
}
return digits==0?c : c/(10**digits);
}



整体思路:获取两个数字之间最大的小数位,先取整再计算。
不直接进行计算,是因为存在0.1+0.2!=0.3的情况,具体原因可以看下文章下方的参考链接,写的很详细。




  • Math.ceil()  总是向上舍入,并返回大于等于给定数字的最小整数。

  • Math.floor()  函数总是返回小于等于一个给定数字的最大整数。

  • Math.round() 四舍五入


【重点】小数位取整:我之前的写法原来是错误的


image.png
我一直以来也是这种形式,预想的是直接乘100变成整数,但是出现了以下情况


19.9 * 100 = 1989.9999999999998
5.02 * 100 = 501.99999999999994

可以看到,出现了意料之外的结果!!

最后采用的方案是:将小数转成字符串,再将小数点替换成空格


//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}

总结


省流:将小数点替换成空格,变成整数,再进行相应计算。

封装的这个函数,只考虑了当前业务场景,未兼容一些边界值情况。



  • 大金额计算问题

  • 计算方式:加法、除法未做处理


参考


# 前端金额运算精度丢失问题及解决方案


作者:前端大明
来源:juejin.cn/post/7341210909069770792
收起阅读 »

如何使用Electron集成环信UIKIT

写在前面环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库,本篇文章介绍如何在Electron中如何集成UIKit,采用框架Electron-vite-react--- 准备工作1.已经在环信即时...
继续阅读 »

写在前面
环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库,本篇文章介绍如何在Electron中如何集成UIKit,采用框架Electron-vite-react


---
 准备工作
1.已经在环信即时通讯云控制台创建了有效的环信即时通讯 IM 开发者账号,并获取了App Key

2.了解并可以创建Electron-vite-react项目

3.了解UIkit各功能以及api调用


---

开始集成
第一步:创建一个Electron项目进度10%
Electron-vite官网有详细的教程,此处不做过多赘述,仅以当前示例项目为参考集成,更多详情指路官网

yarn create @quick-start/electron electronReact --template react

第二步:安装依赖进度15%

yarn install

第三步:启动项目进度20%

yarn run dev

到这一步我们可以得到下图


你的目录结构如下图


第四步:安装UIKit进度50%

下载easemob-chat-uikit
使用 npm 安装 easemob-chat-uikit 包

npm install easemob-chat-uikit --save

使用 yarn 安装 easemob-chat-uikit 包

yarn add easemob-chat-uikit

第五步:引入UIKit组件进度80%

1、删除App.tsx自带的内容,在App.tsx中引入UIKit组件

import {
Provider as UIKitProvider,
Chat,
ConversationList,
useClient
} from 'easemob-chat-uikit'
import 'easemob-chat-uikit/style.css'
import { useEffect } from 'react'
import './App.css'
const ChatApp = () => {
const client = useClient()
useEffect(() => {
client &&
client
.open({
user: 'userId',
pwd: 'pwd'
})
.then((res) => {
console.log('get token success', res)
})
}, [client])
return (
<div className="app_container">
<div className="conversation_container">
<ConversationList />
</div>
<div className="chat_container">
<Chat />
</div>
</div>
)
}
function App(): JSX.Element {
return (
<UIKitProvider
initConfig={{
appKey: 'your app key'
}}
>
<ChatApp />
</UIKitProvider>
)
}

export default App


2、将src/renderer/src/assets/main.css中的css样式全部替换如下
body {
display: flex;
flex-direction: column;
font-family:
Roboto,
-apple-system,
BlinkMacSystemFont,
'Helvetica Neue',
'Segoe UI',
'Oxygen',
'Ubuntu',
'Cantarell',
'Open Sans',
sans-serif;
color: #86a5b1;
background-color: #2f3241;
}

* {
padding: 0;
margin: 0;
}

ul {
list-style: none;
}

code {
font-weight: 600;
padding: 3px 5px;
border-radius: 2px;
background-color: #26282e;
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 85%;
}

a {
color: #9feaf9;
font-weight: 600;
cursor: pointer;
text-decoration: none;
outline: none;
}

a:hover {
border-bottom: 1px solid;
}

.container {
flex: 1;
display: flex;
flex-direction: column;
max-width: 840px;
margin: 0 auto;
padding: 15px 30px 0 30px;
}

.versions {
margin: 0 auto;
float: none;
clear: both;
overflow: hidden;
font-family: 'Menlo', 'Lucida Console', monospace;
color: #c2f5ff;
line-height: 1;
transition: all 0.3s;
}

.versions li {
display: block;
float: left;
border-right: 1px solid rgba(194, 245, 255, 0.4);
padding: 0 20px;
font-size: 13px;
opacity: 0.8;
}

.versions li:last-child {
border: none;
}

.hero-logo {
margin-top: -0.4rem;
transition: all 0.3s;
}

@media (max-width: 840px) {
.versions {
display: none;
}

.hero-logo {
margin-top: -1.5rem;
}
}

.hero-text {
font-weight: 400;
color: #c2f5ff;
text-align: center;
margin-top: -0.5rem;
margin-bottom: 10px;
}

@media (max-width: 660px) {
.hero-logo {
display: none;
}

.hero-text {
margin-top: 20px;
}
}

.hero-tagline {
text-align: center;
margin-bottom: 14px;
}

.links {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
font-size: 18px;
font-weight: 500;
}

.links a {
font-weight: 500;
}

.links .link-item {
padding: 0 4px;
}

.features {
display: flex;
flex-wrap: wrap;
margin: -6px;
}

.features .feature-item {
width: 33.33%;
box-sizing: border-box;
padding: 6px;
}

.features article {
background-color: rgba(194, 245, 255, 0.1);
border-radius: 8px;
box-sizing: border-box;
padding: 12px;
height: 100%;
}

.features span {
color: #d4e8ef;
word-break: break-all;
}

.features .title {
font-size: 17px;
font-weight: 500;
color: #c2f5ff;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.features .detail {
font-size: 14px;
font-weight: 500;
line-height: 22px;
margin-top: 6px;
}

@media (max-width: 660px) {
.features .feature-item {
width: 50%;
}
}

@media (max-width: 480px) {
.links {
flex-direction: column;
line-height: 32px;
}

.links .link-dot {
display: none;
}

.features .feature-item {
width: 100%;
}
}


3、在src/renderer/src目录下添加App.css
.app_container {
width: calc(100%);
height: 100vh;
display: flex;
}
.conversation_container {
width: 30%;
}
.chat_container {
width: 70%;
}


到这一步你可以得到如下图



第六步:解决问题`进度99%`

在第五步执行完毕之后发现调试器有如下图报错


经查阅资料,发现是Electron内容安全策略在搞鬼,并提供了解决方案


接下来我们就需要在src/renderer/index.html中更改meta标签
同样out/renderer/index.html也需要更改meta标签

<meta
http-equiv="Content-Security-Policy"
content="font-src 'self' data:; img-src 'self' data:; default-src 'self'; connect-src * ws://* wss://*; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"
/>


接下来保存代码并运行你将得到下图


第七步:发送消息进度100%
点击好友并发送一条消息,如下图

恭喜你集成完毕~
总结:
通过以上步骤,你已经成功在Electron中集成了环信单聊 UIKit 并实现了基本的即时通讯功能,接下来继续根据 UIKit 提供的组件和 API 文档进行进一步开发吧

收起阅读 »

写了一个字典hook,瞬间让组员开发效率提高20%!!!

web
1、引言 在项目的开发中,发现有很多地方(如:选择器、数据回显等)都需要用到字典数据,而且一个页面通常需要请求多个字典接口,如果每次打开同一个页面都需要重新去请求相同的数据,不但浪费网络资源、也给开发人员造成一定的工作负担。最近在用 taro + react ...
继续阅读 »

1、引言


在项目的开发中,发现有很多地方(如:选择器、数据回显等)都需要用到字典数据,而且一个页面通常需要请求多个字典接口,如果每次打开同一个页面都需要重新去请求相同的数据,不但浪费网络资源、也给开发人员造成一定的工作负担。最近在用 taro + react 开发一个小程序,所以就写一个字典 hook 方便大家开发。


2、实现过程


首先,字典接口返回的数据类型如下图所示:
image.png


其次,在没有实现字典 hook 之前,是这样使用 选择器 组件的:


  const [unitOptions, setUnitOptions] = useState([])

useEffect(() => {
dictAppGetOptionsList(['DEV_TYPE']).then((res: any) => {
let _data = res.rows.map(item => {
return {
label: item.fullName,
value: item.id
}
})
setUnitOptions(_data)
})
}, [])

const popup = (
<PickSelect
defaultValue=""
open={unitOpen}
options={unitOptions}
onCancel={() =>
setUnitOpen(false)}
onClose={() => setUnitOpen(false)}
/>

)

每次都需要在页面组件中请求到字典数据提供给 PickSelect 组件的 options 属性,如果有多个 PickSelect 组件,那就需要请求多次接口,非常麻烦!!!!!


既然字典接口返回的数据格式是一样的,那能不能写一个 hook 接收不同属性,返回不同字典数据呢,而且还能 缓存 请求过的字典数据?


当然是可以的!!!


预想一下如何使用这个字典 hook?


const { list } = useDictionary('DEV_TYPE')

const { label } = useDictionary('DEV_TYPE', 1)

const { label } = useDictionary('DEV_TYPE', 1, '、')

从上面代码中可以看到,第一个参数接收字典名称,第二个参数接收字典对应的值,第三个参数接收分隔符,而且后两个参数是可选的,因此根据上面的用法来写我们的字典 hook 代码。


interface dictOptionsProps {
label: string | number;
value: string | number | boolean | object;
disabled?: boolean;
}

interface DictResponse {
value: string;
list: dictOptionsProps[];
getDictValue: (value: string) => string
}

let timer = null;
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {}; // 字典缓存

// 因为接收不同参数,很适合用函数重载
function useDictionary(type: string): DictResponse;
function useDictionary(
type: string | dictOptionsProps[],
value: number | string | Array<number | string>,
separator?: string
): DictResponse;
function useDictionary(
type: string | dictOptionsProps[],
value?: number | string | Array<string | number>,
separator = ","
): DictResponse {
const [options, setOptions] = useState<dictOptionsProps[]>([]); // 字典数组
const [dictValue, setDictValue] = useState(""); // 字典对应值

const init = () => {
if (!dict[type] || !dict[type].length) {
dict[type] = [];

types.push(type);

// 当多次调用hook时,获取所有参数,合成数组,再去请求,这样可以避免多次调用接口。
timer && clearTimeout(timer);
timer = setTimeout(() => {
dictAppGetOptionsList(types.slice()).then((res) => {
for (const key in dictRes.data) {
const dictList = dictRes.data[key].map((v) => ({
label: v.description,
value: v.subtype,
}));
dict[type] = dictList
setOptions(dictList) // 注意这里会有bug,后面有说明的
}
});
}, 10);
} else {
typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
}
};

// 获取字典对应值的中文名称
const getLabel = useCallback(
(value) => {
if (value === undefined || value === null || !options.length) return "";
const values = Array.isArray(value) ? value : [value];

const items = values.map((v) => {
if (typeof v === "number") v = v.toString();
return options.find((item) => item.value === v) || { label: value };
});
return items.map((v) => v.label).join(separator);
},
[options]
)

useEffect(() => init(), [])
useEffect(() => setDictValue(getLabel(value)), [options, value])

return { value: dictValue, list: options, getDictValue: getLabel };
}

初步的字典hook已经开发完成,在 Input 组件中添加 dict 属性,去试试看效果如何。


export interface IProps extends taroInputProps {
value?: any;
dict?: string; // 字典名称
}

const CnInput = ({
dict,
value,
...props
}: IProps
) => {
const { value: _value } = dict ? useDictionary(dict, value) : { value };

return <Input value={_value} {...props} />
}

添加完成,然后去调用 Input 组件


<CnInput
readonly
dict="DEV_ACCES_TYPE"
value={formData?.accesType}
/>

<CnInput
readonly
dict="DEV_SOURCE"
value={formData?.devSource}
/>


没想到,翻车了


会发现,在一个页面组件中,多次调用 Input 组件,只有最后一个 Input 组件才会回显数据


image.png


这个bug是怎么出现的呢?原来是 setTimeout 搞的鬼,在 useDictionary hook 中,当多次调用 useDictionary hook 的时候,为了能拿到全部的 type 值,请求一次接口拿到所有字典的数据,就把字典接口放在 setTimeout 里,弄成异步的逻辑。但是每次调用都会清除上一次的 setTimeout,只保存了最后一次调用 useDictionary 的 setTimeout ,所以就会出现上面的bug了。


既然知道问题所在,那就知道怎么去解决了。


解决方案: 因为只有调用 setOptions 才会引起页面刷新,为了不让 setTimeout 清除掉 setOptions,就把 setOptions 添加到一个更新队列中,等字典接口数据回来再去执行更新队列就可以了。



let timer = null;
const queue = []; // 更新队列
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {};

function useDictionary2(type: string): DictResponse;
function useDictionary2(
type: string | dictOptionsProps[],
value: number | string | Array<number | string>,
separator?: string
): DictResponse;
function useDictionary2(
type: string | dictOptionsProps[],
value?: number | string | Array<string | number>,
separator = ","
): DictResponse {
const [options, setOptions] = useState<dictOptionsProps[]>([]);
const [dictValue, setDictValue] = useState("");

const getLabel = useCallback(
(value) => {
if (value === undefined || value === null || !options.length) return "";
const values = Array.isArray(value) ? value : [value];

const items = values.map((v) => {
if (typeof v === "number") v = v.toString();
return options.find((item) => item.value === v) || { label: value };
});
return items.map((v) => v.label).join(separator);
},
[options]
);

const init = () => {
if (typeof type === "string") {
if (!dict[type] || !dict[type].length) {
dict[type] = [];

const item = {
key: type,
exeFunc: () => {
if (typeof type === "string") {
setOptions(dict[type]);
} else {
setOptions(type);
}
},
};
queue.push(item); // 把 setOptions 推到 更新队列(queue)中

types.push(type);

timer && clearTimeout(timer);
timer = setTimeout(async () => {
const params = types.slice();

types.length = 0;

try {
let dictRes = await dictAppGetOptionsList(params);
for (const key in dictRes.data) {
dict[key] = dictRes.data[key].map((v) => ({
label: v.description,
value: v.subtype,
}));
}

queue.forEach((item) => item.exeFunc()); // 接口回来了再执行更新队列
queue.length = 0; // 清空更新队列
} catch (error) {
queue.length = 0;
}
}, 10);
} else {
typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
}
}
};

useEffect(() => init(), []);

useEffect(() => setDictValue(getLabel(value)), [options, value]);

return { value: dictValue, list: options, getDictValue: getLabel };
}

export default useDictionary;

修复完成,再去试试看~


image.png


不错不错,已经修复,嘿嘿~


这样就可以愉快的使用 字典 hook 啦,去改造一下 PickSelect 组件


export interface IProps extends PickerProps {
open: boolean;
dict?: string;
options?: dictOptionsProps[];
onClose: () => void;
}
const Base = ({
dict,
open = false,
options = [],
onClose = () => { },
...props
}: Partial<IProps>
) => {
// 如果不传 dict ,就拿 options
const { list: _options } = dict ? useDictionary(dict) : { list: options };

return <Picker.Column>
{_options.map((item) => {
return (
<Picker.Option
value={item.value}
key={item.value as string | number}
>

{item.label}
</Picker.Option>
);
})}
</Picker.Column>


在页面组件调用 PickSelect 组件


image.png


效果:


image.png


这样就只需要传入 dict 值,就可以轻轻松松获取到字典数据啦。不用再手动去调用字典接口啦,省下来的时间又可以愉快的摸鱼咯,哈哈哈


最近也在写 vue3 的项目,用 vue3 也实现一个吧。


// 定时器
let timer = 0
const timeout = 10
// 字典类型缓存
const types: string[] = []
// 响应式的字典对象
const dict: Record<string, Ref<CnPage.OptionProps[]>> = {}

// 请求字典选项
function useDictionary(type: string): Ref<CnPage.OptionProps[]>
// 解析字典选项,可以传入已有字典解析
function useDictionary(
type: string | CnPage.OptionProps[],
value: number | string | Array<number | string>,
separator?: string
): ComputedRef<string>
function useDictionary(
type: string | CnPage.OptionProps[],
value?: number | string | Array<number | string>,
separator = ','
): Ref<CnPage.OptionProps[]> | ComputedRef<string> {
// 请求接口,获取字典
if (typeof type === 'string') {
if (!dict[type]) {
dict[type] = ref<CnPage.OptionProps[]>([])

if (type === 'UNIT_LIST') {
// 单位列表调单独接口获取
getUnitListDict()
} else if (type === 'UNIT_TYPE') {
// 单位类型调单独接口获取
getUnitTypeDict()
} else {
types.push(type)
}
}

// 等一下,人齐了才发车
timer && clearTimeout(timer)
timer = setTimeout(() => {
if (types.length === 0) return
const newTypes = types.slice()
types.length = 0
getDictionary(newTypes).then((res) => {
for (const key in res.data) {
dict[key].value = res.data[key].map((v) => ({
label: v.description,
value: v.subtype
}))
}
})
}, timeout)
}

const options = typeof type === 'string' ? dict[type] : ref(type)
const label = computed(() => {
if (value === undefined || value === null) return ''
const values = Array.isArray(value) ? value : [value]
const items = values.map(
(value) => {
if (typeof value === 'number') value = value.toString()
return options.value.find((v) => v.value === value) || { label: value }
}
)
return items.map((v) => v.label).join(separator)
})

return value === undefined ? options : label
}

export default useDictionary

感觉 vue3 更简单啊!


到此结束!如果有错误,欢迎大佬指正~


作者:用户2885248830266
来源:juejin.cn/post/7377559533785022527
收起阅读 »

为了解决小程序tabbar闪烁的问题,我将小程序重构成了 SPA

web
(日落西山,每次看到此景,我总是会想到明朝(明朝那些事儿第六部的标题,日落西山))  前言 几个月前,因工作需求,我开发了一个小程序,当时遇到了一个需求,是关于tabbar权限的问题。小程序的用户分两种,普通用户和vip用户,普通用户tabbar有两个,vip...
继续阅读 »

1.jpg


(日落西山,每次看到此景,我总是会想到明朝(明朝那些事儿第六部的标题,日落西山))


 前言


几个月前,因工作需求,我开发了一个小程序,当时遇到了一个需求,是关于tabbar权限的问题。小程序的用户分两种,普通用户和vip用户,普通用户tabbar有两个,vip用户小程序下面的tabbar有五个。  

因为涉及自定义tabbar的问题,所以官方自带的tabbar肯定就不能用了,我们需要自定义tabbar。官方也提供了自定义tabbar的功能。


官网自定义tabbar


官网地址:基础能力 / 自定义 tabBar (qq.com)


{
"tabBar": {
"custom": true,
"list": []
}
}

就是需要在 app.json 中的 tabBar 项指定 custom 字段,需要注意的是 list 字段也需要存在。


然后,在代码根目录下添加入口文件:


custom-tab-bar/index.js
custom-tab-bar/index.json
custom-tab-bar/index.wxml
custom-tab-bar/index.wxss

具体代码,大家可以参考官网案例。


需要注意的是每个tabbar页面 / 组件都需要在onshow / show 函数中执行以下函数,否则就会出现tabbar按钮切换两次,才会变成选中色的问题。


      if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({
selected: 0 // 第n个tabbar页面就填 n-1
})
}

接下来就是我的思路


2.png


我在 custom-tab-bar/index.js 中定义了一个函数,这个函数去判断当前登录人是否为vip,如果是就替换掉tabbar 的数据。


那么之前每个页面的代码就要写成这样


      if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().change_tabbar_list()
this.getTabBar().setData({
selected: 0 // 第n个tabbar页面就填 n-1
})
}

ok,我们来看一下效果。注意看视频下方的tabbar,每个页面,第一次点击的时候,有明显的闪烁bug。(大家也可以参考一下市面上的小程序,小部分的小程序有这个闪烁问题,大部分的小程序没有这个闪烁的问题(如:携程小程序))



bug产生原因


那么我们就要去思考了,为什么人家的小程序没有这个bug呢?


想这个问题前,要先去想这个bug是怎么产生的,我猜测是每个tabbar页面都有个初始化的过程,第一次渲染页面的时候要去重新渲染tabbar,每个页面的tabbar都是从0开始渲染,然后会缓存到每个页面上,所以第二次点击就没有这个bug了。


解决tabbar闪烁问题


为了解决这个问题,我想到了SPA ,也就是只留一个页面,其他的tabbar页面都弄成组件。


效果展示



已经解决,tabbar闪烁的问题。


代码思路,通过wx:if 控制组件的显示隐藏。


3.png


4.png


源码地址:gitlab.com/wechat-mini…

https克隆地址:gitlab.com/wechat-mini…


写在最后


1、我也是在网上见过别人的一些评论,说如果将小程序重构成这种单页面,会有卡顿问题,我目前没有发现这个问题,可能是我做的小程序功能比较少。


2、至于生命周期,将页面切换成组件后,页面的那些生命周期也肯定都不能使用了,只能用组件的生命周期,我之前开发使用组件的生命周期实现业务逻辑也没什么问题。 触底加载这些也只能换成组件去实现了。


3、小程序最上面的标题,也可以使用以下代码来实现。就是在每个组件初始化的时候要去执行下列代码。


            wx.setNavigationBarTitle({
title: '',
});

作者:楚留香Ex
来源:juejin.cn/post/7317281367111827475
收起阅读 »

如何将微信小程序从WebView迁移到Skyline

web
什么是 Skyline 微信小程序新的渲染引擎,使用更加高效的渲染管线,提供更好的性能和全新的交互动画体验。 具体可以查阅官网介绍 将开发者工具切换成 Sykline 模式 调试基础库切到 2.30.4 或以上版本 确保右上角 > 详情 > 本地...
继续阅读 »

什么是 Skyline


微信小程序新的渲染引擎,使用更加高效的渲染管线,提供更好的性能和全新的交互动画体验。


具体可以查阅官网介绍


将开发者工具切换成 Sykline 模式



  1. 调试基础库切到 2.30.4 或以上版本

  2. 确保右上角 > 详情 > 本地设置里的 开启 Skyline 渲染调试、启用独立域进行调试 选项被勾选上

  3. 确保右上角 > 详情 > 本地设置里的 将 JS 编译成 ES5 选项被勾选上


使用 skylint 工具迁移



npx skylint


image.png


image.png


使用过程中可能会出现文件未找到错误,例如


image.png


原因就是使用绝对路径 <import src="/components/chooserList/index.wxml" />导入模块,而 skylint 无法找到该文件,需要修改为相对路径 <import src="../../components/chooserList/index.wxml" />导入模块


有几种提示不是很准确,可以评估下:



  1. @position-fixed 不支持 position: fixed:如果你根据不同 renderer 兼容,则会导致该提示一直存在

  2. @no-pseudo-element 不支持伪元素: 目前对已经支持的 ::before 和 ::after 也会进行提示


手动迁移


在 app.json 配置



{
"lazyCodeLoading": "requiredComponents", // 开启按需注入
"rendererOptions": {
"skyline": {
"defaultDisplayBlock": true // skyline 下节点默认为 flex 布局,可以在此切换为默认 block 布局
}
}
}


在 page.json 配置



{
"renderer": "skyline", // 声明为 skyline 渲染,对于已有的项目,建议渐进式迁移,对于新项目,直接全局打开,在 app.json 里进行配置
"componentFramework": "glass-easel", // 声明使用新版 glass-easel 组件框架
"disableScroll": true, // skyline 不支持页面全局滚动,为了使之与WebView保持兼容,在此禁止滚动
"navigationStyle": "custom" // skyline 不支持原生导航栏,为了使之与WebView保持兼容,并且自行实现自定义导航栏
}


skyline 不支持页面全局滚动,如果需要页面滚动,在需要滚动的区域使用 scroll-view 实现



<scroll-view type="list" scroll-y style="flex: 1; width: 100%; overflow: hidden;"></scroll-view>



page {
display: flex;
flex-direction: column;
height: 100vh;
}


skyline 渲染模式下 flex-direction 默认值是 column,为了使之与WebView保持兼容,需要在 flex 布局里将 flex-direction 默认值改为 row


在真机上调试 skyline 渲染模式


小程序菜单 > 开发调试 > Switch Render,会出现三个选项,说明如下:


Auto :跟随 AB 实验,即对齐小程序正式用户的表现


WebView :强制切为 WebView 渲染


Skyline :若当前页面已迁移到 Skyline,则强制切为 Skyline 渲染


image.png


常见问题


position: fixed 不支持


需要将



<view class="background"></view>



.background {

position: fixed;

}


修改为



<root-portal>

<view class="background"></view>

</root-portal>



.background {

position: absolute;

}


如果无法做到适配,则可以根据不同 renderer 兼容



<view class="position {{renderer}}"></view>



.position {
position: fixed;
}

.position.skyline {
position: absolute;
}



Page({
data: {
renderer: 'webview'
},

onLoad() {
this.setData({
renderer: this.renderer,
})
},
})


不支持 Canvas 旧版接口


Skyline 渲染模式下,旧版 Canvas 绘制图案无效(使用 wx.createCanvasContext 创建的上下文)


在真机中图片的 referer 丢失


测试结果如下:


使用 WebView 渲染 Image,请求的 header 是:



{
host: 'xxx',
connection: 'keep-alive',
accept: 'image/webp,image/avif,video/*;q=0.8,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5',
'user-agent': 'xxx',
'accept-language': 'zh-CN,zh-Hans;q=0.9',
referer: 'https://servicewechat.com/',
'accept-encoding': 'gzip, deflate'
}


使用 Skyline 渲染 Image,请求的 header 是:



{

'user-agent': 'Dart/2.16 (dart:io)',
'accept-encoding': 'gzip',
host: 'xxx'
}


官方 Demo


可以参考官方Demo学习使用 Skyline 的增强特性,比如 Worklet 动画、手势系统等,但在首次下载编译时,会遇到【交互动画】页面为空的问题,主要原因是该页面是由 TypeScript 写的,编译成 JavaScript 需要开启工具内置的 TypeScript编译插件,需要在project.config.json project.config.json 配置:



setting.useCompilerPlugins: ["typescript"]


参考



作者:不二先生不二
来源:juejin.cn/post/7262656196854644792
收起阅读 »

小程序实现无感登录+权限分配

web
djngo开发:小程序实现无感登录+权限分配 由来 最近开发一个预约系统,需要区分普通用户和工作人员。由于账号密码/短信验证过于繁琐,因而选择记录openid实现无感登录。(基本小程序都这样操作) 同时事先在数据库中录入客户手机号,即可在用户登录时根据有无手机...
继续阅读 »

djngo开发:小程序实现无感登录+权限分配


由来


最近开发一个预约系统,需要区分普通用户和工作人员。由于账号密码/短信验证过于繁琐,因而选择记录openid实现无感登录。(基本小程序都这样操作)


同时事先在数据库中录入客户手机号,即可在用户登录时根据有无手机号来区分普通用户和工作人员。这样在项目交付时,工作人员和普通用户一样可以直接登录无感登录小程序


1. 开发思路


微信的openid是一种唯一标识用户身份的字符串

用户登录小程序,通过手机号快速验证组件获取动态令牌code,后端向微信服务器发送get请求并带上code获取每个用户唯一的openid,然后记录到mysql中,并签发token。该openid就是登录小程序的唯一凭证。


2.简单实现



  • 获取openid,如果通过openid查不用户,就自动新建用户,并返回token。


#####LoginView###########

code = request.data.get("code")
appid = appid # 微信小程序的appid
appsecret = "xxxxxxxx" # 微信小程序的密钥,登录微信公众平台即可获取
# 获取openid和session_token
querystring = {"appid":appid,"js_code":code,"secret":appsecret,"grant_type":"authorization_code"}
jscode2session = requests.get('https://api.weixin.qq.com/sns/jscode2session',params=querystring)
if not jscode2session.json().get("errcode"):
data = jscode2session.json()
########拿到openid#########
openid=data.get("openid")
#######去数据库比对,如果通过openid查到用户并且未被禁用,就新建##########
try:
user = models.UserInfo.objects.get(openid)
if user.is_deleted: # 检查用户是否被禁用
return ErrorResponse(msg='用户已被禁用,无法登录',data=data,code=302)
except models.UserInfo.DoesNotExist:
models.UserInfo.objects.create()

3.更进一步:通过手机号来区别普通用户和工作人员


openid虽然做到的唯一性验证,但是当用户数量庞大时,该如何区分用户角色:



  • 一:手动在后台根据已有用户分配权限

  • 二:登录时根据某一标识区分角色


    方法一显然不靠谱,因为用户至少会超过1000人,方法二需要额外标识,显然手机号最合适。



3.1 前端获取手机号的动态令牌


小程序提供了手机号快速验证组件,方便我们获取手机号


bindgetphonenumber 事件回调中的动态令牌code传到开发者后台


  <view class="title">欢迎来到广盈预约</view>
<view class="card">
<view class="button">快捷登录</view>
<button
style="opacity: 0"
class="bottom-button"
open-type="getPhoneNumber|agreePrivacyAuthorization"
bindgetphonenumber="getrealtimephonenumber"
bindagreeprivacyauthorization="handleAgreePrivacyAuthorization"
>

同意隐私协议并授权手机号注册
</button>
</view>
</view>

Page({
getPhoneNumber (e) { console.log(e.detail.code) // 动态令牌 }
})

image.png



注意:如果你想获取用户手机号就必须添加用户授权《隐私保护协议》bindagreeprivacyauthorization,否则小程序无法上线



3.2 后端带着动态令牌去微信服务器获取手机号


简单来说就是用户登录时数据库中没有手机号对应的用户,后端就会自动建立一个账号,并分配权限为普通用户,然后直接登录。


这样的话,只需要第一次登录时获取手机号,以后登录就可以直接进入系统。


class RegisterView(APIView):  
authentication_classes = []
permission_classes = []
def getmobile(self,appid,code):
"""获取用户的手机号"""
try:
appsecret = "cxxxxxxxxx"
querystring = {"appid":appid,"secret":appsecret,"grant_type":"client_credential"}
response = requests.get('https://api.weixin.qq.com/cgi-bin/token',params=querystring)
access_token = response.json().get("access_token")
querystring = {"access_token":access_token}
headers = {"content-type": "application/json"}
payload = {"code":code}
mobile =requests.post(f"https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}",json=payload,headers=headers)
return mobile.json().get('phone_info').get('phoneNumber')
except Exception as e:
return


def post(self, request):
unionid =request.data.get("unionid")
nickname =request.data.get("nickname")
openid = request.data.get("openid")
code = request.data.get("code")
appid = request.data.get("appid")

mobile = self.getmobile(appid=appid,code=code)
if not mobile:
return ErrorResponse(msg="手机号获取失败")

defaults = {
"openid":openid,
"unionid":unionid,
"mobile":mobile,
"nickname":nickname,
}

"""这条语句将查找一个符合mobile=mobile条件的记录,如果找到就更新 defaults中的字段 ,否则就创建
注意: 查询的条件必须是唯一的,否则会造成多条数据返回而报错,这个逻辑同 get() 函数。
注意: 使用的字段,没有唯一的约束,并发的调用这个方法可能会导致多条相同的值插入。
"""

models.UserInfo.objects.update_or_create(mobile=mobile,defaults=defaults)
models.User_GZH.objects.get_or_create(unionid=unionid, defaults={'unionid':unionid})
return DetailResponse()

作者:大海前端
来源:juejin.cn/post/7293177336488804391
收起阅读 »

【已解决】uniapp小程序体积过大、隐私协议的问题

web
概述 在前几天的工作中又遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,说明我们需要进一步的优化它,技术栈是使用uniapp框架+HBuilderX的开发环境,微信小程序更新了隐私协议,Http返回信息{errMsg: "getUserP...
继续阅读 »

概述


在前几天的工作中又遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,说明我们需要进一步的优化它,技术栈是使用uniapp框架+HBuilderX的开发环境,微信小程序更新了隐私协议,Http返回信息{errMsg: "getUserProfile:fail can only be invoked by user TAP gesture."}


定位原因


程序出现问题,首先需要把原因定位归结在第一点,这是解决问题的关键,检查了一下Git仓库的修改情况,发现引入了一个7kb大小的防抖插件,其实7kb的插件不是根本问题,问题是之前的代码写的太不规范了。


压缩资源


尽量把static下面的图片都压缩一下,这里推荐一个好用的压缩网站,图片进行压缩:tinypng.com/


我没有压缩过Js文件,但会有一种方法压缩js文件,使js文件尽量的缩小来减少js文件建立的文件体积。


uniapp官方压缩建议:


小程序工具提示vendor.js过大,已经跳过es6向es5转换。这个转换问题本身不用理会,因为vendor.js已经是es5的了。


关于体积控制,参考如下:



  • 使用运行时代码压缩
    HBuilderX创建的项目勾选运行-->运行到小程序模拟器-->运行时是否压缩代码

  • cli创建的项目可以在package.json中添加参数--minimize,示例:"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch --minimize"


小程序分包处理



  • 在对应平台的配置下添加 "optimization":{"subPackages":true}开启分包优化

  • 目前只支持 mp-weixin、mp-qq、mp-baidu、mp-toutiao、mp-kuaishou的分包优化


分包优化具体逻辑:



  • 静态文件:分包下支持 static 等静态资源拷贝,即分包目录内放置的静态资源不会被打包到主包中,也不可在主包中使用

  • js文件:当某个 js 仅被一个分包引用时,该 js 会被打包到该分包内,否则仍打到主包(即被主包引用,或被超过 1 个分包引用)

  • 自定义组件:若某个自定义组件仅被一个分包引用时,且未放入到分包内,编译时会输出提示信息


分包内静态文件示例


"subPackages": [{
"root": "pages/sub",
"pages": [{
"path": "index/index"
}]
}]

网络请求


还有一个解决小程序体积过大的问题,把非必要的组件都使用Http Api接口的形式去进行交互,尽量去减少本地包中的体积,再根目录下/utils里有一个232kb的获取地址交互,可以替换成Http Api的形式来解决。


隐私协议


在开发微信小程序过程中遇到了{errMsg: "getUserProfile:fail can only be invoked by user TAP gesture."},出现这个信息的原因是微信平台更新了隐私协议,需要再后台备案更新一下,搜索了很多,都不准确,这个隐私协议没有什么特殊情况,2个小时就可以通过了。


设置路径1: 公众号平台->设置->服务内容声明,设置通过后显示的状态是已更新,状态之前的是审核中


111.png


设置路径2: 首页->管理->版本管理->提交审核 ,再这里面提审,隐私协议审核过了,就可以继续开发了。


作者:stark张宇
来源:juejin.cn/post/7296025902911897627
收起阅读 »

uniapp微信小程序授权后得到“微信用户”

web
背景 近日在开发微信小程序的时候,发现数据库多了很多用户名称是"微信用户"的账号信息。接口的响应信息如下。 (nickName=微信用户, avatarUrl=https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4...
继续阅读 »

背景


近日在开发微信小程序的时候,发现数据库多了很多用户名称是"微信用户"的账号信息。接口的响应信息如下。


(nickName=微信用户, avatarUrl=https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132, gender=0, country=, province=, city=, language=), code=0e1abNFa1dBwRG0lnoJa18qT0i2abNFk)

经过排查,发现官方是对微信授权的接口做出了调整。小程序用户头像昵称获取规则调整公告


WX20240206-112518@2x.png

根据上面标红的字体说明,官方的意图就是只提供openid和unionid, 不暴露用户头像昵称数据。

基于此才会在新版的接口中返回"微信用户"的信息。



  • 针对这个问题,官方提供的解决方案如下。


WX20240206-112912@2x.png
以上解决方案,表达的意思是新版用户授权的接口中, 官方只会给你提供unionid和openid.

至于用户的昵称和头像,开发者可以提供功能,以用户的意志去完成修改和更新。

tips: 建议授权接口生成用户名和昵称,采用系统默认的方式。


微信授权流程


152f3cb28a734e768381f986cec1dd26.png


uniapp代码实现


uni.login接口文档


WX20240207-221556@2x.png


后端代码


WX20240207-221641@2x.png


以上是uniapp和springboot部分代码截图展示,关注微信公众号:JeecgFlow,或微信扫描下面二维码.

回复"微信用户"可以获取完整代码。


异常分析


//如果你的接口出现如下信信息,该如何处理呢?
# {errMsg: “getUserProfile:fail api scope is not
declared in the privacy agreement“, errno: 112}

出现问题的原因: api 范围未在隐私协议中声明,建议大家更具公告,更新对应的隐私协议。

【设置-服务内容声明-用户隐私保护指引】,更新隐私协议,在第一条:开发者处理的信息中,点击【增加信息类型】,选择需要授权的信息,头像昵称我已经勾选了,所以列表中不显示了,根据需求选择和填写其他内容,最后确定并生成协议。等待隐私协议审核通过。


68b3f3f0c4ee419d9ca5dec8aa5b0a4c.png
建议按需添加,以防审核不通过。


为了分辨用户,开发者将在获取你的明示同意后,收集你的微信昵称、头像。
为了显示距离,开发者将在获取你的明示同意后,收集你的位置信息。
开发者收集你的地址,用于获取位置信息。
开发者收集你的发票信息,用于维护消费功能。
为了用户互动,开发者将在获取你的明示同意后,收集你的微信运动步数。
为了通过语音与其他用户交流互动,开发者将在获取你的明示同意后,访问你的麦克风。
开发者收集你选中的照片或视频信息,用于提前上传减少上传时间。
为了上传图片或者视频,开发者将在获取你的明示同意后,访问你的摄像头。
为了登录或者注册,开发者将在获取你的明示同意后,收集你的手机号。
开发者使用你的通讯录(仅写入)权限,用于方便用户联系信息。
开发者收集你的设备信息,用于保障你正常使用网络服务。
开发者收集你的身-份-证号码,用于实名认证后才能继续使用的相关网络服务。
开发者收集你的订单信息,用于方便获取订单信息。
开发者收集你的发布内容,用于用户互动。
开发者收集你的所关注账号,用于用户互动。
开发者收集你的操作日志,用于运营维护。
为了保存图片或者上传图片,开发者将在获取你的明示同意后,使用你的相册(仅写入)权限。
为了用户互动,开发者将在获取你的明示同意后,收集你的车牌号。
开发者访问你的蓝牙,用于设备连接。
开发者使用你的日历(仅写入)权限,用于用户日历日程提醒。
开发者收集你的邮箱,用于在必要时和用户联系。
开发者收集你选中的文件,用于提前上传减少上传时间。


当你选择所需的接口后,需要您填写使用说明。 可以参考上面的内容进行填写。

给大家看一下我申请的接口。折腾半天终于把授权登录给整好了。


WX20240208-100953@2x.png


做完上述隐私设置后,需要你重新发布自己的小程序。 并且设置成采集用户隐私。

审核通过后就可以啦。如下图, 请一定注意!!!


WX20240208-101216@2x.png


参考文档


头像昵称填写-微信官方文档

uniapp头像昵称填写

getUserProfile:fail api scope is not declared in the privacy agreement


作者:代码次位面
来源:juejin.cn/post/7332113324651610150
收起阅读 »

UniApp TabBar的巅峰之作:个性化导航的魅力

web
前言在当今数字化时代,用户界面(UI)设计扮演着至关重要的角色,它不仅仅是产品的外表,更是用户与产品互动的第一印象。在一个社交群里,我有幸结识了一位创业的大佬,陈总,他自研的产品UI设计堪称一流,尤其是引人注目的菜单栏设计,深深吸引了我的注意,我就想着将从零玩...
继续阅读 »

前言

在当今数字化时代,用户界面(UI)设计扮演着至关重要的角色,它不仅仅是产品的外表,更是用户与产品互动的第一印象。在一个社交群里,我有幸结识了一位创业的大佬,陈总,他自研的产品UI设计堪称一流,尤其是引人注目的菜单栏设计,深深吸引了我的注意,我就想着将从零玩转系列之微信支付也优化一下

⚠️注意 本次不是从零玩转系列需要有一定的编程能力的同学

二、介绍

UniApp的TabBar

如果应用是一个多 tab 应用,可以通过 tabBar 配置项指定一级导航栏,以及 tab 切换时显示的对应页。

在 pages.json 中提供 tabBar 配置,不仅仅是为了方便快速开发导航,更重要的是在App和小程序端提升性能。在这两个平台,底层原生引擎在启动时无需等待js引擎初始化,即可直接读取 pages.json 中配置的 tabBar 信息,渲染原生tab。

Tips

  • 当设置 position 为 top 时,将不会显示 icon
  • tabBar 中的 list 是一个数组,只能配置最少2个、最多5个 tab,tab 按数组的顺序排序。
  • tabbar 切换第一次加载时可能渲染不及时,可以在每个tabbar页面的onLoad生命周期里先弹出一个等待雪花(hello uni-app使用了此方式)
  • tabbar 的页面展现过一次后就保留在内存中,再次切换 tabbar 页面,只会触发每个页面的onShow,不会再触发onLoad。
  • 顶部的 tabbar 目前仅微信小程序上支持。需要用到顶部选项卡的话,建议不使用 tabbar 的顶部设置,而是自己做顶部选项卡

三、设计

原本的ui样式,真滴丑不好看......

我改造后的,我滴妈真漂亮pink 猛男粉

设计图如下,懂前端的大佬肯定觉得没什么,虽然但是.....我是后端

可以分析他一个大的div包裹并且设置了边框圆形,里面有多个item元素菜单也设置了边框样式,每个菜单上面点击的时候会有背景颜色,我滴妈很简单啊,这我们在 从零玩转系列之微信支付当中讲过呀 给一个 `class样式 如果当前是谁就给谁 通过 vue 的 动态样式 so easy to happy !

四、实现思路

  • 删除TabBar配置的菜单栏:首先,需要从原始TabBar配置中移除默认的菜单栏,这将为自定义TabBar腾出空间。
  • 自定义底部菜单栏:接下来,自定义创建一个底部菜单栏,他是一个组件页面每个页面都需要引入
  • 自定义样式:使用CSS或相关样式设置,将自定义菜单栏精确地定位到底部,确保它与屏幕底部对齐,以实现预期的效果。

五、删除TabBar配置

好的我们尝试来删除 TabBar 配置 重新编译

可以看到报错了,这个错误就是我们使用的是switchTab进行菜单跳转使用别的肯定可以.但是为什么要用switchTab呢?

需求: 和原先的菜单栏功能一样不能销毁其他的菜单页面

那么我们将配置重新填上,他就不会报错了

⚠️注意: 这里有个问题,我们做的是菜单栏在uniapp当中菜单栏跳转是不会销毁其他页面的他其实是根据 switchTab 来进行路由的跳转不回销毁其他TabBar页面

菜单栏跳转的我们是不能销毁的那么这个配置就必须存在了呀,存在就存在无所谓!

遇事不要慌打开文档看看

这个时候我看到了什么?  hide 隐藏啊给我猜到了.绝壁有!!!!

uni.hideTabBar(OBJECT)

好我们知道有这个懂就行,后面我们进行创建我们的 自定义菜单栏组件 tabbar.vue

六、自定义TabBar

创建组件 tabbar.vue 这里我们使用vue3组合式Api搭建页面

<template>

<view class="tab-bar">

<view v-for="(item,index) in tabBarList" :key="index"
:class="{'tab-bar-item': true,currentTar: selected == item.id}"
@click="switchTab(item, index)">

<view class="tab_text" :style="{color: selected == index ? selectedColor : color}">
<image class="tab_img" :src="selected == index ? item.selectedIconPath : item.iconPath">image>
<view>{{ item.text }}view>
view>
view>
view>
template>

代码详细介绍

  1. : 这是一个外部的 view 元素,它用来包裹整个选项卡栏。
  1. : 这是一个 Vue.js 的循环指令 v-for,它用来遍历一个名为 tabBarList 的数据数组,并为数组中的每个元素执行一次循环。在循环过程中,item 是数组中的当前元素,index 是当前元素的索引。v-for 指令还使用 :key="index" 来确保每个循环元素都有一个唯一的标识符。
  1. :tab-bar-item': true,currentTar: selected == item.id}": 这是一个动态的 class 绑定,它根据条件为当前循环的选项卡元素添加不同的 CSS 类。如果 selected 的值等于当前循环元素的 item.id,则添加 currentTar 类,否则添加 tab-bar-item 类。
  1. @click="switchTab(item, index)": 这是一个点击事件绑定,当用户点击选项卡时,会触发名为 switchTab 的方法,并将当前选项卡的 item 对象和索引 index 作为参数传递给该方法。
  1. : 这是一个包含文本内容的 view 元素,它用来显示选项卡的文本。它还具有一个动态的样式绑定,根据条件选择文本的颜色。如果 selected 的值等于当前循环元素的 index,则使用 selectedColor,否则使用 color
  1. : 这是一个 image 元素,它用来显示选项卡的图标。它的 src 属性也是根据条件动态绑定,根据 selected 的值来选择显示不同的图标路径。
  1. {{ item.text }}: 这是一个用来显示选项卡文本内容的 view 元素,它显示了当前选项卡的文本,文本内容来自于 item.text

编写函数

代码当中的 tabBarList 函数要和 pages.json -> tabbar 配置一样哦

<script setup>
import { defineProps, ref } from 'vue'

// 子组件传递参数
const props = defineProps({
selected: {
type: Number,
default: 0
}
})

// 为选中颜色
let color = ref('#000')
// 选中的颜色
let selectedColor = ref('#ffb2b2')
// 菜单栏集合 - 与 pages.json -> tabbar 配置一样
let tabBarList = ref([
{
"id": 0,
"pagePath": "/pages/index/index",
"iconPath": "../../static/icon/icon_2.png",
"selectedIconPath": "../../static/icon/icon_2.png",
"text": "购买课程"
},
{
"id": 1,
"pagePath": "/pages/order/order",
"iconPath": "../../static/icon/gm_1.png",
"selectedIconPath": "../../static/icon/gm_1.png",
"text": "我的订单"
},
{
"id": 2,
"pagePath": "/pages/about/about",
"iconPath": "../../static/icon/about_3.png",
"selectedIconPath": "../../static/icon/about_3.png",
"text": "关于"
}
])

// 跳转tabBar菜单栏
const switchTab = (item) => {
let url = item.pagePath;
uni.switchTab({
url
})
}

script>

自定义TabBar样式


<style lang="less" scoped>
// 外部装修
.tab-bar {
position: fixed;
bottom: 25rpx;
left: 15rpx;
right: 15rpx;
height: 100rpx;
background: white;
padding: 20rpx;
border-radius: 30rpx;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 15px rgba(165, 168, 171, 0.83) !important;

// 当前点击的
.currentTar {
border-radius: 15rpx;
box-shadow: 0 0 15px #D7D7D7FF !important;
transition: all 0.5s ease-in-out;
}

// 给每个 item 设置样式
.tab-bar-item {
//flex: 0.5;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 150rpx;
padding: 15rpx;
background-color: transparent;
transition: all 0.5s ease-in-out;
margin: auto;

// 限制每个icon的大小
.tab_img {
width: 37rpx;
height: 41rpx;
}

// 限制文字大小
.tab_text {
font-size: 20rpx;
margin-top: 9rpx;
flex: 1;
}
}
}
style>

测试

我们自定义的效果出来了但是下面是什么鬼.....

可以看到我们下面也有一个菜单栏是 tabbar 配置产生出来的,我们前面不是说了隐藏吗?

修改函数新增隐藏tabbar代码

// 隐藏原生TabBar
uni.hideTabBar();

最后

本期结束咱们下次再见👋~

🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗


作者:杨不易呀
来源:juejin.cn/post/7330295657167290403

收起阅读 »

uni-app新建透明页面实现全局弹窗

web
需求背景 实现一个能遮住原生 tabbar 和 navbar 的全局操作框 原理 使用一个透明的页面来模拟弹窗,这个页面可以遮住原生 tabbar 和 navbar 页面配置 { "path" : "pages/shootAtWill/shootAtW...
继续阅读 »

需求背景


实现一个能遮住原生 tabbarnavbar 的全局操作框


原理


使用一个透明的页面来模拟弹窗,这个页面可以遮住原生 tabbarnavbar


页面配置


{
"path" : "pages/shootAtWill/shootAtWill",
"style" :
{
"navigationBarTitleText" : "随手拍",
"navigationStyle": "custom",
"backgroundColor": "transparent",
"app-plus": {
"animationType": "slide-in-bottom", // 我这边需求是从底部弹出
"background": "transparent",
"popGesture": "none",
"bounce": "none",
"titleNView": false,
"animationDuration": 150
}
}
}

页面样式


<style>
page {
/* 必须的样式,这是页面背景色为透明色 */
background: transparent;
}
</style>
<style lang="scss" scoped>
// 写你页面的其他样式
</style>

这样的话就新建成功了一个透明的页面,那么这个页面上的东西都可以遮挡住原生 tabbarnavbar


我还加了遮罩:


<template>
<view>
<view class="modal" style="z-index: -1"></view>

</view>
</template>

<style lang="scss" scoped>
.modal {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
}
</style>


效果演示


在这里插入图片描述


作者:鹏北海
来源:juejin.cn/post/7317325043541639178
收起阅读 »

集帅(美)们,别再写 :key = "index" 啦!

web
浅聊一下 灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞... 假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相...
继续阅读 »

浅聊一下


灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞...



假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!



开始


在向掘友们解释为什么不能使用 :key = "index" 之前,我想我还得向你们铺垫一点东西


虚拟DOM


什么是虚拟DOM呢?虚拟DOM是一个对象,没想到吧...我们来看看Vue是如何将template模板里面的东西交给浏览器来渲染的


image.png


首先通过 compiler 将 template模板变成一个虚拟DOM,再将虚拟DOM转换成HTML,最后再交给浏览器V8引擎渲染,那么虚拟DOM是什么样的呢?


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>

<body>
<div id="app">
<ul id="item">
<li v-for="item in list" class="item">{{item}}</li>
</ul>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['vue','js','html'])
return {
list
}
}
}).mount('#app')
</script>
</body>

</html>

在这里,template模板实际上是


 <ul>
<li v-for="item in list">{{item}}</li>
</ul>

通过v-for循环,渲染出来了3个li


<ul>
<li>vue<li>
<li>js<li>
<li>html<li>
</ul>

我们的compiler会将这个模板转化成虚拟DOM


let oldDom = {
tagName = 'ul',
props:{
//存放id 和 class 等
id:'item'
},
children[
{
tagName = 'li',
props:{
class:'item'
},
children:['vue']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['js']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['html']
},
]
}

diff算法


给前面的例子来点刺激的,加上一个按钮和反转函数,点击按钮,list反转


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>

<body>
<div id="app">
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
<button @click="change">change</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['唱','跳','rap','篮球'])
const change = ()=>{
list.value.reverse()
}
const add = ()=>{
list.value.unshift('6')
}
return {
list,
change,
}
}
}).mount('#app')
</script>
</body>

</html>

点击change按钮,此时我们的DOM更改vue又是如何来更新DOM的呢?


image.png


众所周知,回流和重绘会消耗极大的性能,而当DOM发生变更的时候会触发回流重绘(可以去看我的文章(从输入4399.com到页面渲染之间的回流和重绘),那么vue3就有一个diff算法,用来优化性能


image.png


当DOM更改,compiler会生成一个新的虚拟DOM,然后通过diff算法来生成一个补丁包,用来记录旧DOM和新DOM的差异,然后再拿到html里面进行修改,最后再交给浏览器V8进行渲染


简单介绍一下diff算法的比较规则



  1. 同层比较,是不是相同的结点,不相同直接废弃老DOM

  2. 是相同结点,比较结点上的属性,产生一个补丁包

  3. 继续比较下一层的子节点,采用双端对列的方式,尽量复用,产生一个补丁包

  4. 同上


image.png


别再写 :key = "index"


要说别写 :key = "index" ,我们得先明白key是用来干什么的...如果没有key,那么在diff算法对新旧虚拟DOM进行比较的时候就没法比较了,你看这里有两个一样的vue,当反转顺序以后diff算法不知道哪个vue该对应哪个vue了


image.png


如果我们用index来充当key的话来看,当我们在头部再插入一个结点的时候,后面的index其实是改变了的,导致diff算法在比较的时候认为他们与原虚拟DM都不相同,那么diff算法就等于没有用...


image.png


可以用随机数吗?


<li v-for="item in list" :key="Math.random()">

想出这种办法的,也是一个狠人...当然是不行的,因为在template模板更新时,会产生一个新的虚拟DOM,而这个虚拟DOM里面的key也是随机值,和原虚拟DOM里的key99.99999%是不一样的...


结尾


希望你以后再也不会写 :key = "index" 了



假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!



作者:滚去睡觉
来源:juejin.cn/post/7337513012394115111
收起阅读 »

Echarts中国地图下钻,支持下钻到县(vue3)

web
引言 Echarts 大家都不陌生吧,时常被用于绘制各种图表,也作为大屏可视化的常驻用户,这里就不多说了,今天主要是讲述一下 Echarts 的地图下钻,支持下钻到县、返回上一级。 准备工作 地图JSON数据 DataV.GeoAtlas地理小工具系列 (al...
继续阅读 »

引言


Echarts 大家都不陌生吧,时常被用于绘制各种图表,也作为大屏可视化的常驻用户,这里就不多说了,今天主要是讲述一下 Echarts 的地图下钻,支持下钻到县、返回上一级。


准备工作


地图JSON数据


DataV.GeoAtlas地理小工具系列 (aliyun.com) 支持在线调用API和下载json资源(我这里是调用的API)



如果地图json API请求报错403,可参考这个解决办法 :地图请求阿里的geojson数据时,返回403Forbidden解决方案



技术栈



  • vue: 3.3.7

  • vue-echarts: 6.6.1 (直接使用 Echarts 也是一样的,这个只是对 Echarts 的组件封装)

  • vite: 4.5.0


地图效果


0mdtt-ameuf.gif


项目预览地址:UnusualAdmin


项目代码地址:UnusualAdmin


实现


template


这里只需要一个 Echarts 节点和一个按钮就行了


<template>
<div :style="`height: ${calcHeight('main')};`" class="wh-full pos-relative">
<v-chart :option="mapOption" :autoresize="true" @click="handleClick" />
<n-button v-show="isShowBack" class="pos-absolute top-10 left-10" @click="goBack">返回n-button>
div>
template>

获取mapJson


// 使用线上API
const getMapJson = async (mapName: string) => {
const url = `https://geo.datav.aliyun.com/areas_v3/bound/${mapName}.json`
const mapJson = await fetch(url).then(res => res.json())
return mapJson
}

// 使用本地资源
const getMapJson = async (mapName: string) => {
const url = `@/assets/mapJson/${mapName}.json`
const mapJson = await import(/* @vite-ignore */ url)
return mapJson
}


第二种方法(使用本地资源)存在问题:这个方法后续发现,vite打包不会把json文件打包到dist,线上会报错,目前没找到可靠的解决办法(如果放到public文件夹下会打包进去),故舍弃。


如果大家有什么解决这个问题的好办法,请在评论区留言,博主会一一去尝试的🙏🙏🙏



更新地图配置options


const setOptions = (mapName: string, mapData: any) => {
return {
// 鼠标悬浮提示
tooltip: {
show: true,
formatter: function (params: any) {
// 根据需要进行数据处理或格式化操作
if (params && params.data) {
const { adcode, name, data } = params.data;
// 返回自定义的tooltip内容
return `adcode: ${adcode}
name: ${name}
data: ${data}`
;
}
},
},
// 左下角的数据颜色条
visualMap: {
show: true,
min: 0,
max: 100,
left: 'left',
top: 'bottom',
text: ['高', '低'], // 文本,默认为数值文本
calculable: true,
seriesIndex: [0],
inRange: {
color: ['#00467F', '#A5CC82'] // 蓝绿
}
},
// geo地图
geo: {
map: mapName,
roam: true,
select: false,
// 图形上的文本标签,可用于说明图形的一些数据信息,比如值,名称等。
selectedMode: 'single',
label: {
show: true
},
emphasis: {
itemStyle: {
areaColor: '#389BB7',
borderColor: '#389BB7',
borderWidth: 0
},
label: {
fontSize: 14,
},
}
},
series: [
// 地图数据
{
type: 'map',
map: mapName,
roam: true,
geoIndex: 0,
select: false,
data: mapData
},
// 散点
{
name: '散点',
type: 'scatter',
coordinateSystem: 'geo',
data: mapData,
itemStyle: {
color: '#05C3F9'
}
},
// 气泡点
{
name: '点',
type: 'scatter',
coordinateSystem: 'geo',
symbol: 'pin', //气泡
symbolSize: function (val: any) {
if (val) {
return val[2] / 4 + 20;
}
},
label: {
show: true,
formatter: function (params: any) {
return params.data.data || 0;
},
color: '#fff',
fontSize: 9,
},
itemStyle: {
color: '#F62157', //标志颜色
},
zlevel: 6,
data: mapData,
},
// 地图标点
{
name: 'Top 5',
type: 'effectScatter',
coordinateSystem: 'geo',
data: mapData.map((item: { data: number }) => {
if (item.data > 60) return item
}),
symbolSize: 15,
showEffectOn: 'render',
rippleEffect: {
brushType: 'stroke'
},
label: {
formatter: '{b}',
position: 'right',
show: true
},
itemStyle: {
color: 'yellow',
shadowBlur: 10,
shadowColor: 'yellow'
},
zlevel: 1
},
]
}
}

渲染地图


const renderMapEcharts = async (mapName: string) => {
const mapJson = await getMapJson(mapName)
registerMap(mapName, mapJson); // 注册地图
// 为地图生成一些随机数据
const mapdata = mapJson.features.map((item: { properties: any }) => {
const data = (Math.random() * 80 + 20).toFixed(0) // 20-80随机数
const tempValue = item.properties.center ? [...item.properties.center, data] : item.properties.center
return {
name: item.properties.name,
value: tempValue, // 中心点经纬度
adcode: item.properties.adcode, // 区域编码
level: item.properties.level, // 层级
data // 模拟数据
}
});
// 更新地图options
mapOption.value = setOptions(mapName, mapdata)
}

实现地图点击下钻


// 点击下砖
const mapList = ref<string[]>([]) // 记录地图
const handleClick = (param: any) => {
// 只有点击地图才触发
if (param.seriesType !== 'map') return
const { adcode, level } = param.data
const mapName = level === 'district' ? adcode : adcode + '_full'
// 防止最后一个层级被重复点击,返回上一级出错
if (mapList.value[mapList.value.length - 1] === mapName) {
return notification.warning({ content: '已经是最下层了', duration: 1000 })
}
// 每次下转都记录下地图的name,在返回的时候使用
mapList.value.push(mapName)
renderMapEcharts(mapName)
}

返回上一级实现


// 点击返回上一级地图
const goBack = () => {
const mapName = mapList.value[mapList.value.length - 2] || '100000_full'
mapList.value.pop()
renderMapEcharts(mapName)
}

全部代码

<template>
<div :style="`height: ${calcHeight('main')};`" class="wh-full pos-relative">
<v-chart :option="mapOption" :autoresize="true" @click="handleClick" />
<n-button v-show="isShowBack" class="pos-absolute top-10 left-10" @click="goBack">返回</n-button>
</div>
</template>

<script setup lang="ts" name="EchartsMap">
import { use, registerMap } from 'echarts/core'
import VChart from 'vue-echarts'
import { CanvasRenderer } from 'echarts/renderers'
import { MapChart, ScatterChart, EffectScatterChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, VisualMapComponent } from 'echarts/components'
import { calcHeight } from '@/utils/help';

use([
CanvasRenderer,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
VisualMapComponent,
MapChart,
ScatterChart,
EffectScatterChart
])

const notification = useNotification()
const mapOption = ref()
const mapList = ref<string[]>([]) // 记录地图
const isShowBack = computed(() => {
return mapList.value.length !== 0
})

const getMapJson = async (mapName: string) => {
const url = `https://geo.datav.aliyun.com/areas_v3/bound/${mapName}.json`
const mapJson = await fetch(url).then(res => res.json())
return mapJson
}

const setOptions = (mapName: string, mapData: any) => {
return {
tooltip: {
show: true,
formatter: function (params: any) {
// 根据需要进行数据处理或格式化操作
if (params && params.data) {
const { adcode, name, data } = params.data;
// 返回自定义的tooltip内容
return `adcode: ${adcode}<br>name: ${name}<br>data: ${data}`;
}
},
},
visualMap: {
show: true,
min: 0,
max: 100,
left: 'left',
top: 'bottom',
text: ['高', '低'], // 文本,默认为数值文本
calculable: true,
seriesIndex: [0],
inRange: {
color: ['#00467F', '#A5CC82'] // 蓝绿
}
},
geo: {
map: mapName,
roam: true,
select: false,
// zoom: 1.6,
// layoutCenter: ['45%', '70%'],
// layoutSize: 750,
// 图形上的文本标签,可用于说明图形的一些数据信息,比如值,名称等。
selectedMode: 'single',
label: {
show: true
},
emphasis: {
itemStyle: {
areaColor: '#389BB7',
borderColor: '#389BB7',
borderWidth: 0
},
label: {
fontSize: 14,
},
}
},
series: [
// 数据
{
type: 'map',
map: mapName,
roam: true,
geoIndex: 0,
select: false,
data: mapData
},
{
name: '散点',
type: 'scatter',
coordinateSystem: 'geo',
data: mapData,
itemStyle: {
color: '#05C3F9'
}
},
{
name: '点',
type: 'scatter',
coordinateSystem: 'geo',
symbol: 'pin', //气泡
symbolSize: function (val: any) {
if (val) {
return val[2] / 4 + 20;
}
},
label: {
show: true,
formatter: function (params: any) {
return params.data.data || 0;
},
color: '#fff',
fontSize: 9,
},
itemStyle: {
color: '#F62157', //标志颜色
},
zlevel: 6,
data: mapData,
},
{
name: 'Top 5',
type: 'effectScatter',
coordinateSystem: 'geo',
data: mapData.map((item: { data: number }) => {
if (item.data > 60) return item
}),
symbolSize: 15,
showEffectOn: 'render',
rippleEffect: {
brushType: 'stroke'
},
label: {
formatter: '{b}',
position: 'right',
show: true
},
itemStyle: {
color: 'yellow',
shadowBlur: 10,
shadowColor: 'yellow'
},
zlevel: 1
},
]
}
}

const renderMapEcharts = async (mapName: string) => {
const mapJson = await getMapJson(mapName)
registerMap(mapName, mapJson);
const mapdata = mapJson.features.map((item: { properties: any }) => {
const data = (Math.random() * 80 + 20).toFixed(0) // 20-80随机数
const tempValue = item.properties.center ? [...item.properties.center, data] : item.properties.center
return {
name: item.properties.name,
value: tempValue, // 中心点经纬度
adcode: item.properties.adcode, // 区域编码
level: item.properties.level, // 层级
data // 模拟数据
}
});
mapOption.value = setOptions(mapName, mapdata)
}

renderMapEcharts('100000_full') // 初始化绘制中国地图

// 点击下砖
const handleClick = (param: any) => {
// 只有点击地图才触发
if (param.seriesType !== 'map') return
const { adcode, level } = param.data
const mapName = level === 'district' ? adcode : adcode + '_full'
// 防止最后一个层级被重复点击,返回上一级出错
if (mapList.value[mapList.value.length - 1] === mapName) {
return notification.warning({ content: '已经是最下层了', duration: 1000 })
}
mapList.value.push(mapName)
renderMapEcharts(mapName)
}

// 点击返回上一级地图
const goBack = () => {
const mapName = mapList.value[mapList.value.length - 2] || '100000_full'
mapList.value.pop()
renderMapEcharts(mapName)
}
</script>



作者:树深遇鹿
来源:juejin.cn/post/7371641968600383540
收起阅读 »

threejs渲染高级感可视化风力发电车模型

web
本文使用threejs开发一款风力发电机物联可视化系统,包含着色器效果、动画、补间动画和开发过程中使用模型材质遇到的问题,内含大量gif效果图, 视频讲解及源码见文末 技术栈 three.js 0.165.0 vite 4.3.2 nodej...
继续阅读 »

本文使用threejs开发一款风力发电机物联可视化系统,包含着色器效果、动画、补间动画和开发过程中使用模型材质遇到的问题,内含大量gif效果图,



视频讲解及源码见文末



技术栈



  • three.js 0.165.0

  • vite 4.3.2

  • nodejs v18.19.0


效果图


一镜到底动画


一镜到底 (1).gif


切割动画


切割动画.gif


线稿动画


线稿动画.gif


外壳透明度动画


外壳透明度动画.gif


展开齿轮动画


展开齿轮动画.gif


发光线条动画


发光线条.gif


代码及功能介绍


着色器


文中用到一个着色器,就是给模型增加光感的动态光影


创建顶点着色器 vertexShader:


varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

创建片元着色器 vertexShader:


varying vec2 vUv;
uniform vec2 u_center; // 添加这一行

void main() {
// 泡泡颜色
vec3 bubbleColor = vec3(0.9, 0.9, 0.9); // 乳白色
// 泡泡中心位置
vec2 center = u_center;
// 计算当前像素到泡泡中心的距离
float distanceToCenter = distance(vUv, center);
// 计算透明度,可以根据实际需要调整
float alpha = smoothstep(0.1, 0.0, distanceToCenter);

gl_FragColor = vec4(bubbleColor, alpha);

创建着色器材质 bubbleMaterial


export const bubbleMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true, // 开启透明
depthTest: true, // 开启深度测试
depthWrite: false, // 不写入深度缓冲
uniforms: {
u_center: { value: new THREE.Vector2(0.3, 0.3) } // 添加这一行
},
});


从代码中可以看到 uniform声明了一个变量u_center,目的是为了在render方法中动态修改中心位置,从而实现动态光效的效果,


具体引用 render 方法中


 // 更新中心位置(例如,每一帧都改变)  
let t = performance.now() * 0.001;
bubbleMaterial.uniforms.u_center.value.x = Math.sin(t) * 0.5 + 0.5; // x 位置基于时间变化
bubbleMaterial.uniforms.u_center.value.y = Math.cos(t) * 0.5 + 0.5; // y 位置基于时间变化

官网案例 # Uniform,详细介绍了uniform的使用方法,支持通过变量对着色器材质中的属性进行改变


光影着色器.gif


从模型上可能看不出什么,下面的图是在一个圆球上加的这个效果


光影着色器-球体.gif


着色器中有几个参数可以自定义也可以自己修改, float alpha = smoothstep(0.6, 0.0, distanceToCenter);中的smoothstep 是一个常用的函数,用于在两个值之间进行平滑插值。具体来说,smoothstep(edge0, edge1, x) 函数会计算 x 在 edge0 和 edge1 之间的平滑过渡值。当 x 小于 edge0 时,返回值为 0;当 x 大于 edge1 时,返回值为 1;而当 x 在 edge0 和 edge1 之间时,它返回一个在 0 和 1 之间的平滑过渡值。


切割动画


切割动画使用的是数学库平面THREE.Plane和属性 constant,通过修改constant值即可实现动画,从normal法向量起至constant的距离为可展示内容。



从原点到平面的有符号距离。 默认值为 0.



constant取模型的box3包围盒的min值,至max值做补间动画,以下是代码示意


const wind = windGltf.scene
const boxInfo = wind.userData.box3Info;

const max = boxInfo.worldPosition.z + boxInfo.max.z
const min = boxInfo.worldPosition.z + boxInfo.min.z

let tween = new TWEEN.Tween({ d: min - 0.2 })
.to({ d: max + 0.1 }, 1000 * 2)
.start()
.onUpdate(({ d }) => {
clippingPlane.constant = d
})

详看切割效果图


切割动画.gif


图中添加了切割线的辅助线,可以通过右侧的操作面板显示或隐藏。


模型材质需要注意的问题


由于齿轮在风车的内容部,并且风车模型开启了transparent=true,那么计算透明度深度就会出现问题,首先要设置 depthWrite = true,开启深度缓存区,renderOrder = -1



这个值将使得scene graph(场景图)中默认的的渲染顺序被覆盖, 即使不透明对象和透明对象保持独立顺序。 渲染顺序是由低到高来排序的,默认值为0



threejs的透明材质渲染和不透明材质渲染的时候,会互相影响,而调整renderOrder顺序则可以让透明对象和不透明对象相对独立的渲染。


depthWrite对比


depthwrite对比.jpeg


renderOrder 对比


renderOrder 对比.jpeg


自定义动画贝塞尔曲线


众所周知,贝塞尔曲线通常用于调整关键帧动画,创建平滑的、曲线的运动路径。本文中使用的tweenjs就内置了众多的运动曲线easing(easingFunction?: EasingFunction): this;类型,虽然有很多内置,但是毕竟需求是无限的,接下来介绍的方法就是可以自己设置动画的贝塞尔曲线,来控制动画的执行曲线。


具体使用


// 使用示例
const controlPoints = [ { x: 0 }, { x: 0.5 }, { x: 2 }, { x: 1 }];
const cubicBezier = new CubicBezier(controlPoints[0], controlPoints[1], controlPoints[2], controlPoints[3]);

let tween = new TWEEN.Tween(edgeLineGr0up.scale)
.to(windGltf.scene.scale.clone().set(1, 1, 1), 1000 * 2)
.easing((t) => {
return cubicBezier.get(t).x
})
.start()
.onComplete(() => {
lineOpacityAction(0.3)
res({ tween })
})

在tween的easing的回调中添加一个方法,方法中调用了cubicBezier,下面就介绍一下这个方法


源码


[p0] – 起点  
[p1] – 第一个控制点
[p2] – 第二个控制点
[p3] – 终点

export class CubicBezier {
private p0: { x: number; };
private p1: { x: number; };
private p2: { x: number; };
private p3: { x: number; };

constructor(p0: { x: number; }, p1: { x: number; }, p2: { x: number; }, p3: { x: number; }) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.p3 = p3;
}

get(t: number): { x: number; } {
const p0 = this.p0;
const p1 = this.p1;
const p2 = this.p2;
const p3 = this.p3;

const mt = 1 - t;
const mt2 = mt * mt;
const mt3 = mt2 * mt;
const t2 = t * t;
const t3 = t2 * t;

const x = mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x;

return { x };
}
}


CubicBezier支持get方法,通过四个关键点位信息,绘制三次贝塞尔曲线,参数t在0到1之间变化,当t从0变化到1时,曲线上的点从p0平滑地过渡到p3


mt = 1 - t;:这是t的补数(1减去t)。
mt2 = mt * mt; 和 mt3 = mt2 * mt;:计算mt的平方和立方。
t2 = t * t; 和 t3 = t2 * t;:计算t的平方和立方。


这是通过取四个点的x坐标的加权和来完成的,其中权重是基于t的幂的。具体来说,p0的权重是(1-t)^3p1的权重是3 * (1-t)^2 * tp2的权重是3 * (1-t) * t^2,而p3的权重是t^3


{ x: 0 },{ x: 0.5 },{ x: 2 },{ x: 1 } 这组数据形成的曲线效果是由start参数到end的两倍参数再到end参数


具体效果如下


贝塞尔曲线.gif


齿轮


齿轮动画


模型中自带动画


齿轮动画数据.jpeg


源码中有一整套的动画播放类方法,HandleAnimation,其中功能包含播放训话动画,切换动画,播放一次动画,绘制骨骼,镜头跟随等功能。


具体使用方法:


   // 齿轮动画
/**
*
* @param model 动画模型
* @param animations 动画合集
*/

motorAnimation = new HandleAnimation(motorGltf.scene, motorGltf.animations)
// 播放动画 take 001 是默认动画名称
motorAnimation.play('Take 001')

在render中调用


motorAnimation && motorAnimation.upDate()

齿轮展开(补间动画)


补间动画在齿轮展开时调用,使用的tweenjs,这里讲一下定位运动后的模型位置,使用# 变换控制器(TransformControls),代码中有封装好的完整的使用方法,在TransformControls.ts中,包含同时存在轨道控制器时与变换控制器对场景操作冲突时的处理。


使用方法:


/**
* @param mesh 受控模型
* @param draggingChangedCallback 操控回调
*/

TransformControls(mesh, ()=>{
console.log(mesh.position)
})

齿轮展开定位.jpeg


齿轮发光


发光效果方法封装在utls/index.ts中的unreal方法,使用的是threejs提供的虚幻发光通道RenderPass,UnrealBloomPass,以及合成器EffectComposer,方法接受参数如下



// params 默认参数
const createParams = {
threshold: 0,
strength: 0.972, // 强度
radius: 0.21,// 半径
exposure: 1.55 // 扩散
};

/**
*
* @param scene 渲染场景
* @param camera 镜头
* @param renderer 渲染器
* @param width 需要发光位置的宽度
* @param height 发光位置的高度
* @param params 发光参数
* @returns
*/


调用方法如下:



const { finalComposer: F,
bloomComposer: B,
renderScene: R, bloomPass: BP } = unreal(scene, camera, renderer, width, height, params)
finalComposer = F
bloomComposer = B
renderScene = R
bloomPass = BP
bloomPass.threshold = 0


除了调用方法还有一些需要调整的地方,比如发光时模型什么材质,又或者不发光时又是什么材质,这里需要单独定义,并在render渲染函数中调用


 if (guiParams.isLight) {
if (bloomComposer) {
scene.traverse(darkenNonBloomed.bind(this));
bloomComposer.render();
}
if (finalComposer) {
scene.traverse(restoreMaterial.bind(this));
finalComposer.render();
}
}

scene.traverse的回调中,检验模型是否为发光体,再进行材质的更换,这里用的标识是 object.userData.isLighttrue时,判定该物体为发光物体。其他物体则不发光


回调方法


function darkenNonBloomed(obj: THREE.Mesh) {
if (bloomLayer) {
if (!obj.userData.isLight && bloomLayer.test(obj.layers) === false) {
materials[obj.uuid] = obj.material;
obj.material = darkMaterial;
}
}

}

function restoreMaterial(obj: THREE.Mesh) {
if (materials[obj.uuid]) {
obj.material = materials[obj.uuid];
// 用于删除没必要的渲染
delete materials[obj.uuid];
}
}


再场景的右上角我们新增了几个参数,用来调整线条的发光效果,下面通过动图看一下,图片有点大,请耐心等待加载


调试发光效果.gif


好啦,本篇文章到此,如看源码有不明白的地方,可私信~


最近正在筹备工具库,以上可视化常用的方法都将涵盖在里面


历史文章


three.js——商场楼宇室内导航系统 内附源码


three.js——可视化高级涡轮效果+警报效果 内附源码


高德地图巡航功能 内附源码


three.js——3d塔防游戏 内附源码


three.js+物理引擎——跨越障碍的汽车 可操作 可演示


百度地图——如何计算地球任意两点之间距离 内附源码


threejs——可视化地球可操作可定位


three.js 专栏


源码及讲解



源码 http://www.aspiringcode.com/content?id=…


体验地址:display.aspiringcode.com:8888/html/171422…


B站讲解地址:【threejs渲染高级感可视化风力发电车模型】 http://www.bilibili.com/video/BV1gT…


作者:孙_华鹏
来源:juejin.cn/post/7379906492038889512
收起阅读 »

前端太卷了,不玩了,写写node.js全栈涨工资,赶紧学起来吧!!!!!

web
如果你感觉到累了,卷不动了,那就来看这篇文章吧,写写全栈,涨涨工资,吹吹牛皮!人生得意须尽欢,不要只想忙搬砖!首先聊下node.js的优缺点和应用场景Node.js的优点和应用场景Node.js作为后端开发的选择具有许多优点,以下是其中一些:高性能: ...
继续阅读 »

如果你感觉到累了,卷不动了,那就来看这篇文章吧,写写全栈,涨涨工资,吹吹牛皮!人生得意须尽欢,不要只想忙搬砖!

首先聊下node.js的优缺点和应用场景

Node.js的优点和应用场景

Node.js作为后端开发的选择具有许多优点,以下是其中一些:

  1. 高性能: Node.js采用了事件驱动、非阻塞I/O模型,使得它能够处理大量并发请求而不会阻塞线程,从而具有出色的性能表现。
  2. 轻量级和高效: Node.js的设计简洁而轻量,启动速度快,内存占用低,适合构建轻量级、高效的应用程序。
  3. JavaScript全栈: 使用Node.js,开发者可以使用同一种语言(JavaScript)进行前后端开发,简化了开发人员的学习成本和代码维护成本。
  4. 丰富的生态系统: Node.js拥有丰富的第三方模块和库,可以轻松集成各种功能和服务,提高开发效率。
  5. 可扩展性: Node.js具有良好的可扩展性,可以通过添加更多的服务器实例来横向扩展应用程序,满足不断增长的用户需求。
  6. 实时应用: 由于Node.js对于事件驱动和非阻塞I/O的支持,它非常适合构建实时应用,如即时通讯、在线游戏、实时分析等。
  7. 微服务架构: Node.js可以作为微服务架构中的一个服务组件,通过轻量级的设计和快速的响应能力,使得微服务之间的通信更加高效。
  8. 数据流处理: Node.js适合处理大量的数据流,例如文件操作、网络流量分析等,可以通过流式处理来有效地管理和处理数据。

应用场景包括但不限于:

  1. Web应用程序: 开发基于Node.js的Web应用程序,如社交网络、电子商务平台、博客、内容管理系统等。
  2. API服务: 使用Node.js构建RESTful API服务,为移动应用、前端应用提供数据接口。
  3. 实时应用: 构建实时应用程序,如聊天应用、在线游戏、实时地图等,利用Node.js的事件驱动和非阻塞I/O模型实现高效的实时通讯。
  4. 数据流处理: 使用Node.js处理大量的数据流,例如日志处理、实时监控、数据分析等。
  5. 微服务架构: 将Node.js作为微服务架构中的一个服务组件,通过轻量级的设计和快速的响应能力,实现服务之间的高效通信。

总的来说,Node.js作为一种高性能、轻量级的后端开发工具,适用于各种类型的应用场景,尤其在需要处理大量并发请求和实时通讯的应用中表现突出。

node.js的局限性

尽管Node.js在许多方面都表现出色,但它也有一些局限性和适用场景的限制。以下是一些Node.js的局限性:

  1. 单线程阻塞: 虽然Node.js采用了非阻塞I/O的模型,但在处理CPU密集型任务时,单线程的特性可能导致性能瓶颈。由于Node.js是单线程的,处理阻塞操作(如大量计算或长时间的同步操作)会影响整个应用程序的响应性。
  2. 回调地狱(Callback Hell): 在复杂的异步操作中,嵌套的回调函数可能导致代码难以理解和维护,这被称为“回调地狱”问题。虽然可以使用Promise、async/await等来缓解这个问题,但在某些情况下仍可能存在。
  3. 相对较小的标准库: Node.js的标准库相对较小,相比于其他后端语言,需要依赖第三方模块来实现一些常见的功能。这可能需要花费额外的时间来选择、学习和整合这些模块。
  4. 不适合密集型计算: 由于Node.js是单线程的,不适合用于处理大量的计算密集型任务。如果应用程序主要依赖于大量的数学计算或复杂的数据处理,其他多线程的语言可能更合适。
  5. Callback错误处理: 在回调模式下,错误处理可能变得繁琐,需要在每个回调中检查错误对象。这使得开发者需要更加小心地处理错误,以确保它们不会被忽略。
  6. 相对较新的技术栈: 相较于一些传统的后端技术栈,Node.js是相对较新的技术,一些企业可能仍然更倾向于使用更成熟的技术。
  7. 不适合长连接: 对于长连接的应用场景,如传统的即时通讯(IM)系统,Node.js的单线程模型可能不是最佳选择,因为它会导致长时间占用一个线程。

尽管有这些局限性,但Node.js在许多应用场景下仍然是一个强大且高效的工具。选择使用Node.js还是其他后端技术应该根据具体项目的需求、团队的技术栈和开发者的经验来做出。

node.js常用的几种主流框架

Node.js是一个非常灵活的JavaScript运行时环境,它可以用于构建各种类型的应用程序,从简单的命令行工具到大型的网络应用程序。以下是一些常用的Node.js框架:

  1. Express.js:Express.js是Node.js最流行的Web应用程序框架之一,它提供了一组强大的功能,使得构建Web应用变得更加简单和快速。Express.js具有路由、中间件、模板引擎等功能,可以满足大多数Web应用的需求。
  2. Koa.js:Koa.js是由Express.js原班人马打造的下一代Node.js Web框架,它使用了ES6的新特性,如async/await,使得编写异步代码更加简洁。Koa.js更加轻量级和灵活,它提供了更强大的中间件功能,可以更方便地实现定制化的功能。
  3. Nest.js:Nest.js是一个用于构建高效、可扩展的服务器端应用程序的渐进式Node.js框架。它基于Express.js,但引入了许多现代化的概念,如依赖注入、模块化、类型检查等,使得构建复杂应用变得更加简单。
  4. Hapi.js:Hapi.js是一个专注于提供配置简单、可测试性强的Web服务器框架。它提供了一系列的插件,可以轻松地扩展其功能,同时具有强大的路由、验证、缓存等功能,适用于构建大型和高可靠性的Web应用程序。
  5. Meteor.js:Meteor.js是一个全栈JavaScript框架,它可以同时构建客户端和服务器端的应用程序。Meteor.js提供了一整套的工具和库,包括数据库访问、实时数据同步、用户认证等功能,使得构建实时Web应用变得更加简单和快速。
  6. Sails.js:Sails.js是一个基于Express.js的MVC框架,它提供了类似于Ruby on Rails的开发体验,使得构建数据驱动的Web应用变得更加简单。Sails.js具有自动生成API、蓝图路由、数据关联等功能,适用于构建RESTful API和实时Web应用。

Express框架:实践与技术探索

1. Express框架简介:

Express是一个轻量级且灵活的Node.js Web应用程序框架,它提供了一组简洁而强大的工具,帮助开发者快速构建Web应用。Express的核心理念是中间件,通过中间件可以处理HTTP请求、响应以及应用程序的逻辑。


2. 基础搭建与路由:

在开始实践之前,首先需要搭建Express应用程序的基础结构。通过使用express-generator工具或手动创建package.jsonapp.js文件,可以快速启动一个Express项目。接下来,我们将学习如何定义路由以及如何处理HTTP请求和响应。

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

app.get('/', (req, res) => {
res.send('Hello World!');
});

app.listen(3000, () => {
console.log('Express app listening on port 3000');
});

3. 中间件:

Express中间件是一个函数,它可以访问请求对象(req)、响应对象(res)以及应用程序的下一个中间件函数(通常命名为next)。中间件函数可以用来执行任何代码,修改请求和响应对象,以及终止请求-响应周期。

app.use((req, res, next) => {
console.log('Time:', Date.now());
next();
});

4. 模板引擎与视图:

Express框架允许使用各种模板引擎来生成动态HTML内容。常用的模板引擎包括EJS、Pug和Handlebars。通过配置模板引擎,可以将动态数据嵌入到静态模板中,以生成最终的HTML页面。

app.set('view engine', 'ejs');

5. 数据库集成与ORM:

在实际应用中,数据库是不可或缺的一部分。Express框架与各种数据库集成良好,可以通过ORM(对象关系映射)工具来简化数据库操作。常用的ORM工具包括Sequelize、Mongoose等,它们可以帮助开发者更轻松地进行数据模型定义、查询和操作。


6. RESTful API设计与实现:

Express框架非常适合构建RESTful API。通过定义不同的HTTP动词和路由,可以实现资源的创建、读取、更新和删除操作。此外,Express还提供了一系列中间件来处理请求体、响应格式等,使得构建API变得更加简单。

app.get('/api/users', (req, res) => {
// 获取所有用户信息
});

app.post('/api/users', (req, res) => {
// 创建新用户
});

7. 实践案例:

为了更好地理解Express框架的实践,我们将以一个简单的博客应用为例。在这个应用中,我们可以拓展一下用户的注册、登录、文章的创建和展示等功能,并且结合数据库和RESTful API设计。在这个示例中,我们将使用MongoDB作为数据库,并使用Mongoose作为MongoDB的对象建模工具。首先,确保您已经安装了Node.js``和MongoDB,并创建了一个名为blogApp的文件夹来存放我们的项目。

  1. 首先,在项目文件夹中初始化npm,并安装Express、Mongoose和body-parser依赖:
npm init -y
npm install express mongoose body-parser
  1. 在项目文件夹中创建app.js文件,并编写以下代码:
// 导入所需的模块
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');

// 连接MongoDB数据库
mongoose.connect('mongodb://localhost:27017/blog', { useNewUrlParser: true, useUnifiedTopology: true });
const db = mongoose.connection;

// 检测数据库连接状态
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
db.once('open', function() {
console.log('Connected to MongoDB');
});

// 创建Express应用
const app = express();

// 使用body-parser中间件解析请求体
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// 定义用户模型
const User = mongoose.model('User', new mongoose.Schema({
username: String,
password: String
}));

// 注册用户
app.post('/api/register', async (req, res) => {
try {
const { username, password } = req.body;
const user = await User.create({ username, password });
res.json({ success: true, message: 'User registered successfully', user });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

// 用户登录
app.post('/api/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = await User.findOne({ username, password });
if (user) {
res.json({ success: true, message: 'User logged in successfully', user });
} else {
res.status(401).json({ success: false, message: 'Invalid username or password' });
}
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

// 启动Express服务器
const port = 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});

以上代码实现了用户注册和登录的功能,使用了MongoDB作为数据库存储用户信息,并提供了RESTful风格的API接口。

您可以通过以下命令启动服务器:

node app.js
  1. 接下来,我们添加文章模型和相关的路由来实现文章的创建和展示功能。在app.js文件中添加以下代码:
// 定义文章模型
const Article = mongoose.model('Article', new mongoose.Schema({
title: String,
content: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
}));

// 创建文章
app.post('/api/articles', async (req, res) => {
try {
const { title, content, author } = req.body;
const article = await Article.create({ title, content, author });
res.json({ success: true, message: 'Article created successfully', article });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

// 获取所有文章
app.get('/api/articles', async (req, res) => {
try {
const articles = await Article.find().populate('author', 'username');
res.json({ success: true, articles });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

以上代码实现了创建文章和获取所有文章的功能,每篇文章都与特定的作者相关联。

现在,您可以使用POST请求来创建新的用户和文章,使用GET请求来获取所有文章。例如:

  • 注册新用户:发送POST请求到/api/register,传递usernamepassword字段。
  • 用户登录:发送POST请求到/api/login,传递usernamepassword字段。
  • 创建新文章:发送POST请求到/api/articles,传递titlecontentauthor字段(注意,author字段应该是已注册用户的ID)。
  • 获取所有文章:发送GET请求到/api/articles

这个示例演示了如何使用Express框架结合MongoDB实现一个简单的博客应用,并提供了RESTful API接口。可以根据需求扩展和定制这个应用,例如添加用户身份验证、文章编辑和删除功能等。

看完后是不是觉得后端(CRUD)很简单,没错!就是这么简单!喜欢的小伙伴给个点赞加收藏,码字不易!


作者:为了WLB努力
来源:juejin.cn/post/7343138637971734569
收起阅读 »

8年前端,那就聊聊被裁的感悟吧!!

web
前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。 另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的...
继续阅读 »

前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。
另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的际遇


我的经历


第一家公司


第一家公司说来也巧,本来是准备入职一家外包的,在杭州和同学吃个饭,接到了面试通知,一看地址就在楼上,上去一共就3轮面试,不到2个小时直接给了offer。有些东西真的就是命中注定


第二家公司


第二家公司我入职以后挖了上家公司诸多墙角,我一共挖了6个前端,2个后端。拯救朋友们于水深火热之中。





我本以为我能开启美好的新生活,结果第二年就传来我父亲重病的噩耗 肺癌晚期,我学习了大量的肺癌知识什么小细胞,非小细胞,基因检测呀等等。。。





可是最后还是没有挽留住他的生命,我记得我俩在最后一次去武汉的时候,睡在一起,他给我说了很多。


他说:治不好就算了,只是没能看到自己的孙子有些可惜罢了。

他说:我这一辈碌碌无为,没给你带来多么优越的条件,结婚、买房、工作都没给到任何帮助,唯一让我感到欣慰的是你那么努力,比我强多了,家里邻居很多都眼馋你呢。
他说:你小孩的名字想好了吗?你媳妇真是个孝顺的孩子,性格也好,心地善良,你要好好对待她。

他说了很多。。。我都快忘了他说了啥了,我不想忘来着,可是可是,想起来就又好难过。


这只是我人生历程的一部分,我把这些讲出来,是为了让大家明白,你现在所经历的困苦其实没有那么严重,人在逆境之中会放大自己的困难,以博得同情。所以现在很多人给我倒苦水的时候,我总有点不屑一顾的感觉,并不是我有多强,我只是觉得都能过去。


在灰暗的时候,工作总是心不在焉,情绪莫名冲动,我和领导吵过架,和ui妹妹撕破脸,导致人家天天投诉我。我leader说我态度极其嚣张,我说你再多说一句,我干死你所以不裁我裁谁


我的人生感悟


我时常以我爸的角度换位思考,我在得知这个消息后我该咋办?是积极面对,还是放弃治疗?可是所有的都是在假设的前提之下,一切不可为真。只有在其中的才最能明白其中的感受。
那一年我看着他积极想活着的毅力,也看到了他身体日渐消瘦的无奈,无奈之余还要应付各种亲戚的嘘寒问暖


我现在很能明白《天道》中那段,丁元英说的如果为了孝顺的名声,让父亲痛苦没有尊严地活着,还不如让父亲走了。 的意思了。在他昏迷不醒的时候,大小便失禁的时候,真不如有尊严的走了。


我其实已经预感到自己要被裁,我原本是挺担心的,可是后来想想父亲的话,我总结成一句话圆滑对事,诚以待人。 这句话看上去前后矛盾,无外乎俩个观点。


圆滑对事的意思是:就是要学会嘴甜,事嘛能少干就少干,能干几分是几分,累的是别人,爽的是自己,在规则中寻求最大的自我利益化。


诚以待人的意思是:圆滑归圆滑,不能对谁都圆滑,你得有把事情办的很好的能力,你需要给真正需要的人创造价值,而不是为了给压榨者提供以自我健康为代价的价值。



用现在最流行的词来说就是「佛系」。


什么叫活明白了,通常被理解为不争不抢,得之淡然、失之泰然、顺其自然的一种心理状态。


活明白的人一般知道自己要什么样的生活,他们不世故、不圆滑,坦荡的、磊落的做自己应该做的事儿。他们与社会上潜规则里的不良之风格格不入,却不相互抵触,甚至受到局中人的青睐与欣赏。


活明白的人看着更为洒脱,得不张扬,失不气馁,心态随和、随遇而安。


不过,还有一种活明白的人,不被多数人所接受。他们玩世不恭、好吃懒做,把所有一切交给命运去背锅。这种人极度自我,没有什么可以超越他自己的利益,无法想象这种活法,简直就是在浪费六道轮回的名额。


总之,有的人活明白了,是调整自己的心态,维护社会的稳定和安宁。有的人活明白了,是以自我为中心,一边依赖着社会救济,一边责备社会龌蹉。


所以,活明白的人也分善与恶,同样是一种积极向善,另一种是消极向恶,二者同出而异名。



我对生活的态度


离职的第一个月,便独自一人去了南京,杭州,长沙,武汉,孝感。我见了很多老朋友,听听他们发发牢骚,然后找一些小众的景点完成探险。


在南京看了看中医,在杭州露营看了看日落,在长沙夜爬了岳麓山,在武汉坐了超级大摆锤,在孝感去了无名矿坑并在一个奶奶家蹭了中午饭。


我的感受极其良好,我体验了前所未有生活态度,我热情待人,嘻嘻笑笑,我站在山顶敞怀吹风,在无尽的树林中悠然自得,治愈我不少的失落情绪。我将继续为生活的不易奔波,也将继续热爱生活,还会心怀感恩对待他人,也会圆滑处事 事事佛系。


背景1.png


图层 1.png


IMG_6214.JPG


IMG_6198.JPG


IMG_6279.JPG


可能能解决你的问题


要不要和家里人说


我屏蔽了家里人,把负面情绪隐藏,避免波及母亲本就脆弱的内心世界,我还骗她说公司今年不挣钱,提前让我们放假,只给基础工资。如果你家境殷实,家庭和睦,我建议大方的说,这样你和父母又多了一个可以聊的话题,不妨和他们多多交流,耐心一些。


裁员,真不是你的问题


请记住,你没有任何问题,你被裁员是公司的损失,你不需要为此担责,你需要做的是让自己更强,不管是心理、身体还是技术,你得让自己变得精彩,别虚度了这如花般的时光。可能你懒,可能也没什么规划,那就想到啥就做啥好了,可能前几次需要鼓足干劲,后面就会发现轻而易举。


如何度过很丧的阶段


沮丧需要一个发泄的出口,可以保持运动习惯,比如日常爬楼梯、跑步等,一场大汗淋漓后,又是一个打满鸡血积极向上的你。

不要总在家待着,要想办法出门,多建立与社会的联系,多和朋友吹吹牛逼,别把脸面看的那么重要,死皮赖脸反而是一种讨人喜欢的性格。



不管环境怎样,希望你始终向前,披荆斩棘

如果你也正在经历这个阶段,希望你放平心态,积极应对

如果你也在人生的至暗时刻,也请不要彷徨,时间总会治愈一切

不妨试试大胆一点,生活给的惊喜也同样不少

我在一个冬天的夜晚写着文字,希望能对你有些帮助


作者:顾昂_
来源:juejin.cn/post/7331657679012380722
收起阅读 »

Electron实现静默打印小票

web
Electron实现静默打印小票 静默打印流程 1.渲染进程通知主进程打印 //渲染进程 data是打印需要的数据 window.electron.ipcRenderer.send('handlePrint', data) 2.主进程接收消息,创建打印页面...
继续阅读 »

Electron实现静默打印小票


静默打印流程


09c00eb5-f171-4090-a178-37e149d1d0f7.png


1.渲染进程通知主进程打印


//渲染进程 data是打印需要的数据
window.electron.ipcRenderer.send('handlePrint', data)

2.主进程接收消息,创建打印页面


//main.ts
/* 打印页面 */
let printWindow: BrowserWindow | undefined
/**
* @Author: yaoyaolei
* @Date: 2024-06-07 09:27:22
* @LastEditors: yaoyaolei
* @description: 创建打印页面
*/

const createPrintWindow = () => {
return new Promise<void>((resolve) => {
printWindow = new BrowserWindow({
...BASE_WINDOW_CONFIG,
title: 'printWindow',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: true,
contextIsolation: false
}
})

printWindow.on('ready-to-show', () => {
//打印页面创建完成后不需要显示,测试时可以调用show查看页面样式(下面有我处理的样式图片)
// printWindow?.show()
resolve()
})

printWindow.webContents.setWindowOpenHandler((details: { url: string }) => {
shell.openExternal(details.url)
return { action: 'deny' }
})

if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
printWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/print.html`)
} else {
printWindow.loadFile(join(__dirname, `../renderer/print.html`))
}
})
}

ipcMain.on('handlePrint', (_, obj) => {
//主进程接受渲染进程消息,向打印页面传递数据
if (printWindow) {
printWindow!.webContents.send('data', obj)
} else {
createPrintWindow().then(() => {
printWindow!.webContents.send('data', obj)
})
}
})

3.打印页面接收消息,拿到数据渲染页面完成后通知主进程开始打印


<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>打印</title>
<style>
</style>
</head>

<body>

</body>
<script>
window.electron.ipcRenderer.on('data', (_, obj) => {
//这里是接受的消息,处理完成后将html片段放在body里面完成后就可以开始打印了
//样式可以写在style里,也可以内联
console.log('event, data: ', obj);
//这里自由发挥
document.body.innerHTML = '处理的数据'
//通知主进程开始打印
window.electron.ipcRenderer.send('startPrint')
})
</script>
</html>



这个是我处理完的数据样式,这个就是print.html
9f17ea7e-3f83-408f-a780-05d50da305de.png
微信图片_20240609102325.jpg



4,5.主进程接收消息开始打印,并且通知渲染进程打印状态


ipcMain.on('startPrint', () => {
printWindow!.webContents.print(
{
silent: true,
margins: { marginType: 'none' }
},
(success) => {
//通知渲染进程打印状态
if (success) {
mainWindow.webContents.send('printStatus', 'success')
} else {
mainWindow.webContents.send('printStatus', 'error')
}
}
)
})

aa.jpg



完毕~



作者:彷徨的耗子
来源:juejin.cn/post/7377645747448365091
收起阅读 »

几行代码,优雅的避免接口重复请求!同事都说好!

web
背景简介 我们日常开发中,经常会遇到点击一个按钮或者进行搜索时,请求接口的需求。 如果我们不做优化,连续点击按钮或者进行搜索,接口会重复请求。 首先,这会导致性能浪费!最重要的,如果接口响应比较慢,此时,我们在做其他操作会有一系列bug! 那么,我们该如...
继续阅读 »

背景简介


我们日常开发中,经常会遇到点击一个按钮或者进行搜索时,请求接口的需求。


如果我们不做优化,连续点击按钮或者进行搜索,接口会重复请求。




首先,这会导致性能浪费!最重要的,如果接口响应比较慢,此时,我们在做其他操作会有一系列bug!



那么,我们该如何规避这种问题呢?


如何避免接口重复请求


防抖节流方式(不推荐)


使用防抖节流方式避免重复操作是前端的老传统了,不多介绍了


防抖实现


<template>
<div>
<button @click="debouncedFetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const timeoutId = ref(null);

function debounce(fn, delay) {
return function(...args) {
if (timeoutId.value) clearTimeout(timeoutId.value);
timeoutId.value = setTimeout(() => {
fn(...args);
}, delay);
};
}

function fetchData() {
axios.get('http://api/gcshi) // 使用示例API
.then(response => {
console.log(response.data);
})
}

const debouncedFetchData = debounce(fetchData, 300);
</script>

防抖(Debounce)



  • 在setup函数中,定义了timeoutId用于存储定时器ID。

  • debounce函数创建了一个闭包,清除之前的定时器并设置新的定时器,只有在延迟时间内没有新调用时才执行fetchData。

  • debouncedFetchData是防抖后的函数,在按钮点击时调用。


节流实现


<template>
<div>
<button @click="throttledFetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const lastCall = ref(0);

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

function fetchData() {
axios.get('http://api/gcshi') //
.then(response => {
console.log(response.data);
})
}

const throttledFetchData = throttle(fetchData, 1000);
</script>

节流(Throttle)



  • 在setup函数中,定义了lastCall用于存储上次调用的时间戳。

  • throttle函数创建了一个闭包,检查当前时间与上次调用时间的差值,只有大于设定的延迟时间时才执行fetchData。

  • throttledFetchData是节流后的函数,在按钮点击时调用。


节流防抖这种方式感觉用在这里不是很丝滑,代码成本也比较高,因此,很不推荐!


请求锁定(加laoding状态)


请求锁定非常好理解,设置一个laoding状态,如果第一个接口处于laoding中,那么,我们不执行任何逻辑!


<template>
<div>
<button @click="fetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const laoding = ref(false);

function fetchData() {
// 接口请求中,直接返回,避免重复请求
if(laoding.value) return
laoding.value = true
axios.get('http://api/gcshi') //
.then(response => {
laoding.value = fasle
})
}

const throttledFetchData = throttle(fetchData, 1000);
</script>


这种方式简单粗暴,十分好用!


但是也有弊端,比如我搜索A后,接口请求中;但我此时突然想搜B,就不会生效了,因为请求A还没响应



因此,请求锁定这种方式无法取消原先的请求,只能等待一个请求执行完才能继续请求。


axios.CancelToken取消重复请求


基本用法


axios其实内置了一个取消重复请求的方法:axios.CancelToken,我们可以利用axios.CancelToken来取消重复的请求,爆好用!


首先,我们要知道,aixos有一个config的配置项,取消请求就是在这里面配置的。


<template>
<div>
<button @click="fetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

let cancelTokenSource = null;


function fetchData() {
if (cancelTokenSource) {
cancelTokenSource.cancel('取消上次请求');
cancelTokenSource = null;
}
cancelTokenSource = axios.CancelToken.source();

axios.get('http://api/gcshi',{cancelToken: cancelTokenSource.token}) //
.then(response => {
laoding.value = fasle
})
}

</script>


我们测试下,如下图:可以看到,重复的请求会直接被终止掉!



CancelToken官网示例



官网使用方法传送门:http://www.axios-http.cn/docs/cancel…



const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');

也可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建一个 cancel token:


const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});

// 取消请求
cancel();

注意: 可以使用同一个 cancel token 或 signal 取消多个请求。


在过渡期间,您可以使用这两种取消 API,即使是针对同一个请求:


const controller = new AbortController();

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token,
signal: controller.signal
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})

// 取消请求 (message 参数是可选的)
source.cancel('Operation canceled by the user.');
// 或
controller.abort(); // 不支持 message 参数

作者:石小石Orz
来源:juejin.cn/post/7380185173689204746
收起阅读 »

Vite 为何短短几年内变成这样?

web
给前端以福利,给编程以复利。大家好,我是大家的林语冰。 00. 观前须知 在 Web 开发领域,Vite 如今已如雷贯耳。 自 2020 年 4 月发布以来,Vite 的人气蒸蒸日上。目前 Vite 在 GitHub 上的收藏数量已超过 64k,每周下载量超过...
继续阅读 »

给前端以福利,给编程以复利。大家好,我是大家的林语冰。


00. 观前须知


在 Web 开发领域,Vite 如今已如雷贯耳。


自 2020 年 4 月发布以来,Vite 的人气蒸蒸日上。目前 Vite 在 GitHub 上的收藏数量已超过 64k,每周下载量超过 1200 万次,现在为 Nuxt、Remix、Astro 等大多数开源框架提供支持。


尽管众口嚣嚣,我们意识到许多开发者可能仍然不熟悉 Vite 是什么鬼物,也不熟悉 Vite 在推动现代 Web 框架和工具的开发中扮演的重要角色。


在本文中,我们将科普 Vite 的知识储备,以及 Vite 如何在短短几年后发展成为现代 Web 的重量级角色。


00-trend.png



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 What is Vite (and why is it so popular)?



01. Vite 是什么鬼物?


Vite 的发音为 /vit/,在法语中是“快速”或“迅捷”的意思,不得不说 Vite 名副其实。


简而言之,Vite 是一种现代 JS 构建工具,为常见 Web 模式提供开箱即用的支持和构建优化,兼具 rollup 的自由度和成熟度。


Vite 还与 esbuild 和原生 ES 模块强强联手,实现快速无打包开发服务器。


Vite 是由“Vue 之父”尤雨溪(Evan You)构思出来的,旨在通过减少开发者在启动开发服务器和处理文件编辑后重载时遭遇的性能瓶颈,简化打包过程。


02. Vite 的核心特性


00-wall.png


运行 Vite 时,你会注意到的第一个区别在于,开发服务器会即时启动。


这是因为,Vite 采用按需方法将你的应用程序提供给浏览器。Vite 不会首先打包整个源码,而是响应浏览器请求,将你编写的模块即时转换为浏览器可以理解的原生 ESM 模块。


Vite 为 TS、PostCSS、CSS 预处理器等提供开箱即用的支持,且可以通过不断增长的插件生态系统进行扩展,支持所有你喜欢的框架和工具。


每当你在开发期间更改项目中的任意文件时,Vite 都会使用应用程序的模块图,只热重载受影响的模块(HMR)。这允许开发者预览他们的更改,及其对应用程序的影响。


Vite 的 HMR 速度惊人,可以让编辑器自动保存,并获得类似于在浏览器开发工具中修改 CSS 时的反馈循环。


Vite 还执行 依赖预构建(dependency pre-bundling)。在开发过程中,Vite 使用 esbuild 来打包你的依赖并缓存,加快未来服务器的启动速度。


此优化步骤还有助于加快 lodash 等导出许多迷你模块的依赖的加载时间,因为浏览器只加载每个依赖的代码块(chunk)。这还允许 Vite 在依赖中支持 CJS 和 UMD 代码,因为它们被打包到原生 ESM 模块中。


当你准备好部署时,Vite 将使用优化的 rollup 设置来构建你的应用程序。Vite 会执行 CSS 代码分割,添加预加载指令,并优化异步块的加载,无需任何配置。


Vite 提供了一个通用的 rollup 兼容插件 API,适用于开发和生产,使你可以更轻松地扩展和自定义构建过程。


03. Vite 的优势


使用 Vite 有若干主要优势,包括但不限于:


03-1. 开源且独立


Vite 由开源开发者社区“用爱发电”,由来自不同背景的开发者团队领导,Vite 核心仓库最近贡献者数量已突破 900 人。


Vite 得到积极的开发和维护,不断实现新功能并解决错误。


03-2. 本地敏捷开发


开发体验是 Vite 的核心,每次点击保存时,你都能感受到延迟。我们常常认为重载速度是理所当然的。


但随着您的应用程序增长,且重载速度逐渐停止,你将感恩 Vite 几乎能够保持瞬间重载,而无论应用程序大小如何。


03-3. 广泛的生态系统支持


Vite 的方案人气爆棚,大多数框架和工具都默认使用 Vite 或拥有一流的支持。通过选择使用 Vite 作为构建工具,这些项目维护者可以在它们之间共享一个统一基建,且随着时间的推移共同改良 Vite。


因此,它们可以花更多的时间开发用户需要的功能,而减少重新造轮子的时间。


03-4. 易于扩展


Vite 对 rollup 插件 API 的押注得到了回报。插件允许下游项目共享 Vite 核心提供的功能。


我们有很多高质量的插件可供使用,例如 vite-plugin-pwavite-imagetools


03-5. 框架构建难题中的重要角色


Vite 是现代元框架构建的重要组成部分之一,这是一个更大的工具生态系统的一部分。


Volar 提供了在代码编辑器中为 Vue、MDX 和 Astro 等自定义编程语言构建可靠且高性能的编辑体验所需的工具。Volar 允许框架向用户提供悬停信息、诊断和自动补全等功能,并共享 Volar 作为为它们提供支持的通用基建。


另一个很好的例子是 Nitro,它是一个服务器工具包,用于创建功能齐全的 Web 服务器,开箱即用地支持每个主要部署平台。Nitro 是一个与框架无关的库 UnJS 的奇妙集合的一部分。


04. Vite 的未来


evan-vite5.png


在最近的 ViteConf 大会的演讲中,尤雨溪表示,虽然 Vite 取得了巨大进展,但仍面临一些已知的问题和挑战。


Vite 目前使用 rollup 进行生产构建,这比 esbuildBun 等原生打包器慢得多。


Vite 还尽可能减少开发和生产环境之间的不一致性,但考虑到 rollupesbuild 之间的差异,某些不一致性无法避免。


尤雨溪现在领导一个新团队开发 rolldown,这是一个基于 Rust 的 rollup 移植,在 “JS 氧化编译器 OXC”之上构建了最大的兼容性。


这个主意是用 rolldown 替代 Vite 中的 rollupesbuild。Vite 将拥有一个单独基建,兼具 rollup 的自由度和 esbuild 的速度,消除不一致性,使代码库更易于维护,并加快构建时间。


rolldown 目前处于早期阶段,但已经显示出有希望的结果。rolldown 现已开源,rolldown 团队正在寻找贡献者来辅助实现这一愿景。


与此同时,Vite 团队在每个版本中不断改良 Vite。这项工作从上游的为 Vitest 和 Nuxt Dev SSR 提供​​动力的引擎 vite-node 开始,现已发展成为框架作者对 Vite API 的完整修订版。


新版 Environment API 预计在 Vite 6 中发布,这将是自 Vite 2 发布以来 Vite 最大的变化之一。这将允许在任意数量的环境中通过 Vite 插件管道运行代码,解锁对 worker、RSC 等的一流支持。


Vite 正在开辟一条前进的道路,并迅速成为 JS 生态系统事实上的构建工具。


参考文献



粉丝互动


本期话题是:如何评价人气爆棚的 Vite,你最喜欢或期待 Vite 的哪个功能?你可以在本文下方自由言论,文明科普。


欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。


坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~


26-cat.gif


作者:前端俱乐部
来源:juejin.cn/post/7368836713965486119
收起阅读 »

封装WebSocket消息推送,干翻Ajax轮询方式

web
建议可以提前先看下之前两篇文章,深度学习! 仅仅只会Ajax,那就out了!WebSocket实战解锁实时通信新境界! WebSocket: 实时通信的魔法快递,让你的网络生活飞跃升级! 使用AJAX和WebSocket都可以实现消息推送,但它们在实现方式和...
继续阅读 »

建议可以提前先看下之前两篇文章,深度学习!


仅仅只会Ajax,那就out了!WebSocket实战解锁实时通信新境界!



WebSocket: 实时通信的魔法快递,让你的网络生活飞跃升级!


使用AJAX和WebSocket都可以实现消息推送,但它们在实现方式和适用场景上有所不同。下面是使用这两种技术实现消息推送的简要说明。


AJax实现或WebSocket实现对比


AJAX 实现消息推送


AJAX(Asynchronous JavaScript and XML)允许你在不重新加载整个页面的情况下,与服务器进行数据交换。但是,传统的AJAX并不直接支持实时消息推送,因为它基于请求-响应模式。为了模拟消息推送,你可以使用轮询(polling)或长轮询(long-polling)技术。


轮询(Polling)


轮询是定期向服务器发送请求,以检查是否有新的消息。这种方法简单但效率较低,因为即使在没有新消息的情况下,也会频繁地发送请求。


function pollForMessages() {
$.ajax({
url: '/messages', // 假设这是获取消息的API端点
method: 'GET',
success: function(data) {
// 处理接收到的消息
console.log(data);

// 等待一段时间后再次轮询
setTimeout(pollForMessages, 5000); // 每5秒轮询一次
},
error: function() {
// 处理请求失败的情况
setTimeout(pollForMessages, 10000); // 等待更长时间后重试
}
});
}

// 开始轮询
pollForMessages();

长轮询(Long-Polling)


长轮询是轮询的一种改进方式。客户端发起一个请求到服务器,服务器会保持这个连接打开直到有新消息到达或超时,然后返回新消息或超时响应。这种方式比简单轮询减少了无效的请求,但仍然存在一定的延迟和资源浪费。


使用长轮询时,通常需要在服务器端有特殊的支持来保持连接直到有数据可以发送。


WebSocket 实现消息推送


WebSocket 提供了一个全双工的通信通道,允许服务器主动向客户端推送消息。一旦建立了WebSocket连接,服务器和客户端就可以随时向对方发送消息,而不需要像AJAX那样频繁地发起请求。


WebSocket 客户端实现


var socket = new WebSocket('ws://your-server-url');

socket.onopen = function(event) {
// 连接打开后,你可以向服务器发送消息
socket.send('Hello Server!');
};

socket.onmessage = function(event) {
// 当收到服务器发来的消息时,触发此事件
console.log('Received:', event.data);
};

socket.onerror = function(error) {
// 处理错误
console.error('WebSocket Error:', error);
};

socket.onclose = function(event) {
// 连接关闭时触发
console.log('WebSocket is closed now.');
};

WebSocket 服务器端实现


服务器端实现WebSocket通常依赖于特定的服务器软件或框架,如Node.js的ws库、Java的Spring WebSocket等。这些库或框架提供了处理WebSocket连接的API,你可以在这些连接上发送和接收消息。


在WebSocket服务器端,你可以保存与每个客户端的连接,并在需要时向它们发送消息


下面开始做封装WebSocket的介绍


想象


想象一下,你是一位超级快递员,负责把客户的包裹准确无误地送到指定的地址。这些包裹里装的是WebSocket消息,而你的任务是根据每个包裹上的useridurl信息,找到正确的收件人并将包裹送达。


首先,你需要准备一辆超级快递车(也就是WebSocket连接)。这辆车非常智能,它可以记住多个收件人的地址(url),并且同时为他们运送包裹。但是,每个收件人(userid)只能对应一个地址,这样才不会送错。


当有客户找你寄送包裹时,他们会告诉你收件人的userid和地址url。你会把这些信息记在小本本上,然后告诉超级快递车:“嘿,车车,我们要去这个地方送这个包裹给这个人!”


快递车非常听话,它会立即启动并前往指定的地址。一旦到达,它就会静静地等待,直到有包裹需要送出。


当你需要发送消息时,就像把包裹放进快递车里一样简单。你只需告诉快递车:“给这个userid的人送这个包裹!”快递车就会准确无误地将包裹送达给指定的收件人。


如果收件人回复了消息,快递车就像个贴心小助手一样,会第一时间把回信拿给你。你可以轻松地查看并处理这些回信。


这样一来,你就不再需要亲自跑腿送包裹了,超级快递车会帮你搞定一切。你只需要告诉它去哪里、送给谁,然后坐等好消息就行啦!



  1. WebSocketMessenger(快递服务公司)



    • 负责建立和维护WebSocket连接。

    • 采用单例模式,确保同一时间只有一个实例在运行。

    • 存储收件人(recipient)和地址(address)信息。

    • 提供发送消息(send_message)的方法。



  2. 快递员(WebSocket连接实例)



    • WebSocketMessenger创建和管理。

    • 负责实际的消息传递工作。

    • 知道如何与指定的收件人通信(通过地址)。



  3. 客户(发送消息的人)



    • 使用WebSocketMessenger的服务来发送消息。

    • 提供收件人信息和消息内容。



  4. 收件人(接收消息的人)



    • 在WebSocket连接的另一端,接收来自WebSocketMessenger传递的消息。




这些角色通过WebSocket连接进行交互,实现了消息的发送和接收。WebSocketMessenger作为服务提供者,管理着快递员(WebSocket连接实例),而客户和收件人则是服务的使用者。


代码层面


服务node代码可以看上篇文章:
仅仅只会Ajax,那就out了!WebSocket实战解锁实时通信新境界!


// WebSocketMessenger(快递服务公司)
class WebSocketManager {
constructor(url = null, userId = null, receiveMessageCallback = null) {
this.socket = null // WebSocket 对象
this.sendTimeObj = null // 发送信息给服务端的重复调用的时间定时器
this.reconnectTimeObj = null // 尝试链接的宏观定时器
this.reconnectTimeDistance = 5000 // 重连间隔,单位:毫秒
this.maxReconnectAttempts = 10 // 最大重连尝试次数
this.reconnectAttempts = 0 // 当前重连尝试次数
this.id = userId //用户ID(业务逻辑,根据自己业务需求调整)
this.url = url // WebSocket 连接地址
this.receiveMessageCallback = receiveMessageCallback // 接收消息回调函数
}

/**
* 开启WebSocket
*/

async start() {
if (this.url && this.id) {
// 连接WebSocket
this.connectWebSocket()
} else {
console.error('WebSocket erros: 请传入连接地址和用户id')
}
}

/**
* 创建WebSocket连接, 超级快递车
*/

connectWebSocket() {
// 通过id生成唯一值(服务端要求,具体根据自己业务去调整)
let id = `${this.id}-${Math.random()}`
// 创建 WebSocket 对象
this.socket = new WebSocket(this.url, id) // 快递员(WebSocket连接实例

// 处理连接打开事件
this.socket.onopen = (event) => {
// 给服务端发送第一条反馈信息
this.startSendServe()
}

// 处理接收到消息事件
this.socket.onmessage = (event) => {
this.receiveMessage(event)
}

// 处理连接关闭事件
this.socket.onclose = (event) => {
// 清除定时器
clearTimeout(this.sendTimeObj)
clearTimeout(this.reconnectTimeObj)
// 尝试重连
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++
console.log('重试链接次数:'+ this.reconnectAttempts)
this.reconnectTimeObj = setTimeout(() => {
this.connectWebSocket()
}, this.reconnectTimeDistance)
} else {
// 重置重连次数
this.reconnectAttempts = 0
console.error(
'WebSocketManager erros: Max reconnect attempts reached. Unable to reconnect.'
)
}
}

// 处理 WebSocket 错误事件
this.socket.onerror = (event) => {
console.error('WebSocketManager error:', event)
}
}

/**
* 发送给node的第一条信息
*/

startSendServe() {

this.sendMessage('hi I come from client')
}

/**
* 发送消息
* @param {String} message 消息内容
*/

sendMessage(message) {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(message)
} else {
console.error(
'WebSocketManager error: WebSocket connection is not open. Unable to send message.'
)
}
}

/**
* 接收到消息
*/

receiveMessage(event) {
// 根据业务自行处理
console.log('receiveMessage:', event.data)
this.receiveMessageCallback && this.receiveMessageCallback(event.data)
}

/**
* 关闭连接
*/

closeWebSocket() {
this.socket.close()
// 清除定时器 重置重连次数
clearTimeout(this.sendTimeObj)
clearTimeout(this.reconnectTimeObj)
this.reconnectAttempts = 0
}
}

代码解读


该类用于管理和控制WebSocket连接,包括连接建立、消息接收、重连机制等。下面是对代码的详细解读:


构造函数 constructor



  • url: WebSocket的连接地址。

  • userId: 用户的ID,用于业务逻辑处理。

  • receiveMessageCallback: 接收消息时的回调函数。

  • 初始化了一些成员变量,包括socket(WebSocket对象)、定时器对象(sendTimeObjreconnectTimeObj)、重连间隔和尝试次数等。


start 方法



  • 检查urluserId是否存在,若存在则调用connectWebSocket方法建立WebSocket连接。


connectWebSocket 方法



  • 生成一个基于用户ID和随机数的唯一值作为WebSocket的子协议(或协议片段)。

  • 创建新的WebSocket连接。

  • 设置了WebSocket的onopenonmessageoncloseonerror事件处理器。


事件处理器



  • onopen: 当WebSocket连接打开时触发,开始发送消息给服务端(通过startSendServe方法,该方法在代码片段中未给出)。

  • onmessage: 当接收到服务端发送的消息时触发,调用receiveMessage方法处理消息。

  • onclose: 当WebSocket连接关闭时触发,首先清除相关定时器,然后尝试重连。如果重连次数未达到最大限制,则设置定时器在一段时间后重新调用connectWebSocket进行重连;如果达到最大重连次数,则重置重连次数并输出错误信息。

  • onerror: 当WebSocket发生错误时触发,输出错误信息。
    当服务端断开后开始重连


image.png
这里设置重连10次后断开


image.png


receiveMessage 方法



  • 该方法应该是用来处理从服务端接收到的消息,具体实现取决于业务逻辑。根据传入的回调函数receiveMessageCallback,可以对接收到的消息进行相应处理


使用Demo


index.html


<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>Document</title>
<script src="./webSocketManager.js"></script>
<script>
// const WebSocketManager = require('./webSocketManager.js')
console.log(WebSocketManager)
/**
* 接收消息回调
*/

const receiveMessage = (res)=>{
console.log('接收消息回调:',res)
}
const socketManager = new WebSocketManager('ws://localhost:3000', 'userid292992', receiveMessage)
socketManager.start()

</script>
</head>

导入模块即可使用


总结:


相对完善的WebSocket管理器,能够处理连接建立、消息接收和重连等常见场景。但需要注意的是,具体的业务逻辑和错误处理可能需要根据实际情况进行进一步的完善和优化


作者:梦幻星辰吧
来源:juejin.cn/post/7380222254196326412
收起阅读 »

如何在3天内开发一个鸿蒙app

web
华为鸿蒙操作系统(HarmonyOS)自2.0版本正式上线以来,在短时间内就部署超过了2亿台设备,纵观全球操作系统的发展史,也是十分罕见的。与其他手机操作系统不同,HarmonyOS自诞生之日起,就是一款面向多设备、分布式的万物互联操作系统。“1+8+N”是H...
继续阅读 »

华为鸿蒙操作系统(HarmonyOS)自2.0版本正式上线以来,在短时间内就部署超过了2亿台设备,纵观全球操作系统的发展史,也是十分罕见的。与其他手机操作系统不同,HarmonyOS自诞生之日起,就是一款面向多设备、分布式的万物互联操作系统。“1+8+N”是HarmonyOS打造的全场景战略,其中,“1”是智能手机,“8”是指大屏、音箱、眼镜、手表、车机、耳机、平板电脑和PC“八大行星”。


围绕着关键的八大行星,周边还有合作伙伴开发的N个“卫星”,指的是移动办公、智能家居、运动健康、影音娱乐及智能出行等板块的延伸业务。



一个典型鸿蒙应用的产品设计,必然要包含鸿蒙应用的特色。既然鸿蒙操作系统主张万物互联,那么配合HarmonyOS独立操作系统的推进,咱们开发的鸿蒙App肯定不能像andriod app一样,还是要多联动鸿蒙操作系统上的流量入口,方能对于后续的业务规划起到更好的拓展作用。一些创新的点包括:



  • 多设备支持,即手机、平板、手表甚至是智汇屏都可以支持

  • 分布式数据或文件能力,不同设备中的同一款应用数据应该是实时同步的,且不完全需要后台服务即可实现

  • 支持卡片功能

  • 支持应用流转

  • 支持原子化服务


开发鸿蒙原生App的两种主流方式


1、请鸿蒙原生开发工程师,用鸿蒙ArkTS语言重新写一遍


我们可以看到鸿蒙官方的开发者文档上,有很详细的开发教程及文档,其中划重点的是,其技术语言为ArkTS语言(直接区别于IOS和Andriod的开发语言)。


这个办法是最完美的开发方式,但也是最慢的开发方式。如果按照鸿蒙原生开发的“套路”去一步步开发鸿蒙版App,就好比中国人学外语一般,开发者还得从0开始学习新的技术语言(ArkTS语言),可能时间窗口就错过了...



2、混合App开发思路


混合app开发框架是指能够同时支持原生开发和Web开发的框架,它可以将原生应用和Web应用的优势结合起来,为开发者提供更高效、更便捷的开发体验。


混合app开发框架的概念最早可以追溯到2009年,当时PhoneGap(现为Cordova)框架的发布标志着混合app开发的开始。PhoneGap允许开发者使用HTML、CSS和JavaScript来开发跨平台的移动应用,并通过插件来访问原生设备功能。随后,混合app开发框架得到了快速发展,涌现出了许多流行的框架,如Ionic、React Native、Xamarin等。2016年至今,混合app开发框架趋于成熟,并开始向更细分的方向发展。


有了混合开发框架和技术实践下,让”一端开发,多端部署“的概念执行成为可能。


混合app开发框架通常采用以下两种技术原理:



  • WebView:使用WebView控件将Web页面嵌入到原生应用中,从而实现跨平台开发。

  • JavaScript桥:提供JavaScript与原生代码之间的通信桥梁,使得Web代码可以访问原生设备功能。


特性包括以下四点:



  • 跨平台开发:使用一套代码可以开发Android、iOS等多个平台的应用。

  • 快速开发:提供丰富的UI组件和API,可以快速构建应用原型。

  • 性能优化:通过各种技术手段提升应用性能。

  • 原生功能支持:可以访问原生设备功能,提供更好的用户体验。



兼顾跨操作系统 & 跨智能终端的快速应用开发模式


开发应用要快速的话,还有一个隐藏的前提条件就是:面向业务应用场景可以复用,毕竟现在市场需求日新月异,业务流程线上化基本不会做大的调增,新功能的研发面向市场,也希望能够在短周期内能够在全端(至少是手机端的用户全网发放)。但众所周知,如果用操作系统原生语法开发,就会出现研发团队需要维护三套代码,哪怕修改一个功能,也需要三端共同改造,及其麻烦。


小程序技术或者HTML5技术天然的跨端,以及受益于微信小程序生态近几年来的蓬勃发展,小程序应用场景复用且通过“小程序转换工具”(小程序容器技术,如FinClip;或跨端框架,如Flutter、Weex等),将已有微信小程序一键转换成App,并进行用户活跃和留存,加上社交平台应用作为引流,企业可谓低成本(只需有小程序)的将业务覆盖用户整个生命周期,具了解,凡是小程序容器技术,都有将自己SDK适配鸿蒙操作系统的计划(或者说已经适配了)。


3天内开发一个鸿蒙App?


近期在研究FinClip的免费“小程序转换App”工具,结合他们新推出的鸿蒙SDK,发现还挺好用,大致步骤如下:



  1. 上传小程序代码包:如果已经有微信小程序应用,那么下载一下他们家的FinClip Studio(IDE开发工具)进行简单的转换。

  2. 使用微信登录插件:已经预先调试好的微信登录插件,非常方便,能够在转好的App中,通过一个中间转换的小程序调起微信登录接口,快速跑通业务。

  3. 生成App前必要的配置:如App图标、启动闪屏、权限配置等

  4. 生成App:配置一下对应IOS或Andriod的证书,然后「一键」生成App


实操下来,这个工具还是挺方便的。当然,其他跨端转换框架应该也是操作便捷,感兴趣的同学都可以试试。



将小程序转换为App,如果小程序容器技术支持鸿蒙NEXT版本,那么,使用已有小程序+转换App的功能,便能快速开发出一套适配兼容鸿蒙NEXT操作系统的App。



小程序转鸿蒙原生app的创新开发方式,为开发者提供了快速、便捷的开发途径,助力开发者高效地将小程序业务迁移至鸿蒙生态,同时也为用户提供了更加丰富、流畅的应用体验。展望未来,随着技术的不断发展和完善,相信将会有更多创新的开发模式涌现,为开发者和用户带来更加便利、高效的开发和使用体验。


作者:Speedoooo
来源:juejin.cn/post/7379521155286843404
收起阅读 »

汝为傀儡,吾来操纵(🍄Puppeteer🍄)

web
puppeteer是我以前同事使用过的一个工具,用来测试页面的功能,可以模拟用户操作。这几天我也看到了一些相关的文章,也是很感兴趣的,所以准备整理输出一篇Puppeteer的文章,用来学习记忆。后面发现Puppeteer相关的内容比较多,准备分为两部分来讲,这...
继续阅读 »

IMG_20240601_170616_843.jpg

puppeteer是我以前同事使用过的一个工具,用来测试页面的功能,可以模拟用户操作。这几天我也看到了一些相关的文章,也是很感兴趣的,所以准备整理输出一篇Puppeteer的文章,用来学习记忆。

后面发现Puppeteer相关的内容比较多,准备分为两部分来讲,这一部分主要讲理论相关的,也会举些简单的实例。下一章则会主要针对实战来讲解。

介绍

Puppeteer词义解释

  • Puppet:木偶,傀儡
  • Puppeteer:操纵木偶的人

Puppeteer 是一个由 Google 开发的 Node.js 库用于控制 Chrome 或 Chromium 浏览器的高级 API。它可以模拟用户的交互行为,例如点击、填写表单、导航等,同时还可以截取页面内容、生成 PDF、执行自动化测试等功能

官方网站:github.com/GoogleChrom…

Puppeteer 中文文档

官方文档:pptr.dev/

文档地址:zhaoqize.github.io/puppeteer-a…

核心功能

Puppeteer 的核心功能包括以下几个方面:

  1. 控制浏览器:Puppeteer 可以启动一个 Chrome 或 Chromium 浏览器实例,并通过 API 控制浏览器的行为,如打开网页、点击链接、填写表单、执行 JavaScript 等操作。
  2. 页面操作:Puppeteer 可以模拟用户在页面上的操作,包括点击元素、填写表单、滚动页面、截取屏幕截图等,实现对页面的交互操作。
  3. 网页内容抓取:Puppeteer 可以获取页面的 DOM 结构、元素属性、文本内容等信息,从而实现网页内容的抓取和提取。
  4. 页面性能分析:Puppeteer 可以获取页面加载性能数据、网络请求信息、CPU 和内存使用情况等,帮助开发者进行页面性能优化和调试。
  5. 生成 PDF:Puppeteer 可以将网页内容保存为 PDF 文档,支持设置页面大小、方向、页边距等参数,方便生成打印版的网页内容。
  6. 自动化测试:Puppeteer 可以用于编写自动化测试脚本,模拟用户的操作行为,验证页面的功能和交互是否符合预期,实现自动化测试流程。
  7. 爬虫和数据采集:Puppeteer 可以用于编写网络爬虫,自动访问网页、提取数据、填写表单等,实现网页内容的自动采集和处理。

总的来说,Puppeteer 是一个功能强大的浏览器自动化工具,可以实现对浏览器的控制和页面操作,适用于各种场景下的自动化任务,如自动化测试、网页内容抓取、页面性能分析等。

下面介绍一些常用的API。

启动新的浏览器实例

puppeteer.launch()是一个用于启动一个新的浏览器实例的方法。该方法返回一个 Promise,该 Promise 在浏览器实例启动后会被解析为一个 Browser 对象,你可以通过这个对象来操作浏览器。

在 Puppeteer 中,puppeteer.launch() 方法可以接受一个可选的配置对象 options,用于指定启动浏览器实例时的一些参数和选项。下面是一些常用的配置选项:

  1. headless:布尔值,是否以 无头模式 运行浏览器。默认是 true,即以无头模式启动,不会显示浏览器界面。如果设置为 false,则会以有头模式启动,显示浏览器界面。
  2. args:一个字符串数组,传递给浏览器实例的其他参数。 这些参数可以参考 这里
  3. defaultViewport 是一个对象,用于为每个页面设置一个默认视口大小。默认是 800x600。如果为 null 的话就禁用视图口。下面是 defaultViewport 对象中可以设置的属性:

    • width:页面的宽度像素。
    • height:页面的高度像素。
    • deviceScaleFactor:设备的缩放比例,可以认为是设备像素比(device pixel ratio,DPR)。默认值为 1。

      更多

  4. ignoreHTTPSErrors: 布尔值,指定是否忽略 HTTPS 错误。默认是 false
  5. defaultViewport:一个对象,用于指定浏览器的默认视口大小,包括宽度、高度和设备比例因子等。
  6. userDataDir:一个字符串,用于指定用户数据目录的路径,用于存储浏览器的用户数据,比如缓存、Cookies 等。
  7. timeout: 数值,指定启动浏览器的超时时间,单位为毫秒。
  8. slowMo: 数值,指定 Puppeteer 操作的延迟时间,单位为毫秒。可以用来减慢操作的速度,方便调试。

下面是一个简单的示例代码,演示如何使用 puppeteer.launch() 方法来启动一个浏览器实例:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch({
headless: false, // 显示浏览器界面
executablePath: '/path/to/chrome', // 指定浏览器可执行文件路径
args: ['--no-sandbox', '--disable-setuid-sandbox'], // 额外参数
defaultViewport: { width: 1280, height: 800 }, // 默认视口大小
userDataDir: '/path/to/userDataDir'// 用户数据目录
slowMo: 100, // 延迟 100 毫秒
});

// 在这里可以进行其他操作,比如创建新页面、访问网页等

await browser.close();
})();

在上面的代码中,我们通过 puppeteer.launch() 方法启动了一个浏览器实例,并通过 options 参数配置了一些选项,比如显示浏览器界面、指定浏览器可执行文件路径、传递额外参数、设置默认视口大小和用户数据目录。你可以根据需要自定义 options 对象中的属性来满足你的需求。

需要注意的是,在使用完浏览器实例后,应该调用 browser.close() 方法来关闭浏览器,释放资源。

Browser 类

Browser 类表示一个 Chrome 或 Chromium 浏览器实例。它提供了一组方法来操作整个浏览器,如创建新页面、关闭浏览器、监听事件等。

当 Puppeteer 连接到一个 Chromium 实例的时候会通过 puppeteer.launch 或 puppeteer.connect 创建一个 Browser 对象。

以下是一些 Browser 类常用的方法:

  • newPage(): 创建一个新的页面实例。
  • close(): 关闭浏览器实例。
  • version(): 获取浏览器的版本信息。
  • pages(): 获取所有已打开的页面实例。
  • newContext(): 创建一个新的浏览器上下文。
  • target(): 获取指定目标的实例。

下面是使用 Browser 创建 Page 的例子

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.goto('https://example.com');
await browser.close();
});

Page 类

Page 类表示一个浏览器页面。它提供了一系列方法,用于操作和控制页面的行为,例如导航至指定 URL、执行 JavaScript 代码、截取页面截图等。

以下是一些 Page 类常用的方法:

  1. goto(url): 导航到指定的 URL。
await page.goto('https://www.example.com');
  1. waitForSelector(selector): 等待页面中指定的选择器出现。
await page.waitForSelector('.my-element');
  1. click(selector): 点击页面中指定的选择器。
await page.click('.my-button');
  1. type(selector, text): 在指定的输入框中输入文本。
await page.type('input[name="username"]', 'myusername');
  1. evaluate(pageFunction): 在页面上下文中执行指定的函数。
const title = await page.evaluate(() => document.title);
  1. screenshot(options): 截取当前页面的屏幕截图。
await page.screenshot({ path: 'screenshot.png' });
  1. close(): 关闭页面实例。
await page.close();

通过使用 Page 类提供的方法,我们可以模拟用户在浏览器中的操作,实现各种自动化任务,如网页截图、表单填写、点击操作等。

下面会更详细地介绍几个常用的方法。

元素获取

这些方法可以帮助我们在 Puppeteer 中获取页面元素:

  1. page.content(): 返回页面完整的 HTML 代码。
const html = await page.content();
  1. page.$(selector): 使用 document.querySelector 寻找指定元素。
const element = await page.$('.my-element');
  1. page.$$(selector): 使用 document.querySelectorAll 寻找指定元素。
const elements = await page.$$('.my-elements');
  1. page.$x(expression): 使用 XPath 寻找指定元素。
const element = await page.$x('//div[@class="my-element"]');
  1. page.$eval(selector, pageFunction, …args?): 在页面中注入方法,执行 document.querySelector 后将结果作为第一个参数传给函数体。
const text = await page.$eval('.my-element', element => element.textContent);
  1. page.$$eval(selector, pageFunction, …args?): 在页面中注入方法,执行 document.querySelectorAll 后将结果作为第一个参数传给函数体。
const texts = await page.$$eval('.my-elements', elements => elements.map(element => element.textContent));

页面操作

点击操作

  • page.click(selector, options?): 点击选择器匹配的元素,有多个元素满足匹配条件仅作用第一个。
await page.click('.my-button');
  • page.tap(selector): 点击选择器匹配的元素,有多个元素满足匹配条件仅作用第一个,主要针对手机端的触摸事件。
await page.tap('.my-button');
  • page.focus(selector): 给选择器匹配的元素获取焦点,有多个元素满足匹配条件仅作用第一个。
await page.focus('.my-input');
  • page.hover(selector): 鼠标悬浮于选择器匹配的元素,有多个元素满足匹配条件仅作用第一个。
await page.hover('.my-element');

输入操作

page.type 是 Puppeteer 中用于在指定元素上输入文本的方法。该方法接受两个参数:选择器和要输入的文本。

以下是 page.type 方法的用法示例:

await page.type('input[type="text"]', 'Hello, Puppeteer!');

在这个示例中,我们使用选择器 input[type="text"] 来定位页面上的一个文本输入框,并在该输入框中输入文本 'Hello, Puppeteer!'。

键盘模拟按键

page.keyboard 对象提供了一系列方法,可以模拟按键的按下、释放、输入等操作。

以下是一些常用的 page.keyboard 方法:

  1. keyboard.press(key[, options]): 模拟按下指定的键。
  2. keyboard.release(key): 模拟释放指定的键。
  3. keyboard.down(key): 模拟按下指定的键,保持按下状态。
  4. keyboard.up(key): 模拟释放指定的键,取消按下状态。
  5. keyboard.type(text[, options]): 模拟输入指定的文本。

下面是一个示例代码,演示如何在输入框中模拟按键操作:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://www.example.com');

// 获取要输入文本的输入框的选择器
const selector = 'input[type="text"]';

// 等待输入框加载完成
await page.waitForSelector(selector);

// 在输入框中模拟按键操作
await page.focus(selector); // 让输入框获得焦点
await page.keyboard.type('Hello, Puppeteer!'); // 输入文本
await page.keyboard.press('Enter'); // 模拟按下 Enter 键

await browser.close();
})();

在这个示例中,我们首先让输入框获得焦点,然后使用 page.keyboard.type 方法输入文本 'Hello, Puppeteer!',最后使用 page.keyboard.press 方法模拟按下 Enter 键。

鼠标模拟

在 Puppeteer 中,可以使用 page.mouse 对象来模拟鼠标操作。page.mouse 对象提供了一系列方法,可以模拟鼠标的移动、点击、滚动等操作。

以下是一些常用的 page.mouse 方法:

  1. mouse.move(x, y[, options]): 将鼠标移动到指定位置。
  2. mouse.click(x, y[, options]): 在指定位置模拟鼠标点击。
  3. mouse.down([options]): 模拟按下鼠标按钮。
  4. mouse.up([options]): 模拟释放鼠标按钮。
  5. mouse.wheel(deltaX, deltaY): 模拟滚动鼠标滚轮。

下面是一个示例代码,演示如何在页面中模拟鼠标操作:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://www.example.com');

// 获取要点击的元素的选择器
const selector = 'button';

// 等待元素加载完成
await page.waitForSelector(selector);

// 获取元素的位置
const element = await page.$(selector);
const boundingBox = await element.boundingBox();

// 在元素位置模拟鼠标点击
await page.mouse.click(boundingBox.x + boundingBox.width / 2, boundingBox.y + boundingBox.height / 2);

await browser.close();
})();

在这个示例中,我们首先等待页面中的按钮元素加载完成,然后获取按钮元素的位置信息,最后使用 page.mouse.click 方法在按钮元素的中心位置模拟鼠标点击操作。

事件监听

这些是 Puppeteer 中常用的页面事件,可以通过监听这些事件来执行相应的操作。以下是每个事件的简要说明:

  • close: 当页面被关闭时触发。
  • console: 当页面中调用 console API 时触发。
  • error: 当页面发生错误时触发。
  • load: 当页面加载完成时触发。
  • request: 当页面收到请求时触发。
  • requestfailed: 当页面的请求失败时触发。
  • requestfinished: 当页面的请求成功时触发。
  • response: 当页面收到响应时触发。
  • workercreated: 当页面创建 webWorker 时触发。
  • workerdestroyed: 当页面销毁 webWorker 时触发。

您可以通过以下示例代码来监听页面加载完成和页面请求成功的事件:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();

// 监听页面加载完成事件
page.
on('load', () => {
console.log('Page loaded successfully');
});

// 监听页面请求成功事件
page.
on('requestfinished', (request) => {
console.log(`Request finished: ${request.url()}`);
});

await page.goto('https://www.example.com');

await browser.close();
})();

在这个示例中,我们使用 page.on 方法来监听页面加载完成和页面请求成功的事件,并在事件发生时打印相应的信息。

等待元素、请求、响应

在 Puppeteer 中,您可以使用以下方法来等待元素、请求和响应:

  1. page.waitForXPath(xpath, options)等待指定的 XPath 对应的元素出现。参数 options 可以包含 timeout 和 visible 选项。返回一个 ElementHandle 实例。
await page.waitForXPath('//div[@]', { visible: true });
  1. page.waitForSelector(selector, options)等待指定的选择器对应的元素出现。参数 options 可以包含 timeout 和 visible 选项。返回一个 ElementHandle 实例。
await page.waitForSelector('.example', { visible: true });
  1. page.waitForResponse(predicate)等待符合条件的响应结束。参数 predicate 是一个函数,用于判断响应是否符合条件。返回一个 Response 实例。
const response = await page.waitForResponse(response => response.url().includes('/api'));
  1. page.waitForRequest(predicate)等待符合条件的请求出现。参数 predicate 是一个函数,用于判断请求是否符合条件。返回一个 Request 实例。
const request = await page.waitForRequest(request => request.url().includes('/api'));

这些方法可以帮助您在 Puppeteer 中更精确地控制等待元素、请求和响应的时间,以便在需要时执行相应的操作。如果您有任何疑问或需要进一步的解释,请随时告诉我。我将很乐意帮助您。

网络拦截操作

page.setRequestInterception() 方法可以拦截页面中发出的网络请求,并对其进行处理。通过拦截请求,你可以修改请求的行为,例如阻止请求、修改请求的头部、修改请求的内容等。

以下是使用 page.setRequestInterception() 方法的一个示例:

const puppeteer = require('puppeteer');

(async () => {
 const browser = await puppeteer.launch();
 const page = await browser.newPage();

 // 启用请求拦截
 await page.setRequestInterception(true);

 // 监听请求事件
 page.on('request', (request) => {
   // 判断请求的 URL 是否符合条件
   if (request.url().includes('/api')) {
     request.continue(); // 继续请求
  } else {
     request.abort(); // 中止请求
  }
});

 // 导航至指定 URL
 await page.goto('https://example.com');
 
   // 等待页面加载完成
 await page.waitForNavigation();
   
  // 获取符合条件的网络响应
 const responses = await page.waitForResponse(response => response.url().includes('/api'));
 // 获取接口数据
 const responseData = await responses.json();
 console.log(responseData);
 
 await browser.close();
})();

需要注意的是,在使用请求拦截功能时,务必要确保在请求被中止或继续之前,要么调用 interceptedRequest.abort() 中止请求,要么调用 interceptedRequest.continue() 继续请求,否则可能会导致页面无法正常加载。

简单示例

截图

在 Puppeteer 中实现截图可以通过 page.screenshot() 方法来实现。

以下是一个简单的示例代码,演示如何在 Puppeteer 中对页面进行截图:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.goto('https://www.example.com');

// 在当前目录下保存截图
await page.screenshot({ path: 'example.png' });

await browser.close();
})();

在上面的示例中,我们首先启动了一个 Puppeteer 浏览器实例,然后创建了一个新页面并访问了示例网站。接着使用 page.screenshot() 方法对页面进行截图,并将截图保存在当前目录下的 example.png 文件中。最后关闭了浏览器实例。

生成pdf

在 Puppeteer 中生成 PDF 可以通过 page.pdf() 方法来实现。

以下是一个简单的示例代码,演示如何在 Puppeteer 中生成 PDF:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.goto('https://www.example.com');

// 生成 PDF 并保存在当前目录下的 example.pdf 文件中
await page.pdf({ path: 'example.pdf', format: 'A4' });

await browser.close();
})();

在上面的示例中,我们首先启动了一个 Puppeteer 浏览器实例,然后创建了一个新页面并访问了示例网站。接着使用 page.pdf() 方法生成 PDF,并将其保存在当前目录下的 example.pdf 文件中。您还可以调整生成 PDF 的格式、尺寸、页面边距等参数。

设置cookie

在 Puppeteer 中设置 cookie 可以通过 page.setCookie() 方法来实现。

以下是一个简单的示例代码,演示如何在 Puppeteer 中设置 cookie:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();

// 设置 cookie
await page.setCookie({
name: 'username',
value: 'john_doe',
domain: 'www.example.com'
});

await page.goto('https://www.example.com');

// 在页面中获取 cookie
const cookies = await page.cookies();
console.log(cookies);

await browser.close();
})();

在上面的示例中,我们首先启动了一个 Puppeteer 浏览器实例,然后创建了一个新页面。接着使用 page.setCookie() 方法设置了一个名为 username 的 cookie,然后访问了示例网站。最后使用 page.cookies() 方法获取页面中的所有 cookie,并将其打印出来。

您可以根据需要设置更多的 cookie,以及设置 cookie 的路径、过期时间等属性。


作者:Aplee
来源:juejin.cn/post/7379512671617679412
收起阅读 »

最全的docx,pptx,xlsx(excel),pdf文件预览方案总结

web
最近遇到了文件预览的需求,但一搜索发现,这还不是一个简单的功能。于是又去查询了很多资料,调研了一些方案,也踩了好多坑。最后总结方案如下花钱解决(使用市面上现有的文件预览服务)微软google阿里云 IMMXDOCOffice Web 365wps开放平台前端方...
继续阅读 »

最近遇到了文件预览的需求,但一搜索发现,这还不是一个简单的功能。于是又去查询了很多资料,调研了一些方案,也踩了好多坑。最后总结方案如下

  1. 花钱解决(使用市面上现有的文件预览服务)
    1. 微软
    2. google
    3. 阿里云 IMM
    4. XDOC
    5. Office Web 365
    6. wps开放平台
  2. 前端方案
    1. pptx的预览方案
    2. pdf的预览方案
    3. docx的预览方案
    4. xlsx(excel)的预览方案
    5. 前端预览方案总结
  3. 服务端方案
    1. openOffice
    2. kkFileView
    3. onlyOffice

如果有其他人也遇到了同样的问题,有了这篇文章,希望能更方便的解决。

基本涵盖了所有解决方案。因此,标题写上 最全 的文件预览方案调研总结,应该不为过吧。

一.市面上现有的文件预览服务

1.微软

docx,pptx,xlsx可以说是office三件套,那自然得看一下 微软官方 提供的文件预览服务。使用方法特别简单,只需要将文件链接,拼接到参数后面即可。

记得encodeURL

https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}

(1).PPTX预览效果:

image.png

  • 优点:还原度很高,功能很丰富,可以选择翻页,甚至支持点击播放动画。
  • 缺点:不知道是不是墙的原因,加载稍慢。

(2).Excel预览效果:

image.png

(3).Doxc预览效果

image.png

(4).PDF预览效果

这个我测试没有成功,返回了一个错误,其他人可以试试。

image.png

(5).总的来说

对于docx,pptx,xlsx都有较好的支持,pdf不行。

还有一个坑点是:这个服务是否稳定,有什么限制,是否收费,都查不到一个定论。在office官方网站上甚至找不到介绍这个东西的地方。

目前只能找到一个Q&A:answers.microsoft.com/en-us/msoff…

微软官方人员回答表示:

image.png

翻译翻译,就是:几乎永久使用,没有收费计划,不会存储预览的文件数据,限制文件10MB,建议用于 查看互联网上公开的文件

但经过某些用户测试发现:

image.png

使用了微软的文件预览服务,然后删除了文件地址,仍然可访问,但过一段时间会失效。

2.Google Drive 查看器

接入简单,同 Office Web Viewer,只需要把 src 改为https://drive.google.com/viewer?url=${encodeURIComponent(url)}即可。

限制25MB,支持以下格式:

image.png

测试效果,支持docx,pptx,xlsx,pdf预览,但pptx预览的效果不如微软,没有动画效果,样式有小部分会错乱。

由于某些众所周知的原因,不可用

3.阿里云 IMM

官方文档如下:help.aliyun.com/document_de…

image.png

付费使用

4.XDOC 文档预览

说了一些大厂的,在介绍一些其他的,需要自行分辨

官网地址:view.xdocin.com/view-xdocin…

image.png

5.Office Web 365

需要注意的是,虽然名字很像office,但我们看网页的Copyright可以发现,其实是一个西安的公司,不是微软

但毕竟也提供了文件预览的服务

官网地址:http://www.officeweb365.com/

image.png

6.WPS开放平台

官方地址:solution.wps.cn/

image.png

付费使用,价格如下:

image.png

二.前端处理方案

1.pptx的预览方案

先查一下有没有现成的轮子,目前pptx的开源预览方案能找到的只有这个:github.com/g21589/PPTX… 。但已经六七年没有更新,也没有维护,笔者使用的时候发现有很多兼容性问题。

简单来说就是,没有。对于这种情况,我们可以自行解析,主要步骤如下:

  1. 查询pptx的国际标准
  2. 解析pptx文件
  3. 渲染成html或者canvas进行展示

我们先去找一下pptx的国际标准,官方地址:officeopenxml

先解释下什么是officeopenxml:

Office OpenXML,也称为OpenXML或OOXML,是一种基于XML的办公·文档格式,包括文字处理文档、电子表格、演示文稿以及图表、图表、形状和其他图形材料。该规范由微软开发,并于2006年被ECMA国际采用为ECMA-376。第二个版本于2008年12月发布,第三个版本于2011年6月发布。该规范已被ISO和IEC采用为ISO/IEC 29500。

虽然Microsoft继续支持较旧的二进制格式(.doc、.xls和.ppt),但OOXML现在是所有Microsoft Office文档(.docx、.xlsx和.pptx)的默认格式。

由此可见,Office OpenXML由微软开发,目前已经是国际标准。接下来我们看一下pptx里面有哪些内容,具体可以看pptx的官方标准:officeopenxml-pptx

PresentationML或.pptx文件是一个zip文件,其中包含许多“部分”(通常是UTF-8或UTF-16编码)或XML文件。该包还可能包含其他媒体文件,例如图像。该结构根据 OOXML 标准 ECMA-376 第 2 部分中概述的开放打包约定进行组织。

image.png

根据国际标准,我们知道,pptx文件本质就是一个zip文件,其中包含许多部分:

部件的数量和类型将根据演示文稿中的内容而有所不同,但始终会有一个 [Content_Types].xml、一个或多个关系 (.rels) 部件和一个演示文稿部件(演示文稿.xml),它位于 ppt 文件夹中,用于Microsoft Powerpoint 文件。通常,还将至少有一个幻灯片部件,以及一张母版幻灯片和一张版式幻灯片,从中形成幻灯片。

那么js如何读取zip呢?

找到一个工具: http://www.npmjs.com/package/jsz…

于是我们可以开始尝试解析pptx了。

import JSZip from 'jszip'
// 加载pptx数据
const zip = await JSZip.loadAsync(pptxData)
  • 解析[Content_Types].xml

每个pptx必然会有一个 [Content_Types].xml。此文件包含包中部件的所有内容类型的列表。每个部件及其类型都必须列在 [Content_Types].xml 中。通过它里面的内容,可以解析其他的文件数据


const filesInfo = await getContentTypes(zip)

async function getContentTypes(zip: JSZip) {
const ContentTypesJson = await readXmlFile(zip, '[Content_Types].xml')
const subObj = ContentTypesJson['Types']['Override']
const slidesLocArray = []
const slideLayoutsLocArray = []
for (let i = 0; i < subObj.length; i++) {
switch (subObj[i]['attrs']['ContentType']) {
case 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml':
slidesLocArray.push(subObj[i]['attrs']['PartName'].substr(1))
break
case 'application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml':
slideLayoutsLocArray.push(subObj[i]['attrs']['PartName'].substr(1))
break
default:
}
}
return {
slides: slidesLocArray,
slideLayouts: slideLayoutsLocArray,
}
}
  • 解析演示文稿

先获取ppt目录下的presentation.xml演示文稿的大小

由于演示文稿是xml格式,要真正的读取内容需要执行 readXmlFile

const slideSize = await getSlideSize(zip)
async function getSlideSize(zip: JSZip) {
const content = await readXmlFile(zip, 'ppt/presentation.xml')
const sldSzAttrs = content['p:presentation']['p:sldSz']['attrs']
return {
width: (parseInt(sldSzAttrs['cx']) * 96) / 914400,
height: (parseInt(sldSzAttrs['cy']) * 96) / 914400,
}
}

  • 加载主题

根据 officeopenxml的标准解释

每个包都包含一个关系部件,用于定义其他部件之间的关系以及与包外部资源的关系。这样可以将关系与内容分开,并且可以轻松地更改关系,而无需更改引用目标的源。

除了包的关系部分之外,作为一个或多个关系源的每个部件都有自己的关系部分。每个这样的关系部件都可以在部件的_rels子文件夹中找到,并通过在部件名称后附加“.rels”来命名。

其中主题的相关信息就在ppt/_rels/presentation.xml.rels


async function loadTheme(zip: JSZip) {
const preResContent = await readXmlFile(
zip,
'ppt/_rels/presentation.xml.rels',
)
const relationshipArray = preResContent['Relationships']['Relationship']
let themeURI
if (relationshipArray.constructor === Array) {
for (let i = 0; i < relationshipArray.length; i++) {
if (
relationshipArray[i]['attrs']['Type'] ===
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme'
) {
themeURI = relationshipArray[i]['attrs']['Target']
break
}
}
} else if (
relationshipArray['attrs']['Type'] ===
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme'
) {
themeURI = relationshipArray['attrs']['Target']
}

if (themeURI === undefined) {
throw Error("Can't open theme file.")
}

return readXmlFile(zip, 'ppt/' + themeURI)
}

后续ppt里面的其他内容,都可以这么去解析。根据officeopenxml标准,可能包含:

PartDescription
Comments AuthorsContains information about each author who has added a comment to the presentation.
CommentsContains comments for a single slide.
Handout MasterContains the look, position, and size of the slides, notes, header and footer text, date, or page number on the presentation's handout. There can be only one such part.
Notes MasterContains information about the content and formatting of all notes pages. There can be only one such part.
Notes SlideContains the notes for a single slide.
PresentationContains the definition of a slide presentation. There must be one and only one such part. See Presentation.
Presentation PropertiesContains all of the presentation's properties. There must be one and only one such part.
SlideContains the content of a single slide.
Slide LayoutContains the definition for a slide template. It defines the default appearance and positioning of drawing objects on the slide. There must be one or more such parts.
Slide MasterContains the master definition of formatting, text, and objects that appear on each slide in the presentation that is derived from the slide master. There must be one or more such parts.
Slide Synchronization DataContains properties specifying the current state of a slide that is being synchronized with a version of the slide stored on a central server.
User-Defined TagsContains a set of user-defined properties for an object in a presentation. There can be zero or more such parts.
View PropertiesContains display properties for the presentation.

等等内容,我们根据标准一点点解析并渲染就好了。

完整源码:ranui

使用文档:preview组件

2.pdf的预览方案

(1).iframe和embed

pdf比较特别,一般的浏览器默认支持预览pdf。因此,我们可以使用浏览器的能力:

<iframe src="viewFileUrl" />

但这样就完全依赖浏览器,对PDF的展示,交互,是否支持全看浏览器的能力,且不同的浏览器展示和交互往往不同,如果需要统一的话,最好还是尝试其他方案。

embed的解析方式也是一样,这里不举例子了

(2)pdfjs

npm: http://www.npmjs.com/package/pdf…

github地址:github.com/mozilla/pdf…

mozilla出品,就是我们常见的MDN的老大。

而且目前 火狐浏览器 使用的 PDF 预览就是采用这个,我们可以用火狐浏览器打开pdf文件,查看浏览器使用的js就能发现

image.png

需要注意的是,最新版pdf.js限制了node版本,需要大于等于18

github链接:github.com/mozilla/pdf…

image.png

如果你项目node版本小于这个情况,可能会无法使用。

如果遇到这种情况,评论区 @敲敲敲敲暴你脑袋 提出一种解决方案,以前老的版本没有限制,可以用以前版本,详情见评论区。

具体使用情况如下:

import * as pdfjs from 'pdfjs-dist'
import * as pdfjsWorker from 'pdfjs-dist/build/pdf.work.entry'

interface Viewport {
width: number
height: number
viewBox: Array<number>
}

interface RenderContext {
canvasContext: CanvasRenderingContext2D | null
transform: Array<number>
viewport: Viewport
}

interface PDFPageProxy {
pageNumber: number
getViewport: () => Viewport
render: (options: RenderContext) => void
}

interface PDFDocumentProxy {
numPages: number
getPage: (x: number) => Promise<PDFPageProxy>
}

class PdfPreview {
private pdfDoc: PDFDocumentProxy | undefined
pageNumber: number
total: number
dom: HTMLElement
pdf: string | ArrayBuffer
constructor(pdf: string | ArrayBuffer, dom: HTMLElement | undefined) {
this.pageNumber = 1
this.total = 0
this.pdfDoc = undefined
this.pdf = pdf
this.dom = dom ? dom : document.body
}
private getPdfPage = (number: number) => {
return new Promise((resolve, reject) => {
if (this.pdfDoc) {
this.pdfDoc.getPage(number).then((page: PDFPageProxy) => {
const viewport = page.getViewport()
const canvas = document.createElement('canvas')
this.dom.appendChild(canvas)
const context = canvas.getContext('2d')
const [_, __, width, height] = viewport.viewBox
canvas.width = width
canvas.height = height
viewport.width = width
viewport.height = height
canvas.style.width = Math.floor(viewport.width) + 'px'
canvas.style.height = Math.floor(viewport.height) + 'px'
const renderContext = {
canvasContext: context,
viewport: viewport,
transform: [1, 0, 0, -1, 0, viewport.height],
}
page.render(renderContext)
resolve({ success: true, data: page })
})
} else {
reject({ success: false, data: null, message: 'pdfDoc is undefined' })
}
})
}
pdfPreview = () => {
window.pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
window.pdfjsLib
.getDocument(this.pdf)
.promise.then(async (doc: PDFDocumentProxy) => {
this.pdfDoc = doc
this.total = doc.numPages
for (let i = 1; i <= this.total; i++) {
await this.getPdfPage(i)
}
})
}
prevPage = () => {
if (this.pageNumber > 1) {
this.pageNumber -= 1
} else {
this.pageNumber = 1
}
this.getPdfPage(this.pageNumber)
}
nextPage = () => {
if (this.pageNumber < this.total) {
this.pageNumber += 1
} else {
this.pageNumber = this.total
}
this.getPdfPage(this.pageNumber)
}
}

const createReader = (file: File): Promise<string | ArrayBuffer | null> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
resolve(reader.result)
}
reader.onerror = (error) => {
reject(error)
}
reader.onabort = (abort) => {
reject(abort)
}
})
}

export const renderPdf = async (
file: File,
dom?: HTMLElement,
): Promise<void> => {
try {
if (typeof window !== 'undefined') {
const pdf = await createReader(file)
if (pdf) {
const PDF = new PdfPreview(pdf, dom)
PDF.pdfPreview()
}
}
} catch (error) {
console.log('renderPdf', error)
}
}

3.docx的预览方案

我们可以去查看docx的国际标准,去解析文件格式,渲染成htmlcanvas,不过比较好的是,已经有人这么做了,还开源了

npm地址:http://www.npmjs.com/package/doc…

使用方法如下:

import { renderAsync } from 'docx-preview'

interface DocxOptions {
bodyContainer?: HTMLElement | null
styleContainer?: HTMLElement
buffer: Blob
docxOptions?: Partial<Record<string, string | boolean>>
}

export const renderDocx = (options: DocxOptions): Promise<void> | undefined => {
if (typeof window !== 'undefined') {
const { bodyContainer, styleContainer, buffer, docxOptions = {} } = options
const defaultOptions = {
className: 'docx',
ignoreLastRenderedPageBreak: false,
}
const configuration = Object.assign({}, defaultOptions, docxOptions)
if (bodyContainer) {
return renderAsync(buffer, bodyContainer, styleContainer, configuration)
} else {
const contain = document.createElement('div')
document.body.appendChild(contain)
return renderAsync(buffer, contain, styleContainer, configuration)
}
}
}

4.xlsx的预览方案

我们可以使用这个:

npm地址:http://www.npmjs.com/package/@vu…

支持vue2vue3,也有js的版本

对于xlsx的预览方案,这个是找到最好用的了。

5.前端预览方案总结

我们对以上找到的优秀的解决方案,进行改进和总结,并封装成一个web components组件:preview组件

为什么是web components组件?

因为它跟框架无关,可以在任何框架中使用,且使用起来跟原生的div标签一样方便。

并编写使用文档: preview组件文档, 文档支持交互体验。

源码公开,MIT协议。

目前docx,pdf,xlsx预览基本可以了,都是最好的方案。pptx预览效果不太好,因为需要自行解析。不过源码完全公开,需要的可以提issuepr或者干脆自取或修改,源码地址:github.com/chaxus/ran/…

三.服务端预览方案

1.openOffice

由于浏览器不能直接打开docx,pptx,xlsx等格式文件,但可以直接打开pdf和图片.因此,我们可以换一个思路,用服务端去转换下文件的格式,转换成浏览器能识别的格式,然后再让浏览器打开,这不就OK了吗,甚至不需要前端处理了。

我们可以借助openOffice的能力,先介绍一下openOffice:

Apache OpenOffice是领先的开源办公软件套件,用于文字处理,电子表格,演示文稿,图形,数据库等。它有多种语言版本,适用于所有常用计算机。它以国际开放标准格式存储您的所有数据,还可以从其他常见的办公软件包中读取和写入文件。它可以出于任何目的完全免费下载和使用。

官网如下:http://www.openoffice.org/

需要先下载opneOffice,找到bin目录,进行设置

configuration.setOfficeHome("这里的路径一般为C:\\Program Files (x86)\\OpenOffice 4");

测试下转换的文件路径

    public static void main(String[] args) {
convertToPDF("/Users/Desktop/asdf.docx", "/Users/Desktop/adsf.pdf");
}

完整如下:


package org.example;

import org.artofsolving.jodconverter.OfficeDocumentConverter;
import org.artofsolving.jodconverter.office.DefaultOfficeManagerConfiguration;
import org.artofsolving.jodconverter.office.OfficeManager;

import java.io.File;

public class OfficeUtil {

private static OfficeManager officeManager;
private static int port[] = {8100};

/**
* start openOffice service.
*/

public static void startService() {
DefaultOfficeManagerConfiguration configuration = new DefaultOfficeManagerConfiguration();
try {
System.out.println("准备启动office转换服务....");
configuration.setOfficeHome("这里的路径一般为C:\\Program Files (x86)\\OpenOffice 4");
configuration.setPortNumbers(port); // 设置转换端口,默认为8100
configuration.setTaskExecutionTimeout(1000 * 60 * 30L);// 设置任务执行超时为30分钟
configuration.setTaskQueueTimeout(1000 * 60 * 60 * 24L);// 设置任务队列超时为24小时
officeManager = configuration.buildOfficeManager();
officeManager.start(); // 启动服务
System.out.println("office转换服务启动成功!");
} catch (Exception e) {
System.out.println("office转换服务启动失败!详细信息:" + e);
}
}

/**
* stop openOffice service.
*/

public static void stopService() {
System.out.println("准备关闭office转换服务....");
if (officeManager != null) {
officeManager.stop();
}
System.out.println("office转换服务关闭成功!");
}

public static void convertToPDF(String inputFile, String outputFile) {
startService();
System.out.println("进行文档转换转换:" + inputFile + " --> " + outputFile);
OfficeDocumentConverter converter = new OfficeDocumentConverter(officeManager);
converter.convert(new File(inputFile), new File(outputFile));
stopService();
}

public static void main(String[] args) {
convertToPDF("/Users/koolearn/Desktop/asdf.docx", "/Users/koolearn/Desktop/adsf.pdf");
}
}

2.kkFileView

github地址:github.com/kekingcn/kk…

支持的文件预览格式非常丰富 image.png

接下来是 从零到一 的启动步骤,按着步骤来,任何人都能搞定

  1. 安装java:
brew install java
  1. 安装maven,java的包管理工具:
brew install mvn
  1. 检查是否安装成功

执行java --versionmvn -v。我这里遇到mvn找不到java home的报错。解决方式如下:

我用的是zsh,所以需要去.zshrc添加路径:

export JAVA_HOME=$(/usr/libexec/java_home)

添加完后,执行

source .zshrc
  1. 安装下libreoffice:

kkFileView明确要求的额外依赖,否则无法启动

brew install libreoffice 
  1. mvn安装依赖

进入项目,在根目录执行依赖安装,同时清理缓存,跳过单测(遇到了单测报错的问题)

mvn clean install -DskipTests
  1. 启动项目

找到主文件,主函数mian,点击vscode上面的Run即可执行,路径如下图

image.png

  1. 访问页面

启动完成后,点击终端输出的地址

image.png

  1. 最终结果

最终展示如下,可以添加链接进行预览,也可以选择本地文件进行预览

image.png

预览效果非常好

3.onlyOffice

官网地址:http://www.onlyoffice.com/zh

github地址:github.com/ONLYOFFICE

开发者版本和社区版免费,企业版付费:http://www.onlyoffice.com/zh/docs-ent…

预览的文件种类没有kkFileView多,但对office三件套有很好的支持,甚至支持多人编辑。

四.总结

  1. 外部服务,推荐微软的view.officeapps.live.com/op/view.aspx,但只建议预览一些互联网公开的文件,不建议使用在要求保密性和稳定性的文件。
  2. 对保密性和稳定性有要求,且不差钱的,可以试试大厂服务,阿里云解决方案。
  3. 服务端技术比较给力的,使用服务端预览方案。目前最好最全的效果是服务端预览方案。
  4. 不想花钱,没有服务器的,使用前端预览方案,客户端渲染零成本。

五.参考文档:



    作者:然燃
    来源:juejin.cn/post/7268530145208451124
    收起阅读 »

    程序员男盆友给自己做了一款增进感情的小程序

    web
    前言 又是无聊的一天,逛GitHub的时候发现一个给女朋友做了一个互动微信小程序,据说女朋友更爱自己了,所以当晚。。。。给自己做了丰盛的晚餐,我当即点开立马开发粘贴复制起来,想到做的小程序可以和未来的女朋友增进感觉,越加猩粪。。。 回到正题,这个库有1.1k的...
    继续阅读 »

    前言


    又是无聊的一天,逛GitHub的时候发现一个给女朋友做了一个互动微信小程序,据说女朋友更爱自己了,所以当晚。。。。给自己做了丰盛的晚餐,我当即点开立马开发粘贴复制起来,想到做的小程序可以和未来的女朋友增进感觉,越加猩粪。。。


    回到正题,这个库有1.1k的star,推荐新人入坑原生小程序的可以学习


    项目地址:github.com/UxxHans/Rai…


    image.png


    云开发情侣互动小程序(做任务,攒积分,换商品)


    这是使用云开发能力构建的情侣互动小程序,可以跟女朋友互动哦,其中使用了云开发基础能力的使用:



    • 数据库:对文档型数据库进行读写和管理

    • 云函数:在云端运行的代码,开发者只需编写业务逻辑代码


    使用逻辑


    打个比方:



    • 女朋友发布任务->女朋友来做任务->做完后由你来确认完成->女朋友收到积分

    • 你发布商品(洗碗券)->女朋友使用积分购买->商品进入到女朋友的库存->女朋友拿着洗碗券叫你洗碗->你洗碗->女朋友将物品(洗碗券)标记为已使用(不可逆)

    • 这样做的原因是 不想给任何一方能自说自话 增加自己或者对方积分的能力[点击完成任务的人不能是获得积分的人也不能是自己]


    版本新增



    • 将所有非云函数的云逻辑封装为云函数

    • 新增了仓库系统,购买了的商品会存入仓库,然后再被使用

    • 新增了搜索框,可以搜索物品和任务

    • 新增了滑动窗,可以自动播放显示多张图片

    • 新增了商品和任务预设,添加商品或任务可以使用预设,非常迅速

    • 将新增按钮变为可拖拽的页面悬浮按钮

    • 购买,上架,新建任务的时间都会被记录并显示

    • 取消了点击左边圆圈来完成或者购买,统一改为左滑菜单

    • 左滑菜单统一用图标显示,更加精简

    • 使用特效升级了详细信息页面与添加页面的美观度

    • 添加任务或物品界面积分文本框改为滑块

    • 在商城添加了顶栏显示积分,更直观

    • 使用表情符号简单的增加了美感


    效果图与动画


    Animation.gif
    image.png


    部署方式



    image.png



    • 登录之后先在主页完成小程序信息类目

    • 然后可以在管理中的版本管理成员管理中发布小程序体验版并邀请对象使用


    image.png



    • 随后可以在开发中的开发工具里下载微信开发者工具

    • 打开微信开发工具->登录->导入我的文件夹-进入工具

    • 在左上角五个选项中选择云开发->按照提示开通云开发(这里可以选择免费的,不过限量,我开发用的多,6块够用了)


    image.png



    • 进入后点击数据库->在集合名称添加四个集合:MarketListMissionListStorageListUserList

    • 之前使用过上一个版本的,需要清空所有数据,因为字段结构不一样


    image.png



    • UserList中添加两个默认记录, 在两个记录中分别添加两个字段:


    字段 = _openid | 类型 = string | 值 = 先不填
    字段 = credit | 类型 = number | 值 = 0


    • 打开云开发的控制台的概览选项->复制环境ID

    • 打开 miniprogram/envList.js 将内容全部替换成如下,注意替换环境ID


    module.exports = {
    envList: [{
    envId:'上述步骤中你获得的环境ID (保留单引号)'
    }]
    }


    • 右键点击 cloudfunctions 中的每个文件夹并选择云函数云端安装依赖上传 (有点麻烦但是这是一定要做的)


    image.png



    • 如果云开发里面的云函数页面是这样的就是成功了


    image.png



    • 没有安装npm或者NodeJs, 需要先在这里安装: nodejs.org/dist/v16.15…

    • 安装好的,就直接运行cloudfunctions/Install-WX-Server-SDK.bat

    • 不成功的话可以在命令行输入 npm install --save wx-server-sdk@latest

    • 然后创建体验版小程序->通过开发者账号分享到女朋友手机上(要先登录小程序开发者账号)

    • 在两个手机上运行小程序->分别在两个手机上的小程序里新建任务

    • 然后回到云开发控制台的missionlist数据库集合->找自己和女朋友的_openid变量并记录

    • 把这两个记录下来的_openid拷贝到云开发控制台UserList数据集合里刚刚没填的_openid变量中

    • 把这两个记录下来的_openid拷贝到miniprogram/app.js里的_openidA_openidB的值里(A是卡比,B是瓦豆)

    • miniprogram/app.js里把userAuserB改成自己和女朋友的名字

    • 然后再试试看是不是成功了! (别忘了任务和物品左滑可以完成和购买)

    • 消息提醒功能:

    • 参考blog.csdn.net/hell_orld/a…allsobaiduend~default-2-110675777-null-null.142^v87^insert_down28v1,239^v2^insert_chatgpt&utm_term=%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%80%9A%E7%9F%A5%E4%BA%91%E5%BC%80%E5%8F%91&spm=1018.2226.3001.4187配置自己想要的模板\

    • miniprogram/pages/MainPage/index.jsminiprogram/pages/MissionAdd/index.js里把模板号换成自己想要的模板号

    • cloudfunctions/information/index.js里把UserA和UserB的openid值进行修改就能使用消息提醒功能了


    image.png



    • 别忘了最后点击右上角上传->然后在开发者账号上设置小程序为体验版->不用去发布去审核


    image.png



    旧版效果图


    image.png


    作者:嚣张农民
    来源:juejin.cn/post/7298966889358196788
    收起阅读 »

    一个轻量的后台管理模板

    web
    特色: 当前模板将自定义样式配置通过css变量的方式提取了出来,直接通过可视化去配置成你喜欢的样式效果,在下面预览地址中可以体验。 预览地址 可视化配置面版不够好看的话,可以把地址上的vue-admin改为vue-admin-el 项目地址 描述 无UI框...
    继续阅读 »

    特色: 当前模板将自定义样式配置通过css变量的方式提取了出来,直接通过可视化去配置成你喜欢的样式效果,在下面预览地址中可以体验。



    描述


    无UI框架依赖的后台管理模板


    当前项目是基于vue.js去实现的一套后台管理模板,早在2019年就已经在持续迭代,目前已经是较新的vue3.x版本;


    因为在中后台项目中,大多数核心功能只有页面框架样式侧边菜单栏功能,所以除了底层 js 框架vue+vue-router以外,所有样式、功能都采用自行实现方式;之所以不使用第三方UI库的理由是:



    • 不受UI框架的约束,可以使用任何一款自己喜欢的第三方库;

    • 轻量化,因为用到的依赖极少,所以体积非常轻量,同时保证了常用到的大部分功能保留;所有的工程化配置根据自身需求去加入即可,当前模板只做代码减法;

    • 兼容性、拓展性高,模板中每个部分都是可以独立抽离和替换的,并无上手成本;当在引用某一款UI库使用时,直接引入依赖并使用即可,无需修改模板已有功能组件;

    • 别人写的模板代码太多了,都不好改!


    当前模板项目的 package.json 做到了极致的精简


    {
    "dependencies": {
    "nprogress": "0.2.0",
    "vue": "3.4.21",
    "vue-router": "4.3.0"
    },
    "devDependencies": {
    "@types/node": "20.11.28",
    "@types/nprogress": "0.2.0",
    "@vitejs/plugin-vue": "5.0.4",
    "@vitejs/plugin-vue-jsx": "3.1.0",
    "sass": "~1.71.0",
    "typescript": "~5.4.0",
    "vite": "5.2.8",
    "vue-tsc": "~1.8.0"
    }
    }

    功能目录清单



    • vue-router 权限路由功能、路由记录初始进入路径功能

    • layout 部分:可视化配置样式功能、顶部伸缩布局 + 多级侧边菜单栏、路由面包屑、路由历史记录标签栏、整体自适应窗口大小布局、滚动条(类似

    • utils 只保留使用频率极高的:日期格式化、复制、类型判断、网络请求、和一些核心功能函数

    • UI控件 + 通用组件:消息提示条、对话框、高度自适应折叠组件、dialog 组件


    layout 核心布局整体


    大多数情况开发者在选用开源模板时,只是为了侧边菜单栏和顶部的布局不同而选择对应的模板,所以当前项目直接将两种布局写成可以动态切换,并且加入可视化的样式配置操作,这样连css代码都不需要去看了:


    微信截图_20240327154406.png


    侧边菜单栏为什么没有整一个折叠缩略的功能?理由是我觉得这个操作逻辑不是那么的理想,缩小后,我需要鼠标一层一层的放上去找到需要的子菜单,这一点都不方便;而且缩小菜单的目的是为了获得更大内容可视区域,所以缩小后的菜单依然还占用了一部分空间,同时使用功能变得繁琐,那干脆在收起菜单时,将她整个推出屏幕区域,这样就能使可视区域最大化。


    微信截图_20240327152829.png


    路由权限设置


    完全继承了vue-router的数据结构,只在meta对象中加入auth作为路由数组过滤操作去实现权限控制;另外根部对象的name字段则作为路由缓存的唯一值。


    import { RouteRecordRaw } from "vue-router";

    export interface RouteMeta {
    /** 侧边栏菜单名、document.title */
    title: string
    /** 外链地址,优先级会比`path`高 */
    link?: string
    /** `svg`名 */
    icon?: string
    /** 是否在侧边菜单栏不显示该路由 */
    hidden?: boolean
    /**
    * 路由是否需要缓存
    * - 当设置该值为`true`时,路由必须要设置`name`,页面组件中的`name`也是,不然路由缓存不生效
    */

    keepAlive?: boolean
    /**
    * 可以访问该权限的用户类型数组,与`userInfo.type`对应;
    * 传空数组或者不写该字段代表可以全部用户访问
    *
    * | number | 用户类型 |
    * | --- | --- |
    * | 0 | 超级管理员 |
    * | 1 | 普通用户 |
    */

    auth?: Array<number>
    }

    /** 自定义的路由类型-继承`RouteRecordRaw` */
    export type RouteItem = {
    /**
    * 路由名,类似唯一`key`
    * - 路由第一层必须要设置,因为动态路由删除时需要用到,且唯一
    * - 当设置`meta.keepAlive`为`true`时,该值必填,且唯一,另外组件中的`name`也需要对应的同步设置,不然路由缓存不生效
    */

    name?: string
    /** 子级路由 */
    children?: Array<RouteItem>
    /** 标头 */
    meta: RouteMeta
    } & RouteRecordRaw


    代码演示


    状态管理


    Vue3之后不需要Vuex了(虽然我在Vue2中也没用),而是采用另外一种更简单的方式:参考 你不需要vuex


    ts的项目中,因为可以用Readonly去声明状态对象,所以这套程序设计会发挥得最好,具体示例可以在src/store/README.md中查看


    网络请求


    这里我使用的是根据个人习惯用原生写的ajax代码地址


    理由是:



    • 代码少,功能足以覆盖常用的大部分场景

    • ts中可以更友好的声明接口返回类型


    文件:api.ts request中的泛型不是必须的,不传下面 .vue 文件中res.data中的类型则是any


    export interface TableItem {
    id: number
    type: "load" | "update"
    time: string
    }

    /**
    * @param params
    */

    export function getData(params: PageInfo) {
    return request<Api.List<TableItem>>("GET", "/getList", params)
    }

    文件:demo.vue 建议直接在vscode中用鼠标去看提示,那样会更加的直观


    <script lang="ts" steup>
    import { ref } from "vue";
    import { type TableItem, getData } from "@/api.ts";

    const tableData = ref<Array<TableItem>>([]);

    async function getTableData() {
    const res = await getData({
    pageSize: 10,
    currentPage: 1
    })
    if (res.code === 1) {
    tableData.value = res.data.list; // 这里的 .list 就是接口 传入的类型 TableItem
    // do some...
    }
    }
    script>

    强力建议请求函数的封装时,都始终执行 Promise.resolve 去作为正确和错误的响应。接口获取后始终以res.code === 1为判断成功,无需在内部用 try + catch 去包一层


    更多使用示例请在src/api/README.md中查看



    另外可根据自己喜好可以扩展 axios 这类型第三方库。



    SVG 图标组件


    使用方式:到阿里云图标库中下载想要的图标,然后下载svg文件,最后放到src/icons/svg目录下即可


    也是自己写的一个加载器,代码十分简单:


    import { readFileSync, readdirSync } from "fs";

    // svg-sprite-loader 这个貌似在 vite 中用不了
    // 该文件只能作为`vite.config.ts`导入使用
    // 其他地方导入会报错,因为浏览器环境不支持`fs`模块

    /** `id`前缀 */
    let idPerfix = "";

    const svgTitle = /+].*?)>/;

    const clearHeightWidth = /(width|height)="([^>+].*?)"/g;

    const hasViewBox = /(viewBox="[^>+].*?")/g;

    const clearReturn = /(\r)|(\n)/g;

    /**
    * 查找`svg`文件
    * @param dir 文件目录
    */

    function findSvgFile(dir: string): Array<string> {
    const svgRes: Array<string> = []
    const dirents = readdirSync(dir, {
    withFileTypes: true
    });
    dirents.forEach(function(dirent) {
    if (dirent.isDirectory()) {
    svgRes.push(...findSvgFile(dir + dirent.name + "/"));
    } else {
    const svg = readFileSync(dir + dirent.name).toString().replace(clearReturn, "").replace(svgTitle, function(_, group) {
    // console.log(++i)
    // console.log(dirent.name)
    let width = 0;
    let height = 0;
    let content = group.replace(clearHeightWidth, function(val1: string, val2: string, val3: number) {
    if (val2 === "width") {
    width = val3;
    } else if (val2 === "height") {
    height = val3;
    }
    return "";
    });
    if (!hasViewBox.test(group)) {
    content += `viewBox="0 0 ${width} ${height}"`;
    }
    return `${idPerfix}-${dirent.name.replace(".svg", "")}" ${content}>`;
    }).replace("", "");
    svgRes.push(svg);
    }
    });
    return svgRes;
    }

    /**
    * `svg`打包器
    * @param path 资源路径
    * @param perfix 后缀名(标签`id`前缀)
    */

    export function svgBuilder(path: string, perfix = "icon") {
    if (path.trim() === "") return;
    idPerfix = perfix;
    const res = findSvgFile(path);
    // console.log(res.length)
    return {
    name: "svg-transform",
    transformIndexHtml(html: string) {
    return html.replace("",
    `

    ${res.join("")}
    `
    )
    }
    }
    }


    作者:黄景圣
    来源:juejin.cn/post/7350874162011750400
    收起阅读 »

    我们Model3也要有自己的预览网站!

    web
    通过Three.js创建一个互动的在线展示平台,可视化特斯拉Model 3的的部分技术。网站利用Three.js提供的API,实现了Model 3的三维模型展示、动画效果以及与用户交互的功能。预览地址:model3.newhao2021.top/github地...
    继续阅读 »

    preview.gif

    通过Three.js创建一个互动的在线展示平台,可视化特斯拉Model 3的的部分技术。网站利用Three.js提供的API,实现了Model 3的三维模型展示、动画效果以及与用户交互的功能。

    预览地址:model3.newhao2021.top/

    github地址:github.com/varrff/Mode…

    使用

    安装依赖

    pnpm i

    本地调试

    pnpm run dev

    构建

    pnpm run build

    预览

    pnpm run preview

    关键概念

    Catmull-Rom样条曲线

    Catmull-Rom样条曲线是一种平滑的插值曲线,可以用于创建自然的路径和轨迹。在Three.js中,THREE.CatmullRomCurve3类用于生成三维空间中的Catmull-Rom样条曲线。该曲线通过一组控制点进行插值,生成光滑的曲线,常用于动画路径、相机路径等。

    这里使用了样条曲线创建了Autopilot部分的距离预警线

    管道几何体(Tube Geometry)

    管道几何体(Tube Geometry)是Three.js中用于创建沿着一条路径生成的管状三维几何体的类。这种几何体在表示道路、轨迹、隧道等需要具有实际厚度的三维结构时非常有用。下面我们将详细介绍管道几何体的概念、创建方法及其应用。

    这里使用了样条曲线创建了FSD部分的行驶预测路线

    代码部分

    World文件结构

    - src
    - World
    - CameraShake.ts: 摄像机抖动效果文件。
    - Car.ts: 汽车部分。
    - City.ts: Autopilot部分文件。
    - Road.ts: FSD部分文件。
    - Speedup.ts: 加速效果文件。
    - StartRoom.ts: 起始房间对象文件。
    - TestObject.ts: 测试对象文件。
    - World.ts: 世界管理文件,负责加载和管理整个场景中的所有对象和效果。

    首页部分

    首页的加速流光效果以及相机抖动部分推荐alphardex大佬的文章:juejin.cn/post/735276…

    也非常感谢大佬热心帮助我解决了部分问题。

    Autopilot部分(road.ts)

    addExisting 方法

    这个方法用于将现有的模型添加到场景中,并启动动画。

    • 加载模型: 从base.am.items中获取已经加载的GLTF模型。
    • 设置模型位置和缩放: 调整模型的位置和缩放比例,使其适应场景。
    • 添加模型到容器: 将模型添加到当前组件的容器中。

    run 方法

    负责启动模型的动画循环。

    • 启动汽车运行: 调用carRun方法开始汽车动画。
    • 递归动画: 使用requestAnimationFrame进行递归动画,每帧更新模型的位置。

    carRun 方法

    用于处理汽车模型的动画效果。

    • 克隆模型: 使用SkeletonUtils.clone确保每次都是新的克隆对象。
    • 设置材质: 创建并应用新的材质,使汽车模型支持光照和反射。
    • 创建护盾: 调用createShield方法生成护盾效果。
    • 添加汽车到容器: 将新的汽车模型添加到容器中。
    • 定义动画参数和函数: 定义汽车动画的参数和递归动画函数animateCar

    createShield 方法

    用于创建护盾效果。

    • 定义控制点: 使用THREE.Vector3定义护盾的路径控制点。
    • 创建曲线和几何体: 用THREE.CatmullRomCurve3创建样条曲线,并生成对应的管道几何体。
    • 创建材质和纹理: 用Canvas创建线性渐变纹理,并应用到管道材质上。
    • 设置动画: 使用gsap实现护盾渐变动画和控制点的动态更新。

    updateControlPoint 方法

    更新控制点的位置,使护盾效果更加动态。

    • 递增或递减操作: 根据目标值和步长更新控制点的x和z坐标。
    • 更新曲线和几何体: 更新样条曲线的控制点,并重新生成管道几何体。

    removeAllModelsAndAnimations 方法

    用于移除所有模型并停止所有动画。

    • 移除模型和对象: 从容器中移除道路模型、管道和汽车模型,并释放相关资源。
    • 停止动画循环: 取消所有动画帧请求,停止动画。

    playAuto 方法

    用于播放背景音乐。

    • 加载和播放音乐: 使用Howl.js库加载并播放背景音乐。

    FSD部分(city.ts)

    setCar 方法

    用于设置汽车模型,目前只是加载了汽车模型数据。

    createRoad 方法

    用于创建道路。

    • 定义控制点: 使用THREE.Vector3定义道路的路径控制点。
    • 更新道路几何体: 调用updateRoadGeometry方法,根据控制点创建道路几何体。
    • 设置材质和动画: 创建材质并使用GSAP动画库实现过渡动画。

    updateRoadGeometry 方法

    更新道路几何体。

    • 检查控制点: 确认控制点存在。
    • 创建曲线和几何体: 用THREE.CatmullRomCurve3创建样条曲线,并生成管道几何体。
    • 调整顶点位置: 调整几何体顶点的y坐标。
    • 创建材质和纹理: 用Canvas创建线性渐变纹理,并应用到管道材质上。
    • 更新或创建道路对象: 更新现有道路对象的几何体或创建新的道路对象并添加到场景中。

    updateControlPoint 方法

    更新控制点的位置,使道路效果更加动态。

    • 递增或递减操作: 根据目标值和步长更新控制点的x和z坐标。
    • 更新道路几何体: 调用updateRoadGeometry方法更新几何体。

    runRoad 方法

    负责启动道路的动画。

    • 定义多个动画步骤: 使用GSAP库定义一系列动画,平滑地移动和旋转模型。
    • 启动控制点动画: 定义和启动控制点更新动画,使道路效果动态变化。

    不足

    1. 特效效果还是没办法跟大佬的比,有待优化。
    2. 手机上横屏时控制器依然是竖屏的逻辑,没有翻转。
    3. 在手机上显示时,由于刷新率的不同,FSD部分的路线动画会有延迟。

    感慨

    也不知道什么时候能买上一辆Model3嘞,第一次看见总觉得这个流线感真好看,也不像豪车那样的价格遥不可及,尽管后来特斯拉的车都成了街车了,也有太多国产电车后来居上,本地化做的也比特斯拉好。但每次看见,都依然觉得真t*好看。


    作者:超级无敌攻城狮
    来源:juejin.cn/post/7378459137418838016
    收起阅读 »