跳到主要内容

· 3 分钟阅读

一、使用过程中遇到的问题

参考:在项目中使用 vuepress 搭建组件文档踩坑

1. 使用scss

  1. npm install -D node-sass sass-loader
  2. 启动报错,高版本的node-sass,sass-loader与 vuepress 版本并不兼容,可行的版本是:node-sass@4.14.1sass-loader@7.3.1

2. 引入组件库

以element-ui为例

  1. npm install element-ui
  2. 使用enhanceApp.js
// 示例
export default ({
Vue, // VuePress 正在使用的 Vue 构造函数
options, // 附加到根实例的一些选项
router, // 当前应用的路由实例
siteData // 站点元数据
}) => {
// ...做一些其他的应用级别的优化
}

// 使用
// docs/.vuepress/enhanceApp.js
import Element from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

export default ({ Vue, options, router }) => {
Vue.use(Element);
};
  1. 上面引入组件库后打包报错:window is not defined
    info

    vuepress是如何工作的:

    • 一个 VuePress 网站是一个由 Vue、Vue Router和 webpack 驱动的单页应用。
    • 在构建时,vuepress会为应用创建一个服务端渲染(SSR)的版本,然后通过虚拟访问每一条路径来渲染对应的HTML。
import 'element-ui/lib/theme-chalk/index.css';

export default async ({ Vue, options, router, isServer }) => {
if (!isServer) { // 客户端
let Element = await import('element-ui');
Vue.use(Element.default);
}
}

// 使用async-await,避免异步加载第三方依赖包,导致页面渲染会出错
  1. 引入组件库后启动报错:Cannot find module 'core-js/library/fn/xxx/xxx'

    原因应该是 UI 组件中依赖的core-js包和 vuepress 所依赖的core-js包版本不兼容造成的。

// 配置chainWebpack解决该问题
// docs/.vuepress/config.js

chainWebpack: config => {
config.resolve.alias.set('core-js/library/fn', 'core-js/features')
},

二、主题theme

info
  • vuepress的入口文件为主题的Layout.vue
  • 想自定义主题则新建docs/.vuepress/theme/layouts/Layout.vue

1. 继承主题

假设你想创建一个继承自 VuePress 默认主题的派生主题,你只需要在你的主题配置中配置 extend 选项:

// .vuepress/theme/index.js
module.exports = {
extend: '@vuepress/theme-default'
}

2. 组件覆盖

  • 父主题的所有能力都会"传递"给子主题,对于文件级别的约定,子主题可以通过在同样的位置创建同名文件来覆盖它
  • 子主题可以通过@theme 别名访问父主题的组件,比如:当子主题中有Navbar.vue时,如下:
theme
└── components
└── Navbar.vue
info

@theme/components/Navbar.vue 会自动地映射到子主题中的 Navbar 组件,当子主题移除这个组件时,@theme/components/Navbar.vue 又会自动恢复为父主题中的 Navbar 组件。

· 0 分钟阅读

· 81 分钟阅读

文件上传

单文件上传

<div className="empty">
<input
className="uploader-wrapper__input"
type="file"
<!-- capture="environment" 直接调起后置摄像头 capture="user" 直接调起前置摄像头 -->
accept="image/jpg,image/jpeg,image/gif,image/png"
onChange={handleFileChange}
/>
<img src={cameraIcon} />
<div>上传照片</div>
</div>
.empty {
width: 186px;
height: 186px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
&>img {
width: 56px;
height: 56px;
}
position: relative;
.uploader-wrapper__input {
position: absolute !important;
top: 0;
left: 0;
width: 100% !important;
height: 100% !important;
overflow: hidden;
cursor: pointer !important;
opacity: 0;

&:disabled {
cursor: not-allowed;
}
}
}
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
// loading
setShowLoadOverlay(true);
const $el = event.target;
const { files } = $el;
if (files && files[0]) {
const formData = new FormData();
formData.append('file', files[0]);
axios.post(import.meta.env.VITE_UPLOAD_URL, formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
}).then(response => {
setShowLoadOverlay(false);
// 上传成功后的业务逻辑
// ...
}).catch(error => {
console.error('上传失败', error);
Toast.show({
content: '上传失败',
});
setShowLoadOverlay(false);
});
} else {
setShowLoadOverlay(false);
}
}

多文件上传

<div className="empty">
<input
className="uploader-wrapper__input"
type="file"
<!-- capture="environment" 直接调起后置摄像头 capture="user" 直接调起前置摄像头 -->
accept="image/jpg,image/jpeg,image/gif,image/png"
multiple
onChange={handleFileChange}
/>
<img src={cameraIcon} />
<div>上传照片</div>
</div>
.empty {
width: 186px;
height: 186px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
&>img {
width: 56px;
height: 56px;
}
position: relative;
.uploader-wrapper__input {
position: absolute !important;
top: 0;
left: 0;
width: 100% !important;
height: 100% !important;
overflow: hidden;
cursor: pointer !important;
opacity: 0;

&:disabled {
cursor: not-allowed;
}
}
}
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
// loading
setShowLoadOverlay(true);
const $el = event.target;
const { files } = $el;
if (files) {
const formData = new FormData();
for (let i=0; i<files.length; i++) {
formData.append(i, files[i]);
}
axios.post(import.meta.env.VITE_UPLOAD_URL, formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
}).then(response => {
setShowLoadOverlay(false);
// 上传成功后的业务逻辑
// ...
}).catch(error => {
console.error('上传失败', error);
Toast.show({
content: '上传失败',
});
setShowLoadOverlay(false);
});
} else {
setShowLoadOverlay(false);
}
}

图片压缩

/**
* 图片压缩
* @param files 原始图片file列表
* @returns 压缩后的图片file列表
* 注意:图片越大压缩效果越明显,1kb以下可能会出现压缩后size变大的情况
*/
export const compressImg = async (files: FileList) => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d') as CanvasRenderingContext2D;
const base64 = await fileToDataURL(files[0]);
const img = await dataURLToImage(base64);
canvas.width = img.width;
canvas.height = img.height;
context.clearRect(0, 0, img.width, img.height);
context.drawImage(img, 0, 0, img.width, img.height);
const blob = (await canvastoFile(canvas, 'image/jpeg', 0.5)) as Blob;
const compressedFile = await new File([blob], files[0].name, { type: files[0].type });
return [compressedFile];
}
const fileToDataURL = (file: Blob): Promise<any> => {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onloadend = (e) => resolve((e.target as FileReader).result)
reader.readAsDataURL(file)
})
}
const dataURLToImage = (dataURL: string): Promise<HTMLImageElement> => {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve(img)
img.src = dataURL
})
}
const canvastoFile = (
canvas: HTMLCanvasElement,
type: string,
quality: number
): Promise<Blob | null> => {
return new Promise((resolve) => {
canvas.toBlob((blob) => resolve(blob), type, quality)
})
}

使用示例:

async function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
// loading
setShowLoadOverlay(true);
const $el = event.target;
const { files } = $el;
if (files && files[0]) {
// 若图片尺寸超过100kb则压缩
let compressedFiles = files[0].size > 102400 ? await compressImg(files) : files;
const formData = new FormData();
formData.append('file', compressedFiles[0]);
axios.post(import.meta.env.VITE_UPLOAD_URL, formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
}).then(response => {
setShowLoadOverlay(false);
// 上传成功后的业务逻辑
// ...
}).catch(error => {
console.error('上传失败', error);
Toast.show({
content: '上传失败',
});
setShowLoadOverlay(false);
});
} else {
setShowLoadOverlay(false);
}
}

H5

H5页面适配

移动端H5开发之页面适配篇

postcss-pxtorem 总是搭配 amfe-flexible 一起使用

两个库的搭配使用,将页面上的元素某些属性以相对于根元素的倍数来进行展示,从而适配不同的屏幕大小。

pnpm add -D postcss-pxtorem
pnpm add amfe-flexible
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import postCssPxToRem from 'postcss-pxtorem';

export default defineConfig({
plugins: [
react(),
],
css: {
postcss: {
plugins: [
postCssPxToRem({
rootValue: 75, // 设计稿宽度750px
propList: ['*'], // 所有px都转换成rem
selectorBlackList: ['nut-'], // 忽略选择器为'nut-'开头的元素的px
})
]
}
},
})
main.tsx
import 'amfe-flexible';

rem是相对于html元素字体单位的一个相对单位,从本质上来说,它属于一个字体单位,用字体单位来布局,并不是太合适

postcss-px-to-viewport

将px单位转换为视口单位的 (vw, vh, vmin, vmax) 的 PostCSS 插件.

安装:yarn add -D postcss-px-to-viewport

vite项目中配置:

vite.config.js
import { defineConfig } from 'vite' 
import vue from '@vitejs/plugin-vue'
import postcsspxtoviewport from 'postcss-px-to-viewport'
export default defineConfig({
plugins: [
vue()
],
css: {
postcss: {
plugins: [
postcsspxtoviewport({
unitToConvert: 'px', // 要转化的单位
viewportWidth: 750, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
selectorBlackList: ['ignore-'], // 指定不转换为视窗单位的类名,
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
landscape: false // 是否处理横屏情况
})
]
}
}
})

配置说明:

  • propList: 当有些属性的单位我们不希望转换的时候,可以添加在数组后面,并在前面加上!号,如propList: ["*","!letter-spacing"],这表示:所有css属性的属性的单位都进行转化,除了letter-spacing
  • selectorBlackList:转换的黑名单,在黑名单里面的我们可以写入字符串,只要类名包含有这个字符串,就不会被匹配。比如selectorBlackList: ['wrap'],它表示形如wrap,my-wrap,wrapper这样的类名的单位,都不会被转换

使用注释忽略转换:

  • /* px-to-viewport-ignore-next */ 用在单独一行,防止下一行被转换
  • /* px-to-viewport-ignore */ 用在属性后面,防止同一行被转换
/* example input: */
.class {
/* px-to-viewport-ignore-next */
width: 10px;
padding: 10px;
height: 10px; /* px-to-viewport-ignore */
border: solid 2px #000; /* px-to-viewport-ignore */
}

/* example output: */
.class {
width: 10px;
padding: 3.125vw;
height: 10px;
border: solid 2px #000;
}

抓包工具

  • Charles

H5调试工具

  1. vconsole

  2. eruda 通过 url 参数来控制是否加载调试器,即当前端拼接了eruda=true参数的时候,才会引入对应的js文件

;(function () {
var src = '//cdn.jsdelivr.net/npm/eruda';
if (!/eruda=true/.test(window.location) && localStorage.getItem('active-eruda') != 'true') return;
document.write('<scr' + 'ipt src="' + src + '"></scr' + 'ipt>');
document.write('<scr' + 'ipt>eruda.init();</scr' + 'ipt>');
})();

真机本地调试

whistle

  1. 安装:sudo npm install -g whistle
  2. 手机和PC保持在同一网络下(比如连到同一个Wi-Fi或手机热点)
  3. 命令行输入w2 start启动whistle,PC端可以访问http://127.0.0.1:8899/查看抓包记录
  4. 手机配置代理:点击连接的Wi-Fi或手机热点--->配置代理选择手动,服务器填入whistle启动后提示的ip,端口填入默认的8899
  5. 手机配置代理后,PC端访问http://127.0.0.1:8899/,点击HTTPS会弹出一个证书二维码,使用手机扫码器扫描该二维码下载证书
  6. 证书下载后打开手机的设置--->通用--->VPN与设备管理--->安装下载的证书(描述文件)
  7. 安装后就可以真机本地调试https链接了

whistle功能很强大,如图: whistle 常用命令:w2 start w2 restart w2 stop

spy-debugger

  1. 安装:sudo npm install spy-debugger -g
  2. 手机和PC保持在同一网络下(比如连到同一个Wi-Fi或手机热点)
  3. 命令行输入spy-debugger,按命令行提示用浏览器打开相应地址。
  4. 设置手机连接网络的HTTP代理,代理IP地址设置为PC的IP地址,端口为spy-debugger的启动端口(默认端口:9888)。
  5. 手机安装证书。注:手机必须先设置完代理后再通过手机扫码器(非微信)扫如下二维码安装证书(二维码地址)(注意:手机首次调试需要安装证书,已安装了证书的手机无需重复安装。iOS新安装的证书需要手动打开证书信任)。
  6. 用手机浏览器访问你要调试的页面即可。

Vorlon.JS

二维码生成器

  • 草料二维码
  • Chrome插件:Quick QR二维码生成器

使用nutui-react开发地址选择

  • 使用 Cascader import { Cascader } from '@nutui/nutui-react';

  • 新建地址Cascader和编辑地址Cascader如果放在一个自定义组件里则Cascader会回显不了(原因待探索),所以把编辑地址Cascader单独放到一个自定义的组件里

    function AddressBook() {
    const [visible, setVisible] = useState(false);
    const [addressValueList, setAddressValueList] = useState<string[]>([]);
    const [addressInfoList, setAddressInfoList] = useState<any[] | null>(null);

    const cascaderOptionKey = {
    textKey: 'addressId',
    valueKey: 'addressId',
    childrenKey: 'children',
    };

    async function lazyLoadAddress(node: any, resolve: (children: any) => void) {
    if (node.root) { // 首次加载
    let firstLevelAddressList = await getAddressById('0');
    resolve(firstLevelAddressList);
    } else {
    const { addressId } = node;
    let nextLevelAddressList = await getAddressById(addressId);
    resolve(nextLevelAddressList);
    }
    }
    function handleAddressValueChange(value: any, path: any) {
    setAddressValueList(value);
    setAddressInfoList(path);
    }

    return <>
    <div onClick={() => setVisible(true)} className="address-wrapper">
    <div className={addressValueList.length ? 'edit-address' : 'add-address'}>{addressValueList.length ? addressValueList.join('') : '请选择省市区县、乡镇等'}</div>
    {
    !!addressIdValue && addressValueList.length>0
    ?
    <EditAddressPicker
    isVisible={visible}
    defaultAddressList={addressValueList}
    optionKey={cascaderOptionKey}
    fetchData={lazyLoadAddress}
    onChangeAddressValue = {(value: any, path: any) => handleAddressValueChange(value, path)}
    onClosePicker={() => {setVisible(false)}}
    ></EditAddressPicker>
    :
    <Cascader
    visible={visible}
    value={addressValueList}
    title="地址选择"
    closeable={false}
    lazy
    optionKey={cascaderOptionKey}
    onChange={handleAddressValueChange}
    onLoad={lazyLoadAddress}
    onClose={() => {setVisible(false)}}
    />
    }
    </div>
    </>
    }
    interface IAddressPicker {
    isVisible: boolean,
    defaultAddressList: string[],
    optionKey: any,
    fetchData: (arg0: any, arg1: any) => void,
    onChangeAddressValue: (arg0: any, arg1: any) => void,
    onClosePicker: () => void,
    }

    function EditAddressPicker({
    isVisible,
    defaultAddressList,
    optionKey,
    fetchData,
    onChangeAddressValue,
    onClosePicker,
    }: IAddressPicker) {

    function handleAddressValueChange(value: any, path: any) {
    onChangeAddressValue(value, path);
    }

    return <>
    <Cascader
    visible={isVisible}
    defaultValue={defaultAddressList}
    title="地址选择"
    closeable={false}
    lazy
    optionKey={optionKey}
    onLoad={fetchData}
    onChange={handleAddressValueChange}
    onClose={onClosePicker}
    />
    </>
    }

浏览器模拟器造成 React中点击父元素会触发子元素的点击事件 的假象

以下代码在浏览器模拟器上点击img标签或agree-wrapper元素的非span区域,会触发跳转页面。但是,在真机上不会有这种现象。

import { useNavigate, useSearchParams } from "react-router-dom";

function App() {
const [agreeFlag, setAgreeFlag] = useState(false);

const navigate = useNavigate();

function handleChangeAgreeFlag() {
setAgreeFlag(!agreeFlag);
}
function linkAgreement(event: any) {
event.stopPropagation();
navigate('/agreement');
}

return <>
<div className="footer">
<div className="agree-wrapper" onClick={handleChangeAgreeFlag}>
<img src={agreeFlag ? checkedIcon : uncheckIcon} />
<div>请仔细阅读<span onClick={(event)=>linkAgreement(event)}>《操作说明》</span>,以便后续操作</div>
</div>
<div className="btn" onClick={handleSave}>确定</div>
</div>
</>
}
.footer {
position: fixed;
bottom: 0;
padding-bottom: constant(safe-area-inset-bottom); /* 兼容 iOS < 11.2 */
padding-bottom: env(safe-area-inset-bottom); /* 兼容 iOS >= 11.2 */
width: 100%;
height: 230px;
background-color: #FFFFFF;
.btn {
background-image: linear-gradient(90deg, #FF6B22 0%, #FF8727 100%);
border-radius: 45px;
width: 686px;
height: 90px;
margin: 0 auto;
font-size: 32px;
color: #FFFFFF;
letter-spacing: 0;
text-align: center;
line-height: 44px;
font-weight: 600;
padding: 23px 0;
box-sizing: border-box;
}
.agree-wrapper {
display: flex;
align-items: center;
padding: 24px 0 30px 30px;
&>img {
width: 30px;
height: 30px;
}
&>div {
font-size: 24px;
color: #222222;
letter-spacing: 0;
font-weight: 400;
margin-left: 10px;
&>span {
color: #ff6300;
}
}
}
}

小程序

支付宝小程序

1. 联调设置

用于开发版跳转到另外的小程序再跳回来的场景

微信小程序

上报实时日志

实时日志目前只支持在手机端测试。工具端的接口可以调用,但不会上报到后台。

微信小程序嵌入H5页面打开慢的问题

微信小程序嵌入H5页面打开慢的问题,可以从多个角度进行优化。以下是一些常见的优化策略:

  1. 优化H5页面本身
  • 减少资源体积:压缩CSS、JavaScript和图片文件。使用WebP格式替代传统的图片格式,因为WebP提供了更好的压缩率。
  • 优化加载顺序:确保关键的CSS和JavaScript尽早加载。可以使用<link rel="preload">预加载关键资源。
  • 减少HTTP请求:合并CSS和JavaScript文件,减少页面加载时的HTTP请求次数。
  • 使用CDN:将资源放在CDN上,可以加快资源的加载速度,因为CDN可以提供更靠近用户的服务器来响应请求。
  • 懒加载:对于非首屏的图片和内容,可以采用懒加载的方式,等到用户滚动到相应位置时再加载。
  1. 优化小程序与H5的交互
  • 减少数据传输:在小程序和H5页面之间传输的数据量越小,性能越好。尽量减少不必要的数据传输。
  • 使用缓存:对于一些不经常变化的数据,可以在小程序端或H5端使用缓存,避免每次都从服务器获取。
  1. 服务器端优化
  • 开启Gzip压缩:在服务器端配置Gzip压缩,可以显著减少传输的数据量。
  • 优化后端性能:确保后端API响应速度快。可以通过优化数据库查询、使用更快的服务器、增加缓存等方式来提升后端性能。
  • 使用HTTP/2:相比于HTTP/1.x,HTTP/2提供了更高的效率,如服务器推送、头部压缩等特性,可以进一步提升页面加载速度。
  1. 其他
  • 预加载页面:如果可以预测用户的行为,可以提前在小程序中加载H5页面,当用户实际需要时,页面已经加载完成,可以立即显示。
  • 监控性能:使用性能监控工具(如Lighthouse、WebPageTest等)定期检测H5页面的加载性能,及时发现并解决问题。

通过上述方法的综合应用,可以显著提升微信小程序嵌入H5页面的打开速度,改善用户体验。

SSR(服务器端渲染)也可以在一定程度上解决微信小程序嵌入H5页面打开慢的问题,主要通过以下几个方面:

  1. 提升首屏加载速度

服务器端渲染可以直接生成页面的HTML内容,这意味着浏览器可以在下载HTML后立即开始渲染页面,而不需要等待所有的JavaScript都下载并执行完成。这对于提升首屏加载速度非常有效,尤其是在网络条件不佳或设备性能较低的情况下。

  1. 减少白屏时间

由于服务器端渲染的页面在服务器上已经生成了最终的HTML,用户在请求页面时可以更快地看到页面内容,这减少了用户面对空白屏幕的时间,从而提升了用户体验。

  1. 减轻客户端负担

服务器端渲染将部分渲染工作从客户端转移到服务器端,这样可以减轻客户端(在这里指的是微信小程序内嵌的浏览器)的计算负担,特别是对于那些性能较弱的设备,可以显著提升渲染效率。

  1. SEO优化

虽然这一点对于微信小程序嵌入H5页面的性能优化不直接相关,但值得一提的是,SSR对于改善网页的搜索引擎优化(SEO)也非常有帮助。由于搜索引擎更容易抓取和索引预渲染的内容,这对于需要提升搜索引擎可见度的H5页面来说是一个额外的好处。

实施注意事项:

  • 服务器负载:由于服务器端渲染需要服务器进行额外的计算工作,这可能会增加服务器的负载。因此,在实施SSR时,需要考虑服务器资源和负载情况,适当进行优化和扩展。
  • 开发复杂度:相比于客户端渲染,服务器端渲染可能会增加开发的复杂度,特别是在处理数据预取、路由管理等方面。因此,需要权衡其带来的性能提升和开发成本。

总的来说,SSR能够有效提升微信小程序嵌入H5页面的加载速度和用户体验,但同时也需要考虑到实施SSR可能带来的服务器负载增加和开发复杂度提升等问题。

APP拉起小程序

官方文档

考虑到部分场景下 APP 需要通过小程序来承载服务,为此 OpenSDK 提供了移动应用(APP)拉起小程序功能。移动应用(APP)接入此功能后,用户可以在 APP 中跳转至微信某一小程序的指定页面,完成服务后再跳回至原 APP 。

小程序转发朋友和分享朋友圈不可用

只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮。文档:https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html#onShareAppMessage-Object-object

只有定义了此事件处理函数,右上角菜单才会显示“分享到朋友圈”按钮。文档:https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html#onShareTimeline

antv使用记录

使用@ant-design/plots,文档也可以参考g2的文档(g2 API),底层用的是g2

1. 自定义tooltip

  • 问题:闪烁并且有的地方不显示tooltip,解决方案:设置tooltip的position,position: 'top'

2. 图表标注

2.1 辅助线

API:Line Annotation

  • type: 'line', 标识为:辅助线(可带文本)
  • start 起始位置
  • end 结束位置
    info

    起始位置、结束位置 除了指定原始数据之外,还可以使用预设定数据点,如:

    • 'min': 最小值
    • 'max': 最大值
    • 'mean': 平均值
    • 'median': 中位值
    • 'start': 即 0
    • 'end': 即 1

2.2 辅助文本

API:Text Annotation

  • type: 'text', 标识为:辅助文本,在指定位置添加文本说明
  • position 文本标注位置

ant design使用记录

1. formselect结合使用,设置selectvalue不生效

被设置了 name 属性的 Form.Item 包装的控件,表单控件会自动添加 value(或 valuePropName 指定的其他属性) onChange(或 trigger 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:

  • 你不再需要也不应该用 onChange 来做数据收集同步(你可以使用 FormonValuesChange),但还是可以继续监听 onChange 事件。

  • 你不能用控件的 valuedefaultValue 等属性来设置表单域的值,默认值可以用 Form 里的 initialValues 来设置。注意 initialValues 不能被 setState 动态更新,你需要用 setFieldsValue 来更新。

  • 你不应该用 setState,可以使用 form.setFieldsValue 来动态改变表单值。

    form.setFieldsValue({
    字段名:,
    })

2. 表单最后一行右对齐适配电脑分辨率

使用Col嵌套Row

<Form
form={form}
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
initialValues={{ remember: true }}
className='form-container'
>
<div className='content'>
<Row>
<Col span={8}>
<Form.Item
label="AAA"
labelCol={{span: 5}}
name="aaa"
>
<Select
mode="multiple"
style={{width:300,fontSize:11,borderRadius:3}}
size='small'
showArrow={true}
showSearch={false}
allowClear={true}
onChange={(seletedData) => handleCommonChange(seletedData,'aaa')}
>
{
[1,2,3,4].map((item,index) => {
return <Option key={index} value={item}>{item}</Option>
})
}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label="BBB"
labelCol={{span: 4}}
name="bbb"
>
<Select
mode="multiple"
style={{width:300,fontSize:11,borderRadius:3}}
size='small'
showArrow={true}
showSearch={false}
allowClear={true}
onChange={(seletedData) => handleCommonChange(seletedData,'bbb')}
>
{
[1,2,3,4].map((item,index) => {
return <Option key={index} value={item}>{item}</Option>
})
}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label="CCC"
labelCol={{span: 4}}
name="ccc"
>
<Select
mode="multiple"
style={{width:300,fontSize:11,borderRadius:3}}
size='small'
showArrow={true}
showSearch={false}
allowClear={true}
onChange={(seletedData) => handleCommonChange(seletedData,'ccc')}
>
{
[1,2,3,4].map((item,index) => {
return <Option key={index} value={item}>{item}</Option>
})
}
</Select>
</Form.Item>
</Col>
</Row>
<Row>
<Col span={8} offset={16}>
<Row>
<Col span={4}></Col>
<Col span={16}>
<div style={{width:'300px',textAlign:'right'}}>
<Button className='reset-btn' onClick={handleReset}>重置</Button>
<Button className='search-btn' onClick={handleSearch}>查询</Button>
</div>
</Col>
</Row>
</Col>
</Row>
</div>
</Form>

3. 组件的方法自定义传参

  • 比如,DatePickeronChange
const handleDateChange = (_date:unknown,dateString:string,type:string) => {
// type是自定义入参,date、dateString是onChange自带的入参
}

<DatePicker
size='small'
style={{width:300,fontSize:11,borderRadius:3}}
placeholder='请选择日期'
onChange={(date,dateString) => handleDateChange(date,dateString,'beginTime')}
/>

4. Table组件的数据源需要有key这个属性

否则虽然不影响使用但是控制台报错提示

const resultWithKey = res.result.reduce((acc: any[],cur: { key: number; },ind:number) => {
cur.key = ind;
acc.push(cur);
return acc;
}, []);
setTableDatas(resultWithKey); // Table的数据源需要有key prop,否则不影响使用但控制台会报错提示

5. Table组件列特别多时,设置列宽不生效

注意设置 scroll={{ x: '4000px' }} 这个x的宽度首先要能够容纳所有设置的列宽之和('4000px'只是举例),这样在这个总的宽度之内去设置列宽,才能生效。

6. Table组件怎么动态控制勾选框的勾中与取消

在 Ant Design 的 Table 组件中,可以使用 rowSelection 属性来动态控制行的勾选状态。rowSelection 属性接受一个对象,该对象可以包含 selectedRowKeysonChange 等属性来管理选中的行。

以下是一个示例,展示了如何动态控制 Table 中勾选框的勾中与取消:

import React, { useState } from 'react';
import { Table, Button } from 'antd';

const App = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);

const data = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
},
];

const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
];

const rowSelection = {
selectedRowKeys,
onChange: (selectedRowKeys) => {
setSelectedRowKeys(selectedRowKeys);
},
};

const toggleSelection = (key) => {
setSelectedRowKeys((prevSelectedRowKeys) => {
if (prevSelectedRowKeys.includes(key)) {
return prevSelectedRowKeys.filter((selectedKey) => selectedKey !== key);
} else {
return [...prevSelectedRowKeys, key];
}
});
};

return (
<div>
<Button onClick={() => toggleSelection('1')}>Toggle Selection for Row 1</Button>
<Button onClick={() => toggleSelection('2')}>Toggle Selection for Row 2</Button>
<Button onClick={() => toggleSelection('3')}>Toggle Selection for Row 3</Button>
<Table rowSelection={rowSelection} columns={columns} dataSource={data} />
</div>
);
};

export default App;

代码解释:

  1. 状态管理

    • 使用 useState 钩子来管理选中的行的 selectedRowKeys
  2. 数据和列定义

    • data 数组定义了表格的数据源。
    • columns 数组定义了表格的列。
  3. 行选择配置

    • rowSelection 对象包含 selectedRowKeysonChange 属性。
    • selectedRowKeys 用于存储当前选中的行的键。
    • onChange 回调函数在选中行变化时更新 selectedRowKeys
  4. 动态控制勾选状态

    • toggleSelection 函数用于动态控制行的选中状态。它接收一个行的 key 作为参数,并根据当前的 selectedRowKeys 来添加或移除该行的 key
  5. 按钮控制

    • 三个按钮分别用于切换对应行的选中状态。点击按钮时调用 toggleSelection 函数。
  6. Table 组件

    • Table 组件通过 rowSelection 属性来控制行的选择状态,并将 columnsdataSource 传递给表格。

通过这种方式,你可以动态地控制 Ant Design 表格中行的勾选和取消。

7. Table组件需要设置rowKey

不设置rowKey的话会控制台报错:

8. message使用注意事项

message使用报错:

Uncaught Error: Objects are not valid as a React child (found: object with keys {stack, message, name, code, config, request, response, status}). If you meant to render a collection of children, use an array instead.

The above error occurred in the <span> component:

span
div
PureContent
div
div
Notify

这个错误通常是因为你试图直接将一个对象渲染到 React 的 JSX 中,而 React 只能渲染原始类型(如字符串、数字)或 React 元素。如果你在使用 Ant Design 的 message 组件时遇到这个错误,可能是因为你试图显示一个对象而不是一个字符串。

为了避免这个错误,你需要确保传递给 message 组件的内容是一个字符串或其他可以正确渲染的类型。以下是一些具体的解决方案:确保传递字符串给 message 组件

如果你从某个操作(例如 API 请求)中获取了一个错误对象,你需要提取并格式化这个错误信息为字符串,然后再传递给 message 组件。

假设你有一个 API 请求,并且在请求失败时需要显示错误信息:

import { message } from 'antd';
import axios from 'axios';

// 示例 API 请求函数
async function fetchData() {
try {
const response = await axios.get('https://api.example.com/data');
console.log(response.data);
} catch (error) {
// 提取并格式化错误信息
const errorMessage = error.response && error.response.data && error.response.data.message
? error.response.data.message
: error.message;

// 显示错误信息
message.error(`Error: ${errorMessage}`);
}
}

// 调用函数以触发请求
fetchData();

如果你希望显示更详细的错误信息,可以将错误对象的相关信息提取并格式化为字符串:

import { message } from 'antd';
import axios from 'axios';

// 示例 API 请求函数
async function fetchData() {
try {
const response = await axios.get('https://api.example.com/data');
console.log(response.data);
} catch (error) {
// 提取并格式化错误信息
const errorMessage = error.response && error.response.data && error.response.data.message
? error.response.data.message
: error.message;

// 显示错误信息
message.error(`Error: ${errorMessage}`);

// 如果需要显示更多详细信息,可以进一步提取
console.error('Full error details:', error);
}
}

// 调用函数以触发请求
fetchData();

为了避免 Uncaught Error: Objects are not valid as a React child 错误,确保传递给 Ant Design 的 message 组件的内容是一个字符串或其他可以正确渲染的类型。通过提取和格式化错误信息,你可以确保 message 组件显示的是可读的错误消息,而不是 JavaScript 对象。

9. Upload限制上传数量

为了限制 Ant Design 的 Upload 组件只上传一张图片,你可以使用以下几种方法:

  1. 设置 maxCount 属性Upload 组件在 Ant Design 4.9.0 版本之后新增了 maxCount 属性,可以直接限制上传文件的数量。

  2. beforeUpload 中进行文件数量检查: 在 beforeUpload 属性中,你可以检查当前已经上传的文件数量,并在达到限制时阻止新的文件上传。

这里是一个完整的示例,展示了如何使用 maxCountbeforeUpload 来限制上传文件数量为1:

import React, { useState } from 'react';
import { Upload, Button, message } from 'antd';
import { UploadOutlined } from '@ant-design/icons';

const UploadComponent = () => {
const [fileList, setFileList] = useState([]);

const handleChange = (info) => {
let newFileList = [...info.fileList];

// 只保留最新上传的文件
newFileList = newFileList.slice(-1);

setFileList(newFileList);

if (info.file.status === 'done') {
message.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
};

const beforeUpload = (file) => {
if (fileList.length >= 1) {
message.error('You can only upload one file at a time');
return Upload.LIST_IGNORE;
}
return true;
};

return (
<Upload
action="/upload" // 替换为你的上传接口
fileList={fileList}
onChange={handleChange}
beforeUpload={beforeUpload}
maxCount={1} // 限制上传数量为1
>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
);
};

export default UploadComponent;

解释:

  1. fileList 状态

    • 使用 useState 管理上传的文件列表。
    • handleChange 回调中更新文件列表,确保只保留最新的一个文件。
  2. handleChange 回调

    • info.fileList 包含当前上传的所有文件。
    • 使用 slice(-1) 方法只保留最新上传的一个文件。
    • 根据文件状态显示成功或失败的消息。
  3. beforeUpload 回调

    • 在文件上传前检查当前文件列表的长度。
    • 如果文件列表长度已经达到限制(1),则阻止新的文件上传并显示错误消息。
  4. maxCount 属性

    • 直接限制上传文件的数量为1。

这个示例展示了如何使用 Ant Design 的 Upload 组件来限制用户只能上传一张图片。通过结合 maxCount 属性和 beforeUpload 回调,可以确保用户上传的文件数量不会超过限制。

10. Form中使用Upload并回显

在 Ant Design 的表单中使用 Upload 组件并实现文件上传后的回显,你可以通过设置 fileList 属性来控制上传文件列表,并在表单初始化时设置初始值来回显已上传的文件。

以下是一个完整的示例,展示如何在 Ant Design 表单中使用 Upload 组件,并在表单初始化时回显已上传的文件:

import React, { useState, useEffect } from 'react';
import { Form, Upload, Button, message, Input } from 'antd';
import { UploadOutlined } from '@ant-design/icons';

const initialFileList = [
{
uid: '-1',
name: 'example.png',
status: 'done',
url: 'https://example.com/example.png',
},
];

const UploadForm = () => {
const [form] = Form.useForm();
const [fileList, setFileList] = useState(initialFileList);

useEffect(() => {
// 设置表单的初始值,包括上传的文件列表
form.setFieldsValue({
files: fileList,
});
}, [form, fileList]);

const handleChange = (info) => {
let newFileList = [...info.fileList];

// 更新文件列表
setFileList(newFileList);

if (info.file.status === 'done') {
message.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
};

const beforeUpload = (file) => {
// 在这里可以添加文件上传前的检查逻辑
return true;
};

const onFinish = (values) => {
console.log('Form values:', values);
};

return (
<Form form={form} onFinish={onFinish}>
<Form.Item
name="files"
label="Upload"
valuePropName="fileList"
getValueFromEvent={e => e.fileList}
>
<Upload
action="/upload" // 替换为你的上传接口
fileList={fileList}
onChange={handleChange}
beforeUpload={beforeUpload}
listType="picture"
>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
</Form.Item>
<Form.Item
name="description"
label="Description"
rules={[{ required: true, message: 'Please input your description!' }]}
>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};

export default UploadForm;

解释:

  1. 初始文件列表

    • initialFileList 是一个包含已上传文件信息的数组。每个文件对象包含 uid, name, status, 和 url 属性,用于描述文件的唯一标识、文件名、上传状态和文件 URL。
  2. 表单初始化

    • 使用 useEffect 钩子在组件挂载时设置表单的初始值,包括 Upload 组件的文件列表。
  3. handleChange 回调

    • 当上传文件列表发生变化时,更新文件列表状态并根据文件状态显示相应的消息。
  4. beforeUpload 回调

    • 在文件上传前可以添加检查逻辑,当前示例中直接返回 true 允许所有文件上传。
  5. 表单项配置

    • Form.Item 使用 name="files"valuePropName="fileList" 来绑定 Upload 组件的文件列表。
    • getValueFromEvent 属性用于从事件中提取文件列表并更新表单值。
  6. 表单提交

    • onFinish 回调中可以获取表单的所有值,包括上传的文件列表。

通过这种方式,你可以在 Ant Design 表单中使用 Upload 组件,并在表单初始化时回显已上传的文件。

11. Form中动态使用Upload并回显

示例:

import { Upload, UploadFile } from "antd";
import { PlusOutlined } from '@ant-design/icons';
import { UploadFileStatus } from "antd/lib/upload/interface";
import fetchApiData from "@/api";

export default function Demo() {
const [dataInfo, setDataInfo] = useState(null);
const [uploadedFilesInfo, setUploadedFilesInfo] = useState<{[key: string]: UploadFile[]}>({});

useEffect(() => {
getData();
}, []);

const getData = async () => {
let apiResult = await fetchApiData(`/demoApi`);
if (apiResult) {
if (apiResult.formList?.length && apiResult.mapInfo) {
const imageList = apiResult.formList.filter((formItem) => formItem.type == 'image');
let obj: {[key: string]: UploadFile[]} = {};
for (let index = 0; index < imageList.length; index++) {
const imageFormItem = imageList[index];
let uploadFileList = [{
url: apiResult.mapInfo[imageFormItem.name] as string,
uid: `${apiResult.mapInfo.id+1}`,
name: `${imageFormItem.name}.png`,
status: 'done' as UploadFileStatus,
}];
obj[imageFormItem.name] = uploadFileList;
}
setUploadedFilesInfo(obj);
}
setDataInfo(apiResult);
}
}

const beforeUpload = (file: any) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
message.error('只支持上传 JPG/PNG 文件');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('图片大小必须小于 2MB');
}
return isJpgOrPng && isLt2M;
}
const handleUploadChange = (info: any, config: IFormConfig) => {
let newFiles = [...info.fileList];
newFiles = newFiles.map((file) => {
if (file.response) {
file.url = file.response.url;
}
return file;
});
setUploadedFilesInfo({...uploadedFilesInfo, [config.name]: newFiles});
}

return (
<Form
name="returnForm"
form={form}
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
autoComplete="off"
>
{
returnInfo?.formConfigList.map((config) => (
<Form.Item
key={config.name}
name={config.name}
label={config.label}
// labelCol={{span: (config.type == 'image' || config.type == 'text') ? 4 : 4}}
// wrapperCol={{span: (config.type == 'image' || config.type == 'text') ? 16 : 16}}
initialValue={`${returnInfo.dataMap[config.name]}`}
// valuePropName={config.type=='image' ? 'fileList' : 'value'}
// getValueFromEvent={(e) => setFormEvent(e, config)}
rules={[{ required: config.required, message: config.message }]}
>
{
config.type == 'select' && !!config.optionsName &&
<Select options={getSelectOptions(returnInfo[config.optionsName])} onChange={(value, option) => handleSelectChange(config, value, option)} />
}
{
config.type == 'text' &&
<TextArea rows={4} showCount maxLength={200} placeholder={config.placeholder} className="remark-input" />
}
{
config.type == 'input' &&
<Input placeholder={config.placeholder} />
}
{
config.type == 'image' &&
<Upload
name="fileData"
listType="picture-card"
className="img-uploader"
action="/api/upload/commonUploadFile"
maxCount={1}
fileList={uploadedFilesInfo[config.name]}
// showUploadList={false}
beforeUpload={beforeUpload}
onChange={(info) => handleUploadChange(info, config)}
>
<button style={{ border: 0, background: 'none' }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>上传</div>
</button>
</Upload>
}
</Form.Item>
))
}
</Form>
}

12. Upload报错:(intermediate value).forEach is not a function

出现 (intermediate value).forEach is not a function 错误通常是因为 Upload 组件的 fileList 属性传递的值不是一个数组。确保 fileList 始终是一个数组类型。

以下是一个完整的示例,展示如何在 Ant Design 表单中使用 Upload 组件并正确回显已上传的文件,同时确保 fileList 始终是一个数组:

import React, { useState, useEffect } from 'react';
import { Form, Upload, Button, message, Input } from 'antd';
import { UploadOutlined } from '@ant-design/icons';

const initialFileList = [
{
uid: '-1',
name: 'example.png',
status: 'done',
url: 'https://example.com/example.png',
},
];

const UploadForm = () => {
const [form] = Form.useForm();
const [fileList, setFileList] = useState(initialFileList);

useEffect(() => {
// 设置表单的初始值,包括上传的文件列表
form.setFieldsValue({
files: fileList,
});
}, [form, fileList]);

const handleChange = (info) => {
let newFileList = [...info.fileList];

// 更新文件列表
setFileList(newFileList);

if (info.file.status === 'done') {
message.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
};

const beforeUpload = (file) => {
// 在这里可以添加文件上传前的检查逻辑
return true;
};

const onFinish = (values) => {
console.log('Form values:', values);
};

return (
<Form form={form} onFinish={onFinish}>
<Form.Item
name="files"
label="Upload"
valuePropName="fileList"
getValueFromEvent={(e) => (Array.isArray(e) ? e : e && e.fileList)}
>
<Upload
action="/upload" // 替换为你的上传接口
fileList={fileList}
onChange={handleChange}
beforeUpload={beforeUpload}
listType="picture"
>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
</Form.Item>
<Form.Item
name="description"
label="Description"
rules={[{ required: true, message: 'Please input your description!' }]}
>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};

export default UploadForm;

关键点解释:

  1. 确保 fileList 始终是数组

    • getValueFromEvent 属性中,使用 (e) => (Array.isArray(e) ? e : e && e.fileList) 确保 fileList 始终是一个数组。
  2. 初始文件列表

    • initialFileList 是一个包含已上传文件信息的数组。每个文件对象包含 uid, name, status, 和 url 属性,用于描述文件的唯一标识、文件名、上传状态和文件 URL。
  3. 表单初始化

    • 使用 useEffect 钩子在组件挂载时设置表单的初始值,包括 Upload 组件的文件列表。
  4. handleChange 回调

    • 当上传文件列表发生变化时,更新文件列表状态并根据文件状态显示相应的消息。
  5. beforeUpload 回调

    • 在文件上传前可以添加检查逻辑,当前示例中直接返回 true 允许所有文件上传。
  6. 表单项配置

    • Form.Item 使用 name="files"valuePropName="fileList" 来绑定 Upload 组件的文件列表。
    • getValueFromEvent 属性用于从事件中提取文件列表并更新表单值。

通过这种方式,你可以在 Ant Design 表单中使用 Upload 组件,并在表单初始化时回显已上传的文件,同时确保 fileList 始终是一个数组,避免出现 .forEach is not a function 的错误。

13. Form中使用Select并回显

Form.IteminitialValue

14. Form.useWatch

用于直接获取 form 中字段对应的值。

例如:

const reservationStatus = Form.useWatch('reservationStatus', form);

15. Form中使用DatePicker并回显

Form.IteminitialValue使用dayjs处理

<Form.Item
key={detailFormItem.itemCode}
name={detailFormItem.itemCode}
label={detailFormItem.itemName}
initialValue={dayjs(detailFormItem.itemValue)}
rules={[{ required: detailFormItem.required, message: `请填写${detailFormItem.itemName}` }]}
></Form.Item>

elementui使用记录

1. el-radio切换不了

  • 查看选中的值有没有变
  • 选中的值变了,但是显示的没变,可以在change事件中强更新
handleChange() {
this.$forceUpdate();
}

2. el-form校验不通过

  • 初次按规则输入,校验正常通过
  • 回填后,清空,再填,相同规则校验一直不能通过
  • 发现清空时对表单字段的处理有问题,表单对象初始化没有的字段,使用delete关键字清空
<el-form :model="form">
<el-form-item label="AAA:" prop="a" :rules="[ { required: true, message: '请选择AAA', trigger: 'change'} ]">
<el-cascader
v-model="form.a"
:options="aOptions"
:props="aProps"
@change="handleChangeAAA"
style="width: 100%;"
clearable
>
</el-cascader>
</el-form-item>
<el-form-item label="BBB:" prop="b" :rules="[ { required: true, message: '请选择BBB', trigger: 'change'} ]">
<el-select v-model="form.b" placeholder="请选择">
<el-option
v-for="(item, index) in bOptions"
:label="item.label"
:value="item.value"
:key="index"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="CCC:" prop="c" :rules="{ required: true, message: '请选择CCC', trigger: 'change' }">
<el-radio-group v-model="form.c" @change="handleChangeCCC">
<el-radio :label="1" >CCC-1</el-radio>
<el-radio :label="2" >CCC-2</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="DDD:" prop="d" :rules="[
{ required: true, message: '请填写DDD', trigger: ['blur', 'change'] },
{ max: 50, message: '不可超过50个字符', trigger: ['blur', 'change'] }
]">
<el-input v-model="form.d" type="textarea" :rows="2" placeholder="请输入不超过50个字符"></el-input>
</el-form-item>
</el-form>

<script>
export default {
data() {
return {
form: {},
}
},
methods: {
// 回填
handleEcho() {
this.form.a = 1;
this.form.b = 'bbbbbb';
this.form.c = 10;
this.form.d = 'dddddd';
},
// 清空
handleClear() {
delete this.form.a; // this.form.a = null; 赋值null能清空,但再次选择后校验依然是该项没值
this.form.b = ''; // 字符串类型的字段赋值空字符串也正常,不用delete
delete this.form.c; // this.form.c = null; 赋值null能清空,但再次选择后校验依然是该项没值
this.form.d = ''; // 字符串类型的字段赋值空字符串也正常,不用delete
},
},
}
</script>

3. el-upload校验文件类型

<el-upload
class="upload-acceptance"
:action="'https://xxx'"
:headers="{'xxx': 'xxx'}"
:on-preview="handleAcceptPreview"
:on-remove="handleAcceptRemove"
:on-success="handleAcceptSuccess"
multiple
:limit="20"
:before-upload="handleBeforeUpload"
:on-exceed="handleAcceptExceed"
:file-list="fileList"
list-type="picture"
>
<el-button size="small">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传图片,最多支持20个,且单个不超过5MB</div>
</el-upload>
handleBeforeUpload(file) {
// 判断是否为图片
const isImage = file.type.startsWith('image/');
// 判断是否为excel
const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
// 对file name做处理
if (file.name.includes('(')) {
Object.defineProperty(file, 'name', {
writable: true,
value: file.name.replace(/\([^\)]*\)/g,"") // 去掉文件名中的括号
});
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isImage) {
this.$message.error('只能上传图片!');
}
if (!isLt5M) {
this.$message.error('上传图片大小不能超过 5MB!');
}
return isImage && isLt5M;
},

4. form表单嵌套upload进行校验

在使用 Element UI 进行表单验证时,如果表单中嵌套了 el-upload 组件,你可以通过自定义校验规则来实现文件上传的验证。以下是一个示例,展示了如何在表单中嵌套 el-upload 组件并进行校验:

<template>
<el-form :model="form" :rules="rules" ref="form" label-width="120px">
<el-form-item label="Name" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>

<el-form-item label="Upload" prop="upload">
<el-upload
class="upload-demo"
action="https://jsonplaceholder.typicode.com/posts/"
:on-success="handleSuccess"
:before-upload="beforeUpload"
:file-list="form.upload"
>
<el-button slot="trigger" size="small" type="primary">Select File</el-button>
<div slot="tip" class="el-upload__tip">Only jpg/png files with a size less than 500kb are allowed.</div>
</el-upload>
</el-form-item>

<el-form-item>
<el-button type="primary" @click="submitForm('form')">Submit</el-button>
<el-button @click="resetForm('form')">Reset</el-button>
</el-form-item>
</el-form>
</template>

<script>
export default {
data() {
return {
form: {
name: '',
upload: []
},
rules: {
name: [
{ required: true, message: 'Please input name', trigger: 'blur' }
],
upload: [
{ required: true, validator: this.validateUpload, trigger: 'change' }
]
}
};
},
methods: {
handleSuccess(response, file, fileList) {
this.form.upload = fileList;
},
beforeUpload(file) {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
const isLt2M = file.size / 1024 / 1024 < 0.5;

if (!isJPG) {
this.$message.error('Upload image must be JPG or PNG format!');
}
if (!isLt2M) {
this.$message.error('Upload image size cannot exceed 500KB!');
}
return isJPG && isLt2M;
},
validateUpload(rule, value, callback) {
if (this.form.upload.length === 0) {
callback(new Error('Please upload a file'));
} else {
callback();
}
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$message.success('Submit successful!');
} else {
this.$message.error('Submit failed!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
this.form.upload = [];
}
}
};
</script>

<style>
.upload-demo {
margin-bottom: 20px;
}
</style>

在这个示例中,我们做了以下几件事:

  1. el-form 中定义了一个 el-upload 组件来处理文件上传。
  2. 使用 handleSuccess 方法在文件上传成功后更新表单模型中的 upload 属性。
  3. 使用 beforeUpload 方法在上传文件之前进行文件类型和大小的验证。
  4. 定义自定义校验规则 validateUpload,确保用户上传了至少一个文件。
  5. 在表单提交时,进行校验并显示相应的消息。

这样,当用户上传文件并提交表单时,表单会进行验证,确保所有必填项包括文件上传都已完成。

5. 去掉 Element UI 上传组件的文件列表动画

要去掉 Element UI 上传组件的文件列表动画,你可以通过覆盖相关的 CSS 样式来实现。以下是几个步骤:

  1. 首先,找到上传组件的根元素,通常是 .el-upload-list

  2. 然后,覆盖与动画相关的 CSS 属性。主要是 transition 属性。

  3. 你可以在你的组件或全局样式中添加以下 CSS:

<style scoped>
.el-upload-list--text {
transition: none !important;
}

.el-upload-list__item {
transition: none !important;
}

.el-upload-list__item-name {
transition: none !important;
}

/* 如果使用了缩略图模式,也可以添加这个 */
.el-upload-list--picture .el-upload-list__item {
transition: none !important;
}
</style>
  1. 如果你想全局应用这个样式,可以去掉 scoped 属性,或者在你的主 CSS 文件中添加这些样式。

  2. 如果以上方法不完全有效,你可能需要使用 !important 来确保你的样式覆盖了 Element UI 的默认样式。

  3. 对于某些特定的动画,你可能还需要覆盖其他的 CSS 属性,如 animation

例如:

.el-upload-list__item,
.el-upload-list__item-name,
.el-upload-list--picture .el-upload-list__item {
transition: none !important;
animation: none !important;
}

请注意,移除动画可能会影响用户体验,因为动画通常用于提供视觉反馈。确保这符合你的设计需求。

如果你只想移除特定实例的动画,而不是全局移除,你可以给你的上传组件一个特定的类名,然后只针对这个类名应用以上的 CSS 样式。

vxe-grid使用记录

tip

vxe-table: 一个基于 vue 的 PC 端表格组件,支持增删改查、虚拟列表、虚拟树、懒加载、快捷菜单、数据校验、打印导出、表单渲染、数据分页、弹窗、自定义模板、渲染器、贼灵活的配置项等。

1. 按需引入vxe-grid

  • npm install xe-utils vxe-table@legacy
  • npm install babel-plugin-import -D
  • main.js中引入
src/main.js
import XEUtils from 'xe-utils'
import { VXETable,Grid,Table as VTable } from 'vxe-table' // Table导入时重命名是为了避免和其他UI库的Table组件命名冲突
import zhCN from 'vxe-table/lib/locale/lang/zh-CN'
VXETable.setup({ // 设置国际化中文是为了让vxe-grid的loading显示“加载中”
i18n: (key, args) => XEUtils.toFormatString(XEUtils.get(zhCN, key), args)
})
Vue.use(Grid)
Vue.use(VTable) // 需要引入vxe-table的Table组件,否则使用vxe-grid时会报错
  • 配置babel.config.js或者.babelrc
babel.config.js
{
"plugins": [
[
"import",
{
"libraryName": "vxe-table",
"style": true // 样式是否也按需加载
}
]
]
}

生成二维码

在 Vue.js 项目中生成二维码可以使用 qrcode 库。以下是一个简单的步骤指南:

  1. 安装 qrcode

    npm install qrcode
  2. 在 Vue 组件中使用 qrcode

    <template>
    <div>
    <canvas ref="qrcodeCanvas"></canvas>
    </div>
    </template>

    <script>
    import QRCode from 'qrcode'

    export default {
    name: 'QRCodeGenerator',
    mounted() {
    this.generateQRCode()
    },
    methods: {
    generateQRCode() {
    const canvas = this.$refs.qrcodeCanvas
    const text = 'https://example.com' // 你想要生成二维码的文本或URL

    QRCode.toCanvas(canvas, text, function (error) {
    if (error) console.error(error)
    console.log('二维码生成成功!')
    })
    }
    }
    }
    </script>

遇到的问题:需要在弹窗中展示二维码,按以上步骤开发运行报错:TypeError: Cannot read properties of undefined (reading 'getContext')

<template>
<div>
<el-button @click="handleLookQrcode">查看二维码</el-button>
<el-dialog
:visible.sync="qrcodeVisible"
width="500px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
>
<div slot="title">二维码如下</div>
<div>
<canvas ref="qrcodeCanvas"></canvas>
</div>
<div slot="footer">
<el-button @click="qrcodeVisible = false">取消</el-button>
<el-button @click="handleCopy">复制</el-button>
</div>
</el-dialog>
</div>
</template>

<script>
import QRCode from 'qrcode'

export default {
data() {
return {
qrcodeVisible: false,
}
},
methods: {
handleLookQrcode() {
this.qrcodeVisible = true;
this.generateQRCode();
},
generateQRCode() {
const canvas = this.$refs.qrcodeCanvas;
const text = 'https://example.com' // 你想要生成二维码的文本或URL
console.log('canvas', canvas)
try {
QRCode.toCanvas(canvas, text, function (error) {
if (error) console.error(error)
console.log('二维码生成成功!')
})
} catch (error) {
console.log('error', error)
}
},
}
}
</script>

这个报错是因为尝试获取二维码画布的上下文时,画布对象未正确初始化或未找到。如上打印canvasundefined,将handleLookQrcode改为:

handleLookQrcode() {
this.qrcodeVisible = true;
setTimeout(() => {
this.generateQRCode();
}, 0);
},

复制粘贴

为了确保在所有浏览器中都能正常工作,可以使用 clipboard-polyfill 库。安装:npm install clipboard-polyfill

复制文本

<template>
<div>
<button @click="handleCopy">Copy Multiple Texts</button>
</div>
</template>

<script>
import * as clipboard from 'clipboard-polyfill';

export default {
methods: {
async handleCopy() {
try {
const text1 = 'First text content';
const text2 = 'Second text content';

// 将多个文本内容组合成一个字符串
const combinedText = `${text1}\n${text2}`;

// 使用 clipboard-polyfill 复制组合后的文本内容
await clipboard.writeText(combinedText);
alert('Multiple texts copied to clipboard!');
} catch (error) {
console.error('Failed to copy texts: ', error);
}
}
}
};
</script>

复制图片

在 Vue 中复制图片到剪贴板可以通过以下步骤实现:

  1. 获取图片的 Blob 对象
  2. 将 Blob 对象写入剪贴板

以下是一个完整的示例:

<template>
<div>
<img ref="image" src="path/to/your/image.jpg" alt="Image to copy" />
<button @click="copyImage">Copy Image</button>
</div>
</template>

<script>
import * as clipboard from 'clipboard-polyfill';

export default {
methods: {
async copyImage() {
try {
const img = this.$refs.image;
const response = await fetch(img.src);
const blob = await response.blob();
await clipboard.write([new clipboard.ClipboardItem({ [blob.type]: blob })]);
alert('Image copied to clipboard!');
} catch (error) {
console.error('Failed to copy image: ', error);
}
}
}
};
</script>
  1. 获取图片元素:通过 this.$refs.image 获取图片元素。
  2. 获取图片的 Blob 对象:使用 fetch 请求图片的 URL,并将响应转换为 Blob 对象。
  3. 将 Blob 对象写入剪贴板:使用 clipboard-polyfill 库的 write 方法将 Blob 对象写入剪贴板。
warning

只在HTTPS环境下生效,HTTP环境下粘贴的内容为空

复制canvas

以下是一个完整的示例:

<template>
<div>
<canvas ref="qrcodeCanvas"></canvas>
<button @click="copyQRCode">Copy QR Code</button>
</div>
</template>

<script>
import QRCode from 'qrcode';
import * as clipboard from 'clipboard-polyfill';

export default {
mounted() {
this.generateQRCode();
},
methods: {
generateQRCode() {
const canvas = this.$refs.qrcodeCanvas;
QRCode.toCanvas(canvas, 'Your QR Code Data', function (error) {
if (error) console.error(error);
console.log('QR code generated!');
});
},
async copyQRCode() {
try {
const canvas = this.$refs.qrcodeCanvas;
canvas.toBlob(async (blob) => {
if (blob) {
await clipboard.write([new clipboard.ClipboardItem({ [blob.type]: blob })]);
alert('QR Code copied to clipboard!');
} else {
console.error('Failed to convert canvas to Blob');
}
});
} catch (error) {
console.error('Failed to copy QR Code: ', error);
}
}
}
};
</script>
  1. 生成二维码:在 mounted 钩子中调用 generateQRCode 方法,使用 qrcode 库生成二维码并绘制到 <canvas> 元素上。
  2. 复制二维码:在 copyQRCode 方法中,将 <canvas> 元素转换为 Blob 对象,并使用 clipboard-polyfill 库将 Blob 对象写入剪贴板。
warning

只在HTTPS环境下生效,HTTP环境下粘贴的内容为空

复制多个内容

比如同时复制图片和文本,可以将不同类型的内容(如图像和文本)组合在一起。以下是一个示例,展示如何将二维码图像和文本同时写入剪贴板:

<template>
<div>
<canvas ref="qrcodeCanvas"></canvas>
<button @click="handleCopy">Copy QR Code and Text</button>
</div>
</template>

<script>
import QRCode from 'qrcode';
import * as clipboard from 'clipboard-polyfill';

export default {
data() {
return {
qrcodeUrl: 'https://example.com' // 你想要复制的文本内容
};
},
mounted() {
this.generateQRCode();
},
methods: {
generateQRCode() {
const canvas = this.$refs.qrcodeCanvas;
QRCode.toCanvas(canvas, this.qrcodeUrl, function (error) {
if (error) console.error(error);
console.log('QR code generated!');
});
},
handleCopy() {
try {
const canvas = this.$refs.qrcodeCanvas;
canvas.toBlob(async (blob) => {
if (blob) {
const clipboardItems = [
new clipboard.ClipboardItem({ [blob.type]: blob }),
new clipboard.ClipboardItem({ 'text/plain': new Blob([this.qrcodeUrl], { type: 'text/plain' }) })
];
await clipboard.write(clipboardItems);
alert('QR Code and text copied to clipboard!');
} else {
console.error('Failed to convert canvas to Blob');
}
});
} catch (error) {
console.error('Failed to copy QR Code: ', error);
}
}
}
};
</script>
  1. 生成二维码:在 mounted 钩子中调用 generateQRCode 方法,使用 qrcode 库生成二维码并绘制到 <canvas> 元素上。
  2. 复制二维码和文本:在 handleCopy 方法中,将 <canvas> 元素转换为 Blob 对象,并将其与文本内容一起写入剪贴板。
warning

粘贴后发现始终只能粘贴一项,虽然可以将多个内容作为不同的 MIME 类型添加到剪贴板中,但是剪贴板 API 并不支持将不同类型的内容(如图像和文本)同时粘贴到目标应用程序中。大多数应用程序(如文本编辑器、浏览器等)只能处理一种类型的剪贴板内容。

不过,可以通过以下方法来实现类似的效果:将图像和文本组合成一个 HTML 片段,然后将其作为 HTML 内容复制到剪贴板中。这样,当你粘贴时,支持 HTML 粘贴的应用程序(如富文本编辑器)将能够同时显示图像和文本。

<template>
<div>
<canvas ref="qrcodeCanvas"></canvas>
<button @click="handleCopy">Copy QR Code and Text</button>
</div>
</template>

<script>
import QRCode from 'qrcode';
import * as clipboard from 'clipboard-polyfill';

export default {
data() {
return {
qrcodeUrl: 'https://example.com' // 你想要复制的文本内容
};
},
mounted() {
this.generateQRCode();
},
methods: {
generateQRCode() {
const canvas = this.$refs.qrcodeCanvas;
QRCode.toCanvas(canvas, this.qrcodeUrl, function (error) {
if (error) console.error(error);
console.log('QR code generated!');
});
},
async handleCopy() {
try {
const canvas = this.$refs.qrcodeCanvas;
const textContent = this.qrcodeUrl;

// 将 canvas 转换为 Data URL
const imageDataUrl = canvas.toDataURL('image/png');

// 创建 HTML 片段
const htmlContent = `
<div>
<img src="${imageDataUrl}" alt="QR Code">
<p>${textContent}</p>
</div>
`;

// 创建 ClipboardItem 对象
const clipboardItem = new clipboard.ClipboardItem({
'text/html': new Blob([htmlContent], { type: 'text/html' })
});

// 写入剪贴板
await clipboard.write([clipboardItem]);
alert('QR Code and text copied to clipboard!');
} catch (error) {
console.error('Failed to copy QR Code: ', error);
}
}
}
};
</script>
  1. 生成二维码:在 mounted 钩子中调用 generateQRCode 方法,使用 qrcode 库生成二维码并绘制到 <canvas> 元素上。
  2. 将 canvas 转换为 Data URL:使用 canvas.toDataURL 方法将 <canvas> 元素转换为 Data URL。
  3. 创建 HTML 片段:将图像 Data URL 和文本内容组合成一个 HTML 片段。
  4. 创建 ClipboardItem 对象:将 HTML 片段和纯文本内容包装在 ClipboardItem 对象中。
  5. 写入剪贴板:使用 clipboard-polyfill 库的 write 方法将 ClipboardItem 对象写入剪贴板。
tip

这种方式在HTTPS和HTTP环境下都生效。 ::

文件下载

使用<a>配合download属性实现文件下载

例1,使用<a>标签的download属性来实现图片下载

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Download Image</title>
</head>
<body>
<img id="image" src="https://example.com/image.jpg" alt="Example Image" style="display:none;">
<a id="downloadLink" href="#" download="image.jpg">Download Image</a>

<script>
document.addEventListener('DOMContentLoaded', function() {
var image = document.getElementById('image');
var downloadLink = document.getElementById('downloadLink');

// 设置下载链接的href为图片的src
downloadLink.href = image.src;
});
</script>
</body>
</html>

例2,JS创建一个隐藏的<a>标签,设置其href属性为文件的URL,并设置download属性为文件的默认名称。然后,模拟点击这个链接以触发下载,最后移除这个隐藏的链接。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件下载示例</title>
</head>
<body>
<button id="downloadBtn">下载文件</button>

<script>
document.getElementById('downloadBtn').addEventListener('click', function() {
// 文件的URL
var fileUrl = 'https://example.com/path/to/your/file.pdf';
// 创建一个隐藏的a标签
var a = document.createElement('a');
a.href = fileUrl;
a.download = 'file.pdf'; // 设置下载文件的默认名称
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
</script>
</body>
</html>

例3,如果你需要下载的是动态生成的文件(例如,生成的文本或图像),可以使用Blob对象:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件下载示例</title>
</head>
<body>
<button id="downloadBtn">下载文件</button>

<script>
document.getElementById('downloadBtn').addEventListener('click', function() {
// 动态生成的文件内容
var fileContent = '这是一个示例文件的内容';
var blob = new Blob([fileContent], { type: 'text/plain' });
var url = URL.createObjectURL(blob);

// 创建一个隐藏的a标签
var a = document.createElement('a');
a.href = url;
a.download = 'example.txt'; // 设置下载文件的默认名称
document.body.appendChild(a);
a.click();
document.body.removeChild(a);

// 释放URL对象
URL.revokeObjectURL(url);
});
</script>
</body>
</html>

某些浏览器可能会对跨域下载有安全限制,确保文件URL与页面URL在同一个域名下,或者服务器配置了正确的CORS头。 :::

例4,使用Canvas绘制图片并下载(适用于需要对图片进行处理或修改的情况)

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Download Image</title>
</head>
<body>
<canvas id="canvas" style="display:none;"></canvas>
<a id="downloadLink" href="#" download="image.jpg">Download Image</a>

<script>
document.addEventListener('DOMContentLoaded', function() {
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var downloadLink = document.getElementById('downloadLink');
var image = new Image();
image.crossOrigin = 'Anonymous'; // 处理跨域问题
image.src = 'https://example.com/image.jpg';

image.onload = function() {
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);

// 将Canvas内容转换为Data URL
var dataURL = canvas.toDataURL('image/jpeg');

// 设置下载链接的href为Data URL
downloadLink.href = dataURL;
};
});
</script>
</body>
</html>

使用file-saver

安装:

npm install file-saver --save
npm install @types/file-saver --save-dev

使用:

import { saveAs } from 'file-saver';

// Saving text
var blob = new Blob(["Hello, world!"], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "hello world.txt");

// Saving URLs
FileSaver.saveAs("https://httpbin.org/image", "image.jpg");

微信网页开发JS SDK

微信分享给朋友自定义分享内容

在微信分享给朋友的内容中,图片的大小是由微信自动调整的,开发者无法直接控制图片的显示大小。但是,开发者可以通过优化图片的尺寸和比例来确保在微信中显示效果良好。以下是一些建议和步骤,帮助你设置分享内容中的图片:

  1. 图片尺寸建议

    微信对分享图片有一定的推荐尺寸和比例,以下是一些常见的建议:

    • 缩略图尺寸:建议使用 300x300 像素或 5:4 比例的图片。
    • 图片格式:推荐使用 JPEG 或 PNG 格式。
    • 文件大小:尽量保持图片文件大小在 300KB 以内,以确保加载速度和显示效果。
  2. 使用微信分享接口

    在微信的开发接口中,通过 wx.updateAppMessageShareDatawx.onMenuShareAppMessage 方法可以设置分享给朋友的内容,包括标题、描述、链接和缩略图。以下是一个示例:

    wx.ready(function () {
    wx.updateAppMessageShareData({
    title: '分享标题', // 分享标题
    desc: '分享描述', // 分享描述
    link: 'https://example.com', // 分享链接
    imgUrl: 'https://example.com/image.jpg', // 分享图标
    success: function () {
    // 设置成功
    }
    });
    });
  3. 图片优化建议

    • 保持图片清晰度:确保图片在 300x300 像素下仍然清晰可见。
    • 避免过多文字:图片中的文字应简洁明了,避免过多的文字内容。
    • 使用高对比度:确保图片中的主要元素与背景有足够的对比度,以便在缩略图中仍然清晰可辨。

以下是一个完整的示例,展示如何通过微信 JavaScript SDK 设置分享内容:

<!DOCTYPE html>
<html>
<head>
<title>微信分享示例</title>
<script src="https://res.wx.qq.com/open/js/jweixin-1.4.0.js"></script>
<script>
// 微信配置
wx.config({
debug: false,
appId: 'your_app_id',
timestamp: 'timestamp',
nonceStr: 'nonceStr',
signature: 'signature',
jsApiList: [
'updateAppMessageShareData',
'onMenuShareAppMessage'
]
});

wx.ready(function () {
// 设置分享内容
wx.updateAppMessageShareData({
title: '分享标题',
desc: '分享描述',
link: 'https://example.com',
imgUrl: 'https://example.com/image.jpg',
success: function () {
// 设置成功
}
});
});
</script>
</head>
<body>
<h1>微信分享示例</h1>
</body>
</html>
tip
  1. 分享链接的域名必须已经在微信公众平台的“JS接口安全域名”设置中进行过验证和配置。进入“设置” -> “公众号设置” -> “功能设置” -> 在“JS接口安全域名”一栏,添加你的域名并进行验证。

  2. 你在调用 wx.updateAppMessageShareData 时,传递的 link 参数的域名必须与当前页面的域名一致。例如,如果你的页面在 https://example.com/page,那么分享链接的域名也必须是 example.com

使用lodash

为了减少包的体积并仅安装你需要的 lodash 函数,你可以使用 lodash-es 或者直接从 lodash 库中按需导入特定的函数。以下是如何安装和使用最小的 lodash 包的详细说明。

方法一:使用 lodash-es

lodash-eslodash 的 ES 模块版本,支持按需导入,可以更好地与现代打包工具(如 Vite、Webpack)配合使用。

  1. 安装 lodash-es
npm install lodash-es
npm install --save-dev @types/lodash-es
  1. 按需导入 debounce 函数
import debounce from 'lodash-es/debounce';

方法二:按需导入 lodash 函数

你也可以直接从 lodash 库中按需导入特定的函数,这样可以确保只打包你需要的部分。

  1. 安装 lodash
npm install lodash
  1. 按需导入 debounce 函数
import debounce from 'lodash/debounce';

以下是一个示例,展示如何在 React 组件中使用按需导入的 debounce 函数。

import React, { useState, useCallback, useEffect } from 'react';
import debounce from 'lodash-es/debounce'; // 或者使用 'lodash/debounce'

const SearchComponent = () => {
const [query, setQuery] = useState('');

const handleSearch = useCallback(
debounce((value) => {
console.log('Searching for:', value);
}, 300),
[]
);

useEffect(() => {
return () => {
handleSearch.cancel();
};
}, [handleSearch]);

const handleChange = (event) => {
const value = event.target.value;
setQuery(value);
handleSearch(value);
};

return (
<div>
<input
type="text"
value={query}
onChange={handleChange}
placeholder="Search..."
/>
</div>
);
};

export default SearchComponent;

详细说明:

  1. 安装 lodash-eslodash

    • 使用 npm install lodash-es 安装 lodash-es,或者使用 npm install lodash 安装 lodash
  2. 按需导入 debounce 函数

    • 使用 import debounce from 'lodash-es/debounce';lodash-es 导入 debounce 函数,或者使用 import debounce from 'lodash/debounce';lodash 导入 debounce 函数。
  3. 在 React 组件中使用 debounce

    • 使用 useState 钩子来管理输入框的状态。
    • 使用 useCallback 钩子来定义一个被 debounce 的函数 handleSearchdebounce 函数接受两个参数:要被限制执行频率的函数和时间间隔(以毫秒为单位)。
    • handleChange 函数中,更新输入框的状态,并调用被 debouncehandleSearch 函数。
    • 使用 useEffect 钩子在组件卸载时清理 debounce 函数。

通过这种方式,你可以确保只打包和使用 lodash 中你需要的部分,从而减少包的体积。

拖拽

react-draggable

拖拽排序

以下是 react-beautiful-dndreact-dndreact-sortable-hocdnd-kit 这几个库的对比:

1. react-beautiful-dnd

33.6k 官方声明2025-4-30弃用改组件库,推荐使用dnd-kit

优点

  • 易用性:API 设计简洁,易于上手。
  • 用户体验:提供了良好的拖拽动画和用户体验。
  • 文档:文档详细,示例丰富。

缺点

  • 灵活性:对于一些复杂的拖拽需求,可能不够灵活。
  • 性能:在处理大量元素时,性能可能会有所下降。

适用场景

  • 适用于大多数常见的拖拽排序需求,特别是列表和网格布局。

2. react-dnd

21.2k

优点

  • 灵活性:基于 HTML5 拖放 API,适用于复杂的拖放需求。
  • 可扩展性:可以处理多种拖放场景,不仅限于排序。

缺点

  • 学习曲线:API 相对复杂,需要更多的学习和配置。
  • 文档:文档较为详细,但示例相对较少。

适用场景

  • 适用于需要高度自定义和复杂拖放交互的应用,如看板、树形结构等。

3. react-sortable-hoc

10.8k 官方声明该库不再被积极维护。所有开发工作均已转向@dnd-kit。它提供功能奇偶性,采用现代且可扩展的架构构建,支持复杂的用例并具有内置的辅助功能。强烈鼓励新消费者采用@dnd-kit,而不是采用react-sortable-hoc

优点

  • 易用性:API 设计简洁,易于上手。
  • 性能:性能较好,适用于处理大量元素。

缺点

  • 灵活性:功能相对单一,主要用于拖拽排序,不适用于复杂的拖放需求。
  • 维护:库的维护频率较低,可能存在一些未解决的问题。

适用场景

  • 适用于简单的拖拽排序需求,如列表和网格布局。

4. dnd-kit

13.6k npm install @dnd-kit/core

Dnd Kit 的核心是三个主要的组件:DndContext、Draggable 和 Droppable。

DndContext:作为父组件,管理拖放操作的上下文环境。 Draggable:表示可以拖动的元素。 Droppable:表示可以放置拖动元素的目标区域。

优点

  • 现代化:基于现代化的设计,提供了灵活的 API 和良好的性能。
  • 灵活性:可以处理多种拖放场景,支持复杂的交互。
  • 性能:性能优异,适用于处理大量元素。

缺点

  • 学习曲线:API 相对复杂,需要更多的学习和配置。
  • 文档:文档较为详细,但示例相对较少。

适用场景

  • 适用于需要高度自定义和复杂拖放交互的应用,如看板、树形结构等。

示例:以下是一个使用 dnd-kit 实现拖拽排序的示例。这个示例展示了如何使用 dnd-kit 库来创建一个简单的拖拽排序列表。

首先,安装 dnd-kit

npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/modifiers

然后,创建一个组件来实现拖拽排序:

import React, { useState } from 'react';
import {
DndContext,
closestCenter,
useSensor,
useSensors,
PointerSensor,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

const SortableItem = ({ id }) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id });

const style = {
transform: CSS.Transform.toString(transform),
transition,
padding: '8px',
margin: '4px',
backgroundColor: '#f0f0f0',
border: '1px solid #ccc',
};

return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
{id}
</div>
);
};

const DragAndDrop = () => {
const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3', 'Item 4']);

const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 10,
},
})
);

const handleDragEnd = (event) => {
const { active, over } = event;

if (active.id !== over.id) {
setItems((items) => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};

return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items.map((id) => (
<SortableItem key={id} id={id} />
))}
</SortableContext>
</DndContext>
);
};

export default DragAndDrop;

代码解释:

  1. 安装依赖: 安装 @dnd-kit/core@dnd-kit/sortable@dnd-kit/modifiers 这三个包。

  2. SortableItem 组件

    • 使用 useSortable 钩子来使每个项目可拖拽。
    • 设置拖拽元素的样式,包括 transformtransition
  3. DragAndDrop 组件

    • 使用 useState 钩子来管理项目列表的状态。
    • 使用 useSensorsuseSensor 来配置传感器,这里使用 PointerSensor 来处理拖拽事件。
    • DndContext 中配置 collisionDetectiononDragEnd 回调函数。
    • 使用 SortableContext 包裹可拖拽的项目,并设置排序策略为 verticalListSortingStrategy
  4. handleDragEnd 函数

    • 在拖拽结束时,更新项目列表的顺序。

这个示例展示了如何使用 dnd-kit 来实现一个简单的拖拽排序列表。你可以根据需要进一步自定义和扩展这个示例。

activationConstraintdnd-kit 中用于配置拖拽激活条件的一个选项。它允许你设置一些约束条件,只有在满足这些条件时,拖拽操作才会被激活。这样可以避免误触发拖拽操作,提高用户体验。

dnd-kit 中,activationConstraint 可以配置在传感器(如 PointerSensor)中。以下是一个示例,展示了如何使用 activationConstraint 来设置拖拽激活的距离约束

activationConstraint: { distance: 10 } 的含义是:

  • distance:设置拖拽激活的距离约束。只有当用户拖动的距离超过 10 像素时,拖拽操作才会被激活。

除了 distance 之外,activationConstraint 还可以配置其他类型的约束条件,例如时间约束:

const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
delay: 250, // 250 毫秒
tolerance: 5, // 5 像素
},
})
);
  • delay:设置拖拽激活的时间延迟。只有当用户按住元素超过 250 毫秒时,拖拽操作才会被激活。
  • tolerance:设置拖拽激活的容差距离。在延迟时间内,用户可以在 5 像素范围内移动而不会取消拖拽激活。

通过配置 activationConstraint,你可以更精确地控制拖拽操作的激活条件,从而提高用户体验,避免误操作。

平滑滚动

简单滑动使用window.scrollTo:

// 滑动到顶部
window.scrollTo({
top: 0,
behavior: 'smooth' // 平滑滚动
});

如果你正在寻找类似 react-scroll 的库来实现滚动定位功能,以下是一些常用的库和工具,它们可以帮助你在 React 应用中实现平滑滚动和滚动定位功能:

1. react-scroll

react-scroll 是一个非常流行的库,用于在 React 应用中实现平滑滚动和滚动定位功能。4.4k star

安装:

npm install react-scroll
npm install -D @types/react-scroll

示例:

import React from 'react';
import { Link, Element, animateScroll as scroll } from 'react-scroll';

const ScrollExample = () => {
return (
<div>
<button onClick={() => scroll.scrollTo(100)}>Scroll to Position 100</button>
<Link to="item5" smooth={true} duration={500}>
Scroll to Item 5
</Link>
<div style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }}>
<ul>
{Array.from({ length: 20 }, (_, index) => (
<Element name={`item${index + 1}`} key={index}>
<li>Item {index + 1}</li>
</Element>
))}
</ul>
</div>
</div>
);
};

export default ScrollExample;

遇到的问题

根据如下官方示例可以实现预期效果:

import React from 'react';
import { Link, Element } from 'react-scroll';

function App() {
return (
<div>
<nav>
<ul>
<li>
<Link to="section1" smooth={true} duration={500}>Section 1</Link>
</li>
<li>
<Link to="section2" smooth={true} duration={500}>Section 2</Link>
</li>
{/* Add more navigation links as needed */}
</ul>
</nav>
<Element name="section1">
<section style={{ height: '100vh', backgroundColor: 'lightblue' }}>
<h1>Section 1</h1>
<p>This is the content of section 1</p>
</section>
</Element>
<Element name="section2">
<section style={{ height: '100vh', backgroundColor: 'lightgreen' }}>
<h1>Section 2</h1>
<p>This is the content of section 2</p>
</section>
</Element>
{/* Add more sections with Element components as needed */}
</div>
);
}

export default App;

但是,当把父元素高度固定为200px并设置可滚动,section高度设为100px后,点击Link或手动触发滚动都不能生效。解决方案:手动指定父元素为可滚动区域。

import { Link, Element, scroller } from "react-scroll";

export default function Demo() {
const scrollToSection = () => {
scroller.scrollTo('section1', {
duration: 500,
delay: 0,
smooth: 'easeInOutQuart',
offset: 50,
containerId: 'parent-container' // 指定滚动容器的ID,这个很重要
});
};
return (
<div style={{height:'200px', overflow:'auto'}} id="parent-container">
<button onClick={scrollToSection}>Scroll to Section 1</button>
{/* <nav>
<ul>
<li>
<Link to="section1" smooth={true} duration={500}>Section 1</Link>
</li>
<li>
<Link to="section2" smooth={true} duration={500}>Section 2</Link>
</li>
</ul>
</nav> */}
<Element name="section1">
<section style={{ height: '100px', backgroundColor: 'lightblue' }}>
<h1>Section 1</h1>
<p>This is the content of section 1</p>
</section>
</Element>
<Element name="section2">
<section style={{ height: '100px', backgroundColor: 'lightgreen' }}>
<h1>Section 2</h1>
<p>This is the content of section 2</p>
</section>
</Element>
{/* Add more sections with Element components as needed */}
</div>
)
}

2. react-scroll-to-component

react-scroll-to-component 是另一个用于在 React 应用中实现平滑滚动的库。168 star

安装:

npm install react-scroll-to-component

示例:

import React, { useRef } from 'react';
import scrollToComponent from 'react-scroll-to-component';

const ScrollToComponentExample = () => {
const sectionRef = useRef(null);

return (
<div>
<button onClick={() => scrollToComponent(sectionRef.current, { offset: 0, align: 'top', duration: 500 })}>
Scroll to Section
</button>
<div style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }}>
<div ref={sectionRef} style={{ height: '1000px' }}>
<h2>Section</h2>
</div>
</div>
</div>
);
};

export default ScrollToComponentExample;

react-router-hash-link 是一个用于在 React Router 中实现平滑滚动的库。735 star

安装:

npm install react-router-hash-link

示例:

import React from 'react';
import { HashLink as Link } from 'react-router-hash-link';

const HashLinkExample = () => {
return (
<div>
<Link smooth to="#section1">Scroll to Section 1</Link>
<div style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }}>
<div id="section1" style={{ height: '1000px' }}>
<h2>Section 1</h2>
</div>
</div>
</div>
);
};

export default HashLinkExample;

4. react-scrollspy

react-scrollspy 是一个用于在滚动时高亮导航链接的库。不再主动维护,请改用@makotot/ghostui。61 star

安装:

npm install react-scrollspy

示例:

import React from 'react';
import Scrollspy from 'react-scrollspy';

const ScrollSpyExample = () => {
return (
<div>
<Scrollspy items={['section1', 'section2']} currentClassName="is-current">
<li><a href="#section1">Section 1</a></li>
<li><a href="#section2">Section 2</a></li>
</Scrollspy>
<div style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }}>
<div id="section1" style={{ height: '1000px' }}>
<h2>Section 1</h2>
</div>
<div id="section2" style={{ height: '1000px' }}>
<h2>Section 2</h2>
</div>
</div>
</div>
);
};

export default ScrollSpyExample;

5. react-scroll-into-view

react-scroll-into-view 是一个用于在 React 应用中实现平滑滚动的库。52 star

安装:

npm install react-scroll-into-view

示例:

import React from 'react';
import ScrollIntoView from 'react-scroll-into-view';

const ScrollIntoViewExample = () => {
return (
<div>
<ScrollIntoView selector="#section1">
<button>Scroll to Section 1</button>
</ScrollIntoView>
<div style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }}>
<div id="section1" style={{ height: '1000px' }}>
<h2>Section 1</h2>
</div>
</div>
</div>
);
};

export default ScrollIntoViewExample;

轮询

在 Vue.js 项目中使用 Axios 进行轮询

在 Vue.js 项目中使用 Axios 进行轮询是一种常见的需求,特别是当你需要定期从服务器获取最新数据时。以下是一个使用 Axios 进行轮询的示例:

<template>
<div>
<h1>Data Polling Example</h1>
<div v-if="loading">Loading...</div>
<div v-if="error">{{ error }}</div>
<ul v-if="data">
<li v-for="item in data" :key="item.id">{{ item.title }}</li>
</ul>
</div>
</template>

<script>
import axios from 'axios';

export default {
data() {
return {
data: null,
loading: false,
error: null,
pollingInterval: null
};
},
methods: {
async fetchData() {
this.loading = true;
this.error = null;
try {
const response = await axios.get('https://api.example.com/data');
this.data = response.data;
} catch (err) {
this.error = 'Error fetching data: ' + err.message;
} finally {
this.loading = false;
}
},
startPolling() {
this.fetchData(); // 立即执行一次
this.pollingInterval = setInterval(() => {
this.fetchData();
}, 5000); // 每5秒轮询一次
},
stopPolling() {
clearInterval(this.pollingInterval);
}
},
mounted() {
this.startPolling();
},
beforeDestroy() {
this.stopPolling();
}
};
</script>

这个示例实现了以下功能:

  1. fetchData 方法使用 Axios 发送 GET 请求到指定 URL。

  2. startPolling 方法启动轮询:

    • 立即调用一次 fetchData
    • 使用 setInterval 每 5 秒调用一次 fetchData
  3. stopPolling 方法用于停止轮询,清除 setInterval

  4. 在组件挂载时(mounted 钩子)启动轮询。

  5. 在组件销毁前(beforeDestroy 钩子)停止轮询,以防止内存泄漏。

注意事项:

  • 调整轮询间隔(示例中为 5000 毫秒)以适应你的需求。
  • 考虑添加错误处理逻辑,例如在连续失败多次后暂停轮询。
  • 如果你的应用需要在后台持续轮询,考虑使用 Web Workers 或服务端推送技术(如 WebSockets)。
  • 确保你的服务器能够处理频繁的请求,并考虑实现节流或限速机制。

这种轮询方法适用于需要定期更新数据的场景,但要注意不要设置过于频繁的轮询间隔,以免对服务器造成不必要的负担。

· 15 分钟阅读

Yeoman 是一个开源的客户端脚手架生成工具,用于web应用开发。它帮助开发者快速搭建新的项目,提供了一套标准化的工具和流程来加速开发。Yeoman 本身是基于Node.js的,它通过脚手架(scaffolding)、任务运行器(如Grunt和Gulp)以及包管理(如Bower和npm)等工具,帮助开发者自动化常见的开发任务,比如创建项目结构、添加库依赖、运行测试等。Yeoman 主要用于以下几种场景:

  • 快速启动新项目:Yeoman 可以快速生成项目的基础结构,包括目录、文件以及代码模板,这对于想要迅速开始编码的开发者来说非常有用。

  • 保持项目结构一致性:在团队协作中,Yeoman 可以确保每个新项目或组件都遵循相同的目录结构和编码规范,有助于维护代码的一致性和可读性。

  • 自动化工作流:Yeoman 可以集成任务运行器(如Grunt或Gulp)和包管理器(如npm或Bower),自动化重复性的任务,如压缩图片、编译CSS和JavaScript、运行测试等。

  • 快速集成前端库和框架:Yeoman 脚手架通常会包含流行的前端库和框架,如React、Angular、Vue等,这样开发者就不需要手动下载和配置这些依赖。

  • 创建自定义生成器:如果现有的脚手架不能满足特定需求,开发者可以创建自定义的Yeoman生成器,以适应特定的项目结构或编码规范。

总的来说,Yeoman 是一个适用于多种开发场景的工具,特别是在需要快速搭建和维护前端项目结构时。

创建generator

目录结构

创建一个文件夹,该文件夹必须命名为generator-name(其中name是generator的名称)。Yeoman 依赖文件系统来查找可用的generator。在该文件夹下创建package.json,并执行npm install --save yeoman-generator

package.json
{
"name": "generator-demo",
"version": "0.1.0",
"description": "",
"files": [
"generators"
],
"keywords": ["yeoman-generator"],
"dependencies": {
"yeoman-generator": "^1.0.0"
}
}

当您调用 yo name 时使用的默认generator是app generator。它必须包含在 app 目录中。当您调用 yo name:subcommand 时使用的sub generator包含在subcommand目录中。假如generator-demo目录结构如下(package.jsonfiles属性的值为["generators"]),则执行npm install -g yo generator-demo安装yogenerator-demo后,可以执行yo demoyo demo:router

├───package.json
└───generators/
├───app/
│ └───index.js
└───router/
└───index.js

目录结构还可以如下,此时package.jsonfiles属性的值应为["app","router"]

├───package.json
├───app/
│ └───index.js
└───router/
└───index.js

扩展yeoman-generator

Yeoman 提供了一个基础生成器,您可以扩展它来实现您自己的行为。这个基本生成器将添加您期望减轻任务的大部分功能。

generators/app/index.js
const Generator = require('yeoman-generator');

module.exports = class extends Generator {};

某些生成器方法只能在构造函数内部调用。这些特殊方法可能会执行诸如设置重要状态控制之类的操作,并且可能无法在构造函数之外运行。要重写生成器构造函数,请添加一个构造函数方法,如下所示:

generators/app/index.js
const Generator = require('yeoman-generator');

module.exports = class extends Generator {
constructor(args, opts) {
// Calling the super constructor is important so our generator is correctly set up
super(args, opts);

this.option('babel'); // This method adds support for a `--babel` flag
}

method1() {
this.log('method 1 just ran');
}

method2() {
this.log('method 2 just ran');
}
};

运行generator

每次运行generator时,实际上都在使用yeoman-environmentnpm install --save yeoman-environment

const yeoman = require('yeoman-environment');
const env = yeoman.createEnv();

注册generator-demo,有两种方式:

const yeoman = require('yeoman-environment');
const env = yeoman.createEnv();

// 1. 基于路径注册generator,namespace('npm:app')是可选的
env.register(require.resolve('generator-npm'), 'npm:app');

// 2. 提供 generator constructor,这种方式需要提供namespace
const GeneratorNPM = generators.Base.extend(/* put your methods in here */);
env.registerStub(GeneratorNPM, 'npm:app');

可以根据需要注册任意数量的generator。如果namespace发生冲突,本地generator将覆盖全局generator。

运行generator,您只需将如下代码放入 package.json bin 指定的文件中,即可运行 Yeoman generator,而无需使用 yo。本地开发使用npm link调试。

const yeoman = require('yeoman-environment');
const env = yeoman.createEnv();

env.register(require.resolve('generator-npm'), 'npm:app');

const done = function() {
console.log('done')
}
env.run('npm:app', done); // 可以设置options: env.run('npm:app some-name', { 'skip-install': true }, done);
tip

在使用脚手架工具(如Yeoman)创建项目时,你可能不希望立即安装所有依赖项,而是首先调整生成的项目结构或配置。在这种情况下,你可以使用'skip-install': true的配置或命令行选项来跳过自动安装依赖的步骤。

使用lookup获取npm安装的每个Yeoman generator的访问权限,每个查找到的generator会被注册

const yeoman = require('yeoman-environment');
const env = yeoman.createEnv();

env.lookup(function () {
env.run('angular');
});

方法执行顺序

直接附加到 Generator 原型的每个方法都被视为一个任务。每个任务都由 Yeoman environment 按顺序循环运行。

可用的优先级是(按运行顺序):

  1. initializing,初始化方法(检查当前项目状态、获取配置等)
  2. prompting,提示用户选项的地方(调用 this.prompt() 的地方)
  3. configuring,保存配置并配置项目(创建 .editorconfig 文件和其他元数据文件)
  4. default,如果方法名称与优先级不匹配,它将被推送到该组。
  5. writing,编写生成器特定文件(路线、控制器等)的地方
  6. conflicts,处理冲突的地方(内部使用)
  7. install,运行安装的地方(npm、bower)
  8. end,最后调用,用于清理等

如何定义不会自动调用的辅助方法或私有方法?有三种不同的方法可以实现这一目标:

  • 用下划线作为方法名称前缀

    class extends Generator {
    method1() {
    console.log('hey 1');
    }

    _private_method() {
    console.log('private hey');
    }
    }
  • 使用实例方法

    class extends Generator {
    constructor(args, opts) {
    // Calling the super constructor is important so our generator is correctly set up
    super(args, opts)

    this.helperMethod = function () {
    console.log('won\'t be called automatically');
    };
    }
    }
  • 继承父生成器

    class MyBase extends Generator {
    helper() {
    console.log('methods on the parent generator won\'t be called automatically');
    }
    }

    module.exports = class extends MyBase {
    exec() {
    this.helper();
    }
    };

用户交互

Yeoman 使用适配器作为抽象层,以允许 IDE、代码编辑器等轻松提供运行生成器所需的用户界面。

适配器是负责处理与用户的所有交互的对象。如果您想提供与经典命令行不同的交互模型,您必须编写自己的适配器。与用户交互的每种方法都通过此适配器传递(主要是:提示、日志记录和比较)。

要安装适配器,请使用 yeoman.createEnv(args, opts, adapter) 的第三个参数。适配器应至少提供三种方法:

  • prompt(),它提供问答功能(例如,当您开始时,会向用户提示一组可能的操作)。它的签名和行为遵循 Inquirer.js 的签名和行为。当生成器调用 this.prompt 时,该调用最终由适配器处理。该方法是异步的,返回一个Promise
  • diff()
  • log(),输出信息 Yeoman 默认提供了终端适配器(Terminal Adapter),可以使用this.prompt this.log this.diff
module.exports = class extends Generator {
async prompting() {
this.answers = await this.prompt([
{
type: "input",
name: "name",
message: "Your project name",
default: this.appname // Default to current folder name
},
{
type: "confirm",
name: "cool",
message: "Would you like to enable the Cool feature?"
}
]);

this.log("app name", answers.name);
this.log("cool feature", answers.cool);
}

writing() {
this.log("cool feature", this.answers.cool); // user answer `cool` used
}
};

记住用户偏好

用户每次运行生成器时可能会对某些问题给出相同的输入。对于这些问题,您可能想要记住用户之前回答的内容,并将该答案用作新的默认答案。

Yeoman 通过向问题对象添加 store 属性来扩展 Inquirer.js API。此属性允许您指定用户提供的答案应用作将来的默认答案。这可以按如下方式完成:

this.prompt({
type: "input",
name: "username",
message: "What's your GitHub username",
store: true
});

参数

参数直接从命令行传递,比如:yo webapp my-project

module.exports = class extends Generator {
// note: arguments and options should be defined in the constructor.
constructor(args, opts) {
super(args, opts);

// This makes `appname` a required argument.
this.argument("appname", { type: String, required: true });

// And you can then access it later; e.g.
this.log(this.options.appname);
}
};

选项

使用--,比如:yo webapp --coffee

module.exports = class extends Generator {
// note: arguments and options should be defined in the constructor.
constructor(args, opts) {
super(args, opts);

// This method adds support for a `--coffee` flag
this.option("coffee");

// And you can then access it later; e.g.
this.scriptSuffix = this.options.coffee ? ".coffee" : ".js";
}
};

位置上下文

.yo-rc.json

.yo-rc.json 文件是 Yeoman 生成器使用的配置文件。Yeoman 是一个通用的前端项目脚手架工具,用于自动化项目的搭建过程,比如创建新的项目、添加新的模块等。.yo-rc.json 文件存储了关于项目的配置信息,使得项目的结构和依赖能够被重现,这对于团队合作和项目的一致性非常重要。

这个文件通常位于项目的根目录,并且包含了用于生成项目的 Yeoman 生成器的配置选项。这些配置选项可以包括项目名称、版本、使用的技术栈、编码风格偏好等。

例如,如果你使用了一个名为 generator-webapp 的 Yeoman 生成器来创建一个 web 应用,.yo-rc.json 文件可能看起来像这样:

{
"generator-webapp": {
"appName": "My Awesome App",
"ui": {
"key": "value"
},
"wiredep": {
"directory": "bower_components"
}
}
}

这个文件的具体内容将取决于你使用的具体生成器以及在项目初始化过程中所做的选择。

当你或你的团队成员在项目中运行 Yeoman 生成器时,Yeoman 会查找 .yo-rc.json 文件,并使用其中的配置来确保生成的代码和结构与项目的其他部分保持一致。这样做有助于维护项目的一致性和可维护性。

根目录

可以使用 this.destinationRoot() 获取项目根目录 或 使用 this.destinationPath('sub/path') 获取根目录下的文件

// Given destination root is ~/projects
class extends Generator {
paths() {
this.destinationRoot();
// returns '~/projects'

this.destinationPath('index.js');
// returns '~/projects/index.js'
}
}

模板目录

模板上下文是存储模板文件的文件夹。模板上下文默认定义为 ./templates/。您可以使用 this.sourceRoot('new/template/path') 覆盖此默认值。

您可以使用 this.sourceRoot() 或使用 this.templatePath('app/index.js') 加入路径来获取路径值。

class extends Generator {
paths() {
this.sourceRoot();
// returns './templates'

this.templatePath('index.js');
// returns './templates/index.js'
}
};

this.fs

例如,使用 this.fs.copyTpl 方法复制文件,同时将内容作为模板进行处理。 copyTpl 使用 ejs 模板语法。

generators/app/templates/index.html
<html>
<head>
<title><%= title %></title>
</head>
</html>
class extends Generator {
writing() {
this.fs.copyTpl(
this.templatePath('index.html'),
this.destinationPath('public/index.html'),
{ title: 'Templating with Yeoman' }
);
}
}
public/index.html
<html>
<head>
<title>Templating with Yeoman</title>
</head>
</html>

一个非常常见的场景是在提示阶段存储用户答案并将其用于模板:

class extends Generator {
async prompting() {
this.answers = await this.prompt([{
type : 'input',
name : 'title',
message : 'Your project title',
}]);
}

writing() {
this.fs.copyTpl(
this.templatePath('index.html'),
this.destinationPath('public/index.html'),
{ title: this.answers.title } // user answer `title` used
);
}
}
tip

更新预先存在的文件并不总是一项简单的任务。最可靠的方法是解析文件 AST(抽象语法树)并对其进行编辑。此解决方案的主要问题是编辑 AST 可能很冗长并且有点难以掌握。

一些流行的 AST 解析器是:

  • Cheerio 用于解析 HTML。

  • 用于解析 JavaScript 的 Esprima - 您可能对 AST-Query 感兴趣,它提供了较低级别的 API 来编辑 Esprima 语法树。

对于 JSON 文件,您可以使用本机 JSON 对象方法

Gruntfile Editor 用于动态修改 Gruntfile。

使用 RegEx 解析代码文件是一条危险的道路,在此之前,您应该阅读此 CS 人类学答案并掌握 RegEx 解析的缺陷。如果您选择使用 RegEx 而不是 AST 树编辑现有文件,请小心并提供完整的单元测试。 - 请不要破坏您的用户的代码。

· 7 分钟阅读

https://snippet-generator.app/

在 Visual Studio Code (VS Code) 中,代码片段(snippets)是一种便捷的工具,用于快速插入常用的代码模式。代码片段以 JSON 格式定义,并存储在特定语言的代码片段文件中,例如 javascript.json。下面是代码片段格式的详细解释。

代码片段基本结构

代码片段的基本结构

一个代码片段文件包含一个或多个代码片段,每个代码片段都有一个唯一的键(名称),其值是一个对象,该对象定义了代码片段的详细信息。以下是代码片段的基本结构:

{
"Snippet Name": {
"prefix": "trigger",
"body": [
"line1",
"line2",
"line3"
],
"description": "Description of the snippet"
}
}

代码片段字段解释

  1. Snippet Name

    • 代码片段的名称,用于在代码片段文件中唯一标识每个代码片段。这个名称不会在 VS Code 中显示,但在文件中必须是唯一的。
  2. prefix

    • 触发该代码片段的前缀。用户在编辑器中输入这个前缀并按下 Tab 键或其他触发键时,代码片段会被插入。前缀通常是简短且易记的。
  3. body

    • 代码片段的主体,是一个字符串数组。每个数组元素表示代码片段中的一行代码。可以使用特殊占位符和变量来增强代码片段的功能。
    • 可以使用 $1, $2, ... 表示光标位置或占位符,用户可以按 Tab 键在这些位置之间跳转。
    • $0 表示最终光标位置。
    • ${varName} 表示变量,可以在代码片段插入时进行替换。
    • ${1:default} 表示带有默认值的占位符。
  4. description

    • 对代码片段的描述,显示在代码片段选择列表中,帮助用户理解该代码片段的用途。

以下是一个完整的示例,展示了如何定义一个 JavaScript 代码片段:

{
"Log to console": {
"prefix": "log",
"body": [
"console.log('$1');",
"$2"
],
"description": "Log output to console"
},
"Function template": {
"prefix": "func",
"body": [
"function ${1:functionName}(${2:params}) {",
" ${3:// TODO: implement}",
"}"
],
"description": "Create a function template"
}
}

解释示例:

  1. Log to console

    • prefix: log
    • body: 插入两行代码。第一行是 console.log('$1');,其中 $1 是一个占位符,用户可以在插入代码片段后立即输入内容。第二行是 $2,表示第二个光标位置,用户可以在这里继续输入内容。
    • description: "Log output to console"
  2. Function template

    • prefix: func
    • body: 插入一个函数模板。function ${1:functionName}(${2:params}) { 定义了函数名和参数,其中 ${1:functionName}${2:params} 是带有默认值的占位符。$3 是函数体的注释部分,用户可以在这里实现函数逻辑。
    • description: "Create a function template"

使用代码片段

  1. 创建或编辑代码片段文件

    • 打开 VS Code,按 Ctrl+Shift+P(Windows/Linux)或 Cmd+Shift+P(Mac)打开命令面板。
    • 输入 Preferences: Configure User Snippets 并选择你要创建或编辑代码片段的语言(例如 javascript)。
  2. 添加代码片段

    • 在打开的 JSON 文件中添加你的代码片段定义,保存文件。注意:在javascript.json中保存的代码片段通常在.js文件中才会提示
  3. 使用代码片段

    • 在编辑器中输入代码片段的前缀(例如 logfunc),然后按 Tab 键,代码片段会被插入到光标位置。

通过这种方式,你可以快速插入常用的代码模式,提高编码效率。

写脚本注入代码片段到VS Code

bin.js
#!/usr/bin/env node

const program = require('commander')
const { input, confirm, select, rawlist, number, editor } = require('@inquirer/prompts')

program.command('setSnippet')
.description('write snippet to vscode')
.action(async () => {
try {
const answerInfo = {
snippetType: await rawlist({
message: `请选择代码片段类型`,
choices: [
{
name: 'ts',
value: 'typescript',
},
{
name: 'tsx',
value: 'typescriptreact',
},
{
name: 'js',
value: 'javascript',
},
{
name: 'jsx',
value: 'javascriptreact',
},
{
name: 'scss',
value: 'scss',
},
{
name: 'vue',
value: 'vue',
},
],
}),
snippetPrefix: await input({ message: "请输入代码片段的唯一标识:", required: true }),
codeContent: await editor({
message: '请输入代码片段(将转换并注入到vscode中):',
required: true,
}),
}

serviceInstance.setSnippet(answerInfo)
} catch (error) {
if (error.isTtyError) {
console.error('当前环境不支持');
} else if (error.message === 'User force closed the prompt with 0 null') {
console.log('用户中断了输入');
} else {
console.error('发生错误:', error);
}
}
})
/**
* 注入代码片段到VS Code
*/
setSnippet(info) {
let snippets
// 转换为snippet格式
let newSnippet = convertToSnippet(info.codeContent, info.snippetPrefix)
// 获取 VS Code 用户代码片段文件路径
const snippetDirPath = path.join(process.env.HOME, 'Library', 'Application Support', 'Code', 'User', 'snippets')
const snippetFilePath = path.join(snippetDirPath, `${info.snippetType}.json`)
// 确保文件存在
fs.access(snippetFilePath, fs.constants.F_OK, (err) => {
if (err) {
// 文件不存在,创建文件
shell.cd(snippetDirPath)
shell.touch(`${info.snippetType}.json`)
snippets = newSnippet
} else {
// 文件存在
// 读取现有的代码片段文件
let fileContent = fs.readFileSync(snippetFilePath, 'utf8')
if (fileContent) {
// 添加新的代码片段
snippets = JSON.parse(fileContent)
Object.assign(snippets, newSnippet)
} else {
snippets = newSnippet
}
}
// 写回文件
fs.writeFileSync(snippetFilePath, JSON.stringify(snippets, null, 2), 'utf8')
const map = {
'typescript': 'ts',
'typescriptreact': 'tsx',
'javascript': 'js',
'javascriptreact': 'jsx',
'scss': 'scss',
'vue': 'vue',
}
console.log(`注入成功,快去${map[info.snippetType]}文件中使用吧`)
});
}
function convertToSnippet(code, prefix, description) {
if (!description) description = `${prefix}Snippet`;
// 将代码按行分割成数组
const codeLines = code.split('\n');

// 生成代码片段的主体
const snippetBody = codeLines.map(line => line.trim());

// 创建代码片段对象
const snippet = {
[description]: {
"prefix": prefix,
"body": snippetBody,
"description": description
}
};

return snippet;
// 将对象转换为 JSON 字符串并格式化
// return JSON.stringify(snippet, null, 2);
}

module.exports = {
convertToSnippet,
}

· 14 分钟阅读

搭建项目

  1. npm init -y 生成package.json,删除其中的"main": "index.js",设置"name": "react-map-repository"避免和子包name冲突,设置 "packageManager": "pnpm@9.0.6"避免使用turbo时报错missing packageManager field in package.json
  2. 创建pnpm-workspace.yaml
    pnpm-workspace.yaml
    packages:
    - 'packages/*'
  3. 创建packages目录,创建packages/react-map目录,创建packages/core目录
  4. pnpm add -D typescript -w
  5. 创建tsconfig.jsontsconfig.build.json
    tsconfig.json
    {
    "extends": "./tsconfig.build.json",
    "compilerOptions": {
    "baseUrl": "./packages",
    "paths": {
    "@react-map/core": ["core/src"],
    "react-map": ["react-map/src"]
    }
    }
    }
    tsconfig.build.json
    {
    "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "declaration": true,
    "esModuleInterop": true,
    "jsx": "react",
    "lib": ["dom", "es2020"],
    "moduleResolution": "node",
    "strict": true,
    "target": "es2020"
    },
    "include": ["./src/**/*"]
    }
  6. pnpm add -D react react-dom -w
  7. pnpm add -D leaflet @types/leaflet -w
  8. pnpm add -D turbo -w
  9. 创建turbo.json
    turbo.json
    {
    "tasks": {
    "build:clean": {},
    "build:js": {}
    }
    }
    注意使用tasks而不是pipeline
      × found `pipeline` field instead of `tasks`
    ╭─[turbo.json:1:1]
    1 │ {
    2 │ ╭─▶ "pipeline": {
    3 │ │ "build:clean": {},
    4 │ │ "build:js": {}
    5 │ ├─▶ }
    · ╰──── rename `pipeline` field to `tasks`
    6 │ }
    ╰────
    help: changed in 2.0: `pipeline` has been renamed to `tasks`
  10. pnpm add -D @swc/cli @swc/core -w
  11. 创建.swcrc
    .swcrc
    {
    "$schema": "https://swc.rs/schema.json",
    "jsc": {
    "parser": {
    "syntax": "typescript",
    "tsx": true
    },
    "target": "es2020"
    }
    }
  12. pnpm add --save-dev @biomejs/biome -w
  13. 创建biome.json
    biome.json
    {
    "$schema": "https://biomejs.dev/schemas/1.7.1/schema.json",
    "organizeImports": {
    "enabled": true
    },
    "formatter": {
    "enabled": true,
    "formatWithErrors": false,
    "ignore": [],
    "attributePosition": "auto",
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 80
    },
    "javascript": {
    "formatter": {
    "arrowParentheses": "always",
    "bracketSameLine": true,
    "bracketSpacing": true,
    "jsxQuoteStyle": "double",
    "quoteProperties": "asNeeded",
    "quoteStyle": "single",
    "semicolons": "asNeeded",
    "trailingComma": "all"
    }
    },
    "linter": {
    "enabled": true,
    "ignore": ["lib/**", "__tests__/**"],
    "rules": {
    "recommended": true
    }
    }
    }
  14. 创建.gitignore
    node_modules
    .turbo
  15. 创建README.md
  16. pnpm add -D del-cli -w
  17. pnpm add -D cross-env -w
  18. 更新项目根目录package.jsonscripts
    package.json
    "scripts": {
    "lint": "biome check --apply ./packages",
    "test": "jest",
    "build": "turbo run build:clean && pnpm run -r build:types && turbo run build:js"
    },
  19. cd packages/core/然后npm init -y 生成package.json,设置name main types exports files sideEffects
    packages/core/package.json
    {
    "name": "@react-map/core",
    "main": "lib/index.js",
    "types": "lib/index.d.ts",
    "exports": {
    ".": "./lib/index.js"
    },
    "files": ["lib/*"],
    "sideEffects": false,
    }
  20. packages/core/中安装依赖:pnpm add -D @types/react @types/react-dom
  21. 手动设置peerDependencies
    packages/core/package.json
    "peerDependencies": {
    "leaflet": "^1.9.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
    },
  22. 创建packages/core/tsconfig.json
    packages/core/tsconfig.json
    {
    "extends": "../../tsconfig.build.json",
    "compilerOptions": {
    "outDir": "./lib"
    },
    "include": ["./src/**/*"]
    }
  23. 创建packages/core/srcpackages/core/src/index.tspackages/core/README.mdpackages/core/.gitignore
    packages/core/.gitignore
    /coverage
    /lib
  24. 更新packages/core/package.jsonscripts
    packages/core/package.json
    "scripts": {
    "build:clean": "del lib",
    "build:js": "swc src -d ./lib --config-file ../../.swcrc --strip-leading-paths",
    "build:types": "tsc --emitDeclarationOnly",
    "build": "pnpm run build:clean && pnpm run build:types && pnpm run build:js",
    "test:types": "tsc --noEmit",
    "test:unit": "cross-env NODE_ENV=test jest",
    "test": "pnpm run test:types && pnpm run test:unit",
    "start": "pnpm run test && pnpm run build",
    "prepare": "pnpm run build"
    },
  25. cd ../react-map/然后npm init -y 生成package.json,设置main types exports files sideEffects
    packages/react-map/package.json
    {
    "main": "lib/index.js",
    "types": "lib/index.d.ts",
    "exports": {
    ".": "./lib/index.js",
    "./*": "./lib/*.js"
    },
    "files": ["lib/*"],
    "sideEffects": false,
    }
  26. 创建packages/react-map/tsconfig.json
    packages/react-map/tsconfig.json
    {
    "extends": "../../tsconfig.build.json",
    "compilerOptions": {
    "outDir": "./lib"
    },
    "include": ["./src/**/*"]
    }
  27. packages/react-map/中安装依赖:pnpm add -D @types/leaflet @types/react @types/react-dom; pnpm add @react-map/core
    packages/react-map/package.json
    {
    "dependencies": {
    "@react-map/core": "workspace:^"
    }
    }
  28. 手动设置peerDependencies
    packages/react-map/package.json
    {
    "peerDependencies": {
    "leaflet": "^1.9.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
    },
    }
  29. 创建packages/react-map/srcpackages/react-map/src/index.tspackages/react-map/README.mdpackages/react-map/.gitignore
    packages/react-map/.gitignore
    /coverage
    /lib
  30. 更新packages/react-map/package.jsonscripts
    packages/react-map/package.json
    {
    "scripts": {
    "build:clean": "del lib",
    "build:js": "swc src -d ./lib --config-file ../../.swcrc --strip-leading-paths",
    "build:types": "tsc --emitDeclarationOnly",
    "build": "pnpm run build:clean && pnpm run build:types && pnpm run build:js",
    "test:types": "tsc --noEmit",
    "test:unit": "cross-env NODE_ENV=test jest",
    "test": "pnpm run test:types && pnpm run test:unit",
    "start": "pnpm run test && pnpm run build",
    "prepare": "pnpm run build"
    },
    }

版本管理

使用Changesets:

  1. 在项目根目录下安装依赖:pnpm add -D @changesets/cli -w

  2. 初始化 Changesets 配置: pnpm [exec] changeset init,这将创建一个 .changeset 目录和配置文件.changeset/config.json

    .changeset/config.json
    {
    "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
    "changelog": "@changesets/cli/changelog", // changelog 生成方式
    "commit": false, // 不要让 changeset 在 publish 的时候帮我们做 git add
    "fixed": [],
    "linked": [], // 配置哪些包要共享版本
    "access": "restricted", // 公私有安全设定,内网建议 restricted ,开源使用 public
    "baseBranch": "main", // 项目主分支
    "updateInternalDependencies": "patch", // 确保某包依赖的包发生 upgrade,该包也要发生 version upgrade 的衡量单位(量级)
    "ignore": [] // 不需要变动 version 的包
    }
  3. 添加变更文件:pnpm [exec] changeset(注意:该命令生效的前提是远程存在配置文件中指定的baseBranch分支,默认为main添加变更文件 添加变更文件 添加变更文件 成功执行该命令后,会在.changeset目录生成一个变更文件: 生成变更文件

  4. 当你准备好发布时,执行 pnpm [exec] changeset version 命令来应用变更文件中的更改,并更新包的版本号。成功执行该命令后,将更新 package.json文件中的version,并且生成 CHANGELOG.md 文件,以记录这些更改的详细信息。同时,相应的变更文件被消耗,即会被自动删除。

  5. 执行 pnpm [exec] changeset publish 命令来发布更新的包到 npm。成功执行该命令后,将自动根据更新的版本号发布包,并将变更文件移动到 .changeset/README.md 中。

  6. 将命令集成到项目根目录package.jsonscripts中:

    package.json
    {
    "scripts": {
    // Include build, lint, test - all the things you need to run
    // before publishing
    "publish-packages": "turbo run build lint test && changeset version && changeset publish"
    }
    }

不知道为啥执行pnpm [exec] changeset时,没有选择semver bump type的引导,默认是major

🦋  Which semver bump type should this change have? …
❯◯ patch
◯ minor
◯ major

可以在生成的变更文件中手动修改semver bump type

本地测试npm包

  1. cd packages/然后执行pnpm create vite创建一个react项目
    ✔ Project name: demo
    ✔ Select a framework: › React
    ✔ Select a variant: › TypeScript
  2. cd packages/demo安装依赖:pnpm add react-map
    packages/demo/package.json
    {
    "dependencies": {
    // ...
    "react-map": "workspace:^"
    },
    }

开发组件

泛型类型参数默认值

在 TypeScript 中,Props extends MapContainerProps = MapContainerProps 这种语法用于定义泛型类型参数的默认值。

  • Props:这是一个泛型类型参数的名称。
  • extends MapContainerProps:这表示 Props 必须是 MapContainerProps 或其子类型。
  • = MapContainerProps:这表示如果在使用这个泛型时没有显式地提供 Props 类型参数,那么 Props 的默认类型将是 MapContainerProps

组件库的依赖

组件的依赖使用peerDependenciesdependenciesdevDependencies:

  • dependencies 用于依赖的自开发组件库
  • peerDependencies 用于依赖的基本库和插件,从 npm v7 开始会自动安装peerDependencies指定的包(pnpm也会自动安装peerDependencies指定的包)
    tip

    使用pnpm add --save-peer <pkg> 将安装依赖到 peerDependencies 和 devDependencies

  • devDependencies 用于依赖的类型声明
{
"devDependencies": {
"@types/leaflet": "^1.9.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0"
},
"peerDependencies": {
"@geoman-io/leaflet-geoman-free": "^2.16.0",
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"dependencies": {
"react-map-core": "^1.0.0"
},
}

遇到的问题

  1. 在 Monorepo 项目中,开发的两个包都需要发布,并且这两个包有依赖关系,这个时候使用pnpm changeset publish发布两个包时发生了报错:Cannot find module 'react-map-core' or its corresponding type declarations. 有依赖关系的包是不是需要分开发布(按依赖关系先后发布)?

  2. 当在执行 pnpm changeset publish 或其他 npm 发布命令时,遇到提示 This operation requires a one-time password from your authenticator,这意味着你的 npm 账户启用了双因素认证(2FA)。在这种情况下,你需要提供一次性密码(OTP)才能完成发布操作。如果是发布内部包,则需要设置publishConfig,避免发布到外部npm上。

  3. 测试npm包的项目启动时报错:UnhandledPromiseRejectionWarning: SyntaxError: Unexpected token '??=',这个错误通常是由于你的 JavaScript 环境(如 Node.js 或浏览器)不支持新的语法特性。??= 是一个新的逻辑赋值运算符(Nullish Coalescing Assignment),它在较新的 ECMAScript 版本中引入。更换node版本到16即可。

  4. Argument of type '(e: DrawEvent) => void' is not assignable to parameter of type 'LeafletEventHandlerFn'. 使用类型断言as unknown as LeafletEventHandlerFn解决,如:map.on('draw:created', handleDrawEvent as unknown as LeafletEventHandlerFn);

  5. 使用 pnpm -F 过滤项目执行命令时报错:No projects matched the filters。假如项目结构如下:

    my-monorepo/
    ├── package.json
    ├── pnpm-workspace.yaml
    ├── packages/
    │ ├── package-a/
    │ │ ├── package.json
    │ │ └── index.js
    │ └── package-b/
    │ ├── package.json
    │ └── index.js
    pnpm-workspace.yaml
    packages:
    - 'packages/*'

    执行pnpm -F package-a build报错:No projects matched the filters,这是因为package-apackage.jsonname不是package-a,如下情况应该执行pnpm -F a build。如果package-apackage.jsonname@example/a,则还是执行pnpm -F a build

    package-a/package.json
    {
    "name": "a",
    "version": "1.0.0",
    "scripts": {
    "build": "echo Building package-a"
    }
    }
  6. 在 TypeScript 中,接口(interface)只能扩展对象类型或具有静态已知成员的对象类型的交集。

    在 TypeScript 中,接口(interface)只能扩展对象类型或具有静态已知成员的对象类型的交集。如果你尝试扩展一个不符合这些条件的类型,就会遇到类似 An interface can only extend an object type or intersection of object types with statically known members 的错误。

    确保你扩展的类型是一个对象类型或对象类型的交集。以下是一些常见的解决方法:

    // 确保你扩展的类型是一个对象类型
    interface Person {
    name: string;
    age: number;
    }

    interface Employee extends Person {
    employeeId: number;
    }
    // 如果你需要扩展多个类型,可以使用类型别名和交叉类型
    type Person = {
    name: string;
    age: number;
    };

    type Contact = {
    email: string;
    phone: string;
    };

    interface Employee extends Person, Contact {
    employeeId: number;
    }

    确保你扩展的类型是静态已知的,而不是动态生成的。例如,不要扩展一个泛型类型参数,除非它被约束为一个对象类型。

    // 扩展对象类型
    interface Person {
    name: string;
    age: number;
    }

    interface Employee extends Person {
    employeeId: number;
    }

    const employee: Employee = {
    name: "John Doe",
    age: 30,
    employeeId: 1234
    };
    // 使用类型别名和交叉类型
    type Person = {
    name: string;
    age: number;
    };

    type Contact = {
    email: string;
    phone: string;
    };

    interface Employee extends Person, Contact {
    employeeId: number;
    }

    const employee: Employee = {
    name: "John Doe",
    age: 30,
    email: "john.doe@example.com",
    phone: "123-456-7890",
    employeeId: 1234
    };

    如果你需要扩展一个泛型类型参数,确保它被约束为一个对象类型:

    interface Person {
    name: string;
    age: number;
    }

    interface Employee<T extends Person> {
    employeeId: number;
    details: T;
    }

    const employee: Employee<Person> = {
    employeeId: 1234,
    details: {
    name: "John Doe",
    age: 30
    }
    };

    以下是一些会导致错误的示例:

    // 错误:不能扩展非对象类型
    interface Employee extends string {
    employeeId: number;
    }
    // 错误:不能扩展动态生成的类型
    function createType<T>() {
    return class {
    value: T;
    };
    }

    interface Employee extends createType<number> {
    employeeId: number;
    }

发布包

在 Monorepo 项目中,使用 workspace: 协议来管理依赖关系是一种常见的做法,特别是在使用 Yarn Workspaces 或 pnpm Workspaces 时。workspace: 协议允许你在 Monorepo 项目中引用其他工作区包,而不需要发布它们到 npm registry。

Changesets 在生成版本时会自动处理 workspace: 协议,将其替换为实际的版本号。以下是 Changesets 的工作原理:

  1. 创建变更集:当你运行 pnpm changeset 时,Changesets 会引导你创建一个变更集文件,记录需要发布的包和版本号。
  2. 生成版本:当你运行 pnpm changeset version 时,Changesets 会根据变更集文件生成新的版本号,并自动将 workspace: 协议替换为实际的版本号。
  3. 发布包:当你运行 pnpm changeset publish 时,Changesets 会将新版本发布到 npm registry。
tip
  1. 在每个要发布的包的 package.json 文件中,配置 publishConfig 字段以指定发布时使用的 npm registry。
package.json
{
"publishConfig": {
"registry": "https://your-internal-registry.com/"
},
}
  1. 执行pnpm changeset publish前,先登录npm registry: npm login
  2. 执行pnpm publish发布包的话也能自动将 workspace: 协议替换为实际的版本号。

· 7 分钟阅读

Cornerstone.js

Cornerstone.js 是一个用于医疗图像处理的 JavaScript 库,主要用于在 Web 应用中显示和操作 DICOM 图像。它提供了丰富的功能来处理医学影像数据,支持放大、缩小、平移、窗宽窗位调整等功能。

以下是一些关于如何使用 Cornerstone.js 的基本示例和步骤。

首先,你需要安装 Cornerstone.js。可以通过 npm 安装:

npm install cornerstone-core
npm install cornerstone-tools

基本使用

  1. 引入 Cornerstone.js

    在你的 JavaScript 文件中引入 Cornerstone.js 和相关工具:

    import cornerstone from 'cornerstone-core';
    import cornerstoneTools from 'cornerstone-tools';
  2. 设置 HTML 结构

    创建一个容器来显示图像:

    <div id="dicomImage" style="width: 512px; height: 512px;"></div>
  3. 初始化 Cornerstone.js

    在你的 JavaScript 文件中初始化 Cornerstone.js:

    const element = document.getElementById('dicomImage');
    cornerstone.enable(element);
  4. 加载和显示 DICOM 图像

    使用 Cornerstone.js 加载和显示 DICOM 图像:

    const imageId = 'wadouri:http://example.com/path/to/your/dicom/file.dcm';

    cornerstone.loadImage(imageId).then(function(image) {
    cornerstone.displayImage(element, image);
    });

添加工具

Cornerstone Tools 提供了很多有用的工具来与图像进行交互,比如缩放、平移、窗宽窗位调整等。以下是如何添加一些基本工具:

// 启用工具
cornerstoneTools.init();

// 添加缩放工具
cornerstoneTools.addTool(cornerstoneTools.ZoomTool);
cornerstoneTools.setToolActive('Zoom', { mouseButtonMask: 1 });

// 添加平移工具
cornerstoneTools.addTool(cornerstoneTools.PanTool);
cornerstoneTools.setToolActive('Pan', { mouseButtonMask: 2 });

// 添加窗宽窗位调整工具
cornerstoneTools.addTool(cornerstoneTools.WwwcTool);
cornerstoneTools.setToolActive('Wwwc', { mouseButtonMask: 4 });

以下是一个完整的示例,展示了如何使用 Cornerstone.js 和 Cornerstone Tools 来加载和显示 DICOM 图像,并添加一些基本的交互工具:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cornerstone.js Example</title>
<style>
#dicomImage {
width: 512px;
height: 512px;
border: 1px solid black;
}
</style>
</head>
<body>
<div id="dicomImage"></div>
<script src="https://unpkg.com/cornerstone-core"></script>
<script src="https://unpkg.com/cornerstone-tools"></script>
<script>
// 初始化 Cornerstone.js
const element = document.getElementById('dicomImage');
cornerstone.enable(element);

const imageId = 'wadouri:http://example.com/path/to/your/dicom/file.dcm';

// 加载和显示 DICOM 图像
cornerstone.loadImage(imageId).then(function(image) {
cornerstone.displayImage(element, image);
});

// 启用工具
cornerstoneTools.init();

// 添加缩放工具
cornerstoneTools.addTool(cornerstoneTools.ZoomTool);
cornerstoneTools.setToolActive('Zoom', { mouseButtonMask: 1 });

// 添加平移工具
cornerstoneTools.addTool(cornerstoneTools.PanTool);
cornerstoneTools.setToolActive('Pan', { mouseButtonMask: 2 });

// 添加窗宽窗位调整工具
cornerstoneTools.addTool(cornerstoneTools.WwwcTool);
cornerstoneTools.setToolActive('Wwwc', { mouseButtonMask: 4 });
</script>
</body>
</html>

以上示例展示了如何使用 Cornerstone.js 来加载和显示 DICOM 图像,并添加一些基本的交互工具。Cornerstone.js 是一个功能强大的库,可以帮助开发者在 Web 应用中处理医学影像数据。如果你需要更复杂的功能,可以参考 Cornerstone.js 和 Cornerstone Tools 的官方文档。

类似的库

除了 Cornerstone.js 之外,还有其他一些库和工具可以用于处理和显示医学影像数据,特别是 DICOM 图像。以下是一些类似的库和工具:

1. OHIF Viewer

OHIF Viewer 是一个基于 Web 的 DICOM 图像查看器,使用了 Cornerstone.js 作为其核心图像处理库。它提供了一个完整的、功能丰富的 DICOM 查看器,支持多种医学影像处理功能。

  • 特点

    • 支持多种视图模式(如 MPR、多平面重建)。
    • 支持 DICOMweb 和 WADO-RS 协议。
    • 提供丰富的工具集,如测量工具、注释工具等。
    • 可扩展性强,适合集成到现有的医疗影像系统中。
  • 网址OHIF Viewer

2. Papaya

Papaya 是一个基于 Web 的医学影像查看器,支持多种医学影像格式,包括 NIFTI、DICOM 等。它是一个轻量级的、跨平台的查看器,适用于快速查看和处理医学影像数据。

  • 特点

    • 支持多种医学影像格式。
    • 提供多种图像处理和分析工具。
    • 轻量级,易于集成和使用。
  • 网址Papaya

3. AMI (A Medical Imaging Library)

AMI 是一个基于 Three.js 的开源库,用于在 Web 上处理和显示医学影像数据。它支持 2D 和 3D 图像显示,并提供了丰富的图像处理和分析功能。

  • 特点

    • 基于 Three.js,支持 3D 图像显示。
    • 支持多种医学影像格式,如 DICOM、NIFTI 等。
    • 提供丰富的图像处理和分析工具。
  • 网址AMI

4. Med3Web

Med3Web 是一个用于医学影像数据可视化的开源 Web 应用。它支持 2D 和 3D 图像显示,并提供了多种图像处理和分析工具。

  • 特点

    • 支持 2D 和 3D 图像显示。
    • 提供多种图像处理和分析工具。
    • 支持多种医学影像格式。
  • 网址Med3Web

5. VTK.js

VTK.js 是一个用于科学数据可视化的 JavaScript 库,基于 VTK(Visualization Toolkit)。虽然 VTK.js 并不是专门为医学影像设计的,但它非常强大,可以用于处理和显示复杂的医学影像数据。

  • 特点

    • 支持 2D 和 3D 数据的可视化。
    • 提供丰富的数据处理和分析工具。
    • 可扩展性强,适合构建复杂的医学影像应用。
  • 网址VTK.js

结论

这些库和工具各有特点,可以根据你的具体需求选择合适的工具。如果你需要一个功能丰富且易于集成的解决方案,OHIF Viewer 和 Papaya 是不错的选择。如果你需要更多的定制和扩展能力,AMI 和 VTK.js 是很好的选择。希望这些信息对你有所帮助!

· 6 分钟阅读

浅谈单点登录 SSO 实现方案

关于鉴权,看懂这篇就够了

JWT

JSON Web Token(JWT) 入门教程

Learn how to use JSON Web Tokens (JWT) for Authentication

JWT 的特点:

  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

  • JWT 不加密的情况下,不能将秘密数据写入 JWT。

  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

  • JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

JWT 的原理

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户。以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT 的数据结构

jwt

JWT 的三个部分依次如下,中间用点(.)分隔:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子:

{
"alg": "HS256", // 签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256)
"typ": "JWT" // 表示这个令牌(token)的类型(type),JWT 令牌统一写为"JWT"。
}

需要将上面的 JSON 对象使用 Base64URL 算法 转成字符串。

Payload

Payload 部分也是一个 JSON 对象(这个 JSON 对象也要使用 Base64URL 算法转成字符串),用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

  • iss:签发人 (issuer)
  • exp:过期时间 (expiration time)
  • sub:主题 (subject)
  • aud:受众 (audience)
  • nbf:生效时间 (Not Before)
  • iat:签发时间 (Issued At)
  • jti:编号 (JWT ID)

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子:

{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

Base64URL 算法

这个算法跟 Base64 算法基本类似,但有一些小的不同。JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法。

JWT 的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息 Authorization 字段里面。

Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

如何强制JWT令牌失效

OAuth2.0实战!退出登录时如何让JWT令牌失效?

如何自动刷新JWT令牌

· 2 分钟阅读

对于大多数现代编码系统(如 UTF-8),每个汉字通常占用 3 个字节,而每个英文字母、数字和常见符号通常占用 1 个字节。

如下示例,让我们逐个字符计算:

Q3NKA综合-头客-优选服务立减测试_优惠详情:7418945413963368458
  • Q3NKA:5 个英文字母和数字,每个占 1 个字节,共 5 个字节。
  • 综合:2 个汉字,每个占 3 个字节,共 6 个字节。
  • -:2 个减号,每个占 1 个字节,共 2 个字节。
  • 头客:2 个汉字,每个占 3 个字节,共 6 个字节。
  • 优选服务立减测试:8 个汉字,每个占 3 个字节,共 24 个字节。
  • _:1 个下划线,占 1 个字节。
  • 优惠详情:4 个汉字,每个占 3 个字节,共 12 个字节。
  • ::1 个冒号,占 1 个字节。
  • 7418945413963368458:19 个数字,每个占 1 个字节,共 19 个字节。

总字节数为:

5 + 6 + 2 + 6 + 24 + 1 + 12 + 1 + 19 = 76 个字节