8

通过React16.8构建新闻网站第五章:用户模块实现

 3 years ago
source link: https://zhuanlan.zhihu.com/p/250178609
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

通过React16.8构建新闻网站第五章:用户模块实现

北京奇观技术有限责任公司 软件开发工程师

专栏地址

实现效果

登录前:

登陆注册导航条变化

目录结构

新建文件User 为用户模块

User下的index.js为父组件 包括注册组件和登录组件还有登出组件

Login为登录组件

Logout为登出组件

Register为注册组件

User目录

1 User组件

User组件是一个弹窗 用户点击注册登录后弹出注册和登录的弹窗

import React, { useState, useEffect } from 'react';
import {Tabs, Modal} from 'antd';
//注册登录的模块的父组件 用于弹出注册登录模块
const User = props => {
    //调用父组件的关闭弹窗方法
    const handleCancel = ()=>{
        props.setModalVisible(false);
    }
    //这个组件通过父组件传递的参数来控制弹出关闭 父组件为Header组件
    return (
        <Modal 
            title="用户中心" visible={props.visible} onCancel={handleCancel} onOk={handleCancel} footer={null}>
            <Tabs type="card">
                <Tabs.TabPane tab='登录' key='1'>
                    我是登录组件
                </Tabs.TabPane>
                <Tabs.TabPane tab='注册' key='2'>
                    我是注册组件
                </Tabs.TabPane>
            </Tabs>
        </Modal>
    )
}
export default User;

2 NewsHeader组件

我们需要在头部组件点击注册登录,来控制用户组件的弹出

修改 NewsHeader组件(代码只展示了修改部分)

//引入导航
import Nav from './Nav';
const NewsHeader = ()=> {
    //注册登录的弹窗 默认不弹出
    const [modalVisable,setModalVisable] = useState(false);

    //点击切换 修改current
    const handleClick = e => {
        //点击注册登录的位置 直接弹窗 然后把那个链接设为高亮
        if(e.key == 'user'){
            setCurrent('user');
            //弹出弹窗
            setModalVisable(true);
        }
        setCurrent(e.key)
    };

    return(
        <User
            setModalVisible={setModalVisable} 
            visible={modalVisable}
        />   
    )
}

export default NewsHeader;

3 nav组件加入注册登录的导航

<Menu.Item key="user" icon={<AppstoreOutlined />}>
    注册/登录
</Menu.Item>
前三步实现的效果

4 登录组件

登录组件时用户组件的子组件是NewsHeader组件的孙子组件

这里我们涉及一个数据传递的概念 我们如何把NewsHeader组件的状态和方法传递到Login组件上

如果只是使用普通的props传递 我们需要把父组件的方法连续传递两层才能传递到孙子组件

所以这里我先介绍一下useContext这个Hook的使用,其实就是React16.8上下文的使用

在父组件中 我们定义上下文 把count和setcount传递下去

//使用这个Hook 还需要以前版本使用的createContext
import React,{useState,useContext,createContext} from 'react';
//创建一个上下文并且这个上下文还需要导出
export const Text = createContext();
import Item from './item';
export default function () {
    const [count,setCount] = useState(0);
    return({
    	<div>
    		<button onClick = {()}=>{setCount(count + 1)}>
    		+1
    		</button>
                {/* 被这个Provider包裹的全部子组件孙子组件都可以使用value传递的上下文参数*/}
    		<Text.Provider value = {{count,setCount}}>  
    			<Item />
    		</Text.Provider>
    	</div>
    })
}

子组件或是孙子组件

首先我们去拿到父组件暴露出来的上下文 然后通过useContext拿到上下文中传递的方法和变量就可以直接使用了

//子组件
import React,{useContext} from 'react';
import {Text} from './index';//需要把上下文也引入

export default function Item() {
    //然后就可以使用父组件中这个上下文provider的value变量和方法
    const  {count,setCount} = useContext(Text);
    return(
    	<div>
    		Item_count:{count}
    		<br />
    		<button onClick = {()}=>{setCount(count - 1)}>
            		01
    		</button>
    	</div>
    )
}

登录组件具体实现:

通过 Ant Design 的Form组件 登录成功调用上下文中的设置弹窗方法和保存用户数据方法

import React, {useContext, useState, useEffect } from 'react';
import {Text} from '../../BaseLayout/NewsHeader';
import { Form, Input, Button, Checkbox, message} from 'antd';
import {api,host} from '../../until';

//表单的布局 账号密码输入框的布局
const layout = {
    labelCol: {
        span: 6,
    },
    wrapperCol: {
        span: 16,
    },
};
//表单的布局 下面记住我和确定注册的布局
const tailLayout = {
    wrapperCol: {
        offset: 8,
        span: 16,
    },
};
  
const Login = props => {

    //使用父组件中这个上下文provider的value变量和方法
    //setModalVisable设置弹窗关闭 loginclick登录成功
    const  {setModalVisable,loginclick} = useContext(Text);

    const onFinish = values => {
        console.log('Success:', values);
        let {account,password} = values;
        //登录判断 数据库判单是否有这个账号密码 如果有返回这个用户的全部数据
        api({
            url:host +'newsLogin',
            args: {
                account,
                password,
            },
            callback: (res) => {
                if(res.code == '401'){
                    message.warn("登录失败,该账户不存在");
                    return 0;
                }else if(res.code == '402'){
                    message.warn("登录失败,密码错误");
                    return 0;
                }else{
                    message.success("登录成功");
                    let userLogin = {userName: res.msg[0].name, userId: res.msg[0].id,userAvatar:res.msg[0].avatar};
                    //调用上下文的方法
                    //登录成功保存用户数据
                    loginclick(userLogin);
                    //设置模态框消失
                    setModalVisable(false);
                }
            }
        });
    };
    
    const onFinishFailed = errorInfo => {
        console.log('Failed:', errorInfo);
    };
    
    return (
        <Form
            {...layout}
            name="basic"
            initialValues={{
                remember: true,
            }}
            onFinish={onFinish}
            onFinishFailed={onFinishFailed}
        >
            <Form.Item
                label="Account"
                name="account"
                rules={[
                    {
                        required: true,
                        message: 'Please input your account!',
                    },
                ]}
            >
                <Input />
            </Form.Item>

            <Form.Item
                label="Password"
                name="password"
                rules={[
                    {
                        required: true,
                        message: 'Please input your password!',
                    },
                ]}
            >
                <Input.Password />
            </Form.Item>

            <Form.Item {...tailLayout} name="remember" valuePropName="checked">
                <Checkbox>Remember me</Checkbox>
            </Form.Item>

            <Form.Item {...tailLayout}>
                <Button type="primary" htmlType="submit" style={{height:'40px'}}>
                    Submit
                </Button>
            </Form.Item>
        </Form>
    )
}
export default Login;

User组件修改

把Login组件引入

import Login from './Login/index'
<Tabs.TabPane tab='登录' key='1'>
    <Login />
</Tabs.TabPane>

NewsHeader组件修改

把上下文暴露出去 为Login组件提供方法

import React, { useState, useEffect ,createContext } from 'react';
//创建一个上下文并且这个上下文还需要导出
//使用useContextHook 因为我们的注册登录组件是header组件的孙子组件 
//如果想调用header组件定义的方法 需要层层嵌套所以这里使用上下文去传递方法
export const Text = createContext();

const NewsHeader = ()=> {
     //点击登录表单中的登录按钮,成功后保存用户信息
    const loginclick = (userLogin) => {
        localStorage.userName = userLogin.userName;
        localStorage.userId = userLogin.userId;
        localStorage.userAvatar = userLogin.userAvatar;
    }
    
    return(
        {/* 被这个Provider包裹的全部子组件孙子组件都可以使用value传递的上下文参数 */}
        {/* 传递一个设置弹窗关闭的方法 和一个登录成功保存用户数据的方法 */}
        <Text.Provider value = {{setModalVisable,loginclick}}> 
            {/* 登录注册的弹窗 */}
            <User
                visible = {modalVisable}
            />
        </Text.Provider>
    )
}

5 注册组件

注册组件具体实现

表单部分还是使用Ant Design的Form表单 头像的上传使用的Ant Design的Upload组件,

后端通过OSS对象存储把我上传的头像传入OSS服务器,并返回链接(这里不做讲解),直接看前端实现代码

Register

import React, {useContext, useState, useEffect } from 'react';
import {Text} from '../../BaseLayout/NewsHeader';
import { Form, Input, Button, message, Upload, Modal} from 'antd';
//antd v4升级后 拿不到以前的Icon了 换成了SmileOutlined
import { SmileOutlined } from '@ant-design/icons'
import {api,host} from '../../until';

//定义icon
const Icon = props => (
    <span>
        <SmileOutlined type={props.type} style={{ marginRight: 8 }} />
        {props.text}
    </span>
);

//表单的布局 账号密码输入框的布局
const layout = {
    labelCol: {
        span: 6,
    },
    wrapperCol: {
        span: 16,
    },
};

//表单的布局 确定注册的布局
const tailLayout = {
    wrapperCol: {
        offset: 8,
        span: 16,
    },
};

//上传unload的样式
const uploadButton = (
    <div>
        <Icon type="plus" />
        <div className="ant-upload-text">Upload</div>
    </div>
);

const Register = props => { 
    //使用父组件中这个上下文provider的value变量和方法
    //setModalVisable设置弹窗关闭 loginclick登录成功
    const {setModalVisable,loginclick} = useContext(Text);

    const [avatar,setAvatar] = useState('');//用户上传的头像
    const [previewVisible,setPreviewVisible] = useState(false);//上传头像的弹窗
    const [previewImage,setPreviewImage] = useState('');//预览头像的链接
    const [fileList,setFileList] = useState([]);//用户上传的图片列表 这里只让传一张

    //form表单点击确定成功的时候
    const onFinish = values => {
        console.log('Success:', values);
        //取出表单内的值
        let {account,name,passwordFirst,passwordSecond} = values;
        if(passwordFirst !== passwordSecond){
            message.warn('两次密码输入不一致');
            return 0;
        }
        //如果用户没设置头像提示他去设置头像
        if(!avatar){
            message.warn('请设置头像');
            return 0;
        }

        //请求注册接口
        api({
            url:host +'newsCreateUser',
            args: {
                name,
                account,
                password:passwordFirst,
                avatar
            },
            callback: (res) => {
                console.log(res);
                if(res.code == '400'){
                    message.warn("注册失败,该账户已存在");
                }else{
                    message.success("注册成功,并为您自动登录");
                    let id  = res.data[0].id;
                    let userLogin = {userName: name, userId: id,userAvatar:avatar};
                    //调用上下文的方法
                    //注册成功自动为用户登录
                    loginclick(userLogin);
                    //设置模态框消失
                    setModalVisable(false);
                }
            }
        });
        
    };
    //form表单点击确定失败的时候 (通过表单绑定的验证没有通过)
    const onFinishFailed = errorInfo => {
        console.log('Failed:', errorInfo);
    };

    //把图片转为base64 
    const getBase64 = (file)=>{
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.readAsDataURL(file);
            reader.onload = () => resolve(reader.result);
            reader.onerror = error => reject(error);
        });
    }

    //关闭上传图片的弹窗
    const handleCancel = ()=>{
        setPreviewVisible(false);
    }

    //预览图片
    const handlePreview = async file => {
        if (!file.url && !file.preview) {
          file.preview = await getBase64(file.originFileObj);
        }
        //修改状态
        setPreviewImage(file.url || file.preview);
        setPreviewVisible(true);
    };

    //上传图片
    const handleChange = ({ fileList }) => setFileList(fileList);
    
    /**
     * zyx
     * 2019.10.21
     * 上传文件之前的钩子,参数为上传的文件
     */
    const beforeUpload = (file) => {
        let formData = new FormData();
        formData.append('file', file);
        fetch('http://182.92.64.245/tp5/public/index.php/index/index/savaImgToOss', {
            method:'post',
            body: formData
        }).then(response => response.json())
        .catch(error => console.error('Error:', error))
        .then(response => {
            let img = response.msg;
            //设置头像地址
            setAvatar(img);
        })
    }
    return (
        <Form
            {...layout}
            name="basic"
            onFinish={onFinish}
            onFinishFailed={onFinishFailed}
        >
            <Form.Item
                label="Name"
                name="name"
                rules={[
                    {
                        required: true,
                        message: '请输入您的昵称',
                    },
                ]}
            >
                <Input />
            </Form.Item>

            <Form.Item
                label="Account"
                name="account"
                rules={[
                    {
                        required: true,
                        message: '请输入您的账号',
                    },
                ]}
            >
                <Input />
            </Form.Item>

            <Form.Item
                label="密码"
                name="passwordFirst"
                rules={[
                    {
                        required: true,
                        message: '请输入您的密码',
                    },
                ]}
            >
                <Input.Password />
            </Form.Item>

            <Form.Item
                label="确认密码"
                name="passwordSecond"
                rules={[
                    {
                        required: true,
                        message: '请再次输入您的密码',
                    },
                ]}
            >
                <Input.Password />
            </Form.Item>

            <Form.Item 
                label="头像"
            >
                <Upload
                    action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
                    listType="picture-card"
                    fileList={fileList}
                    onPreview={handlePreview}
                    onChange={handleChange}
                    beforeUpload = {beforeUpload}
                    >
                        {fileList.length >= 1 ? null : uploadButton}
                </Upload>
                <Modal visible={previewVisible} footer={null} onCancel={handleCancel}>
                    <img alt="example" style={{ width: '100%' }} src={previewImage} />
                </Modal>
            </Form.Item>

            <Form.Item {...tailLayout}>
                <Button type="primary" htmlType="submit" style={{height:'40px'}}>
                    Submit
                </Button>
            </Form.Item>
        </Form>
    )
}
export default Register;

User组件修改

把Register组件引入

import Register from './Register'
<Tabs.TabPane tab='注册' key='2'>
    <Register />
</Tabs.TabPane>

这里注册组件同时也调用父组件的上下文方法setModalVisable和loginclick

1是为了注册成功时关闭弹窗 2时为了注册成功后直接调用登录方法完成自动登录

6 完善登录方法,并绘制登出组件

先写一个登出组件

Layout

import React, {useContext,useState, useEffect } from 'react';
import './index.scss';
import {Text} from '../../BaseLayout/NewsHeader/index';
import { Button} from 'antd';

//注销组件
const Logout = props => {
    //引入父组件的上下文方法  
    //我们需要引入父组件上下文的变量userName和logoutClick方法来渲染这个组件
    const {userName,logoutClick} = useContext(Text);
    console.log(logoutClick);
    console.log(props.userName);
    return (
        <div className='logout'>
            <Button type='primary'>{userName}</Button>
              
            <Button type='ghost' onClick={logoutClick}>注销用户</Button>
        </div>
    );
}

export default Logout;

我们注册登录成功后 导航栏目渲染layout组件 而不渲染注册登录组件导航

首先我们需要去做登录登出的状态绑定

NewHeader index.js

我们需要一个状态 来控制导航栏的注销组件和登录登出组件的渲染

这里用userName 当localStorage.userName 存在时说明是登录状态 不存在是未登录状态

 //1是为了传递用户名 2是为了判断是否登录修改登录状态
    const [userName,setUserName] = useState(localStorage.userName)
    //点击登录表单中的登录按钮,成功后保存用户信息
    const loginclick = (userLogin) => {
        localStorage.userName = userLogin.userName;
        localStorage.userId = userLogin.userId;
        localStorage.userAvatar = userLogin.userAvatar;
        //修改登录状态为1
        setUserName(userLogin.userName);
    }
    //登出的方法 点击退出登录把全部登录信息删除
    const logoutClick = ()=>{
        localStorage.userName = '';
        localStorage.userId = '';
        localStorage.userAvatar = '';
        //修改登录状态为0
        setUserName('');
    }

还是这个文件 把userName 和 logoutClick 方法通过上下文传递给nav组件和lougout组件

<Text.Provider value = {{userName,logoutClick}}> 
    <Nav 
        current = {current}
        menuItemClick = {handleClick}
    />
</Text.Provider> 

Nav组件修改:

import React, { useContext,useState, useEffect } from 'react';
import {Text} from './index';
//建立导航
const Nav = props =>{
    const {userName} = useContext(Text);
    //定义icon
    const userShow = ()=>{
        
        if(userName){
            //说明已经登录
            return(
                <Menu.Item key="logout">
                    <Logout />
                </Menu.Item> 
            )
        }else{
            //说明没有登录
            return(
                <Menu.Item key="user" icon={<AppstoreOutlined />}>
                    注册/登录
                </Menu.Item>
            )
        }
    };
    return(
        <Menu mode="horizontal" selectedKeys={[props.current] }
            style={{background: '#f0f2f5'}}
            onClick={props.menuItemClick}
            >
            ...
            删除{<Menu.Item key="user" icon={<AppstoreOutlined />}>
                注册/登录
            </Menu.Item>}

            {userShow()}
        </Menu>
    );
};
export default Nav;

在这个模块我们大量使用useContext(Text);上下文,因为react的状态不是双向绑定的,我们改变子组件状态无法影响到父组件,并且在层级较深的情况下,使用props传递父组件的方法就显的很繁琐,所以这里我们使用useContext(Text)。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK