跳到主要内容

· 1 分钟阅读

PV具体是指网站的是页面浏览量或者点击量,页面被刷新一次就计算一次,UV指访问网站的一台电脑客户端为一个访客。UV访客的统计是24小时内相同的客户端只被计算一次。一个UV可以用很多PV,一个PV也只能对应一个IP。PV是和IP的数量是成正比的,因为页面被刷新一次那么PV就会被记录一次,所以IP越多,说明网站的PV数据也就随之增多。但是需要注意的是PV并不是网站的页面的访问者数量,而是网站被访问的页面数量。因为一个访问者可以多次刷新页面,增加PV数量。

· 5 分钟阅读

react-spring 是一个用于构建交互式、数据驱动和动画 UI 组件的库。它可以为 HTML、SVG、Native Elements、Three.js 等制作动画。

yarn add @react-spring/web
pnpm add @react-spring/web

使用 react-spring 打造流暢的使用者體驗

animated组件

处理动画的实际组件是animated组件,其是一个高阶组件 (HOC)。(高阶组件是一个接受一个组件并返回一个新组件的函数)

import { animated } from '@react-spring/web'

export default function MyComponent() {
return (
<animated.div
style={{
width: 80,
height: 80,
background: '#ff6d6d',
borderRadius: 8,
}}
/>
)
}

API

Globals

Globals 是一个内部对象,用于配置全局属性,比如默认的弹簧配置或插值器。这些全局设置会影响所有 react-spring 动画的默认行为。请注意,Globals 的使用应该谨慎,因为它会影响你应用中所有的 react-spring 动画。通常,它只在应用初始化时设置一次,并且很少需要修改。

skipAnimation: 如果skipAnimation设置为true,那么动画将“跳转”到目标值,类似于immediate属性为true时。

import { useRef, useEffect } from 'react'
import { useSpring, animated, Globals } from '@react-spring/web'

export default function MyApp() {
const isRight = useRef(false)

const [springs, api] = useSpring(
() => ({
x: 0,
}),
[]
)

const handleClick = () => {
api.start({
x: isRight.current ? 0 : 200,
onRest: () => {
isRight.current = !isRight.current
},
})
}

useEffect(() => {
Globals.assign({
skipAnimation: true,
})

return () => {
Globals.assign({
skipAnimation: false,
})
}
})

return (
<animated.div onClick={handleClick} className="spring-box" style={springs}>
Click me!
</animated.div>
)
}

Hooks

useSpring

useSpring 实际上并没有为任何东西设置动画。它只是返回我们传递给animated组件的 SpringValues。以下是一些常用的属性:

  • from: 定义动画开始时的样式或状态。
  • to: 定义动画结束时的样式或状态。
  • config: 定义动画的配置,如弹簧的张力(tension)、摩擦力(friction)等。
  • delay: 设置动画延迟开始的时间(以毫秒为单位)。
  • reset: 如果设置为 true,动画将重置到 from 状态并重新开始。
  • reverse: 如果设置为 true,动画将从 to 状态反向运行到 from 状态。
  • immediate: 如果设置为 true 或一个函数返回 true,动画将立即跳转到结束状态,不会有过渡效果。
  • onStart: 动画开始时的回调函数。
  • onRest: 动画静止时的回调函数。
  • onUpdate: 动画更新时的回调函数。
  • loop: 如果设置为 true 或一个函数,动画将循环播放。
  • pause: 如果设置为 true,动画将暂停。

如下将useSpring返回的springs应用于animated组件,则初始化时发生动画(向右移动):

import { useSpring, animated } from '@react-spring/web'

export default function MyComponent() {
const springs = useSpring({
from: { x: 0 },
to: { x: 100 },
})

return (
<animated.div
style={{
width: 80,
height: 80,
background: '#ff6d6d',
borderRadius: 8,
...springs,
}}
/>
)
}

useSpring接收两种类型的参数,一种是如上的config对象,另一种是函数。当其参数是函数时,它返回一个数组:

import { useSpring, animated } from '@react-spring/web'

export default function MyComponent() {
const [springs, api] = useSpring(() => ({ // api用于控制springs
from: { x: 0 },
}))

const handleClick = () => {
api.start({ // api有许多不同的方法,我们可以使用它们来控制动画。
from: {
x: 0,
},
to: {
x: 100,
},
})
}

return (
<animated.div
onClick={handleClick}
style={{
width: 80,
height: 80,
background: '#ff6d6d',
borderRadius: 8,
...springs,
}}
/>
)
}

cancel属性

cancel设置为true时,动画将停止工作,设置为false时,动画将重新开始。

useReducedMotion

· 6 分钟阅读

Shell知识

1. if else-if else

if condition1
then
command1
elif condition2
then
command2
else
commandN
fi
  • if else 的 [...] 判断语句中大于使用 -gt,小于使用 -lt
if [ "$a" -gt "$b" ]; then
...
fi
  • 如果使用 ((...)) 作为判断语句,大于和小于可以直接使用 ><
if (( a > b )); then
...
fi

2. shell条件判断if中的-a到-z的意思

  • [ -a FILE ] 如果 FILE 存在则为真。

  • [ -b FILE ] 如果 FILE 存在且是一个块特殊文件则为真。

  • [ -c FILE ] 如果 FILE 存在且是一个字特殊文件则为真。

  • [ -d FILE ] 如果 FILE 存在且是一个目录则为真。

  • [ -e FILE ] 如果 FILE 存在则为真。

  • [ -f FILE ] 如果 FILE 存在且是一个普通文件则为真。

  • [ -g FILE ] 如果 FILE 存在且已经设置了SGID则为真。

  • [ -h FILE ] 如果 FILE 存在且是一个符号连接则为真。

  • [ -k FILE ] 如果 FILE 存在且已经设置了粘制位则为真。

  • [ -p FILE ] 如果 FILE 存在且是一个名字管道(F如果O)则为真。

  • [ -r FILE ] 如果 FILE 存在且是可读的则为真。

  • [ -s FILE ] 如果 FILE 存在且大小不为0则为真。

  • [ -t FD ] 如果文件描述符 FD 打开且指向一个终端则为真。

  • [ -u FILE ] 如果 FILE 存在且设置了SUID (set user ID)则为真。

  • [ -w FILE ] 如果 FILE 如果 FILE 存在且是可写的则为真。

  • [ -x FILE ] 如果 FILE 存在且是可执行的则为真。

  • [ -O FILE ] 如果 FILE 存在且属有效用户ID则为真。

  • [ -G FILE ] 如果 FILE 存在且属有效用户组则为真。

  • [ -L FILE ] 如果 FILE 存在且是一个符号连接则为真。

  • [ -N FILE ] 如果 FILE 存在 and has been mod如果ied since it was last read则为真。

  • [ -S FILE ] 如果 FILE 存在且是一个套接字则为真。

  • [ FILE1 -nt FILE2 ] 如果 FILE1 has been changed more recently than FILE2, or 如果 FILE1 exists and FILE2 does not则为真。

  • [ FILE1 -ot FILE2 ] 如果 FILE1 比 FILE2 要老, 或者 FILE2 存在且 FILE1 不存在则为真。

  • [ FILE1 -ef FILE2 ] 如果 FILE1 和 FILE2 指向相同的设备和节点号则为真。

  • [ -o OPTIONNAME ] 如果 shell选项 “OPTIONNAME” 开启则为真。

  • [ -z STRING ] “STRING” 的长度为零则为真。

  • [ -n STRING ] or [ STRING ] “STRING” 的长度为非零 non-zero则为真。

  • [ STRING1 == STRING2 ] 如果2个字符串相同。 “=” may be used instead of “==” for strict POSIX compliance则为真。

  • [ STRING1 != STRING2 ] 如果字符串不相等则为真。

  • [ STRING1 < STRING2 ] 如果 “STRING1” sorts before “STRING2” lexicographically in the current locale则为真。

  • [ STRING1 > STRING2 ] 如果 “STRING1” sorts after “STRING2” lexicographically in the current locale则为真。

  • [ ARG1 OP ARG2 ]“OP” is one of -eq, -ne, -lt, -le, -gt or -ge. These arithmetic binary operators return true if “ARG1” is equal to, not equal to, less than, less than or equal to, greater than, or greater than or equal to “ARG2”, respectively. “ARG1” and “ARG2” are integers.

3. echo 用于字符串的输出

  • 显示普通字符串 echo "It is a test" 或者 echo It is a test

  • 显示转义字符 echo "\"It is a test\""

  • 显示变量 echo "$name It is a test"

  • 显示换行

echo -e "OK! \n" # -e 开启转义
echo "It is a test"

结果:

OK!

It is a test

4. 函数

  • 定义一个函数并进行调用
#!/bin/bash

demoFun(){
echo "这是我的第一个 shell 函数!"
}
echo "-----函数开始执行-----"
demoFun
echo "-----函数执行完毕-----"

## 结果:
-----函数开始执行-----
这是我的第一个 shell 函数!
-----函数执行完毕-----
  • 定义一个带有return语句的函数
#!/bin/bash

funWithReturn(){
echo "这个函数会对输入的两个数字进行相加运算..."
echo "输入第一个数字: "
read aNum
echo "输入第二个数字: "
read anotherNum
echo "两个数字分别为 $aNum$anotherNum !"
return $(($aNum+$anotherNum))
}
funWithReturn
echo "输入的两个数字之和为 $? !"

## 结果:
这个函数会对输入的两个数字进行相加运算...
输入第一个数字:
1
输入第二个数字:
2
两个数字分别为 12 !
输入的两个数字之和为 3 !

5. ps 用于显示当前进程的状态

JS Shell

  • zx

· 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 分钟阅读

· 39 分钟阅读

文件上传

单文件上传

<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可能带来的服务器负载增加和开发复杂度提升等问题。

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'只是举例),这样在这个总的宽度之内去设置列宽,才能生效。

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;
},

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");

· 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 树编辑现有文件,请小心并提供完整的单元测试。 - 请不要破坏您的用户的代码。

· 11 分钟阅读

处理不同文件

babel-loader

@babel/preset-env

遇到的报错

  • Error: Can't resolve 'babel-loader'

    解决:使用require.resolve('babel-loader')

    // webpack配置

    loader: 'babel-loader',
    // 改为
    loader: require.resolve('babel-loader'),
  • Error: Cannot find package '@babel/preset-env'

    解决:使用require('@babel/preset-env').default

    // webpack配置

    presets: [
    [
    '@babel/preset-env',
    {
    // ...
    }
    ]
    ]

    // 改为
    presets: [
    [
    require('@babel/preset-env').default,
    {
    // ...
    }
    ]
    ]
  • Error: Can't resolve 'core-js/modules/es6.symbol.js' Can't resolve 'core-js/modules/es.string.trim.js'等一堆关于core-js的报错

    报错的原因是使用了"useBuiltIns": "usage"

    // webpack配置
    {
    test: /\.(?:js|mjs|cjs|ts)$/,
    exclude: /node_modules/,
    use: {
    loader: require.resolve('babel-loader'),
    options: {
    presets: [
    ['@babel/preset-env', {
    "targets": {
    "edge": "17",
    "firefox": "60",
    "chrome": "67",
    "safari": "11.1"
    },
    "useBuiltIns": "usage",
    "corejs": "3.6.5"
    }],
    ['@babel/preset-typescript']
    ]
    }
    }
    }

    解决方案:

    • 如果是monorepo工程,使用workspace形式引入依赖,比如封装webpack配置的应用是A,使用该webpack进行打包的应用是B,则需要在B中安装core-js

      B/package.json
      {
      "devDependencies": {
      "A": "workspace:*"
      },
      "dependencies": {
      "core-js": "3.6.5"
      }
      }

      或者不需要在B中安装core-js,而是在A中安装core-js,不过需要指定core-js的路径:

      // A中webpack配置
      resolve: {
      alias: {
      "core-js": path.resolve(__dirname, '../node_modules/core-js')
      },
      },
    • 如果是封装webpack配置的应用A发布npm包然后应用B引入的情况,则在应用A中安装core-js。在A中未指定版本安装core-js后,在B中引入A,启动发现还是报这个错,猜测是不是没找到依赖的core-js,所以使用resolve.alias指定core-js的路径:

      // webpack配置
      resolve: {
      alias: {
      "core-js": path.resolve(__dirname, '../node_modules/core-js')
      },
      },

      然后使用npm link调试后发现报错变成了node_modules/babel-runtime/core-js/object/assign.js Can't resolve 'core-js/library/fn/object/assign',这个错误表明项目中的某个部分(可能是一个依赖)正在尝试导入 core-js 的一个旧版本路径,但是这个路径在你安装的 core-js 版本中找不到。core-js 3.x 版本中已经不再使用 core-js/library/... 这样的路径。根据报错的提示知道是babel-runtime使用了低版本的core-js,但是B中没有手动安装babel-runtime,所以babel-runtime应该是B使用的某个npm包的依赖。

      tip

      要检查哪个 npm 包依赖于 babel-runtime,你可以使用以下方法之一:

      • 使用 npm 的 ls 命令来列出项目依赖树,并搜索 babel-runtimenpm ls babel-runtime 这个命令会显示所有依赖 babel-runtime 的包及其版本。
      • 如果你使用的是 npm 或 Yarn,你可以在 package-lock.jsonyarn.lock 文件中搜索 babel-runtime 来查看哪些包依赖于它。这些锁文件包含了你项目中所有依赖的确切版本和来源。
      • 有一些第三方工具可以帮助你分析项目的依赖树,例如 depchecknpm-check。这些工具可以提供关于项目依赖的更详细信息。比如安装 depcheck: npm install -g depcheck 然后在项目目录中运行它: depcheck

      在B的package-lock.json中搜索babel-runtime发现是使用的组件库依赖了babel-runtime,而babel-runtime依赖的core-js是2.4.0版本,所以将A中的core-js重新安装2.4.0版本,然后使用npm link调试,B可以正常启动。

      // webpack配置
      module: {
      rules: [
      {
      test: /\.(?:js|mjs|cjs|ts)$/,
      exclude: /node_modules/,
      use: {
      loader: require.resolve('babel-loader'),
      options: {
      presets: [
      [
      require('@babel/preset-env').default,
      {
      targets: {
      "edge": "17",
      "firefox": "60",
      "chrome": "67",
      "safari": "11.1"
      },
      useBuiltIns: "usage",
      corejs: '2.4.0',
      }
      ],
      [
      require('@babel/preset-typescript').default
      ]
      ]
      }
      }
      },
      ]
      },
      resolve: {
      alias: {
      "core-js": path.resolve(__dirname, '../node_modules/core-js')
      },
      },

      然后尝试将指定core-js的路径改为不指定,发现B仍可以正常启动。

      // webpack配置
      {
      test: /\.(?:js|mjs|cjs|ts)$/,
      exclude: /node_modules/,
      use: {
      loader: require.resolve('babel-loader'),
      options: {
      presets: [
      [
      require('@babel/preset-env').default,
      {
      targets: {
      "edge": "17",
      "firefox": "60",
      "chrome": "67",
      "safari": "11.1"
      },
      useBuiltIns: "usage",
      corejs: '2.4.0',
      }
      ],
      [
      require('@babel/preset-typescript').default
      ]
      ]
      }
      }
      },

vue-loader

在 webpack 中,所有的预处理器需要匹配对应的 loader。Vue Loader 允许你使用其它 webpack loader 处理 Vue 组件的某一部分。它会根据 lang 特性以及你 webpack 配置中的规则自动推断出要使用的 loader。

你应该将 vue-loadervue-template-compiler 一起安装(vue-template-compiler 用于将 Vue 组件的模板编译成渲染函数)。每次升级项目中的 vue 包时,也应该匹配升级 vue-template-compilervue-template-compiler 的版本应该与你使用的 vue 版本相匹配。对于 vue 2.6.x,你应该使用与 vue 2.6.x 相同的 vue-template-compiler 版本。例如,如果你使用的是 vue 2.6.12,那么你应该安装 vue-template-compiler 2.6.12。这样可以确保模板编译器与 vue 的运行时版本兼容,避免出现版本不匹配的问题。)。

请确保在你的 webpack 配置中添加 Vue Loader 的插件,这个插件是必须的! 它的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。例如,如果你有一条匹配 /\.js$/ 的规则,那么它会应用到 .vue 文件里的 <script> 块。

webpack.config.js
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
mode: 'development',
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
// 它会应用到普通的 `.js` 文件
// 以及 `.vue` 文件中的 `<script>` 块
{
test: /\.js$/,
loader: 'babel-loader'
},
// 它会应用到普通的 `.css` 文件
// 以及 `.vue` 文件中的 `<style>` 块
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
}
]
},
plugins: [
// 请确保引入这个插件来施展魔法
new VueLoaderPlugin()
]
}

遇到的报错

  • TypeError: Cannot read properties of undefined (reading 'styles')

    需要降低版本,vue2使用"vue-loader": "^15.10.0"

  • vue-template-compiler可以安装在封装webpack的npm包中,它起初是安装在业务工程中,业务工程中uninstall vue-template-compiler后启动报错Cannot find module 'vue-template-compiler',是Node.js 的缓存问题,清除 npm 缓存 npm cache clean --force 或者删除 node_modules 文件夹和 package-lock.json 文件,然后重新运行 npm install

style-loader css-loader

style-loader用于将 CSS 注入 DOM。

css-loaderimport/require() 一样解释 @import 和 url() 并解析它们。

建议将 style-loadercss-loader 结合使用

webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
};

sass-loader

加载 Sass/SCSS 文件并将其编译为 CSS。

npm install sass-loader sass webpack --save-dev
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
"style-loader",
// Translates CSS into CommonJS
"css-loader",
// Compiles Sass to CSS
"sass-loader",
],
},
],
},
};

postcss-loader

使用 PostCSS 处理 CSS。您需要 webpack v5 才能使用最新版本。对于 Webpack v4,您必须安装 postcss-loader v4。

PostCSS 是一个用于转换 CSS 的工具,它通过 JavaScript 插件来转换样式表。这些插件可以让你使用未来的 CSS 特性、优化 CSS 文件的大小、增加浏览器兼容性等。

postcss-preset-env插件 允许你使用未来的 CSS 特性,它会根据目标浏览器或环境转换这些特性为当前可用的等效写法。

npm install --save-dev postcss-loader postcss

css-loaderstyle-loader 之前使用它,但在其他预处理器加载器(例如 sass-loader less-loader `stylus-loader)之后使用它,因为 webpack 加载器执行顺序是从右到左/从下到上。

webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
importLoaders: 1,
},
},
"postcss-loader",
],
},
],
},
};
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: [
{ loader: "style-loader" },
{ loader: "css-loader" },
{
loader: "postcss-loader",
options: { implementation: require("postcss") },
},
{ loader: "sass-loader" },
],
},
],
},
};

遇到的问题

启动后el-table不渲染

webpack打包vue项目,打包成功,el-table没有渲染出来bug

解决:设置resolve.alias

webpack.config.js
resolve: {
alias: {
"vue$": 'vue/dist/vue.esm.js'
},
}

在执行npm包的bin文件报错syntax error near unexpected token '('

解决:在bin文件中添加Shebang行: #!/usr/bin/env node

Shebang行是位于脚本文件顶部的特殊注释行,它告诉操作系统应该使用哪个解释器来执行该文件中的脚本。Shebang行的格式如下:#!/path/to/interpreter,例如,对于一个使用bash解释器的脚本,Shebang行会是这样的:#!/bin/bash

对于一个Node.js脚本,Shebang行应该是:#!/usr/bin/env node。这里,/usr/bin/env是一个程序,它在系统的环境变量PATH中查找node命令并执行它,这样做可以确保脚本使用的是系统路径中的Node.js解释器。

Shebang行必须是文件的第一行,而且它的前两个字符必须是#!。如果你的npm包的bin文件是一个Node.js脚本,确保它以正确的Shebang行开头。如果没有Shebang行或者Shebang行错误,当你尝试执行脚本时,可能会遇到syntax error near unexpected token '('之类的错误。

边界处理

· 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: 协议替换为实际的版本号。

· 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令牌