前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >react实战:umi问卷发布系统

react实战:umi问卷发布系统

作者头像
一粒小麦
发布2019-07-31 16:07:51
5.5K0
发布2019-07-31 16:07:51
举报
文章被收录于专栏:一Li小麦

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

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

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

本文将用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的基础上,针对后台管理,抽取了更加详细的业务组件。官网描述其为"开箱即用"的解决方案。

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

代码语言:javascript
复制
npm install ant-design-pro --save

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

代码语言:javascript
复制
$ 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 的最佳实践”。(项目已集成)


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

代码语言:javascript
复制
git clone -b step01 https://github.com/57code/umi-test.git

在这个项目里:

直接 umi dev执行即可。

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

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

代码语言:javascript
复制
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)写一写:

代码语言:javascript
复制
.header {
    color: white;
  }
  .content {
    margin: 16px;
  }
  .box {
    padding: 24px;
    background: #fff;
    min-height: 500px;
  }
  .footer {
    text-align: center;
  }

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

代码语言:javascript
复制
// 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")。

代码语言:javascript
复制
<Header className={styles.header}>
        {/* 新增内容 */}
        // <Menu />
</Header>
动态菜单
代码语言:javascript
复制
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。

导航匹配路由

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

代码语言:javascript
复制
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的用法:

代码语言:javascript
复制
import {Exception} from 'ant-design-pro'
export default function() {
  return (
    <Exception type="404" backText="返回首页"></Exception>
  );
}

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

页面

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

代码语言:javascript
复制
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

代码语言:javascript
复制
// 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)。

代码语言:javascript
复制
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。

代码语言:javascript
复制
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)之类的都可以去掉了。

代码语言:javascript
复制
// 调用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框架报错。

代码语言:javascript
复制
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初始化时执行一次。

代码语言:javascript
复制
// 全局入口
import interceptor from './interceptor'

是的这样就可以了。

路由守卫

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

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

代码语言:javascript
复制
{
  path: "/me",
  component: "./me",
  Routes: ["./routes/PrivateRoute.js"]
},

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

代码语言:javascript
复制
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方法。

路由配置
代码语言:javascript
复制
// config.js
{
  path: "/questionBank",
  component: "./questionBank/_layout",
  routes: [
    { path: "/questionBank/", component: "./questionBank/index" },
    { path: "/questionBank/:id", component: "./questionBank/$id" }
  ]
},
mock接口

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

代码语言:javascript
复制
// 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状态变更等事宜。

代码语言:javascript
复制
// /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,

触发数据修改

代码语言:javascript
复制
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新的接口。

代码语言:javascript
复制
// 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:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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

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

代码语言:javascript
复制
// 判断是否存在数组中,有则返回索引值,没有则返回-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>
      // ...
    )
}

那么效果就基本实现了。


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 项目技术栈
  • 后台布局容器(layout/index.js)
    • 导航
      • 动态菜单
      • 导航匹配路由
  • 用户登录认证(又是登录)
    • 页面
      • Mock数据
        • model
          • 让login组件带上状态
            • 错误处理
              • 路由守卫
              • 题库(questionBank)
                • 路由配置
                  • mock接口
                    • models
                      • 页面
                        • 链接dva
                        • 标签筛选
                    相关产品与服务
                    登录保护
                    登录保护(LoginProtection,LP)针对网站和 App 的用户登录场景,实时检测是否存在盗号、撞库等恶意登录行为,帮助开发者发现异常登录,降低恶意用户登录给业务带来的风险。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档