2 - 【 ReactJS 入门 】

1 前端开发的演变

到目前为止,前端的开发经历了四个阶段,目前处于第四个阶段。这四个阶段分别是:

阶段一:静态页面阶段

在第一个阶段中前端页面都是静态的,所有前端代码和前端数据都是后端生成的。前端只是纯粹的展示功能,js 脚本的作用只是增加一些特殊效果,比如那时很流行用脚本控制页面上飞来飞去的广告。

那时的网站开发,采用的是后端 MVC 模式。

  • Model(模型层):提供/保存数据
  • Controller(控制层):数据处理,实现业务逻辑
  • View(视图层):展示数据,提供用户界面

前端只是后端 MVC 的 V。

阶段二:ajax 阶段

2004年,AJAX 技术诞生,改变了前端开发。Gmail 和 Google 地图这样革命性的产品出现,使得开发者发现,前端 的作用不仅仅是展示页面,还可以管理数据并与用户互动。

就是从这个阶段开始,前端脚本开始变得复杂,不再仅仅是一些玩具性的功能。

阶段三:前端 MVC 阶段

2010年,第一个前端 MVC 框架 Backbone.js 诞生。它基本上是把 MVC 模式搬到了前端,但是只有 M (读写数据) 和 V(展示数据),没有 C(处理数据)。

有些框架提出了 MVVM 模式,用 View Model 代替 Controller。Model 拿到数据以后,View Model 将数据处理成视图层(View)需要的格式,在视图层展示出来。

阶段四:SPA 阶段 - 浏览器里面的应用程序

前端可以做到读写数据切换视图用户交互,这意味着,网页其实是一个应用程序,而不是信息的纯展示。这种单张网页的应用程序称为 SPA(single-page-application)。

2010年后,前端工程师从开发页面(切模板),逐渐变成了开发“前端应用”(跑在浏览器里面的应用程序)。

目前,最流行的前端框架 Vue、Angular、React 等等,都属于 SPA 开发框架。

2 ReactJS 简介

官网:https://reactjs.org/

在这里插入图片描述

官方一句很简单的话,道出了什么是 ReactJS,就是,一个用于构建用户界面的 JavaScript 框架,是 Facebook 开发的一款的 JS 框架。

ReactJS 把复杂的页面,拆分成一个个的组件,将这些组件一个个的拼装起来,就会呈现多样的页面。ReactJS 可以用于 MVC 架构,也可以用于 MVVM 架构,或者别的架构。

ReactJS 圈内的一些框架简介:

  • Flux
    • Flux 是 Facebook 用户建立客户端 Web 应用的前端架构, 它通过利用一个单向的数据流补充了 React 的组合视图组件,这更是一种模式而非框架。
  • Redux
    • Redux 是 JavaScript 状态容器,提供可预测化的状态管理。Redux 可以让 React 组件状态共享变得简单。
  • Ant Design of React
    • 阿里开源的基于 React 的企业级后台产品,其中集成了多种框架,包含了上面提到的 Flux、Redux。
    • Ant Design 提供了丰富的组件,包括: 按钮、表单、表格、布局、分页、树组件、日历等。

3 搭建环境

3.1 创建项目

我们依然选择使用 UmiJS 作为构建工具。

  • 创建工程:
    在这里插入图片描述
    在这里插入图片描述

    需要配置 JSX 语法以满足 React 语法
    在这里插入图片描述

  • 输入命令,进行初始化:

    tyarn init -y
    # 生成package.json
    

    在这里插入图片描述

  • 初始化完成:

    在这里插入图片描述

  • 在命令输入如下命令:

    tyarn add umi --dev 
    # 项目中添加umi的依赖
    

会生成下面两个文件:node_modulesyarn.lock

  1. 为项目引入依赖,在文件夹 node_modules
  2. 生成 yarn.lock (yarn.lock 是为了维护树关系,保证依赖间的相互关系,和包的下载路径。)

3.2 编写 HelloWorld 程序

第一步,在工程的根目录下创建 config 目录,在config目录下创建config.js 文件。

  • UmiJS 的约定中,config/config.js 将作为 UmiJS的全局配置文件
    在这里插入图片描述

umi 中,约定的目录结构如下:

在这里插入图片描述

  • config.js 文件中输入以下内容,以便后面使用:

    // 导出一个对象,暂时设置为空对象,后面再填充内容 
    export default {};
    

第二步,创建 HelloWorld.js 页面文件

  • umi 中,约定存放页面代码的文件夹是在 src/pages,可以通过 singular:false 来设置单数的命名方式,我们采用默认即可。

    在这里插入图片描述

    HelloWorld.js 文件中输入如下内容:

    export default () => {
        return <div>hello world</div>;
    }
    

    在这里,可以会比较奇怪,怎么可以在 js 文件中写 html 代码,其实,这是 react 自创的写法,叫 JSX,后面我们再细说。

第三步,启动服务查看页面效果

# 启动服务 
umi dev

在这里插入图片描述

默认会访问 src/pages 下面的 index.js

在这里插入图片描述

可以看到,通过 /HelloWorld 路径即可访问到刚刚写的 HelloWorld.js 文件。

在 umi 中,可以使用约定式的路由,在 pages 下面的 JS 文件都会按照文件名映射到一个路由,比如上面这个例子, 访问 /helloworld 会对应到 HelloWorld.js。 当然了,也可以自定义路由,具体的路由配置在后面讲解。

3.3 添加 umi-plugin-react 插件

umi-plugin-react 插件是 umi 官方基于 react 封装的插件,包含了13个常用的进阶功能。

具体可查看: https://umijs.org/zh/plugin/umi-plugin-react.html

# 添加插件
tyarn add umi-plugin-react --dev
# --dev作用
# 将依赖添加到 package.json 下面的 devDependencies 中

添加成功:

在这里插入图片描述

接下来,在 config.js 文件中引入该插件:

export default {
    plugins: [
        ['umi-plugin-react', {
             // 暂时不启用任何功能
        }] 
    ]
};

3.4 构建和部署

现在我们写的 js,必须通过 umi 先转码后才能正常的执行,那么我们最终要发布的项目是普通的 html、js、css,那么应该怎么操作呢?

其实,通过 umi 是可以进行转码生成文件的,具体操作如下:

umi build

在这里插入图片描述

在这里插入图片描述

可以看到,已经生成了 index.htmlumi.js 文件。我们打开 umi.js 文件看看。

在这里插入图片描述

首先,看到的是 umi.js 文件是一个已经压缩过的文件,然后搜索“hello world”,可以找到,我们刚刚写的代码已经被转码了。

至此,开发环境搭建完毕。

4 React 快速入门

4.1 JSX 语法

JSX 语法就是,可以在js文件中插入html片段,是 React 自创的一种语法。 JSX 语法会被 Babel 等转码工具进行转码,得到正常的js代码再执行。

使用 JSX 语法,需要 2 点注意:

  1. 所有的 html 标签必须是闭合的,如:

    正确的写法:<div>hello world</div>
    错误的写法:<div>hello world
    
    
  2. 在 JSX 语法中,只能有一个根标签,不能有多个。

    const div1 = <div>hello world</div> // 正确
    const div2 = <div>hello</div> <div>world</div> // 错误
    const div2 = 
      <div>
        <div>hello</div> <div>world</div>
      </div>// 正确
    
    

在 JSX 语法中,如果想要在 html 标签中插入 js 脚本,需要通过 {} 插入 js 脚本。

在这里插入图片描述

在这里插入图片描述

4.2 组件

组件是 React 中最重要也是最核心的概念,一个网页,可以被拆分成一个个的组件。

在 React 中,这样定义一个组件:

import React from 'react';// 第一步,导入React

class HelloWorld extends React.Component{ // 第二步,编写类并且继承 React.Component
    render() { // 第三步,重写render()方法,用于渲染页面
        return <div>hello world!</div> // JSX语法
    }
}

export default HelloWorld; // 第四步,导出该类

在这里插入图片描述

查看效果:

在这里插入图片描述

4.2.1 导入自定义组件

创建 Show.js 文件,用于测试导入组件:

import React from 'react';
import HelloWorld from './HelloWorld';// 导入HelloWorld组件

class Show extends React.Component{
    render() {
        return (
            <HelloWorld></HelloWorld>// 使用HelloWorld组件
        );
    }
}

export default Show;

测试:

在这里插入图片描述

4.2.2 组件参数

组件是可以传递参数的,有2种方式传递,分别是属性和标签包裹的内容传递,具体使用如下:

在这里插入图片描述

import React from 'react';
import HelloWorld from './HelloWorld';// 导入HelloWorld组件

class Show extends React.Component{
    render() {
        return (
            <HelloWorld name="张三">Show的内容</HelloWorld>// 使用HelloWorld组件
        );
    }
}

export default Show;

其中,name="张三"就是属性传递,"Show的内容"就是标签包裹的内容传递。 那么,在 HelloWord.js 组件中如何接收参数呢?
对应的也是2种方法:

  • 属性:this.props.name 接收;
  • 标签内容:this.props.children 接收;

使用如下:

在这里插入图片描述

import React from 'react';// 第一步,导入React

class HelloWorld extends React.Component{ // 第二步,编写类并且继承 React.Component
    render() { // 第三步,重写render()方法,用于渲染页面
        return <div>我是第一个ReactJS的组件,name = {this.props.name},内容 = {this.props.children}!</div> // JSX语法
    }
}

export default HelloWorld; // 第四步,导出该类

测试:

在这里插入图片描述

在这里插入图片描述

4.2.3 组件的状态

每一个组件都有一个状态,其保存在 this.state 中,当状态值发生变化时,React框架会自动调用render()方法,重新渲染页面

其中,要注意两点:

  • 一: this.state 值的设置要在构造参数中完成;
  • 二: 要修改 this.state 的值,需要调用 this.setState() 完成,不能直接对 this.state 进行修改;

下面通过一个案例进行演示,这个案例将实现:通过点击按钮,不断的更新 this.state,从而反应到页面中。

import React from 'react';

class List extends React.Component{

    constructor(props) { // 构造参数中必须要props参数
        super(props); // 调用父类的构造方法

        this.state = { // 初始化this.state
            dataList : [1,2,3],
            maxNum : 3
        };
    }

    render() {
        return (
            <div>
                <ul>
                    {
                        // 遍历值
                        this.state.dataList.map((value,index) => {
                            return <li key={index}>{value}</li>
                        })
                    }
                </ul>

                <button
                    onClick={()=>{ // 为按钮添加点击事件
                        let maxNum = this.state.maxNum + 1;
                        // 在原有的数组上面添加值 maxNum,生成新的数组
                        let list = [...this.state.dataList, maxNum];
                        this.setState({ // 更新状态值
                            dataList : list,
                            maxNum : maxNum
                        });
                    }}>
                    添加
                </button>
            </div>
        )
    }
}

export default List;

  • 初始状态:

    在这里插入图片描述

  • 当点击“添加”按钮:

    在这里插入图片描述

  • 过程分析:

    在这里插入图片描述

4.2.4 生命周期

组件的运行过程中,存在不同的阶段。React 为这些阶段提供了钩子方法,允许开发者自定义每个阶段自动执行的函数。这些方法统称为生命周期方法(lifecycle methods)。

在这里插入图片描述

生命周期示例:

import React from 'react'; // 第一步,导入React

class LifeCycle extends React.Component {

    constructor(props) {
        super(props);
        // 构造方法
        console.log("constructor()");
    }

    componentDidMount() {
        // 组件挂载后调用
        console.log("componentDidMount()");
    }

    componentWillUnmount() {
        // 在组件从 DOM 中移除之前立刻被调用。
        console.log("componentWillUnmount()");
    }

    componentDidUpdate() {
        // 在组件完成更新后立即调用。在初始化时不会被调用。
        console.log("componentDidUpdate()");
    }

    shouldComponentUpdate(nextProps, nextState){
        // 每当this.props或this.state有变化,在render方法执行之前,就会调用这个方法。
        // 该方法返回一个布尔值,表示是否应该继续执行render方法,即如果返回false,UI 就不会更新,默认返回true。
        // 组件挂载时,render方法的第一次执行,不会调用这个方法。
        console.log("shouldComponentUpdate()");
    }

    render() {
        return (
            <div>
                <h1>React Life Cycle!</h1>
            </div>
        );
    }
}

export default LifeCycle;

测试结果:

在这里插入图片描述

5 Model

5.1 分层

在这里插入图片描述

上图中,左侧是服务端代码的层次结构,由 ControllerServiceData Access 三层组 成服务端系统:

  • Controller 层负责与用户直接打交道,渲染页面、提供接口等,侧重于展示型逻辑。
  • Service 层负责处理业务逻辑,供 Controller 层调用。
  • Data Access 层顾名思义,负责与数据源对接,进行纯粹的数据读写,供 Service 层 调用。

上图的 右侧 是前端代码的结构,同样需要进行必要的分层:

  • Page 负责与用户直接打交道:渲染页面、接受用户的操作输入,侧重于展示型交互 性逻辑。
  • Model 负责处理业务逻辑,为 Page 做数据、状态的读写、变换、暂存等。
  • Service 负责与 HTTP 接口对接,进行纯粹的数据读写。

5.2 使用 DVA 进行数据分层管理

dva 是基于 reduxredux-sagareact-router 的轻量级前端框架。

官 网:https://dvajs.com/

在这里插入图片描述

首先,我们先将 dva 框架引入进来,由于 umidva 进行了整合,所以导入就变得非常简单了。

config.js 文件中进行配置:

export default {
    plugins: [
        ['umi-plugin-react', {
            dva: true // 开启dva功能
        }]
    ]
};

接下来,创建 model 文件,在 umi 中,约定在 src/models 文件夹中定义model,所以,在该文件夹下创建 ListData.js 文件:

在这里插入图片描述

编写内容:

export default {
    namespace: 'list', // 命名空间
    state: {
        data: [1, 2, 3],
        maxNum: 3
    }
}

在这里插入图片描述

下面对 List.js 进行改造

import React from 'react';
import { connect } from 'dva';// 引入dvaconst namespace = 'list';// 说明:第一个回调函数的作用:将page层和model层进行链接,返回model中的数据
// 并且,将返回的数据绑定到this.props
// state是整个项目的state,所以需要用命名空间来区分
const mapStateToProps = (state) => {
    const dataList = state[namespace].data;
    return {
        dataList
    };
};
​
@connect(mapStateToProps) // 接收数据
class List extends React.Component{render() {
        return (
            <div>
                <ul>
                    {
                        // 遍历值
                        this.props.dataList.map((value,index) => {
                            return <li key={index}>{value}</li>
                        })
                    }
                </ul>
                <button
                    onClick={()=>{ // 为按钮添加点击事件}}>
                    添加
                </button>
            </div>
        );
    }
}export default List;

测试

在这里插入图片描述

可以看到,效果是一样的。 流程说明:

  • umi 框架启动,会自动读取 models 目录下 model 文件,即 ListData.js 中的数据

  • @connect 修饰符的第一个参数,接收一个方法,该方法必须返回 {},将接收到 model 数据

  • 在全局的数据中,会有很多,所以需要通过 namespace 进行区分,所以通过 state[namespace] 进行获取数据

  • 拿到 model 数据中的 data,也就是 [1, 2, 3] 数据,进行包裹 {} 后返回

  • 返回的数据,将被封装到 this.props 中,所以通过 this.props.dataList 即可获取到 model 中的数据

刚刚只是将数据展现出来,如果点击按钮,需要修改 state 的值,怎么操作呢? 首先,在 model 中新增 reducers 方法,用于更新 state 中的数据:

export default {
    namespace: 'list', // 命名空间
    state: {
        data: [1, 2, 3],
        maxNum: 3
    },
    reducers : {
        addNewData(state){ // state是更新前的对象
            let maxNum = state.maxNum + 1;
            let list = [...state.data, maxNum];
            return { // 返回更新后的state对象
                data : list,
                maxNum : maxNum
            }
        }
    }
}

接下来修改 List.js 新增点击事件:

import React from 'react';
import { connect } from 'dva';// 引入const namespace = 'list';// 说明:第一个回调函数的作用:将page层和model层进行链接,返回model中的数据
// 并且,将返回的数据绑定到this.props
// state是整个项目的state,所以需要用命名空间来区分
const mapStateToProps = (state) => {
    const dataList = state[namespace].data;
    const maxNum = state[namespace].maxNum;
    return {
        dataList,
        maxNum
    };
};// 第二个函数作用:将定义的函数绑定到this.props中,调用model层中定义的函数
const mapDispatchToProps = (dispatch) => { // 定义方法,dispatch是内置函数
    return { // 返回的这个对象将绑定到this.props对象中
        addNewData: () => { // 定义方法
            dispatch({ // 通过调用dispatch()方法,调用model中reducers的方法
                type: namespace + "/addNewData" // 指定方法,格式:namespace/方法名
            });
        }
    }
}
​
@connect(mapStateToProps, mapDispatchToProps)// mapDispatchToProps:函数, 将方法映射到props中
class List extends React.Component{render() {
        return (
            <div>
                <ul>
                    {
                        // 遍历值
                        this.props.dataList.map((value,index) => {
                            return <li key={index}>{value}</li>
                        })
                    }
                </ul>
                <button
                    onClick={()=>{this.props.addNewData()}}>
                    添加
                </button>
            </div>
        );
    }
}export default List;


测试:

在这里插入图片描述

测试结果,和之前实现效果一样。 流程梳理如下:

在这里插入图片描述

5.3 在 model 中请求数据

前面我的数据是写死在 model 中的,实际开发中,更多的是需要异步加载数据,那么在 model 中如何异步加载数据呢? 首先,创建 src 下创建 util 目录,并且创建 request.js 文件,输入如下内容: (用于异步请求数据)

// import fetch from 'dva/fetch';
function checkStatus(response) {
    if (response.status >= 200 && response.status < 300) {
        return response;
    }
    
    const error = new Error(response.statusText);
    error.response = response;
    throw error;
}

/**
 * Requests a URL, returning a promise.
 *
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch"
 * @return {object}           An object containing either "data" or "err"
 */
export default async function request(url, options) {
    const response = await fetch(url, options);
    checkStatus(response);
    return await response.json();
}

在这里插入图片描述

  • 然后,在 modelListData.js新增请求方法:

    import request from '../util/request';// 引入request.js
    
    export default {
        namespace: 'list', // 命名空间
        state: {
            data: [1, 2, 3],
            maxNum: 3
        },
        reducers: {
            addNewData(state, result) { // result就是拿到的结果数据
                if(result.data){ // 判断result中的data是否存在,如果存在,说明是初始化数据,直接返回
                    return result.data;
                }
                let maxNum = state.maxNum + 1;
                let list = [...state.data, maxNum];
                return { // 更新状态值
                    data: list,
                    maxNum: maxNum
                }
            }
        },
        effects: { // 新增effects配置,用于异步加载数据
            *initData(params, sagaEffects) { // 定义异步方法(函数前面有*)
            const {call, put} = sagaEffects; // 获取到call、put方法
                const url = "/ds/list"; // 定义请求的url
                let data = yield call(request, url); // 执行请求
                yield put({ // 调用reducers中的方法
                    type : "addNewData", // 指定reducers中的方法addNewData
                    data : data // 传递ajax回来的数据
                });
            }
        }
    }
    
    

在这里插入图片描述

  • 改造页面逻辑:

    import React from 'react';
    import { connect } from 'dva';// 引入dva
    
    const namespace = 'list';
    
    // 说明:第一个回调函数的作用:将page层和model层进行链接,返回model中的数据
    // 并且,将返回的数据绑定到this.props
    // state是整个项目的state,所以需要用命名空间来区分
    const mapStateToProps = (state) => {
        const dataList = state[namespace].data;
        const maxNum = state[namespace].maxNum;
        return {
            dataList,
            maxNum
        };
    };
    
    // 第二个函数作用:将定义的函数绑定到this.props中,调用model层中定义的函数
    const mapDispatchToProps = (dispatch) => { // 定义方法,dispatch是内置函数
        return { // 返回的这个对象将绑定到this.props对象中
            addNewData: () => { // 定义方法
                dispatch({ // 通过调用dispatch()方法,调用model中reducers的方法
                    type: namespace + "/addNewData" // 指定方法,格式:namespace/方法名
                });
            },
            initData : () => { // 新增初始化方法的定义
                dispatch({
                    type: namespace + "/initData"
                });
            }
        }
    }
    
    @connect(mapStateToProps, mapDispatchToProps)// mapDispatchToProps:函数,将方法映射到props中
    class List extends React.Component{
    
        // 添加生命周期函数,调用上面定义的第二个函数,调用 initData 方法
        componentDidMount(){
            this.props.initData(); // 组件加载完后进行初始化操作
        }
    
        render() {
            return (
                <div>
                    <ul>
                        {
                            // 遍历值
                            this.props.dataList.map((value,index) => {
                                return <li key={index}>{value}</li>
                            })
                        }
                    </ul>
                    <button
                        onClick={()=>{this.props.addNewData()}}>
                        添加
                    </button>
                </div>
            );
        }
    }
    
    export default List;
    
    

在这里插入图片描述

  • 测试:

    在这里插入图片描述

    测试结果,发现会报错,原因是返回的数据不是json导致,解析出错。

  • 查看下请求:

    在这里插入图片描述

    可以看到,返回的是html代码,所以会导致出错。

5.4 mock 数据

umi 中支持对请求的模拟,由于我们现在没有真正的服务可以返回数据,所以才需要模拟。

在项目根目录下创建 mock 目录,然后创建 MockListData.js 文件,并且输入如下内容:

模拟一个 get 请求,路径为 /ds/list

export default {
    'get /ds/list': function (req, res) { // 模拟请求返回数据
        res.json({
            data: [1, 2, 3, 4],
            maxNum: 4
        }); 
    }
}

在这里插入图片描述

进行测试:

在这里插入图片描述

发现,可以正常返回数据了。

页面效果也正常了:

在这里插入图片描述

发布了675 篇原创文章 · 获赞 214 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/weixin_42112635/article/details/104821433
今日推荐