javascript第三方库经验汇总

2022-01-25 fishedee 前端

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';


chainWebpack: (memo) => {
    // 更多配置 https://github.com/Microsoft/monaco-editor-webpack-plugin#options
    memo.plugin('monaco-editor-webpack-plugin').use(MonacoWebpackPlugin, [
        // 按需配置
        { 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可以说是事实上的图表标准了。

代码在这里

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

就这么简单

相关文章