0 概述
javascript第三方库经验汇总,主要针对小型库
1 io-ts
代码在这里
TypeScript已经保证了编译时的类型安全性,但是运行时的类型安全性如何保证?例如,如何保证后端返回的json结构体,满足TypeScript的类型约束,如果后端的json结构体不满足约束,显然会产生运行时的一大堆undefined报错。
要解决这个问题,一般有几个思路:
- JSONSchema,定义一个json schema,指定json结构体的类型约束,然后在运行时的时候将数据放入schema中进行校验。好处是适用性强,缺点是JSON schema与TypeScript的类型声明相似,但不同,要写两次。可以看这里
- 开发者定义一个TypeScript类型,并且使用代码生成工具,生成一份JSONSchema的定义。优点是,只需写一份TypeScript声明,缺点当然是所JSONSchema的限制,TypeScript的类型交集,并集这些都用不了。另外一个缺点是,需要额外的代码生成工具,略为麻烦。可以看这里
- 开发者定义一套DSL语法,这些语法既能用于编译时的TypeScript类型声明,又能用于运行时的TypeScript类型校验。优点是省事,TypeScript的类型约束几乎都能用上,缺点就是只支持TypeScript了。我们现在介绍的io-ts就属于这一类了。
1.1 依赖
npm install io-ts --save
npm install fp-ts --save
安装依赖
1.2 定义基础类型
import * as t from 'io-ts';
import { isRight } from 'fp-ts/Either';
export default () => {
const go = () => {
//定义一个t类型,输入为string,输出为string,检验的输入参数为unknown
//Type<A, O, I>
const string = new t.Type<string, string, unknown>(
//类型名称
//readonly name: string,
'string',
//运行时和编译时的类型判断
//readonly is: (u: unknown) => u is A,
(input: unknown): input is string => typeof input === 'string',
//校验输入为I类型的时候,它是否满足A类型
//readonly validate: (input: I, context: Context) => Either<Errors, A>,
(input, context) =>
typeof input === 'string'
? t.success(input)
: t.failure(input, context),
//encode,从输入类型A,运行时转换为输出类型O
//readonly encode: (a: A) => O
t.identity,
);
//decode,Type类型自带的一个方法,从任意的类型I,转换为输入类型A
//decode(i: I): Either<Errors, A>
console.log(
isRight(string.decode('a string')), // true
);
console.log(
string.decode('a string'), // 返回Either类型
);
console.log(
isRight(string.decode(null)), // true
);
console.log(
string.decode(null), // 返回Either类型
);
};
return (
<div>
<button onClick={go}>点我</button>
</div>
);
};
要点如上了,我们定义了一个string类型。实际使用中,我们很少需要定义自己的基础类型,因为io-ts自身就定义了一大堆的基础类型了。我们要注意如下:
- 对于一个指定的类型,我们可以传入decode方法,来校验和转换一个输入到我们目标的类型上
- decode的返回值是一个并集类型,可以为isLeft(校验失败),也可以是isRight(校验正确)。
1.3 定义组合类型
import * as t from 'io-ts';
import { isRight } from 'fp-ts/Either';
export default () => {
const go = () => {
//使用组合的方式来定义一个类型
const userType = t.type({
userId: t.number,
name: t.string,
});
//使用decode的方式来校验类型,这个会返回false
const validation = userType.decode({ userId: 22 });
console.log(isRight(validation));
//使用decode的方式来校验类型,这个会返回true
const validation2 = userType.decode({ userId: 123, name: '789' });
console.log(isRight(validation2));
if (validation2._tag == 'Right') {
console.log('ok', validation2.right);
}
//将io-ts类型转换为ts类型
//我们得到了一个编译时的类型定义,避免将一个类型定义写2次
type UserType = t.TypeOf<typeof userType>;
const mm: UserType = {
userId: 123,
name: 'u89',
};
console.log(mm);
};
return (
<div>
<button onClick={go}>点我</button>
</div>
);
};
更多的时候,我们就直接用组合的方式定义自己的类型就可以了。
- 使用t.type定义组合类型
- 使用t.string,t.number等等这些来引用基础类型
- 使用decode方法来尝试校验和转换
- 使用t.TypeOf来获取DSL语言定义编译时的TypeScript类型,这个好用
1.4 定义错误报告
import * as t from 'io-ts';
import { isRight } from 'fp-ts/Either';
//自带的错误报告器
import { PathReporter } from 'io-ts/PathReporter';
import { pipe } from 'fp-ts/function';
import { fold } from 'fp-ts/Either';
//自定义的错误报告期
function getErrorPaths<A>(v: t.Validation<A>): Array<string> {
return pipe(
v,
fold(
(errors) =>
errors.map(
(error) =>
`path:[${error.context
.map(({ key }) => key)
.join('.')}],valueType:${typeof error.value}`,
),
() => ['no errors'],
),
);
}
export default () => {
const go = () => {
//使用组合的方式来定义一个类型
const userType = t.type({
userId: t.number,
name: t.string,
});
const countryType = t.type({
//联合类型
name: t.union([t.string, t.undefined]),
//数组类型
people: t.array(userType),
//映射类型
peopleMap: t.record(t.string, userType),
});
//编译时的类型提示也是正确的
type MM = t.TypeOf<typeof countryType>;
//报错
const validation = countryType.decode({
name: 'China',
people: [
{
userId: 10001,
name: 'fish1',
},
{
userId: 10002,
name: 'fish2',
},
],
//缺少peopleMap参数
});
console.log(PathReporter.report(validation));
console.log(getErrorPaths(validation));
/*
输出如下:
"Invalid value undefined supplied to : { name: (string | undefined), people: Array<{ userId: number, name: string }>, peopleMap: { [K in string]: { userId: number, name: string } } }/peopleMap: { [K in string]: { userId: number, name: string } }"
*/
/*
['path:[.peopleMap],valueType:undefined']
*/
//报错2
const validation2 = countryType.decode({
people: [
{
userId: 10001,
},
{
userId: 10002,
name: 'fish2',
},
],
peopleMap: {
fish1: {
name: 123,
userId: 123,
},
},
});
console.log(PathReporter.report(validation2));
console.log(getErrorPaths(validation2));
/*
""Invalid value undefined supplied to : { name: (string | undefined), people: Array<{ userId: number, name: string }>, peopleMap: { [K in string]: { userId: number, name: string } } }/people: Array<{ userId: number, name: string }>/0: { userId: number, name: string }/name: string""
*/
/*
['path:[.people.0.name],valueType:undefined', 'path:[.peopleMap.fish1.name],valueType:number']
*/
};
return (
<div>
<button onClick={go}>点我</button>
</div>
);
};
校验失败的时候,我们当然希望知道是哪个字段出现的错误了。我们既可以用PathReporter,或者自定义的getErrorPaths来输出校验错误的位置。另外,要点有:
- 可以使用t.union,t.intersection来定义并集类型,和交集类型
- 可以使用t.array定义数组类型
- 可以使用t.record定义映射类型
- 可以使用t.undefined定义undefined类型
1.5 定义递归类型
import * as t from 'io-ts';
import { isRight } from 'fp-ts/Either';
export default () => {
const go = () => {
//递归类型需要先声明TS类型才能使用
interface Category {
name: string;
categories: Array<Category>;
}
const CategoryType: t.Type<Category> = t.recursion('Category', () =>
t.type({
name: t.string,
categories: t.array(CategoryType),
}),
);
//使用decode的方式来校验类型,这个会返回false
const validation = CategoryType.decode({
name: 'fish',
categories: [
{
name: 'fish2',
categories: [],
},
{
name: 'fish3',
categories: [
{
name: 'cat4',
categories: [],
},
{
name: 'cat5',
categories: [],
},
],
},
],
});
console.log(isRight(validation));
};
return (
<div>
<button onClick={go}>点我</button>
</div>
);
};
定义递归类型(类型定义本身指向自身)就麻烦一点了,需要
- 先声明TypeScript类型,再定义io-ts类型。不能反过来,这是递归类型定义的不便之处。
- 定义递归类型,需要指定类型名称
1.6 工具
顺手写了一个从JSON转换到io-ts的工具,特性如下:
- 支持组合类型,支持object嵌套object,object嵌套array,array嵌套object,array嵌套array等多种场景
- 自动支持同一个object下同一个key对应多种不同的数据类型
- 支持null,number,和string类型
- 不支持record类型
2 monaco-editor
代码在这里
众所周知,VS Code几乎成为了Web开发编辑器的标准。而且其内核只用了javascript的技术下,就能实现如此高的性能,简直就是个工程奇迹。打开大文件代码的初始化性能,滚动,编辑的体验和性能都很好。如果我们在需要的业务也能使用这项技术就太好了。
VSCode开源了编辑器内核以及它的React封装版本,让这个思想成为了可能。
2.1 依赖和配置
npm install monaco-editor --save
npm install react-monaco-editor --save
//格式化工具
npm install js-beautify --save
安装依赖
import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin';
: (memo) => {
chainWebpack// 更多配置 https://github.com/Microsoft/monaco-editor-webpack-plugin#options
.plugin('monaco-editor-webpack-plugin').use(MonacoWebpackPlugin, [
memo// 按需配置
languages: ['javascript'] },
{ ;
])return memo;
, }
对于umi,我们在.umirc.ts需要加上依赖的配置,才能打开js语言的语法高亮功能。
具体看这里
2.2 测试Demo
import { Button, Dropdown, Menu, Space, Tag } from 'antd';
import ProCard from '@ant-design/pro-card';
import {
SearchOutlined,
CheckCircleOutlined,
SyncOutlined,
} from '@ant-design/icons';
import MonacoEditor from 'react-monaco-editor';
import { useEffect, useRef } from 'react';
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
var beautify = require('js-beautify').js;
const BasicEditor: React.FC<any> = (props) => {
const editorRef = useRef<monacoEditor.editor.IStandaloneCodeEditor>();
const editorRef2 = useRef<monacoEditor.editor.IStandaloneCodeEditor>();
const options = {
selectOnLineNumbers: true,
};
useEffect(() => {
editorRef.current?.focus();
}, []);
const encode = () => {
const model = editorRef.current!.getModel();
const value = model!.getValue();
try {
const newValue = beautify(value, {
indent_size: 2,
space_in_empty_paren: true,
});
editorRef2.current!.setValue(newValue);
} catch (e) {
alert(e);
}
};
return (
<div
style={{
border: '1px solid black',
padding: '20px',
display: 'flex',
flexDirection: 'row',
width: '100%',
height: '100vh',
boxSizing: 'border-box',
}}
>
<div
style={{
flex: '1',
border: '1px solid black',
height: '100%',
}}
>
<MonacoEditor
language="javascript"
theme="vs-dark"
defaultValue=""
options={options}
editorDidMount={(editor) => {
editorRef.current = editor;
}}
/>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '10px',
height: '100%',
padding: '40px',
}}
>
<Button type="primary" onClick={encode}>
{'编码 >'}
</Button>
<Button type="primary">{'< 解码'}</Button>
</div>
<div
style={{
flex: '1',
border: '1px solid black',
height: '100%',
}}
>
<MonacoEditor
language="javascript"
theme="vs-dark"
defaultValue={''}
options={options}
editorDidMount={(editor) => {
editorRef2.current = editor;
}}
/>
</div>
</div>
);
};
export default BasicEditor;
轻松地几行代码,我们就实现了一个JSON的格式化工具了。要点如下:
- MonacoEditor不要使用受控修改模式,这样的话性能很差,需要使用命令方式的配置方式。获取MonacoEditor的引用,并且在必需的时候才进行getValue与setValue的操作。这样性能最好。
- 避免使用value,和onChange。仅仅使用defaultValue。
- 使用js-beautify,作为代码格式化工具
效果是真的挺好的
3 underscore
工具库了,没啥好说的
代码在这里
3.1 依赖
npm install underscore --save
npm install @types/underscore --save
安装依赖
3.2 模板
import { useEffect, useRef, useState } from 'react';
import _ from 'underscore';
import { Button } from 'antd';
const tpl = `
<div>
<h1><%= title %></h1>
<!--等于号没有转义-->
<div><%= html %></div>
<!--减号有转义-->
<div><%- html %></div>
<ul>
<% for(var i in list ){ var single = list[i]%>
<li><%= single.name %> - <%= single.id %></li>
<% } %>
</ul>
<% if(age >= 100 ){ %>
<h3>Very old <%= age %></h3>
<% }else{ %>
<h3>normal age <%= age %></h3>
<% }%>
</div>
`
const tplRender = _.template(tpl);
const templatePage: React.FC<any> = (props) => {
const [state, setState] = useState(0);
const refData = useRef({
html: '', data: {
title: '标题',
html: '<p>你好<span style="color:red;">xxx</p>',
list: [
{
name: 'fish',
id: 10001,
},
{
name: 'age',
id: 10002,
}
],
age: 28,
}
});
useEffect(() => {
refData.current.html = tplRender(refData.current.data);
setState((v) => v + 1);
}, []);
const toggleAge = () => {
let currentData = refData.current.data;
if (currentData.age >= 100) {
currentData.age = 28;
} else {
currentData.age = 128;
}
refData.current.html = tplRender(refData.current.data);
setState((v) => v + 1);
}
return (
<div>
<div><Button onClick={toggleAge}>{'切换age'}</Button></div>
<div dangerouslySetInnerHTML={{ __html: refData.current!.html }}></div>
</div>
);
}
export default templatePage;
要点如下:
- <%,嵌入原生的js语句,可以直接用for与if
- <%=,无转义的直接输出
- <%-,有转义的输出
- data,传入数据必须为object类型,然后取数据的时候直接取key,相当于with语句下运行
4 axios
axios可以说是最为简单的ajax工具,十分简单上手,API设计得很好。官网在这里
代码在这里
4.1 依赖
//必选依赖
npm install axios --save
//urlencode要用到,可选依赖
npm install qs --save
依赖如上,也比较简单了
4.2 GET与POST
package spring_test;
import lombok.Data;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/normal")
public class NormalController {
@GetMapping("/get")
public String get(@RequestParam(value = "name",required = true) String name){
return name;
}
@Data
public static class Form{
private String name;
private Integer age;
}
@PostMapping(value = "/postForm",consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public Form postForm(Form form){
return form;
}
@PostMapping("/postJson")
public Form postJson(@RequestBody Form form ){
return form;
}
}
先用Spring实现一个简单的Get,PostForm和PostJson的服务器
import axios from 'axios';
import { Space, Button } from 'antd';
import { useState } from 'react';
import qs from 'qs';
const AxiosBasicTest: React.FC<any> = (props) => {
const [state, setState] = useState('');
const get = async () => {
try {
let response = await axios({
method: 'GET',
url: '/api/normal/get',
params: {
name: "fish",
}
});
setState(JSON.stringify(response, null, 4));
} catch (e) {
alert(e);
}
}
const postForm = async () => {
try {
let response = await axios({
method: 'POST',
url: '/api/normal/postForm',
data: qs.stringify({
name: "fish",
age: 110,
}),
headers: {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8'
}
});
setState(JSON.stringify(response, null, 4));
} catch (e) {
alert(e);
}
}
const postJson = async () => {
try {
let response = await axios({
method: 'POST',
url: '/api/normal/postJson',
data: {
name: "fish",
age: 110,
}
});
setState(JSON.stringify(response, null, 4));
} catch (e) {
alert(e);
}
}
return (
<div>
<Space>
<Button onClick={get}>{'GET请求'}</Button>
<Button onClick={postForm}>{'POST form表单'}</Button>
<Button onClick={postJson}>{'POST Json表单'}</Button>
</Space>
<textarea
style={{ width: '100%', height: '500px', border: '1px solid black' }}
value={state}
onChange={() => { }}
disabled={true}
/>
</div>
);
}
export default AxiosBasicTest;
axios发送get,postForm和postJson的请求也比较简单,要点如下:
- axios处理json是比较简单的,默认返回值已经用JSON.parse处理过了,不需要重复处理。axios发送JSON格式的表单也是直接放入data字段就可以了,也比较简单。
- axios发送urlencoded表单要麻烦一点,data字段要用qs转换一下,另外要修改一下headers的content-type字段即可。
- axios的返回值,headers也是小写的。
4.3 下载文件
server端已经实现了excel导出功能,也没什么好说的了
import ProCard from "@ant-design/pro-card";
import { Button } from 'antd';
import axios from "axios";
const AxiosExcelTest: React.FC<any> = (props) => {
const getFileName = (headers: any): string => {
const disposition = headers['content-disposition'];
if (typeof disposition != 'string') {
return '';
}
const nameIndex = disposition.indexOf('=');
const name = disposition.substring(nameIndex + 1);
return decodeURIComponent(name);
}
const axoisDownload = async () => {
let response = await axios({
method: 'GET',
url: '/api/excel/get4',
responseType: 'arraybuffer',
});
const aEle = document.createElement("a"); // 创建a标签
const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8' });
const href = window.URL.createObjectURL(blob); // 创建下载的链接
aEle.href = href;
const name = getFileName(response.headers);
aEle.download = name;
document.body.appendChild(aEle);
aEle.click(); // 点击下载
document.body.removeChild(aEle); // 下载完成移除元素
window.URL.revokeObjectURL(href); // 释放掉blob对象
}
return (
<div>
<ProCard
bordered={true}
title="a标签触发">
<a href="/api/excel/get4" target='_blank'>{'下载Excel'}</a>
</ProCard>
<ProCard
bordered={true}
title="window.open触发">
<Button onClick={() => {
const url = "/api/excel/get4";
window.open(url);
}}>{'下载Excel'}</Button>
</ProCard>
<ProCard
bordered={true}
title="axios触发">
<Button onClick={axoisDownload}>{'下载Excel'}</Button>
</ProCard>
</div>
);
}
export default AxiosExcelTest;
用Excel导出有几种方法:
- 用a标签,加入_blank导出,这种方式兼容性最好
- 用window.open标签导出,在PC端基本没有问题
- 用axios拉取数据,注意设置一下responseType的类型。然后放入blob,并创建a标签,手动触发click。这种方法也可以,只是需要html5的支持,基本也是OK的。
4.4 上传文件
import ProCard from "@ant-design/pro-card";
import { Button, Upload } from 'antd';
import axios from "axios";
import type { RcFile, UploadFile, UploadProps } from 'antd/es/upload/interface';
const AxiosExcelTest: React.FC<any> = (props) => {
const axiosUpload = async (file: RcFile) => {
const formData = new FormData();
formData.append('data', file);
formData.append('type', "HelloMan");
const response = await axios({
method: "POST",
headers: {
'Content-Type': 'mulitpart/form-data',
},
url: "/api/excel/post1",
data: formData,
});
console.log(response.data);
}
return (
<div>
<ProCard
bordered={true}
title="axios上传excel">
<Upload
maxCount={1}
showUploadList={false}
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
beforeUpload={axiosUpload}>
<Button >{'上传Excel'}</Button>
</Upload>
</ProCard>
</div >
);
}
export default AxiosExcelTest;
直接往FormData填入数据,然后上传就可以了。注意是Content-Type,不是Content-type
5 深拷贝
代码在这里
js的深拷贝,需要考虑几点:
- 对object,array,甚至symbol的支持
- 对原型链的支持
- 对循环引用的支持
npm install clone --save
npm install @types/clone --save
clone库完美支持以上的特性
5.1 基础
import { useEffect } from "react";
import clone from 'clone';
import { or } from "fp-ts/lib/Predicate";
const Page: React.FC<any> = (props) => {
const data = [
{
a: 3,
b: 4,
c: [1, 2]
},
];
type dataType = typeof data;
const clone1 = (e: dataType) => {
//clone默认就支持循环引用的对象
return clone(e);
}
const clone2 = (e: dataType) => {
//使用JSON深拷贝的方式,效率第一点,循环引用也不支持
let j2 = JSON.stringify(e);
return JSON.parse(j2);
}
const cloneTest = (cloneWork: (a: dataType) => dataType) => {
let origin = data;
let after = cloneWork(origin);
after[0].c = [34];
after.push({
a: 5,
b: 6,
c: [],
});
console.log("origin", origin);
console.log("after", after);
}
useEffect(() => {
cloneTest(clone1);
cloneTest(clone2);
});
return (
<div>{'Clone测试'}</div>
);
}
export default Page;
要点如下:
- 用clone库的话性能好,支持特性也多
- 用JSON来做一次序列化和反序列化也行,就是性能差一点,而且不支持循环引用。
6 日期和时间
js的日期和时间简直就是个残废,我们需要更好的替代品。
代码在这里
6.1 moment
moment是最为老牌成熟的库了,支持的特性很多,基本是没有坑,API设计还是过得去,唯一的缺点是:
- 库比较大,67.8KB
- 原地修改类型,非不可变类型,这一点真的蛋疼,一不容易就踩坑上
npm install moment --save
加入依赖,官网在这里
import moment from "moment";
import { useEffect } from "react";
const Page: React.FC<any> = (props) => {
const testNow = () => {
console.log('testNow');
var now: moment.Moment = moment();
console.log(now.format());
}
const testFormatAndParse = () => {
console.log('testFormatAndParse');
var now: moment.Moment = moment();
//年,月,日,时,分,秒
var format1 = now.format('YYYY-MM-DD HH:mm:ss');
console.log(format1);
//年的第几周,周的星期几。大写的为ISO表示,小写为local表示
var format2 = now.format('ww-e WW-E');
console.log(format2);
//parse
const parse1 = moment('2021-11-29', 'YYYY-MM-DD');
console.log(parse1.format());
const parse2 = moment('2021-11-29 18:29:30', 'YYYY-MM-DD HH:mm:ss');
console.log(parse2.format());
}
const testGet = () => {
console.log('testGet');
//以下的API都有搭配的set方法,就不多说了
var now = moment();
console.log('year ', now.year());//年份
console.log('month', now.month());//月份,范围为0-11
console.log('date', now.date());//天数,范围为1-31
console.log('hour', now.hour());//小时,范围为0-23,
console.log('minute', now.minute());//分钟,范围为0-59
console.log('second', now.second());//秒数,范围为0-59
console.log('week', now.week());//星期数,与本地有关
console.log('weekDay', now.weekday());//星期几,与本地有关
console.log('isoWeek', now.isoWeek());//ISO星期数,ISO标准
console.log('day', now.day());//星期几,0为星期天,1-6为对应的星期,ISO标准
console.log('unix', now.unix());//unix时间戳
//set方法是原地修改的,moment不是immutable类型,这点设计并不好
const threeDate = now.date(3);
console.log('threeDate', threeDate.format(), now.format());
//创建副本
const now2 = moment();
const threeDate2 = now2.clone().date(3);
console.log('threeDate2', threeDate2.format(), now2.format());
}
const testOperation = () => {
console.log('testOperation');
const now = moment();
console.log('now', now.format());
//最近7天范围内,substract也是原地修改,需要用clone创建副本
const prevSevenDay = now.clone().subtract(7, 'day');
console.log('prevSevenDay', prevSevenDay.format(), now.format());
//本月范围内,startOf和endOf都是原地修改,需要用clone创建副本
const firstDayOfMonth = now.clone().startOf('month');
const endDayOfMonth = now.clone().endOf('month');
console.log('month range ', firstDayOfMonth.format(), endDayOfMonth.format());
//本周范围内,注意从周日开始,周六结束
const firstDayOfWeek = now.clone().startOf('week');
const endDayOfWeek = now.clone().endOf('week');
console.log('week range ', firstDayOfWeek.format(), endDayOfWeek.format());
//比较
console.log('equal diff', firstDayOfWeek.diff(firstDayOfWeek));
console.log('less diff', firstDayOfWeek.diff(endDayOfWeek));
console.log('great diff', endDayOfWeek.diff(firstDayOfWeek));
}
useEffect(() => {
testNow();
testFormatAndParse();
testGet();
testOperation();
}, []);
return (<div>{'Moment测试'}</div>);
}
export default Page;
代码也比较简单,没啥好说的了,注意几点:
- 月份,是从0开始,而不是从1开始的。
- 星期,是从星期日(0)开始,而不是从星期一开始。
- 原地修改类型,修改之前记得先用clone复制一下
6.2 dayjs
dayjs可是针对性地解决moment的问题,改进如下:
- 包很小,只有2KB
- 默认就是不可变类型,这点很好。
缺点就是支持的特性少一点,80%场景能覆盖。
npm install dayjs --save
加入依赖,官网在这里
import dayjs from "dayjs";
import 'dayjs/locale/zh-cn' // 导入本地化语言
import { useEffect } from "react";
const Page: React.FC<any> = (props) => {
const testNow = () => {
console.log('testNow');
var now: dayjs.Dayjs = dayjs();
console.log(now.format());
}
const testFormatAndParse = () => {
console.log('testFormatAndParse');
var now: dayjs.Dayjs = dayjs();
//年,月,日,时,分,秒
var format1 = now.format('YYYY-MM-DD HH:mm:ss');
console.log(format1);
//没有week
//var format2 = now.format('ww-e WW-E');
//console.log(format2);
//parse
const parse1 = dayjs('2021-11-29', 'YYYY-MM-DD');
console.log(parse1.format());
const parse2 = dayjs('2021-11-29 18:29:30', 'YYYY-MM-DD HH:mm:ss');
console.log(parse2.format());
}
const testGet = () => {
console.log('testGet');
//以下的API都有搭配的set方法,就不多说了
var now = dayjs();
console.log('year ', now.year());//年份
console.log('month', now.month());//月份,范围为0-11
console.log('date', now.date());//天数,范围为1-31
console.log('hour', now.hour());//小时,范围为0-23,
console.log('minute', now.minute());//分钟,范围为0-59
console.log('second', now.second());//秒数,范围为0-59
//console.log('week', now.week());//没有星期数
//console.log('weekDay', now.weekday());//没有星期数
//console.log('isoWeek', now.iso());//没有星期数
console.log('day', now.day());//星期几,0为星期天,1-6为对应的星期,ISO标准
console.log('unix', now.unix());//unix时间戳
//set方法返回新数据的,dayjs是immutable类型,这点设计很好
const threeDate = now.date(3);
console.log('threeDate', threeDate.format(), now.format());
}
const testOperation = () => {
console.log('testOperation');
const now = dayjs();
console.log('now', now.format());
//最近7天范围内
const prevSevenDay = now.subtract(7, 'day');
console.log('prevSevenDay', prevSevenDay.format(), now.format());
//本月范围内
const firstDayOfMonth = now.startOf('month');
const endDayOfMonth = now.endOf('month');
console.log('month range ', firstDayOfMonth.format(), endDayOfMonth.format());
//本周范围内,注意从周日开始,周六结束
const firstDayOfWeek = now.startOf('week');
const endDayOfWeek = now.endOf('week');
console.log('week range ', firstDayOfWeek.format(), endDayOfWeek.format());
//比较
console.log('equal diff', firstDayOfWeek.diff(firstDayOfWeek));
console.log('less diff', firstDayOfWeek.diff(endDayOfWeek));
console.log('great diff', endDayOfWeek.diff(firstDayOfWeek));
}
useEffect(() => {
testNow();
testFormatAndParse();
testGet();
testOperation();
}, []);
return (<div>{'Dayjs测试'}</div>);
}
export default Page;
要点如下:
- API设计基本与moment类型,迁移比较方便
- 没有获取当前是一年中的第几个星期的API
- 默认就是不可变类型,终于不用小心翼翼地使用clone了
6.3 小结
moment在2020年末的时候就宣布进入维护期了,看这里,大家还是改用Day.js吧。
7 echarts
echarts可以说是事实上的图表标准了。
- ECharts官网
- ECharts For React,一个包装Echarts的React库
代码在这里
7.1 依赖
npm install echarts-for-react --save
npm install echarts --save
依赖也比较简单
7.2 基础
import ProCard from '@ant-design/pro-card';
import ReactECharts from 'echarts-for-react';
const option = {
title: {
text: 'Stacked Area Chart'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
legend: {
data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']
},
toolbox: {
feature: {
saveAsImage: {}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: 'Email',
type: 'line',
stack: 'Total',
areaStyle: {},
emphasis: {
focus: 'series'
},
data: [120, 132, 101, 134, 90, 230, 210]
},
{
name: 'Union Ads',
type: 'line',
stack: 'Total',
areaStyle: {},
emphasis: {
focus: 'series'
},
data: [220, 182, 191, 234, 290, 330, 310]
},
{
name: 'Video Ads',
type: 'line',
stack: 'Total',
areaStyle: {},
emphasis: {
focus: 'series'
},
data: [150, 232, 201, 154, 190, 330, 410]
},
{
name: 'Direct',
type: 'line',
stack: 'Total',
areaStyle: {},
emphasis: {
focus: 'series'
},
data: [320, 332, 301, 334, 390, 330, 320]
},
{
name: 'Search Engine',
type: 'line',
stack: 'Total',
label: {
show: true,
position: 'top'
},
areaStyle: {},
emphasis: {
focus: 'series'
},
data: [820, 932, 901, 934, 1290, 1330, 1320]
}
]
};
const Page: React.FC<any> = (props) => {
return (
<ProCard title="Echarts测试">
<ReactECharts option={option} style={{ height: '80vh' }} />
</ProCard>
);
}
export default Page;
代码也比较简单,就是复制一下Echarts官网的option进去就可以了,没啥好说的。
8 browser-fs-access
浏览器的一个问题是,对本地文件和文件夹的支持不好,我们常常需要下载一个Excel到本地。但是,浏览器都是默认下载到本地的其中一个目录,无法进行目录选择。
browser-fs-access很好的解决了这个问题,官网在这里
代码在这里
8.1 依赖
npm install --save browser-fs-access
没啥好说的,比较简单
8.2 基础
import styles from './index.less';
import {Button,Modal} from 'antd';
import { fileSave } from 'browser-fs-access';
import { useRef } from 'react';
import axios from 'axios';
const GlobalCatch = async(handler:()=>Promise<void>)=>{
try{
await handler();
}catch(e){
Modal.error({
content:'错误:'+e,
});
}
}
const imageToBlob = async (img:any):Promise<Blob> => {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob) => {
resolve(blob!);
});
});
};
export default function IndexPage() {
const imageRef = useRef<any>();
const saveImage = async()=>{
const blob = await imageToBlob(imageRef.current);
//这个blob可以来自于
await fileSave(blob, {
fileName: 'floppy.png',
extensions: ['.png'],
});
}
const saveResponse = async ()=>{
const blob = await axios({
method:'GET',
url:'/floppy.png',
responseType:'blob',
});
await fileSave(blob.data, {
fileName: 'floppy_reponse.png',
extensions: ['.png'],
});
}
return (
<div>
<h1 className={styles.title}>Page index</h1>
<img ref={imageRef} src={'floppy.png'}/>
<Button onClick={()=>{
GlobalCatch(saveImage);
}}>{'保存图片'}</Button>
<Button onClick={()=>{
GlobalCatch(saveResponse);
}}>{'保存Response'}</Button>
</div>
);
}
我们可以调用fileSave方法,将blob数据存放到本地,还可以指定默认的文件名和后缀名。blob可以是来自于浏览器的图片,也可以来自于Response的数据。
9 xlsx
官网在这里
代码在这里
中文文档在这里
这个库并不强大,community只有基本功能,不支持style,建议不要使用。依赖这个库的加入style属性的第三方库建议不要使用,落后于community版本,也不健壮。需要强大和稳定的xlsx,建议还是需要使用java的poj。
9.1 依赖
yarn add https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz --save
以上为依赖
9.2 基础
import XLSX, { read, writeFileXLSX } from "xlsx";
import { Button } from 'antd';
const onClick = async () => {
const url = "https://sheetjs.com/data/executive.json";
const raw_data = await (await fetch(url)).json();
const prez = raw_data.filter((row: any) => row.terms.some((term: any) => term.type === "prez"));
//将json转换为object[]
const rows = prez.map((row: any) => ({
name: row.name.first + " " + row.name.last,
birthday: row.bio.birthday
}));
//创建book与添加sheet
const worksheet = XLSX.utils.json_to_sheet(rows);
const workbook = XLSX.utils.book_new();
//第三个参数为sheet的名字
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
//添加表头,aoa是二维表格数据
XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
//设置所有列的列宽,wch是字符宽度
const max_width = rows.reduce((maxLength: any, single: any) => Math.max(maxLength, single.name.length), 10);
worksheet["!cols"] = [{ wch: max_width }];
//创建并写入,格式通过文件名的后缀.xlsx来确定
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });
}
export default function IndexPage() {
return (
<div>
<Button onClick={onClick}>{'基础'}</Button>
</div>
);
}
没啥好说的
9.3 寻址
import XLSX, { read, writeFileXLSX } from "xlsx";
import { Button } from 'antd';
const colTest = async () => {
let col_index = XLSX.utils.decode_col("D");
let col_name = XLSX.utils.encode_col(5);//从0开始,也就是第6个
//3,F
console.log(col_index, col_name);
}
const rowTest = async () => {
let row_index = XLSX.utils.decode_row("4");
let row_name = XLSX.utils.encode_row(5);//从0开始,也就是第6个
//3,'6'
console.log(row_index, row_name);
}
const cellTest = async () => {
var address = XLSX.utils.decode_cell("A2");//
var a1_addr = XLSX.utils.encode_cell({ r: 1, c: 0 });//第2行,第1列,下标都是从0开始
//{c: 0, r: 1} 'A2'
console.log(address, a1_addr);
}
const rangeTest = async () => {
var range = XLSX.utils.decode_range("A1:D3");
//s是start,e是end
var a1_range = XLSX.utils.encode_range({ s: { c: 1, r: 1 }, e: { c: 3, r: 2 } });
/*
e: {c: 3, r: 2}
s : {c: 0, r: 0}
'B2:D3'
*/
console.log(range, a1_range);
}
export default function IndexPage() {
return (
<div>
<Button onClick={colTest}>{'col的编码与解码'}</Button>
<Button onClick={rowTest}>{'row的编码与解码'}</Button>
<Button onClick={cellTest}>{'cell的编码与解码'}</Button>
<Button onClick={rangeTest}>{'range的编码与解码'}</Button>
</div>
);
}
注意,列宽的实现
基本操作了
9.4 列
import XLSX, { read, writeFileXLSX } from "xlsx";
import { Button } from 'antd';
const onClick = async () => {
const rows = [
{
number: '1001',
name: 'fish',
},
{
number: '1002',
name: 'cat',
}
];
//创建book与添加sheet
const worksheet = XLSX.utils.json_to_sheet(rows);
const workbook = XLSX.utils.book_new();
//第三个参数为sheet的名字
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
//添加表头,aoa是二维表格数据
XLSX.utils.sheet_add_aoa(worksheet, [["编号", "名称"]], { origin: "A1" });
//设置所有列的列宽,wch是字符宽度,wpx是像素宽度
worksheet["!cols"] = [{ wpx: 100 }, { wpx: 200 }];
//创建并写入,格式通过文件名的后缀.xlsx来确定
XLSX.writeFile(workbook, "Workbook.xlsx", { compression: true });
}
export default function IndexPage() {
return (
<div>
<Button onClick={onClick}>{'列测试'}</Button>
</div>
);
}
9.5 单元格
import XLSX, { read, writeFileXLSX } from "xlsx";
import { Button } from 'antd';
const onClick = async () => {
const rows = [
{
number: '1001',
name: 'fish',
age: 10,
tall: 1.72,
money: '100.23',
birthday: '1959-01-03',
firstCreated: '2001-02-03 12:12:23',
},
{
number: '1002',
name: 'cat',
age: 11,
tall: 0.83333,
money: '-100.11',
birthday: '1983-04-05',
firstCreated: '2002-03-04 08:24:56',
}
];
//创建book
const workbook = XLSX.utils.book_new();
//添加表头,aoa是二维表格数据
const aoas: any[] = [["编号", "名称", "年龄", "身高", "余额", "生日", "首次创建"]];
rows.forEach(row => {
let aoa = [];
//文本类型
aoa.push({
t: 's',
v: row.number,
});
aoa.push({
t: 's',
v: row.name,
});
//数字类型
aoa.push({
t: 'n',
v: row.age,
});
aoa.push({
t: 'n',
v: row.tall,
});
//decimal类型
aoa.push({
t: 'n',
v: row.money,
});
//date类型
aoa.push({
t: 'd',
v: row.birthday,//FIXME,被固定转换为8:00:43的时间戳尾部
});
aoa.push({
t: 'd',
v: row.firstCreated,
});
aoas.push(aoa);
});
//第三个参数为sheet的名字
let worksheet = XLSX.utils.aoa_to_sheet(aoas);
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
//设置所有列的列宽,wch是字符宽度,wpx是像素宽度
worksheet["!cols"] = [{ wpx: 100 }, { wpx: 200 }];
//创建并写入,格式通过文件名的后缀.xlsx来确定
XLSX.writeFile(workbook, "Workbook.xlsx", { compression: true });
}
export default function IndexPage() {
return (
<div>
<Button onClick={onClick}>{'cell测试'}</Button>
</div>
);
}
注意单元格格式的实现
9.6 单元格合并
import XLSX, { read, writeFileXLSX } from "xlsx";
import { Button } from 'antd';
const onClick = async () => {
const rows = [
{
number: '1001',
name: 'fish',
},
{
number: '1002',
name: 'cat',
},
{
number: '1003',
name: 'cat3',
},
{
number: '1004',
name: 'cat4',
}
];
//创建book与添加sheet
const worksheet = XLSX.utils.json_to_sheet(rows);
const workbook = XLSX.utils.book_new();
//第三个参数为sheet的名字
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
//添加表头,aoa是二维表格数据
XLSX.utils.sheet_add_aoa(worksheet, [["编号", "名称"]], { origin: "A1" });
//合并多行
const range = { s: { c: 1, r: 1 }, e: { c: 1, r: 2 } };
if (!worksheet["!merges"]) {
worksheet['!merges'] = [];
}
worksheet['!merges'].push(range);
//创建并写入,格式通过文件名的后缀.xlsx来确定
XLSX.writeFile(workbook, "Workbook.xlsx", { compression: true });
}
export default function IndexPage() {
return (
<div>
<Button onClick={onClick}>{'合并范围'}</Button>
</div>
);
}
单元格合并
9.7 对话框导出
import XLSX, { read, writeFileXLSX } from "xlsx";
import { Button, Modal } from 'antd';
import { fileSave } from 'browser-fs-access';
const onClick = async () => {
const url = "https://sheetjs.com/data/executive.json";
const raw_data = await (await fetch(url)).json();
const prez = raw_data.filter((row: any) => row.terms.some((term: any) => term.type === "prez"));
//将json转换为object[]
const rows = prez.map((row: any) => ({
name: row.name.first + " " + row.name.last,
birthday: row.bio.birthday
}));
//创建book与添加sheet
const worksheet = XLSX.utils.json_to_sheet(rows);
const workbook = XLSX.utils.book_new();
//第三个参数为sheet的名字
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
//添加表头,aoa是二维表格数据
XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
//设置所有列的列宽,wch是字符宽度
const max_width = rows.reduce((maxLength: any, single: any) => Math.max(maxLength, single.name.length), 10);
worksheet["!cols"] = [{ wch: max_width }];
//创建并写入blob
const u8 = XLSX.write(workbook, { compression: true, type: 'buffer', bookType: 'xlsx' });
const blob = new Blob([u8], { type: "application/vnd.ms-excel" });
await fileSave(blob, {
fileName: '测试.xlsx',
extensions: ['.xlsx'],
});
Modal.success({
content: '导出完成!'
});
}
export default function IndexPage() {
return (
<div>
<Button onClick={onClick}>{'触发导出'}</Button>
</div>
);
}
转换为fileSave再导出就可以了。
10 resize-observer-polyfill
我们有时候需要侦听一个div的变化,从而自动刷新div里面的内容。
代码在这里
10.1 依赖
yarn add resize-observer-polyfill --save
比较简单
10.2 基础
import { Button } from 'antd';
import { MutableRefObject, useEffect, useRef, useState } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
type Size = { width: number; height: number };
function useResizeObserver(target: MutableRefObject<any>, handler: (size: Size) => void) {
useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { clientWidth, clientHeight } = entry.target;
handler({
width: clientWidth,
height: clientHeight,
});
});
});
resizeObserver.observe(target.current);
return () => {
resizeObserver.disconnect();
};
}, []);
}
export default function IndexPage() {
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
const divRef = useRef<HTMLDivElement>(null);
useResizeObserver(divRef, (size) => {
console.log('newSize', size);
setSize(size);
});
return (
<div ref={divRef} style={{
width: '100%',
height: '100vh',
padding: '10px',
background: 'yellow',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
justifyContent: "center",
alignItems: 'center',
}}>
<div>{`width:${size.width}`}</div>
<div>{`height:${size.height}`}</div>
</div>
);
}
就这么简单
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!