注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

20行js就能实现逐字显示效果???-打字机效果

web
效果演示 横版 竖版 思路分析 可以看到文字是一段一段的并且独占一行,使用段落标签p表示一行 一段文字内,字是一个一个显示的,所以这里每一个字都用一个span标签装起来 每一个字都是从透明到不透明的过渡效果,使用css3的过渡属性transition让每...
继续阅读 »

效果演示


横版


原生JavaScript实现逐字显示效果(打字机效果)插图


竖版


原生JavaScript实现逐字显示效果(打字机效果)插图1


思路分析



  1. 可以看到文字是一段一段的并且独占一行,使用段落标签p表示一行

  2. 一段文字内,字是一个一个显示的,所以这里每一个字都用一个span标签装起来

  3. 每一个字都是从透明到不透明的过渡效果,使用css3的过渡属性transition让每个字都从透明过渡到不透明


基本结构


HTML基本结构


<div id="container"></div>

这里只需要一个容器,其他的结构通过js动态生成


CSS


#container {
/* 添加这行样式=>文字纵向从右往左显示 */
/* 目前先不设置,后面可以取消注释 */
/* writing-mode: vertical-rl; */
}
#container span {
/* 这里opacity先设置为1,让其不透明,可以看到每一步的效果 */
/* 写完js之后到回来改为0 */
opacity: 1;
transition: opacity 0.5s;
}

文本数据


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']

使用数组存放文本数据,一个元素代表一段文字


创建p标签


使用for/of循环遍历数组创建对应个数的p标签,添加到html页面中


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']
// 获取dom元素
const container = document.querySelector('#container')
// for/of循环遍历数组
for (const item of data) {
// 打印每一个item => 数组的每一个元素
console.log(item)
// 创建p标签
const p = document.createElement('p')
// 将p标签插入到container
container.append(p)
}

item代表数组的每一个元素,也就是每一段文字,所以会创建4个p标签


原生JavaScript实现逐字显示效果(打字机效果)插图2


原生JavaScript实现逐字显示效果(打字机效果)插图3


与数组元素数量对应的p标签就生成好了


接下来就是将每一个元素里面的文本添加到span标签中


创建span标签


为每一个字创建一个span标签,然后让span标签的内容等于对应的字,再将每一个生成的span插入到p标签


本节代码


// 遍历item的每一个字
for (let i = 0; i < item.length; i++) {
// 创建span
let span = document.createElement('span')
// span的内容等于item的每一个字
span.innerHTML = item[i]
// 将span插入到p标签中
p.append(span)
}

合并后代码


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']
// 获取dom元素
const container = document.querySelector('#container')
// for/of循环遍历数组
for (const item of data) {
// 打印每一个item => 数组的每一个元素
console.log(item)
// 创建p标签
const p = document.createElement('p')
// 遍历item的每一个字
for (let i = 0; i < item.length; i++) {
// 创建span
let span = document.createElement('span')
// span的内容等于item的每一个字
span.innerHTML = item[i]
// 将span插入到p标签中
p.append(span)
}
// 将p标签插入到container
container.append(p)
}

原生JavaScript实现逐字显示效果(打字机效果)插图4


此时已经完成了渲染数组,并将数组的每一个元素的文字渲染到单独的span中


接下来就要让每一个文字做到从看不见到看的见的效果


添加透明度过渡效果


将css样式中的opacity由1改为0


因为每个字的出现时间不一样,所以不能直接在循环的时候直接添加过渡效果,添加以下代码,让span标签在添加到p标签前也添加到新数组中


const arr = []
// 将span也添加到新数组中
arr.push(span)

最后遍历arr数组,为每一个元素添加一个过渡延迟效果


// 延时1毫秒等待上方循环渲染完成
setTimeout(() => {
// 遍历arr数组的每一个元素
arr.forEach((item, index) => {
// 给每一个元素添加过渡延迟属性
item.style.transitionDelay = `${index * 0.2}s`
// 将透明度设置为不透明
item.style.opacity = 1
})
}, 1)

最后的最后将css样式中的opacity改为0,让所有的字透明


#container span {
opacity: 0;
transition: opacity 0.5s;
}

完整js代码


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']
const arr = []
// 获取dom元素
const container = document.querySelector('#container')
// for/of循环遍历数组
for (const item of data) {
// 打印每一个item => 数组的每一个元素
console.log(item)
// 创建p标签
const p = document.createElement('p')
// 遍历item的每一个字
for (let i = 0; i < item.length; i++) {
// 创建span
let span = document.createElement('span')
// span的内容等于item的每一个字
span.innerHTML = item[i]
// 将span插入到p标签中
p.append(span)
// 将span也添加到新数组中
arr.push(span)
}
// 将p标签插入到container
container.append(p)
}
// 延时1毫秒等待上方循环渲染完成
setTimeout(() => {
// 遍历arr数组的每一个元素
arr.forEach((item, index) => {
// 给每一个元素添加过渡延迟属性
// 让每一个字都比前一个字延时0.2秒的时间
item.style.transitionDelay = `${index * 0.2}s`
// 将透明度设置为不透明
item.style.opacity = 1
})
}, 1)

至此,已经完成了逐字显示的效果,最后介绍一个css属性


writing-mode


使用这个属性可以改变文字方向,实现纵向从左往右或从右往左显示


以下摘自mdn文档


writing-mode 属性定义了文本水平或垂直排布以及在块级元素中文本的行进方向。为整个文档设置该属性时,应在根元素上设置它(对于 HTML 文档,应该在 html 元素上设置)


horizontal-tb

对于左对齐(ltr)文本,内容从左到右水平流动。对于右对齐(rtl)文本,内容从右到左水平流动。下一水平行位于上一行下方。


vertical-rl

对于左对齐(ltr)文本,内容从上到下垂直流动,下一垂直行位于上一行左侧。对于右对齐(rtl)文本,内容从下到上垂直流动,下一垂直行位于上一行右侧。


vertical-lr

对于左对齐(ltr)文本,内容从上到下垂直流动,下一垂直行位于上一行右侧。对于右对齐(rtl)文本,内容从下到上垂直流动,下一垂直行位于上一行左侧。


作者:AiYu
来源:juejin.cn/post/7271165389692960828
收起阅读 »

前端使用a链接下载内容增加loading效果

web
问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。代码如下:// utils.js const XLSX = require('xlsx') // 将一个sheet转成最终的ex...
继续阅读 »
  1. 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。
  2. 代码如下:
// utils.js
const XLSX = require('xlsx')
// 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
export const sheet2blob = (sheet, sheetName) => {
sheetName = sheetName || 'sheet1'
var workbook = {
SheetNames: [sheetName],
Sheets: {}
}
workbook.Sheets[sheetName] = sheet
// 生成excel的配置项
var wopts = {
bookType: 'xlsx', // 要生成的文件类型
bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
type: 'binary'
}
var wbout = XLSX.write(workbook, wopts)
var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
// 字符串转ArrayBuffer
function s2ab(s) {
var buf = new ArrayBuffer(s.length)
var view = new Uint8Array(buf)
for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
return buf
}
return blob
}

/**
* 通用的打开下载对话框方法,没有测试过具体兼容性
* @param url 下载地址,也可以是一个blob对象,必选
* @param saveName 保存文件名,可选
*/

export const openDownloadDialog = (url, saveName) => {
if (typeof url === 'object' && url instanceof Blob) {
url = URL.createObjectURL(url) // 创建blob地址
}
var aLink = document.createElement('a')
aLink.href = url
aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
var event
if (window.MouseEvent) event = new MouseEvent('click')
else {
event = document.createEvent('MouseEvents')
event.initMouseEvent(
'click',
true,
false,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
)
}
aLink.dispatchEvent(event)
}

"clickExportBtn"
>
<i class="el-icon-download">i>下载数据

<div class="mongolia" v-if="loadingSummaryData">
<el-icon class="el-icon-loading loading-icon">
<Loading />
el-icon>
<p>loading...p>
div>

clickExportBtn: _.throttle(async function() {
const downloadDatas = []
const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
summaryDataForDownloads.map(summaryItem =>
downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
)
// donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
this.loadingSummaryData = true
const downloadBlob = aoa2sheet(downloadDatas.flat(1))
openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
this.loadingSummaryData = false
}, 2000),

// css
.mongolia {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
color: #409eff;
z-index: 9999;
}
.loading-icon {
color: #409eff;
font-size: 32px;
}
  1. 解决方案探究:
  • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:

    • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。
    • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。
    • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。
    • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。
  • 查阅资料后找到了如下几种方案:

      1. 使用 setTimeout 使 openDownloadDialog 异步执行
      clickExport() {
      this.loadingSummaryData = true;

      setTimeout(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }
      1. 对 openDownloadDialog 内部进行优化
      • 避免大循环或递归逻辑
      • 将计算工作分批进行
      • 使用 Web Worker 隔离耗时任务
        • 在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。

            1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。
            1. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。
            1. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。
            1. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。
            1. 代码应该是自包含的,不依赖外部变量或状态。
            1. 可以导入其他脚本依赖,比如用 import 引入工具函数等。
            1. 避免修改或依赖全局作用域,比如定义全局变量等。
        • 总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。

          // 创建 Worker
          const worker = new Worker('downloadWorker.js');

          // 点击下载时向 Worker 发送消息
          function clickDownload() {

          showLoading();

          worker.postMessage({
          url: fileURL,
          filename: 'report.xlsx'
          });

          worker.onmessage = function(e) {
          // 收到下载完成的消息
          hideLoading();
          }

          }

          // 显示 loading
          function showLoading() {
          loading.style.display = 'block';
          }

          // 隐藏 loading
          function hideLoading() {
          loading.style.display = 'none';
          }

          // downloadWorker.js

          onmessage = function(e) {
          const url = e.data.url;
          const filename = e.data.filename;

          // 创建并点击链接触发下载
          const a = document.createElement('a');
          a.href = url;
          a.download = filename;
          a.click();

          postMessage('下载完成');
          }

          id="loading" style="display:none;">
          Downloading...





      1. 使用 requestIdleCallback 进行调度
      clickExport() {
      this.loadingSummaryData = true;

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }
    • 当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:

      使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。

      因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。

      requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。

      但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。

      所以需要权衡执行速度和避免阻塞之间的关系:

      • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。
      • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。

      偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。

      此外,可以结合两者试试:

      clickExport() {

      this.loadingSummaryData = true;

      setTimeout(() => {

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob);
      });

      this.loadingSummaryData = false;

      }, 200);

      }

      setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。

      1. 分析线程模型,找到具体的阻塞点
      • 使用 Performance 工具分析线程
      • debugger 及 console.log 打印关键函数的执行时间
      • 检查是否有非主线程的任务阻塞了主线程
      1. 调整 vue 的批量更新策略
      new Vue({
      config: {
      // 修改批量更新的队列长度
      batchUpdateDuration: 100
      }
      })


作者:李卫泽
来源:juejin.cn/post/7268050036474609683
收起阅读 »

实现丝滑的无缝滚动轮播图

web
一. 目标效果 二. 实现思路 使用Animate API或者CSS动画的方式都可以,我选择的是Animate API。 实现无缝滚动的一般思路 Translate位移+无限循环动画。但是这样会有一个小问题,就是在动画结束又开始的一瞬间会闪烁一下,不是很完美...
继续阅读 »

一. 目标效果


2023-12-15-13-59-18.gif


二. 实现思路


使用Animate API或者CSS动画的方式都可以,我选择的是Animate API。


实现无缝滚动的一般思路


Translate位移+无限循环动画。但是这样会有一个小问题,就是在动画结束又开始的一瞬间会闪烁一下,不是很完美。


解决方法


复制一份数据, 原来的1份数据变成2份数据。然后动画的关键帧设置位移的终点为50%,这样每次动画的结束帧就在数据的中间位置, 注意如果数据之间有间距的话,还要加上间距的一半。这样即可实现无限滚动,并且足够丝滑。3


三. 实现


核心代码


以下代码示例都使用React框架。


  // 使用Web animate Api 添加动画
useEffect(() => {
if (!container.current) return;
// 获取gap值
const gap = getComputedStyle(container.current).gap.split('px')[0] ?? 0;

// 滚动容器(container)的50%宽度 + 滚动容器的50%gap值
// 如果不加滚动容器的50%gap值, 在动画结束又开始的瞬间会跳一下
const translateX = container.current.clientWidth / 2 + Number(gap) / 2;
if (isNaN(translateX)) {
throw new Error('translateX is NaN!');
}

// 定义关键帧, 执行动画
let keyframes: Keyframe[] = [];
if (type === 'rtl') {
keyframes = [
{
transform: 'translateX(0)',
},
{
transform: `translateX(-${translateX}px)`,
},
];
} else if (type === 'ltr') {
keyframes = [
{
transform: `translateX(-${translateX}px)`,
},
{
transform: 'translateX(0)',
},
];
}

animation.current = container.current.animate(keyframes, {
duration,
easing: 'linear',
iterations: Infinity,
});
}, []);

return (
// 使用context传递store和dispatch
<SwiperContext.Provider value={{ store, dispatch }}>
<div className={classNames(['w-full overflow-x-hidden', wrapperClassName])}>
{/* 使用inline-flex代替flex,让ul的宽度被子元素撑开 */}
<ul
className={classNames(['inline-flex flex-nowrap gap-5', className])}
style={style}
ref={container}
>

{/* 类似于HOC的效果, 为Item组件添加_key */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}

{/* 实现无缝滚动, 复制一组子元素进行占位 */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}
</ul>
</div>
</SwiperContext.Provider>

);

其中type只是为了区分在x轴上的滚动方向而已,根据方向应用不同的动画。动画的实例使用useRef()去保存。
方便后续调用此动画实例进行动画的暂停和播放。


完整代码


SwiperBox.tsx


import { useHover } from 'ahooks';
import classNames from 'classnames';
import { isNaN, isUndefined } from 'lodash-es';
import {
cloneElement,
CSSProperties,
ReactElement,
ReactNode,
useContext,
useEffect,
useRef,
} from 'react';

import { SwiperContext } from './swiper-context';
import useSwiperReducer, { SwiperActions } from './use-swiper-reducer';

interface SwiperBoxProp {
/**
* 轮播方向
*
* @type {('ltr' | 'rtl')}
* @memberOf SwiperBoxProp
*/

type: 'ltr' | 'rtl';

/**
* 子节点
*
* @type {ReactNode[]}
* @memberOf SwiperBoxProp
*/

children: ReactNode[];

/**
* 类名
*
* @type {string}
* @memberOf SwiperBoxProp
*/

className?: string;

/**
* 外层节点类名
*
* @type {string}
* @memberOf SwiperBoxProp
*/

wrapperClassName?: string;

/**
* 节点样式
*
* @type {CSSProperties}
* @memberOf SwiperBoxProp
*/

style?: CSSProperties;

/**
* 动画持续时间
*
* @type {EffectTiming['duration']}
* @memberOf SwiperBoxProp
*/

duration?: EffectTiming['duration'];

/**
* 鼠标悬停时触发
* @type {boolean} isHovering 是否悬停
* @type {string} key 节点key
*
* @memberOf SwiperBoxProp
*/

hoverOnChange?: (isHovering: boolean, key: string) => void;
}

/**
* 无限循环、无缝轮播组件
* 使用这个组件必须通过gap的方式(eg: gap-4)来设置滚动项之间的距离, 不能使用margin的方式, 不然无缝滚动会有问题
*/

function SwiperBox(prop: SwiperBoxProp) {
const {
type,
className,
wrapperClassName,
style,
children,
duration = 3000,
hoverOnChange,
} = prop;
const [store, dispatch] = useSwiperReducer();
const { activeKey } = store;
// 滚动容器
const container = useRef<HTMLUListElement>(null);
// 动画实例
const animation = useRef<Animation | null>(null);

// activeKey改变时通知外部组件
useEffect(() => {
hoverOnChange &&
!!Object.keys(activeKey).length &&
hoverOnChange(activeKey.isHovering, activeKey.key);
}, [activeKey]);

// 获取所有的key值并存储
useEffect(() => {
dispatch(
SwiperActions.updateKeys(children.map((child) => (child as ReactElement).key ?? ''))
);
}, []);

// 使用Web animate Api 添加动画
useEffect(() => {
if (!container.current) return;
// 获取gap值
const gap = getComputedStyle(container.current).gap.split('px')[0] ?? 0;

// 滚动容器(container)的50%宽度 + 滚动容器的50%gap值
// 如果不加滚动容器的50%gap值, 在动画结束又开始的瞬间会跳一下
const translateX = container.current.clientWidth / 2 + Number(gap) / 2;
if (isNaN(translateX)) {
throw new Error('translateX is NaN!');
}

// 定义关键帧, 执行动画
let keyframes: Keyframe[] = [];
if (type === 'rtl') {
keyframes = [
{
transform: 'translateX(0)',
},
{
transform: `translateX(-${translateX}px)`,
},
];
} else if (type === 'ltr') {
keyframes = [
{
transform: `translateX(-${translateX}px)`,
},
{
transform: 'translateX(0)',
},
];
}

animation.current = container.current.animate(keyframes, {
duration,
easing: 'linear',
iterations: Infinity,
});
}, []);

// 鼠标移入动画暂停/播放
useEffect(() => {
if (!animation.current) return;
if (isUndefined(activeKey.isHovering)) return;

if (activeKey.isHovering) {
animation.current.pause();
} else {
animation.current.play();
}
}, [activeKey]);

return (
// 使用context传递store和dispatch
<SwiperContext.Provider value={{ store, dispatch }}>
<div className={classNames(['w-full overflow-x-hidden', wrapperClassName])}>
{/* 使用inline-flex代替flex,让ul的宽度被子元素撑开 */}
<ul
className={classNames(['inline-flex flex-nowrap gap-5', className])}
style={style}
ref={container}
>

{/* 类似于HOC的效果, 为Item组件添加_key */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}

{/* 实现无缝滚动, 复制一组子元素进行占位 */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}
</ul>
</div>
</SwiperContext.Provider>

);
}

interface SwiperBoxItemProp {
children: ReactNode;
// 唯一标识, React不会将key转发到组件中, 因此自定义一个唯一的_key
_key?: string;
}

function SwiperBoxItem(prop: SwiperBoxItemProp) {
const { children, _key } = prop;

const container = useRef<HTMLLIElement>(null);
const context = useContext(SwiperContext);

// 鼠标hover
const onEnter = () => {
context && _key && context.dispatch(SwiperActions.onEnter(true, _key));
};

// 鼠标退出hover
const onLeave = () => {
context && _key && context.dispatch(SwiperActions.onLeave(false, _key));
};

useHover(container, {
onEnter,
onLeave,
});

return (
<li
ref={container}
className="transition-transform duration-500 ease-out hover:scale-105"
>

{children}
</li>

);
}

const SwiperWithAnimation = {
Box: SwiperBox,
Item: SwiperBoxItem,
};

export default SwiperWithAnimation;


swiper-context.ts


import { createContext, Dispatch } from 'react';

import { SwiperAction, SwiperState } from './use-swiper-reducer';

export type SwiperContextType = {
store: SwiperState;
dispatch: Dispatch<SwiperAction>;
};
export const SwiperContext = createContext<SwiperContextType | null>(null);


useSwiperReducer.ts


import { useReducer } from 'react';

export interface SwiperState {
activeKey: { isHovering: boolean; key: string };
totalKeys: string[];
}

export type SwiperAction<T = any> = {
type: string;
payload: T;
};

export const SwiperActions = {
onEnter: (isHovering: boolean, key: string) => ({
type: 'onEnter',
payload: { isHovering, key },
}),
onLeave: (isHovering: boolean, key: string) => ({
type: 'onLeave',
payload: { isHovering, key },
}),
updateKeys: (keys: string[]) => ({
type: 'update_keys',
payload: keys,
}),
};

export default function useSwiperReducer() {
const initialState: SwiperState = {
activeKey: {} as SwiperState['activeKey'],
totalKeys: [] as SwiperState['totalKeys'],
};

const reducer = (store: SwiperState, { type, payload }: SwiperAction): SwiperState => {
switch (type) {
case 'onEnter':
return {
...store,
activeKey: payload,
};
case 'onLeave':
return {
...store,
activeKey: payload,
};
case 'update_keys':
return {
...store,
totalKeys: payload,
};
default:
return store;
}
};

const [store, dispatch] = useReducer(reducer, initialState);

return [store, dispatch] as const;
}


四、如何使用


import SwiperWithAnimation from '@/components/swiper-box/SwiperBox';
import { uniqueId } from 'lodash-es';


const DATA = new Array(2).fill(0).map(() => uniqueId('data'));
/**
* 测试页面
*/

export default function TestPage() {
// 鼠标hover事件
const hoverOnChange = (isHovering: boolean, key: string) => {
console.log('isHovering: ', isHovering);
console.log('key: ', key);
};

return (
<div>
<SwiperWithAnimation.Box
type="ltr"
wrapperClassName="py-9 m-auto !w-[600px] border border-red-200"
className="gap-8"
hoverOnChange={hoverOnChange}
>

{DATA.map((data) => (
<SwiperWithAnimation.Item key={data}>
<div className="f-c h-[300px] w-[300px] rounded-lg bg-theme-primary">
<div className="text-2xl">{data}</div>
</div>
</SwiperWithAnimation.Item>
))}
</SwiperWithAnimation.Box>
</div>

);
}


作者:In74
来源:juejin.cn/post/7312421872414818331
收起阅读 »

css 实现 'X' 号的显示(close关闭 icon), 并支持动画效果

web
最近项目上要实现一个小 'x' 的关闭样式, 今天记录一下处理过程 先看效果 HTML DOM 元素说明 要渲染内容必须有 dom 节点, 这里我们使用 span 作为容器, 然后所有的处理都基于它进行处理 <span class="close-x"&...
继续阅读 »

最近项目上要实现一个小 'x' 的关闭样式, 今天记录一下处理过程


先看效果


HTML DOM 元素说明


要渲染内容必须有 dom 节点, 这里我们使用 span 作为容器, 然后所有的处理都基于它进行处理


<span class="close-x">span>

第一步, 设置 close-x 的样式


@closeXSize: 20px; // 大小/尺寸
@closeXLine: 2px; // 线条宽度
.close-x {
position: relative;
display: inline-block;
width: @closeXSize;
height: @closeXSize;
cursor: pointer;
}


  • 通过使用 closeXSize closeXLine, 方便对尺寸进行调整

    渲染出来大概是这样的
    image.png


第二步, 通过伪元素 before after 画两条线


.close-x {
// ...

&::before, &::after {
position: absolute;
left: 50%;
width: @closeXLine;
height: 100%;
margin-left: (@closeXLine / -2);
content: '';
background: #000;
}
}


  • margin-left 的设置是为了处理'线'的自身宽度

    渲染出来大概是这样的
    image.png


第三步, 分别设置旋转角度


.close-x {
// ...

&::before {
transform: rotate(-45deg);
}

&::after {
transform: rotate(45deg);
}
}

渲染出来大概是这样的, 基本上就完成了
image.png


继续优化, 锦上添花



  • 先来定义一个动画, 动画的意思是这样的:

    • 当为 0% 时旋转角为 0 度,

    • 当为 100% 时旋转角为 360 度




@keyframes rotating {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}

持续旋转


.rotate-infinite {
animation: rotating .3s infinite linear;
}

// 使用方式 增加类 rotate-infinite
//

加载时旋转一次


.rotate-one {
animation: rotating .3s linear;
}

// 使用方式 增加类 rotate-one
//

hover 时旋转一次


.rotate-hover:hover {
.rotate-one();
}

// 使用方式 增加类 rotate-hover
//

选中时旋转


.rotate-active:active {
.rotate-infinite();
}

// 使用方式 增加类 rotate-active
//

纯JS实现


function addCloseX(content) {
const closeXSize = 20;
const closeXLine = 2;

const closeXWrap = document.createElement('div');
closeXWrap.style.cssText = `
position: relative;
display: inline-block;
width:
${closeXSize}px;
height:
${closeXSize}px;
cursor: pointer;
`
;

const baseStyle = `
display: block;
height: 100%;
width:
${closeXLine}px;
margin: auto;
background: #000;
`
;
const xLineOne = document.createElement('i');
xLineOne.style.cssText = baseStyle + `
transform: rotate(45deg);
`
;
const xLineTwo = document.createElement('i');
xLineTwo.style.cssText = baseStyle + `
margin-top: -100%;
transform: rotate(-45deg);
`
;
closeXWrap.appendChild(xLineOne);
closeXWrap.appendChild(xLineTwo);

content.appendChild(closeXWrap);
}

addCloseX(document.getElementById('close'))

需要提供一下注入的位置, 以上示例需要我们提供这样的 dmo 节点:


<div id="close">div>


  • 这种方式没有使用样式表, 所有的样式都使用了行内样式的方式实现的

  • 因为只用到了行内样式, 所以没办法使用伪元素, 故引入了两个 i 标签代替


结束


相关文档


CSS 实现圆(环)形进度条


作者:洲_
来源:juejin.cn/post/7263069805254197307
收起阅读 »

一次面试让我真正认识了 “:visible.sync”

web
面试官提出了一个很实际的问题:如何封装一个不需要在每个父组件中都定义关闭方法的全局对话框组件,类似于 Element UI 的 el-dialog。这篇技术文章将带你了解如何实现这样的组件,并解释 :visible.sync 这个 Vue 2 的语法糖。 如何...
继续阅读 »

面试官提出了一个很实际的问题:如何封装一个不需要在每个父组件中都定义关闭方法的全局对话框组件,类似于 Element UI 的 el-dialog。这篇技术文章将带你了解如何实现这样的组件,并解释 :visible.sync 这个 Vue 2 的语法糖。


如何封装一个类似 el-dialog 的全局对话框组件


el-dialog 是 Element UI 中的一个常用对话框组件,它提供了一种简洁的方式来展示模态框。在我们自己的项目中,我们可能也会需要封装一个自定义的、功能类似的对话框组件。


步骤一:创建 MyDialog 组件


在 src/components 目录下创建 MyDialog.vue 文件:


<template>
<!-- 对话框的 HTML 代码结构如上所示 -->
</template>

<script>
export default {
// 组件逻辑如上所示
};
</script>

<style scoped>
/* 对话框的样式如上所示 */
</style>

步骤二:在 main.js 中全局注册


在 main.js 文件中,导入 MyDialog 并全局注册这个组件:


import Vue from 'vue';
import MyDialog from './components/MyDialog.vue';

Vue.component('my-dialog', MyDialog);

步骤三:在父组件中使用 .sync 修饰符


要想让 MyDialog 组件的显示状态能够通过父组件控制,但不需要在每个父组件中定义方法来关闭对话框,我们可以使用 Vue 的 .sync 修饰符。


在父组件中,你可以这样使用 MyDialog 组件:


<template>
<my-dialog :title="'自定义对话框'" :visible.sync="dialogVisible">
<!-- 对话框的内容 -->
</my-dialog>
</template>

<script>
export default {
data() {
return {
dialogVisible: false
};
}
// 无需定义关闭对话框的方法
};
</script>

理解 .sync 修饰符


.sync 是 Vue 2 中的一个定制的语法糖,用于创建双向绑定。通常来说,Vue 使用单向数据流,即数据的变化是单向传播的。通过 .sync,子组件可以通过事件更新父组件的状态,使数据的变更成为双向的。


当你在父组件的一个属性上加了 .sync 时,实际上 Vue 会自动更新父组件的状态,当子组件触发了一个特定命名形式的事件(update:xxx)时。


示例:MyDialog 组件


在 MyDialog 组件中,当用户点击关闭按钮时,组件需要通知父组件更新visible属性。这可以通过在子组件内部触发 update:visible 事件来实现:


methods: {
handleClose() {
this.$emit('update:visible', false);
}
}

结论


通过正确使用 Vue 的 .sync 修饰符和事件系统,我们可以轻松地封装和使用类似于 el-dialog 的全局对话框组件,而无需在每个使用它的父组件中定义关闭方法。这种方式使代码更加干净、可维护,并遵循 Vue 的设计原则。


以这种方法,你可以增强组件的可重用性与可维护性,同时学习到 Vue.js 高级组件通信的实用技巧。


注意:在 Vue 3 中,.sync 已经被废弃。相同功能可以通过 v-model 或其它定制的事件和属性实现。所以,确保你的实现与你使用的 Vue 版本相一致。


作者:超级vip管理员
来源:juejin.cn/post/7314493016497635368
收起阅读 »

主管让我说说 qiankun 是咋回事😶

web
前言 最近乙方要移交给我们开发的一个项目的代码,其中前端用到了 qiankun 微前端技术,因为第一版代码之前让我看过,写过基础开发文档,然后主管昨天就找我问了一下,本来以为就是问下具体概念和开发,没想到问起了是怎么实现的🥲,之前了解 qiankun 也就是看...
继续阅读 »

前言


最近乙方要移交给我们开发的一个项目的代码,其中前端用到了 qiankun 微前端技术,因为第一版代码之前让我看过,写过基础开发文档,然后主管昨天就找我问了一下,本来以为就是问下具体概念和开发,没想到问起了是怎么实现的🥲,之前了解 qiankun 也就是看了下开发配置,并没有去关注具体实现,一下子给我难住了。后面又给我留下了几个问题,让我去了解了解,琢磨琢磨,这篇文章就是记一下自己 search 到的一些知识和自己的理解,可能有很多问题,期待JY们指正。


QA


Q:父应用和子应用可以在不同的nginx上吗?


A:可以,父子应用既可以在同一个nginx也可以在不同的nginx上。


Q:从SLB过来的请求是先到父应用再路由到子应用?


A:不是,父应用在运行时,通过 fetch 拿到子应用的 html 文件上的 js、css 依赖(import-html-entry),划出一个独立容器(sandbox)运行子应用,所有子应用都是运行在父应用这个基座上的“应用级组件”,子应用成为了父应用的一部分,子应用中配置的代理不会生效,父子应用共享同一个网络环境,都运行在同一个IP上,请求都从同一个IP发出,子应用的所有网络请求都通过父应用配置的代理转发。


Q:父应用和子应用通信?(是不是通过网络通信)


A:qiankun的父子应用通信不是通过网络通信。


父子应用通信是直接通过浏览器存储或者内存等,例如路由的 query、localStorage、eventBus 或者qiankun提供的全局状态管理工具都可以管理。


子应用挂载时,也可以类似React组件通过props传递具体数据和父应用中改变数据的函数,也可以传递一个全局状态,其包含变量修改和监听变化的函数,父子应用都可以监听变量的变化和修改变量。


Nginx配置


父应用上的 nginx 配置类似本地文件中的 proxy 代理配置,在父应用上分别配置每个子应用的 html 文件所在的地址(资源代理),和子应用的后端接口地址(请求代理)。


export default {
"/root-app": {
target: "https://xxx.xxx.com:xxxx/",
changeOrigin: true,
},

// child1
// 资源代理
"/child1/": {
target: "https://xxx.xxx.com:xxxx/",
changeOrigin: true,
},
// 接口代理
"/child1-api/": {
target: "https://xxx.xxx.com:xxxx/",
changeOrigin: true,
},
// ......
};

不允许主应用跨域访问微应用,做法就是将主应用服务器上一个特殊路径的请求全部转发到微应用的服务器上,即通过代理实现“微应用部署在主应用服务器上”的效果。


例如,主应用在 A 服务器,微应用在 B 服务器,使用路径 /app1 来区分微应用,即 A 服务器上所有 /app1 开头的请求都转发到 B 服务器上。此时主应用的 Nginx 代理配置为:


/app1/ {
proxy_pass http://www.b.com/app1/;
proxy_set_header Host $host:$server_port;
}

演示图


资源文件


从子应用 html 上解析出 js 和 css 加载到父应用基座未命名文件 (2).png


网络请求


未命名文件 (1).png


核心


应用的加载


qiankun 的一个重要的依赖库 import-html-entry ,其功能是主应用拉取子应用 html 中的 js 和 css 文件并加载到父应用基座,css 嵌入到 html,js放在内存中在适当时机 eval 运行


应用的隔离与通信


通过 sandbox 进行 js 和 css 隔离。


js 隔离

js 隔离通过给全局 window 一个 proxy 包裹传递进来,子应用的 js 运行在 proxy 上,子应用卸载时,proxy 跟着清除,这样避免了污染真正的 window,另外对于不支持 proxy 的浏览器,没有 polyfill 方案,qiankun 采用 snapshot 快照方案,保存子应用挂载前的 window 状态,在子应用卸载时,恢复到挂载前的状态,但这种解决方案无法处理基座上同时挂载多个子应用的情景;


css 隔离

css 隔离通过 shadowdom,将子应用的根节点挂载到 shadowdom 中,shadowdom 内部的样式并不会影响全局样式,但是有个缺点,很多组件库的类似弹窗提醒组件会把 dom 提升到顶层,这样注定会污染到全局的样式;


qiankun 的一个实验性解决方案,类似 vue 的 scoped 方案/css-module,给子应用的 css 变量装饰一下(一般是hash),这样避免来避免子应用的样式污染到全局。


彻底解决:约定主子应用完全使用不同的 css 命名; react 的 css-in-js 方案;使用 postcss 全局加变量;全部写 tailwindcss ......


通信

父子应用通信是直接通过浏览器存储或者内存等,例如路由的 query、localStorage、eventBus 或者qiankun提供的全局状态管理工具都可以管理,简单来说就是全局变量。


子应用挂载时,也可以类似React组件通过props传递具体数据和父应用中改变数据的函数,也可以传递一个全局状态,其包含变量修改和监听变化的函数,父子应用都可以监听变量的变化和修改变量。


理解


子应用是可以独立开发、独立部署、独立运行的应用,但在父应用上并不是“独立”运行,而是父应用通过网络动态 fetch 到子应用的 html 文件,然后解析出 html 上的 js 和 css 依赖,处理后加载到父应用基座,将子应用作为自己的一个特殊组件加载渲染到一个“独立沙箱容器”中。


问题



  • 多应用模块共享、代码复用问题没有解决。父子应用如果存在相同依赖,在子应用加载时,是不是还是会去重新加载一遍?

  • 子应用 css 隔离仍存在问题,不支持 proxy 的浏览器无法支持多个子应用同时加载的情形;

  • 当前项目是否真的大到需要使用微前端来增加开发和维护复杂度;

  • 根据我的搜索,qiankun 对于 vite 构建的项目支持度貌似不够,而我们最新项目基本都是通过 vite 构建,可能会有问题。


作者:HyaCinth
来源:juejin.cn/post/7314196310647423039
收起阅读 »

数据库连接神器:JDBC的基本概述、组成及工作原理全解析!

JDBC(Java DataBase Connectivity)是一种用于执行SQL语句的 Java API,是Java和数据库之间的一个桥梁,是一个规范而不是一个实现,能够交给数据库执行SQL语句。在信息化时代,数据库已经成为了存储和管理数据的重要工具。而J...
继续阅读 »

JDBC(Java DataBase Connectivity)是一种用于执行SQL语句的 Java API,是Java和数据库之间的一个桥梁,是一个规范而不是一个实现,能够交给数据库执行SQL语句。

在信息化时代,数据库已经成为了存储和管理数据的重要工具。而Java作为一种广泛使用的编程语言,其与数据库的交互就显得尤为重要。JDBC就是为了解决这个问题而生的。通过JDBC,我们可以在Java程序中轻松地执行SQL语句,实现对数据库的增删改查操作。今天我们就来聊一聊JDBC的相关概念。

一、JDBC简介

概念:

JDBC(Java DataBase Connectivity) :Java数据库连接技术。
具体讲就是通过Java连接广泛的数据库,并对表中数据执行增、删、改、查等操作的技术。如图所示:

Description

本质上,JDBC的作用和图形化客户端的作用相同,都是发送SQL操作数据库。差别在图形化界面的操作是图形化、傻瓜化的,而JDBC则需要通过编码(这时候不要思考JDBC代码怎么写,也不要觉得它有多难)完成图形操作时的效果。

也就是说,JDBC本质上也是一种发送SQL操作数据库的client技术,只不过需要通过Java编码完成。

作用:

  • 通过JDBC技术与数据库进行交互,使用Java语言发送SQL语句到数据库中,可以实现对数据的增、删、改、查等功能,可以更高效、安全的管理数据。
  • JDBC是数据库与Java代码的桥梁(链接)。

二、JDBC的组成

JDBC是由一组用Java语言编写的类和接口组成,主要有驱动管理、Connection接口、Statement接口、ResultSet接口这几个部分。

Description

Connection 接口

定义:在 JDBC 程序中用于代表数据库的连接,是数据库编程中最重要的一个对象,客户端与数据库所有的交互都是通过connection 对象完成的。

Connection conn = DriverManager.getConnection(url,user,password);

常见方法:

  • createStatement() :创建向数据库发送的sql的statement对象。

  • prepareStatement(sql) :创建向数据库发送预编译sql的PrepareSatement对象。

  • prepareCall(sql) :创建执行存储过程的callableStatement对象。(常用)

  • setAutoCommit(boolean autoCommit):设置事务是否自动提交。

//关闭自动提交事务  
setAutoCommit(false);
//关闭后需要手动打开提交事务
  • commit() : 在链接上提交事务。

  • rollback() : 在此链接上回滚事务。

Statement 接口

  • statement:由createStatement创建,用于发送简单的SQL语句(不带参数)。
Statement st = conn.createStatement();
  • PreparedStatement :继承自Statement接口,是Statement的子类,可发送含有参数的SQL语句。效率更高,并且可以防止SQL注入,建议使用。

  • PreparedStatement ps = conn.prepareStatement(sql语句);

PreparedStatement 的优势:
Statement会使数据库频繁编译SQL,可能造成数据库缓冲区溢出。PreparedStatement 可对SQL进行预编译,从而提高数据库的执行效率。
并且PreperedStatement对于sql中的参数,允许使用占位符的形式进行替换,简化sql语句的编写,可以避免SQL注入的问题。

  • CallableStatement:继承自PreparedStatement接口,由方法 prepareCall创建,用于调用存储过程。

常见方法:

  • executeQuery(String sql) :用于向数据发送查询语句。

  • executeUpdate(String sql) :用于向数据库发送insert、update或delete语句。

  • execute(String sql):用于向数据库发送任意sql语句。

  • addBatch(String sql):把多条sql语句放到一个批处理中。

  • executeBatch():向数据库发送一批sql语句执行。

ResultSet 接口

ResultSet:用于代表Sql语句的执行结果。

Resultset封装执行结果时,采用的类似于表格的方式,ResultSet 对象维护了一个指向表格数据行的游标,初始的时候,游标在第一行之前,调用ResultSet.next() 方法,可以使游标指向具体的数据行,进行调用方法获取该行的数据。

常用方法:

  • ResultSet.next() :移动到下一行;

  • ResultSet.Previous() :移动到前一行

  • ResultSet.absolute(int row):移动到指定行

  • ResultSet.beforeFirst():移动resultSet的最前面

  • ResultSet.afterLast():移动resultSet的最后面

在这里给大家分享一下【云端源想】学习平台,无论你是初学者还是有经验的开发者,这里都有你需要的一切。包含课程视频、在线书籍、在线编程、一对一咨询等等,现在功能全部是免费的,

点击这里,立即开始你的学习之旅!

三、JDBC的工作原理

JDBC的工作原理可以分为以下几个步骤:

Description

1、加载并注册JDBC驱动:
这是建立数据库连接的第一步,我们需要先加载JDBC驱动,然后通过DriverManager的registerDriver方法进行注册。

2、建立数据库连接:
通过DriverManager的getConnection方法,我们可以建立与数据库的连接。

3、创建Statement对象:
通过Connection对象的createStatement方法,我们可以创建一个Statement对象,用于执行SQL语句。

4、执行SQL语句:
通过Statement对象的executeQuery或executeUpdate方法,我们可以执行SQL语句,获取结果或者更新数据库。

5、处理结果:
对于查询操作,我们需要处理ResultSet结果集;对于更新操作,我们不需要处理结果。

6、关闭资源:
最后,我们需要关闭打开的资源,包括ResultSet、Statement和Connection。

下面,我们来看一个简单的JDBC使用示例。假设我们要查询为"students"的表中的所有数据:

Description

四、面向过程的实现过程

1.在pom.xml中引入mysql的驱动文件

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
  1. 加载驱动类
Class.forName("com.mysql.cj.jdbc.Driver");
  1. 建立Java同数据库中间的连接通道
String url = "jdbc:mydql://locallhost:3306/test";//test是数据库名称
String user = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url,user,password);
  1. 产生负责’传递命令’的‘传令官’对象
String sql ="insert into emp values(null,"苏醒","歌手",7956,now(),79429,6799,30,1)";
PrepareStement ps = conn.prepareStement(sql);
  1. 接收结果集(只有查询有结果集)
int row = ps.excuteUpdate();//交由MySQL执行命令
System.out.println(row + "行受到影响!");
  1. 关闭连接通道
ps.close();
conn.close();

参数的传递方式

//键盘赋值
private static Scanner scan;
static{
scan = new Scanner(System.in);
}

拼接字符串方式

Description

占位符方式 ‘?’

Description

使用占位符的好处:

  • 可以有效避免SQL注入问题

  • 可以自动根据复制时的数据类型来决定是否引入"

删除

  • 物理删除

Description

  • 逻辑删除

Description

查询操作

  • 全查询

Description

  • 按ID查询

Description

五、面向对象(JDBC)的实现方式

面向对象是指将多个功能查分成多个包进行对数据库的增删改查(CRUD)操作。

db包作用

db包中只需要一个类–>DBManager类,这个类的主要作用就是负责管理数据的的连接。

Description

bean包作用

一般和数据库中的表对应,bean包中的类一般都和表名相同,首字母大写,驼峰命名。

Description

dao包作用

DAO是Data Access Object数据访问接口,一般以bean包的类名为前缀,以DAO结尾,负责执行CRUD操作,一个dao类负责一个表的CRUD,也可以说成是对一个bean类的CRUD(增删改查)。

public class EmpDAO(){


// 一般对于删除操作,都是进行更新状态将之隐藏
public void delete(int id){
try{
conn = DBManager.getConnection();
String sql = "update emp set state = 0 where empNo = "+ id;
ps = conn.prepareStatement(sql);
ps.executeUpdate();
}catch(ClassNotFoundException e){
e.printStackTrace();
}catch(SQLException e){
e.printStackTrace();
}finally{
DBManager.closeConn(conn, ps);
}
}

//存储
public void save(Emp emp) {


try {
conn = DBManager.getConnection();
String sql = "insert into emp values(null,?,?,?,now(),?,?,?,1)";
ps = conn.prepareStatement(sql);


ps.setString(1, emp.getEname());
ps.setString(2, emp.getJob());
ps.setInt(3, emp.getMgr());
ps.setDouble(4, emp.getSal());
ps.setDouble(5, emp.getComm());
ps.setInt(6, emp.getDeptNo());
ps.executeUpdate();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {


DBManager.closeConn(conn, ps);
}
}

//更新--修改
public void update(Emp emp) {
try {
conn = DBManager.getConnection();
String sql = "update emp set ename=?,job=?,mgr=?,sal=?,comm=?,deptNo=? where empNo=?";
ps = conn.prepareStatement(sql);
ps.setString(1, emp.getEname());
ps.setString(2, emp.getJob());
ps.setInt(3, emp.getMgr());
ps.setDouble(4, emp.getSal());
ps.setDouble(5, emp.getComm());
ps.setInt(6, emp.getDeptNo());
ps.setInt(7, emp.getEmpNo());
ps.executeUpdate();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBManager.closeConn(conn, ps);
}
}
//单条信息查询--按ID查询--将填写的信息填写在emp属性里中,然后将emp
public Emp findEmpByNo(int id) {
Emp emp = new Emp();


try {
conn = DBManager.getConnection();
String sql = "select * from emp where empno=? and state = 1";
ps = conn.prepareStatement(sql);
ps.setInt(1, id);
rs = ps.executeQuery();
if (rs.next()) {
//取出第一列的值赋给empNO
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {


DBManager.closeConn(conn, ps);
}
return emp;
}


//全表查询--集合
public List<Emp> findAllEmp() {
List<Emp> list = new ArrayList<>();


try {
conn = DBManager.getConnection();
String sql = "select * from emp where state = 1";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
Emp emp = new Emp();//每循环一次new一个新对象,给对象付一次值
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
list.add(emp);//循环一次在集合中增加一条数据
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBManager.closeConn(conn, ps);
}


return list;


}
}

多表联查


//多表联查
//方法一:
public List<Emp> findAllEmp2(){
List<Emp> list = new ArrayList<>();
try {
conn = DBManager.getConnection();
String sql = "select * from emp e left join Dept d on e.deptNo = d.deptNo where state =1";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()){
Emp emp = new Emp();
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
emp.setDname(rs.getString(11));
emp.setLoc(rs.getString(12));
list.add(emp);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBManager.closeConn(conn, ps,rs);
}
return list;
}


//方法二:
public List<Emp> findAllEmp3(){
List<Emp> list = new ArrayList<>();


try {
conn = DBManager.getConnection();
String sql = "select * from emp e left join Dept d on e.deptNo = d.deptNo where state =1";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()){
Emp emp = new Emp();
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
Dept dept = new Dept();
dept.setDeptNo(rs.getInt(10));
dept.setDname(rs.getString(11));
dept.setLoc(rs.getString(12));
emp.setDept(dept);//引入dept表
list.add(emp);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBManager.closeConn(conn, ps,rs);
}


return list;
}
}

以上就是JDBC的基本概述了,JDBC这个看似复杂的技术,其实是一个非常实用的工具。

只要你掌握了它,就可以轻松地处理和管理你的数据。无论你是一位经验丰富的程序员,还是一位刚刚入门的新手,我都强烈推荐你学习和使用JDBC。相信我,当你掌握了JDBC,你会发现它为你的工作和学习带来了极大的便利。

收起阅读 »

Vue实现一个textarea幽灵建议功能

web
不知道你有没有发现Bing AI聊天有个输入提示功能,在用户输入部分内容时后面会给出灰色提示文案。用户只要按下tab键就可以快速添加提示的后续内容。我将这个功能称为幽灵建议。接下来我将用Vue框架来实现这个功能。 布局样式 布局使用label标签作为容器,这...
继续阅读 »

不知道你有没有发现Bing AI聊天有个输入提示功能,在用户输入部分内容时后面会给出灰色提示文案。用户只要按下tab键就可以快速添加提示的后续内容。我将这个功能称为幽灵建议。接下来我将用Vue框架来实现这个功能。



布局样式


布局使用label标签作为容器,这样即使建议内容在上层,也不会影响输入框的输入。


<label class="container">
<textarea></textarea>
<div class="ghost-content"></div>
</label>

样式需要确保输入框与建议内容容器除了颜色外都要一致。建议内容可以通过z-index: -1置于输入框底部,但要注意输入框必须是透明背景。


.container {
position: relative;
display: block;
width: 300px;
height: 200px;
font-size: 14px;
line-height: 21px;
}
.container textarea {
width: 100%;
height: 100%;
padding: 0;
border: 0;
font: inherit;
color: #212121;
background-color: #fff;
outline: none;
}
.ghost-content {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #212121;
opacity: 0.3;
}

显示逻辑


显示逻辑比较简单,当输入框中显示输入内容时,找到匹配的内容后将其显示在建议容器中。以下是代码示例:


import { ref } from 'vue'

const content = ref('') // 输入框内容
const ghostContent = ref('') // 建议内容
const suggestions = ['你好啊', '怎么学编程'] // 建议列表

const handleInput = () => {
ghostContent.value = '' // 内容变化时,清空建议
// 如果为空或者建议内容改变,则不进行后续匹配
if (content.value === '') {
return
}
const suggestion = suggestions.find((item) => item.startsWith(content.value))
if (suggestion) {
ghostContent.value = suggestion
}
}

const handleTabKeydown = () => {
// 监听tab键按下,将输入框内容设置为建议内容,同时清空建议内容
content.value = ghostContent.value
ghostContent.value = ''
}

按照以上代码的写法,已经可以实现幽灵建议的功能了。但还存在一个小问题,输入框内容和建议内容的重叠部分会显得比较粗。因此,最好将重叠部分的文字颜色设置为透明。我的解决方法是使用span标签来包裹重叠部分的内容,然后将span的文字样式设置为透明。此外,为了表示可以使用tab键,我在末尾添加了符号。改进后的代码如下:


// 重复部分省略
// ...
const ghostHTML = ref('') // 建议内容HTML
const handleInput = () => {
ghostContent.value = ''
ghostHTML.value = ''
if (content.value === '' || fromSuggestion) {
fromSuggestion && (fromSuggestion = false)
return
}
const suggestion = suggestions.find((item) => item.startsWith(content.value))
if (suggestion) {
ghostContent.value = suggestion
ghostHTML.value = suggestion.replace(content.value, `<span>${content.value}</span>`) + ' →' // 显示内容替换
}
}

const handleTabKeydown = () => {
content.value = ghostContent.value
ghostContent.value = ''
ghostHTML.value = ''
}

最后,补充一下HTML代码。


<label class="container">
<textarea v-model="content" @input="handleInput" @keydown.tab.prevent="handleTabKeydown"></textarea>
<div class="ghost-content" v-html="ghostHTML"></div>
</label>


  • 我们需要阻止tab按下的默认事件,按下tab键会导致切换到其他元素,使输入框失去焦点;

  • 使用v-html来绑定HTML内容。


作者:60岁咯
来源:juejin.cn/post/7273674732448120895
收起阅读 »

谈谈国内前端的三大怪啖

web
因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。今天聊三个事情:小程序微前端模块加载小程序每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。“我们...
继续阅读 »

因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。

今天聊三个事情:

  • 小程序
  • 微前端
  • 模块加载

小程序

每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。

“我们为什么需要小程序?”

第一次被问到这个问题,是因为一个法国的同事。他被派去做一个移动端业务,刚好那个业务是采用小程序在做。于是一个法国小哥就在被痛苦的中文文档和黑盒逻辑中来回折磨着 🤦。

于是,当我们在有一次交流中,他问出了我这个问题:我们为什么需要小程序?

说实话,我试图解释了 19 年国内的现状,以及微信小程序推出时候所带来的便利和体验等等。总之,在我看来并不够深刻的见解。

即便到现在为止,每次当我使用小程序的时候,依旧会复现这个问题。在 ChatGPT 11 月份出来的时候,我也问了它这个很有国内特色的问题:

看起来它回答的还算不错,至少我想如果它来糊弄那些老外,应该会比我做的更好些。

但如果扪心自问,单从技术上来讲。以上这些事情,一定是有其他方案能解决的。

所以从某种程度上来看,这更像是一场截胡的商业案例:

应用市场

全世界的互联网人都知道应用市场是非常有价值的事情,可以说操作系统最值钱的部分就在于他们构建了自己的应用市场。

只要应用在这里注册发行,雁过拔毛,这家公司就是互联网世界里的统治阶级,规则的制定者。

反之则需要受制于人,APP 做的再大,也只是应用市场里的一个应用,做的好与坏还得让应用商店的评判。

另外,不得不承认的是,一个庞大如苹果谷歌这样的公司,他们的应用商店对于普通国内开发者来说,确实是有门槛的。

在国内海量的 APP 需求来临之前,能否提供一个更低成本的解决方案,来消化这些公司的投资?

毕竟不是所有的小企业都需要 APP,其实他们大部分需求 Web 就可以解决,但是 Web 没牌面啊,做 Web 还得砸搜索的钱才有流量。(某度搜索又做成那样...)

那做操作系统?太不容易,那么多人都溺死在水里了,这水太深。

那有没有一种办法可以既能构建生态,又有 APP 的心智,还能给入驻企业提供流量?

于是,在 19 年夏天,滨海大厦下的软件展业基地里,每天都在轮番播放着,做 XX小程序,拥抱下一个风口...

全新体验心智

小程序用起来挺方便的。

你有没有想过,这些美妙感觉的具体都来自哪些?以及这些真的是 Web 技术本身无法提供的吗?

  1. 靠谱感,每个小程序都有约束和规范,于是你可以将他们整整齐齐的陈放在你的列表里,仿佛你已经拥有了这一个个精心雕琢的作品,相对于一条条记不住的网页地址和鱼龙混杂的网页内容来说,这让你觉得小程序更加的有分量和靠谱。
  2. 安全感,沉浸式的头部,没有一闪而过的加载条,这一切无打扰的设计,都让你觉得这是一个在你本地的 APP,而不是随时可能丢失网页。你不会因为网速白屏而感到焦虑,尽管网络差的时候,你的 KFC 依旧下不了单 😂
  3. 沉浸感,我不知道是否打开网页时顶部黑黑的状态栏是故意留下的,还是不小心的... 这些限制都让用户非常强烈的意识到这是一个网页而不是 APP,而小程序中虽然上面也存在一个空间的空白,但是却可以被更加沉浸的主题色和氛围图替代。网页这个需求做不了?我不信。
H5小程序
  1. 顺滑感,得益于 Native 的容器实现,小程序在所有的视图切换时,都可以表现出于原生应用一样的顺滑感。其实这个问题才是在很多 Hybrid 应用中,主要想借助客户端来处理和解决的问题。类似容器预开、容器切换等技术是可以解决相关问题的,只是还没有一个标准。

我这里没有提性能,说实话我不认为性能在小程序中是有优势的(Native 调用除外,如地图等,不是一个讨论范畴)。作为普通用户,我们感受到的只是离线加载下带来的顺滑而已。

而上述提到的许多优势,这对于一个高品质的 Web 应用来说是可以做得到的,但注意这里是高品质的 Web 应用。而这种“高品质”在小程序这里,只是入驻门槛而已。

心智,这个词,听起来很黑话,但却很恰当。当小程序通过长期这样的筛选,所沉淀出来一批这样品质的应用时。就会让每个用户即便在还没打开一个新的小程序之前,也有不错体验的心理预期。这就是心智,一种感觉跟 Web 不一样,跟 APP 有点像的心智。

打造心智,这件事情好像就是国内互联网企业最擅长做的事情,总是能从一些细微的差别中开辟一条独立的领域,然后不断强化灌输本来就不大的差异,等流量起来再去捞钱变现。


我总是感觉现在的互利网充斥着如何赚钱的想法,好像永远赚不够。“赚钱”这个事情,在这些公司眼里就是圈人圈地抢资源,看看谁先占得先机,别人有的我也得有,这好像是最重要的事情。

很少有企业在思考如何创造些没有的市场,创造些真正对技术发展有贡献,对社会发展有推动作用的事情。所以在国内互联网圈也充斥着一种奇怪的价值观,有技术的不一定比赚过钱的受待见。

管你是 PHP 还是 GO,管你是在做游戏还是直播带货,只要赚到钱就是高人。并且端的是理所应当、理直气壮,有些老板甚至把拍摄满屋子的程序员为自己打工作为一种乐趣。什么情怀、什么优雅、什么愿景,人生就俩字:搞钱。

不是故意高雅,赚钱这件事情本身不寒碜,只是在已经赚到盆满钵满、一家独大的时候还在只是想着赚更多的钱,好像赚钱的目的就是为了赚钱一样,这就有点不合适。企业到一定程度是要有社会责任的,龙头企业每一个决定和举措,都有会影响接下来的几年里这个行业的价值观走向。

当然也不是完全没有好格局的企业,我非常珍惜每一个值得尊重的中国企业,来自一个蔚来车主。

小程序在商业上固然是成功的,但吃的红利可以说还是来自 网页 到 应用 的心智变革。将本来流向 APP 的红利,截在了小程序生态里。

但对于技术生态的发展却是带来了无数条新的岔路,小程序的玩法就决定了它必须生长在某个巨型应用里面,不论是用户数据获取、还是 API 的调用,其实都是取决于应用容器的标准规范。

不同公司和应用之间则必然会产生差异,并且这种差异是墒增式的差异,只会随着时间的推移越变越大。如果每个企业都只关注到自己业务的增长,无外部约束的话,企业必然会根据自己的业务发展和政策需要,选择成本较低的调整 API,甚至会故意制造一些壁垒来增加这种差异。

小程序,应该是 浏览器 与 操作系统 的融合,这本应该是推动这两项技术操刀解决的事情。

微前端

qiankun、wujie、single-spa 是近两年火遍前端的技术方案,同样一个问题:我们为什么需要微前端?

我不确定是否每个在使用这项技术的前端都想清楚了这个问题,但至少在我面试过的候选人中,我很少遇到对自己项目中已经在使用的微前端,有很深的思考和理解的人。

先说下我的看法:

  1. 微前端,重在解决项目管理而不在用户体验。
  2. 微前端,解决不了该优化和需要规范的问题。
  3. 微前端,在挽救没想清楚 MPA 的 SPA 项目。

没有万能银弹

银色子弹(英文:Silver Bullet),或者称“银弹”“银质子弹”,指由纯银质或镀银的子弹。在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银色子弹往往被描绘成具有驱魔功效的武器,是针对狼人、吸血鬼等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。

所有技术的发展都是建立在前一项技术的基础之上,但技术依赖的选择过程中一定需要保留第一性原理的意识。

当 React、Vue 兴起,当 打包技术(Webpack) 兴起,当 网页应用(SPA) 兴起,这些杰出的技术突破都在不同场景和领域中给行业提供了新的思路、新的方案。

不知从何时开始,前端除了 div 竟说不出其他的标签(还有说 View 的),项目中再也不会考虑给一个通用的 class 解决通用样式问题。

不知从何时开始,有没有权限需要等到 API 请求过后才知道,没有权限的话再把页面跳转过去申请。

不知从何时开始,大家的页面都放在了一个项目里,两个这样的巨石应用融合竟然变成了一件困难的事。

上面这些不合理的现状,都是在不同的场景下,不思考适不适合,单一信奉 “一招吃遍天” 下演化出的问题。

B 端应用,是否应该使用 SPA? 这其实是一个需要思考的问题。

微前端从某种程度上来讲,是认准 SPA 了必须是互联网下一代应用标准的产物,好像有了 SPA 以后,MPA 就变得一文不值。甭管页面是移动端的还是PC的;甭管页面是面对 C 端的还是 B 端的;甭管一个系统是有 20 个页面还是 200 个页面,一律行这套逻辑。

SPA 不是万能银弹,React 不是万能银弹,Tailwind 不是万能银弹。在新技术出现的时候,保持热情也要保持克制。

ps. 我也十分痛恨 React 带来的这种垄断式的生态,一个 React 组件将 HTML 和 Style 都吃进去,让你即使在做一个移动端的纯展示页面时,也需要背上这些称重的负担。

质疑 “墨守成规”,打开视野,深度把玩,理性消费。

分而治之

分治法,一个很基本的工程思维。

在我看来在一个正常商业迭代项目中的主要维护者,最好不要超过 3 个人,注意是主要维护者(Maintainer) 。

你应该让每个项目都有清晰的责任人,而不是某行代码,某个模块。责任人的理解是有归属感,有边界感的那种,不是口头意义上的责任人。(一些公司喜欢搞这种虚头巴脑的事情,什么连坐…)

我想大部分想引入微前端的需求都是类似 如何更好的划分项目边界,同时保留更好的团队协同。

比如 导航菜单 应该是独立收口独立管理的,并且当其更新时,应该同时应用于所有页面中。类似的还有 环境变量、时区、主题、监控及埋点。微前端将这些归纳在主应用中。

而具体的页面内容,则由对应的业务进行开发子应用,最后想办法将路由关系注册进主应用即可。

当然这样纯前端的应用切换,还会出现不同应用之间的全局变量差异、样式污染等问题,需要提供完善的沙箱容器、隔离环境、应用之间通信等一系列问题,这里不展开。

当微前端做到这一部分的时候,我不禁在想,这好像是在用 JavaScript 实现一个浏览器的运行容器。这种本应该浏览器本身该做的事情,难道 JS 可以做的更好?

只是做到更好的项目拆分,组织协同的话,引入后端服务,由后端管控路由表和页面规则,将页面直接做成 MPA,这个方案或许并不比引入微前端成本高多少。

体验差异

从 SPA 再回 MPA,说了半天不又回去了么。

所以不防想想:在 B端 业务中使用 SPA 的优势在哪里?

流畅的用户体验:

这个话题其实涵盖范围很广,对于 SPA 能带来的 “流畅体验”,对于大多数情况下是指:导航菜单不变,内容变化 发生变化,页面切换中间不出现白屏

但要做到这个点,其实对于 MPA 其实并没有那么困难,你只需要保证你的 FCP 在 500ms 以内就行。

以上的页面切换全部都是 MPA 的多页面切换,我们只是简单做了导航菜单的 拆分 和 SWR,并没有什么特殊的 preload、prefetch 处理,就得到了这样的效果。

因为浏览器本身在页面切换时会在 unload 之前先 hold 当前页面的视图不变,发起一下一个 document 的请求,当页面的视图渲染做到足够快和界面结构稳定就可以得到这样的效果。

这项浏览器的优化手段我找了很久,想找一篇关于它的博文介绍,但确实没找到相关内容,所以 500ms 也是我的一个大致测算,如果你知道相关的内容,可以在评论区补充,不胜感激。

所以从这个角度来看,浏览器本身就在尽最大的努力做这些优化,并且他们的优化会更底层、更有效的。

离线访问 (PWA)

SPA 确实会有更好的 PWA 组织能力,一个完整的 SPA 应用甚至可以只针对编译层做改动就可以支持 PWA 能力。

但如果看微前端下的 SPA 应用,需要支持 PWA 那就同样需要分析各子应用之间的元数据,定制 Service Worker。这种组织关系和定制 SW,对于元数据对于数据是来自前端还是后端,并不在意。

也就是说微前端模式下的 PWA,同样的投入成本,把页面都管理在后端服务中的 MPA 应用也是可以做到相同效果的。

项目协同、代码复用

有人说 SPA 项目下,项目中的组件、代码片段是可以相互之间复用的,在 MPA 下就相对麻烦。

这其实涉及到项目划分的领域,还是要看具体的需求也业务复杂度来定。如果说整个系统就是二三十个页面,这做成 SPA 使用前端接管路由高效简单,无可厚非。

但如果你本身在面对的是一个服务于复杂业务的 B 端系统,比如类似 阿里云、教务系统、ERP 系统或者一些大型内部系统,这种往往需要多团队合作开发。这种时候就需要跟完善的项目划分、组织协同和系统整合的方案。

这个时候 SPA 所体现出的优势在这样的诉求下就会相对较弱,在同等投入的情况下 MPA 的方案反而会有更少的执行成本。

也不是所有项目一开始就会想的那么清楚,或许一开始的时候就是个简单的 SPA 项目,但是随着项目的不断迭代,才变成了一个个复杂的巨石应用,现在如果再拆出来也会有许多迁移成本。引入微前端,则可以...

这大概是许多微前端项目启动的背景介绍,我想说的是:对于屎山,我从来不信奉“四两拨千斤”

如果没有想好当下的核心问题,就引入新的“银弹”解决问题,只会是屎山雕花。

项目协同,抽象和复用这些本身不是微前端该解决的问题,这是综合因素影响下的历史背景问题。也是需要一个个资深工程师来把控和解决的核心问题,就是需要面对不同的场景给出不同的治理方案。

这个道理跟防沙治沙一样,哪有那么多一蹴而就、立竿见影的好事。

模块加载

模块加载这件事情,从玉伯大佬的成名作 sea.js 开始就是一个非常值得探讨的问题。在当时 jQuery 的时代里,这是一个绝对超前的项目,我也在实际业务中体会过在无编译的环境下 sea.js 的便捷。

实际上,不论是微前端、低代码、会场搭建等热门话题离不开这项技术基础。

import * from * 我们每天都在用,但最终的产物往往是一个自运行的 JS Bundle,这来自于 Webpack、Vite 等编译技术的发展。让我们可以更好的组织项目结构,以构建更复杂的前端应用。

模块的概念用久了,就会自然而然的在遇到浏览器环境中,遇到动态模块加载的需求时,想到这种类似模块加载的能力。

比如在遇到会场千奇百怪的个性化营销需求时,能否将模块的 Props 开放出来,给到非技术人员,以更加灵活的方式让他们去做自由组合。

比如在低代码平台中,让开发者自定义扩展组件,动态的将开发好的组件注册进低代码平台中,以支持更加个性的需求。

在万物皆组件的思想影响下,把一整个完整页面都看做一个组件也不是不可以。于是在一些团队中,甚至提倡所有页面都可以搭建、搭建不了的就做一个大的页面组件。这样及可以减少维护运行时的成本,又可以统一管控和优化,岂不美哉。

当这样大一统的“天才方案”逐渐发展成为标准后,也一定会出现一些特殊场景无法使用,但没关系,这些天才设计者肯定会提供一种更加天才的扩展方案出来。比如插件,比如扩展,比如 IF ELSE。再后来,就会有性能优化了,而优化的 追赶对象 就是用原来那种简单直出的方案。

有没有发现,这好像是在轮回式的做着自己出的数学题,一道接一道,仿佛将 1 + 1的结论重新演化了一遍。

题外话,我曾经自己实现过一套通过 JSON Schema 描述 React 结构的 “库” ,用于一个低代码核心层的渲染。在我的实现过程中,我越发觉得我在做一件把 JSX 翻译成 JS 的事情,但 JSX 或者 HTML 本身不就是一种 DSL 么。为什么一定要把它翻译成 JSON 来传输呢?或者说这样的封装其本身有意义么?这不就是在做 PHP、.Net 直接返回带有数据的 HTML Ajax 一样的事情么。

传统的浏览器运行环境下要实现一个模块加载器,无非是在全局挂载一个注册器,通过 Script 插入一段新的 JS,该 JS 通过特殊的头尾协议,将运行时的代码声明成一个函数,注册进事先挂载好的注册器。

但实际的实现场景往往要比这复杂的多,也有一些问题是这种非原生方式无法攻克的问题。比如全局注册器的命名冲突;同模块不同版本的加载冲突;并发加载下的时序问题;多次模块加载的缓存问题 等等等等等...

到最后发现,这些事情好像又是在用 JS 做浏览器该做的事情。然而浏览器果然就做了,,Vite 就主要采用这种模式实现了它 1 年之内让各大知名项目切换到 Vite 的壮举。

“但我们用不了,有兼容性问题。”

哇哦,当我看着大家随意写出的 display: grid 样式定义,不禁再次感叹人们对未知的恐惧。

import.meta 的兼容性是另外一个版本,是需要 iOS12 以上,详情参考:caniuse.com/?search=imp…

试想一下,现在的低代码、会场搭建等等各类场景的模块加载部分,如果都直接采用 ESM 的形式处理,这对于整个前端生态和开发体验来说会有多么大的提升。

模块加载,时至今日,本来就已经不再需要 loader。 正如 seajs 中写的:前端模块化开发那点历史

历史不是过去,历史正在上演。随着 W3C 等规范、以及浏览器的飞速发展,前端的模块化开发会逐步成为基础设施。一切终究都会成为历史,未来会更好。

结语

文章的结尾,我想感叹另外一件事,国人为什么一定要有自己的操作系统?为什么一定需要参与到一些规范的制定中?

因为我们的智慧需要有开花的土壤,国内这千千万开发者的抱负需要有地方释放。

如果没有自己掌握核心技术,就是只能在问题出现的时候用另类的方式来解决。最后在一番折腾后,发现更底层的技术只要稍稍一改就可以实现的更好。这就像三体中提到的 “智子” 一样,不断在影响着我们前进的动力和方向。

不论是小程序、微前端还是模块加载。试想一下,如果我们有自己的互联网底蕴,能决定或者影响操作系统和浏览器的底层能力。这些 “怪啖” 要么不会出现,要么就是人类的科技创新。

希望未来技术人不用再追逐 Write Once, Run Everywhere 的事情...


作者:YeeWang
来源:juejin.cn/post/7267091810366488632
收起阅读 »

QR码是怎么工作的?

web
原文链接: typefully.com/DanHollick/… 作者:Dan Hollick 你有想过QR码是如何工作的吗? 我也没有想过,但是它真的很低调很迷人~ 【警告】这里有一些非常书呆子的东西👇 ) QR码是由丰田的一个子公司发明的,目的是为了在整...
继续阅读 »

原文链接: typefully.com/DanHollick/…


作者:Dan Hollick


你有想过QR码是如何工作的吗?


我也没有想过,但是它真的很低调很迷人~


【警告】这里有一些非常书呆子的东西👇 )


image.png


QR码是由丰田的一个子公司发明的,目的是为了在整个制造过程中跟踪零件信息。


之前出现的条形码被证明是不足够的 - 它们只能从特定的角度读取,并且相对于他们的大小来说,并不能储存很多的数据。


那么 QR 码不只解决了这些问题


image.png


QR 码最独一无二的地方在于这些正方体,这些正方体被称为“查找器”,这些正方体帮助了你的阅读器检测到码的存在。


第四个小的正方体,被称作对齐模式,它是用来定向代码的,使它可以在任何角度呈现,阅读器仍然哪个方向是向上的。


image.png


你可能从来都没有注意过,但是每个 QR 码都有这些叫做定时模式的黑白相间的点。


这些黑白相间的点告诉阅读器单个模块有多大以及整个 QR 码有多大 -- 也就是版本。


image.png


版本一:最小的。
版本四十:最大的。


image.png


关于格式的信息被存在查找器旁边的两个条纹中。


它被存储了两次,所以即使QR码被部分遮挡,它也是可读的。(你会注意到这是一个反复出现的主题。)


image.png


它存储了三个重要的信息片段



  1. 掩码(Mask)

  2. 纠错级别

  3. 纠错格式


我知道这听起来很无聊,但是实际上,他还是很有意思的(doge


image.png


首先,纠错 - 这是什么玩意?


从本质上讲,它规定了在 QR 码中存储多少冗余信息,以确保即使部分信息丢失也能保持可读。


image.png


这真的很酷好吗 - 如果您的代码在户外,您可以选择更高的冗余级别,以确保它在模糊的时候也能正常工作。


试试下面这个二维码


image.png


第二个,这个 mask,这个是个什么东西?


首先你需要知道,QR 阅读器在黑色区域和白色区域的数量一样的时候工作的最好。


但是数据可能无法发挥作用,因此使用掩码来平衡。


image.png


当掩版应用于QR码时,任何落在掩码暗部的东西都会被反转。


白色区域变成黑色,黑色区域变成白色。


image.png


有8种标准模式,一个接一个地应用。使用达到最佳结果的模式,并存储该信息,以便读者可以不应用掩码。


最后呢,就到了我们的实际数据的部分。


奇怪的是,数据从右下角开始,然后像图中那样蜿蜒而上。


从哪开始几乎不重要了,因为它可以从每个角度读取。


image.png


这里的第一个信息块告诉读者数据是以什么模式编码的,第二个告诉它长度。


在我们的例子中,每个字符占用8位块,或者称为字节,共有24个字节。


image.png


在我们的数据之后还有一些剩余的空间。这是存储纠错信息的地方,以便在部分模糊的情况下可以读取。它的工作方式实际上非常非常复杂,所以我把它省略了。基本上就是这样!


image.png


对于制作 QR 码的书呆子来说,一个有意思的事实是: QR码最酷的事情是发明QR码的Denso Wave公司从未行使他们的专利,并且免费发布这项技术!


作者:阳树阳树
来源:juejin.cn/post/7311142182810992703
收起阅读 »

商品 sku 在库存影响下的选中与禁用

web
分享一下,最近使用 React 封装的一个 Skus 组件,主要用于处理商品的sku在受到库存的影响下,sku项的选中和禁用问题; 需求分析 需要展示商品各规格下的sku信息,以及根据该sku的库存是否为空,判断是否禁用该sku的选择。 以下讲解将按照我的 ...
继续阅读 »

分享一下,最近使用 React 封装的一个 Skus 组件,主要用于处理商品的sku在受到库存的影响下,sku项的选中和禁用问题;


需求分析


需要展示商品各规格下的sku信息,以及根据该sku的库存是否为空,判断是否禁用该sku的选择。


sku-2.gif

以下讲解将按照我的 Skus组件 来,我这里放上我组件库中的线上 demo 和码上掘金的一个 demo 供大家体验;由于码上掘金导入不了组件库,我就上传了一份开发组件前的一份类似的代码,功能和代码思路是差不多的,大家也可以自己尝试写一下,可能你的思路会更优;


线上 Demo 地址


码上掘金



传入的sku数据结构


需要传入的商品的sku数据类型大致如下:


type SkusProps = { 
/** 传入的skus数据列表 */
data: SkusItem[]
// ... 其他的props
}

type SkusItem = {
/** 库存 */
stock?: number;
/** 该sku下的所有参数 */
params: SkusItemParam[];
};

type SkusItemParam = {
name: string;
value: string;
}

转化成需要的数据类型:


type SkuStateItem = {
value: string;
/** 与该sku搭配时,该禁用的sku组合 */
disabledSkus: string[][];
}[];

生成数据


定义 sku 分类


首先假装请求接口,造一些假数据出来,我这里自定义了最多 6^6 = 46656 种 sku。


sku-66.gif

下面的是自定义的一些数据:


const skuData: Record<string, string[]> = {
'颜色': ['红','绿','蓝','黑','白','黄'],
'大小': ['S','M','L','XL','XXL','MAX'],
'款式': ['圆领','V领','条纹','渐变','轻薄','休闲'],
'面料': ['纯棉','涤纶','丝绸','蚕丝','麻','鹅绒'],
'群体': ['男','女','中性','童装','老年','青少年'],
'价位': ['<30','<50','<100','<300','<800','<1500'],
}
const skuNames = Object.keys(skuData)

页面初始化



  • checkValArr: 需要展示的sku分类是哪些;

  • skusList: 接口获取的skus数据;

  • noStockSkus: 库存为零对应的skus(方便查看)。


export default () => {
// 这个是选中项对应的sku类型分别是哪几个。
const [checkValArr, setCheckValArr] = useState<number[]>([4, 5, 2, 3, 0, 0]);
// 接口请求到的skus数据
const [skusList, setSkusList] = useState<SkusItem[]>([]);
// 库存为零对应的sku数组
const [noStockSkus, setNoStockSkus] = useState<string[][]>([])

useEffect(() => {
const checkValTrueArr = checkValArr.filter(Boolean)
const _noStockSkus: string[][] = [[]]
const list = getSkusData(checkValTrueArr, _noStockSkus)
setSkusList(list)
setNoStockSkus([..._noStockSkus])
}, [checkValArr])

// ....

return <>...</>
}

根据上方的初始化sku数据,生成一一对应的sku,并随机生成对应sku的库存。


getSkusData 函数讲解


先看总数(total)为当前需要的各sku分类的乘积;比如这里就是上面传入的 checkValArr 数组 [4,5,2,3]120种sku选择。对应的就是 skuData 中的 [颜色前四项,大小前五项,款式前两项,面料前三项] 即下图的展示。


image.png

遍历 120 次,每次生成一个sku,并随机生成库存数量,40%的概率库存为0;然后遍历 skuNames 然后找到当前对应的sku分类即 [颜色,大小,款式,面料] 4项;


接下来就是较为关键的如何根据 sku的分类顺序 生成对应的 120个相应的sku。


请看下面代码中注释为 LHH-1 的地方,该 value 的获取是通过 indexArr 数组取出来的。可以看到上面 indexArr 数组的初始值为 [0,0,0,0] 4个零的索引,分别对应 4 个sku的分类;



  • 第一次遍历:


indexArr: [0,0,0,0] -> skuName.forEach -> 红,S,圆领,纯棉


看LHH-2标记处: 索引+1 -> indexArr: [0,0,0,1];



  • 第二次遍历:


indexArr: [0,0,0,1] -> skuName.forEach -> 红,S,圆领,涤纶


看LHH-2标记处: 索引+1 -> indexArr: [0,0,0,2];



  • 第三次遍历:


indexArr: [0,0,0,2] -> skuName.forEach -> 红,S,圆领,丝绸


看LHH-2标记处: 由于已经到达该分类下的最后一个,所以前一个索引加一,后一个重新置为0 -> indexArr: [0,0,1,0];



  • 第四次遍历:


indexArr: [0,0,1,0] -> skuName.forEach -> 红,S,V领,纯棉


看LHH-2标记处: 索引+1 -> indexArr: [0,0,1,1];



  • 接下来的一百多次遍历跟上面的遍历同理


image.png
function getSkusData(skuCategorys: number[], noStockSkus?: string[][]) {
// 最终生成的skus数据;
const skusList: SkusItem[] = []
// 对应 skuState 中各 sku ,主要用于下面遍历时,对 product 中 skus 的索引操作
const indexArr = Array.from({length: skuCategorys.length}, () => 0);
// 需要遍历的总次数
const total = skuCategorys.reduce((pre, cur) => pre * (cur || 1), 1)
for(let i = 1; i <= total; i++) {
const sku: SkusItem = {
// 库存:60%的几率为0-50,40%几率为0
stock: Math.floor(Math.random() * 10) >= 4 ? Math.floor(Math.random() * 50) : 0,
params: [],
}
// 生成每个 sku 对应的 params
let skuI = 0;
skuNames.forEach((name, j) => {
if(skuCategorys[j]) {
// 注意:LHH-1
const value = skuData[name][indexArr[skuI]]
sku.params.push({
name,
value,
})
skuI++;
}
})
skusList.push(sku)

// 注意: LHH-2
indexArr[indexArr.length - 1]++;
for(let j = indexArr.length - 1; j >= 0; j--) {
if(indexArr[j] >= skuCategorys[j] && j !== 0) {
indexArr[j - 1]++
indexArr[j] = 0
}
}

if(noStockSkus) {
if(!sku.stock) {
noStockSkus.at(-1)?.push(sku.params.map(p => p.value).join(' / '))
}
if(indexArr[0] === noStockSkus.length && noStockSkus.length < skuCategorys[0]) {
noStockSkus.push([])
}
}
}
return skusList
}

Skus 组件的核心部分的实现


初始化数据


需要将上面生成的数据转化为以下结构:


type SkuStateItem = {
value: string;
/** 与该sku搭配时,该禁用的sku组合 */
disabledSkus: string[][];
}[];

export default function Skus() {
// 转化成遍历判断用的数据类型
const [skuState, setSkuState] = useState<Record<string, SkuStateItem>>({});
// 当前选中的sku值
const [checkSkus, setCheckSkus] = useState<Record<string, string>>({});

// ...
}

将初始sku数据生成目标结构


根据 data (即上面的假数据)生成该数据结构。


第一次遍历是对skus第一项进行的,会生成如下结构:


const _skuState = {
'颜色': [{value: '红', disabledSkus: []}],
'大小': [{value: 'S', disabledSkus: []}],
'款式': [{value: '圆领', disabledSkus: []}],
'面料': [{value: '纯棉', disabledSkus: []}],
}

第二次遍历则会完整遍历剩下的skus数据,并往该对象中填充完整。


export default function Skus() {
// ...
useEffect(() => {
if(!data?.length) return
// 第一次对skus第一项的遍历
const _checkSkus: Record<string, string> = {}
const _skuState = data[0].params.reduce((pre, cur) => {
pre[cur.name] = [{value: cur.value, disabledSkus: []}]
_checkSkus[cur.name] = ''
return pre
}, {} as Record<string, SkuStateItem>)
setCheckSkus(_checkSkus)

// 第二次遍历
data.slice(1).forEach(item => {
const skuParams = item.params
skuParams.forEach((p, i) => {
// 当前 params 不在 _skuState 中
if(!_skuState[p.name]?.find(params => params.value === p.value)) {
_skuState[p.name].push({value: p.value, disabledSkus: []})
}
})
})

// ...接下面
}, [data])
}

第三次遍历主要用于为每个 sku的可点击项 生成一个对应的禁用sku数组 disabledSkus ,只要当前选择的sku项,满足该数组中的任一项,该sku选项就会被禁用。之所以保存这样的一个二维数组,是为了方便后面点击时的条件判断(有点空间换时间的概念)。


遍历 data 当库存小于等于0时,将当前的sku的所有参数传入 disabledSkus 中。


例:第一项 sku(红,S,圆领,纯棉)库存假设为0,则该选项会被添加到 disabledSkus 数组中,那么该sku选择时,勾选前三个后,第四个 纯棉 的勾选会被禁用。


image.png
export default function Skus() {
// ...
useEffect(() => {
// ... 接上面
// 第三次遍历
data.forEach(sku => {
// 遍历获取库存需要禁用的sku
const stock = sku.stock!
// stockLimitValue 是一个传参 代表库存的限制值,默认为0
// isStockGreaterThan 是一个传参,用来判断限制值是大于还是小于,默认为false
if(
typeof stock === 'number' &&
isStockGreaterThan ? stock >= stockLimitValue : stock <= stockLimitValue
) {
const curSkuArr = sku.params.map(p => p.value)
for(const name in _skuState) {
const curSkuItem = _skuState[name].find(v => curSkuArr.includes(v.value))
curSkuItem?.disabledSkus?.push(
sku.params.reduce((pre, p) => {
if(p.name !== name) {
pre.push(p.value)
}
return pre
}, [] as string[])
)
}
}
})

setSkuState(_skuState)
}, [data])
}

遍历渲染 skus 列表


根据上面的 skuState,生成用于渲染的列表,渲染列表的类型如下:


type RenderSkuItem = {
name: string;
values: RenderSkuItemValue[];
}
type RenderSkuItemValue = {
/** sku的值 */
value: string;
/** 选中状态 */
isChecked: boolean
/** 禁用状态 */
disabled: boolean;
}

export default function Skus() {
// ...
/** 用于渲染的列表 */
const list: RenderSkuItem[] = []
for(const name in skuState) {
list.push({
name,
values: skuState[name].map(sku => {
const isChecked = sku.value === checkSkus[name]
const disabled = isChecked ? false : isSkuDisable(name, sku)
return { value: sku.value, disabled, isChecked }
})
})
}
// ...
}

html css 大家都会,以下就简单展示了。最外层遍历sku的分类,第二次遍历遍历每个sku分类下的名称,第二次遍历的 item(类型为:RenderSkuItemValue),里面会有sku的值,选中状态和禁用状态的属性。


export default function Skus() {
// ...
return list?.map((p) => (
<div key={p.name}>
{/* 例:颜色、大小、款式、面料 */}
<div>{p.name}</div>
<div>
{p.values.map((sku) => (
<div
key={p.name + sku.value}
onClick={() =>
selectSkus(p.name, sku)}
>
{/* classBem 是用来判断当前状态,增加类名的一个方法而已 */}
<span className={classBem(`sku`, {active: sku.isChecked, disabled: sku.disabled})}>
{/* 例:红、绿、蓝、黑 */}
{sku.value}
</span>
</div>
))}
</div>
</div>

))
}

selectSkus 点击选择 sku


通过 checkSkus 设置 sku 对应分类下的 sku 选中项,同时触发 onChange 给父组件传递一些信息出去。


const selectSkus = (skuName: string, {value, disabled, isChecked}: RenderSkuItemValue) => {
const _checkSkus = {...checkSkus}
_checkSkus[skuName] = isChecked ? '' : value;
const curSkuItem = getCurSkuItem(_checkSkus)
// 该方法主要是 sku 组件点击后触发的回调,用于给父组件获取到一些信息。
onChange?.(_checkSkus, {
skuName,
value,
disabled,
isChecked: disabled ? false : !isChecked,
dataItem: curSkuItem,
stock: curSkuItem?.stock
})
if(!disabled) {
setCheckSkus(_checkSkus)
}
}

getCurSkuItem 获取当前选中的是哪个sku



  • isInOrder.current 是用来判断当前的 skus 数据是否是整齐排列的,这里当成 true 就好,判断该值的过程就不放到本文了,感兴趣可以看 源码


由于sku是按顺序排列的,所以只需按顺序遍历上面生成的 skuState,找出当前sku选中项对应的索引位置,然后通过 就可以直接得出对应的索引位置。这样的好处是能减少很多次遍历。


如果直接遍历原来那份填充所有 sku 的 data 数据,则需要很多次的遍历,当sku是 6^6 时, 则每次变换选中的sku时最多需要 46656 * 6 (data总长度 * 里面 sku 的 params) 次。


const getCurSkuItem = (_checkSkus: Record<string, string>) => {
const length = Object.keys(skuState).length
if(!length || Object.values(_checkSkus).filter(Boolean).length < length) return void 0
if(isInOrder.current) {
let skuI = 0;
// 由于sku是按顺序排列的,所以索引可以通过计算得出
Object.keys(_checkSkus).forEach((name, i) => {
const index = skuState[name].findIndex(v => v.value === _checkSkus[name])
const othTotal = Object.values(skuState).slice(i + 1).reduce((pre, cur) => (pre *= cur.length), 1)
skuI += index * othTotal;
})
return data?.[skuI]
}
// 这样需要遍历太多次
return data.find(s => (
s.params.every(p => _checkSkus[p.name] === getSkuParamValue(p))
))
}

isSkuDisable 判断该 sku 是否是禁用的


该方法是在上面 遍历渲染 skus 列表 时使用的。



  1. 开始还未有选中值时,需要校验 disabledSkus 的数组长度,是否等于该sku参数可以组合的sku总数,如果相等则表示禁用。

  2. 判断当前选中的 sku 还能组成多少种组合。例:当前选中 红,S ,而 isSkuDisable 方法当前判断的 sku 为 款式 中的 圆领,则还有三种组合 红\S\圆领\纯棉红\S\圆领\涤纶红\S\圆领\丝绸

  3. 如果当前判断的 sku 的 disabledSkus 数组中存在这三项,则表示该 sku 选项会被禁用,无法点击。


const isCheckValue = !!Object.keys(checkSkus).length

const isSkuDisable = (skuName: string, sku: SkuStateItem[number]) => {
if(!sku.disabledSkus.length) return false
// 1.当一开始没有选中值时,判断某个sku是否为禁用
if(!isCheckValue) {
let checkTotal = 1;
for(const name in skuState) {
if(name !== skuName) {
checkTotal *= skuState[name].length
}
}
return sku.disabledSkus.length === checkTotal
}

// 排除当前的传入的 sku 那一行
const newCheckSkus: Record<string, string> = {...checkSkus}
delete newCheckSkus[skuName]

// 2.当前选中的 sku 一共能有多少种组合
let total = 1;
for(const name in newCheckSkus) {
if(!newCheckSkus[name]) {
total *= skuState[name].length
}
}

// 3.选中的 sku 在禁用数组中有多少组
let num = 0;
for(const strArr of sku.disabledSkus) {
if(Object.values(newCheckSkus).every(str => !str ? true : strArr.includes(str))) {
num++;
}
}

return num === total
}

至此整个商品sku从生成假数据到sku的选中和禁用的处理的核心代码就完毕了。还有更多的细节问题可以直接查看 源码 会更清晰。


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

设计呀,你是真会给前端找事呀!!!

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

我在美团三年的前端工程化实践回顾

web
时间过得真快,从20年9月加入美团,转眼已经三年了。在美团的这几年,我应该有接近一半的时间,在做前端工程化相关的工作。 三年,正好合同已经到期,也到了离开的时候,最近相对不忙,正好回顾一下自己做前端工程化的一些思考与踩过的坑。 对前端工程的理解 前端技术的演进...
继续阅读 »

时间过得真快,从20年9月加入美团,转眼已经三年了。在美团的这几年,我应该有接近一半的时间,在做前端工程化相关的工作。


三年,正好合同已经到期,也到了离开的时候,最近相对不忙,正好回顾一下自己做前端工程化的一些思考与踩过的坑。


对前端工程的理解


前端技术的演进


在谈前端工程这个概念之前,我们先回顾一下2000年以后前端技术的演进,主要分四个阶段:



  • 页面开发阶段(2000~2009) :在ECMAScript 2009发布之前,很多前端工作都是以单页面开发为主,需要重点解决兼容性问题,靠工具库提高效率,代表技术如:jQuery、ExtJS等。

  • 模块化开发阶段(2009~2015) :以模块化开发为主,要解决性能问题,靠构建工具和UI框架提高效率,特别是基于Node.js的各种前端工具,代表技术如:Angular、React、Less、Gulp等。

  • 应用开发阶段(2015~2022) :以应用开发为主,要解决工程化问题,靠自动化工具和跨平台提高效率,代表技术如:Webpack、React Native、Flutter等。

  • 智能辅助开发阶段(2022以后) :将前端工程化与 AI 结合,将重复冗余的流程通过智能化实现开发提效,实现智能代码生成、评审、智能编写单测、代码语言转化等。


软件工程的三要素


同时,我们也需要了解一下软件工程的概念。1983年IEEE是这么定义的:软件工程是软件开发、运行、维护和修复软件的系统方法。


基于此,软件界一些前辈提出了软件工程的三要素:



  • 方法:是完成软件开发的各项任务的技术方法,为软件开发提供“如何做”的技术。

  • 工具:为运用方法而提供的自动的或半自动的软件工程的支撑环境。

  • 过程:是为了获得高质量的软件所需要完成的一系列任务的框架。


前端工程化的定义


前端工程化这个词,是国内前端圈子2018年前后才出现的,大概的意思是将(后端已经比较成熟的)许多软件工程概念、实践、工具引入前端开发,提升开发效率。


关于前端工程化的定义,众说纷纭。我们团队在21年初,经过三个多月的调研和讨论,才形成了一个大家都能认可的定义:在前端开发和运维过程中,以降低成本、提高效率、保障质量为目的,通过一系列规范、工具、流程(分别对应软件工程中的方法、工具和过程)作为手段的实践体系。


前端工程化的演进


美团由于业务广泛,大大小小的前端团队得有30个以上,每个团队的业务场景不同,都会建设或采用一套合适的前端工程方案,但其演进过程,一般都会经历以下阶段:



  • 工具化:以针对各自业务场景开发脚手架为主,内置常用的前端组件库,提供代码格式检查、埋点及监控等插件,提升项目初始化的效率。

  • 规范化:面向完成需求的整个研发流程,梳理需求管理、视觉交互设计、评审、开发、联调、测试验收、上线部署和质量监控等相关的规范,进一步建设工具来约束研发过程中的不确定性。

  • 平台化:将支撑研发的有关工具和系统聚合起来,通过套件和插件的设计模式,实现对不同场景的支撑,支持在线初始化项目,横向打通研发的整体链路。

  • 体系化:紧跟前沿技术,集成低代码、在线IDE、代码智能生成或推荐等能力,建设需求、设计、研发、运营一体化的云开发平台。


中后台项目的工程化实践


工程化演进的动力,源于业务复杂度的增加及团队规模的扩大。我在美团工作过的两个部门,都是属于基础研发平台的,在我加入后,所在前端团队需要开发维护的中后台项目都在变多(第一个部门有60多个,第二个部门也有10多个),团队规模也进一步扩大(第一个部门30多人,第二个部门10多人)。


在第二个部门的工程化,主要借助我在第一个部门的前端工具建设,进行定制化应用。因此,着重介绍一下我在第一个部门的前端工程化实践。


工具建设


团队的工具建设,开始于2018年,为了建设美团私有云平台,需要收拢美团基础研发平台所有 IaaS、PaaS 产品,预期两年内会有几十个增量项目接入,我们需要提供高效、稳定的前端支持。急需解决的问题有:



  • 缺乏研发工具。 这一时期,我们的开发手段还比较原始,业务强相关的大量重复工作难以避免,如:前端工程的搭建,接入统一通用的SDK。

  • 机器资源不足。 当时的前端项目还是直接部署在机器上,团队能申请的机器资源有限,难以承接即将接入的大量项目。


针对这两个问题,首先我们建设了自己脚手架工具,并统一了研发流程:



  • 项目模板:我们提供了两套模板用于初始化项目,一套适用于接入私有云平台(面向美团所有研发,需要统一的顶导、侧导,对视觉交互要求高,上线管控严格),一套适用于普通的后台管理(只是部门内少数研发使用,重在快速实现功能)。

  • 研发工具:通过一套自研的中后台组件库把控整体的视觉交互,并提供私有云平台本地开发调试的代理转发工具,解决接口请求的鉴权问题。

  • 集成服务工具:提供了将本地静态资源发布到云存储和接入公司埋点监控等服务的工具,简化和统一了不同项目接入相同的服务。


其次,我们升级了静态资源部署方案:团队的前端大多数项目都是纯静态页面,可以使用云存储代替机器存储,从而解放大多数机器资源。故而我们基于 s3plus 对象存储,研发配套的部署工具,实现了静态 web 项目的无服务架构。


规范制定


到 2020 年的时候,我们需要支持 80 多个基础技术中后台项目的前端工作,当时团队支持项目上存在以下 2 个问题:



  • 无规范,协作难。 随着团队规模的扩大,各个小组的规范和工具出现分叉,同类项目有多套规范及协作工具,跨项目及跨职能协作同学认知和上手成本高,跨项目协作或人员调动阻力大。

  • 工具分散,存在内耗。 团队共存多套同类工具,低水平轮子多,维护成本高;工具没有形成生态,不能发挥规模效应,效率提升有限。


首先,我们联合多个小组接口人,共同梳理标准规范,并通过标准宣讲,拉齐各职能角色的认知,最终形成了六个大类(分别为需求、设计、研发、发布、架构和运维)、26个小类的文字版规范。


然后,我们联合多个部门的前端物料接口人,基于中后台项目前端界面的常见场景,制定了统一的设计规范,从零建设区块和页面模板库,整合已有的基础组件、业务组件和项目模板,形成了完整的前端物料体系。


接着,我们把发布工具从 Plus 平台迁移至 DevTools 流水线,并且通过 WebStatic 平台进行静态网站托管。这样的好处是,发布规则可以通过流水线定制,加入标准化监测度量等工具,从而实现卡控;流水线运行的容器天然可以作为中转站,将前端资源发布S3,解放了机器;WebStatic 平台接管静态网站托管,可以让我们省去复杂重复的路由配置。


最后,我们采用“普法”和“执法”并重的原则,首先通过课程分享和改造宣讲,普及并对齐标准化的价值,完成团队“普法”过程。“执法”前,我们基于标准沉淀多种一键接入工具降低接入成本;无法自动接入的标准,官方给出最佳实践及预计改造时长,协助业务同学排期;“执法”中,提供了检查工具,用于标准校验并收集项目标准化数据,帮助标准化持续运营。


同时,我们把规范标准区分“强制”、“推荐”两个等级。存量项目只需遵守“强制”等级,不影响项目进度的前提下达成团队标准下限;对于增量项目,提供工具高效遵循全部标准。


搭建平台


2021年团队引入了大量前端外包同学,原本的研发工具及增量项目的服务搭建对于外包同学,都有较高的学习成本,因此这一年我们将提效的重点放在了研发工具的体验优化,以及发布规范、架构规范的配套工具落地。主要解决如下问题:



  • 架构规范难学习。项目从创建到上线,需要对接代码仓库、Appkey、发布工具、资源托管服务及网关配置等,涉及基础服务产品多,申请及系统切换操作复杂,即使工作经验丰富,也未必熟悉项目所需的全部中间件。

  • 部分研发工具难上手,体验较差。研发套件中包含工具类型繁多,建设初期,文档完善程度参差不齐,高频使用的物料工具对于新人上手也不够友好。


我们决定通过建设研发工作台落地架构规范,通过自动化的研发流程串联降低新人学习成本,快速搭建增量项目;同时,为解决研发套件使用体验问题,我们同步建设了 VSCode IDE插件,集成高频使用的插件、物料使用等工具,降低学习和切换成本,增强用户的使用体验。


同时,为满足不同业务场景的定制需求,我们将各场景研发流程抽象成 「场景模板」, 它是最佳实践的载体,前文中的自动化创建流程就是基于场景模板来串联。


场景模板由初始化模板(生成项目基础结构),研发工具插件(CLI 层面的插件 preset),基础服务配置方案组成,每部分可以灵活配置,一定程度上满足不同业务场景的定制需求,团队工程负责人可以按需定制自己的场景模板。


平台化以后,我们的前端工程方案就可以满足公司更多部门接入使用,发挥更大的价值。比如,我转岗的部门在推进工程化的时间,基于这套方案,只需结合视觉项目的特点,替换前端物料、生成项目的脚手架即可。


形成体系


2022年外包团队规模和产品规模即将进一步扩大,然而当前工具对于效率的提升也逐渐出现瓶颈,我们期望对当前的主要业务场景,即对中后台业务,进行深度提效的探索。另一方面,现有大部分规范已有配套工具保障,但前置的需求以及后置的运维环节依然没有形成闭环,我们期望平台能有更沉浸式的体验,建设中后台场景的体系化解决方案。需要解决以下问题:



  • 提效瓶颈,研发提效工具待加强: 分析业务现状和参考业界,传统编码(ProCode)的辅助工具完整性和易用性需持续加强,业界的低代码(LowCode)实践也很适用于我们的中后台场景,我们也将在这一领域探索建设。

  • 需求、运维规范未闭环: 当前的平台能够串联从创建项目到部署的研发流程,但是前置的需求、设计管理和后置的运维规范还不完善,对于相关工具(如项目管理和监控工具)的应用也没有形成标准。


我们希望将研发工作台打造为云开发平台,通过集成在线 IDE 开发工具和低代码自动化研发工具,对于中后台场景深度提效;同时也要与项目管理、设计平台、监控等平台加深协作与融合,串联前置的需求设计环节与后置的运维环节,形成中后台场景的体系化开发平台。


sdk项目的工程化实践


2022年初,为了从北京换到深圳定居,我换了部门,在新的部门,需要开发维护的npm包比之前多了很多。如果没有统一的工程标准,不仅开发维护的效率很低,同时SDK的易用性也会比较差。


过去一年,我参与了多个SDK项目的开发维护工作,同时前两年也参与了面向公司的中后台前端项目的工程建设,于是我将这些实践和采坑经验进行总结,形成了一套前端sdk项目的工程标准。


业务项目和SDK项目的区别


通过表格对比可以看到,业务项目和SDK的项目还是有较大区别的,除了有一些公共标准可以复用外,SDK项目需要增加打包构建、发npm包、多包管理、文档管理和门户建设等相关的工程标准。


类型产品要求使用方式技术栈
业务项目更加注重功能的实现,如果是C端项目,还需要关注首屏。多数都过网页链接使用。以Vue/React框架开发为主(需要非常熟悉相关框架的api),还会涉及HTML、CSS,多数使用Webpack进行打包,一般不用考虑测试。
SDK项目更加关注稳定性、兼容性、性能、包大小和使用文档。一般通过npm包安装到项目中或在html 文件中以cdn嵌入脚本使用。纯JS/TS开发为主(需要深入了解编程语言特性,比如类的创建、继承和各种封装),除了组件库基本不涉及HTML和CSS,多数使用Rollup打包和使用Jest进行单元测试。

工程方案


我们基于业务项目制定的规范,对于有差异的部分,比如依赖包管理方案、目录结构、文件命名、本地开发等,制定了新的规范;而对于没有包含的部分,比如文档管理、官网建设、发布npm包及其权限管理等,补充了新的规范。


我们基于前面建设的云开发平台,提供了一个面向 SDK 开发的场景模板来创建项目:创建成功后,会自动注册前端appkey,创建仓库并使用sdk的项目模板初始化项目,把sdk官网的静态资源接入webstatic,并接入发布流水线,提供默认域名供用户访问。


相比之前的方案,这个方案不仅开发调试及发布验证更加方便,还能提供默认的域名访问该sdk网站,让用户可以快速查看相关的接入文档和教程,体验sdk提供的功能。


相关说明


整个项目使用vite工具进行构建,使用rollup 进行打包,打包成功后,即可通过本地或流水线发布到公司的私有npm。


我们没有使用lerna进行多包管理,而是使用了pnpm的方案,所以要求必须本地安装pnpm,然后通过pnpm安装package.json文件中引入的npm包。


只需配置根目录下的pnpm-workspace.yaml文件即可,示例如下:


packages:
- 'packages/**'
- site

在packages目录下,一个子目录对应为一个子npm包。site 目录为sdk 对应的官网代码,本地开发时,可以在site 中的某个页面,引入packages中的某个包进行源码调试。


在根目录执行 npm start 就可以打开sdk 官网了,然后跳转到demo 演示的页面,修改site目录下对应页面或者packages下对应的npm包,即可开始进行SDK的开发调试了。


所有的文档管理相关代码,都在site 目录下的src目录,如果需要更新文档,直接在markdown 目录编辑对应的文档即可。


如果发布了新的sdk,需要验证sdk的可用性,需要先将site目录下package.json文件对应的npm包修改为最新的版本,提交后远程仓库后,再选择对应流水线发布到自己想验证的环境。


总结


在美团工作的三年,在技术和视野上,对我的帮助都很大,接触的领导和身边的小伙伴都很优秀,有些工作是我对最终结果负责,有些我只是重在参与。


我们会把事情分为业务开发和框架(工具)开发:



  • 业务开发主要是实现产品需求,要对业务有深入了解,掌握团队所用到的技术栈和工具。

  • 框架(工具)开发主要是为提升业务开发效率而开发的框架或工具,框架是把系统可复用层抽象出来,如网络层,存储层等,工具是研发过程中效率工具,如自动化测试工具,持续交付工具等。


通常上系统是有多个模块组成,那么会有一个从复杂到简单的拆解过程,既然系统有分层架构,那我们会按照每个人技术水平来安排不同复杂度的工作。我们要不断提升两个标准,一是通过对人才的培养提高上限,二是通过工程工具建设提升团队下限。


工程化永远是围绕着质量、体验和效率三个维度进行建设,来保证高效、高质量地完成业务需求,减少跨项目、跨团队的协作成本。但前端工程化不是万金油,它是在特定时期面对特定场景的解决方案。


平台体系的建设往往会被业务结构和技术架构所约束,要尽量结合团队的业务场景和技术现状来制定合理的解决方案,避免仅凭个人的技术思考来主观驱动,所以还是要结合自身组织特点,先清楚地认识自己所处的阶段,再去实践并验证。


老王(王慧文)在演讲曾提到过:“不要为自己设限”,所以前端工程师在前端工程化中,应该积极承担业务工程化建设或工程工具建设工作。《论语》中说道:“工欲善其事,必先利其器”,所以面对复杂工程,我们要学会用工具来提升效率,使复杂问题简单化。


2023年我的主要精力都在做前端智能化,在工程化上的投入比较少,但是我相信借助AI,前端工程化一定会迎来重大的变革。


作者:三一习惯
来源:juejin.cn/post/7268533072995598347
收起阅读 »

前端学哪些技能饭碗越铁收入还高

web
随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。 但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深...
继续阅读 »

随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。


但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深耕。


本文,我们就聊聊,掌握了哪些技能,能让前端同学,收入高且稳定。


端智能


首推的是端智能,很多行业大咖都认为,随着ChatGPT的横空出世,开启了第四次工业革命,很多产品都可以用大模型重做一遍。当前,我创业的方向,也和大模型有关。


当前的大模型主要还跑在云端,但云端的成本高,大模型的未来在端智能,这也是小米创始人雷军在今年一次发布会上提出的观点。


在2023年8月14日的雷军年度演讲中,雷军宣布小米已经在手机端跑通13亿参数的大模型,部分场景效果媲美云端。


目前,端上大模型的可行性和前景已经得到了业内的普遍认可,国内外各个科技大厂在大模型的端侧部署领域均开始布局,目前大量工程已在PC端、手机端实现大模型的离线部署,更有部分App登陆应用商店,只需下载即可畅通无阻地对话。


我们相信,在不久的将来,端上大模型推理将会成为智能应用的重要组成部分,为用户带来更加便捷、智能的体验。


我在美团从零研发了web端智能推理引擎,当时立项时,就给老板画饼,美团每天的几百亿推理调用,如果有一半用端智能替代的话,每年能为公司节省上亿元。


要想掌握端智能,需要学习深度学习的基本知识,还要掌握图形学和C++编程,通过webgl或webassembly 技术实现在Web端执行深度学习算法。


图形学


前面提到的端智能,只是涉及到了图形学中的webgl计算,但图形学的未来在元宇宙,通过3D渲染,实现VR、AR、XR等各种R。


计算机图形学是一门快速发展的领域,涵盖了三维建模、渲染、动画、虚拟现实等众多技术和应用。在电影、广告、游戏等领域中,计算机图形学的应用已经非常广泛。


熟练使用threejs开发各种3D应用,只能算是入门。真正的图形学高手,不仅可以架构类似3D家装软件的大型应用,而且能掌握渲染管线的底层原理,熟练掌握各种模型格式和解决各种软件,进行模型转换遇到的各种兼容问题。


随着计算机硬件和算法的不断进步,计算机图形学正迎来新的发展趋势。


首先是实时渲染与逼真度提升



  • 实时渲染技术:随着游戏和虚拟现实的兴起,对实时渲染的需求越来越高。计算机图形学将继续致力于研发更高效的实时渲染算法和硬件加速技术,以实现更逼真、流畅的视觉效果。

  • 光线追踪与全局照明:传统的实时渲染技术在光照模拟方面存在挑战。计算机图形学将借助光线追踪等技术,实现更精确的全局照明效果,提升场景的真实感和细节表现。


其次是虚拟与增强现实的融合



  • 混合现实技术:计算机图形学将与传感器技术、机器视觉等相结合,推动虚拟现实与增强现实的融合发展。通过实时感知和交互,用户可以在真实世界中与虚拟对象进行互动,创造更沉浸式的体验。

  • 空间感知与虚拟对象定位:计算机图形学将致力于解决空间感知和虚拟对象定位的挑战。利用深度学习、摄像头阵列等技术,实现高精度的空间感知和虚实融合,为虚拟与增强现实应用带来更自然、精确的交互方式。


再次是计算机图形学与人工智能的融合



  • 生成对抗网络(GAN)在图形生成中的应用:GAN等人工智能技术为计算机图形学带来了新的创作手段。通过训练模型生成逼真的图像和场景,计算机图形学能够更便捷地创建大量内容,并提供个性化的用户体验。

  • 计算机图形学驱动的虚拟人物与角色生成:结合计算机图形学和人工智能技术,研究人员正在努力开发高度逼真的虚拟人物和角色生成方法。这将应用于游戏、影视等领域,带来更具情感表达和交互性的虚拟角色。


最后是可视化分析与科学研究。



  • 一是大数据可视化:随着大数据时代的到来,计算机图形学在可视化分析方面扮演着关键角色。通过创新的可视化方法和交互技术,研究人员能够更深入地理解和分析庞大而复杂的数据集,揭示潜在的模式和趋势。

  • 二是科学数据可视化:计算机图形学在科学研究中的应用也日益重要。通过将科学数据转化为可视化形式,研究人员能够更直观地理解复杂的数据模式和关系,加快对科学问题的洞察和发现。这种可视化分析有助于领域如天文学、生物学、气象学等的研究进展。


工程提效


其实,过去三年我在美团的工作,至少有一半的精力是做和工程提效相关的事情。当然,也做了降本的事情,从零搭建外包团队。


就像我之前总结的文章:我在美团三年的前端工程化实践回顾 中提到那样,前端工程提效,一般会按照工具化、标准化、平台化和体系化进行演进。


相比前面的端智能和图形学,除了建设低代码平台和WebIDE有点技术难度,其他更多需要的是沟通、整合资源的能力,既要有很强的项目管理能力,又要人产品思维。


前两个方向做好了我们一般称为技术专家,而工程提效则更偏管理者,未来可以成为高管或自己创业。


总结


端智能和图形学,我在美团都尝试过进行深耕,但自己性格外向,很难坐得住。工程提效做得也一般,主要是因为需要换城市而换了部门,没有机缘继续推进,在美团很难往上走,所以只能尝试自己创业。


作者:三一习惯
来源:juejin.cn/post/7310143510103064585
收起阅读 »

树形列表翻页,后端: 搞不了搞不了~~

web
背景 记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用...
继续阅读 »

背景


记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用户说一进到这个报告页面就卡住不动了,上去一查发现不得了,都是铁杆用户,每年作品都几百个,导致几年下来,这个报告返回了几千个作品,包含上千的图片。


问题分析


上千的图片,肯定会卡,首先想到的是做图片懒加载。这个很简单,使用一个vue的全局指令就可以了。但是上线发现,没啥用,dom节点多的时候,懒加载也卡。


然后就问服务端能不能支持分页,服务端说数据太散,连表太多,树形结构很难做分页。光查询出来就已经很费劲了。


没办法于是想了一下如何前端来处理掉。


思路



  1. 由于是app中的嵌入页面,首先考虑通过滚动进行分页加载。

  2. 一次性拿了全部的数据,肯定不能直接全部渲染,我们可以只渲染一部分,比如第一个节点,或者前几个节点。

  3. 随着滚动一个节点一个节点或者一批一批的渲染到dom中。


实现


本文仅展示一种基于vue的实现


1. 容器

设计一个可以进行滚动翻页的容器 然后绑定滚动方法OnPageScrolling



<style lang="less" scoped>

.study-backup {

overflow-x: hidden;

overflow-y: auto;

-webkit-overflow-scrolling: touch;

width: 100%;

height: 100%;

position: relative;

min-height: 100vh;

background: #f5f8fb;

box-sizing: border-box;

}

</style>

<template>

<section class="report" @scroll="OnPageScrolling($event)">

</section>

</template>



2.初始化数据

这里定义一下树形列表的数据结构,实现初始化渲染,可以渲染一个树节点,或者一个树节点的部分子节点



GetTreeData() {

treeapi

.GetTreeData({ ... })

.then((result) => {

// 处理结果

const data = Handle(result)

// 这里备份一份数据 不参与展示

this.backTreeList = data.map((item) => {

return {

id: item.id,

children: item.children

}

})

// 这里可以初始化为第一个树节点

const nextTree = this.backTreeList[0]

const nextTansformTree = nextTree.children.splice(0)

this.treeList = [{

id: nextTree.id,

children: nextTansformTree

}]

// 这里可以初始化为第一树节点 但是只渲染第一个子节点

const nextTree = this.backTreeList[0]

const nextTansformTree = nextTree.children.splice(0, 1)

this.treeList = [{

id: nextTree.id,

children: nextTansformTree

}]

})

},


3.滚动加载

这里通过不断的把 backTreeList 的子节点转存入 treeList来实现分页加载。



OnPageScrolling(event) {

const container = event.target

const scrollTop = container.scrollTop

const scrollHeight = container.scrollHeight

const clientHeight = container.clientHeight

// console.log(scrollTop, clientHeight, scrollHeight)

// 判断是否接近底部

if (scrollTop + clientHeight >= scrollHeight - 10) {

// 执行滚动到底部的操作

const currentReport = this.backTreeList[this.treeList.length - 1]

// 检测匹配的当前树节点 treeList的长度作为游标定位

if (currentReport) {

// 判断当前节点的子节点是否还存在 如果存在则转移到渲染树中

if (currentReport.children.length > 0) {

const transformMonth = currentReport.children.splice(0, 1)

this.treeList[this.treeList.length - 1].children.push(

transformMonth[0]

)

// 如果不存在 则寻找下一树节点进行复制 同时复制下一节点的第一个子节点 当然如果寻找不到下一树节点则终止翻页

} else if (this.treeList.length < this.backTreeList.length) {

const nextTree = this.backTreeList[this.treeList.length]

const nextTansformTree = nextTree.children.splice(0, 1)

this.treeList.push({

id: nextTree.id,

children: nextTansformTree

})

}

}

}

}


4. 逻辑细节

从上面代码可以看到,翻页的操作是树copy的操作,将备份树的子节点转移到渲染树中



  1. copy备份树的第一个节点到渲染树,同时将备份树的第一个节点的子节点的第一个节点转移到渲染树的第一个节点的子节点中

  2. 所谓转移操作,就是数组splice操作,从一颗树中删除,然后把删除的内容插入到另一颗树中

  3. 由于渲染树是从长度1开始的,所以我们可以根据渲染树的长度作为游标和备份树进行匹配,设渲染树的长度为当前游标

  4. 根据当前游标查询备份树,如果备份树的当前游标节点的子节点不为空,则进行转移

  5. 如果备份树的当前游标节点的子节点为空,则查找备份树的当前游标节点的下一节点,设为下一树节点

  6. 如果找到了备份树的当前游标节点的下一节点,扩展渲染树,将下一树节点复制到渲染树,同时将下一树节点的子节点的第一节点复制到渲染树

  7. 循环4-6,将备份树完全转移到渲染树,完成所有翻页


扩展思路


这个方法可以进行封装,将每次复制的节点数目和每次复制的子节点数目作为传参,一次可复制多个节点,这里就不做展开


作者:CodePlayer
来源:juejin.cn/post/7270503053358612520
收起阅读 »

你不会还在useEffect中请求数据吧

web
使用React Query代替useEffect获取数据的优势与对比 在构建现代React应用时,我们经常需要从后端API获取数据来渲染界面。传统的方式是使用React的useEffect钩子结合fetch或axios等HTTP请求库来完成数据获取和状态管理。...
继续阅读 »

使用React Query代替useEffect获取数据的优势与对比


在构建现代React应用时,我们经常需要从后端API获取数据来渲染界面。传统的方式是使用React的useEffect钩子结合fetchaxios等HTTP请求库来完成数据获取和状态管理。然而,随着React Query的出现,获取和同步服务器状态的方式得到了显著的改进。本文将详细介绍使用React Query代替useEffect获取数据的原因,并通过示例对比两种方式在代码层面的不同,在最后总结React Query的优势。


传统方式:使用useEffect获取数据


在没有使用React Query之前,我们通常会这样获取数据:


import React, { useState, useEffect } from 'react';

function MyComponent() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('https://my-api/data');
const result = await response.json();
setData(result);
} catch (error) {
setError(error);
}
setIsLoading(false);
};

fetchData();
}, []);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
}

这段代码虽然能工作,但存在几个问题:缺乏缓存策略、复杂的错误处理、不自动的数据更新、重复的数据请求等。


使用React Query改进数据获取


接下来,看看React Query如何为我们解决上述问题和简化代码:


import React from 'react';
import { useQuery } from 'react-query';

async function fetchData() {
const response = await fetch('https://my-api/data');
return response.json();
}

function MyComponent() {
const { data, isLoading, error } = useQuery('data', fetchData);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
}

在这个改进后的版本中,我们用useQuery钩子来代理数据加载。这行代码做了很多工作:它自动进行数据请求,处理加载状态和错误状态,还负责缓存和更新数据。


使用React Query的原因


简化的状态管理


React Query内部处理了数据的加载(isLoading)、数据更新(isFetching)、错误(error)状态管理,这使得开发者无需手动设置这些状态。


自动化的数据缓存和无效化


React Query还提供了出色的数据缓存策略。默认情况下,当组件卸载再重新挂载时,React Query会使用旧的缓存数据,同时在后台静默地为你刷新数据,保证数据的新鲜度。


更好的错误和重试处理


通过对错误状态的内部管理,React Query提供了错误捕获的机制并允许自动重试功能。这比手动实现要简单得多。


优化请求节省带宽


React Query会自动去重和合并并发的查询请求,减少不必要的网络请求,节省宽带。


React Query的优势


总结来说,React Query的主要优势包括:



  • 自动化:管理请求生命周期(查询、缓存、更新、重试)无需手动编写代码。

  • 减少样板代码:少写很多状态处理的逻辑,让代码简洁易维护。

  • 性能提升:智能缓存和数据更新策略,更少的重新渲染。

  • 鲁棒性:更健壮的错误处理和重试逻辑。

  • 开箱即用:丰富的功能如后台获取、分页、无限加载等。


在创建现代化的React应用程序时,React Query提供了一种更智能、更高效和简单的方法来处理数据获取和同步,这也是越来越多的React开发者选择它的原因。


React Query


下面将详细介绍React Query的功能,以及它如何在一个实际的场景中被使用。我们将构建一个用户列表的应用,这个应用将展示用户数据、支持数据刷新、加载更多用户以及处理错误重试。


项目准备


首先,确保已经在React项目中安装了React Query:


npm install react-query

或者


yarn add react-query

功能概览



  • 数据获取 (useQuery): 用于获取数据并提供状态管理,比如loading, error, data。

  • 缓存与背景更新 (staleTimecacheTime): 确定数据保持新鲜的时间,以及未被使用时保持在缓存中的时间。

  • 自动重试 (retry): 当请求失败时,自动进行重试。

  • 分页和加载更多 (页码或游标): 当我们需要分页或者无限加载数据时使用。

  • 数据预加载 (queryClient.prefetchQuery): 加载关键数据以提升用户体验。

  • 数据变异 (useMutation): 提交数据至服务器,并更新本地缓存。


示例应用


获取用户列表


我们使用useQuery钩子来获取用户数据。这个钩子会自动发起请求并监听数据状态。


import { useQuery } from 'react-query';

const fetchUsers = async (page = 0) => {
const response = await fetch(`https://my-api/users?page=${page}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};

function Users() {
const { data, error, isLoading, isFetching } = useQuery('users', fetchUsers);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
<>
{data.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
{isFetching ? <span>Updating...</span> : null}
</>

);
}

自动刷新和背景更新


React Query可以配置数据自动刷新的时间,我们可以设置staleTime来避免不必要的后台更新,同时让我们的数据保持最新。


const { data } = useQuery('users', fetchUsers, {
staleTime: 5 * 60 * 1000 // 每5分钟更新一次数据
});

自动重试


如果请求失败,React Query可以自动尝试重新获取数据:


const { data } = useQuery('users', fetchUsers, {
retry: 2 // 请求失败会尝试2次重试
});

分页和加载更多


对于需要加载更多数据的情况,我们可以使用React Query的页码或游标方法来实现:


const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
'users',
({ pageParam = 0 }) => fetchUsers(pageParam),
{
getNextPageParam: (lastPage, allPages) => lastPage.nextPage,
}
);

// ...

<button onClick={() => fetchNextPage()} disabled={!hasNextPage}>
Load More
</button>


加载更多的按钮会根据hasNextPage来判断是否还有更多数据可以加载。


数据预加载


我们可以在用户的鼠标悬浮到某个按钮上时提前获取数据:


const queryClient = useQueryClient();

// ...

<button
onMouseEnter={() =>
queryClient.prefetchQuery('more-users-data', fetchAdditionalUsers)}
>
Show More Users
</button>


数据变异


当需要提交数据到服务端时,我们可以使用useMutation来处理:


import { useMutation, useQueryClient } from 'react-query';

const addUser = async (newUser) => {
const response = await fetch(`https://my-api/users`, {
method: 'POST',
body: JSON.stringify(newUser)
});
if (!response.ok) {
throw new Error('Could not add user');
}
return response.json();
};

function AddUser() {
const queryClient = useQueryClient();
const mutation = useMutation(addUser, {
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries('users');
}
});

return (
<button
onClick={() =>
{
const newUser = { name: 'New User' };
mutation.mutate(newUser);
}}
>
Add User
</button>

);
}

当我们向服务端增加一个新用户时,使用useMutation并提供一个成功的回调,该回调通过调用queryClient.invalidateQueries来标记用户列表的缓存为无效,以便它可以自动重新获取最新的用户列表。


总结React Query的优势


通过上述示例,我们可以看到React Query提供了强大且灵活的功能来处理数据的获取、缓存、更新、预加载、变异等操作。它大大简化了数据同步和状态管理的复杂性,使开发者可以专注于构建交互式的用户界面,而不必担心数据操作的底层细节。此外,React Query的自动重试和智能缓存策略可以提高应用的健壮性和用户的体验。


最后,简要地复习一下React Query的优势:



  1. 内置缓存功能:React Query 为获取的数据提供缓存机制,这意味着当组件重新渲染或者同用户交互时,相同的数据正在加载,不需要再次发起网络请求,可以直接从缓存中获取数据。这减少了不必要的网络请求,提高了应用的效率。

  2. 错误处理和错误重试:在处理异常数据时,错误处理和错误和错误重试在其他较繁琐。React Query 提供了强化的方式来处理这些状态,简化了开发者的工作。

  3. 优化数据获取:React Query 会自动合并重复的查询请求,并将它们批量处理。这意味着如果多个组件请求相同的数据,React Query 只会发送一次网络请求,并且将数据分发给所有请求的组件。

  4. 简洁高效和提高内存性能:通过减少不必要的网络请求和优化数据处理,React Query 可以帮助节省带宽并提高应用的响应性能。

  5. 数据同步:在复杂的应用中,保持组件间数据的同步是一个挑战。React Query 通过其高层机制,帮助保持不同组件间数据的一致性。


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

深入探究npm run的底层原理

web
起因 某一天,我正在很悠闲的喝着小茶,无所事事浏览着技术文章,忽然看到一篇文章成功的吸引了我的眼球——你真的了解npm run吗,看完之后打开了一扇新世界的大门。 我陷入了沉思之后,有那么一瞬间感觉到了不可思议。惯性思维下,我们已经习惯性的认为命令就是要通过n...
继续阅读 »

起因


某一天,我正在很悠闲的喝着小茶,无所事事浏览着技术文章,忽然看到一篇文章成功的吸引了我的眼球——你真的了解npm run吗,看完之后打开了一扇新世界的大门。


我陷入了沉思之后,有那么一瞬间感觉到了不可思议。惯性思维下,我们已经习惯性的认为命令就是要通过npm run的方式进行执行,而没有进一步思考为什么是这样的。大多数可能跟我一样,第一反应是:难道不是因为在 package.json 中定义了各种的 script 命令,然后我们通过npm run xxx的方式进行执行吗?


揭开事情的迷雾,我们抱着打破沙锅问到底的心态来思考一下:


我们为什么要执行 npm run start 命令,而不是直接执行 react-scripts start 呢?


难道不是因为使用方便吗


这是我的第一反应,但是,实际上可能不是的。为了验证一下两者的区别,我立马跑去打开我的测试项目,分别使用了两种方式。意外的发现直接执行命令的方式竟然报错了,而通过npm run 的方式执行的时候一如既往的Ok。


image.png


为什么命令会不存在呢


报错信息很明显,直接执行命令的时候,系统报错:react-scripts 命令不存在。


这个时候我就感到有点不可思议了。


既然命令不存在,凭什么npm run就可以执行呢?


不知道大家在windows上安装node的时候,需要将node配置到系统环境变量里面去了,然后我们可以全局通过 node -v 来验证node是否安装成功和查询当前node版本信息。难道说 npm run 的玄机跟node配置过环境变量有关系吗?


抱着怀疑的态度,我在node文件夹中一通翻找(我是基于nvm进行node管理的,可能跟直接使用node的目录结构有所出入),终于找到了问题的关键信息:安装依赖的时候,会在这里创建几个命令文件。


image.png


经过几次反复的安装、删除依赖操作之后,终于确认了我的想法。每次我们通过 npm i xxx -g 安装某个依赖的时候,除了在node下的node_modules文件夹中安装对应的依赖包之外,还会在node下创建这个依赖的可执行文件(对应不同的环境,会有好几个不同的命令文件)。


这个时候,我忽然想起来了linux上的操作,在linux上安装全局依赖的时候,我们安装完依赖之后,还需要手动创建软连接。两相印证,事实的真相已经很明显了。


    ln -s /usr/local/src/nodejs/bin/node /usr/local/bin/node

ln -s /usr/local/src/nodejs/bin/npm /usr/local/bin/npm

安装依赖的时候,会在bin目录下创建一个对应的可行性文件,这个其实就跟我们node文件夹下创建的这个npm文件夹的性质是一样的。


npm run 命令可执行总结


我们经过一番摸索终于弄清楚了,这里我们再一起来总结一下:



  1. 我们安装依赖的时候,在对应的文件夹下创建了对应的可行性命令;

  2. 我们执行npm run命令的时候会在当前目录中查找相关命令,如果找到的话,直接运行对应的命令;

  3. 如果没有找到的话,会到全局的node文件夹下查找相关的命令,如果找到的话,直接运行对应的命令;

  4. 如果依然没有找到的话,就会报错误信息了


为什么会创建多个可执行文件呢


前面我们说到了,创建的可执行文件是有多个。细心的你可能已经注意到其中的一个可执行文件xxx.cmd了,它的类型很明显已经告诉我们它是什么了 —— Windows 命令脚本。大胆的猜测一下:另外几个分别对应的是不同环境的可执行命令,比方说:没有文件后缀的可执行文件,其实就是我们前面说到的在linux中安装的软链接的方式。


我们大致看一下其中一个cross-env.cmd的可执行命令的内容(假装可以看得懂)。


    @ECHO off
SETLOCAL
CALL :find_dp0

IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)

"%_prog%" "%dp0%\node_modules\cross-env\src\bin\cross-env.js" %*
ENDLOCAL
EXIT /b %errorlevel%
:find_dp0
SET dp0=%~dp0
EXIT /b

虽然看不懂,但是其中很重要的一个点我们其实还是可以猜出来的 "%_prog%" "%dp0%\node_modules\cross-env\src\bin\cross-env.js" %*将可执行命令 cross-env 指向对应的依赖的bin文件 bin\cross-env.js。


这里其实变相的给我们解释了另外一个问题。


为什么安装依赖可以创建可执行命令呢


在依赖的package.json中配置了bin属性,定义了可执行命令的名字和可执行命令的文件,当我们通过npm安装依赖的时候,npm就会根据声明的bin属性来创建对应的可执行文件。


    "bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},

相信看到这里之后,大家应该心里已经很清楚npm run的底层原理了。


打完收功


好了,有关npm run的内容暂时就这么多了,希望对大家有所帮助。


欢迎大家在下方进行留言交流。


作者:花开花落花中妖
来源:juejin.cn/post/7313203461705580580
收起阅读 »

董老师的话充满力量——手写call、apply、bind

web
前言: 大家好,我是小瑜。最近在网上看到了东方甄选和董宇辉的小作文事件,我也一直是众多吃瓜群众的一员,看完后董宇辉俞敏洪的联合直播,心中也有很多感触。 董宇辉老师说,一放假,就喜欢往家里跑,因为接近土地可以感到踏实。一个千万网红这种纯粹质朴的精神着实让人很贴切...
继续阅读 »

前言:


大家好,我是小瑜。最近在网上看到了东方甄选和董宇辉的小作文事件,我也一直是众多吃瓜群众的一员,看完后董宇辉俞敏洪的联合直播,心中也有很多感触。


董宇辉老师说,一放假,就喜欢往家里跑,因为接近土地可以感到踏实。一个千万网红这种纯粹质朴的精神着实让人很贴切。


特别是董老师的另外一番话:你必须人生中有一段经历是自己走过去的。你充满了痛苦,然后充满了孤独,但这个东西叫做成长,好的生活和幸福的经历是不能带来成长的,所以能见到很多人,四五十岁看着还很幼稚,说明他从来没有受到苦,能让你成长的东西,就是让你反思的东西,因为在历史的长河中进化是痛苦的,逼不得已,人才会进步很成长,所以成长都不快乐。但同时也恭喜你一直在成长。


在学习以及编写这篇文章的时候,我也是痛苦的,同时也有所收获。接下来给大家分享this指向以及手写call、apply、bind。


在这之间给大家简单举几个例子说明下this指向的不同


普通函数调用


// 谁调用就是谁, 直接调用window
function sayHi() {
console.log(this); // window
}
sayHi() // === window.sayHi()

对象中的方法调用


const obj = {
name: 'zs',
objSayHi() {
console.log(this) // obj
setTimeout(() => {
console.log(this, 'setTimeout'); // obj
}, 1000),
function inner() {
console.log(this); // window
}
inner()
},
qwe: () => console.log(this) // window
}
obj.objSayHi()
obj.qwe()

obj.objSayHi() => obj



  • 因为是 **obj **对象调用,所以 **this **指向 **obj **这个对象


obj.qwe() => window



  • 对于箭头函数 qwe,它捕获的是定义时外部的 this 上下文。在浏览器中全局范围内的箭头函数 qwethis 指向的是全局对象 window(或者是全局的 this,具体取决于执行上下文)。


inner() => window



  • inner() 函数是通过常规函数声明方式定义的。在 JavaScript 中,常规函数声明方式中的 this 在严格模式下指向 undefined,而在非严格模式下(例如浏览器环境中),this 指向全局对象(在浏览器中通常是 window 对象)。因此,当 inner() 函数在 objSayHi() 方法内部被调用时,其 this 指向全局对象 window


setTimeout => obj



  • objSayHi 方法中,setTimeout 中的回调函数使用了箭头函数。箭头函数内部的 this 会捕获最近的普通函数(非箭头函数)的 this 值,也就是 objSayHi 被调用时的 this。因此,setTimeout 中的箭头函数捕获到的 this 值指向的是 obj 对象。


总结:浏览器环境中, 谁调用this指向谁,但是箭头函数的this义是外部的 this 上下文。通过常规函数声明方式定义this指向window。其他关于this指向可以参考这张图


image.png


修改this指向


call


第1个参数为this,第2-n为传入该函数的参数


function myThis1(name, age) {
console.log(this);
console.log(name);
console.log(age);
}
const obj = {
name: 'zs',
age: 18
}
myThis1.call(obj, 'ls', 20) // {name:"zs",age:18} ls 18

apply


第1个参数为this,第2-n已数组的方式传递


function myThis1(name, age) {
console.log(this);
console.log(name);
console.log(age);
}
const obj = {
name: 'zs',
age: 18
}
myThis1.apply(obj, ['王五', 18]) // {name:"zs",age:18} 王五 18

bind


bind() 方法创建一个新函数,当调用该新函数时,它会调用原始函数并将其 this 关键字设置为给定的值,同时,还可以传入一系列指定的参数


function myThis1(name, age) {
console.log(this);
console.log(name);
console.log(age);
}
const obj = {
name: 'zs',
age: 18
}

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/92cfe20fc6374374bacf97bcc3d31ac6~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=814&h=308&s=145955&e=png&b=fafafa)
const fn = myThis1.bind(obj, '赵六',30)
fn() // {name:"zs",age:18} 赵六 30

手写call函数


要求实现


const obj = {
name: 'zs',
age: 20
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
myFn.myCall(obj, 1, 2, 3, 4, 5)

简单思路:



  1. ** **原本并不存在 **myCall **方法,那么如何去创建这个方法?

  2. 如何让函数内部的 **this **为 某个对象?

  3. 如何将调用时传入的参数传入到 **myFn **函数中?


实现思路1:通过函数原型的方式,给原型添加 myCall 方法,这样通过原型链就可以使用


Function.prototype.myCall = function () {
console.log('myCall被调用了');
}
myFn.myCall()


实现思路2:在myCall调用的时候将obj传入到函数中,并根据谁调用this就指向谁的原则给对象添加this方法并执行


首先可以打印看一下thisArg,this 分别是什么


const obj = {
name: 'zs',
age: 20
}
Function.prototype.myCall = function (thisArg) {
console.log('myCall被调用了',thisArg,this);
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
myFn.myCall(obj)

image.png
很明显 **thisArg 就是 obj **对象 而 this就是 myFn 这个函数,那么就可以根据谁调用this就指向谁的原则,将obj这个对象也就是 **thisArg **添加 myCall 方法 = this


  Function.prototype.myCall = function (thisArg) {
console.log('myCall被调用了', thisArg, this);
thisArg.myCall = this
thisArg.myCall()
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
const obj = {
name: 'zs',
age: 20
}
myFn.myCall(obj, 1, 2, 3, 4, 5)

此时就发现,this已经成功指向了这个obj对象,但是还差参数没有传递,接下去就去实现


image.png


实现思路3:利用剩余参数加展开运算符传入参数


  Function.prototype.myCall = function (thisArg,...args) {
console.log('myCall被调用了', thisArg, this);
thisArg.myCall = this
thisArg.myCall(...args)
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
const obj = {
name: 'zs',
age: 20
}
myFn.myCall(obj, 1, 2, 3, 4, 5)

此时就基本上可以完成了,还有一点优化,就是查看** obj 发现 myCall **是一直存在的,因为之前通过给原型添加方法,希望的是使用完成后将myCall方法删除,这里只需要 在 **myCall 最后再添加一句 delete thisArg.myCall **即可


优化: 增加返回值并 利用 Symbol 动态生成唯一的属性名


Function.prototype.myCall = function (thisArg, ...args) {
const key = Symbol()
thisArg[key] = this
const res = thisArg[key](...args)
delete thisArg[key]
return res
}

手写apply


apply 方法同理 call 只是第二个参数需要改为数组


Function.prototype.myApply = function (thisArg, args) {
console.log(args);
const key = Symbol()
thisArg[key] = this
const res = thisArg[key](args)
delete thisArg[key]
return res
}
const obj = {
name: 'zs',
age: 20
}
function myFn(args) {
const div = `大家好,我的名字叫${this.name} 我今年${this.age}岁了,${args.toString()}`
return div
}
const res = myFn.myApply(obj, [1, 2, 3, 4, 5])
console.log(res);

手写bind


Function.prototype.myBind = function (thisArg, ...args) {
const fn = this
return function (...args1) {
const allArgs = [...args, ...args1]
// 判断是否为new的构造函数
if (new.target) {
return new fn(...allArgs)

} else {
return fn.call(thisArg, allArgs)
}
}
}
const obj = {
name: 'zs',
age: 20
}
function myFn(...arg) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了,${arg}`);
const div = `大家好,我的名字叫${this.name} 我今年${this.age}岁了,${arg}`
return div
}
const res = myFn.myBind(obj, '1')
console.log(res('122'));

作者:不知名小瑜
来源:juejin.cn/post/7313135267572121612
收起阅读 »

你真的需要Pinia🍍吗?

web
尤大大:理论上来说,每一个 Vue 组件实例都已经在“管理”它自己的响应式状态了。 🤦‍♂️:不会吧🤡!既然Vue本身具备状态管理的能力,我们还有必要引入Pinia🍍或者Vuex等状态管理工具吗? Vue实例作为状态管理器应该怎么实现?按照vue官网我们来实践...
继续阅读 »

尤大大:理论上来说,每一个 Vue 组件实例都已经在“管理”它自己的响应式状态了


🤦‍♂️:不会吧🤡!既然Vue本身具备状态管理的能力,我们还有必要引入Pinia🍍或者Vuex等状态管理工具吗?


Vue实例作为状态管理器应该怎么实现?按照vue官网我们来实践一次。


简单状态管理 😎


状态管理器


我们以Vue3为例,实现一个状态管理。首先创建一个名为auth.ts的ts文件,这文件将用来定义状态管理器。


import { reactive, readonly } from 'vue';

export interface Account {
name: string;
}

export interface AuthStore {
account: Account | null;
isAuthed: boolean;
}

const auth = reactive<AuthStore>({
isAuthed: false,
account: null,
});

export const useAuthStore = () => {
return {
state: readonly(auth),
actions: {
login(account: Account) {
auth.isAuthed = true;
auth.account = account;
},
logout() {
auth.isAuthed = false;
auth.account = null;
},
},
};
};

export default useAuthStore;

接下来创建两个组件Info.vueLogin.vue,在这两个组件中使用我们自定义的useAuthStore状态管理器。


Login.vue


使用import { useAuthStore } from '../auth';来引入这个store,通过useAuthStore()获取store实例。


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

import { useAuthStore } from '../auth';

const username = ref('');
const { state, actions } = useAuthStore();
</script>

<template>
<div>
<div v-if="!state.isAuthed">
<div>
<span>用户名:</span>
<input v-model="username" />
</div>
<button @click="actions.login({ name: username })">登录</button>
</div>
<button v-if="state.isAuthed" @click="actions.logout">退出</button>
</div>
</template>

Info.vue


<script setup lang="ts">
import { useAuthStore } from '../auth';

const { state } = useAuthStore();
</script>

<template>
<div>
<div v-if="!state.isAuthed">
<h1>请登录</h1>
</div>
<div v-if="state.isAuthed">
<h1>欢迎:{{ state.account?.name }}</h1>
</div>
</div>
</template>

使用效果


Kapture 2023-06-25 at 21.35.52.gif


解读


使用reactive()是因为State是一个对象,当然也可以使用ref()。但是,就必须使用.value来访问数据,这并不是想要的效果。


为了实现单向数据流useAuthStore中的State采用Vue3的readonly API将状态对象置为只读的对象,这样避免了在使用该状态对象时直接操作State的情况。因此想要修改State就只能通过Actions,就像下图这样:


image.png


Vue2也可以么?😲


虽然 Vue2 中没有reactive()ref()API,但是事实是 Vue2 也实现简单的状态管理。利用 Vue2 中的Vue.observable()可以将一个普通对象转换为响应式对象,从而实现当State变更时驱动View更新。


🤔需要注意的是 Vue2 中没有 readonly() API,因此在这个例子中,我们直接使用 auth 作为状态。要确保状态不被意外修改,你需要确保只在 actions 对象中的方法内修改状态。


import Vue from 'vue';

export interface Account {
name: string;
}

export interface AuthStore {
account: Account | null;
isAuthed: boolean;
}

const auth = Vue.observable<AuthStore>({
isAuthed: false,
account: null,
});

export const useAuthStore = () => {
return {
state: auth,
actions: {
login(account: Account) {
auth.isAuthed = true;
auth.account = account;
},
logout() {
auth.isAuthed = false;
auth.account = null;
},
},
};
};

export default useAuthStore;

Login.vue


在vue2中将useAuthStore()解构进组件的data中即可。


<template>
...
</template>

<script>
import { useAuthStore } from '../auth';

export default {
data() {
const { state, actions } = useAuthStore();
return {
authState: state,
login: actions.login,
logout: actions.logout,
};
},
};
</script>

首先从 useAuthStore 文件中导入 useAuthStore 函数。然后,在组件的 data 选项中,我们调用 useAuthStore() 并将返回的 state 和 actions 解构。接下来,我们将 statelogin 和 logout 添加到组件的响应式数据中,以便在模板中使用。最后,在模板中,我们根据 authState.isAuthed 的值显示不同的内容,并使用 login 和 logout 方法处理按钮点击事件。


关于服务器端渲染 🧐


在 SSR 环境下,应用模块通常只在服务器启动时初始化一次。同一个应用模块会在多个服务器请求之间被复用,而我们的单例状态对象也一样。如果我们用单个用户特定的数据对共享的单例状态进行修改,那么这个状态可能会意外地泄露给另一个用户的请求。我们把这种情况称为跨请求状态污染


如果使用 SSR,则需要避免所有请求共享同一存储。在这种情况下,需要为每个请求创建一个单独的存储并提供/注入它。


// app.js (在服务端和客户端间共享)
import { createSSRApp } from 'vue'
import { createStore } from './store.js'

// 每次请求时调用
export function createApp() {
const app = createSSRApp(/* ... */)
// 对每个请求都创建新的 store 实例
const store = createStore(/* ... */)
// 提供应用级别的 store
app.provide('store', store)
// 也为激活过程暴露出 store
return { app, store }
}

优势与不足


优势



  • 简单易学:对于初学者和小型项目来说,这种方法更容易理解和实现。它不需要引入额外的库或学习新的概念。

  • 轻量级:由于不需要引入额外的库,这种方法在体积上更轻量级,对于那些对性能有严格要求的项目来说,这可能是一个优势。

  • 灵活性:这种方法允许开发人员根据项目需求自由地调整状态管理结构。这种灵活性可能适用于一些具有特殊需求的项目。


不足



  • 缺乏结构和约束:这种方法没有强制执行任何特定的结构或约束,这可能导致不一致的代码和难以维护的项目。当多个开发人员协同工作时,这可能会导致问题。

  • 缺乏调试工具:与像 Vuex 或 Pinia 这样的专门的状态管理库相比,这种方法没有提供调试工具,这可能会使调试和追踪状态变更更加困难。

  • 可扩展性:对于大型应用程序,这种简单的状态管理可能不够强大,因为它可能无法很好地处理复杂的状态逻辑和多个状态模块。

  • 性能优化:这种方法可能无法提供像 Vuex 或 Pinia 这样的库所提供的性能优化,例如,缓存计算属性。


🚀简单状态管理 vs Pinia🚀


开发 Vue 应用时,状态管理是一个重要的考虑因素。Vue 自身提供了一些状态管理工具,如 ref 和 reactive,但在某些情况下,引入专门的状态管理库(如 Pinia 或 Vuex)可能会带来更多的便利和优势。那么,在什么情况下你真的需要 Pinia?让我们来总结一下。


使用 Vue 自身的状态管理


在以下场景下,使用 Vue 自身的状态管理就可以完美解决问题:



  1. 当应用的规模较小,组件层级较浅时,Vue 自身的状态管理可以很好地处理状态。

  2. 当组件之间的状态共享较少,且状态变化较简单时,Vue 的响应式系统足以应对这些需求。

  3. 当应用的状态变化逻辑较为简单,易于维护时,Vue 的状态管理可以很好地解决问题。


在这些场景下,使用 Vue 自身的状态管理,如 ref 和 reactive,可以满足应用的需求,而无需引入额外的状态管理库。


这样的小型项目存在吗?


小型项目通常具有以下特点:



  1. 功能有限:项目的功能和需求相对较少,不需要复杂的状态管理。

  2. 规模较小:项目的代码量和组件数量较少,易于维护。

  3. 开发周期短:项目的开发和发布周期相对较短。

  4. 团队规模较小:负责项目的开发人员数量较少。


这些小型项目可能包括个人博客、简历网站、小型企业网站、原型和概念验证等。


何时考虑使用 Pinia


选择是否一开始就使用 Pinia 取决于项目的需求和预期的复杂性。以下是一些建议:



  1. 如果您预计项目将迅速增长并变得复杂,那么从一开始就使用 Pinia 可能是一个明智的选择。这样,您可以从一开始就利用 Pinia 提供的强大功能、更好的开发体验和更强的约定。

  2. 如果项目是一个小型项目,且预计不会变得很复杂,那么可以从简单的状态管理方法开始。这样,您可以减少项目的依赖和包大小,同时保持灵活性。然后,根据项目的发展情况,您可以在需要时迁移到 Pinia。

  3. 如果您的团队已经熟悉 Pinia 或类似的状态管理库,那么从一开始就使用 Pinia 可能会使团队更加高效。


总之,在决定是否从一开始就使用 Pinia 时,您应该权衡项目的需求、预期的复杂性和团队的经验。如果您认为 Pinia 可以为您的项目带来长期的好处,那么从一开始就使用它是合理的。


最后


没有最好的架构,只有最合适的选择。对于小型项目和初学者,简单的状态管理方法可能是一个合适的选择。然而,在大型、复杂的应用程序中,使用像 Pinia 这样的专门的状态管理库可能更加合适,因为它们提供了更强大的功能、更好的开发体验和更强的约定。


关于项目是否应该使用第三方的状态管理库,完全取决于项目自身和开发团队的选择!


如果您有不同的看法,以自身看法为准


作者:youth君
来源:juejin.cn/post/7248606372954456120
收起阅读 »

第一次使用canvas,实现环状类地铁时刻图

web
前情提要 今天,产品找到我,说能不能实现这个图呢 众所周知,产品说啥就是啥,于是就直接开干。 小波折 为了实现我的需求做了下调研,看了d3还有x6之类的库,感觉都太重了,也不一定能实现我的需求。于是在沸点上寻求了下建议 掘友们建议canvas直接画 于是...
继续阅读 »

前情提要


今天,产品找到我,说能不能实现这个图呢


image.png


众所周知,产品说啥就是啥,于是就直接开干。


小波折


为了实现我的需求做了下调研,看了d3还有x6之类的库,感觉都太重了,也不一定能实现我的需求。于是在沸点上寻求了下建议



掘友们建议canvas直接画



于是决定手撸


结果


之前没有使用canvas画过东西,于是花了一天边看文档,边画,最终画完了,效果如下:


image.png


代码及思路


首先构造数据集在画布上的节点位置


 let stations = new Array(13).fill(null);

/** 拐角的节点 */
const cornerP = [
{ x: 20, y: 67.5, type: 'corner', showP: true },
{ x: 55, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 112.5, type: 'corner', showP: false },
{ x: 55, y: 112.5, type: 'corner', showP: false },
{ x: 20, y: 92.5, type: 'corner', showP: true },
];
/** 生成站点笔触位置 */
function getStationsPosition(): {
num: string;
status: number;
x: number;
y: number;
type?: string;
}[] {
const middleIndex = Math.floor(stations.length / 2);
const { width, height } = canvasRef.current as HTMLCanvasElement;
let centerPoint = { x: width - 20, y: height / 2 + 20 };
let leftArr = stations.filter((v, _i) => _i < middleIndex);
const leftWidth = (width - 40 - 35 - 32.5) / (leftArr.length - 1);
const leftP = leftArr.map((v, i) => ({
x: leftWidth * i + 55,
y: height / 2 + 20 - 32.5,
}));

const rightArr = stations.filter((v, _i) => _i > middleIndex);
const rightWidth = (width - 40 - 35 - 32.5) / (rightArr.length - 1);
const rightP = rightArr.map((v, i) => ({
x: 370 - 32.5 - rightWidth * i,
y: height / 2 + 20 + 32.5,
}));

return [
cornerP[0],
cornerP[1],
...leftP,
cornerP[2],
centerPoint,
cornerP[3],
...rightP,
cornerP[4],
cornerP[5],
].map((v, i) => ({
...v,
num: String(2),
status: i > 3 ? 0 : i > 2 ? 1 : 2,
}));
}

为了避免实际使用过程中,数据点位不够,上面的点位生成主动加入了拐角的点位。


然后画出背景路径


   function drawBgLine(
points: ReturnType<typeof getStationsPosition>,
color?: string,
) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

points.forEach((item, index) => {
const next = points[index + 1];
if (next) {
if (next.y === item.y) {
ctx.beginPath();
ctx.moveTo(item.x, item.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, )';
ctx.lineTo(next.x, next.y);
ctx.stroke();
} else if (Math.abs(next.y - item.y) === 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.arc(
next.x,
item.y,
32.5,
(Math.PI / 180) * 0,
(Math.PI / 180) * 90,
);
} else {
ctx.arc(
item.x,
next.y,
32.5,
(Math.PI / 180) * 270,
(Math.PI / 180) * 0,
);
}
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
} else if (Math.abs(next.y - item.y) < 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(next.x, item.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(next.x, next.y, 4, 0, Math.PI * 2);

ctx.fill();
} else {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(item.x, next.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
}
}
}
});
}

此处主要的思路是根据相领点位的高低差,来画不同的路径


然后画进度图层


  function drawProgressBgLine(points: ReturnType<typeof getStationsPosition>) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

const index = points.findIndex((v) => v.status === 0);

const newArr = points.slice(0, index);
const lastEl = points[index];
const curEl = points[index - 1];
console.log(lastEl, curEl);

if (lastEl) {
/**处于顶部的时候画出箭头 */
if (lastEl.y === curEl.y) {
if (lastEl.x > curEl.x) {
const centerP = {
x: (lastEl.x - curEl.x) / 2 + curEl.x,
y: curEl.y,
};
const img = new Image();
img.src = carIcon;
img.onload = function () {
ctx.drawImage(img, centerP.x - 12, centerP.y - 32, 19, 24);
};

ctx.beginPath();
ctx.moveTo(curEl.x, curEl.y);
ctx.lineTo(centerP.x, centerP.y);
/**生成三角形标记 */
ctx.lineTo(centerP.x, centerP.y - 2);
ctx.lineTo(centerP.x + 3, centerP.y);
ctx.lineTo(centerP.x, centerP.y + 2);
ctx.lineTo(centerP.x, centerP.y);
ctx.fillStyle = 'rgba(107, 255, 236, 1)';
ctx.fill();

ctx.lineWidth = 4;
ctx.strokeStyle = 'rgba(107, 255, 236, 1)';
ctx.stroke();
}
/** 其他条件暂时留空 */
}
}

/** 生成带进度颜色背景 */
drawBgLine(newArr, 'rgba(107, 255, 236, 1)');
}

主要是已经走过的路径线路变蓝,未走过的,获取两点中间位置,添加图标,箭头。这里箭头判断我未补全,等待实际使用补全


最后画出节点就可以了


  function draw() {
if (!canvasRef.current) {
return;
}
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

ctx.clearRect(0, 0, canvasRef.current?.width, canvasRef.current?.height);
if (ctx) {
/** 绘制当前遍数的文字 */
ctx.font = '12px serif';

ctx.fillStyle = '#fff';
ctx.fillText(text, 10, canvasRef.current?.height / 2 + 24);

const points = getStationsPosition();
/** 画出背景线 */
drawBgLine(points);

/** 画出当前进度 */
drawProgressBgLine(points);

points.forEach((item) => {
if (item.type !== 'corner') {
ctx.clearRect(item.x - 6, item.y - 6, 12, 12);
ctx.beginPath();
/** 生成标记点 */
ctx.moveTo(item.x, item.y);

ctx.fillStyle =
item.status === 2
? 'rgba(255, 157, 31, 1)'
: item.status === 1
? 'rgba(107, 255, 236, 1)'
: 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 6, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();

ctx.fillStyle = '#fff';
ctx.fillText(item.num, item.x - 4, item.y - 12);
}
});
}
}

最后贴一下全部代码


import carIcon from '@/assets/images/map/map_car1.png';
import { useEffect, useRef } from 'react';
const LineCanvas = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
let text = '第3遍(15:00-18:00)';

let stations = new Array(13).fill(null);

/** 拐角的节点 */
const cornerP = [
{ x: 20, y: 67.5, type: 'corner', showP: true },
{ x: 55, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 112.5, type: 'corner', showP: false },
{ x: 55, y: 112.5, type: 'corner', showP: false },
{ x: 20, y: 92.5, type: 'corner', showP: true },
];
/** 生成站点笔触位置 */
function getStationsPosition(): {
num: string;
status: number;
x: number;
y: number;
type?: string;
}[] {
const middleIndex = Math.floor(stations.length / 2);
const { width, height } = canvasRef.current as HTMLCanvasElement;
let centerPoint = { x: width - 20, y: height / 2 + 20 };
let leftArr = stations.filter((v, _i) => _i < middleIndex);
const leftWidth = (width - 40 - 35 - 32.5) / (leftArr.length - 1);
const leftP = leftArr.map((v, i) => ({
x: leftWidth * i + 55,
y: height / 2 + 20 - 32.5,
}));

const rightArr = stations.filter((v, _i) => _i > middleIndex);
const rightWidth = (width - 40 - 35 - 32.5) / (rightArr.length - 1);
const rightP = rightArr.map((v, i) => ({
x: 370 - 32.5 - rightWidth * i,
y: height / 2 + 20 + 32.5,
}));

return [
cornerP[0],
cornerP[1],
...leftP,
cornerP[2],
centerPoint,
cornerP[3],
...rightP,
cornerP[4],
cornerP[5],
].map((v, i) => ({
...v,
num: String(2),
status: i > 3 ? 0 : i > 2 ? 1 : 2,
}));
}

function drawBgLine(
points: ReturnType<typeof getStationsPosition>,
color?: string,
) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

points.forEach((item, index) => {
const next = points[index + 1];
if (next) {
if (next.y === item.y) {
ctx.beginPath();
ctx.moveTo(item.x, item.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.lineTo(next.x, next.y);
ctx.stroke();
} else if (Math.abs(next.y - item.y) === 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.arc(
next.x,
item.y,
32.5,
(Math.PI / 180) * 0,
(Math.PI / 180) * 90,
);
} else {
ctx.arc(
item.x,
next.y,
32.5,
(Math.PI / 180) * 270,
(Math.PI / 180) * 0,
);
}
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
} else if (Math.abs(next.y - item.y) < 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(next.x, item.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(next.x, next.y, 4, 0, Math.PI * 2);

ctx.fill();
} else {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(item.x, next.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
}
}
}
});
}

function drawProgressBgLine(points: ReturnType<typeof getStationsPosition>) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

const index = points.findIndex((v) => v.status === 0);

const newArr = points.slice(0, index);
const lastEl = points[index];
const curEl = points[index - 1];
console.log(lastEl, curEl);

if (lastEl) {
/**处于顶部的时候画出箭头 */
if (lastEl.y === curEl.y) {
if (lastEl.x > curEl.x) {
const centerP = {
x: (lastEl.x - curEl.x) / 2 + curEl.x,
y: curEl.y,
};
const img = new Image();
img.src = carIcon;
img.onload = function () {
ctx.drawImage(img, centerP.x - 12, centerP.y - 32, 19, 24);
};

ctx.beginPath();
ctx.moveTo(curEl.x, curEl.y);
ctx.lineTo(centerP.x, centerP.y);
/**生成三角形标记 */
ctx.lineTo(centerP.x, centerP.y - 2);
ctx.lineTo(centerP.x + 3, centerP.y);
ctx.lineTo(centerP.x, centerP.y + 2);
ctx.lineTo(centerP.x, centerP.y);
ctx.fillStyle = 'rgba(107, 255, 236, 1)';
ctx.fill();

ctx.lineWidth = 4;
ctx.strokeStyle = 'rgba(107, 255, 236, 1)';
ctx.stroke();
}
/** 其他条件暂时留空 */
}
}

/** 生成带进度颜色背景 */
drawBgLine(newArr, 'rgba(107, 255, 236, 1)');
}
function draw() {
if (!canvasRef.current) {
return;
}
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

ctx.clearRect(0, 0, canvasRef.current?.width, canvasRef.current?.height);
if (ctx) {
/** 绘制当前遍数的文字 */
ctx.font = '12px serif';

ctx.fillStyle = '#fff';
ctx.fillText(text, 10, canvasRef.current?.height / 2 + 24);

const points = getStationsPosition();
/** 画出背景线 */
drawBgLine(points);

/** 画出当前进度 */
drawProgressBgLine(points);

points.forEach((item) => {
if (item.type !== 'corner') {
ctx.clearRect(item.x - 6, item.y - 6, 12, 12);
ctx.beginPath();
/** 生成标记点 */
ctx.moveTo(item.x, item.y);

ctx.fillStyle =
item.status === 2
? 'rgba(255, 157, 31, 1)'
: item.status === 1
? 'rgba(107, 255, 236, 1)'
: 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 6, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();

ctx.fillStyle = '#fff';
ctx.fillText(item.num, item.x - 4, item.y - 12);
}
});
}
}

useEffect(() => {
draw();
}, []);

return <canvas ref={canvasRef} width="390" height="120"></canvas>;
};

export default LineCanvas;


转载请注明出处!


作者:MshengYang_lazy
来源:juejin.cn/post/7312723512724439094
收起阅读 »

Echarts高级配色

web
Echarts高级配色 Echarts是一款功能强大的JavaScript图表库,能够为用户提供丰富多样的数据可视化效果。其中,配色是图表呈现中非常重要的一部分,合适的配色方案能够使图表更加美观、易于辨识,并提升数据可视化的表达力。Echarts提供了多种高级...
继续阅读 »

Echarts高级配色


Echarts是一款功能强大的JavaScript图表库,能够为用户提供丰富多样的数据可视化效果。其中,配色是图表呈现中非常重要的一部分,合适的配色方案能够使图表更加美观、易于辨识,并提升数据可视化的表达力。Echarts提供了多种高级配色设置的方式,让用户可以根据自己的需求,轻松地定制图表的配色方案。


Echarts配色配置概述


Echarts提供了两种方式来配置配色方案:使用预定义的配色方案和自定义配色方案。预定义的配色方案包括一系列经过精心设计的颜色配置,而自定义配色方案则允许用户根据自己的需求,自由地调整配色方案。


以下将对Echarts的高级配色进行详细介绍,并提供相应的代码示例。


使用预定义的配色方案


Echarts提供了一些预定义的配色方案,以供用户选择使用。这些预定义的配色方案是经过深思熟虑和优化的,能够使图表在不同场景下保持一致和美观。


以下是一些常见的预定义配色方案及其名称:



  • colorBlind:适用于色盲人群的配色方案,通过优化颜色对比度,使得色盲人群更容易分辨。

  • light:明亮配色方案,适用于明亮的背景或需要突出显示的图表。

  • dark:低亮度配色方案,适用于暗色背景或需要弱化图表的亮度。


使用预定义的配色方案非常简单,只需在图表的配置项中设置配色方案的名称即可。


option = {
// 其他配置项...
color: 'light', // 使用预定义的明亮配色方案
};

在上面的示例代码中,通过设置配色方案的名称为light,来应用明亮的预定义配色方案。


自定义配色方案


Echarts也支持用户根据自己的需求,定制个性化的配色方案。自定义配色方案使用户可以根据自己的品牌风格、场景需求等,灵活地设置图表的颜色。


以下是一个自定义配色方案的示例:


option = {
// 其他配置项...
color: ['#FF0000', '#00FF00', '#0000FF'], // 使用自定义配色方案
};

在上述示例中,通过设置color字段为一个颜色数组,来使用自定义的配色方案。在这个例子中,我们使用红色、绿色和蓝色来自定义配色方案。


配色方案详解


Echarts提供了丰富的配置选项,用户可以通过调整配置项来实现个性化的配色方案。以下是一些常用的配色方案的配置项:



  • color:图表的系列颜色配置,可以设置为预定义的配色方案名称或自定义的颜色数组。

  • backgroundColor:图表背景色配置,可以设置为颜色值或渐变色。

  • textStyle:图表中文字的样式配置,包括字体、字号和颜色等。

  • axisLineaxisLabelaxisTick:坐标轴线、刻度线、刻度标签的样式配置。


通过修改这些配置项的值,我们可以轻松地调整图表的配色方案。


完整示例


下面是一个使用Echarts配色功能的示例代码,包括预定义配色方案和自定义配色方案的应用。


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Echarts高级配色示例</title>
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
</head>
<body>
<div id="chart" style="width: 600px; height: 400px;"></div>
<script>
// 初始化Echarts实例
var myChart = echarts.init(document.getElementById('chart'));

// 配置项
var option = {
title: {
text: 'Echarts高级配色示例'
},
// 其他配置项...
textStyle: {
fontFamily: 'Arial, sans-serif',
fontSize: 12,
fontWeight: 'normal'
},
tooltip: {
// 配置提示框样式
},
xAxis: {
// 配置X轴样式
},
yAxis: {
// 配置Y轴样式
},
series: [{
type: 'bar',
// 配置系列样式
}],
// 使用预定义配色方案
color: 'colorBlind',
};

// 使用配置项显示图表
myChart.setOption(option);
</script>
</body>
</html>

在上述示例中,我们首先引入了Echarts库,并创建一个容器元素来显示图表。然后,我们初始化Echarts实例,并设置图表的配置项,包括标题、文字样式、提示框样式、坐标轴样式和系列样式等。最后,调用setOption方法将配置项应用于图表。


通过配色方案的选择和自定义,我们可以灵活定制图表的配色方案,使图表更加美观和易于辨识。


总结


Echarts的高级配色功能使用户可以根据自己的需求,定制图表的颜色配色方案。预定义配色方案提供了一系列经过优化的配色方案,能够满足常见的图表需求,而自定义配色方案则允许用户根据自己的品牌风格和场景需求,灵活地设置图表的颜色。


通过使用配色功能,我们可以轻松定制个性化的图表样式,使数据可视化更加美观和易于理解。在实际应用中,根据需要选择合适的预定义配色方案,或者自定义配色方案,都能为数据可视化带来不同的风格和效果。


通过本文的全面介绍和示例代码的演示,相信您已经掌握了Echarts的高级配色功能,并可以灵活应用于实际的数据可视化项目中。继续探索和研究Echarts的配色功能,将为您的数据可视化项目增添更多的创意和魅力!


作者:程序员也要学好英语
来源:juejin.cn/post/7313027887885123599
收起阅读 »

让你的PDF合成后不再失真

web
前言 现在的前端要处理越来越多的业务需求,如果你的业务中有涉及PDF的处理,你肯定会遇到这么一种情况, 在一个原始的pdf文件上合成进一张图片,或者一段文字。 之前的解决方案基本上是把图片扔给后端,让后端处理,处理好之后,前端再调用接口拿到。 如果非要前端来做...
继续阅读 »

前言


现在的前端要处理越来越多的业务需求,如果你的业务中有涉及PDF的处理,你肯定会遇到这么一种情况,
在一个原始的pdf文件上合成进一张图片,或者一段文字。


之前的解决方案基本上是把图片扔给后端,让后端处理,处理好之后,前端再调用接口拿到。


如果非要前端来做,也不是不可以。


一脸无奈的小


canvas


网上搜了一圈,主流的方案是,用canvas画布将pdf画出来,再将图片合成进canvans
这里也提供一下这个方案的代码


const renderPDF = async (pdfData, index, pages, newPdf, images = []) => {
await pdfData.getPage(index).then(async (pdfPage) => {
const viewport = pdfPage.getViewport({ scale: 3, rotation: 0 })
const canvas = document.createElement("canvas")
const context = canvas.getContext("2d")
canvas.width = 600 * 3
canvas.height = 800 * 3
// PDF渲染到canvas
const renderTask = pdfPage.render({
canvasContext: context,
viewport: viewport,
})

await renderTask.promise.then(() => {
if (index > 1) {
newPdf.addPage()
}
newPdf.addImage(canvas, "JPEG", 0, 0, 600, 800, undefined, "FAST")
images.forEach((item) => {
let width = item.width
let height = item.height
if (index == pages) {
item.src !== "" &&
newPdf.addImage(
item.src,
"PNG",
item.x,
item.y,
width,
height,
undefined,
"FAST"
)
}
})
})
})
}

但是!


这样会有一个很严重的问题,那就是pdf失真,显得很模糊,当然也有解决方案,那就是canvas的缩放比例增加,


image.png
但是,缩放比例的增加却带来了pdf文件大小的倍数及增加,前端渲染的压力很大,只有7张的pdf,已经渲染出了8M大小常常见到loading等待。


所以有没有更好的方法解决呢?


暴漫g


有的。


pdf-lib


那就是今天所推荐的库 pdf-lib
github地址
他在github上的star 有5.6k,算的上是成熟,顶级的开源项目


image.png


在任何JavaScript环境中创建和修改PDF文档。


好,今天就只介绍如何将图片合成进pdf的功能 ,抛砖引玉。


熊猫头抛砖头 .gif


其余的功能由您自己探索。


合成的思路是这样的:



  • 1、我们的原始pdf,转换成pdf-lib 可识别的格式

  • 2、同时将我们的图片合成进 pdf-lib里

  • 3、pdf-lib 导出合成后的pdf



由于他只是一个工具,没有办法展示pdf
最后找一个pdf预览工具显示在页面即可
我找的是 vue-pdf-embed



这样,使用pdf-lib 方案,就不再是canvas画布画出来的。
我们可以看到,生成后的pdf文件体积增加不大,


image.png


而且能够保留原始pdf的文字选择,不再是图片了


image.png


同样,页面的缩放不会出现模糊失真的情况(因为不是图片,还是保持文字的矢量)。


代码


以下是代码,请查收


感谢 给你磕头 GIF .gif


import { PDFDocument } from "pdf-lib"

const getNewPdf = async (pdfBase64, imagesList = []) => {
// 创建新的pdf
const pdfDoc = await PDFDocument.create()

let page = ""
// 传入的pdf进行格式转换
const usConstitutionPdf = await PDFDocument.load(pdfBase64)
// 获取转换后的每一页数据
const userPdf = usConstitutionPdf.getPages()
// 将每一个数据 导入到我们新建的pdf每一页上
for (let index = 0; index < userPdf.length; index++) {
page = pdfDoc.addPage()
const element = userPdf[index]
const firstPageOfConstitution = await pdfDoc.embedPage(element)
page.drawPage(firstPageOfConstitution)
// 如果有传入图片,则遍历信息,并将他合成到对应的页码上
const imageSel = imagesList.filter((i) => i.pageIndex === index)
if (imageSel.length > 0) {
for (let idx = 0; idx < imageSel.length; idx++) {
const el = imageSel[idx]
const pngImage = await pdfDoc.embedPng(el.src)
page.drawImage(pngImage, {
x: +el.x,
y: +el.y,
width: +el.width,
height: +el.height,
})
}
}
}
// 保存pdf
const pdfBytes = await pdfDoc.save()
// 将arrayButter 转换成 base64 格式
function ArrayBufferToBase64(buffer) {
//第一步,将ArrayBuffer转为二进制字符串
var binary = ""
var bytes = new Uint8Array(buffer)
for (var len = bytes.byteLength, i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i])
}
//将二进制字符串转为base64字符串
return window.btoa(binary)
}

// console.log("data:application/pdf;base64," + ArrayBufferToBase64(pdfBytes))
// 最后将合成的pdf返回
return "data:application/pdf;base64," + ArrayBufferToBase64(pdfBytes)
}
export default getNewPdf


这里的传参要注意,


可爱小男生拿喇叭注意啦_爱给网_aigei_com.gif



  • pdfBase64 是base64位的格式

  • imagesList 数组对象格式为
    [
    {
    src:'base64',
    x:'',
    yL'',
    width:'',
    height:'',
    pageIndex:''
    }
    ]



最后也附上vue文件中如何使用的代码


<template>
<div>
<el-button @click="pdfComposite">生成新的pdf</el-button>
<div class="pdf-content">
<vue-pdf-embed
:source="url"
/>
</div>
</div>

</template>

<script>
import VuePdfEmbed from "vue-pdf-embed/dist/vue2-pdf-embed"
import getNewPdf from "./utils"
import { pngText, pdfbase64 } from "../data"
export default {
name: "PdfPreview",
components: {
VuePdfEmbed,
},

data() {
return {
url: pdfbase64,// 原始的base64位 pdf
}
},
methods: {
pdfComposite() {
// getNewPdf 返回的是promise 对象
getNewPdf(this.url, pngText).then(res =>{
this.url = res
})
},
},
}
</script>

<style >
.pdf-content {
width: 400px;
min-height: 600px;
}
</style>


作者:前端代码王
来源:juejin.cn/post/7293175592163049506
收起阅读 »

阿里妈妈刀隶体使用

web
最近在做的一个前端小项目,需要一种比较炫酷的字体,在网上找到了iconfont中的刀隶体,感觉还不错,本文记录了如何在前端项目中使用这种字体的步骤。 1. 找到刀隶体生成的网站 访问下面这个网站就可以了: 阿里妈妈刀隶体字体 http://www.iconfo...
继续阅读 »

最近在做的一个前端小项目,需要一种比较炫酷的字体,在网上找到了iconfont中的刀隶体,感觉还不错,本文记录了如何在前端项目中使用这种字体的步骤。


1. 找到刀隶体生成的网站


访问下面这个网站就可以了:


阿里妈妈刀隶体字体


http://www.iconfont.cn/fonts/detai…


2. 生成自己想要的字并下载到本地


找到文本输入框
image.png


然后输入自己想要展示的字体:我是一只小青蛙,最爱说笑话
image.png


最后点击下载子集按钮


image.png


下载好的压缩包:


image.png


将压缩包中的内容复制到剪切板:


image.png


3. 项目中引入


在项目中创建管理字体的目录


mkdir -p src/assets/font

然后到font目录下粘贴复制的字体文件夹


最后在项目的根样式文件中(一般来说是src/index.css)引入新字体:


@font-face {
font-family: "DaoLiTi";
font-weight: 400;
src: url("./assets/font/jHsMvOZ7UDEO/XBddIp4y0BmM.woff2") format("woff2"),
url("./assets/font/jHsMvOZ7UDEO/XBddIp4y0BmM.woff") format("woff");
font-display: swap;
}

body {
font-family: 'DaoLiTi', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
}

4. 新字体使用及效果


<span style={{fontFamily: 'DaoLiTi'}}>我是一只小青蛙,最爱说笑话</span>


5. 注意点


DaoLiTi这个名字是可以自定义的,但是在样式文件中的无论什么地方使用的时候都不能少了引号。


此外就是除了“我是一只小青蛙,最爱说笑话”,其他字是没有刀隶体效果的!


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

你真的懂 Base64 吗?短链服务常用的 Base62 呢?

web
黄山的冬天,中国 (© Hung Chung Chih/Shutterstock) Base64 前端的日常开发中可能会接触到 Base64 ,比如页面上的小图片,在为了节省网络资源的情况下,通常会将图片转为 Base64 直接嵌入到 html 或者 css ...
继续阅读 »

黄山的冬天,中国 (© Hung Chung Chih/Shutterstock)

Base64


前端的日常开发中可能会接触到 Base64 ,比如页面上的小图片,在为了节省网络资源的情况下,通常会将图片转为 Base64 直接嵌入到 html 或者 css 里。这里,我们先来看看 Base64 是什么,以及 Base64 编码做了什么。


什么是 Base64


Base64 是一种基于64个可打印字符来表示二进制数据的表示方法。一般来说,64个字符包括 A-Z,a-z,0-9 以及 +/ 两个字符(via)。换句话说, Base64 可以将二进制数据转换为这 64 个字符来表示,数据来源可以是图片,也可以是任意的字符串。


Base64 做了些什么


上面提到了 Base64 做的其实就是将二进制数据按照对应规则进行转化,转化的流程其实也很简单



  1. 得到一份二进制数据

  2. 将二进制数据 6位 一组进行划分,并进行适当的补位

  3. 按照 Base64 索引表,将每组数据转换为 Base64 索引表对应的字符,补位位置用 =


下面我们来尝试一下将 aa 这个字符串进行一下 Base64 编码:


第一步:通过 ASCII 表将 aa 转换为对应的二进制表示

可知 a 在 ASCII 表对应的二进制表示为 0110 0001,则 aa 对应为 0110 0001 0110 0001

注:



  1. ascii 参考

  2. 中文等其他字符参考其他表(UTF-8)进行转换即可


第二步:数据分组和补位

按 6 位一组划分后 011000 010110 0001,我们发现数据还少两位,所以我们需要按规则对数据进行补位,补一字节(8位),二进制数据变为 0110 0001 0110 0001 0000 0000,划分为 011000 010110 000100 000000


第三步:查 Base64 索引表 进行转换

参考一张常用的索引表
image.png
可知 aa = 011000 010110 000100 000000 对应为 011000(Y) 010110(W) 000100(E) 000000(补位=) 即 YWE=,我们就得到了 aa 的 Base64 编码为 YWE= ,是不是挺简单。


Base62


说完了 Base64,我们再来聊聊 Base62。在通过 url 传递数据的场景下,通过 Base64 进行编码的数据会带来问题(Base64 中的 / 等可能会带来路径的解析异常),所以在 Base62 里,去掉了 +/= 字符。
说到这里,大家可能觉得就讲完了,Base62 就是丢掉了几个不安全的字符而已,其余转换方法和 Base64 一样,我起初也是这么认为的。


不一样的 Base62 结果


当我尝试对 aa 进行 Base62 编码时,按推算好像也不太对? = 补位已经被去掉了,怎么来做实现呢?
在我找了几个 online 转换进行测试后,发现 aa 对应的 Base62 编码为 6U5 看着跟 Base64 毫无关系对吧,实际上也是的。


揭开面纱看看


查了几份资料以及现有的仓库实现后,我发现 Base62 编码的流程是这样的:



  1. 获得一份二进制数据

  2. 二进制数据 转 10进制

  3. 10进制 转 62进制(按索引表)


我们再来试试将 aa 转 Base62:


第一步:转二进制

aa 对应 0110 0001 0110 0001


第二步:转10进制

0110000101100001 对应十进制为 24929


第三步:转62进制(参考索引表)

image.png
24929 = 6622+3062+5

按表可知,6=6 30=U 5=5,即 6U5


注:



  1. 索引表可以自行更换,并不一定是上图顺序

  2. 现有的仓库实现里,部分只实现了 10进制 转 62进制(base62/base62.js),有的实现了更完整的转换 (tuupola/base62


分析一下原因


说实话我查到的资料不多,但是根据 en.wikipedia.org/wiki/Talk:B… 猜测,文里提到 Base64 后的数据会膨胀到 133% 。Base62 还存在对数据进行压缩的改进,所以采用了这样与 Base64 差别有点大的方式来设计。


总结一下


文章简单的谈了谈 Base64 是什么,怎么实现以及 Base62 的实现,并分析了一下 Base62 设计的初衷,整体来说还是挺简单,希望对你有所帮助 :)


作者:破竹
来源:juejin.cn/post/7311596852264878115
收起阅读 »

关于解构赋值的一些意想不到的坑

web
今天群里有个人问了一个问题,问我们为什么报错,代码如下 var arr = [1,2,3,4,5,6,7,8,9,10] for(let i = arr.length; i > 0; i--) { let index = Math.floor(M...
继续阅读 »

今天群里有个人问了一个问题,问我们为什么报错,代码如下


var arr = [1,2,3,4,5,6,7,8,9,10]
for(let i = arr.length; i > 0; i--) {
let index = Math.floor(Math.random() * i)
// console.log(i, index);
[arr[i-1], arr[index]] = [arr[index], arr[i-1]]
// ReferenceError: Cannot access 'index' before initialization
}
console.log(arr)

我乍一看,这能报错?不应该啊,怎么能呢,于是我特意复制下来跑了一下,嘿,还真是


关于ReferenceError: Cannot access 'xxx' before initialization的报错,往往和暂时性死区有关,但我看了看顺序,是先定义的index啊,没有错。


抱着求知的心态,上网查了一些文章,都没有提到这种问题,于是只能去看ecma规范,但对不起,我英语太差了,我就连在哪都没找到。


后来我想起自己曾经遇到过类似的问题,只不过是在解构对象的时候遇到的


大概是这样的操作


let a = xxx

({
a: this.options.a,
b: this.options.b,
......
} = /*一个对象*/ ?? {})

当时也报了错,我就想起来了



js中是允许语句不使用;结尾的,许多小伙伴可能养成了这个习惯,虽然不写分号有时候确实很爽很轻松,也是一些企业的规范,但是等到流泪的时候可就知道惨了。



只需要将上述结构赋值的代码的前面一个语句加上分号,就可以解决这个问题


相当于把一个语句拆开了


什么?你问我怎么就成同一个语句了?


我没记错的话,js在执行的时候是会忽略换行符的吧,或者说这个换行符没那么重要,所以我们平时看到的很多库打包出来的min.js文件都是只有一行的然后通过分号分割语句。


如果把上述代码换行内容忽视掉,就变成了这个样子,只放了部分代码


    let index = Math.floor(Math.random() * i)[arr[i-1], arr[index]] = [arr[index], arr[i-1]]

这不报错谁报错啊,根据等号从右到左的运算顺序,不就是访问了暂时性死区嘛


所以加上分号,问题就引刃而解了。


想当年因为先学c++和java的缘故,总是养成写分号的习惯,在切图仔里面似乎成为了一个异类,现在知道了吧,养成写分号的好习惯啊,呜呜呜呜


最后附上修改后的代码


var arr = [1,2,3,4,5,6,7,8,9,10]
for(let i = arr.length; i > 0; i--) {
let index = Math.floor(Math.random() * i);
// console.log(i, index);
[arr[i-1], arr[index]] = [arr[index], arr[i-1]]
// ReferenceError: Cannot access 'index' before initialization
}
console.log(arr)

更好笑的是,这哥们明显是想通过console测试一下的,结果他发现console就不报错,不console就报错,这是真的折磨哈哈哈哈哈哈


养成语句加分号的好习惯!!
从你我做起!


作者:笑心
来源:juejin.cn/post/7311681326712995903
收起阅读 »

如何实现一个可视化数据转换的小工具

web
前端开发过程中,经常有两个不同的组件、或者两个不同的业务模块需要对接,但是双方提供的数据结构又不一致的场景。这个时候就需要一个转换器来实现两个模块无缝对接,通过这个转换器的处理达到数据结构一致的目的。 基本需求梳理 场景中两个相同或者不同数据结构对象,对象是...
继续阅读 »

前端开发过程中,经常有两个不同的组件、或者两个不同的业务模块需要对接,但是双方提供的数据结构又不一致的场景。这个时候就需要一个转换器来实现两个模块无缝对接,通过这个转换器的处理达到数据结构一致的目的。


基本需求梳理



  • 场景中两个相同或者不同数据结构对象,对象是任意数据结构的,对接场景下数据结构是固定的

  • 针对当前场景下转换器处理是相同的

  • 尽量图形化操作就可以换成配置以及预览效果


设计基本思路


实现思路



  • 两个不同的数据结构可以通过字段映射的方式来取值和设置值,实现数据的对接

  • 取值路径和设置值的路径规则最好一条一条保存下来,作为转换器的规则描述

  • 取值路径和设置值路径可以通过lodash的get和set实现

  • 可视化操作可以通过json树的渲染及操作来实现


设计实现思路草图


我根据自己的想法大致设计一下交互方式如下:


数据转换器 (3).png


实现步骤


json树操作


我找了一下josn树操作的组件,发现react-json-view挺不错的;可以实现值的复制、选择;但是选择是针对叶子节点的,所以这里我使用复制功能来实现,无论是叶子节点还是非叶子节点都可以复制到,(enableClipboard)复制的时候获取当前的path信息即可。另外path多了一层根路径默认是'root',如果不想要操作保存的时候去掉即可。


如下图所示,鼠标悬浮点击这个icon图标来选中key,取其路径值保存起来
image.png


// 复制的操作
const enableClipboard = (copy) => {
const { namespace } = copy;
if (namespace?.length === 1) { // 复制的根元素
setSourceKeyPath([])
} else {
const curNamespace = namespace.splice(1, namespace.length - 1);
setSourceKeyPath(curNamespace)
}
}

路径保存


路径映射保存在数组里。


转换器处理


转换器只需要根据规则数组map处理一下每条规则进行对原数据和目标数据进行取值设置值操作就可以了


// dataSource 是规则数组
// targetData 是目标数据
// sourceData 是源数据
dataSource.map((item) => {
set(targetData, item.targetKeyPath, get(sourceData, item.sourceKeyPath))
return item
});

效果预览


image.png
另外数据随机生成我使用的是@faker-js/faker


部署到网站


已经部署到我的个人网站timesky.top/data-conver…


作者:TimeSky
来源:juejin.cn/post/7313242069099954226
收起阅读 »

Antd Upload上传后还想要拖拽?行~开干!玩的就是真实

web
原创 陈夏杨 / 叫叫技术团队 基于 Antd Upload 实现拖拽(兼容低版本) 背景 哎呀!我想要的是边上传边调整位置可以拖拽的那种效果!哪种?就那种。(这句话是不是似曾相识,没错,到这里作为开发还没领悟那就要面壁思过了~哈哈。)话不多说,目前 Ant...
继续阅读 »

原创 陈夏杨 / 叫叫技术团队



基于 Antd Upload 实现拖拽(兼容低版本)


背景


哎呀!我想要的是边上传边调整位置可以拖拽的那种效果!哪种?就那种。(这句话是不是似曾相识,没错,到这里作为开发还没领悟那就要面壁思过了~哈哈。)话不多说,目前 Antd 的 Upload 组件并未支持拖拽排序功能,社区也没有发现可以借鉴的 demo,于是我们调研后采用 react-dnd 和 react-sortable-hoc 实现“就那种”效果。


技术分析


其实这个需求之前我们已经有一些基于 react-dnd 技术的沉淀,但是都是基于 html 的 dom 元素进行 ref 绑定操作,并没有搭配 Upload 组件。如下 demo


拖拽2.gif
以上是基于 react-dnd 实现的场景拖拽,直接上核心代码


const [, dragPreview] = useDrag({
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging()
}),
// item 中包含 index 属性,则在 drop 组件 hover 和 drop 是可以根据第一个参数获取到 index 值
item: { type: 'page', index }
});
const [, drop] = useDrop({
accept: 'page',
hover(item: { type: string; index: number }, monitor: any) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;

// 拖拽元素下标与鼠标悬浮元素下标一致时,不进行操作
if (dragIndex === hoverIndex) {
return;
}
// 确定屏幕上矩形范围
const hoverBoundingRect = ref.current!.getBoundingClientRect();
// 获取中点垂直坐标
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// 确定鼠标位置
const clientOffset = monitor.getClientOffset();
// 获取距顶部距离
const hoverClientY = (clientOffset as any).y - hoverBoundingRect.top;
/**
* 只在鼠标越过一半物品高度时执行移动。
* 当向下拖动时,仅当光标低于50%时才移动。
* 当向上拖动时,仅当光标在50%以上时才移动。
* 可以防止鼠标位于元素一半高度时元素抖动的状况
*/

// 向下拖动
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// 向上拖动
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}

// 执行 move 回调函数
moveCard(dragIndex, hoverIndex);

/**
* 如果拖拽的组件为 Box,则 dragIndex 为 undefined,此时不对 item 的 index 进行修改
* 如果拖拽的组件为 Card,则将 hoverIndex 赋值给 item 的 index 属性
*/

if (item.index !== undefined) {
item.index = hoverIndex;
}
}
});
dragPreview(drop(ref));

因为 Upload 组件上传文件是通过自身 fileList api 底层消化处理的,所以处理起来比较麻烦,还好 Antd 4.16.0 版本Upload 提供的 itemRender 解决了这个问题。但是对于低于这个版本的后续也有解决方案。


注意:如果 Antd 用的是最新的版本 5.x.x,其实官网也提供了 集成 dnd-kit 来实现对上传列表拖拽排序


技术选型


市面上可以实现拖拽排序的库有很多,比如 SortableJS、react-dnd、react-beautiful-dnd、react-sortable-hoc 等。
我列了一个表格:


优点缺点
SortableJS足够轻量级,而且功能齐全React 中使用起来并不是太方便,而且它的配置项写起来实在不太符合 React 的思维
react-dnd库小,贴合 react 拖拽场景多行拖拽不理想,react-beautiful-dnd 库比较大赖于HTML5 拖放 API,这有一些严重的限制
react-beautiful-dnd动画效果和细节非常完美同上
react-sortable-hoc多行拖拽优势很明显(相比其他库大多依赖于HTML5拖放API ,这有一些严重的限制。例如,如果你需要支持触摸设备,如果你需要锁定拖动到一个轴上,或者想在节点排序时设置动画,事情就会变得很棘手。React-sortablehoc 旨在提供一组简单的 higher-order 组件来填补这些空白。如果您正在寻找一种 dead-simple ,mobile-friendly 的方式来向列表中添加可排序功能,那么您就在正确的位置了。)列表过长,需要滑动的列表中拖拽时,滑动后位置不匹配会发生偏移

实践中还会有一些踩坑:



  • reat-dnd 在项目中快速拖拽时一直报错,"Invariant Violation: Expected targetIds to be registered. "在他的 issue 中也有很多人反应这个问题,虽然有修复过但是并没有完全修复,在 overStack 中也并没有找到好的解决方案。

  • 这里以我们实践结论,从 react-dnd、react-sortable-hoc 两个库进行讲解


react-dnd


概念

react dnd 是一组 react 高阶组件,使用的时候只需要使用对应的 API 将目标组件进行包裹,即可实现拖动或接受拖动元素的功能。
在拖动的过程中,不需要开发者自己判断拖动状态,只需要在传入的配置对象中各个状态属性中做对应处理即可,因为react-dnd 使用了 redux 管理自身内部的状态。
值得注意的是,react-dnd 并不会改变页面的视图,它只会改变页面元素的数据流向,因此它所提供的拖拽效果并不是很炫酷的,我们可能需要写额外的视图层来完成想要的效果,但是这种拖拽管理方式非常的通用,可以在任何场景下使用,非常适合用来定制。


安装

npm i react-dnd

核心 API

介绍实现拖拽和数据流转的核心 API ,这里以 hook 为例。


DndProvider

使用 react-dnd 需要最外层元素加 DndProvider ,DndProvider 的本质是一个由 React.createContext 创建一个上下文的容器(组件),用于控制拖拽的行为,数据的共享,类似于 react-redux 的 Provider。


import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

<DndProvider backend={HTML5Backend}>组建模块</DndProvider>;

Backend

react dnd 将 DOM 事件相关的代码独立出来,将拖拽事件转换为 react dnd 内部的 redux action。由于拖拽发生在 H5 的时候是 ondrag,发生在移动设备的时候是由 touch 模拟,react dnd 将这部分单独抽出来,方便后续的扩展,这部分就叫做 backend。它是 dnd 在 DOM 层的实现。



  • react-dnd-html5-backend : 用于控制 html5 事件的 backend

  • react-dnd-touch-backend : 用于控制移动端 touch 事件的 backend

  • react-dnd-test-backend : 用户可以参考自定义 backend


useDrag

让 DOM 实现拖拽能力的构子


import React from 'react';
import { useDrag } from 'react-dnd';

export default function Player() {
// 第一个返回值是一个对象,主要放一些拖拽物的状态。后面会介绍,先不管
// 第二个返回值:顾名思义就是一个Ref,只要将它注入到DOM中,该DOM就会变成一个可拖拽的DOM
const [_, dragRef] = useDrag(
{
type: 'Player', // 给拖拽物命名,后面用于分辨该拖拽物是谁,支持string和symbol
item: { id: 1 } // 拖拽物所携带的数据,让后面一些事件可以拿到数据,已达到交互的目的
},
[]
);
// 注入Ref,现在这个DOM就可以拖拽了
return <div ref={dragRef} />;
}

返回三个参数

第一个返回值是一个对象 表示关联在拖拽过程中的变量,需要在传入 useDrag 的规范方法的 collect 属性中进行映射绑定,比如:isDraging, canDrag 等

第二个返回值 代表拖拽元素的 ref

第三个返回值 代表拖拽元素拖拽后实际操作到的 dom

传入两个参数



  • 第一个参数,是一个对象,是用于描述了drag 的配置信息,常用属性


type指定元素的类型,只有类型相同的元素才能进行 drop 操作
item元素在拖拽过程中,描述该对象的数据,如果指定的是一个方法,则方法会在开始拖拽时调用,并且需要返回一个对象来描述该元素。
end(item, monitor)拖拽结束的回调函数,item 表示拖拽物的描述数据,monitor 表示一个 DragTargetMonitor 实例
isDragging(monitor)判断元素是否在拖拽过程中,可以覆盖Monitor对象中的 isDragging方法,monitor 表示一个 DragTargetMonitor 实例
canDrag(monitor)判断是否可以拖拽的方法,需要返回一个 bool 值,可以覆盖 Monitor 对象中的 canDrag 方法,与 isDragging 同理,monitor 表示一个 DragTargetMonitor 实例
collect它应该返回一个描述状态的普通对象,然后返回以注入到组件中。它接收两个参数,一个 DragTargetMonitor 实例和拖拽元素描述信息item


  • 第二个参数是一个数组,表示对方法更新的约束,只有当数组中的参数发生改变,才会重新生成方法,基于react 的 useMemo 实现


useDrop

实现拖拽物放置的钩子


import { useDrop } from 'react-dnd';

export const Dustbin = () => {
const [_, dropRef] = useDrop({
accept: ['Player'], // 指明该区域允许接收的拖放物。可以是单个,也可以是数组
// 里面的值就是useDrag所定义的type

// 当拖拽物在这个拖放区域放下时触发,这个item就是拖拽物的item(拖拽物携带的数据)
drop: (item) => {}
});

// 将ref注入进去,这个DOM就可以处理拖拽物了
return <div ref={dropRef}></div>;
};

返回两个参数

第一个返回值是一个对象 表示关联在拖拽过程中的变量,需要在传入 useDrag 的规范方法的 collect 属性中进行映射绑定。

第二个返回值 代表拖拽元素的 ref
传入一个参数
用于描述drop的配置信息,常用属性


accept指定接收元素的类型,只有类型相同的元素才能进行 drop 操作
drop(item, monitor)有拖拽物放置到元素上触发的回调方法,item 表示拖拽物的描述数据,monitor 表示 DropTargetMonitor 实例,该方法返回一个对象,对象的数据可以由拖拽物的 monitor.getDropResult 方法获得
hover(item, monitor)当拖住物在上方 hover 时触发,item 表示拖拽物的描述数据,monitor表示 DropTargetMonitor 实例,返回一个 bool 值
canDrop(item, monitor)判断拖拽物是否可以放置,item 表示拖拽物的描述数据,monitor 表示 DropTargetMonitor 实例,返回一个 bool 值

API 数据流转

image.png


react-sortable-hoc


概念

react-sortable-hoc 是一个基于 React 的拖拽排序组件,它可以让你轻松地实现拖拽排序功能。它提供了一系列的 API,可以让你自定义拖拽排序的行为。它支持拖拽排序的单个列表和多个列表,以及拖拽排序的可视化。


安装

npm install react-sortable-hoc

引入

import { SortableContainer, SortableElement, arrayMove, SortableHandle } from 'react-sortable-hoc';

核心 API


  • sortableContainer 是所有可排序元素的容器

  • sortableElement 是每个可渲染元素的容器

  • sortableHandle 是定义拖拽手柄的容器

  • arrayMove 主要用于将移动后的数据排列好后返回


SortableContainer HOC

PropertyTypeDefaultDescription
axisStringy项目可以水平、垂直或网格排序。可能值:x、y 或 xy
lockAxisString如果您愿意,可以在排序时将移动锁定在轴上。这不是 HTML5 拖放所能做到的。可能值:x 或 y。
helperClassString您可以提供一个要添加到 sortable helper 的类,以向其添加一些样式
transitionDurationNumber300元素移动位置时转换的持续时间。{ 39d 要禁用 @661 }
keyboardSortingTransitionDurationNumbertransitionDuration在键盘排序期间移动辅助对象时转换的持续时间。如果要禁用键盘排序助手的转换,请将其设置为 0。如果未定义,则默认为 transitionDuration 设置的值
keyCodesArray{lift: [32],drop: [32],cancel: [27],up: [38, 37],down: [40, 39]}一个包含每个 keyboard-accessible 操作的键码数组的对象。
pressDelayNumber0如果您希望元素只在按下一段时间后才可排序,请更改此属性。mobile 的一个合理的默认值是 200。不能与 distance 属性一起使用。
pressThresholdNumber5忽略冲压事件之前要容忍的移动像素数。
distanceNumber0如果您希望元素只在被拖动一定数量的像素之后才变得可排序。不能与 pressDelay 属性一起使用。
shouldCancelStartFunctionFunction此函数在排序开始前调用,可用于在排序开始前以编程方式取消排序。默认情况下,如果事件目标是 input、textarea、select 或 option,它将取消排序。
updateBeforeSortStartFunction在排序开始之前调用此函数。它可以返回一个 promise,允许您在排序开始之前运行异步更新(比如 setState )。function ({ node, index, collection, isKeySorting }, event )
onSortStartFunction开始排序时调用的回调。function({ node, index, collection, isKeySorting }, event ) |
onSortMoveFunction当光标移动时在排序期间调用的回调。function ( event )|
onSortOverFunction在向上移动时调用的回调。function ({ index, oldIndex, newIndex, collection, isKeySorting }, e )
onSortEndFunction排序结束时调用的回调。function ({ oldIndex, newIndex, collection, isKeySorting }, e )
useDragHandleBooleanfalse如果您使用的是SortableHandleHOC,请将其设置为true
useWindowAsScrollContainerBooleanfalse如果需要,可以将window设置为滚动容器
hideSortableGhostBooleantrue是否 auto-hide 重影元素。默认情况下,为了方便起见,React Sortable List 将自动隐藏当前正在排序的元素。如果要应用自己的样式,请将此设置为 false。
lockToContainerEdgesBooleanfalse您可以将可排序元素的移动锁定到其父元素 SortableContainer
lockOffsetOffsetValue*|[OffsetValue*,OffsetValue*]"50%"当 lockToContainerEdges 设置为 true 时,这将控制可排序辅助对象与其父对象 SortableContainer 的上/下边缘之间的偏移距离。百分比值相对于当前正在排序的项的高度。如果您希望指定不同的行为来锁定容器的顶部和底部,您还可以传入 array(例如:["0%", "100%"])。
getContainerFunction返回可滚动容器元素的可选函数。此属性默认为 SortableContainer 元素本身或(如果 useWindowAsScrollContainer 为真)窗口。使用此函数指定一个自定义容器对象(例如,这对于与某些第三方组件(如 FlexTable )集成非常有用)。这个函数被传递给一个参数(即 wrappedInstanceReact 元素),它应该返回一个 DOM 元素。
getHelperDimensionsFunctionFunction可选的function ({ node, index, collection }),它应该返回 SortableHelper 的计算维度。有关详细信息,请参见默认实现 |
helperContainerHTMLElement | 函数document.body默认情况下,克隆的可排序帮助程序将附加到文档正文。使用此属性可指定要附加到可排序克隆的其他容器。接受 HTMLElement 或返回 HTMLElement 的函数,该函数将在排序开始之前调用
disableAutoscrollBooleanfalse拖动时禁用自动滚动

如何使用

直接上demo


import React from 'react';
import { arrayMove, SortableContainer, SortableElement } from 'react-sortable-hoc';

// 需要拖动的元素的容器
const SortableItem = SortableElement((value) => <div>{value}</div>);
// 整个元素排序的容器
const SortableList = SortableContainer((items) => {
return items.map((value, index) => {
return <SortableItem key={`item-${index}`} index={index} value={value} />;
});
});

// 拖动排序组件
class SortableComponnet extends React.Component {
state = {
items: ['1', '2', '3']
};
onSortEnd = ({ oldIndex, newIndex }) => {
this.setState(({ items }) => {
arrayMove(items, oldIndex, newIndex);
});
};
render() {
return (
<div>
<SortableList
distance={5}
axis={'xy'}
items={this.state.items}
helperClass={style.helperClass}
onSortEnd={this.onSortEnd}
/>

</div>

);
}
}

export default SortableComponnet;

在上面的示例中,我们使用 SortableContainer 组件容纳了一组可拖拽排序的元素,使用 SortableElement 组件包裹了每个元素,并且实现了 onSortEnd 回调函数,以便在拖拽排序完成后更新状态。


效果展示

拖拽3.gif


踩坑

image.png



解决:这种报错的解决方法都是 SortableElement 和 SortableContainer 返回组件时外面都要单独在包一个 html 容器标签, 例子是包了个 <div>



结果导向


Antd 版本 4.16.0及以上


使用 react-dnd 搭配 Upload 上传组件 itemRender api实现

注:这里主要基于 react-dnd 实现,Antd 5.x.x 官网有基于 dnd-kit 来实现对上传列表拖拽排序。


const Box: React.FC<BoxProps> = ({ children, index, className, onClick, moveCard }) => {
const ref = useRef<HTMLDivElement>(null);

const [, dragPreview] = useDrag({
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging()
}),
// item 中包含 index 属性,则在 drop 组件 hover 和 drop 是可以根据第一个参数获取到 index 值
item: { type: 'page', index }
});
const [, drop] = useDrop({
accept: 'page',
hover(item: { type: string; index: number }) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// 自定义逻辑处理

// 执行 move 回调函数
moveCard(dragIndex, hoverIndex);
}
});
dragPreview(drop(ref));

return (
<div ref={ref} className={className} onClick={onClick}>
{children}
</div>

);
};

使用

使用 Antd Upload itemRender


image.png


Antd 版本低于 4.16.0


使用 react-sortable-hoc 库实现


  • SortableItem


const SortableItem = SortableElement((params: SortableItemParams) => (
<div>
<UploadList
locale={{ previewFile: '预览图片', removeFile: '删除图片' }}
showDownloadIcon={false}
showRemoveIcon={params?.props?.disabled}
listType={params?.props?.listType}
onPreview={params.onPreview}
onRemove={params.onRemove}
items={[params.item]}
/>

</div>

));


  • SortableList


const SortableList = SortableContainer((params: SortableListParams) => {
return (
<div className='sortableList'>
{params.items.map((item, index) => (
<SortableItem
key={`${item.uid}`}
index={index}
item={item}
props={params.props}
onPreview={params.onPreview}
onRemove={params.onRemove}
/>

))}
{/* 这里是上传组件,设置最大限制后超出隐藏 */}
<Upload {...params.props} showUploadList={false} onChange={params.onChange}>
{params.props.children}
</Upload>
</div>

);
});


  • DragHoc


const DragHoc: React.FC<Props> = memo(
({ onChange: onFileChange, axis, onPreview, onRemove, ...props }) => {
const fileList = props.fileList || [];
const onSortEnd = ({ oldIndex, newIndex }: SortEnd) => {
onFileChange({ fileList: arrayMove(fileList, oldIndex, newIndex) });
};

const onChange = ({ fileList: newFileList }: UploadChangeParam) => {
onFileChange({ fileList: newFileList });
};

return (
<>
<SortableList
// 当移动 1 之后再触发排序事件默认是0会导致无法触发图片的预览和删除事件
distance={1}
items={fileList}
onSortEnd={onSortEnd}
axis={axis || 'xy'}
helperClass='SortableHelper'
props={props}
onChange={onChange}
onRemove={onRemove}
onPreview={onPreview}
/>

</>

);
}
);

使用方式

可直接替换掉 Upload 组件,props 不变。
如果项目中有已经封装好的 Upload 上传组件,尽量不改变原有逻辑代码前提下,更希望以插件的形式按需加载?方案:



可以剔除掉 SortableList 中 SortableContainer 包裹的 Upload 组件(这一步经过实践是可行的,说 Upload UploadList 都要被 SortableContainer 包裹,否走会重复上传和拖拽失败?目前我是没遇到,重复上传是因为 Upload 组件 showUploadList 拖拽场景下必须是 false )



使用案例:( isDrag 表示需要拖拽场景,继而加载)



{isDrag && (
<DragHoc
accept={accept}
axis={axis}
showUploadList={{ showRemoveIcon }}
fileList={fileList}
onChange={(e) =>
{
if (onChange) {
onChange(e);
}
}}
onPreview={preview}
onRemove={remove}
listType={listType}
/>

)}

注:当然也可以用 react-dnd 来实现,只是多行拖拽流畅性较差。感兴趣也可以试试


踩坑

图片按钮点击无效

在 Antd 的 Upload 组件中,图片墙上会有「预览」、「删除」等按钮,但是在 react-sortable-hoc 的逻辑中,只要我点击了图片,就会触发图片的拖拽函数,无法触发图片上的各种按钮,所以需要在 SortableList 上重新设置一下 distance 属性,设置成 1 即可。

官网:



If you'd like elements to only become sortable after being dragged a certain number of pixels. Cannot be used in conjunction with the pressDelay prop.

(如果您希望元素仅在拖动一定数量的像素后才可排序。不能与 pressDelay 道具一起使用。默认为 0)



上传图片一直 uploading

image.png

原因:当图片列表发生变化,整个 sortable 容器被删除并重新渲染,导致请求失效。


解决方案:



需要将 SortableItem,SortableList 写在 React.FC 外面,每次组件内部 state 发生变化,不会重新执行 SortableContainer 和 SortableElement 方法,就可以让可排序容器里面的元素自动只更新需要改变的 DOM 元素,而不会整个删除并重新渲染了。



图片的 disabled 状态失效

原因:SortableContainer 包裹的组件对 Upload 图片和上传进行了拆分处理,所以需要单独去控制预览和删除按钮


const SortableItem = SortableElement((params: SortableItemParams) => (
<div>
<UploadList
locale={{ previewFile: '预览图片', removeFile: '删除图片' }}
showDownloadIcon={false}
showRemoveIcon={params?.props?.disabled} //这里需要单独控制
listType={params?.props?.listType}
onPreview={params.onPreview}
onRemove={params.onRemove}
items={[params.item]}
/>

</div>

));

效果展示

拖拽1.gif


参考文献



作者:叫叫技术团队
来源:juejin.cn/post/7312634879987122186
收起阅读 »

16进制竟然可以减小代码体积

web
随着前端项目的复杂度越来越高,打包后的文件体积也越来越大,这直接影响到页面的加载速度。为了优化加载速度,我们需要采取各种方式来减小包的体积。使用 16 进制存储就是一种非常有效的方法。 以 @tenado/lunarjs 库为例,它提供了阴阳历转换以及获取节日...
继续阅读 »

随着前端项目的复杂度越来越高,打包后的文件体积也越来越大,这直接影响到页面的加载速度。为了优化加载速度,我们需要采取各种方式来减小包的体积。使用 16 进制存储就是一种非常有效的方法。


@tenado/lunarjs 库为例,它提供了阴阳历转换以及获取节日信息等功能。如果我们查看它的源码,可以看到使用了 16 进制存储。例如src/lunar.js


# src/lunar.js
export default [ 0x4bd8, 0x4ae0, 0xa570, 0x54d5, 0xd260, ... ];

这些信息如果用字符串存储,会占用很多字节。但使用 16 进制后,每个阴历信息只需要 6 个字符串。这极大地减少了库的大小。在实际生产环境中,使用 16 进制存储甚至可以节省几十 KB 的大小。


此外,16 进制还可以用于压缩其他数据,比如图片等资源。使用 16 进制编码,可以达到无损压缩的效果,相比传统压缩算法可以减小体积而不影响质量。


总之,16 进制编码是一种非常高效的存储方式,可以大幅减小项目的打包体积,提升页面加载速度。在前端优化中,合理使用 16 进制编码是一个非常重要和有效的手段。


16 进制的基本概念


16 进制在数学中是一种逢16进1的进位制。一般用数字0到9和字母A到F表示,其中:A~F相当于十进制的10~15,这些称作十六进制数字,在 js 中,16 进制使用 0x 前缀表示。


它的优点是可以使用更少的位数来表示一个数值,每个 16 进制位数代表 4 个二进制位,例如0x10,表示16进制的10,十进制的16,二进制表示为00010000,占用 4 个二进制位。


16 进制在代码中的表示


在库@tenado/lunarjs 中,保存了 1900-2100 年的阴历信息,数据格式如下:


{
1900: {
year: 1900,
firstMonth: 1,
firstDay: 31,
isRun: false,
runMonth: 8,
runMonthDays: 29,
monthsDays: [29, 30, 29, 29, 30, 29, 30, 30, 30, 30, 29, 30],
},
1901: {
year: 1901,
firstMonth: 2,
firstDay: 19,
isRun: false,
runMonth: 0,
runMonthDays: 0,
monthsDays: [29, 30, 29, 29, 30, 29, 30, 29, 30, 30, 30, 29],
},
}

将 1900-2100 年的数据存起来,占用的体积大概是 41k,作为包来说这个体积很大,这里存为十六进制数据,将数据压缩到 4k。


如何压缩数据呢?查看数据规律,我们发现:



闰月天数:只有三种值,即 0、29、30,在计算的时候先判断是否为闰月,再计算天数,0 代表 29, 1 代表 30




1-12 月天数,天数可以为 29 和 30,分别用 0 和 1 表示




闰月月份,闰月可能为 1-12,因此我们使用4个二进制数表示,最大可以表示 16



用 17 个二进制数表示阴历数据信息,从右到左:


1716-54-1
闰月天数1-12 月天数闰月月份

这里transform/index.js实现了一个简单的转换处理,将数据转换为 1900-2100 年依次按 index 排序的数组,数组的每一项里面存储了该年的阴历信息。


使用位运算从 16 进制中还原数据


位运算是将参与运算的数字转换为二进制,然后逐位对应进行运算。



按位与& 按位与运算为:两位全为1,结果为1,即1&1=1,1&0=0,0&1=0,0&0=0




按位或| 按位或运算为:两位只要有一位为1,结果则为1,即1|1=1,1|0=1,0|1=1,0|0=0




异或运算^ 两位为异,即一位为1一位为0,则结果为1,否则为0。即1 ^ 1=0,1 ^ 0=1,0 ^ 1=1,0 ^ 0=0




取反~ 将一个数按位取反,即~ 0 = 1,~ 1 = 0




右移>> 将一个数右移若干位,右边舍弃,正数左边补0,负数左边补1。每右移一位,相当于除以一次2 例如8 >> 2表示将8的二进制数1000右移两位变成0010 例如i >>= 2表示将变量i的二进制右移两位,并将结果赋值给i




设置二进制指定位置的值为1 value | (1 << position),例如设置十进制数8(1000)的第2位二进制数为1,注意这里index从0开始,且是从右向左计算,可以这样做8 | (1 << 2),结果为1100




设置二进制指定位置的值为0 value & ~(1 << position),例如设置十进制数8(1000)的第3位二进制数为0,注意这里index从0开始,可以这样做8 & ~(1 << 3),结果为0000



1、获取 1-4 位存储的闰月月份信息


取出16进制数据中存储的,从右边数1-4位数据,使用二进制数1111和十六进制数据按位与运算,可以获取到月份信息。通过转换1111可以得到对应的十六进制为0xf


例如,从src/lunar.js的数据里面获取1900年,即index为0的数据,进行位运算,0x4bd8 & 0xf可以得到结果为8,即1900年的8月为闰月。


2、获取 5-16 位存储的月天数信息


取出16进制数据中存储的,从右边数5-16位数据,仍旧可以使用按位与运算,获取到月天数信息。


从16开始的二进制数为1000000000000000,对应的十六进制为0x8000,到4结束的二进制数为1000,对应的十六进制为0x8,每次向右移一位进行按位与计算,可以获取到1-12月的天数数据,可以这样计算:


let sum = 0;
const lunar = 0x4bd8;
for (let i = 0x8000; i > 0x8; i >>= 1) {
sum += lunar & i ? 1 : 0;
}

3、获取 17 位存储的闰月天数信息


取出16进制数据中存储的,从右边数17位数据,使用二进制数10000000000000000和十六进制数据按位与运算,可以获取到闰月天数信息,10000000000000000对应的十六进制为0x100000x4bd8 & 0x10000可以得到结果为0,即1900年的闰月天数为29。


总结


总结起来,使用16进制保存数据可以有效减小包体积,提高前端项目的加载速度。在具体实现上,可以利用位运算来从16进制中还原数据。以下是一些关键点的总结:


1、数据格式


数据以16进制形式存储,可以有效减小体积。在JavaScript中,16进制使用0x前缀表示,例如0x4bd8。


2、数据规律


观察数据规律,了解存储的信息是如何组织的,包括每部分数据的含义和位数


3、使用位运算还原数据


使用位运算可以从16进制中还原具体的数据。以下是一些常用的位运算操作:


按位与&: 用于提取指定位的信息。
右移>>: 用于将二进制数向右移动,类似于除以2的操作。
设置二进制指定位置的值为1: 使用value | (1 << position)操作。
设置二进制指定位置的值为0: 使用value & ~(1 << position)操作。

4、注意事项


使用16进制存储需要在代码中添加注释,以便他人理解和维护。


数据存储格式的选择要根据具体场景和需求,权衡可读性和体积优化。


综合以上总结,合理使用16进制存储数据是一种有效的前端优化手段,特别适用于需要大量静态数据的情况。


作者:是阿派啊
来源:juejin.cn/post/7312611470733836340
收起阅读 »

一个看起来只有2个字长度却有8的字符串引起的bug

web
前言 我们有一个需求,用户的昵称如果长度超过6就截取前6个字符并显示...。今天,测试突然提了一个bug,某个用户的昵称只显示了...,鼠标hover的时候又显示2个字的昵称。刚看到这个问题的时候我也是一头雾水。 找出原因 在看到这个现象后,我发现其他昵称都...
继续阅读 »

前言


我们有一个需求,用户的昵称如果长度超过6就截取前6个字符并显示...。今天,测试突然提了一个bug,某个用户的昵称只显示了...,鼠标hover的时候又显示2个字的昵称。刚看到这个问题的时候我也是一头雾水。


找出原因


image.png

在看到这个现象后,我发现其他昵称都显示正常,但实在摸不着头脑这到底是怎么回事。然后查看了一下其他2个字的昵称是没问题的,然后通过console.log发现这个昵称居然长度有8,走了截取的分支。然后通过google发现这里面应该包含了零宽字符。

其实,第一时间就应该想到这个字符串不对劲的,但完全忘记了零宽字符的存在,走了不少弯路。


在查找的过程中发现,Array.from可以查看字符串的真实长度,除了emoji


image.png

不过Array.from并不能解决我的问题。


使用正则匹配unicode码点过滤零宽字符


在网上找了个方法来过滤掉这些看不见的字符,最常见的解决方案就是下面这行代码。


str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");

然而并没有用,我开始怀疑是不是这个方法有问题,然后遍历了这个昵称,把它的每个字符都转换成码点,发现这个昵称里的零宽字符并不是常见的这几种。


后来,又找到了一个比较完善的码点正则,但它太完善了,很长很长,也会过滤掉emoji,这可不行,用户昵称可能会包含emoji的。(这里就不贴出来代码了,太长了而且不适合我的情况。)


使用正则匹配unicode类别


一个字符有多种unicode属性,而正则支持按unicode属性匹配。


function stripNonPrintableAndNormalize(text, stripSurrogatesAndFormats) {
// strip control chars. optionally, keep surrogates and formats
if(stripSurrogatesAndFormats) {
text = text.replace(/\p{C}/gu, '');
} else {
text = text.replace(/\p{Cc}/gu, '');
text = text.replace(/\p{Co}/gu, '');
text = text.replace(/\p{Cn}/gu, '');
}

// other common tasks are to normalize newlines and other whitespace

// normalize newline
text = text.replace(/\n\r/g, '\n');
text = text.replace(/\p{Zl}/gu, '\n');
text = text.replace(/\p{Zp}/gu, '\n');

// normalize space
text = text.replace(/\p{Zs}/gu, ' ');

return text;
}
console.log("⁡⁡⁠河豚".length);
console.log(stripNonPrintableAndNormalize("⁡⁡⁠河豚", true).length);

image.png


总结


这个昵称其实就是包含了&nobreak;,通过unicode类别匹配可以过滤掉它。


我之前有在原贴用户主页的控制台中看见了&nobreak;,但当时居然没当回事,以为是别人对昵称做的处理。如果直接搜它马上就能解决问题了,有不少人遇到non-break-space引发的bug。谨以此记,吸取教训。


参考链接:stackoverflow中的解决办法unicode属性


作者:河豚学前端
来源:juejin.cn/post/7312241785542541327
收起阅读 »

告别繁琐操作!Maven常用命令一网打尽,让你的项目开发事半功倍!

Maven作为一款强大的项目管理工具,已经成为了Java开发者的必备技能。那么,如何才能更好地利用Maven来管理我们的项目呢?本文将为你介绍Maven的常用命令,让你的项目构建更轻松!一、maven 的概念模型Maven 包含了一个项目对象模型 ,一组标准集...
继续阅读 »

Maven作为一款强大的项目管理工具,已经成为了Java开发者的必备技能。那么,如何才能更好地利用Maven来管理我们的项目呢?本文将为你介绍Maven的常用命令,让你的项目构建更轻松!

一、maven 的概念模型

Maven 包含了一个项目对象模型 ,一组标准集合,一个项目生命周期,一个依赖管理系统,和用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑。

Description

项目对象模型 (Project Object Model)

一个 maven 工程都有一个 pom.xml 文件,通过 pom.xml 文件定义项目的坐标、项目依赖、项目信息、插件目标等。

依赖管理系统(Dependency Management System)

通过 maven 的依赖管理对项目所依赖的 jar 包进行统一管理。

比如:项目依赖 junit4.9,通过在 pom.xml 中定义 junit4.9 的依赖即使用 junit4.9,如下所示是 junit4.9的依赖定义:

<dependencies>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>4.9</version>

<scope>test</scope>
</dependency>
<dependencies>

一个项目生命周期(Project Lifecycle)

使用 maven 完成项目的构建,项目构建包括:清理、编译、测试、部署等过程,maven 将这些过程规范为一个生命周期,如下所示是生命周期的各个阶段:
Description
maven 通过执行一些简单命令即可实现上边生命周期的各个过程,比如执行 mvn compile 执行编译、执行 mvn clean 执行清理。

一组标准集合

maven将整个项目管理过程定义一组标准,比如:通过 maven 构建工程有标准的目录结构,有标准的生命周期阶段、依赖管理有标准的坐标定义等。

插件(plugin)目标(goal)

maven 管理项目生命周期过程都是基于插件完成的。

二、Maven的常用命令

我们可以在cmd 中通过maven命令来对我们的maven工程进行编译、测试、运行、打包、安装、部署。下面将简单介绍一些我们日常开发中经常会用到的一些maven 命令。
Description

1、mvn compile 编译命令

compile 是 maven 工程的编译命令,作用是将 src/main/java 下的文件编译为 class 文件输出到 target目录下。

cmd 进入命令状态,执行mvn compile,如下图提示成功:
Description

查看 target 目录,class 文件已生成,编译完成。
Description

2、mvn test 测试命令

test 是 maven 工程的测试命令 mvn test,会执行src/test/java下的单元测试类。

cmd 执行 mvn test 执行 src/test/java 下单元测试类,下图为测试结果,运行 1 个测试用例,全部成功。

Description

3 、mvn clean 清理命令

clean 是 maven 工程的清理命令,执行 clean 会删除 target 目录及内容。

4、mvn package打包命令

package 是 maven 工程的打包命令,对于 java 工程执行 package 打成 jar 包,对于web 工程打成war包。

只打包不测试(跳过测试):

mvn install -Dmaven.test.skip=true

5、 mvn install安装命令

install 是 maven 工程的安装命令,执行 install 将 maven 打成 jar 包或 war 包发布到本地仓库。

从运行结果中,可以看出:当后面的命令执行时,前面的操作过程也都会自动执行。

6、 mvn deploy 部署命令

这个命令用于将项目部署到远程仓库,以便其他项目可以引用。在执行这个命令之前,需要先执行mvn install命令。

7、mvn help:system

这个命令用于查看系统中可用的Maven版本。

8、mvn -v

这个命令用于查看当前环境中Maven的版本信息。

9、源码打包

#源码打包
mvn source:jar

mvn source:jar-no-fork

10、Maven 指令的生命周期

关于Maven的三套生命周期,前面我们已经详细的讲过了,这里再简单地回顾一下。

maven 对项目构建过程分为三套相互独立的生命周期,请注意这里说的是“三套”,而且“相互独立”。

Description

这三套生命周期分别是:

  • Clean Lifecycle 在进行真正的构建之前进行一些清理工作。

  • Default Lifecycle 构建的核心部分,编译,测试,打包,部署等等。

  • Site Lifecycle 生成项目报告,站点,发布站点。

在这里给大家分享一下【云端源想】学习平台,无论你是初学者还是有经验的开发者,这里都有你需要的一切。包含课程视频、在线书籍、在线编程、一对一咨询等等,现在功能全部是免费的,点击这里,立即开始你的学习之旅!

三、Maven常用技巧

1、使用镜像仓库加速构建

由于Maven默认从中央仓库下载依赖,速度较慢。我们可以配置镜像仓库来加速构建。在settings.xml文件中添加以下内容:


<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>
</mirrors>

2、使用插件自定义构建过程

Maven提供了丰富的插件来帮助我们完成各种任务,如代码检查、静态代码分析、单元测试等。我们可以在pom.xml文件中添加相应的插件来自定义构建过程。例如,添加SonarQube插件进行代码质量检查:


<build>
<plugins>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.1.2184</version>
</plugin>
</plugins>
</build>

四、第一个Maven程序

IDEA创建 Maven项目

打开我们的IDEA,选择 Create New Project

Description
然后,选择 Maven 项目
Description
填写项目信息,然后点击next
Description
选择工作空间,然后第一个Maven程序就创建完成了
Description
以上就是IDEA创建创建第一个 Maven项目的步骤啦,都看到这里了,不要偷懒记得打开电脑和我一起试一试哦。

目录结构

Java Web 的 Maven 基本结构如下:

├─src
│ ├─main
│ │ ├─java
│ │ ├─resources
│ │ └─webapp
│ │ └─WEB-INF
│ └─test
│ └─java

结构说明:

  • src:源码目录

  • src/main/java:Java 源码目录

  • src/main/resources:资源文件目录

  • src/main/webapp:Web 相关目录

  • src/test:单元测试

小结:

通过掌握Maven的常用命令和技巧,我们可以更高效地管理Java项目,提高开发效率。希望这篇文章能帮助大家更好地使用Maven。

收起阅读 »

如何告别502、友好的告知用户网站/app正在升级维护

web
封面只是想分享一张喜欢的图片,嘻嘻嘻 一个产品,用户除了会在意流程简不简单、好不好用、页面好不好看以外,各种小细节也很重要。 今天讲讲我接到的一个新需求——整改版本更新功能。 一、以前是这样的 1. 发版前:微信群通知用户 如果计划今晚发版,研发部就通知运营...
继续阅读 »

封面只是想分享一张喜欢的图片,嘻嘻嘻



一个产品,用户除了会在意流程简不简单、好不好用、页面好不好看以外,各种小细节也很重要。


今天讲讲我接到的一个新需求——整改版本更新功能。


一、以前是这样的


1. 发版前:微信群通知用户


如果计划今晚发版,研发部就通知运营部,运营部再在群里通知各用户"我们将于YYYY年MM月DD日 hh:mm:ss发版,请大家......"


image.png


2. 发版时:接口502+页面无响应


在这之前,发版时用户仍停留在网站,但接口返回502,但502未告知给用户,所以用户不知道这时到底发生什么了,只知道页面没数据了、卡着动不了了、怎么刷新都没办法......


image.png


3. 发版后:app粗暴的强制更新


在这之前我们的项目还处于快速开发的过程,迭代间隔短频率高,有时我们会更新影响流程的重要功能,担心用户未及时更新,所以我们只提供了强制更新方案。


二、现在是这样的


这都什么时代了,还需要口口相传......


1. 发版前:升级预告


大家想想618、双十一、双十二,各大平台是不是很早就开始宣传“走过路过别错过,满三百减五十,快来加购吧~”


我们产品的用户群体有一线工人、办公室文员、喝茶的经理、车上的老总,提前告知用户,可以让经理及时换班、让工人规避做工单的风险等等。总之,有事早通知,准是没错的。


怎么通知呢?



  1. 在内部管理平台新增一条升级预告消息,可以包含版本号version、预告内容content、预告状态status(是否有效)、平台(区分网站和APP,因为可能不是同时都需要发版)

  2. 网站:

    1)用户登录或主动刷新页面时,页面顶部显示升级预告,实现逻辑和app一样

  3. APP:

    1)充分利用消息推送、短信推送,可以根据自身业务来定,看这次更新是否需要紧急通知用户,我们仅使用消息推送。平台新增一条升级预告后,就主动推送一条app消息给用户


    2)打开app后,弹框弹出升级提醒(包含“我知道了”和“不再提醒”按钮),在store记录预告消息的id。




  • 点击“不再提醒”,则在store缓存中将isRemind置为false

  • 点击“我知道了”,则下次打开首页还会显示弹框。

  • 每次打开首页时,从接口获取到数据,如果消息状态有效status: true,则判断消息id同缓存中消息id是否一致:一致则表示是同一条消息,则根据缓存中的isRemind来判断是否显示消息;不一致则表示是新消息,直接显示。


1702434850828_56C69DC0-7552-4e81-AA8D-0CB2B275BB09.png


2. 发版时:


场景:我们产品是全量发布,发布完成后网页能正常访问,但这时产品会进行验收,还不希望用户进行访问。


页面:pc和app的升级中页面单独写在项目中,这样可以在页面中写监听:监听到版本正常后就返回首页。


方案:发版中和验收中,运维将别人的IP设置为不能访问,将公司特定IP设置为可访问。


网站:不能访问的,nginx就设置跳转到升级中页面;能访问的就不做处理,发版时页面会接口报502(只能内部人员能看见),发版后验收时页面正常使用。


APP:app中不好重定向,所以通过配置文件来告诉前端该用户是否应该访问页面。远程存在2个json配置文件,内容就是一个对象之类的{isEntry: 1}{isEntry: 0},分别表示可以访问和不可访问;app打开时,前端请求json,根据是否可以访问做处理,不能访问的,前端让重定向到升级中页面,能访问的不做处理。


3. 发版后:


内部管理平台:上传新的安装包、配置升级文案等


网站:所有人正常使用


APP:所有人正常使用,打开App如果版本较低则会主动打开"版本更新"弹框。


APP可以提升用户体验的地方:



  • 版本判断放在登录之前,先检查是否有更新,也就是这个接口不需要用户权限控制

  • 升级页面判断是否连接了wifi,连接wifi则更新不耗流量,没连接则显示此安装包只有多大

  • 提供“暂不更新”和“检查更新”的入口


image.png


总结


思考方案时,自己感觉做了一件大事;但多思考几次后,特别是写完文章后,又觉得新的升级方案其实很简单,上面说了一堆废话。



没关系,把每一件小事做好,就很好啦~~



作者:LJINGER
来源:juejin.cn/post/7311695633563795506
收起阅读 »

⭐️天啦噜~实习生被当作正式员工直接上手toc端项目啦

web
背景 本需求的提出基于谷歌应用商店的硬性要求:需要在xxx日前于谷歌商店上架账号注销的网页,保证玩家能够通过网页进行账号注销,从而满足谷歌商店的硬性需求。(问了产品和运营在谷歌商店哪,结果都找不到在哪,离谱)账号注销 配置解析 package.json 我个人...
继续阅读 »

背景


本需求的提出基于谷歌应用商店的硬性要求:需要在xxx日前于谷歌商店上架账号注销的网页,保证玩家能够通过网页进行账号注销,从而满足谷歌商店的硬性需求。(问了产品和运营在谷歌商店哪,结果都找不到在哪,离谱)账号注销


配置解析


package.json


我个人拉项目的时候比较喜欢从package.json中开始了解项目,比如项目中用了哪些第三方依赖,项目使用的是vue-cli启动还是webpack启动等等......


"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"postinstall": "patch-package"
},

比如上述中使用的是vue-cli(vue官方脚手架)启动的项目


serve:不用说,使用vue-cli启动项目


build:使用vue-cli打包项目,打包成js,css,html文件,具体看下图(这里是vue-cli打包为样例,vite打包的话未说明)


image.png


这里统一说明,下列所有文件,


前面的那串类似190.xxx.xxx,是由Webpack 为每个模块分配一个唯一的数字标识,这个标识通常代表了模块在整个打包中的位置。


中间的那串类似xxx.a768c482.xxx,都是由webpack或者vue-cli在构建(build)时,通过计算文件内容生成的哈希值,这样可以确保文件内容的唯一性和变化时生成不同的哈希值。所以在文件内容发生变化时,生成的文件名也会相应变化,从而避免浏览器缓存旧的文件。



注:每个css和js的前缀都基本对应,并且由于是webacpk生成的,所以可以自己额外的对其命名进行配置。



css(压缩过)


image.png



  • chunk-vendors:以chunk-vendors开头的,主要是对于引入的第三方依赖的样式,比如项目中使用的ant-design-vue,这里面就包含了ant-design-vue的样式


image.png

  • app:项目自身的样式代码,除了路由router里配置的组件

  • 其他:路由router中配置的组件里的样式(删掉路由配置的组件后,相应的打包样式文件消失了)


js(压缩过)


与css类似,多了map(映射文件)和-legacy后缀


source map文件包含了源代码与生成代码之间的映射关系,用于在浏览器中调试时将生成代码映射回源代码。


-legacy 的后缀通常表示这部分代码是针对不支持现代 JavaScript 特性的旧版浏览器生成的。


image.png


img 项目中使用过的图片,没使用的不会进行打包


index.html 原项目中public/index.html压缩后的


favicon.icon 原项目中public/favicon.ico图标


lint: 检查代码风格和潜在错误的方法。


也可以在项目根目录下的 .eslintrc.js 文件中进行自定义的规则定制


module.exports = {
root: true, // 表示 ESLint 应该停止在父级目录中查找配置文件。
env: { // 将 Node.js 的全局对象和一些特定于 Node.js 环境的变量(例如 `process`、
node: true, // `require` 等) 考虑在内,以避免对这些变量的使用产生未定义的警告或错误。
},
extends: [ // 包含了所使用的 ESLint 规则集,包含几个扩展
"plugin:vue/essential",
"eslint:recommended",
"@vue/typescript/recommended",
"plugin:prettier/recommended",
],
parserOptions: { //指定解析器版本,确保 ESLint 解析器能够正确理解代码中使用的 JavaScript 特性
ecmaVersion: 2020,
},
rules: {
// 在生产环境中允许控制台输出,但在开发环境中关闭。
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"vue/multi-word-component-names": "off", // 关闭 Vue 组件名使用多个单词的规则。
},
};


检查警告效果图:
image.png


postinstall:会检测 node_modules 中的包是否有需要修复的问题,并自动打补丁。


gitHooks


"gitHooks": {
"pre-commit": "lint-staged"
}

指定了在执行 Git 提交前(pre-commit 钩子)运行 lint-staged。这是一种通过 git 钩子(git hooks)来自动化代码检查和格式化的方法。(即当你执行git commit 后会进行检查)可以在lint-staged.config.js中配置,也可以在package.json中。


// lint-staged.config.js
module.exports = {
"*.{js,jsx,vue,ts,tsx}": "vue-cli-service lint", // js,jsx,vue,ts,tsx文件都会检查
};


env环境变量


环境变量在不同的环境下是不同的,比如现在下面的环境变量是开发环境的,当到正式环境时,baseUrl会换成类似https://juejin.cn/,也就是把原本32进制的ip地址换成了这种形式。


后端是对打包(build)后项目进行部署的,而env文件后端需要看到并且对你的环境变量相应的替换,才能正式上线部署。


window.$$env = {
baseUrl: "/test/apis",
appId: "test",
publicPath: "/test",
};

export interface Env {
baseUrl: string;
appId: string;
publicPath: string;
}

const env = (window as any).$$env as Env;
export default env;


封装网络拦截


先使用枚举定义状态码


export enum HttpCode {
Ok = 0,
ServerError = 500,
COOKIE_INVALID = 204,
INFO_INVALID = 205,
ERR_PRODUCT_CHANGE = 402,
SUSPENSION = 503,
}


封装一个网路拦截


export class apiService {
static instance: AxiosInstance | null = null;

// 重置网络拦截
static resetConfig(config?: AxiosRequestConfig, appId?: string) {
this.instance = this.createAxiosInstance(config, appId);
}

static getInstance() {
return this.instance || this.createAxiosInstance();
}

static createAxiosInstance(config?: AxiosRequestConfig, appId?: string) {
// 创建axios实例
xxx
// 请求拦截
xxx
// 响应拦截
xxx
return instance;
}
}

创建axios实例


const instance = Axios.create({
withCredentials: true, // 允许发送跨域请求的时候携带认证信息,通常是 Cookie
baseURL: env.baseUrl, // 网路请求前缀
timeout: 30 * 1000, // 超时请求时间限制
...config,
});

请求拦截


根据项目需求传请求头,比如用户信息,Authorization等


// 请求拦截
instance.interceptors.request.use((config) => {
config.headers = {
...config.headers,
"x-yh-appid": env.appId,
};
return config;
});

响应拦截


根据后端传回来的状态码处理相应的状态


// 响应拦截
instance.interceptors.response.use(
(response: AxiosResponse<any>) => {
const { code = -1, data = {}, msg = "" } = response.data;
if (handleUnlogin(code)) {
return Promise.reject(msg);
}
if (code === HttpCode.SUSPENSION) {
redirectSuspension();
return Promise.reject(msg);
}
if (code === HttpCode.Ok) {
return Promise.resolve(data);
}
return Promise.reject(msg);
},
(error) => {
return handleHttpError(error);
}
);

根据后端发送的状态码,判断用户是否登录


export function handleUnlogin(code: number) {
if ([HttpCode.COOKIE_INVALID, HttpCode.INFO_INVALID].includes(code)) {
localStorage.removeItem(LOCALSTORAGE_CURRENCY_CODE);
redirectLogin();
return true;
}
return false;
}

处理后端返回的错误信息


export function handleHttpError(error: any) {
const { status = 500, data = {} } = error.response || {};
let msg = data.msg || error.message;
switch (status) {
case HttpCode.ServerError:
msg = "Server internal error";
break;
}
return Promise.reject(msg);
}

功能设计


国际化设计


没配置翻译前但使用了vue-i18n


// 
export enum Direction {
UP = "上",
DOWN = "下",
LEFT = "左",
RIGHT = "右",
}

使用方法,vue中通过$t()来注入翻译文本


<script lang="ts">
import { Translate } from "@/constants";
import { defineComponent } from "vue";
export default defineComponent({
setup() {
return { Translate };
},
});
</script>

<span>{{ $t(Translate.UP) }}</span>
<input :placeholder="$t(Translate.UP)" />

实际展示


<span></span>
<input placeholder="上" />

配置翻译后


// main.ts
import i18n from "./locales";

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

src/locales/index


// src/locales/index
import VueI18n from "vue-i18n";
import en from "./language/en";

const i18n = new VueI18n({
locale: "en",
messages: {
en,
},
});

src/locales/language/en


// src/locales/language/en
import { Translate } from "@/translate";

export default {
[Translate.UP]: "Up",
[Translate.DOWN]: "Downe",
[Translate.LEFT]: "Left",
[Translate.RIGHT]: "Right",
}

实际展示


<span>Up</span>
<input placeholder="Up" />

main.ts中引入了配置好后的i18n,就会对每个组件中$t(Translate.xx)进行翻译,然后如果想翻译成其他语言,只需要修改在src/locales/index并且在src/locales/language中新增一个其他语言的文件


比如日文(看看就行,翻译别当真)


// src/locales/index
const i18n = new VueI18n({
locale: "ja",
messages: {
ja,
},
});

// src/locales/language/ja
import { Translate } from "@/translate";

export default {
[Translate.UP]: "じょうげ",
[Translate.DOWN]: "さゆう"
}

pc端和移动端适配设计(适用于结构类似,各自两套样式)


适配原理


export class SettingService {
// 表示该属性是只读的,即一旦被赋值,就不能再被修改。
// 确保 `mode` 属性在运行时保持不变,避免了一些意外的修改。
readonly mode: "pc" | "mobile";

constructor() {
// 判断设备是什么类型的,进行初始化
this.mode = isAndriod() || isIos(false) ? "mobile" : "pc";
// 将 `mode` 作为全局变量挂载到 Vue 的原型上,以便在整个应用程序中访问
Vue.prototype.$global = { mode: this.mode };
}
// 引入该方法判断是否是pc端,便于读取mode状态
isPc() {
return this.mode === "pc";
}
}

export const settingService = new SettingService();

整个项目适配


pc端和移动端各自展示的窗口样式是不同的,所以需要在容器中设置不同的样式


// router.js
{
path: "/",
component: settingService.isPc() ? PcLayout : MobileLayout,
name: "layout",
redirect: "/notice",
}

// pc端
<template>
<div class="layout">
<layout-header />
<div class="layout-kv"></div>
<router-view></router-view>
<layout-footer />
</div>
</template>

// 移动端
<template>
<div class="layout">
<layout-header />
<router-view class="layout-body"></router-view>
<layout-footer />
</div>
</template>

在App.vue中设置了 <body> 元素的 screen-mode 属性,属性值为 settingService.mode。这样,通过在 <body> 元素上设置这个属性,可以影响到整个页面中使用了相应选择器的样式。


<template>
<loading v-if="initing" />
<router-view v-else />
</template>
// App.vue
export default {
name: "App",
mounted() {
document.body.setAttribute("screen-mode", settingService.mode);
}
}

适配案例(即使用方法)


<div class="myClass1">不会覆盖</div>
<div class="myClass2">会覆盖</div>

默认为[screen-mode="mobile"]上面的样式,当切换到移动端时,下面的会覆盖上面同一类名的样式。


.myClass1 {
color: red
}
.myClass2 {
color: red;
font-size: 16px;
}

[screen-mode="mobile"] {
.myClass2 {
color: blue;
font-size: 32dpx;
}
}

解决页面显示的是缓存的内容而不是最新的内容


// 浏览器回退强制刷逻辑
window.addEventListener("pageshow", (event) => {
if (event.persisted) {
window.location.reload();
}
});

监听了 pageshow 事件,该事件在页面显示时触发,包括页面加载和页面回退(从缓存中重新显示页面)。



  • window.addEventListener("pageshow", (event) => {...});: 给 window 对象添加了一个 pageshow 事件监听器。当页面被显示时,这个监听器中的回调函数将被执行。

  • if (event.persisted) {...}: event.persisted 是一个布尔值,表示页面是否是从缓存中恢复显示的。如果为 true,表示页面是通过浏览器的后退/前进按钮从缓存中加载的。

  • window.location.reload(): 如果页面是从缓存中加载的,就调用 window.location.reload() 强制刷新页面,以确保页面的状态和内容是最新的。


这种逻辑通常用于解决缓存导致的页面状态不一致的问题。在有些情况下,浏览器为了提高性能会缓存页面,但有时这可能导致页面显示的是缓存的内容而不是最新的内容。通过在 pageshow 事件中检测 event.persisted,可以判断页面是否是从缓存中加载的,如果是,则强制刷新页面,确保它是最新的状态。


实时监听登录状态设计(操作浏览器前进回退刷新)


popstate 事件监听器,它会在浏览器的历史记录发生变化(比如用户点击浏览器的后退或前进或刷新按钮,或者执行了类似 history.back()history.forward()history.go(-1) 等 JavaScript 操作导致页面的 URL 发生了变化)。但使用router.push之类的操作不会触发。


 window.addEventListener("popstate", () => {
if (!settingService.hasUser() && !isLogin()) {
router.push("/login");
return;
}
});

对用户进行埋点(埋点时机)


埋点是对用户的一些信息进行收集,比如用户登录网站的时间,用户的昵称等等。


业务功能


1. 阅读须知,滑到底部并且勾选了同意按钮才能执行下一步


image.png
<div
ref="scrollContainer"
style="height: 340px;overflow-y: auto;"
@scroll="handleScroll"
>

文本内容
</div>
<div>
<input @change="handleScroll" type="checkbox" v-model="isChecked" />
<label for="customCheckbox">I know and satisfy all the conditions</label>
</div>
<!-- 执行下一步按钮 -->
// 如果阅读完了并且勾选了同意按钮,则可以执行下一步,否则不能
<button
v-if="isReaded && isChecked"
@submit="gotoPage"
/>

<button v-else disabled/>

// 创建响应式 ref
const scrollContainer: any = ref(null);
// 是否阅读须知到底部
let isReaded = ref<boolean>(false);
// 是否勾选同意
const isChecked = ref<boolean>(false);

// 滚动事件处理逻辑
const handleScroll = () => {
if (scrollContainer.value) {
// 判断是否滚动到底部
1const height = scrollContainer.value.scrollHeight - scrollContainer.value.scrollTop;
const isAtBottom = height <= scrollContainer.value.clientHeight + 20;
if (isAtBottom && isChecked.value) {
// 表示已经阅读完了并且勾选了
isReaded.value = true;
}
}
};


【1】// 滑动框的总高度 scrollContainer.value.scrollHeight = 974


// scrollContainer.value.scrollTop = 滑动条距离顶部的距离


// 滑动框的可见高度 scrollContainer.value.clientHeight = 340


// 当scrollHeight-scrollTop 达到340时,即滚动到底部了


// 在上述基础上增加一个区域20,即360,防止不同设备的滚动条滚动高度不一致



2. 勾选原因才能执行下一步


image.png

该部分主要是勾选了“other"才会弹出文本框,并且后端传的数据是数组,因此需要对其进行处理。


<div v-for="option in options" :key="option.id">
<input
type="radio"
:id="option.id"
:value="option.id"
name="group"
v-model="selectedOption"
/>

<label :for="option.id">{{ option.label }}</label>
</div>
// 只有勾选了btn4才会展示
<textarea
v-if="selectedOption === 'btn4'"
v-model="textareaValue"
placeholder="If you do have any comments or suggestions please fill in here"
>
</textarea>

// 如果勾选了按钮,或者选择勾选了btn4并且输入了值
<button
v-if="(selectedOption && selectedOption !== 'btn4') || textareaValue"
@submit="gotoPage"
/>

<button v-else disabled />

const textareaValue = ref("");
const selectedOption = ref(null);
// 初始化原因列表
let options = ref([
{ id: "btn1", label: "1" },
{ id: "btn2", label: "2" },
{ id: "btn3", label: "3" },
{ id: "btn4", label: "4" },
]);

// 后端传的数据为["原因1","原因2","原因3","原因4"],需要进行处理
options.value.forEach((option, index) => {
option.label = resp.reason[index];
});

const gotoPage = () => {
// 把数组对象变为纯数组,并且由于other的值为文本输入值,需要进行判断
const reasonList = options.value.map((item) => {
if (selectedOption.value === item.id) {
if (selectedOption.value === "btn4") {
return textareaValue.value;
} else {
return item.label;
}
}
// 如果没有匹配的项,返回 undefined
});
// 最后数值为[undefined, "don't like this game", undefined, undefined]

// 再从reasonList中[undefined, "don't like this game", undefined, undefined]筛选出来原因即可
let reason = reasonList.filter((item) => item !== undefined)[0];

router.push({
path: "/reconfirm",
query: { reason: reason }
});
};


3. 文本框右下角显示输入值和限制


主要是样式,通过定位来进行布局。


image.png
<div v-if="selectedOption === 'bnt4'" style="position: relative">
<textarea
v-model="textareaValue"
:maxlength="maxCharacters"
>
</textarea>
<div
:class="{
'character-count': true,
'red-text': textareaValue.length === maxCharacters,
}"

>

{{ textareaValue.length }} / {{ maxCharacters }}
</div>
</div>

// 限制最大输入字数
const maxCharacters = 140;
const textareaValue = ref("");

.character-count {
position: absolute;
right: 10px;
bottom: 10px;
color: #888;
font-size: 12px;
}
.red-text {
color: red;
}

4. 输入指定的字才能执行下一步


image.png

主要是对@input的使用,然后进行判断


<div>
<textarea
type="text"
v-model="textareaValue"
:placeholder="reConfirmText"
@input="inputChange"
/>

</div>
<button v-if="isEqual" @submit="gotoPage" />
<button v-else disabled />

const reConfirmText = "I confirm to delete Ninja Must Die account";
const textareaValue = ref("");
const isEqual = ref(false);

const gotoPage = () => {
router.push("/hesitation");
};

const inputChange = () => {
if (textareaValue.value === reConfirmText) {
isEqual.value = true;
} else {
isEqual.value = false;
}
}

5. 登录界面需要使用iframe全屏引入


<iframe src="example.com" class="iframe"></iframe>

.iframe {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
margin: 0;
padding: 0;
overflow: hidden;
z-index: 9999;
}

其他


代码优化(导师指点)



  1. 关于跳转路径的变量,用env传递,不要写死

  2. 常量尽量抽离出来,可以做成枚举的做成枚举

  3. 关于find,map之类的函数,能抽离出来的抽离出来,不要直接用,太抽象了


使用到的git操作(非常规)


1. 从一个仓库的代码放到另一个仓库上



场景:从第一个仓库中拉取代码到本地(比如团队中的模板仓库),但你需要把本地开发的代码(处于第一个仓库)推到第二个仓库中(真正开发仓库)



但你首先得在仓库上加ssh地址,打开powershell粘贴下述命令


ssh-keygen -t rsa -C "xxx@xxx.com"

回车到底


image.png


cat ~/.ssh/id_rsa.pub

复制所有


image.png

打开仓库,找到SSH Keys复制上去点击Add key即可


image.png
image.png
image.png

image.png


接下来就是正式操作了


git remote remove origin
git remote add origin xxx(目标的仓库ssh地址)
git checkout -b 'feature/zyj20231114'(在目标仓库新建一个开发分支)
git push --set-upstream origin feature/zyj20231114
git add
git commit
git push

2. 提交一个空白内容的提交



场景:由于是新项目,创建完主分支后,后端才会其打镜像,但需要前端再提交一次来触发dockek里镜像更新的脚本。(应该是这样,我个臭前端怎么可能太清楚后端弄镜像的啊,)



git commit --allow-empty -m “Message”

作者:吃腻的奶油
来源:juejin.cn/post/7311368716804603944
收起阅读 »

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

推送数据?也许你不需要 WebSocket

web
提到推送数据,大家可能会首先想到 WebSocket。 确实,WebSocket 能双向通信,自然也能做服务器到浏览器的消息推送。 但如果只是单向推送消息的话,HTTP 就有这种功能,它就是 Server Send Event。 WebSocket 的通信过程...
继续阅读 »

提到推送数据,大家可能会首先想到 WebSocket。


确实,WebSocket 能双向通信,自然也能做服务器到浏览器的消息推送。


但如果只是单向推送消息的话,HTTP 就有这种功能,它就是 Server Send Event。


WebSocket 的通信过程是这样的:



首先通过 http 切换协议,服务端返回 101 的状态码后,就代表协议切换成功。


之后就是 WebSocket 格式数据的通信了,一方可以随时向另一方推送消息。


而 HTTP 的 Server Send Event 是这样的:



服务端返回的 Content-Type 是 text/event-stream,这是一个流,可以多次返回内容。


Sever Send Event 就是通过这种消息来随时推送数据。


可能你是第一次听说 SSE,但你肯定用过基于它的应用。


比如你用的 CICD 平台,它的日志是实时打印的。


那它是如何实时传输构建日志的呢?


明显需要一段一段的传输,这种一般就是用 SSE 来推送数据。


再比如说 ChatGPT,它回答一个问题不是一次性给你全部的,而是一部分一部分的加载回答。


这也是基于 SSE。




知道了什么是 SSE 以及它的应用,我们来自己实现一下吧:


创建 nest 项目:


npx nest new sse-test


把它跑起来:


npm run start:dev


访问 http://localhost:3000 可以看到 hello world,代表服务器跑成功了:



然后在 AppController 添加一个 stream 接口:



这里不是通过 @Get、@Post 等装饰器标识,而是通过 @Sse 标识这是一个 event stream 类型的接口。


@Sse('stream')
stream() {
return new Observable((observer) => {
observer.next({ data: { msg: 'aaa'} });

setTimeout(() => {
observer.next({ data: { msg: 'bbb'} });
}, 2000);

setTimeout(() => {
observer.next({ data: { msg: 'ccc'} });
}, 5000);
});
}

返回的是一个 Observable 对象,然后内部用 observer.next 返回消息。


可以返回任意的 json 数据。


我们先返回了一个 aaa、过了 2s 返回了 bbb,过了 5s 返回了 ccc。


然后写个前端页面:


创建一个 react 项目:


npx create-react-app --template=typescript sse-test-frontend


在 App.tsx 里写如下代码:


import { useEffect } from 'react';

function App() {

useEffect(() => {
const eventSource = new EventSource('http://localhost:3000/stream');
eventSource.onmessage = ({ data }) => {
console.log('New message', JSON.parse(data));
};
}, []);

return (
<div>hello</div>
);
}

export default App;

这个 EventSource 是浏览器原生 api,就是用来获取 sse 接口的响应的,它会把每次消息传入 onmessage 的回调函数。


我们在 nest 服务开启跨域支持:



然后把 react 项目 index.tsx 里这几行代码删掉,它会导致额外的渲染:



执行 npm run start


因为 3000 端口被占用了,它会跑在 3001:



浏览器访问下:



看到一段段的响应了没?


这就是 Server Send Event。


在 devtools 里可以看到,响应的 Content-Type 是 text/event-stream:



然后在 EventStream 里可以看到每一次收到的消息:



这样,服务端就可以随时向网页推送消息了。


那它兼容性怎么样呢?


可以在 MDN 看到:



除了 ie、edge 外,其他浏览器都没任何兼容问题。


基本是可以放心用的。


那用在哪呢?


一些只需要服务端推送的场景就特别适合 Server Send Event。


比如这个站内信:



这种推送用 WebSocket 就没必要了,可以用 SSE 来做。


那连接断了怎么办呢?


不用担心,浏览器会自动重连。


这点和 WebSocket 不同,WebSocket 如果断开之后是需要手动重连的,而 SSE 不用。


再比如说日志的实时推送。


我们来测试下:


tail -f 命令可以实时看到文件的最新内容:



我们通过 child_process 模块的 exec 来执行这个命令,然后监听它的 stdout 输出:


const { exec } = require("child_process");

const childProcess = exec('tail -f ./log');

childProcess.stdout.on('data', (msg) => {
console.log(msg);
});

用 node 执行它:



然后添加一个 sse 的接口:


@Sse('stream2')
stream2() {
const childProcess = exec('tail -f ./log');

return new Observable((observer) => {
childProcess.stdout.on('data', (msg) => {
observer.next({ data: { msg: msg.toString() }});
})
});

监听到新的数据之后,把它返回给浏览器。


浏览器连接这个新接口:



测试下:



可以看到,浏览器收到了实时的日志。


很多构建日志都是通过 SSE 的方式实时推送的。


日志之类的只是文本,那如果是二进制数据呢?


二进制数据在 node 里是通过 Buffer 存储的。


const { readFileSync } = require("fs");

const buffer = readFileSync('./package.json');

console.log(buffer);


而 Buffer 有个 toJSON 方法:



这样不就可以通过 sse 的接口返回了么?


试一下:


@Sse('stream3')
stream3() {
return new Observable((observer) => {
const json = readFileSync('./package.json').toJSON();
observer.next({ data: { msg: json }});
});
}



确实可以。


也就是说,基于 sse,除了可以推送文本外,还可以推送任意二进制数据。


总结


服务端实时推送数据,除了用 WebSocket 外,还可以用 HTTP 的 Server Send Event。


只要 http 返回 Content-Type 为 text/event-stream 的 header,就可以通过 stream 的方式多次返回消息了。


它传输的是 json 格式的内容,可以用来传输文本或者二进制内容。


我们通过 Nest 实现了 sse 的接口,用 @Sse 装饰器标识方法,然后返回 Observe 对象就可以了。内部可以通过 observer.next 随时返回数据。


前端使用 EventSource 的 onmessage 来接收消息。


这个 api 的兼容性很好,除了 ie 外可以放心的用。


它的应用场景有很多,比如站内信、构建日志实时展示、chatgpt 的消息返回等。


再遇到需要消息推送的场景,不要直接 WebSocket 了,也许 Server Send Event 更合适呢?


作者:zxg_神说要有光
来源:juejin.cn/post/7272564663116759074
收起阅读 »

只会Vue的我,用两天学会了react,这个方法您也可以

web
背景 由于工作需要学习react框架;最开始看文档的时候感觉还挺难的。但当我看了半天文档以后才发现,原来react这样学才是最快的;前提是同学们会vue一类的框架哈。 该方法适用于会vue的同学们食用 我们在学习以前先去想一想,在vue中我们常用的方法是什么,...
继续阅读 »

背景


由于工作需要学习react框架;最开始看文档的时候感觉还挺难的。但当我看了半天文档以后才发现,原来react这样学才是最快的;前提是同学们会vue一类的框架哈。


该方法适用于会vue的同学们食用


我们在学习以前先去想一想,在vue中我们常用的方法是什么,我们遇到一些场景时在vue中是怎么做的。


当我们想到这儿的时候就会发现,对啊;既然vue是这样做的,那么react中是怎么做的呢?别急,我们一步一步对比着来。


这样岂不是更能理解哦!下面就让我们开始吧!


冲冲冲。。。


Vue梳理


在开始之前,我们先来梳理一下我们在vue中常用的API或者场景有哪些。


以下这几种就是我们常见的一些功能,主要是列表渲染、表单输入和一些计算属性等等;我们只需要根据原有的需要的功能去学习即可。



  • 组件传值

  • 获取DOM

  • 列表渲染

  • 条件渲染

  • class

  • 计算属性

  • 监听器

  • 表单输入

  • 模板


vue/react对比学习


组件传值


vue


// 父组件
<GoodsList v-if="!isGoodsIdShow" :goodsList="goodsList"/>
// 子组件 -- 通过props获取即可
props: {
goodsList:{
type:Array,
default:function(){
return []
}
}
}

react


// 父组件
export default function tab(props:any) {
const [serverUrl, setServerUrl] = useState<string | undefined>('https://');
console.log(props);
// 父组件接收子组件的值并修改
const changeMsg = (msg?:string) => {
setServerUrl(msg);
};

return(
<View className='tab'>
<View className='box'>
<TabName msg={serverUrl} changeMsg={changeMsg} />
</View>
</View>

)
}

// 子组件
function TabName(props){
console.log('props',props);
// 子传父
const handleClick = (msg:string) => {
props.changeMsg(msg);
};
return (
<View>
<Text>{props.msg}</Text>
<Button onClick={()=>{handleClick('77777')}}>测试</Button>
</View>

);
};

获取DOM


vue


this.$refs['ref']

react


// 声明ref    
const domRef = useRef<HTMLInputElement>(null);
// 通过点击事件选择input框
const handleBtnClick = ()=> {
domRef.current?.focus();
console.log(domRef,'domRef')
}

return(
<View className='home'>
<View className='box'>
<Input ref={domRef} type="text" />
<button onClick={handleBtnClick}>增加</button>
</View>
</View>

)

列表渲染


vue


<div v-for="(item, index) in mealList" :key="index">
{{item}}
</div>

react


//声明对象类型
type Coordinates = {
name:string,
age:number
};
// 对象
let [userState, setUserState] = useState<Coordinates>({ name: 'John', age: 30 });
// 数组
let [list, setList] = useState<Coordinates[]>([{ name: '李四', age: 30 }]);

// 如果你的 => 后面跟了一对花括号 { ,那你必须使用 return 来指定返回值!
const listItem = list.map((oi)=>{
return <View key={oi.age}>{oi.name}</View>
});

return (
{
list.map((oi)=>{
return <Text className='main-list-title' key={oi.age}>{oi.name}</Text>
})
}
<View>{ listItem }</View>
</View>
)

条件渲染


计算属性


vue


computed: {
userinfo() {
return this.$store.state.userinfo;
},
},

react


const [serverUrl, setServerUrl] = useState('https://localhost:1234');
let [age, setAge] = useState(2);

const name = useMemo(() => {
return serverUrl + " " + age;
}, [serverUrl]);
console.log(name) // https://localhost:1234 2

监听器


vue


watch: {
// 保证自定义菜单始终显示在页面中
customContextmenuTop(top) {
...相关操作
}
},

react


import { useEffect, useState } from 'react';

export default function home() {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
const [age, setAge] = useState(2);

/**
* useEffect第二个参数中所传递的值才会进行根据值的变化而出发;
* 如果没有穿值的话,就不会监听数据变化
*/

useEffect(()=>{
if (age !== 5) {
setAge(++age)
}
},[age])

useEffect(()=>{
if(serverUrl !== 'w3c') {
setServerUrl('w3c');
}
},[serverUrl])

return(78)
}

总结


从上面的方法示例我们可以得出一个结论:在其他框架(自己会的)中常用到的方法或者场景进行针对性的学习即可。


这样的好处是你能快速的上手开发,然后在实际开发场景中遇到解决不了的问题再去查文档或者百度。


这只是我的一点小小的发现,哈哈哈。。。


如果对你有感触的话,可以尝试一下这个方法;我觉得还是很不错的


注意:react推荐函数式组件开发,不推荐类组件开发,我在上面没有说明,大家也可以去文档看看,类组件和函数组件还是有很大差别的,如:函数组件没有生命周期,一般使用监听来完成的,监听的使用方法还是有所不同,大家可以具体的去试试,我在这儿也是告诉大家一些方法;具体去学了才是你的。


为了方便自己学习记录,以及给大家提供思路,我下期给大家带来 vite + ts + react的搭建


作者:雾恋
来源:juejin.cn/post/7268844150233219107
收起阅读 »

看不了电视直播了?那就自己做一个(一)

web
事情的起因是这样的,前两天打开电视看直播,突然变成了下面这个画面。 开始以为是太久没更新了,想着重新安装一下就好了。结果上网一查才知道,电视直播软件都下架了。 电视直播只能安装有线,或者通过央视频手机收看了。有线暂时安装不了,用手机看又总是感觉很别扭。 虽...
继续阅读 »

事情的起因是这样的,前两天打开电视看直播,突然变成了下面这个画面。


j29zRYWy.jpeg


开始以为是太久没更新了,想着重新安装一下就好了。结果上网一查才知道,电视直播软件都下架了。


电视直播只能安装有线,或者通过央视频手机收看了。有线暂时安装不了,用手机看又总是感觉很别扭。


a71ea8d3fd1f4134205611b2199935ccd0c85e21.png


虽然也可以通过投屏的方式用电视播放,但切换频道时还得使用手机操作,非常的麻烦。


广电的这波操作出发点可能是好的,后续应该会提供其他收看方式,但是目前这个真空期着实有点尴尬。思来想去,干脆自己动手做一个吧


就是干.jpg


经过一个周末的折腾,过程虽然有一点点曲折,但总算是完成了第一个电视版本,感觉和以前相比,清晰度还不错,切换也更流畅。


111.gif


再来看一下卫视。


也是OK的


22 (1).gif


电视端有了,又突然想着把它放到手机上,虽然手机上可以直接使用央视频播放,但还是有点繁琐,于是又稍微做了一些调整,推出了一个手机端的版本,切换还是相当的丝滑。


手机.gif


最后我其实还改了一版在电脑上使用的,但是这个除了摸鱼好像也没别的用处,所以对于我来说意义不大。


实现篇


接下来说一下具体的实现,会涉及到一些编程相关的内容,如果不感兴趣可以直接跳到结尾。


客户端应用开发


这一篇先介绍客户端的应用的开发,主要就是安卓应用的开发。虽然以前没有这方面经验,但是想法有了,剩下的交给chatGpt就好了。


1. 播放器


首先是播放器的选择,一开始我采用了原生MediaPlayer,主要是考虑到跟各版本安卓系统的兼容性会好一点,而且它使用起来非常的简单,十几行代码就搞定了。


public class PlayActivity extends Activity {
ChannelService channelService;
VideoView videoView;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
channelService = new HttpChannelService();
setContentView(R.layout.activity_play);
videoView = findViewById(R.id.video_view);
channelService.loadChannels((success, message) -> runOnUiThread(() -> {
Channel channel = channelService.getDefaultChannel();
videoView.setVideoURI(Uri.parse(channel.getSource()));
videoView.start();
}));
}
}

后来替换成了谷歌开源的ExoPlayer,因为只是简单使用,所以代码基本上也没什么差别。


public class PlayActivity extends Activity {
ChannelService channelService;
private StyledPlayerView videoView;
private ExoPlayer player;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView();
initChannels();
}

private void initView() {
setContentView(R.layout.activity_play);
videoView = findViewById(R.id.video_view);
player = new ExoPlayer.Builder(this).build();
videoView.setPlayer(player);
}

private void initChannels() {
channelService = new HttpChannelService();
channelService.loadChannels((boolean success, String message) -> runOnUiThread(() -> {
ChannelService.Channel channel = channelService.getDefaultChannel();
play(channel);
}));
}

private void play(Channel channel){
player.setMediaItem(MediaItem.fromUri(channel.getSource()));
player.prepare();
player.setPlayWhenReady(true);
}
}

2. 监听器


接下来就是考虑对遥控器按键的监听处理,对于这个应用而言,只需要监控方向键以及退出键就好了,当然也可以根据需要对菜单键或者确定键进行响应。


videoView.setOnKeyListener((view, keyCode, event) -> {
switch (keyCode) {
// 向下操作处理 切换下一个频道
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.getAction() == KeyEvent.ACTION_DOWN) {
Channel channel = channelService.getNextChannel();
play(channel);
return true;
}
break;
}
return false;
});

3. 视频源管理


把ChannelService留到最后讲,是因为它的作用通过下面的接口定义就一目了然了。


这里之所以要定义成接口,比如常见的通过m3u获取,是因为考虑到视频源可能有不同实现,关于实现部分会在下一篇详细讲解。


public interface ChannelService {

/**
* 加载频道
*/

void loadChannels(LoadCallBack callBack);

/**
* 获取默认频道
*/

Channel getDefaultChannel();

/**
* 获取下一个频道
*/

Channel getNextChannel();

/**
* 获取前一个频道
*/

Channel getPrevChannel();

}

4. 手机版


手机版因为没有了遥控器,所以需要对触屏动作进行监听来对视频进行操控,主要就是左右滑动的切换,以及上下滑动的音量调节等。


结语


因为是即兴的创作,也没有打算能长久使用,所以很多细节我并没有考虑,比如内容的缓存,节目回看,网络监控等等这些。但是这几天使用下来体验还是挺不错的。后续我可以把整个源码开放出来,大家有兴趣可以自行去补充。


到这里客户端的实现就讲完了,下一篇再讲一下其他部分的实现。


作者:双子小匠
来源:juejin.cn/post/7311961893610995748
收起阅读 »

带圆角的虚线边框?CSS 不在话下

今天,我们来看这么一个非常常见的切图场景,我们需要一个带圆角的虚线边框,像是这样: 这个我们使用 CSS 还是可以轻松解决的,代码也很简单,核心代码: div { border-radius: 25px; border: 2px dashed...
继续阅读 »

今天,我们来看这么一个非常常见的切图场景,我们需要一个带圆角的虚线边框,像是这样:



这个我们使用 CSS 还是可以轻松解决的,代码也很简单,核心代码:


div {
border-radius: 25px;
border: 2px dashed #aaa;
}

但是,原生的 dashed 有一个问题,就是我们无法控制虚线的单段长度与间隙


假设,我们要这么一个效果呢虚线效果呢:



此时,由于无法控制 border: 2px dashed #aaa 产生的虚线的单段长度与线段之间的间隙,border 方案就不再适用了。


那么,在 CSS 中,我们还有其它方式能够实现带圆角,且虚线的单段长度与线段之间间隙可控的方式吗?


本文,我们就一起探讨探讨。


实现不带圆角的虚线效果


上面的场景,使用 CSS 实现起来比较麻烦的地方在于,图形有一个 border-radius


如果不带圆角,我们可以使用渐变,很容易的模拟虚线效果。


我们可以使用线性渐变,轻松的模拟虚线的效果:


div {
width: 150px;
height: 100px;
background: linear-gradient(90deg, #333 50%, transparent 0) repeat-x;
background-size: 4px 1px;
background-position: 0 0;
}

看看,使用渐变模拟的虚线如下:



解释一下上面的代码:



  1. linear-gradient(90deg, #333 50%, transparent 0),实现一段渐变内容,100% - 50% 的内容是 #333 颜色,剩下的一半 50% - 0 的颜色是透明色 transprent

  2. repeat-x 表示只在 x 方向重复

  3. background-size: 4px 1px 表示上述渐变内容的长宽分别是 4px\ 1px,这样配合 repeat-x就能实现只有 X 方向的重复

  4. 最后的 background-position: 0 0 控制渐变的定位


因此,我们只需要修改 background 的参数,就可以得到各种不一样的虚线效果:



完整的代码,你可以戳这里:CodePen Demo -- Linear-gradient Dashed Effect


并且,渐变是支持多重渐变的,因此,我们把容器的 4 个边都用渐变表示即可:


div {
background:
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y;
background-size: 4px 1px, 4px 1px, 1px 4px, 1px 4px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
}

效果如下:



但是,如果要求的元素带 border-radius 圆角,这个方法就不好使了,整个效果就会穿帮。


因此,在有圆角的情况下,我们就需要另辟蹊径。


利用渐变实现带圆角的虚线效果


当然,本质上我们还是需要借助渐变效果,只是,我们需要转换一下思路。


譬如,我们可以使用角向渐变。


假设,我们有这么一个带圆角的元素:


<div>div>

div {
width: 300px;
height: 200px;
background: #eee;
border-radius: 20px;
}

效果如下:



如果我们修改内部的 background: #eee,把它替换成重复角向渐变的这么一个图形:


div {
//...
- background: #eee;
+ background: repeating-conic-gradient(#000, #000 3deg, transparent 3deg, transparent 6deg);
}

解释一下,这段代码创建了一个重复的角向渐变背景,从黑色(#000)开始,每 3deg 变为透明,然后再从透明到黑色,以此循环重复。


此时,这样的背景效果可用于创建一种渐变黑色到透明的重复纹理效果:



在这个基础上,我们只需要给这个图形上层,再利用伪元素,叠加一层颜色,就得到了我们想要的边框效果,并且,边框间隙和大小可以简单调整。


完整的代码:


div {
position: relative;
width: 300px;
height: 200px;
border-radius: 20px;
background: repeating-conic-gradient(#000, #000 3deg, transparent 3deg, transparent 6deg);

&::before {
content: "";
position: absolute;
inset: 1px;
background: #eee;
border-radius: 20px;
}
}

效果如下:



乍一看,效果还不错。但是如果仔细观察,会发现有一个致命问题:虚线线段的每一截长度不一致


只有当图形的高宽一致时,线段长度才会一致。高宽比越远离 1,差异则越大:



完整的代码,你可以戳这里:CodePen Demo -- BorderRadius Dashed Border


那有没有办法让虚线长度能够保持一样呢?


可以!我们再换一种渐变,我们改造一下底下的角向渐变,重新利用重复线性渐变:


div {
border-radius: 20px;
background:
repeating-linear-gradient(
-45deg,
#000 0,
#000 7px,
transparent 7px,
transparent 10px
);
}

此时,我们能得到这样一个斜 45° 的重复线性渐变图形:



与上面方法一类似,再通过在这个图形的基础上,在元素中心,叠加多一层纯色遮罩图形,只漏出最外围一圈的图形,带圆角的虚线边框就实现了:



此方法比上面第一种渐变方法更好之处在于,虚线每一条线段的长度是固定的!是不是非常的巧妙?


完整的代码,你可以戳这里:CodePen Demo -- BorderRadius Dashed Border


最佳解决方案:SVG


当然,上面使用 CSS 实现带圆角的虚线边框,还是需要一定的 CSS 功底。


并且,不管是哪个方法,都存在一定的瑕疵。譬如如果希望边框中间不是背景色,而是镂空的,上述两种 CSS 方式都将不再使用。


因此,对于带圆角的虚线边框场景,最佳方式一定是 SVG。(切图也算是吧,但是灵活度太低)


只是很多人看到 SVG 会天然的感到抗拒,或者认为 SVG 不太好掌握。


所以,本文再介绍一个非常有用的开源工具 -- Customize your CSS Border



通过这个开源工具,我们可以快速生成我们想要的虚线边框效果,并且一键复制可以嵌入到 CSS background 中的 SVG 代码图片格式。


图形的大小、边框的粗细、虚线的线宽与间距,圆角大小统统是可以可视化调整的。


通过一个动图,简单感受一下:



总结一下


本文介绍了 2 种在 CSS 中,不借助切图和 SVG 实现带圆角的虚线边框的方式:



  1. 重复角向渐变叠加遮罩层

  2. 重复线性渐变叠加遮罩层


当然,两种 CSS 方式都存在一定瑕疵,但是对于一些简单场景是能够 Cover 住的。


最后,介绍了借助 SVG 工具 Customize your CSS Border 快速生成带圆角的虚线边框的方式。将 SVG 生成的矢量图像数据直接嵌入到 background URL 中,能够应付几乎所有场景,相对而言是更好的选择。


最后


好了,本文到此结束,希望本文对你有所帮助 :)


作者:Chokcoco
来源:juejin.cn/post/7311681326712487999
收起阅读 »

前端打包后,静态文件的名字为什么是一串Hash值?

web
引言 前段时间公司需要招聘几个初级前端,面试过程中,问了这么一个问题:“项目打包后的dist文件夹中,比如js、css这些文件的名称为什么是hash值的,就是一串无规则的字符串”。基本上都不知道静态文件为什么需要这种无规则的hash值,今天就稍微说一下哈。 静...
继续阅读 »

引言


前段时间公司需要招聘几个初级前端,面试过程中,问了这么一个问题:“项目打包后的dist文件夹中,比如js、css这些文件的名称为什么是hash值的,就是一串无规则的字符串”。基本上都不知道静态文件为什么需要这种无规则的hash值,今天就稍微说一下哈。


静态文件何时被加载


拿常用的单页面应用举例,当我们访问一个网站的时候,最终会指向 index.html 这个文件,也就是打包后的 dist 文件夹中的 index.html


比如说 https://some-domain.com/home, 并点击回车键,我们的服务器中实际上没有这个路由,但我们不是返回 404,而是返回我们的 index.html。为什么地址中我们没有输入index.html这个路径,但还是指向到 index.html文件并加载它?因为现在大多数都用nginx去部署,一般在url中输入地址的时候末尾都会加个 “/” ,nginx中已经把“/”重定向到 index.html文件了


image.png


此时按下回车,这个 index.html 文件就被加载获取到了,然后开始自上而下的去加载里面的引用和代码,比如在html中引入的css、js、图片文件。


浏览器默认缓存


当用户按下回车键就向目标服务器去请求index.html文件,加载解析index.html文件的同时就会连带着加载里面的js、css文件。有没有想过,用户第一次已经从服务器请求下载静态文件到客户端了,第二次去浏览该网站该不会还让我去向服务器请求吧,不会吧不会吧,如果每次都请求下载,那用户的体验多不好,每次请求都需要时间,不说服务器的压力会增加,最重要的是用户的体验感,展现到用户眼前的时间会增加!


所以说浏览器已经想到这个了,当请求静态资源的时候,就会默认缓存请求过来的静态文件,这种浏览器自带的默认缓存就叫做 启发式缓存 。 除非手动设置不需要缓存no-store,此时请求静态文件的时候文件才不会缓存到本地!


浏览器默认缓存详情可见 MDN 启发式缓存。不管什么缓存都有缓存的时效性吧,如果想知道 启发式缓存 到底把静态文件缓存了多久,可以阅读笔者的这篇文章 浏览器的启发式缓存到底缓存了多久?


vue-cli里的默认配置,css和js的名字都加了哈希值,所以新版本css、js和就旧版本的名字是不同的,不会有缓存问题。


Hash值的作用


那既然知道了浏览器会有默认的缓存,当加载静态资源的时候会命中启发式缓存并缓存到本地。那如果我们重新部署前端包的时候,如何去请求新的静态资源呢,而不是缓存的静态资源?这时候就得用到hash值了


下面模拟了掘金网站的静态资源获取,当请求静态资源的时候,实际访问的是服务器中静态资源存放的位置
image.png


返回即是当前请求静态资源的具体内容
image.png


第一次进来的时候会去请求服务器的资源缓存到本地,比如 0dbcf63.js 这个文件就被缓存到本地了,后面再正常刷新就直接获取的是缓存中的资源了(disk cache 内存缓存)。


如果前端包重新部署后,试想一下如果 0dbcf63.js这个js文件不叫这个无规则的名字,而是固定的名字,那浏览器怎么知道去请求缓存中的还是服务器中的,所以浏览器的机制就是请求缓存中的,除非缓存中的过期了,才会去请求服务器中的静态资源。如果没有请求到最新的资源,页面也不会更新到最新的内容,影响用户的体验。


那浏览器这个机制是怎么判断的,那就是根据资源的名称,该资源的资源名称如果没变 并且 没有设置不缓存 并且 资源没过期,那就会请求缓存中的资源,否则就会请求服务器中的资源


image.png



当静态资源的名称发生变化的时候,请求路径就会发生变化,所以就会重新命中服务器新部署的静态资源!这就是为什么需要hash值的原因,为了区分之前部署的文件和这次部署文件的区别,好让浏览器去重新获取最新的资源。



第三方库


由于像 lodash 或 react 这样的第三方库很少像本地源代码一样频繁修改,因此通常推荐将第三方库提取到单独的 chunk-vendor 中


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

module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGr0ups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};

这样依赖的静态文件就会打包到chunk-vendor中,并且多次打包不会改变文件的hash值,以上是webpack原生的配置,如果使用的vue脚手架,那么脚手架已经都配置好了。


image.png


作者:Lakeiedward
来源:juejin.cn/post/7311219067199881254
收起阅读 »

三行代码实现完美瀑布流

web
需求 最近准备做一个瀑布流的需求,这里每个卡片的高度是不固定的,有点类似下图这样的。 难点 如果绝对定位,如何定位每个卡片的位置。 因为可以发现需求里边的每个卡片的高度都是不固定的,所以如果想使用绝对定位的话,需要实时动态的计算每个卡片的left、top,...
继续阅读 »

需求


最近准备做一个瀑布流的需求,这里每个卡片的高度是不固定的,有点类似下图这样的。



难点



  1. 如果绝对定位,如何定位每个卡片的位置。
    因为可以发现需求里边的每个卡片的高度都是不固定的,所以如果想使用绝对定位的话,需要实时动态的计算每个卡片的left、top,这里会涉及大量的计算。


因为每个卡片的高度是不固定,所以如果想要计算left、top,必须首先获取卡片的高度,但是卡片里边不仅包含图片,还有文字,这个时候计算高度是比较困难的。




  1. 如何结合虚拟列表实现瀑布流


这个时候必须要根据scrollTop的位置,判断什么时候需要加载哪些数据,判断可视区域里边数据的起始索引以及结束索引,这里同样会涉及大量的计算,同时还因为每个卡片的高度不固定,甚至只有图片和文字加载到浏览器以后,才能得到真实的高度,这样会更困难。


解决方案


解决方案1



  1. 如果绝对定位,如何定位每个卡片的位置。


1.1 后端计算
后端可以先把每个图片的高度和宽度提前计算好,直接返回给前端进行处理,然后前端根据后端返回的图片高度和宽度,然后再动态的计算出每个卡片的高度(文字部分也可以固定高度,使用省略号实现)。


1.2 前端计算


前端计算还是比较麻烦的,需要先等卡片组件加载完成,才能得到宽度和高度,而且因为数据量比较大,每个卡片计算出来以后,还需要去根据计算出来的结果去更新left、top,会非常麻烦。
这里可以采用node作为中间层进行计算,还是使用类似后端计算的思路。


还有一种方法是使用observe api 动态观察每个卡片,当观察到卡片加载完成后,再动态根据卡片的宽度和高度计算,不过这样同样很麻烦。



  1. 如何结合虚拟列表实现瀑布流


这里因为卡片的高度是不固定的,同时也是瀑布流,所以不能使用react-window 来解决,不过可以使用react-window的类似思路,自己封装一个npm 包,根据scroll事件判断需要加载那些数据。


解决方案2


使用css3的columns来实现,该技术解决方案不需要计算高度,也不需要去定位,但是columns这个属性会把卡片高度给切开
如下图:



不过可以使用下面代码来解决,


js
复制代码

.test {
// color: red;
// height: 2000px;
background-color: red;
gap: 1rem;
columns: 5;
.no-break {
break-inside: avoid;
}
}

效果如下:



作者:邹小邹
链接:https://juejin.cn/post/7273890921506553890
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

被中文输入法坑死了

web
PM:在PC端做一个@功能吧,就是那种...。 我:你不用解释🤔我知道那个功能,监听keydown事件,然后e.keycode === 50,那可太简单了。 那可太简单了,可太简单了,太简单了,简单了,单了,了......(掉进坑里的回声) 坑1:KeyB...
继续阅读 »

PM:在PC端做一个@功能吧,就是那种...。



我:你不用解释🤔我知道那个功能,监听keydown事件,然后e.keycode === 50,那可太简单了。



那可太简单了,可太简单了,太简单了,简单了,单了,了......(掉进坑里的回声)


坑1:KeyBoardEvent.keycode



废弃的属性你就坚持用吧,一用一个不吱声。以后线上跑得好好的代码突然报错了,你都不知道bug在哪儿。


现在的web标准里,要确定一个键盘事件是靠e.keye.codecode代表触发事件的物理按键,比如2的位置code='Digit2'key返回用户按下的物理按键的值,它还与 shiftKey 等调节性按键的状态、键盘的区域和布局有关。


所以对于@来说,直接判断e.key === "@"来做后续的操作就行了。


addEventListner('keydown', (e) => {
if (e.key === "@") {
e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符的,可监听输入的元素
// 唤起小窗....
}
});


仔细看上面的这几行代码和注释,要开始考(坑)了。


坑2:输入法的坑


起因


在我美滋滋地以为避过了坑1就没事了的时候,一个夜晚我的测试同学告诉我,在测试环境突然就体验不到这个功能了,无论输入多少个@都不行,白天还好好的🤯。


好一个「白天还好好的」。


我自己测试的时候又百分百能体验到🤔,所以最开始我还在怀疑他没有配上测试系统......


于是,让测试同学的windows电脑连到我的开发环境debug一看:


好家伙,真是好家伙😅他的电脑的e.key === "Process"????!!!


什么意思呢,就是正常我们理想中的@字符产生是shift+2按键的组合,监听keydown之后我们会按顺序收到两个回调:



  1. e.key === "Shift"e.code === "ShiftLeft"或者shiftRight

  2. e.key === "@"e.code === "Digit2"


但是实际在测试同学的电脑里,1是一样的,但是2变了,2变成了e.key === "Process"


虽然键盘事件有变化,但是在前端页面上的@字符是没有任何变化的。难怪他说他会突然失效了。我问他做了什么怎么会突然变了,他想了想说晚上从系统输入法换成了微信输入法.....


上网检索(chatGPT)了一番,明白了一个新的知识点:


输入法的全称叫Input Method Editor输入法编辑器(IME)。本质上它也是个编辑器。为了能输入各类字符(比如汉字和阿拉伯字等),IME会先处理用户的英文字母输入然后通过系统的底层调用传递给浏览器,浏览器再显示到用户的界面。这里的Process很大概率就是当时输入法给出的某个值表示那个时刻它还在处理中。


解决办法


既然KeyBoardEvent靠不住,那我们换一种监听方式。


我找到了一个非常适用于输入法的监听事件叫做CompositionEvent,它表示用户间接输入文本(如使用输入法)时发生的事件。此接口的常用事件有compositionstart, compositionupdatecompositionend。它们三个事件分别对应的动作,通俗一点说就是你用输入法开始打字、正在打字和结束打字。


于是乎,我监听compositionend不就行了!在输入法end的时候我再去看你end的字符是不是@不就行了!


// addEventListner('keydown', (e) => {
addEventListner('compositionend', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

对于输入法来说,按键的up和down的key值就算不尽人意也没什么损失,毕竟用户毫无感知。但是,compositionend永远是不会错的,如果compositionende.data都不是@字符了,那么在用户的编辑器界面上的显示肯定也会跟着出错。


所以监听这个肯定就是万无一失的方法了,哈哈哈我真是个“天才”(蠢材)。
修改之后让测试同学尝试之后果然就可以了。


坑3:输入法继续坑


起因


时间过去了没一会,本天才就收到了另一个测试同学反馈的问题说为什么输入了一个@字符之后,会出现两个@在界面上?


我第一反应就是难道没有执行到e.preventDefalut()?既然后续功能能正常使用,没执行到也不应该啊🤔。然后在我电脑一通尝试,发现safari浏览器在输入法为中文的情况下也会触发这个问题。


于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开二度):


执行到了,也没有报错什么的,但是@字符并没有被prevent掉🤯。


再加上我自己传入的@,所以界面上就出现了两个@字符。啊这这这,这很难评......


ceeb653ely8gzozgvjq1cg20j20hhgvb.gif


我是左思右想,百思不得其解,于是只能:



stack overflow上也有这个问题


上面大概的意思就是compositionend事件里使用 e.preventDefault() 在技术上可行的,但它可能不会产生你期望的效果。可能是因为 compositionend 事件标志着一次输入构成(composition)会话的结束,而在这个点上阻止默认行为可能没有意义,因为输入的流程已经完成了。


更推荐用keydown,compositionstartinput来处理这种情况。


keydown是不可能keydown了,已经被坑了。compositionstart也不行,因为刚开始输入那会才按下了shift键,@字符还没出来呢。那就只能input了。


解决办法


最开始我没有选择input就是因为它不能使用e.preventDefault()。我必须要对输入的字符串进行单独处理,去掉@,当时觉得很麻烦就没有选择这个方法。


额....好好好,行行行,现在还是必须得处理一下了。


// addEventListner('keydown', (e) => {
// addEventListner('compositionend', (e) => {
addEventListner('input', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
?怎么去处理字符呢 // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

对于这个处理字符的方法,也是一个新知识点了。起初我还想的是去处理编辑器里的content,然后再给它插入回去,这样子复杂度很高并且出错的概率极大。


这里的解决办法主要是使用CharacterData接口。CharacterData 接口是操作那些包含字符数据的节点的核心,特别是在需要动态地更改文本内容时。


例如,在一个文本节点上使用 deleteData() 方法可以从文本中移除一部分内容,而不必完全替换或重写整个节点


const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
}

写完这个之后,我用自己的safari浏览器测试发现果然没有问题了。


哈哈哈我真是个“天才”(蠢材)。


坑4:输入法深坑🕳️


我自信满满地让测试同学再重试一下😎,然后测试同学说:和之前一样啊,还是有两个@字符。


我:啊?啊啊??啊啊啊???


IMG_6547.jpg


于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开三度):


发现测试同学电脑上的anchorOffset和正常的情况下是不一样的,会小一位,所以导致anchorOffset - 1拿到的前一个字符并不等于@,所以后续也没有把它处理掉🤯。


我是左思右想,百思不得其解,stack overflow上也没有相关的问题。不过,结合IME的概念,肯定还是输入法的问题。


结合之前keydown的e.key==="Processing",可能在input触发时输入法的编辑器其实还是没有完成工作(composition),导致在那个时候SelectionanchorOffset不一致。其实浏览器的Selection肯定不会错,那anchorOffset看起来像是错了,我觉得应该是输入法在转换的过程对我们的前端页面做了一些用户看不到的东西,而anchorOffset把它显化出来罢了。


解决办法


于是乎,我尝试性的对处理字符串的那串代码进行延时,目的是为了等待输入法彻底工作完毕。


// addEventListner('keydown', (e) => {
// addEventListner('compositionend', (e) => {
addEventListner('input', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
setTimeout(() => {
const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
} // 这里去掉@字符是为了后续插入和监听方便处理
});

// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

然后,问题真的就彻底解决了。


这个功能做起来可太简单了......😅


作者:Liqiuyue
来源:juejin.cn/post/7307041255740981286
收起阅读 »

使用flex实现瀑布流

web
什么是瀑布流 瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。 特点: 固定宽度,高度不一 参差不齐的布局 使用flex实现瀑布流 实现的效果是分成两...
继续阅读 »

什么是瀑布流


瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。


特点:



  • 固定宽度,高度不一

  • 参差不齐的布局


使用flex实现瀑布流


实现的效果是分成两列的瀑布流,往下滑动会加载下一页的数据,并渲染到页面中!


微信图片_20230731231617.jpg


样式的实现


<view class="blessing-con">
<view class='blessing-con-half'>
<view id="leftHalf">
<view class="blessing-con-half-item" :class="bgColor[index % 3]"
v-for="(item, index) in newBlessingWordsList1" :key="index">
<view class="item-con">
</view>
</view>
</view>
</view>
<view class='blessing-con-half'>
<view id="rightHalf">
<view class="blessing-con-half-item" :class="bgColor[(index + 1) % 3]"
v-for="(item, index) in newBlessingWordsList2" :key="index">
<view class="item-con"></view>
</view>
</view>
</view>
</view>
<view class="blessing-more" @click="handlerMore">
<image v-if="hasWallNext" class="more-icon"
src="xx/blessingGame/arr-down.png">
</image>
<view class="blessing-more-text">{{ blessingStatus }}</view>
</view>

.blessing-con 定义外层容器为flex布局,同时设置主轴对齐方式为space-between


.blessing-con-half定义左右两侧的容器的样式


.blessing-con-half-item定义每一个小盒子的样式


.blessing-con {
padding: 32rpx 20rpx;
display: flex;
justify-content: space-between;
height: 1100rpx;
overflow-y: auto;
.blessing-con-half {
width: 320rpx;
height: 100%;
box-sizing: border-box;
.blessing-con-half-item {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
box-sizing: border-box;
margin: 0 0 24rpx;
position: relative;
}
}
}

这里每个小盒子的背景色按蓝-黄-红的顺序,同时通过伪类给盒子顶部添加锯齿图片,实现锯齿效果


bgColor: ['blueCol', 'yellowCol', 'pinkCol'], //祝福墙背景

// 不同颜色
.blessing-con-half-item {
&.pinkCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/pink-bg.png');
}
.item-con {
background: #FFE7DF;
}
}

&.yellowCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/orange-bg.png');
}
.item-con {
background: #fff0e0;
}
}

&.blueCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/blue-bg.png');
}
.item-con {
background: #e0f7ff;
}
}
}
}

功能实现


在data中定义两个数组存储左右列表的数据


data(){
return{
blessingWordsList: [],// 祝福墙数据
newBlessingWordsList: [],//已添加的数据
newBlessingWordsList1: [],//左列表
newBlessingWordsList2: [],//右列表
isloading:false,//是否正在加载
hasWallNext:false,//是否有下一页
leftHeight: 0,//左高度
rightHeight: 0,//右高度
blessingWordsCount: 0,//计数器
isActive: 0, //tab初始化索引
timer:null,//定时器
}
}

调取接口请求列表数据



  • 第一次请求数据需要初始化列表数据和计数器

  • 每一次请求完都需要开启定时器


// 获取祝福墙列表(type=1则请求下一页)
async getBlessingWall(type = 0) {
try {
let res = await api.blessingWall({
activityId: this.activityId,
pageNum: this.pageWallNum,
pageSize: this.pageWallSize
})
this.isloading = false
if (res.code == 1 && res.rows) {
let list = res.rows
this.blessingWordsList = (type==0 ? list : [...this.blessingWordsList, ...list])
if (this.blessingWordsList.length > 0 && !this.timer && this.isActive == 1) {
if(this.pageWallNum == 1){
this.newBlessingWordsList = []
this.newBlessingWordsList1 = []
this.newBlessingWordsList2 = []
this.blessingWordsCount = 0
}
this.start()
}
// 处理请求下一页的情况
if (type == 1) {
this.start()
}
this.hasWallNext = res.hasNext
if (!this.hasWallNext) {
this.blessingStatus = "没有更多了哟"
} else {
this.blessingStatus = "点击加载更多"
}
}
} catch (error) {
console.log(error)
}
},
// 加载更多
async handlerMore() {
if (this.hasWallNext && !this.isloading) {
this.isloading = true
this.pageWallNum++
await this.getBlessingWall(1)
}
},

开启一个定时器,用于动态添加左右列表的数据


start() {
// 清除定时器
clearInterval(this.timer)
this.timer = null;

this.timer = setInterval(() => {
let len = this.blessingWordsList.length
if (this.blessingWordsCount < len) {
let isHave = false
// 在列表中获取一个元素
let item =this.blessingWordsList[this.blessingWordsCount]
// 判断新列表中是否已经存在相同元素,防止重复添加
this.newBlessingWordsList.forEach((tmp)=>{
if(tmp.id == item.id){
isHave = true
}
})
// 如果不存在
if (!isHave) {
this.newBlessingWordsList.push(item)//添加该元素
this.$nextTick(() => {
this.getHei(item)//添加元素到左右列表
})
}
} else {
// 遍历完列表中的数据,则清除定时器
clearInterval(this.timer)
this.timer = null;
}
}, 10)
}

计算当前左右容器的高度,判断数据要添加到哪一边



  • 使用uni-app的方法获取左右容器的dom对象,再获取他们当前的高度

  • 比较左右高度,向两个数组动态插入数据

  • 每插入一条数据,计数器+1


getHei(item) {
const query = uni.createSelectorQuery().in(this)
// 左边
query.select('#leftHalf').boundingClientRect(res => {
if (res) {
this.leftHeight = res.height
}
// 右边
const query1 = uni.createSelectorQuery().in(this)
query1.select('#rightHalf').boundingClientRect(dataRight => {
if (dataRight) {
this.rightHeight = dataRight.height != 0 ? dataRight.height : 0
if (this.leftHeight == this.rightHeight || this.leftHeight < this.rightHeight) {
// 相等 || 左边小
this.newBlessingWordsList1.push(item)
} else {
// 右边小
this.newBlessingWordsList2.push(item)
}
}
this.blessingWordsCount++
}).exec()
}).exec()
},

这里有一个注意点,调用start方法的时候,必须确保页面渲染了左右容器的元素,否则会拿不到容器的高度


比如我这个项目是有tab切换的!


微信图片_20230731231616.jpg
进入页面的时候会请求一次数据,这时候因为tab初始状态在0,所以并不会调用start方法,要到切换tab到1时,才会调用start方法开始计算高度。


data(){
return{
isActive: 0, //tab初始化索引
timer:null,//定时器
}
}
async onLoad(options) {
this.getBlessingWall()
}
// tab选项卡切换
tabClick(index) {
this.isActive = index
this.isLoaded = false;
if (this.blessingWordsList.length > 0 && !this.timer && this.isActive == 1) {
if(this.pageWallNum == 1){
this.newBlessingWordsList = []
this.newBlessingWordsList1 = []
this.newBlessingWordsList2 = []
this.blessingWordsCount = 0
}
this.start()
}
},

最后


这次选用了flex实现瀑布流,实现瀑布流的方式还有其他几种方法,后续有机会的话,我会补充其他几种方式,如果感兴趣的话,可以点点关注哦!


作者:藤原豆腐店
来源:juejin.cn/post/7260713996165021754
收起阅读 »

前端同事最讨厌的后端行为,看看你中了没有

web
前端同事最讨厌的后端行为,看看你中了没有 听说这是前端程序员最讨厌的后端行为,不知道你有没有碰到过,或者你的前端同事虽然没跟你说过,但是你已经被偷偷吐槽了。 前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己...
继续阅读 »

前端同事最讨厌的后端行为,看看你中了没有



听说这是前端程序员最讨厌的后端行为,不知道你有没有碰到过,或者你的前端同事虽然没跟你说过,但是你已经被偷偷吐槽了。




前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己的接口测试员,耽误前端的开发进度。



听到这个吐槽,仿佛看到曾经羞愧的自己。这个毛病以前我也有啊,有些接口,尤其是大表单提交接口,表单特别大,字段很多,有时候就偷懒了,直接编译过了,就发到测试环境了。前端同时联调的时候一调接口,异常了。


好在后来改了,毕竟让人发现自己接口写的有问题,也是一件丢脸的事儿。


但是我还真见过后端的同学,写完接口一个都不测,直接发测试环境的。


我就碰到过厉害的,编译都不过,就直接提代码。以前,有个新来的同事,分了任务就默默的干着,啥也不问,然后他做的功能测试就各种发现问题。说过之后,就改一下,但是基本上还是不测试,本想再给他机会的,所以后来他每次提代码,我都review一下。直到有一天,我发现忍不了了,他把一段全局配置给注释了,然后把代码提了,我过去问他是不是本地调试,忘了取消注释了。他的回答直接让我震惊了,他说:不是的,是因为不注释那段代码,我本地跑步起来,所以肯定是那段代码有问题,所以就注释了。


然后,当晚,他就离职了。


解决方式


对于这种大表单类似的问题,应该怎么处理呢?


好像没有别的方法,只能克服自己的懒惰,为自己写的代码负责。就想着,万一接口有问题,别人可能会怀疑你水平不行,你水平不行,就是你不行啊,程序员怎么能不行呢。


你可以找那么在线 Java BeanJSON的功能,直接帮你生成请求参数,或者现在更可以借助 ChatGPT ,帮你生成请求参数,而且生成的参数可能比你自己瞎填的看上去更合理。


或者,如果是小团队,不拘一格的话,可以让前端的同事把代码提了,你本地跑着自测一下,让前端同事先做别的功能,穿插进行也可以。



前端吐槽:后端修改了字段或返回结构不通知前端



这个就有点不讲武德了。


正常情况下,返回结构和字段都是事先约定好的,一般都是先写接口,做一些 Mock 数据,然后再实现真实的逻辑。


除了约定好返回字段和结构外,还包括接口地址、请求方法、头信息等等,而且一个项目都会有项目接口规范,同一类接口的返回字段可能有很多相同的部分。


后端如果改接口,必须要及时通知前端,这其实应该是正常的开发流程。后端改了接口,不告诉前端,到时候测试出问题了,一般都会先找前端,这不相当于让前端背锅了吗,确实不地道啊。


后端的同学们,谨记啊。



前端吐槽:为了获取一个信息,要先调用好几个接口,可能参数还是相同的



假设在一个详情页面,以前端的角度就是,我获取详情信息,就调用详情接口好了,为什么调用详情接口之前,要调用3、4个其他的接口,你详情里需要啥参数,我直接给你传过去不就好了吗。


在后端看来可能是这样的,我这几个接口之前就写好了,前端拿过去就能用,只不过就是多调几次罢了,没什么大不了的吧。


有些时候,可能确实是必须这么做的,比如页面内容太多,有的部分查询逻辑复杂,比较耗时,这时候需要异步加载,这样搞确实比较好。


但是更多时候其实就是后端犯懒了,不想再写个新接口。除了涉及到性能的问题,大多数逻辑都应该在后端处理,能用一个接口处理完,就不应该让前端多调用第二个接口。


有前端的朋友曾经问过我,他说,他们现在做的系统中有些接口是根据用户身份来展示数据的,但是前端调用登录接口登录系统后,在调用其他接口的时候,除了在 Header 中加入 token 外,还有传很多关于用户信息的很多参数,这样做是不是不合理的。


这肯定不合理,token 本来就是根据用户身份产生的,后端拿到 token 就能获取用户信息,这是常识问题,让前端在接口中再传一遍,既不合理也不安全。


类似的问题还有,比如后端接口返回一堆数据,然后有的部分有用、有的部分没有,有的部分还涉及到逻辑,不借助文档根本就看不明白怎么用,这其实并不合理。


接口应该尽量只包含有用的部分,并且尽可能结构清晰,配合简单的字段说明就能让人明白是怎么回事,是最好的效果。


如果前后端都感觉形势不对了,后端一个接口处理性能跟不上了,前端处理又太麻烦了。这时候就要向上看了,产品设计上可能需要改一改了。


后端的同学可以学一点前端,前端的同学也可以学一点后端,当你都懂一些的时候,就能两方面考虑了,这样做出来的东西可能会更好用一点。总之,前后端相互理解,毕竟都是为了生活嘛。


作者:古时的风筝
来源:juejin.cn/post/7254927062425829413
收起阅读 »

面试官:你能说说常见的前端加密方法吗?

web
前言 本篇文章略微介绍一下前端中常见的加密算法。前端中常见的加密算法主要形式包括——哈希函数,对称加密和非对称加密算法。 一、哈希函数 定义:哈希也叫散列,是指将任意长度的消息映射为固定长度的输出的算法,该输出一般叫做散列值或者哈希值,也叫做摘要(Dige...
继续阅读 »

前言


本篇文章略微介绍一下前端中常见的加密算法。前端中常见的加密算法主要形式包括——哈希函数,对称加密和非对称加密算法。


一、哈希函数


image.png



  • 定义:哈希也叫散列,是指将任意长度的消息映射为固定长度的输出的算法,该输出一般叫做散列值或者哈希值,也叫做摘要(Digest)。简单来说,这种映射就是一种数据压缩,而且散列是不可逆的,也就是无法通过输出还原输入。

  • 特点:不可逆性(单向性)、抗碰撞性(消息不同其散列值也不同)、长度固定

  • 常见应用场景:由于不可逆性,常用于密码存储、数字签名、电子邮件验证、验证下载等方面,更多的是用用在验证数据的完整性方面。



    • 密码存储:明文保存密码是危险的。通常我们把密码哈希加密之后保存,这样即使泄漏了密码,因为是散列后的值,也没有办法推导出密码明文(字典攻击难以破解)。验证的时候,只需要对密码(明文)做同样的散列,对比散列后的输出和保存的密码散列值,就可以验证同一性。

    • 可用于验证下载文件的完整性以及防篡改:比如网站提供安装包的时候,通常也同时提供md5值,这样用户下载之后,可以重算安装包的md5值,如果一致,则证明下载到本地的安装包跟网站提供的安装包是一致的,网络传输过程中没有出错。



  • 优势:不可逆,速度快、存储体积小,可以帮助保护数据的完整性和减轻篡改风险。

  • 缺点:安全性不高、容易受到暴力破解


image.png


常见类型:SHA-512、SHA-256、MD5(MD5生成的散列码是128位)等。



  • MD5(Message Digest Algorithm 5) :是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文。

  • SHA(Secure Hash Algorithm) :可以对任意长度的数据运算生成一个固定位数的数值。

  • SHA/MD5对比:SHA在安全性方面优于MD5,并且可以选择多种不同的密钥长度。 但是,由于内存需求更高,运行速度可能会更慢。 不过,MD5因其速度而得到广泛使用,但是由于存在碰撞攻击风险,因此不再推荐使用。


二、对称加密



  • 定义:指加密和解密使用同一种密钥的算法。


image.png



  • 特点:优点是速度快,通信效率高;缺点是安全性相对较低。信息传输使用一对一,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和N个客户端通信,需要维持N个密码记录且不能修改密码。

  • 优势:效率高,算法简单,系统开销小,速度快,适合大数量级的加解密,安全性中等

  • 缺点:秘钥管理比较难,密钥存在泄漏风险。

  • 常见应用场景:适用于需要高速加密/解密的场景,例如 HTTP 传输的 SSL/TLS 部分,适用于加密大量数据,如文件加密、网络通信加密、数据加密、电子邮件、Web 聊天等。



    • 文件加密:将文件用相同的密钥加密后传输或存储,只有拥有密钥的用户才能解密文件。

    • 数据库加密:对数据库中的敏感信息进行加密保护,防止未经授权的人员访问。

    • 通信加密:将网络数据通过对称加密算法进行加密,确保数据传输的机密性,比较适合大量短消息的加密和解密。

    • 个人硬盘加密:对称加密可以为硬盘加密提供较好的安全性和高处理速度,这对个人电脑而言可能是一个不错的选择。



  • 常见类型DES,3DES,AES 等:



    • DES(Data Encryption Standard):分组式加密算法,以64位为分组对数据加密,加解密使用同一个算法,速度较快,适用于加密大量数据的场合。

    • 3DES(Triple DES):三重数据加密算法,是基于DES,对每个数据块应用三次DES加密算法,强度更高。

    • AES(Advanced Encryption Standard):高级加密标准算法,速度快,安全级别高,目前已被广泛应用,适用于加密大量数据,如文件加密、网络通信加密等。




AES与DES区别

AES与DES之间的主要区别在于加密过程。在DES中,将明文分为两半,然后再进行进一步处理;而在AES中,整个块不进行除法,整个块一起处理以生成密文。相对而言,AES比DES快得多,与DES相比,AES能够在几秒钟内加密大型文件。



  • DES



    • 优点:DES算法具有极高安全性,到目前为止,除了用穷举搜索法对DES算法进行攻击外,还没有发现更有效的办法。

    • 缺点:分组比较短、密钥太短、密码生命周期短、运算速度较慢。



  • AES



    • 优点:运算速度快,对内存的需求非常低,适合于受限环境。分组长度和密钥长度设计灵活, AES标准支持可变分组长度;具有很好的抵抗差分密码分析及线性密码分析的能力。

    • 缺点:目前尚未存在对AES 算法完整版的成功攻击,但已经提出对其简化算法的攻击。




三、非对称加密


-定义:指加密和解密使用不同密钥的算法,通常情况下使用公共密钥进行加密,而私有密钥用于解密数据。公钥和私钥是成对存在,公钥是从私钥中提取产生公开给所有人的,如果使用公钥对数据进行加密,那么只有对应的私钥(不能公开)才能解密,反之亦然。


image.png



  • 特点:缺点是加密解密速度较慢,通信效率较低,优点是安全性高,需要两个不同密钥,信息一对多。因为它使用的是不同的密钥,所以需要耗费更多的计算资源。服务器只需要维持一个私钥就可以和多个客户端进行通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密的速度慢。

  • 优势:秘钥容易管理,不存在密钥的交换问题,安全性好,主要用在数字签名,更适用于区块链技术的点对点之间交易的安全性与可信性。

  • 缺点:加解密的计算量大,比对称加密算法计算复杂,性能消耗高,速度慢,适合小数据量或数据签名

  • 常见应用场景:在实际应用中,非对称加密通常用于需要确保数据完整性和安全性的场合,例如数字证书的颁发、SSL/TLS 协议的加密、数字签名、加密小文件、密钥交换、实现安全的远程通信等。



    • 数字签名:数字签名是为了保证数据的真实性和完整性,通常使用非对称加密实现。发送方使用自己的私钥对数据进行签名,接收方使用发送方的公钥对签名进行验证,如果验证通过,则可以确认数据的来源和完整性。常见的数字签名算法都基于非对称加密,如RSA、DSA等。

    • ** 身份认证**:Web浏览器和服务器使用SSL/TLS技术来进行安全通信,其中就使用了非对称加密技术。Web浏览器在与服务器建立连接时,会对服务器进行身份验证并请求其证书。服务器将其证书发送给浏览器,证书包含服务器的公钥。浏览器使用该公钥来加密随机生成的“对话密钥”,然后将其发送回服务器。服务器使用自己的私钥解密此“对话密钥”,以确保双方之间的会话是安全的。

    • 安全电子邮件:非对称加密可用于电子邮件中,确保邮件内容只能由预期的收件人看到。发件人使用收件人的公钥对邮件进行加密,收件人使用自己的私钥对其进行解密。这确保了只有目标收件人才能读取邮件。



  • 常见类型RSA,DSA,DSS,ECC 等



    • RSA:由 RSA 公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变的。RSA 是一种非对称加密算法,即加密和解密使用一对不同的密钥,分别称为公钥和私钥。公钥用于加密数据,私钥用于解密数据。RSA 算法的安全性基于大数分解问题,密钥长度通常选择 1024 位、2048 位或更长。RSA 算法用于保护数据的机密性、确保数据的完整性和实现数字签名等功能。

    • DSA(Digital Signature Algorithm) :数字签名算法,仅能用于签名,不能用于加解密。

    • ECC(Elliptic Curves Cryptography) :椭圆曲线密码编码学。

    • DSS:数字签名标准,可用于签名,也可以用于加解密。




总结


前端使用非对称加密原理很简单,平时用的比较多的也是非对称加密,前后端共用一套加密解密算法,前端使用公钥对数据加密,后端使用私钥将数据解密为明文。中间攻击人拿到密文,如果没有私钥的话是没办法破解的。


欢迎大佬继续评论区补充


作者:优秀稳妥的Zn
来源:juejin.cn/post/7280057907055919144
收起阅读 »

你的网站如何接入QQ,微信登录

web
主要实现步骤 对接第三方平台,获取第三方平台的用户信息。 利用该用户信息,完成本应用的注册。 qq登录接入 接入前的配置 qq互联 登录后,点击头像,进行开发者信息填写,等待审核。 邮箱验证后,等待审核。 审核通过后,然后就可以创建应用了。 然后填写...
继续阅读 »

主要实现步骤



  • 对接第三方平台,获取第三方平台的用户信息。

  • 利用该用户信息,完成本应用的注册。


qq登录接入


接入前的配置


qq互联


登录后,点击头像,进行开发者信息填写,等待审核。


image.png


邮箱验证后,等待审核。


image.png


审核通过后,然后就可以创建应用了。


image.png


然后填写一些网站信息,等待审核。审核通过后,即可使用。


开始接入



  1. 导入qq登录的sdk



<script type="text/javascript" charset="utf-8" src="https://connect.qq.com/qc_jssdk.js" data-appid="您应用的appid"
data-redirecturi="qq扫码后的回调地址(上面配置中可以查到)">
script>


  1. 点击qq登录,弹出扫码窗口。


// QQ 登录的 URL
const QQ_LOGIN_URL =
'https://graph.qq.com/oauth2.0/authorize?client_id=您的appid&response_type=token&scope=all&redirect_uri=您的扫码后的回调地址'
window.open(
QQ_LOGIN_URL,
'oauth2Login_10609',
'height=525,width=585, toolbar=no, menubar=no, scrollbars=no, status=no, location=yes, resizable=yes'
)


  1. 挂起qq登录。需要注意的是,扫码登录成功后,调试代码需要在线上环境。


"qqLoginBtn" v-show="false">

// QQ 登录挂起
onMounted(() => {
QC.Login(
{
btnId: 'qqLoginBtn' //插入按钮的节点id
},
// 登录成功之后的回调,但是需要注意,这个回调只会在《登录回调页面中被执行》
// 登录存在缓存,登录成功一次之后,下次进入会自动重新登录(即:触发该方法,所以我们应该在离开登录页面时,注销登录)
// data就是当前qq的详细信息
(data, opts) => {
console.log('QQ登录成功')
// 1. 注销登录,否则在后续登录中会直接触发该回调
QC.Login.signOut()
// 2. 获取当前用户唯一标识,作为判断用户是否已注册的依据。(来决定是否跳转到注册页面)
const accessToken = /access_token=((.*))&expires_in/.exec(
window.location.hash
)[1]
// 3. 拼接请求对象
const oauthObj = {
nickname: data.nickname,
figureurl_qq_2: data.figureurl_qq_2,
accessToken
}
// 4. 完成跨页面传输 (需要将数据传递到项目页面,而非qq登录弹框页面中进行操作)
brodacast.send(oauthObj)

// 针对于 移动端而言:通过移动端触发 QQ 登录会展示三个页面,原页面、QQ 吊起页面、回调页面。并且移动端一个页面展示整屏内容,且无法直接通过 window.close() 关闭,所以在移动端中,我们需要在当前页面继续进行后续操作。
oauthLogin(LOGIN_TYPE_QQ, oauthObj)
// 5. 在 PC 端下,关闭第三方窗口
window.close()
}
)
})


  1. 跨页面窗口通信


想要实现跨页面信息传输,通常有两种方式:



  • BroadcastChannel:允许 同源 的不同浏览器窗口,Tab页,frame或者 iframe 下的不同文档之间相互通信。但是会存在兼容性问题。

  • localStorage + window.onstorage:通过localStorage 进行 同源 的数据传输。用来处理 BroadcastChannel 不兼容的浏览器。以前写过一篇文章


// brodacast.js
// 频道名
const LOGIN_SUCCESS_CHANNEL = 'LOGIN_SUCCESS_CHANNEL'

// safari@15.3 不支持 BroadcastChannel,所以我们需要对其进行判定使用,在不支持 BroadcastChannel 的浏览器中,使用 localstorage
let broadcastChannel = null
if (window.BroadcastChannel) {
broadcastChannel = new BroadcastChannel(LOGIN_SUCCESS_CHANNEL)
}

/**
* 等待 QQ 登录成功
* 因为 QQ 登录会在一个新的窗口中进行,用户扫码登录成功之后会回调《新窗口的 QC.Login 第二参数 cb》,而不会回调到原页面。
* 所以我们需要在《新窗口中通知到原页面》,所以就需要涉及到 JS 的跨页面通讯,而跨页面通讯指的主要就是《同源页面的通讯》
* 同源页面的通讯方式有很多,我们这里主要介绍:
* 1. BroadcastChannel ->
https://developer.mozilla.org/zh-CN/docs/Web/API/BroadcastChannel

* 2. window.onstorage:注意:该事件不在导致数据变化的当前页面触发
*/

/**
* 等待回调,它将返回一个 promise,并携带对应的数据
*/

const wait = () => {
return new Promise((resolve, reject) => {
if (broadcastChannel) {
// 触发 message 事件时的回调函数
broadcastChannel.onmessage = async (event) => {
// 改变 promise 状态
resolve(event.data)
}
} else {
// 触发 localStorage 的 setItem 事件时回调函数
window.onstorage = (e) => {
// 判断当前的事件名
if (e.key === LOGIN_SUCCESS_CHANNEL) {
// 改变 promise 状态
resolve(JSON.parse(e.newValue))
}
}
}
})
}

/**
* 发送消息。
* broadcastChannel:触发 message
* localStorage:触发 setItem
*/

const send = (data) => {
if (broadcastChannel) {
broadcastChannel.postMessage(data)
} else {
localStorage.setItem(LOGIN_SUCCESS_CHANNEL, JSON.stringify(data))
}
}

/**
* 清除
*/

const clear = () => {
if (broadcastChannel) {
broadcastChannel.close()
broadcastChannel = null
}
localStorage.removeItem(LOGIN_SUCCESS_CHANNEL)
}

export default {
wait,
send,
clear
}


  1. 拿到数据后,进行登录(自己服务器登录接口)操作。



  • 传入对应参数(loginType, accessToken)等参数进行用户注册判断。

  • 通过accessToken判断用户已经注册,那么我们就直接在后台查出用户名和密码直接登录了。

  • 通过accessToken判断用户未注册,那么我们将跳转到注册页面,让其注册。


 // 打开视窗之后开始等待
brodacast.wait().then(async (oauthObj) => {
// 登录成功,关闭通知
brodacast.clear()
// TODO: 执行登录操作
oauthLogin("QQ", oauthObj)
})

// oauthLogin.js
import store from '@/store'
import router from '@/router'
import { message } from '@/libs'
import { LOGIN_TYPE_OAUTH_NO_REGISTER_CODE } from '@/constants'

/**
* 第三方登录统一处理方法
*
@param {*} oauthType 登录方式
*
@param {*} oauthData 第三方数据
*/

export const oauthLogin = async (oauthType, oauthData) => {
const code = await store.dispatch('user/login', {
loginType: oauthType,
...oauthData
})
// 返回 204 表示当前用户未注册,此时给用户一个提示,走注册页面
if (code === LOGIN_TYPE_OAUTH_NO_REGISTER_CODE) {
message('success', `欢迎您 ${oauthData.nickname},请创建您的账号`, 6000)
// 进入注册页面,同时携带当前的第三方数据和注册标记
router.push({
path: '/register',
query: {
reqType: oauthType,
...oauthData
}
})
return
}

// 否则表示用户已注册,直接进入首页
router.push('/')
}

微信扫码登录接入


微信开放平台


登录后,进行对应的应用注册,填写一大堆详细信息,然后进行交钱,就可以使用微信登录了。


image.png


开始接入


整个微信登录流程与QQ登录流程略有不同,分为以下几步:


1.通过 微信登录前置数据获取 接口,获取登录数据(比如 APP ID)。就是后台将一些敏感数据通过接口返回。


2.根据获取到的数据,拼接得到 open url 地址。打开该地址,展示微信登录二维码。移动端微信扫码确定登录。


// 2. 根据获取到的数据,拼接得到 `open url` 地址
window.open(
`https://open.weixin.qq.com/connect/qrconnect?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`,
'',
'height=525,width=585, toolbar=no, menubar=no, scrollbars=no, status=no, location=yes, resizable=yes'
)

3.等待用户扫码后,从当前窗口中解析 window.location.search 得到用户的 code数据。 微信扫码后,会重定向到登录页面。


/**
* 微信登录成功之后的窗口数据解析
*/

if (window.location.search) {
const code = /code=((.*))&state/.exec(window.location.search)[1]
if (code) {
brodacast.send({
code
})
// 关闭回调网页
window.close()
}
}

4.根据 appId、appSecret、code 通过接口获取用户的 access_token


5.根据 access_token 获取用户信息


6.通过用户信息触发 oauthLogin 方法。


调用的接口,都是后端通过微信提供的api来获取到对应的数据,然后再通过接口返回给开发者。  以前也写过微信登录文章


// 等待扫码登录成功通知
brodacast.wait().then(async ({ code }) => {
console.log('微信扫码登录成功')
console.log(code)
// 微信登录成功,关闭通知
brodacast.clear()
// 获取 AccessToken 和 openid
const { access_token, openid } = await getWXLoginToken(
appId,
appSecret,
code
)
// 获取登录用户信息
const { nickname, headimgurl } = await getWXLoginUserInfo(
access_token,
openid
)
console.log(nickname, headimgurl)
// 执行登录操作
oauthLogin(LOGIN_TYPE_WX, {
openid,
nickname,
headimgurl
})
})

需要注意的是,在手机端,普通h5页面是不能使用微信扫码登录的。


总结


相同点



  • 接入前需要配置一些内容信息。

  • 都需要在线上环境进行调试。

  • 都是扫码后在三方窗口中获取对应的信息,发送到当前项目页面进行请求,判断用户是否已经注册,还是未注册。已经注册时,调用login接口时,password直接传递空字符串即可,后端可以通过唯一标识,获取到对应的用户名和密码,直接返回token进行登录。未注册,就跳转到注册页面,让其注册。


不同点



  • qq接入需要导入qc_sdk。

  • qq直接扫码后即可获取到用户信息,就可以直接调用login接口进行判断用户是否注册了。

  • 微信扫码后,获取code来换取access_token, openid,然后再通过access_token, openid来换取用户信息。然后再调用login接口进行判断用户是否注册了。


作者:Spirited_Away
来源:juejin.cn/post/7311343161363234866
收起阅读 »

HTML问题:如何实现分享URL预览?

web
前端功能问题系列文章,点击上方合集↑ 序言 大家好,我是大澈! 本文约2100+字,整篇阅读大约需要3分钟。 本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。 感谢关注微信公众号...
继续阅读 »

前端功能问题系列文章,点击上方合集↑


序言


大家好,我是大澈!


本文约2100+字,整篇阅读大约需要3分钟。


本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。


感谢关注微信公众号:“程序员大澈”,然后加入问答群,从此让解决问题的你不再孤单!


1. 需求分析


为了提高用户对页面链接分享的体验,需要对分享链接做一些处理。


以 Telegram(国外某一通讯软件) 为例,当在 Telegram 上分享已做过处理的链接时,它会自动尝试获取链接的预览信息,包括标题、描述和图片。


如此当接收者看到时,可以立即获取到分享链接的一些重要信息。这有助于接收者更好地了解链接的内容,决定是否点击查看详细内容。


图片


2. 实现步骤


2.1 实现前的说明


对于URL分享预览这个功能问题,在项目中挺常用的,只不过今天我们是以一些框架分享API的底层原理角度来讲的。


实现这种功能的关键,是在分享的链接中嵌入适当的元数据信息,应用软件会自动解析,请求分享链接的预览信息,并根据返回的元数据生成预览卡片。


对于国内的应用软件,目前我试过抖音,它可以实现分享和复制粘贴都自动解析,而微信、QQ等只能实现分享的自动解析。


对于国外的应用软件,我只实验过Telegram,它可以实现分享和复制粘贴都自动解析,但我想FacebookTwitterInstagram这些应用应该也都是可以的。


2.2 实现代码


实现URL链接的分享预览,你可以使用 Open Graph协议或 Twitter Cards,然后在 HTML 的 标签中,添加以下 meta 标签来定义链接预览的信息。


使用时,将所有meta全部复制过去,然后根据需求进行自定义即可。


还要注意两点,确保你页面的服务器正确配置了 SSL 证书,以及确保链接的URL有效(即:服务器没有做白名单限制)。


<head>
  
  <meta property="og:title" content="预览标题">
  <meta property="og:description" content="预览描述">
  <meta property="og:image:width" content="图片宽度">
  <meta property="og:image:height" content="图片高度">
  <meta property="og:image" content="预览图片的URL">
  <meta property="og:url" content="链接的URL">
  
  
  <meta name="twitter:card" content="summary">
  <meta name="twitter:title" content="预览标题">
  <meta name="twitter:description" content="预览描述">
  <meta property="twitter:image:width" content="图片宽度">
  <meta property="twitter:image:height" content="图片高度">
  <meta name="twitter:image" content="预览图片的URL">
  <meta name="twitter:url" content="链接的URL">
head>

下面我们做一些概念的整理、总结和学习。


3. 问题详解


3.1 什么是Open Graph协议?


Open Graph协议是一种用于在社交媒体平台上定义和传递网页元数据的协议。它由 Facebook 提出,并得到了其他社交媒体平台的支持和采纳。Open Graph 协议旨在标准化网页上的元数据,使网页在社交媒体上的分享和预览更加一致和可控。


通过在网页的 HTML  标签中添加特定的 meta 标签,使用 Open Graph 协议可以定义和传递与网页相关的元数据信息,如标题、描述、图片等。这些元数据信息可以被社交媒体平台解析和使用,用于生成链接预览、分享内容和提供更丰富的社交图谱。


使用 Open Graph 协议,网页的所有者可以控制链接在社交媒体上的预览内容,确保链接在分享时显示的标题、描述和图片等信息准确、有吸引力,并能够准确传达链接的主题和内容。这有助于提高链接的点击率、转化率和用户体验。


Open Graph 协议定义了一组标准的 meta 标签属性,如 og:titleog:descriptionog:image 等,用于提供链接预览所需的元数据信息。通过在网页中添加这些 meta 标签并设置相应的属性值,可以实现链接预览在社交媒体平台上的一致展示。


需要注意的是,Open Graph 协议是一种开放的标准,并不限于 Facebook 平台。其他社交媒体平台,如 Twitter、LinkedIn 等,也支持使用 Open Graph 协议定义和传递网页元数据,以实现链接预览的一致性。


图片


3.2 什么是Twitter Cards?


Twitter Cards 是一种由 Twitter 推出的功能,它允许网站所有者在他们的网页上定义和传递特定的元数据,以便在 Twitter 上分享链接时生成更丰富和吸引人的预览卡片。通过使用 Twitter Cards,网页链接在 Twitter 上的分享可以展示标题、描述、图片、链接和其他相关信息,以提供更具吸引力和信息丰富的链接预览。


Twitter Cards 提供了多种类型的卡片,以适应不同类型的内容和需求。以下是 Twitter Cards 的一些常见类型:



  • Summary CardSummary Card 类型的卡片包含一个标题、描述和可选的图片。它适用于分享文章、博客帖子等内容。

  • Summary Card with Large ImageSummary Card with Large Image 类型的卡片与 Summary Card 类型类似,但图片尺寸更大,更突出地展示在卡片上。

  • App CardApp Card 类型的卡片用于分享移动应用程序的信息。它包含应用的名称、图标、描述和下载按钮,以便用户可以直接从预览卡片中下载应用。

  • Player CardPlayer Card 类型的卡片用于分享包含媒体播放器的内容,如音频文件、视频等。它允许在预览卡片上直接播放媒体内容。


通过在网页的 HTML  标签中添加特定的 meta 标签,使用 Twitter Cards 可以定义和传递与链接预览相关的元数据信息,如标题、描述、图片、链接等。这些元数据信息将被 Twitter 解析和使用,用于生成链接预览卡片。


使用 Twitter Cards 可以使链接在 Twitter 上的分享更加吸引人和信息丰富,提高链接的点击率和用户参与度。它为网站所有者提供了更多控制链接在 Twitter 上展示的能力,并提供了一种更好的方式来呈现他们的内容。


图片


图片


作者:程序员大澈
来源:juejin.cn/post/7310112330663231515
收起阅读 »

大厂是怎么封装api层的?ts,axios 基于网易公开课

web
先看一下使用方法 先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。 挺香的。 上核心代码 代码一:utils/request/getrequest.ts import axios, { type Axi...
继续阅读 »

先看一下使用方法
请求封装2.png


先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。
挺香的。


上核心代码


WX20231124-143933@2x.png


代码一:utils/request/getrequest.ts



import axios, { type AxiosRequestConfig, type CancelTokenSource } from "axios";
import { manualStopProgram } from '@/utils/index';

import server from "./server";
import type { RequestConfig, ApiRouter, ServerRes } from './server.types'

class Requestextends keyof ApiRouter = keyof ApiRouter> {
requestRouter: ApiRouter = {} as ApiRouter
requestTimes = 0;
requestMap: Record<string, CancelTokenSource> = {};
toLogined = false

/**
*
@feat <注册请求路由>
*/

parseRouter(routerName: T, defaultAxiosConfigMap: Record) {
const apiModule = this.requestRouter[routerName] = {} as ApiRouter[T]

Object.entries(defaultAxiosConfigMap).forEach((item) => {
type ApiName = keyof ApiRouter[T]

const [apiName, defaultRequestConfig] = item as [ApiName, RequestConfig];

const request = this.sendMes.bind(
this,
routerName,
apiName,
defaultRequestConfig,
);

apiModule[apiName] = request as ApiRouter[T][ApiName]
apiModule[apiName].state = "ready";
});
}
async sendMes<ApiName extends keyof ApiRouter[T] = keyof ApiRouter[T]>(
routerName: T,
apiName: ApiName,
defaultRequestConfig: RequestConfig,
requestParams: Record<string, any>,
otherAxiosConfig?: RequestConfig
): Promise<ServerRes> {
this.requestTimes += 1;

return new Promise(async (resolve, reject) => {
try {
const selfMe = this.requestRouter[routerName][apiName];
const requstConfig: RequestConfig = {
...defaultRequestConfig,
...otherAxiosConfig,
data: requestParams,
};

/**
*
@feat <取消上一个同url请求>
*
@remarks [注:
* 个别页面需要同个api地址,多次请求,请传uniKey 作为区分,
* 不然有概率出现上一个请求被干掉
* ]
*/

if (selfMe.state === 'pending') this.cancelLastSameUrlRequest(requstConfig);

// 保险方案,传了 uniKey 才取消请求
// if (selfMe.state === 'pending' && requstConfig.uniKey) this.cancelLastSameUrlRequest(requstConfig);

const successCb = (res: ServerRes) => {
const ret = this.responseHandle(res, requstConfig)
resolve(ret);
};
const failCb = (error: unknown) => {
console.error("接口报错: " + requstConfig.url, error);
// 处理错误逻辑
throw error;
};
const complete = () => {
selfMe.state = "ready";
this.requestTimes -= 1;

if (this.requestTimes === 0) {
this.toLogined = false;
}
};

selfMe.state = "pending";
requstConfig.cancelToken = this.axiosSourceHandle(requstConfig).token;

await server(requstConfig).then(successCb).catch(failCb).finally(complete);

} catch (error) {
reject(error);
}
})
}

responseHandle(res: ServerRes, config: RequestConfig) {
const { code } = res;
console.warn(`请求返回: ${config.url}`, res);

if (code === 405) throw String("405 检查请求方式");
if (code === 401) this.toLogin();
if (code !== 200) throw String(res.message);

return res;
}

toLogin() {
if (this.toLogined) return;
throw String("请先登录");
}

generateReqKey(requestConfig: RequestConfig) {
return `${requestConfig.url}__${requestConfig.uniKey || ''}`
}
axiosSourceHandle(requestConfig: RequestConfig) {
const cancelToken = axios.CancelToken;
const source = cancelToken.source();

const reqKey = this.generateReqKey(requestConfig);
this.requestMap[reqKey] = source;

return source;
}
// 处理取消上一个请求
cancelLastSameUrlRequest(requestConfig: RequestConfig) {
const reqKey = this.generateReqKey(requestConfig);
const currentReqKey = this.requestMap[reqKey];

currentReqKey.cancel(`${manualStopProgram} reqKey: ${reqKey}`); // manualStopProgram 是一个标识,让外面的提示框忽略报错
}
}


export default new Request();



代码二:utils/request/server.ts


import axios from "axios";
import { UserInfo } from "@/utils/index";
import type { RequestConfig, ServerRes } from "./server.types";

export default async function server(
axiosRequestConfig: RequestConfig
): Promise<ServerRes> {
const token = UserInfo.getToken() || "";
const reqData = (() => {
const data = axiosRequestConfig.data;
const isFormData = data instanceof FormData;

if (isFormData) {
data.
append("token", token);
return data;
}
return {
...data,
token
};
}
)();

const { data: resBody, status } = await axios({
...axiosRequestConfig,
withCredentials: true,
data: reqData
}
).
catch((err) => {
const errMsg = err && typeof err === "object" && err !== null && "message" in err
if (errMsg) throw err.message;
throw err;
}
);

return resBody;
}

export {
server
}

import type { AxiosRequestConfig } from "axios";
import type { Api } from "@/apis/index";

export type RequestConfigany> = AxiosRequestConfig & {
uniKey?: string | number;
};

export type ApiConfig = {
params: T;
return: K;
};

export type List_pagiantion = {
page: number;
page_size: number;
};

// 这里有点绕,把各个api的参数和返回值 合成一个个特定的函数
export type ApiRouter = {
[K in keyof Api]: {
[T in keyof Api[K]]: Api[K][T] extends ApiConfig<any, any>
? {
(params: Api[K][T]["params"], otherRequestConfig?: RequestConfig): Promise<{
message: string;
code: number;
data: Api[K][T]["return"];
}>;
state?: 'pending' | 'ready'
}
: never;
};
}

export type ApiRouter__requestConfig = {
[K in keyof Api]: {
[T in keyof Api[K]]: RequestConfig;
};
}
export type ServerRes = {
code: number,
message: string,
data: any
};

接下来是apis文件夹(即开头的那个图片),在这里配置接口信息,日常业务代码在这里写


接口.png


代码一 写api配置 :src/apis/modules/admin-admin/index.ts


import type { ApiRouter__requestConfig } from "@/utils/modules/request/server.types.d";

const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {
getList: {
method: "post",
url: "/admin/admin/getList"
},
};

export default indexAdmin;

代码二 写接口类型声明 :src/apis/modules/admin-admin/index.types.d.ts


import type { ApiConfig } from "@/utils/modules/request/server.types.d";

export type AdminAdmin = {
getList: ApiConfig<
{
page: number,
page_size: number,
phone?: string,
status?: ManagerStatus,
groups_id?: number
},
{
count: number,
list: {
id: number,
phone: string,
groups_id: number,
create_at: string,
status: number,
status_txt: string,
groups_txt: string
}[]
}
>,
};


代码三 注册路由 :src/apis/index.ts


import { request } from "@/utils/index";
import indexAdmin from "./modules/index-admin/index";

request.parseRouter("indexAdmin", indexAdmin);

export type Api = {
indexAdmin: IndexAdmin;
}

// 这个是另一个作用,到处配置项,配合接口做权限控制,下面在说
export function getApiConfigMap() {
return {
indexAdmin,
};
}

下面说说这个封装方式好在哪里:


1. 解决的痛点:


以往我们看到最常见的封装方式,就这种

export function Api1() {
return axios.get('xx')
}

export function Api2() {
return axios.get('xx')
}

export function Api3() {
return axios.get('xx')
}

export function Api4() {
return axios.get('xx')
}

这种就非常麻木,一直写函数,每一个都要写配置项,没有数据结构结构=(无法复用)。
如果换成上面的 const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {},这种写法,就有数据结构了,有了结构之后就可以进行组合复用
比如上面提到的 getApiConfigMap 可以把数据结构直接导出,配合接口做按钮级权限控制,
接口会返回一份配置项{authen1: '/admin/admin/getList'}。
我们二者一比对,就能判断出是否有权限了。


比如看下面代码
PermissionWrapper 是一个权限容器组件 hasPermission=true就显示按钮
store.state.myPermission?.enterpriseAlarm?.edit 是用 getApiConfigMap 和结构权限表配合生成的


        <PermissionWrapper
hasPermission={store.
state.myPermission?.enterpriseAlarm?.edit}
>

<el-button type="primary" size="small" text onClick={openEditDialog}>
编辑
el-button>

PermissionWrapper>

ts直接提示,写起来很舒服,快准狠
ts提示.png


2. 请求函数封闭又开放


经过上面的 parseRouter 注册路由之后,sendMes 生成了N个请求函数,独一无二的函数,里面的fail success 可以做的事情很多,不如限制登录,取消上一个请求等等。大家有啥想法欢迎评论区写出来,我们一起优化它。


sendMes 最后一个参数,保持了开放性,在调用的时候我们传入uniKey就可以取消上一个请求了,还有一些特殊的参数,随便造。


3. 方便提取Api类型的参数和返回值类型(这是我额外拓展的)


我们经常会需要把参数和返回值的类型拿出来到页面上使用,这时候,就可以通过下面这个XX全局声明拿到。


declare namespace XX {
export type PromiseParams = T extends Promise ? T : never;

/**
*
@feat < 提取 api 参数 >
*
@describe <
type GetListParams = XX.ExtractApiParams

* >
*/

export type ExtractApiParams<Function_type> = Function_type extends (e: infer U) => any
? Excludeundefined>
: never;

/**
*
@feat < 提取 api 返回值 >
*
@describe <
type GetListReturn = XX.ExtractApiReturnType

* >
*/

export type ExtractApiReturnType<Function_type> = PromiseParams<
ReturnType<Function_type>
>["data"];

/**
*
@feat < 提取 api 为分页的 list item>
*
@describe <
type TableRow = XX.ExtractPromiseListItem

* >
*/

export type ExtractPromiseListItem<Function_type> =
ExtractApiReturnType<Function_type>["list"][number];
}

下面是一些使用方法举例
image.png


image.png


image.png


image.png


用上面的写法很方便就能在页面中把具体的类型拿出来。做到一次类型声明,到处使用。


api封装是一个长期的话题,axios很好用,但其实它就是一个请求方法而已。相信大家也见过很多乱七八糟的写法。特别是一些老项目,想新增api都不知道放在哪个文件夹。


很幸运无意中看到网易公开课的老师们讲解,那时候他们写的是js版本,看到这种由配置对象直接生成api函数的做法瞬间眼前一亮,这不就是我一直在找的封装方式,满足了我所有的想象。感谢感谢


后来我花了点时间,让它变成ts版本,还封装XX这个全局声明,让它彻底好用起来。希望这个封装能让大家受益。


细心的读者可能会发现上面的代码,一直抛错误,但是却没有拦截提示。 这是笔者推崇的报错终止程序,而不是用return的方式。(js终止程序,我常用throw 替代 return


如果您有什么好的建议或想法,欢迎评论区留言。有用请点点赞,还有更多经验总结在路上。
嘴下留情,骂我倒无所谓,重要的是别把评论区搞得乌烟瘴气


原课程链接:js es5版本,有兴趣的可以看看。但我觉得它那个取消请求,得再升级一下不然万一同个页面调用同个接口2次,就会取消第一个请求了。所以我加装了uniKey作为标识。
live.study.163.com/live/index.…


作者:闸蟹
来源:juejin.cn/post/7304594468157849640
收起阅读 »

喊话各大流行UI库,你们的Select组件到底行不行啊?

web
各种 UI 库的 Select,你们能不能人性化一点! 最近在云效上合并代码,本想着懒的目的输入了非连续的关键字搜索分支,结果... 大概逻辑就是,搜索时必须输入连续的字母,比如,要找 “master-alpha”分支,非要输入 master-al 才能搜到...
继续阅读 »

各种 UI 库的 Select,你们能不能人性化一点!


最近在云效上合并代码,本想着懒的目的输入了非连续的关键字搜索分支,结果...


1.gif


大概逻辑就是,搜索时必须输入连续的字母,比如,要找 “master-alpha”分支,非要输入 master-al 才能搜到,像图中输入 “masal” 就完全搜索不到。这导致了很多场景下使用起来很不方便,例如我们只记得几个非连续的关键字,或者懒得打那么多连续的关键字来搜索,用户体验较差。


然后我又看了几个流行组件库的 Select。


Element-ui


2.gif


Antd


3.gif


Naive-ui


4.gif


全军覆没!


那我们来自己实现一个吧!先来两个实战图。


不带高亮的非连续搜索


6.gif


带高亮的非连续搜索


5.gif


实现不带高亮的非连续搜索


以vue3+ElementUI为例,在这里将会用到一个小小的js库叫sdm2来实现非连续的字符串匹配。


视图部分


<el-select
v-model="value"
size="large"
placeholder="Filter options"
filterable
:filter-method="q => (query = q)">

<el-option
v-for="item in optionsComputed"
:key="item.value"
:value="item.value"
:label="item.label">

el-option>
el-select>

没有什么特别的,就是加了个filterMethod函数将关键词赋值给query状态,然后optionsComputedquery值根据关键词进行筛选


import { match } from 'sdm2';

const options = [/* 选项列表 */];
const query = ref('');
const optionsComputed = computed(() => options.filter(({ label }) =>
// 使用sdm2的match函数筛选
match(label, query.value, {
// 忽略大小写匹配
ignoreCase: true,
}
)));

就这么简单完成了。


实现带高亮的非连续搜索


视图部分


高亮部分使用v-html动态解析html字符串。


<el-select
v-model="value"
size="large"
placeholder="Filter options"
filterable
:filter-method="q => (query = q)">

<el-option
v-for="item in optionsComputed"
:key="item.value"
:value="item.value"
:label="item.label">

<div v-html="item.highlight">div>
el-option>
el-select>

为了让匹配到的关键字高亮,我们需要将匹配到的关键字转换为html字符串,并将高亮部分用包裹,最后用v-html动态解析。


import { filterMap } from 'sdm2';

const options = [/* 选项列表 */];
const query = ref('');
const optionsComputed = computed(() =>
// 为了过滤出选项,并将到的转换为html字符串,此时我们要用sdm2的filterMap函数
filterMap(options, query.value, {
ignoreCase: true,

// 把matchStr返回的字符串作为被匹配项
matchStr: ({ label }) => label,

// 匹配到后转换为html高亮字符串
onMatched: (matchedStr) => `${matchedStr}`,

// 将匹配到的项转换转换为需要的格式,str为onMatched转换后的字符串,origin为数组的每项原始值
onMap: ({ str, origin }) => {
return {
highlight: str,
...origin
}
}
})
);

然后一个带高亮的非连续搜索就完成了。


总结


这样你的搜索库就又更智能点了吧,然后各位 UI 库作者,你们也可以考虑考虑这个方案,或者有哪位朋友愿意的话也可以去为他们提一个issue或PR。

作者:古韵
来源:juejin.cn/post/7310104657212178459
收起阅读 »

Ant Design Mini 支持微信小程序啦!

web
Ant Design Mini 经过一段时间的开发与不断的努力,我们兴奋地宣布,Ant Desigin Mini 组件库中已有 16 个核心组件完成了微信小程序的适配工作!现在你不仅可以在支付宝小程序中使用 Ant Desigin Mini 组件库,也可以在微...
继续阅读 »

Ant Design Mini


经过一段时间的开发与不断的努力,我们兴奋地宣布,Ant Desigin Mini 组件库中已有 16 个核心组件完成了微信小程序的适配工作!现在你不仅可以在支付宝小程序中使用 Ant Desigin Mini 组件库,也可以在微信小程序中使用了!


目前这项适配正处于 Beta 阶段,我们诚挚地邀请大家前来体验。首批适配的组件包括:ButtonSliderContainerIconLoadingSwitchTagInputCalendarListResultPopoverMaskStepperPopupCheckbox


先来看看 Demo 效果:
image.png


我们的官网文档、Demo 都已同步更新双端切换能力:
2023-11-28 14.44.51.gif


你可以参考以下文档进行接入:



双平台无法对齐的特性


受制于各平台框架设计,以下这些特性存在差异:



  • 两个平台的事件 API 不同。支付宝小程序可以把实例上通过 props 传递给子组件,而微信需要在 data 里传递函数。视图层的写法也所有不同。

    • 下面是 Calendar 在两种平台的使用方式。

      • 微信小程序






Page({
data:{
handleFormat() {
}
}
})

<calendar onFormat="{{handleFormat}}" />

  - 支付宝小程序

Page({
handleFormat() {

}
})
<calendar onFormat="handleFormat" />


  • 微信小程序不支持在 slot 为空时显示默认值, 所以我们在微信平台取消了部分 slot,对应会损失一些定制能力。 比如说 Calendar 组件, 在支付宝可以通过 calendarTitle 这个 slot 定制标题,在微信端只能通过 css 来控制样式。


<View class="ant-calendar-title-container">
{/* #if ALIPAY */}
<Slot name="calendarTitle">
{/* #endif */}
<View class="ant-calendar-title">{currentMonth.title}</View>
{/* #if ALIPAY */}
</Slot>
{/* #endif */}
</View>


  • 微信小程序不支持循环渲染 slot , 所以部分组件无法迁移到微信, 比如说 IndexBar 组件, 使用了 Slot 递归渲染整个组件的内容。这种写法无法迁移到微信。


<view a:for="{{items}}">
<slot
value="{{item}}"
index="{{index}}"
name="labelPreview" />

</view>

双平台适配背后的工程技术


下面我们为大家介绍一下 Antd Mini 支持多平台背后的一些工程方案。


中立的模板语法: 使用 tsx 开发小程序


由于支付宝和微信的小程序的语法各有差异,为了解决让 Antd Mini 同时支持两个端,我们团队选择的 tsx 的一个子集作为小程序的模板语言。
使用 tsx 具有以下优势:



  • 可以直接使用 babel 解析代码,无需自己开发编译器。

  • 各个 IDE 原生支持 TSX 的类型推导与代码提示。

  • 视图层和逻辑层可以复用同一份 props 类型。

  • 可以直接通过 import 导入其他的小程序组件,使用 typescript 进行类型检查。

  • 视图层脚本也可以享受类型校验,无需依赖平台 IDE


由于小程序的视图语法比较受限,从 tsx 向跨平台视图语法转换是比较容易的。我们基于 babel 开发了一个简单的编译器,解析原先 tsx 的语法树以后,将 React 的语法平行转换为可读性比较强的小程序视图语法。
具体举例来看:



  • 条件判断 : 我们使用了 &&以及 ?: 三元表达式替代了之前的 :if 标签。

    • tsx: !!a && <Text>a</Text>

    • 微信小程序:<text wx:if="{{ !!a }}" />

    • 支付宝小程序:<text wx:if="{{ !!a }}" />



  • 循环: 我们使用了 map 代替之前的 :for 标签,从源码里自动分析出 :for-item :for-index :key 等标签。

    • tsx:




{todoList.map((task, taskIndex) => (
<Text
hidden={!mixin.value}
key={task.id}
data-item-id={taskIndex}
data-num={20}
>

{taskIndex} {task}
</Text>

))}


  • 微信小程序:


<block
wx:for="{{ todoList }}"
wx:for-index="taskIndex"
wx:for-item="task"
wx:key="{{ task.id }}">
<!-- display: inline -->
<text
hidden="{{ !mixin.value }}"
data-item-id="{{ taskIndex }}"
data-num="{{ 20 }}"
>
{{ taskIndex }}{{ task }}</text
>
</block>



  • 支付宝小程序


  <block
a:for="{{ todoList }}"
a:for-index="taskIndex"
a:for-item="task"
a:key="{{ task.id }}">
<!-- display: inline -->
<text
hidden="{{ !mixin.value }}"
data-item-id="{{ taskIndex }}"
data-num="{{ 20 }}"
>
{{ taskIndex }}{{ task }}</text
>
</block>



  • 事件绑定: 我们会按照配置,自动将直接转换为两个平台的格式。

    • tsx:<Text onClick="handleClick" />

    • 微信小程序: <text bind:click="handleClick" />

    • 支付宝小程序: <text onClick="handleClick" />



  • 视图层脚本

    • 我们还规定了以 sjs.ts 作为视图层脚本的文件格式。 在编译时候转换为对应平台的文件格式。

      • tsx: import helper from './helper.sjs.ts'

      • 微信小程序: <wxs src="./helper.wxs" module="helper" />

      • 支付宝小程序: <import-sjs src="./helper.sjs" module="helper" />





  • 类型方案

    • 为了让逻辑层类型与视图层关联,我们设计了一些工具类型。 比如说下面使用的 TSXMLProps,将 IProps 的 onClick 转换成了字符串。




// Calendar.axml.tsx
import { TSXMLProps, View } from 'tsxml';

interface IProps {
className?: string;
style?: string;
onClick: (e) => void;
}

interface InternalData {
size: number;
}

export default (
{ className, style }: TSXMLProps<IProps>,
{ size }: InternalData
) => (
<View class={`ant-calendar ${className ? className : ''}`} style={style}>
{size}
</View>

);

// Page.axml.tsx

import Calendar from './Calendar.axml.tsx'

export default () => (<Calendar onClick="handleClick" />)

目前使用 tsx 的这套方案还存在一些限制:



  • 和小程序相同,一个文件内只能定义一个组件。

  • 如果使用自定义组件,需要配置组件事件在各个平台的写法。


老组件语法转换?用 AI 就行了


在决定使用 tsx 语法之后,我们还面临一个很棘手的工作量问题:如何把历史组件库 axml 代码全量转换为最新的 tsx 语法?
这时候就该 ChatGPT 出场了,我们请 AI 来帮助我们完成这个一次性转换工作。
为了让转换结果更靠谱,我们使用了一些技巧:



  • 使用了 tsx 编译器等测试用例作为 prompt ,让 AI 可以更好的了解 tsx 的写法。

  • 除了 tsx 文件以外,我们还将组件的 props.ts 与 config.json 加到了 propmt 里,可以帮助 AI 生成更好的 import 导入。


在这里,你可以看到这份转换的完整 prompt。


确保 AI 产出的正确性?再用我们的编译器转回来


为了确保 AI 产出的代码是正确的,我们使用编译器将 AI 编写的 tsx 重新编译回 axml ,再用 git diff 对原始代码做比对,由此即可核查 AI 转换的正确性。


当然,这两次转换的过程不会完全等价,比如转换 map 的过程中会出现一层嵌套的 <block/>。好在这样的差异不多,一般肉眼看一遍就能确认正确性了。


跨平台通用的组件逻辑:小程序函数式组件(functional-mini)


除了视图,我们还需要确保组件逻辑适配到双端。这里我们使用了小程序函数式组件( functional-mini )的形式来编写,functional-mini 的源码及文档放置均在 ant-design/functional-mini


使用了函数式组件后,Antd Mini 用上了计算属性、useEffect 等特性,也能通过 hooks 来替换原有的大量 mixin 实现,让代码的可维护性提升了一个台阶。


以典型的 Popover 组件为例,逻辑部分适配完成后,它的代码完全变成了 React 风格,数据变更流程一目了然:


const Popover = (props: IPopoverProps) => {
const [value] = useMergedState(props.defaultVisible, {
value: props.visible,
});
const [popoverStyle, setPopoverStyle] = useState({
popoverContentStyle: '',
adjustedPlacement: '',
});

useEffect(() => {
setPopoverStyle({
popoverContentStyle: '',
adjustedPlacement: '',
});
}, [value, props.autoAdjustOverflow, props.placement]);

return {
adjustedPlacement: popoverStyle.adjustedPlacement,
popoverContentStyle: popoverStyle.popoverContentStyle,
};
};

关于小程序函数式组件的原理、特性介绍,我们将在后续的分享中另行展开。


写在最后


欢迎大家一起来尝试 Ant Design Mini 的跨平台能力,你可以在 issue 区提出宝贵的建议与 Bug 反馈。


官网: mini.ant.design/


国内镜像:ant-design-mini.antgroup.com/


作者:支付宝体验科技
来源:juejin.cn/post/7311603519570952246
收起阅读 »

大厂前端开发规定,你也能写成诗一样的代码(保姆级教程)

web
BEM 使用起来很多人不晓得BEM是什么东西 我来解释给你们听  BEM是一种前端开发的命名方法论,全称为Block-Element-Modifier,中文翻译为块、元素、修饰符。BEM的主要思想是将页面分解成多个可重用、独立的模块,每个模块由一个主块(Blo...
继续阅读 »

BEM 使用起来

很多人不晓得BEM是什么东西 我来解释给你们听

  BEM是一种前端开发的命名方法论,全称为Block-Element-Modifier,中文翻译为块、元素、修饰符。BEM的主要思想是将页面分解成多个可重用、独立的模块,每个模块由一个主块(Block)和零个或多个元素(Element)组成,可以使用修饰符(Modifier)来描述模块的不同状态和变化。

BEM的命名规则如下:

  • 块(Block):一个独立的、可重用的组件,通常由一个或多个元素组成,并且有一个命名空间作为标识符。通常以单个单词命名,使用连字符分割单词,例如:menu、button、header等。
  • 元素(Element):块的组成部分,不能独立存在,必须隶属于某个块。元素的命名使用两个连字符“__”与块名分隔,例如:menu__item、button__text、header__logo等。
  • 修饰符(Modifier):描述块或元素的某种状态或变化,以单个单词或多个单词组成,使用两个连字符“--”与块或元素名分隔。例如:menu--horizontal、button--disabled、header__logo--small等。

  通过使用BEM命名方法论,可以实现更好的代码复用性、可维护性和可扩展性。BEM的命名规则清晰明了,易于理解和使用,可以有效地提高团队开发效率和代码质量。

page+ hd/bd/ft 用起来

"page+ hd/bd/ft" 是一种简化的命名约定,常用于网页布局中。下面是对这些缩写的解释:

  • page:页面的整体容器,表示整个页面的最外层包裹元素。
  • hd:代表页头(header),用于放置页面的标题、导航栏等顶部内容。
  • bd:代表页体(body),用于放置页面的主要内容,如文章、图片、表格等。
  • ft:代表页脚(footer),用于放置页面的底部内容,如版权信息、联系方式等。

  这种命名约定的好处是简洁明了,可以快速理解页面的结构和布局。通过将页面划分为页头、页体和页脚,可以更好地组织和管理页面的各个部分,提高代码的可读性和可维护性。

更好的使用工具(stylus插件)

,Stylus 是一种 CSS 预处理器,它允许你使用更加简洁、优雅的语法编写 CSS。通过在命令行中运行 npm i -g stylus 命令,你可以在全局范围内安装 Stylus,并开始使用它来编写样式文件。 .styl 是 Stylus 文件的扩展名,你可以使用 Stylus 编写样式规则。然后,你可以将这些编写好的 Stylus 文件转换为普通的 CSS 文件,以便在网页中使用。

  具体地说,你可以创建一个名为 common.styl 的文件,并在其中编写 Stylus 样式规则。然后,通过运行 stylus -w common.styl -o common.css 命令,你可以让 Stylus 监听 common.styl 文件的变化,并自动将其编译为 common.css 文件。

  以下是一份示例代码来说明这个过程:

  1. 创建 common.styl 文件,并在其中编写样式规则:
// common.styl
$primary-color = #ff0000

body
font-family Arial, sans-serif
background-color $primary-color

h1
color white
  1. 打开终端,进入 common.styl 文件所在的目录,运行以下命令:
Copy Code
stylus -w common.styl -o common.css

  这将启动 Stylus 监听模式,并将 common.styl 文件编译为 common.css 文件。每当你在 common.styl 文件中进行更改时,Stylus 将自动重新编译 common.css 文件,以反映出最新的样式更改。 请注意,为了运行上述命令,你需要先在全局范围内安装 Stylus,可以使用 npm i -g stylus 命令进行安装。

stylus的优点

  Stylus 作为一种 CSS 预处理器,在实际开发中有以下几个优点:

  1. 更加简洁、优雅的语法:Stylus 的语法比原生 CSS 更加简洁,可以让我们更快地编写样式规则,同时保持代码的可读性和可维护性。
  2. 变量和函数支持:Stylus 支持变量和函数,可以提高样式表的重用性和可维护性。通过使用变量和函数,我们可以在整个样式表中轻松更改颜色、字体等属性,而无需手动修改每个样式规则。
  3. 混合(Mixins)支持:Stylus 的混合功能允许我们将一个样式规则集合包装在一个可重用的块中,并将其应用于多个元素。这可以大大简化样式表的编写,并减少重复代码。
  4. 自动前缀处理:Stylus 可以自动添加适当的浏览器前缀,以确保样式规则在不同的浏览器中得到正确的渲染。
  5. 非常灵活的配置:Stylus 提供了非常灵活的配置选项,可以根据项目的需要启用或禁用不同的功能,例如自动压缩、源映射等。

  总之,Stylus 通过提供更加简洁、灵活的语法和功能,可以使我们更加高效地编写 CSS 样式表,并提高代码的可重用性和可维护性。

最后一个大招阿里的适配神器 flexible.js

  flexible.js 是一款由阿里巴巴的前端团队开发的移动端适配解决方案。它通过对 Viewport 的缩放和 rem 单位的使用,实现了在不同设备上的自适应布局。

具体来说,flexible.js 主要包括以下几个步骤:

  1. 根据屏幕的宽度计算出一个缩放比例,并将该值设置到 Viewport 的 meta 标签中。
  2. 计算出 1rem 对应的像素值,并将其动态设置到 HTML 元素的 font-size 属性中。
  3. 在 CSS 中使用 rem 单位来定义样式规则。这些规则会自动根据 HTML 元素的 font-size 属性进行适配。

  通过这种方式,我们可以实现在不同设备上的自适应布局。具体来说,我们只需要在 CSS 中使用 rem 单位来定义样式规则,而不需要关注具体的像素值。当页面在不同设备上打开时,flexible.js 会自动根据屏幕宽度和像素密度等信息进行适配,从而保证页面的布局和样式在不同设备上都可以得到正确的显示。

  需要注意的是,flexible.js 并不能完全解决移动端适配的所有问题,还有一些特殊情况需要我们手动处理。例如,一些图片或者 Canvas 等元素可能需要根据不同设备的像素密度进行缩放,而这些操作需要我们手动实现。不过,flexible.js 可以帮助我们简化移动端适配的工作,提高开发效率。

  下期我来教大家手动写适配器 喜欢的来个关注 点赞 这个也是以后写文章的动力所在 谢谢大家能观看我的文章 咱下期在见 拜拜


作者:扯蛋438
来源:juejin.cn/post/7303126570323443722

收起阅读 »

JS问题:简单的console.log不要再用了!试试这个

web
前端功能问题系列文章,点击上方合集↑ 序言 大家好,我是大澈! 本文约1500+字,整篇阅读大约需要3分钟。 本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。 1. 需求分析 一...
继续阅读 »

前端功能问题系列文章,点击上方合集↑


序言


大家好,我是大澈!


本文约1500+字,整篇阅读大约需要3分钟。


本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。


1. 需求分析


一般情况下,我们在项目中进行代码调试时,往往只会在逻辑中使用console.log进行控制台打印调试。


这种方式虽然比较常规直接,但是如果打印数据多了,就会导致你的控制台消息变得异常混乱。


所以,我们有了更好的选择,那就是console对象提供的其它API,来让我们能够更清晰的区分打印信息。


图片


2. 实现步骤


2.1 console.warn


当我们需要区分一些比较重要的打印信息时,可以使用warn进行警告提示。


图片



2.2 console.error


当我们需要区分一些异常错误的打印信息时,可以使用error进行错误提示。


图片


2.3 console.time/timeEnd


想看看一段代码运行需要多长时间,可以使用time


这对于需要一些时间的CPU密集型应用程序非常有用,例如神经网络或 HTML Canvas读取。


下面执行这段代码:


console.time("Loop timer")
for(let i = 0; i < 10000; i++){
    // Some code here
}
console.timeEnd("Loop timer")


结果如下:图片


2.4 console.trace


想看看函数的调用顺序是怎样的吗?可以使用trace


下面执行这段代码:



  function trace(){
    console.trace()
  }
  function randomFunction(){
      trace();
  }
  randomFunction()


setup中,randomFunction 调用trace,然后又调用console.trace


因此,当您调用 randomFunction 时,您将得到类似的输出,结果如下:


图片


2.5 console.group/groupEnd


当我们需要将一类打印信息进行分组时,可以使用group


下面执行这段代码:


console.group("My message group");

console.log("Test2!");
console.log("Test2!");
console.log("Test2!");

console.groupEnd()

结果如下:


图片



2.6 console.table


在控制台中打印表格信息,可以使用table


对!你没听错,就是让我们以表格形式展示打印信息。


如果使用log打印:


var person1 = {name: "Weirdo", age : "-23", hobby: "singing"}
var person2 = {name: "SomeName", age : "Infinity", hobby: "programming"}

console.log(person1, person2);

结果如下:


这样做是不是让数据看起来很混乱。


图片


反之,如果我们使用table输出:


var person1 = {name: "Weirdo", age : "-23", hobby: "singing"}
var person2 = {name: "SomeName", age : "Infinity", hobby: "programming"}

console.table({person1, person2})

结果如下:


怎么样!从来不知道控制台可以看起来如此干净,对吧!


图片


2.7 console.clear


最后,使用clear把控制台清空吧!


图片


3. 问题详解


3.1 可以自定义log的样式吗?


答案当然是可以的,只需要借助%c这个占位符。


%c 是console的占位符,用于指定输出样式或应用 CSS 样式到特定的输出文本。


但请注意,%c 占位符只在部分浏览器中支持,如 Chrome、Firefox 等。


通过使用 %c 占位符,可以在 console.log 中为特定的文本应用自定义的 CSS 样式。这样可以改变输出文本的颜色、字体、背景等样式属性,以便在控制台中以不同的样式突出显示特定的信息。


以下是使用%c 占位符应用样式的示例:


console.log("%c Hello, World!", 
  "color: red; font-weight: bold;border1px solid red;");

结果如下:


图片


通过使用 %c 占位符和自定义的样式规则,可以在控制台输出中以不同的样式突出显示特定的文本,使得输出更加清晰和易于识别。


这在调试和日志记录过程中非常有用,特别是当需要突出显示特定类型的信息或错误时。


结语


建立这个平台的初衷:



  • 打造一个仅包含前端问题的问答平台,让大家高效搜索处理同样问题。

  • 通过不断积累问题,一起练习逻辑思维,并顺便学习相关的知识点。

  • 遇到难题,遇到有共鸣的问题,一起讨论,一起沉淀,一起成长。

作者:程序员大澈
来源:juejin.cn/post/7310102466570321958
收起阅读 »