前端项目之 Dodo 算账

417 阅读6分钟

今天来总结一下我最近做的一个 React 的本地项目,该项目主要从我以及身边的同学的实际需求出发所设计的一个极简记账项目。我们为什么总是在不知不觉中就把生活费或者工资花光了呢?

其实就是我们没有对我们的每笔支出或收入进行统计!

但是大家都知道,现在市面上的记账 APP 大多都含有很多广告,这就导致我们记账的时候容易收到广告的诱惑而“被动消费”。

所以我就决定自己用 React 做一个本地版的记账页面,要求的话主要针对手机页面就可以。

说干就干!

先附上源码链接预览链接

以下是我在完成项目过程中遇到的问题和总结:

一、create-react-app 快速启动

为了能够快速地创建一个 React 项目,我决定使用 create-react-app 来快速创建,首先先进行全局安装,方便以后其他项目也可以利用,具体用法是:

yarn global add create-react-app  //全局安装
yarn create-react-app my-app      //我用的是 yarn,大家也可以使用 npm 或 npx
cd my-app
yarn start

二、支持 CSS

  • 在刚开始安装各种 loader 的时候我就发现,为了支持 css 的 node-sass 速度非常慢,而且就算我爬上了梯子,很多时候也会莫名说我网络连接错误

  • 于是我就在网上找解决方法,发现可以用 dart-sass 来代替使用

  • 但是显然很多其他的依赖需要的是 node-sass,如果使用 dart-sass ,会存在找不到依赖的问题,于是继续寻找答案

  • 我发现 npm@6.9.0 中有新的 package alias 功能,可以对依赖进行指向

  • 于是我可以通过 yarn add --dev node-sass@npm: dart-sass来偷天换日!

  • 其原理就是实际安装的是 dart-sass 的内容,但是用 node-sass 的名字来 “欺骗” React

一波三折!

三、使用 styled-components

但是根据以往项目的经验,使用普通的 css 文件引用的方式,会额外生成很多文件,再加上 react 项目的小组件非常多,为了精确控制每一个部分的样式,我决定使用 styled-components,它可以实现 css 和 JS 共同存在一个文件中,有效减少文件数量。如:

const Wrapper = styled.section`
  background: #f1f4f4;
  padding: 12px 16px;
  font-size: 14px;
  
  >label{
    color:#2BA245
  }
`;

const NoteSection: React.FunctionComponent = (props) => {
    return (
        <Wrapper>
            <div>hi</div>
        </Wrapper>
    );
};

四、使用 TypeScript 代替 JavaScript

其实以我现在的理解来看,二者的使用方法是几乎一致的,但是 TS 需要更加注意数据类型,相当于 JS 的严格模式,这会让我对数据类型和各组件之间的关系更加清晰。比如我在定义存储数据的基本类型时,就可以清晰指出每个变量的类型:

type Category = '-' | '+';

const defaultFormData = {
    tagIds: [] as number[],
    note: '',
    createdAt: day(new Date()).format('YYYY-MM-DD'),
    category: '-' as Category,
    amount: 0,
};

五、使用 react-router-dom 路由

为了实现页面的跳转,除了使用简单的 window.history.back() 返回上一页以外,还应该实现不同页面之间任意跳转,所以我决定使用 react router

yarn add react-router-dom;
yarn add --dev @types/react-router-dom  //安装 typescipt 依赖

以下是我如何设置我自己的路由的,与官方文档有些许不同,大家可以参考:

function App() {
    return (
        <AppWrapper>
            <Router>
                <Switch>
                    <Route exact path="/tags/:id">
                        <Tag/>
                    </Route>
                    <Route exact path="/tags">
                        <Tags/>
                    </Route>
                    <Route exact path="/money">
                        <Money/>
                    </Route>
                    <Route exact path="/statistics">
                        <Statistics/>
                    </Route>
                    <Redirect exact from={'/'} to={'/money'}/>
                    <Route path="*">
                        <NoMatch/>
                    </Route>
                </Switch>
            </Router>
        </AppWrapper>
    );
}

其中 exact 是指精确匹配路径;NoMatch 组件会在没有我列举的站点名时渲染,可以理解为“404页面”。

页面跳转效果:

5.gif

六、自定义 Hook

React 非常特别的一点就是可以自定义 Hook,实际上是利用 react 本身提供的 useEffect、useState 等方法来封装一个属于自己的 Hook 钩子:

const useTags = () => { //封装自定义 Hook
    const [tags, setTags] = useState<{ id: number; name: string }[]>([]);

    useEffect(()=>{
        let localTags = JSON.parse(window.localStorage.getItem('tags') || '[]');
        if (localTags.length===0){
            localTags = [
                {id: createId(), name: '衣'},
                {id: createId(), name: '食'},
                {id: createId(), name: '住'},
                {id: createId(), name: '行'}
            ]
        }
        setTags(localTags);
    },[]) //挂载时执行

    useUpdate(()=>{
        window.localStorage.setItem('tags',JSON.stringify(tags));
    },tags)

    const findTag = (id: number) => tags.filter(tag => tag.id === id)[0];
    const findTagIndex = (id: number) => {
        let result = -1;
        for (let i = 0; i < tags.length; i++) {
            if (tags[i].id === id) {
                result = i;
                break;
            }
        }
        return result;
    };
    const updateTag = (id: number, {name}: { name: string }) => {
        setTags(tags.map(tag=> tag.id === id? {id, name} : tag))
    };
    const deleteTag = (id:number)=>{
        setTags(tags.filter(tag =>tag.id!==id))
    }
    const addTag = () => {
        const tagName = window.prompt('新标签的名称为');
        if (tagName !== null && tagName !== '') {
            setTags([...tags, {id: createId(), name: tagName}]);
        }
    };

    const getName = (id:number)=>{
        const tag = tags.filter(t=>t.id===id)[0]
        return tag?tag.name : ''
    }
    return {tags, getName,setTags, findTag, updateTag, findTagIndex,deleteTag,addTag};

};

export {useTags};

通过上述的封装,我就同时对 tags 进行7种操作,体现了 react 的灵活性。

七、使用 localStorage 本地存储

由于时间有限,这次做的暂时是以本地版为目标,就没有实现后端的数据储存功能。那么如何存储本地的数据呢?

我决定使用简单的 localStorage,分别使用以下两条命令进行读写操作:

window.localStorage.getItem(key);
window.setItem(key,value)

localStorage 效果:

image.png

八、使用 Echarts 数据可视化

为了能在数据的页面更形象化,我决定引用简单的 Echarts 进行图标可视化操作,具体 API 可以查看官方文档。以下列举我的引用:

//自定义 options 样式和数据
const chartOptions = () => {
    const keys = dayArray.map(item => item.key);
    const values = dayArray.map(item => item.value);
    return {
        grid: {
            left: 0,
            right: 0,
        },
        xAxis: {
            type: 'category',
            data: keys,
            axisTick: {
                show: false
            },
            axisLine: {
                lineStyle: {
                    color: '#4a4a4a'
                }
            },
            axisLabel: {
                formatter: function (value: string) {
                    return value.substring(5);
                }
            }
        },
        yAxis: {
            show: false
        },
        series: [
            {
                data: values,
                detail: {
                    show: true
                },
                type: 'bar',
                color: ['#80b77e'],
                itemStyle: {
                    opacity: 0.9,

                        label:{
                            show:true,
                            formatter:'¥{c}',
                            position: 'top',
                            textStyle:{
                                color: '#80b77e'
                            }
                        }
                    ,
                },
                select: {
                    itemStyle: {
                        borderColor: 'black'
                    }
                }
            }
        ],
        tooltip: {
            showContent: true,
            triggerOn: 'click',
            formatter(params:any) {
                let { value, name } = params;
                return `${name} <br/> ¥ ${value}`;
            },
            position: 'top'
        },
    };
};

//渲染
<StatisticsChart>{chartOptions()}</StatisticsChart>

//组件
const StatisticsChart: React.FC = (props) => {
    return <ReactECharts option={props.children} className="ReactEcharts"/>;
};


export {StatisticsChart};

图表效果:

image.png

九、实现日期选择器

总体思路跟实现备注一样,可以利用浏览器自身的 type = "date" 属性,只需要稍微改造一下备注组件即可(当然也需要自定义样式,这里暂不展示):

type Props = {
    value: string;
    onChange:(value:string)=>void;
    createdAt?: string;
    type?:string;
    label?:string;
}

const NoteSection: React.FunctionComponent<Props> = (props) => {
    let note = props.value
    const type = props.type
    const label = props.label
    if (props.type==="date"){
            note = dayjs(note).format('YYYY-MM-DD')
    }
    //非受控模式
    const onChange:ChangeEventHandler<HTMLInputElement> = (e)=>{
            props.onChange(e.target.value)
    }
    return (
        <Wrapper>
            <Input label={label || ''} type={type}
                   value={note}
                   onChange={onChange}
                    placeholder="请填写备注,注意一天只能添加一项喔!"
            >
            </Input>
        </Wrapper>
    );
};

export {NoteSection};


//渲染
<NoteSection value={selected.createdAt} onChange={(createdAt) => onChange({createdAt})} type="date"
             label="日期"
/>

十、封装 SVG Icon

为了引入导航栏的 svg 图片,我决定将它们封装在自定义的 Icon 组件中,使用 svgo-loader 和 svg-sprite-loader 来生成一个标签插入到页面中,并通过正则表达式和一些技巧将它们封装好:

import React from 'react';
import cs from 'classnames'
// require( 'icons/tag.svg');  //svgo-loader; svg-sprite-loader(生成标签)
// require('icons/money.svg');
// require( 'icons/chart.svg')
// 直接 require 一个目录
let importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
try {
    importAll(require.context('icons', true, /.svg$/));
} catch (error) {
    console.log(error);
}

type Props = {
    name?: string
} & React.SVGAttributes<SVGElement>

const Icon = (props: Props) => {
    const {name, children,className, ...rest} = props;
    return (
        <svg className={cs('icon',className)} {...rest}>
            {props.name && <use xlinkHref={'#' + props.name}/>}
        </svg>
    );
};

export default Icon;

实现效果:

image.png

总结

整个页面大概花费了两个星期左右,各种功能能够正常实现,包括标签选择、标签编辑、页面切换、输入数字、日期选择、添加备注、列出数据、数据可视化、数据合计等。

在完成这个项目期间我更深入地了解了 React、React Router、自定义 Hook、webpack、TypeScript、LocalStorage、Echarts 等开发功能,这些对我来说都是非常宝贵的经验。

©本总结教程版权归作者所有,转载需注明出处