项目实战【vue,react,微信小程序】(1706C)

365 阅读5分钟

目录

一、接口封装

二、vue图片懒加载

三、书城后端接口

四、登录、注册、修改密码后端接口

五、时间戳转日期

六、文件上传单个文件

 七、上传多个文件

八、设置淘宝镜像

九、yarn和npm清缓存

十、react图片懒加载

十一、yarn

十二、react路由懒加载

十三、让所有接口延时返回

十四、前端使用代理解决跨域问题

十五、mock很多数据、分页查找

十六、如何上线

十七、小程序轮播图

十八、父子组件通讯

十九、路由

二十、tabBar

二十一、组件生命周期

二十二、计算属性

二十三、小程序购车

二十四、使用 Component 构造器构造页面

二十五、使用组件observers做数据监听器

github源码:


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

一、接口封装

index.js:

import axios from 'axios'
import urls from './urls'

console.log(process)
if (process.env.NODE_ENV === 'development') {
  console.log('env', process.env.NODE_ENV)
  axios.defaults.baseURL = 'http://localhost'
}


axios.interceptors.request.use((config) => {
  return config
})

axios.interceptors.response.use((res) => {
  if (res.data.code === 400) {
    alert(res.data.message)
  }
  return res
})

const common = async (config) => {
  let response = await axios(config)
  return response.data
}

export default {
  login: (data) => common({ url: urls.login, data, method: 'post' }),
  getNav: () => common({ url: urls.getNav }),
  getList: (url) => common({ url: urls.getList + url }),
  updateMyBook: (data) => common({ url: urls.updateMyBook, data, method: 'post' }),
  add:(data) => common({ url: urls.add, data, method: 'post' }),
  getMyBook: () => common({ url: urls.getMyBook }),
  getDetail: (url) => common({ url: urls.getDetail + url })
}

urls.js:

const urls = {
  login: '/api/login',
  getNav: '/api/nav',
  getList: '/api/list',
  updateMyBook: '/api/update_my_book',
  add: '/api/add',
  getMyBook: '/api/get_my_book',
  getDetail: '/api/detail'
}

export default urls

拦截器参考链接:www.axios-js.com/zh-cn/docs/…

二、vue图片懒加载

参考链接:www.npmjs.com/package/vue…

注册:

import Vue from 'vue'
import VueLazyload from 'vue-lazyload'
import loadingImg from './images/loading.gif'

Vue.use(VueLazyload, {
  preLoad: 1.3,
  error: loadingImg,
  loading: loadingImg,
  attempt: 1
})

new Vue({
  render: h => h(App)
}).$mount('#app')

 v-lazy:

      <div class="m-list-item-img-wrap">
        <img v-lazy="item.avatar" alt="" class="m-list-item-img">
      </div>

样式:

.m-list-item-img-wrap{display: flex; width: 112px;height: 150px;background: #dddddd;}
.m-list-item-img{width: 100%;}
.m-list-item-img[lazy=loading]{margin: auto; width: 40px;height: 40px;}

三、书城后端接口

const express = require('express')
const { bookNavData, bookMallData, bookMallDetailData } = require('./data')
const bodyParser = require('body-parser')
const cors = require('cors')
const history = require('connect-history-api-fallback')

const app = express()

//用户列表
let userList = [{
  id: '001',
  username: 'admin',
  password: '123456'
}, {
  id: '002',
  username: 'xu',
  password: '123'
}, {
  id: '003',
  username: 'a',
  password: '123456'
}]

//书包
let myBook = []

//跨域
app.use(cors())
//解析post请求

// parse application/json
app.use(bodyParser.json())
app.use(history())

// parse application/x-www-form-urlencoded
//app.use(bodyParser.urlencoded({ extended: false }))

//静态web服务器
app.use(express.static( __dirname + '/public'))
 
//登录
app.post('/api/login', (req, res) => {
  let { username, password } = req.body
  console.log(JSON.stringify(req.body))
  console.log(username, password)
  let user = userList.find(item => item.username === username)
  if (user) {
    if (user.password === password) {
      res.send({
        code: 200,
        data: {
          username
        },
        message: '登录成功'
      })
    } else {
      res.send({
        code: 400,
        message: '密码错误'
      })
    }
  } else {
    res.send({
      code: 400,
      message: '用户名不存在'
    })
  }

})

//导航
app.get('/api/nav', (req, res) => {
  res.send({
    code: 200,
    data: bookNavData,
    message: '导航'
  })
})

//列表
app.get('/api/list', (req, res) => {
  let { id } = req.query
  let list = bookMallData.find(item => item.id == id).list
  list.forEach(item => {
    item.is_in_my_book = myBook.findIndex(book => book.id === item.id) >= 0
  })
  res.send({
    code: 200,
    data: list,
    message: '列表'
  })
})

//详情
app.get('/api/detail', (req, res) => {
  let { id } = req.query
  let detail 
  bookMallDetailData.forEach(item => {
    item.list.forEach(book => {
      if (book.id == id) {
        book.is_in_my_book = myBook.findIndex(item => item.id === book.id) >= 0
        detail = book
      }
    })
  })

  res.send({
    code: 200,
    data: detail,
    message: '详情'
  })
})

//更新书包
app.post('/api/update_my_book', (req, res) => {
  let { myBookNew } = req.body
  myBook = myBookNew
  res.send({
    code: 200,
    data: myBook,
    message: '更新成功'
  })
})

//添加
app.post('/api/add', (req, res) => {
  let { book } = req.body
  myBook.push(book)
  res.send({
    code: 200,
    data: myBook,
    message: '添加成功'
  }) 
})

//获取书包
app.get('/api/get_my_book', (req, res) => {
  res.send({
    code: 200,
    data: myBook,
    message: '书包'
  })
})

app.listen(80)
console.log(80)

四、登录、注册、修改密码后端接口

const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')
const history = require('connect-history-api-fallback')
const uuidv1 = require('uuid/v1')
const jwt = require('jwt-simple')
const redis = require('redis')

//用户列表
let userList = [{
  id: '001',
  username: 'admin',
  password: '123456'
}, {
  id: '002',
  username: 'xu',
  password: '123'
}, {
  id: '003',
  username: 'a',
  password: '123456'
}]

//token加密密码
let secret = 'xxx'

const app = express()

const client = redis.createClient()
client.on('error', err => {
  console.log('redis错误:' + err)
})

//跨域
app.use(cors())
//解析post请求

// parse application/json
app.use(bodyParser.json())
//let jsonParser = bodyParser.json()

//处理react前端路由(BrowserRoute),vue前端路由(mode:history),注意:开启后无法用postman和浏览器地址栏调试get接口
//app.use(history())

//静态web服务器
app.use(express.static(__dirname + '/public'))

//通过中间件检查登录是否过期,并自动续期
const checkTokenByMiddleware = (req, res, next) => {
  let token = req.headers.token
  client.get(token, (err, response) => {
    if (response) {
      client.set(token, token, 'EX', 60)
      next()
    } else {
      res.send({
        code: 403,
        message: '登录过期'
      })
    }
  })
  console.log(2)
}

//登录
app.post('/api/login', (req, res) => {
  let { username, password } = req.body
  let user = userList.find(item => item.username === username)
  if (user) {
    if (user.password === password) {
      let token = jwt.encode(user.id, secret)
      client.set(token, token, 'EX', 60)
      res.send({
        code: 200,
        data: {
          username,
          token
        },
        message: '登录成功'
      })
    } else {
      res.send({
        code: 400,
        message: '密码错误'
      })
    }
  } else {
    res.send({
      code: 400,
      message: '用户名不存在'
    })
  }
})

//注册
app.post('/api/register', (req, res) => {
  let { username, password } = req.body
  console.log(username, password)
  let user = userList.find(item => item.username === username)
  if (user) {
    res.send({
      code: 400,
      message: '用户名已存在'
    })
  } else {
    let id = uuidv1()
    userList.push({
      id,
      username,
      password,
    })
    let token = jwt.encode(id, secret)
    client.set(token, token, 'EX', 60)
    res.send({
      code: 200,
      data: {
        userList,
        token
      },
      message: '注册成功'
    })
  }
})

//修改密码
app.post('/api/modify_password', checkTokenByMiddleware, async (req, res) => {
  let token = req.headers.token
  let { password } = req.body
  let id = jwt.decode(token, secret)
  console.log(id)
  let index = userList.findIndex(item => item.id === id)
  userList[index].password = password
  res.send({
    code: 200,
    data: userList,
    message: '修改成功'
  })
})

//动态路由
app.get('/api/test/:id', (req, res) => {
  let { id } = req.params
  res.send({
    code: 200,
    data: id,
    message: '动态路由测试'
  })
})

app.listen(82)
console.log(82)

五、时间戳转日期

参考链接:momentjs.cn/

import moment from "moment"
moment.locale('zh-cn')     

console.log(moment(1573042782076).format('YYYY年MM月DD日 hh:mm:ss'))
console.log(moment(1573042782076).fromNow())

六、文件上传单个文件

controller/upload.js:

const multer = require('multer')

const storage = multer.diskStorage({
  destination: (req ,file, cb) => {
    cb(null, 'upload')
  },
  filename: (req, file, cb) => {
    cb(null, `${Date.now()} - ${file.originalname}` )
  }
})

const upload = multer({ storage })

const uploadImg = (req, res) => {
  res.send({
    code: 200,
    data: req.file,
    message: '上传成功'
  })
}

module.exports = {
  upload,
  uploadImg
}

router/upload.js:

const express = require('express')
const router = express.Router()
const { upload, uploadImg } = require('../controller/upload')

router.post('/upload', upload.single('img'), uploadImg)

module.exports = router

app.js:

const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')
const history = require('connect-history-api-fallback')
const upload = require('./router/upload')

const app = express()

//跨域
app.use(cors())
//解析post请求

// parse application/json
app.use(bodyParser.json())
//let jsonParser = bodyParser.json()

//处理react前端路由(BrowserRoute),vue前端路由(mode:history),注意:开启后无法用postman和浏览器地址栏调试get接口
app.use(history())

//静态web服务器
app.use(express.static(__dirname + '/public'))
app.use(express.static(__dirname + '/upload'))

//上传文件
app.use('/api/', upload)

app.listen(82)
console.log(82)

使用postman测试上传接口:

前端:

<input type="file" @change="handleUpload" >

    handleUpload(e) {
      const data = new FormData()

      data.append('img', e.target.files[0])

      axios({
        url: '/api/upload',
        data,
        method: 'post',
        timeout: 1000 * 60
      }).then(res => {
        if (res.code === 200) {

        }
      })
    }

 七、上传多个文件

controller/upload.js:

const multer = require('multer')

const storage = multer.diskStorage({
  destination: (req ,file, cb) => {
    cb(null, 'upload')
  },
  filename: (req, file, cb) => {
    cb(null, `${Date.now()}-${file.originalname}` )
  }
})

const upload = multer({ storage })

const uploadImg = (req, res) => {
  res.send({
    code: 200,
    data: req.files,   //单个文件:req.file 多个文件:req.files
    message: '上传成功'
  })
}

module.exports = {
  upload,
  uploadImg
}

router/upload.js:

const express = require('express')
const router = express.Router()
const { upload, uploadImg } = require('../controller/upload')

//上传单个文件
//router.post('/upload', upload.single('img'), uploadImg)

router.post('/upload', upload.array('img', 9), uploadImg)

module.exports = router

app.js和上传单个文件相同

使用postman测试上传多个文件:

前端代码:

<input type="file" multiple="multiple" @change="handleUpload" >


    handleUpload(e) {
      const data = new FormData()
      let files = e.target.files
      files.forEach(item => {
        data.append('img', item)
      })
      axios({
        url: '/api/upload',
        data,
        method: 'post',
        timeout: 1000 * 60
      }).then(res => {
        if (res.code === 200) {

        }
      })
    }
  },

 

 

 

八、设置淘宝镜像

yarn config set registry http://registry.npm.taobao.org/

yarn config set sass-binary-site http://npm.taobao.org/mirrors/node-sass

九、yarn和npm清缓存

npm cache clean --force

yarn cache clean

十、react图片懒加载

参考链接:

www.npmjs.com/package/rea…

import LazyLoad from 'react-lazy-load'

      <div key={item.id} className="m-list-item">
        <LazyLoad className="m-list-item-img-wrap">
          <img src={item.avatar} className="m-list-item-img"></img>
        </LazyLoad>
        <div className="m-list-item-info">
          {item.title}
        </div>
      </div>
.m-list-item{display: flex;margin: 5px;}
.m-list-item-img-wrap{display: flex; width: 112px;height: 150px;background: #dddddd;}
.m-list-item-img-wrap::before{content: '';margin: auto;width: 38px;height: 38px;background-image: url(./images/loading.png);animation: loading 0.5s linear infinite;}
.m-list-item-img{position: absolute; width: 112px;height: 150px;}
.m-list-item-info{flex:1}

@keyframes loading {
  from {transform: rotate(0deg);}
  to{transform: rotate(360deg);}
}

 

 

十一、yarn

yarn是什么? 能干什么?
yarn是facebook发布的一种包管理工具,作用同npm 一样,是一个包管理用具

 

优点:

快速: 1.会缓存它下载的每个包, 无需重复下载;能并行化操作以最大资源利用率

可靠: 使用格式详尽而又简洁的 lockfile文件 和确定性算法来安装依赖,能够保证在一个系统上的运行的安装过程也会以同样的方式运行在其他系统上。

安全: 安装包被执行前校验其完整性

 

十二、react路由懒加载

参考链接:

zh-hans.reactjs.org/docs/code-s…

import React, { Component, Suspense, lazy } from 'react'
import { NavLink, Switch, Route } from 'react-router-dom'
import Home from './Home'
//import MyBook from './MyBook'
import Loading from '../components/Loading'
const MyBook = lazy(async () => { 
  return await new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(import('./MyBook'))
    }, 2000)
  })
})

export default class Index extends Component {
  render() {
    return (
      <div>
        <div>
          <NavLink to="/index/home" className="m-nav-item">首页</NavLink>
          <NavLink to="/index/my_book" className="m-nav-item">书包</NavLink>
        </div>
        <Suspense fallback={<Loading lazyLoading={true}></Loading>}>
          <Switch>
            <Route path="/index/home" component={Home}></Route>
            <Route path="/index/my_book" component={MyBook}></Route>
          </Switch>
        </Suspense>
      </div>
    )
  }
}

Loading.js:

import React, { Component } from 'react'
import { connect } from 'react-redux'

class Loading extends Component {
  render() {
    let { loading, lazyLoading } = this.props
    return (
      <div className={"m-loading-wrap " + (loading ? 'active ' : '') + (lazyLoading ? 'active' : '')}>
        <span className="m-loading-img"></span>
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  state = state.toJS()
  return {
    loading: state.loading
  }
}

export default connect(mapStateToProps)(Loading)

loading样式文件:

.m-loading-wrap{position: fixed;display: none; top: 0;left: 0;right: 0;bottom: 0; background: rgba(0, 0, 0, 0.5);z-index: 999;}
.m-loading-wrap.active{display: flex;}
.m-loading-img{ display: inline-block;margin: auto; width: 38px;height: 38px;background-image: url(./images/loading.png);background-size: 100% 100%; animation: loading 0.5s linear infinite; }

@keyframes loading {
  from {transform: rotate(0deg);}
  to{transform: rotate(360deg);}
}

路由懒加载时,需要懒加载的路由组件特别小,这时很难观察到加载的loading效果,怎么办呢?

可以把使用Promise模拟延时:

const MyBook = lazy(async () => { 
  return await new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(import('./MyBook'))
    }, 2000)
  })
})

十三、让所有接口延时返回

目的是测试前端loading效果

//让所有接口延时返回
app.use((req, res, next) => {
  setTimeout(() => {
    next()
  }, 500)
})

十四、前端使用代理解决跨域问题

To tell the development server to proxy any unknown requests to your API server in development, add a proxy field to your package.json, for example:

"proxy": "http://localhost:83"

也可以增加一个文件:

装包:

yarn add http-proxy-middleware

 在src目录下面建一个文件,文件名是setupProxy.js  即 src/setupProxy.js:

const proxy = require('http-proxy-middleware');
module.exports = function(app) {
  app.use(
    '/api',
    proxy({
      target: 'http://localhost:83',
      changeOrigin: true,
    })
  );
};

参考链接:

create-react-app.dev/docs/proxyi…

十五、mock很多数据、分页查找

装包:

yarn add mockjs

 

const Mock = require('mockjs')


const mockDataList = Mock.mock({
	'list|100': [{
		'id|+1': 1,
		'name': '@cname',
		'title': '@ctitle',
		'image': '@image(300x300)',
		'address': '@county(true)'
	}]
}).list
//分页
app.get('/api/mock_data', (req, res) => {
  let { page, size } = req.query
  let start = (page - 1) * size
  let end = start + Number(size)
  console.log(start, end)
  let data = mockDataList.slice(start, end)

  res.send({
    code: 200,
    data: data,
    message: '列表'
  })
})

前端分页,滚动到底加载更多:

import React, { Component } from 'react'
import axios from 'axios'

let updateDone = true
export default class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      list: [],
      end: '',
      page: 1
    }
  }
  handleScroll(e) {
    let { list, end, page} = this.state
    if (
      e.target.clientHeight + e.target.scrollTop + 200 >
        e.target.scrollHeight &&
      end === "" && updateDone
    ) {
      updateDone = false
      page = page + 1;
      axios({
        url: `/api/mock_data?page=${page}&size=20`
      }).then(res => {
        if (res.data.code === 200) {
          this.setState({
            list: list.concat(res.data.data),
            page
          })
          if (res.data.data.length < 20) {
            console.log("到底了");
            this.setState({
              end: "到底了"
            })
          }
        }
      });
    }
  }
  componentDidUpdate() {
    updateDone = true
  }
  componentDidMount() {
    axios({
      url: '/api/mock_data?page=1&size=20'
    }).then(res => {
      if (res.data.code === 200) {
        this.setState({
          list: res.data.data
        })
      }
    })
  }
  render() {
    let { list, end } = this.state
    let listDom = list.map(item => (
      <div key={item.id} className="m-list-item">{item.name}</div>
    ))

    return (
      <div className="m-warp" onScroll={this.handleScroll.bind(this)}>
        {listDom}
        <div className="m-end">{end}</div>
      </div>
    )
  }
}

十六、如何上线

打包:

yarn build

利用 Express 托管静态文件:

//静态web服务器
app.use(express.static(__dirname + '/public'))
app.use(express.static('public'))

参考链接:

www.expressjs.com.cn/starter/sta…

利用 connect-history-api-fallback 解决前端history路由刷新报错:

const history = require('connect-history-api-fallback')

//处理react前端路由(BrowserRoute),vue前端路由(mode:history),注意:开启后无法用postman和浏览器地址栏调试get接口
app.use(history())

参考链接:

www.npmjs.com/package/con…

 

十七、小程序轮播图

  //图片的宽高比等于手机屏幕的宽度和swiper的高度比
  handleImageLoad(e) {
    let { windowWidth } = wx.getSystemInfoSync()
    let { height, width } = e.detail
    height = height /  width * windowWidth
    this.setData({
      height
    })
  },
  <swiper 
    indicator-dots="{{true}}"
    autoplay="{{true}}"
    interval="{{5000}}"
    circular="{{true}}"
    style="height:{{height}}px">
    <swiper-item wx:for="{{banner}}" wx:key="{{index}}">
      <image src="{{item}}" mode="widthFix" class="m-item-img" bindload="handleImageLoad"></image>
    </swiper-item>
  </swiper>

十八、父子组件通讯

父组件:

<book-nav navList="{{navList}}" currentId="{{currentId}}" bind:onNav="handleNav"></book-nav>

 

  handleNav(e) {
    let { id } = e.detail
    this.setData({
      currentId: id
    })
    wx.request({
      url: `${host}/api/list?id=${id}`,
      success: (res) => {
        if (res.data.code === 200) {
          this.setData({
            currentList: res.data.data
          })
        }
      }
    })
  },

子组件:

// components/book-nav/book-nav.js
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    navList: Array,
    currentId: Number
  },

  /**
   * 组件的初始数据
   */
  data: {

  },

  /**
   * 组件的方法列表
   */
  methods: {
    handleNav(e) {
      let { id } = e.mark
      this.triggerEvent('onNav', { id })
    }
  }
})

十九、路由

      wx.navigateTo({
        url: `/pages/detail/detail?id=${id}`,
      })
  onLoad: function (options) {
    let { id } = options
    wx.request({
      url: `${host}/api/detail?id=${id}`,
      success: (res) => {
        if (res.data.code === 200) {
          this.setData({
            detail: res.data.data
          })
        }
      }
    })
  },

二十、tabBar

  "tabBar": {
    "color": "#333333",
    "selectedColor": "#ff0000",
    "list": [
      {
        "pagePath": "pages/home/home",
        "text": "首页",
        "iconPath": "/static/images/home.png",
        "selectedIconPath": "/static/images/home-active.png"
      },
      {
        "pagePath": "pages/my-book/my-book",
        "text": "书包",
        "iconPath": "/static/images/cart.png",
        "selectedIconPath": "/static/images/cart-active.png"
      },
      {
        "pagePath": "pages/index/index",
        "text": "我的",
        "iconPath": "/static/images/me.png",
        "selectedIconPath": "/static/images/me-active.png"
      }
    ]
  }

二十一、组件生命周期

  lifetimes: {
    ready() {
      wx.request({
        url: `${host}/api/my_book`,
      })
    }
  }
  pageLifetimes: {
    show() {
      wx.request({
        url: `${host}/api/my_book`,
        success: (res) => {
          if (res.data.code === 200) {
            this.setData({
              myBook: res.data.data
            })
          }
        }
      })
    }
  }

二十二、计算属性

在终端进入小程序目录,使用下面的命令省事package.json文件

npm init -y

装包:

yarn add miniprogram-computed

构建npm:

构建后生成miniprogram_npm:

使用计算属性计算总价,总数:

// pages/my-book/my-book.js
const computedBehavior = require('miniprogram-computed')
const { host } = getApp().globalData

Component({
  behaviors: [computedBehavior],
  /**
   * 组件的属性列表
   */
  properties: {

  },

  /**
   * 组件的初始数据
   */
  data: {
    myBook: []
  },

  computed: {
    total(data) {
      let totalPrice = 0, totalCount = 0
      data.myBook.forEach(item => {
        totalCount += item.count
        totalPrice += item.count * item.price
      })
      return {
        totalCount,
        totalPrice
      }
    }
  },

  /**
   * 组件的方法列表
   */
  methods: {

  },

  pageLifetimes: {
    show() {
      wx.request({
        url: `${host}/api/my_book`,
        success: (res) => {
          if (res.data.code === 200) {
            this.setData({
              myBook: res.data.data
            })
          }
        }
      })
    }
  }
})

参考链接:

github.com/wechat-mini…

developers.weixin.qq.com/miniprogram…

二十三、小程序购车

 

二十四、使用 Component 构造器构造页面

页面的生命周期方法(即 on 开头的方法),应写在 methods 定义段中。

Component({

  properties: {
    paramA: Number,
    paramB: String,
  },

  methods: {
    onLoad: function(options) {
      let { id } = options
      this.data.paramA // 页面参数 paramA 的值
      this.data.paramB // 页面参数 paramB 的值
    },
    onShow() {

    }
  }
})

也可以写在pageLifetimes字段里(去掉on):


Component({
  properties: {

  },


  data: {
    detail: {}
  },

  pageLifetimes: {
    load(options) {
      let { id } = options
    },
    show() {

    }
  }
})

二十五、使用组件observers做数据监听器

可以代替计算属性

  observers: {
    'myBook': function (myBook) {
      this.update(myBook)
      let totalPrice = 0, totalCount = 0
      myBook.filter(item => item.checked).forEach(item => {
        totalCount += item.count
        totalPrice += item.count * item.price
      })
      let total = {
        totalCount,
        totalPrice,
        selectedAll: myBook.length === myBook.filter(item => item.checked).length && myBook.length > 0
      }
      this.setData({
        total
      })
    }
  },

二十六、antd移动版

mobile.ant.design/components/…

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

github源码:

github.com/baweireact/…