专栏首页一Li小麦react实战:umi问卷发布系统

react实战:umi问卷发布系统

"我在团队中的地位,在于我懂他们不会的东西。因此要保持核心竞争力,就是不要告诉别人自己会的东西"

技术团队中,保持技术分享和持续的学习是完全必要的。企业主会说:"公司不是培训机构。"这固然正确。但一个公司,总会遇到这种或那种需要攻关的难题。当你不愿意分享解决方案,或者身边的同事既不愿意学习,也不接受新的东西,反而一而再再而三糊弄。那团队怎么配合?

有个技术大牛曾经曰过(名字不可考,但确不是我臆造的):一个乐队里,你要把自己当成最水的那个。如果你不幸成为了乐队里最牛的那个成员,就可以考虑离开这个乐队了。同理,在类似的技术团队里,你不牛,就是留下去的理由。你牛,你就应该培育副手。自身的核心竞争力在于能够不断地提出攻关的方案,去带领团队成员去以技术创新驱动业务发展。

本文将用umi完成一个问卷发布系统项目。(logo暂时盗用问卷星)

笔者曾经写过类似的,一个相当大的项目。由于种种原因,留下了太多太多太多的遗憾。现在想实现一个精简优化版(不妨称之为umi问卷发布系统)。使用更加规范,更加精致的技术手段去实现。当然,我希望会是一个更加牛逼的体现。

和分享一样,如果一个项目不敢开源,那就是代码写的烂。因此届时也将会是开源的。

而本文将避免涉及产品业务的内容,更偏重于讨论技术问题:

  • 布局
  • antd-pro
  • 用户登录认证
  • 题库

看这篇文章之前,建议重新复习这2篇文章的内容。

React全家桶之Redux使用

react全家桶之router使用

项目技术栈

阿里系项目框架。 蚂蚁金服antd-pro https://pro.ant.design/index-cn umi:https://umijs.org/zh/guide/ dva:https://dvajs.com/guide/

antd-pro在antd的基础上,针对后台管理,抽取了更加详细的业务组件。官网描述其为"开箱即用"的解决方案。

设计精美,遗憾的是,文档有点烂。

npm install ant-design-pro --save

umi,中文可发音为乌米,是一个可插拔的企业级 react 应用框架。也是蚂蚁金服的底层前端框架。

$ sudo npm i yarn tyarn -g
# 后面文档里的 yarn 换成 tyarn
$ tyarn -v

$ yarn global add umi
$ umi -v
2.0.0

Dva,原为《守望先锋》的游戏角色。是由阿里架构师 sorrycc 带领 team 完成的一套前端框架,在作者的 github 里是这么描述它的:”dva 是 react 和 redux 的最佳实践”。(项目已集成)


在本项目中,可以克隆一下项目的基础(原型是一个商城架构)

git clone -b step01 https://github.com/57code/umi-test.git

在这个项目里:

直接 umi dev执行即可。

后台布局容器(layout/index.js)

后台布局一般是要自己写。但在antd-pro中,这是不必要的。在antd-pro中,自动化创建优秀到让人咋舌的地步。修改 layout/index.js

import {Layout} from 'antd';
import styles from './index.css'

const {Header,Footer,Content}=Layout;

export default function(props) {
  return (
    <Layout>
      <Header className={styles.header}>导航</Header>

      <Content className={styles.content}>
        <div className={styles.box}>
          {props.children}
        </div>
      </Content>

      <Footer className={styles.footer}>页脚</Footer>
    </Layout>
  )
}

接着把样式(style)写一写:

.header {
    color: white;
  }
  .content {
    margin: 16px;
  }
  .box {
    padding: 24px;
    background: #fff;
    min-height: 500px;
  }
  .footer {
    text-align: center;
  }

如果想要自己的组件生效,需要改一下配置(config),让layout作为父组件把路由包起来即可:

// config/index.js
export default {
  plugins: [
    [
      "umi-plugin-react",
      {
        antd: true,
        dva: true
      }
    ]
  ],
  routes: [
    { path: "/login", component: "./login" },
    {
      path: "/",
      component: "../layouts",
      routes: [
        // 移动之前路由配置到这里

         ]
    }
  ]
};

把所有后台相关的页面组件全部放倒layout中。不需要登录的除外。

导航

导航可引入antd的菜单( Menu)组件和umi的Link组件( importLinkfrom"umi/link")。

<Header className={styles.header}>
        {/* 新增内容 */}
        // <Menu />
</Header>
动态菜单
const menus=[
    {path:'/',name:'首页'},
    {path:'/questionBank',name:'题库'},
    {path:'/addQuestionnaire',name:'我的'}
  ]

  let menus=menus.map((x,i)=>{
            return <Menu.Item key={x.path}>
                    <Link to={x.path}>{x.name}</Link> 
                  </Menu.Item>
          });

注意,这里不以i作为key。

导航匹配路由

当我在那个路由下,自动激活路由。

const selectedKeys=menus.filter(x=>{
    if(x.path==='/'){
      return pathname==='/';
    }
    return pathname.indexOf(x.path)!==-1;
  }).map(x=>{return x.path});

<Menu
    // ...
    defaultSelectedKeys={selectedKeys} >

用户登录认证(又是登录)

先以404页面为例示范antd-pro的用法:

import {Exception} from 'ant-design-pro'
export default function() {
  return (
    <Exception type="404" backText="返回首页"></Exception>
  );
}

登录,有太多的东西可以扯,接下来看看umi下的登录流程业务是如何实现的。

页面

antd-pro给我们提供了一个特别好用的组件Login,里面有优秀的语义化应用。

import React, { Component } from "react";

import styles from "./login.css";
import router from "umi/router";
import { Login } from "ant-design-pro";
const { UserName, Password, Submit } = Login; // 通用的用户名、密码和提交组件

// 改为类形式组件,可持有状态
export default class extends Component {
  // let from = props.location.state.from || "/";
  // 重定向地址
  onSubmit = (err, values) => {
    console.log(err, values);
  };

  render() {
    return (
      <div className={styles.loginForm}>
        {/* logo */}
        <img className={styles.logo}
          src="https://www.wjx.cn/images/commonImgPC/logo@2x.png" /> 
        {/* 登录表单 */}
        <Login onSubmit={this.onSubmit}>
        <UserName
            name="username"
            placeholder="dangjingtao"
            rules={[{ required: true, message: "请输入用户名" }]}
          /> 
          <Password
            name="password"
            placeholder="123456"
            rules={[{ required: true, message: "请输入密码" }]}
          />

          <Submit>登录</Submit> 
        </Login>

      </div>);
  }
}

目前情况下如果要做有状态组件,还只能用传统的class-like component。

Mock数据

login要求登录发回一个对象,包括权限,基本信息和token。

在mock下新建login.js

// mock登录接口
export default {
    "post /api/login"(req, res, next) {
        const { username, password } = req.body;
        // console.log(username, password);
        if (username == "dangjingtao" && password == "123456") {
            return res.json({
                code: 0,
                data: {
                    token: "666",
                    role: "admin",
                    balance: 1000,
                    username: "党某某"
                }
            });
        }

        if (username == "djtao" && password == "123") {
            return res.json({
                code: 0,
                data: {
                    token: "666",
                    role: "user",
                    balance: 100,
                    username: "东尼大涛"
                }
            });
        }
        // 返回一个失败的回调
        return res.status(401).json({
            code: -1,
            msg: "密码错误"
        });
    }
};

mock的接口和写express基本一样。

有了"接口",就可以尝试写一个model与之联调(tiao)了。

model

models主要放登录方法,保存登录态(redux)。

import axios from "axios";
import router from "umi/router";

// 初始状态:本地缓存或空值对象
const userinfo = JSON.parse(localStorage.getItem("userinfo")) || {
    token: "",
    role: "",
    username: "",
    balance: 0
};

// 登录请求返回值
function login(payload) {
    return axios.post("/api/login", payload);
}

export default {
    // 命名空间。可省略
    namespace: "user", 
    state: userinfo,
    // 副作用登录后续操作
    effects: {
        // action: user/login
        async login({ payload }, { call, put }) {

            // 调用login传参数payload
            const { data: { code, data: userinfo } } = await login(payload);

            if (code == 0) {
                // 登录成功: 缓存用户信息
                localStorage.setItem("userinfo", JSON.stringify(userinfo)); 
                // 派发action
                await put({ type: "init", payload: userinfo }); 
                // 重定向
                router.push('/');
            } else {
                // 登录失败:弹出提示信息,可以通过响应拦截器实现

            }
        }
    },

    reducers: {
        init(state, action) {
            // 覆盖旧状态
            return action.payload;
        }
    }
};

让login组件带上状态

从dva中获取connect。

import React, { Component } from "react";

import styles from "./login.css";
import router from "umi/router";
import { Login } from "ant-design-pro";
import { connect } from "dva"
const { UserName, Password, Submit } = Login; // 通用的用户名、密码和提交组件

export default connect()(function (props) {
  // let from = props.location.state.from || "/";

  // 登录业务
  const onSubmit = (err, values) => {
    console.log(err, values);
    // value就是你的传参
    if (!err) {
      props.dispatch({
        type:'user/login',
        payload:values
      })
    }
  };

  return (
    <div className={styles.loginForm}>
      {/* logo */}
      <img className={styles.logo}
        src="https://www.wjx.cn/images/commonImgPC/logo@2x.png" />
      {/* 登录表单 */}
      <Login onSubmit={onSubmit}>
        <UserName
          name="username"
          placeholder="dangjingtao"
          rules={[{ required: true, message: "请输入用户名" }]}
        />
        <Password
          name="password"
          placeholder="123456"
          rules={[{ required: true, message: "请输入密码" }]}
        />

        <Submit>登录</Submit>
      </Login>

    </div>);

})

错误处理

一个登录业务逻辑写到现在,已经有很多地方可以捕捉登录错误。从前端角度说,最佳的捕捉地点user.js中的effect。那么什么 if(code===0)之类的都可以去掉了。

// 调用login传参数payload
const { data: { code, data: userinfo } } = await login(payload);

try {
        // 登录成功: 缓存用户信息
        localStorage.setItem("userinfo", JSON.stringify(userinfo)); 
        // 派发action
        await put({ type: "init", payload: userinfo }); 
        // 重定向
        router.push('/');
} catch (error) {
    // 登录失败:弹出提示信息,可以通过响应拦截器实现
    console.log(error)
}

然后是axios拦截器,在src下新建interceptor.js,直接调用ui框架报错。

import axios from "axios";
import { notification } from "antd";
// 列举常见错误码
const codeMessage = {
    202: "一个请求已经进入后台排队(异步任务)。",
    401: "用户没有权限(令牌、用户名、密码错误)。",
    404: "发出的请求针对的是不存在的记录,服务器没有进行操作。", 500: "服务器发生错误,请检查服务器。"
};

// 仅拦截异常状态响应
axios.interceptors.response.use(null, ({ response }) => {
    if (codeMessage[response.status]) {
        notification.error({
            message: `请求错误 ${response.status}: ${response.config.url}`,
            description: codeMessage[response.status]
        });
    }
    return Promise.reject(err);
});

然而intercepter是不会无端起作用的。必须找个地方执行一下。

在src下新建一个global.js,gloal.js将在umi初始化时执行一次。

// 全局入口
import interceptor from './interceptor'

是的这样就可以了。

路由守卫

login页面守卫的是"私有"的路由。回到config下的config.js:

我要保护 /me下的一系列路由,最直接的方法是输出一个高阶组件 PrivateRoute.js,让它来承载登录保护的路由。

{
  path: "/me",
  component: "./me",
  Routes: ["./routes/PrivateRoute.js"]
},

继续翻到routers文件夹下的PrivateRoute.js,添加登录态判断(又是拿connect):

import Redirect from "umi/redirect";
import {connect} from 'dva';

export default connect(state=>({
  isLogin:!!state.user.token
})) (props => {
  console.log(props);
  if (!props.isLogin) {
    // 如果没登录,重定向。
    return (
      <Redirect
        to={{
          pathname: "/login",
          state: { from: props.location.pathname } // 传递重定向地址
        }}
      />
    );
  }else{
    // 登录了
    return (
      <div>
        <div>PrivateRoute (routes/PrivateRoute.js)</div>
        {props.children}
      </div>
    );
  }
});

题库(questionBank)

从业务上说,题库相当于一个市场。用户就像买菜的人,可以从中采集内容。添加到"我的收藏中"

技术上说,题库的主体是一个列表页,透过列表可以拿到详情页。通过实现题库,可以学习如何在umi的框架下创建页面。

页面的架构,应该是在pages下面定义一个questionBank文件夹,在里面写子页面,样式和models方法。

路由配置

// config.js
{
  path: "/questionBank",
  component: "./questionBank/_layout",
  routes: [
    { path: "/questionBank/", component: "./questionBank/index" },
    { path: "/questionBank/:id", component: "./questionBank/$id" }
  ]
},

mock接口

定义:/api/questions为获取题库的列表。有基本的筛选功能。

// mock/questions.js
let data = [
    { 
        title: "试试你的爱情果是什么",
        type:"singleChoice",
        question:"请选择你的爱情果:(只能选一个哦)",
          tags:'1,2',
        options:[
            {
                option:"A",
                content:"菠萝",
                discription:"丑陋式的恋爱"
            },
            {
                option:"B",
                content:"柠檬",
                discription:"同性恋"
            },
            {
                option:"C",
                content:"西瓜",
                discription:"老土式的恋爱"
            },
            {
                option:"D",
                content:"椰子",
                discription:"暴力式恋爱的爱"
            },
        ]

    },
        // ...
];

export default {
    // "method url": (req, res) => {}
    "get /api/questions": function (req, res, next) {
        setTimeout(() => {
            res.json({
                result: data
            });
        }, 2500);
    }
}

models

models负责接口调用,redux状态变更等事宜。

// /questionBank/models/
import axios from 'axios';

// api
function getQuestions(){
  return axios.get('/api/questions')
}

export default {
  namespace: "questionBank",
  state: { // 初始状态包括问题和标签
    questions: [], // 课程
  },
  effects: {
    *getList(action, { call, put }) {
      let res=yield call(getQuestions);
      const  questions = res.data; 
      // 派发initGoods
      yield put({ type: "init", payload: questions});
    }
  },
  reducers: {
    init(state, { payload }) {
      state.questions=payload.result;
      return state;
    },
  }
};

页面

链接dva

通过connect链接到redux,

触发数据修改

import React, { Component } from "react";
import {List,Avatar,Progress} from 'antd'
import styles from "./index.less";
import { connect } from "dva";

@connect(
  state => ({
    questions: state.questionBank.questions,
    tags:[]
    // tags: state.goods.tags,
    // loading: state.loading
  })
)
class Questions extends Component{
  constructor(props){
    super(props);
    console.log(props);

    this.state={
      questions:new Array(8).fill({}), // 设置size可用于骨架屏展示
      tags:[]
    }
  }

  componentDidMount(){
    this.props.dispatch({
      type:'questionBank/getList'
    }).then(()=>{
      this.setState({
        questions:this.props.questions
      })
    })
  }

  render(){
    let {questions}=this.state;
    return (
      <div className={styles.normal}>
        <h2>题库</h2>
        <ul>
                    {
            //...
          }
        </ul>
      </div>
    );
  }
}
export default Questions;

接下来就是写题库样式了。

标签筛选

假设我有一系列的标签比如恋爱,都市,职场等等。允许作者进行快捷筛选。怎么办?

那么得先mock新的接口。

// mock/question.js
let tags=[
    {
        id:1,
        name:'恋爱'
    },
    {
        id:2,
        name:'都市'
    },
    {
        id:3,
        name:'职场'
    }
]

// ...
    "get /api/questionTags":function(req,res,next){
        setTimeout(()=>{
            res.json({
                code:0,
                types
            })
        },2500)
    }

修改models:

export default {
  namespace: "questionBank",
  state: { // 初始状态包括问题和标签
    questions: [], // 问题
    tags:[]
  },
  effects: {
    *getList(action, { call, put }) {
      let res=yield call(getQuestions);

      // 派发initGoods
      yield put({ type: "init", payload: res.data});
    },
    *getTags(action,{call,put}){
      let res =yield call(getTags)

      yield put({ type: "init", payload: res.data});
    }
  },
  reducers: {
    init(state, { payload }) {
      console.log(Object.assign(state,payload))
      return {...state,payload};
    },
  }
};

在page页面中拿tags:

componentDidMount(){
    this.props.dispatch({
      type:'questionBank/getList'
    }).then(()=>{
      this.setState({
        questions:this.props.questions
      })
    })
    this.props.dispatch({
      type:'questionBank/getTags'
    }).then(()=>{
      this.setState({
        tags:this.props.tags
      })
    })
  }

tag应该是多选的。所以引入新状态tagSelect=[]

那么展示页面就不能是tag。而是根据tag过滤之后的 displayQuestion

接下来就是一串无聊的业务代码了。因为多处用到了比较,所以双循环也很多:

// 判断是否存在数组中,有则返回索引值,没有则返回-1
  isSelect = (item, arr) => {
    return arr.indexOf(item)
  }

  // 标签选择处理:参数为0时,默认全选
  setTags = (id) => {
    if (id === 0) {
      this.setState({
        tagSelect: []
      })
    } else {
      this.setState((preState) => {
        let ret = preState.tagSelect;
        let isSelect = this.isSelect(id, ret);

        if (isSelect < 0) {
          ret.push(id);
        } else {
          ret.splice(isSelect, 1)
        }

        return {
          tagSelect: ret
        };
      })
    }
  }

  // 标签过滤
  filter = (data) => {
    if (this.state.tagSelect.length == 0) {
      return data;
    } else {
      return data.filter((x, i) => {
        const itemTags = x.tags.split(',');
        // 问题标签中只要有一个在tagSelect中,就可以了
        let bCheck = itemTags.some((y, i) => {
          return this.state.tagSelect.indexOf(Number(y)) >= 0;
        });
        return bCheck;
      })
    }
  }

render(){

        let { questions, tags, tagSelect } = this.state;
    // 标签渲染
    let cTags = tags.map(x => {
      return (<CheckableTag
        checked={this.isSelect(x.id, tagSelect) >= 0}
        onChange={() => { this.setTags(x.id) }} key={x.id}>
        {x.name}
      </CheckableTag>)
    });

    // 列表标签条件渲染
    let displayQuestions = this.filter(questions);

         // ...
      return (
      // ...
        <div>
          <CheckableTag
            key={0}
            checked={this.state.tagSelect.length == 0 || this.state.tagSelect.length == this.state.tags.length}
            onChange={() => { this.setTags(0) }}
          >全部</CheckableTag>
          {cTags}
       </div>
      // ...
    )
}

那么效果就基本实现了。


本文分享自微信公众号 - 一Li小麦(gh_c88159ec1309),作者:一li小麦

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-07-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 基于react的H5音频播放器

    项目是基于React,镶嵌在页面。为此开发了组件audio.js。不过不管什么框架。逻辑都是一样的。

    一粒小麦
  • 组件设计基础(2)

    早期的react设计了许多的生命周期钩子。它们严格定义了组件的生命周期,一般说,生命周期可能会经历如下三个过程:

    一粒小麦
  • 实现redux

    上面实现了兄弟组件的通信,但是复用性差,而且store里的listeners不应该被外界修改。

    一粒小麦
  • cssjshtml vue.js router几种跳转方式

      goToBefore(){       //跳转到上一次浏览的页面       this.$router.go(-1);     },     //...

    葫芦
  • Vuex状态管理总结

    3、Vuex 应用的核心是 store(仓库)-- 包含 state(组件中的共享状态)和 mutations(改变状态的方法)

    Leophen
  • 5. ListView应用

    ListView大概是所有移动应用都会用到的组件了,大部分都在首页,这章结合redux来看如何从API取数据再到如何应用redux更新渲染组件ListView。

    MasterVin
  • java中\n\r的区别 原

    回车”(Carriage Return)和“换行”(Line Feed)这两个概念的来历和区别。  在计算机还没有出现之 前,有一种叫做电传打字机(Telet...

    wuweixiang
  • react入门(六):状态提升&context上下文小白速懂

    使用 react 经常会遇到几个组件需要共用状态数据的情况。这种情况下,我们最好将这部分共享的状态提升至他们最近的父组件当中进行管理。

    柴小智
  • 教你如何在React及Redux项目中进行服务端渲染

    使用 redux-saga 处理异步action,使用 express 处理页面渲染

    书童小二
  • [天池比赛] 新冠疫情相似句对判断

    比赛链接:https://tianchi.aliyun.com/competition/entrance/231776/introduction?spm=517...

    MachineLP

扫码关注云+社区

领取腾讯云代金券