注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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
    收起阅读 »

    每一个前端,都要拥有属于自己的埋点库~

    web
    前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 简介 sunshine-track 应用于前端监控, 基于 行为上报,实现了 用户行为、错误监控、页面跳转、页面白屏检测、页面性能检测等上报功能。适用于 Vu...
    继续阅读 »

    前言


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



    简介


    sunshine-track 应用于前端监控, 基于 行为上报,实现了 用户行为、错误监控、页面跳转、页面白屏检测、页面性能检测等上报功能。适用于 Vue、React、Angular 等框架



    本项目源码:github.com/sanxin-lin/…
    各位兄弟姐妹如果觉得喜欢的话,可以点个star 哦~



    功能


    sunshine-track具备以下功能:



    • ✅ 用户行为上报:包括 点击、跳转页面、跳转页面记录数组、请求

    • ✅ 用户手动上报:提供 Vue 自定义指令 以及add、report函数,实现用户手动上报

    • ✅ 自定义上报:提供 格式化上报数据、自定义上报函数、自定义决定上不上报 等配置项,更灵活地上报数据

    • ✅ 请求数据上报:提供 检测请求返回、过滤请求 等配置项,让用户决定上报哪些请求数据

    • ✅ 上报方式:提供 上报方式 配置项,用户可选择 img、http、beacon 三种方式,http方式又支持 xhr、fetch 两种,且支持 自定义headers

    • ✅ 上报数据缓存:可配置 本地缓存、浏览器本地缓存、IndexedDB 三种方式

    • ✅ 上报数据阈值:可配置上报数据 阈值 ,达到 阈值 后进行上报操作

    • ✅ 全局点击上报:可通过配置 选择器、元素文本,对全局DOM节点进行点击上报

    • ✅ 页面的性能检测,包括 白屏、FP、FCP、LCP、CLS、TTFB、FID


    上报数据格式


    选项描述类型
    uuid   上报数据的idstring
    type   上报数据的类型string
    data   上报数据any
    time    上报时间number
    status    上报状态string
    domain    当前域名string
    href    当前网页路径string
    userAgent    当前user-agentstring
    deviceInfo   设备的相关信息object

    安装



    使用



    全局点击监听


    可以通过配置globalClickListeners来对于某些DOM节点进行点击监听上报



    配置上报阈值


    上报分为几种:



    • 用户行为上报:点击、跳转页面、请求,这些上报数据会缓存着,当达到阈值时再进行上报

    • 错误上报:请求报错、代码报错、异步错误,这些是立即上报

    • 页面性能上报:白屏、FP、FCP、LCP、CLS、TTFB、FID,这些是立即上报


    用户行为上报的阈值默认是 10,支持自定义 maxEvents



    配置缓存方式


    如果你想要避免用户重新打开网页之后,造成上报数据的丢失,那么你可以配置缓存方式,通过配置cacheType



    • normal:默认,本地缓存

    • storage:浏览器 localStorage 本地缓存

    • db:浏览器 IndexedDB 本地缓存


    app.use(Track, {
    ...options,
    cacheType: 'storage' // 配置缓存方式
    })

    打印上报数据


    可以通过配置 log ,开启打印上报数据



    灵活上报请求数据


    请求也是一种行为,也是需要上报的,或许我们有这个需求



    • 过滤:某些请求我们并不想上报

    • 自定义校验请求响应数据:每个项目的响应规则可能都不同,我们想自己判断哪些响应是成功,哪些是失败



    格式化上报数据、自定义决定上不上报、自定义上报


    如果你想在数据上报之前,格式化上报数据的话,可以配置report中的format



    如果你想要自己决定某次上报的时候,进行取消,可以配置report中的isReport



    如果你不想用这个库自带的上报功能,想要自己上报,可以配置report中的customReport



    手动上报


    手动上报分为三种:



    • 手动添加上报数据:添加到缓存中,等到达到阈值再上报

    • 手动执行数据上报:立即上报

    • 自定义指令上报:如果你是 Vue 项目,支持指令上报



    如果你是 Vue 项目,可以使用指令v-track进行上报



    配置参数


    选项描述类型
    projectKey   项目keystring
    userId   用户idstring
    report.url   上报urlstring
    report.reportType  上报方式img、http、beacon
    report.headers  上报自定义请求头,http 上报模式生效object
    report.format  上报数据格式化function
    report.customReport  自定义上报function
    report.isReport  自定义决定上不上报function
    cacheType   数据缓存方式normal、storage、db
    globalClickListeners   上报状态array
    log   当前域名boolean
    maxEvents   上报阈值number
    historyUrlsNum   需要记录的url跳转数组number
    checkHttpStatus   判断响应数据function
    filterHttpUrl   过滤上报请求数据function
    switchs.xhr   是否开启xhr请求上报boolean
    switchs.fetch   是否开启fetch请求上报boolean
    switchs.error   是否开启错误上报boolean
    switchs.whitescreen   是否开启白屏检测上报boolean
    switchs.hashchange   是否开启hash变化请求上报boolean
    switchs.history   是否开启history变化上报boolean
    switchs.performance   是否开启页面性能上报boolean


    本项目源码:github.com/sanxin-lin/…
    各位兄弟姐妹如果觉得喜欢的话,可以点个star 哦~





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

    给圆点添加呼吸动画,老板说我很有想法

    web
    需求简介 这几天老板安排了一个活:要实现一些异常信息点的展示,展示的方式就是画一个红色的点。 需求很简单,我也快速实现了。但是想着我刚入职不久,所以得想办法表现一下自己。于是,我自作主张,决定给这个小圆点实现一个呼吸的效果动画。 实现方案 要实现这样一个小...
    继续阅读 »

    需求简介


    这几天老板安排了一个活:要实现一些异常信息点的展示,展示的方式就是画一个红色的点



    需求很简单,我也快速实现了。但是想着我刚入职不久,所以得想办法表现一下自己。于是,我自作主张,决定给这个小圆点实现一个呼吸的效果动画



    实现方案


    要实现这样一个小圆点的动画非常简单,借助css的animation实现即可


    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Breathing Circle Animation</title>
    <link rel="stylesheet" href="styles.css">
    </head>
    <body>
    // 白边红色小圆点
    <div class="dot">
    // 小圆点的背景元素
    <div class="breathing-background"></div>
    </div>
    </body>
    </html>

    .dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    border: 2px solid #fff;
    border-radius: 50%;
    background-color: red;
    position: relative;
    z-index: 1;
    }
    .breathing-background {
    position: absolute;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    opacity: 0.2;
    animation: breathing 2s cubic-bezier(0, 0, 0.25, 1) infinite;
    }
    // 动画 变大再变小
    @keyframes breathing {
    0% {
    transform: scale(1);
    }
    50% {
    transform: scale(5);
    opacity: 0.2;
    }
    100% {
    transform: scale(5);
    opacity: 0;
    }
    }

    上面的动画实现主要依赖于CSS关键帧动画和定位属性。



    • 定位:通过设置.dot为相对定位(position: relative)和.breathing-background为绝对定位(position: absolute),确保两个元素在同一个位置上重叠。

    • 层叠顺序:使用z-index属性确保.dot在.breathing-background的前面,从而保证红色小圆点在呼吸动画背景上显示。

    • 动画效果:@keyframes breathing定义了从正常尺寸到放大再到透明的动画过程,通过transform: scale和opacity属性的变化来实现呼吸效果。

    • 动画循环:通过animation属性设置动画的持续时间、缓动函数和无限循环,使呼吸动画效果持续进行。


    上面的代码很简单,实现的效果也简单粗暴



    老板反应


    做完之后,我很高兴的就提交代码了,我很满意自己小改动。

    过了很久,老板看后,把我叫到办公室,深色凝重的说了一句:你很有想法


    随后老板又问我,你加这个闪烁的背景想表达啥?


    我一时语塞,解释:这样不是看起来更好看,更能清晰的表达这个异常的状态吗?


    老板又怼我,谁让你乱加动画了?时间多的没处用是吧?删了。


    我不太理解老板为啥生气,回去后也是默默地删除了代码。。。。。



    后来我反思了一下,程序员还是别乱加自己的想法在需求里,毕竟我们还是不懂产品,做的越多,错的越多。做好本分工作就行了。



    作者:快乐就是哈哈哈
    来源:juejin.cn/post/7376172288977879091
    收起阅读 »

    🚀独立开发,做的页面不好看?我总结了一些工具与方法🚀

    web
    前言 我有时候会自己开发一些项目,但是不比在公司里面,自己开发项目的时候没有设计稿,所以做出来的页面比较难看。 开发了几个项目之后,我也总结了以下的一些画页面的资源或者方法,希望对大家有帮助~ 颜色&字体 这一部分主要参考的是antd的方案,主要包括颜...
    继续阅读 »

    前言


    我有时候会自己开发一些项目,但是不比在公司里面,自己开发项目的时候没有设计稿,所以做出来的页面比较难看。


    开发了几个项目之后,我也总结了以下的一些画页面的资源或者方法,希望对大家有帮助~


    颜色&字体


    这一部分主要参考的是antd的方案,主要包括颜色与字体(包括字体的颜色、大小)的使用与搭配。


    颜色


    对于颜色来说,整个站点最好有一个主题色,然后有一种色彩生成算法,基于这个主题色去生成一套色板。在 antd 的官网中共计 120 个颜色,包含 12 个主色以及衍生色。


    image.png


    12 种颜色方案都是比较好看的,如果你想定义自己的主题色,这里也有一个色板生成工具


    image.png


    同样你也可以将这套色板生成算法引入到你的程序中,这是他的npm包


    确认好主题色之后,再来看看中性色。


    image.png


    这里它也提供了我们相对常用的一些中性色,有了主题色与中性色之后,我们就可以定义一个 less/sass 文件,把我们常用的这些颜色写成变量导入使用,确保我们的站点色彩是保持统一的。


    @primary-color: #1890ff;
    @primary-text-color: #000000e0;
    @first-text-color: #000000e0;
    @sceond-text-color: #000000a6;
    @border-color: #d9d9d9ff;
    @disabled-color: #00000040;
    @divider-color: #0505050f;
    @background-color: #f5f5f5ff;

    这几种色彩看起来如下:


    image.png


    字号


    image.png


    antd 中,它同样对字体大小也有着十分深厚的研究,我们这里就简单一点,大多数浏览器的默认字体大小是 16px,我们就以这个值为基准,来设计 5 个字号如下:


    @smallest-size: 12px;
    @small-size: 14px;
    @size: 16px;
    @large-size: 20px;
    @largest-size: 24px;

    这五种字号看起来如下:


    image.png


    渐变


    UI 设计中,渐变是一种将两种或多种颜色逐渐过渡或混合在一起的效果。渐变可以增加界面的视觉吸引力、深度和层次感,并帮助引导用户的视线,提高用户体验。


    渐变在以下几个方面有着重要的意义:



    1. 引导视线:通过渐变的色彩变化,可以引导用户的视线,突出重要内容或者引导用户进行特定的操作。

    2. 增加层次感:渐变可以使界面元素看起来更具立体感和深度,提高UI设计的质感和视觉吸引力。

    3. 提升品牌形象:使用特定颜色的渐变可以帮助强化品牌形象,让界面更具有品牌特色和辨识度。

    4. 增强用户体验:合理使用渐变可以使界面更加舒适和美观,从而提升用户体验和用户满意度。


    这里我一般用的是这个渐变生成工具,可以比较方便的调出来需要的渐变色,支持生成多种渐变色+代码,并支持实时预览。


    image.png


    阴影


    同时,阴影在UI设计中也是不可或缺的部分,它有如下几个重要的意义:



    1. 层次感和深度感:阴影可以帮助界面元素之间建立层次感和深度感。通过添加阴影,设计师可以模拟光源的位置和界面元素之间的距离,使得用户能够更清晰地理解界面的结构。

    2. 突出重点:阴影可以用来突出重点,比如突出显示某个按钮或者卡片。适当的阴影可以使重要的元素脱颖而出,引导用户的注意力。

    3. 视觉吸引力:精心设计的阴影可以增加界面的美感和吸引力。合适的阴影可以使界面看起来更加立体和生动,从而提升用户的体验。

    4. 可视化元素状态:阴影还可以用来表达界面元素的状态,比如悬停或者按下状态。通过微调阴影的属性,可以使用户更清晰地感知到界面元素的交互状态。


    我一般用这个阴影生成工具,它同样也支持在线修改多个阴影及预览,同时支持复制代码。


    image.png


    字体图标


    想让我们的网页更生动,那怎么能少的了一个个可爱的 icon 呢,下面就是几个开源 icon 的网站。



    image.png



    image.png



    image.png



    image.png



    image.png


    图片素材


    除了 icon 之外,图片素材也是必不可少的,这里介绍我主要用的两个网站。


    第一个是花瓣网,这个网站可能找过素材的同学都不会陌生,上面确实有大量的素材供你选择。


    image.png


    另外一个是可画,它是一个图像编辑器,但是提供了大量的模版,我们也很轻松可以从中提取素材。


    image.png


    组件库


    最后要介绍的是组件库,组件库一来可以提供大量的基础组件,降低开发成本,而来也可以让我们站点的交互更加统一。以下是我常用的组件库:



    最后


    以上就是我独立开发项目时会思考以及参照的工具,如果你有一些其他想法,欢迎评论区交流。觉得有意思的话,点点关注点点赞吧~


    作者:可乐鸡翅kele
    来源:juejin.cn/post/7359854125912227894
    收起阅读 »

    【技巧】JS代码这么写,前端小姐姐都会爱上你

    web
    前言 🍊缘由 JS代码小技巧,教你如何守株待妹 🍍你想听的故事: 顶着『前端小王子』的称号,却无法施展自己的才能。 想当年本狗赤手空拳打入前端阵地,就是想通过技术的制高点来带动前端妹子。奈何时不待我,前端妹子成了稀有资源,只剩下抠脚大汉前端大叔。 秉承没有妹...
    继续阅读 »

    前言


    🍊缘由


    JS代码小技巧,教你如何守株待妹



    🍍你想听的故事:


    顶着『前端小王子』的称号,却无法施展自己的才能


    想当年本狗赤手空拳打入前端阵地,就是想通过技术的制高点来带动前端妹子。奈何时不待我,前端妹子成了稀有资源,只剩下抠脚大汉前端大叔。


    秉承没有妹子也得继续学习的态度,本狗将实际代码编写中JS使用技巧总结。分享给小伙伴们,希望这些姿势知识 能够成为吸引妹子的引路石。


    正文


    一.JS解构赋值妙用


    1.采用短路语法防止报错



    解构时加入短路语法兜底,防止解构对象如果为 undefined 、null 时,会报错



    const user = null;
    // 短路语法,如果user为undefined 、null则以{}作为解构对象
    const {name, age, sex} = user || {};

    举例🌰


    通过接口获取用户user对象,解构对象信息


    ❌错误示例


    未使用短路语法兜底,不严谨写法


    // 模拟后端接口返回user为null时
    const user = null;
    const {name, age, sex} = user;
    console.log("用户信息name=", name, "age=", age, "sex=", sex);

    // 控制台直接报错
    // Cannot destructure property 'name' of 'user' as it is null.


    ✅正确示例


    使用短路语法兜底,严谨写法


    // 模拟后端接口返回user为null时
    const user = null;
    // 加入短路语法,意思为如果user为空则以{}作为解构对象
    const {name, age, sex} = user || {};
    console.log("用户信息name=", name, "age=", age, "sex=", sex);

    // 控制台打印
    // 用户信息name= undefined age= undefined sex= undefined


    2.深度解构



    解构赋值可以深度解构:嵌套的对象也可以通过解构进行赋值



    举例🌰


    通过模拟接口获取用户user对象,解构user对象中联系人concat信息


    // 深度解构
    const user = {
    name:'波',
    age:'18',
    // 联系人
    concat: {
    concatName:'霸',
    concatAge:'20',
    },
    };
    const {concat: {concatName, concatAge}} = user || {};
    console.log("用户联系人concatName=", concatName, "concatAge=", concatAge);

    // 控制台打印
    // 用户联系人concatName= 霸 concatAge= 20


    3.解构时赋值默认值



    解构赋值时可以采取默认值填充



    举例🌰


    通过模拟接口获取用户user对象,解构user对象时,没有dept科室字段时,可以加入默认值


    // 解构时设置默认值
    const user = {
    name:'波',
    age:'18',
    };
    const {name, age, dept = '信息科'} = user || {};
    console.log("用户信息name=", name, "age=", age, "dept=", dept);

    // 控制台打印
    // 用户信息name= 波 age= 18 dept= 信息科




    二.数组小技巧


    1.按条件向数组添加数据



    根据条件向数组中添加数据



    举例🌰


    设置一个路径白名单数组列表,当是开发环境添加部分白名单路径,若生产环境则不需要添加



    // 不是生产环境
    const isEnvProduction = false;

    // 基础白名单路径
    const baseUrl = [
    '/login',
    '/register'
    ]

    // 开发环境白名单路径
    const devUrl = [
    '/test',
    '/demo'
    ]
    // 如果是生产环境则不添加开发白名单
    const whiteList = [...baseUrl, ...(isEnvProduction? [] : devUrl)];

    console.table(whiteList)


    // 控制台打印
    // Array(4) ["/login", "/register", "/test", "/demo"]


    // 是生产环境
    const isEnvProduction = true;

    // 基础白名单路径
    const baseUrl = [
    '/login',
    '/register'
    ]

    // 开发环境白名单路径
    const devUrl = [
    '/test',
    '/demo'
    ]
    // 如果是生产环境则不添加开发白名单
    const whiteList = [...baseUrl, ...(isEnvProduction? [] : devUrl)];

    console.table(whiteList)
    // 控制台打印
    // Array(2) ["/login", "/register"]


    2.获取数组最后一个元素



    给到一个数组,然后访问最后一个元素



    举例🌰


    获取一个数组中最后一个值


    const arr = [1, 2, 3, 4];
    // 通过slice(-1) 获取只包含最后一个元素的数组,通过解构获取值
    const [last] = arr.slice(-1) || {};
    console.log('last=',last)

    // 控制台打印
    // last= 4


    3.使用 includes 优化 if



    灵活使用数组中方法includes可以对if-else进行优化



    举例🌰


    如果条件a值是 1,2,3时,打印有个男孩叫小帅


    一般写法


    const a = 1;

    // 基本写法
    if(a==1 || a==2 || a==3){
    console.log('基本写法:有个男孩叫小帅');
    }

    // 优化写法
    if([1, 2, 3].includes(a)){
    console.log('优化写法:有个男孩叫小帅');
    }

    // 控制台打印
    // 基本写法:有个男孩叫小帅
    // 优化写法:有个男孩叫小帅





    三.JS常用功能片段


    1.通过URL解析搜索参数



    通过页面URL获取解析挂参参数,适用于当前页面需要使用到URL参数时解析使用




    // 通过URL解析搜索参数

    const getQueryParamByName = (key) => {
    const query = new URLSearchParams(location.search)
    return decodeURIComponent(query.get(key))
    }

    const url = "http://javadog.net?user=javadog&age=31"

    // 模拟浏览器参数(此处是模拟浏览器参数!!!)
    const location = {
    search: '?user=javadog&age=31'
    }

    console.log('狗哥名称:', getQueryParamByName('user'));
    console.log('狗哥年龄:', getQueryParamByName('age'));

    // 控制台打印
    // 狗哥名称: javadog
    // 狗哥年龄: 31


    2.页面滚动回到顶部



    页面浏览到某处,点击返回顶部



    // 页面滚动回到顶部
    const scrollTop = () => {
    // 该函数用于获取当前网页滚动条垂直方向的滚动距离
    const range = document.documentElement.scrollTop || document.body.scrollTop
    // 如果大于0
    if (range > 0) {
    // 该函数用于实现页面的平滑滚动效果
    window.requestAnimationFrame(scrollTop)
    window.scrollTo(0, range - range / 8)
    }
    }



    3.获取页面滚动距离



    获取页面滚动距离,根据滚动需求处理业务



    // 该函数用于获取当前页面滚动的位置,可选参数target默认为window对象
    const getPageScrollPosition = (target = window) => ({
    // 函数返回一个包含x和y属性的对象,分别表示页面在水平和垂直方向上的滚动位置。函数内部通过判断target对象是否具有pageXOffset和pageYOffset属性来确定滚动位置的获取方式,如果存在则使用该属性值,否则使用scrollLeft和scrollTop属性。
    x: target.pageXOffset !== undefined ? target.pageXOffset : target.scrollLeft,
    y: target.pageYOffset !== undefined ? target.pageYOffset : target.scrollTop,
    })

    getPageScrollPosition()



    总结


    这篇文章主要介绍了JavaScript编程中的几个实用技巧,包括解构赋值的妙用、数组操作以及一些常用的JS功能片段,总结如下:


    解构赋值妙用



    • 短路语法防止报错:在解构可能为undefined或null的对象时,使用短路语法(|| {})来避免错误。

    • 深度解构:可以解构嵌套的对象,方便地获取深层属性。

    • 解构时赋值默认值:在解构时可以为未定义的属性提供默认值。


    数组小技巧



    • 按条件向数组添加数据:根据条件动态地决定是否向数组添加特定元素。

    • 获取数组最后一个元素:使用slice(-1)获取数组的最后一个元素。

    • 使用includes优化if语句:用includes检查元素是否在数组中,简化条件判断。


    JS常用功能片段



    • 通过URL解析搜索参数:创建函数解析URL的查询参数,便于获取URL中的参数值。

    • 页面滚动回到顶部:实现页面平滑滚动回顶部的函数。

    • 获取页面滚动距离:获取页面滚动位置的函数,可用于处理滚动相关的业务逻辑。


    🍈猜你想问


    如何与狗哥联系进行探讨


    关注公众号【JavaDog程序狗】

    公众号回复【入群】或者【加入】,便可成为【程序员学习交流摸鱼群】的一员,问题随便问,牛逼随便吹,目前群内已有超过200+个小伙伴啦!!!


    2.踩踩狗哥博客

    javadog.net



    大家可以在里面留言,随意发挥,有问必答






    🍯猜你喜欢


    文章推荐


    【工具】珍藏免费宝藏工具,不好用你来捶我


    【插件】IDEA这款插件,爱到无法自拔


    【规范】看看人家Git提交描述,那叫一个规矩


    【工具】用nvm管理nodejs版本切换,真香!


    【项目实战】SpringBoot+uniapp+uview2打造H5+小程序+APP入门学习的聊天小项目


    【项目实战】SpringBoot+uniapp+uview2打造一个企业黑红名单吐槽小程序


    【模块分层】还不会SpringBoot项目模块分层?来这手把手教你!


    【ChatGPT】SpringBoot+uniapp+uview2对接OpenAI,带你开发玩转ChatGPT



    作者:JavaDog程序狗
    来源:juejin.cn/post/7376532114105663539
    收起阅读 »

    解决vite项目首次打开页面卡顿的问题

    web
    问题描述 在vite项目中我们可能会遇到这样一种情况。 在我们本地开发,第一次进入页面的时候,页面会卡顿很长时间。越是复杂的卡顿时间越久。 要是我们一天只专注于一两个页面 那这个就不是问题。 但有的时候我们要开发一个流程性的东西,要进入各种各样的页面查看。这样...
    继续阅读 »

    问题描述


    在vite项目中我们可能会遇到这样一种情况。


    在我们本地开发,第一次进入页面的时候,页面会卡顿很长时间。越是复杂的卡顿时间越久。


    要是我们一天只专注于一两个页面 那这个就不是问题。


    但有的时候我们要开发一个流程性的东西,要进入各种各样的页面查看。这样就很痛苦了。


    问题原因


    为什么会出现这种情况呢?因为路由的懒加载与vite的编译机制。


    路由的懒加载:没有进入过的页面不加载


    vite的编译机制:没有加载的不编译。


    这样就会出现 我们在进入一个新页面的时候他才会编译。我们感觉卡顿的过程就是他编译的过程。


    解决思路


    问题找到了,那么解决起来就简单了。我们本地开发的时候,取消路由的懒加载就可以了。


    const routes = [
    {
    path: `/home`,
    name: `Home`,
    component: () => import(`@/views/home/HomePage.vue`),
    meta: { title: `首页` },
    },
    {
    path: `/test1`,
    name: `test1`,
    component: () => import(`@/views/demo/Test1.vue`),
    meta: { title: `测试1` },
    },
    {
    path: `/test2`,
    name: `test2`,
    component: () => import(`@/views/demo/Test2.vue`),
    meta: { title: `测试2` },
    }
    ]

    if (import.meta.env.MODE === `development`) {
    routes.forEach(item => item.component())
    }

    示例代码如上。上述的问题是解决了,但是又产生了新的问题。项目太大的时候启动会非常慢。


    于是我想了一个折中的方案。初始打开项目的时候路由还是懒加载的,然后我在浏览器网络空闲的时候去加载资源。这样你首次进系统打开的第一个页面可能还是需要等待,但是之后的所有页面就不需要等待了。


    那么问题又来了?怎么监听浏览器的网络空闲呢?这就要用的浏览器的一个api PerformanceObserver。这个api可能很多小伙伴都不知道,它主要是帮助你监控和观察与性能相关的各种指标。想要详细了解的可以点击这里查看


    我们今天用的就是resource类型监听所有的网络请求,代码示例如下


     const observer: PerformanceObserver = new PerformanceObserver((list: PerformanceObserverEntryList) => {
    const entries: PerformanceEntryList = list.getEntries()
    for (const entry of entries) {
    if (entry.entryType === `resource`) {
    //网络请求结束
    }
    }
    })
    observer.observe({ entryTypes: [`resource`] })

    监听到网络请求后,我们怎么判断是否空闲呢?也很简单,只要一秒钟以内没有新的网络请求出现我们就认为当前网络是空闲的。这不就防抖函数嘛。


    const routes = [
    {
    path: `/home`,
    name: `Home`,
    component: () => import(`@/views/home/HomePage.vue`),
    meta: { title: `首页` },
    },
    {
    path: `/test1`,
    name: `test1`,
    component: () => import(`@/views/demo/Test1.vue`),
    meta: { title: `测试1` },
    },
    {
    path: `/test2`,
    name: `test2`,
    component: () => import(`@/views/demo/Test2.vue`),
    meta: { title: `测试2` },
    }
    ]

    if (import.meta.env.MODE === `development`) {
    const componentsToLoad = routes.map(item => item.component)
    const loadComponentsWhenNetworkIdle = debounce(
    () => {
    if (componentsToLoad.length > 0) {
    const componentLoader = componentsToLoad.pop()
    componentLoader && componentLoader()
    // eslint-disable-next-line
    console.log(`剩余${componentsToLoad.length}个路由未加载`, componentsToLoad)
    }
    },
    1000,
    false
    )

    const observer: PerformanceObserver = new PerformanceObserver((list: PerformanceObserverEntryList) => {
    const entries: PerformanceEntryList = list.getEntries()
    for (const entry of entries) {
    if (entry.entryType === `resource`) {
    loadComponentsWhenNetworkIdle()
    }
    }
    })
    observer.observe({ entryTypes: [`resource`] })
    }

    完整的代码如上。当我们判断出网络空闲后,就从componentsToLoad数组中删除一个组件,并加载删除的这个组件,然后就会重新触发网络请求。一直重复这个流程,直到componentsToLoad数组为空。


    这只是个示例的代码,获取componentsToLoad变量防抖函数的配置(初始化不执行,无操作后1秒钟后执行)还要根据你的实际项目进行修改!


    可优化项


    以上方法确实是按照我们的预期实现了,但是还有一些小小的问题。例如:



    1. 我们在加载组件的时候如果恰好是当前打开的页面,是不会重新触发网络请求的。因此可能会断掉componentsToLoad数组的删除,加载组件,触发网络请求这个流程。不过问题不大,你在当前页面如果有操作重新触发网络请求了,这个流程还会继续走下去,直到componentsToLoad数组为空。

    2. 每次刷新页面componentsToLoad数组都是会重新获取到值的,也就是我们走过的流程会重新走。不过问题不大,第二次走都是走缓存了,执行速度很快,而且也是本地开发那点性能损坏可以忽略不计。


    这些问题影响都不是很大,所以我就没继续做优化。有兴趣的小伙伴可以继续研究下去。


    作者:热心市民王某
    来源:juejin.cn/post/7280745727160811579
    收起阅读 »

    前端大师课:“鬼剑士,听我指令,砍碎屏幕”是怎么实现的?

    web
    前言:属于我们那个年代的"地下城与勇士"的手游上线了,为了做好推广和裂变,有个特别游戏意思的效果你可能在各个微信群里都看到了:你只需要在微信群里发送"鬼剑士,听我指令,砍碎屏幕"、“鬼剑士”、“地下城与勇士”这些关键词,就会触发特别炫酷的动画效果。那这种效果如...
    继续阅读 »

    前言:属于我们那个年代的"地下城与勇士"的手游上线了,为了做好推广和裂变,有个特别游戏意思的效果你可能在各个微信群里都看到了:你只需要在微信群里发送"鬼剑士,听我指令,砍碎屏幕"、“鬼剑士”、“地下城与勇士”这些关键词,就会触发特别炫酷的动画效果。
    那这种效果如果让我们技术来做:
    1.要怎么实现呢?
    2.有几种实现方法呢?
    3.关键代码能给我看看吗?

    方案简述

    为了提供更详细的解析,我们可以进一步探讨“地下城与勇士手游”(DNF手游)在微信聊天中实现“鬼剑士,听我指令,砍碎屏幕”这一互动特效的可能技术细节。虽然没有直接的源码分析,我们可以基于现有的技术框架和前端开发实践来构建一个理论上的实现模型。

    前端监听设计

    • 关键词识别: 微信聊天界面的输入检测可能是通过前端JavaScript监听input事件,配合正则表达式匹配用户输入的关键词(如“鬼剑士,听我指令,砍碎屏幕”)。一旦匹配成功,就向后端发送请求或直接触发前端动画逻辑。

    后端交互

    • 请求处理: 用户输入关键词后,前端可能通过Ajax请求或WebSocket向服务器发送一个事件。服务器确认后,返回一个响应,指示前端继续执行动画展示或直接携带福袋奖励信息。

    前端动画实现

    • 动画序列: 利用HTML5 元素或WebGL技术,开发者可以创建复杂的2D或3D动画。对于“砍碎屏幕”的效果,可能事先设计好一系列帧动画或使用骨骼动画技术来展现鬼剑士的动作和屏幕碎裂的过程。
    • 碎片生成与物理模拟: 通过JavaScript库(如Three.js的粒子系统或matter.js)模拟屏幕碎裂后的碎片效果,包括碎片的随机分布、速度、旋转和重力影响等,增加真实感。
    • 音频同步: 使用Web Audio API同步播放砍击和碎裂的音效,增强用户的沉浸感。

    福袋奖励机制

    • • 动画结束后展示福袋: 动画播放完毕后,前端动态插入一个福袋图标或弹窗,作为用户交互元素。这可能是通过DOM操作实现的,如创建一个新的
      元素并应用CSS样式使其表现为福袋。
    • • 点击事件处理: 给福袋元素绑定点击事件,触发领奖逻辑。这可能涉及再次向服务器发送请求验证用户资格,并根据响应展示奖励内容。

    优化与兼容性

    • 性能优化: 动画应考虑在不同设备上的流畅度,可能采用分层渲染、帧率限制、资源按需加载等策略。
    • 跨平台兼容: 确保在微信内置浏览器上的表现良好,需要对微信环境下的特定API和限制有深入了解,比如微信小程序的Canvas组件和其特定的适配要求。

    安全与隐私

    • 数据保护: 在处理用户交互和服务器通信时,确保遵循数据保护法规,比如加密传输敏感信息,避免泄露用户隐私。

    综上所述,这个互动特效的实现是一个从用户输入监测、前后端交互、动画设计与渲染、到用户反馈与奖励领取的全链路流程,需要综合运用多种前端技术和良好的产品设计思路。

    微信聊天界面元素震动效果设计及API应用

    虽然微信没有直接公开针对UI元素震动的特定API,但在微信小程序或基于微信环境的H5游戏中设计类似聊天界面元素的震动效果,利用一些基础的动画技术和微信小程序提供的动画库来模拟这种效果。比如通过CSS动画与微信小程序的动画接口来实现这一功能。以下是两种主流实现方式:

    1. CSS动画实现震动效果(H5环境)

    核心概念

    • @keyframes: CSS的关键帧动画,用于定义一个动画序列中不同时间点的样式变化。
    • transform: CSS属性,用于改变元素的形状、大小和位置。其中,translateX()用于水平移动元素。

    实现步骤

    1.  定义动画样式:在CSS中,创建一个名为.shake的类,利用@keyframes定义震动序列。动画包括了元素在原位置与左右轻微偏移之间的快速切换,营造出震动感。

      .shake {
        animation: shake 0.5s/* 动画名称与持续时间 */
        transform-origin: center center; /* 设置变换中心点 */
      }

      @keyframes shake {
        0%100% { transformtranslateX(0); } /* 开始与结束位置 */
        10%30%50%70%90% { transformtranslateX(-5px); } /* 向左偏移 */
        20%40%60%80% { transformtranslateX(5px); } /* 向右偏移 */
      }

    2. 应用动画:在JavaScript中,通过动态添加或移除.shake类到目标元素上,触发这个震动动画。

    2. 微信小程序wx.createAnimation实现震动

    核心概念

    • wx.createAnimation: 微信小程序提供的动画实例创建方法,允许更精细地控制动画过程。
    • step() : 动画实例的方法,用于生成当前动画状态的数据,用于在setData中更新视图。

    实现步骤

    1. 初始化动画数据:在Page的data中定义一个空的animationData对象,用于存储动画实例导出的状态数据。

      data: {
        animationData: {},
      },

    2. 创建震动动画逻辑:定义一个函数,如shakeElement,使用wx.createAnimation创建动画实例,并定义震动序列。通过连续的translateX操作模拟震动,然后通过step()函数记录每个阶段的状态,并通过setData更新到视图上。

    ```
    shakeElement: function () {
      let animation = wx.createAnimation({
        duration: 300// 动画持续时间
        timingFunction: 'ease'// 动画速度曲线
      });

      // 震动序列定义
      animation.translateX(-5).step(); // 向左偏移
      this.setData({ animationData: animation.export() });
      setTimeout(() => {
        animation.translateX(5).step(); // 向右偏移
        this.setData({ animationData: animation.export() });
      }, 100);
      setTimeout(() => {
        animation.translateX(0).step(); // 回到原位
        this.setData({ animationData: animation.export() });
      }, 200);
    },
    ```

    3. 应用动画数据:在WXML模板中,为目标元素绑定动画数据。

    ```
    style="{{animationData}}" class="your-element-class">震动的文字或图标
    ```

    注意事项与最佳实践

    • 性能监控:频繁或长时间的动画可能影响应用性能,尤其是低配置设备。适时停止或限制动画触发频率。
    • 用户体验:震动效果应适度且符合用户预期,过度使用可能造成用户反感。
    • 跨平台兼容性:虽然上述方法主要针对微信环境,但在实现时也应考虑浏览器的兼容性问题,特别是对于H5应用。
    • 动画细节调整:根据实际需求调整震动幅度、频率和持续时间,以达到最佳视觉效果。

    动手能力强的你,可以去试试,下一节,将讲一个具体的demo给大家演示一下哈。


      作者:蜡笔小新爱学习
      来源:juejin.cn/post/7371423076661542952
      收起阅读 »

      带你从0到1部署nestjs项目

      web
      前言 最近跟着一个掘金大佬做了一个全栈项目,前端react,后端我是用的nest,大佬用的midway 大佬博客地址(前端小付 的个人主页 - 动态 - 掘金 (juejin.cn) 最近项目也是部署上线了,因为域名还没备案,地址就先不发出来了,这篇文章就讲讲...
      继续阅读 »

      前言


      最近跟着一个掘金大佬做了一个全栈项目,前端react,后端我是用的nest,大佬用的midway


      大佬博客地址(前端小付 的个人主页 - 动态 - 掘金 (juejin.cn)


      最近项目也是部署上线了,因为域名还没备案,地址就先不发出来了,这篇文章就讲讲如何部署。一直有兄弟问prisma如何部署,这篇文章就帮你扫清障碍,文章可能比较长,希望耐心看完


      后端技术栈



      • nestjs

      • mysql

      • redis

      • minio

      • prisma


      部署需要掌握的知识



      • docker

      • github actions

      • 服务器


      实战


      nestjs打包镜像


      我们部署的时候用的docker,docker需要拉镜像,然后生成容器,docker的知识可以去学习下,这里就默认大家会了,我们在打包的时候要写Dockerfile文件,后端项目是需要保留node_modules的,所以打包的时候一起打进去,我的项目用的pnpm包管理工具,我的文件挂载时有点点问题,我就没有用pm2去执行多阶段打包,多阶段打包速度会比较快,还有就是比如开发环境的依赖可以不打,当然这都是优化的地方,暂时没有去做,大家可以自行尝试


      # 因为我们项目使用的是pnpm安装依赖,所以找了个支持pnpm的基础镜像,如果你们使用npm,这里可以替换成node镜像
      # FROM nginx:alpine
      FROM gplane/pnpm:8 as builder

      # 设置时区
      ENV TZ=Asia/Shanghai \
      DEBIAN_FRONTEND=noninteractive
      RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata && rm -rf /var/lib/apt/lists/*

      # 创建工作目录
      RUN mkdir -p /app

      # 指定工作目录
      WORKDIR /app

      # 复制当前代码到/app工作目录
      COPY . ./

      RUN npm config set registry https://registry.npm.taobao.org/
      # pnpm 安装依赖
      COPY package.json /app/package.json

      RUN rm -rf /app/pnpm-lock.yml
      RUN cd /app && rm -rf /app/node_modules && pnpm install

      RUN cd /app && rm -rf /app/dist && pnpm build

      EXPOSE 3000
      # 启动服务
      CMD pnpm run start:prod


      这样后端镜像就构建好了,接下来去编写github action的文件,github actions是做ci/cd的,让我们每次的部署走自动化流程,不要每次手动去做这些工作


      github actions


      在我们的根目录下面创建这样一个文件,这个文件名字可以随便取


      12.png


      然后在里面编写逻辑


      name: Docker

      on:
      push:
      branches: ['main']

      env:
      REGISTRY: ghcr.io
      IMAGE_NAME: ${{ github.repository }}

      jobs:
      build:
      runs-on: ubuntu-latest
      permissions:
      contents: read
      packages: write
      id-token: write

      steps:
      - name: Checkout repository
      uses: actions/checkout@v3

      - name: Setup Docker buildx
      uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf

      - name: Cache Docker layers
      uses: actions/cache@v2
      with:
      path: /tmp/.buildx-cache
      key: ${{ runner.os }}-buildx-${{ github.sha }}
      restore-keys: |
      ${{ runner.os }}-buildx-

      - name: Log int0 registry ${{ env.REGISTRY }}
      if: github.event_name != 'pull_request'
      uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
      with:
      registry: ${{ env.REGISTRY }}
      username: ${{ github.actor }}
      password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract Docker metadata
      id: meta
      uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
      with:
      images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push Docker image
      id: build-and-push
      uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
      with:
      context: .
      push: ${{ github.event_name != 'pull_request' }}
      tags: ${{ steps.meta.outputs.tags }}
      labels: ${{ steps.meta.outputs.labels }}
      cache-from: type=local,src=/tmp/.buildx-cache
      cache-to: type=local,dest=/tmp/.buildx-cache-new

      - name: Move cache
      run: |
      rm -rf /tmp/.buildx-cache
      mv /tmp/.buildx-cache-new /tmp/.buildx-cache

      - name: SSH Command
      uses: D3rHase/ssh-command-action@v0.2.1
      with:
      HOST: ${{ secrets.SERVER_IP }}
      PORT: 22
      USER: root
      PRIVATE_SSH_KEY: ${{ secrets.SERVER_KEY }}
      COMMAND: cd /root && ./run.sh

      这里的['main']就是我们要执行哪个分支,你不是main分支,那就改成你的分支就可以,其他都是固定的模板,直接用


      SSH Command 这个是我们取做ci/cd的时候,每次我们提交代码,然后配置了ssh密钥,就可以让服务器执行run.sh命令,这个shell脚本我们后面可以用到,这里就记住是让服务器去执行拉取镜像以及执行启动容器的。


      当我们做到这一步之后,我们提交代码的时候,应该会出现这样的情况


      13.png


      因为还没有去配置ssh密钥,这个肯定跑不起来,看到我们上面ssh command里面有两个变量,就是我们要配置的,接下来我们去搞服务器。


      服务器


      最近双十一活动,买个服务器还是挺香的,我买的阿里云2核2g的99/年,买的时候选操作系统,随便一个都可以,我因为对ubuntu熟悉一下,就买了ubuntu操作系统的,买好之后,记得重置密码


      14.png


      后面我们用shell工具连接的时候需要用到密码的


      之后我们去下载一个shell工具,连接服务器用的,常见的有xshell finalshell,我用的第二个。


      15.png


      就傻瓜式安装,下一步就可以,然后我们去连接一下服务器,去下载宝塔。


      16.png


      第二步那里选择ssh连接就可以了,然后主机就是你的服务器公网ip,密码就是刚刚的,用户名就是root


      连接上了之后,去下载宝塔,这个是ubuntu的命令,其他的操作系统有差别,可以去搜一下就有


      wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh


      下载好之后输入bt default命令就可以打开了


      17.png


      因为宝塔是个可视化操作面板,比较方便,所以先弄好。


      接下来我们去搞服务器密钥


      18.png


      我们在这里创建好密钥对,记得它只有一次机会,所以下载好了记得保存在你记得住的地方,然后创建好,记得要绑定,不然没效果,然后我们就要得用ssh密钥来连接服务器了


      20.png


      至此,我们的服务器也弄好了


      github绑定密钥


      21.png


      这个是settings界面的,然后大家按照步骤创建就可以,到这里我们的配置就结束了。


      创建shell脚本


      我们上面不是说了,我们要写一个bash文件吗,现在就要来写,这个bash文件我们要执行拉镜像和跑容器


      23.png


      我们可以选择在宝塔中操作


      docker-compose pull && docker-compose up --remove-orphans

      然后我们在同目录下也就是root目录下面新建一个docker-compose.yml文件,来启动容器的,这个文件就不要展示了,也就是创建了哪些服务,挂载哪些卷,如果有需要评论区说一下就行,很简单,因为我们用了很多服务,mysql redis minio nginx 这些多镜像,就得多个容器来跑,docker-compose无疑就好


      到这里后端项目就部署完了,我们还得迁移数据库对吧


      数据库部署


      pirsma迁移

      因为我用的mysql和prisma,typeorm思路差不多,可以一样用。我们的prisma以及typeorm迁移的时候只可以同步表结构,数据不会同步过去,所以我们跑迁移命令的时候,跑完会发现没有数据,我们需要手动导入数据


      另外注意点,我们docker-compose.yml里面的mysql容器名字对应我们连接的主机名,这里记得更改prisma连接,不然你的prisma还连接在localhost肯定找不到


      我们来上手操作


      24.png


      这是我现在在跑的容器,我要找到我的后端项目对应的容器id,进去执行命令


      docker exec -it <容器id> /bin/sh 跑这个我们就可以在容器内部执行命令


      25.png


      然后就可以把表结构同步过去了,我们也可以在生成Dockerfile的时候写迁移命令也是可以的,这样就不用手动同步了


      数据库导出

      我们需要将本地的数据迁移上去,需要先导出sql文件,这个就不用在这里展开说了,很简单,不会可以去找个博客教程,不到30s就完了,导出后我们需要将那个sql文件


      然后我们在宝塔操作,找到你正在运行的mysql容器目录


      26.png


      将你的sql文件上传上去,放哪里都无所谓,记得路径就行


      然后我们进入mysql容器里面,跑上面的那个命令



      1. 登录账号 mysql -u root -p

      2. 输入密码 ******* 输入你数据库连接的那个密码

      3. 进入之后 USE <database_name> 就选中了那张表

      4. 然后执行 source 刚刚的那个sql文件路径


      这样操作数据就同步上去了,注意,数据同步前是一定要有表结构的,所以有先后顺序,这个地方注意。


      也可以用这个命令, 将sql文件拷贝到你的容器内,然后跑上面的步骤,看个人喜好了。
      docker cp /本地路径/your_file.sql 容器名称:/容器路径/your_file.sql


      到这里我们的部署就结束了,等项目正式上线的时候,还有其他注意点还会再写一篇博客的


      最后


      项目是跟着开头提到的小付大佬学习的,主要想学下react,没想到是个全栈项目,就用nestjs写了后端,也学到了很多前端,后端,部署的知识,强烈推荐大家可以去看看。最后 觉得不错的话,可以给个点赞加关注😘


      作者:西檬
      来源:juejin.cn/post/7299859799780655155
      收起阅读 »

      webview预加载的技术原理和注意点

      web
      此文章介绍webview预加载的技术原理和注意点 背景 网页优化,对网页的webview进行预加载,用户点开页面达到秒开效果 原理 即空间换时间,提前加载页面url 由于首页就有网页入口,所以需要在首页Activity进行预加载。 创建webview Web...
      继续阅读 »

      此文章介绍webview预加载的技术原理和注意点


      背景


      网页优化,对网页的webview进行预加载,用户点开页面达到秒开效果


      原理


      即空间换时间,提前加载页面url


      由于首页就有网页入口,所以需要在首页Activity进行预加载。


      创建webview



      • WebView(MutableContextWrapper(context),使用MutableContextWrapper替换Context,,方便后续复用时替换为webview容器Activity的上下文对象

      • WebSettings、原生方法初始化,保证预加载时功能正常,因为后续不会再进行loadUrl,必须保证h5页面正常显示

      • WebViewClient、WebChromeClient监听



        • 重写onPageFinished、onReceivedTitle方法,主要为了title的接收,并且记录下来,后续webview复用时直接显示title

        • 重写onReceivedSslError方法,避免证书错误加载显示失败



      • 首页预加载容器Layout,置于最底层,宽度全屏,高度设置为全屏高度 - 顶部导航栏高度 - 状态栏高度


      viewGr0up.addView(WebView(MutableContextWrapper(context)).also { web ->  // 初始化webview 
      }, ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT))


      • 刷新逻辑,绑码状态或登录信息改变时,刷新已经预加载好的webview的url


      复用webview



      • webview容器整改

        • 判断是否需要使用已预加载的webview,如果需要复用,则根布局添加预加载webview进来,注意布局层级,避免覆盖了其他控件





      webView?.let { web ->
      (web.context as MutableContextWrapper).baseContext = activity
      }

      container.addView(it, 0, ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT))


      • 原webview容器使用ViewStub代替,如果不需要复用则将ViewStub inflate,进行正常的h5页面加载

      • 添加预加载webview后,直接显示,不需要loadUrl,但是白屏分析之类的逻辑别忘了手动调用


      页面关闭



      • webview跟随Activity一起进行销毁,但是需要通知首页重新生成一个webview,准备给下一次用户点击使用

      • 首页关闭,页面栈清空时,需要清空单例中webview对象,并且调用destroy

      • 不推荐回收webview继续使用,因为在实际测试中表现并不好,重建webview可以规避很多问题场景


      如果用户点击比较快时,依然会看到加载过程和骨架屏


      问题点和解决



      • 复用webview时,页面视觉残留上一次h5页面状态



        • 原因:页面关闭时,触发Activity的onStop方法,webview调用onPause,此时webview被暂停,webview的reload也不会立即刷新

        • 解决:回收webview时,对webview重新恢复交互状态(onResume)



      • 页面关闭,迅速点开,页面先显示上一次h5页面状态,然后开始reload



        • 原因:当Activity反复打开关闭时,Activity的回收、对象GC会滞后,导致webview已经被复用并且上屏了,webview才开始触发reload

        • 解决:webview不进行回收,每次页面关闭都进行销毁,重新创建webview



      • webview多次reload后,网络请求失败
        axios网络请求失败,response报文为空,暂未找到原因,了解的大佬麻烦解答一下,谢谢。当不回收webview后,此场景不存在了

      • h5页面正常显示后,又刷新一次页面



        • 原因:webview复用时,对webview重新进行初始化(重新创建原生能力、重置上下文对象等)时,会重新对UserAgent进行赋值,导致重新刷新了一次。

        • 排查过程
          发现网页骨架屏刚出现时点开不会重复刷新;骨架屏消失后点开也不会重复刷新;唯独骨架屏时,刚出现vConsole绿色块时点开会出现重复刷新。
          对webview的shouldOverrideUrlLoading方法进行debug,发现并没有进入断点,说明并不是调用了reload,推测有什么逻辑导致网页重新刷新了一次。
          随即用傻子办法,一段一段代码注释,发现友盟组件attach webview和通用webview容器设置userAgent的代码会导致重复刷新,难道友盟组件也有设置userAgent的代码?
          然后查看友盟组件源码,不出所料,发现友盟组件中反射了setUserAgentString方法,并且对userAgent拼接了"Umeng4Aplus/1.0.0字符串,如下图所示。


          那是否设置的userAgent有什么敏感字符串导致刷新?随即将userAgent只设置为空字符串,发现也会导致重复刷新。
          到这里水落石处,但为什么userAgent发现变化会导致网页重复刷新?
          询问前端大佬,回复没有监听userAgent,userAgent变化也不会做什么操作,故而没有得到具体答案,了解的大佬麻烦解答一下,感谢。

        • 解决:webview复用时,不进行userAgent的重复赋值




      IMG20240529101049.png



      • 复用webview时,页面白屏等待一会后秒开h5页面

        • 原因:预加载时webview在1像素的layout中加载,复用到通用webview容器中,webview控件的布局已经结束,但需要时间对H5进行渲染,在重复打开关闭后或性能低下的手机表现更甚

        • 解决:首页预加载webview时,已通用webview容器同大小进行布局



      • 内存泄漏问题

        • 原因:部分原生方法对象中对Activity和Fragment有强引用,当原生方法对象被addJavascriptInterface添加进webview时,复用的webview生命周期长如Application,就会强引用到Activity,导致无法回收,内存泄漏

        • 解决:webview回收时清空Activity、Fragment的引用

        • 不复用webview后此问题不存在了




      作者:聪本尊18680
      来源:juejin.cn/post/7373937820179005478
      收起阅读 »

      设计问能不能实现这种碎片化的效果

      web
      前言 某天设计发来一个 网站,问我能不能实现这种类似的效果。 不知你们什么感想,反正我当时第一次看到这个网站的效果时,心里第一反应就是这做个锤子啊。 F12 调试 让我们打开调试,瞅瞅别人是如何实现。 可以看到在该层级下,页面有很多个 shard-wrap ...
      继续阅读 »

      前言


      某天设计发来一个 网站,问我能不能实现这种类似的效果。


      shard-img-reverse-xs.gif

      不知你们什么感想,反正我当时第一次看到这个网站的效果时,心里第一反应就是这做个锤子啊。


      F12 调试


      让我们打开调试,瞅瞅别人是如何实现。


      可以看到在该层级下,页面有很多个 shard-wrap 元素,而每一个元素都是定位覆盖其父元素的。


      image.png

      当我们添加 display: none; 后,可以看到嘴角这里少了一块。


      image.png

      而继续展开其内部的元素就可以看到主要的实现原理了:clip-path: polygon();


      image.png

      clip-pathpolygon 更详细的解释可以看 MDN,简单来说就是在一个 div 里面绘制一个多边形。


      比如上图的意思就是:选取 div 内部坐标为 (9.38%,59.35%),(13.4%,58.95%),(9.28%,61.08%) 这三个点,并将其连起来,所以就能构成一个三角形了。然后再填充 backgroundColor 该三角形就有对应颜色了。


      实现过程


      调试看完别人的实现后发现,好像也不是很难。但是数据又如何搞来呢?


      当然我们可以直接在接口那里找别人的数据,但是我找了一会发现不太好找。


      于是想到咱们可是一名前端啊,简单写段 js 扒拉下来不就好了吗,想要更多,就滑一下滚轮,切换下一个碎片图像,再执行一次即可。


      function getShardDomData() {
      const doms = document.getElementsByClassName('shard')
      const list = []
      for (let i = 0; i < doms.length; i++) {
      const style = window.getComputedStyle(doms[i])
      let str = style.clipPath.replace('polygon(', '').replace(')', '')
      list.push({
      polygon: str,
      color: style.backgroundColor,
      })
      }
      return list
      }
      console.log('res: ', getShardDomData());

      image.png

      碎片化组件


      简单封装一个碎片化组件,通过 transitiondelay 增加动画效果以及延迟,即可实现切换时的碎片化动画效果。我这里是用的 tailwindcss 开发的,大家可以换成对应 css 即可。


      export type ShardComItem = {
      color: string
      polygon: string
      }

      export type ShardComProps = {
      items: ShardComItem[]
      }

      export default function ShardCom({items}: ShardComProps) {
      return (
      <div className="relative w-full h-full min-w-20">
      {items?.map((item, index) => (
      <div className="absolute w-full h-full" key={`${index}`}>
      <div
      className="w-full h-full transition-all duration-1000 ease-in-out"
      style={{
      backgroundColor: item.color,
      clipPath: `polygon(${item.polygon})`,
      transitionDelay: `${index * 15}ms`,
      }}
      >
      </div>
      </div>
      ))}
      </div>

      )
      }

      模仿实现的 demo 地址


      组件的代码放码上掘金了,感兴趣可以自提。



      自制画板绘画 clip-path


      当然只扒拉别人的数据,这肯定是不行的,假如设计师想自己设计一份碎片化效果该怎么办呢?


      解决方法也很简单:那就自己简单开发一个绘图界面,让设计师自己来拖拽生成想要的效果即可。


      线上画板地址


      image.png

      画板的实现就放到 下篇文章 中讲述了。


      最后


      当然最终只是简陋的实现了一部分效果罢了,还是缺少很多动画的,和 原网站 存在不少差距的,大家看个乐就行。


      作者:滑动变滚动的蜗牛
      来源:juejin.cn/post/7372013979467333643
      收起阅读 »

      我是如何把个人网站首屏加载时间从18秒优化到5秒的

      web
      起因是这样的,自己做了一个网站,开发的时候好好的,部署到服务器上去后,打开的时候白屏了好长时间才展示内容, 这可不能忍,必须找出原因优化掉!服务器配置CPU:1核,内存:2GiB,带宽:1Mbps这上来就找到原因了啊,这配置这么低,肯定慢啊,怎么办?...
      继续阅读 »

      起因是这样的,自己做了一个网站,开发的时候好好的,部署到服务器上去后,打开的时候白屏了好长时间才展示内容, 这可不能忍,必须找出原因优化掉!

      服务器配置

      CPU:1核,内存:2GiB,带宽:1Mbps

      这上来就找到原因了啊,这配置这么低,肯定慢啊,怎么办?

      换!!!

      然而贫穷像是一万多条无形的枷锁束缚住了我,让我换服务器的双手动弹不得。

      此路不通,只能另寻他法解决了。

      优化前首屏加载测试

      测试结果分析

      1. 从截图可以看到,首屏加载耗时19.15秒,主要是chunk-vendors.2daba5b2.js这个文件加载耗时最长,为17.6秒,大小为1.9M,其他文件均在4秒内加载完毕。通常页面加载的一个文件大小超过300k,已经算比较大了。第二个比较耗时的文件是chunk-vendors.62bee483.css,这个应该是样式文件。其他的文件加载耗时都不超过1秒,所以后面优化先从那两个文件下手。
      2. 重新编译项目,看下项目生成的文件

      可以看到前面提到的两个文件比较大,后面列出了每个文件使用gz压缩后的大小,但是浏览器实际并没有加载压缩后的文件,而是原始文件。再打开打包文件夹,发现实际生成的js文件夹中除了js文件,还有js.map文件,js.map文件通常用于开发环境调试用,方便我们查找错误,在生成环境是不需要用到的,而且都比较大,这也是一个优化的点。

      分析项目依赖情况

      运行vue ui,编译查看chunk-vendors中的结构发现,主要是element-ui依赖比较大,其次是vue和mavon-editor

      整个项目的情况如下

      那么如何优化呢

      开启nginx压缩配置

      修改nginx配置,启用gzip压缩

      gzip on;
      gzip_vary on;
      gzip_proxied any;
      gzip_comp_level 6;
      gzip_buffers 16 8k;
      gzip_http_version 1.1;
      gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

      测试页面加载时间缩短到5.2秒,chunk-vendors.js传输大小为556k,加载时间为4秒,其他文件加载时间基本不超过200毫秒

      生产配置不生成js.map

      修改项目根目录中vue.config.js配置文件,设置productionSourceMap: false

      module.exports = {
      runtimeCompiler: true,
      productionSourceMap: false
      }

      打包测试文件夹大小由9.1M减小到2.26M

      配置gzip压缩插件

      执行npm i compression-webpack-plugin@5.0.1 -D安装插件,在vue.config.js中修改打包配置

      const CompressionPlugin = require("compression-webpack-plugin");
      const productionGzipExt = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;
      module.exports = {
      runtimeCompiler: true,

      productionSourceMap: false,

      configureWebpack: () => {
      if (process.env.NODE_ENV === "production") {
      return {
      plugins: [
      new CompressionPlugin({
      filename: "[path].gz[query]",
      algorithm: "gzip",
      test: productionGzipExt,
      threshold: 1024, // 大于1024字节的资源才处理
      minRatio: 0.8, // 压缩率要高于80%
      deleteOriginalAssets: false, // 删除原始资源,如果不支持gzip压缩的浏览器无法正常加载则关闭此项
      }),
      ],
      };
      }
      },
      };

      插件需要指定版本,最新版本的会报错这个和nginx压缩配置感觉重复了,实际测试和nginx压缩配置的速度差不多,如果两个压缩都有,速度并没有提升

      修改elementui组件按需引入

      1. 执行npm install babel-plugin-component -D安装 babel-plugin-component2. 修改.babelrc内容如下:
      {
      "presets": [["@babel/preset-env", { "modules": false}]],
      "plugins": [
      [
      "component",
      {
      "libraryName": "element-ui",
      "styleLibraryName": "theme-chalk"
      }
      ]
      ]
      }
      1. 在main.js中引入需要用到的组件,示例如下:
      import Vue from "vue";
      import App from "./App.vue";
      import router from "./router";
      import store from "./store";
      import "element-ui/lib/theme-chalk/index.css";
      import mavonEditor from "mavon-editor";
      import "mavon-editor/dist/css/index.css";
      import axios from "axios";
      import {
      Avatar,
      Button,
      Container,
      DatePicker,
      Dialog,
      Dropdown,
      DropdownItem,
      DropdownMenu,
      Footer,
      Form,
      FormItem,
      Header,
      Image,
      Input,
      Main,
      Message,
      MessageBox,
      Notification,
      Option,
      Select,
      Table,
      TableColumn,
      TabPane,
      Tabs,
      Timeline,
      TimelineItem,
      } from "element-ui";

      Vue.use(Button);
      Vue.use(Dialog);
      Vue.use(Dropdown);
      Vue.use(DropdownMenu);
      Vue.use(DropdownItem);
      Vue.use(Input);
      Vue.use(Select);
      Vue.use(Table);
      Vue.use(TableColumn);
      Vue.use(DatePicker);
      Vue.use(Form);
      Vue.use(FormItem);
      Vue.use(Tabs);
      Vue.use(TabPane);
      Vue.use(Header);
      Vue.use(Main);
      Vue.use(Footer);
      Vue.use(Timeline);
      Vue.use(TimelineItem);
      Vue.use(Image);
      Vue.use(Avatar);
      Vue.use(Container);
      Vue.use(Option);
      Vue.use(mavonEditor);
      Vue.prototype.$notify = Notification;
      Vue.prototype.$message = Message;
      Vue.prototype.$confirm = MessageBox.confirm;
      Vue.prototype.$axios = axios;
      Vue.config.productionTip = false;

      axios.interceptors.request.use(
      (config) => {
      config.url = "/api/" + config.url;
      config.headers.token = sessionStorage.getItem("identityId");
      return config;
      },
      (error) => {
      return Promise.reject(error);
      }
      );

      axios.interceptors.response.use(
      (response) => {
      if (response.data && response.data.exceptionCode) {
      const exceptionType = response.data.exceptionType;
      Notification({ title: response.data.exceptionMessage, type: exceptionType.toLowerCase() });
      return Promise.reject(response.data);
      }
      return response;
      },
      (error) => {
      return Promise.reject(error);
      }
      );

      new Vue({
      router,
      store,
      render: (h) => h(App),
      }).$mount("#app");

      修改按需引入后elementui依赖大小约为1.3M

      修改组件局部引入为异步组件

      在一个组件中引入其他组件时使用异步的方式引入,如

      export default {
      components: {
      register: () => import('./views/Register.vue'),
      login: () => import('./views/Login.vue')
      }
      };

      完成后此时chunk-vendors.js这个文件已经从优化前的1.9M缩小到890k

      页面加载约3秒可以显示出来,其他资源在页面显示后继续后台加载,全部加载完总耗时约5秒,请求数68次

      组件按组分块

      使用命名chunk语法webpackChunkName: "块名"将某个路由下的组件打包在同一个异步块中,如

      import Vue from "vue";
      import VueRouter from "vue-router";

      Vue.use(VueRouter);

      const routes = [
      {
      path: "/",
      redirect: 'home'
      },
      {
      path: '/home',
      component: () => import(/* webpackChunkName: "home-page" */ '../views/Home.vue')
      },
      {
      path: '/documents',
      component: () => import(/* webpackChunkName: "home-page" */ '../views/documents/DocumentList.vue')
      },
      {
      path: '/documentcontent',
      component: () => import(/* webpackChunkName: "home-page" */ '../views/documents/DocumentContent.vue')
      },
      {
      path: '/write',
      component: () => import(/* webpackChunkName: "home-page" */ '../views/WriteMarkdown.vue')
      },
      {
      path: '/about',
      component: () => import(/* webpackChunkName: "home-page" */ '../views/About.vue')
      },
      {
      path: '/management',
      component: () => import(/* webpackChunkName: "management" */ '../views/management/Management.vue'),
      children: [
      { path: '', component: () => import(/* webpackChunkName: "management" */ '../views/management/ManagementOptions.vue') },
      { path: 'developplan', component: () => import(/* webpackChunkName: "management" */ '../views/management/DevelopmentPlan.vue') },
      { path: 'tags', component: () => import(/* webpackChunkName: "management" */ '../views/management/TagsManage.vue') },
      { path: 'documents', component: () => import(/* webpackChunkName: "management" */ '../views/management/DocumentsManage.vue') }
      ]
      },
      {
      path: '/games',
      component: () => import(/* webpackChunkName: "games" */ '../views/games/Games.vue'),
      children: [
      { path: '', component: () => import(/* webpackChunkName: "games" */ '../views/games/GameList.vue') },
      { path: 'minesweeper', component: () => import(/* webpackChunkName: "games" */ '../views/games/minesweeper/MineSweeper.vue') }
      ]
      },
      {
      path: '/tools',
      component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ToolsView.vue'),
      children: [
      { path: '', component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ToolsList.vue') },
      { path: 'imageconverter', component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ImageConverter.vue') }
      ]
      }
      ];

      const router = new VueRouter({
      mode: "history",
      base: process.env.BASE_URL,
      routes,
      });

      export default router;

      打包编译后文件比之前要减少了一部分,并且合并后的文件资源也不大,完全可以接受

      页面加载耗时基本没变,但是请求数减少到51次

      总结

      1. nginx压缩对性能的提升最大,通过压缩文件缩短资源加载时间
      2. gzip压缩插件会将文件压缩成gz格式,暂时不知道怎么用
      3. elementui按需引入会减小依赖资源的大小,chunk-vendors.js文件体积会减小
      4. 使用异步组件可以在后台加载渲染不到的资源,优先加载渲染需要的资源,缩短页面响应时间,但同时会增加打包后的文件数量,导致页面请求数量增加。
      5. 组件按路由分组,打包的时候会将相同组名的资源打包到一个文件中,可以减小请求数


      作者:宠老婆的程序员
      来源:juejin.cn/post/7351292656633331747
      收起阅读 »

      接了个私活,分享下我是如何从 0 到 1 交付项目的

      web
      大家好,我是阿杆,不是阿轩。 最近有个校友找到我,他自己办了个公司,想做个微信小程序,于是找我帮他开发,当然不是免费的。 我一想,那挺好呀,虽然我没接过私活吧,但不代表我以后不会接私活,这不正好可以练习一下子。 前前后后弄了一个半月到两个月,也算是积累了一点经...
      继续阅读 »

      大家好,我是阿杆,不是阿轩。


      最近有个校友找到我,他自己办了个公司,想做个微信小程序,于是找我帮他开发,当然不是免费的。


      我一想,那挺好呀,虽然我没接过私活吧,但不代表我以后不会接私活,这不正好可以练习一下子。


      前前后后弄了一个半月到两个月,也算是积累了一点经验,分享给大家,如果以后也接到私活,可以参考一下我的开展方式。


      由于文中涉及到实际业务的东西不方便透露, 下面我将用图书管理系统来代替实际业务,并且称这位校友为“老板”。


      image-20240421154347807


      总览


      我接手的这个项目是完完全全从0开始的,老板只有一个idea,然后说他的idea是来自于另一个小程序的,有部分内容可以参考那个小程序,其他什么都没有了。


      先讲一下我的总体流程:



      1. 确定老板的大致需求,以及预期费用

      2. 详细梳理开发功能点,并简单画下原型图

      3. 工时评估,确定费用

      4. 出正式的UI设计稿

      5. 拟定合同、签合同

      6. 开发阶段

      7. 验收、上线


      大概就是这么些步骤,也对应本文的目录,如果你有想看的部分,可以直接跳转到对应的部分。


      下面我会详细讲讲每一步我都做了些什么。


      确定需求


      首先老板找到我,跟我说他想做一个图书管理的微信小程序,然后讲了几个小程序内的功能点。


      我也向他提了几个问题:



      1. 预算有多少?



        这个肯定得问的,要是预算和工作量严重不匹配,那肯定做不了的。毕竟都是出来赚钱的,总不能让咱用爱发电吧?




      2. 预计一年内会有多少用户量?会有多少数据量?



        这个问题我主要是想知道并发量会有多少、数据量会有多少?这样方便我后续判断系统需要的配置,也便于我后续对整个系统的设计。


        好在整体用户量和数据量都不大,这对我来说也就没什么压力了,至于以后会发展到如何,这不是我该考虑的事情,我顶多把代码写好看点,他后续发展壮大了肯定是把项目接到公司里雇人做的,跟我也没什么关系。




      3. 你那边能够提供什么?



        这个主要是看对方有什么资源,是否能够对项目开发有一定的帮助。


        在我这个项目里,老板那边是什么都没有的,没有设计图、没有服务器资源、也没有辅助人员,所有内容都包揽在我这边,然后有什么问题就直接问他。




      4. 你希望多久完成?



        如果老板很急的话,那可能得多叫几个人一起做,如果时间充足的话,自己一个人做也不是不可以。





      好了,第一次对话大概就是这么些内容,但仅靠一次对话肯定是无法确定需求的,只能了解个大概。


      我根据老板的想法,写了一份 需求分析 出来,首先列出了几个大概的功能点:


      大致功能点列举


      然后根据这些功能点进行扩展,把所有功能列举出来,画成一个思维导图(打码比较多,大家将就将就😅):


      延伸的思维导图


      好,那么第一版的需求分析差不多就出来了,接着我打电话给老板,对着这个思维导图,一个一个的跟老板确认,需不需要这些功能。


      老板一般会提出一些异议,我再继续修改思维导图,直到老板觉得这些功能他都满意了。当然这过程中我也会给一些自己的建议,有些超预算的功能,还是建议他不要做。


      到这里,需求就基本确定好了。


      梳理开发功能点、绘制原型图


      由于我不会前端开发,只是个简单的后端仔,所以我还找了一个前端同学一起做。


      我和前端两个人根据前面的需求文档,详细的梳理出了 小程序 和 后台管理系统 的功能,这个部分是比较重要的,因为后续画设计稿和开发都会以这份文档为主:


      小程序功能梳理文档


      还画了一些简单的原型图,这玩意丑点没事,能让人看懂就行🤣🤣:


      小程序原型图-我的信息


      后台管理系统原型图


      这些东西弄完之后,再找老板进行一遍确认,把里面每个点都确认下来,达成共识。


      工时评估,确定费用


      老板觉得OK了,就到了该谈钱的时候了,前面只是聊了预算,并不是正式的确定费用。


      那咱们也不能张嘴就要,要多了老板不乐意,要少了自己吃亏。


      所以咱们先评估下工时,这边我分了几个部分分别进行工时评估:



      • 需求分析、功能梳理(就是前面做的那些,还没收钱的呢)

      • UI设计、交互设计

      • 前端开发

      • 后端开发

      • 系统运维(包含服务器购买、搭建、认证、配置等)

      • 后期维护


      其中设计稿是找另一位朋友做的,钱单独算,然后其他部分都是我和前端同学两个人评估的,评估的粒度还是比较细的,是以小时为单位进行计算的,给大家大概看一下:


      前端开发工时评估


      后端开发工时评估


      评估完之后汇总一下,然后根据我们自己工作的时薪,给老板一个最终的定价,正常的话还需要在这个定价上再乘一个接单系数(1.2~1.5),但是我们这个老板是校友啊,而且预算也不多,所以就没乘这个系数了(还给他打了折😂,交个朋友)。


      定价报出去之后,老板觉得贵了怎么办?很简单,砍功能呗,要么你去找别人做也行。



      预付订金



      我觉得正常应该在梳理功能之前就要付一部分订金,也不用多少,几百块就行,算是给我们梳理功能的钱。



      这里接下来就要画UI图了,我们先找老板付个订金,订金分为三部分:



      • 给前端的订金

      • 给后端的订金

      • 给UI同学画设计稿的完整费用


      因为UI设计是我这边联系的,所以我肯定得先保障她的费用能完整到手,不然到时候画完图跟我说不做了,那我咋对得起画图的人。


      画UI图


      这部分就不用咱们操心了,把文档交给设计同学,然后等她出图就行。


      这个过程中也可以时不时去看看她画的内容符不符合咱们的预期,当个小小的监工。


      盯着干活


      画完稿子需要跟老板、开发都对一遍,看看有没有出入,符不符合预期,有问题及时修改下,没问题就按照这份稿子进行开发了。


      拟定合同、签合同



      合同也是我来拟定的,其实是先到网上找了个软件开发的合同模板,然后再根据自己的想法进行合理的调整。



      为什么我要到这一步才签合同呢?我觉得合同内容越细致越好,最好是能够把要开发的内容、样式都写在合同上,这样省得后面扯皮。


      现在文档也出了,图也画完了,那咱们把这些东西都贴在和合同的附件上,然后附上这些条约:



      • 乙方将严格按照经过甲方审核的《软件功能设计书》的要求进行软件的开发设计。

      • 甲方托付乙方开发的软件在签订合同之后如需增加其它功能,必须以书面形式呈交给乙方,乙方做改动并酌情收取适当费用。


      这样就可以保障我们在开发完后不会被恶意的增加或者修改功能了。


      再改一次


      这里我再列一些其他需要特别注意的点:



      1. 乙方交付日期,以及最多延期多久,如果超时怎么办?

      2. 甲方付款方式和日期(我们是用的 442 ,开工付 40%,中期验收付 40%,开发完验收付 20%)。

      3. 甲方拖欠项目款的处理方式(支付迟延履行金等)。

      4. 服务器费用是谁出?如果是乙方,需要注意包服务器的时限。

      5. 项目维护期,一般一年半年的吧。

      6. 乙方不保证项目 100% 可用,只能保障支撑 多少人 使用,支撑同时在线人数 多少人 ,如果遇到恶意攻击,不归乙方负责。

      7. 软件归属权是谁的?(如果项目款比较少的话,乙方可以要求要软件归属权,之后甲方如果想把项目接回去自己公司维护的话,需要从乙方手里买,这样乙方可以回点血)


      大概就是这些吧,还有其他的东西基本都是按照模板的,没怎么改。


      弄完给老板看看,没问题就签了,有问题两方再协商一下,我们这边是直接签了的。



      开发阶段


      开发没什么好说的,跟你在公司开发一样。


      不过你接私活可不能在公司开发🚫,只能回家了自己干,不然被抓到上班干私活,你看老板裁不裁你就完事了。


      微信小程序上线注意事项


      微信小程序对请求的接口有三个基本要求:



      1. 必须是有备案的域名。

      2. 必须是有SSL证书(https)。

      3. 域名不得带端口号。


      这个域名的问题必须要尽早解决,不然后面开发完了再去弄的话,工信部备案审核都要挺久的,不延期都难。


      还有一种方式,我在逛微信开放社区看到的,使用云函数进行中转,间接请求ip接口,感觉是可行的,也比较省事,具体操作大家可以自己去探索一下。


      我也是吃了没有经验的亏,买域名 + 工信部备案 + 公安备案 + 小程序备案,这一套操作下来真给我整难受死了,直接用云函数省事多了。



      验收、上线


      这部分也没什么好说的,大家在公司也经常经历这个步骤。


      多沟通,多确认,


      唯一需要提醒的是,验收的时候咱不能无条件接收老板的任何要求,毕竟价格和开发内容都是已经定好的,如果要加内容或者改内容,记得酌情要一点工时费,可不能亏待了自己。



      后记


      整个过程中,其实沟通是最重要的,写代码谁不会是吧?但是得让老板觉得OK才行,如果有什么疑问或者觉得不合理的地方啊,最好是尽早沟通,不然越到后面只会让问题变的越来越大。


      最近刚做完这个项目,说实话没赚什么钱,甚至有点小亏😅。而且这个老板还有点拖欠工资的感觉,中期项目款拖到了项目交付才给,项目尾款到目前还没付😅😅。不过还好合同里写到了关于这块的处理方式,倒也不担心他不付这个钱。


      (虽然我也不知道在哪能接到靠谱的私活🤣,但也可以先收藏本文,万一之后来活了,还能翻出来看看)


      最后,希望各位都能接到 very good 的私活,祝大家早日实现财富自由!


      webwxgetmsgimg (1)


      作者:阿杆
      来源:juejin.cn/post/7359764922727333939
      收起阅读 »

      仿今日头条,H5 循环播放的通知栏如何实现?

      web
      我们在各大 App 活动页面,经常会看到循环播放的通知栏。比如春节期间,我就在今日头条 App 中看到了如下通知:「春节期间,部分商品受物流影响延迟发货,请耐心等待,祝你新春快乐!」。 那么,这种循环播放的通知栏如何实现呢?本文我会先介绍它的布局、再介绍它的...
      继续阅读 »

      我们在各大 App 活动页面,经常会看到循环播放的通知栏。比如春节期间,我就在今日头条 App 中看到了如下通知:「春节期间,部分商品受物流影响延迟发货,请耐心等待,祝你新春快乐!」。


      toutiao.gif


      那么,这种循环播放的通知栏如何实现呢?本文我会先介绍它的布局、再介绍它的逻辑,并给出完整的代码。最终我实现的效果如下:


      loop-notice.gif


      拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


      布局代码


      我们先看布局,如下图所示,循环播放的布局不是普通的左中右布局。可以看到,当文字向左移动时,左边的通知 Icon 和右边的留白会把文字挡住一部分。


      block-out.png


      为了实现这样的效果,我们给容器 box 设置一个相对定位,并把 box 中的 HTML 代码分为三部分:



      • 第一部分是 content,它包裹着需要循环播放的文字;

      • 第二部分是 left,它是左边的通知 Icon,我们给它设置绝对定位和 left: 0;

      • 第三部分是 right,它是右边的留白,我们给它设置绝对定位和 right: 0;


      <div class="box">
      <div class="content">

      div>
      <div class="left">🔔div>
      <div class="right">div>
      div>

      .box {
      position: relative;
      overflow: hidden;
      /* ... 省略 */
      }
      .left {
      position: absolute;
      left: 0;
      /* ... 省略 */
      }
      .right {
      position: absolute;
      right: 0;
      /* ... 省略 */
      }

      现在我们来看包裹文字的 content。content 内部包裹了三段一模一样的文字 notice,每段 notice 之间还有一个 space 元素作为间距。



      <div id="content">
      <div class="notice">春节期间,部分商品...div>
      <div class="space">div>
      <div class="notice">春节期间,部分商品...div>
      <div class="space">div>
      <div class="notice">春节期间,部分商品...div>
      div>


      为什么要放置三段一模一样的文字呢?这和循环播放的逻辑有关。


      逻辑代码


      我们并没有实现真正的循环播放,而是欺骗了用户的视觉。如下图所示:



      • 播放通知时,content 从 0 开始向左移动。

      • 向左移动 2 * noticeWidth + spaceWidth 时,继续向左移动便会露馅。因为第 3 段文字后不会有第 4 段文字。


        如果我们把 content 向左移动的距离强行从 2 * noticeWidth + spaceWidth 改为 noticeWidth,不难看出,用户在 box 可视区域内看到的情况基本一致的。


        然后 content 继续向左移动,向左移动的距离大于等于 2 * noticeWidth + spaceWidth 时,就把距离重新设为 noticeWidth。循环往复,就能欺骗用户视觉,让用户认为 content 能无休无止向左移动。



      no-overflow-with-comment.png


      欺骗视觉的代码如下:



      • 我们通过修改 translateX,让 content 不断地向左移动,每次向左移动 1.5px;

      • translateX >= noticeWidth * 2 + spaceWidth 时,我们又会把 translateX 强制设为 noticeWidth

      • 为了保证移动动画更丝滑,我们并没有采用 setInterval,而是使用 requestAnimationFrame。


      const content = document.getElementById("content");
      const notice = document.getElementsByClassName("notice");
      const space = document.getElementsByClassName("space");
      const noticeWidth = notice[0].offsetWidth;
      const spaceWidth = space[0].offsetWidth;

      let translateX = 0;
      function move() {
      translateX += 1.5;
      if (translateX >= noticeWidth * 2 + spaceWidth) {
      translateX = noticeWidth;
      }
      content.style.transform = `translateX(${-translateX}px)`;
      requestAnimationFrame(move);
      }

      move();

      完整代码


      完整代码如下,你可以在 codepen 或者码上掘金上查看。



      总结


      本文我介绍了如何用 H5 实现循环播放的通知栏:



      • 布局方面,我们需要用绝对定位的通知 Icon、留白挡住循环文字的左侧和右侧;此外,循环播放的文字我们额外复制 2 份。

      • 逻辑方面,通知栏向左移动 2 * noticeWidth + spaceWidth 后,我们需要强制把通知栏向左移动的距离从 2 * noticeWidth + spaceWidth 变为 noticeWidth,以此来欺骗用户视觉。




      作者:小霖家的混江龙
      来源:juejin.cn/post/7372765277460496394
      收起阅读 »

      完美代替节假日API,查询中国特色日期

      web
      马上端午节到了,趁着前夕,写了个关于中国特色日期查询的库;由于中国的节假日不固定,以及阴历日期较为特殊,尽可能的做了代码上压缩,做到了 .min.js 源文件尺寸小于 16kb,gzip 压缩后只有 7kb 大小,欢迎大家 PR 使用。 关于中国节假日,后面会...
      继续阅读 »

      马上端午节到了,趁着前夕,写了个关于中国特色日期查询的库;由于中国的节假日不固定,以及阴历日期较为特殊,尽可能的做了代码上压缩,做到了 .min.js 源文件尺寸小于 16kbgzip 压缩后只有 7kb 大小,欢迎大家 PR 使用。


      关于中国节假日,后面会跟随国务院发布进行更新,一言既出,驷马难追。


      当前版本


      NPM Version
      GitHub License
      README


      项目地址:github.com/vsme/chines…


      提供的功能



      1. 中国节假日(含调休日、工作日)查询,支持 2004年 至 2024年,包括 2020年 的春节延长;

      2. 24节气查询;

      3. 农历 阳历 互相转换,含有生肖和天干地支。


      还需要的功能欢迎补充。


      对于非 JS 语言


      提供了中国节假日的 JSON 文件,通过链接 chinese-days.json 可以直接引用。


      https://cdn.jsdelivr.net/npm/chinese-days/dist/chinese-days.json


      快速开始


      推荐方式:直接浏览器引入,会跟随国务院发布更新。


      <script src="https://cdn.jsdelivr.net/npm/chinese-days/dist/index.min.js"></script>
      <script>
      const { isHoliday } = chineseDays
      console.log(isHoliday('2024-01-01'))
      </script>

      其他方式安装


      npm i chinese-days

      使用 ESM 导入


      import chineseDays from 'chinese-days'
      console.log(chineseDays)

      在 Node 中使用


      const { isWorkday, isHoliday } = require('chinese-days');
      console.log(isWorkday('2020-01-01'));
      console.log(isHoliday('2020-01-01'));

      节假日模块


      isWorkday 检查某个日期是否为工作日


      console.log(isWorkday('2023-01-01')); // false

      isHoliday 检查某个日期是否为节假日


      console.log(isHoliday('2023-01-01')); // true

      isInLieu 检查某个日期是否为调休日(in lieu day)


      在中国的节假日安排中,调休日是为了连休假期或补班而调整的工作日或休息日。例如,当某个法定假日与周末相连时,可能会将某个周末调整为工作日,或者将某个工作日调整为休息日,以便连休更多天。


      // 检查 2024-05-02 返回 `true` 则表示是一个调休日。
      console.log(isInLieu('2024-05-02')); // true

      // 检查 2024-05-01 返回 `false` 则表示不是一个调休日。
      console.log(isInLieu('2024-05-01')); // false

      getDayDetail 检查指定日期是否是工作日


      函数用于检查指定日期是否是工作日,并返回一个是否工作日的布尔值和日期的详情。



      1. 如果指定日期是工作日,则返回 true 和工作日名称,如果是被调休的工作日,返回 true 和节假日详情。

      2. 如果是节假日,则返回 false 和节假日详情。


      // 示例用法

      // 正常工作日 周五
      console.log(getDayDetail('2024-02-02')); // { "date": "2024-02-02", "work":true,"name":"Friday"}
      // 节假日 周末
      console.log(getDayDetail('2024-02-03')); // { "date": "2024-02-03", "work":false,"name":"Saturday"}
      // 调休需要上班
      console.log(getDayDetail('2024-02-04')); // { "date": "2024-02-04", "work":true,"name":"Spring Festival,春节,3"}
      // 节假日 春节
      console.log(getDayDetail('2024-02-17')); // { "date": "2024-02-17", "work":false,"name":"Spring Festival,春节,3"}

      getHolidaysInRange 获取指定日期范围内的所有节假日


      接收起始日期和结束日期,并可选地决定是否包括周末。如果包括周末,则函数会返回包括周末在内的所有节假日;否则,只返回工作日的节假日。



      tip: 即使不包括周末,周末的节假日仍然会被返回



      // 示例用法
      const start = '2024-04-26';
      const end = '2024-05-06';

      // 获取从 2024-05-01 到 2024-05-10 的所有节假日,包括周末
      const holidaysIncludingWeekends = getHolidaysInRange(start, end, true);
      console.log('Holidays including weekends:', holidaysIncludingWeekends.map(d => getDayDetail(d)));

      // 获取从 2024-05-01 到 2024-05-10 的节假日,不包括周末
      const holidaysExcludingWeekends = getHolidaysInRange(start, end, false);
      console.log('Holidays excluding weekends:', holidaysExcludingWeekends.map(d => getDayDetail(d)));

      getWorkdaysInRange 取指定日期范围内的工作日列表


      接收起始日期和结束日期,并可选地决定是否包括周末。如果包括周末,则函数会返回包括周末在内的所有工作日;否则,只返回周一到周五的工作日。


      // 示例用法
      const start = '2024-04-26';
      const end = '2024-05-06';

      // 获取从 2024-05-01 到 2024-05-10 的所有工作日,包括周末
      const workdaysIncludingWeekends = getWorkdaysInRange(start, end, true);
      console.log('Workdays including weekends:', workdaysIncludingWeekends);

      // 获取从 2024-05-01 到 2024-05-10 的工作日,不包括周末
      const workdaysExcludingWeekends = getWorkdaysInRange(start, end, false);
      console.log('Workdays excluding weekends:', workdaysExcludingWeekends);

      findWorkday 查找工作日


      查找从今天开始 未来的第 {deltaDays} 个工作日。


      // 查找从今天开始 未来的第 {deltaDays} 个工作日
      // 如果 deltaDays 为 0,首先检查当前日期是否为工作日。如果是,则直接返回当前日期。
      // 如果当前日期不是工作日,会查找下一个工作日。
      const currentWorkday = findWorkday(0);
      console.log(currentWorkday);

      // 查找从今天开始未来的第一个工作日
      const nextWorkday = findWorkday(1);
      console.log(nextWorkday);

      // 查找从今天开始之前的前一个工作日
      const previousWorkday = findWorkday(-1);
      console.log(previousWorkday);

      // 可以传第二个参数 查找具体日期的上下工作日
      // 查找从 2024-05-18 开始,未来的第二个工作日
      const secondNextWorkday = findWorkday(2, '2024-05-18');
      console.log(secondNextWorkday);

      节气模块


      获取 24 节气的日期


      import { getSolarTerms } from "chinese-days";

      /** 获取范围内 节气日期数组 */
      const solarTerms = getSolarTerms("2024-05-01", "2024-05-20");
      solarTerms.forEach(({ date, term, name }) => {
      console.log(`${name}: ${date}, ${term}`);
      });
      // 立夏: 2024-05-05, the_beginning_of_summer
      // 小满: 2024-05-20, lesser_fullness_of_grain

      // 没有节气 返回 []
      getSolarTerms("2024-05-21", "2024-05-25");
      // return []

      /* 不传 end 参数, 获取某天 节气 */
      getSolarTerms("2024-05-20");
      // return: [{date: '2024-05-20', term: 'lesser_fullness_of_grain', name: '小满'}]

      阳历农历互转


      特别说明,此库中:



      1. 2057-09-28 为:农历丁丑(牛)年八月三十;

      2. 2097-08-07 为:农历丁巳(蛇)年七月初一。


      阳历转换农历


      // 2097-8-7
      console.log(getLunarDate('2097-08-07'))

      // 2057-9-28
      console.log(getLunarDate('2057-09-28'))
      // {
      // date: "2057-09-28",
      // lunarYear: 2057,
      // lunarMon: 8,
      // lunarDay: 30,
      // isLeap: false,
      // lunarDayCN: "三十",
      // lunarMonCN: "八月",
      // lunarYearCN: "二零五七",
      // yearCyl: "丁丑",
      // monCyl: "己酉",
      // dayCyl: "戊子",
      // zodiac: "牛"
      // }

      // 非闰月 和 闰月例子
      console.log(getLunarDate('2001-04-27'))
      console.log(getLunarDate('2001-05-27'))

      根据阳历日期区间,批量获取农历日期


      console.log(getLunarDatesInRange('2001-05-21', '2001-05-26'))

      农历转换阳历


      当为阴历闰月的时候,会出现一个农历日期对应两个阳历日期的情况,所以返回对象形式。


      console.log(getSolarDateFromLunar('2001-03-05'))
      // {date: '2001-03-29', leapMonthDate: undefined}

      console.log(getSolarDateFromLunar('2001-04-05'))
      // {date: '2001-04-27', leapMonthDate: '2001-05-27'}

      欢迎贡献代码



      1. Fork + Clone 项目到本地;

      2. 节假日: 修改 节假日定义

      3. 农历定义: 修改 农历定义

      4. 其他修改需要自己查看源码;

      5. 执行命令 npm run generate 自动生成 节假日常量文件

      6. 提交PR。


      致谢



      1. 农历数据来自于 Bigkoo/Android-PickerView 项目。

      2. 中国节假日数据参考了 Python 版本的 LKI/chinese-calendar 项目。


      作者:Yaavi
      来源:juejin.cn/post/7371815617462714402
      收起阅读 »

      在滴滴开发H5一年了,我遇到了这些问题

      web
      IOS圆角不生效 ios中使用border-radius配合overflow:hidden出现了失效的情况: 出现此问题的原因是因为ios手机会在transform的时候导致border-radius失效 解决方法:在使用动画效果带transform的元...
      继续阅读 »

      IOS圆角不生效


      ios中使用border-radius配合overflow:hidden出现了失效的情况:


      image.png



      出现此问题的原因是因为ios手机会在transform的时候导致border-radius失效



      解决方法:在使用动画效果带transform的元素的上一级div元素的css加上下面语句:


      -webkit-transform:rotate(0deg);

      IOS文本省略溢出问题


      在部分ios手机上会出现以下情况:


      image.png


      原因


      在目标元素上设置font-size = line-height,并加上以下单行省略代码:


      .text-overflow {
      display: -webkit-box;
      overflow : hidden;
      text-overflow: ellipsis;
      word-break: break-all;
      -webkit-line-clamp: 1;
      -webkit-box-orient: vertical;
      }

      或者:


      .text-overflow {
      overflow : hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
      }

      由于不同系统包含的字体的行高不一致,即使设置了height = line-height 一样会有以上问题


      解决方案


      经过测试,在height = line-height = font-szie的情况下,加上padding-top: 1px可以解决这个问题,即在需要使用单行省略的地方加上:


      .demo {
      height: 28px;
      line-height: 28px;
      font-size: 28px;
      padding-top: 1px;
      }

      如:<div class="text-overflow demo">我是需要进行单行省略的文案</div>


      安卓手机按钮点击后有橙色边框


      image.png


      解决方案:


      button:focus {
      outline: none;
      }

      优惠券打孔效果


      需求中经常需要实现一类效果:优惠券打孔,如下图所示:


      image.png


      通常情况下会找设计采用图片的的形式,但这个方案最大的缺陷是无法适配背景的变化。
      因此,我们可以采用如下方案,左右两侧各打一个孔,且穿透背景:


      image.png


      具体细节可以参考这篇文章:纯 CSS 实现优惠券透明圆形镂空打孔效果


      Clipboard兼容性问题


      navigator.clipboard兼容性不是很好,低版本浏览器不支持


      image.png


      解决方案:


      const copyText = (text: string) => {
      return new Promise(resolve => {
      if (navigator.clipboard?.writeText) {
      return resolve(navigator.clipboard.writeText(text))
      }
      // 创建输入框
      const textarea = document.createElement('textarea')
      document.body.appendChild(textarea)
      // 隐藏此输入框
      textarea.style.position = 'absolute'
      textarea.style.clip = 'rect(0 0 0 0)'
      // 赋值
      textarea.value = text
      // 选中
      textarea.select()
      // 复制
      document.execCommand('copy', true)
      textarea.remove()
      return resolve(true)
      })
      }

      Unocss打包后样式不生效


      这个问题是由webpack缓存导致的,在vue.config.js中添加以下代码:


      config.module.rule('vue').uses.delete('cache-loader')

      具体原因见:UnoCSS webpack插件原理


      低端机型options请求不过问题


      在我们的业务需求中,覆盖的人群很广,涉及到的机型也很多。于是我们发现在部分低端机型下(oppo R11、R9等),有很多请求只有options请求,没有真正的业务请求。导致用户拿不到数据,报network error错误,我们的埋点数据也记录到了这一异常。


      在我们的这个项目中,我们的后台有两个,一个提供物料,一个提供别的数据。但是奇怪的是,物料后台是可以正常获取数据,但业务后台就不行!


      经过仔细对比二者发送的options请求,发现了问题所在:


      image.png


      发现二者主要存在以下差异:



      1. Access-Control-Allow-Headers: *

      2. Access-Control-Allow-origin: *


      于是我便开始排查两个响应头的兼容性,发现在这些低端机型上,Access-Control-Allow-Headers: *确实会有问题,这些旧手机无法识别这个通配符,或者直接进行了忽略,导致options请求没过,自然就没有后续真正的请求了。


      image.png


      解决方案:由后台枚举前端需要的headers,在Access-Control-Allow-Headers中返回。


      此外,将Access-Control-Allow-Origin设置为*也有一些别的限制:



      参考



      作者:WeilinerL
      来源:juejin.cn/post/7372396174249459750
      收起阅读 »

      前端命令行部署:再也不用把dist包给后端部署服务了!

      web
      好物推荐 超简单的命令行部署。给在小公司部署还是给后端dist包的萌新小伙伴们~ 这边项目本身就是使用命令行部署到,不过那个命令行工具是自己写的,是嵌入到公司某一个私有npm包里,和其他依赖耦合在一起。灵活性不是很好。 这两天发现了一个别人写的一个deploy...
      继续阅读 »

      好物推荐


      超简单的命令行部署。给在小公司部署还是给后端dist包的萌新小伙伴们~


      这边项目本身就是使用命令行部署到,不过那个命令行工具是自己写的,是嵌入到公司某一个私有npm包里,和其他依赖耦合在一起。灵活性不是很好。
      这两天发现了一个别人写的一个deploy cli。感觉蛮好用的。分享一下。


      希望可以帮助更多刚入行小伙伴了解更多前端玩法。


      前端命令行部署


      很多公司的前端部署流程都是先打一个dist包。然后给后端同事帮忙部署。


      前端:::
      1714281510854.png


      后端:::


      529ae5c36b03377bf116bafea2e95f1.png


      (开玩笑的,工作中的后端同事都没那么调皮)


      本文的内容就是如何使用命令行进行前端自动部署。


      我们整个网站的读取,其实就是我们上传一个静态的文件包到服务器,然后服务器上的后台服务读取我们的静态包,来进行页面的展示。所以,前端自动化部署的关键,就是,能把dist包传到服务器的指定目录下就OK了。


      部署流程


      推荐一个deploy cli工具(deploy-cli-service)


      安装


      1. 执行 npm install deploy-cli-service -g 进行全局安装 。

      2. 执行 deploy-cli-service - v 查看版本


      初始化配置文件

      在项目根目录执行 deploy-cli-service init 进行初始化


      deploy-cli-service init命令执行后项目目录下会出现一个名为deploy.config的文件


      image.png


      deploy-cli-service init初始化的内容会被默认输入到 deploy.config


      修改配置文件

      deploy-cli-service init初始化之后输入的内容都会默认被写入deploy.config文件中。


      image.png


      然后看看相关的属性有没有什么需要修改的就ok。


      配置部署命令


      image.png


      "deploy:test": "deploy-cli-service deploy --mode test"," 写入到 package.json中的script里。


      然后在命令行执行 "npm run deploy:test"


      成功部署后会如下显示


      image.png


      image.png


      注意


      配置 deploy.config.js时尽量使用ssh证书登录,不要使用服务器密码,把服务器密码写在前端代码里是一件非常不好的操作。


      deploy-cli-service npm地址


      luck


      作者:工边页字
      来源:juejin.cn/post/7362924623825256463
      收起阅读 »

      互联网+《周易》:我在github学算卦

      web
      前言 《周易》乃周文王姬昌所作,是中国传统思想文化中自然哲学与人文实践的理论根源,是古代汉民族思想、智慧的结晶,被誉为“大道之源”。内容极其丰富,对中国几千年来的政治、经济、文化等各个领域都产生了极其深刻的影响。 像这种千古奇书,每个中国人都应该读一读,一是因...
      继续阅读 »

      前言


      《周易》乃周文王姬昌所作,是中国传统思想文化中自然哲学与人文实践的理论根源,是古代汉民族思想、智慧的结晶,被誉为“大道之源”。内容极其丰富,对中国几千年来的政治、经济、文化等各个领域都产生了极其深刻的影响。


      像这种千古奇书,每个中国人都应该读一读,一是因为这是老祖宗的智慧,我们不能丢弃;二是因为《周易》蕴含宇宙人文的运行规律,浅读可修身养性,熟读可明自我,深究可知未来,参透就可知天命了。


      东汉著名史学家、文学家班固在《汉书•艺文志》中提出《周易》的成书是:人更三圣,世历三古


      那么在哪里才可以读到呢?


      其实易经的完本在网上随便就可以找到,但是都不适合在摸鱼的时候读 (!🤡),打开花花绿绿或者神神叨叨的小网站,你的 leader 肯定一眼就看出你在摸鱼。


      既然没有这种网站,那干脆自己做一个。


      vitePress + github pages 快速搭建


      vitePress 快速开始


      pnpm add -D vitepress

      pnpm vitepress init

      填写完 cli 里的几个问题,项目就可以直接运行了。可以看到网站直接解析了几个 示例的 md 文件,非常的神奇。


      处理《周易》文本


      那么哪里才可以找到《周易》的 markdown 版本呢,找了一圈也没有找到,最后找到了一个 txt 的,我觉得写个脚本转换一下。


      首先,我拿 vscode 的正则给每个标题加上井号,使其成为一级标题


      QQ2024511-183935.webp


      此时,所有的标题都被改成了md格式的一级标题,然后直接将后缀名从 .txt 改为 .md 即可。


      看过 vitepress 的文档并经过实操后发现,它的目录是一个一个的小 markdown 文件组成的,而单个 markdown 内的标题等在右侧显示


      image.png


      那么此时就需要把《周易》完本,按照六十四卦分为六十四个 md 文件。


      我写了一个node脚本:


      const fs = require('fs');

      // 读取zhouyi.md文件
      fs.readFile('zhouyi.md', 'utf8', (err, data) => {
       if (err) {
         console.error('读取文件出错:', err);
         return;
      }

       // 按一级标题进行分割
       const sections = data.split('\n# ');

       // 循环处理每个一级标题的内容
       sections.forEach((section, index) => {
         // 提取标题和内容
         const lines = section.split('\n');
         const title = lines[0];
         const content = lines.slice(1).join('\n');

         // 写入到单独的文件中
         const fileName = `zhouyi_${index + 1}.md`;
         fs.writeFile(fileName, `# ${title}\n\n${content}`, err => {
           if (err) {
             console.error(`写入文件 ${fileName} 出错:`, err);
          } else {
             console.log(`已创建文件: ${fileName}`);
          }
        });
      });
      });


      取名为md-slicer.js ,在控制台输入


      node md-slicer.js

      即可生成


      image.png


      然后写一个在 .vitepress/config.mtssidebar的生成函数:


      let itemsLength = 64
      function getSidebar() {
       let items: {}[] = [{
         text: '《周易》是什么?',
         link: '/what.md'
      }]
       for (let i = 1; i <= itemsLength; i++) {
         items.push({ text: `第${numberToChinese(i)}卦`, link: `/zhouyi_${i}.md` })
      }
       return items
      }

      numberToChinese函数用来将阿拉伯数字转为中文数字,因为周易只有六十四卦,所以不用考虑很多,够用即可


      // numberToChinese
      function numberToChinese(number) {
       const chineseNumbers = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
       const chineseUnits = ['', '十', '百', '千', '万', '亿'];

       // 将数字转换为字符串,以便于处理每一位
       const numStr = String(number);

       let result = '';
       let zeroFlag = false; // 用于标记是否需要加上“零”

       for (let i = 0; i < numStr.length; i++) {
         const digit = parseInt(numStr[i]); // 当前位的数字
         const unit = chineseUnits[numStr.length - i - 1]; // 当前位的单位

         if (digit !== 0) {
           if (zeroFlag) {
             result += chineseNumbers[0]; // 如果前一位是零,则在当前位加上“零”
             zeroFlag = false;
          }
           result += chineseNumbers[digit] == "一" && unit == "十" ? unit : chineseNumbers[digit] + unit; // 加上当前位的数字和单位,当一十时,省略前面的一
        } else {
           zeroFlag = true; // 如果当前位是零,则标记为需要加上“零”
        }
      }
       return result;
      }

      然后,设置一下vitepress基础配置和打包输出路径


      export default defineConfig({
       title: "周易",
       description: "周易",
       base: "/thebookofchanges/",
       head: [
        ['link', { rel: 'icon', href: 'yi.svg' }] // 这里是你的 Logo 图片路径
      ],
       outDir: 'docs', // 输出到docs ,可以直接在 github pages 使用
       themeConfig: {
         // https://vitepress.dev/reference/default-theme-config
         nav: [
          { text: '首页', link: '/' },
          { text: '阅读', link: '/zhouyi_1.md' }
        ],
         logo: '/yi.svg',
         sidebar: [
          {
             text: '目录',
             items: getSidebar()
          }
        ],

         socialLinks: [
          { icon: 'github', link: 'https://github.com/LarryZhu-dev/thebookofchanges' }
        ]
      }
      })


      然后简单给网站设计一个logo


      image.png


      字体是华文隶书,转化为路径后,将它拉瘦一点,再导出为 svg。


      最后,用 pnpm run docs:build打包即可,打包时注意设置基本路径为 github pages 的仓库名。


      发布


      push到github后,在 Setting/Pages 页面发布即可。


      image.png


      效果预览


      最后,网站运行在:larryzhu-dev.github.io/thebookofch…


      image.png


      image.png


      仓库地址:github.com/LarryZhu-de… 来点star🤣


      结语


      现在只有简单的原文,如有 《周易》大佬,欢迎大佬提交注解PR。


      作者:德莱厄斯
      来源:juejin.cn/post/7367659849101312015
      收起阅读 »

      7个Js async/await高级用法

      web
      7个Js async/await高级用法 JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护...
      继续阅读 »

      7个Js async/await高级用法


      JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护性。在掌握了基础用法之后,下面将介绍一些高级用法,以便充分利用async/await实现更复杂的异步流程控制。


      1. async/await与高阶函数


      当需要对数组中的元素执行异步操作时,可结合async/await与数组的高阶函数(如mapfilter等)。


      // 异步过滤函数
      async function asyncFilter(array, predicate) {
      const results = await Promise.all(array.map(predicate));

      return array.filter((_value, index) => results[index]);
      }

      // 示例
      async function isOddNumber(n) {
      await delay(100); // 模拟异步操作
      return n % 2 !== 0;
      }

      async function filterOddNumbers(numbers) {
      return asyncFilter(numbers, isOddNumber);
      }

      filterOddNumbers([1, 2, 3, 4, 5]).then(console.log); // 输出: [1, 3, 5]

      2. 控制并发数


      在处理诸如文件上传等场景时,可能需要限制同时进行的异步操作数量以避免系统资源耗尽。


      async function asyncPool(poolLimit, array, iteratorFn) {
      const result = [];
      const executing = [];

      for (const item of array) {
      const p = Promise.resolve().then(() => iteratorFn(item, array));
      result.push(p);

      if (poolLimit <= array.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= poolLimit) {
      await Promise.race(executing);
      }
      }
      }

      return Promise.all(result);
      }

      // 示例
      async function uploadFile(file) {
      // 文件上传逻辑
      }

      async function limitedFileUpload(files) {
      return asyncPool(3, files, uploadFile);
      }

      3. 使用async/await优化递归


      递归函数是编程中的一种常用技术,async/await可以很容易地使递归函数进行异步操作。


      // 异步递归函数
      async function asyncRecursiveSearch(nodes) {
      for (const node of nodes) {
      await asyncProcess(node);
      if (node.children) {
      await asyncRecursiveSearch(node.children);
      }
      }
      }

      // 示例
      async function asyncProcess(node) {
      // 对节点进行异步处理逻辑
      }

      4. 异步初始化类实例


      在JavaScript中,类的构造器(constructor)不能是异步的。但可以通过工厂函数模式来实现类实例的异步初始化。


      class Example {
      constructor(data) {
      this.data = data;
      }

      static async create() {
      const data = await fetchData(); // 异步获取数据
      return new Example(data);
      }
      }

      // 使用方式
      Example.create().then((exampleInstance) => {
      // 使用异步初始化的类实例
      });

      5. 在async函数中使用await链式调用


      使用await可以直观地按顺序执行链式调用中的异步操作。


      class ApiClient {
      constructor() {
      this.value = null;
      }

      async firstMethod() {
      this.value = await fetch('/first-url').then(r => r.json());
      return this;
      }

      async secondMethod() {
      this.value = await fetch('/second-url').then(r => r.json());
      return this;
      }
      }

      // 使用方式
      const client = new ApiClient();
      const result = await client.firstMethod().then(c => c.secondMethod());

      6. 结合async/await和事件循环


      使用async/await可以更好地控制事件循环,像处理DOM事件或定时器等场合。


      // 异步定时器函数
      async function asyncSetTimeout(fn, ms) {
      await new Promise(resolve => setTimeout(resolve, ms));
      fn();
      }

      // 示例
      asyncSetTimeout(() => console.log('Timeout after 2 seconds'), 2000);

      7. 使用async/await简化错误处理


      错误处理是异步编程中的重要部分。通过async/await,可以将错误处理的逻辑更自然地集成到同步代码中。


      async function asyncOperation() {
      try {
      const result = await mightFailOperation();
      return result;
      } catch (error) {
      handleAsyncError(error);
      }
      }

      async function mightFailOperation() {
      // 有可能失败的异步操作
      }

      function handleAsyncError(error) {
      // 错误处理逻辑
      }

      通过以上七个async/await的高级用法,开发者可以在JavaScript中以更加声明式和直观的方式处理复杂的异步逻辑,同时保持代码整洁和可维护性。在实践中不断应用和掌握这些用法,能够有效地提升编程效率和项目的质量。


      作者:慕仲卿
      来源:juejin.cn/post/7311603994928513076
      收起阅读 »

      Google 如果把 Go 团队给裁了会怎么样?

      大家好,我是煎鱼。 节前有一则劲爆消息,Google 把 Python 基础团队和 flutter/dart 团队里相当多的开发人员给解雇了,据说可能是要换个城市重组(真是熟悉的 CY 套路)。 据悉被解雇的人中基本都是负责了 Python 重要维护的相关核心...
      继续阅读 »

      大家好,我是煎鱼。


      节前有一则劲爆消息,Google 把 Python 基础团队和 flutter/dart 团队里相当多的开发人员给解雇了,据说可能是要换个城市重组(真是熟悉的 CY 套路)。


      据悉被解雇的人中基本都是负责了 Python 重要维护的相关核心成员。


      如下图所示:



      此时引发了国内外社区一个较大的担忧,如果 Google 如法炮制,要放弃 Go 核心团队。会发生什么事,会不会有什么问题?



      现在有什么


      先知道可能会失去什么,那得先盘点一下 Go 这一门编程语言和 Go 核心团队在 Google 获得了什么。


      根据我们以往对 @Lance Taylor 所澄清以及各处的描述,可以估算 Go 在 Google 大概获得了什么。


      其至少包含以下内容:



      1. 工作岗位:Go 核心团队相关成员的工作岗位,包含薪资、福利等各种薪酬内容。

      2. 软硬件资源:Go 相关的软硬件资源(例如:知识产权、服务器、域名、模块管理镜像)等网上冲浪所需信息。

      3. 线下活动:Go 世界各地部分大会的开展可能会变少,或缩减规模(资金、背书等)。

      4. 大厂内部资源:因为失去 Google 内部的资源,可能逐步失去一些先进项目的熏陶和引入使用 Go 这一门编程语言的机会。

      5. 推广和反馈渠道:Go 一些显著问题和特性的发现、响应,可能会变慢。因为 Go 对于 Google 内部的问题处理和特性需要,历史上来看都是按最高优先级处理。


      可能会发生什么事


      如果真的一刀切,Google 把 Go 核心团队干没了,基础设施全部都不提供了。


      大家普遍认为,会出现如下几种情况:



      1. 如果 Go 团队中的很多人被裁员,他们会另谋高就。各散东西。维护积极性和组织性会大幅下降。

      2. 如果 Google 决定完全停止对 Go 的投资,Go 的维护可能会变得更加复杂,因为它需要运行大量的基础设施。在这种情况下,可能会出现 Go 由 Google 转移到一个外部的基金会,会有明显的阶段性维护波动。

      3. 如果 Google 选择在内部其他团队对 Go 继续投入,较差的情况是 Google 会灵活运用他们对知识产权的所有权 --Go 很可能会更名为其他东西。


      基金会方面,另外大家认为最有可能接受 Go 的基金会是:CNCF,因为 Go 项目在 CNCF 中基于数量来讲是最大的。


      如下图部分所示:



      同时 CNCF 和 Go 的云原生属性最为强烈,契合度非常高。


      参考 Rust 发展史


      @azuled 根据 Rust 的发展历史,给出了自己的一些见解。如下所表述:


      1、Rust 被踢出 Mozilla 核心,成为一个独立的基金会,但它仍然存活了下来。事实上,它后来可能做得更好。


      2、我认为很有可能围绕 Go 成立一个非营利组织,而且很有可能有足够多的大公司使用它来支持它,至少在一段时间内是这样。


      总结


      在目前这个大行情下,Go 作为 Google Cloud 团队的一员,和云原生的故事捆绑在一起。如果 Google 业绩出现波动,或者要继续降本增效。


      这类没有直接营收的基础部门或团队还是比较危险的,因为其会在企业中根据利润中心、成本中心进行分摊和计算人效成本等。


      如果真的强硬切割,势必会对 Go 这门编程语言产生阶段性的冲击。但未来是好是坏,就不好说了。



      作者:煎鱼eddycjy
      来源:juejin.cn/post/7366070642047008783
      收起阅读 »

      20个你不得不知道的Js async/await用法

      web
      20个你不得不知道的Js async/await用法 JavaScript的async和await关键词是现代JavaScript异步编程的核心。它们让异步代码看起来和同步代码几乎一样,使得异步编程变得更加直观和易于管理。本文介绍20个关于async/awai...
      继续阅读 »

      20个你不得不知道的Js async/await用法


      JavaScript的asyncawait关键词是现代JavaScript异步编程的核心。它们让异步代码看起来和同步代码几乎一样,使得异步编程变得更加直观和易于管理。本文介绍20个关于async/await的实用技巧,将大大提升编程效率和代码的清晰度。


      1. 基础用法


      async函数返回一个Promise,而await关键词可以暂停async函数的执行,等待Promise解决。


      async function fetchData() {
      let data = await fetch('url');
      data = await data.json();
      return data;
      }

      2. 错误处理


      使用try...catch结构处理async/await中的错误。


      async function fetchData() {
      try {
      let response = await fetch('url');
      response = await response.json();
      return response;
      } catch (error) {
      console.error('Fetching data error:', error);
      }
      }

      3. 并行执行


      Promise.all()可以用来并行执行多个await操作。


      async function fetchMultipleUrls(urls) {
      const promises = urls.map(url => fetch(url).then(r => r.json()));
      return await Promise.all(promises);
      }

      4. 条件异步


      根据条件执行await


      async function fetchData(condition) {
      if (condition) {
      return await fetch('url');
      }
      return 'No fetch needed';
      }

      5. 循环中的await


      在循环中使用await时,每次迭代都会等待。


      async function sequentialStart(urls) {
      for (const url of urls) {
      const response = await fetch(url);
      console.log(await response.json());
      }
      }

      6. 异步迭代器


      对于异步迭代器(例如Node.js中的Streams),可以使用for-await-of循环。


      async function processStream(stream) {
      for await (const chunk of stream) {
      console.log(chunk);
      }
      }

      7. await之后立即解构


      直接在await表达式后使用解构。


      async function getUser() {
      const { data: user } = await fetch('user-url').then(r => r.json());
      return user;
      }

      8. 使用默认参数避免无效的await


      如果await可能是不必要的,可以使用默认参数避免等待。


      async function fetchData(url = 'default-url') {
      const response = await fetch(url);
      return response.json();
      }

      9. await在类的方法中


      在类的方法中使用async/await


      class DataFetcher {
      async getData() {
      const data = await fetch('url').then(r => r.json());
      return data;
      }
      }

      10. 立刻执行的async箭头函数


      可以立即执行的async箭头函数。


      (async () => {
      const data = await fetch('url').then(r => r.json());
      console.log(data);
      })();

      11. 使用async/await进行延时


      利用async/await实现延时。


      function delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
      }

      async function delayedLog(item) {
      await delay(1000);
      console.log(item);
      }

      12. 使用async/await处理事件监听器


      在事件处理函数中使用async/await


      document.getElementById('button').addEventListener('click', async (event) => {
      event.preventDefault();
      const data = await fetch('url').then(r => r.json());
      console.log(data);
      });

      13. 以顺序方式处理数组


      使用async/await以确定的顺序处理数组。


      async function processArray(array) {
      for (const item of array) {
      await delayedLog(item);
      }
      console.log('Done!');
      }

      14. 组合async/awaitdestructuring以及spread运算符


      结合使用async/await,解构和展开操作符。


      async function getConfig() {
      const { data, ...rest } = await fetch('config-url').then(r => r.json());
      return { config: data, ...rest };
      }

      15. 在对象方法中使用async/await


      async方法作为对象的属性。


      const dataRetriever = {
      async fetchData() {
      return await fetch('url').then(r => r.json());
      }
      };

      16. 异步生成器函数


      使用async生成器函数结合yield


      async function* asyncGenerator(array) {
      for (const item of array) {
      yield await processItem(item);
      }
      }

      17. 使用顶级await


      在模块顶层使用await(需要特定的JavaScript环境支持)。


      // ECMAScript 2020引入顶级await特性, 部署时注意兼容性
      const config = await fetch('config-url').then(r => r.json());

      18. async/await与IIFE结合


      async函数与立即执行函数表达式(IIFE)结合。


      (async function() {
      const data = await fetch('url').then(r => r.json());
      console.log(data);
      })();

      19. 使用async/await优化递归调用


      优化递归函数。


      async function asyncRecursiveFunction(items) {
      if (items.length === 0) return 'done';
      const currentItem = items.shift();
      await delay(1000);
      console.log(currentItem);
      return asyncRecursiveFunction(items);
      }

      20. 在switch语句中使用await


      switch语句的每个case中使用await


      async function fetchDataBasedOnType(type) {
      switch (type) {
      case 'user':
      return await fetch('user-url').then(r => r.json());
      case 'post':
      return await fetch('post-url').then(r => r.json());
      default:
      throw new Error('Unknown type');
      }
      }

      作者:慕仲卿
      来源:juejin.cn/post/7311603506222956559
      收起阅读 »

      Shadcn UI 现代 UI 组件库

      web
      前言 不知道大家是否使用过 Shadcn UI,它在Github 上拥有了 35k star,它与大多数 UI 组件库(如 Ant desgin 和 Chakra UI)不同,一般组件库都是通过 npm 的方式给项目使用,代码都是存在 node_modules...
      继续阅读 »

      image.png


      前言


      不知道大家是否使用过 Shadcn UI,它在Github 上拥有了 35k star,它与大多数 UI 组件库(如 Ant desgin 和 Chakra UI)不同,一般组件库都是通过 npm 的方式给项目使用,代码都是存在 node_modules 中,而 Shadcn UI 可以将单个 UI 组件的源代码下载到项目源代码中(src 目录下),开发者可以自由的修改和使用想要的 UI 组件,它已经被一些知名的网站(vercel.combestofjs.org)等使用。那么它到底有什么优势呢? 一起来来探讨下。


      Shadcn UI 介绍


      Shadcn UI 实际上并不是组件库或 UI 框架。相反,它是可以根据文档“让我们复制并粘贴到应用程序中的可复用组件的集合”。它是由 vercel 的工程师Shadcn创建的,他还创建了一些知名的开源项目,如 TaxonomyNext.js for DrupalReflexjs


      Radix UI - 是一个无头 UI 库。也就是说,它有组件 API,但没有样式。Shadcn UI 建立在 Tailwind CSS 和 Radix UI 之上,目前支持 Next.js、Gatsby、Remix、Astro、Laravel 和 Vite,并且拥有与其他项目快速集成的能力——安装指南


      Shadcn UI 功能特点


      多主题和主题编辑器



      在 Shadcn UI 的官网上有一个主题编辑器,我们可以点击 Customize 按钮实时切换风格和主题颜色,设计完成后,我们只需要拷贝 css 主要变量到我们的程序中即可。 下图是需要拷贝的 css 颜色变量。



      颜色使用 hls 表示,主题变量分为背景色(background) 和 前景色(foreground),Shadcn UI 约定 css 变量省略 background,比如 --card 就是表示的是 card 组件的背景颜色。


      深色模式


      可以看到复制的 css 变量支持生成深色模式,如果你使用 react, 可以使用 next-themes,这个包来实现主题切换,当然也可以通过 js 在 html 上切换 dark 这个样式来实现。 除了 react 版,社区还自发实现了 vuesvelte 版本


      CLI


      除了手动从文档中复制组件代码到项目中,还可以使用 cli 来自动生成代码



      • 初始化配置


      npx shadcn-ui@latest init



      • 添加组件


      npx shadcn-ui@latest add


      按空格选择想要的组件,按回车就会下载选中的 UI 组件代码



      下载的源码在 components/ui 目录下,并且自动安装 Radix UI 对应的组件。


      丰富的组件库


      Shadcn UI 拥有丰富的组件,包括 常见的 Form、 Table、 Tab 等 40+ 组件。





      使用 Shadcn UI 创建登录表单


      接下来我们一起实战下,使用 Shadcn UI 创建登录表单, 由于 Shadcn UI 是一个纯 UI 组件,对于复杂的表单,我们还需要使用 react-hook-form 和 zod。


      首先下载 UI


      npx shadcn-ui@latest add form

      安装 react-hook-form 以及 zod 验证相关的包


      yarn add add react-hook-form zod @hookform/resolvers

      zod 用于格式验证


      下面代码是最基本的 Form 结构


      import {
      Form,
      FormControl,
      FormDescription,
      FormField,
      FormItem,
      FormLabel,
      FormMessage,
      } from "@/components/ui/form"
      import { Input } from "@/components/ui/input"

      <FormField
      control={form.control}
      name="username"
      render={({ field }) => (
      <FormItem>
      <FormLabel>Username</FormLabel>
      <FormControl>
      <Input placeholder="shadcn" {...field} />
      </FormControl>
      <FormDescription>This is your public display name.</FormDescription>
      <FormMessage />
      </FormItem>

      )}
      />


      • FormField 用于生成受控的表单字段

      • FormMessage 显示表单错误信息


      登录表单代码


      "use client"

      import { zodResolver } from "@hookform/resolvers/zod"
      import { useForm } from "react-hook-form"
      import * as z from "zod"

      import { Button } from "@/components/ui/button"
      import {
      Form,
      FormControl,
      FormField,
      FormItem,
      FormLabel,
      FormMessage,
      } from "@/components/ui/form"
      import { Input } from "@/components/ui/input"

      const formSchema = z.object({
      email: z.string().email({message:'邮箱格式不正确'}),
      password: z.string({required_error:'不能为空'}).min(6, {
      message: "密码必须大于6位",
      }),
      })

      export default function ProfileForm() {
      // 1. Define your form.
      const form = useForm<z.infer<typeof formSchema>>({
      resolver: zodResolver(formSchema),
      defaultValues: {
      email: "",
      },
      })

      // 2. Define a submit handler.
      function onSubmit(values: z.infer<typeof formSchema>) {
      // Do something with the form values.
      // ✅ This will be type-safe and validated.
      console.log(values)
      }

      return (
      <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 w-80 mx-auto mt-10">
      <FormField
      control={form.control}
      name="email"
      render={({ field }) =>
      (
      <FormItem>
      <FormLabel>邮箱</FormLabel>
      <FormControl>
      <Input placeholder="请输入邮箱" {...field} />
      </FormControl>
      <FormMessage />
      </FormItem>
      )}
      />
      <FormField
      control={form.control}
      name="password"
      render={({ field }) =>
      (
      <FormItem>
      <FormLabel>密码</FormLabel>
      <FormControl>
      <Input placeholder="请输入密码" {...field} />
      </FormControl>
      <FormMessage />
      </FormItem>
      )}
      />
      <Button type="submit">登录</Button>
      </form>
      </Form>

      )
      }


      展示效果



      小结


      与其他组件库相比,Shadcn UI 提供了几个好处。



      • 易用性:使用复制和粘贴或 CLI 安装方法可以轻松访问其组件.

      • 可访问性:Shadcn UI 的组件是完全可访问的,并符合 Web 内容可访问性指南 (WCAG) 标准,它支持屏幕阅读器、键盘导航和其他辅助设备。

      • 灵活和可扩展性:Shadcn UI 只会下载需要使用的组件在源码中,并且开发者可以灵活定制和修改。


      当然需要手动拷贝安装每一个组件可能是一件麻烦的事情,这也会导致源码量的增加,因此是否使用 Shadcn UI 还得开发者自行决定,总的来说 Shadcn UI,我还是非常看好,我将配合 next.js 在一些新项目中使用。


      作者:狂奔滴小马
      来源:juejin.cn/post/7301573649328668687
      收起阅读 »

      产品经理:为什么你做的地图比以前丝滑了许多?

      web
      从业多年第一次接触地图相关的需求,开发过程中产生了一些思考,遂记录下来,欢迎讨论Vue3 + 高德地图 JS API 2.0 + 高德地图 AMapUI组件库近两年前端大家是真的不好混,在职的人呢被极限压榨,待业的人呢投简历都是【未读不回】。照常理来说,地图相...
      继续阅读 »

      从业多年第一次接触地图相关的需求,开发过程中产生了一些思考,遂记录下来,欢迎讨论

      Vue3 + 高德地图 JS API 2.0 + 高德地图 AMapUI组件库

      近两年前端大家是真的不好混,在职的人呢被极限压榨,待业的人呢投简历都是【未读不回】。

      1.gif

      照常理来说,地图相关的需求都是由组内的地图大佬负责的,但眼瞅着公司里前端同学越来越少,这“泼天的富贵”终于有一天也落到了我头上。

      需求的内容倒是很简单:要在地图上绘制一些轨迹点和一条轨迹线,以及一个目标点KeyPoint,让使用者来审查轨迹线是否经过KeyPoint,以及系统中记录KeyPoint的信息是否正确。当轨迹未经过 或 KeyPoint信息不正确时,会再提供一些辅助点SubPoint供用户选择,替换掉KeyPoint。(轨迹点也属于一种SubPoint

      本着能CV就不手写的原则,我打开了项目代码(Vue2)寻找之前类似的地图需求,看看能不能套用一下然后快速下班,结果我看到了若干个大几千行的文件,以及这样的渲染效果(轨迹点上的箭头表示当前移动的方向):

      1.png

      2.png

      大哥喂,咱就是说,方向盘打不正的话要抓紧去修,上路是要出事故的 3.jpg

      得,言归正传,且不说那加起来几万行的代码我能不能捋顺喽,就是这个效果,干脆我还是用Vue3重新实现一下吧。

      别忘了,前端的老本行是什么


      业务的关注点

      开始之前,我们先思考一个问题:业务的关注点是什么?

      想明白了这个问题,我们在设计地图样式以及一些交互细节时,才能有更好的针对性。

      (让我看看有多少人是默认样式+内置组件一把梭的)

      image.png

      ok那既然涉及到了地图,归根结底我们的关注点无非是这三方面:

      • 线
      • 区域

      如果按照关注点的归属粗略的分为两类:外部添加的地图自身的

      当业务更关注外部添加的元素时(如maker、轨迹),随着地图缩放、地形改变、POI显隐,我们添加的元素是否始终有一个比较醒目的显示效果?

      当业务更关注地图自身的元素时(如兴趣点),对于POI的 pick 动作,是否贴合业务流程?是否足够智能与便捷?(可参考高德自己的效果)

      这里针对第一类推荐两个初始化地图的可选配置项:

      1. features:地图显示要素(查看效果
      2. mapStyle:地图主题(查看效果

      相信很多人可能都没关注过这两个配置项,而这两个东西组合起来使用,不仅能使你添加的外部元素始终处于一个高醒目的level,也可以与你项目本身的风格主题更搭,如何抉择,诸君自行思量。

      (浅浅吐槽一下,高德提供的功能和配置项非常丰富,但文档真的是一言难尽。。。一样的功能在不同的地方都有文档,有些内容还不一致)

      4.png

      选择画点的方法

      当你大概明白自己要做什么样的地图之后,让我们稍微进入一点正题:怎么选择合理的画点方法?

      高德提供了哪些画点的方法呢?

      • JS API
        1. 默认点标记Marker
        2. 圆形标记CircleMarker
        3. 灵活点标记ElasticMarker
        4. 海量标注LabelMarker:需要维护图层、维护避让等级、自定义样式实现起来比较麻烦
        5. 海量点标记MassMarker:无法显示文字label
        6. 点聚合
          • 按距离聚合MarkerCluster:需要维护权重
          • 按索引聚合IndexCluster:需要维护索引规则
      • JS API UI组件库
        1. 简单标注SimpleMarker
        2. 字体图标标注AwesomeMarker
        3. 矢量标注SvgMarker
        4. 海量点PointSimplifier:可使用Canvas

      至于像文本标记、折线、多边形等等一些通过某些黑科技实现类似点标记的方法(GPT说的),和Native端的画点方法,不在本文的讨论范围中。


      美国五星上将麦克阿瑟曾说过,一切抛开实际背景去讨论问题的行为都是耍流氓。

      画点的方法找到了很多,那我们要画什么样的点呢?

      1. 从数量上看

      动辄上万

      为什么我要先看数量呢,因为可自定义样式的画点方法很多,但是要支持大数量级渲染且性能良好,就把上边一多半方法给pass掉了。

      还剩下这些可供选择:海量标注LabelMarker海量点标记MassMarker按距离聚合MarkerCluster按索引聚合IndexCluster海量点PointSimplifier

      2. 从样式上看

      需要自定义。从上边的截图中可以看出,点的形状为圆形,黑边黄底,中心有个箭头,且整体随着当前运动方向有一个rotate deg

      上述画点方法至少都支持(图片 或 HTML String 或 CSS Style)中的一种方式,而这三种方式理论上也都能实现我们想要的效果,所以下一个。

      3. 特性

      test.gif

      虽然我们需要关注轨迹点,但并不是所有状态下都需要。比如在地图的缩放等级很小时(看到的是省、国家级别),并不需要把每一个轨迹点都展示出来。所以可以看到,之前的实现效果中,放大缩小都会重新适应尺寸,并且临近的点有自动合并的效果。

      海量标注LabelMarker海量点标记MassMarker退出了游戏,他俩是全量绘制并且没有外部接入的话点是始终展示的。

      至此,只有按距离聚合MarkerCluster按索引聚合IndexCluster海量点PointSimplifier三者进入了决赛圈。

      现在来综合对比一下这三种方法:

      方法1w+点渲染性能自定义样式合并逻辑
      按距离聚合MarkerCluster渲染迅速,操作不卡顿HTML String或图片距离+权重就近合并
      按索引聚合IndexCluster渲染迅速,操作不卡顿HTML String或图片距离+索引分组合并
      海量点PointSimplifier渲染迅速,操作不卡顿Canvas或图片TopN

      渲染方面在1w+点的竞赛中大家表现得都不错,官网示例中心可以看到,这里不再赘述。

      自定义样式则有三种途径,图片、HTML字符串和新出现的Canvas。图片和Canvas比较简单,我们先讲一讲这个HTML字符串,也就是原生的HTML。

      假如你用了某个现代化的前端框架在开发你的系统,用到了高德地图,并且想画一些漂漂亮亮的点在你需要标注的地方。在翻阅了文档之后,发现似乎直接传入HTML字符串这种方法是最快的,于是你开开心心的输入了一个

      My Marker
      试试水,接着,保存、等待hot reload,并把期待的目光投向了屏幕...

      353ad3a27c3ac95c86c30e66f1b4f15.png

      好家伙,这小Marker不仔细看,还真有点找不到呢...你决定继续添加一些样式

      2M2we.gif

      之后代码可能逐渐变成了这样...

      微信图片_20240514153858.png

      你:

      WzgGg.png

      把手指从ctrl C V三个键拿下来之后,你陷入了沉思:我能不能用XXX UI组件库来自定义Marker?

      答案当然是肯定的~下面请允许我用Vue@3.3.4 + ant-design-vue@3.2.20来做个示范~~

      首先,他接收的是HTML字符串,所以直接传进去一个vue组件肯定是不行的

      import MyComponent from './MyComponent.vue'

      // 以普通点标记举例
      new Marker({
      content: MyComponent,
      // ...other configs
      })

      // not work

      所以我们要做的就是把MyComponent给转成原生HTML,最简单的办法当然就是Vue实例的mount()API啦:

      MyComponent作为根组件创建一个新的Vue实例

      import {createApp} from 'vue'
      import MyComponent from './MyComponent.vue'

      const app = craeteApp(MyComponent)

      将实例挂载到一个div上,得到原生的HTML

      const div = document.createElement("div")
      app.mount(div)

      console.log('div: ', div)

      打印一下:

      223.png

      使用:

      // 以普通点标记举例
      const marker = new Marker({
      content: div,
      // ...other configs
      });

      效果图就不放啦,有几个注意的点要提一下:

      1. 最重要的放在最前边:如果你的点在整个页面的生命周期内仅会绘制一次,那你可以跳过这一条。否则一定要记得app.unmount()。一种比较好的实践是,把点数据画点的方法移除点的方法写进一个hook里。
      // example
      import { createGlobalState } from "@vueuse/core";
      import { ref, createApp } from "vue";
      import Map from './Map.js' // 地图实例
      import MyComponent from "./MyComponent.vue";

      export const useCustomPoints = createGlobalState(() => {
      const pointData = ref([]);
      const removePointsCb = [];

      const setPoint = () => {
      const data = pointData.value.map(point => {
      const div = document.createElement("div");
      const app = createApp(MyComponent);
      app.mount(div);

      removePointsCb.push(() => app.unmount()) // 清除图标实例的回调

      // 以普通点标记举例
      const marker = new Map.Constructors.Marker({
      map: Map,
      content: div,
      // ...other config
      });

      return marker
      })

      Map.add(data); // 将点添加到地图上

      removePointsCb.push(() => Map.remove(data)); // 移除点的回调
      }

      const removePoints = () => {
      while (removePointsCb.length) {
      removePointsCb.pop()();
      }
      };

      return {
      pointData,
      setPoint,
      removePoint
      }
      })
      1. 可以通过createApp的第二个参数传递props进去,这些props是响应式的
      2. 新创建的Vue实例与你项目自身的实例不共享全局的配置,比如路由组件Store等,需要单独配置
      3. Vue2以及其他的一些框架,实现思路类似

      好了,言归正传。

      看起来似乎三种方法都可以实现需求,但是仔细翻看点聚合方法的文档,发现使用图片自定义点时没有提供旋转的配置,也就是说我们可能需要准备n张图片(取决于你想实现角度渲染的精确度),不,这太不优雅了。而如果使用原生HTML去自定义,要么接受丑炸的效果(纯手工css),要么面临着卡顿的风险(大量的app实例)

      没办法,只好被(xin)迫(ran)接受用海量点PointSimplifier的Canvas去做了~毕竟能用Canvas画就约等于能画一切嘛~~

      抱着视死如归的心情去翻了一下JS API UI组件库的海量点PointSimplifiercanvas绘制function文档,发现了一句了不得的话:

      微信图片_20240514173923.png

      划重点:通常只是描绘路径尽量不要fill或者stroke引擎自己一次性

      翻译:该函数通常只是描绘路径,但是也能描绘形状。尽量不要fill或者stroke,除非你能搞明白我们的描绘机制。所有点的路径描绘完成后,引擎自己会在尾部调用fill以及stroke,一次性绘出所有路径,所以你要注意尾部的这次操作,避免冲突。

      三个字总结海量点PointSimplifier的描绘机制就是:连笔画

      不是每个点都创建一个新的Canvas画布,绘制完成后立即渲染;而是所有的点都共用一个Canvas画布,以当次你能绘制的区域坐标作为参数,重复绘制n(点的数量)次,最后一把全渲染出来。

      微信图片_20240515171523.png

      微信图片_20240516093003.png

      明白了这个,我们在书写function的逻辑的时候,只要注意保证每次绘制开始、结束时笔触的落点和绘制上下文状态即可。绘制一个有旋转角度的、中间有箭头的圆形(圆形的背景色是通过海量点PointSimplifier的lineStyle配置的),示例代码如下:

      由于叠加了变换,处理状态时偷懒使用了save()、restore()

      renderOptions: {
      // 这里使用了样式分组引擎:https://lbs.amap.com/demo/amap-ui/demos/amap-ui-pointsimplifier/group-style-render
      // 以点的旋转角作为组id入参,方便操作
      // 无需分组时,renderOptions.pointStyle.content = renderOptions.groupStyleOptions.pointStyle.content 逻辑一致
      groupStyleOptions: function (gid) {
      return {
      pointStyle: {
      content: function (ctx, x, y, width, height) {
      // 存了一个坐标,画箭头的时候用
      const startX = x + width / 2;
      const startY = y + height / 4;

      // 移动到画布的最右侧、中间位置
      ctx.moveTo(x + width, y + height / 2);

      // 画圆
      ctx.arc(
      x + width / 2,
      y + height / 2,
      width / 2,
      0,
      Math.PI * 2,
      true
      );

      // 变换前保存一下状态
      ctx.save();

      // 以圆心为旋转的中心点
      ctx.translate(x + width / 2, y + height / 2);
      // 按照轨迹方向旋转
      ctx.rotate((Math.PI / 180) * gid);
      // 重置中心点
      ctx.translate(-(x + width / 2), -(y + height / 2));

      // 画箭头
      ctx.moveTo(startX, startY);
      ctx.lineTo(x + width / 4, y + height / 2);
      ctx.moveTo(startX, startY);
      ctx.lineTo(startX, y + (height * 3) / 4);
      ctx.moveTo(startX, startY);
      ctx.lineTo(x + (width * 3) / 4, y + height / 2);

      // 由于箭头需要在旋转的状态下绘制,所以在箭头绘制完成后再恢复状态
      ctx.restore();
      },
      },
      };
      },
      }

      来一个无旋转时的笔触顺序动图,我尽力了6j4l.png

      test.gif

      画完之后,看一下对比效果:




      OK,点画出来了。

      上边特性中有提到:当我们距离很远时,就不需要再关注某个具体的轨迹点。所以可以再进一步优化,当地图的缩放等级zoom小于某个阈值时,清空point:

      import {computed, watch, ref} from 'vue'
      import {Map, PointSimplifierIns} from 'Map.js' // 地图实例、海量点实例

      const zoom = ref(null);

      const showPoint = computed(() => zoom.value > 10);

      const pointData = ref([ /* ...赋值逻辑省略 */]);

      Map.on("zoomchange",
      debounce(() => {
      zoom.value = Map.getZoom();
      }, 200)
      );

      watch(showPoint, (show) => {
      PointSimplifierIns.setData(show ? pointData.value : []);
      })

      效果如下:

      test.gif

      控制显示隐藏没有用自带的show()hide()方法,而是选择直接重设数据源,是因为:海量点PointSimplifiershow状态下时对地图进行缩放,会自动重绘适应尺寸;hide状态下则不会。从show变为hide时,会保存当前zoom下点的尺寸,供下次hideshow时用。如果地图缩放的太快,当前的zoom与上次保存尺寸时的zoom跨度太大,可能会导致点位不匹配现象。


      选择画轨迹的方法

      画线的选择过程就简单了很多,之前需求中是用折线Polyline实现的,画出来的效果总感觉差点意思,所以就去翻了翻高德的文档,共找到常规画线方法3种:

      • JS API
        1. 折线Polyline
        2. 贝塞尔曲线BesizerCurve
      • JS API UI组件库
        1. 轨迹展示PathSimplifier

      基本上毫无疑问了嘛~我们本身就是要画轨迹,还有什么好选的~~ 必须用轨迹展示

      不过这里还是分享一些对三种方法实际体验之后的感受:

      1. 折线Polyline:无法识别线上的点。如果轨迹数据没有经过噪点清除,画出来之后在细节处会有比较严重的锯齿。不过整体上感觉,倒也不是不能用~
      2. 贝塞尔曲线BesizerCurve:无法识别线上的点。但理论上是唯一可以绘制出完全符合真实运动轨迹的、贴合地图路线的方法了,代价也是相当的大——至少要在原本轨迹点的基础上额外维护n-1个控制点,放弃~~
      3. 轨迹展示PathSimplifier:性能好,相同数据量下的显示效果要比折线画出来的平滑许多。以及来自官网的优点罗列:
        • 使用Simplify.js进行点的简化处理,提高性能
        • 支持识别线上的点做信息展示
        • 内置巡航器
        • 样式配置更加丰富

      实现过程比较简单,照着文档撸就行,可以对比下折线和轨迹展示两种方式,在拐角细节处的差异:

      折线


      轨迹展示


      小tips: 适当增加线宽lineWidth可以有效的缓解锯齿现象


      Loading的区域与时机

      当我第一次打开上文提到的老版本地图页面时,除了渲染效果不够理想外,最大的一个感受就是:Loading太长

      不是想像中那样常规的:打开页面,给一个满屏Spin等待加载各种数据、等待绘制点、线的动作,所有准备工作完成后,取消Spin允许用户开始操作。

      咱就是说,像这样的交互逻辑,其实也没啥问题。毕竟谁还没个业务繁忙的时候,最简单最原始最暴力的满屏Spin虽然在体验上不尽如人意,但我觉得是符合上线标准的。

      但您猜我看到了什么?

      微信图片_20240520171349.png

      Form、Map、Action Bar三个区域各自一个小Spin,整体有个大Spin,可以透过大Spin的透明遮罩层看到下面的小spin们反复交替进行,以及大Spin自己也时不时的闪现一下...

      7D91.gif

      Spin为什么会闪现?回到需求当中来:

      地图上点、线的绘制依赖了多个数据源

      • 轨迹点、轨迹线数据源
      • KeyPoint数据源
      • SubPoint数据源
        • Type 1
        • Type 2

      这些接口一部分是并发请求,但也有个别的接口请求参数依赖于其他接口的返回值

      以及,使用高德地图提供的API绘制点、线时,也共用了接口请求时的Spin。

      还有诸多类似这样的代码:

      setTimeout(() => {
      loading = false
      }, 2000)

      对渲染流程管理混乱、对数据流向不了解、对自己代码不自信,故意延迟loading的结束时机,防止用户过早操作导致报错


      const interval = setInterval(() => {
      if(conditon) {
      clearInterval(interval);
      loading = false
      }
      }, 1000)

      依赖第三方的内容加载,或将多个小loading合并为一个大loading


      最终的结果就是让人一整个loading住...

      pj7dW.png

      而我做了哪些改变

      首先,将单个loading覆盖的区域尽可能的缩小

      举个例子,上边提到的Action Bar,假设里边既有展示KeyPoint信息的列表,又有展示所有SubPoint信息的列表。在之前的处理方案中,Action Bar区域只有一个整体的Spin,所以整个区域loading的流程大概是:

      %%{init: { 'theme': 'base', 'themeVariables': {
      'cScale0': '#996666', 'cScaleLabel0': '#ffffff',
      'cScale1': '#996633','cScaleLabel1': '#ffffff',
      'cScale2': '#999999', 'cScaleLabel2': '#ffffff'
      }}}%%

      timeline
      title Loading 状态

      section 阶段一
      show : request for KeyPoint data
      hide : request success

      section 阶段二
      show : request for SubPoint type1 data
      hide : request success

      section 阶段三
      show : request for SubPoint type2 data
      hide : request success

      而我则是把每个数据源对应的列表都单独分配了一个loading组件

      聪明的看官老爷可能会问,同时存在多个Spin,不也很奇怪吗?

      test.gif

      所以我选择了骨架屏Skeleton作为loading组件:

      test.gif

      受gif图的帧率影响,实际效果还是很丝滑的。(但使用骨架屏时也有一个注意的点:骨架屏的占位高度需要配置段落占位图行数来调整,避免loading结束时真实的渲染内容与骨架屏高度相差太大产生视差)

      然后,在Map区域用其他形式的提示代替传统的loading

      与上边类似,Map区域不仅同时用到了KeyPointSubPoint数据源,而且在绘制点、线时也有loading。并且也是一个整体的Spin,你应该能想象出每次数据初始化时,Map上闪来闪去的Spin。。。

      地图本身,是高德提供出来可以开箱即用的组件,我们所添加的点、线只是附加属性,并不应该使用整体的Spin遮罩阻止用户使用地图的其他功能。在某些附加属性成功添加之前,用户只需要知道与之相关的功能是不可用状态即可。

      我的方案是:图例化提供一个loading-box,里边展示了每个数据源的加载状态

      test.gif

      为了不遮挡地图,loading-box不是始终展示的,基础显示逻辑是:

      1. watch监听loadings 数组
      2. 只要有一个数据源loading中,则显示。
      3. 全部数据源都不在loading中,则debounce n秒后隐藏。

      显示动作是实时的,只要有一个数据源在loading中,就应该立刻让用户感知到。

      而隐藏动作如果也是实时的,loading-box的显隐切换会比较频繁,显得很突兀。

      • 如果使用setTimeout做延时,期望是发出hide指令n秒后执行,但无法保证n秒后没有新的loading正在进行,导致显隐切换逻辑紊乱。
      • 如果使用throttle做延时,导致的问题与setTimeout相同,只是发生概率会小一些。
      • 相比之下debounce最适合做这个场景的解决方案。

      再结合上边提到的hook写法,把loading状态也放进去,方便loading-box使用:

      // example
      import { createGlobalState } from "@vueuse/core";
      import { ref, createApp } from "vue";

      export const useCustomPoints = createGlobalState(() => {
      const pointData = ref([]);
      const pointLoading = ref(false);
      const removePointsCb = [];

      const getPoint = async () => {
      pointLoading.value = true;
      const data = await requestPointData();
      pointLoading.value = false;

      pointData.value = data;
      }

      const setPoint = () => {
      // ...
      }

      const removePoint = () => {
      // ...
      };

      return {
      pointData,
      pointLoading,
      getPoint,
      setPoint,
      removePoint
      }
      })
      // loading-box.vue

      <script setup>
      import { watch } from 'vue';
      import { useCustomPoints } from 'useCustomPoints.js';

      const { pointLoading } = useCustomPoints();

      watch([pointLoading, /* and other loadings */], () => {
      // do loading-box show/hide logic
      })
      script>

      应用了上述loading相关的优化后,虽然跟核心业务逻辑相关的代码改动几乎为0,但用户的体验却有相当大的提升,究其原因:

      在老版本的实现中,因为全屏Spin的存在,任何一项页面准备工作完成前,页面都无可交互区域;拆分loading后,把一大部分无可交互区域的时间变成了局部可交互区域的时间,甚至在Map模块替换了loading的形式,完全避免了Spin遮罩层这种阻隔用户的效果。加上Spin动画本身的耗时、显示/隐藏Spin的耗时,积少成多,产生质变。

      image.png

      可以看到,在局部loading耗时完全一样的情况下,老版本中:

      无可交互区域时间 = 全屏Spin时间 = 局部loading的最大时间

      而在新版本中:

      无可交互区域时间 = 几个all loading片段的时间之和

      而这,也是一些复杂应用做体验优化的思路之一。


      结语

      okok,先写到这,毕竟马上就要下班了

      1b006c53e15945a09253df289b6192cc~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.awebp

      没什么高大上理论也没什么八股文,只是一个从业多年一事无成小前端在重构需求时的一些感想~~

      还是那句话,欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~


      彩蛋

      文章标题来自需求上线后,产品经理的真实评价

      image.png


      作者:Elecat
      来源:juejin.cn/post/7371633297153687606
      收起阅读 »

      2024 前端趋势:全栈也许已经是必选项

      web
      过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。 React 与 Vue 生态对比 首先,我们来看看 React 与 Vue 生态的星趋势对比: 上图中,React 整个生态的星星数远超于 Vue,第十名都要比 Vue 第一名的多...
      继续阅读 »

      过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。


      React 与 Vue 生态对比


      首先,我们来看看 React 与 Vue 生态的星趋势对比:


      截屏2024-02-29 10.05.39转存失败,建议直接上传图片文件


      上图中,React 整个生态的星星数远超于 Vue,第十名都要比 Vue 第一名的多。我们将其做一个分类:


      排名ReactVue
      1UI全栈
      2白板演示文稿
      3全栈后台管理系统
      4状态管理hook
      5后台管理系统UI
      6文档文档
      7全栈框架集成UI
      8全栈框架UI框架
      9后台管理系统UI
      10无服务栈状态管理

      可以看到 React 这边的生态链基本成熟,几乎每一个分类都有一个上榜的库,不再像 Vue 那样还在卷 UI 框架。


      在全栈方面,Vue 的首位就是全栈 Nuxt。


      React 的 Next.js 虽然不在首位,但是服务端/全栈相关的内容就占了 4 个,其中包含第 10 名的无服务栈。另外值得注意的是,React 这边还有服务端组件的概念。Shadcn/ui 能占到第一位,因为它基于无头 UI Radix 实现的,在服务端组件也能运用。所以,服务端/全栈在 React 中占的比重相当大的。


      这样看来,前端往服务端进发已经成为一个必然趋势。


      htmx 框架的倒退


      再看看框架这边,htmx 在星趋势里,排行第二位,2023增长的星星数为 15.6K,与第一位的 React 颇为相近。


      而 htmx 也是今年讨论度最高的。


      在我经历过前后端不分离的阶段中,使用 jsp 生成前端页面,js 更多是页面炫技的工具。然后在 jQuery + Ajax 得到广泛应用之后,才真正有前后端分离的项目。


      htmx 的出现,不了解的人,可能觉得是倒退到 Java + jQuery + Ajax 的前后端分离状态。但是,写过例子之后,我发现,它其实是倒退到了前后端不分离的阶段。


      用 java 也好,世界上最好的 php 也好,或者用现在的 nodejs 服务,都能接入 htmx。你只要在服务端返回 html 即可。


      /** nodejs fastity 写的一个例子 **/
      import fastify from 'fastify'
      import fastifyHtml from 'fastify-html'
      import formbody from '@fastify/formbody';

      const app = fastify()
      await app.register(fastifyHtml)
      await app.register(formbody);
      // 省略首页引入 htmx

      // 首页的模板,提供一个按钮,点击后请求 html,然后将请求返回的内容渲染到 parent-div 中
      app.get('/', async (req, reply) => {
      const name = req.query.name || 'World'
      return reply.html`

      Hello ${name}


      `
      , reply
      })

      // 请求返回 html
      app.post('/clicked', (req, reply) => {
      reply.html`

      Clicked!

      `
      ;
      })

      await app.listen({ port: 3000 })

      也许大家会觉得离谱,但是很显然,事情已经开始发生了变化,后端也来抢前端饭碗了。


      截屏2024-02-29 10.32.24.png


      htmx 在 github 上已经有不少跟随者,能搜出前端代码已有不少,前三就有基于 Python 语言的 Django 服务端框架。


      jQuery 见势头不错,今年也更新了 4.0 的 beta 版本,对现代浏览器提供了更好的支持。这一切似乎为旧架构重回大众视野做好了准备。


      企业角度


      站在企业角度来看,一个人把前后端都干了不是更好吗?


      的确如此。前后端一把撸更符合企业的利益。国外的小公司更以全栈作为首选项。


      也许有人觉得国情不同,但是在我接触的前端群里,这两年都有人在群里说他们公司前后端分离的情况。


      还有的人还喜欢大厂那一套,注意分工合作,但是其实大厂里遗留项目也不少,有的甚至是 php;还有新的实验项目,如果能投入最少人力,快速试错,这种全栈的框架自然也是最优选择。


      我并不是说,前后端分离不值得。但是目前已经进入 AI 赛道,企业对后台系统的开发,并不愿意投入更多了。能用就行已经成为当前企业的目标,自然我们也应该跟着变化。


      全栈破局


      再说说前端已死的论调。我恰恰觉得这是最好做改变的时机。


      在浏览器对新技术支持稳定,UI 框架趋同,UI 组件库稳定之后,前端不再需要为浏览器不兼容素手无策了,不再需要苦哈哈地为1个像素争辩不停了,也不再需要为产品莫名其妙的交互焦头烂额了。


      这并不意味着前端已死,反而可能我们某个阶段的任务完成了,后面有更重要的任务交给我们。也许,全栈就是一个破局。


      在云服务/云原生如此普遍的情况下,语言不再是企业开发考虑的主要因素,这也为 nodejs 全栈铺平了道路。


      前端一直拣最苦最脏的话来做,从 UI 中拿到了切图的工作,然后接手了浏览器兼容的活,后来又从后端拿到了渲染页面的工作。


      那我们为何不再进一步,主动把 API 开发的工作也拿过来?


      作者:陈佬昔没带相机
      来源:juejin.cn/post/7340603873604599843
      收起阅读 »

      8个小而美的前端库

      web
      前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。 2024年推荐以下小而美的库。 radash 实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数)...
      继续阅读 »

      前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。


      2024年推荐以下小而美的库。


      radash


      实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数),源码可读性高,如果不想安装它,大部分函数可以直接复制到本地使用。



      use-debounce


      React Hook Debouce 库,让你不再为使用防抖烦恼。库的特点:体积小 < 1 Kb、与 underscore / lodash impl 兼容 - 一次学习,随处使用、服务器渲染友好。



      timeago.js


      格式化日期时间库,比如:“3 hours ago”,支持多语言,仅 2Kb 大小。同时提供了 React 版本 timeago-react。


      timeage.format(1544666010224, 'zh_CN') // 输出 “5 年前”
      timeage.format(Date.now() - 1000, 'zh_CN') // 输出 “刚刚”
      timeage.format(Date.now() - 1000 * 60 * 5, 'zh_CN') // 输出 “5 分钟前”

      react-use


      实用 Hook 大合集 - 内容丰富,从跟踪电池状态和地理位置,到设置收藏夹、防抖和播放视频,无所不包。



      dayjs


      Day.js 是一个简约的 JavaScript 库,仅 2 Kb 大小。它可以使用基本兼容 Moment.js,为你提供日期的解析、处理和显示,支持多语言能力。



      filesize


      filesize.js 提供了一种简单方法,便于从数字(浮点数或整数)或字符串转换成可读性高的文件大小。


      import {filesize} from "filesize";
      filesize(265318, {standard: "jedec"}); // "259.1 KB"
      driver.js:driver.js 是一款用原生 js 实现的页面引导库,上手非常简单,体积在 gzip 压缩下仅仅 5kb。

      driver.js


      driver.js 是一款用原生 js 实现的页面引导库,上手非常简单,体积在 gzip 压缩下仅仅 5kb。



      @formkit/drag-and-drop


      FormKit DnD 是一个小型库,它简单、灵活、与框架无关,压缩后只有 4Kb 左右,设计理念为数据优先。



      小结


      前端小而美的库使用起来一般都比较顺手,欢迎在评论区推荐你们开发中的使用小而美的库。


      作者:晓得迷路了
      来源:juejin.cn/post/7350140676615798824
      收起阅读 »

      我为展开收起功能做了动画,被老板称赞!

      web
      需求简介 这几天接了个新项目,需要实现下图中左侧边栏的菜单切换。这种功能其实就是一个折叠面板,实现方式多种多样。 实现上面的功能,无非就是一个v-show的事儿,但没有过渡,会显得非常生硬。想添加一些过渡效果, 最简单的就是使用element ui、或者a...
      继续阅读 »

      需求简介


      这几天接了个新项目,需要实现下图中左侧边栏的菜单切换。这种功能其实就是一个折叠面板,实现方式多种多样。



      实现上面的功能,无非就是一个v-show的事儿,但没有过渡,会显得非常生硬。想添加一些过渡效果,



      最简单的就是使用element ui、或者ant的折叠面板组件了。但可惜的是,我们的项目不能使用任何第三方组件库。



      为了做好产品,我还是略施拳脚,实现了一个简单且丝滑的过渡效果:



      老板看后,觉得我的细节处理的很好,给我一顿画饼,承诺只要我好好坚持,一定可以等到升职加薪!当然,我胃口小,老板的饼消化不了。我还是分享一下自己在不借助第三方组件的情况下,如何快速的实现这样一个效果。


      技术实现方案


      业务分析


      仔细观察需求,我们可以分析出其实动画主要是两个部分:一级标题的箭头旋转二级标题区域的折叠展开



      我们先实现一下基本的html结构:


      <template>
      <div class="nav-bar-content">

      <div class="header-wrap" @click="open = !open">
      <span class="text">自动化需求计算条件输如</span>
      <span class="arrow">
      >
      </span>
      </div>

      <div v-show="open" class="content">
      <p>算法及跃变计算条件</p>
      <p>空间品质判断条件</p>
      <p>需求自动计算条件</p>
      <p>通风系统</p>
      </div>

      </div>

      </template>

      <script setup>
      const open = ref(false);
      </script>


      上述代码非常简单,点击一级标题时,更改open的值,从而实现二级标题的内容区域展示与隐藏。


      箭头旋转动画



      实现箭头旋转动画其实非常容易,我们只要在红色面板展开时,给箭头添加一个新的类名,在这个类名中做一些动画处理即可。


      <template>
      <div class="header-wrap" @click="open = !open">
      <span class="text">自动化需求计算条件输如</span>
      <span class="arrow flex-be-ce" :class="{ open: open }">
      >
      </span>
      </div>

      </template>
      <style lang="less" scoped>
      .arrow {
      width: 16px;
      height: 16px;
      cursor: pointer;
      margin-left: 1px;
      transition: transform 0.2s ease;
      }
      .open {
      transform: rotate(90deg);
      transition: transform 0.2s ease;
      }
      </style>


      上述的代码通过 CSS 的 transform 属性和动态绑定open类名实现了箭头的旋转效果。



      注意:arrow也需要定义过渡效果



      折叠区域动画效果


      要实现折叠区域的动画效果,大致思路和上面一样。


      使用vue的transition组件实现


      借助vue的transition组件,我们可以实现折叠区域进入(v-show='true')和消失(v-show='fasle')的动画。一种可行的动画方案就是让面板进入前位置在y轴-100%的位置,进入后处于正常位置。



      <template>
      <div class="nav-bar-content">

      <div class="header-wrap" @click="open = !open">
      <span class="text">自动化需求计算条件输如</span>
      <span class="arrow" :class="{ open: open }">
      >
      </span>
      </div>

      <div class="content-wrap">
      <Transition>
      <div v-show="open" class="content">
      <p>算法及跃变计算条件</p>
      <p>空间品质判断条件</p>
      <p>需求自动计算条件</p>
      <p>通风系统</p>
      </div>
      </Transition>
      </div>

      </div>

      </template>

      <script setup>

      const open = ref(false);
      </script>


      <style lang="less" scoped>
      .v-enter-active,
      .v-leave-active {
      transition: transform 0.5s ease;
      }
      .v-enter-from,
      .v-leave-to {
      transform: translateY(-100%);
      }
      </style>


      上述效果有一点瑕疵,就是出现位置把一级标题盖住了,我们稍微修改下


      <div class="content-wrap">
      <Transition>
      <div v-show="open" class="content">
      <p>算法及跃变计算条件</p>
      <p>空间品质判断条件</p>
      <p>需求自动计算条件</p>
      <p>通风系统</p>
      </div>
      </Transition>

      </div>

      .content-wrap {
      overflow: hidden;
      }


      使用动态类名的方式实现


      效果好很多!但这种效果和第三方组件库的效果不太一致,我们以element的折叠面板效果为例:



      我们可以发现,它的这种动画,是折叠面板的高度从0逐渐增高的一个过程。所以最简单的就是,如果我们知道折叠面板的高度,一个类名就可以搞定!


      <template>
      <div class="nav-bar-content">

      <div class="header-wrap" @click="open = !open">
      <span class="text">自动化需求计算条件输如</span>
      <span class="arrow flex-be-ce" :class="{ open: open }">
      >
      </span>
      </div>

      <div class="content-wrap" :style="{ height: open ? '300px' : 0 }">
      <div class="content">
      <p>算法及跃变计算条件</p>
      <p>空间品质判断条件</p>
      <p>需求自动计算条件</p>
      <p>通风系统</p>
      </div>
      </div>

      </div>

      </template>

      <script setup>
      const open = ref(false);
      </script>


      <style lang="less" scoped>
      .content-wrap {
      height: 0;
      transition: height 0.5s ease;
      }
      </style>



      如果这个折叠面板的内容通过父组件传递,高度是动态的,我们只需要使用js计算这里的高度即可:


      <template>
      <div class="nav-bar-content">

      <div class="header-wrap" @click="open = !open">
      <span class="text">自动化需求计算条件输如</span>
      <span class="arrow flex-be-ce" :class="{ open: open }">
      >
      </span>
      </div>

      <div class="content-wrap" :style="{ height: open ? '300px' : 0 }">
      <div class="content" ref="contentRef">
      <slot></slot>
      </div>
      </div>

      </div>

      </template>

      <script setup>
      const open = ref(false);
      const contentRef = ref();
      const height = ref(0);
      onMounted(() => {
      height.value = contentRef.value.offsetHeight + 'px';
      });
      </script>


      <style lang="less" scoped>
      .content-wrap {
      height: 0;
      transition: height 0.5s ease;
      }
      </style>


      这样,我们就通过几行代码就实现了一个非常简单的折叠面板手风琴效果!



      总结


      要想实现一个折叠面板的效果,最简单的还是直接使用第三方组件库,但是如果项目不能使用其他组件库的话,手写一个也是非常简单的!也希望大家能在评论区给出更好的实现方式,供大家学习!


      作者:石小石Orz
      来源:juejin.cn/post/7369029201579278351
      收起阅读 »

      产品经理:实现一个微信输入框

      web
      近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。 初期认为这应该改动不大,就是把input换...
      继续阅读 »


      近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。


      初期认为这应该改动不大,就是把input换成textarea吧。但是实际开发过程发现并没有这么简单,本文仅作为开发过程的记录,因为是基于uniapp开发,相关实现代码都是基于uniapp


      简单分析我们大概需要实现以下几个功能点:



      • 默认单行输入

      • 可多行输入,但有最大行数限制

      • 超过限制行术后内容在内部滚动

      • 支持回车发送内容

      • 支持常见组合键在输入框内换行输入

      • 多行输入时高度自适应 & 页面整体自适应


      单行输入


      默认单行输入比较简单直接使用input输入框即可,使用textarea的时候目前的实现方式是通过设置行内样式的高度控制,如我们的行内高度是36px,那么就设置其高度为36px。为什么要通过这种方式设置呢?因为要考虑后续多行输入超出最大行数的限制,需要通过高度来控制textarea的最大高度。


      <textarea style="{ height: 36px }" />


      多行输入


      多行输入核心要注意的就是控制元素的高度,因为不能随着用户的输入一直增加高度,我们需要设置一个最大的行数限制,超出限制后就不再增加高度,内容可以继续输入,可以在输入框内上下滚动查看内容。


      这里需要借助于uniapp内置在textarea@linechange事件,输入框行数变化时调用并回传高度和行数。如果不使用uniapp则需要对输入文字的长度及当前行高计算出对应的行数,这种情况还需要考虑单行文本没有满一行且换行的情况。


      代码如下,在linechange事件中获取到最新的高度设置为textarea的高度,当超出最大的行数限制后则不处理。


      linechange(event) {
      const { height, lineCount } = event.detail
      if (lineCount < maxLine) {
      this.textareaHeight = height
      }
      }

      这是正常的输入,还有一种情况是用户直接粘贴内容输入的场景,这种时候不会触发@linechange事件,需要手动处理,根据粘贴文本后的textarea的滚动高度进行计算出对应的行数,如超出限制行数则设置为最大高度,否则就设置为实际的行数所对应的高度。代码如下:


      const paddingTop = parseInt(getComputedStyle(textarea).paddingTop);
      const paddingBottom = parseInt(getComputedStyle(textarea).paddingBottom);
      const textHeight = textarea.scrollHeight - paddingTop - paddingBottom;
      const numberOfLines = Math.floor(textHeight / lineHeight);

      if (numberOfLines > 1 && this.lineCount === 1) {
      const lineCount = numberOfLines < maxLine ? numberOfLines : maxLine
      this.textareaHeight = lineCount * lineHeight
      }

      键盘发送内容


      正常我们使用电脑聊天时发送内容都是使用回车键发送内容,使用ctrlshiftalt等和回车键的组合键将输入框的文本进行换行处理。所以接下来要实现的就是对键盘事件的监听,基于事件进行发送内容和内容换行输入处理。


      首先是事件的监听,uniapp不支持keydown的事件监听,所以这里使用了原生JS做监听处理,为了避免重复监听,对每次开始监听前先进行移除事件的监听,代码如下:


      this.$refs.textarea.$el.removeEventListener('keydown', this.textareaKeydownHandle)
      this.$refs.textarea.$el.addEventListener('keydown', this.textareaKeydownHandle)

      然后是对textareaKeydownHandle方法的实现,这里需要注意的是组合键对内容换行的处理,需要获取到当前光标的位置,使用textarea.selectionStart可获取,基于光标位置增加一个换行\n的输入即可实现换行,核心代码如下:


      const cursorPosition = textarea.selectionStart;
      if(
      (e.keyCode == 13 && e.ctrlKey) ||
      (e.keyCode == 13 && e.metaKey) ||
      (e.keyCode == 13 && e.shiftKey) ||
      (e.keyCode == 13 && e.altKey)
      ){
      // 换行
      this.content = `${this.content.substring(0, cursorPosition)}\n${this.content.substring(cursorPosition)}`
      }else if(e.keyCode == 13){
      // 发送
      this.onSend();
      e.preventDefault();
      }

      高度自适应


      当多行输入内容时输入框所占据的高度增加,导致页面实际内容区域的高度减小,如果不进行动态处理会导致实际内容会被遮挡。如下图所示,红色区域就是需要动态处理的高度。



      主要需要处理的场景就是输入内容行数变化的时候和用户粘贴文本的时候,这两种情况都会基于当前的可视行数计算输入框的高度,那么内容区域的高度就好计算了,使用整个窗口的高度减去输入框的高度和其他固定的高度如导航高度和底部安全距离高度即是真实内容的高度。


      this.contentHeight = this.windowHeight - this.navBarHeight - this.fixedBottomHeight - this.textareaHeight;

      最后


      到此整个输入框的体验优化核心实现过程就结束了,增加了多行输入,组合键换行输入内容,键盘发送内容,整体内容高度自适应等。整体实现过程的细节功能点还是比较多,有实现过类似需求的同学欢迎留言交流~


      看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~




      作者:南城FE
      来源:juejin.cn/post/7267791228872753167
      收起阅读 »

      前端视角下的鸿蒙开发

      web
      前言 鸿蒙系统,一个从诞生就一直处于舆论风口浪尖上的系统,从最开始的“套壳”安卓的说法,到去年的不再兼容安卓的NEXT版本的技术预览版发布,对于鸿蒙到底是什么,以及鸿蒙的应用开发的讨论从来没停止过。 这次我们就从一个前端开发的角度来了解一下鸿蒙,学习一下鸿蒙...
      继续阅读 »

      前言



      鸿蒙系统,一个从诞生就一直处于舆论风口浪尖上的系统,从最开始的“套壳”安卓的说法,到去年的不再兼容安卓的NEXT版本的技术预览版发布,对于鸿蒙到底是什么,以及鸿蒙的应用开发的讨论从来没停止过。


      这次我们就从一个前端开发的角度来了解一下鸿蒙,学习一下鸿蒙应用的开发。



      一、 什么是鸿蒙


      在开始之前,先问大家一个问题,大家听说过几种鸿蒙?


      其实到目前为止,我们经常听到的鸿蒙系统,总共有三种,分别是:


      OpenHarmony,HarmonyOS,以及HarmonyOS NEXT。


      1. OpenHarmony


      OpenHarmony


      OpenHarmony(开源鸿蒙系统),由开放原子开源基金会进行管理。开放原子开源基金会由华为、阿里、腾讯、百度、浪潮、招商银行、360等十家互联网企业共同发起组建。包含了“鸿蒙操作系统”的基础能力,是“纯血”鸿蒙的底座。


      这个版本的鸿蒙是开源的,代码仓库的地址在这里:gitee.com/openharmony


      从我个人的一些粗浅理解来看,OpenHarmony类似于Android里的AOSP,可以装到各种设备上,比如手表、电视甚至是一些嵌入式设备上,详见可见官网的一些例子


      2. HarmonyOS


      HarmonyOS


      基于 OpenHarmony、AOSP等开源项目,同时加入了自己的HMS(因为被美国限制后无法使用GMS)的商用版本,可以兼容安卓,也可以运行部分OpenHarmony开发的鸿蒙原生应用。


      这个也是目前经常被吐槽是“套壳”安卓的系统,截止到目前(2024.04)已经更新到了HarmonyOS 4.2。


      3. HarmonyOS NEXT


      HarmonyOS NEXT


      2023年秋季发布的技术预览版,在当前HarmonyOS的基础上去除了AOSP甚至是JVM,不再兼容安卓,只能运行鸿蒙原生应用,同时对OpenHarmony的能里进行了大量的更新,增加和修改了很多API。


      这个也就是所谓的“纯血”鸿蒙系统,可惜的是这个目前我们用不到,需要以公司名义找华为合作开权限,或者个人开发者使用一台Mate60 Pro做专门的开发机。并且目前由于有保密协议,网上也没有太多关于最新API的消息。



      NEXT版本文档:developer.huawei.com/consumer/cn…



      无法直接访问的NEXT版本的开发文档


      据说目前HarmonyOS NEXT使用的API版本已经到了API12,目前官网可以访问的最新文档还是API9,所以接下来的内容也都是基于API9的版本来的。


      4. 小结


      所以一个粗略的视角来看,OpenHarmony、HarmonyOS以及HarmonyOS NEXT这三者之间的关系是这样的:


      三者之间的关系


      二、 初识鸿蒙开发


      在大概知道了什么是鸿蒙之后,我们先来简单看一下鸿蒙开发的套件。下图是官网所描述的一些开发套件,包括了设计、开发、测试、上架所涉及到的技术和产品。


      鸿蒙开发套件


      我们这篇文章里主要讨论右下角的三个:ArkTSArkUIArkCompiler


      ArkTS&ArkUI


      ArkCompiler


      三、 关于ArkTS的一些疑惑


      作为一个前端开发,最常用的编程语言就是JavaScript或者TypeScript,那么在看到鸿蒙应用开发用到的编程语言是ArkTS之后,我脑子里最先蹦出来的就是下面这几个问题:


      1. ArkTS语言的运行时是啥?


      既然编程语言是TS(TS的拓展,ArkTS),那么它的运行时是什么呢?是V8?JSC?Hermes?还是其他什么呢?


      2. ArkTS还是单线程语言吗?


      ArkTS还是和JS一样,是单线程语言吗?


      3. 基于TS拓展了什么?


      TS是JS的超集,对JS进行了拓展,增加了开发时的类型支持。而ArkTS对对TS又进行了拓展,是TS的超集,那它基于TS拓展了什么内容呢?


      下面我们一个一个来看。


      1. Question1 - ArkTS语言的运行时


      先说结论,ArkTS的运行时不是V8,不是JSC、Hermes,不是目前任何一种JS引擎。ArkTS的运行时是一个自研的运行时,叫做方舟语言运行时(简称方舟运行时)。


      方舟运行时


      而这个运行时,执行的也不是JS/TS/ArkTS代码,而是执行的字节码和机器码
      这是因为方舟运行时是ArkCompiler(方舟编译器)的一部分,对于JS/TS/ArkTS的编译在运行前就进行了(和Hermes有点像,下面会讲到)。


      方舟开发框架示意图


      我们来简单了解一下ArkCompiler,从官网的描述可以看到,ArkCompiler关注的重点主要有三个方面:



      • AOT 编译模式

      • LiteActor 轻量化并发

      • 源码安全


      AOT 编译模式


      首先是编译模式,我们知道,目前编程语言大多以下几方式运行:



      • 机器码AOT编译


        在程序运行之前进行AST生成和代码编译,编译为机器码,在运行的时候无需编译,直接运行,比如C语言。


      • 中间产物AOT编译


        在程序运行前进行AST生成并进行编译,但不是编译为机器码,而是编译为中间产物,之后在运行时将字节码解释为机器码再执行。比如Hermes或Java编译为字节码,之后运行时由Hermes引擎或JVM解释执行字节码。


      • 完全的解释执行


        在程序运行前不进行任何编译,在运行时动态地根据源码生成AST,再编译为字节码,最后解释执行字节码。比如没有开启JIT的V8引擎执行JS代码时的流程。


      • 混合的JIT编译


        在通过解释执行字节码时(运行时动态生成或者AOT编译生成),对多次执行的热点代码进行进一步的优化编译,生成机器码,后续执行到这部分逻辑时,直接执行优化后的机器码。比如开启JIT的V8引擎运行JS或者支持JIT的JVM运行class文件。




      当然,以上仅考虑生产环境下的运行方式,不考虑部分语言在生产和开发阶段不同的运行方式。比如Dart和Swift,一般是开发阶段通过JIT实时编译快速启动,生产环境下为了性能通过AOT编译。



      在V8 JIT出现之前,所有的JS虚拟机所采用的都是采用的完全解释执行的方式,在运行时把源码生成AST语法树,之后生成字节码,然后将字节码解释为机器码执行,这是JS执行速度过慢的主要原因之一。


      而这么做有以下两个方面的原因:



      • JS是动态语言,变量类型在运行时可能改变

      • JS主要用于Web应用,Web应用如果提前编译为字节码将导致体积增大很多,对网络资源的消耗会更大


      我们一个一个来说。


      a. JS变量类型在运行时可能改变

      首先我们来看一张图,这张图描述了现在V8引擎的工作流程,目前Chrome和Node里的JS引擎都是这个:


      V8现有工作流程


      从上面可以看到,V8在拿到JS源码后,会先解析成AST,之后经过Ignition解释器把语法树编译成字节码,然后再解释字节码执行。


      于此同时还会收集热点代码,比如代码一共运行了多少次、如何运行的等信息,也就是上面的Feedback的流程。


      如果发现一段代码会被重复执行,则监视器会将此段代码标记为热点代码,交给V8的Turbofan编译器对这段字节码进行编译,编译为对应平台(Intel、ARM、MIPS等)的二进制机器码,并执行机器码,也就是图里的Optimize流程。


      等后面V8再次执行这段代码,则会跳过解释器,直接运行优化后的机器码,从而提升这段代码的运行效率。


      但是我们发现,图里面除了Optimize外,还有一个Deoptimize,反优化,也就是说被优化成机器码的代码逻辑,可能还会被反优化回字节码,这是为什么呢?


      其实原因就是上面提到的“JS变量类型在运行时可能改变”,我们来看一个例子:


      JS变量类型在运行时可能改变


      比如一个add函数,因为JS没有类型信息,底层编译成字节码后伪代码逻辑大概如这张图所示。会判断xy的各种类型,逻辑比较复杂。


      在Ignition解释器执行add(1, 2)时,已经知道add函数的两个参数都是整数,那么TurboFan在进一步编译字节码时,就可以假定add函数的参数是整数,这样可以极大地简化生成的汇编代码,不再判断各种类型,伪代码如第三张图里所示。


      接下来的add(3, 4)add(5, 6)由于入参也是整数,可以直接执行之前编译的机器码,但是add("7", "8")时,发现并不是整数,这时候就只能将这段逻辑Deoptimize为字节码,然后解释执行字节码。


      这就是所谓的Deoptimize,反优化。可以看出,如果我们的JS代码中变量的类型变来变去,是会给V8引擎增加不少麻烦,为了提高性能,我们可以尽量不要去改变变量的类型。


      虽然说使用TS可以部分缓解这个问题,但是TS只能约束开发时的类型,运行的时候TS的类型信息是会被丢弃的,也无法约束,V8还是要做上面的一些假定类型的优化,无法一开始就编译为机器码。


      TS类型信息运行时被丢弃


      可以说TS的类型信息被浪费了,没有给运行时代码特别大的好处。


      b. JS编译为字节码将导致体积增大

      上面说到JS主要用于Web应用,Web应用如果提前编译为字节码将导致体积增大很多,对网络资源的消耗会更大。那么对于非Web应用,其实是可以做到提前编译为字节码的,比如Hermes引擎。


      Hermes作为React Native的运行时,是作为App预装到用户的设备上,除了热更新这种场景外,绝大部分情况下是不需要打开App时动态下载代码资源的,所以体积增大的问题影响不是很大,但是预编译带来的运行时效率提升的好处却比较明显。


      所以相对于V8,Hermes去掉了JIT,支持了生成字节码,在构建App的时候,就把JS代码进行了预编译,预编译为了Hermes运行时可以直接处理的字节码,省去了在运行阶段解析AST语法树、编译为字节码的工作。


      Hermes对JS编译和执行流程的改进



      一句题外话,Hermes去除了对JIT的支持,除了因为JIT会导致JS引擎启动时间变长、内存占用增大外,还有一部分可能的原因是,在iOS上,苹果为了安全考虑,不允许除了Safari和WebView(只有WKWebView支持JIT,UIWebView不支持)之外的第三方应用里直接使用JSC的JIT能力,也不允许第三方JS运行时支持JIT(相关问题)。


      甚至V8专门出了一个去掉JIT的JIT-less V8版本来在iOS上集成,Hermes似乎也不太可能完全没考虑到这一点。



      c. 取长补短

      在讨论了V8的JIT和Hermes的预编译之后,我们再来看看ArkCompiler,截取一段官方博客里的描述


      博客描述


      还记得上面说的“TS的类型信息被浪费了”吗?TS的类型信息只在开发时有用,在编译阶段就被丢弃了,而ArkCompiler就是利用了这一点,直接在App构建阶段,利用TS的类型信息直接预编译为字节码以及优化机器码。


      即在ArkCompiler中,不存在TS->JS的这一步转译,而是直接从TS编译为了字节码和优化机器码(这里存疑,官网文档没有找到很明确的说法,不是特别确定是否有TS->JS的转译。详见评论区,如果有知道的大佬可以在评论区交流一下)。


      同时由于鸿蒙应用也是一个App而不是Web应用,所以ArkCompiler和Hermes一样,也是在构建App时就进行了预编译,而不是在运行阶段做这个事情。


      ArkCompiler对JS/TS编译和执行流程的改进


      简单总结下来,ArkCompiler像Hermes一样支持生成字节码,同时又将V8引擎JIT生成机器码的工作也提前在预编译阶段做了。是比Hermes只生成字节码的AOT更进一步的AOT(同时生成字节码和部分优化后的机器码)。


      LiteActor轻量化并发


      到这里其实已经可以回答上面讲到的第二个问题了,ArkTS还是单线程语言吗?


      答案是:是的,还是单线程语言。但是ArkTS里通过Worker和TaskTool这两种方式支持并发。


      同时ArkCompiler对现有的Worker进行了一些优化,直接看官网博客


      LiteActor轻量化并发


      LiteActor轻量化并发博客描述


      这里的Actor是什么呢?Actor是一种并发编程里的线程模型。


      线程模型比较常见的就是共享内存模型,多个线程之间共享内存,比如Java里多个线程共享内存数据,需要通过synchronized同步锁之类的来防止数据一致性的问题。


      Actor模型是另一种线程模型,“Actor”是处理并发计算的基本单位,每个Actor都有自己的状态,并且可以接收和发送消息。当一个Actor接收到消息时,它可以改变自己的状态,发送消息给其他Actor,或者创建新的Actor。


      这种模型可以帮助开发者更好地管理复杂的状态和并发问题,因为每个Actor都是独立的,它们之间不会共享状态,这可以避免很多并发问题。同时,Actor模型也使得代码更易于理解和维护,因为每个Actor都是独立的,它们的行为可以被清晰地定义和隔离。


      到这里大家应该已经比较明白了,前端里的Web Worker就是这种线程模型的一种体现,通过Worker来开启不同的线程。


      源码安全


      按照官网的说法,ArkCompiler会把ArkTS编译为字节码,并且ArkCompiler使用多种混淆技术提供更高强度的混淆与保护,使得HarmonyOS应用包中装载的是多重混淆后的字节码,有效提高了应用代码安全的强度。


      源码安全


      2. Question2 - ArkTS还是单线程语言吗


      这个刚刚已经回答了,还是单线程语言,借用官网的描述:



      HarmonyOS应用中每个进程都会有一个主线程,主线程有如下职责:



      1. 执行UI绘制;

      2. 管理主线程的ArkTS引擎实例,使多个UIAbility组件能够运行在其之上;

      3. 管理其他线程(例如Worker线程)的ArkTS引擎实例,例如启动和终止其他线程;

      4. 分发交互事件;

      5. 处理应用代码的回调,包括事件处理和生命周期管理;

      6. 接收Worker线程发送的消息;


      除主线程外,还有一类与主线程并行的独立线程Worker,主要用于执行耗时操作,但不可以直接操作UI。Worker线程在主线程中创建,与主线程相互独立。最多可以创建8个Worker。



      ArkTS线程模型


      3. Question3 - 基于TS拓展了什么


      当前,ArkTS在TS的基础上主要扩展了如下能力:



      • 基本语法:ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。

      • 状态管理:ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。

      • 渲染控制:ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。


      而上面这些,也就是我们接下来要介绍的ArkTS+ArkUI的语法。


      四、 ArkTS & ArkUI


      首先,在聊ArkUI之前,还有一个问题大家可能比较感兴趣:ArkUI是怎么渲染我们写的UI呢?


      答案是自绘,类似于Flutter,使用自己的渲染引擎(应该是发展于Skia),而不是像RN那样将UI转为不同平台上的底层UI。


      不管是从官网的描述[1]、[2]来看,还是社区里的讨论来看,ArkUI的渲染无疑是自绘制的,并且ArkUI和Flutter之间的联系很密切:


      社区里的一些讨论


      1. 基本语法


      从前端的角度来看,ArkTS和ArkUI的定位其实就是类似于前端中TS+React+配套状态管理工具(如Redux),可以用TS写声明式UI(有点像写jsx),下面是基本语法:


      基本语法



      • 装饰器


        用于装饰类、结构、方法以及变量,并赋予其特殊的含义。


        如上述示例中@Entry、@Component和@State都是装饰器,@Component表示自定义组件,@Entry表示该自定义组件为入口组件,@State表示组件中的状态变量,状态变量变化会触发UI刷新


      • 自定义组件


        可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello


      • UI描述


        以声明式的方式来描述UI的结构,例如build()方法中的代码块


      • 系统组件


        ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的ColumnTextDividerButton


      • 事件方法


        组件可以通过链式调用设置多个事件的响应逻辑,如跟随在Button后面的onClick()


      • 属性方法


        组件可以通过链式调用配置多项属性,如fontSize()width()height()backgroundColor()



      2. 数据驱动UI


      作为一个声明式的UI框架,ArkUI和其他众多UI框架(比如React、Vue)一样,都是通过数据来驱动UI变化的,即UI = f(State)。我们这里引用一下官网的描述:



      在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。


      自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系。



      State和UI



      View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
      State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。



      在ArkUI中,提供了大量的状态管理相关的装饰器,比如@State@Prop@Link等。


      ArkTS & ArkUI的状态管理总览


      更多细节详见状态管理


      3. 渲染控制


      在ArkUI中,可以像React那样,通过if elsefor each等进行跳转渲染、列表渲染等,更多细节详见渲染控制



      ArkUI通过自定义组件build()函数和@builder装饰器中的声明式UI描述语句构建相应的UI。在声明式描述语句中开发者除了使用系统组件外,还可以使用渲染控制语句来辅助UI的构建,这些渲染控制语句包括控制组件是否显示的条件渲染语句,基于数组数据快速生成组件的循环渲染语句以及针对大数据量场景的数据懒加载语句。



      4. 更多语法


      语法其实不是我们这篇文章的重点,上面是一些大概的介绍,更多语法可以详见官网,或者我的另外一篇专门讲解语法的笔记《前端视角下的ArkTS语法》(先留个占位符,有时间了补充一下)。


      5. ArkTS & ArkUI小结


      从前面的内容其实可以看到,ArkUI和RN相似点还挺多的:



      1. 都是使用JS/TS作为语言(ArkTS)

      2. 都有自己的JS引擎/运行时(ArkCompiler,方舟运行时)

      3. 引擎还都支持直接AOT编译成字节码


      不同的是RN是将JS声明的UI,转换成iOS、Android原生的组件来渲染,而ArkUI则是采用自绘制的渲染引擎来自绘UI。


      从这点来看,鸿蒙更像是Flutter,只不过把开发语言从Dart换成了JS/TS(ArkTS),和Flutter同样是自绘制的渲染引擎。


      社区里其实也有类似的思考:其它方向的探索:JS Engine + Flutter RenderPipeLine。而ArkUI则是对这种思路的实现。


      感觉这也可以从侧面解释为什么ArkUI的语法和Flutter比较像,应该参考了不少Flutter的实现(比如渲染引擎)。


      而华为宣称鸿蒙可以反向兼容Flutter甚至是RN也就没有那么难以理解了,毕竟ArkUI里Flutter和RN的影子确实不少。


      另外,除了ArkUI以外,华为还提供了一个跨平台的开发框架ArkUI-X,可以像Flutter那样,跨HarmonyOS、Android、iOS三个平台。


      这么看来,ArkTS&ArkUI从开发语言、声明式UI的语法、设计思想来看,不管是前端、iOS、安卓、或者Flutter、RN,鸿蒙应用开发都是比较入门友好的。


      五、 其他


      1. 包管理工具


      HarmonyOS开发中,使用的包管理工具是ohpm,目前看来像是一个借鉴pnpm的三方包管理工具,详见官方文档


      另外,鸿蒙也提供了第三方包发布的仓库:ohpm.openharmony.cn


      2. 应用程序结构


      在鸿蒙系统中,一个应用包含一个或者多个Module,每一个Module都可以独立进行编译和运行。


      应用程序结构


      发布时,每个Module编译为一个.hap后缀的文件,即HAP。每个HarmonyOS应用可以包含多个.hap文件。


      在应用上架到应用市场时,需要把应用包含的所有.hap文件打包为一个.app后缀的文件用于上架。


      但是.app包不能直接安装到设备上,只是上架应用市场的单元,安装到设备上的是.hap


      打包结构


      开发态和打包后视图


      鸿蒙应用的整体开发调试与发布部署流程大概是这样的:


      开发-调试-发布-部署


      HAP可分为Entry和Feature两种类型:



      • Entry类型的HAP:是应用的主模块
        在同一个应用中,同一设备类型只支持一个Entry类型的HAP,通常用于实现应用的入口界面、入口图标、主特性功能等。

      • Feature类型的HAP:是应用的动态特性模块
        一个应用程序包可以包含一个或多个Feature类型的HAP,也可以不包含;Feature类型的HAP通常用于实现应用的特性功能,可按需下载安装


      而设计成多hap,主要是有3个目标:



      1. 为了解耦应用的各个模块,比如一个支付类型的App,Entry类型的hap可以是首页主界面,上面的扫一扫、消息、理财等可以的feature类型的HAP

      2. 方便开发者将多HAP合理地组合并部署到不同的设备上,比如有三个HAP,Entry、Feature1和Feature2,其中A类型的设备只能部署Entry和Feature1。B类型的设备只能部署Entry和Feature2

      3. 方便应用资源共享,减少程序包大小。多个HAP都需要用到的资源文件可以放到单独的HAP中



      多说一句:从这些描述来看,给我的感觉是每个.hap有点类似于前端项目中Mono-repo仓库中的一个package,各个package之间有一定的依赖,同时每个package可以独立发布。



      另外,HarmonyOS也支持类似RN热更新的功能,叫做快速修复(quick fix)。


      六、 总结


      现在再回到最开始那个问题:什么是鸿蒙?从前端视角来看,它是这样一个系统:



      • ArkTS作为应用开发语言

      • 类Flutter、Compose、Swift的声明式UI语法

      • 和React有些相似的数组驱动UI的设计思想

      • ArkCompiler进行字节码和机器码的AOT编译 + 方舟运行时

      • 类似Flutter Skia渲染引擎的自绘制渲染引擎

      • 通过提供一系列ohos.xxx的系统内置包来提供TS访问系统底层的能力(比如网络、媒体、文件、USB等)


      所以关于HarmonyOS是不是安卓套壳,个人感觉其实已经比较明了了:以前应该是,但快要发布的HarmonyOS NEXT大概率不再是了。


      其他一些讨论


      其实在华为宣布了HarmonyOS NEXT不再兼容安卓后,安卓套壳的声音越来越少了,但现在网上另外一种声音越来越多了:




      1. HarmonyOS NEXT是一个大号的小程序底座,上面的应用都是网页应用,应用可以直接右键查看源码,没有安全性可言

      2. HarmonyOS NEXT上的微信小程序就是在小程序里运行小程序

      3. 因为使用的是ArkTS开发,所以的HarmonyOS NEXT上的应用性能必然很差



      这种说法往往来自于只知道鸿蒙系统应用开发语言是TS,但是没有去进一步了解的人,而且这种说法还有很多人信。其实只要稍微看下文档,就知道这种说法是完全错误的


      首先它的View层不是DOM,而是类似Flutter的自绘制的渲染引擎,不能因为使用了TS就说是网页,就像可以说React Web是网页应用,但不能说React Native是网页应用,同样也不是说Flutter是网页应用。


      另外开发语言本身并不能决定最终运行性能,还是要看编译器和运行时的优化。同样是JS,从完全的解释执行(JS->AST->字节码->执行),到开启JIT的V8,性能都会有质的飞跃。从一些编程语言性能测试中可以看到,开启JIT的NodeJs的性能,甚至和Flutter所使用的Dart差不多。


      而ArkCompiler是结合了Hermes和V8 JIT的特点,AOT编译为字节码和机器码,所以理论上讲性能应该相当不错。


      (当然我也没有实机可以测试,只能根据文档来分析)。


      上面这种HarmonyOS NEXT是网页应用的说法还有可能是由于,最早鸿蒙应用支持使用HTML、CSS、JS三件套进行兼容Web的开发,导致了刻板印象。这种开发方式使用的是FA模型,而目前这种方式已经不是鸿蒙主推的开发方式了。


      到这里这篇文章就结束了,整体上是站在一个前端开发的视角下来认识和了解鸿蒙开发的,希望能帮助一些像我一样对鸿蒙开发感兴趣的前端开发入门。大家如果感兴趣可以到鸿蒙官网查看更多的了解。


      如果感觉对你有帮助,可以点个赞哦~


      作者:酥风
      来源:juejin.cn/post/7366948087129309220
      收起阅读 »

      责任链模式最强工具res-chain🚀

      web
      上面的logo是由ai生成 责任链模式介绍 责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它通过把请求的发送者和接收者解耦,将多个对象连接成一个链,并沿着这条链传递请求,直到有一个对象能够处理它为止,从而避...
      继续阅读 »
      image.png

      上面的logo是由ai生成



      责任链模式介绍


      责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它通过把请求的发送者和接收者解耦,将多个对象连接成一个链,并沿着这条链传递请求,直到有一个对象能够处理它为止,从而避免了请求的发送者和接收者之间的直接耦合


      在责任链模式中,每个处理者都持有对下一个处理者的引用,即构成一个链表结构。当请求从链头开始流经链上的每个处理者时,如果某个处理者能够处理该请求,就直接处理,否则将请求发送给下一个处理者,直到有一个处理者能够处理为止。


      这种方式可以灵活地动态添加或修改请求的处理流程,同时也避免了由于请求类型过多而导致类的爆炸性增长的问题。


      看完以上责任链的描述,有没有发现跟Node.js的某些库特别的像,没错,就是koa。什么?没用过koa?那我建议你立马学起来,因为它用起来特别的简单。


      下面来一个简单使用koa的例子:


      const Koa = require('koa');
      const app = new Koa();

      app.use(async (ctx, next) => {
      if (ctx.request.url === '/') {
      ctx.body = 'home';
      return;
      }

      next(); // 执行下面的回调函数
      });

      app.use(async (ctx, next) => {
      if (ctx.request.url === '/hello') {
      ctx.body = 'hello world';
      return;
      }
      });

      app.listen(3000);

      通过node运行上面的代码,在浏览器请求localhost:3000,接口就会返回home,当我们请求localhost:3000/hello,接口就会返回hello world


      上面对请求的处理过程就很符合职责链模式的思想,我们可以清楚的知道每个链做的工作,并且链条的顺序流程也很清晰。


      有人就会问,只在一个回调里面也能处理呀,比如下面的代码:


      app.use(async (ctx, next) => {
      if (ctx.request.url === '/') {
      ctx.body = 'home';
      return;
      } else if (ctx.request.url === '/home') {
      ctx.body = 'hello world';
      return
      }
      });

      是的,上面的代码确实可以实现,但是这就要回到我们使用责任链模式的初衷了:为了逻辑解耦。


      责任链解决的问题


      我们继续接着聊上一节的问题,使用if确实可以实现相同效果,但是在某些场景中,if并没有职责链那么好用,为什么这么说呢。


      我们找一个应用案例举个例子:



      假设我们负责一个售卖手机的网站,需求的定义是:经过分别缴纳500元定金和200元定金的两轮预订,现在到了正式购买阶段。公司对于交了定金的用户有一定的优惠政策,规则如下:




      • 缴纳500元定金的用户可以收到100元优惠券;

      • 缴纳200元定金的用户可以收到50元优惠券;

      • 没有缴纳定金的用户进入普通购买模式,没有优惠券。

      • 而且在库存不足的情况下,不一定能保证买得到。


      下面开始设计几个字段,解释它们的含义:



      • orderType:表示订单类型,值为1表示500元定金用户,值为2表示200元定金用户,值为3表示普通用户。

      • pay:表示用户是否支付定金,值为布尔值true和false,就算用户下了500元定金的订单,但是如果没有支付定金,那也会降级为普通用户购买模式。

      • stock:表示当前用户普通购买的手机库存数量,已经支付过定金的用户不受限制。


      下面我们分别用if和职责链模式来实现:


      使用if:


      const order = function (orderType, pay, stock) {
      if (orderType === 1) {
      if (pay === true) {
      console.log('500元定金预购,得到100元优惠券')
      } else {
      if (stock > 0) {
      console.log('普通用户购买,无优惠券')
      } else {
      console.log('手机库存不足')
      }
      } else if (orderType === 2) {
      if (pay === true) {
      console.log('200元定金预购,得到50元优惠券')
      } else {
      if (stock > 0) {
      console.log('普通用户购买,无优惠券')
      } else {
      console.log('手机库存不足')
      }
      }
      } else if (orderType === 3) {
      if (stock > 0) {
      console.log('普通用户购买,无优惠券')
      } else {
      console.log('手机库存不足')
      }
      }
      }

      order(1, true, 500) // 输出:500元定金预购,得到100元优惠券'

      虽然上面的代码也可以实现需求,但是代码实在是难以阅读,维护起来更是困难,如果继续在这个代码上开发,未来肯定会成为一座很大的屎山。


      下面我们使用责任链模式来实现:


      function printResult(orderType, pay, stock) {
      // 这里ResChain类是模拟koa的写法,后面会讲如何实现ResChain
      // 请先耐心看完它是如何处理的
      const resChain = new ResChain();
      // 针对500元定金的情况
      resChain.add('order500', (_, next) => {
      if (orderType === 1 && pay === true) {
      console.log('500元定金预购,拿到100元优惠券');
      return;
      }
      next(); // 这里将会调用order200对应的回调函数
      });
      // 针对200元定金的情况
      resChain.add('order200', (_, next) => {
      if (orderType === 2 && pay === true) {
      console.log('200元定金预购,拿到50元优惠券');
      return;
      }
      next(); // 这里会调用noOrder对应回调函数
      });
      // 针对普通用户购买的情况
      resChain.add('noOrder', (_, next) => {
      if (stock > 0) {
      console.log('普通用户购买,无优惠券');
      } else {
      console.log('手机库存不足');
      }
      });

      resChain.run(); // 开始执行order500对应的回调函数
      }

      // 测试
      printResult(1, true, 500); // 500元定金预购,得到100元优惠券
      printResult(1, false, 500); // 普通用户购买,无优惠券
      printResult(2, true, 500); // 200元定金预购,得到50元优惠券
      printResult(3, false, 500); // 普通用户购买,无优惠券
      printResult(3, false, 0); // 手机库存不足

      以上的代码经过责任链处理之后特别的清晰,并且减少了大量的if-else嵌套,每个链的职责分,我们可以看出责任链模式存在的优点:



      1. 降低了代码之间的耦合,很好的对每个处理逻辑进行封装。在每个链条内,只需要关注自身的逻辑实现。

      2. 增强了代码的可维护性。我们可以很轻易在原有链条内的任何位置添加新的节点,或者对链条内的节点进行替换或者删除。


      责任链还特别的灵活,如果说后面pm找我们加需求,需要加多一个预付定金400,返回80元优惠券,处理起来也是易如反掌,只需要怼回去,只需要在order500下面加多一个节点处理即可:


      ... 
      resChain.add('order500', (_, next) => {
      if (orderType === 1 && pay === true) {
      console.log('500元定金预购,拿到100元优惠券');
      return;
      }
      next();
      })
      + // 加上这一块
      + resChain.add('order400', (_, next) => {
      + if (orderType === 3 && pay === true) {
      + console.log('400元定金预购,拿80元优惠券');
      + return;
      + }
      + next();
      + })

      resChain.add('order200', (_, next) => {
      if (orderType === 2 && pay === true) {
      console.log('200元定金预购,拿到50元优惠券');
      return;
      }
      next();
      })
      ...

      就是这么简单。那这个ResChain是如何实现的呢?


      封装ResChain


      先别急,首先我们来了解一下koa是如何实现:在链节点的回调函数内调用next就可以跳到下一个节点的呢?


      话不多说,直接看源码,参考的库是koa-compose,代码也是特别的简洁:


      function compose (middleware) {
      // 这里传入的middleware是函数数组,例如: [fn1, fn2, fn3, fn4, ...]
      if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
      // 判断数组里的元素是不是函数类型
      for (const fn of middleware) {
      if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
      }

      return function (context, next) {
      // last called middleware #
      let index = -1
      return dispatch(0);

      // 这里利用了函数申明提升的特性
      function dispatch (i) {
      // 这里是防止重复调用next
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i

      // 从middleware中取出回调函数
      let fn = middleware[i]
      if (i === middleware.length) fn = next

      // 如果fn为空了,则结束运行
      if (!fn) return Promise.resolve()

      try {
      // next函数其实就是middleware的下一个函数,执行next就是执行下一个函数
      return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
      return Promise.reject(err)
      }
      }
      }
      }

      看完源代码,我们接着来实现ResChain类,首先整理一下应该要有的方法:



      • add方法。可以添加回调函数,并按添加的顺序执行。

      • run方法。开始按顺序执行责任链。


      add方法执行的时候,把回调函数按顺序push进一个数组中。


      export class ResChain {

      /**
      * 按顺序存放链的key
      */

      keyOrder = [];
      /**
      * key对应的函数
      */

      key2FnMap = new Map();
      /**
      * 每个节点都可以拿到的对象
      */

      ctx = {}
      constructor(ctx) {
      this.ctx = ctx;
      }

      // 这里用key来标识当前callback的唯一性,后面重复添加可以区分。
      add(key, callback) {
      if (this.key2FnMap.has(key)) {
      throw new Error(`Chain ${key} already exists`);
      }

      this.keyOrder.push(key);
      this.key2FnMap.set(key, callback);
      return this;
      }

      async run() {
      let index = -1;
      const dispatch = (i) => {
      if (i <= index) {
      return Promise.reject(new Error('next() called multiple times'));
      }

      index = i;
      const fn = this.key2FnMap.get(this.keyOrder[i]);
      if (!fn) {
      return Promise.resolve(void 0);
      }

      return fn(this.ctx, dispatch.bind(null, i + 1));
      };

      return dispatch(0);
      }
      }

      add方法的第一个参数key可以用来判断是否已经添加过相同的回调。


      有人会说,koa的中间件是异步函数的,你这个行不行?


      当然可以,接下来看个异步的例子:


      const resChain = new ResChain();

      resChain.add('async1', async (_, next) => {
      console.log('async1');
      await next();
      });


      resChain.add('async2', async (_, next) => {
      console.log('async2')
      // 这里可以执行一些异步处理函数
      await new Promise((resolve, reject) => {
      setTimeOut(() => {
      resolve();
      }, 1000)
      });

      await next();
      });


      resChain.add('key3', async (_, next) => {
      console.log('key3');
      await next();
      });


      // 执行责任链
      await resChain.run();

      console.log('finished');

      // 先输出 async1 async2 然后停顿了1秒钟之后,才输出async3 finished


      🚧 需要注意:如果是异步模式,则链上的每个回调函数必须要 await next(),因为next函数代表下一个环的异步函数。



      koa的中间件方式简直一毛一样。


      有人可能还注意到了,ResChain实例化的时候可以传入对象,比如下面的代码:


      const resChain = new ResChain({ interrupt: false });

      传入对象具体有什么用法呢?可以用来获取一些在链中处理好的数据,来实现发送者和处理者的解耦。可能比较抽象,我们来举个例子。


      比如需要进行数据校验的场景,如果不通过,则中断提交:


      const ctx = {
      // 表单项
      model: {
      name: '',
      phone: '',
      },
      // 错误提示
      error: '',
      // 是否中断
      interrupt: false,
      }
      const resChain = new ResChain(ctx);

      resChain.add('校验name', (ctx, next) => {
      const { name = '' } = ctx;
      if (name === '') {
      ctx.error = '请填写name';
      ctx.interrupt = true;
      return;
      }

      next();
      })

      resChain.add('校验phone', (ctx, next) => {
      const { phone = '' } = ctx;
      if (phone === '') {
      ctx.error = '请填写手机号';
      ctx.interrupt = true;
      return;
      }

      next();
      })

      // 执行责任链
      resChain.run();

      // 如果需要中断,则提示
      if (resChain.ctx.interrupt) {
      alert(resChain.ctx.error);
      return;
      }

      如果是使用if来实现:


      const ctx = {
      // 表单项
      model: {
      name: '',
      phone: '',
      },
      // 错误提示
      error: '',
      // 是否中断
      interrupt: false,
      }

      if(ctx.model.name === '') {
      ctx.error = '请填写用户名';
      ctx.interrupt = true;
      }

      if (!ctx.interrupt && ctx.model.phone === '') {
      ctx.error = '请填写手机号';
      ctx.interrupt = true;
      }

      // 如果需要中断,则提示
      if (resChain.ctx.interrupt) {
      alert(resChain.ctx.error);
      return;
      }

      可以发现,对phone的判断逻辑,就要先判断interrupt是否为false才能继续,而且如果下面还有其他的字段校验,那必须都走一遍if。


      这也是责任链的一个优势,可以在某个环节按自己的想法停止,不用继续走后面的节点。


      目前我已经把这个工具上传到npm了,如果想要在自己的项目中使用,直接安装:
      res-chain即可使用:


      npm install res-chain

      # 或者
      # yarn add res-chain

      引入:


      import { ResChain } from 'res-chain';
      // CommonJS方式的引入也是支持的
      // const { ResChain } = 'res-chain';

      const resChain = new ResChain();

      resChain.add('key1', (_, next) => {
      console.log('key1');
      next();
      });

      resChain.add('key2', (_, next) => {
      console.log('key2');
      // 这里没有调用next,则不会执行key3
      });

      resChain.add('key3', (_, next) => {
      console.log('key3');
      next();
      });

      // 执行职责链
      resChain.run(); // => 将会按顺序输出 key1 key2

      芜湖起飞🚀。


      有了这个工具函数,我们就可以视场景去优化项目中的一大坨if-else嵌套,或者直接使用它来实现一些业务中比较复杂的逻辑。


      起源


      这个工具诞生的过程还挺巧合的,某一天周六我在公司加班赶需求,发现需要在一堆旧逻辑if-else中添加新的逻辑,强迫症的我实在是无法忍受在💩山上继续堆💩。。。


      我陷入沉思,用什么方式去优化呢?看了网上责任链模式的实现,感觉还是不够优雅。


      无意中翻到了之前用koa写的项目,突然灵光乍现💡,koa的中间件不就是一个很棒的实践。调用next就能够往下一个节点走,不调用的话就可以终止。


      于是立即动工,三下五除二就完成了。我还推荐给部门的其他前端小伙伴,他们也在一些需求的复杂逻辑中有运用。


      总结



      过去无意学到的某个知识,或者某个概念,在未来也许会发挥作用,你只需要做的就是等待。



      没有koa这么棒的库,估计也不会有这工具了,所以还是得感谢它的作者如此聪明。😁


      如果你也喜欢这个工具,欢迎去github里给个🌟,感谢。


      如果有什么更好的建议,在底下留言,一起探讨。


      工具链接


      res-chain


      参考



      作者:Johnhom
      来源:juejin.cn/post/7368662916151377959
      收起阅读 »

      你没见过的【只读模式】,被我玩出花了

      web
      前言 不是标题党,不是标题党,不是标题党,重要的话说三遍!大家常见的【只读模式】,下面简称 readonly,可能最常用的是在 表单场景中,除了正常的表单场景,你还会想象到它可以应用在我们中后台场景的 编辑表格、描述列表、查询表格 吗?先看看效果吧 ~ 表单场...
      继续阅读 »

      前言


      不是标题党,不是标题党,不是标题党,重要的话说三遍!大家常见的【只读模式】,下面简称 readonly,可能最常用的是在 表单场景中,除了正常的表单场景,你还会想象到它可以应用在我们中后台场景的 编辑表格描述列表查询表格 吗?先看看效果吧 ~


      表单场景


      form-readonly.gif


      表单列表场景


      form-list-readonly.gif


      描述列表场景


      description-readonly.gif


      查询表格场景


      table-readonly.gif


      编辑表格场景


      edit-table-readonly.gif


      上面看到的所有效果,背后都有 readonly 的存在



      1. 表单场景示例中表单列表场景示例中 使用 readonly,在实际业务中可能会应用到 编辑详情

      2. 描述列表场景示例中 使用 readonly,在实际业务中可能会应用到 单独的详情页 页面中

      3. 查询表格场景示例中 使用 readonly,在实际业务中应用很广泛,比如常见的日期,后端可能会返回字符串、空、时间戳,就不需要用户单独处理了 (挺麻烦的,不是吗)

      4. 编辑表格场景示例中 使用 readonly,在做一些类似 行编辑单元格编辑 功能中常用


      下面就以 实现思路 + 伪代码 的方式和大家分享 readonly 的玩法


      以 Date 组件为例


      我们这里说的 Date 就是单纯的日期组件,不包含 pickermonth(月份)quarter(季度) 等,我们先思考一下,如何让 日期组件 可以在多处公用(查询表格、表单、编辑表格、描述列表)


      多处公用


      我们可以将 Date 组件进行封装,变成 ProDate,我们在 ProDate 中扩展一个属性为 readonly,在扩展一个插槽 readonly,方便用户自定义,以下为伪代码


      <script lang="tsx">
      import { DatePicker, TypographyText } from 'ant-design-vue'

      export default defineComponent({
      name: 'ProDate',
      inheritAttrs: false,
      props: {
      readonly:{
      type: Boolean,
      default:false
      }
      },
      slots: {
      readonly: { rawValue: any }
      },
      setup(props, { slots, expose }) {
      const getReadonlyText = computed(() => {
      const value = toValue(dateValue)
      return getDateText(value, {
      format: toValue(valueFormat),
      defaultValue: toValue(emptyText),
      })
      })

      return {
      readonly,
      getReadonlyText,
      }
      },
      render() {
      const {
      readonly,
      getReadonlyText,
      } = this

      if (readonly)
      return $slots.readonly?.({ rawValue: xxx }) ?? getReadonlyText

      return <DatePicker {...xxx} v-slots={...xxx} />
      },
      })
      </script>


      上面的伪代码中,我们扩展了 readonly 属性和 readonly 插槽,我们 readonly 模式下会调用 getDateText 方法返回值,下面代码是 getDateText 的实现


      interface GetDateTextOptions {
      format: string | ((date: number | string | Dayjs) => any)
      defaultValue: any
      }

      // 工具函数
      export function getDateText(date: Dayjs | number | string | undefined | null, options: GetDateTextOptions) {
      const {
      format,
      defaultValue,
      } = options

      if (isNull(date) || isUndefined(date))
      return defaultValue

      if (isNumber(date) || isString(date)) {
      // 可能为时间戳或者字符串
      return isFunction(format)
      ? format(date)
      : dayjs(date).format(format)
      }

      if (isDayjs(date)) {
      return isFunction(format)
      ? format(date)
      : date.format(format)
      }

      return defaultValue
      }

      好了,伪代码我们实现完了,现在我们就假设我们的 ProDate 就是加强版的 DatePicker,这样我们就能很方便的集成到各个组件中了


      集成到 表单中


      因为我们是加强版的 DatePicker,还应该支持原来的 DatePicker 用法,我们上面伪代码没有写出来的,但是如果使用的话,还是如下使用


      <template>
      <AForm>
      <AFormItem>
      <ProDate v-model:value="xxxx" />
      </AFormItem>
      </AForm>

      </template>

      这样的话,我们如果是只读模式,可以在 ProDate 中增加 readonly 属性或插槽即可,当然,为了方便,我们实际上应该给 Form 组件也扩展一个 readonly 属性,然后 ProDatereadonly 属性的默认值应该是从 Form 中去取,这里实现我就不写出来了,思路的话可以通过在 Formprovide 注入默认值,然后 ProDate 中通过 inject


      好了,我们集成到 表单中 就说这么多,实际上还是有很多的细节的,如果大家想看的话,后面再写吧


      集成到 描述列表中


      描述列表用的是 Descriptions 组件,因为大部分用来做详情页,比较简单,所以这里我将它封装成了 json 方式,用 schemas 属性来描述每一项的内容,大概是以下用法


      <ProDescriptions
      title="详情页"
      :data-source="{time:'2023-01-30'}"
      :schemas="[
      {
      label:'日期',
      name:'time',
      component:'ProDate'
      }
      ]"

      />

      解释一下:


      上面的 schemas 中的项可以简单看成如下代码


      <DescriptionsItem>
      <ProDate
      readonly
      :value="get(dataSource,'time')"
      />

      </DescriptionsItem>

      我们在描述组件中应该始终传递 readonly: true,这样渲染出来虽然也是一个文本,但是经过了 ProDate 组件的日期处理,这样就可以很方便的直接展示了,而不用去写一个 render 函数自己去处理


      集成到 查询表格中


      实际上是和 集成到描述列表中 一样的思路,无非是将 ProDescriptions 组件换成 ProTable 组件,schemas 我们用同一套就可以,伪代码如下


      <ProTable
      title="详情页"
      :data-source="{time:'2023-01-30'}"
      :schemas="[
      {
      label:'日期',
      name:'time',
      component:'ProDate'
      }
      ]"

      />

      当然我们在 ProTable 内部对 schemas 的处理就要在 customRender 函数中去渲染了,内部实现的伪代码如下


      <Table 
      :columns="[
      {
      title:'日期',
      dataIndex:'time',
      customRender:({record}) =>{
      return <ProDate
      readonly
      value={get(record,'time')}
      />
      }
      }
      ]"

      />

      ProTableProDescriptions 的处理方式是类似的


      集成到 编辑表格中


      没啥好说的,实际上是和 集成到表单中 一样的思路,伪代码用法如下


      <ProForm>
      <ProEditTable
      :data-source="{time:'2023-01-30'}"
      :schemas="[
      {
      label:'日期',
      name:'time',
      component:'ProDate'
      }
      ]"

      />

      </ProForm>

      我们还是复用同一套的 schemas,只不过组件换成了 ProEditTable,不同的是,我们在内部就不能写死 readonly 了,因为可能会 全局切换成编辑或者只读某一行切换成编辑或者只读某个单元格切换成编辑或者只读,所以我们这里应该对每一个单元格都需要定义一个 readonly 的响应式属性,方便切换,具体的实现就不细说了,因为偏题了


      结语


      好了,我们分享了 只读模式 下不同组件下的表现,而不是简单的在 表单中 为了好看而实现的,下期再见 ~


      作者:一名爱小惠的前端
      来源:juejin.cn/post/7329691357211361318
      收起阅读 »

      面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

      web
      扯皮 这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。 因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大...
      继续阅读 »

      扯皮


      这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。


      因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大致过一遍,直到看见关于 Promise 的取消以及监听进度...🤔


      只能说以后要是我当上面试官一定让候选人来谈谈这两个点,然后顺势安利我这篇文章🤣


      不过好像目前为止也没见哪个面试官出过...


      2024-04-03 更新:这段时间在刷牛客,无意间看到了 25 届佬: 收心檬 的个人主页 - 文章 - 掘金 (juejin.cn) 美团暑期实习的面经,绷不住了🤣


      pic.png


      原面经链接:美团暑期一面_牛客网 (nowcoder.com)


      不知道这位面试官是不是看了我的文章出的题,例子举的都大差不差🤣


      我确实标题党了想整个活,没想到大厂面试官真出啊,还是实习生,刁难人有一手的🙃...


      正文


      取消功能


      我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。


      但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它永远停留至 pending 状态


      奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了🤔


       const p = new Promise((resolve, reject) => {
      setTimeout(() => {
      // handler data, no resolve and reject
      }, 1000);
      });
      console.log(p); // Promise {<pending>} 💡

      但注意我们的需求条件,是在状态转换过程中,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。


      其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:


      const getData = () =>
      new Promise((resolve) => {
      setTimeout(() => {
      console.log("发送网络请求获取数据"); // ❗
      resolve("success get Data");
      }, 2500);
      });

      const timer = () =>
      new Promise((_, reject) => {
      setTimeout(() => {
      reject("timeout");
      }, 2000);
      });

      const p = Promise.race([getData(), timer()])
      .then((res) => {
      console.log("获取数据:", res);
      })
      .catch((err) => {
      console.log("超时: ", err);
      });

      问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎🤔,即使超时网络请求还会发出:


      超时中断.png


      而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。


      比如让用户进行控制,一个按钮用来表示发送请求,一个按钮表示取消,来中断 promise 的流程:



      当然这里我们不讨论关于请求的取消操作,重点在 Promise 上



      取消请求.png


      其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生,clearTimeoutclearInterval 嘛。


      OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:


      <!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>
      <button id="send">Send</button>
      <button id="cancel">Cancel</button>

      <script>
      class CancelToken {
      constructor(cancelFn) {
      this.promise = new Promise((resolve, reject) => {
      cancelFn(() => {
      console.log("delay cancelled");
      resolve();
      });
      });
      }
      }
      const sendButton = document.querySelector("#send");
      const cancelButton = document.querySelector("#cancel");

      function cancellableDelayedResolve(delay) {
      console.log("prepare send request");
      return new Promise((resolve, reject) => {
      const id = setTimeout(() => {
      console.log("ajax get data");
      resolve();
      }, delay);

      const cancelToken = new CancelToken((cancelCallback) =>
      cancelButton.addEventListener("click", cancelCallback)
      );
      cancelToken.promise.then(() => clearTimeout(id));
      });
      }
      sendButton.addEventListener("click", () => cancellableDelayedResolve(1000));
      </script>
      </body>
      </html>

      这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:


      首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为取消功能期限,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。


      这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧,路过的大佬如果有理解的希望能帮忙解释一下。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理🤔?


      介于自己没理解,我就按照自己的思路封装个不一样的🤣:


      const sendButton = document.querySelector("#send");
      const cancelButton = document.querySelector("#cancel");

      class CancelPromise {

      // delay: 取消功能期限 request:获取数据请求(必须返回 promise)
      constructor(delay, request) {
      this.req = request;
      this.delay = delay;
      this.timer = null;
      }

      delayResolve() {
      return new Promise((resolve, reject) => {
      console.log("prepare request");
      this.timer = setTimeout(() => {
      console.log("send request");
      this.timer = null;
      this.req().then(
      (res) => resolve(res),
      (err) => reject(err)
      );
      }, this.delay);
      });
      }

      cancelResolve() {
      console.log("cancel promise");
      this.timer && clearTimeout(this.timer);
      }
      }

      // 模拟网络请求
      function getData() {
      return new Promise((resolve) => {
      setTimeout(() => {
      resolve("this is data");
      }, 2000);
      });
      }

      const cp = new CancelPromise(1000, getData);

      sendButton.addEventListener("click", () =>
      cp.delayResolve().then((res) => {
      console.log("拿到数据:", res);
      })
      );
      cancelButton.addEventListener("click", () => cp.cancelResolve());

      正常发送请求获取数据:


      发送请求.gif


      中断 promise:


      取消请求.gif


      没啥大毛病捏~


      进度通知功能


      进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:



      执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用



      这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:


      class TrackablePromise extends Promise {
      constructor(executor) {
      const notifyHandlers = [];
      super((resolve, reject) => {
      return executor(resolve, reject, (status) => {
      notifyHandlers.map((handler) => handler(status));
      });
      });
      this.notifyHandlers = notifyHandlers;
      }
      notify(notifyHandler) {
      this.notifyHandlers.push(notifyHandler);
      return this;
      }
      }
      let p = new TrackablePromise((resolve, reject, notify) => {
      function countdown(x) {
      if (x > 0) {
      notify(`${20 * x}% remaining`);
      setTimeout(() => countdown(x - 1), 1000);
      } else {
      resolve();
      }
      }
      countdown(5);
      });

      p.notify((x) => setTimeout(console.log, 0, "progress:", x));
      p.then(() => setTimeout(console.log, 0, "completed"));


      emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?


      不好就自己再写一个🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:



      // 模拟数据请求
      function getData(timer, value) {
      return new Promise((resolve) => {
      setTimeout(() => {
      resolve(value);
      }, timer);
      });
      }

      let p = new TrackablePromise(async (resolve, reject, notify) => {
      try {
      const res1 = await getData1();
      notify("已获取到一阶段数据");
      const res2 = await getData2();
      notify("已获取到二阶段数据");
      const res3 = await getData3();
      notify("已获取到三阶段数据");
      resolve([res1, res2, res3]);
      } catch (error) {
      notify("出错!");
      reject(error);
      }
      });

      p.notify((x) => console.log(x));
      p.then((res) => console.log("Get All Data:", res));


      notify获取数据.gif


      对味儿了~😀


      End


      关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。


      实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...


      至于说进度通知功能,仁者见仁智者见智吧...


      但不管怎么样两个功能实现的思路都是比较有趣的,而且不太常见,不考虑实用性确实能够成为一道考题,只能说很符合面试官的口味😏


      作者:討厭吃香菜
      来源:juejin.cn/post/7312349904046735400
      收起阅读 »

      检测图片是否cmyk

      web
      引入 最近业务上有要求,要求如果是 Jpeg 格式文件, 前端在上传的时候要求判断一下这个文件是否 CMYK 颜色模式(color mode/ color space)。 这个颜色模式是打印行业需要的。如果不是则禁止上传,并提示用户。 一开始我以为这个应该存储...
      继续阅读 »

      引入


      最近业务上有要求,要求如果是 Jpeg 格式文件, 前端在上传的时候要求判断一下这个文件是否 CMYK 颜色模式(color mode/ color space)。 这个颜色模式是打印行业需要的。如果不是则禁止上传,并提示用户。


      一开始我以为这个应该存储在 exif 文件信息中, 去拿一下就好了, 但是简单测试发现两个问题:



      1. 文件是否携带 exif 信息是不确定的, 即便出自设计师导出文件, 有可能也是不携带颜色模式信息的。

      2. 除此之外, 依靠 exif 信息去判断,严格来说,即便携带,也是不准确的, 因为这个信息是可以被人为修改的。


      经过一番研究, 我暂时发现可能有两种方式,去达成目的。 但是这篇文章实际不是以解决问题为导向,而是期望尽可能的深入一丢丢。 如果急于找到解决方案, 直接翻到文章底部查看具体 编码实现 即可。


      什么是 CMYK 颜色模式?



      了解 Photoshop 颜色模式 (adobe.com)



      CMYK 是一种颜色模式,它表示四种颜色通道:青色(Cyan)、品红色(Magenta)、黄色(Yellow)和黑色(Key,通常表示黑色)。这种颜色模式主要用于印刷和彩色印刷工作中。


      以下是 CMYK 颜色模式中各颜色通道的简要介绍:



      1. 青色 (Cyan): 表示蓝绿色。在印刷中,它用于调整蓝色和绿色的浓度。

      2. 品红色 (Magenta): 表示品红或洋红色。在印刷中,它用于调整红色和蓝色的浓度。

      3. 黄色 (Yellow): 表示黄色。在印刷中,它用于调整红色和绿色的浓度。

      4. 黑色 (Key): 通常表示黑色。在印刷中,黑色是通过使用黑色油墨单独添加的,以增加图像的深度和对比度。在 CMYK 模式中,K 代表 Key,以避免与蓝色 (B) 冲突。


      这四个颜色通道可以叠加在一起以创建各种颜色。通过调整每个通道的浓度,可以实现广泛的颜色表达。CMYK 被广泛用于印刷领域,因为它能够准确地模拟很多颜色,并提供了在印刷过程中需要的色彩控制。


      与 RGB(红绿蓝)颜色模式不同,CMYK 是一种适合印刷的颜色模式,因为它更好地反映了油墨混合的方式,并考虑到印刷物质上的光的特性


      怎么在web判断一个 jpeg/jpg 文件 颜色模式是否 cmyk ?


      简单说一下这两种方法, 实际上是同一种原理, 因为对于一张图片而言, 它除了携带有 exif 文件元信息之外, 还有文件头信息。


      既然不能通过 exif 元信息去判断, 那么我们可以通过文件头信息去做判断。


      首先,简单测试可以发现, 即便一个 cmyk 图片没有 exif 描述元信息标识这是一个 cmyk 颜色模式的图片, 但是 各种设计类软件都能够标记出来。 以ps为例:


      image-20231128163932682.png
      但是 exif 信息中是没有的:


      image-20231128164033843.png


      甚至一些解析库,就连最基本携带的元信息都没读出来:



      stackblitz.com/edit/exif-j…



      image-20231128164214625.png


      为什么设计软件可以标记出这个图片是否是 cmyk 颜色模式?


      这个问题, 我在网上翻了很久,确实是找不到相关文章有阐述设计软件的原理。 不过Ai 的回答是这样的, 具备一定的参考性:



      有朋友找到了记得踢我一脚,这里提前感谢啦~



      image-20231128174834089.png


      用 ImageMagic 解析图片文件



      什么是 imageMagic ?


      ImageMagick 主要由大量的命令行程序组成,而不提供像 Adobe Photoshop、GIMP 这样的图形界面。它还为很多程序语言提供了 API 库。


      ImageMagick 的功能包括:



      • 查看、编辑位图文件

      • 进行图像格式转换

      • 图像特效处理

      • 图像合成

      • 图像批处理


      ImageMagick 广泛用于图像处理、图形设计、Web 开发等领域。它是许多开源软件项目的重要组成部分,例如 GIMP、Inkscape、Linux 系统中的图像工具等。


      ImageMagick 的优势包括:



      • 功能强大,支持多种图像格式和图像处理功能

      • 开放源代码,免费使用

      • 、、可移植性强,支持多种操作系统



      @jayce: imageMagic 类似于 ffmpeg, 只不过它专注图像处理




      我们可以利用 ImageMagic 的 identify 工具命令 去解析图片以查看一些信息:


      image-20231128180008082.png


      加上 -verbose 选项可以查看更多详细信息:


      $ ./magick identify -verbose ./CMYK.jpg
      $ ./magick identify -verbose ./RGB.jpg

      image-20231129092244504.png


      这些数据是什么? 从哪里解析出来的呢? 这个需要看一下 jpeg 文件的一些标准文件结构


      ISO/IEC 10918-1 和 ISO/IEC 10918-5


      这两个文件都是 JPEG 的标准文档,只是不同的部分,wiki 上对二者描述大致是 5 是 对 1 的很多细节的展开和补充。是补充规范


      JPEG File Interchange Format (JFIF) 和 Exif


      JFIF(JPEG File Interchange Format)和 EXIF(Exchangeable image file format)是两种与 JPEG 图像相关的标准,但它们具有不同的目的和功能。


      JFIF 是一个图片文件格式标准, 它被发布于 10918-5, 是对 10918-1 的细节补充。



      1. JFIF (JPEG File Interchange Format):

        • 目的: JFIF 是一种用于在不同设备和平台之间交换 JPEG 图像的简单格式。它定义了 JPEG 文件的基本结构,以确保文件在不同系统中的一致性和可互操作性。

        • 特点: JFIF 文件通常包含了基本的图像数据,但不一定包含元数据信息。它主要关注图像的编码和解码,而不太关心图像的其他详细信息。JFIF 文件通常使用 .jpg 或 .jpeg 扩展名。



      2. EXIF (Exchangeable image file format):

        • 目的: EXIF 是一种在数字摄影中广泛使用的标准,用于嵌入图像文件中的元数据信息。这些元数据可以包括拍摄日期、相机型号、曝光时间、光圈值等。EXIF 提供了更丰富的信息,有助于记录和存储与拍摄有关的详细数据。

        • 特点: EXIF 数据以二进制格式嵌入在 JPEG 图像中,提供了关于图像和拍摄条件的详细信息。这对于数字相机和其他支持 EXIF 的设备非常有用。EXIF 文件通常使用 .jpg 或 .jpeg 扩展名。




      JPEG 文件标准结构语法


      jpeg 作为压缩数据结构, 是一个非常复杂的数据组织, 我们的关注点只在关系到我们想要解决的问题。 标准文档 ISO/IEC 10918-1 : 1993(E).中有部分相关说明。


      概要:


      结构上来说, jpeg 的数据格式由以下几个部分,有序组成: parameters, markers, 以及 entropy-coded data segments, 其中 parameters 和 markers 部分通常被组织到 marker segments, 因为它们都是用字节对齐的代码表, 都是由8位字节的有序序列组成。


      Parameters


      这部分携带有参数编码关键信息, 是图片成功被解析的关键。


      Markers


      Markers 标记用于标识压缩数据格式的各种结构部分。大多数标记开始包含一组相关参数的标记段;有些标记是单独存在的。所有标记都被分配了两个字节的代码


      例如 SOI : 从 0xFF,0xD8这两个字节开始,标记为图片文件的文件头开始, SOF0: 从 0xFF, 0xD8这两个字节开始,标记了 ”帧“ 的开始,它实际上会携带有图片的一些基本信息, 例如宽高,以及颜色通道等。 这个颜色通道其实也是我们主要需要关注的地方。


      下表是完整的标记代码:


      image-20231129095415255.png



      @refer:


      http://www.digicamsoft.com/itu/itu-t81…
      http://www.digicamsoft.com/itu/itu-t81…



      wiki 上也有相关的帧头部字段说明:


      Short nameBytesPayloadNameComments
      SOI0xFF, 0xD8noneStart Of Image
      SOF00xFF, 0xC0variable sizeStart Of Frame (baseline DCT)Indicates that this is a baseline DCT-based JPEG, and specifies the width, height, number of components, and component subsampling (e.g., 4:2:0).
      SOF20xFF, 0xC2variable sizeStart Of Frame (progressive DCT)Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, and component subsampling (e.g., 4:2:0).
      DHT0xFF, 0xC4variable sizeDefine Huffman Table(s)Specifies one or more Huffman tables.
      DQT0xFF, 0xDBvariable sizeDefine Quantization Table(s)Specifies one or more quantization tables.
      DRI0xFF, 0xDD4 bytesDefine Restart IntervalSpecifies the interval between RSTn markers, in Minimum Coded Units (MCUs). This marker is followed by two bytes indicating the fixed size so it can be treated like any other variable size segment.
      SOS0xFF, 0xDAvariable sizeStart Of ScanBegins a top-to-bottom scan of the image. In baseline DCT JPEG images, there is generally a single scan. Progressive DCT JPEG images usually contain multiple scans. This marker specifies which slice of data it will contain, and is immediately followed by entropy-coded data.
      RSTn0xFF, 0xDn (n=0..7)noneRestartInserted every r macroblocks, where r is the restart interval set by a DRI marker. Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7.
      APPn0xFF, 0xEnvariable sizeApplication-specificFor example, an Exif JPEG file uses an APP1 marker to store metadata, laid out in a structure based closely on TIFF.
      COM0xFF, 0xFEvariable sizeCommentContains a text comment.
      EOI0xFF, 0xD9noneEnd Of Image


      Syntax and structure



      整体结构


      image-20231129111546427.png



      @refer: http://www.digicamsoft.com/itu/itu-t81…



      Frame Header


      image-20231129111645470.png


      image-20231129112439294.png


      image-20231129112306831.png



      @refer: http://www.digicamsoft.com/itu/itu-t81…



      SOFn : 帧开始标记标记帧参数的开始。下标n标识编码过程是基线顺序、扩展顺序、渐进还是无损,以及使用哪种熵编码过程。


      在其标准文档中,我们有找到 SOFn 的子字段说明,不过在其他地方,倒是看到了不少描述:


      特别是在这里 JPEG File Layout and Format


      image-20231129141121729.png


      可以看到,在 SOFn 这个标记中, 有一个字段为会指明 components 的数量,它代表的实际上颜色通道, 如果是 1,那么就是灰度图, 如果是3,那就是RGB, 如果是 4 就是 CMYK.


      到这里我们就知道了, 我们可以读取到这个对应的字节段,从而判断一个图片的颜色模式了。


      怎么读取呢?


      这篇资料说了明了 Jpeg 文件格式中字节和上述字段的关联关系: Anatomy of a JPEG


      注意这篇资料中有一段描述,会影响到我们后续的逻辑判断:


      image-20231129142053598.png



      就是 SOF0 是必须的,但是可以被 SOFn>=1 替换。 所以在做逻辑判断的时候,后续的也要判断。



      我们可以先大概看看一个图片文件的字节流数据长什么样子:(因为所有的字段都是 FF 字节位开头,所以高亮了)


      1701248911601.png



      以上页面可以在这里访问: jaycethanks.github.io/demos/DemoP…



      但样太不便于阅读了, 而且实在太长了。 这里有个网站 here,可以将关键的字节段截取出来:


      image-20231129171534152.png


      我们主要看这里:


      image-20231129171621345.png
      可以看到 components 为 4.


      如果是 RGB:


      image-20231129171722071.png


      这里就是 3,


      如果是灰度图,components 就会是1


      image-20231129172500605.png


      EXIF 在哪里?


      一个额外的小问题, 我们常见的 exif 元信息存储在哪里呢?


      其实上面的 Markers 部分给出的表格中也说明了 ,在 Appn 中可以找到 exif 信息, 但是wiki 上说的是 App0, 在这个解析网站中,我们可以看到:


      image-20231201113938640.png


      编码实现


      有了上述具体的分析, 我们就能有大致思路, 这里直接给出相关代码:



      代码参考 github.com/zengming00/node-jpg-is-cmyk




      /**
      *
      @refer https://github.com/zengming00/node-jpg-is-cmyk/blob/master/src/index.ts

      *
      @refer https://cyber.meme.tips/jpdump/#
      *
      @refer https://mykb.cipindanci.com/archive/SuperKB/1294/JPEG%20File%20Layout%20and%20Format.htm
      *
      @refer https://www.ccoderun.ca/programming/2017-01-31_jpeg/
      *
      * 通过 jpg 文件头判断是否是 CMYK 颜色模式
      *
      @param { Uint8Array } data
      */

      function checkCmyk(data: Uint8Array) {
      let pos = 0;
      while (pos < data.length) {
      pos++;
      switch (data[pos]) {
      case 0xd8: {// SOI - Start of Image
      pos++;
      break;
      }
      case 0xd9: {// EOI - End of Image
      pos++;
      break;
      }
      case 0xc0: // SOF0 - Start of Frame, Baseline DCT
      case 0xc1: // SOF1 - Start of Frame, Extended Sequential DCT
      case 0xc2: { // SOF2 - Start of Frame, Progressive DCT
      pos++;
      const len = (data[pos] << 8) | data[pos + 1];
      const compoNum = data[pos + 7];
      if (compoNum === 4) {
      // 如果components 数量为4, 那么就认为是 cmyk
      return true;
      }
      pos += len;
      break;
      }
      case 0xc4: { // DHT - Define Huffman Table
      pos++;
      const len = (data[pos] << 8) | data[pos + 1];
      pos += len;
      break;
      }
      case 0xda: { // SOS - Start of Scan
      pos++;
      const len = (data[pos] << 8) | data[pos + 1];
      pos += len;
      break;
      }
      case 0xdb: { // DQT - Define Quantization Table
      pos++;
      const len = (data[pos] << 8) | data[pos + 1];
      pos += len;
      break;
      }
      case 0xdd: { // DRI - Define Restart Interval
      pos++;
      const len = (data[pos] << 8) | data[pos + 1];
      pos += len;
      break;
      }
      case 0xe0: { // APP0 - Application-specific marker
      pos++;
      const len = (data[pos] << 8) | data[pos + 1];
      pos += len;
      break;
      }
      case 0xfe: { // COM - Comment
      pos++;
      const len = (data[pos] << 8) | data[pos + 1];
      pos += len;
      break;
      }
      default: {
      pos++;
      const len = (data[pos] << 8) | data[pos + 1];
      pos += len;
      }
      }
      }
      return false;
      }

      有没有其他的方法?


      既然 imageMagic 这么成熟且强大, 我们有办法利用它来做判断吗?


      我们可以通过 wasm, 在web中去利用这些工具, 我找到了 WASM-ImageMagick 这个, 但是他的打包好像有些问题 vite 引入的时候会报错,看着好像也没有要修复的意思, issue 里面有老哥自己修改了打包配置进行了修复在这里: image-magick


      我们就写的demo测试函数:


      import * as Magick from '@xn-sakina/image-magick'

      export default function (file: File) {
      if (!file) return;
      // 创建FileReader对象
      var reader = new FileReader();
      // 当读取完成时的回调函数
      reader.onload = async function (e) {
      // 获取ArrayBuffer
      var arrayBuffer = e.target?.result as ArrayBuffer;
      if (arrayBuffer) {
      // 将 ArrayBuffer 转换为 Uint8Array
      const sourceBytes = new Uint8Array(arrayBuffer);
      const inputFiles = [{ name: 'srcFile.png', content: sourceBytes }]
      let commands: string[] = ["identify srcFile.png"]
      const { stdout } = await Magick.execute({inputFiles, commands});

      // 这里打印一下结果
      console.log('stdout:',stdout[0])

      }
      };
      // 读取文件为ArrayBuffer
      reader.readAsArrayBuffer(file);
      }

      import isCmyk from '../utils/isCmyk.ts'
      const handleFileChange = (e: Event) => {
      const file = (e.target as HTMLInputElement)?.files?.[0]
      isCmyk(file) // 这里文件上传调用一下
      ......

      测试几个文件


      image-20231130104941592.png


      可以看到, Gray, RGB, CMYK 检测都可以正常输出, 说明可以这么干。



      但是这个库, 文档写的太乱了。 - -



      这个库的大小有 5 m之大 - -, npm 上找了下, 目前相关的包,也没有比这个更小的好像。


      作者:sun_zy
      来源:jaycethanks.github.io/blog_11ty/posts/Others/%E6%A3%80%E6%B5%8B%E5%9B%BE%E7%89%87%E6%98%AF%E5%90%A6cmyk/
      收起阅读 »

      需求小能手——拦截浏览器窗口关闭

      web
      前言 最近碰到一个需求,网页端页面有评价功能,要求用户点击关闭浏览器时强制弹出评价对话框让用户评价。刚听到这个需求我大意了没有闪,以为很简单,没想到很难实现,很多需求果然不能想当然啊,接下来我们来看一下该功能实现的一些思路。 窗口关闭 要想实现该功能最简单的想...
      继续阅读 »

      前言


      最近碰到一个需求,网页端页面有评价功能,要求用户点击关闭浏览器时强制弹出评价对话框让用户评价。刚听到这个需求我大意了没有闪,以为很简单,没想到很难实现,很多需求果然不能想当然啊,接下来我们来看一下该功能实现的一些思路。


      窗口关闭


      要想实现该功能最简单的想法就是监听浏览器关闭事件,然后阻止默认事件,执行自定义的事件。整个思路核心就是监听事件,搜索一番果然有浏览器关闭触发的事件。


      事件



      • onunload:资源被卸载时触发,浏览器窗口关闭时卸载资源就会触发,我们监听一下该事件看能不能阻止窗口关闭。


          window.addEventListener('unload', function (e) {
      console.log(e);
      e.preventDefault()
      });

      打开页面再关闭,会发现控制台打印出了e然后就关闭了,看来在onunload事件中并不能阻止窗口关闭,得另找方法,刚好在onunload事件介绍中还链接了一个事件——beforeonunlaod。



      • beforeunload :当窗口关闭或刷新时触发,该事件在onunload之前触发。并且在该事件中可以弹出对话框,询问用户是否确认离开或者重新加载,这不是正是我们想要的效果。根据mdn上的介绍,要想出现弹出对话看需要用preventDefault()事件,并且为了兼容性我们最好再加上以下方法中的一个:

        1.将e.renturenValue赋一个字符串。

        2.事件函数返回一个字符串。
        接下来让我们试一试:


          window.addEventListener('beforeunload', function (e) {
      e.preventDefault()
      e.returnValue = ''
      });

      打开关闭未生效,再检查下代码没问题呀,这是因为浏览器本身安全机制导致的,在ie浏览器中没有任何限制,但是在chrome、edge等浏览器中用户必须在短时间操作过页面才能触发。打开页面点几个文字在关闭窗口,这次就能出现弹窗了。

      2(W_WV8AVWRT3(4R1HWBRR7.png

      当我们点击离开页面就会关闭,点击取消继续停留,上面提到过刷线也能触发,我们再点下刷新。

      T456)ZI7MJ2XK1X3M%BE7SN.png

      出现的提示有所改变,我们知道浏览器的刷新有好几种方式,我们可以都尝试一下:



      • ctrl+R:本身就是浏览器刷新按钮的快捷键,能够触发。

      • f5:能否触发。

      • 前进、后退:能够触发。

        这三种方式提示内容跟点击刷新按钮一样。回到我们的需求,虽然已经能够阻止窗口关闭,但是刷新依旧能阻止,我们需求是用户关闭,所以我们要区分用户操作是刷新还是关闭。


      区分


      要想区分就要找到以下两者之间的区别,两者都会执行onbeforeunload与onunload两个事件,不能直接通过某个事件区分。但是两个事件之间的时间差是不同的。刷新时两者时间差在10毫秒左右,而关闭时在3毫秒左右,判断以下时间差就能区分出来。


             var time = null;
      window.addEventListener('beforeunload', function (e) {
      time = new Date().getTime();
      });
      window.addEventListener('unload', function (e) {
      const nowTime = new Date().getTime();
      if (nowTime - time < 5) {
      console.log('窗口关闭');
      }
      });

      用此方法就能区分出来,但是此判断是在onunload事件中的,而窗口弹出是在beforeunlaod,这方法只适用于在关闭时执行某个函数,但不能满足我们的需求。除此之外还有一个问题就是刷新默认弹出对话框的内容是不能修改的,所以如果我们想要弹出自定义的对话框是不可能的。经过分析操作能够做到的就是,在用户刷新或关闭时出现系统自带对话框,同时在下方弹出自定义对话框,然后用户点击取消再去操作自定义对话框。


      总结


      总的来说要想拦截浏览器窗口关闭并且弹出自定义对话框,目前我还没有完美的实现方案,只能带有众多缺陷的去实现。如果我们只是想在关闭窗口前执行函数那就使用时间差区分即可。


      作者:躺平使者
      来源:juejin.cn/post/7281912738862481448
      收起阅读 »

      前端实现文件预览img、docx、xlsx、ppt、pdf、md、txt、audio、video

      web
      前言 最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇 具体的预览需求: 预览需要支持的文件类型有: png、jpg、jpeg...
      继续阅读 »

      前言



      最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇



      具体的预览需求:
      预览需要支持的文件类型有: png、jpg、jpeg、docx、xlsx、ppt、pdf、md、txt、audio、video,另外对于不同文档还需要有定位的功能。例如:pdf 定位到页码,txtmarkdown定位到文字并滚动到指定的位置,音视频定位到具体的时间等等。




      ⚠️ 补充: 我的需求是需要先将文件上传到后台,然后我拿到url地址去展示,对于markdowntxt的文件需要先用fetch获取,其他的展示则直接使用url链接就可以。


      不同文件的实现方式不同,下面分类讲解,总共分为以下几类:



      1. 自有标签文件:png、jpg、jpeg、audio、video

      2. 纯文字的文件: markdown & txt

      3. office 类型的文件: docx、xlsx、ppt

      4. embed 引入文件:pdf

      5. iframe:引入外部完整的网站




      自有标签文件:png、jpg、jpeg、audio、video



      对于图片、音视频的预览,直接使用对应的标签即可,如下:



      图片:png、jpg、jpeg


      示例代码:


       <img src={url} key={docId} alt={name} width="100%" />;

      预览效果如下:


      截屏2024-04-30 11.18.01.png


      音频:audio


      示例代码:


      <audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
      <track kind="captions" />
      <source src={url} type="audio/mpeg" />
      </audio>

      预览效果如下:


      截屏2024-04-30 11.18.45.png


      视频:video


      示例代码:


      <video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
      <track kind="captions" />
      <source src={url} type="video/mp4" />
      </video>

      预览效果如下:


      截屏2024-05-13 18.21.13.png


      关于音视频的定位的完整代码:


      import React, { useRef, useEffect } from 'react';

      interface IProps {
      type: 'audio' | 'video';
      url: string;
      timeInSeconds: number;
      }

      function AudioAndVideo(props: IProps) {
      const { type, url, timeInSeconds } = props;
      const videoRef = useRef<HTMLVideoElement>(null);
      const audioRef = useRef<HTMLAudioElement>(null);

      useEffect(() => {
      // 音视频定位
      const secondsTime = timeInSeconds / 1000;
      if (type === 'audio' && audioRef.current) {
      audioRef.current.currentTime = secondsTime;
      }
      if (type === 'video' && videoRef.current) {
      videoRef.current.currentTime = secondsTime;
      }
      }, [type, timeInSeconds]);

      return (
      <div>
      {type === 'audio' ? (
      <audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
      <track kind="captions" />
      <source src={url} type="audio/mpeg" />
      </audio>
      ) : (
      <video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
      <track kind="captions" />
      <source src={url} type="video/mp4" />
      </video>
      )}
      </div>
      );
      }

      export default AudioAndVideo;



      纯文字的文件: markdown & txt



      对于markdown、txt类型的文件,如果拿到的是文件的url的话,则无法直接显示,需要请求到内容,再进行展示。



      markdown 文件



      在展示markdown文件时,需要满足字体高亮、代码高亮、如果有字体高亮,需要滚动到字体所在位置、如果有外部链接,需要新开tab页面再打开。



      需要引入两个库:


      marked:它的作用是将markdown文本转换(解析)为HTML


      highlight: 它允许开发者在网页上高亮显示代码。


      字体高亮的代码实现:



      高亮的样式,可以在行间样式定义



        const highlightAndMarkFirst = (text: string, highlightText: string) => {
      let firstMatchDone = false;
      const regex = new RegExp(`(${highlightText})`, 'gi');
      return text.replace(regex, (match) => {
      if (!firstMatchDone) {
      firstMatchDone = true;
      return `<span id='first-match' style="color: red;">${match}</span>`;
      }
      return `<span style="color: red;">${match}</span>`;
      });
      };

      代码高亮的代码实现:



      需要借助hljs这个库进行转换



      marked.use({
      renderer: {
      code(code, infostring) {
      const validLang = !!(infostring && hljs.getLanguage(infostring));
      const highlighted = validLang
      ? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
      : code;
      return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
      }
      },
      });

      链接跳转新tab页的代码实现:


      marked.use({
      renderer: {
      // 链接跳转
      link(href, title, text) {
      const isExternal = !href.startsWith('/') && !href.startsWith('#');
      if (isExternal) {
      return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
      }
      return `<a href="${href}" title="${title}">${text}</a>`;
      },
      },
      });

      滚动到高亮的位置的代码实现:



      需要配合上面的代码高亮的方法



      const firstMatchElement = document.getElementById('first-match');
      if (firstMatchElement) {
      firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }

      完整的代码如下:



      入参的docUrlmarkdown文件的线上url地址,searchText 是需要高亮的内容。



      import React, { useEffect, useState, useRef } from 'react';
      import { marked } from 'marked';
      import hljs from 'highlight.js';

      const preStyle = {
      width: '100%',
      maxHeight: '64vh',
      minHeight: '64vh',
      overflow: 'auto',
      };

      // Markdown展示组件
      function MarkdownViewer({ docUrl, searchText }: { docUrl: string; searchText: string }) {
      const [markdown, setMarkdown] = useState('');
      const markdownRef = useRef<HTMLDivElement | null>(null);

      const highlightAndMarkFirst = (text: string, highlightText: string) => {
      let firstMatchDone = false;
      const regex = new RegExp(`(${highlightText})`, 'gi');
      return text.replace(regex, (match) => {
      if (!firstMatchDone) {
      firstMatchDone = true;
      return `<span id='first-match' style="color: red;">${match}</span>`;
      }
      return `<span style="color: red;">${match}</span>`;
      });
      };

      useEffect(() => {
      // 如果没有搜索内容,直接加载原始Markdown文本
      fetch(docUrl)
      .then((response) => response.text())
      .then((text) => {
      const highlightedText = searchText ? highlightAndMarkFirst(text, searchText) : text;
      setMarkdown(highlightedText);
      })
      .catch((error) => console.error('加载Markdown文件失败:', error));
      }, [searchText, docUrl]);

      useEffect(() => {
      if (markdownRef.current) {
      // 支持代码高亮
      marked.use({
      renderer: {
      code(code, infostring) {
      const validLang = !!(infostring && hljs.getLanguage(infostring));
      const highlighted = validLang
      ? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
      : code;
      return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
      },
      // 链接跳转
      link(href, title, text) {
      const isExternal = !href.startsWith('/') && !href.startsWith('#');
      if (isExternal) {
      return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
      }
      return `<a href="${href}" title="${title}">${text}</a>`;
      },
      },
      });
      const htmlContent = marked.parse(markdown);
      markdownRef.current!.innerHTML = htmlContent as string;
      // 当markdown更新后,检查是否需要滚动到高亮位置
      const firstMatchElement = document.getElementById('first-match');
      if (firstMatchElement) {
      firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
      }
      }, [markdown]);

      return (
      <div style={preStyle}>
      <div ref={markdownRef} />
      </div>

      );
      }

      export default MarkdownViewer;

      预览效果如下:


      截屏2024-05-13 17.59.04.png


      txt 文件预览展示



      支持高亮和滚动到指定位置



      支持高亮的代码:


        function highlightText(text: string) {
      if (!searchText.trim()) return text;
      const regex = new RegExp(`(${searchText})`, 'gi');
      return text.replace(regex, `<span style="color: red">$1</span>`);
      }

      完整代码:


      import React, { useEffect, useState, useRef } from 'react';
      import { preStyle } from './config';

      function TextFileViewer({ docurl, searchText }: { docurl: string; searchText: string }) {
      const [paragraphs, setParagraphs] = useState<string[]>([]);
      const targetRef = useRef<HTMLDivElement | null>(null);

      function highlightText(text: string) {
      if (!searchText.trim()) return text;
      const regex = new RegExp(`(${searchText})`, 'gi');
      return text.replace(regex, `<span style="color: red">$1</span>`);
      }

      useEffect(() => {
      fetch(docurl)
      .then((response) => response.text())
      .then((text) => {
      const highlightedText = highlightText(text);
      const paras = highlightedText
      .split('\n')
      .map((para) => para.trim())
      .filter((para) => para);
      setParagraphs(paras);
      })
      .catch((error) => {
      console.error('加载文本文件出错:', error);
      });
      // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [docurl, searchText]);

      useEffect(() => {
      // 处理高亮段落的滚动逻辑
      const timer = setTimeout(() => {
      if (targetRef.current) {
      targetRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
      }, 100);

      return () => clearTimeout(timer);
      }, [paragraphs]);

      return (
      <div style={preStyle}>
      {paragraphs.map((para: string, index: number) => {
      const paraKey = para + index;

      // 确定这个段落是否包含高亮文本
      const isTarget = para.includes(`>${searchText}<`);
      return (
      <p key={paraKey} ref={isTarget && !targetRef.current ? targetRef : null}>
      <div dangerouslySetInnerHTML={{ __html: para }} />
      </p>
      );
      })}
      </div>

      );
      }

      export default TextFileViewer;

      预览效果如下:


      截屏2024-05-13 18.34.27.png




      office 类型的文件: docx、xlsx、ppt



      docx、xlsx、ppt 文件的预览,用的是office的线上预览链接 + 我们文件的线上url即可。




      关于定位:用这种方法我暂时尝试是无法定位页码的,所以定位的功能我采取的是后端将office 文件转成pdf,再进行定位,如果只是纯展示,忽略这个问题即可。



      示例代码:


      <iframe
      src={`https://view.officeapps.live.com/op/view.aspx?src=${url}`}
      width="100%"
      height="500px"
      frameBorder="0"
      ></iframe>

      预览效果如下:


      截屏2024-05-07 17.58.45.png




      embed 引入文件:pdf



      pdf文档预览时,可以采用embed的方式,这个httpsUrl就是你的pdf文档的链接地址



      示例代码:


       <embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;

      关于定位,其实是地址上拼接的页码sourcePage,如下:


       const httpsUrl = sourcePage
      ? `${doc.url}#page=${sourcePage}`
      : doc.url;

      <embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;


      预览效果如下:


      截屏2024-05-07 17.50.07.png




      iframe:引入外部完整的网站



      除了上面的各种文件,我们还需要预览一些外部的网址,那就要用到iframe的方式



      示例代码:


       <iframe
      title="网址"
      width="100%"
      height="100%"
      src={doc.url}
      allow="microphone;camera;midi;encrypted-media;"/>


      预览效果如下:


      截屏2024-05-07 17.51.26.png




      总结: 到这里我们支持的所有文件都讲述完了,有什么问题,欢迎评论区留言!


      作者:玖月晴空
      来源:juejin.cn/post/7366432628440924170
      收起阅读 »

      28个令人惊艳的JavaScript单行代码

      web
      JavaScript作为一种强大而灵活的脚本语言,充满了许多令人惊艳的特性。本文将带你探索28个令人惊艳的JavaScript单行代码,展示它们的神奇魅力。 1. 阶乘计算 使用递归函数计算给定数字的阶乘。 const factorial = n => ...
      继续阅读 »

      JavaScript作为一种强大而灵活的脚本语言,充满了许多令人惊艳的特性。本文将带你探索28个令人惊艳的JavaScript单行代码,展示它们的神奇魅力。


      1. 阶乘计算


      使用递归函数计算给定数字的阶乘。


      const factorial = n => n === 0 ? 1 : n * factorial(n - 1);
      console.log(factorial(5)); // 输出 120

      2. 判断一个变量是否为对象类型


      const isObject = variable === Object(variable);

      3. 数组去重


      利用Set数据结构的特性,去除数组中的重复元素。


      const uniqueArray = [...new Set(array)];

      4. 数组合并


      合并多个数组,创建一个新的数组。


      const mergedArray = [].concat(...arrays);

      5. 快速最大值和最小值


      获取数组中的最大值和最小值。


      const max = Math.max(...array);
      const min = Math.min(...array);

      6. 数组求和


      快速计算数组中所有元素的和。


      const sum = array.reduce((acc, cur) => acc + cur, 0);

      7. 获取随机整数


      生成一个指定范围内的随机整数。


      const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

      8. 反转字符串


      将字符串反转。


      const reversedString = string.split('').reverse().join('');

      9. 检查回文字符串


      判断一个字符串是否为回文字符串。


      const isPalindrome = string === string.split('').reverse().join('');

      10. 扁平化数组


      将多维数组转换为一维数组。


      const flattenedArray = array.flat(Infinity);

      11. 取随机数组元素


      从数组中随机取出一个元素。


      const randomElement = array[Math.floor(Math.random() * array.length)];

      12. 判断数组元素唯一


      检查数组中的元素是否唯一。


      const isUnique = array.length === new Set(array).size;

      13. 字符串压缩


      将字符串中重复的字符进行压缩。


      const compressedString = string.replace(/(.)\1+/g, match => match[0] + match.length);

      14. 生成斐波那契数列


      生成斐波那契数列的前n项。


      const fibonacci = Array(n).fill().map((_, i, arr) => i <= 1 ? i : arr[i - 1] + arr[i - 2]);

      15. 数组求交集


      获取多个数组的交集。


      const intersection = arrays.reduce((acc, cur) => acc.filter(value => cur.includes(value)));

      16. 验证邮箱格式


      检查字符串是否符合邮箱格式。


      const isValidEmail = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/.test(email);

      17. 数组去除假值


      移除数组中的所有假值,如falsenull0""undefined


      const truthyArray = array.filter(Boolean);

      18. 求阶乘


      计算一个数的阶乘。


      const factorial = n => n <= 1 ? 1 : n * factorial(n - 1);

      19. 判断质数


      检查一个数是否为质数。


      const isPrime = n => ![...Array(n).keys()].slice(2).some(i => n % i === 0);

      20. 检查对象是空对象


      判断对象是否为空对象。


      const isEmptyObject = Object.keys(object).length === 0 && object.constructor === Object;

      21. 判断回调函数为真


      检查数组中的每个元素是否满足特定条件。


      const allTrue = array.every(condition);

      22. 检查回调函数为假


      检查数组中是否有元素满足特定条件。


      const anyFalse = array.some(condition);

      23. 数组排序


      对数组进行排序。


      const sortedArray = array.sort((a, b) => a - b);

      24. 日期格式化


      将日期对象格式化为指定格式的字符串。


      const formattedDate = new Date().toISOString().slice(0, 10);

      25. 将字符串转为整数类型


      const intValue = +str;

      26. 计算数组中元素出现的次数


      统计数组中各元素的出现次数。


      const countOccurrences = array.reduce((acc, cur) => (acc[cur] ? acc[cur]++ : acc[cur] = 1, acc), {});

      27. 交换两个变量的值


      [a, b] = [b, a];

      28. 利用逗号运算符分隔多个表达式


      const result = (expression1, expression2, ..., expressionN);

      作者:慕仲卿
      来源:juejin.cn/post/7307963529872605218
      收起阅读 »

      如何快速实现多行文本擦除效果

      web
      今天来实现一个多行文本擦除的效果,有种经典咏流传节目中表演开始前阅读诗句的一些既视感,在工作中其实也遇到过这样的需求当时是用的其他方法来实现的,现在发现了更简单的一种方法并且里面也涵盖了不少的知识点。 以上就是最终要实现的效果,比较敏感的同学呢应该能看到文本...
      继续阅读 »

      今天来实现一个多行文本擦除的效果,有种经典咏流传节目中表演开始前阅读诗句的一些既视感,在工作中其实也遇到过这样的需求当时是用的其他方法来实现的,现在发现了更简单的一种方法并且里面也涵盖了不少的知识点。


      img1.gif


      以上就是最终要实现的效果,比较敏感的同学呢应该能看到文本是由歌词组成的哈哈,没错今天是我偶像发新歌的一天,就用歌词来致敬一下吧!


      思路


      首先先来捋一下思路,乍一看效果好像只有一段文本,但其实是由两段相同文本组成的。



      1. 两段相同文本组成,这是为了让它们实现重合,第二段文本会覆盖在第一段文本上。

      2. 修改第二段文本背景色为渐变色。

      3. 最后对渐变颜色的背景色添加动画效果。


      先来搭建一下结构部分:


      <body>
      <div class="container">
      <p>
      失去你以来 万物在摇摆 你指的山海 像玩具一块一块 我是你缔造又提防的AI 如果我存在 是某种伤害
      不被你所爱 也不能具象出来 我想拥有你说失去过谁的 那种痛感 失去你以来 万物在摇摆 你指的山海 像玩具一块一块我是你缔造又提防的AI 如果我存在 只对你无害 想做你所爱 再造你要的时代 执行你最初设计我的大概
      成为主宰 失去你以来 万物在摇摆 你指的山海 像玩具一块一块 也许我本来 就是种伤害 我终于明白 我根本就不存在 谁不在造物主设置的循环 活去死来
      </p>
      <p class="eraser">
      <span class="text">
      失去你以来 万物在摇摆 你指的山海 像玩具一块一块 我是你缔造又提防的AI 如果我存在 是某种伤害
      不被你所爱 也不能具象出来 我想拥有你说失去过谁的 那种痛感 失去你以来 万物在摇摆 你指的山海 像玩具一块一块我是你缔造又提防的AI 如果我存在 只对你无害 想做你所爱 再造你要的时代
      执行你最初设计我的大概
      成为主宰 失去你以来 万物在摇摆 你指的山海 像玩具一块一块 也许我本来 就是种伤害 我终于明白 我根本就不存在 谁不在造物主设置的循环 活去死来
      </span>
      </p>
      </div>
      </body>

      代码中两段文本都是由p标签包裹,第二段中加入了一个span标签是因为后面修改背景色的时候凸显出行的效果,这个下面加上样式后就看到了。


      添加样式:


      * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      }

      body {
      background: #000;
      color: #fff;
      }

      .container {
      width: 60%;
      text-indent: 20px;
      line-height: 2;
      font-size: 18px;
      margin: 30px auto;
      }

      img2.png


      现在只需要给第二段增加一个定位效果即可实现文本的覆盖:


      * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      }

      body {
      background: #000;
      color: #fff;
      }

      .container {
      width: 60%;
      /* 直接加在父元素中即可对所有块级元素的子元素进行首行缩进 */
      text-indent: 20px;
      line-height: 2;
      font-size: 18px;
      margin: 30px auto;
      position: relative;
      }

      .eraser {
      position: absolute;
      /* 这里等同于top:0 right:0 bottom:0 left:0 */
      inset: 0;
      /*
      这里解释一下inset属性,inset属性用作定位元素的top、right、bottom 、left这些属性的简写
      依照的也是上右下左的顺序。
      例如:inset:1px 2px 等同于 top:1px right:2px bottom:1px left:2px
      */

      }

      image.png


      那接下来就应该修改背景颜色了。


      以上重复代码省略......

      .text {
      background: #fff;
      }

      这时候给span标签加上背景颜色后会看到:


      image.png


      而不是这样的效果,这就是为什么需要加一个span标签的原因了。


      image.png


      以上重复代码省略......

      .text {
      background: linear-gradient(to right, #0000 10%, #000 10%);
      color:transparent;
      }

      image.png


      下面要调整的就是将渐变里面的百分比变为动态的,我们可以声明一个变量:


      以上重复代码省略......

      .text {
      --p:0%;
      background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px)); // 加上30px显示一个默认的渐变区域
      color:transparent;
      }

      image.png


      下面就该加上动画效果了,在设置动画时改变--p变量的值为100%


      以上重复代码省略......

      .text {
      --p:0%;
      background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px));
      color:transparent;
      animation: erase 8s linear;
      }

      @keyframes erase{
      to{
      --p:100%;
      }
      }

      但是这样写完之后发现并没有出现动画的效果,这是因为css动画中只有数值类的css属性才会生效,这里已经是一个数值了但--p还不是一个属性,所以我们要把他变成一个css属性,可以利用@property规则来帮助我们生成一个-xxx的自定义,它的结构:


      @property 属性名称 {
      syntax: '<类型>'; // 必须
      initial-value: 默认值; // 必须
      inherits: false; // 是否可继承 非必须
      }

      以上重复代码省略......

      .text {
      --p:0%;
      background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px));
      color:transparent;
      animation: erase 8s linear;
      }

      @property --p {
      syntax: '<percentage>';
      initial-value: 0%;
      inherits: false;
      }

      @keyframes erase{
      to{
      --p:100%;
      }
      }

      到此为止也就实现开头的效果了!!!


      作者:孤独的根号_
      来源:juejin.cn/post/7333761832472838144
      收起阅读 »

      超级离谱的前端需求:搜索图片里的文字!!难倒我了!

      web
      前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 是这样的,我们公司有一个平台,这个平台上面有一个页面,是一个我们公司内部存放一些字幕图片的,图片很多,差不多每一页有100张的样子,类似于下面这样的图片 ...
      继续阅读 »

      前言


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


      背景


      是这样的,我们公司有一个平台,这个平台上面有一个页面,是一个我们公司内部存放一些字幕图片的,图片很多,差不多每一页有100张的样子,类似于下面这样的图片



      前几天上面大佬们说想要更加方便快捷地找到某一张图片,怎么个快捷法呢?就是通过搜索文字,能搜索到包含这些文字的图片。。。我一想,这需求简直逆天啊!!!!平时只做过搜索文字的,没做过根据文字搜索出图片的。。。。



      思路


      其实思路很清晰,分析出每一张图片上的文字,并存在对象的keyword中,搜搜的时候去过滤出keyword包含搜索文字的图片即可。


      但是难就难在,我要怎么分析出图片上的文字并存起来呢?


      tesseract.js


      于是我就去网上找找有哪些库可以实现这个功能,你还真别说,还真有!!这个库就是tesseract.js



      tesseract.js 是一个可以分析出图片上文字的一个库,我们通过一个小例子来看看他的使用方式


      首先需要安装这个库


      npm i tesseract.js

      接着引入并使用它解析图片文字,它识别后会返回一个 Promise,成功的话会走 then



      可以看出他直接能把图片上的结果解析出来!!!真的牛逼!!!有了这个,那我轻轻松松就可以完成上面交代的任务了!!!



      实现功能


      我们需要解析每一张图片的文字,并存入 keyword属性中,以供过滤筛选



      可以看到每一张图片都解析得到keyword



      那么搜索效果自然可以完成



      搜索中文呢?


      上面只能解析英文,可以看到有 eng 这个参数,那怎么才能解析中文呢?只需要改成chi_sim即可




      如果你想要中文和英文一起解析,可以这么写eng+chi_sim





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

      设计呀,你是真会给前端找事呀!!!

      web
      背景 设计:我想要的你听明白了吗,你做出来的和我想要的差距很大,你怎么没有一点审美(你个臭男人,你怎么不按我画的做)! 我:啊?这样自适应不是很好吗,适配了大部分机型呀,而且不会有啥显示的兼容性,避免不必要的客户咨询和客户投诉。 设计: 你上一家公司就是因为...
      继续阅读 »

      背景



      • 设计:我想要的你听明白了吗,你做出来的和我想要的差距很大,你怎么没有一点审美(你个臭男人,你怎么不按我画的做)!

      • :啊?这样自适应不是很好吗,适配了大部分机型呀,而且不会有啥显示的兼容性,避免不必要的客户咨询和客户投诉。

      • 设计: 你上一家公司就是因为有你这样的优秀员工才倒闭的吧?!

      • :啊?ntm和产品是一家的是吗?





      我该如何应对


      先看我实现的


      b0nh2-9h1qy.gif


      在看看设计想要的


      9e2b0572-aff4-4644-9eeb-33a9ea76265c.gif
      总结一下:



      • 1.一个的时候宽度固定,不管屏幕多大都占屏幕的一半。

      • 2.俩个的时候,各占屏幕的一半,当屏幕过小的时候两个并排展示换行。

      • 3.三个的时候,上面俩,下面一个,且宽度要一样。

      • 4.大于三个的时候,以此类推。



      有句话叫做什么,乍一看很合理,细想一下,这不是扯淡么。



      所以我又和设计进行了亲切的对话



      • :两个的时候你能考虑到小屏的问题,那一个和三个的时候你为啥不考虑,难道你脑袋有泡,在想一个和三个的时候泡刚好堵住了?

      • 设计: 你天天屌不拉几的,我就要这样,这样好看,你懂个毛的设计,你知道什么是美感和人体工学设计,视觉效果拉满吗?

      • :啊?我的姑奶奶耶,你是不是和产品一个学校毕业的,咋就一根筋呢?

      • 产品:ui说的对,我听ui的。汪汪汪(🐶)


      当时那个画面就像是,就像是:





      而我就像是
      1b761c13b4439463a77ac8abf563677d.png


      那咋办,写呗,我能咋办?



      我月黑风夜,
      黑衣傍我身,
      潜入尔等房,
      打你小屁屁?



      代码实现


         class={[
      'group-even-number' : this.evenNumber,
      'group-odd-number' : this.oddNumber,
      'themeSelectBtnBg'
      ]}
      value={this.currentValue}
      onInput={(value: any) => {
      this.click(value)
      }}
      >
      ...


         .themeSelectBtnBg {
      display: flex;
      &:nth-child(2n - 1) {
      margin-left: 0;
      margin-right: 10px;
      }
      &:nth-child(2n) {
      margin-left: 0;
      margin-right: 0;
      }

      }
      // 奇数的情况,宽度动态计算,将元素挤下去
      .group-odd-number {
      // 需要减去padding的宽度
      width: calc(50% - 7.5px);
      }

      .group-even-number {
      justify-content: space-between;
      @media screen and (max-width:360px) {
      justify-content: unset;
      margin-right: unset;
      flex: 1;
      flex-wrap: wrap;
      }
      }

      行吧,咱就这样吧




      作者:顾昂_
      来源:juejin.cn/post/7304268647101939731
      收起阅读 »