Articles19
Tags0
Categories0

记账本笔记整理

记账本笔记整理

一.项目需求

一个可以根据月份统计支出收入的记账本,可以增删改查

1.运用框架:react

2.样式:bootstrap

在react中使用bootstrap可以直接在index.html引入bootstrap文件

<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
      <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
      <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">

在bootstrap中使用flex布局:

<div className="d-flex justify-content-between align-items-center"></div>

其中:justify-content是项目水平布局,align-items-center是项目垂直布局居中。

3.图标库:react-ionicons

关于Ionicons,可以参考:http://ionicons.com/

另外补充图标的知识:svg与font icon

svg是可以使用css控制,font icon 只可以用字符的样式

4.数据类型控制:PropTypes

PriceList.propTypes = {

  items:PropTypes.array.isRequired,

  onModifyItem:PropTypes.func.isRequired,

  onDeleteItem:PropTypes.func.isRequired,

}

5.图表:react charts library(Recharts)

二.一些处理

1.添加默认props,当没有props时使用默认props:

PriceList.defaultProps={

  onModifyItem:()=>{}

}

2.可以把字符串一些常量放在某个文件中进行引用

3.对于根据不同条件显示不同内容可以使用:{isOpen&&要执行的代码}(类似if或?:的判断语句)

isOpen是判断条件

4.在事件中可以添加event.preventDefault()来阻止冒泡事件

5.react中若有多个节点,可以用

<React.Fragment></React.Fragment>

来包裹。

三.日期处理

1.将传入的日期字符串格式化:

 const parseToYearAndMonth=(str)=>{
  const date=str?new Date(str):new Date()
  return {
    year:date.getFullYear(),
    month:date.getMonth()+1
  }
}

2.判断是否是有效的日期:

const isValidDate=(dateString)=>{
  const regEx= /^\d{4}-\d{2}-\d{2}$/;
  //如2019-01-01的格式
  if(!dateString.match(regEx))return false;
  const d=new Date(dateString);
  //getTime()返回距 1970 年 1 月 1 日之间的毫秒数:
  if(Number.isNaN(d.getTime()))return false;
  //toISOString使用 ISO 标准返回 Date 对象的字符串格式:
  return d.toISOString().slice(0,10)===dateString

}

3.日期选择器的月份和年份:

 //生成一组连续数字的数组
 const range=(size,startAt=0)=>{
  let arr=[]
  for(let i=0;i<size;i++){
    arr[i]=startAt+i
  }
  return arr
}
//arr是1到12的数组,月份
 const monthRange=range(12,1)
 //arr是-4到4的数组,year是传入的年份,若为2019则年份选择器的年份为2015-2023
 const yearRange=range(9,-4).map(number=>number+year)

4.日期选择器样式代码:

<button className="btn btn-lg btn-secondary dropdown-toggle"
          onClick={this.toggleDrodowm}>
          {`${year}年${padLeft(month)}月`}
</button>
        {isOpen &&
          <div className="dropdown-menu" style={{ display: 'block' ,width:'300px'}}>
            <div className='row'>
              <div className='col border-right'>
               {yearRange.map((yearNumber,index)=>
                <a 
                key={index}
                className={yearNumber===selectedYear?'dropdown-item active':'dropdown-item'}
                href='#'
                onClick={(event)=>{
                  this.selectedYear(event,yearNumber)
                }}
                >
                {yearNumber}年
                </a>
                )}
              </div>
              <div className='col months-range'>
              {monthRange.map((monthNumber,index)=>
                <a 
                key={index}
                className={monthNumber==month?'dropdown-item active':'dropdown-item'}
                href='#'
                onClick={(event)=>{
                  this.selectedMonth(event,monthNumber)
                }}
                >
                {padLeft(monthNumber)}月
                </a>)
                }

5.关于日期选择器,点击按钮和除日期选择器其他地方,日期选择器消失的实现:

 componentDidMount(){
    document.addEventListener('click',this.handleClick,false)
    //监听点击事件,一有监听到,执行回调函数handleClick
    }
    //组件关闭时清除事件监听
    componentWillUnmount(){
    document.removeEventListener('click',this.handleClick,false)
  }
   handleClick=(event)=>{
    if(this.node.contains(event.target)){
      return
    }
    //this.node为绑定ref处
    this.setState({
      isOpen:false
     })
  }
  ....
  render(){
  return(
    <div className="dropdown month-picker-component"  ref={(ref)=>{this.node=ref}}>
    ....
    </div>
  )
  }

四.数据处理

1.对于数据一对多的处理,参考数据库处理,采用外键

const items=[{
  id:1,
  title:'去云南旅游',
  date:'2019-01-01',
  price:2100,
  cid:1
},
{
  id:2,
  date:'2019-01-12',
  title:'去泰国旅游',
  price:3200,
  cid:1
},
{
  id:3,
  date:'2019-04-12',
  title:'炒股',
  price:2200,
  cid:2
}]
const category=[
  {
    id:1,
    name:'旅游',
    type:'outcome',
    iconName:'ios-plane'
  },
  {
    id:2,
    name:'理财',
    type:'income',
    iconName:'logo-yen'
  }
]

以cid为纽带连接两者:

const itemsWithCategory=items.map(item=>{
     item.category=category.filter(citem=>citem.id===item.cid)[0]
     console.log(item)
     return item
   }).filter(item=>{
     return item.date.includes(`${currentDate.year}-${padLeft(currentDate.month)}`)
   })
   //filter是过滤数据,此处是根据日期过滤,采用includes实现

2.数据操作方法的简化:采用对象键值对替代数组,就可以采用object[id]直接得到值(Flatten State)

//数据扁平化:
const flatternArr=(arr)=>(
  arr.reduce((map,item)=>{
    map[item.id]=item
    return map
  },{})
)

对象的操作方法:在items中添加类别:

const itemsWithCategory = Object.keys(items).map(id => {
    items[id].category = data.category[items[id].cid]
    return items[id]
  })
  //先取得对象的属性名数组,在添加类别

五.表单处理

<form onSubmit={(event) => { this.submitForm(event) }} noValidate>
<div className="form-group">
          <label htmlFor="date">日期 *</label>
          {/*htmlFor:htmlFor 属性设置或返回 lable 的 for 属性的值。for 属性指定 label 要绑定到哪一个表单元素。*/}
          <input
            type="date" className="form-control"
            id="date" placeholder="请输入日期"
            defaultValue={date}
            ref={(input) => { this.dateInput = input }}
          />
          {/*type规范输入框输入的内容,defaultValue指定默认值,用于编辑页面,ref把值绑定该节点,可以传输给提交*/}
        </div>
        <button type="submit" className='btn btn-primary mr-3'>提交</button>
        {/*用type绑定submit*/}
        <button className="btn btn-secondary" onClick={this.props.onCancelSubmit}> 取消 </button>
        {/*错误信息提示*/}
        {!this.state.validatePass &&
          <div className='alert alert-danger mt-5' role="alert">
            {this.state.errorMessage}
          </div>}
      </form>

提交函数的处理:

const {item,onFormSubmit}=this.props
    const editMode=!!item.id
    //如果是编辑,item.id存在,editMode为true,否则为false
    const price=this.priceInput.value.trim()*1
    const date=this.this.dateInput.value.trim()
    const title=this.titleInput.value.trim()
    //去除字符串的前后空格,price转化为数字
    if(price && date && title){
      if (price < 0) {
        this.setState({
          validatePass: false,
          errorMessage: '价格数字必须大于0'
        })     
      } else if (!isValidDate(date)) {
        this.setState({
          validatePass: false,
          errorMessage: '请填写正确的日期格式'
        })
      }else{
        this.setState({
          validatePass: true,
          errorMessage: ''
        })
        if(editMode){
          // 如果是编辑就用所填内容替代item
          onFormSubmit({...item,title,price,date},editMode)
        }else{
          onFormSubmit({ title, price, date }, editMode)
        }
      }  
    }else {
      this.setState({
        validatePass: false,
        errorMessage: '请输入所有必选项'
      })
    }
    event.preventDefault()

  }

新增项目和编辑项目是同个组件不同路由,编辑会携带项目id区分:

编辑的路由设置:

<Route path='/edit/:id'component={Create}/>

//获取参数
cosnt Create=({match})=>{

return <h1>this is the create page{match.params.id}</h1>

}

六.context

提供者:

//App.js
<AppContext.Provider value={{
      state:this.state,
      actions:this.actions
    }}>
    。。。。
    </AppContext.Provider>

消费者:可以把消费的封装:

import React from 'react'
import{AppContext}from'./App'
const WithContext=(Component)=>{
  return(props)=>(
    <AppContext.Consumer>
      {({state,actions})=>(
        <Component {...props} data={state} actions={actions}/>
      )}
    </AppContext.Consumer>
  )
}
export default WithContext

组件引用:

import WithContext from '../withContext'
class Home extends Component{
render() {
    const {data}=this.props
    console.log('dtata',data)
    }
    return(
    <p>{data}</p>
    )
}
export default WithContext(Home)

七.关于分类图标选中问题

选中图标,图标高亮,其他图标灰色

 <div className="row">
         {categories.map((category,index)=>{
           const iconColor=(category.id===selectedCategoryId)?Colors.white:Colors.gray
           const backColor = (category.id === selectedCategoryId) ? Colors.blue : Colors.lightGray
           const activeClassName = (selectedCategoryId === category.id)?'category-item col-3 active' : 'category-item col-3'
           return(
             <div className={activeClassName} key={index} role="button" style={{ textAlign: 'center'}}
             onClick={(event)=>{this.selectCategory(event, category)}}
             >
               <Ionicon
                  className="rounded-circle"
                  style={{ backgroundColor: backColor, padding: '5px' }} 
                  fontSize="50px"
                  color={iconColor}
                  icon={category.iconName}
                />
                 <p>{category.name}</p>

               </div>
           )
         })}
        </div>

八.Tabs标签(高复用性,利用child实现)

export class Tabs extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      activeIndex: props.activeIndex
    }
  }
  tabChange = (event, index) => {
    event.preventDefault()
    this.setState({
      activeIndex: index
    })
    this.props.onTabChange(index)
  }
  render() {
    const { children } = this.props
    const { activeIndex } = this.state
    return (
      <ul className="nav nav-tabs nav-fill my-4">
        {React.Children.map(children, (child, index) => {
          const activeClassName = (activeIndex === index) ? 'nav-link active' : 'nav-link'
          return (
            <li className='nav-item'>
              <a
                onClick={(event) => { this.tabChange(event, index) }}
                className={activeClassName}
                role="button"
              >
                {child}
              </a>
            </li>
          )
        })}

      </ul>
    )
  }
}
Tabs.propTypes = {
  activeIndex: PropTypes.number.isRequired,
  onTabChange: PropTypes.func.isRequired,
}
export const Tab = ({ children }) => 
<React.Fragment>{children}</React.Fragment>
//使用
const tabsText = [TYPE_OUTCOME, TYPE_INCOME]
const tabIndex = tabsText.findIndex(text => text === selectedTab)
 tabChange = (index) => {
    this.setState({
      selectedTab: tabsText[index]
    })
  }
 <Tabs activeIndex={tabIndex} onTabChange={this.tabChange}>
          <Tab>支出</Tab>
          <Tab>收入</Tab>
        </Tabs>

九.基础增删改

1.添加一个新项目

const newItem={
  id:items.length+1,
  date:'2019-01-01',
  title:'新添加的项目',
  price:100,
  cid:1

}
 createItem=()=>{
   this.setState({
     items:[newItem,...this.state.items]
   })
  }

2.删除一个项目(采用filter)

 deleteItem=(deletedItem)=>{
    const filteredItems=this.state.items.filter(item=>item.id!==deletedItem.id)
    this.setState({
      items:filteredItems
    })

  }

3.编辑一个项目

ModifyItem=(modifyItem)=>{
  const modifyItems=this.state.items.map(item=>{
    if(item.id===modifyItem.id){
      return{...item,title:'更新后的标题'}
      //此处应改为跳转到对应页面
    }else{
      return item
    }
  })
  this.setState({
    items:modifyItems
  })
  }

十.数据扁平化后优化增删改

采用App context提供action

1.action

this.actions={
      deleteItem:(item)=>{
        delete this.state.items[item.id]
        //对象删除的方法
        // console.log('删除后',this.state.items))
        this.setState({
          items:this.state.items
        })
      },
      createItem:(data,categoryId)=>{

        const newId=ID()
        const parsedDate=parseToYearAndMonth(data.date)
        //添加data未填写自动生成的数据
        data.monthCategory=`${ parsedDate.year}-${ parsedDate.month}`
        data.timestamp=new Date(data.date).getTime()
        const newItem={...data,id:newId,cid:categoryId}
        this.setState({
          items:{...this.state.items,[newId]:newItem}
          //items对象中新增item:[newId]:newItem
        })
      },
      updateItem:(item,updatedCategoryId)=>{
        const modifedItem={
          ...item,
          cid:updatedCategoryId,
          timestamp:new Date(item.date).getTime()
        }
        this.setState({
          items:{...this.state.items,[modifedItem.id]:modifedItem}
        })
      }
    }
  }

2.编辑与新增的按钮方法

//编辑,传入id
ModifyItem=(modifyItem)=>{
this.props.history.push(`/edit/${modifyItem.id}`)
  }
  //新增
 createItem=()=>{
  this.props.history.push('/create')
  }  

3.把数据传入表格:

//得到编辑项的类别
 this.state={
 selectedCategory:  (id&&items[id])?category[items[id].cid]:null}
 //id可以拿到编辑项原来的数据,若id不存在即是新增
const {id}=this.props.match.params
    const editItem=(id&&items[id])?items[id]:{}<PriceForm 
          onFormSubmit={this.submitForm}
          onCancelSubmit={this.cancelSubmit}
          item={editItem}
        />
        //组件需要绑定withRouter
        export default withRouter(WithContext(Create))

4.组件表格使用action

 //提交,根据isEditMode判断是编辑还是新增
submitForm=(data,isEditMode)=>{
    if(!isEditMode){

     //新增,传入数据和选中的类别this.props.actions.createItem(data,this.state.selectedCategory.id)
    }else{
      this.props.actions.updateItem(data,this.state.selectedCategory.id)
    }
    //回到首页
    this.props.history.push('/')
  }
  //如果是编辑,则传入的item有值,使用defaultValue={title}写入默认值
  <input
            type='text'
            className='form-control'
            id='title'
            placeholder='请输入标题'
            defaultValue={title}
            ref={(input) => { this.titleInput = input }}
          />

5.其中随机id的生成

export const ID = () => {
  // Math.random should be unique because of its seeding algorithm.
  // Convert it to base 36 (numbers + letters), and grab the first 9 characters
  // after the decimal.
  return '_' + Math.random().toString(36).substr(2, 9);
}

十一.与后端交互–准备

1.数据模拟:采用json-server,不仅可以模拟数据,还可以模拟方法(增删改查)

安装:

cnpm i --save json-server

配置(package.json):

"scripts": { 

"mock": "json-server --watch db.json --port 3004"
}

数据文件db.json(在根目录):

db.json:

{

  "categories": [

    {

      "name": "旅行",

     "iconName": "ios-plane",

      "id": "1",

      "type": "outcome"

    },

]}

运行:

npm run mock

2.交互请求的方式:

get:读

post:写

delete:删除

put和patch:更新,其中put是把请求的数据全部替换原有数据,而patch仅更新请求的数据

筛选与排序:/items?monthCategory=2018-8&_sort=timeStamp -GET

3.使用postman测试API接口

4.concurrently:将多个命令(如mock和start结合成一个命令)

因为mock和start都默认端口3000,则同时运行npm mock和npm start会报错,所以需要把mock修改端口:

 "mock": "json-server --watch db.json --port 3004"

然后将两个命令结合成一个命令:npm start

安装concurrently:

npm i concurrently --save-dev

修改配置:

"start": "concurrently \"react-scripts start\" \"npm run mock\""

5.跨域处理(create-react-app):

在package.json中添加:

"proxy":"http://localhost:3004"

十二.发起请求

//actions
//async异步转同步,包裹一个promise对象
 getInitalData:async ()=>{
        this.setState({
          isLoading:true
        })
        const {currentDate}=this.state
        const getURLWithData=`/items?monthCategory=${currentDate.year}-${currentDate.month}&_sort=timestamp&_order=desc`
        // const promiseArr=[axios.get('/categories'),axios.get(getURLWithData)]
        // Promise.all(promiseArr).then(arr=>{
        //   const [categories,items]=arr
        //   this.setState({
        //     items:flatternArr(items.data),
        //     category:flatternArr(categories.data),
        //     isLoading:false
        //   })
        // })
        //await:等待请求完成才可以接下去处理
        const results=await  Promise.all([axios.get('/categories'),axios.get(getURLWithData)])

          const [categories,items]=results
          this.setState({
            items:flatternArr(items.data),
            category:flatternArr(categories.data),
            isLoading:false
          })
          //需要返回数据
          return items

      },
      //多个请求promise
      //单个请求直接用axios
        deleteItem: async (item)=>{
        // axios.delete(`/items/${item.id}`).then(()=>{
        //   delete this.state.items[item.id]
        // // console.log('删除后',this.state.items))
        // this.setState({
        //   items:this.state.items
        // })
        // })
        const deleteItem=await axios.delete(`/items/${item.id}`)

          delete this.state.items[item.id]
          this.setState({
            items:this.state.items
          })
       return deleteItem

      },
      //使用数据
      //home
       componentDidMount(){
    this.props.actions.getInitalData().then(items=>{
      console.log('haha',items)
    })

  }
  //在this.props.data中就可以使用得到的数据了

十三.添加loading

在数据未加载成功前,可以以loading代替数据

1.样式

const Loader=()=>(
  <div className='loading-component text-center'>
    <Ionicon 
    icon='ios-refresh'
    fontSize='40px'
    color="#347eff"
    rotate={true}//是否自动旋转
    />
    <h5>加载中</h5>
  </div>
)

2.当没数据时显示:

 {isLoading&& <Loader/>}
          {
            !isLoading && 数据}

拿到数据时loading为false:

 getInitalData:async ()=>{
        this.setState({
          isLoading:true
        })
     //拿数据省略
          this.setState({
            items:flatternArr(items.data),
            category:flatternArr(categories.data),
            isLoading:false
          })
          return items

      },

3.将loading作为公共函数添加到每个action

const withLoading=(cb)=>{
      return (...args)=>{//得到参数
      this.setState({
        isLoading:true
      })
      return cb(...args)//执行参数
    }
    }

添加:

 getEditData:withLoading(async(id)=>{
        let promiseArr=[axios.get('/categories')]
        if(id){
          const getURLWithID=`/items/${id}`
          promiseArr.push(axios.get(getURLWithID))
        }
        const [categories,editItem]=await Promise.all(promiseArr)
        if(id){
          this.setState({
            category:flatternArr(categories.data),
            isLoading:false,
            items:{...this.state.items,[id]:editItem.data}
          })
        }else{
          this.setState({
            category:flatternArr(categories.data),
            isLoading:false,

          })
        }
        return {
          categories:flatternArr(categories.data),
          editItem:editItem?editItem.data:null
        }
      }),

十四.优化请求

让已加载的数据不再加载

getEditData:withLoading(async(id)=>{
        const {items,category}=this.state
        let promiseArr=[]
        if(Object.keys(category).length===0){
          promiseArr.push(axios.get('/categories'))
        }
        const itemIdISexit=(Object.keys(items).indexOf(id)>-1)//判断该id是否被加载过
        if(id && !itemIdISexit){//第一次加载
          const getURLWithID=`/items/${id}`
          promiseArr.push(axios.get(getURLWithID))
        }
        const [categories,editItem]=await Promise.all(promiseArr)
        const finalCategories=categories?flatternArr(categories.data):category
        //如果不是第一次加载,就直接用原先的categories,若是第一次就处理得到的数据
        const finalItem=editItem?editItem.data:items[id]
        if(id){
          this.setState({
            category:finalCategories,
            isLoading:false,
            items:{...this.state.items,[id]:finalItem}
          })
        }else{
          this.setState({
            category:flatternArr(categories.data),
            isLoading:false,

          })
        }
        return {
          categories:finalCategories,
          editItem:finalItem
        }
      }),

十五.模拟生产环境

express(本机的部署)

build项目:

npm build

根目录文件:server.js

需要部署的资源分为静态文件(如css,html等)和API(增删改查方法等)

const jsonServer=require('json-server')
const express=require('express')
const server = jsonServer.create()
const router=jsonServer.router('db.json')
const path=require('path')
const middlewares=jsonServer.defaults()
//中间件,可以在request和response之间做一系列的操作
const port =process.env.LEANCLOUD_APP_PORT||3000//lean cloud的端口
const root =__dirname+'/build'
//静态资源
server.use(express.static(root,{maxAge:8640000}))//过期时间
const reactRouterWhiteList=['/create','/edit/:itemId']//白名单
server.get(reactRouterWhiteList,(requst,response)=>{
  response.sendFile(path.resolve(root,'index.html'))
})
//新增或编辑当前两个页面刷新不跳转
server.use(router)
server.use(middlewares)
server.listen(3000,()=>{
  console.log('running')
})
//router和middlewares处理api

十六.线上部署

云平台的部署:

leanCloud:云引擎和云缓存

Author:shuo
Link:http://yoursite.com/2019/10/08/记账本笔记整理/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可